diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000000..76e9aa3c6d --- /dev/null +++ b/.gitattributes @@ -0,0 +1,15 @@ +# Set default behaviour, in case users don't have core.autocrlf set. +* text=auto + +# Explicitly declare text files we want to always be normalized and converted +# to native line endings on checkout. +*.java text +*.groovy text +*.scala text +*.clj text +*.txt text +*.md text + +# Denote all files that are truly binary and should not be modified. +*.png binary +*.jpg binary diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md new file mode 100644 index 0000000000..3412422e07 --- /dev/null +++ b/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,13 @@ +Thanks for using RxJava but before you post an issue, please consider the following points: + + - [ ] Please include the library version number, including the minor and patch version, in the issue text. In addition, if you'd include the major version in the title (such as `3.x`) that would be great. + + - [ ] If you think you found a bug, please include a code sample that reproduces the problem. Dumping a stacktrace is usually not enough for us. + + - [ ] RxJava has more than 150 operators, we recommend searching the [javadoc](http://reactivex.io/RxJava/3.x/javadoc/io/reactivex/Observable.html) for keywords of what you try to accomplish. + + - [ ] If you have a question/issue about a library/technology built on top of RxJava (such as Retrofit, RxNetty, etc.), please consider asking a question on StackOverflow first (then maybe on their issue list). + + - [ ] Questions like "how do I X with RxJava" are generally better suited for StackOverflow (where it may already have an answer). + + - [ ] Please avoid cross-posting questions on StackOverflow, this issue list, the Gitter room or the mailing list. \ No newline at end of file diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000000..885315565f --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,7 @@ +Thank you for contributing to RxJava. Before pressing the "Create Pull Request" button, please consider the following points: + + - [ ] Please give a description about what and why you are contributing, even if it's trivial. + + - [ ] Please include the issue list number(s) or other PR numbers in the description if you are contributing in response to those. + + - [ ] Please include a reasonable set of unit tests if you contribute new code or change an existing one. If you contribute an operator, (if applicable) please make sure you have tests for working with an `empty`, `just`, `range` of values as well as an `error` source, with and/or without backpressure and see if unsubscription/cancellation propagates correctly. diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000000..def5ba5d90 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,12 @@ +version: 2 +updates: + + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "daily" + + - package-ecosystem: "gradle" + directory: "/" + schedule: + interval: "daily" diff --git a/.github/workflows/gradle-wrapper-validation.yml b/.github/workflows/gradle-wrapper-validation.yml new file mode 100644 index 0000000000..44e5c1a692 --- /dev/null +++ b/.github/workflows/gradle-wrapper-validation.yml @@ -0,0 +1,13 @@ +name: "Validate Gradle Wrapper" +on: [push, pull_request] + +permissions: + contents: read + +jobs: + validation: + name: "Validation" + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: gradle/wrapper-validation-action@f9c9c575b8b21b6485636a91ffecd10e558c62f6 # v3.5.0 diff --git a/.github/workflows/gradle_branch.yml b/.github/workflows/gradle_branch.yml new file mode 100644 index 0000000000..650cbe7c05 --- /dev/null +++ b/.github/workflows/gradle_branch.yml @@ -0,0 +1,37 @@ +# This workflow will build a Java project with Gradle +# For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-gradle + +name: Branch + +on: + push: + branches-ignore: [ '3.x' ] + +permissions: + contents: read + +jobs: + build: + + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - name: Set up JDK 8 + uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1 + with: + distribution: 'zulu' + java-version: '8' + - name: Cache Gradle packages + uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 + with: + path: ~/.gradle/caches + key: ${{ runner.os }}-gradle-${{ secrets.CACHE_VERSION }}-${{ hashFiles('**/*.gradle') }} + restore-keys: ${{ runner.os }}-gradle-${{ secrets.CACHE_VERSION }} + - name: Grant execute permission for gradlew + run: chmod +x gradlew + - name: Build RxJava + run: ./gradlew build --stacktrace + - name: Upload to Codecov + uses: codecov/codecov-action@b9fd7d16f6d7d1b5d2bec1a2887e65ceed900238 # v4.6.0 + - name: Generate Javadoc + run: ./gradlew javadoc --stacktrace diff --git a/.github/workflows/gradle_jdk11.yml b/.github/workflows/gradle_jdk11.yml new file mode 100644 index 0000000000..33c781fca9 --- /dev/null +++ b/.github/workflows/gradle_jdk11.yml @@ -0,0 +1,39 @@ +# This workflow will build a Java project with Gradle +# For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-gradle + +name: JDK 11 + +on: + push: + branches: [ 3.x ] + pull_request: + branches: [ 3.x ] + +permissions: + contents: read + +jobs: + build: + + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - name: Set up JDK 11 + uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1 + with: + distribution: 'zulu' + java-version: '11' + - name: Cache Gradle packages + uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 + with: + path: ~/.gradle/caches + key: ${{ runner.os }}-gradle-1-${{ hashFiles('**/*.gradle') }} + restore-keys: ${{ runner.os }}-gradle-1- + - name: Grant execute permission for gradlew + run: chmod +x gradlew + - name: Verify generated module-info + run: ./gradlew -PjavaCompatibility=9 jar + - name: Build RxJava + run: ./gradlew build --stacktrace + - name: Generate Javadoc + run: ./gradlew javadoc --stacktrace diff --git a/.github/workflows/gradle_pr.yml b/.github/workflows/gradle_pr.yml new file mode 100644 index 0000000000..712139379f --- /dev/null +++ b/.github/workflows/gradle_pr.yml @@ -0,0 +1,37 @@ +# This workflow will build a Java project with Gradle +# For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-gradle + +name: Pull Request + +on: + pull_request: + branches: [ 3.x ] + +permissions: + contents: read + +jobs: + build: + + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - name: Set up JDK 8 + uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1 + with: + distribution: 'zulu' + java-version: '8' + - name: Cache Gradle packages + uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 + with: + path: ~/.gradle/caches + key: ${{ runner.os }}-gradle-1-${{ hashFiles('**/*.gradle') }} + restore-keys: ${{ runner.os }}-gradle-1- + - name: Grant execute permission for gradlew + run: chmod +x gradlew + - name: Build RxJava + run: ./gradlew build --stacktrace + - name: Upload to Codecov + uses: codecov/codecov-action@b9fd7d16f6d7d1b5d2bec1a2887e65ceed900238 # v4.6.0 + - name: Generate Javadoc + run: ./gradlew javadoc --stacktrace diff --git a/.github/workflows/gradle_release.yml b/.github/workflows/gradle_release.yml new file mode 100644 index 0000000000..a341c27ffc --- /dev/null +++ b/.github/workflows/gradle_release.yml @@ -0,0 +1,68 @@ +# This workflow will build a Java project with Gradle +# For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-gradle + +name: Release + +on: + release: + types: [ released, prereleased ] + branches: [ '3.x' ] + tags: + - 'v3.*.*' + +permissions: + contents: read + +jobs: + build: + + runs-on: ubuntu-latest + permissions: + contents: write + env: + CI_BUILD_NUMBER: ${{ github.run_number }} + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - name: Set up JDK 8 + uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1 + with: + distribution: 'zulu' + java-version: '8' + - name: Cache Gradle packages + uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 + with: + path: ~/.gradle/caches + key: ${{ runner.os }}-gradle-${{ secrets.CACHE_VERSION }}-${{ hashFiles('**/*.gradle') }} + restore-keys: ${{ runner.os }}-gradle-${{ secrets.CACHE_VERSION }} + - name: Grant execute permission for gradlew + run: chmod +x gradlew + - name: Grant execute permission for push + run: chmod +x push_javadoc.sh + - name: Extract version tag + run: echo "BUILD_TAG=${GITHUB_REF:10}" >> $GITHUB_ENV + - name: Build RxJava + run: ./gradlew build --stacktrace --no-daemon + - name: Upload to Codecov + uses: codecov/codecov-action@b9fd7d16f6d7d1b5d2bec1a2887e65ceed900238 # v4.6.0 + - name: Upload release + run: ./gradlew -PreleaseMode=full publish --no-daemon --no-parallel --stacktrace + env: + # Define secrets at https://github.com/ReactiveX/RxJava/settings/secrets/actions + # ------------------------------------------------------------------------------ + ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.SONATYPE_USER }} + ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.SONATYPE_PASSWORD }} + ORG_GRADLE_PROJECT_SIGNING_PRIVATE_KEY: ${{ secrets.SIGNING_PRIVATE_KEY }} + ORG_GRADLE_PROJECT_SIGNING_PASSWORD: ${{ secrets.SIGNING_PASSWORD }} + - name: Publish release + run: ./gradlew -PreleaseMode=full closeAndReleaseRepository --no-daemon --no-parallel --stacktrace + env: + # Define secrets at https://github.com/ReactiveX/RxJava/settings/secrets/actions + # ------------------------------------------------------------------------------ + ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.SONATYPE_USER }} + ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.SONATYPE_PASSWORD }} + - name: Push Javadoc + run: ./push_javadoc.sh + env: + # Define secrets at https://github.com/ReactiveX/RxJava/settings/secrets/actions + # ------------------------------------------------------------------------------ + JAVADOCS_TOKEN: ${{ secrets.JAVADOCS_TOKEN }} diff --git a/.github/workflows/gradle_snapshot.yml b/.github/workflows/gradle_snapshot.yml new file mode 100644 index 0000000000..5a66712f73 --- /dev/null +++ b/.github/workflows/gradle_snapshot.yml @@ -0,0 +1,56 @@ +# This workflow will build a Java project with Gradle +# For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-gradle + +name: Snapshot + +on: + push: + branches: [ '3.x' ] + +permissions: + contents: read + +jobs: + build: + + runs-on: ubuntu-latest + if: github.repository == 'ReactiveX/RxJava' + permissions: + contents: write + env: + # ------------------------------------------------------------------------------ + CI_BUILD_NUMBER: ${{ github.run_number }} + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - name: Set up JDK 8 + uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1 + with: + distribution: 'zulu' + java-version: '8' + - name: Cache Gradle packages + uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 + with: + path: ~/.gradle/caches + key: ${{ runner.os }}-gradle-${{ secrets.CACHE_VERSION }}-${{ hashFiles('**/*.gradle') }} + restore-keys: ${{ runner.os }}-gradle-${{ secrets.CACHE_VERSION }} + - name: Grant execute permission for gradlew + run: chmod +x gradlew + - name: Grant execute permission for push + run: chmod +x push_javadoc.sh + - name: Build RxJava + run: ./gradlew build --stacktrace --no-daemon + - name: Upload Snapshot + run: ./gradlew -PreleaseMode=branch publish --no-daemon --no-parallel --stacktrace + env: + # Define secrets at https://github.com/ReactiveX/RxJava/settings/secrets/actions + # ------------------------------------------------------------------------------ + ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.SONATYPE_USER }} + ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.SONATYPE_PASSWORD }} + - name: Upload to Codecov + uses: codecov/codecov-action@b9fd7d16f6d7d1b5d2bec1a2887e65ceed900238 # v4.6.0 + - name: Push Javadoc + run: ./push_javadoc.sh + # Define secrets at https://github.com/ReactiveX/RxJava/settings/secrets/actions + # ------------------------------------------------------------------------------ + env: + JAVADOCS_TOKEN: ${{ secrets.JAVADOCS_TOKEN }} diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml new file mode 100644 index 0000000000..06bb057ff7 --- /dev/null +++ b/.github/workflows/scorecard.yml @@ -0,0 +1,59 @@ +name: Scorecard supply-chain security +on: + # For Branch-Protection check. Only the default branch is supported. See + # https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection + branch_protection_rule: + # To guarantee Maintained check is occasionally updated. See + # https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained + schedule: + - cron: '43 12 * * 4' + push: + branches: [ "3.x" ] + +permissions: read-all + +jobs: + analysis: + name: Scorecard analysis + runs-on: ubuntu-latest + permissions: + # Needed to upload the results to code-scanning dashboard. + security-events: write + # Needed to publish results and get a badge (see publish_results below). + id-token: write + + steps: + - name: "Checkout code" + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + persist-credentials: false + + - name: "Run analysis" + uses: ossf/scorecard-action@f49aabe0b5af0936a0987cfb85d86b75731b0186 # v2.4.1 + with: + results_file: results.sarif + results_format: sarif + # (Optional) "write" PAT token. Uncomment the `repo_token` line below if + # you want to enable the Branch-Protection check on a *public* repository + # To create the PAT, follow the steps in https://github.com/ossf/scorecard-action#authentication-with-fine-grained-pat-optional. + # repo_token: ${{ secrets.SCORECARD_TOKEN }} + + # - Publish results to OpenSSF REST API for easy access by consumers + # - Allows the repository to include the Scorecard badge. + # - See https://github.com/ossf/scorecard-action#publishing-results. + publish_results: true + + # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF + # format to the repository Actions tab. + - name: "Upload artifact" + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: SARIF file + path: results.sarif + retention-days: 5 + + # Upload the results to GitHub's code scanning dashboard. + - name: "Upload to code-scanning" + uses: github/codeql-action/upload-sarif@60168efe1c415ce0f5521ea06d5c2062adbeed1b # v3.28.17 + with: + sarif_file: results.sarif diff --git a/.gitignore b/.gitignore index f69630c23c..b60171cf2d 100644 --- a/.gitignore +++ b/.gitignore @@ -39,6 +39,7 @@ Thumbs.db # Gradle Files # ################ .gradle +.gradletasknamecache .m2 # Build output directies @@ -62,6 +63,20 @@ bin/ # NetBeans specific files/directories .nbattrs +/.nb-gradle/profiles/private/ +.nb-gradle-properties # Scala build *.cache +/.nb-gradle/private/ + +# PMD files +.pmd +.ruleset +test-output/ + +# Checkstyle local config +.checkstyle + +# Some editor's config +.editorconfig diff --git a/.gitpod.yml b/.gitpod.yml new file mode 100644 index 0000000000..954870bba6 --- /dev/null +++ b/.gitpod.yml @@ -0,0 +1,139 @@ +## Learn more about this file at 'https://www.gitpod.io/docs/references/gitpod-yml' +## +## This '.gitpod.yml' file when placed at the root of a project instructs +## Gitpod how to prepare & build the project, start development environments +## and configure continuous prebuilds. Prebuilds when enabled builds a project +## like a CI server so you can start coding right away - no more waiting for +## dependencies to download and builds to finish when reviewing pull-requests +## or hacking on something new. +## +## With Gitpod you can develop software from any device (even iPads) via +## desktop or browser based versions of VS Code or any JetBrains IDE and +## customise it to your individual needs - from themes to extensions, you +## have full control. +## +## The easiest way to try out Gitpod is install the browser extenion: +## 'https://www.gitpod.io/docs/browser-extension' or by prefixing +## 'https://gitpod.io/#' to the source control URL of any project. +## +## For example: 'https://gitpod.io/#https://github.com/gitpod-io/gitpod' + + +## The 'image' section defines which Docker image Gitpod should use. +## By default, Gitpod uses a standard Docker Image called 'workspace-full' +## which can be found at 'https://github.com/gitpod-io/workspace-images' +## +## Workspaces started based on this default image come pre-installed with +## Docker, Go, Java, Node.js, C/C++, Python, Ruby, Rust, PHP as well as +## tools such as Homebrew, Tailscale, Nginx and several more. +## +## If this image does not include the tools needed for your project then +## a public Docker image or your own Docker file can be configured. +## +## Learn more about images at 'https://www.gitpod.io/docs/config-docker' + +#image: node:buster # use 'https://hub.docker.com/_/node' +# +#image: # leave image undefined if using a Dockerfile +# file: .gitpod.Dockerfile # relative path to the Dockerfile from the +# # root of the project + +## The 'tasks' section defines how Gitpod prepares and builds this project +## or how Gitpod can start development servers. With Gitpod, there are three +## types of tasks: +## +## - before: Use this for tasks that need to run before init and before command. +## - init: Use this to configure prebuilds of heavy-lifting tasks such as +## downloading dependencies or compiling source code. +## - command: Use this to start your database or application when the workspace starts. +## +## Learn more about these tasks at 'https://www.gitpod.io/docs/config-start-tasks' + +#tasks: +# - before: | +# # commands to execute... +# +# - init: | +# # sudo apt-get install python3 # can be used to install operating system +# # dependencies but these are not kept after the +# # prebuild completes thus Gitpod recommends moving +# # operating system dependency installation steps +# # to a custom Dockerfile to make prebuilds faster +# # and to keep your codebase DRY. +# # 'https://www.gitpod.io/docs/config-docker' +# +# # pip install -r requirements.txt # install codebase dependencies +# # cmake # precompile codebase +# +# - name: Web Server +# openMode: split-left +# env: +# WEBSERVER_PORT: 8080 +# command: | +# python3 -m http.server $WEBSERVER_PORT +# +# - name: Web Browser +# openMode: split-right +# env: +# WEBSERVER_PORT: 8080 +# command: | +# gp await-port $WEBSERVER_PORT +# lynx `gp url` + +tasks: + - command: ./gradlew build + +## The 'ports' section defines various ports your may listen on are +## configured in Gitpod on an authenticated URL. By default, all ports +## are in private visibility state. +## +## Learn more about ports at 'https://www.gitpod.io/docs/config-ports' + +#ports: +# - port: 8080 # alternatively configure entire ranges via '8080-8090' +# visibility: private # either 'public' or 'private' (default) +# onOpen: open-browser # either 'open-browser', 'open-preview' or 'ignore' + + +## The 'vscode' section defines a list of Visual Studio Code extensions from +## the OpenVSX.org registry to be installed upon workspace startup. OpenVSX +## is an open alternative to the proprietary Visual Studio Code Marketplace +## and extensions can be added by sending a pull-request with the extension +## identifier to https://github.com/open-vsx/publish-extensions +## +## The identifier of an extension is always ${publisher}.${name}. +## +## For example: 'vscodevim.vim' +## +## Learn more at 'https://www.gitpod.io/docs/ides-and-editors/vscode' + +#vscode: +# extensions: +# - vscodevim.vim +# - esbenp.prettier-vscode@9.5.0 +# - https://example.com/abc/releases/extension-0.26.0.vsix + + +## The 'github' section defines configuration of continuous prebuilds +## for GitHub repositories when the GitHub application +## 'https://github.com/apps/gitpod-io' is installed in GitHub and granted +## permissions to access the repository. +## +## Learn more at 'https://www.gitpod.io/docs/prebuilds' + +github: + prebuilds: + # enable for the default branch + master: true + # enable for all branches in this repo + branches: true + # enable for pull requests coming from this repo + pullRequests: true + # enable for pull requests coming from forks + pullRequestsFromForks: true + # add a check to pull requests + addCheck: true + # add a "Review in Gitpod" button as a comment to pull requests + addComment: false + # add a "Review in Gitpod" button to the pull request's description + addBadge: true diff --git a/CHANGES.md b/CHANGES.md deleted file mode 100644 index ab7c27cb49..0000000000 --- a/CHANGES.md +++ /dev/null @@ -1,13 +0,0 @@ -# RxJava Releases # - -### Version 0.5.1 ([Maven Central](http://search.maven.org/#search%7Cga%7C1%7Cg%3A%22com.netflix.rxjava%22%20AND%20v%3A%220.5.1%22)) ### - -* variety of code cleanup commits -* [Pull 132](https://github.com/Netflix/Hystrix/pull/132) Broke rxjava-examples module into each language-adaptor module -* [Issue 118](https://github.com/Netflix/Hystrix/issues/118) & [Issue 119](https://github.com/Netflix/Hystrix/issues/119) Cleaned up Javadocs still referencing internal Netflix paths -* Javadoc and README changes - -### Version 0.5.0 ([Maven Central](http://search.maven.org/#search%7Cga%7C1%7Cg%3A%22com.netflix.rxjava%22%20AND%20v%3A%220.5.0%22)) ### - -* Initial open source release -* See [Netflix Tech Blog](http://techblog.netflix.com/2013/02/rxjava-netflix-api.html) for introduction diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 082524fc0c..98ae7225f3 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,31 +1,28 @@ -# Contributing to RxJava +# Contributing to RxJava 3.x -If you would like to contribute code you can do so through GitHub by forking the repository and sending a pull request (on a branch other than `master` or `gh-pages`). +If you would like to contribute code you can do so through GitHub by forking the repository and sending a pull request targeting the branch `3.x`. When submitting code, please make every effort to follow existing conventions and style in order to keep the code as readable as possible. ## License -By contributing your code, you agree to license your contribution under the terms of the APLv2: https://github.com/Netflix/RxJava/blob/master/LICENSE +By contributing your code, you agree to license your contribution under the terms of the APLv2: https://github.com/ReactiveX/RxJava/blob/3.x/LICENSE All files are released with the Apache 2.0 license. If you are adding a new file it should have a header like this: ``` -/** - * Copyright 2013 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. */ ``` diff --git a/COPYRIGHT b/COPYRIGHT new file mode 100644 index 0000000000..5d2c6e53f3 --- /dev/null +++ b/COPYRIGHT @@ -0,0 +1,13 @@ +Copyright (c) 2016-present, RxJava Contributors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. \ No newline at end of file diff --git a/DESIGN.md b/DESIGN.md new file mode 100644 index 0000000000..a8410d492e --- /dev/null +++ b/DESIGN.md @@ -0,0 +1,598 @@ +## RxJava v3 Design + +This document explains the terminology, principles, contracts, and other aspects of the design of RxJava v3. +Its intended audience is the implementers of the library. + +### Terminology & Definitions + +##### Interactive + +Producer obeys consumer-driven flow control. +Consumer manages capacity by requesting data. + + +##### Reactive + +Producer is in charge. Consumer has to do whatever it needs to keep up. + + +##### Hot + +When used to refer to a data source (such as an `Observable`), it means it does not have side-effects when subscribed to. + +For example, an `Observable` of mouse events. Subscribing to that `Observable` does not cause the mouse events, but starts receiving them. + +(Note: Yes, there are *some* side-effects of adding a listener, but they are inconsequential as far as the 'hot' usage is concerned). + + +##### Cold + +When used to refer to a data source (such as an `Observable`), it means it has side-effects when subscribed to. + +For example, an `Observable` of data from a remote API (such as an RPC call). Each time that `Observable` is subscribed to causes a new network call to occur. + + +##### Reactive/Push + +Producer is in charge. Consumer has to do whatever it needs to keep up. + +Examples: + +- `Observable` (RxJS, Rx.Net, RxJava v1.x without backpressure, RxJava v2). +- Callbacks (the producer calls the function at its convenience). +- IRQ, mouse events, IO interrupts. +- 3.x `Flowable` (with `request(n)` credit always granted faster or in larger quantity than producer). +- Reactive Streams `Publisher` (with `request(n)` credit always granted faster or in larger quantity than producer). +- Java 9 `Flow.Publisher` (with `request(n)` credit always granted faster than or in larger quantity than producer). + + +##### Synchronous Interactive/Pull + +Consumer is in charge. Producer has to do whatever it needs to keep up. + +Examples: + +- `Iterable`. +- 3.x/1.x `Observable` (without concurrency, producer and consumer on the same thread). +- 3.x `Flowable` (without concurrency, producer and consumer on the same thread). +- Reactive Streams `Publisher` (without concurrency, producer and consumer on the same thread). +- Java 9 `Flow.Publisher` (without concurrency, producer and consumer on the same thread). + + +##### Async Pull (Async Interactive) + +Consumer requests data when it wishes, and the data is then pushed when the producer wishes to. + +Examples: + +- `Future` & `Promise`. +- `Single` (lazy `Future`). +- 3.x `Flowable`. +- Reactive Streams `Publisher`. +- Java 9 `Flow.Publisher`. +- 1.x `Observable` (with backpressure). +- `AsyncEnumerable`/`AsyncIterable`. + +There is an overhead (performance and mental) for achieving this, which is why we also have the 3.x `Observable` without backpressure. + + +##### Flow Control + +Flow control is any mitigation strategy that a consumer applies to reduce the flow of data. + +Examples: + +- Controlling the production of data, such as with `Iterator.next` or `Subscription.request(n)`. +- Preventing the delivery of data, such as buffer, drop, sample/throttle, and debounce. + + +##### Eager + +Containing object immediately start work when it is created. + +Examples: + +- A `Future` once created has work being performed and represents the eventual value of that work. It can not be deferred once created. + + +##### Lazy + +Containing object does nothing until it is subscribed to or otherwise started. + +Examples: + +- `Observable.create` does not start any work until `Observable.subscribe` starts the work. + + +### RxJava & Related Types + +##### Observable + +Stream that supports async and synchronous push. It does *not* support interactive flow control (`request(n)`). + +Usable for: + +- Sync or async. +- Push. +- 0, 1, many or infinite items. + +Flow control support: + +- Buffering, sampling, throttling, windowing, dropping, etc. +- Temporal and count-based strategies. + +*Type Signature* + +```java +class Observable { + void subscribe(Observer observer); + + interface Observer { + void onNext(T t); + void onError(Throwable t); + void onComplete(); + void onSubscribe(Disposable d); + } +} +``` + +The rule for using this type signature is: + +> onSubscribe onNext* (onError | onComplete)? + + +##### Flowable + +Stream that supports async and synchronous push and pull. It supports interactive flow control (`request(n)`). + +Usable for: + +- Pull sources. +- Push Observables with backpressure strategy (i.e. `Observable.toFlowable(onBackpressureStrategy)`). +- Sync or async. +- 0, 1, many or infinite items. + +Flow control support: + +- Buffering, sampling, throttling, windowing, dropping, etc. +- Temporal and count-based strategies. +- `request(n)` consumer demand signal: + - For pull-based sources, this allows batched "async pull". + - For push-based sources, this allows backpressure signals to conditionally apply strategies (i.e. drop, first, buffer, sample, fail, etc.). + +You get a `Flowable` from: + +- Converting a Observable with a backpressure strategy. +- Create from sync/async `onSubscribe` API (which participate in backpressure semantics). + +*Type Signature* + +```java +class Flowable implements Flow.Publisher, io.reactivestreams.Publisher { + void subscribe(Subscriber subscriber); + + interface Subscription implements Flow.Subscription, io.reactivestreams.Subscription { + void cancel(); + void request(long n); + } +} +``` + +*NOTE: To support `Flow.Publisher` in Java 9+ without breaking Java 7+ compatibility, we want to use the [multi-release jar file support](http://openjdk.java.net/jeps/238).* + +The rule for using this type signature is: + +> onSubscribe onNext* (onError | onComplete)? + + +##### Single + +Lazy representation of a single response (lazy equivalent of `Future`/`Promise`). + +Usable for: + +- Pull sources. +- Push sources being windowed or flow controlled (such as `window(1)` or `take(1)`). +- Sync or async. +- 1 item. + +Flow control: + +- Not applicable (don't subscribe if the single response is not wanted). + +*Type Signature* + +```java +class Single { + void subscribe(Single.Subscriber subscriber); +} + +interface SingleSubscriber { + void onSuccess(T t); + void onError(Throwable t); + void onSubscribe(Disposable d); +} +``` + +> onSubscribe (onError | onSuccess)? + + +##### Completable + +Lazy representation of a unit of work that can complete or fail. + +- Semantic equivalent of `Observable.empty().doOnSubscribe()`. +- Alternative for scenarios often represented with types such as `Single` or `Observable`. + +Usable for: + +- Sync or async. +- 0 items. + +*Type Signature* + +```java +class Completable { + void subscribe(Completable.Subscriber subscriber); +} + +interface CompletableSubscriber { + void onComplete(); + void onError(Throwable t); + void onSubscribe(Disposable d); +} +``` + +> onSubscribe (onError | onComplete)? + +##### Observer + +Reactive consumer of events (without consumer-driven flow control). Involved in subscription lifecycle to allow unsubscription. + +##### Publisher + +Interactive producer of events (with flow control). Implemented by `Flowable`. + +[Reactive Streams producer](https://github.com/reactive-streams/reactive-streams-jvm/blob/v1.0.0/README.md#1-publisher-code) of data. + +##### Subscriber + +Interactive consumer of events (with consumer-driven flow control). Involved in subscription lifecycle to allow unsubscription. + +[Reactive Streams consumer](https://github.com/reactive-streams/reactive-streams-jvm/blob/v1.0.0/README.md#2-subscriber-code) of data. + +##### Subscription + +[Reactive Streams state](https://github.com/reactive-streams/reactive-streams-jvm/blob/v1.0.0/README.md#3-subscription-code) of subscription supporting flow control and cancellation. + +`Disposable` is a similar type used for lifecycle management on the `Observable` type without interactive flow control. + +##### Processor + +[Reactive Streams operator](https://github.com/reactive-streams/reactive-streams-jvm/blob/v1.0.0/README.md#4processor-code) for defining behavior between `Publisher` and `Subscriber`. It must obey the contracts of `Publisher` and `Subscriber`, meaning it is sequential, serialized, and must obey `request(n)` flow control. + +##### Subject + +A "hot", push-based data source that allows a producer to emit events to it and consumers to subscribe to events in a multicast manner. It is "hot" because consumers subscribing to it does not cause side-effects, or affect the data flow in any way. It is push-based and reactive because the producer is fully in charge. + +A `Subject` is used to decouple unsubscription. Termination is fully in the control of the producer. `onError` and `onComplete` are still terminal events. +`Subject`s are stateful and retain their terminal state (for replaying to all/future subscribers). + +Relation to Reactive Streams: + +- It can not implement Reactive Streams `Publisher` unless it is created with a default consumer-driven flow control strategy. +- It can not implement `Processor` since a `Processor` must compose `request(n)` which can not be done with multicasting or push. + +Here is an approach to converting from a `Subject` to Reactive Streams types by adding a default flow control strategy: + +```java +Subject s = PublishSubject.create(); +// convert to Publisher with backpressure strategy +Publisher p = s.toPublisher(onBackpressureStrategy); + +// now the request(n) semantics are handled by default +p.subscribe(subscriber1); +p.subscribe(subscriber2); +``` + +In this example, `subscriber1` and `subscriber2` can consume at different rates, `request(n)` will propagate to the provided `onBackpressureStrategy`, not the original `Subject` which can't propagate `request(n)` upstream. + + +##### Disposable + +A type representing work or resource that can be cancelled or disposed. + +Examples: + +- An `Observable.subscribe` passes a `Disposable` to the `Observable.onSubscribe` to allow the `Observer` to dispose of the subscription. +- A `Scheduler` returns a `Disposable` that you use for disposing of the `Scheduler`. + +`Subscription` is a similar type used for lifecycle management on the `Flowable` type with interactive flow control. + +##### Operator + +An operator follows a specific lifecycle (union of the producer/consumer contract). + +- It must propagate the `subscribe` event upstream (to the producer). +- It must obey the RxJava contract (serialize all events, `onError`/`onComplete` are terminal). +- If it has resources to cleanup it is responsible for watching `onError`, `onComplete`, and `cancel/dispose`, and doing the necessary cleanup. +- It must propagate the `cancel/dispose` upstream. + +In the addition of the previous rules, an operator for `Flowable`: + +- It must propagate/negotiate the `request(n)` event. + + +### Creation + +Unlike RxJava 1.x, 3.x base classes are to be abstract, stateless and generally no longer wrap an `onSubscribe` callback - this saves allocation in assembly time without limiting the expressiveness. Operator methods and standard factories still live as final on the base classes. + +Instead of the indirection of an `onSubscribe` and `lift`, operators are to be implemented by extending the base classes. For example, the `map` +operator will look like this: + +```java +public final class FlowableMap extends Flowable { + + final Flowable source; + + final Function mapper; + + public FlowableMap(Flowable source, Function mapper) { + this.source = source; + this.mapper = mapper; + } + + @Override + protected void subscribeActual(Subscriber subscriber) { + source.subscribe(new FlowableMapSubscriber(subscriber, mapper)); + } + + static final class FlowableMapSubscriber implements Subscriber, Subscription { + // ... + } +} +``` + +Since Java still doesn't have extension methods, "adding" more operators can only happen through helper methods such as `lift(C -> C)` and `compose(R -> P)` where `C` is the default consumer type (i.e. `rs.Subscriber`), `R` is the base type (i.e. `Flowable`) and `P` is the base interface (i.e. `rs.Publisher`). As before, the library itself may gain or lose standard operators and/or overloads through the same community process. + +In concert, `create(onSubscribe)` will not be available; standard operators extend the base types directly. The conversion of other RS-based libraries will happen through the `Flowable.wrap(Publisher)` static method. + +(*The unfortunate effect of `create` in 1.x was the ignorance of the Observable contract and beginner's first choice as an entry point. We can't eliminate this path since `rs.Publisher` is a single method functional interface that can be implemented just as badly.*) + +Therefore, new standard factory methods will try to address the common entry point requirements. + +The `Flowable` will contain the following `create` methods: + + - `create(SyncGenerator)`: safe, synchronous generation of signals, one-by-one. + - `create(AsyncOnSubscribe)`: batch-create signals based on request patterns. + - `create(Consumer>)`: relay multiple values or error from multi-valued reactive-sources (i.e. button-clicks) while also give flow control options right there (buffer, drop, error, etc.). + - `createSingle(Consumer>)`: relay a single value or error from other reactive sources (i.e. addListener callbacks). + - `createEmpty(Consumer)`: signal a completion or error from valueless reactive sources. + +The `Observable` will contain the following `create` methods: + + - `create(SyncGenerator)`: safe, synchronous generation of signals, one-by-one. + - `create(Consumer>)`: relay multiple values or error from multi-valued reactive-sources (i.e. button-clicks) while also give flow control options right there (buffer, drop, error, etc.). + - `createSingle(Consumer>)`: relay a single value or error from other reactive sources (i.e. addListener callbacks). + - `createEmpty(Consumer)`: signal a completion or error from valueless reactive sources. + +The `Single` will contain the following `create` method: + + - `create(Consumer>)`: relay a single value or error from other reactive sources (i.e. addListener callbacks). + +The `Completable` will contain the following `create` method: + + - `create(Consumer)`: signal a completion or error from valueless reactive sources. + + +The first two `create` methods take an implementation of an interface which provides state and the generator methods: + +```java +interface SyncGenerator { + + S createState(); + + S generate(S state, Observer output); + + void disposeState(S state); +} + +interface AsyncGenerator { + + S createState(); + + S generate(S state, long requested, Observer> output); + + void disposeState(S state); +} +``` + +These latter three `create` methods will provide the following interaction interfaces to the `java.util.function.Consumer`: + +```java +interface SingleEmitter { + + complete(T value); + + fail(Throwable error); + + stop(); + + setDisposable(Disposable d); + +} + +interface FlowEmitter { + + void next(T value); + + void fail(Throwable error); + + void complete(); + + void stop(); + + setDisposable(Disposable d); + + enum BackpressureHandling { + IGNORE, + ERROR, + DROP, + LATEST, + BUFFER + } + + void setBackpressureHandling(BackpressureHandling mode); + +} + +interface CompletableEmitter { + + complete(); + + fail(Throwable error); + + stop(); + + setDisposable(Disposable d); + +} + +``` + +By extending the base classes, operator implementations would loose the tracking/wrapping features of 1.x. To avoid this, the methods `subscribe(C)` will be final and operators have to implement a protected `subscribeActual` (or any other reasonable name). + +```java +@Override +public final void subscribe(Subscriber s) { + subscribeActual(hook.onSubscribe(s)); +} + +protected abstract void subscribeActual(Subscriber s); +``` + +Assembly-time hooks will be moved into the individual standard methods on the base types: + +```java +public final Flowable map(Function mapper) { + return hook.onAssembly(new FlowableMap(this, mapper)); +} +``` + +### Terminal behavior + +A producer can terminate a stream by emitting `onComplete` or `onError`. A consumer can terminate a stream by calling `cancel`/`dispose`. + +Any resource cleanup of the source or operators must account for any of these three termination events. In other words, if an operator needs cleanup, then it should register the cleanup callback with `cancel`/`dispose`, `onError` and `onComplete`. + +The final `subscribe` will *not* invoke `cancel`/`dispose` after receiving an `onComplete` or `onError`. + +### JVM target and source compatibility + +The 3.x version will target JDK6+ to let Android users consume the new version of RxJava. + +### Future work + +This section contains current design work which needs more discussion and elaboration before it is merged into this document as a stated goal for 3.x. + +#### Custom Observable, Single, Completable, or Flowable + +We are investigate a base interface (similar to `Publisher`) for the `Observable`, `Single`, and `Completable` (currently referred to as `Consumable` or `ConsumableObservable`). This would empower library owners and api developers to implement their own type of `Observable`, `Single`, or `Completable` without extending the class. This would result in a change the type signatures of `subscribe` as well as any operator that operates over an `Observable`, `Single`, or `Completable` to accept a more generic type (i.e. `ConsumableObservable`). For more information see the proof of concept project [Consumable](https://github.com/stealthcode/Consumable). + +#### Fusion + +Operator fusion exploits the declarative nature of building flows; the developer specifies the "what", "where" and "when", the library then tries to optimize the "how". + +There are two main levels of operator fusion: *macro* and *micro*. + +##### Macro-fusion + +Macro fusion deals with the higher level view of the operators, their identity and their combination (mostly in the form of subsequence). This is partially an internal affair of the operators, triggered by the downstream operator and may work with several cases. Given an operator application pair `a().b()` where `a` could be a source or an intermediate operator itself, when the application of `b` happens in assembly time, the following can happen: + + - `b` identifies `a` and decides to not apply itself. Example: `empty().flatMap()` is functionally a no-op. + - `b` identifies `a` and decides to apply a different, conventional operator. Example: `just().subscribeOn()` is turned into `just().observeOn()`. + - `b` decides to apply a new custom operator, combining and inlining existing behavior. Example: `just().subscribeOn()` internally goes to `ScalarScheduledPublisher`. + - `a` is `b` and the two operator's parameter set can be combined into a single application. Example: `filter(p1).filter(p2)` combined into `filter(p1 && p2)`. + +Participating in the macro-fusion externally is possible by implementing a marker interface when extending `Flowable`. Two kinds of interfaces are available: + + - `java.util.Callable`: the Java standard, throwing interface, indicating the single value has to be extracted in subscription time (or later). + - `ScalarCallable`: to indicate the single value can be safely extracted during assembly time and used/inlined in other operators: + +```java +interface ScalarCallable extends java.util.Callable { + @Override + T call(); +} +``` + +`ScalarCallable` is also `Callable` and thus its value can be extracted practically anytime. For convenience (and for sense), `ScalarCallable` overrides and hides the superclass' `throws Exception` clause - throwing during assembly time is likely unreasonable for scalars. + +Since Reactive-Streams doesn't allow `null`s in the value flow, we have the opportunity to define `ScalarCallable`s and `Callable`s returning `null` should be considered as an empty source - allowing operators to dispatch on the type `Callable` first then branch on the nullness of `call()`. + +Interoperating with other libraries, at this level is possible. Reactor-Core uses the same pattern and the two libraries can work with each other's `Publisher+Callable` types. Unfortunately, this means subscription-time only fusion as `ScalarCallable`s live locally in each library. + +##### Micro-fusion + +Micro-fusion goes a step deeper and tries to reuse internal structures, mostly queues, in operator pairs, saving on allocation and sometimes on atomic operations. It's property is that, in a way, subverts the standard Reactive-Streams protocol between subsequent operators that both support fusion. However, from the outside world's view, they still work according to the RS protocol. + +Currently, two main kinds of micro-fusion opportunities are available. + +###### 1) Conditional Subscriber + +This extends the RS `Subscriber`interface with an extra method: `boolean tryOnNext(T value)` and can help avoiding small request amounts in case an operator didn't forward but dropped the value. The canonical use is for the `filter()` operator where if the predicate returns false, the operator has to request 1 from upstream (since the downstream doesn't know there was a value dropped and thus not request itself). Operators wanting to participate in this fusion have to implement and subscribe with an extended `Subscriber` interface: + +```java +interface ConditionalSubscriber { + boolean tryOnNext(T value); +} + +//... +@Override +protected void subscribeActual(Subscriber s) { + if (s instanceof ConditionalSubscriber) { + source.subscribe(new FilterConditionalSubscriber<>(s, predicate)); + } else { + source.subscribe(new FilterRegularSubscriber<>(s, predicate)); + } +} +``` + +(Note that this may lead to extra case-implementations in operators that have some kind of queue-drain emission model.) + +###### 2) Queue-fusion + +The second category is when two (or more) operators share the same underlying queue and each append activity at the exit point (i.e. `poll()`) of the queue. This can work in two modes: synchronous and asynchronous. + +In synchronous mode, the elements of the sequence is already available (i.e. a fixed `range()` or `fromArray()`, or can be synchronously calculated in a pull fashion in `fromIterable`. In this mode, the requesting and regular onError-path is bypassed and is forbidden. Sources have to return null from `pull()` and false from `isEmpty()` if they have no more values and throw from these methods if they want to indicate an exceptional case. + +In asynchronous mode, elements may become available at any time, therefore, `pull` returning null, as with regular queue-drain, is just the indication of temporary lack of source values. Completion and error still has to go through `onComplete` and `onError` as usual, requesting still happens as usual but when a value is available in the shared queue, it is indicated by an `onNext(null)` call. This can trigger a chain of `drain` calls without moving values in or out of different queues. + +In both modes, `cancel` works and behaves as usual. + +Since this fusion mode is an optional extension, the mode switch has to be negotiated and the shared queue interface established. Operators already working with internal queues then can, mostly, keep their current `drain()` algorithm. Queue-fusion has its own interface and protocol built on top of the existing `onSubscribe`-`Subscription` rail: + +```java +interface QueueSubscription implements Queue, Subscription { + int NONE = 0; + int SYNC = 1; + int ASYNC = 2; + int ANY = SYNC | ASYNC; + int BOUNDARY = 4; + + int requestFusion(int mode); +} +``` + +For performance, the mode is an integer bitflags setup, called early during subscription time, and allows negotiating the fusion mode. Usually, producers can do only one mode and consumers can do both mode. Because fused, intermediate operators attach logic (which is many times user-callback) to the exit point of the queue interface (poll()), it may change the computation location of those callbacks in an unwanted way. The flag `BOUNDARY` is added by consumers indicating that they will consume the queue over an async boundary. Intermediate operators, such as `map` and `filter` then can reject the fusion in such sequences. + +Since RxJava 3.x is still JDK 6 compatible, the `QueueSubscription` can't itself default unnecessary methods and implementations are required to throw `UnsupportedOperationException` for `Queue` methods other than the following: + + - `poll()`. + - `isEmpty()`. + - `clear()`. + - `size()`. + +Even though other modern libraries also define this interface, they live in local packages and thus non-reusable without dragging in the whole library. Therefore, until externalized and standardized, cross-library micro-fusion won't happen. + +A consequence of the extension of the `onSubscribe`-`Subscription` rail is that intermediate operators are no longer allowed to pass an upstream `Subscription` directly to its downstream `Subscriber.onSubscribe`. Doing so is likely to have the fused sequence skip the operator completely, losing behavior or causing runtime exceptions. Since RS `Subscriber` is an interface, operators can simply implement both `Subscriber` and `Subscription` on themselves, delegating the `request` and `cancel` calls to the upstream and calling `child.onSubscribe(this)`. diff --git a/LICENSE b/LICENSE index 7f8ced0d1f..d645695673 100644 --- a/LICENSE +++ b/LICENSE @@ -187,7 +187,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright 2012 Netflix, Inc. + Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/README.md b/README.md index dc4736be35..1456d2b68c 100644 --- a/README.md +++ b/README.md @@ -1,116 +1,622 @@ -# RxJava: Functional Reactive Programming on the JVM +# RxJava: Reactive Extensions for the JVM -This library is a Java implementation of Rx Observables. + +[![codecov.io](http://codecov.io/github/ReactiveX/RxJava/coverage.svg?branch=3.x)](https://codecov.io/gh/ReactiveX/RxJava/branch/3.x) +[![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.reactivex.rxjava3/rxjava/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.reactivex.rxjava3/rxjava) +[![Contribute with Gitpod](https://img.shields.io/badge/Contribute%20with-Gitpod-908a85?logo=gitpod)](https://gitpod.io/#https://github.com/ReactiveX/RxJava) +[![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/ReactiveX/RxJava/badge)](https://securityscorecards.dev/viewer/?uri=github.com/ReactiveX/RxJava) -Some of the goals of RxJava are: +RxJava is a Java VM implementation of [Reactive Extensions](http://reactivex.io): a library for composing asynchronous and event-based programs by using observable sequences. -- Stay close to the original Rx.Net implementation while adjusting naming conventions and idioms to Java -- All contracts of Rx should be the same -- Target the JVM not a language. The first languages supported (beyond Java itself) are -Groovy, -Clojure, -Scala -and JRuby. -New language adapters can be contributed. -- Support Java 5 (to include Android support) and higher with an eventual goal to target a build for Java 8 with its lambda support. +It extends the [observer pattern](http://en.wikipedia.org/wiki/Observer_pattern) to support sequences of data/events and adds operators that allow you to compose sequences together declaratively while abstracting away concerns about things like low-level threading, synchronization, thread-safety and concurrent data structures. -Learn more about Rx on the Wiki Home and the Netflix TechBlog post where RxJava was introduced. +#### Version 3.x ([Javadoc](http://reactivex.io/RxJava/3.x/javadoc/)) -## Full Documentation +- Single dependency: [Reactive-Streams](https://github.com/reactive-streams/reactive-streams-jvm). +- Java 8+ or Android API 21+ required. +- Java 8 lambda-friendly API. +- [Android](https://github.com/ReactiveX/RxAndroid) desugar friendly. +- Fixed API mistakes and many limits of RxJava 2. +- Intended to be a replacement for RxJava 2 with relatively few binary incompatible changes. +- Non-opinionated about the source of concurrency (threads, pools, event loops, fibers, actors, etc.). +- Async or synchronous execution. +- Virtual time and schedulers for parameterized concurrency. +- Test and diagnostic support via test schedulers, test consumers and plugin hooks. +- Interop with newer JDK versions via 3rd party libraries, such as + - [Java 9 Flow API](https://github.com/akarnokd/RxJavaJdk9Interop#rxjavajdk9interop) + - [Java 21 Virtual Threads](https://github.com/akarnokd/RxJavaFiberInterop#rxjavafiberinterop) + +Learn more about RxJava in general on the Wiki Home. + +:information_source: Please read the [What's different in 3.0](https://github.com/ReactiveX/RxJava/wiki/What's-different-in-3.0) for details on the changes and migration information when upgrading from 2.x. + +#### Version 2.x + +The [2.x version](https://github.com/ReactiveX/RxJava/tree/2.x) is end-of-life as of **February 28, 2021**. No further development, support, maintenance, PRs and updates will happen. The [Javadoc]([Javadoc](http://reactivex.io/RxJava/2.x/javadoc/)) of the very last version, **2.2.21**, will remain accessible. + +#### Version 1.x + +The [1.x version](https://github.com/ReactiveX/RxJava/tree/1.x) is end-of-life as of **March 31, 2018**. No further development, support, maintenance, PRs and updates will happen. The [Javadoc]([Javadoc](http://reactivex.io/RxJava/1.x/javadoc/)) of the very last version, **1.3.8**, will remain accessible. + +## Getting started + +### Setting up the dependency + +The first step is to include RxJava 3 into your project, for example, as a Gradle compile dependency: + +```groovy +implementation "io.reactivex.rxjava3:rxjava:3.x.y" +``` + +(Please replace `x` and `y` with the latest version numbers: [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.reactivex.rxjava3/rxjava/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.reactivex.rxjava3/rxjava) +) + +### Hello World + +The second is to write the **Hello World** program: + +```java +package rxjava.examples; + +import io.reactivex.rxjava3.core.*; + +public class HelloWorld { + public static void main(String[] args) { + Flowable.just("Hello world").subscribe(System.out::println); + } +} +``` + +Note that RxJava 3 components now live under `io.reactivex.rxjava3` and the base classes and interfaces live under `io.reactivex.rxjava3.core`. + +### Base classes + +RxJava 3 features several base classes you can discover operators on: + + - [`io.reactivex.rxjava3.core.Flowable`](http://reactivex.io/RxJava/3.x/javadoc/io/reactivex/rxjava3/core/Flowable.html): 0..N flows, supporting Reactive-Streams and backpressure + - [`io.reactivex.rxjava3.core.Observable`](http://reactivex.io/RxJava/3.x/javadoc/io/reactivex/rxjava3/core/Observable.html): 0..N flows, no backpressure, + - [`io.reactivex.rxjava3.core.Single`](http://reactivex.io/RxJava/3.x/javadoc/io/reactivex/rxjava3/core/Single.html): a flow of exactly 1 item or an error, + - [`io.reactivex.rxjava3.core.Completable`](http://reactivex.io/RxJava/3.x/javadoc/io/reactivex/rxjava3/core/Completable.html): a flow without items but only a completion or error signal, + - [`io.reactivex.rxjava3.core.Maybe`](http://reactivex.io/RxJava/3.x/javadoc/io/reactivex/rxjava3/core/Maybe.html): a flow with no items, exactly one item or an error. + +### Some terminology + +#### Upstream, downstream + +The dataflows in RxJava consist of a source, zero or more intermediate steps followed by a data consumer or combinator step (where the step is responsible to consume the dataflow by some means): + +```java +source.operator1().operator2().operator3().subscribe(consumer); + +source.flatMap(value -> source.operator1().operator2().operator3()); +``` + +Here, if we imagine ourselves on `operator2`, looking to the left towards the source is called the **upstream**. Looking to the right towards the subscriber/consumer is called the **downstream**. This is often more apparent when each element is written on a separate line: + +```java +source + .operator1() + .operator2() + .operator3() + .subscribe(consumer) +``` + +#### Objects in motion + +In RxJava's documentation, **emission**, **emits**, **item**, **event**, **signal**, **data** and **message** are considered synonyms and represent the object traveling along the dataflow. + +#### Backpressure + +When the dataflow runs through asynchronous steps, each step may perform different things with different speed. To avoid overwhelming such steps, which usually would manifest itself as increased memory usage due to temporary buffering or the need for skipping/dropping data, so-called backpressure is applied, which is a form of flow control where the steps can express how many items are they ready to process. This allows constraining the memory usage of the dataflows in situations where there is generally no way for a step to know how many items the upstream will send to it. + +In RxJava, the dedicated `Flowable` class is designated to support backpressure and `Observable` is dedicated to the non-backpressured operations (short sequences, GUI interactions, etc.). The other types, `Single`, `Maybe` and `Completable` don't support backpressure nor should they; there is always room to store one item temporarily. + +#### Assembly time + +The preparation of dataflows by applying various intermediate operators happens in the so-called **assembly time**: + +```java +Flowable flow = Flowable.range(1, 5) +.map(v -> v * v) +.filter(v -> v % 3 == 0) +; +``` + +At this point, the data is not flowing yet and no side-effects are happening. + +#### Subscription time + +This is a temporary state when `subscribe()` is called on a flow that establishes the chain of processing steps internally: + +```java +flow.subscribe(System.out::println) +```` + +This is when the **subscription side-effects** are triggered (see `doOnSubscribe`). Some sources block or start emitting items right away in this state. + +#### Runtime + +This is the state when the flows are actively emitting items, errors or completion signals: + +```java + +Observable.create(emitter -> { + while (!emitter.isDisposed()) { + long time = System.currentTimeMillis(); + emitter.onNext(time); + if (time % 2 != 0) { + emitter.onError(new IllegalStateException("Odd millisecond!")); + break; + } + } +}) +.subscribe(System.out::println, Throwable::printStackTrace); +``` + +Practically, this is when the body of the given example above executes. + +### Simple background computation + +One of the common use cases for RxJava is to run some computation, network request on a background thread and show the results (or error) on the UI thread: + +```java +import io.reactivex.rxjava3.schedulers.Schedulers; + +Flowable.fromCallable(() -> { + Thread.sleep(1000); // imitate expensive computation + return "Done"; +}) + .subscribeOn(Schedulers.io()) + .observeOn(Schedulers.single()) + .subscribe(System.out::println, Throwable::printStackTrace); + +Thread.sleep(2000); // <--- wait for the flow to finish +``` + +This style of chaining methods is called a **fluent API** which resembles the **builder pattern**. However, RxJava's reactive types are immutable; each of the method calls returns a new `Flowable` with added behavior. To illustrate, the example can be rewritten as follows: + +```java +Flowable source = Flowable.fromCallable(() -> { + Thread.sleep(1000); // imitate expensive computation + return "Done"; +}); + +Flowable runBackground = source.subscribeOn(Schedulers.io()); + +Flowable showForeground = runBackground.observeOn(Schedulers.single()); + +showForeground.subscribe(System.out::println, Throwable::printStackTrace); + +Thread.sleep(2000); +``` + +Typically, you can move computations or blocking IO to some other thread via `subscribeOn`. Once the data is ready, you can make sure they get processed on the foreground or GUI thread via `observeOn`. + +### Schedulers + +RxJava operators don't work with `Thread`s or `ExecutorService`s directly but with so-called `Scheduler`s that abstract away sources of concurrency behind a uniform API. RxJava 3 features several standard schedulers accessible via `Schedulers` utility class. + +- `Schedulers.computation()`: Run computation intensive work on a fixed number of dedicated threads in the background. Most asynchronous operators use this as their default `Scheduler`. +- `Schedulers.io()`: Run I/O-like or blocking operations on a dynamically changing set of threads. +- `Schedulers.single()`: Run work on a single thread in a sequential and FIFO manner. +- `Schedulers.trampoline()`: Run work in a sequential and FIFO manner in one of the participating threads, usually for testing purposes. + +These are available on all JVM platforms but some specific platforms, such as Android, have their own typical `Scheduler`s defined: `AndroidSchedulers.mainThread()`, `SwingScheduler.instance()` or `JavaFXScheduler.platform()`. + +In addition, there is an option to wrap an existing `Executor` (and its subtypes such as `ExecutorService`) into a `Scheduler` via `Schedulers.from(Executor)`. This can be used, for example, to have a larger but still fixed pool of threads (unlike `computation()` and `io()` respectively). + +The `Thread.sleep(2000);` at the end is no accident. In RxJava the default `Scheduler`s run on daemon threads, which means once the Java main thread exits, they all get stopped and background computations may never happen. Sleeping for some time in this example situations lets you see the output of the flow on the console with time to spare. + +### Concurrency within a flow + +Flows in RxJava are sequential in nature split into processing stages that may run **concurrently** with each other: + +```java +Flowable.range(1, 10) + .observeOn(Schedulers.computation()) + .map(v -> v * v) + .blockingSubscribe(System.out::println); +``` + +This example flow squares the numbers from 1 to 10 on the **computation** `Scheduler` and consumes the results on the "main" thread (more precisely, the caller thread of `blockingSubscribe`). However, the lambda `v -> v * v` doesn't run in parallel for this flow; it receives the values 1 to 10 on the same computation thread one after the other. + +### Parallel processing + +Processing the numbers 1 to 10 in parallel is a bit more involved: + +```java +Flowable.range(1, 10) + .flatMap(v -> + Flowable.just(v) + .subscribeOn(Schedulers.computation()) + .map(w -> w * w) + ) + .blockingSubscribe(System.out::println); +``` + +Practically, parallelism in RxJava means running independent flows and merging their results back into a single flow. The operator `flatMap` does this by first mapping each number from 1 to 10 into its own individual `Flowable`, runs them and merges the computed squares. + +Note, however, that `flatMap` doesn't guarantee any order and the items from the inner flows may end up interleaved. There are alternative operators: + + - `concatMap` that maps and runs one inner flow at a time and + - `concatMapEager` which runs all inner flows "at once" but the output flow will be in the order those inner flows were created. + +Alternatively, the `Flowable.parallel()` operator and the `ParallelFlowable` type help achieve the same parallel processing pattern: + +```java +Flowable.range(1, 10) + .parallel() + .runOn(Schedulers.computation()) + .map(v -> v * v) + .sequential() + .blockingSubscribe(System.out::println); +``` + +### Dependent sub-flows + +`flatMap` is a powerful operator and helps in a lot of situations. For example, given a service that returns a `Flowable`, we'd like to call another service with values emitted by the first service: + +```java +Flowable inventorySource = warehouse.getInventoryAsync(); + +inventorySource + .flatMap(inventoryItem -> erp.getDemandAsync(inventoryItem.getId()) + .map(demand -> "Item " + inventoryItem.getName() + " has demand " + demand)) + .subscribe(System.out::println); +``` + +### Continuations + +Sometimes, when an item has become available, one would like to perform some dependent computations on it. This is sometimes called **continuations** and, depending on what should happen and what types are involved, may involve various operators to accomplish. + +#### Dependent + +The most typical scenario is to given a value, invoke another service, await and continue with its result: + +```java +service.apiCall() +.flatMap(value -> service.anotherApiCall(value)) +.flatMap(next -> service.finalCall(next)) +``` + +It is often the case also that later sequences would require values from earlier mappings. This can be achieved by moving the outer `flatMap` into the inner parts of the previous `flatMap` for example: + +```java +service.apiCall() +.flatMap(value -> + service.anotherApiCall(value) + .flatMap(next -> service.finalCallBoth(value, next)) +) +``` + +Here, the original `value` will be available inside the inner `flatMap`, courtesy of lambda variable capture. + +#### Non-dependent + +In other scenarios, the result(s) of the first source/dataflow is irrelevant and one would like to continue with a quasi independent another source. Here, `flatMap` works as well: + +```java +Observable continued = sourceObservable.flatMapSingle(ignored -> someSingleSource) +continued.map(v -> v.toString()) + .subscribe(System.out::println, Throwable::printStackTrace); +``` -- [Wiki](https://github.com/Netflix/RxJava/wiki) -- Javadoc +however, the continuation in this case stays `Observable` instead of the likely more appropriate `Single`. (This is understandable because +from the perspective of `flatMapSingle`, `sourceObservable` is a multi-valued source and thus the mapping may result in multiple values as well). -## Code +Often though there is a way that is somewhat more expressive (and also lower overhead) by using `Completable` as the mediator and its operator `andThen` to resume with something else: -- Java Core - - Observable - - Observer -- Groovy Adaptor -- Clojure Adaptor -- Scala Adaptor -- JRuby Adaptor +```java +sourceObservable + .ignoreElements() // returns Completable + .andThen(someSingleSource) + .map(v -> v.toString()) +``` + +The only dependency between the `sourceObservable` and the `someSingleSource` is that the former should complete normally in order for the latter to be consumed. + +#### Deferred-dependent + +Sometimes, there is an implicit data dependency between the previous sequence and the new sequence that, for some reason, was not flowing through the "regular channels". One would be inclined to write such continuations as follows: + +```java +AtomicInteger count = new AtomicInteger(); + +Observable.range(1, 10) + .doOnNext(ignored -> count.incrementAndGet()) + .ignoreElements() + .andThen(Single.just(count.get())) + .subscribe(System.out::println); +``` + +Unfortunately, this prints `0` because `Single.just(count.get())` is evaluated at **assembly time** when the dataflow hasn't even run yet. We need something that defers the evaluation of this `Single` source until **runtime** when the main source completes: + +```java +AtomicInteger count = new AtomicInteger(); + +Observable.range(1, 10) + .doOnNext(ignored -> count.incrementAndGet()) + .ignoreElements() + .andThen(Single.defer(() -> Single.just(count.get()))) + .subscribe(System.out::println); +``` + +or + +```java +AtomicInteger count = new AtomicInteger(); + +Observable.range(1, 10) + .doOnNext(ignored -> count.incrementAndGet()) + .ignoreElements() + .andThen(Single.fromCallable(() -> count.get())) + .subscribe(System.out::println); +``` + + +### Type conversions + +Sometimes, a source or service returns a different type than the flow that is supposed to work with it. For example, in the inventory example above, `getDemandAsync` could return a `Single`. If the code example is left unchanged, this will result in a compile-time error (however, often with a misleading error message about lack of overload). + +In such situations, there are usually two options to fix the transformation: 1) convert to the desired type or 2) find and use an overload of the specific operator supporting the different type. + +#### Converting to the desired type + +Each reactive base class features operators that can perform such conversions, including the protocol conversions, to match some other type. The following matrix shows the available conversion options: + +| | Flowable | Observable | Single | Maybe | Completable | +|----------|----------|------------|--------|-------|-------------| +|**Flowable** | | `toObservable` | `first`, `firstOrError`, `single`, `singleOrError`, `last`, `lastOrError`1 | `firstElement`, `singleElement`, `lastElement` | `ignoreElements` | +|**Observable**| `toFlowable`2 | | `first`, `firstOrError`, `single`, `singleOrError`, `last`, `lastOrError`1 | `firstElement`, `singleElement`, `lastElement` | `ignoreElements` | +|**Single** | `toFlowable`3 | `toObservable` | | `toMaybe` | `ignoreElement` | +|**Maybe** | `toFlowable`3 | `toObservable` | `toSingle` | | `ignoreElement` | +|**Completable** | `toFlowable` | `toObservable` | `toSingle` | `toMaybe` | | + +1: When turning a multi-valued source into a single-valued source, one should decide which of the many source values should be considered as the result. + +2: Turning an `Observable` into `Flowable` requires an additional decision: what to do with the potential unconstrained flow +of the source `Observable`? There are several strategies available (such as buffering, dropping, keeping the latest) via the `BackpressureStrategy` parameter or via standard `Flowable` operators such as `onBackpressureBuffer`, `onBackpressureDrop`, `onBackpressureLatest` which also +allow further customization of the backpressure behavior. + +3: When there is only (at most) one source item, there is no problem with backpressure as it can be always stored until the downstream is ready to consume. + + +#### Using an overload with the desired type + +Many frequently used operator has overloads that can deal with the other types. These are usually named with the suffix of the target type: + +| Operator | Overloads | +|----------|-----------| +| `flatMap` | `flatMapSingle`, `flatMapMaybe`, `flatMapCompletable`, `flatMapIterable` | +| `concatMap` | `concatMapSingle`, `concatMapMaybe`, `concatMapCompletable`, `concatMapIterable` | +| `switchMap` | `switchMapSingle`, `switchMapMaybe`, `switchMapCompletable` | + +The reason these operators have a suffix instead of simply having the same name with different signature is type erasure. Java doesn't consider signatures such as `operator(Function>)` and `operator(Function>)` different (unlike C#) and due to erasure, the two `operator`s would end up as duplicate methods with the same signature. + +### Operator naming conventions + +Naming in programming is one of the hardest things as names are expected to be not long, expressive, capturing and easily memorable. Unfortunately, the target language (and pre-existing conventions) may not give too much help in this regard (unusable keywords, type erasure, type ambiguities, etc.). + +#### Unusable keywords + +In the original Rx.NET, the operator that emits a single item and then completes is called `Return(T)`. Since the Java convention is to have a lowercase letter start a method name, this would have been `return(T)` which is a keyword in Java and thus not available. Therefore, RxJava chose to name this operator `just(T)`. The same limitation exists for the operator `Switch`, which had to be named `switchOnNext`. Yet another example is `Catch` which was named `onErrorResumeNext`. + +#### Type erasure + +Many operators that expect the user to provide some function returning a reactive type can't be overloaded because the type erasure around a `Function` turns such method signatures into duplicates. RxJava chose to name such operators by appending the type as suffix as well: + +```java +Flowable flatMap(Function> mapper) + +Flowable flatMapMaybe(Function> mapper) +``` + +#### Type ambiguities + +Even though certain operators have no problems from type erasure, their signature may turn up being ambiguous, especially if one uses Java 8 and lambdas. For example, there are several overloads of `concatWith` taking the various other reactive base types as arguments (for providing convenience and performance benefits in the underlying implementation): + +```java +Flowable concatWith(Publisher other); + +Flowable concatWith(SingleSource other); +``` + +Both `Publisher` and `SingleSource` appear as functional interfaces (types with one abstract method) and may encourage users to try to provide a lambda expression: + +```java +someSource.concatWith(s -> Single.just(2)) +.subscribe(System.out::println, Throwable::printStackTrace); +``` + +Unfortunately, this approach doesn't work and the example does not print `2` at all. In fact, since version 2.1.10, it doesn't +even compile because at least 4 `concatWith` overloads exist and the compiler finds the code above ambiguous. + +The user in such situations probably wanted to defer some computation until the `someSource` has completed, thus the correct +unambiguous operator should have been `defer`: + +```java +someSource.concatWith(Single.defer(() -> Single.just(2))) +.subscribe(System.out::println, Throwable::printStackTrace); +``` + +Sometimes, a suffix is added to avoid logical ambiguities that may compile but produce the wrong type in a flow: + +```java +Flowable merge(Publisher> sources); + +Flowable mergeArray(Publisher... sources); +``` + +This can get also ambiguous when functional interface types get involved as the type argument `T`. + +#### Error handling + +Dataflows can fail, at which point the error is emitted to the consumer(s). Sometimes though, multiple sources may fail at which point there is a choice whether or not wait for all of them to complete or fail. To indicate this opportunity, many operator names are suffixed with the `DelayError` words (while others feature a `delayError` or `delayErrors` boolean flag in one of their overloads): + +```java +Flowable concat(Publisher> sources); + +Flowable concatDelayError(Publisher> sources); +``` + +Of course, suffixes of various kinds may appear together: + +```java +Flowable concatArrayEagerDelayError(Publisher... sources); +``` + +#### Base class vs base type + +The base classes can be considered heavy due to the sheer number of static and instance methods on them. RxJava 3's design was heavily influenced by the [Reactive Streams](https://github.com/reactive-streams/reactive-streams-jvm#reactive-streams) specification, therefore, the library features a class and an interface per each reactive type: + +| Type | Class | Interface | Consumer | +|------|-------|-----------|----------| +| 0..N backpressured | `Flowable` | `Publisher`1 | `Subscriber` | +| 0..N unbounded | `Observable` | `ObservableSource`2 | `Observer` | +| 1 element or error | `Single` | `SingleSource` | `SingleObserver` | +| 0..1 element or error | `Maybe` | `MaybeSource` | `MaybeObserver` | +| 0 element or error | `Completable` | `CompletableSource` | `CompletableObserver` | + +1The `org.reactivestreams.Publisher` is part of the external Reactive Streams library. It is the main type to interact with other reactive libraries through a standardized mechanism governed by the [Reactive Streams specification](https://github.com/reactive-streams/reactive-streams-jvm#specification). + +2The naming convention of the interface was to append `Source` to the semi-traditional class name. There is no `FlowableSource` since `Publisher` is provided by the Reactive Streams library (and subtyping it wouldn't have helped with interoperation either). These interfaces are, however, not standard in the sense of the Reactive Streams specification and are currently RxJava specific only. + +### R8 and ProGuard settings + +By default, RxJava itself doesn't require any ProGuard/R8 settings and should work without problems. Unfortunately, the Reactive Streams dependency since version 1.0.3 has embedded Java 9 class files in its JAR that can cause warnings with the plain ProGuard: + +``` +Warning: org.reactivestreams.FlowAdapters$FlowPublisherFromReactive: can't find superclass or interface java.util.concurrent.Flow$Publisher +Warning: org.reactivestreams.FlowAdapters$FlowToReactiveProcessor: can't find superclass or interface java.util.concurrent.Flow$Processor +Warning: org.reactivestreams.FlowAdapters$FlowToReactiveSubscriber: can't find superclass or interface java.util.concurrent.Flow$Subscriber +Warning: org.reactivestreams.FlowAdapters$FlowToReactiveSubscription: can't find superclass or interface java.util.concurrent.Flow$Subscription +Warning: org.reactivestreams.FlowAdapters: can't find referenced class java.util.concurrent.Flow$Publisher +``` + +It is recommended one sets up the following `-dontwarn` entry in the application's `proguard-ruleset` file: + +``` +-dontwarn java.util.concurrent.Flow* +``` + +For R8, the RxJava jar includes the `META-INF/proguard/rxjava3.pro` with the same no-warning clause and should apply automatically. + +### Further reading + +For further details, consult the [wiki](https://github.com/ReactiveX/RxJava/wiki). + +## Communication + +- Google Group: [RxJava](http://groups.google.com/d/forum/rxjava) +- Twitter: [@RxJava](http://twitter.com/RxJava) +- [GitHub Issues](https://github.com/ReactiveX/RxJava/issues) +- StackOverflow: [rx-java](http://stackoverflow.com/questions/tagged/rx-java) and [rx-java2](http://stackoverflow.com/questions/tagged/rx-java2) +- [Gitter.im](https://gitter.im/ReactiveX/RxJava) + +## Versioning + +Version 3.x is in development. Bugfixes will be applied to both 2.x and 3.x branches, but new features will only be added to 3.x. + +Minor 3.x increments (such as 3.1, 3.2, etc) will occur when non-trivial new functionality is added or significant enhancements or bug fixes occur that may have behavioral changes that may affect some edge cases (such as dependence on behavior resulting from a bug). An example of an enhancement that would classify as this is adding reactive pull backpressure support to an operator that previously did not support it. This should be backwards compatible but does behave differently. + +Patch 3.x.y increments (such as 3.0.0 -> 3.0.1, 3.3.1 -> 3.3.2, etc) will occur for bug fixes and trivial functionality (like adding a method overload). New functionality marked with an [`@Beta`][beta source link] or [`@Experimental`][experimental source link] annotation can also be added in the patch releases to allow rapid exploration and iteration of unstable new functionality. + +#### @Beta + +APIs marked with the [`@Beta`][beta source link] annotation at the class or method level are subject to change. They can be modified in any way, or even removed, at any time. If your code is a library itself (i.e. it is used on the CLASSPATH of users outside your control), you should not use beta APIs, unless you repackage them (e.g. using ProGuard, shading, etc). + +#### @Experimental + +APIs marked with the [`@Experimental`][experimental source link] annotation at the class or method level will almost certainly change. They can be modified in any way, or even removed, at any time. You should not use or rely on them in any production code. They are purely to allow broad testing and feedback. + +#### @Deprecated + +APIs marked with the `@Deprecated` annotation at the class or method level will remain supported until the next major release, but it is recommended to stop using them. + +#### io.reactivex.rxjava3.internal.* + +All code inside the `io.reactivex.rxjava3.internal.*` packages are considered private API and should not be relied upon at all. It can change at any time. + +## Full Documentation + +- [Wiki](https://github.com/ReactiveX/RxJava/wiki) +- [Javadoc](http://reactivex.io/RxJava/3.x/javadoc/) +- [Latest snaphot Javadoc](http://reactivex.io/RxJava/3.x/javadoc/snapshot/) +- Javadoc of a specific [release version](https://github.com/ReactiveX/RxJava/tags): `http://reactivex.io/RxJava/3.x/javadoc/3.x.y/` ## Binaries -Binaries and dependency information for Maven, Ivy, Gradle and others can be found at [http://search.maven.org](http://search.maven.org/#search%7Cga%7C1%7Cg%3A%22com.netflix.rxjava%22). +Binaries and dependency information for Maven, Ivy, Gradle and others can be found at [http://search.maven.org](http://search.maven.org/#search%7Cga%7C1%7Cio.reactivex.rxjava3). + +Example for Gradle: -Example for Maven: +```groovy +implementation 'io.reactivex.rxjava3:rxjava:x.y.z' +``` + +and for Maven: ```xml - com.netflix.rxjava - rxjava-core - 0.5.0 + io.reactivex.rxjava3 + rxjava + x.y.z ``` and for Ivy: ```xml - + ``` -If you need to download the jars instead of using a build system, create a Maven pom file like this with the desired version: +### Snapshots -```xml - - - 4.0.0 - com.netflix.rxjava.download - rxjava-download - 1.0-SNAPSHOT - Simple POM to download rxjava-core and dependencies - http://github.com/Netflix/RxJava - - - com.netflix.rxjava - rxjava-core - 0.5.0 - - - - -``` +Snapshots after May 1st, 2021 are available via https://oss.sonatype.org/content/repositories/snapshots/io/reactivex/rxjava3/rxjava/ -Then execute: +```groovy +repositories { + maven { url 'https://oss.sonatype.org/content/repositories/snapshots' } +} +dependencies { + implementation 'io.reactivex.rxjava3:rxjava:3.0.0-SNAPSHOT' +} ``` -mvn -f download-rxjava-pom.xml dependency:copy-dependencies -``` - -It will download rxjava-core-*.jar and its dependencies into ./target/dependency/. -You need Java 6 or later. +JavaDoc snapshots are available at http://reactivex.io/RxJava/3.x/javadoc/snapshot ## Build To build: ``` -$ git clone git@github.com:Netflix/RxJava.git +$ git clone git@github.com:ReactiveX/RxJava.git $ cd RxJava/ $ ./gradlew build ``` -Futher details on building can be found on the [Getting Started](https://github.com/Netflix/RxJava/wiki/Getting-Started) page of the wiki. +Further details on building can be found on the [Getting Started](https://github.com/ReactiveX/RxJava/wiki/Getting-Started) page of the wiki. ## Bugs and Feedback -For bugs, questions and discussions please use the [Github Issues](https://github.com/Netflix/RxJava/issues). +For bugs, questions and discussions please use the [Github Issues](https://github.com/ReactiveX/RxJava/issues). ## LICENSE -Copyright 2013 Netflix, Inc. + Copyright (c) 2016-present, RxJava Contributors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 - + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. +[beta source link]: https://github.com/ReactiveX/RxJava/blob/3.x/src/main/java/io/reactivex/rxjava3/annotations/Beta.java +[experimental source link]: https://github.com/ReactiveX/RxJava/blob/3.x/src/main/java/io/reactivex/rxjava3/annotations/Experimental.java diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000000..1003574331 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,7 @@ +# Security Policy + +If you have discovered a security vulnerability in this project, please report it privately. **Do not disclose it as a public issue.** This gives us time to work with you to fix the issue before public exposure, reducing the chance that the exploit will be used before a patch is released. + +Please disclose it at [security advisory](https://github.com/ReactiveX/RxJava/security/advisories/new). + +This project is maintained by a team of volunteers on a reasonable-effort basis. As such, please give us at least 90 days to work on a fix before public exposure. diff --git a/build.gradle b/build.gradle index 9073b716ee..afc9c0c593 100644 --- a/build.gradle +++ b/build.gradle @@ -1,28 +1,222 @@ -ext.githubProjectName = rootProject.name +plugins { + id("java-library") + id("checkstyle") + id("eclipse") + id("jacoco") + id("maven-publish") + id("ru.vyarus.animalsniffer") version "2.0.1" + id("me.champeau.gradle.jmh") version "0.5.3" + id("com.github.hierynomus.license") version "0.16.1" + id("biz.aQute.bnd.builder") version "6.4.0" + id("com.vanniktech.maven.publish") version "0.19.0" + id("org.beryx.jar") version "1.2.0" +} + +ext { + reactiveStreamsVersion = "1.0.4" + junitVersion = "4.13.2" + testNgVersion = "7.5" + mockitoVersion = "4.11.0" + jmhLibVersion = "1.21" + guavaVersion = "33.4.8-jre" +} + +def releaseTag = System.getenv("BUILD_TAG") +if (releaseTag != null && !releaseTag.isEmpty()) { + if (releaseTag.startsWith("v")) { + releaseTag = releaseTag.substring(1) + } + project.version = releaseTag + + logger.lifecycle("Releasing with version: " + project.version) +} + +repositories { + mavenCentral() +} + +dependencies { + signature "org.codehaus.mojo.signature:java18:1.0@signature" + + api "org.reactivestreams:reactive-streams:$reactiveStreamsVersion" + jmh "org.reactivestreams:reactive-streams:$reactiveStreamsVersion" + + testImplementation "junit:junit:$junitVersion" + testImplementation "org.mockito:mockito-core:$mockitoVersion" + + testImplementation "org.reactivestreams:reactive-streams-tck:$reactiveStreamsVersion" + testImplementation "org.testng:testng:$testNgVersion" + testImplementation "com.google.guava:guava:$guavaVersion" +} + +java { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 +} + +tasks.withType(JavaCompile) { + options.compilerArgs << "-parameters" +} + +apply from: file("gradle/javadoc_cleanup.gradle") + +javadoc { + exclude "**/internal/**" + exclude "**/test/**" + exclude "**/perf/**" + exclude "**/jmh/**" + options { + windowTitle = "RxJava Javadoc ${project.version}" + } + // Clear the following options to make the docs consistent with the old format + options.addStringOption("top").value = "" + options.addStringOption("doctitle").value = "" + options.addStringOption("header").value = "" + options.stylesheetFile = project.file("gradle/stylesheet.css") + + options.links( + "https://docs.oracle.com/javase/8/docs/api/", + "https://reactivex.io/RxJava/org.reactivestreams.javadoc/$%7BreactiveStreamsVersion%7D/" + ) + + finalizedBy javadocCleanup +} + +animalsniffer { + annotation = "io.reactivex.rxjava3.internal.util.SuppressAnimalSniffer" +} + +jar { + from('.') { + include 'LICENSE' + include 'COPYRIGHT' + into('META-INF/') + } + + // Cover for bnd still not supporting MR Jars: https://github.com/bndtools/bnd/issues/2227 + bnd('-fixupmessages': '^Classes found in the wrong directory: \\\\{META-INF/versions/9/module-info\\\\.class=module-info}$') + bnd( + "Bundle-Name": "rxjava", + "Bundle-Vendor": "RxJava Contributors", + "Bundle-Description": "Reactive Extensions for the JVM - a library for composing asynchronous and event-based programs using observable sequences for the Java VM.", + "Import-Package": "!org.junit,!junit.framework,!org.mockito.*,!org.testng.*,*", + "Bundle-DocURL": "https://github.com/ReactiveX/RxJava", + "Eclipse-ExtensibleAPI": "true", + "Export-Package": "!io.reactivex.rxjava3.internal.*, io.reactivex.rxjava3.*", + "Bundle-SymbolicName": "io.reactivex.rxjava3.rxjava", + "Multi-Release": "true" + ) + + moduleInfoPath = 'src/main/module/module-info.java' +} + +license { + header project.file("config/license/HEADER") + ext.year = Calendar.getInstance().get(Calendar.YEAR) + skipExistingHeaders true + ignoreFailures true + excludes(["**/*.md", "**/*.txt"]) +} + +jmh { + jmhVersion = jmhLibVersion + humanOutputFile = null + includeTests = false + jvmArgs = ["-Djmh.ignoreLock=true"] + jvmArgsAppend = ["-Djmh.separateClasspathJAR=true"] -buildscript { - repositories { mavenCentral() } - apply from: file('gradle/buildscript.gradle'), to: buildscript + if (project.hasProperty("jmh")) { + include = [".*" + project.jmh + ".*"] + logger.info("JMH: {}", include) + } +} + +test { + maxHeapSize = "1200m" } -allprojects { - repositories { mavenCentral() } +task testNG(type: Test) { + useTestNG() } -apply from: file('gradle/convention.gradle') -apply from: file('gradle/maven.gradle') -//apply from: file('gradle/check.gradle') -apply from: file('gradle/license.gradle') -apply from: file('gradle/release.gradle') +check.dependsOn testNG -subprojects { +tasks.withType(Test) { + testLogging { + events = ["skipped", "failed"] + exceptionFormat = "full" - group = "com.netflix.${githubProjectName}" + debug.events = ["skipped", "failed"] + debug.exceptionFormat = "full" - sourceSets.test.java.srcDir 'src/main/java' + info.events = ["failed", "skipped"] + info.exceptionFormat = "full" - tasks.withType(Javadoc).each { - it.classpath = sourceSets.main.compileClasspath + warn.events = ["failed", "skipped"] + warn.exceptionFormat = "full" + } + + if (System.getenv("CI") == null) { + maxParallelForks = Runtime.runtime.availableProcessors().intdiv(2) ?: 1 } } +jacocoTestReport { + dependsOn test + dependsOn testNG + + reports { + xml.enabled = true + html.enabled = true + } +} + +check.dependsOn jacocoTestReport + +checkstyle { + configFile = project.file("config/checkstyle/checkstyle.xml") + configProperties = [ + "checkstyle.suppressions.file": project.file("config/checkstyle/suppressions.xml"), + "checkstyle.header.file" : project.file("config/license/HEADER_JAVA") + ] +} + +if (project.hasProperty("releaseMode")) { + logger.lifecycle("ReleaseMode: {}", project.releaseMode) + + /* + if ("branch" == project.releaseMode) { + + if (version.endsWith("-SNAPSHOT")) { + publishing { + repositories { + maven { + url = "https://s01.oss.sonatype.org/content/repositories/snapshots/" + } + } + } + + mavenPublish { + nexus { + stagingProfile = "io.reactivex" + } + } + } + } + */ + + if ("full" == project.releaseMode) { + signing { + if (project.hasProperty("SIGNING_PRIVATE_KEY") && project.hasProperty("SIGNING_PASSWORD")) { + useInMemoryPgpKeys(project.getProperty("SIGNING_PRIVATE_KEY"), project.getProperty("SIGNING_PASSWORD")) + } + } + /* + mavenPublish { + nexus { + stagingProfile = "io.reactivex" + } + } + */ + } +} diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 0000000000..174689e7ec --- /dev/null +++ b/codecov.yml @@ -0,0 +1,13 @@ +coverage: + status: + project: + default: + target: 95% + threshold: 1% + + patch: + default: + target: 95% + threshold: 1% + + changes: no diff --git a/codequality/HEADER b/codequality/HEADER deleted file mode 100644 index 3102e4b449..0000000000 --- a/codequality/HEADER +++ /dev/null @@ -1,13 +0,0 @@ -Copyright ${year} Netflix, Inc. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. diff --git a/codequality/checkstyle.xml b/codequality/checkstyle.xml deleted file mode 100644 index 481d2829fd..0000000000 --- a/codequality/checkstyle.xml +++ /dev/null @@ -1,189 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/config/checkstyle/checkstyle.xml b/config/checkstyle/checkstyle.xml new file mode 100644 index 0000000000..05896aee12 --- /dev/null +++ b/config/checkstyle/checkstyle.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/config/checkstyle/suppressions.xml b/config/checkstyle/suppressions.xml new file mode 100644 index 0000000000..cf580e45e6 --- /dev/null +++ b/config/checkstyle/suppressions.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + diff --git a/config/license/HEADER b/config/license/HEADER new file mode 100644 index 0000000000..3949e0b453 --- /dev/null +++ b/config/license/HEADER @@ -0,0 +1,10 @@ +Copyright (c) 2016-present, RxJava Contributors. + +Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in +compliance with the License. You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under the License is +distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See +the License for the specific language governing permissions and limitations under the License. diff --git a/config/license/HEADER_JAVA b/config/license/HEADER_JAVA new file mode 100644 index 0000000000..d95b44938b --- /dev/null +++ b/config/license/HEADER_JAVA @@ -0,0 +1,12 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ diff --git a/docs/Additional-Reading.md b/docs/Additional-Reading.md new file mode 100644 index 0000000000..85e7d47077 --- /dev/null +++ b/docs/Additional-Reading.md @@ -0,0 +1,63 @@ +A more complete and up-to-date list of resources can be found at the [reactivex.io site](http://reactivex.io/tutorials.html) + +# Introducing Reactive Programming +* [Introduction to Rx](http://www.introtorx.com/): a free, on-line book by Lee Campbell **(1.x)** +* [The introduction to Reactive Programming you've been missing](https://gist.github.com/staltz/868e7e9bc2a7b8c1f754) by Andre Staltz +* [Mastering Observables](http://docs.couchbase.com/developer/java-2.0/observables.html) from the Couchbase documentation **(1.x)** +* [Reactive Programming in Java 8 With RxJava](http://pluralsight.com/training/Courses/TableOfContents/reactive-programming-java-8-rxjava), a course designed by Russell Elledge **(1.x)** +* [33rd Degree Reactive Java](http://www.slideshare.net/tkowalcz/33rd-degree-reactive-java) by Tomasz Kowalczewski **(1.x)** +* [What Every Hipster Should Know About Functional Reactive Programming](http://www.infoq.com/presentations/game-functional-reactive-programming) - Bodil Stokke demos the creation of interactive game mechanics in RxJS +* [Your Mouse is a Database](http://queue.acm.org/detail.cfm?id=2169076) by Erik Meijer +* [A Playful Introduction to Rx](https://www.youtube.com/watch?v=WKore-AkisY) a video lecture by Erik Meijer +* Wikipedia: [Reactive Programming](http://en.wikipedia.org/wiki/Reactive_programming) and [Functional Reactive Programming](http://en.wikipedia.org/wiki/Functional_reactive_programming) +* [What is Reactive Programming?](https://www.youtube.com/watch?v=-8Y1-lE6NSA) a video presentation by Jafar Husain. +* [2 minute introduction to Rx](https://medium.com/@andrestaltz/2-minute-introduction-to-rx-24c8ca793877) by André Staltz +* StackOverflow: [What is (functional) reactive programming?](http://stackoverflow.com/a/1030631/1946802) +* [The Reactive Manifesto](http://www.reactivemanifesto.org/) +* Grokking RxJava, [Part 1: The Basics](http://blog.danlew.net/2014/09/15/grokking-rxjava-part-1/), [Part 2: Operator, Operator](http://blog.danlew.net/2014/09/22/grokking-rxjava-part-2/), [Part 3: Reactive with Benefits](http://blog.danlew.net/2014/09/30/grokking-rxjava-part-3/), [Part 4: Reactive Android](http://blog.danlew.net/2014/10/08/grokking-rxjava-part-4/) - published in Sep/Oct 2014 by Daniel Lew **(1.x)** +* [Reactive Programming on Android with RxJava](https://leanpub.com/reactiveandroid), a free e-book by Chris Arriola and Angus Huang. + +# How Netflix Is Using RxJava +* LambdaJam Chicago 2013: [Functional Reactive Programming in the Netflix API](https://speakerdeck.com/benjchristensen/functional-reactive-programming-in-the-netflix-api-lambdajam-2013) by Ben Christensen **(1.x)** +* QCon London 2013 presentation: [Functional Reactive Programming in the Netflix API](http://www.infoq.com/presentations/netflix-functional-rx) and a related [interview](http://www.infoq.com/interviews/christensen-hystrix-rxjava) with Ben Christensen **(1.x)** +* [Functional Reactive in the Netflix API with RxJava](http://techblog.netflix.com/2013/02/rxjava-netflix-api.html) by Ben Christensen and Jafar Husain **(1.x)** +* [Optimizing the Netflix API](http://techblog.netflix.com/2013/01/optimizing-netflix-api.html) by Ben Christensen **(1.x)** +* [Reactive Programming at Netflix](http://techblog.netflix.com/2013/01/reactive-programming-at-netflix.html) by Jafar Husain **(1.x)** + +# RxScala +* [RxJava: Reactive Extensions in Scala](http://www.youtube.com/watch?v=tOMK_FYJREw&feature=youtu.be): video of Ben Christensen and Matt Jacobs presenting at SF Scala **(1.x)** + +# Rx.NET +* [rx.codeplex.com](https://rx.codeplex.com) +* [Rx Design Guidelines (PDF)](http://go.microsoft.com/fwlink/?LinkID=205219) +* [Channel 9 MSDN videos on Reactive Extensions](http://channel9.msdn.com/Tags/reactive+extensions) +* [Beginner’s Guide to the Reactive Extensions](http://msdn.microsoft.com/en-us/data/gg577611) +* [Rx Is now Open Source](http://www.hanselman.com/blog/ReactiveExtensionsRxIsNowOpenSource.aspx) by Scott Hanselman +* [Rx Workshop: Observables vs. Events](http://channel9.msdn.com/Series/Rx-Workshop/Rx-Workshop-Observables-versus-Events) +* [Rx Workshop: Unified Programming Model](http://channel9.msdn.com/Series/Rx-Workshop/Rx-Workshop-Unified-Programming-Model) +* [MSDN Rx forum](http://social.msdn.microsoft.com/Forums/en-US/home?forum=rx) + +# RxJS +* [the RxJS github site](https://github.com/reactivex/rxjs) +* An interactive tutorial: [Functional Programming in Javascript](http://jhusain.github.io/learnrx/) and [an accompanying lecture (video)](http://www.youtube.com/watch?v=LB4lhFJBBq0) by Jafar Husain +* [Netflix JavaScript Talks - Async JavaScript with Reactive Extensions](https://www.youtube.com/watch?v=XRYN2xt11Ek) video of a talk by Jafar Husain about the Rx way of programming +* [RxJS](https://xgrommx.github.io/rx-book/), an on-line book by @xgrommx +* [Journey from procedural to reactive Javascript with stops](https://glebbahmutov.com/blog/journey-from-procedural-to-reactive-javascript-with-stops/) by Gleb Bahmutov + +# RxAndroid + +* [FRP on Android](http://slides.com/yaroslavheriatovych/frponandroid#/) - publish in Jan 2014 by Yaroslav Heriatovych **(1.x)** +* [RxAndroid Github page](https://github.com/ReactiveX/RxAndroid) **(2.x)** +* [RxAndroid basics](https://medium.com/@kurtisnusbaum/rxandroid-basics-part-1-c0d5edcf6850) **(1.x & 2.x)** +* [RxJava and RxAndroid on AndroidHive](https://www.androidhive.info/RxJava/) **(1.x & 2.x)** +* [Reactive Programming with RxAndroid in Kotlin: An Introduction](https://www.raywenderlich.com/384-reactive-programming-with-rxandroid-in-kotlin-an-introduction) **(2.x)** +* [Difference between RxJava and RxAndroid](https://stackoverflow.com/questions/49651249/difference-between-rxjava-and-rxandroid) **(2.x)** +* [Reactive programming with RxAndroid](https://www.androidauthority.com/reactive-programming-with-rxandroid-711104/) **(1.x)** +* [RxJava - Vogella.com](http://www.vogella.com/tutorials/RxJava/article.html) **(2.x)** +* [Funcitional reactive Android](https://www.toptal.com/android/functional-reactive-android-rxjava) **(1.x)** +* [Reactive Programming with RxAndroid and Kotlin](https://www.pluralsight.com/courses/rxandroid-kotlin-reactive-programming) + +# Miscellany +* [RxJava Observables and Akka Actors](http://onoffswitch.net/rxjava-observables-akka-actors/) by Anton Kropp **(1.x & 2.x)** +* [Vert.x and RxJava](http://slid.es/petermd/eclipsecon2014) by @petermd **(1.x)** +* [RxJava in Different Flavours of Java](http://instil.co/2014/08/05/rxjava-in-different-flavours-of-java/): Java 7 and Java 8 implementations of the same code **(1.x)** diff --git a/docs/Alphabetical-List-of-Observable-Operators.md b/docs/Alphabetical-List-of-Observable-Operators.md new file mode 100644 index 0000000000..e5728356bc --- /dev/null +++ b/docs/Alphabetical-List-of-Observable-Operators.md @@ -0,0 +1,250 @@ +* **`aggregate( )`** — _see [**`reduce( )`**](https://github.com/ReactiveX/RxJava/wiki/Mathematical-and-Aggregate-Operators#reduce)_ +* [**`all( )`**](https://github.com/ReactiveX/RxJava/wiki/Conditional-and-Boolean-Operators#boolean-operators) — determine whether all items emitted by an Observable meet some criteria +* [**`amb( )`**](https://github.com/ReactiveX/RxJava/wiki/Conditional-and-Boolean-Operators#conditional-operators) — given two or more source Observables, emits all of the items from the first of these Observables to emit an item +* **`ambWith( )`** — _instance version of [**`amb( )`**](https://github.com/ReactiveX/RxJava/wiki/Conditional-and-Boolean-Operators#conditional-operators)_ +* [**`and( )`**](https://github.com/ReactiveX/RxJava/wiki/Combining-Observables#and-then-and-when) — combine the emissions from two or more source Observables into a `Pattern` (`rxjava-joins`) +* **`apply( )`** (scala) — _see [**`create( )`**](https://github.com/ReactiveX/RxJava/wiki/Creating-Observables#create)_ +* **`asObservable( )`** (kotlin) — _see [**`from( )`**](https://github.com/ReactiveX/RxJava/wiki/Creating-Observables#from) (et al.)_ +* [**`asyncAction( )`**](https://github.com/ReactiveX/RxJava/wiki/Async-Operators#toasync-or-asyncaction-or-asyncfunc) — convert an Action into an Observable that executes the Action and emits its return value (`rxjava-async`) +* [**`asyncFunc( )`**](https://github.com/ReactiveX/RxJava/wiki/Async-Operators#toasync-or-asyncaction-or-asyncfunc) — convert a function into an Observable that executes the function and emits its return value (`rxjava-async`) +* [**`averageDouble( )`**](https://github.com/ReactiveX/RxJava/wiki/Mathematical-and-Aggregate-Operators#averagedouble) — calculates the average of Doubles emitted by an Observable and emits this average (`rxjava-math`) +* [**`averageFloat( )`**](https://github.com/ReactiveX/RxJava/wiki/Mathematical-and-Aggregate-Operators#averagefloat) — calculates the average of Floats emitted by an Observable and emits this average (`rxjava-math`) +* [**`averageInteger( )`**](https://github.com/ReactiveX/RxJava/wiki/Mathematical-and-Aggregate-Operators) — calculates the average of Integers emitted by an Observable and emits this average (`rxjava-math`) +* [**`averageLong( )`**](https://github.com/ReactiveX/RxJava/wiki/Mathematical-and-Aggregate-Operators) — calculates the average of Longs emitted by an Observable and emits this average (`rxjava-math`) +* **`blocking( )`** (clojure) — _see [**`toBlocking( )`**](https://github.com/ReactiveX/RxJava/wiki/Blocking-Observable-Operators)_ +* [**`buffer( )`**](https://github.com/ReactiveX/RxJava/wiki/Transforming-Observables#buffer) — periodically gather items from an Observable into bundles and emit these bundles rather than emitting the items one at a time +* [**`byLine( )`**](https://github.com/ReactiveX/RxJava/wiki/String-Observables) (`StringObservable`) — converts an Observable of Strings into an Observable of Lines by treating the source sequence as a stream and splitting it on line-endings +* [**`cache( )`**](https://github.com/ReactiveX/RxJava/wiki/Observable-Utility-Operators) — remember the sequence of items emitted by the Observable and emit the same sequence to future Subscribers +* [**`cast( )`**](https://github.com/ReactiveX/RxJava/wiki/Transforming-Observables#cast) — cast all items from the source Observable into a particular type before reemitting them +* **`catch( )`** (clojure) — _see [**`onErrorResumeNext( )`**](https://github.com/ReactiveX/RxJava/wiki/Error-Handling-Operators#onerrorresumenext)_ +* [**`chunkify( )`**](https://github.com/ReactiveX/RxJava/wiki/Phantom-Operators#chunkify) — returns an iterable that periodically returns a list of items emitted by the source Observable since the last list (⁇) +* [**`collect( )`**](https://github.com/ReactiveX/RxJava/wiki/Mathematical-and-Aggregate-Operators#collect) — collects items emitted by the source Observable into a single mutable data structure and returns an Observable that emits this structure +* [**`combineLatest( )`**](https://github.com/ReactiveX/RxJava/wiki/Combining-Observables#combinelatest) — when an item is emitted by either of two Observables, combine the latest item emitted by each Observable via a specified function and emit items based on the results of this function +* **`combineLatestWith( )`** (scala) — _instance version of [**`combineLatest( )`**](https://github.com/ReactiveX/RxJava/wiki/Combining-Observables#combinelatest)_ +* [**`concat( )`**](http://reactivex.io/documentation/operators/concat.html) — concatenate two or more Observables sequentially +* [**`concatMap( )`**](https://github.com/ReactiveX/RxJava/wiki/Transforming-Observables#concatmap) — transform the items emitted by an Observable into Observables, then flatten this into a single Observable, without interleaving +* **`concatWith( )`** — _instance version of [**`concat( )`**](http://reactivex.io/documentation/operators/concat.html)_ +* [**`connect( )`**](https://github.com/ReactiveX/RxJava/wiki/Connectable-Observable-Operators) — instructs a Connectable Observable to begin emitting items +* **`cons( )`** (clojure) — _see [**`concat( )`**](http://reactivex.io/documentation/operators/concat.html)_ +* [**`contains( )`**](https://github.com/ReactiveX/RxJava/wiki/Conditional-and-Boolean-Operators#boolean-operators) — determine whether an Observable emits a particular item or not +* [**`count( )`**](https://github.com/ReactiveX/RxJava/wiki/Mathematical-and-Aggregate-Operators#count) — counts the number of items emitted by an Observable and emits this count +* [**`countLong( )`**](https://github.com/ReactiveX/RxJava/wiki/Mathematical-and-Aggregate-Operators#count) — counts the number of items emitted by an Observable and emits this count +* [**`create( )`**](https://github.com/ReactiveX/RxJava/wiki/Creating-Observables#create) — create an Observable from scratch by means of a function +* **`cycle( )`** (clojure) — _see [**`repeat( )`**](https://github.com/ReactiveX/RxJava/wiki/Creating-Observables)_ +* [**`debounce( )`**](https://github.com/ReactiveX/RxJava/wiki/Filtering-Observables#debounce) — only emit an item from the source Observable after a particular timespan has passed without the Observable emitting any other items +* [**`decode( )`**](https://github.com/ReactiveX/RxJava/wiki/String-Observables) (`StringObservable`) — convert a stream of multibyte characters into an Observable that emits byte arrays that respect character boundaries +* [**`defaultIfEmpty( )`**](https://github.com/ReactiveX/RxJava/wiki/Conditional-and-Boolean-Operators#conditional-operators) — emit items from the source Observable, or emit a default item if the source Observable completes after emitting no items +* [**`defer( )`**](https://github.com/ReactiveX/RxJava/wiki/Creating-Observables#defer) — do not create the Observable until a Subscriber subscribes; create a fresh Observable on each subscription +* [**`deferFuture( )`**](https://github.com/ReactiveX/RxJava/wiki/Async-Operators) — convert a Future that returns an Observable into an Observable, but do not attempt to get the Observable that the Future returns until a Subscriber subscribes (`rxjava-async`) +* [**`deferCancellableFuture( )`**](https://github.com/ReactiveX/RxJava/wiki/Phantom-Operators#fromcancellablefuture-startcancellablefuture-and-defercancellablefuture) — convert a Future that returns an Observable into an Observable in a way that monitors the subscription status of the Observable to determine whether to halt work on the Future, but do not attempt to get the returned Observable until a Subscriber subscribes (⁇)(`rxjava-async`) +* [**`delay( )`**](https://github.com/ReactiveX/RxJava/wiki/Observable-Utility-Operators) — shift the emissions from an Observable forward in time by a specified amount +* [**`dematerialize( )`**](https://github.com/ReactiveX/RxJava/wiki/Observable-Utility-Operators) — convert a materialized Observable back into its non-materialized form +* [**`distinct( )`**](https://github.com/ReactiveX/RxJava/wiki/Filtering-Observables#distinct) — suppress duplicate items emitted by the source Observable +* [**`distinctUntilChanged( )`**](https://github.com/ReactiveX/RxJava/wiki/Filtering-Observables#distinctuntilchanged) — suppress duplicate consecutive items emitted by the source Observable +* **`do( )`** (clojure) — _see [**`doOnEach( )`**](https://github.com/ReactiveX/RxJava/wiki/Observable-Utility-Operators)_ +* [**`doOnCompleted( )`**](https://github.com/ReactiveX/RxJava/wiki/Observable-Utility-Operators) — register an action to take when an Observable completes successfully +* [**`doOnEach( )`**](https://github.com/ReactiveX/RxJava/wiki/Observable-Utility-Operators) — register an action to take whenever an Observable emits an item +* [**`doOnError( )`**](https://github.com/ReactiveX/RxJava/wiki/Observable-Utility-Operators) — register an action to take when an Observable completes with an error +* **`doOnNext( )`** — _see [**`doOnEach( )`**](https://github.com/ReactiveX/RxJava/wiki/Observable-Utility-Operators)_ +* **`doOnRequest( )`** — register an action to take when items are requested from an Observable via reactive-pull backpressure (⁇) +* [**`doOnSubscribe( )`**](https://github.com/ReactiveX/RxJava/wiki/Observable-Utility-Operators) — register an action to take when an observer subscribes to an Observable +* [**`doOnTerminate( )`**](https://github.com/ReactiveX/RxJava/wiki/Observable-Utility-Operators) — register an action to take when an Observable completes, either successfully or with an error +* [**`doOnUnsubscribe( )`**](https://github.com/ReactiveX/RxJava/wiki/Observable-Utility-Operators) — register an action to take when an observer unsubscribes from an Observable +* [**`doWhile( )`**](https://github.com/ReactiveX/RxJava/wiki/Conditional-and-Boolean-Operators) — emit the source Observable's sequence, and then repeat the sequence as long as a condition remains true (`contrib-computation-expressions`) +* **`drop( )`** (scala/clojure) — _see [**`skip( )`**](https://github.com/ReactiveX/RxJava/wiki/Filtering-Observables#skip)_ +* **`dropRight( )`** (scala) — _see [**`skipLast( )`**](https://github.com/ReactiveX/RxJava/wiki/Filtering-Observables#skiplast)_ +* **`dropUntil( )`** (scala) — _see [**`skipUntil( )`**](https://github.com/ReactiveX/RxJava/wiki/Conditional-and-Boolean-Operators)_ +* **`dropWhile( )`** (scala) — _see [**`skipWhile( )`**](https://github.com/ReactiveX/RxJava/wiki/Conditional-and-Boolean-Operators)_ +* **`drop-while( )`** (clojure) — _see [**`skipWhile( )`**](https://github.com/ReactiveX/RxJava/wiki/Conditional-and-Boolean-Operators#skipwhile)_ +* [**`elementAt( )`**](https://github.com/ReactiveX/RxJava/wiki/Filtering-Observables#elementat) — emit item _n_ emitted by the source Observable +* [**`elementAtOrDefault( )`**](https://github.com/ReactiveX/RxJava/wiki/Filtering-Observables) — emit item _n_ emitted by the source Observable, or a default item if the source Observable emits fewer than _n_ items +* [**`empty( )`**](https://github.com/ReactiveX/RxJava/wiki/Creating-Observables#empty) — create an Observable that emits nothing and then completes +* [**`encode( )`**](https://github.com/ReactiveX/RxJava/wiki/String-Observables) (`StringObservable`) — transform an Observable that emits strings into an Observable that emits byte arrays that respect character boundaries of multibyte characters in the original strings +* [**`error( )`**](https://github.com/ReactiveX/RxJava/wiki/Creating-Observables#error) — create an Observable that emits nothing and then signals an error +* **`every( )`** (clojure) — _see [**`all( )`**](https://github.com/ReactiveX/RxJava/wiki/Conditional-and-Boolean-Operators#boolean-operators)_ +* [**`exists( )`**](https://github.com/ReactiveX/RxJava/wiki/Conditional-and-Boolean-Operators#boolean-operators) — determine whether an Observable emits any items or not +* [**`filter( )`**](https://github.com/ReactiveX/RxJava/wiki/Filtering-Observables#filter) — filter items emitted by an Observable +* **`finally( )`** (clojure) — _see [**`finallyDo( )`**](https://github.com/ReactiveX/RxJava/wiki/Observable-Utility-Operators)_ +* **`filterNot( )`** (scala) — _see [**`filter( )`**](https://github.com/ReactiveX/RxJava/wiki/Filtering-Observables#filter)_ +* [**`finallyDo( )`**](https://github.com/ReactiveX/RxJava/wiki/Observable-Utility-Operators) — register an action to take when an Observable completes +* [**`first( )`**](https://github.com/ReactiveX/RxJava/wiki/Filtering-Observables#first) (`Observable`) — emit only the first item emitted by an Observable, or the first item that meets some condition +* [**`first( )`**](https://github.com/ReactiveX/RxJava/wiki/Blocking-Observable-Operators) (`BlockingObservable`) — emit only the first item emitted by an Observable, or the first item that meets some condition +* [**`firstOrDefault( )`**](https://github.com/ReactiveX/RxJava/wiki/Filtering-Observables) (`Observable`) — emit only the first item emitted by an Observable, or the first item that meets some condition, or a default value if the source Observable is empty +* [**`firstOrDefault( )`**](https://github.com/ReactiveX/RxJava/wiki/Blocking-Observable-Operators) (`BlockingObservable`) — emit only the first item emitted by an Observable, or the first item that meets some condition, or a default value if the source Observable is empty +* **`firstOrElse( )`** (scala) — _see [**`firstOrDefault( )`**](https://github.com/ReactiveX/RxJava/wiki/Filtering-Observables) or [**`firstOrDefault( )`**](https://github.com/ReactiveX/RxJava/wiki/Blocking-Observable-Operators) (`BlockingObservable`)_ +* [**`flatMap( )`**](https://github.com/ReactiveX/RxJava/wiki/Transforming-Observables#flatmap) — transform the items emitted by an Observable into Observables, then flatten this into a single Observable +* [**`flatMapIterable( )`**](https://github.com/ReactiveX/RxJava/wiki/Transforming-Observables#flatmapiterable) — create Iterables corresponding to each emission from a source Observable and merge the results into a single Observable +* **`flatMapIterableWith( )`** (scala) — _instance version of [**`flatMapIterable( )`**](https://github.com/ReactiveX/RxJava/wiki/Transforming-Observables#flatmapiterable)_ +* **`flatMapWith( )`** (scala) — _instance version of [**`flatmap( )`**](https://github.com/ReactiveX/RxJava/wiki/Transforming-Observables#flatmap)_ +* **`flatten( )`** (scala) — _see [**`merge( )`**](https://github.com/ReactiveX/RxJava/wiki/Combining-Observables#merge)_ +* **`flattenDelayError( )`** (scala) — _see [**`mergeDelayError( )`**](https://github.com/ReactiveX/RxJava/wiki/Combining-Observables#mergedelayerror)_ +* **`foldLeft( )`** (scala) — _see [**`reduce( )`**](https://github.com/ReactiveX/RxJava/wiki/Mathematical-and-Aggregate-Operators#reduce)_ +* **`forall( )`** (scala) — _see [**`all( )`**](https://github.com/ReactiveX/RxJava/wiki/Conditional-and-Boolean-Operators#boolean-operators)_ +* **`forEach( )`** (`Observable`) — _see [**`subscribe( )`**](https://github.com/ReactiveX/RxJava/wiki/Observable)_ +* [**`forEach( )`**](https://github.com/ReactiveX/RxJava/wiki/Blocking-Observable-Operators) (`BlockingObservable`) — invoke a function on each item emitted by the Observable; block until the Observable completes +* [**`forEachFuture( )`**](https://github.com/ReactiveX/RxJava/wiki/Async-Operators) (`Async`) — pass Subscriber methods to an Observable but also have it behave like a Future that blocks until it completes (`rxjava-async`) +* [**`forEachFuture( )`**](https://github.com/ReactiveX/RxJava/wiki/Phantom-Operators#foreachfuture) (`BlockingObservable`)— create a futureTask that will invoke a specified function on each item emitted by an Observable (⁇) +* [**`forIterable( )`**](https://github.com/ReactiveX/RxJava/wiki/Phantom-Operators#foriterable) — apply a function to the elements of an Iterable to create Observables which are then concatenated (⁇) +* [**`from( )`**](https://github.com/ReactiveX/RxJava/wiki/Creating-Observables#from) — convert an Iterable, a Future, or an Array into an Observable +* [**`from( )`**](https://github.com/ReactiveX/RxJava/wiki/String-Observables) (`StringObservable`) — convert a stream of characters or a Reader into an Observable that emits byte arrays or Strings +* [**`fromAction( )`**](https://github.com/ReactiveX/RxJava/wiki/Async-Operators) — convert an Action into an Observable that invokes the action and emits its result when a Subscriber subscribes (`rxjava-async`) +* [**`fromCallable( )`**](https://github.com/ReactiveX/RxJava/wiki/Async-Operators) — convert a Callable into an Observable that invokes the callable and emits its result or exception when a Subscriber subscribes (`rxjava-async`) +* [**`fromCancellableFuture( )`**](https://github.com/ReactiveX/RxJava/wiki/Phantom-Operators#fromcancellablefuture-startcancellablefuture-and-defercancellablefuture) — convert a Future into an Observable in a way that monitors the subscription status of the Observable to determine whether to halt work on the Future, but do not attempt to get the Future's value until a Subscriber subscribes (⁇)(`rxjava-async`) +* **`fromFunc0( )`** — _see [**`fromCallable( )`**](https://github.com/ReactiveX/RxJava/wiki/Async-Operators) (`rxjava-async`)_ +* [**`fromFuture( )`**](https://github.com/ReactiveX/RxJava/wiki/Phantom-Operators#fromfuture) — convert a Future into an Observable, but do not attempt to get the Future's value until a Subscriber subscribes (⁇) +* [**`fromRunnable( )`**](https://github.com/ReactiveX/RxJava/wiki/Async-Operators#fromrunnable) — convert a Runnable into an Observable that invokes the runable and emits its result when a Subscriber subscribes (`rxjava-async`) +* [**`generate( )`**](https://github.com/ReactiveX/RxJava/wiki/Phantom-Operators#generate-and-generateabsolutetime) — create an Observable that emits a sequence of items as generated by a function of your choosing (⁇) +* [**`generateAbsoluteTime( )`**](https://github.com/ReactiveX/RxJava/wiki/Phantom-Operators#generate-and-generateabsolutetime) — create an Observable that emits a sequence of items as generated by a function of your choosing, with each item emitted at an item-specific time (⁇) +* **`generator( )`** (clojure) — _see [**`generate( )`**](https://github.com/ReactiveX/RxJava/wiki/Phantom-Operators#generate-and-generateabsolutetime)_ +* [**`getIterator( )`**](https://github.com/ReactiveX/RxJava/wiki/Blocking-Observable-Operators) — convert the sequence emitted by the Observable into an Iterator +* [**`groupBy( )`**](https://github.com/ReactiveX/RxJava/wiki/Transforming-Observables#groupby) — divide an Observable into a set of Observables that emit groups of items from the original Observable, organized by key +* **`group-by( )`** (clojure) — _see [**`groupBy( )`**](https://github.com/ReactiveX/RxJava/wiki/Transforming-Observables#groupby)_ +* [**`groupByUntil( )`**](https://github.com/ReactiveX/RxJava/wiki/Phantom-Operators) — a variant of the [`groupBy( )`](https://github.com/ReactiveX/RxJava/wiki/Transforming-Observables#groupby) operator that closes any open GroupedObservable upon a signal from another Observable (⁇) +* [**`groupJoin( )`**](https://github.com/ReactiveX/RxJava/wiki/Combining-Observables#joins) — combine the items emitted by two Observables whenever one item from one Observable falls within a window of duration specified by an item emitted by the other Observable +* **`head( )`** (scala) — _see [**`first( )`**](https://github.com/ReactiveX/RxJava/wiki/Blocking-Observable-Operators) (`BlockingObservable`)_ +* **`headOption( )`** (scala) — _see [**`firstOrDefault( )`**](https://github.com/ReactiveX/RxJava/wiki/Filtering-Observables) or [**`firstOrDefault( )`**](https://github.com/ReactiveX/RxJava/wiki/Blocking-Observable-Operators) (`BlockingObservable`)_ +* **`headOrElse( )`** (scala) — _see [**`firstOrDefault( )`**](https://github.com/ReactiveX/RxJava/wiki/Filtering-Observables) or [**`firstOrDefault( )`**](https://github.com/ReactiveX/RxJava/wiki/Blocking-Observable-Operators) (`BlockingObservable`)_ +* [**`ifThen( )`**](https://github.com/ReactiveX/RxJava/wiki/Conditional-and-Boolean-Operators#conditional-operators) — only emit the source Observable's sequence if a condition is true, otherwise emit an empty or default sequence (`contrib-computation-expressions`) +* [**`ignoreElements( )`**](https://github.com/ReactiveX/RxJava/wiki/Filtering-Observables#ignoreelements) — discard the items emitted by the source Observable and only pass through the error or completed notification +* [**`interval( )`**](https://github.com/ReactiveX/RxJava/wiki/Creating-Observables#interval) — create an Observable that emits a sequence of integers spaced by a given time interval +* **`into( )`** (clojure) — _see [**`reduce( )`**](https://github.com/ReactiveX/RxJava/wiki/Mathematical-and-Aggregate-Operators#reduce)_ +* [**`isEmpty( )`**](https://github.com/ReactiveX/RxJava/wiki/Conditional-and-Boolean-Operators#boolean-operators) — determine whether an Observable emits any items or not +* **`items( )`** (scala) — _see [**`just( )`**](https://github.com/ReactiveX/RxJava/wiki/Creating-Observables#just)_ +* [**`join( )`**](https://github.com/ReactiveX/RxJava/wiki/Combining-Observables#joins) — combine the items emitted by two Observables whenever one item from one Observable falls within a window of duration specified by an item emitted by the other Observable +* [**`join( )`**](https://github.com/ReactiveX/RxJava/wiki/String-Observables) (`StringObservable`) — converts an Observable that emits a sequence of strings into an Observable that emits a single string that concatenates them all, separating them by a specified string +* [**`just( )`**](https://github.com/ReactiveX/RxJava/wiki/Creating-Observables#just) — convert an object into an Observable that emits that object +* [**`last( )`**](https://github.com/ReactiveX/RxJava/wiki/Blocking-Observable-Operators) (`BlockingObservable`) — block until the Observable completes, then return the last item emitted by the Observable +* [**`last( )`**](https://github.com/ReactiveX/RxJava/wiki/Filtering-Observables#last) (`Observable`) — emit only the last item emitted by the source Observable +* **`lastOption( )`** (scala) — _see [**`lastOrDefault( )`**](https://github.com/ReactiveX/RxJava/wiki/Filtering-Observables) or [**`lastOrDefault( )`**](https://github.com/ReactiveX/RxJava/wiki/Blocking-Observable-Operators) (`BlockingObservable`)_ +* [**`lastOrDefault( )`**](https://github.com/ReactiveX/RxJava/wiki/Blocking-Observable-Operators) (`BlockingObservable`) — block until the Observable completes, then return the last item emitted by the Observable or a default item if there is no last item +* [**`lastOrDefault( )`**](https://github.com/ReactiveX/RxJava/wiki/Filtering-Observables) (`Observable`) — emit only the last item emitted by an Observable, or a default value if the source Observable is empty +* **`lastOrElse( )`** (scala) — _see [**`lastOrDefault( )`**](https://github.com/ReactiveX/RxJava/wiki/Filtering-Observables) or [**`lastOrDefault( )`**](https://github.com/ReactiveX/RxJava/wiki/Blocking-Observable-Operators) (`BlockingObservable`)_ +* [**`latest( )`**](https://github.com/ReactiveX/RxJava/wiki/Blocking-Observable-Operators) — returns an iterable that blocks until or unless the Observable emits an item that has not been returned by the iterable, then returns the latest such item +* **`length( )`** (scala) — _see [**`count( )`**](https://github.com/ReactiveX/RxJava/wiki/Mathematical-and-Aggregate-Operators#count)_ +* **`limit( )`** — _see [**`take( )`**](https://github.com/ReactiveX/RxJava/wiki/Filtering-Observables#take)_ +* **`longCount( )`** (scala) — _see [**`countLong( )`**](https://github.com/ReactiveX/RxJava/wiki/Mathematical-and-Aggregate-Operators)_ +* [**`map( )`**](https://github.com/ReactiveX/RxJava/wiki/Transforming-Observables#map) — transform the items emitted by an Observable by applying a function to each of them +* **`mapcat( )`** (clojure) — _see [**`concatMap( )`**](https://github.com/ReactiveX/RxJava/wiki/Transforming-Observables#concatmap)_ +* **`mapMany( )`** — _see: [**`flatMap( )`**](https://github.com/ReactiveX/RxJava/wiki/Transforming-Observables#flatmap)_ +* [**`materialize( )`**](https://github.com/ReactiveX/RxJava/wiki/Observable-Utility-Operators) — convert an Observable into a list of Notifications +* [**`max( )`**](https://github.com/ReactiveX/RxJava/wiki/Mathematical-and-Aggregate-Operators#max) — emits the maximum value emitted by a source Observable (`rxjava-math`) +* [**`maxBy( )`**](https://github.com/ReactiveX/RxJava/wiki/Mathematical-and-Aggregate-Operators) — emits the item emitted by the source Observable that has the maximum key value (`rxjava-math`) +* [**`merge( )`**](https://github.com/ReactiveX/RxJava/wiki/Combining-Observables#merge) — combine multiple Observables into one +* [**`mergeDelayError( )`**](https://github.com/ReactiveX/RxJava/wiki/Combining-Observables#mergedelayerror) — combine multiple Observables into one, allowing error-free Observables to continue before propagating errors +* **`merge-delay-error( )`** (clojure) — _see [**`mergeDelayError( )`**](https://github.com/ReactiveX/RxJava/wiki/Combining-Observables#mergedelayerror)_ +* **`mergeMap( )`** * — _see: [**`flatMap( )`**](https://github.com/ReactiveX/RxJava/wiki/Transforming-Observables#flatmap)_ +* **`mergeMapIterable( )`** — _see: [**`flatMapIterable( )`**](https://github.com/ReactiveX/RxJava/wiki/Transforming-Observables#flatmapiterable)_ +* **`mergeWith( )`** — _instance version of [**`merge( )`**](https://github.com/ReactiveX/RxJava/wiki/Combining-Observables#merge)_ +* [**`min( )`**](https://github.com/ReactiveX/RxJava/wiki/Mathematical-and-Aggregate-Operators#min) — emits the minimum value emitted by a source Observable (`rxjava-math`) +* [**`minBy( )`**](https://github.com/ReactiveX/RxJava/wiki/Mathematical-and-Aggregate-Operators) — emits the item emitted by the source Observable that has the minimum key value (`rxjava-math`) +* [**`mostRecent( )`**](https://github.com/ReactiveX/RxJava/wiki/Blocking-Observable-Operators) — returns an iterable that always returns the item most recently emitted by the Observable +* [**`multicast( )`**](https://github.com/ReactiveX/RxJava/wiki/Phantom-Operators#multicast) — represents an Observable as a Connectable Observable +* [**`never( )`**](https://github.com/ReactiveX/RxJava/wiki/Creating-Observables#never) — create an Observable that emits nothing at all +* [**`next( )`**](https://github.com/ReactiveX/RxJava/wiki/Blocking-Observable-Operators) — returns an iterable that blocks until the Observable emits another item, then returns that item +* **`nonEmpty( )`** (scala) — _see [**`isEmpty( )`**](https://github.com/ReactiveX/RxJava/wiki/Conditional-and-Boolean-Operators#boolean-operators)_ +* **`nth( )`** (clojure) — _see [**`elementAt( )`**](https://github.com/ReactiveX/RxJava/wiki/Filtering-Observables#elementat) and [**`elementAtOrDefault( )`**](https://github.com/ReactiveX/RxJava/wiki/Filtering-Observables)_ +* [**`observeOn( )`**](https://github.com/ReactiveX/RxJava/wiki/Observable-Utility-Operators) — specify on which Scheduler a Subscriber should observe the Observable +* [**`ofType( )`**](https://github.com/ReactiveX/RxJava/wiki/Filtering-Observables#oftype) — emit only those items from the source Observable that are of a particular class +* [**`onBackpressureBlock( )`**](https://github.com/ReactiveX/RxJava/wiki/Backpressure#reactive-pull-backpressure-isnt-magic) — block the Observable's thread until the Observer is ready to accept more items from the Observable (⁇) +* [**`onBackpressureBuffer( )`**](https://github.com/ReactiveX/RxJava/wiki/Backpressure#reactive-pull-backpressure-isnt-magic) — maintain a buffer of all emissions from the source Observable and emit them to downstream Subscribers according to the requests they generate +* [**`onBackpressureDrop( )`**](https://github.com/ReactiveX/RxJava/wiki/Backpressure#reactive-pull-backpressure-isnt-magic) — drop emissions from the source Observable unless there is a pending request from a downstream Subscriber, in which case emit enough items to fulfill the request +* [**`onErrorFlatMap( )`**](https://github.com/ReactiveX/RxJava/wiki/Phantom-Operators#onerrorflatmap) — instructs an Observable to emit a sequence of items whenever it encounters an error (⁇) +* [**`onErrorResumeNext( )`**](https://github.com/ReactiveX/RxJava/wiki/Error-Handling-Operators#onerrorresumenext) — instructs an Observable to emit a sequence of items if it encounters an error +* [**`onErrorReturn( )`**](https://github.com/ReactiveX/RxJava/wiki/Error-Handling-Operators#onerrorreturn) — instructs an Observable to emit a particular item when it encounters an error +* [**`onExceptionResumeNext( )`**](https://github.com/ReactiveX/RxJava/wiki/Error-Handling-Operators#onexceptionresumenext) — instructs an Observable to continue emitting items after it encounters an exception (but not another variety of throwable) +* **`orElse( )`** (scala) — _see [**`defaultIfEmpty( )`**](https://github.com/ReactiveX/RxJava/wiki/Conditional-and-Boolean-Operators#conditional-operators)_ +* [**`parallel( )`**](https://github.com/ReactiveX/RxJava/wiki/Phantom-Operators#parallel) — split the work done on the emissions from an Observable into multiple Observables each operating on its own parallel thread (⁇) +* [**`parallelMerge( )`**](https://github.com/ReactiveX/RxJava/wiki/Phantom-Operators#parallelmerge) — combine multiple Observables into smaller number of Observables (⁇) +* [**`pivot( )`**](https://github.com/ReactiveX/RxJava/wiki/Phantom-Operators#pivot) — combine multiple sets of grouped observables so that they are arranged primarily by group rather than by set (⁇) +* [**`publish( )`**](https://github.com/ReactiveX/RxJava/wiki/Connectable-Observable-Operators) — represents an Observable as a Connectable Observable +* [**`publishLast( )`**](https://github.com/ReactiveX/RxJava/wiki/Phantom-Operators#publishlast) — represent an Observable as a Connectable Observable that emits only the last item emitted by the source Observable (⁇) +* [**`range( )`**](https://github.com/ReactiveX/RxJava/wiki/Creating-Observables#range) — create an Observable that emits a range of sequential integers +* [**`reduce( )`**](https://github.com/ReactiveX/RxJava/wiki/Mathematical-and-Aggregate-Operators#reduce) — apply a function to each emitted item, sequentially, and emit only the final accumulated value +* **`reductions( )`** (clojure) — _see [**`scan( )`**](https://github.com/ReactiveX/RxJava/wiki/Transforming-Observables#scan)_ +* [**`refCount( )`**](https://github.com/ReactiveX/RxJava/wiki/Connectable-Observable-Operators) — makes a Connectable Observable behave like an ordinary Observable +* [**`repeat( )`**](https://github.com/ReactiveX/RxJava/wiki/Creating-Observables) — create an Observable that emits a particular item or sequence of items repeatedly +* [**`repeatWhen( )`**](https://github.com/ReactiveX/RxJava/wiki/Creating-Observables#repeatwhen) — create an Observable that emits a particular item or sequence of items repeatedly, depending on the emissions of a second Observable +* [**`replay( )`**](https://github.com/ReactiveX/RxJava/wiki/Connectable-Observable-Operators#observablereplay) — ensures that all Subscribers see the same sequence of emitted items, even if they subscribe after the Observable begins emitting the items +* **`rest( )`** (clojure) — _see [**`next( )`**](https://github.com/ReactiveX/RxJava/wiki/Blocking-Observable-Operators#next)_ +* **`return( )`** (clojure) — _see [**`just( )`**](https://github.com/ReactiveX/RxJava/wiki/Creating-Observables#just)_ +* [**`retry( )`**](https://github.com/ReactiveX/RxJava/wiki/Error-Handling-Operators#retry) — if a source Observable emits an error, resubscribe to it in the hopes that it will complete without error +* [**`retrywhen( )`**](https://github.com/ReactiveX/RxJava/wiki/Error-Handling-Operators#retrywhen) — if a source Observable emits an error, pass that error to another Observable to determine whether to resubscribe to the source +* [**`runAsync( )`**](https://github.com/ReactiveX/RxJava/wiki/Async-Operators) — returns a `StoppableObservable` that emits multiple actions as generated by a specified Action on a Scheduler (`rxjava-async`) +* [**`sample( )`**](https://github.com/ReactiveX/RxJava/wiki/Filtering-Observables#sample) — emit the most recent items emitted by an Observable within periodic time intervals +* [**`scan( )`**](https://github.com/ReactiveX/RxJava/wiki/Transforming-Observables#scan) — apply a function to each item emitted by an Observable, sequentially, and emit each successive value +* **`seq( )`** (clojure) — _see [**`getIterator( )`**](https://github.com/ReactiveX/RxJava/wiki/Blocking-Observable-Operators)_ +* [**`sequenceEqual( )`**](https://github.com/ReactiveX/RxJava/wiki/Conditional-and-Boolean-Operators) — test the equality of sequences emitted by two Observables +* **`sequenceEqualWith( )`** (scala) — _instance version of [**`sequenceEqual( )`**](https://github.com/ReactiveX/RxJava/wiki/Conditional-and-Boolean-Operators)_ +* [**`serialize( )`**](https://github.com/ReactiveX/RxJava/wiki/Observable-Utility-Operators#serialize) — force an Observable to make serialized calls and to be well-behaved +* **`share( )`** — _see [**`refCount( )`**](https://github.com/ReactiveX/RxJava/wiki/Connectable-Observable-Operators)_ +* [**`single( )`**](https://github.com/ReactiveX/RxJava/wiki/Blocking-Observable-Operators) (`BlockingObservable`) — if the source Observable completes after emitting a single item, return that item, otherwise throw an exception +* [**`single( )`**](https://github.com/ReactiveX/RxJava/wiki/Observable-Utility-Operators) (`Observable`) — if the source Observable completes after emitting a single item, emit that item, otherwise notify of an exception +* **`singleOption( )`** (scala) — _see [**`singleOrDefault( )`**](https://github.com/ReactiveX/RxJava/wiki/Blocking-Observable-Operators#single-and-singleordefault) (`BlockingObservable`)_ +* [**`singleOrDefault( )`**](https://github.com/ReactiveX/RxJava/wiki/Blocking-Observable-Operators) (`BlockingObservable`) — if the source Observable completes after emitting a single item, return that item, otherwise return a default item +* [**`singleOrDefault( )`**](https://github.com/ReactiveX/RxJava/wiki/Observable-Utility-Operators) (`Observable`) — if the source Observable completes after emitting a single item, emit that item, otherwise emit a default item +* **`singleOrElse( )`** (scala) — _see [**`singleOrDefault( )`**](https://github.com/ReactiveX/RxJava/wiki/Observable-Utility-Operators)_ +* **`size( )`** (scala) — _see [**`count( )`**](https://github.com/ReactiveX/RxJava/wiki/Mathematical-and-Aggregate-Operators#count)_ +* [**`skip( )`**](https://github.com/ReactiveX/RxJava/wiki/Filtering-Observables#skip) — ignore the first _n_ items emitted by an Observable +* [**`skipLast( )`**](https://github.com/ReactiveX/RxJava/wiki/Filtering-Observables#skiplast) — ignore the last _n_ items emitted by an Observable +* [**`skipUntil( )`**](https://github.com/ReactiveX/RxJava/wiki/Conditional-and-Boolean-Operators#conditional-operators) — discard items emitted by a source Observable until a second Observable emits an item, then emit the remainder of the source Observable's items +* [**`skipWhile( )`**](https://github.com/ReactiveX/RxJava/wiki/Conditional-and-Boolean-Operators#conditional-operators) — discard items emitted by an Observable until a specified condition is false, then emit the remainder +* **`sliding( )`** (scala) — _see [**`window( )`**](https://github.com/ReactiveX/RxJava/wiki/Transforming-Observables#window)_ +* **`slidingBuffer( )`** (scala) — _see [**`buffer( )`**](https://github.com/ReactiveX/RxJava/wiki/Transforming-Observables#buffer)_ +* [**`split( )`**](https://github.com/ReactiveX/RxJava/wiki/String-Observables) (`StringObservable`) — converts an Observable of Strings into an Observable of Strings that treats the source sequence as a stream and splits it on a specified regex boundary +* [**`start( )`**](https://github.com/ReactiveX/RxJava/wiki/Async-Operators) — create an Observable that emits the return value of a function (`rxjava-async`) +* [**`startCancellableFuture( )`**](https://github.com/ReactiveX/RxJava/wiki/Phantom-Operators#fromcancellablefuture-startcancellablefuture-and-defercancellablefuture) — convert a function that returns Future into an Observable that emits that Future's return value in a way that monitors the subscription status of the Observable to determine whether to halt work on the Future (⁇)(`rxjava-async`) +* [**`startFuture( )`**](https://github.com/ReactiveX/RxJava/wiki/Async-Operators) — convert a function that returns Future into an Observable that emits that Future's return value (`rxjava-async`) +* [**`startWith( )`**](https://github.com/ReactiveX/RxJava/wiki/Combining-Observables#startwith) — emit a specified sequence of items before beginning to emit the items from the Observable +* [**`stringConcat( )`**](https://github.com/ReactiveX/RxJava/wiki/String-Observables) (`StringObservable`) — converts an Observable that emits a sequence of strings into an Observable that emits a single string that concatenates them all +* [**`subscribeOn( )`**](https://github.com/ReactiveX/RxJava/wiki/Observable-Utility-Operators) — specify which Scheduler an Observable should use when its subscription is invoked +* [**`sumDouble( )`**](https://github.com/ReactiveX/RxJava/wiki/Mathematical-and-Aggregate-Operators#sumdouble) — adds the Doubles emitted by an Observable and emits this sum (`rxjava-math`) +* [**`sumFloat( )`**](https://github.com/ReactiveX/RxJava/wiki/Mathematical-and-Aggregate-Operators#sumfloat) — adds the Floats emitted by an Observable and emits this sum (`rxjava-math`) +* [**`sumInt( )`**](https://github.com/ReactiveX/RxJava/wiki/Mathematical-and-Aggregate-Operators#sumint) — adds the Integers emitted by an Observable and emits this sum (`rxjava-math`) +* [**`sumLong( )`**](https://github.com/ReactiveX/RxJava/wiki/Mathematical-and-Aggregate-Operators#sumlong) — adds the Longs emitted by an Observable and emits this sum (`rxjava-math`) +* **`switch( )`** (scala) — _see [**`switchOnNext( )`**](https://github.com/ReactiveX/RxJava/wiki/Combining-Observables#switchonnext)_ +* [**`switchCase( )`**](https://github.com/ReactiveX/RxJava/wiki/Conditional-and-Boolean-Operators#conditional-operators) — emit the sequence from a particular Observable based on the results of an evaluation (`contrib-computation-expressions`) +* [**`switchMap( )`**](https://github.com/ReactiveX/RxJava/wiki/Transforming-Observables#switchmap) — transform the items emitted by an Observable into Observables, and mirror those items emitted by the most-recently transformed Observable +* [**`switchOnNext( )`**](https://github.com/ReactiveX/RxJava/wiki/Combining-Observables#switchonnext) — convert an Observable that emits Observables into a single Observable that emits the items emitted by the most-recently emitted of those Observables +* **`synchronize( )`** — _see [**`serialize( )`**](https://github.com/ReactiveX/RxJava/wiki/Observable-Utility-Operators)_ +* [**`take( )`**](https://github.com/ReactiveX/RxJava/wiki/Filtering-Observables#take) — emit only the first _n_ items emitted by an Observable +* [**`takeFirst( )`**](https://github.com/ReactiveX/RxJava/wiki/Filtering-Observables) — emit only the first item emitted by an Observable, or the first item that meets some condition +* [**`takeLast( )`**](https://github.com/ReactiveX/RxJava/wiki/Filtering-Observables#takelast) — only emit the last _n_ items emitted by an Observable +* [**`takeLastBuffer( )`**](https://github.com/ReactiveX/RxJava/wiki/Filtering-Observables) — emit the last _n_ items emitted by an Observable, as a single list item +* **`takeRight( )`** (scala) — _see [**`last( )`**](https://github.com/ReactiveX/RxJava/wiki/Filtering-Observables#last) (`Observable`) or [**`takeLast( )`**](https://github.com/ReactiveX/RxJava/wiki/Filtering-Observables#takelast)_ +* [**`takeUntil( )`**](https://github.com/ReactiveX/RxJava/wiki/Conditional-and-Boolean-Operators#conditional-operators) — emits the items from the source Observable until a second Observable emits an item +* [**`takeWhile( )`**](https://github.com/ReactiveX/RxJava/wiki/Conditional-and-Boolean-Operators#conditional-operators) — emit items emitted by an Observable as long as a specified condition is true, then skip the remainder +* **`take-while( )`** (clojure) — _see [**`takeWhile( )`**](https://github.com/ReactiveX/RxJava/wiki/Conditional-and-Boolean-Operators#conditional-operators)_ +* [**`then( )`**](https://github.com/ReactiveX/RxJava/wiki/Combining-Observables#rxjava-joins) — transform a series of `Pattern` objects via a `Plan` template (`rxjava-joins`) +* [**`throttleFirst( )`**](https://github.com/ReactiveX/RxJava/wiki/Filtering-Observables#throttlefirst) — emit the first items emitted by an Observable within periodic time intervals +* [**`throttleLast( )`**](https://github.com/ReactiveX/RxJava/wiki/Filtering-Observables#throttlelast) — emit the most recent items emitted by an Observable within periodic time intervals +* [**`throttleWithTimeout( )`**](https://github.com/ReactiveX/RxJava/wiki/Filtering-Observables#throttlewithtimeout) — only emit an item from the source Observable after a particular timespan has passed without the Observable emitting any other items +* **`throw( )`** (clojure) — _see [**`error( )`**](https://github.com/ReactiveX/RxJava/wiki/Creating-Observables#error)_ +* [**`timeInterval( )`**](https://github.com/ReactiveX/RxJava/wiki/Observable-Utility-Operators) — emit the time lapsed between consecutive emissions of a source Observable +* [**`timeout( )`**](https://github.com/ReactiveX/RxJava/wiki/Filtering-Observables#timeout) — emit items from a source Observable, but issue an exception if no item is emitted in a specified timespan +* [**`timer( )`**](https://github.com/ReactiveX/RxJava/wiki/Creating-Observables#timer) — create an Observable that emits a single item after a given delay +* [**`timestamp( )`**](https://github.com/ReactiveX/RxJava/wiki/Observable-Utility-Operators) — attach a timestamp to every item emitted by an Observable +* [**`toAsync( )`**](https://github.com/ReactiveX/RxJava/wiki/Async-Operators) — convert a function or Action into an Observable that executes the function and emits its return value (`rxjava-async`) +* [**`toBlocking( )`**](https://github.com/ReactiveX/RxJava/wiki/Blocking-Observable-Operators) — transform an Observable into a BlockingObservable +* **`toBlockingObservable( )`** - _see [**`toBlocking( )`**](https://github.com/ReactiveX/RxJava/wiki/Blocking-Observable-Operators)_ +* [**`toFuture( )`**](https://github.com/ReactiveX/RxJava/wiki/Blocking-Observable-Operators) — convert the Observable into a Future +* [**`toIterable( )`**](https://github.com/ReactiveX/RxJava/wiki/Blocking-Observable-Operators) — convert the sequence emitted by the Observable into an Iterable +* **`toIterator( )`** — _see [**`getIterator( )`**](https://github.com/ReactiveX/RxJava/wiki/Blocking-Observable-Operators)_ +* [**`toList( )`**](https://github.com/ReactiveX/RxJava/wiki/Mathematical-and-Aggregate-Operators#tolist) — collect all items from an Observable and emit them as a single List +* [**`toMap( )`**](https://github.com/ReactiveX/RxJava/wiki/Mathematical-and-Aggregate-Operators#tomap) — convert the sequence of items emitted by an Observable into a map keyed by a specified key function +* [**`toMultimap( )`**](https://github.com/ReactiveX/RxJava/wiki/Mathematical-and-Aggregate-Operators#tomultimap) — convert the sequence of items emitted by an Observable into an ArrayList that is also a map keyed by a specified key function +* **`toSeq( )`** (scala) — _see [**`toList( )`**](https://github.com/ReactiveX/RxJava/wiki/Mathematical-and-Aggregate-Operators#tolist)_ +* [**`toSortedList( )`**](https://github.com/ReactiveX/RxJava/wiki/Mathematical-and-Aggregate-Operators#tosortedlist) — collect all items from an Observable and emit them as a single, sorted List +* **`tumbling( )`** (scala) — _see [**`window( )`**](https://github.com/ReactiveX/RxJava/wiki/Transforming-Observables#window)_ +* **`tumblingBuffer( )`** (scala) — _see [**`buffer( )`**](https://github.com/ReactiveX/RxJava/wiki/Transforming-Observables#buffer)_ +* [**`using( )`**](https://github.com/ReactiveX/RxJava/wiki/Observable-Utility-Operators) — create a disposable resource that has the same lifespan as an Observable +* [**`when( )`**](https://github.com/ReactiveX/RxJava/wiki/Combining-Observables#rxjava-joins) — convert a series of `Plan` objects into an Observable (`rxjava-joins`) +* **`where( )`** — _see: [**`filter( )`**](https://github.com/ReactiveX/RxJava/wiki/Filtering-Observables#filter)_ +* [**`whileDo( )`**](https://github.com/ReactiveX/RxJava/wiki/Conditional-and-Boolean-Operators#conditional-operators) — if a condition is true, emit the source Observable's sequence and then repeat the sequence as long as the condition remains true (`contrib-computation-expressions`) +* [**`window( )`**](https://github.com/ReactiveX/RxJava/wiki/Transforming-Observables#window) — periodically subdivide items from an Observable into Observable windows and emit these windows rather than emitting the items one at a time +* [**`zip( )`**](https://github.com/ReactiveX/RxJava/wiki/Combining-Observables#zip) — combine sets of items emitted by two or more Observables together via a specified function and emit items based on the results of this function +* **`zipWith( )`** — _instance version of [**`zip( )`**](https://github.com/ReactiveX/RxJava/wiki/Combining-Observables#zip)_ +* **`zipWithIndex( )`** (scala) — _see [**`zip( )`**](https://github.com/ReactiveX/RxJava/wiki/Combining-Observables#zip)_ +* **`++`** (scala) — _see [**`concat( )`**](https://github.com/ReactiveX/RxJava/wiki/Mathematical-and-Aggregate-Operators)_ +* **`+:`** (scala) — _see [**`startWith( )`**](https://github.com/ReactiveX/RxJava/wiki/Combining-Observables#startwith)_ + +(⁇) — this proposed operator is not part of RxJava 1.0 diff --git a/docs/Async-Operators.md b/docs/Async-Operators.md new file mode 100644 index 0000000000..17e3bda561 --- /dev/null +++ b/docs/Async-Operators.md @@ -0,0 +1,11 @@ +The following operators are part of the distinct `rxjava-async` module. They are used to convert synchronous methods into Observables. + +* [**`start( )`**](http://reactivex.io/documentation/operators/start.html) — create an Observable that emits the return value of a function +* [**`toAsync( )` or `asyncAction( )` or `asyncFunc( )`**](http://reactivex.io/documentation/operators/start.html) — convert a function or Action into an Observable that executes the function and emits its return value +* [**`startFuture( )`**](http://reactivex.io/documentation/operators/start.html) — convert a function that returns Future into an Observable that emits that Future's return value +* [**`deferFuture( )`**](http://reactivex.io/documentation/operators/start.html) — convert a Future that returns an Observable into an Observable, but do not attempt to get the Observable that the Future returns until a Subscriber subscribes +* [**`forEachFuture( )`**](http://reactivex.io/documentation/operators/start.html) — pass Subscriber methods to an Observable but also have it behave like a Future that blocks until it completes +* [**`fromAction( )`**](http://reactivex.io/documentation/operators/start.html) — convert an Action into an Observable that invokes the action and emits its result when a Subscriber subscribes +* [**`fromCallable( )`**](http://reactivex.io/documentation/operators/start.html) — convert a Callable into an Observable that invokes the callable and emits its result or exception when a Subscriber subscribes +* [**`fromRunnable( )`**](http://reactivex.io/documentation/operators/start.html) — convert a Runnable into an Observable that invokes the runable and emits its result when a Subscriber subscribes +* [**`runAsync( )`**](http://reactivex.io/documentation/operators/start.html) — returns a `StoppableObservable` that emits multiple actions as generated by a specified Action on a Scheduler \ No newline at end of file diff --git a/docs/Backpressure-(2.0).md b/docs/Backpressure-(2.0).md new file mode 100644 index 0000000000..6b2f2860af --- /dev/null +++ b/docs/Backpressure-(2.0).md @@ -0,0 +1,503 @@ +*Originally contributed to [StackOverflow Documentation](https://stackoverflow.com/documentation/rx-java/2341/backpressure) (going [defunct](https://meta.stackoverflow.com/questions/354217/sunsetting-documentation/)) by [@akarnokd](https://github.com/akarnokd), revised for version 2.x.* + +# Introduction + +**Backpressure** is when in an `Flowable` processing pipeline, some asynchronous stages can't process the values fast enough and need a way to tell the upstream producer to slow down. + +The classic case of the need for backpressure is when the producer is a hot source: + +```java + PublishProcessor source = PublishProcessor.create(); + + source + .observeOn(Schedulers.computation()) + .subscribe(v -> compute(v), Throwable::printStackTrace); + + for (int i = 0; i < 1_000_000; i++) { + source.onNext(i); + } + + Thread.sleep(10_000); +``` + +In this example, the main thread will produce 1 million items to an end consumer which is processing it on a background thread. It is likely the `compute(int)` method takes some time but the overhead of the `Flowable` operator chain may also add to the time it takes to process items. However, the producing thread with the for loop can't know this and keeps `onNext`ing. + +Internally, asynchronous operators have buffers to hold such elements until they can be processed. In the classical Rx.NET and early RxJava, these buffers were unbounded, meaning that they would likely hold nearly all 1 million elements from the example. The problem starts when there are, for example, 1 billion elements or the same 1 million sequence appears 1000 times in a program, leading to `OutOfMemoryError` and generally slowdowns due to excessive GC overhead. + +Similar to how error-handling became a first-class citizen and received operators to deal with it (via `onErrorXXX` operators), backpressure is another property of dataflows that the programmer has to think about and handle (via `onBackpressureXXX` operators). + +Beyond the `PublishProcessor`above, there are other operators that don't support backpressure, mostly due to functional reasons. For example, the operator `interval` emits values periodically, backpressuring it would lead to shifting in the period relative to a wall clock. + +In modern RxJava, most asynchronous operators now have a bounded internal buffer, like `observeOn` above and any attempt to overflow this buffer will terminate the whole sequence with `MissingBackpressureException`. The documentation of each operator has a description about its backpressure behavior. + +However, backpressure is present more subtly in regular cold sequences (which don't and shouldn't yield `MissingBackpressureException`). If the first example is rewritten: + + Flowable.range(1, 1_000_000) + .observeOn(Schedulers.computation()) + .subscribe(v -> compute(v), Throwable::printStackTrace); + + Thread.sleep(10_000); + +There is no error and everything runs smoothly with small memory usage. The reason for this is that many source operators can "generate" values on demand and thus the operator `observeOn` can tell the `range` generate at most so many values the `observeOn` buffer can hold at once without overflow. + +This negotiation is based on the computer science concept of co-routines (I call you, you call me). The operator `range` sends a callback, in the form of an implementation of the `org.reactivestreams.Subscription` interface, to the `observeOn` by calling its (inner `Subscriber`'s) `onSubscribe`. In return, the `observeOn` calls `Subscription.request(n)` with a value to tell the `range` it is allowed to produce (i.e., `onNext` it) that many **additional** elements. It is then the `observeOn`'s responsibility to call the `request` method in the right time and with the right value to keep the data flowing but not overflowing. + +Expressing backpressure in end-consumers is rarely necessary (because they are synchronous in respect to their immediate upstream and backpressure naturally happens due to call-stack blocking), but it may be easier to understand the workings of it: + +```java + Flowable.range(1, 1_000_000) + .subscribe(new DisposableSubscriber() { + @Override + public void onStart() { + request(1); + } + + public void onNext(Integer v) { + compute(v); + + request(1); + } + + @Override + public void onError(Throwable ex) { + ex.printStackTrace(); + } + + @Override + public void onComplete() { + System.out.println("Done!"); + } + }); +``` + +Here the `onStart` implementation indicates `range` to produce its first value, which is then received in `onNext`. Once the `compute(int)` finishes, the another value is then requested from `range`. In a naive implementation of `range`, such call would recursively call `onNext`, leading to `StackOverflowError` which is of course undesirable. + +To prevent this, operators use so-called trampolining logic that prevents such reentrant calls. In `range`'s terms, it will remember that there was a `request(1)` call while it called `onNext()` and once `onNext()` returns, it will make another round and call `onNext()` with the next integer value. Therefore, if the two are swapped, the example still works the same: + +```java + @Override + public void onNext(Integer v) { + request(1); + + compute(v); + } +``` + +However, this is not true for `onStart`. Although the `Flowable` infrastructure guarantees it will be called at most once on each `Subscriber`, the call to `request(1)` may trigger the emission of an element right away. If one has initialization logic after the call to `request(1)` which is needed by `onNext`, you may end up with exceptions: + +```java + Flowable.range(1, 1_000_000) + .subscribe(new DisposableSubscriber() { + + String name; + + @Override + public void onStart() { + request(1); + + name = "RangeExample"; + } + + @Override + public void onNext(Integer v) { + compute(name.length + v); + + request(1); + } + + // ... rest is the same + }); +``` + +In this synchronous case, a `NullPointerException` will be thrown immediately while still executing `onStart`. A more subtle bug happens if the call to `request(1)` triggers an asynchronous call to `onNext` on some other thread and reading `name` in `onNext` races writing it in `onStart` post `request`. + +Therefore, one should do all field initialization in `onStart` or even before that and call `request()` last. Implementations of `request()` in operators ensure proper happens-before relation (or in other terms, memory release or full fence) when necessary. + +# The onBackpressureXXX operators + +Most developers encounter backpressure when their application fails with `MissingBackpressureException` and the exception usually points to the `observeOn` operator. The actual cause is usually the non-backpressured use of `PublishProcessor`, `timer()` or `interval()` or custom operators created via `create()`. + +There are several ways of dealing with such situations. + +## Increasing the buffer sizes + +Sometimes such overflows happen due to bursty sources. Suddenly, the user taps the screen too quickly and `observeOn`'s default 16-element internal buffer on Android overflows. + +Most backpressure-sensitive operators in the recent versions of RxJava now allow programmers to specify the size of their internal buffers. The relevant parameters are usually called `bufferSize`, `prefetch` or `capacityHint`. Given the overflowing example in the introduction, we can just increase the buffer size of `observeOn` to have enough room for all values. + +```java + PublishProcessor source = PublishProcessor.create(); + + source.observeOn(Schedulers.computation(), 1024 * 1024) + .subscribe(e -> { }, Throwable::printStackTrace); + + for (int i = 0; i < 1_000_000; i++) { + source.onNext(i); + } +``` + +Note however that generally, this may be only a temporary fix as the overflow can still happen if the source overproduces the predicted buffer size. In this case, one can use one of the following operators. + +## Batching/skipping values with standard operators + +In case the source data can be processed more efficiently in batch, one can reduce the likelihood of `MissingBackpressureException` by using one of the standard batching operators (by size and/or by time). + +``` + PublishProcessor source = PublishProcessor.create(); + + source + .buffer(1024) + .observeOn(Schedulers.computation(), 1024) + .subscribe(list -> { + list.parallelStream().map(e -> e * e).first(); + }, Throwable::printStackTrace); + + for (int i = 0; i < 1_000_000; i++) { + source.onNext(i); + } +``` + +If some of the values can be safely ignored, one can use the sampling (with time or another `Flowable`) and throttling operators (`throttleFirst`, `throttleLast`, `throttleWithTimeout`). + +```java + PublishProcessor source = PublishProcessor.create(); + + source + .sample(1, TimeUnit.MILLISECONDS) + .observeOn(Schedulers.computation(), 1024) + .subscribe(v -> compute(v), Throwable::printStackTrace); + + for (int i = 0; i < 1_000_000; i++) { + source.onNext(i); + } +``` + +Note however that these operators only reduce the rate of value reception by the downstream and thus they may still lead to `MissingBackpressureException`. + +## onBackpressureBuffer() + +This operator in its parameterless form reintroduces an unbounded buffer between the upstream source and the downstream operator. Being unbounded means as long as the JVM doesn't run out of memory, it can handle almost any amount coming from a bursty source. + +```java + Flowable.range(1, 1_000_000) + .onBackpressureBuffer() + .observeOn(Schedulers.computation(), 8) + .subscribe(e -> { }, Throwable::printStackTrace); +``` + +In this example, the `observeOn` goes with a very low buffer size yet there is no `MissingBackpressureException` as `onBackpressureBuffer` soaks up all the 1 million values and hands over small batches of it to `observeOn`. + +Note however that `onBackpressureBuffer` consumes its source in an unbounded manner, that is, without applying any backpressure to it. This has the consequence that even a backpressure-supporting source such as `range` will be completely realized. + +There are 4 additional overloads of `onBackpressureBuffer` + +### onBackpressureBuffer(int capacity) + +This is a bounded version that signals `BufferOverflowError`in case its buffer reaches the given capacity. + +```java + Flowable.range(1, 1_000_000) + .onBackpressureBuffer(16) + .observeOn(Schedulers.computation()) + .subscribe(e -> { }, Throwable::printStackTrace); +``` + +The relevance of this operator is decreasing as more and more operators now allow setting their buffer sizes. For the rest, this gives an opportunity to "extend their internal buffer" by having a larger number with `onBackpressureBuffer` than their default. + +### onBackpressureBuffer(int capacity, Action onOverflow) + +This overload calls a (shared) action in case an overflow happens. Its usefulness is rather limited as there is no other information provided about the overflow than the current call stack. + +### onBackpressureBuffer(int capacity, Action onOverflow, BackpressureOverflowStrategy strategy) + +This overload is actually more useful as it let's one define what to do in case the capacity has been reached. The `BackpressureOverflow.Strategy` is an interface actually but the class `BackpressureOverflow` offers 4 static fields with implementations of it representing typical actions: + + - `ON_OVERFLOW_ERROR`: this is the default behavior of the previous two overloads, signalling a `BufferOverflowException` + - `ON_OVERFLOW_DEFAULT`: currently it is the same as `ON_OVERFLOW_ERROR` + - `ON_OVERFLOW_DROP_LATEST` : if an overflow would happen, the current value will be simply ignored and only the old values will be delivered once the downstream requests. + - `ON_OVERFLOW_DROP_OLDEST` : drops the oldest element in the buffer and adds the current value to it. + +```java + Flowable.range(1, 1_000_000) + .onBackpressureBuffer(16, () -> { }, + BufferOverflowStrategy.ON_OVERFLOW_DROP_OLDEST) + .observeOn(Schedulers.computation()) + .subscribe(e -> { }, Throwable::printStackTrace); +``` + +Note that the last two strategies cause discontinuity in the stream as they drop out elements. In addition, they won't signal `BufferOverflowException`. + +## onBackpressureDrop() + +Whenever the downstream is not ready to receive values, this operator will drop that element from the sequence. One can think of it as a 0 capacity `onBackpressureBuffer` with strategy `ON_OVERFLOW_DROP_LATEST`. + +This operator is useful when one can safely ignore values from a source (such as mouse moves or current GPS location signals) as there will be more up-to-date values later on. + +```java + component.mouseMoves() + .onBackpressureDrop() + .observeOn(Schedulers.computation(), 1) + .subscribe(event -> compute(event.x, event.y)); +``` + +It may be useful in conjunction with the source operator `interval()`. For example, if one wants to perform some periodic background task but each iteration may last longer than the period, it is safe to drop the excess interval notification as there will be more later on: + +```java + Flowable.interval(1, TimeUnit.MINUTES) + .onBackpressureDrop() + .observeOn(Schedulers.io()) + .doOnNext(e -> networkCall.doStuff()) + .subscribe(v -> { }, Throwable::printStackTrace); +``` + +There exist one overload of this operator: `onBackpressureDrop(Consumer onDrop)` where the (shared) action is called with the value being dropped. This variant allows cleaning up the values themselves (e.g., releasing associated resources). + +## onBackpressureLatest() + +The final operator keeps only the latest value and practically overwrites older, undelivered values. One can think of this as a variant of the `onBackpressureBuffer` with a capacity of 1 and strategy of `ON_OVERFLOW_DROP_OLDEST`. + +Unlike `onBackpressureDrop` there is always a value available for consumption if the downstream happened to be lagging behind. This can be useful in some telemetry-like situations where the data may come in some bursty pattern but only the very latest is interesting for processing. + +For example, if the user clicks a lot on the screen, we'd still want to react to its latest input. + +```java + component.mouseClicks() + .onBackpressureLatest() + .observeOn(Schedulers.computation()) + .subscribe(event -> compute(event.x, event.y), Throwable::printStackTrace); +``` + +The use of `onBackpressureDrop` in this case would lead to a situation where the very last click gets dropped and leaves the user wondering why the business logic wasn't executed. + +# Creating backpressured datasources + +Creating backpressured data sources is the relatively easier task when dealing with backpressure in general because the library already offers static methods on `Flowable` that handle backpressure for the developer. We can distinguish two kinds of factory methods: cold "generators" that either return and generate elements based on downstream demand and hot "pushers" that usually bridge non-reactive and/or non-backpressurable data sources and layer some backpressure handling on top of them. + +## just + +The most basic backpressure aware source is created via `just`: + +```java + Flowable.just(1).subscribe(new DisposableSubscriber() { + @Override + public void onStart() { + request(0); + } + + @Override + public void onNext(Integer v) { + System.out.println(v); + } + + // the rest is omitted for brevity + } +``` + +Since we explicitly don't request in `onStart`, this will not print anything. `just` is great when there is a constant value we'd like to jump-start a sequence. + +Unfortunately, `just` is often mistaken for a way to compute something dynamically to be consumed by `Subscriber`s: + +```java + int counter; + + int computeValue() { + return ++counter; + } + + Flowable o = Flowable.just(computeValue()); + + o.subscribe(System.out:println); + o.subscribe(System.out:println); +``` + +Surprising to some, this prints 1 twice instead of printing 1 and 2 respectively. If the call is rewritten, it becomes obvious why it works so: + +```java + int temp = computeValue(); + + Flowable o = Flowable.just(temp); +``` + +The `computeValue` is called as part of the main routine and not in response to the subscribers subscribing. + +## fromCallable + +What people actually need is the method `fromCallable`: + +```java + Flowable o = Flowable.fromCallable(() -> computeValue()); +``` + +Here the `computeValue` is executed only when a subscriber subscribes and for each of them, printing the expected 1 and 2. Naturally, `fromCallable` also properly supports backpressure and won't emit the computed value unless requested. Note however that the computation does happen anyway. In case the computation itself should be delayed until the downstream actually requests, we can use `just` with `map`: + +```java + Flowable.just("This doesn't matter").map(ignored -> computeValue())... +``` + +`just` won't emit its constant value until requested when it is mapped to the result of the `computeValue`, still called for each subscriber individually. + +## fromArray + +If the data is already available as an array of objects, a list of objects or any `Iterable` source, the respective `from` overloads will handle the backpressure and emission of such sources: + +```java + Flowable.fromArray(1, 2, 3, 4, 5).subscribe(System.out::println); +``` + +For convenience (and avoiding warnings about generic array creation) there are 2 to 10 argument overloads to `just` that internally delegate to `from`. + +The `fromIterable` also gives an interesting opportunity. Many value generation can be expressed in a form of a state-machine. Each requested element triggers a state transition and computation of the returned value. + +Writing such state machines as `Iterable`s is somewhat complicated (but still easier than writing an `Flowable` for consuming it) and unlike C#, Java doesn't have any support from the compiler to build such state machines by simply writing classically looking code (with `yield return` and `yield break`). Some libraries offer some help, such as Google Guava's `AbstractIterable` and IxJava's `Ix.generate()` and `Ix.forloop()`. These are by themselves worthy of a full series so let's see some very basic `Iterable` source that repeats some constant value indefinitely: + +```java + Iterable iterable = () -> new Iterator() { + @Override + public boolean hasNext() { + return true; + } + + @Override + public Integer next() { + return 1; + } + }; + + Flowable.fromIterable(iterable).take(5).subscribe(System.out::println); +``` + +If we'd consume the `iterator` via classic for-loop, that would result in an infinite loop. Since we build an `Flowable` out of it, we can express our will to consume only the first 5 of it and then stop requesting anything. This is the true power of lazily evaluating and computing inside `Flowable`s. + +## generate() + +Sometimes, the data source to be converted into the reactive world itself is synchronous (blocking) and pull-like, that is, we have to call some `get` or `read` method to get the next piece of data. One could, of course, turn that into an `Iterable` but when such sources are associated with resources, we may leak those resources if the downstream unsubscribes the sequence before it would end. + +To handle such cases, RxJava has the `generate` factory method family. + +```java + Flowable o = Flowable.generate( + () -> new FileInputStream("data.bin"), + (inputstream, output) -> { + try { + int abyte = inputstream.read(); + if (abyte < 0) { + output.onComplete(); + } else { + output.onNext(abyte); + } + } catch (IOException ex) { + output.onError(ex); + } + return inputstream; + }, + inputstream -> { + try { + inputstream.close(); + } catch (IOException ex) { + RxJavaPlugins.onError(ex); + } + } + ); +``` + +Generally, `generate` uses 3 callbacks. + +The first callbacks allows one to create a per-subscriber state, such as the `FileInputStream` in the example; the file will be opened independently to each individual subscriber. + +The second callback takes this state object and provides an output `Observer` whose `onXXX` methods can be called to emit values. This callback is executed as many times as the downstream requested. At each invocation, it has to call `onNext` at most once optionally followed by either `onError` or `onComplete`. In the example we call `onComplete()` if the read byte is negative, indicating and end of file, and call `onError` in case the read throws an `IOException`. + +The final callback gets invoked when the downstream unsubscribes (closing the inputstream) or when the previous callback called the terminal methods; it allows freeing up resources. Since not all sources need all these features, the static methods of `Flowable.generate` let's one create instances without them. + +Unfortunately, many method calls across the JVM and other libraries throw checked exceptions and need to be wrapped into `try-catch`es as the functional interfaces used by this class don't allow throwing checked exceptions. + +Of course, we can imitate other typical sources, such as an unbounded range with it: + +```java + Flowable.generate( + () -> 0, + (current, output) -> { + output.onNext(current); + return current + 1; + }, + e -> { } + ); +``` + +In this setup, the `current` starts out with `0` and next time the lambda is invoked, the parameter `current` now holds `1`. + +*(Remark: the 1.x classes `SyncOnSubscribe` and `AsyncOnSubscribe` are no longer available.)* + +## create(emitter) + +Sometimes, the source to be wrapped into an `Flowable` is already hot (such as mouse moves) or cold but not backpressurable in its API (such as an asynchronous network callback). + +To handle such cases, a recent version of RxJava introduced the `create(emitter)` factory method. It takes two parameters: + + - a callback that will be called with an instance of the `Emitter` interface for each incoming subscriber, + - a `BackpressureStrategy` enumeration that mandates the developer to specify the backpressure behavior to be applied. It has the usual modes, similar to `onBackpressureXXX` in addition to signalling a `MissingBackpressureException` or simply ignoring such overflow inside it altogether. + +Note that it currently doesn't support additional parameters to those backpressure modes. If one needs those customization, using `NONE` as the backpressure mode and applying the relevant `onBackpressureXXX` on the resulting `Flowable` is the way to go. + +The first typical case for its use when one wants to interact with a push-based source, such as GUI events. Those APIs feature some form of `addListener`/`removeListener` calls that one can utilize: + +```java + Flowable.create(emitter -> { + ActionListener al = e -> { + emitter.onNext(e); + }; + + button.addActionListener(al); + + emitter.setCancellation(() -> + button.removeListener(al)); + + }, BackpressureStrategy.BUFFER); +``` + +The `Emitter` is relatively straightforward to use; one can call `onNext`, `onError` and `onComplete` on it and the operator handles backpressure and unsubscription management on its own. In addition, if the wrapped API supports cancellation (such as the listener removal in the example), one can use the `setCancellation` (or `setSubscription` for `Subscription`-like resources) to register a cancellation callback that gets invoked when the downstream unsubscribes or the `onError`/`onComplete` is called on the provided `Emitter`instance. + +These methods allow only a single resource to be associated with the emitter at a time and setting a new one unsubscribes the old one automatically. If one has to handle multiple resources, create a `CompositeSubscription`, associate it with the emitter and then add further resources to the `CompositeSubscription` itself: + +```java + Flowable.create(emitter -> { + CompositeSubscription cs = new CompositeSubscription(); + + Worker worker = Schedulers.computation().createWorker(); + + ActionListener al = e -> { + emitter.onNext(e); + }; + + button.addActionListener(al); + + cs.add(worker); + cs.add(Subscriptions.create(() -> + button.removeActionListener(al)); + + emitter.setSubscription(cs); + + }, BackpressureMode.BUFFER); +``` + +The second scenario usually involves some asynchronous, callback-based API that has to be converted into an `Flowable`. + +```java + Flowable.create(emitter -> { + + someAPI.remoteCall(new Callback() { + @Override + public void onSuccess(Data data) { + emitter.onNext(data); + emitter.onComplete(); + } + + @Override + public void onFailure(Exception error) { + emitter.onError(error); + } + }); + + }, BackpressureMode.LATEST); +``` + +In this case, the delegation works the same way. Unfortunately, usually, these classical callback-style APIs don't support cancellation, but if they do, one can setup their cancellation just like in the previoius examples (with perhaps a more involved way though). Note the use of the `LATEST` backpressure mode; if we know there will be only a single value, we don't need the `BUFFER` strategy as it allocates a default 128 element long buffer (that grows as necessary) that is never going to be fully utilized. \ No newline at end of file diff --git a/docs/Backpressure.md b/docs/Backpressure.md new file mode 100644 index 0000000000..8529ec0995 --- /dev/null +++ b/docs/Backpressure.md @@ -0,0 +1,175 @@ +# Introduction + +In RxJava it is not difficult to get into a situation in which an Observable is emitting items more rapidly than an operator or subscriber can consume them. This presents the problem of what to do with such a growing backlog of unconsumed items. + +For example, imagine using the [`zip`](http://reactivex.io/documentation/operators/zip.html) operator to zip together two infinite Observables, one of which emits items twice as frequently as the other. A naive implementation of the `zip` operator would have to maintain an ever-expanding buffer of items emitted by the faster Observable to eventually combine with items emitted by the slower one. This could cause RxJava to seize an unwieldy amount of system resources. + +There are a variety of strategies with which you can exercise flow control and backpressure in RxJava in order to alleviate the problems caused when a quickly-producing Observable meets a slow-consuming observer. This page explains some of these strategies, and also shows you how you can design your own Observables and Observable operators to respect requests for flow control. + +## Hot and cold Observables, and multicasted Observables + +A _cold_ Observable emits a particular sequence of items, but can begin emitting this sequence when its Observer finds it to be convenient, and at whatever rate the Observer desires, without disrupting the integrity of the sequence. For example if you convert a static Iterable into an Observable, that Observable will emit the same sequence of items no matter when it is later subscribed to or how frequently those items are observed. Examples of items emitted by a cold Observable might include the results of a database query, file retrieval, or web request. + +A _hot_ Observable begins generating items to emit immediately when it is created. Subscribers typically begin observing the sequence of items emitted by a hot Observable from somewhere in the middle of the sequence, beginning with the first item emitted by the Observable subsequent to the establishment of the subscription. Such an Observable emits items at its own pace, and it is up to its observers to keep up. Examples of items emitted by a hot Observable might include mouse & keyboard events, system events, or stock prices. + +When a cold Observable is _multicast_ (when it is converted into a `ConnectableObservable` and its [`connect()`](http://reactivex.io/documentation/operators/connect.html) method is called), it effectively becomes _hot_ and for the purposes of backpressure and flow-control it should be treated as a hot Observable. + +Cold Observables are ideal for the reactive pull model of backpressure described below. Hot Observables typically do not cope well with a reactive pull model, and are better candidates for some of the other flow control strategies discussed on this page, such as the use of [the `onBackpressureBuffer` or `onBackpressureDrop` operators](http://reactivex.io/documentation/operators/backpressure.html), throttling, buffers, or windows. + +# Useful operators that avoid the need for backpressure + +Your first line of defense against the problems of over-producing Observables is to use some of the ordinary set of Observable operators to reduce the number of emitted items to a more manageable number. The examples in this section will show how you might use such operators to handle a bursty Observable like the one illustrated in the following marble diagram: + +​ + +By fine-tuning the parameters to these operators you can ensure that a slow-consuming observer is not overwhelmed by a fast-producing Observable. + +## Throttling + +Operators like [`sample( )` or `throttleLast( )`](http://reactivex.io/documentation/operators/sample.html), [`throttleFirst( )`](http://reactivex.io/documentation/operators/sample.html), and [`throttleWithTimeout( )` or `debounce( )`](http://reactivex.io/documentation/operators/debounce.html) allow you to regulate the rate at which an Observable emits items. + +The following diagrams show how you could use each of these operators on the bursty Observable shown above. + +### sample (or throttleLast) +The `sample` operator periodically "dips" into the sequence and emits only the most recently emitted item during each dip: + +​ +```java +Observable burstySampled = bursty.sample(500, TimeUnit.MILLISECONDS); +``` + +### throttleFirst +The `throttleFirst` operator is similar, but emits not the most recently emitted item, but the first item that was emitted after the previous "dip": + +​ +```java +Observable burstyThrottled = bursty.throttleFirst(500, TimeUnit.MILLISECONDS); +``` + +### debounce (or throttleWithTimeout) +The `debounce` operator emits only those items from the source Observable that are not followed by another item within a specified duration: + +​ +```java +Observable burstyDebounced = bursty.debounce(10, TimeUnit.MILLISECONDS); +``` + +## Buffers and windows + +You can also use an operator like [`buffer( )`](http://reactivex.io/documentation/operators/buffer.html) or [`window( )`](http://reactivex.io/documentation/operators/window.html) to collect items from the over-producing Observable and then emit them, less-frequently, as collections (or Observables) of items. The slow consumer can then decide whether to process only one particular item from each collection, to process some combination of those items, or to schedule work to be done on each item in the collection, as appropriate. + +The following diagrams show how you could use each of these operators on the bursty Observable shown above. + +### buffer + +You could, for example, close and emit a buffer of items from the bursty Observable periodically, at a regular interval of time: + +​ +```java +Observable> burstyBuffered = bursty.buffer(500, TimeUnit.MILLISECONDS); +``` + +Or you could get fancy, and collect items in buffers during the bursty periods and emit them at the end of each burst, by using the `debounce` operator to emit a buffer closing indicator to the `buffer` operator: + +​ +```java +// we have to multicast the original bursty Observable so we can use it +// both as our source and as the source for our buffer closing selector: +Observable burstyMulticast = bursty.publish().refCount(); +// burstyDebounced will be our buffer closing selector: +Observable burstyDebounced = burstMulticast.debounce(10, TimeUnit.MILLISECONDS); +// and this, finally, is the Observable of buffers we're interested in: +Observable> burstyBuffered = burstyMulticast.buffer(burstyDebounced); +```` + +### window + +`window` is similar to `buffer`. One variant of `window` allows you to periodically emit Observable windows of items at a regular interval of time: + +​ +```java +Observable> burstyWindowed = bursty.window(500, TimeUnit.MILLISECONDS); +```` + +You could also choose to emit a new window each time you have collected a particular number of items from the source Observable: + +​ +```java +Observable> burstyWindowed = bursty.window(5); +``` + +# Callstack blocking as a flow-control alternative to backpressure + +Another way of handling an overproductive Observable is to block the callstack (parking the thread that governs the overproductive Observable). This has the disadvantage of going against the “reactive” and non-blocking model of Rx. However this can be a viable option if the problematic Observable is on a thread that can be blocked safely. Currently RxJava does not expose any operators to facilitate this. + +If the Observable, all of the operators that operate on it, and the observer that is subscribed to it, are all operating in the same thread, this effectively establishes a form of backpressure by means of callstack blocking. But be aware that many Observable operators do operate in distinct threads by default (the javadocs for those operators will indicate this). + +# How a subscriber establishes “reactive pull” backpressure + +When you subscribe to an `Observable` with a `Subscriber`, you can request reactive pull backpressure by calling `Subscriber.request(n)` in the `Subscriber`’s `onStart()` method (where _n_ is the maximum number of items you want the `Observable` to emit before the next `request()` call). + +Then, after handling this item (or these items) in `onNext()`, you can call `request()` again to instruct the `Observable` to emit another item (or items). Here is an example of a `Subscriber` that requests one item at a time from `someObservable`: + +```java +someObservable.subscribe(new Subscriber() { + @Override + public void onStart() { + request(1); + } + + @Override + public void onCompleted() { + // gracefully handle sequence-complete + } + + @Override + public void onError(Throwable e) { + // gracefully handle error + } + + @Override + public void onNext(T n) { + // do something with the emitted item "n" + // request another item: + request(1); + } +}); +``` + +You can pass a magic number to `request`, `request(Long.MAX_VALUE)`, to disable reactive pull backpressure and to ask the Observable to emit items at its own pace. `request(0)` is a legal call, but has no effect. Passing values less than zero to `request` will cause an exception to be thrown. + +## Reactive pull backpressure isn’t magic + +Backpressure doesn’t make the problem of an overproducing Observable or an underconsuming Subscriber go away. It just moves the problem up the chain of operators to a point where it can be handled better. + +Let’s take a closer look at the problem of the uneven [`zip`](http://reactivex.io/documentation/operators/zip.html). + +You have two Observables, _A_ and _B_, where _B_ is inclined to emit items more frequently than _A_. When you try to `zip` these two Observables together, the `zip` operator combines item _n_ from _A_ and item _n_ from _B_, but meanwhile _B_ has also emitted items _n_+1 to _n_+_m_. The `zip` operator has to hold on to these items so it can combine them with items _n_+1 to _n_+_m_ from _A_ as they are emitted, but meanwhile _m_ keeps growing and so the size of the buffer needed to hold on to these items keeps increasing. + +You could attach a throttling operator to _B_, but this would mean ignoring some of the items _B_ emits, which might not be appropriate. What you’d really like to do is to signal to _B_ that it needs to slow down and then let _B_ decide how to do this in a way that maintains the integrity of its emissions. + +The reactive pull backpressure model lets you do this. It creates a sort of active pull from the Subscriber in contrast to the normal passive push Observable behavior. + +The `zip` operator as implemented in RxJava uses this technique. It maintains a small buffer of items for each source Observable, and it requests no more items from each source Observable than would fill its buffer. Each time `zip` emits an item, it removes the corresponding items from its buffers and requests exactly one more item from each of its source Observables. + +(Many RxJava operators exercise reactive pull backpressure. Some operators do not need to use this variety of backpressure, as they operate in the same thread as the Observable they operate on, and so they exert a form of blocking backpressure simply by not giving the Observable the opportunity to emit another item until they have finished processing the previous one. For other operators, backpressure is inappropriate as they have been explicitly designed to deal with flow control in other ways. The RxJava javadocs for those operators that are methods of the Observable class indicate which ones do not use reactive pull backpressure and why.) + +For this to work, though, Observables _A_ and _B_ must respond correctly to the `request()`. If an Observable has not been written to support reactive pull backpressure (such support is not a requirement for Observables), you can apply one of the following operators to it, each of which forces a simple form of backpressure behavior: + +
+
onBackpressureBuffer
+
maintains a buffer of all emissions from the source Observable and emits them to downstream Subscribers according to the requests they generate

an experimental version of this operator (not available in RxJava 1.0) allows you to set the capacity of the buffer; applying this operator will cause the resulting Observable to terminate with an error if this buffer is overrun​
+
onBackpressureDrop
+
drops emissions from the source Observable unless there is a pending request from a downstream Subscriber, in which case it will emit enough items to fulfill the request
+
onBackpressureBlock (experimental, not in RxJava 1.0)
+
blocks the thread on which the source Observable is operating until such time as a Subscriber issues a request for items, and then unblocks the thread only so long as there are pending requests
+
+ +If you do not apply any of these operators to an Observable that does not support backpressure, _and_ if either you as the Subscriber or some operator between you and the Observable attempts to apply reactive pull backpressure, you will encounter a `MissingBackpressureException` which you will be notified of via your `onError()` callback. + +# Further reading + +If the standard operators aren't providing the expected behavior, [one can write custom operators in RxJava](https://github.com/ReactiveX/RxJava/wiki/Implementing-custom-operators-(draft)). + +# See also +* [RxJava 0.20.0-RC1 release notes](https://github.com/ReactiveX/RxJava/releases/tag/0.20.0-RC1) diff --git a/docs/Blocking-Observable-Operators.md b/docs/Blocking-Observable-Operators.md new file mode 100644 index 0000000000..fe2a640f49 --- /dev/null +++ b/docs/Blocking-Observable-Operators.md @@ -0,0 +1,49 @@ +This section explains the [`BlockingObservable`](http://reactivex.io/RxJava/javadoc/rx/observables/BlockingObservable.html) subclass. A Blocking Observable extends the ordinary Observable class by providing a set of operators on the items emitted by the Observable that block. + +To transform an `Observable` into a `BlockingObservable`, use the [`Observable.toBlocking( )`](http://reactivex.io/RxJava/javadoc/rx/Observable.html#toBlocking()) method or the [`BlockingObservable.from( )`](http://reactivex.io/RxJava/javadoc/rx/observables/BlockingObservable.html#from(rx.Observable)) method. + +* [**`forEach( )`**](http://reactivex.io/documentation/operators/subscribe.html) — invoke a function on each item emitted by the Observable; block until the Observable completes +* [**`first( )`**](http://reactivex.io/documentation/operators/first.html) — block until the Observable emits an item, then return the first item emitted by the Observable +* [**`firstOrDefault( )`**](http://reactivex.io/documentation/operators/first.html) — block until the Observable emits an item or completes, then return the first item emitted by the Observable or a default item if the Observable did not emit an item +* [**`last( )`**](http://reactivex.io/documentation/operators/last.html) — block until the Observable completes, then return the last item emitted by the Observable +* [**`lastOrDefault( )`**](http://reactivex.io/documentation/operators/last.html) — block until the Observable completes, then return the last item emitted by the Observable or a default item if there is no last item +* [**`mostRecent( )`**](http://reactivex.io/documentation/operators/first.html) — returns an iterable that always returns the item most recently emitted by the Observable +* [**`next( )`**](http://reactivex.io/documentation/operators/takelast.html) — returns an iterable that blocks until the Observable emits another item, then returns that item +* [**`latest( )`**](http://reactivex.io/documentation/operators/first.html) — returns an iterable that blocks until or unless the Observable emits an item that has not been returned by the iterable, then returns that item +* [**`single( )`**](http://reactivex.io/documentation/operators/first.html) — if the Observable completes after emitting a single item, return that item, otherwise throw an exception +* [**`singleOrDefault( )`**](http://reactivex.io/documentation/operators/first.html) — if the Observable completes after emitting a single item, return that item, otherwise return a default item +* [**`toFuture( )`**](http://reactivex.io/documentation/operators/to.html) — convert the Observable into a Future +* [**`toIterable( )`**](http://reactivex.io/documentation/operators/to.html) — convert the sequence emitted by the Observable into an Iterable +* [**`getIterator( )`**](http://reactivex.io/documentation/operators/to.html) — convert the sequence emitted by the Observable into an Iterator + +> This documentation accompanies its explanations with a modified form of "marble diagrams." Here is how these marble diagrams represent Blocking Observables: + + + +#### see also: +* javadoc: `BlockingObservable` +* javadoc: `toBlocking()` +* javadoc: `BlockingObservable.from()` + +## Appendix: similar blocking and non-blocking operators + + + + + + + + + + + + + + + + + + + + +
operatorresult when it acts onequivalent in Rx.NET
Observable that emits multiple itemsObservable that emits one itemObservable that emits no items
Observable.firstthe first itemthe single itemNoSuchElementfirstAsync
BlockingObservable.firstthe first itemthe single itemNoSuchElementfirst
Observable.firstOrDefaultthe first itemthe single itemthe default itemfirstOrDefaultAsync
BlockingObservable.firstOrDefaultthe first itemthe single itemthe default itemfirstOrDefault
Observable.lastthe last itemthe single itemNoSuchElementlastAsync
BlockingObservable.lastthe last itemthe single itemNoSuchElementlast
Observable.lastOrDefaultthe last itemthe single itemthe default itemlastOrDefaultAsync
BlockingObservable.lastOrDefaultthe last itemthe single itemthe default itemlastOrDefault
Observable.singleIllegal Argumentthe single itemNoSuchElementsingleAsync
BlockingObservable.singleIllegal Argumentthe single itemNoSuchElementsingle
Observable.singleOrDefaultIllegal Argumentthe single itemthe default itemsingleOrDefaultAsync
BlockingObservable.singleOrDefaultIllegal Argumentthe single itemthe default itemsingleOrDefault
\ No newline at end of file diff --git a/docs/Combining-Observables.md b/docs/Combining-Observables.md new file mode 100644 index 0000000000..fcde91b76b --- /dev/null +++ b/docs/Combining-Observables.md @@ -0,0 +1,166 @@ +This section explains operators you can use to combine multiple Observables. + +# Outline + +- [`combineLatest`](#combinelatest) +- [`join` and `groupJoin`](#joins) +- [`merge`](#merge) +- [`mergeDelayError`](#mergedelayerror) +- [`rxjava-joins`](#rxjava-joins) +- [`startWith`](#startwith) +- [`switchOnNext`](#switchonnext) +- [`zip`](#zip) + +## startWith + +**Available in:** ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Flowable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Observable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Maybe`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Single`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Completable` + +**ReactiveX documentation:** [http://reactivex.io/documentation/operators/startwith.html](http://reactivex.io/documentation/operators/startwith.html) + +Emit a specified sequence of items before beginning to emit the items from the Observable. + +#### startWith Example + +```java +Observable names = Observable.just("Spock", "McCoy"); +names.startWith("Kirk").subscribe(item -> System.out.println(item)); + +// prints Kirk, Spock, McCoy +``` + +## merge + +Combines multiple Observables into one. + + +### merge + +**Available in:** ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Flowable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Observable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Maybe`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Single`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Completable` + +**ReactiveX documentation:** [http://reactivex.io/documentation/operators/merge.html](http://reactivex.io/documentation/operators/merge.html) + +Combines multiple Observables into one. Any `onError` notifications passed from any of the source observables will immediately be passed through to through to the observers and will terminate the merged `Observable`. + +#### merge Example + +```java +Observable.just(1, 2, 3) + .mergeWith(Observable.just(4, 5, 6)) + .subscribe(item -> System.out.println(item)); + +// prints 1, 2, 3, 4, 5, 6 +``` + +### mergeDelayError + +**Available in:** ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Flowable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Observable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Maybe`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Single`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Completable` + +**ReactiveX documentation:** [http://reactivex.io/documentation/operators/merge.html](http://reactivex.io/documentation/operators/merge.html) + +Combines multiple Observables into one. Any `onError` notifications passed from any of the source observables will be withheld until all merged Observables complete, and only then will be passed along to the observers. + +#### mergeDelayError Example + +```java +Observable observable1 = Observable.error(new IllegalArgumentException("")); +Observable observable2 = Observable.just("Four", "Five", "Six"); +Observable.mergeDelayError(observable1, observable2) + .subscribe(item -> System.out.println(item)); + +// emits 4, 5, 6 and then the IllegalArgumentException (in this specific +// example, this throws an `OnErrorNotImplementedException`). +``` + +## zip + +**Available in:** ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Flowable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Observable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Maybe`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Single`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Completable` + +**ReactiveX documentation:** [http://reactivex.io/documentation/operators/zip.html](http://reactivex.io/documentation/operators/zip.html) + +Combines sets of items emitted by two or more Observables together via a specified function and emit items based on the results of this function. + +#### zip Example + +```java +Observable firstNames = Observable.just("James", "Jean-Luc", "Benjamin"); +Observable lastNames = Observable.just("Kirk", "Picard", "Sisko"); +firstNames.zipWith(lastNames, (first, last) -> first + " " + last) + .subscribe(item -> System.out.println(item)); + +// prints James Kirk, Jean-Luc Picard, Benjamin Sisko +``` + +## combineLatest + +**Available in:** ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Flowable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Observable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Maybe`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Single`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Completable` + +**ReactiveX documentation:** [http://reactivex.io/documentation/operators/combinelatest.html](http://reactivex.io/documentation/operators/combinelatest.html) + +When an item is emitted by either of two Observables, combine the latest item emitted by each Observable via a specified function and emit items based on the results of this function. + +#### combineLatest Example + +```java +Observable newsRefreshes = Observable.interval(100, TimeUnit.MILLISECONDS); +Observable weatherRefreshes = Observable.interval(50, TimeUnit.MILLISECONDS); +Observable.combineLatest(newsRefreshes, weatherRefreshes, + (newsRefreshTimes, weatherRefreshTimes) -> + "Refreshed news " + newsRefreshTimes + " times and weather " + weatherRefreshTimes) + .subscribe(item -> System.out.println(item)); + +// prints: +// Refreshed news 0 times and weather 0 +// Refreshed news 0 times and weather 1 +// Refreshed news 0 times and weather 2 +// Refreshed news 1 times and weather 2 +// Refreshed news 1 times and weather 3 +// Refreshed news 1 times and weather 4 +// Refreshed news 2 times and weather 4 +// Refreshed news 2 times and weather 5 +// ... +``` + +## switchOnNext + +**Available in:** ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Flowable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Observable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Maybe`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Single`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Completable` + +**ReactiveX documentation:** [http://reactivex.io/documentation/operators/switch.html](http://reactivex.io/documentation/operators/switch.html) + +Convert an Observable that emits Observables into a single Observable that emits the items emitted by the most-recently emitted of those Observables. + +#### switchOnNext Example + +```java +Observable> timeIntervals = + Observable.interval(1, TimeUnit.SECONDS) + .map(ticks -> Observable.interval(100, TimeUnit.MILLISECONDS) + .map(innerInterval -> "outer: " + ticks + " - inner: " + innerInterval)); +Observable.switchOnNext(timeIntervals) + .subscribe(item -> System.out.println(item)); + +// prints: +// outer: 0 - inner: 0 +// outer: 0 - inner: 1 +// outer: 0 - inner: 2 +// outer: 0 - inner: 3 +// outer: 0 - inner: 4 +// outer: 0 - inner: 5 +// outer: 0 - inner: 6 +// outer: 0 - inner: 7 +// outer: 0 - inner: 8 +// outer: 1 - inner: 0 +// outer: 1 - inner: 1 +// outer: 1 - inner: 2 +// outer: 1 - inner: 3 +// ... +``` + +## joins + +* [**`join( )` and `groupJoin( )`**](http://reactivex.io/documentation/operators/join.html) — combine the items emitted by two Observables whenever one item from one Observable falls within a window of duration specified by an item emitted by the other Observable + +## rxjava-joins + +* (`rxjava-joins`) [**`and( )`, `then( )`, and `when( )`**](http://reactivex.io/documentation/operators/and-then-when.html) — combine sets of items emitted by two or more Observables by means of `Pattern` and `Plan` intermediaries + +> (`rxjava-joins`) — indicates that this operator is currently part of the optional `rxjava-joins` package under `rxjava-contrib` and is not included with the standard RxJava set of operators diff --git a/docs/Conditional-and-Boolean-Operators.md b/docs/Conditional-and-Boolean-Operators.md new file mode 100644 index 0000000000..e6d1358e31 --- /dev/null +++ b/docs/Conditional-and-Boolean-Operators.md @@ -0,0 +1,169 @@ +This section explains operators with which you conditionally emit or transform Observables, or can do boolean evaluations of them: + +### Conditional Operators + +### Outline + +- [`amb`](#all) +- [`defaultIfEmpty`](#defaultIfEmpty) +- [`skipUntil`](#skipUntil) +- [`skipWhile`](#skipWhile) +- [`takeUntil`](#takeUntil) +- [`takeWhile`](#takeUntil) + +## amb + +**Available in:** ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Flowable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Observable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Maybe`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Single`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Completable` + +**ReactiveX documentation:** [http://reactivex.io/documentation/operators/amb.html](http://reactivex.io/documentation/operators/amb.html) + +given two or more source Observables, emits all of the items from the first of these Observables to emit an item + +```java + Observable source1 = Observable.range(1, 5); + Observable source2 = Observable.range(6, 5); + Observable.amb(new ArrayList(Arrays.asList(source1, source2))) + .subscribe(next -> System.out.printf("next: %s\n", next), // onNext + throwable -> System.out.printf("error: %s\n", throwable), //onError + () -> System.out.println("Completed") //onComplete + ); +``` +## defaultIfEmpty + +**Available in:** ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Flowable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Observable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Maybe`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Single`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Completable` + +**ReactiveX documentation:** [http://reactivex.io/documentation/operators/defaultifempty.html](http://reactivex.io/documentation/operators/defaultifempty.html) + +emit items from the source Observable, or emit a default item if the source Observable completes after emitting no items + +```java + Observable.empty().defaultIfEmpty(1).blockingSubscribe(next -> System.out.printf("next: %s\n", next), // onNext + throwable -> System.out.printf("error: %s", throwable), //onError + () -> System.out.println("Completed") //onComplete + ); +``` + +## skipUntil + +**Available in:** ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Flowable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Observable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Maybe`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Single`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Completable` + +**ReactiveX documentation:** [http://reactivex.io/documentation/operators/skipuntil.html](http://reactivex.io/documentation/operators/skipuntil.html) + +discard items emitted by a source Observable until a second Observable emits an item, then emit the remainder of the source Observable's items + +```java +Observable observable1 = Observable.range(1, 10).doOnNext(next -> Thread.sleep(1000)); + +observable1.skipUntil(Observable.timer(3, TimeUnit.SECONDS)) + .subscribe(next -> System.out.printf("next: %s\n", next), // onNext + throwable -> System.out.printf("error: %s", throwable), //onError + () -> System.out.println("Completed") //onComplete + ); +``` +## skipWhile + +**Available in:** ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Flowable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Observable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Maybe`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Single`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Completable` + +**ReactiveX documentation:** [http://reactivex.io/documentation/operators/skipwhile.html](http://reactivex.io/documentation/operators/skipwhile.html) + +discard items emitted by an Observable until a specified condition is false, then emit the remainder + +```java +Observable.range(1, 10).skipWhile(next -> next < 5) + .subscribe(next -> System.out.printf("next: %s\n", next), // onNext + throwable -> System.out.printf("error: %s", throwable), //onError + () -> System.out.println("Completed") //onComplete + ); +``` + +## takeUntil + +**Available in:** ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Flowable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Observable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Maybe`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Single`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Completable` + +**ReactiveX documentation:** [http://reactivex.io/documentation/operators/takeuntil.html](http://reactivex.io/documentation/operators/takeuntil.html) + +emits the items from the source Observable until a second Observable emits an item or issues a notification + +```java +Observable.range(1, 10).takeUntil(value -> value >= 5) + .subscribe(next -> System.out.printf("next: %s\n", next), // onNext + throwable -> System.out.printf("error: %s", throwable), //onError + () -> System.out.println("Completed") //onComplete + ); +``` + +## takeWhile + +**Available in:** ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Flowable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Observable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Maybe`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Single`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Completable` + +**ReactiveX documentation:** [http://reactivex.io/documentation/operators/takewhile.html](http://reactivex.io/documentation/operators/takewhile.html) + +emit items emitted by an Observable as long as a specified condition is true, then skip the remainder + +```java + Observable.range(1, 10).takeWhile(value -> value <= 5) + .subscribe(next -> System.out.printf("next: %s\n", next), // onNext + throwable -> System.out.printf("error: %s", throwable), //onError + () -> System.out.println("Completed") //onComplete + ); +``` + +### Boolean Operators + +### Outline + +- [`all`](#all) +- [`contains`](#contains) +- [`isEmpty`](#isEmpty) +- [`sequenceEqual`](#sequenceEqual) + +## all +**Available in:** ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Flowable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Observable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Maybe`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Single`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Completable` + +**ReactiveX documentation:** [http://reactivex.io/documentation/operators/all.html](http://reactivex.io/documentation/operators/all.html) + +determine whether all items emitted by an Observable meet some criteria + +```java +Flowable.range(0,10).doOnNext(next -> System.out.println(next)).all(integer -> integer<10). + blockingSubscribe(success->System.out.println("Success: "+success)); +``` + +## contains +**Available in:** ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Flowable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Observable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Maybe`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Single`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Completable` + +**ReactiveX documentation:** [http://reactivex.io/documentation/operators/contains.html](http://reactivex.io/documentation/operators/contains.html) + +determine whether an Observable emits a particular item or not + +```java +Flowable.range(1,10).doOnNext(next->System.out.println(next)) + .contains(4).blockingSubscribe(contains->System.out.println("contains: "+contains)); +``` + +## isEmpty +**Available in:** ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Flowable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Observable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Maybe`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Single`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Completable` + +**ReactiveX documentation:** [http://reactivex.io/documentation/operators/contains.html](http://reactivex.io/documentation/operators/contains.html) + +determine whether the source Publisher is empty + +```java +Flowable.empty().isEmpty().subscribe(isEmpty -> System.out.printf("isEmpty: %s", isEmpty)); +``` + +## sequenceEqual +**Available in:** ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Flowable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Observable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Maybe`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Single`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Completable` + +**ReactiveX documentation:** [http://reactivex.io/documentation/operators/sequenceequal.html](http://reactivex.io/documentation/operators/sequenceequal.html) + +test the equality of the sequences emitted by two Observables + +```java +Flowable flowable1 = Flowable.range(1,3).doOnNext(next-> System.out.print("flowable1: "+next + " ")); + +Flowable flowable2 = Flowable.range(1,3).doOnNext(next-> System.out.println("flowable2: "+next)); + +Flowable.sequenceEqual(Flowable.fromPublisher(flowable1),Flowable.fromPublisher(flowable2)) + .blockingSubscribe(sequenceEqual->System.out.println("sequenceEqual: "+sequenceEqual)); +``` diff --git a/docs/Connectable-Observable-Operators.md b/docs/Connectable-Observable-Operators.md new file mode 100644 index 0000000000..157ded821b --- /dev/null +++ b/docs/Connectable-Observable-Operators.md @@ -0,0 +1,70 @@ +This section explains the [`ConnectableObservable`](http://reactivex.io/RxJava/javadoc/rx/observables/ConnectableObservable.html) subclass and its operators: + +* [**`ConnectableObservable.connect( )`**](http://reactivex.io/documentation/operators/connect.html) — instructs a Connectable Observable to begin emitting items +* [**`Observable.publish( )`**](http://reactivex.io/documentation/operators/publish.html) — represents an Observable as a Connectable Observable +* [**`Observable.replay( )`**](http://reactivex.io/documentation/operators/replay.html) — ensures that all Subscribers see the same sequence of emitted items, even if they subscribe after the Observable begins emitting the items +* [**`ConnectableObservable.refCount( )`**](http://reactivex.io/documentation/operators/refcount.html) — makes a Connectable Observable behave like an ordinary Observable + +A Connectable Observable resembles an ordinary Observable, except that it does not begin emitting items when it is subscribed to, but only when its `connect()` method is called. In this way you can wait for all intended Subscribers to subscribe to the Observable before the Observable begins emitting items. + + + +The following example code shows two Subscribers subscribing to the same Observable. In the first case, they subscribe to an ordinary Observable; in the second case, they subscribe to a Connectable Observable that only connects after both Subscribers subscribe. Note the difference in the output: + +**Example #1:** +```java +Observable firstMillion = Observable.range(1, 1000000).sample(7, java.util.concurrent.TimeUnit.MILLISECONDS); + +firstMillion.subscribe(next -> System.out.println("Subscriber #1: " + next), // onNext + throwable -> System.out.println("Error: " + throwable), // onError + () -> System.out.println("Sequence #1 complete") // onComplete + ); +firstMillion.subscribe(next -> System.out.println("Subscriber #2: " + next), // onNext + throwable -> System.out.println("Error: " + throwable), // onError + () -> System.out.println("Sequence #2 complete") // onComplete + ); +``` +``` +Subscriber #1:211128 +Subscriber #1:411633 +Subscriber #1:629605 +Subscriber #1:841903 +Sequence #1 complete +Subscriber #2:244776 +Subscriber #2:431416 +Subscriber #2:621647 +Subscriber #2:826996 +Sequence #2 complete +``` +**Example #2:** +```java +ConnectableObservable firstMillion = Observable.range(1, 1000000).sample(7, java.util.concurrent.TimeUnit.MILLISECONDS).publish(); + +firstMillion.subscribe(next -> System.out.println("Subscriber #1: " + next), // onNext + throwable -> System.out.println("Error: " + throwable), // onError + () -> System.out.println("Sequence #1 complete") // onComplete + ); + +firstMillion.subscribe(next -> System.out.println("Subscriber #2: " + next), // onNext + throwable -> System.out.println("Error: " + throwable), // onError + () -> System.out.println("Sequence #2 complete") // onComplete + ); + +firstMillion.connect(); +``` +``` +Subscriber #2:208683 +Subscriber #1:208683 +Subscriber #2:432509 +Subscriber #1:432509 +Subscriber #2:644270 +Subscriber #1:644270 +Subscriber #2:887885 +Subscriber #1:887885 +Sequence #2 complete +Sequence #1 complete +``` + +#### see also: +* javadoc: `ConnectableObservable` +* Introduction to Rx: Publish and Connect \ No newline at end of file diff --git a/docs/Creating-Observables.md b/docs/Creating-Observables.md new file mode 100644 index 0000000000..360e03ab09 --- /dev/null +++ b/docs/Creating-Observables.md @@ -0,0 +1,426 @@ +This page shows methods that create reactive sources, such as `Observable`s. + +### Outline + +- [`create`](#create) +- [`defer`](#defer) +- [`empty`](#empty) +- [`error`](#error) +- [`from`](#from) +- [`generate`](#generate) +- [`interval`](#interval) +- [`just`](#just) +- [`never`](#never) +- [`range`](#range) +- [`timer`](#timer) + +## just + +**Available in:** ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Flowable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Observable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Maybe`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Single`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Completable` + +**ReactiveX documentation:** [http://reactivex.io/documentation/operators/just.html](http://reactivex.io/documentation/operators/just.html) + +Constructs a reactive type by taking a pre-existing object and emitting that specific object to the downstream consumer upon subscription. + +#### just example: + +```java +String greeting = "Hello world!"; + +Observable observable = Observable.just(greeting); + +observable.subscribe(item -> System.out.println(item)); +``` + +There exist overloads with 2 to 9 arguments for convenience, which objects (with the same common type) will be emitted in the order they are specified. + +```java +Observable observable = Observable.just("1", "A", "3.2", "def"); + + observable.subscribe(item -> System.out.print(item), error -> error.printStackTrace(), + () -> System.out.println()); +``` + +## From + +Constructs a sequence from a pre-existing source or generator type. + +*Note: These static methods use the postfix naming convention (i.e., the argument type is repeated in the method name) to avoid overload resolution ambiguities.* + +**ReactiveX documentation:** [http://reactivex.io/documentation/operators/from.html](http://reactivex.io/documentation/operators/from.html) + +### fromIterable + +**Available in:** ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Flowable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Observable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Maybe`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Single`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Completable` + +Signals the items from a `java.lang.Iterable` source (such as `List`s, `Set`s or `Collection`s or custom `Iterable`s) and then completes the sequence. + +#### fromIterable example: + +```java +List list = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8)); + +Observable observable = Observable.fromIterable(list); + +observable.subscribe(item -> System.out.println(item), error -> error.printStackTrace(), + () -> System.out.println("Done")); +``` + +### fromArray + +**Available in:** ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Flowable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Observable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Maybe`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Single`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Completable` + +Signals the elements of the given array and then completes the sequence. + +#### fromArray example: + +```java +Integer[] array = new Integer[10]; +for (int i = 0; i < array.length; i++) { + array[i] = i; +} + +Observable observable = Observable.fromArray(array); + +observable.subscribe(item -> System.out.println(item), error -> error.printStackTrace(), + () -> System.out.println("Done")); +``` + +*Note: RxJava does not support primitive arrays, only (generic) reference arrays.* + +### fromCallable + +**Available in:** ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Flowable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Observable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Maybe`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Single`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Completable` + +When a consumer subscribes, the given `java.util.concurrent.Callable` is invoked and its returned value (or thrown exception) is relayed to that consumer. + +#### fromCallable example: + +```java +Callable callable = () -> { + System.out.println("Hello World!"); + return "Hello World!"); +} + +Observable observable = Observable.fromCallable(callable); + +observable.subscribe(item -> System.out.println(item), error -> error.printStackTrace(), + () -> System.out.println("Done")); +``` + +*Remark: In `Completable`, the actual returned value is ignored and the `Completable` simply completes.* + +## fromAction + +**Available in:** ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Flowable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Observable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Maybe`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Single`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Completable` + +When a consumer subscribes, the given `io.reactivex.function.Action` is invoked and the consumer completes or receives the exception the `Action` threw. + +#### fromAction example: + +```java +Action action = () -> System.out.println("Hello World!"); + +Completable completable = Completable.fromAction(action); + +completable.subscribe(() -> System.out.println("Done"), error -> error.printStackTrace()); +``` + +*Note: the difference between `fromAction` and `fromRunnable` is that the `Action` interface allows throwing a checked exception while the `java.lang.Runnable` does not.* + +## fromRunnable + +**Available in:** ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Flowable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Observable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Maybe`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Single`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Completable` + +When a consumer subscribes, the given `io.reactivex.function.Action` is invoked and the consumer completes or receives the exception the `Action` threw. + +#### fromRunnable example: + +```java +Runnable runnable = () -> System.out.println("Hello World!"); + +Completable completable = Completable.fromRunnable(runnable); + +completable.subscribe(() -> System.out.println("Done"), error -> error.printStackTrace()); +``` + +*Note: the difference between `fromAction` and `fromRunnable` is that the `Action` interface allows throwing a checked exception while the `java.lang.Runnable` does not.* + +### fromFuture + +**Available in:** ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Flowable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Observable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Maybe`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Single`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Completable` + +Given a pre-existing, already running or already completed `java.util.concurrent.Future`, wait for the `Future` to complete normally or with an exception in a blocking fashion and relay the produced value or exception to the consumers. + +#### fromFuture example: + +```java +ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor(); + +Future future = executor.schedule(() -> "Hello world!", 1, TimeUnit.SECONDS); + +Observable observable = Observable.fromFuture(future); + +observable.subscribe( + item -> System.out.println(item), + error -> error.printStackTrace(), + () -> System.out.println("Done")); + +executor.shutdown(); +``` + +### from{reactive type} + +Wraps or converts another reactive type to the target reactive type. + +The following combinations are available in the various reactive types with the following signature pattern: `targetType.from{sourceType}()` + +**Available in:** + +targetType \ sourceType | Publisher | Observable | Maybe | Single | Completable +----|---------------|-----------|---------|-----------|---------------- +Flowable | ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) | | | | | +Observable | ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) | | | | | +Maybe | | | | ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) | ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) +Single | ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) | ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) | | | +Completable | ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) | ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) | ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) | ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) | + +*Note: not all possible conversion is implemented via the `from{reactive type}` method families. Check out the `to{reactive type}` method families for further conversion possibilities. + +#### from{reactive type} example: + +```java +Flux reactorFlux = Flux.fromCompletionStage(CompletableFuture.completedFuture(1)); + +Observable observable = Observable.fromPublisher(reactorFlux); + +observable.subscribe( + item -> System.out.println(item), + error -> error.printStackTrace(), + () -> System.out.println("Done")); +``` + +## generate + +**Available in:** ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Flowable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Observable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Maybe`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Single`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Completable` + +**ReactiveX documentation:** [http://reactivex.io/documentation/operators/create.html](http://reactivex.io/documentation/operators/create.html) + +Creates a cold, synchronous and stateful generator of values. + +#### generate example: + +```java +int startValue = 1; +int incrementValue = 1; +Flowable flowable = Flowable.generate(() -> startValue, (s, emitter) -> { + int nextValue = s + incrementValue; + emitter.onNext(nextValue); + return nextValue; +}); +flowable.subscribe(value -> System.out.println(value)); +``` + +## create + +**Available in:** ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Flowable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Observable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Maybe`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Single`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Completable` + +**ReactiveX documentation:** [http://reactivex.io/documentation/operators/create.html](http://reactivex.io/documentation/operators/create.html) + +Construct a **safe** reactive type instance which when subscribed to by a consumer, runs an user-provided function and provides a type-specific `Emitter` for this function to generate the signal(s) the designated business logic requires. This method allows bridging the non-reactive, usually listener/callback-style world, with the reactive world. + +#### create example: + +```java +ScheduledExecutorService executor = Executors.newSingleThreadedScheduledExecutor(); + +ObservableOnSubscribe handler = emitter -> { + + Future future = executor.schedule(() -> { + emitter.onNext("Hello"); + emitter.onNext("World"); + emitter.onComplete(); + return null; + }, 1, TimeUnit.SECONDS); + + emitter.setCancellable(() -> future.cancel(false)); +}; + +Observable observable = Observable.create(handler); + +observable.subscribe(item -> System.out.println(item), error -> error.printStackTrace(), + () -> System.out.println("Done")); + +Thread.sleep(2000); +executor.shutdown(); +``` + +*Note: `Flowable.create()` must also specify the backpressure behavior to be applied when the user-provided function generates more items than the downstream consumer has requested.* + +## defer + +**Available in:** ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Flowable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Observable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Maybe`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Single`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Completable` + +**ReactiveX documentation:** [http://reactivex.io/documentation/operators/defer.html](http://reactivex.io/documentation/operators/defer.html) + +Calls an user-provided `java.util.concurrent.Callable` when a consumer subscribes to the reactive type so that the `Callable` can generate the actual reactive instance to relay signals from towards the consumer. `defer` allows: + +- associating a per-consumer state with such generated reactive instances, +- allows executing side-effects before an actual/generated reactive instance gets subscribed to, +- turn hot sources (i.e., `Subject`s and `Processor`s) into cold sources by basically making those hot sources not exist until a consumer subscribes. + +#### defer example: + +```java +Observable observable = Observable.defer(() -> { + long time = System.currentTimeMillis(); + return Observable.just(time); +}); + +observable.subscribe(time -> System.out.println(time)); + +Thread.sleep(1000); + +observable.subscribe(time -> System.out.println(time)); +``` + +## range + +**Available in:** ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Flowable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Observable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Maybe`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Single`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Completable` + +**ReactiveX documentation:** [http://reactivex.io/documentation/operators/range.html](http://reactivex.io/documentation/operators/range.html) + +Generates a sequence of values to each individual consumer. The `range()` method generates `Integer`s, the `rangeLong()` generates `Long`s. + +#### range example: +```java +String greeting = "Hello World!"; + +Observable indexes = Observable.range(0, greeting.length()); + +Observable characters = indexes + .map(index -> greeting.charAt(index)); + +characters.subscribe(character -> System.out.print(character), error -> error.printStackTrace(), + () -> System.out.println()); +``` + +## interval + +**Available in:** ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Flowable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Observable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Maybe`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Single`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Completable` + +**ReactiveX documentation:** [http://reactivex.io/documentation/operators/interval.html](http://reactivex.io/documentation/operators/interval.html) + +Periodically generates an infinite, ever increasing numbers (of type `Long`). The `intervalRange` variant generates a limited amount of such numbers. + +#### interval example: + +```java +Observable clock = Observable.interval(1, TimeUnit.SECONDS); + +clock.subscribe(time -> { + if (time % 2 == 0) { + System.out.println("Tick"); + } else { + System.out.println("Tock"); + } +}); +``` + +## timer + +**Available in:** ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Flowable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Observable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Maybe`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Single`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Completable` + +**ReactiveX documentation:** [http://reactivex.io/documentation/operators/timer.html](http://reactivex.io/documentation/operators/timer.html) + +After the specified time, this reactive source signals a single `0L` (then completes for `Flowable` and `Observable`). + +#### timer example: + +```java +Observable eggTimer = Observable.timer(5, TimeUnit.MINUTES); + +eggTimer.blockingSubscribe(v -> System.out.println("Egg is ready!")); +``` + +## empty + +**Available in:** ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Flowable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Observable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Maybe`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Single`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Completable` + +**ReactiveX documentation:** [http://reactivex.io/documentation/operators/empty-never-throw.html](http://reactivex.io/documentation/operators/empty-never-throw.html) + +This type of source signals completion immediately upon subscription. + +#### empty example: + +```java +Observable empty = Observable.empty(); + +empty.subscribe( + v -> System.out.println("This should never be printed!"), + error -> System.out.println("Or this!"), + () -> System.out.println("Done will be printed.")); +``` + +## never + +**Available in:** ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Flowable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Observable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Maybe`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Single`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Completable` + +**ReactiveX documentation:** [http://reactivex.io/documentation/operators/empty-never-throw.html](http://reactivex.io/documentation/operators/empty-never-throw.html) + +This type of source does not signal any `onNext`, `onSuccess`, `onError` or `onComplete`. This type of reactive source is useful in testing or "disabling" certain sources in combinator operators. + +#### never example: + +```java +Observable never = Observable.never(); + +never.subscribe( + v -> System.out.println("This should never be printed!"), + error -> System.out.println("Or this!"), + () -> System.out.println("This neither!")); +``` + +## error + +**Available in:** ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Flowable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Observable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Maybe`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Single`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Completable` + +**ReactiveX documentation:** [http://reactivex.io/documentation/operators/empty-never-throw.html](http://reactivex.io/documentation/operators/empty-never-throw.html) + +Signal an error, either pre-existing or generated via a `java.util.concurrent.Callable`, to the consumer. + +#### error example: + +```java +Observable error = Observable.error(new IOException()); + +error.subscribe( + v -> System.out.println("This should never be printed!"), + e -> e.printStackTrace(), + () -> System.out.println("This neither!")); +``` + +A typical use case is to conditionally map or suppress an exception in a chain utilizing `onErrorResumeNext`: + +```java +Observable observable = Observable.fromCallable(() -> { + if (Math.random() < 0.5) { + throw new IOException(); + } + throw new IllegalArgumentException(); +}); + +Observable result = observable.onErrorResumeNext(error -> { + if (error instanceof IllegalArgumentException) { + return Observable.empty(); + } + return Observable.error(error); +}); + +for (int i = 0; i < 10; i++) { + result.subscribe( + v -> System.out.println("This should never be printed!"), + error -> error.printStackTrace(), + () -> System.out.println("Done")); +} +``` \ No newline at end of file diff --git a/docs/Error-Handling-Operators.md b/docs/Error-Handling-Operators.md new file mode 100644 index 0000000000..0b2eace61f --- /dev/null +++ b/docs/Error-Handling-Operators.md @@ -0,0 +1,284 @@ +There are a variety of operators that you can use to react to or recover from `onError` notifications from reactive sources, such as `Observable`s. For example, you might: + +1. swallow the error and switch over to a backup Observable to continue the sequence +1. swallow the error and emit a default item +1. swallow the error and immediately try to restart the failed Observable +1. swallow the error and try to restart the failed Observable after some back-off interval + +# Outline + +- [`doOnError`](#doonerror) +- [`onErrorComplete`](#onerrorcomplete) +- [`onErrorResumeNext`](#onerrorresumenext) +- [`onErrorReturn`](#onerrorreturn) +- [`onErrorReturnItem`](#onerrorreturnitem) +- [`onExceptionResumeNext`](#onexceptionresumenext) +- [`retry`](#retry) +- [`retryUntil`](#retryuntil) +- [`retryWhen`](#retrywhen) + +## doOnError + +**Available in:** ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Flowable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Observable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Maybe`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Single`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Completable` + +**ReactiveX documentation:** [http://reactivex.io/documentation/operators/do.html](http://reactivex.io/documentation/operators/do.html) + +Instructs a reactive type to invoke the given `io.reactivex.functions.Consumer` when it encounters an error. + +### doOnError example + +```java +Observable.error(new IOException("Something went wrong")) + .doOnError(error -> System.err.println("The error message is: " + error.getMessage())) + .subscribe( + x -> System.out.println("onNext should never be printed!"), + Throwable::printStackTrace, + () -> System.out.println("onComplete should never be printed!")); +``` + +## onErrorComplete + +**Available in:** ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Flowable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Observable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Maybe`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Single`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Completable` + +**ReactiveX documentation:** [http://reactivex.io/documentation/operators/catch.html](http://reactivex.io/documentation/operators/catch.html) + +Instructs a reactive type to swallow an error event and replace it by a completion event. + +Optionally, a `io.reactivex.functions.Predicate` can be specified that gives more control over when an error event should be replaced by a completion event, and when not. + +### onErrorComplete example + +```java +Completable.fromAction(() -> { + throw new IOException(); +}).onErrorComplete(error -> { + // Only ignore errors of type java.io.IOException. + return error instanceof IOException; +}).subscribe( + () -> System.out.println("IOException was ignored"), + error -> System.err.println("onError should not be printed!")); +``` + +## onErrorResumeNext + +**Available in:** ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Flowable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Observable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Maybe`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Single`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Completable` + +**ReactiveX documentation:** [http://reactivex.io/documentation/operators/catch.html](http://reactivex.io/documentation/operators/catch.html) + +Instructs a reactive type to emit a sequence of items if it encounters an error. + +### onErrorResumeNext example + +```java + Observable numbers = Observable.generate(() -> 1, (state, emitter) -> { + emitter.onNext(state); + + return state + 1; +}); + +numbers.scan(Math::multiplyExact) + .onErrorResumeNext(Observable.empty()) + .subscribe( + System.out::println, + error -> System.err.println("onError should not be printed!")); + +// prints: +// 1 +// 2 +// 6 +// 24 +// 120 +// 720 +// 5040 +// 40320 +// 362880 +// 3628800 +// 39916800 +// 479001600 +``` + +## onErrorReturn + +**Available in:** ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Flowable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Observable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Maybe`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Single`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Completable` + +**ReactiveX documentation:** [http://reactivex.io/documentation/operators/catch.html](http://reactivex.io/documentation/operators/catch.html) + +Instructs a reactive type to emit the item returned by the specified `io.reactivex.functions.Function` when it encounters an error. + +### onErrorReturn example + +```java +Single.just("2A") + .map(v -> Integer.parseInt(v, 10)) + .onErrorReturn(error -> { + if (error instanceof NumberFormatException) return 0; + else throw new IllegalArgumentException(); + }) + .subscribe( + System.out::println, + error -> System.err.println("onError should not be printed!")); + +// prints 0 +``` + +## onErrorReturnItem + +**Available in:** ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Flowable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Observable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Maybe`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Single`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Completable` + +**ReactiveX documentation:** [http://reactivex.io/documentation/operators/catch.html](http://reactivex.io/documentation/operators/catch.html) + +Instructs a reactive type to emit a particular item when it encounters an error. + +### onErrorReturnItem example + +```java +Single.just("2A") + .map(v -> Integer.parseInt(v, 10)) + .onErrorReturnItem(0) + .subscribe( + System.out::println, + error -> System.err.println("onError should not be printed!")); + +// prints 0 +``` + +## onExceptionResumeNext + +**Available in:** ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Flowable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Observable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Maybe`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Single`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Completable` + +**ReactiveX documentation:** [http://reactivex.io/documentation/operators/catch.html](http://reactivex.io/documentation/operators/catch.html) + +Instructs a reactive type to continue emitting items after it encounters an `java.lang.Exception`. Unlike [`onErrorResumeNext`](#onerrorresumenext), this one lets other types of `Throwable` continue through. + +### onExceptionResumeNext example + +```java +Observable exception = Observable.error(IOException::new) + .onExceptionResumeNext(Observable.just("This value will be used to recover from the IOException")); + +Observable error = Observable.error(Error::new) + .onExceptionResumeNext(Observable.just("This value will not be used")); + +Observable.concat(exception, error) + .subscribe( + message -> System.out.println("onNext: " + message), + err -> System.err.println("onError: " + err)); + +// prints: +// onNext: This value will be used to recover from the IOException +// onError: java.lang.Error +``` + +## retry + +**Available in:** ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Flowable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Observable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Maybe`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Single`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Completable` + +**ReactiveX documentation:** [http://reactivex.io/documentation/operators/retry.html](http://reactivex.io/documentation/operators/retry.html) + +Instructs a reactive type to resubscribe to the source reactive type if it encounters an error in the hopes that it will complete without error. + +### retry example + +```java +Observable source = Observable.interval(0, 1, TimeUnit.SECONDS) + .flatMap(x -> { + if (x >= 2) return Observable.error(new IOException("Something went wrong!")); + else return Observable.just(x); + }); + +source.retry((retryCount, error) -> retryCount < 3) + .blockingSubscribe( + x -> System.out.println("onNext: " + x), + error -> System.err.println("onError: " + error.getMessage())); + +// prints: +// onNext: 0 +// onNext: 1 +// onNext: 0 +// onNext: 1 +// onNext: 0 +// onNext: 1 +// onError: Something went wrong! +``` + +## retryUntil + +**Available in:** ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Flowable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Observable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Maybe`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Single`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Completable` + +**ReactiveX documentation:** [http://reactivex.io/documentation/operators/retry.html](http://reactivex.io/documentation/operators/retry.html) + +Instructs a reactive type to resubscribe to the source reactive type if it encounters an error until the given `io.reactivex.functions.BooleanSupplier` returns `true`. + +### retryUntil example + +```java +LongAdder errorCounter = new LongAdder(); +Observable source = Observable.interval(0, 1, TimeUnit.SECONDS) + .flatMap(x -> { + if (x >= 2) return Observable.error(new IOException("Something went wrong!")); + else return Observable.just(x); + }) + .doOnError((error) -> errorCounter.increment()); + +source.retryUntil(() -> errorCounter.intValue() >= 3) + .blockingSubscribe( + x -> System.out.println("onNext: " + x), + error -> System.err.println("onError: " + error.getMessage())); + +// prints: +// onNext: 0 +// onNext: 1 +// onNext: 0 +// onNext: 1 +// onNext: 0 +// onNext: 1 +// onError: Something went wrong! +``` + +## retryWhen + +**Available in:** ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Flowable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Observable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Maybe`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Single`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Completable` + +**ReactiveX documentation:** [http://reactivex.io/documentation/operators/retry.html](http://reactivex.io/documentation/operators/retry.html) + +Instructs a reactive type to pass any error to another `Observable` or `Flowable` to determine whether to resubscribe to the source. + +### retryWhen example + +```java +Observable source = Observable.interval(0, 1, TimeUnit.SECONDS) + .flatMap(x -> { + if (x >= 2) return Observable.error(new IOException("Something went wrong!")); + else return Observable.just(x); + }); + +source.retryWhen(errors -> { + return errors.map(error -> 1) + + // Count the number of errors. + .scan(Math::addExact) + + .doOnNext(errorCount -> System.out.println("No. of errors: " + errorCount)) + + // Limit the maximum number of retries. + .takeWhile(errorCount -> errorCount < 3) + + // Signal resubscribe event after some delay. + .flatMapSingle(errorCount -> Single.timer(errorCount, TimeUnit.SECONDS)); +}).blockingSubscribe( + x -> System.out.println("onNext: " + x), + Throwable::printStackTrace, + () -> System.out.println("onComplete")); + +// prints: +// onNext: 0 +// onNext: 1 +// No. of errors: 1 +// onNext: 0 +// onNext: 1 +// No. of errors: 2 +// onNext: 0 +// onNext: 1 +// No. of errors: 3 +// onComplete +``` diff --git a/docs/Error-Handling.md b/docs/Error-Handling.md new file mode 100644 index 0000000000..3de9c396d9 --- /dev/null +++ b/docs/Error-Handling.md @@ -0,0 +1,24 @@ +An Observable typically does not _throw_ exceptions. Instead it notifies any observers that an unrecoverable error has occurred by terminating the Observable sequence with an `onError` notification. + +There are some exceptions to this. For example, if the `onError()` call _itself_ fails, the Observable will not attempt to notify the observer of this by again calling `onError` but will throw a `RuntimeException`, an `OnErrorFailedException`, or an `OnErrorNotImplementedException`. + +# Techniques for recovering from onError notifications + +So rather than _catch_ exceptions, your observer or operator should more typically respond to `onError` notifications of exceptions. There are also a variety of Observable operators that you can use to react to or recover from `onError` notifications from Observables. For example, you might use an operator to: + +1. swallow the error and switch over to a backup Observable to continue the sequence +1. swallow the error and emit a default item +1. swallow the error and immediately try to restart the failed Observable +1. swallow the error and try to restart the failed Observable after some back-off interval + +You can use the operators described in [[Error Handling Operators]] to implement these strategies. + +# RxJava-specific exceptions and what to do about them + +
+
CompositeException
This indicates that more than one exception occurred. You can use the exception’s getExceptions() method to retrieve the individual exceptions that make up the composite.
+
MissingBackpressureException
This indicates that a Subscriber or operator attempted to apply reactive pull backpressure to an Observable that does not implement it. See [[Backpressure]] for work-arounds for Observables that do not implement reactive pull backpressure.
+
OnErrorFailedException
This indicates that an Observable tried to call its observer’s onError() method, but that method itself threw an exception.
+
OnErrorNotImplementedException
This indicates that an Observable tried to call its observer’s onError() method, but that no such method existed. You can eliminate this by either fixing the Observable so that it no longer reaches an error condition, by implementing an onError handler in the observer, or by intercepting the onError notification before it reaches the observer by using one of the operators described elsewhere on this page.
+
OnErrorThrowable
Observers pass throwables of this sort into their observers’ onError() handlers. A Throwable of this variety contains more information about the error and about the Observable-specific state of the system at the time of the error than does a standard Throwable.
+
\ No newline at end of file diff --git a/docs/Filtering-Observables.md b/docs/Filtering-Observables.md new file mode 100644 index 0000000000..512b69ba8a --- /dev/null +++ b/docs/Filtering-Observables.md @@ -0,0 +1,760 @@ +This page shows operators you can use to filter and select items emitted by reactive sources, such as `Observable`s. + +# Outline + +- [`debounce`](#debounce) +- [`distinct`](#distinct) +- [`distinctUntilChanged`](#distinctuntilchanged) +- [`elementAt`](#elementat) +- [`elementAtOrError`](#elementatorerror) +- [`filter`](#filter) +- [`first`](#first) +- [`firstElement`](#firstelement) +- [`firstOrError`](#firstorerror) +- [`ignoreElement`](#ignoreelement) +- [`ignoreElements`](#ignoreelements) +- [`last`](#last) +- [`lastElement`](#lastelement) +- [`lastOrError`](#lastorerror) +- [`ofType`](#oftype) +- [`sample`](#sample) +- [`skip`](#skip) +- [`skipLast`](#skiplast) +- [`take`](#take) +- [`takeLast`](#takelast) +- [`throttleFirst`](#throttlefirst) +- [`throttleLast`](#throttlelast) +- [`throttleLatest`](#throttlelatest) +- [`throttleWithTimeout`](#throttlewithtimeout) +- [`timeout`](#timeout) + +## debounce + +**Available in:** ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Flowable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Observable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Maybe`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Single`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Completable` + +**ReactiveX documentation:** [http://reactivex.io/documentation/operators/debounce.html](http://reactivex.io/documentation/operators/debounce.html) + +Drops items emitted by a reactive source that are followed by newer items before the given timeout value expires. The timer resets on each emission. + +This operator keeps track of the most recent emitted item, and emits this item only when enough time has passed without the source emitting any other items. + +### debounce example + +```java +// Diagram: +// -A--------------B----C-D-------------------E-|----> +// a---------1s +// b---------1s +// c---------1s +// d---------1s +// e-|----> +// -----------A---------------------D-----------E-|--> + +Observable source = Observable.create(emitter -> { + emitter.onNext("A"); + + Thread.sleep(1_500); + emitter.onNext("B"); + + Thread.sleep(500); + emitter.onNext("C"); + + Thread.sleep(250); + emitter.onNext("D"); + + Thread.sleep(2_000); + emitter.onNext("E"); + emitter.onComplete(); +}); + +source.subscribeOn(Schedulers.io()) + .debounce(1, TimeUnit.SECONDS) + .blockingSubscribe( + item -> System.out.println("onNext: " + item), + Throwable::printStackTrace, + () -> System.out.println("onComplete")); + +// prints: +// onNext: A +// onNext: D +// onNext: E +// onComplete +``` + +## distinct + +**Available in:** ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Flowable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Observable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Maybe`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Single`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Completable` + +**ReactiveX documentation:** [http://reactivex.io/documentation/operators/distinct.html](http://reactivex.io/documentation/operators/distinct.html) + +Filters a reactive source by only emitting items that are distinct by comparison from previous items. A `io.reactivex.functions.Function` can be specified that projects each item emitted by the source into a new value that will be used for comparison with previous projected values. + +### distinct example + +```java +Observable.just(2, 3, 4, 4, 2, 1) + .distinct() + .subscribe(System.out::println); + +// prints: +// 2 +// 3 +// 4 +// 1 +``` + +## distinctUntilChanged + +**Available in:** ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Flowable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Observable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Maybe`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Single`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Completable` + +**ReactiveX documentation:** [http://reactivex.io/documentation/operators/distinct.html](http://reactivex.io/documentation/operators/distinct.html) + +Filters a reactive source by only emitting items that are distinct by comparison from their immediate predecessors. A `io.reactivex.functions.Function` can be specified that projects each item emitted by the source into a new value that will be used for comparison with previous projected values. Alternatively, a `io.reactivex.functions.BiPredicate` can be specified that is used as the comparator function to compare immediate predecessors with each other. + +### distinctUntilChanged example + +```java +Observable.just(1, 1, 2, 1, 2, 3, 3, 4) + .distinctUntilChanged() + .subscribe(System.out::println); + +// prints: +// 1 +// 2 +// 1 +// 2 +// 3 +// 4 +``` + +## elementAt + +**Available in:** ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Flowable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Observable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Maybe`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Single`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Completable` + +**ReactiveX documentation:** [http://reactivex.io/documentation/operators/elementat.html](http://reactivex.io/documentation/operators/elementat.html) + +Emits the single item at the specified zero-based index in a sequence of emissions from a reactive source. A default item can be specified that will be emitted if the specified index is not within the sequence. + +### elementAt example + +```java +Observable source = Observable.generate(() -> 1L, (state, emitter) -> { + emitter.onNext(state); + + return state + 1L; +}).scan((product, x) -> product * x); + +Maybe element = source.elementAt(5); +element.subscribe(System.out::println); + +// prints 720 +``` + +## elementAtOrError + +**Available in:** ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Flowable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Observable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Maybe`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Single`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Completable` + +**ReactiveX documentation:** [http://reactivex.io/documentation/operators/elementat.html](http://reactivex.io/documentation/operators/elementat.html) + +Emits the single item at the specified zero-based index in a sequence of emissions from a reactive source, or signals a `java.util.NoSuchElementException` if the specified index is not within the sequence. + +### elementAtOrError example + +```java +Observable source = Observable.just("Kirk", "Spock", "Chekov", "Sulu"); +Single element = source.elementAtOrError(4); + +element.subscribe( + name -> System.out.println("onSuccess will not be printed!"), + error -> System.out.println("onError: " + error)); + +// prints: +// onError: java.util.NoSuchElementException +``` + +## filter + +**Available in:** ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Flowable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Observable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Maybe`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Single`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Completable` + +**ReactiveX documentation:** [http://reactivex.io/documentation/operators/filter.html](http://reactivex.io/documentation/operators/filter.html) + +Filters items emitted by a reactive source by only emitting those that satisfy a specified predicate. + +### filter example + +```java +Observable.just(1, 2, 3, 4, 5, 6) + .filter(x -> x % 2 == 0) + .subscribe(System.out::println); + +// prints: +// 2 +// 4 +// 6 +``` + +## first + +**Available in:** ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Flowable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Observable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Maybe`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Single`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Completable` + +**ReactiveX documentation:** [http://reactivex.io/documentation/operators/first.html](http://reactivex.io/documentation/operators/first.html) + +Emits only the first item emitted by a reactive source, or emits the given default item if the source completes without emitting an item. This differs from [`firstElement`](#firstelement) in that this operator returns a `Single` whereas [`firstElement`](#firstelement) returns a `Maybe`. + +### first example + +```java +Observable source = Observable.just("A", "B", "C"); +Single firstOrDefault = source.first("D"); + +firstOrDefault.subscribe(System.out::println); + +// prints A +``` + +## firstElement + +**Available in:** ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Flowable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Observable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Maybe`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Single`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Completable` + +**ReactiveX documentation:** [http://reactivex.io/documentation/operators/first.html](http://reactivex.io/documentation/operators/first.html) + +Emits only the first item emitted by a reactive source, or just completes if the source completes without emitting an item. This differs from [`first`](#first) in that this operator returns a `Maybe` whereas [`first`](#first) returns a `Single`. + +### firstElement example + +```java +Observable source = Observable.just("A", "B", "C"); +Maybe first = source.firstElement(); + +first.subscribe(System.out::println); + +// prints A +``` + +## firstOrError + +**Available in:** ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Flowable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Observable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Maybe`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Single`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Completable` + +**ReactiveX documentation:** [http://reactivex.io/documentation/operators/first.html](http://reactivex.io/documentation/operators/first.html) + +Emits only the first item emitted by a reactive source, or signals a `java.util.NoSuchElementException` if the source completes without emitting an item. + +### firstOrError example + +```java +Observable emptySource = Observable.empty(); +Single firstOrError = emptySource.firstOrError(); + +firstOrError.subscribe( + element -> System.out.println("onSuccess will not be printed!"), + error -> System.out.println("onError: " + error)); + +// prints: +// onError: java.util.NoSuchElementException +``` + +## ignoreElement + +**Available in:** ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Flowable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Observable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Maybe`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Single`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Completable` + +**ReactiveX documentation:** [http://reactivex.io/documentation/operators/ignoreelements.html](http://reactivex.io/documentation/operators/ignoreelements.html) + +Ignores the single item emitted by a `Single` or `Maybe` source, and returns a `Completable` that signals only the error or completion event from the source. + +### ignoreElement example + +```java +Single source = Single.timer(1, TimeUnit.SECONDS); +Completable completable = source.ignoreElement(); + +completable.doOnComplete(() -> System.out.println("Done!")) + .blockingAwait(); + +// prints (after 1 second): +// Done! +``` + +## ignoreElements + +**Available in:** ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Flowable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Observable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Maybe`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Single`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Completable` + +**ReactiveX documentation:** [http://reactivex.io/documentation/operators/ignoreelements.html](http://reactivex.io/documentation/operators/ignoreelements.html) + +Ignores all items from the `Observable` or `Flowable` source, and returns a `Completable` that signals only the error or completion event from the source. + +### ignoreElements example + +```java +Observable source = Observable.intervalRange(1, 5, 1, 1, TimeUnit.SECONDS); +Completable completable = source.ignoreElements(); + +completable.doOnComplete(() -> System.out.println("Done!")) + .blockingAwait(); + +// prints (after 5 seconds): +// Done! +``` + +## last + +**Available in:** ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Flowable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Observable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Maybe`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Single`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Completable` + +**ReactiveX documentation:** [http://reactivex.io/documentation/operators/last.html](http://reactivex.io/documentation/operators/last.html) + +Emits only the last item emitted by a reactive source, or emits the given default item if the source completes without emitting an item. This differs from [`lastElement`](#lastelement) in that this operator returns a `Single` whereas [`lastElement`](#lastelement) returns a `Maybe`. + +### last example + +```java +Observable source = Observable.just("A", "B", "C"); +Single lastOrDefault = source.last("D"); + +lastOrDefault.subscribe(System.out::println); + +// prints C +``` + +## lastElement + +**Available in:** ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Flowable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Observable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Maybe`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Single`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Completable` + +**ReactiveX documentation:** [http://reactivex.io/documentation/operators/last.html](http://reactivex.io/documentation/operators/last.html) + +Emits only the last item emitted by a reactive source, or just completes if the source completes without emitting an item. This differs from [`last`](#last) in that this operator returns a `Maybe` whereas [`last`](#last) returns a `Single`. + +### lastElement example + +```java +Observable source = Observable.just("A", "B", "C"); +Maybe last = source.lastElement(); + +last.subscribe(System.out::println); + +// prints C +``` + +## lastOrError + +**Available in:** ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Flowable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Observable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Maybe`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Single`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Completable` + +**ReactiveX documentation:** [http://reactivex.io/documentation/operators/last.html](http://reactivex.io/documentation/operators/last.html) + +Emits only the last item emitted by a reactive source, or signals a `java.util.NoSuchElementException` if the source completes without emitting an item. + +### lastOrError example + +```java +Observable emptySource = Observable.empty(); +Single lastOrError = emptySource.lastOrError(); + +lastOrError.subscribe( + element -> System.out.println("onSuccess will not be printed!"), + error -> System.out.println("onError: " + error)); + +// prints: +// onError: java.util.NoSuchElementException +``` + +## ofType + +**Available in:** ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Flowable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Observable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Maybe`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Single`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Completable` + +**ReactiveX documentation:** [http://reactivex.io/documentation/operators/filter.html](http://reactivex.io/documentation/operators/filter.html) + +Filters items emitted by a reactive source by only emitting those of the specified type. + +### ofType example + +```java +Observable numbers = Observable.just(1, 4.0, 3, 2.71, 2f, 7); +Observable integers = numbers.ofType(Integer.class); + +integers.subscribe((Integer x) -> System.out.println(x)); + +// prints: +// 1 +// 3 +// 7 +``` + +## sample + +**Available in:** ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Flowable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Observable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Maybe`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Single`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Completable` + +**ReactiveX documentation:** [http://reactivex.io/documentation/operators/sample.html](http://reactivex.io/documentation/operators/sample.html) + +Filters items emitted by a reactive source by only emitting the most recently emitted item within periodic time intervals. + +### sample example + +```java +// Diagram: +// -A----B-C-------D-----E-|--> +// -0s-----c--1s---d----2s-|--> +// -----------C---------D--|--> + +Observable source = Observable.create(emitter -> { + emitter.onNext("A"); + + Thread.sleep(500); + emitter.onNext("B"); + + Thread.sleep(200); + emitter.onNext("C"); + + Thread.sleep(800); + emitter.onNext("D"); + + Thread.sleep(600); + emitter.onNext("E"); + emitter.onComplete(); +}); + +source.subscribeOn(Schedulers.io()) + .sample(1, TimeUnit.SECONDS) + .blockingSubscribe( + item -> System.out.println("onNext: " + item), + Throwable::printStackTrace, + () -> System.out.println("onComplete")); + +// prints: +// onNext: C +// onNext: D +// onComplete +``` + +## skip + +**Available in:** ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Flowable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Observable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Maybe`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Single`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Completable` + +**ReactiveX documentation:** [http://reactivex.io/documentation/operators/skip.html](http://reactivex.io/documentation/operators/skip.html) + +Drops the first *n* items emitted by a reactive source, and emits the remaining items. + +### skip example + +```java +Observable source = Observable.just(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + +source.skip(4) + .subscribe(System.out::println); + +// prints: +// 5 +// 6 +// 7 +// 8 +// 9 +// 10 +``` + +## skipLast + +**Available in:** ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Flowable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Observable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Maybe`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Single`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Completable` + +**ReactiveX documentation:** [http://reactivex.io/documentation/operators/skiplast.html](http://reactivex.io/documentation/operators/skiplast.html) + +Drops the last *n* items emitted by a reactive source, and emits the remaining items. + +### skipLast example + +```java +Observable source = Observable.just(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + +source.skipLast(4) + .subscribe(System.out::println); + +// prints: +// 1 +// 2 +// 3 +// 4 +// 5 +// 6 +``` + +## take + +**Available in:** ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Flowable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Observable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Maybe`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Single`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Completable` + +**ReactiveX documentation:** [http://reactivex.io/documentation/operators/take.html](http://reactivex.io/documentation/operators/take.html) + +Emits only the first *n* items emitted by a reactive source. + +### take example + +```java +Observable source = Observable.just(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + +source.take(4) + .subscribe(System.out::println); + +// prints: +// 1 +// 2 +// 3 +// 4 +``` + +## takeLast + +**Available in:** ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Flowable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Observable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Maybe`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Single`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Completable` + +**ReactiveX documentation:** [http://reactivex.io/documentation/operators/takelast.html](http://reactivex.io/documentation/operators/takelast.html) + +Emits only the last *n* items emitted by a reactive source. + +### takeLast example + +```java +Observable source = Observable.just(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + +source.takeLast(4) + .subscribe(System.out::println); + +// prints: +// 7 +// 8 +// 9 +// 10 +``` + +## throttleFirst + +**Available in:** ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Flowable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Observable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Maybe`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Single`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Completable` + +**ReactiveX documentation:** [http://reactivex.io/documentation/operators/sample.html](http://reactivex.io/documentation/operators/sample.html) + +Emits only the first item emitted by a reactive source during sequential time windows of a specified duration. + +### throttleFirst example + +```java +// Diagram: +// -A----B-C-------D-----E-|--> +// a---------1s +// d-------|--> +// -A--------------D-------|--> + +Observable source = Observable.create(emitter -> { + emitter.onNext("A"); + + Thread.sleep(500); + emitter.onNext("B"); + + Thread.sleep(200); + emitter.onNext("C"); + + Thread.sleep(800); + emitter.onNext("D"); + + Thread.sleep(600); + emitter.onNext("E"); + emitter.onComplete(); +}); + +source.subscribeOn(Schedulers.io()) + .throttleFirst(1, TimeUnit.SECONDS) + .blockingSubscribe( + item -> System.out.println("onNext: " + item), + Throwable::printStackTrace, + () -> System.out.println("onComplete")); + +// prints: +// onNext: A +// onNext: D +// onComplete +``` + +## throttleLast + +**Available in:** ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Flowable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Observable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Maybe`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Single`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Completable` + +**ReactiveX documentation:** [http://reactivex.io/documentation/operators/sample.html](http://reactivex.io/documentation/operators/sample.html) + +Emits only the last item emitted by a reactive source during sequential time windows of a specified duration. + +### throttleLast example + +```java +// Diagram: +// -A----B-C-------D-----E-|--> +// -0s-----c--1s---d----2s-|--> +// -----------C---------D--|--> + +Observable source = Observable.create(emitter -> { + emitter.onNext("A"); + + Thread.sleep(500); + emitter.onNext("B"); + + Thread.sleep(200); + emitter.onNext("C"); + + Thread.sleep(800); + emitter.onNext("D"); + + Thread.sleep(600); + emitter.onNext("E"); + emitter.onComplete(); +}); + +source.subscribeOn(Schedulers.io()) + .throttleLast(1, TimeUnit.SECONDS) + .blockingSubscribe( + item -> System.out.println("onNext: " + item), + Throwable::printStackTrace, + () -> System.out.println("onComplete")); + +// prints: +// onNext: C +// onNext: D +// onComplete +``` + +## throttleLatest + +**Available in:** ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Flowable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Observable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Maybe`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Single`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Completable` + +**ReactiveX documentation:** [http://reactivex.io/documentation/operators/sample.html](http://reactivex.io/documentation/operators/sample.html) + +Emits the next item emitted by a reactive source, then periodically emits the latest item (if any) when the specified timeout elapses between them. + +### throttleLatest example + +```java +// Diagram: +// -A----B-C-------D-----E-|--> +// -a------c--1s +// -----d----1s +// -e-|--> +// -A---------C---------D--|--> + +Observable source = Observable.create(emitter -> { + emitter.onNext("A"); + + Thread.sleep(500); + emitter.onNext("B"); + + Thread.sleep(200); + emitter.onNext("C"); + + Thread.sleep(800); + emitter.onNext("D"); + + Thread.sleep(600); + emitter.onNext("E"); + emitter.onComplete(); +}); + +source.subscribeOn(Schedulers.io()) + .throttleLatest(1, TimeUnit.SECONDS) + .blockingSubscribe( + item -> System.out.println("onNext: " + item), + Throwable::printStackTrace, + () -> System.out.println("onComplete")); + +// prints: +// onNext: A +// onNext: C +// onNext: D +// onComplete +``` + +## throttleWithTimeout + +**Available in:** ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Flowable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Observable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Maybe`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Single`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Completable` + +**ReactiveX documentation:** [http://reactivex.io/documentation/operators/debounce.html](http://reactivex.io/documentation/operators/debounce.html) + +> Alias to [debounce](#debounce) + +Drops items emitted by a reactive source that are followed by newer items before the given timeout value expires. The timer resets on each emission. + +### throttleWithTimeout example + +```java +// Diagram: +// -A--------------B----C-D-------------------E-|----> +// a---------1s +// b---------1s +// c---------1s +// d---------1s +// e-|----> +// -----------A---------------------D-----------E-|--> + +Observable source = Observable.create(emitter -> { + emitter.onNext("A"); + + Thread.sleep(1_500); + emitter.onNext("B"); + + Thread.sleep(500); + emitter.onNext("C"); + + Thread.sleep(250); + emitter.onNext("D"); + + Thread.sleep(2_000); + emitter.onNext("E"); + emitter.onComplete(); +}); + +source.subscribeOn(Schedulers.io()) + .throttleWithTimeout(1, TimeUnit.SECONDS) + .blockingSubscribe( + item -> System.out.println("onNext: " + item), + Throwable::printStackTrace, + () -> System.out.println("onComplete")); + +// prints: +// onNext: A +// onNext: D +// onNext: E +// onComplete +``` + +## timeout + +**Available in:** ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Flowable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Observable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Maybe`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Single`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Completable` + +**ReactiveX documentation:** [http://reactivex.io/documentation/operators/timeout.html](http://reactivex.io/documentation/operators/timeout.html) + +Emits the items from the `Observable` or `Flowable` source, but terminates with a `java.util.concurrent.TimeoutException` if the next item is not emitted within the specified timeout duration starting from the previous item. For `Maybe`, `Single` and `Completable` the specified timeout duration specifies the maximum time to wait for a success or completion event to arrive. If the `Maybe`, `Single` or `Completable` does not complete within the given time a `java.util.concurrent.TimeoutException` will be emitted. + +### timeout example + +```java +// Diagram: +// -A-------B---C-----------D-|--> +// a---------1s +// b---------1s +// c---------1s +// -A-------B---C---------X------> + +Observable source = Observable.create(emitter -> { + emitter.onNext("A"); + + Thread.sleep(800); + emitter.onNext("B"); + + Thread.sleep(400); + emitter.onNext("C"); + + Thread.sleep(1200); + emitter.onNext("D"); + emitter.onComplete(); +}); + +source.timeout(1, TimeUnit.SECONDS) + .subscribe( + item -> System.out.println("onNext: " + item), + error -> System.out.println("onError: " + error), + () -> System.out.println("onComplete will not be printed!")); + +// prints: +// onNext: A +// onNext: B +// onNext: C +// onError: java.util.concurrent.TimeoutException: The source did not signal an event for 1 seconds and has been terminated. +``` diff --git a/docs/Getting-Started.md b/docs/Getting-Started.md new file mode 100644 index 0000000000..69b69a8ca6 --- /dev/null +++ b/docs/Getting-Started.md @@ -0,0 +1,129 @@ +## Getting Binaries + +You can find binaries and dependency information for Maven, Ivy, Gradle, SBT, and others at [http://search.maven.org](https://search.maven.org/search?q=g:io.reactivex.rxjava3%20AND%20rxjava). + +Example for Maven: + +```xml + + io.reactivex.rxjava3 + rxjava + 3.0.4 + +``` +and for Ivy: + +```xml + +``` + +and for SBT: + +```scala +libraryDependencies += "io.reactivex" %% "rxscala" % "0.26.5" + +libraryDependencies += "io.reactivex.rxjava3" % "rxjava" % "3.0.4" +``` + +and for Gradle: +```groovy +implementation 'io.reactivex.rxjava3:rxjava:3.0.4' +``` + +If you need to download the jars instead of using a build system, create a Maven `pom` file like this with the desired version: + +```xml + + + 4.0.0 + io.reactivex.rxjava3 + rxjava + 3.0.4 + RxJava + Reactive Extensions for Java + https://github.com/ReactiveX/RxJava + + + io.reactivex.rxjava3 + rxjava + 3.0.4 + + + +``` + +Then execute: + +``` +$ mvn -f download-rxjava-pom.xml dependency:copy-dependencies +``` + +That command downloads `rxjava-*.jar` and its dependencies into `./target/dependency/`. + +You need Java 6 or later. + +### Snapshots + +Snapshots after May 1st, 2021 are available via https://oss.sonatype.org/content/repositories/snapshots/io/reactivex/rxjava3/rxjava/ + +```groovy +repositories { + maven { url 'https://oss.sonatype.org/content/repositories/snapshots' } +} + +dependencies { + implementation 'io.reactivex.rxjava3:rxjava:3.0.0-SNAPSHOT' +} +``` + +JavaDoc snapshots are available at http://reactivex.io/RxJava/3.x/javadoc/snapshot + + +## Building + +To check out and build the RxJava source, issue the following commands: + +``` +$ git clone git@github.com:ReactiveX/RxJava.git +$ cd RxJava/ +$ ./gradlew build +``` + +To do a clean build, issue the following command: + +``` +$ ./gradlew clean build +``` + +A build should look similar to this: + +``` +$ ./gradlew build +:rxjava:compileJava +:rxjava:processResources UP-TO-DATE +:rxjava:classes +:rxjava:jar +:rxjava:sourcesJar +:rxjava:signArchives SKIPPED +:rxjava:assemble +:rxjava:licenseMain UP-TO-DATE +:rxjava:licenseTest UP-TO-DATE +:rxjava:compileTestJava +:rxjava:processTestResources UP-TO-DATE +:rxjava:testClasses +:rxjava:test +:rxjava:check +:rxjava:build + +BUILD SUCCESSFUL + +Total time: 30.758 secs +``` + +On a clean build you will see the unit tests run. They will look something like this: + +``` +> Building > :rxjava:test > 91 tests completed +``` diff --git a/docs/Home.md b/docs/Home.md new file mode 100644 index 0000000000..474c8edc77 --- /dev/null +++ b/docs/Home.md @@ -0,0 +1,24 @@ +RxJava is a Java VM implementation of [ReactiveX (Reactive Extensions)](https://reactivex.io): a library for composing asynchronous and event-based programs by using observable sequences. + +For more information about ReactiveX, see the [Introduction to ReactiveX](http://reactivex.io/intro.html) page. + +### RxJava is Lightweight + +RxJava tries to be very lightweight. It is implemented as a single JAR that is focused on just the Observable abstraction and related higher-order functions. + +### RxJava is a Polyglot Implementation + +RxJava supports Java 6 or higher and JVM-based languages such as [Groovy](https://github.com/ReactiveX/RxGroovy), [Clojure](https://github.com/ReactiveX/RxClojure), [JRuby](https://github.com/ReactiveX/RxJRuby), [Kotlin](https://github.com/ReactiveX/RxKotlin) and [Scala](https://github.com/ReactiveX/RxScala). + +RxJava is meant for a more polyglot environment than just Java/Scala, and it is being designed to respect the idioms of each JVM-based language. (This is something we’re still working on.) + +### RxJava Libraries + +The following external libraries can work with RxJava: + +* [Hystrix](https://github.com/Netflix/Hystrix/wiki/How-To-Use#wiki-Reactive-Execution) latency and fault tolerance bulkheading library. +* [Camel RX](http://camel.apache.org/rx.html) provides an easy way to reuse any of the [Apache Camel components, protocols, transports and data formats](http://camel.apache.org/components.html) with the RxJava API +* [rxjava-http-tail](https://github.com/myfreeweb/rxjava-http-tail) allows you to follow logs over HTTP, like `tail -f` +* [mod-rxvertx - Extension for VertX](https://github.com/vert-x/mod-rxvertx) that provides support for Reactive Extensions (RX) using the RxJava library +* [rxjava-jdbc](https://github.com/davidmoten/rxjava-jdbc) - use RxJava with jdbc connections to stream ResultSets and do functional composition of statements +* [rtree](https://github.com/davidmoten/rtree) - immutable in-memory R-tree and R*-tree with RxJava api including backpressure \ No newline at end of file diff --git a/docs/How-To-Use-RxJava.md b/docs/How-To-Use-RxJava.md new file mode 100644 index 0000000000..41a46bb75d --- /dev/null +++ b/docs/How-To-Use-RxJava.md @@ -0,0 +1,405 @@ +# Hello World! + +The following sample implementations of “Hello World” in Java, Groovy, Clojure, and Scala create an Observable from a list of Strings, and then subscribe to this Observable with a method that prints “Hello _String_!” for each string emitted by the Observable. + +You can find additional code examples in the `/src/examples` folders of each [language adaptor](https://github.com/ReactiveX/): + +* [RxGroovy examples](https://github.com/ReactiveX/RxGroovy/tree/1.x/src/examples/groovy/rx/lang/groovy/examples) +* [RxClojure examples](https://github.com/ReactiveX/RxClojure/tree/0.x/src/examples/clojure/rx/lang/clojure/examples) +* [RxScala examples](https://github.com/ReactiveX/RxScala/tree/0.x/examples/src/main/scala) + +### Java + +```java +public static void hello(String... args) { + Flowable.fromArray(args).subscribe(s -> System.out.println("Hello " + s + "!")); +} +``` + +If your platform doesn't support Java 8 lambdas (yet), you have to create an inner class of ```Consumer``` manually: +```java +public static void hello(String... args) { + Flowable.fromArray(args).subscribe(new Consumer() { + @Override + public void accept(String s) { + System.out.println("Hello " + s + "!"); + } + }); +} +``` + +```java +hello("Ben", "George"); +Hello Ben! +Hello George! +``` + +### Groovy + +```groovy +def hello(String[] names) { + Observable.from(names).subscribe { println "Hello ${it}!" } +} +``` + +```groovy +hello("Ben", "George") +Hello Ben! +Hello George! +``` + +### Clojure + +```clojure +(defn hello + [&rest] + (-> (Observable/from &rest) + (.subscribe #(println (str "Hello " % "!"))))) +``` + +``` +(hello ["Ben" "George"]) +Hello Ben! +Hello George! +``` +### Scala + +```scala +import rx.lang.scala.Observable + +def hello(names: String*) { + Observable.from(names) subscribe { n => + println(s"Hello $n!") + } +} +``` + +```scala +hello("Ben", "George") +Hello Ben! +Hello George! +``` + +# How to Design Using RxJava + +To use RxJava you create Observables (which emit data items), transform those Observables in various ways to get the precise data items that interest you (by using Observable operators), and then observe and react to these sequences of interesting items (by implementing Observers or Subscribers and then subscribing them to the resulting transformed Observables). + +## Creating Observables + +To create an Observable, you can either implement the Observable's behavior manually by passing a function to [`create( )`](http://reactivex.io/documentation/operators/create.html) that exhibits Observable behavior, or you can convert an existing data structure into an Observable by using [some of the Observable operators that are designed for this purpose](Creating-Observables). + +### Creating an Observable from an Existing Data Structure + +You use the Observable [`just( )`](http://reactivex.io/documentation/operators/just.html) and [`from( )`](http://reactivex.io/documentation/operators/from.html) methods to convert objects, lists, or arrays of objects into Observables that emit those objects: + +```groovy +Observable o = Observable.from("a", "b", "c"); + +def list = [5, 6, 7, 8] +Observable o2 = Observable.from(list); + +Observable o3 = Observable.just("one object"); +``` + +These converted Observables will synchronously invoke the [`onNext( )`](Observable#onnext-oncompleted-and-onerror) method of any subscriber that subscribes to them, for each item to be emitted by the Observable, and will then invoke the subscriber’s [`onCompleted( )`](Observable#onnext-oncompleted-and-onerror) method. + +### Creating an Observable via the `create( )` method + +You can implement asynchronous i/o, computational operations, or even “infinite” streams of data by designing your own Observable and implementing it with the [`create( )`](http://reactivex.io/documentation/operators/create.html) method. + +#### Synchronous Observable Example + +```groovy +/** + * This example shows a custom Observable that blocks + * when subscribed to (does not spawn an extra thread). + */ +def customObservableBlocking() { + return Observable.create { aSubscriber -> + 50.times { i -> + if (!aSubscriber.unsubscribed) { + aSubscriber.onNext("value_${i}") + } + } + // after sending all values we complete the sequence + if (!aSubscriber.unsubscribed) { + aSubscriber.onCompleted() + } + } +} + +// To see output: +customObservableBlocking().subscribe { println(it) } +``` + +#### Asynchronous Observable Example + +The following example uses Groovy to create an Observable that emits 75 strings. + +It is written verbosely, with static typing and implementation of the `Func1` anonymous inner class, to make the example more clear: + +```groovy +/** + * This example shows a custom Observable that does not block + * when subscribed to as it spawns a separate thread. + */ +def customObservableNonBlocking() { + return Observable.create({ subscriber -> + Thread.start { + for (i in 0..<75) { + if (subscriber.unsubscribed) { + return + } + subscriber.onNext("value_${i}") + } + // after sending all values we complete the sequence + if (!subscriber.unsubscribed) { + subscriber.onCompleted() + } + } + } as Observable.OnSubscribe) +} + +// To see output: +customObservableNonBlocking().subscribe { println(it) } +``` + +Here is the same code in Clojure that uses a Future (instead of raw thread) and is implemented more consisely: + +```clojure +(defn customObservableNonBlocking [] + "This example shows a custom Observable that does not block + when subscribed to as it spawns a separate thread. + + returns Observable" + (Observable/create + (fn [subscriber] + (let [f (future + (doseq [x (range 50)] (-> subscriber (.onNext (str "value_" x)))) + ; after sending all values we complete the sequence + (-> subscriber .onCompleted)) + )) + )) +``` + +```clojure +; To see output +(.subscribe (customObservableNonBlocking) #(println %)) +``` + +Here is an example that fetches articles from Wikipedia and invokes onNext with each one: + +```clojure +(defn fetchWikipediaArticleAsynchronously [wikipediaArticleNames] + "Fetch a list of Wikipedia articles asynchronously. + + return Observable of HTML" + (Observable/create + (fn [subscriber] + (let [f (future + (doseq [articleName wikipediaArticleNames] + (-> subscriber (.onNext (http/get (str "http://en.wikipedia.org/wiki/" articleName))))) + ; after sending response to onnext we complete the sequence + (-> subscriber .onCompleted)) + )))) +``` + +```clojure +(-> (fetchWikipediaArticleAsynchronously ["Tiger" "Elephant"]) + (.subscribe #(println "--- Article ---\n" (subs (:body %) 0 125) "..."))) +``` + +Back to Groovy, the same Wikipedia functionality but using closures instead of anonymous inner classes: + +```groovy +/* + * Fetch a list of Wikipedia articles asynchronously. + */ +def fetchWikipediaArticleAsynchronously(String... wikipediaArticleNames) { + return Observable.create { subscriber -> + Thread.start { + for (articleName in wikipediaArticleNames) { + if (subscriber.unsubscribed) { + return + } + subscriber.onNext(new URL("http://en.wikipedia.org/wiki/$%7BarticleName%7D").text) + } + if (!subscriber.unsubscribed) { + subscriber.onCompleted() + } + } + return subscriber + } +} + +fetchWikipediaArticleAsynchronously("Tiger", "Elephant") + .subscribe { println "--- Article ---\n${it.substring(0, 125)}" } +``` + +Results: + +```text +--- Article --- + + + +Tiger - Wikipedia, the free encyclopedia ... +--- Article --- + + + +Elephant - Wikipedia, the free encyclopedia</tit ... +``` + +Note that all of the above examples ignore error handling, for brevity. See below for examples that include error handling. + +More information can be found on the [[Observable]] and [[Creating Observables|Creating-Observables]] pages. + +## Transforming Observables with Operators + +RxJava allows you to chain _operators_ together to transform and compose Observables. + +The following example, in Groovy, uses a previously defined, asynchronous Observable that emits 75 items, skips over the first 10 of these ([`skip(10)`](http://reactivex.io/documentation/operators/skip.html)), then takes the next 5 ([`take(5)`](http://reactivex.io/documentation/operators/take.html)), and transforms them ([`map(...)`](http://reactivex.io/documentation/operators/map.html)) before subscribing and printing the items: + +```groovy +/** + * Asynchronously calls 'customObservableNonBlocking' and defines + * a chain of operators to apply to the callback sequence. + */ +def simpleComposition() { + customObservableNonBlocking().skip(10).take(5) + .map({ stringValue -> return stringValue + "_xform"}) + .subscribe({ println "onNext => " + it}) +} +``` + +This results in: + +```text +onNext => value_10_xform +onNext => value_11_xform +onNext => value_12_xform +onNext => value_13_xform +onNext => value_14_xform +``` + +Here is a marble diagram that illustrates this transformation: + +<img src="https://github.com/ReactiveX/RxJava/wiki/images/rx-operators/Composition.1.v3.png" width="640" height="536" /> + +This next example, in Clojure, consumes three asynchronous Observables, including a dependency from one to another, and emits a single response item by combining the items emitted by each of the three Observables with the [`zip`](http://reactivex.io/documentation/operators/zip.html) operator and then transforming the result with [`map`](http://reactivex.io/documentation/operators/map.html): + +```clojure +(defn getVideoForUser [userId videoId] + "Get video metadata for a given userId + - video metadata + - video bookmark position + - user data + return Observable<Map>" + (let [user-observable (-> (getUser userId) + (.map (fn [user] {:user-name (:name user) :language (:preferred-language user)}))) + bookmark-observable (-> (getVideoBookmark userId videoId) + (.map (fn [bookmark] {:viewed-position (:position bookmark)}))) + ; getVideoMetadata requires :language from user-observable so nest inside map function + video-metadata-observable (-> user-observable + (.mapMany + ; fetch metadata after a response from user-observable is received + (fn [user-map] + (getVideoMetadata videoId (:language user-map)))))] + ; now combine 3 observables using zip + (-> (Observable/zip bookmark-observable video-metadata-observable user-observable + (fn [bookmark-map metadata-map user-map] + {:bookmark-map bookmark-map + :metadata-map metadata-map + :user-map user-map})) + ; and transform into a single response object + (.map (fn [data] + {:video-id videoId + :video-metadata (:metadata-map data) + :user-id userId + :language (:language (:user-map data)) + :bookmark (:viewed-position (:bookmark-map data)) + }))))) +``` + +The response looks like this: + +```clojure +{:video-id 78965, + :video-metadata {:video-id 78965, :title House of Cards: Episode 1, + :director David Fincher, :duration 3365}, + :user-id 12345, :language es-us, :bookmark 0} +``` + +And here is a marble diagram that illustrates how that code produces that response: + +<img src="https://github.com/ReactiveX/RxJava/wiki/images/rx-operators/Composition.2.v3.png" width="640" height="742" /> + +The following example, in Groovy, comes from [Ben Christensen’s QCon presentation on the evolution of the Netflix API](https://speakerdeck.com/benjchristensen/evolution-of-the-netflix-api-qcon-sf-2013). It combines two Observables with the [`merge`](http://reactivex.io/documentation/operators/merge.html) operator, then uses the [`reduce`](http://reactivex.io/documentation/operators/reduce.html) operator to construct a single item out of the resulting sequence, then transforms that item with [`map`](http://reactivex.io/documentation/operators/map.html) before emitting it: + +```groovy +public Observable getVideoSummary(APIVideo video) { + def seed = [id:video.id, title:video.getTitle()]; + def bookmarkObservable = getBookmark(video); + def artworkObservable = getArtworkImageUrl(video); + return( Observable.merge(bookmarkObservable, artworkObservable) + .reduce(seed, { aggregate, current -> aggregate << current }) + .map({ [(video.id.toString() : it] })) +} +``` + +And here is a marble diagram that illustrates how that code uses the [`reduce`](http://reactivex.io/documentation/operators/reduce.html) operator to bring the results from multiple Observables together in one structure: + +<img src="https://github.com/ReactiveX/RxJava/wiki/images/rx-operators/Composition.3.v3.png" width="640" height="640" /> + +## Error Handling + +Here is a version of the Wikipedia example from above revised to include error handling: + +```groovy +/* + * Fetch a list of Wikipedia articles asynchronously, with error handling. + */ +def fetchWikipediaArticleAsynchronouslyWithErrorHandling(String... wikipediaArticleNames) { + return Observable.create({ subscriber -> + Thread.start { + try { + for (articleName in wikipediaArticleNames) { + if (true == subscriber.isUnsubscribed()) { + return; + } + subscriber.onNext(new URL("http://en.wikipedia.org/wiki/"+articleName).getText()); + } + if (false == subscriber.isUnsubscribed()) { + subscriber.onCompleted(); + } + } catch(Throwable t) { + if (false == subscriber.isUnsubscribed()) { + subscriber.onError(t); + } + } + return (subscriber); + } + }); +} +``` + +Notice how it now invokes [`onError(Throwable t)`](Observable#onnext-oncompleted-and-onerror) if an error occurs and note that the following code passes [`subscribe()`](http://reactivex.io/documentation/operators/subscribe.html) a second method that handles `onError`: + +```groovy +fetchWikipediaArticleAsynchronouslyWithErrorHandling("Tiger", "NonExistentTitle", "Elephant") + .subscribe( + { println "--- Article ---\n" + it.substring(0, 125) }, + { println "--- Error ---\n" + it.getMessage() }) +``` + +See the [Error-Handling-Operators](Error-Handling-Operators) page for more information on specialized error handling techniques in RxJava, including methods like [`onErrorResumeNext()`](http://reactivex.io/documentation/operators/catch.html) and [`onErrorReturn()`](http://reactivex.io/documentation/operators/catch.html) that allow Observables to continue with fallbacks in the event that they encounter errors. + +Here is an example of how you can use such a method to pass along custom information about any exceptions you encounter. Imagine you have an Observable or cascade of Observables — `myObservable` — and you want to intercept any exceptions that would normally pass through to an Subscriber’s `onError` method, replacing these with a customized Throwable of your own design. You could do this by modifying `myObservable` with the [`onErrorResumeNext()`](http://reactivex.io/documentation/operators/catch.html) method, and passing into that method an Observable that calls `onError` with your customized Throwable (a utility method called [`error()`](http://reactivex.io/documentation/operators/empty-never-throw.html) will generate such an Observable for you): + +```groovy +myModifiedObservable = myObservable.onErrorResumeNext({ t -> + Throwable myThrowable = myCustomizedThrowableCreator(t); + return (Observable.error(myThrowable)); +}); +``` diff --git a/docs/How-to-Contribute.md b/docs/How-to-Contribute.md new file mode 100644 index 0000000000..d7c4a3ceb7 --- /dev/null +++ b/docs/How-to-Contribute.md @@ -0,0 +1,41 @@ +RxJava is still a work in progress and has a long list of work documented in the [Issues](https://github.com/ReactiveX/RxJava/issues). + + +If you wish to contribute we would ask that you: +- read [Rx Design Guidelines](http://blogs.msdn.com/b/rxteam/archive/2010/10/28/rx-design-guidelines.aspx) +- review existing code and comply with existing patterns and idioms +- include unit tests +- stick to Rx contracts as defined by the Rx.Net implementation when porting operators (each issue attempts to reference the correct documentation from MSDN) + +Information about licensing can be found at: [CONTRIBUTING](https://github.com/ReactiveX/RxJava/blob/2.x/CONTRIBUTING.md). + +## How to import the project into Eclipse +Two options below: + +### Import as Eclipse project + + ./gradlew eclipse + +In Eclipse +* choose File - Import - General - Existing Projects into Workspace +* Browse to RxJava folder +* click Finish. +* Right click on the project in Package Explorer, select Properties - Java Compiler - Errors/Warnings - click Enable project specific settings. +* Still in Errors/Warnings, go to Deprecated and restricted API and set Forbidden reference (access-rules) to Warning. + +### Import as Gradle project + +You need the Gradle plugin for Eclipse installed. + +In Eclipse +* choose File - Import - Gradle - Gradle Project. +* Browse to RxJava folder +* click Build Model +* select the project +* click Finish + + + + + + diff --git a/docs/Implementing-Your-Own-Operators.md b/docs/Implementing-Your-Own-Operators.md new file mode 100644 index 0000000000..9e4a165f8b --- /dev/null +++ b/docs/Implementing-Your-Own-Operators.md @@ -0,0 +1,114 @@ +You can implement your own Observable operators. This page shows you how. + +If your operator is designed to *originate* an Observable, rather than to transform or react to a source Observable, use the [`create( )`](http://reactivex.io/documentation/operators/create.html) method rather than trying to implement `Observable` manually. Otherwise, you can create a custom operator by following the instructions on this page. + +If your operator is designed to act on the individual items emitted by a source Observable, follow the instructions under [_Sequence Operators_](Implementing-Your-Own-Operators#sequence-operators) below. If your operator is designed to transform the source Observable as a whole (for instance, by applying a particular set of existing RxJava operators to it) follow the instructions under [_Transformational Operators_](Implementing-Your-Own-Operators#transformational-operators) below. + +(**Note:** in Xtend, a Groovy-like language, you can implement your operators as _extension methods_ and can thereby chain them directly without using the methods described on this page. See [RxJava and Xtend](http://mnmlst-dvlpr.blogspot.de/2014/07/rxjava-and-xtend.html) for details.) + +# Sequence Operators + +The following example shows how you can use the `lift( )` operator to chain your custom operator (in this example: `myOperator`) alongside standard RxJava operators like `ofType` and `map`: +```groovy +fooObservable = barObservable.ofType(Integer).map({it*2}).lift(new myOperator<T>()).map({"transformed by myOperator: " + it}); +``` +The following section shows how you form the scaffolding of your operator so that it will work correctly with `lift( )`. + +## Implementing Your Operator + +Define your operator as a public class that implements the [`Operator`](http://reactivex.io/RxJava/javadoc/rx/Observable.Operator.html) interface, like so: +```java +public class myOperator<T> implements Operator<T> { + public myOperator( /* any necessary params here */ ) { + /* any necessary initialization here */ + } + + @Override + public Subscriber<? super T> call(final Subscriber<? super T> s) { + return new Subscriber<t>(s) { + @Override + public void onCompleted() { + /* add your own onCompleted behavior here, or just pass the completed notification through: */ + if(!s.isUnsubscribed()) { + s.onCompleted(); + } + } + + @Override + public void onError(Throwable t) { + /* add your own onError behavior here, or just pass the error notification through: */ + if(!s.isUnsubscribed()) { + s.onError(t); + } + } + + @Override + public void onNext(T item) { + /* this example performs some sort of operation on each incoming item and emits the results */ + if(!s.isUnsubscribed()) { + transformedItem = myOperatorTransformOperation(item); + s.onNext(transformedItem); + } + } + }; + } +} +``` + +# Transformational Operators + +The following example shows how you can use the `compose( )` operator to chain your custom operator (in this example, an operator called `myTransformer` that transforms an Observable that emits Integers into one that emits Strings) alongside standard RxJava operators like `ofType` and `map`: +```groovy +fooObservable = barObservable.ofType(Integer).map({it*2}).compose(new myTransformer<Integer,String>()).map({"transformed by myOperator: " + it}); +``` +The following section shows how you form the scaffolding of your operator so that it will work correctly with `compose( )`. + +## Implementing Your Transformer + +Define your transforming function as a public class that implements the [`Transformer`](http://reactivex.io/RxJava/javadoc/rx/Observable.Transformer.html) interface, like so: + +````java +public class myTransformer<Integer,String> implements Transformer<Integer,String> { + public myTransformer( /* any necessary params here */ ) { + /* any necessary initialization here */ + } + + @Override + public Observable<String> call(Observable<Integer> source) { + /* + * this simple example Transformer applies map() to the source Observable + * in order to transform the "source" observable from one that emits + * integers to one that emits string representations of those integers. + */ + return source.map( new Func1<Integer,String>() { + @Override + public String call(Integer t1) { + return String.valueOf(t1); + } + } ); + } +} +```` + +## See also + +* [“Don’t break the chain: use RxJava’s compose() operator”](http://blog.danlew.net/2015/03/02/dont-break-the-chain/) by Dan Lew + +# Other Considerations + +* Your sequence operator may want to check [its Subscriber’s `isUnsubscribed( )` status](Observable#unsubscribing) before it emits any item to (or sends any notification to) the Subscriber. There’s no need to waste time generating items that no Subscriber is interested in seeing. +* Take care that your sequence operator obeys the core tenets of the Observable contract: + * It may call a Subscriber’s [`onNext( )`](Observable#onnext-oncompleted-and-onerror) method any number of times, but these calls must be non-overlapping. + * It may call either a Subscriber’s [`onCompleted( )`](Observable#onnext-oncompleted-and-onerror) or [`onError( )`](Observable#onnext-oncompleted-and-onerror) method, but not both, exactly once, and it may not subsequently call a Subscriber’s [`onNext( )`](Observable#onnext-oncompleted-and-onerror) method. + * If you are unable to guarantee that your operator conforms to the above two tenets, you can add the [`serialize( )`](Observable-Utility-Operators#serialize) operator to it, which will force the correct behavior. +* Keep an eye on [Issue #1962](https://github.com/ReactiveX/RxJava/issues/1962) — there are plans to create a test scaffold that you can use to write tests which verify that your new operator conforms to the Observable contract. +* Do not block within your operator. +* When possible, you should compose new operators by combining existing operators, rather than implementing them with new code. RxJava itself does this with some of its standard operators, for example: + * [`first( )`](http://reactivex.io/documentation/operators/first.html) is defined as <tt>[take(1)](http://reactivex.io/documentation/operators/take.html).[single( )](http://reactivex.io/documentation/operators/first.html)</tt> + * [`ignoreElements( )`](http://reactivex.io/documentation/operators/ignoreelements.html) is defined as <tt>[filter(alwaysFalse( ))](http://reactivex.io/documentation/operators/filter.html)</tt> + * [`reduce(a)`](http://reactivex.io/documentation/operators/reduce.html) is defined as <tt>[scan(a)](http://reactivex.io/documentation/operators/scan.html).[last( )](http://reactivex.io/documentation/operators/last.html)</tt> +* If your operator uses functions or lambdas that are passed in as parameters (predicates, for instance), note that these may be sources of exceptions, and be prepared to catch these and notify subscribers via `onError( )` calls. + * Some exceptions are considered “fatal” and for them there’s no point in trying to call `onError( )` because that will either be futile or will just compound the problem. You can use the `Exceptions.throwIfFatal(throwable)` method to filter out such fatal exceptions and rethrow them rather than try to notify about them. +* In general, notify subscribers of error conditions immediately, rather than making an effort to emit more items first. +* Be aware that “<code>null</code>” is a valid item that may be emitted by an Observable. A frequent source of bugs is to test some variable meant to hold an emitted item against <code>null</code> as a substitute for testing whether or not an item was emitted. An emission of “<code>null</code>” is still an emission and is not the same as not emitting anything. +* It can be tricky to make your operator behave well in *backpressure* scenarios. See [Advanced RxJava](http://akarnokd.blogspot.hu/), a blog from Dávid Karnok, for a good discussion of the factors at play and how to deal with them. \ No newline at end of file diff --git a/docs/Implementing-custom-operators-(draft).md b/docs/Implementing-custom-operators-(draft).md new file mode 100644 index 0000000000..a95b6968f0 --- /dev/null +++ b/docs/Implementing-custom-operators-(draft).md @@ -0,0 +1,508 @@ +# Introduction + +RxJava features over 100 operators to support the most common reactive dataflow patterns. Generally, there exist a combination of operators, typically `flatMap`, `defer` and `publish`, that allow composing less common patterns with standard guarantees. When you have an uncommon pattern and you can't seem to find the right operators, try asking about it on our issue list (or Stackoverflow) first. + +If none of this applies to your use case, you may want to implement a custom operator. Be warned that **writing operators is hard**: when one writes an operator, the `Observable` **protocol**, **unsubscription**, **backpressure** and **concurrency** have to be taken into account and adhered to the letter. + +*Note that this page uses Java 8 syntax for brevity.* + +# Considerations + +## Observable protocol + +The `Observable` protocol states that you have to call the `Observer` methods, `onNext`, `onError` and `onCompleted` in a sequential manner. In other words, these can't be called concurrently and have to be **serialized**. The `SerializedObserver` and `SerializedSubscriber` wrappers help you with these. Note that there are cases where this serialization has to happen. + +In addition, there is an expected pattern of method calls on `Observer`: + +``` +onNext* (onError | onCompleted)? +``` + +A custom operator has to honor this pattern on its push side as well. For example, if your operator turns an `onNext` into an `onError`, the upstream has to be stopped and no further methods can be called on the dowstream. + +## Unsubscription + +The basic `Observer` method has no direct means to signal to the upstream source to stop emitting events. One either has to get the `Subscription` that the `Observable.subscribe(Observer<T>)` returns **and** be asynchronous itself. + +This shortcoming was resolved by introducing the `Subscriber` class that implements the `Subscription` interface. The interface allows detecting if a `Subscriber` is no longer interested in the events. + +```java +interface Subscription { + boolean isUnsubscribed(); + + void unsubscribe(); +} +``` + +In an operator, this allows active checking of the `Subscriber` state before emitting an event. + +In some cases, one needs to react to the child unsubscribing immediately and not just before an emission. To support this case, the `Subscriber` class has an `add(Subscription)` method that let's the operator register `Subscription`s of its own which get unsubscribed when the downstream calls `Subscriber.unsubscribe()`. + +```java +InputStream in = ... + +child.add(Subscriptions.create(() -> { + try { + in.close(); + } catch (IOException ex) { + RxJavaHooks.onError(ex); + } +})); +``` + +## Backpressure + +The name of this feature is often misinterpreted. It is about telling the upstream how many `onNext` events the downstream is ready to receive. For example, if the downstream requests 5, the upstream can only call `onNext` 5 times. If the upstream can't produce 5 elements but 3, it should deliver that 3 element followed by an `onError` or `onCompleted` (depending on the operator's purpose). The requests are cumulative in the sense that if the downstream requests 5 and then 2, there is going to be 7 requests outstanding. + +Backpressure handling adds a great deal of complexity to most operators: one has to track how many elements the downstream requested, how many have been delivered (by usually subtracting from the request amount) and sometimes how many elements are still available (but can't be delivered without requests). In addition, the downstream can request from any thread and is not required to happen on the common thread where otherwise the `onXXX` methods are called. + +The backpressure 'channel' is established between the upstream and downstream via the `Producer` interface: + +```java +interface Producer { + void request(long n); +} +``` + +When an upstream supports backpressure, it will call the `Subscriber.setProducer(Producer)` method on its downstream `Subscriber` with the implementation of this interface. The downstream then can respond with `Long.MAX_VALUE` to start an unbounded streaming (effectively no backpressure between the immediate upstream and downstream) or any other positive value. A request amount of zero should be ignored. + +Protocol-vise, there is no strict time when a producer can be set and it may never appear. Operators have to be ready to deal with this situation and assume the upstream runs in unbounded mode (as if `Long.MAX_VALUE` was requested). + +Often, operators may implement `Producer` and `Subscription` in a single class to handle both requests and unsubscriptions from the downstream: + +```java +final class MyEmitter implements Producer, Subscription { + final Subscriber<Integer> subscriber; + + public MyEmitter(Subscriber<Integer> subscriber) { + this.subscriber = subscriber; + } + + @Override + public void request(long n) { + if (n > 0) { + subscriber.onCompleted(); + } + } + + @Override + public void unsubscribe() { + System.out.println("Unsubscribed"); + } + + @Override + public boolean isUnsubscribed() { + return true; + } +} + +MyEmitter emitter = new MyEmitter(child); + +child.add(emitter); +child.setProducer(emitter); +``` + +Unfortunately, you can't implement `Producer` on a `Subscriber` because of an API oversight: `Subscriber` has a protected final `request(long n)` method to perform **deferred requesting** (store and accumulate the local request amounts until `setProducer` is called). + +## Concurrency + +When writing operators, we mostly have to deal with concurrency via the standard Java concurrency primitives: `AtomicXXX` classes, volatile variables, `Queue`s, mutual exclusion, Executors, etc. + +### RxJava tools + +RxJava has a few support classes and utilities that let's one deal with concurrency inside operators. + +The first one, `BackpressureUtils` deals with managing the cumulative requested and produced element counts for an operator. Its `getAndAddRequested()` method takes an `AtomicLong`, accumulates request amounts atomically and makes sure they don't overflow `Long.MAX_VALUE`. Its pair `produced()` subtracts the amount operators have produced, thus when both are in play, the given `AtomicLong` holds the current outstanding request amount for the downstream. + +Operators sometimes have to switch between multiple sources. If a previous source didn't fulfill all its requested amount, the new source has to start with that unfulfilled amount. Otherwise as the downstream didn't receive the requested amount (and no terminal event either), it can't know when to request more. If this switch happens at an `Observable` boundary (think `concat`), the `ProducerArbiter` helps managing the change. + +If there is only one item to emit eventually, the `SingleProducer` and `SingleDelayedProducer` help work out the backpressure handling: + +```java +child.setProducer(new SingleProducer<>(child, 1)); + +// or + +SingleDelayedProducer<Integer> p = new SingleDelayedProducer<>(child); + +child.add(p); +child.setProducer(p); + +p.setValue(2); +``` + +### The queue-drain approach + +Usually, one has to serialize calls to the `onXXX` methods so only one thread at a time is in any of them. The first thought, namely using `synchronized` blocks, is forbidden. It may cause deadlocks and unnecessary thread blocking. + +Most operators, however, can use a non-blocking approach called queue-drain. It works by posting the element to be emitted (or work to be performed) onto a **queue** then atomically increments a counter. If the value before the increment was zero, it means the current thread won the right to emit the contents of the queue. Once the queue is **drained**, the counter is decremented until zero and the thread continues with other activities. + +In code: + +```java +final AtomicInteger counter = new AtomicInteger(); +final Queue<T> queue = new ConcurrentLinkedQueue<>(); + +public void onNext(T t) { + queue.offer(t); + drain(); +} + +void drain() { + if (counter.getAndIncrement() == 0) { + do { + t = queue.poll(); + child.onNext(t); + } while (counter.decrementAndGet() != 0); + } +} +``` + +Often, the when the downstream requests some amount, that should also trigger a similar drain() call: + +```java + +final AtomicLong requested = new AtomicLong(); + +@Override +public void request(long n) { + if (n > 0) { + BackpressureUtils.getAndAddRequested(requested, n); + drain(); + } +} +``` + +Many operators do more than just draining the queue and emitting its content: they have to coordinate with the downstream to emit as many items from the queue as the downstream requested. + +For example, if one writes an operator that is unbounded-in but honors the requests of the downstream, the following `drain` pattern will do the job: + +```java +// downstream's consumer +final Subscriber<? super T> child; +// temporary storage for values +final Queue<T> queue; +// mutual exclusion +final AtomicInteger counter = new AtomicInteger(); +// tracks the downstream request amount +final AtomicLong requested = new AtomicLong(); + +// no more values expected from upstream +volatile boolean done; +// the upstream error if any +Throwable error; + +void drain() { + if (counter.getAndIncrement() != 0) { + return; + } + + int missed = 1; + Subscriber<? super T> child = this.child; + Queue<T> queue = this.queue; + + for (;;) { + long requests = requested.get(); + long emission = 0L; + + while (emission != requests) { // don't emit more than requested + if (child.isUnsubscribed()) { + return; + } + + boolean stop = done; // order matters here! + T t = queue.poll(); + boolean empty = t == null; + + // if no more values, emit an error or completion event + if (stop && empty) { + Throwable ex = error; + if (ex != null) { + child.onError(ex); + } else { + child.onCompleted(); + } + return; + } + // the upstream hasn't stopped yet but we don't have a value available + if (empty) { + break; + } + + child.onNext(t); + emission++; + } + + // if we are at a request boundary, a terminal event can be still emitted without requests + if (emission == requests) { + if (child.isUnsubscribed()) { + return; + } + + boolean stop = done; // order matters here! + boolean empty = queue.isEmpty(); + + // if no more values, emit an error or completion event + if (stop && empty) { + Throwable ex = error; + if (ex != null) { + child.onError(ex); + } else { + child.onCompleted(); + } + return; + } + } + + // decrement the current request amount by the emission count + if (emission != 0L && requests != Long.MAX_VALUE) { + BackpressureUtils.produced(requested, emission); + } + + // indicate that we have performed the outstanding amount of work + missed = counter.addAndGet(-missed); + if (missed == 0) { + return; + } + // if a concurrent getAndIncrement() happened, we loop back and continue + } +} +``` + +# Creating source operators + +One creates a source operator by implementing the `OnSubscribe` interface and then calls `Observable.create` with it: + +```java +OnSubscribe<T> onSubscribe = (Subscriber<? super T> child) -> { + // logic here +}; + +Observable<T> observable = Observable.create(onSubscribe); +``` + +*Note: a common mistake when writing an operator is that one simply calls `onNext` disregarding backpressure; one should use `fromCallable` instead for synchronously (blockingly) generating a single value.* + +The `logic here` could be arbitrary complex logic. Usually, one creates a class implementing `Subscription` and `Producer`, sets it on the `child` and works out the emission pattern: + +```java +OnSubscribe<T> onSubscribe = (Subscriber<? super T> child) -> { + MySubscription mys = new MySubscription(child, otherParams); + child.add(mys); + child.setProducer(mys); + + mys.runBusinessLogic(); +}; +``` + +## Converting a callback-API to reactive + +One of the reasons custom sources are created is when one converts a classical, callback-based 'reactive' API to RxJava. In this case, one has to setup the callback on the non-RxJava source and wire up unsubscription if possible: + +```java +OnSubscribe<Data> onSubscribe = (Subscriber<? super Data> child) -> { + Callback cb = event -> { + if (event.isSuccess()) { + child.setProducer(new SingleProducer<Data>(child, event.getData())); + } else { + child.onError(event.getError()); + } + }; + + Closeable c = api.query("someinput", cb); + + child.add(Subscriptions.create(() -> Closeables.closeQuietly(c))); +}; +``` + +In this example, the `api` takes a callback and returns a `Closeable`. Our handler signals the data by setting a `SingleProducer` of it to deal with downstream backpressure. If the downstream wants to cancel a running API call, the wrap to `Subscription` will close the query. + +However, in case the callback is called more than once, one has to deal with backpressure a different way. At this level, perhaps the most easiest way is to apply `onBackpressureBuffer` or `onBackpressureDrop` on the created `Observable`: + +```java +OnSubscribe<Data> onSubscribe = (Subscriber<? super Data> child) -> { + Callback cb = event -> { + if (event.isSuccess()) { + child.onNext(event.getData()); + } else { + child.onError(event.getError()); + } + }; + + Closeable c = api.query("someinput", cb); + + child.add(Subscriptions.create(() -> Closeables.closeQuietly(c))); +}; + +Observable<T> observable = Observable.create(onSubscribe).onBackpressureBuffer(); +``` + +# Creating intermediate operators + +Writing an intermediate operator is more difficult because one may need to coordinate request amount between the upstream and downstream. + +Intermediate operators are nothing but `Subscriber`s themselves, wrapping the downstream `Subscriber` themselves, modulating the calls to `onXXX`methods and they get subscribed to the upstream's `Observable`: + +```java +Func1<T, R> mapper = ... + +Observable<T> source = ... + +OnSubscribe<R> onSubscribe = (Subscriber<? super R> child) -> { + + source.subscribe(new MapSubscriber<T, R>(child) { + @Override + public void onNext(T t) { + child.onNext(function.call(t)); + } + + // ... etc + }); + +} +``` + +Depending on whether the safety-net of the `Observable.subscribe` method is too much of an overhead, one can call `Observable.unsafeSubscribe` but then the operator has to manage and unsubscribe its own resources manually. + +This approach has a common pattern that can be factored out - at the expense of more allocation and indirection - and became the `lift` operator. + +The `lift` operator takes an `Observable.Operator<R, T>` interface implementor where `R` is the output type towards the downstream and `T` is the input type from the upstream. In our example, we can rewrite the operator as follows: + +```java + +Operator<R, T> op = child -> + return new MapSubscriber<T, R>(child) { + @Override + public void onNext(T t) { + child.onNext(function.call(t)); + } + + // ... etc + }; +} + +source.lift(op)... +``` + +The constructor of `Subscriber(Subscriber<?>)` has some caveats: it shares the underlying resource management between `child` and `MapSubscriber`. This has the unfortunate effect that when the business logic calls `MapSubscriber.unsubscribe`, it may inadvertently unsubscribe the `child`'s resources prematurely. In addition, it sets up the `Subscriber` in a way that calls to `setProducer` are forwarded to the `child` as well. + +Sometimes it is acceptable, but generally one should avoid this coupling by implementing these custom `Subscriber`s among the following pattern: + +```java +public final class MapSubscriber<T, R> extends Subscriber<T> { + final Subscriber<? super R> child; + + final Function<T, R> mapper; + + public MapSubscriber(Subscriber<? super R> child, Func1<T, R> mapper) { + // no call to super(child) ! + this.child = child; + this.mapper = mapper; + + // prevent premature requesting + this.request(0); + } + + // setup the unsubscription and request links to downstream + void init() { + child.add(this); + child.setProducer(n -> requestMore(n)); + } + + @Override + public void onNext(T t) { + try { + child.onNext(mapper.call(t)); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + // if something crashed non-fatally, unsubscribe from upstream and signal the error + unsubscribe(); + onError(ex); + } + } + + @Override + public void onError(Throwable e) { + child.onError(e); + } + + @Override + public void onCompleted() { + child.onCompleted(); + } + + void requestMore(long n) { + // deal with the downstream requests + this.request(n); + } +} + +Operator<R, T> op = child -> { + MapSubscriber<T, R> parent = new MapSubscriber<T, R>(child, mapper); + parent.init(); + return parent; +} +``` + +Some operators may not emit the received value to the `child` subscriber (such as filter). In this case, one has to call `request(1)` to ask for a replenishment because the downstream doesn't know about the dropped value and won't request itself: + +```java +// ... + + @Override + public void onNext(T t) { + try { + if (predicate.call(t)) { + child.onNext(t); + } else { + request(1); + } + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + unsubscribe(); + onError(ex); + } + } + +// ... +``` + +When an operator maps an `onNext` emission to a terminal event then before calling the terminal event it should unsubscribe the subscriber to upstream (usually called the parent). In addition, because upstream may (legally) do something like this: + +```java +child.onNext(blah); +// no check for unsubscribed here +child.onCompleted(); +``` + +we should ensure that the operator complies with the `Observable` contract and only emits one terminal event so we use a defensive done flag: + +```java +boolean done; // = false; + +@Override +public void onError(Throwable e) { + if (done) { + return; + } + done = true; + ... +} + +@Override +public void onCompleted(Throwable e) { + if (done) { + return; + } + done = true; + ... +} +``` + +An example of this pattern is seen in `OnSubscribeMap`. + +# Further reading + +Writing operators that consume multiple source `Observable`s or produce to multiple `Subscriber`s are the most difficult one to implement. + +For inspiration, see the [blog posts](http://akarnokd.blogspot.hu/) of @akarnokd about the RxJava internals. The reader is advised to read from the very first post on and keep reading in sequence. \ No newline at end of file diff --git a/docs/Mathematical-and-Aggregate-Operators.md b/docs/Mathematical-and-Aggregate-Operators.md new file mode 100644 index 0000000000..f6bf79019e --- /dev/null +++ b/docs/Mathematical-and-Aggregate-Operators.md @@ -0,0 +1,366 @@ +This page shows operators that perform mathematical or other operations over an entire sequence of items emitted by an `Observable` or `Flowable`. Because these operations must wait for the source `Observable`/`Flowable` to complete emitting items before they can construct their own emissions (and must usually buffer these items), these operators are dangerous to use on `Observable`s and `Flowable`s that may have very long or infinite sequences. + +# Outline + +- [Mathematical Operators](#mathematical-operators) + - [`averageDouble`](#averagedouble) + - [`averageFloat`](#averagefloat) + - [`max`](#max) + - [`min`](#min) + - [`sumDouble`](#sumdouble) + - [`sumFloat`](#sumfloat) + - [`sumInt`](#sumint) + - [`sumLong`](#sumlong) +- [Standard Aggregate Operators](#standard-aggregate-operators) + - [`count`](#count) + - [`reduce`](#reduce) + - [`reduceWith`](#reducewith) + - [`collect`](#collect) + - [`collectInto`](#collectinto) + - [`toList`](#tolist) + - [`toSortedList`](#tosortedlist) + - [`toMap`](#tomap) + - [`toMultimap`](#tomultimap) + +## Mathematical Operators + +> The operators in this section are part of the [`RxJava2Extensions`](https://github.com/akarnokd/RxJava2Extensions) project. You have to add the `rxjava2-extensions` module as a dependency to your project. It can be found at [http://search.maven.org](http://search.maven.org/#search%7Cga%7C1%7Cg%3A%22com.github.akarnokd%22). + +> Note that unlike the standard RxJava aggregator operators, these mathematical operators return `Observable` and `Flowable` instead of the `Single` or `Maybe`. + +*The examples below assume that the `MathObservable` and `MathFlowable` classes are imported from the `rxjava2-extensions` module:* + +```java +import hu.akarnokd.rxjava2.math.MathObservable; +import hu.akarnokd.rxjava2.math.MathFlowable; +``` + +### averageDouble + +**Available in:** ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Flowable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Observable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Maybe`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Single`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Completable` + +**ReactiveX documentation:** [http://reactivex.io/documentation/operators/average.html](http://reactivex.io/documentation/operators/average.html) + +Calculates the average of `Number`s emitted by an `Observable` and emits this average as a `Double`. + +#### averageDouble example + +```java +Observable<Integer> numbers = Observable.just(1, 2, 3); +MathObservable.averageDouble(numbers).subscribe((Double avg) -> System.out.println(avg)); + +// prints 2.0 +``` + +### averageFloat + +**Available in:** ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Flowable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Observable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Maybe`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Single`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Completable` + +**ReactiveX documentation:** [http://reactivex.io/documentation/operators/average.html](http://reactivex.io/documentation/operators/average.html) + +Calculates the average of `Number`s emitted by an `Observable` and emits this average as a `Float`. + +#### averageFloat example + +```java +Observable<Integer> numbers = Observable.just(1, 2, 3); +MathObservable.averageFloat(numbers).subscribe((Float avg) -> System.out.println(avg)); + +// prints 2.0 +``` + +### max + +**Available in:** ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Flowable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Observable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Maybe`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Single`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Completable` + +**ReactiveX documentation:** [http://reactivex.io/documentation/operators/max.html](http://reactivex.io/documentation/operators/max.html) + +Emits the maximum value emitted by a source `Observable`. A `Comparator` can be specified that will be used to compare the elements emitted by the `Observable`. + +#### max example + +```java +Observable<Integer> numbers = Observable.just(4, 9, 5); +MathObservable.max(numbers).subscribe(System.out::println); + +// prints 9 +``` + +The following example specifies a `Comparator` to find the longest `String` in the source `Observable`: + +```java +final Observable<String> names = Observable.just("Kirk", "Spock", "Chekov", "Sulu"); +MathObservable.max(names, Comparator.comparingInt(String::length)) + .subscribe(System.out::println); + +// prints Chekov +``` + +### min + +**Available in:** ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Flowable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Observable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Maybe`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Single`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Completable` + +**ReactiveX documentation:** [http://reactivex.io/documentation/operators/min.html](http://reactivex.io/documentation/operators/min.html) + +Emits the minimum value emitted by a source `Observable`. A `Comparator` can be specified that will be used to compare the elements emitted by the `Observable`. + +#### min example + +```java +Observable<Integer> numbers = Observable.just(4, 9, 5); +MathObservable.min(numbers).subscribe(System.out::println); + +// prints 4 +``` + +### sumDouble + +**Available in:** ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Flowable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Observable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Maybe`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Single`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Completable` + +**ReactiveX documentation:** [http://reactivex.io/documentation/operators/sum.html](http://reactivex.io/documentation/operators/sum.html) + +Adds the `Double`s emitted by an `Observable` and emits this sum. + +#### sumDouble example + +```java +Observable<Double> numbers = Observable.just(1.0, 2.0, 3.0); +MathObservable.sumDouble(numbers).subscribe((Double sum) -> System.out.println(sum)); + +// prints 6.0 +``` + +### sumFloat + +**Available in:** ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Flowable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Observable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Maybe`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Single`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Completable` + +**ReactiveX documentation:** [http://reactivex.io/documentation/operators/sum.html](http://reactivex.io/documentation/operators/sum.html) + +Adds the `Float`s emitted by an `Observable` and emits this sum. + +#### sumFloat example + +```java +Observable<Float> numbers = Observable.just(1.0F, 2.0F, 3.0F); +MathObservable.sumFloat(numbers).subscribe((Float sum) -> System.out.println(sum)); + +// prints 6.0 +``` + +### sumInt + +**Available in:** ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Flowable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Observable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Maybe`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Single`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Completable` + +**ReactiveX documentation:** [http://reactivex.io/documentation/operators/sum.html](http://reactivex.io/documentation/operators/sum.html) + +Adds the `Integer`s emitted by an `Observable` and emits this sum. + +#### sumInt example + +```java +Observable<Integer> numbers = Observable.range(1, 100); +MathObservable.sumInt(numbers).subscribe((Integer sum) -> System.out.println(sum)); + +// prints 5050 +``` + +### sumLong + +**Available in:** ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Flowable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Observable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Maybe`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Single`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Completable` + +**ReactiveX documentation:** [http://reactivex.io/documentation/operators/sum.html](http://reactivex.io/documentation/operators/sum.html) + +Adds the `Long`s emitted by an `Observable` and emits this sum. + +#### sumLong example + +```java +Observable<Long> numbers = Observable.rangeLong(1L, 100L); +MathObservable.sumLong(numbers).subscribe((Long sum) -> System.out.println(sum)); + +// prints 5050 +``` + +## Standard Aggregate Operators + +> Note that these standard aggregate operators return a `Single` or `Maybe` because the number of output items is always know to be at most one. + +### count + +**Available in:** ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Flowable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Observable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Maybe`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Single`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Completable` + +**ReactiveX documentation:** [http://reactivex.io/documentation/operators/count.html](http://reactivex.io/documentation/operators/count.html) + +Counts the number of items emitted by an `Observable` and emits this count as a `Long`. + +#### count example + +```java +Observable.just(1, 2, 3).count().subscribe(System.out::println); + +// prints 3 +``` + +### reduce + +**Available in:** ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Flowable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Observable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Maybe`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Single`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Completable` + +**ReactiveX documentation:** [http://reactivex.io/documentation/operators/reduce.html](http://reactivex.io/documentation/operators/reduce.html) + +Apply a function to each emitted item, sequentially, and emit only the final accumulated value. + +#### reduce example + +```java +Observable.range(1, 5) + .reduce((product, x) -> product * x) + .subscribe(System.out::println); + +// prints 120 +``` + +### reduceWith + +**Available in:** ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Flowable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Observable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Maybe`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Single`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Completable` + +**ReactiveX documentation:** [http://reactivex.io/documentation/operators/reduce.html](http://reactivex.io/documentation/operators/reduce.html) + +Apply a function to each emitted item, sequentially, and emit only the final accumulated value. + + +#### reduceWith example + +```java +Observable.just(1, 2, 2, 3, 4, 4, 4, 5) + .reduceWith(TreeSet::new, (set, x) -> { + set.add(x); + return set; + }) + .subscribe(System.out::println); + +// prints [1, 2, 3, 4, 5] +``` + +### collect + +**Available in:** ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Flowable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Observable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Maybe`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Single`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Completable` + +**ReactiveX documentation:** [http://reactivex.io/documentation/operators/reduce.html](http://reactivex.io/documentation/operators/reduce.html) + +Collect items emitted by the source `Observable` into a single mutable data structure and return an `Observable` that emits this structure. + +#### collect example + +```java +Observable.just("Kirk", "Spock", "Chekov", "Sulu") + .collect(() -> new StringJoiner(" \uD83D\uDD96 "), StringJoiner::add) + .map(StringJoiner::toString) + .subscribe(System.out::println); + +// prints Kirk 🖖 Spock 🖖 Chekov 🖖 Sulu +``` + +### collectInto + +**Available in:** ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Flowable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Observable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Maybe`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Single`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Completable` + +**ReactiveX documentation:** [http://reactivex.io/documentation/operators/reduce.html](http://reactivex.io/documentation/operators/reduce.html) + +Collect items emitted by the source `Observable` into a single mutable data structure and return an `Observable` that emits this structure. + +#### collectInto example + +*Note: the mutable value that will collect the items (here the `StringBuilder`) will be shared between multiple subscribers.* + +```java +Observable.just('R', 'x', 'J', 'a', 'v', 'a') + .collectInto(new StringBuilder(), StringBuilder::append) + .map(StringBuilder::toString) + .subscribe(System.out::println); + +// prints RxJava +``` + +### toList + +**Available in:** ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Flowable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Observable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Maybe`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Single`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Completable` + +**ReactiveX documentation:** [http://reactivex.io/documentation/operators/to.html](http://reactivex.io/documentation/operators/to.html) + +Collect all items from an `Observable` and emit them as a single `List`. + +#### toList example + +```java +Observable.just(2, 1, 3) + .toList() + .subscribe(System.out::println); + +// prints [2, 1, 3] +``` + +### toSortedList + +**Available in:** ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Flowable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Observable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Maybe`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Single`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Completable` + +**ReactiveX documentation:** [http://reactivex.io/documentation/operators/to.html](http://reactivex.io/documentation/operators/to.html) + +Collect all items from an `Observable` and emit them as a single, sorted `List`. + +#### toSortedList example + +```java +Observable.just(2, 1, 3) + .toSortedList(Comparator.reverseOrder()) + .subscribe(System.out::println); + +// prints [3, 2, 1] +``` + +### toMap + +**Available in:** ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Flowable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Observable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Maybe`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Single`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Completable` + +**ReactiveX documentation:** [http://reactivex.io/documentation/operators/to.html](http://reactivex.io/documentation/operators/to.html) + +Convert the sequence of items emitted by an `Observable` into a `Map` keyed by a specified key function. + +#### toMap example + +```java +Observable.just(1, 2, 3, 4) + .toMap((x) -> { + // defines the key in the Map + return x; + }, (x) -> { + // defines the value that is mapped to the key + return (x % 2 == 0) ? "even" : "odd"; + }) + .subscribe(System.out::println); + +// prints {1=odd, 2=even, 3=odd, 4=even} +``` + +### toMultimap + +**Available in:** ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Flowable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Observable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Maybe`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Single`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Completable` + +**ReactiveX documentation:** [http://reactivex.io/documentation/operators/to.html](http://reactivex.io/documentation/operators/to.html) + +Convert the sequence of items emitted by an `Observable` into a `Collection` that is also a `Map` keyed by a specified key function. + +#### toMultimap example + +```java +Observable.just(1, 2, 3, 4) + .toMultimap((x) -> { + // defines the key in the Map + return (x % 2 == 0) ? "even" : "odd"; + }, (x) -> { + // defines the value that is mapped to the key + return x; + }) + .subscribe(System.out::println); + +// prints {even=[2, 4], odd=[1, 3]} +``` diff --git a/docs/Observable-Utility-Operators.md b/docs/Observable-Utility-Operators.md new file mode 100644 index 0000000000..b23c871894 --- /dev/null +++ b/docs/Observable-Utility-Operators.md @@ -0,0 +1,28 @@ +This page lists various utility operators for working with Observables. + +* [**`materialize( )`**](http://reactivex.io/documentation/operators/materialize-dematerialize.html) — convert an Observable into a list of Notifications +* [**`dematerialize( )`**](http://reactivex.io/documentation/operators/materialize-dematerialize.html) — convert a materialized Observable back into its non-materialized form +* [**`timestamp( )`**](http://reactivex.io/documentation/operators/timestamp.html) — attach a timestamp to every item emitted by an Observable +* [**`serialize( )`**](http://reactivex.io/documentation/operators/serialize.html) — force an Observable to make serialized calls and to be well-behaved +* [**`cache( )`**](http://reactivex.io/documentation/operators/replay.html) — remember the sequence of items emitted by the Observable and emit the same sequence to future Subscribers +* [**`observeOn( )`**](http://reactivex.io/documentation/operators/observeon.html) — specify on which Scheduler a Subscriber should observe the Observable +* [**`subscribeOn( )`**](http://reactivex.io/documentation/operators/subscribeon.html) — specify which Scheduler an Observable should use when its subscription is invoked +* [**`doOnEach( )`**](http://reactivex.io/documentation/operators/do.html) — register an action to take whenever an Observable emits an item +* [**`doOnNext( )`**](http://reactivex.io/documentation/operators/do.html) — register an action to call just before the Observable passes an `onNext` event along to its downstream +* [**`doAfterNext( )`**](http://reactivex.io/documentation/operators/do.html) — register an action to call after the Observable has passed an `onNext` event along to its downstream +* [**`doOnCompleted( )`**](http://reactivex.io/documentation/operators/do.html) — register an action to take when an Observable completes successfully +* [**`doOnError( )`**](http://reactivex.io/documentation/operators/do.html) — register an action to take when an Observable completes with an error +* [**`doOnTerminate( )`**](http://reactivex.io/documentation/operators/do.html) — register an action to call just before an Observable terminates, either successfully or with an error +* [**`doAfterTerminate( )`**](http://reactivex.io/documentation/operators/do.html) — register an action to call just after an Observable terminated, either successfully or with an error +* [**`doOnSubscribe( )`**](http://reactivex.io/documentation/operators/do.html) — register an action to take when an observer subscribes to an Observable +* *1.x* [**`doOnUnsubscribe( )`**](http://reactivex.io/documentation/operators/do.html) — register an action to take when an observer unsubscribes from an Observable +* [**`finallyDo( )`**](http://reactivex.io/documentation/operators/do.html) — register an action to take when an Observable completes +* [**`doFinally( )`**](http://reactivex.io/documentation/operators/do.html) — register an action to call when an Observable terminates or it gets disposed +* [**`delay( )`**](http://reactivex.io/documentation/operators/delay.html) — shift the emissions from an Observable forward in time by a specified amount +* [**`delaySubscription( )`**](http://reactivex.io/documentation/operators/delay.html) — hold an Subscriber's subscription request for a specified amount of time before passing it on to the source Observable +* [**`timeInterval( )`**](http://reactivex.io/documentation/operators/timeinterval.html) — emit the time lapsed between consecutive emissions of a source Observable +* [**`using( )`**](http://reactivex.io/documentation/operators/using.html) — create a disposable resource that has the same lifespan as an Observable +* [**`single( )`**](http://reactivex.io/documentation/operators/first.html) — if the Observable completes after emitting a single item, return that item, otherwise throw an exception +* [**`singleOrDefault( )`**](http://reactivex.io/documentation/operators/first.html) — if the Observable completes after emitting a single item, return that item, otherwise return a default item +* [**`repeat( )`**](http://reactivex.io/documentation/operators/repeat.html) — create an Observable that emits a particular item or sequence of items repeatedly +* [**`repeatWhen( )`**](http://reactivex.io/documentation/operators/repeat.html) — create an Observable that emits a particular item or sequence of items repeatedly, depending on the emissions of a second Observable \ No newline at end of file diff --git a/docs/Observable.md b/docs/Observable.md new file mode 100644 index 0000000000..fec5467908 --- /dev/null +++ b/docs/Observable.md @@ -0,0 +1,3 @@ +In RxJava an object that implements the _Observer_ interface _subscribes_ to an object of the _Observable_ class. Then that subscriber reacts to whatever item or sequence of items the Observable object _emits_. This pattern facilitates concurrent operations because it does not need to block while waiting for the Observable to emit objects, but instead it creates a sentry in the form of a subscriber that stands ready to react appropriately at whatever future time the Observable does so. + +For information about the Observable class, see [the Observable documentation page at ReactiveX.io](http://reactivex.io/documentation/observable.html). \ No newline at end of file diff --git a/docs/Operator-Matrix.md b/docs/Operator-Matrix.md new file mode 100644 index 0000000000..afeb8e4182 --- /dev/null +++ b/docs/Operator-Matrix.md @@ -0,0 +1,359 @@ +Operator | ![Flowable](https://raw.github.com/wiki/ReactiveX/RxJava/images/opmatrix-flowable.png) | ![Observable](https://raw.github.com/wiki/ReactiveX/RxJava/images/opmatrix-observable.png) | ![Maybe](https://raw.github.com/wiki/ReactiveX/RxJava/images/opmatrix-maybe.png) | ![Single](https://raw.github.com/wiki/ReactiveX/RxJava/images/opmatrix-single.png) | ![Completable](https://raw.github.com/wiki/ReactiveX/RxJava/images/opmatrix-completable.png) | +-----|---|---|---|---|---| +<a name='all'></a>`all`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Use contains().'>([1](#notes-1))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Use contains().'>([1](#notes-1))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Always empty.'>([2](#notes-2))</sup>| +<a name='amb'></a>`amb`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)| +<a name='ambArray'></a>`ambArray`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)| +<a name='ambWith'></a>`ambWith`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)| +<a name='andThen'></a>`andThen`|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Use concatWith.'>([3](#notes-3))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Use concatWith.'>([3](#notes-3))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Use concatWith.'>([3](#notes-3))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Use concatWith.'>([3](#notes-3))</sup>|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)| +<a name='any'></a>`any`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Use contains().'>([1](#notes-1))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Use contains().'>([1](#notes-1))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Always empty.'>([2](#notes-2))</sup>| +<a name='blockingAwait'></a>`blockingAwait`|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Use blockingFirst().'>([4](#notes-4))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Use blockingFirst().'>([4](#notes-4))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Use blockingGet().'>([5](#notes-5))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Use blockingGet().'>([5](#notes-5))</sup>|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)| +<a name='blockingFirst'></a>`blockingFirst`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='At most one element to get. Use blockingGet().'>([6](#notes-6))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='At most one element to get. Use blockingGet().'>([6](#notes-6))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='No elements to get. Use blockingAwait().'>([7](#notes-7))</sup>| +<a name='blockingForEach'></a>`blockingForEach`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Use blockingSubscribe()'>([8](#notes-8))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Use blockingSubscribe()'>([8](#notes-8))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Use blockingSubscribe()'>([8](#notes-8))</sup>| +<a name='blockingGet'></a>`blockingGet`|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Use blockingFirst().'>([4](#notes-4))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Use blockingFirst().'>([4](#notes-4))</sup>|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='No elements to get. Use blockingAwait().'>([7](#notes-7))</sup>| +<a name='blockingIterable'></a>`blockingIterable`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='At most one element to get. Use blockingGet().'>([6](#notes-6))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='At most one element to get. Use blockingGet().'>([6](#notes-6))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='No elements to get. Use blockingAwait().'>([7](#notes-7))</sup>| +<a name='blockingLast'></a>`blockingLast`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='At most one element to get. Use blockingGet().'>([6](#notes-6))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='At most one element to get. Use blockingGet().'>([6](#notes-6))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='No elements to get. Use blockingAwait().'>([7](#notes-7))</sup>| +<a name='blockingLatest'></a>`blockingLatest`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='At most one element to get. Use blockingGet().'>([6](#notes-6))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='At most one element to get. Use blockingGet().'>([6](#notes-6))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='No elements to get. Use blockingAwait().'>([7](#notes-7))</sup>| +<a name='blockingMostRecent'></a>`blockingMostRecent`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='At most one element to get. Use blockingGet().'>([6](#notes-6))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='At most one element to get. Use blockingGet().'>([6](#notes-6))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='No elements to get. Use blockingAwait().'>([7](#notes-7))</sup>| +<a name='blockingNext'></a>`blockingNext`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='At most one element to get. Use blockingGet().'>([6](#notes-6))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='At most one element to get. Use blockingGet().'>([6](#notes-6))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='No elements to get. Use blockingAwait().'>([7](#notes-7))</sup>| +<a name='blockingSingle'></a>`blockingSingle`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='At most one element to get. Use blockingGet().'>([6](#notes-6))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='At most one element to get. Use blockingGet().'>([6](#notes-6))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='No elements to get. Use blockingAwait().'>([7](#notes-7))</sup>| +<a name='blockingStream'></a>`blockingStream`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='At most one element to get. Use blockingGet().'>([6](#notes-6))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='At most one element to get. Use blockingGet().'>([6](#notes-6))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='No elements to get. Use blockingAwait().'>([7](#notes-7))</sup>| +<a name='blockingSubscribe'></a>`blockingSubscribe`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)| +<a name='buffer'></a>`buffer`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Use map() to transform into a list/collection.'>([9](#notes-9))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Use map() to transform into a list/collection.'>([10](#notes-10))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Always empty. Use andThen() to bring in a list/collection.'>([11](#notes-11))</sup>| +<a name='cache'></a>`cache`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)| +<a name='cacheWithInitialCapacity'></a>`cacheWithInitialCapacity`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='At most one element to store. Use cache().'>([12](#notes-12))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='At most one element to store. Use cache().'>([12](#notes-12))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='At most one element to store. Use cache().'>([12](#notes-12))</sup>| +<a name='cast'></a>`cast`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Always empty.'>([2](#notes-2))</sup>| +<a name='collect'></a>`collect`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='At most one element to collect. Use map() to transform into a list/collection.'>([13](#notes-13))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='One element to collect. Use map() to transform into a list/collection.'>([14](#notes-14))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Always empty. Use andThen() to bring in a collection.'>([15](#notes-15))</sup>| +<a name='collectInto'></a>`collectInto`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='At most one element to collect. Use map() to transform into a list/collection.'>([13](#notes-13))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='One element to collect. Use map() to transform into a list/collection.'>([14](#notes-14))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Always empty. Use andThen() to bring in a collection.'>([15](#notes-15))</sup>| +<a name='combineLatest'></a>`combineLatest`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='At most one element per source. Use zip().'>([16](#notes-16))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='At most one element per source. Use zip().'>([16](#notes-16))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Always empty. Use merge().'>([17](#notes-17))</sup>| +<a name='combineLatestArray'></a>`combineLatestArray`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='At most one element per source. Use zipArray().'>([18](#notes-18))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='At most one element per source. Use zipArray().'>([18](#notes-18))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Always empty. Use mergeArray().'>([19](#notes-19))</sup>| +<a name='combineLatestArrayDelayError'></a>`combineLatestArrayDelayError`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='At most one element per source. Use zipArray().'>([18](#notes-18))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='At most one element per source. Use zipArray().'>([18](#notes-18))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Always empty. Use mergeArrayDelayError().'>([20](#notes-20))</sup>| +<a name='combineLatestDelayError'></a>`combineLatestDelayError`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='At most one element per source. Use zip().'>([16](#notes-16))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='At most one element per source. Use zip().'>([16](#notes-16))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Always empty. Use mergeDelayError().'>([21](#notes-21))</sup>| +<a name='complete'></a>`complete`|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Use empty().'>([22](#notes-22))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Use empty().'>([22](#notes-22))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Use empty().'>([22](#notes-22))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Never empty.'>([23](#notes-23))</sup>|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)| +<a name='compose'></a>`compose`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)| +<a name='concat'></a>`concat`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)| +<a name='concatArray'></a>`concatArray`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)| +<a name='concatArrayDelayError'></a>`concatArrayDelayError`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)| +<a name='concatArrayEager'></a>`concatArrayEager`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='No items to keep ordered. Use mergeArray().'>([24](#notes-24))</sup>| +<a name='concatArrayEagerDelayError'></a>`concatArrayEagerDelayError`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='No items to keep ordered. Use mergeArrayDelayError().'>([25](#notes-25))</sup>| +<a name='concatDelayError'></a>`concatDelayError`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)| +<a name='concatEager'></a>`concatEager`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='No items to keep ordered. Use merge().'>([26](#notes-26))</sup>| +<a name='concatEagerDelayError'></a>`concatEagerDelayError`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='No items to keep ordered. Use mergeDelayError().'>([27](#notes-27))</sup>| +<a name='concatMap'></a>`concatMap`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Always empty thus no items to map.'>([28](#notes-28))</sup>| +<a name='concatMapCompletable'></a>`concatMapCompletable`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Always empty thus no items to map.'>([28](#notes-28))</sup>| +<a name='concatMapCompletableDelayError'></a>`concatMapCompletableDelayError`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Either the upstream fails (thus no inner) or the mapped-in source, but never both. Use concatMapCompletable.'>([29](#notes-29))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Either the upstream fails (thus no inner) or the mapped-in source, but never both. Use concatMapCompletable.'>([29](#notes-29))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Always empty thus no items to map.'>([28](#notes-28))</sup>| +<a name='concatMapDelayError'></a>`concatMapDelayError`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Either the upstream fails (thus no inner) or the mapped-in source, but never both. Use concatMap.'>([30](#notes-30))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Either the upstream fails (thus no inner) or the mapped-in source, but never both. Use concatMap.'>([30](#notes-30))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Always empty thus no items to map.'>([28](#notes-28))</sup>| +<a name='concatMapEager'></a>`concatMapEager`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='At most one item to map. Use concatMap().'>([31](#notes-31))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='At most one item to map. Use concatMap().'>([31](#notes-31))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Always empty thus no items to map.'>([28](#notes-28))</sup>| +<a name='concatMapEagerDelayError'></a>`concatMapEagerDelayError`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='At most one item to map. Use concatMap().'>([31](#notes-31))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='At most one item to map. Use concatMap().'>([31](#notes-31))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Always empty thus no items to map.'>([28](#notes-28))</sup>| +<a name='concatMapIterable'></a>`concatMapIterable`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='At most one item. Use flattenAsFlowable.'>([32](#notes-32))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='At most one item. Use flattenAsFlowable.'>([32](#notes-32))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Always empty thus no items to map.'>([28](#notes-28))</sup>| +<a name='concatMapMaybe'></a>`concatMapMaybe`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Use concatMap.'>([33](#notes-33))</sup>|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Always empty thus no items to map.'>([28](#notes-28))</sup>| +<a name='concatMapMaybeDelayError'></a>`concatMapMaybeDelayError`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Either the upstream fails (thus no inner) or the mapped-in source, but never both. Use concatMapMaybe.'>([34](#notes-34))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Either the upstream fails (thus no inner) or the mapped-in source, but never both. Use concatMapMaybe.'>([34](#notes-34))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Always empty thus no items to map.'>([28](#notes-28))</sup>| +<a name='concatMapSingle'></a>`concatMapSingle`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Use concatMap().'>([35](#notes-35))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Always empty thus no items to map.'>([28](#notes-28))</sup>| +<a name='concatMapSingleDelayError'></a>`concatMapSingleDelayError`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Either the upstream fails (thus no inner) or the mapped-in source, but never both. Use concatMapSingle.'>([36](#notes-36))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Either the upstream fails (thus no inner) or the mapped-in source, but never both. Use concatMapSingle.'>([36](#notes-36))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Always empty thus no items to map.'>([28](#notes-28))</sup>| +<a name='concatMapStream'></a>`concatMapStream`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='At most one item. Use flattenStreamAsFlowable.'>([37](#notes-37))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='At most one item. Use flattenStreamAsFlowable.'>([37](#notes-37))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Always empty thus no items to map.'>([28](#notes-28))</sup>| +<a name='concatWith'></a>`concatWith`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)| +<a name='contains'></a>`contains`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Always empty.'>([2](#notes-2))</sup>| +<a name='count'></a>`count`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Never empty thus always 1.'>([38](#notes-38))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Always empty thus always 0.'>([39](#notes-39))</sup>| +<a name='create'></a>`create`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)| +<a name='debounce'></a>`debounce`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='At most one item signaled so no subsequent items to work with.'>([40](#notes-40))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='At most one item signaled so no subsequent items to work with.'>([40](#notes-40))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Always empty thus no items to work with.'>([41](#notes-41))</sup>| +<a name='defaultIfEmpty'></a>`defaultIfEmpty`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Never empty.'>([23](#notes-23))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Always empty. Use andThen() to chose the follow-up sequence.'>([42](#notes-42))</sup>| +<a name='defer'></a>`defer`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)| +<a name='delay'></a>`delay`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)| +<a name='delaySubscription'></a>`delaySubscription`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)| +<a name='dematerialize'></a>`dematerialize`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Always empty thus no items to work with.'>([41](#notes-41))</sup>| +<a name='distinct'></a>`distinct`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='At most one item, always distinct.'>([43](#notes-43))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='At most one item, always distinct.'>([43](#notes-43))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Always empty thus no items to work with.'>([41](#notes-41))</sup>| +<a name='distinctUntilChanged'></a>`distinctUntilChanged`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='At most one item, always distinct.'>([43](#notes-43))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='At most one item, always distinct.'>([43](#notes-43))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Always empty thus no items to work with.'>([41](#notes-41))</sup>| +<a name='doAfterNext'></a>`doAfterNext`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Different terminology. Use doAfterSuccess().'>([44](#notes-44))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Different terminology. Use doAfterSuccess().'>([44](#notes-44))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Always empty.'>([2](#notes-2))</sup>| +<a name='doAfterSuccess'></a>`doAfterSuccess`|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Different terminology. Use doAfterNext().'>([45](#notes-45))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Different terminology. Use doAfterNext().'>([45](#notes-45))</sup>|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Always empty thus no items to work with.'>([41](#notes-41))</sup>| +<a name='doAfterTerminate'></a>`doAfterTerminate`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)| +<a name='doFinally'></a>`doFinally`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)| +<a name='doOnCancel'></a>`doOnCancel`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Different terminology. Use doOnDispose().'>([46](#notes-46))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Different terminology. Use doOnDispose().'>([46](#notes-46))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Different terminology. Use doOnDispose().'>([46](#notes-46))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Different terminology. Use doOnDispose().'>([46](#notes-46))</sup>| +<a name='doOnComplete'></a>`doOnComplete`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Always succeeds or fails, there is no onComplete signal.'>([47](#notes-47))</sup>|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)| +<a name='doOnDispose'></a>`doOnDispose`|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Different terminology. Use doOnCancel().'>([48](#notes-48))</sup>|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)| +<a name='doOnEach'></a>`doOnEach`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='At most one item. Use doOnEvent().'>([49](#notes-49))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='At most one item. Use doOnEvent().'>([49](#notes-49))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Always empty thus no items to work with.'>([41](#notes-41))</sup>| +<a name='doOnError'></a>`doOnError`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)| +<a name='doOnEvent'></a>`doOnEvent`|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Use doOnEach().'>([50](#notes-50))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Use doOnEach().'>([50](#notes-50))</sup>|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)| +<a name='doOnLifecycle'></a>`doOnLifecycle`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)| +<a name='doOnNext'></a>`doOnNext`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Different terminology. Use doOnSuccess().'>([51](#notes-51))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Different terminology. Use doOnSuccess().'>([51](#notes-51))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Always empty thus no items to work with.'>([41](#notes-41))</sup>| +<a name='doOnRequest'></a>`doOnRequest`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Backpressure related and not supported outside Flowable.'>([52](#notes-52))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Backpressure related and not supported outside Flowable.'>([52](#notes-52))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Backpressure related and not supported outside Flowable.'>([52](#notes-52))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Backpressure related and not supported outside Flowable.'>([52](#notes-52))</sup>| +<a name='doOnSubscribe'></a>`doOnSubscribe`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)| +<a name='doOnSuccess'></a>`doOnSuccess`|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Different terminology. Use doOnNext().'>([53](#notes-53))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Different terminology. Use doOnNext().'>([53](#notes-53))</sup>|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Always empty thus no items to work with.'>([41](#notes-41))</sup>| +<a name='doOnTerminate'></a>`doOnTerminate`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)| +<a name='elementAt'></a>`elementAt`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='At most one item with index 0. Use defaultIfEmpty.'>([54](#notes-54))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Always one item with index 0.'>([55](#notes-55))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Always empty thus no items to work with.'>([41](#notes-41))</sup>| +<a name='elementAtOrError'></a>`elementAtOrError`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='At most one item with index 0. Use toSingle.'>([56](#notes-56))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Always one item with index 0.'>([55](#notes-55))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Always empty thus no items to work with.'>([41](#notes-41))</sup>| +<a name='empty'></a>`empty`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Never empty.'>([23](#notes-23))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Use complete().'>([57](#notes-57))</sup>| +<a name='error'></a>`error`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)| +<a name='filter'></a>`filter`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Always empty thus no items to work with.'>([41](#notes-41))</sup>| +<a name='first'></a>`first`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='At most one item. Use defaultIfEmpty.'>([58](#notes-58))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Always one item.'>([59](#notes-59))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Always empty. Use andThen() to chose the follow-up sequence.'>([42](#notes-42))</sup>| +<a name='firstElement'></a>`firstElement`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='At most one item, would be no-op.'>([60](#notes-60))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Always one item, would be no-op.'>([61](#notes-61))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Always empty.'>([2](#notes-2))</sup>| +<a name='firstOrError'></a>`firstOrError`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='At most one item, would be no-op.'>([60](#notes-60))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Always one item, would be no-op.'>([61](#notes-61))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Always empty. Use andThen().'>([62](#notes-62))</sup>| +<a name='firstOrErrorStage'></a>`firstOrErrorStage`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='At most one item. Use toCompletionStage().'>([63](#notes-63))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='At most one item. Use toCompletionStage().'>([63](#notes-63))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Always empty. Use andThen().'>([64](#notes-64))</sup>| +<a name='firstStage'></a>`firstStage`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='At most one item. Use toCompletionStage().'>([63](#notes-63))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='At most one item. Use toCompletionStage().'>([63](#notes-63))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='At most one item. Use toCompletionStage().'>([63](#notes-63))</sup>| +<a name='flatMap'></a>`flatMap`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Always empty thus no items to map.'>([28](#notes-28))</sup>| +<a name='flatMapCompletable'></a>`flatMapCompletable`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Always empty thus no items to map.'>([28](#notes-28))</sup>| +<a name='flatMapIterable'></a>`flatMapIterable`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='At most one item. Use flattenAsFlowable.'>([32](#notes-32))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='At most one item. Use flattenAsFlowable.'>([32](#notes-32))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Always empty thus no items to map.'>([28](#notes-28))</sup>| +<a name='flatMapMaybe'></a>`flatMapMaybe`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Use flatMap().'>([65](#notes-65))</sup>|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Always empty thus no items to map.'>([28](#notes-28))</sup>| +<a name='flatMapObservable'></a>`flatMapObservable`|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Not supported. Use flatMap.'>([66](#notes-66))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Use flatMap.'>([67](#notes-67))</sup>|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Always empty thus no items to map.'>([28](#notes-28))</sup>| +<a name='flatMapPublisher'></a>`flatMapPublisher`|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Use flatMap.'>([67](#notes-67))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Not supported. Use flatMap.'>([68](#notes-68))</sup>|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Always empty thus no items to map.'>([28](#notes-28))</sup>| +<a name='flatMapSingle'></a>`flatMapSingle`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Use flatMap().'>([65](#notes-65))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Always empty thus no items to map.'>([28](#notes-28))</sup>| +<a name='flatMapStream'></a>`flatMapStream`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='At most one item. Use flattenStreamAsFlowable.'>([37](#notes-37))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='At most one item. Use flattenStreamAsFlowable.'>([37](#notes-37))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Always empty thus no items to map.'>([28](#notes-28))</sup>| +<a name='flattenAsFlowable'></a>`flattenAsFlowable`|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Use flatMapIterable().'>([69](#notes-69))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Use flatMapIterable().'>([69](#notes-69))</sup>|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Always empty thus no items to map.'>([28](#notes-28))</sup>| +<a name='flattenAsObservable'></a>`flattenAsObservable`|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Use flatMapIterable().'>([69](#notes-69))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Use flatMapIterable().'>([69](#notes-69))</sup>|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Always empty thus no items to map.'>([28](#notes-28))</sup>| +<a name='flattenStreamAsFlowable'></a>`flattenStreamAsFlowable`|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Use flatMapStream().'>([70](#notes-70))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Use flatMapStream().'>([70](#notes-70))</sup>|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Always empty thus no items to map.'>([28](#notes-28))</sup>| +<a name='flattenStreamAsObservable'></a>`flattenStreamAsObservable`|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Use flatMapStream().'>([70](#notes-70))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Use flatMapStream().'>([70](#notes-70))</sup>|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Always empty thus no items to map.'>([28](#notes-28))</sup>| +<a name='forEach'></a>`forEach`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Use subscribe().'>([71](#notes-71))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Use subscribe().'>([71](#notes-71))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Use subscribe().'>([71](#notes-71))</sup>| +<a name='forEachWhile'></a>`forEachWhile`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Use subscribe().'>([71](#notes-71))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Use subscribe().'>([71](#notes-71))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Use subscribe().'>([71](#notes-71))</sup>| +<a name='fromAction'></a>`fromAction`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Never empty.'>([23](#notes-23))</sup>|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)| +<a name='fromArray'></a>`fromArray`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='At most one item. Use just().'>([72](#notes-72))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Always one item. Use just().'>([73](#notes-73))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Always empty. Use complete().'>([74](#notes-74))</sup>| +<a name='fromCallable'></a>`fromCallable`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)| +<a name='fromCompletable'></a>`fromCompletable`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Always error.'>([75](#notes-75))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Use wrap().'>([76](#notes-76))</sup>| +<a name='fromCompletionStage'></a>`fromCompletionStage`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)| +<a name='fromFuture'></a>`fromFuture`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)| +<a name='fromIterable'></a>`fromIterable`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='At most one item. Use just().'>([72](#notes-72))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Always one item. Use just().'>([73](#notes-73))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Always empty. Use complete().'>([74](#notes-74))</sup>| +<a name='fromMaybe'></a>`fromMaybe`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Use wrap().'>([76](#notes-76))</sup>|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)| +<a name='fromObservable'></a>`fromObservable`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Use wrap().'>([76](#notes-76))</sup>|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)| +<a name='fromOptional'></a>`fromOptional`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Always one item. Use just().'>([73](#notes-73))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Always empty. Use complete().'>([74](#notes-74))</sup>| +<a name='fromPublisher'></a>`fromPublisher`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)| +<a name='fromRunnable'></a>`fromRunnable`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Never empty.'>([23](#notes-23))</sup>|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)| +<a name='fromSingle'></a>`fromSingle`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Use wrap().'>([76](#notes-76))</sup>|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)| +<a name='fromStream'></a>`fromStream`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='At most one item. Use just().'>([72](#notes-72))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Always one item. Use just().'>([73](#notes-73))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Always empty. Use complete().'>([74](#notes-74))</sup>| +<a name='fromSupplier'></a>`fromSupplier`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)| +<a name='generate'></a>`generate`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Use fromSupplier().'>([77](#notes-77))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Use fromSupplier().'>([77](#notes-77))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Use fromSupplier().'>([77](#notes-77))</sup>| +<a name='groupBy'></a>`groupBy`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='At most one item.'>([78](#notes-78))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='At most one item.'>([78](#notes-78))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Always empty thus no items to group.'>([79](#notes-79))</sup>| +<a name='groupJoin'></a>`groupJoin`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='At most one item.'>([78](#notes-78))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='At most one item.'>([78](#notes-78))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Always empty thus no items to join.'>([80](#notes-80))</sup>| +<a name='hide'></a>`hide`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)| +<a name='ignoreElement'></a>`ignoreElement`|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Use ignoreElements().'>([81](#notes-81))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Use ignoreElements().'>([81](#notes-81))</sup>|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Always empty.'>([2](#notes-2))</sup>| +<a name='ignoreElements'></a>`ignoreElements`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Use ignoreElement().'>([82](#notes-82))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Use ignoreElement().'>([82](#notes-82))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Always empty.'>([2](#notes-2))</sup>| +<a name='interval'></a>`interval`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='At most one item. Use timer().'>([83](#notes-83))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='At most one item. Use timer().'>([83](#notes-83))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='At most one item. Use timer().'>([83](#notes-83))</sup>| +<a name='intervalRange'></a>`intervalRange`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='At most one item. Use timer().'>([83](#notes-83))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='At most one item. Use timer().'>([83](#notes-83))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='At most one item. Use timer().'>([83](#notes-83))</sup>| +<a name='isEmpty'></a>`isEmpty`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Always one item.'>([59](#notes-59))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Always empty.'>([2](#notes-2))</sup>| +<a name='join'></a>`join`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='At most one item. Use zip()'>([84](#notes-84))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='At most one item. Use zip()'>([84](#notes-84))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Always empty thus no items to join.'>([80](#notes-80))</sup>| +<a name='just'></a>`just`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Always empty.'>([2](#notes-2))</sup>| +<a name='last'></a>`last`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='At most one item. Use defaultIfEmpty.'>([58](#notes-58))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Always one item.'>([59](#notes-59))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Always empty. Use andThen() to chose the follow-up sequence.'>([42](#notes-42))</sup>| +<a name='lastElement'></a>`lastElement`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='At most one item, would be no-op.'>([60](#notes-60))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Always one item, would be no-op.'>([61](#notes-61))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Always empty.'>([2](#notes-2))</sup>| +<a name='lastOrError'></a>`lastOrError`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='At most one item, would be no-op.'>([60](#notes-60))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Always one item, would be no-op.'>([61](#notes-61))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Always empty. Use andThen().'>([62](#notes-62))</sup>| +<a name='lastOrErrorStage'></a>`lastOrErrorStage`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='At most one item. Use toCompletionStage().'>([63](#notes-63))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='At most one item. Use toCompletionStage().'>([63](#notes-63))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Always empty. Use andThen().'>([64](#notes-64))</sup>| +<a name='lastStage'></a>`lastStage`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='At most one item. Use toCompletionStage().'>([63](#notes-63))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='At most one item. Use toCompletionStage().'>([63](#notes-63))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='At most one item. Use toCompletionStage().'>([63](#notes-63))</sup>| +<a name='lift'></a>`lift`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)| +<a name='map'></a>`map`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Always empty thus no items to map.'>([28](#notes-28))</sup>| +<a name='mapOptional'></a>`mapOptional`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Always empty thus no items to map.'>([28](#notes-28))</sup>| +<a name='materialize'></a>`materialize`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)| +<a name='merge'></a>`merge`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)| +<a name='mergeArray'></a>`mergeArray`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)| +<a name='mergeArrayDelayError'></a>`mergeArrayDelayError`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)| +<a name='mergeDelayError'></a>`mergeDelayError`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)| +<a name='mergeWith'></a>`mergeWith`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)| +<a name='never'></a>`never`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)| +<a name='observeOn'></a>`observeOn`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)| +<a name='ofType'></a>`ofType`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Always empty thus no items to filter.'>([85](#notes-85))</sup>| +<a name='onBackpressureBuffer'></a>`onBackpressureBuffer`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Backpressure related and not supported outside Flowable.'>([52](#notes-52))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Backpressure related and not supported outside Flowable.'>([52](#notes-52))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Backpressure related and not supported outside Flowable.'>([52](#notes-52))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Backpressure related and not supported outside Flowable.'>([52](#notes-52))</sup>| +<a name='onBackpressureDrop'></a>`onBackpressureDrop`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Backpressure related and not supported outside Flowable.'>([52](#notes-52))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Backpressure related and not supported outside Flowable.'>([52](#notes-52))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Backpressure related and not supported outside Flowable.'>([52](#notes-52))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Backpressure related and not supported outside Flowable.'>([52](#notes-52))</sup>| +<a name='onBackpressureLatest'></a>`onBackpressureLatest`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Backpressure related and not supported outside Flowable.'>([52](#notes-52))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Backpressure related and not supported outside Flowable.'>([52](#notes-52))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Backpressure related and not supported outside Flowable.'>([52](#notes-52))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Backpressure related and not supported outside Flowable.'>([52](#notes-52))</sup>| +<a name='onErrorComplete'></a>`onErrorComplete`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)| +<a name='onErrorResumeNext'></a>`onErrorResumeNext`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)| +<a name='onErrorResumeWith'></a>`onErrorResumeWith`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)| +<a name='onErrorReturn'></a>`onErrorReturn`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)| +<a name='onErrorReturnItem'></a>`onErrorReturnItem`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)| +<a name='onTerminateDetach'></a>`onTerminateDetach`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)| +<a name='parallel'></a>`parallel`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Needs backpressure thus not supported outside Flowable.'>([86](#notes-86))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Needs backpressure thus not supported outside Flowable.'>([86](#notes-86))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Needs backpressure thus not supported outside Flowable.'>([86](#notes-86))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Needs backpressure thus not supported outside Flowable.'>([86](#notes-86))</sup>| +<a name='publish'></a>`publish`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Connectable sources not supported outside Flowable and Observable. Use a MaybeSubject.'>([87](#notes-87))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Connectable sources not supported outside Flowable and Observable. Use a SingleSubject.'>([88](#notes-88))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Connectable sources not supported outside Flowable and Observable. Use a ConnectableSubject.'>([89](#notes-89))</sup>| +<a name='range'></a>`range`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='At most one item. Use just().'>([90](#notes-90))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='At most one item. Use just().'>([90](#notes-90))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Always empty. Use complete().'>([74](#notes-74))</sup>| +<a name='rangeLong'></a>`rangeLong`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='At most one item. Use just().'>([90](#notes-90))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='At most one item. Use just().'>([90](#notes-90))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Always empty. Use complete().'>([74](#notes-74))</sup>| +<a name='rebatchRequests'></a>`rebatchRequests`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Backpressure related and not supported outside Flowable.'>([52](#notes-52))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Backpressure related and not supported outside Flowable.'>([52](#notes-52))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Backpressure related and not supported outside Flowable.'>([52](#notes-52))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Backpressure related and not supported outside Flowable.'>([52](#notes-52))</sup>| +<a name='reduce'></a>`reduce`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='At most one item. Use map().'>([91](#notes-91))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='At most one item. Use map().'>([91](#notes-91))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Always empty thus no items to reduce.'>([92](#notes-92))</sup>| +<a name='reduceWith'></a>`reduceWith`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='At most one item. Use map().'>([91](#notes-91))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='At most one item. Use map().'>([91](#notes-91))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Always empty thus no items to reduce.'>([92](#notes-92))</sup>| +<a name='repeat'></a>`repeat`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)| +<a name='repeatUntil'></a>`repeatUntil`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)| +<a name='repeatWhen'></a>`repeatWhen`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)| +<a name='replay'></a>`replay`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Connectable sources not supported outside Flowable and Observable. Use a MaybeSubject.'>([87](#notes-87))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Connectable sources not supported outside Flowable and Observable. Use a SingleSubject.'>([88](#notes-88))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Connectable sources not supported outside Flowable and Observable. Use a ConnectableSubject.'>([89](#notes-89))</sup>| +<a name='retry'></a>`retry`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)| +<a name='retryUntil'></a>`retryUntil`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)| +<a name='retryWhen'></a>`retryWhen`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)| +<a name='safeSubscribe'></a>`safeSubscribe`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)| +<a name='sample'></a>`sample`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='At most one item, would be no-op.'>([60](#notes-60))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='At most one item, would be no-op.'>([60](#notes-60))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Always empty thus no items to work with.'>([41](#notes-41))</sup>| +<a name='scan'></a>`scan`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='At most one item. Use map().'>([91](#notes-91))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='At most one item. Use map().'>([91](#notes-91))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Always empty thus no items to reduce.'>([92](#notes-92))</sup>| +<a name='scanWith'></a>`scanWith`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='At most one item. Use map().'>([91](#notes-91))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='At most one item. Use map().'>([91](#notes-91))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Always empty thus no items to reduce.'>([92](#notes-92))</sup>| +<a name='sequenceEqual'></a>`sequenceEqual`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)| +<a name='serialize'></a>`serialize`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='At most one signal type.'>([93](#notes-93))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='At most one signal type.'>([93](#notes-93))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='At most one signal type.'>([93](#notes-93))</sup>| +<a name='share'></a>`share`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Connectable sources not supported outside Flowable and Observable. Use a MaybeSubject.'>([87](#notes-87))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Connectable sources not supported outside Flowable and Observable. Use a SingleSubject.'>([88](#notes-88))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Connectable sources not supported outside Flowable and Observable. Use a ConnectableSubject.'>([89](#notes-89))</sup>| +<a name='single'></a>`single`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='At most one item. Use defaultIfEmpty.'>([58](#notes-58))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Always one item.'>([59](#notes-59))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Always empty. Use andThen() to chose the follow-up sequence.'>([42](#notes-42))</sup>| +<a name='singleElement'></a>`singleElement`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='At most one item, would be no-op.'>([60](#notes-60))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Always one item, would be no-op.'>([61](#notes-61))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Always empty.'>([2](#notes-2))</sup>| +<a name='singleOrError'></a>`singleOrError`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='At most one item, would be no-op.'>([60](#notes-60))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Always one item, would be no-op.'>([61](#notes-61))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Always empty. Use andThen().'>([62](#notes-62))</sup>| +<a name='singleOrErrorStage'></a>`singleOrErrorStage`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='At most one item. Use toCompletionStage().'>([63](#notes-63))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='At most one item. Use toCompletionStage().'>([63](#notes-63))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Always empty. Use andThen().'>([64](#notes-64))</sup>| +<a name='singleStage'></a>`singleStage`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='At most one item. Use toCompletionStage().'>([63](#notes-63))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='At most one item. Use toCompletionStage().'>([63](#notes-63))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='At most one item. Use toCompletionStage().'>([63](#notes-63))</sup>| +<a name='skip'></a>`skip`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='At most one item, would be no-op.'>([60](#notes-60))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='At most one item, would be no-op.'>([60](#notes-60))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='At most one item, would be no-op.'>([60](#notes-60))</sup>| +<a name='skipLast'></a>`skipLast`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='At most one item, would be no-op.'>([60](#notes-60))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='At most one item, would be no-op.'>([60](#notes-60))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='At most one item, would be no-op.'>([60](#notes-60))</sup>| +<a name='skipUntil'></a>`skipUntil`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='At most one item. Use takeUntil().'>([94](#notes-94))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='At most one item. Use takeUntil().'>([94](#notes-94))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='At most one item. Use takeUntil().'>([94](#notes-94))</sup>| +<a name='skipWhile'></a>`skipWhile`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='At most one item. Use filter().'>([95](#notes-95))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='At most one item. Use filter().'>([95](#notes-95))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Always empty.'>([2](#notes-2))</sup>| +<a name='sorted'></a>`sorted`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='At most one item.'>([78](#notes-78))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='At most one item.'>([78](#notes-78))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='At most one item.'>([78](#notes-78))</sup>| +<a name='startWith'></a>`startWith`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)| +<a name='startWithArray'></a>`startWithArray`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Use startWith() of Flowable or Observable.'>([96](#notes-96))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Use startWith() of Flowable or Observable.'>([96](#notes-96))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Use startWith() of Flowable or Observable.'>([96](#notes-96))</sup>| +<a name='startWithItem'></a>`startWithItem`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Use startWith() of another reactive type.'>([97](#notes-97))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Use startWith() of another reactive type.'>([97](#notes-97))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Use startWith() of another reactive type.'>([97](#notes-97))</sup>| +<a name='startWithIterable'></a>`startWithIterable`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Use startWith() of Flowable or Observable.'>([98](#notes-98))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Use startWith() of Flowable or Observable.'>([98](#notes-98))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Use startWith() of Flowable or Observable.'>([98](#notes-98))</sup>| +<a name='subscribe'></a>`subscribe`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)| +<a name='subscribeOn'></a>`subscribeOn`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)| +<a name='subscribeWith'></a>`subscribeWith`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)| +<a name='switchIfEmpty'></a>`switchIfEmpty`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Never empty.'>([23](#notes-23))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Always empty. Use defaultIfEmpty().'>([99](#notes-99))</sup>| +<a name='switchMap'></a>`switchMap`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='At most one item. Use flatMap().'>([100](#notes-100))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='At most one item. Use flatMap().'>([100](#notes-100))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Always empty thus no items to map.'>([28](#notes-28))</sup>| +<a name='switchMapCompletable'></a>`switchMapCompletable`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='At most one item. Use flatMap().'>([100](#notes-100))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='At most one item. Use flatMap().'>([100](#notes-100))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Always empty thus no items to map.'>([28](#notes-28))</sup>| +<a name='switchMapCompletableDelayError'></a>`switchMapCompletableDelayError`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='At most one item. Use flatMap().'>([100](#notes-100))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='At most one item. Use flatMap().'>([100](#notes-100))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Always empty thus no items to map.'>([28](#notes-28))</sup>| +<a name='switchMapDelayError'></a>`switchMapDelayError`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='At most one item. Use flatMap().'>([100](#notes-100))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='At most one item. Use flatMap().'>([100](#notes-100))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Always empty thus no items to map.'>([28](#notes-28))</sup>| +<a name='switchMapMaybe'></a>`switchMapMaybe`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='At most one item. Use flatMap().'>([100](#notes-100))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='At most one item. Use flatMap().'>([100](#notes-100))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Always empty thus no items to map.'>([28](#notes-28))</sup>| +<a name='switchMapMaybeDelayError'></a>`switchMapMaybeDelayError`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='At most one item. Use flatMap().'>([100](#notes-100))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='At most one item. Use flatMap().'>([100](#notes-100))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Always empty thus no items to map.'>([28](#notes-28))</sup>| +<a name='switchMapSingle'></a>`switchMapSingle`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='At most one item. Use flatMap().'>([100](#notes-100))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='At most one item. Use flatMap().'>([100](#notes-100))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Always empty thus no items to map.'>([28](#notes-28))</sup>| +<a name='switchMapSingleDelayError'></a>`switchMapSingleDelayError`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='At most one item. Use flatMap().'>([100](#notes-100))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='At most one item. Use flatMap().'>([100](#notes-100))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Always empty thus no items to map.'>([28](#notes-28))</sup>| +<a name='switchOnNext'></a>`switchOnNext`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)| +<a name='switchOnNextDelayError'></a>`switchOnNextDelayError`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)| +<a name='take'></a>`take`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='At most one item, would be no-op.'>([60](#notes-60))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='At most one item, would be no-op.'>([60](#notes-60))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='At most one item, would be no-op.'>([60](#notes-60))</sup>| +<a name='takeLast'></a>`takeLast`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='At most one item, would be no-op.'>([60](#notes-60))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='At most one item, would be no-op.'>([60](#notes-60))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='At most one item, would be no-op.'>([60](#notes-60))</sup>| +<a name='takeUntil'></a>`takeUntil`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)| +<a name='takeWhile'></a>`takeWhile`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='At most one item. Use filter().'>([95](#notes-95))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='At most one item. Use filter().'>([95](#notes-95))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Always empty.'>([2](#notes-2))</sup>| +<a name='test'></a>`test`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)| +<a name='throttleFirst'></a>`throttleFirst`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='At most one item signaled so no subsequent items to work with.'>([40](#notes-40))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='At most one item signaled so no subsequent items to work with.'>([40](#notes-40))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Always empty thus no items to work with.'>([41](#notes-41))</sup>| +<a name='throttleLast'></a>`throttleLast`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='At most one item signaled so no subsequent items to work with.'>([40](#notes-40))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='At most one item signaled so no subsequent items to work with.'>([40](#notes-40))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Always empty thus no items to work with.'>([41](#notes-41))</sup>| +<a name='throttleLatest'></a>`throttleLatest`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='At most one item signaled so no subsequent items to work with.'>([40](#notes-40))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='At most one item signaled so no subsequent items to work with.'>([40](#notes-40))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Always empty thus no items to work with.'>([41](#notes-41))</sup>| +<a name='throttleWithTimeout'></a>`throttleWithTimeout`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='At most one item signaled so no subsequent items to work with.'>([40](#notes-40))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='At most one item signaled so no subsequent items to work with.'>([40](#notes-40))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Always empty thus no items to work with.'>([41](#notes-41))</sup>| +<a name='timeInterval'></a>`timeInterval`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Always empty thus no items to work with.'>([41](#notes-41))</sup>| +<a name='timeout'></a>`timeout`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)| +<a name='timer'></a>`timer`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)| +<a name='timestamp'></a>`timestamp`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Always empty thus no items to work with.'>([41](#notes-41))</sup>| +<a name='to'></a>`to`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)| +<a name='toCompletionStage'></a>`toCompletionStage`|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Use firstStage.'>([101](#notes-101))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Use firstStage.'>([101](#notes-101))</sup>|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)| +<a name='toFlowable'></a>`toFlowable`|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Would be no-op.'>([102](#notes-102))</sup>|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)| +<a name='toFuture'></a>`toFuture`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)| +<a name='toList'></a>`toList`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='At most one element to collect. Use map() to transform into a list/collection.'>([13](#notes-13))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='One element to collect. Use map() to transform into a list/collection.'>([14](#notes-14))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Always empty. Use andThen() to bring in a collection.'>([15](#notes-15))</sup>| +<a name='toMap'></a>`toMap`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='At most one element to collect. Use map() to transform into a list/collection.'>([13](#notes-13))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='One element to collect. Use map() to transform into a list/collection.'>([14](#notes-14))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Always empty. Use andThen() to bring in a collection.'>([15](#notes-15))</sup>| +<a name='toMaybe'></a>`toMaybe`|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Use firstElement.'>([103](#notes-103))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Use firstElement.'>([103](#notes-103))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Would be no-op.'>([102](#notes-102))</sup>|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)| +<a name='toMultimap'></a>`toMultimap`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='At most one element to collect. Use map() to transform into a list/collection.'>([13](#notes-13))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='One element to collect. Use map() to transform into a list/collection.'>([14](#notes-14))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Always empty. Use andThen() to bring in a collection.'>([15](#notes-15))</sup>| +<a name='toObservable'></a>`toObservable`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Would be no-op.'>([102](#notes-102))</sup>|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)| +<a name='toSingle'></a>`toSingle`|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Use firstOrError.'>([104](#notes-104))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Use firstOrError.'>([104](#notes-104))</sup>|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Would be no-op.'>([102](#notes-102))</sup>|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)| +<a name='toSingleDefault'></a>`toSingleDefault`|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Use first.'>([105](#notes-105))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Use first.'>([105](#notes-105))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Use defaultIfEmpty().'>([106](#notes-106))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Would be no-op.'>([102](#notes-102))</sup>|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)| +<a name='toSortedList'></a>`toSortedList`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='At most one element to collect. Use map() to transform into a list/collection.'>([13](#notes-13))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='One element to collect. Use map() to transform into a list/collection.'>([14](#notes-14))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Always empty. Use andThen() to bring in a collection.'>([15](#notes-15))</sup>| +<a name='unsafeCreate'></a>`unsafeCreate`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)| +<a name='unsubscribeOn'></a>`unsubscribeOn`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)| +<a name='using'></a>`using`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)| +<a name='window'></a>`window`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Use map() to transform into a nested source.'>([107](#notes-107))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Use map() to transform into a nested source.'>([108](#notes-108))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Always empty. Use andThen() to bring in a nested source.'>([109](#notes-109))</sup>| +<a name='withLatestFrom'></a>`withLatestFrom`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='At most one element per source. Use zip().'>([16](#notes-16))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='At most one element per source. Use zip().'>([16](#notes-16))</sup>|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Always empty. Use merge().'>([17](#notes-17))</sup>| +<a name='wrap'></a>`wrap`|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Use fromPublisher().'>([110](#notes-110))</sup>|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)| +<a name='zip'></a>`zip`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Use merge().'>([111](#notes-111))</sup>| +<a name='zipArray'></a>`zipArray`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Use mergeArray().'>([112](#notes-112))</sup>| +<a name='zipWith'></a>`zipWith`|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)|![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) <sup title='Use mergeWith().'>([113](#notes-113))</sup>| +<a name='total'></a>**237 operators** | **216** | **210** | **118** | **108** | **84** | + +#### Notes +<a name='notes-1'></a><sup>1</sup> Use [`contains()`](#contains).<br/> +<a name='notes-2'></a><sup>2</sup> Always empty.<br/> +<a name='notes-3'></a><sup>3</sup> Use [`concatWith`](#concatWith).<br/> +<a name='notes-4'></a><sup>4</sup> Use [`blockingFirst()`](#blockingFirst), [`blockingSingle()`](#blockingSingle) or [`blockingLast()`](#blockingLast).<br/> +<a name='notes-5'></a><sup>5</sup> Use [`blockingGet()`](#blockingGet).<br/> +<a name='notes-6'></a><sup>6</sup> At most one element to get. Use [`blockingGet()`](#blockingGet).<br/> +<a name='notes-7'></a><sup>7</sup> No elements to get. Use [`blockingAwait()`](#blockingAwait).<br/> +<a name='notes-8'></a><sup>8</sup> Use [`blockingSubscribe()`](#blockingSubscribe)<br/> +<a name='notes-9'></a><sup>9</sup> Use [`map()`](#map) and [`switchIfEmpty()`](#switchIfEmpty) to transform into a list/collection.<br/> +<a name='notes-10'></a><sup>10</sup> Use [`map()`](#map) to transform into a list/collection.<br/> +<a name='notes-11'></a><sup>11</sup> Always empty. Use [`andThen()`](#andThen) to bring in a list/collection.<br/> +<a name='notes-12'></a><sup>12</sup> At most one element to store. Use [`cache()`](#cache).<br/> +<a name='notes-13'></a><sup>13</sup> At most one element to collect. Use [`map()`](#map) and [`switchIfEmpty()`](#switchIfEmpty) to transform into a list/collection.<br/> +<a name='notes-14'></a><sup>14</sup> One element to collect. Use [`map()`](#map) to transform into a list/collection.<br/> +<a name='notes-15'></a><sup>15</sup> Always empty. Use [`andThen()`](#andThen) to bring in a collection.<br/> +<a name='notes-16'></a><sup>16</sup> At most one element per source. Use [`zip()`](#zip).<br/> +<a name='notes-17'></a><sup>17</sup> Always empty. Use [`merge()`](#merge).<br/> +<a name='notes-18'></a><sup>18</sup> At most one element per source. Use [`zipArray()`](#zipArray).<br/> +<a name='notes-19'></a><sup>19</sup> Always empty. Use [`mergeArray()`](#mergeArray).<br/> +<a name='notes-20'></a><sup>20</sup> Always empty. Use [`mergeArrayDelayError()`](#mergeArrayDelayError).<br/> +<a name='notes-21'></a><sup>21</sup> Always empty. Use [`mergeDelayError()`](#mergeDelayError).<br/> +<a name='notes-22'></a><sup>22</sup> Use [`empty()`](#empty).<br/> +<a name='notes-23'></a><sup>23</sup> Never empty.<br/> +<a name='notes-24'></a><sup>24</sup> No items to keep ordered. Use [`mergeArray()`](#mergeArray).<br/> +<a name='notes-25'></a><sup>25</sup> No items to keep ordered. Use [`mergeArrayDelayError()`](#mergeArrayDelayError).<br/> +<a name='notes-26'></a><sup>26</sup> No items to keep ordered. Use [`merge()`](#merge).<br/> +<a name='notes-27'></a><sup>27</sup> No items to keep ordered. Use [`mergeDelayError()`](#mergeDelayError).<br/> +<a name='notes-28'></a><sup>28</sup> Always empty thus no items to map.<br/> +<a name='notes-29'></a><sup>29</sup> Either the upstream fails (thus no inner) or the mapped-in source, but never both. Use [`concatMapCompletable`](#concatMapCompletable).<br/> +<a name='notes-30'></a><sup>30</sup> Either the upstream fails (thus no inner) or the mapped-in source, but never both. Use [`concatMap`](#concatMap).<br/> +<a name='notes-31'></a><sup>31</sup> At most one item to map. Use [`concatMap()`](#concatMap).<br/> +<a name='notes-32'></a><sup>32</sup> At most one item. Use [`flattenAsFlowable`](#flattenAsFlowable) or [`flattenAsObservable`](#flattenAsObservable).<br/> +<a name='notes-33'></a><sup>33</sup> Use [`concatMap`](#concatMap).<br/> +<a name='notes-34'></a><sup>34</sup> Either the upstream fails (thus no inner) or the mapped-in source, but never both. Use [`concatMapMaybe`](#concatMapMaybe).<br/> +<a name='notes-35'></a><sup>35</sup> Use [`concatMap()`](#concatMap).<br/> +<a name='notes-36'></a><sup>36</sup> Either the upstream fails (thus no inner) or the mapped-in source, but never both. Use [`concatMapSingle`](#concatMapSingle).<br/> +<a name='notes-37'></a><sup>37</sup> At most one item. Use [`flattenStreamAsFlowable`](#flattenStreamAsFlowable) or [`flattenStreamAsObservable`](#flattenStreamAsObservable).<br/> +<a name='notes-38'></a><sup>38</sup> Never empty thus always 1.<br/> +<a name='notes-39'></a><sup>39</sup> Always empty thus always 0.<br/> +<a name='notes-40'></a><sup>40</sup> At most one item signaled so no subsequent items to work with.<br/> +<a name='notes-41'></a><sup>41</sup> Always empty thus no items to work with.<br/> +<a name='notes-42'></a><sup>42</sup> Always empty. Use [`andThen()`](#andThen) to chose the follow-up sequence.<br/> +<a name='notes-43'></a><sup>43</sup> At most one item, always distinct.<br/> +<a name='notes-44'></a><sup>44</sup> Different terminology. Use [`doAfterSuccess()`](#doAfterSuccess).<br/> +<a name='notes-45'></a><sup>45</sup> Different terminology. Use [`doAfterNext()`](#doAfterNext).<br/> +<a name='notes-46'></a><sup>46</sup> Different terminology. Use [`doOnDispose()`](#doOnDispose).<br/> +<a name='notes-47'></a><sup>47</sup> Always succeeds or fails, there is no `onComplete` signal.<br/> +<a name='notes-48'></a><sup>48</sup> Different terminology. Use [`doOnCancel()`](#doOnCancel).<br/> +<a name='notes-49'></a><sup>49</sup> At most one item. Use [`doOnEvent()`](#doOnEvent).<br/> +<a name='notes-50'></a><sup>50</sup> Use [`doOnEach()`](#doOnEach).<br/> +<a name='notes-51'></a><sup>51</sup> Different terminology. Use [`doOnSuccess()`](#doOnSuccess).<br/> +<a name='notes-52'></a><sup>52</sup> Backpressure related and not supported outside `Flowable`.<br/> +<a name='notes-53'></a><sup>53</sup> Different terminology. Use [`doOnNext()`](#doOnNext).<br/> +<a name='notes-54'></a><sup>54</sup> At most one item with index 0. Use [`defaultIfEmpty`](#defaultIfEmpty).<br/> +<a name='notes-55'></a><sup>55</sup> Always one item with index 0.<br/> +<a name='notes-56'></a><sup>56</sup> At most one item with index 0. Use [`toSingle`](#toSingle).<br/> +<a name='notes-57'></a><sup>57</sup> Use [`complete()`](#complete).<br/> +<a name='notes-58'></a><sup>58</sup> At most one item. Use [`defaultIfEmpty`](#defaultIfEmpty).<br/> +<a name='notes-59'></a><sup>59</sup> Always one item.<br/> +<a name='notes-60'></a><sup>60</sup> At most one item, would be no-op.<br/> +<a name='notes-61'></a><sup>61</sup> Always one item, would be no-op.<br/> +<a name='notes-62'></a><sup>62</sup> Always empty. Use [`andThen()`](#andThen) and [`error()`](#error).<br/> +<a name='notes-63'></a><sup>63</sup> At most one item. Use [`toCompletionStage()`](#toCompletionStage).<br/> +<a name='notes-64'></a><sup>64</sup> Always empty. Use [`andThen()`](#andThen), [`error()`](#error) and [`toCompletionStage()`](#toCompletionStage).<br/> +<a name='notes-65'></a><sup>65</sup> Use [`flatMap()`](#flatMap).<br/> +<a name='notes-66'></a><sup>66</sup> Not supported. Use [`flatMap`](#flatMap) and [`toFlowable()`](#toFlowable).<br/> +<a name='notes-67'></a><sup>67</sup> Use [`flatMap`](#flatMap).<br/> +<a name='notes-68'></a><sup>68</sup> Not supported. Use [`flatMap`](#flatMap) and [`toObservable()`](#toFlowable).<br/> +<a name='notes-69'></a><sup>69</sup> Use [`flatMapIterable()`](#flatMapIterable).<br/> +<a name='notes-70'></a><sup>70</sup> Use [`flatMapStream()`](#flatMapStream).<br/> +<a name='notes-71'></a><sup>71</sup> Use [`subscribe()`](#subscribe).<br/> +<a name='notes-72'></a><sup>72</sup> At most one item. Use [`just()`](#just) or [`empty()`](#empty).<br/> +<a name='notes-73'></a><sup>73</sup> Always one item. Use [`just()`](#just).<br/> +<a name='notes-74'></a><sup>74</sup> Always empty. Use [`complete()`](#complete).<br/> +<a name='notes-75'></a><sup>75</sup> Always error.<br/> +<a name='notes-76'></a><sup>76</sup> Use [`wrap()`](#wrap).<br/> +<a name='notes-77'></a><sup>77</sup> Use [`fromSupplier()`](#fromSupplier).<br/> +<a name='notes-78'></a><sup>78</sup> At most one item.<br/> +<a name='notes-79'></a><sup>79</sup> Always empty thus no items to group.<br/> +<a name='notes-80'></a><sup>80</sup> Always empty thus no items to join.<br/> +<a name='notes-81'></a><sup>81</sup> Use [`ignoreElements()`](#ignoreElements).<br/> +<a name='notes-82'></a><sup>82</sup> Use [`ignoreElement()`](#ignoreElement).<br/> +<a name='notes-83'></a><sup>83</sup> At most one item. Use [`timer()`](#timer).<br/> +<a name='notes-84'></a><sup>84</sup> At most one item. Use [`zip()`](#zip)<br/> +<a name='notes-85'></a><sup>85</sup> Always empty thus no items to filter.<br/> +<a name='notes-86'></a><sup>86</sup> Needs backpressure thus not supported outside `Flowable`.<br/> +<a name='notes-87'></a><sup>87</sup> Connectable sources not supported outside `Flowable` and `Observable`. Use a `MaybeSubject`.<br/> +<a name='notes-88'></a><sup>88</sup> Connectable sources not supported outside `Flowable` and `Observable`. Use a `SingleSubject`.<br/> +<a name='notes-89'></a><sup>89</sup> Connectable sources not supported outside `Flowable` and `Observable`. Use a `ConnectableSubject`.<br/> +<a name='notes-90'></a><sup>90</sup> At most one item. Use [`just()`](#just).<br/> +<a name='notes-91'></a><sup>91</sup> At most one item. Use [`map()`](#map).<br/> +<a name='notes-92'></a><sup>92</sup> Always empty thus no items to reduce.<br/> +<a name='notes-93'></a><sup>93</sup> At most one signal type.<br/> +<a name='notes-94'></a><sup>94</sup> At most one item. Use [`takeUntil()`](#takeUntil).<br/> +<a name='notes-95'></a><sup>95</sup> At most one item. Use [`filter()`](#filter).<br/> +<a name='notes-96'></a><sup>96</sup> Use [`startWith()`](#startWith) and [`fromArray()`](#fromArray) of `Flowable` or `Observable`.<br/> +<a name='notes-97'></a><sup>97</sup> Use [`startWith()`](#startWith) and [`just()`](#just) of another reactive type.<br/> +<a name='notes-98'></a><sup>98</sup> Use [`startWith()`](#startWith) and [`fromIterable()`](#fromArray) of `Flowable` or `Observable`.<br/> +<a name='notes-99'></a><sup>99</sup> Always empty. Use [`defaultIfEmpty()`](#defaultIfEmpty).<br/> +<a name='notes-100'></a><sup>100</sup> At most one item. Use [`flatMap()`](#flatMap).<br/> +<a name='notes-101'></a><sup>101</sup> Use [`firstStage`](#firstStage), [`lastStage`](#lastStage) or [`singleStage`](#singleStage).<br/> +<a name='notes-102'></a><sup>102</sup> Would be no-op.<br/> +<a name='notes-103'></a><sup>103</sup> Use [`firstElement`](#firstElement), [`lastElement`](#lastElement) or [`singleElement`](#singleElement).<br/> +<a name='notes-104'></a><sup>104</sup> Use [`firstOrError`](#firstOrError), [`lastOrError`](#lastOrError) or [`singleOrError`](#singleOrError).<br/> +<a name='notes-105'></a><sup>105</sup> Use [`first`](#first), [`last`](#last) or [`single`](#single).<br/> +<a name='notes-106'></a><sup>106</sup> Use [`defaultIfEmpty()`](#defaultIfEmpty).<br/> +<a name='notes-107'></a><sup>107</sup> Use [`map()`](#map) and [`switchIfEmpty()`](#switchIfEmpty) to transform into a nested source.<br/> +<a name='notes-108'></a><sup>108</sup> Use [`map()`](#map) to transform into a nested source.<br/> +<a name='notes-109'></a><sup>109</sup> Always empty. Use [`andThen()`](#andThen) to bring in a nested source.<br/> +<a name='notes-110'></a><sup>110</sup> Use [`fromPublisher()`](#fromPublisher).<br/> +<a name='notes-111'></a><sup>111</sup> Use [`merge()`](#merge).<br/> +<a name='notes-112'></a><sup>112</sup> Use [`mergeArray()`](#mergeArray).<br/> +<a name='notes-113'></a><sup>113</sup> Use [`mergeWith()`](#mergeWith).<br/> + +#### Under development + +*Currently, all intended operators are implemented.* diff --git a/docs/Parallel-flows.md b/docs/Parallel-flows.md new file mode 100644 index 0000000000..8cd0e9a8c9 --- /dev/null +++ b/docs/Parallel-flows.md @@ -0,0 +1,33 @@ +# Introduction + +Version 2.0.5 introduced the `ParallelFlowable` API that allows parallel execution of a few select operators such as `map`, `filter`, `concatMap`, `flatMap`, `collect`, `reduce` and so on. Note that is a **parallel mode** for `Flowable` (a sub-domain specific language) instead of a new reactive base type. + +Consequently, several typical operators such as `take`, `skip` and many others are not available and there is no `ParallelObservable` because **backpressure** is essential in not flooding the internal queues of the parallel operators as by expectation, we want to go parallel because the processing of the data is slow on one thread. + +The easiest way of entering the parallel world is by using `Flowable.parallel`: + +```java +ParallelFlowable<Integer> source = Flowable.range(1, 1000).parallel(); +``` + +By default, the parallelism level is set to the number of available CPUs (`Runtime.getRuntime().availableProcessors()`) and the prefetch amount from the sequential source is set to `Flowable.bufferSize()` (128). Both can be specified via overloads of `parallel()`. + +`ParallelFlowable` follows the same principles of parametric asynchrony as `Flowable` does, therefore, `parallel()` on itself doesn't introduce the asynchronous consumption of the sequential source but only prepares the parallel flow; the asynchrony is defined via the `runOn(Scheduler)` operator. + +```java +ParallelFlowable<Integer> psource = source.runOn(Schedulers.io()); +``` + +The parallelism level (`ParallelFlowable.parallelism()`) doesn't have to match the parallelism level of the `Scheduler`. The `runOn` operator will use as many `Scheduler.Worker` instances as defined by the parallelized source. This allows `ParallelFlowable` to work for CPU intensive tasks via `Schedulers.computation()`, blocking/IO bound tasks through `Schedulers.io()` and unit testing via `TestScheduler`. You can specify the prefetch amount on `runOn` as well. + +Once the necessary parallel operations have been applied, you can return to the sequential `Flowable` via the `ParallelFlowable.sequential()` operator. + +```java +Flowable<Integer> result = psource.filter(v -> v % 3 == 0).map(v -> v * v).sequential(); +``` + +Note that `sequential` doesn't guarantee any ordering between values flowing through the parallel operators. + +# Parallel operators + +TBD \ No newline at end of file diff --git a/docs/Phantom-Operators.md b/docs/Phantom-Operators.md new file mode 100644 index 0000000000..5193147a22 --- /dev/null +++ b/docs/Phantom-Operators.md @@ -0,0 +1,166 @@ +These operators have been proposed but are not part of the 1.0 release of RxJava. + +* [**`chunkify( )`**](Phantom-Operators#chunkify) — returns an iterable that periodically returns a list of items emitted by the source Observable since the last list +* [**`fromFuture( )`**](Phantom-Operators#fromfuture) — convert a Future into an Observable, but do not attempt to get the Future's value until a Subscriber subscribes +* [**`forEachFuture( )`**](Phantom-Operators#foreachfuture) — create a futureTask that will invoke a specified function on each item emitted by an Observable +* [**`forIterable( )`**](Phantom-Operators#foriterable) — apply a function to the elements of an Iterable to create Observables which are then concatenated +* [**`fromCancellableFuture( )`, `startCancellableFuture( )`, and `deferCancellableFuture( )`**](Phantom-Operators#fromcancellablefuture-startcancellablefuture-and-defercancellablefuture-) — versions of Future-to-Observable converters that monitor the subscription status of the Observable to determine whether to halt work on the Future +* [**`generate( )` and `generateAbsoluteTime( )`**](Phantom-Operators#generate-and-generateabsolutetime) — create an Observable that emits a sequence of items as generated by a function of your choosing +* [**`groupByUntil( )`**](Phantom-Operators#groupbyuntil) — a variant of the `groupBy` operator that closes any open `GroupedObservable` upon a signal from another Observable +* [**`multicast( )`**](Phantom-Operators#multicast) — represents an Observable as a Connectable Observable +* [**`onErrorFlatMap( )`**](Phantom-Operators#onerrorflatmap) — instructs an Observable to emit a sequence of items whenever it encounters an error +* [**`parallel( )`**](Phantom-Operators#parallel) — split the work done on the emissions from an Observable into multiple Observables each operating on its own parallel thread +* [**`parallelMerge( )`**](Phantom-Operators#parallelmerge) — combine multiple Observables into a smaller number of Observables, to facilitate parallelism +* [**`pivot( )`**](Phantom-Operators#pivot) — combine multiple sets of grouped observables so that they are arranged primarily by group rather than by set +* [**`publishLast( )`**](Phantom-Operators#publishlast) — represent an Observable as a Connectable Observable that emits only the last item emitted by the source Observable + + +*** + +## chunkify( ) +#### returns an iterable that periodically returns a list of items emitted by the source Observable since the last list +<img src="http://github.com/Netflix/RxJava/wiki/images/rx-operators/B.chunkify.v3.png" width="640" height="490" /> + +The `chunkify( )` operator represents a blocking observable as an Iterable, that, each time you iterate over it, returns a list of items emitted by the source Observable since the previous iteration. These lists may be empty if there have been no such items emitted. + +*** + +## fromFuture( ) +#### convert a Future into an Observable, but do not attempt to get the Future's value until a Subscriber subscribes +<img src="http://github.com/Netflix/RxJava/wiki/images/rx-operators/fromFuture.v3.png" width="640" height="335" /> + +The `fromFuture( )` method also converts a Future into an Observable, but it obtains this Future indirectly, by means of a function you provide. It creates the Observable immediately, but waits to call the function and to obtain the Future until a Subscriber subscribes to it. + +*** + +## forEachFuture( ) +#### create a futureTask that will invoke a specified function on each item emitted by an Observable +<img src="http://github.com/Netflix/RxJava/wiki/images/rx-operators/B.forEachFuture.v3.png" width="640" height="375" /> + +The `forEachFuture( )` returns a `FutureTask` for each item emitted by the source Observable (or each item and each notification) that, when executed, will apply a function you specify to each such item (or item and notification). + +*** + +## forIterable( ) +#### apply a function to the elements of an Iterable to create Observables which are then concatenated +<img src="http://github.com/Netflix/RxJava/wiki/images/rx-operators/forIterable.v3.png" width="640" height="310" /> + +`forIterable( )` is similar to `from(Iterable )` but instead of the resulting Observable emitting the elements of the Iterable as its own emitted items, it applies a specified function to each of these elements to generate one Observable per element, and then concatenates the emissions of these Observables to be its own sequence of emitted items. + +*** + +## fromCancellableFuture( ), startCancellableFuture( ), and deferCancellableFuture( ) +#### versions of Future-to-Observable converters that monitor the subscription status of the Observable to determine whether to halt work on the Future + +If the a subscriber to the Observable that results when a Future is converted to an Observable later unsubscribes from that Observable, it can be useful to have the ability to stop attempting to retrieve items from the Future. The "cancellable" Future enables you do do this. These three methods will return Observables that, when unsubscribed to, will also "unsubscribe" from the underlying Futures. + +*** + +## generate( ) and generateAbsoluteTime( ) +#### create an Observable that emits a sequence of items as generated by a function of your choosing +<img src="http://github.com/Netflix/RxJava/wiki/images/rx-operators/generate.v3.png" width="640" height="315" /> + +The basic form of `generate( )` takes four parameters. These are `initialState` and three functions: `iterate( )`, `condition( )`, and `resultSelector( )`. `generate( )` uses these four parameters to generate an Observable sequence, which is its return value. It does so in the following way. + +`generate( )` creates each emission from the sequence by applying the `resultSelector( )` function to the current _state_ and emitting the resulting item. The first state, which determines the first emitted item, is `initialState`. `generate( )` determines each subsequent state by applying `iterate( )` to the current state. Before emitting an item, `generate( )` tests the result of `condition( )` applied to the current state. If the result of this test is `false`, instead of calling `resultSelector( )` and emitting the resulting value, `generate( )` terminates the sequence and stops iterating the state. + +There are also versions of `generate( )` that allow you to do the work of generating the sequence on a particular `Scheduler` and that allow you to set the time interval between emissions by applying a function to the current state. The `generateAbsoluteTime( )` allows you to control the time at which an item is emitted by applying a function to the state to get an absolute system clock time (rather than an interval from the previous emission). + +<img src="http://github.com/Netflix/RxJava/wiki/images/rx-operators/generateAbsoluteTime.v3.png" width="640" height="330" /> + +#### see also: +* <a href="http://www.introtorx.com/Content/v1.0.10621.0/04_CreatingObservableSequences.html#ObservableGenerate">Introduction to Rx: Generate</a> +* Linq: <a href="http://msdn.microsoft.com/en-us/library/system.reactive.linq.observable.generate.aspx">`Generate`</a> +* RxJS: <a href="https://github.com/Reactive-Extensions/RxJS/blob/master/doc/api/core/observable.md#rxobservablegenerateinitialstate-condition-iterate-resultselector-scheduler">`generate`</a>, <a href="https://github.com/Reactive-Extensions/RxJS/blob/master/doc/api/core/observable.md#rxobservablegeneratewithabsolutetimeinitialstate-condition-iterate-resultselector-timeselector-scheduler">`generateWithAbsoluteTime`</a>, and <a href="https://github.com/Reactive-Extensions/RxJS/blob/master/doc/api/core/observable.md#rxobservablegeneratewithrelativetimeinitialstate-condition-iterate-resultselector-timeselector-scheduler">`generateWithRelativeTime`</a> + +*** +## groupByUntil( ) +#### a variant of the `groupBy` operator that closes any open `GroupedObservable` upon a signal from another Observable + +This version of `groupBy` adds another parameter: an Observable that emits duration markers. When a duration marker is emitted by this Observable, any grouped Observables that have been opened are closed, and `groupByUntil( )` will create new grouped Observables for any subsequent emissions by the source Observable. + +<img src="http://github.com/ReactiveX/RxJava/wiki/images/rx-operators/groupByUntil.v3.png" width="640" height="375" />​ + +Another variety of `groupByUntil( )` limits the number of groups that can be active at any particular time. If an item is emitted by the source Observable that would cause the number of groups to exceed this maximum, before the new group is emitted, one of the existing groups is closed (that is, the Observable it represents terminates by calling its Subscribers' `onCompleted` methods and then expires). + +*** + +## multicast( ) +#### represents an Observable as a Connectable Observable +To represent an Observable as a Connectable Observable, use the `multicast( )` method. + +#### see also: +* javadoc: <a href="http://reactivex.io/RxJava/javadoc/rx/Observable.html#multicast(rx.functions.Func0)">`multicast(subjectFactory)`</a> +* javadoc: <a href="http://reactivex.io/RxJava/javadoc/rx/Observable.html#multicast(rx.functions.Func0,%20rx.functions.Func1)">`multicast(subjectFactory, selector)`</a> +* RxJS: <a href="https://github.com/Reactive-Extensions/RxJS/blob/master/doc/api/core/observable.md#rxobservableprototypemulticastsubject--subjectselector-selector">`multicast`</a> +* Linq: <a href="http://msdn.microsoft.com/en-us/library/system.reactive.linq.observable.multicast.aspx">`Multicast`</a> +* <a href="http://www.introtorx.com/Content/v1.0.10621.0/14_HotAndColdObservables.html#PublishAndConnect">Introduction to Rx: Publish and Connect</a> +* <a href="http://www.introtorx.com/Content/v1.0.10621.0/14_HotAndColdObservables.html#Multicast">Introduction to Rx: Multicast</a> + +*** + +## onErrorFlatMap( ) +#### instructs an Observable to emit a sequence of items whenever it encounters an error +<img src="http://github.com/Netflix/RxJava/wiki/images/rx-operators/onErrorFlatMap.v3.png" width="640" height="310" />​ + +The `onErrorFlatMap( )` method is similar to `onErrorResumeNext( )` except that it does not assume the source Observable will correctly terminate when it issues an error. Because of this, after emitting its backup sequence of items, `onErrorFlatMap( )` relinquishes control of the emitted sequence back to the source Observable. If that Observable again issues an error, `onErrorFlatMap( )` will again emit its backup sequence. + +The backup sequence is an Observable that is returned from a function that you pass to `onErrorFlatMap( )`. This function takes the Throwable issued by the source Observable as its argument, and so you can customize the sequence based on the nature of the Throwable. + +Because `onErrorFlatMap( )` is designed to work with pathological source Observables that do not terminate after issuing an error, it is mostly useful in debugging/testing scenarios. + +Note that you should apply `onErrorFlatMap( )` directly to the pathological source Observable, and not to that Observable after it has been modified by additional operators, as such operators may effectively renormalize the source Observable by unsubscribing from it immediately after it issues an error. Below, for example, is an illustration showing how `onErrorFlatMap( )` will respond to two error-generating Observables that have been merged by the `merge( )` operator. Note that it will *not* react to both errors generated by both Observables, but only to the single error passed along by `merge( )`: + +<img src="http://github.com/Netflix/RxJava/wiki/images/rx-operators/onErrorFlatMap.withMerge.v3.png" width="640" height="630" />​ + +*** + +## parallel( ) +#### split the work done on the emissions from an Observable into multiple Observables each operating on its own parallel thread +<img src="http://github.com/ReactiveX/RxJava/wiki/images/rx-operators/parallel.v3.png" width="640" height="475" />​ + +The `parallel( )` method splits an Observable into as many Observables as there are available processors, and does work in parallel on each of these Observables. `parallel( )` then merges the results of these parallel computations back into a single, well-behaved Observable sequence. + +For the simple “run things in parallel” use case, you can instead use something like this: +```java +streamOfItems.flatMap(item -> { + itemToObservable(item).subscribeOn(Schedulers.io()); +}); +``` +Kick off your work for each item inside [`flatMap`](Transforming-Observables#flatmap-concatmap-and-flatmapiterable) using [`subscribeOn`](Observable-Utility-Operators#subscribeon) to make it asynchronous, or by using a function that already makes asynchronous calls. + +#### see also: +* <a href="http://www.grahamlea.com/2014/07/rxjava-threading-examples/">RxJava Threading Examples</a> by Graham Lea + +*** + +## parallelMerge( ) +#### combine multiple Observables into a smaller number of Observables, to facilitate parallelism +<img src="http://github.com/ReactiveX/RxJava/wiki/images/rx-operators/parallelMerge.v3.png" width="640" height="535" />​ + +Use the `parallelMerge( )` method to take an Observable that emits a large number of Observables and to reduce it to an Observable that emits a particular, smaller number of Observables that emit the same set of items as the original larger set of Observables: for instance a number of Observables that matches the number of parallel processes that you want to use when processing the emissions from the complete set of Observables. + +*** + +## pivot( ) +#### combine multiple sets of grouped observables so that they are arranged primarily by group rather than by set +<img src="http://github.com/Netflix/RxJava/wiki/images/rx-operators/pivot.v3.png" width="640" height="580" />​ + +If you combine multiple sets of grouped observables, such as those created by [`groupBy( )` and `groupByUntil( )`](Transforming-Observables#wiki-groupby-and-groupbyuntil), then even if those grouped observables have been grouped by a similar differentiation function, the resulting grouping will be primarily based on which set the observable came from, not on which group the observable belonged to. + +An example may make this clearer. Imagine you use `groupBy( )` to group the emissions of an Observable (Observable1) that emits integers into two grouped observables, one emitting the even integers and the other emitting the odd integers. You then repeat this process on a second Observable (Observable2) that emits another set of integers. You hope then to combine the sets of grouped observables emitted by each of these into a single grouped Observable by means of a operator like `from(Observable1, Observable2)`. + +The result will be a grouped observable that emits two groups: the grouped observable resulting from transforming Observable1, and the grouped observable resulting from transforming Observable2. Each of those grouped observables emit observables that in turn emit the odds and evens from the source observables. You can use `pivot( )` to change this around: by applying `pivot( )` to this grouped observable it will transform into one that emits two different groups: the odds group and the evens group, with each of these groups emitting a separate observable corresponding to which source observable its set of integers came from. Here is an illustration: + +<img src="http://github.com/Netflix/RxJava/wiki/images/rx-operators/pivot.ex.v3.png" width="640" height="1140" />​ + +*** + +## publishLast( ) +#### represent an Observable as a Connectable Observable that emits only the last item emitted by the source Observable +<img src="http://github.com/ReactiveX/RxJava/wiki/images/rx-operators/publishLast.v3.png" width="640" height="310" /> + +#### see also: +* RxJS: <a href="https://github.com/Reactive-Extensions/RxJS/blob/master/doc/api/core/observable.md#rxobservableprototypepublishlatestselector">`publishLast`</a> +* Linq: <a href="http://msdn.microsoft.com/en-us/library/system.reactive.linq.observable.publishlast.aspx">`PublishLast`</a> +* <a href="http://www.introtorx.com/Content/v1.0.10621.0/14_HotAndColdObservables.html#PublishLast">Introduction to Rx: PublishLast</a> \ No newline at end of file diff --git a/docs/Plugins.md b/docs/Plugins.md new file mode 100644 index 0000000000..38dfcafd12 --- /dev/null +++ b/docs/Plugins.md @@ -0,0 +1,171 @@ +Plugins allow you to modify the default behavior of RxJava in several respects: + +* by changing the set of default computation, i/o, and new thread Schedulers +* by registering a handler for extraordinary errors that RxJava may encounter +* by registering functions that can take note of the occurrence of several regular RxJava activities + +As of 1.1.7 the regular `RxJavaPlugins` and the other hook classes have been deprecated in favor of `RxJavaHooks`. + +# RxJavaHooks + +The new `RxJavaHooks` allows you to hook into the lifecycle of the `Observable`, `Single` and `Completable` types, the `Scheduler`s returned by `Schedulers` and offers a catch-all for undeliverable errors. + +You can now change these hooks at runtime and there is no need to prepare hooks via system parameters anymore. Since users may still rely on the old hooking system, RxJavaHooks delegates to those old hooks by default. + +The `RxJavaHooks` has setters and getters of the various hook types: + +| Hook | Description | +|------|-------------| +| onError : `Action1<Throwable>` | Sets the catch-all callback | +| onObservableCreate : `Func1<Observable.OnSubscribe, Observable.OnSubscribe>` | Called when operators and sources are instantiated on `Observable` | +| onObservableStart : `Func2<Observable, Observable.OnSubscribe, Observable.OnSubscribe>` | Called before subscribing to an `Observable` actually happens | +| onObservableSubscribeError : `Func1<Throwable, Throwable>` | Called when subscribing to an `Observable` fails | +| onObservableReturn : `Func1<Subscription, Subscription>` | Called when the subscribing to an `Observable` succeeds and before returning the `Subscription` handler for it | +| onObservableLift : `Func1<Observable.Operator, Observable.Operator>` | Called when the operator `lift` is used with `Observable` | +| onSingleCreate : `Func1<Single.OnSubscribe, Single.OnSubscribe>` | Called when operators and sources are instantiated on `Single` | +| onSingleStart : `Func2<Single, Observable.OnSubscribe, Observable.OnSubscribe>` | Called before subscribing to a `Single` actually happens | +| onSingleSubscribeError : `Func1<Throwable, Throwable>` | Called when subscribing to a `Single` fails | +| onSingleReturn : `Func1<Subscription, Subscription>` | Called when the subscribing to a `Single` succeeds and before returning the `Subscription` handler for it | +| onSingleLift : `Func1<Observable.Operator, Observable.Operator>` | Called when the operator `lift` is used (note: `Observable.Operator` is deliberate here) | +| onCompletableCreate : `Func1<Completable.OnSubscribe, Completable.OnSubscribe>` | Called when operators and sources are instantiated on `Completable` | +| onCompletableStart : `Func2<Completable, Completable.OnSubscribe, Completable.OnSubscribe>` | Called before subscribing to a `Completable` actually happens | +| onCompletableSubscribeError : `Func1<Throwable, Throwable>` | Called when subscribing to a `Completable` fails | +| onCompletableLift : `Func1<Completable.Operator, Completable.Operator>` | Called when the operator `lift` is used with `Completable` | +| onComputationScheduler : `Func1<Scheduler, Scheduler>` | Called when using `Schedulers.computation()` | +| onIOScheduler : `Func1<Scheduler, Scheduler>` | Called when using `Schedulers.io()` | +| onNewThreadScheduler : `Func1<Scheduler, Scheduler>` | Called when using `Schedulers.newThread()` | +| onScheduleAction : `Func1<Action0, Action0>` | Called when a task gets scheduled in any of the `Scheduler`s | +| onGenericScheduledExecutorService : `Func0<ScheduledExecutorService>` | that should return single-threaded executors to support background timed tasks of RxJava itself | + +Reading and changing these hooks is thread-safe. + +You can also clear all hooks via `clear()` or reset to the default behavior (of delegating to the old RxJavaPlugins system) via `reset()`. + +Example: + +```java +RxJavaHooks.setOnObservableCreate(o -> { + System.out.println("Creating " + o.getClass()); + return o; +}); +try { + Observable.range(1, 10) + .map(v -> v * 2) + .filter(v -> v % 4 == 0) + .subscribe(System.out::println); +} finally { + RxJavaHooks.reset(); +} +``` + +In addition, the `RxJavaHooks` offers the so-called assembly tracking feature. This shims a custom `Observable`, `Single` and `Completable` into their chains which captures the current stacktrace when those operators were instantiated (assembly-time). Whenever an error is signalled via onError, these middle components attach this assembly-time stacktraces as last causes of that exception. This may help locating the problematic sequence in a codebase where there are too many similar flows and the plain exception itself doesn't tell which one failed in your codebase. + +Example: + +```java +RxJavaHooks.enableAssemblyTracking(); +try { + Observable.empty().single() + .subscribe(System.out::println, Throwable::printStackTrace); +} finally { + RxJavaHooks.resetAssemblyTracking(); +} +``` + +This will print something like this: + +``` +java.lang.NoSuchElementException +at rx.internal.operators.OnSubscribeSingle(OnSubscribeSingle.java:57) +... +Assembly trace: +at com.example.TrackingExample(TrackingExample:10) +``` + +The stacktrace string is also available in a field to support debugging and discovering the status of various operators in a running chain. + +The stacktrace is filtered by removing irrelevant entries such as Thread entry points, unit test runners and the entries of the tracking system itself to reduce noise. + +# RxJavaSchedulersHook + +**Deprecated** + +This plugin allows you to override the default computation, i/o, and new thread Schedulers with Schedulers of your choosing. To do this, extend the class `RxJavaSchedulersHook` and override these methods: + +* `Scheduler getComputationScheduler( )` +* `Scheduler getIOScheduler( )` +* `Scheduler getNewThreadScheduler( )` +* `Action0 onSchedule(action)` + +Then follow these steps: + +1. Create an object of the new `RxJavaDefaultSchedulers` subclass you have implemented. +1. Obtain the global `RxJavaPlugins` instance via `RxJavaPlugins.getInstance( )`. +1. Pass your default schedulers object to the `registerSchedulersHook( )` method of that instance. + +When you do this, RxJava will begin to use the Schedulers returned by your methods rather than its built-in defaults. + +# RxJavaErrorHandler + +**Deprecated** + +This plugin allows you to register a function that will handle errors that are passed to `SafeSubscriber.onError(Throwable)`. (`SafeSubscriber` is used for wrapping the incoming `Subscriber` when one calls `subscribe()`). To do this, extend the class `RxJavaErrorHandler` and override this method: + +* `void handleError(Throwable e)` + +Then follow these steps: + +1. Create an object of the new `RxJavaErrorHandler` subclass you have implemented. +1. Obtain the global `RxJavaPlugins` instance via `RxJavaPlugins.getInstance( )`. +1. Pass your error handler object to the `registerErrorHandler( )` method of that instance. + +When you do this, RxJava will begin to use your error handler to field errors that are passed to `SafeSubscriber.onError(Throwable)`. + +For example, this will call the hook: + +```java +RxJavaPlugins.getInstance().reset(); + +RxJavaPlugins.getInstance().registerErrorHandler(new RxJavaErrorHandler() { + @Override + public void handleError(Throwable e) { + e.printStackTrace(); + } +}); + +Observable.error(new IOException()) +.subscribe(System.out::println, e -> { }); +``` + +however, this call and chained operators in general won't trigger it in each stage: + +```java +Observable.error(new IOException()) +.map(v -> "" + v) +.unsafeSubscribe(System.out::println, e -> { }); +``` + +# RxJavaObservableExecutionHook + +**Deprecated** + +This plugin allows you to register functions that RxJava will call upon certain regular RxJava activities, for instance for logging or metrics-collection purposes. To do this, extend the class `RxJavaObservableExecutionHook` and override any or all of these methods: + +<table><thead> + <tr><th>method</th><th>when invoked</th></tr> + </thead><tbody> + <tr><td><tt>onCreate( )</tt></td><td>during <tt>Observable.create( )</tt></td></tr> + <tr><td><tt>onSubscribeStart( )</tt></td><td>immediately before <tt>Observable.subscribe( )</tt></td></tr> + <tr><td><tt>onSubscribeReturn( )</tt></td><td>immediately after <tt>Observable.subscribe( )</tt></td></tr> + <tr><td><tt>onSubscribeError( )</tt></td><td>upon a failed execution of <tt>Observable.subscribe( )</tt></td></tr> + <tr><td><tt>onLift( )</tt></td><td>during <tt>Observable.lift( )</tt></td></tr> + </tbody> +</table> + +Then follow these steps: + +1. Create an object of the new `RxJavaObservableExecutionHook` subclass you have implemented. +1. Obtain the global `RxJavaPlugins` instance via `RxJavaPlugins.getInstance( )`. +1. Pass your execution hooks object to the `registerObservableExecutionHook( )` method of that instance. + +When you do this, RxJava will begin to call your functions when it encounters the specific conditions they were designed to take note of. \ No newline at end of file diff --git a/docs/Problem-Solving-Examples-in-RxJava.md b/docs/Problem-Solving-Examples-in-RxJava.md new file mode 100644 index 0000000000..6b848ae693 --- /dev/null +++ b/docs/Problem-Solving-Examples-in-RxJava.md @@ -0,0 +1,130 @@ +This page will present some elementary RxJava puzzles and walk through some solutions as a way of introducing you to some of the RxJava operators. + +# Project Euler problem #1 + +There used to be a site called "Project Euler" that presented a series of mathematical computing conundrums (some fairly easy, others quite baffling) and challenged people to solve them. The first one was a sort of warm-up exercise: + +> If we list all the natural numbers below 10 that are multiples of 3 or 5, we get 3, 5, 6 and 9. The sum of these multiples is 23. Find the sum of all the multiples of 3 or 5 below 1000. + +There are several ways we could go about this with RxJava. We might, for instance, begin by going through all of the natural numbers below 1000 with [`range`](Creating-Observables#range) and then [`filter`](Filtering-Observables#filter) out those that are not a multiple either of 3 or of 5: +### Java +```java +Observable<Integer> threesAndFives = Observable.range(1, 999).filter(e -> e % 3 == 0 || e % 5 == 0); +``` +### Groovy +````groovy +def threesAndFives = Observable.range(1,999).filter({ !((it % 3) && (it % 5)) }); +```` +Or, we could generate two Observable sequences, one containing the multiples of three and the other containing the multiples of five (by [`map`](https://github.com/Netflix/RxJava/wiki/Transforming-Observables#map)ping each value onto its appropriate multiple), making sure to only generating new multiples while they are less than 1000 (the [`takeWhile`](Conditional-and-Boolean-Operators#takewhile-and-takewhilewithindex) operator will help here), and then [`merge`](Combining-Observables#merge) these sets: +### Java +```java +Observable<Integer> threes = Observable.range(1, 999).map(e -> e * 3).takeWhile(e -> e < 1000); +Observable<Integer> fives = Observable.range(1, 999).map(e -> e * 5).takeWhile(e -> e < 1000); +Observable<Integer> threesAndFives = Observable.merge(threes, fives).distinct(); +``` +### Groovy +````groovy +def threes = Observable.range(1,999).map({it*3}).takeWhile({it<1000}); +def fives = Observable.range(1,999).map({it*5}).takeWhile({it<1000}); +def threesAndFives = Observable.merge(threes, fives).distinct(); +```` +Don't forget the [`distinct`](Filtering-Observables#distinct) operator here, otherwise merge will duplicate numbers like 15 that are multiples of both 5 and 3. + +Next, we want to sum up the numbers in the resulting sequence. If you have installed the optional `rxjava-math` module, this is elementary: just use an operator like [`sumInteger` or `sumLong`](Mathematical-and-Aggregate-Operators#suminteger-sumlong-sumfloat-and-sumdouble) on the `threesAndFives` Observable. But what if you don't have this module? How could you use standard RxJava operators to sum up a sequence and emit that sum? + +There are a number of operators that reduce a sequence emitted by a source Observable to a single value emitted by the resulting Observable. Most of the ones that are not in the `rxjava-math` module emit boolean evaluations of the sequence; we want something that can emit a number. The [`reduce`](Mathematical-and-Aggregate-Operators#reduce) operator will do the job: +### Java +```java +Single<Integer> summer = threesAndFives.reduce(0, (a, b) -> a + b); +``` +### Groovy +````groovy +def summer = threesAndFives.reduce(0, { a, b -> a+b }); +```` +Here is how `reduce` gets the job done. It starts with 0 as a seed. Then, with each item that `threesAndFives` emits, it calls the closure `{ a, b -> a+b }`, passing it the current seed value as `a` and the emission as `b`. The closure adds these together and returns that sum, and `reduce` uses this returned value to overwrite its seed. When `threesAndFives` completes, `reduce` emits the final value returned from the closure as its sole emission: +<table> + <thead> + <tr><th>iteration</th><th>seed</th><th>emission</th><th>reduce</th></tr> + </thead> + <tbody> + <tr><td>1</td><td>0</td><td>3</td><td>3</td></tr> + <tr><td>2</td><td>3</td><td>5</td><td>8</td></tr> + <tr><td>3</td><td>8</td><td>6</td><td>14</td></tr> + <tr><td colspan="4"><center>…</center></td></tr> + <tr><td>466</td><td>232169</td><td>999</td><td>233168</td></tr> + </tbody> +</table> +Finally, we want to see the result. This means we must [subscribe](Observable#onnext-oncompleted-and-onerror) to the Observable we have constructed: + +### Java +```java +summer.subscribe(System.out::print); +``` +### Groovy +````groovy +summer.subscribe({println(it);}); +```` + +# Generate the Fibonacci Sequence + +How could you create an Observable that emits [the Fibonacci sequence](http://en.wikipedia.org/wiki/Fibonacci_number)? + +The most direct way would be to use the [`create`](Creating-Observables#wiki-create) operator to make an Observable "from scratch," and then use a traditional loop within the closure you pass to that operator to generate the sequence. Something like this: +### Java +```java +Observable<Integer> fibonacci = Observable.create(emitter -> { + int f1 = 0, f2 = 1, f = 1; + while (!emitter.isDisposed()) { + emitter.onNext(f); + f = f1 + f2; + f1 = f2; + f2 = f; + } +}); +``` +### Groovy +````groovy +def fibonacci = Observable.create({ emitter -> + def f1=0, f2=1, f=1; + while(!emitter.isDisposed()) { + emitter.onNext(f); + f = f1+f2; + f1 = f2; + f2 = f; + }; +}); +```` +But this is a little too much like ordinary linear programming. Is there some way we can instead create this sequence by composing together existing Observable operators? + +Here's an option that does this: +### Java +```java +Observable<Integer> fibonacci = + Observable.fromArray(0) + .repeat() + .scan(new int[]{0, 1}, (a, b) -> new int[]{a[1], a[0] + a[1]}) + .map(a -> a[1]); +``` +### Groovy +````groovy +def fibonacci = Observable.from(0).repeat().scan([0,1], { a,b -> [a[1], a[0]+a[1]] }).map({it[1]}); +```` +It's a little [janky](http://www.urbandictionary.com/define.php?term=janky). Let's walk through it: + +The `Observable.from(0).repeat()` creates an Observable that just emits a series of zeroes. This just serves as grist for the mill to keep [`scan`](Transforming-Observables#scan) operating. The way `scan` usually behaves is that it operates on the emissions from an Observable, one at a time, accumulating the result of operations on each emission in some sort of register, which it emits as its own emissions. The way we're using it here, it ignores the emissions from the source Observable entirely, and simply uses these emissions as an excuse to transform and emit its register. That register gets `[0,1]` as a seed, and with each iteration changes the register from `[a,b]` to `[b,a+b]` and then emits this register. + +This has the effect of emitting the following sequence of items: `[0,1], [1,1], [1,2], [2,3], [3,5], [5,8]...` + +The second item in this array describes the Fibonacci sequence. We can use `map` to reduce the sequence to just that item. + +To print out a portion of this sequence (using either method), you would use code like the following: +### Java +```java +fibonacci.take(15).subscribe(System.out::println); +``` +### Groovy +````groovy +fibonnaci.take(15).subscribe({println(it)})]; +```` + +Is there a less-janky way to do this? The [`generate`](https://github.com/Netflix/RxJava/wiki/Phantom-Operators#generate-and-generateabsolutetime) operator would avoid the silliness of creating an Observable that does nothing but turn the crank of `seed`, but this operator is not yet part of RxJava. Perhaps you can think of a more elegant solution? \ No newline at end of file diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000000..474c8edc77 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,24 @@ +RxJava is a Java VM implementation of [ReactiveX (Reactive Extensions)](https://reactivex.io): a library for composing asynchronous and event-based programs by using observable sequences. + +For more information about ReactiveX, see the [Introduction to ReactiveX](http://reactivex.io/intro.html) page. + +### RxJava is Lightweight + +RxJava tries to be very lightweight. It is implemented as a single JAR that is focused on just the Observable abstraction and related higher-order functions. + +### RxJava is a Polyglot Implementation + +RxJava supports Java 6 or higher and JVM-based languages such as [Groovy](https://github.com/ReactiveX/RxGroovy), [Clojure](https://github.com/ReactiveX/RxClojure), [JRuby](https://github.com/ReactiveX/RxJRuby), [Kotlin](https://github.com/ReactiveX/RxKotlin) and [Scala](https://github.com/ReactiveX/RxScala). + +RxJava is meant for a more polyglot environment than just Java/Scala, and it is being designed to respect the idioms of each JVM-based language. (<a href="https://github.com/Netflix/RxJava/pull/304">This is something we’re still working on.</a>) + +### RxJava Libraries + +The following external libraries can work with RxJava: + +* [Hystrix](https://github.com/Netflix/Hystrix/wiki/How-To-Use#wiki-Reactive-Execution) latency and fault tolerance bulkheading library. +* [Camel RX](http://camel.apache.org/rx.html) provides an easy way to reuse any of the [Apache Camel components, protocols, transports and data formats](http://camel.apache.org/components.html) with the RxJava API +* [rxjava-http-tail](https://github.com/myfreeweb/rxjava-http-tail) allows you to follow logs over HTTP, like `tail -f` +* [mod-rxvertx - Extension for VertX](https://github.com/vert-x/mod-rxvertx) that provides support for Reactive Extensions (RX) using the RxJava library +* [rxjava-jdbc](https://github.com/davidmoten/rxjava-jdbc) - use RxJava with jdbc connections to stream ResultSets and do functional composition of statements +* [rtree](https://github.com/davidmoten/rtree) - immutable in-memory R-tree and R*-tree with RxJava api including backpressure \ No newline at end of file diff --git a/docs/Reactive-Streams.md b/docs/Reactive-Streams.md new file mode 100644 index 0000000000..c3a65b883a --- /dev/null +++ b/docs/Reactive-Streams.md @@ -0,0 +1,121 @@ +# Reactive Streams + RxJava + +[Reactive Streams](https://github.com/reactive-streams/reactive-streams-jvm/) has been a [collaborative effort](https://medium.com/@viktorklang/reactive-streams-1-0-0-interview-faaca2c00bec) to standardize the protocol for asynchronous streams on the JVM. The RxJava team was [part of the effort](https://github.com/reactive-streams/reactive-streams-jvm/graphs/contributors) from the beginning and supports the use of Reactive Streams APIs and eventually the [Java 9 Flow APIs](http://cs.oswego.edu/pipermail/concurrency-interest/2015-January/013641.html) which are [resulting from the success of the Reactive Stream effort](https://github.com/reactive-streams/reactive-streams-jvm/issues/195). + +## How does this relate to RxJava itself? + +#### RxJava 1.x + +Currently RxJava 1.x does not directly implement the Reactive Streams APIs. This is due to RxJava 1.x already existing and not being able to break public APIs. It does however comply semantically with the non-blocking "reactive pull" approach to backpressure and flow control and thus can use a bridge between types. The [RxJavaReactiveStreams module](https://github.com/ReactiveX/RxJavaReactiveStreams) bridges between the RxJava 1.x types and Reactive Streams types for interop between Reactive Streams implementations and passes the Reactive Streams [TCK compliance tests](https://github.com/ReactiveX/RxJavaReactiveStreams/blob/0.x/rxjava-reactive-streams/build.gradle#L8). + +Its API looks like this: + +```java +package rx; + +import org.reactivestreams.Publisher; + +public abstract class RxReactiveStreams { + + public static <T> Publisher<T> toPublisher(Observable<T> observable) { … } + + public static <T> Observable<T> toObservable(Publisher<T> publisher) { … } + +} +``` + +#### RxJava 2.x + +[RxJava 2.x](https://github.com/ReactiveX/RxJava/issues/2450) will target Reactive Streams APIs directly for Java 8+. The plan is to also support Java 9 `j.u.c.Flow` types by leveraging new Java multi-versioned jars to support this when using RxJava 2.x in Java 9 while still working on Java 8. + +RxJava 2 will truly be "Reactive Extensions" now that there is an interface to extend. RxJava 1 didn't have a base interface or contract to extend so had to define it from scratch. RxJava 2 intends on being a high performing, battle-tested, lightweight (single dependency on Reactive Streams), non-opinionated implementation of Reactive Streams and `j.u.c.Flow` that provides a library of higher-order functions with parameterized concurrency. + +## Public APIs of Libraries + +A strong area of value for Reactive Streams is public APIs exposed in libraries. Following is some guidance and recommendation on how to use both Reactive Streams and RxJava in creating reactive libraries while decoupling the concrete implementations. + +### Pros of Exposing Reactive Stream APIs instead of RxJava + +* Lightweight: Very lightweight dependency on interfaces without any concrete implementations. This keeps dependency graphs and bytesize small. +* Future Proof: Since the Reactive Stream API is so simple, was collaboratively defined and is [becoming part](https://github.com/reactive-streams/reactive-streams-jvm/issues/195) of [JDK 9](http://cs.oswego.edu/pipermail/concurrency-interest/2015-January/013641.html) it is a future proof API for exposing async access to data. The [`j.u.c.Flow` APIs](http://gee.cs.oswego.edu/dl/jsr166/dist/docs/java/util/concurrent/Flow.html) of JDK 9 match the APIs of Reactive Streams so any types that implement the Reactive Streams `Publisher` will also be able to implement the `Flow.Publisher` type. +* Interop: An API exposed with Reactive Streams types can easily be consumed by any implementation such as RxJava, Akka Streams and Reactor. + +### Cons of Exposing Reactive Stream APIs instead of RxJava + +* A Reactive Stream `Publisher` is not very useful by itself. Without higher-order functions like `flatMap` it is just a better callback. This means that consumption of a `Publisher` will almost always need to be converted or wrapped into a Reactive Stream implementation. This can be verbose and awkward to always be wrapping `Publisher` APIs into a concrete implementation. If the JVM supported extension methods this would be elegant, but since it doesn't it is explicit and verbose. + + Specifically the Reactive Streams and Flow `Publisher` interfaces do not provide any implementations of operators like `flatMap`, `merge`, `filter`, `take`, `zip` and the many others used to compose and transform async streams. A `Publisher` can only be subscribed to. A concrete implementation such as RxJava is needed to provide composition. +* The Reactive Streams specification and binary artifacts do not provide a concrete implementation of `Publisher`. Generally a library will need or want capabilities provides by RxJava, Akka Streams, etc for its internal use or just to produce a valid `Publisher` that supports backpressure semantics (which are non-trivial to implement correctly). + +### Recommended Approach + +Now that Reactive Streams has achieved 1.0 we recommend using it for core APIs that are intended for interop. This will allow embracing asynchronous stream semantics without a hard dependency on any single implementation. This means a consumer of the API can then choose RxJava 1.x, RxJava 2.x, Akka Streams, Reactor or other stream composition libraries as suits them best. It also provides better future proofing, for example as RxJava moves from 1.x to 2.x. + +However, to limit the cons listed above, we also recommend making it easy for consumption without developers needing to explicitly wrap the APIs with their composition library of choice. For this reason we recommend providing wrapper modules for popular Reactive Stream implementations on top of the core API, otherwise your customers will each need to do this themselves. + +Note that if Java offered extension methods this approach wouldn't be needed, but until Java offers that (not anytime soon if ever) the following is an approach to achieve the pros and address the cons. + +For example, a database driver may have modules such as this: + + +// core library exposing Reactive Stream Publisher APIs +* async-database-driver + +// integration jars wrapped with concrete implementations +* async-database-driver-rxjava1 +* async-database-driver-rxjava2 +* async-database-driver-akka-stream + +The "core" may expose an API like this: + +```java +package com.database.driver; + +public class Database { + public org.reactivestreams.Publisher getValue(String key); +} +``` + +The RxJava 1.x wrapper could then be a separate module that provides RxJava specific APIs like this: + +```java +package com.database.driver.rxjava1; + +public class Database { + public rx.Observable getValue(String key); +} +``` + +The core `Publisher` API can be wrapped as simply as this: + +```java +public rx.Observable getValue(String key) { + return RxReactiveStreams.toObservable(coreDatabase.getValue(key)); +} +``` + +The RxJava 2.x wrapper would differ like this (once 2.x is available): + +```java +package com.database.driver.rxjava2; + +public class Database { + public io.reactivex.Observable getValue(String key); +} +``` + +The Akka Streams wrapper would in turn look like this: + +```java +package com.database.driver.akkastream; + +public class Database { + public akka.stream.javadsl.Source getValue(String key); +} +``` + +A developer could then choose to depend directly on the `async-database-driver` APIs but most will use one of the wrappers that supports the composition library they have chosen. + +---- + +If something could be clarified further, please help improve this page via discussion at https://github.com/ReactiveX/RxJava/issues/2917 \ No newline at end of file diff --git a/docs/Scheduler.md b/docs/Scheduler.md new file mode 100644 index 0000000000..95657cc488 --- /dev/null +++ b/docs/Scheduler.md @@ -0,0 +1,3 @@ +If you want to introduce multithreading into your cascade of Observable operators, you can do so by instructing those operators (or particular Observables) to operate on particular Schedulers. + +For more information about Schedulers, see [the ReactiveX `Scheduler` documentation page](http://reactivex.io/documentation/scheduler.html). \ No newline at end of file diff --git a/docs/String-Observables.md b/docs/String-Observables.md new file mode 100644 index 0000000000..3dc3038b94 --- /dev/null +++ b/docs/String-Observables.md @@ -0,0 +1,9 @@ +The `StringObservable` class contains methods that represent operators particular to Observables that deal in string-based sequences and streams. These include: + +* [**`byLine( )`**](http://reactivex.io/documentation/operators/map.html) — converts an Observable of Strings into an Observable of Lines by treating the source sequence as a stream and splitting it on line-endings +* [**`decode( )`**](http://reactivex.io/documentation/operators/from.html) — convert a stream of multibyte characters into an Observable that emits byte arrays that respect character boundaries +* [**`encode( )`**](http://reactivex.io/documentation/operators/map.html) — transform an Observable that emits strings into an Observable that emits byte arrays that respect character boundaries of multibyte characters in the original strings +* [**`from( )`**](http://reactivex.io/documentation/operators/from.html) — convert a stream of characters or a Reader into an Observable that emits byte arrays or Strings +* [**`join( )`**](http://reactivex.io/documentation/operators/sum.html) — converts an Observable that emits a sequence of strings into an Observable that emits a single string that concatenates them all, separating them by a specified string +* [**`split( )`**](http://reactivex.io/documentation/operators/flatmap.html) — converts an Observable of Strings into an Observable of Strings that treats the source sequence as a stream and splits it on a specified regex boundary +* [**`stringConcat( )`**](http://reactivex.io/documentation/operators/sum.html) — converts an Observable that emits a sequence of strings into an Observable that emits a single string that concatenates them all \ No newline at end of file diff --git a/docs/Subject.md b/docs/Subject.md new file mode 100644 index 0000000000..ffb7feeb51 --- /dev/null +++ b/docs/Subject.md @@ -0,0 +1,12 @@ +A <a href="http://reactivex.io/RxJava/javadoc/rx/subjects/Subject.html">`Subject`</a> is a sort of bridge or proxy that acts both as an `Subscriber` and as an `Observable`. Because it is a Subscriber, it can subscribe to one or more Observables, and because it is an Observable, it can pass through the items it observes by reemitting them, and it can also emit new items. + +For more information about the varieties of Subject and how to use them, see [the ReactiveX `Subject` documentation](http://reactivex.io/documentation/subject.html). + +#### Serializing +When you use a Subject as a Subscriber, take care not to call its `onNext( )` method (or its other `on` methods) from multiple threads, as this could lead to non-serialized calls, which violates the Observable contract and creates an ambiguity in the resulting Subject. + +To protect a Subject from this danger, you can convert it into a [`SerializedSubject`](http://reactivex.io/RxJava/javadoc/rx/subjects/SerializedSubject.html) with code like the following: + +```java +mySafeSubject = new SerializedSubject( myUnsafeSubject ); +``` diff --git a/docs/The-RxJava-Android-Module.md b/docs/The-RxJava-Android-Module.md new file mode 100644 index 0000000000..ad9817c80a --- /dev/null +++ b/docs/The-RxJava-Android-Module.md @@ -0,0 +1,3 @@ +## RxAndroid + +See the [RxAndroid](https://github.com/ReactiveX/RxAndroid) project page and the [the RxAndroid wiki](https://github.com/ReactiveX/RxAndroid/wiki) for details. diff --git a/docs/Transforming-Observables.md b/docs/Transforming-Observables.md new file mode 100644 index 0000000000..3a3d94cd2e --- /dev/null +++ b/docs/Transforming-Observables.md @@ -0,0 +1,777 @@ +This page shows operators with which you can transform items that are emitted by reactive sources, such as `Observable`s. + +# Outline + +- [`buffer`](#buffer) +- [`cast`](#cast) +- [`concatMap`](#concatmap) +- [`concatMapCompletable`](#concatmapcompletable) +- [`concatMapCompletableDelayError`](#concatmapcompletabledelayerror) +- [`concatMapDelayError`](#concatmapdelayerror) +- [`concatMapEager`](#concatmapeager) +- [`concatMapEagerDelayError`](#concatmapeagerdelayerror) +- [`concatMapIterable`](#concatmapiterable) +- [`concatMapMaybe`](#concatmapmaybe) +- [`concatMapMaybeDelayError`](#concatmapmaybedelayerror) +- [`concatMapSingle`](#concatmapsingle) +- [`concatMapSingleDelayError`](#concatmapsingledelayerror) +- [`flatMap`](#flatmap) +- [`flatMapCompletable`](#flatmapcompletable) +- [`flatMapIterable`](#flatmapiterable) +- [`flatMapMaybe`](#flatmapmaybe) +- [`flatMapObservable`](#flatmapobservable) +- [`flatMapPublisher`](#flatmappublisher) +- [`flatMapSingle`](#flatmapsingle) +- [`flatMapSingleElement`](#flatmapsingleelement) +- [`flattenAsFlowable`](#flattenasflowable) +- [`flattenAsObservable`](#flattenasobservable) +- [`groupBy`](#groupby) +- [`map`](#map) +- [`scan`](#scan) +- [`switchMap`](#switchmap) +- [`window`](#window) + +## buffer + +**Available in:** ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Flowable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Observable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Maybe`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Single`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Completable` + +**ReactiveX documentation:** [http://reactivex.io/documentation/operators/buffer.html](http://reactivex.io/documentation/operators/buffer.html) + +Collects the items emitted by a reactive source into buffers, and emits these buffers. + +### buffer example + +```java +Observable.range(0, 10) + .buffer(4) + .subscribe((List<Integer> buffer) -> System.out.println(buffer)); + +// prints: +// [0, 1, 2, 3] +// [4, 5, 6, 7] +// [8, 9] +``` + +## cast + +**Available in:** ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Flowable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Observable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Maybe`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Single`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Completable` + +**ReactiveX documentation:** [http://reactivex.io/documentation/operators/map.html](http://reactivex.io/documentation/operators/map.html) + +Converts each item emitted by a reactive source to the specified type, and emits these items. + +### cast example + +```java +Observable<Number> numbers = Observable.just(1, 4.0, 3f, 7, 12, 4.6, 5); + +numbers.filter((Number x) -> Integer.class.isInstance(x)) + .cast(Integer.class) + .subscribe((Integer x) -> System.out.println(x)); + +// prints: +// 1 +// 7 +// 12 +// 5 +``` + +## concatMap + +**Available in:** ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Flowable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Observable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Maybe`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Single`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Completable` + +**ReactiveX documentation:** [http://reactivex.io/documentation/operators/flatmap.html](http://reactivex.io/documentation/operators/flatmap.html) + +Applies the given `io.reactivex.functions.Function` to each item emitted by a reactive source, where that function returns a reactive source, and emits the items that result from concatenating the results of these function applications. + +### concatMap example + +```java +Observable.range(0, 5) + .concatMap(i -> { + long delay = Math.round(Math.random() * 2); + + return Observable.timer(delay, TimeUnit.SECONDS).map(n -> i); + }) + .blockingSubscribe(System.out::print); + +// prints 01234 +``` + +## concatMapCompletable + +**Available in:** ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Flowable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Observable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Maybe`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Single`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Completable` + +**ReactiveX documentation:** [http://reactivex.io/documentation/operators/flatmap.html](http://reactivex.io/documentation/operators/flatmap.html) + +Applies the given `io.reactivex.functions.Function` to each item emitted by a reactive source, where that function returns a `io.reactivex.CompletableSource`, subscribes to them one at a time and returns a `Completable` that completes when all sources completed. + +### concatMapCompletable example + +```java +Observable<Integer> source = Observable.just(2, 1, 3); +Completable completable = source.concatMapCompletable(x -> { + return Completable.timer(x, TimeUnit.SECONDS) + .doOnComplete(() -> System.out.println("Info: Processing of item \"" + x + "\" completed")); + }); + +completable.doOnComplete(() -> System.out.println("Info: Processing of all items completed")) + .blockingAwait(); + +// prints: +// Info: Processing of item "2" completed +// Info: Processing of item "1" completed +// Info: Processing of item "3" completed +// Info: Processing of all items completed +``` + +## concatMapCompletableDelayError + +**Available in:** ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Flowable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Observable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Maybe`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Single`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Completable` + +**ReactiveX documentation:** [http://reactivex.io/documentation/operators/flatmap.html](http://reactivex.io/documentation/operators/flatmap.html) + +Applies the given `io.reactivex.functions.Function` to each item emitted by a reactive source, where that function returns a `io.reactivex.CompletableSource`, subscribes to them one at a time and returns a `Completable` that completes when all sources completed. Any errors from the sources will be delayed until all of them terminate. + +### concatMapCompletableDelayError example + +```java +Observable<Integer> source = Observable.just(2, 1, 3); +Completable completable = source.concatMapCompletableDelayError(x -> { + if (x.equals(2)) { + return Completable.error(new IOException("Processing of item \"" + x + "\" failed!")); + } else { + return Completable.timer(1, TimeUnit.SECONDS) + .doOnComplete(() -> System.out.println("Info: Processing of item \"" + x + "\" completed")); + } +}); + +completable.doOnError(error -> System.out.println("Error: " + error.getMessage())) + .onErrorComplete() + .blockingAwait(); + +// prints: +// Info: Processing of item "1" completed +// Info: Processing of item "3" completed +// Error: Processing of item "2" failed! +``` + +## concatMapDelayError + +**Available in:** ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Flowable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Observable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Maybe`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Single`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Completable` + +**ReactiveX documentation:** [http://reactivex.io/documentation/operators/flatmap.html](http://reactivex.io/documentation/operators/flatmap.html) + +Applies the given `io.reactivex.functions.Function` to each item emitted by a reactive source, where that function returns a reactive source, and emits the items that result from concatenating the results of these function applications. Any errors from the sources will be delayed until all of them terminate. + +### concatMapDelayError example + +```java +Observable.intervalRange(1, 3, 0, 1, TimeUnit.SECONDS) + .concatMapDelayError(x -> { + if (x.equals(1L)) return Observable.error(new IOException("Something went wrong!")); + else return Observable.just(x, x * x); + }) + .blockingSubscribe( + x -> System.out.println("onNext: " + x), + error -> System.out.println("onError: " + error.getMessage())); + +// prints: +// onNext: 2 +// onNext: 4 +// onNext: 3 +// onNext: 9 +// onError: Something went wrong! +``` + +## concatMapEager + +**Available in:** ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Flowable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Observable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Maybe`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Single`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Completable` + +**ReactiveX documentation:** [http://reactivex.io/documentation/operators/flatmap.html](http://reactivex.io/documentation/operators/flatmap.html) + +Applies the given `io.reactivex.functions.Function` to each item emitted by a reactive source, where that function returns a reactive source, and emits the items that result from concatenating the results of these function applications. Unlike [`concatMap`](#concatmap), this operator eagerly subscribes to all sources. + +### concatMapEager example + +```java +Observable.range(0, 5) + .concatMapEager(i -> { + long delay = Math.round(Math.random() * 3); + + return Observable.timer(delay, TimeUnit.SECONDS) + .map(n -> i) + .doOnNext(x -> System.out.println("Info: Finished processing item " + x)); + }) + .blockingSubscribe(i -> System.out.println("onNext: " + i)); + +// prints (lines beginning with "Info..." can be displayed in a different order): +// Info: Finished processing item 2 +// Info: Finished processing item 0 +// onNext: 0 +// Info: Finished processing item 1 +// onNext: 1 +// onNext: 2 +// Info: Finished processing item 3 +// Info: Finished processing item 4 +// onNext: 3 +// onNext: 4 +``` + +## concatMapEagerDelayError + +**Available in:** ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Flowable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Observable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Maybe`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Single`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Completable` + +**ReactiveX documentation:** [http://reactivex.io/documentation/operators/flatmap.html](http://reactivex.io/documentation/operators/flatmap.html) + +Applies the given `io.reactivex.functions.Function` to each item emitted by a reactive source, where that function returns a reactive source, and emits the items that result from concatenating the results of these function applications. A `boolean` value must be specified, which if `true` indicates that all errors from all sources will be delayed until the end, otherwise if `false`, an error from the main source will be signalled when the current source terminates. Unlike [concatMapDelayError](#concatmapdelayerror), this operator eagerly subscribes to all sources. + +### concatMapEagerDelayError example + +```java +Observable<Integer> source = Observable.create(emitter -> { + emitter.onNext(1); + emitter.onNext(2); + emitter.onError(new Error("Fatal error!")); +}); + +source.doOnError(error -> System.out.println("Info: Error from main source " + error.getMessage())) + .concatMapEagerDelayError(x -> { + return Observable.timer(1, TimeUnit.SECONDS).map(n -> x) + .doOnSubscribe(it -> System.out.println("Info: Processing of item \"" + x + "\" started")); + }, true) + .blockingSubscribe( + x -> System.out.println("onNext: " + x), + error -> System.out.println("onError: " + error.getMessage())); + +// prints: +// Info: Processing of item "1" started +// Info: Processing of item "2" started +// Info: Error from main source Fatal error! +// onNext: 1 +// onNext: 2 +// onError: Fatal error! +``` + +## concatMapIterable + +**Available in:** ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Flowable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Observable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Maybe`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Single`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Completable` + +**ReactiveX documentation:** [http://reactivex.io/documentation/operators/flatmap.html](http://reactivex.io/documentation/operators/flatmap.html) + +Applies the given `io.reactivex.functions.Function` to each item emitted by a reactive source, where that function returns a `java.lang.Iterable`, and emits the items that result from concatenating the results of these function applications. + +### concatMapIterable example + +```java +Observable.just("A", "B", "C") + .concatMapIterable(item -> List.of(item, item, item)) + .subscribe(System.out::print); + +// prints AAABBBCCC +``` + +## concatMapMaybe + +**Available in:** ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Flowable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Observable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Maybe`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Single`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Completable` + +**ReactiveX documentation:** [http://reactivex.io/documentation/operators/flatmap.html](http://reactivex.io/documentation/operators/flatmap.html) + +Applies the given `io.reactivex.functions.Function` to each item emitted by a reactive source, where that function returns a `io.reactivex.MaybeSource`, and emits the items that result from concatenating these `MaybeSource`s. + +### concatMapMaybe example + +```java +Observable.just("5", "3,14", "2.71", "FF") + .concatMapMaybe(v -> { + return Maybe.fromCallable(() -> Double.parseDouble(v)) + .doOnError(e -> System.out.println("Info: The value \"" + v + "\" could not be parsed.")) + + // Ignore values that can not be parsed. + .onErrorComplete(); + }) + .subscribe(x -> System.out.println("onNext: " + x)); + +// prints: +// onNext: 5.0 +// Info: The value "3,14" could not be parsed. +// onNext: 2.71 +// Info: The value "FF" could not be parsed. +``` + +## concatMapMaybeDelayError + +**Available in:** ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Flowable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Observable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Maybe`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Single`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Completable` + +**ReactiveX documentation:** [http://reactivex.io/documentation/operators/flatmap.html](http://reactivex.io/documentation/operators/flatmap.html) + +Applies the given `io.reactivex.functions.Function` to each item emitted by a reactive source, where that function returns a `io.reactivex.MaybeSource`, and emits the items that result from concatenating these `MaybeSource`s. Any errors from the sources will be delayed until all of them terminate. + +### concatMapMaybeDelayError example + +```java +DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("dd.MM.uuuu"); +Observable.just("04.03.2018", "12-08-2018", "06.10.2018", "01.12.2018") + .concatMapMaybeDelayError(date -> { + return Maybe.fromCallable(() -> LocalDate.parse(date, dateFormatter)); + }) + .subscribe( + localDate -> System.out.println("onNext: " + localDate), + error -> System.out.println("onError: " + error.getMessage())); + +// prints: +// onNext: 2018-03-04 +// onNext: 2018-10-06 +// onNext: 2018-12-01 +// onError: Text '12-08-2018' could not be parsed at index 2 +``` + +## concatMapSingle + +**Available in:** ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Flowable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Observable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Maybe`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Single`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Completable` + +**ReactiveX documentation:** [http://reactivex.io/documentation/operators/flatmap.html](http://reactivex.io/documentation/operators/flatmap.html) + +Applies the given `io.reactivex.functions.Function` to each item emitted by a reactive source, where that function returns a `io.reactivex.SingleSource`, and emits the items that result from concatenating these ``SingleSource`s. + +### concatMapSingle example + +```java +Observable.just("5", "3,14", "2.71", "FF") + .concatMapSingle(v -> { + return Single.fromCallable(() -> Double.parseDouble(v)) + .doOnError(e -> System.out.println("Info: The value \"" + v + "\" could not be parsed.")) + + // Return a default value if the given value can not be parsed. + .onErrorReturnItem(42.0); + }) + .subscribe(x -> System.out.println("onNext: " + x)); + +// prints: +// onNext: 5.0 +// Info: The value "3,14" could not be parsed. +// onNext: 42.0 +// onNext: 2.71 +// Info: The value "FF" could not be parsed. +// onNext: 42.0 +``` + +## concatMapSingleDelayError + +**Available in:** ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Flowable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Observable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Maybe`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Single`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Completable` + +**ReactiveX documentation:** [http://reactivex.io/documentation/operators/flatmap.html](http://reactivex.io/documentation/operators/flatmap.html) + +Applies the given `io.reactivex.functions.Function` to each item emitted by a reactive source, where that function returns a `io.reactivex.SingleSource`, and emits the items that result from concatenating the results of these function applications. Any errors from the sources will be delayed until all of them terminate. + +### concatMapSingleDelayError example + +```java +DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("dd.MM.uuuu"); +Observable.just("24.03.2018", "12-08-2018", "06.10.2018", "01.12.2018") + .concatMapSingleDelayError(date -> { + return Single.fromCallable(() -> LocalDate.parse(date, dateFormatter)); + }) + .subscribe( + localDate -> System.out.println("onNext: " + localDate), + error -> System.out.println("onError: " + error.getMessage())); + +// prints: +// onNext: 2018-03-24 +// onNext: 2018-10-06 +// onNext: 2018-12-01 +// onError: Text '12-08-2018' could not be parsed at index 2 +``` + +## flatMap + +**Available in:** ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Flowable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Observable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Maybe`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Single`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Completable` + +**ReactiveX documentation:** [http://reactivex.io/documentation/operators/flatmap.html](http://reactivex.io/documentation/operators/flatmap.html) + +Applies the given `io.reactivex.functions.Function` to each item emitted by a reactive source, where that function returns a reactive source, and emits the items that result from merging the results of these function applications. + +### flatMap example + +```java +Observable.just("A", "B", "C") + .flatMap(a -> { + return Observable.intervalRange(1, 3, 0, 1, TimeUnit.SECONDS) + .map(b -> '(' + a + ", " + b + ')'); + }) + .blockingSubscribe(System.out::println); + +// prints (not necessarily in this order): +// (A, 1) +// (C, 1) +// (B, 1) +// (A, 2) +// (C, 2) +// (B, 2) +// (A, 3) +// (C, 3) +// (B, 3) +``` + +## flatMapCompletable + +**Available in:** ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Flowable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Observable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Maybe`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Single`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Completable` + +**ReactiveX documentation:** [http://reactivex.io/documentation/operators/flatmap.html](http://reactivex.io/documentation/operators/flatmap.html) + +Applies the given `io.reactivex.functions.Function` to each item emitted by a reactive source, where that function returns a `io.reactivex.CompletableSource`, and returns a `Completable` that completes when all sources completed. + +### flatMapCompletable example + +```java +Observable<Integer> source = Observable.just(2, 1, 3); +Completable completable = source.flatMapCompletable(x -> { + return Completable.timer(x, TimeUnit.SECONDS) + .doOnComplete(() -> System.out.println("Info: Processing of item \"" + x + "\" completed")); +}); + +completable.doOnComplete(() -> System.out.println("Info: Processing of all items completed")) + .blockingAwait(); + +// prints: +// Info: Processing of item "1" completed +// Info: Processing of item "2" completed +// Info: Processing of item "3" completed +// Info: Processing of all items completed +``` + +## flatMapIterable + +**Available in:** ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Flowable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Observable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Maybe`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Single`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Completable` + +**ReactiveX documentation:** [http://reactivex.io/documentation/operators/flatmap.html](http://reactivex.io/documentation/operators/flatmap.html) + +Applies the given `io.reactivex.functions.Function` to each item emitted by a reactive source, where that function returns a `java.lang.Iterable`, and emits the elements from these `Iterable`s. + +### flatMapIterable example + +```java +Observable.just(1, 2, 3, 4) + .flatMapIterable(x -> { + switch (x % 4) { + case 1: + return List.of("A"); + case 2: + return List.of("B", "B"); + case 3: + return List.of("C", "C", "C"); + default: + return List.of(); + } + }) + .subscribe(System.out::println); + +// prints: +// A +// B +// B +// C +// C +// C +``` + +## flatMapMaybe + +**Available in:** ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Flowable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Observable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Maybe`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Single`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Completable` + +**ReactiveX documentation:** [http://reactivex.io/documentation/operators/flatmap.html](http://reactivex.io/documentation/operators/flatmap.html) + +Applies the given `io.reactivex.functions.Function` to each item emitted by a reactive source, where that function returns a `io.reactivex.MaybeSource`, and emits the items that result from merging these `MaybeSource`s. + +### flatMapMaybe example + +```java +Observable.just(9.0, 16.0, -4.0) + .flatMapMaybe(x -> { + if (x.compareTo(0.0) < 0) return Maybe.empty(); + else return Maybe.just(Math.sqrt(x)); + }) + .subscribe( + System.out::println, + Throwable::printStackTrace, + () -> System.out.println("onComplete")); + +// prints: +// 3.0 +// 4.0 +// onComplete +``` + +## flatMapObservable + +**Available in:** ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Flowable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Observable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Maybe`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Single`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Completable` + +**ReactiveX documentation:** [http://reactivex.io/documentation/operators/flatmap.html](http://reactivex.io/documentation/operators/flatmap.html) + +Applies the given `io.reactivex.functions.Function` to the item emitted by a `Maybe` or `Single`, where that function returns an `io.reactivex.ObservableSource`, and returns an `Observable` that emits the items emitted by this `ObservableSource`. + +### flatMapObservable example + +```java +Single<String> source = Single.just("Kirk, Spock, Chekov, Sulu"); +Observable<String> names = source.flatMapObservable(text -> { + return Observable.fromArray(text.split(",")) + .map(String::strip); +}); + +names.subscribe(name -> System.out.println("onNext: " + name)); + +// prints: +// onNext: Kirk +// onNext: Spock +// onNext: Chekov +// onNext: Sulu +``` + +## flatMapPublisher + +**Available in:** ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Flowable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Observable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Maybe`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Single`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Completable` + +**ReactiveX documentation:** [http://reactivex.io/documentation/operators/flatmap.html](http://reactivex.io/documentation/operators/flatmap.html) + +Applies the given `io.reactivex.functions.Function` to the item emitted by a `Maybe` or `Single`, where that function returns an `org.reactivestreams.Publisher`, and returns a `Flowable` that emits the items emitted by this `Publisher`. + +### flatMapPublisher example + +```java +Single<String> source = Single.just("Kirk, Spock, Chekov, Sulu"); +Flowable<String> names = source.flatMapPublisher(text -> { + return Flowable.fromArray(text.split(",")) + .map(String::strip); +}); + +names.subscribe(name -> System.out.println("onNext: " + name)); + +// prints: +// onNext: Kirk +// onNext: Spock +// onNext: Chekov +// onNext: Sulu +``` + +## flatMapSingle + +**Available in:** ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Flowable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Observable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Maybe`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Single`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Completable` + +**ReactiveX documentation:** [http://reactivex.io/documentation/operators/flatmap.html](http://reactivex.io/documentation/operators/flatmap.html) + +Applies the given `io.reactivex.functions.Function` to each item emitted by a reactive source, where that function returns a `io.reactivex.SingleSource`, and emits the items that result from merging these `SingleSource`s. + +### flatMapSingle example + +```java +Observable.just(4, 2, 1, 3) + .flatMapSingle(x -> Single.timer(x, TimeUnit.SECONDS).map(i -> x)) + .blockingSubscribe(System.out::print); + +// prints 1234 +``` + +*Note:* `Maybe::flatMapSingle` returns a `Single` that signals an error notification if the `Maybe` source is empty: + +```java +Maybe<Object> emptySource = Maybe.empty(); +Single<Object> result = emptySource.flatMapSingle(x -> Single.just(x)); +result.subscribe( + x -> System.out.println("onSuccess will not be printed!"), + error -> System.out.println("onError: Source was empty!")); + +// prints: +// onError: Source was empty! +``` + +Use [`Maybe::flatMapSingleElement`](#flatmapsingleelement) -- which returns a `Maybe` -- if you don't want this behaviour. + +## flatMapSingleElement + +**Available in:** ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Flowable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Observable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Maybe`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Single`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Completable` + +**ReactiveX documentation:** [http://reactivex.io/documentation/operators/flatmap.html](http://reactivex.io/documentation/operators/flatmap.html) + +Applies the given `io.reactivex.functions.Function` to the item emitted by a `Maybe`, where that function returns a `io.reactivex.SingleSource`, and returns a `Maybe` that either emits the item emitted by this `SingleSource` or completes if the source `Maybe` just completes. + +### flatMapSingleElement example + +```java +Maybe<Integer> source = Maybe.just(-42); +Maybe<Integer> result = source.flatMapSingleElement(x -> { + return Single.just(Math.abs(x)); +}); + +result.subscribe(System.out::println); + +// prints 42 +``` + +## flattenAsFlowable + +**Available in:** ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Flowable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Observable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Maybe`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Single`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Completable` + +**ReactiveX documentation:** [http://reactivex.io/documentation/operators/flatmap.html](http://reactivex.io/documentation/operators/flatmap.html) + +Applies the given `io.reactivex.functions.Function` to the item emitted by a `Maybe` or `Single`, where that function returns a `java.lang.Iterable`, and returns a `Flowable` that emits the elements from this `Iterable`. + +### flattenAsFlowable example + +```java +Single<Double> source = Single.just(2.0); +Flowable<Double> flowable = source.flattenAsFlowable(x -> { + return List.of(x, Math.pow(x, 2), Math.pow(x, 3)); +}); + +flowable.subscribe(x -> System.out.println("onNext: " + x)); + +// prints: +// onNext: 2.0 +// onNext: 4.0 +// onNext: 8.0 +``` + +## flattenAsObservable + +**Available in:** ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Flowable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Observable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Maybe`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Single`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Completable` + +**ReactiveX documentation:** [http://reactivex.io/documentation/operators/flatmap.html](http://reactivex.io/documentation/operators/flatmap.html) + +Applies the given `io.reactivex.functions.Function` to the item emitted by a `Maybe` or `Single`, where that function returns a `java.lang.Iterable`, and returns an `Observable` that emits the elements from this `Iterable`. + +### flattenAsObservable example + +```java +Single<Double> source = Single.just(2.0); +Observable<Double> observable = source.flattenAsObservable(x -> { + return List.of(x, Math.pow(x, 2), Math.pow(x, 3)); +}); + +observable.subscribe(x -> System.out.println("onNext: " + x)); + +// prints: +// onNext: 2.0 +// onNext: 4.0 +// onNext: 8.0 +``` + +## groupBy + +**Available in:** ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Flowable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Observable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Maybe`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Single`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Completable` + +**ReactiveX documentation:** [http://reactivex.io/documentation/operators/groupby.html](http://reactivex.io/documentation/operators/groupby.html) + +Groups the items emitted by a reactive source according to a specified criterion, and emits these grouped items as a `GroupedObservable` or `GroupedFlowable`. + +### groupBy example + +```java +Observable<String> animals = Observable.just( + "Tiger", "Elephant", "Cat", "Chameleon", "Frog", "Fish", "Turtle", "Flamingo"); + +animals.groupBy(animal -> animal.charAt(0), String::toUpperCase) + .concatMapSingle(Observable::toList) + .subscribe(System.out::println); + +// prints: +// [TIGER, TURTLE] +// [ELEPHANT] +// [CAT, CHAMELEON] +// [FROG, FISH, FLAMINGO] +``` + +## map + +**Available in:** ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Flowable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Observable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Maybe`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Single`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Completable` + +**ReactiveX documentation:** [http://reactivex.io/documentation/operators/map.html](http://reactivex.io/documentation/operators/map.html) + +Applies the given `io.reactivex.functions.Function` to each item emitted by a reactive source and emits the results of these function applications. + +### map example + +```java +Observable.just(1, 2, 3) + .map(x -> x * x) + .subscribe(System.out::println); + +// prints: +// 1 +// 4 +// 9 +``` + +## scan + +**Available in:** ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Flowable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Observable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Maybe`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Single`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Completable` + +**ReactiveX documentation:** [http://reactivex.io/documentation/operators/scan.html](http://reactivex.io/documentation/operators/scan.html) + +Applies the given `io.reactivex.functions.BiFunction` to a seed value and the first item emitted by a reactive source, then feeds the result of that function application along with the second item emitted by the reactive source into the same function, and so on until all items have been emitted by the reactive source, emitting each intermediate result. + +### scan example + +```java +Observable.just(5, 3, 8, 1, 7) + .scan(0, (partialSum, x) -> partialSum + x) + .subscribe(System.out::println); + +// prints: +// 0 +// 5 +// 8 +// 16 +// 17 +// 24 +``` + +## switchMap + +**Available in:** ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Flowable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Observable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Maybe`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Single`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Completable` + +**ReactiveX documentation:** [http://reactivex.io/documentation/operators/flatmap.html](http://reactivex.io/documentation/operators/flatmap.html) + +Applies the given `io.reactivex.functions.Function` to each item emitted by a reactive source, where that function returns a reactive source, and emits the items emitted by the most recently projected of these reactive sources. + +### switchMap example + +```java +Observable.interval(0, 1, TimeUnit.SECONDS) + .switchMap(x -> { + return Observable.interval(0, 750, TimeUnit.MILLISECONDS) + .map(y -> x); + }) + .takeWhile(x -> x < 3) + .blockingSubscribe(System.out::print); + +// prints 001122 +``` + +## window + +**Available in:** ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Flowable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png) `Observable`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Maybe`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Single`, ![image](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png) `Completable` + +**ReactiveX documentation:** [http://reactivex.io/documentation/operators/window.html](http://reactivex.io/documentation/operators/window.html) + +Collects the items emitted by a reactive source into windows, and emits these windows as a `Flowable` or `Observable`. + +### window example + +```java +Observable.range(1, 10) + + // Create windows containing at most 2 items, and skip 3 items before starting a new window. + .window(2, 3) + .flatMapSingle(window -> { + return window.map(String::valueOf) + .reduce(new StringJoiner(", ", "[", "]"), StringJoiner::add); + }) + .subscribe(System.out::println); + +// prints: +// [1, 2] +// [4, 5] +// [7, 8] +// [10] +``` diff --git a/docs/What's-different-in-2.0.md b/docs/What's-different-in-2.0.md new file mode 100644 index 0000000000..edc681fa7f --- /dev/null +++ b/docs/What's-different-in-2.0.md @@ -0,0 +1,970 @@ +RxJava 2.0 has been completely rewritten from scratch on top of the Reactive-Streams specification. The specification itself has evolved out of RxJava 1.x and provides a common baseline for reactive systems and libraries. + +Because Reactive-Streams has a different architecture, it mandates changes to some well known RxJava types. This wiki page attempts to summarize what has changed and describes how to rewrite 1.x code into 2.x code. + +For technical details on how to write operators for 2.x, please visit the [Writing Operators](https://github.com/ReactiveX/RxJava/wiki/Writing-operators-for-2.0) wiki page. + +# Contents + + - [Maven address and base package](#maven-address-and-base-package) + - [Javadoc](#javadoc) + - [Nulls](#nulls) + - [Observable and Flowable](#observable-and-flowable) + - [Single](#single) + - [Completable](#completable) + - [Maybe](#maybe) + - [Base reactive interfaces](#base-reactive-interfaces) + - [Subjects and Processors](#subjects-and-processors) + - [Other classes](#other-classes) + - [Functional interfaces](#functional-interfaces) + - [Subscriber](#subscriber) + - [Subscription](#subscription) + - [Backpressure](#backpressure) + - [Reactive-Streams compliance](#reactive-streams-compliance) + - [Runtime hooks](#runtime-hooks) + - [Error handling](#error-handling) + - [Scheduler](#schedulers) + - [Entering the reactive world](#entering-the-reactive-world) + - [Leaving the reactive world](#leaving-the-reactive-world) + - [Testing](#testing) + - [Operator differences](#operator-differences) + - [Miscellaneous changes](#miscellaneous-changes) + + +# Maven address and base package + +To allow having RxJava 1.x and RxJava 2.x side-by-side, RxJava 2.x is under the maven coordinates `io.reactivex.rxjava2:rxjava:2.x.y` and classes are accessible below `io.reactivex`. + +Users switching from 1.x to 2.x have to re-organize their imports, but carefully. + +# Javadoc + +The official Javadoc pages for 2.x is hosted at http://reactivex.io/RxJava/2.x/javadoc/ + +# Nulls + +RxJava 2.x no longer accepts `null` values and the following will yield `NullPointerException` immediately or as a signal to downstream: + +```java +Observable.just(null); + +Single.just(null); + +Observable.fromCallable(() -> null) + .subscribe(System.out::println, Throwable::printStackTrace); + +Observable.just(1).map(v -> null) + .subscribe(System.out::println, Throwable::printStackTrace); +``` + +This means that `Observable<Void>` can no longer emit any values but only terminate normally or with an exception. API designers may instead choose to define `Observable<Object>` with no guarantee on what `Object` will be (which should be irrelevant anyway). For example, if one needs a signaller-like source, a shared enum can be defined and its solo instance `onNext`'d: + +```java +enum Irrelevant { INSTANCE; } + +Observable<Object> source = Observable.create((ObservableEmitter<Object> emitter) -> { + System.out.println("Side-effect 1"); + emitter.onNext(Irrelevant.INSTANCE); + + System.out.println("Side-effect 2"); + emitter.onNext(Irrelevant.INSTANCE); + + System.out.println("Side-effect 3"); + emitter.onNext(Irrelevant.INSTANCE); +}); + +source.subscribe(e -> { /* Ignored. */ }, Throwable::printStackTrace); +``` + +# Observable and Flowable + +A small regret about introducing backpressure in RxJava 0.x is that instead of having a separate base reactive class, the `Observable` itself was retrofitted. The main issue with backpressure is that many hot sources, such as UI events, can't be reasonably backpressured and cause unexpected `MissingBackpressureException` (i.e., beginners don't expect them). + +We try to remedy this situation in 2.x by having `io.reactivex.Observable` non-backpressured and the new `io.reactivex.Flowable` be the backpressure-enabled base reactive class. + +The good news is that operator names remain (mostly) the same. The bad news is that one should be careful when performing 'organize imports' as it may select the non-backpressured `io.reactivex.Observable` unintended. + +## Which type to use? + +When architecting dataflows (as an end-consumer of RxJava) or deciding upon what type your 2.x compatible library should take and return, you can consider a few factors that should help you avoid problems down the line such as `MissingBackpressureException` or `OutOfMemoryError`. + +### When to use Observable + + - You have a flow of no more than 1000 elements at its longest: i.e., you have so few elements over time that there is practically no chance for OOME in your application. + - You deal with GUI events such as mouse moves or touch events: these can rarely be backpressured reasonably and aren't that frequent. You may be able to handle an element frequency of 1000 Hz or less with Observable but consider using sampling/debouncing anyway. + - Your flow is essentially synchronous but your platform doesn't support Java Streams or you miss features from it. Using `Observable` has lower overhead in general than `Flowable`. *(You could also consider IxJava which is optimized for Iterable flows supporting Java 6+)*. + +### When to use Flowable + + - Dealing with 10k+ of elements that are generated in some fashion somewhere and thus the chain can tell the source to limit the amount it generates. + - Reading (parsing) files from disk is inherently blocking and pull-based which works well with backpressure as you control, for example, how many lines you read from this for a specified request amount). + - Reading from a database through JDBC is also blocking and pull-based and is controlled by you by calling `ResultSet.next()` for likely each downstream request. + - Network (Streaming) IO where either the network helps or the protocol used supports requesting some logical amount. + - Many blocking and/or pull-based data sources which may eventually get a non-blocking reactive API/driver in the future. + + +# Single + +The 2.x `Single` reactive base type, which can emit a single `onSuccess` or `onError` has been redesigned from scratch. Its architecture now derives from the Reactive-Streams design. Its consumer type (`rx.Single.SingleSubscriber<T>`) has been changed from being a class that accepts `rx.Subscription` resources to be an interface `io.reactivex.SingleObserver<T>` that has only 3 methods: + +```java +interface SingleObserver<T> { + void onSubscribe(Disposable d); + void onSuccess(T value); + void onError(Throwable error); +} +``` + +and follows the protocol `onSubscribe (onSuccess | onError)?`. + +# Completable + +The `Completable` type remains largely the same. It was already designed along the Reactive-Streams style for 1.x so no user-level changes there. + +Similar to the naming changes, `rx.Completable.CompletableSubscriber` has become `io.reactivex.CompletableObserver` with `onSubscribe(Disposable)`: + +```java +interface CompletableObserver<T> { + void onSubscribe(Disposable d); + void onComplete(); + void onError(Throwable error); +} +``` + +and still follows the protocol `onSubscribe (onComplete | onError)?`. + +# Maybe + +RxJava 2.0.0-RC2 introduced a new base reactive type called `Maybe`. Conceptually, it is a union of `Single` and `Completable` providing the means to capture an emission pattern where there could be 0 or 1 item or an error signalled by some reactive source. + +The `Maybe` class is accompanied by `MaybeSource` as its base interface type, `MaybeObserver<T>` as its signal-receiving interface and follows the protocol `onSubscribe (onSuccess | onError | onComplete)?`. Because there could be at most 1 element emitted, the `Maybe` type has no notion of backpressure (because there is no buffer bloat possible as with unknown length `Flowable`s or `Observable`s. + +This means that an invocation of `onSubscribe(Disposable)` is potentially followed by one of the other `onXXX` methods. Unlike `Flowable`, if there is only a single value to be signalled, only `onSuccess` is called and `onComplete` is not. + +Working with this new base reactive type is practically the same as the others as it offers a modest subset of the `Flowable` operators that make sense with a 0 or 1 item sequence. + +```java +Maybe.just(1) +.map(v -> v + 1) +.filter(v -> v == 1) +.defaultIfEmpty(2) +.test() +.assertResult(2); +``` + +# Base reactive interfaces + +Following the style of extending the Reactive-Streams `Publisher<T>` in `Flowable`, the other base reactive classes now extend similar base interfaces (in package `io.reactivex`): + +```java +interface ObservableSource<T> { + void subscribe(Observer<? super T> observer); +} + +interface SingleSource<T> { + void subscribe(SingleObserver<? super T> observer); +} + +interface CompletableSource { + void subscribe(CompletableObserver observer); +} + +interface MaybeSource<T> { + void subscribe(MaybeObserver<? super T> observer); +} +``` + +Therefore, many operators that required some reactive base type from the user now accept `Publisher` and `XSource`: + +```java +Flowable<R> flatMap(Function<? super T, ? extends Publisher<? extends R>> mapper); + +Observable<R> flatMap(Function<? super T, ? extends ObservableSource<? extends R>> mapper); +``` + +By having `Publisher` as input this way, you can compose with other Reactive-Streams compliant libraries without the need to wrap them or convert them into `Flowable` first. + +If an operator has to offer a reactive base type, however, the user will receive the full reactive class (as giving out an `XSource` is practically useless as it doesn't have operators on it): + +```java +Flowable<Flowable<Integer>> windows = source.window(5); + +source.compose((Flowable<T> flowable) -> + flowable + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread())); +``` + +# Subjects and Processors + +In the Reactive-Streams specification, the `Subject`-like behavior, namely being a consumer and supplier of events at the same time, is done by the `org.reactivestreams.Processor` interface. As with the `Observable`/`Flowable` split, the backpressure-aware, Reactive-Streams compliant implementations are based on the `FlowableProcessor<T>` class (which extends `Flowable` to give a rich set of instance operators). An important change regarding `Subject`s (and by extension, `FlowableProcessor`) that they no longer support `T -> R` like conversion (that is, input is of type `T` and the output is of type `R`). (We never had a use for it in 1.x and the original `Subject<T, R>` came from .NET where there is a `Subject<T>` overload because .NET allows the same class name with a different number of type arguments.) + +The `io.reactivex.subjects.AsyncSubject`, `io.reactivex.subjects.BehaviorSubject`, `io.reactivex.subjects.PublishSubject`, `io.reactivex.subjects.ReplaySubject` and `io.reactivex.subjects.UnicastSubject` in 2.x don't support backpressure (as part of the 2.x `Observable` family). + +The `io.reactivex.processors.AsyncProcessor`, `io.reactivex.processors.BehaviorProcessor`, `io.reactivex.processors.PublishProcessor`, `io.reactivex.processors.ReplayProcessor` and `io.reactivex.processors.UnicastProcessor` are backpressure-aware. The `BehaviorProcessor` and `PublishProcessor` don't coordinate requests (use `Flowable.publish()` for that) of their downstream subscribers and will signal them `MissingBackpressureException` if the downstream can't keep up. The other `XProcessor` types honor backpressure of their downstream subscribers but otherwise, when subscribed to a source (optional), they consume it in an unbounded manner (requesting `Long.MAX_VALUE`). + +## TestSubject + +The 1.x `TestSubject` has been dropped. Its functionality can be achieved via `TestScheduler`, `PublishProcessor`/`PublishSubject` and `observeOn(testScheduler)`/scheduler parameter. + +```java +TestScheduler scheduler = new TestScheduler(); +PublishSubject<Integer> ps = PublishSubject.create(); + +TestObserver<Integer> ts = ps.delay(1000, TimeUnit.MILLISECONDS, scheduler) +.test(); + +ts.assertEmpty(); + +ps.onNext(1); + +scheduler.advanceTimeBy(999, TimeUnit.MILLISECONDS); + +ts.assertEmpty(); + +scheduler.advanceTimeBy(1, TimeUnit.MILLISECONDS); + +ts.assertValue(1); +``` + +## SerializedSubject + +The `SerializedSubject` is no longer a public class. You have to use `Subject.toSerialized()` and `FlowableProcessor.toSerialized()` instead. + +# Other classes + +The `rx.observables.ConnectableObservable` is now `io.reactivex.observables.ConnectableObservable<T>` and `io.reactivex.flowables.ConnectableFlowable<T>`. + +## GroupedObservable + +The `rx.observables.GroupedObservable` is now `io.reactivex.observables.GroupedObservable<T>` and `io.reactivex.flowables.GroupedFlowable<T>`. + +In 1.x, you could create an instance with `GroupedObservable.from()` which was used internally by 1.x. In 2.x, all use cases now extend `GroupedObservable` directly thus the factory methods are no longer available; the whole class is now abstract. + +You can extend the class and add your own custom `subscribeActual` behavior to achieve something similar to the 1.x features: + +```java +class MyGroup<K, V> extends GroupedObservable<K, V> { + final K key; + + final Subject<V> subject; + + public MyGroup(K key) { + this.key = key; + this.subject = PublishSubject.create(); + } + + @Override + public T getKey() { + return key; + } + + @Override + protected void subscribeActual(Observer<? super T> observer) { + subject.subscribe(observer); + } +} +``` + +(The same approach works with `GroupedFlowable` as well.) + +# Functional interfaces + +Because both 1.x and 2.x is aimed at Java 6+, we can't use the Java 8 functional interfaces such as `java.util.function.Function`. Instead, we defined our own functional interfaces in 1.x and 2.x follows this tradition. + +One notable difference is that all our functional interfaces now define `throws Exception`. This is a large convenience for consumers and mappers that otherwise throw and would need `try-catch` to transform or suppress a checked exception. + +```java +Flowable.just("file.txt") +.map(name -> Files.readLines(name)) +.subscribe(lines -> System.out.println(lines.size()), Throwable::printStackTrace); +``` + +If the file doesn't exist or can't be read properly, the end consumer will print out `IOException` directly. Note also the `Files.readLines(name)` invoked without try-catch. + +## Actions + +As the opportunity to reduce component count, 2.x doesn't define `Action3`-`Action9` and `ActionN` (these were unused within RxJava itself anyway). + +The remaining action interfaces were named according to the Java 8 functional types. The no argument `Action0` is replaced by the `io.reactivex.functions.Action` for the operators and `java.lang.Runnable` for the `Scheduler` methods. `Action1` has been renamed to `Consumer` and `Action2` is called `BiConsumer`. `ActionN` is replaced by the `Consumer<Object[]>` type declaration. + +## Functions + +We followed the naming convention of Java 8 by defining `io.reactivex.functions.Function` and `io.reactivex.functions.BiFunction`, plus renaming `Func3` - `Func9` into `Function3` - `Function9` respectively. The `FuncN` is replaced by the `Function<Object[], R>` type declaration. + +In addition, operators requiring a predicate no longer use `Func1<T, Boolean>` but have a separate, primitive-returning type of `Predicate<T>` (allows better inlining due to no autoboxing). + +# Subscriber + +The Reactive-Streams specification has its own Subscriber as an interface. This interface is lightweight and combines request management with cancellation into a single interface `org.reactivestreams.Subscription` instead of having `rx.Producer` and `rx.Subscription` separately. This allows creating stream consumers with less internal state than the quite heavy `rx.Subscriber` of 1.x. + +```java +Flowable.range(1, 10).subscribe(new Subscriber<Integer>() { + @Override + public void onSubscribe(Subscription s) { + s.request(Long.MAX_VALUE); + } + + @Override + public void onNext(Integer t) { + System.out.println(t); + } + + @Override + public void onError(Throwable t) { + t.printStackTrace(); + } + + @Override + public void onComplete() { + System.out.println("Done"); + } +}); +``` + +Due to the name conflict, replacing the package from `rx` to `org.reactivestreams` is not enough. In addition, `org.reactivestreams.Subscriber` has no notion of adding resources to it, cancelling it or requesting from the outside. + +To bridge the gap we defined abstract classes `DefaultSubscriber`, `ResourceSubscriber` and `DisposableSubscriber` (plus their `XObserver` variants) for `Flowable` (and `Observable`) respectively that offers resource tracking support (of `Disposable`s) just like `rx.Subscriber` and can be cancelled/disposed externally via `dispose()`: + +```java +ResourceSubscriber<Integer> subscriber = new ResourceSubscriber<Integer>() { + @Override + public void onStart() { + request(Long.MAX_VALUE); + } + + @Override + public void onNext(Integer t) { + System.out.println(t); + } + + @Override + public void onError(Throwable t) { + t.printStackTrace(); + } + + @Override + public void onComplete() { + System.out.println("Done"); + } +}; + +Flowable.range(1, 10).delay(1, TimeUnit.SECONDS).subscribe(subscriber); + +subscriber.dispose(); +``` + +Note also that due to Reactive-Streams compatibility, the method `onCompleted` has been renamed to `onComplete` without the trailing `d`. + +Since 1.x `Observable.subscribe(Subscriber)` returned `Subscription`, users often added the `Subscription` to a `CompositeSubscription` for example: + +```java +CompositeSubscription composite = new CompositeSubscription(); + +composite.add(Observable.range(1, 5).subscribe(new TestSubscriber<Integer>())); +``` + +Due to the Reactive-Streams specification, `Publisher.subscribe` returns void and the pattern by itself no longer works in 2.0. To remedy this, the method `E subscribeWith(E subscriber)` has been added to each base reactive class which returns its input subscriber/observer as is. With the two examples before, the 2.x code can now look like this since `ResourceSubscriber` implements `Disposable` directly: + +```java +CompositeDisposable composite2 = new CompositeDisposable(); + +composite2.add(Flowable.range(1, 5).subscribeWith(subscriber)); +``` + +### Calling request from onSubscribe/onStart + +Note that due to how request management works, calling `request(n)` from `Subscriber.onSubscribe` or `ResourceSubscriber.onStart` may trigger calls to `onNext` immediately before the `request()` call itself returns to the `onSubscribe`/`onStart` method of yours: + +```java +Flowable.range(1, 3).subscribe(new Subscriber<Integer>() { + + @Override + public void onSubscribe(Subscription s) { + System.out.println("OnSubscribe start"); + s.request(Long.MAX_VALUE); + System.out.println("OnSubscribe end"); + } + + @Override + public void onNext(Integer v) { + System.out.println(v); + } + + @Override + public void onError(Throwable e) { + e.printStackTrace(); + } + + @Override + public void onComplete() { + System.out.println("Done"); + } +}); +``` + +This will print: + +``` +OnSubscribe start +1 +2 +3 +Done +OnSubscribe end +``` + +The problem comes when one does some initialization in `onSubscribe`/`onStart` after calling `request` there and `onNext` may or may not see the effects of the initialization. To avoid this situation, make sure you call `request` **after** all initialization have been done in `onSubscribe`/`onStart`. + +This behavior differs from 1.x where a `request` call went through a deferred logic that accumulated requests until an upstream `Producer` arrived at some time (This nature adds overhead to all operators and consumers in 1.x.) In 2.x, there is always a `Subscription` coming down first and 90% of the time there is no need to defer requesting. + +# Subscription + +In RxJava 1.x, the interface `rx.Subscription` was responsible for stream and resource lifecycle management, namely unsubscribing a sequence and releasing general resources such as scheduled tasks. The Reactive-Streams specification took this name for specifying an interaction point between a source and a consumer: `org.reactivestreams.Subscription` allows requesting a positive amount from the upstream and allows cancelling the sequence. + +To avoid the name clash, the 1.x `rx.Subscription` has been renamed into `io.reactivex.Disposable` (somewhat resembling .NET's own IDisposable). + +Because Reactive-Streams base interface, `org.reactivestreams.Publisher` defines the `subscribe()` method as `void`, `Flowable.subscribe(Subscriber)` no longer returns any `Subscription` (or `Disposable`). The other base reactive types also follow this signature with their respective subscriber types. + +The other overloads of `subscribe` now return `Disposable` in 2.x. + +The original `Subscription` container types have been renamed and updated + + - `CompositeSubscription` to `CompositeDisposable` + - `SerialSubscription` and `MultipleAssignmentSubscription` have been merged into `SerialDisposable`. The `set()` method disposes the old value and `replace()` method does not. + - `RefCountSubscription` has been removed. + +# Backpressure + +The Reactive-Streams specification mandates operators supporting backpressure, specifically via the guarantee that they don't overflow their consumers when those don't request. Operators of the new `Flowable` base reactive type now consider downstream request amounts properly, however, this doesn't mean `MissingBackpressureException` is gone. The exception is still there but this time, the operator that can't signal more `onNext` will signal this exception instead (allowing better identification of who is not properly backpressured). + +As an alternative, the 2.x `Observable` doesn't do backpressure at all and is available as a choice to switch over. + +# Reactive-Streams compliance + +**updated in 2.0.7** + +**The `Flowable`-based sources and operators are, as of 2.0.7, fully Reactive-Streams version 1.0.0 specification compliant.** + +Before 2.0.7, the operator `strict()` had to be applied in order to achieve the same level of compliance. In 2.0.7, the operator `strict()` returns `this`, is deprecated and will be removed completely in 2.1.0. + +As one of the primary goals of RxJava 2, the design focuses on performance and in order enable it, RxJava 2.0.7 adds a custom `io.reactivex.FlowableSubscriber` interface (extends `org.reactivestreams.Subscriber`) but adds no new methods to it. The new interface is **constrained to RxJava 2** and represents a consumer to `Flowable` that is able to work in a mode that relaxes the Reactive-Streams version 1.0.0 specification in rules §1.3, §2.3, §2.12 and §3.9: + + - §1.3 relaxation: `onSubscribe` may run concurrently with `onNext` in case the `FlowableSubscriber` calls `request()` from inside `onSubscribe` and it is the responsibility of `FlowableSubscriber` to ensure thread-safety between the remaining instructions in `onSubscribe` and `onNext`. + - §2.3 relaxation: calling `Subscription.cancel` and `Subscription.request` from `FlowableSubscriber.onComplete()` or `FlowableSubscriber.onError()` is considered a no-operation. + - §2.12 relaxation: if the same `FlowableSubscriber` instance is subscribed to multiple sources, it must ensure its `onXXX` methods remain thread safe. + - §3.9 relaxation: issuing a non-positive `request()` will not stop the current stream but signal an error via `RxJavaPlugins.onError`. + +From a user's perspective, if one was using the `subscribe` methods other than `Flowable.subscribe(Subscriber<? super T>)`, there is no need to do anything regarding this change and there is no extra penalty for it. + +If one was using `Flowable.subscribe(Subscriber<? super T>)` with the built-in RxJava `Subscriber` implementations such as `DisposableSubscriber`, `TestSubscriber` and `ResourceSubscriber`, there is a small runtime overhead (one `instanceof` check) associated when the code is not recompiled against 2.0.7. + +If a custom class implementing `Subscriber` was employed before, subscribing it to a `Flowable` adds an internal wrapper that ensures observing the Flowable is 100% compliant with the specification at the cost of some per-item overhead. + +In order to help lift these extra overheads, a new method `Flowable.subscribe(FlowableSubscriber<? super T>)` has been added which exposes the original behavior from before 2.0.7. It is recommended that new custom consumer implementations extend `FlowableSubscriber` instead of just `Subscriber`. + +# Runtime hooks + +The 2.x redesigned the `RxJavaPlugins` class which now supports changing the hooks at runtime. Tests that want to override the schedulers and the lifecycle of the base reactive types can do it on a case-by-case basis through callback functions. + +The class-based `RxJavaObservableHook` and friends are now gone and `RxJavaHooks` functionality is incorporated into `RxJavaPlugins`. + +# Error handling + +One important design requirement for 2.x is that no `Throwable` errors should be swallowed. This means errors that can't be emitted because the downstream's lifecycle already reached its terminal state or the downstream cancelled a sequence which was about to emit an error. + +Such errors are routed to the `RxJavaPlugins.onError` handler. This handler can be overridden with the method `RxJavaPlugins.setErrorHandler(Consumer<Throwable>)`. Without a specific handler, RxJava defaults to printing the `Throwable`'s stacktrace to the console and calls the current thread's uncaught exception handler. + +On desktop Java, this latter handler does nothing on an `ExecutorService` backed `Scheduler` and the application can keep running. However, Android is more strict and terminates the application in such uncaught exception cases. + +If this behavior is desirable can be debated, but in any case, if you want to avoid such calls to the uncaught exception handler, the **final application** that uses RxJava 2 (directly or transitively) should set a no-op handler: + +```java +// If Java 8 lambdas are supported +RxJavaPlugins.setErrorHandler(e -> { }); + +// If no Retrolambda or Jack +RxJavaPlugins.setErrorHandler(Functions.<Throwable>emptyConsumer()); +``` +It is not advised intermediate libraries change the error handler outside their own testing environment. + +Unfortunately, RxJava can't tell which of these out-of-lifecycle, undeliverable exceptions should or shouldn't crash your app. Identifying the source and reason for these exceptions can be tiresome, especially if they originate from a source and get routed to `RxJavaPlugins.onError` somewhere lower the chain. + +Therefore, 2.0.6 introduces specific exception wrappers to help distinguish and track down what was happening the time of the error: +- `OnErrorNotImplementedException`: reintroduced to detect when the user forgot to add error handling to `subscribe()`. +- `ProtocolViolationException`: indicates a bug in an operator +- `UndeliverableException`: wraps the original exception that can't be delivered due to lifecycle restrictions on a `Subscriber`/`Observer`. It is automatically applied by `RxJavaPlugins.onError` with intact stacktrace that may help find which exact operator rerouted the original error. + +If an undeliverable exception is an instance/descendant of `NullPointerException`, `IllegalStateException` (`UndeliverableException` and `ProtocolViolationException` extend this), `IllegalArgumentException`, `CompositeException`, `MissingBackpressureException` or `OnErrorNotImplementedException`, the `UndeliverableException` wrapping doesn't happen. + +In addition, some 3rd party libraries/code throw when they get interrupted by a cancel/dispose call which leads to an undeliverable exception most of the time. Internal changes in 2.0.6 now consistently cancel or dispose a `Subscription`/`Disposable` before cancelling/disposing a task or worker (which causes the interrupt on the target thread). + +``` java +// in some library +try { + doSomethingBlockingly() +} catch (InterruptedException ex) { + // check if the interrupt is due to cancellation + // if so, no need to signal the InterruptedException + if (!disposable.isDisposed()) { + observer.onError(ex); + } +} +``` + +If the library/code already did this, the undeliverable `InterruptedException`s should stop now. If this pattern was not employed before, we encourage updating the code/library in question. + +If one decides to add a non-empty global error consumer, here is an example that manages the typical undeliverable exceptions based on whether they represent a likely bug or an ignorable application/network state: + +```java +RxJavaPlugins.setErrorHandler(e -> { + if (e instanceof UndeliverableException) { + e = e.getCause(); + } + if ((e instanceof IOException) || (e instanceof SocketException)) { + // fine, irrelevant network problem or API that throws on cancellation + return; + } + if (e instanceof InterruptedException) { + // fine, some blocking code was interrupted by a dispose call + return; + } + if ((e instanceof NullPointerException) || (e instanceof IllegalArgumentException)) { + // that's likely a bug in the application + Thread.currentThread().getUncaughtExceptionHandler() + .handleException(Thread.currentThread(), e); + return; + } + if (e instanceof IllegalStateException) { + // that's a bug in RxJava or in a custom operator + Thread.currentThread().getUncaughtExceptionHandler() + .handleException(Thread.currentThread(), e); + return; + } + Log.warning("Undeliverable exception received, not sure what to do", e); +}); +``` + +# Schedulers + +The 2.x API still supports the main default scheduler types: `computation`, `io`, `newThread` and `trampoline`, accessible through `io.reactivex.schedulers.Schedulers` utility class. + +The `immediate` scheduler is not present in 2.x. It was frequently misused and didn't implement the `Scheduler` specification correctly anyway; it contained blocking sleep for delayed action and didn't support recursive scheduling at all. Use `Schedulers.trampoline()` instead. + +The `Schedulers.test()` has been removed as well to avoid the conceptional difference with the rest of the default schedulers. Those return a "global" scheduler instance whereas `test()` returned always a new instance of the `TestScheduler`. Test developers are now encouraged to simply `new TestScheduler()` in their code. + +The `io.reactivex.Scheduler` abstract base class now supports scheduling tasks directly without the need to create and then destroy a `Worker` (which is often forgotten): + +```java +public abstract class Scheduler { + + public Disposable scheduleDirect(Runnable task) { ... } + + public Disposable scheduleDirect(Runnable task, long delay, TimeUnit unit) { ... } + + public Disposable scheduleDirectPeriodically(Runnable task, long initialDelay, + long period, TimeUnit unit) { ... } + + public long now(TimeUnit unit) { ... } + + // ... rest is the same: lifecycle methods, worker creation +} +``` + +The main purpose is to avoid the tracking overhead of the `Worker`s for typically one-shot tasks. The methods have a default implementation that reuses `createWorker` properly but can be overridden with more efficient implementations if necessary. + +The method that returns the scheduler's own notion of current time, `now()` has been changed to accept a `TimeUnit` to indicate the unit of measure. + +# Entering the reactive world + +One of the design flaws of RxJava 1.x was the exposure of the `rx.Observable.create()` method that while powerful, not the typical operator you want to use to enter the reactive world. Unfortunately, so many depend on it that we couldn't remove or rename it. + +Since 2.x is a fresh start, we won't make that mistake again. Each reactive base type `Flowable`, `Observable`, `Single`, `Maybe` and `Completable` feature a safe `create` operator that does the right thing regarding backpressure (for `Flowable`) and cancellation (all): + +```java +Flowable.create((FlowableEmitter<Integer> emitter) -> { + emitter.onNext(1); + emitter.onNext(2); + emitter.onComplete(); +}, BackpressureStrategy.BUFFER); +``` + +Practically, the 1.x `fromEmitter` (formerly `fromAsync`) has been renamed to `Flowable.create`. The other base reactive types have similar `create` methods (minus the backpressure strategy). + +# Leaving the reactive world + +Apart from subscribing to the base types with their respective consumers (`Subscriber`, `Observer`, `SingleObserver`, `MaybeObserver` and `CompletableObserver`) and functional-interface based consumers (such as `subscribe(Consumer<T>, Consumer<Throwable>, Action)`), the formerly separate 1.x `BlockingObservable` (and similar classes for the others) has been integrated with the main reactive type. Now you can directly block for some results by invoking a `blockingX` operation directly: + +```java +List<Integer> list = Flowable.range(1, 100).toList().blockingGet(); // toList() returns Single + +Integer i = Flowable.range(100, 100).blockingLast(); +``` + +(The reason for this is twofold: performance and ease of use of the library as a synchronous Java 8 Streams-like processor.) + +Another significant difference between `rx.Subscriber` (and co) and `org.reactivestreams.Subscriber` (and co) is that in 2.x, your `Subscriber`s and `Observer`s are not allowed to throw anything but fatal exceptions (see `Exceptions.throwIfFatal()`). (The Reactive-Streams specification allows throwing `NullPointerException` if the `onSubscribe`, `onNext` or `onError` receives a `null` value, but RxJava doesn't let `null`s in any way.) This means the following code is no longer legal: + +```java +Subscriber<Integer> subscriber = new Subscriber<Integer>() { + @Override + public void onSubscribe(Subscription s) { + s.request(Long.MAX_VALUE); + } + + public void onNext(Integer t) { + if (t == 1) { + throw new IllegalArgumentException(); + } + } + + public void onError(Throwable e) { + if (e instanceof IllegalArgumentException) { + throw new UnsupportedOperationException(); + } + } + + public void onComplete() { + throw new NoSuchElementException(); + } +}; + +Flowable.just(1).subscribe(subscriber); +``` + +The same applies to `Observer`, `SingleObserver`, `MaybeObserver` and `CompletableObserver`. + +Since many of the existing code targeting 1.x do such things, the method `safeSubscribe` has been introduced that does handle these non-conforming consumers. + +Alternatively, you can use the `subscribe(Consumer<T>, Consumer<Throwable>, Action)` (and similar) methods to provide a callback/lambda that can throw: + +```java +Flowable.just(1) +.subscribe( + subscriber::onNext, + subscriber::onError, + subscriber::onComplete, + subscriber::onSubscribe +); +``` + +# Testing + +Testing RxJava 2.x works the same way as it does in 1.x. `Flowable` can be tested with `io.reactivex.subscribers.TestSubscriber` whereas the non-backpressured `Observable`, `Single`, `Maybe` and `Completable` can be tested with `io.reactivex.observers.TestObserver`. + +## test() "operator" + +To support our internal testing, all base reactive types now feature `test()` methods (which is a huge convenience for us) returning `TestSubscriber` or `TestObserver`: + +```java +TestSubscriber<Integer> ts = Flowable.range(1, 5).test(); + +TestObserver<Integer> to = Observable.range(1, 5).test(); + +TestObserver<Integer> tso = Single.just(1).test(); + +TestObserver<Integer> tmo = Maybe.just(1).test(); + +TestObserver<Integer> tco = Completable.complete().test(); +``` + +The second convenience is that most `TestSubscriber`/`TestObserver` methods return the instance itself allowing chaining the various `assertX` methods. The third convenience is that you can now fluently test your sources without the need to create or introduce `TestSubscriber`/`TestObserver` instance in your code: + +```java +Flowable.range(1, 5) +.test() +.assertResult(1, 2, 3, 4, 5) +; +``` + +### Notable new assert methods + + - `assertResult(T... items)`: asserts if subscribed, received exactly the given items in the given order followed by `onComplete` and no errors + - `assertFailure(Class<? extends Throwable> clazz, T... items)`: asserts if subscribed, received exactly the given items in the given order followed by a `Throwable` error of wich `clazz.isInstance()` returns true. + - `assertFailureAndMessage(Class<? extends Throwable> clazz, String message, T... items)`: same as `assertFailure` plus validates the `getMessage()` contains the specified message + - `awaitDone(long time, TimeUnit unit)` awaits a terminal event (blockingly) and cancels the sequence if the timeout elapsed. + - `assertOf(Consumer<TestSubscriber<T>> consumer)` compose some assertions into the fluent chain (used internally for fusion test as operator fusion is not part of the public API right now). + +One of the benefits is that changing `Flowable` to `Observable` here the test code part doesn't have to change at all due to the implicit type change of the `TestSubscriber` to `TestObserver`. + +## cancel and request upfront + +The `test()` method on `TestObserver` has a `test(boolean cancel)` overload which cancels/disposes the `TestSubscriber`/`TestObserver` before it even gets subscribed: + +```java +PublishSubject<Integer> pp = PublishSubject.create(); + +// nobody subscribed yet +assertFalse(pp.hasSubscribers()); + +pp.test(true); + +// nobody remained subscribed +assertFalse(pp.hasSubscribers()); +``` + +`TestSubscriber` has the `test(long initialRequest)` and `test(long initialRequest, boolean cancel)` overloads to specify the initial request amount and whether the `TestSubscriber` should be also immediately cancelled. If the `initialRequest` is given, the `TestSubscriber` offers the `requestMore(long)` method to keep requesting in a fluent manner: + +```java +Flowable.range(1, 5) +.test(0) +.assertValues() +.requestMore(1) +.assertValues(1) +.requestMore(2) +.assertValues(1, 2, 3) +.requestMore(2) +.assertResult(1, 2, 3, 4, 5); +``` + +or alternatively the `TestSubscriber` instance has to be captured to gain access to its `request()` method: + +```java +PublishProcessor<Integer> pp = PublishProcessor.create(); + +TestSubscriber<Integer> ts = pp.test(0L); + +ts.request(1); + +pp.onNext(1); +pp.onNext(2); + +ts.assertFailure(MissingBackpressureException.class, 1); +``` + +## Testing an async source + +Given an asynchronous source, fluent blocking for a terminal event is now possible: + +```java +Flowable.just(1) +.subscribeOn(Schedulers.single()) +.test() +.awaitDone(5, TimeUnit.SECONDS) +.assertResult(1); +``` + +## Mockito & TestSubscriber + +Those who are using Mockito and mocked `Observer` in 1.x has to mock the `Subscriber.onSubscribe` method to issue an initial request, otherwise, the sequence will hang or fail with hot sources: + +```java +@SuppressWarnings("unchecked") +public static <T> Subscriber<T> mockSubscriber() { + Subscriber<T> w = mock(Subscriber.class); + + Mockito.doAnswer(new Answer<Object>() { + @Override + public Object answer(InvocationOnMock a) throws Throwable { + Subscription s = a.getArgumentAt(0, Subscription.class); + s.request(Long.MAX_VALUE); + return null; + } + }).when(w).onSubscribe((Subscription)any()); + + return w; +} +``` + +# Operator differences + +Most operators are still there in 2.x and practically all of them have the same behavior as they had in 1.x. The following subsections list each base reactive type and the difference between 1.x and 2.x. + +Generally, many operators gained overloads that now allow specifying the internal buffer size or prefetch amount they should run their upstream (or inner sources). + +Some operator overloads have been renamed with a postfix, such as `fromArray`, `fromIterable` etc. The reason for this is that when the library is compiled with Java 8, the javac often can't disambiguate between functional interface types. + +Operators marked as `@Beta` or `@Experimental` in 1.x are promoted to standard. + +## 1.x Observable to 2.x Flowable + +### Factory methods: + +| 1.x | 2.x | +|----------|-----------| +| `amb` | added `amb(ObservableSource...)` overload, 2-9 argument versions dropped | +| RxRingBuffer.SIZE | `bufferSize()` | +| `combineLatest` | added varargs overload, added overloads with `bufferSize` argument, `combineLatest(List)` dropped | +| `concat` | added overload with `prefetch` argument, 5-9 source overloads dropped, use `concatArray` instead | +| N/A | added `concatArray` and `concatArrayDelayError` | +| N/A | added `concatArrayEager` and `concatArrayEagerDelayError` | +| `concatDelayError` | added overloads with option to delay till the current ends or till the very end | +| `concatEagerDelayError` | added overloads with option to delay till the current ends or till the very end | +| `create(SyncOnSubscribe)` | replaced with `generate` + overloads (distinct interfaces, you can implement them all at once) | +| `create(AsnycOnSubscribe)` | not present | +| `create(OnSubscribe)` | repurposed with safe `create(FlowableOnSubscribe, BackpressureStrategy)`, raw support via `unsafeCreate()` | +| `from` | disambiguated into `fromArray`, `fromIterable`, `fromFuture` | +| N/A | added `fromPublisher` | +| `fromAsync` | renamed to `create()` | +| N/A | added `intervalRange()` | +| `limit` | dropped, use `take` | +| `merge` | added overloads with `prefetch` | +| `mergeDelayError` | added overloads with `prefetch` | +| `sequenceEqual` | added overload with `bufferSize` | +| `switchOnNext` | added overload with `prefetch` | +| `switchOnNextDelayError` | added overload with `prefetch` | +| `timer` | deprecated overloads dropped | +| `zip` | added overloads with `bufferSize` and `delayErrors` capabilities, disambiguated to `zipArray` and `zipIterable` | + +### Instance methods: + +| 1.x | 2.x | +|----------|----------| +| `all` | **RC3** returns `Single<Boolean>` now | +| `any` | **RC3** returns `Single<Boolean>` now | +| `asObservable` | renamed to `hide()`, hides all identities now | +| `buffer` | overloads with custom `Collection` supplier | +| `cache(int)` | deprecated and dropped | +| `collect` | **RC3** returns `Single<U>` | +| `collect(U, Action2<U, T>)` | disambiguated to `collectInto` and **RC3** returns `Single<U>` | +| `concatMap` | added overloads with `prefetch` | +| `concatMapDelayError` | added overloads with `prefetch`, option to delay till the current ends or till the very end | +| `concatMapEager` | added overloads with `prefetch` | +| `concatMapEagerDelayError` | added overloads with `prefetch`, option to delay till the current ends or till the very end | `contains` | **RC3** returns `Single<Boolean> now | +| `count` | **RC3** returns `Single<Long>` now | +| `countLong` | dropped, use `count` | +| `distinct` | overload with custom `Collection` supplier. | +| `doOnCompleted` | renamed to `doOnComplete`, note the missing `d`! | +| `doOnUnsubscribe` | renamed to `Flowable.doOnCancel` and `doOnDispose` for the others, [additional info](https://github.com/ReactiveX/RxJava/wiki/What's-different-in-2.0#dooncanceldoondisposeunsubscribeon) | +| N/A | added `doOnLifecylce` to handle `onSubscribe`, `request` and `cancel` peeking | +| `elementAt(int)` | **RC3** no longer signals NoSuchElementException if the source is shorter than the index | +| `elementAt(Func1, int)` | dropped, use `filter(predicate).elementAt(int)` | +| `elementAtOrDefault(int, T)` | renamed to `elementAt(int, T)` and **RC3** returns `Single<T>` | +| `elementAtOrDefault(Func1, int, T)` | dropped, use `filter(predicate).elementAt(int, T)` | +| `first()` | **RC3** renamed to `firstElement` and returns `Maybe<T>` | +| `first(Func1)` | dropped, use `filter(predicate).first()` | +| `firstOrDefault(T)` | renamed to `first(T)` and **RC3** returns `Single<T>` | +| `firstOrDefault(Func1, T)` | dropped, use `filter(predicate).first(T)` | +| `flatMap` | added overloads with `prefetch` | +| N/A | added `forEachWhile(Predicate<T>, [Consumer<Throwable>, [Action]])` for conditionally stopping consumption | +| `groupBy` | added overload with `bufferSize` and `delayError` option, *the custom internal map version didn't make it into RC1* | +| `ignoreElements` | **RC3** returns `Completable` | +| `isEmpty` | **RC3** returns `Single<Boolean>` | +| `last()` | **RC3** renamed to `lastElement` and returns `Maybe<T>` | +| `last(Func1)` | dropped, use `filter(predicate).last()` | +| `lastOrDefault(T)` | renamed to `last(T)` and **RC3** returns `Single<T>` | +| `lastOrDefault(Func1, T)` | dropped, use `filter(predicate).last(T)` | +| `nest` | dropped, use manual `just` | +| `publish(Func1)` | added overload with `prefetch` | +| `reduce(Func2)` | **RC3** returns `Maybe<T>` | +| N/A | added `reduceWith(Callable, BiFunction)` to reduce in a Subscriber-individual manner, returns `Single<T>` | +| N/A | added `repeatUntil(BooleanSupplier)` | +| `repeatWhen(Func1, Scheduler)` | dropped the overload, use `subscribeOn(Scheduler).repeatWhen(Function)` instead | +| `retry` | added `retry(Predicate)`, `retry(int, Predicate)` | +| N/A | added `retryUntil(BooleanSupplier)` | +| `retryWhen(Func1, Scheduler)` | dropped the overload, use `subscribeOn(Scheduler).retryWhen(Function)` instead | +| `sample` | doesn't emit the very last item if the upstream completes within the period, added overloads with `emitLast` parameter | +| N/A | added `scanWith(Callable, BiFunction)` to scan in a Subscriber-individual manner | +| `single()` | **RC3** renamed to `singleElement` and returns `Maybe<T>` | +| `single(Func1)` | dropped, use `filter(predicate).single()` | +| `singleOrDefault(T)` | renamed to `single(T)` and **RC3** returns `Single<T>` | +| `singleOrDefault(Func1, T)` | dropped, use `filter(predicate).single(T)` | +| `skipLast` | added overloads with `bufferSize` and `delayError` options | +| `startWith` | 2-9 argument version dropped, use `startWithArray` instead | +| N/A | added `startWithArray` to disambiguate | +| `subscribe` | No longer wraps all consumer types (i.e., `Observer`) with a safety wrapper, (just like the 1.x `unsafeSubscribe` no longer available). Use `safeSubscribe` to get an explicit safety wrapper around a consumer type. | +| N/A | added `subscribeWith` that returns its input after subscription | +| `switchMap` | added overload with `prefetch` argument | +| `switchMapDelayError` | added overload with `prefetch` argument | +| `takeLastBuffer` | dropped | +| N/A | added `test()` (returns TestSubscriber subscribed to this) with overloads to fluently test | +| `throttleLast` | doesn't emit the very last item if the upstream completes within the period, use `sample` with the `emitLast` parameter | +| `timeout(Func0<Observable>, ...)` | signature changed to `timeout(Publisher, ...)` and dropped the function, use `defer(Callable<Publisher>>)` if necessary | +| `toBlocking().y` | inlined as `blockingY()` operators, except `toFuture` | +| `toCompletable` | **RC3** dropped, use `ignoreElements` | +| `toList` | **RC3** returns `Single<List<T>>` | +| `toMap` | **RC3** returns `Single<Map<K, V>>` | +| `toMultimap` | **RC3** returns `Single<Map<K, Collection<V>>>` | +| N/A | added `toFuture` | +| N/A | added `toObservable` | +| `toSingle` | **RC3** dropped, use `single(T)` | +| `toSortedList` | **RC3** returns `Single<List<T>>` | +| `unsafeSubscribe` | Removed as the Reactive Streams specification mandates the `onXXX` methods don't crash and therefore the default is to not have a safety net in `subscribe`. The new `safeSubscribe` method was introduced to explicitly add the safety wrapper around a consumer type. | +| `withLatestFrom` | 5-9 source overloads dropped | +| `zipWith` | added overloads with `prefetch` and `delayErrors` options | + +### Different return types + +Some operators that produced exactly one value or an error now return `Single` in 2.x (or `Maybe` if an empty source is allowed). + +*(Remark: this is "experimental" in RC2 and RC3 to see how it feels to program with such mixed-type sequences and whether or not there has to be too much `toObservable`/`toFlowable` back-conversion.)* + +| Operator | Old return type | New return type | Remark | +|----------|-----------------|-----------------|--------| +| `all(Predicate)` | `Observable<Boolean>` | `Single<Boolean>` | Emits true if all elements match the predicate | +| `any(Predicate)` | `Observable<Boolean>` | `Single<Boolean>` | Emits true if any elements match the predicate | +| `count()` | `Observable<Long>` | `Single<Long>` | Counts the number of elements in the sequence | +| `elementAt(int)` | `Observable<T>` | `Maybe<T>` | Emits the element at the given index or completes | +| `elementAt(int, T)` | `Observable<T>` | `Single<T>` | Emits the element at the given index or the default | +| `elementAtOrError(int)` | `Observable<T>` | `Single<T>` | Emits the indexth element or a `NoSuchElementException` | +| `first(T)` | `Observable<T>` | `Single<T>` | Emits the very first element or `NoSuchElementException` | +| `firstElement()` | `Observable<T>` | `Maybe<T>` | Emits the very first element or completes | +| `firstOrError()` | `Observable<T>` | `Single<T>` | Emits the first element or a `NoSuchElementException` if the source is empty | +| `ignoreElements()` | `Observable<T>` | `Completable` | Ignore all but the terminal events | +| `isEmpty()` | `Observable<Boolean>` | `Single<Boolean>` | Emits true if the source is empty | +| `last(T)` | `Observable<T>` | `Single<T>` | Emits the very last element or the default item | +| `lastElement()` | `Observable<T>` | `Maybe<T>` | Emits the very last element or completes | +| `lastOrError()` | `Observable<T>` | `Single<T>` | Emits the lastelement or a `NoSuchElementException` if the source is empty | +| `reduce(BiFunction)` | `Observable<T>` | `Maybe<T>` | Emits the reduced value or completes | +| `reduce(Callable, BiFunction)` | `Observable<U>` | `Single<U>` | Emits the reduced value (or the initial value) | +| `reduceWith(U, BiFunction)` | `Observable<U>` | `Single<U>` | Emits the reduced value (or the initial value) | +| `single(T)` | `Observable<T>` | `Single<T>` | Emits the only element or the default item | +| `singleElement()` | `Observable<T>` | `Maybe<T>` | Emits the only element or completes | +| `singleOrError()` | `Observable<T>` | `Single<T>` | Emits the one and only element, IndexOutOfBoundsException if the source is longer than 1 item or a `NoSuchElementException` if the source is empty | +| `toList()` | `Observable<List<T>>` | `Single<List<T>>` | collects all elements into a `List` | +| `toMap()` | `Observable<Map<K, V>>` | `Single<Map<K, V>>` | collects all elements into a `Map` | +| `toMultimap()` | `Observable<Map<K, Collection<V>>>` | `Single<Map<K, Collection<V>>>` | collects all elements into a `Map` with collection | +| `toSortedList()` | `Observable<List<T>>` | `Single<List<T>>` | collects all elements into a `List` and sorts it | + +### Removals + +To make sure the final API of 2.0 is clean as possible, we remove methods and other components between release candidates without deprecating them. + +| Removed in version | Component | Remark | +|---------|-----------|--------| +| RC3 | `Flowable.toCompletable()` | use `Flowable.ignoreElements()` | +| RC3 | `Flowable.toSingle()` | use `Flowable.single(T)` | +| RC3 | `Flowable.toMaybe()` | use `Flowable.singleElement()` | +| RC3 | `Observable.toCompletable()` | use `Observable.ignoreElements()` | +| RC3 | `Observable.toSingle()` | use `Observable.single(T)` | +| RC3 | `Observable.toMaybe()` | use `Observable.singleElement()` | + +# Miscellaneous changes + +## doOnCancel/doOnDispose/unsubscribeOn + +In 1.x, the `doOnUnsubscribe` was always executed on a terminal event because 1.x' `SafeSubscriber` called `unsubscribe` on itself. This was practically unnecessary and the Reactive-Streams specification states that when a terminal event arrives at a `Subscriber`, the upstream `Subscription` should be considered cancelled and thus calling `cancel()` is a no-op. + +For the same reason, `unsubscribeOn` is not called on the regular termination path but only when there is an actual `cancel` (or `dispose`) call on the chain. + +Therefore, the following sequence won't call `doOnCancel`: + +```java +Flowable.just(1, 2, 3) +.doOnCancel(() -> System.out.println("Cancelled!")) +.subscribe(System.out::println); +``` + +However, the following will call since the `take` operator cancels after the set amount of `onNext` events have been delivered: + +```java +Flowable.just(1, 2, 3) +.doOnCancel(() -> System.out.println("Cancelled!")) +.take(2) +.subscribe(System.out::println); +``` + +If you need to perform cleanup on both regular termination or cancellation, consider the operator `using` instead. + +Alternatively, the `doFinally` operator (introduced in 2.0.1 and standardized in 2.1) calls a developer specified `Action` that gets executed after a source completed, failed with an error or got cancelled/disposed: + +```java +Flowable.just(1, 2, 3) +.doFinally(() -> System.out.println("Finally")) +.subscribe(System.out::println); + +Flowable.just(1, 2, 3) +.doFinally(() -> System.out.println("Finally")) +.take(2) // cancels the above after 2 elements +.subscribe(System.out::println); +``` diff --git a/docs/What's-different-in-3.0.md b/docs/What's-different-in-3.0.md new file mode 100644 index 0000000000..5d66960847 --- /dev/null +++ b/docs/What's-different-in-3.0.md @@ -0,0 +1,80 @@ +Table of contents + +# Introduction +TBD. + +### API signature changes + +#### as() and to() operators + +In 2.x, the `to()` operator used the generic `Function` to allow assembly-time conversion of flows into arbitrary types. The drawback of this +approach was that each base reactive type had the same `Function` interface in their method signature, +thus it was impossible to implement multiple converters for different reactive types within the same class. +To work around this issue, the `as` operator and `XConverter` interfaces have been introduced +in 2.x, which interfaces are distinct and can be implemented on the same class. Changing the signature of `to` in 2.x was not possible due +to the pledged binary compatibility of the library. + +From 3.x, the `as()` methods have been removed and the `to()` methods now each work with their respective `XConverer` interfaces: + +- `Flowable.to(Function<Flowable<T>, R>)` is now `Flowable.to(FlowableConverter<T, R>)` +- `Observable.to(Function<Observable<T>, R>)` is now `Observable.to(ObservableConverter<T, R>)` +- `Maybe.to(Function<Flowable<T>, R>)` is now `Maybe.to(MaybeConverter<T, R>)` +- `Single.to(Function<Flowable<T>, R>)` is now `Maybe.to(SingleConverter<T, R>)` +- `Completable.to(Function<Completable, R>)` is now `Completable.to(CompletableConverter<R>)` +- `ParallelFlowable.to(Function<ParallelFlowable<T>, R)` is now `ParallelFlowable.to(ParallelFlowableConverter<T, R>)` + +If one was using these methods with a lambda expression, only a recompilation is needed: + +```java +// before +source.to(flowable -> flowable.blockingFirst()); + +// after +source.to(flowable -> flowable.blockingFirst()); +``` + +If one was implementing a Function interface (typically anonymously), the interface type, type arguments and the `throws` clause have to be adjusted + +```java +// before +source.to(new Function<Flowable<Integer>, Integer>() { + @Override + public Integer apply(Flowable<Integer> t) throws Exception { + return t.blockingFirst(); + } +}); + +// after +source.to(new FlowableConverter<Integer, Integer>() { + @Override + public Integer apply(Flowable<Integer> t) { + return t.blockingFirst(); + } +}); +``` + +TBD. + +- some operators returning a more appropriate Single or Maybe +- functional interfaces throws widening to Throwable +- standard methods removed +- standard methods signature changes + +### Standardized operators + +(former experimental and beta operators from 2.x) + +TBD. + +### Operator behavior changes + +TBD. + +- connectable sources lifecycle-fixes + + +### Test support changes + +TBD. + +- methods removed from the test consumers diff --git a/docs/Writing-operators-for-2.0.md b/docs/Writing-operators-for-2.0.md new file mode 100644 index 0000000000..1a51664880 --- /dev/null +++ b/docs/Writing-operators-for-2.0.md @@ -0,0 +1,1746 @@ +##### Table of contents + + - [Introduction](#introduction) + - [Warning on internal components](#warning-on-internal-components) + - [Atomics, serialization, deferred actions](#atomics-serialization-deferred-actions) + - [Field updaters and Android](#field-updaters-and-android) + - [Request accounting](#request-accounting) + - [Once](#once) + - [Serialization](#serialization) + - [Queues](#queues) + - [Deferred actions](#deferred-actions) + - [Deferred cancellation](#deferred-cancellation) + - [Deferred requesting](#deferred-requesting) + - [Atomic error management](#atomic-error-management) + - [Half-serialization](#half-serialization) + - [Fast-path queue-drain](#fast-path-queue-drain) + - [FlowableSubscriber](#flowablesubscriber) + - [Backpressure and cancellation](#backpressure-and-cancellation) + - [Replenishing](#replenishing) + - [Stable prefetching](#stable-prefetching) + - [Single-valued results](#single-valued-results) + - [Single-element post-complete](#single-element-post-complete) + - [Multi-element post-complete](#multi-element-post-complete) + - [Creating operator classes](#creating-operator-classes) + - [Operator by extending a base reactive class](#operator-by-extending-a-base-reactive-class) + - [Operator targeting lift()](#operator-targeting-lift) + - [Operator fusion](#operator-fusion) + - [Generations](#generations) + - [Components](#components) + - [Callable and ScalarCallable](#callable-and-scalarcallable) + - [ConditionalSubscriber](#conditionalsubscriber) + - [QueueSubscription and QueueDisposable](#queuesubscription-and-queuedisposable) + - [Example implementations](#example-implementations) + - [Map-filter hybrid](https://github.com/ReactiveX/RxJava/wiki/Writing-operators-for-2.0#map--filter-hybrid) + - [Ordered merge](#ordered-merge) + +# Introduction + +Writing operators, source-like (`fromEmitter`) or intermediate-like (`flatMap`) **has always been a hard task to do in RxJava**. There are many rules to obey, many cases to consider but at the same time, many (legal) shortcuts to take to build a well performing code. Now writing an operator specifically for 2.x is 10 times harder than for 1.x. If you want to exploit all the advanced, 4th generation features, that's even 2-3 times harder on top (so 30 times harder in total). + +*(If you have been following [my blog](http://akarnokd.blogspot.hu/) about RxJava internals, writing operators is maybe only 2 times harder than 1.x; some things have moved around, some tools popped up while others have been dropped but there is a relatively straight mapping from 1.x concepts and approaches to 2.x concepts and approaches.)* + +In this article, I'll describe the how-to's from the perspective of a developer who skipped the 1.x knowledge base and basically wants to write operators that conforms the Reactive-Streams specification as well as RxJava 2.x's own extensions and additional expectations/requirements. + +Since **Reactor 3** has the same architecture as **RxJava 2** (no accident, I architected and contributed 80% of **Reactor 3** as well) the same principles outlined in this page applies to writing operators for **Reactor 3**. Note however that they chose different naming and locations for their utility and support classes so you may have to search for the equivalent components. + +## Warning on internal components + +RxJava has several hundred public classes implementing various operators and helper facilities. Since there is no way to hide these in Java 6-8, the general contract is that anything below `io.reactivex.internal` is considered private and subject to change without warnings. It is not recommended to reference these in your code (unless you contribute to RxJava itself) and must be prepared that even a patch change may shuffle/rename things around in them. That being said, they usually contain valuable tools for operator builders and as such are quite attractive to use them in your custom code. + +# Atomics, serialization, deferred actions + +As RxJava itself has building blocks for creating reactive dataflows, its components have building blocks as well in the form of concurrency primitives and algorithms. Many refer to the book [Concurrency in Practice](https://www.amazon.com/Java-Concurrency-Practice-Brian-Goetz/dp/0321349601) for learning the fundamentals needed. Unfortunately, other than some explanation of the Java Memory Model, the book lacks the techniques required for developing operators for RxJava 1.x and 2.x. + +## Field updaters and Android + +If you looked at the source code of RxJava and then at **Reactor 3**, you might have noticed that RxJava doesn't use the +`AtomicXFieldUpdater` classes. The reason for this is that on certain Android devices, the runtime "messes up" the field +names and the reflection logic behind the field updaters fails to locate those fields in the operators. To avoid this we decided to only use the full `AtomicX` classes (as fields or extending them). + +If you target the RxJava library with your custom operator (or Android), you are encouraged to do the same. If you plan have operators running on desktop Java, feel free to use the field updaters instead. + +## Request accounting + +When dealing with backpressure in `Flowable` operators, one needs a way to account the downstream requests and emissions in response to those requests. For this we use a single `AtomicLong`. Accounting must be atomic because requesting more and emitting items to fulfill an earlier request may happen at the same time. + +The naive approach for accounting would be to simply call `AtomicLong.getAndAdd()` with new requests and `AtomicLong.addAndGet()` for decrementing based on how many elements were emitted. + +The problem with this is that the Reactive-Streams specification declares `Long.MAX_VALUE` as the upper bound for outstanding requests (interprets it as the unbounded mode) but adding two large longs may overflow the `long` into a negative value. In addition, if for some reason, there are more values emitted than were requested, the subtraction may yield a negative current request value, causing crashes or hangs. + +Therefore, both addition and subtraction have to be capped at `Long.MAX_VALUE` and `0` respectively. Since there is no dedicated `AtomicLong` method for it, we have to use a Compare-And-Set loop. (Usually, requesting happens relatively rarely compared to emission amounts thus the lack of dedicated machine code instruction is not a performance bottleneck.) + +```java +public static long add(AtomicLong requested, long n) { + for (;;) { + long current = requested.get(); + if (current == Long.MAX_VALUE) { + return Long.MAX_VALUE; + } + long update = current + n; + if (update < 0L) { + update = Long.MAX_VALUE; + } + if (requested.compareAndSet(current, update)) { + return current; + } + } +} + +public static long produced(AtomicLong requested, long n) { +for (;;) { + long current = requested.get(); + if (current == Long.MAX_VALUE) { + return Long.MAX_VALUE; + } + long update = current - n; + if (update < 0L) { + update = 0; + } + if (requested.compareAndSet(current, update)) { + return update; + } + } +} +``` + +In fact, these are so common in RxJava's operators that these algorithms are available as utility methods on the **internal** `BackpressureHelper` class under the same name. + +Sometimes, instead of having a separate `AtomicLong` field, your operator can extend `AtomicLong` saving on the indirection and class size. The practice in RxJava 2.x operators is that unless there is another atomic counter needed by the operator, (such as work-in-progress counter, see the later subsection) and otherwise doesn't need a base class, they extend `AtomicLong` directly. + +The `BackpressureHelper` class features special versions of `add` and `produced` which treat `Long.MIN_VALUE` as a cancellation indication and won't change the `AtomicLong`s value if they see it. + +## Once + +RxJava 2 expanded the single-event reactive types to include `Maybe` (called as the reactive `Optional` by some). The common property of `Single`, `Completable` and `Maybe` is that they can only call one of the 3 kinds of methods on their consumers: `(onSuccess | onError | onComplete)`. Since they also participate in concurrent scenarios, an operator needs a way to ensure that only one of them is called even though the input sources may call multiple of them at once. + +To ensure this, operators may use the `AtomicBoolean.compareAndSet` to atomically chose the event to relay (and thus the other events to drop). + +```java +final AtomicBoolean once = new AtomicBoolean(); + +final MaybeObserver<? super T> child = ...; + +void emitSuccess(T value) { + if (once.compareAndSet(false, true)) { + child.onSuccess(value); + } +} + +void emitFailure(Throwable e) { + if (once.compareAndSet(false, true)) { + child.onError(e); + } else { + RxJavaPlugins.onError(e); + } +} +``` + +Note that the same sequential requirement applies to these 0-1 reactive sources as to `Flowable`/`Observable`, therefore, if your operator doesn't have to deal with events from multiple sources (and pick one of them), you don't need this construct. + +## Serialization + +With more complicated sources, it may happen that multiple things happen that may trigger emission towards the downstream, such as upstream becoming available while the downstream requests for more data while the sequence gets cancelled by a timeout. + +Instead of working out the often very complicated state transitions via atomics, perhaps the easiest way is to serialize the events, actions or tasks and have one thread perform the necessary steps after that. This is what I call **queue-drain** approach (or trampolining by some). + +(The other approach, **emitter-loop** is no longer recommended with 2.x due to its potential blocking `synchronized` constructs that looks performant in single-threaded case but destroys it in true concurrent case.) + + +The concept is relatively simple: have a concurrent queue and a work-in-progress atomic counter, enqueue the item, increment the counter and if the counter transitioned from 0 to 1, keep draining the queue, work with the element and decrement the counter until it reaches zero again: + +```java +final ConcurrentLinkedQueue<Runnable> queue = ...; +final AtomicInteger wip = ...; + +void execute(Runnable r) { + queue.offer(r); + if (wip.getAndIncrement() == 0) { + do { + queue.poll().run(); + } while (wip.decrementAndGet() != 0); + } +} +``` + +The same pattern applies when one has to emit onNext values to a downstream consumer: + +```java +final ConcurrentLinkedQueue<T> queue = ...; +final AtomicInteger wip = ...; +final Subscriber<? super T> child = ...; + +void emit(T r) { + queue.offer(r); + if (wip.getAndIncrement() == 0) { + do { + child.onNext(queue.poll()); + } while (wip.decrementAndGet() != 0); + } +} +``` + +### Queues + +Using `ConcurrentLinkedQueue` is a reliable although mostly an overkill for such situations because it allocates on each call to `offer()` and is unbounded. It can be replaced with more optimized queues (see [JCTools](https://github.com/JCTools/JCTools/)) and RxJava itself also has some customized queues available (internal!): + + - `SpscArrayQueue` used when the queue is known to be fed by a single thread but the serialization has to look at other things (request, cancellation, termination) that can be read from other fields. Example: `observeOn` has a fixed request pattern which fits into this type of queue and extra fields for passing an error, completion or downstream requests into the drain logic. + - `SpscLinkedArrayQueue` used when the queue is known to be fed by a single thread but there is no bound on the element count. Example: `UnicastProcessor`, almost all buffering `Observable` operator. Some operators use it with multiple event sources by synchronizing on the `offer` side - a tradeoff between allocation and potential blocking: + +```java +SpscLinkedArrayQueue<T> q = ... +synchronized(q) { + q.offer(value); +} +``` + + - `MpscLinkedQueue` where there could be many feeders and unknown number of elements. Example: `buffer` with reactive boundary. + +The RxJava 2.x implementations of these types of queues have different class hierarchy than the JDK/JCTools versions. Our classes don't implement the `java.util.Queue` interface but rather a custom, simplified interface: + +```java +interface SimpleQueue<T> { + boolean offer(T t); + + boolean offer(T t1, T t2); + + T poll() throws Exception; + + boolean isEmpty(); + + void clear(); +} + +interface SimplePlainQueue<T> extends SimpleQueue<T> { + @Override + T poll(); +} + +public final class SpscArrayQueue<T> implements SimplePlainQueue<T> { + // ... +} +``` + +This simplified queue API gets rid of the unused parts (iterator, collections API remnants) and adds a bi-offer method (only implemented atomically in `SpscLinkedArrayQueue` currently). The second interface, `SimplePlainQueue` is defined to suppress the `throws Exception` on poll on queue types that won't throw that exception and there is no need for try-catch around them. + +## Deferred actions + +The Reactive-Streams has a strict requirement that calling `onSubscribe()` must happen before any calls to the rest of the `onXXX` methods and by nature, any calls to `Subscription.request()` and `Subscription.cancel()`. The same logic applies to the design of `Observable`, `Single`, `Completable` and `Maybe` with their connection type of `Disposable`. + +Often though, such call to `onSubscribe` may happen later than the respective `cancel()` needs to happen. For example, the user may want to call `cancel()` before the respective `Subscription` actually becomes available in `subscribeOn`. Other operators may need to call `onSubscribe` before they connect to other sources but at that time, there is no direct way for relaying a `cancel` call to an unavailable upstream `Subscription`. + +The solution is **deferred cancellation** and **deferred requesting** in general. + +### Deferred cancellation + +This approach affects all 5 reactive types and works the same way for everyone. First, have an `AtomicReference` that will hold the respective connection type (or any other type whose method call has to happen later). Two methods are needed handling the `AtomicReference` class, one that sets the actual instance and one that calls the `cancel`/`dispose` method on it. + +```java +static final Disposable DISPOSED; +static { + DISPOSED = Disposables.empty(); + DISPOSED.dispose(); +} + +static boolean set(AtomicReference<Disposable> target, Disposable value) { + for (;;) { + Disposable current = target.get(); + if (current == DISPOSED) { + if (value != null) { + value.dispose(); + } + return false; + } + if (target.compareAndSet(current, value)) { + if (current != null) { + current.dispose(); + } + return true; + } + } +} + +static boolean dispose(AtomicReference<Disposable> target) { + Disposable current = target.getAndSet(DISPOSED); + if (current != DISPOSED) { + if (current != null) { + current.dispose(); + } + return true; + } + return false; +} +``` + +The approach uses an unique sentinel value `DISPOSED` - that should not appear elsewhere in your code - to indicate once a late actual `Disposable` arrives, it should be disposed immediately. Both methods return true if the operation succeeded or false if the target was already disposed. + +Sometimes, only one call to `set` is permitted (i.e., `setOnce`) and other times, the previous non-null value needs no call to `dispose` because it is known to be disposed already (i.e., `replace`). + +As with the request management, there are utility classes and methods for these operations: + + - (internal) `SequentialDisposable` that uses `update`, `replace` and `dispose` but leaks the API of `AtomicReference` + - `SerialDisposable` that has safe API with `set`, `replace` and `dispose` among other things + - (internal) `DisposableHelper` that features the methods shown above and the global disposed sentinel used by RxJava. It may come handy when one uses `AtomicReference<Disposable>` as a base class. + +The same pattern applies to `Subscription` with its `cancel()` method and with helper (internal) class `SubscriptionHelper` (but no `SequentialSubscription` or `SerialSubscription`, see next subsection). + +### Deferred requesting + +With `Flowable`s (and Reactive-Streams `Publisher`s) the `request()` calls need to be deferred as well. In one form (the simpler one), the respective late `Subscription` will eventually arrive and we need to relay all previous and all subsequent request amount to its `request()` method. + +In 1.x, this behavior was implicitly provided by `rx.Subscriber` but at a high cost that had to be payed by all instances whether or not they needed this feature. + +The solution works by having the `AtomicReference` for the `Subscription` and an `AtomicLong` to store and accumulate the requests until the actual `Subscription` arrives, then atomically request all deferred value once. + +```java +static boolean deferredSetOnce(AtomicReference<Subscription> subscription, + AtomicLong requested, Subscription newSubscription) { + if (subscription.compareAndSet(null, newSubscription) { + long r = requested.getAndSet(0L); + if (r != 0) { + newSubscription.request(r); + } + return true; + } + newSubscription.cancel(); + if (subscription.get() != SubscriptionHelper.CANCELLED) { + RxJavaPlugins.onError(new IllegalStateException("Subscription already set!")); + } + return false; +} + +static void deferredRequest(AtomicReference<Subscription> subscription, + AtomicLong requested, long n) { + Subscription current = subscription.get(); + if (current != null) { + current.request(n); + } else { + BackpressureHelper.add(requested, n); + current = subscription.get(); + if (current != null) { + long r = requested.getAndSet(0L); + if (r != 0L) { + current.request(r); + } + } + } +} +``` + +In `deferredSetOnce`, if the CAS from null to the `newSubscription` succeeds, we atomically exchange the request amount to 0L and if the original value was nonzero, we request from `newSubscription`. In `deferredRequest`, if there is a `Subscription` we simply request from it directly. Otherwise, we accumulate the requests via the helper method then check again if the `Subscription` arrived or not. If it arrived in the meantime, we atomically exchange the accumulated request value and if nonzero, request it from the newly retrieved `Subscription`. This non-blocking logic makes sure that in case of concurrent invocations of the two methods, no accumulated request is left behind. + +This complex logic and methods, along with other safeguards are available in the (internal) `SubscriptionHelper` utility class and can be used like this: + +```java +final class Operator<T> implements Subscriber<T>, Subscription { + final Subscriber<? super T> child; + + final AtomicReference<Subscription> ref = new AtomicReference<>(); + final AtomicLong requested = new AtomicLong(); + + public Operator(Subscriber<? super T> child) { + this.child = child; + } + + @Override + public void onSubscribe(Subscription s) { + SubscriptionHelper.deferredSetOnce(ref, requested, s); + } + + @Override + public void onNext(T t) { ... } + + @Override + public void onError(Throwable t) { ... } + + @Override + public void onComplete() { ... } + + @Override + public void cancel() { + SubscriptionHelper.cancel(ref); + } + + @Override + public void request(long n) { + SubscriptionHelper.deferredRequested(ref, requested, n); + } +} + +Operator<T> parent = new Operator<T>(child); + +child.onSubscribe(parent); + +source.subscribe(parent); +``` + +The second form is when multiple `Subscription`s replace each other and we not only need to hold onto request amounts when there is none of them but make sure a newer `Subscription` is requested only that much the previous `Subscription`'s upstream didn't deliver. This is called **Subscription arbitration** and the relevant algorithms are quite verbose and will be omitted here. There is, however, an utility class that manages this: (internal) `SubscriptionArbiter`. + +You can extend it (to save on object headers) or have it as a field. Its main use is to send it to the downstream via `onSubscribe` and update its current `Subscription` in the current operator. Note that even though its methods are thread-safe, it is intended for swapping `Subscription`s when the current one finished emitting events. This makes sure that any newer `Subscription` is requested the right amount and not more due to production/switch race. + +```java +final SubscriptionArbiter arbiter = ... + +// ... + +child.onSubscribe(arbiter); + +// ... + +long produced; + +@Override +public void onSubscribe(Subscription s) { + arbiter.setSubscription(s); +} + +@Override +public void onNext(T value) { + produced++; + child.onNext(value); +} + +@Override +public void onComplete() { + long p = produced; + if (p != 0L) { + arbiter.produced(p); + } + subscribeNext(); +} +``` + +For better performance, most operators can count the produced element amount and issue a single `SubscriptionArbiter.produced()` call just before switching to the next `Subscription`. + + +## Atomic error management + +In some cases, multiple sources may signal a `Throwable` at the same time but the contract forbids calling `onError` multiple times. Once can, of course use the **once** approach with `AtomicReference<Throwable>` and throw out but the first one to set the `Throwable` on it. + +The alternative is to collect these `Throwable`s into a `CompositeException` as long as possible and at one point lock out the others. This works by doing a copy-on-write scheme or by linking `CompositeException`s atomically and having a terminal sentinel to indicate all further errors should be dropped. + +```java +static final Throwable TERMINATED = new Throwable(); + +static boolean addThrowable(AtomicReference<Throwable> ref, Throwable e) { + for (;;) { + Throwable current = ref.get(); + if (current == TERMINATED) { + return false; + } + Throwable next; + if (current == null) { + next = e; + } else { + next = new CompositeException(current, e); + } + if (ref.compareAndSet(current, next)) { + return true; + } + } +} + +static Throwable terminate(AtomicReference<Throwable> ref) { + return ref.getAndSet(TERMINATED); +} +``` + +as with most common logic, this is supported by the (internal) `ExceptionHelper` utility class and the (internal) `AtomicThrowable` class. + +The usage pattern looks as follows: + +```java + +final AtomicThrowable errors = ...; + +@Override +public void onError(Throwable e) { + if (errors.addThrowable(e)) { + drain(); + } else { + RxJavaPlugins.onError(e); + } +} + +void drain() { + // ... + if (errors.get() != null) { + child.onError(errors.terminate()); + return; + } + // ... +} +``` + +## Half-serialization + +Sometimes having the queue-drain, `SerializedSubscriber` or `SerializedObserver` is a bit of an overkill. Such cases include when there is only one thread calling `onNext` but other threads may call `onError` or `onComplete` concurrently. Example operators include `takeUntil` where the other source may "interrupt" the main sequence and inject an `onComplete` into it before the main source itself would complete some time later. This is what I call **half-serialization**. + +The approach uses the concepts of the deferred actions and atomic error management discussed above and has 3 methods for the `onNext`, `onError` and `onComplete` management: + +```java +public static <T> void onNext(Subscriber<? super T> subscriber, T value, + AtomicInteger wip, AtomicThrowable error) { + if (wip.get() == 0 && wip.compareAndSet(0, 1)) { + subscriber.onNext(value); + if (wip.decrementAndGet() != 0) { + Throwable ex = error.terminate(); + if (ex != null) { + subscriber.onError(ex); + } else { + subscriber.onComplete(); + } + } + } +} + +public static void onError(Subscriber<?> subscriber, Throwable ex, + AtomicInteger wip, AtomicThrowable error) { + if (error.addThrowable(ex)) { + if (wip.getAndIncrement() == 0) { + subscriber.onError(error.terminate()); + } + } else { + RxJavaPlugins.onError(ex); + } +} + +public static void onComplete(Subscriber<?> subscriber, + AtomicInteger wip, AtomicThrowable error) { + if (wip.getAndIncrement() == 0) { + Throwable ex = error.terminate(); + if (ex != null) { + subscriber.onError(ex); + } else { + subscriber.onComplete(); + } + } +} +``` + +Here, the `wip` counter indicates there is an active emission happening and if found non-zero when trying to leave the `onNext`, it is taken as indication there was a concurrent `onError` or `onComplete()` call and the child must be notified. All subsequent calls to any of these methods are ignored. In this case, the `wip` is never decremented back to zero. + +RxJava 2.x, again, supports these with the (internal) utility class `HalfSerializer` and allows targeting `Subscriber`s and `Observer`s with it. + +## Fast-path queue-drain + +In some operators, it is unlikely concurrent threads try to enter into the drain loop at the same time and having to play the full enqueue-increment-dequeue adds unnecessary overhead. + +Luckily, such situations can be detected by a simple compare-and-set attempt on the work-in-progress counter, trying to change the amount from 0 to 1. If it fails, there is a concurrent drain in progress and we revert back to the classical queue-drain logic. If succeeds, we don't enqueue anything but emit the value / perform the action right there and we try to leave the serialized section. + +```java +public void onNext(T v) { + if (wip.get() == 0 && wip.compareAndSet(0, 1)) { + child.onNext(v); + if (wip.decrementAndGet() == 0) { + break; + } + } else { + queue.offer(v); + if (wip.getAndIncrement() != 0) { + break; + } + } + drainLoop(); +} + +void drain() { + if (getAndIncrement() == 0) { + drainLoop(); + } +} + +void drainLoop() { + // the usual drain loop part after the classical getAndIncrement() +} +``` + +In this pattern, the classical `drain` is spit into `drain` and `drainLoop`. The new `drain` does the increment-check and calls `drainLoop` and `drainLoop` contains the remaining logic with the loop, emission and wip management as usual. + +On the fast path, when we try to leave it, it is possible a concurrent call to `onNext` or `drain` incremented the `wip` counter further and the decrement didn't return it to zero. This is an indication for further work and we call `drainLoop` to process it. + +## FlowableSubscriber + +Version 2.0.7 introduced a new interface, `FlowableSubscriber` that extends `Subscriber` from Reactive-Streams. It has the same methods with the same parameter types but different textual rules attached to it, a set of relaxations to the Reactive-Streams specification to enable better performing RxJava internals while still honoring the specification to the letter for non-RxJava consumers of `Flowable`s. + +The rule relaxations are as follows: + +- §1.3 relaxation: `onSubscribe` may run concurrently with onNext in case the `FlowableSubscriber` calls `request()` from inside `onSubscribe` and it is the responsibility of `FlowableSubscriber` to ensure thread-safety between the remaining instructions in `onSubscribe` and `onNext`. +- §2.3 relaxation: calling `Subscription.cancel` and `Subscription.request` from `FlowableSubscriber.onComplete()` or `FlowableSubscriber.onError()` is considered a no-operation. +- §2.12 relaxation: if the same `FlowableSubscriber` instance is subscribed to multiple sources, it must ensure its `onXXX` methods remain thread safe. +- §3.9 relaxation: issuing a non-positive `request()` will not stop the current stream but signal an error via `RxJavaPlugins.onError`. + +When a `Flowable` gets subscribed by a `Subscriber`, an `instanceof` check will detect `FlowableSubscriber` and not apply the `StrictSubscriber` wrapper that makes sure the relaxations don't happen. In practice, ensuring rule §3.9 has the most overhead because a bad request may happen concurrently with an emission of any normal event and thus has to be serialized with one of the methods described in previous sections. + +**In fact, 2.x was always implemented in this relaxed manner thus looking at existing code and style is the way to go.** + +Therefore, it is strongly recommended one implements custom intermediate and end operators via `FlowableSubscriber`. + +From a source operator's perspective, extending the `Flowable` class and implementing `subscribeActual` has no need for +dispatching over the type of the `Subscriber`; the backing infrastructure already applies wrapping if necessary thus one can be sure in `subscribeActual(Subscriber<? super T> s)` the parameter `s` is a `FlowableSubscriber`. (The signature couldn't be changed for compatibility reasons.) Since the two interfaces on the Java level are the same, no real preferential treating is necessary within sources (i.e., don't cast `s` into `FlowableSubscriber`. + +The other base reactive consumers, `Observer`, `SingleObserver`, `MaybeObserver` and `CompletableObserver` don't need such relaxation, because + +- they are only defined and used in RxJava (i.e., no other library implemented with them), +- they were conceptionally always derived from the relaxed `Subscriber` RxJava had, +- they don't have backpressure thus no `request()` call that would introduce another concurrency to think about, +- there is no way to trigger emission from upstream before `onSubscribe(Disposable)` returns in standard operators (again, no `request()` method). + +# Backpressure and cancellation + +Backpressure (or flow control) in Reactive-Streams is the means to tell the upstream how many elements to produce or to tell it to stop producing elements altogether. Unlike the name suggest, there is no physical pressure preventing the upstream from calling `onNext` but the protocol to honor the request amount. + +## Replenishing + +When dealing with basic transformations in a flow, there are often cases when the number of items the upstream sends should be different what the downstream receives. Some operators may want to filter out certain items, others would batch up items before sending one item to the downstream. + +However, when an item is not forwarded by an operator, the downstream has no way of knowing its `request(1)` triggered an item generation that got dropped/buffered. Therefore, it can't know to (nor should it) repeat `request(1)` to "nudge" the source somewhere more upstream to try producing another item which now hopefully will result in an item being received by the downstream. Unlike, say the ACK-NACK based protocols, the requesting specified by the Reactive Streams are to be treated as cumulative. In the previous example, an impatient downstream would have 2 outstanding requests. + +Therefore, if an operator is not guaranteed to relay an upstream item to downstream, and thus keeping a 1:1 ratio, it has the duty to keep requesting items from the upstream until said operator ends up in a position to supply an item to the downstream. + +This may sound a bit complicated, but perhaps a demonstration of a `filter` operator can help: + +```java +final class FilterOddSubscriber implements FlowableSubscriber<Integer>, Subscription { + + final Subscriber<? super Integer> downstream; + + Subscription upstream; + + // ... + + @Override + public void onSubscribe(Subscription s) { + if (upstream != null) { + s.cancel(); + } else { + upstream = s; + downstream.onSubscribe(this); + } + } + + @Override + public void onNext(Integer item) { + if (item % 2 != 0) { + downstream.onNext(item); + } else { + upstream.request(1); + } + } + + @Override + public void request(long n) { + upstream.request(n); + } + + // the rest omitted for brevity +} +``` + +In such operators, thee downstream's `request` calls are forwarded to the upstream as is, and for `n` times (at most, unless completed) the `onNext` will be invoked. In this operator, we look for the odd numbers of the flow. If we find one, the downstream will be notified. If the incoming item is even, we won't forward it to the downstream. However, the downstream is still expecting at least 1 item, but since the upstream and downstream practically talk to each other directly, the upstream considers its duty to generate items fulfilled. This misalignment is then resolved by requesting 1 more item from the upstream for the previous ignored item. If more items arrive that get ignored, more will be requested as replenishment. + +Given that backpressure involves some overhead in the form of one or more atomic operations, requesting one by one could add a lot of overhead if the number of items filtered out is significantly more than those that passed through. If necessary, this situation can be either solved by [decoupling the upstream and downstream's request management](#stable-prefetching) or using an RxJava-specific type and protocol extension in the form of [ConditionalSubscriber](#conditionalsubscriber)s. + +## Stable prefetching + +In a previous section, we saw primitives to deal with request accounting and delayed `Subscriptions`, but often, operators have to react to request amount changes as well. This comes up when the operator has to decouple the downstream request amount from the amount it requests from upstream, such as `observeOn`. + +Such logic can get quite complicated in operators but one of the simplest manifestation can be the `rebatchRequest` operator that combines request management with serialization to ensure that upstream is requested with a predictable pattern no matter how the downstream requested (less, more or even unbounded): + +```java +final class RebatchRequests<T> extends AtomicInteger +implements FlowableSubscriber<T>, Subscription { + + final Subscriber<? super T> child; + + final AtomicLong requested; + + final SpscArrayQueue<T> queue; + + final int batchSize; + + final int limit; + + Subscription s; + + volatile boolean done; + Throwable error; + + volatile boolean cancelled; + + long emitted; + + public RebatchRequests(Subscriber<? super T> child, int batchSize) { + this.child = child; + this.batchSize = batchSize; + this.limit = batchSize - (batchSize >> 2); // 75% of batchSize + this.requested = new AtomicLong(); + this.queue = new SpscArrayQueue<T>(batchSize); + } + + @Override + public void onSubscribe(Subscription s) { + this.s = s; + child.onSubscribe(this); + s.request(batchSize); + } + + @Override + public void onNext(T t) { + queue.offer(t); + drain(); + } + + @Override + public void onError(Throwable t) { + error = t; + done = true; + drain(); + } + + @Override + public void onComplete() { + done = true; + drain(); + } + + @Override + public void request(long n) { + BackpressureHelper.add(requested, n); + drain(); + } + + @Override + public void cancel() { + cancelled = true; + s.cancel(); + } + + void drain() { + // see next code example + } +} +``` + +Here we extend `AtomicInteger` since the work-in-progress counting happens more often and is worth avoiding the extra indirection. The class extends `Subscription` and it hands itself to the `child` `Subscriber` to capture its `request()` (and `cancel()`) calls and route it to the main `drain` logic. Some operators need only this, some other operators (such as `observeOn` not only routes the downstream request but also does extra cancellations (cancels the asynchrony providing `Worker` as well) in its `cancel()` method. + +**Important**: when implementing operators for `Flowable` and `Observable` in RxJava 2.x, you are not allowed to pass along an upstream `Subscription` or `Disposable` to the child `Subscriber`/`Observer` when the operator logic itself doesn't require hooking the `request`/`cancel`/`dispose` calls. The reason for this is how operator-fusion is implemented on top of `Subscription` and `Disposable` passing through `onSubscribe` in RxJava 2.x (and in **Reactor 3**). See the next section about operator-fusion. There is no fusion in `Single`, `Completable` or `Maybe` (because there is no requesting or unbounded buffering with them) and their operators can pass the upstream `Disposable` along as is. + +Next comes the `drain` method whose pattern appears in many operators (with slight variations on how and what the emission does). + +```java +void drain() { + if (getAndIncrement() != 0) { + return; + } + + int missed = 1; + + for (;;) { + long r = requested.get(); + long e = 0L; + long f = emitted; + + while (e != r) { + if (cancelled) { + return; + } + boolean d = done; + + if (d) { + Throwable ex = error; + if (ex != null) { + child.onError(ex); + return; + } + } + + T v = queue.poll(); + boolean empty = v == null; + + if (d && empty) { + child.onComplete(); + return; + } + + if (empty) { + break; + } + + child.onNext(v); + + e++; + if (++f == limit) { + s.request(f); + f = 0L; + } + } + + if (e == r) { + if (cancelled) { + return; + } + + if (done) { + Throwable ex = error; + if (ex != null) { + child.onError(ex); + return; + } + if (queue.isEmpty()) { + child.onComplete(); + return; + } + } + } + + if (e != 0L) { + BackpressureHelper.produced(requested, e); + } + + emitted = f; + missed = addAndGet(-missed); + if (missed == 0) { + break; + } + } +} +``` + +This particular pattern is called the **stable-request queue-drain**. Another variation doesn't care about request amount stability towards upstream and simply requests the amount it delivered to the child: + +```java +void drain() { + if (getAndIncrement() != 0) { + return; + } + + int missed = 1; + + for (;;) { + long r = requested.get(); + long e = 0L; + + while (e != r) { + if (cancelled) { + return; + } + boolean d = done; + + if (d) { + Throwable ex = error; + if (ex != null) { + child.onError(ex); + return; + } + } + + T v = queue.poll(); + boolean empty = v == null; + + if (d && empty) { + child.onComplete(); + return; + } + + if (empty) { + break; + } + + child.onNext(v); + + e++; + } + + if (e == r) { + if (cancelled) { + return; + } + + if (done) { + Throwable ex = error; + if (ex != null) { + child.onError(ex); + return; + } + if (queue.isEmpty()) { + child.onComplete(); + return; + } + } + } + + if (e != 0L) { + BackpressureHelper.produced(requested, e); + s.request(e); + } + + missed = addAndGet(-missed); + if (missed == 0) { + break; + } + } +} +``` + +The third variation allows delaying a potential error until the upstream has terminated and all normal elements have been delivered to the child: + +```java +final boolean delayError; + +void drain() { + if (getAndIncrement() != 0) { + return; + } + + int missed = 1; + + for (;;) { + long r = requested.get(); + long e = 0L; + + while (e != r) { + if (cancelled) { + return; + } + boolean d = done; + + if (d && !delayError) { + Throwable ex = error; + if (ex != null) { + child.onError(ex); + return; + } + } + + T v = queue.poll(); + boolean empty = v == null; + + if (d && empty) { + Throwable ex = error; + if (ex != null) { + child.onError(ex); + } else { + child.onComplete(); + } + return; + } + + if (empty) { + break; + } + + child.onNext(v); + + e++; + } + + if (e == r) { + if (cancelled) { + return; + } + + if (done) { + if (delayError) { + if (queue.isEmpty()) { + Throwable ex = error; + if (ex != null) { + child.onError(ex); + } else { + child.onComplete(); + } + return; + } + } else { + Throwable ex = error; + if (ex != null) { + child.onError(ex); + return; + } + if (queue.isEmpty()) { + child.onComplete(); + return; + } + } + } + } + + if (e != 0L) { + BackpressureHelper.produced(requested, e); + s.request(e); + } + + missed = addAndGet(-missed); + if (missed == 0) { + break; + } + } +} +``` + +If the downstream cancels the operator, the `queue` may still hold elements which may get referenced longer than expected if the operator chain itself is referenced in some way. On the user level, applying `onTerminateDetach` will forget all references going upstream and downstream and can help with this situation. On the operator level, RxJava 2.x usually calls `clear()` on the `queue` when the sequence is cancelled or ends before the queue is drained naturally. This requires some slight change to the drain loop: + +```java +final boolean delayError; + +@Override +public void cancel() { + cancelled = true; + s.cancel(); + if (getAndIncrement() == 0) { + queue.clear(); // <---------------------------- + } +} + +void drain() { + if (getAndIncrement() != 0) { + return; + } + + int missed = 1; + + for (;;) { + long r = requested.get(); + long e = 0L; + + while (e != r) { + if (cancelled) { + queue.clear(); // <---------------------------- + return; + } + boolean d = done; + + if (d && !delayError) { + Throwable ex = error; + if (ex != null) { + queue.clear(); // <---------------------------- + child.onError(ex); + return; + } + } + + T v = queue.poll(); + boolean empty = v == null; + + if (d && empty) { + Throwable ex = error; + if (ex != null) { + child.onError(ex); + } else { + child.onComplete(); + } + return; + } + + if (empty) { + break; + } + + child.onNext(v); + + e++; + } + + if (e == r) { + if (cancelled) { + queue.clear(); // <---------------------------- + return; + } + + if (done) { + if (delayError) { + if (queue.isEmpty()) { + Throwable ex = error; + if (ex != null) { + child.onError(ex); + } else { + child.onComplete(); + } + return; + } + } else { + Throwable ex = error; + if (ex != null) { + queue.clear(); // <---------------------------- + child.onError(ex); + return; + } + if (queue.isEmpty()) { + child.onComplete(); + return; + } + } + } + } + + if (e != 0L) { + BackpressureHelper.produced(requested, e); + s.request(e); + } + + missed = addAndGet(-missed); + if (missed == 0) { + break; + } + } +} +``` + +Since the queue is single-producer-single-consumer, its `clear()` must be called from a single thread - which is provided by the serialization loop and is enabled by the `getAndIncrement() == 0` "half-loop" inside `cancel()`. + +An important note on the order of calls to `done` and the `queue`'s state: + +```java +boolean d = done; +T v = queue.poll(); +``` + +and + +```java +boolean d = done; +boolean empty = queue.isEmpty(); +``` + +These must happen in the order specified. If they were swapped, it is possible when the drain runs asynchronously to an `onNext`/`onComplete()`, the queue may appear empty at first, then it gets elements followed by `done = true` and a late `done` check in the drain loop may complete the sequence thinking it delivered all values there was. + +## Single valued results + +Sometimes an operator only emits one single value at some point instead of emitting more or all of its sources. Such operators include `fromCallable`, `reduce`, `any`, `all`, `first`, etc. + +The classical queue-drain works here but is a bit of an overkill to allocate objects to store the work-in-progress counter, request accounting and the queue itself. These elements can be reduced to a single state-machine with one state counter object - often inlinded by extending AtomicInteger - and a plain field for storing the single value to be emitted. + +The state machine handing the possible concurrent downstream requests and normal completion path is a bit complicated to show here and is quite easy to get wrong. + +RxJava 2.x supports this kind of behavior through the (internal) `DeferredScalarSubscription` for operators without an upstream source (`fromCallable`) and the (internal) `DeferredScalarSubscriber` for reduce-like operators with an upstream source. + +Using the `DeferredScalarSubscription` is straightforward, one creates it, sends it to the downstream via `onSubscribe` and later on calls `complete(T)` to signal the end with a single value: + +```java +DeferredScalarSubscription<Integer> dss = new DeferredScalarSubscription<>(child); +child.onSubscribe(dss); + +dss.complete(1); +``` + +Using the `DeferredScalarSubscriber` requires more coding and extending the class itself: + +```java +final class Counter extends DeferredScalarSubscriber<Object, Integer> { + public Counter(Subscriber<? super Integer> child) { + super(child); + value = 0; + hasValue = true; + } + + @Override + public void onNext(Object t) { + value++; + } +} +``` + +By default, the `DeferredScalarSubscriber.onSubscribe()` requests `Long.MAX_VALUE` from the upstream (but the method can be overridden in subclasses). + +## Single-element post-complete + +Some operators have to modulate a sequence of elements in a 1:1 fashion but when the upstream terminates, they need to produce a final element followed by a terminal event (usually `onComplete`). + +```java +final class OnCompleteEndWith implements Subscriber<T>, Subscription { + final Subscriber<? super T> child; + + final T finalElement; + + Subscription s; + + public OnCompleteEndWith(Subscriber<? super T> child, T finalElement) { + this.child = child; + this.finalElement = finalElement; + } + + @Override + public void onSubscribe(Subscription s) { + this.s = s; + child.onSubscribe(this); + } + + @Override + public void onNext(T t) { + child.onNext(t); + } + + @Overide + public void onError(Throwable t) { + child.onError(t); + } + + @Override + public void onComplete() { + child.onNext(finalElement); + child.onComplete(); + } + + @Override + public void request(long n) { + s.request(n); + } + + @Override + public void cancel() { + s.cancel(); + } +} +``` + +This works if the downstream request more than the upstream produces + 1, otherwise the call to `onComplete` may overflow the child `Subscriber`. + +Heavyweight solutions such as queue-drain or `SubscriptionArbiter` with `ScalarSubscriber` can be used here, however, there is a more elegant solution to the problem. + +The idea is that request amounts occupy only 63 bits of a 64 bit (atomic) long type. If we'd mask out the lower 63 bits when working with the amount, we can use the most significant bit to indicate the upstream sequence has finished and then on, any 0 to n request amount change can trigger the emission of the `finalElement`. Since a downstream `request()` can race with an upstream `onComplete`, marking the bit atomically via a compare-and-set ensures correct state transition. + +For this, the `OnCompleteEndWith` has to be changed by adding an `AtomicLong` for accounting requests, a long for counting the production, then updating `request()` and `onComplete()` methods: + +```java + +final class OnCompleteEndWith +extends AtomicLong +implements FlowableSubscriber<T>, Subscription { + final Subscriber<? super T> child; + + final T finalElement; + + Subscription s; + + long produced; + + static final class long REQUEST_MASK = Long.MAX_VALUE; // 0b01111...111L + static final class long COMPLETE_MASK = Long.MIN_VALUE; // 0b10000...000L + + public OnCompleteEndWith(Subscriber<? super T> child, T finalElement) { + this.child = child; + this.finalElement = finalElement; + } + + @Override + public void onSubscribe(Subscription s) { ... } + + @Override + public void onNext(T t) { + produced++; // <------------------------ + child.onNext(t); + } + + @Overide + public void onError(Throwable t) { ... } + + @Override + public void onComplete() { + long p = produced; + if (p != 0L) { + produced = 0L; + BackpressureHelper.produced(this, p); + } + + for (;;) { + long current = get(); + if ((current & COMPLETE_MASK) != 0) { + break; + } + if ((current & REQUEST_MASK) != 0) { + lazySet(Long.MIN_VALUE + 1); + child.onNext(finalElement); + child.onComplete(); + return; + } + if (compareAndSet(current, COMPLETE_MASK)) { + break; + } + } + } + + @Override + public void request(long n) { + for (;;) { + long current = get(); + if ((current & COMPLETE_MASK) != 0) { + if (compareAndSet(current, COMPLETE_MASK + 1)) { + child.onNext(finalElement); + child.onComplete(); + } + break; + } + long u = BackpressureHelper.addCap(current, n); + if (compareAndSet(current, u)) { + s.request(n); + break; + } + } + } + + @Override + public void cancel() { ... } +} +``` + +RxJava 2 has a couple of operators, `materialize`, `mapNotification`, `onErrorReturn`, that require this type of behavior and for that, the (internal) `SinglePostCompleteSubscriber` class captures the algorithms above: + +```java +final class OnCompleteEndWith<T> extends SinglePostCompleteSubscriber<T, T> { + final Subscriber<? super T> child; + + public OnCompleteEndWith(Subscriber<? super T> child, T finalElement) { + this.child = child; + this.value = finalElement; + } + + @Override + public void onNext(T t) { + produced++; // <------------------------ + child.onNext(t); + } + + @Overide + public void onError(Throwable t) { ... } + + @Override + public void onComplete() { + complete(value); + } +} +``` + +## Multi-element post-complete + +Certain operators may need to emit multiple elements after the main sequence completes, which may or may not relay elements from the live upstream before its termination. An example operator is `buffer(int, int)` when the skip < size yielding overlapping buffers. In this operator, it is possible when the upstream completes, several overlapping buffers are waiting to be emitted to the child but that has to happen only when the child actually requested more buffers. + +The state machine for this case is complicated but RxJava has two (internal) utility methods on `QueueDrainHelper` for dealing with the situation: + +```java +<T> void postComplete(Subscriber<? super T> actual, + Queue<T> queue, + AtomicLong state, + BooleanSupplier isCancelled); + +<T> boolean postCompleteRequest(long n, + Subscriber<? super T> actual, + Queue<T> queue, + AtomicLong state, + BooleanSupplier isCancelled); +``` + +They take the child `Subscriber`, the queue to drain from, the state holding the current request amount and a callback to see if the downstream cancelled the sequence. + +Usage of these methods is as follows: + +```java +final class EmitTwice<T> extends AtomicLong +implements FlowableSubscriber<T>, Subscription, BooleanSupplier { + final Subscriber<? super T> child; + + final ArrayDeque<T> buffer; + + volatile boolean cancelled; + + Subscription s; + + long produced; + + public EmitTwice(Subscriber<? super T> child) { + this.child = child; + this.buffer = new ArrayDeque<>(); + } + + @Override + public void onSubscribe(Subscription s) { + this.s = s; + child.onSubscribe(this); + } + + @Override + public void onNext(T t) { + produced++; + buffer.offer(t); + child.onNext(t); + } + + @Override + public void onError(Throwable t) { + buffer.clear(); + child.onError(t); + } + + @Override + public void onComplete() { + long p = produced; + if (p != 0L) { + produced = 0L; + BackpressureHelper.produced(this, p); + } + QueueDrainHelper.postComplete(child, buffer, this, this); + } + + @Override + public boolean getAsBoolean() { + return cancelled; + } + + @Override + public void cancel() { + cancelled = true; + s.cancel(); + } + + @Override + public void request(long n) { + if (!QueueDrainHelper.postCompleteRequest(n, child, buffer, this, this)) { + s.request(n); + } + } +} +``` + +# Creating operator classes + +Creating operator implementations in 2.x is simpler than in 1.x and incurs less allocation as well. You have the choice to implement your operator as a `Subscriber`-transformer to be used via `lift` or as a fully-fledged base reactive class. + +## Operator by extending a base reactive class + +In 1.x, extending `Observable` was possible but convoluted because you had to implement the `OnSubscribe` interface separately and pass it to `Observable.create()` or to the `Observable(OnSubscribe)` protected constructor. + +In 2.x, all base reactive classes are abstract and you can extend them directly without any additional indirection: + +```java +public final class FlowableMyOperator extends Flowable<Integer> { + final Publisher<Integer> source; + + public FlowableMyOperator(Publisher<Integer> source) { + this.source = source; + } + + @Override + protected void subscribeActual(Subscriber<? super Integer> s) { + source.map(v -> v + 1).subscribe(s); + } +} +``` + +When taking other reactive types as inputs in these operators, it is recommended one defines the base reactive interfaces instead of the abstract classes, allowing better interoperability between libraries (especially with `Flowable` operators and other Reactive-Streams `Publisher`s). To recap, these are the class-interface pairs: + + - `Flowable` - `Publisher` - `FlowableSubscriber`/`Subscriber` + - `Observable` - `ObservableSource` - `Observer` + - `Single` - `SingleSource` - `SingleObserver` + - `Completable` - `CompletableSource` - `CompletableObserver` + - `Maybe` - `MaybeSource` - `MaybeObserver` + +RxJava 2.x locks down `Flowable.subscribe` (and the same methods in the other types) in order to provide runtime hooks into the various flows, therefore, implementors are given the `subscribeActual()` to be overridden. When it is invoked, all relevant hooks and wrappers have been applied. Implementors should avoid throwing unchecked exceptions as the library generally can't deliver it to the respective `Subscriber` due to lifecycle restrictions of the Reactive-Streams specification and sends it to the global error consumer via `RxJavaPlugins.onError`. + +Unlike in 1.x, In the example above, the incoming `Subscriber` is simply used directly for subscribing again (but still at most once) without any kind of wrapping. In 1.x, one needs to call `Subscribers.wrap` to avoid double calls to `onStart` and cause unexpected double initialization or double-requesting. + +Unless one contributes a new operator to RxJava, working with such classes may become tedious, especially if they are intermediate operators: + +```java +new FlowableThenSome( + new FlowableOther( + new FlowableMyOperator(Flowable.range(1, 10).map(v -> v * v)) + ) +) +``` + +This is an unfortunate effect of Java lacking extension method support. A possible ease on this burden is by using `compose` to have fluent inline application of the custom operator: + +```java +Flowable.range(1, 10).map(v -> v * v) +.compose(f -> new FlowableOperatorWithParameter(f, 10)); + +Flowable.range(1, 10).map(v -> v * v) +.compose(FlowableMyOperator::new); +``` + +## Operator targeting lift() + +The alternative to the fluent application problem is to have a `Subscription`-transformer implemented instead of extending the whole reactive base class and use the respective type's `lift()` operator to get it into the sequence. + +First one has to implement the respective `XOperator` interface: + +```java +public final class MyOperator implements FlowableOperator<Integer, Integer> { + + @Override + public Subscriber<? super Integer> apply(Subscriber<? super Integer> child) { + return new Op(child); + } + + static final class Op implements FlowableSubscriber<Integer>, Subscription { + final Subscriber<? super Integer> child; + + Subscription s; + + public Op(Subscriber<? super Integer> child) { + this.child = child; + } + + @Override + pubic void onSubscribe(Subscription s) { + this.s = s; + child.onSubscribe(this); + } + + @Override + public void onNext(Integer v) { + child.onNext(v * v); + } + + @Override + public void onError(Throwable e) { + child.onError(e); + } + + @Override + public void onComplete() { + child.onComplete(); + } + + @Override + public void cancel() { + s.cancel(); + } + + @Override + public void request(long n) { + s.request(n); + } + } +} +``` + +You may recognize that implementing operators via extension or lifting looks quite similar. In both cases, one usually implements a `FlowableSubscriber` (`Observer`, etc) that takes a downstream `Subscriber`, implements the business logic in the `onXXX` methods and somehow (manually or as part of `lift()`'s lifecycle) gets subscribed to an upstream source. + +The benefit of applying the Reactive-Streams design to all base reactive types is that each consumer type is now an interface and can be applied to operators that have to extend some class. This was a pain in 1.x because `Subscriber` and `SingleSubscriber` are classes themselves, plus `Subscriber.request()` is a protected-final method and an operator's `Subscriber` can't implement the `Producer` interface at the same time. In 2.x there is no such problem and one can have both `Subscriber`, `Subscription` or even `Observer` together in the same consumer type. + +# Operator fusion + +Operator fusion has the premise that certain operators can be combined into one single operator (macro-fusion) or their internal data structures shared between each other (micro-fusion) that allows fewer allocations, lower overhead and better performance. + +This advanced concept was invented, worked out and studied in the [Reactive-Streams-Commons](https://github.com/reactor/reactive-streams-commons) research project manned by the leads of RxJava and Project Reactor. Both libraries use the results in their implementation, which look the same but are incompatible due to different classes and packages involved. In addition, RxJava 2.x's approach is a more polished version of the invention due to delays between the two project's development. + +Since operator-fusion is optional, you may chose to not bother making your operator fusion-enabled. The `DeferredScalarSubscription` is fusion-enabled and needs no additional development in this regard though. + +If you chose to ignore operator-fusion, you still have to follow the requirement of never forwarding a `Subscription`/`Disposable` coming through `onSubscribe` of `Subscriber`/`Observer` as this may break the fusion protocol and may skip your operator's business logic entirely: + +```java +final class SomeOp<T> implements Subscriber<T>, Subscription { + + // ... + Subscription s; + + public void onSubscribe(Subscription s) { + this.s = s; + child.onSubscribe(this); // <--------------------------- + } + + @Override + public void cancel() { + s.cancel(); + } + + @Override + public void request(long n) { + s.request(n); + } + + // ... +} +``` + +Yes, this adds one more indirection between operators but it is still cheap (and would be necessary for the operator anyway) but enables huge performance gains with the right chain of operators. + +## Generations + +Given this novel approach, a generation number can be assigned to various implementation styles of reactive architectures: + +#### Generation 0 +These are the classical libraries that either use `java.util.Observable` or are listener based (Java Swing's `ActionListener`). Their common property is that they don't support composition (of events and cancellation). See also **Google Agera**. + +#### Generation 1 +This is the level of the **Rx.NET** library (even up to 3.x) that supports composition, but has no notion for backpressure and doesn't properly support synchronous cancellation. Many JavaScript libraries such as **RxJS 5** are still on this level. See also **Google gRPC**. + +#### Generation 2 +This is what **RxJava 1.x** is categorized, it supports composition, backpressure and synchronous cancellation along with the ability to lift an operator into a sequence. + +#### Generation 3 +This is the level of the Reactive-Streams based libraries such as **Reactor 2** and **Akka-Stream**. They are based upon a specification that evolved out of RxJava but left behind its drawbacks (such as the need to return anything from `subscribe()`). This is incompatible with RxJava 1.x and thus 2.x had to be rewritten from scratch. + +#### Generation 4 +This level expands upon the Reactive-Streams interfaces with operator-fusion (in a compatible fashion, that is, op-fusion is optional between two stages and works without them). **Reactor 3** and **RxJava 2** are at this level. The material around **Akka-Stream** mentions operator-fusion as well, however, **Akka-Stream** is not a native Reactive-Streams implementation (requires a materializer to get a `Publisher` out) and as such it is only Gen 3. + +There are discussions among the 4th generation library providers to have the elements of operator-fusion standardized in Reactive-Streams 2.0 specification (or in a neighboring extension) and have **RxJava 3** and **Reactor 4** work together on that aspect as well. + +## Components + +### Callable and ScalarCallable + +Certain `Flowable` sources, similar to `Single` or `Completable` are known to ever emit zero or one item and that single item is known to be constant or is computed synchronously. Well known examples of this are `just()`, `empty()` and `fromCallable`. Subscribing to these sources, like any other sources, adds the same infrastructure overhead which can often be avoided if the consumer could just pick or have the item calculated on the spot. + +For example, `just` and `empty` appears as the mapping result of a `flatMap` operation: + +```java +source.flatMap(v -> { + if (v % 2 == 0) { + return just(v); + } + return empty(); +}) +``` + +Here, if we'd somehow recognize that `empty()` won't emit a value but only `onComplete` we could simply avoid subscribing to it inside `flatMap`, saving on the overhead. Similarly, recognizing that `just` emits exactly one item we can route it differently inside `flatMap` and again, avoiding creating a lot of objects to get to the same single item. + +In other times, knowing the emission property can simplify or chose a different operator instead of the applied one. For example, applying `flatMap` to an `empty()` source has no use since there won't be any item to be flattened into a sequence; the whole flattened sequence is going to be empty. Knowing that a source is `just` to `flatMap`, there is no need for the complicated inner mechanisms as there is going to be only one mapped inner source and one can subscribe the downstream's `Subscriber` to it directly. + +```java +Flowable.just(1).flatMap(v -> Flowable.range(v, 5)).subscribe(...); + +// in some specialized operator: + +T value; // from just() + +@Override +public void subscribeActual(Subscriber<? super T> s) { + mapper.apply(value).subscribe(s); +} +``` + +There could be other sources with these properties, therefore, RxJava 2 uses the `io.reactivex.internal.fusion.ScalarCallable` and `java.util.Callable` interfaces to indicate a source is a constant or sequentially computable. When a source `Flowable` or `Observable` is marked with one of these interfaces, many fusion enabled operators will perform special actions to avoid the overhead of a normal and general source. + +We use Java's own and preexisting `java.util.Callable` interface to indicate a synchronously computable source. The `ScalarCallable` is an extension to this interface by which it suppresses the `throws Exception` of `Callable.call()`: + +```java +interface Callable<T> { + T call() throws Exception; +} + +interface ScalarCallable<T> extends Callable<T> { + @Override + T call(); +} +``` + +The reason for the two separate interfaces is that if a source is constant, like `just`, one can perform assembly-time optimizations with it knowing that each regular `subscribe` invocation would have resulted in the same single value. + +`Callable` denotes sources, such as `fromCallable` that indicates the single value has to be calculated at runtime of the flow. By this logic, you can see that `ScalarCallable` is a `Callable` on its own right because the constant can be "calculated" as late as the runtime phase of the flow. + +Since Reactive-Streams forbids using `null`s as emission values, we can use `null` in `(Scalar)Callable` marked sources to indicate there is no value to be emitted, thus one can't mistake an user's `null` with the empty indicator `null`. For example, this is how `empty()` is implemented: + +```java +final class FlowableEmpty extends Flowable<Object> implements ScalarCallable<Object> { + @Override + public void subscribeActual(Subscriber<? super T> s) { + EmptySubscription.complete(s); + } + + @Override + public Object call() { + return null; // interpreted as no value available + } +} +``` + +Sources implementing `Callable` may throw checked exceptions from `call()` which is handled by the consumer operators as an indication to signal `onError` in an operator specific manner (such as delayed). + +```java +final class FlowableIOException extends Flowable<Object> implements Callable<Object> { + @Override + public void subscribeActual(Subscriber<? super T> s) { + EmptySubscription.error(new IOException(), s); + } + + @Override + public Object call() throws Exception { + throw new IOException(); + } +} +``` + +However, implementors of `ScalarCallable` should avoid throwing any exception and limit the code in `call()` be constant or simple computation that can be legally executed during assembly time. + +As the consumer of sources, one may want to deal with such kind of special `Flowable`s or `Observable`s. For example, if you create an operator that can leverage the knowledge of a single element source as its main input, you can check the types and extract the value of a `ScalarCallable` at assembly time right in the operator: + +```java +// Flowable.java +public final Flowable<Integer> plusOne() { + if (this instanceof ScalarCallable) { + Integer value = ((ScalarCallable<Integer>)this).call(); + if (value == null) { + return empty(); + } + return just(value + 1); + } + return cast(Integer.class).map(v -> v + 1); +} +``` + +or as a `FlowableTransformer`: + +```java +FlowableTransformer<Integer, Integer> plusOneTransformer = source -> { + if (source instanceof ScalarCallable) { + Integer value = ((ScalarCallable<Integer>)source).call(); + if (value == null) { + return empty(); + } + return just(value + 1); + } + return source.map(v -> v + 1); +}; +``` + +However, it is not mandatory to handle `ScalarCallable`s and `Callable`s separately. Since the former extends the latter, the type check can be deferred till subscription time and handled with the same code path: + +```java +final class FlowablePlusOne extends Flowable<Integer> { + final Publisher<Integer> source; + + FlowablePlusOne(Publisher<Integer> source) { + this.source = source; + } + + @Override + public void subscribeActual(Subscriber<? super Integer> s) { + if (source instanceof Callable) { + Integer value; + + try { + value = ((Callable<Integer>)source).call(); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + EmptySubscription.error(ex, s); + return; + } + + s.onSubscribe(new ScalarSubscription<Integer>(s, value + 1)); + } else { + new FlowableMap<>(source, v -> v + 1).subscribe(s); + } + } +} +``` + +### ConditionalSubscriber + +TBD + +### QueueSubscription and QueueDisposable + +TBD + +# Example implementations + +TBD + +## `map` + `filter` hybrid + +TBD + +## Ordered `merge` + +TBD diff --git a/docs/_Footer.md b/docs/_Footer.md new file mode 100644 index 0000000000..8ecc48ce6f --- /dev/null +++ b/docs/_Footer.md @@ -0,0 +1,2 @@ +**Copyright (c) 2016-present, RxJava Contributors.** +[Twitter @RxJava](https://twitter.com/#!/RxJava) | [Gitter @RxJava](https://gitter.im/ReactiveX/RxJava) diff --git a/docs/_Sidebar.md b/docs/_Sidebar.md new file mode 100644 index 0000000000..22961acec3 --- /dev/null +++ b/docs/_Sidebar.md @@ -0,0 +1,32 @@ +* [Introduction](https://github.com/ReactiveX/RxJava/wiki/Home) +* [Getting Started](https://github.com/ReactiveX/RxJava/wiki/Getting-Started) +* [How to Use RxJava](https://github.com/ReactiveX/RxJava/wiki/How-To-Use-RxJava) +* [Reactive Streams](https://github.com/ReactiveX/RxJava/wiki/Reactive-Streams) +* [The reactive types of RxJava](https://github.com/ReactiveX/RxJava/wiki/Observable) +* [Schedulers](https://github.com/ReactiveX/RxJava/wiki/Scheduler) +* [Subjects](https://github.com/ReactiveX/RxJava/wiki/Subject) +* [Error Handling](https://github.com/ReactiveX/RxJava/wiki/Error-Handling) +* [Operators (Alphabetical List)](https://github.com/ReactiveX/RxJava/wiki/Alphabetical-List-of-Observable-Operators) + * [Async](https://github.com/ReactiveX/RxJava/wiki/Async-Operators) + * [Blocking](https://github.com/ReactiveX/RxJava/wiki/Blocking-Observable-Operators) + * [Combining](https://github.com/ReactiveX/RxJava/wiki/Combining-Observables) + * [Conditional & Boolean](https://github.com/ReactiveX/RxJava/wiki/Conditional-and-Boolean-Operators) + * [Connectable](https://github.com/ReactiveX/RxJava/wiki/Connectable-Observable-Operators) + * [Creation](https://github.com/ReactiveX/RxJava/wiki/Creating-Observables) + * [Error management](https://github.com/ReactiveX/RxJava/wiki/Error-Handling-Operators) + * [Filtering](https://github.com/ReactiveX/RxJava/wiki/Filtering-Observables) + * [Mathematical and Aggregate](https://github.com/ReactiveX/RxJava/wiki/Mathematical-and-Aggregate-Operators) + * [Parallel flows](https://github.com/ReactiveX/RxJava/wiki/Parallel-flows) + * [String](https://github.com/ReactiveX/RxJava/wiki/String-Observables) + * [Transformation](https://github.com/ReactiveX/RxJava/wiki/Transforming-Observables) + * [Utility](https://github.com/ReactiveX/RxJava/wiki/Observable-Utility-Operators) + * [Notable 3rd party Operators (Alphabetical List)](https://github.com/ReactiveX/RxJava/wiki/Alphabetical-List-of-3rd-party-Operators) + * [Operator matrix](https://github.com/ReactiveX/RxJava/wiki/Operator-Matrix) +* [Plugins](https://github.com/ReactiveX/RxJava/wiki/Plugins) +* [How to Contribute](https://github.com/ReactiveX/RxJava/wiki/How-to-Contribute) +* [Writing operators](https://github.com/ReactiveX/RxJava/wiki/Writing-operators-for-2.0) +* [Backpressure](https://github.com/ReactiveX/RxJava/wiki/Backpressure-(2.0)) + * [another explanation](https://github.com/ReactiveX/RxJava/wiki/Backpressure) +* JavaDoc: [1.x](http://reactivex.io/RxJava/1.x/javadoc), [2.x](http://reactivex.io/RxJava/2.x/javadoc), [3.x](http://reactivex.io/RxJava/3.x/javadoc) +* [Coming from RxJava 1](https://github.com/ReactiveX/RxJava/wiki/What's-different-in-2.0) +* [Additional Reading](https://github.com/ReactiveX/RxJava/wiki/Additional-Reading) diff --git a/docs/_Sidebar.md.md b/docs/_Sidebar.md.md new file mode 100644 index 0000000000..32e201fc46 --- /dev/null +++ b/docs/_Sidebar.md.md @@ -0,0 +1,32 @@ +* [Introduction](https://github.com/ReactiveX/RxJava/wiki/Home) +* [Getting Started](https://github.com/ReactiveX/RxJava/wiki/Getting-Started) +* [How to Use RxJava](https://github.com/ReactiveX/RxJava/wiki/How-To-Use-RxJava) +* [Reactive Streams](https://github.com/ReactiveX/RxJava/wiki/Reactive-Streams) +* [The reactive types of RxJava](https://github.com/ReactiveX/RxJava/wiki/Observable) +* [Schedulers](https://github.com/ReactiveX/RxJava/wiki/Scheduler) +* [Subjects](https://github.com/ReactiveX/RxJava/wiki/Subject) +* [Error Handling](https://github.com/ReactiveX/RxJava/wiki/Error-Handling) +* [Operators (Alphabetical List)](https://github.com/ReactiveX/RxJava/wiki/Alphabetical-List-of-Observable-Operators) + * [Async](https://github.com/ReactiveX/RxJava/wiki/Async-Operators) + * [Blocking](https://github.com/ReactiveX/RxJava/wiki/Blocking-Observable-Operators) + * [Combining](https://github.com/ReactiveX/RxJava/wiki/Combining-Observables) + * [Conditional & Boolean](https://github.com/ReactiveX/RxJava/wiki/Conditional-and-Boolean-Operators) + * [Connectable](https://github.com/ReactiveX/RxJava/wiki/Connectable-Observable-Operators) + * [Creation](https://github.com/ReactiveX/RxJava/wiki/Creating-Observables) + * [Error management](https://github.com/ReactiveX/RxJava/wiki/Error-Handling-Operators) + * [Filtering](https://github.com/ReactiveX/RxJava/wiki/Filtering-Observables) + * [Mathematical and Aggregate](https://github.com/ReactiveX/RxJava/wiki/Mathematical-and-Aggregate-Operators) + * [Parallel flows](https://github.com/ReactiveX/RxJava/wiki/Parallel-flows) + * [String](https://github.com/ReactiveX/RxJava/wiki/String-Observables) + * [Transformation](https://github.com/ReactiveX/RxJava/wiki/Transforming-Observables) + * [Utility](https://github.com/ReactiveX/RxJava/wiki/Observable-Utility-Operators) + * [Notable 3rd party Operators (Alphabetical List)](https://github.com/ReactiveX/RxJava/wiki/Alphabetical-List-of-3rd-party-Operators) + * [Operator matrix](https://github.com/ReactiveX/RxJava/wiki/Operator-Matrix) +* [Plugins](https://github.com/ReactiveX/RxJava/wiki/Plugins) +* [How to Contribute](https://github.com/ReactiveX/RxJava/wiki/How-to-Contribute) +* [Writing operators](https://github.com/ReactiveX/RxJava/wiki/Writing-operators-for-2.0) +* [Backpressure](https://github.com/ReactiveX/RxJava/wiki/Backpressure-(2.0)) + * [another explanation](https://github.com/ReactiveX/RxJava/wiki/Backpressure) +* [JavaDoc](http://reactivex.io/RxJava/2.x/javadoc) +* [Coming from RxJava 1](https://github.com/ReactiveX/RxJava/wiki/What's-different-in-2.0) +* [Additional Reading](https://github.com/ReactiveX/RxJava/wiki/Additional-Reading) diff --git a/gradle.properties b/gradle.properties index 1b124d874e..e685b8103a 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1 +1,24 @@ -version=0.5.1-SNAPSHOT +group=io.reactivex.rxjava3 +version=3.0.0-SNAPSHOT +description=RxJava: Reactive Extensions for the JVM – a library for composing asynchronous and event-based programs using observable sequences for the Java VM. + +POM_ARTIFACT_ID=rxjava +POM_NAME=RxJava +POM_PACKAGING=jar + +POM_DESCRIPTION=Reactive Extensions for Java +POM_INCEPTION_YEAR=2013 + +POM_URL=https://github.com/ReactiveX/RxJava +POM_SCM_URL=https://github.com/ReactiveX/RxJava +POM_SCM_CONNECTION=scm:git:git://github.com/ReactiveX/RxJava.git +POM_SCM_DEV_CONNECTION=scm:git:ssh://git@github.com/ReactiveX/RxJava.git + +POM_LICENCE_NAME=The Apache Software License, Version 2.0 +POM_LICENCE_URL=https://www.apache.org/licenses/LICENSE-2.0.txt +POM_LICENCE_DIST=repo + +POM_DEVELOPER_ID=akarnokd +POM_DEVELOPER_NAME=David Karnok +POM_DEVELOPER_URL=https://github.com/akarnokd/ +POM_DEVELOPER_EMAIL=akarnokd@gmail.com diff --git a/gradle/buildscript.gradle b/gradle/buildscript.gradle deleted file mode 100644 index 4d6a29aabe..0000000000 --- a/gradle/buildscript.gradle +++ /dev/null @@ -1,13 +0,0 @@ -// Executed in context of buildscript -repositories { - // Repo in addition to maven central - maven { - name 'build-repo' - url 'https://raw.github.com/Netflix-Skunkworks/build-repo/master/releases/' // gradle-release/gradle-release/1.0-SNAPSHOT/gradle-release-1.0-SNAPSHOT.jar - } -} -dependencies { - classpath 'nl.javadude.gradle.plugins:license-gradle-plugin:0.6.0' - classpath 'com.mapvine:gradle-cobertura-plugin:0.1' - classpath 'gradle-release:gradle-release:1.0-SNAPSHOT' -} diff --git a/gradle/check.gradle b/gradle/check.gradle deleted file mode 100644 index 7617f17b35..0000000000 --- a/gradle/check.gradle +++ /dev/null @@ -1,25 +0,0 @@ -subprojects { - // Checkstyle - apply plugin: 'checkstyle' - tasks.withType(Checkstyle) { ignoreFailures = true } - checkstyle { - ignoreFailures = true // Waiting on GRADLE-2163 - configFile = rootProject.file('codequality/checkstyle.xml') - } - - // FindBugs - apply plugin: 'findbugs' - //tasks.withType(Findbugs) { reports.html.enabled true } - - // PMD - apply plugin: 'pmd' - //tasks.withType(Pmd) { reports.html.enabled true } - - apply plugin: 'cobertura' - cobertura { - sourceDirs = sourceSets.main.java.srcDirs - format = 'html' - includes = ['**/*.java', '**/*.groovy'] - excludes = [] - } -} diff --git a/gradle/convention.gradle b/gradle/convention.gradle deleted file mode 100644 index 8b877071d9..0000000000 --- a/gradle/convention.gradle +++ /dev/null @@ -1,83 +0,0 @@ - -// For Artifactory -rootProject.status = version.contains('-SNAPSHOT')?'snapshot':'release' - -subprojects { project -> - apply plugin: 'java' // Plugin as major conventions - - version = rootProject.version - - sourceCompatibility = 1.6 - - // GRADLE-2087 workaround, perform after java plugin - status = rootProject.status - - task sourcesJar(type: Jar, dependsOn:classes) { - from sourceSets.main.allSource - classifier 'sources' - extension 'jar' - } - - task javadocJar(type: Jar, dependsOn:javadoc) { - from javadoc.destinationDir - classifier 'javadoc' - extension 'jar' - } - - configurations.add('sources') - configurations.add('javadoc') - configurations.archives { - extendsFrom configurations.sources - extendsFrom configurations.javadoc - } - - // When outputing to an Ivy repo, we want to use the proper type field - gradle.taskGraph.whenReady { - def isNotMaven = !it.hasTask(project.uploadMavenCentral) - if (isNotMaven) { - def artifacts = project.configurations.sources.artifacts - def sourceArtifact = artifacts.iterator().next() - sourceArtifact.type = 'sources' - } - } - - artifacts { - sources(sourcesJar) { - // Weird Gradle quirk where type will be used for the extension, but only for sources - type 'jar' - } - javadoc(javadocJar) { - type 'javadoc' - } - } - - // Ensure output is on a new line - javadoc.doFirst { println "" } - - configurations { - provided { - description = 'much like compile, but indicates you expect the JDK or a container to provide it. It is only available on the compilation classpath, and is not transitive.' - transitive = true - visible = true - } - } - - project.sourceSets { - main.compileClasspath += project.configurations.provided - main.runtimeClasspath -= project.configurations.provided - test.compileClasspath += project.configurations.provided - test.runtimeClasspath += project.configurations.provided - } -} - -task aggregateJavadoc(type: Javadoc) { - description = 'Aggregate all subproject docs into a single docs directory' - source subprojects.collect {project -> project.sourceSets.main.allJava } - classpath = files(subprojects.collect {project -> project.sourceSets.main.compileClasspath}) - destinationDir = new File(projectDir, 'doc') -} - -// Generate wrapper, which is distributed as part of source to alleviate the need of installing gradle -task createWrapper(type: Wrapper) { - gradleVersion = '1.1' -} diff --git a/gradle/doclet-exclude.jar b/gradle/doclet-exclude.jar deleted file mode 100644 index 4e4fd96380..0000000000 Binary files a/gradle/doclet-exclude.jar and /dev/null differ diff --git a/gradle/javadocStyleSheet.css b/gradle/javadocStyleSheet.css deleted file mode 100644 index fdaada472e..0000000000 --- a/gradle/javadocStyleSheet.css +++ /dev/null @@ -1,59 +0,0 @@ -# originally from http://sensemaya.org/files/stylesheet.css and then modified -# http://sensemaya.org/maya/2009/07/10/making-javadoc-more-legible - -/* Javadoc style sheet */ - -/* Define colors, fonts and other style attributes here to override the defaults */ - -/* Page background color */ -body { background-color: #FFFFFF; color:#333; font-size: 100%; } - -body { font-size: 0.875em; line-height: 1.286em; font-family: "Helvetica", "Arial", sans-serif; } - -code { color: #777; line-height: 1.286em; font-family: "Consolas", "Lucida Console", "Droid Sans Mono", "Andale Mono", "Monaco", "Lucida Sans Typewriter"; } - -a { text-decoration: none; color: #16569A; /* also try #2E85ED, #0033FF, #6C93C6, #1D7BBE, #1D8DD2 */ } -a:hover { text-decoration: underline; } - - -table[border="1"] { border: 1px solid #ddd; } -table[border="1"] td, table[border="1"] th { border: 1px solid #ddd; } -table[cellpadding="3"] td { padding: 0.5em; } - -font[size="-1"] { font-size: 0.85em; line-height: 1.5em; } -font[size="-2"] { font-size: 0.8em; } -font[size="+2"] { font-size: 1.4em; line-height: 1.3em; padding: 0.4em 0; } - -/* Headings */ -h1 { font-size: 1.5em; line-height: 1.286em;} -h2.title { color: #c81f08; } - -/* Table colors */ -.TableHeadingColor { background: #ccc; color:#444; } /* Dark mauve */ -.TableSubHeadingColor { background: #ddd; color:#444; } /* Light mauve */ -.TableRowColor { background: #FFFFFF; color:#666; font-size: 0.95em; } /* White */ -.TableRowColor code { color:#000; } /* White */ - -/* Font used in left-hand frame lists */ -.FrameTitleFont { font-size: 100%; } -.FrameHeadingFont { font-size: 90%; } -.FrameItemFont { font-size: 0.9em; line-height: 1.3em; -} -/* Java Interfaces */ -.FrameItemFont a i { - font-style: normal; color: #16569A; -} -.FrameItemFont a:hover i { - text-decoration: underline; -} - - -/* Navigation bar fonts and colors */ -.NavBarCell1 { background-color:#E0E6DF; } /* Light mauve */ -.NavBarCell1Rev { background-color:#16569A; color:#FFFFFF} /* Dark Blue */ -.NavBarFont1 { } -.NavBarFont1Rev { color:#FFFFFF; } - -.NavBarCell2 { background-color:#FFFFFF; color:#000000} -.NavBarCell3 { background-color:#FFFFFF; color:#000000} - diff --git a/gradle/javadoc_cleanup.gradle b/gradle/javadoc_cleanup.gradle new file mode 100644 index 0000000000..63b4f7f045 --- /dev/null +++ b/gradle/javadoc_cleanup.gradle @@ -0,0 +1,74 @@ +// remove the excessive whitespaces between method arguments in the javadocs +task javadocCleanup(dependsOn: "javadoc") doLast { + fixJavadocFile(rootProject.file('build/docs/javadoc/io/reactivex/rxjava3/core/Flowable.html')) + fixJavadocFile(rootProject.file('build/docs/javadoc/io/reactivex/rxjava3/core/Observable.html')) + fixJavadocFile(rootProject.file('build/docs/javadoc/io/reactivex/rxjava3/core/Single.html')) + fixJavadocFile(rootProject.file('build/docs/javadoc/io/reactivex/rxjava3/core/Maybe.html')) + fixJavadocFile(rootProject.file('build/docs/javadoc/io/reactivex/rxjava3/core/Completable.html')) + + fixJavadocFile(rootProject.file('build/docs/javadoc/io/reactivex/rxjava3/flowables/ConnectableFlowable.html')) + fixJavadocFile(rootProject.file('build/docs/javadoc/io/reactivex/rxjava3/observables/ConnectableObservable.html')) + + fixJavadocFile(rootProject.file('build/docs/javadoc/io/reactivex/rxjava3/subjects/ReplaySubject.html')) + fixJavadocFile(rootProject.file('build/docs/javadoc/io/reactivex/rxjava3/processors/ReplayProcessor.html')) + fixJavadocFile(rootProject.file('build/docs/javadoc/io/reactivex/rxjava3/subjects/PublishSubject.html')) + fixJavadocFile(rootProject.file('build/docs/javadoc/io/reactivex/rxjava3/processors/PublishProcessor.html')) + fixJavadocFile(rootProject.file('build/docs/javadoc/io/reactivex/rxjava3/subjects/AsyncSubject.html')) + fixJavadocFile(rootProject.file('build/docs/javadoc/io/reactivex/rxjava3/processors/AsyncProcessor.html')) + fixJavadocFile(rootProject.file('build/docs/javadoc/io/reactivex/rxjava3/subjects/BehaviorSubject.html')) + fixJavadocFile(rootProject.file('build/docs/javadoc/io/reactivex/rxjava3/processors/BehaviorProcessor.html')) + fixJavadocFile(rootProject.file('build/docs/javadoc/io/reactivex/rxjava3/processors/MulticastProcessor.html')) + fixJavadocFile(rootProject.file('build/docs/javadoc/io/reactivex/rxjava3/subjects/UnicastSubject.html')) + fixJavadocFile(rootProject.file('build/docs/javadoc/io/reactivex/rxjava3/processors/UnicastProcessor.html')) + + fixJavadocFile(rootProject.file('build/docs/javadoc/io/reactivex/rxjava3/plugins/RxJavaPlugins.html')) + + fixJavadocFile(rootProject.file('build/docs/javadoc/io/reactivex/rxjava3/parallel/ParallelFlowable.html')) + + fixJavadocFile(rootProject.file('build/docs/javadoc/io/reactivex/rxjava3/disposables/Disposable.html')) + + fixJavadocFile(rootProject.file('build/docs/javadoc/io/reactivex/rxjava3/observers/TestObserver.html')) + fixJavadocFile(rootProject.file('build/docs/javadoc/io/reactivex/rxjava3/observers/BaseTestConsumer.html')) + fixJavadocFile(rootProject.file('build/docs/javadoc/io/reactivex/rxjava3/subscribers/TestSubscriber.html')) +} + +def fixJavadocFile(file) { + logger.lifecycle("Cleaning up: " + file) + String fileContents = file.getText('UTF-8') + + // lots of spaces after the previous method argument + fileContents = fileContents.replaceAll(",\\s{4,}", ",\n ") + + // lots of spaces after the @NonNull annotations + fileContents = fileContents.replaceAll("@NonNull</a>\\s{4,}", "@NonNull</a> ") + + // lots of spaces after the @Nullable annotations + fileContents = fileContents.replaceAll("@Nullable</a>\\s{4,}", "@Nullable</a> ") + + // javadoc bug: duplicates the link to @NonNull for some reason + def nonNullText1 = "<a href=\"../annotations/NonNull.html\" title=\"annotation in io.reactivex.rxjava3.annotations\">@NonNull</a>" + + fileContents = fileContents.replace(nonNullText1 + " " + nonNullText1, nonNullText1) + fileContents = fileContents.replace(nonNullText1 + "\n " + nonNullText1, nonNullText1) + fileContents = fileContents.replace(nonNullText1 + "\r\n " + nonNullText1, nonNullText1) + + def nonNullText2 = "<a href=\"../../../../io/reactivex/rxjava3/annotations/NonNull.html\" title=\"annotation in io.reactivex.rxjava3.annotations\">@NonNull</a>" + fileContents = fileContents.replace(nonNullText2 + " " + nonNullText2, nonNullText2) + fileContents = fileContents.replace(nonNullText2 + "\n " + nonNullText2, nonNullText2) + fileContents = fileContents.replace(nonNullText2 + "\r\n " + nonNullText2, nonNullText2) + + // javadoc bug: duplicates the link to @Nullable for some reason + def nullableText1 = "<a href=\"../annotations/Nullable.html\" title=\"annotation in io.reactivex.rxjava3.annotations\">@Nullable</a>" + + fileContents = fileContents.replace(nullableText1 + " " + nullableText1, nullableText1) + fileContents = fileContents.replace(nullableText1 + "\n " + nullableText1, nullableText1) + fileContents = fileContents.replace(nullableText1 + "\r\n " + nullableText1, nullableText1) + + def nullableText2 = "<a href=\"../../../../io/reactivex/rxjava3/annotations/Nullable.html\" title=\"annotation in io.reactivex.rxjava3.annotations\">@Nullable</a>" + + fileContents = fileContents.replace(nullableText2 + " " + nullableText2, nullableText2) + fileContents = fileContents.replace(nullableText2 + "\n " + nullableText2, nullableText2) + fileContents = fileContents.replace(nullableText2 + "\r\n " + nullableText2, nullableText2) + + file.setText(fileContents, 'UTF-8') +} diff --git a/gradle/license.gradle b/gradle/license.gradle deleted file mode 100644 index 11a51f1137..0000000000 --- a/gradle/license.gradle +++ /dev/null @@ -1,9 +0,0 @@ -// Dependency for plugin was set in buildscript.gradle - -subprojects { -apply plugin: 'license' //nl.javadude.gradle.plugins.license.LicensePlugin -license { - header rootProject.file('codequality/HEADER') - ext.year = Calendar.getInstance().get(Calendar.YEAR) -} -} diff --git a/gradle/maven.gradle b/gradle/maven.gradle deleted file mode 100644 index a3a3d44240..0000000000 --- a/gradle/maven.gradle +++ /dev/null @@ -1,62 +0,0 @@ -// Maven side of things -subprojects { - apply plugin: 'maven' // Java plugin has to have been already applied for the conf2scope mappings to work - apply plugin: 'signing' - - signing { - required { gradle.taskGraph.hasTask(uploadMavenCentral) } - sign configurations.archives - } - - /** - * Publishing to Maven Central example provided from http://jedicoder.blogspot.com/2011/11/automated-gradle-project-deployment-to.html - */ - task uploadMavenCentral(type:Upload, dependsOn: signArchives) { - configuration = configurations.archives - doFirst { - repositories.mavenDeployer { - beforeDeployment { org.gradle.api.artifacts.maven.MavenDeployment deployment -> signing.signPom(deployment) } - - // To test deployment locally, use the following instead of oss.sonatype.org - //repository(url: "file://localhost/${rootProject.rootDir}/repo") - - repository(url: 'https://oss.sonatype.org/service/local/staging/deploy/maven2') { - authentication(userName: rootProject.sonatypeUsername, password: rootProject.sonatypePassword) - } - - // Prevent datastamp from being appending to artifacts during deployment - uniqueVersion = false - - // Closure to configure all the POM with extra info, common to all projects - pom.project { - name "${project.name}" - description "${project.name} developed by Netflix" - developers { - developer { - id 'netflixgithub' - name 'Netflix Open Source Development' - email 'talent@netflix.com' - } - } - licenses { - license { - name 'The Apache Software License, Version 2.0' - url 'http://www.apache.org/licenses/LICENSE-2.0.txt' - distribution 'repo' - } - } - url "https://github.com/Netflix/$%7BrootProject.githubProjectName%7D" - scm { - connection "scm:git:git@github.com:Netflix/${rootProject.githubProjectName}.git" - url "scm:git:git@github.com:Netflix/${rootProject.githubProjectName}.git" - developerConnection "scm:git:git@github.com:Netflix/${rootProject.githubProjectName}.git" - } - issueManagement { - system 'github' - url "https://github.com/Netflix/$%7BrootProject.githubProjectName%7D/issues" - } - } - } - } - } -} diff --git a/gradle/netflix-oss.gradle b/gradle/netflix-oss.gradle deleted file mode 100644 index a87bc54efe..0000000000 --- a/gradle/netflix-oss.gradle +++ /dev/null @@ -1 +0,0 @@ -apply from: 'http://artifacts.netflix.com/gradle-netflix-local/artifactory.gradle' diff --git a/gradle/release.gradle b/gradle/release.gradle deleted file mode 100644 index cd135643bd..0000000000 --- a/gradle/release.gradle +++ /dev/null @@ -1,65 +0,0 @@ -apply plugin: 'release' - -// Ignore release plugin's task because it calls out via GradleBuild. This is a good place to put an email to send out -task release(overwrite: true, dependsOn: commitNewVersion) << { - // This is a good place to put an email to send out -} -commitNewVersion.dependsOn updateVersion -updateVersion.dependsOn createReleaseTag -createReleaseTag.dependsOn preTagCommit -def buildTasks = tasks.matching { it.name =~ /:build/ } -preTagCommit.dependsOn buildTasks -preTagCommit.dependsOn checkSnapshotDependencies -//checkSnapshotDependencies.dependsOn confirmReleaseVersion // Introduced in 1.0, forces readLine -//confirmReleaseVersion.dependsOn unSnapshotVersion -checkSnapshotDependencies.dependsOn unSnapshotVersion // Remove once above is fixed -unSnapshotVersion.dependsOn checkUpdateNeeded -checkUpdateNeeded.dependsOn checkCommitNeeded -checkCommitNeeded.dependsOn initScmPlugin - -[ - uploadIvyLocal: 'uploadLocal', - uploadArtifactory: 'artifactoryPublish', // Call out to compile against internal repository - buildWithArtifactory: 'build' // Build against internal repository -].each { key, value -> - // Call out to compile against internal repository - task "${key}"(type: GradleBuild) { - startParameter = project.gradle.startParameter.newInstance() - startParameter.addInitScript( file('gradle/netflix-oss.gradle') ) - startParameter.getExcludedTaskNames().add('check') - tasks = [ 'build', value ] - } -} -task releaseArtifactory(dependsOn: [checkSnapshotDependencies, uploadArtifactory]) - -// Ensure upload happens before taggging but after all pre-checks -releaseArtifactory.dependsOn checkSnapshotDependencies -createReleaseTag.dependsOn releaseArtifactory -gradle.taskGraph.whenReady { taskGraph -> - if ( taskGraph.hasTask(uploadArtifactory) && rootProject.status == 'release' && !taskGraph.hasTask(':release') ) { - throw new GradleException('"release" task has to be run before uploading a release to Artifactory') - } -} -subprojects.each { project -> - project.uploadMavenCentral.dependsOn rootProject.checkSnapshotDependencies - rootProject.createReleaseTag.dependsOn project.uploadMavenCentral - - gradle.taskGraph.whenReady { taskGraph -> - if ( taskGraph.hasTask(project.uploadMavenCentral) && !taskGraph.hasTask(':release') ) { - throw new GradleException('"release" task has to be run before uploading to Maven Central') - } - } -} - -// Prevent plugin from asking for a version number interactively -ext.'gradle.release.useAutomaticVersion' = "true" - -release { - // http://tellurianring.com/wiki/gradle/release - failOnCommitNeeded=true - failOnPublishNeeded=true - failOnUnversionedFiles=true - failOnUpdateNeeded=true - includeProjectNameInTag=true - requireBranch = null -} diff --git a/gradle/stylesheet.css b/gradle/stylesheet.css new file mode 100644 index 0000000000..60f1d665bf --- /dev/null +++ b/gradle/stylesheet.css @@ -0,0 +1,573 @@ +/* Javadoc style sheet */ +/* +Overall document style +*/ + +@import url('http://github.com/resources/fonts/dejavu.css'); + +body { + background-color:#ffffff; + color:#353833; + font-family:'DejaVu Sans', Arial, Helvetica, sans-serif; + font-size:14px; + margin:0; +} +a:link, a:visited { + text-decoration:none; + color:#4A6782; +} +a:hover, a:focus { + text-decoration:none; + color:#bb7a2a; +} +a:active { + text-decoration:none; + color:#4A6782; +} +a[name] { + color:#353833; +} +a[name]:hover { + text-decoration:none; + color:#353833; +} +pre { + font-family:'DejaVu Sans Mono', monospace; + font-size:14px; +} +h1 { + font-size:20px; +} +h2 { + font-size:18px; +} +h3 { + font-size:16px; + font-style:italic; +} +h4 { + font-size:13px; +} +h5 { + font-size:12px; +} +h6 { + font-size:11px; +} +ul { + list-style-type:disc; +} +code, tt { + font-family:'DejaVu Sans Mono', monospace; + font-size:14px; + padding-top:4px; + margin-top:8px; + line-height:1.4em; +} +dt code { + font-family:'DejaVu Sans Mono', monospace; + font-size:14px; + padding-top:4px; +} +table tr td dt code { + font-family:'DejaVu Sans Mono', monospace; + font-size:14px; + vertical-align:top; + padding-top:4px; +} +sup { + font-size:8px; +} +/* +Document title and Copyright styles +*/ +.clear { + clear:both; + height:0px; + overflow:hidden; +} +.aboutLanguage { + float:right; + padding:0px 21px; + font-size:11px; + z-index:200; + margin-top:-9px; +} +.legalCopy { + margin-left:.5em; +} +.bar a, .bar a:link, .bar a:visited, .bar a:active { + color:#FFFFFF; + text-decoration:none; +} +.bar a:hover, .bar a:focus { + color:#bb7a2a; +} +.tab { + background-color:#0066FF; + color:#ffffff; + padding:8px; + width:5em; + font-weight:bold; +} +/* +Navigation bar styles +*/ +.bar { + background-color:#4D7A97; + color:#FFFFFF; + padding:.8em .5em .4em .8em; + height:auto;/*height:1.8em;*/ + font-size:11px; + margin:0; +} +.topNav { + background-color:#4D7A97; + color:#FFFFFF; + float:left; + padding:0; + width:100%; + clear:right; + height:2.8em; + padding-top:10px; + overflow:hidden; + font-size:12px; +} +.bottomNav { + margin-top:10px; + background-color:#4D7A97; + color:#FFFFFF; + float:left; + padding:0; + width:100%; + clear:right; + height:2.8em; + padding-top:10px; + overflow:hidden; + font-size:12px; +} +.subNav { + background-color:#dee3e9; + float:left; + width:100%; + overflow:hidden; + font-size:12px; +} +.subNav div { + clear:left; + float:left; + padding:0 0 5px 6px; + text-transform:uppercase; +} +ul.navList, ul.subNavList { + float:left; + margin:0 25px 0 0; + padding:0; +} +ul.navList li{ + list-style:none; + float:left; + padding: 5px 6px; + text-transform:uppercase; +} +ul.subNavList li{ + list-style:none; + float:left; +} +.topNav a:link, .topNav a:active, .topNav a:visited, .bottomNav a:link, .bottomNav a:active, .bottomNav a:visited { + color:#FFFFFF; + text-decoration:none; + text-transform:uppercase; +} +.topNav a:hover, .bottomNav a:hover { + text-decoration:none; + color:#bb7a2a; + text-transform:uppercase; +} +.navBarCell1Rev { + background-color:#F8981D; + color:#253441; + margin: auto 5px; +} +.skipNav { + position:absolute; + top:auto; + left:-9999px; + overflow:hidden; +} +/* +Page header and footer styles +*/ +.header, .footer { + clear:both; + margin:0 20px; + padding:5px 0 0 0; +} +.indexHeader { + margin:10px; + position:relative; +} +.indexHeader span{ + margin-right:15px; +} +.indexHeader h1 { + font-size:13px; +} +.title { + color:#2c4557; + margin:10px 0; +} +.subTitle { + margin:5px 0 0 0; +} +.header ul { + margin:0 0 15px 0; + padding:0; +} +.footer ul { + margin:20px 0 5px 0; +} +.header ul li, .footer ul li { + list-style:none; + font-size:13px; +} +/* +Heading styles +*/ +div.details ul.blockList ul.blockList ul.blockList li.blockList h4, div.details ul.blockList ul.blockList ul.blockListLast li.blockList h4 { + background-color:#dee3e9; + border:1px solid #d0d9e0; + margin:0 0 6px -8px; + padding:7px 5px; +} +ul.blockList ul.blockList ul.blockList li.blockList h3 { + background-color:#dee3e9; + border:1px solid #d0d9e0; + margin:0 0 6px -8px; + padding:7px 5px; +} +ul.blockList ul.blockList li.blockList h3 { + padding:0; + margin:15px 0; +} +ul.blockList li.blockList h2 { + padding:0px 0 20px 0; +} +/* +Page layout container styles +*/ +.contentContainer, .sourceContainer, .classUseContainer, .serializedFormContainer, .constantValuesContainer { + clear:both; + padding:10px 20px; + position:relative; +} +.indexContainer { + margin:10px; + position:relative; + font-size:12px; +} +.indexContainer h2 { + font-size:13px; + padding:0 0 3px 0; +} +.indexContainer ul { + margin:0; + padding:0; +} +.indexContainer ul li { + list-style:none; + padding-top:2px; +} +.contentContainer .description dl dt, .contentContainer .details dl dt, .serializedFormContainer dl dt { + font-size:12px; + font-weight:bold; + margin:10px 0 0 0; + color:#4E4E4E; +} +.contentContainer .description dl dd, .contentContainer .details dl dd, .serializedFormContainer dl dd { + /* margin:5px 0 10px 0px; */ + font-size:14px; + font-family:'DejaVu Serif', Georgia, "Times New Roman", Times, serif; +} +.serializedFormContainer dl.nameValue dt { + margin-left:1px; + font-size:1.1em; + display:inline; + font-weight:bold; +} +.serializedFormContainer dl.nameValue dd { + margin:0 0 0 1px; + font-size:1.1em; + display:inline; +} +/* +List styles +*/ +ul.horizontal li { + display:inline; + font-size:0.9em; +} +ul.inheritance { + margin:0; + padding:0; +} +ul.inheritance li { + display:inline; + list-style:none; +} +ul.inheritance li ul.inheritance { + margin-left:15px; + padding-left:15px; + padding-top:1px; +} +ul.blockList, ul.blockListLast { + margin:10px 0 10px 0; + padding:0; +} +ul.blockList li.blockList, ul.blockListLast li.blockList { + list-style:none; + margin-bottom:15px; + line-height:1.4; +} +ul.blockList ul.blockList li.blockList, ul.blockList ul.blockListLast li.blockList { + padding:0px 20px 5px 10px; + border:1px solid #ededed; + background-color:#f8f8f8; +} +ul.blockList ul.blockList ul.blockList li.blockList, ul.blockList ul.blockList ul.blockListLast li.blockList { + padding:0 0 5px 8px; + background-color:#ffffff; + border:none; +} +ul.blockList ul.blockList ul.blockList ul.blockList li.blockList { + margin-left:0; + padding-left:0; + padding-bottom:15px; + border:none; +} +ul.blockList ul.blockList ul.blockList ul.blockList li.blockListLast { + list-style:none; + border-bottom:none; + padding-bottom:0; +} +table tr td dl, table tr td dl dt, table tr td dl dd { + margin-top:0; + margin-bottom:1px; +} +/* +Table styles +*/ +.overviewSummary, .memberSummary, .typeSummary, .useSummary, .constantsSummary, .deprecatedSummary { + width:100%; + border-left:1px solid #EEE; + border-right:1px solid #EEE; + border-bottom:1px solid #EEE; +} +.overviewSummary, .memberSummary { + padding:0px; +} +.overviewSummary caption, .memberSummary caption, .typeSummary caption, +.useSummary caption, .constantsSummary caption, .deprecatedSummary caption { + position:relative; + text-align:left; + background-repeat:no-repeat; + color:#253441; + font-weight:bold; + clear:none; + overflow:hidden; + padding:0px; + padding-top:10px; + padding-left:1px; + margin:0px; + white-space:pre; +} +.overviewSummary caption a:link, .memberSummary caption a:link, .typeSummary caption a:link, +.useSummary caption a:link, .constantsSummary caption a:link, .deprecatedSummary caption a:link, +.overviewSummary caption a:hover, .memberSummary caption a:hover, .typeSummary caption a:hover, +.useSummary caption a:hover, .constantsSummary caption a:hover, .deprecatedSummary caption a:hover, +.overviewSummary caption a:active, .memberSummary caption a:active, .typeSummary caption a:active, +.useSummary caption a:active, .constantsSummary caption a:active, .deprecatedSummary caption a:active, +.overviewSummary caption a:visited, .memberSummary caption a:visited, .typeSummary caption a:visited, +.useSummary caption a:visited, .constantsSummary caption a:visited, .deprecatedSummary caption a:visited { + color:#FFFFFF; +} +.overviewSummary caption span, .memberSummary caption span, .typeSummary caption span, +.useSummary caption span, .constantsSummary caption span, .deprecatedSummary caption span { + white-space:nowrap; + padding-top:5px; + padding-left:12px; + padding-right:12px; + padding-bottom:7px; + display:inline-block; + float:left; + background-color:#F8981D; + border: none; + height:16px; +} +.memberSummary caption span.activeTableTab span { + white-space:nowrap; + padding-top:5px; + padding-left:12px; + padding-right:12px; + margin-right:3px; + display:inline-block; + float:left; + background-color:#F8981D; + height:16px; +} +.memberSummary caption span.tableTab span { + white-space:nowrap; + padding-top:5px; + padding-left:12px; + padding-right:12px; + margin-right:3px; + display:inline-block; + float:left; + background-color:#4D7A97; + height:16px; +} +.memberSummary caption span.tableTab, .memberSummary caption span.activeTableTab { + padding-top:0px; + padding-left:0px; + padding-right:0px; + background-image:none; + float:none; + display:inline; +} +.overviewSummary .tabEnd, .memberSummary .tabEnd, .typeSummary .tabEnd, +.useSummary .tabEnd, .constantsSummary .tabEnd, .deprecatedSummary .tabEnd { + display:none; + width:5px; + position:relative; + float:left; + background-color:#F8981D; +} +.memberSummary .activeTableTab .tabEnd { + display:none; + width:5px; + margin-right:3px; + position:relative; + float:left; + background-color:#F8981D; +} +.memberSummary .tableTab .tabEnd { + display:none; + width:5px; + margin-right:3px; + position:relative; + background-color:#4D7A97; + float:left; + +} +.overviewSummary td, .memberSummary td, .typeSummary td, +.useSummary td, .constantsSummary td, .deprecatedSummary td { + text-align:left; + padding:0px 0px 12px 10px; +} +th.colOne, th.colFirst, th.colLast, .useSummary th, .constantsSummary th, +td.colOne, td.colFirst, td.colLast, .useSummary td, .constantsSummary td{ + vertical-align:top; + padding-right:0px; + padding-top:8px; + padding-bottom:3px; +} +th.colFirst, th.colLast, th.colOne, .constantsSummary th { + background:#dee3e9; + text-align:left; + padding:8px 3px 3px 7px; +} +td.colFirst, th.colFirst { + white-space:nowrap; + font-size:13px; +} +td.colLast, th.colLast { + font-size:13px; +} +td.colOne, th.colOne { + font-size:13px; +} +.overviewSummary td.colFirst, .overviewSummary th.colFirst, +.useSummary td.colFirst, .useSummary th.colFirst, +.overviewSummary td.colOne, .overviewSummary th.colOne, +.memberSummary td.colFirst, .memberSummary th.colFirst, +.memberSummary td.colOne, .memberSummary th.colOne, +.typeSummary td.colFirst{ + width:25%; + vertical-align:top; +} +td.colOne a:link, td.colOne a:active, td.colOne a:visited, td.colOne a:hover, td.colFirst a:link, td.colFirst a:active, td.colFirst a:visited, td.colFirst a:hover, td.colLast a:link, td.colLast a:active, td.colLast a:visited, td.colLast a:hover, .constantValuesContainer td a:link, .constantValuesContainer td a:active, .constantValuesContainer td a:visited, .constantValuesContainer td a:hover { + font-weight:bold; +} +.tableSubHeadingColor { + background-color:#EEEEFF; +} +.altColor { + background-color:#FFFFFF; +} +.rowColor { + background-color:#EEEEEF; +} +/* +Content styles +*/ +.description pre { + margin-top:0; +} +.deprecatedContent { + margin:0; + padding:10px 0; +} +.docSummary { + padding:0; +} + +ul.blockList ul.blockList ul.blockList li.blockList h3 { + font-style:normal; +} + +div.block { + font-size:14px; + font-family:'DejaVu Serif', Georgia, "Times New Roman", Times, serif; +} + +td.colLast div { + padding-top:0px; +} + +td.colLast a { + padding-bottom:3px; +} +/* +Formatting effect styles +*/ +.sourceLineNo { + color:green; + padding:0 30px 0 0; +} +h1.hidden { + visibility:hidden; + overflow:hidden; + font-size:10px; +} +.block { + display:block; + margin:3px 10px 2px 0px; + color:#474747; +} +.deprecatedLabel, .descfrmTypeLabel, .memberNameLabel, .memberNameLink, +.overrideSpecifyLabel, .packageHierarchyLabel, .paramLabel, .returnLabel, +.seeLabel, .simpleTagLabel, .throwsLabel, .typeNameLabel, .typeNameLink { + font-weight:bold; +} +.deprecationComment, .emphasizedPhrase, .interfaceName { + font-style:italic; +} + +div.block div.block span.deprecationComment, div.block div.block span.emphasizedPhrase, +div.block div.block span.interfaceName { + font-style:normal; +} + +div.contentContainer ul.blockList li.blockList h2{ + padding-bottom:0px; +} diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 7f1e239c84..afba109285 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index e230e2b1c4..c7d437bbb4 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Tue Aug 14 16:28:54 PDT 2012 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.4-bin.zip +networkTimeout=10000 zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=http\://services.gradle.org/distributions/gradle-1.3-bin.zip diff --git a/gradlew b/gradlew index e61422d06d..65dcd68d65 100755 --- a/gradlew +++ b/gradlew @@ -1,79 +1,129 @@ -#!/bin/bash +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ############################################################################## -## -## Gradle start up script for UN*X -## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# ############################################################################## -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS="" +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done -APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD="maximum" +MAX_FD=maximum -warn ( ) { +warn () { echo "$*" -} +} >&2 -die ( ) { +die () { echo echo "$*" echo exit 1 -} +} >&2 # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false -case "`uname`" in - CYGWIN* ) - cygwin=true - ;; - Darwin* ) - darwin=true - ;; - MINGW* ) - msys=true - ;; +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; esac -# For Cygwin, ensure paths are in UNIX format before anything is touched. -if $cygwin ; then - [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` -fi - -# Attempt to set APP_HOME -# Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi -done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" -APP_HOME="`pwd -P`" -cd "$SAVED" - CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACMD=$JAVA_HOME/jre/sh/java else - JAVACMD="$JAVA_HOME/bin/java" + JAVACMD=$JAVA_HOME/bin/java fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME @@ -82,7 +132,7 @@ Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else - JAVACMD="java" + JAVACMD=java which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the @@ -90,75 +140,105 @@ location of your Java installation." fi # Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then - MAX_FD_LIMIT=`ulimit -H -n` - if [ $? -eq 0 ] ; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ] ; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac fi -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -fi +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) -# For Cygwin, switch paths to Windows format before running java -if $cygwin ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) fi - i=$((i+1)) + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg done - case $i in - (0) set -- ;; - (1) set -- "$args0" ;; - (2) set -- "$args0" "$args1" ;; - (3) set -- "$args0" "$args1" "$args2" ;; - (4) set -- "$args0" "$args1" "$args2" "$args3" ;; - (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; - esac fi -# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules -function splitJvmOpts() { - JVM_OPTS=("$@") -} -eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS -JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi -exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat index aec99730b4..93e3f59f13 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -1,90 +1,92 @@ -@if "%DEBUG%" == "" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS= - -set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto init - -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto init - -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:init -@rem Get command-line arguments, handling Windowz variants - -if not "%OS%" == "Windows_NT" goto win9xME_args -if "%@eval[2+2]" == "4" goto 4NT_args - -:win9xME_args -@rem Slurp the command line arguments. -set CMD_LINE_ARGS= -set _SKIP=2 - -:win9xME_args_slurp -if "x%~1" == "x" goto execute - -set CMD_LINE_ARGS=%* -goto execute - -:4NT_args -@rem Get arguments from the 4NT Shell from JP Software -set CMD_LINE_ARGS=%$ - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% - -:end -@rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/language-adaptors/README.md b/language-adaptors/README.md deleted file mode 100644 index 78b7265df5..0000000000 --- a/language-adaptors/README.md +++ /dev/null @@ -1,9 +0,0 @@ -# Language Adaptors - -RxJava supports JVM languages via implementations of <a href="https://github.com/Netflix/RxJava/blob/master/rxjava-core/src/main/java/rx/util/functions/FunctionLanguageAdaptor.java">FunctionLanguageAdaptor</a> - -If there is a language you'd like supported please look at the existing adaptors (such as <a href="https://github.com/Netflix/RxJava/blob/master/language-adaptors/rxjava-groovy/src/main/java/rx/lang/groovy/GroovyAdaptor.java">Groovy</a>) and implement the adaptor for your language of choice. - -If you feel it would be valuable for the community submit a pull request and we'll accept it into the main project. - -Please comply with the conventions established by the existing language adaptors if you intend to submit a pull request. diff --git a/language-adaptors/rxjava-clojure/README.md b/language-adaptors/rxjava-clojure/README.md deleted file mode 100644 index 35faeab8ba..0000000000 --- a/language-adaptors/rxjava-clojure/README.md +++ /dev/null @@ -1,44 +0,0 @@ -# Clojure Adaptor for RxJava - - -This adaptor allows 'fn' functions to be used and RxJava will know how to invoke them. - -This enables code such as: - -```clojure -(-> - (Observable/toObservable ["one" "two" "three"]) - (.take 2) - (.subscribe (fn [arg] (println arg)))) -``` - -This still dependes on Clojure using Java interop against the Java API. - -A future goal is a Clojure wrapper to expose the functions in a more idiomatic way. - - -# Binaries - -Binaries and dependency information for Maven, Ivy, Gradle and others can be found at [http://search.maven.org](http://search.maven.org/#search%7Cga%7C1%7Ca%3A%22rxjava-clojure%22). - -Example for Maven: - -```xml -<dependency> - <groupId>com.netflix.rxjava</groupId> - <artifactId>rxjava-clojure</artifactId> - <version>x.y.z</version> -</dependency> -``` - -and for Ivy: - -```xml -<dependency org="com.netflix.rxjava" name="rxjava-clojure" rev="x.y.z" /> -``` - -and for Leiningen: - -```clojure -[com.netflix.rxjava/rxjava-clojure "x.y.z"] -``` diff --git a/language-adaptors/rxjava-clojure/build.gradle b/language-adaptors/rxjava-clojure/build.gradle deleted file mode 100644 index 0ff569c87d..0000000000 --- a/language-adaptors/rxjava-clojure/build.gradle +++ /dev/null @@ -1,72 +0,0 @@ -apply plugin: 'java' -apply plugin: 'clojure' -apply plugin: 'eclipse' -apply plugin: 'idea' - -dependencies { - compile project(':rxjava-core') - provided 'org.clojure:clojure:1.4.+' - provided 'junit:junit:4.10' - provided 'org.mockito:mockito-core:1.8.5' - - // clojure - testCompile 'clj-http:clj-http:0.6.4' // https://clojars.org/clj-http -} - -/* - * Clojure - */ -aotCompile = true -warnOnReflection = true - -buildscript { - repositories { maven { url "http://clojars.org/repo" } } - dependencies { classpath "clojuresque:clojuresque:1.5.4" } -} - -repositories { - mavenCentral() - clojarsRepo() -} - -/* - * Add Counterclockwise and include 'provided' dependencies - */ -eclipse { - project { - natures "ccw.nature" - } - classpath { - plusConfigurations += configurations.provided - downloadSources = true - downloadJavadoc = true - } -} - - -// setup Eclipse -eclipse { - classpath { - //you can tweak the classpath of the Eclipse project by adding extra configurations: - plusConfigurations += configurations.provided - - downloadSources = true - downloadJavadoc = true - } -} - -// include /src/examples folder -sourceSets { - examples -} - -// make 'examples' use the same classpath -configurations { - examplesCompile.extendsFrom compile - examplesRuntime.extendsFrom runtime -} - -// include 'examples' in build task -build.dependsOn examplesClasses - - diff --git a/language-adaptors/rxjava-clojure/src/examples/clojure/rx/lang/clojure/examples/rx_examples.clj b/language-adaptors/rxjava-clojure/src/examples/clojure/rx/lang/clojure/examples/rx_examples.clj deleted file mode 100644 index 4803825a7c..0000000000 --- a/language-adaptors/rxjava-clojure/src/examples/clojure/rx/lang/clojure/examples/rx_examples.clj +++ /dev/null @@ -1,267 +0,0 @@ -(ns rx.lang.clojure.examples.rx-examples - (import rx.Observable) - (:require [clj-http.client :as http])) - -; NOTE on naming conventions. I'm using camelCase names (against clojure convention) -; in this file as I'm purposefully keeping functions and methods across -; different language implementations in-sync for easy comparison. - -; -------------------------------------------------- -; Hello World! -; -------------------------------------------------- - -(defn hello - [& args] - (-> (Observable/toObservable args) - (.subscribe #(println (str "Hello " % "!"))))) - -; To see output -(comment - (hello "Ben" "George")) - -; -------------------------------------------------- -; Create Observable from Existing Data -; -------------------------------------------------- - -(defn existingDataFromNumbers [] - (Observable/toObservable [1 2 3 4 5 6])) - -(defn existingDataFromNumbersUsingFrom [] - (Observable/from [1 2 3 4 5 6])) - -(defn existingDataFromObjects [] - (Observable/toObservable ["a" "b" "c"])) - -(defn existingDataFromObjectsUsingFrom [] - (Observable/from ["a" "b" "c"])) - -(defn existingDataFromList [] - (let [list [5, 6, 7, 8]] - (Observable/toObservable list))) - -(defn existingDataFromListUsingFrom [] - (let [list [5, 6, 7, 8]] - (Observable/from list))) - -(defn existingDataWithJust [] - (Observable/just "one object")) - -; -------------------------------------------------- -; Custom Observable -; -------------------------------------------------- - -(defn customObservableBlocking [] - "This example shows a custom Observable that blocks - when subscribed to (does not spawn an extra thread). - - returns Observable<String>" - (Observable/create - (fn [observer] - (doseq [x (range 50)] (-> observer (.onNext (str "value_" x)))) - ; after sending all values we complete the sequence - (-> observer .onCompleted) - ; return a NoOpSubsription since this blocks and thus - ; can't be unsubscribed from - (Observable/noOpSubscription)))) - -; To see output -(comment - (.subscribe (customObservableBlocking) println)) - -(defn customObservableNonBlocking [] - "This example shows a custom Observable that does not block - when subscribed to as it spawns a separate thread. - - returns Observable<String>" - (Observable/create - (fn [observer] - (let [f (future - (doseq [x (range 50)] - (-> observer (.onNext (str "anotherValue_" x)))) - ; after sending all values we complete the sequence - (-> observer .onCompleted))] - ; return a subscription that cancels the future - (Observable/createSubscription #(future-cancel f)))))) - -; To see output -(comment - (.subscribe (customObservableNonBlocking) println)) - - -(defn fetchWikipediaArticleAsynchronously [wikipediaArticleNames] - "Fetch a list of Wikipedia articles asynchronously. - - return Observable<String> of HTML" - (Observable/create - (fn [observer] - (let [f (future - (doseq [articleName wikipediaArticleNames] - (-> observer (.onNext (http/get (str "http://en.wikipedia.org/wiki/" articleName))))) - ; after sending response to onnext we complete the sequence - (-> observer .onCompleted))] - ; a subscription that cancels the future if unsubscribed - (Observable/createSubscription #(future-cancel f)))))) - -; To see output -(comment - (-> (fetchWikipediaArticleAsynchronously ["Tiger" "Elephant"]) - (.subscribe #(println "--- Article ---\n" (subs (:body %) 0 125) "...")))) - - -; -------------------------------------------------- -; Composition - Simple -; -------------------------------------------------- - -(defn simpleComposition [] - "Asynchronously calls 'customObservableNonBlocking' and defines - a chain of operators to apply to the callback sequence." - (-> - (customObservableNonBlocking) - (.skip 10) - (.take 5) - (.map #(str % "_transformed")) - (.subscribe #(println "onNext =>" %)))) - -; To see output -(comment - (simpleComposition)) - - -; -------------------------------------------------- -; Composition - Multiple async calls combined -; -------------------------------------------------- - -(defn getUser [userId] - "Asynchronously fetch user data - - return Observable<Map>" - (Observable/create - (fn [observer] - (let [f (future - (try - ; simulate fetching user data via network service call with latency - (Thread/sleep 60) - (-> observer (.onNext {:user-id userId - :name "Sam Harris" - :preferred-language (if (= 0 (rand-int 2)) "en-us" "es-us") })) - (-> observer .onCompleted) - (catch Exception e (-> observer (.onError e))))) ] - ; a subscription that cancels the future if unsubscribed - (Observable/createSubscription #(future-cancel f)))))) - -(defn getVideoBookmark [userId, videoId] - "Asynchronously fetch bookmark for video - - return Observable<Integer>" - (Observable/create - (fn [observer] - (let [f (future - (try - ; simulate fetching user data via network service call with latency - (Thread/sleep 20) - (-> observer (.onNext {:video-id videoId - ; 50/50 chance of giving back position 0 or 0-2500 - :position (if (= 0 (rand-int 2)) 0 (rand-int 2500))})) - (-> observer .onCompleted) - (catch Exception e (-> observer (.onError e)))))] - ; a subscription that cancels the future if unsubscribed - (Observable/createSubscription #(future-cancel f)))))) - -(defn getVideoMetadata [videoId, preferredLanguage] - "Asynchronously fetch movie metadata for a given language - return Observable<Map>" - (Observable/create - (fn [observer] - (let [f (future - (try - ; simulate fetching video data via network service call with latency - (Thread/sleep 50) - ; contrived metadata for en-us or es-us - (if (= "en-us" preferredLanguage) - (-> observer (.onNext {:video-id videoId - :title "House of Cards: Episode 1" - :director "David Fincher" - :duration 3365}))) - (if (= "es-us" preferredLanguage) - (-> observer (.onNext {:video-id videoId - :title "Cámara de Tarjetas: Episodio 1" - :director "David Fincher" - :duration 3365}))) - (-> observer .onCompleted) - (catch Exception e (-> observer (.onError e))))) ] - ; a subscription that cancels the future if unsubscribed - (Observable/createSubscription #(future-cancel f)))))) - - -(defn getVideoForUser [userId videoId] - "Get video metadata for a given userId - - video metadata - - video bookmark position - - user data - return Observable<Map>" - (let [user-observable (-> (getUser userId) - (.map (fn [user] {:user-name (:name user) - :language (:preferred-language user)}))) - bookmark-observable (-> (getVideoBookmark userId videoId) - (.map (fn [bookmark] {:viewed-position (:position bookmark)}))) - ; getVideoMetadata requires :language from user-observable so nest inside map function - video-metadata-observable (-> user-observable - (.mapMany - ; fetch metadata after a response from user-observable is received - (fn [user-map] - (getVideoMetadata videoId (:language user-map)))))] - ; now combine 3 async sequences using zip - (-> (Observable/zip bookmark-observable video-metadata-observable user-observable - (fn [bookmark-map metadata-map user-map] - {:bookmark-map bookmark-map - :metadata-map metadata-map - :user-map user-map})) - ; and transform into a single response object - (.map (fn [data] - {:video-id videoId - :video-metadata (:metadata-map data) - :user-id userId - :language (:language (:user-map data)) - :bookmark (:viewed-position (:bookmark-map data)) }))))) - -; To see output like this: -; {:video-id 78965, :video-metadata {:video-id 78965, :title Cámara de Tarjetas: Episodio 1, -; :director David Fincher, :duration 3365}, :user-id 12345, :language es-us, :bookmark 0} -; -(comment - (-> (getVideoForUser 12345 78965) - (.subscribe - (fn [x] (println "--- Object ---\n" x)) - (fn [e] (println "--- Error ---\n" e)) - (fn [] (println "--- Completed ---"))))) - - -; -------------------------------------------------- -; Error Handling -; -------------------------------------------------- - -(defn fetchWikipediaArticleAsynchronouslyWithErrorHandling [wikipediaArticleNames] - "Fetch a list of Wikipedia articles asynchronously - with proper error handling. - - return Observable<String> of HTML" - (Observable/create - (fn [observer] - (let [f (future - (try - (doseq [articleName wikipediaArticleNames] - (-> observer (.onNext (http/get (str "http://en.wikipedia.org/wiki/" articleName))))) - ;(catch Exception e (prn "exception"))) - (catch Exception e (-> observer (.onError e)))) - ; after sending response to onNext we complete the sequence - (-> observer .onCompleted))] - ; a subscription that cancels the future if unsubscribed - (Observable/createSubscription #(future-cancel f)))))) - -; To see output -(comment - (-> (fetchWikipediaArticleAsynchronouslyWithErrorHandling ["Tiger" "NonExistentTitle" "Elephant"]) - (.subscribe #(println "--- Article ---\n" (subs (:body %) 0 125) "...") - #(println "--- Error ---\n" (.getMessage %))))) - - diff --git a/language-adaptors/rxjava-clojure/src/main/java/rx/lang/clojure/ClojureAdaptor.java b/language-adaptors/rxjava-clojure/src/main/java/rx/lang/clojure/ClojureAdaptor.java deleted file mode 100644 index 4420f7c919..0000000000 --- a/language-adaptors/rxjava-clojure/src/main/java/rx/lang/clojure/ClojureAdaptor.java +++ /dev/null @@ -1,128 +0,0 @@ -/** - * Copyright 2013 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package rx.lang.clojure; - -import java.util.Arrays; - -import org.junit.Before; -import org.junit.Test; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; - -import rx.Observer; -import rx.util.functions.FunctionLanguageAdaptor; - -import clojure.lang.IFn; -import clojure.lang.RT; -import clojure.lang.Var; - -public class ClojureAdaptor implements FunctionLanguageAdaptor { - - @Override - public Object call(Object function, Object[] args) { - if (args.length == 0) { - return ((IFn) function).invoke(); - } else if (args.length == 1) { - return ((IFn) function).invoke(args[0]); - } else if (args.length == 2) { - return ((IFn) function).invoke(args[0], args[1]); - } else if (args.length == 3) { - return ((IFn) function).invoke(args[0], args[1], args[2]); - } else if (args.length == 4) { - return ((IFn) function).invoke(args[0], args[1], args[2], args[3]); - } else if (args.length == 5) { - return ((IFn) function).invoke(args[0], args[1], args[2], args[3], args[4]); - } else if (args.length == 6) { - return ((IFn) function).invoke(args[0], args[1], args[2], args[3], args[4], args[5]); - } else if (args.length == 7) { - return ((IFn) function).invoke(args[0], args[1], args[2], args[3], args[4], args[5], args[6]); - } else if (args.length == 8) { - return ((IFn) function).invoke(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7]); - } else if (args.length == 9) { - return ((IFn) function).invoke(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7], args[8]); - } else if (args.length == 10) { - return ((IFn) function).invoke(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7], args[8], args[9]); - } else if (args.length == 11) { - return ((IFn) function).invoke(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7], args[8], args[9], args[10]); - } else if (args.length == 12) { - return ((IFn) function).invoke(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7], args[8], args[9], args[10], args[11]); - } else if (args.length == 13) { - return ((IFn) function).invoke(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7], args[8], args[9], args[10], args[11], args[12]); - } else if (args.length == 14) { - return ((IFn) function).invoke(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7], args[8], args[9], args[10], args[11], args[12], args[13]); - } else if (args.length == 15) { - return ((IFn) function).invoke(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7], args[8], args[9], args[10], args[11], args[12], args[13], args[14]); - } else if (args.length == 16) { - return ((IFn) function).invoke(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7], args[8], args[9], args[10], args[11], args[12], args[13], args[14], args[15]); - } else if (args.length == 17) { - return ((IFn) function).invoke(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7], args[8], args[9], args[10], args[11], args[12], args[13], args[14], args[15], args[16]); - } else if (args.length == 18) { - return ((IFn) function).invoke(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7], args[8], args[9], args[10], args[11], args[12], args[13], args[14], args[15], args[16], args[17]); - } else if (args.length == 19) { - return ((IFn) function).invoke(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7], args[8], args[9], args[10], args[11], args[12], args[13], args[14], args[15], args[16], args[17], args[18]); - } else if (args.length == 20) { - return ((IFn) function).invoke(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7], args[8], args[9], args[10], args[11], args[12], args[13], args[14], args[15], args[16], args[17], args[18], args[19]); - } else { - return ((IFn) function).invoke(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7], args[8], args[9], args[10], args[11], args[12], args[13], args[14], args[15], args[16], args[17], args[18], args[19], Arrays.copyOfRange(args, 20, args.length)); - } - } - - @Override - public Class<?>[] getFunctionClass() { - return new Class<?>[] { IFn.class }; - } - - public static class UnitTest { - - @Mock - ScriptAssertion assertion; - - @Mock - Observer<Integer> w; - - @Before - public void before() { - MockitoAnnotations.initMocks(this); - } - - @Test - public void testTake() { - runClojureScript("(-> (rx.Observable/toObservable [\"one\" \"two\" \"three\"]) (.take 2) (.subscribe (fn [arg] (println arg))))"); - } - - // commented out for now as I can't figure out how to set the var 'a' with the 'assertion' instance when running the code from java - // @Test - // public void testFilter() { - // runClojureScript("(-> (org.rx.reactive.Observable/toObservable [1 2 3]) (.filter (fn [v] (>= v 2))) (.subscribe (fn [result] (a.received(result)))))"); - // verify(assertion, times(0)).received(1); - // verify(assertion, times(1)).received(2); - // verify(assertion, times(1)).received(3); - // } - - private static interface ScriptAssertion { - public void error(Exception o); - - public void received(Object o); - } - - private void runClojureScript(String script) { - Object code = RT.var("clojure.core", "read-string").invoke(script); - Var eval = RT.var("clojure.core", "eval"); - Object result = eval.invoke(code); - System.out.println("Result: " + result); - } - } -} diff --git a/language-adaptors/rxjava-clojure/src/test/clojure/rx/lang/clojure/observable_tests.clj b/language-adaptors/rxjava-clojure/src/test/clojure/rx/lang/clojure/observable_tests.clj deleted file mode 100644 index 3daa472789..0000000000 --- a/language-adaptors/rxjava-clojure/src/test/clojure/rx/lang/clojure/observable_tests.clj +++ /dev/null @@ -1,5 +0,0 @@ -(ns rx.lang.clojure.observable-tests - (import rx.Observable)) - -;; still need to get this wired up in build.gradle to run as tests -; (-> (rx.Observable/toObservable [\"one\" \"two\" \"three\"]) (.take 2) (.subscribe (fn [arg] (println arg)))) \ No newline at end of file diff --git a/language-adaptors/rxjava-groovy/README.md b/language-adaptors/rxjava-groovy/README.md deleted file mode 100644 index d1d9698699..0000000000 --- a/language-adaptors/rxjava-groovy/README.md +++ /dev/null @@ -1,38 +0,0 @@ -# Groovy Adaptor for RxJava - - -This adaptor allows 'groovy.lang.Closure' functions to be used and RxJava will know how to invoke them. - -This enables code such as: - -```groovy - Observable.toObservable("one", "two", "three") - .take(2) - .subscribe({arg -> println(arg)}) -``` - -# Binaries - -Binaries and dependency information for Maven, Ivy, Gradle and others can be found at [http://search.maven.org](http://search.maven.org/#search%7Cga%7C1%7Ca%3A%22rxjava-groovy%22). - -Example for Maven: - -```xml -<dependency> - <groupId>com.netflix.rxjava</groupId> - <artifactId>rxjava-groovy</artifactId> - <version>x.y.z</version> -</dependency> -``` - -and for Ivy: - -```xml -<dependency org="com.netflix.rxjava" name="rxjava-groovy" rev="x.y.z" /> -``` - -and for Gradle: - -```groovy -compile 'com.netflix.rxjava:rxjava-groovy:x.y.z' -``` diff --git a/language-adaptors/rxjava-groovy/build.gradle b/language-adaptors/rxjava-groovy/build.gradle deleted file mode 100644 index b0e2108611..0000000000 --- a/language-adaptors/rxjava-groovy/build.gradle +++ /dev/null @@ -1,36 +0,0 @@ -apply plugin: 'java' -apply plugin: 'groovy' -apply plugin: 'eclipse' -apply plugin: 'idea' - -dependencies { - compile project(':rxjava-core') - groovy 'org.codehaus.groovy:groovy-all:2.+' - provided 'junit:junit:4.10' - provided 'org.mockito:mockito-core:1.8.5' -} - -// include /src/examples folder -sourceSets { - examples -} - -// make 'examples' use the same classpath -configurations { - examplesCompile.extendsFrom compile - examplesRuntime.extendsFrom runtime -} - -// include 'examples' in build task -build.dependsOn examplesClasses - -// setup Eclipse -eclipse { - classpath { - //you can tweak the classpath of the Eclipse project by adding extra configurations: - plusConfigurations += configurations.provided - - downloadSources = true - downloadJavadoc = true - } -} \ No newline at end of file diff --git a/language-adaptors/rxjava-groovy/src/examples/groovy/rx/lang/groovy/examples/RxExamples.groovy b/language-adaptors/rxjava-groovy/src/examples/groovy/rx/lang/groovy/examples/RxExamples.groovy deleted file mode 100644 index 2571565765..0000000000 --- a/language-adaptors/rxjava-groovy/src/examples/groovy/rx/lang/groovy/examples/RxExamples.groovy +++ /dev/null @@ -1,229 +0,0 @@ -/** - * Copyright 2013 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package rx.lang.groovy.examples; - -import rx.Observable; -import rx.Observer; -import rx.Subscription; -import rx.util.functions.Func1; - -// -------------------------------------------------- -// Hello World! -// -------------------------------------------------- - -def hello(String[] names) { - Observable.toObservable(names) - .subscribe({ println "Hello " + it + "!"}) -} - -hello("Ben", "George") - - -// -------------------------------------------------- -// Create Observables from Existing Data -// -------------------------------------------------- - -def existingDataFromNumbers() { - Observable<Integer> o = Observable.toObservable(1, 2, 3, 4, 5, 6); -} - -def existingDataFromNumbersUsingFrom() { - Observable<Integer> o2 = Observable.from(1, 2, 3, 4, 5, 6); -} - -def existingDataFromObjects() { - Observable<String> o = Observable.toObservable("a", "b", "c"); -} - -def existingDataFromObjectsUsingFrom() { - Observable<String> o = Observable.from("a", "b", "c"); -} - -def existingDataFromList() { - def list = [5, 6, 7, 8] - Observable<Integer> o = Observable.toObservable(list); -} - -def existingDataFromListUsingFrom() { - def list = [5, 6, 7, 8] - Observable<Integer> o2 = Observable.from(list); -} - -def existingDataWithJust() { - Observable<String> o = Observable.just("one object"); -} - - -// -------------------------------------------------- -// Create Custom Observables -// -------------------------------------------------- - - -/** - * This example shows a custom Observable that blocks - * when subscribed to (does not spawn an extra thread). - * - * @return Observable<String> - */ -def customObservableBlocking() { - return Observable.create(new Func1<Observer<String>, Subscription>() { - def Subscription call(Observer<String> observer) { - for(int i=0; i<50; i++) { - observer.onNext("value_" + i); - } - // after sending all values we complete the sequence - observer.onCompleted(); - // return a NoOpSubsription since this blocks and thus - // can't be unsubscribed from - return Observable.noOpSubscription(); - }; - }); -} - -// To see output: -customObservableBlocking().subscribe({ println(it)}); - -/** - * This example shows a custom Observable that does not block - * when subscribed to as it spawns a separate thread. - * - * @return Observable<String> - */ -def customObservableNonBlocking() { - return Observable.create(new Func1<Observer<String>, Subscription>() { - /** - * This 'call' method will be invoked with the Observable is subscribed to. - * - * It spawns a thread to do it asynchronously. - */ - def Subscription call(Observer<String> observer) { - // For simplicity this example uses a Thread instead of an ExecutorService/ThreadPool - final Thread t = new Thread(new Runnable() { - void run() { - for(int i=0; i<75; i++) { - observer.onNext("anotherValue_" + i); - } - // after sending all values we complete the sequence - observer.onCompleted(); - }; - }); - t.start(); - - return new Subscription() { - public void unsubscribe() { - // Ask the thread to stop doing work. - // For this simple example it just interrupts. - t.interrupt(); - } - }; - }; - }); -} - -// To see output: -customObservableNonBlocking().subscribe({ println(it)}); - - -/** - * Fetch a list of Wikipedia articles asynchronously. - * - * @param wikipediaArticleName - * @return Observable<String> of HTML - */ -def fetchWikipediaArticleAsynchronously(String... wikipediaArticleNames) { - return Observable.create({ Observer<String> observer -> - Thread.start { - for(articleName in wikipediaArticleNames) { - observer.onNext(new URL("http://en.wikipedia.org/wiki/"+articleName).getText()); - } - observer.onCompleted(); - } - return Observable.noOpSubscription(); - }); -} - -// To see output: -fetchWikipediaArticleAsynchronously("Tiger", "Elephant") - .subscribe({ println "--- Article ---\n" + it.substring(0, 125)}) - - -// -------------------------------------------------- -// Composition -// -------------------------------------------------- - -/** - * Asynchronously calls 'customObservableNonBlocking' and defines - * a chain of operators to apply to the callback sequence. - */ -def simpleComposition() { - customObservableNonBlocking() - .skip(10) - .take(5) - .map({ stringValue -> return stringValue + "_transformed"}) - .subscribe({ println "onNext => " + it}) -} - -// To see output: -simpleComposition(); - -/* - -(defn simpleComposition [] - "Asynchronously calls 'customObservableNonBlocking' and defines - a chain of operators to apply to the callback sequence." - (-> - (customObservableNonBlocking) - (.skip 10) - (.take 5) - (.map #(do (str % "_transformed"))) - (.subscribe #(println "onNext =>" %)))) - */ - - - -// -------------------------------------------------- -// Error Handling -// -------------------------------------------------- - - - -/** - * Fetch a list of Wikipedia articles asynchronously with error handling. - * - * @param wikipediaArticleName - * @return Observable<String> of HTML - */ -def fetchWikipediaArticleAsynchronouslyWithErrorHandling(String... wikipediaArticleNames) { - return Observable.create({ Observer<String> observer -> - Thread.start { - try { - for(articleName in wikipediaArticleNames) { - observer.onNext(new URL("http://en.wikipedia.org/wiki/"+articleName).getText()); - } - observer.onCompleted(); - } catch(Exception e) { - observer.onError(e); - } - } - return Observable.noOpSubscription(); - }); -} - -fetchWikipediaArticleAsynchronouslyWithErrorHandling("Tiger", "NonExistentTitle", "Elephant") - .subscribe( - { println "--- Article ---\n" + it.substring(0, 125)}, - { println "--- Error ---\n" + it.getMessage()}) - diff --git a/language-adaptors/rxjava-groovy/src/examples/groovy/rx/lang/groovy/examples/VideoExample.groovy b/language-adaptors/rxjava-groovy/src/examples/groovy/rx/lang/groovy/examples/VideoExample.groovy deleted file mode 100644 index b8a3f5182e..0000000000 --- a/language-adaptors/rxjava-groovy/src/examples/groovy/rx/lang/groovy/examples/VideoExample.groovy +++ /dev/null @@ -1,246 +0,0 @@ -/** - * Copyright 2013 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package rx.lang.groovy.examples; - -import rx.Observable; -import rx.Observer; -import rx.Subscription; -import rx.util.functions.Func1; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.LinkedBlockingQueue; -import java.util.concurrent.ThreadPoolExecutor; -import java.util.concurrent.TimeUnit; - -class VideoExample { - -static void main(String[] args) { - VideoExample v = new VideoExample(); - println("---- sequence of video dictionaries ----") - v.getVideoGridForDisplay(1).subscribe( - { videoDictionary -> // onNext - // this will print the dictionary for each video - // and is a good representation of how progressive rendering could work - println(videoDictionary) }, - { exception -> // onError - println("Error: " + exception) }, - { // onCompleted - v.executor.shutdownNow(); - }); - - VideoExample v2 = new VideoExample(); - v2.getVideoGridForDisplay(1).toList().subscribe( - { videoDictionaryList -> // onNext - // this will be called once with a list - // and demonstrates how a sequence can be combined - // for document style responses (most webservices) - println("\n ---- single list of video dictionaries ----\n" + videoDictionaryList) }, - { exception -> // onError - println("Error: " + exception) }, - { // onCompleted - v2.executor.shutdownNow(); - }); -} -/** - * Demonstrate how Rx is used to compose Observables together such as - * how a web service would to generate a JSON response. - * - * The simulated methods for the metadata represent different services - * that are often backed by network calls. - * - * This will return a sequence of dictionaries such as this: - * - * [id:1000, title:video-1000-title, length:5428, bookmark:0, - * rating:[actual:4, average:3, predicted:0]] - */ -def Observable getVideoGridForDisplay(userId) { - getListOfLists(userId).mapMany({ VideoList list -> - // for each VideoList we want to fetch the videos - list.getVideos() - .take(10) // we only want the first 10 of each list - .mapMany({ Video video -> - // for each video we want to fetch metadata - def m = video.getMetadata().map({ Map<String, String> md -> - // transform to the data and format we want - return [title: md.get("title"), - length: md.get("duration")] - }) - def b = video.getBookmark(userId).map({ position -> - return [bookmark: position] - }) - def r = video.getRating(userId).map({ VideoRating rating -> - return [rating: - [actual: rating.getActualStarRating(), - average: rating.getAverageStarRating(), - predicted: rating.getPredictedStarRating()]] - }) - // compose these together - return Observable.zip(m, b, r, { - metadata, bookmark, rating -> - // now transform to complete dictionary of data - // we want for each Video - return [id: video.videoId] << metadata << bookmark << rating - }) - }) - }) -} - -/** - * Retrieve a list of lists of videos (grid). - * - * Observable<VideoList> is the "push" equivalent to List<VideoList> - */ -def Observable<VideoList> getListOfLists(userId) { - return Observable.create({ observer -> - // this will happen on a separate thread as it requires a network call - executor.execute(new Runnable() { - def void run() { - // simulate network latency - Thread.sleep(180); - for(i in 0..15) { - observer.onNext(new VideoList(i)) - } - observer.onCompleted(); - } - }) - }) -} - -/** - * Represents a list of videos as part of a grid (list of lists). - */ -class VideoList { - - int listPosition; - VideoList(int position) { - this.listPosition = position - } - - def String getListName() { - return "ListName-" + listPosition - } - - def Integer getListPosition() { - return listPosition - } - - def Observable<Video> getVideos() { - return Observable.create({ observer -> - // we already have the videos once a list is loaded - // so we won't launch another thread but return - // the sequence of videos via push - for(i in 0..50) { - observer.onNext(new Video((listPosition*1000)+i)) - } - observer.onCompleted(); - }) - } -} - -class Video { - int videoId; - Video(int videoId) { - this.videoId = videoId; - } - - // synchronous - def Observable<Map<String, String>> getMetadata() { - // simulate fetching metadata from an in-memory cache - // so it will not asynchronously execute on a thread but - // immediately return an Observable with the data - return Observable.create({ observer -> - observer.onNext([ - title: "video-" + videoId + "-title", - actors: ["actor1", "actor2"], - duration: 5428]) - observer.onCompleted(); - }); - } - - // asynchronous - def Observable<Integer> getBookmark(userId) { - // simulate fetching the bookmark for this user - // that specifies the last played position if - // this video has been played before - return Observable.create({ observer -> - // this will happen on a separate thread as it requires a network call - executor.execute(new Runnable() { - def void run() { - // simulate network latency - Thread.sleep(4); - if(randint(6) > 1) { - // most of the time they haven't watched a movie - // so the position is 0 - observer.onNext(randint(0)); - } else { - observer.onNext(randint(4000)); - } - observer.onCompleted(); - } - }) - }) - } - - // asynchronous - def Observable<VideoRating> getRating(userId) { - // simulate fetching the VideoRating for this user - return Observable.create({ observer -> - // this will happen on a separate thread as it requires a network call - executor.execute(new Runnable() { - def void run() { - // simulate network latency - Thread.sleep(10); - observer.onNext(new VideoRating(videoId, userId)) - observer.onCompleted(); - } - }) - }) - } -} - -class VideoRating { - int videoId, userId - VideoRating(videoId, userId) { - this.videoId = videoId; - this.userId = userId; - } - - def Integer getPredictedStarRating() { - return randint(5) - } - - def Integer getAverageStarRating() { - return randint(4) - } - - def Integer getActualStarRating() { - return randint(5) - } -} - -ExecutorService executor = new ThreadPoolExecutor(4, 4, 1, TimeUnit.MINUTES, new LinkedBlockingQueue<Runnable>()); - -def randint(int max) { - return Math.round(Math.random() * max) -} - -def combine( Map... m ) { - m.collectMany { it.entrySet() }.inject( [:] ) { result, e -> - result << [ (e.key):e.value + ( result[ e.key ] ?: 0 ) ] - } - } - -} - diff --git a/language-adaptors/rxjava-groovy/src/main/java/rx/lang/groovy/GroovyAdaptor.java b/language-adaptors/rxjava-groovy/src/main/java/rx/lang/groovy/GroovyAdaptor.java deleted file mode 100644 index 70cef9c18e..0000000000 --- a/language-adaptors/rxjava-groovy/src/main/java/rx/lang/groovy/GroovyAdaptor.java +++ /dev/null @@ -1,31 +0,0 @@ -/** - * Copyright 2013 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package rx.lang.groovy; - -import groovy.lang.Closure; -import rx.util.functions.FunctionLanguageAdaptor; - -public class GroovyAdaptor implements FunctionLanguageAdaptor { - - @Override - public Object call(Object function, Object[] args) { - return ((Closure<?>) function).call(args); - } - - public Class<?>[] getFunctionClass() { - return new Class<?>[] { Closure.class }; - } -} diff --git a/language-adaptors/rxjava-groovy/src/test/groovy/rx/lang/groovy/ObservableTests.groovy b/language-adaptors/rxjava-groovy/src/test/groovy/rx/lang/groovy/ObservableTests.groovy deleted file mode 100644 index 8f50ec7c9e..0000000000 --- a/language-adaptors/rxjava-groovy/src/test/groovy/rx/lang/groovy/ObservableTests.groovy +++ /dev/null @@ -1,239 +0,0 @@ -/** - * Copyright 2013 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package rx.lang.groovy - -import static org.mockito.Matchers.*; -import static org.mockito.Mockito.*; - -import java.util.Arrays; - -import org.junit.Before; -import org.junit.Test; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; - -import rx.Notification; -import rx.Observable; -import rx.Observer; -import rx.Subscription; -import rx.util.functions.Func1; - -def class ObservableTests { - - @Mock - ScriptAssertion a; - - @Mock - Observer<Integer> w; - - @Before - public void before() { - MockitoAnnotations.initMocks(this); - } - - @Test - public void testCreate() { - Observable.create({it.onNext('hello');it.onCompleted();}).subscribe({ result -> a.received(result)}); - verify(a, times(1)).received("hello"); - } - - @Test - public void testFilter() { - Observable.filter(Observable.toObservable(1, 2, 3), {it >= 2}).subscribe({ result -> a.received(result)}); - verify(a, times(0)).received(1); - verify(a, times(1)).received(2); - verify(a, times(1)).received(3); - } - - @Test - public void testLast() { - new TestFactory().getObservable().last().subscribe({ result -> a.received(result)}); - verify(a, times(1)).received("hello_1"); - } - - @Test - public void testMap1() { - new TestFactory().getObservable().map({v -> 'say' + v}).subscribe({ result -> a.received(result)}); - verify(a, times(1)).received("sayhello_1"); - } - - @Test - public void testMap2() { - Observable.map(Observable.toObservable(1, 2, 3), {'hello_' + it}).subscribe({ result -> a.received(result)}); - verify(a, times(1)).received("hello_" + 1); - verify(a, times(1)).received("hello_" + 2); - verify(a, times(1)).received("hello_" + 3); - } - - @Test - public void testMaterialize() { - Observable.materialize(Observable.toObservable(1, 2, 3)).subscribe({ result -> a.received(result)}); - // we expect 4 onNext calls: 3 for 1, 2, 3 ObservableNotification.OnNext and 1 for ObservableNotification.OnCompleted - verify(a, times(4)).received(any(Notification.class)); - verify(a, times(0)).error(any(Exception.class)); - } - - @Test - public void testMergeDelayError() { - Observable.mergeDelayError( - Observable.toObservable(1, 2, 3), - Observable.merge( - Observable.toObservable(6), - Observable.error(new NullPointerException()), - Observable.toObservable(7)), - Observable.toObservable(4, 5)) - .subscribe( { result -> a.received(result)}, { exception -> a.error(exception)}); - - verify(a, times(1)).received(1); - verify(a, times(1)).received(2); - verify(a, times(1)).received(3); - verify(a, times(1)).received(4); - verify(a, times(1)).received(5); - verify(a, times(1)).received(6); - verify(a, times(0)).received(7); - verify(a, times(1)).error(any(NullPointerException.class)); - } - - @Test - public void testMerge() { - Observable.merge( - Observable.toObservable(1, 2, 3), - Observable.merge( - Observable.toObservable(6), - Observable.error(new NullPointerException()), - Observable.toObservable(7)), - Observable.toObservable(4, 5)) - .subscribe({ result -> a.received(result)}, { exception -> a.error(exception)}); - - // executing synchronously so we can deterministically know what order things will come - verify(a, times(1)).received(1); - verify(a, times(1)).received(2); - verify(a, times(1)).received(3); - verify(a, times(0)).received(4); // the NPE will cause this sequence to be skipped - verify(a, times(0)).received(5); // the NPE will cause this sequence to be skipped - verify(a, times(1)).received(6); // this comes before the NPE so should exist - verify(a, times(0)).received(7);// this comes in the sequence after the NPE - verify(a, times(1)).error(any(NullPointerException.class)); - } - - @Test - public void testScriptWithMaterialize() { - new TestFactory().getObservable().materialize().subscribe({ result -> a.received(result)}); - // 2 times: once for hello_1 and once for onCompleted - verify(a, times(2)).received(any(Notification.class)); - } - - @Test - public void testScriptWithMerge() { - TestFactory f = new TestFactory(); - Observable.merge(f.getObservable(), f.getObservable()).subscribe({ result -> a.received(result)}); - verify(a, times(1)).received("hello_1"); - verify(a, times(1)).received("hello_2"); - } - - @Test - public void testScriptWithOnNext() { - new TestFactory().getObservable().subscribe({ result -> a.received(result)}); - verify(a).received("hello_1"); - } - - @Test - public void testSkipTake() { - Observable.skip(Observable.toObservable(1, 2, 3), 1).take(1).subscribe({ result -> a.received(result)}); - verify(a, times(0)).received(1); - verify(a, times(1)).received(2); - verify(a, times(0)).received(3); - } - - @Test - public void testSkip() { - Observable.skip(Observable.toObservable(1, 2, 3), 2).subscribe({ result -> a.received(result)}); - verify(a, times(0)).received(1); - verify(a, times(0)).received(2); - verify(a, times(1)).received(3); - } - - @Test - public void testTake() { - Observable.take(Observable.toObservable(1, 2, 3), 2).subscribe({ result -> a.received(result)}); - verify(a, times(1)).received(1); - verify(a, times(1)).received(2); - verify(a, times(0)).received(3); - } - - @Test - public void testToSortedList() { - new TestFactory().getNumbers().toSortedList().subscribe({ result -> a.received(result)}); - verify(a, times(1)).received(Arrays.asList(1, 2, 3, 4, 5)); - } - - @Test - public void testToSortedListStatic() { - Observable.toSortedList(Observable.toObservable(1, 3, 2, 5, 4)).subscribe({ result -> a.received(result)}); - verify(a, times(1)).received(Arrays.asList(1, 2, 3, 4, 5)); - } - - @Test - public void testToSortedListWithFunction() { - new TestFactory().getNumbers().toSortedList({a, b -> a - b}).subscribe({ result -> a.received(result)}); - verify(a, times(1)).received(Arrays.asList(1, 2, 3, 4, 5)); - } - - @Test - public void testToSortedListWithFunctionStatic() { - Observable.toSortedList(Observable.toObservable(1, 3, 2, 5, 4), {a, b -> a - b}).subscribe({ result -> a.received(result)}); - verify(a, times(1)).received(Arrays.asList(1, 2, 3, 4, 5)); - } - - def class TestFactory { - int counter = 1; - - public Observable<Integer> getNumbers() { - return Observable.toObservable(1, 3, 2, 5, 4); - } - - public TestObservable getObservable() { - return new TestObservable(counter++); - } - } - - def interface ScriptAssertion { - public void error(Exception o); - - public void received(Object o); - } - - def class TestObservable extends Observable<String> { - private final int count; - - public TestObservable(int count) { - this.count = count; - } - - public Subscription subscribe(Observer<String> observer) { - - observer.onNext("hello_" + count); - observer.onCompleted(); - - return new Subscription() { - - public void unsubscribe() { - // unregister ... will never be called here since we are executing synchronously - } - }; - } - } -} \ No newline at end of file diff --git a/language-adaptors/rxjava-jruby/README.md b/language-adaptors/rxjava-jruby/README.md deleted file mode 100644 index 037c143054..0000000000 --- a/language-adaptors/rxjava-jruby/README.md +++ /dev/null @@ -1,32 +0,0 @@ -# JRuby Adaptor for RxJava - - -This adaptor allows 'org.jruby.RubyProc' lambda functions to be used and RxJava will know how to invoke them. - -This enables code such as: - -```ruby - Observable.toObservable("one", "two", "three") - .take(2) - .subscribe(lambda{|arg| puts arg}) -``` - -# Binaries - -Binaries and dependency information for Maven, Ivy, Gradle and others can be found at [http://search.maven.org](http://search.maven.org/#search%7Cga%7C1%7Ca%3A%22rxjava-jruby%22). - -Example for Maven: - -```xml -<dependency> - <groupId>com.netflix.rxjava</groupId> - <artifactId>rxjava-jruby</artifactId> - <version>x.y.z</version> -</dependency> -``` - -and for Ivy: - -```xml -<dependency org="com.netflix.rxjava" name="rxjava-jruby" rev="x.y.z" /> -``` diff --git a/language-adaptors/rxjava-jruby/build.gradle b/language-adaptors/rxjava-jruby/build.gradle deleted file mode 100644 index 17d2cb2f11..0000000000 --- a/language-adaptors/rxjava-jruby/build.gradle +++ /dev/null @@ -1,20 +0,0 @@ -apply plugin: 'java' -apply plugin: 'eclipse' -apply plugin: 'idea' - -dependencies { - compile project(':rxjava-core') - provided 'org.jruby:jruby:1.6+' - provided 'junit:junit:4.10' - provided 'org.mockito:mockito-core:1.8.5' -} - -eclipse { - classpath { - //you can tweak the classpath of the Eclipse project by adding extra configurations: - plusConfigurations += configurations.provided - - downloadSources = true - downloadJavadoc = true - } -} \ No newline at end of file diff --git a/language-adaptors/rxjava-jruby/src/main/java/rx/lang/jruby/JRubyAdaptor.java b/language-adaptors/rxjava-jruby/src/main/java/rx/lang/jruby/JRubyAdaptor.java deleted file mode 100644 index b5b3bc5bc2..0000000000 --- a/language-adaptors/rxjava-jruby/src/main/java/rx/lang/jruby/JRubyAdaptor.java +++ /dev/null @@ -1,225 +0,0 @@ -/** - * Copyright 2013 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package rx.lang.jruby; - -import static org.mockito.Matchers.*; -import static org.mockito.Mockito.*; - -import java.util.Arrays; - -import org.jruby.Ruby; -import org.jruby.RubyProc; -import org.jruby.embed.ScriptingContainer; -import org.jruby.javasupport.JavaEmbedUtils; -import org.jruby.runtime.builtin.IRubyObject; -import org.junit.Before; -import org.junit.Test; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; - -import rx.Notification; -import rx.Observable; -import rx.Observer; -import rx.Subscription; -import rx.util.functions.Func1; -import rx.util.functions.FunctionLanguageAdaptor; - -public class JRubyAdaptor implements FunctionLanguageAdaptor { - - @Override - public Object call(Object function, Object[] args) { - RubyProc rubyProc = ((RubyProc) function); - Ruby ruby = rubyProc.getRuntime(); - IRubyObject rubyArgs[] = new IRubyObject[args.length]; - for (int i = 0; i < args.length; i++) { - rubyArgs[i] = JavaEmbedUtils.javaToRuby(ruby, args[i]); - } - return rubyProc.getBlock().call(ruby.getCurrentContext(), rubyArgs); - } - - @Override - public Class<?>[] getFunctionClass() { - return new Class<?>[] { RubyProc.class }; - } - - public static class UnitTest { - - @Mock - ScriptAssertion assertion; - - @Mock - Observer<Integer> w; - - @Before - public void before() { - MockitoAnnotations.initMocks(this); - } - - @Test - public void testCreateViaGroovy() { - runGroovyScript("Observable.create(lambda{|it| it.onNext('hello');it.onCompleted();}).subscribe(lambda{|result| a.received(result)});"); - verify(assertion, times(1)).received("hello"); - } - - @Test - public void testFilterViaGroovy() { - runGroovyScript("Observable.filter(Observable.toObservable(1, 2, 3), lambda{|it| it >= 2}).subscribe(lambda{|result| a.received(result)});"); - verify(assertion, times(0)).received(1L); - verify(assertion, times(1)).received(2L); - verify(assertion, times(1)).received(3L); - } - - @Test - public void testLast() { - String script = "mockApiCall.getObservable().last().subscribe(lambda{|result| a.received(result)})"; - runGroovyScript(script); - verify(assertion, times(1)).received("hello_1"); - } - - @Test - public void testMap() { - String script = "mockApiCall.getObservable().map(lambda{|v| 'say' + v}).subscribe(lambda{|result| a.received(result)});"; - runGroovyScript(script); - verify(assertion, times(1)).received("sayhello_1"); - } - - @Test - public void testMaterializeViaGroovy() { - runGroovyScript("Observable.materialize(Observable.toObservable(1, 2, 3)).subscribe(lambda{|result| a.received(result)});"); - // we expect 4 onNext calls: 3 for 1, 2, 3 ObservableNotification.OnNext and 1 for ObservableNotification.OnCompleted - verify(assertion, times(4)).received(any(Notification.class)); - verify(assertion, times(0)).error(any(Exception.class)); - } - - @Test - public void testScriptWithMaterialize() { - String script = "mockApiCall.getObservable().materialize().subscribe(lambda{|result| a.received(result)});"; - runGroovyScript(script); - // 2 times: once for hello_1 and once for onCompleted - verify(assertion, times(2)).received(any(Notification.class)); - } - - @Test - public void testScriptWithMerge() { - String script = "Observable.merge(mockApiCall.getObservable(), mockApiCall.getObservable()).subscribe(lambda{|result| a.received(result)});"; - runGroovyScript(script); - verify(assertion, times(1)).received("hello_1"); - verify(assertion, times(1)).received("hello_2"); - } - - @Test - public void testScriptWithOnNext() { - String script = "mockApiCall.getObservable().subscribe(lambda{|result| a.received(result)})"; - runGroovyScript(script); - verify(assertion).received("hello_1"); - } - - @Test - public void testSkipTakeViaGroovy() { - runGroovyScript("Observable.skip(Observable.toObservable(1, 2, 3), 1).take(1).subscribe(lambda{|result| a.received(result)});"); - verify(assertion, times(0)).received(1); - verify(assertion, times(1)).received(2L); - verify(assertion, times(0)).received(3); - } - - @Test - public void testSkipViaGroovy() { - runGroovyScript("Observable.skip(Observable.toObservable(1, 2, 3), 2).subscribe(lambda{|result| a.received(result)});"); - verify(assertion, times(0)).received(1); - verify(assertion, times(0)).received(2); - verify(assertion, times(1)).received(3L); - } - - @Test - public void testTakeViaGroovy() { - runGroovyScript("Observable.take(Observable.toObservable(1, 2, 3), 2).subscribe(lambda{|result| a.received(result)});"); - verify(assertion, times(1)).received(1L); - verify(assertion, times(1)).received(2L); - verify(assertion, times(0)).received(3); - } - - @Test - public void testToSortedList() { - runGroovyScript("mockApiCall.getNumbers().toSortedList().subscribe(lambda{|result| a.received(result)});"); - verify(assertion, times(1)).received(Arrays.asList(1, 2, 3, 4, 5)); - } - - private void runGroovyScript(String script) { - ScriptingContainer container = new ScriptingContainer(); - container.put("mockApiCall", new TestFactory()); - container.put("a", assertion); - - StringBuilder b = new StringBuilder(); - // force JRuby to always use subscribe(Object) - b.append("import \"rx.Observable\"").append("\n"); - b.append("class Observable").append("\n"); - b.append(" java_alias :subscribe, :subscribe, [java.lang.Object]").append("\n"); - b.append("end").append("\n"); - b.append(script); - - container.runScriptlet(b.toString()); - } - - private static interface ScriptAssertion { - public void error(Exception o); - - public void received(Object o); - } - - public static class TestFactory { - int counter = 1; - - public Observable<Integer> getNumbers() { - return Observable.toObservable(1, 3, 2, 5, 4); - } - - public TestObservable getObservable() { - return new TestObservable(counter++); - } - } - - private static class TestObservable extends Observable<String> { - private final int count; - - public TestObservable(int count) { - super(new Func1<Observer<String>, Subscription>() { - - @Override - public Subscription call(Observer<String> t1) { - // do nothing, override subscribe for test - return null; - } - }); - this.count = count; - } - - public Subscription subscribe(Observer<String> observer) { - - observer.onNext("hello_" + count); - observer.onCompleted(); - - return new Subscription() { - - public void unsubscribe() { - // unregister ... will never be called here since we are executing synchronously - } - - }; - } - } - } - -} diff --git a/language-adaptors/rxjava-scala/README.md b/language-adaptors/rxjava-scala/README.md deleted file mode 100644 index b5943af9c5..0000000000 --- a/language-adaptors/rxjava-scala/README.md +++ /dev/null @@ -1,34 +0,0 @@ -# Scala Adaptor for RxJava - - -This adaptor allows 'fn' functions to be used and RxJava will know how to invoke them. - -This enables code such as: - -```scala -Observable.toObservable("1", "2", "3") - .take(2) - .subscribe((callback: String) => { - println(callback) - }) -``` - -# Binaries - -Binaries and dependency information for Maven, Ivy, Gradle and others can be found at [http://search.maven.org](http://search.maven.org/#search%7Cga%7C1%7Ca%3A%22rxjava-scala%22). - -Example for Maven: - -```xml -<dependency> - <groupId>com.netflix.rxjava</groupId> - <artifactId>rxjava-scala</artifactId> - <version>x.y.z</version> -</dependency> -``` - -and for Ivy: - -```xml -<dependency org="com.netflix.rxjava" name="rxjava-scala" rev="x.y.z" /> -``` diff --git a/language-adaptors/rxjava-scala/build.gradle b/language-adaptors/rxjava-scala/build.gradle deleted file mode 100644 index 25be45894d..0000000000 --- a/language-adaptors/rxjava-scala/build.gradle +++ /dev/null @@ -1,46 +0,0 @@ -apply plugin: 'scala' -apply plugin: 'eclipse' -apply plugin: 'idea' - -tasks.withType(ScalaCompile) { - scalaCompileOptions.fork = true - scalaCompileOptions.unchecked = true - - configure(scalaCompileOptions.forkOptions) { - memoryMaximumSize = '1g' - jvmArgs = ['-XX:MaxPermSize=512m'] - } -} - -dependencies { - // Scala compiler and related tools - scalaTools 'org.scala-lang:scala-compiler:2.10+' - scalaTools 'org.scala-lang:scala-library:2.10+' - provided 'org.scalatest:scalatest_2.10:1.9.1' - - compile project(':rxjava-core') - provided 'junit:junit:4.10' - provided 'org.mockito:mockito-core:1.8.5' - - testCompile 'org.scalatest:scalatest_2.10:1.9.1' -} - -task test(overwrite: true, dependsOn: testClasses) << { - ant.taskdef(name: 'scalatest', - classname: 'org.scalatest.tools.ScalaTestAntTask', - classpath: sourceSets.test.runtimeClasspath.asPath - ) - ant.scalatest(runpath: sourceSets.test.classesDir, - haltonfailure: 'true', - fork: 'false') {reporter(type: 'stdout')} -} - -eclipse { - classpath { - //you can tweak the classpath of the Eclipse project by adding extra configurations: - plusConfigurations += configurations.provided - - downloadSources = true - downloadJavadoc = true - } -} \ No newline at end of file diff --git a/language-adaptors/rxjava-scala/src/main/scala/rx/lang/scala/ScalaAdaptor.scala b/language-adaptors/rxjava-scala/src/main/scala/rx/lang/scala/ScalaAdaptor.scala deleted file mode 100644 index 431b63ffbc..0000000000 --- a/language-adaptors/rxjava-scala/src/main/scala/rx/lang/scala/ScalaAdaptor.scala +++ /dev/null @@ -1,187 +0,0 @@ -/** - * Copyright 2013 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package rx.lang.scala - -import rx.util.functions.FunctionLanguageAdaptor -import org.junit.{Assert, Before, Test} -import rx.Observable -import org.scalatest.junit.JUnitSuite -import org.mockito.Mockito._ -import org.mockito.{MockitoAnnotations, Mock} - -import scala.collection.JavaConverters._ -import collection.mutable.ArrayBuffer - -class ScalaAdaptor extends FunctionLanguageAdaptor { - - val ON_NEXT = "onNext" - val ON_ERROR = "onError" - val ON_COMPLETED = "onCompleted" - - def getFunctionClass: Array[Class[_]] = { - return Array(classOf[Map[String, _]], classOf[(AnyRef) => Object], classOf[(AnyRef, AnyRef) => Object], - classOf[(AnyRef, AnyRef, AnyRef) => Object], classOf[(AnyRef, AnyRef, AnyRef, AnyRef) => Object], - classOf[(AnyRef, AnyRef, AnyRef, AnyRef) => Object], - classOf[(AnyRef, AnyRef, AnyRef, AnyRef, AnyRef) => Object], - classOf[(AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef) => Object], - classOf[(AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef) =>Object], - classOf[(AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef) => Object], - classOf[(AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef) => Object], - classOf[(AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef) => Object], - classOf[(AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef) => Object], - classOf[(AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef) => Object], - classOf[(AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef) => Object], - classOf[(AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef) => Object], - classOf[(AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef) => Object], - classOf[(AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef) => Object], - classOf[(AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef) => Object], - classOf[(AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef) => Object], - classOf[(AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef) => Object], - classOf[(AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef) => Object]) - } - - def call(function: AnyRef, args: Array[AnyRef]) : Object = { - function match { - case (func: Map[String, _]) => return matchOption(func.get(ON_NEXT), args) - case _ => return matchFunction(function, args) - } - } - - private def matchOption(funcOption: Option[_], args: Array[AnyRef]) : Object = { - funcOption match { - case Some(func: AnyRef) => return matchFunction(func, args) - case _ => return None - } - } - - private def matchFunction(function: AnyRef, args: Array[AnyRef]) : Object = function match { - case (f: ((AnyRef) => Object)) => return f(args(0)) - case (f: ((AnyRef, AnyRef) => Object)) => return f(args(0), args(1)) - case (f: ((AnyRef, AnyRef, AnyRef) => Object)) => return f(args(0), args(1), args(2)) - case (f: ((AnyRef, AnyRef, AnyRef, AnyRef) => Object)) => - return f(args(0), args(1), args(2), args(3)) - case (f: ((AnyRef, AnyRef, AnyRef, AnyRef, AnyRef) => Object)) => - return f(args(0), args(1), args(2), args(3), args(4)) - case (f: ((AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef) => Object)) => - return f(args(0), args(1), args(2), args(3), args(4), args(5)) - case (f: ((AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef) => Object)) => - return f(args(0), args(1), args(2), args(3), args(4), args(5), args(6)) - case (f: ((AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef) => Object)) => - return f(args(0), args(1), args(2), args(3), args(4), args(5), args(6), args(7)) - case (f: ((AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef) => Object)) => - return f(args(0), args(1), args(2), args(3), args(4), args(5), args(6), args(7), args(8)) - case (f: ((AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef) => Object)) => - return f(args(0), args(1), args(2), args(3), args(4), args(5), args(6), args(7), args(8), args(9)) - case (f: ((AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef) => Object)) => - return f(args(0), args(1), args(2), args(3), args(4), args(5), args(6), args(7), args(8), args(9), args(10)) - case (f: ((AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef) => Object)) => - return f(args(0), args(1), args(2), args(3), args(4), args(5), args(6), args(7), args(8), args(9), args(10), args(11)) - case (f: ((AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef) => Object)) => - return f(args(0), args(1), args(2), args(3), args(4), args(5), args(6), args(7), args(8), args(9), args(10), args(11), args(12)) - case (f: ((AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef) => Object)) => - return f(args(0), args(1), args(2), args(3), args(4), args(5), args(6), args(7), args(8), args(9), args(10), args(11), args(12), args(13)) - case (f: ((AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef) => Object)) => - return f(args(0), args(1), args(2), args(3), args(4), args(5), args(6), args(7), args(8), args(9), args(10), args(11), args(12), args(13), args(14)) - case (f: ((AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef) => Object)) => - return f(args(0), args(1), args(2), args(3), args(4), args(5), args(6), args(7), args(8), args(9), args(10), args(11), args(12), args(13), args(14), args(15)) - case (f: ((AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef) => Object)) => - return f(args(0), args(1), args(2), args(3), args(4), args(5), args(6), args(7), args(8), args(9), args(10), args(11), args(12), args(13), args(14), args(15), args(16)) - case (f: ((AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef) => Object)) => - return f(args(0), args(1), args(2), args(3), args(4), args(5), args(6), args(7), args(8), args(9), args(10), args(11), args(12), args(13), args(14), args(15), args(16), args(17)) - case (f: ((AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef) => Object)) => - return f(args(0), args(1), args(2), args(3), args(4), args(5), args(6), args(7), args(8), args(9), args(10), args(11), args(12), args(13), args(14), args(15), args(16), args(17), args(18)) - case (f: ((AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef) => Object)) => - return f(args(0), args(1), args(2), args(3), args(4), args(5), args(6), args(7), args(8), args(9), args(10), args(11), args(12), args(13), args(14), args(15), args(16), args(17), args(18), args(19)) - case (f: ((AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef) => Object)) => - return f(args(0), args(1), args(2), args(3), args(4), args(5), args(6), args(7), args(8), args(9), args(10), args(11), args(12), args(13), args(14), args(15), args(16), args(17), args(18), args(19), args(20)) - case (f: ((AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef, AnyRef) => Object)) => - return f(args(0), args(1), args(2), args(3), args(4), args(5), args(6), args(7), args(8), args(9), args(10), args(11), args(12), args(13), args(14), args(15), args(16), args(17), args(18), args(19), args(20), args(21)) - - } -} - -class UnitTestSuite extends JUnitSuite { - @Mock private[this] - val assertion: ScriptAssertion = null - - @Before def before { - MockitoAnnotations.initMocks(this) - } - - @Test def testTake() { - Observable.toObservable("1", "2", "3").take(1).subscribe(Map( - "onNext" -> ((callback: String) => { - print("testTake: callback = " + callback) - assertion.received(callback) - }) - )) - verify(assertion, times(1)).received("1") - } - - @Test def testFilterWithToList() { - val numbers = Observable.toObservable[Int](1, 2, 3, 4, 5, 6, 7, 8, 9) - numbers.filter((x: Int) => 0 == (x % 2)).toList().subscribe( - (callback: java.util.List[Int]) => { - val lst = callback.asScala.toList - println("filter onNext -> got " + lst) - assertion.received(lst) - } - ) - verify(assertion, times(1)).received(List(2,4,6,8)) - } - - @Test def testLast() { - val numbers = Observable.toObservable[Int](1, 2, 3, 4, 5, 6, 7, 8, 9) - numbers.last().subscribe((callback: Int) => { - println("testLast: onNext -> got " + callback) - assertion.received(callback) - }) - verify(assertion, times(1)).received(9) - } - - @Test def testMap() { - val numbers = Observable.toObservable(1, 2, 3, 4, 5, 6, 7, 8, 9) - val mappedNumbers = new ArrayBuffer[Int]() - numbers.map(((x: Int)=> { x * x })).subscribe(((squareVal: Int) => { - println("square is " + squareVal ) - mappedNumbers += squareVal - })) - Assert.assertEquals(List(1,4,9,16,25,36,49,64,81), mappedNumbers.toList) - - } - - @Test def testZip() { - val numbers = Observable.toObservable(1, 2, 3) - val colors = Observable.toObservable("red", "green", "blue") - val characters = Observable.toObservable("lion-o", "cheetara", "panthro") - - Observable.zip(numbers.toList, colors.toList, characters.toList, ((n: java.util.List[Int], c: java.util.List[String], t: java.util.List[String]) => { Map( - "numbers" -> n, - "colors" -> c, - "thundercats" -> t - )})).subscribe((m: Map[String, _]) => { - println("zipped map is " + m.toString()) - }) - - - } - - trait ScriptAssertion { - def error(ex: Exception) - - def received(obj: Any) - } -} diff --git a/pmd.xml b/pmd.xml new file mode 100644 index 0000000000..728e1e4d1d --- /dev/null +++ b/pmd.xml @@ -0,0 +1,222 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ruleset xmlns="http://pmd.sourceforge.net/ruleset/2.0.0" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + name="pmd" + xsi:schemaLocation="http://pmd.sourceforge.net/ruleset/2.0.0%20http://pmd.sourceforge.net/ruleset_2_0_0.xsd"> + <description>RxJava PMD ruleset</description> + <rule ref="rulesets/java/design.xml/AbstractClassWithoutAbstractMethod"/> + <rule ref="rulesets/java/design.xml/AbstractClassWithoutAnyMethod"/> + <rule ref="rulesets/java/design.xml/AccessorClassGeneration"/> + <rule ref="rulesets/java/optimizations.xml/AddEmptyString"/> + <rule ref="rulesets/java/strings.xml/AppendCharacterWithChar"/> + <rule ref="rulesets/java/design.xml/AssignmentToNonFinalStatic"/> + <rule ref="rulesets/java/controversial.xml/AvoidAccessibilityAlteration"/> + <rule ref="rulesets/java/optimizations.xml/AvoidArrayLoops"/> + <rule ref="rulesets/java/migrating.xml/AvoidAssertAsIdentifier"/> + <rule ref="rulesets/java/basic.xml/AvoidBranchingStatementAsLastInLoop"/> + <rule ref="rulesets/java/finalizers.xml/AvoidCallingFinalize"/> + <rule ref="rulesets/java/strictexception.xml/AvoidCatchingGenericException"/> + <rule ref="rulesets/java/strictexception.xml/AvoidCatchingNPE"/> + <rule ref="rulesets/java/design.xml/AvoidConstantsInterface"/> + <rule ref="rulesets/java/basic.xml/AvoidDecimalLiteralsInBigDecimalConstructor"/> + <rule ref="rulesets/java/naming.xml/AvoidDollarSigns"/> + <rule ref="rulesets/java/migrating.xml/AvoidEnumAsIdentifier"/> + <rule ref="rulesets/java/controversial.xml/AvoidFinalLocalVariable"/> + <rule ref="rulesets/java/design.xml/AvoidInstanceofChecksInCatchClause"/> + <rule ref="rulesets/java/strictexception.xml/AvoidLosingExceptionInformation"/> + <rule ref="rulesets/java/basic.xml/AvoidMultipleUnaryOperators"/> + <rule ref="rulesets/java/controversial.xml/AvoidPrefixingMethodParameters"/> + <rule ref="rulesets/java/logging-java.xml/AvoidPrintStackTrace"/> + <rule ref="rulesets/java/design.xml/AvoidProtectedFieldInFinalClass"/> + <rule ref="rulesets/java/design.xml/AvoidProtectedMethodInFinalClassNotExtending"/> + <rule ref="rulesets/java/strictexception.xml/AvoidRethrowingException"/> + <rule ref="rulesets/java/strings.xml/AvoidStringBufferField"/> + <rule ref="rulesets/java/design.xml/AvoidSynchronizedAtMethodLevel"/> + <rule ref="rulesets/java/basic.xml/AvoidThreadGroup"/> + <rule ref="rulesets/java/strictexception.xml/AvoidThrowingNewInstanceOfSameException"/> + <rule ref="rulesets/java/strictexception.xml/AvoidThrowingRawExceptionTypes"/> + <rule ref="rulesets/java/basic.xml/AvoidUsingHardCodedIP"/> + <rule ref="rulesets/java/controversial.xml/AvoidUsingNativeCode"/> + <rule ref="rulesets/java/basic.xml/AvoidUsingOctalValues"/> + <rule ref="rulesets/java/controversial.xml/AvoidUsingShortType"/> + <rule ref="rulesets/java/design.xml/BadComparison"/> + <rule ref="rulesets/java/basic.xml/BigIntegerInstantiation"/> + <rule ref="rulesets/java/naming.xml/BooleanGetMethodName"/> + <rule ref="rulesets/java/basic.xml/BooleanInstantiation"/> + <rule ref="rulesets/java/basic.xml/BrokenNullCheck"/> + <rule ref="rulesets/java/migrating.xml/ByteInstantiation"/> + <rule ref="rulesets/java/android.xml/CallSuperFirst"/> + <rule ref="rulesets/java/android.xml/CallSuperLast"/> + <rule ref="rulesets/java/basic.xml/CheckResultSet"/> + <rule ref="rulesets/java/basic.xml/CheckSkipResult"/> + <rule ref="rulesets/java/basic.xml/ClassCastExceptionWithToArray"/> + <rule ref="rulesets/java/naming.xml/ClassNamingConventions"/> + <rule ref="rulesets/java/design.xml/ClassWithOnlyPrivateConstructorsShouldBeFinal"/> + <rule ref="rulesets/java/clone.xml/CloneMethodMustImplementCloneable"/> + <rule ref="rulesets/java/clone.xml/CloneThrowsCloneNotSupportedException"/> + <rule ref="rulesets/java/design.xml/CloseResource"/> + <rule ref="rulesets/java/comments.xml/CommentContent"/> + <rule ref="rulesets/java/strings.xml/ConsecutiveAppendsShouldReuse"/> + <rule ref="rulesets/java/strings.xml/ConsecutiveLiteralAppends"/> + <rule ref="rulesets/java/design.xml/ConstructorCallsOverridableMethod"/> + <rule ref="rulesets/java/coupling.xml/CouplingBetweenObjects"/> + <rule ref="rulesets/java/design.xml/DefaultLabelNotLastInSwitchStmt"/> + <rule ref="rulesets/java/controversial.xml/DoNotCallGarbageCollectionExplicitly"/> + <rule ref="rulesets/java/j2ee.xml/DoNotCallSystemExit"/> + <rule ref="rulesets/java/strictexception.xml/DoNotExtendJavaLangError"/> + <rule ref="rulesets/java/android.xml/DoNotHardCodeSDCard"/> + <rule ref="rulesets/java/strictexception.xml/DoNotThrowExceptionInFinally"/> + <rule ref="rulesets/java/basic.xml/DontCallThreadRun"/> + <rule ref="rulesets/java/imports.xml/DontImportJavaLang"/> + <rule ref="rulesets/java/basic.xml/DontUseFloatTypeForLoopIndices"/> + <rule ref="rulesets/java/basic.xml/DoubleCheckedLocking"/> + <rule ref="rulesets/java/imports.xml/DuplicateImports"/> + <rule ref="rulesets/java/finalizers.xml/EmptyFinalizer"/> + <rule ref="rulesets/java/empty.xml/EmptyFinallyBlock"/> + <rule ref="rulesets/java/empty.xml/EmptyInitializer"/> + <rule ref="rulesets/java/empty.xml/EmptyStatementBlock"/> + <rule ref="rulesets/java/empty.xml/EmptyStatementNotInLoop"/> + <rule ref="rulesets/java/empty.xml/EmptyStaticInitializer"/> + <rule ref="rulesets/java/empty.xml/EmptySwitchStatements"/> + <rule ref="rulesets/java/empty.xml/EmptySynchronizedBlock"/> + <rule ref="rulesets/java/empty.xml/EmptyTryBlock"/> + <rule ref="rulesets/java/empty.xml/EmptyWhileStmt"/> + <rule ref="rulesets/java/design.xml/EqualsNull"/> + <rule ref="rulesets/java/strictexception.xml/ExceptionAsFlowControl"/> + <rule ref="rulesets/java/coupling.xml/ExcessiveImports"/> + <rule ref="rulesets/java/basic.xml/ExtendsObject"/> + <rule ref="rulesets/java/design.xml/FieldDeclarationsShouldBeAtStartOfClass"/> + <rule ref="rulesets/java/design.xml/FinalFieldCouldBeStatic"/> + <rule ref="rulesets/java/finalizers.xml/FinalizeDoesNotCallSuperFinalize"/> + <rule ref="rulesets/java/finalizers.xml/FinalizeOnlyCallsSuperFinalize"/> + <rule ref="rulesets/java/finalizers.xml/FinalizeOverloaded"/> + <rule ref="rulesets/java/finalizers.xml/FinalizeShouldBeProtected"/> + <rule ref="rulesets/java/basic.xml/ForLoopShouldBeWhileLoop"/> + <rule ref="rulesets/java/braces.xml/ForLoopsMustUseBraces"/> + <rule ref="rulesets/java/logging-jakarta-commons.xml/GuardDebugLogging"/> + <rule ref="rulesets/java/logging-jakarta-commons.xml/GuardLogStatement"/> + <rule ref="rulesets/java/logging-java.xml/GuardLogStatementJavaUtil"/> + <rule ref="rulesets/java/design.xml/IdempotentOperations"/> + <rule ref="rulesets/java/braces.xml/IfElseStmtsMustUseBraces"/> + <rule ref="rulesets/java/braces.xml/IfStmtsMustUseBraces"/> + <rule ref="rulesets/java/design.xml/ImmutableField"/> + <rule ref="rulesets/java/imports.xml/ImportFromSamePackage"/> + <rule ref="rulesets/java/strings.xml/InefficientEmptyStringCheck"/> + <rule ref="rulesets/java/strings.xml/InefficientStringBuffering"/> + <rule ref="rulesets/java/design.xml/InstantiationToGetClass"/> + <rule ref="rulesets/java/strings.xml/InsufficientStringBufferDeclaration"/> + <rule ref="rulesets/java/migrating.xml/IntegerInstantiation"/> + <rule ref="rulesets/java/migrating.xml/JUnit4SuitesShouldUseSuiteAnnotation"/> + <rule ref="rulesets/java/migrating.xml/JUnit4TestShouldUseAfterAnnotation"/> + <rule ref="rulesets/java/migrating.xml/JUnit4TestShouldUseBeforeAnnotation"/> + <rule ref="rulesets/java/junit.xml/JUnitAssertionsShouldIncludeMessage"/> + <rule ref="rulesets/java/junit.xml/JUnitSpelling"/> + <rule ref="rulesets/java/junit.xml/JUnitStaticSuite"/> + <rule ref="rulesets/java/junit.xml/JUnitTestContainsTooManyAsserts"/> + <rule ref="rulesets/java/junit.xml/JUnitTestsShouldIncludeAssert"/> + <rule ref="rulesets/java/migrating.xml/JUnitUseExpected"/> + <rule ref="rulesets/java/basic.xml/JumbledIncrementer"/> + <rule ref="rulesets/java/j2ee.xml/LocalHomeNamingConvention"/> + <rule ref="rulesets/java/j2ee.xml/LocalInterfaceSessionNamingConvention"/> + <rule ref="rulesets/java/logging-java.xml/LoggerIsNotStaticFinal"/> + <rule ref="rulesets/java/design.xml/LogicInversion"/> + <rule ref="rulesets/java/migrating.xml/LongInstantiation"/> + <rule ref="rulesets/java/coupling.xml/LooseCoupling"/> + <rule ref="rulesets/java/j2ee.xml/MDBAndSessionBeanNamingConvention"/> + <rule ref="rulesets/java/naming.xml/MethodNamingConventions"/> + <rule ref="rulesets/java/sunsecure.xml/MethodReturnsInternalArray"/> + <rule ref="rulesets/java/naming.xml/MethodWithSameNameAsEnclosingClass"/> + <rule ref="rulesets/java/naming.xml/MisleadingVariableName"/> + <rule ref="rulesets/java/basic.xml/MisplacedNullCheck"/> + <rule ref="rulesets/java/design.xml/MissingBreakInSwitch"/> + <rule ref="rulesets/java/javabeans.xml/MissingSerialVersionUID"/> + <rule ref="rulesets/java/design.xml/MissingStaticMethodInNonInstantiatableClass"/> + <rule ref="rulesets/java/logging-java.xml/MoreThanOneLogger"/> + <rule ref="rulesets/java/naming.xml/NoPackage"/> + <rule ref="rulesets/java/design.xml/NonCaseLabelInSwitchStatement"/> + <rule ref="rulesets/java/design.xml/NonStaticInitializer"/> + <rule ref="rulesets/java/design.xml/NonThreadSafeSingleton"/> + <rule ref="rulesets/java/controversial.xml/OneDeclarationPerLine"/> + <rule ref="rulesets/java/design.xml/OptimizableToArrayCall"/> + <rule ref="rulesets/java/basic.xml/OverrideBothEqualsAndHashcode"/> + <rule ref="rulesets/java/naming.xml/PackageCase"/> + <rule ref="rulesets/java/design.xml/PositionLiteralsFirstInCaseInsensitiveComparisons"/> + <rule ref="rulesets/java/design.xml/PositionLiteralsFirstInComparisons"/> + <rule ref="rulesets/java/optimizations.xml/PrematureDeclaration"/> + <rule ref="rulesets/java/design.xml/PreserveStackTrace"/> + <rule ref="rulesets/java/clone.xml/ProperCloneImplementation"/> + <rule ref="rulesets/java/logging-jakarta-commons.xml/ProperLogger"/> + <rule ref="rulesets/java/optimizations.xml/RedundantFieldInitializer"/> + <rule ref="rulesets/java/j2ee.xml/RemoteInterfaceNamingConvention"/> + <rule ref="rulesets/java/j2ee.xml/RemoteSessionInterfaceNamingConvention"/> + <rule ref="rulesets/java/migrating.xml/ReplaceEnumerationWithIterator"/> + <rule ref="rulesets/java/migrating.xml/ReplaceHashtableWithMap"/> + <rule ref="rulesets/java/migrating.xml/ReplaceVectorWithList"/> + <rule ref="rulesets/java/design.xml/ReturnEmptyArrayRatherThanNull"/> + <rule ref="rulesets/java/basic.xml/ReturnFromFinallyBlock"/> + <rule ref="rulesets/java/migrating.xml/ShortInstantiation"/> + <rule ref="rulesets/java/strictexception.xml/SignatureDeclareThrowsException"/> + <rule ref="rulesets/java/design.xml/SimpleDateFormatNeedsLocale"/> + <rule ref="rulesets/java/junit.xml/SimplifyBooleanAssertion"/> + <rule ref="rulesets/java/design.xml/SimplifyBooleanExpressions"/> + <rule ref="rulesets/java/design.xml/SimplifyBooleanReturns"/> + <rule ref="rulesets/java/design.xml/SimplifyConditional"/> + <rule ref="rulesets/java/optimizations.xml/SimplifyStartsWith"/> + <rule ref="rulesets/java/design.xml/SingularField"/> + <rule ref="rulesets/java/j2ee.xml/StaticEJBFieldShouldBeFinal"/> + <rule ref="rulesets/java/strings.xml/StringBufferInstantiationWithChar"/> + <rule ref="rulesets/java/strings.xml/StringInstantiation"/> + <rule ref="rulesets/java/strings.xml/StringToString"/> + <rule ref="rulesets/java/naming.xml/SuspiciousConstantFieldName"/> + <rule ref="rulesets/java/naming.xml/SuspiciousEqualsMethodName"/> + <rule ref="rulesets/java/naming.xml/SuspiciousHashcodeMethodName"/> + <rule ref="rulesets/java/controversial.xml/SuspiciousOctalEscape"/> + <rule ref="rulesets/java/design.xml/SwitchDensity"/> + <rule ref="rulesets/java/design.xml/SwitchStmtsShouldHaveDefault"/> + <rule ref="rulesets/java/logging-java.xml/SystemPrintln"/> + <rule ref="rulesets/java/junit.xml/TestClassWithoutTestCases"/> + <rule ref="rulesets/java/design.xml/TooFewBranchesForASwitchStatement"/> + <rule ref="rulesets/java/imports.xml/TooManyStaticImports"/> + <rule ref="rulesets/java/basic.xml/UnconditionalIfStatement"/> + <rule ref="rulesets/java/junit.xml/UnnecessaryBooleanAssertion"/> + <rule ref="rulesets/java/strings.xml/UnnecessaryCaseChange"/> + <rule ref="rulesets/java/controversial.xml/UnnecessaryConstructor"/> + <rule ref="rulesets/java/unnecessary.xml/UnnecessaryConversionTemporary"/> + <rule ref="rulesets/java/unnecessary.xml/UnnecessaryFinalModifier"/> + <rule ref="rulesets/java/imports.xml/UnnecessaryFullyQualifiedName"/> + <rule ref="rulesets/java/design.xml/UnnecessaryLocalBeforeReturn"/> + <rule ref="rulesets/java/controversial.xml/UnnecessaryParentheses"/> + <rule ref="rulesets/java/unnecessary.xml/UnnecessaryReturn"/> + <rule ref="rulesets/java/optimizations.xml/UnnecessaryWrapperObjectCreation"/> + <rule ref="rulesets/java/design.xml/UnsynchronizedStaticDateFormatter"/> + <rule ref="rulesets/java/unusedcode.xml/UnusedFormalParameter"/> + <rule ref="rulesets/java/imports.xml/UnusedImports"/> + <rule ref="rulesets/java/unusedcode.xml/UnusedLocalVariable"/> + <rule ref="rulesets/java/unusedcode.xml/UnusedModifier"/> + <rule ref="rulesets/java/unnecessary.xml/UnusedNullCheckInEquals"/> + <rule ref="rulesets/java/unusedcode.xml/UnusedPrivateField"/> + <rule ref="rulesets/java/unusedcode.xml/UnusedPrivateMethod"/> + <rule ref="rulesets/java/optimizations.xml/UseArrayListInsteadOfVector"/> + <rule ref="rulesets/java/optimizations.xml/UseArraysAsList"/> + <rule ref="rulesets/java/junit.xml/UseAssertEqualsInsteadOfAssertTrue"/> + <rule ref="rulesets/java/junit.xml/UseAssertNullInsteadOfAssertTrue"/> + <rule ref="rulesets/java/junit.xml/UseAssertSameInsteadOfAssertTrue"/> + <rule ref="rulesets/java/junit.xml/UseAssertTrueInsteadOfAssertEquals"/> + <rule ref="rulesets/java/design.xml/UseCollectionIsEmpty"/> + <rule ref="rulesets/java/controversial.xml/UseConcurrentHashMap"/> + <rule ref="rulesets/java/logging-jakarta-commons.xml/UseCorrectExceptionLogging"/> + <rule ref="rulesets/java/strings.xml/UseEqualsToCompareStrings"/> + <rule ref="rulesets/java/strings.xml/UseIndexOfChar"/> + <rule ref="rulesets/java/design.xml/UseLocaleWithCaseConversions"/> + <rule ref="rulesets/java/design.xml/UseNotifyAllInsteadOfNotify"/> + <rule ref="rulesets/java/controversial.xml/UseObjectForClearerAPI"/> + <rule ref="rulesets/java/j2ee.xml/UseProperClassLoader"/> + <rule ref="rulesets/java/optimizations.xml/UseStringBufferForStringAppends"/> + <rule ref="rulesets/java/strings.xml/UseStringBufferLength"/> + <rule ref="rulesets/java/design.xml/UseUtilityClass"/> + <rule ref="rulesets/java/unnecessary.xml/UselessOperationOnImmutable"/> + <rule ref="rulesets/java/unnecessary.xml/UselessOverridingMethod"/> + <rule ref="rulesets/java/strings.xml/UselessStringValueOf"/> + <rule ref="rulesets/java/naming.xml/VariableNamingConventions"/> + <rule ref="rulesets/java/braces.xml/WhileLoopsMustUseBraces"/> +</ruleset> \ No newline at end of file diff --git a/push_javadoc.sh b/push_javadoc.sh new file mode 100644 index 0000000000..28ce74f1db --- /dev/null +++ b/push_javadoc.sh @@ -0,0 +1,122 @@ +#!/bin/bash +# ---------------------------------------------------------- +# Automatically push back the generated JavaDocs to gh-pages +# ---------------------------------------------------------- +# based on https://gist.github.com/willprice/e07efd73fb7f13f917ea + +# specify the common address for the repository +targetRepo=github.com/ReactiveX/RxJava.git +# ======================================================================= + +# get the current build tag if any +buildTag="$BUILD_TAG" +echo -e "Build tag: '$buildTag'" + +if [ "$buildTag" == "" ]; then + buildTag="snapshot" +else + buildTag="${buildTag:1}" +fi + +echo -e "JavaDocs pushback for tag: $buildTag" + +# check if the token is actually there +if [ "$JAVADOCS_TOKEN" == "" ]; then + echo -e "No access to GitHub, skipping JavaDocs pushback." + exit 0 +fi + +# prepare the git information +git config --global user.email "akarnokd+ci@gmail.com" +git config --global user.name "akarnokd+ci" + +# setup the remote +echo -e "Adding the target repository to git" +git remote add origin-pages https://${JAVADOCS_TOKEN}@${targetRepo} > /dev/null 2>&1 + +# stash changes due to chmod +echo -e "Stashing any local non-ignored changes" +git stash + +# get the gh-pages +echo -e "Update branches and checking out gh-pages" +git fetch --all +git branch -a +git checkout -b gh-pages origin-pages/gh-pages + +# releases should update 2 extra locations +if [ "$buildTag" != "snapshot" ]; then + # for releases, add a new directory with the new version + # and carefully replace the others + + # !!!!!!!!!!!!!!!!!!!!!!!!!!!!! + # enable once 3.x is mainstream + # vvvvvvvvvvvvvvvvvvvvvvvvvvvvv + + # 1.) main javadoc + # ---------------- + # remove the io subdir + #echo -e "Removing javadoc/io" + #rm -r javadoc/io + + # remove the html files + #echo -e "Removing javadoc/*.html" + #rm javadoc/*.html + + # copy the new doc + #echo -e "Copying to javadoc/" + #yes | cp -rf ./build/docs/javadoc/ . + + # ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + # enable once 3.x is mainstream + # !!!!!!!!!!!!!!!!!!!!!!!!!!!!! + + # 2.) 3.x javadoc + # remove the io subdir + echo -e "Removing 3.x/javadoc/io" + rm -r 3.x/javadoc/io + + # remove the html files + echo -e "Removing 3.x/javadoc/*.html" + rm 3.x/javadoc/*.html + + # copy the new doc + echo -e "Copying to 3.x/javadoc/" + yes | cp -rf ./build/docs/javadoc/ 3.x/ +fi + +# 3.) create a version/snapshot specific copy of the docs +# clear the existing tag +echo -e "Removing to 3.x/javadoc/${buildTag}" +rm -r 3.x/javadoc/${buildTag} + +# copy the new doc +echo -e "Copying to 3.x/javadoc/${buildTag}" +yes | cp -rf ./build/docs/javadoc/ 3.x/javadoc/${buildTag}/ + + +# stage all changed and new files +echo -e "Staging new files" +git add *.html +git add *.css +git add *.js +git add *package-list* + +# remove tracked but deleted files +echo -e "Removing deleted files" +git add -u + +# commit all +echo -e "commit CI build: $CI_BUILD_NUMBER for $buildTag" +git commit --message "CI build: $CI_BUILD_NUMBER for $buildTag" + +# debug file list +#find -name "*.html" + +# push it +echo -e "Pushing back changes." +git push --quiet --set-upstream origin-pages gh-pages + + +# we are done +echo -e "JavaDocs pushback complete." \ No newline at end of file diff --git a/rxjava-core/build.gradle b/rxjava-core/build.gradle deleted file mode 100644 index 499cae4e75..0000000000 --- a/rxjava-core/build.gradle +++ /dev/null @@ -1,38 +0,0 @@ -apply plugin: 'java' -apply plugin: 'eclipse' -apply plugin: 'idea' - -// we want to target Java 1.5 so this can be used on Android -sourceCompatibility = JavaVersion.VERSION_1_6 -targetCompatibility = JavaVersion.VERSION_1_5 - -dependencies { - compile 'org.slf4j:slf4j-api:1.7.0' - compile 'com.google.code.findbugs:jsr305:2.0.0' - provided 'junit:junit:4.10' - provided 'org.mockito:mockito-core:1.8.5' -} - -eclipse { - classpath { - //you can tweak the classpath of the Eclipse project by adding extra configurations: - plusConfigurations += configurations.provided - - downloadSources = true - downloadJavadoc = true - } -} - -javadoc { - // we do not want the org.rx.operations package include - exclude '**/operations/**' - - options { - doclet = "org.benjchristensen.doclet.DocletExclude" - docletpath = [rootProject.file('./gradle/doclet-exclude.jar')] - stylesheetFile = rootProject.file('./gradle/javadocStyleSheet.css') - windowTitle = "RxJava Javadoc ${project.version}" - } - options.addStringOption('top').value = '<h2 class="title" style="padding-top:40px">RxJava</h2>' -} - diff --git a/rxjava-core/src/main/java/rx/Notification.java b/rxjava-core/src/main/java/rx/Notification.java deleted file mode 100644 index 08c6113ec6..0000000000 --- a/rxjava-core/src/main/java/rx/Notification.java +++ /dev/null @@ -1,161 +0,0 @@ -/** - * Copyright 2013 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package rx; - -/** - * An object representing a notification sent to an {@link Observable}. - * - * For the Microsoft Rx equivalent see: http://msdn.microsoft.com/en-us/library/hh229462(v=vs.103).aspx - */ -public class Notification<T> { - - private final Kind kind; - private final Exception exception; - private final T value; - - /** - * A constructor used to represent an onNext notification. - * - * @param value - * The data passed to the onNext method. - */ - public Notification(T value) { - this.value = value; - this.exception = null; - this.kind = Kind.OnNext; - } - - /** - * A constructor used to represent an onError notification. - * - * @param exception - * The exception passed to the onError notification. - */ - public Notification(Exception exception) { - this.exception = exception; - this.value = null; - this.kind = Kind.OnError; - } - - /** - * A constructor used to represent an onCompleted notification. - */ - public Notification() { - this.exception = null; - this.value = null; - this.kind = Kind.OnCompleted; - } - - /** - * Retrieves the exception associated with an onError notification. - * - * @return The exception associated with an onError notification. - */ - public Exception getException() { - return exception; - } - - /** - * Retrieves the data associated with an onNext notification. - * - * @return The data associated with an onNext notification. - */ - public T getValue() { - return value; - } - - /** - * Retrieves a value indicating whether this notification has a value. - * - * @return a value indicating whether this notification has a value. - */ - public boolean hasValue() { - return isOnNext() && value != null; - } - - /** - * Retrieves a value indicating whether this notification has an exception. - * - * @return a value indicating whether this notification has an exception. - */ - public boolean hasException() { - return isOnError() && exception != null; - } - - /** - * The kind of notification: OnNext, OnError, OnCompleted - * - * @return - */ - public Kind getKind() { - return kind; - } - - public boolean isOnError() { - return getKind() == Kind.OnError; - } - - public boolean isOnCompleted() { - return getKind() == Kind.OnCompleted; - } - - public boolean isOnNext() { - return getKind() == Kind.OnNext; - } - - public static enum Kind { - OnNext, OnError, OnCompleted - } - - @Override - public String toString() { - StringBuilder str = new StringBuilder("[").append(super.toString()).append(" ").append(getKind()); - if (hasValue()) - str.append(" ").append(getValue()); - if (hasException()) - str.append(" ").append(getException().getMessage()); - str.append("]"); - return str.toString(); - } - - @Override - public int hashCode() { - int hash = getKind().hashCode(); - if (hasValue()) - hash = hash * 31 + getValue().hashCode(); - if (hasException()) - hash = hash * 31 + getException().hashCode(); - return hash; - } - - @Override - public boolean equals(Object obj) { - if (obj == null) - return false; - if (this == obj) - return true; - if (obj.getClass() != getClass()) - return false; - Notification<?> notification = (Notification<?>) obj; - if (notification.getKind() != getKind()) - return false; - if (hasValue() && !getValue().equals(notification.getValue())) - return false; - if (hasException() && !getException().equals(notification.getException())) - return false; - return true; - } -} diff --git a/rxjava-core/src/main/java/rx/Observable.java b/rxjava-core/src/main/java/rx/Observable.java deleted file mode 100644 index 0fd95c0068..0000000000 --- a/rxjava-core/src/main/java/rx/Observable.java +++ /dev/null @@ -1,2349 +0,0 @@ -/** - * Copyright 2013 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package rx; - -import static org.mockito.Matchers.*; -import static org.mockito.Mockito.*; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.Map; -import java.util.concurrent.Future; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; - -import org.junit.Before; -import org.junit.Test; -import org.mockito.Mock; -import org.mockito.Mockito; -import org.mockito.MockitoAnnotations; - -import rx.operators.OperationConcat; -import rx.operators.OperationFilter; -import rx.operators.OperationLast; -import rx.operators.OperationMap; -import rx.operators.OperationMaterialize; -import rx.operators.OperationMerge; -import rx.operators.OperationMergeDelayError; -import rx.operators.OperationOnErrorResumeNextViaFunction; -import rx.operators.OperationOnErrorResumeNextViaObservable; -import rx.operators.OperationOnErrorReturn; -import rx.operators.OperationScan; -import rx.operators.OperationSkip; -import rx.operators.OperationSynchronize; -import rx.operators.OperationTake; -import rx.operators.OperationToObservableFuture; -import rx.operators.OperationToObservableIterable; -import rx.operators.OperationToObservableList; -import rx.operators.OperationToObservableSortedList; -import rx.operators.OperationZip; -import rx.util.AtomicObservableSubscription; -import rx.util.AtomicObserver; -import rx.util.functions.Action0; -import rx.util.functions.Action1; -import rx.util.functions.Func1; -import rx.util.functions.Func2; -import rx.util.functions.Func3; -import rx.util.functions.Func4; -import rx.util.functions.FuncN; -import rx.util.functions.FunctionLanguageAdaptor; -import rx.util.functions.Functions; - -/** - * The Observable interface that implements the Reactive Pattern. - * <p> - * It provides overloaded methods for subscribing as well as delegate methods to the various operators. - * <p> - * The documentation for this interface makes use of marble diagrams. The following legend explains - * these diagrams: - * <p> - * <img width="640" src="https://github.com/Netflix/RxJava/wiki/images/rx-operators/legend.png"> - * <p> - * For more information see the <a href="https://github.com/Netflix/RxJava/wiki/Observable">RxJava Wiki</a> - * - * @param <T> - */ -public class Observable<T> { - - private final Func1<Observer<T>, Subscription> onSubscribe; - private final boolean isTrusted; - - protected Observable(Func1<Observer<T>, Subscription> onSubscribe) { - this(onSubscribe, false); - } - - protected Observable() { - this(null, false); - } - - private Observable(Func1<Observer<T>, Subscription> onSubscribe, boolean isTrusted) { - this.onSubscribe = onSubscribe; - this.isTrusted = isTrusted; - } - - /** - * an {@link Observer} must call an Observable's <code>subscribe</code> method in order to register itself - * to receive push-based notifications from the Observable. A typical implementation of the - * <code>subscribe</code> method does the following: - * <p> - * It stores a reference to the Observer in a collection object, such as a <code>List<T></code> - * object. - * <p> - * It returns a reference to the {@link Subscription} interface. This enables - * Observers to unsubscribe (that is, to stop receiving notifications) before the Observable has - * finished sending them and has called the Observer's {@link Observer#onCompleted()} method. - * <p> - * At any given time, a particular instance of an <code>Observable<T></code> implementation is - * responsible for accepting all subscriptions and notifying all subscribers. Unless the - * documentation for a particular <code>Observable<T></code> implementation indicates otherwise, - * Observers should make no assumptions about the <code>Observable<T></code> implementation, such - * as the order of notifications that multiple Observers will receive. - * <p> - * For more information see the <a href="https://github.com/Netflix/RxJava/wiki/Observable">RxJava Wiki</a> - * - * - * @param Observer - * @return a {@link Subscription} reference that allows observers - * to stop receiving notifications before the provider has finished sending them - */ - public Subscription subscribe(Observer<T> observer) { - if (onSubscribe == null) { - throw new IllegalStateException("onSubscribe function can not be null."); - // the subscribe function can also be overridden but generally that's not the appropriate approach so I won't mention that in the exception - } - if (isTrusted) { - return onSubscribe.call(observer); - } else { - AtomicObservableSubscription subscription = new AtomicObservableSubscription(); - return subscription.wrap(onSubscribe.call(new AtomicObserver<T>(subscription, observer))); - } - } - - @SuppressWarnings({ "rawtypes", "unchecked" }) - public Subscription subscribe(final Map<String, Object> callbacks) { - // lookup and memoize onNext - Object _onNext = callbacks.get("onNext"); - if (_onNext == null) { - throw new RuntimeException("onNext must be implemented"); - } - final FuncN onNext = Functions.from(_onNext); - - return subscribe(new Observer() { - - public void onCompleted() { - Object onComplete = callbacks.get("onCompleted"); - if (onComplete != null) { - Functions.from(onComplete).call(); - } - } - - public void onError(Exception e) { - handleError(e); - Object onError = callbacks.get("onError"); - if (onError != null) { - Functions.from(onError).call(e); - } - } - - public void onNext(Object args) { - onNext.call(args); - } - - }); - } - - @SuppressWarnings({ "rawtypes", "unchecked" }) - public Subscription subscribe(final Object o) { - if (o instanceof Observer) { - // in case a dynamic language is not correctly handling the overloaded methods and we receive an Observer just forward to the correct method. - return subscribe((Observer) o); - } - - // lookup and memoize onNext - if (o == null) { - throw new RuntimeException("onNext must be implemented"); - } - final FuncN onNext = Functions.from(o); - - return subscribe(new Observer() { - - public void onCompleted() { - // do nothing - } - - public void onError(Exception e) { - handleError(e); - // no callback defined - } - - public void onNext(Object args) { - onNext.call(args); - } - - }); - } - - public Subscription subscribe(final Action1<T> onNext) { - - return subscribe(new Observer<T>() { - - public void onCompleted() { - // do nothing - } - - public void onError(Exception e) { - handleError(e); - // no callback defined - } - - public void onNext(T args) { - if (onNext == null) { - throw new RuntimeException("onNext must be implemented"); - } - onNext.call(args); - } - - }); - } - - @SuppressWarnings({ "rawtypes", "unchecked" }) - public Subscription subscribe(final Object onNext, final Object onError) { - // lookup and memoize onNext - if (onNext == null) { - throw new RuntimeException("onNext must be implemented"); - } - final FuncN onNextFunction = Functions.from(onNext); - - return subscribe(new Observer() { - - public void onCompleted() { - // do nothing - } - - public void onError(Exception e) { - handleError(e); - if (onError != null) { - Functions.from(onError).call(e); - } - } - - public void onNext(Object args) { - onNextFunction.call(args); - } - - }); - } - - public Subscription subscribe(final Action1<T> onNext, final Action1<Exception> onError) { - - return subscribe(new Observer<T>() { - - public void onCompleted() { - // do nothing - } - - public void onError(Exception e) { - handleError(e); - if (onError != null) { - onError.call(e); - } - } - - public void onNext(T args) { - if (onNext == null) { - throw new RuntimeException("onNext must be implemented"); - } - onNext.call(args); - } - - }); - } - - @SuppressWarnings({ "rawtypes", "unchecked" }) - public Subscription subscribe(final Object onNext, final Object onError, final Object onComplete) { - // lookup and memoize onNext - if (onNext == null) { - throw new RuntimeException("onNext must be implemented"); - } - final FuncN onNextFunction = Functions.from(onNext); - - return subscribe(new Observer() { - - public void onCompleted() { - if (onComplete != null) { - Functions.from(onComplete).call(); - } - } - - public void onError(Exception e) { - handleError(e); - if (onError != null) { - Functions.from(onError).call(e); - } - } - - public void onNext(Object args) { - onNextFunction.call(args); - } - - }); - } - - public Subscription subscribe(final Action1<T> onNext, final Action1<Exception> onError, final Action0 onComplete) { - - return subscribe(new Observer<T>() { - - public void onCompleted() { - onComplete.call(); - } - - public void onError(Exception e) { - handleError(e); - if (onError != null) { - onError.call(e); - } - } - - public void onNext(T args) { - if (onNext == null) { - throw new RuntimeException("onNext must be implemented"); - } - onNext.call(args); - } - - }); - } - - private void handleError(Exception e) { - // not implemented yet since open-sourcing - // intended for plugins to capture and log all errors - // even if Observers drop them on the floor - } - - /** - * An Observable that never sends any information to an {@link Observer}. - * - * This Observable is useful primarily for testing purposes. - * - * @param <T> - * the type of item emitted by the Observable - */ - private static class NeverObservable<T> extends Observable<T> { - public NeverObservable() { - super(new Func1<Observer<T>, Subscription>() { - - @Override - public Subscription call(Observer<T> t1) { - return new NoOpObservableSubscription(); - } - - }); - } - } - - /** - * A {@link Subscription} that does nothing when its unsubscribe method is called. - */ - private static class NoOpObservableSubscription implements Subscription { - public void unsubscribe() { - } - } - - /** - * an Observable that calls {@link Observer#onError(Exception)} when the Observer subscribes. - * - * @param <T> - * the type of object returned by the Observable - */ - private static class ThrowObservable<T> extends Observable<T> { - - public ThrowObservable(final Exception exception) { - super(new Func1<Observer<T>, Subscription>() { - - /** - * Accepts an {@link Observer} and calls its <code>onError</code> method. - * - * @param observer - * an {@link Observer} of this Observable - * @return a reference to the subscription - */ - @Override - public Subscription call(Observer<T> observer) { - observer.onError(exception); - return new NoOpObservableSubscription(); - } - - }); - } - - } - - /** - * Creates an Observable that will execute the given function when a {@link Observer} subscribes to it. - * <p> - * Write the function you pass to <code>create</code> so that it behaves as an Observable - calling the passed-in - * <code>onNext</code>, <code>onError</code>, and <code>onCompleted</code> methods appropriately. - * <p> - * A well-formed Observable must call either the {@link Observer}'s <code>onCompleted</code> method exactly once or its <code>onError</code> method exactly once. - * <p> - * See <a href="http://go.microsoft.com/fwlink/?LinkID=205219">Rx Design Guidelines (PDF)</a> for detailed information. - * - * @param <T> - * the type emitted by the Observable sequence - * @param func - * a function that accepts an <code>Observer<T></code> and calls its <code>onNext</code>, <code>onError</code>, and <code>onCompleted</code> methods - * as appropriate, and returns a {@link Subscription} to allow canceling the subscription (if applicable) - * @return an Observable that, when an {@link Observer} subscribes to it, will execute the given function - */ - public static <T> Observable<T> create(Func1<Observer<T>, Subscription> func) { - return new Observable<T>(func); - } - - /* - * Private version that creates a 'trusted' Observable to allow performance optimizations. - */ - private static <T> Observable<T> _create(Func1<Observer<T>, Subscription> func) { - return new Observable<T>(func, true); - } - - /** - * Creates an Observable that will execute the given function when a {@link Observer} subscribes to it. - * <p> - * This method accept {@link Object} to allow different languages to pass in closures using {@link FunctionLanguageAdaptor}. - * <p> - * Write the function you pass to <code>create</code> so that it behaves as an Observable - calling the passed-in - * <code>onNext</code>, <code>onError</code>, and <code>onCompleted</code> methods appropriately. - * <p> - * A well-formed Observable must call either the {@link Observer}'s <code>onCompleted</code> method exactly once or its <code>onError</code> method exactly once. - * <p> - * See <a href="http://go.microsoft.com/fwlink/?LinkID=205219">Rx Design Guidelines (PDF)</a> for detailed information. - * - * @param <T> - * the type emitted by the Observable sequence - * @param func - * a function that accepts an <code>Observer<T></code> and calls its <code>onNext</code>, <code>onError</code>, and <code>onCompleted</code> methods - * as appropriate, and returns a {@link Subscription} to allow canceling the subscription (if applicable) - * @return an Observable that, when an {@link Observer} subscribes to it, will execute the given function - */ - public static <T> Observable<T> create(final Object callback) { - @SuppressWarnings("rawtypes") - final FuncN _f = Functions.from(callback); - return create(new Func1<Observer<T>, Subscription>() { - - @Override - public Subscription call(Observer<T> t1) { - return (Subscription) _f.call(t1); - } - - }); - } - - /** - * Returns an Observable that returns no data to the {@link Observer} and immediately invokes its <code>onCompleted</code> method. - * <p> - * <img width="640" src="https://raw.github.com/wiki/Netflix/RxJava/images/rx-operators/empty.png"> - * - * @param <T> - * the type of item emitted by the Observable - * @return an Observable that returns no data to the {@link Observer} and immediately invokes the {@link Observer}'s <code>onCompleted</code> method - */ - public static <T> Observable<T> empty() { - return toObservable(new ArrayList<T>()); - } - - /** - * Returns an Observable that calls <code>onError</code> when an {@link Observer} subscribes to it. - * <p> - * - * @param exception - * the error to throw - * @param <T> - * the type of object returned by the Observable - * @return an Observable object that calls <code>onError</code> when an {@link Observer} subscribes - */ - public static <T> Observable<T> error(Exception exception) { - return new ThrowObservable<T>(exception); - } - - /** - * Filters an Observable by discarding any of its emissions that do not meet some test. - * <p> - * <img width="640" src="https://raw.github.com/wiki/Netflix/RxJava/images/rx-operators/filter.png"> - * - * @param that - * the Observable to filter - * @param predicate - * a function that evaluates the items emitted by the source Observable, returning <code>true</code> if they pass the filter - * @return an Observable that emits only those items in the original Observable that the filter evaluates as true - */ - public static <T> Observable<T> filter(Observable<T> that, Func1<T, Boolean> predicate) { - return _create(OperationFilter.filter(that, predicate)); - } - - /** - * Filters an Observable by discarding any of its emissions that do not meet some test. - * <p> - * <img width="640" src="https://raw.github.com/wiki/Netflix/RxJava/images/rx-operators/filter.png"> - * - * @param that - * the Observable to filter - * @param predicate - * a function that evaluates the items emitted by the source Observable, returning <code>true</code> if they pass the filter - * @return an Observable that emits only those items in the original Observable that the filter evaluates as true - */ - public static <T> Observable<T> filter(Observable<T> that, final Object function) { - @SuppressWarnings("rawtypes") - final FuncN _f = Functions.from(function); - return filter(that, new Func1<T, Boolean>() { - - @Override - public Boolean call(T t1) { - return (Boolean) _f.call(t1); - - } - - }); - } - - /** - * Converts an {@link Iterable} sequence to an Observable sequence. - * - * @param iterable - * the source {@link Iterable} sequence - * @param <T> - * the type of items in the {@link Iterable} sequence and the type emitted by the resulting Observable - * @return an Observable that emits each item in the source {@link Iterable} sequence - * @see {@link #toObservable(Iterable)} - */ - public static <T> Observable<T> from(Iterable<T> iterable) { - return toObservable(iterable); - } - - /** - * Converts an Array to an Observable sequence. - * - * @param iterable - * the source Array - * @param <T> - * the type of items in the Array, and the type of items emitted by the resulting Observable - * @return an Observable that emits each item in the source Array - * @see {@link #toObservable(Object...)} - */ - public static <T> Observable<T> from(T... items) { - return toObservable(items); - } - - /** - * Returns an Observable that notifies an {@link Observer} of a single value and then completes. - * <p> - * To convert any object into an Observable that emits that object, pass that object into the <code>just</code> method. - * <p> - * This is similar to the {@link toObservable} method, except that <code>toObservable</code> will convert - * an {@link Iterable} object into an Observable that emits each of the items in the {@link Iterable}, one - * at a time, while the <code>just</code> method would convert the {@link Iterable} into an Observable - * that emits the entire {@link Iterable} as a single item. - * <p> - * <img width="640" src="https://raw.github.com/wiki/Netflix/RxJava/images/rx-operators/just.png"> - * - * @param value - * the value to pass to the Observer's <code>onNext</code> method - * @param <T> - * the type of the value - * @return an Observable that notifies an {@link Observer} of a single value and then completes - */ - public static <T> Observable<T> just(T value) { - List<T> list = new ArrayList<T>(); - list.add(value); - - return toObservable(list); - } - - /** - * Takes the last item emitted by a source Observable and returns an Observable that emits only - * that item as its sole emission. - * <p> - * <img width="640" src="https://raw.github.com/wiki/Netflix/RxJava/images/rx-operators/last.png"> - * - * @param that - * the source Observable - * @return an Observable that emits a single item, which is identical to the last item emitted - * by the source Observable - */ - public static <T> Observable<T> last(final Observable<T> that) { - return _create(OperationLast.last(that)); - } - - /** - * Applies a function of your choosing to every notification emitted by an Observable, and returns - * this transformation as a new Observable sequence. - * <p> - * <img width="640" src="https://raw.github.com/wiki/Netflix/RxJava/images/rx-operators/map.png"> - * - * @param sequence - * the source Observable - * @param func - * a function to apply to each item in the sequence emitted by the source Observable - * @param <T> - * the type of items emitted by the the source Observable - * @param <R> - * the type of items returned by map function - * @return an Observable that is the result of applying the transformation function to each item - * in the sequence emitted by the source Observable - */ - public static <T, R> Observable<R> map(Observable<T> sequence, Func1<T, R> func) { - return _create(OperationMap.map(sequence, func)); - } - - /** - * Applies a function of your choosing to every notification emitted by an Observable, and returns - * this transformation as a new Observable sequence. - * <p> - * <img width="640" src="https://raw.github.com/wiki/Netflix/RxJava/images/rx-operators/map.png"> - * - * @param sequence - * the source Observable - * @param func - * a function to apply to each item in the sequence emitted by the source Observable - * @param <T> - * the type of items emitted by the the source Observable - * @param <R> - * the type of items returned by map function - * @return an Observable that is the result of applying the transformation function to each item - * in the sequence emitted by the source Observable - */ - public static <T, R> Observable<R> map(Observable<T> sequence, final Object function) { - @SuppressWarnings("rawtypes") - final FuncN _f = Functions.from(function); - return map(sequence, new Func1<T, R>() { - - @SuppressWarnings("unchecked") - @Override - public R call(T t1) { - return (R) _f.call(t1); - } - - }); - } - - /** - * Creates a new Observable sequence by applying a function that you supply to each object in the - * original Observable sequence, where that function is itself an Observable that emits objects, - * and then merges the results of that function applied to every item emitted by the original - * Observable, emitting these merged results as its own sequence. - * <p> - * <img width="640" src="https://raw.github.com/wiki/Netflix/RxJava/images/rx-operators/mapMany.png"> - * - * @param sequence - * the source Observable - * @param func - * a function to apply to each item emitted by the source Observable, generating a - * Observable - * @param <T> - * the type emitted by the source Observable - * @param <R> - * the type emitted by the Observables emitted by <code>func</code> - * @return an Observable that emits a sequence that is the result of applying the transformation - * function to each item emitted by the source Observable and merging the results of - * the Observables obtained from this transformation - */ - public static <T, R> Observable<R> mapMany(Observable<T> sequence, Func1<T, Observable<R>> func) { - return _create(OperationMap.mapMany(sequence, func)); - } - - /** - * Creates a new Observable sequence by applying a function that you supply to each object in the - * original Observable sequence, where that function is itself an Observable that emits objects, - * and then merges the results of that function applied to every item emitted by the original - * Observable, emitting these merged results as its own sequence. - * <p> - * <img width="640" src="https://raw.github.com/wiki/Netflix/RxJava/images/rx-operators/mapMany.png"> - * - * @param sequence - * the source Observable - * @param func - * a function to apply to each item emitted by the source Observable, generating a - * Observable - * @param <T> - * the type emitted by the source Observable - * @param <R> - * the type emitted by the Observables emitted by <code>func</code> - * @return an Observable that emits a sequence that is the result of applying the transformation - * function to each item emitted by the source Observable and merging the results of - * the Observables obtained from this transformation - */ - public static <T, R> Observable<R> mapMany(Observable<T> sequence, final Object function) { - @SuppressWarnings("rawtypes") - final FuncN _f = Functions.from(function); - return mapMany(sequence, new Func1<T, R>() { - - @SuppressWarnings("unchecked") - @Override - public R call(T t1) { - return (R) _f.call(t1); - } - - }); - } - - /** - * Materializes the implicit notifications of an observable sequence as explicit notification values. - * <p> - * <img width="640" src="https://raw.github.com/wiki/Netflix/RxJava/images/rx-operators/materialize.png"> - * - * @param source - * An observable sequence of elements to project. - * @return An observable sequence whose elements are the result of materializing the notifications of the given sequence. - * @see http://msdn.microsoft.com/en-us/library/hh229453(v=VS.103).aspx - */ - public static <T> Observable<Notification<T>> materialize(final Observable<T> sequence) { - return _create(OperationMaterialize.materialize(sequence)); - } - - /** - * Flattens the Observable sequences from a list of Observables into one Observable sequence - * without any transformation. You can combine the output of multiple Observables so that they - * act like a single Observable, by using the <code>merge</code> method. - * <p> - * <img width="640" src="https://raw.github.com/wiki/Netflix/RxJava/images/rx-operators/merge.png"> - * - * @param source - * a list of Observables that emit sequences of items - * @return an Observable that emits a sequence of elements that are the result of flattening the - * output from the <code>source</code> list of Observables - * @see <a href="http://msdn.microsoft.com/en-us/library/hh229099(v=vs.103).aspx">MSDN: Observable.Merge Method</a> - */ - public static <T> Observable<T> merge(List<Observable<T>> source) { - return _create(OperationMerge.merge(source)); - } - - /** - * Flattens the Observable sequences emitted by a sequence of Observables that are emitted by a - * Observable into one Observable sequence without any transformation. You can combine the output - * of multiple Observables so that they act like a single Observable, by using the <code>merge</code> method. - * <p> - * <img width="640" src="https://raw.github.com/wiki/Netflix/RxJava/images/rx-operators/merge.png"> - * - * @param source - * an Observable that emits Observables - * @return an Observable that emits a sequence of elements that are the result of flattening the - * output from the Observables emitted by the <code>source</code> Observable - * @see <a href="http://msdn.microsoft.com/en-us/library/hh229099(v=vs.103).aspx">MSDN: Observable.Merge Method</a> - */ - public static <T> Observable<T> merge(Observable<Observable<T>> source) { - return _create(OperationMerge.merge(source)); - } - - /** - * Flattens the Observable sequences from a series of Observables into one Observable sequence - * without any transformation. You can combine the output of multiple Observables so that they - * act like a single Observable, by using the <code>merge</code> method. - * <p> - * <img width="640" src="https://raw.github.com/wiki/Netflix/RxJava/images/rx-operators/merge.png"> - * - * @param source - * a series of Observables that emit sequences of items - * @return an Observable that emits a sequence of elements that are the result of flattening the - * output from the <code>source</code> Observables - * @see <a href="http://msdn.microsoft.com/en-us/library/hh229099(v=vs.103).aspx">MSDN: Observable.Merge Method</a> - */ - public static <T> Observable<T> merge(Observable<T>... source) { - return _create(OperationMerge.merge(source)); - } - - /** - * Combines the objects emitted by two or more Observables, and emits the result as a single Observable, - * by using the <code>concat</code> method. - * <p> - * <img width="640" src="https://raw.github.com/wiki/Netflix/RxJava/images/rx-operators/concat.png"> - * - * @param source - * a series of Observables that emit sequences of items - * @return an Observable that emits a sequence of elements that are the result of combining the - * output from the <code>source</code> Observables - * @see <a href="http://msdn.microsoft.com/en-us/library/system.reactive.linq.observable.concat(v=vs.103).aspx">MSDN: Observable.Concat Method</a> - */ - public static <T> Observable<T> concat(Observable<T>... source) { - return _create(OperationConcat.concat(source)); - } - - /** - * Same functionality as <code>merge</code> except that errors received to onError will be held until all sequences have finished (onComplete/onError) before sending the error. - * <p> - * Only the first onError received will be sent. - * <p> - * This enables receiving all successes from merged sequences without one onError from one sequence causing all onNext calls to be prevented. - * <p> - * <img width="640" src="https://raw.github.com/wiki/Netflix/RxJava/images/rx-operators/mergeDelayError.png"> - * - * @param source - * a list of Observables that emit sequences of items - * @return an Observable that emits a sequence of elements that are the result of flattening the - * output from the <code>source</code> list of Observables - * @see <a href="http://msdn.microsoft.com/en-us/library/hh229099(v=vs.103).aspx">MSDN: Observable.Merge Method</a> - */ - public static <T> Observable<T> mergeDelayError(List<Observable<T>> source) { - return _create(OperationMergeDelayError.mergeDelayError(source)); - } - - /** - * Same functionality as <code>merge</code> except that errors received to onError will be held until all sequences have finished (onComplete/onError) before sending the error. - * <p> - * Only the first onError received will be sent. - * <p> - * This enables receiving all successes from merged sequences without one onError from one sequence causing all onNext calls to be prevented. - * <p> - * <img width="640" src="https://raw.github.com/wiki/Netflix/RxJava/images/rx-operators/mergeDelayError.png"> - * - * @param source - * an Observable that emits Observables - * @return an Observable that emits a sequence of elements that are the result of flattening the - * output from the Observables emitted by the <code>source</code> Observable - * @see <a href="http://msdn.microsoft.com/en-us/library/hh229099(v=vs.103).aspx">MSDN: Observable.Merge Method</a> - */ - public static <T> Observable<T> mergeDelayError(Observable<Observable<T>> source) { - return _create(OperationMergeDelayError.mergeDelayError(source)); - } - - /** - * Same functionality as <code>merge</code> except that errors received to onError will be held until all sequences have finished (onComplete/onError) before sending the error. - * <p> - * Only the first onError received will be sent. - * <p> - * This enables receiving all successes from merged sequences without one onError from one sequence causing all onNext calls to be prevented. - * <p> - * <img width="640" src="https://raw.github.com/wiki/Netflix/RxJava/images/rx-operators/mergeDelayError.png"> - * - * @param source - * a series of Observables that emit sequences of items - * @return an Observable that emits a sequence of elements that are the result of flattening the - * output from the <code>source</code> Observables - * @see <a href="http://msdn.microsoft.com/en-us/library/hh229099(v=vs.103).aspx">MSDN: Observable.Merge Method</a> - */ - public static <T> Observable<T> mergeDelayError(Observable<T>... source) { - return _create(OperationMergeDelayError.mergeDelayError(source)); - } - - /** - * Returns an Observable that never sends any information to an {@link Observer}. - * - * This observable is useful primarily for testing purposes. - * - * @param <T> - * the type of item (not) emitted by the Observable - * @return an Observable that never sends any information to an {@link Observer} - */ - public static <T> Observable<T> never() { - return new NeverObservable<T>(); - } - - /** - * A {@link Subscription} that does nothing. - * - * //TODO should this be moved to a Subscriptions utility class? - * - * @return - */ - public static Subscription noOpSubscription() { - return new NoOpObservableSubscription(); - } - - /** - * A {@link Subscription} implemented via a Func - * - * //TODO should this be moved to a Subscriptions utility class? - * - * @return - */ - public static Subscription createSubscription(final Action0 unsubscribe) { - return new Subscription() { - - @Override - public void unsubscribe() { - unsubscribe.call(); - } - - }; - } - - /** - * A {@link Subscription} implemented via an anonymous function (such as closures from other languages). - * - * //TODO should this be moved to a Subscriptions utility class? - * - * @return - */ - public static Subscription createSubscription(final Object unsubscribe) { - final FuncN<?> f = Functions.from(unsubscribe); - return new Subscription() { - - @Override - public void unsubscribe() { - f.call(); - } - - }; - } - - /** - * Instruct an Observable to pass control to another Observable (the return value of a function) - * rather than calling <code>onError</code> if it encounters an error. - * <p> - * By default, when an Observable encounters an error that prevents it from emitting the expected item to its Observer, - * the Observable calls its {@link Observer}'s <code>onError</code> function, and then quits without calling any more - * of its {@link Observer}'s closures. The <code>onErrorResumeNext</code> method changes this behavior. If you pass a - * function that emits an Observable (<code>resumeFunction</code>) to an Observable's <code>onErrorResumeNext</code> method, - * if the original Observable encounters an error, instead of calling its {@link Observer}'s <code>onError</code> function, it - * will instead relinquish control to this new Observable, which will call the {@link Observer}'s <code>onNext</code> method if - * it is able to do so. In such a case, because no Observable necessarily invokes <code>onError</code>, the Observer may - * never know that an error happened. - * <p> - * You can use this to prevent errors from propagating or to supply fallback data should errors be encountered. - * <p> - * <img width="640" src="https://raw.github.com/wiki/Netflix/RxJava/images/rx-operators/onErrorResumeNext.png"> - * - * @param that - * the source Observable - * @param resumeFunction - * a function that returns an Observable that will take over if the source Observable - * encounters an error - * @return the source Observable, with its behavior modified as described - */ - public static <T> Observable<T> onErrorResumeNext(final Observable<T> that, final Func1<Exception, Observable<T>> resumeFunction) { - return _create(OperationOnErrorResumeNextViaFunction.onErrorResumeNextViaFunction(that, resumeFunction)); - } - - /** - * Instruct an Observable to pass control to another Observable (the return value of a function) - * rather than calling <code>onError</code> if it encounters an error. - * <p> - * By default, when an Observable encounters an error that prevents it from emitting the expected item to its Observer, - * the Observable calls its {@link Observer}'s <code>onError</code> function, and then quits without calling any more - * of its {@link Observer}'s closures. The <code>onErrorResumeNext</code> method changes this behavior. If you pass a - * function that emits an Observable (<code>resumeFunction</code>) to an Observable's <code>onErrorResumeNext</code> method, - * if the original Observable encounters an error, instead of calling its {@link Observer}'s <code>onError</code> function, it - * will instead relinquish control to this new Observable, which will call the {@link Observer}'s <code>onNext</code> method if - * it is able to do so. In such a case, because no Observable necessarily invokes <code>onError</code>, the Observer may - * never know that an error happened. - * <p> - * You can use this to prevent errors from propagating or to supply fallback data should errors be encountered. - * <p> - * <img width="640" src="https://raw.github.com/wiki/Netflix/RxJava/images/rx-operators/onErrorResumeNext.png"> - * - * @param that - * the source Observable - * @param resumeFunction - * a function that returns an Observable that will take over if the source Observable - * encounters an error - * @return the source Observable, with its behavior modified as described - */ - public static <T> Observable<T> onErrorResumeNext(final Observable<T> that, final Object resumeFunction) { - @SuppressWarnings("rawtypes") - final FuncN _f = Functions.from(resumeFunction); - return onErrorResumeNext(that, new Func1<Exception, Observable<T>>() { - - @SuppressWarnings("unchecked") - @Override - public Observable<T> call(Exception e) { - return (Observable<T>) _f.call(e); - } - }); - } - - /** - * Instruct an Observable to pass control to another Observable rather than calling <code>onError</code> if it encounters an error. - * <p> - * By default, when an Observable encounters an error that prevents it from emitting the expected item to its Observer, - * the Observable calls its {@link Observer}'s <code>onError</code> function, and then quits without calling any more - * of its {@link Observer}'s closures. The <code>onErrorResumeNext</code> method changes this behavior. If you pass a - * function that emits an Observable (<code>resumeFunction</code>) to an Observable's <code>onErrorResumeNext</code> method, - * if the original Observable encounters an error, instead of calling its {@link Observer}'s <code>onError</code> function, it - * will instead relinquish control to this new Observable, which will call the {@link Observer}'s <code>onNext</code> method if - * it is able to do so. In such a case, because no Observable necessarily invokes <code>onError</code>, the Observer may - * never know that an error happened. - * <p> - * You can use this to prevent errors from propagating or to supply fallback data should errors be encountered. - * <p> - * <img width="640" src="https://raw.github.com/wiki/Netflix/RxJava/images/rx-operators/onErrorResumeNext.png"> - * - * @param that - * the source Observable - * @param resumeFunction - * a function that returns an Observable that will take over if the source Observable - * encounters an error - * @return the source Observable, with its behavior modified as described - */ - public static <T> Observable<T> onErrorResumeNext(final Observable<T> that, final Observable<T> resumeSequence) { - return _create(OperationOnErrorResumeNextViaObservable.onErrorResumeNextViaObservable(that, resumeSequence)); - } - - /** - * Instruct an Observable to emit a particular item to its Observer's <code>onNext</code> function - * rather than calling <code>onError</code> if it encounters an error. - * <p> - * By default, when an Observable encounters an error that prevents it from emitting the expected item to its {@link Observer}, the Observable calls its {@link Observer}'s <code>onError</code> - * function, and then quits - * without calling any more of its {@link Observer}'s closures. The <code>onErrorReturn</code> method changes - * this behavior. If you pass a function (<code>resumeFunction</code>) to an Observable's <code>onErrorReturn</code> - * method, if the original Observable encounters an error, instead of calling its {@link Observer}'s - * <code>onError</code> function, it will instead pass the return value of <code>resumeFunction</code> to the {@link Observer}'s <code>onNext</code> method. - * <p> - * You can use this to prevent errors from propagating or to supply fallback data should errors be encountered. - * - * @param that - * the source Observable - * @param resumeFunction - * a function that returns a value that will be passed into an {@link Observer}'s <code>onNext</code> function if the Observable encounters an error that would - * otherwise cause it to call <code>onError</code> - * @return the source Observable, with its behavior modified as described - */ - public static <T> Observable<T> onErrorReturn(final Observable<T> that, Func1<Exception, T> resumeFunction) { - return _create(OperationOnErrorReturn.onErrorReturn(that, resumeFunction)); - } - - /** - * Returns an Observable that applies a function of your choosing to the first item emitted by a - * source Observable, then feeds the result of that function along with the second item emitted - * by an Observable into the same function, and so on until all items have been emitted by the - * source Observable, emitting the final result from the final call to your function as its sole - * output. - * <p> - * This technique, which is called "reduce" here, is sometimes called "fold," "accumulate," "compress," or "inject" in other programming contexts. Groovy, for instance, has an <code>inject</code> - * method that does a similar operation on lists. - * <p> - * <img width="640" src="https://raw.github.com/wiki/Netflix/RxJava/images/rx-operators/reduce.png"> - * - * @param <T> - * the type item emitted by the source Observable - * @param sequence - * the source Observable - * @param accumulator - * an accumulator function to be invoked on each element from the sequence, whose - * result will be used in the next accumulator call (if applicable) - * - * @return an Observable that emits a single element that is the result of accumulating the - * output from applying the accumulator to the sequence of items emitted by the source - * Observable - * @see <a href="http://msdn.microsoft.com/en-us/library/hh229154(v%3Dvs.103).aspx">MSDN: Observable.Aggregate</a> - * @see <a href="http://en.wikipedia.org/wiki/Fold_(higher-order_function)">Wikipedia: Fold (higher-order function)</a> - */ - public static <T> Observable<T> reduce(Observable<T> sequence, Func2<T, T, T> accumulator) { - return last(_create(OperationScan.scan(sequence, accumulator))); - } - - /** - * Returns an Observable that applies a function of your choosing to the first item emitted by a - * source Observable, then feeds the result of that function along with the second item emitted - * by an Observable into the same function, and so on until all items have been emitted by the - * source Observable, emitting the final result from the final call to your function as its sole - * output. - * <p> - * This technique, which is called "reduce" here, is sometimes called "fold," "accumulate," "compress," or "inject" in other programming contexts. Groovy, for instance, has an <code>inject</code> - * method that does a similar operation on lists. - * <p> - * <img width="640" src="https://raw.github.com/wiki/Netflix/RxJava/images/rx-operators/reduce.png"> - * - * @param <T> - * the type item emitted by the source Observable - * @param sequence - * the source Observable - * @param accumulator - * an accumulator function to be invoked on each element from the sequence, whose - * result will be used in the next accumulator call (if applicable) - * - * @return an Observable that emits a single element that is the result of accumulating the - * output from applying the accumulator to the sequence of items emitted by the source - * Observable - * @see <a href="http://msdn.microsoft.com/en-us/library/hh229154(v%3Dvs.103).aspx">MSDN: Observable.Aggregate</a> - * @see <a href="http://en.wikipedia.org/wiki/Fold_(higher-order_function)">Wikipedia: Fold (higher-order function)</a> - */ - public static <T> Observable<T> reduce(final Observable<T> sequence, final Object accumulator) { - @SuppressWarnings("rawtypes") - final FuncN _f = Functions.from(accumulator); - return reduce(sequence, new Func2<T, T, T>() { - - @SuppressWarnings("unchecked") - @Override - public T call(T t1, T t2) { - return (T) _f.call(t1, t2); - } - - }); - } - - /** - * Returns an Observable that applies a function of your choosing to the first item emitted by a - * source Observable, then feeds the result of that function along with the second item emitted - * by an Observable into the same function, and so on until all items have been emitted by the - * source Observable, emitting the final result from the final call to your function as its sole - * output. - * <p> - * This technique, which is called "reduce" here, is sometimes called "fold," "accumulate," "compress," or "inject" in other programming contexts. Groovy, for instance, has an <code>inject</code> - * method that does a similar operation on lists. - * <p> - * <img width="640" src="https://raw.github.com/wiki/Netflix/RxJava/images/rx-operators/reduce.png"> - * - * @param <T> - * the type item emitted by the source Observable - * @param sequence - * the source Observable - * @param initialValue - * a seed passed into the first execution of the accumulator function - * @param accumulator - * an accumulator function to be invoked on each element from the sequence, whose - * result will be used in the next accumulator call (if applicable) - * - * @return an Observable that emits a single element that is the result of accumulating the - * output from applying the accumulator to the sequence of items emitted by the source - * Observable - * @see <a href="http://msdn.microsoft.com/en-us/library/hh229154(v%3Dvs.103).aspx">MSDN: Observable.Aggregate</a> - * @see <a href="http://en.wikipedia.org/wiki/Fold_(higher-order_function)">Wikipedia: Fold (higher-order function)</a> - */ - public static <T> Observable<T> reduce(Observable<T> sequence, T initialValue, Func2<T, T, T> accumulator) { - return last(_create(OperationScan.scan(sequence, initialValue, accumulator))); - } - - /** - * Returns an Observable that applies a function of your choosing to the first item emitted by a - * source Observable, then feeds the result of that function along with the second item emitted - * by an Observable into the same function, and so on until all items have been emitted by the - * source Observable, emitting the final result from the final call to your function as its sole - * output. - * <p> - * This technique, which is called "reduce" here, is sometimes called "fold," "accumulate," "compress," or "inject" in other programming contexts. Groovy, for instance, has an <code>inject</code> - * method that does a similar operation on lists. - * <p> - * <img width="640" src="https://raw.github.com/wiki/Netflix/RxJava/images/rx-operators/reduce.png"> - * - * @param <T> - * the type item emitted by the source Observable - * @param sequence - * the source Observable - * @param initialValue - * a seed passed into the first execution of the accumulator function - * @param accumulator - * an accumulator function to be invoked on each element from the sequence, whose - * result will be used in the next accumulator call (if applicable) - * @return an Observable that emits a single element that is the result of accumulating the - * output from applying the accumulator to the sequence of items emitted by the source - * Observable - * @see <a href="http://msdn.microsoft.com/en-us/library/hh229154(v%3Dvs.103).aspx">MSDN: Observable.Aggregate</a> - * @see <a href="http://en.wikipedia.org/wiki/Fold_(higher-order_function)">Wikipedia: Fold (higher-order function)</a> - */ - public static <T> Observable<T> reduce(final Observable<T> sequence, final T initialValue, final Object accumulator) { - @SuppressWarnings("rawtypes") - final FuncN _f = Functions.from(accumulator); - return reduce(sequence, initialValue, new Func2<T, T, T>() { - - @SuppressWarnings("unchecked") - @Override - public T call(T t1, T t2) { - return (T) _f.call(t1, t2); - } - - }); - } - - /** - * Returns an Observable that applies a function of your choosing to the first item emitted by a - * source Observable, then feeds the result of that function along with the second item emitted - * by an Observable into the same function, and so on until all items have been emitted by the - * source Observable, emitting the result of each of these iterations as its own sequence. - * <p> - * <img width="640" src="https://raw.github.com/wiki/Netflix/RxJava/images/rx-operators/scan.png"> - * - * @param <T> - * the type item emitted by the source Observable - * @param sequence - * the source Observable - * @param accumulator - * an accumulator function to be invoked on each element from the sequence, whose - * result will be emitted and used in the next accumulator call (if applicable) - * @return an Observable that emits a sequence of items that are the result of accumulating the - * output from the sequence emitted by the source Observable - * @see <a href="http://msdn.microsoft.com/en-us/library/hh211665(v%3Dvs.103).aspx">MSDN: Observable.Scan</a> - */ - public static <T> Observable<T> scan(Observable<T> sequence, Func2<T, T, T> accumulator) { - return _create(OperationScan.scan(sequence, accumulator)); - } - - /** - * Returns an Observable that applies a function of your choosing to the first item emitted by a - * source Observable, then feeds the result of that function along with the second item emitted - * by an Observable into the same function, and so on until all items have been emitted by the - * source Observable, emitting the result of each of these iterations as its own sequence. - * <p> - * <img width="640" src="https://raw.github.com/wiki/Netflix/RxJava/images/rx-operators/scan.png"> - * - * @param <T> - * the type item emitted by the source Observable - * @param sequence - * the source Observable - * @param accumulator - * an accumulator function to be invoked on each element from the sequence, whose - * result will be emitted and used in the next accumulator call (if applicable) - * @return an Observable that emits a sequence of items that are the result of accumulating the - * output from the sequence emitted by the source Observable - * @see <a href="http://msdn.microsoft.com/en-us/library/hh211665(v%3Dvs.103).aspx">MSDN: Observable.Scan</a> - */ - public static <T> Observable<T> scan(final Observable<T> sequence, final Object accumulator) { - @SuppressWarnings("rawtypes") - final FuncN _f = Functions.from(accumulator); - return scan(sequence, new Func2<T, T, T>() { - - @SuppressWarnings("unchecked") - @Override - public T call(T t1, T t2) { - return (T) _f.call(t1, t2); - } - - }); - } - - /** - * Returns an Observable that applies a function of your choosing to the first item emitted by a - * source Observable, then feeds the result of that function along with the second item emitted - * by an Observable into the same function, and so on until all items have been emitted by the - * source Observable, emitting the result of each of these iterations as its own sequence. - * <p> - * <img width="640" src="https://raw.github.com/wiki/Netflix/RxJava/images/rx-operators/scan.png"> - * - * @param <T> - * the type item emitted by the source Observable - * @param sequence - * the source Observable - * @param initialValue - * the initial (seed) accumulator value - * @param accumulator - * an accumulator function to be invoked on each element from the sequence, whose - * result will be emitted and used in the next accumulator call (if applicable) - * @return an Observable that emits a sequence of items that are the result of accumulating the - * output from the sequence emitted by the source Observable - * @see <a href="http://msdn.microsoft.com/en-us/library/hh211665(v%3Dvs.103).aspx">MSDN: Observable.Scan</a> - */ - public static <T> Observable<T> scan(Observable<T> sequence, T initialValue, Func2<T, T, T> accumulator) { - return _create(OperationScan.scan(sequence, initialValue, accumulator)); - } - - /** - * Returns an Observable that applies a function of your choosing to the first item emitted by a - * source Observable, then feeds the result of that function along with the second item emitted - * by an Observable into the same function, and so on until all items have been emitted by the - * source Observable, emitting the result of each of these iterations as its own sequence. - * <p> - * <img width="640" src="https://raw.github.com/wiki/Netflix/RxJava/images/rx-operators/scan.png"> - * - * @param <T> - * the type item emitted by the source Observable - * @param sequence - * the source Observable - * @param initialValue - * the initial (seed) accumulator value - * @param accumulator - * an accumulator function to be invoked on each element from the sequence, whose - * result will be emitted and used in the next accumulator call (if applicable) - * @return an Observable that emits a sequence of items that are the result of accumulating the - * output from the sequence emitted by the source Observable - * @see <a href="http://msdn.microsoft.com/en-us/library/hh211665(v%3Dvs.103).aspx">MSDN: Observable.Scan</a> - */ - public static <T> Observable<T> scan(final Observable<T> sequence, final T initialValue, final Object accumulator) { - @SuppressWarnings("rawtypes") - final FuncN _f = Functions.from(accumulator); - return scan(sequence, initialValue, new Func2<T, T, T>() { - - @SuppressWarnings("unchecked") - @Override - public T call(T t1, T t2) { - return (T) _f.call(t1, t2); - } - - }); - } - - /** - * Returns an Observable that skips the first <code>num</code> items emitted by the source - * Observable. You can ignore the first <code>num</code> items emitted by an Observable and attend - * only to those items that come after, by modifying the Observable with the <code>skip</code> method. - * <p> - * <img width="640" src="https://raw.github.com/wiki/Netflix/RxJava/images/rx-operators/skip.png"> - * - * @param items - * the source Observable - * @param num - * the number of items to skip - * @return an Observable that emits the same sequence of items emitted by the source Observable, - * except for the first <code>num</code> items - * @see <a href="http://msdn.microsoft.com/en-us/library/hh229847(v=vs.103).aspx">MSDN: Observable.Skip Method</a> - */ - public static <T> Observable<T> skip(final Observable<T> items, int num) { - return _create(OperationSkip.skip(items, num)); - } - - /** - * Accepts an Observable and wraps it in another Observable that ensures that the resulting - * Observable is chronologically well-behaved. - * <p> - * A well-behaved observable ensures <code>onNext</code>, <code>onCompleted</code>, or <code>onError</code> calls to its subscribers are not interleaved, <code>onCompleted</code> and - * <code>onError</code> are only called once respectively, and no - * <code>onNext</code> calls follow <code>onCompleted</code> and <code>onError</code> calls. - * - * @param observable - * the source Observable - * @param <T> - * the type of item emitted by the source Observable - * @return an Observable that is a chronologically well-behaved version of the source Observable - */ - public static <T> Observable<T> synchronize(Observable<T> observable) { - return _create(OperationSynchronize.synchronize(observable)); - } - - /** - * Returns an Observable that emits the first <code>num</code> items emitted by the source - * Observable. - * <p> - * You can choose to pay attention only to the first <code>num</code> values emitted by an Observable by calling its <code>take</code> method. This method returns an Observable that will call a - * subscribing Observer's <code>onNext</code> function a - * maximum of <code>num</code> times before calling <code>onCompleted</code>. - * <p> - * <img width="640" src="https://raw.github.com/wiki/Netflix/RxJava/images/rx-operators/take.png"> - * - * @param items - * the source Observable - * @param num - * the number of items from the start of the sequence emitted by the source - * Observable to emit - * @return an Observable that only emits the first <code>num</code> items emitted by the source - * Observable - */ - public static <T> Observable<T> take(final Observable<T> items, final int num) { - return _create(OperationTake.take(items, num)); - } - - /** - * Returns an Observable that emits a single item, a list composed of all the items emitted by - * the source Observable. - * <p> - * Normally, an Observable that returns multiple items will do so by calling its Observer's <code>onNext</code> function for each such item. You can change this behavior, instructing the - * Observable - * to - * compose a list of all of these multiple items and - * then to call the Observer's <code>onNext</code> function once, passing it the entire list, by calling the Observable object's <code>toList</code> method prior to calling its - * <code>subscribe</code> - * method. - * <p> - * <img width="640" src="https://raw.github.com/wiki/Netflix/RxJava/images/rx-operators/toList.png"> - * - * @param that - * the source Observable - * @return an Observable that emits a single item: a <code>List</code> containing all of the - * items emitted by the source Observable - */ - public static <T> Observable<List<T>> toList(final Observable<T> that) { - return _create(OperationToObservableList.toObservableList(that)); - } - - /** - * Converts an Iterable sequence to an Observable sequence. - * - * Any object that supports the Iterable interface can be converted into an Observable that emits - * each iterable item in the object, by passing the object into the <code>toObservable</code> method. - * <p> - * <img width="640" src="https://raw.github.com/wiki/Netflix/RxJava/images/rx-operators/toObservable.png"> - * - * @param iterable - * the source Iterable sequence - * @param <T> - * the type of items in the iterable sequence and the type emitted by the resulting - * Observable - * @return an Observable that emits each item in the source Iterable sequence - */ - public static <T> Observable<T> toObservable(Iterable<T> iterable) { - return _create(OperationToObservableIterable.toObservableIterable(iterable)); - } - - /** - * Converts an Future to an Observable sequence. - * - * Any object that supports the {@link Future} interface can be converted into an Observable that emits - * the return value of the get() method in the object, by passing the object into the <code>toObservable</code> method. - * The subscribe method on this synchronously so the Subscription returned doesn't nothing. - * - * @param future - * the source {@link Future} - * @param <T> - * the type of of object that the future's returns and the type emitted by the resulting - * Observable - * @return an Observable that emits the item from the source Future - */ - public static <T> Observable<T> toObservable(Future<T> future) { - return _create(OperationToObservableFuture.toObservableFuture(future)); - } - - /** - * Converts an Future to an Observable sequence. - * - * Any object that supports the {@link Future} interface can be converted into an Observable that emits - * the return value of the get() method in the object, by passing the object into the <code>toObservable</code> method. - * The subscribe method on this synchronously so the Subscription returned doesn't nothing. - * If the future timesout the {@link TimeoutException} exception is passed to the onError. - * - * @param future - * the source {@link Future} - * @param time - * the maximum time to wait - * @param unit - * the time unit of the time argument - * @param <T> - * the type of of object that the future's returns and the type emitted by the resulting - * Observable - * @return an Observable that emits the item from the source Future - */ - public static <T> Observable<T> toObservable(Future<T> future, long time, TimeUnit unit) { - return _create(OperationToObservableFuture.toObservableFuture(future, time, unit)); - } - - /** - * Converts an Array sequence to an Observable sequence. - * - * An Array can be converted into an Observable that emits each item in the Array, by passing the - * Array into the <code>toObservable</code> method. - * <p> - * <img width="640" src="https://raw.github.com/wiki/Netflix/RxJava/images/rx-operators/toObservable.png"> - * - * @param iterable - * the source Array - * @param <T> - * the type of items in the Array, and the type of items emitted by the resulting - * Observable - * @return an Observable that emits each item in the source Array - */ - public static <T> Observable<T> toObservable(T... items) { - return toObservable(Arrays.asList(items)); - } - - /** - * Sort T objects by their natural order (object must implement Comparable). - * <p> - * <img width="640" src="https://raw.github.com/wiki/Netflix/RxJava/images/rx-operators/toSortedList.png"> - * - * @param sequence - * @throws ClassCastException - * if T objects do not implement Comparable - * @return - */ - public static <T> Observable<List<T>> toSortedList(Observable<T> sequence) { - return _create(OperationToObservableSortedList.toSortedList(sequence)); - } - - /** - * Sort T objects using the defined sort function. - * <p> - * <img width="640" src="https://raw.github.com/wiki/Netflix/RxJava/images/rx-operators/toSortedList.png"> - * - * @param sequence - * @param sortFunction - * @return - */ - public static <T> Observable<List<T>> toSortedList(Observable<T> sequence, Func2<T, T, Integer> sortFunction) { - return _create(OperationToObservableSortedList.toSortedList(sequence, sortFunction)); - } - - /** - * Sort T objects using the defined sort function. - * <p> - * <img width="640" src="https://raw.github.com/wiki/Netflix/RxJava/images/rx-operators/toSortedList.png"> - * - * @param sequence - * @param sortFunction - * @return - */ - public static <T> Observable<List<T>> toSortedList(Observable<T> sequence, final Object sortFunction) { - @SuppressWarnings("rawtypes") - final FuncN _f = Functions.from(sortFunction); - return _create(OperationToObservableSortedList.toSortedList(sequence, new Func2<T, T, Integer>() { - - @Override - public Integer call(T t1, T t2) { - return (Integer) _f.call(t1, t2); - } - - })); - } - - /** - * Returns an Observable that applies a function of your choosing to the combination of items - * emitted, in sequence, by two other Observables, with the results of this function becoming the - * sequence emitted by the returned Observable. - * <p> - * <code>zip</code> applies this function in strict sequence, so the first item emitted by the new Observable will be the result of the function applied to the first item emitted by - * <code>w0</code> - * and the first item emitted by <code>w1</code>; the - * second item emitted by the new Observable will be the result of the function applied to the second item emitted by <code>w0</code> and the second item emitted by <code>w1</code>; and so forth. - * <p> - * The resulting <code>Observable<R></code> returned from <code>zip</code> will call <code>onNext</code> as many times as the number <code>onNext</code> calls of the source Observable with the - * shortest sequence. - * <p> - * <img width="640" src="https://raw.github.com/wiki/Netflix/RxJava/images/rx-operators/zip.png"> - * - * @param w0 - * one source Observable - * @param w1 - * another source Observable - * @param reduceFunction - * a function that, when applied to an item emitted by each of the source Observables, - * results in a value that will be emitted by the resulting Observable - * @return an Observable that emits the zipped results - */ - public static <R, T0, T1> Observable<R> zip(Observable<T0> w0, Observable<T1> w1, Func2<T0, T1, R> reduceFunction) { - return _create(OperationZip.zip(w0, w1, reduceFunction)); - } - - /** - * Returns an Observable that applies a function of your choosing to the combination of items - * emitted, in sequence, by two other Observables, with the results of this function becoming the - * sequence emitted by the returned Observable. - * <p> - * <code>zip</code> applies this function in strict sequence, so the first item emitted by the new Observable will be the result of the function applied to the first item emitted by - * <code>w0</code> - * and the first item emitted by <code>w1</code>; the - * second item emitted by the new Observable will be the result of the function applied to the second item emitted by <code>w0</code> and the second item emitted by <code>w1</code>; and so forth. - * <p> - * The resulting <code>Observable<R></code> returned from <code>zip</code> will call <code>onNext</code> as many times as the number <code>onNext</code> calls of the source Observable with the - * shortest sequence. - * <p> - * <img width="640" src="https://raw.github.com/wiki/Netflix/RxJava/images/rx-operators/zip.png"> - * - * @param w0 - * one source Observable - * @param w1 - * another source Observable - * @param reduceFunction - * a function that, when applied to an item emitted by each of the source Observables, - * results in a value that will be emitted by the resulting Observable - * @return an Observable that emits the zipped results - */ - public static <R, T0, T1> Observable<R> zip(Observable<T0> w0, Observable<T1> w1, final Object function) { - @SuppressWarnings("rawtypes") - final FuncN _f = Functions.from(function); - return zip(w0, w1, new Func2<T0, T1, R>() { - - @SuppressWarnings("unchecked") - @Override - public R call(T0 t0, T1 t1) { - return (R) _f.call(t0, t1); - } - - }); - } - - /** - * Returns an Observable that applies a function of your choosing to the combination of items - * emitted, in sequence, by three other Observables, with the results of this function becoming - * the sequence emitted by the returned Observable. - * <p> - * <code>zip</code> applies this function in strict sequence, so the first item emitted by the new Observable will be the result of the function applied to the first item emitted by - * <code>w0</code>, - * the first item emitted by <code>w1</code>, and the - * first item emitted by <code>w2</code>; the second item emitted by the new Observable will be the result of the function applied to the second item emitted by <code>w0</code>, the second item - * emitted by <code>w1</code>, and the second item - * emitted by <code>w2</code>; and so forth. - * <p> - * The resulting <code>Observable<R></code> returned from <code>zip</code> will call <code>onNext</code> as many times as the number <code>onNext</code> calls of the source Observable with the - * shortest sequence. - * <p> - * <img width="640" src="https://raw.github.com/wiki/Netflix/RxJava/images/rx-operators/zip.png"> - * - * @param w0 - * one source Observable - * @param w1 - * another source Observable - * @param w2 - * a third source Observable - * @param function - * a function that, when applied to an item emitted by each of the source Observables, - * results in a value that will be emitted by the resulting Observable - * @return an Observable that emits the zipped results - */ - public static <R, T0, T1, T2> Observable<R> zip(Observable<T0> w0, Observable<T1> w1, Observable<T2> w2, Func3<T0, T1, T2, R> function) { - return _create(OperationZip.zip(w0, w1, w2, function)); - } - - /** - * Returns an Observable that applies a function of your choosing to the combination of items - * emitted, in sequence, by three other Observables, with the results of this function becoming - * the sequence emitted by the returned Observable. - * <p> - * <code>zip</code> applies this function in strict sequence, so the first item emitted by the new Observable will be the result of the function applied to the first item emitted by - * <code>w0</code>, - * the first item emitted by <code>w1</code>, and the - * first item emitted by <code>w2</code>; the second item emitted by the new Observable will be the result of the function applied to the second item emitted by <code>w0</code>, the second item - * emitted by <code>w1</code>, and the second item - * emitted by <code>w2</code>; and so forth. - * <p> - * The resulting <code>Observable<R></code> returned from <code>zip</code> will call <code>onNext</code> as many times as the number <code>onNext</code> calls of the source Observable with the - * shortest sequence. - * <p> - * <img width="640" src="https://raw.github.com/wiki/Netflix/RxJava/images/rx-operators/zip.png"> - * - * @param w0 - * one source Observable - * @param w1 - * another source Observable - * @param w2 - * a third source Observable - * @param function - * a function that, when applied to an item emitted by each of the source Observables, - * results in a value that will be emitted by the resulting Observable - * @return an Observable that emits the zipped results - */ - public static <R, T0, T1, T2> Observable<R> zip(Observable<T0> w0, Observable<T1> w1, Observable<T2> w2, final Object function) { - @SuppressWarnings("rawtypes") - final FuncN _f = Functions.from(function); - return zip(w0, w1, w2, new Func3<T0, T1, T2, R>() { - - @SuppressWarnings("unchecked") - @Override - public R call(T0 t0, T1 t1, T2 t2) { - return (R) _f.call(t0, t1, t2); - } - - }); - } - - /** - * Returns an Observable that applies a function of your choosing to the combination of items - * emitted, in sequence, by four other Observables, with the results of this function becoming - * the sequence emitted by the returned Observable. - * <p> - * <code>zip</code> applies this function in strict sequence, so the first item emitted by the new Observable will be the result of the function applied to the first item emitted by - * <code>w0</code>, - * the first item emitted by <code>w1</code>, the - * first item emitted by <code>w2</code>, and the first item emitted by <code>w3</code>; the second item emitted by the new Observable will be the result of the function applied to the second item - * emitted by each of those Observables; and so forth. - * <p> - * The resulting <code>Observable<R></code> returned from <code>zip</code> will call <code>onNext</code> as many times as the number <code>onNext</code> calls of the source Observable with the - * shortest sequence. - * <p> - * <img width="640" src="https://raw.github.com/wiki/Netflix/RxJava/images/rx-operators/zip.png"> - * - * @param w0 - * one source Observable - * @param w1 - * another source Observable - * @param w2 - * a third source Observable - * @param w3 - * a fourth source Observable - * @param reduceFunction - * a function that, when applied to an item emitted by each of the source Observables, - * results in a value that will be emitted by the resulting Observable - * @return an Observable that emits the zipped results - */ - public static <R, T0, T1, T2, T3> Observable<R> zip(Observable<T0> w0, Observable<T1> w1, Observable<T2> w2, Observable<T3> w3, Func4<T0, T1, T2, T3, R> reduceFunction) { - return _create(OperationZip.zip(w0, w1, w2, w3, reduceFunction)); - } - - /** - * Returns an Observable that applies a function of your choosing to the combination of items - * emitted, in sequence, by four other Observables, with the results of this function becoming - * the sequence emitted by the returned Observable. - * <p> - * <code>zip</code> applies this function in strict sequence, so the first item emitted by the new Observable will be the result of the function applied to the first item emitted by - * <code>w0</code>, - * the first item emitted by <code>w1</code>, the - * first item emitted by <code>w2</code>, and the first item emitted by <code>w3</code>; the second item emitted by the new Observable will be the result of the function applied to the second item - * emitted by each of those Observables; and so forth. - * <p> - * The resulting <code>Observable<R></code> returned from <code>zip</code> will call <code>onNext</code> as many times as the number <code>onNext</code> calls of the source Observable with the - * shortest sequence. - * <p> - * <img width="640" src="https://raw.github.com/wiki/Netflix/RxJava/images/rx-operators/zip.png"> - * - * @param w0 - * one source Observable - * @param w1 - * another source Observable - * @param w2 - * a third source Observable - * @param w3 - * a fourth source Observable - * @param function - * a function that, when applied to an item emitted by each of the source Observables, - * results in a value that will be emitted by the resulting Observable - * @return an Observable that emits the zipped results - */ - public static <R, T0, T1, T2, T3> Observable<R> zip(Observable<T0> w0, Observable<T1> w1, Observable<T2> w2, Observable<T3> w3, final Object function) { - @SuppressWarnings("rawtypes") - final FuncN _f = Functions.from(function); - return zip(w0, w1, w2, w3, new Func4<T0, T1, T2, T3, R>() { - - @SuppressWarnings("unchecked") - @Override - public R call(T0 t0, T1 t1, T2 t2, T3 t3) { - return (R) _f.call(t0, t1, t2, t3); - } - - }); - } - - /** - * Filters an Observable by discarding any of its emissions that do not meet some test. - * <p> - * <img width="640" src="https://raw.github.com/wiki/Netflix/RxJava/images/rx-operators/filter.png"> - * - * @param predicate - * a function that evaluates the items emitted by the source Observable, returning - * <code>true</code> if they pass the filter - * @return an Observable that emits only those items in the original Observable that the filter - * evaluates as <code>true</code> - */ - public Observable<T> filter(Func1<T, Boolean> predicate) { - return filter(this, predicate); - } - - /** - * Filters an Observable by discarding any of its emissions that do not meet some test. - * <p> - * <img width="640" src="https://raw.github.com/wiki/Netflix/RxJava/images/rx-operators/filter.png"> - * - * @param callback - * a function that evaluates the items emitted by the source Observable, returning - * <code>true</code> if they pass the filter - * @return an Observable that emits only those items in the original Observable that the filter - * evaluates as "true" - */ - public Observable<T> filter(final Object callback) { - @SuppressWarnings("rawtypes") - final FuncN _f = Functions.from(callback); - return filter(this, new Func1<T, Boolean>() { - - public Boolean call(T t1) { - return (Boolean) _f.call(t1); - } - }); - } - - /** - * Converts an Observable that emits a sequence of objects into one that only emits the last - * object in this sequence before completing. - * <p> - * <img width="640" src="https://raw.github.com/wiki/Netflix/RxJava/images/rx-operators/last.png"> - * - * @return an Observable that emits only the last item emitted by the original Observable - */ - public Observable<T> last() { - return last(this); - } - - /** - * Applies a function of your choosing to every item emitted by an Observable, and returns this - * transformation as a new Observable sequence. - * <p> - * <img width="640" src="https://raw.github.com/wiki/Netflix/RxJava/images/rx-operators/map.png"> - * - * @param func - * a function to apply to each item in the sequence. - * @return an Observable that emits a sequence that is the result of applying the transformation - * function to each item in the sequence emitted by the input Observable. - */ - public <R> Observable<R> map(Func1<T, R> func) { - return map(this, func); - } - - /** - * Applies a function of your choosing to every item emitted by an Observable, and returns this - * transformation as a new Observable sequence. - * <p> - * <img width="640" src="https://raw.github.com/wiki/Netflix/RxJava/images/rx-operators/map.png"> - * - * @param callback - * a function to apply to each item in the sequence. - * @return an Observable that emits a sequence that is the result of applying the transformation - * function to each item in the sequence emitted by the input Observable. - */ - public <R> Observable<R> map(final Object callback) { - @SuppressWarnings("rawtypes") - final FuncN _f = Functions.from(callback); - return map(this, new Func1<T, R>() { - - @SuppressWarnings("unchecked") - public R call(T t1) { - return (R) _f.call(t1); - } - }); - } - - /** - * Creates a new Observable sequence by applying a function that you supply to each item in the - * original Observable sequence, where that function is itself an Observable that emits items, and - * then merges the results of that function applied to every item emitted by the original - * Observable, emitting these merged results as its own sequence. - * <p> - * <img width="640" src="https://raw.github.com/wiki/Netflix/RxJava/images/rx-operators/mapMany.png"> - * - * @param func - * a function to apply to each item in the sequence, that returns an Observable. - * @return an Observable that emits a sequence that is the result of applying the transformation - * function to each item in the input sequence and merging the results of the - * Observables obtained from this transformation. - */ - public <R> Observable<R> mapMany(Func1<T, Observable<R>> func) { - return mapMany(this, func); - } - - /** - * Creates a new Observable sequence by applying a function that you supply to each item in the - * original Observable sequence, where that function is itself an Observable that emits items, and - * then merges the results of that function applied to every item emitted by the original - * Observable, emitting these merged results as its own sequence. - * <p> - * <img width="640" src="https://raw.github.com/wiki/Netflix/RxJava/images/rx-operators/mapMany.png"> - * - * @param callback - * a function to apply to each item in the sequence that returns an Observable. - * @return an Observable that emits a sequence that is the result of applying the transformation' - * function to each item in the input sequence and merging the results of the - * Observables obtained from this transformation. - */ - public <R> Observable<R> mapMany(final Object callback) { - @SuppressWarnings("rawtypes") - final FuncN _f = Functions.from(callback); - return mapMany(this, new Func1<T, Observable<R>>() { - - @SuppressWarnings("unchecked") - public Observable<R> call(T t1) { - return (Observable<R>) _f.call(t1); - } - }); - } - - /** - * Materializes the implicit notifications of this observable sequence as explicit notification values. - * <p> - * <img width="640" src="https://raw.github.com/wiki/Netflix/RxJava/images/rx-operators/materialize.png"> - * - * @return An observable sequence whose elements are the result of materializing the notifications of the given sequence. - * @see http://msdn.microsoft.com/en-us/library/hh229453(v=VS.103).aspx - */ - public Observable<Notification<T>> materialize() { - return materialize(this); - } - - /** - * Instruct an Observable to pass control to another Observable rather than calling <code>onError</code> if it encounters an error. - * <p> - * By default, when an Observable encounters an error that prevents it from emitting the expected - * item to its Observer, the Observable calls its Observer's <code>onError</code> function, and - * then quits without calling any more of its Observer's closures. The - * <code>onErrorResumeNext</code> method changes this behavior. If you pass another Observable - * (<code>resumeFunction</code>) to an Observable's <code>onErrorResumeNext</code> method, if the - * original Observable encounters an error, instead of calling its Observer's - * <code>onErrort</code> function, it will instead relinquish control to - * <code>resumeFunction</code> which will call the Observer's <code>onNext</code> method if it - * is able to do so. In such a case, because no Observable necessarily invokes - * <code>onError</code>, the Observer may never know that an error happened. - * <p> - * You can use this to prevent errors from propagating or to supply fallback data should errors - * be encountered. - * <p> - * <img width="640" src="https://raw.github.com/wiki/Netflix/RxJava/images/rx-operators/onErrorResumeNext.png"> - * - * @param resumeFunction - * @return the original Observable, with appropriately modified behavior - */ - public Observable<T> onErrorResumeNext(final Func1<Exception, Observable<T>> resumeFunction) { - return onErrorResumeNext(this, resumeFunction); - } - - /** - * Instruct an Observable to emit a particular item rather than calling <code>onError</code> if - * it encounters an error. - * <p> - * By default, when an Observable encounters an error that prevents it from emitting the expected - * item to its Observer, the Observable calls its Observer's <code>onError</code> function, and - * then quits without calling any more of its Observer's closures. The - * <code>onErrorResumeNext</code> method changes this behavior. If you pass another Observable - * (<code>resumeFunction</code>) to an Observable's <code>onErrorResumeNext</code> method, if the - * original Observable encounters an error, instead of calling its Observer's - * <code>onError</code> function, it will instead relinquish control to - * <code>resumeFunction</code> which will call the Observer's <code>onNext</code> method if it - * is able to do so. In such a case, because no Observable necessarily invokes - * <code>onError</code>, the Observer may never know that an error happened. - * <p> - * You can use this to prevent errors from propagating or to supply fallback data should errors - * be encountered. - * <p> - * <img width="640" src="https://raw.github.com/wiki/Netflix/RxJava/images/rx-operators/onErrorResumeNext.png"> - * - * @param resumeFunction - * @return the original Observable with appropriately modified behavior - */ - public Observable<T> onErrorResumeNext(final Object resumeFunction) { - @SuppressWarnings("rawtypes") - final FuncN _f = Functions.from(resumeFunction); - return onErrorResumeNext(this, new Func1<Exception, Observable<T>>() { - - @SuppressWarnings("unchecked") - public Observable<T> call(Exception e) { - return (Observable<T>) _f.call(e); - } - }); - } - - /** - * Instruct an Observable to pass control to another Observable rather than calling - * <code>onError</code> if it encounters an error. - * <p> - * By default, when an Observable encounters an error that prevents it from emitting the expected - * item to its Observer, the Observable calls its Observer's <code>onError</code> function, and - * then quits without calling any more of its Observer's closures. The - * <code>onErrorResumeNext</code> method changes this behavior. If you pass another Observable - * (<code>resumeSequence</code>) to an Observable's <code>onErrorResumeNext</code> method, if the - * original Observable encounters an error, instead of calling its Observer's - * <code>onError</code> function, it will instead relinquish control to - * <code>resumeSequence</code> which will call the Observer's <code>onNext</code> method if it - * is able to do so. In such a case, because no Observable necessarily invokes - * <code>onError</code>, the Observer may never know that an error happened. - * <p> - * You can use this to prevent errors from propagating or to supply fallback data should errors - * be encountered. - * <p> - * <img width="640" src="https://raw.github.com/wiki/Netflix/RxJava/images/rx-operators/onErrorResumeNext.png"> - * - * @param resumeSequence - * @return the original Observable, with appropriately modified behavior - */ - public Observable<T> onErrorResumeNext(final Observable<T> resumeSequence) { - return onErrorResumeNext(this, resumeSequence); - } - - /** - * Instruct an Observable to emit a particular item rather than calling <code>onError</code> if - * it encounters an error. - * <p> - * By default, when an Observable encounters an error that prevents it from emitting the expected - * object to its Observer, the Observable calls its Observer's <code>onError</code> function, and - * then quits without calling any more of its Observer's closures. The - * <code>onErrorReturn</code> method changes this behavior. If you pass a function - * (<code>resumeFunction</code>) to an Observable's <code>onErrorReturn</code> method, if the - * original Observable encounters an error, instead of calling its Observer's - * <code>onError</code> function, it will instead call pass the return value of - * <code>resumeFunction</code> to the Observer's <code>onNext</code> method. - * <p> - * You can use this to prevent errors from propagating or to supply fallback data should errors - * be encountered. - * - * @param resumeFunction - * @return the original Observable with appropriately modified behavior - */ - public Observable<T> onErrorReturn(Func1<Exception, T> resumeFunction) { - return onErrorReturn(this, resumeFunction); - } - - /** - * Instruct an Observable to emit a particular item rather than calling <code>onError</code> if - * it encounters an error. - * <p> - * By default, when an Observable encounters an error that prevents it from emitting the expected - * object to its Observer, the Observable calls its Observer's <code>onError</code> function, and - * then quits without calling any more of its Observer's closures. The - * <code>onErrorReturn</code> method changes this behavior. If you pass a function - * (<code>resumeFunction</code>) to an Observable's <code>onErrorReturn</code> method, if the - * original Observable encounters an error, instead of calling its Observer's - * <code>onError</code> function, it will instead call pass the return value of - * <code>resumeFunction</code> to the Observer's <code>onNext</code> method. - * <p> - * You can use this to prevent errors from propagating or to supply fallback data should errors - * be encountered. - * - * @param that - * @param resumeFunction - * @return the original Observable with appropriately modified behavior - */ - public Observable<T> onErrorReturn(final Object resumeFunction) { - @SuppressWarnings("rawtypes") - final FuncN _f = Functions.from(resumeFunction); - return onErrorReturn(this, new Func1<Exception, T>() { - - @SuppressWarnings("unchecked") - public T call(Exception e) { - return (T) _f.call(e); - } - }); - } - - /** - * Returns an Observable that applies a function of your choosing to the first item emitted by a - * source Observable, then feeds the result of that function along with the second item emitted - * by an Observable into the same function, and so on until all items have been emitted by the - * source Observable, emitting the final result from the final call to your function as its sole - * output. - * <p> - * This technique, which is called "reduce" here, is sometimes called "fold," "accumulate," - * "compress," or "inject" in other programming contexts. Groovy, for instance, has an - * <code>inject</code> method that does a similar operation on lists. - * <p> - * <img width="640" src="https://raw.github.com/wiki/Netflix/RxJava/images/rx-operators/reduce.png"> - * - * @param accumulator - * An accumulator function to be invoked on each element from the sequence, whose result - * will be used in the next accumulator call (if applicable). - * - * @return An observable sequence with a single element from the result of accumulating the - * output from the list of Observables. - * @see <a href="http://msdn.microsoft.com/en-us/library/hh229154(v%3Dvs.103).aspx">MSDN: Observable.Aggregate</a> - * @see <a href="http://en.wikipedia.org/wiki/Fold_(higher-order_function)">Wikipedia: Fold (higher-order function)</a> - */ - public Observable<T> reduce(Func2<T, T, T> accumulator) { - return reduce(this, accumulator); - } - - /** - * Returns an Observable that applies a function of your choosing to the first item emitted by a - * source Observable, then feeds the result of that function along with the second item emitted - * by an Observable into the same function, and so on until all items have been emitted by the - * source Observable, emitting the final result from the final call to your function as its sole - * output. - * <p> - * This technique, which is called "reduce" here, is sometimes called "fold," "accumulate," - * "compress," or "inject" in other programming contexts. Groovy, for instance, has an - * <code>inject</code> method that does a similar operation on lists. - * <p> - * <img width="640" src="https://raw.github.com/wiki/Netflix/RxJava/images/rx-operators/reduce.png"> - * - * @param accumulator - * An accumulator function to be invoked on each element from the sequence, whose result - * will be used in the next accumulator call (if applicable). - * - * @return an Observable that emits a single element from the result of accumulating the output - * from the list of Observables. - * @see <a href="http://msdn.microsoft.com/en-us/library/hh229154(v%3Dvs.103).aspx">MSDN: Observable.Aggregate</a> - * @see <a href="http://en.wikipedia.org/wiki/Fold_(higher-order_function)">Wikipedia: Fold (higher-order function)</a> - */ - public Observable<T> reduce(Object accumulator) { - return reduce(this, accumulator); - } - - /** - * Returns an Observable that applies a function of your choosing to the first item emitted by a - * source Observable, then feeds the result of that function along with the second item emitted - * by an Observable into the same function, and so on until all items have been emitted by the - * source Observable, emitting the final result from the final call to your function as its sole - * output. - * <p> - * This technique, which is called "reduce" here, is sometimes called "fold," "accumulate," - * "compress," or "inject" in other programming contexts. Groovy, for instance, has an - * <code>inject</code> method that does a similar operation on lists. - * <p> - * <img width="640" src="https://raw.github.com/wiki/Netflix/RxJava/images/rx-operators/reduce.png"> - * - * @param initialValue - * The initial (seed) accumulator value. - * @param accumulator - * An accumulator function to be invoked on each element from the sequence, whose - * result will be used in the next accumulator call (if applicable). - * - * @return an Observable that emits a single element from the result of accumulating the output - * from the list of Observables. - * @see <a href="http://msdn.microsoft.com/en-us/library/hh229154(v%3Dvs.103).aspx">MSDN: Observable.Aggregate</a> - * @see <a href="http://en.wikipedia.org/wiki/Fold_(higher-order_function)">Wikipedia: Fold (higher-order function)</a> - */ - public Observable<T> reduce(T initialValue, Func2<T, T, T> accumulator) { - return reduce(this, initialValue, accumulator); - } - - /** - * Returns an Observable that applies a function of your choosing to the first item emitted by a - * source Observable, then feeds the result of that function along with the second item emitted - * by an Observable into the same function, and so on until all items have been emitted by the - * source Observable, emitting the final result from the final call to your function as its sole - * output. - * <p> - * This technique, which is called "reduce" here, is sometimes called "fold," "accumulate," - * "compress," or "inject" in other programming contexts. Groovy, for instance, has an - * <code>inject</code> method that does a similar operation on lists. - * <p> - * <img width="640" src="https://raw.github.com/wiki/Netflix/RxJava/images/rx-operators/reduce.png"> - * - * @param initialValue - * The initial (seed) accumulator value. - * @param accumulator - * An accumulator function to be invoked on each element from the sequence, whose - * result will be used in the next accumulator call (if applicable). - * @return an Observable that emits a single element from the result of accumulating the output - * from the list of Observables. - * @see <a href="http://msdn.microsoft.com/en-us/library/hh229154(v%3Dvs.103).aspx">MSDN: Observable.Aggregate</a> - * @see <a href="http://en.wikipedia.org/wiki/Fold_(higher-order_function)">Wikipedia: Fold (higher-order function)</a> - */ - public Observable<T> reduce(T initialValue, Object accumulator) { - return reduce(this, initialValue, accumulator); - } - - /** - * Returns an Observable that applies a function of your choosing to the first item emitted by a - * source Observable, then feeds the result of that function along with the second item emitted - * by an Observable into the same function, and so on until all items have been emitted by the - * source Observable, emitting the result of each of these iterations. It emits the result of - * each of these iterations as a sequence from the returned Observable. This sort of function is - * sometimes called an accumulator. - * <p> - * <img width="640" src="https://raw.github.com/wiki/Netflix/RxJava/images/rx-operators/scan.png"> - * - * @param accumulator - * An accumulator function to be invoked on each element from the sequence whose - * result will be sent via <code>onNext</code> and used in the next accumulator call - * (if applicable). - * @return an Observable sequence whose elements are the result of accumulating the output from - * the list of Observables. - * @see <a href="http://msdn.microsoft.com/en-us/library/hh211665(v%3Dvs.103).aspx">MSDN: Observable.Scan</a> - */ - public Observable<T> scan(Func2<T, T, T> accumulator) { - return scan(this, accumulator); - } - - /** - * Returns an Observable that applies a function of your choosing to the first item emitted by a - * source Observable, then feeds the result of that function along with the second item emitted - * by an Observable into the same function, and so on until all items have been emitted by the - * source Observable, emitting the result of each of these iterations. It emits the result of - * each of these iterations as a sequence from the returned Observable. This sort of function is - * sometimes called an accumulator. - * <p> - * <img width="640" src="https://raw.github.com/wiki/Netflix/RxJava/images/rx-operators/scan.png"> - * - * @param accumulator - * An accumulator function to be invoked on each element from the sequence whose - * result will be sent via <code>onNext</code> and used in the next accumulator call - * (if applicable). - * - * @return an Observable sequence whose elements are the result of accumulating the output from - * the list of Observables. - * @see <a href="http://msdn.microsoft.com/en-us/library/hh211665(v%3Dvs.103).aspx">MSDN: Observable.Scan</a> - */ - public Observable<T> scan(final Object accumulator) { - return scan(this, accumulator); - } - - /** - * Returns an Observable that applies a function of your choosing to the first item emitted by a - * source Observable, then feeds the result of that function along with the second item emitted - * by an Observable into the same function, and so on until all items have been emitted by the - * source Observable, emitting the result of each of these iterations. This sort of function is - * sometimes called an accumulator. - * <p> - * <img width="640" src="https://raw.github.com/wiki/Netflix/RxJava/images/rx-operators/scan.png"> - * - * @param initialValue - * The initial (seed) accumulator value. - * @param accumulator - * An accumulator function to be invoked on each element from the sequence whose - * result will be sent via <code>onNext</code> and used in the next accumulator call - * (if applicable). - * @return an Observable sequence whose elements are the result of accumulating the output from - * the list of Observables. - * @see <a href="http://msdn.microsoft.com/en-us/library/hh211665(v%3Dvs.103).aspx">MSDN: Observable.Scan</a> - */ - public Observable<T> scan(T initialValue, Func2<T, T, T> accumulator) { - return scan(this, initialValue, accumulator); - } - - /** - * Returns an Observable that applies a function of your choosing to the first item emitted by a - * source Observable, then feeds the result of that function along with the second item emitted - * by an Observable into the same function, then feeds the result of that function along with the - * third item into the same function, and so on, emitting the result of each of these - * iterations. This sort of function is sometimes called an accumulator. - * <p> - * <img width="640" src="https://raw.github.com/wiki/Netflix/RxJava/images/rx-operators/scan.png"> - * - * @param initialValue - * The initial (seed) accumulator value. - * @param accumulator - * An accumulator function to be invoked on each element from the sequence whose result - * will be sent via <code>onNext</code> and used in the next accumulator call (if - * applicable). - * @return an Observable sequence whose elements are the result of accumulating the output from - * the list of Observables. - * @see <a href="http://msdn.microsoft.com/en-us/library/hh211665(v%3Dvs.103).aspx">MSDN: Observable.Scan</a> - */ - public Observable<T> scan(final T initialValue, final Object accumulator) { - return scan(this, initialValue, accumulator); - } - - /** - * Returns an Observable that skips the first <code>num</code> items emitted by the source - * Observable. - * You can ignore the first <code>num</code> items emitted by an Observable and attend only to - * those items that come after, by modifying the Observable with the <code>skip</code> method. - * <p> - * <img width="640" src="https://raw.github.com/wiki/Netflix/RxJava/images/rx-operators/skip.png"> - * - * @param num - * The number of items to skip - * @return an Observable sequence that is identical to the source Observable except that it does - * not emit the first <code>num</code> items from that sequence. - */ - public Observable<T> skip(int num) { - return skip(this, num); - } - - /** - * Returns an Observable that emits the first <code>num</code> items emitted by the source - * Observable. - * - * You can choose to pay attention only to the first <code>num</code> values emitted by a - * Observable by calling its <code>take</code> method. This method returns an Observable that will - * call a subscribing Observer's <code>onNext</code> function a maximum of <code>num</code> times - * before calling <code>onCompleted</code>. - * <p> - * <img width="640" src="https://raw.github.com/wiki/Netflix/RxJava/images/rx-operators/take.png"> - * - * @param num - * @return an Observable that emits only the first <code>num</code> items from the source - * Observable, or all of the items from the source Observable if that Observable emits - * fewer than <code>num</code> items. - */ - public Observable<T> take(final int num) { - return take(this, num); - } - - /** - * Returns an Observable that emits a single item, a list composed of all the items emitted by - * the source Observable. - * - * Normally, an Observable that returns multiple items will do so by calling its Observer's - * <code>onNext</code> function for each such item. You can change this behavior, instructing - * the Observable to compose a list of all of these multiple items and then to call the - * Observer's <code>onNext</code> function once, passing it the entire list, by calling the - * Observable object's <code>toList</code> method prior to calling its <code>subscribe</code> - * method. - * <p> - * <img width="640" src="https://raw.github.com/wiki/Netflix/RxJava/images/rx-operators/toList.png"> - * - * @return an Observable that emits a single item: a List containing all of the items emitted by - * the source Observable. - */ - public Observable<List<T>> toList() { - return toList(this); - } - - /** - * Sort T objects by their natural order (object must implement Comparable). - * <p> - * <img width="640" src="https://raw.github.com/wiki/Netflix/RxJava/images/rx-operators/toSortedList.png"> - * - * @throws ClassCastException - * if T objects do not implement Comparable - * @return - */ - public Observable<List<T>> toSortedList() { - return toSortedList(this); - } - - /** - * Sort T objects using the defined sort function. - * <p> - * <img width="640" src="https://raw.github.com/wiki/Netflix/RxJava/images/rx-operators/toSortedList.png"> - * - * @param sortFunction - * @return - */ - public Observable<List<T>> toSortedList(Func2<T, T, Integer> sortFunction) { - return toSortedList(this, sortFunction); - } - - /** - * Sort T objects using the defined sort function. - * <p> - * <img width="640" src="https://raw.github.com/wiki/Netflix/RxJava/images/rx-operators/toSortedList.png"> - * - * @param sortFunction - * @return - */ - public Observable<List<T>> toSortedList(final Object sortFunction) { - return toSortedList(this, sortFunction); - } - - public static class UnitTest { - - @Mock - Observer<Integer> w; - - @Before - public void before() { - MockitoAnnotations.initMocks(this); - } - - @Test - public void testCreate() { - - Observable<String> observable = create(new Func1<Observer<String>, Subscription>() { - - @Override - public Subscription call(Observer<String> Observer) { - Observer.onNext("one"); - Observer.onNext("two"); - Observer.onNext("three"); - Observer.onCompleted(); - return Observable.noOpSubscription(); - } - - }); - - @SuppressWarnings("unchecked") - Observer<String> aObserver = mock(Observer.class); - observable.subscribe(aObserver); - verify(aObserver, times(1)).onNext("one"); - verify(aObserver, times(1)).onNext("two"); - verify(aObserver, times(1)).onNext("three"); - verify(aObserver, Mockito.never()).onError(any(Exception.class)); - verify(aObserver, times(1)).onCompleted(); - } - - @Test - public void testReduce() { - Observable<Integer> Observable = toObservable(1, 2, 3, 4); - reduce(Observable, new Func2<Integer, Integer, Integer>() { - - @Override - public Integer call(Integer t1, Integer t2) { - return t1 + t2; - } - - }).subscribe(w); - // we should be called only once - verify(w, times(1)).onNext(anyInt()); - verify(w).onNext(10); - } - - @Test - public void testReduceWithInitialValue() { - Observable<Integer> Observable = toObservable(1, 2, 3, 4); - reduce(Observable, 50, new Func2<Integer, Integer, Integer>() { - - @Override - public Integer call(Integer t1, Integer t2) { - return t1 + t2; - } - - }).subscribe(w); - // we should be called only once - verify(w, times(1)).onNext(anyInt()); - verify(w).onNext(60); - } - - } -} diff --git a/rxjava-core/src/main/java/rx/Observer.java b/rxjava-core/src/main/java/rx/Observer.java deleted file mode 100644 index dc12c8e97c..0000000000 --- a/rxjava-core/src/main/java/rx/Observer.java +++ /dev/null @@ -1,57 +0,0 @@ -/** - * Copyright 2013 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package rx; - -/** - * Provides a mechanism for receiving push-based notifications. - * <p> - * After an Observer calls an {@link Observable}'s <code>Observable.subscribe</code> method, the {@link Observable} calls the Observer's <code>onNext</code> method to provide notifications. A - * well-behaved {@link Observable} will - * call an Observer's <code>onCompleted</code> closure exactly once or the Observer's <code>onError</code> closure exactly once. - * <p> - * For more information see the <a href="https://github.com/Netflix/RxJava/wiki/Observable">RxJava Wiki</a> - * - * @param <T> - */ -public interface Observer<T> { - - /** - * Notifies the Observer that the {@link Observable} has finished sending push-based notifications. - * <p> - * The {@link Observable} will not call this closure if it calls <code>onError</code>. - */ - public void onCompleted(); - - /** - * Notifies the Observer that the {@link Observable} has experienced an error condition. - * <p> - * If the {@link Observable} calls this closure, it will not thereafter call <code>onNext</code> or <code>onCompleted</code>. - * - * @param e - */ - public void onError(Exception e); - - /** - * Provides the Observer with new data. - * <p> - * The {@link Observable} calls this closure 1 or more times, unless it calls <code>onError</code> in which case this closure may never be called. - * <p> - * The {@link Observable} will not call this closure again after it calls either <code>onCompleted</code> or <code>onError</code>. - * - * @param args - */ - public void onNext(T args); -} diff --git a/rxjava-core/src/main/java/rx/Subscription.java b/rxjava-core/src/main/java/rx/Subscription.java deleted file mode 100644 index 89bef356cb..0000000000 --- a/rxjava-core/src/main/java/rx/Subscription.java +++ /dev/null @@ -1,27 +0,0 @@ -/** - * Copyright 2013 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package rx; - -public interface Subscription { - - /** - * Stop receiving notifications on the {@link Observer} that was registered when this Subscription was received. - * <p> - * This allows unregistering an {@link Observer} before it has finished receiving all events (ie. before onCompleted is called). - */ - public void unsubscribe(); - -} diff --git a/rxjava-core/src/main/java/rx/concurrency/package-info.java b/rxjava-core/src/main/java/rx/concurrency/package-info.java deleted file mode 100644 index e0657722fb..0000000000 --- a/rxjava-core/src/main/java/rx/concurrency/package-info.java +++ /dev/null @@ -1,19 +0,0 @@ -/** - * Copyright 2013 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -/** - * Rx Schedulers - */ -package rx.concurrency; \ No newline at end of file diff --git a/rxjava-core/src/main/java/rx/operators/OperationCombineLatest.java b/rxjava-core/src/main/java/rx/operators/OperationCombineLatest.java deleted file mode 100644 index 208091874b..0000000000 --- a/rxjava-core/src/main/java/rx/operators/OperationCombineLatest.java +++ /dev/null @@ -1,807 +0,0 @@ -/** - * Copyright 2013 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package rx.operators; - -import static org.mockito.Matchers.*; -import static org.mockito.Mockito.*; - -import java.util.Arrays; -import java.util.HashMap; -import java.util.HashSet; -import java.util.LinkedHashMap; -import java.util.LinkedList; -import java.util.Map; -import java.util.concurrent.atomic.AtomicBoolean; - -import org.junit.Test; -import org.mockito.InOrder; - -import rx.Observable; -import rx.Observer; -import rx.Subscription; -import rx.util.functions.Func1; -import rx.util.functions.Func2; -import rx.util.functions.Func3; -import rx.util.functions.Func4; -import rx.util.functions.FuncN; -import rx.util.functions.Functions; - -public class OperationCombineLatest { - - public static <T0, T1, R> Func1<Observer<R>, Subscription> combineLatest(Observable<T0> w0, Observable<T1> w1, Func2<T0, T1, R> combineLatestFunction) { - Aggregator<R> a = new Aggregator<R>(Functions.fromFunc(combineLatestFunction)); - a.addObserver(new CombineObserver<R, T0>(a, w0)); - a.addObserver(new CombineObserver<R, T1>(a, w1)); - return a; - } - - public static <T0, T1, T2, R> Func1<Observer<R>, Subscription> combineLatest(Observable<T0> w0, Observable<T1> w1, Observable<T2> w2, Func3<T0, T1, T2, R> combineLatestFunction) { - Aggregator<R> a = new Aggregator<R>(Functions.fromFunc(combineLatestFunction)); - a.addObserver(new CombineObserver<R, T0>(a, w0)); - a.addObserver(new CombineObserver<R, T1>(a, w1)); - a.addObserver(new CombineObserver<R, T2>(a, w2)); - return a; - } - - public static <T0, T1, T2, T3, R> Func1<Observer<R>, Subscription> combineLatest(Observable<T0> w0, Observable<T1> w1, Observable<T2> w2, Observable<T3> w3, Func4<T0, T1, T2, T3, R> combineLatestFunction) { - Aggregator<R> a = new Aggregator<R>(Functions.fromFunc(combineLatestFunction)); - a.addObserver(new CombineObserver<R, T0>(a, w0)); - a.addObserver(new CombineObserver<R, T1>(a, w1)); - a.addObserver(new CombineObserver<R, T2>(a, w2)); - a.addObserver(new CombineObserver<R, T3>(a, w3)); - return a; - } - - private static class CombineObserver<R, T> implements Observer<T> { - final Observable<T> w; - final Aggregator<R> a; - private Subscription subscription; - - public CombineObserver(Aggregator<R> a, Observable<T> w) { - this.a = a; - this.w = w; - } - - public synchronized void startWatching() { - if (subscription != null) { - throw new RuntimeException("This should only be called once."); - } - subscription = w.subscribe(this); - } - - @Override - public void onCompleted() { - a.complete(this); - } - - @Override - public void onError(Exception e) { - a.error(this, e); - } - - @Override - public void onNext(Object args) { - a.next(this, args); - } - } - - /** - * Receive notifications from each of the Observables we are reducing and execute the combineLatestFunction whenever we have received events from all Observables. - * - * @param <R> - */ - private static class Aggregator<R> implements Func1<Observer<R>, Subscription> { - - private final FuncN<R> combineLatestFunction; - private Observer<R> Observer; - private AtomicBoolean running = new AtomicBoolean(true); - - /** - * Use LinkedHashMap to retain the order we receive the CombineLatestObserver objects in. - * <p> - * Note that access to this LinkedList inside MUST BE SYNCHRONIZED - */ - private Map<CombineObserver<R, ?>, LinkedList<Object>> receivedValuesPerObserver = new LinkedHashMap<CombineObserver<R, ?>, LinkedList<Object>>(); - - /** - * store when a Observer completes - * <p> - * Note that access to this set MUST BE SYNCHRONIZED - * */ - private HashSet<CombineObserver<R, ?>> completed = new HashSet<CombineObserver<R, ?>>(); - - /** - * The last value from a Observer - * <p> - * Note that access to this set MUST BE SYNCHRONIZED - * */ - private HashMap<CombineObserver<R, ?>, Object> lastValue = new HashMap<CombineObserver<R, ?>, Object>(); - - public Aggregator(FuncN<R> combineLatestFunction) { - this.combineLatestFunction = combineLatestFunction; - } - - /** - * Receive notification of a Observer starting (meaning we should require it for aggregation) - * - * @param w - */ - synchronized void addObserver(CombineObserver<R, ?> w) { - // initialize this CombineLatestObserver - receivedValuesPerObserver.put(w, new LinkedList<Object>()); - } - - /** - * Receive notification of a Observer completing its iterations. - * - * @param w - */ - synchronized void complete(CombineObserver<R, ?> w) { - // store that this ZipObserver is completed - completed.add(w); - // if all CombineObservers are completed, we mark the whole thing as completed - if (completed.size() == receivedValuesPerObserver.size()) { - if (running.get()) { - // mark ourselves as done - Observer.onCompleted(); - // just to ensure we stop processing in case we receive more onNext/complete/error calls after this - running.set(false); - } - } - } - - /** - * Receive error for a Observer. Throw the error up the chain and stop processing. - * - * @param w - */ - synchronized void error(CombineObserver<R, ?> w, Exception e) { - Observer.onError(e); - /* tell ourselves to stop processing onNext events, event if the Observers don't obey the unsubscribe we're about to send */ - running.set(false); - /* tell all Observers to unsubscribe since we had an error */ - stop(); - } - - /** - * Receive the next value from a Observer. - * <p> - * If we have received values from all Observers, trigger the combineLatest function, otherwise store the value and keep waiting. - * - * @param w - * @param arg - */ - void next(CombineObserver<R, ?> w, Object arg) { - if (Observer == null) { - throw new RuntimeException("This shouldn't be running if a Observer isn't registered"); - } - - /* if we've been 'unsubscribed' don't process anything further even if the things we're watching keep sending (likely because they are not responding to the unsubscribe call) */ - if (!running.get()) { - return; - } - - // define here so the variable is out of the synchronized scope - Object[] argsToCombineLatest = new Object[receivedValuesPerObserver.size()]; - - // we synchronize everything that touches receivedValues and the internal LinkedList objects - synchronized (this) { - // add this value to the queue of the CombineLatestObserver for values received - receivedValuesPerObserver.get(w).add(arg); - // remember this as the last value for this Observer - lastValue.put(w, arg); - - // if all CombineLatestObservers in 'receivedValues' map have a value, invoke the combineLatestFunction - for (CombineObserver<R, ?> rw : receivedValuesPerObserver.keySet()) { - if (receivedValuesPerObserver.get(rw).peek() == null && !completed.contains(rw)) { - // we have a null (and the Observer isn't completed) meaning the queues aren't all populated so won't do anything - return; - } - } - // if we get to here this means all the queues have data (or some are completed) - int i = 0; - boolean foundData = false; - for (CombineObserver<R, ?> _w : receivedValuesPerObserver.keySet()) { - LinkedList<Object> q = receivedValuesPerObserver.get(_w); - if (q.peek() == null) { - // this is a completed Observer - // we rely on the check above looking at completed.contains to mean that NULL here represents a completed Observer - argsToCombineLatest[i++] = lastValue.get(_w); - } else { - foundData = true; - argsToCombineLatest[i++] = q.remove(); - } - } - if (completed.size() == receivedValuesPerObserver.size() && !foundData) { - // all are completed and queues have run out of data, so return and don't send empty data - return; - } - } - // if we did not return above from the synchronized block we can now invoke the combineLatestFunction with all of the args - // we do this outside the synchronized block as it is now safe to call this concurrently and don't need to block other threads from calling - // this 'next' method while another thread finishes calling this combineLatestFunction - Observer.onNext(combineLatestFunction.call(argsToCombineLatest)); - } - - @Override - public Subscription call(Observer<R> Observer) { - if (this.Observer != null) { - throw new IllegalStateException("Only one Observer can subscribe to this Observable."); - } - this.Observer = Observer; - - /* start the Observers */ - for (CombineObserver<R, ?> rw : receivedValuesPerObserver.keySet()) { - rw.startWatching(); - } - - return new Subscription() { - - @Override - public void unsubscribe() { - stop(); - } - - }; - } - - private void stop() { - /* tell ourselves to stop processing onNext events */ - running.set(false); - /* propogate to all Observers to unsubscribe */ - for (CombineObserver<R, ?> rw : receivedValuesPerObserver.keySet()) { - if (rw.subscription != null) { - rw.subscription.unsubscribe(); - } - } - } - - } - - public static class UnitTest { - - @SuppressWarnings("unchecked") - /* mock calls don't do generics */ - @Test - public void testCombineLatestDifferentLengthObservableSequences1() { - Observer<String> w = mock(Observer.class); - - TestObservable w1 = new TestObservable(); - TestObservable w2 = new TestObservable(); - TestObservable w3 = new TestObservable(); - - Observable<String> combineLatestW = Observable.create(combineLatest(w1, w2, w3, getConcat3StringsCombineLatestFunction())); - combineLatestW.subscribe(w); - - /* simulate sending data */ - // once for w1 - w1.Observer.onNext("1a"); - w1.Observer.onCompleted(); - // twice for w2 - w2.Observer.onNext("2a"); - w2.Observer.onNext("2b"); - w2.Observer.onCompleted(); - // 4 times for w3 - w3.Observer.onNext("3a"); - w3.Observer.onNext("3b"); - w3.Observer.onNext("3c"); - w3.Observer.onNext("3d"); - w3.Observer.onCompleted(); - - /* we should have been called 4 times on the Observer */ - InOrder inOrder = inOrder(w); - inOrder.verify(w).onNext("1a2a3a"); - inOrder.verify(w).onNext("1a2b3b"); - inOrder.verify(w).onNext("1a2b3c"); - inOrder.verify(w).onNext("1a2b3d"); - - inOrder.verify(w, times(1)).onCompleted(); - } - - @Test - public void testCombineLatestDifferentLengthObservableSequences2() { - @SuppressWarnings("unchecked") - Observer<String> w = mock(Observer.class); - - TestObservable w1 = new TestObservable(); - TestObservable w2 = new TestObservable(); - TestObservable w3 = new TestObservable(); - - Observable<String> combineLatestW = Observable.create(combineLatest(w1, w2, w3, getConcat3StringsCombineLatestFunction())); - combineLatestW.subscribe(w); - - /* simulate sending data */ - // 4 times for w1 - w1.Observer.onNext("1a"); - w1.Observer.onNext("1b"); - w1.Observer.onNext("1c"); - w1.Observer.onNext("1d"); - w1.Observer.onCompleted(); - // twice for w2 - w2.Observer.onNext("2a"); - w2.Observer.onNext("2b"); - w2.Observer.onCompleted(); - // 1 times for w3 - w3.Observer.onNext("3a"); - w3.Observer.onCompleted(); - - /* we should have been called 1 time only on the Observer since we only combine the "latest" we don't go back and loop through others once completed */ - InOrder inOrder = inOrder(w); - inOrder.verify(w, times(1)).onNext("1a2a3a"); - inOrder.verify(w, never()).onNext(anyString()); - - inOrder.verify(w, times(1)).onCompleted(); - - } - - /** - * Testing internal private logic due to the complexity so I want to use TDD to test as a I build it rather than relying purely on the overall functionality expected by the public methods. - */ - @SuppressWarnings("unchecked") - /* mock calls don't do generics */ - @Test - public void testAggregatorSimple() { - FuncN<String> combineLatestFunction = getConcatCombineLatestFunction(); - /* create the aggregator which will execute the combineLatest function when all Observables provide values */ - Aggregator<String> a = new Aggregator<String>(combineLatestFunction); - - /* define a Observer to receive aggregated events */ - Observer<String> aObserver = mock(Observer.class); - Observable.create(a).subscribe(aObserver); - - /* mock the Observable Observers that are 'pushing' data for us */ - CombineObserver<String, String> r1 = mock(CombineObserver.class); - CombineObserver<String, String> r2 = mock(CombineObserver.class); - - /* pretend we're starting up */ - a.addObserver(r1); - a.addObserver(r2); - - /* simulate the Observables pushing data into the aggregator */ - a.next(r1, "hello"); - a.next(r2, "world"); - - InOrder inOrder = inOrder(aObserver); - - verify(aObserver, never()).onError(any(Exception.class)); - verify(aObserver, never()).onCompleted(); - inOrder.verify(aObserver, times(1)).onNext("helloworld"); - - a.next(r1, "hello "); - a.next(r2, "again"); - - verify(aObserver, never()).onError(any(Exception.class)); - verify(aObserver, never()).onCompleted(); - inOrder.verify(aObserver, times(1)).onNext("hello again"); - - a.complete(r1); - a.complete(r2); - - inOrder.verify(aObserver, never()).onNext(anyString()); - verify(aObserver, times(1)).onCompleted(); - } - - @SuppressWarnings("unchecked") - /* mock calls don't do generics */ - @Test - public void testAggregatorDifferentSizedResultsWithOnComplete() { - FuncN<String> combineLatestFunction = getConcatCombineLatestFunction(); - /* create the aggregator which will execute the combineLatest function when all Observables provide values */ - Aggregator<String> a = new Aggregator<String>(combineLatestFunction); - - /* define a Observer to receive aggregated events */ - Observer<String> aObserver = mock(Observer.class); - Observable.create(a).subscribe(aObserver); - - /* mock the Observable Observers that are 'pushing' data for us */ - CombineObserver<String, String> r1 = mock(CombineObserver.class); - CombineObserver<String, String> r2 = mock(CombineObserver.class); - - /* pretend we're starting up */ - a.addObserver(r1); - a.addObserver(r2); - - /* simulate the Observables pushing data into the aggregator */ - a.next(r1, "hello"); - a.next(r2, "world"); - a.complete(r2); - - verify(aObserver, never()).onError(any(Exception.class)); - verify(aObserver, never()).onCompleted(); - verify(aObserver, times(1)).onNext("helloworld"); - - a.next(r1, "hi"); - a.complete(r1); - - verify(aObserver, never()).onError(any(Exception.class)); - verify(aObserver, times(1)).onCompleted(); - verify(aObserver, times(1)).onNext("hiworld"); - } - - @SuppressWarnings("unchecked") - /* mock calls don't do generics */ - @Test - public void testAggregateMultipleTypes() { - FuncN<String> combineLatestFunction = getConcatCombineLatestFunction(); - /* create the aggregator which will execute the combineLatest function when all Observables provide values */ - Aggregator<String> a = new Aggregator<String>(combineLatestFunction); - - /* define a Observer to receive aggregated events */ - Observer<String> aObserver = mock(Observer.class); - Observable.create(a).subscribe(aObserver); - - /* mock the Observable Observers that are 'pushing' data for us */ - CombineObserver<String, String> r1 = mock(CombineObserver.class); - CombineObserver<String, Integer> r2 = mock(CombineObserver.class); - - /* pretend we're starting up */ - a.addObserver(r1); - a.addObserver(r2); - - /* simulate the Observables pushing data into the aggregator */ - a.next(r1, "hello"); - a.next(r2, "world"); - a.complete(r2); - - verify(aObserver, never()).onError(any(Exception.class)); - verify(aObserver, never()).onCompleted(); - verify(aObserver, times(1)).onNext("helloworld"); - - a.next(r1, "hi"); - a.complete(r1); - - verify(aObserver, never()).onError(any(Exception.class)); - verify(aObserver, times(1)).onCompleted(); - verify(aObserver, times(1)).onNext("hiworld"); - } - - @SuppressWarnings("unchecked") - /* mock calls don't do generics */ - @Test - public void testAggregate3Types() { - FuncN<String> combineLatestFunction = getConcatCombineLatestFunction(); - /* create the aggregator which will execute the combineLatest function when all Observables provide values */ - Aggregator<String> a = new Aggregator<String>(combineLatestFunction); - - /* define a Observer to receive aggregated events */ - Observer<String> aObserver = mock(Observer.class); - Observable.create(a).subscribe(aObserver); - - /* mock the Observable Observers that are 'pushing' data for us */ - CombineObserver<String, String> r1 = mock(CombineObserver.class); - CombineObserver<String, Integer> r2 = mock(CombineObserver.class); - CombineObserver<String, int[]> r3 = mock(CombineObserver.class); - - /* pretend we're starting up */ - a.addObserver(r1); - a.addObserver(r2); - a.addObserver(r3); - - /* simulate the Observables pushing data into the aggregator */ - a.next(r1, "hello"); - a.next(r2, 2); - a.next(r3, new int[] { 5, 6, 7 }); - - verify(aObserver, never()).onError(any(Exception.class)); - verify(aObserver, never()).onCompleted(); - verify(aObserver, times(1)).onNext("hello2[5, 6, 7]"); - } - - @SuppressWarnings("unchecked") - /* mock calls don't do generics */ - @Test - public void testAggregatorsWithDifferentSizesAndTiming() { - FuncN<String> combineLatestFunction = getConcatCombineLatestFunction(); - /* create the aggregator which will execute the combineLatest function when all Observables provide values */ - Aggregator<String> a = new Aggregator<String>(combineLatestFunction); - - /* define a Observer to receive aggregated events */ - Observer<String> aObserver = mock(Observer.class); - Observable.create(a).subscribe(aObserver); - - /* mock the Observable Observers that are 'pushing' data for us */ - CombineObserver<String, String> r1 = mock(CombineObserver.class); - CombineObserver<String, String> r2 = mock(CombineObserver.class); - - /* pretend we're starting up */ - a.addObserver(r1); - a.addObserver(r2); - - /* simulate the Observables pushing data into the aggregator */ - a.next(r1, "one"); - a.next(r1, "two"); - a.next(r1, "three"); - a.next(r2, "A"); - - verify(aObserver, never()).onError(any(Exception.class)); - verify(aObserver, never()).onCompleted(); - verify(aObserver, times(1)).onNext("oneA"); - - a.next(r1, "four"); - a.complete(r1); - a.next(r2, "B"); - verify(aObserver, times(1)).onNext("twoB"); - a.next(r2, "C"); - verify(aObserver, times(1)).onNext("threeC"); - a.next(r2, "D"); - verify(aObserver, times(1)).onNext("fourD"); - a.next(r2, "E"); - verify(aObserver, times(1)).onNext("fourE"); - a.complete(r2); - - verify(aObserver, never()).onError(any(Exception.class)); - verify(aObserver, times(1)).onCompleted(); - } - - @SuppressWarnings("unchecked") - /* mock calls don't do generics */ - @Test - public void testAggregatorError() { - FuncN<String> combineLatestFunction = getConcatCombineLatestFunction(); - /* create the aggregator which will execute the combineLatest function when all Observables provide values */ - Aggregator<String> a = new Aggregator<String>(combineLatestFunction); - - /* define a Observer to receive aggregated events */ - Observer<String> aObserver = mock(Observer.class); - Observable.create(a).subscribe(aObserver); - - /* mock the Observable Observers that are 'pushing' data for us */ - CombineObserver<String, String> r1 = mock(CombineObserver.class); - CombineObserver<String, String> r2 = mock(CombineObserver.class); - - /* pretend we're starting up */ - a.addObserver(r1); - a.addObserver(r2); - - /* simulate the Observables pushing data into the aggregator */ - a.next(r1, "hello"); - a.next(r2, "world"); - - verify(aObserver, never()).onError(any(Exception.class)); - verify(aObserver, never()).onCompleted(); - verify(aObserver, times(1)).onNext("helloworld"); - - a.error(r1, new RuntimeException("")); - a.next(r1, "hello"); - a.next(r2, "again"); - - verify(aObserver, times(1)).onError(any(Exception.class)); - verify(aObserver, never()).onCompleted(); - // we don't want to be called again after an error - verify(aObserver, times(0)).onNext("helloagain"); - } - - @SuppressWarnings("unchecked") - /* mock calls don't do generics */ - @Test - public void testAggregatorUnsubscribe() { - FuncN<String> combineLatestFunction = getConcatCombineLatestFunction(); - /* create the aggregator which will execute the combineLatest function when all Observables provide values */ - Aggregator<String> a = new Aggregator<String>(combineLatestFunction); - - /* define a Observer to receive aggregated events */ - Observer<String> aObserver = mock(Observer.class); - Subscription subscription = Observable.create(a).subscribe(aObserver); - - /* mock the Observable Observers that are 'pushing' data for us */ - CombineObserver<String, String> r1 = mock(CombineObserver.class); - CombineObserver<String, String> r2 = mock(CombineObserver.class); - - /* pretend we're starting up */ - a.addObserver(r1); - a.addObserver(r2); - - /* simulate the Observables pushing data into the aggregator */ - a.next(r1, "hello"); - a.next(r2, "world"); - - verify(aObserver, never()).onError(any(Exception.class)); - verify(aObserver, never()).onCompleted(); - verify(aObserver, times(1)).onNext("helloworld"); - - subscription.unsubscribe(); - a.next(r1, "hello"); - a.next(r2, "again"); - - verify(aObserver, times(0)).onError(any(Exception.class)); - verify(aObserver, never()).onCompleted(); - // we don't want to be called again after an error - verify(aObserver, times(0)).onNext("helloagain"); - } - - @SuppressWarnings("unchecked") - /* mock calls don't do generics */ - @Test - public void testAggregatorEarlyCompletion() { - FuncN<String> combineLatestFunction = getConcatCombineLatestFunction(); - /* create the aggregator which will execute the combineLatest function when all Observables provide values */ - Aggregator<String> a = new Aggregator<String>(combineLatestFunction); - - /* define a Observer to receive aggregated events */ - Observer<String> aObserver = mock(Observer.class); - Observable.create(a).subscribe(aObserver); - - /* mock the Observable Observers that are 'pushing' data for us */ - CombineObserver<String, String> r1 = mock(CombineObserver.class); - CombineObserver<String, String> r2 = mock(CombineObserver.class); - - /* pretend we're starting up */ - a.addObserver(r1); - a.addObserver(r2); - - /* simulate the Observables pushing data into the aggregator */ - a.next(r1, "one"); - a.next(r1, "two"); - a.complete(r1); - a.next(r2, "A"); - - verify(aObserver, never()).onError(any(Exception.class)); - verify(aObserver, never()).onCompleted(); - verify(aObserver, times(1)).onNext("oneA"); - - a.complete(r2); - - verify(aObserver, never()).onError(any(Exception.class)); - verify(aObserver, times(1)).onCompleted(); - // we shouldn't get this since completed is called before any other onNext calls could trigger this - verify(aObserver, never()).onNext("twoA"); - } - - @SuppressWarnings("unchecked") - /* mock calls don't do generics */ - @Test - public void testCombineLatest2Types() { - Func2<String, Integer, String> combineLatestFunction = getConcatStringIntegerCombineLatestFunction(); - - /* define a Observer to receive aggregated events */ - Observer<String> aObserver = mock(Observer.class); - - Observable<String> w = Observable.create(combineLatest(Observable.toObservable("one", "two"), Observable.toObservable(2, 3, 4), combineLatestFunction)); - w.subscribe(aObserver); - - verify(aObserver, never()).onError(any(Exception.class)); - verify(aObserver, times(1)).onCompleted(); - verify(aObserver, times(1)).onNext("one2"); - verify(aObserver, times(1)).onNext("two3"); - verify(aObserver, times(1)).onNext("two4"); - } - - @SuppressWarnings("unchecked") - /* mock calls don't do generics */ - @Test - public void testCombineLatest3TypesA() { - Func3<String, Integer, int[], String> combineLatestFunction = getConcatStringIntegerIntArrayCombineLatestFunction(); - - /* define a Observer to receive aggregated events */ - Observer<String> aObserver = mock(Observer.class); - - Observable<String> w = Observable.create(combineLatest(Observable.toObservable("one", "two"), Observable.toObservable(2), Observable.toObservable(new int[] { 4, 5, 6 }), combineLatestFunction)); - w.subscribe(aObserver); - - verify(aObserver, never()).onError(any(Exception.class)); - verify(aObserver, times(1)).onCompleted(); - verify(aObserver, times(1)).onNext("one2[4, 5, 6]"); - } - - @SuppressWarnings("unchecked") - /* mock calls don't do generics */ - @Test - public void testCombineLatest3TypesB() { - Func3<String, Integer, int[], String> combineLatestFunction = getConcatStringIntegerIntArrayCombineLatestFunction(); - - /* define a Observer to receive aggregated events */ - Observer<String> aObserver = mock(Observer.class); - - Observable<String> w = Observable.create(combineLatest(Observable.toObservable("one"), Observable.toObservable(2), Observable.toObservable(new int[] { 4, 5, 6 }, new int[] { 7, 8 }), combineLatestFunction)); - w.subscribe(aObserver); - - verify(aObserver, never()).onError(any(Exception.class)); - verify(aObserver, times(1)).onCompleted(); - verify(aObserver, times(1)).onNext("one2[4, 5, 6]"); - verify(aObserver, times(1)).onNext("one2[7, 8]"); - } - - private Func3<String, String, String, String> getConcat3StringsCombineLatestFunction() { - Func3<String, String, String, String> combineLatestFunction = new Func3<String, String, String, String>() { - - @Override - public String call(String a1, String a2, String a3) { - if (a1 == null) { - a1 = ""; - } - if (a2 == null) { - a2 = ""; - } - if (a3 == null) { - a3 = ""; - } - return a1 + a2 + a3; - } - - }; - return combineLatestFunction; - } - - private FuncN<String> getConcatCombineLatestFunction() { - FuncN<String> combineLatestFunction = new FuncN<String>() { - - @Override - public String call(Object... args) { - String returnValue = ""; - for (Object o : args) { - if (o != null) { - returnValue += getStringValue(o); - } - } - System.out.println("returning: " + returnValue); - return returnValue; - } - - }; - return combineLatestFunction; - } - - private Func2<String, Integer, String> getConcatStringIntegerCombineLatestFunction() { - Func2<String, Integer, String> combineLatestFunction = new Func2<String, Integer, String>() { - - @Override - public String call(String s, Integer i) { - return getStringValue(s) + getStringValue(i); - } - - }; - return combineLatestFunction; - } - - private Func3<String, Integer, int[], String> getConcatStringIntegerIntArrayCombineLatestFunction() { - Func3<String, Integer, int[], String> combineLatestFunction = new Func3<String, Integer, int[], String>() { - - @Override - public String call(String s, Integer i, int[] iArray) { - return getStringValue(s) + getStringValue(i) + getStringValue(iArray); - } - - }; - return combineLatestFunction; - } - - private static String getStringValue(Object o) { - if (o == null) { - return ""; - } else { - if (o instanceof int[]) { - return Arrays.toString((int[]) o); - } else { - return String.valueOf(o); - } - } - } - - private static class TestObservable extends Observable<String> { - - Observer<String> Observer; - - @Override - public Subscription subscribe(Observer<String> Observer) { - // just store the variable where it can be accessed so we can manually trigger it - this.Observer = Observer; - return Observable.noOpSubscription(); - } - - } - } - -} diff --git a/rxjava-core/src/main/java/rx/operators/OperationConcat.java b/rxjava-core/src/main/java/rx/operators/OperationConcat.java deleted file mode 100644 index c621002533..0000000000 --- a/rxjava-core/src/main/java/rx/operators/OperationConcat.java +++ /dev/null @@ -1,315 +0,0 @@ -/** - * Copyright 2013 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package rx.operators; - -import static org.junit.Assert.*; -import static org.mockito.Mockito.*; - -import java.lang.reflect.Array; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.CountDownLatch; - -import org.junit.Assert; -import org.junit.Before; -import org.junit.Test; - -import rx.Observable; -import rx.Observer; -import rx.Subscription; -import rx.util.AtomicObservableSubscription; -import rx.util.functions.Action1; -import rx.util.functions.Func1; - -public final class OperationConcat { - - /** - * Combine the observable sequences from the list of Observables into one observable sequence without any transformation. - * - * @param sequences - * An observable sequence of elements to project. - * @return An observable sequence whose elements are the result of combining the output from the list of Observables. - */ - public static <T> Func1<Observer<T>, Subscription> concat(final Observable<T>... sequences) { - return new Func1<Observer<T>, Subscription>() { - - @Override - public Subscription call(Observer<T> observer) { - return new Concat<T>(sequences).call(observer); - } - }; - } - - public static <T> Func1<Observer<T>, Subscription> concat(final List<Observable<T>> sequences) { - @SuppressWarnings("unchecked") - Observable<T>[] o = sequences.toArray((Observable<T>[]) Array.newInstance(Observable.class, sequences.size())); - return concat(o); - } - - public static <T> Func1<Observer<T>, Subscription> concat(final Observable<Observable<T>> sequences) { - final List<Observable<T>> list = new ArrayList<Observable<T>>(); - sequences.toList().subscribe(new Action1<List<Observable<T>>>() { - @Override - public void call(List<Observable<T>> t1) { - list.addAll(t1); - } - - }); - - return concat(list); - } - - private static class Concat<T> implements Func1<Observer<T>, Subscription> { - private final Observable<T>[] sequences; - private int num = 0; - private int count = 0; - private Subscription s; - - Concat(final Observable<T>... sequences) { - this.sequences = sequences; - this.num = sequences.length - 1; - } - - private final AtomicObservableSubscription Subscription = new AtomicObservableSubscription(); - - private final Subscription actualSubscription = new Subscription() { - - @Override - public void unsubscribe() { - if (null != s) - s.unsubscribe(); - } - }; - - public Subscription call(Observer<T> observer) { - s = sequences[count].subscribe(new ConcatObserver(observer)); - - return Subscription.wrap(actualSubscription); - } - - private class ConcatObserver implements Observer<T> { - private final Observer<T> observer; - - ConcatObserver(Observer<T> observer) { - this.observer = observer; - } - - @Override - public void onCompleted() { - if (num == count) - observer.onCompleted(); - else { - count++; - s = sequences[count].subscribe(this); - } - } - - @Override - public void onError(Exception e) { - observer.onError(e); - - } - - @Override - public void onNext(T args) { - observer.onNext(args); - - } - } - } - - public static class UnitTest { - private final static String[] expected = { "1", "3", "5", "7", "2", "4", "6" }; - private int index = 0; - - Observer<String> observer = new Observer<String>() { - - @Override - public void onCompleted() { - } - - @Override - public void onError(Exception e) { - // TODO Auto-generated method stub - } - - @Override - public void onNext(String args) { - Assert.assertEquals(expected[index], args); - index++; - } - }; - - @Before - public void before() { - index = 0; - } - - @Test - public void testConcat() { - final String[] o = { "1", "3", "5", "7" }; - final String[] e = { "2", "4", "6" }; - - final Observable<String> odds = Observable.toObservable(o); - final Observable<String> even = Observable.toObservable(e); - - @SuppressWarnings("unchecked") - Observable<String> concat = Observable.create(concat(odds, even)); - concat.subscribe(observer); - Assert.assertEquals(expected.length, index); - - } - - @Test - public void testConcatWithList() { - final String[] o = { "1", "3", "5", "7" }; - final String[] e = { "2", "4", "6" }; - - final Observable<String> odds = Observable.toObservable(o); - final Observable<String> even = Observable.toObservable(e); - final List<Observable<String>> list = new ArrayList<Observable<String>>(); - list.add(odds); - list.add(even); - Observable<String> concat = Observable.create(concat(list)); - concat.subscribe(observer); - Assert.assertEquals(expected.length, index); - - } - - @Test - public void testConcatUnsubscribe() { - final CountDownLatch callOnce = new CountDownLatch(1); - final CountDownLatch okToContinue = new CountDownLatch(1); - final TestObservable w1 = new TestObservable(null, null, "one", "two", "three"); - final TestObservable w2 = new TestObservable(callOnce, okToContinue, "four", "five", "six"); - - @SuppressWarnings("unchecked") - Observer<String> aObserver = mock(Observer.class); - @SuppressWarnings("unchecked") - Observable<String> concat = Observable.create(concat(w1, w2)); - Subscription s1 = concat.subscribe(aObserver); - - try { - //Block main thread to allow observable "w1" to complete and observable "w2" to call onNext once. - callOnce.await(); - s1.unsubscribe(); - //Unblock the observable to continue. - okToContinue.countDown(); - w1.t.join(); - w2.t.join(); - } catch (Exception e) { - e.printStackTrace(); - fail(e.getMessage()); - } - - verify(aObserver, times(1)).onNext("one"); - verify(aObserver, times(1)).onNext("two"); - verify(aObserver, times(1)).onNext("three"); - verify(aObserver, times(1)).onNext("four"); - verify(aObserver, never()).onNext("five"); - verify(aObserver, never()).onNext("six"); - } - - @Test - public void testMergeObservableOfObservables() { - final String[] o = { "1", "3", "5", "7" }; - final String[] e = { "2", "4", "6" }; - - final Observable<String> odds = Observable.toObservable(o); - final Observable<String> even = Observable.toObservable(e); - - Observable<Observable<String>> observableOfObservables = Observable.create(new Func1<Observer<Observable<String>>, Subscription>() { - - @Override - public Subscription call(Observer<Observable<String>> observer) { - // simulate what would happen in an observable - observer.onNext(odds); - observer.onNext(even); - observer.onCompleted(); - - return new Subscription() { - - @Override - public void unsubscribe() { - // unregister ... will never be called here since we are executing synchronously - } - - }; - } - - }); - Observable<String> concat = Observable.create(concat(observableOfObservables)); - concat.subscribe(observer); - Assert.assertEquals(expected.length, index); - } - - private static class TestObservable extends Observable<String> { - - private final Subscription s = new Subscription() { - - @Override - public void unsubscribe() { - subscribed = false; - } - - }; - private final String[] values; - private Thread t = null; - private int count = 0; - private boolean subscribed = true; - private final CountDownLatch once; - private final CountDownLatch okToContinue; - - public TestObservable(CountDownLatch once, CountDownLatch okToContinue, String... values) { - this.values = values; - this.once = once; - this.okToContinue = okToContinue; - } - - @Override - public Subscription subscribe(final Observer<String> observer) { - t = new Thread(new Runnable() { - - @Override - public void run() { - try { - while (count < values.length && subscribed) { - observer.onNext(values[count]); - count++; - //Unblock the main thread to call unsubscribe. - if (null != once) - once.countDown(); - //Block until the main thread has called unsubscribe. - if (null != once) - okToContinue.await(); - } - if (subscribed) - observer.onCompleted(); - } catch (InterruptedException e) { - e.printStackTrace(); - fail(e.getMessage()); - } - } - - }); - t.start(); - return s; - } - - } - - } -} \ No newline at end of file diff --git a/rxjava-core/src/main/java/rx/operators/OperationFilter.java b/rxjava-core/src/main/java/rx/operators/OperationFilter.java deleted file mode 100644 index 3d6c0ffb79..0000000000 --- a/rxjava-core/src/main/java/rx/operators/OperationFilter.java +++ /dev/null @@ -1,96 +0,0 @@ -/** - * Copyright 2013 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package rx.operators; - -import static org.mockito.Matchers.*; -import static org.mockito.Mockito.*; - -import org.junit.Test; -import org.mockito.Mockito; - -import rx.Observable; -import rx.Observer; -import rx.Subscription; -import rx.util.AtomicObservableSubscription; -import rx.util.functions.Func1; - -public final class OperationFilter<T> { - - public static <T> Func1<Observer<T>, Subscription> filter(Observable<T> that, Func1<T, Boolean> predicate) { - return new Filter<T>(that, predicate); - } - - private static class Filter<T> implements Func1<Observer<T>, Subscription> { - - private final Observable<T> that; - private final Func1<T, Boolean> predicate; - private final AtomicObservableSubscription subscription = new AtomicObservableSubscription(); - - public Filter(Observable<T> that, Func1<T, Boolean> predicate) { - this.that = that; - this.predicate = predicate; - } - - public Subscription call(final Observer<T> observer) { - return subscription.wrap(that.subscribe(new Observer<T>() { - public void onNext(T value) { - try { - if (predicate.call(value)) { - observer.onNext(value); - } - } catch (Exception ex) { - observer.onError(ex); - // this will work if the sequence is asynchronous, it will have no effect on a synchronous observable - subscription.unsubscribe(); - } - } - - public void onError(Exception ex) { - observer.onError(ex); - } - - public void onCompleted() { - observer.onCompleted(); - } - })); - } - - } - - public static class UnitTest { - - @Test - public void testFilter() { - Observable<String> w = Observable.toObservable("one", "two", "three"); - Observable<String> observable = Observable.create(filter(w, new Func1<String, Boolean>() { - - @Override - public Boolean call(String t1) { - return t1.equals("two"); - } - })); - - @SuppressWarnings("unchecked") - Observer<String> aObserver = mock(Observer.class); - observable.subscribe(aObserver); - verify(aObserver, Mockito.never()).onNext("one"); - verify(aObserver, times(1)).onNext("two"); - verify(aObserver, Mockito.never()).onNext("three"); - verify(aObserver, Mockito.never()).onError(any(Exception.class)); - verify(aObserver, times(1)).onCompleted(); - } - } -} \ No newline at end of file diff --git a/rxjava-core/src/main/java/rx/operators/OperationLast.java b/rxjava-core/src/main/java/rx/operators/OperationLast.java deleted file mode 100644 index 6b83a74263..0000000000 --- a/rxjava-core/src/main/java/rx/operators/OperationLast.java +++ /dev/null @@ -1,91 +0,0 @@ -/** - * Copyright 2013 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package rx.operators; - -import static org.mockito.Matchers.*; -import static org.mockito.Mockito.*; - -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicReference; - -import org.junit.Test; -import org.mockito.Mockito; - -import rx.Observable; -import rx.Observer; -import rx.Subscription; -import rx.util.functions.Func1; - -/** - * Returns the last element of an observable sequence. - * - * @param <T> - */ -public final class OperationLast<T> { - - public static <T> Func1<Observer<T>, Subscription> last(Observable<T> observable) { - return new Last<T>(observable); - } - - private static class Last<T> implements Func1<Observer<T>, Subscription> { - - private final AtomicReference<T> lastValue = new AtomicReference<T>(); - private final Observable<T> that; - private final AtomicBoolean onNextCalled = new AtomicBoolean(false); - - public Last(Observable<T> that) { - this.that = that; - } - - public Subscription call(final Observer<T> observer) { - return that.subscribe(new Observer<T>() { - public void onNext(T value) { - onNextCalled.set(true); - lastValue.set(value); - } - - public void onError(Exception ex) { - observer.onError(ex); - } - - public void onCompleted() { - if (onNextCalled.get()) { - observer.onNext(lastValue.get()); - } - observer.onCompleted(); - } - }); - } - } - - public static class UnitTest { - - @Test - public void testLast() { - Observable<String> w = Observable.toObservable("one", "two", "three"); - Observable<String> observable = Observable.create(last(w)); - - @SuppressWarnings("unchecked") - Observer<String> aObserver = mock(Observer.class); - observable.subscribe(aObserver); - verify(aObserver, Mockito.never()).onNext("one"); - verify(aObserver, Mockito.never()).onNext("two"); - verify(aObserver, times(1)).onNext("three"); - verify(aObserver, Mockito.never()).onError(any(Exception.class)); - verify(aObserver, times(1)).onCompleted(); - } - } -} \ No newline at end of file diff --git a/rxjava-core/src/main/java/rx/operators/OperationMap.java b/rxjava-core/src/main/java/rx/operators/OperationMap.java deleted file mode 100644 index 11aabc8280..0000000000 --- a/rxjava-core/src/main/java/rx/operators/OperationMap.java +++ /dev/null @@ -1,256 +0,0 @@ -/** - * Copyright 2013 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package rx.operators; - -import static org.mockito.Matchers.*; -import static org.mockito.Mockito.*; - -import java.util.HashMap; -import java.util.Map; - -import org.junit.Before; -import org.junit.Test; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; - -import rx.Observable; -import rx.Observer; -import rx.Subscription; -import rx.util.functions.Func1; - -public final class OperationMap { - - /** - * Accepts a sequence and a transformation function. Returns a sequence that is the result of - * applying the transformation function to each item in the sequence. - * - * @param sequence - * the input sequence. - * @param func - * a function to apply to each item in the sequence. - * @param <T> - * the type of the input sequence. - * @param <R> - * the type of the output sequence. - * @return a sequence that is the result of applying the transformation function to each item in the input sequence. - */ - public static <T, R> Func1<Observer<R>, Subscription> map(Observable<T> sequence, Func1<T, R> func) { - return new MapObservable<T, R>(sequence, func); - } - - /** - * Accepts a sequence of observable sequences and a transformation function. Returns a flattened sequence that is the result of - * applying the transformation function to each item in the sequence of each observable sequence. - * <p> - * The closure should return an Observable which will then be merged. - * - * @param sequence - * the input sequence. - * @param func - * a function to apply to each item in the sequence. - * @param <T> - * the type of the input sequence. - * @param <R> - * the type of the output sequence. - * @return a sequence that is the result of applying the transformation function to each item in the input sequence. - */ - public static <T, R> Func1<Observer<R>, Subscription> mapMany(Observable<T> sequence, Func1<T, Observable<R>> func) { - return OperationMerge.merge(Observable.create(map(sequence, func))); - } - - /** - * An observable sequence that is the result of applying a transformation to each item in an input sequence. - * - * @param <T> - * the type of the input sequence. - * @param <R> - * the type of the output sequence. - */ - private static class MapObservable<T, R> implements Func1<Observer<R>, Subscription> { - public MapObservable(Observable<T> sequence, Func1<T, R> func) { - this.sequence = sequence; - this.func = func; - } - - private Observable<T> sequence; - - private Func1<T, R> func; - - public Subscription call(Observer<R> observer) { - return sequence.subscribe(new MapObserver<T, R>(observer, func)); - } - } - - /** - * An observer that applies a transformation function to each item and forwards the result to an inner observer. - * - * @param <T> - * the type of the observer items. - * @param <R> - * the type of the inner observer items. - */ - private static class MapObserver<T, R> implements Observer<T> { - public MapObserver(Observer<R> observer, Func1<T, R> func) { - this.observer = observer; - this.func = func; - } - - Observer<R> observer; - - Func1<T, R> func; - - public void onNext(T value) { - try { - observer.onNext(func.call(value)); - } catch (Exception ex) { - observer.onError(ex); - } - } - - public void onError(Exception ex) { - observer.onError(ex); - } - - public void onCompleted() { - observer.onCompleted(); - } - } - - public static class UnitTest { - @Mock - Observer<String> stringObserver; - - @Before - public void before() { - MockitoAnnotations.initMocks(this); - } - - @Test - public void testMap() { - Map<String, String> m1 = getMap("One"); - Map<String, String> m2 = getMap("Two"); - @SuppressWarnings("unchecked") - Observable<Map<String, String>> observable = Observable.toObservable(m1, m2); - - Observable<String> m = Observable.create(map(observable, new Func1<Map<String, String>, String>() { - - @Override - public String call(Map<String, String> map) { - return map.get("firstName"); - } - - })); - m.subscribe(stringObserver); - - verify(stringObserver, never()).onError(any(Exception.class)); - verify(stringObserver, times(1)).onNext("OneFirst"); - verify(stringObserver, times(1)).onNext("TwoFirst"); - verify(stringObserver, times(1)).onCompleted(); - - } - - @Test - public void testMapMany() { - /* simulate a top-level async call which returns IDs */ - Observable<Integer> ids = Observable.toObservable(1, 2); - - /* now simulate the behavior to take those IDs and perform nested async calls based on them */ - Observable<String> m = Observable.create(mapMany(ids, new Func1<Integer, Observable<String>>() { - - @SuppressWarnings("unchecked") - @Override - public Observable<String> call(Integer id) { - /* simulate making a nested async call which creates another Observable */ - Observable<Map<String, String>> subObservable = null; - if (id == 1) { - Map<String, String> m1 = getMap("One"); - Map<String, String> m2 = getMap("Two"); - subObservable = Observable.toObservable(m1, m2); - } else { - Map<String, String> m3 = getMap("Three"); - Map<String, String> m4 = getMap("Four"); - subObservable = Observable.toObservable(m3, m4); - } - - /* simulate kicking off the async call and performing a select on it to transform the data */ - return Observable.create(map(subObservable, new Func1<Map<String, String>, String>() { - @Override - public String call(Map<String, String> map) { - return map.get("firstName"); - } - })); - } - - })); - m.subscribe(stringObserver); - - verify(stringObserver, never()).onError(any(Exception.class)); - verify(stringObserver, times(1)).onNext("OneFirst"); - verify(stringObserver, times(1)).onNext("TwoFirst"); - verify(stringObserver, times(1)).onNext("ThreeFirst"); - verify(stringObserver, times(1)).onNext("FourFirst"); - verify(stringObserver, times(1)).onCompleted(); - } - - @Test - public void testMapMany2() { - Map<String, String> m1 = getMap("One"); - Map<String, String> m2 = getMap("Two"); - @SuppressWarnings("unchecked") - Observable<Map<String, String>> observable1 = Observable.toObservable(m1, m2); - - Map<String, String> m3 = getMap("Three"); - Map<String, String> m4 = getMap("Four"); - @SuppressWarnings("unchecked") - Observable<Map<String, String>> observable2 = Observable.toObservable(m3, m4); - - @SuppressWarnings("unchecked") - Observable<Observable<Map<String, String>>> observable = Observable.toObservable(observable1, observable2); - - Observable<String> m = Observable.create(mapMany(observable, new Func1<Observable<Map<String, String>>, Observable<String>>() { - - @Override - public Observable<String> call(Observable<Map<String, String>> o) { - return Observable.create(map(o, new Func1<Map<String, String>, String>() { - - @Override - public String call(Map<String, String> map) { - return map.get("firstName"); - } - })); - } - - })); - m.subscribe(stringObserver); - - verify(stringObserver, never()).onError(any(Exception.class)); - verify(stringObserver, times(1)).onNext("OneFirst"); - verify(stringObserver, times(1)).onNext("TwoFirst"); - verify(stringObserver, times(1)).onNext("ThreeFirst"); - verify(stringObserver, times(1)).onNext("FourFirst"); - verify(stringObserver, times(1)).onCompleted(); - - } - - private Map<String, String> getMap(String prefix) { - Map<String, String> m = new HashMap<String, String>(); - m.put("firstName", prefix + "First"); - m.put("lastName", prefix + "Last"); - return m; - } - - } -} diff --git a/rxjava-core/src/main/java/rx/operators/OperationMaterialize.java b/rxjava-core/src/main/java/rx/operators/OperationMaterialize.java deleted file mode 100644 index 809e23ec3e..0000000000 --- a/rxjava-core/src/main/java/rx/operators/OperationMaterialize.java +++ /dev/null @@ -1,233 +0,0 @@ -/** - * Copyright 2013 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package rx.operators; - -import static org.junit.Assert.*; - -import java.util.List; -import java.util.Vector; - -import org.junit.Test; - -import rx.Notification; -import rx.Observable; -import rx.Observer; -import rx.Subscription; -import rx.util.functions.Func1; - -/** - * Materializes the implicit notifications of an observable sequence as explicit notification values. - * <p> - * In other words, converts a sequence of OnNext, OnError and OnCompleted events into a sequence of ObservableNotifications containing the OnNext, OnError and OnCompleted values. - * <p> - * See http://msdn.microsoft.com/en-us/library/hh229453(v=VS.103).aspx for the Microsoft Rx equivalent. - */ -public final class OperationMaterialize { - - /** - * Materializes the implicit notifications of an observable sequence as explicit notification values. - * - * @param source - * An observable sequence of elements to project. - * @return An observable sequence whose elements are the result of materializing the notifications of the given sequence. - * @see http://msdn.microsoft.com/en-us/library/hh229453(v=VS.103).aspx - */ - public static <T> Func1<Observer<Notification<T>>, Subscription> materialize(final Observable<T> sequence) { - return new MaterializeObservable<T>(sequence); - } - - private static class MaterializeObservable<T> implements Func1<Observer<Notification<T>>, Subscription> { - - private final Observable<T> sequence; - - public MaterializeObservable(Observable<T> sequence) { - this.sequence = sequence; - } - - @Override - public Subscription call(final Observer<Notification<T>> observer) { - return sequence.subscribe(new Observer<T>() { - - @Override - public void onCompleted() { - observer.onNext(new Notification<T>()); - observer.onCompleted(); - } - - @Override - public void onError(Exception e) { - observer.onNext(new Notification<T>(e)); - observer.onCompleted(); - } - - @Override - public void onNext(T value) { - observer.onNext(new Notification<T>(value)); - } - - }); - } - - } - - public static class UnitTest { - @Test - public void testMaterialize1() { - // null will cause onError to be triggered before "three" can be returned - final TestAsyncErrorObservable o1 = new TestAsyncErrorObservable("one", "two", null, "three"); - - TestObserver Observer = new TestObserver(); - Observable<Notification<String>> m = Observable.create(materialize(o1)); - m.subscribe(Observer); - - try { - o1.t.join(); - } catch (InterruptedException e) { - throw new RuntimeException(e); - } - - assertFalse(Observer.onError); - assertTrue(Observer.onCompleted); - assertEquals(3, Observer.notifications.size()); - assertEquals("one", Observer.notifications.get(0).getValue()); - assertTrue(Observer.notifications.get(0).isOnNext()); - assertEquals("two", Observer.notifications.get(1).getValue()); - assertTrue(Observer.notifications.get(1).isOnNext()); - assertEquals(NullPointerException.class, Observer.notifications.get(2).getException().getClass()); - assertTrue(Observer.notifications.get(2).isOnError()); - } - - @Test - public void testMaterialize2() { - final TestAsyncErrorObservable o1 = new TestAsyncErrorObservable("one", "two", "three"); - - TestObserver Observer = new TestObserver(); - Observable<Notification<String>> m = Observable.create(materialize(o1)); - m.subscribe(Observer); - - try { - o1.t.join(); - } catch (InterruptedException e) { - throw new RuntimeException(e); - } - - assertFalse(Observer.onError); - assertTrue(Observer.onCompleted); - assertEquals(4, Observer.notifications.size()); - assertEquals("one", Observer.notifications.get(0).getValue()); - assertTrue(Observer.notifications.get(0).isOnNext()); - assertEquals("two", Observer.notifications.get(1).getValue()); - assertTrue(Observer.notifications.get(1).isOnNext()); - assertEquals("three", Observer.notifications.get(2).getValue()); - assertTrue(Observer.notifications.get(2).isOnNext()); - assertTrue(Observer.notifications.get(3).isOnCompleted()); - } - - @Test - public void testMultipleSubscribes() { - final TestAsyncErrorObservable o1 = new TestAsyncErrorObservable("one", "two", null, "three"); - - Observable<Notification<String>> m = Observable.create(materialize(o1)); - - TestObserver Observer1 = new TestObserver(); - m.subscribe(Observer1); - - TestObserver Observer2 = new TestObserver(); - m.subscribe(Observer2); - - try { - o1.t.join(); - } catch (InterruptedException e) { - throw new RuntimeException(e); - } - - assertEquals(3, Observer1.notifications.size()); - assertEquals(3, Observer2.notifications.size()); - } - - } - - private static class TestObserver implements Observer<Notification<String>> { - - boolean onCompleted = false; - boolean onError = false; - List<Notification<String>> notifications = new Vector<Notification<String>>(); - - @Override - public void onCompleted() { - this.onCompleted = true; - } - - @Override - public void onError(Exception e) { - this.onError = true; - } - - @Override - public void onNext(Notification<String> value) { - this.notifications.add(value); - } - - } - - private static class TestAsyncErrorObservable extends Observable<String> { - - String[] valuesToReturn; - - TestAsyncErrorObservable(String... values) { - valuesToReturn = values; - } - - Thread t; - - @Override - public Subscription subscribe(final Observer<String> observer) { - t = new Thread(new Runnable() { - - @Override - public void run() { - for (String s : valuesToReturn) { - if (s == null) { - System.out.println("throwing exception"); - try { - Thread.sleep(100); - } catch (Exception e) { - - } - observer.onError(new NullPointerException()); - return; - } else { - observer.onNext(s); - } - } - System.out.println("subscription complete"); - observer.onCompleted(); - } - - }); - t.start(); - - return new Subscription() { - - @Override - public void unsubscribe() { - - } - - }; - } - } -} diff --git a/rxjava-core/src/main/java/rx/operators/OperationMerge.java b/rxjava-core/src/main/java/rx/operators/OperationMerge.java deleted file mode 100644 index d9e2d61504..0000000000 --- a/rxjava-core/src/main/java/rx/operators/OperationMerge.java +++ /dev/null @@ -1,576 +0,0 @@ -/** - * Copyright 2013 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package rx.operators; - -import static org.junit.Assert.*; -import static org.mockito.Matchers.*; -import static org.mockito.Mockito.*; - -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.atomic.AtomicBoolean; - -import org.junit.Before; -import org.junit.Test; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; - -import rx.Observable; -import rx.Observer; -import rx.Subscription; -import rx.util.functions.Func1; - -public final class OperationMerge { - - /** - * Flattens the observable sequences from the list of Observables into one observable sequence without any transformation. - * - * @param source - * An observable sequence of elements to project. - * @return An observable sequence whose elements are the result of flattening the output from the list of Observables. - * @see http://msdn.microsoft.com/en-us/library/hh229099(v=vs.103).aspx - */ - public static <T> Func1<Observer<T>, Subscription> merge(final Observable<Observable<T>> o) { - // wrap in a Func so that if a chain is built up, then asynchronously subscribed to twice we will have 2 instances of Take<T> rather than 1 handing both, which is not thread-safe. - return new Func1<Observer<T>, Subscription>() { - - @Override - public Subscription call(Observer<T> observer) { - return new MergeObservable<T>(o).call(observer); - } - }; - } - - public static <T> Func1<Observer<T>, Subscription> merge(final Observable<T>... sequences) { - return merge(Observable.create(new Func1<Observer<Observable<T>>, Subscription>() { - private volatile boolean unsubscribed = false; - - @Override - public Subscription call(Observer<Observable<T>> observer) { - for (Observable<T> o : sequences) { - if (!unsubscribed) { - observer.onNext(o); - } else { - // break out of the loop if we are unsubscribed - break; - } - } - if (!unsubscribed) { - observer.onCompleted(); - } - return new Subscription() { - - @Override - public void unsubscribe() { - unsubscribed = true; - } - - }; - } - })); - } - - public static <T> Func1<Observer<T>, Subscription> merge(final List<Observable<T>> sequences) { - return merge(Observable.create(new Func1<Observer<Observable<T>>, Subscription>() { - - private volatile boolean unsubscribed = false; - - @Override - public Subscription call(Observer<Observable<T>> observer) { - for (Observable<T> o : sequences) { - if (!unsubscribed) { - observer.onNext(o); - } else { - // break out of the loop if we are unsubscribed - break; - } - } - if (!unsubscribed) { - observer.onCompleted(); - } - - return new Subscription() { - - @Override - public void unsubscribe() { - unsubscribed = true; - } - - }; - } - })); - } - - /** - * This class is NOT thread-safe if invoked and referenced multiple times. In other words, don't subscribe to it multiple times from different threads. - * <p> - * It IS thread-safe from within it while receiving onNext events from multiple threads. - * <p> - * This should all be fine as long as it's kept as a private class and a new instance created from static factory method above. - * <p> - * Note how the take() factory method above protects us from a single instance being exposed with the Observable wrapper handling the subscribe flow. - * - * @param <T> - */ - private static final class MergeObservable<T> implements Func1<Observer<T>, Subscription> { - private final Observable<Observable<T>> sequences; - private final MergeSubscription ourSubscription = new MergeSubscription(); - private AtomicBoolean stopped = new AtomicBoolean(false); - private volatile boolean parentCompleted = false; - private final ConcurrentHashMap<ChildObserver, ChildObserver> childObservers = new ConcurrentHashMap<ChildObserver, ChildObserver>(); - private final ConcurrentHashMap<ChildObserver, Subscription> childSubscriptions = new ConcurrentHashMap<ChildObserver, Subscription>(); - - private MergeObservable(Observable<Observable<T>> sequences) { - this.sequences = sequences; - } - - public MergeSubscription call(Observer<T> actualObserver) { - /** - * Subscribe to the parent Observable to get to the children Observables - */ - sequences.subscribe(new ParentObserver(actualObserver)); - - /* return our subscription to allow unsubscribing */ - return ourSubscription; - } - - /** - * Manage the internal subscription with a thread-safe means of stopping/unsubscribing so we don't unsubscribe twice. - * <p> - * Also has the stop() method returning a boolean so callers know if their thread "won" and should perform further actions. - */ - private class MergeSubscription implements Subscription { - - @Override - public void unsubscribe() { - stop(); - } - - public boolean stop() { - // try setting to false unless another thread beat us - boolean didSet = stopped.compareAndSet(false, true); - if (didSet) { - // this thread won the race to stop, so unsubscribe from the actualSubscription - for (Subscription _s : childSubscriptions.values()) { - _s.unsubscribe(); - } - return true; - } else { - // another thread beat us - return false; - } - } - } - - /** - * Subscribe to the top level Observable to receive the sequence of Observable<T> children. - * - * @param <T> - */ - private class ParentObserver implements Observer<Observable<T>> { - private final Observer<T> actualObserver; - - public ParentObserver(Observer<T> actualObserver) { - this.actualObserver = actualObserver; - } - - @Override - public void onCompleted() { - parentCompleted = true; - // this *can* occur before the children are done, so if it does we won't send onCompleted - // but will let the child worry about it - // if however this completes and there are no children processing, then we will send onCompleted - - if (childObservers.size() == 0) { - if (!stopped.get()) { - if (ourSubscription.stop()) { - actualObserver.onCompleted(); - } - } - } - } - - @Override - public void onError(Exception e) { - actualObserver.onError(e); - } - - @Override - public void onNext(Observable<T> childObservable) { - if (stopped.get()) { - // we won't act on any further items - return; - } - - if (childObservable == null) { - throw new IllegalArgumentException("Observable<T> can not be null."); - } - - /** - * For each child Observable we receive we'll subscribe with a separate Observer - * that will each then forward their sequences to the actualObserver. - * <p> - * We use separate child Observers for each sequence to simplify the onComplete/onError handling so each sequence has its own lifecycle. - */ - ChildObserver _w = new ChildObserver(actualObserver); - childObservers.put(_w, _w); - Subscription _subscription = childObservable.subscribe(_w); - // remember this Observer and the subscription from it - childSubscriptions.put(_w, _subscription); - } - } - - /** - * Subscribe to each child Observable<T> and forward their sequence of data to the actualObserver - * - */ - private class ChildObserver implements Observer<T> { - - private final Observer<T> actualObserver; - - public ChildObserver(Observer<T> actualObserver) { - this.actualObserver = actualObserver; - } - - @Override - public void onCompleted() { - // remove self from map of Observers - childObservers.remove(this); - // if there are now 0 Observers left, so if the parent is also completed we send the onComplete to the actualObserver - // if the parent is not complete that means there is another sequence (and child Observer) to come - if (!stopped.get()) { - if (childObservers.size() == 0 && parentCompleted) { - if (ourSubscription.stop()) { - // this thread 'won' the race to unsubscribe/stop so let's send onCompleted - actualObserver.onCompleted(); - } - } - } - } - - @Override - public void onError(Exception e) { - if (!stopped.get()) { - if (ourSubscription.stop()) { - // this thread 'won' the race to unsubscribe/stop so let's send the error - actualObserver.onError(e); - } - } - } - - @Override - public void onNext(T args) { - // in case the Observable is poorly behaved and doesn't listen to the unsubscribe request - // we'll ignore anything that comes in after we've unsubscribed - if (!stopped.get()) { - actualObserver.onNext(args); - } - } - - } - } - - public static class UnitTest { - @Mock - Observer<String> stringObserver; - - @Before - public void before() { - MockitoAnnotations.initMocks(this); - } - - @Test - public void testMergeObservableOfObservables() { - final Observable<String> o1 = new TestSynchronousObservable(); - final Observable<String> o2 = new TestSynchronousObservable(); - - Observable<Observable<String>> observableOfObservables = Observable.create(new Func1<Observer<Observable<String>>, Subscription>() { - - @Override - public Subscription call(Observer<Observable<String>> observer) { - // simulate what would happen in an observable - observer.onNext(o1); - observer.onNext(o2); - observer.onCompleted(); - - return new Subscription() { - - @Override - public void unsubscribe() { - // unregister ... will never be called here since we are executing synchronously - } - - }; - } - - }); - Observable<String> m = Observable.create(merge(observableOfObservables)); - m.subscribe(stringObserver); - - verify(stringObserver, never()).onError(any(Exception.class)); - verify(stringObserver, times(1)).onCompleted(); - verify(stringObserver, times(2)).onNext("hello"); - } - - @Test - public void testMergeArray() { - final Observable<String> o1 = new TestSynchronousObservable(); - final Observable<String> o2 = new TestSynchronousObservable(); - - @SuppressWarnings("unchecked") - Observable<String> m = Observable.create(merge(o1, o2)); - m.subscribe(stringObserver); - - verify(stringObserver, never()).onError(any(Exception.class)); - verify(stringObserver, times(2)).onNext("hello"); - verify(stringObserver, times(1)).onCompleted(); - } - - @Test - public void testMergeList() { - final Observable<String> o1 = new TestSynchronousObservable(); - final Observable<String> o2 = new TestSynchronousObservable(); - List<Observable<String>> listOfObservables = new ArrayList<Observable<String>>(); - listOfObservables.add(o1); - listOfObservables.add(o2); - - Observable<String> m = Observable.create(merge(listOfObservables)); - m.subscribe(stringObserver); - - verify(stringObserver, never()).onError(any(Exception.class)); - verify(stringObserver, times(1)).onCompleted(); - verify(stringObserver, times(2)).onNext("hello"); - } - - @Test - public void testUnSubscribe() { - TestObservable tA = new TestObservable(); - TestObservable tB = new TestObservable(); - - @SuppressWarnings("unchecked") - Observable<String> m = Observable.create(merge(tA, tB)); - Subscription s = m.subscribe(stringObserver); - - tA.sendOnNext("Aone"); - tB.sendOnNext("Bone"); - s.unsubscribe(); - tA.sendOnNext("Atwo"); - tB.sendOnNext("Btwo"); - tA.sendOnCompleted(); - tB.sendOnCompleted(); - - verify(stringObserver, never()).onError(any(Exception.class)); - verify(stringObserver, times(1)).onNext("Aone"); - verify(stringObserver, times(1)).onNext("Bone"); - assertTrue(tA.unsubscribed); - assertTrue(tB.unsubscribed); - verify(stringObserver, never()).onNext("Atwo"); - verify(stringObserver, never()).onNext("Btwo"); - verify(stringObserver, never()).onCompleted(); - } - - @Test - public void testMergeArrayWithThreading() { - final TestASynchronousObservable o1 = new TestASynchronousObservable(); - final TestASynchronousObservable o2 = new TestASynchronousObservable(); - - @SuppressWarnings("unchecked") - Observable<String> m = Observable.create(merge(o1, o2)); - m.subscribe(stringObserver); - - try { - o1.t.join(); - o2.t.join(); - } catch (InterruptedException e) { - throw new RuntimeException(e); - } - - verify(stringObserver, never()).onError(any(Exception.class)); - verify(stringObserver, times(2)).onNext("hello"); - verify(stringObserver, times(1)).onCompleted(); - } - - /** - * unit test from OperationMergeDelayError backported here to show how these use cases work with normal merge - */ - @Test - public void testError1() { - // we are using synchronous execution to test this exactly rather than non-deterministic concurrent behavior - final Observable<String> o1 = new TestErrorObservable("four", null, "six"); // we expect to lose "six" - final Observable<String> o2 = new TestErrorObservable("one", "two", "three"); // we expect to lose all of these since o1 is done first and fails - - @SuppressWarnings("unchecked") - Observable<String> m = Observable.create(merge(o1, o2)); - m.subscribe(stringObserver); - - verify(stringObserver, times(1)).onError(any(NullPointerException.class)); - verify(stringObserver, never()).onCompleted(); - verify(stringObserver, times(0)).onNext("one"); - verify(stringObserver, times(0)).onNext("two"); - verify(stringObserver, times(0)).onNext("three"); - verify(stringObserver, times(1)).onNext("four"); - verify(stringObserver, times(0)).onNext("five"); - verify(stringObserver, times(0)).onNext("six"); - } - - /** - * unit test from OperationMergeDelayError backported here to show how these use cases work with normal merge - */ - @Test - public void testError2() { - // we are using synchronous execution to test this exactly rather than non-deterministic concurrent behavior - final Observable<String> o1 = new TestErrorObservable("one", "two", "three"); - final Observable<String> o2 = new TestErrorObservable("four", null, "six"); // we expect to lose "six" - final Observable<String> o3 = new TestErrorObservable("seven", "eight", null);// we expect to lose all of these since o2 is done first and fails - final Observable<String> o4 = new TestErrorObservable("nine");// we expect to lose all of these since o2 is done first and fails - - @SuppressWarnings("unchecked") - Observable<String> m = Observable.create(merge(o1, o2, o3, o4)); - m.subscribe(stringObserver); - - verify(stringObserver, times(1)).onError(any(NullPointerException.class)); - verify(stringObserver, never()).onCompleted(); - verify(stringObserver, times(1)).onNext("one"); - verify(stringObserver, times(1)).onNext("two"); - verify(stringObserver, times(1)).onNext("three"); - verify(stringObserver, times(1)).onNext("four"); - verify(stringObserver, times(0)).onNext("five"); - verify(stringObserver, times(0)).onNext("six"); - verify(stringObserver, times(0)).onNext("seven"); - verify(stringObserver, times(0)).onNext("eight"); - verify(stringObserver, times(0)).onNext("nine"); - } - - private static class TestSynchronousObservable extends Observable<String> { - - @Override - public Subscription subscribe(Observer<String> observer) { - - observer.onNext("hello"); - observer.onCompleted(); - - return new Subscription() { - - @Override - public void unsubscribe() { - // unregister ... will never be called here since we are executing synchronously - } - - }; - } - } - - private static class TestASynchronousObservable extends Observable<String> { - Thread t; - - @Override - public Subscription subscribe(final Observer<String> observer) { - t = new Thread(new Runnable() { - - @Override - public void run() { - observer.onNext("hello"); - observer.onCompleted(); - } - - }); - t.start(); - - return new Subscription() { - - @Override - public void unsubscribe() { - - } - - }; - } - } - - /** - * A Observable that doesn't do the right thing on UnSubscribe/Error/etc in that it will keep sending events down the pipe regardless of what happens. - */ - private static class TestObservable extends Observable<String> { - - Observer<String> observer = null; - volatile boolean unsubscribed = false; - Subscription s = new Subscription() { - - @Override - public void unsubscribe() { - unsubscribed = true; - - } - - }; - - /* used to simulate subscription */ - public void sendOnCompleted() { - observer.onCompleted(); - } - - /* used to simulate subscription */ - public void sendOnNext(String value) { - observer.onNext(value); - } - - /* used to simulate subscription */ - @SuppressWarnings("unused") - public void sendOnError(Exception e) { - observer.onError(e); - } - - @Override - public Subscription subscribe(final Observer<String> observer) { - this.observer = observer; - return s; - } - } - - private static class TestErrorObservable extends Observable<String> { - - String[] valuesToReturn; - - TestErrorObservable(String... values) { - valuesToReturn = values; - } - - @Override - public Subscription subscribe(Observer<String> observer) { - - for (String s : valuesToReturn) { - if (s == null) { - System.out.println("throwing exception"); - observer.onError(new NullPointerException()); - } else { - observer.onNext(s); - } - } - observer.onCompleted(); - - return new Subscription() { - - @Override - public void unsubscribe() { - // unregister ... will never be called here since we are executing synchronously - } - - }; - } - } - } -} diff --git a/rxjava-core/src/main/java/rx/operators/OperationMergeDelayError.java b/rxjava-core/src/main/java/rx/operators/OperationMergeDelayError.java deleted file mode 100644 index c110c872f4..0000000000 --- a/rxjava-core/src/main/java/rx/operators/OperationMergeDelayError.java +++ /dev/null @@ -1,817 +0,0 @@ -/** - * Copyright 2013 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package rx.operators; - -import static org.junit.Assert.*; -import static org.mockito.Matchers.*; -import static org.mockito.Mockito.*; - -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentLinkedQueue; -import java.util.concurrent.atomic.AtomicBoolean; - -import org.junit.Before; -import org.junit.Test; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; - -import rx.Observable; -import rx.Observer; -import rx.Subscription; -import rx.util.CompositeException; -import rx.util.functions.Func1; - -/** - * Same functionality as OperationMerge except that onError events will be skipped so that all onNext calls are passed on until all sequences finish with onComplete or onError, and then the first - * onError received (if any) will be passed on. - * <p> - * This allows retrieving all successful onNext calls without being blocked by an onError early in a sequence. - * <p> - * NOTE: If this is used on an infinite stream it will never call onError and effectively will swallow errors. - */ -public final class OperationMergeDelayError { - - /** - * Flattens the observable sequences from the list of Observables into one observable sequence without any transformation and delays any onError calls until after all sequences have called - * onError or onComplete so as to allow all successful - * onNext calls to be received. - * - * @param source - * An observable sequence of elements to project. - * @return An observable sequence whose elements are the result of flattening the output from the list of Observables. - * @see http://msdn.microsoft.com/en-us/library/hh229099(v=vs.103).aspx - */ - public static <T> Func1<Observer<T>, Subscription> mergeDelayError(final Observable<Observable<T>> sequences) { - // wrap in a Func so that if a chain is built up, then asynchronously subscribed to twice we will have 2 instances of Take<T> rather than 1 handing both, which is not thread-safe. - return new Func1<Observer<T>, Subscription>() { - - @Override - public Subscription call(Observer<T> observer) { - return new MergeDelayErrorObservable<T>(sequences).call(observer); - } - }; - } - - public static <T> Func1<Observer<T>, Subscription> mergeDelayError(final Observable<T>... sequences) { - return mergeDelayError(Observable.create(new Func1<Observer<Observable<T>>, Subscription>() { - private volatile boolean unsubscribed = false; - - @Override - public Subscription call(Observer<Observable<T>> observer) { - for (Observable<T> o : sequences) { - if (!unsubscribed) { - observer.onNext(o); - } else { - // break out of the loop if we are unsubscribed - break; - } - } - if (!unsubscribed) { - observer.onCompleted(); - } - return new Subscription() { - - @Override - public void unsubscribe() { - unsubscribed = true; - } - - }; - } - })); - } - - public static <T> Func1<Observer<T>, Subscription> mergeDelayError(final List<Observable<T>> sequences) { - return mergeDelayError(Observable.create(new Func1<Observer<Observable<T>>, Subscription>() { - - private volatile boolean unsubscribed = false; - - @Override - public Subscription call(Observer<Observable<T>> observer) { - for (Observable<T> o : sequences) { - if (!unsubscribed) { - observer.onNext(o); - } else { - // break out of the loop if we are unsubscribed - break; - } - } - if (!unsubscribed) { - observer.onCompleted(); - } - - return new Subscription() { - - @Override - public void unsubscribe() { - unsubscribed = true; - } - - }; - } - })); - } - - /** - * This class is NOT thread-safe if invoked and referenced multiple times. In other words, don't subscribe to it multiple times from different threads. - * <p> - * It IS thread-safe from within it while receiving onNext events from multiple threads. - * <p> - * This should all be fine as long as it's kept as a private class and a new instance created from static factory method above. - * <p> - * Note how the take() factory method above protects us from a single instance being exposed with the Observable wrapper handling the subscribe flow. - * - * @param <T> - */ - private static final class MergeDelayErrorObservable<T> implements Func1<Observer<T>, Subscription> { - private final Observable<Observable<T>> sequences; - private final MergeSubscription ourSubscription = new MergeSubscription(); - private AtomicBoolean stopped = new AtomicBoolean(false); - private volatile boolean parentCompleted = false; - private final ConcurrentHashMap<ChildObserver, ChildObserver> childObservers = new ConcurrentHashMap<ChildObserver, ChildObserver>(); - private final ConcurrentHashMap<ChildObserver, Subscription> childSubscriptions = new ConcurrentHashMap<ChildObserver, Subscription>(); - // onErrors we received that will be delayed until everything is completed and then sent - private ConcurrentLinkedQueue<Exception> onErrorReceived = new ConcurrentLinkedQueue<Exception>(); - - private MergeDelayErrorObservable(Observable<Observable<T>> sequences) { - this.sequences = sequences; - } - - public Subscription call(Observer<T> actualObserver) { - /** - * Subscribe to the parent Observable to get to the children Observables - */ - sequences.subscribe(new ParentObserver(actualObserver)); - - /* return our subscription to allow unsubscribing */ - return ourSubscription; - } - - /** - * Manage the internal subscription with a thread-safe means of stopping/unsubscribing so we don't unsubscribe twice. - * <p> - * Also has the stop() method returning a boolean so callers know if their thread "won" and should perform further actions. - */ - private class MergeSubscription implements Subscription { - - @Override - public void unsubscribe() { - stop(); - } - - public boolean stop() { - // try setting to false unless another thread beat us - boolean didSet = stopped.compareAndSet(false, true); - if (didSet) { - // this thread won the race to stop, so unsubscribe from the actualSubscription - for (Subscription _s : childSubscriptions.values()) { - _s.unsubscribe(); - } - return true; - } else { - // another thread beat us - return false; - } - } - } - - /** - * Subscribe to the top level Observable to receive the sequence of Observable<T> children. - * - * @param <T> - */ - private class ParentObserver implements Observer<Observable<T>> { - private final Observer<T> actualObserver; - - public ParentObserver(Observer<T> actualObserver) { - this.actualObserver = actualObserver; - } - - @Override - public void onCompleted() { - parentCompleted = true; - // this *can* occur before the children are done, so if it does we won't send onCompleted - // but will let the child worry about it - // if however this completes and there are no children processing, then we will send onCompleted - - if (childObservers.size() == 0) { - if (!stopped.get()) { - if (ourSubscription.stop()) { - if (onErrorReceived.size() == 1) { - // an onError was received from 1 ChildObserver so we now send it as a delayed error - actualObserver.onError(onErrorReceived.peek()); - } else if (onErrorReceived.size() > 1) { - // an onError was received from more than 1 ChildObserver so we now send it as a delayed error - actualObserver.onError(new CompositeException(onErrorReceived)); - } else { - // no delayed error so send onCompleted - actualObserver.onCompleted(); - } - } - } - } - } - - @Override - public void onError(Exception e) { - actualObserver.onError(e); - } - - @Override - public void onNext(Observable<T> childObservable) { - if (stopped.get()) { - // we won't act on any further items - return; - } - - if (childObservable == null) { - throw new IllegalArgumentException("Observable<T> can not be null."); - } - - /** - * For each child Observable we receive we'll subscribe with a separate Observer - * that will each then forward their sequences to the actualObserver. - * <p> - * We use separate child Observers for each sequence to simplify the onComplete/onError handling so each sequence has its own lifecycle. - */ - ChildObserver _w = new ChildObserver(actualObserver); - childObservers.put(_w, _w); - Subscription _subscription = childObservable.subscribe(_w); - // remember this Observer and the subscription from it - childSubscriptions.put(_w, _subscription); - } - } - - /** - * Subscribe to each child Observable<T> and forward their sequence of data to the actualObserver - * - */ - private class ChildObserver implements Observer<T> { - - private final Observer<T> actualObserver; - private volatile boolean finished = false; - - public ChildObserver(Observer<T> actualObserver) { - this.actualObserver = actualObserver; - } - - @Override - public void onCompleted() { - // remove self from map of Observers - childObservers.remove(this); - // if there are now 0 Observers left, so if the parent is also completed we send the onComplete to the actualObserver - // if the parent is not complete that means there is another sequence (and child Observer) to come - if (!stopped.get()) { - finishObserver(); - } - } - - @Override - public void onError(Exception e) { - if (!stopped.get()) { - onErrorReceived.add(e); - // mark this ChildObserver as done - childObservers.remove(this); - // but do NOT forward to actualObserver as we want other ChildObservers to continue until completion - // and we'll delay the sending of onError until all others are done - - // we mark finished==true as a safety to ensure that if further calls to onNext occur we ignore them - finished = true; - - // check for whether the parent is completed and if so then perform the 'finishing' actions - finishObserver(); - } - } - - /** - * onComplete and onError when called need to check for the parent being complete and if so send the onCompleted or onError to the actualObserver. - * <p> - * This does NOT get invoked if synchronous execution occurs, but will when asynchronously executing. - * <p> - * TestCase testErrorDelayed4WithThreading specifically tests this use case. - */ - private void finishObserver() { - if (childObservers.size() == 0 && parentCompleted) { - if (ourSubscription.stop()) { - // this thread 'won' the race to unsubscribe/stop so let's send onError or onCompleted - if (onErrorReceived.size() == 1) { - // an onError was received from 1 ChildObserver so we now send it as a delayed error - actualObserver.onError(onErrorReceived.peek()); - } else if (onErrorReceived.size() > 1) { - // an onError was received from more than 1 ChildObserver so we now send it as a delayed error - actualObserver.onError(new CompositeException(onErrorReceived)); - } else { - // no delayed error so send onCompleted - actualObserver.onCompleted(); - } - } - } - } - - @Override - public void onNext(T args) { - // in case the Observable is poorly behaved and doesn't listen to the unsubscribe request - // we'll ignore anything that comes in after we've unsubscribed or an onError has been received and delayed - if (!stopped.get() && !finished) { - actualObserver.onNext(args); - } - } - - } - } - - public static class UnitTest { - @Mock - Observer<String> stringObserver; - - @Before - public void before() { - MockitoAnnotations.initMocks(this); - } - - @Test - public void testErrorDelayed1() { - final Observable<String> o1 = new TestErrorObservable("four", null, "six"); // we expect to lose "six" from the source (and it should never be sent by the source since onError was called - final Observable<String> o2 = new TestErrorObservable("one", "two", "three"); - - @SuppressWarnings("unchecked") - Observable<String> m = Observable.create(mergeDelayError(o1, o2)); - m.subscribe(stringObserver); - - verify(stringObserver, times(1)).onError(any(NullPointerException.class)); - verify(stringObserver, never()).onCompleted(); - verify(stringObserver, times(1)).onNext("one"); - verify(stringObserver, times(1)).onNext("two"); - verify(stringObserver, times(1)).onNext("three"); - verify(stringObserver, times(1)).onNext("four"); - verify(stringObserver, times(0)).onNext("five"); - verify(stringObserver, times(0)).onNext("six"); - } - - @Test - public void testErrorDelayed2() { - final Observable<String> o1 = new TestErrorObservable("one", "two", "three"); - final Observable<String> o2 = new TestErrorObservable("four", null, "six"); // we expect to lose "six" from the source (and it should never be sent by the source since onError was called - final Observable<String> o3 = new TestErrorObservable("seven", "eight", null); - final Observable<String> o4 = new TestErrorObservable("nine"); - - @SuppressWarnings("unchecked") - Observable<String> m = Observable.create(mergeDelayError(o1, o2, o3, o4)); - m.subscribe(stringObserver); - - verify(stringObserver, times(1)).onError(any(NullPointerException.class)); - verify(stringObserver, never()).onCompleted(); - verify(stringObserver, times(1)).onNext("one"); - verify(stringObserver, times(1)).onNext("two"); - verify(stringObserver, times(1)).onNext("three"); - verify(stringObserver, times(1)).onNext("four"); - verify(stringObserver, times(0)).onNext("five"); - verify(stringObserver, times(0)).onNext("six"); - verify(stringObserver, times(1)).onNext("seven"); - verify(stringObserver, times(1)).onNext("eight"); - verify(stringObserver, times(1)).onNext("nine"); - } - - @Test - public void testErrorDelayed3() { - final Observable<String> o1 = new TestErrorObservable("one", "two", "three"); - final Observable<String> o2 = new TestErrorObservable("four", "five", "six"); - final Observable<String> o3 = new TestErrorObservable("seven", "eight", null); - final Observable<String> o4 = new TestErrorObservable("nine"); - - @SuppressWarnings("unchecked") - Observable<String> m = Observable.create(mergeDelayError(o1, o2, o3, o4)); - m.subscribe(stringObserver); - - verify(stringObserver, times(1)).onError(any(NullPointerException.class)); - verify(stringObserver, never()).onCompleted(); - verify(stringObserver, times(1)).onNext("one"); - verify(stringObserver, times(1)).onNext("two"); - verify(stringObserver, times(1)).onNext("three"); - verify(stringObserver, times(1)).onNext("four"); - verify(stringObserver, times(1)).onNext("five"); - verify(stringObserver, times(1)).onNext("six"); - verify(stringObserver, times(1)).onNext("seven"); - verify(stringObserver, times(1)).onNext("eight"); - verify(stringObserver, times(1)).onNext("nine"); - } - - @Test - public void testErrorDelayed4() { - final Observable<String> o1 = new TestErrorObservable("one", "two", "three"); - final Observable<String> o2 = new TestErrorObservable("four", "five", "six"); - final Observable<String> o3 = new TestErrorObservable("seven", "eight"); - final Observable<String> o4 = new TestErrorObservable("nine", null); - - @SuppressWarnings("unchecked") - Observable<String> m = Observable.create(mergeDelayError(o1, o2, o3, o4)); - m.subscribe(stringObserver); - - verify(stringObserver, times(1)).onError(any(NullPointerException.class)); - verify(stringObserver, never()).onCompleted(); - verify(stringObserver, times(1)).onNext("one"); - verify(stringObserver, times(1)).onNext("two"); - verify(stringObserver, times(1)).onNext("three"); - verify(stringObserver, times(1)).onNext("four"); - verify(stringObserver, times(1)).onNext("five"); - verify(stringObserver, times(1)).onNext("six"); - verify(stringObserver, times(1)).onNext("seven"); - verify(stringObserver, times(1)).onNext("eight"); - verify(stringObserver, times(1)).onNext("nine"); - } - - @Test - public void testErrorDelayed4WithThreading() { - final TestAsyncErrorObservable o1 = new TestAsyncErrorObservable("one", "two", "three"); - final TestAsyncErrorObservable o2 = new TestAsyncErrorObservable("four", "five", "six"); - final TestAsyncErrorObservable o3 = new TestAsyncErrorObservable("seven", "eight"); - // throw the error at the very end so no onComplete will be called after it - final TestAsyncErrorObservable o4 = new TestAsyncErrorObservable("nine", null); - - @SuppressWarnings("unchecked") - Observable<String> m = Observable.create(mergeDelayError(o1, o2, o3, o4)); - m.subscribe(stringObserver); - - try { - o1.t.join(); - o2.t.join(); - o3.t.join(); - o4.t.join(); - } catch (InterruptedException e) { - throw new RuntimeException(e); - } - - verify(stringObserver, times(1)).onError(any(NullPointerException.class)); - verify(stringObserver, never()).onCompleted(); - verify(stringObserver, times(1)).onNext("one"); - verify(stringObserver, times(1)).onNext("two"); - verify(stringObserver, times(1)).onNext("three"); - verify(stringObserver, times(1)).onNext("four"); - verify(stringObserver, times(1)).onNext("five"); - verify(stringObserver, times(1)).onNext("six"); - verify(stringObserver, times(1)).onNext("seven"); - verify(stringObserver, times(1)).onNext("eight"); - verify(stringObserver, times(1)).onNext("nine"); - } - - @Test - public void testCompositeErrorDelayed1() { - final Observable<String> o1 = new TestErrorObservable("four", null, "six"); // we expect to lose "six" from the source (and it should never be sent by the source since onError was called - final Observable<String> o2 = new TestErrorObservable("one", "two", null); - - @SuppressWarnings("unchecked") - Observable<String> m = Observable.create(mergeDelayError(o1, o2)); - m.subscribe(stringObserver); - - verify(stringObserver, times(1)).onError(any(CompositeException.class)); - verify(stringObserver, never()).onCompleted(); - verify(stringObserver, times(1)).onNext("one"); - verify(stringObserver, times(1)).onNext("two"); - verify(stringObserver, times(0)).onNext("three"); - verify(stringObserver, times(1)).onNext("four"); - verify(stringObserver, times(0)).onNext("five"); - verify(stringObserver, times(0)).onNext("six"); - } - - @Test - public void testCompositeErrorDelayed2() { - final Observable<String> o1 = new TestErrorObservable("four", null, "six"); // we expect to lose "six" from the source (and it should never be sent by the source since onError was called - final Observable<String> o2 = new TestErrorObservable("one", "two", null); - - @SuppressWarnings("unchecked") - Observable<String> m = Observable.create(mergeDelayError(o1, o2)); - CaptureObserver w = new CaptureObserver(); - m.subscribe(w); - - assertNotNull(w.e); - if (w.e instanceof CompositeException) { - assertEquals(2, ((CompositeException) w.e).getExceptions().size()); - w.e.printStackTrace(); - } else { - fail("Expecting CompositeException"); - } - - } - - /** - * The unit tests below are from OperationMerge and should ensure the normal merge functionality is correct. - */ - - @Test - public void testMergeObservableOfObservables() { - final Observable<String> o1 = new TestSynchronousObservable(); - final Observable<String> o2 = new TestSynchronousObservable(); - - Observable<Observable<String>> observableOfObservables = Observable.create(new Func1<Observer<Observable<String>>, Subscription>() { - - @Override - public Subscription call(Observer<Observable<String>> observer) { - // simulate what would happen in an observable - observer.onNext(o1); - observer.onNext(o2); - observer.onCompleted(); - - return new Subscription() { - - @Override - public void unsubscribe() { - // unregister ... will never be called here since we are executing synchronously - } - - }; - } - - }); - Observable<String> m = Observable.create(mergeDelayError(observableOfObservables)); - m.subscribe(stringObserver); - - verify(stringObserver, never()).onError(any(Exception.class)); - verify(stringObserver, times(1)).onCompleted(); - verify(stringObserver, times(2)).onNext("hello"); - } - - @Test - public void testMergeArray() { - final Observable<String> o1 = new TestSynchronousObservable(); - final Observable<String> o2 = new TestSynchronousObservable(); - - @SuppressWarnings("unchecked") - Observable<String> m = Observable.create(mergeDelayError(o1, o2)); - m.subscribe(stringObserver); - - verify(stringObserver, never()).onError(any(Exception.class)); - verify(stringObserver, times(2)).onNext("hello"); - verify(stringObserver, times(1)).onCompleted(); - } - - @Test - public void testMergeList() { - final Observable<String> o1 = new TestSynchronousObservable(); - final Observable<String> o2 = new TestSynchronousObservable(); - List<Observable<String>> listOfObservables = new ArrayList<Observable<String>>(); - listOfObservables.add(o1); - listOfObservables.add(o2); - - Observable<String> m = Observable.create(mergeDelayError(listOfObservables)); - m.subscribe(stringObserver); - - verify(stringObserver, never()).onError(any(Exception.class)); - verify(stringObserver, times(1)).onCompleted(); - verify(stringObserver, times(2)).onNext("hello"); - } - - @Test - public void testUnSubscribe() { - TestObservable tA = new TestObservable(); - TestObservable tB = new TestObservable(); - - @SuppressWarnings("unchecked") - Observable<String> m = Observable.create(mergeDelayError(tA, tB)); - Subscription s = m.subscribe(stringObserver); - - tA.sendOnNext("Aone"); - tB.sendOnNext("Bone"); - s.unsubscribe(); - tA.sendOnNext("Atwo"); - tB.sendOnNext("Btwo"); - tA.sendOnCompleted(); - tB.sendOnCompleted(); - - verify(stringObserver, never()).onError(any(Exception.class)); - verify(stringObserver, times(1)).onNext("Aone"); - verify(stringObserver, times(1)).onNext("Bone"); - assertTrue(tA.unsubscribed); - assertTrue(tB.unsubscribed); - verify(stringObserver, never()).onNext("Atwo"); - verify(stringObserver, never()).onNext("Btwo"); - verify(stringObserver, never()).onCompleted(); - } - - @Test - public void testMergeArrayWithThreading() { - final TestASynchronousObservable o1 = new TestASynchronousObservable(); - final TestASynchronousObservable o2 = new TestASynchronousObservable(); - - @SuppressWarnings("unchecked") - Observable<String> m = Observable.create(mergeDelayError(o1, o2)); - m.subscribe(stringObserver); - - try { - o1.t.join(); - o2.t.join(); - } catch (InterruptedException e) { - throw new RuntimeException(e); - } - - verify(stringObserver, never()).onError(any(Exception.class)); - verify(stringObserver, times(2)).onNext("hello"); - verify(stringObserver, times(1)).onCompleted(); - } - - private static class TestSynchronousObservable extends Observable<String> { - - @Override - public Subscription subscribe(Observer<String> observer) { - - observer.onNext("hello"); - observer.onCompleted(); - - return new Subscription() { - - @Override - public void unsubscribe() { - // unregister ... will never be called here since we are executing synchronously - } - - }; - } - } - - private static class TestASynchronousObservable extends Observable<String> { - Thread t; - - @Override - public Subscription subscribe(final Observer<String> observer) { - t = new Thread(new Runnable() { - - @Override - public void run() { - observer.onNext("hello"); - observer.onCompleted(); - } - - }); - t.start(); - - return new Subscription() { - - @Override - public void unsubscribe() { - - } - - }; - } - } - - /** - * A Observable that doesn't do the right thing on UnSubscribe/Error/etc in that it will keep sending events down the pipe regardless of what happens. - */ - private static class TestObservable extends Observable<String> { - - Observer<String> observer = null; - volatile boolean unsubscribed = false; - Subscription s = new Subscription() { - - @Override - public void unsubscribe() { - unsubscribed = true; - - } - - }; - - /* used to simulate subscription */ - public void sendOnCompleted() { - observer.onCompleted(); - } - - /* used to simulate subscription */ - public void sendOnNext(String value) { - observer.onNext(value); - } - - /* used to simulate subscription */ - @SuppressWarnings("unused") - public void sendOnError(Exception e) { - observer.onError(e); - } - - @Override - public Subscription subscribe(final Observer<String> observer) { - this.observer = observer; - return s; - } - } - - private static class TestErrorObservable extends Observable<String> { - - String[] valuesToReturn; - - TestErrorObservable(String... values) { - valuesToReturn = values; - } - - @Override - public Subscription subscribe(Observer<String> observer) { - boolean errorThrown = false; - for (String s : valuesToReturn) { - if (s == null) { - System.out.println("throwing exception"); - observer.onError(new NullPointerException()); - errorThrown = true; - // purposefully not returning here so it will continue calling onNext - // so that we also test that we handle bad sequences like this - } else { - observer.onNext(s); - } - } - if (!errorThrown) { - observer.onCompleted(); - } - - return new Subscription() { - - @Override - public void unsubscribe() { - // unregister ... will never be called here since we are executing synchronously - } - - }; - } - } - - private static class TestAsyncErrorObservable extends Observable<String> { - - String[] valuesToReturn; - - TestAsyncErrorObservable(String... values) { - valuesToReturn = values; - } - - Thread t; - - @Override - public Subscription subscribe(final Observer<String> observer) { - t = new Thread(new Runnable() { - - @Override - public void run() { - for (String s : valuesToReturn) { - if (s == null) { - System.out.println("throwing exception"); - try { - Thread.sleep(100); - } catch (Exception e) { - - } - observer.onError(new NullPointerException()); - return; - } else { - observer.onNext(s); - } - } - System.out.println("subscription complete"); - observer.onCompleted(); - } - - }); - t.start(); - - return new Subscription() { - - @Override - public void unsubscribe() { - - } - - }; - } - } - - private static class CaptureObserver implements Observer<String> { - volatile Exception e; - - @Override - public void onCompleted() { - - } - - @Override - public void onError(Exception e) { - this.e = e; - } - - @Override - public void onNext(String args) { - - } - - } - } -} diff --git a/rxjava-core/src/main/java/rx/operators/OperationOnErrorResumeNextViaFunction.java b/rxjava-core/src/main/java/rx/operators/OperationOnErrorResumeNextViaFunction.java deleted file mode 100644 index 8ffb03a0bc..0000000000 --- a/rxjava-core/src/main/java/rx/operators/OperationOnErrorResumeNextViaFunction.java +++ /dev/null @@ -1,256 +0,0 @@ -/** - * Copyright 2013 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package rx.operators; - -import static org.junit.Assert.*; -import static org.mockito.Matchers.*; -import static org.mockito.Mockito.*; - -import java.util.Arrays; -import java.util.concurrent.atomic.AtomicReference; - -import org.junit.Test; -import org.mockito.Mockito; - -import rx.Observable; -import rx.Observer; -import rx.Subscription; -import rx.util.AtomicObservableSubscription; -import rx.util.CompositeException; -import rx.util.functions.Func1; - -public final class OperationOnErrorResumeNextViaFunction<T> { - - public static <T> Func1<Observer<T>, Subscription> onErrorResumeNextViaFunction(Observable<T> originalSequence, Func1<Exception, Observable<T>> resumeFunction) { - return new OnErrorResumeNextViaFunction<T>(originalSequence, resumeFunction); - } - - private static class OnErrorResumeNextViaFunction<T> implements Func1<Observer<T>, Subscription> { - - private final Func1<Exception, Observable<T>> resumeFunction; - private final Observable<T> originalSequence; - - public OnErrorResumeNextViaFunction(Observable<T> originalSequence, Func1<Exception, Observable<T>> resumeFunction) { - this.resumeFunction = resumeFunction; - this.originalSequence = originalSequence; - } - - public Subscription call(final Observer<T> observer) { - // AtomicReference since we'll be accessing/modifying this across threads so we can switch it if needed - final AtomicReference<AtomicObservableSubscription> subscriptionRef = new AtomicReference<AtomicObservableSubscription>(new AtomicObservableSubscription()); - - // subscribe to the original Observable and remember the subscription - subscriptionRef.get().wrap(new AtomicObservableSubscription(originalSequence.subscribe(new Observer<T>() { - public void onNext(T value) { - // forward the successful calls - observer.onNext(value); - } - - /** - * Instead of passing the onError forward, we intercept and "resume" with the resumeSequence. - */ - public void onError(Exception ex) { - /* remember what the current subscription is so we can determine if someone unsubscribes concurrently */ - AtomicObservableSubscription currentSubscription = subscriptionRef.get(); - // check that we have not been unsubscribed before we can process the error - if (currentSubscription != null) { - try { - Observable<T> resumeSequence = resumeFunction.call(ex); - /* error occurred, so switch subscription to the 'resumeSequence' */ - AtomicObservableSubscription innerSubscription = new AtomicObservableSubscription(resumeSequence.subscribe(observer)); - /* we changed the sequence, so also change the subscription to the one of the 'resumeSequence' instead */ - if (!subscriptionRef.compareAndSet(currentSubscription, innerSubscription)) { - // we failed to set which means 'subscriptionRef' was set to NULL via the unsubscribe below - // so we want to immediately unsubscribe from the resumeSequence we just subscribed to - innerSubscription.unsubscribe(); - } - } catch (Exception e) { - // the resume function failed so we need to call onError - // I am using CompositeException so that both exceptions can be seen - observer.onError(new CompositeException("OnErrorResume function failed", Arrays.asList(ex, e))); - } - } - } - - public void onCompleted() { - // forward the successful calls - observer.onCompleted(); - } - }))); - - return new Subscription() { - public void unsubscribe() { - // this will get either the original, or the resumeSequence one and unsubscribe on it - Subscription s = subscriptionRef.getAndSet(null); - if (s != null) { - s.unsubscribe(); - } - } - }; - } - } - - public static class UnitTest { - - @Test - public void testResumeNextWithSynchronousExecution() { - final AtomicReference<Exception> receivedException = new AtomicReference<Exception>(); - Observable<String> w = Observable.create(new Func1<Observer<String>, Subscription>() { - - @Override - public Subscription call(Observer<String> observer) { - observer.onNext("one"); - observer.onError(new Exception("injected failure")); - return Observable.noOpSubscription(); - } - }); - - Func1<Exception, Observable<String>> resume = new Func1<Exception, Observable<String>>() { - - @Override - public Observable<String> call(Exception t1) { - receivedException.set(t1); - return Observable.toObservable("twoResume", "threeResume"); - } - - }; - Observable<String> observable = Observable.create(onErrorResumeNextViaFunction(w, resume)); - - @SuppressWarnings("unchecked") - Observer<String> aObserver = mock(Observer.class); - observable.subscribe(aObserver); - - verify(aObserver, Mockito.never()).onError(any(Exception.class)); - verify(aObserver, times(1)).onCompleted(); - verify(aObserver, times(1)).onNext("one"); - verify(aObserver, Mockito.never()).onNext("two"); - verify(aObserver, Mockito.never()).onNext("three"); - verify(aObserver, times(1)).onNext("twoResume"); - verify(aObserver, times(1)).onNext("threeResume"); - assertNotNull(receivedException.get()); - } - - @Test - public void testResumeNextWithAsyncExecution() { - final AtomicReference<Exception> receivedException = new AtomicReference<Exception>(); - Subscription s = mock(Subscription.class); - TestObservable w = new TestObservable(s, "one"); - Func1<Exception, Observable<String>> resume = new Func1<Exception, Observable<String>>() { - - @Override - public Observable<String> call(Exception t1) { - receivedException.set(t1); - return Observable.toObservable("twoResume", "threeResume"); - } - - }; - Observable<String> observable = Observable.create(onErrorResumeNextViaFunction(w, resume)); - - @SuppressWarnings("unchecked") - Observer<String> aObserver = mock(Observer.class); - observable.subscribe(aObserver); - - try { - w.t.join(); - } catch (InterruptedException e) { - fail(e.getMessage()); - } - - verify(aObserver, Mockito.never()).onError(any(Exception.class)); - verify(aObserver, times(1)).onCompleted(); - verify(aObserver, times(1)).onNext("one"); - verify(aObserver, Mockito.never()).onNext("two"); - verify(aObserver, Mockito.never()).onNext("three"); - verify(aObserver, times(1)).onNext("twoResume"); - verify(aObserver, times(1)).onNext("threeResume"); - assertNotNull(receivedException.get()); - } - - /** - * Test that when a function throws an exception this is propagated through onError - */ - @Test - public void testFunctionThrowsError() { - Subscription s = mock(Subscription.class); - TestObservable w = new TestObservable(s, "one"); - Func1<Exception, Observable<String>> resume = new Func1<Exception, Observable<String>>() { - - @Override - public Observable<String> call(Exception t1) { - throw new RuntimeException("exception from function"); - } - - }; - Observable<String> observable = Observable.create(onErrorResumeNextViaFunction(w, resume)); - - @SuppressWarnings("unchecked") - Observer<String> aObserver = mock(Observer.class); - observable.subscribe(aObserver); - - try { - w.t.join(); - } catch (InterruptedException e) { - fail(e.getMessage()); - } - - // we should get the "one" value before the error - verify(aObserver, times(1)).onNext("one"); - - // we should have received an onError call on the Observer since the resume function threw an exception - verify(aObserver, times(1)).onError(any(Exception.class)); - verify(aObserver, times(0)).onCompleted(); - } - - private static class TestObservable extends Observable<String> { - - final Subscription s; - final String[] values; - Thread t = null; - - public TestObservable(Subscription s, String... values) { - this.s = s; - this.values = values; - } - - @Override - public Subscription subscribe(final Observer<String> observer) { - System.out.println("TestObservable subscribed to ..."); - t = new Thread(new Runnable() { - - @Override - public void run() { - try { - System.out.println("running TestObservable thread"); - for (String s : values) { - System.out.println("TestObservable onNext: " + s); - observer.onNext(s); - } - throw new RuntimeException("Forced Failure"); - } catch (Exception e) { - observer.onError(e); - } - } - - }); - System.out.println("starting TestObservable thread"); - t.start(); - System.out.println("done starting TestObservable thread"); - return s; - } - - } - } -} \ No newline at end of file diff --git a/rxjava-core/src/main/java/rx/operators/OperationOnErrorResumeNextViaObservable.java b/rxjava-core/src/main/java/rx/operators/OperationOnErrorResumeNextViaObservable.java deleted file mode 100644 index 84409e2b25..0000000000 --- a/rxjava-core/src/main/java/rx/operators/OperationOnErrorResumeNextViaObservable.java +++ /dev/null @@ -1,167 +0,0 @@ -/** - * Copyright 2013 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package rx.operators; - -import static org.junit.Assert.*; -import static org.mockito.Matchers.*; -import static org.mockito.Mockito.*; - -import java.util.concurrent.atomic.AtomicReference; - -import org.junit.Test; -import org.mockito.Mockito; - -import rx.Observable; -import rx.Observer; -import rx.Subscription; -import rx.util.AtomicObservableSubscription; -import rx.util.functions.Func1; - -public final class OperationOnErrorResumeNextViaObservable<T> { - - public static <T> Func1<Observer<T>, Subscription> onErrorResumeNextViaObservable(Observable<T> originalSequence, Observable<T> resumeSequence) { - return new OnErrorResumeNextViaObservable<T>(originalSequence, resumeSequence); - } - - private static class OnErrorResumeNextViaObservable<T> implements Func1<Observer<T>, Subscription> { - - private final Observable<T> resumeSequence; - private final Observable<T> originalSequence; - - public OnErrorResumeNextViaObservable(Observable<T> originalSequence, Observable<T> resumeSequence) { - this.resumeSequence = resumeSequence; - this.originalSequence = originalSequence; - } - - public Subscription call(final Observer<T> observer) { - final AtomicObservableSubscription subscription = new AtomicObservableSubscription(); - - // AtomicReference since we'll be accessing/modifying this across threads so we can switch it if needed - final AtomicReference<AtomicObservableSubscription> subscriptionRef = new AtomicReference<AtomicObservableSubscription>(subscription); - - // subscribe to the original Observable and remember the subscription - subscription.wrap(originalSequence.subscribe(new Observer<T>() { - public void onNext(T value) { - // forward the successful calls - observer.onNext(value); - } - - /** - * Instead of passing the onError forward, we intercept and "resume" with the resumeSequence. - */ - public void onError(Exception ex) { - /* remember what the current subscription is so we can determine if someone unsubscribes concurrently */ - AtomicObservableSubscription currentSubscription = subscriptionRef.get(); - // check that we have not been unsubscribed before we can process the error - if (currentSubscription != null) { - /* error occurred, so switch subscription to the 'resumeSequence' */ - AtomicObservableSubscription innerSubscription = new AtomicObservableSubscription(resumeSequence.subscribe(observer)); - /* we changed the sequence, so also change the subscription to the one of the 'resumeSequence' instead */ - if (!subscriptionRef.compareAndSet(currentSubscription, innerSubscription)) { - // we failed to set which means 'subscriptionRef' was set to NULL via the unsubscribe below - // so we want to immediately unsubscribe from the resumeSequence we just subscribed to - innerSubscription.unsubscribe(); - } - } - } - - public void onCompleted() { - // forward the successful calls - observer.onCompleted(); - } - })); - - return new Subscription() { - public void unsubscribe() { - // this will get either the original, or the resumeSequence one and unsubscribe on it - Subscription s = subscriptionRef.getAndSet(null); - if (s != null) { - s.unsubscribe(); - } - } - }; - } - } - - public static class UnitTest { - - @Test - public void testResumeNext() { - Subscription s = mock(Subscription.class); - TestObservable w = new TestObservable(s, "one"); - Observable<String> resume = Observable.toObservable("twoResume", "threeResume"); - Observable<String> observable = Observable.create(onErrorResumeNextViaObservable(w, resume)); - - @SuppressWarnings("unchecked") - Observer<String> aObserver = mock(Observer.class); - observable.subscribe(aObserver); - - try { - w.t.join(); - } catch (InterruptedException e) { - fail(e.getMessage()); - } - - verify(aObserver, Mockito.never()).onError(any(Exception.class)); - verify(aObserver, times(1)).onCompleted(); - verify(aObserver, times(1)).onNext("one"); - verify(aObserver, Mockito.never()).onNext("two"); - verify(aObserver, Mockito.never()).onNext("three"); - verify(aObserver, times(1)).onNext("twoResume"); - verify(aObserver, times(1)).onNext("threeResume"); - - } - - private static class TestObservable extends Observable<String> { - - final Subscription s; - final String[] values; - Thread t = null; - - public TestObservable(Subscription s, String... values) { - this.s = s; - this.values = values; - } - - @Override - public Subscription subscribe(final Observer<String> observer) { - System.out.println("TestObservable subscribed to ..."); - t = new Thread(new Runnable() { - - @Override - public void run() { - try { - System.out.println("running TestObservable thread"); - for (String s : values) { - System.out.println("TestObservable onNext: " + s); - observer.onNext(s); - } - throw new RuntimeException("Forced Failure"); - } catch (Exception e) { - observer.onError(e); - } - } - - }); - System.out.println("starting TestObservable thread"); - t.start(); - System.out.println("done starting TestObservable thread"); - return s; - } - - } - } -} \ No newline at end of file diff --git a/rxjava-core/src/main/java/rx/operators/OperationOnErrorReturn.java b/rxjava-core/src/main/java/rx/operators/OperationOnErrorReturn.java deleted file mode 100644 index fef25bb8f3..0000000000 --- a/rxjava-core/src/main/java/rx/operators/OperationOnErrorReturn.java +++ /dev/null @@ -1,227 +0,0 @@ -/** - * Copyright 2013 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package rx.operators; - -import static org.junit.Assert.*; -import static org.mockito.Matchers.*; -import static org.mockito.Mockito.*; - -import java.util.Arrays; -import java.util.concurrent.atomic.AtomicReference; - -import org.junit.Test; -import org.mockito.Mockito; - -import rx.Observable; -import rx.Observer; -import rx.Subscription; -import rx.util.AtomicObservableSubscription; -import rx.util.CompositeException; -import rx.util.functions.Func1; - -/** - * When an onError occurs the resumeFunction will be executed and it's response passed to onNext instead of calling onError. - */ -public final class OperationOnErrorReturn<T> { - - public static <T> Func1<Observer<T>, Subscription> onErrorReturn(Observable<T> originalSequence, Func1<Exception, T> resumeFunction) { - return new OnErrorReturn<T>(originalSequence, resumeFunction); - } - - private static class OnErrorReturn<T> implements Func1<Observer<T>, Subscription> { - private final Func1<Exception, T> resumeFunction; - private final Observable<T> originalSequence; - - public OnErrorReturn(Observable<T> originalSequence, Func1<Exception, T> resumeFunction) { - this.resumeFunction = resumeFunction; - this.originalSequence = originalSequence; - } - - public Subscription call(final Observer<T> observer) { - final AtomicObservableSubscription subscription = new AtomicObservableSubscription(); - - // AtomicReference since we'll be accessing/modifying this across threads so we can switch it if needed - final AtomicReference<AtomicObservableSubscription> subscriptionRef = new AtomicReference<AtomicObservableSubscription>(subscription); - - // subscribe to the original Observable and remember the subscription - subscription.wrap(originalSequence.subscribe(new Observer<T>() { - public void onNext(T value) { - // forward the successful calls - observer.onNext(value); - } - - /** - * Instead of passing the onError forward, we intercept and "resume" with the resumeSequence. - */ - public void onError(Exception ex) { - /* remember what the current subscription is so we can determine if someone unsubscribes concurrently */ - AtomicObservableSubscription currentSubscription = subscriptionRef.get(); - // check that we have not been unsubscribed before we can process the error - if (currentSubscription != null) { - try { - /* error occurred, so execute the function, give it the exception and call onNext with the response */ - onNext(resumeFunction.call(ex)); - /* - * we are not handling an exception thrown from this function ... should we do something? - * error handling within an error handler is a weird one to determine what we should do - * right now I'm going to just let it throw whatever exceptions occur (such as NPE) - * but I'm considering calling the original Observer.onError to act as if this OnErrorReturn operator didn't happen - */ - - /* we are now completed */ - onCompleted(); - - /* unsubscribe since it blew up */ - currentSubscription.unsubscribe(); - } catch (Exception e) { - // the return function failed so we need to call onError - // I am using CompositeException so that both exceptions can be seen - observer.onError(new CompositeException("OnErrorReturn function failed", Arrays.asList(ex, e))); - } - } - } - - public void onCompleted() { - // forward the successful calls - observer.onCompleted(); - } - })); - - return new Subscription() { - public void unsubscribe() { - // this will get either the original, or the resumeSequence one and unsubscribe on it - Subscription s = subscriptionRef.getAndSet(null); - if (s != null) { - s.unsubscribe(); - } - } - }; - } - } - - public static class UnitTest { - - @Test - public void testResumeNext() { - Subscription s = mock(Subscription.class); - TestObservable w = new TestObservable(s, "one"); - final AtomicReference<Exception> capturedException = new AtomicReference<Exception>(); - - Observable<String> observable = Observable.create(onErrorReturn(w, new Func1<Exception, String>() { - - @Override - public String call(Exception e) { - capturedException.set(e); - return "failure"; - } - - })); - - @SuppressWarnings("unchecked") - Observer<String> aObserver = mock(Observer.class); - observable.subscribe(aObserver); - - try { - w.t.join(); - } catch (InterruptedException e) { - fail(e.getMessage()); - } - - verify(aObserver, Mockito.never()).onError(any(Exception.class)); - verify(aObserver, times(1)).onCompleted(); - verify(aObserver, times(1)).onNext("one"); - verify(aObserver, times(1)).onNext("failure"); - assertNotNull(capturedException.get()); - } - - /** - * Test that when a function throws an exception this is propagated through onError - */ - @Test - public void testFunctionThrowsError() { - Subscription s = mock(Subscription.class); - TestObservable w = new TestObservable(s, "one"); - final AtomicReference<Exception> capturedException = new AtomicReference<Exception>(); - - Observable<String> observable = Observable.create(onErrorReturn(w, new Func1<Exception, String>() { - - @Override - public String call(Exception e) { - capturedException.set(e); - throw new RuntimeException("exception from function"); - } - - })); - - @SuppressWarnings("unchecked") - Observer<String> aObserver = mock(Observer.class); - observable.subscribe(aObserver); - - try { - w.t.join(); - } catch (InterruptedException e) { - fail(e.getMessage()); - } - - // we should get the "one" value before the error - verify(aObserver, times(1)).onNext("one"); - - // we should have received an onError call on the Observer since the resume function threw an exception - verify(aObserver, times(1)).onError(any(Exception.class)); - verify(aObserver, times(0)).onCompleted(); - assertNotNull(capturedException.get()); - } - - private static class TestObservable extends Observable<String> { - - final Subscription s; - final String[] values; - Thread t = null; - - public TestObservable(Subscription s, String... values) { - this.s = s; - this.values = values; - } - - @Override - public Subscription subscribe(final Observer<String> observer) { - System.out.println("TestObservable subscribed to ..."); - t = new Thread(new Runnable() { - - @Override - public void run() { - try { - System.out.println("running TestObservable thread"); - for (String s : values) { - System.out.println("TestObservable onNext: " + s); - observer.onNext(s); - } - throw new RuntimeException("Forced Failure"); - } catch (Exception e) { - observer.onError(e); - } - } - - }); - System.out.println("starting TestObservable thread"); - t.start(); - System.out.println("done starting TestObservable thread"); - return s; - } - - } - } -} \ No newline at end of file diff --git a/rxjava-core/src/main/java/rx/operators/OperationScan.java b/rxjava-core/src/main/java/rx/operators/OperationScan.java deleted file mode 100644 index 91d2078e73..0000000000 --- a/rxjava-core/src/main/java/rx/operators/OperationScan.java +++ /dev/null @@ -1,221 +0,0 @@ -/** - * Copyright 2013 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package rx.operators; - -import static org.mockito.Matchers.*; -import static org.mockito.Mockito.*; - -import org.junit.Before; -import org.junit.Test; -import org.mockito.MockitoAnnotations; - -import rx.Observable; -import rx.Observer; -import rx.Subscription; -import rx.util.AtomicObservableSubscription; -import rx.util.functions.Func1; -import rx.util.functions.Func2; - -public final class OperationScan { - /** - * Applies an accumulator function over an observable sequence and returns each intermediate result with the specified source and accumulator. - * - * @param sequence - * An observable sequence of elements to project. - * @param initialValue - * The initial (seed) accumulator value. - * @param accumulator - * An accumulator function to be invoked on each element from the sequence. - * - * @return An observable sequence whose elements are the result of accumulating the output from the list of Observables. - * @see http://msdn.microsoft.com/en-us/library/hh211665(v=vs.103).aspx - */ - public static <T> Func1<Observer<T>, Subscription> scan(Observable<T> sequence, T initialValue, Func2<T, T, T> accumulator) { - return new Accumulator<T>(sequence, initialValue, accumulator); - } - - /** - * Applies an accumulator function over an observable sequence and returns each intermediate result with the specified source and accumulator. - * - * @param sequence - * An observable sequence of elements to project. - * @param accumulator - * An accumulator function to be invoked on each element from the sequence. - * - * @return An observable sequence whose elements are the result of accumulating the output from the list of Observables. - * @see http://msdn.microsoft.com/en-us/library/hh211665(v=vs.103).aspx - */ - public static <T> Func1<Observer<T>, Subscription> scan(Observable<T> sequence, Func2<T, T, T> accumulator) { - return new Accumulator<T>(sequence, null, accumulator); - } - - private static class Accumulator<T> implements Func1<Observer<T>, Subscription> { - private final Observable<T> sequence; - private final T initialValue; - private Func2<T, T, T> accumlatorFunction; - private final AtomicObservableSubscription subscription = new AtomicObservableSubscription(); - - private Accumulator(Observable<T> sequence, T initialValue, Func2<T, T, T> accumulator) { - this.sequence = sequence; - this.initialValue = initialValue; - this.accumlatorFunction = accumulator; - } - - public Subscription call(final Observer<T> observer) { - - return subscription.wrap(sequence.subscribe(new Observer<T>() { - private T acc = initialValue; - private boolean hasSentInitialValue = false; - - /** - * We must synchronize this because we can't allow - * multiple threads to execute the 'accumulatorFunction' at the same time because - * the accumulator code very often will be doing mutation of the 'acc' object such as a non-threadsafe HashMap - * - * Because it's synchronized it's using non-atomic variables since everything in this method is single-threaded - */ - public synchronized void onNext(T value) { - if (acc == null) { - // we assume that acc is not allowed to be returned from accumulatorValue - // so it's okay to check null as being the state we initialize on - acc = value; - // this is all we do for this first value if we didn't have an initialValue - return; - } - if (!hasSentInitialValue) { - hasSentInitialValue = true; - observer.onNext(acc); - } - - try { - - acc = accumlatorFunction.call(acc, value); - if (acc == null) { - onError(new IllegalArgumentException("Null is an unsupported return value for an accumulator.")); - return; - } - observer.onNext(acc); - } catch (Exception ex) { - observer.onError(ex); - // this will work if the sequence is asynchronous, it will have no effect on a synchronous observable - subscription.unsubscribe(); - } - } - - public void onError(Exception ex) { - observer.onError(ex); - } - - // synchronized because we access 'hasSentInitialValue' - public synchronized void onCompleted() { - // if only one sequence value existed, we send it without any accumulation - if (!hasSentInitialValue) { - observer.onNext(acc); - } - observer.onCompleted(); - } - })); - } - } - - public static class UnitTest { - - @Before - public void before() { - MockitoAnnotations.initMocks(this); - } - - @Test - public void testScanIntegersWithInitialValue() { - @SuppressWarnings("unchecked") - Observer<Integer> Observer = mock(Observer.class); - - Observable<Integer> observable = Observable.toObservable(1, 2, 3); - - Observable<Integer> m = Observable.create(scan(observable, 0, new Func2<Integer, Integer, Integer>() { - - @Override - public Integer call(Integer t1, Integer t2) { - return t1 + t2; - } - - })); - m.subscribe(Observer); - - verify(Observer, never()).onError(any(Exception.class)); - verify(Observer, times(1)).onNext(0); - verify(Observer, times(1)).onNext(1); - verify(Observer, times(1)).onNext(3); - verify(Observer, times(1)).onNext(6); - verify(Observer, times(4)).onNext(anyInt()); - verify(Observer, times(1)).onCompleted(); - verify(Observer, never()).onError(any(Exception.class)); - } - - @Test - public void testScanIntegersWithoutInitialValue() { - @SuppressWarnings("unchecked") - Observer<Integer> Observer = mock(Observer.class); - - Observable<Integer> observable = Observable.toObservable(1, 2, 3); - - Observable<Integer> m = Observable.create(scan(observable, new Func2<Integer, Integer, Integer>() { - - @Override - public Integer call(Integer t1, Integer t2) { - return t1 + t2; - } - - })); - m.subscribe(Observer); - - verify(Observer, never()).onError(any(Exception.class)); - verify(Observer, never()).onNext(0); - verify(Observer, times(1)).onNext(1); - verify(Observer, times(1)).onNext(3); - verify(Observer, times(1)).onNext(6); - verify(Observer, times(3)).onNext(anyInt()); - verify(Observer, times(1)).onCompleted(); - verify(Observer, never()).onError(any(Exception.class)); - } - - @Test - public void testScanIntegersWithoutInitialValueAndOnlyOneValue() { - @SuppressWarnings("unchecked") - Observer<Integer> Observer = mock(Observer.class); - - Observable<Integer> observable = Observable.toObservable(1); - - Observable<Integer> m = Observable.create(scan(observable, new Func2<Integer, Integer, Integer>() { - - @Override - public Integer call(Integer t1, Integer t2) { - return t1 + t2; - } - - })); - m.subscribe(Observer); - - verify(Observer, never()).onError(any(Exception.class)); - verify(Observer, never()).onNext(0); - verify(Observer, times(1)).onNext(1); - verify(Observer, times(1)).onNext(anyInt()); - verify(Observer, times(1)).onCompleted(); - verify(Observer, never()).onError(any(Exception.class)); - } - } - -} diff --git a/rxjava-core/src/main/java/rx/operators/OperationSkip.java b/rxjava-core/src/main/java/rx/operators/OperationSkip.java deleted file mode 100644 index cbd3509446..0000000000 --- a/rxjava-core/src/main/java/rx/operators/OperationSkip.java +++ /dev/null @@ -1,146 +0,0 @@ -/** - * Copyright 2013 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package rx.operators; - -import static org.mockito.Matchers.*; -import static org.mockito.Mockito.*; - -import java.util.concurrent.atomic.AtomicInteger; - -import org.junit.Test; - -import rx.Observable; -import rx.Observer; -import rx.Subscription; -import rx.util.functions.Func1; - -/** - * Skips a specified number of contiguous values from the start of a Observable sequence and then returns the remaining values. - * - * @param <T> - */ -public final class OperationSkip { - - /** - * Skips a specified number of contiguous values from the start of a Observable sequence and then returns the remaining values. - * - * @param items - * @param num - * @return - * - * @see http://msdn.microsoft.com/en-us/library/hh229847(v=vs.103).aspx - */ - public static <T> Func1<Observer<T>, Subscription> skip(final Observable<T> items, final int num) { - // wrap in a Observable so that if a chain is built up, then asynchronously subscribed to twice we will have 2 instances of Take<T> rather than 1 handing both, which is not thread-safe. - return new Func1<Observer<T>, Subscription>() { - - @Override - public Subscription call(Observer<T> observer) { - return new Skip<T>(items, num).call(observer); - } - - }; - } - - /** - * This class is NOT thread-safe if invoked and referenced multiple times. In other words, don't subscribe to it multiple times from different threads. - * <p> - * It IS thread-safe from within it while receiving onNext events from multiple threads. - * - * @param <T> - */ - private static class Skip<T> implements Func1<Observer<T>, Subscription> { - private final int num; - private final Observable<T> items; - - Skip(final Observable<T> items, final int num) { - this.num = num; - this.items = items; - } - - public Subscription call(Observer<T> observer) { - return items.subscribe(new ItemObserver(observer)); - } - - /** - * Used to subscribe to the 'items' Observable sequence and forward to the actualObserver up to 'num' count. - */ - private class ItemObserver implements Observer<T> { - - private AtomicInteger counter = new AtomicInteger(); - private final Observer<T> observer; - - public ItemObserver(Observer<T> observer) { - this.observer = observer; - } - - @Override - public void onCompleted() { - observer.onCompleted(); - } - - @Override - public void onError(Exception e) { - observer.onError(e); - } - - @Override - public void onNext(T args) { - // skip them until we reach the 'num' value - if (counter.incrementAndGet() > num) { - observer.onNext(args); - } - } - - } - - } - - public static class UnitTest { - - @Test - public void testSkip1() { - Observable<String> w = Observable.toObservable("one", "two", "three"); - Observable<String> skip = Observable.create(skip(w, 2)); - - @SuppressWarnings("unchecked") - Observer<String> aObserver = mock(Observer.class); - skip.subscribe(aObserver); - verify(aObserver, never()).onNext("one"); - verify(aObserver, never()).onNext("two"); - verify(aObserver, times(1)).onNext("three"); - verify(aObserver, never()).onError(any(Exception.class)); - verify(aObserver, times(1)).onCompleted(); - } - - @Test - public void testSkip2() { - Observable<String> w = Observable.toObservable("one", "two", "three"); - Observable<String> skip = Observable.create(skip(w, 1)); - - @SuppressWarnings("unchecked") - Observer<String> aObserver = mock(Observer.class); - skip.subscribe(aObserver); - verify(aObserver, never()).onNext("one"); - verify(aObserver, times(1)).onNext("two"); - verify(aObserver, times(1)).onNext("three"); - verify(aObserver, never()).onError(any(Exception.class)); - verify(aObserver, times(1)).onCompleted(); - } - - } - -} \ No newline at end of file diff --git a/rxjava-core/src/main/java/rx/operators/OperationSynchronize.java b/rxjava-core/src/main/java/rx/operators/OperationSynchronize.java deleted file mode 100644 index 67fd646745..0000000000 --- a/rxjava-core/src/main/java/rx/operators/OperationSynchronize.java +++ /dev/null @@ -1,270 +0,0 @@ -/** - * Copyright 2013 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package rx.operators; - -import static org.mockito.Matchers.*; -import static org.mockito.Mockito.*; - -import org.junit.Test; -import org.mockito.Mockito; - -import rx.Observable; -import rx.Observer; -import rx.Subscription; -import rx.util.AtomicObservableSubscription; -import rx.util.SynchronizedObserver; -import rx.util.functions.Func1; - -/** - * An observable that wraps an observable of the same type and then enforces the semantics - * expected of a well-behaved observable. - * <p> - * An observable that ensures onNext, onCompleted, or onError calls on its subscribers are - * not interleaved, onCompleted and onError are only called once respectively, and no - * onNext calls follow onCompleted and onError calls. - * <p> - * NOTE: {@link Observable#create} already wraps Observables so this is generally redundant. - * - * @param <T> - * The type of the observable sequence. - */ -public final class OperationSynchronize<T> { - - /** - * Accepts an observable and wraps it in another observable which ensures that the resulting observable is well-behaved. - * - * A well-behaved observable ensures onNext, onCompleted, or onError calls to its subscribers are - * not interleaved, onCompleted and onError are only called once respectively, and no - * onNext calls follow onCompleted and onError calls. - * - * @param observable - * @param <T> - * @return - */ - public static <T> Func1<Observer<T>, Subscription> synchronize(Observable<T> observable) { - return new Synchronize<T>(observable); - } - - private static class Synchronize<T> implements Func1<Observer<T>, Subscription> { - - public Synchronize(Observable<T> innerObservable) { - this.innerObservable = innerObservable; - } - - private Observable<T> innerObservable; - private SynchronizedObserver<T> atomicObserver; - - public Subscription call(Observer<T> observer) { - AtomicObservableSubscription subscription = new AtomicObservableSubscription(); - atomicObserver = new SynchronizedObserver<T>(observer, subscription); - return subscription.wrap(innerObservable.subscribe(atomicObserver)); - } - - } - - public static class UnitTest { - - /** - * Ensure onCompleted can not be called after an Unsubscribe - */ - @Test - public void testOnCompletedAfterUnSubscribe() { - TestObservable t = new TestObservable(null); - Observable<String> st = Observable.create(synchronize(t)); - - @SuppressWarnings("unchecked") - Observer<String> w = mock(Observer.class); - Subscription ws = st.subscribe(w); - - t.sendOnNext("one"); - ws.unsubscribe(); - t.sendOnCompleted(); - - verify(w, times(1)).onNext("one"); - verify(w, Mockito.never()).onCompleted(); - } - - /** - * Ensure onNext can not be called after an Unsubscribe - */ - @Test - public void testOnNextAfterUnSubscribe() { - TestObservable t = new TestObservable(null); - Observable<String> st = Observable.create(synchronize(t)); - - @SuppressWarnings("unchecked") - Observer<String> w = mock(Observer.class); - Subscription ws = st.subscribe(w); - - t.sendOnNext("one"); - ws.unsubscribe(); - t.sendOnNext("two"); - - verify(w, times(1)).onNext("one"); - verify(w, Mockito.never()).onNext("two"); - } - - /** - * Ensure onError can not be called after an Unsubscribe - */ - @Test - public void testOnErrorAfterUnSubscribe() { - TestObservable t = new TestObservable(null); - Observable<String> st = Observable.create(synchronize(t)); - - @SuppressWarnings("unchecked") - Observer<String> w = mock(Observer.class); - Subscription ws = st.subscribe(w); - - t.sendOnNext("one"); - ws.unsubscribe(); - t.sendOnError(new RuntimeException("bad")); - - verify(w, times(1)).onNext("one"); - verify(w, Mockito.never()).onError(any(Exception.class)); - } - - /** - * Ensure onNext can not be called after onError - */ - @Test - public void testOnNextAfterOnError() { - TestObservable t = new TestObservable(null); - Observable<String> st = Observable.create(synchronize(t)); - - @SuppressWarnings("unchecked") - Observer<String> w = mock(Observer.class); - @SuppressWarnings("unused") - Subscription ws = st.subscribe(w); - - t.sendOnNext("one"); - t.sendOnError(new RuntimeException("bad")); - t.sendOnNext("two"); - - verify(w, times(1)).onNext("one"); - verify(w, times(1)).onError(any(Exception.class)); - verify(w, Mockito.never()).onNext("two"); - } - - /** - * Ensure onCompleted can not be called after onError - */ - @Test - public void testOnCompletedAfterOnError() { - TestObservable t = new TestObservable(null); - Observable<String> st = Observable.create(synchronize(t)); - - @SuppressWarnings("unchecked") - Observer<String> w = mock(Observer.class); - @SuppressWarnings("unused") - Subscription ws = st.subscribe(w); - - t.sendOnNext("one"); - t.sendOnError(new RuntimeException("bad")); - t.sendOnCompleted(); - - verify(w, times(1)).onNext("one"); - verify(w, times(1)).onError(any(Exception.class)); - verify(w, Mockito.never()).onCompleted(); - } - - /** - * Ensure onNext can not be called after onCompleted - */ - @Test - public void testOnNextAfterOnCompleted() { - TestObservable t = new TestObservable(null); - Observable<String> st = Observable.create(synchronize(t)); - - @SuppressWarnings("unchecked") - Observer<String> w = mock(Observer.class); - @SuppressWarnings("unused") - Subscription ws = st.subscribe(w); - - t.sendOnNext("one"); - t.sendOnCompleted(); - t.sendOnNext("two"); - - verify(w, times(1)).onNext("one"); - verify(w, Mockito.never()).onNext("two"); - verify(w, times(1)).onCompleted(); - verify(w, Mockito.never()).onError(any(Exception.class)); - } - - /** - * Ensure onError can not be called after onCompleted - */ - @Test - public void testOnErrorAfterOnCompleted() { - TestObservable t = new TestObservable(null); - Observable<String> st = Observable.create(synchronize(t)); - - @SuppressWarnings("unchecked") - Observer<String> w = mock(Observer.class); - @SuppressWarnings("unused") - Subscription ws = st.subscribe(w); - - t.sendOnNext("one"); - t.sendOnCompleted(); - t.sendOnError(new RuntimeException("bad")); - - verify(w, times(1)).onNext("one"); - verify(w, times(1)).onCompleted(); - verify(w, Mockito.never()).onError(any(Exception.class)); - } - - /** - * A Observable that doesn't do the right thing on UnSubscribe/Error/etc in that it will keep sending events down the pipe regardless of what happens. - */ - private static class TestObservable extends Observable<String> { - - Observer<String> observer = null; - - public TestObservable(Subscription s) { - } - - /* used to simulate subscription */ - public void sendOnCompleted() { - observer.onCompleted(); - } - - /* used to simulate subscription */ - public void sendOnNext(String value) { - observer.onNext(value); - } - - /* used to simulate subscription */ - public void sendOnError(Exception e) { - observer.onError(e); - } - - @Override - public Subscription subscribe(final Observer<String> observer) { - this.observer = observer; - return new Subscription() { - - @Override - public void unsubscribe() { - // going to do nothing to pretend I'm a bad Observable that keeps allowing events to be sent - } - - }; - } - - } - } - -} \ No newline at end of file diff --git a/rxjava-core/src/main/java/rx/operators/OperationTake.java b/rxjava-core/src/main/java/rx/operators/OperationTake.java deleted file mode 100644 index 3601de490f..0000000000 --- a/rxjava-core/src/main/java/rx/operators/OperationTake.java +++ /dev/null @@ -1,216 +0,0 @@ -/** - * Copyright 2013 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package rx.operators; - -import static org.junit.Assert.*; -import static org.mockito.Matchers.*; -import static org.mockito.Mockito.*; - -import java.util.concurrent.atomic.AtomicInteger; - -import org.junit.Test; - -import rx.Observable; -import rx.Observer; -import rx.Subscription; -import rx.util.AtomicObservableSubscription; -import rx.util.functions.Func1; - -/** - * Returns a specified number of contiguous values from the start of an observable sequence. - * - * @param <T> - */ -public final class OperationTake { - - /** - * Returns a specified number of contiguous values from the start of an observable sequence. - * - * @param items - * @param num - * @return - */ - public static <T> Func1<Observer<T>, Subscription> take(final Observable<T> items, final int num) { - // wrap in a Watchbable so that if a chain is built up, then asynchronously subscribed to twice we will have 2 instances of Take<T> rather than 1 handing both, which is not thread-safe. - return new Func1<Observer<T>, Subscription>() { - - @Override - public Subscription call(Observer<T> observer) { - return new Take<T>(items, num).call(observer); - } - - }; - } - - /** - * This class is NOT thread-safe if invoked and referenced multiple times. In other words, don't subscribe to it multiple times from different threads. - * <p> - * It IS thread-safe from within it while receiving onNext events from multiple threads. - * <p> - * This should all be fine as long as it's kept as a private class and a new instance created from static factory method above. - * <p> - * Note how the take() factory method above protects us from a single instance being exposed with the Observable wrapper handling the subscribe flow. - * - * @param <T> - */ - private static class Take<T> implements Func1<Observer<T>, Subscription> { - private final int num; - private final Observable<T> items; - private final AtomicObservableSubscription subscription = new AtomicObservableSubscription(); - - Take(final Observable<T> items, final int num) { - this.num = num; - this.items = items; - } - - public Subscription call(Observer<T> observer) { - return subscription.wrap(items.subscribe(new ItemObserver(observer))); - } - - /** - * Used to subscribe to the 'items' Observable sequence and forward to the actualObserver up to 'num' count. - */ - private class ItemObserver implements Observer<T> { - - private AtomicInteger counter = new AtomicInteger(); - private final Observer<T> observer; - - public ItemObserver(Observer<T> observer) { - this.observer = observer; - } - - @Override - public void onCompleted() { - observer.onCompleted(); - } - - @Override - public void onError(Exception e) { - observer.onError(e); - } - - @Override - public void onNext(T args) { - if (counter.getAndIncrement() < num) { - observer.onNext(args); - } else { - // this will work if the sequence is asynchronous, it will have no effect on a synchronous observable - subscription.unsubscribe(); - } - } - - } - - } - - public static class UnitTest { - - @Test - public void testTake1() { - Observable<String> w = Observable.toObservable("one", "two", "three"); - Observable<String> take = Observable.create(take(w, 2)); - - @SuppressWarnings("unchecked") - Observer<String> aObserver = mock(Observer.class); - take.subscribe(aObserver); - verify(aObserver, times(1)).onNext("one"); - verify(aObserver, times(1)).onNext("two"); - verify(aObserver, never()).onNext("three"); - verify(aObserver, never()).onError(any(Exception.class)); - verify(aObserver, times(1)).onCompleted(); - } - - @Test - public void testTake2() { - Observable<String> w = Observable.toObservable("one", "two", "three"); - Observable<String> take = Observable.create(take(w, 1)); - - @SuppressWarnings("unchecked") - Observer<String> aObserver = mock(Observer.class); - take.subscribe(aObserver); - verify(aObserver, times(1)).onNext("one"); - verify(aObserver, never()).onNext("two"); - verify(aObserver, never()).onNext("three"); - verify(aObserver, never()).onError(any(Exception.class)); - verify(aObserver, times(1)).onCompleted(); - } - - @Test - public void testUnsubscribeAfterTake() { - Subscription s = mock(Subscription.class); - TestObservable w = new TestObservable(s, "one", "two", "three"); - - @SuppressWarnings("unchecked") - Observer<String> aObserver = mock(Observer.class); - Observable<String> take = Observable.create(take(w, 1)); - take.subscribe(aObserver); - - // wait for the Observable to complete - try { - w.t.join(); - } catch (Exception e) { - e.printStackTrace(); - fail(e.getMessage()); - } - - System.out.println("TestObservable thread finished"); - verify(aObserver, times(1)).onNext("one"); - verify(aObserver, never()).onNext("two"); - verify(aObserver, never()).onNext("three"); - verify(s, times(1)).unsubscribe(); - } - - private static class TestObservable extends Observable<String> { - - final Subscription s; - final String[] values; - Thread t = null; - - public TestObservable(Subscription s, String... values) { - this.s = s; - this.values = values; - } - - @Override - public Subscription subscribe(final Observer<String> observer) { - System.out.println("TestObservable subscribed to ..."); - t = new Thread(new Runnable() { - - @Override - public void run() { - try { - System.out.println("running TestObservable thread"); - for (String s : values) { - System.out.println("TestObservable onNext: " + s); - observer.onNext(s); - } - observer.onCompleted(); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - - }); - System.out.println("starting TestObservable thread"); - t.start(); - System.out.println("done starting TestObservable thread"); - return s; - } - - } - } - -} \ No newline at end of file diff --git a/rxjava-core/src/main/java/rx/operators/OperationToObservableFuture.java b/rxjava-core/src/main/java/rx/operators/OperationToObservableFuture.java deleted file mode 100644 index 0c19619e4e..0000000000 --- a/rxjava-core/src/main/java/rx/operators/OperationToObservableFuture.java +++ /dev/null @@ -1,96 +0,0 @@ -package rx.operators; - -import static org.mockito.Mockito.*; - -import java.util.concurrent.Future; -import java.util.concurrent.TimeUnit; - -import org.junit.Test; - -import rx.Observable; -import rx.Observer; -import rx.Subscription; -import rx.util.functions.Func1; - -public class OperationToObservableFuture { - private static class ToObservableFuture<T> implements Func1<Observer<T>, Subscription> { - private final Future<T> that; - private final Long time; - private final TimeUnit unit; - - public ToObservableFuture(Future<T> that) { - this.that = that; - this.time = null; - this.unit = null; - } - - public ToObservableFuture(Future<T> that, long time, TimeUnit unit) { - this.that = that; - this.time = time; - this.unit = unit; - } - - @Override - public Subscription call(Observer<T> observer) { - try { - T value = (time == null) ? that.get() : that.get(time, unit); - - if (!that.isCancelled()) { - observer.onNext(value); - } - observer.onCompleted(); - } catch (Exception e) { - observer.onError(e); - } - - // the get() has already completed so there is no point in - // giving the user a way to cancel. - return Observable.noOpSubscription(); - } - } - - public static <T> Func1<Observer<T>, Subscription> toObservableFuture(final Future<T> that) { - return new ToObservableFuture<T>(that); - } - - public static <T> Func1<Observer<T>, Subscription> toObservableFuture(final Future<T> that, long time, TimeUnit unit) { - return new ToObservableFuture<T>(that, time, unit); - } - - @SuppressWarnings("unchecked") - public static class UnitTest { - @Test - public void testSuccess() throws Exception { - Future<Object> future = mock(Future.class); - Object value = new Object(); - when(future.get()).thenReturn(value); - ToObservableFuture<Object> ob = new ToObservableFuture<Object>(future); - Observer<Object> o = mock(Observer.class); - - Subscription sub = ob.call(o); - sub.unsubscribe(); - - verify(o, times(1)).onNext(value); - verify(o, times(1)).onCompleted(); - verify(o, never()).onError(null); - verify(future, never()).cancel(true); - } - - @Test - public void testFailure() throws Exception { - Future<Object> future = mock(Future.class); - RuntimeException e = new RuntimeException(); - when(future.get()).thenThrow(e); - ToObservableFuture<Object> ob = new ToObservableFuture<Object>(future); - Observer<Object> o = mock(Observer.class); - - Subscription sub = ob.call(o); - sub.unsubscribe(); - - verify(o, never()).onNext(null); - verify(o, never()).onCompleted(); - verify(o, times(1)).onError(e); - verify(future, never()).cancel(true); - } - } -} diff --git a/rxjava-core/src/main/java/rx/operators/OperationToObservableIterable.java b/rxjava-core/src/main/java/rx/operators/OperationToObservableIterable.java deleted file mode 100644 index 810491fb18..0000000000 --- a/rxjava-core/src/main/java/rx/operators/OperationToObservableIterable.java +++ /dev/null @@ -1,76 +0,0 @@ -/** - * Copyright 2013 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package rx.operators; - -import static org.mockito.Matchers.*; -import static org.mockito.Mockito.*; - -import java.util.Arrays; - -import org.junit.Test; -import org.mockito.Mockito; - -import rx.Observable; -import rx.Observer; -import rx.Subscription; -import rx.util.functions.Func1; - -/** - * Accepts an Iterable object and exposes it as an Observable. - * - * @param <T> - * The type of the Iterable sequence. - */ -public final class OperationToObservableIterable<T> { - - public static <T> Func1<Observer<T>, Subscription> toObservableIterable(Iterable<T> list) { - return new ToObservableIterable<T>(list); - } - - private static class ToObservableIterable<T> implements Func1<Observer<T>, Subscription> { - public ToObservableIterable(Iterable<T> list) { - this.iterable = list; - } - - public Iterable<T> iterable; - - public Subscription call(Observer<T> observer) { - for (T item : iterable) { - observer.onNext(item); - } - observer.onCompleted(); - - return Observable.noOpSubscription(); - } - } - - public static class UnitTest { - - @Test - public void testIterable() { - Observable<String> observable = Observable.create(toObservableIterable(Arrays.<String> asList("one", "two", "three"))); - - @SuppressWarnings("unchecked") - Observer<String> aObserver = mock(Observer.class); - observable.subscribe(aObserver); - verify(aObserver, times(1)).onNext("one"); - verify(aObserver, times(1)).onNext("two"); - verify(aObserver, times(1)).onNext("three"); - verify(aObserver, Mockito.never()).onError(any(Exception.class)); - verify(aObserver, times(1)).onCompleted(); - } - } -} \ No newline at end of file diff --git a/rxjava-core/src/main/java/rx/operators/OperationToObservableList.java b/rxjava-core/src/main/java/rx/operators/OperationToObservableList.java deleted file mode 100644 index 9719c4216d..0000000000 --- a/rxjava-core/src/main/java/rx/operators/OperationToObservableList.java +++ /dev/null @@ -1,98 +0,0 @@ -/** - * Copyright 2013 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package rx.operators; - -import static org.mockito.Matchers.*; -import static org.mockito.Mockito.*; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.concurrent.ConcurrentLinkedQueue; - -import org.junit.Test; -import org.mockito.Mockito; - -import rx.Observable; -import rx.Observer; -import rx.Subscription; -import rx.util.functions.Func1; - -public final class OperationToObservableList<T> { - - public static <T> Func1<Observer<List<T>>, Subscription> toObservableList(Observable<T> that) { - return new ToObservableList<T>(that); - } - - private static class ToObservableList<T> implements Func1<Observer<List<T>>, Subscription> { - - private final Observable<T> that; - final ConcurrentLinkedQueue<T> list = new ConcurrentLinkedQueue<T>(); - - public ToObservableList(Observable<T> that) { - this.that = that; - } - - public Subscription call(final Observer<List<T>> observer) { - - return that.subscribe(new Observer<T>() { - public void onNext(T value) { - // onNext can be concurrently executed so list must be thread-safe - list.add(value); - } - - public void onError(Exception ex) { - observer.onError(ex); - } - - public void onCompleted() { - try { - // copy from LinkedQueue to List since ConcurrentLinkedQueue does not implement the List interface - ArrayList<T> l = new ArrayList<T>(list.size()); - for (T t : list) { - l.add(t); - } - - // benjchristensen => I want to make this list immutable but some clients are sorting this - // instead of using toSortedList() and this change breaks them until we migrate their code. - // observer.onNext(Collections.unmodifiableList(l)); - observer.onNext(l); - observer.onCompleted(); - } catch (Exception e) { - onError(e); - } - - } - }); - } - } - - public static class UnitTest { - - @Test - public void testList() { - Observable<String> w = Observable.toObservable("one", "two", "three"); - Observable<List<String>> observable = Observable.create(toObservableList(w)); - - @SuppressWarnings("unchecked") - Observer<List<String>> aObserver = mock(Observer.class); - observable.subscribe(aObserver); - verify(aObserver, times(1)).onNext(Arrays.asList("one", "two", "three")); - verify(aObserver, Mockito.never()).onError(any(Exception.class)); - verify(aObserver, times(1)).onCompleted(); - } - } -} \ No newline at end of file diff --git a/rxjava-core/src/main/java/rx/operators/OperationToObservableSortedList.java b/rxjava-core/src/main/java/rx/operators/OperationToObservableSortedList.java deleted file mode 100644 index 4c1e8885d2..0000000000 --- a/rxjava-core/src/main/java/rx/operators/OperationToObservableSortedList.java +++ /dev/null @@ -1,178 +0,0 @@ -/** - * Copyright 2013 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package rx.operators; - -import static org.mockito.Matchers.*; -import static org.mockito.Mockito.*; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.Comparator; -import java.util.List; -import java.util.concurrent.ConcurrentLinkedQueue; - -import org.junit.Test; -import org.mockito.Mockito; - -import rx.Observable; -import rx.Observer; -import rx.Subscription; -import rx.util.functions.Func1; -import rx.util.functions.Func2; - -/** - * Similar to toList in that it converts a sequence<T> into a List<T> except that it accepts a Function that will provide an implementation of Comparator. - * - * @param <T> - */ -public final class OperationToObservableSortedList<T> { - - /** - * Sort T objects by their natural order (object must implement Comparable). - * - * @param sequence - * @throws ClassCastException - * if T objects do not implement Comparable - * @return - */ - public static <T> Func1<Observer<List<T>>, Subscription> toSortedList(Observable<T> sequence) { - return new ToObservableSortedList<T>(sequence); - } - - /** - * Sort T objects using the defined sort function. - * - * @param sequence - * @param sortFunction - * @return - */ - public static <T> Func1<Observer<List<T>>, Subscription> toSortedList(Observable<T> sequence, Func2<T, T, Integer> sortFunction) { - return new ToObservableSortedList<T>(sequence, sortFunction); - } - - private static class ToObservableSortedList<T> implements Func1<Observer<List<T>>, Subscription> { - - private final Observable<T> that; - private final ConcurrentLinkedQueue<T> list = new ConcurrentLinkedQueue<T>(); - private final Func2<T, T, Integer> sortFunction; - - // unchecked as we're support Object for the default - @SuppressWarnings("unchecked") - private ToObservableSortedList(Observable<T> that) { - this(that, defaultSortFunction); - } - - private ToObservableSortedList(Observable<T> that, Func2<T, T, Integer> sortFunction) { - this.that = that; - this.sortFunction = sortFunction; - } - - public Subscription call(final Observer<List<T>> observer) { - return that.subscribe(new Observer<T>() { - public void onNext(T value) { - // onNext can be concurrently executed so list must be thread-safe - list.add(value); - } - - public void onError(Exception ex) { - observer.onError(ex); - } - - public void onCompleted() { - try { - // copy from LinkedQueue to List since ConcurrentLinkedQueue does not implement the List interface - ArrayList<T> l = new ArrayList<T>(list.size()); - for (T t : list) { - l.add(t); - } - - // sort the list before delivery - Collections.sort(l, new Comparator<T>() { - - @Override - public int compare(T o1, T o2) { - return sortFunction.call(o1, o2); - } - - }); - - observer.onNext(Collections.unmodifiableList(l)); - observer.onCompleted(); - } catch (Exception e) { - onError(e); - } - - } - }); - } - - // raw because we want to support Object for this default - @SuppressWarnings("rawtypes") - private static Func2 defaultSortFunction = new DefaultComparableFunction(); - - private static class DefaultComparableFunction implements Func2<Object, Object, Integer> { - - // unchecked because we want to support Object for this default - @SuppressWarnings("unchecked") - @Override - public Integer call(Object t1, Object t2) { - Comparable<Object> c1 = (Comparable<Object>) t1; - Comparable<Object> c2 = (Comparable<Object>) t2; - return c1.compareTo(c2); - } - - } - - } - - public static class UnitTest { - - @Test - public void testSortedList() { - Observable<Integer> w = Observable.toObservable(1, 3, 2, 5, 4); - Observable<List<Integer>> observable = Observable.create(toSortedList(w)); - - @SuppressWarnings("unchecked") - Observer<List<Integer>> aObserver = mock(Observer.class); - observable.subscribe(aObserver); - verify(aObserver, times(1)).onNext(Arrays.asList(1, 2, 3, 4, 5)); - verify(aObserver, Mockito.never()).onError(any(Exception.class)); - verify(aObserver, times(1)).onCompleted(); - } - - @Test - public void testSortedListWithCustomFunction() { - Observable<Integer> w = Observable.toObservable(1, 3, 2, 5, 4); - Observable<List<Integer>> observable = Observable.create(toSortedList(w, new Func2<Integer, Integer, Integer>() { - - @Override - public Integer call(Integer t1, Integer t2) { - return t2 - t1; - } - - })); - - @SuppressWarnings("unchecked") - Observer<List<Integer>> aObserver = mock(Observer.class); - observable.subscribe(aObserver); - verify(aObserver, times(1)).onNext(Arrays.asList(5, 4, 3, 2, 1)); - verify(aObserver, Mockito.never()).onError(any(Exception.class)); - verify(aObserver, times(1)).onCompleted(); - } - - } -} \ No newline at end of file diff --git a/rxjava-core/src/main/java/rx/operators/OperationZip.java b/rxjava-core/src/main/java/rx/operators/OperationZip.java deleted file mode 100644 index a282200344..0000000000 --- a/rxjava-core/src/main/java/rx/operators/OperationZip.java +++ /dev/null @@ -1,814 +0,0 @@ -/** - * Copyright 2013 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package rx.operators; - -import static org.mockito.Matchers.*; -import static org.mockito.Mockito.*; - -import java.util.Arrays; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentLinkedQueue; -import java.util.concurrent.atomic.AtomicBoolean; - -import javax.annotation.concurrent.GuardedBy; -import javax.annotation.concurrent.ThreadSafe; - -import org.junit.Test; -import org.mockito.InOrder; - -import rx.Observable; -import rx.Observer; -import rx.Subscription; -import rx.util.AtomicObservableSubscription; -import rx.util.SynchronizedObserver; -import rx.util.functions.Func1; -import rx.util.functions.Func2; -import rx.util.functions.Func3; -import rx.util.functions.Func4; -import rx.util.functions.FuncN; -import rx.util.functions.Functions; - -public final class OperationZip { - - public static <T0, T1, R> Func1<Observer<R>, Subscription> zip(Observable<T0> w0, Observable<T1> w1, Func2<T0, T1, R> zipFunction) { - Aggregator<R> a = new Aggregator<R>(Functions.fromFunc(zipFunction)); - a.addObserver(new ZipObserver<R, T0>(a, w0)); - a.addObserver(new ZipObserver<R, T1>(a, w1)); - return a; - } - - public static <T0, T1, T2, R> Func1<Observer<R>, Subscription> zip(Observable<T0> w0, Observable<T1> w1, Observable<T2> w2, Func3<T0, T1, T2, R> zipFunction) { - Aggregator<R> a = new Aggregator<R>(Functions.fromFunc(zipFunction)); - a.addObserver(new ZipObserver<R, T0>(a, w0)); - a.addObserver(new ZipObserver<R, T1>(a, w1)); - a.addObserver(new ZipObserver<R, T2>(a, w2)); - return a; - } - - public static <T0, T1, T2, T3, R> Func1<Observer<R>, Subscription> zip(Observable<T0> w0, Observable<T1> w1, Observable<T2> w2, Observable<T3> w3, Func4<T0, T1, T2, T3, R> zipFunction) { - Aggregator<R> a = new Aggregator<R>(Functions.fromFunc(zipFunction)); - a.addObserver(new ZipObserver<R, T0>(a, w0)); - a.addObserver(new ZipObserver<R, T1>(a, w1)); - a.addObserver(new ZipObserver<R, T2>(a, w2)); - a.addObserver(new ZipObserver<R, T3>(a, w3)); - return a; - } - - @ThreadSafe - private static class ZipObserver<R, T> implements Observer<T> { - final Observable<T> w; - final Aggregator<R> a; - private final AtomicObservableSubscription subscription = new AtomicObservableSubscription(); - private final AtomicBoolean subscribed = new AtomicBoolean(false); - - public ZipObserver(Aggregator<R> a, Observable<T> w) { - this.a = a; - this.w = w; - } - - public void startWatching() { - if (subscribed.compareAndSet(false, true)) { - // only subscribe once even if called more than once - subscription.wrap(w.subscribe(this)); - } - } - - @Override - public void onCompleted() { - a.complete(this); - } - - @Override - public void onError(Exception e) { - a.error(this, e); - } - - @Override - public void onNext(T args) { - try { - a.next(this, args); - } catch (Exception e) { - onError(e); - } - } - } - - /** - * Receive notifications from each of the Observables we are reducing and execute the zipFunction whenever we have received events from all Observables. - * - * @param <T> - */ - @ThreadSafe - private static class Aggregator<T> implements Func1<Observer<T>, Subscription> { - - private volatile SynchronizedObserver<T> observer; - private final FuncN<T> zipFunction; - private final AtomicBoolean started = new AtomicBoolean(false); - private final AtomicBoolean running = new AtomicBoolean(true); - private final ConcurrentHashMap<ZipObserver<T, ?>, Boolean> completed = new ConcurrentHashMap<ZipObserver<T, ?>, Boolean>(); - - /* we use ConcurrentHashMap despite synchronization of methods because stop() does NOT use synchronization and this map is used by it and can be called by other threads */ - private ConcurrentHashMap<ZipObserver<T, ?>, ConcurrentLinkedQueue<Object>> receivedValuesPerObserver = new ConcurrentHashMap<ZipObserver<T, ?>, ConcurrentLinkedQueue<Object>>(); - /* we use a ConcurrentLinkedQueue to retain ordering (I'd like to just use a ConcurrentLinkedHashMap for 'receivedValuesPerObserver' but that doesn't exist in standard java */ - private ConcurrentLinkedQueue<ZipObserver<T, ?>> observers = new ConcurrentLinkedQueue<ZipObserver<T, ?>>(); - - public Aggregator(FuncN<T> zipFunction) { - this.zipFunction = zipFunction; - } - - /** - * Receive notification of a Observer starting (meaning we should require it for aggregation) - * - * @param w - */ - @GuardedBy("Invoked ONLY from the static factory methods at top of this class which are always an atomic execution by a single thread.") - private void addObserver(ZipObserver<T, ?> w) { - // initialize this ZipObserver - observers.add(w); - receivedValuesPerObserver.put(w, new ConcurrentLinkedQueue<Object>()); - } - - /** - * Receive notification of a Observer completing its iterations. - * - * @param w - */ - void complete(ZipObserver<T, ?> w) { - // store that this ZipObserver is completed - completed.put(w, Boolean.TRUE); - // if all ZipObservers are completed, we mark the whole thing as completed - if (completed.size() == observers.size()) { - if (running.compareAndSet(true, false)) { - // this thread succeeded in setting running=false so let's propagate the completion - // mark ourselves as done - observer.onCompleted(); - } - } - } - - /** - * Receive error for a Observer. Throw the error up the chain and stop processing. - * - * @param w - */ - void error(ZipObserver<T, ?> w, Exception e) { - if (running.compareAndSet(true, false)) { - // this thread succeeded in setting running=false so let's propagate the error - observer.onError(e); - /* since we receive an error we want to tell everyone to stop */ - stop(); - } - } - - /** - * Receive the next value from a Observer. - * <p> - * If we have received values from all Observers, trigger the zip function, otherwise store the value and keep waiting. - * - * @param w - * @param arg - */ - void next(ZipObserver<T, ?> w, Object arg) { - if (observer == null) { - throw new RuntimeException("This shouldn't be running if a Observer isn't registered"); - } - - /* if we've been 'unsubscribed' don't process anything further even if the things we're watching keep sending (likely because they are not responding to the unsubscribe call) */ - if (!running.get()) { - return; - } - - // store the value we received and below we'll decide if we are to send it to the Observer - receivedValuesPerObserver.get(w).add(arg); - - // define here so the variable is out of the synchronized scope - Object[] argsToZip = new Object[observers.size()]; - - /* we have to synchronize here despite using concurrent data structures because the compound logic here must all be done atomically */ - synchronized (this) { - // if all ZipObservers in 'receivedValues' map have a value, invoke the zipFunction - for (ZipObserver<T, ?> rw : receivedValuesPerObserver.keySet()) { - if (receivedValuesPerObserver.get(rw).peek() == null) { - // we have a null meaning the queues aren't all populated so won't do anything - return; - } - } - // if we get to here this means all the queues have data - int i = 0; - for (ZipObserver<T, ?> rw : observers) { - argsToZip[i++] = receivedValuesPerObserver.get(rw).remove(); - } - } - // if we did not return above from the synchronized block we can now invoke the zipFunction with all of the args - // we do this outside the synchronized block as it is now safe to call this concurrently and don't need to block other threads from calling - // this 'next' method while another thread finishes calling this zipFunction - observer.onNext(zipFunction.call(argsToZip)); - } - - @Override - public Subscription call(Observer<T> observer) { - if (started.compareAndSet(false, true)) { - AtomicObservableSubscription subscription = new AtomicObservableSubscription(); - this.observer = new SynchronizedObserver<T>(observer, subscription); - /* start the Observers */ - for (ZipObserver<T, ?> rw : observers) { - rw.startWatching(); - } - - return subscription.wrap(new Subscription() { - - @Override - public void unsubscribe() { - stop(); - } - - }); - } else { - /* a Observer already has subscribed so blow up */ - throw new IllegalStateException("Only one Observer can subscribe to this Observable."); - } - } - - /* - * Do NOT synchronize this because it gets called via unsubscribe which can occur on other threads - * and result in deadlocks. (http://jira/browse/API-4060) - * - * AtomicObservableSubscription uses compareAndSet instead of locking to avoid deadlocks but ensure single-execution. - * - * We do the same in the implementation of this method. - * - * ThreadSafety of this method is provided by: - * - AtomicBoolean[running].compareAndSet - * - ConcurrentLinkedQueue[Observers] - * - ZipObserver.subscription being an AtomicObservableSubscription - */ - private void stop() { - /* tell ourselves to stop processing onNext events by setting running=false */ - if (running.compareAndSet(true, false)) { - /* propogate to all Observers to unsubscribe if this thread succeeded in setting running=false */ - for (ZipObserver<T, ?> o : observers) { - if (o.subscription != null) { - o.subscription.unsubscribe(); - } - } - } - } - - } - - public static class UnitTest { - - @SuppressWarnings("unchecked") - /* mock calls don't do generics */ - @Test - public void testZippingDifferentLengthObservableSequences1() { - Observer<String> w = mock(Observer.class); - - TestObservable w1 = new TestObservable(); - TestObservable w2 = new TestObservable(); - TestObservable w3 = new TestObservable(); - - Observable<String> zipW = Observable.create(zip(w1, w2, w3, getConcat3StringsZipr())); - zipW.subscribe(w); - - /* simulate sending data */ - // once for w1 - w1.Observer.onNext("1a"); - w1.Observer.onCompleted(); - // twice for w2 - w2.Observer.onNext("2a"); - w2.Observer.onNext("2b"); - w2.Observer.onCompleted(); - // 4 times for w3 - w3.Observer.onNext("3a"); - w3.Observer.onNext("3b"); - w3.Observer.onNext("3c"); - w3.Observer.onNext("3d"); - w3.Observer.onCompleted(); - - /* we should have been called 1 time on the Observer */ - InOrder inOrder = inOrder(w); - inOrder.verify(w).onNext("1a2a3a"); - - inOrder.verify(w, times(1)).onCompleted(); - } - - @Test - public void testZippingDifferentLengthObservableSequences2() { - @SuppressWarnings("unchecked") - Observer<String> w = mock(Observer.class); - - TestObservable w1 = new TestObservable(); - TestObservable w2 = new TestObservable(); - TestObservable w3 = new TestObservable(); - - Observable<String> zipW = Observable.create(zip(w1, w2, w3, getConcat3StringsZipr())); - zipW.subscribe(w); - - /* simulate sending data */ - // 4 times for w1 - w1.Observer.onNext("1a"); - w1.Observer.onNext("1b"); - w1.Observer.onNext("1c"); - w1.Observer.onNext("1d"); - w1.Observer.onCompleted(); - // twice for w2 - w2.Observer.onNext("2a"); - w2.Observer.onNext("2b"); - w2.Observer.onCompleted(); - // 1 times for w3 - w3.Observer.onNext("3a"); - w3.Observer.onCompleted(); - - /* we should have been called 1 time on the Observer */ - InOrder inOrder = inOrder(w); - inOrder.verify(w).onNext("1a2a3a"); - - inOrder.verify(w, times(1)).onCompleted(); - - } - - /** - * Testing internal private logic due to the complexity so I want to use TDD to test as a I build it rather than relying purely on the overall functionality expected by the public methods. - */ - @SuppressWarnings("unchecked") - /* mock calls don't do generics */ - @Test - public void testAggregatorSimple() { - FuncN<String> zipr = getConcatZipr(); - /* create the aggregator which will execute the zip function when all Observables provide values */ - Aggregator<String> a = new Aggregator<String>(zipr); - - /* define a Observer to receive aggregated events */ - Observer<String> aObserver = mock(Observer.class); - a.call(aObserver); - - /* mock the Observable Observers that are 'pushing' data for us */ - ZipObserver<String, String> r1 = mock(ZipObserver.class); - ZipObserver<String, String> r2 = mock(ZipObserver.class); - - /* pretend we're starting up */ - a.addObserver(r1); - a.addObserver(r2); - - /* simulate the Observables pushing data into the aggregator */ - a.next(r1, "hello"); - a.next(r2, "world"); - - InOrder inOrder = inOrder(aObserver); - - verify(aObserver, never()).onError(any(Exception.class)); - verify(aObserver, never()).onCompleted(); - inOrder.verify(aObserver, times(1)).onNext("helloworld"); - - a.next(r1, "hello "); - a.next(r2, "again"); - - verify(aObserver, never()).onError(any(Exception.class)); - verify(aObserver, never()).onCompleted(); - inOrder.verify(aObserver, times(1)).onNext("hello again"); - - a.complete(r1); - a.complete(r2); - - inOrder.verify(aObserver, never()).onNext(anyString()); - verify(aObserver, times(1)).onCompleted(); - } - - @SuppressWarnings("unchecked") - /* mock calls don't do generics */ - @Test - public void testAggregatorDifferentSizedResultsWithOnComplete() { - FuncN<String> zipr = getConcatZipr(); - /* create the aggregator which will execute the zip function when all Observables provide values */ - Aggregator<String> a = new Aggregator<String>(zipr); - - /* define a Observer to receive aggregated events */ - Observer<String> aObserver = mock(Observer.class); - a.call(aObserver); - - /* mock the Observable Observers that are 'pushing' data for us */ - ZipObserver<String, String> r1 = mock(ZipObserver.class); - ZipObserver<String, String> r2 = mock(ZipObserver.class); - - /* pretend we're starting up */ - a.addObserver(r1); - a.addObserver(r2); - - /* simulate the Observables pushing data into the aggregator */ - a.next(r1, "hello"); - a.next(r2, "world"); - a.complete(r2); - - InOrder inOrder = inOrder(aObserver); - - inOrder.verify(aObserver, never()).onError(any(Exception.class)); - inOrder.verify(aObserver, never()).onCompleted(); - inOrder.verify(aObserver, times(1)).onNext("helloworld"); - - a.next(r1, "hi"); - a.complete(r1); - - inOrder.verify(aObserver, never()).onError(any(Exception.class)); - inOrder.verify(aObserver, times(1)).onCompleted(); - inOrder.verify(aObserver, never()).onNext(anyString()); - } - - @SuppressWarnings("unchecked") - /* mock calls don't do generics */ - @Test - public void testAggregateMultipleTypes() { - FuncN<String> zipr = getConcatZipr(); - /* create the aggregator which will execute the zip function when all Observables provide values */ - Aggregator<String> a = new Aggregator<String>(zipr); - - /* define a Observer to receive aggregated events */ - Observer<String> aObserver = mock(Observer.class); - a.call(aObserver); - - /* mock the Observable Observers that are 'pushing' data for us */ - ZipObserver<String, String> r1 = mock(ZipObserver.class); - ZipObserver<String, Integer> r2 = mock(ZipObserver.class); - - /* pretend we're starting up */ - a.addObserver(r1); - a.addObserver(r2); - - /* simulate the Observables pushing data into the aggregator */ - a.next(r1, "hello"); - a.next(r2, "world"); - a.complete(r2); - - InOrder inOrder = inOrder(aObserver); - - inOrder.verify(aObserver, never()).onError(any(Exception.class)); - inOrder.verify(aObserver, never()).onCompleted(); - inOrder.verify(aObserver, times(1)).onNext("helloworld"); - - a.next(r1, "hi"); - a.complete(r1); - - inOrder.verify(aObserver, never()).onError(any(Exception.class)); - inOrder.verify(aObserver, times(1)).onCompleted(); - inOrder.verify(aObserver, never()).onNext(anyString()); - } - - @SuppressWarnings("unchecked") - /* mock calls don't do generics */ - @Test - public void testAggregate3Types() { - FuncN<String> zipr = getConcatZipr(); - /* create the aggregator which will execute the zip function when all Observables provide values */ - Aggregator<String> a = new Aggregator<String>(zipr); - - /* define a Observer to receive aggregated events */ - Observer<String> aObserver = mock(Observer.class); - a.call(aObserver); - - /* mock the Observable Observers that are 'pushing' data for us */ - ZipObserver<String, String> r1 = mock(ZipObserver.class); - ZipObserver<String, Integer> r2 = mock(ZipObserver.class); - ZipObserver<String, int[]> r3 = mock(ZipObserver.class); - - /* pretend we're starting up */ - a.addObserver(r1); - a.addObserver(r2); - a.addObserver(r3); - - /* simulate the Observables pushing data into the aggregator */ - a.next(r1, "hello"); - a.next(r2, 2); - a.next(r3, new int[] { 5, 6, 7 }); - - verify(aObserver, never()).onError(any(Exception.class)); - verify(aObserver, never()).onCompleted(); - verify(aObserver, times(1)).onNext("hello2[5, 6, 7]"); - } - - @SuppressWarnings("unchecked") - /* mock calls don't do generics */ - @Test - public void testAggregatorsWithDifferentSizesAndTiming() { - FuncN<String> zipr = getConcatZipr(); - /* create the aggregator which will execute the zip function when all Observables provide values */ - Aggregator<String> a = new Aggregator<String>(zipr); - - /* define a Observer to receive aggregated events */ - Observer<String> aObserver = mock(Observer.class); - a.call(aObserver); - - /* mock the Observable Observers that are 'pushing' data for us */ - ZipObserver<String, String> r1 = mock(ZipObserver.class); - ZipObserver<String, String> r2 = mock(ZipObserver.class); - - /* pretend we're starting up */ - a.addObserver(r1); - a.addObserver(r2); - - /* simulate the Observables pushing data into the aggregator */ - a.next(r1, "one"); - a.next(r1, "two"); - a.next(r1, "three"); - a.next(r2, "A"); - - verify(aObserver, never()).onError(any(Exception.class)); - verify(aObserver, never()).onCompleted(); - verify(aObserver, times(1)).onNext("oneA"); - - a.next(r1, "four"); - a.complete(r1); - a.next(r2, "B"); - verify(aObserver, times(1)).onNext("twoB"); - a.next(r2, "C"); - verify(aObserver, times(1)).onNext("threeC"); - a.next(r2, "D"); - verify(aObserver, times(1)).onNext("fourD"); - a.next(r2, "E"); - verify(aObserver, never()).onNext("E"); - a.complete(r2); - - verify(aObserver, never()).onError(any(Exception.class)); - verify(aObserver, times(1)).onCompleted(); - } - - @SuppressWarnings("unchecked") - /* mock calls don't do generics */ - @Test - public void testAggregatorError() { - FuncN<String> zipr = getConcatZipr(); - /* create the aggregator which will execute the zip function when all Observables provide values */ - Aggregator<String> a = new Aggregator<String>(zipr); - - /* define a Observer to receive aggregated events */ - Observer<String> aObserver = mock(Observer.class); - a.call(aObserver); - - /* mock the Observable Observers that are 'pushing' data for us */ - ZipObserver<String, String> r1 = mock(ZipObserver.class); - ZipObserver<String, String> r2 = mock(ZipObserver.class); - - /* pretend we're starting up */ - a.addObserver(r1); - a.addObserver(r2); - - /* simulate the Observables pushing data into the aggregator */ - a.next(r1, "hello"); - a.next(r2, "world"); - - verify(aObserver, never()).onError(any(Exception.class)); - verify(aObserver, never()).onCompleted(); - verify(aObserver, times(1)).onNext("helloworld"); - - a.error(r1, new RuntimeException("")); - a.next(r1, "hello"); - a.next(r2, "again"); - - verify(aObserver, times(1)).onError(any(Exception.class)); - verify(aObserver, never()).onCompleted(); - // we don't want to be called again after an error - verify(aObserver, times(0)).onNext("helloagain"); - } - - @SuppressWarnings("unchecked") - /* mock calls don't do generics */ - @Test - public void testAggregatorUnsubscribe() { - FuncN<String> zipr = getConcatZipr(); - /* create the aggregator which will execute the zip function when all Observables provide values */ - Aggregator<String> a = new Aggregator<String>(zipr); - - /* define a Observer to receive aggregated events */ - Observer<String> aObserver = mock(Observer.class); - Subscription subscription = a.call(aObserver); - - /* mock the Observable Observers that are 'pushing' data for us */ - ZipObserver<String, String> r1 = mock(ZipObserver.class); - ZipObserver<String, String> r2 = mock(ZipObserver.class); - - /* pretend we're starting up */ - a.addObserver(r1); - a.addObserver(r2); - - /* simulate the Observables pushing data into the aggregator */ - a.next(r1, "hello"); - a.next(r2, "world"); - - verify(aObserver, never()).onError(any(Exception.class)); - verify(aObserver, never()).onCompleted(); - verify(aObserver, times(1)).onNext("helloworld"); - - subscription.unsubscribe(); - a.next(r1, "hello"); - a.next(r2, "again"); - - verify(aObserver, times(0)).onError(any(Exception.class)); - verify(aObserver, never()).onCompleted(); - // we don't want to be called again after an error - verify(aObserver, times(0)).onNext("helloagain"); - } - - @SuppressWarnings("unchecked") - /* mock calls don't do generics */ - @Test - public void testAggregatorEarlyCompletion() { - FuncN<String> zipr = getConcatZipr(); - /* create the aggregator which will execute the zip function when all Observables provide values */ - Aggregator<String> a = new Aggregator<String>(zipr); - - /* define a Observer to receive aggregated events */ - Observer<String> aObserver = mock(Observer.class); - a.call(aObserver); - - /* mock the Observable Observers that are 'pushing' data for us */ - ZipObserver<String, String> r1 = mock(ZipObserver.class); - ZipObserver<String, String> r2 = mock(ZipObserver.class); - - /* pretend we're starting up */ - a.addObserver(r1); - a.addObserver(r2); - - /* simulate the Observables pushing data into the aggregator */ - a.next(r1, "one"); - a.next(r1, "two"); - a.complete(r1); - a.next(r2, "A"); - - InOrder inOrder = inOrder(aObserver); - - inOrder.verify(aObserver, never()).onError(any(Exception.class)); - inOrder.verify(aObserver, never()).onCompleted(); - inOrder.verify(aObserver, times(1)).onNext("oneA"); - - a.complete(r2); - - inOrder.verify(aObserver, never()).onError(any(Exception.class)); - inOrder.verify(aObserver, times(1)).onCompleted(); - inOrder.verify(aObserver, never()).onNext(anyString()); - } - - @SuppressWarnings("unchecked") - /* mock calls don't do generics */ - @Test - public void testZip2Types() { - Func2<String, Integer, String> zipr = getConcatStringIntegerZipr(); - - /* define a Observer to receive aggregated events */ - Observer<String> aObserver = mock(Observer.class); - - Observable<String> w = Observable.create(zip(Observable.toObservable("one", "two"), Observable.toObservable(2, 3, 4), zipr)); - w.subscribe(aObserver); - - verify(aObserver, never()).onError(any(Exception.class)); - verify(aObserver, times(1)).onCompleted(); - verify(aObserver, times(1)).onNext("one2"); - verify(aObserver, times(1)).onNext("two3"); - verify(aObserver, never()).onNext("4"); - } - - @SuppressWarnings("unchecked") - /* mock calls don't do generics */ - @Test - public void testZip3Types() { - Func3<String, Integer, int[], String> zipr = getConcatStringIntegerIntArrayZipr(); - - /* define a Observer to receive aggregated events */ - Observer<String> aObserver = mock(Observer.class); - - Observable<String> w = Observable.create(zip(Observable.toObservable("one", "two"), Observable.toObservable(2), Observable.toObservable(new int[] { 4, 5, 6 }), zipr)); - w.subscribe(aObserver); - - verify(aObserver, never()).onError(any(Exception.class)); - verify(aObserver, times(1)).onCompleted(); - verify(aObserver, times(1)).onNext("one2[4, 5, 6]"); - verify(aObserver, never()).onNext("two"); - } - - @Test - public void testOnNextExceptionInvokesOnError() { - Func2<Integer, Integer, Integer> zipr = getDivideZipr(); - - @SuppressWarnings("unchecked") - Observer<Integer> aObserver = mock(Observer.class); - - Observable<Integer> w = Observable.create(zip(Observable.toObservable(10, 20, 30), Observable.toObservable(0, 1, 2), zipr)); - w.subscribe(aObserver); - - verify(aObserver, times(1)).onError(any(Exception.class)); - } - - private Func2<Integer, Integer, Integer> getDivideZipr() { - Func2<Integer, Integer, Integer> zipr = new Func2<Integer, Integer, Integer>() { - - @Override - public Integer call(Integer i1, Integer i2) { - return i1 / i2; - } - - }; - return zipr; - } - - private Func3<String, String, String, String> getConcat3StringsZipr() { - Func3<String, String, String, String> zipr = new Func3<String, String, String, String>() { - - @Override - public String call(String a1, String a2, String a3) { - if (a1 == null) { - a1 = ""; - } - if (a2 == null) { - a2 = ""; - } - if (a3 == null) { - a3 = ""; - } - return a1 + a2 + a3; - } - - }; - return zipr; - } - - private FuncN<String> getConcatZipr() { - FuncN<String> zipr = new FuncN<String>() { - - @Override - public String call(Object... args) { - String returnValue = ""; - for (Object o : args) { - if (o != null) { - returnValue += getStringValue(o); - } - } - System.out.println("returning: " + returnValue); - return returnValue; - } - - }; - return zipr; - } - - private Func2<String, Integer, String> getConcatStringIntegerZipr() { - Func2<String, Integer, String> zipr = new Func2<String, Integer, String>() { - - @Override - public String call(String s, Integer i) { - return getStringValue(s) + getStringValue(i); - } - - }; - return zipr; - } - - private Func3<String, Integer, int[], String> getConcatStringIntegerIntArrayZipr() { - Func3<String, Integer, int[], String> zipr = new Func3<String, Integer, int[], String>() { - - @Override - public String call(String s, Integer i, int[] iArray) { - return getStringValue(s) + getStringValue(i) + getStringValue(iArray); - } - - }; - return zipr; - } - - private static String getStringValue(Object o) { - if (o == null) { - return ""; - } else { - if (o instanceof int[]) { - return Arrays.toString((int[]) o); - } else { - return String.valueOf(o); - } - } - } - - private static class TestObservable extends Observable<String> { - - Observer<String> Observer; - - @Override - public Subscription subscribe(Observer<String> Observer) { - // just store the variable where it can be accessed so we can manually trigger it - this.Observer = Observer; - return Observable.noOpSubscription(); - } - - } - } - -} diff --git a/rxjava-core/src/main/java/rx/operators/package.html b/rxjava-core/src/main/java/rx/operators/package.html deleted file mode 100644 index 80ba7542bf..0000000000 --- a/rxjava-core/src/main/java/rx/operators/package.html +++ /dev/null @@ -1,23 +0,0 @@ -<!-- - - Copyright 2013 Netflix, Inc. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - ---> -<body> - <p>Operators that allow composing Observables to transform and - manipulate data in an asynchronous, functional and thread-safe manner.</p> - <p>The operators are all exposed via the ObservableExtensions class</p> - -</body> \ No newline at end of file diff --git a/rxjava-core/src/main/java/rx/package-info.java b/rxjava-core/src/main/java/rx/package-info.java deleted file mode 100644 index 67d6870274..0000000000 --- a/rxjava-core/src/main/java/rx/package-info.java +++ /dev/null @@ -1,45 +0,0 @@ -/** - * Copyright 2013 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -/** - * <p>Rx Observables</p> - * - * <p>A library that enables subscribing to and composing asynchronous events and - * callbacks.</p> - * <p>The Observable/Observer interfaces and associated operators (in - * the .operations package) are inspired by and attempt to conform to the - * Reactive Rx library in Microsoft .Net.</p> - * <p> - * More information can be found at <a - * href="http://msdn.microsoft.com/en-us/data/gg577609">http://msdn.microsoft.com/en-us/data/gg577609</a>. - * </p> - * - * - * <p>Compared with the Microsoft implementation: - * <ul> - * <li>Observable == IObservable</li> - * <li>Observer == IObserver</li> - * <li>Subscription == IDisposable</li> - * <li>ObservableExtensions == Observable</li> - * </ul> - * </p> - * <p>Services which intend on exposing data asynchronously and wish - * to allow reactive processing and composition can implement the - * Watchable interface which then allows Watchers to subscribe to them - * and receive events.</p> - * <p>Usage examples can be found on the Watchable and Watcher - * classes.</p> - */ -package rx; \ No newline at end of file diff --git a/rxjava-core/src/main/java/rx/subjects/Subject.java b/rxjava-core/src/main/java/rx/subjects/Subject.java deleted file mode 100644 index bda5f1033e..0000000000 --- a/rxjava-core/src/main/java/rx/subjects/Subject.java +++ /dev/null @@ -1,126 +0,0 @@ -package rx.subjects; - -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicReference; - -import junit.framework.Assert; - -import org.junit.Test; - -import rx.Notification; -import rx.Observable; -import rx.Observer; -import rx.Subscription; -import rx.util.AtomicObservableSubscription; -import rx.util.SynchronizedObserver; -import rx.util.functions.Action1; -import rx.util.functions.Func1; - -public class Subject<T> extends Observable<T> implements Observer<T> { - public static <T> Subject<T> create() { - final ConcurrentHashMap<Subscription, Observer<T>> observers = new ConcurrentHashMap<Subscription, Observer<T>>(); - - Func1<Observer<T>, Subscription> onSubscribe = new Func1<Observer<T>, Subscription>() { - @Override - public Subscription call(Observer<T> observer) { - final AtomicObservableSubscription subscription = new AtomicObservableSubscription(); - - subscription.wrap(new Subscription() { - @Override - public void unsubscribe() { - // on unsubscribe remove it from the map of outbound observers to notify - observers.remove(subscription); - } - }); - - // on subscribe add it to the map of outbound observers to notify - observers.put(subscription, new SynchronizedObserver<T>(observer, subscription)); - return subscription; - } - }; - - return new Subject<T>(onSubscribe, observers); - } - - private final ConcurrentHashMap<Subscription, Observer<T>> observers; - - protected Subject(Func1<Observer<T>, Subscription> onSubscribe, ConcurrentHashMap<Subscription, Observer<T>> observers) { - super(onSubscribe); - this.observers = observers; - } - - @Override - public void onCompleted() { - for (Observer<T> observer : observers.values()) { - observer.onCompleted(); - } - } - - @Override - public void onError(Exception e) { - for (Observer<T> observer : observers.values()) { - observer.onError(e); - } - } - - @Override - public void onNext(T args) { - for (Observer<T> observer : observers.values()) { - observer.onNext(args); - } - } - - public static class UnitTest { - @Test - public void test() { - Subject<Integer> subject = Subject.create(); - final AtomicReference<List<Notification<String>>> actualRef = new AtomicReference<List<Notification<String>>>(); - - Observable<List<Notification<Integer>>> wNotificationsList = subject.materialize().toList(); - wNotificationsList.subscribe(new Action1<List<Notification<String>>>() { - @Override - public void call(List<Notification<String>> actual) { - actualRef.set(actual); - } - }); - - Subscription sub = Observable.create(new Func1<Observer<Integer>, Subscription>() { - @Override - public Subscription call(final Observer<Integer> observer) { - final AtomicBoolean stop = new AtomicBoolean(false); - new Thread() { - @Override - public void run() { - int i = 1; - while (!stop.get()) { - observer.onNext(i++); - } - observer.onCompleted(); - } - }.start(); - return new Subscription() { - @Override - public void unsubscribe() { - stop.set(true); - } - }; - } - }).subscribe(subject); - // the subject has received an onComplete from the first subscribe because - // it is synchronous and the next subscribe won't do anything. - Observable.toObservable(-1, -2, -3).subscribe(subject); - - List<Notification<Integer>> expected = new ArrayList<Notification<Integer>>(); - expected.add(new Notification<Integer>(-1)); - expected.add(new Notification<Integer>(-2)); - expected.add(new Notification<Integer>(-3)); - expected.add(new Notification<Integer>()); - Assert.assertTrue(actualRef.get().containsAll(expected)); - - sub.unsubscribe(); - } - } -} diff --git a/rxjava-core/src/main/java/rx/util/AtomicObservableSubscription.java b/rxjava-core/src/main/java/rx/util/AtomicObservableSubscription.java deleted file mode 100644 index 3b572a3f2c..0000000000 --- a/rxjava-core/src/main/java/rx/util/AtomicObservableSubscription.java +++ /dev/null @@ -1,77 +0,0 @@ -/** - * Copyright 2013 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package rx.util; - -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicReference; - -import javax.annotation.concurrent.ThreadSafe; - -import rx.Subscription; - -/** - * Thread-safe wrapper around Observable Subscription that ensures unsubscribe can be called only once. - * <p> - * Also used to: - * <p> - * <ul> - * <li>allow the AtomicObserver to have access to the subscription in asynchronous execution for checking if unsubscribed occurred without onComplete/onError.</li> - * <li>handle both synchronous and asynchronous subscribe() execution flows</li> - * </ul> - */ -@ThreadSafe -public final class AtomicObservableSubscription implements Subscription { - - private AtomicReference<Subscription> actualSubscription = new AtomicReference<Subscription>(); - private AtomicBoolean unsubscribed = new AtomicBoolean(false); - - public AtomicObservableSubscription() { - - } - - public AtomicObservableSubscription(Subscription actualSubscription) { - this.actualSubscription.set(actualSubscription); - } - - /** - * Wraps the actual subscription once it exists (if it wasn't available when constructed) - * - * @param actualSubscription - * @throws IllegalStateException - * if trying to set more than once (or use this method after setting via constructor) - */ - public AtomicObservableSubscription wrap(Subscription actualSubscription) { - if (!this.actualSubscription.compareAndSet(null, actualSubscription)) { - throw new IllegalStateException("Can not set subscription more than once."); - } - return this; - } - - @Override - public void unsubscribe() { - // get the real thing and set to null in an atomic operation so we will only ever call unsubscribe once - Subscription actual = actualSubscription.getAndSet(null); - // if it's not null we will unsubscribe - if (actual != null) { - actual.unsubscribe(); - unsubscribed.set(true); - } - } - - public boolean isUnsubscribed() { - return unsubscribed.get(); - } -} diff --git a/rxjava-core/src/main/java/rx/util/AtomicObserver.java b/rxjava-core/src/main/java/rx/util/AtomicObserver.java deleted file mode 100644 index 86eee9ccfa..0000000000 --- a/rxjava-core/src/main/java/rx/util/AtomicObserver.java +++ /dev/null @@ -1,75 +0,0 @@ -package rx.util; - -import java.util.concurrent.atomic.AtomicBoolean; - -import rx.Observer; - -/** - * Wrapper around Observer to ensure compliance with Rx contract. - * <p> - * The following is taken from the Rx Design Guidelines document: http://go.microsoft.com/fwlink/?LinkID=205219 - * <pre> - * Messages sent to instances of the IObserver interface follow the following grammar: - * - * OnNext* (OnCompleted | OnError)? - * - * This grammar allows observable sequences to send any amount (0 or more) of OnNext messages to the subscribed - * observer instance, optionally followed by a single success (OnCompleted) or failure (OnError) message. - * - * The single message indicating that an observable sequence has finished ensures that consumers of the observable - * sequence can deterministically establish that it is safe to perform cleanup operations. - * - * A single failure further ensures that abort semantics can be maintained for operators that work on - * multiple observable sequences (see paragraph 6.6). - * </pre> - * - * <p> - * This wrapper will do the following: - * <ul> - * <li>Allow only single execution of either onError or onCompleted.</li> - * <li>Once an onComplete or onError are performed, no further calls can be executed</li> - * <li>If unsubscribe is called, this means we call completed() and don't allow any further onNext calls.</li> - * <li>When onError or onComplete occur it will unsubscribe from the Observable (if executing asynchronously).</li> - * </ul> - * <p> - * It will not synchronized onNext execution. Use the {@link SynchronizedObserver} to do that. - * - * @param <T> - */ -public class AtomicObserver<T> implements Observer<T> { - - private final Observer<T> actual; - private final AtomicBoolean isFinished = new AtomicBoolean(false); - private final AtomicObservableSubscription subscription; - - public AtomicObserver(AtomicObservableSubscription subscription, Observer<T> actual) { - this.subscription = subscription; - this.actual = actual; - } - - @Override - public void onCompleted() { - if (isFinished.compareAndSet(false, true)) { - actual.onCompleted(); - // auto-unsubscribe - subscription.unsubscribe(); - } - } - - @Override - public void onError(Exception e) { - if (isFinished.compareAndSet(false, true)) { - actual.onError(e); - // auto-unsubscribe - subscription.unsubscribe(); - } - } - - @Override - public void onNext(T args) { - if (!isFinished.get()) { - actual.onNext(args); - } - } - -} diff --git a/rxjava-core/src/main/java/rx/util/CompositeException.java b/rxjava-core/src/main/java/rx/util/CompositeException.java deleted file mode 100644 index c19a5f5ff0..0000000000 --- a/rxjava-core/src/main/java/rx/util/CompositeException.java +++ /dev/null @@ -1,65 +0,0 @@ -/** - * Copyright 2013 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package rx.util; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.List; - -/** - * Exception that is a composite of 1 or more other exceptions. - * <p> - * The <code>getMessage()</code> will return a concatenation of the composite exceptions. - */ -public class CompositeException extends RuntimeException { - - private static final long serialVersionUID = 3026362227162912146L; - - private final List<Exception> exceptions; - private final String message; - - public CompositeException(String messagePrefix, Collection<Exception> errors) { - StringBuilder _message = new StringBuilder(); - if (messagePrefix != null) { - _message.append(messagePrefix).append(" => "); - } - - List<Exception> _exceptions = new ArrayList<Exception>(); - for (Exception e : errors) { - _exceptions.add(e); - if (_message.length() > 0) { - _message.append(", "); - } - _message.append(e.getClass().getSimpleName()).append(":").append(e.getMessage()); - } - this.exceptions = Collections.unmodifiableList(_exceptions); - this.message = _message.toString(); - } - - public CompositeException(Collection<Exception> errors) { - this(null, errors); - } - - public List<Exception> getExceptions() { - return exceptions; - } - - @Override - public String getMessage() { - return message; - } -} \ No newline at end of file diff --git a/rxjava-core/src/main/java/rx/util/SynchronizedObserver.java b/rxjava-core/src/main/java/rx/util/SynchronizedObserver.java deleted file mode 100644 index eddb0c85ed..0000000000 --- a/rxjava-core/src/main/java/rx/util/SynchronizedObserver.java +++ /dev/null @@ -1,650 +0,0 @@ -/** - * Copyright 2013 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package rx.util; - -import static org.junit.Assert.*; -import static org.mockito.Matchers.*; -import static org.mockito.Mockito.*; - -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.Future; -import java.util.concurrent.LinkedBlockingQueue; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicInteger; - -import javax.annotation.concurrent.ThreadSafe; - -import org.junit.Before; -import org.junit.Test; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; - -import rx.Observable; -import rx.Observer; -import rx.Subscription; -import rx.util.functions.Func1; - -/** - * A thread-safe Observer for transitioning states in operators. - * <p> - * Execution rules are: - * <ul> - * <li>Allow only single-threaded, synchronous, ordered execution of onNext, onCompleted, onError</li> - * <li>Once an onComplete or onError are performed, no further calls can be executed</li> - * <li>If unsubscribe is called, this means we call completed() and don't allow any further onNext calls.</li> - * </ul> - * - * @param <T> - */ -@ThreadSafe -public final class SynchronizedObserver<T> implements Observer<T> { - - /** - * Intrinsic synchronized locking with double-check short-circuiting was chosen after testing several other implementations. - * - * The code and results can be found here: - * - https://github.com/benjchristensen/JavaLockPerformanceTests/tree/master/results/Observer - * - https://github.com/benjchristensen/JavaLockPerformanceTests/tree/master/src/com/benjchristensen/performance/locks/Observer - * - * The major characteristic that made me choose synchronized instead of Reentrant or a customer AbstractQueueSynchronizer implementation - * is that intrinsic locking performed better when nested, and AtomicObserver will end up nested most of the time since Rx is - * compositional by its very nature. - * - * // TODO composing of this class should rarely happen now with updated design so this decision should be revisited - */ - - private final Observer<T> observer; - private final AtomicObservableSubscription subscription; - private volatile boolean finishRequested = false; - private volatile boolean finished = false; - - public SynchronizedObserver(Observer<T> Observer, AtomicObservableSubscription subscription) { - this.observer = Observer; - this.subscription = subscription; - } - - public void onNext(T arg) { - if (finished || finishRequested || subscription.isUnsubscribed()) { - // if we're already stopped, or a finish request has been received, we won't allow further onNext requests - return; - } - synchronized (this) { - // check again since this could have changed while waiting - if (finished || finishRequested || subscription.isUnsubscribed()) { - // if we're already stopped, or a finish request has been received, we won't allow further onNext requests - return; - } - observer.onNext(arg); - } - } - - public void onError(Exception e) { - if (finished || subscription.isUnsubscribed()) { - // another thread has already finished us, so we won't proceed - return; - } - finishRequested = true; - synchronized (this) { - // check again since this could have changed while waiting - if (finished || subscription.isUnsubscribed()) { - return; - } - observer.onError(e); - finished = true; - } - } - - public void onCompleted() { - if (finished || subscription.isUnsubscribed()) { - // another thread has already finished us, so we won't proceed - return; - } - finishRequested = true; - synchronized (this) { - // check again since this could have changed while waiting - if (finished || subscription.isUnsubscribed()) { - return; - } - observer.onCompleted(); - finished = true; - } - } - - public static class UnitTest { - @Mock - Observer<String> aObserver; - - @Before - public void before() { - MockitoAnnotations.initMocks(this); - } - - @Test - public void testSingleThreadedBasic() { - Subscription s = mock(Subscription.class); - TestSingleThreadedObservable onSubscribe = new TestSingleThreadedObservable(s, "one", "two", "three"); - Observable<String> w = Observable.create(onSubscribe); - - AtomicObservableSubscription as = new AtomicObservableSubscription(s); - SynchronizedObserver<String> aw = new SynchronizedObserver<String>(aObserver, as); - - w.subscribe(aw); - onSubscribe.waitToFinish(); - - verify(aObserver, times(1)).onNext("one"); - verify(aObserver, times(1)).onNext("two"); - verify(aObserver, times(1)).onNext("three"); - verify(aObserver, never()).onError(any(Exception.class)); - verify(aObserver, times(1)).onCompleted(); - // non-deterministic because unsubscribe happens after 'waitToFinish' releases - // so commenting out for now as this is not a critical thing to test here - // verify(s, times(1)).unsubscribe(); - } - - @Test - public void testMultiThreadedBasic() { - Subscription s = mock(Subscription.class); - TestMultiThreadedObservable onSubscribe = new TestMultiThreadedObservable(s, "one", "two", "three"); - Observable<String> w = Observable.create(onSubscribe); - - AtomicObservableSubscription as = new AtomicObservableSubscription(s); - BusyObserver busyObserver = new BusyObserver(); - SynchronizedObserver<String> aw = new SynchronizedObserver<String>(busyObserver, as); - - w.subscribe(aw); - onSubscribe.waitToFinish(); - - assertEquals(3, busyObserver.onNextCount.get()); - assertFalse(busyObserver.onError); - assertTrue(busyObserver.onCompleted); - // non-deterministic because unsubscribe happens after 'waitToFinish' releases - // so commenting out for now as this is not a critical thing to test here - // verify(s, times(1)).unsubscribe(); - - // we can have concurrency ... - assertTrue(onSubscribe.maxConcurrentThreads.get() > 1); - // ... but the onNext execution should be single threaded - assertEquals(1, busyObserver.maxConcurrentThreads.get()); - } - - @Test - public void testMultiThreadedWithNPE() { - Subscription s = mock(Subscription.class); - TestMultiThreadedObservable onSubscribe = new TestMultiThreadedObservable(s, "one", "two", "three", null); - Observable<String> w = Observable.create(onSubscribe); - - AtomicObservableSubscription as = new AtomicObservableSubscription(s); - BusyObserver busyObserver = new BusyObserver(); - SynchronizedObserver<String> aw = new SynchronizedObserver<String>(busyObserver, as); - - w.subscribe(aw); - onSubscribe.waitToFinish(); - - System.out.println("maxConcurrentThreads: " + onSubscribe.maxConcurrentThreads.get()); - - // we can't know how many onNext calls will occur since they each run on a separate thread - // that depends on thread scheduling so 0, 1, 2 and 3 are all valid options - // assertEquals(3, busyObserver.onNextCount.get()); - assertTrue(busyObserver.onNextCount.get() < 4); - assertTrue(busyObserver.onError); - // no onCompleted because onError was invoked - assertFalse(busyObserver.onCompleted); - // non-deterministic because unsubscribe happens after 'waitToFinish' releases - // so commenting out for now as this is not a critical thing to test here - //verify(s, times(1)).unsubscribe(); - - // we can have concurrency ... - assertTrue(onSubscribe.maxConcurrentThreads.get() > 1); - // ... but the onNext execution should be single threaded - assertEquals(1, busyObserver.maxConcurrentThreads.get()); - } - - @Test - public void testMultiThreadedWithNPEinMiddle() { - Subscription s = mock(Subscription.class); - TestMultiThreadedObservable onSubscribe = new TestMultiThreadedObservable(s, "one", "two", "three", null, "four", "five", "six", "seven", "eight", "nine"); - Observable<String> w = Observable.create(onSubscribe); - - AtomicObservableSubscription as = new AtomicObservableSubscription(s); - BusyObserver busyObserver = new BusyObserver(); - SynchronizedObserver<String> aw = new SynchronizedObserver<String>(busyObserver, as); - - w.subscribe(aw); - onSubscribe.waitToFinish(); - - System.out.println("maxConcurrentThreads: " + onSubscribe.maxConcurrentThreads.get()); - // this should not be the full number of items since the error should stop it before it completes all 9 - System.out.println("onNext count: " + busyObserver.onNextCount.get()); - assertTrue(busyObserver.onNextCount.get() < 9); - assertTrue(busyObserver.onError); - // no onCompleted because onError was invoked - assertFalse(busyObserver.onCompleted); - // non-deterministic because unsubscribe happens after 'waitToFinish' releases - // so commenting out for now as this is not a critical thing to test here - // verify(s, times(1)).unsubscribe(); - - // we can have concurrency ... - assertTrue(onSubscribe.maxConcurrentThreads.get() > 1); - // ... but the onNext execution should be single threaded - assertEquals(1, busyObserver.maxConcurrentThreads.get()); - } - - /** - * A non-realistic use case that tries to expose thread-safety issues by throwing lots of out-of-order - * events on many threads. - * - * @param w - * @param tw - */ - @Test - public void runConcurrencyTest() { - ExecutorService tp = Executors.newFixedThreadPool(20); - try { - TestConcurrencyObserver tw = new TestConcurrencyObserver(); - AtomicObservableSubscription s = new AtomicObservableSubscription(); - SynchronizedObserver<String> w = new SynchronizedObserver<String>(tw, s); - - Future<?> f1 = tp.submit(new OnNextThread(w, 12000)); - Future<?> f2 = tp.submit(new OnNextThread(w, 5000)); - Future<?> f3 = tp.submit(new OnNextThread(w, 75000)); - Future<?> f4 = tp.submit(new OnNextThread(w, 13500)); - Future<?> f5 = tp.submit(new OnNextThread(w, 22000)); - Future<?> f6 = tp.submit(new OnNextThread(w, 15000)); - Future<?> f7 = tp.submit(new OnNextThread(w, 7500)); - Future<?> f8 = tp.submit(new OnNextThread(w, 23500)); - - Future<?> f10 = tp.submit(new CompletionThread(w, TestConcurrencyObserverEvent.onCompleted, f1, f2, f3, f4)); - try { - Thread.sleep(1); - } catch (InterruptedException e) { - // ignore - } - Future<?> f11 = tp.submit(new CompletionThread(w, TestConcurrencyObserverEvent.onCompleted, f4, f6, f7)); - Future<?> f12 = tp.submit(new CompletionThread(w, TestConcurrencyObserverEvent.onCompleted, f4, f6, f7)); - Future<?> f13 = tp.submit(new CompletionThread(w, TestConcurrencyObserverEvent.onCompleted, f4, f6, f7)); - Future<?> f14 = tp.submit(new CompletionThread(w, TestConcurrencyObserverEvent.onCompleted, f4, f6, f7)); - // // the next 4 onError events should wait on same as f10 - Future<?> f15 = tp.submit(new CompletionThread(w, TestConcurrencyObserverEvent.onError, f1, f2, f3, f4)); - Future<?> f16 = tp.submit(new CompletionThread(w, TestConcurrencyObserverEvent.onError, f1, f2, f3, f4)); - Future<?> f17 = tp.submit(new CompletionThread(w, TestConcurrencyObserverEvent.onError, f1, f2, f3, f4)); - Future<?> f18 = tp.submit(new CompletionThread(w, TestConcurrencyObserverEvent.onError, f1, f2, f3, f4)); - - waitOnThreads(f1, f2, f3, f4, f5, f6, f7, f8, f10, f11, f12, f13, f14, f15, f16, f17, f18); - @SuppressWarnings("unused") - int numNextEvents = tw.assertEvents(null); // no check of type since we don't want to test barging results here, just interleaving behavior - // System.out.println("Number of events executed: " + numNextEvents); - } catch (Exception e) { - fail("Concurrency test failed: " + e.getMessage()); - e.printStackTrace(); - } finally { - tp.shutdown(); - try { - tp.awaitTermination(5000, TimeUnit.MILLISECONDS); - } catch (InterruptedException e) { - e.printStackTrace(); - } - } - } - - private static void waitOnThreads(Future<?>... futures) { - for (Future<?> f : futures) { - try { - f.get(10, TimeUnit.SECONDS); - } catch (Exception e) { - System.err.println("Failed while waiting on future."); - e.printStackTrace(); - } - } - } - - /** - * A thread that will pass data to onNext - */ - public static class OnNextThread implements Runnable { - - private final Observer<String> Observer; - private final int numStringsToSend; - - OnNextThread(Observer<String> Observer, int numStringsToSend) { - this.Observer = Observer; - this.numStringsToSend = numStringsToSend; - } - - @Override - public void run() { - for (int i = 0; i < numStringsToSend; i++) { - Observer.onNext("aString"); - } - } - } - - /** - * A thread that will call onError or onNext - */ - public static class CompletionThread implements Runnable { - - private final Observer<String> Observer; - private final TestConcurrencyObserverEvent event; - private final Future<?>[] waitOnThese; - - CompletionThread(Observer<String> Observer, TestConcurrencyObserverEvent event, Future<?>... waitOnThese) { - this.Observer = Observer; - this.event = event; - this.waitOnThese = waitOnThese; - } - - @Override - public void run() { - /* if we have 'waitOnThese' futures, we'll wait on them before proceeding */ - if (waitOnThese != null) { - for (Future<?> f : waitOnThese) { - try { - f.get(); - } catch (Exception e) { - System.err.println("Error while waiting on future in CompletionThread"); - } - } - } - - /* send the event */ - if (event == TestConcurrencyObserverEvent.onError) { - Observer.onError(new RuntimeException("mocked exception")); - } else if (event == TestConcurrencyObserverEvent.onCompleted) { - Observer.onCompleted(); - - } else { - throw new IllegalArgumentException("Expecting either onError or onCompleted"); - } - } - } - - private static enum TestConcurrencyObserverEvent { - onCompleted, onError, onNext - } - - private static class TestConcurrencyObserver implements Observer<String> { - - /** used to store the order and number of events received */ - private final LinkedBlockingQueue<TestConcurrencyObserverEvent> events = new LinkedBlockingQueue<TestConcurrencyObserverEvent>(); - private final int waitTime; - - @SuppressWarnings("unused") - public TestConcurrencyObserver(int waitTimeInNext) { - this.waitTime = waitTimeInNext; - } - - public TestConcurrencyObserver() { - this.waitTime = 0; - } - - @Override - public void onCompleted() { - events.add(TestConcurrencyObserverEvent.onCompleted); - } - - @Override - public void onError(Exception e) { - events.add(TestConcurrencyObserverEvent.onError); - } - - @Override - public void onNext(String args) { - events.add(TestConcurrencyObserverEvent.onNext); - // do some artificial work to make the thread scheduling/timing vary - int s = 0; - for (int i = 0; i < 20; i++) { - s += s * i; - } - - if (waitTime > 0) { - try { - Thread.sleep(waitTime); - } catch (InterruptedException e) { - // ignore - } - } - } - - /** - * Assert the order of events is correct and return the number of onNext executions. - * - * @param expectedEndingEvent - * @return int count of onNext calls - * @throws IllegalStateException - * If order of events was invalid. - */ - public int assertEvents(TestConcurrencyObserverEvent expectedEndingEvent) throws IllegalStateException { - int nextCount = 0; - boolean finished = false; - for (TestConcurrencyObserverEvent e : events) { - if (e == TestConcurrencyObserverEvent.onNext) { - if (finished) { - // already finished, we shouldn't get this again - throw new IllegalStateException("Received onNext but we're already finished."); - } - nextCount++; - } else if (e == TestConcurrencyObserverEvent.onError) { - if (finished) { - // already finished, we shouldn't get this again - throw new IllegalStateException("Received onError but we're already finished."); - } - if (expectedEndingEvent != null && TestConcurrencyObserverEvent.onError != expectedEndingEvent) { - throw new IllegalStateException("Received onError ending event but expected " + expectedEndingEvent); - } - finished = true; - } else if (e == TestConcurrencyObserverEvent.onCompleted) { - if (finished) { - // already finished, we shouldn't get this again - throw new IllegalStateException("Received onCompleted but we're already finished."); - } - if (expectedEndingEvent != null && TestConcurrencyObserverEvent.onCompleted != expectedEndingEvent) { - throw new IllegalStateException("Received onCompleted ending event but expected " + expectedEndingEvent); - } - finished = true; - } - } - - return nextCount; - } - - } - - /** - * This spawns a single thread for the subscribe execution - * - */ - private static class TestSingleThreadedObservable implements Func1<Observer<String>, Subscription> { - - final Subscription s; - final String[] values; - private Thread t = null; - - public TestSingleThreadedObservable(final Subscription s, final String... values) { - this.s = s; - this.values = values; - - } - - public Subscription call(final Observer<String> observer) { - System.out.println("TestSingleThreadedObservable subscribed to ..."); - t = new Thread(new Runnable() { - - @Override - public void run() { - try { - System.out.println("running TestSingleThreadedObservable thread"); - for (String s : values) { - System.out.println("TestSingleThreadedObservable onNext: " + s); - observer.onNext(s); - } - observer.onCompleted(); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - - }); - System.out.println("starting TestSingleThreadedObservable thread"); - t.start(); - System.out.println("done starting TestSingleThreadedObservable thread"); - return s; - } - - public void waitToFinish() { - try { - t.join(); - } catch (InterruptedException e) { - throw new RuntimeException(e); - } - } - - } - - /** - * This spawns a thread for the subscription, then a separate thread for each onNext call. - * - */ - private static class TestMultiThreadedObservable implements Func1<Observer<String>, Subscription> { - - final Subscription s; - final String[] values; - Thread t = null; - AtomicInteger threadsRunning = new AtomicInteger(); - AtomicInteger maxConcurrentThreads = new AtomicInteger(); - ExecutorService threadPool; - - public TestMultiThreadedObservable(Subscription s, String... values) { - this.s = s; - this.values = values; - this.threadPool = Executors.newCachedThreadPool(); - } - - @Override - public Subscription call(final Observer<String> observer) { - System.out.println("TestMultiThreadedObservable subscribed to ..."); - t = new Thread(new Runnable() { - - @Override - public void run() { - try { - System.out.println("running TestMultiThreadedObservable thread"); - for (final String s : values) { - threadPool.execute(new Runnable() { - - @Override - public void run() { - threadsRunning.incrementAndGet(); - try { - // perform onNext call - System.out.println("TestMultiThreadedObservable onNext: " + s); - if (s == null) { - // force an error - throw new NullPointerException(); - } - observer.onNext(s); - // capture 'maxThreads' - int concurrentThreads = threadsRunning.get(); - int maxThreads = maxConcurrentThreads.get(); - if (concurrentThreads > maxThreads) { - maxConcurrentThreads.compareAndSet(maxThreads, concurrentThreads); - } - } catch (Exception e) { - observer.onError(e); - } finally { - threadsRunning.decrementAndGet(); - } - } - }); - } - // we are done spawning threads - threadPool.shutdown(); - } catch (Exception e) { - throw new RuntimeException(e); - } - - // wait until all threads are done, then mark it as COMPLETED - try { - // wait for all the threads to finish - threadPool.awaitTermination(2, TimeUnit.SECONDS); - } catch (InterruptedException e) { - throw new RuntimeException(e); - } - observer.onCompleted(); - } - }); - System.out.println("starting TestMultiThreadedObservable thread"); - t.start(); - System.out.println("done starting TestMultiThreadedObservable thread"); - return s; - } - - public void waitToFinish() { - try { - t.join(); - } catch (InterruptedException e) { - throw new RuntimeException(e); - } - } - } - - private static class BusyObserver implements Observer<String> { - volatile boolean onCompleted = false; - volatile boolean onError = false; - AtomicInteger onNextCount = new AtomicInteger(); - AtomicInteger threadsRunning = new AtomicInteger(); - AtomicInteger maxConcurrentThreads = new AtomicInteger(); - - @Override - public void onCompleted() { - System.out.println(">>> BusyObserver received onCompleted"); - onCompleted = true; - } - - @Override - public void onError(Exception e) { - System.out.println(">>> BusyObserver received onError: " + e.getMessage()); - onError = true; - } - - @Override - public void onNext(String args) { - threadsRunning.incrementAndGet(); - try { - onNextCount.incrementAndGet(); - System.out.println(">>> BusyObserver received onNext: " + args); - try { - // simulate doing something computational - Thread.sleep(200); - } catch (InterruptedException e) { - e.printStackTrace(); - } - } finally { - // capture 'maxThreads' - int concurrentThreads = threadsRunning.get(); - int maxThreads = maxConcurrentThreads.get(); - if (concurrentThreads > maxThreads) { - maxConcurrentThreads.compareAndSet(maxThreads, concurrentThreads); - } - threadsRunning.decrementAndGet(); - } - } - - } - - } - -} \ No newline at end of file diff --git a/rxjava-core/src/main/java/rx/util/functions/Action0.java b/rxjava-core/src/main/java/rx/util/functions/Action0.java deleted file mode 100644 index 62d57bd563..0000000000 --- a/rxjava-core/src/main/java/rx/util/functions/Action0.java +++ /dev/null @@ -1,20 +0,0 @@ -/** - * Copyright 2013 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package rx.util.functions; - -public interface Action0 extends Function { - public void call(); -} \ No newline at end of file diff --git a/rxjava-core/src/main/java/rx/util/functions/Action1.java b/rxjava-core/src/main/java/rx/util/functions/Action1.java deleted file mode 100644 index 14fa7ced8c..0000000000 --- a/rxjava-core/src/main/java/rx/util/functions/Action1.java +++ /dev/null @@ -1,20 +0,0 @@ -/** - * Copyright 2013 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package rx.util.functions; - -public interface Action1<T1> extends Function { - public void call(T1 t1); -} \ No newline at end of file diff --git a/rxjava-core/src/main/java/rx/util/functions/Action2.java b/rxjava-core/src/main/java/rx/util/functions/Action2.java deleted file mode 100644 index 8a17875a9e..0000000000 --- a/rxjava-core/src/main/java/rx/util/functions/Action2.java +++ /dev/null @@ -1,20 +0,0 @@ -/** - * Copyright 2013 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package rx.util.functions; - -public interface Action2<T1, T2> extends Function { - public void call(T1 t1, T2 t2); -} \ No newline at end of file diff --git a/rxjava-core/src/main/java/rx/util/functions/Action3.java b/rxjava-core/src/main/java/rx/util/functions/Action3.java deleted file mode 100644 index 2b613b621e..0000000000 --- a/rxjava-core/src/main/java/rx/util/functions/Action3.java +++ /dev/null @@ -1,20 +0,0 @@ -/** - * Copyright 2013 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package rx.util.functions; - -public interface Action3<T1, T2, T3> extends Function { - public void call(T1 t1, T2 t2, T3 t3); -} \ No newline at end of file diff --git a/rxjava-core/src/main/java/rx/util/functions/Func0.java b/rxjava-core/src/main/java/rx/util/functions/Func0.java deleted file mode 100644 index 4e7d397037..0000000000 --- a/rxjava-core/src/main/java/rx/util/functions/Func0.java +++ /dev/null @@ -1,20 +0,0 @@ -/** - * Copyright 2013 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package rx.util.functions; - -public interface Func0<R> extends Function { - public R call(); -} \ No newline at end of file diff --git a/rxjava-core/src/main/java/rx/util/functions/Func1.java b/rxjava-core/src/main/java/rx/util/functions/Func1.java deleted file mode 100644 index 3eb17e625c..0000000000 --- a/rxjava-core/src/main/java/rx/util/functions/Func1.java +++ /dev/null @@ -1,20 +0,0 @@ -/** - * Copyright 2013 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package rx.util.functions; - -public interface Func1<T1, R> extends Function { - public R call(T1 t1); -} \ No newline at end of file diff --git a/rxjava-core/src/main/java/rx/util/functions/Func2.java b/rxjava-core/src/main/java/rx/util/functions/Func2.java deleted file mode 100644 index ab2a04442d..0000000000 --- a/rxjava-core/src/main/java/rx/util/functions/Func2.java +++ /dev/null @@ -1,20 +0,0 @@ -/** - * Copyright 2013 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package rx.util.functions; - -public interface Func2<T1, T2, R> extends Function { - public R call(T1 t1, T2 t2); -} \ No newline at end of file diff --git a/rxjava-core/src/main/java/rx/util/functions/Func3.java b/rxjava-core/src/main/java/rx/util/functions/Func3.java deleted file mode 100644 index fc1dfb49b0..0000000000 --- a/rxjava-core/src/main/java/rx/util/functions/Func3.java +++ /dev/null @@ -1,20 +0,0 @@ -/** - * Copyright 2013 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package rx.util.functions; - -public interface Func3<T1, T2, T3, R> extends Function { - public R call(T1 t1, T2 t2, T3 t3); -} \ No newline at end of file diff --git a/rxjava-core/src/main/java/rx/util/functions/Func4.java b/rxjava-core/src/main/java/rx/util/functions/Func4.java deleted file mode 100644 index 09d738ff72..0000000000 --- a/rxjava-core/src/main/java/rx/util/functions/Func4.java +++ /dev/null @@ -1,20 +0,0 @@ -/** - * Copyright 2013 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package rx.util.functions; - -public interface Func4<T1, T2, T3, T4, R> extends Function { - public R call(T1 t1, T2 t2, T3 t3, T4 t4); -} \ No newline at end of file diff --git a/rxjava-core/src/main/java/rx/util/functions/Func5.java b/rxjava-core/src/main/java/rx/util/functions/Func5.java deleted file mode 100644 index b6550ef0b9..0000000000 --- a/rxjava-core/src/main/java/rx/util/functions/Func5.java +++ /dev/null @@ -1,20 +0,0 @@ -/** - * Copyright 2013 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package rx.util.functions; - -public interface Func5<T1, T2, T3, T4, T5, R> extends Function { - public R call(T1 t1, T2 t2, T3 t3, T4 t4, T5 t5); -} \ No newline at end of file diff --git a/rxjava-core/src/main/java/rx/util/functions/Func6.java b/rxjava-core/src/main/java/rx/util/functions/Func6.java deleted file mode 100644 index a241d55538..0000000000 --- a/rxjava-core/src/main/java/rx/util/functions/Func6.java +++ /dev/null @@ -1,20 +0,0 @@ -/** - * Copyright 2013 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package rx.util.functions; - -public interface Func6<T1, T2, T3, T4, T5, T6, R> extends Function { - public R call(T1 t1, T2 t2, T3 t3, T4 t4, T5 t5, T6 t6); -} \ No newline at end of file diff --git a/rxjava-core/src/main/java/rx/util/functions/Func7.java b/rxjava-core/src/main/java/rx/util/functions/Func7.java deleted file mode 100644 index 972cc405de..0000000000 --- a/rxjava-core/src/main/java/rx/util/functions/Func7.java +++ /dev/null @@ -1,20 +0,0 @@ -/** - * Copyright 2013 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package rx.util.functions; - -public interface Func7<T1, T2, T3, T4, T5, T6, T7, R> extends Function { - public R call(T1 t1, T2 t2, T3 t3, T4 t4, T5 t5, T6 t6, T7 t7); -} \ No newline at end of file diff --git a/rxjava-core/src/main/java/rx/util/functions/Func8.java b/rxjava-core/src/main/java/rx/util/functions/Func8.java deleted file mode 100644 index 5cbd10c1f7..0000000000 --- a/rxjava-core/src/main/java/rx/util/functions/Func8.java +++ /dev/null @@ -1,20 +0,0 @@ -/** - * Copyright 2013 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package rx.util.functions; - -public interface Func8<T1, T2, T3, T4, T5, T6, T7, T8, R> extends Function { - public R call(T1 t1, T2 t2, T3 t3, T4 t4, T5 t5, T6 t6, T7 t7, T8 t8); -} \ No newline at end of file diff --git a/rxjava-core/src/main/java/rx/util/functions/Func9.java b/rxjava-core/src/main/java/rx/util/functions/Func9.java deleted file mode 100644 index 8f2e546e4a..0000000000 --- a/rxjava-core/src/main/java/rx/util/functions/Func9.java +++ /dev/null @@ -1,20 +0,0 @@ -/** - * Copyright 2013 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package rx.util.functions; - -public interface Func9<T1, T2, T3, T4, T5, T6, T7, T8, T9, R> extends Function { - public R call(T1 t1, T2 t2, T3 t3, T4 t4, T5 t5, T6 t6, T7 t7, T8 t8, T9 t9); -} \ No newline at end of file diff --git a/rxjava-core/src/main/java/rx/util/functions/FuncN.java b/rxjava-core/src/main/java/rx/util/functions/FuncN.java deleted file mode 100644 index 7b1b48763b..0000000000 --- a/rxjava-core/src/main/java/rx/util/functions/FuncN.java +++ /dev/null @@ -1,20 +0,0 @@ -/** - * Copyright 2013 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package rx.util.functions; - -public interface FuncN<R> extends Function { - public R call(Object... args); -} \ No newline at end of file diff --git a/rxjava-core/src/main/java/rx/util/functions/Function.java b/rxjava-core/src/main/java/rx/util/functions/Function.java deleted file mode 100644 index cfe85a221f..0000000000 --- a/rxjava-core/src/main/java/rx/util/functions/Function.java +++ /dev/null @@ -1,10 +0,0 @@ -package rx.util.functions; - -/** - * All Func and Action interfaces extend from this. - * <p> - * Marker interface to allow isntanceof checks. - */ -public interface Function { - -} diff --git a/rxjava-core/src/main/java/rx/util/functions/FunctionLanguageAdaptor.java b/rxjava-core/src/main/java/rx/util/functions/FunctionLanguageAdaptor.java deleted file mode 100644 index 6ec87f358a..0000000000 --- a/rxjava-core/src/main/java/rx/util/functions/FunctionLanguageAdaptor.java +++ /dev/null @@ -1,39 +0,0 @@ -/** - * Copyright 2013 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package rx.util.functions; - -public interface FunctionLanguageAdaptor { - - /** - * Invoke the function and return the results. - * - * @param function - * @param args - * @return Object results from function execution - */ - Object call(Object function, Object[] args); - - /** - * The Class of the Function that this adaptor serves. - * <p> - * Example: groovy.lang.Closure - * <p> - * This should not return classes of java.* packages. - * - * @return Class[] of classes that this adaptor should be invoked for. - */ - public Class<?>[] getFunctionClass(); -} diff --git a/rxjava-core/src/main/java/rx/util/functions/Functions.java b/rxjava-core/src/main/java/rx/util/functions/Functions.java deleted file mode 100644 index 6b9237cdf1..0000000000 --- a/rxjava-core/src/main/java/rx/util/functions/Functions.java +++ /dev/null @@ -1,550 +0,0 @@ -/** - * Copyright 2013 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package rx.util.functions; - -import java.util.Collection; -import java.util.concurrent.ConcurrentHashMap; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * Allows execution of functions from multiple different languages. - * <p> - * Language support is provided via implementations of {@link FunctionLanguageAdaptor}. - * <p> - * This class will dynamically look for known language adaptors on the classpath at startup or new ones can be registered using {@link #registerLanguageAdaptor(Class, FunctionLanguageAdaptor)}. - */ -public class Functions { - - private static final Logger logger = LoggerFactory.getLogger(Functions.class); - - private final static ConcurrentHashMap<Class<?>, FunctionLanguageAdaptor> languageAdaptors = new ConcurrentHashMap<Class<?>, FunctionLanguageAdaptor>(); - - static { - /* optimistically look for supported languages if they are in the classpath */ - loadLanguageAdaptor("Groovy"); - loadLanguageAdaptor("JRuby"); - loadLanguageAdaptor("Clojure"); - loadLanguageAdaptor("Scala"); - // as new languages arise we can add them here but this does not prevent someone from using 'registerLanguageAdaptor' directly - } - - private static void loadLanguageAdaptor(String name) { - String className = "rx.lang." + name.toLowerCase() + "." + name + "Adaptor"; - try { - Class<?> c = Class.forName(className); - FunctionLanguageAdaptor a = (FunctionLanguageAdaptor) c.newInstance(); - registerLanguageAdaptor(a.getFunctionClass(), a); - } catch (ClassNotFoundException e) { - logger.info("Could not found function language adaptor: " + name + " with path: " + className); - } catch (Exception e) { - logger.error("Failed trying to initialize function language adaptor: " + className, e); - } - } - - public static void registerLanguageAdaptor(Class<?>[] functionClasses, FunctionLanguageAdaptor adaptor) { - for (Class<?> functionClass : functionClasses) { - if (functionClass.getPackage().getName().startsWith("java.")) { - throw new IllegalArgumentException("FunctionLanguageAdaptor implementations can not specify java.lang.* classes."); - } - languageAdaptors.put(functionClass, adaptor); - } - } - - public static void removeLanguageAdaptor(Class<?> functionClass) { - languageAdaptors.remove(functionClass); - } - - public static Collection<FunctionLanguageAdaptor> getRegisteredLanguageAdaptors() { - return languageAdaptors.values(); - } - - /** - * Utility method for determining the type of closure/function and executing it. - * - * @param function - * @param args - */ - @SuppressWarnings({ "rawtypes" }) - public static FuncN from(final Object function) { - if (function == null) { - throw new RuntimeException("function is null. Can't send arguments to null function."); - } - - /* check for typed Rx Function implementation first */ - if (function instanceof Function) { - return fromFunction((Function) function); - } else { - /* not an Rx Function so try language adaptors */ - - // check for language adaptor - for (final Class c : languageAdaptors.keySet()) { - if (c.isInstance(function)) { - final FunctionLanguageAdaptor la = languageAdaptors.get(c); - // found the language adaptor so wrap in FuncN and return - return new FuncN() { - - @Override - public Object call(Object... args) { - return la.call(function, args); - } - - }; - } - } - // no language adaptor found - } - - // no support found - throw new RuntimeException("Unsupported closure type: " + function.getClass().getSimpleName()); - } - - // - // @SuppressWarnings("unchecked") - // private static <R> R executionRxFunction(Function function, Object... args) { - // // check Func* classes - // if (function instanceof Func0) { - // Func0<R> f = (Func0<R>) function; - // if (args.length != 0) { - // throw new RuntimeException("The closure was Func0 and expected no arguments, but we received: " + args.length); - // } - // return (R) f.call(); - // } else if (function instanceof Func1) { - // Func1<Object, R> f = (Func1<Object, R>) function; - // if (args.length != 1) { - // throw new RuntimeException("The closure was Func1 and expected 1 argument, but we received: " + args.length); - // } - // return f.call(args[0]); - // } else if (function instanceof Func2) { - // Func2<Object, Object, R> f = (Func2<Object, Object, R>) function; - // if (args.length != 2) { - // throw new RuntimeException("The closure was Func2 and expected 2 arguments, but we received: " + args.length); - // } - // return f.call(args[0], args[1]); - // } else if (function instanceof Func3) { - // Func3<Object, Object, Object, R> f = (Func3<Object, Object, Object, R>) function; - // if (args.length != 3) { - // throw new RuntimeException("The closure was Func3 and expected 3 arguments, but we received: " + args.length); - // } - // return (R) f.call(args[0], args[1], args[2]); - // } else if (function instanceof Func4) { - // Func4<Object, Object, Object, Object, R> f = (Func4<Object, Object, Object, Object, R>) function; - // if (args.length != 1) { - // throw new RuntimeException("The closure was Func4 and expected 4 arguments, but we received: " + args.length); - // } - // return f.call(args[0], args[1], args[2], args[3]); - // } else if (function instanceof Func5) { - // Func5<Object, Object, Object, Object, Object, R> f = (Func5<Object, Object, Object, Object, Object, R>) function; - // if (args.length != 1) { - // throw new RuntimeException("The closure was Func5 and expected 5 arguments, but we received: " + args.length); - // } - // return f.call(args[0], args[1], args[2], args[3], args[4]); - // } else if (function instanceof Func6) { - // Func6<Object, Object, Object, Object, Object, Object, R> f = (Func6<Object, Object, Object, Object, Object, Object, R>) function; - // if (args.length != 1) { - // throw new RuntimeException("The closure was Func6 and expected 6 arguments, but we received: " + args.length); - // } - // return f.call(args[0], args[1], args[2], args[3], args[4], args[5]); - // } else if (function instanceof Func7) { - // Func7<Object, Object, Object, Object, Object, Object, Object, R> f = (Func7<Object, Object, Object, Object, Object, Object, Object, R>) function; - // if (args.length != 1) { - // throw new RuntimeException("The closure was Func7 and expected 7 arguments, but we received: " + args.length); - // } - // return f.call(args[0], args[1], args[2], args[3], args[4], args[5], args[6]); - // } else if (function instanceof Func8) { - // Func8<Object, Object, Object, Object, Object, Object, Object, Object, R> f = (Func8<Object, Object, Object, Object, Object, Object, Object, Object, R>) function; - // if (args.length != 1) { - // throw new RuntimeException("The closure was Func8 and expected 8 arguments, but we received: " + args.length); - // } - // return f.call(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7]); - // } else if (function instanceof Func9) { - // Func9<Object, Object, Object, Object, Object, Object, Object, Object, Object, R> f = (Func9<Object, Object, Object, Object, Object, Object, Object, Object, Object, R>) function; - // if (args.length != 1) { - // throw new RuntimeException("The closure was Func9 and expected 9 arguments, but we received: " + args.length); - // } - // return f.call(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7], args[8]); - // } else if (function instanceof FuncN) { - // FuncN<R> f = (FuncN<R>) function; - // return f.call(args); - // } else if (function instanceof Action0) { - // Action0 f = (Action0) function; - // if (args.length != 1) { - // throw new RuntimeException("The closure was Action0 and expected 0 arguments, but we received: " + args.length); - // } - // f.call(); - // return null; - // } else if (function instanceof Action1) { - // Action1<Object> f = (Action1<Object>) function; - // if (args.length != 1) { - // throw new RuntimeException("The closure was Action1 and expected 1 argument, but we received: " + args.length); - // } - // f.call(args[0]); - // return null; - // } else if (function instanceof Action2) { - // Action2<Object, Object> f = (Action2<Object, Object>) function; - // if (args.length != 1) { - // throw new RuntimeException("The closure was Action2 and expected 2 argument, but we received: " + args.length); - // } - // f.call(args[0], args[1]); - // return null; - // } else if (function instanceof Action3) { - // Action3<Object, Object, Object> f = (Action3<Object, Object, Object>) function; - // if (args.length != 1) { - // throw new RuntimeException("The closure was Action1 and expected 1 argument, but we received: " + args.length); - // } - // f.call(args[0], args[1], args[2]); - // return null; - // } - // - // throw new RuntimeException("Unknown implementation of Function: " + function.getClass().getSimpleName()); - // } - - @SuppressWarnings({ "unchecked", "rawtypes" }) - private static FuncN fromFunction(Function function) { - // check Func* classes - if (function instanceof Func0) { - return fromFunc((Func0) function); - } else if (function instanceof Func1) { - return fromFunc((Func1) function); - } else if (function instanceof Func2) { - return fromFunc((Func2) function); - } else if (function instanceof Func3) { - return fromFunc((Func3) function); - } else if (function instanceof Func4) { - return fromFunc((Func4) function); - } else if (function instanceof Func5) { - return fromFunc((Func5) function); - } else if (function instanceof Func6) { - return fromFunc((Func6) function); - } else if (function instanceof Func7) { - return fromFunc((Func7) function); - } else if (function instanceof Func8) { - return fromFunc((Func8) function); - } else if (function instanceof Func9) { - return fromFunc((Func9) function); - } else if (function instanceof FuncN) { - return (FuncN) function; - } else if (function instanceof Action0) { - return fromAction((Action0) function); - } else if (function instanceof Action1) { - return fromAction((Action1) function); - } else if (function instanceof Action2) { - return fromAction((Action2) function); - } else if (function instanceof Action3) { - return fromAction((Action3) function); - } - - throw new RuntimeException("Unknown implementation of Function: " + function.getClass().getSimpleName()); - } - - /** - * Convert a function to FuncN to allow heterogeneous handling of functions with different arities. - * - * @param f - * @return {@link FuncN} - */ - public static <R> FuncN<R> fromFunc(final Func0<R> f) { - return new FuncN<R>() { - - @Override - public R call(Object... args) { - if (args.length != 0) { - throw new RuntimeException("Func0 expecting 0 arguments."); - } - return f.call(); - } - - }; - } - - /** - * Convert a function to FuncN to allow heterogeneous handling of functions with different arities. - * - * @param f - * @return {@link FuncN} - */ - public static <T0, R> FuncN<R> fromFunc(final Func1<T0, R> f) { - return new FuncN<R>() { - - @SuppressWarnings("unchecked") - @Override - public R call(Object... args) { - if (args.length != 1) { - throw new RuntimeException("Func1 expecting 1 argument."); - } - return f.call((T0) args[0]); - } - - }; - } - - /** - * Convert a function to FuncN to allow heterogeneous handling of functions with different arities. - * - * @param f - * @return {@link FuncN} - */ - public static <T0, T1, R> FuncN<R> fromFunc(final Func2<T0, T1, R> f) { - return new FuncN<R>() { - - @SuppressWarnings("unchecked") - @Override - public R call(Object... args) { - if (args.length != 2) { - throw new RuntimeException("Func2 expecting 2 arguments."); - } - return f.call((T0) args[0], (T1) args[1]); - } - - }; - } - - /** - * Convert a function to FuncN to allow heterogeneous handling of functions with different arities. - * - * @param f - * @return {@link FuncN} - */ - public static <T0, T1, T2, R> FuncN<R> fromFunc(final Func3<T0, T1, T2, R> f) { - return new FuncN<R>() { - - @SuppressWarnings("unchecked") - @Override - public R call(Object... args) { - if (args.length != 3) { - throw new RuntimeException("Func3 expecting 3 arguments."); - } - return f.call((T0) args[0], (T1) args[1], (T2) args[2]); - } - - }; - } - - /** - * Convert a function to FuncN to allow heterogeneous handling of functions with different arities. - * - * @param f - * @return {@link FuncN} - */ - public static <T0, T1, T2, T3, R> FuncN<R> fromFunc(final Func4<T0, T1, T2, T3, R> f) { - return new FuncN<R>() { - - @SuppressWarnings("unchecked") - @Override - public R call(Object... args) { - if (args.length != 4) { - throw new RuntimeException("Func4 expecting 4 arguments."); - } - return f.call((T0) args[0], (T1) args[1], (T2) args[2], (T3) args[3]); - } - - }; - } - - /** - * Convert a function to FuncN to allow heterogeneous handling of functions with different arities. - * - * @param f - * @return {@link FuncN} - */ - public static <T0, T1, T2, T3, T4, R> FuncN<R> fromFunc(final Func5<T0, T1, T2, T3, T4, R> f) { - return new FuncN<R>() { - - @SuppressWarnings("unchecked") - @Override - public R call(Object... args) { - if (args.length != 5) { - throw new RuntimeException("Func5 expecting 5 arguments."); - } - return f.call((T0) args[0], (T1) args[1], (T2) args[2], (T3) args[3], (T4) args[4]); - } - - }; - } - - /** - * Convert a function to FuncN to allow heterogeneous handling of functions with different arities. - * - * @param f - * @return {@link FuncN} - */ - public static <T0, T1, T2, T3, T4, T5, R> FuncN<R> fromFunc(final Func6<T0, T1, T2, T3, T4, T5, R> f) { - return new FuncN<R>() { - - @SuppressWarnings("unchecked") - @Override - public R call(Object... args) { - if (args.length != 6) { - throw new RuntimeException("Func6 expecting 6 arguments."); - } - return f.call((T0) args[0], (T1) args[1], (T2) args[2], (T3) args[3], (T4) args[4], (T5) args[5]); - } - - }; - } - - /** - * Convert a function to FuncN to allow heterogeneous handling of functions with different arities. - * - * @param f - * @return {@link FuncN} - */ - public static <T0, T1, T2, T3, T4, T5, T6, R> FuncN<R> fromFunc(final Func7<T0, T1, T2, T3, T4, T5, T6, R> f) { - return new FuncN<R>() { - - @SuppressWarnings("unchecked") - @Override - public R call(Object... args) { - if (args.length != 7) { - throw new RuntimeException("Func7 expecting 7 arguments."); - } - return f.call((T0) args[0], (T1) args[1], (T2) args[2], (T3) args[3], (T4) args[4], (T5) args[5], (T6) args[6]); - } - - }; - } - - /** - * Convert a function to FuncN to allow heterogeneous handling of functions with different arities. - * - * @param f - * @return {@link FuncN} - */ - public static <T0, T1, T2, T3, T4, T5, T6, T7, R> FuncN<R> fromFunc(final Func8<T0, T1, T2, T3, T4, T5, T6, T7, R> f) { - return new FuncN<R>() { - - @SuppressWarnings("unchecked") - @Override - public R call(Object... args) { - if (args.length != 8) { - throw new RuntimeException("Func8 expecting 8 arguments."); - } - return f.call((T0) args[0], (T1) args[1], (T2) args[2], (T3) args[3], (T4) args[4], (T5) args[5], (T6) args[6], (T7) args[7]); - } - - }; - } - - /** - * Convert a function to FuncN to allow heterogeneous handling of functions with different arities. - * - * @param f - * @return {@link FuncN} - */ - public static <T0, T1, T2, T3, T4, T5, T6, T7, T8, R> FuncN<R> fromFunc(final Func9<T0, T1, T2, T3, T4, T5, T6, T7, T8, R> f) { - return new FuncN<R>() { - - @SuppressWarnings("unchecked") - @Override - public R call(Object... args) { - if (args.length != 9) { - throw new RuntimeException("Func9 expecting 9 arguments."); - } - return f.call((T0) args[0], (T1) args[1], (T2) args[2], (T3) args[3], (T4) args[4], (T5) args[5], (T6) args[6], (T7) args[7], (T8) args[8]); - } - - }; - } - - /** - * Convert a function to FuncN to allow heterogeneous handling of functions with different arities. - * - * @param f - * @return {@link FuncN} - */ - public static FuncN<Void> fromAction(final Action0 f) { - return new FuncN<Void>() { - - @Override - public Void call(Object... args) { - if (args.length != 0) { - throw new RuntimeException("Action0 expecting 0 arguments."); - } - f.call(); - return null; - } - - }; - } - - /** - * Convert a function to FuncN to allow heterogeneous handling of functions with different arities. - * - * @param f - * @return {@link FuncN} - */ - public static <T0> FuncN<Void> fromAction(final Action1<T0> f) { - return new FuncN<Void>() { - - @SuppressWarnings("unchecked") - @Override - public Void call(Object... args) { - if (args.length != 1) { - throw new RuntimeException("Action1 expecting 1 argument."); - } - f.call((T0) args[0]); - return null; - } - - }; - } - - /** - * Convert a function to FuncN to allow heterogeneous handling of functions with different arities. - * - * @param f - * @return {@link FuncN} - */ - public static <T0, T1> FuncN<Void> fromAction(final Action2<T0, T1> f) { - return new FuncN<Void>() { - - @SuppressWarnings("unchecked") - @Override - public Void call(Object... args) { - if (args.length != 2) { - throw new RuntimeException("Action3 expecting 2 arguments."); - } - f.call((T0) args[0], (T1) args[1]); - return null; - } - - }; - } - - /** - * Convert a function to FuncN to allow heterogeneous handling of functions with different arities. - * - * @param f - * @return {@link FuncN} - */ - public static <T0, T1, T2> FuncN<Void> fromAction(final Action3<T0, T1, T2> f) { - return new FuncN<Void>() { - - @SuppressWarnings("unchecked") - @Override - public Void call(Object... args) { - if (args.length != 3) { - throw new RuntimeException("Action3 expecting 3 arguments."); - } - f.call((T0) args[0], (T1) args[1], (T2) args[2]); - return null; - } - - }; - } - -} diff --git a/rxjava-core/src/test/java/README.txt b/rxjava-core/src/test/java/README.txt deleted file mode 100644 index 3e20b05ea7..0000000000 --- a/rxjava-core/src/test/java/README.txt +++ /dev/null @@ -1,5 +0,0 @@ -This test folder only contains performance and functional/integration style tests. - -The unit tests themselves are embedded as inner classes of the Java code (such as here https://github.com/Netflix/RxJava/tree/master/rxjava-core/src/main/java/rx/operators). - -Also, each of the language adaptors has a /src/test/ folder which further testing. See Groovy for an example: https://github.com/Netflix/RxJava/tree/master/language-adaptors/rxjava-groovy/src/test \ No newline at end of file diff --git a/rxjava-core/src/test/java/rx/performance/PerformanceTest.java b/rxjava-core/src/test/java/rx/performance/PerformanceTest.java deleted file mode 100644 index 8438545d70..0000000000 --- a/rxjava-core/src/test/java/rx/performance/PerformanceTest.java +++ /dev/null @@ -1,250 +0,0 @@ -package rx.performance; - -import java.util.concurrent.atomic.AtomicInteger; - -import rx.Observable; -import rx.Observer; -import rx.util.functions.Func1; - -public class PerformanceTest { - - /* - * >>> Statically typed <<< - * - * Without chaining: - * - * Sum: 710082754 Time: 130.683ms - * runNonCompositionalTestWithDirectLoop - * Sum: 710082754 Time: 21.011ms - * runNonCompositionalTestWithArrayOfFunctions - * Sum: 710082754 Time: 20.84ms - * - * - * With chaining (composition collapsing): - * - * Sum: 710082754 Time: 28.787ms - * runNonCompositionalTestWithDirectLoop - * Sum: 710082754 Time: 19.525ms - * runNonCompositionalTestWithArrayOfFunctions - * Sum: 710082754 Time: 19.175ms - * - * - * >>> Dynamically typed <<< - * - * When going via generic Functions.execute even with chained sequence: - * - * runCompositionTest - * Sum: 710082754 Time: 577.3ms <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< very bad when dynamic - * runNonCompositionalTestWithDirectLoop - * Sum: 710082754 Time: 31.591ms - * runNonCompositionalTestWithArrayOfFunctions - * Sum: 710082754 Time: 38.093ms - * runCompositionTest - * - * - * With Function memoization so we only pay dynamic price during sequence setup, not in onNext: - * - * - * Using ArrayList - * - * runCompositionTest - * Sum: 710082754 Time: 27.078ms - * runNonCompositionalTestWithDirectLoop - * Sum: 710082754 Time: 18.911ms - * runNonCompositionalTestWithArrayOfFunctions - * Sum: 710082754 Time: 18.852ms - * - * - * Using LinkedBlockingQueue - * - * runCompositionTest - * Sum: 710082754 Time: 46.532ms - * runNonCompositionalTestWithDirectLoop - * Sum: 710082754 Time: 18.946ms - * runNonCompositionalTestWithArrayOfFunctions - * Sum: 710082754 Time: 18.746ms - */ - - public static void main(String[] args) { - PerformanceTest test = new PerformanceTest(); - Integer[] values = new Integer[100001]; - for (int i = 0; i < values.length; i++) { - values[i] = i; - } - - for (int i = 0; i < 100; i++) { - System.out.println("-------------------------------"); -// test.runCompositionTestWithMultipleOperations(values); - test.runCompositionTest(values); - test.runNonCompositionalTestWithDirectLoop(values); - test.runNonCompositionalTestWithArrayOfFunctions(values); - } - } - - public void runCompositionTestWithMultipleOperations(Integer[] values) { - System.out.println("runCompositionTestWithMultipleOperations"); - - // old code before memoizing - // Count: 200002 Time: 403.095ms - - // new code with memoizing but no chaining - // Count: 200002 Time: 103.128ms - - final AtomicInteger onNextSum = new AtomicInteger(0); - final long start = System.nanoTime(); - - MathFunction m = new MathFunction(); - - Observable<Integer> a = Observable.from(values) - .map(m).map(m).map(m).map(m).map(m).map(m).map(m).map(m).map(m).map(m) - .map(m).map(m).map(m).map(m).map(m).map(m).map(m).map(m).map(m).map(m); - - final Observable<Integer> b = Observable.from(values) - .map(m).map(m).map(m).map(m).map(m).map(m).map(m).map(m).map(m).map(m) - .map(m).map(m).map(m).map(m).map(m).map(m).map(m).map(m).map(m).map(m); - - Observable.merge(a, b).filter(new Func1<Integer, Boolean>() { - - @Override - public Boolean call(Integer t1) { - return t1 > 10; - } - - }).map(new Func1<Integer, String>() { - - @Override - public String call(Integer t1) { - return t1 + "-value-from-b"; - } - }).take(1000000).subscribe(new TestStringObserver(onNextSum, start)); - - } - - public void runCompositionTest(Integer[] values) { - System.out.println("runCompositionTest"); - - final AtomicInteger onNextSum = new AtomicInteger(0); - final long start = System.nanoTime(); - - MathFunction m = new MathFunction(); - // 50 levels of composition (same function so that's not the cost) - Observable.from(values) - .map(m).map(m).map(m).map(m).map(m).map(m).map(m).map(m).map(m).map(m) - .map(m).map(m).map(m).map(m).map(m).map(m).map(m).map(m).map(m).map(m) - .map(m).map(m).map(m).map(m).map(m).map(m).map(m).map(m).map(m).map(m) - .map(m).map(m).map(m).map(m).map(m).map(m).map(m).map(m).map(m).map(m) - .map(m).map(m).map(m).map(m).map(m).map(m).map(m).map(m).map(m).map(m) - .subscribe(new TestObserver(onNextSum, start)); - } - - public void runNonCompositionalTestWithDirectLoop(Integer[] values) { - System.out.println("runNonCompositionalTestWithDirectLoop"); - - final AtomicInteger onNextSum = new AtomicInteger(0); - final long start = System.nanoTime(); - final MathFunction m = new MathFunction(); - - Observable.from(values).map(new Func1<Integer, Integer>() { - - @Override - public Integer call(Integer t1) { - // iterate the 50 times here in a loop rather than via composition - for (int i = 0; i < 50; i++) { - t1 = m.call(t1); - } - return t1; - } - - }).subscribe(new TestObserver(onNextSum, start)); - - } - - public void runNonCompositionalTestWithArrayOfFunctions(Integer[] values) { - System.out.println("runNonCompositionalTestWithArrayOfFunctions"); - - final AtomicInteger onNextSum = new AtomicInteger(0); - final long start = System.nanoTime(); - final MathFunction m = new MathFunction(); - final Func1[] functionCalls = new Func1<?, ?>[50]; - for (int i = 0; i < 50; i++) { - functionCalls[i] = m; - } - - Observable.from(values).map(new Func1<Integer, Integer>() { - - @Override - public Integer call(Integer t1) { - // iterate the 50 times here in a loop rather than via composition - for (Func1<Integer, Integer> f : functionCalls) { - t1 = f.call(t1); - } - return t1; - } - - }).subscribe(new TestObserver(onNextSum, start)); - - } - - private static final class TestObserver implements Observer<Integer> { - private final AtomicInteger onNextSum; - private final long start; - - private TestObserver(AtomicInteger onNextSum, long start) { - this.onNextSum = onNextSum; - this.start = start; - } - - @Override - public void onNext(Integer i) { - onNextSum.addAndGet(i); - } - - @Override - public void onError(Exception e) { - e.printStackTrace(); - } - - @Override - public void onCompleted() { - long end = System.nanoTime(); - System.out.println("Sum: " + onNextSum.get() + " Time: " + ((double) (end - start)) / 1000 / 1000 + "ms"); - } - } - - private static final class TestStringObserver implements Observer<String> { - private final AtomicInteger onNextSum; - private final long start; - - private TestStringObserver(AtomicInteger onNextSum, long start) { - this.onNextSum = onNextSum; - this.start = start; - } - - @Override - public void onNext(String i) { - // System.out.println(i); - onNextSum.incrementAndGet(); - } - - @Override - public void onError(Exception e) { - e.printStackTrace(); - } - - @Override - public void onCompleted() { - long end = System.nanoTime(); - System.out.println("Count: " + onNextSum.get() + " Time: " + ((double) (end - start)) / 1000 / 1000 + "ms"); - } - } - - private static class MathFunction implements Func1<Integer, Integer> { - - @Override - public Integer call(Integer t1) { - return t1 + 1; - } - - } - -} diff --git a/rxjava-core/src/test/java/rx/performance/TestChainPerformance.java b/rxjava-core/src/test/java/rx/performance/TestChainPerformance.java deleted file mode 100644 index 60e02707c5..0000000000 --- a/rxjava-core/src/test/java/rx/performance/TestChainPerformance.java +++ /dev/null @@ -1,85 +0,0 @@ -package rx.performance; -import java.util.ArrayList; -import java.util.concurrent.Callable; - -import rx.util.functions.Func1; - -public class TestChainPerformance { - - public static void main(String[] args) { - TestChainPerformance test = new TestChainPerformance(); - Integer[] values = new Integer[100001]; - for (int i = 0; i < values.length; i++) { - values[i] = i; - } - - try { - for (int i = 0; i < 100; i++) { - System.out.println("-------------------------------"); - test.runChained(values); - test.runComposed(values); - } - } catch (Exception e) { - e.printStackTrace(); - } - - } - - public void runComposed(final Integer[] values) throws Exception { - long start = System.nanoTime(); - - Callable<Integer> c = null; - for (int i = 0; i < 250; i++) { - final Callable<Integer> previousC = c; - c = new Callable<Integer>() { - - @Override - public Integer call() throws Exception { - MathFunction f = new MathFunction(); - int sum = 0; - for (int v : values) { - sum += f.call(v); - } - if (previousC != null) { - sum += previousC.call(); - } - return sum; - } - - }; - } - - int sum = c.call(); - - long end = System.nanoTime(); - System.out.println("Composed => Sum: " + sum + " Time: " + ((double) (end - start)) / 1000 / 1000 + "ms"); - } - - public void runChained(Integer[] values) { - long start = System.nanoTime(); - int sum = 0; - - ArrayList<Func1<Integer, Integer>> functions = new ArrayList<Func1<Integer, Integer>>(); - for (int i = 0; i < 250; i++) { - functions.add(new MathFunction()); - } - - for (int v : values) { - for (Func1<Integer, Integer> f : functions) { - sum += f.call(v); - } - } - - long end = System.nanoTime(); - System.out.println("Iterative => Sum: " + sum + " Time: " + ((double) (end - start)) / 1000 / 1000 + "ms"); - } - - private static class MathFunction implements Func1<Integer, Integer> { - - @Override - public Integer call(Integer t1) { - return t1 + 1; - } - - } -} diff --git a/settings.gradle b/settings.gradle index df14bb21d7..c5620daef9 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,6 +1 @@ rootProject.name='rxjava' -include 'rxjava-core', \ -'language-adaptors:rxjava-groovy', \ -'language-adaptors:rxjava-jruby', \ -'language-adaptors:rxjava-clojure', \ -'language-adaptors:rxjava-scala' diff --git a/src/jmh/java/io/reactivex/rxjava3/core/BinaryFlatMapPerf.java b/src/jmh/java/io/reactivex/rxjava3/core/BinaryFlatMapPerf.java new file mode 100644 index 0000000000..c4a0a385a5 --- /dev/null +++ b/src/jmh/java/io/reactivex/rxjava3/core/BinaryFlatMapPerf.java @@ -0,0 +1,262 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.core; + +import java.util.*; +import java.util.concurrent.TimeUnit; + +import org.openjdk.jmh.annotations.*; +import org.openjdk.jmh.infra.Blackhole; +import org.reactivestreams.Publisher; + +import io.reactivex.rxjava3.functions.Function; + +@BenchmarkMode(Mode.Throughput) +@Warmup(iterations = 5) +@Measurement(iterations = 5, time = 5, timeUnit = TimeUnit.SECONDS) +@OutputTimeUnit(TimeUnit.SECONDS) +@Fork(value = 1) +@State(Scope.Thread) +public class BinaryFlatMapPerf { + @Param({ "1", "1000", "1000000" }) + public int times; + + Flowable<Integer> singleFlatMapPublisher; + + Flowable<Integer> singleFlatMapHidePublisher; + + Flowable<Integer> singleFlattenAsPublisher; + + Flowable<Integer> maybeFlatMapPublisher; + + Flowable<Integer> maybeFlatMapHidePublisher; + + Flowable<Integer> maybeFlattenAsPublisher; + + Flowable<Integer> completableFlatMapPublisher; + + Flowable<Integer> completableFlattenAsPublisher; + + Observable<Integer> singleFlatMapObservable; + + Observable<Integer> singleFlatMapHideObservable; + + Observable<Integer> singleFlattenAsObservable; + + Observable<Integer> maybeFlatMapObservable; + + Observable<Integer> maybeFlatMapHideObservable; + + Observable<Integer> maybeFlattenAsObservable; + + Observable<Integer> completableFlatMapObservable; + + Observable<Integer> completableFlattenAsObservable; + + @Setup + public void setup() { + + // -------------------------------------------------------------------------- + + final Integer[] array = new Integer[times]; + Arrays.fill(array, 777); + + final List<Integer> list = Arrays.asList(array); + + final Flowable<Integer> arrayFlowable = Flowable.fromArray(array); + final Flowable<Integer> arrayFlowableHide = Flowable.fromArray(array).hide(); + final Flowable<Integer> listFlowable = Flowable.fromIterable(list); + + final Observable<Integer> arrayObservable = Observable.fromArray(array); + final Observable<Integer> arrayObservableHide = Observable.fromArray(array).hide(); + final Observable<Integer> listObservable = Observable.fromIterable(list); + + // -------------------------------------------------------------------------- + + singleFlatMapPublisher = Single.just(1).flatMapPublisher(new Function<Integer, Publisher<? extends Integer>>() { + @Override + public Publisher<? extends Integer> apply(Integer v) { + return arrayFlowable; + } + }); + + singleFlatMapHidePublisher = Single.just(1).flatMapPublisher(new Function<Integer, Publisher<? extends Integer>>() { + @Override + public Publisher<? extends Integer> apply(Integer v) { + return arrayFlowableHide; + } + }); + + singleFlattenAsPublisher = Single.just(1).flattenAsFlowable(new Function<Integer, Iterable<? extends Integer>>() { + @Override + public Iterable<? extends Integer> apply(Integer v) { + return list; + } + }); + + maybeFlatMapPublisher = Maybe.just(1).flatMapPublisher(new Function<Integer, Publisher<? extends Integer>>() { + @Override + public Publisher<? extends Integer> apply(Integer v) { + return arrayFlowable; + } + }); + + maybeFlatMapHidePublisher = Maybe.just(1).flatMapPublisher(new Function<Integer, Publisher<? extends Integer>>() { + @Override + public Publisher<? extends Integer> apply(Integer v) { + return arrayFlowableHide; + } + }); + + maybeFlattenAsPublisher = Maybe.just(1).flattenAsFlowable(new Function<Integer, Iterable<? extends Integer>>() { + @Override + public Iterable<? extends Integer> apply(Integer v) { + return list; + } + }); + + completableFlatMapPublisher = Completable.complete().andThen(listFlowable); + + completableFlattenAsPublisher = Completable.complete().andThen(arrayFlowable); + + // -------------------------------------------------------------------------- + + singleFlatMapObservable = Single.just(1).flatMapObservable(new Function<Integer, Observable<? extends Integer>>() { + @Override + public Observable<? extends Integer> apply(Integer v) { + return arrayObservable; + } + }); + + singleFlatMapHideObservable = Single.just(1).flatMapObservable(new Function<Integer, Observable<? extends Integer>>() { + @Override + public Observable<? extends Integer> apply(Integer v) { + return arrayObservableHide; + } + }); + + singleFlattenAsObservable = Single.just(1).flattenAsObservable(new Function<Integer, Iterable<? extends Integer>>() { + @Override + public Iterable<? extends Integer> apply(Integer v) { + return list; + } + }); + + maybeFlatMapObservable = Maybe.just(1).flatMapObservable(new Function<Integer, Observable<? extends Integer>>() { + @Override + public Observable<? extends Integer> apply(Integer v) { + return arrayObservable; + } + }); + + maybeFlatMapHideObservable = Maybe.just(1).flatMapObservable(new Function<Integer, Observable<? extends Integer>>() { + @Override + public Observable<? extends Integer> apply(Integer v) { + return arrayObservableHide; + } + }); + + maybeFlattenAsObservable = Maybe.just(1).flattenAsObservable(new Function<Integer, Iterable<? extends Integer>>() { + @Override + public Iterable<? extends Integer> apply(Integer v) { + return list; + } + }); + + completableFlatMapObservable = Completable.complete().andThen(listObservable); + + completableFlattenAsObservable = Completable.complete().andThen(arrayObservable); + + } + + @Benchmark + public void singleFlatMapPublisher(Blackhole bh) { + singleFlatMapPublisher.subscribe(new PerfConsumer(bh)); + } + + @Benchmark + public void singleFlatMapHidePublisher(Blackhole bh) { + singleFlatMapHidePublisher.subscribe(new PerfConsumer(bh)); + } + + @Benchmark + public void singleFlattenAsPublisher(Blackhole bh) { + singleFlattenAsPublisher.subscribe(new PerfConsumer(bh)); + } + + @Benchmark + public void maybeFlatMapPublisher(Blackhole bh) { + maybeFlatMapPublisher.subscribe(new PerfConsumer(bh)); + } + + @Benchmark + public void maybeFlatMapHidePublisher(Blackhole bh) { + maybeFlatMapHidePublisher.subscribe(new PerfConsumer(bh)); + } + + @Benchmark + public void maybeFlattenAsPublisher(Blackhole bh) { + maybeFlattenAsPublisher.subscribe(new PerfConsumer(bh)); + } + + @Benchmark + public void completableFlatMapPublisher(Blackhole bh) { + completableFlatMapPublisher.subscribe(new PerfConsumer(bh)); + } + + @Benchmark + public void completableFlattenAsPublisher(Blackhole bh) { + completableFlattenAsPublisher.subscribe(new PerfConsumer(bh)); + } + + @Benchmark + public void singleFlatMapObservable(Blackhole bh) { + singleFlatMapObservable.subscribe(new PerfConsumer(bh)); + } + + @Benchmark + public void singleFlatMapHideObservable(Blackhole bh) { + singleFlatMapHideObservable.subscribe(new PerfConsumer(bh)); + } + + @Benchmark + public void singleFlattenAsObservable(Blackhole bh) { + singleFlattenAsObservable.subscribe(new PerfConsumer(bh)); + } + + @Benchmark + public void maybeFlatMapObservable(Blackhole bh) { + maybeFlatMapObservable.subscribe(new PerfConsumer(bh)); + } + + @Benchmark + public void maybeFlatMapHideObservable(Blackhole bh) { + maybeFlatMapHideObservable.subscribe(new PerfConsumer(bh)); + } + + @Benchmark + public void maybeFlattenAsObservable(Blackhole bh) { + maybeFlattenAsObservable.subscribe(new PerfConsumer(bh)); + } + + @Benchmark + public void completableFlatMapObservable(Blackhole bh) { + completableFlatMapObservable.subscribe(new PerfConsumer(bh)); + } + + @Benchmark + public void completableFlattenAsObservable(Blackhole bh) { + completableFlattenAsObservable.subscribe(new PerfConsumer(bh)); + } +} diff --git a/src/jmh/java/io/reactivex/rxjava3/core/BlockingGetPerf.java b/src/jmh/java/io/reactivex/rxjava3/core/BlockingGetPerf.java new file mode 100644 index 0000000000..7f3711788a --- /dev/null +++ b/src/jmh/java/io/reactivex/rxjava3/core/BlockingGetPerf.java @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.core; + +import java.util.concurrent.TimeUnit; + +import org.openjdk.jmh.annotations.*; + +@BenchmarkMode(Mode.Throughput) +@Warmup(iterations = 5) +@Measurement(iterations = 5, time = 5, timeUnit = TimeUnit.SECONDS) +@OutputTimeUnit(TimeUnit.SECONDS) +@Fork(value = 1) +@State(Scope.Thread) +public class BlockingGetPerf { + Flowable<Integer> flowable; + + Observable<Integer> observable; + + Single<Integer> single; + + Maybe<Integer> maybe; + + Completable completable; + + @Setup + public void setup() { + flowable = Flowable.just(1); + + observable = Observable.just(1); + + single = Single.just(1); + + maybe = Maybe.just(1); + + completable = Completable.complete(); + } + + @Benchmark + public Object flowableBlockingFirst() { + return flowable.blockingFirst(); + } + + @Benchmark + public Object flowableBlockingLast() { + return flowable.blockingLast(); + } + + @Benchmark + public Object observableBlockingLast() { + return observable.blockingLast(); + } + + @Benchmark + public Object observableBlockingFirst() { + return observable.blockingFirst(); + } + + @Benchmark + public Object single() { + return single.blockingGet(); + } + + @Benchmark + public Object maybe() { + return maybe.blockingGet(); + } + + @Benchmark + public void completable() { + completable.blockingAwait(); + } +} diff --git a/src/jmh/java/io/reactivex/rxjava3/core/BlockingPerf.java b/src/jmh/java/io/reactivex/rxjava3/core/BlockingPerf.java new file mode 100644 index 0000000000..5fb2825b3e --- /dev/null +++ b/src/jmh/java/io/reactivex/rxjava3/core/BlockingPerf.java @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.core; + +import java.util.Arrays; +import java.util.concurrent.TimeUnit; + +import org.openjdk.jmh.annotations.*; + +@BenchmarkMode(Mode.Throughput) +@Warmup(iterations = 5) +@Measurement(iterations = 5, time = 5, timeUnit = TimeUnit.SECONDS) +@OutputTimeUnit(TimeUnit.SECONDS) +@Fork(value = 1) +@State(Scope.Thread) +public class BlockingPerf { + @Param({ "1", "1000", "1000000" }) + public int times; + + Flowable<Integer> flowable; + + Observable<Integer> observable; + + @Setup + public void setup() { + Integer[] array = new Integer[times]; + Arrays.fill(array, 777); + + flowable = Flowable.fromArray(array); + + observable = Observable.fromArray(array); + } + + @Benchmark + public Object flowableBlockingFirst() { + return flowable.blockingFirst(); + } + + @Benchmark + public Object flowableBlockingLast() { + return flowable.blockingLast(); + } + + @Benchmark + public Object observableBlockingLast() { + return observable.blockingLast(); + } + + @Benchmark + public Object observableBlockingFirst() { + return observable.blockingFirst(); + } +} diff --git a/src/jmh/java/io/reactivex/rxjava3/core/CallableAsyncPerf.java b/src/jmh/java/io/reactivex/rxjava3/core/CallableAsyncPerf.java new file mode 100644 index 0000000000..1bae28f9f9 --- /dev/null +++ b/src/jmh/java/io/reactivex/rxjava3/core/CallableAsyncPerf.java @@ -0,0 +1,190 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.core; + +import java.util.concurrent.*; + +import org.openjdk.jmh.annotations.*; +import org.openjdk.jmh.infra.Blackhole; + +import io.reactivex.rxjava3.internal.schedulers.SingleScheduler; +import io.reactivex.rxjava3.schedulers.Schedulers; + +@BenchmarkMode(Mode.Throughput) +@Warmup(iterations = 5) +@Measurement(iterations = 5, time = 5, timeUnit = TimeUnit.SECONDS) +@OutputTimeUnit(TimeUnit.SECONDS) +@Fork(value = 1) +@State(Scope.Thread) +public class CallableAsyncPerf { + + Flowable<Integer> subscribeOnFlowable; + + Flowable<Integer> observeOnFlowable; + + Flowable<Integer> pipelineFlowable; + + Observable<Integer> subscribeOnObservable; + + Observable<Integer> observeOnObservable; + + Observable<Integer> pipelineObservable; + + Single<Integer> observeOnSingle; + + Single<Integer> subscribeOnSingle; + + Single<Integer> pipelineSingle; + + Completable observeOnCompletable; + + Completable subscribeOnCompletable; + + Completable pipelineCompletable; + + Maybe<Integer> observeOnMaybe; + + Maybe<Integer> subscribeOnMaybe; + + Maybe<Integer> pipelineMaybe; + + @Setup + public void setup() { + + Scheduler s = Schedulers.single(); + + Scheduler s2 = new SingleScheduler(); + + Callable<Integer> c = new Callable<Integer>() { + @Override + public Integer call() { + return 1; + } + }; + + subscribeOnFlowable = Flowable.fromCallable(c).subscribeOn(s); + + observeOnFlowable = Flowable.fromCallable(c).observeOn(s); + + pipelineFlowable = Flowable.fromCallable(c).subscribeOn(s).observeOn(s2); + + // ---- + + subscribeOnObservable = Observable.fromCallable(c).subscribeOn(s); + + observeOnObservable = Observable.fromCallable(c).observeOn(s); + + pipelineObservable = Observable.fromCallable(c).subscribeOn(s).observeOn(s2); + + // ---- + + observeOnSingle = Single.fromCallable(c).observeOn(s); + + subscribeOnSingle = Single.fromCallable(c).subscribeOn(s); + + pipelineSingle = Single.fromCallable(c).subscribeOn(s).observeOn(s2); + + // ---- + + observeOnCompletable = Completable.fromCallable(c).observeOn(s); + + subscribeOnCompletable = Completable.fromCallable(c).subscribeOn(s); + + pipelineCompletable = Completable.fromCallable(c).subscribeOn(s).observeOn(s2); + + // ---- + + observeOnMaybe = Maybe.fromCallable(c).observeOn(s); + + subscribeOnMaybe = Maybe.fromCallable(c).subscribeOn(s); + + pipelineMaybe = Maybe.fromCallable(c).subscribeOn(s).observeOn(s2); + } + + @Benchmark + public void subscribeOnFlowable(Blackhole bh) { + subscribeOnFlowable.subscribeWith(new PerfAsyncConsumer(bh)).await(1); + } + + @Benchmark + public void observeOnFlowable(Blackhole bh) { + observeOnFlowable.subscribeWith(new PerfAsyncConsumer(bh)).await(1); + } + + @Benchmark + public void pipelineFlowable(Blackhole bh) { + pipelineFlowable.subscribeWith(new PerfAsyncConsumer(bh)).await(1); + } + + @Benchmark + public void subscribeOnObservable(Blackhole bh) { + subscribeOnObservable.subscribeWith(new PerfAsyncConsumer(bh)).await(1); + } + + @Benchmark + public void observeOnObservable(Blackhole bh) { + observeOnObservable.subscribeWith(new PerfAsyncConsumer(bh)).await(1); + } + + @Benchmark + public void pipelineObservable(Blackhole bh) { + pipelineObservable.subscribeWith(new PerfAsyncConsumer(bh)).await(1); + } + + @Benchmark + public void observeOnSingle(Blackhole bh) { + observeOnSingle.subscribeWith(new PerfAsyncConsumer(bh)).await(1); + } + + @Benchmark + public void subscribeOnSingle(Blackhole bh) { + subscribeOnSingle.subscribeWith(new PerfAsyncConsumer(bh)).await(1); + } + + @Benchmark + public void pipelineSingle(Blackhole bh) { + pipelineSingle.subscribeWith(new PerfAsyncConsumer(bh)).await(1); + } + + @Benchmark + public void observeOnCompletable(Blackhole bh) { + observeOnCompletable.subscribeWith(new PerfAsyncConsumer(bh)).await(1); + } + + @Benchmark + public void subscribeOnCompletable(Blackhole bh) { + subscribeOnCompletable.subscribeWith(new PerfAsyncConsumer(bh)).await(1); + } + + @Benchmark + public void pipelineCompletable(Blackhole bh) { + pipelineCompletable.subscribeWith(new PerfAsyncConsumer(bh)).await(1); + } + + @Benchmark + public void observeOnMaybe(Blackhole bh) { + observeOnMaybe.subscribeWith(new PerfAsyncConsumer(bh)).await(1); + } + + @Benchmark + public void subscribeOnMaybe(Blackhole bh) { + subscribeOnMaybe.subscribeWith(new PerfAsyncConsumer(bh)).await(1); + } + + @Benchmark + public void pipelineMaybe(Blackhole bh) { + pipelineMaybe.subscribeWith(new PerfAsyncConsumer(bh)).await(1); + } + +} diff --git a/src/jmh/java/io/reactivex/rxjava3/core/EachTypeFlatMapPerf.java b/src/jmh/java/io/reactivex/rxjava3/core/EachTypeFlatMapPerf.java new file mode 100644 index 0000000000..14aa8c3605 --- /dev/null +++ b/src/jmh/java/io/reactivex/rxjava3/core/EachTypeFlatMapPerf.java @@ -0,0 +1,124 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.core; + +import java.util.concurrent.TimeUnit; + +import org.openjdk.jmh.annotations.*; +import org.openjdk.jmh.infra.Blackhole; +import org.reactivestreams.Publisher; + +import io.reactivex.rxjava3.functions.Function; + +@BenchmarkMode(Mode.Throughput) +@Warmup(iterations = 5) +@Measurement(iterations = 5, time = 5, timeUnit = TimeUnit.SECONDS) +@OutputTimeUnit(TimeUnit.SECONDS) +@Fork(value = 1) +@State(Scope.Thread) +public class EachTypeFlatMapPerf { + @Param({ "1", "1000", "1000000" }) + public int times; + + Flowable<Integer> bpRange; + Observable<Integer> nbpRange; + Single<Integer> singleJust; + + Flowable<Integer> bpRangeMapJust; + Observable<Integer> nbpRangeMapJust; + Single<Integer> singleJustMapJust; + + Flowable<Integer> bpRangeMapRange; + Observable<Integer> nbpRangeMapRange; + + @Setup + public void setup() { + bpRange = Flowable.range(1, times); + nbpRange = Observable.range(1, times); + + bpRangeMapJust = bpRange.flatMap(new Function<Integer, Publisher<Integer>>() { + @Override + public Publisher<Integer> apply(Integer v) { + return Flowable.just(v); + } + }); + nbpRangeMapJust = nbpRange.flatMap(new Function<Integer, Observable<Integer>>() { + @Override + public Observable<Integer> apply(Integer v) { + return Observable.just(v); + } + }); + + bpRangeMapRange = bpRange.flatMap(new Function<Integer, Publisher<Integer>>() { + @Override + public Publisher<Integer> apply(Integer v) { + return Flowable.range(v, 2); + } + }); + nbpRangeMapRange = nbpRange.flatMap(new Function<Integer, Observable<Integer>>() { + @Override + public Observable<Integer> apply(Integer v) { + return Observable.range(v, 2); + } + }); + + singleJust = Single.just(1); + singleJustMapJust = singleJust.flatMap(new Function<Integer, Single<Integer>>() { + @Override + public Single<Integer> apply(Integer v) { + return Single.just(v); + } + }); + } + + @Benchmark + public void bpRange(Blackhole bh) { + bpRange.subscribe(new PerfSubscriber(bh)); + } + + @Benchmark + public void bpRangeMapJust(Blackhole bh) { + bpRangeMapJust.subscribe(new PerfSubscriber(bh)); + } + + @Benchmark + public void bpRangeMapRange(Blackhole bh) { + bpRangeMapRange.subscribe(new PerfSubscriber(bh)); + } + + @Benchmark + public void nbpRange(Blackhole bh) { + nbpRange.subscribe(new PerfObserver(bh)); + } + + @Benchmark + public void nbpRangeMapJust(Blackhole bh) { + nbpRangeMapJust.subscribe(new PerfObserver(bh)); + } + + @Benchmark + public void nbpRangeMapRange(Blackhole bh) { + nbpRangeMapRange.subscribe(new PerfObserver(bh)); + } + + @Benchmark + public void singleJust(Blackhole bh) { + singleJust.subscribe(new LatchedSingleObserver<>(bh)); + } + + @Benchmark + public void singleJustMapJust(Blackhole bh) { + singleJustMapJust.subscribe(new LatchedSingleObserver<>(bh)); + } +} diff --git a/src/jmh/java/io/reactivex/rxjava3/core/FlatMapJustPerf.java b/src/jmh/java/io/reactivex/rxjava3/core/FlatMapJustPerf.java new file mode 100644 index 0000000000..597fae480d --- /dev/null +++ b/src/jmh/java/io/reactivex/rxjava3/core/FlatMapJustPerf.java @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.core; + +import java.util.concurrent.TimeUnit; + +import org.openjdk.jmh.annotations.*; +import org.openjdk.jmh.infra.Blackhole; +import org.reactivestreams.Publisher; + +import io.reactivex.rxjava3.functions.Function; + +@BenchmarkMode(Mode.Throughput) +@Warmup(iterations = 5) +@Measurement(iterations = 5, time = 5, timeUnit = TimeUnit.SECONDS) +@OutputTimeUnit(TimeUnit.SECONDS) +@Fork(value = 1) +@State(Scope.Thread) +public class FlatMapJustPerf { + @Param({ "1", "10", "100", "1000", "10000", "100000", "1000000" }) + public int times; + + Flowable<Integer> flowable; + + Observable<Integer> observable; + + @Setup + public void setup() { + Integer[] array = new Integer[times]; + + flowable = Flowable.fromArray(array).flatMap(new Function<Integer, Publisher<Integer>>() { + @Override + public Publisher<Integer> apply(Integer v) { + return Flowable.just(v); + } + }); + + observable = Observable.fromArray(array).flatMap(new Function<Integer, Observable<Integer>>() { + @Override + public Observable<Integer> apply(Integer v) { + return Observable.just(v); + } + }); + } + + @Benchmark + public void flowable(Blackhole bh) { + flowable.subscribe(new PerfConsumer(bh)); + } + + @Benchmark + public void observable(Blackhole bh) { + observable.subscribe(new PerfConsumer(bh)); + } +} diff --git a/src/jmh/java/io/reactivex/rxjava3/core/FlattenCrossMapPerf.java b/src/jmh/java/io/reactivex/rxjava3/core/FlattenCrossMapPerf.java new file mode 100644 index 0000000000..c5d516423f --- /dev/null +++ b/src/jmh/java/io/reactivex/rxjava3/core/FlattenCrossMapPerf.java @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.core; + +import java.util.Arrays; +import java.util.concurrent.TimeUnit; + +import org.openjdk.jmh.annotations.*; +import org.openjdk.jmh.infra.Blackhole; + +import io.reactivex.rxjava3.functions.Function; + +@BenchmarkMode(Mode.Throughput) +@Warmup(iterations = 5) +@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) +@OutputTimeUnit(TimeUnit.SECONDS) +@Fork(value = 1) +@State(Scope.Thread) +public class FlattenCrossMapPerf { + @Param({ "1", "10", "100", "1000", "10000", "100000", "1000000" }) + public int times; + + Flowable<Integer> flowable; + + Observable<Integer> observable; + + @Setup + public void setup() { + Integer[] array = new Integer[times]; + Arrays.fill(array, 777); + + Integer[] arrayInner = new Integer[1000000 / times]; + Arrays.fill(arrayInner, 888); + + final Iterable<Integer> list = Arrays.asList(arrayInner); + + flowable = Flowable.fromArray(array).flatMapIterable(new Function<Integer, Iterable<Integer>>() { + @Override + public Iterable<Integer> apply(Integer v) { + return list; + } + }); + + observable = Observable.fromArray(array).flatMapIterable(new Function<Integer, Iterable<Integer>>() { + @Override + public Iterable<Integer> apply(Integer v) { + return list; + } + }); + } + + @Benchmark + public void flowable(Blackhole bh) { + flowable.subscribe(new PerfConsumer(bh)); + } + + @Benchmark + public void observable(Blackhole bh) { + observable.subscribe(new PerfConsumer(bh)); + } +} diff --git a/src/jmh/java/io/reactivex/rxjava3/core/FlattenJustPerf.java b/src/jmh/java/io/reactivex/rxjava3/core/FlattenJustPerf.java new file mode 100644 index 0000000000..3bb1ad1473 --- /dev/null +++ b/src/jmh/java/io/reactivex/rxjava3/core/FlattenJustPerf.java @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.core; + +import java.util.*; +import java.util.concurrent.TimeUnit; + +import org.openjdk.jmh.annotations.*; +import org.openjdk.jmh.infra.Blackhole; + +import io.reactivex.rxjava3.functions.Function; + +@BenchmarkMode(Mode.Throughput) +@Warmup(iterations = 5) +@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) +@OutputTimeUnit(TimeUnit.SECONDS) +@Fork(value = 1) +@State(Scope.Thread) +public class FlattenJustPerf { + @Param({ "1", "10", "100", "1000", "10000", "100000", "1000000" }) + public int times; + + Flowable<Integer> flowable; + + Observable<Integer> observable; + + @Setup + public void setup() { + Integer[] array = new Integer[times]; + Arrays.fill(array, 777); + + final Iterable<Integer> singletonList = Collections.singletonList(1); + + flowable = Flowable.fromArray(array).flatMapIterable(new Function<Integer, Iterable<Integer>>() { + @Override + public Iterable<Integer> apply(Integer v) { + return singletonList; + } + }); + + observable = Observable.fromArray(array).flatMapIterable(new Function<Integer, Iterable<Integer>>() { + @Override + public Iterable<Integer> apply(Integer v) { + return singletonList; + } + }); + } + + @Benchmark + public void flowable(Blackhole bh) { + flowable.subscribe(new PerfConsumer(bh)); + } + + @Benchmark + public void observable(Blackhole bh) { + observable.subscribe(new PerfConsumer(bh)); + } +} diff --git a/src/jmh/java/io/reactivex/rxjava3/core/FlattenRangePerf.java b/src/jmh/java/io/reactivex/rxjava3/core/FlattenRangePerf.java new file mode 100644 index 0000000000..0339f345c3 --- /dev/null +++ b/src/jmh/java/io/reactivex/rxjava3/core/FlattenRangePerf.java @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.core; + +import java.util.Arrays; +import java.util.concurrent.TimeUnit; + +import org.openjdk.jmh.annotations.*; +import org.openjdk.jmh.infra.Blackhole; + +import io.reactivex.rxjava3.functions.Function; + +@BenchmarkMode(Mode.Throughput) +@Warmup(iterations = 5) +@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) +@OutputTimeUnit(TimeUnit.SECONDS) +@Fork(value = 1) +@State(Scope.Thread) +public class FlattenRangePerf { + @Param({ "1", "10", "100", "1000", "10000", "100000", "1000000" }) + public int times; + + Flowable<Integer> flowable; + + Observable<Integer> observable; + + @Setup + public void setup() { + Integer[] array = new Integer[times]; + Arrays.fill(array, 777); + + final Iterable<Integer> list = Arrays.asList(1, 2); + + flowable = Flowable.fromArray(array).flatMapIterable(new Function<Integer, Iterable<Integer>>() { + @Override + public Iterable<Integer> apply(Integer v) { + return list; + } + }); + + observable = Observable.fromArray(array).flatMapIterable(new Function<Integer, Iterable<Integer>>() { + @Override + public Iterable<Integer> apply(Integer v) { + return list; + } + }); + } + + @Benchmark + public void flowable(Blackhole bh) { + flowable.subscribe(new PerfConsumer(bh)); + } + + @Benchmark + public void observable(Blackhole bh) { + observable.subscribe(new PerfConsumer(bh)); + } +} diff --git a/src/jmh/java/io/reactivex/rxjava3/core/FlowableFlatMapCompletableAsyncPerf.java b/src/jmh/java/io/reactivex/rxjava3/core/FlowableFlatMapCompletableAsyncPerf.java new file mode 100644 index 0000000000..0ebb2d4458 --- /dev/null +++ b/src/jmh/java/io/reactivex/rxjava3/core/FlowableFlatMapCompletableAsyncPerf.java @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.core; + +import java.util.Arrays; +import java.util.concurrent.TimeUnit; + +import org.openjdk.jmh.annotations.*; +import org.openjdk.jmh.infra.Blackhole; + +import io.reactivex.rxjava3.functions.Action; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.schedulers.Schedulers; + +@BenchmarkMode(Mode.Throughput) +@Warmup(iterations = 5) +@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) +@OutputTimeUnit(TimeUnit.SECONDS) +@Fork(value = 1) +@State(Scope.Thread) +public class FlowableFlatMapCompletableAsyncPerf implements Action { + + @Param({"1", "10", "100", "1000", "10000", "100000", "1000000"}) + int items; + + @Param({"1", "8", "32", "128", "256"}) + int maxConcurrency; + + @Param({"1", "10", "100", "1000"}) + int work; + + Completable flatMapCompletable; + + Flowable<Object> flatMap; + + @Override + public void run() { + Blackhole.consumeCPU(work); + } + + @Setup + public void setup() { + Integer[] array = new Integer[items]; + Arrays.fill(array, 777); + + flatMapCompletable = Flowable.fromArray(array) + .flatMapCompletable(Functions.justFunction(Completable.fromAction(this).subscribeOn(Schedulers.computation())), false, maxConcurrency); + + flatMap = Flowable.fromArray(array) + .flatMap(Functions.justFunction(Completable.fromAction(this).subscribeOn(Schedulers.computation()).toFlowable()), false, maxConcurrency); + } + +// @Benchmark + public Object flatMap(Blackhole bh) { + return flatMap.subscribeWith(new PerfAsyncConsumer(bh)).await(items); + } + + @Benchmark + public Object flatMapCompletable(Blackhole bh) { + return flatMapCompletable.subscribeWith(new PerfAsyncConsumer(bh)).await(items); + } +} diff --git a/src/jmh/java/io/reactivex/rxjava3/core/FlowableFlatMapCompletableSyncPerf.java b/src/jmh/java/io/reactivex/rxjava3/core/FlowableFlatMapCompletableSyncPerf.java new file mode 100644 index 0000000000..400e0609a8 --- /dev/null +++ b/src/jmh/java/io/reactivex/rxjava3/core/FlowableFlatMapCompletableSyncPerf.java @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.core; + +import java.util.Arrays; +import java.util.concurrent.TimeUnit; + +import org.openjdk.jmh.annotations.*; +import org.openjdk.jmh.infra.Blackhole; + +import io.reactivex.rxjava3.internal.functions.Functions; + +@BenchmarkMode(Mode.Throughput) +@Warmup(iterations = 5) +@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) +@OutputTimeUnit(TimeUnit.SECONDS) +@Fork(value = 1) +@State(Scope.Thread) +public class FlowableFlatMapCompletableSyncPerf { + + @Param({"1", "10", "100", "1000", "10000", "100000", "1000000"}) + int items; + + @Param({"1", "8", "32", "128", "256"}) + int maxConcurrency; + + Completable flatMapCompletable; + + Flowable<Object> flatMap; + + @Setup + public void setup() { + Integer[] array = new Integer[items]; + Arrays.fill(array, 777); + + flatMapCompletable = Flowable.fromArray(array) + .flatMapCompletable(Functions.justFunction(Completable.complete()), false, maxConcurrency); + + flatMap = Flowable.fromArray(array) + .flatMap(Functions.justFunction(Completable.complete().toFlowable()), false, maxConcurrency); + } + + @Benchmark + public Object flatMap(Blackhole bh) { + return flatMap.subscribeWith(new PerfConsumer(bh)); + } + + @Benchmark + public Object flatMapCompletable(Blackhole bh) { + return flatMapCompletable.subscribeWith(new PerfConsumer(bh)); + } +} diff --git a/src/jmh/java/io/reactivex/rxjava3/core/InputWithIncrementingInteger.java b/src/jmh/java/io/reactivex/rxjava3/core/InputWithIncrementingInteger.java new file mode 100644 index 0000000000..44d109fb4d --- /dev/null +++ b/src/jmh/java/io/reactivex/rxjava3/core/InputWithIncrementingInteger.java @@ -0,0 +1,124 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.core; + +import java.util.Iterator; + +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.infra.Blackhole; +import org.reactivestreams.*; + +import io.reactivex.rxjava3.internal.subscriptions.EmptySubscription; +import io.reactivex.rxjava3.subscribers.DefaultSubscriber; + +/** + * Exposes an Observable and Observer that increments n Integers and consumes them in a Blackhole. + */ +public abstract class InputWithIncrementingInteger { + final class DefaultSubscriberImpl extends DefaultSubscriber<Integer> { + @Override + public void onComplete() { + + } + + @Override + public void onError(Throwable e) { + + } + + @Override + public void onNext(Integer t) { + bh.consume(t); + } + } + + static final class IncrementingIterable implements Iterable<Integer> { + + final class IncrementingIterator implements Iterator<Integer> { + int i; + + @Override + public boolean hasNext() { + return i < size; + } + + @Override + public Integer next() { + Blackhole.consumeCPU(10); + return i++; + } + + @Override + public void remove() { + + } + } + + final int size; + + IncrementingIterable(int size) { + this.size = size; + } + + @Override + public Iterator<Integer> iterator() { + return new IncrementingIterator(); + } + } + + static final class IncrementingPublisher implements Publisher<Integer> { + + final int size; + + IncrementingPublisher(int size) { + this.size = size; + } + + @Override + public void subscribe(Subscriber<? super Integer> s) { + s.onSubscribe(EmptySubscription.INSTANCE); + for (int i = 0; i < size; i++) { + s.onNext(i); + } + s.onComplete(); + } + } + + public Iterable<Integer> iterable; + public Flowable<Integer> flowable; + public Flowable<Integer> firehose; + public Blackhole bh; + + public abstract int getSize(); + + @Setup + public void setup(final Blackhole bh) { + this.bh = bh; + final int size = getSize(); + flowable = Flowable.range(0, size); + + firehose = Flowable.unsafeCreate(new IncrementingPublisher(size)); + iterable = new IncrementingIterable(size); + + } + + public PerfSubscriber newLatchedObserver() { + return new PerfSubscriber(bh); + } + + public FlowableSubscriber<Integer> newSubscriber() { + return new DefaultSubscriberImpl(); + } + +} diff --git a/src/jmh/java/io/reactivex/rxjava3/core/JustAsyncPerf.java b/src/jmh/java/io/reactivex/rxjava3/core/JustAsyncPerf.java new file mode 100644 index 0000000000..6906470aea --- /dev/null +++ b/src/jmh/java/io/reactivex/rxjava3/core/JustAsyncPerf.java @@ -0,0 +1,183 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.core; + +import java.util.concurrent.TimeUnit; + +import org.openjdk.jmh.annotations.*; +import org.openjdk.jmh.infra.Blackhole; + +import io.reactivex.rxjava3.internal.schedulers.SingleScheduler; +import io.reactivex.rxjava3.schedulers.Schedulers; + +@BenchmarkMode(Mode.Throughput) +@Warmup(iterations = 5) +@Measurement(iterations = 5, time = 5, timeUnit = TimeUnit.SECONDS) +@OutputTimeUnit(TimeUnit.SECONDS) +@Fork(value = 1) +@State(Scope.Thread) +public class JustAsyncPerf { + + Flowable<Integer> subscribeOnFlowable; + + Flowable<Integer> observeOnFlowable; + + Flowable<Integer> pipelineFlowable; + + Observable<Integer> subscribeOnObservable; + + Observable<Integer> observeOnObservable; + + Observable<Integer> pipelineObservable; + + Single<Integer> observeOnSingle; + + Single<Integer> subscribeOnSingle; + + Single<Integer> pipelineSingle; + + Completable observeOnCompletable; + + Completable subscribeOnCompletable; + + Completable pipelineCompletable; + + Maybe<Integer> observeOnMaybe; + + Maybe<Integer> subscribeOnMaybe; + + Maybe<Integer> pipelineMaybe; + + @Setup + public void setup() { + + Scheduler s = Schedulers.single(); + + Scheduler s2 = new SingleScheduler(); + + subscribeOnFlowable = Flowable.just(1).subscribeOn(s); + + observeOnFlowable = Flowable.just(1).observeOn(s); + + pipelineFlowable = Flowable.just(1).subscribeOn(s).observeOn(s2); + + // ---- + + subscribeOnObservable = Observable.just(1).subscribeOn(s); + + observeOnObservable = Observable.just(1).observeOn(s); + + pipelineObservable = Observable.just(1).subscribeOn(s).observeOn(s2); + + // ---- + + observeOnSingle = Single.just(1).observeOn(s); + + subscribeOnSingle = Single.just(1).subscribeOn(s); + + pipelineSingle = Single.just(1).subscribeOn(s).observeOn(s2); + + // ---- + + observeOnCompletable = Completable.complete().observeOn(s); + + subscribeOnCompletable = Completable.complete().subscribeOn(s); + + pipelineCompletable = Completable.complete().subscribeOn(s).observeOn(s2); + + // ---- + + observeOnMaybe = Maybe.just(1).observeOn(s); + + subscribeOnMaybe = Maybe.just(1).subscribeOn(s); + + pipelineMaybe = Maybe.just(1).subscribeOn(s).observeOn(s2); + } + + @Benchmark + public void subscribeOnFlowable(Blackhole bh) { + subscribeOnFlowable.subscribeWith(new PerfAsyncConsumer(bh)).await(1); + } + + @Benchmark + public void observeOnFlowable(Blackhole bh) { + observeOnFlowable.subscribeWith(new PerfAsyncConsumer(bh)).await(1); + } + + @Benchmark + public void pipelineFlowable(Blackhole bh) { + pipelineFlowable.subscribeWith(new PerfAsyncConsumer(bh)).await(1); + } + + @Benchmark + public void subscribeOnObservable(Blackhole bh) { + subscribeOnObservable.subscribeWith(new PerfAsyncConsumer(bh)).await(1); + } + + @Benchmark + public void observeOnObservable(Blackhole bh) { + observeOnObservable.subscribeWith(new PerfAsyncConsumer(bh)).await(1); + } + + @Benchmark + public void pipelineObservable(Blackhole bh) { + pipelineObservable.subscribeWith(new PerfAsyncConsumer(bh)).await(1); + } + + @Benchmark + public void observeOnSingle(Blackhole bh) { + observeOnSingle.subscribeWith(new PerfAsyncConsumer(bh)).await(1); + } + + @Benchmark + public void subscribeOnSingle(Blackhole bh) { + subscribeOnSingle.subscribeWith(new PerfAsyncConsumer(bh)).await(1); + } + + @Benchmark + public void pipelineSingle(Blackhole bh) { + pipelineSingle.subscribeWith(new PerfAsyncConsumer(bh)).await(1); + } + + @Benchmark + public void observeOnCompletable(Blackhole bh) { + observeOnCompletable.subscribeWith(new PerfAsyncConsumer(bh)).await(1); + } + + @Benchmark + public void subscribeOnCompletable(Blackhole bh) { + subscribeOnCompletable.subscribeWith(new PerfAsyncConsumer(bh)).await(1); + } + + @Benchmark + public void pipelineCompletable(Blackhole bh) { + pipelineCompletable.subscribeWith(new PerfAsyncConsumer(bh)).await(1); + } + + @Benchmark + public void observeOnMaybe(Blackhole bh) { + observeOnMaybe.subscribeWith(new PerfAsyncConsumer(bh)).await(1); + } + + @Benchmark + public void subscribeOnMaybe(Blackhole bh) { + subscribeOnMaybe.subscribeWith(new PerfAsyncConsumer(bh)).await(1); + } + + @Benchmark + public void pipelineMaybe(Blackhole bh) { + pipelineMaybe.subscribeWith(new PerfAsyncConsumer(bh)).await(1); + } + +} diff --git a/src/jmh/java/io/reactivex/rxjava3/core/LatchedSingleObserver.java b/src/jmh/java/io/reactivex/rxjava3/core/LatchedSingleObserver.java new file mode 100644 index 0000000000..6bcdba465e --- /dev/null +++ b/src/jmh/java/io/reactivex/rxjava3/core/LatchedSingleObserver.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.core; + +import java.util.concurrent.CountDownLatch; + +import org.openjdk.jmh.infra.Blackhole; + +import io.reactivex.rxjava3.disposables.Disposable; + +public final class LatchedSingleObserver<T> implements SingleObserver<T> { + final CountDownLatch cdl; + final Blackhole bh; + public LatchedSingleObserver(Blackhole bh) { + this.bh = bh; + this.cdl = new CountDownLatch(1); + } + + @Override + public void onSubscribe(Disposable d) { + + } + + @Override + public void onSuccess(T value) { + bh.consume(value); + cdl.countDown(); + } + + @Override + public void onError(Throwable e) { + e.printStackTrace(); + cdl.countDown(); + } +} diff --git a/src/jmh/java/io/reactivex/rxjava3/core/MemoryPerf.java b/src/jmh/java/io/reactivex/rxjava3/core/MemoryPerf.java new file mode 100644 index 0000000000..7cd45de74e --- /dev/null +++ b/src/jmh/java/io/reactivex/rxjava3/core/MemoryPerf.java @@ -0,0 +1,517 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.core; + +import java.lang.management.ManagementFactory; +import java.util.concurrent.Callable; + +import org.reactivestreams.Subscription; + +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.functions.*; + +/** + * Measure various prepared flows about their memory usage and print the result + * in a JMH compatible format; run {@link #main(String[])}. + */ +public final class MemoryPerf { + + private MemoryPerf() { } + + static long memoryUse() { + return ManagementFactory.getMemoryMXBean().getHeapMemoryUsage().getUsed(); + } + + static final class MyRx2Subscriber implements FlowableSubscriber<Object> { + + org.reactivestreams.Subscription upstream; + + @Override + public void onSubscribe(Subscription s) { + this.upstream = s; + } + + @Override + public void onComplete() { + + } + + @Override + public void onError(Throwable e) { + + } + + @Override + public void onNext(Object t) { + + } + } + + static final class MyRx2Observer implements io.reactivex.rxjava3.core.Observer<Object>, io.reactivex.rxjava3.core.SingleObserver<Object>, + io.reactivex.rxjava3.core.MaybeObserver<Object>, io.reactivex.rxjava3.core.CompletableObserver { + + Disposable upstream; + + @Override + public void onSubscribe(Disposable d) { + this.upstream = d; + } + + @Override + public void onComplete() { + + } + + @Override + public void onError(Throwable e) { + + } + + @Override + public void onNext(Object t) { + + } + + @Override + public void onSuccess(Object value) { + + } + } + static <U> void checkMemory(Callable<U> item, String name, String typeLib) throws Exception { + checkMemory(item, name, typeLib, 1000000); + } + + static <U> void checkMemory(Callable<U> item, String name, String typeLib, int n) throws Exception { + // make sure classes are initialized + item.call(); + + Object[] array = new Object[n]; + + Thread.sleep(100); + System.gc(); + Thread.sleep(100); + + long before = memoryUse(); + + for (int i = 0; i < n; i++) { + array[i] = item.call(); + } + + Thread.sleep(100); + System.gc(); + Thread.sleep(100); + + long after = memoryUse(); + + double use = Math.max(0.0, (after - before) / 1024.0 / 1024.0); + + System.out.print(name); + System.out.print(" "); + System.out.print(typeLib); + System.out.print(" thrpt "); + System.out.print(n); + System.out.printf(" %.3f 0.000 MB%n", use); + + if (array.hashCode() == 1) { + System.out.print(""); + } + + array = null; + item = null; + + Thread.sleep(100); + System.gc(); + Thread.sleep(100); + } + + public static void main(String[] args) throws Exception { + + System.out.println("Benchmark (lib-type) Mode Cnt Score Error Units"); + + // --------------------------------------------------------------------------------------------------------------------- + + checkMemory(new Callable<Object>() { + @Override + public Object call() { + return io.reactivex.rxjava3.core.Observable.just(1); + } + }, "just", "Rx2Observable"); + + checkMemory(new Callable<Object>() { + @Override + public Object call() { + return io.reactivex.rxjava3.core.Observable.range(1, 10); + } + }, "range", "Rx2Observable"); + + checkMemory(new Callable<Object>() { + @Override + public Object call() { + return io.reactivex.rxjava3.core.Observable.empty(); + } + }, "empty", "Rx2Observable"); + + checkMemory(new Callable<Object>() { + @Override + public Object call() { + return io.reactivex.rxjava3.core.Observable.fromCallable(new Callable<Object>() { + @Override + public Object call() { + return 1; + } + }); + } + }, "fromCallable", "Rx2Observable"); + + checkMemory(new Callable<Object>() { + @Override + public Object call() { + return new MyRx2Observer(); + } + }, "consumer", "Rx2Observable"); + + checkMemory(new Callable<Object>() { + @Override + public Object call() { + return new io.reactivex.rxjava3.observers.TestObserver<>(); + } + }, "test-consumer", "Rx2Observable"); + + checkMemory(new Callable<Object>() { + @Override + public Object call() { + return io.reactivex.rxjava3.core.Observable.just(1).subscribeWith(new MyRx2Observer()); + } + }, "just+consumer", "Rx2Observable"); + + checkMemory(new Callable<Object>() { + @Override + public Object call() { + return io.reactivex.rxjava3.core.Observable.range(1, 10).subscribeWith(new MyRx2Observer()); + } + }, "range+consumer", "Rx2Observable"); + + checkMemory(new Callable<Object>() { + @Override + public Object call() { + return io.reactivex.rxjava3.core.Observable.range(1, 10).map(new Function<Integer, Object>() { + @Override + public Object apply(Integer v) { + return v; + } + }).subscribeWith(new MyRx2Observer()); + } + }, "range+map+consumer", "Rx2Observable"); + + checkMemory(new Callable<Object>() { + @Override + public Object call() { + return io.reactivex.rxjava3.core.Observable.range(1, 10).map(new Function<Integer, Object>() { + @Override + public Object apply(Integer v) { + return v; + } + }).filter(new Predicate<Object>() { + @Override + public boolean test(Object v) { + return true; + } + }).subscribeWith(new MyRx2Observer()); + } + }, "range+map+filter+consumer", "Rx2Observable"); + + checkMemory(new Callable<Object>() { + @Override + public Object call() { + return io.reactivex.rxjava3.core.Observable.range(1, 10).subscribeOn(io.reactivex.rxjava3.schedulers.Schedulers.computation()).subscribeWith(new MyRx2Observer()); + } + }, "range+subscribeOn+consumer", "Rx2Observable"); + + checkMemory(new Callable<Object>() { + @Override + public Object call() { + return io.reactivex.rxjava3.core.Observable.range(1, 10).observeOn(io.reactivex.rxjava3.schedulers.Schedulers.computation()).subscribeWith(new MyRx2Observer()); + } + }, "range+observeOn+consumer", "Rx2Observable"); + + checkMemory(new Callable<Object>() { + @Override + public Object call() { + return io.reactivex.rxjava3.core.Observable.range(1, 10).subscribeOn(io.reactivex.rxjava3.schedulers.Schedulers.computation()).observeOn(io.reactivex.rxjava3.schedulers.Schedulers.computation()).subscribeWith(new MyRx2Observer()); + } + }, "range+subscribeOn+observeOn+consumer", "Rx2Observable"); + + checkMemory(new Callable<Object>() { + @Override + public Object call() { + return io.reactivex.rxjava3.subjects.AsyncSubject.create(); + } + }, "Async", "Rx2Observable"); + + checkMemory(new Callable<Object>() { + @Override + public Object call() { + return io.reactivex.rxjava3.subjects.PublishSubject.create(); + } + }, "Publish", "Rx2Observable"); + + checkMemory(new Callable<Object>() { + @Override + public Object call() { + return io.reactivex.rxjava3.subjects.ReplaySubject.create(); + } + }, "Replay", "Rx2Observable"); + + checkMemory(new Callable<Object>() { + @Override + public Object call() { + return io.reactivex.rxjava3.subjects.BehaviorSubject.create(); + } + }, "Behavior", "Rx2Observable"); + + checkMemory(new Callable<Object>() { + @Override + public Object call() { + return io.reactivex.rxjava3.subjects.UnicastSubject.create(); + } + }, "Unicast", "Rx2Observable"); + + checkMemory(new Callable<Object>() { + @Override + public Object call() { + return io.reactivex.rxjava3.subjects.AsyncSubject.create().subscribeWith(new MyRx2Observer()); + } + }, "Async+consumer", "Rx2Observable"); + + checkMemory(new Callable<Object>() { + @Override + public Object call() { + return io.reactivex.rxjava3.subjects.PublishSubject.create().subscribeWith(new MyRx2Observer()); + } + }, "Publish+consumer", "Rx2Observable"); + + checkMemory(new Callable<Object>() { + @Override + public Object call() { + return io.reactivex.rxjava3.subjects.ReplaySubject.create().subscribeWith(new MyRx2Observer()); + } + }, "Replay+consumer", "Rx2Observable"); + + checkMemory(new Callable<Object>() { + @Override + public Object call() { + return io.reactivex.rxjava3.subjects.BehaviorSubject.create().subscribeWith(new MyRx2Observer()); + } + }, "Behavior+consumer", "Rx2Observable"); + + checkMemory(new Callable<Object>() { + @Override + public Object call() { + return io.reactivex.rxjava3.subjects.UnicastSubject.create().subscribeWith(new MyRx2Observer()); + } + }, "Unicast+consumer", "Rx2Observable"); + + // --------------------------------------------------------------------------------------------------------------------- + + checkMemory(new Callable<Object>() { + @Override + public Object call() { + return io.reactivex.rxjava3.core.Flowable.just(1); + } + }, "just", "Rx2Flowable"); + + checkMemory(new Callable<Object>() { + @Override + public Object call() { + return io.reactivex.rxjava3.core.Flowable.range(1, 10); + } + }, "range", "Rx2Flowable"); + + checkMemory(new Callable<Object>() { + @Override + public Object call() { + return io.reactivex.rxjava3.core.Flowable.empty(); + } + }, "empty", "Rx2Flowable"); + + checkMemory(new Callable<Object>() { + @Override + public Object call() { + return io.reactivex.rxjava3.core.Flowable.empty(); + } + }, "empty", "Rx2Flowable", 10000000); + + checkMemory(new Callable<Object>() { + @Override + public Object call() { + return io.reactivex.rxjava3.core.Flowable.fromCallable(new Callable<Object>() { + @Override + public Object call() { + return 1; + } + }); + } + }, "fromCallable", "Rx2Flowable"); + + checkMemory(new Callable<Object>() { + @Override + public Object call() { + return new MyRx2Subscriber(); + } + }, "consumer", "Rx2Flowable"); + + checkMemory(new Callable<Object>() { + @Override + public Object call() { + return new io.reactivex.rxjava3.observers.TestObserver<>(); + } + }, "test-consumer", "Rx2Flowable"); + + checkMemory(new Callable<Object>() { + @Override + public Object call() { + return io.reactivex.rxjava3.core.Flowable.just(1).subscribeWith(new MyRx2Subscriber()); + } + }, "just+consumer", "Rx2Flowable"); + + checkMemory(new Callable<Object>() { + @Override + public Object call() { + return io.reactivex.rxjava3.core.Flowable.range(1, 10).subscribeWith(new MyRx2Subscriber()); + } + }, "range+consumer", "Rx2Flowable"); + + checkMemory(new Callable<Object>() { + @Override + public Object call() { + return io.reactivex.rxjava3.core.Flowable.range(1, 10).map(new Function<Integer, Object>() { + @Override + public Object apply(Integer v) { + return v; + } + }).subscribeWith(new MyRx2Subscriber()); + } + }, "range+map+consumer", "Rx2Flowable"); + + checkMemory(new Callable<Object>() { + @Override + public Object call() { + return io.reactivex.rxjava3.core.Flowable.range(1, 10).map(new Function<Integer, Object>() { + @Override + public Object apply(Integer v) { + return v; + } + }).filter(new Predicate<Object>() { + @Override + public boolean test(Object v) { + return true; + } + }).subscribeWith(new MyRx2Subscriber()); + } + }, "range+map+filter+consumer", "Rx2Flowable"); + + checkMemory(new Callable<Object>() { + @Override + public Object call() { + return io.reactivex.rxjava3.core.Flowable.range(1, 10).subscribeOn(io.reactivex.rxjava3.schedulers.Schedulers.computation()).subscribeWith(new MyRx2Subscriber()); + } + }, "range+subscribeOn+consumer", "Rx2Flowable"); + + checkMemory(new Callable<Object>() { + @Override + public Object call() { + return io.reactivex.rxjava3.core.Flowable.range(1, 10).observeOn(io.reactivex.rxjava3.schedulers.Schedulers.computation()).subscribeWith(new MyRx2Subscriber()); + } + }, "range+observeOn+consumer", "Rx2Flowable"); + + checkMemory(new Callable<Object>() { + @Override + public Object call() { + return io.reactivex.rxjava3.core.Flowable.range(1, 10).subscribeOn(io.reactivex.rxjava3.schedulers.Schedulers.computation()).observeOn(io.reactivex.rxjava3.schedulers.Schedulers.computation()).subscribeWith(new MyRx2Subscriber()); + } + }, "range+subscribeOn+observeOn+consumer", "Rx2Flowable"); + + checkMemory(new Callable<Object>() { + @Override + public Object call() { + return io.reactivex.rxjava3.processors.AsyncProcessor.create(); + } + }, "Async", "Rx2Flowable"); + + checkMemory(new Callable<Object>() { + @Override + public Object call() { + return io.reactivex.rxjava3.processors.PublishProcessor.create(); + } + }, "Publish", "Rx2Flowable"); + + checkMemory(new Callable<Object>() { + @Override + public Object call() { + return io.reactivex.rxjava3.processors.ReplayProcessor.create(); + } + }, "Replay", "Rx2Flowable"); + + checkMemory(new Callable<Object>() { + @Override + public Object call() { + return io.reactivex.rxjava3.processors.BehaviorProcessor.create(); + } + }, "Behavior", "Rx2Flowable"); + + checkMemory(new Callable<Object>() { + @Override + public Object call() { + return io.reactivex.rxjava3.processors.UnicastProcessor.create(); + } + }, "Unicast", "Rx2Flowable"); + + checkMemory(new Callable<Object>() { + @Override + public Object call() { + return io.reactivex.rxjava3.processors.AsyncProcessor.create().subscribeWith(new MyRx2Subscriber()); + } + }, "Async+consumer", "Rx2Flowable"); + + checkMemory(new Callable<Object>() { + @Override + public Object call() { + return io.reactivex.rxjava3.processors.PublishProcessor.create().subscribeWith(new MyRx2Subscriber()); + } + }, "Publish+consumer", "Rx2Flowable"); + + checkMemory(new Callable<Object>() { + @Override + public Object call() { + return io.reactivex.rxjava3.processors.ReplayProcessor.create().subscribeWith(new MyRx2Subscriber()); + } + }, "Replay+consumer", "Rx2Flowable"); + + checkMemory(new Callable<Object>() { + @Override + public Object call() { + return io.reactivex.rxjava3.processors.BehaviorProcessor.create().subscribeWith(new MyRx2Subscriber()); + } + }, "Behavior+consumer", "Rx2Flowable"); + + checkMemory(new Callable<Object>() { + @Override + public Object call() { + return io.reactivex.rxjava3.processors.UnicastProcessor.create().subscribeWith(new MyRx2Subscriber()); + } + }, "Unicast+consumer", "Rx2Flowable"); + + // --------------------------------------------------------------------------------------------------------------------- + } +} diff --git a/src/jmh/java/io/reactivex/rxjava3/core/ObservableFlatMapPerf.java b/src/jmh/java/io/reactivex/rxjava3/core/ObservableFlatMapPerf.java new file mode 100644 index 0000000000..65145b07b4 --- /dev/null +++ b/src/jmh/java/io/reactivex/rxjava3/core/ObservableFlatMapPerf.java @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.core; + +import java.util.Arrays; +import java.util.concurrent.TimeUnit; + +import org.openjdk.jmh.annotations.*; +import org.openjdk.jmh.infra.Blackhole; + +import io.reactivex.rxjava3.functions.Function; + +@BenchmarkMode(Mode.Throughput) +@Warmup(iterations = 5) +@Measurement(iterations = 5, time = 5, timeUnit = TimeUnit.SECONDS) +@OutputTimeUnit(TimeUnit.SECONDS) +@Fork(value = 1) +@State(Scope.Thread) +public class ObservableFlatMapPerf { + @Param({ "1", "10", "100", "1000", "10000", "100000", "1000000" }) + public int count; + + Observable<Integer> source; + + @Setup + public void setup() { + int d = 1000000 / count; + + Integer[] mainArray = new Integer[count]; + Integer[] innerArray = new Integer[d]; + + Arrays.fill(mainArray, 777); + Arrays.fill(innerArray, 777); + + Observable<Integer> outer = Observable.fromArray(mainArray); + final Observable<Integer> inner = Observable.fromArray(innerArray); + + source = outer.flatMap(new Function<Integer, Observable<Integer>>() { + @Override + public Observable<Integer> apply(Integer t) { + return inner; + } + }); + } + + @Benchmark + public void flatMapXRange(Blackhole bh) { + source.subscribe(new PerfObserver(bh)); + } +} diff --git a/src/jmh/java/io/reactivex/rxjava3/core/OperatorFlatMapPerf.java b/src/jmh/java/io/reactivex/rxjava3/core/OperatorFlatMapPerf.java new file mode 100644 index 0000000000..777fc9e1a6 --- /dev/null +++ b/src/jmh/java/io/reactivex/rxjava3/core/OperatorFlatMapPerf.java @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.core; + +import java.util.concurrent.TimeUnit; + +import org.openjdk.jmh.annotations.*; +import org.reactivestreams.Publisher; + +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.schedulers.Schedulers; + +@BenchmarkMode(Mode.Throughput) +@OutputTimeUnit(TimeUnit.SECONDS) +@State(Scope.Thread) +public class OperatorFlatMapPerf { + + @State(Scope.Thread) + public static class Input extends InputWithIncrementingInteger { + + @Param({ "1", "1000", "1000000" }) + public int size; + + @Override + public int getSize() { + return size; + } + + } + + @Benchmark + public void flatMapIntPassthruSync(Input input) { + input.flowable.flatMap(new Function<Integer, Publisher<Integer>>() { + @Override + public Publisher<Integer> apply(Integer v) { + return Flowable.just(v); + } + }).subscribe(input.newSubscriber()); + } + + @Benchmark + public void flatMapIntPassthruAsync(Input input) throws InterruptedException { + PerfSubscriber latchedObserver = input.newLatchedObserver(); + input.flowable.flatMap(new Function<Integer, Publisher<Integer>>() { + @Override + public Publisher<Integer> apply(Integer i) { + return Flowable.just(i).subscribeOn(Schedulers.computation()); + } + }).subscribe(latchedObserver); + if (input.size == 1) { + while (latchedObserver.latch.getCount() != 0) { } + } else { + latchedObserver.latch.await(); + } + } + + @Benchmark + public void flatMapTwoNestedSync(final Input input) { + Flowable.range(1, 2).flatMap(new Function<Integer, Publisher<Integer>>() { + @Override + public Publisher<Integer> apply(Integer i) { + return input.flowable; + } + }).subscribe(input.newSubscriber()); + } + + // this runs out of memory currently + // @Benchmark + // public void flatMapTwoNestedAsync(final Input input) throws InterruptedException { + // Observable.range(1, 2).flatMap(new Func1<Integer, Observable<Integer>>() { + // + // @Override + // public Observable<Integer> call(Integer i) { + // return input.observable.subscribeOn(Schedulers.computation()); + // } + // + // }).subscribe(input.observer); + // } + +} diff --git a/src/jmh/java/io/reactivex/rxjava3/core/OperatorMergePerf.java b/src/jmh/java/io/reactivex/rxjava3/core/OperatorMergePerf.java new file mode 100644 index 0000000000..24605fc02a --- /dev/null +++ b/src/jmh/java/io/reactivex/rxjava3/core/OperatorMergePerf.java @@ -0,0 +1,175 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.core; + +import java.util.*; +import java.util.concurrent.TimeUnit; + +import org.openjdk.jmh.annotations.*; +import org.openjdk.jmh.infra.Blackhole; + +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.schedulers.Schedulers; + +@BenchmarkMode(Mode.Throughput) +@OutputTimeUnit(TimeUnit.SECONDS) +public class OperatorMergePerf { + + // flatMap + @Benchmark + public void oneStreamOfNthatMergesIn1(final InputMillion input) throws InterruptedException { + Flowable<Flowable<Integer>> os = Flowable.range(1, input.size) + .map(new Function<Integer, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Integer v) { + return Flowable.just(v); + } + }); + PerfSubscriber o = input.newLatchedObserver(); + Flowable.merge(os).subscribe(o); + + if (input.size == 1) { + while (o.latch.getCount() != 0) { } + } else { + o.latch.await(); + } + } + + // flatMap + @Benchmark + public void merge1SyncStreamOfN(final InputMillion input) throws InterruptedException { + Flowable<Flowable<Integer>> os = Flowable.just(1).map(new Function<Integer, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Integer i) { + return Flowable.range(0, input.size); + } + }); + PerfSubscriber o = input.newLatchedObserver(); + Flowable.merge(os).subscribe(o); + + if (input.size == 1) { + while (o.latch.getCount() != 0) { } + } else { + o.latch.await(); + } + } + + @Benchmark + public void mergeNSyncStreamsOfN(final InputThousand input) throws InterruptedException { + Flowable<Flowable<Integer>> os = input.flowable.map(new Function<Integer, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Integer i) { + return Flowable.range(0, input.size); + } + }); + PerfSubscriber o = input.newLatchedObserver(); + Flowable.merge(os).subscribe(o); + if (input.size == 1) { + while (o.latch.getCount() != 0) { } + } else { + o.latch.await(); + } + } + + @Benchmark + public void mergeNAsyncStreamsOfN(final InputThousand input) throws InterruptedException { + Flowable<Flowable<Integer>> os = input.flowable.map(new Function<Integer, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Integer i) { + return Flowable.range(0, input.size).subscribeOn(Schedulers.computation()); + } + }); + PerfSubscriber o = input.newLatchedObserver(); + Flowable.merge(os).subscribe(o); + if (input.size == 1) { + while (o.latch.getCount() != 0) { } + } else { + o.latch.await(); + } + } + + @Benchmark + public void mergeTwoAsyncStreamsOfN(final InputThousand input) throws InterruptedException { + PerfSubscriber o = input.newLatchedObserver(); + Flowable<Integer> ob = Flowable.range(0, input.size).subscribeOn(Schedulers.computation()); + Flowable.merge(ob, ob).subscribe(o); + if (input.size == 1) { + while (o.latch.getCount() != 0) { } + } else { + o.latch.await(); + } + } + + @Benchmark + public void mergeNSyncStreamsOf1(final InputForMergeN input) throws InterruptedException { + PerfSubscriber o = input.newLatchedObserver(); + Flowable.merge(input.observables).subscribe(o); + if (input.size == 1) { + while (o.latch.getCount() != 0) { } + } else { + o.latch.await(); + } + } + + @State(Scope.Thread) + public static class InputForMergeN { + @Param({ "1", "100", "1000" }) + // @Param({ "1000" }) + public int size; + + private Blackhole bh; + List<Flowable<Integer>> observables; + + @Setup + public void setup(final Blackhole bh) { + this.bh = bh; + observables = new ArrayList<>(); + for (int i = 0; i < size; i++) { + observables.add(Flowable.just(i)); + } + } + + public PerfSubscriber newLatchedObserver() { + return new PerfSubscriber(bh); + } + } + + @State(Scope.Thread) + public static class InputMillion extends InputWithIncrementingInteger { + + @Param({ "1", "1000", "1000000" }) + // @Param({ "1000" }) + public int size; + + @Override + public int getSize() { + return size; + } + + } + + @State(Scope.Thread) + public static class InputThousand extends InputWithIncrementingInteger { + + @Param({ "1", "1000" }) + // @Param({ "1000" }) + public int size; + + @Override + public int getSize() { + return size; + } + + } +} diff --git a/src/jmh/java/io/reactivex/rxjava3/core/PerfAsyncConsumer.java b/src/jmh/java/io/reactivex/rxjava3/core/PerfAsyncConsumer.java new file mode 100644 index 0000000000..ccaaf75573 --- /dev/null +++ b/src/jmh/java/io/reactivex/rxjava3/core/PerfAsyncConsumer.java @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.core; + +import java.util.concurrent.CountDownLatch; + +import org.openjdk.jmh.infra.Blackhole; +import org.reactivestreams.Subscription; + +import io.reactivex.rxjava3.disposables.Disposable; + +/** + * A multi-type asynchronous consumer. + */ +public final class PerfAsyncConsumer extends CountDownLatch implements FlowableSubscriber<Object>, Observer<Object>, +SingleObserver<Object>, CompletableObserver, MaybeObserver<Object> { + + final Blackhole bh; + + public PerfAsyncConsumer(Blackhole bh) { + super(1); + this.bh = bh; + } + + @Override + public void onSuccess(Object value) { + bh.consume(value); + countDown(); + } + + @Override + public void onSubscribe(Disposable d) { + } + + @Override + public void onSubscribe(Subscription s) { + s.request(Long.MAX_VALUE); + } + + @Override + public void onNext(Object t) { + bh.consume(t); + } + + @Override + public void onError(Throwable t) { + t.printStackTrace(); + countDown(); + } + + @Override + public void onComplete() { + bh.consume(true); + countDown(); + } + + /** + * Wait for the terminal signal. + * @param count if less than 1001, a spin-wait is used + * @return this + */ + public PerfAsyncConsumer await(int count) { + if (count <= 1000) { + while (getCount() != 0) { } + } else { + try { + await(); + } catch (InterruptedException ex) { + throw new RuntimeException(ex); + } + } + return this; + } + +} diff --git a/src/jmh/java/io/reactivex/rxjava3/core/PerfBoundedSubscriber.java b/src/jmh/java/io/reactivex/rxjava3/core/PerfBoundedSubscriber.java new file mode 100644 index 0000000000..68083976be --- /dev/null +++ b/src/jmh/java/io/reactivex/rxjava3/core/PerfBoundedSubscriber.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.core; + +import java.util.concurrent.CountDownLatch; + +import org.openjdk.jmh.infra.Blackhole; +import org.reactivestreams.Subscription; + +/** + * Performance subscriber with a one-time request from the upstream. + */ +public class PerfBoundedSubscriber extends CountDownLatch implements FlowableSubscriber<Object> { + + final Blackhole bh; + + final long request; + + public PerfBoundedSubscriber(Blackhole bh, long request) { + super(1); + this.bh = bh; + this.request = request; + } + + @Override + public void onSubscribe(Subscription s) { + s.request(request); + } + + @Override + public void onComplete() { + countDown(); + } + + @Override + public void onError(Throwable e) { + countDown(); + } + + @Override + public void onNext(Object t) { + bh.consume(t); + } + +} diff --git a/src/jmh/java/io/reactivex/rxjava3/core/PerfConsumer.java b/src/jmh/java/io/reactivex/rxjava3/core/PerfConsumer.java new file mode 100644 index 0000000000..60f93f089a --- /dev/null +++ b/src/jmh/java/io/reactivex/rxjava3/core/PerfConsumer.java @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.core; + +import org.openjdk.jmh.infra.Blackhole; +import org.reactivestreams.Subscription; + +import io.reactivex.rxjava3.disposables.Disposable; + +/** + * A multi-type synchronous consumer. + */ +public final class PerfConsumer implements FlowableSubscriber<Object>, Observer<Object>, +SingleObserver<Object>, CompletableObserver, MaybeObserver<Object> { + + final Blackhole bh; + + public PerfConsumer(Blackhole bh) { + this.bh = bh; + } + + @Override + public void onSuccess(Object value) { + bh.consume(value); + } + + @Override + public void onSubscribe(Disposable d) { + } + + @Override + public void onSubscribe(Subscription s) { + s.request(Long.MAX_VALUE); + } + + @Override + public void onNext(Object t) { + bh.consume(t); + } + + @Override + public void onError(Throwable t) { + t.printStackTrace(); + } + + @Override + public void onComplete() { + bh.consume(true); + } +} diff --git a/src/jmh/java/io/reactivex/rxjava3/core/PerfInteropConsumer.java b/src/jmh/java/io/reactivex/rxjava3/core/PerfInteropConsumer.java new file mode 100644 index 0000000000..fc7f0b04b0 --- /dev/null +++ b/src/jmh/java/io/reactivex/rxjava3/core/PerfInteropConsumer.java @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.core; + +import org.openjdk.jmh.infra.Blackhole; +import org.reactivestreams.*; + +import io.reactivex.rxjava3.disposables.Disposable; + +/** + * A multi-type synchronous consumer that doesn't implement FlowableSubscriber and + * thus should be treated by Flowable as a candidate for strict interop. + */ +public final class PerfInteropConsumer implements Subscriber<Object>, Observer<Object>, +SingleObserver<Object>, CompletableObserver, MaybeObserver<Object> { + + final Blackhole bh; + + public PerfInteropConsumer(Blackhole bh) { + this.bh = bh; + } + + @Override + public void onSuccess(Object value) { + bh.consume(value); + } + + @Override + public void onSubscribe(Disposable d) { + } + + @Override + public void onSubscribe(Subscription s) { + s.request(Long.MAX_VALUE); + } + + @Override + public void onNext(Object t) { + bh.consume(t); + } + + @Override + public void onError(Throwable t) { + t.printStackTrace(); + } + + @Override + public void onComplete() { + bh.consume(true); + } +} diff --git a/src/jmh/java/io/reactivex/rxjava3/core/PerfObserver.java b/src/jmh/java/io/reactivex/rxjava3/core/PerfObserver.java new file mode 100644 index 0000000000..a62a1e68f6 --- /dev/null +++ b/src/jmh/java/io/reactivex/rxjava3/core/PerfObserver.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.core; + +import java.util.concurrent.CountDownLatch; + +import org.openjdk.jmh.infra.Blackhole; + +import io.reactivex.rxjava3.disposables.Disposable; + +public final class PerfObserver implements Observer<Object> { + final CountDownLatch cdl; + final Blackhole bh; + public PerfObserver(Blackhole bh) { + this.bh = bh; + this.cdl = new CountDownLatch(1); + } + + @Override + public void onSubscribe(Disposable d) { + + } + + @Override + public void onNext(Object value) { + bh.consume(value); + } + + @Override + public void onError(Throwable e) { + e.printStackTrace(); + cdl.countDown(); + } + + @Override + public void onComplete() { + cdl.countDown(); + } +} diff --git a/src/jmh/java/io/reactivex/rxjava3/core/PerfSubscriber.java b/src/jmh/java/io/reactivex/rxjava3/core/PerfSubscriber.java new file mode 100644 index 0000000000..8c0f30cffa --- /dev/null +++ b/src/jmh/java/io/reactivex/rxjava3/core/PerfSubscriber.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.core; + +import java.util.concurrent.CountDownLatch; + +import org.openjdk.jmh.infra.Blackhole; +import org.reactivestreams.Subscription; + +public class PerfSubscriber implements FlowableSubscriber<Object> { + + public final CountDownLatch latch = new CountDownLatch(1); + private final Blackhole bh; + + public PerfSubscriber(Blackhole bh) { + this.bh = bh; + } + + @Override + public void onSubscribe(Subscription s) { + s.request(Long.MAX_VALUE); + } + + @Override + public void onComplete() { + latch.countDown(); + } + + @Override + public void onError(Throwable e) { + latch.countDown(); + } + + @Override + public void onNext(Object t) { + bh.consume(t); + } + +} diff --git a/src/jmh/java/io/reactivex/rxjava3/core/PublishProcessorPerf.java b/src/jmh/java/io/reactivex/rxjava3/core/PublishProcessorPerf.java new file mode 100644 index 0000000000..1278bfd641 --- /dev/null +++ b/src/jmh/java/io/reactivex/rxjava3/core/PublishProcessorPerf.java @@ -0,0 +1,106 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.core; + +import java.util.concurrent.TimeUnit; + +import org.openjdk.jmh.annotations.*; +import org.openjdk.jmh.infra.Blackhole; + +import io.reactivex.rxjava3.processors.PublishProcessor; +import io.reactivex.rxjava3.subjects.PublishSubject; + +@BenchmarkMode(Mode.Throughput) +@Warmup(iterations = 5) +@Measurement(iterations = 5, time = 5, timeUnit = TimeUnit.SECONDS) +@OutputTimeUnit(TimeUnit.SECONDS) +@Fork(value = 1) +@State(Scope.Thread) +public class PublishProcessorPerf { + + PublishProcessor<Integer> unbounded; + + PublishProcessor<Integer> bounded; + + PublishSubject<Integer> subject; + + @Setup + public void setup(Blackhole bh) { + unbounded = PublishProcessor.create(); + unbounded.subscribe(new PerfConsumer(bh)); + + bounded = PublishProcessor.create(); + bounded.subscribe(new PerfBoundedSubscriber(bh, 1000 * 1000)); + + subject = PublishSubject.create(); + subject.subscribe(new PerfConsumer(bh)); + } + + @Benchmark + public void unbounded1() { + unbounded.onNext(1); + } + + @Benchmark + public void unbounded1k() { + for (int i = 0; i < 1000; i++) { + unbounded.onNext(1); + } + } + + @Benchmark + public void unbounded1m() { + for (int i = 0; i < 1000000; i++) { + unbounded.onNext(1); + } + } + + @Benchmark + public void bounded1() { + bounded.onNext(1); + } + + @Benchmark + public void bounded1k() { + for (int i = 0; i < 1000; i++) { + bounded.onNext(1); + } + } + + @Benchmark + public void bounded1m() { + for (int i = 0; i < 1000000; i++) { + bounded.onNext(1); + } + } + + @Benchmark + public void subject1() { + subject.onNext(1); + } + + @Benchmark + public void subject1k() { + for (int i = 0; i < 1000; i++) { + subject.onNext(1); + } + } + + @Benchmark + public void subject1m() { + for (int i = 0; i < 1000000; i++) { + subject.onNext(1); + } + } +} diff --git a/src/jmh/java/io/reactivex/rxjava3/core/RangePerf.java b/src/jmh/java/io/reactivex/rxjava3/core/RangePerf.java new file mode 100644 index 0000000000..0773cd4f72 --- /dev/null +++ b/src/jmh/java/io/reactivex/rxjava3/core/RangePerf.java @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.core; + +import java.util.concurrent.TimeUnit; + +import org.openjdk.jmh.annotations.*; +import org.openjdk.jmh.infra.Blackhole; + +import io.reactivex.rxjava3.internal.schedulers.SingleScheduler; +import io.reactivex.rxjava3.schedulers.Schedulers; + +@BenchmarkMode(Mode.Throughput) +@Warmup(iterations = 5) +@Measurement(iterations = 5, time = 5, timeUnit = TimeUnit.SECONDS) +@OutputTimeUnit(TimeUnit.SECONDS) +@Fork(value = 1) +@State(Scope.Thread) +public class RangePerf { + @Param({ "1", "1000", "1000000" }) + public int times; + + Flowable<Integer> range; + + Flowable<Integer> rangeAsync; + + Flowable<Integer> rangeAsyncPipeline; + + @Setup + public void setup() { + range = Flowable.range(1, times); + + rangeAsync = range.observeOn(Schedulers.single()); + + rangeAsyncPipeline = range.subscribeOn(new SingleScheduler()).observeOn(Schedulers.single()); + } + + @Benchmark + public Object rangeSync(Blackhole bh) { + PerfSubscriber lo = new PerfSubscriber(bh); + + range.subscribe(lo); + + return lo; + } + +// @Benchmark + public void rangeAsync(Blackhole bh) throws Exception { + PerfSubscriber lo = new PerfSubscriber(bh); + + rangeAsync.subscribe(lo); + + if (times == 1) { + while (lo.latch.getCount() != 0) { } + } else { + lo.latch.await(); + } + } + +// @Benchmark + public void rangePipeline(Blackhole bh) throws Exception { + PerfSubscriber lo = new PerfSubscriber(bh); + + rangeAsyncPipeline.subscribe(lo); + + if (times == 1) { + while (lo.latch.getCount() != 0) { } + } else { + lo.latch.await(); + } + } + +} diff --git a/src/jmh/java/io/reactivex/rxjava3/core/ReducePerf.java b/src/jmh/java/io/reactivex/rxjava3/core/ReducePerf.java new file mode 100644 index 0000000000..3d2bb476c8 --- /dev/null +++ b/src/jmh/java/io/reactivex/rxjava3/core/ReducePerf.java @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.core; + +import java.util.Arrays; +import java.util.concurrent.TimeUnit; + +import org.openjdk.jmh.annotations.*; +import org.openjdk.jmh.infra.Blackhole; + +import io.reactivex.rxjava3.functions.BiFunction; + +@BenchmarkMode(Mode.Throughput) +@Warmup(iterations = 5) +@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) +@OutputTimeUnit(TimeUnit.SECONDS) +@Fork(value = 1, jvmArgsAppend = { "-XX:MaxInlineLevel=20" }) +@State(Scope.Thread) +public class ReducePerf implements BiFunction<Integer, Integer, Integer> { + @Param({ "1", "1000", "1000000" }) + public int times; + + Single<Integer> obsSingle; + + Single<Integer> flowSingle; + + Maybe<Integer> obsMaybe; + + Maybe<Integer> flowMaybe; + + @Override + public Integer apply(Integer t1, Integer t2) { + return t1 + t2; + } + + @Setup + public void setup() { + Integer[] array = new Integer[times]; + Arrays.fill(array, 777); + + obsSingle = Observable.fromArray(array).reduce(0, this); + + obsMaybe = Observable.fromArray(array).reduce(this); + + flowSingle = Flowable.fromArray(array).reduce(0, this); + + flowMaybe = Flowable.fromArray(array).reduce(this); + } + + @Benchmark + public void obsSingle(Blackhole bh) { + obsSingle.subscribe(new PerfConsumer(bh)); + } + + @Benchmark + public void flowSingle(Blackhole bh) { + flowSingle.subscribe(new PerfConsumer(bh)); + } + + @Benchmark + public void obsMaybe(Blackhole bh) { + obsMaybe.subscribe(new PerfConsumer(bh)); + } + + @Benchmark + public void flowMaybe(Blackhole bh) { + flowMaybe.subscribe(new PerfConsumer(bh)); + } +} diff --git a/src/jmh/java/io/reactivex/rxjava3/core/RxVsStreamPerf.java b/src/jmh/java/io/reactivex/rxjava3/core/RxVsStreamPerf.java new file mode 100644 index 0000000000..8861a6cdf5 --- /dev/null +++ b/src/jmh/java/io/reactivex/rxjava3/core/RxVsStreamPerf.java @@ -0,0 +1,116 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.core; + +import java.util.List; +import java.util.concurrent.TimeUnit; + +import org.openjdk.jmh.annotations.*; +import org.openjdk.jmh.infra.Blackhole; +import org.reactivestreams.Publisher; + +import io.reactivex.rxjava3.functions.Function; + +@BenchmarkMode(Mode.Throughput) +@Warmup(iterations = 5) +@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) +@OutputTimeUnit(TimeUnit.SECONDS) +@Fork(value = 1) +@State(Scope.Thread) +public class RxVsStreamPerf { + @Param({ "1", "1000", "1000000" }) + public int times; + + Flowable<Integer> range; + + Observable<Integer> rangeObservable; + + Flowable<Integer> rangeFlatMap; + + Observable<Integer> rangeObservableFlatMap; + + Flowable<Integer> rangeFlatMapJust; + + Observable<Integer> rangeObservableFlatMapJust; + + List<Integer> values; + + @Setup + public void setup() { + range = Flowable.range(1, times); + + rangeFlatMapJust = range.flatMap(new Function<Integer, Publisher<Integer>>() { + @Override + public Publisher<Integer> apply(Integer v) { + return Flowable.just(v); + } + }); + + rangeFlatMap = range.flatMap(new Function<Integer, Publisher<Integer>>() { + @Override + public Publisher<Integer> apply(Integer v) { + return Flowable.range(v, 2); + } + }); + + rangeObservable = Observable.range(1, times); + + rangeObservableFlatMapJust = rangeObservable.flatMap(new Function<Integer, Observable<Integer>>() { + @Override + public Observable<Integer> apply(Integer v) { + return Observable.just(v); + } + }); + + rangeObservableFlatMap = rangeObservable.flatMap(new Function<Integer, Observable<Integer>>() { + @Override + public Observable<Integer> apply(Integer v) { + return Observable.range(v, 2); + } + }); + + values = range.toList().blockingGet(); + } + + @Benchmark + public void range(Blackhole bh) { + range.subscribe(new PerfSubscriber(bh)); + } + + @Benchmark + public void rangeObservable(Blackhole bh) { + rangeObservable.subscribe(new PerfObserver(bh)); + } + + @Benchmark + public void rangeFlatMap(Blackhole bh) { + rangeFlatMap.subscribe(new PerfSubscriber(bh)); + } + + @Benchmark + public void rangeObservableFlatMap(Blackhole bh) { + rangeObservableFlatMap.subscribe(new PerfObserver(bh)); + } + + @Benchmark + public void rangeFlatMapJust(Blackhole bh) { + rangeFlatMapJust.subscribe(new PerfSubscriber(bh)); + } + + @Benchmark + public void rangeObservableFlatMapJust(Blackhole bh) { + rangeObservableFlatMapJust.subscribe(new PerfObserver(bh)); + } + +} diff --git a/src/jmh/java/io/reactivex/rxjava3/core/StrictPerf.java b/src/jmh/java/io/reactivex/rxjava3/core/StrictPerf.java new file mode 100644 index 0000000000..23315ef617 --- /dev/null +++ b/src/jmh/java/io/reactivex/rxjava3/core/StrictPerf.java @@ -0,0 +1,119 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.core; + +import java.util.Arrays; +import java.util.concurrent.TimeUnit; + +import org.openjdk.jmh.annotations.*; +import org.openjdk.jmh.infra.Blackhole; +import org.reactivestreams.*; + +@BenchmarkMode(Mode.Throughput) +@Warmup(iterations = 5) +@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) +@OutputTimeUnit(TimeUnit.SECONDS) +@Fork(value = 1) +@State(Scope.Thread) +public class StrictPerf { + @Param({ "1", "10", "100", "1000", "10000", "100000", "1000000" }) + public int count; + + @Param({ "1", "10", "100", "1000", "10000" }) + public int cpu; + + Flowable<Integer> source; + + @Setup + public void setup() { + Integer[] array = new Integer[count]; + Arrays.fill(array, 777); + + source = Flowable.fromArray(array); + } + + @Benchmark + public void internal(Blackhole bh) { + source.subscribe(new InternalConsumer(bh, cpu)); + } + + @Benchmark + public void external(Blackhole bh) { + source.subscribe(new ExternalConsumer(bh, cpu)); + } + + static final class InternalConsumer implements FlowableSubscriber<Object> { + final Blackhole bh; + + final int cycles; + + InternalConsumer(Blackhole bh, int cycles) { + this.bh = bh; + this.cycles = cycles; + } + + @Override + public void onNext(Object t) { + bh.consume(t); + Blackhole.consumeCPU(cycles); + } + + @Override + public void onError(Throwable t) { + bh.consume(t); + } + + @Override + public void onComplete() { + bh.consume(true); + } + + @Override + public void onSubscribe(Subscription s) { + s.request(Long.MAX_VALUE); + } + } + + static final class ExternalConsumer implements Subscriber<Object> { + final Blackhole bh; + + final int cycles; + + ExternalConsumer(Blackhole bh, int cycles) { + this.bh = bh; + this.cycles = cycles; + } + + @Override + public void onNext(Object t) { + bh.consume(t); + Blackhole.consumeCPU(cycles); + } + + @Override + public void onError(Throwable t) { + bh.consume(t); + } + + @Override + public void onComplete() { + bh.consume(true); + } + + @Override + public void onSubscribe(Subscription s) { + s.request(Long.MAX_VALUE); + } + } +} diff --git a/src/jmh/java/io/reactivex/rxjava3/core/TakeUntilPerf.java b/src/jmh/java/io/reactivex/rxjava3/core/TakeUntilPerf.java new file mode 100644 index 0000000000..a6a3949605 --- /dev/null +++ b/src/jmh/java/io/reactivex/rxjava3/core/TakeUntilPerf.java @@ -0,0 +1,95 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.core; + +import java.util.concurrent.*; + +import org.openjdk.jmh.annotations.*; + +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.schedulers.Schedulers; + +@BenchmarkMode(Mode.Throughput) +@Warmup(iterations = 5) +@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) +@OutputTimeUnit(TimeUnit.SECONDS) +@Fork(value = 1) +@State(Scope.Thread) +@AuxCounters +public class TakeUntilPerf implements Consumer<Integer> { + + public volatile int items; + + static final int count = 10000; + + Flowable<Integer> flowable; + + Observable<Integer> observable; + + @Override + public void accept(Integer t) { + items++; + } + + @Setup + public void setup() { + + flowable = Flowable.range(1, 1000 * 1000).takeUntil(Flowable.fromCallable(new Callable<Object>() { + @Override + public Object call() { + int c = count; + while (items < c) { } + return 1; + } + }).subscribeOn(Schedulers.single())); + + observable = Observable.range(1, 1000 * 1000).takeUntil(Observable.fromCallable(new Callable<Object>() { + @Override + public Object call() { + int c = count; + while (items < c) { } + return 1; + } + }).subscribeOn(Schedulers.single())); + } + + @Benchmark + public void flowable() { + final CountDownLatch cdl = new CountDownLatch(1); + + flowable.subscribe(this, Functions.emptyConsumer(), new Action() { + @Override + public void run() { + cdl.countDown(); + } + }); + + while (cdl.getCount() != 0) { } + } + + @Benchmark + public void observable() { + final CountDownLatch cdl = new CountDownLatch(1); + + observable.subscribe(this, Functions.emptyConsumer(), new Action() { + @Override + public void run() { + cdl.countDown(); + } + }); + + while (cdl.getCount() != 0) { } + } +} diff --git a/src/jmh/java/io/reactivex/rxjava3/core/ToFlowablePerf.java b/src/jmh/java/io/reactivex/rxjava3/core/ToFlowablePerf.java new file mode 100644 index 0000000000..7b7f0dc36d --- /dev/null +++ b/src/jmh/java/io/reactivex/rxjava3/core/ToFlowablePerf.java @@ -0,0 +1,111 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.core; + +import java.util.Arrays; +import java.util.concurrent.TimeUnit; + +import org.openjdk.jmh.annotations.*; +import org.reactivestreams.Publisher; + +import io.reactivex.rxjava3.functions.*; + +@BenchmarkMode(Mode.Throughput) +@Warmup(iterations = 5) +@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) +@OutputTimeUnit(TimeUnit.SECONDS) +@Fork(value = 1) +@State(Scope.Thread) +public class ToFlowablePerf { + @Param({ "1", "1000", "1000000" }) + public int times; + + Maybe<Integer> flowable; + + Flowable<Integer> flowableInner; + + Observable<Integer> observable; + + Observable<Integer> observableInner; + + @Setup + public void setup() { + Integer[] array = new Integer[times]; + Arrays.fill(array, 777); + + Flowable<Integer> source = Flowable.fromArray(array); + + final BiFunction<Integer, Integer, Integer> second = new BiFunction<Integer, Integer, Integer>() { + @Override + public Integer apply(Integer a, Integer b) { + return b; + } + }; + + flowable = source.reduce(second); + + flowableInner = source.concatMap(new Function<Integer, Publisher<Integer>>() { + @Override + public Publisher<Integer> apply(Integer v) { + return Flowable.range(1, 50).reduce(second).toFlowable(); + } + }); + + Observable<Integer> sourceObs = Observable.fromArray(array); + + observable = sourceObs.reduce(second).toObservable(); + + observableInner = sourceObs.concatMap(new Function<Integer, Observable<Integer>>() { + @Override + public Observable<Integer> apply(Integer v) { + return Observable.range(1, 50).reduce(second).toObservable(); + } + }); + } + + @Benchmark + public Object flowable() { + return flowable.blockingGet(); + } + + @Benchmark + public Object flowableInner() { + return flowableInner.blockingLast(); + } + + @Benchmark + public Object observable() { + return observable.blockingLast(); + } + + @Benchmark + public Object observableInner() { + return observableInner.blockingLast(); + } + + static volatile Object o; + + public static void main(String[] args) { + ToFlowablePerf p = new ToFlowablePerf(); + p.times = 1000000; + p.setup(); + + for (int j = 0; j < 15; j++) { + for (int i = 0; i < 600; i++) { + o = p.flowable(); + } + System.out.println("--- " + j); + } + } +} diff --git a/src/jmh/java/io/reactivex/rxjava3/core/XMapYPerf.java b/src/jmh/java/io/reactivex/rxjava3/core/XMapYPerf.java new file mode 100644 index 0000000000..b95d76ffe4 --- /dev/null +++ b/src/jmh/java/io/reactivex/rxjava3/core/XMapYPerf.java @@ -0,0 +1,482 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.core; + +import java.util.*; +import java.util.concurrent.TimeUnit; + +import org.openjdk.jmh.annotations.*; +import org.openjdk.jmh.infra.Blackhole; +import org.reactivestreams.Publisher; + +import io.reactivex.rxjava3.functions.Function; + +@BenchmarkMode(Mode.Throughput) +@Warmup(iterations = 5) +@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) +@OutputTimeUnit(TimeUnit.SECONDS) +@Fork(value = 1) +@State(Scope.Thread) +public class XMapYPerf { + @Param({ "1", "10", "100", "1000", "10000", "100000", "1000000" }) + public int times; + + Flowable<Integer> flowFlatMapIterable1; + + Flowable<Integer> flowFlatMapIterable0; + + Flowable<Integer> flowFlatMapFlowable0; + + Flowable<Integer> flowFlatMapFlowable1; + + Flowable<Integer> flowFlatMapSingle1; + + Flowable<Integer> flowFlatMapMaybe1; + + Flowable<Integer> flowFlatMapMaybe0; + + Completable flowFlatMapCompletable0; + + // oooooooooooooooooooooooooooooooooooooooooo + + Flowable<Integer> flowFlatMapSingleAsFlow1; + + Flowable<Integer> flowFlatMapMaybeAsFlow1; + + Flowable<Integer> flowFlatMapMaybeAsFlow0; + + Flowable<Integer> flowFlatMapCompletableAsFlow0; + + Flowable<Integer> flowFlatMapIterableAsFlow1; + + Flowable<Integer> flowFlatMapIterableAsFlow0; + + // ----------------------------------------------------------------- + + Observable<Integer> obsFlatMapIterable0; + + Observable<Integer> obsFlatMapIterable1; + + Observable<Integer> obsFlatMapObservable0; + + Observable<Integer> obsFlatMapObservable1; + + Observable<Integer> obsFlatMapSingle1; + + Observable<Integer> obsFlatMapMaybe1; + + Observable<Integer> obsFlatMapMaybe0; + + Completable obsFlatMapCompletable0; + + // oooooooooooooooooooooooooooooooooooooooooo + + Observable<Integer> obsFlatMapSingleAsObs1; + + Observable<Integer> obsFlatMapMaybeAsObs1; + + Observable<Integer> obsFlatMapMaybeAsObs0; + + Observable<Integer> obsFlatMapCompletableAsObs0; + + Observable<Integer> obsFlatMapIterableAsObs1; + + Observable<Integer> obsFlatMapIterableAsObs0; + + @Setup + public void setup() { + Integer[] values = new Integer[times]; + Arrays.fill(values, 777); + + Flowable<Integer> fsource = Flowable.fromArray(values); + + flowFlatMapFlowable1 = fsource.flatMap(new Function<Integer, Publisher<Integer>>() { + @Override + public Publisher<Integer> apply(Integer v) { + return Flowable.just(v); + } + }); + + flowFlatMapFlowable0 = fsource.flatMap(new Function<Integer, Publisher<Integer>>() { + @Override + public Publisher<Integer> apply(Integer v) { + return Flowable.empty(); + } + }); + + flowFlatMapSingle1 = fsource.flatMapSingle(new Function<Integer, SingleSource<Integer>>() { + @Override + public SingleSource<Integer> apply(Integer v) { + return Single.just(v); + } + }); + + flowFlatMapMaybe1 = fsource.flatMapMaybe(new Function<Integer, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Integer v) { + return Maybe.just(v); + } + }); + + flowFlatMapMaybe0 = fsource.flatMapMaybe(new Function<Integer, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Integer v) { + return Maybe.empty(); + } + }); + + flowFlatMapCompletable0 = fsource.flatMapCompletable(new Function<Integer, CompletableSource>() { + @Override + public CompletableSource apply(Integer v) { + return Completable.complete(); + } + }); + + flowFlatMapIterable1 = fsource.flatMapIterable(new Function<Integer, Iterable<Integer>>() { + @Override + public Iterable<Integer> apply(Integer v) { + return Collections.singletonList(v); + } + }); + + flowFlatMapIterable0 = fsource.flatMapIterable(new Function<Integer, Iterable<Integer>>() { + @Override + public Iterable<Integer> apply(Integer v) { + return Collections.emptyList(); + } + }); + + flowFlatMapSingle1 = fsource.flatMapSingle(new Function<Integer, SingleSource<Integer>>() { + @Override + public SingleSource<Integer> apply(Integer v) { + return Single.just(v); + } + }); + + flowFlatMapMaybe1 = fsource.flatMapMaybe(new Function<Integer, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Integer v) { + return Maybe.just(v); + } + }); + + flowFlatMapMaybe0 = fsource.flatMapMaybe(new Function<Integer, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Integer v) { + return Maybe.empty(); + } + }); + + flowFlatMapCompletable0 = fsource.flatMapCompletable(new Function<Integer, CompletableSource>() { + @Override + public CompletableSource apply(Integer v) { + return Completable.complete(); + } + }); + + // ooooooooooooooooooooooooo + + flowFlatMapSingleAsFlow1 = fsource.flatMap(new Function<Integer, Publisher<Integer>>() { + @Override + public Publisher<Integer> apply(Integer v) { + return Single.just(v).toFlowable(); + } + }); + + flowFlatMapMaybeAsFlow1 = fsource.flatMap(new Function<Integer, Publisher<Integer>>() { + @Override + public Publisher<Integer> apply(Integer v) { + return Maybe.just(v).toFlowable(); + } + }); + + flowFlatMapMaybeAsFlow0 = fsource.flatMap(new Function<Integer, Publisher<Integer>>() { + @Override + public Publisher<Integer> apply(Integer v) { + return Maybe.<Integer>empty().toFlowable(); + } + }); + + flowFlatMapCompletableAsFlow0 = fsource.flatMap(new Function<Integer, Publisher<Integer>>() { + @Override + public Publisher<Integer> apply(Integer v) { + return Completable.complete().toFlowable(); + } + }); + + flowFlatMapIterableAsFlow1 = fsource.flatMap(new Function<Integer, Publisher<Integer>>() { + @Override + public Publisher<Integer> apply(Integer v) { + return Flowable.fromIterable(Collections.singletonList(v)); + } + }); + + flowFlatMapIterableAsFlow0 = fsource.flatMap(new Function<Integer, Publisher<Integer>>() { + @Override + public Publisher<Integer> apply(Integer v) { + return Flowable.fromIterable(Collections.emptyList()); + } + }); + + // ------------------------------------------------------------------- + + Observable<Integer> osource = Observable.fromArray(values); + + obsFlatMapObservable1 = osource.flatMap(new Function<Integer, Observable<Integer>>() { + @Override + public Observable<Integer> apply(Integer v) { + return Observable.just(v); + } + }); + + obsFlatMapObservable0 = osource.flatMap(new Function<Integer, Observable<Integer>>() { + @Override + public Observable<Integer> apply(Integer v) { + return Observable.empty(); + } + }); + + obsFlatMapSingle1 = osource.flatMapSingle(new Function<Integer, SingleSource<Integer>>() { + @Override + public SingleSource<Integer> apply(Integer v) { + return Single.just(v); + } + }); + + obsFlatMapMaybe1 = osource.flatMapMaybe(new Function<Integer, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Integer v) { + return Maybe.just(v); + } + }); + + obsFlatMapMaybe0 = osource.flatMapMaybe(new Function<Integer, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Integer v) { + return Maybe.empty(); + } + }); + + obsFlatMapCompletable0 = osource.flatMapCompletable(new Function<Integer, CompletableSource>() { + @Override + public CompletableSource apply(Integer v) { + return Completable.complete(); + } + }); + + obsFlatMapIterable1 = osource.flatMapIterable(new Function<Integer, Iterable<Integer>>() { + @Override + public Iterable<Integer> apply(Integer v) { + return Collections.singletonList(v); + } + }); + + obsFlatMapIterable0 = osource.flatMapIterable(new Function<Integer, Iterable<Integer>>() { + @Override + public Iterable<Integer> apply(Integer v) { + return Collections.emptyList(); + } + }); + + // ooooooooooooooooooooooooo + + obsFlatMapSingleAsObs1 = osource.flatMap(new Function<Integer, Observable<Integer>>() { + @Override + public Observable<Integer> apply(Integer v) { + return Single.just(v).toObservable(); + } + }); + + obsFlatMapMaybeAsObs1 = osource.flatMap(new Function<Integer, Observable<Integer>>() { + @Override + public Observable<Integer> apply(Integer v) { + return Maybe.just(v).toObservable(); + } + }); + + obsFlatMapMaybeAsObs0 = osource.flatMap(new Function<Integer, Observable<Integer>>() { + @Override + public Observable<Integer> apply(Integer v) { + return Maybe.<Integer>empty().toObservable(); + } + }); + + obsFlatMapCompletableAsObs0 = osource.flatMap(new Function<Integer, Observable<Integer>>() { + @Override + public Observable<Integer> apply(Integer v) { + return Completable.complete().toObservable(); + } + }); + + obsFlatMapIterableAsObs1 = osource.flatMap(new Function<Integer, Observable<Integer>>() { + @Override + public Observable<Integer> apply(Integer v) { + return Observable.fromIterable(Collections.singletonList(v)); + } + }); + + obsFlatMapIterableAsObs0 = osource.flatMap(new Function<Integer, Observable<Integer>>() { + @Override + public Observable<Integer> apply(Integer v) { + return Observable.fromIterable(Collections.emptyList()); + } + }); + } + + @Benchmark + public void flowFlatMapIterable1(Blackhole bh) { + flowFlatMapIterable1.subscribe(new PerfConsumer(bh)); + } + + @Benchmark + public void flowFlatMapIterable0(Blackhole bh) { + flowFlatMapIterable0.subscribe(new PerfConsumer(bh)); + } + + @Benchmark + public void flowFlatMapFlowable0(Blackhole bh) { + flowFlatMapFlowable0.subscribe(new PerfConsumer(bh)); + } + + @Benchmark + public void flowFlatMapFlowable1(Blackhole bh) { + flowFlatMapFlowable1.subscribe(new PerfConsumer(bh)); + } + + @Benchmark + public void flowFlatMapSingle1(Blackhole bh) { + flowFlatMapSingle1.subscribe(new PerfConsumer(bh)); + } + + @Benchmark + public void flowFlatMapMaybe1(Blackhole bh) { + flowFlatMapMaybe1.subscribe(new PerfConsumer(bh)); + } + + @Benchmark + public void flowFlatMapMaybe0(Blackhole bh) { + flowFlatMapMaybe0.subscribe(new PerfConsumer(bh)); + } + + @Benchmark + public void flowFlatMapCompletable0(Blackhole bh) { + flowFlatMapCompletable0.subscribe(new PerfConsumer(bh)); + } + + // oooooooooooooooooooooooooooooooo + + @Benchmark + public void flowFlatMapIterableAsFlow1(Blackhole bh) { + flowFlatMapIterableAsFlow1.subscribe(new PerfConsumer(bh)); + } + + @Benchmark + public void flowFlatMapIterableAsFlow0(Blackhole bh) { + flowFlatMapIterableAsFlow0.subscribe(new PerfConsumer(bh)); + } + + @Benchmark + public void flowFlatMapSingleAsFlow1(Blackhole bh) { + flowFlatMapSingleAsFlow1.subscribe(new PerfConsumer(bh)); + } + + @Benchmark + public void flowFlatMapMaybeAsFlow1(Blackhole bh) { + flowFlatMapMaybeAsFlow1.subscribe(new PerfConsumer(bh)); + } + + @Benchmark + public void flowFlatMapMaybeAsFlow0(Blackhole bh) { + flowFlatMapMaybeAsFlow0.subscribe(new PerfConsumer(bh)); + } + + @Benchmark + public void flowFlatMapCompletableAsFlow0(Blackhole bh) { + flowFlatMapCompletableAsFlow0.subscribe(new PerfConsumer(bh)); + } + + // -------------------------------------------------------------------------------- + + @Benchmark + public void obsFlatMapIterable0(Blackhole bh) { + obsFlatMapIterable0.subscribe(new PerfConsumer(bh)); + } + + @Benchmark + public void obsFlatMapIterable1(Blackhole bh) { + obsFlatMapIterable1.subscribe(new PerfConsumer(bh)); + } + + @Benchmark + public void obsFlatMapObservable0(Blackhole bh) { + obsFlatMapObservable0.subscribe(new PerfConsumer(bh)); + } + + @Benchmark + public void obsFlatMapObservable1(Blackhole bh) { + obsFlatMapObservable1.subscribe(new PerfConsumer(bh)); + } + + @Benchmark + public void obsFlatMapSingle1(Blackhole bh) { + obsFlatMapSingle1.subscribe(new PerfConsumer(bh)); + } + + @Benchmark + public void obsFlatMapMaybe1(Blackhole bh) { + obsFlatMapMaybe1.subscribe(new PerfConsumer(bh)); + } + + @Benchmark + public void obsFlatMapMaybe0(Blackhole bh) { + obsFlatMapMaybe0.subscribe(new PerfConsumer(bh)); + } + + @Benchmark + public void obsFlatMapCompletable0(Blackhole bh) { + obsFlatMapCompletable0.subscribe(new PerfConsumer(bh)); + } + + // oooooooooooooooooooooooooooooooo + + @Benchmark + public void obsFlatMapIterableAsObs1(Blackhole bh) { + obsFlatMapIterableAsObs1.subscribe(new PerfConsumer(bh)); + } + + @Benchmark + public void obsFlatMapIterableAsObs0(Blackhole bh) { + obsFlatMapIterableAsObs0.subscribe(new PerfConsumer(bh)); + } + + @Benchmark + public void obsFlatMapSingleAsObs1(Blackhole bh) { + obsFlatMapSingleAsObs1.subscribe(new PerfConsumer(bh)); + } + + @Benchmark + public void obsFlatMapMaybeAsObs1(Blackhole bh) { + obsFlatMapMaybeAsObs1.subscribe(new PerfConsumer(bh)); + } + + @Benchmark + public void obsFlatMapMaybeAsObs0(Blackhole bh) { + obsFlatMapMaybeAsObs0.subscribe(new PerfConsumer(bh)); + } + + @Benchmark + public void obsFlatMapCompletableAsObs0(Blackhole bh) { + obsFlatMapCompletableAsObs0.subscribe(new PerfConsumer(bh)); + } +} diff --git a/src/jmh/java/io/reactivex/rxjava3/parallel/ParallelPerf.java b/src/jmh/java/io/reactivex/rxjava3/parallel/ParallelPerf.java new file mode 100644 index 0000000000..c6fd16ffb6 --- /dev/null +++ b/src/jmh/java/io/reactivex/rxjava3/parallel/ParallelPerf.java @@ -0,0 +1,112 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.parallel; + +import java.util.Arrays; +import java.util.concurrent.TimeUnit; + +import org.openjdk.jmh.annotations.*; +import org.openjdk.jmh.infra.Blackhole; +import org.reactivestreams.Publisher; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.flowables.GroupedFlowable; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.schedulers.Schedulers; + +@BenchmarkMode(Mode.Throughput) +@Warmup(iterations = 5) +@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) +@Fork(value = 1, jvmArgsAppend = { "-XX:MaxInlineLevel=20" }) +@OutputTimeUnit(TimeUnit.SECONDS) +@State(Scope.Thread) +public class ParallelPerf implements Function<Integer, Integer> { + + @Param({"10000"}) + public int count; + + @Param({"1", "10", "100", "1000", "10000"}) + public int compute; + + @Param({"1", "2", "3", "4"}) + public int parallelism; + + Flowable<Integer> flatMap; + + Flowable<Integer> groupBy; + + Flowable<Integer> parallel; + + @Override + public Integer apply(Integer t) { + Blackhole.consumeCPU(compute); + return t; + } + + @Setup + public void setup() { + + final int cpu = parallelism; + + Integer[] ints = new Integer[count]; + Arrays.fill(ints, 777); + + Flowable<Integer> source = Flowable.fromArray(ints); + + flatMap = source.flatMap(new Function<Integer, Publisher<Integer>>() { + @Override + public Publisher<Integer> apply(Integer v) { + return Flowable.just(v).subscribeOn(Schedulers.computation()) + .map(ParallelPerf.this); + } + }, cpu); + + groupBy = source.groupBy(new Function<Integer, Integer>() { + int i; + @Override + public Integer apply(Integer v) { + return (i++) % cpu; + } + }) + .flatMap(new Function<GroupedFlowable<Integer, Integer>, Publisher<Integer>>() { + @Override + public Publisher<Integer> apply(GroupedFlowable<Integer, Integer> g) { + return g.observeOn(Schedulers.computation()).map(ParallelPerf.this); + } + }); + + parallel = source.parallel(cpu).runOn(Schedulers.computation()).map(this).sequential(); + } + + void subscribe(Flowable<Integer> f, Blackhole bh) { + PerfAsyncConsumer consumer = new PerfAsyncConsumer(bh); + f.subscribe(consumer); + consumer.await(count); + } + + @Benchmark + public void flatMap(Blackhole bh) { + subscribe(flatMap, bh); + } + + @Benchmark + public void groupBy(Blackhole bh) { + subscribe(groupBy, bh); + } + + @Benchmark + public void parallel(Blackhole bh) { + subscribe(parallel, bh); + } +} diff --git a/src/jmh/java/io/reactivex/rxjava3/xmapz/FlowableConcatMapCompletablePerf.java b/src/jmh/java/io/reactivex/rxjava3/xmapz/FlowableConcatMapCompletablePerf.java new file mode 100644 index 0000000000..f1124f7a81 --- /dev/null +++ b/src/jmh/java/io/reactivex/rxjava3/xmapz/FlowableConcatMapCompletablePerf.java @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.xmapz; + +import java.util.Arrays; +import java.util.concurrent.TimeUnit; + +import org.openjdk.jmh.annotations.*; +import org.openjdk.jmh.infra.Blackhole; +import org.reactivestreams.Publisher; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.functions.Function; + +@BenchmarkMode(Mode.Throughput) +@Warmup(iterations = 5) +@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) +@OutputTimeUnit(TimeUnit.SECONDS) +@Fork(value = 1) +@State(Scope.Thread) +public class FlowableConcatMapCompletablePerf { + @Param({ "1", "10", "100", "1000", "10000", "100000", "1000000" }) + public int count; + + Flowable<Integer> flowableConvert; + + Completable flowableDedicated; + + Flowable<Integer> flowablePlain; + + @Setup + public void setup() { + Integer[] sourceArray = new Integer[count]; + Arrays.fill(sourceArray, 777); + + Flowable<Integer> source = Flowable.fromArray(sourceArray); + + flowablePlain = source.concatMap(new Function<Integer, Publisher<? extends Integer>>() { + @Override + public Publisher<? extends Integer> apply(Integer v) { + return Flowable.empty(); + } + }); + + flowableConvert = source.concatMap(new Function<Integer, Publisher<? extends Integer>>() { + @Override + public Publisher<? extends Integer> apply(Integer v) { + return Completable.complete().toFlowable(); + } + }); + + flowableDedicated = source.concatMapCompletable(new Function<Integer, Completable>() { + @Override + public Completable apply(Integer v) { + return Completable.complete(); + } + }); + } + + @Benchmark + public Object flowablePlain(Blackhole bh) { + return flowablePlain.subscribeWith(new PerfConsumer(bh)); + } + + @Benchmark + public Object flowableConvert(Blackhole bh) { + return flowableConvert.subscribeWith(new PerfConsumer(bh)); + } + + @Benchmark + public Object flowableDedicated(Blackhole bh) { + return flowableDedicated.subscribeWith(new PerfConsumer(bh)); + } +} diff --git a/src/jmh/java/io/reactivex/rxjava3/xmapz/FlowableConcatMapMaybeEmptyPerf.java b/src/jmh/java/io/reactivex/rxjava3/xmapz/FlowableConcatMapMaybeEmptyPerf.java new file mode 100644 index 0000000000..7eabfcfbde --- /dev/null +++ b/src/jmh/java/io/reactivex/rxjava3/xmapz/FlowableConcatMapMaybeEmptyPerf.java @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.xmapz; + +import java.util.Arrays; +import java.util.concurrent.TimeUnit; + +import org.openjdk.jmh.annotations.*; +import org.openjdk.jmh.infra.Blackhole; +import org.reactivestreams.Publisher; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.functions.Function; + +@BenchmarkMode(Mode.Throughput) +@Warmup(iterations = 5) +@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) +@OutputTimeUnit(TimeUnit.SECONDS) +@Fork(value = 1) +@State(Scope.Thread) +public class FlowableConcatMapMaybeEmptyPerf { + @Param({ "1", "10", "100", "1000", "10000", "100000", "1000000" }) + public int count; + + Flowable<Integer> concatMapToFlowableEmpty; + + Flowable<Integer> flowableDedicated; + + Flowable<Integer> flowablePlain; + + @Setup + public void setup() { + Integer[] sourceArray = new Integer[count]; + Arrays.fill(sourceArray, 777); + + Flowable<Integer> source = Flowable.fromArray(sourceArray); + + flowablePlain = source.concatMap(new Function<Integer, Publisher<? extends Integer>>() { + @Override + public Publisher<? extends Integer> apply(Integer v) { + return Flowable.empty(); + } + }); + + concatMapToFlowableEmpty = source.concatMap(new Function<Integer, Publisher<? extends Integer>>() { + @Override + public Publisher<? extends Integer> apply(Integer v) { + return Maybe.<Integer>empty().toFlowable(); + } + }); + + flowableDedicated = source.concatMapMaybe(new Function<Integer, Maybe<? extends Integer>>() { + @Override + public Maybe<? extends Integer> apply(Integer v) { + return Maybe.empty(); + } + }); + } + + @Benchmark + public Object flowablePlain(Blackhole bh) { + return flowablePlain.subscribeWith(new PerfConsumer(bh)); + } + + @Benchmark + public Object flowableConvert(Blackhole bh) { + return concatMapToFlowableEmpty.subscribeWith(new PerfConsumer(bh)); + } + + @Benchmark + public Object flowableDedicated(Blackhole bh) { + return flowableDedicated.subscribeWith(new PerfConsumer(bh)); + } +} diff --git a/src/jmh/java/io/reactivex/rxjava3/xmapz/FlowableConcatMapMaybePerf.java b/src/jmh/java/io/reactivex/rxjava3/xmapz/FlowableConcatMapMaybePerf.java new file mode 100644 index 0000000000..87ee5a07e4 --- /dev/null +++ b/src/jmh/java/io/reactivex/rxjava3/xmapz/FlowableConcatMapMaybePerf.java @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.xmapz; + +import java.util.Arrays; +import java.util.concurrent.TimeUnit; + +import org.openjdk.jmh.annotations.*; +import org.openjdk.jmh.infra.Blackhole; +import org.reactivestreams.Publisher; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.functions.Function; + +@BenchmarkMode(Mode.Throughput) +@Warmup(iterations = 5) +@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) +@OutputTimeUnit(TimeUnit.SECONDS) +@Fork(value = 1) +@State(Scope.Thread) +public class FlowableConcatMapMaybePerf { + @Param({ "1", "10", "100", "1000", "10000", "100000", "1000000" }) + public int count; + + Flowable<Integer> flowableConvert; + + Flowable<Integer> flowableDedicated; + + Flowable<Integer> flowablePlain; + + @Setup + public void setup() { + Integer[] sourceArray = new Integer[count]; + Arrays.fill(sourceArray, 777); + + Flowable<Integer> source = Flowable.fromArray(sourceArray); + + flowablePlain = source.concatMap(new Function<Integer, Publisher<? extends Integer>>() { + @Override + public Publisher<? extends Integer> apply(Integer v) { + return Flowable.just(v); + } + }); + + flowableConvert = source.concatMap(new Function<Integer, Publisher<? extends Integer>>() { + @Override + public Publisher<? extends Integer> apply(Integer v) { + return Maybe.just(v).toFlowable(); + } + }); + + flowableDedicated = source.concatMapMaybe(new Function<Integer, Maybe<? extends Integer>>() { + @Override + public Maybe<? extends Integer> apply(Integer v) { + return Maybe.just(v); + } + }); + } + + @Benchmark + public Object flowablePlain(Blackhole bh) { + return flowablePlain.subscribeWith(new PerfConsumer(bh)); + } + + @Benchmark + public Object flowableConvert(Blackhole bh) { + return flowableConvert.subscribeWith(new PerfConsumer(bh)); + } + + @Benchmark + public Object flowableDedicated(Blackhole bh) { + return flowableDedicated.subscribeWith(new PerfConsumer(bh)); + } +} diff --git a/src/jmh/java/io/reactivex/rxjava3/xmapz/FlowableConcatMapSinglePerf.java b/src/jmh/java/io/reactivex/rxjava3/xmapz/FlowableConcatMapSinglePerf.java new file mode 100644 index 0000000000..cbeac5ada9 --- /dev/null +++ b/src/jmh/java/io/reactivex/rxjava3/xmapz/FlowableConcatMapSinglePerf.java @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.xmapz; + +import java.util.Arrays; +import java.util.concurrent.TimeUnit; + +import org.openjdk.jmh.annotations.*; +import org.openjdk.jmh.infra.Blackhole; +import org.reactivestreams.Publisher; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.functions.Function; + +@BenchmarkMode(Mode.Throughput) +@Warmup(iterations = 5) +@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) +@OutputTimeUnit(TimeUnit.SECONDS) +@Fork(value = 1) +@State(Scope.Thread) +public class FlowableConcatMapSinglePerf { + @Param({ "1", "10", "100", "1000", "10000", "100000", "1000000" }) + public int count; + + Flowable<Integer> flowableConvert; + + Flowable<Integer> flowableDedicated; + + Flowable<Integer> flowablePlain; + + @Setup + public void setup() { + Integer[] sourceArray = new Integer[count]; + Arrays.fill(sourceArray, 777); + + Flowable<Integer> source = Flowable.fromArray(sourceArray); + + flowablePlain = source.concatMap(new Function<Integer, Publisher<? extends Integer>>() { + @Override + public Publisher<? extends Integer> apply(Integer v) { + return Flowable.just(v); + } + }); + + flowableConvert = source.concatMap(new Function<Integer, Publisher<? extends Integer>>() { + @Override + public Publisher<? extends Integer> apply(Integer v) { + return Single.just(v).toFlowable(); + } + }); + + flowableDedicated = source.concatMapSingle(new Function<Integer, Single<? extends Integer>>() { + @Override + public Single<? extends Integer> apply(Integer v) { + return Single.just(v); + } + }); + } + + @Benchmark + public Object flowablePlain(Blackhole bh) { + return flowablePlain.subscribeWith(new PerfConsumer(bh)); + } + + @Benchmark + public Object flowableConvert(Blackhole bh) { + return flowableConvert.subscribeWith(new PerfConsumer(bh)); + } + + @Benchmark + public Object flowableDedicated(Blackhole bh) { + return flowableDedicated.subscribeWith(new PerfConsumer(bh)); + } +} diff --git a/src/jmh/java/io/reactivex/rxjava3/xmapz/FlowableFlatMapCompletablePerf.java b/src/jmh/java/io/reactivex/rxjava3/xmapz/FlowableFlatMapCompletablePerf.java new file mode 100644 index 0000000000..b46725986f --- /dev/null +++ b/src/jmh/java/io/reactivex/rxjava3/xmapz/FlowableFlatMapCompletablePerf.java @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.xmapz; + +import java.util.Arrays; +import java.util.concurrent.TimeUnit; + +import org.openjdk.jmh.annotations.*; +import org.openjdk.jmh.infra.Blackhole; +import org.reactivestreams.Publisher; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.functions.Function; + +@BenchmarkMode(Mode.Throughput) +@Warmup(iterations = 5) +@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) +@OutputTimeUnit(TimeUnit.SECONDS) +@Fork(value = 1) +@State(Scope.Thread) +public class FlowableFlatMapCompletablePerf { + @Param({ "1", "10", "100", "1000", "10000", "100000", "1000000" }) + public int count; + + Flowable<Integer> flowableConvert; + + Completable flowableDedicated; + + Flowable<Integer> flowablePlain; + + @Setup + public void setup() { + Integer[] sourceArray = new Integer[count]; + Arrays.fill(sourceArray, 777); + + Flowable<Integer> source = Flowable.fromArray(sourceArray); + + flowablePlain = source.flatMap(new Function<Integer, Publisher<? extends Integer>>() { + @Override + public Publisher<? extends Integer> apply(Integer v) { + return Flowable.empty(); + } + }); + + flowableConvert = source.flatMap(new Function<Integer, Publisher<? extends Integer>>() { + @Override + public Publisher<? extends Integer> apply(Integer v) { + return Completable.complete().toFlowable(); + } + }); + + flowableDedicated = source.flatMapCompletable(new Function<Integer, Completable>() { + @Override + public Completable apply(Integer v) { + return Completable.complete(); + } + }); + } + + @Benchmark + public Object flowablePlain(Blackhole bh) { + return flowablePlain.subscribeWith(new PerfConsumer(bh)); + } + + @Benchmark + public Object flowableConvert(Blackhole bh) { + return flowableConvert.subscribeWith(new PerfConsumer(bh)); + } + + @Benchmark + public Object flowableDedicated(Blackhole bh) { + return flowableDedicated.subscribeWith(new PerfConsumer(bh)); + } +} diff --git a/src/jmh/java/io/reactivex/rxjava3/xmapz/FlowableFlatMapMaybeEmptyPerf.java b/src/jmh/java/io/reactivex/rxjava3/xmapz/FlowableFlatMapMaybeEmptyPerf.java new file mode 100644 index 0000000000..8ab19a00c6 --- /dev/null +++ b/src/jmh/java/io/reactivex/rxjava3/xmapz/FlowableFlatMapMaybeEmptyPerf.java @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.xmapz; + +import java.util.Arrays; +import java.util.concurrent.TimeUnit; + +import org.openjdk.jmh.annotations.*; +import org.openjdk.jmh.infra.Blackhole; +import org.reactivestreams.Publisher; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.functions.Function; + +@BenchmarkMode(Mode.Throughput) +@Warmup(iterations = 5) +@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) +@OutputTimeUnit(TimeUnit.SECONDS) +@Fork(value = 1) +@State(Scope.Thread) +public class FlowableFlatMapMaybeEmptyPerf { + @Param({ "1", "10", "100", "1000", "10000", "100000", "1000000" }) + public int count; + + Flowable<Integer> flowableConvert; + + Flowable<Integer> flowableDedicated; + + Flowable<Integer> flowablePlain; + + @Setup + public void setup() { + Integer[] sourceArray = new Integer[count]; + Arrays.fill(sourceArray, 777); + + Flowable<Integer> source = Flowable.fromArray(sourceArray); + + flowablePlain = source.flatMap(new Function<Integer, Publisher<? extends Integer>>() { + @Override + public Publisher<? extends Integer> apply(Integer v) { + return Flowable.empty(); + } + }); + + flowableConvert = source.flatMap(new Function<Integer, Publisher<? extends Integer>>() { + @Override + public Publisher<? extends Integer> apply(Integer v) { + return Maybe.<Integer>empty().toFlowable(); + } + }); + + flowableDedicated = source.flatMapMaybe(new Function<Integer, Maybe<? extends Integer>>() { + @Override + public Maybe<? extends Integer> apply(Integer v) { + return Maybe.empty(); + } + }); + } + + @Benchmark + public Object flowablePlain(Blackhole bh) { + return flowablePlain.subscribeWith(new PerfConsumer(bh)); + } + + @Benchmark + public Object flowableConvert(Blackhole bh) { + return flowableConvert.subscribeWith(new PerfConsumer(bh)); + } + + @Benchmark + public Object flowableDedicated(Blackhole bh) { + return flowableDedicated.subscribeWith(new PerfConsumer(bh)); + } +} diff --git a/src/jmh/java/io/reactivex/rxjava3/xmapz/FlowableFlatMapMaybePerf.java b/src/jmh/java/io/reactivex/rxjava3/xmapz/FlowableFlatMapMaybePerf.java new file mode 100644 index 0000000000..d0f3730b42 --- /dev/null +++ b/src/jmh/java/io/reactivex/rxjava3/xmapz/FlowableFlatMapMaybePerf.java @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.xmapz; + +import java.util.Arrays; +import java.util.concurrent.TimeUnit; + +import org.openjdk.jmh.annotations.*; +import org.openjdk.jmh.infra.Blackhole; +import org.reactivestreams.Publisher; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.functions.Function; + +@BenchmarkMode(Mode.Throughput) +@Warmup(iterations = 5) +@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) +@OutputTimeUnit(TimeUnit.SECONDS) +@Fork(value = 1) +@State(Scope.Thread) +public class FlowableFlatMapMaybePerf { + @Param({ "1", "10", "100", "1000", "10000", "100000", "1000000" }) + public int count; + + Flowable<Integer> flowableConvert; + + Flowable<Integer> flowableDedicated; + + Flowable<Integer> flowablePlain; + + @Setup + public void setup() { + Integer[] sourceArray = new Integer[count]; + Arrays.fill(sourceArray, 777); + + Flowable<Integer> source = Flowable.fromArray(sourceArray); + + flowablePlain = source.flatMap(new Function<Integer, Publisher<? extends Integer>>() { + @Override + public Publisher<? extends Integer> apply(Integer v) { + return Flowable.just(v); + } + }); + + flowableConvert = source.flatMap(new Function<Integer, Publisher<? extends Integer>>() { + @Override + public Publisher<? extends Integer> apply(Integer v) { + return Maybe.just(v).toFlowable(); + } + }); + + flowableDedicated = source.flatMapMaybe(new Function<Integer, Maybe<? extends Integer>>() { + @Override + public Maybe<? extends Integer> apply(Integer v) { + return Maybe.just(v); + } + }); + } + + @Benchmark + public Object flowablePlain(Blackhole bh) { + return flowablePlain.subscribeWith(new PerfConsumer(bh)); + } + + @Benchmark + public Object flowableConvert(Blackhole bh) { + return flowableConvert.subscribeWith(new PerfConsumer(bh)); + } + + @Benchmark + public Object flowableDedicated(Blackhole bh) { + return flowableDedicated.subscribeWith(new PerfConsumer(bh)); + } +} diff --git a/src/jmh/java/io/reactivex/rxjava3/xmapz/FlowableFlatMapSinglePerf.java b/src/jmh/java/io/reactivex/rxjava3/xmapz/FlowableFlatMapSinglePerf.java new file mode 100644 index 0000000000..4f50938647 --- /dev/null +++ b/src/jmh/java/io/reactivex/rxjava3/xmapz/FlowableFlatMapSinglePerf.java @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.xmapz; + +import java.util.Arrays; +import java.util.concurrent.TimeUnit; + +import org.openjdk.jmh.annotations.*; +import org.openjdk.jmh.infra.Blackhole; +import org.reactivestreams.Publisher; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.functions.Function; + +@BenchmarkMode(Mode.Throughput) +@Warmup(iterations = 5) +@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) +@OutputTimeUnit(TimeUnit.SECONDS) +@Fork(value = 1) +@State(Scope.Thread) +public class FlowableFlatMapSinglePerf { + @Param({ "1", "10", "100", "1000", "10000", "100000", "1000000" }) + public int count; + + Flowable<Integer> flowableConvert; + + Flowable<Integer> flowableDedicated; + + Flowable<Integer> flowablePlain; + + @Setup + public void setup() { + Integer[] sourceArray = new Integer[count]; + Arrays.fill(sourceArray, 777); + + Flowable<Integer> source = Flowable.fromArray(sourceArray); + + flowablePlain = source.flatMap(new Function<Integer, Publisher<? extends Integer>>() { + @Override + public Publisher<? extends Integer> apply(Integer v) { + return Flowable.just(v); + } + }); + + flowableConvert = source.flatMap(new Function<Integer, Publisher<? extends Integer>>() { + @Override + public Publisher<? extends Integer> apply(Integer v) { + return Single.just(v).toFlowable(); + } + }); + + flowableDedicated = source.flatMapSingle(new Function<Integer, Single<? extends Integer>>() { + @Override + public Single<? extends Integer> apply(Integer v) { + return Single.just(v); + } + }); + } + + @Benchmark + public Object flowablePlain(Blackhole bh) { + return flowablePlain.subscribeWith(new PerfConsumer(bh)); + } + + @Benchmark + public Object flowableConvert(Blackhole bh) { + return flowableConvert.subscribeWith(new PerfConsumer(bh)); + } + + @Benchmark + public Object flowableDedicated(Blackhole bh) { + return flowableDedicated.subscribeWith(new PerfConsumer(bh)); + } +} diff --git a/src/jmh/java/io/reactivex/rxjava3/xmapz/FlowableSwitchMapCompletablePerf.java b/src/jmh/java/io/reactivex/rxjava3/xmapz/FlowableSwitchMapCompletablePerf.java new file mode 100644 index 0000000000..4e419a5d2e --- /dev/null +++ b/src/jmh/java/io/reactivex/rxjava3/xmapz/FlowableSwitchMapCompletablePerf.java @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.xmapz; + +import java.util.Arrays; +import java.util.concurrent.TimeUnit; + +import org.openjdk.jmh.annotations.*; +import org.openjdk.jmh.infra.Blackhole; +import org.reactivestreams.Publisher; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.functions.Function; + +@BenchmarkMode(Mode.Throughput) +@Warmup(iterations = 5) +@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) +@OutputTimeUnit(TimeUnit.SECONDS) +@Fork(value = 1) +@State(Scope.Thread) +public class FlowableSwitchMapCompletablePerf { + @Param({ "1", "10", "100", "1000", "10000", "100000", "1000000" }) + public int count; + + Flowable<Integer> flowableConvert; + + Completable flowableDedicated; + + Flowable<Integer> flowablePlain; + + @Setup + public void setup() { + Integer[] sourceArray = new Integer[count]; + Arrays.fill(sourceArray, 777); + + Flowable<Integer> source = Flowable.fromArray(sourceArray); + + flowablePlain = source.switchMap(new Function<Integer, Publisher<? extends Integer>>() { + @Override + public Publisher<? extends Integer> apply(Integer v) { + return Flowable.empty(); + } + }); + + flowableConvert = source.switchMap(new Function<Integer, Publisher<? extends Integer>>() { + @Override + public Publisher<? extends Integer> apply(Integer v) { + return Completable.complete().toFlowable(); + } + }); + + flowableDedicated = source.switchMapCompletable(new Function<Integer, Completable>() { + @Override + public Completable apply(Integer v) { + return Completable.complete(); + } + }); + } + + @Benchmark + public Object flowablePlain(Blackhole bh) { + return flowablePlain.subscribeWith(new PerfConsumer(bh)); + } + + @Benchmark + public Object flowableConvert(Blackhole bh) { + return flowableConvert.subscribeWith(new PerfConsumer(bh)); + } + + @Benchmark + public Object flowableDedicated(Blackhole bh) { + return flowableDedicated.subscribeWith(new PerfConsumer(bh)); + } +} diff --git a/src/jmh/java/io/reactivex/rxjava3/xmapz/FlowableSwitchMapMaybeEmptyPerf.java b/src/jmh/java/io/reactivex/rxjava3/xmapz/FlowableSwitchMapMaybeEmptyPerf.java new file mode 100644 index 0000000000..83ad00e0f9 --- /dev/null +++ b/src/jmh/java/io/reactivex/rxjava3/xmapz/FlowableSwitchMapMaybeEmptyPerf.java @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.xmapz; + +import java.util.Arrays; +import java.util.concurrent.TimeUnit; + +import org.openjdk.jmh.annotations.*; +import org.openjdk.jmh.infra.Blackhole; +import org.reactivestreams.Publisher; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.functions.Function; + +@BenchmarkMode(Mode.Throughput) +@Warmup(iterations = 5) +@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) +@OutputTimeUnit(TimeUnit.SECONDS) +@Fork(value = 1) +@State(Scope.Thread) +public class FlowableSwitchMapMaybeEmptyPerf { + @Param({ "1", "10", "100", "1000", "10000", "100000", "1000000" }) + public int count; + + Flowable<Integer> flowableConvert; + + Flowable<Integer> flowableDedicated; + + Flowable<Integer> flowablePlain; + + @Setup + public void setup() { + Integer[] sourceArray = new Integer[count]; + Arrays.fill(sourceArray, 777); + + Flowable<Integer> source = Flowable.fromArray(sourceArray); + + flowablePlain = source.switchMap(new Function<Integer, Publisher<? extends Integer>>() { + @Override + public Publisher<? extends Integer> apply(Integer v) { + return Flowable.empty(); + } + }); + + flowableConvert = source.switchMap(new Function<Integer, Publisher<? extends Integer>>() { + @Override + public Publisher<? extends Integer> apply(Integer v) { + return Maybe.<Integer>empty().toFlowable(); + } + }); + + flowableDedicated = source.switchMapMaybe(new Function<Integer, Maybe<? extends Integer>>() { + @Override + public Maybe<? extends Integer> apply(Integer v) { + return Maybe.empty(); + } + }); + } + + @Benchmark + public Object flowablePlain(Blackhole bh) { + return flowablePlain.subscribeWith(new PerfConsumer(bh)); + } + + @Benchmark + public Object flowableConvert(Blackhole bh) { + return flowableConvert.subscribeWith(new PerfConsumer(bh)); + } + + @Benchmark + public Object flowableDedicated(Blackhole bh) { + return flowableDedicated.subscribeWith(new PerfConsumer(bh)); + } +} diff --git a/src/jmh/java/io/reactivex/rxjava3/xmapz/FlowableSwitchMapMaybePerf.java b/src/jmh/java/io/reactivex/rxjava3/xmapz/FlowableSwitchMapMaybePerf.java new file mode 100644 index 0000000000..e36b49c4d3 --- /dev/null +++ b/src/jmh/java/io/reactivex/rxjava3/xmapz/FlowableSwitchMapMaybePerf.java @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.xmapz; + +import java.util.Arrays; +import java.util.concurrent.TimeUnit; + +import org.openjdk.jmh.annotations.*; +import org.openjdk.jmh.infra.Blackhole; +import org.reactivestreams.Publisher; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.functions.Function; + +@BenchmarkMode(Mode.Throughput) +@Warmup(iterations = 5) +@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) +@OutputTimeUnit(TimeUnit.SECONDS) +@Fork(value = 1) +@State(Scope.Thread) +public class FlowableSwitchMapMaybePerf { + @Param({ "1", "10", "100", "1000", "10000", "100000", "1000000" }) + public int count; + + Flowable<Integer> flowableConvert; + + Flowable<Integer> flowableDedicated; + + Flowable<Integer> flowablePlain; + + @Setup + public void setup() { + Integer[] sourceArray = new Integer[count]; + Arrays.fill(sourceArray, 777); + + Flowable<Integer> source = Flowable.fromArray(sourceArray); + + flowablePlain = source.switchMap(new Function<Integer, Publisher<? extends Integer>>() { + @Override + public Publisher<? extends Integer> apply(Integer v) { + return Flowable.just(v); + } + }); + + flowableConvert = source.switchMap(new Function<Integer, Publisher<? extends Integer>>() { + @Override + public Publisher<? extends Integer> apply(Integer v) { + return Maybe.just(v).toFlowable(); + } + }); + + flowableDedicated = source.switchMapMaybe(new Function<Integer, Maybe<? extends Integer>>() { + @Override + public Maybe<? extends Integer> apply(Integer v) { + return Maybe.just(v); + } + }); + } + + @Benchmark + public Object flowablePlain(Blackhole bh) { + return flowablePlain.subscribeWith(new PerfConsumer(bh)); + } + + @Benchmark + public Object flowableConvert(Blackhole bh) { + return flowableConvert.subscribeWith(new PerfConsumer(bh)); + } + + @Benchmark + public Object flowableDedicated(Blackhole bh) { + return flowableDedicated.subscribeWith(new PerfConsumer(bh)); + } +} diff --git a/src/jmh/java/io/reactivex/rxjava3/xmapz/FlowableSwitchMapSinglePerf.java b/src/jmh/java/io/reactivex/rxjava3/xmapz/FlowableSwitchMapSinglePerf.java new file mode 100644 index 0000000000..0da6941895 --- /dev/null +++ b/src/jmh/java/io/reactivex/rxjava3/xmapz/FlowableSwitchMapSinglePerf.java @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.xmapz; + +import java.util.Arrays; +import java.util.concurrent.TimeUnit; + +import org.openjdk.jmh.annotations.*; +import org.openjdk.jmh.infra.Blackhole; +import org.reactivestreams.Publisher; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.functions.Function; + +@BenchmarkMode(Mode.Throughput) +@Warmup(iterations = 5) +@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) +@OutputTimeUnit(TimeUnit.SECONDS) +@Fork(value = 1) +@State(Scope.Thread) +public class FlowableSwitchMapSinglePerf { + @Param({ "1", "10", "100", "1000", "10000", "100000", "1000000" }) + public int count; + + Flowable<Integer> flowableConvert; + + Flowable<Integer> flowableDedicated; + + Flowable<Integer> flowablePlain; + + @Setup + public void setup() { + Integer[] sourceArray = new Integer[count]; + Arrays.fill(sourceArray, 777); + + Flowable<Integer> source = Flowable.fromArray(sourceArray); + + flowablePlain = source.switchMap(new Function<Integer, Publisher<? extends Integer>>() { + @Override + public Publisher<? extends Integer> apply(Integer v) { + return Flowable.just(v); + } + }); + + flowableConvert = source.switchMap(new Function<Integer, Publisher<? extends Integer>>() { + @Override + public Publisher<? extends Integer> apply(Integer v) { + return Single.just(v).toFlowable(); + } + }); + + flowableDedicated = source.switchMapSingle(new Function<Integer, Single<? extends Integer>>() { + @Override + public Single<? extends Integer> apply(Integer v) { + return Single.just(v); + } + }); + } + + @Benchmark + public Object flowablePlain(Blackhole bh) { + return flowablePlain.subscribeWith(new PerfConsumer(bh)); + } + + @Benchmark + public Object flowableConvert(Blackhole bh) { + return flowableConvert.subscribeWith(new PerfConsumer(bh)); + } + + @Benchmark + public Object flowableDedicated(Blackhole bh) { + return flowableDedicated.subscribeWith(new PerfConsumer(bh)); + } +} diff --git a/src/jmh/java/io/reactivex/rxjava3/xmapz/ObservableConcatMapCompletablePerf.java b/src/jmh/java/io/reactivex/rxjava3/xmapz/ObservableConcatMapCompletablePerf.java new file mode 100644 index 0000000000..48b20dc005 --- /dev/null +++ b/src/jmh/java/io/reactivex/rxjava3/xmapz/ObservableConcatMapCompletablePerf.java @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.xmapz; + +import java.util.Arrays; +import java.util.concurrent.TimeUnit; + +import org.openjdk.jmh.annotations.*; +import org.openjdk.jmh.infra.Blackhole; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.functions.Function; + +@BenchmarkMode(Mode.Throughput) +@Warmup(iterations = 5) +@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) +@OutputTimeUnit(TimeUnit.SECONDS) +@Fork(value = 1) +@State(Scope.Thread) +public class ObservableConcatMapCompletablePerf { + @Param({ "1", "10", "100", "1000", "10000", "100000", "1000000" }) + public int count; + + Observable<Integer> observableConvert; + + Completable observableDedicated; + + Observable<Integer> observablePlain; + + @Setup + public void setup() { + Integer[] sourceArray = new Integer[count]; + Arrays.fill(sourceArray, 777); + + Observable<Integer> source = Observable.fromArray(sourceArray); + + observablePlain = source.concatMap(new Function<Integer, Observable<? extends Integer>>() { + @Override + public Observable<? extends Integer> apply(Integer v) { + return Observable.empty(); + } + }); + + observableConvert = source.concatMap(new Function<Integer, Observable<? extends Integer>>() { + @Override + public Observable<? extends Integer> apply(Integer v) { + return Completable.complete().toObservable(); + } + }); + + observableDedicated = source.concatMapCompletable(new Function<Integer, Completable>() { + @Override + public Completable apply(Integer v) { + return Completable.complete(); + } + }); + } + + @Benchmark + public Object observablePlain(Blackhole bh) { + return observablePlain.subscribeWith(new PerfConsumer(bh)); + } + + @Benchmark + public Object observableConvert(Blackhole bh) { + return observableConvert.subscribeWith(new PerfConsumer(bh)); + } + + @Benchmark + public Object observableDedicated(Blackhole bh) { + return observableDedicated.subscribeWith(new PerfConsumer(bh)); + } +} diff --git a/src/jmh/java/io/reactivex/rxjava3/xmapz/ObservableConcatMapMaybeEmptyPerf.java b/src/jmh/java/io/reactivex/rxjava3/xmapz/ObservableConcatMapMaybeEmptyPerf.java new file mode 100644 index 0000000000..4528c90b50 --- /dev/null +++ b/src/jmh/java/io/reactivex/rxjava3/xmapz/ObservableConcatMapMaybeEmptyPerf.java @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.xmapz; + +import java.util.Arrays; +import java.util.concurrent.TimeUnit; + +import org.openjdk.jmh.annotations.*; +import org.openjdk.jmh.infra.Blackhole; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.functions.Function; + +@BenchmarkMode(Mode.Throughput) +@Warmup(iterations = 5) +@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) +@OutputTimeUnit(TimeUnit.SECONDS) +@Fork(value = 1) +@State(Scope.Thread) +public class ObservableConcatMapMaybeEmptyPerf { + @Param({ "1", "10", "100", "1000", "10000", "100000", "1000000" }) + public int count; + + Observable<Integer> concatMapToObservableEmpty; + + Observable<Integer> observableDedicated; + + Observable<Integer> observablePlain; + + @Setup + public void setup() { + Integer[] sourceArray = new Integer[count]; + Arrays.fill(sourceArray, 777); + + Observable<Integer> source = Observable.fromArray(sourceArray); + + observablePlain = source.concatMap(new Function<Integer, Observable<? extends Integer>>() { + @Override + public Observable<? extends Integer> apply(Integer v) { + return Observable.empty(); + } + }); + + concatMapToObservableEmpty = source.concatMap(new Function<Integer, Observable<? extends Integer>>() { + @Override + public Observable<? extends Integer> apply(Integer v) { + return Maybe.<Integer>empty().toObservable(); + } + }); + + observableDedicated = source.concatMapMaybe(new Function<Integer, Maybe<? extends Integer>>() { + @Override + public Maybe<? extends Integer> apply(Integer v) { + return Maybe.empty(); + } + }); + } + + @Benchmark + public Object observablePlain(Blackhole bh) { + return observablePlain.subscribeWith(new PerfConsumer(bh)); + } + + @Benchmark + public Object observableConvert(Blackhole bh) { + return concatMapToObservableEmpty.subscribeWith(new PerfConsumer(bh)); + } + + @Benchmark + public Object observableDedicated(Blackhole bh) { + return observableDedicated.subscribeWith(new PerfConsumer(bh)); + } +} diff --git a/src/jmh/java/io/reactivex/rxjava3/xmapz/ObservableConcatMapMaybePerf.java b/src/jmh/java/io/reactivex/rxjava3/xmapz/ObservableConcatMapMaybePerf.java new file mode 100644 index 0000000000..204020abfe --- /dev/null +++ b/src/jmh/java/io/reactivex/rxjava3/xmapz/ObservableConcatMapMaybePerf.java @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.xmapz; + +import java.util.Arrays; +import java.util.concurrent.TimeUnit; + +import org.openjdk.jmh.annotations.*; +import org.openjdk.jmh.infra.Blackhole; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.functions.Function; + +@BenchmarkMode(Mode.Throughput) +@Warmup(iterations = 5) +@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) +@OutputTimeUnit(TimeUnit.SECONDS) +@Fork(value = 1) +@State(Scope.Thread) +public class ObservableConcatMapMaybePerf { + @Param({ "1", "10", "100", "1000", "10000", "100000", "1000000" }) + public int count; + + Observable<Integer> observableConvert; + + Observable<Integer> observableDedicated; + + Observable<Integer> observablePlain; + + @Setup + public void setup() { + Integer[] sourceArray = new Integer[count]; + Arrays.fill(sourceArray, 777); + + Observable<Integer> source = Observable.fromArray(sourceArray); + + observablePlain = source.concatMap(new Function<Integer, Observable<? extends Integer>>() { + @Override + public Observable<? extends Integer> apply(Integer v) { + return Observable.just(v); + } + }); + + observableConvert = source.concatMap(new Function<Integer, Observable<? extends Integer>>() { + @Override + public Observable<? extends Integer> apply(Integer v) { + return Maybe.just(v).toObservable(); + } + }); + + observableDedicated = source.concatMapMaybe(new Function<Integer, Maybe<? extends Integer>>() { + @Override + public Maybe<? extends Integer> apply(Integer v) { + return Maybe.just(v); + } + }); + } + + @Benchmark + public Object observablePlain(Blackhole bh) { + return observablePlain.subscribeWith(new PerfConsumer(bh)); + } + + @Benchmark + public Object observableConvert(Blackhole bh) { + return observableConvert.subscribeWith(new PerfConsumer(bh)); + } + + @Benchmark + public Object observableDedicated(Blackhole bh) { + return observableDedicated.subscribeWith(new PerfConsumer(bh)); + } +} diff --git a/src/jmh/java/io/reactivex/rxjava3/xmapz/ObservableConcatMapSinglePerf.java b/src/jmh/java/io/reactivex/rxjava3/xmapz/ObservableConcatMapSinglePerf.java new file mode 100644 index 0000000000..e2e34b24f5 --- /dev/null +++ b/src/jmh/java/io/reactivex/rxjava3/xmapz/ObservableConcatMapSinglePerf.java @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.xmapz; + +import java.util.Arrays; +import java.util.concurrent.TimeUnit; + +import org.openjdk.jmh.annotations.*; +import org.openjdk.jmh.infra.Blackhole; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.functions.Function; + +@BenchmarkMode(Mode.Throughput) +@Warmup(iterations = 5) +@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) +@OutputTimeUnit(TimeUnit.SECONDS) +@Fork(value = 1) +@State(Scope.Thread) +public class ObservableConcatMapSinglePerf { + @Param({ "1", "10", "100", "1000", "10000", "100000", "1000000" }) + public int count; + + Observable<Integer> observableConvert; + + Observable<Integer> observableDedicated; + + Observable<Integer> observablePlain; + + @Setup + public void setup() { + Integer[] sourceArray = new Integer[count]; + Arrays.fill(sourceArray, 777); + + Observable<Integer> source = Observable.fromArray(sourceArray); + + observablePlain = source.concatMap(new Function<Integer, Observable<? extends Integer>>() { + @Override + public Observable<? extends Integer> apply(Integer v) { + return Observable.just(v); + } + }); + + observableConvert = source.concatMap(new Function<Integer, Observable<? extends Integer>>() { + @Override + public Observable<? extends Integer> apply(Integer v) { + return Single.just(v).toObservable(); + } + }); + + observableDedicated = source.concatMapSingle(new Function<Integer, Single<? extends Integer>>() { + @Override + public Single<? extends Integer> apply(Integer v) { + return Single.just(v); + } + }); + } + + @Benchmark + public Object observablePlain(Blackhole bh) { + return observablePlain.subscribeWith(new PerfConsumer(bh)); + } + + @Benchmark + public Object observableConvert(Blackhole bh) { + return observableConvert.subscribeWith(new PerfConsumer(bh)); + } + + @Benchmark + public Object observableDedicated(Blackhole bh) { + return observableDedicated.subscribeWith(new PerfConsumer(bh)); + } +} diff --git a/src/jmh/java/io/reactivex/rxjava3/xmapz/ObservableFlatMapCompletablePerf.java b/src/jmh/java/io/reactivex/rxjava3/xmapz/ObservableFlatMapCompletablePerf.java new file mode 100644 index 0000000000..b6daa57eb6 --- /dev/null +++ b/src/jmh/java/io/reactivex/rxjava3/xmapz/ObservableFlatMapCompletablePerf.java @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.xmapz; + +import java.util.Arrays; +import java.util.concurrent.TimeUnit; + +import org.openjdk.jmh.annotations.*; +import org.openjdk.jmh.infra.Blackhole; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.functions.Function; + +@BenchmarkMode(Mode.Throughput) +@Warmup(iterations = 5) +@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) +@OutputTimeUnit(TimeUnit.SECONDS) +@Fork(value = 1) +@State(Scope.Thread) +public class ObservableFlatMapCompletablePerf { + @Param({ "1", "10", "100", "1000", "10000", "100000", "1000000" }) + public int count; + + Observable<Integer> observableConvert; + + Completable observableDedicated; + + Observable<Integer> observablePlain; + + @Setup + public void setup() { + Integer[] sourceArray = new Integer[count]; + Arrays.fill(sourceArray, 777); + + Observable<Integer> source = Observable.fromArray(sourceArray); + + observablePlain = source.flatMap(new Function<Integer, Observable<? extends Integer>>() { + @Override + public Observable<? extends Integer> apply(Integer v) { + return Observable.empty(); + } + }); + + observableConvert = source.flatMap(new Function<Integer, Observable<? extends Integer>>() { + @Override + public Observable<? extends Integer> apply(Integer v) { + return Completable.complete().toObservable(); + } + }); + + observableDedicated = source.flatMapCompletable(new Function<Integer, Completable>() { + @Override + public Completable apply(Integer v) { + return Completable.complete(); + } + }); + } + + @Benchmark + public Object observablePlain(Blackhole bh) { + return observablePlain.subscribeWith(new PerfConsumer(bh)); + } + + @Benchmark + public Object observableConvert(Blackhole bh) { + return observableConvert.subscribeWith(new PerfConsumer(bh)); + } + + @Benchmark + public Object observableDedicated(Blackhole bh) { + return observableDedicated.subscribeWith(new PerfConsumer(bh)); + } +} diff --git a/src/jmh/java/io/reactivex/rxjava3/xmapz/ObservableFlatMapMaybeEmptyPerf.java b/src/jmh/java/io/reactivex/rxjava3/xmapz/ObservableFlatMapMaybeEmptyPerf.java new file mode 100644 index 0000000000..5d0327fa46 --- /dev/null +++ b/src/jmh/java/io/reactivex/rxjava3/xmapz/ObservableFlatMapMaybeEmptyPerf.java @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.xmapz; + +import java.util.Arrays; +import java.util.concurrent.TimeUnit; + +import org.openjdk.jmh.annotations.*; +import org.openjdk.jmh.infra.Blackhole; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.functions.Function; + +@BenchmarkMode(Mode.Throughput) +@Warmup(iterations = 5) +@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) +@OutputTimeUnit(TimeUnit.SECONDS) +@Fork(value = 1) +@State(Scope.Thread) +public class ObservableFlatMapMaybeEmptyPerf { + @Param({ "1", "10", "100", "1000", "10000", "100000", "1000000" }) + public int count; + + Observable<Integer> observableConvert; + + Observable<Integer> observableDedicated; + + Observable<Integer> observablePlain; + + @Setup + public void setup() { + Integer[] sourceArray = new Integer[count]; + Arrays.fill(sourceArray, 777); + + Observable<Integer> source = Observable.fromArray(sourceArray); + + observablePlain = source.flatMap(new Function<Integer, Observable<? extends Integer>>() { + @Override + public Observable<? extends Integer> apply(Integer v) { + return Observable.empty(); + } + }); + + observableConvert = source.flatMap(new Function<Integer, Observable<? extends Integer>>() { + @Override + public Observable<? extends Integer> apply(Integer v) { + return Maybe.<Integer>empty().toObservable(); + } + }); + + observableDedicated = source.flatMapMaybe(new Function<Integer, Maybe<? extends Integer>>() { + @Override + public Maybe<? extends Integer> apply(Integer v) { + return Maybe.empty(); + } + }); + } + + @Benchmark + public Object observablePlain(Blackhole bh) { + return observablePlain.subscribeWith(new PerfConsumer(bh)); + } + + @Benchmark + public Object observableConvert(Blackhole bh) { + return observableConvert.subscribeWith(new PerfConsumer(bh)); + } + + @Benchmark + public Object observableDedicated(Blackhole bh) { + return observableDedicated.subscribeWith(new PerfConsumer(bh)); + } +} diff --git a/src/jmh/java/io/reactivex/rxjava3/xmapz/ObservableFlatMapMaybePerf.java b/src/jmh/java/io/reactivex/rxjava3/xmapz/ObservableFlatMapMaybePerf.java new file mode 100644 index 0000000000..e2a7c43bea --- /dev/null +++ b/src/jmh/java/io/reactivex/rxjava3/xmapz/ObservableFlatMapMaybePerf.java @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.xmapz; + +import java.util.Arrays; +import java.util.concurrent.TimeUnit; + +import org.openjdk.jmh.annotations.*; +import org.openjdk.jmh.infra.Blackhole; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.functions.Function; + +@BenchmarkMode(Mode.Throughput) +@Warmup(iterations = 5) +@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) +@OutputTimeUnit(TimeUnit.SECONDS) +@Fork(value = 1) +@State(Scope.Thread) +public class ObservableFlatMapMaybePerf { + @Param({ "1", "10", "100", "1000", "10000", "100000", "1000000" }) + public int count; + + Observable<Integer> observableConvert; + + Observable<Integer> observableDedicated; + + Observable<Integer> observablePlain; + + @Setup + public void setup() { + Integer[] sourceArray = new Integer[count]; + Arrays.fill(sourceArray, 777); + + Observable<Integer> source = Observable.fromArray(sourceArray); + + observablePlain = source.flatMap(new Function<Integer, Observable<? extends Integer>>() { + @Override + public Observable<? extends Integer> apply(Integer v) { + return Observable.just(v); + } + }); + + observableConvert = source.flatMap(new Function<Integer, Observable<? extends Integer>>() { + @Override + public Observable<? extends Integer> apply(Integer v) { + return Maybe.just(v).toObservable(); + } + }); + + observableDedicated = source.flatMapMaybe(new Function<Integer, Maybe<? extends Integer>>() { + @Override + public Maybe<? extends Integer> apply(Integer v) { + return Maybe.just(v); + } + }); + } + + @Benchmark + public Object observablePlain(Blackhole bh) { + return observablePlain.subscribeWith(new PerfConsumer(bh)); + } + + @Benchmark + public Object observableConvert(Blackhole bh) { + return observableConvert.subscribeWith(new PerfConsumer(bh)); + } + + @Benchmark + public Object observableDedicated(Blackhole bh) { + return observableDedicated.subscribeWith(new PerfConsumer(bh)); + } +} diff --git a/src/jmh/java/io/reactivex/rxjava3/xmapz/ObservableFlatMapSinglePerf.java b/src/jmh/java/io/reactivex/rxjava3/xmapz/ObservableFlatMapSinglePerf.java new file mode 100644 index 0000000000..add0cd310c --- /dev/null +++ b/src/jmh/java/io/reactivex/rxjava3/xmapz/ObservableFlatMapSinglePerf.java @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.xmapz; + +import java.util.Arrays; +import java.util.concurrent.TimeUnit; + +import org.openjdk.jmh.annotations.*; +import org.openjdk.jmh.infra.Blackhole; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.functions.Function; + +@BenchmarkMode(Mode.Throughput) +@Warmup(iterations = 5) +@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) +@OutputTimeUnit(TimeUnit.SECONDS) +@Fork(value = 1) +@State(Scope.Thread) +public class ObservableFlatMapSinglePerf { + @Param({ "1", "10", "100", "1000", "10000", "100000", "1000000" }) + public int count; + + Observable<Integer> observableConvert; + + Observable<Integer> observableDedicated; + + Observable<Integer> observablePlain; + + @Setup + public void setup() { + Integer[] sourceArray = new Integer[count]; + Arrays.fill(sourceArray, 777); + + Observable<Integer> source = Observable.fromArray(sourceArray); + + observablePlain = source.flatMap(new Function<Integer, Observable<? extends Integer>>() { + @Override + public Observable<? extends Integer> apply(Integer v) { + return Observable.just(v); + } + }); + + observableConvert = source.flatMap(new Function<Integer, Observable<? extends Integer>>() { + @Override + public Observable<? extends Integer> apply(Integer v) { + return Single.just(v).toObservable(); + } + }); + + observableDedicated = source.flatMapSingle(new Function<Integer, Single<? extends Integer>>() { + @Override + public Single<? extends Integer> apply(Integer v) { + return Single.just(v); + } + }); + } + + @Benchmark + public Object observablePlain(Blackhole bh) { + return observablePlain.subscribeWith(new PerfConsumer(bh)); + } + + @Benchmark + public Object observableConvert(Blackhole bh) { + return observableConvert.subscribeWith(new PerfConsumer(bh)); + } + + @Benchmark + public Object observableDedicated(Blackhole bh) { + return observableDedicated.subscribeWith(new PerfConsumer(bh)); + } +} diff --git a/src/jmh/java/io/reactivex/rxjava3/xmapz/ObservableSwitchMapCompletablePerf.java b/src/jmh/java/io/reactivex/rxjava3/xmapz/ObservableSwitchMapCompletablePerf.java new file mode 100644 index 0000000000..69b8e71f18 --- /dev/null +++ b/src/jmh/java/io/reactivex/rxjava3/xmapz/ObservableSwitchMapCompletablePerf.java @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.xmapz; + +import java.util.Arrays; +import java.util.concurrent.TimeUnit; + +import org.openjdk.jmh.annotations.*; +import org.openjdk.jmh.infra.Blackhole; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.functions.Function; + +@BenchmarkMode(Mode.Throughput) +@Warmup(iterations = 5) +@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) +@OutputTimeUnit(TimeUnit.SECONDS) +@Fork(value = 1) +@State(Scope.Thread) +public class ObservableSwitchMapCompletablePerf { + @Param({ "1", "10", "100", "1000", "10000", "100000", "1000000" }) + public int count; + + Observable<Integer> observableConvert; + + Completable observableDedicated; + + Observable<Integer> observablePlain; + + @Setup + public void setup() { + Integer[] sourceArray = new Integer[count]; + Arrays.fill(sourceArray, 777); + + Observable<Integer> source = Observable.fromArray(sourceArray); + + observablePlain = source.switchMap(new Function<Integer, Observable<? extends Integer>>() { + @Override + public Observable<? extends Integer> apply(Integer v) { + return Observable.empty(); + } + }); + + observableConvert = source.switchMap(new Function<Integer, Observable<? extends Integer>>() { + @Override + public Observable<? extends Integer> apply(Integer v) { + return Completable.complete().toObservable(); + } + }); + + observableDedicated = source.switchMapCompletable(new Function<Integer, Completable>() { + @Override + public Completable apply(Integer v) { + return Completable.complete(); + } + }); + } + + @Benchmark + public Object observablePlain(Blackhole bh) { + return observablePlain.subscribeWith(new PerfConsumer(bh)); + } + + @Benchmark + public Object observableConvert(Blackhole bh) { + return observableConvert.subscribeWith(new PerfConsumer(bh)); + } + + @Benchmark + public Object observableDedicated(Blackhole bh) { + return observableDedicated.subscribeWith(new PerfConsumer(bh)); + } +} diff --git a/src/jmh/java/io/reactivex/rxjava3/xmapz/ObservableSwitchMapMaybeEmptyPerf.java b/src/jmh/java/io/reactivex/rxjava3/xmapz/ObservableSwitchMapMaybeEmptyPerf.java new file mode 100644 index 0000000000..3930534eb8 --- /dev/null +++ b/src/jmh/java/io/reactivex/rxjava3/xmapz/ObservableSwitchMapMaybeEmptyPerf.java @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.xmapz; + +import java.util.Arrays; +import java.util.concurrent.TimeUnit; + +import org.openjdk.jmh.annotations.*; +import org.openjdk.jmh.infra.Blackhole; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.functions.Function; + +@BenchmarkMode(Mode.Throughput) +@Warmup(iterations = 5) +@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) +@OutputTimeUnit(TimeUnit.SECONDS) +@Fork(value = 1) +@State(Scope.Thread) +public class ObservableSwitchMapMaybeEmptyPerf { + @Param({ "1", "10", "100", "1000", "10000", "100000", "1000000" }) + public int count; + + Observable<Integer> observableConvert; + + Observable<Integer> observableDedicated; + + Observable<Integer> observablePlain; + + @Setup + public void setup() { + Integer[] sourceArray = new Integer[count]; + Arrays.fill(sourceArray, 777); + + Observable<Integer> source = Observable.fromArray(sourceArray); + + observablePlain = source.switchMap(new Function<Integer, Observable<? extends Integer>>() { + @Override + public Observable<? extends Integer> apply(Integer v) { + return Observable.empty(); + } + }); + + observableConvert = source.switchMap(new Function<Integer, Observable<? extends Integer>>() { + @Override + public Observable<? extends Integer> apply(Integer v) { + return Maybe.<Integer>empty().toObservable(); + } + }); + + observableDedicated = source.switchMapMaybe(new Function<Integer, Maybe<? extends Integer>>() { + @Override + public Maybe<? extends Integer> apply(Integer v) { + return Maybe.empty(); + } + }); + } + + @Benchmark + public Object observablePlain(Blackhole bh) { + return observablePlain.subscribeWith(new PerfConsumer(bh)); + } + + @Benchmark + public Object observableConvert(Blackhole bh) { + return observableConvert.subscribeWith(new PerfConsumer(bh)); + } + + @Benchmark + public Object observableDedicated(Blackhole bh) { + return observableDedicated.subscribeWith(new PerfConsumer(bh)); + } +} diff --git a/src/jmh/java/io/reactivex/rxjava3/xmapz/ObservableSwitchMapMaybePerf.java b/src/jmh/java/io/reactivex/rxjava3/xmapz/ObservableSwitchMapMaybePerf.java new file mode 100644 index 0000000000..30158d012d --- /dev/null +++ b/src/jmh/java/io/reactivex/rxjava3/xmapz/ObservableSwitchMapMaybePerf.java @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.xmapz; + +import java.util.Arrays; +import java.util.concurrent.TimeUnit; + +import org.openjdk.jmh.annotations.*; +import org.openjdk.jmh.infra.Blackhole; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.functions.Function; + +@BenchmarkMode(Mode.Throughput) +@Warmup(iterations = 5) +@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) +@OutputTimeUnit(TimeUnit.SECONDS) +@Fork(value = 1) +@State(Scope.Thread) +public class ObservableSwitchMapMaybePerf { + @Param({ "1", "10", "100", "1000", "10000", "100000", "1000000" }) + public int count; + + Observable<Integer> observableConvert; + + Observable<Integer> observableDedicated; + + Observable<Integer> observablePlain; + + @Setup + public void setup() { + Integer[] sourceArray = new Integer[count]; + Arrays.fill(sourceArray, 777); + + Observable<Integer> source = Observable.fromArray(sourceArray); + + observablePlain = source.switchMap(new Function<Integer, Observable<? extends Integer>>() { + @Override + public Observable<? extends Integer> apply(Integer v) { + return Observable.just(v); + } + }); + + observableConvert = source.switchMap(new Function<Integer, Observable<? extends Integer>>() { + @Override + public Observable<? extends Integer> apply(Integer v) { + return Maybe.just(v).toObservable(); + } + }); + + observableDedicated = source.switchMapMaybe(new Function<Integer, Maybe<? extends Integer>>() { + @Override + public Maybe<? extends Integer> apply(Integer v) { + return Maybe.just(v); + } + }); + } + + @Benchmark + public Object observablePlain(Blackhole bh) { + return observablePlain.subscribeWith(new PerfConsumer(bh)); + } + + @Benchmark + public Object observableConvert(Blackhole bh) { + return observableConvert.subscribeWith(new PerfConsumer(bh)); + } + + @Benchmark + public Object observableDedicated(Blackhole bh) { + return observableDedicated.subscribeWith(new PerfConsumer(bh)); + } +} diff --git a/src/jmh/java/io/reactivex/rxjava3/xmapz/ObservableSwitchMapSinglePerf.java b/src/jmh/java/io/reactivex/rxjava3/xmapz/ObservableSwitchMapSinglePerf.java new file mode 100644 index 0000000000..75aeb504f9 --- /dev/null +++ b/src/jmh/java/io/reactivex/rxjava3/xmapz/ObservableSwitchMapSinglePerf.java @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.xmapz; + +import java.util.Arrays; +import java.util.concurrent.TimeUnit; + +import org.openjdk.jmh.annotations.*; +import org.openjdk.jmh.infra.Blackhole; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.functions.Function; + +@BenchmarkMode(Mode.Throughput) +@Warmup(iterations = 5) +@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) +@OutputTimeUnit(TimeUnit.SECONDS) +@Fork(value = 1) +@State(Scope.Thread) +public class ObservableSwitchMapSinglePerf { + @Param({ "1", "10", "100", "1000", "10000", "100000", "1000000" }) + public int count; + + Observable<Integer> observableConvert; + + Observable<Integer> observableDedicated; + + Observable<Integer> observablePlain; + + @Setup + public void setup() { + Integer[] sourceArray = new Integer[count]; + Arrays.fill(sourceArray, 777); + + Observable<Integer> source = Observable.fromArray(sourceArray); + + observablePlain = source.switchMap(new Function<Integer, Observable<? extends Integer>>() { + @Override + public Observable<? extends Integer> apply(Integer v) { + return Observable.just(v); + } + }); + + observableConvert = source.switchMap(new Function<Integer, Observable<? extends Integer>>() { + @Override + public Observable<? extends Integer> apply(Integer v) { + return Single.just(v).toObservable(); + } + }); + + observableDedicated = source.switchMapSingle(new Function<Integer, Single<? extends Integer>>() { + @Override + public Single<? extends Integer> apply(Integer v) { + return Single.just(v); + } + }); + } + + @Benchmark + public Object observablePlain(Blackhole bh) { + return observablePlain.subscribeWith(new PerfConsumer(bh)); + } + + @Benchmark + public Object observableConvert(Blackhole bh) { + return observableConvert.subscribeWith(new PerfConsumer(bh)); + } + + @Benchmark + public Object observableDedicated(Blackhole bh) { + return observableDedicated.subscribeWith(new PerfConsumer(bh)); + } +} diff --git a/src/main/java/io/reactivex/rxjava3/annotations/BackpressureKind.java b/src/main/java/io/reactivex/rxjava3/annotations/BackpressureKind.java new file mode 100644 index 0000000000..3b979a045d --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/annotations/BackpressureKind.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.annotations; + +/** + * Enumeration for various kinds of backpressure support. + * @since 2.0 + */ +public enum BackpressureKind { + /** + * The backpressure-related requests pass through this operator without change. + */ + PASS_THROUGH, + /** + * The operator fully supports backpressure and may coordinate downstream requests + * with upstream requests through batching, arbitration or by other means. + */ + FULL, + /** + * The operator performs special backpressure management; see the associated javadoc. + */ + SPECIAL, + /** + * The operator requests {@link Long#MAX_VALUE} from upstream but respects the backpressure + * of the downstream. + */ + UNBOUNDED_IN, + /** + * The operator will emit a {@link io.reactivex.rxjava3.exceptions.MissingBackpressureException MissingBackpressureException} + * if the downstream didn't request enough or in time. + */ + ERROR, + /** + * The operator ignores all kinds of backpressure and may overflow the downstream. + */ + NONE +} diff --git a/src/main/java/io/reactivex/rxjava3/annotations/BackpressureSupport.java b/src/main/java/io/reactivex/rxjava3/annotations/BackpressureSupport.java new file mode 100644 index 0000000000..b73a477fec --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/annotations/BackpressureSupport.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.annotations; + +import java.lang.annotation.*; + +/** + * Indicates the backpressure support kind of the associated operator or class. + * @since 2.0 + */ +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Target({ElementType.METHOD, ElementType.TYPE}) +public @interface BackpressureSupport { + /** + * The backpressure supported by this method or class. + * @return backpressure supported by this method or class. + */ + BackpressureKind value(); +} diff --git a/src/main/java/io/reactivex/rxjava3/annotations/Beta.java b/src/main/java/io/reactivex/rxjava3/annotations/Beta.java new file mode 100644 index 0000000000..ca75ea0b3b --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/annotations/Beta.java @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.annotations; + +/** + * Indicates the feature is in beta state: it will be most likely stay but + * the signature may change between versions without warning. + */ +public @interface Beta { + +} diff --git a/src/main/java/io/reactivex/rxjava3/annotations/CheckReturnValue.java b/src/main/java/io/reactivex/rxjava3/annotations/CheckReturnValue.java new file mode 100644 index 0000000000..02c7f2e0d9 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/annotations/CheckReturnValue.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.annotations; + +import java.lang.annotation.*; + +/** + * Marks methods whose return values should be checked. + * <p>History: 2.0.2 - experimental + * @since 2.1 + */ +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Target(ElementType.METHOD) +public @interface CheckReturnValue { + +} diff --git a/src/main/java/io/reactivex/rxjava3/annotations/Experimental.java b/src/main/java/io/reactivex/rxjava3/annotations/Experimental.java new file mode 100644 index 0000000000..061361f9b4 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/annotations/Experimental.java @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.annotations; + +/** + * Indicates the feature is in experimental state: its existence, signature or behavior + * might change without warning from one release to the next. + */ +public @interface Experimental { + +} diff --git a/src/main/java/io/reactivex/rxjava3/annotations/NonNull.java b/src/main/java/io/reactivex/rxjava3/annotations/NonNull.java new file mode 100644 index 0000000000..4495092fc7 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/annotations/NonNull.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.annotations; + +import static java.lang.annotation.ElementType.*; +import static java.lang.annotation.RetentionPolicy.CLASS; + +import java.lang.annotation.*; + +/** + * Indicates that a field/parameter/variable/type parameter/return type is never null. + */ +@Documented +@Target(value = {FIELD, METHOD, PARAMETER, LOCAL_VARIABLE, TYPE_PARAMETER, TYPE_USE}) +@Retention(value = CLASS) +public @interface NonNull { } + diff --git a/src/main/java/io/reactivex/rxjava3/annotations/Nullable.java b/src/main/java/io/reactivex/rxjava3/annotations/Nullable.java new file mode 100644 index 0000000000..a6af9eb1b5 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/annotations/Nullable.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.annotations; + +import static java.lang.annotation.ElementType.*; +import static java.lang.annotation.RetentionPolicy.CLASS; + +import java.lang.annotation.*; + +/** + * Indicates that a field/parameter/variable/type parameter/return type may be null. + */ +@Documented +@Target(value = {FIELD, METHOD, PARAMETER, LOCAL_VARIABLE, TYPE_PARAMETER, TYPE_USE}) +@Retention(value = CLASS) +public @interface Nullable { } + diff --git a/src/main/java/io/reactivex/rxjava3/annotations/SchedulerSupport.java b/src/main/java/io/reactivex/rxjava3/annotations/SchedulerSupport.java new file mode 100644 index 0000000000..0132b6a33d --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/annotations/SchedulerSupport.java @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.annotations; + +import java.lang.annotation.*; + +import io.reactivex.rxjava3.schedulers.Schedulers; + +/** + * Indicates what kind of scheduler the class or method uses. + * <p> + * Constants are provided for instances from {@link Schedulers} as well as values for + * {@linkplain #NONE not using a scheduler} and {@linkplain #CUSTOM a manually-specified scheduler}. + * Libraries providing their own values should namespace them with their base package name followed + * by a colon ({@code :}) and then a human-readable name (e.g., {@code com.example:ui-thread}). + * @since 2.0 + */ +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Target({ElementType.CONSTRUCTOR, ElementType.METHOD, ElementType.TYPE}) +public @interface SchedulerSupport { + /** + * A special value indicating the operator/class doesn't use schedulers. + */ + String NONE = "none"; + /** + * A special value indicating the operator/class requires a scheduler to be manually specified. + */ + String CUSTOM = "custom"; + + // Built-in schedulers: + /** + * The operator/class runs on RxJava's {@linkplain Schedulers#computation() computation + * scheduler} or takes timing information from it. + */ + String COMPUTATION = "io.reactivex:computation"; + /** + * The operator/class runs on RxJava's {@linkplain Schedulers#io() I/O scheduler} or takes + * timing information from it. + */ + String IO = "io.reactivex:io"; + /** + * The operator/class runs on RxJava's {@linkplain Schedulers#newThread() new thread scheduler} + * or takes timing information from it. + */ + String NEW_THREAD = "io.reactivex:new-thread"; + /** + * The operator/class runs on RxJava's {@linkplain Schedulers#trampoline() trampoline scheduler} + * or takes timing information from it. + */ + String TRAMPOLINE = "io.reactivex:trampoline"; + /** + * The operator/class runs on RxJava's {@linkplain Schedulers#single() single scheduler} + * or takes timing information from it. + * <p>History: 2.0.8 - experimental + * @since 2.2 + */ + String SINGLE = "io.reactivex:single"; + + /** + * The kind of scheduler the class or method uses. + * @return the name of the scheduler the class or method uses + */ + String value(); +} diff --git a/src/main/java/io/reactivex/rxjava3/annotations/package-info.java b/src/main/java/io/reactivex/rxjava3/annotations/package-info.java new file mode 100644 index 0000000000..a4daca2a32 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/annotations/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +/** + * Annotations for indicating operator behavior, API stability + * ({@link io.reactivex.rxjava3.annotations.Experimental @Experimental} and {@link io.reactivex.rxjava3.annotations.Beta @Beta}) and + * nullability indicators ({@link io.reactivex.rxjava3.annotations.Nullable Nullable} and {@link io.reactivex.rxjava3.annotations.NonNull NonNull}). + */ +package io.reactivex.rxjava3.annotations; diff --git a/src/main/java/io/reactivex/rxjava3/core/BackpressureOverflowStrategy.java b/src/main/java/io/reactivex/rxjava3/core/BackpressureOverflowStrategy.java new file mode 100644 index 0000000000..144dbb6807 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/core/BackpressureOverflowStrategy.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.core; + +/** + * Options to deal with buffer overflow when using onBackpressureBuffer. + */ +public enum BackpressureOverflowStrategy { + /** + * Signal a {@link io.reactivex.rxjava3.exceptions.MissingBackpressureException MissingBackpressureException} + * and terminate the sequence. + */ + ERROR, + /** Drop the oldest value from the buffer. */ + DROP_OLDEST, + /** Drop the latest value from the buffer. */ + DROP_LATEST +} diff --git a/src/main/java/io/reactivex/rxjava3/core/BackpressureStrategy.java b/src/main/java/io/reactivex/rxjava3/core/BackpressureStrategy.java new file mode 100644 index 0000000000..f2bf8d01ae --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/core/BackpressureStrategy.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.core; + +/** + * Represents the options for applying backpressure to a source sequence. + */ +public enum BackpressureStrategy { + /** + * The {@code onNext} events are written without any buffering or dropping. + * Downstream has to deal with any overflow. + * <p>Useful when one applies one of the custom-parameter onBackpressureXXX operators. + */ + MISSING, + /** + * Signals a {@link io.reactivex.rxjava3.exceptions.MissingBackpressureException MissingBackpressureException} + * in case the downstream can't keep up. + */ + ERROR, + /** + * Buffers <em>all</em> {@code onNext} values until the downstream consumes it. + */ + BUFFER, + /** + * Drops the most recent {@code onNext} value if the downstream can't keep up. + */ + DROP, + /** + * Keeps only the latest {@code onNext} value, overwriting any previous value if the + * downstream can't keep up. + */ + LATEST +} diff --git a/src/main/java/io/reactivex/rxjava3/core/Completable.java b/src/main/java/io/reactivex/rxjava3/core/Completable.java new file mode 100644 index 0000000000..e31a44c32c --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/core/Completable.java @@ -0,0 +1,3472 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.core; + +import java.util.*; +import java.util.concurrent.*; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.annotations.*; +import io.reactivex.rxjava3.disposables.*; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.functions.*; +import io.reactivex.rxjava3.internal.fuseable.*; +import io.reactivex.rxjava3.internal.jdk8.*; +import io.reactivex.rxjava3.internal.observers.*; +import io.reactivex.rxjava3.internal.operators.completable.*; +import io.reactivex.rxjava3.internal.operators.maybe.*; +import io.reactivex.rxjava3.internal.operators.mixed.*; +import io.reactivex.rxjava3.internal.operators.single.SingleDelayWithCompletable; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.schedulers.Schedulers; + +/** + * The {@code Completable} class represents a deferred computation without any value but + * only indication for completion or exception. + * <p> + * {@code Completable} behaves similarly to {@link Observable} except that it can only emit either + * a completion or error signal (there is no {@code onNext} or {@code onSuccess} as with the other + * reactive types). + * <p> + * The {@code Completable} class implements the {@link CompletableSource} base interface and the default consumer + * type it interacts with is the {@link CompletableObserver} via the {@link #subscribe(CompletableObserver)} method. + * The {@code Completable} operates with the following sequential protocol: + * <pre><code> + * onSubscribe (onError | onComplete)? + * </code></pre> + * <p> + * Note that as with the {@code Observable} protocol, {@code onError} and {@code onComplete} are mutually exclusive events. + * <p> + * Like {@code Observable}, a running {@code Completable} can be stopped through the {@link Disposable} instance + * provided to consumers through {@link CompletableObserver#onSubscribe}. + * <p> + * Like an {@code Observable}, a {@code Completable} is lazy, can be either "hot" or "cold", synchronous or + * asynchronous. {@code Completable} instances returned by the methods of this class are <em>cold</em> + * and there is a standard <em>hot</em> implementation in the form of a subject: + * {@link io.reactivex.rxjava3.subjects.CompletableSubject CompletableSubject}. + * <p> + * The documentation for this class makes use of marble diagrams. The following legend explains these diagrams: + * <p> + * <img width="640" height="577" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Completable.png" alt=""> + * <p> + * See {@link Flowable} or {@code Observable} for the + * implementation of the Reactive Pattern for a stream or vector of values. + * <p> + * Example: + * <pre><code> + * Disposable d = Completable.complete() + * .delay(10, TimeUnit.SECONDS, Schedulers.io()) + * .subscribeWith(new DisposableCompletableObserver() { + * @Override + * public void onStart() { + * System.out.println("Started"); + * } + * + * @Override + * public void onError(Throwable error) { + * error.printStackTrace(); + * } + * + * @Override + * public void onComplete() { + * System.out.println("Done!"); + * } + * }); + * + * Thread.sleep(5000); + * + * d.dispose(); + * </code></pre> + * <p> + * Note that by design, subscriptions via {@link #subscribe(CompletableObserver)} can't be disposed + * from the outside (hence the + * {@code void} return of the {@link #subscribe(CompletableObserver)} method) and it is the + * responsibility of the implementor of the {@code CompletableObserver} to allow this to happen. + * RxJava supports such usage with the standard + * {@link io.reactivex.rxjava3.observers.DisposableCompletableObserver DisposableCompletableObserver} instance. + * For convenience, the {@link #subscribeWith(CompletableObserver)} method is provided as well to + * allow working with a {@code CompletableObserver} (or subclass) instance to be applied with in + * a fluent manner (such as in the example above). + * + * @see io.reactivex.rxjava3.observers.DisposableCompletableObserver + */ +public abstract class Completable implements CompletableSource { + /** + * Returns a {@code Completable} which terminates as soon as one of the source {@code Completable}s + * terminates (normally or with an error) and disposes all other {@code Completable}s. + * <p> + * <img width="640" height="518" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Completable.ambArray.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code ambArray} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param sources the array of source {@code Completable}s. A subscription to each source will + * occur in the same order as in this array. + * @return the new {@code Completable} instance + * @throws NullPointerException if {@code sources} is {@code null} + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + @SafeVarargs + public static Completable ambArray(@NonNull CompletableSource... sources) { + Objects.requireNonNull(sources, "sources is null"); + if (sources.length == 0) { + return complete(); + } + if (sources.length == 1) { + return wrap(sources[0]); + } + + return RxJavaPlugins.onAssembly(new CompletableAmb(sources, null)); + } + + /** + * Returns a {@code Completable} which terminates as soon as one of the source {@code Completable}s in the {@link Iterable} sequence + * terminates (normally or with an error) and disposes all other {@code Completable}s. + * <p> + * <img width="640" height="518" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Completable.amb.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code amb} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param sources the {@code Iterable} of source {@code Completable}s. A subscription to each source will + * occur in the same order as in this {@code Iterable}. + * @return the new {@code Completable} instance + * @throws NullPointerException if {@code sources} is {@code null} + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public static Completable amb(@NonNull Iterable<@NonNull ? extends CompletableSource> sources) { + Objects.requireNonNull(sources, "sources is null"); + + return RxJavaPlugins.onAssembly(new CompletableAmb(null, sources)); + } + + /** + * Returns a {@code Completable} instance that completes immediately when subscribed to. + * <p> + * <img width="640" height="472" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Completable.complete.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code complete} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @return the shared {@code Completable} instance + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public static Completable complete() { + return RxJavaPlugins.onAssembly(CompletableEmpty.INSTANCE); + } + + /** + * Returns a {@code Completable} which completes only when all sources complete, one after another. + * <p> + * <img width="640" height="284" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Completable.concatArray.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code concatArray} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param sources the sources to concatenate + * @return the new {@code Completable} instance + * @throws NullPointerException if {@code sources} is {@code null} + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + @SafeVarargs + public static Completable concatArray(@NonNull CompletableSource... sources) { + Objects.requireNonNull(sources, "sources is null"); + if (sources.length == 0) { + return complete(); + } else + if (sources.length == 1) { + return wrap(sources[0]); + } + return RxJavaPlugins.onAssembly(new CompletableConcatArray(sources)); + } + + /** + * Returns a {@code Completable} which completes only when all sources complete, one after another. + * <p> + * <img width="640" height="324" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Completable.concatArrayDelayError.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code concatArrayDelayError} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param sources the sources to concatenate + * @return the new {@code Completable} instance + * @throws NullPointerException if {@code sources} is {@code null} + * @since 3.0.0 + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + @SafeVarargs + public static Completable concatArrayDelayError(@NonNull CompletableSource... sources) { + return Flowable.fromArray(sources).concatMapCompletableDelayError(Functions.identity(), true, 2); + } + + /** + * Returns a {@code Completable} which completes only when all sources complete, one after another. + * <p> + * <img width="640" height="303" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Completable.concat.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code concat} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param sources the sources to concatenate + * @return the new {@code Completable} instance + * @throws NullPointerException if {@code sources} is {@code null} + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public static Completable concat(@NonNull Iterable<@NonNull ? extends CompletableSource> sources) { + Objects.requireNonNull(sources, "sources is null"); + + return RxJavaPlugins.onAssembly(new CompletableConcatIterable(sources)); + } + + /** + * Returns a {@code Completable} which completes only when all sources complete, one after another. + * <p> + * <img width="640" height="238" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Completable.concat.p.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The returned {@code Completable} honors the backpressure of the downstream consumer + * and expects the other {@link Publisher} to honor it as well.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code concat} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param sources the sources to concatenate + * @return the new {@code Completable} instance + * @throws NullPointerException if {@code sources} is {@code null} + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @BackpressureSupport(BackpressureKind.FULL) + @NonNull + public static Completable concat(@NonNull Publisher<@NonNull ? extends CompletableSource> sources) { + return concat(sources, 2); + } + + /** + * Returns a {@code Completable} which completes only when all sources complete, one after another. + * <p> + * <img width="640" height="238" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Completable.concat.pn.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The returned {@code Completable} honors the backpressure of the downstream consumer + * and expects the other {@link Publisher} to honor it as well.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code concat} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param sources the sources to concatenate + * @param prefetch the number of sources to prefetch from the sources + * @return the new {@code Completable} instance + * @throws NullPointerException if {@code sources} is {@code null} + * @throws IllegalArgumentException if {@code prefetch} is non-positive + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + @BackpressureSupport(BackpressureKind.FULL) + public static Completable concat(@NonNull Publisher<@NonNull ? extends CompletableSource> sources, int prefetch) { + Objects.requireNonNull(sources, "sources is null"); + ObjectHelper.verifyPositive(prefetch, "prefetch"); + return RxJavaPlugins.onAssembly(new CompletableConcat(sources, prefetch)); + } + + /** + * Returns a {@code Completable} which completes only when all sources complete, one after another. + * <p> + * <img width="640" height="361" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Completable.concatDelayError.i.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code concatDelayError} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param sources the sources to concatenate + * @return the new {@code Completable} instance + * @throws NullPointerException if {@code sources} is {@code null} + * @since 3.0.0 + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public static Completable concatDelayError(@NonNull Iterable<@NonNull ? extends CompletableSource> sources) { + return Flowable.fromIterable(sources).concatMapCompletableDelayError(Functions.identity()); + } + + /** + * Returns a {@code Completable} which completes only when all sources complete, one after another. + * <p> + * <img width="640" height="396" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Completable.concatDelayError.p.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The returned {@code Completable} honors the backpressure of the downstream consumer + * and expects the other {@link Publisher} to honor it as well.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code concatDelayError} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param sources the sources to concatenate + * @return the new {@code Completable} instance + * @throws NullPointerException if {@code sources} is {@code null} + * @since 3.0.0 + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @BackpressureSupport(BackpressureKind.FULL) + @NonNull + public static Completable concatDelayError(@NonNull Publisher<@NonNull ? extends CompletableSource> sources) { + return concatDelayError(sources, 2); + } + + /** + * Returns a {@code Completable} which completes only when all sources complete, one after another. + * <p> + * <img width="640" height="359" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Completable.concatDelayError.pn.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The returned {@code Completable} honors the backpressure of the downstream consumer + * and expects the other {@link Publisher} to honor it as well.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code concatDelayError} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param sources the sources to concatenate + * @param prefetch the number of sources to prefetch from the sources + * @return the new {@code Completable} instance + * @throws NullPointerException if {@code sources} is {@code null} + * @throws IllegalArgumentException if {@code prefetch} is non-positive + * @since 3.0.0 + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + @BackpressureSupport(BackpressureKind.FULL) + public static Completable concatDelayError(@NonNull Publisher<@NonNull ? extends CompletableSource> sources, int prefetch) { + return Flowable.fromPublisher(sources).concatMapCompletableDelayError(Functions.identity(), true, prefetch); + } + + /** + * Provides an API (via a cold {@code Completable}) that bridges the reactive world with the callback-style world. + * <p> + * <img width="640" height="442" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Completable.create.png" alt=""> + * <p> + * Example: + * <pre><code> + * Completable.create(emitter -> { + * Callback listener = new Callback() { + * @Override + * public void onEvent(Event e) { + * emitter.onComplete(); + * } + * + * @Override + * public void onFailure(Exception e) { + * emitter.onError(e); + * } + * }; + * + * AutoCloseable c = api.someMethod(listener); + * + * emitter.setCancellable(c::close); + * + * }); + * </code></pre> + * <p> + * Whenever a {@link CompletableObserver} subscribes to the returned {@code Completable}, the provided + * {@link CompletableOnSubscribe} callback is invoked with a fresh instance of a {@link CompletableEmitter} + * that will interact only with that specific {@code CompletableObserver}. If this {@code CompletableObserver} + * disposes the flow (making {@link CompletableEmitter#isDisposed} return {@code true}), + * other observers subscribed to the same returned {@code Completable} are not affected. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code create} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param source the emitter that is called when a {@code CompletableObserver} subscribes to the returned {@code Completable} + * @return the new {@code Completable} instance + * @throws NullPointerException if {@code source} is {@code null} + * @see CompletableOnSubscribe + * @see Cancellable + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public static Completable create(@NonNull CompletableOnSubscribe source) { + Objects.requireNonNull(source, "source is null"); + return RxJavaPlugins.onAssembly(new CompletableCreate(source)); + } + + /** + * Compares two {@link CompletableSource}s and emits {@code true} via a {@link Single} if both complete. + * <p> + * <img width="640" height="187" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Completable.sequenceEqual.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code sequenceEqual} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param source1 the first {@code CompletableSource} instance + * @param source2 the second {@code CompletableSource} instance + * @return the new {@code Single} instance + * @throws NullPointerException if {@code source1} or {@code source2} is {@code null} + * @since 3.0.0 + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public static Single<Boolean> sequenceEqual(@NonNull CompletableSource source1, @NonNull CompletableSource source2) { // NOPMD + Objects.requireNonNull(source1, "source1 is null"); + Objects.requireNonNull(source2, "source2 is null"); + return mergeArrayDelayError(source1, source2).andThen(Single.just(true)); + } + + /** + * Constructs a {@code Completable} instance by wrapping the given source callback + * <strong>without any safeguards; you should manage the lifecycle and response + * to downstream disposal</strong>. + * <p> + * <img width="640" height="260" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Completable.unsafeCreate.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code unsafeCreate} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param onSubscribe the callback which will receive the {@link CompletableObserver} instances + * when the {@code Completable} is subscribed to. + * @return the new {@code Completable} instance + * @throws NullPointerException if {@code onSubscribe} is {@code null} + * @throws IllegalArgumentException if {@code source} is a {@code Completable} + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public static Completable unsafeCreate(@NonNull CompletableSource onSubscribe) { + Objects.requireNonNull(onSubscribe, "onSubscribe is null"); + if (onSubscribe instanceof Completable) { + throw new IllegalArgumentException("Use of unsafeCreate(Completable)!"); + } + return RxJavaPlugins.onAssembly(new CompletableFromUnsafeSource(onSubscribe)); + } + + /** + * Defers the subscription to a {@code Completable} instance returned by a supplier. + * <p> + * <img width="640" height="298" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Completable.defer.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code defer} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param supplier the supplier that returns the {@code Completable} that will be subscribed to. + * @return the new {@code Completable} instance + * @throws NullPointerException if {@code supplier} is {@code null} + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public static Completable defer(@NonNull Supplier<? extends @NonNull CompletableSource> supplier) { + Objects.requireNonNull(supplier, "supplier is null"); + return RxJavaPlugins.onAssembly(new CompletableDefer(supplier)); + } + + /** + * Creates a {@code Completable} which calls the given error supplier for each subscriber + * and emits its returned {@link Throwable}. + * <p> + * <img width="640" height="462" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Completable.error.f.png" alt=""> + * <p> + * If the {@code errorSupplier} returns {@code null}, the downstream {@link CompletableObserver}s will receive a + * {@link NullPointerException}. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code error} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param supplier the error supplier, not {@code null} + * @return the new {@code Completable} instance + * @throws NullPointerException if {@code supplier} is {@code null} + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public static Completable error(@NonNull Supplier<? extends @NonNull Throwable> supplier) { + Objects.requireNonNull(supplier, "supplier is null"); + return RxJavaPlugins.onAssembly(new CompletableErrorSupplier(supplier)); + } + + /** + * Creates a {@code Completable} instance that emits the given {@link Throwable} exception to subscribers. + * <p> + * <img width="640" height="462" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Completable.error.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code error} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param throwable the {@code Throwable} instance to emit, not {@code null} + * @return the new {@code Completable} instance + * @throws NullPointerException if {@code throwable} is {@code null} + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public static Completable error(@NonNull Throwable throwable) { + Objects.requireNonNull(throwable, "throwable is null"); + return RxJavaPlugins.onAssembly(new CompletableError(throwable)); + } + + /** + * Returns a {@code Completable} instance that runs the given {@link Action} for each {@link CompletableObserver} and + * emits either an exception or simply completes. + * <p> + * <img width="640" height="297" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Completable.fromAction.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code fromAction} does not operate by default on a particular {@link Scheduler}.</dd> + * <dt><b>Error handling:</b></dt> + * <dd> If the {@code Action} throws an exception, the respective {@link Throwable} is + * delivered to the downstream via {@link CompletableObserver#onError(Throwable)}, + * except when the downstream has disposed this {@code Completable} source. + * In this latter case, the {@code Throwable} is delivered to the global error handler via + * {@link RxJavaPlugins#onError(Throwable)} as an {@link io.reactivex.rxjava3.exceptions.UndeliverableException UndeliverableException}. + * </dd> + * </dl> + * @param action the {@code Action} to run for each subscribing {@code CompletableObserver} + * @return the new {@code Completable} instance + * @throws NullPointerException if {@code action} is {@code null} + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public static Completable fromAction(@NonNull Action action) { + Objects.requireNonNull(action, "action is null"); + return RxJavaPlugins.onAssembly(new CompletableFromAction(action)); + } + + /** + * Returns a {@code Completable} which when subscribed, executes the {@link Callable} function, ignores its + * normal result and emits {@code onError} or {@code onComplete} only. + * <p> + * <img width="640" height="286" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Completable.fromCallable.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code fromCallable} does not operate by default on a particular {@link Scheduler}.</dd> + * <dt><b>Error handling:</b></dt> + * <dd> If the {@code Callable} throws an exception, the respective {@link Throwable} is + * delivered to the downstream via {@link CompletableObserver#onError(Throwable)}, + * except when the downstream has disposed this {@code Completable} source. + * In this latter case, the {@code Throwable} is delivered to the global error handler via + * {@link RxJavaPlugins#onError(Throwable)} as an {@link io.reactivex.rxjava3.exceptions.UndeliverableException UndeliverableException}. + * </dd> + * </dl> + * @param callable the {@code Callable} instance to execute for each subscribing {@link CompletableObserver} + * @return the new {@code Completable} instance + * @throws NullPointerException if {@code callable} is {@code null} + * @see #defer(Supplier) + * @see #fromSupplier(Supplier) + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public static Completable fromCallable(@NonNull Callable<?> callable) { + Objects.requireNonNull(callable, "callable is null"); + return RxJavaPlugins.onAssembly(new CompletableFromCallable(callable)); + } + + /** + * Returns a {@code Completable} instance that reacts to the termination of the given {@link Future} in a blocking fashion. + * <p> + * <img width="640" height="628" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Completable.fromFuture.png" alt=""> + * <p> + * Note that disposing the {@code Completable} won't cancel the {@code Future}. + * Use {@link #doOnDispose(Action)} and call {@link Future#cancel(boolean)} in the + * {@link Action}. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code fromFuture} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param future the {@code Future} to react to + * @return the new {@code Completable} instance + * @throws NullPointerException if {@code future} is {@code null} + * @see #fromCompletionStage(CompletionStage) + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public static Completable fromFuture(@NonNull Future<?> future) { + Objects.requireNonNull(future, "future is null"); + return fromAction(Functions.futureAction(future)); + } + + /** + * Returns a {@code Completable} instance that when subscribed to, subscribes to the {@link MaybeSource} instance and + * emits an {@code onComplete} event if the maybe emits {@code onSuccess}/{@code onComplete} or forwards any + * {@code onError} events. + * <p> + * <img width="640" height="235" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Completable.fromMaybe.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code fromMaybe} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * <p>History: 2.1.17 - beta + * @param <T> the value type of the {@code MaybeSource} element + * @param maybe the {@code MaybeSource} instance to subscribe to, not {@code null} + * @return the new {@code Completable} instance + * @throws NullPointerException if {@code maybe} is {@code null} + * @since 2.2 + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T> Completable fromMaybe(@NonNull MaybeSource<T> maybe) { + Objects.requireNonNull(maybe, "maybe is null"); + return RxJavaPlugins.onAssembly(new MaybeIgnoreElementCompletable<>(maybe)); + } + + /** + * Returns a {@code Completable} instance that runs the given {@link Runnable} for each {@link CompletableObserver} and + * emits either its unchecked exception or simply completes. + * <p> + * <img width="640" height="297" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Completable.fromRunnable.png" alt=""> + * <p> + * If the code to be wrapped needs to throw a checked or more broader {@link Throwable} exception, that + * exception has to be converted to an unchecked exception by the wrapped code itself. Alternatively, + * use the {@link #fromAction(Action)} method which allows the wrapped code to throw any {@code Throwable} + * exception and will signal it to observers as-is. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code fromRunnable} does not operate by default on a particular {@link Scheduler}.</dd> + * <dt><b>Error handling:</b></dt> + * <dd> If the {@code Runnable} throws an exception, the respective {@code Throwable} is + * delivered to the downstream via {@link CompletableObserver#onError(Throwable)}, + * except when the downstream has disposed this {@code Completable} source. + * In this latter case, the {@code Throwable} is delivered to the global error handler via + * {@link RxJavaPlugins#onError(Throwable)} as an {@link io.reactivex.rxjava3.exceptions.UndeliverableException UndeliverableException}. + * </dd> + * </dl> + * @param run the {@code Runnable} to run for each {@code CompletableObserver} + * @return the new {@code Completable} instance + * @throws NullPointerException if {@code run} is {@code null} + * @see #fromAction(Action) + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public static Completable fromRunnable(@NonNull Runnable run) { + Objects.requireNonNull(run, "run is null"); + return RxJavaPlugins.onAssembly(new CompletableFromRunnable(run)); + } + + /** + * Returns a {@code Completable} instance that subscribes to the given {@link ObservableSource}, ignores all values and + * emits only the terminal event. + * <p> + * <img width="640" height="414" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Completable.fromObservable.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code fromObservable} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <T> the type of the {@code ObservableSource} + * @param observable the {@code ObservableSource} instance to subscribe to, not {@code null} + * @return the new {@code Completable} instance + * @throws NullPointerException if {@code observable} is {@code null} + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T> Completable fromObservable(@NonNull ObservableSource<T> observable) { + Objects.requireNonNull(observable, "observable is null"); + return RxJavaPlugins.onAssembly(new CompletableFromObservable<>(observable)); + } + + /** + * Returns a {@code Completable} instance that subscribes to the given {@link Publisher}, ignores all values and + * emits only the terminal event. + * <p> + * <img width="640" height="422" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Completable.fromPublisher.png" alt=""> + * <p> + * The {@code Publisher} must follow the + * <a href="https://github.com/reactive-streams/reactive-streams-jvm#reactive-streams">Reactive-Streams specification</a>. + * Violating the specification may result in undefined behavior. + * <p> + * If possible, use {@link #create(CompletableOnSubscribe)} to create a + * source-like {@code Completable} instead. + * <p> + * Note that even though {@code Publisher} appears to be a functional interface, it + * is not recommended to implement it through a lambda as the specification requires + * state management that is not achievable with a stateless lambda. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The returned {@code Completable} honors the backpressure of the downstream consumer + * and expects the other {@code Publisher} to honor it as well.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code fromPublisher} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <T> the type of the {@code Publisher} + * @param publisher the {@code Publisher} instance to subscribe to, not {@code null} + * @return the new {@code Completable} instance + * @throws NullPointerException if {@code publisher} is {@code null} + * @see #create(CompletableOnSubscribe) + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.UNBOUNDED_IN) + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T> Completable fromPublisher(@NonNull Publisher<T> publisher) { + Objects.requireNonNull(publisher, "publisher is null"); + return RxJavaPlugins.onAssembly(new CompletableFromPublisher<>(publisher)); + } + + /** + * Returns a {@code Completable} instance that when subscribed to, subscribes to the {@link SingleSource} instance and + * emits a completion event if the single emits {@code onSuccess} or forwards any {@code onError} events. + * <p> + * <img width="640" height="356" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Completable.fromSingle.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code fromSingle} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <T> the value type of the {@code SingleSource} + * @param single the {@code SingleSource} instance to subscribe to, not {@code null} + * @return the new {@code Completable} instance + * @throws NullPointerException if {@code single} is {@code null} + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T> Completable fromSingle(@NonNull SingleSource<T> single) { + Objects.requireNonNull(single, "single is null"); + return RxJavaPlugins.onAssembly(new CompletableFromSingle<>(single)); + } + + /** + * Returns a {@code Completable} which when subscribed, executes the {@link Supplier} function, ignores its + * normal result and emits {@code onError} or {@code onComplete} only. + * <p> + * <img width="640" height="286" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Completable.fromSupplier.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code fromSupplier} does not operate by default on a particular {@link Scheduler}.</dd> + * <dt><b>Error handling:</b></dt> + * <dd> If the {@code Supplier} throws an exception, the respective {@link Throwable} is + * delivered to the downstream via {@link CompletableObserver#onError(Throwable)}, + * except when the downstream has disposed this {@code Completable} source. + * In this latter case, the {@code Throwable} is delivered to the global error handler via + * {@link RxJavaPlugins#onError(Throwable)} as an {@link io.reactivex.rxjava3.exceptions.UndeliverableException UndeliverableException}. + * </dd> + * </dl> + * @param supplier the {@code Supplier} instance to execute for each {@link CompletableObserver} + * @return the new {@code Completable} instance + * @throws NullPointerException if {@code supplier} is {@code null} + * @see #defer(Supplier) + * @see #fromCallable(Callable) + * @since 3.0.0 + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public static Completable fromSupplier(@NonNull Supplier<?> supplier) { + Objects.requireNonNull(supplier, "supplier is null"); + return RxJavaPlugins.onAssembly(new CompletableFromSupplier(supplier)); + } + + /** + * Returns a {@code Completable} instance that subscribes to all sources at once and + * completes only when all source {@link CompletableSource}s complete or one of them emits an error. + * <p> + * <img width="640" height="270" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Completable.mergeArray.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code mergeArray} does not operate by default on a particular {@link Scheduler}.</dd> + * <dt><b>Error handling:</b></dt> + * <dd>If any of the source {@code CompletableSource}s signal a {@link Throwable} via {@code onError}, the resulting + * {@code Completable} terminates with that {@code Throwable} and all other source {@code CompletableSource}s are disposed. + * If more than one {@code CompletableSource} signals an error, the resulting {@code Completable} may terminate with the + * first one's error or, depending on the concurrency of the sources, may terminate with a + * {@link CompositeException} containing two or more of the various error signals. + * {@code Throwable}s that didn't make into the composite will be sent (individually) to the global error handler via + * {@link RxJavaPlugins#onError(Throwable)} method as {@link UndeliverableException} errors. Similarly, {@code Throwable}s + * signaled by source(s) after the returned {@code Completable} has been disposed or terminated with a + * (composite) error will be sent to the same global error handler. + * Use {@link #mergeArrayDelayError(CompletableSource...)} to merge sources and terminate only when all source {@code CompletableSource}s + * have completed or failed with an error. + * </dd> + * </dl> + * @param sources the array of {@code CompletableSource}s. + * @return the new {@code Completable} instance + * @throws NullPointerException if {@code sources} is {@code null} + * @see #mergeArrayDelayError(CompletableSource...) + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + @SafeVarargs + public static Completable mergeArray(@NonNull CompletableSource... sources) { + Objects.requireNonNull(sources, "sources is null"); + if (sources.length == 0) { + return complete(); + } else + if (sources.length == 1) { + return wrap(sources[0]); + } + return RxJavaPlugins.onAssembly(new CompletableMergeArray(sources)); + } + + /** + * Returns a {@code Completable} instance that subscribes to all sources at once and + * completes only when all source {@link CompletableSource}s complete or one of them emits an error. + * <p> + * <img width="640" height="311" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Completable.merge.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code merge} does not operate by default on a particular {@link Scheduler}.</dd> + * <dt><b>Error handling:</b></dt> + * <dd>If any of the source {@code CompletableSource}s signal a {@link Throwable} via {@code onError}, the resulting + * {@code Completable} terminates with that {@code Throwable} and all other source {@code CompletableSource}s are disposed. + * If more than one {@code CompletableSource} signals an error, the resulting {@code Completable} may terminate with the + * first one's error or, depending on the concurrency of the sources, may terminate with a + * {@link CompositeException} containing two or more of the various error signals. + * {@code Throwable}s that didn't make into the composite will be sent (individually) to the global error handler via + * {@link RxJavaPlugins#onError(Throwable)} method as {@link UndeliverableException} errors. Similarly, {@code Throwable}s + * signaled by source(s) after the returned {@code Completable} has been disposed or terminated with a + * (composite) error will be sent to the same global error handler. + * Use {@link #mergeDelayError(Iterable)} to merge sources and terminate only when all source {@code CompletableSource}s + * have completed or failed with an error. + * </dd> + * </dl> + * @param sources the {@link Iterable} sequence of {@code CompletableSource}s. + * @return the new {@code Completable} instance + * @throws NullPointerException if {@code sources} is {@code null} + * @see #mergeDelayError(Iterable) + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public static Completable merge(@NonNull Iterable<@NonNull ? extends CompletableSource> sources) { + Objects.requireNonNull(sources, "sources is null"); + return RxJavaPlugins.onAssembly(new CompletableMergeIterable(sources)); + } + + /** + * Returns a {@code Completable} instance that subscribes to all sources at once and + * completes only when all source {@link CompletableSource}s complete or one of them emits an error. + * <p> + * <img width="640" height="336" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Completable.merge.p.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator consumes the given {@link Publisher} in an unbounded manner + * (requesting {@link Long#MAX_VALUE} upfront).</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code merge} does not operate by default on a particular {@link Scheduler}.</dd> + * <dt><b>Error handling:</b></dt> + * <dd>If any of the source {@code CompletableSource}s signal a {@link Throwable} via {@code onError}, the resulting + * {@code Completable} terminates with that {@code Throwable} and all other source {@code CompletableSource}s are disposed. + * If more than one {@code CompletableSource} signals an error, the resulting {@code Completable} may terminate with the + * first one's error or, depending on the concurrency of the sources, may terminate with a + * {@link CompositeException} containing two or more of the various error signals. + * {@code Throwable}s that didn't make into the composite will be sent (individually) to the global error handler via + * {@link RxJavaPlugins#onError(Throwable)} method as {@link UndeliverableException} errors. Similarly, {@code Throwable}s + * signaled by source(s) after the returned {@code Completable} has been disposed or terminated with a + * (composite) error will be sent to the same global error handler. + * Use {@link #mergeDelayError(Publisher)} to merge sources and terminate only when all source {@code CompletableSource}s + * have completed or failed with an error. + * </dd> + * </dl> + * @param sources the {@code Publisher} sequence of {@code CompletableSource}s. + * @return the new {@code Completable} instance + * @throws NullPointerException if {@code sources} is {@code null} + * @see #mergeDelayError(Publisher) + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @BackpressureSupport(BackpressureKind.UNBOUNDED_IN) + @NonNull + public static Completable merge(@NonNull Publisher<@NonNull ? extends CompletableSource> sources) { + return merge0(sources, Integer.MAX_VALUE, false); + } + + /** + * Returns a {@code Completable} instance that keeps subscriptions to a limited number of sources at once and + * completes only when all source {@link CompletableSource}s complete or one of them emits an error. + * <p> + * <img width="640" height="269" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Completable.merge.pn.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator consumes the given {@link Publisher} in a bounded manner, + * requesting {@code maxConcurrency} items first, then keeps requesting as + * many more as the inner {@code CompletableSource}s terminate.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code merge} does not operate by default on a particular {@link Scheduler}.</dd> + * <dt><b>Error handling:</b></dt> + * <dd>If any of the source {@code CompletableSource}s signal a {@link Throwable} via {@code onError}, the resulting + * {@code Completable} terminates with that {@code Throwable} and all other source {@code CompletableSource}s are disposed. + * If more than one {@code CompletableSource} signals an error, the resulting {@code Completable} may terminate with the + * first one's error or, depending on the concurrency of the sources, may terminate with a + * {@link CompositeException} containing two or more of the various error signals. + * {@code Throwable}s that didn't make into the composite will be sent (individually) to the global error handler via + * {@link RxJavaPlugins#onError(Throwable)} method as {@link UndeliverableException} errors. Similarly, {@code Throwable}s + * signaled by source(s) after the returned {@code Completable} has been disposed or terminated with a + * (composite) error will be sent to the same global error handler. + * Use {@link #mergeDelayError(Publisher, int)} to merge sources and terminate only when all source {@code CompletableSource}s + * have completed or failed with an error. + * </dd> + * </dl> + * @param sources the {@code Publisher} sequence of {@code CompletableSource}s. + * @param maxConcurrency the maximum number of concurrent subscriptions + * @return the new {@code Completable} instance + * @throws NullPointerException if {@code sources} is {@code null} + * @throws IllegalArgumentException if {@code maxConcurrency} is less than 1 + * @see #mergeDelayError(Publisher, int) + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @BackpressureSupport(BackpressureKind.FULL) + @NonNull + public static Completable merge(@NonNull Publisher<@NonNull ? extends CompletableSource> sources, int maxConcurrency) { + return merge0(sources, maxConcurrency, false); + } + + /** + * Returns a {@code Completable} instance that keeps subscriptions to a limited number of {@link CompletableSource}s at once and + * completes only when all source {@code CompletableSource}s terminate in one way or another, combining any exceptions + * signaled by either the source {@link Publisher} or the inner {@code CompletableSource} instances. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator consumes the given {@code Publisher} in a bounded manner, + * requesting {@code maxConcurrency} items first, then keeps requesting as + * many more as the inner {@code CompletableSource}s terminate.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code merge0} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param sources the {@code Publisher} sequence of {@code CompletableSource}s. + * @param maxConcurrency the maximum number of concurrent subscriptions + * @param delayErrors delay all errors from the main source and from the inner {@code CompletableSource}s? + * @return the new {@code Completable} instance + * @throws NullPointerException if {@code sources} is {@code null} + * @throws IllegalArgumentException if {@code maxConcurrency} is less than 1 + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + @BackpressureSupport(BackpressureKind.FULL) + private static Completable merge0(@NonNull Publisher<@NonNull ? extends CompletableSource> sources, int maxConcurrency, boolean delayErrors) { + Objects.requireNonNull(sources, "sources is null"); + ObjectHelper.verifyPositive(maxConcurrency, "maxConcurrency"); + return RxJavaPlugins.onAssembly(new CompletableMerge(sources, maxConcurrency, delayErrors)); + } + + /** + * Returns a {@code Completable} that subscribes to all {@link CompletableSource}s in the source array and delays + * any error emitted by any of the inner {@code CompletableSource}s until all of + * them terminate in a way or another. + * <p> + * <img width="640" height="430" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Completable.mergeArrayDelayError.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code mergeArrayDelayError} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param sources the array of {@code CompletableSource}s + * @return the new {@code Completable} instance + * @throws NullPointerException if {@code sources} is {@code null} + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + @SafeVarargs + public static Completable mergeArrayDelayError(@NonNull CompletableSource... sources) { + Objects.requireNonNull(sources, "sources is null"); + return RxJavaPlugins.onAssembly(new CompletableMergeArrayDelayError(sources)); + } + + /** + * Returns a {@code Completable} that subscribes to all {@link CompletableSource}s in the source sequence and delays + * any error emitted by any of the inner {@code CompletableSource}s until all of + * them terminate in a way or another. + * <p> + * <img width="640" height="476" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Completable.mergeDelayError.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code mergeDelayError} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param sources the sequence of {@code CompletableSource}s + * @return the new {@code Completable} instance + * @throws NullPointerException if {@code sources} is {@code null} + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public static Completable mergeDelayError(@NonNull Iterable<@NonNull ? extends CompletableSource> sources) { + Objects.requireNonNull(sources, "sources is null"); + return RxJavaPlugins.onAssembly(new CompletableMergeDelayErrorIterable(sources)); + } + + /** + * Returns a {@code Completable} that subscribes to all {@link CompletableSource}s in the source sequence and delays + * any error emitted by either the sources {@link Publisher} or any of the inner {@code CompletableSource}s until all of + * them terminate in a way or another. + * <p> + * <img width="640" height="466" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Completable.mergeDelayError.p.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator consumes the {@code Publisher} in an unbounded manner + * (requesting {@link Long#MAX_VALUE} from it).</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code mergeDelayError} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param sources the sequence of {@code CompletableSource}s + * @return the new {@code Completable} instance + * @throws NullPointerException if {@code sources} is {@code null} + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @BackpressureSupport(BackpressureKind.UNBOUNDED_IN) + @NonNull + public static Completable mergeDelayError(@NonNull Publisher<@NonNull ? extends CompletableSource> sources) { + return merge0(sources, Integer.MAX_VALUE, true); + } + + /** + * Returns a {@code Completable} that subscribes to a limited number of inner {@link CompletableSource}s at once in + * the source sequence and delays any error emitted by either the sources + * {@link Publisher} or any of the inner {@code CompletableSource}s until all of + * them terminate in a way or another. + * <p> + * <img width="640" height="440" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Completable.mergeDelayError.pn.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator requests {@code maxConcurrency} items from the {@code Publisher} + * upfront and keeps requesting as many more as many inner {@code CompletableSource}s terminate.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code mergeDelayError} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param sources the sequence of {@code CompletableSource}s + * @param maxConcurrency the maximum number of concurrent subscriptions to have + * at a time to the inner {@code CompletableSource}s + * @return the new {@code Completable} instance + * @throws NullPointerException if {@code sources} is {@code null} + * @throws IllegalArgumentException if {@code maxConcurrency} is non-positive + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @BackpressureSupport(BackpressureKind.FULL) + @NonNull + public static Completable mergeDelayError(@NonNull Publisher<@NonNull ? extends CompletableSource> sources, int maxConcurrency) { + return merge0(sources, maxConcurrency, true); + } + + /** + * Returns a {@code Completable} that never calls {@code onError} or {@code onComplete}. + * <p> + * <img width="640" height="512" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Completable.never.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code never} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @return the singleton instance that never calls {@code onError} or {@code onComplete} + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public static Completable never() { + return RxJavaPlugins.onAssembly(CompletableNever.INSTANCE); + } + + /** + * Returns a {@code Completable} instance that fires its {@code onComplete} event after the given delay elapsed. + * <p> + * <img width="640" height="413" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Completable.timer.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code timer} does operate by default on the {@code computation} {@link Scheduler}.</dd> + * </dl> + * @param delay the delay time + * @param unit the delay unit + * @return the new {@code Completable} instance + * @throws NullPointerException if {@code unit} is {@code null} + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.COMPUTATION) + @NonNull + public static Completable timer(long delay, @NonNull TimeUnit unit) { + return timer(delay, unit, Schedulers.computation()); + } + + /** + * Returns a {@code Completable} instance that fires its {@code onComplete} event after the given delay elapsed + * by using the supplied {@link Scheduler}. + * <p> + * <img width="640" height="413" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Completable.timer.s.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code timer} operates on the {@code Scheduler} you specify.</dd> + * </dl> + * @param delay the delay time + * @param unit the delay unit + * @param scheduler the {@code Scheduler} where to emit the {@code onComplete} event + * @return the new {@code Completable} instance + * @throws NullPointerException if {@code unit} or {@code scheduler} is {@code null} + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.CUSTOM) + public static Completable timer(long delay, @NonNull TimeUnit unit, @NonNull Scheduler scheduler) { + Objects.requireNonNull(unit, "unit is null"); + Objects.requireNonNull(scheduler, "scheduler is null"); + return RxJavaPlugins.onAssembly(new CompletableTimer(delay, unit, scheduler)); + } + + /** + * Creates a {@link NullPointerException} instance and sets the given {@link Throwable} as its initial cause. + * @param ex the {@code Throwable} instance to use as cause, not {@code null} (not verified) + * @return the new {@code NullPointerException} + */ + private static NullPointerException toNpe(Throwable ex) { + NullPointerException npe = new NullPointerException("Actually not, but can't pass out an exception otherwise..."); + npe.initCause(ex); + return npe; + } + + /** + * Switches between {@link CompletableSource}s emitted by the source {@link Publisher} whenever + * a new {@code CompletableSource} is emitted, disposing the previously running {@code CompletableSource}, + * exposing the setup as a {@code Completable} sequence. + * <p> + * <img width="640" height="518" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Completable.switchOnNext.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The {@code sources} {@code Publisher} is consumed in an unbounded manner (requesting {@link Long#MAX_VALUE}).</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code switchOnNext} does not operate by default on a particular {@link Scheduler}.</dd> + * <dt><b>Error handling:</b></dt> + * <dd>The returned sequence fails with the first error signaled by the {@code sources} {@code Publisher} + * or the currently running {@code CompletableSource}, disposing the rest. Late errors are + * forwarded to the global error handler via {@link RxJavaPlugins#onError(Throwable)}.</dd> + * </dl> + * @param sources the {@code Publisher} sequence of inner {@code CompletableSource}s to switch between + * @return the new {@code Completable} instance + * @throws NullPointerException if {@code sources} is {@code null} + * @since 3.0.0 + * @see #switchOnNextDelayError(Publisher) + * @see <a href="http://reactivex.io/documentation/operators/switch.html">ReactiveX operators documentation: Switch</a> + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + @BackpressureSupport(BackpressureKind.UNBOUNDED_IN) + public static Completable switchOnNext(@NonNull Publisher<@NonNull ? extends CompletableSource> sources) { + Objects.requireNonNull(sources, "sources is null"); + return RxJavaPlugins.onAssembly(new FlowableSwitchMapCompletablePublisher<>(sources, Functions.identity(), false)); + } + + /** + * Switches between {@link CompletableSource}s emitted by the source {@link Publisher} whenever + * a new {@code CompletableSource} is emitted, disposing the previously running {@code CompletableSource}, + * exposing the setup as a {@code Completable} sequence and delaying all errors from + * all of them until all terminate. + * <p> + * <img width="640" height="415" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Completable.switchOnNextDelayError.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The {@code sources} {@code Publisher} is consumed in an unbounded manner (requesting {@link Long#MAX_VALUE}).</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code switchOnNextDelayError} does not operate by default on a particular {@link Scheduler}.</dd> + * <dt><b>Error handling:</b></dt> + * <dd>The returned {@code Completable} collects all errors emitted by either the {@code sources} + * {@code Publisher} or any inner {@code CompletableSource} and emits them as a {@link CompositeException} + * when all sources terminate. If only one source ever failed, its error is emitted as-is at the end.</dd> + * </dl> + * @param sources the {@code Publisher} sequence of inner {@code CompletableSource}s to switch between + * @return the new {@code Completable} instance + * @throws NullPointerException if {@code sources} is {@code null} + * @since 3.0.0 + * @see #switchOnNext(Publisher) + * @see <a href="http://reactivex.io/documentation/operators/switch.html">ReactiveX operators documentation: Switch</a> + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + @BackpressureSupport(BackpressureKind.UNBOUNDED_IN) + public static Completable switchOnNextDelayError(@NonNull Publisher<@NonNull ? extends CompletableSource> sources) { + Objects.requireNonNull(sources, "sources is null"); + return RxJavaPlugins.onAssembly(new FlowableSwitchMapCompletablePublisher<>(sources, Functions.identity(), true)); + } + + /** + * Returns a {@code Completable} instance which manages a resource along + * with a custom {@link CompletableSource} instance while the subscription is active. + * <p> + * <img width="640" height="389" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Completable.using.png" alt=""> + * <p> + * This overload disposes eagerly before the terminal event is emitted. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code using} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <R> the resource type + * @param resourceSupplier the {@link Supplier} that returns a resource to be managed. + * @param sourceSupplier the {@link Function} that given a resource returns a {@code CompletableSource} instance that will be subscribed to + * @param resourceCleanup the {@link Consumer} that disposes the resource created by the resource supplier + * @return the new {@code Completable} instance + * @throws NullPointerException if {@code resourceSupplier}, {@code sourceSupplier} + * or {@code resourceCleanup} is {@code null} + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public static <@NonNull R> Completable using(@NonNull Supplier<R> resourceSupplier, + @NonNull Function<? super R, ? extends CompletableSource> sourceSupplier, + @NonNull Consumer<? super R> resourceCleanup) { + return using(resourceSupplier, sourceSupplier, resourceCleanup, true); + } + + /** + * Returns a {@code Completable} instance which manages a resource along + * with a custom {@link CompletableSource} instance while the subscription is active and performs eager or lazy + * resource disposition. + * <p> + * <img width="640" height="332" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Completable.using.b.png" alt=""> + * <p> + * If this overload performs a lazy disposal after the terminal event is emitted. + * The exceptions thrown at this time will be delivered to the global {@link RxJavaPlugins#onError(Throwable)} handler only. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code using} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <R> the resource type + * @param resourceSupplier the {@link Supplier} that returns a resource to be managed + * @param sourceSupplier the {@link Function} that given a resource returns a non-{@code null} + * {@code CompletableSource} instance that will be subscribed to + * @param resourceCleanup the {@link Consumer} that disposes the resource created by the resource supplier + * @param eager + * If {@code true} then resource disposal will happen either on a {@code dispose()} call before the upstream is disposed + * or just before the emission of a terminal event ({@code onComplete} or {@code onError}). + * If {@code false} the resource disposal will happen either on a {@code dispose()} call after the upstream is disposed + * or just after the emission of a terminal event ({@code onComplete} or {@code onError}). + * @return the new {@code Completable} instance + * @throws NullPointerException if {@code resourceSupplier}, {@code sourceSupplier} + * or {@code resourceCleanup} is {@code null} + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull R> Completable using( + @NonNull Supplier<R> resourceSupplier, + @NonNull Function<? super R, ? extends CompletableSource> sourceSupplier, + @NonNull Consumer<? super R> resourceCleanup, + boolean eager) { + Objects.requireNonNull(resourceSupplier, "resourceSupplier is null"); + Objects.requireNonNull(sourceSupplier, "sourceSupplier is null"); + Objects.requireNonNull(resourceCleanup, "resourceCleanup is null"); + + return RxJavaPlugins.onAssembly(new CompletableUsing<>(resourceSupplier, sourceSupplier, resourceCleanup, eager)); + } + + /** + * Wraps the given {@link CompletableSource} into a {@code Completable} + * if not already {@code Completable}. + * <p> + * <img width="640" height="354" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Completable.wrap.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code wrap} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param source the source to wrap + * @return the new wrapped or cast {@code Completable} instance + * @throws NullPointerException if {@code source} is {@code null} + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public static Completable wrap(@NonNull CompletableSource source) { + Objects.requireNonNull(source, "source is null"); + if (source instanceof Completable) { + return RxJavaPlugins.onAssembly((Completable)source); + } + return RxJavaPlugins.onAssembly(new CompletableFromUnsafeSource(source)); + } + + /** + * Returns a {@code Completable} that emits the a terminated event of either this {@code Completable} + * or the other {@link CompletableSource}, whichever fires first. + * <p> + * <img width="640" height="485" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Completable.ambWith.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code ambWith} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param other the other {@code CompletableSource}, not {@code null}. A subscription to this provided source will occur after subscribing + * to the current source. + * @return the new {@code Completable} instance + * @throws NullPointerException if {@code other} is {@code null} + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public final Completable ambWith(@NonNull CompletableSource other) { + Objects.requireNonNull(other, "other is null"); + return ambArray(this, other); + } + + /** + * Returns an {@link Observable} which will subscribe to this {@code Completable} and once that is completed then + * will subscribe to the {@code next} {@link ObservableSource}. An error event from this {@code Completable} will be + * propagated to the downstream observer and will result in skipping the subscription to the + * next {@code ObservableSource}. + * <p> + * <img width="640" height="278" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Completable.andThen.o.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code andThen} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <T> the value type of the next {@code ObservableSource} + * @param next the {@code ObservableSource} to subscribe after this {@code Completable} is completed, not {@code null} + * @return the new {@code Observable} that composes this {@code Completable} and the next {@code ObservableSource} + * @throws NullPointerException if {@code next} is {@code null} + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public final <@NonNull T> Observable<T> andThen(@NonNull ObservableSource<T> next) { + Objects.requireNonNull(next, "next is null"); + return RxJavaPlugins.onAssembly(new CompletableAndThenObservable<>(this, next)); + } + + /** + * Returns a {@link Flowable} which will subscribe to this {@code Completable} and once that is completed then + * will subscribe to the {@code next} {@link Publisher}. An error event from this {@code Completable} will be + * propagated to the downstream subscriber and will result in skipping the subscription to the next + * {@code Publisher}. + * <p> + * <img width="640" height="249" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Completable.andThen.p.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The returned {@code Flowable} honors the backpressure of the downstream consumer + * and expects the other {@code Publisher} to honor it as well.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code andThen} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <T> the value type of the next {@code Publisher} + * @param next the {@code Publisher} to subscribe after this {@code Completable} is completed, not {@code null} + * @return the new {@code Flowable} that composes this {@code Completable} and the next {@code Publisher} + * @throws NullPointerException if {@code next} is {@code null} + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + public final <@NonNull T> Flowable<T> andThen(@NonNull Publisher<T> next) { + Objects.requireNonNull(next, "next is null"); + return RxJavaPlugins.onAssembly(new CompletableAndThenPublisher<>(this, next)); + } + + /** + * Returns a {@link Single} which will subscribe to this {@code Completable} and once that is completed then + * will subscribe to the {@code next} {@link SingleSource}. An error event from this {@code Completable} will be + * propagated to the downstream observer and will result in skipping the subscription to the next + * {@code SingleSource}. + * <p> + * <img width="640" height="437" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Completable.andThen.s.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code andThen} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T> the value type of the next {@code SingleSource} + * @param next the {@code SingleSource} to subscribe after this {@code Completable} is completed, not {@code null} + * @return the new {@code Single} that composes this {@code Completable} and the next {@code SingleSource} + * @throws NullPointerException if {@code next} is {@code null} + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public final <@NonNull T> Single<T> andThen(@NonNull SingleSource<T> next) { + Objects.requireNonNull(next, "next is null"); + return RxJavaPlugins.onAssembly(new SingleDelayWithCompletable<>(next, this)); + } + + /** + * Returns a {@link Maybe} which will subscribe to this {@code Completable} and once that is completed then + * will subscribe to the {@code next} {@link MaybeSource}. An error event from this {@code Completable} will be + * propagated to the downstream observer and will result in skipping the subscription to the next + * {@code MaybeSource}. + * <p> + * <img width="640" height="281" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Completable.andThen.m.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code andThen} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T> the value type of the next {@code MaybeSource} + * @param next the {@code MaybeSource} to subscribe after this {@code Completable} is completed, not {@code null} + * @return the new {@code Maybe} that composes this {@code Completable} and the next {@code MaybeSource} + * @throws NullPointerException if {@code next} is {@code null} + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public final <@NonNull T> Maybe<T> andThen(@NonNull MaybeSource<T> next) { + Objects.requireNonNull(next, "next is null"); + return RxJavaPlugins.onAssembly(new MaybeDelayWithCompletable<>(next, this)); + } + + /** + * Returns a {@code Completable} that first runs this {@code Completable} + * and then the other {@link CompletableSource}. An error event from this {@code Completable} will be + * propagated to the downstream observer and will result in skipping the subscription to the next + * {@code CompletableSource}. + * <p> + * <img width="640" height="437" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Completable.andThen.c.png" alt=""> + * <p> + * This is an alias for {@link #concatWith(CompletableSource)}. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code andThen} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param next the other {@code CompletableSource}, not {@code null} + * @return the new {@code Completable} instance + * @throws NullPointerException if {@code next} is {@code null} + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Completable andThen(@NonNull CompletableSource next) { + Objects.requireNonNull(next, "next is null"); + return RxJavaPlugins.onAssembly(new CompletableAndThenCompletable(this, next)); + } + + /** + * Subscribes to and awaits the termination of this {@code Completable} instance in a blocking manner and + * rethrows any exception emitted. + * <p> + * <img width="640" height="433" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Completable.blockingAwait.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code blockingAwait} does not operate by default on a particular {@link Scheduler}.</dd> + * <dt><b>Error handling:</b></dt> + * <dd>If the source signals an error, the operator wraps a checked {@link Exception} + * into {@link RuntimeException} and throws that. Otherwise, {@code RuntimeException}s and + * {@link Error}s are rethrown as they are.</dd> + * </dl> + * @throws RuntimeException wrapping an {@link InterruptedException} if the current thread is interrupted + */ + @SchedulerSupport(SchedulerSupport.NONE) + public final void blockingAwait() { + BlockingMultiObserver<Void> observer = new BlockingMultiObserver<>(); + subscribe(observer); + observer.blockingGet(); + } + + /** + * Subscribes to and awaits the termination of this {@code Completable} instance in a blocking manner + * with a specific timeout and rethrows any exception emitted within the timeout window. + * <p> + * <img width="640" height="348" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Completable.blockingAwait.t.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code blockingAwait} does not operate by default on a particular {@link Scheduler}.</dd> + * <dt><b>Error handling:</b></dt> + * <dd>If the source signals an error, the operator wraps a checked {@link Exception} + * into {@link RuntimeException} and throws that. Otherwise, {@code RuntimeException}s and + * {@link Error}s are rethrown as they are.</dd> + * </dl> + * @param timeout the timeout value + * @param unit the timeout unit + * @return {@code true} if the this {@code Completable} instance completed normally within the time limit, + * {@code false} if the timeout elapsed before this {@code Completable} terminated. + * @throws RuntimeException wrapping an {@link InterruptedException} if the current thread is interrupted + * @throws NullPointerException if {@code unit} is {@code null} + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + public final boolean blockingAwait(long timeout, @NonNull TimeUnit unit) { + Objects.requireNonNull(unit, "unit is null"); + BlockingMultiObserver<Void> observer = new BlockingMultiObserver<>(); + subscribe(observer); + return observer.blockingAwait(timeout, unit); + } + + /** + * Subscribes to the current {@code Completable} and <em>blocks the current thread</em> until it terminates. + * <p> + * <img width="640" height="346" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Completable.blockingSubscribe.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code blockingSubscribe} does not operate by default on a particular {@link Scheduler}.</dd> + * <dt><b>Error handling:</b></dt> + * <dd>If the current {@code Completable} signals an error, + * the {@link Throwable} is routed to the global error handler via {@link RxJavaPlugins#onError(Throwable)}. + * If the current thread is interrupted, an {@link InterruptedException} is routed to the same global error handler. + * </dd> + * </dl> + * @since 3.0.0 + * @see #blockingSubscribe(Action) + * @see #blockingSubscribe(Action, Consumer) + */ + @SchedulerSupport(SchedulerSupport.NONE) + public final void blockingSubscribe() { + blockingSubscribe(Functions.EMPTY_ACTION, Functions.ERROR_CONSUMER); + } + + /** + * Subscribes to the current {@code Completable} and calls given {@code onComplete} callback on the <em>current thread</em> + * when it completes normally. + * <p> + * <img width="640" height="351" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Completable.blockingSubscribe.a.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code blockingSubscribe} does not operate by default on a particular {@link Scheduler}.</dd> + * <dt><b>Error handling:</b></dt> + * <dd>If either the current {@code Completable} signals an error or {@code onComplete} throws, + * the respective {@link Throwable} is routed to the global error handler via {@link RxJavaPlugins#onError(Throwable)}. + * If the current thread is interrupted, an {@link InterruptedException} is routed to the same global error handler. + * </dd> + * </dl> + * @param onComplete the {@link Action} to call if the current {@code Completable} completes normally + * @throws NullPointerException if {@code onComplete} is {@code null} + * @since 3.0.0 + * @see #blockingSubscribe(Action, Consumer) + */ + @SchedulerSupport(SchedulerSupport.NONE) + public final void blockingSubscribe(@NonNull Action onComplete) { + blockingSubscribe(onComplete, Functions.ERROR_CONSUMER); + } + + /** + * Subscribes to the current {@code Completable} and calls the appropriate callback on the <em>current thread</em> + * when it terminates. + * <p> + * <img width="640" height="352" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Completable.blockingSubscribe.ac.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code blockingSubscribe} does not operate by default on a particular {@link Scheduler}.</dd> + * <dt><b>Error handling:</b></dt> + * <dd>If either {@code onComplete} or {@code onError} throw, the {@link Throwable} is routed to the + * global error handler via {@link RxJavaPlugins#onError(Throwable)}. + * If the current thread is interrupted, the {@code onError} consumer is called with an {@link InterruptedException}. + * </dd> + * </dl> + * @param onComplete the {@link Action} to call if the current {@code Completable} completes normally + * @param onError the {@link Consumer} to call if the current {@code Completable} signals an error + * @throws NullPointerException if {@code onComplete} or {@code onError} is {@code null} + * @since 3.0.0 + */ + @SchedulerSupport(SchedulerSupport.NONE) + public final void blockingSubscribe(@NonNull Action onComplete, @NonNull Consumer<? super Throwable> onError) { + Objects.requireNonNull(onComplete, "onComplete is null"); + Objects.requireNonNull(onError, "onError is null"); + BlockingMultiObserver<Void> observer = new BlockingMultiObserver<>(); + subscribe(observer); + observer.blockingConsume(Functions.emptyConsumer(), onError, onComplete); + } + + /** + * Subscribes to the current {@code Completable} and calls the appropriate {@link CompletableObserver} method on the <em>current thread</em>. + * <p> + * <img width="640" height="468" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Completable.blockingSubscribe.o.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code blockingSubscribe} does not operate by default on a particular {@link Scheduler}.</dd> + * <dt><b>Error handling:</b></dt> + * <dd>An {@code onError} signal is delivered to the {@link CompletableObserver#onError(Throwable)} method. + * If any of the {@code CompletableObserver}'s methods throw, the {@link RuntimeException} is propagated to the caller of this method. + * If the current thread is interrupted, an {@link InterruptedException} is delivered to {@code observer.onError}. + * </dd> + * </dl> + * @param observer the {@code CompletableObserver} to call methods on the current thread + * @throws NullPointerException if {@code observer} is {@code null} + * @since 3.0.0 + */ + @SchedulerSupport(SchedulerSupport.NONE) + public final void blockingSubscribe(@NonNull CompletableObserver observer) { + Objects.requireNonNull(observer, "observer is null"); + BlockingDisposableMultiObserver<Void> blockingObserver = new BlockingDisposableMultiObserver<>(); + observer.onSubscribe(blockingObserver); + subscribe(blockingObserver); + blockingObserver.blockingConsume(observer); + } + + /** + * Subscribes to this {@code Completable} only once, when the first {@link CompletableObserver} + * subscribes to the result {@code Completable}, caches its terminal event + * and relays/replays it to observers. + * <p> + * <img width="640" height="375" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Completable.cache.png" alt=""> + * <p> + * Note that this operator doesn't allow disposing the connection + * of the upstream source. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code cache} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * <p>History: 2.0.4 - experimental + * @return the new {@code Completable} instance + * @since 2.1 + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Completable cache() { + return RxJavaPlugins.onAssembly(new CompletableCache(this)); + } + + /** + * Calls the given transformer function with this instance and returns the function's resulting + * {@link CompletableSource} wrapped with {@link #wrap(CompletableSource)}. + * <p> + * <img width="640" height="625" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Completable.compose.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code compose} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param transformer the transformer function, not {@code null} + * @return the new {@code Completable} instance + * @throws NullPointerException if {@code transformer} is {@code null} + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Completable compose(@NonNull CompletableTransformer transformer) { + return wrap(Objects.requireNonNull(transformer, "transformer is null").apply(this)); + } + + /** + * Concatenates this {@code Completable} with another {@link CompletableSource}. + * An error event from this {@code Completable} will be + * propagated to the downstream observer and will result in skipping the subscription to the next + * {@code CompletableSource}. + * <p> + * <img width="640" height="317" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Completable.concatWith.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code concatWith} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param other the other {@code CompletableSource}, not {@code null} + * @return the new {@code Completable} which subscribes to this and then the other {@code CompletableSource} + * @throws NullPointerException if {@code other} is {@code null} + * @see #andThen(CompletableSource) + * @see #andThen(MaybeSource) + * @see #andThen(ObservableSource) + * @see #andThen(SingleSource) + * @see #andThen(Publisher) + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public final Completable concatWith(@NonNull CompletableSource other) { + Objects.requireNonNull(other, "other is null"); + return RxJavaPlugins.onAssembly(new CompletableAndThenCompletable(this, other)); + } + + /** + * Returns a {@code Completable} which delays the emission of the completion event by the given time. + * <p> + * <img width="640" height="344" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Completable.delay.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code delay} does operate by default on the {@code computation} {@link Scheduler}.</dd> + * </dl> + * @param time the delay time + * @param unit the delay unit + * @return the new {@code Completable} instance + * @throws NullPointerException if {@code unit} is {@code null} + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.COMPUTATION) + @NonNull + public final Completable delay(long time, @NonNull TimeUnit unit) { + return delay(time, unit, Schedulers.computation(), false); + } + + /** + * Returns a {@code Completable} which delays the emission of the completion event by the given time while + * running on the specified {@link Scheduler}. + * <p> + * <img width="640" height="313" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Completable.delay.s.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code delay} operates on the {@code Scheduler} you specify.</dd> + * </dl> + * @param time the delay time + * @param unit the delay unit + * @param scheduler the {@code Scheduler} to run the delayed completion on + * @return the new {@code Completable} instance + * @throws NullPointerException if {@code unit} or {@code scheduler} is {@code null} + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.CUSTOM) + @NonNull + public final Completable delay(long time, @NonNull TimeUnit unit, @NonNull Scheduler scheduler) { + return delay(time, unit, scheduler, false); + } + + /** + * Returns a {@code Completable} which delays the emission of the completion event, and optionally the error as well, by the given time while + * running on the specified {@link Scheduler}. + * <p> + * <img width="640" height="253" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Completable.delay.sb.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code delay} operates on the {@code Scheduler} you specify.</dd> + * </dl> + * @param time the delay time + * @param unit the delay unit + * @param scheduler the {@code Scheduler} to run the delayed completion on + * @param delayError delay the error emission as well? + * @return the new {@code Completable} instance + * @throws NullPointerException if {@code unit} or {@code scheduler} is {@code null} + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.CUSTOM) + public final Completable delay(long time, @NonNull TimeUnit unit, @NonNull Scheduler scheduler, boolean delayError) { + Objects.requireNonNull(unit, "unit is null"); + Objects.requireNonNull(scheduler, "scheduler is null"); + return RxJavaPlugins.onAssembly(new CompletableDelay(this, time, unit, scheduler, delayError)); + } + + /** + * Returns a {@code Completable} that delays the subscription to the upstream by a given amount of time. + * <p> + * <img width="640" height="475" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Completable.delaySubscription.t.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>This version of {@code delaySubscription} operates by default on the {@code computation} {@link Scheduler}.</dd> + * </dl> + * <p>History: 2.2.3 - experimental + * + * @param time the time to delay the subscription + * @param unit the time unit of {@code delay} + * @return the new {@code Completable} instance + * @throws NullPointerException if {@code unit} is {@code null} + * @since 3.0.0 + * @see <a href="http://reactivex.io/documentation/operators/delay.html">ReactiveX operators documentation: Delay</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.COMPUTATION) + @NonNull + public final Completable delaySubscription(long time, @NonNull TimeUnit unit) { + return delaySubscription(time, unit, Schedulers.computation()); + } + + /** + * Returns a {@code Completable} that delays the subscription to the upstream by a given amount of time, + * both waiting and subscribing on a given {@link Scheduler}. + * <p> + * <img width="640" height="420" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Completable.delaySubscription.ts.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>You specify which {@code Scheduler} this operator will use.</dd> + * </dl> + * <p>History: 2.2.3 - experimental + * @param time the time to delay the subscription + * @param unit the time unit of {@code delay} + * @param scheduler the {@code Scheduler} on which the waiting and subscription will happen + * @return the new {@code Completable} instance + * @throws NullPointerException if {@code unit} or {@code scheduler} is {@code null} + * @since 3.0.0 + * @see <a href="http://reactivex.io/documentation/operators/delay.html">ReactiveX operators documentation: Delay</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.CUSTOM) + @NonNull + public final Completable delaySubscription(long time, @NonNull TimeUnit unit, @NonNull Scheduler scheduler) { + return Completable.timer(time, unit, scheduler).andThen(this); + } + + /** + * Returns a {@code Completable} which calls the given {@code onComplete} {@link Action} if this {@code Completable} completes. + * <p> + * <img width="640" height="304" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Completable.doOnComplete.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code doOnComplete} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param onComplete the {@code Action} to call when this emits an {@code onComplete} event + * @return the new {@code Completable} instance + * @throws NullPointerException if {@code onComplete} is {@code null} + * @see #doFinally(Action) + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Completable doOnComplete(@NonNull Action onComplete) { + return doOnLifecycle(Functions.emptyConsumer(), Functions.emptyConsumer(), + onComplete, Functions.EMPTY_ACTION, + Functions.EMPTY_ACTION, Functions.EMPTY_ACTION); + } + + /** + * Calls the shared {@link Action} if a {@link CompletableObserver} subscribed to the current + * {@code Completable} disposes the common {@link Disposable} it received via {@code onSubscribe}. + * <p> + * <img width="640" height="589" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Completable.doOnDispose.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code doOnDispose} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param onDispose the {@code Action} to call when the downstream observer disposes the subscription + * @return the new {@code Completable} instance + * @throws NullPointerException if {@code onDispose} is {@code null} + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Completable doOnDispose(@NonNull Action onDispose) { + return doOnLifecycle(Functions.emptyConsumer(), Functions.emptyConsumer(), + Functions.EMPTY_ACTION, Functions.EMPTY_ACTION, + Functions.EMPTY_ACTION, onDispose); + } + + /** + * Returns a {@code Completable} which calls the given {@code onError} {@link Consumer} if this {@code Completable} emits an error. + * <p> + * <img width="640" height="304" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Completable.doOnError.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code doOnError} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param onError the error {@code Consumer} receiving the upstream {@link Throwable} if the upstream signals it via {@code onError} + * @return the new {@code Completable} instance + * @throws NullPointerException if {@code onError} is {@code null} + * @see #doFinally(Action) + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Completable doOnError(@NonNull Consumer<? super Throwable> onError) { + return doOnLifecycle(Functions.emptyConsumer(), onError, + Functions.EMPTY_ACTION, Functions.EMPTY_ACTION, + Functions.EMPTY_ACTION, Functions.EMPTY_ACTION); + } + + /** + * Returns a {@code Completable} which calls the given {@code onEvent} {@link Consumer} with the {@link Throwable} for an {@code onError} + * or {@code null} for an {@code onComplete} signal from this {@code Completable} before delivering the signal to the downstream. + * <p> + * <img width="640" height="305" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Completable.doOnEvent.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code doOnEvent} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param onEvent the event {@code Consumer} that receives {@code null} for upstream + * completion or a {@code Throwable} if the upstream signaled an error + * @return the new {@code Completable} instance + * @throws NullPointerException if {@code onEvent} is {@code null} + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public final Completable doOnEvent(@NonNull Consumer<@Nullable ? super Throwable> onEvent) { + Objects.requireNonNull(onEvent, "onEvent is null"); + return RxJavaPlugins.onAssembly(new CompletableDoOnEvent(this, onEvent)); + } + + /** + * Calls the appropriate {@code onXXX} method (shared between all {@link CompletableObserver}s) for the lifecycle events of + * the sequence (subscription, disposal). + * <p> + * <img width="640" height="257" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Completable.doOnLifecycle.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code doOnLifecycle} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param onSubscribe + * a {@link Consumer} called with the {@link Disposable} sent via {@link CompletableObserver#onSubscribe(Disposable)} + * @param onDispose + * called when the downstream disposes the {@code Disposable} via {@code dispose()} + * @return the new {@code Completable} instance + * @throws NullPointerException if {@code onSubscribe} or {@code onDispose} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/do.html">ReactiveX operators documentation: Do</a> + * @since 3.0.0 + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Completable doOnLifecycle(@NonNull Consumer<? super Disposable> onSubscribe, @NonNull Action onDispose) { + return doOnLifecycle(onSubscribe, Functions.emptyConsumer(), + Functions.EMPTY_ACTION, Functions.EMPTY_ACTION, + Functions.EMPTY_ACTION, onDispose); + } + + /** + * Returns a {@code Completable} instance that calls the various callbacks upon the specific + * lifecycle events. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code doOnLifecycle} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param onSubscribe the {@link Consumer} called when a {@link CompletableObserver} subscribes. + * @param onError the {@code Consumer} called when this emits an {@code onError} event + * @param onComplete the {@link Action} called just before when the current {@code Completable} completes normally + * @param onTerminate the {@code Action} called just before this {@code Completable} terminates + * @param onAfterTerminate the {@code Action} called after this {@code Completable} completes normally + * @param onDispose the {@code Action} called when the downstream disposes the subscription + * @return the new {@code Completable} instance + * @throws NullPointerException if {@code onSubscribe}, {@code onError}, {@code onComplete} + * {@code onTerminate}, {@code onAfterTerminate} or {@code onDispose} is {@code null} + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + private Completable doOnLifecycle( + final Consumer<? super Disposable> onSubscribe, + final Consumer<? super Throwable> onError, + final Action onComplete, + final Action onTerminate, + final Action onAfterTerminate, + final Action onDispose) { + Objects.requireNonNull(onSubscribe, "onSubscribe is null"); + Objects.requireNonNull(onError, "onError is null"); + Objects.requireNonNull(onComplete, "onComplete is null"); + Objects.requireNonNull(onTerminate, "onTerminate is null"); + Objects.requireNonNull(onAfterTerminate, "onAfterTerminate is null"); + Objects.requireNonNull(onDispose, "onDispose is null"); + return RxJavaPlugins.onAssembly(new CompletablePeek(this, onSubscribe, onError, onComplete, onTerminate, onAfterTerminate, onDispose)); + } + + /** + * Returns a {@code Completable} instance that calls the given {@code onSubscribe} callback with the disposable + * that the downstream {@link CompletableObserver}s receive upon subscription. + * <p> + * <img width="640" height="304" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Completable.doOnSubscribe.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code doOnSubscribe} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param onSubscribe the {@link Consumer} called when a downstream {@code CompletableObserver} subscribes + * @return the new {@code Completable} instance + * @throws NullPointerException if {@code onSubscribe} is {@code null} + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Completable doOnSubscribe(@NonNull Consumer<? super Disposable> onSubscribe) { + return doOnLifecycle(onSubscribe, Functions.emptyConsumer(), + Functions.EMPTY_ACTION, Functions.EMPTY_ACTION, + Functions.EMPTY_ACTION, Functions.EMPTY_ACTION); + } + + /** + * Returns a {@code Completable} instance that calls the given {@code onTerminate} {@link Action} just before this {@code Completable} + * completes normally or with an exception. + * <p> + * <img width="640" height="305" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Completable.doOnTerminate.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code doOnTerminate} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param onTerminate the {@code Action} to call just before this {@code Completable} terminates + * @return the new {@code Completable} instance + * @throws NullPointerException if {@code onTerminate} is {@code null} + * @see #doFinally(Action) + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Completable doOnTerminate(@NonNull Action onTerminate) { + return doOnLifecycle(Functions.emptyConsumer(), Functions.emptyConsumer(), + Functions.EMPTY_ACTION, onTerminate, + Functions.EMPTY_ACTION, Functions.EMPTY_ACTION); + } + + /** + * Returns a {@code Completable} instance that calls the given {@code onAfterTerminate} {@link Action} after this {@code Completable} + * completes normally or with an exception. + * <p> + * <img width="640" height="305" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Completable.doAfterTerminate.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code doAfterTerminate} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param onAfterTerminate the {@code Action} to call after this {@code Completable} terminates + * @return the new {@code Completable} instance + * @throws NullPointerException if {@code onAfterTerminate} is {@code null} + * @see #doFinally(Action) + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Completable doAfterTerminate(@NonNull Action onAfterTerminate) { + return doOnLifecycle( + Functions.emptyConsumer(), + Functions.emptyConsumer(), + Functions.EMPTY_ACTION, + Functions.EMPTY_ACTION, + onAfterTerminate, + Functions.EMPTY_ACTION); + } + /** + * Calls the specified {@link Action} after this {@code Completable} signals {@code onError} or {@code onComplete} or gets disposed by + * the downstream. + * <p> + * <img width="640" height="331" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Completable.doFinally.png" alt=""> + * <p> + * In case of a race between a terminal event and a dispose call, the provided {@code onFinally} action + * is executed once per subscription. + * <p> + * Note that the {@code onFinally} action is shared between subscriptions and as such + * should be thread-safe. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code doFinally} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * <p>History: 2.0.1 - experimental + * @param onFinally the {@code Action} called when this {@code Completable} terminates or gets disposed + * @return the new {@code Completable} instance + * @throws NullPointerException if {@code onFinally} is {@code null} + * @since 2.1 + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public final Completable doFinally(@NonNull Action onFinally) { + Objects.requireNonNull(onFinally, "onFinally is null"); + return RxJavaPlugins.onAssembly(new CompletableDoFinally(this, onFinally)); + } + + /** + * <strong>This method requires advanced knowledge about building operators, please consider + * other standard composition methods first;</strong> + * Returns a {@code Completable} which, when subscribed to, invokes the {@link CompletableOperator#apply(CompletableObserver) apply(CompletableObserver)} method + * of the provided {@link CompletableOperator} for each individual downstream {@link Completable} and allows the + * insertion of a custom operator by accessing the downstream's {@link CompletableObserver} during this subscription phase + * and providing a new {@code CompletableObserver}, containing the custom operator's intended business logic, that will be + * used in the subscription process going further upstream. + * <p> + * <img width="640" height="313" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Completable.lift.png" alt=""> + * <p> + * Generally, such a new {@code CompletableObserver} will wrap the downstream's {@code CompletableObserver} and forwards the + * {@code onError} and {@code onComplete} events from the upstream directly or according to the + * emission pattern the custom operator's business logic requires. In addition, such operator can intercept the + * flow control calls of {@code dispose} and {@code isDisposed} that would have traveled upstream and perform + * additional actions depending on the same business logic requirements. + * <p> + * Example: + * <pre><code> + * // Step 1: Create the consumer type that will be returned by the CompletableOperator.apply(): + * + * public final class CustomCompletableObserver implements CompletableObserver, Disposable { + * + * // The downstream's CompletableObserver that will receive the onXXX events + * final CompletableObserver downstream; + * + * // The connection to the upstream source that will call this class' onXXX methods + * Disposable upstream; + * + * // The constructor takes the downstream subscriber and usually any other parameters + * public CustomCompletableObserver(CompletableObserver downstream) { + * this.downstream = downstream; + * } + * + * // In the subscription phase, the upstream sends a Disposable to this class + * // and subsequently this class has to send a Disposable to the downstream. + * // Note that relaying the upstream's Disposable directly is not allowed in RxJava + * @Override + * public void onSubscribe(Disposable d) { + * if (upstream != null) { + * d.dispose(); + * } else { + * upstream = d; + * downstream.onSubscribe(this); + * } + * } + * + * // Some operators may handle the upstream's error while others + * // could just forward it to the downstream. + * @Override + * public void onError(Throwable throwable) { + * downstream.onError(throwable); + * } + * + * // When the upstream completes, usually the downstream should complete as well. + * // In completable, this could also mean doing some side-effects + * @Override + * public void onComplete() { + * System.out.println("Sequence completed"); + * downstream.onComplete(); + * } + * + * // Some operators may use their own resources which should be cleaned up if + * // the downstream disposes the flow before it completed. Operators without + * // resources can simply forward the dispose to the upstream. + * // In some cases, a disposed flag may be set by this method so that other parts + * // of this class may detect the dispose and stop sending events + * // to the downstream. + * @Override + * public void dispose() { + * upstream.dispose(); + * } + * + * // Some operators may simply forward the call to the upstream while others + * // can return the disposed flag set in dispose(). + * @Override + * public boolean isDisposed() { + * return upstream.isDisposed(); + * } + * } + * + * // Step 2: Create a class that implements the CompletableOperator interface and + * // returns the custom consumer type from above in its apply() method. + * // Such class may define additional parameters to be submitted to + * // the custom consumer type. + * + * final class CustomCompletableOperator implements CompletableOperator { + * @Override + * public CompletableObserver apply(CompletableObserver upstream) { + * return new CustomCompletableObserver(upstream); + * } + * } + * + * // Step 3: Apply the custom operator via lift() in a flow by creating an instance of it + * // or reusing an existing one. + * + * Completable.complete() + * .lift(new CustomCompletableOperator()) + * .test() + * .assertResult(); + * </code></pre> + * <p> + * Creating custom operators can be complicated and it is recommended one consults the + * <a href="https://github.com/ReactiveX/RxJava/wiki/Writing-operators-for-2.0">RxJava wiki: Writing operators</a> page about + * the tools, requirements, rules, considerations and pitfalls of implementing them. + * <p> + * Note that implementing custom operators via this {@code lift()} method adds slightly more overhead by requiring + * an additional allocation and indirection per assembled flows. Instead, extending the abstract {@code Completable} + * class and creating a {@link CompletableTransformer} with it is recommended. + * <p> + * Note also that it is not possible to stop the subscription phase in {@code lift()} as the {@code apply()} method + * requires a non-{@code null} {@code CompletableObserver} instance to be returned, which is then unconditionally subscribed to + * the current {@code Completable}. For example, if the operator decided there is no reason to subscribe to the + * upstream source because of some optimization possibility or a failure to prepare the operator, it still has to + * return a {@code CompletableObserver} that should immediately dispose the upstream's {@link Disposable} in its + * {@code onSubscribe} method. Again, using a {@code CompletableTransformer} and extending the {@code Completable} is + * a better option as {@link #subscribeActual} can decide to not subscribe to its upstream after all. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code lift} does not operate by default on a particular {@link Scheduler}, however, the + * {@code CompletableOperator} may use a {@code Scheduler} to support its own asynchronous behavior.</dd> + * </dl> + * + * @param onLift the {@code CompletableOperator} that receives the downstream's {@code CompletableObserver} and should return + * a {@code CompletableObserver} with custom behavior to be used as the consumer for the current + * {@code Completable}. + * @return the new {@code Completable} instance + * @throws NullPointerException if {@code onLift} is {@code null} + * @see <a href="https://github.com/ReactiveX/RxJava/wiki/Writing-operators-for-2.0">RxJava wiki: Writing operators</a> + * @see #compose(CompletableTransformer) + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public final Completable lift(@NonNull CompletableOperator onLift) { + Objects.requireNonNull(onLift, "onLift is null"); + return RxJavaPlugins.onAssembly(new CompletableLift(this, onLift)); + } + + /** + * Maps the signal types of this {@code Completable} into a {@link Notification} of the same kind + * and emits it as a single success value to downstream. + * <p> + * <img width="640" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/materialize.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code materialize} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * <p>History: 2.2.4 - experimental + * @param <T> the intended target element type of the {@code Notification} + * @return the new {@link Single} instance + * @since 3.0.0 + * @see Single#dematerialize(Function) + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull T> Single<Notification<T>> materialize() { + return RxJavaPlugins.onAssembly(new CompletableMaterialize<>(this)); + } + + /** + * Returns a {@code Completable} which subscribes to this and the other {@link CompletableSource} and completes + * when both of them complete or one emits an error. + * <p> + * <img width="640" height="442" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Completable.mergeWith.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code mergeWith} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param other the other {@code CompletableSource} instance + * @return the new {@code Completable} instance + * @throws NullPointerException if {@code other} is {@code null} + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public final Completable mergeWith(@NonNull CompletableSource other) { + Objects.requireNonNull(other, "other is null"); + return mergeArray(this, other); + } + + /** + * Returns a {@code Completable} which emits the terminal events from the thread of the specified {@link Scheduler}. + * <p> + * <img width="640" height="523" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Completable.observeOn.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code observeOn} operates on a {@code Scheduler} you specify.</dd> + * </dl> + * @param scheduler the {@code Scheduler} to emit terminal events on + * @return the new {@code Completable} instance + * @throws NullPointerException if {@code scheduler} is {@code null} + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.CUSTOM) + public final Completable observeOn(@NonNull Scheduler scheduler) { + Objects.requireNonNull(scheduler, "scheduler is null"); + return RxJavaPlugins.onAssembly(new CompletableObserveOn(this, scheduler)); + } + + /** + * Returns a {@code Completable} instance that if this {@code Completable} emits an error, it will emit an {@code onComplete} + * and swallow the upstream {@link Throwable}. + * <p> + * <img width="640" height="585" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Completable.onErrorComplete.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code onErrorComplete} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @return the new {@code Completable} instance + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Completable onErrorComplete() { + return onErrorComplete(Functions.alwaysTrue()); + } + + /** + * Returns a {@code Completable} instance that if this {@code Completable} emits an error and the {@link Predicate} returns + * {@code true}, it will emit an {@code onComplete} and swallow the {@link Throwable}. + * <p> + * <img width="640" height="283" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Completable.onErrorComplete.f.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code onErrorComplete} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param predicate the {@code Predicate} to call when a {@code Throwable} is emitted which should return {@code true} + * if the {@code Throwable} should be swallowed and replaced with an {@code onComplete}. + * @return the new {@code Completable} instance + * @throws NullPointerException if {@code predicate} is {@code null} + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public final Completable onErrorComplete(@NonNull Predicate<? super Throwable> predicate) { + Objects.requireNonNull(predicate, "predicate is null"); + + return RxJavaPlugins.onAssembly(new CompletableOnErrorComplete(this, predicate)); + } + + /** + * Returns a {@code Completable} instance that when encounters an error from this {@code Completable}, calls the + * specified {@code mapper} {@link Function} that returns a {@link CompletableSource} instance for it and resumes the + * execution with it. + * <p> + * <img width="640" height="426" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Completable.onErrorResumeNext.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code onErrorResumeNext} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param fallbackSupplier the {@code mapper} {@code Function} that takes the error and should return a {@code CompletableSource} as + * continuation. + * @return the new {@code Completable} instance + * @throws NullPointerException if {@code fallbackSupplier} is {@code null} + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public final Completable onErrorResumeNext(@NonNull Function<? super Throwable, ? extends CompletableSource> fallbackSupplier) { + Objects.requireNonNull(fallbackSupplier, "fallbackSupplier is null"); + return RxJavaPlugins.onAssembly(new CompletableResumeNext(this, fallbackSupplier)); + } + /** + * Resumes the flow with the given {@link CompletableSource} when the current {@code Completable} fails instead of + * signaling the error via {@code onError}. + * <p> + * <img width="640" height="409" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Completable.onErrorResumeWith.png" alt=""> + * <p> + * You can use this to prevent errors from propagating or to supply fallback data should errors be + * encountered. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code onErrorResumeWith} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param fallback + * the next {@code CompletableSource} that will take over if the current {@code Completable} encounters + * an error + * @return the new {@code Completable} instance + * @throws NullPointerException if {@code fallback} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/catch.html">ReactiveX operators documentation: Catch</a> + * @since 3.0.0 + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public final Completable onErrorResumeWith(@NonNull CompletableSource fallback) { + Objects.requireNonNull(fallback, "fallback is null"); + return onErrorResumeNext(Functions.justFunction(fallback)); + } + + /** + * Ends the flow with a success item returned by a function for the {@link Throwable} error signaled by the current + * {@code Completable} instead of signaling the error via {@code onError}. + * <p> + * <img width="640" height="567" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Completable.onErrorReturn.png" alt=""> + * <p> + * You can use this to prevent errors from propagating or to supply fallback data should errors be + * encountered. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code onErrorReturn} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T> the item type to return on error + * @param itemSupplier + * a function that returns a single value that will be emitted as success value + * the current {@code Completable} signals an {@code onError} event + * @return the new {@link Maybe} instance + * @throws NullPointerException if {@code itemSupplier} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/catch.html">ReactiveX operators documentation: Catch</a> + * @since 3.0.0 + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public final <@NonNull T> Maybe<T> onErrorReturn(@NonNull Function<? super Throwable, ? extends T> itemSupplier) { + Objects.requireNonNull(itemSupplier, "itemSupplier is null"); + return RxJavaPlugins.onAssembly(new CompletableOnErrorReturn<>(this, itemSupplier)); + } + + /** + * Ends the flow with the given success item when the current {@code Completable} + * fails instead of signaling the error via {@code onError}. + * <p> + * <img width="640" height="567" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Completable.onErrorReturnItem.png" alt=""> + * <p> + * You can use this to prevent errors from propagating or to supply fallback data should errors be + * encountered. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code onErrorReturnItem} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T> the item type to return on error + * @param item + * the value that is emitted as {@code onSuccess} in case the current {@code Completable} signals an {@code onError} + * @return the new {@link Maybe} instance + * @throws NullPointerException if {@code item} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/catch.html">ReactiveX operators documentation: Catch</a> + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public final <@NonNull T> Maybe<T> onErrorReturnItem(@NonNull T item) { + Objects.requireNonNull(item, "item is null"); + return onErrorReturn(Functions.justFunction(item)); + } + + /** + * Nulls out references to the upstream producer and downstream {@link CompletableObserver} if + * the sequence is terminated or downstream calls {@code dispose()}. + * <p> + * <img width="640" height="326" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Completable.onTerminateDetach.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code onTerminateDetach} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * <p>History: 2.1.5 - experimental + * @return the new {@code Completable} instance + * @since 2.2 + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Completable onTerminateDetach() { + return RxJavaPlugins.onAssembly(new CompletableDetach(this)); + } + + /** + * Returns a {@code Completable} that repeatedly subscribes to this {@code Completable} until disposed. + * <p> + * <img width="640" height="373" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Completable.repeat.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code repeat} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @return the new {@code Completable} instance + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Completable repeat() { + return fromPublisher(toFlowable().repeat()); + } + + /** + * Returns a {@code Completable} that subscribes repeatedly at most the given number of times to this {@code Completable}. + * <p> + * <img width="640" height="408" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Completable.repeat.n.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code repeat} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param times the number of times the re-subscription should happen + * @return the new {@code Completable} instance + * @throws IllegalArgumentException if {@code times} is negative + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Completable repeat(long times) { + return fromPublisher(toFlowable().repeat(times)); + } + + /** + * Returns a {@code Completable} that repeatedly subscribes to this {@code Completable} so long as the given + * stop {@link BooleanSupplier} returns {@code false}. + * <p> + * <img width="640" height="381" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Completable.repeatUntil.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code repeatUntil} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param stop the {@code BooleanSupplier} that should return {@code true} to stop resubscribing. + * @return the new {@code Completable} instance + * @throws NullPointerException if {@code stop} is {@code null} + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Completable repeatUntil(@NonNull BooleanSupplier stop) { + return fromPublisher(toFlowable().repeatUntil(stop)); + } + + /** + * Returns a {@code Completable} instance that repeats when the {@link Publisher} returned by the handler {@link Function} + * emits an item or completes when this {@code Publisher} emits an {@code onComplete} event. + * <p> + * <img width="640" height="586" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Completable.repeatWhen.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code repeatWhen} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param handler the {@code Function} that transforms the stream of values indicating the completion of + * this {@code Completable} and returns a {@code Publisher} that emits items for repeating or completes to indicate the + * repetition should stop + * @return the new {@code Completable} instance + * @throws NullPointerException if {@code handler} is {@code null} + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Completable repeatWhen(@NonNull Function<? super Flowable<Object>, @NonNull ? extends Publisher<@NonNull ?>> handler) { + return fromPublisher(toFlowable().repeatWhen(handler)); + } + + /** + * Returns a {@code Completable} that retries this {@code Completable} as long as it emits an {@code onError} event. + * <p> + * <img width="640" height="368" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Completable.retry.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code retry} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @return the new {@code Completable} instance + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Completable retry() { + return fromPublisher(toFlowable().retry()); + } + + /** + * Returns a {@code Completable} that retries this {@code Completable} in case of an error as long as the {@code predicate} + * returns {@code true}. + * <p> + * <img width="640" height="325" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Completable.retry.ff.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code retry} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param predicate the {@link Predicate} called when this {@code Completable} emits an error with the repeat count and the latest {@link Throwable} + * and should return {@code true} to retry. + * @return the new {@code Completable} instance + * @throws NullPointerException if {@code predicate} is {@code null} + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Completable retry(@NonNull BiPredicate<? super Integer, ? super Throwable> predicate) { + return fromPublisher(toFlowable().retry(predicate)); + } + + /** + * Returns a {@code Completable} that when this {@code Completable} emits an error, retries at most the given + * number of times before giving up and emitting the last error. + * <p> + * <img width="640" height="451" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Completable.retry.n.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code retry} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param times the number of times to resubscribe if the current {@code Completable} fails + * @return the new {@code Completable} instance + * @throws IllegalArgumentException if {@code times} is negative + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Completable retry(long times) { + return fromPublisher(toFlowable().retry(times)); + } + + /** + * Returns a {@code Completable} that when this {@code Completable} emits an error, retries at most times + * or until the predicate returns {@code false}, whichever happens first and emitting the last error. + * <p> + * <img width="640" height="361" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Completable.retry.nf.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code retry} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * <p>History: 2.1.8 - experimental + * @param times the number of times to resubscribe if the current {@code Completable} fails + * @param predicate the {@link Predicate} that is called with the latest {@link Throwable} and should return + * {@code true} to indicate the returned {@code Completable} should resubscribe to this {@code Completable}. + * @return the new {@code Completable} instance + * @throws NullPointerException if {@code predicate} is {@code null} + * @throws IllegalArgumentException if {@code times} is negative + * @since 2.2 + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Completable retry(long times, @NonNull Predicate<? super Throwable> predicate) { + return fromPublisher(toFlowable().retry(times, predicate)); + } + + /** + * Returns a {@code Completable} that when this {@code Completable} emits an error, calls the given predicate with + * the latest {@link Throwable} to decide whether to resubscribe to the upstream or not. + * <p> + * <img width="640" height="336" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Completable.retry.f.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code retry} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param predicate the {@link Predicate} that is called with the latest {@code Throwable} and should return + * {@code true} to indicate the returned {@code Completable} should resubscribe to this {@code Completable}. + * @return the new {@code Completable} instance + * @throws NullPointerException if {@code predicate} is {@code null} + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Completable retry(@NonNull Predicate<? super Throwable> predicate) { + return fromPublisher(toFlowable().retry(predicate)); + } + + /** + * Retries until the given stop function returns {@code true}. + * <p> + * <img width="640" height="354" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Completable.retryUntil.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code retryUntil} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param stop the function that should return {@code true} to stop retrying + * @return the new {@code Completable} instance + * @throws NullPointerException if {@code stop} is {@code null} + * @since 3.0.0 + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public final Completable retryUntil(@NonNull BooleanSupplier stop) { + Objects.requireNonNull(stop, "stop is null"); + return retry(Long.MAX_VALUE, Functions.predicateReverseFor(stop)); + } + + /** + * Returns a {@code Completable} which given a {@link Publisher} and when this {@code Completable} emits an error, delivers + * that error through a {@link Flowable} and the {@code Publisher} should signal a value indicating a retry in response + * or a terminal event indicating a termination. + * <p> + * <img width="640" height="586" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Completable.retryWhen.png" alt=""> + * <p> + * Note that the inner {@code Publisher} returned by the handler function should signal + * either {@code onNext}, {@code onError} or {@code onComplete} in response to the received + * {@link Throwable} to indicate the operator should retry or terminate. If the upstream to + * the operator is asynchronous, signaling {@code onNext} followed by {@code onComplete} immediately may + * result in the sequence to be completed immediately. Similarly, if this inner + * {@code Publisher} signals {@code onError} or {@code onComplete} while the upstream is + * active, the sequence is terminated with the same signal immediately. + * <p> + * The following example demonstrates how to retry an asynchronous source with a delay: + * <pre><code> + * Completable.timer(1, TimeUnit.SECONDS) + * .doOnSubscribe(s -> System.out.println("subscribing")) + * .doOnComplete(() -> { throw new RuntimeException(); }) + * .retryWhen(errors -> { + * AtomicInteger counter = new AtomicInteger(); + * return errors + * .takeWhile(e -> counter.getAndIncrement() != 3) + * .flatMap(e -> { + * System.out.println("delay retry by " + counter.get() + " second(s)"); + * return Flowable.timer(counter.get(), TimeUnit.SECONDS); + * }); + * }) + * .blockingAwait(); + * </code></pre> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code retryWhen} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param handler the {@link Function} that receives a {@code Flowable} delivering {@code Throwable}s and should return a {@code Publisher} that + * emits items to indicate retries or emits terminal events to indicate termination. + * @return the new {@code Completable} instance + * @throws NullPointerException if {@code handler} is {@code null} + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Completable retryWhen(@NonNull Function<? super Flowable<Throwable>, @NonNull ? extends Publisher<@NonNull ?>> handler) { + return fromPublisher(toFlowable().retryWhen(handler)); + } + + /** + * Wraps the given {@link CompletableObserver}, catches any {@link RuntimeException}s thrown by its + * {@link CompletableObserver#onSubscribe(Disposable)}, {@link CompletableObserver#onError(Throwable)} + * or {@link CompletableObserver#onComplete()} methods and routes those to the global + * error handler via {@link RxJavaPlugins#onError(Throwable)}. + * <p> + * By default, the {@code Completable} protocol forbids the {@code onXXX} methods to throw, but some + * {@code CompletableObserver} implementation may do it anyway, causing undefined behavior in the + * upstream. This method and the underlying safe wrapper ensures such misbehaving consumers don't + * disrupt the protocol. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code safeSubscribe} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param observer the potentially misbehaving {@code CompletableObserver} + * @throws NullPointerException if {@code observer} is {@code null} + * @see #subscribe(Action, Consumer) + * @since 3.0.0 + */ + @SchedulerSupport(SchedulerSupport.NONE) + public final void safeSubscribe(@NonNull CompletableObserver observer) { + Objects.requireNonNull(observer, "observer is null"); + subscribe(new SafeCompletableObserver(observer)); + } + + /** + * Returns a {@code Completable} which first runs the other {@link CompletableSource} + * then the current {@code Completable} if the other completed normally. + * <p> + * <img width="640" height="437" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Completable.startWith.c.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code startWith} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param other the other {@code CompletableSource} to run first + * @return the new {@code Completable} instance + * @throws NullPointerException if {@code other} is {@code null} + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public final Completable startWith(@NonNull CompletableSource other) { + Objects.requireNonNull(other, "other is null"); + return concatArray(other, this); + } + + /** + * Returns a {@link Flowable} which first runs the other {@link SingleSource} + * then the current {@code Completable} if the other succeeded normally. + * <p> + * <img width="640" height="388" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Completable.startWith.s.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The returned {@code Flowable} honors the backpressure of the downstream consumer.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code startWith} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <T> the element type of the {@code other} {@code SingleSource}. + * @param other the other {@code SingleSource} to run first + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code other} is {@code null} + * @since 3.0.0 + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + @BackpressureSupport(BackpressureKind.FULL) + public final <@NonNull T> Flowable<T> startWith(@NonNull SingleSource<T> other) { + Objects.requireNonNull(other, "other is null"); + return Flowable.concat(Single.wrap(other).toFlowable(), toFlowable()); + } + + /** + * Returns a {@link Flowable} which first runs the other {@link MaybeSource} + * then the current {@code Completable} if the other succeeded or completed normally. + * <p> + * <img width="640" height="266" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Completable.startWith.m.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The returned {@code Flowable} honors the backpressure of the downstream consumer.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code startWith} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <T> the element type of the {@code other} {@code MaybeSource}. + * @param other the other {@code MaybeSource} to run first + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code other} is {@code null} + * @since 3.0.0 + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + @BackpressureSupport(BackpressureKind.FULL) + public final <@NonNull T> Flowable<T> startWith(@NonNull MaybeSource<T> other) { + Objects.requireNonNull(other, "other is null"); + return Flowable.concat(Maybe.wrap(other).toFlowable(), toFlowable()); + } + + /** + * Returns an {@link Observable} which first delivers the events + * of the other {@link ObservableSource} then runs the current {@code Completable}. + * <p> + * <img width="640" height="289" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Completable.startWith.o.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code startWith} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <T> the value type + * @param other the other {@code ObservableSource} to run first + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code other} is {@code null} + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public final <@NonNull T> Observable<T> startWith(@NonNull ObservableSource<T> other) { + Objects.requireNonNull(other, "other is null"); + return Observable.wrap(other).concatWith(this.toObservable()); + } + + /** + * Returns a {@link Flowable} which first delivers the events + * of the other {@link Publisher} then runs the current {@code Completable}. + * <p> + * <img width="640" height="250" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Completable.startWith.p.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The returned {@code Flowable} honors the backpressure of the downstream consumer + * and expects the other {@code Publisher} to honor it as well.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code startWith} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <T> the value type + * @param other the other {@code Publisher} to run first + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code other} is {@code null} + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + public final <@NonNull T> Flowable<T> startWith(@NonNull Publisher<T> other) { + Objects.requireNonNull(other, "other is null"); + return this.<T>toFlowable().startWith(other); + } + + /** + * Hides the identity of this {@code Completable} and its {@link Disposable}. + * <p> + * <img width="640" height="432" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Completable.hide.png" alt=""> + * <p> + * Allows preventing certain identity-based optimizations (fusion). + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code hide} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * <p>History: 2.0.5 - experimental + * @return the new {@code Completable} instance + * @since 2.1 + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Completable hide() { + return RxJavaPlugins.onAssembly(new CompletableHide(this)); + } + + /** + * Subscribes to this {@code Completable} and returns a {@link Disposable} which can be used to dispose + * the subscription. + * <p> + * <img width="640" height="352" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Completable.subscribe.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code subscribe} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @return the new {@code Disposable} that can be used for disposing the subscription at any time + * @see #subscribe(Action, Consumer, DisposableContainer) + */ + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Disposable subscribe() { + EmptyCompletableObserver observer = new EmptyCompletableObserver(); + subscribe(observer); + return observer; + } + + @SchedulerSupport(SchedulerSupport.NONE) + @Override + public final void subscribe(@NonNull CompletableObserver observer) { + Objects.requireNonNull(observer, "observer is null"); + try { + + observer = RxJavaPlugins.onSubscribe(this, observer); + + Objects.requireNonNull(observer, "The RxJavaPlugins.onSubscribe hook returned a null CompletableObserver. Please check the handler provided to RxJavaPlugins.setOnCompletableSubscribe for invalid null returns. Further reading: https://github.com/ReactiveX/RxJava/wiki/Plugins"); + + subscribeActual(observer); + } catch (NullPointerException ex) { // NOPMD + throw ex; + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + RxJavaPlugins.onError(ex); + throw toNpe(ex); + } + } + + /** + * Implement this method to handle the incoming {@link CompletableObserver}s and + * perform the business logic in your operator. + * <p>There is no need to call any of the plugin hooks on the current {@code Completable} instance or + * the {@code CompletableObserver}; all hooks and basic safeguards have been + * applied by {@link #subscribe(CompletableObserver)} before this method gets called. + * @param observer the {@code CompletableObserver} instance, never {@code null} + */ + protected abstract void subscribeActual(@NonNull CompletableObserver observer); + + /** + * Subscribes a given {@link CompletableObserver} (subclass) to this {@code Completable} and returns the given + * {@code CompletableObserver} as is. + * <p> + * <img width="640" height="349" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Completable.subscribeWith.png" alt=""> + * <p>Usage example: + * <pre><code> + * Completable source = Completable.complete().delay(1, TimeUnit.SECONDS); + * CompositeDisposable composite = new CompositeDisposable(); + * + * DisposableCompletableObserver ds = new DisposableCompletableObserver() { + * // ... + * }; + * + * composite.add(source.subscribeWith(ds)); + * </code></pre> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code subscribeWith} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <E> the type of the {@code CompletableObserver} to use and return + * @param observer the {@code CompletableObserver} (subclass) to use and return, not {@code null} + * @return the input {@code observer} + * @throws NullPointerException if {@code observer} is {@code null} + * @since 2.0 + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull E extends CompletableObserver> E subscribeWith(E observer) { + subscribe(observer); + return observer; + } + + /** + * Subscribes to this {@code Completable} and calls back either the {@code onError} or {@code onComplete} functions. + * <p> + * <img width="640" height="352" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Completable.subscribe.ff.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code subscribe} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param onComplete the {@link Action} that is called if the {@code Completable} completes normally + * @param onError the {@link Consumer} that is called if this {@code Completable} emits an error + * @return the new {@link Disposable} that can be used for disposing the subscription at any time + * @throws NullPointerException if {@code onComplete} or {@code onError} is {@code null} + * @see #subscribe(Action, Consumer, DisposableContainer) + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public final Disposable subscribe(@NonNull Action onComplete, @NonNull Consumer<? super Throwable> onError) { + Objects.requireNonNull(onError, "onError is null"); + Objects.requireNonNull(onComplete, "onComplete is null"); + + CallbackCompletableObserver observer = new CallbackCompletableObserver(onError, onComplete); + subscribe(observer); + return observer; + } + + /** + * Wraps the given onXXX callbacks into a {@link Disposable} {@link CompletableObserver}, + * adds it to the given {@link DisposableContainer} and ensures, that if the upstream + * terminates or this particular {@code Disposable} is disposed, the {@code CompletableObserver} is removed + * from the given composite. + * <p> + * The {@code CompletableObserver} will be removed after the callback for the terminal event has been invoked. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code subscribe} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param onError the callback for an upstream error + * @param onComplete the callback for an upstream completion + * @param container the {@code DisposableContainer} (such as {@link CompositeDisposable}) to add and remove the + * created {@code Disposable} {@code CompletableObserver} + * @return the {@code Disposable} that allows disposing the particular subscription. + * @throws NullPointerException + * if {@code onComplete}, {@code onError} + * or {@code container} is {@code null} + * @since 3.1.0 + */ + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Disposable subscribe( + @NonNull Action onComplete, + @NonNull Consumer<? super Throwable> onError, + @NonNull DisposableContainer container) { + Objects.requireNonNull(onComplete, "onComplete is null"); + Objects.requireNonNull(onError, "onError is null"); + Objects.requireNonNull(container, "container is null"); + + DisposableAutoReleaseMultiObserver<Void> observer = new DisposableAutoReleaseMultiObserver<>( + container, Functions.emptyConsumer(), onError, onComplete); + container.add(observer); + subscribe(observer); + return observer; + } + + /** + * Subscribes to this {@code Completable} and calls the given {@link Action} when this {@code Completable} + * completes normally. + * <p> + * <img width="640" height="352" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Completable.subscribe.f.png" alt=""> + * <p> + * If the {@code Completable} emits an error, it is wrapped into an + * {@link OnErrorNotImplementedException} + * and routed to the global {@link RxJavaPlugins#onError(Throwable)} handler. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code subscribe} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param onComplete the {@code Action} called when this {@code Completable} completes normally + * @return the new {@link Disposable} that can be used for disposing the subscription at any time + * @throws NullPointerException if {@code onComplete} is {@code null} + * @see #subscribe(Action, Consumer, DisposableContainer) + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public final Disposable subscribe(@NonNull Action onComplete) { + return subscribe(onComplete, Functions.ON_ERROR_MISSING); + } + + /** + * Returns a {@code Completable} which subscribes the downstream subscriber on the specified scheduler, making + * sure the subscription side-effects happen on that specific thread of the {@link Scheduler}. + * <p> + * <img width="640" height="686" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Completable.subscribeOn.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code subscribeOn} operates on a {@code Scheduler} you specify.</dd> + * </dl> + * @param scheduler the {@code Scheduler} to subscribe on + * @return the new {@code Completable} instance + * @throws NullPointerException if {@code scheduler} is {@code null} + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.CUSTOM) + public final Completable subscribeOn(@NonNull Scheduler scheduler) { + Objects.requireNonNull(scheduler, "scheduler is null"); + + return RxJavaPlugins.onAssembly(new CompletableSubscribeOn(this, scheduler)); + } + + /** + * Terminates the downstream if this or the other {@code Completable} + * terminates (wins the termination race) while disposing the connection to the losing source. + * <p> + * <img width="640" height="468" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Completable.takeuntil.c.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code takeUntil} does not operate by default on a particular {@link Scheduler}.</dd> + * <dt><b>Error handling:</b></dt> + * <dd>If both this and the other sources signal an error, only one of the errors + * is signaled to the downstream and the other error is signaled to the global + * error handler via {@link RxJavaPlugins#onError(Throwable)}.</dd> + * </dl> + * <p>History: 2.1.17 - experimental + * @param other the other completable source to observe for the terminal signals + * @return the new {@code Completable} instance + * @throws NullPointerException if {@code other} is {@code null} + * @since 2.2 + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public final Completable takeUntil(@NonNull CompletableSource other) { + Objects.requireNonNull(other, "other is null"); + + return RxJavaPlugins.onAssembly(new CompletableTakeUntilCompletable(this, other)); + } + + /** + * Returns a {@code Completabl}e that runs this {@code Completable} and emits a {@link TimeoutException} in case + * this {@code Completable} doesn't complete within the given time. + * <p> + * <img width="640" height="348" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Completable.timeout.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code timeout} signals the {@code TimeoutException} on the {@code computation} {@link Scheduler}.</dd> + * </dl> + * @param timeout the timeout value + * @param unit the unit of {@code timeout} + * @return the new {@code Completable} instance + * @throws NullPointerException if {@code unit} is {@code null} + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.COMPUTATION) + @NonNull + public final Completable timeout(long timeout, @NonNull TimeUnit unit) { + return timeout0(timeout, unit, Schedulers.computation(), null); + } + + /** + * Returns a {@code Completable} that runs this {@code Completable} and switches to the other {@link CompletableSource} + * in case this {@code Completable} doesn't complete within the given time. + * <p> + * <img width="640" height="308" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Completable.timeout.c.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code timeout} subscribes to the other {@code CompletableSource} on + * the {@code computation} {@link Scheduler}.</dd> + * </dl> + * @param timeout the timeout value + * @param unit the unit of {@code timeout} + * @param fallback the other {@code CompletableSource} instance to switch to in case of a timeout + * @return the new {@code Completable} instance + * @throws NullPointerException if {@code unit} or {@code fallback} is {@code null} + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.COMPUTATION) + public final Completable timeout(long timeout, @NonNull TimeUnit unit, @NonNull CompletableSource fallback) { + Objects.requireNonNull(fallback, "fallback is null"); + return timeout0(timeout, unit, Schedulers.computation(), fallback); + } + + /** + * Returns a {@code Completable} that runs this {@code Completable} and emits a {@link TimeoutException} in case + * this {@code Completable} doesn't complete within the given time while "waiting" on the specified + * {@link Scheduler}. + * <p> + * <img width="640" height="348" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Completable.timeout.s.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code timeout} signals the {@code TimeoutException} on the {@code Scheduler} you specify.</dd> + * </dl> + * @param timeout the timeout value + * @param unit the unit of {@code timeout} + * @param scheduler the {@code Scheduler} to use to wait for completion and signal {@code TimeoutException} + * @return the new {@code Completable} instance + * @throws NullPointerException if {@code unit} or {@code scheduler} is {@code null} + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.CUSTOM) + @NonNull + public final Completable timeout(long timeout, @NonNull TimeUnit unit, @NonNull Scheduler scheduler) { + return timeout0(timeout, unit, scheduler, null); + } + + /** + * Returns a {@code Completable} that runs this {@code Completable} and switches to the other {@link CompletableSource} + * in case this {@code Completable} doesn't complete within the given time while "waiting" on + * the specified {@link Scheduler}. + * <p> + * <img width="640" height="308" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Completable.timeout.sc.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code timeout} subscribes to the other {@code CompletableSource} on + * the {@code Scheduler} you specify.</dd> + * </dl> + * @param timeout the timeout value + * @param unit the unit of {@code timeout} + * @param scheduler the {@code Scheduler} to use to wait for completion + * @param fallback the other {@code Completable} instance to switch to in case of a timeout + * @return the new {@code Completable} instance + * @throws NullPointerException if {@code unit}, {@code scheduler} or {@code fallback} is {@code null} + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.CUSTOM) + public final Completable timeout(long timeout, @NonNull TimeUnit unit, @NonNull Scheduler scheduler, @NonNull CompletableSource fallback) { + Objects.requireNonNull(fallback, "fallback is null"); + return timeout0(timeout, unit, scheduler, fallback); + } + + /** + * Returns a {@code Completable} that runs this {@code Completable} and optionally switches to the other {@link CompletableSource} + * in case this {@code Completable} doesn't complete within the given time while "waiting" on + * the specified {@link Scheduler}. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>You specify the {@code Scheduler} this operator runs on.</dd> + * </dl> + * @param timeout the timeout value + * @param unit the unit of {@code timeout} + * @param scheduler the {@code Scheduler} to use to wait for completion + * @param fallback the other {@code Completable} instance to switch to in case of a timeout, + * if {@code null} a {@link TimeoutException} is emitted instead + * @return the new {@code Completable} instance + * @throws NullPointerException if {@code unit}, {@code scheduler} or {@code fallback} is {@code null} + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.CUSTOM) + private Completable timeout0(long timeout, TimeUnit unit, Scheduler scheduler, CompletableSource fallback) { + Objects.requireNonNull(unit, "unit is null"); + Objects.requireNonNull(scheduler, "scheduler is null"); + return RxJavaPlugins.onAssembly(new CompletableTimeout(this, timeout, unit, scheduler, fallback)); + } + + /** + * Calls the specified {@link CompletableConverter} function during assembly time and returns its resulting value. + * <p> + * <img width="640" height="751" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Completable.to.png" alt=""> + * <p> + * This allows fluent conversion to any other type. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code to} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * <p>History: 2.1.7 - experimental + * @param <R> the resulting object type + * @param converter the {@code CompletableConverter} that receives the current {@code Completable} instance and returns a value to be the result of {@code to()} + * @return the converted value + * @throws NullPointerException if {@code converter} is {@code null} + * @since 2.2 + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + public final <R> R to(@NonNull CompletableConverter<? extends R> converter) { + return Objects.requireNonNull(converter, "converter is null").apply(this); + } + + /** + * Returns a {@link Flowable} which when subscribed to subscribes to this {@code Completable} and + * relays the terminal events to the downstream {@link Subscriber}. + * <p> + * <img width="640" height="585" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Completable.toFlowable.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The returned {@code Flowable} honors the backpressure of the downstream consumer.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code toFlowable} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <T> the value type + * @return the new {@code Flowable} instance + */ + @CheckReturnValue + @SuppressWarnings("unchecked") + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull T> Flowable<T> toFlowable() { + if (this instanceof FuseToFlowable) { + return ((FuseToFlowable<T>)this).fuseToFlowable(); + } + return RxJavaPlugins.onAssembly(new CompletableToFlowable<>(this)); + } + /** + * Returns a {@link Future} representing the termination of the current {@code Completable} + * via a {@code null} value. + * <p> + * <img width="640" height="433" src="https://github.com/ReactiveX/RxJava/wiki/images/rx-operators/Completable.toFuture.png" alt=""> + * <p> + * Cancelling the {@code Future} will cancel the subscription to the current {@code Completable}. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code toFuture} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @return the new {@code Future} instance + * @see <a href="http://reactivex.io/documentation/operators/to.html">ReactiveX documentation: To</a> + * @since 3.0.0 + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Future<Void> toFuture() { + return subscribeWith(new FutureMultiObserver<>()); + } + + /** + * Converts this {@code Completable} into a {@link Maybe}. + * <p> + * <img width="640" height="585" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Completable.toMaybe.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code toMaybe} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T> the value type + * @return the new {@code Maybe} instance + */ + @CheckReturnValue + @SuppressWarnings("unchecked") + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull T> Maybe<T> toMaybe() { + if (this instanceof FuseToMaybe) { + return ((FuseToMaybe<T>)this).fuseToMaybe(); + } + return RxJavaPlugins.onAssembly(new MaybeFromCompletable<>(this)); + } + + /** + * Returns an {@link Observable} which when subscribed to subscribes to this {@code Completable} and + * relays the terminal events to the downstream {@link Observer}. + * <p> + * <img width="640" height="293" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Completable.toObservable.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code toObservable} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <T> the value type + * @return the new {@code Observable} created + */ + @CheckReturnValue + @SuppressWarnings("unchecked") + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull T> Observable<T> toObservable() { + if (this instanceof FuseToObservable) { + return ((FuseToObservable<T>)this).fuseToObservable(); + } + return RxJavaPlugins.onAssembly(new CompletableToObservable<>(this)); + } + + /** + * Converts this {@code Completable} into a {@link Single} which when this {@code Completable} completes normally, + * calls the given {@link Supplier} and emits its returned value through {@code onSuccess}. + * <p> + * <img width="640" height="583" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Completable.toSingle.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code toSingle} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <T> the value type + * @param completionValueSupplier the value supplier called when this {@code Completable} completes normally + * @return the new {@code Single} instance + * @throws NullPointerException if {@code completionValueSupplier} is {@code null} + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public final <@NonNull T> Single<T> toSingle(@NonNull Supplier<? extends T> completionValueSupplier) { + Objects.requireNonNull(completionValueSupplier, "completionValueSupplier is null"); + return RxJavaPlugins.onAssembly(new CompletableToSingle<>(this, completionValueSupplier, null)); + } + + /** + * Converts this {@code Completable} into a {@link Single} which when this {@code Completable} completes normally, + * emits the given value through {@code onSuccess}. + * <p> + * <img width="640" height="583" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Completable.toSingleDefault.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code toSingleDefault} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <T> the value type + * @param completionValue the value to emit when this {@code Completable} completes normally + * @return the new {@code Single} instance + * @throws NullPointerException if {@code completionValue} is {@code null} + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public final <@NonNull T> Single<T> toSingleDefault(T completionValue) { + Objects.requireNonNull(completionValue, "completionValue is null"); + return RxJavaPlugins.onAssembly(new CompletableToSingle<>(this, null, completionValue)); + } + + /** + * Returns a {@code Completable} which makes sure when an observer disposes the subscription, the + * {@code dispose()} method is called on the specified {@link Scheduler}. + * <p> + * <img width="640" height="716" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Completable.unsubscribeOn.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code unsubscribeOn} calls {@code dispose()} of the upstream on the {@code Scheduler} you specify.</dd> + * </dl> + * @param scheduler the target {@code Scheduler} where to execute the disposing + * @return the new {@code Completable} instance + * @throws NullPointerException if {@code scheduler} is {@code null} + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.CUSTOM) + public final Completable unsubscribeOn(@NonNull Scheduler scheduler) { + Objects.requireNonNull(scheduler, "scheduler is null"); + return RxJavaPlugins.onAssembly(new CompletableDisposeOn(this, scheduler)); + } + // ------------------------------------------------------------------------- + // Fluent test support, super handy and reduces test preparation boilerplate + // ------------------------------------------------------------------------- + + /** + * Creates a {@link TestObserver} and subscribes + * it to this {@code Completable}. + * <p> + * <img width="640" height="458" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Completable.test.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code test} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @return the new {@code TestObserver} instance + * @since 2.0 + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final TestObserver<Void> test() { + TestObserver<Void> to = new TestObserver<>(); + subscribe(to); + return to; + } + + /** + * Creates a {@link TestObserver} optionally in cancelled state, then subscribes it to this {@code Completable}. + * @param dispose if {@code true}, the {@code TestObserver} will be cancelled before subscribing to this + * {@code Completable}. + * <p> + * <img width="640" height="499" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Completable.test.b.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code test} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @return the new {@code TestObserver} instance + * @since 2.0 + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final TestObserver<Void> test(boolean dispose) { + TestObserver<Void> to = new TestObserver<>(); + + if (dispose) { + to.dispose(); + } + subscribe(to); + return to; + } + + // ------------------------------------------------------------------------- + // JDK 8 Support + // ------------------------------------------------------------------------- + + /** + * Signals completion (or error) when the {@link CompletionStage} terminates. + * <p> + * <img width="640" height="262" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/fromCompletionStage.c.png" alt=""> + * <p> + * Note that the operator takes an already instantiated, running or terminated {@code CompletionStage}. + * If the {@code CompletionStage} is to be created per consumer upon subscription, use {@link #defer(Supplier)} + * around {@code fromCompletionStage}: + * <pre><code> + * Maybe.defer(() -> Completable.fromCompletionStage(createCompletionStage())); + * </code></pre> + * <p> + * Canceling the flow can't cancel the execution of the {@code CompletionStage} because {@code CompletionStage} + * itself doesn't support cancellation. Instead, the operator detaches from the {@code CompletionStage}. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code fromCompletionStage} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param stage the {@code CompletionStage} to convert to a {@code Completable} and + * signal {@code onComplete} or {@code onError} when the {@code CompletionStage} terminates normally or with a failure + * @return the new {@code Completable} instance + * @throws NullPointerException if {@code stage} is {@code null} + * @since 3.0.0 + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public static Completable fromCompletionStage(@NonNull CompletionStage<?> stage) { + Objects.requireNonNull(stage, "stage is null"); + return RxJavaPlugins.onAssembly(new CompletableFromCompletionStage<>(stage)); + } + + /** + * Signals the given default item when the upstream completes or signals the upstream error via + * a {@link CompletionStage}. + * <p> + * <img width="640" height="323" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/toCompletionStage.c.png" alt=""> + * <p> + * The upstream can be canceled by converting the resulting {@code CompletionStage} into + * {@link CompletableFuture} via {@link CompletionStage#toCompletableFuture()} and + * calling {@link CompletableFuture#cancel(boolean)} on it. + * The upstream will be also cancelled if the resulting {@code CompletionStage} is converted to and + * completed manually by {@link CompletableFuture#complete(Object)} or {@link CompletableFuture#completeExceptionally(Throwable)}. + * <p> + * {@code CompletionStage}s don't have a notion of emptiness and allow {@code null}s, therefore, one can either use + * a {@code defaultItem} of {@code null} or turn the flow into a sequence of {@link Optional}s and default to {@link Optional#empty()}: + * <pre><code> + * CompletionStage<Optional<T>> stage = source.map(Optional::of).toCompletionStage(Optional.empty()); + * </code></pre> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code toCompletionStage} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <T> the type of the default item to signal upon completion + * @param defaultItem the item to signal upon completion + * @return the new {@code CompletionStage} instance + * @since 3.0.0 + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@Nullable T> CompletionStage<T> toCompletionStage(T defaultItem) { + return subscribeWith(new CompletionStageConsumer<>(true, defaultItem)); + } +} diff --git a/src/main/java/io/reactivex/rxjava3/core/CompletableConverter.java b/src/main/java/io/reactivex/rxjava3/core/CompletableConverter.java new file mode 100644 index 0000000000..a213c6f5ab --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/core/CompletableConverter.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.core; + +import io.reactivex.rxjava3.annotations.NonNull; + +/** + * Convenience interface and callback used by the {@link Completable#to} operator to turn a Completable into another + * value fluently. + * <p>History: 2.1.7 - experimental + * @param <R> the output type + * @since 2.2 + */ +@FunctionalInterface +public interface CompletableConverter<@NonNull R> { + /** + * Applies a function to the upstream Completable and returns a converted value of type {@code R}. + * + * @param upstream the upstream Completable instance + * @return the converted value + */ + R apply(@NonNull Completable upstream); +} diff --git a/src/main/java/io/reactivex/rxjava3/core/CompletableEmitter.java b/src/main/java/io/reactivex/rxjava3/core/CompletableEmitter.java new file mode 100644 index 0000000000..2c5a9811c4 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/core/CompletableEmitter.java @@ -0,0 +1,98 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.core; + +import io.reactivex.rxjava3.annotations.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.functions.Cancellable; + +/** + * Abstraction over an RxJava {@link CompletableObserver} that allows associating + * a resource with it. + * <p> + * All methods are safe to call from multiple threads, but note that there is no guarantee + * whose terminal event will win and get delivered to the downstream. + * <p> + * Calling {@link #onComplete()} multiple times has no effect. + * Calling {@link #onError(Throwable)} multiple times or after {@code onComplete} will route the + * exception into the global error handler via {@link io.reactivex.rxjava3.plugins.RxJavaPlugins#onError(Throwable)}. + * <p> + * The emitter allows the registration of a single resource, in the form of a {@link Disposable} + * or {@link Cancellable} via {@link #setDisposable(Disposable)} or {@link #setCancellable(Cancellable)} + * respectively. The emitter implementations will dispose/cancel this instance when the + * downstream cancels the flow or after the event generator logic calls + * {@link #onError(Throwable)}, {@link #onComplete()} or when {@link #tryOnError(Throwable)} succeeds. + * <p> + * Only one {@code Disposable} or {@code Cancellable} object can be associated with the emitter at + * a time. Calling either {@code set} method will dispose/cancel any previous object. If there + * is a need for handling multiple resources, one can create a {@link io.reactivex.rxjava3.disposables.CompositeDisposable} + * and associate that with the emitter instead. + * <p> + * The {@link Cancellable} is logically equivalent to {@code Disposable} but allows using cleanup logic that can + * throw a checked exception (such as many {@code close()} methods on Java IO components). Since + * the release of resources happens after the terminal events have been delivered or the sequence gets + * cancelled, exceptions throw within {@code Cancellable} are routed to the global error handler via + * {@link io.reactivex.rxjava3.plugins.RxJavaPlugins#onError(Throwable)}. + */ +public interface CompletableEmitter { + + /** + * Signal the completion. + */ + void onComplete(); + + /** + * Signal an exception. + * @param t the exception, not null + */ + void onError(@NonNull Throwable t); + + /** + * Sets a Disposable on this emitter; any previous {@link Disposable} + * or {@link Cancellable} will be disposed/cancelled. + * @param d the disposable, null is allowed + */ + void setDisposable(@Nullable Disposable d); + + /** + * Sets a Cancellable on this emitter; any previous {@link Disposable} + * or {@link Cancellable} will be disposed/cancelled. + * @param c the cancellable resource, null is allowed + */ + void setCancellable(@Nullable Cancellable c); + + /** + * Returns true if the downstream disposed the sequence or the + * emitter was terminated via {@link #onError(Throwable)}, + * {@link #onComplete} or a successful {@link #tryOnError(Throwable)}. + * <p>This method is thread-safe. + * @return true if the downstream disposed the sequence or the emitter was terminated + */ + boolean isDisposed(); + + /** + * Attempts to emit the specified {@link Throwable} error if the downstream + * hasn't cancelled the sequence or is otherwise terminated, returning false + * if the emission is not allowed to happen due to lifecycle restrictions. + * <p> + * Unlike {@link #onError(Throwable)}, the {@link io.reactivex.rxjava3.plugins.RxJavaPlugins#onError(Throwable) RxjavaPlugins.onError} + * is not called if the error could not be delivered. + * <p>History: 2.1.1 - experimental + * @param t the throwable error to signal if possible + * @return true if successful, false if the downstream is not able to accept further + * events + * @since 2.2 + */ + boolean tryOnError(@NonNull Throwable t); +} diff --git a/src/main/java/io/reactivex/rxjava3/core/CompletableObserver.java b/src/main/java/io/reactivex/rxjava3/core/CompletableObserver.java new file mode 100644 index 0000000000..9339307047 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/core/CompletableObserver.java @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.core; + +import io.reactivex.rxjava3.annotations.NonNull; +import io.reactivex.rxjava3.disposables.Disposable; + +/** + * Provides a mechanism for receiving push-based notification of a valueless completion or an error. + * <p> + * When a {@code CompletableObserver} is subscribed to a {@link CompletableSource} through the {@link CompletableSource#subscribe(CompletableObserver)} method, + * the {@code CompletableSource} calls {@link #onSubscribe(Disposable)} with a {@link Disposable} that allows + * disposing the sequence at any time. A well-behaved + * {@code CompletableSource} will call a {@code CompletableObserver}'s {@link #onError(Throwable)} + * or {@link #onComplete()} method exactly once as they are considered mutually exclusive <strong>terminal signals</strong>. + * <p> + * Calling the {@code CompletableObserver}'s method must happen in a serialized fashion, that is, they must not + * be invoked concurrently by multiple threads in an overlapping fashion and the invocation pattern must + * adhere to the following protocol: + * <pre><code> onSubscribe (onError | onComplete)?</code></pre> + * <p> + * Subscribing a {@code CompletableObserver} to multiple {@code CompletableSource}s is not recommended. If such reuse + * happens, it is the duty of the {@code CompletableObserver} implementation to be ready to receive multiple calls to + * its methods and ensure proper concurrent behavior of its business logic. + * <p> + * Calling {@link #onSubscribe(Disposable)} or {@link #onError(Throwable)} with a + * {@code null} argument is forbidden. + * <p> + * The implementations of the {@code onXXX} methods should avoid throwing runtime exceptions other than the following cases: + * <ul> + * <li>If the argument is {@code null}, the methods can throw a {@code NullPointerException}. + * Note though that RxJava prevents {@code null}s to enter into the flow and thus there is generally no + * need to check for nulls in flows assembled from standard sources and intermediate operators. + * </li> + * <li>If there is a fatal error (such as {@code VirtualMachineError}).</li> + * </ul> + * @since 2.0 + */ +public interface CompletableObserver { + /** + * Called once by the {@link Completable} to set a {@link Disposable} on this instance which + * then can be used to cancel the subscription at any time. + * @param d the {@code Disposable} instance to call dispose on for cancellation, not null + */ + void onSubscribe(@NonNull Disposable d); + + /** + * Called once the deferred computation completes normally. + */ + void onComplete(); + + /** + * Called once if the deferred computation 'throws' an exception. + * @param e the exception, not {@code null}. + */ + void onError(@NonNull Throwable e); +} diff --git a/src/main/java/io/reactivex/rxjava3/core/CompletableOnSubscribe.java b/src/main/java/io/reactivex/rxjava3/core/CompletableOnSubscribe.java new file mode 100644 index 0000000000..e73fae2d5c --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/core/CompletableOnSubscribe.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.core; + +import io.reactivex.rxjava3.annotations.NonNull; + +/** + * A functional interface that has a {@code subscribe()} method that receives + * a {@link CompletableEmitter} instance that allows pushing + * an event in a cancellation-safe manner. + */ +@FunctionalInterface +public interface CompletableOnSubscribe { + + /** + * Called for each {@link CompletableObserver} that subscribes. + * @param emitter the safe emitter instance, never {@code null} + * @throws Throwable on error + */ + void subscribe(@NonNull CompletableEmitter emitter) throws Throwable; +} + diff --git a/src/main/java/io/reactivex/rxjava3/core/CompletableOperator.java b/src/main/java/io/reactivex/rxjava3/core/CompletableOperator.java new file mode 100644 index 0000000000..f06a94f36a --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/core/CompletableOperator.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.core; + +import io.reactivex.rxjava3.annotations.NonNull; + +/** + * Interface to map/wrap a downstream observer to an upstream observer. + */ +@FunctionalInterface +public interface CompletableOperator { + /** + * Applies a function to the child {@link CompletableObserver} and returns a new parent {@code CompletableObserver}. + * @param observer the child {@code CompletableObserver} instance + * @return the parent {@code CompletableObserver} instance + * @throws Throwable on failure + */ + @NonNull + CompletableObserver apply(@NonNull CompletableObserver observer) throws Throwable; +} diff --git a/src/main/java/io/reactivex/rxjava3/core/CompletableSource.java b/src/main/java/io/reactivex/rxjava3/core/CompletableSource.java new file mode 100644 index 0000000000..90d3853b8a --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/core/CompletableSource.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.core; + +import io.reactivex.rxjava3.annotations.NonNull; + +/** + * Represents a basic {@link Completable} source base interface, + * consumable via an {@link CompletableObserver}. + * + * @since 2.0 + */ +@FunctionalInterface +public interface CompletableSource { + + /** + * Subscribes the given {@link CompletableObserver} to this {@code CompletableSource} instance. + * @param observer the {@code CompletableObserver}, not {@code null} + * @throws NullPointerException if {@code observer} is {@code null} + */ + void subscribe(@NonNull CompletableObserver observer); +} diff --git a/src/main/java/io/reactivex/rxjava3/core/CompletableTransformer.java b/src/main/java/io/reactivex/rxjava3/core/CompletableTransformer.java new file mode 100644 index 0000000000..2887c4717c --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/core/CompletableTransformer.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.core; + +import io.reactivex.rxjava3.annotations.NonNull; + +/** + * Convenience interface and callback used by the compose operator to turn a {@link Completable} into another + * {@code Completable} fluently. + */ +@FunctionalInterface +public interface CompletableTransformer { + /** + * Applies a function to the upstream {@link Completable} and returns a {@link CompletableSource}. + * @param upstream the upstream {@code Completable} instance + * @return the transformed {@code CompletableSource} instance + */ + @NonNull + CompletableSource apply(@NonNull Completable upstream); +} diff --git a/src/main/java/io/reactivex/rxjava3/core/Emitter.java b/src/main/java/io/reactivex/rxjava3/core/Emitter.java new file mode 100644 index 0000000000..83410f056e --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/core/Emitter.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.core; + +import io.reactivex.rxjava3.annotations.NonNull; + +/** + * Base interface for emitting signals in a push-fashion in various generator-like source + * operators (create, generate). + * <p> + * Note that the {@link Emitter#onNext}, {@link Emitter#onError} and + * {@link Emitter#onComplete} methods provided to the function via the {@link Emitter} instance should be called synchronously, + * never concurrently. Calling them from multiple threads is not supported and leads to an + * undefined behavior. + * + * @param <T> the value type emitted + */ +public interface Emitter<@NonNull T> { + + /** + * Signal a normal value. + * @param value the value to signal, not {@code null} + */ + void onNext(@NonNull T value); + + /** + * Signal a {@link Throwable} exception. + * @param error the {@code Throwable} to signal, not {@code null} + */ + void onError(@NonNull Throwable error); + + /** + * Signal a completion. + */ + void onComplete(); +} diff --git a/src/main/java/io/reactivex/rxjava3/core/Flowable.java b/src/main/java/io/reactivex/rxjava3/core/Flowable.java new file mode 100644 index 0000000000..39f3c63b43 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/core/Flowable.java @@ -0,0 +1,20899 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.core; + +import java.util.*; +import java.util.concurrent.*; +import java.util.stream.*; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.annotations.*; +import io.reactivex.rxjava3.disposables.*; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.flowables.*; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.functions.*; +import io.reactivex.rxjava3.internal.jdk8.*; +import io.reactivex.rxjava3.internal.operators.flowable.*; +import io.reactivex.rxjava3.internal.operators.maybe.MaybeToFlowable; +import io.reactivex.rxjava3.internal.operators.mixed.*; +import io.reactivex.rxjava3.internal.operators.observable.ObservableFromPublisher; +import io.reactivex.rxjava3.internal.operators.single.SingleToFlowable; +import io.reactivex.rxjava3.internal.schedulers.ImmediateThinScheduler; +import io.reactivex.rxjava3.internal.subscribers.*; +import io.reactivex.rxjava3.internal.util.*; +import io.reactivex.rxjava3.operators.ScalarSupplier; +import io.reactivex.rxjava3.parallel.ParallelFlowable; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.schedulers.*; +import io.reactivex.rxjava3.subscribers.*; + +/** + * The {@code Flowable} class that implements the <a href="https://github.com/reactive-streams/reactive-streams-jvm">Reactive Streams</a> {@link Publisher} + * Pattern and offers factory methods, intermediate operators and the ability to consume reactive dataflows. + * <p> + * <em>Reactive Streams</em> operates with {@code Publisher}s which {@code Flowable} extends. Many operators + * therefore accept general {@code Publisher}s directly and allow direct interoperation with other + * <em>Reactive Streams</em> implementations. + * <p> + * The {@code Flowable} hosts the default buffer size of 128 elements for operators, accessible via {@link #bufferSize()}, + * that can be overridden globally via the system parameter {@code rx3.buffer-size}. Most operators, however, have + * overloads that allow setting their internal buffer size explicitly. + * <p> + * The documentation for this class makes use of marble diagrams. The following legend explains these diagrams: + * <p> + * <img width="640" height="317" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/legend.v3.png" alt=""> + * <p> + * The {@code Flowable} follows the protocol + * <pre><code> + * onSubscribe onNext* (onError | onComplete)? + * </code></pre> + * where the stream can be disposed through the {@link Subscription} instance provided to consumers through + * {@link Subscriber#onSubscribe(Subscription)}. + * Unlike the {@code Observable.subscribe()} of version 1.x, {@link #subscribe(Subscriber)} does not allow external cancellation + * of a subscription and the {@link Subscriber} instance is expected to expose such capability if needed. + * <p> + * {@code Flowable}s support backpressure and require {@code Subscriber}s to signal demand via {@link Subscription#request(long)}. + * <p> + * Example: + * <pre><code> + * Disposable d = Flowable.just("Hello world!") + * .delay(1, TimeUnit.SECONDS) + * .subscribeWith(new DisposableSubscriber<String>() { + * @Override public void onStart() { + * System.out.println("Start!"); + * request(1); + * } + * @Override public void onNext(String t) { + * System.out.println(t); + * request(1); + * } + * @Override public void onError(Throwable t) { + * t.printStackTrace(); + * } + * @Override public void onComplete() { + * System.out.println("Done!"); + * } + * }); + * + * Thread.sleep(500); + * // the sequence can now be cancelled via dispose() + * d.dispose(); + * </code></pre> + * <p> + * The <em>Reactive Streams</em> specification is relatively strict when defining interactions between {@code Publisher}s and {@code Subscriber}s, so much so + * that there is a significant performance penalty due certain timing requirements and the need to prepare for invalid + * request amounts via {@link Subscription#request(long)}. + * Therefore, RxJava has introduced the {@link FlowableSubscriber} interface that indicates the consumer can be driven with relaxed rules. + * All RxJava operators are implemented with these relaxed rules in mind. + * If the subscribing {@code Subscriber} does not implement this interface, for example, due to it being from another <em>Reactive Streams</em> compliant + * library, the {@code Flowable} will automatically apply a compliance wrapper around it. + * <p> + * {@code Flowable} is an abstract class, but it is not advised to implement sources and custom operators by extending the class directly due + * to the large amounts of <a href="https://github.com/reactive-streams/reactive-streams-jvm#specification">Reactive Streams</a> + * rules to be followed to the letter. See <a href="https://github.com/ReactiveX/RxJava/wiki/Writing-operators-for-2.0">the wiki</a> for + * some guidance if such custom implementations are necessary. + * <p> + * The recommended way of creating custom {@code Flowable}s is by using the {@link #create(FlowableOnSubscribe, BackpressureStrategy)} factory method: + * <pre><code> + * Flowable<String> source = Flowable.create(new FlowableOnSubscribe<String>() { + * @Override + * public void subscribe(FlowableEmitter<String> emitter) throws Exception { + * + * // signal an item + * emitter.onNext("Hello"); + * + * // could be some blocking operation + * Thread.sleep(1000); + * + * // the consumer might have cancelled the flow + * if (emitter.isCancelled()) { + * return; + * } + * + * emitter.onNext("World"); + * + * Thread.sleep(1000); + * + * // the end-of-sequence has to be signaled, otherwise the + * // consumers may never finish + * emitter.onComplete(); + * } + * }, BackpressureStrategy.BUFFER); + * + * System.out.println("Subscribe!"); + * + * source.subscribe(System.out::println); + * + * System.out.println("Done!"); + * </code></pre> + * <p> + * RxJava reactive sources, such as {@code Flowable}, are generally synchronous and sequential in nature. In the ReactiveX design, the location (thread) + * where operators run is <i>orthogonal</i> to when the operators can work with data. This means that asynchrony and parallelism + * has to be explicitly expressed via operators such as {@link #subscribeOn(Scheduler)}, {@link #observeOn(Scheduler)} and {@link #parallel()}. In general, + * operators featuring a {@link Scheduler} parameter are introducing this type of asynchrony into the flow. + * <p> + * For more information see the <a href="http://reactivex.io/documentation/Publisher.html">ReactiveX documentation</a>. + * + * @param <T> + * the type of the items emitted by the {@code Flowable} + * @see Observable + * @see ParallelFlowable + * @see io.reactivex.rxjava3.subscribers.DisposableSubscriber + */ +public abstract class Flowable<@NonNull T> implements Publisher<T> { + /** The default buffer size. */ + static final int BUFFER_SIZE; + static { + BUFFER_SIZE = Math.max(1, Integer.getInteger("rx3.buffer-size", 128)); + } + + /** + * Mirrors the one {@link Publisher} in an {@link Iterable} of several {@code Publisher}s that first either emits an item or sends + * a termination notification. + * <p> + * <img width="640" height="417" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Flowable.amb.png" alt=""> + * <p> + * When one of the {@code Publisher}s signal an item or terminates first, all subscriptions to the other + * {@code Publisher}s are canceled. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator itself doesn't interfere with backpressure which is determined by the winning + * {@code Publisher}'s backpressure behavior.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code amb} does not operate by default on a particular {@link Scheduler}.</dd> + * <dt><b>Error handling:</b></dt> + * <dd> + * If any of the losing {@code Publisher}s signals an error, the error is routed to the global + * error handler via {@link RxJavaPlugins#onError(Throwable)}. + * </dd> + * </dl> + * + * @param <T> the common element type + * @param sources + * an {@code Iterable} of {@code Publisher}s sources competing to react first. A subscription to each {@code Publisher} will + * occur in the same order as in this {@code Iterable}. + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code sources} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/amb.html">ReactiveX operators documentation: Amb</a> + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.PASS_THROUGH) + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T> Flowable<T> amb(@NonNull Iterable<@NonNull ? extends Publisher<? extends T>> sources) { + Objects.requireNonNull(sources, "sources is null"); + return RxJavaPlugins.onAssembly(new FlowableAmb<>(null, sources)); + } + + /** + * Mirrors the one {@link Publisher} in an array of several {@code Publisher}s that first either emits an item or sends + * a termination notification. + * <p> + * <img width="640" height="417" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Flowable.ambArray.png" alt=""> + * <p> + * When one of the {@code Publisher}s signal an item or terminates first, all subscriptions to the other + * {@code Publisher}s are canceled. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator itself doesn't interfere with backpressure which is determined by the winning + * {@code Publisher}'s backpressure behavior.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code ambArray} does not operate by default on a particular {@link Scheduler}.</dd> + * <dt><b>Error handling:</b></dt> + * <dd> + * If any of the losing {@code Publisher}s signals an error, the error is routed to the global + * error handler via {@link RxJavaPlugins#onError(Throwable)}. + * </dd> + * </dl> + * + * @param <T> the common element type + * @param sources + * an array of {@code Publisher} sources competing to react first. A subscription to each {@code Publisher} will + * occur in the same order as in this array. + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code sources} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/amb.html">ReactiveX operators documentation: Amb</a> + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.PASS_THROUGH) + @SchedulerSupport(SchedulerSupport.NONE) + @SafeVarargs + public static <@NonNull T> Flowable<T> ambArray(@NonNull Publisher<? extends T>... sources) { + Objects.requireNonNull(sources, "sources is null"); + int len = sources.length; + if (len == 0) { + return empty(); + } else + if (len == 1) { + return fromPublisher(sources[0]); + } + return RxJavaPlugins.onAssembly(new FlowableAmb<>(sources, null)); + } + + /** + * Returns the default internal buffer size used by most async operators. + * <p>The value can be overridden via system parameter {@code rx3.buffer-size} + * <em>before</em> the {@code Flowable} class is loaded. + * @return the default internal buffer size. + */ + @CheckReturnValue + public static int bufferSize() { + return BUFFER_SIZE; + } + + /** + * Combines a collection of source {@link Publisher}s by emitting an item that aggregates the latest values of each of + * the source {@code Publisher}s each time an item is received from any of the source {@code Publisher}s, where this + * aggregation is defined by a specified function. + * <p> + * Note on method signature: since Java doesn't allow creating a generic array with {@code new T[]}, the + * implementation of this operator has to create an {@code Object[]} instead. Unfortunately, a + * {@code Function<Integer[], R>} passed to the method would trigger a {@link ClassCastException}. + * <p> + * If any of the sources never produces an item but only terminates (normally or with an error), the + * resulting sequence terminates immediately (normally or with all the errors accumulated until that point). + * If that input source is also synchronous, other sources after it will not be subscribed to. + * <p> + * If the provided array of source {@code Publisher}s is empty, the resulting sequence completes immediately without emitting + * any items and without any calls to the combiner function. + * + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The returned {@code Publisher} honors backpressure from downstream. The source {@code Publisher}s + * are requested in a bounded manner, however, their backpressure is not enforced (the operator won't signal + * {@link MissingBackpressureException}) and may lead to {@link OutOfMemoryError} due to internal buffer bloat.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code combineLatestArray} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T> + * the common base type of source values + * @param <R> + * the result type + * @param sources + * the collection of source {@code Publisher}s + * @param combiner + * the aggregation function used to combine the items emitted by the source {@code Publisher}s + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code sources} or {@code combiner} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/combinelatest.html">ReactiveX operators documentation: CombineLatest</a> + */ + @SchedulerSupport(SchedulerSupport.NONE) + @CheckReturnValue + @BackpressureSupport(BackpressureKind.FULL) + @NonNull + public static <@NonNull T, @NonNull R> Flowable<R> combineLatestArray(@NonNull Publisher<? extends T>[] sources, @NonNull Function<? super Object[], ? extends R> combiner) { + return combineLatestArray(sources, combiner, bufferSize()); + } + + /** + * Combines a collection of source {@link Publisher}s by emitting an item that aggregates the latest values of each of + * the source {@code Publisher}s each time an item is received from any of the source {@code Publisher}s, where this + * aggregation is defined by a specified function. + * <p> + * Note on method signature: since Java doesn't allow creating a generic array with {@code new T[]}, the + * implementation of this operator has to create an {@code Object[]} instead. Unfortunately, a + * {@code Function<Integer[], R>} passed to the method would trigger a {@link ClassCastException}. + * <p> + * If any of the sources never produces an item but only terminates (normally or with an error), the + * resulting sequence terminates immediately (normally or with all the errors accumulated until that point). + * If that input source is also synchronous, other sources after it will not be subscribed to. + * <p> + * If the provided array of source {@code Publisher}s is empty, the resulting sequence completes immediately without emitting + * any items and without any calls to the combiner function. + * + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The returned {@code Publisher} honors backpressure from downstream. The source {@code Publisher}s + * are requested in a bounded manner, however, their backpressure is not enforced (the operator won't signal + * {@link MissingBackpressureException}) and may lead to {@link OutOfMemoryError} due to internal buffer bloat.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code combineLatestArray} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T> + * the common base type of source values + * @param <R> + * the result type + * @param sources + * the collection of source {@code Publisher}s + * @param combiner + * the aggregation function used to combine the items emitted by the source {@code Publisher}s + * @param bufferSize + * the internal buffer size and prefetch amount applied to every source {@code Flowable} + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code sources} or {@code combiner} is {@code null} + * @throws IllegalArgumentException if {@code bufferSize} is non-positive + * @see <a href="http://reactivex.io/documentation/operators/combinelatest.html">ReactiveX operators documentation: CombineLatest</a> + */ + @SchedulerSupport(SchedulerSupport.NONE) + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.FULL) + public static <@NonNull T, @NonNull R> Flowable<R> combineLatestArray(@NonNull Publisher<? extends T>[] sources, @NonNull Function<? super Object[], ? extends R> combiner, int bufferSize) { + Objects.requireNonNull(sources, "sources is null"); + if (sources.length == 0) { + return empty(); + } + Objects.requireNonNull(combiner, "combiner is null"); + ObjectHelper.verifyPositive(bufferSize, "bufferSize"); + return RxJavaPlugins.onAssembly(new FlowableCombineLatest<>(sources, combiner, bufferSize, false)); + } + + /** + * Combines a collection of source {@link Publisher}s by emitting an item that aggregates the latest values of each of + * the source {@code Publisher}s each time an item is received from any of the source {@code Publisher}s, where this + * aggregation is defined by a specified function. + * <p> + * Note on method signature: since Java doesn't allow creating a generic array with {@code new T[]}, the + * implementation of this operator has to create an {@code Object[]} instead. Unfortunately, a + * {@code Function<Integer[], R>} passed to the method would trigger a {@link ClassCastException}. + * <p> + * If any of the sources never produces an item but only terminates (normally or with an error), the + * resulting sequence terminates immediately (normally or with all the errors accumulated until that point). + * If that input source is also synchronous, other sources after it will not be subscribed to. + * <p> + * If the provided iterable of source {@code Publisher}s is empty, the resulting sequence completes immediately without emitting + * any items and without any calls to the combiner function. + * + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The returned {@code Publisher} honors backpressure from downstream. The source {@code Publisher}s + * are requested in a bounded manner, however, their backpressure is not enforced (the operator won't signal + * {@link MissingBackpressureException}) and may lead to {@link OutOfMemoryError} due to internal buffer bloat.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code combineLatest} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T> + * the common base type of source values + * @param <R> + * the result type + * @param sources + * the collection of source {@code Publisher}s + * @param combiner + * the aggregation function used to combine the items emitted by the source {@code Publisher}s + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code sources} or {@code combiner} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/combinelatest.html">ReactiveX operators documentation: CombineLatest</a> + */ + @SchedulerSupport(SchedulerSupport.NONE) + @CheckReturnValue + @BackpressureSupport(BackpressureKind.FULL) + @NonNull + public static <@NonNull T, @NonNull R> Flowable<R> combineLatest(@NonNull Iterable<@NonNull ? extends Publisher<? extends T>> sources, + @NonNull Function<? super Object[], ? extends R> combiner) { + return combineLatest(sources, combiner, bufferSize()); + } + + /** + * Combines a collection of source {@link Publisher}s by emitting an item that aggregates the latest values of each of + * the source {@code Publisher}s each time an item is received from any of the source {@code Publisher}s, where this + * aggregation is defined by a specified function. + * <p> + * Note on method signature: since Java doesn't allow creating a generic array with {@code new T[]}, the + * implementation of this operator has to create an {@code Object[]} instead. Unfortunately, a + * {@code Function<Integer[], R>} passed to the method would trigger a {@link ClassCastException}. + * <p> + * If any of the sources never produces an item but only terminates (normally or with an error), the + * resulting sequence terminates immediately (normally or with all the errors accumulated until that point). + * If that input source is also synchronous, other sources after it will not be subscribed to. + * <p> + * If the provided iterable of source {@code Publisher}s is empty, the resulting sequence completes immediately without emitting any items and + * without any calls to the combiner function. + * + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The returned {@code Publisher} honors backpressure from downstream. The source {@code Publisher}s + * are requested in a bounded manner, however, their backpressure is not enforced (the operator won't signal + * {@link MissingBackpressureException}) and may lead to {@link OutOfMemoryError} due to internal buffer bloat.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code combineLatest} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T> + * the common base type of source values + * @param <R> + * the result type + * @param sources + * the collection of source {@code Publisher}s + * @param combiner + * the aggregation function used to combine the items emitted by the source {@code Publisher}s + * @param bufferSize + * the internal buffer size and prefetch amount applied to every source {@code Flowable} + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code sources} or {@code combiner} is {@code null} + * @throws IllegalArgumentException if {@code bufferSize} is non-positive + * @see <a href="http://reactivex.io/documentation/operators/combinelatest.html">ReactiveX operators documentation: CombineLatest</a> + */ + @SchedulerSupport(SchedulerSupport.NONE) + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.FULL) + public static <@NonNull T, @NonNull R> Flowable<R> combineLatest(@NonNull Iterable<@NonNull ? extends Publisher<? extends T>> sources, + @NonNull Function<? super Object[], ? extends R> combiner, int bufferSize) { + Objects.requireNonNull(sources, "sources is null"); + Objects.requireNonNull(combiner, "combiner is null"); + ObjectHelper.verifyPositive(bufferSize, "bufferSize"); + return RxJavaPlugins.onAssembly(new FlowableCombineLatest<>(sources, combiner, bufferSize, false)); + } + + /** + * Combines a collection of source {@link Publisher}s by emitting an item that aggregates the latest values of each of + * the source {@code Publisher}s each time an item is received from any of the source {@code Publisher}s, where this + * aggregation is defined by a specified function. + * <p> + * Note on method signature: since Java doesn't allow creating a generic array with {@code new T[]}, the + * implementation of this operator has to create an {@code Object[]} instead. Unfortunately, a + * {@code Function<Integer[], R>} passed to the method would trigger a {@link ClassCastException}. + * <p> + * If any of the sources never produces an item but only terminates (normally or with an error), the + * resulting sequence terminates immediately (normally or with all the errors accumulated until that point). + * If that input source is also synchronous, other sources after it will not be subscribed to. + * <p> + * If the provided array of source {@code Publisher}s is empty, the resulting sequence completes immediately without emitting + * any items and without any calls to the combiner function. + * + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The returned {@code Publisher} honors backpressure from downstream. The source {@code Publisher}s + * are requested in a bounded manner, however, their backpressure is not enforced (the operator won't signal + * {@link MissingBackpressureException}) and may lead to {@link OutOfMemoryError} due to internal buffer bloat.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code combineLatestArrayDelayError} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T> + * the common base type of source values + * @param <R> + * the result type + * @param sources + * the collection of source {@code Publisher}s + * @param combiner + * the aggregation function used to combine the items emitted by the source {@code Publisher}s + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code sources} or {@code combiner} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/combinelatest.html">ReactiveX operators documentation: CombineLatest</a> + */ + @SchedulerSupport(SchedulerSupport.NONE) + @CheckReturnValue + @BackpressureSupport(BackpressureKind.FULL) + @NonNull + public static <@NonNull T, @NonNull R> Flowable<R> combineLatestArrayDelayError(@NonNull Publisher<? extends T>[] sources, + @NonNull Function<? super Object[], ? extends R> combiner) { + return combineLatestArrayDelayError(sources, combiner, bufferSize()); + } + + /** + * Combines a collection of source {@link Publisher}s by emitting an item that aggregates the latest values of each of + * the source {@code Publisher}s each time an item is received from any of the source {@code Publisher}s, where this + * aggregation is defined by a specified function and delays any error from the sources until + * all source {@code Publisher}s terminate. + * <p> + * Note on method signature: since Java doesn't allow creating a generic array with {@code new T[]}, the + * implementation of this operator has to create an {@code Object[]} instead. Unfortunately, a + * {@code Function<Integer[], R>} passed to the method would trigger a {@link ClassCastException}. + * <p> + * If any of the sources never produces an item but only terminates (normally or with an error), the + * resulting sequence terminates immediately (normally or with all the errors accumulated until that point). + * If that input source is also synchronous, other sources after it will not be subscribed to. + * <p> + * If the provided array of source {@code Publisher}s is empty, the resulting sequence completes immediately without emitting + * any items and without any calls to the combiner function. + * + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The returned {@code Publisher} honors backpressure from downstream. The source {@code Publisher}s + * are requested in a bounded manner, however, their backpressure is not enforced (the operator won't signal + * {@link MissingBackpressureException}) and may lead to {@link OutOfMemoryError} due to internal buffer bloat.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code combineLatestArrayDelayError} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T> + * the common base type of source values + * @param <R> + * the result type + * @param sources + * the collection of source {@code Publisher}s + * @param combiner + * the aggregation function used to combine the items emitted by the source {@code Publisher}s + * @param bufferSize + * the internal buffer size and prefetch amount applied to every source {@code Flowable} + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code sources} or {@code combiner} is {@code null} + * @throws IllegalArgumentException if {@code bufferSize} is non-positive + * @see <a href="http://reactivex.io/documentation/operators/combinelatest.html">ReactiveX operators documentation: CombineLatest</a> + */ + @SchedulerSupport(SchedulerSupport.NONE) + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.FULL) + public static <@NonNull T, @NonNull R> Flowable<R> combineLatestArrayDelayError(@NonNull Publisher<? extends T>[] sources, + @NonNull Function<? super Object[], ? extends R> combiner, int bufferSize) { + Objects.requireNonNull(sources, "sources is null"); + Objects.requireNonNull(combiner, "combiner is null"); + ObjectHelper.verifyPositive(bufferSize, "bufferSize"); + if (sources.length == 0) { + return empty(); + } + return RxJavaPlugins.onAssembly(new FlowableCombineLatest<>(sources, combiner, bufferSize, true)); + } + + /** + * Combines a collection of source {@link Publisher}s by emitting an item that aggregates the latest values of each of + * the source {@code Publisher}s each time an item is received from any of the source {@code Publisher}s, where this + * aggregation is defined by a specified function and delays any error from the sources until + * all source {@code Publisher}s terminate. + * <p> + * Note on method signature: since Java doesn't allow creating a generic array with {@code new T[]}, the + * implementation of this operator has to create an {@code Object[]} instead. Unfortunately, a + * {@code Function<Integer[], R>} passed to the method would trigger a {@link ClassCastException}. + * <p> + * If any of the sources never produces an item but only terminates (normally or with an error), the + * resulting sequence terminates immediately (normally or with all the errors accumulated until that point). + * If that input source is also synchronous, other sources after it will not be subscribed to. + * <p> + * If the provided iterable of source {@code Publisher}s is empty, the resulting sequence completes immediately without emitting + * any items and without any calls to the combiner function. + * + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The returned {@code Publisher} honors backpressure from downstream. The source {@code Publisher}s + * are requested in a bounded manner, however, their backpressure is not enforced (the operator won't signal + * {@link MissingBackpressureException}) and may lead to {@link OutOfMemoryError} due to internal buffer bloat.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code combineLatestDelayError} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T> + * the common base type of source values + * @param <R> + * the result type + * @param sources + * the collection of source {@code Publisher}s + * @param combiner + * the aggregation function used to combine the items emitted by the source {@code Publisher}s + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code sources} or {@code combiner} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/combinelatest.html">ReactiveX operators documentation: CombineLatest</a> + */ + @SchedulerSupport(SchedulerSupport.NONE) + @CheckReturnValue + @BackpressureSupport(BackpressureKind.FULL) + @NonNull + public static <@NonNull T, @NonNull R> Flowable<R> combineLatestDelayError(@NonNull Iterable<@NonNull ? extends Publisher<? extends T>> sources, + @NonNull Function<? super Object[], ? extends R> combiner) { + return combineLatestDelayError(sources, combiner, bufferSize()); + } + + /** + * Combines a collection of source {@link Publisher}s by emitting an item that aggregates the latest values of each of + * the source {@code Publisher}s each time an item is received from any of the source {@code Publisher}s, where this + * aggregation is defined by a specified function and delays any error from the sources until + * all source {@code Publisher}s terminate. + * <p> + * Note on method signature: since Java doesn't allow creating a generic array with {@code new T[]}, the + * implementation of this operator has to create an {@code Object[]} instead. Unfortunately, a + * {@code Function<Integer[], R>} passed to the method would trigger a {@link ClassCastException}. + * <p> + * If any of the sources never produces an item but only terminates (normally or with an error), the + * resulting sequence terminates immediately (normally or with all the errors accumulated until that point). + * If that input source is also synchronous, other sources after it will not be subscribed to. + * <p> + * If the provided iterable of source {@code Publisher}s is empty, the resulting sequence completes immediately without emitting + * any items and without any calls to the combiner function. + * + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The returned {@code Publisher} honors backpressure from downstream. The source {@code Publisher}s + * are requested in a bounded manner, however, their backpressure is not enforced (the operator won't signal + * {@link MissingBackpressureException}) and may lead to {@link OutOfMemoryError} due to internal buffer bloat.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code combineLatestDelayError} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T> + * the common base type of source values + * @param <R> + * the result type + * @param sources + * the collection of source {@code Publisher}s + * @param combiner + * the aggregation function used to combine the items emitted by the source {@code Publisher}s + * @param bufferSize + * the internal buffer size and prefetch amount applied to every source {@code Flowable} + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code sources} or {@code combiner} is {@code null} + * @throws IllegalArgumentException if {@code bufferSize} is non-positive + * @see <a href="http://reactivex.io/documentation/operators/combinelatest.html">ReactiveX operators documentation: CombineLatest</a> + */ + @SchedulerSupport(SchedulerSupport.NONE) + @CheckReturnValue + @BackpressureSupport(BackpressureKind.FULL) + @NonNull + public static <@NonNull T, @NonNull R> Flowable<R> combineLatestDelayError(@NonNull Iterable<@NonNull ? extends Publisher<? extends T>> sources, + @NonNull Function<? super Object[], ? extends R> combiner, int bufferSize) { + Objects.requireNonNull(sources, "sources is null"); + Objects.requireNonNull(combiner, "combiner is null"); + ObjectHelper.verifyPositive(bufferSize, "bufferSize"); + return RxJavaPlugins.onAssembly(new FlowableCombineLatest<>(sources, combiner, bufferSize, true)); + } + + /** + * Combines two source {@link Publisher}s by emitting an item that aggregates the latest values of each of the + * source {@code Publisher}s each time an item is received from either of the source {@code Publisher}s, where this + * aggregation is defined by a specified function. + * <p> + * If any of the sources never produces an item but only terminates (normally or with an error), the + * resulting sequence terminates immediately (normally or with all the errors accumulated until that point). + * If that input source is also synchronous, other sources after it will not be subscribed to. + * <p> + * <img width="640" height="380" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/combineLatest.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The returned {@code Publisher} honors backpressure from downstream. The source {@code Publisher}s + * are requested in a bounded manner, however, their backpressure is not enforced (the operator won't signal + * {@link MissingBackpressureException}) and may lead to {@link OutOfMemoryError} due to internal buffer bloat.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code combineLatest} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T1> the element type of the first source + * @param <T2> the element type of the second source + * @param <R> the combined output type + * @param source1 + * the first source {@code Publisher} + * @param source2 + * the second source {@code Publisher} + * @param combiner + * the aggregation function used to combine the items emitted by the source {@code Publisher}s + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code source1}, {@code source2} or {@code combiner} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/combinelatest.html">ReactiveX operators documentation: CombineLatest</a> + */ + @SuppressWarnings("unchecked") + @CheckReturnValue + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public static <@NonNull T1, @NonNull T2, @NonNull R> Flowable<R> combineLatest( + @NonNull Publisher<? extends T1> source1, @NonNull Publisher<? extends T2> source2, + @NonNull BiFunction<? super T1, ? super T2, ? extends R> combiner) { + Objects.requireNonNull(source1, "source1 is null"); + Objects.requireNonNull(source2, "source2 is null"); + Objects.requireNonNull(combiner, "combiner is null"); + return combineLatestArray(new Publisher[] { source1, source2 }, Functions.toFunction(combiner), bufferSize()); + } + + /** + * Combines three source {@link Publisher}s by emitting an item that aggregates the latest values of each of the + * source {@code Publisher}s each time an item is received from any of the source {@code Publisher}s, where this + * aggregation is defined by a specified function. + * <p> + * If any of the sources never produces an item but only terminates (normally or with an error), the + * resulting sequence terminates immediately (normally or with all the errors accumulated until that point). + * If that input source is also synchronous, other sources after it will not be subscribed to. + * <p> + * <img width="640" height="380" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/combineLatest.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The returned {@code Publisher} honors backpressure from downstream. The source {@code Publisher}s + * are requested in a bounded manner, however, their backpressure is not enforced (the operator won't signal + * {@link MissingBackpressureException}) and may lead to {@link OutOfMemoryError} due to internal buffer bloat.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code combineLatest} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T1> the element type of the first source + * @param <T2> the element type of the second source + * @param <T3> the element type of the third source + * @param <R> the combined output type + * @param source1 + * the first source {@code Publisher} + * @param source2 + * the second source {@code Publisher} + * @param source3 + * the third source {@code Publisher} + * @param combiner + * the aggregation function used to combine the items emitted by the source {@code Publisher}s + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code source1}, {@code source2}, {@code source3} or {@code combiner} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/combinelatest.html">ReactiveX operators documentation: CombineLatest</a> + */ + @SuppressWarnings("unchecked") + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T1, @NonNull T2, @NonNull T3, @NonNull R> Flowable<R> combineLatest( + @NonNull Publisher<? extends T1> source1, @NonNull Publisher<? extends T2> source2, + @NonNull Publisher<? extends T3> source3, + @NonNull Function3<? super T1, ? super T2, ? super T3, ? extends R> combiner) { + Objects.requireNonNull(source1, "source1 is null"); + Objects.requireNonNull(source2, "source2 is null"); + Objects.requireNonNull(source3, "source3 is null"); + Objects.requireNonNull(combiner, "combiner is null"); + return combineLatestArray(new Publisher[] { source1, source2, source3 }, Functions.toFunction(combiner), bufferSize()); + } + + /** + * Combines four source {@link Publisher}s by emitting an item that aggregates the latest values of each of the + * source {@code Publisher}s each time an item is received from any of the source {@code Publisher}s, where this + * aggregation is defined by a specified function. + * <p> + * If any of the sources never produces an item but only terminates (normally or with an error), the + * resulting sequence terminates immediately (normally or with all the errors accumulated until that point). + * If that input source is also synchronous, other sources after it will not be subscribed to. + * <p> + * <img width="640" height="380" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/combineLatest.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The returned {@code Publisher} honors backpressure from downstream. The source {@code Publisher}s + * are requested in a bounded manner, however, their backpressure is not enforced (the operator won't signal + * {@link MissingBackpressureException}) and may lead to {@link OutOfMemoryError} due to internal buffer bloat.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code combineLatest} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T1> the element type of the first source + * @param <T2> the element type of the second source + * @param <T3> the element type of the third source + * @param <T4> the element type of the fourth source + * @param <R> the combined output type + * @param source1 + * the first source {@code Publisher} + * @param source2 + * the second source {@code Publisher} + * @param source3 + * the third source {@code Publisher} + * @param source4 + * the fourth source {@code Publisher} + * @param combiner + * the aggregation function used to combine the items emitted by the source {@code Publisher}s + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code source1}, {@code source2}, {@code source3}, + * {@code source4} or {@code combiner} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/combinelatest.html">ReactiveX operators documentation: CombineLatest</a> + */ + @SuppressWarnings("unchecked") + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T1, @NonNull T2, @NonNull T3, @NonNull T4, @NonNull R> Flowable<R> combineLatest( + @NonNull Publisher<? extends T1> source1, @NonNull Publisher<? extends T2> source2, + @NonNull Publisher<? extends T3> source3, @NonNull Publisher<? extends T4> source4, + @NonNull Function4<? super T1, ? super T2, ? super T3, ? super T4, ? extends R> combiner) { + Objects.requireNonNull(source1, "source1 is null"); + Objects.requireNonNull(source2, "source2 is null"); + Objects.requireNonNull(source3, "source3 is null"); + Objects.requireNonNull(source4, "source4 is null"); + Objects.requireNonNull(combiner, "combiner is null"); + return combineLatestArray(new Publisher[] { source1, source2, source3, source4 }, Functions.toFunction(combiner), bufferSize()); + } + + /** + * Combines five source {@link Publisher}s by emitting an item that aggregates the latest values of each of the + * source {@code Publisher}s each time an item is received from any of the source {@code Publisher}s, where this + * aggregation is defined by a specified function. + * <p> + * If any of the sources never produces an item but only terminates (normally or with an error), the + * resulting sequence terminates immediately (normally or with all the errors accumulated until that point). + * If that input source is also synchronous, other sources after it will not be subscribed to. + * <p> + * <img width="640" height="380" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/combineLatest.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The returned {@code Publisher} honors backpressure from downstream. The source {@code Publisher}s + * are requested in a bounded manner, however, their backpressure is not enforced (the operator won't signal + * {@link MissingBackpressureException}) and may lead to {@link OutOfMemoryError} due to internal buffer bloat.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code combineLatest} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T1> the element type of the first source + * @param <T2> the element type of the second source + * @param <T3> the element type of the third source + * @param <T4> the element type of the fourth source + * @param <T5> the element type of the fifth source + * @param <R> the combined output type + * @param source1 + * the first source {@code Publisher} + * @param source2 + * the second source {@code Publisher} + * @param source3 + * the third source {@code Publisher} + * @param source4 + * the fourth source {@code Publisher} + * @param source5 + * the fifth source {@code Publisher} + * @param combiner + * the aggregation function used to combine the items emitted by the source {@code Publisher}s + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code source1}, {@code source2}, {@code source3}, + * {@code source4}, {@code source5} or {@code combiner} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/combinelatest.html">ReactiveX operators documentation: CombineLatest</a> + */ + @SuppressWarnings("unchecked") + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T1, @NonNull T2, @NonNull T3, @NonNull T4, @NonNull T5, @NonNull R> Flowable<R> combineLatest( + @NonNull Publisher<? extends T1> source1, @NonNull Publisher<? extends T2> source2, + @NonNull Publisher<? extends T3> source3, @NonNull Publisher<? extends T4> source4, + @NonNull Publisher<? extends T5> source5, + @NonNull Function5<? super T1, ? super T2, ? super T3, ? super T4, ? super T5, ? extends R> combiner) { + Objects.requireNonNull(source1, "source1 is null"); + Objects.requireNonNull(source2, "source2 is null"); + Objects.requireNonNull(source3, "source3 is null"); + Objects.requireNonNull(source4, "source4 is null"); + Objects.requireNonNull(source5, "source5 is null"); + Objects.requireNonNull(combiner, "combiner is null"); + return combineLatestArray(new Publisher[] { source1, source2, source3, source4, source5 }, Functions.toFunction(combiner), bufferSize()); + } + + /** + * Combines six source {@link Publisher}s by emitting an item that aggregates the latest values of each of the + * source {@code Publisher}s each time an item is received from any of the source {@code Publisher}s, where this + * aggregation is defined by a specified function. + * <p> + * If any of the sources never produces an item but only terminates (normally or with an error), the + * resulting sequence terminates immediately (normally or with all the errors accumulated until that point). + * If that input source is also synchronous, other sources after it will not be subscribed to. + * <p> + * <img width="640" height="380" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/combineLatest.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The returned {@code Publisher} honors backpressure from downstream. The source {@code Publisher}s + * are requested in a bounded manner, however, their backpressure is not enforced (the operator won't signal + * {@link MissingBackpressureException}) and may lead to {@link OutOfMemoryError} due to internal buffer bloat.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code combineLatest} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T1> the element type of the first source + * @param <T2> the element type of the second source + * @param <T3> the element type of the third source + * @param <T4> the element type of the fourth source + * @param <T5> the element type of the fifth source + * @param <T6> the element type of the sixth source + * @param <R> the combined output type + * @param source1 + * the first source {@code Publisher} + * @param source2 + * the second source {@code Publisher} + * @param source3 + * the third source {@code Publisher} + * @param source4 + * the fourth source {@code Publisher} + * @param source5 + * the fifth source {@code Publisher} + * @param source6 + * the sixth source {@code Publisher} + * @param combiner + * the aggregation function used to combine the items emitted by the source {@code Publisher}s + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code source1}, {@code source2}, {@code source3}, + * {@code source4}, {@code source5}, {@code source6} or {@code combiner} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/combinelatest.html">ReactiveX operators documentation: CombineLatest</a> + */ + @SuppressWarnings("unchecked") + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T1, @NonNull T2, @NonNull T3, @NonNull T4, @NonNull T5, @NonNull T6, @NonNull R> Flowable<R> combineLatest( + @NonNull Publisher<? extends T1> source1, @NonNull Publisher<? extends T2> source2, + @NonNull Publisher<? extends T3> source3, @NonNull Publisher<? extends T4> source4, + @NonNull Publisher<? extends T5> source5, @NonNull Publisher<? extends T6> source6, + @NonNull Function6<? super T1, ? super T2, ? super T3, ? super T4, ? super T5, ? super T6, ? extends R> combiner) { + Objects.requireNonNull(source1, "source1 is null"); + Objects.requireNonNull(source2, "source2 is null"); + Objects.requireNonNull(source3, "source3 is null"); + Objects.requireNonNull(source4, "source4 is null"); + Objects.requireNonNull(source5, "source5 is null"); + Objects.requireNonNull(source6, "source6 is null"); + Objects.requireNonNull(combiner, "combiner is null"); + return combineLatestArray(new Publisher[] { source1, source2, source3, source4, source5, source6 }, Functions.toFunction(combiner), bufferSize()); + } + + /** + * Combines seven source {@link Publisher}s by emitting an item that aggregates the latest values of each of the + * source {@code Publisher}s each time an item is received from any of the source {@code Publisher}s, where this + * aggregation is defined by a specified function. + * <p> + * If any of the sources never produces an item but only terminates (normally or with an error), the + * resulting sequence terminates immediately (normally or with all the errors accumulated until that point). + * If that input source is also synchronous, other sources after it will not be subscribed to. + * <p> + * <img width="640" height="380" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/combineLatest.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The returned {@code Publisher} honors backpressure from downstream. The source {@code Publisher}s + * are requested in a bounded manner, however, their backpressure is not enforced (the operator won't signal + * {@link MissingBackpressureException}) and may lead to {@link OutOfMemoryError} due to internal buffer bloat.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code combineLatest} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T1> the element type of the first source + * @param <T2> the element type of the second source + * @param <T3> the element type of the third source + * @param <T4> the element type of the fourth source + * @param <T5> the element type of the fifth source + * @param <T6> the element type of the sixth source + * @param <T7> the element type of the seventh source + * @param <R> the combined output type + * @param source1 + * the first source {@code Publisher} + * @param source2 + * the second source {@code Publisher} + * @param source3 + * the third source {@code Publisher} + * @param source4 + * the fourth source {@code Publisher} + * @param source5 + * the fifth source {@code Publisher} + * @param source6 + * the sixth source {@code Publisher} + * @param source7 + * the seventh source {@code Publisher} + * @param combiner + * the aggregation function used to combine the items emitted by the source {@code Publisher}s + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code source1}, {@code source2}, {@code source3}, + * {@code source4}, {@code source5}, {@code source6}, + * {@code source7} or {@code combiner} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/combinelatest.html">ReactiveX operators documentation: CombineLatest</a> + */ + @SuppressWarnings("unchecked") + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T1, @NonNull T2, @NonNull T3, @NonNull T4, @NonNull T5, @NonNull T6, @NonNull T7, @NonNull R> Flowable<R> combineLatest( + @NonNull Publisher<? extends T1> source1, @NonNull Publisher<? extends T2> source2, + @NonNull Publisher<? extends T3> source3, @NonNull Publisher<? extends T4> source4, + @NonNull Publisher<? extends T5> source5, @NonNull Publisher<? extends T6> source6, + @NonNull Publisher<? extends T7> source7, + @NonNull Function7<? super T1, ? super T2, ? super T3, ? super T4, ? super T5, ? super T6, ? super T7, ? extends R> combiner) { + Objects.requireNonNull(source1, "source1 is null"); + Objects.requireNonNull(source2, "source2 is null"); + Objects.requireNonNull(source3, "source3 is null"); + Objects.requireNonNull(source4, "source4 is null"); + Objects.requireNonNull(source5, "source5 is null"); + Objects.requireNonNull(source6, "source6 is null"); + Objects.requireNonNull(source7, "source7 is null"); + Objects.requireNonNull(combiner, "combiner is null"); + return combineLatestArray(new Publisher[] { source1, source2, source3, source4, source5, source6, source7 }, Functions.toFunction(combiner), bufferSize()); + } + + /** + * Combines eight source {@link Publisher}s by emitting an item that aggregates the latest values of each of the + * source {@code Publisher}s each time an item is received from any of the source {@code Publisher}s, where this + * aggregation is defined by a specified function. + * <p> + * If any of the sources never produces an item but only terminates (normally or with an error), the + * resulting sequence terminates immediately (normally or with all the errors accumulated until that point). + * If that input source is also synchronous, other sources after it will not be subscribed to. + * <p> + * <img width="640" height="380" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/combineLatest.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The returned {@code Publisher} honors backpressure from downstream. The source {@code Publisher}s + * are requested in a bounded manner, however, their backpressure is not enforced (the operator won't signal + * {@link MissingBackpressureException}) and may lead to {@link OutOfMemoryError} due to internal buffer bloat.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code combineLatest} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T1> the element type of the first source + * @param <T2> the element type of the second source + * @param <T3> the element type of the third source + * @param <T4> the element type of the fourth source + * @param <T5> the element type of the fifth source + * @param <T6> the element type of the sixth source + * @param <T7> the element type of the seventh source + * @param <T8> the element type of the eighth source + * @param <R> the combined output type + * @param source1 + * the first source {@code Publisher} + * @param source2 + * the second source {@code Publisher} + * @param source3 + * the third source {@code Publisher} + * @param source4 + * the fourth source {@code Publisher} + * @param source5 + * the fifth source {@code Publisher} + * @param source6 + * the sixth source {@code Publisher} + * @param source7 + * the seventh source {@code Publisher} + * @param source8 + * the eighth source {@code Publisher} + * @param combiner + * the aggregation function used to combine the items emitted by the source {@code Publisher}s + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code source1}, {@code source2}, {@code source3}, + * {@code source4}, {@code source5}, {@code source6}, + * {@code source7}, {@code source8} or {@code combiner} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/combinelatest.html">ReactiveX operators documentation: CombineLatest</a> + */ + @SuppressWarnings("unchecked") + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T1, @NonNull T2, @NonNull T3, @NonNull T4, @NonNull T5, @NonNull T6, @NonNull T7, @NonNull T8, @NonNull R> Flowable<R> combineLatest( + @NonNull Publisher<? extends T1> source1, @NonNull Publisher<? extends T2> source2, + @NonNull Publisher<? extends T3> source3, @NonNull Publisher<? extends T4> source4, + @NonNull Publisher<? extends T5> source5, @NonNull Publisher<? extends T6> source6, + @NonNull Publisher<? extends T7> source7, @NonNull Publisher<? extends T8> source8, + @NonNull Function8<? super T1, ? super T2, ? super T3, ? super T4, ? super T5, ? super T6, ? super T7, ? super T8, ? extends R> combiner) { + Objects.requireNonNull(source1, "source1 is null"); + Objects.requireNonNull(source2, "source2 is null"); + Objects.requireNonNull(source3, "source3 is null"); + Objects.requireNonNull(source4, "source4 is null"); + Objects.requireNonNull(source5, "source5 is null"); + Objects.requireNonNull(source6, "source6 is null"); + Objects.requireNonNull(source7, "source7 is null"); + Objects.requireNonNull(source8, "source8 is null"); + Objects.requireNonNull(combiner, "combiner is null"); + return combineLatestArray(new Publisher[] { source1, source2, source3, source4, source5, source6, source7, source8 }, Functions.toFunction(combiner), bufferSize()); + } + + /** + * Combines nine source {@link Publisher}s by emitting an item that aggregates the latest values of each of the + * source {@code Publisher}s each time an item is received from any of the source {@code Publisher}s, where this + * aggregation is defined by a specified function. + * <p> + * If any of the sources never produces an item but only terminates (normally or with an error), the + * resulting sequence terminates immediately (normally or with all the errors accumulated until that point). + * If that input source is also synchronous, other sources after it will not be subscribed to. + * <p> + * <img width="640" height="380" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/combineLatest.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The returned {@code Publisher} honors backpressure from downstream. The source {@code Publisher}s + * are requested in a bounded manner, however, their backpressure is not enforced (the operator won't signal + * {@link MissingBackpressureException}) and may lead to {@link OutOfMemoryError} due to internal buffer bloat.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code combineLatest} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T1> the element type of the first source + * @param <T2> the element type of the second source + * @param <T3> the element type of the third source + * @param <T4> the element type of the fourth source + * @param <T5> the element type of the fifth source + * @param <T6> the element type of the sixth source + * @param <T7> the element type of the seventh source + * @param <T8> the element type of the eighth source + * @param <T9> the element type of the ninth source + * @param <R> the combined output type + * @param source1 + * the first source {@code Publisher} + * @param source2 + * the second source {@code Publisher} + * @param source3 + * the third source {@code Publisher} + * @param source4 + * the fourth source {@code Publisher} + * @param source5 + * the fifth source {@code Publisher} + * @param source6 + * the sixth source {@code Publisher} + * @param source7 + * the seventh source {@code Publisher} + * @param source8 + * the eighth source {@code Publisher} + * @param source9 + * the ninth source {@code Publisher} + * @param combiner + * the aggregation function used to combine the items emitted by the source {@code Publisher}s + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code source1}, {@code source2}, {@code source3}, + * {@code source4}, {@code source5}, {@code source6}, + * {@code source7}, {@code source8}, {@code source9} + * or {@code combiner} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/combinelatest.html">ReactiveX operators documentation: CombineLatest</a> + */ + @SuppressWarnings("unchecked") + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T1, @NonNull T2, @NonNull T3, @NonNull T4, @NonNull T5, @NonNull T6, @NonNull T7, @NonNull T8, @NonNull T9, @NonNull R> Flowable<R> combineLatest( + @NonNull Publisher<? extends T1> source1, @NonNull Publisher<? extends T2> source2, + @NonNull Publisher<? extends T3> source3, @NonNull Publisher<? extends T4> source4, + @NonNull Publisher<? extends T5> source5, @NonNull Publisher<? extends T6> source6, + @NonNull Publisher<? extends T7> source7, @NonNull Publisher<? extends T8> source8, + @NonNull Publisher<? extends T9> source9, + @NonNull Function9<? super T1, ? super T2, ? super T3, ? super T4, ? super T5, ? super T6, ? super T7, ? super T8, ? super T9, ? extends R> combiner) { + Objects.requireNonNull(source1, "source1 is null"); + Objects.requireNonNull(source2, "source2 is null"); + Objects.requireNonNull(source3, "source3 is null"); + Objects.requireNonNull(source4, "source4 is null"); + Objects.requireNonNull(source5, "source5 is null"); + Objects.requireNonNull(source6, "source6 is null"); + Objects.requireNonNull(source7, "source7 is null"); + Objects.requireNonNull(source8, "source8 is null"); + Objects.requireNonNull(source9, "source9 is null"); + Objects.requireNonNull(combiner, "combiner is null"); + return combineLatestArray(new Publisher[] { source1, source2, source3, source4, source5, source6, source7, source8, source9 }, Functions.toFunction(combiner), bufferSize()); + } + + /** + * Concatenates elements of each {@link Publisher} provided via an {@link Iterable} sequence into a single sequence + * of elements without interleaving them. + * <p> + * <img width="640" height="380" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/concat.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream. The {@code Publisher} + * sources are expected to honor backpressure as well. + * If any of the source {@code Publisher}s violate this, it <em>may</em> throw an + * {@link IllegalStateException} when that {@code Publisher} completes.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code concat} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <T> the common value type of the sources + * @param sources the {@code Iterable} sequence of {@code Publisher}s + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code sources} is {@code null} + */ + @SuppressWarnings({ "unchecked", "rawtypes" }) + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T> Flowable<T> concat(@NonNull Iterable<@NonNull ? extends Publisher<? extends T>> sources) { + Objects.requireNonNull(sources, "sources is null"); + // unlike general sources, fromIterable can only throw on a boundary because it is consumed only there + return fromIterable(sources).concatMapDelayError((Function)Functions.identity(), false, 2); + } + + /** + * Returns a {@code Flowable} that emits the items emitted by each of the {@link Publisher}s emitted by the source + * {@code Publisher}, one after the other, without interleaving them. + * <p> + * <img width="640" height="380" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/concat.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream. Both the outer and inner {@code Publisher} + * sources are expected to honor backpressure as well. If the outer violates this, a + * {@link MissingBackpressureException} is signaled. If any of the inner {@code Publisher}s violates + * this, it <em>may</em> throw an {@link IllegalStateException} when an inner {@code Publisher} completes.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code concat} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T> the common element base type + * @param sources + * a {@code Publisher} that emits {@code Publisher}s + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code sources} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/concat.html">ReactiveX operators documentation: Concat</a> + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public static <@NonNull T> Flowable<T> concat(@NonNull Publisher<@NonNull ? extends Publisher<? extends T>> sources) { + return concat(sources, bufferSize()); + } + + /** + * Returns a {@code Flowable} that emits the items emitted by each of the {@link Publisher}s emitted by the source + * {@code Publisher}, one after the other, without interleaving them. + * <p> + * <img width="640" height="380" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/concat.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream. Both the outer and inner {@code Publisher} + * sources are expected to honor backpressure as well. If the outer violates this, a + * {@link MissingBackpressureException} is signaled. If any of the inner {@code Publisher}s violates + * this, it <em>may</em> throw an {@link IllegalStateException} when an inner {@code Publisher} completes.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code concat} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T> the common element base type + * @param sources + * a {@code Publisher} that emits {@code Publisher}s + * @param prefetch + * the number of {@code Publisher}s to prefetch from the sources sequence. + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code sources} is {@code null} + * @throws IllegalArgumentException if {@code prefetch} is non-positive + * @see <a href="http://reactivex.io/documentation/operators/concat.html">ReactiveX operators documentation: Concat</a> + */ + @SuppressWarnings({ "unchecked", "rawtypes" }) + @CheckReturnValue + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public static <@NonNull T> Flowable<T> concat(@NonNull Publisher<@NonNull ? extends Publisher<? extends T>> sources, int prefetch) { + return fromPublisher(sources).concatMap((Function)Functions.identity(), prefetch); + } + + /** + * Returns a {@code Flowable} that emits the items emitted by two {@link Publisher}s, one after the other, without + * interleaving them. + * <p> + * <img width="640" height="380" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/concat.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream. The {@code Publisher} + * sources are expected to honor backpressure as well. + * If any of the source {@code Publisher}s violate this, it <em>may</em> throw an + * {@link IllegalStateException} when that source {@code Publisher} completes.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code concat} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T> the common element base type + * @param source1 + * a {@code Publisher} to be concatenated + * @param source2 + * a {@code Publisher} to be concatenated + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code source1} or {@code source2} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/concat.html">ReactiveX operators documentation: Concat</a> + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T> Flowable<T> concat(@NonNull Publisher<? extends T> source1, @NonNull Publisher<? extends T> source2) { + Objects.requireNonNull(source1, "source1 is null"); + Objects.requireNonNull(source2, "source2 is null"); + return concatArray(source1, source2); + } + + /** + * Returns a {@code Flowable} that emits the items emitted by three {@link Publisher}s, one after the other, without + * interleaving them. + * <p> + * <img width="640" height="380" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/concat.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream. The {@code Publisher} + * sources are expected to honor backpressure as well. + * If any of the source {@code Publisher}s violate this, it <em>may</em> throw an + * {@link IllegalStateException} when that source {@code Publisher} completes.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code concat} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T> the common element base type + * @param source1 + * a {@code Publisher} to be concatenated + * @param source2 + * a {@code Publisher} to be concatenated + * @param source3 + * a {@code Publisher} to be concatenated + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code source1}, {@code source2} or {@code source3} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/concat.html">ReactiveX operators documentation: Concat</a> + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T> Flowable<T> concat( + @NonNull Publisher<? extends T> source1, @NonNull Publisher<? extends T> source2, + @NonNull Publisher<? extends T> source3) { + Objects.requireNonNull(source1, "source1 is null"); + Objects.requireNonNull(source2, "source2 is null"); + Objects.requireNonNull(source3, "source3 is null"); + return concatArray(source1, source2, source3); + } + + /** + * Returns a {@code Flowable} that emits the items emitted by four {@link Publisher}s, one after the other, without + * interleaving them. + * <p> + * <img width="640" height="380" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/concat.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream. The {@code Publisher} + * sources are expected to honor backpressure as well. + * If any of the source {@code Publisher}s violate this, it <em>may</em> throw an + * {@link IllegalStateException} when that source {@code Publisher} completes.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code concat} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T> the common element base type + * @param source1 + * a {@code Publisher} to be concatenated + * @param source2 + * a {@code Publisher} to be concatenated + * @param source3 + * a {@code Publisher} to be concatenated + * @param source4 + * a {@code Publisher} to be concatenated + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code source1}, {@code source2}, {@code source3} or {@code source4} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/concat.html">ReactiveX operators documentation: Concat</a> + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T> Flowable<T> concat( + @NonNull Publisher<? extends T> source1, @NonNull Publisher<? extends T> source2, + @NonNull Publisher<? extends T> source3, @NonNull Publisher<? extends T> source4) { + Objects.requireNonNull(source1, "source1 is null"); + Objects.requireNonNull(source2, "source2 is null"); + Objects.requireNonNull(source3, "source3 is null"); + Objects.requireNonNull(source4, "source4 is null"); + return concatArray(source1, source2, source3, source4); + } + + /** + * Concatenates a variable number of {@link Publisher} sources. + * <p> + * Note: named this way because of overload conflict with {@code concat(Publisher<Publisher<T>>}). + * <p> + * <img width="640" height="380" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/concat.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream. The {@code Publisher} + * sources are expected to honor backpressure as well. + * If any of the source {@code Publisher}s violate this, it <em>may</em> throw an + * {@link IllegalStateException} when that source {@code Publisher} completes.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code concatArray} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param sources the array of source {@code Publisher}s + * @param <T> the common base value type + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code sources} is {@code null} + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + @SafeVarargs + @NonNull + public static <@NonNull T> Flowable<T> concatArray(@NonNull Publisher<? extends T>... sources) { + Objects.requireNonNull(sources, "sources is null"); + if (sources.length == 0) { + return empty(); + } else + if (sources.length == 1) { + return fromPublisher(sources[0]); + } + return RxJavaPlugins.onAssembly(new FlowableConcatArray<>(sources, false)); + } + + /** + * Concatenates a variable number of {@link Publisher} sources and delays errors from any of them + * till all terminate. + * <p> + * <img width="640" height="380" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/concat.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream. The {@code Publisher} + * sources are expected to honor backpressure as well. + * If any of the source {@code Publisher}s violate this, it <em>may</em> throw an + * {@link IllegalStateException} when that source {@code Publisher} completes.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code concatArrayDelayError} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param sources the array of source {@code Publisher}s + * @param <T> the common base value type + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code sources} is {@code null} + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + @SafeVarargs + @NonNull + public static <@NonNull T> Flowable<T> concatArrayDelayError(@NonNull Publisher<? extends T>... sources) { + Objects.requireNonNull(sources, "sources is null"); + if (sources.length == 0) { + return empty(); + } else + if (sources.length == 1) { + return fromPublisher(sources[0]); + } + return RxJavaPlugins.onAssembly(new FlowableConcatArray<>(sources, true)); + } + + /** + * Concatenates an array of {@link Publisher}s eagerly into a single stream of values. + * <p> + * <img width="640" height="380" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Flowable.concatArrayEager.png" alt=""> + * <p> + * Eager concatenation means that once a subscriber subscribes, this operator subscribes to all of the + * source {@code Publisher}s. The operator buffers the values emitted by these {@code Publisher}s and then drains them + * in order, each one after the previous one completes. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream. The {@code Publisher} + * sources are expected to honor backpressure as well. + * If any of the source {@code Publisher}s violate this, the operator will signal a + * {@link MissingBackpressureException}.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>This method does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <T> the value type + * @param sources an array of {@code Publisher}s that need to be eagerly concatenated + * @return the new {@code Flowable} instance with the specified concatenation behavior + * @throws NullPointerException if {@code sources} is {@code null} + * @since 2.0 + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + @SafeVarargs + @NonNull + public static <@NonNull T> Flowable<T> concatArrayEager(@NonNull Publisher<? extends T>... sources) { + return concatArrayEager(bufferSize(), bufferSize(), sources); + } + + /** + * Concatenates an array of {@link Publisher}s eagerly into a single stream of values. + * <p> + * <img width="640" height="406" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Flowable.concatArrayEager.nn.png" alt=""> + * <p> + * Eager concatenation means that once a subscriber subscribes, this operator subscribes to all of the + * source {@code Publisher}s. The operator buffers the values emitted by these {@code Publisher}s and then drains them + * in order, each one after the previous one completes. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream. The {@code Publisher} + * sources are expected to honor backpressure as well. + * If any of the source {@code Publisher}s violate this, the operator will signal a + * {@link MissingBackpressureException}.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>This method does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <T> the value type + * @param sources an array of {@code Publisher}s that need to be eagerly concatenated + * @param maxConcurrency the maximum number of concurrent subscriptions at a time, {@link Integer#MAX_VALUE} + * is interpreted as an indication to subscribe to all sources at once + * @param prefetch the number of elements to prefetch from each {@code Publisher} source + * @return the new {@code Flowable} instance with the specified concatenation behavior + * @throws NullPointerException if {@code sources} is {@code null} + * @throws IllegalArgumentException if {@code maxConcurrency} or {@code prefetch} is non-positive + * @since 2.0 + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + @SuppressWarnings({ "rawtypes", "unchecked" }) + @SafeVarargs + public static <@NonNull T> Flowable<T> concatArrayEager(int maxConcurrency, int prefetch, @NonNull Publisher<? extends T>... sources) { + Objects.requireNonNull(sources, "sources is null"); + ObjectHelper.verifyPositive(maxConcurrency, "maxConcurrency"); + ObjectHelper.verifyPositive(prefetch, "prefetch"); + return RxJavaPlugins.onAssembly(new FlowableConcatMapEager(new FlowableFromArray(sources), Functions.identity(), maxConcurrency, prefetch, ErrorMode.IMMEDIATE)); + } + + /** + * Concatenates an array of {@link Publisher}s eagerly into a single stream of values + * and delaying any errors until all sources terminate. + * <p> + * <img width="640" height="358" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Flowable.concatArrayEagerDelayError.png" alt=""> + * <p> + * Eager concatenation means that once a subscriber subscribes, this operator subscribes to all of the + * source {@code Publisher}s. The operator buffers the values emitted by these {@code Publisher}s + * and then drains them in order, each one after the previous one completes. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream. The {@code Publisher} + * sources are expected to honor backpressure as well. + * If any of the source {@code Publisher}s violate this, the operator will signal a + * {@link MissingBackpressureException}.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>This method does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <T> the value type + * @param sources an array of {@code Publisher}s that need to be eagerly concatenated + * @return the new {@code Flowable} instance with the specified concatenation behavior + * @throws NullPointerException if {@code sources} is {@code null} + * @since 2.2.1 - experimental + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @BackpressureSupport(BackpressureKind.FULL) + @SafeVarargs + @NonNull + public static <@NonNull T> Flowable<T> concatArrayEagerDelayError(@NonNull Publisher<? extends T>... sources) { + return concatArrayEagerDelayError(bufferSize(), bufferSize(), sources); + } + + /** + * Concatenates an array of {@link Publisher}s eagerly into a single stream of values + * and delaying any errors until all sources terminate. + * <p> + * <img width="640" height="359" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Flowable.concatArrayEagerDelayError.nn.png" alt=""> + * <p> + * Eager concatenation means that once a subscriber subscribes, this operator subscribes to all of the + * source {@code Publisher}s. The operator buffers the values emitted by these {@code Publisher}s + * and then drains them in order, each one after the previous one completes. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream. The {@code Publisher} + * sources are expected to honor backpressure as well. + * If any of the source {@code Publisher}s violate this, the operator will signal a + * {@link MissingBackpressureException}.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>This method does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <T> the value type + * @param sources an array of {@code Publisher}s that need to be eagerly concatenated + * @param maxConcurrency the maximum number of concurrent subscriptions at a time, {@link Integer#MAX_VALUE} + * is interpreted as indication to subscribe to all sources at once + * @param prefetch the number of elements to prefetch from each {@code Publisher} source + * @return the new {@code Flowable} instance with the specified concatenation behavior + * @throws NullPointerException if {@code sources} is {@code null} + * @throws IllegalArgumentException if {@code maxConcurrency} or {@code prefetch} is non-positive + * @since 2.2.1 - experimental + */ + @SuppressWarnings({ "rawtypes", "unchecked" }) + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @BackpressureSupport(BackpressureKind.FULL) + @SafeVarargs + @NonNull + public static <@NonNull T> Flowable<T> concatArrayEagerDelayError(int maxConcurrency, int prefetch, @NonNull Publisher<? extends T>... sources) { + return fromArray(sources).concatMapEagerDelayError((Function)Functions.identity(), true, maxConcurrency, prefetch); + } + + /** + * Concatenates the {@link Iterable} sequence of {@link Publisher}s into a single sequence by subscribing to each {@code Publisher}, + * one after the other, one at a time and delays any errors till the all inner {@code Publisher}s terminate. + * + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream. Both the outer and inner {@code Publisher} + * sources are expected to honor backpressure as well. If the outer violates this, a + * {@link MissingBackpressureException} is signaled. If any of the inner {@code Publisher}s violates + * this, it <em>may</em> throw an {@link IllegalStateException} when an inner {@code Publisher} completes.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code concatDelayError} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T> the common element base type + * @param sources the {@code Iterable} sequence of {@code Publisher}s + * @return the new {@code Flowable} with the concatenating behavior + * @throws NullPointerException if {@code sources} is {@code null} + */ + @SuppressWarnings({ "unchecked", "rawtypes" }) + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T> Flowable<T> concatDelayError(@NonNull Iterable<@NonNull ? extends Publisher<? extends T>> sources) { + Objects.requireNonNull(sources, "sources is null"); + return fromIterable(sources).concatMapDelayError((Function)Functions.identity()); + } + + /** + * Concatenates the {@link Publisher} sequence of {@code Publisher}s into a single sequence by subscribing to each inner {@code Publisher}, + * one after the other, one at a time and delays any errors till the all inner and the outer {@code Publisher}s terminate. + * + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>{@code concatDelayError} fully supports backpressure.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code concatDelayError} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T> the common element base type + * @param sources the {@code Publisher} sequence of {@code Publisher}s + * @return the new {@code Flowable} with the concatenating behavior + * @throws NullPointerException if {@code sources} is {@code null} + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public static <@NonNull T> Flowable<T> concatDelayError(@NonNull Publisher<@NonNull ? extends Publisher<? extends T>> sources) { + return concatDelayError(sources, bufferSize(), true); + } + + /** + * Concatenates the {@link Publisher} sequence of {@code Publisher}s into a single sequence by subscribing to each inner {@code Publisher}, + * one after the other, one at a time and delays any errors till the all inner and the outer {@code Publisher}s terminate. + * + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>{@code concatDelayError} fully supports backpressure.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code concatDelayError} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T> the common element base type + * @param sources the {@code Publisher} sequence of {@code Publisher}s + * @param prefetch the number of elements to prefetch from the outer {@code Publisher} + * @param tillTheEnd if {@code true}, exceptions from the outer and all inner {@code Publisher}s are delayed to the end + * if {@code false}, exception from the outer {@code Publisher} is delayed till the current inner {@code Publisher} terminates + * @return the new {@code Flowable} with the concatenating behavior + * @throws NullPointerException if {@code sources} is {@code null} + * @throws IllegalArgumentException if {@code prefetch} is {@code null} + */ + @SuppressWarnings({ "unchecked", "rawtypes" }) + @CheckReturnValue + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public static <@NonNull T> Flowable<T> concatDelayError(@NonNull Publisher<@NonNull ? extends Publisher<? extends T>> sources, int prefetch, boolean tillTheEnd) { + return fromPublisher(sources).concatMapDelayError((Function)Functions.identity(), tillTheEnd, prefetch); + } + + /** + * Concatenates a sequence of {@link Publisher}s eagerly into a single stream of values. + * <p> + * <img width="640" height="422" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Flowable.concatEager.i.png" alt=""> + * <p> + * Eager concatenation means that once a subscriber subscribes, this operator subscribes to all of the + * source {@code Publisher}s. The operator buffers the values emitted by these {@code Publisher}s and then drains them + * in order, each one after the previous one completes. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>Backpressure is honored towards the downstream and the inner {@code Publisher}s are + * expected to support backpressure. Violating this assumption, the operator will + * signal {@link MissingBackpressureException}.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>This method does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <T> the value type + * @param sources a sequence of {@code Publisher}s that need to be eagerly concatenated + * @return the new {@code Flowable} instance with the specified concatenation behavior + * @throws NullPointerException if {@code sources} is {@code null} + * @since 2.0 + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public static <@NonNull T> Flowable<T> concatEager(@NonNull Iterable<@NonNull ? extends Publisher<? extends T>> sources) { + return concatEager(sources, bufferSize(), bufferSize()); + } + + /** + * Concatenates a sequence of {@link Publisher}s eagerly into a single stream of values and + * runs a limited number of inner sequences at once. + * <p> + * <img width="640" height="375" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Flowable.concatEager.in.png" alt=""> + * <p> + * Eager concatenation means that once a subscriber subscribes, this operator subscribes to all of the + * source {@code Publisher}s. The operator buffers the values emitted by these {@code Publisher}s and then drains them + * in order, each one after the previous one completes. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>Backpressure is honored towards the downstream and both the outer and inner {@code Publisher}s are + * expected to support backpressure. Violating this assumption, the operator will + * signal {@link MissingBackpressureException}.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>This method does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <T> the value type + * @param sources a sequence of {@code Publisher}s that need to be eagerly concatenated + * @param maxConcurrency the maximum number of concurrently running inner {@code Publisher}s; {@link Integer#MAX_VALUE} + * is interpreted as all inner {@code Publisher}s can be active at the same time + * @param prefetch the number of elements to prefetch from each inner {@code Publisher} source + * @return the new {@code Flowable} instance with the specified concatenation behavior + * @throws NullPointerException if {@code sources} is {@code null} + * @throws IllegalArgumentException if {@code maxConcurrency} or {@code prefetch} is non-positive + * @since 2.0 + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + @SuppressWarnings({ "rawtypes", "unchecked" }) + public static <@NonNull T> Flowable<T> concatEager(@NonNull Iterable<@NonNull ? extends Publisher<? extends T>> sources, int maxConcurrency, int prefetch) { + Objects.requireNonNull(sources, "sources is null"); + ObjectHelper.verifyPositive(maxConcurrency, "maxConcurrency"); + ObjectHelper.verifyPositive(prefetch, "prefetch"); + return RxJavaPlugins.onAssembly(new FlowableConcatMapEager(new FlowableFromIterable(sources), Functions.identity(), maxConcurrency, prefetch, ErrorMode.BOUNDARY)); + } + + /** + * Concatenates a {@link Publisher} sequence of {@code Publisher}s eagerly into a single stream of values. + * <p> + * <img width="640" height="490" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Flowable.concatEager.p.png" alt=""> + * <p> + * Eager concatenation means that once a subscriber subscribes, this operator subscribes to all of the + * emitted source {@code Publisher}s as they are observed. The operator buffers the values emitted by these + * {@code Publisher}s and then drains them in order, each one after the previous one completes. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>Backpressure is honored towards the downstream and both the outer and inner {@code Publisher}s are + * expected to support backpressure. Violating this assumption, the operator will + * signal {@link MissingBackpressureException}.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>This method does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <T> the value type + * @param sources a sequence of {@code Publisher}s that need to be eagerly concatenated + * @return the new {@code Flowable} instance with the specified concatenation behavior + * @throws NullPointerException if {@code sources} is {@code null} + * @since 2.0 + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public static <@NonNull T> Flowable<T> concatEager(@NonNull Publisher<@NonNull ? extends Publisher<? extends T>> sources) { + return concatEager(sources, bufferSize(), bufferSize()); + } + + /** + * Concatenates a {@link Publisher} sequence of {@code Publisher}s eagerly into a single stream of values and + * runs a limited number of inner sequences at once. + * <p> + * <img width="640" height="421" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Flowable.concatEager.pn.png" alt=""> + * <p> + * Eager concatenation means that once a subscriber subscribes, this operator subscribes to all of the + * emitted source {@code Publisher}s as they are observed. The operator buffers the values emitted by these + * {@code Publisher}s and then drains them in order, each one after the previous one completes. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>Backpressure is honored towards the downstream and both the outer and inner {@code Publisher}s are + * expected to support backpressure. Violating this assumption, the operator will + * signal {@link MissingBackpressureException}.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>This method does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <T> the value type + * @param sources a sequence of {@code Publisher}s that need to be eagerly concatenated + * @param maxConcurrency the maximum number of concurrently running inner {@code Publisher}s; {@link Integer#MAX_VALUE} + * is interpreted as all inner {@code Publisher}s can be active at the same time + * @param prefetch the number of elements to prefetch from each inner {@code Publisher} source + * @return the new {@code Flowable} instance with the specified concatenation behavior + * @throws NullPointerException if {@code sources} is {@code null} + * @throws IllegalArgumentException if {@code maxConcurrency} or {@code prefetch} is non-positive + * @since 2.0 + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + @SuppressWarnings({ "rawtypes", "unchecked" }) + public static <@NonNull T> Flowable<T> concatEager(@NonNull Publisher<@NonNull ? extends Publisher<? extends T>> sources, int maxConcurrency, int prefetch) { + Objects.requireNonNull(sources, "sources is null"); + ObjectHelper.verifyPositive(maxConcurrency, "maxConcurrency"); + ObjectHelper.verifyPositive(prefetch, "prefetch"); + return RxJavaPlugins.onAssembly(new FlowableConcatMapEagerPublisher(sources, Functions.identity(), maxConcurrency, prefetch, ErrorMode.IMMEDIATE)); + } + + /** + * Concatenates a sequence of {@link Publisher}s eagerly into a single stream of values, + * delaying errors until all the inner sequences terminate. + * <p> + * <img width="640" height="428" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Flowable.concatEagerDelayError.i.png" alt=""> + * <p> + * Eager concatenation means that once a subscriber subscribes, this operator subscribes to all of the + * source {@code Publisher}s. The operator buffers the values emitted by these {@code Publisher}s and then drains them + * in order, each one after the previous one completes. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>Backpressure is honored towards the downstream and the inner {@code Publisher}s are + * expected to support backpressure. Violating this assumption, the operator will + * signal {@link MissingBackpressureException}.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>This method does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <T> the value type + * @param sources a sequence of {@code Publisher}s that need to be eagerly concatenated + * @return the new {@code Flowable} instance with the specified concatenation behavior + * @throws NullPointerException if {@code sources} is {@code null} + * @since 3.0.0 + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public static <@NonNull T> Flowable<T> concatEagerDelayError(@NonNull Iterable<@NonNull ? extends Publisher<? extends T>> sources) { + return concatEagerDelayError(sources, bufferSize(), bufferSize()); + } + + /** + * Concatenates a sequence of {@link Publisher}s eagerly into a single stream of values, + * delaying errors until all the inner sequences terminate and runs a limited number + * of inner sequences at once. + * <p> + * <img width="640" height="380" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Flowable.concatEagerDelayError.in.png" alt=""> + * <p> + * Eager concatenation means that once a subscriber subscribes, this operator subscribes to all of the + * source {@code Publisher}s. The operator buffers the values emitted by these {@code Publisher}s and then drains them + * in order, each one after the previous one completes. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>Backpressure is honored towards the downstream and both the outer and inner {@code Publisher}s are + * expected to support backpressure. Violating this assumption, the operator will + * signal {@link MissingBackpressureException}.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>This method does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <T> the value type + * @param sources a sequence of {@code Publisher}s that need to be eagerly concatenated + * @param maxConcurrency the maximum number of concurrently running inner {@code Publisher}s; {@link Integer#MAX_VALUE} + * is interpreted as all inner {@code Publisher}s can be active at the same time + * @param prefetch the number of elements to prefetch from each inner {@code Publisher} source + * @return the new {@code Flowable} instance with the specified concatenation behavior + * @throws NullPointerException if {@code sources} is {@code null} + * @throws IllegalArgumentException if {@code maxConcurrency} or {@code prefetch} is non-positive + * @since 3.0.0 + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + @SuppressWarnings({ "rawtypes", "unchecked" }) + public static <@NonNull T> Flowable<T> concatEagerDelayError(@NonNull Iterable<@NonNull ? extends Publisher<? extends T>> sources, int maxConcurrency, int prefetch) { + Objects.requireNonNull(sources, "sources is null"); + ObjectHelper.verifyPositive(maxConcurrency, "maxConcurrency"); + ObjectHelper.verifyPositive(prefetch, "prefetch"); + return RxJavaPlugins.onAssembly(new FlowableConcatMapEager(new FlowableFromIterable(sources), Functions.identity(), maxConcurrency, prefetch, ErrorMode.END)); + } + + /** + * Concatenates a {@link Publisher} sequence of {@code Publisher}s eagerly into a single stream of values, + * delaying errors until all the inner and the outer sequences terminate. + * <p> + * <img width="640" height="496" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Flowable.concatEagerDelayError.p.png" alt=""> + * <p> + * Eager concatenation means that once a subscriber subscribes, this operator subscribes to all of the + * emitted source {@code Publisher}s as they are observed. The operator buffers the values emitted by these + * {@code Publisher}s and then drains them in order, each one after the previous one completes. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>Backpressure is honored towards the downstream and both the outer and inner {@code Publisher}s are + * expected to support backpressure. Violating this assumption, the operator will + * signal {@link MissingBackpressureException}.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>This method does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <T> the value type + * @param sources a sequence of {@code Publisher}s that need to be eagerly concatenated + * @return the new {@code Flowable} instance with the specified concatenation behavior + * @throws NullPointerException if {@code sources} is {@code null} + * @since 3.0.0 + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public static <@NonNull T> Flowable<T> concatEagerDelayError(@NonNull Publisher<@NonNull ? extends Publisher<? extends T>> sources) { + return concatEagerDelayError(sources, bufferSize(), bufferSize()); + } + + /** + * Concatenates a {@link Publisher} sequence of {@code Publisher}s eagerly into a single stream of values, + * delaying errors until all the inner and outer sequences terminate and runs a limited number of inner + * sequences at once. + * <p> + * <img width="640" height="421" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Flowable.concatEagerDelayError.pn.png" alt=""> + * <p> + * Eager concatenation means that once a subscriber subscribes, this operator subscribes to all of the + * emitted source {@code Publisher}s as they are observed. The operator buffers the values emitted by these + * {@code Publisher}s and then drains them in order, each one after the previous one completes. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>Backpressure is honored towards the downstream and both the outer and inner {@code Publisher}s are + * expected to support backpressure. Violating this assumption, the operator will + * signal {@link MissingBackpressureException}.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>This method does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <T> the value type + * @param sources a sequence of {@code Publisher}s that need to be eagerly concatenated + * @param maxConcurrency the maximum number of concurrently running inner {@code Publisher}s; {@link Integer#MAX_VALUE} + * is interpreted as all inner {@code Publisher}s can be active at the same time + * @param prefetch the number of elements to prefetch from each inner {@code Publisher} source + * @return the new {@code Flowable} instance with the specified concatenation behavior + * @throws NullPointerException if {@code sources} is {@code null} + * @throws IllegalArgumentException if {@code maxConcurrency} or {@code prefetch} is non-positive + * @since 3.0.0 + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + @SuppressWarnings({ "rawtypes", "unchecked" }) + public static <@NonNull T> Flowable<T> concatEagerDelayError(@NonNull Publisher<@NonNull ? extends Publisher<? extends T>> sources, int maxConcurrency, int prefetch) { + Objects.requireNonNull(sources, "sources is null"); + ObjectHelper.verifyPositive(maxConcurrency, "maxConcurrency"); + ObjectHelper.verifyPositive(prefetch, "prefetch"); + return RxJavaPlugins.onAssembly(new FlowableConcatMapEagerPublisher(sources, Functions.identity(), maxConcurrency, prefetch, ErrorMode.END)); + } + + /** + * Provides an API (via a cold {@code Flowable}) that bridges the reactive world with the callback-style, + * generally non-backpressured world. + * <p> + * Example: + * <pre><code> + * Flowable.<Event>create(emitter -> { + * Callback listener = new Callback() { + * @Override + * public void onEvent(Event e) { + * emitter.onNext(e); + * if (e.isLast()) { + * emitter.onComplete(); + * } + * } + * + * @Override + * public void onFailure(Exception e) { + * emitter.onError(e); + * } + * }; + * + * AutoCloseable c = api.someMethod(listener); + * + * emitter.setCancellable(c::close); + * + * }, BackpressureStrategy.BUFFER); + * </code></pre> + * <p> + * Whenever a {@link Subscriber} subscribes to the returned {@code Flowable}, the provided + * {@link FlowableOnSubscribe} callback is invoked with a fresh instance of a {@link FlowableEmitter} + * that will interact only with that specific {@code Subscriber}. If this {@code Subscriber} + * cancels the flow (making {@link FlowableEmitter#isCancelled} return {@code true}), + * other observers subscribed to the same returned {@code Flowable} are not affected. + * <p> + * You should call the {@link FlowableEmitter#onNext(Object)}, {@link FlowableEmitter#onError(Throwable)} + * and {@link FlowableEmitter#onComplete()} methods in a serialized fashion. The + * rest of its methods are thread-safe. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The backpressure behavior is determined by the {@code mode} parameter.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code create} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T> the element type + * @param source the emitter that is called when a {@code Subscriber} subscribes to the returned {@code Flowable} + * @param mode the backpressure mode to apply if the downstream {@code Subscriber} doesn't request (fast) enough + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code source} or {@code mode} is {@code null} + * @see FlowableOnSubscribe + * @see BackpressureStrategy + * @see Cancellable + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.SPECIAL) + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T> Flowable<T> create(@NonNull FlowableOnSubscribe<T> source, @NonNull BackpressureStrategy mode) { + Objects.requireNonNull(source, "source is null"); + Objects.requireNonNull(mode, "mode is null"); + return RxJavaPlugins.onAssembly(new FlowableCreate<>(source, mode)); + } + + /** + * Returns a {@code Flowable} that calls a {@link Publisher} factory to create a {@code Publisher} for each new {@link Subscriber} + * that subscribes. That is, for each subscriber, the actual {@code Publisher} that subscriber observes is + * determined by the factory function. + * <p> + * <img width="640" height="340" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/defer.v3.png" alt=""> + * <p> + * The defer {@code Subscriber} allows you to defer or delay emitting items from a {@code Publisher} until such time as a + * {@code Subscriber} subscribes to the {@code Publisher}. This allows a {@code Subscriber} to easily obtain updates or a + * refreshed version of the sequence. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator itself doesn't interfere with backpressure which is determined by the {@code Publisher} + * returned by the {@code supplier}.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code defer} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param supplier + * the {@code Publisher} factory function to invoke for each {@code Subscriber} that subscribes to the + * resulting {@code Flowable} + * @param <T> + * the type of the items emitted by the {@code Publisher} + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code supplier} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/defer.html">ReactiveX operators documentation: Defer</a> + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.PASS_THROUGH) + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T> Flowable<T> defer(@NonNull Supplier<? extends @NonNull Publisher<? extends T>> supplier) { + Objects.requireNonNull(supplier, "supplier is null"); + return RxJavaPlugins.onAssembly(new FlowableDefer<>(supplier)); + } + + /** + * Returns a {@code Flowable} that emits no items to the {@link Subscriber} and immediately invokes its + * {@link Subscriber#onComplete onComplete} method. + * <p> + * <img width="640" height="190" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/empty.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>This source doesn't produce any elements and effectively ignores downstream backpressure.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code empty} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T> + * the type of the items (ostensibly) emitted by the {@link Publisher} + * @return the shared {@code Flowable} instance + * @see <a href="http://reactivex.io/documentation/operators/empty-never-throw.html">ReactiveX operators documentation: Empty</a> + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.PASS_THROUGH) + @SchedulerSupport(SchedulerSupport.NONE) + @SuppressWarnings("unchecked") + @NonNull + public static <@NonNull T> Flowable<T> empty() { + return RxJavaPlugins.onAssembly((Flowable<T>) FlowableEmpty.INSTANCE); + } + + /** + * Returns a {@code Flowable} that invokes a {@link Subscriber}'s {@link Subscriber#onError onError} method when the + * {@code Subscriber} subscribes to it. + * <p> + * <img width="640" height="190" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/error.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>This source doesn't produce any elements and effectively ignores downstream backpressure.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code error} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param supplier + * a {@link Supplier} factory to return a {@link Throwable} for each individual {@code Subscriber} + * @param <T> + * the type of the items (ostensibly) emitted by the resulting {@code Flowable} + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code supplier} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/empty-never-throw.html">ReactiveX operators documentation: Throw</a> + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.PASS_THROUGH) + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T> Flowable<T> error(@NonNull Supplier<? extends @NonNull Throwable> supplier) { + Objects.requireNonNull(supplier, "supplier is null"); + return RxJavaPlugins.onAssembly(new FlowableError<>(supplier)); + } + + /** + * Returns a {@code Flowable} that invokes a {@link Subscriber}'s {@link Subscriber#onError onError} method when the + * {@code Subscriber} subscribes to it. + * <p> + * <img width="640" height="190" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/error.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>This source doesn't produce any elements and effectively ignores downstream backpressure.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code error} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param throwable + * the particular {@link Throwable} to pass to {@link Subscriber#onError onError} + * @param <T> + * the type of the items (ostensibly) emitted by the resulting {@code Flowable} + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code throwable} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/empty-never-throw.html">ReactiveX operators documentation: Throw</a> + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.PASS_THROUGH) + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T> Flowable<T> error(@NonNull Throwable throwable) { + Objects.requireNonNull(throwable, "throwable is null"); + return error(Functions.justSupplier(throwable)); + } + + /** + * Returns a {@code Flowable} instance that runs the given {@link Action} for each {@link Subscriber} and + * emits either its exception or simply completes. + * <p> + * <img width="640" height="285" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Flowable.fromAction.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>This source doesn't produce any elements and effectively ignores downstream backpressure.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code fromAction} does not operate by default on a particular {@link Scheduler}.</dd> + * <dt><b>Error handling:</b></dt> + * <dd> If the {@code Action} throws an exception, the respective {@link Throwable} is + * delivered to the downstream via {@link Subscriber#onError(Throwable)}, + * except when the downstream has canceled the resulting {@code Flowable} source. + * In this latter case, the {@code Throwable} is delivered to the global error handler via + * {@link RxJavaPlugins#onError(Throwable)} as an {@link io.reactivex.rxjava3.exceptions.UndeliverableException UndeliverableException}. + * </dd> + * </dl> + * @param <T> the target type + * @param action the {@code Action} to run for each {@code Subscriber} + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code action} is {@code null} + * @since 3.0.0 + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + @BackpressureSupport(BackpressureKind.PASS_THROUGH) + public static <@NonNull T> Flowable<T> fromAction(@NonNull Action action) { + Objects.requireNonNull(action, "action is null"); + return RxJavaPlugins.onAssembly(new FlowableFromAction<>(action)); + } + + /** + * Converts an array into a {@link Publisher} that emits the items in the array. + * <p> + * <img width="640" height="315" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/from.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream and iterates the given {@code array} + * on demand (i.e., when requested).</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code fromArray} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param items + * the array of elements + * @param <T> + * the type of items in the array and the type of items to be emitted by the resulting {@code Flowable} + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code items} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/from.html">ReactiveX operators documentation: From</a> + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + @SafeVarargs + public static <@NonNull T> Flowable<T> fromArray(@NonNull T... items) { + Objects.requireNonNull(items, "items is null"); + if (items.length == 0) { + return empty(); + } + if (items.length == 1) { + return just(items[0]); + } + return RxJavaPlugins.onAssembly(new FlowableFromArray<>(items)); + } + + /** + * Returns a {@code Flowable} that, when a {@link Subscriber} subscribes to it, invokes a function you specify and then + * emits the value returned from that function. + * <p> + * <img width="640" height="310" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/fromCallable.v3.png" alt=""> + * <p> + * This allows you to defer the execution of the function you specify until a {@code Subscriber} subscribes to the + * {@link Publisher}. That is to say, it makes the function "lazy." + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code fromCallable} does not operate by default on a particular {@link Scheduler}.</dd> + * <dt><b>Error handling:</b></dt> + * <dd> If the {@link Callable} throws an exception, the respective {@link Throwable} is + * delivered to the downstream via {@link Subscriber#onError(Throwable)}, + * except when the downstream has canceled this {@code Flowable} source. + * In this latter case, the {@code Throwable} is delivered to the global error handler via + * {@link RxJavaPlugins#onError(Throwable)} as an {@link io.reactivex.rxjava3.exceptions.UndeliverableException UndeliverableException}. + * </dd> + * </dl> + * + * @param callable + * a function, the execution of which should be deferred; {@code fromCallable} will invoke this + * function only when a {@code Subscriber} subscribes to the {@code Publisher} that {@code fromCallable} returns + * @param <T> + * the type of the item emitted by the {@code Publisher} + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code callable} is {@code null} + * @see #defer(Supplier) + * @see #fromSupplier(Supplier) + * @since 2.0 + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T> Flowable<T> fromCallable(@NonNull Callable<? extends T> callable) { + Objects.requireNonNull(callable, "callable is null"); + return RxJavaPlugins.onAssembly(new FlowableFromCallable<>(callable)); + } + + /** + * Wraps a {@link CompletableSource} into a {@code Flowable}. + * <p> + * <img width="640" height="278" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Flowable.fromCompletable.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>This source doesn't produce any elements and effectively ignores downstream backpressure.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code fromCompletable} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <T> the target type + * @param completableSource the {@code CompletableSource} to convert from + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code completableSource} is {@code null} + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + @BackpressureSupport(BackpressureKind.PASS_THROUGH) + public static <@NonNull T> Flowable<T> fromCompletable(@NonNull CompletableSource completableSource) { + Objects.requireNonNull(completableSource, "completableSource is null"); + return RxJavaPlugins.onAssembly(new FlowableFromCompletable<>(completableSource)); + } + + /** + * Converts a {@link Future} into a {@link Publisher}. + * <p> + * <img width="640" height="315" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/from.Future.v3.png" alt=""> + * <p> + * The operator calls {@link Future#get()}, which is a blocking method, on the subscription thread. + * It is recommended applying {@link #subscribeOn(Scheduler)} to move this blocking wait to a + * background thread, and if the {@link Scheduler} supports it, interrupt the wait when the flow + * is disposed. + * <p> + * Also note that this operator will consume a {@link CompletionStage}-based {@code Future} subclass (such as + * {@link CompletableFuture}) in a blocking manner as well. Use the {@link #fromCompletionStage(CompletionStage)} + * operator to convert and consume such sources in a non-blocking fashion instead. + * <p> + * Unlike 1.x, canceling the {@code Flowable} won't cancel the future. If necessary, one can use composition to achieve the + * cancellation effect: {@code futurePublisher.doOnCancel(() -> future.cancel(true));}. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code fromFuture} does not operate by default on a particular {@code Scheduler}.</dd> + * </dl> + * + * @param future + * the source {@code Future} + * @param <T> + * the type of object that the {@code Future} returns, and also the type of item to be emitted by + * the resulting {@code Flowable} + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code future} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/from.html">ReactiveX operators documentation: From</a> + * @see #fromCompletionStage(CompletionStage) + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T> Flowable<T> fromFuture(@NonNull Future<? extends T> future) { + Objects.requireNonNull(future, "future is null"); + return RxJavaPlugins.onAssembly(new FlowableFromFuture<>(future, 0L, null)); + } + + /** + * Converts a {@link Future} into a {@link Publisher}, with a timeout on the {@code Future}. + * <p> + * <img width="640" height="315" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/from.Future.v3.png" alt=""> + * <p> + * The operator calls {@link Future#get(long, TimeUnit)}, which is a blocking method, on the subscription thread. + * It is recommended applying {@link #subscribeOn(Scheduler)} to move this blocking wait to a + * background thread, and if the {@link Scheduler} supports it, interrupt the wait when the flow + * is disposed. + * <p> + * Unlike 1.x, canceling the {@code Flowable} won't cancel the future. If necessary, one can use composition to achieve the + * cancellation effect: {@code futurePublisher.doOnCancel(() -> future.cancel(true));}. + * <p> + * Also note that this operator will consume a {@link CompletionStage}-based {@code Future} subclass (such as + * {@link CompletableFuture}) in a blocking manner as well. Use the {@link #fromCompletionStage(CompletionStage)} + * operator to convert and consume such sources in a non-blocking fashion instead. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code fromFuture} does not operate by default on a particular {@code Scheduler}.</dd> + * </dl> + * + * @param future + * the source {@code Future} + * @param timeout + * the maximum time to wait before calling {@code get} + * @param unit + * the {@link TimeUnit} of the {@code timeout} argument + * @param <T> + * the type of object that the {@code Future} returns, and also the type of item to be emitted by + * the resulting {@code Flowable} + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code future} or {@code unit} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/from.html">ReactiveX operators documentation: From</a> + * @see #fromCompletionStage(CompletionStage) + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T> Flowable<T> fromFuture(@NonNull Future<? extends T> future, long timeout, @NonNull TimeUnit unit) { + Objects.requireNonNull(future, "future is null"); + Objects.requireNonNull(unit, "unit is null"); + return RxJavaPlugins.onAssembly(new FlowableFromFuture<>(future, timeout, unit)); + } + + /** + * Converts an {@link Iterable} sequence into a {@link Publisher} that emits the items in the sequence. + * <p> + * <img width="640" height="315" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/from.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream and iterates the given {@code iterable} + * on demand (i.e., when requested).</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code fromIterable} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param source + * the source {@code Iterable} sequence + * @param <T> + * the type of items in the {@code Iterable} sequence and the type of items to be emitted by the + * resulting {@code Flowable} + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code source} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/from.html">ReactiveX operators documentation: From</a> + * @see #fromStream(Stream) + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T> Flowable<T> fromIterable(@NonNull Iterable<? extends T> source) { + Objects.requireNonNull(source, "source is null"); + return RxJavaPlugins.onAssembly(new FlowableFromIterable<>(source)); + } + + /** + * Returns a {@code Flowable} instance that when subscribed to, subscribes to the {@link MaybeSource} instance and + * emits {@code onSuccess} as a single item or forwards any {@code onComplete} or + * {@code onError} signal. + * <p> + * <img width="640" height="226" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Flowable.fromMaybe.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code fromMaybe} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <T> the value type of the {@code MaybeSource} element + * @param maybe the {@code MaybeSource} instance to subscribe to, not {@code null} + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code maybe} is {@code null} + * @since 3.0.0 + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + @BackpressureSupport(BackpressureKind.FULL) + public static <@NonNull T> Flowable<T> fromMaybe(@NonNull MaybeSource<T> maybe) { + Objects.requireNonNull(maybe, "maybe is null"); + return RxJavaPlugins.onAssembly(new MaybeToFlowable<>(maybe)); + } + + /** + * Converts the given {@link ObservableSource} into a {@code Flowable} by applying the specified backpressure strategy. + * <p> + * Marble diagrams for the various backpressure strategies are as follows: + * <ul> + * <li>{@link BackpressureStrategy#BUFFER} + * <p> + * <img width="640" height="264" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Flowable.fromObservable.buffer.png" alt=""> + * </li> + * <li>{@link BackpressureStrategy#DROP} + * <p> + * <img width="640" height="374" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Flowable.fromObservable.drop.png" alt=""> + * </li> + * <li>{@link BackpressureStrategy#LATEST} + * <p> + * <img width="640" height="284" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Flowable.fromObservable.latest.png" alt=""> + * </li> + * <li>{@link BackpressureStrategy#ERROR} + * <p> + * <img width="640" height="365" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Flowable.fromObservable.error.png" alt=""> + * </li> + * <li>{@link BackpressureStrategy#MISSING} + * <p> + * <img width="640" height="397" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Flowable.fromObservable.missing.png" alt=""> + * </li> + * </ul> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator applies the chosen backpressure strategy of {@link BackpressureStrategy} enum.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code fromObservable} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T> the element type of the source and resulting sequence + * @param source the {@code ObservableSource} to convert + * @param strategy the backpressure strategy to apply + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code source} or {@code strategy} is {@code null} + */ + @BackpressureSupport(BackpressureKind.SPECIAL) + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public static <@NonNull T> Flowable<T> fromObservable(@NonNull ObservableSource<T> source, @NonNull BackpressureStrategy strategy) { + Objects.requireNonNull(source, "source is null"); + Objects.requireNonNull(strategy, "strategy is null"); + Flowable<T> f = new FlowableFromObservable<>(source); + switch (strategy) { + case DROP: + return f.onBackpressureDrop(); + case LATEST: + return f.onBackpressureLatest(); + case MISSING: + return f; + case ERROR: + return RxJavaPlugins.onAssembly(new FlowableOnBackpressureError<>(f)); + default: + return f.onBackpressureBuffer(); + } + } + + /** + * Converts an arbitrary <em>Reactive Streams</em> {@link Publisher} into a {@code Flowable} if not already a + * {@code Flowable}. + * <p> + * The {@code Publisher} must follow the + * <a href="https://github.com/reactive-streams/reactive-streams-jvm#reactive-streams">Reactive-Streams specification</a>. + * Violating the specification may result in undefined behavior. + * <p> + * If possible, use {@link #create(FlowableOnSubscribe, BackpressureStrategy)} to create a + * source-like {@code Flowable} instead. + * <p> + * Note that even though {@code Publisher} appears to be a functional interface, it + * is not recommended to implement it through a lambda as the specification requires + * state management that is not achievable with a stateless lambda. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator is a pass-through for backpressure and its behavior is determined by the + * backpressure behavior of the wrapped publisher.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code fromPublisher} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <T> the value type of the flow + * @param publisher the {@code Publisher} to convert + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code publisher} is {@code null} + * @see #create(FlowableOnSubscribe, BackpressureStrategy) + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.PASS_THROUGH) + @SchedulerSupport(SchedulerSupport.NONE) + @SuppressWarnings("unchecked") + public static <@NonNull T> Flowable<T> fromPublisher(@NonNull Publisher<? extends T> publisher) { + if (publisher instanceof Flowable) { + return RxJavaPlugins.onAssembly((Flowable<T>)publisher); + } + Objects.requireNonNull(publisher, "publisher is null"); + + return RxJavaPlugins.onAssembly(new FlowableFromPublisher<>(publisher)); + } + + /** + * Returns a {@code Flowable} instance that runs the given {@link Runnable} for each {@link Subscriber} and + * emits either its unchecked exception or simply completes. + * <p> + * <img width="640" height="286" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Flowable.fromRunnable.png" alt=""> + * <p> + * If the code to be wrapped needs to throw a checked or more broader {@link Throwable} exception, that + * exception has to be converted to an unchecked exception by the wrapped code itself. Alternatively, + * use the {@link #fromAction(Action)} method which allows the wrapped code to throw any {@code Throwable} + * exception and will signal it to observers as-is. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>This source doesn't produce any elements and effectively ignores downstream backpressure.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code fromRunnable} does not operate by default on a particular {@link Scheduler}.</dd> + * <dt><b>Error handling:</b></dt> + * <dd> If the {@code Runnable} throws an exception, the respective {@code Throwable} is + * delivered to the downstream via {@link Subscriber#onError(Throwable)}, + * except when the downstream has canceled the resulting {@code Flowable} source. + * In this latter case, the {@code Throwable} is delivered to the global error handler via + * {@link RxJavaPlugins#onError(Throwable)} as an {@link io.reactivex.rxjava3.exceptions.UndeliverableException UndeliverableException}. + * </dd> + * </dl> + * @param <T> the target type + * @param run the {@code Runnable} to run for each {@code Subscriber} + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code run} is {@code null} + * @since 3.0.0 + * @see #fromAction(Action) + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + @BackpressureSupport(BackpressureKind.PASS_THROUGH) + public static <@NonNull T> Flowable<T> fromRunnable(@NonNull Runnable run) { + Objects.requireNonNull(run, "run is null"); + return RxJavaPlugins.onAssembly(new FlowableFromRunnable<>(run)); + } + + /** + * Returns a {@code Flowable} instance that when subscribed to, subscribes to the {@link SingleSource} instance and + * emits {@code onSuccess} as a single item or forwards the {@code onError} signal. + * <p> + * <img width="640" height="341" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Flowable.fromSingle.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code fromSingle} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <T> the value type of the {@code SingleSource} element + * @param source the {@code SingleSource} instance to subscribe to, not {@code null} + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code source} is {@code null} + * @since 3.0.0 + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + @BackpressureSupport(BackpressureKind.FULL) + public static <@NonNull T> Flowable<T> fromSingle(@NonNull SingleSource<T> source) { + Objects.requireNonNull(source, "source is null"); + return RxJavaPlugins.onAssembly(new SingleToFlowable<>(source)); + } + + /** + * Returns a {@code Flowable} that, when a {@link Subscriber} subscribes to it, invokes a supplier function you specify and then + * emits the value returned from that function. + * <p> + * <img width="640" height="310" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Flowable.fromSupplier.v3.png" alt=""> + * <p> + * This allows you to defer the execution of the function you specify until a {@code Subscriber} subscribes to the + * {@link Publisher}. That is to say, it makes the function "lazy." + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code fromSupplier} does not operate by default on a particular {@link Scheduler}.</dd> + * <dt><b>Error handling:</b></dt> + * <dd> If the {@link Supplier} throws an exception, the respective {@link Throwable} is + * delivered to the downstream via {@link Subscriber#onError(Throwable)}, + * except when the downstream has canceled this {@code Flowable} source. + * In this latter case, the {@code Throwable} is delivered to the global error handler via + * {@link RxJavaPlugins#onError(Throwable)} as an {@link io.reactivex.rxjava3.exceptions.UndeliverableException UndeliverableException}. + * </dd> + * </dl> + * + * @param supplier + * a function, the execution of which should be deferred; {@code fromSupplier} will invoke this + * function only when a {@code Subscriber} subscribes to the {@code Publisher} that {@code fromSupplier} returns + * @param <T> + * the type of the item emitted by the {@code Publisher} + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code supplier} is {@code null} + * @see #defer(Supplier) + * @see #fromCallable(Callable) + * @since 3.0.0 + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T> Flowable<T> fromSupplier(@NonNull Supplier<? extends T> supplier) { + Objects.requireNonNull(supplier, "supplier is null"); + return RxJavaPlugins.onAssembly(new FlowableFromSupplier<>(supplier)); + } + + /** + * Returns a cold, synchronous, stateless and backpressure-aware generator of values. + * <p> + * Note that the {@link Emitter#onNext}, {@link Emitter#onError} and + * {@link Emitter#onComplete} methods provided to the function via the {@link Emitter} instance should be called synchronously, + * never concurrently and only while the function body is executing. Calling them from multiple threads + * or outside the function call is not supported and leads to an undefined behavior. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors downstream backpressure.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code generate} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T> the generated value type + * @param generator the {@link Consumer} called whenever a particular downstream {@link Subscriber} has + * requested a value. The callback then should call {@code onNext}, {@code onError} or + * {@code onComplete} to signal a value or a terminal event. Signaling multiple {@code onNext} + * in a call will make the operator signal {@link IllegalStateException}. + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code generator} is {@code null} + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T> Flowable<T> generate(@NonNull Consumer<@NonNull Emitter<T>> generator) { + Objects.requireNonNull(generator, "generator is null"); + return generate(Functions.nullSupplier(), + FlowableInternalHelper.simpleGenerator(generator), + Functions.emptyConsumer()); + } + + /** + * Returns a cold, synchronous, stateful and backpressure-aware generator of values. + * <p> + * Note that the {@link Emitter#onNext}, {@link Emitter#onError} and + * {@link Emitter#onComplete} methods provided to the function via the {@link Emitter} instance should be called synchronously, + * never concurrently and only while the function body is executing. Calling them from multiple threads + * or outside the function call is not supported and leads to an undefined behavior. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors downstream backpressure.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code generate} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <S> the type of the per-{@link Subscriber} state + * @param <T> the generated value type + * @param initialState the {@link Supplier} to generate the initial state for each {@code Subscriber} + * @param generator the {@link Consumer} called with the current state whenever a particular downstream {@code Subscriber} has + * requested a value. The callback then should call {@code onNext}, {@code onError} or + * {@code onComplete} to signal a value or a terminal event. Signaling multiple {@code onNext} + * in a call will make the operator signal {@link IllegalStateException}. + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code initialState} or {@code generator} is {@code null} + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T, @NonNull S> Flowable<T> generate(@NonNull Supplier<S> initialState, @NonNull BiConsumer<S, Emitter<T>> generator) { + Objects.requireNonNull(generator, "generator is null"); + return generate(initialState, FlowableInternalHelper.simpleBiGenerator(generator), + Functions.emptyConsumer()); + } + + /** + * Returns a cold, synchronous, stateful and backpressure-aware generator of values. + * <p> + * Note that the {@link Emitter#onNext}, {@link Emitter#onError} and + * {@link Emitter#onComplete} methods provided to the function via the {@link Emitter} instance should be called synchronously, + * never concurrently and only while the function body is executing. Calling them from multiple threads + * or outside the function call is not supported and leads to an undefined behavior. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors downstream backpressure.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code generate} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <S> the type of the per-{@link Subscriber} state + * @param <T> the generated value type + * @param initialState the {@link Supplier} to generate the initial state for each {@code Subscriber} + * @param generator the {@link Consumer} called with the current state whenever a particular downstream {@code Subscriber} has + * requested a value. The callback then should call {@code onNext}, {@code onError} or + * {@code onComplete} to signal a value or a terminal event. Signaling multiple {@code onNext} + * in a call will make the operator signal {@link IllegalStateException}. + * @param disposeState the {@code Consumer} that is called with the current state when the generator + * terminates the sequence or it gets canceled + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code initialState}, {@code generator} or {@code disposeState} is {@code null} + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T, @NonNull S> Flowable<T> generate(@NonNull Supplier<S> initialState, @NonNull BiConsumer<S, Emitter<T>> generator, + @NonNull Consumer<? super S> disposeState) { + Objects.requireNonNull(generator, "generator is null"); + return generate(initialState, FlowableInternalHelper.simpleBiGenerator(generator), disposeState); + } + + /** + * Returns a cold, synchronous, stateful and backpressure-aware generator of values. + * <p> + * Note that the {@link Emitter#onNext}, {@link Emitter#onError} and + * {@link Emitter#onComplete} methods provided to the function via the {@link Emitter} instance should be called synchronously, + * never concurrently and only while the function body is executing. Calling them from multiple threads + * or outside the function call is not supported and leads to an undefined behavior. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors downstream backpressure.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code generate} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <S> the type of the per-{@link Subscriber} state + * @param <T> the generated value type + * @param initialState the {@link Supplier} to generate the initial state for each {@code Subscriber} + * @param generator the {@link Function} called with the current state whenever a particular downstream {@code Subscriber} has + * requested a value. The callback then should call {@code onNext}, {@code onError} or + * {@code onComplete} to signal a value or a terminal event and should return a (new) state for + * the next invocation. Signaling multiple {@code onNext} + * in a call will make the operator signal {@link IllegalStateException}. + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code initialState} or {@code generator} is {@code null} + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public static <@NonNull T, @NonNull S> Flowable<T> generate(@NonNull Supplier<S> initialState, @NonNull BiFunction<S, @NonNull Emitter<T>, S> generator) { + return generate(initialState, generator, Functions.emptyConsumer()); + } + + /** + * Returns a cold, synchronous, stateful and backpressure-aware generator of values. + * <p> + * Note that the {@link Emitter#onNext}, {@link Emitter#onError} and + * {@link Emitter#onComplete} methods provided to the function via the {@link Emitter} instance should be called synchronously, + * never concurrently and only while the function body is executing. Calling them from multiple threads + * or outside the function call is not supported and leads to an undefined behavior. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors downstream backpressure.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code generate} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <S> the type of the per-{@link Subscriber} state + * @param <T> the generated value type + * @param initialState the {@link Supplier} to generate the initial state for each {@code Subscriber} + * @param generator the {@link Function} called with the current state whenever a particular downstream {@code Subscriber} has + * requested a value. The callback then should call {@code onNext}, {@code onError} or + * {@code onComplete} to signal a value or a terminal event and should return a (new) state for + * the next invocation. Signaling multiple {@code onNext} + * in a call will make the operator signal {@link IllegalStateException}. + * @param disposeState the {@link Consumer} that is called with the current state when the generator + * terminates the sequence or it gets canceled + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code initialState}, {@code generator} or {@code disposeState} is {@code null} + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T, @NonNull S> Flowable<T> generate(@NonNull Supplier<S> initialState, @NonNull BiFunction<S, @NonNull Emitter<T>, S> generator, @NonNull Consumer<? super S> disposeState) { + Objects.requireNonNull(initialState, "initialState is null"); + Objects.requireNonNull(generator, "generator is null"); + Objects.requireNonNull(disposeState, "disposeState is null"); + return RxJavaPlugins.onAssembly(new FlowableGenerate<>(initialState, generator, disposeState)); + } + + /** + * Returns a {@code Flowable} that emits a {@code 0L} after the {@code initialDelay} and ever-increasing numbers + * after each {@code period} of time thereafter. + * <p> + * <img width="640" height="200" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/timer.p.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator generates values based on time and ignores downstream backpressure which + * may lead to {@link MissingBackpressureException} at some point in the chain. + * Downstream consumers should consider applying one of the {@code onBackpressureXXX} operators as well.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code interval} operates by default on the {@code computation} {@link Scheduler}.</dd> + * </dl> + * + * @param initialDelay + * the initial delay time to wait before emitting the first value of 0L + * @param period + * the period of time between emissions of the subsequent numbers + * @param unit + * the time unit for both {@code initialDelay} and {@code period} + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code unit} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/interval.html">ReactiveX operators documentation: Interval</a> + * @since 1.0.12 + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.ERROR) + @SchedulerSupport(SchedulerSupport.COMPUTATION) + @NonNull + public static Flowable<Long> interval(long initialDelay, long period, @NonNull TimeUnit unit) { + return interval(initialDelay, period, unit, Schedulers.computation()); + } + + /** + * Returns a {@code Flowable} that emits a {@code 0L} after the {@code initialDelay} and ever-increasing numbers + * after each {@code period} of time thereafter, on a specified {@link Scheduler}. + * <p> + * <img width="640" height="200" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/timer.ps.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator generates values based on time and ignores downstream backpressure which + * may lead to {@link MissingBackpressureException} at some point in the chain. + * Downstream consumers should consider applying one of the {@code onBackpressureXXX} operators as well.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>You specify which {@code Scheduler} this operator will use.</dd> + * </dl> + * + * @param initialDelay + * the initial delay time to wait before emitting the first value of 0L + * @param period + * the period of time between emissions of the subsequent numbers + * @param unit + * the time unit for both {@code initialDelay} and {@code period} + * @param scheduler + * the {@code Scheduler} on which the waiting happens and items are emitted + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code unit} or {@code scheduler} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/interval.html">ReactiveX operators documentation: Interval</a> + * @since 1.0.12 + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.ERROR) + @SchedulerSupport(SchedulerSupport.CUSTOM) + public static Flowable<Long> interval(long initialDelay, long period, @NonNull TimeUnit unit, @NonNull Scheduler scheduler) { + Objects.requireNonNull(unit, "unit is null"); + Objects.requireNonNull(scheduler, "scheduler is null"); + return RxJavaPlugins.onAssembly(new FlowableInterval(Math.max(0L, initialDelay), Math.max(0L, period), unit, scheduler)); + } + + /** + * Returns a {@code Flowable} that emits a sequential number every specified interval of time. + * <p> + * <img width="640" height="195" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/interval.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator signals a {@link MissingBackpressureException} if the downstream + * is not ready to receive the next value.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code interval} operates by default on the {@code computation} {@link Scheduler}.</dd> + * </dl> + * + * @param period + * the period size in time units (see below) + * @param unit + * time units to use for the interval size + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code unit} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/interval.html">ReactiveX operators documentation: Interval</a> + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.ERROR) + @SchedulerSupport(SchedulerSupport.COMPUTATION) + @NonNull + public static Flowable<Long> interval(long period, @NonNull TimeUnit unit) { + return interval(period, period, unit, Schedulers.computation()); + } + + /** + * Returns a {@code Flowable} that emits a sequential number every specified interval of time, on a + * specified {@link Scheduler}. + * <p> + * <img width="640" height="200" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/interval.s.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator generates values based on time and ignores downstream backpressure which + * may lead to {@link MissingBackpressureException} at some point in the chain. + * Downstream consumers should consider applying one of the {@code onBackpressureXXX} operators as well.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>You specify which {@code Scheduler} this operator will use.</dd> + * </dl> + * + * @param period + * the period size in time units (see below) + * @param unit + * time units to use for the interval size + * @param scheduler + * the {@code Scheduler} to use for scheduling the items + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code unit} or {@code scheduler} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/interval.html">ReactiveX operators documentation: Interval</a> + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.ERROR) + @SchedulerSupport(SchedulerSupport.CUSTOM) + @NonNull + public static Flowable<Long> interval(long period, @NonNull TimeUnit unit, @NonNull Scheduler scheduler) { + return interval(period, period, unit, scheduler); + } + + /** + * Signals a range of long values, the first after some initial delay and the rest periodically after. + * <p> + * The sequence completes immediately after the last value {@code (start + count - 1)} has been reached. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator signals a {@link MissingBackpressureException} if the downstream can't keep up.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code intervalRange} by default operates on the {@link Schedulers#computation() computation} {@link Scheduler}.</dd> + * </dl> + * @param start that start value of the range + * @param count the number of values to emit in total, if zero, the operator emits an {@code onComplete} after the initial delay. + * @param initialDelay the initial delay before signaling the first value (the start) + * @param period the period between subsequent values + * @param unit the unit of measure of the {@code initialDelay} and {@code period} amounts + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code unit} is {@code null} + * @throws IllegalArgumentException + * if {@code count} is less than zero, or if {@code start} + {@code count} − 1 exceeds + * {@link Long#MAX_VALUE} + * @see #range(int, int) + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.ERROR) + @SchedulerSupport(SchedulerSupport.COMPUTATION) + public static Flowable<Long> intervalRange(long start, long count, long initialDelay, long period, @NonNull TimeUnit unit) { + return intervalRange(start, count, initialDelay, period, unit, Schedulers.computation()); + } + + /** + * Signals a range of long values, the first after some initial delay and the rest periodically after. + * <p> + * The sequence completes immediately after the last value (start + count - 1) has been reached. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator signals a {@link MissingBackpressureException} if the downstream can't keep up.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>you provide the {@link Scheduler}.</dd> + * </dl> + * @param start that start value of the range + * @param count the number of values to emit in total, if zero, the operator emits an {@code onComplete} after the initial delay. + * @param initialDelay the initial delay before signaling the first value (the start) + * @param period the period between subsequent values + * @param unit the unit of measure of the {@code initialDelay} and {@code period} amounts + * @param scheduler the target {@code Scheduler} where the values and terminal signals will be emitted + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code unit} or {@code scheduler} is {@code null} + * @throws IllegalArgumentException + * if {@code count} is less than zero, or if {@code start} + {@code count} − 1 exceeds + * {@link Long#MAX_VALUE} + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.ERROR) + @SchedulerSupport(SchedulerSupport.CUSTOM) + public static Flowable<Long> intervalRange(long start, long count, long initialDelay, long period, @NonNull TimeUnit unit, @NonNull Scheduler scheduler) { + if (count < 0L) { + throw new IllegalArgumentException("count >= 0 required but it was " + count); + } + if (count == 0L) { + return Flowable.<Long>empty().delay(initialDelay, unit, scheduler); + } + + long end = start + (count - 1); + if (start > 0 && end < 0) { + throw new IllegalArgumentException("Overflow! start + count is bigger than Long.MAX_VALUE"); + } + Objects.requireNonNull(unit, "unit is null"); + Objects.requireNonNull(scheduler, "scheduler is null"); + + return RxJavaPlugins.onAssembly(new FlowableIntervalRange(start, end, Math.max(0L, initialDelay), Math.max(0L, period), unit, scheduler)); + } + + /** + * Returns a {@code Flowable} that signals the given (constant reference) item and then completes. + * <p> + * <img width="640" height="310" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/just.v3.png" alt=""> + * <p> + * Note that the item is taken and re-emitted as is and not computed by any means by {@code just}. Use {@link #fromCallable(Callable)} + * to generate a single item on demand (when {@link Subscriber}s subscribe to it). + * <p> + * See the multi-parameter overloads of {@code just} to emit more than one (constant reference) items one after the other. + * Use {@link #fromArray(Object...)} to emit an arbitrary number of items that are known upfront. + * <p> + * To emit the items of an {@link Iterable} sequence (such as a {@link java.util.List}), use {@link #fromIterable(Iterable)}. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code just} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param item + * the item to emit + * @param <T> + * the type of that item + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code item} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/just.html">ReactiveX operators documentation: Just</a> + * @see #just(Object, Object) + * @see #fromCallable(Callable) + * @see #fromArray(Object...) + * @see #fromIterable(Iterable) + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T> Flowable<T> just(T item) { + Objects.requireNonNull(item, "item is null"); + return RxJavaPlugins.onAssembly(new FlowableJust<>(item)); + } + + /** + * Converts two items into a {@link Publisher} that emits those items. + * <p> + * <img width="640" height="315" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/just.m.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream and signals each value on-demand (i.e., when requested).</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code just} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param item1 + * first item + * @param item2 + * second item + * @param <T> + * the type of these items + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code item1} or {@code item2} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/just.html">ReactiveX operators documentation: Just</a> + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T> Flowable<T> just(T item1, T item2) { + Objects.requireNonNull(item1, "item1 is null"); + Objects.requireNonNull(item2, "item2 is null"); + + return fromArray(item1, item2); + } + + /** + * Converts three items into a {@link Publisher} that emits those items. + * <p> + * <img width="640" height="315" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/just.m.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream and signals each value on-demand (i.e., when requested).</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code just} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param item1 + * first item + * @param item2 + * second item + * @param item3 + * third item + * @param <T> + * the type of these items + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code item1}, {@code item2} or {@code item3} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/just.html">ReactiveX operators documentation: Just</a> + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T> Flowable<T> just(T item1, T item2, T item3) { + Objects.requireNonNull(item1, "item1 is null"); + Objects.requireNonNull(item2, "item2 is null"); + Objects.requireNonNull(item3, "item3 is null"); + + return fromArray(item1, item2, item3); + } + + /** + * Converts four items into a {@link Publisher} that emits those items. + * <p> + * <img width="640" height="315" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/just.m.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream and signals each value on-demand (i.e., when requested).</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code just} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param item1 + * first item + * @param item2 + * second item + * @param item3 + * third item + * @param item4 + * fourth item + * @param <T> + * the type of these items + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code item1}, {@code item2}, {@code item3}, + * or {@code item4} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/just.html">ReactiveX operators documentation: Just</a> + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T> Flowable<T> just(T item1, T item2, T item3, T item4) { + Objects.requireNonNull(item1, "item1 is null"); + Objects.requireNonNull(item2, "item2 is null"); + Objects.requireNonNull(item3, "item3 is null"); + Objects.requireNonNull(item4, "item4 is null"); + + return fromArray(item1, item2, item3, item4); + } + + /** + * Converts five items into a {@link Publisher} that emits those items. + * <p> + * <img width="640" height="315" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/just.m.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream and signals each value on-demand (i.e., when requested).</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code just} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param item1 + * first item + * @param item2 + * second item + * @param item3 + * third item + * @param item4 + * fourth item + * @param item5 + * fifth item + * @param <T> + * the type of these items + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code item1}, {@code item2}, {@code item3}, + * {@code item4} or {@code item5} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/just.html">ReactiveX operators documentation: Just</a> + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T> Flowable<T> just(T item1, T item2, T item3, T item4, T item5) { + Objects.requireNonNull(item1, "item1 is null"); + Objects.requireNonNull(item2, "item2 is null"); + Objects.requireNonNull(item3, "item3 is null"); + Objects.requireNonNull(item4, "item4 is null"); + Objects.requireNonNull(item5, "item5 is null"); + + return fromArray(item1, item2, item3, item4, item5); + } + + /** + * Converts six items into a {@link Publisher} that emits those items. + * <p> + * <img width="640" height="315" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/just.m.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream and signals each value on-demand (i.e., when requested).</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code just} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param item1 + * first item + * @param item2 + * second item + * @param item3 + * third item + * @param item4 + * fourth item + * @param item5 + * fifth item + * @param item6 + * sixth item + * @param <T> + * the type of these items + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code item1}, {@code item2}, {@code item3}, + * {@code item4}, {@code item5} or {@code item6} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/just.html">ReactiveX operators documentation: Just</a> + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T> Flowable<T> just(T item1, T item2, T item3, T item4, T item5, T item6) { + Objects.requireNonNull(item1, "item1 is null"); + Objects.requireNonNull(item2, "item2 is null"); + Objects.requireNonNull(item3, "item3 is null"); + Objects.requireNonNull(item4, "item4 is null"); + Objects.requireNonNull(item5, "item5 is null"); + Objects.requireNonNull(item6, "item6 is null"); + + return fromArray(item1, item2, item3, item4, item5, item6); + } + + /** + * Converts seven items into a {@link Publisher} that emits those items. + * <p> + * <img width="640" height="315" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/just.m.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream and signals each value on-demand (i.e., when requested).</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code just} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param item1 + * first item + * @param item2 + * second item + * @param item3 + * third item + * @param item4 + * fourth item + * @param item5 + * fifth item + * @param item6 + * sixth item + * @param item7 + * seventh item + * @param <T> + * the type of these items + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code item1}, {@code item2}, {@code item3}, + * {@code item4}, {@code item5}, {@code item6} + * or {@code item7} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/just.html">ReactiveX operators documentation: Just</a> + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T> Flowable<T> just(T item1, T item2, T item3, T item4, T item5, T item6, T item7) { + Objects.requireNonNull(item1, "item1 is null"); + Objects.requireNonNull(item2, "item2 is null"); + Objects.requireNonNull(item3, "item3 is null"); + Objects.requireNonNull(item4, "item4 is null"); + Objects.requireNonNull(item5, "item5 is null"); + Objects.requireNonNull(item6, "item6 is null"); + Objects.requireNonNull(item7, "item7 is null"); + + return fromArray(item1, item2, item3, item4, item5, item6, item7); + } + + /** + * Converts eight items into a {@link Publisher} that emits those items. + * <p> + * <img width="640" height="315" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/just.m.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream and signals each value on-demand (i.e., when requested).</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code just} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param item1 + * first item + * @param item2 + * second item + * @param item3 + * third item + * @param item4 + * fourth item + * @param item5 + * fifth item + * @param item6 + * sixth item + * @param item7 + * seventh item + * @param item8 + * eighth item + * @param <T> + * the type of these items + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code item1}, {@code item2}, {@code item3}, + * {@code item4}, {@code item5}, {@code item6}, + * {@code item7} or {@code item8} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/just.html">ReactiveX operators documentation: Just</a> + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T> Flowable<T> just(T item1, T item2, T item3, T item4, T item5, T item6, T item7, T item8) { + Objects.requireNonNull(item1, "item1 is null"); + Objects.requireNonNull(item2, "item2 is null"); + Objects.requireNonNull(item3, "item3 is null"); + Objects.requireNonNull(item4, "item4 is null"); + Objects.requireNonNull(item5, "item5 is null"); + Objects.requireNonNull(item6, "item6 is null"); + Objects.requireNonNull(item7, "item7 is null"); + Objects.requireNonNull(item8, "item8 is null"); + + return fromArray(item1, item2, item3, item4, item5, item6, item7, item8); + } + + /** + * Converts nine items into a {@link Publisher} that emits those items. + * <p> + * <img width="640" height="315" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/just.m.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream and signals each value on-demand (i.e., when requested).</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code just} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param item1 + * first item + * @param item2 + * second item + * @param item3 + * third item + * @param item4 + * fourth item + * @param item5 + * fifth item + * @param item6 + * sixth item + * @param item7 + * seventh item + * @param item8 + * eighth item + * @param item9 + * ninth item + * @param <T> + * the type of these items + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code item1}, {@code item2}, {@code item3}, + * {@code item4}, {@code item5}, {@code item6}, + * {@code item7}, {@code item8} or {@code item9} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/just.html">ReactiveX operators documentation: Just</a> + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T> Flowable<T> just(T item1, T item2, T item3, T item4, T item5, T item6, T item7, T item8, T item9) { + Objects.requireNonNull(item1, "item1 is null"); + Objects.requireNonNull(item2, "item2 is null"); + Objects.requireNonNull(item3, "item3 is null"); + Objects.requireNonNull(item4, "item4 is null"); + Objects.requireNonNull(item5, "item5 is null"); + Objects.requireNonNull(item6, "item6 is null"); + Objects.requireNonNull(item7, "item7 is null"); + Objects.requireNonNull(item8, "item8 is null"); + Objects.requireNonNull(item9, "item9 is null"); + + return fromArray(item1, item2, item3, item4, item5, item6, item7, item8, item9); + } + + /** + * Converts ten items into a {@link Publisher} that emits those items. + * <p> + * <img width="640" height="315" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/just.m.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream and signals each value on-demand (i.e., when requested).</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code just} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param item1 + * first item + * @param item2 + * second item + * @param item3 + * third item + * @param item4 + * fourth item + * @param item5 + * fifth item + * @param item6 + * sixth item + * @param item7 + * seventh item + * @param item8 + * eighth item + * @param item9 + * ninth item + * @param item10 + * tenth item + * @param <T> + * the type of these items + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code item1}, {@code item2}, {@code item3}, + * {@code item4}, {@code item5}, {@code item6}, + * {@code item7}, {@code item8}, {@code item9}, + * or {@code item10} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/just.html">ReactiveX operators documentation: Just</a> + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T> Flowable<T> just(T item1, T item2, T item3, T item4, T item5, T item6, T item7, T item8, T item9, T item10) { + Objects.requireNonNull(item1, "item1 is null"); + Objects.requireNonNull(item2, "item2 is null"); + Objects.requireNonNull(item3, "item3 is null"); + Objects.requireNonNull(item4, "item4 is null"); + Objects.requireNonNull(item5, "item5 is null"); + Objects.requireNonNull(item6, "item6 is null"); + Objects.requireNonNull(item7, "item7 is null"); + Objects.requireNonNull(item8, "item8 is null"); + Objects.requireNonNull(item9, "item9 is null"); + Objects.requireNonNull(item10, "item10 is null"); + + return fromArray(item1, item2, item3, item4, item5, item6, item7, item8, item9, item10); + } + + /** + * Flattens an {@link Iterable} of {@link Publisher}s into one {@code Publisher}, without any transformation, while limiting the + * number of concurrent subscriptions to these {@code Publisher}s. + * <p> + * <img width="640" height="380" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/merge.v3.png" alt=""> + * <p> + * You can combine the items emitted by multiple {@code Publisher}s so that they appear as a single {@code Publisher}, by + * using the {@code merge} method. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream. The source {@code Publisher}s are expected to honor + * backpressure; if violated, the operator <em>may</em> signal {@link MissingBackpressureException}.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code merge} does not operate by default on a particular {@link Scheduler}.</dd> + * <dt><b>Error handling:</b></dt> + * <dd>If any of the source {@code Publisher}s signal a {@link Throwable} via {@code onError}, the resulting + * {@code Flowable} terminates with that {@code Throwable} and all other source {@code Publisher}s are canceled. + * If more than one {@code Publisher} signals an error, the resulting {@code Flowable} may terminate with the + * first one's error or, depending on the concurrency of the sources, may terminate with a + * {@link CompositeException} containing two or more of the various error signals. + * {@code Throwable}s that didn't make into the composite will be sent (individually) to the global error handler via + * {@link RxJavaPlugins#onError(Throwable)} method as {@link UndeliverableException} errors. Similarly, {@code Throwable}s + * signaled by source(s) after the returned {@code Flowable} has been canceled or terminated with a + * (composite) error will be sent to the same global error handler. + * Use {@link #mergeDelayError(Iterable, int, int)} to merge sources and terminate only when all source {@code Publisher}s + * have completed or failed with an error. + * </dd> + * </dl> + * + * @param <T> the common element base type + * @param sources + * the {@code Iterable} of {@code Publisher}s + * @param maxConcurrency + * the maximum number of {@code Publisher}s that may be subscribed to concurrently + * @param bufferSize + * the number of items to prefetch from each inner {@code Publisher} + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code sources} is {@code null} + * @throws IllegalArgumentException + * if {@code maxConcurrency} or {@code bufferSize} is non-positive + * @see <a href="http://reactivex.io/documentation/operators/merge.html">ReactiveX operators documentation: Merge</a> + * @see #mergeDelayError(Iterable, int, int) + */ + @SuppressWarnings({ "unchecked", "rawtypes" }) + @CheckReturnValue + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public static <@NonNull T> Flowable<T> merge(@NonNull Iterable<@NonNull ? extends Publisher<? extends T>> sources, int maxConcurrency, int bufferSize) { + return fromIterable(sources).flatMap((Function)Functions.identity(), false, maxConcurrency, bufferSize); + } + + /** + * Flattens an array of {@link Publisher}s into one {@code Publisher}, without any transformation, while limiting the + * number of concurrent subscriptions to these {@code Publisher}s. + * <p> + * <img width="640" height="380" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/merge.v3.png" alt=""> + * <p> + * You can combine the items emitted by multiple {@code Publisher}s so that they appear as a single {@code Publisher}, by + * using the {@code merge} method. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream. The source {@code Publisher}s are expected to honor + * backpressure; if violated, the operator <em>may</em> signal {@link MissingBackpressureException}.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code mergeArray} does not operate by default on a particular {@link Scheduler}.</dd> + * <dt><b>Error handling:</b></dt> + * <dd>If any of the source {@code Publisher}s signal a {@link Throwable} via {@code onError}, the resulting + * {@code Flowable} terminates with that {@code Throwable} and all other source {@code Publisher}s are canceled. + * If more than one {@code Publisher} signals an error, the resulting {@code Flowable} may terminate with the + * first one's error or, depending on the concurrency of the sources, may terminate with a + * {@link CompositeException} containing two or more of the various error signals. + * {@code Throwable}s that didn't make into the composite will be sent (individually) to the global error handler via + * {@link RxJavaPlugins#onError(Throwable)} method as {@link UndeliverableException} errors. Similarly, {@code Throwable}s + * signaled by source(s) after the returned {@code Flowable} has been canceled or terminated with a + * (composite) error will be sent to the same global error handler. + * Use {@link #mergeArrayDelayError(int, int, Publisher[])} to merge sources and terminate only when all source {@code Publisher}s + * have completed or failed with an error. + * </dd> + * </dl> + * + * @param <T> the common element base type + * @param sources + * the array of {@code Publisher}s + * @param maxConcurrency + * the maximum number of {@code Publisher}s that may be subscribed to concurrently + * @param bufferSize + * the number of items to prefetch from each inner {@code Publisher} + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code sources} is {@code null} + * @throws IllegalArgumentException + * if {@code maxConcurrency} or {@code bufferSize} is non-positive + * @see <a href="http://reactivex.io/documentation/operators/merge.html">ReactiveX operators documentation: Merge</a> + * @see #mergeArrayDelayError(int, int, Publisher...) + */ + @SuppressWarnings({ "unchecked", "rawtypes" }) + @CheckReturnValue + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + @SafeVarargs + @NonNull + public static <@NonNull T> Flowable<T> mergeArray(int maxConcurrency, int bufferSize, @NonNull Publisher<? extends T>... sources) { + return fromArray(sources).flatMap((Function)Functions.identity(), false, maxConcurrency, bufferSize); + } + + /** + * Flattens an {@link Iterable} of {@link Publisher}s into one {@code Publisher}, without any transformation. + * <p> + * <img width="640" height="380" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/merge.v3.png" alt=""> + * <p> + * You can combine the items emitted by multiple {@code Publisher}s so that they appear as a single {@code Publisher}, by + * using the {@code merge} method. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream. The source {@code Publisher}s are expected to honor + * backpressure; if violated, the operator <em>may</em> signal {@link MissingBackpressureException}.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code merge} does not operate by default on a particular {@link Scheduler}.</dd> + * <dt><b>Error handling:</b></dt> + * <dd>If any of the source {@code Publisher}s signal a {@link Throwable} via {@code onError}, the resulting + * {@code Flowable} terminates with that {@code Throwable} and all other source {@code Publisher}s are canceled. + * If more than one {@code Publisher} signals an error, the resulting {@code Flowable} may terminate with the + * first one's error or, depending on the concurrency of the sources, may terminate with a + * {@link CompositeException} containing two or more of the various error signals. + * {@code Throwable}s that didn't make into the composite will be sent (individually) to the global error handler via + * {@link RxJavaPlugins#onError(Throwable)} method as {@link UndeliverableException} errors. Similarly, {@code Throwable}s + * signaled by source(s) after the returned {@code Flowable} has been canceled or terminated with a + * (composite) error will be sent to the same global error handler. + * Use {@link #mergeDelayError(Iterable)} to merge sources and terminate only when all source {@code Publisher}s + * have completed or failed with an error. + * </dd> + * </dl> + * + * @param <T> the common element base type + * @param sources + * the {@code Iterable} of {@code Publisher}s + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code sources} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/merge.html">ReactiveX operators documentation: Merge</a> + * @see #mergeDelayError(Iterable) + */ + @SuppressWarnings({ "unchecked", "rawtypes" }) + @CheckReturnValue + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public static <@NonNull T> Flowable<T> merge(@NonNull Iterable<@NonNull ? extends Publisher<? extends T>> sources) { + return fromIterable(sources).flatMap((Function)Functions.identity()); + } + + /** + * Flattens an {@link Iterable} of {@link Publisher}s into one {@code Publisher}, without any transformation, while limiting the + * number of concurrent subscriptions to these {@code Publisher}s. + * <p> + * <img width="640" height="380" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/merge.v3.png" alt=""> + * <p> + * You can combine the items emitted by multiple {@code Publisher}s so that they appear as a single {@code Publisher}, by + * using the {@code merge} method. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream. The source {@code Publisher}s are expected to honor + * backpressure; if violated, the operator <em>may</em> signal {@link MissingBackpressureException}.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code merge} does not operate by default on a particular {@link Scheduler}.</dd> + * <dt><b>Error handling:</b></dt> + * <dd>If any of the source {@code Publisher}s signal a {@link Throwable} via {@code onError}, the resulting + * {@code Flowable} terminates with that {@code Throwable} and all other source {@code Publisher}s are canceled. + * If more than one {@code Publisher} signals an error, the resulting {@code Flowable} may terminate with the + * first one's error or, depending on the concurrency of the sources, may terminate with a + * {@link CompositeException} containing two or more of the various error signals. + * {@code Throwable}s that didn't make into the composite will be sent (individually) to the global error handler via + * {@link RxJavaPlugins#onError(Throwable)} method as {@link UndeliverableException} errors. Similarly, {@code Throwable}s + * signaled by source(s) after the returned {@code Flowable} has been canceled or terminated with a + * (composite) error will be sent to the same global error handler. + * Use {@link #mergeDelayError(Iterable, int)} to merge sources and terminate only when all source {@code Publisher}s + * have completed or failed with an error. + * </dd> + * </dl> + * + * @param <T> the common element base type + * @param sources + * the {@code Iterable} of {@code Publisher}s + * @param maxConcurrency + * the maximum number of {@code Publisher}s that may be subscribed to concurrently + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code sources} is {@code null} + * @throws IllegalArgumentException + * if {@code maxConcurrency} is less than or equal to 0 + * @see <a href="http://reactivex.io/documentation/operators/merge.html">ReactiveX operators documentation: Merge</a> + * @see #mergeDelayError(Iterable, int) + */ + @SuppressWarnings({ "unchecked", "rawtypes" }) + @CheckReturnValue + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public static <@NonNull T> Flowable<T> merge(@NonNull Iterable<@NonNull ? extends Publisher<? extends T>> sources, int maxConcurrency) { + return fromIterable(sources).flatMap((Function)Functions.identity(), maxConcurrency); + } + + /** + * Flattens a {@link Publisher} that emits {@code Publisher}s into a single {@code Publisher} that emits the items emitted by + * thos {@code Publisher}s , without any transformation. + * <p> + * <img width="640" height="370" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/merge.oo.v3.png" alt=""> + * <p> + * You can combine the items emitted by multiple {@code Publisher}s so that they appear as a single {@code Publisher}, by + * using the {@code merge} method. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream. The outer {@code Publisher} is consumed + * in unbounded mode (i.e., no backpressure is applied to it). The inner {@code Publisher}s are expected to honor + * backpressure; if violated, the operator <em>may</em> signal {@link MissingBackpressureException}.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code merge} does not operate by default on a particular {@link Scheduler}.</dd> + * <dt><b>Error handling:</b></dt> + * <dd>If any of the source {@code Publisher}s signal a {@link Throwable} via {@code onError}, the resulting + * {@code Flowable} terminates with that {@code Throwable} and all other source {@code Publisher}s are canceled. + * If more than one {@code Publisher} signals an error, the resulting {@code Flowable} may terminate with the + * first one's error or, depending on the concurrency of the sources, may terminate with a + * {@link CompositeException} containing two or more of the various error signals. + * {@code Throwable}s that didn't make into the composite will be sent (individually) to the global error handler via + * {@link RxJavaPlugins#onError(Throwable)} method as {@link UndeliverableException} errors. Similarly, {@code Throwable}s + * signaled by source(s) after the returned {@code Flowable} has been canceled or terminated with a + * (composite) error will be sent to the same global error handler. + * Use {@link #mergeDelayError(Publisher)} to merge sources and terminate only when all source {@code Publisher}s + * have completed or failed with an error. + * </dd> + * </dl> + * + * @param <T> the common element base type + * @param sources + * a {@code Publisher} that emits {@code Publisher}s + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code sources} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/merge.html">ReactiveX operators documentation: Merge</a> + * @see #mergeDelayError(Publisher) + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public static <@NonNull T> Flowable<T> merge(@NonNull Publisher<@NonNull ? extends Publisher<? extends T>> sources) { + return merge(sources, bufferSize()); + } + + /** + * Flattens a {@link Publisher} that emits {@code Publisher}s into a single {@code Publisher} that emits the items emitted by + * those {@code Publisher}s, without any transformation, while limiting the maximum number of concurrent + * subscriptions to these {@code Publisher}s. + * <p> + * <img width="640" height="370" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/merge.oo.v3.png" alt=""> + * <p> + * You can combine the items emitted by multiple {@code Publisher}s so that they appear as a single {@code Publisher}, by + * using the {@code merge} method. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream. Both the outer and inner {@code Publisher}s are expected to honor + * backpressure; if violated, the operator <em>may</em> signal {@link MissingBackpressureException}.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code merge} does not operate by default on a particular {@link Scheduler}.</dd> + * <dt><b>Error handling:</b></dt> + * <dd>If any of the source {@code Publisher}s signal a {@link Throwable} via {@code onError}, the resulting + * {@code Flowable} terminates with that {@code Throwable} and all other source {@code Publisher}s are canceled. + * If more than one {@code Publisher} signals an error, the resulting {@code Flowable} may terminate with the + * first one's error or, depending on the concurrency of the sources, may terminate with a + * {@link CompositeException} containing two or more of the various error signals. + * {@code Throwable}s that didn't make into the composite will be sent (individually) to the global error handler via + * {@link RxJavaPlugins#onError(Throwable)} method as {@link UndeliverableException} errors. Similarly, {@code Throwable}s + * signaled by source(s) after the returned {@code Flowable} has been canceled or terminated with a + * (composite) error will be sent to the same global error handler. + * Use {@link #mergeDelayError(Publisher, int)} to merge sources and terminate only when all source {@code Publisher}s + * have completed or failed with an error. + * </dd> + * </dl> + * + * @param <T> the common element base type + * @param sources + * a {@code Publisher} that emits {@code Publisher}s + * @param maxConcurrency + * the maximum number of {@code Publisher}s that may be subscribed to concurrently + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code sources} is {@code null} + * @throws IllegalArgumentException + * if {@code maxConcurrency} is less than or equal to 0 + * @see <a href="http://reactivex.io/documentation/operators/merge.html">ReactiveX operators documentation: Merge</a> + * @see #mergeDelayError(Publisher, int) + * @since 1.1.0 + */ + @SuppressWarnings({ "unchecked", "rawtypes" }) + @CheckReturnValue + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public static <@NonNull T> Flowable<T> merge(@NonNull Publisher<@NonNull ? extends Publisher<? extends T>> sources, int maxConcurrency) { + return fromPublisher(sources).flatMap((Function)Functions.identity(), maxConcurrency); + } + + /** + * Flattens an array of {@link Publisher}s into one {@code Publisher}, without any transformation. + * <p> + * <img width="640" height="370" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/merge.io.v3.png" alt=""> + * <p> + * You can combine items emitted by multiple {@code Publisher}s so that they appear as a single {@code Publisher}, by + * using the {@code merge} method. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream. The source {@code Publisher}s are expected to honor + * backpressure; if violated, the operator <em>may</em> signal {@link MissingBackpressureException}.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code mergeArray} does not operate by default on a particular {@link Scheduler}.</dd> + * <dt><b>Error handling:</b></dt> + * <dd>If any of the source {@code Publisher}s signal a {@link Throwable} via {@code onError}, the resulting + * {@code Flowable} terminates with that {@code Throwable} and all other source {@code Publisher}s are canceled. + * If more than one {@code Publisher} signals an error, the resulting {@code Flowable} may terminate with the + * first one's error or, depending on the concurrency of the sources, may terminate with a + * {@link CompositeException} containing two or more of the various error signals. + * {@code Throwable}s that didn't make into the composite will be sent (individually) to the global error handler via + * {@link RxJavaPlugins#onError(Throwable)} method as {@link UndeliverableException} errors. Similarly, {@code Throwable}s + * signaled by source(s) after the returned {@code Flowable} has been canceled or terminated with a + * (composite) error will be sent to the same global error handler. + * Use {@link #mergeArrayDelayError(Publisher...)} to merge sources and terminate only when all source {@code Publisher}s + * have completed or failed with an error. + * </dd> + * </dl> + * + * @param <T> the common element base type + * @param sources + * the array of {@code Publisher}s + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code sources} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/merge.html">ReactiveX operators documentation: Merge</a> + * @see #mergeArrayDelayError(Publisher...) + */ + @SuppressWarnings({ "unchecked", "rawtypes" }) + @CheckReturnValue + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + @SafeVarargs + @NonNull + public static <@NonNull T> Flowable<T> mergeArray(@NonNull Publisher<? extends T>... sources) { + return fromArray(sources).flatMap((Function)Functions.identity(), sources.length); + } + + /** + * Flattens two {@link Publisher}s into a single {@code Publisher}, without any transformation. + * <p> + * <img width="640" height="380" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/merge.v3.png" alt=""> + * <p> + * You can combine items emitted by multiple {@code Publisher}s so that they appear as a single {@code Publisher}, by + * using the {@code merge} method. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream. The source {@code Publisher}s are expected to honor + * backpressure; if violated, the operator <em>may</em> signal {@link MissingBackpressureException}.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code merge} does not operate by default on a particular {@link Scheduler}.</dd> + * <dt><b>Error handling:</b></dt> + * <dd>If any of the source {@code Publisher}s signal a {@link Throwable} via {@code onError}, the resulting + * {@code Flowable} terminates with that {@code Throwable} and all other source {@code Publisher}s are canceled. + * If more than one {@code Publisher} signals an error, the resulting {@code Flowable} may terminate with the + * first one's error or, depending on the concurrency of the sources, may terminate with a + * {@link CompositeException} containing two or more of the various error signals. + * {@code Throwable}s that didn't make into the composite will be sent (individually) to the global error handler via + * {@link RxJavaPlugins#onError(Throwable)} method as {@link UndeliverableException} errors. Similarly, {@code Throwable}s + * signaled by source(s) after the returned {@code Flowable} has been canceled or terminated with a + * (composite) error will be sent to the same global error handler. + * Use {@link #mergeDelayError(Publisher, Publisher)} to merge sources and terminate only when all source {@code Publisher}s + * have completed or failed with an error. + * </dd> + * </dl> + * + * @param <T> the common element base type + * @param source1 + * a {@code Publisher} to be merged + * @param source2 + * a {@code Publisher} to be merged + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code source1} or {@code source2} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/merge.html">ReactiveX operators documentation: Merge</a> + * @see #mergeDelayError(Publisher, Publisher) + */ + @SuppressWarnings({ "unchecked", "rawtypes" }) + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T> Flowable<T> merge(@NonNull Publisher<? extends T> source1, @NonNull Publisher<? extends T> source2) { + Objects.requireNonNull(source1, "source1 is null"); + Objects.requireNonNull(source2, "source2 is null"); + return fromArray(source1, source2).flatMap((Function)Functions.identity(), false, 2); + } + + /** + * Flattens three {@link Publisher}s into a single {@code Publisher}, without any transformation. + * <p> + * <img width="640" height="380" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/merge.v3.png" alt=""> + * <p> + * You can combine items emitted by multiple {@code Publisher}s so that they appear as a single {@code Publisher}, by + * using the {@code merge} method. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream. The source {@code Publisher}s are expected to honor + * backpressure; if violated, the operator <em>may</em> signal {@link MissingBackpressureException}.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code merge} does not operate by default on a particular {@link Scheduler}.</dd> + * <dt><b>Error handling:</b></dt> + * <dd>If any of the source {@code Publisher}s signal a {@link Throwable} via {@code onError}, the resulting + * {@code Flowable} terminates with that {@code Throwable} and all other source {@code Publisher}s are canceled. + * If more than one {@code Publisher} signals an error, the resulting {@code Flowable} may terminate with the + * first one's error or, depending on the concurrency of the sources, may terminate with a + * {@link CompositeException} containing two or more of the various error signals. + * {@code Throwable}s that didn't make into the composite will be sent (individually) to the global error handler via + * {@link RxJavaPlugins#onError(Throwable)} method as {@link UndeliverableException} errors. Similarly, {@code Throwable}s + * signaled by source(s) after the returned {@code Flowable} has been canceled or terminated with a + * (composite) error will be sent to the same global error handler. + * Use {@link #mergeDelayError(Publisher, Publisher, Publisher)} to merge sources and terminate only when all source {@code Publisher}s + * have completed or failed with an error. + * </dd> + * </dl> + * + * @param <T> the common element base type + * @param source1 + * a {@code Publisher} to be merged + * @param source2 + * a {@code Publisher} to be merged + * @param source3 + * a {@code Publisher} to be merged + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code source1}, {@code source2} or {@code source3} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/merge.html">ReactiveX operators documentation: Merge</a> + * @see #mergeDelayError(Publisher, Publisher, Publisher) + */ + @SuppressWarnings({ "unchecked", "rawtypes" }) + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T> Flowable<T> merge(@NonNull Publisher<? extends T> source1, @NonNull Publisher<? extends T> source2, @NonNull Publisher<? extends T> source3) { + Objects.requireNonNull(source1, "source1 is null"); + Objects.requireNonNull(source2, "source2 is null"); + Objects.requireNonNull(source3, "source3 is null"); + return fromArray(source1, source2, source3).flatMap((Function)Functions.identity(), false, 3); + } + + /** + * Flattens four {@link Publisher}s into a single {@code Publisher}, without any transformation. + * <p> + * <img width="640" height="380" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/merge.v3.png" alt=""> + * <p> + * You can combine items emitted by multiple {@code Publisher}s so that they appear as a single {@code Publisher}, by + * using the {@code merge} method. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream. The source {@code Publisher}s are expected to honor + * backpressure; if violated, the operator <em>may</em> signal {@link MissingBackpressureException}.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code merge} does not operate by default on a particular {@link Scheduler}.</dd> + * <dt><b>Error handling:</b></dt> + * <dd>If any of the source {@code Publisher}s signal a {@link Throwable} via {@code onError}, the resulting + * {@code Flowable} terminates with that {@code Throwable} and all other source {@code Publisher}s are canceled. + * If more than one {@code Publisher} signals an error, the resulting {@code Flowable} may terminate with the + * first one's error or, depending on the concurrency of the sources, may terminate with a + * {@link CompositeException} containing two or more of the various error signals. + * {@code Throwable}s that didn't make into the composite will be sent (individually) to the global error handler via + * {@link RxJavaPlugins#onError(Throwable)} method as {@link UndeliverableException} errors. Similarly, {@code Throwable}s + * signaled by source(s) after the returned {@code Flowable} has been canceled or terminated with a + * (composite) error will be sent to the same global error handler. + * Use {@link #mergeDelayError(Publisher, Publisher, Publisher, Publisher)} to merge sources and terminate only when all source {@code Publisher}s + * have completed or failed with an error. + * </dd> + * </dl> + * + * @param <T> the common element base type + * @param source1 + * a {@code Publisher} to be merged + * @param source2 + * a {@code Publisher} to be merged + * @param source3 + * a {@code Publisher} to be merged + * @param source4 + * a {@code Publisher} to be merged + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code source1}, {@code source2}, {@code source3} or {@code source4} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/merge.html">ReactiveX operators documentation: Merge</a> + * @see #mergeDelayError(Publisher, Publisher, Publisher, Publisher) + */ + @SuppressWarnings({ "unchecked", "rawtypes" }) + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T> Flowable<T> merge( + @NonNull Publisher<? extends T> source1, @NonNull Publisher<? extends T> source2, + @NonNull Publisher<? extends T> source3, @NonNull Publisher<? extends T> source4) { + Objects.requireNonNull(source1, "source1 is null"); + Objects.requireNonNull(source2, "source2 is null"); + Objects.requireNonNull(source3, "source3 is null"); + Objects.requireNonNull(source4, "source4 is null"); + return fromArray(source1, source2, source3, source4).flatMap((Function)Functions.identity(), false, 4); + } + + /** + * Flattens an {@link Iterable} of {@link Publisher}s into one {@code Publisher}, in a way that allows a {@link Subscriber} to receive all + * successfully emitted items from each of the source {@code Publisher}s without being interrupted by an error + * notification from one of them. + * <p> + * This behaves like {@link #merge(Publisher)} except that if any of the merged {@code Publisher}s notify of an + * error via {@link Subscriber#onError onError}, {@code mergeDelayError} will refrain from propagating that + * error notification until all of the merged {@code Publisher}s have finished emitting items. + * <p> + * <img width="640" height="380" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/mergeDelayError.v3.png" alt=""> + * <p> + * Even if multiple merged {@code Publisher}s send {@code onError} notifications, {@code mergeDelayError} will only + * invoke the {@code onError} method of its {@code Subscriber}s once. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream. All inner {@code Publisher}s are expected to honor + * backpressure; if violated, the operator <em>may</em> signal {@link MissingBackpressureException}.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code mergeDelayError} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T> the common element base type + * @param sources + * the {@code Iterable} of {@code Publisher}s + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code sources} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/merge.html">ReactiveX operators documentation: Merge</a> + */ + @SuppressWarnings({ "unchecked", "rawtypes" }) + @CheckReturnValue + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public static <@NonNull T> Flowable<T> mergeDelayError(@NonNull Iterable<@NonNull ? extends Publisher<? extends T>> sources) { + return fromIterable(sources).flatMap((Function)Functions.identity(), true); + } + + /** + * Flattens an {@link Iterable} of {@link Publisher}s into one {@code Publisher}, in a way that allows a {@link Subscriber} to receive all + * successfully emitted items from each of the source {@code Publisher}s without being interrupted by an error + * notification from one of them, while limiting the number of concurrent subscriptions to these {@code Publisher}s. + * <p> + * This behaves like {@link #merge(Publisher)} except that if any of the merged {@code Publisher}s notify of an + * error via {@link Subscriber#onError onError}, {@code mergeDelayError} will refrain from propagating that + * error notification until all of the merged {@code Publisher}s have finished emitting items. + * <p> + * <img width="640" height="380" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/mergeDelayError.v3.png" alt=""> + * <p> + * Even if multiple merged {@code Publisher}s send {@code onError} notifications, {@code mergeDelayError} will only + * invoke the {@code onError} method of its {@code Subscriber}s once. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream. All inner {@code Publisher}s are expected to honor + * backpressure; if violated, the operator <em>may</em> signal {@link MissingBackpressureException}.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code mergeDelayError} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T> the common element base type + * @param sources + * the {@code Iterable} of {@code Publisher}s + * @param maxConcurrency + * the maximum number of {@code Publisher}s that may be subscribed to concurrently + * @param bufferSize + * the number of items to prefetch from each inner {@code Publisher} + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code sources} is {@code null} + * @throws IllegalArgumentException if {@code maxConcurrency} or {@code bufferSize} is non-positive + * @see <a href="http://reactivex.io/documentation/operators/merge.html">ReactiveX operators documentation: Merge</a> + */ + @SuppressWarnings({ "unchecked", "rawtypes" }) + @CheckReturnValue + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public static <@NonNull T> Flowable<T> mergeDelayError(@NonNull Iterable<@NonNull ? extends Publisher<? extends T>> sources, int maxConcurrency, int bufferSize) { + return fromIterable(sources).flatMap((Function)Functions.identity(), true, maxConcurrency, bufferSize); + } + + /** + * Flattens an array of {@link Publisher}s into one {@code Publisher}, in a way that allows a {@link Subscriber} to receive all + * successfully emitted items from each of the source {@code Publisher}s without being interrupted by an error + * notification from one of them, while limiting the number of concurrent subscriptions to these {@code Publisher}s. + * <p> + * This behaves like {@link #merge(Publisher)} except that if any of the merged {@code Publisher}s notify of an + * error via {@link Subscriber#onError onError}, {@code mergeDelayError} will refrain from propagating that + * error notification until all of the merged {@code Publisher}s have finished emitting items. + * <p> + * <img width="640" height="380" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/mergeDelayError.v3.png" alt=""> + * <p> + * Even if multiple merged {@code Publisher}s send {@code onError} notifications, {@code mergeDelayError} will only + * invoke the {@code onError} method of its {@code Subscriber}s once. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream. All source {@code Publisher}s are expected to honor + * backpressure; if violated, the operator <em>may</em> signal {@link MissingBackpressureException}.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code mergeArrayDelayError} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T> the common element base type + * @param sources + * the array of {@code Publisher}s + * @param maxConcurrency + * the maximum number of {@code Publisher}s that may be subscribed to concurrently + * @param bufferSize + * the number of items to prefetch from each inner {@code Publisher} + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code sources} is {@code null} + * @throws IllegalArgumentException if {@code maxConcurrency} or {@code bufferSize} is non-positive + * @see <a href="http://reactivex.io/documentation/operators/merge.html">ReactiveX operators documentation: Merge</a> + */ + @SuppressWarnings({ "unchecked", "rawtypes" }) + @CheckReturnValue + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + @SafeVarargs + @NonNull + public static <@NonNull T> Flowable<T> mergeArrayDelayError(int maxConcurrency, int bufferSize, @NonNull Publisher<? extends T>... sources) { + return fromArray(sources).flatMap((Function)Functions.identity(), true, maxConcurrency, bufferSize); + } + + /** + * Flattens an {@link Iterable} of {@link Publisher}s into one {@code Publisher}, in a way that allows a {@link Subscriber} to receive all + * successfully emitted items from each of the source {@code Publisher}s without being interrupted by an error + * notification from one of them, while limiting the number of concurrent subscriptions to these {@code Publisher}s. + * <p> + * This behaves like {@link #merge(Publisher)} except that if any of the merged {@code Publisher}s notify of an + * error via {@link Subscriber#onError onError}, {@code mergeDelayError} will refrain from propagating that + * error notification until all of the merged {@code Publisher}s have finished emitting items. + * <p> + * <img width="640" height="380" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/mergeDelayError.v3.png" alt=""> + * <p> + * Even if multiple merged {@code Publisher}s send {@code onError} notifications, {@code mergeDelayError} will only + * invoke the {@code onError} method of its {@code Subscriber}s once. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream. All inner {@code Publisher}s are expected to honor + * backpressure; if violated, the operator <em>may</em> signal {@link MissingBackpressureException}.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code mergeDelayError} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T> the common element base type + * @param sources + * the {@code Iterable} of {@code Publisher}s + * @param maxConcurrency + * the maximum number of {@code Publisher}s that may be subscribed to concurrently + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code sources} is {@code null} + * @throws IllegalArgumentException if {@code maxConcurrency} is non-positive + * @see <a href="http://reactivex.io/documentation/operators/merge.html">ReactiveX operators documentation: Merge</a> + */ + @SuppressWarnings({ "unchecked", "rawtypes" }) + @CheckReturnValue + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public static <@NonNull T> Flowable<T> mergeDelayError(@NonNull Iterable<@NonNull ? extends Publisher<? extends T>> sources, int maxConcurrency) { + return fromIterable(sources).flatMap((Function)Functions.identity(), true, maxConcurrency); + } + + /** + * Flattens a {@link Publisher} that emits {@code Publisher}s into one {@code Publisher}, in a way that allows a {@link Subscriber} to + * receive all successfully emitted items from all of the source {@code Publisher}s without being interrupted by + * an error notification from one of them. + * <p> + * This behaves like {@link #merge(Publisher)} except that if any of the merged {@code Publisher}s notify of an + * error via {@link Subscriber#onError onError}, {@code mergeDelayError} will refrain from propagating that + * error notification until all of the merged {@code Publisher}s have finished emitting items. + * <p> + * <img width="640" height="380" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/mergeDelayError.v3.png" alt=""> + * <p> + * Even if multiple merged {@code Publisher}s send {@code onError} notifications, {@code mergeDelayError} will only + * invoke the {@code onError} method of its {@code Subscriber}s once. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream. The outer {@code Publisher} is consumed + * in unbounded mode (i.e., no backpressure is applied to it). The inner {@code Publisher}s are expected to honor + * backpressure; if violated, the operator <em>may</em> signal {@link MissingBackpressureException}.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code mergeDelayError} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T> the common element base type + * @param sources + * a {@code Publisher} that emits {@code Publisher}s + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code sources} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/merge.html">ReactiveX operators documentation: Merge</a> + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public static <@NonNull T> Flowable<T> mergeDelayError(@NonNull Publisher<@NonNull ? extends Publisher<? extends T>> sources) { + return mergeDelayError(sources, bufferSize()); + } + + /** + * Flattens a {@link Publisher} that emits {@code Publisher}s into one {@code Publisher}, in a way that allows a {@link Subscriber} to + * receive all successfully emitted items from all of the source {@code Publisher}s without being interrupted by + * an error notification from one of them, while limiting the + * number of concurrent subscriptions to these {@code Publisher}s. + * <p> + * This behaves like {@link #merge(Publisher)} except that if any of the merged {@code Publisher}s notify of an + * error via {@link Subscriber#onError onError}, {@code mergeDelayError} will refrain from propagating that + * error notification until all of the merged {@code Publisher}s have finished emitting items. + * <p> + * <img width="640" height="380" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/mergeDelayError.v3.png" alt=""> + * <p> + * Even if multiple merged {@code Publisher}s send {@code onError} notifications, {@code mergeDelayError} will only + * invoke the {@code onError} method of its {@code Subscriber}s once. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream. Both the outer and inner {@code Publisher}s are expected to honor + * backpressure; if violated, the operator <em>may</em> signal {@link MissingBackpressureException}.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code mergeDelayError} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T> the common element base type + * @param sources + * a {@code Publisher} that emits {@code Publisher}s + * @param maxConcurrency + * the maximum number of {@code Publisher}s that may be subscribed to concurrently + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code sources} is {@code null} + * @throws IllegalArgumentException if {@code maxConcurrency} is non-positive + * @see <a href="http://reactivex.io/documentation/operators/merge.html">ReactiveX operators documentation: Merge</a> + * @since 2.0 + */ + @SuppressWarnings({ "unchecked", "rawtypes" }) + @CheckReturnValue + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public static <@NonNull T> Flowable<T> mergeDelayError(@NonNull Publisher<@NonNull ? extends Publisher<? extends T>> sources, int maxConcurrency) { + return fromPublisher(sources).flatMap((Function)Functions.identity(), true, maxConcurrency); + } + + /** + * Flattens an array of {@link Publisher}s into one {@code Flowable}, in a way that allows a {@link Subscriber} to receive all + * successfully emitted items from each of the source {@code Publisher}s without being interrupted by an error + * notification from one of them. + * <p> + * This behaves like {@link #merge(Publisher)} except that if any of the merged {@code Publisher}s notify of an + * error via {@link Subscriber#onError onError}, {@code mergeDelayError} will refrain from propagating that + * error notification until all of the merged {@code Publisher}s have finished emitting items. + * <p> + * <img width="640" height="380" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/mergeDelayError.v3.png" alt=""> + * <p> + * Even if multiple merged {@code Publisher}s send {@code onError} notifications, {@code mergeDelayError} will only + * invoke the {@code onError} method of its {@code Subscriber}s once. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream. Both the outer and inner {@code Publisher}s are expected to honor + * backpressure; if violated, the operator <em>may</em> signal {@link MissingBackpressureException}.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code mergeArrayDelayError} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T> the common element base type + * @param sources + * the array of {@code Publisher}s + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code sources} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/merge.html">ReactiveX operators documentation: Merge</a> + */ + @SuppressWarnings({ "unchecked", "rawtypes" }) + @CheckReturnValue + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + @SafeVarargs + @NonNull + public static <@NonNull T> Flowable<T> mergeArrayDelayError(@NonNull Publisher<? extends T>... sources) { + return fromArray(sources).flatMap((Function)Functions.identity(), true, sources.length); + } + + /** + * Flattens two {@link Publisher}s into one {@code Publisher}, in a way that allows a {@link Subscriber} to receive all + * successfully emitted items from each of the source {@code Publisher}s without being interrupted by an error + * notification from one of them. + * <p> + * This behaves like {@link #merge(Publisher, Publisher)} except that if any of the merged {@code Publisher}s + * notify of an error via {@link Subscriber#onError onError}, {@code mergeDelayError} will refrain from + * propagating that error notification until all of the merged {@code Publisher}s have finished emitting items. + * <p> + * <img width="640" height="380" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/mergeDelayError.v3.png" alt=""> + * <p> + * Even if both merged {@code Publisher}s send {@code onError} notifications, {@code mergeDelayError} will only + * invoke the {@code onError} method of its {@code Subscriber}s once. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream. The source {@code Publisher}s are expected to honor + * backpressure; if violated, the operator <em>may</em> signal {@link MissingBackpressureException}.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code mergeDelayError} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T> the common element base type + * @param source1 + * a {@code Publisher} to be merged + * @param source2 + * a {@code Publisher} to be merged + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code source1} or {@code source2} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/merge.html">ReactiveX operators documentation: Merge</a> + */ + @SuppressWarnings({ "unchecked", "rawtypes" }) + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T> Flowable<T> mergeDelayError(@NonNull Publisher<? extends T> source1, @NonNull Publisher<? extends T> source2) { + Objects.requireNonNull(source1, "source1 is null"); + Objects.requireNonNull(source2, "source2 is null"); + return fromArray(source1, source2).flatMap((Function)Functions.identity(), true, 2); + } + + /** + * Flattens three {@link Publisher}s into one {@code Publisher}, in a way that allows a {@link Subscriber} to receive all + * successfully emitted items from all of the source {@code Publisher}s without being interrupted by an error + * notification from one of them. + * <p> + * This behaves like {@link #merge(Publisher, Publisher, Publisher)} except that if any of the merged + * {@code Publisher}s notify of an error via {@link Subscriber#onError onError}, {@code mergeDelayError} will refrain + * from propagating that error notification until all of the merged {@code Publisher}s have finished emitting + * items. + * <p> + * <img width="640" height="380" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/mergeDelayError.v3.png" alt=""> + * <p> + * Even if multiple merged {@code Publisher}s send {@code onError} notifications, {@code mergeDelayError} will only + * invoke the {@code onError} method of its {@code Subscriber}s once. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream. The source {@code Publisher}s are expected to honor + * backpressure; if violated, the operator <em>may</em> signal {@link MissingBackpressureException}.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code mergeDelayError} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T> the common element base type + * @param source1 + * a {@code Publisher} to be merged + * @param source2 + * a {@code Publisher} to be merged + * @param source3 + * a {@code Publisher} to be merged + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code source1}, {@code source2} or {@code source3} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/merge.html">ReactiveX operators documentation: Merge</a> + */ + @SuppressWarnings({ "unchecked", "rawtypes" }) + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T> Flowable<T> mergeDelayError(@NonNull Publisher<? extends T> source1, @NonNull Publisher<? extends T> source2, @NonNull Publisher<? extends T> source3) { + Objects.requireNonNull(source1, "source1 is null"); + Objects.requireNonNull(source2, "source2 is null"); + Objects.requireNonNull(source3, "source3 is null"); + return fromArray(source1, source2, source3).flatMap((Function)Functions.identity(), true, 3); + } + + /** + * Flattens four {@link Publisher}s into one {@code Publisher}, in a way that allows a {@link Subscriber} to receive all + * successfully emitted items from all of the source {@code Publisher}s without being interrupted by an error + * notification from one of them. + * <p> + * This behaves like {@link #merge(Publisher, Publisher, Publisher, Publisher)} except that if any of + * the merged {@code Publisher}s notify of an error via {@link Subscriber#onError onError}, {@code mergeDelayError} + * will refrain from propagating that error notification until all of the merged {@code Publisher}s have finished + * emitting items. + * <p> + * <img width="640" height="380" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/mergeDelayError.v3.png" alt=""> + * <p> + * Even if multiple merged {@code Publisher}s send {@code onError} notifications, {@code mergeDelayError} will only + * invoke the {@code onError} method of its {@code Subscriber}s once. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream. The source {@code Publisher}s are expected to honor + * backpressure; if violated, the operator <em>may</em> signal {@link MissingBackpressureException}.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code mergeDelayError} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T> the common element base type + * @param source1 + * a {@code Publisher} to be merged + * @param source2 + * a {@code Publisher} to be merged + * @param source3 + * a {@code Publisher} to be merged + * @param source4 + * a {@code Publisher} to be merged + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code source1}, {@code source2}, {@code source3} or {@code source4} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/merge.html">ReactiveX operators documentation: Merge</a> + */ + @SuppressWarnings({ "unchecked", "rawtypes" }) + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T> Flowable<T> mergeDelayError( + @NonNull Publisher<? extends T> source1, @NonNull Publisher<? extends T> source2, + @NonNull Publisher<? extends T> source3, @NonNull Publisher<? extends T> source4) { + Objects.requireNonNull(source1, "source1 is null"); + Objects.requireNonNull(source2, "source2 is null"); + Objects.requireNonNull(source3, "source3 is null"); + Objects.requireNonNull(source4, "source4 is null"); + return fromArray(source1, source2, source3, source4).flatMap((Function)Functions.identity(), true, 4); + } + + /** + * Returns a {@code Flowable} that never sends any items or notifications to a {@link Subscriber}. + * <p> + * <img width="640" height="185" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/never.v3.png" alt=""> + * <p> + * This {@link Publisher} is useful primarily for testing purposes. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>This source doesn't produce any elements and effectively ignores downstream backpressure.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code never} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T> + * the type of items (not) emitted by the {@code Publisher} + * @return the shared {@code Flowable} instance + * @see <a href="http://reactivex.io/documentation/operators/empty-never-throw.html">ReactiveX operators documentation: Never</a> + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.PASS_THROUGH) + @SchedulerSupport(SchedulerSupport.NONE) + @SuppressWarnings("unchecked") + @NonNull + public static <@NonNull T> Flowable<T> never() { + return RxJavaPlugins.onAssembly((Flowable<T>) FlowableNever.INSTANCE); + } + + /** + * Returns a {@code Flowable} that emits a sequence of {@link Integer}s within a specified range. + * <p> + * <img width="640" height="195" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/range.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream and signals values on-demand (i.e., when requested).</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code range} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param start + * the value of the first {@code Integer} in the sequence + * @param count + * the number of sequential {@code Integer}s to generate + * @return the new {@code Flowable} instance + * @throws IllegalArgumentException + * if {@code count} is less than zero, or if {@code start} + {@code count} − 1 exceeds + * {@link Integer#MAX_VALUE} + * @see <a href="http://reactivex.io/documentation/operators/range.html">ReactiveX operators documentation: Range</a> + * @see #rangeLong(long, long) + * @see #intervalRange(long, long, long, long, TimeUnit) + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public static Flowable<Integer> range(int start, int count) { + if (count < 0) { + throw new IllegalArgumentException("count >= 0 required but it was " + count); + } else + if (count == 0) { + return empty(); + } else + if (count == 1) { + return just(start); + } else + if ((long)start + (count - 1) > Integer.MAX_VALUE) { + throw new IllegalArgumentException("Integer overflow"); + } + return RxJavaPlugins.onAssembly(new FlowableRange(start, count)); + } + + /** + * Returns a {@code Flowable} that emits a sequence of {@link Long}s within a specified range. + * <p> + * <img width="640" height="195" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/range.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream and signals values on-demand (i.e., when requested).</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code rangeLong} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param start + * the value of the first {@code Long} in the sequence + * @param count + * the number of sequential {@code Long}s to generate + * @return the new {@code Flowable} instance + * @throws IllegalArgumentException + * if {@code count} is less than zero, or if {@code start} + {@code count} − 1 exceeds + * {@link Long#MAX_VALUE} + * @see <a href="http://reactivex.io/documentation/operators/range.html">ReactiveX operators documentation: Range</a> + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public static Flowable<Long> rangeLong(long start, long count) { + if (count < 0) { + throw new IllegalArgumentException("count >= 0 required but it was " + count); + } + + if (count == 0) { + return empty(); + } + + if (count == 1) { + return just(start); + } + + long end = start + (count - 1); + if (start > 0 && end < 0) { + throw new IllegalArgumentException("Overflow! start + count is bigger than Long.MAX_VALUE"); + } + + return RxJavaPlugins.onAssembly(new FlowableRangeLong(start, count)); + } + + /** + * Returns a {@link Single} that emits a {@link Boolean} value that indicates whether two {@link Publisher} sequences are the + * same by comparing the items emitted by each {@code Publisher} pairwise. + * <p> + * <img width="640" height="385" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/sequenceEqual.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>This operator honors downstream backpressure and expects both of its sources + * to honor backpressure as well. If violated, the operator will emit a {@link MissingBackpressureException}.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code sequenceEqual} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param source1 + * the first {@code Publisher} to compare + * @param source2 + * the second {@code Publisher} to compare + * @param <T> + * the type of items emitted by each {@code Publisher} + * @return the new {@code Single} instance + * @throws NullPointerException if {@code source1} or {@code source2} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/sequenceequal.html">ReactiveX operators documentation: SequenceEqual</a> + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public static <@NonNull T> Single<Boolean> sequenceEqual(@NonNull Publisher<? extends T> source1, @NonNull Publisher<? extends T> source2) { + return sequenceEqual(source1, source2, ObjectHelper.equalsPredicate(), bufferSize()); + } + + /** + * Returns a {@link Single} that emits a {@link Boolean} value that indicates whether two {@link Publisher} sequences are the + * same by comparing the items emitted by each {@code Publisher} pairwise based on the results of a specified + * equality function. + * <p> + * <img width="640" height="385" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/sequenceEqual.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream. The source {@code Publisher}s are expected to honor + * backpressure; if violated, the operator signals a {@link MissingBackpressureException}.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code sequenceEqual} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param source1 + * the first {@code Publisher} to compare + * @param source2 + * the second {@code Publisher} to compare + * @param isEqual + * a function used to compare items emitted by each {@code Publisher} + * @param <T> + * the type of items emitted by each {@code Publisher} + * @return the new {@code Single} instance + * @throws NullPointerException if {@code source1}, {@code source2} or {@code isEqual} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/sequenceequal.html">ReactiveX operators documentation: SequenceEqual</a> + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public static <@NonNull T> Single<Boolean> sequenceEqual(@NonNull Publisher<? extends T> source1, @NonNull Publisher<? extends T> source2, + @NonNull BiPredicate<? super T, ? super T> isEqual) { + return sequenceEqual(source1, source2, isEqual, bufferSize()); + } + + /** + * Returns a {@link Single} that emits a {@link Boolean} value that indicates whether two {@link Publisher} sequences are the + * same by comparing the items emitted by each {@code Publisher} pairwise based on the results of a specified + * equality function. + * <p> + * <img width="640" height="385" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/sequenceEqual.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream. The source {@code Publisher}s are expected to honor + * backpressure; if violated, the operator signals a {@link MissingBackpressureException}.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code sequenceEqual} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param source1 + * the first {@code Publisher} to compare + * @param source2 + * the second {@code Publisher} to compare + * @param isEqual + * a function used to compare items emitted by each {@code Publisher} + * @param bufferSize + * the number of items to prefetch from the first and second source {@code Publisher} + * @param <T> + * the type of items emitted by each {@code Publisher} + * @return the new {@code Single} instance + * @throws NullPointerException if {@code source1}, {@code source2} or {@code isEqual} is {@code null} + * @throws IllegalArgumentException if {@code bufferSize} is non-positive + * @see <a href="http://reactivex.io/documentation/operators/sequenceequal.html">ReactiveX operators documentation: SequenceEqual</a> + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T> Single<Boolean> sequenceEqual(@NonNull Publisher<? extends T> source1, @NonNull Publisher<? extends T> source2, + @NonNull BiPredicate<? super T, ? super T> isEqual, int bufferSize) { + Objects.requireNonNull(source1, "source1 is null"); + Objects.requireNonNull(source2, "source2 is null"); + Objects.requireNonNull(isEqual, "isEqual is null"); + ObjectHelper.verifyPositive(bufferSize, "bufferSize"); + return RxJavaPlugins.onAssembly(new FlowableSequenceEqualSingle<>(source1, source2, isEqual, bufferSize)); + } + + /** + * Returns a {@link Single} that emits a {@link Boolean} value that indicates whether two {@link Publisher} sequences are the + * same by comparing the items emitted by each {@code Publisher} pairwise. + * <p> + * <img width="640" height="385" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/sequenceEqual.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>This operator honors downstream backpressure and expects both of its sources + * to honor backpressure as well. If violated, the operator will emit a {@link MissingBackpressureException}.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code sequenceEqual} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param source1 + * the first {@code Publisher} to compare + * @param source2 + * the second {@code Publisher} to compare + * @param bufferSize + * the number of items to prefetch from the first and second source {@code Publisher} + * @param <T> + * the type of items emitted by each {@code Publisher} + * @return the new {@code Single} instance + * @throws NullPointerException if {@code source1} or {@code source2} is {@code null} + * @throws IllegalArgumentException if {@code bufferSize} is non-positive + * @see <a href="http://reactivex.io/documentation/operators/sequenceequal.html">ReactiveX operators documentation: SequenceEqual</a> + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public static <@NonNull T> Single<Boolean> sequenceEqual(@NonNull Publisher<? extends T> source1, @NonNull Publisher<? extends T> source2, int bufferSize) { + return sequenceEqual(source1, source2, ObjectHelper.equalsPredicate(), bufferSize); + } + + /** + * Converts a {@link Publisher} that emits {@code Publisher}s into a {@code Publisher} that emits the items emitted by the + * most recently emitted of those {@code Publisher}s. + * <p> + * <img width="640" height="370" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/switchDo.v3.png" alt=""> + * <p> + * {@code switchOnNext} subscribes to a {@code Publisher} that emits {@code Publisher}s. Each time it observes one of + * these emitted {@code Publisher}s, the {@code Publisher} returned by {@code switchOnNext} begins emitting the items + * emitted by that {@code Publisher}. When a new {@code Publisher} is emitted, {@code switchOnNext} stops emitting items + * from the earlier-emitted {@code Publisher} and begins emitting items from the new one. + * <p> + * The resulting {@code Flowable} completes if both the outer {@code Publisher} and the last inner {@code Publisher}, if any, complete. + * If the outer {@code Publisher} signals an {@code onError}, the inner {@code Publisher} is canceled and the error delivered in-sequence. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream. The outer {@code Publisher} is consumed in an + * unbounded manner (i.e., without backpressure) and the inner {@code Publisher}s are expected to honor + * backpressure but it is not enforced; the operator won't signal a {@link MissingBackpressureException} + * but the violation <em>may</em> lead to {@link OutOfMemoryError} due to internal buffer bloat.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code switchOnNext} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T> the item type + * @param sources + * the source {@code Publisher} that emits {@code Publisher}s + * @param bufferSize + * the number of items to prefetch from the inner {@code Publisher}s + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code sources} is {@code null} + * @throws IllegalArgumentException if {@code bufferSize} is non-positive + * @see <a href="http://reactivex.io/documentation/operators/switch.html">ReactiveX operators documentation: Switch</a> + */ + @SuppressWarnings({ "unchecked", "rawtypes" }) + @CheckReturnValue + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public static <@NonNull T> Flowable<T> switchOnNext(@NonNull Publisher<? extends Publisher<? extends T>> sources, int bufferSize) { + return fromPublisher(sources).switchMap((Function)Functions.identity(), bufferSize); + } + + /** + * Converts a {@link Publisher} that emits {@code Publisher}s into a {@code Publisher} that emits the items emitted by the + * most recently emitted of those {@code Publisher}s. + * <p> + * <img width="640" height="370" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/switchDo.v3.png" alt=""> + * <p> + * {@code switchOnNext} subscribes to a {@code Publisher} that emits {@code Publisher}s. Each time it observes one of + * these emitted {@code Publisher}s, the {@code Publisher} returned by {@code switchOnNext} begins emitting the items + * emitted by that {@code Publisher}. When a new {@code Publisher} is emitted, {@code switchOnNext} stops emitting items + * from the earlier-emitted {@code Publisher} and begins emitting items from the new one. + * <p> + * The resulting {@code Flowable} completes if both the outer {@code Publisher} and the last inner {@code Publisher}, if any, complete. + * If the outer {@code Publisher} signals an {@code onError}, the inner {@code Publisher} is canceled and the error delivered in-sequence. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream. The outer {@code Publisher} is consumed in an + * unbounded manner (i.e., without backpressure) and the inner {@code Publisher}s are expected to honor + * backpressure but it is not enforced; the operator won't signal a {@link MissingBackpressureException} + * but the violation <em>may</em> lead to {@link OutOfMemoryError} due to internal buffer bloat.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code switchOnNext} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T> the item type + * @param sources + * the source {@code Publisher} that emits {@code Publisher}s + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code sources} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/switch.html">ReactiveX operators documentation: Switch</a> + */ + @SuppressWarnings({ "unchecked", "rawtypes" }) + @CheckReturnValue + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public static <@NonNull T> Flowable<T> switchOnNext(@NonNull Publisher<@NonNull ? extends Publisher<? extends T>> sources) { + return fromPublisher(sources).switchMap((Function)Functions.identity()); + } + + /** + * Converts a {@link Publisher} that emits {@code Publisher}s into a {@code Publisher} that emits the items emitted by the + * most recently emitted of those {@code Publisher}s and delays any exception until all {@code Publisher}s terminate. + * <p> + * <img width="640" height="370" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/switchDo.v3.png" alt=""> + * <p> + * {@code switchOnNext} subscribes to a {@code Publisher} that emits {@code Publisher}s. Each time it observes one of + * these emitted {@code Publisher}s, the {@code Publisher} returned by {@code switchOnNext} begins emitting the items + * emitted by that {@code Publisher}. When a new {@code Publisher} is emitted, {@code switchOnNext} stops emitting items + * from the earlier-emitted {@code Publisher} and begins emitting items from the new one. + * <p> + * The resulting {@code Flowable} completes if both the main {@code Publisher} and the last inner {@code Publisher}, if any, complete. + * If the main {@code Publisher} signals an {@code onError}, the termination of the last inner {@code Publisher} will emit that error as is + * or wrapped into a {@link CompositeException} along with the other possible errors the former inner {@code Publisher}s signaled. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream. The outer {@code Publisher} is consumed in an + * unbounded manner (i.e., without backpressure) and the inner {@code Publisher}s are expected to honor + * backpressure but it is not enforced; the operator won't signal a {@link MissingBackpressureException} + * but the violation <em>may</em> lead to {@link OutOfMemoryError} due to internal buffer bloat.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code switchOnNextDelayError} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T> the item type + * @param sources + * the source {@code Publisher} that emits {@code Publisher}s + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code sources} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/switch.html">ReactiveX operators documentation: Switch</a> + * @since 2.0 + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public static <@NonNull T> Flowable<T> switchOnNextDelayError(@NonNull Publisher<@NonNull ? extends Publisher<? extends T>> sources) { + return switchOnNextDelayError(sources, bufferSize()); + } + + /** + * Converts a {@link Publisher} that emits {@code Publisher}s into a {@code Publisher} that emits the items emitted by the + * most recently emitted of those {@code Publisher}s and delays any exception until all {@code Publisher}s terminate. + * <p> + * <img width="640" height="370" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/switchDo.v3.png" alt=""> + * <p> + * {@code switchOnNext} subscribes to a {@code Publisher} that emits {@code Publisher}s. Each time it observes one of + * these emitted {@code Publisher}s, the {@code Publisher} returned by {@code switchOnNext} begins emitting the items + * emitted by that {@code Publisher}. When a new {@code Publisher} is emitted, {@code switchOnNext} stops emitting items + * from the earlier-emitted {@code Publisher} and begins emitting items from the new one. + * <p> + * The resulting {@code Flowable} completes if both the main {@code Publisher} and the last inner {@code Publisher}, if any, complete. + * If the main {@code Publisher} signals an {@code onError}, the termination of the last inner {@code Publisher} will emit that error as is + * or wrapped into a {@link CompositeException} along with the other possible errors the former inner {@code Publisher}s signaled. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream. The outer {@code Publisher} is consumed in an + * unbounded manner (i.e., without backpressure) and the inner {@code Publisher}s are expected to honor + * backpressure but it is not enforced; the operator won't signal a {@link MissingBackpressureException} + * but the violation <em>may</em> lead to {@link OutOfMemoryError} due to internal buffer bloat.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code switchOnNextDelayError} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T> the item type + * @param sources + * the source {@code Publisher} that emits {@code Publisher}s + * @param prefetch + * the number of items to prefetch from the inner {@code Publisher}s + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code sources} is {@code null} + * @throws IllegalArgumentException if {@code prefetch} is non-positive + * @see <a href="http://reactivex.io/documentation/operators/switch.html">ReactiveX operators documentation: Switch</a> + * @since 2.0 + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public static <@NonNull T> Flowable<T> switchOnNextDelayError(@NonNull Publisher<@NonNull ? extends Publisher<? extends T>> sources, int prefetch) { + return fromPublisher(sources).switchMapDelayError(Functions.<Publisher<? extends T>>identity(), prefetch); + } + + /** + * Returns a {@code Flowable} that emits {@code 0L} after a specified delay, and then completes. + * <p> + * <img width="640" height="200" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/timer.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>This operator does not support backpressure as it uses time. If the downstream needs a slower rate + * it should slow the timer or use something like {@link #onBackpressureDrop}.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code timer} operates by default on the {@code computation} {@link Scheduler}.</dd> + * </dl> + * + * @param delay + * the initial delay before emitting a single {@code 0L} + * @param unit + * time units to use for {@code delay} + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code unit} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/timer.html">ReactiveX operators documentation: Timer</a> + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.ERROR) + @SchedulerSupport(SchedulerSupport.COMPUTATION) + @NonNull + public static Flowable<Long> timer(long delay, @NonNull TimeUnit unit) { + return timer(delay, unit, Schedulers.computation()); + } + + /** + * Returns a {@code Flowable} that emits {@code 0L} after a specified delay, on a specified {@link Scheduler}, and then + * completes. + * <p> + * <img width="640" height="200" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/timer.s.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>This operator does not support backpressure as it uses time. If the downstream needs a slower rate + * it should slow the timer or use something like {@link #onBackpressureDrop}.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>You specify which {@code Scheduler} this operator will use.</dd> + * </dl> + * + * @param delay + * the initial delay before emitting a single 0L + * @param unit + * time units to use for {@code delay} + * @param scheduler + * the {@code Scheduler} to use for scheduling the item + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code unit} or {@code scheduler} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/timer.html">ReactiveX operators documentation: Timer</a> + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.ERROR) + @SchedulerSupport(SchedulerSupport.CUSTOM) + public static Flowable<Long> timer(long delay, @NonNull TimeUnit unit, @NonNull Scheduler scheduler) { + Objects.requireNonNull(unit, "unit is null"); + Objects.requireNonNull(scheduler, "scheduler is null"); + + return RxJavaPlugins.onAssembly(new FlowableTimer(Math.max(0L, delay), unit, scheduler)); + } + + /** + * Create a {@code Flowable} by wrapping a {@link Publisher} <em>which has to be implemented according + * to the <b>Reactive Streams</b> specification by handling backpressure and + * cancellation correctly; no safeguards are provided by the {@code Flowable} itself</em>. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>This operator is a pass-through for backpressure and the behavior is determined by the + * provided {@code Publisher} implementation.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code unsafeCreate} by default doesn't operate on any particular {@link Scheduler}.</dd> + * </dl> + * @param <T> the value type emitted + * @param onSubscribe the {@code Publisher} instance to wrap + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code onSubscribe} is {@code null} + * @throws IllegalArgumentException if {@code onSubscribe} is a subclass of {@code Flowable}; such + * instances don't need conversion and is possibly a port remnant from 1.x or one should use {@link #hide()} + * instead. + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.NONE) + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T> Flowable<T> unsafeCreate(@NonNull Publisher<T> onSubscribe) { + Objects.requireNonNull(onSubscribe, "onSubscribe is null"); + if (onSubscribe instanceof Flowable) { + throw new IllegalArgumentException("unsafeCreate(Flowable) should be upgraded"); + } + return RxJavaPlugins.onAssembly(new FlowableFromPublisher<>(onSubscribe)); + } + + /** + * Constructs a {@code Flowable} that creates a dependent resource object, a {@link Publisher} with + * that resource and calls the provided {@code resourceDisposer} function if this inner source terminates or the + * downstream cancels the flow. + * <p> + * <img width="640" height="400" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/using.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator is a pass-through for backpressure and otherwise depends on the + * backpressure support of the {@code Publisher} returned by the {@code resourceFactory}.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code using} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T> the element type of the generated {@code Publisher} + * @param <D> the type of the resource associated with the output sequence + * @param resourceSupplier + * the factory function to create a resource object that depends on the {@code Publisher} + * @param sourceSupplier + * the factory function to create a {@code Publisher} + * @param resourceCleanup + * the function that will dispose of the resource + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code resourceSupplier}, {@code sourceSupplier} or {@code resourceCleanup} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/using.html">ReactiveX operators documentation: Using</a> + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.PASS_THROUGH) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public static <@NonNull T, @NonNull D> Flowable<T> using( + @NonNull Supplier<? extends D> resourceSupplier, + @NonNull Function<? super D, @NonNull ? extends Publisher<? extends T>> sourceSupplier, + @NonNull Consumer<? super D> resourceCleanup) { + return using(resourceSupplier, sourceSupplier, resourceCleanup, true); + } + + /** + * Constructs a {@code Flowable} that creates a dependent resource object, a {@link Publisher} with + * that resource and calls the provided {@code resourceDisposer} function if this inner source terminates or the + * downstream disposes the flow; doing it before these end-states have been reached if {@code eager == true}, after otherwise. + * <p> + * <img width="640" height="400" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/using.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator is a pass-through for backpressure and otherwise depends on the + * backpressure support of the {@code Publisher} returned by the {@code resourceFactory}.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code using} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T> the element type of the generated {@code Publisher} + * @param <D> the type of the resource associated with the output sequence + * @param resourceSupplier + * the factory function to create a resource object that depends on the {@code Publisher} + * @param sourceSupplier + * the factory function to create a {@code Publisher} + * @param resourceCleanup + * the function that will dispose of the resource + * @param eager + * If {@code true}, the resource disposal will happen either on a {@code cancel()} call before the upstream is disposed + * or just before the emission of a terminal event ({@code onComplete} or {@code onError}). + * If {@code false} the resource disposal will happen either on a {@code cancel()} call after the upstream is disposed + * or just after the emission of a terminal event ({@code onComplete} or {@code onError}). + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code resourceSupplier}, {@code sourceSupplier} or {@code resourceCleanup} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/using.html">ReactiveX operators documentation: Using</a> + * @since 2.0 + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.PASS_THROUGH) + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T, @NonNull D> Flowable<T> using( + @NonNull Supplier<? extends D> resourceSupplier, + @NonNull Function<? super D, @NonNull ? extends Publisher<? extends T>> sourceSupplier, + @NonNull Consumer<? super D> resourceCleanup, + boolean eager) { + Objects.requireNonNull(resourceSupplier, "resourceSupplier is null"); + Objects.requireNonNull(sourceSupplier, "sourceSupplier is null"); + Objects.requireNonNull(resourceCleanup, "resourceCleanup is null"); + return RxJavaPlugins.onAssembly(new FlowableUsing<T, D>(resourceSupplier, sourceSupplier, resourceCleanup, eager)); + } + + /** + * Returns a {@code Flowable} that emits the results of a specified combiner function applied to combinations of + * items emitted, in sequence, by an {@link Iterable} of other {@link Publisher}s. + * <p> + * {@code zip} applies this function in strict sequence, so the first item emitted by the new {@code Publisher} + * will be the result of the function applied to the first item emitted by each of the source {@code Publisher}s; + * the second item emitted by the new {@code Publisher} will be the result of the function applied to the second + * item emitted by each of those {@code Publisher}s; and so forth. + * <p> + * The resulting {@code Flowable} returned from {@code zip} will invoke {@code onNext} as many times as + * the number of {@code onNext} invocations of the source {@code Publisher} that emits the fewest items. + * <p> + * The operator subscribes to its sources in the order they are specified and completes eagerly if + * one of the sources is shorter than the rest while canceling the other sources. Therefore, it + * is possible those other sources will never be able to run to completion (and thus not calling + * {@code doOnComplete()}). This can also happen if the sources are exactly the same length; if + * source A completes and B has been consumed and is about to complete, the operator detects A won't + * be sending further values and it will cancel B immediately. For example: + * <pre><code>zip(Arrays.asList(range(1, 5).doOnComplete(action1), range(6, 5).doOnComplete(action2)), (a) -> a)</code></pre> + * {@code action1} will be called but {@code action2} won't. + * <br>To work around this termination property, + * use {@link #doOnCancel(Action)} as well or use {@code using()} to do cleanup in case of completion + * or cancellation. + * <p> + * <img width="640" height="380" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/zip.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator expects backpressure from the sources and honors backpressure from the downstream. + * (I.e., zipping with {@link #interval(long, TimeUnit)} may result in {@link MissingBackpressureException}, use + * one of the {@code onBackpressureX} to handle similar, backpressure-ignoring sources.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code zip} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T> the common value type + * @param <R> the zipped result type + * @param sources + * an {@code Iterable} of source {@code Publisher}s + * @param zipper + * a function that, when applied to an item emitted by each of the source {@code Publisher}s, results in + * an item that will be emitted by the resulting {@code Flowable} + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code sources} or {@code zipper} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/zip.html">ReactiveX operators documentation: Zip</a> + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T, @NonNull R> Flowable<R> zip(@NonNull Iterable<@NonNull ? extends Publisher<? extends T>> sources, @NonNull Function<? super Object[], ? extends R> zipper) { + Objects.requireNonNull(zipper, "zipper is null"); + Objects.requireNonNull(sources, "sources is null"); + return RxJavaPlugins.onAssembly(new FlowableZip<>(null, sources, zipper, bufferSize(), false)); + } + + /** + * Returns a {@code Flowable} that emits the results of a specified combiner function applied to combinations of + * items emitted, in sequence, by an {@link Iterable} of other {@link Publisher}s. + * <p> + * {@code zip} applies this function in strict sequence, so the first item emitted by the new {@code Publisher} + * will be the result of the function applied to the first item emitted by each of the source {@code Publisher}s; + * the second item emitted by the new {@code Publisher} will be the result of the function applied to the second + * item emitted by each of those {@code Publisher}s; and so forth. + * <p> + * The resulting {@code Floawble} returned from {@code zip} will invoke {@code onNext} as many times as + * the number of {@code onNext} invocations of the source {@code Publisher} that emits the fewest items. + * <p> + * The operator subscribes to its sources in the order they are specified and completes eagerly if + * one of the sources is shorter than the rest while canceling the other sources. Therefore, it + * is possible those other sources will never be able to run to completion (and thus not calling + * {@code doOnComplete()}). This can also happen if the sources are exactly the same length; if + * source A completes and B has been consumed and is about to complete, the operator detects A won't + * be sending further values and it will cancel B immediately. For example: + * <pre><code>zip(Arrays.asList(range(1, 5).doOnComplete(action1), range(6, 5).doOnComplete(action2)), (a) -> a)</code></pre> + * {@code action1} will be called but {@code action2} won't. + * <br>To work around this termination property, + * use {@link #doOnCancel(Action)} as well or use {@code using()} to do cleanup in case of completion + * or cancellation. + * <p> + * <img width="640" height="380" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/zip.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator expects backpressure from the sources and honors backpressure from the downstream. + * (I.e., zipping with {@link #interval(long, TimeUnit)} may result in {@link MissingBackpressureException}, use + * one of the {@code onBackpressureX} to handle similar, backpressure-ignoring sources.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code zip} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * + * @param sources + * an {@code Iterable} of source {@code Publisher}s + * @param zipper + * a function that, when applied to an item emitted by each of the source {@code Publisher}s, results in + * an item that will be emitted by the resulting {@code Flowable} + * @param delayError + * delay errors signaled by any of the source {@code Publisher} until all {@code Publisher}s terminate + * @param bufferSize + * the number of elements to prefetch from each source {@code Publisher} + * @param <T> the common source value type + * @param <R> the zipped result type + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code sources} or {@code zipper} is {@code null} + * @throws IllegalArgumentException if {@code bufferSize} is non-positive + * @see <a href="http://reactivex.io/documentation/operators/zip.html">ReactiveX operators documentation: Zip</a> + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T, @NonNull R> Flowable<R> zip(@NonNull Iterable<@NonNull ? extends Publisher<? extends T>> sources, + @NonNull Function<? super Object[], ? extends R> zipper, boolean delayError, + int bufferSize) { + Objects.requireNonNull(zipper, "zipper is null"); + Objects.requireNonNull(sources, "sources is null"); + ObjectHelper.verifyPositive(bufferSize, "bufferSize"); + return RxJavaPlugins.onAssembly(new FlowableZip<>(null, sources, zipper, bufferSize, delayError)); + } + + /** + * Returns a {@code Flowable} that emits the results of a specified combiner function applied to combinations of + * two items emitted, in sequence, by two other {@link Publisher}s. + * <p> + * <img width="640" height="380" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/zip.v3.png" alt=""> + * <p> + * {@code zip} applies this function in strict sequence, so the first item emitted by the new {@code Publisher} + * will be the result of the function applied to the first item emitted by {@code o1} and the first item + * emitted by {@code o2}; the second item emitted by the new {@code Publisher} will be the result of the function + * applied to the second item emitted by {@code o1} and the second item emitted by {@code o2}; and so forth. + * <p> + * The resulting {@code Flowable} returned from {@code zip} will invoke {@link Subscriber#onNext onNext} + * as many times as the number of {@code onNext} invocations of the source {@code Publisher} that emits the fewest + * items. + * <p> + * The operator subscribes to its sources in the order they are specified and completes eagerly if + * one of the sources is shorter than the rest while canceling the other sources. Therefore, it + * is possible those other sources will never be able to run to completion (and thus not calling + * {@code doOnComplete()}). This can also happen if the sources are exactly the same length; if + * source A completes and B has been consumed and is about to complete, the operator detects A won't + * be sending further values and it will cancel B immediately. For example: + * <pre><code>zip(range(1, 5).doOnComplete(action1), range(6, 5).doOnComplete(action2), (a, b) -> a + b)</code></pre> + * {@code action1} will be called but {@code action2} won't. + * <br>To work around this termination property, + * use {@link #doOnCancel(Action)} as well or use {@code using()} to do cleanup in case of completion + * or cancellation. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator expects backpressure from the sources and honors backpressure from the downstream. + * (I.e., zipping with {@link #interval(long, TimeUnit)} may result in {@link MissingBackpressureException}, use + * one of the {@code onBackpressureX} to handle similar, backpressure-ignoring sources.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code zip} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T1> the value type of the first source + * @param <T2> the value type of the second source + * @param <R> the zipped result type + * @param source1 + * the first source {@code Publisher} + * @param source2 + * a second source {@code Publisher} + * @param zipper + * a function that, when applied to an item emitted by each of the source {@code Publisher}s, results + * in an item that will be emitted by the resulting {@code Flowable} + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code source1}, {@code source2} or {@code zipper} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/zip.html">ReactiveX operators documentation: Zip</a> + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T1, @NonNull T2, @NonNull R> Flowable<R> zip( + @NonNull Publisher<? extends T1> source1, @NonNull Publisher<? extends T2> source2, + @NonNull BiFunction<? super T1, ? super T2, ? extends R> zipper) { + Objects.requireNonNull(source1, "source1 is null"); + Objects.requireNonNull(source2, "source2 is null"); + Objects.requireNonNull(zipper, "zipper is null"); + return zipArray(Functions.toFunction(zipper), false, bufferSize(), source1, source2); + } + + /** + * Returns a {@code Flowable} that emits the results of a specified combiner function applied to combinations of + * two items emitted, in sequence, by two other {@link Publisher}s. + * <p> + * <img width="640" height="380" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/zip.v3.png" alt=""> + * <p> + * {@code zip} applies this function in strict sequence, so the first item emitted by the new {@code Publisher} + * will be the result of the function applied to the first item emitted by {@code o1} and the first item + * emitted by {@code o2}; the second item emitted by the new {@code Publisher} will be the result of the function + * applied to the second item emitted by {@code o1} and the second item emitted by {@code o2}; and so forth. + * <p> + * The resulting {@code Flowable} returned from {@code zip} will invoke {@link Subscriber#onNext onNext} + * as many times as the number of {@code onNext} invocations of the source {@code Publisher} that emits the fewest + * items. + * <p> + * The operator subscribes to its sources in the order they are specified and completes eagerly if + * one of the sources is shorter than the rest while canceling the other sources. Therefore, it + * is possible those other sources will never be able to run to completion (and thus not calling + * {@code doOnComplete()}). This can also happen if the sources are exactly the same length; if + * source A completes and B has been consumed and is about to complete, the operator detects A won't + * be sending further values and it will cancel B immediately. For example: + * <pre><code>zip(range(1, 5).doOnComplete(action1), range(6, 5).doOnComplete(action2), (a, b) -> a + b)</code></pre> + * {@code action1} will be called but {@code action2} won't. + * <br>To work around this termination property, + * use {@link #doOnCancel(Action)} as well or use {@code using()} to do cleanup in case of completion + * or cancellation. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator expects backpressure from the sources and honors backpressure from the downstream. + * (I.e., zipping with {@link #interval(long, TimeUnit)} may result in {@link MissingBackpressureException}, use + * one of the {@code onBackpressureX} to handle similar, backpressure-ignoring sources.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code zip} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T1> the value type of the first source + * @param <T2> the value type of the second source + * @param <R> the zipped result type + * @param source1 + * the first source {@code Publisher} + * @param source2 + * a second source {@code Publisher} + * @param zipper + * a function that, when applied to an item emitted by each of the source {@code Publisher}s, results + * in an item that will be emitted by the resulting {@code Flowable} + * @param delayError delay errors from any of the source {@code Publisher}s till the other terminates + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code source1}, {@code source2} or {@code zipper} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/zip.html">ReactiveX operators documentation: Zip</a> + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T1, @NonNull T2, @NonNull R> Flowable<R> zip( + @NonNull Publisher<? extends T1> source1, @NonNull Publisher<? extends T2> source2, + @NonNull BiFunction<? super T1, ? super T2, ? extends R> zipper, boolean delayError) { + Objects.requireNonNull(source1, "source1 is null"); + Objects.requireNonNull(source2, "source2 is null"); + Objects.requireNonNull(zipper, "zipper is null"); + return zipArray(Functions.toFunction(zipper), delayError, bufferSize(), source1, source2); + } + + /** + * Returns a {@code Flowable} that emits the results of a specified combiner function applied to combinations of + * two items emitted, in sequence, by two other {@link Publisher}s. + * <p> + * <img width="640" height="380" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/zip.v3.png" alt=""> + * <p> + * {@code zip} applies this function in strict sequence, so the first item emitted by the new {@code Publisher} + * will be the result of the function applied to the first item emitted by {@code o1} and the first item + * emitted by {@code o2}; the second item emitted by the new {@code Publisher} will be the result of the function + * applied to the second item emitted by {@code o1} and the second item emitted by {@code o2}; and so forth. + * <p> + * The resulting {@code Flowable} returned from {@code zip} will invoke {@link Subscriber#onNext onNext} + * as many times as the number of {@code onNext} invocations of the source {@code Publisher} that emits the fewest + * items. + * <p> + * The operator subscribes to its sources in the order they are specified and completes eagerly if + * one of the sources is shorter than the rest while canceling the other sources. Therefore, it + * is possible those other sources will never be able to run to completion (and thus not calling + * {@code doOnComplete()}). This can also happen if the sources are exactly the same length; if + * source A completes and B has been consumed and is about to complete, the operator detects A won't + * be sending further values and it will cancel B immediately. For example: + * <pre><code>zip(range(1, 5).doOnComplete(action1), range(6, 5).doOnComplete(action2), (a, b) -> a + b)</code></pre> + * {@code action1} will be called but {@code action2} won't. + * <br>To work around this termination property, + * use {@link #doOnCancel(Action)} as well or use {@code using()} to do cleanup in case of completion + * or cancellation. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator expects backpressure from the sources and honors backpressure from the downstream. + * (I.e., zipping with {@link #interval(long, TimeUnit)} may result in {@link MissingBackpressureException}, use + * one of the {@code onBackpressureX} to handle similar, backpressure-ignoring sources.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code zip} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T1> the value type of the first source + * @param <T2> the value type of the second source + * @param <R> the zipped result type + * @param source1 + * the first source {@code Publisher} + * @param source2 + * a second source {@code Publisher} + * @param zipper + * a function that, when applied to an item emitted by each of the source {@code Publisher}s, results + * in an item that will be emitted by the resulting {@code Flowable} + * @param delayError delay errors from any of the source {@code Publisher}s till the other terminates + * @param bufferSize the number of elements to prefetch from each source {@code Publisher} + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code source1}, {@code source2} or {@code zipper} is {@code null} + * @throws IllegalArgumentException if {@code bufferSize} is non-positive + * @see <a href="http://reactivex.io/documentation/operators/zip.html">ReactiveX operators documentation: Zip</a> + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T1, @NonNull T2, @NonNull R> Flowable<R> zip( + @NonNull Publisher<? extends T1> source1, @NonNull Publisher<? extends T2> source2, + @NonNull BiFunction<? super T1, ? super T2, ? extends R> zipper, boolean delayError, int bufferSize) { + Objects.requireNonNull(source1, "source1 is null"); + Objects.requireNonNull(source2, "source2 is null"); + Objects.requireNonNull(zipper, "zipper is null"); + return zipArray(Functions.toFunction(zipper), delayError, bufferSize, source1, source2); + } + + /** + * Returns a {@code Flowable} that emits the results of a specified combiner function applied to combinations of + * three items emitted, in sequence, by three other {@link Publisher}s. + * <p> + * <img width="640" height="380" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/zip.v3.png" alt=""> + * <p> + * {@code zip} applies this function in strict sequence, so the first item emitted by the new {@code Publisher} + * will be the result of the function applied to the first item emitted by {@code o1}, the first item + * emitted by {@code o2}, and the first item emitted by {@code o3}; the second item emitted by the new + * {@code Publisher} will be the result of the function applied to the second item emitted by {@code o1}, the + * second item emitted by {@code o2}, and the second item emitted by {@code o3}; and so forth. + * <p> + * The resulting {@code Flowable} returned from {@code zip} will invoke {@link Subscriber#onNext onNext} + * as many times as the number of {@code onNext} invocations of the source {@code Publisher} that emits the fewest + * items. + * <p> + * The operator subscribes to its sources in the order they are specified and completes eagerly if + * one of the sources is shorter than the rest while canceling the other sources. Therefore, it + * is possible those other sources will never be able to run to completion (and thus not calling + * {@code doOnComplete()}). This can also happen if the sources are exactly the same length; if + * source A completes and B has been consumed and is about to complete, the operator detects A won't + * be sending further values and it will cancel B immediately. For example: + * <pre><code>zip(range(1, 5).doOnComplete(action1), range(6, 5).doOnComplete(action2), ..., (a, b, c) -> a + b)</code></pre> + * {@code action1} will be called but {@code action2} won't. + * <br>To work around this termination property, + * use {@link #doOnCancel(Action)} as well or use {@code using()} to do cleanup in case of completion + * or cancellation. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator expects backpressure from the sources and honors backpressure from the downstream. + * (I.e., zipping with {@link #interval(long, TimeUnit)} may result in {@link MissingBackpressureException}, use + * one of the {@code onBackpressureX} to handle similar, backpressure-ignoring sources.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code zip} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T1> the value type of the first source + * @param <T2> the value type of the second source + * @param <T3> the value type of the third source + * @param <R> the zipped result type + * @param source1 + * the first source {@code Publisher} + * @param source2 + * a second source {@code Publisher} + * @param source3 + * a third source {@code Publisher} + * @param zipper + * a function that, when applied to an item emitted by each of the source {@code Publisher}s, results in + * an item that will be emitted by the resulting {@code Flowable} + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code source1}, {@code source2}, {@code source3} or {@code zipper} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/zip.html">ReactiveX operators documentation: Zip</a> + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T1, @NonNull T2, @NonNull T3, @NonNull R> Flowable<R> zip( + @NonNull Publisher<? extends T1> source1, @NonNull Publisher<? extends T2> source2, @NonNull Publisher<? extends T3> source3, + @NonNull Function3<? super T1, ? super T2, ? super T3, ? extends R> zipper) { + Objects.requireNonNull(source1, "source1 is null"); + Objects.requireNonNull(source2, "source2 is null"); + Objects.requireNonNull(source3, "source3 is null"); + Objects.requireNonNull(zipper, "zipper is null"); + return zipArray(Functions.toFunction(zipper), false, bufferSize(), source1, source2, source3); + } + + /** + * Returns a {@code Flowable} that emits the results of a specified combiner function applied to combinations of + * four items emitted, in sequence, by four other {@link Publisher}s. + * <p> + * <img width="640" height="380" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/zip.v3.png" alt=""> + * <p> + * {@code zip} applies this function in strict sequence, so the first item emitted by the new {@code Publisher} + * will be the result of the function applied to the first item emitted by {@code o1}, the first item + * emitted by {@code o2}, the first item emitted by {@code o3}, and the first item emitted by {@code 04}; + * the second item emitted by the new {@code Publisher} will be the result of the function applied to the second + * item emitted by each of those {@code Publisher}s; and so forth. + * <p> + * The resulting {@code Flowable} returned from {@code zip} will invoke {@link Subscriber#onNext onNext} + * as many times as the number of {@code onNext} invocations of the source {@code Publisher} that emits the fewest + * items. + * <p> + * The operator subscribes to its sources in the order they are specified and completes eagerly if + * one of the sources is shorter than the rest while canceling the other sources. Therefore, it + * is possible those other sources will never be able to run to completion (and thus not calling + * {@code doOnComplete()}). This can also happen if the sources are exactly the same length; if + * source A completes and B has been consumed and is about to complete, the operator detects A won't + * be sending further values and it will cancel B immediately. For example: + * <pre><code>zip(range(1, 5).doOnComplete(action1), range(6, 5).doOnComplete(action2), ..., (a, b, c, d) -> a + b)</code></pre> + * {@code action1} will be called but {@code action2} won't. + * <br>To work around this termination property, + * use {@link #doOnCancel(Action)} as well or use {@code using()} to do cleanup in case of completion + * or cancellation. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator expects backpressure from the sources and honors backpressure from the downstream. + * (I.e., zipping with {@link #interval(long, TimeUnit)} may result in {@link MissingBackpressureException}, use + * one of the {@code onBackpressureX} to handle similar, backpressure-ignoring sources.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code zip} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T1> the value type of the first source + * @param <T2> the value type of the second source + * @param <T3> the value type of the third source + * @param <T4> the value type of the fourth source + * @param <R> the zipped result type + * @param source1 + * the first source {@code Publisher} + * @param source2 + * a second source {@code Publisher} + * @param source3 + * a third source {@code Publisher} + * @param source4 + * a fourth source {@code Publisher} + * @param zipper + * a function that, when applied to an item emitted by each of the source {@code Publisher}s, results in + * an item that will be emitted by the resulting {@code Flowable} + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code source1}, {@code source2}, {@code source3}, + * {@code source4} or {@code zipper} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/zip.html">ReactiveX operators documentation: Zip</a> + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T1, @NonNull T2, @NonNull T3, @NonNull T4, @NonNull R> Flowable<R> zip( + @NonNull Publisher<? extends T1> source1, @NonNull Publisher<? extends T2> source2, @NonNull Publisher<? extends T3> source3, + @NonNull Publisher<? extends T4> source4, + @NonNull Function4<? super T1, ? super T2, ? super T3, ? super T4, ? extends R> zipper) { + Objects.requireNonNull(source1, "source1 is null"); + Objects.requireNonNull(source2, "source2 is null"); + Objects.requireNonNull(source3, "source3 is null"); + Objects.requireNonNull(source4, "source4 is null"); + Objects.requireNonNull(zipper, "zipper is null"); + return zipArray(Functions.toFunction(zipper), false, bufferSize(), source1, source2, source3, source4); + } + + /** + * Returns a {@code Flowable} that emits the results of a specified combiner function applied to combinations of + * five items emitted, in sequence, by five other {@link Publisher}s. + * <p> + * <img width="640" height="380" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/zip.v3.png" alt=""> + * <p> + * {@code zip} applies this function in strict sequence, so the first item emitted by the new {@code Publisher} + * will be the result of the function applied to the first item emitted by {@code o1}, the first item + * emitted by {@code o2}, the first item emitted by {@code o3}, the first item emitted by {@code o4}, and + * the first item emitted by {@code o5}; the second item emitted by the new {@code Publisher} will be the result of + * the function applied to the second item emitted by each of those {@code Publisher}s; and so forth. + * <p> + * The resulting {@code Flowable} returned from {@code zip} will invoke {@link Subscriber#onNext onNext} + * as many times as the number of {@code onNext} invocations of the source {@code Publisher} that emits the fewest + * items. + * <p> + * The operator subscribes to its sources in the order they are specified and completes eagerly if + * one of the sources is shorter than the rest while canceling the other sources. Therefore, it + * is possible those other sources will never be able to run to completion (and thus not calling + * {@code doOnComplete()}). This can also happen if the sources are exactly the same length; if + * source A completes and B has been consumed and is about to complete, the operator detects A won't + * be sending further values and it will cancel B immediately. For example: + * <pre><code>zip(range(1, 5).doOnComplete(action1), range(6, 5).doOnComplete(action2), ..., (a, b, c, d, e) -> a + b)</code></pre> + * {@code action1} will be called but {@code action2} won't. + * <br>To work around this termination property, + * use {@link #doOnCancel(Action)} as well or use {@code using()} to do cleanup in case of completion + * or cancellation. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator expects backpressure from the sources and honors backpressure from the downstream. + * (I.e., zipping with {@link #interval(long, TimeUnit)} may result in {@link MissingBackpressureException}, use + * one of the {@code onBackpressureX} to handle similar, backpressure-ignoring sources.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code zip} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T1> the value type of the first source + * @param <T2> the value type of the second source + * @param <T3> the value type of the third source + * @param <T4> the value type of the fourth source + * @param <T5> the value type of the fifth source + * @param <R> the zipped result type + * @param source1 + * the first source {@code Publisher} + * @param source2 + * a second source {@code Publisher} + * @param source3 + * a third source {@code Publisher} + * @param source4 + * a fourth source {@code Publisher} + * @param source5 + * a fifth source {@code Publisher} + * @param zipper + * a function that, when applied to an item emitted by each of the source {@code Publisher}s, results in + * an item that will be emitted by the resulting {@code Flowable} + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code source1}, {@code source2}, {@code source3}, + * {@code source4}, {@code source5} or {@code zipper} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/zip.html">ReactiveX operators documentation: Zip</a> + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T1, @NonNull T2, @NonNull T3, @NonNull T4, @NonNull T5, @NonNull R> Flowable<R> zip( + @NonNull Publisher<? extends T1> source1, @NonNull Publisher<? extends T2> source2, @NonNull Publisher<? extends T3> source3, + @NonNull Publisher<? extends T4> source4, @NonNull Publisher<? extends T5> source5, + @NonNull Function5<? super T1, ? super T2, ? super T3, ? super T4, ? super T5, ? extends R> zipper) { + Objects.requireNonNull(source1, "source1 is null"); + Objects.requireNonNull(source2, "source2 is null"); + Objects.requireNonNull(source3, "source3 is null"); + Objects.requireNonNull(source4, "source4 is null"); + Objects.requireNonNull(source5, "source5 is null"); + Objects.requireNonNull(zipper, "zipper is null"); + return zipArray(Functions.toFunction(zipper), false, bufferSize(), source1, source2, source3, source4, source5); + } + + /** + * Returns a {@code Flowable} that emits the results of a specified combiner function applied to combinations of + * six items emitted, in sequence, by six other {@link Publisher}s. + * <p> + * <img width="640" height="380" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/zip.v3.png" alt=""> + * <p> + * {@code zip} applies this function in strict sequence, so the first item emitted by the new {@code Publisher} + * will be the result of the function applied to the first item emitted by each source {@code Publisher}, the + * second item emitted by the new {@code Publisher} will be the result of the function applied to the second item + * emitted by each of those {@code Publisher}s, and so forth. + * <p> + * The resulting {@code Flowable} returned from {@code zip} will invoke {@link Subscriber#onNext onNext} + * as many times as the number of {@code onNext} invocations of the source {@code Publisher} that emits the fewest + * items. + * <p> + * The operator subscribes to its sources in the order they are specified and completes eagerly if + * one of the sources is shorter than the rest while canceling the other sources. Therefore, it + * is possible those other sources will never be able to run to completion (and thus not calling + * {@code doOnComplete()}). This can also happen if the sources are exactly the same length; if + * source A completes and B has been consumed and is about to complete, the operator detects A won't + * be sending further values and it will cancel B immediately. For example: + * <pre><code>zip(range(1, 5).doOnComplete(action1), range(6, 5).doOnComplete(action2), ..., (a, b, c, d, e, f) -> a + b)</code></pre> + * {@code action1} will be called but {@code action2} won't. + * <br>To work around this termination property, + * use {@link #doOnCancel(Action)} as well or use {@code using()} to do cleanup in case of completion + * or cancellation. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator expects backpressure from the sources and honors backpressure from the downstream. + * (I.e., zipping with {@link #interval(long, TimeUnit)} may result in {@link MissingBackpressureException}, use + * one of the {@code onBackpressureX} to handle similar, backpressure-ignoring sources.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code zip} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T1> the value type of the first source + * @param <T2> the value type of the second source + * @param <T3> the value type of the third source + * @param <T4> the value type of the fourth source + * @param <T5> the value type of the fifth source + * @param <T6> the value type of the sixth source + * @param <R> the zipped result type + * @param source1 + * the first source {@code Publisher} + * @param source2 + * a second source {@code Publisher} + * @param source3 + * a third source {@code Publisher} + * @param source4 + * a fourth source {@code Publisher} + * @param source5 + * a fifth source {@code Publisher} + * @param source6 + * a sixth source {@code Publisher} + * @param zipper + * a function that, when applied to an item emitted by each of the source {@code Publisher}s, results in + * an item that will be emitted by the resulting {@code Flowable} + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code source1}, {@code source2}, {@code source3}, + * {@code source4}, {@code source5}, {@code source6} + * or {@code zipper} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/zip.html">ReactiveX operators documentation: Zip</a> + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T1, @NonNull T2, @NonNull T3, @NonNull T4, @NonNull T5, @NonNull T6, @NonNull R> Flowable<R> zip( + @NonNull Publisher<? extends T1> source1, @NonNull Publisher<? extends T2> source2, @NonNull Publisher<? extends T3> source3, + @NonNull Publisher<? extends T4> source4, @NonNull Publisher<? extends T5> source5, @NonNull Publisher<? extends T6> source6, + @NonNull Function6<? super T1, ? super T2, ? super T3, ? super T4, ? super T5, ? super T6, ? extends R> zipper) { + Objects.requireNonNull(source1, "source1 is null"); + Objects.requireNonNull(source2, "source2 is null"); + Objects.requireNonNull(source3, "source3 is null"); + Objects.requireNonNull(source4, "source4 is null"); + Objects.requireNonNull(source5, "source5 is null"); + Objects.requireNonNull(source6, "source6 is null"); + Objects.requireNonNull(zipper, "zipper is null"); + return zipArray(Functions.toFunction(zipper), false, bufferSize(), source1, source2, source3, source4, source5, source6); + } + + /** + * Returns a {@code Flowable} that emits the results of a specified combiner function applied to combinations of + * seven items emitted, in sequence, by seven other {@link Publisher}s. + * <p> + * <img width="640" height="380" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/zip.v3.png" alt=""> + * <p> + * {@code zip} applies this function in strict sequence, so the first item emitted by the new {@code Publisher} + * will be the result of the function applied to the first item emitted by each source {@code Publisher}, the + * second item emitted by the new {@code Publisher} will be the result of the function applied to the second item + * emitted by each of those {@code Publisher}s, and so forth. + * <p> + * The resulting {@code Flowable} returned from {@code zip} will invoke {@link Subscriber#onNext onNext} + * as many times as the number of {@code onNext} invocations of the source {@code Publisher} that emits the fewest + * items. + * <p> + * The operator subscribes to its sources in the order they are specified and completes eagerly if + * one of the sources is shorter than the rest while canceling the other sources. Therefore, it + * is possible those other sources will never be able to run to completion (and thus not calling + * {@code doOnComplete()}). This can also happen if the sources are exactly the same length; if + * source A completes and B has been consumed and is about to complete, the operator detects A won't + * be sending further values and it will cancel B immediately. For example: + * <pre><code>zip(range(1, 5).doOnComplete(action1), range(6, 5).doOnComplete(action2), ..., (a, b, c, d, e, f, g) -> a + b)</code></pre> + * {@code action1} will be called but {@code action2} won't. + * <br>To work around this termination property, + * use {@link #doOnCancel(Action)} as well or use {@code using()} to do cleanup in case of completion + * or cancellation. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator expects backpressure from the sources and honors backpressure from the downstream. + * (I.e., zipping with {@link #interval(long, TimeUnit)} may result in {@link MissingBackpressureException}, use + * one of the {@code onBackpressureX} to handle similar, backpressure-ignoring sources.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code zip} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T1> the value type of the first source + * @param <T2> the value type of the second source + * @param <T3> the value type of the third source + * @param <T4> the value type of the fourth source + * @param <T5> the value type of the fifth source + * @param <T6> the value type of the sixth source + * @param <T7> the value type of the seventh source + * @param <R> the zipped result type + * @param source1 + * the first source {@code Publisher} + * @param source2 + * a second source {@code Publisher} + * @param source3 + * a third source {@code Publisher} + * @param source4 + * a fourth source {@code Publisher} + * @param source5 + * a fifth source {@code Publisher} + * @param source6 + * a sixth source {@code Publisher} + * @param source7 + * a seventh source {@code Publisher} + * @param zipper + * a function that, when applied to an item emitted by each of the source {@code Publisher}s, results in + * an item that will be emitted by the resulting {@code Flowable} + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code source1}, {@code source2}, {@code source3}, + * {@code source4}, {@code source5}, {@code source6}, + * {@code source7} or {@code zipper} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/zip.html">ReactiveX operators documentation: Zip</a> + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T1, @NonNull T2, @NonNull T3, @NonNull T4, @NonNull T5, @NonNull T6, @NonNull T7, @NonNull R> Flowable<R> zip( + @NonNull Publisher<? extends T1> source1, @NonNull Publisher<? extends T2> source2, @NonNull Publisher<? extends T3> source3, + @NonNull Publisher<? extends T4> source4, @NonNull Publisher<? extends T5> source5, @NonNull Publisher<? extends T6> source6, + @NonNull Publisher<? extends T7> source7, + @NonNull Function7<? super T1, ? super T2, ? super T3, ? super T4, ? super T5, ? super T6, ? super T7, ? extends R> zipper) { + Objects.requireNonNull(source1, "source1 is null"); + Objects.requireNonNull(source2, "source2 is null"); + Objects.requireNonNull(source3, "source3 is null"); + Objects.requireNonNull(source4, "source4 is null"); + Objects.requireNonNull(source5, "source5 is null"); + Objects.requireNonNull(source6, "source6 is null"); + Objects.requireNonNull(source7, "source7 is null"); + Objects.requireNonNull(zipper, "zipper is null"); + return zipArray(Functions.toFunction(zipper), false, bufferSize(), source1, source2, source3, source4, source5, source6, source7); + } + + /** + * Returns a {@code Flowable} that emits the results of a specified combiner function applied to combinations of + * eight items emitted, in sequence, by eight other {@link Publisher}s. + * <p> + * <img width="640" height="380" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/zip.v3.png" alt=""> + * <p> + * {@code zip} applies this function in strict sequence, so the first item emitted by the new {@code Publisher} + * will be the result of the function applied to the first item emitted by each source {@code Publisher}, the + * second item emitted by the new {@code Publisher} will be the result of the function applied to the second item + * emitted by each of those {@code Publisher}s, and so forth. + * <p> + * The resulting {@code Flowable} returned from {@code zip} will invoke {@link Subscriber#onNext onNext} + * as many times as the number of {@code onNext} invocations of the source {@code Publisher} that emits the fewest + * items. + * <p> + * The operator subscribes to its sources in the order they are specified and completes eagerly if + * one of the sources is shorter than the rest while canceling the other sources. Therefore, it + * is possible those other sources will never be able to run to completion (and thus not calling + * {@code doOnComplete()}). This can also happen if the sources are exactly the same length; if + * source A completes and B has been consumed and is about to complete, the operator detects A won't + * be sending further values and it will cancel B immediately. For example: + * <pre><code>zip(range(1, 5).doOnComplete(action1), range(6, 5).doOnComplete(action2), ..., (a, b, c, d, e, f, g, h) -> a + b)</code></pre> + * {@code action1} will be called but {@code action2} won't. + * <br>To work around this termination property, + * use {@link #doOnCancel(Action)} as well or use {@code using()} to do cleanup in case of completion + * or cancellation. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator expects backpressure from the sources and honors backpressure from the downstream. + * (I.e., zipping with {@link #interval(long, TimeUnit)} may result in {@link MissingBackpressureException}, use + * one of the {@code onBackpressureX} to handle similar, backpressure-ignoring sources.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code zip} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T1> the value type of the first source + * @param <T2> the value type of the second source + * @param <T3> the value type of the third source + * @param <T4> the value type of the fourth source + * @param <T5> the value type of the fifth source + * @param <T6> the value type of the sixth source + * @param <T7> the value type of the seventh source + * @param <T8> the value type of the eighth source + * @param <R> the zipped result type + * @param source1 + * the first source {@code Publisher} + * @param source2 + * a second source {@code Publisher} + * @param source3 + * a third source {@code Publisher} + * @param source4 + * a fourth source {@code Publisher} + * @param source5 + * a fifth source {@code Publisher} + * @param source6 + * a sixth source {@code Publisher} + * @param source7 + * a seventh source {@code Publisher} + * @param source8 + * an eighth source {@code Publisher} + * @param zipper + * a function that, when applied to an item emitted by each of the source {@code Publisher}s, results in + * an item that will be emitted by the resulting {@code Flowable} + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code source1}, {@code source2}, {@code source3}, + * {@code source4}, {@code source5}, {@code source6}, + * {@code source7}, {@code source8} or {@code zipper} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/zip.html">ReactiveX operators documentation: Zip</a> + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T1, @NonNull T2, @NonNull T3, @NonNull T4, @NonNull T5, @NonNull T6, @NonNull T7, @NonNull T8, @NonNull R> Flowable<R> zip( + @NonNull Publisher<? extends T1> source1, @NonNull Publisher<? extends T2> source2, @NonNull Publisher<? extends T3> source3, + @NonNull Publisher<? extends T4> source4, @NonNull Publisher<? extends T5> source5, @NonNull Publisher<? extends T6> source6, + @NonNull Publisher<? extends T7> source7, @NonNull Publisher<? extends T8> source8, + @NonNull Function8<? super T1, ? super T2, ? super T3, ? super T4, ? super T5, ? super T6, ? super T7, ? super T8, ? extends R> zipper) { + Objects.requireNonNull(source1, "source1 is null"); + Objects.requireNonNull(source2, "source2 is null"); + Objects.requireNonNull(source3, "source3 is null"); + Objects.requireNonNull(source4, "source4 is null"); + Objects.requireNonNull(source5, "source5 is null"); + Objects.requireNonNull(source6, "source6 is null"); + Objects.requireNonNull(source7, "source7 is null"); + Objects.requireNonNull(source8, "source8 is null"); + Objects.requireNonNull(zipper, "zipper is null"); + return zipArray(Functions.toFunction(zipper), false, bufferSize(), source1, source2, source3, source4, source5, source6, source7, source8); + } + + /** + * Returns a {@code Flowable} that emits the results of a specified combiner function applied to combinations of + * nine items emitted, in sequence, by nine other {@link Publisher}s. + * <p> + * <img width="640" height="380" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/zip.v3.png" alt=""> + * <p> + * {@code zip} applies this function in strict sequence, so the first item emitted by the new {@code Publisher} + * will be the result of the function applied to the first item emitted by each source {@code Publisher}, the + * second item emitted by the new {@code Publisher} will be the result of the function applied to the second item + * emitted by each of those {@code Publisher}s, and so forth. + * <p> + * The resulting {@code Flowable} returned from {@code zip} will invoke {@link Subscriber#onNext onNext} + * as many times as the number of {@code onNext} invocations of the source {@code Publisher} that emits the fewest + * items. + * <p> + * The operator subscribes to its sources in the order they are specified and completes eagerly if + * one of the sources is shorter than the rest while canceling the other sources. Therefore, it + * is possible those other sources will never be able to run to completion (and thus not calling + * {@code doOnComplete()}). This can also happen if the sources are exactly the same length; if + * source A completes and B has been consumed and is about to complete, the operator detects A won't + * be sending further values and it will cancel B immediately. For example: + * <pre><code>zip(range(1, 5).doOnComplete(action1), range(6, 5).doOnComplete(action2), ..., (a, b, c, d, e, f, g, h, i) -> a + b)</code></pre> + * {@code action1} will be called but {@code action2} won't. + * <br>To work around this termination property, + * use {@link #doOnCancel(Action)} as well or use {@code using()} to do cleanup in case of completion + * or cancellation. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator expects backpressure from the sources and honors backpressure from the downstream. + * (I.e., zipping with {@link #interval(long, TimeUnit)} may result in {@link MissingBackpressureException}, use + * one of the {@code onBackpressureX} to handle similar, backpressure-ignoring sources.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code zip} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T1> the value type of the first source + * @param <T2> the value type of the second source + * @param <T3> the value type of the third source + * @param <T4> the value type of the fourth source + * @param <T5> the value type of the fifth source + * @param <T6> the value type of the sixth source + * @param <T7> the value type of the seventh source + * @param <T8> the value type of the eighth source + * @param <T9> the value type of the ninth source + * @param <R> the zipped result type + * @param source1 + * the first source {@code Publisher} + * @param source2 + * a second source {@code Publisher} + * @param source3 + * a third source {@code Publisher} + * @param source4 + * a fourth source {@code Publisher} + * @param source5 + * a fifth source {@code Publisher} + * @param source6 + * a sixth source {@code Publisher} + * @param source7 + * a seventh source {@code Publisher} + * @param source8 + * an eighth source {@code Publisher} + * @param source9 + * a ninth source {@code Publisher} + * @param zipper + * a function that, when applied to an item emitted by each of the source {@code Publisher}s, results in + * an item that will be emitted by the resulting {@code Flowable} + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code source1}, {@code source2}, {@code source3}, + * {@code source4}, {@code source5}, {@code source6}, + * {@code source7}, {@code source8}, {@code source9} + * or {@code zipper} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/zip.html">ReactiveX operators documentation: Zip</a> + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T1, @NonNull T2, @NonNull T3, @NonNull T4, @NonNull T5, @NonNull T6, @NonNull T7, @NonNull T8, @NonNull T9, @NonNull R> Flowable<R> zip( + @NonNull Publisher<? extends T1> source1, @NonNull Publisher<? extends T2> source2, @NonNull Publisher<? extends T3> source3, + @NonNull Publisher<? extends T4> source4, @NonNull Publisher<? extends T5> source5, @NonNull Publisher<? extends T6> source6, + @NonNull Publisher<? extends T7> source7, @NonNull Publisher<? extends T8> source8, @NonNull Publisher<? extends T9> source9, + @NonNull Function9<? super T1, ? super T2, ? super T3, ? super T4, ? super T5, ? super T6, ? super T7, ? super T8, ? super T9, ? extends R> zipper) { + + Objects.requireNonNull(source1, "source1 is null"); + Objects.requireNonNull(source2, "source2 is null"); + Objects.requireNonNull(source3, "source3 is null"); + Objects.requireNonNull(source4, "source4 is null"); + Objects.requireNonNull(source5, "source5 is null"); + Objects.requireNonNull(source6, "source6 is null"); + Objects.requireNonNull(source7, "source7 is null"); + Objects.requireNonNull(source8, "source8 is null"); + Objects.requireNonNull(source9, "source9 is null"); + Objects.requireNonNull(zipper, "zipper is null"); + return zipArray(Functions.toFunction(zipper), false, bufferSize(), source1, source2, source3, source4, source5, source6, source7, source8, source9); + } + + /** + * Returns a {@code Flowable} that emits the results of a specified combiner function applied to combinations of + * items emitted, in sequence, by an array of other {@link Publisher}s. + * <p> + * {@code zip} applies this function in strict sequence, so the first item emitted by the new {@code Publisher} + * will be the result of the function applied to the first item emitted by each of the source {@code Publisher}s; + * the second item emitted by the new {@code Publisher} will be the result of the function applied to the second + * item emitted by each of those {@code Publisher}s; and so forth. + * <p> + * The resulting {@code Flowable} returned from {@code zip} will invoke {@code onNext} as many times as + * the number of {@code onNext} invocations of the source {@code Publisher} that emits the fewest items. + * <p> + * The operator subscribes to its sources in the order they are specified and completes eagerly if + * one of the sources is shorter than the rest while canceling the other sources. Therefore, it + * is possible those other sources will never be able to run to completion (and thus not calling + * {@code doOnComplete()}). This can also happen if the sources are exactly the same length; if + * source A completes and B has been consumed and is about to complete, the operator detects A won't + * be sending further values and it will cancel B immediately. For example: + * <pre><code>zip(new Publisher[]{range(1, 5).doOnComplete(action1), range(6, 5).doOnComplete(action2)}, (a) -> + * a)</code></pre> + * {@code action1} will be called but {@code action2} won't. + * <br>To work around this termination property, + * use {@link #doOnCancel(Action)} as well or use {@code using()} to do cleanup in case of completion + * or cancellation. + * <p> + * <img width="640" height="380" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/zip.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator expects backpressure from the sources and honors backpressure from the downstream. + * (I.e., zipping with {@link #interval(long, TimeUnit)} may result in {@link MissingBackpressureException}, use + * one of the {@code onBackpressureX} to handle similar, backpressure-ignoring sources.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code zipArray} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T> the common element type + * @param <R> the result type + * @param sources + * an array of source {@code Publisher}s + * @param zipper + * a function that, when applied to an item emitted by each of the source {@code Publisher}s, results in + * an item that will be emitted by the resulting {@code Flowable} + * @param delayError + * delay errors signaled by any of the source {@code Publisher} until all {@code Publisher}s terminate + * @param bufferSize + * the number of elements to prefetch from each source {@code Publisher} + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code sources} or {@code zipper} is {@code null} + * @throws IllegalArgumentException if {@code bufferSize} is non-positive + * @see <a href="http://reactivex.io/documentation/operators/zip.html">ReactiveX operators documentation: Zip</a> + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + @SafeVarargs + public static <@NonNull T, @NonNull R> Flowable<R> zipArray(@NonNull Function<? super Object[], ? extends R> zipper, + boolean delayError, int bufferSize, @NonNull Publisher<? extends T>... sources) { + Objects.requireNonNull(sources, "sources is null"); + if (sources.length == 0) { + return empty(); + } + Objects.requireNonNull(zipper, "zipper is null"); + ObjectHelper.verifyPositive(bufferSize, "bufferSize"); + return RxJavaPlugins.onAssembly(new FlowableZip<>(sources, null, zipper, bufferSize, delayError)); + } + + // *************************************************************************************************** + // Instance operators + // *************************************************************************************************** + + /** + * Returns a {@link Single} that emits a {@link Boolean} that indicates whether all of the items emitted by the current + * {@code Flowable} satisfy a condition. + * <p> + * <img width="640" height="315" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/all.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream and consumes the current {@code Flowable} in an unbounded + * manner (i.e., without applying backpressure).</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code all} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param predicate + * a function that evaluates an item and returns a {@code Boolean} + * @return the new {@code Single} instance + * @throws NullPointerException if {@code predicate} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/all.html">ReactiveX operators documentation: All</a> + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.UNBOUNDED_IN) + @SchedulerSupport(SchedulerSupport.NONE) + public final Single<Boolean> all(@NonNull Predicate<? super T> predicate) { + Objects.requireNonNull(predicate, "predicate is null"); + return RxJavaPlugins.onAssembly(new FlowableAllSingle<>(this, predicate)); + } + + /** + * Mirrors the {@link Publisher} (current or provided) that first either emits an item or sends a termination + * notification. + * <p> + * <img width="640" height="376" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Flowable.ambWith.png" alt=""> + * <p> + * When the current {@code Flowable} signals an item or terminates first, the subscription to the other + * {@code Publisher} is canceled. If the other {@code Publisher} signals an item or terminates first, + * the subscription to the current {@code Flowable} is canceled. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator itself doesn't interfere with backpressure which is determined by the winning + * {@code Publisher}'s backpressure behavior.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code ambWith} does not operate by default on a particular {@link Scheduler}.</dd> + * <dt><b>Error handling:</b></dt> + * <dd> + * If the losing {@code Publisher} signals an error, the error is routed to the global + * error handler via {@link RxJavaPlugins#onError(Throwable)}. + * </dd> + * </dl> + * + * @param other + * a {@code Publisher} competing to react first. A subscription to this provided {@code Publisher} will occur after subscribing + * to the current {@code Flowable}. + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code other} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/amb.html">ReactiveX operators documentation: Amb</a> + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + public final Flowable<T> ambWith(@NonNull Publisher<? extends T> other) { + Objects.requireNonNull(other, "other is null"); + return ambArray(this, other); + } + + /** + * Returns a {@link Single} that emits {@code true} if any item emitted by the current {@code Flowable} satisfies a + * specified condition, otherwise {@code false}. <em>Note:</em> this always emits {@code false} if the + * current {@code Flowable} is empty. + * <p> + * <img width="640" height="320" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/exists.v3.png" alt=""> + * <p> + * In Rx.Net this is the {@code any} operator but we renamed it in RxJava to better match Java naming + * idioms. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream and consumes the current {@code Flowable} in an unbounded manner + * (i.e., no backpressure applied to it).</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code any} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param predicate + * the condition to test items emitted by the current {@code Flowable} + * @return the new {@code Single} instance + * @throws NullPointerException if {@code predicate} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/contains.html">ReactiveX operators documentation: Contains</a> + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.UNBOUNDED_IN) + @SchedulerSupport(SchedulerSupport.NONE) + public final Single<Boolean> any(@NonNull Predicate<? super T> predicate) { + Objects.requireNonNull(predicate, "predicate is null"); + return RxJavaPlugins.onAssembly(new FlowableAnySingle<>(this, predicate)); + } + + /** + * Returns the first item emitted by this {@code Flowable}, or throws + * {@link NoSuchElementException} if it emits no items. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator consumes the current {@code Flowable} in an unbounded manner + * (i.e., no backpressure applied to it).</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code blockingFirst} does not operate by default on a particular {@link Scheduler}.</dd> + * <dt><b>Error handling:</b></dt> + * <dd>If the source signals an error, the operator wraps a checked {@link Exception} + * into {@link RuntimeException} and throws that. Otherwise, {@code RuntimeException}s and + * {@link Error}s are rethrown as they are.</dd> + * </dl> + * + * @return the new {@code Flowable} instance + * @throws NoSuchElementException + * if this {@code Flowable} emits no items + * @see <a href="http://reactivex.io/documentation/operators/first.html">ReactiveX documentation: First</a> + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.UNBOUNDED_IN) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final T blockingFirst() { + BlockingFirstSubscriber<T> s = new BlockingFirstSubscriber<>(); + subscribe(s); + T v = s.blockingGet(); + if (v != null) { + return v; + } + throw new NoSuchElementException(); + } + + /** + * Returns the first item emitted by this {@code Flowable}, or a default value if it emits no + * items. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator consumes the current {@code Flowable} in an unbounded manner + * (i.e., no backpressure applied to it).</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code blockingFirst} does not operate by default on a particular {@link Scheduler}.</dd> + * <dt><b>Error handling:</b></dt> + * <dd>If the source signals an error, the operator wraps a checked {@link Exception} + * into {@link RuntimeException} and throws that. Otherwise, {@code RuntimeException}s and + * {@link Error}s are rethrown as they are.</dd> + * </dl> + * + * @param defaultItem + * a default value to return if this {@code Flowable} emits no items + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code defaultItem} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/first.html">ReactiveX documentation: First</a> + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.UNBOUNDED_IN) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final T blockingFirst(@NonNull T defaultItem) { + Objects.requireNonNull(defaultItem, "defaultItem is null"); + BlockingFirstSubscriber<T> s = new BlockingFirstSubscriber<>(); + subscribe(s); + T v = s.blockingGet(); + return v != null ? v : defaultItem; + } + + /** + * Consumes the current {@code Flowable} in a blocking fashion and invokes the given + * {@link Consumer} with each upstream item on the <em>current thread</em> until the + * upstream terminates. + * <p> + * <img width="640" height="330" src="https://github.com/ReactiveX/RxJava/wiki/images/rx-operators/B.forEach.v3.png" alt=""> + * <p> + * <em>Note:</em> the method will only return if the upstream terminates or the current + * thread is interrupted. + * <p> + * This method executes the {@code Consumer} on the current thread while + * {@link #subscribe(Consumer)} executes the consumer on the original caller thread of the + * sequence. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator requests {@link Flowable#bufferSize()} upfront, then 75% of this + * amount when 75% is received.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code blockingForEach} does not operate by default on a particular {@link Scheduler}.</dd> + * <dt><b>Error handling:</b></dt> + * <dd>If the source signals an error, the operator wraps a checked {@link Exception} + * into {@link RuntimeException} and throws that. Otherwise, {@code RuntimeException}s and + * {@link Error}s are rethrown as they are.</dd> + * </dl> + * + * @param onNext + * the {@code Consumer} to invoke for each item emitted by the {@code Flowable} + * @throws NullPointerException if {@code onNext} is {@code null} + * @throws RuntimeException + * if an error occurs; {@code Error}s and {@code RuntimeException}s are rethrown + * as they are, checked {@code Exception}s are wrapped into {@code RuntimeException}s + * @see <a href="http://reactivex.io/documentation/operators/subscribe.html">ReactiveX documentation: Subscribe</a> + * @see #subscribe(Consumer) + * @see #blockingForEach(Consumer, int) + */ + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + public final void blockingForEach(@NonNull Consumer<? super T> onNext) { + blockingForEach(onNext, bufferSize()); + } + + /** + * Consumes the current {@code Flowable} in a blocking fashion and invokes the given + * {@link Consumer} with each upstream item on the <em>current thread</em> until the + * upstream terminates. + * <p> + * <img width="640" height="330" src="https://github.com/ReactiveX/RxJava/wiki/images/rx-operators/B.forEach.v3.png" alt=""> + * <p> + * <em>Note:</em> the method will only return if the upstream terminates or the current + * thread is interrupted. + * <p> + * This method executes the {@code Consumer} on the current thread while + * {@link #subscribe(Consumer)} executes the consumer on the original caller thread of the + * sequence. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator requests the given {@code prefetch} amount upfront, then 75% of this + * amount when 75% is received.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code blockingForEach} does not operate by default on a particular {@link Scheduler}.</dd> + * <dt><b>Error handling:</b></dt> + * <dd>If the source signals an error, the operator wraps a checked {@link Exception} + * into {@link RuntimeException} and throws that. Otherwise, {@code RuntimeException}s and + * {@link Error}s are rethrown as they are.</dd> + * </dl> + * + * @param onNext + * the {@code Consumer} to invoke for each item emitted by the {@code Flowable} + * @param bufferSize + * the number of items to prefetch upfront, then 75% of it after 75% received + * @throws NullPointerException if {@code onNext} is {@code null} + * @throws IllegalArgumentException if {@code bufferSize} is non-positive + * @throws RuntimeException + * if an error occurs; {@code Error}s and {@code RuntimeException}s are rethrown + * as they are, checked {@code Exception}s are wrapped into {@code RuntimeException}s + * @see <a href="http://reactivex.io/documentation/operators/subscribe.html">ReactiveX documentation: Subscribe</a> + * @see #subscribe(Consumer) + */ + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + public final void blockingForEach(@NonNull Consumer<? super T> onNext, int bufferSize) { + Objects.requireNonNull(onNext, "onNext is null"); + Iterator<T> it = blockingIterable(bufferSize).iterator(); + while (it.hasNext()) { + try { + onNext.accept(it.next()); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + ((Disposable)it).dispose(); + throw ExceptionHelper.wrapOrThrow(e); + } + } + } + + /** + * Converts this {@code Flowable} into an {@link Iterable}. + * <p> + * <img width="640" height="315" src="https://github.com/ReactiveX/RxJava/wiki/images/rx-operators/B.toIterable.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator expects the upstream to honor backpressure otherwise the returned + * {@code Iterable}'s iterator will throw a {@link MissingBackpressureException}.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code blockingIterable} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @return the new {@code Iterable} instance + * @see <a href="http://reactivex.io/documentation/operators/to.html">ReactiveX documentation: To</a> + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Iterable<T> blockingIterable() { + return blockingIterable(bufferSize()); + } + + /** + * Converts this {@code Flowable} into an {@link Iterable}. + * <p> + * <img width="640" height="315" src="https://github.com/ReactiveX/RxJava/wiki/images/rx-operators/B.toIterable.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator expects the upstream to honor backpressure otherwise the returned + * {@code Iterable}'s iterator will throw a {@link MissingBackpressureException}. + * </dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code blockingIterable} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param bufferSize the number of items to prefetch from the current {@code Flowable} + * @return the new {@code Iterable} instance + * @throws IllegalArgumentException if {@code bufferSize} is non-positive + * @see <a href="http://reactivex.io/documentation/operators/to.html">ReactiveX documentation: To</a> + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Iterable<T> blockingIterable(int bufferSize) { + ObjectHelper.verifyPositive(bufferSize, "bufferSize"); + return new BlockingFlowableIterable<>(this, bufferSize); + } + + /** + * Returns the last item emitted by this {@code Flowable}, or throws + * {@link NoSuchElementException} if this {@code Flowable} emits no items. + * <p> + * <img width="640" height="315" src="https://github.com/ReactiveX/RxJava/wiki/images/rx-operators/B.last.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator consumes the current {@code Flowable} in an unbounded manner + * (i.e., no backpressure applied to it).</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code blockingLast} does not operate by default on a particular {@link Scheduler}.</dd> + * <dt><b>Error handling:</b></dt> + * <dd>If the source signals an error, the operator wraps a checked {@link Exception} + * into {@link RuntimeException} and throws that. Otherwise, {@code RuntimeException}s and + * {@link Error}s are rethrown as they are.</dd> + * </dl> + * + * @return the new {@code Flowable} instance + * @throws NoSuchElementException + * if this {@code Flowable} emits no items + * @see <a href="http://reactivex.io/documentation/operators/last.html">ReactiveX documentation: Last</a> + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.UNBOUNDED_IN) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final T blockingLast() { + BlockingLastSubscriber<T> s = new BlockingLastSubscriber<>(); + subscribe(s); + T v = s.blockingGet(); + if (v != null) { + return v; + } + throw new NoSuchElementException(); + } + + /** + * Returns the last item emitted by this {@code Flowable}, or a default value if it emits no + * items. + * <p> + * <img width="640" height="310" src="https://github.com/ReactiveX/RxJava/wiki/images/rx-operators/B.lastOrDefault.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator consumes the current {@code Flowable} in an unbounded manner + * (i.e., no backpressure applied to it).</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code blockingLast} does not operate by default on a particular {@link Scheduler}.</dd> + * <dt><b>Error handling:</b></dt> + * <dd>If the source signals an error, the operator wraps a checked {@link Exception} + * into {@link RuntimeException} and throws that. Otherwise, {@code RuntimeException}s and + * {@link Error}s are rethrown as they are.</dd> + * </dl> + * + * @param defaultItem + * a default value to return if this {@code Flowable} emits no items + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code defaultItem} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/last.html">ReactiveX documentation: Last</a> + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.UNBOUNDED_IN) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final T blockingLast(@NonNull T defaultItem) { + Objects.requireNonNull(defaultItem, "defaultItem is null"); + BlockingLastSubscriber<T> s = new BlockingLastSubscriber<>(); + subscribe(s); + T v = s.blockingGet(); + return v != null ? v : defaultItem; + } + + /** + * Returns an {@link Iterable} that returns the latest item emitted by this {@code Flowable}, + * waiting if necessary for one to become available. + * <p> + * If this {@code Flowable} produces items faster than {@code Iterator.next} takes them, + * {@code onNext} events might be skipped, but {@code onError} or {@code onComplete} events are not. + * <p> + * Note also that an {@code onNext} directly followed by {@code onComplete} might hide the {@code onNext} + * event. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator consumes the current {@code Flowable} in an unbounded manner + * (i.e., no backpressure applied to it).</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code blockingLatest} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @return the new {@code Iterable} instance + * @see <a href="http://reactivex.io/documentation/operators/first.html">ReactiveX documentation: First</a> + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.UNBOUNDED_IN) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Iterable<T> blockingLatest() { + return new BlockingFlowableLatest<>(this); + } + + /** + * Returns an {@link Iterable} that always returns the item most recently emitted by this + * {@code Flowable}. + * <p> + * <img width="640" height="490" src="https://github.com/ReactiveX/RxJava/wiki/images/rx-operators/B.mostRecent.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator consumes the current {@code Flowable} in an unbounded manner + * (i.e., no backpressure applied to it).</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code blockingMostRecent} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param initialItem + * the initial item that the {@code Iterable} sequence will yield if this + * {@code Flowable} has not yet emitted an item + * @return the new {@code Iterable} instance + * @throws NullPointerException if {@code initialItem} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/first.html">ReactiveX documentation: First</a> + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.UNBOUNDED_IN) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Iterable<T> blockingMostRecent(@NonNull T initialItem) { + Objects.requireNonNull(initialItem, "initialItem is null"); + return new BlockingFlowableMostRecent<>(this, initialItem); + } + + /** + * Returns an {@link Iterable} that blocks until this {@code Flowable} emits another item, then + * returns that item. + * <p> + * <img width="640" height="490" src="https://github.com/ReactiveX/RxJava/wiki/images/rx-operators/B.next.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator consumes the current {@code Flowable} in an unbounded manner + * (i.e., no backpressure applied to it).</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code blockingNext} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @return the new {@code Iterable} instance + * @see <a href="http://reactivex.io/documentation/operators/takelast.html">ReactiveX documentation: TakeLast</a> + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.UNBOUNDED_IN) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Iterable<T> blockingNext() { + return new BlockingFlowableNext<>(this); + } + + /** + * If this {@code Flowable} completes after emitting a single item, return that item, otherwise + * throw a {@link NoSuchElementException}. + * <p> + * <img width="640" height="315" src="https://github.com/ReactiveX/RxJava/wiki/images/rx-operators/B.single.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator consumes the current {@code Flowable} in an unbounded manner + * (i.e., no backpressure applied to it).</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code blockingSingle} does not operate by default on a particular {@link Scheduler}.</dd> + * <dt><b>Error handling:</b></dt> + * <dd>If the source signals an error, the operator wraps a checked {@link Exception} + * into {@link RuntimeException} and throws that. Otherwise, {@code RuntimeException}s and + * {@link Error}s are rethrown as they are.</dd> + * </dl> + * + * @return the new {@code Flowable} instance + * @see <a href="http://reactivex.io/documentation/operators/first.html">ReactiveX documentation: First</a> + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.UNBOUNDED_IN) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final T blockingSingle() { + return singleOrError().blockingGet(); + } + + /** + * If this {@code Flowable} completes after emitting a single item, return that item; if it emits + * more than one item, throw an {@link IllegalArgumentException}; if it emits no items, return a default + * value. + * <p> + * <img width="640" height="315" src="https://github.com/ReactiveX/RxJava/wiki/images/rx-operators/B.singleOrDefault.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator consumes the current {@code Flowable} in an unbounded manner + * (i.e., no backpressure applied to it).</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code blockingSingle} does not operate by default on a particular {@link Scheduler}.</dd> + * <dt><b>Error handling:</b></dt> + * <dd>If the source signals an error, the operator wraps a checked {@link Exception} + * into {@link RuntimeException} and throws that. Otherwise, {@code RuntimeException}s and + * {@link Error}s are rethrown as they are.</dd> + * </dl> + * + * @param defaultItem + * a default value to return if this {@code Flowable} emits no items + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code defaultItem} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/first.html">ReactiveX documentation: First</a> + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.UNBOUNDED_IN) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final T blockingSingle(@NonNull T defaultItem) { + return single(defaultItem).blockingGet(); + } + + /** + * Returns a {@link Future} representing the only value emitted by this {@code Flowable}. + * <p> + * <img width="640" height="311" src="https://github.com/ReactiveX/RxJava/wiki/images/rx-operators/Flowable.toFuture.png" alt=""> + * <p> + * If the {@code Flowable} emits more than one item, {@link java.util.concurrent.Future} will receive an + * {@link java.lang.IndexOutOfBoundsException}. If the {@code Flowable} is empty, {@link java.util.concurrent.Future} + * will receive a {@link java.util.NoSuchElementException}. The {@code Flowable} source has to terminate in order + * for the returned {@code Future} to terminate as well. + * <p> + * If the {@code Flowable} may emit more than one item, use {@code Flowable.toList().toFuture()}. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator consumes the current {@code Flowable} in an unbounded manner + * (i.e., no backpressure applied to it).</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code toFuture} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @return the new {@code Future} instance + * @see <a href="http://reactivex.io/documentation/operators/to.html">ReactiveX documentation: To</a> + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.UNBOUNDED_IN) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Future<T> toFuture() { + return subscribeWith(new FutureSubscriber<>()); + } + + /** + * Runs the current {@code Flowable} to a terminal event, ignoring any values and rethrowing any exception. + * <p> + * Note that calling this method will block the caller thread until the upstream terminates + * normally or with an error. Therefore, calling this method from special threads such as the + * Android Main Thread or the Swing Event Dispatch Thread is not recommended. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator consumes the current {@code Flowable} in an unbounded manner + * (i.e., no backpressure applied to it).</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code blockingSubscribe} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @since 2.0 + * @see #blockingSubscribe(Consumer) + * @see #blockingSubscribe(Consumer, Consumer) + * @see #blockingSubscribe(Consumer, Consumer, Action) + */ + @BackpressureSupport(BackpressureKind.UNBOUNDED_IN) + @SchedulerSupport(SchedulerSupport.NONE) + public final void blockingSubscribe() { + FlowableBlockingSubscribe.subscribe(this); + } + + /** + * Subscribes to the source and calls the given callbacks <strong>on the current thread</strong>. + * <p> + * If the {@code Flowable} emits an error, it is wrapped into an + * {@link io.reactivex.rxjava3.exceptions.OnErrorNotImplementedException OnErrorNotImplementedException} + * and routed to the {@link RxJavaPlugins#onError(Throwable)} handler. + * Using the overloads {@link #blockingSubscribe(Consumer, Consumer)} + * or {@link #blockingSubscribe(Consumer, Consumer, Action)} instead is recommended. + * <p> + * Note that calling this method will block the caller thread until the upstream terminates + * normally or with an error. Therefore, calling this method from special threads such as the + * Android Main Thread or the Swing Event Dispatch Thread is not recommended. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator consumes the current {@code Flowable} in an unbounded manner + * (i.e., no backpressure applied to it).</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code blockingSubscribe} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param onNext the callback action for each source value + * @throws NullPointerException if {@code onNext} is {@code null} + * @since 2.0 + * @see #blockingSubscribe(Consumer, Consumer) + * @see #blockingSubscribe(Consumer, Consumer, Action) + */ + @BackpressureSupport(BackpressureKind.UNBOUNDED_IN) + @SchedulerSupport(SchedulerSupport.NONE) + public final void blockingSubscribe(@NonNull Consumer<? super T> onNext) { + FlowableBlockingSubscribe.subscribe(this, onNext, Functions.ON_ERROR_MISSING, Functions.EMPTY_ACTION); + } + + /** + * Subscribes to the source and calls the given callbacks <strong>on the current thread</strong>. + * <p> + * If the {@code Flowable} emits an error, it is wrapped into an + * {@link io.reactivex.rxjava3.exceptions.OnErrorNotImplementedException OnErrorNotImplementedException} + * and routed to the {@link RxJavaPlugins#onError(Throwable)} handler. + * Using the overloads {@link #blockingSubscribe(Consumer, Consumer)} + * or {@link #blockingSubscribe(Consumer, Consumer, Action)} instead is recommended. + * <p> + * Note that calling this method will block the caller thread until the upstream terminates + * normally or with an error. Therefore, calling this method from special threads such as the + * Android Main Thread or the Swing Event Dispatch Thread is not recommended. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator consumes the current {@code Flowable} in an bounded manner (up to bufferSize + * outstanding request amount for items).</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code blockingSubscribe} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * <p>History: 2.1.15 - experimental + * @param onNext the callback action for each source value + * @param bufferSize the size of the buffer + * @throws NullPointerException if {@code onNext} is {@code null} + * @throws IllegalArgumentException if {@code bufferSize} is non-positive + * @see #blockingSubscribe(Consumer, Consumer) + * @see #blockingSubscribe(Consumer, Consumer, Action) + * @since 2.2 + */ + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + public final void blockingSubscribe(@NonNull Consumer<? super T> onNext, int bufferSize) { + FlowableBlockingSubscribe.subscribe(this, onNext, Functions.ON_ERROR_MISSING, Functions.EMPTY_ACTION, bufferSize); + } + + /** + * Subscribes to the source and calls the given callbacks <strong>on the current thread</strong>. + * <p> + * Note that calling this method will block the caller thread until the upstream terminates + * normally or with an error. Therefore, calling this method from special threads such as the + * Android Main Thread or the Swing Event Dispatch Thread is not recommended. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator consumes the current {@code Flowable} in an unbounded manner + * (i.e., no backpressure applied to it).</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code blockingSubscribe} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param onNext the callback action for each source value + * @param onError the callback action for an error event + * @throws NullPointerException if {@code onNext} or {@code onError} is {@code null} + * @since 2.0 + * @see #blockingSubscribe(Consumer, Consumer, Action) + */ + @BackpressureSupport(BackpressureKind.UNBOUNDED_IN) + @SchedulerSupport(SchedulerSupport.NONE) + public final void blockingSubscribe(@NonNull Consumer<? super T> onNext, @NonNull Consumer<? super Throwable> onError) { + FlowableBlockingSubscribe.subscribe(this, onNext, onError, Functions.EMPTY_ACTION); + } + + /** + * Subscribes to the source and calls the given callbacks <strong>on the current thread</strong>. + * <p> + * Note that calling this method will block the caller thread until the upstream terminates + * normally or with an error. Therefore, calling this method from special threads such as the + * Android Main Thread or the Swing Event Dispatch Thread is not recommended. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator consumes the current {@code Flowable} in an bounded manner (up to bufferSize + * outstanding request amount for items).</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code blockingSubscribe} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * <p>History: 2.1.15 - experimental + * @param onNext the callback action for each source value + * @param onError the callback action for an error event + * @param bufferSize the size of the buffer + * @throws NullPointerException if {@code onNext} or {@code onError} is {@code null} + * @throws IllegalArgumentException if {@code bufferSize} is non-positive + * @since 2.2 + * @see #blockingSubscribe(Consumer, Consumer, Action) + */ + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + public final void blockingSubscribe(@NonNull Consumer<? super T> onNext, @NonNull Consumer<? super Throwable> onError, + int bufferSize) { + FlowableBlockingSubscribe.subscribe(this, onNext, onError, Functions.EMPTY_ACTION, bufferSize); + } + + /** + * Subscribes to the source and calls the given callbacks <strong>on the current thread</strong>. + * <p> + * Note that calling this method will block the caller thread until the upstream terminates + * normally or with an error. Therefore, calling this method from special threads such as the + * Android Main Thread or the Swing Event Dispatch Thread is not recommended. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator consumes the current {@code Flowable} in an unbounded manner + * (i.e., no backpressure applied to it).</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code blockingSubscribe} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param onNext the callback action for each source value + * @param onError the callback action for an error event + * @param onComplete the callback action for the completion event. + * @throws NullPointerException if {@code onNext}, {@code onError} or {@code onComplete} is {@code null} + * @since 2.0 + */ + @BackpressureSupport(BackpressureKind.UNBOUNDED_IN) + @SchedulerSupport(SchedulerSupport.NONE) + public final void blockingSubscribe(@NonNull Consumer<? super T> onNext, @NonNull Consumer<? super Throwable> onError, @NonNull Action onComplete) { + FlowableBlockingSubscribe.subscribe(this, onNext, onError, onComplete); + } + + /** + * Subscribes to the source and calls the given callbacks <strong>on the current thread</strong>. + * <p> + * Note that calling this method will block the caller thread until the upstream terminates + * normally or with an error. Therefore, calling this method from special threads such as the + * Android Main Thread or the Swing Event Dispatch Thread is not recommended. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator consumes the current {@code Flowable} in an bounded manner (up to bufferSize + * outstanding request amount for items).</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code blockingSubscribe} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * <p>History: 2.1.15 - experimental + * @param onNext the callback action for each source value + * @param onError the callback action for an error event + * @param onComplete the callback action for the completion event. + * @param bufferSize the size of the buffer + * @throws NullPointerException if {@code onNext}, {@code onError} or {@code onComplete} is {@code null} + * @throws IllegalArgumentException if {@code bufferSize} is non-positive + * @since 2.2 + */ + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + public final void blockingSubscribe(@NonNull Consumer<? super T> onNext, @NonNull Consumer<? super Throwable> onError, @NonNull Action onComplete, + int bufferSize) { + FlowableBlockingSubscribe.subscribe(this, onNext, onError, onComplete, bufferSize); + } + + /** + * Subscribes to the source and calls the {@link Subscriber} methods <strong>on the current thread</strong>. + * <p> + * Note that calling this method will block the caller thread until the upstream terminates + * normally, with an error or the {@code Subscriber} cancels the {@link Subscription} it receives via + * {@link Subscriber#onSubscribe(Subscription)}. + * Therefore, calling this method from special threads such as the + * Android Main Thread or the Swing Event Dispatch Thread is not recommended. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The supplied {@code Subscriber} determines how backpressure is applied.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code blockingSubscribe} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * The cancellation and backpressure is composed through. + * @param subscriber the subscriber to forward events and calls to in the current thread + * @throws NullPointerException if {@code subscriber} is {@code null} + * @since 2.0 + */ + @BackpressureSupport(BackpressureKind.SPECIAL) + @SchedulerSupport(SchedulerSupport.NONE) + public final void blockingSubscribe(@NonNull Subscriber<? super T> subscriber) { + Objects.requireNonNull(subscriber, "subscriber is null"); + FlowableBlockingSubscribe.subscribe(this, subscriber); + } + + /** + * Returns a {@code Flowable} that emits buffers of items it collects from the current {@code Flowable}. The resulting + * {@code Flowable} emits connected, non-overlapping buffers, each containing {@code count} items. When the current + * {@code Flowable} completes, the resulting {@code Flowable} emits the current buffer and propagates the notification from the + * current {@code Flowable}. Note that if the current {@code Flowable} issues an {@code onError} notification the event is passed on + * immediately without first emitting the buffer it is in the process of assembling. + * <p> + * <img width="640" height="320" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/buffer3.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream and expects the current {@code Flowable} to honor it as + * well, although not enforced; violation <em>may</em> lead to {@link MissingBackpressureException} somewhere + * downstream.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>This version of {@code buffer} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param count + * the maximum number of items in each buffer before it should be emitted + * @return the new {@code Flowable} instance + * @throws IllegalArgumentException if {@code count} is non-positive + * @see <a href="http://reactivex.io/documentation/operators/buffer.html">ReactiveX operators documentation: Buffer</a> + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Flowable<List<T>> buffer(int count) { + return buffer(count, count); + } + + /** + * Returns a {@code Flowable} that emits buffers of items it collects from the current {@code Flowable}. The resulting + * {@code Flowable} emits buffers every {@code skip} items, each containing {@code count} items. When the current + * {@code Flowable} completes, the resulting {@code Flowable} emits the current buffer and propagates the notification from the + * current {@code Flowable}. Note that if the current {@code Flowable} issues an {@code onError} notification the event is passed on + * immediately without first emitting the buffer it is in the process of assembling. + * <p> + * <img width="640" height="320" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/buffer4.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream and expects the current {@code Flowable} to honor it as + * well, although not enforced; violation <em>may</em> lead to {@link MissingBackpressureException} somewhere + * downstream.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>This version of {@code buffer} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param count + * the maximum size of each buffer before it should be emitted + * @param skip + * how many items emitted by the current {@code Flowable} should be skipped before starting a new + * buffer. Note that when {@code skip} and {@code count} are equal, this is the same operation as + * {@link #buffer(int)}. + * @return the new {@code Flowable} instance + * @throws IllegalArgumentException if {@code count} or {@code skip} is non-positive + * @see <a href="http://reactivex.io/documentation/operators/buffer.html">ReactiveX operators documentation: Buffer</a> + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Flowable<List<T>> buffer(int count, int skip) { + return buffer(count, skip, ArrayListSupplier.asSupplier()); + } + + /** + * Returns a {@code Flowable} that emits buffers of items it collects from the current {@code Flowable}. The resulting + * {@code Flowable} emits buffers every {@code skip} items, each containing {@code count} items. When the current + * {@code Flowable} completes, the resulting {@code Flowable} emits the current buffer and propagates the notification from the + * current {@code Flowable}. Note that if the current {@code Flowable} issues an {@code onError} notification the event is passed on + * immediately without first emitting the buffer it is in the process of assembling. + * <p> + * <img width="640" height="320" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/buffer4.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream and expects the current {@code Flowable} to honor it as + * well, although not enforced; violation <em>may</em> lead to {@link MissingBackpressureException} somewhere + * downstream.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>This version of {@code buffer} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <U> the collection subclass type to buffer into + * @param count + * the maximum size of each buffer before it should be emitted + * @param skip + * how many items emitted by the current {@code Flowable} should be skipped before starting a new + * buffer. Note that when {@code skip} and {@code count} are equal, this is the same operation as + * {@link #buffer(int)}. + * @param bufferSupplier + * a factory function that returns an instance of the collection subclass to be used and returned + * as the buffer + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code bufferSupplier} is {@code null} + * @throws IllegalArgumentException if {@code count} or {@code skip} is non-positive + * @see <a href="http://reactivex.io/documentation/operators/buffer.html">ReactiveX operators documentation: Buffer</a> + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + public final <@NonNull U extends Collection<? super T>> Flowable<U> buffer(int count, int skip, @NonNull Supplier<U> bufferSupplier) { + ObjectHelper.verifyPositive(count, "count"); + ObjectHelper.verifyPositive(skip, "skip"); + Objects.requireNonNull(bufferSupplier, "bufferSupplier is null"); + return RxJavaPlugins.onAssembly(new FlowableBuffer<>(this, count, skip, bufferSupplier)); + } + + /** + * Returns a {@code Flowable} that emits buffers of items it collects from the current {@code Flowable}. The resulting + * {@code Flowable} emits connected, non-overlapping buffers, each containing {@code count} items. When the current + * {@code Flowable} completes, the resulting {@code Flowable} emits the current buffer and propagates the notification from the + * current {@code Flowable}. Note that if the current {@code Flowable} issues an {@code onError} notification the event is passed on + * immediately without first emitting the buffer it is in the process of assembling. + * <p> + * <img width="640" height="320" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/buffer3.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream and expects the current {@code Flowable} to honor it as + * well, although not enforced; violation <em>may</em> lead to {@link MissingBackpressureException} somewhere + * downstream.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>This version of {@code buffer} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <U> the collection subclass type to buffer into + * @param count + * the maximum number of items in each buffer before it should be emitted + * @param bufferSupplier + * a factory function that returns an instance of the collection subclass to be used and returned + * as the buffer + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code bufferSupplier} is {@code null} + * @throws IllegalArgumentException if {@code count} is non-positive + * @see <a href="http://reactivex.io/documentation/operators/buffer.html">ReactiveX operators documentation: Buffer</a> + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull U extends Collection<? super T>> Flowable<U> buffer(int count, @NonNull Supplier<U> bufferSupplier) { + return buffer(count, count, bufferSupplier); + } + + /** + * Returns a {@code Flowable} that emits buffers of items it collects from the current {@code Flowable}. The resulting + * {@code Flowable} starts a new buffer periodically, as determined by the {@code timeskip} argument. It emits + * each buffer after a fixed timespan, specified by the {@code timespan} argument. When the current + * {@code Flowable} completes, the resulting {@code Flowable} emits the current buffer and propagates the notification from the + * current {@code Flowable}. Note that if the current {@code Flowable} issues an {@code onError} notification the event is passed on + * immediately without first emitting the buffer it is in the process of assembling. + * <p> + * <img width="640" height="320" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/buffer7.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>This operator does not support backpressure as it uses time. It requests {@link Long#MAX_VALUE} + * upstream and does not obey downstream requests.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>This version of {@code buffer} operates by default on the {@code computation} {@link Scheduler}.</dd> + * </dl> + * + * @param timespan + * the period of time each buffer collects items before it is emitted + * @param timeskip + * the period of time after which a new buffer will be created + * @param unit + * the unit of time that applies to the {@code timespan} and {@code timeskip} arguments + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code unit} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/buffer.html">ReactiveX operators documentation: Buffer</a> + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.ERROR) + @SchedulerSupport(SchedulerSupport.COMPUTATION) + @NonNull + public final Flowable<List<T>> buffer(long timespan, long timeskip, @NonNull TimeUnit unit) { + return buffer(timespan, timeskip, unit, Schedulers.computation(), ArrayListSupplier.asSupplier()); + } + + /** + * Returns a {@code Flowable} that emits buffers of items it collects from the current {@code Flowable}. The resulting + * {@code Flowable} starts a new buffer periodically, as determined by the {@code timeskip} argument, and on the + * specified {@code scheduler}. It emits each buffer after a fixed timespan, specified by the + * {@code timespan} argument. When the current {@code Flowable} completes, the resulting {@code Flowable} emits the current buffer + * and propagates the notification from the current {@code Flowable}. Note that if the current {@code Flowable} issues an {@code onError} + * notification the event is passed on immediately without first emitting the buffer it is in the process of + * assembling. + * <p> + * <img width="640" height="320" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/buffer7.s.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>This operator does not support backpressure as it uses time. It requests {@link Long#MAX_VALUE} + * upstream and does not obey downstream requests.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>You specify which {@link Scheduler} this operator will use.</dd> + * </dl> + * + * @param timespan + * the period of time each buffer collects items before it is emitted + * @param timeskip + * the period of time after which a new buffer will be created + * @param unit + * the unit of time that applies to the {@code timespan} and {@code timeskip} arguments + * @param scheduler + * the {@code Scheduler} to use when determining the end and start of a buffer + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code unit} or {@code scheduler} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/buffer.html">ReactiveX operators documentation: Buffer</a> + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.ERROR) + @SchedulerSupport(SchedulerSupport.CUSTOM) + @NonNull + public final Flowable<List<T>> buffer(long timespan, long timeskip, @NonNull TimeUnit unit, @NonNull Scheduler scheduler) { + return buffer(timespan, timeskip, unit, scheduler, ArrayListSupplier.asSupplier()); + } + + /** + * Returns a {@code Flowable} that emits buffers of items it collects from the current {@code Flowable}. The resulting + * {@code Flowable} starts a new buffer periodically, as determined by the {@code timeskip} argument, and on the + * specified {@code scheduler}. It emits each buffer after a fixed timespan, specified by the + * {@code timespan} argument. When the current {@code Flowable} completes, the resulting {@code Flowable} emits the current buffer + * and propagates the notification from the current {@code Flowable}. Note that if the current {@code Flowable} issues an {@code onError} + * notification the event is passed on immediately without first emitting the buffer it is in the process of + * assembling. + * <p> + * <img width="640" height="320" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/buffer7.s.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>This operator does not support backpressure as it uses time. It requests {@link Long#MAX_VALUE} + * upstream and does not obey downstream requests.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>You specify which {@link Scheduler} this operator will use.</dd> + * </dl> + * + * @param <U> the collection subclass type to buffer into + * @param timespan + * the period of time each buffer collects items before it is emitted + * @param timeskip + * the period of time after which a new buffer will be created + * @param unit + * the unit of time that applies to the {@code timespan} and {@code timeskip} arguments + * @param scheduler + * the {@code Scheduler} to use when determining the end and start of a buffer + * @param bufferSupplier + * a factory function that returns an instance of the collection subclass to be used and returned + * as the buffer + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code unit}, {@code scheduler} or {@code bufferSupplier} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/buffer.html">ReactiveX operators documentation: Buffer</a> + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.ERROR) + @SchedulerSupport(SchedulerSupport.CUSTOM) + public final <@NonNull U extends Collection<? super T>> Flowable<U> buffer(long timespan, long timeskip, @NonNull TimeUnit unit, + @NonNull Scheduler scheduler, @NonNull Supplier<U> bufferSupplier) { + Objects.requireNonNull(unit, "unit is null"); + Objects.requireNonNull(scheduler, "scheduler is null"); + Objects.requireNonNull(bufferSupplier, "bufferSupplier is null"); + return RxJavaPlugins.onAssembly(new FlowableBufferTimed<>(this, timespan, timeskip, unit, scheduler, bufferSupplier, Integer.MAX_VALUE, false)); + } + + /** + * Returns a {@code Flowable} that emits buffers of items it collects from the current {@code Flowable}. The resulting + * {@code Flowable} emits connected, non-overlapping buffers, each of a fixed duration specified by the + * {@code timespan} argument. When the current {@code Flowable} completes, the resulting {@code Flowable} emits the current buffer + * and propagates the notification from the current {@code Flowable}. Note that if the current {@code Flowable} issues an {@code onError} + * notification the event is passed on immediately without first emitting the buffer it is in the process of + * assembling. + * <p> + * <img width="640" height="320" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/buffer5.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>This operator does not support backpressure as it uses time. It requests {@link Long#MAX_VALUE} + * upstream and does not obey downstream requests.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>This version of {@code buffer} operates by default on the {@code computation} {@link Scheduler}.</dd> + * </dl> + * + * @param timespan + * the period of time each buffer collects items before it is emitted and replaced with a new + * buffer + * @param unit + * the unit of time that applies to the {@code timespan} argument + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code unit} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/buffer.html">ReactiveX operators documentation: Buffer</a> + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.ERROR) + @SchedulerSupport(SchedulerSupport.COMPUTATION) + @NonNull + public final Flowable<List<T>> buffer(long timespan, @NonNull TimeUnit unit) { + return buffer(timespan, unit, Schedulers.computation(), Integer.MAX_VALUE); + } + + /** + * Returns a {@code Flowable} that emits buffers of items it collects from the current {@code Flowable}. The resulting + * {@code Flowable} emits connected, non-overlapping buffers, each of a fixed duration specified by the + * {@code timespan} argument or a maximum size specified by the {@code count} argument (whichever is reached + * first). When the current {@code Flowable} completes, the resulting {@code Flowable} emits the current buffer and propagates the + * notification from the current {@code Flowable}. Note that if the current {@code Flowable} issues an {@code onError} notification the event + * is passed on immediately without first emitting the buffer it is in the process of assembling. + * <p> + * <img width="640" height="320" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/buffer6.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>This operator does not support backpressure as it uses time. It requests {@link Long#MAX_VALUE} + * upstream and does not obey downstream requests.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>This version of {@code buffer} operates by default on the {@code computation} {@link Scheduler}.</dd> + * </dl> + * + * @param timespan + * the period of time each buffer collects items before it is emitted and replaced with a new + * buffer + * @param unit + * the unit of time which applies to the {@code timespan} argument + * @param count + * the maximum size of each buffer before it is emitted + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code unit} is {@code null} + * @throws IllegalArgumentException if {@code count} is non-positive + * @see <a href="http://reactivex.io/documentation/operators/buffer.html">ReactiveX operators documentation: Buffer</a> + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.ERROR) + @SchedulerSupport(SchedulerSupport.COMPUTATION) + @NonNull + public final Flowable<List<T>> buffer(long timespan, @NonNull TimeUnit unit, int count) { + return buffer(timespan, unit, Schedulers.computation(), count); + } + + /** + * Returns a {@code Flowable} that emits buffers of items it collects from the current {@code Flowable}. The resulting + * {@code Flowable} emits connected, non-overlapping buffers, each of a fixed duration specified by the + * {@code timespan} argument as measured on the specified {@code scheduler}, or a maximum size specified by + * the {@code count} argument (whichever is reached first). When the current {@code Flowable} completes, the resulting + * {@code Flowable} emits the current buffer and propagates the notification from the current {@code Flowable}. Note that if the + * current {@code Flowable} issues an {@code onError} notification the event is passed on immediately without first emitting the + * buffer it is in the process of assembling. + * <p> + * <img width="640" height="320" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/buffer6.s.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>This operator does not support backpressure as it uses time. It requests {@link Long#MAX_VALUE} + * upstream and does not obey downstream requests.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>You specify which {@link Scheduler} this operator will use.</dd> + * </dl> + * + * @param timespan + * the period of time each buffer collects items before it is emitted and replaced with a new + * buffer + * @param unit + * the unit of time which applies to the {@code timespan} argument + * @param scheduler + * the {@code Scheduler} to use when determining the end and start of a buffer + * @param count + * the maximum size of each buffer before it is emitted + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code unit} or {@code scheduler} is {@code null} + * @throws IllegalArgumentException if {@code count} is non-positive + * @see <a href="http://reactivex.io/documentation/operators/buffer.html">ReactiveX operators documentation: Buffer</a> + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.ERROR) + @SchedulerSupport(SchedulerSupport.CUSTOM) + @NonNull + public final Flowable<List<T>> buffer(long timespan, @NonNull TimeUnit unit, @NonNull Scheduler scheduler, int count) { + return buffer(timespan, unit, scheduler, count, ArrayListSupplier.asSupplier(), false); + } + + /** + * Returns a {@code Flowable} that emits buffers of items it collects from the current {@code Flowable}. The resulting + * {@code Flowable} emits connected, non-overlapping buffers, each of a fixed duration specified by the + * {@code timespan} argument as measured on the specified {@code scheduler}, or a maximum size specified by + * the {@code count} argument (whichever is reached first). When the current {@code Flowable} completes, the resulting + * {@code Flowable} emits the current buffer and propagates the notification from the current {@code Flowable}. Note that if the + * current {@code Flowable} issues an {@code onError} notification the event is passed on immediately without first emitting the + * buffer it is in the process of assembling. + * <p> + * <img width="640" height="320" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/buffer6.s.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>This operator does not support backpressure as it uses time. It requests {@link Long#MAX_VALUE} + * upstream and does not obey downstream requests.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>You specify which {@link Scheduler} this operator will use.</dd> + * </dl> + * + * @param <U> the collection subclass type to buffer into + * @param timespan + * the period of time each buffer collects items before it is emitted and replaced with a new + * buffer + * @param unit + * the unit of time which applies to the {@code timespan} argument + * @param scheduler + * the {@code Scheduler} to use when determining the end and start of a buffer + * @param count + * the maximum size of each buffer before it is emitted + * @param bufferSupplier + * a factory function that returns an instance of the collection subclass to be used and returned + * as the buffer + * @param restartTimerOnMaxSize if {@code true}, the time window is restarted when the max capacity of the current buffer + * is reached + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code unit}, {@code scheduler} or {@code bufferSupplier} is {@code null} + * @throws IllegalArgumentException if {@code count} is non-positive + * @see <a href="http://reactivex.io/documentation/operators/buffer.html">ReactiveX operators documentation: Buffer</a> + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.ERROR) + @SchedulerSupport(SchedulerSupport.CUSTOM) + @NonNull + public final <@NonNull U extends Collection<? super T>> Flowable<U> buffer( + long timespan, @NonNull TimeUnit unit, + @NonNull Scheduler scheduler, int count, + @NonNull Supplier<U> bufferSupplier, + boolean restartTimerOnMaxSize) { + Objects.requireNonNull(unit, "unit is null"); + Objects.requireNonNull(scheduler, "scheduler is null"); + Objects.requireNonNull(bufferSupplier, "bufferSupplier is null"); + ObjectHelper.verifyPositive(count, "count"); + return RxJavaPlugins.onAssembly(new FlowableBufferTimed<>(this, timespan, timespan, unit, scheduler, bufferSupplier, count, restartTimerOnMaxSize)); + } + + /** + * Returns a {@code Flowable} that emits buffers of items it collects from the current {@code Flowable}. The resulting + * {@code Flowable} emits connected, non-overlapping buffers, each of a fixed duration specified by the + * {@code timespan} argument and on the specified {@code scheduler}. When the current {@code Flowable} completes, the + * resulting {@code Flowable} emits the current buffer and propagates the notification from the current {@code Flowable}. Note that + * if the current {@code Flowable} issues an {@code onError} notification the event is passed on immediately without first emitting + * the buffer it is in the process of assembling. + * <p> + * <img width="640" height="320" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/buffer5.s.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>This operator does not support backpressure as it uses time. It requests {@link Long#MAX_VALUE} + * upstream and does not obey downstream requests.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>You specify which {@link Scheduler} this operator will use.</dd> + * </dl> + * + * @param timespan + * the period of time each buffer collects items before it is emitted and replaced with a new + * buffer + * @param unit + * the unit of time which applies to the {@code timespan} argument + * @param scheduler + * the {@code Scheduler} to use when determining the end and start of a buffer + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code unit} or {@code scheduler} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/buffer.html">ReactiveX operators documentation: Buffer</a> + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.ERROR) + @SchedulerSupport(SchedulerSupport.CUSTOM) + @NonNull + public final Flowable<List<T>> buffer(long timespan, @NonNull TimeUnit unit, @NonNull Scheduler scheduler) { + return buffer(timespan, unit, scheduler, Integer.MAX_VALUE, ArrayListSupplier.asSupplier(), false); + } + + /** + * Returns a {@code Flowable} that emits buffers of items it collects from the current {@code Flowable}. The resulting + * {@code Flowable} emits buffers that it creates when the specified {@code openingIndicator} {@link Publisher} emits an + * item, and closes when the {@code Publisher} returned from {@code closingIndicator} emits an item. If any of the current + * {@code PFlowable}, {@code openingIndicator} or {@code closingIndicator} issues an {@code onError} notification the event is passed + * on immediately without first emitting the buffer it is in the process of assembling. + * <p> + * <img width="640" height="470" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/buffer2.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>This operator does not support backpressure as it is instead controlled by the given {@code Publisher}s and + * buffers data. It requests {@link Long#MAX_VALUE} upstream and does not obey downstream requests.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>This version of {@code buffer} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <TOpening> the element type of the buffer-opening {@code Publisher} + * @param <TClosing> the element type of the individual buffer-closing {@code Publisher}s + * @param openingIndicator + * the {@code Publisher} that, when it emits an item, causes a new buffer to be created + * @param closingIndicator + * the {@link Function} that is used to produce a {@code Publisher} for every buffer created. When this + * {@code Publisher} emits an item, the associated buffer is emitted. + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code openingIndicator} or {@code closingIndicator} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/buffer.html">ReactiveX operators documentation: Buffer</a> + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.ERROR) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull TOpening, @NonNull TClosing> Flowable<List<T>> buffer( + @NonNull Publisher<? extends TOpening> openingIndicator, + @NonNull Function<? super TOpening, @NonNull ? extends Publisher<? extends TClosing>> closingIndicator) { + return buffer(openingIndicator, closingIndicator, ArrayListSupplier.asSupplier()); + } + + /** + * Returns a {@code Flowable} that emits buffers of items it collects from the current {@code Flowable}. The resulting + * {@code Flowable} emits buffers that it creates when the specified {@code openingIndicator} {@link Publisher} emits an + * item, and closes when the {@code Publisher} returned from {@code closingIndicator} emits an item. If any of the current + * {@code Flowable}, {@code openingIndicator} or {@code closingIndicator} issues an {@code onError} notification the event is passed + * on immediately without first emitting the buffer it is in the process of assembling. + * <p> + * <img width="640" height="470" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/buffer2.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>This operator does not support backpressure as it is instead controlled by the given {@code Publisher}s and + * buffers data. It requests {@link Long#MAX_VALUE} upstream and does not obey downstream requests.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>This version of {@code buffer} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <U> the collection subclass type to buffer into + * @param <TOpening> the element type of the buffer-opening {@code Publisher} + * @param <TClosing> the element type of the individual buffer-closing {@code Publisher}s + * @param openingIndicator + * the {@code Publisher} that, when it emits an item, causes a new buffer to be created + * @param closingIndicator + * the {@link Function} that is used to produce a {@code Publisher} for every buffer created. When this + * {@code Publisher} emits an item, the associated buffer is emitted. + * @param bufferSupplier + * a factory function that returns an instance of the collection subclass to be used and returned + * as the buffer + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code openingIndicator}, {@code closingIndicator} or {@code bufferSupplier} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/buffer.html">ReactiveX operators documentation: Buffer</a> + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.ERROR) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull TOpening, @NonNull TClosing, @NonNull U extends Collection<? super T>> Flowable<U> buffer( + @NonNull Publisher<? extends TOpening> openingIndicator, + @NonNull Function<? super TOpening, @NonNull ? extends Publisher<? extends TClosing>> closingIndicator, + @NonNull Supplier<U> bufferSupplier) { + Objects.requireNonNull(openingIndicator, "openingIndicator is null"); + Objects.requireNonNull(closingIndicator, "closingIndicator is null"); + Objects.requireNonNull(bufferSupplier, "bufferSupplier is null"); + return RxJavaPlugins.onAssembly(new FlowableBufferBoundary<T, U, TOpening, TClosing>(this, openingIndicator, closingIndicator, bufferSupplier)); + } + + /** + * Returns a {@code Flowable} that emits non-overlapping buffered items from the current {@code Flowable} each time the + * specified boundary {@link Publisher} emits an item. + * <p> + * <img width="640" height="395" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/buffer8.v3.png" alt=""> + * <p> + * Completion of either the source or the boundary {@code Publisher} causes the returned {@code Publisher} to emit the + * latest buffer and complete. If either the current {@code Flowable} or the boundary {@code Publisher} issues an {@code onError} notification + * the event is passed on immediately without first emitting the buffer it is in the process of assembling. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>This operator does not support backpressure as it is instead controlled by the {@code Publisher} + * {@code boundary} and buffers data. It requests {@link Long#MAX_VALUE} upstream and does not obey + * downstream requests.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>This version of {@code buffer} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <B> + * the boundary value type (ignored) + * @param boundaryIndicator + * the boundary {@code Publisher} + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code boundaryIndicator} is {@code null} + * @see #buffer(Publisher, int) + * @see <a href="http://reactivex.io/documentation/operators/buffer.html">ReactiveX operators documentation: Buffer</a> + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.ERROR) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull B> Flowable<List<T>> buffer(@NonNull Publisher<B> boundaryIndicator) { + return buffer(boundaryIndicator, ArrayListSupplier.asSupplier()); + } + + /** + * Returns a {@code Flowable} that emits non-overlapping buffered items from the current {@code Flowable} each time the + * specified boundary {@link Publisher} emits an item. + * <p> + * <img width="640" height="395" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/buffer8.v3.png" alt=""> + * <p> + * Completion of either the source or the boundary {@code Publisher} causes the returned {@code Publisher} to emit the + * latest buffer and complete. If either the current {@code Flowable} or the boundary {@code Publisher} issues an {@code onError} notification + * the event is passed on immediately without first emitting the buffer it is in the process of assembling. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>This operator does not support backpressure as it is instead controlled by the {@code Publisher} + * {@code boundary} and buffers data. It requests {@link Long#MAX_VALUE} upstream and does not obey + * downstream requests.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>This version of {@code buffer} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <B> + * the boundary value type (ignored) + * @param boundaryIndicator + * the boundary {@code Publisher} + * @param initialCapacity + * the initial capacity of each buffer chunk + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code boundaryIndicator} is {@code null} + * @throws IllegalArgumentException if {@code initialCapacity} is non-positive + * @see <a href="http://reactivex.io/documentation/operators/buffer.html">ReactiveX operators documentation: Buffer</a> + * @see #buffer(Publisher) + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.ERROR) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull B> Flowable<List<T>> buffer(@NonNull Publisher<B> boundaryIndicator, int initialCapacity) { + ObjectHelper.verifyPositive(initialCapacity, "initialCapacity"); + return buffer(boundaryIndicator, Functions.createArrayList(initialCapacity)); + } + + /** + * Returns a {@code Flowable} that emits non-overlapping buffered items from the current {@code Flowable} each time the + * specified boundary {@link Publisher} emits an item. + * <p> + * <img width="640" height="395" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/buffer8.v3.png" alt=""> + * <p> + * Completion of either the source or the boundary {@code Publisher} causes the returned {@code Publisher} to emit the + * latest buffer and complete. If either the current {@code Flowable} or the boundary {@code Publisher} issues an {@code onError} notification + * the event is passed on immediately without first emitting the buffer it is in the process of assembling. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>This operator does not support backpressure as it is instead controlled by the {@code Publisher} + * {@code boundary} and buffers data. It requests {@link Long#MAX_VALUE} upstream and does not obey + * downstream requests.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>This version of {@code buffer} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <U> the collection subclass type to buffer into + * @param <B> + * the boundary value type (ignored) + * @param boundaryIndicator + * the boundary {@code Publisher} + * @param bufferSupplier + * a factory function that returns an instance of the collection subclass to be used and returned + * as the buffer + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code boundaryIndicator} or {@code bufferSupplier} is {@code null} + * @see #buffer(Publisher, int) + * @see <a href="http://reactivex.io/documentation/operators/buffer.html">ReactiveX operators documentation: Buffer</a> + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.ERROR) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull B, @NonNull U extends Collection<? super T>> Flowable<U> buffer(@NonNull Publisher<B> boundaryIndicator, @NonNull Supplier<U> bufferSupplier) { + Objects.requireNonNull(boundaryIndicator, "boundaryIndicator is null"); + Objects.requireNonNull(bufferSupplier, "bufferSupplier is null"); + return RxJavaPlugins.onAssembly(new FlowableBufferExactBoundary<>(this, boundaryIndicator, bufferSupplier)); + } + + /** + * Returns a {@code Flowable} that subscribes to this {@link Publisher} lazily, caches all of its events + * and replays them, in the same order as received, to all the downstream subscribers. + * <p> + * <img width="640" height="410" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/cache.v3.png" alt=""> + * <p> + * This is useful when you want a {@code Publisher} to cache responses and you can't control the + * subscribe/cancel behavior of all the {@link Subscriber}s. + * <p> + * The operator subscribes only when the first downstream subscriber subscribes and maintains + * a single subscription towards this {@code Publisher}. In contrast, the operator family of {@link #replay()} + * that return a {@link ConnectableFlowable} require an explicit call to {@link ConnectableFlowable#connect()}. + * <p> + * <em>Note:</em> You sacrifice the ability to cancel the origin when you use the {@code cache} + * operator so be careful not to use this operator on {@code Publisher}s that emit an infinite or very large number + * of items that will use up memory. + * A possible workaround is to apply {@link #takeUntil(Publisher)} with a predicate or + * another source before (and perhaps after) the application of {@code cache()}. + * <pre><code> + * AtomicBoolean shouldStop = new AtomicBoolean(); + * + * source.takeUntil(v -> shouldStop.get()) + * .cache() + * .takeUntil(v -> shouldStop.get()) + * .subscribe(...); + * </code></pre> + * Since the operator doesn't allow clearing the cached values either, the possible workaround is + * to forget all references to it via {@link #onTerminateDetach()} applied along with the previous + * workaround: + * <pre><code> + * AtomicBoolean shouldStop = new AtomicBoolean(); + * + * source.takeUntil(v -> shouldStop.get()) + * .onTerminateDetach() + * .cache() + * .takeUntil(v -> shouldStop.get()) + * .onTerminateDetach() + * .subscribe(...); + * </code></pre> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator consumes this {@code Publisher} in an unbounded fashion but respects the backpressure + * of each downstream {@code Subscriber} individually.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code cache} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @return the new {@code Flowable} instance + * @see <a href="http://reactivex.io/documentation/operators/replay.html">ReactiveX operators documentation: Replay</a> + * @see #takeUntil(Predicate) + * @see #takeUntil(Publisher) + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Flowable<T> cache() { + return cacheWithInitialCapacity(16); + } + + /** + * Returns a {@code Flowable} that subscribes to this {@link Publisher} lazily, caches all of its events + * and replays them, in the same order as received, to all the downstream subscribers. + * <p> + * <img width="640" height="410" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/cache.v3.png" alt=""> + * <p> + * This is useful when you want a {@code Publisher} to cache responses and you can't control the + * subscribe/cancel behavior of all the {@link Subscriber}s. + * <p> + * The operator subscribes only when the first downstream subscriber subscribes and maintains + * a single subscription towards this {@code Publisher}. In contrast, the operator family of {@link #replay()} + * that return a {@link ConnectableFlowable} require an explicit call to {@link ConnectableFlowable#connect()}. + * <p> + * <em>Note:</em> You sacrifice the ability to cancel the origin when you use the {@code cache} + * operator so be careful not to use this operator on {@code Publisher}s that emit an infinite or very large number + * of items that will use up memory. + * A possible workaround is to apply {@link #takeUntil(Publisher)} with a predicate or + * another source before (and perhaps after) the application of {@code cacheWithInitialCapacity()}. + * <pre><code> + * AtomicBoolean shouldStop = new AtomicBoolean(); + * + * source.takeUntil(v -> shouldStop.get()) + * .cache() + * .takeUntil(v -> shouldStop.get()) + * .subscribe(...); + * </code></pre> + * Since the operator doesn't allow clearing the cached values either, the possible workaround is + * to forget all references to it via {@link #onTerminateDetach()} applied along with the previous + * workaround: + * <pre><code> + * AtomicBoolean shouldStop = new AtomicBoolean(); + * + * source.takeUntil(v -> shouldStop.get()) + * .onTerminateDetach() + * .cache() + * .takeUntil(v -> shouldStop.get()) + * .onTerminateDetach() + * .subscribe(...); + * </code></pre> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator consumes this {@code Publisher} in an unbounded fashion but respects the backpressure + * of each downstream {@code Subscriber} individually.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code cacheWithInitialCapacity} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * <p> + * <em>Note:</em> The capacity hint is not an upper bound on cache size. For that, consider + * {@link #replay(int)} in combination with {@link ConnectableFlowable#autoConnect()} or similar. + * + * @param initialCapacity hint for number of items to cache (for optimizing underlying data structure) + * @return the new {@code Flowable} instance + * @throws IllegalArgumentException if {@code initialCapacity} is non-positive + * @see <a href="http://reactivex.io/documentation/operators/replay.html">ReactiveX operators documentation: Replay</a> + * @see #takeUntil(Predicate) + * @see #takeUntil(Publisher) + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Flowable<T> cacheWithInitialCapacity(int initialCapacity) { + ObjectHelper.verifyPositive(initialCapacity, "initialCapacity"); + return RxJavaPlugins.onAssembly(new FlowableCache<>(this, initialCapacity)); + } + + /** + * Returns a {@code Flowable} that emits the upstream items while + * they can be cast via {@link Class#cast(Object)} until the upstream terminates, + * or until the upstream signals an item which can't be cast, + * resulting in a {@link ClassCastException} to be signaled to the downstream. + * <p> + * <img width="640" height="305" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/cast.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator doesn't interfere with backpressure which is determined by the current {@code Flowable}'s + * backpressure behavior.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code cast} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <U> the output value type cast to + * @param clazz + * the target class to use to try and cast the upstream items into + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code clazz} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/map.html">ReactiveX operators documentation: Map</a> + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.PASS_THROUGH) + @SchedulerSupport(SchedulerSupport.NONE) + public final <@NonNull U> Flowable<U> cast(@NonNull Class<U> clazz) { + Objects.requireNonNull(clazz, "clazz is null"); + return map(Functions.castFunction(clazz)); + } + + /** + * Collects items emitted by the finite source {@link Publisher} into a single mutable data structure and returns + * a {@link Single} that emits this structure. + * <p> + * <img width="640" height="330" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/collect.v3.png" alt=""> + * <p> + * This is a simplified version of {@code reduce} that does not need to return the state on each pass. + * <p> + * Note that this operator requires the upstream to signal {@code onComplete} for the accumulator object to + * be emitted. Sources that are infinite and never complete will never emit anything through this + * operator and an infinite source may lead to a fatal {@link OutOfMemoryError}. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>This operator does not support backpressure because by intent it will receive all values and reduce + * them to a single {@code onNext}.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code collect} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <U> the accumulator and output type + * @param initialItemSupplier + * the mutable data structure that will collect the items + * @param collector + * a function that accepts the {@code state} and an emitted item, and modifies {@code state} + * accordingly + * @return the new {@code Single} instance + * @throws NullPointerException if {@code initialItemSupplier} or {@code collector} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/reduce.html">ReactiveX operators documentation: Reduce</a> + * @see #collect(Collector) + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.UNBOUNDED_IN) + @SchedulerSupport(SchedulerSupport.NONE) + public final <@NonNull U> Single<U> collect(@NonNull Supplier<? extends U> initialItemSupplier, @NonNull BiConsumer<? super U, ? super T> collector) { + Objects.requireNonNull(initialItemSupplier, "initialItemSupplier is null"); + Objects.requireNonNull(collector, "collector is null"); + return RxJavaPlugins.onAssembly(new FlowableCollectSingle<>(this, initialItemSupplier, collector)); + } + + /** + * Collects items emitted by the finite source {@link Publisher} into a single mutable data structure and returns + * a {@link Single} that emits this structure. + * <p> + * <img width="640" height="330" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/collect.v3.png" alt=""> + * <p> + * This is a simplified version of {@code reduce} that does not need to return the state on each pass. + * <p> + * Note that this operator requires the upstream to signal {@code onComplete} for the accumulator object to + * be emitted. Sources that are infinite and never complete will never emit anything through this + * operator and an infinite source may lead to a fatal {@link OutOfMemoryError}. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>This operator does not support backpressure because by intent it will receive all values and reduce + * them to a single {@code onNext}.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code collectInto} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <U> the accumulator and output type + * @param initialItem + * the mutable data structure that will collect the items + * @param collector + * a function that accepts the {@code state} and an emitted item, and modifies {@code state} + * accordingly + * @return the new {@code Single} instance + * @throws NullPointerException if {@code initialItem} or {@code collector} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/reduce.html">ReactiveX operators documentation: Reduce</a> + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.UNBOUNDED_IN) + @SchedulerSupport(SchedulerSupport.NONE) + public final <@NonNull U> Single<U> collectInto(U initialItem, @NonNull BiConsumer<? super U, ? super T> collector) { + Objects.requireNonNull(initialItem, "initialItem is null"); + return collect(Functions.justSupplier(initialItem), collector); + } + + /** + * Transform the current {@code Flowable} by applying a particular {@link FlowableTransformer} function to it. + * <p> + * This method operates on the {@code Flowable} itself whereas {@link #lift} operates on the {@code Flowable}'s + * {@link Subscriber}s. + * <p> + * If the operator you are creating is designed to act on the individual items emitted by a current + * {@code Flowable}, use {@link #lift}. If your operator is designed to transform the current {@code Flowable} as a whole + * (for instance, by applying a particular set of existing RxJava operators to it) use {@code compose}. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator itself doesn't interfere with the backpressure behavior which only depends + * on what kind of {@link Publisher} the {@code FlowableTransformer} returns.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code compose} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <R> the value type of the output {@code Publisher} + * @param composer implements the function that transforms the current {@code Flowable} + * @return the new composed {@code Flowable} instance + * @throws NullPointerException if {@code composer} is {@code null} + * @see <a href="https://github.com/ReactiveX/RxJava/wiki/Implementing-Your-Own-Operators">RxJava wiki: Implementing Your Own Operators</a> + */ + @SuppressWarnings("unchecked") + @CheckReturnValue + @BackpressureSupport(BackpressureKind.PASS_THROUGH) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull R> Flowable<R> compose(@NonNull FlowableTransformer<? super T, ? extends R> composer) { + return fromPublisher(((FlowableTransformer<T, R>) Objects.requireNonNull(composer, "composer is null")).apply(this)); + } + + /** + * Returns a new {@code Flowable} that emits items resulting from applying a function that you supply to each item + * emitted by the current {@code Flowable}, where that function returns a {@link Publisher}, and then emitting the items + * that result from concatenating those returned {@code Publisher}s. + * <p> + * <img width="640" height="305" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/concatMap.v3.png" alt=""> + * <p> + * Note that there is no guarantee where the given {@code mapper} function will be executed; it could be on the subscribing thread, + * on the upstream thread signaling the new item to be mapped or on the thread where the inner source terminates. To ensure + * the {@code mapper} function is confined to a known thread, use the {@link #concatMap(Function, int, Scheduler)} overload. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream. Both this and the inner {@code Publisher}s are + * expected to honor backpressure as well. If the current {@code Flowable} violates the rule, the operator will + * signal a {@link MissingBackpressureException}. If any of the inner {@code Publisher}s doesn't honor + * backpressure, that <em>may</em> throw an {@link IllegalStateException} when that + * {@code Publisher} completes.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code concatMap} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <R> the type of the inner {@code Publisher} sources and thus the output type + * @param mapper + * a function that, when applied to an item emitted by the current {@code Flowable}, returns a + * {@code Publisher} + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code mapper} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/flatmap.html">ReactiveX operators documentation: FlatMap</a> + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull R> Flowable<R> concatMap(@NonNull Function<? super T, @NonNull ? extends Publisher<? extends R>> mapper) { + return concatMap(mapper, 2); + } + + /** + * Returns a new {@code Flowable} that emits items resulting from applying a function that you supply to each item + * emitted by the current {@code Flowable}, where that function returns a {@link Publisher}, and then emitting the items + * that result from concatenating those returned {@code Publisher}s. + * <p> + * <img width="640" height="305" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/concatMap.v3.png" alt=""> + * <p> + * Note that there is no guarantee where the given {@code mapper} function will be executed; it could be on the subscribing thread, + * on the upstream thread signaling the new item to be mapped or on the thread where the inner source terminates. To ensure + * the {@code mapper} function is confined to a known thread, use the {@link #concatMap(Function, int, Scheduler)} overload. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream. Both this and the inner {@code Publisher}s are + * expected to honor backpressure as well. If the current {@code Flowable} violates the rule, the operator will + * signal a {@link MissingBackpressureException}. If any of the inner {@code Publisher}s doesn't honor + * backpressure, that <em>may</em> throw an {@link IllegalStateException} when that + * {@code Publisher} completes.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code concatMap} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <R> the type of the inner {@code Publisher} sources and thus the output type + * @param mapper + * a function that, when applied to an item emitted by the current {@code Flowable}, returns a + * {@code Publisher} + * @param prefetch + * the number of elements to prefetch from the current {@code Flowable} + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code mapper} is {@code null} + * @throws IllegalArgumentException if {@code prefetch} is non-positive + * @see <a href="http://reactivex.io/documentation/operators/flatmap.html">ReactiveX operators documentation: FlatMap</a> + * @see #concatMap(Function, int, Scheduler) + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + public final <@NonNull R> Flowable<R> concatMap(@NonNull Function<? super T, @NonNull ? extends Publisher<? extends R>> mapper, int prefetch) { + Objects.requireNonNull(mapper, "mapper is null"); + ObjectHelper.verifyPositive(prefetch, "prefetch"); + if (this instanceof ScalarSupplier) { + @SuppressWarnings("unchecked") + T v = ((ScalarSupplier<T>)this).get(); + if (v == null) { + return empty(); + } + return FlowableScalarXMap.scalarXMap(v, mapper); + } + return RxJavaPlugins.onAssembly(new FlowableConcatMap<>(this, mapper, prefetch, ErrorMode.IMMEDIATE)); + } + + /** + * Returns a new {@code Flowable} that emits items resulting from applying a function (on a designated scheduler) + * that you supply to each item emitted by the current {@code Flowable}, where that function returns a {@link Publisher}, and then emitting the items + * that result from concatenating those returned {@code Publisher}s. + * <p> + * <img width="640" height="305" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/concatMap.v3.png" alt=""> + * <p> + * The difference between {@link #concatMap(Function, int)} and this operator is that this operator guarantees the {@code mapper} + * function is executed on the specified scheduler. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream. Both this and the inner {@code Publisher}s are + * expected to honor backpressure as well. If the current {@code Flowable} violates the rule, the operator will + * signal a {@link MissingBackpressureException}. If any of the inner {@code Publisher}s doesn't honor + * backpressure, that <em>may</em> throw an {@link IllegalStateException} when that + * {@code Publisher} completes.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code concatMap} executes the given {@code mapper} function on the provided {@link Scheduler}.</dd> + * </dl> + * + * @param <R> the type of the inner {@code Publisher} sources and thus the output type + * @param mapper + * a function that, when applied to an item emitted by the current {@code Flowable}, returns a + * {@code Publisher} + * @param prefetch + * the number of elements to prefetch from the current {@code Flowable} + * @param scheduler + * the scheduler where the {@code mapper} function will be executed + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code mapper} or {@code scheduler} is {@code null} + * @throws IllegalArgumentException if {@code prefetch} is non-positive + * @see <a href="http://reactivex.io/documentation/operators/flatmap.html">ReactiveX operators documentation: FlatMap</a> + * @since 3.0.0 + * @see #concatMap(Function, int) + * @see #concatMapDelayError(Function, boolean, int, Scheduler) + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.CUSTOM) + public final <@NonNull R> Flowable<R> concatMap(@NonNull Function<? super T, @NonNull ? extends Publisher<? extends R>> mapper, int prefetch, @NonNull Scheduler scheduler) { + Objects.requireNonNull(mapper, "mapper is null"); + ObjectHelper.verifyPositive(prefetch, "prefetch"); + Objects.requireNonNull(scheduler, "scheduler is null"); + return RxJavaPlugins.onAssembly(new FlowableConcatMapScheduler<>(this, mapper, prefetch, ErrorMode.IMMEDIATE, scheduler)); + } + + /** + * Maps the upstream items into {@link CompletableSource}s and subscribes to them one after the + * other completes. + * <p> + * <img width="640" height="305" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/concatMap.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator expects the upstream to support backpressure. If this {@code Flowable} violates the rule, the operator will + * signal a {@link MissingBackpressureException}.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code concatMapCompletable} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * <p>History: 2.1.11 - experimental + * @param mapper the function called with the upstream item and should return + * a {@code CompletableSource} to become the next source to + * be subscribed to + * @return the new {@link Completable} instance + * @throws NullPointerException if {@code mapper} is {@code null} + * @see #concatMapCompletableDelayError(Function) + * @since 2.2 + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @BackpressureSupport(BackpressureKind.FULL) + @NonNull + public final Completable concatMapCompletable(@NonNull Function<? super T, ? extends CompletableSource> mapper) { + return concatMapCompletable(mapper, 2); + } + + /** + * Maps the upstream items into {@link CompletableSource}s and subscribes to them one after the + * other completes. + * <p> + * <img width="640" height="305" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/concatMap.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator expects the upstream to support backpressure. If this {@code Flowable} violates the rule, the operator will + * signal a {@link MissingBackpressureException}.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code concatMapCompletable} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * <p>History: 2.1.11 - experimental + * @param mapper the function called with the upstream item and should return + * a {@code CompletableSource} to become the next source to + * be subscribed to + * @param prefetch The number of upstream items to prefetch so that fresh items are + * ready to be mapped when a previous {@code CompletableSource} terminates. + * The operator replenishes after half of the prefetch amount has been consumed + * and turned into {@code CompletableSource}s. + * @return the new {@link Completable} instance + * @throws NullPointerException if {@code mapper} is {@code null} + * @throws IllegalArgumentException if {@code prefetch} is non-positive + * @see #concatMapCompletableDelayError(Function, boolean, int) + * @since 2.2 + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + @BackpressureSupport(BackpressureKind.FULL) + public final Completable concatMapCompletable(@NonNull Function<? super T, ? extends CompletableSource> mapper, int prefetch) { + Objects.requireNonNull(mapper, "mapper is null"); + ObjectHelper.verifyPositive(prefetch, "prefetch"); + return RxJavaPlugins.onAssembly(new FlowableConcatMapCompletable<>(this, mapper, ErrorMode.IMMEDIATE, prefetch)); + } + + /** + * Maps the upstream items into {@link CompletableSource}s and subscribes to them one after the + * other terminates, delaying all errors till both this {@code Flowable} and all + * inner {@code CompletableSource}s terminate. + * <p> + * <img width="640" height="305" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/concatMap.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator expects the upstream to support backpressure. If this {@code Flowable} violates the rule, the operator will + * signal a {@link MissingBackpressureException}.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code concatMapCompletableDelayError} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * <p>History: 2.1.11 - experimental + * @param mapper the function called with the upstream item and should return + * a {@code CompletableSource} to become the next source to + * be subscribed to + * @return the new {@link Completable} instance + * @throws NullPointerException if {@code mapper} is {@code null} + * @see #concatMapCompletable(Function, int) + * @since 2.2 + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @BackpressureSupport(BackpressureKind.FULL) + @NonNull + public final Completable concatMapCompletableDelayError(@NonNull Function<? super T, ? extends CompletableSource> mapper) { + return concatMapCompletableDelayError(mapper, true, 2); + } + + /** + * Maps the upstream items into {@link CompletableSource}s and subscribes to them one after the + * other terminates, optionally delaying all errors till both this {@code Flowable} and all + * inner {@code CompletableSource}s terminate. + * <p> + * <img width="640" height="305" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/concatMap.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator expects the upstream to support backpressure. If this {@code Flowable} violates the rule, the operator will + * signal a {@link MissingBackpressureException}.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code concatMapCompletableDelayError} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * <p>History: 2.1.11 - experimental + * @param mapper the function called with the upstream item and should return + * a {@code CompletableSource} to become the next source to + * be subscribed to + * @param tillTheEnd If {@code true}, errors from this {@code Flowable} or any of the + * inner {@code CompletableSource}s are delayed until all + * of them terminate. If {@code false}, an error from this + * {@code Flowable} is delayed until the current inner + * {@code CompletableSource} terminates and only then is + * it emitted to the downstream. + * @return the new {@link Completable} instance + * @throws NullPointerException if {@code mapper} is {@code null} + * @see #concatMapCompletable(Function) + * @since 2.2 + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @BackpressureSupport(BackpressureKind.FULL) + @NonNull + public final Completable concatMapCompletableDelayError(@NonNull Function<? super T, ? extends CompletableSource> mapper, boolean tillTheEnd) { + return concatMapCompletableDelayError(mapper, tillTheEnd, 2); + } + + /** + * Maps the upstream items into {@link CompletableSource}s and subscribes to them one after the + * other terminates, optionally delaying all errors till both this {@code Flowable} and all + * inner {@code CompletableSource}s terminate. + * <p> + * <img width="640" height="305" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/concatMap.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator expects the upstream to support backpressure. If this {@code Flowable} violates the rule, the operator will + * signal a {@link MissingBackpressureException}.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code concatMapCompletableDelayError} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * <p>History: 2.1.11 - experimental + * @param mapper the function called with the upstream item and should return + * a {@code CompletableSource} to become the next source to + * be subscribed to + * @param tillTheEnd If {@code true}, errors from this {@code Flowable} or any of the + * inner {@code CompletableSource}s are delayed until all + * of them terminate. If {@code false}, an error from this + * {@code Flowable} is delayed until the current inner + * {@code CompletableSource} terminates and only then is + * it emitted to the downstream. + * @param prefetch The number of upstream items to prefetch so that fresh items are + * ready to be mapped when a previous {@code CompletableSource} terminates. + * The operator replenishes after half of the prefetch amount has been consumed + * and turned into {@code CompletableSource}s. + * @return the new {@link Completable} instance + * @throws NullPointerException if {@code mapper} is {@code null} + * @throws IllegalArgumentException if {@code prefetch} is non-positive + * @see #concatMapCompletable(Function, int) + * @since 2.2 + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + @BackpressureSupport(BackpressureKind.FULL) + public final Completable concatMapCompletableDelayError(@NonNull Function<? super T, ? extends CompletableSource> mapper, boolean tillTheEnd, int prefetch) { + Objects.requireNonNull(mapper, "mapper is null"); + ObjectHelper.verifyPositive(prefetch, "prefetch"); + return RxJavaPlugins.onAssembly(new FlowableConcatMapCompletable<>(this, mapper, tillTheEnd ? ErrorMode.END : ErrorMode.BOUNDARY, prefetch)); + } + + /** + * Maps each of the items into a {@link Publisher}, subscribes to them one after the other, + * one at a time and emits their values in order + * while delaying any error from either this or any of the inner {@code Publisher}s + * till all of them terminate. + * <p> + * Note that there is no guarantee where the given {@code mapper} function will be executed; it could be on the subscribing thread, + * on the upstream thread signaling the new item to be mapped or on the thread where the inner source terminates. To ensure + * the {@code mapper} function is confined to a known thread, use the {@link #concatMapDelayError(Function, boolean, int, Scheduler)} overload. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream. Both this and the inner {@code Publisher}s are + * expected to honor backpressure as well. If the current {@code Flowable} violates the rule, the operator will + * signal a {@link MissingBackpressureException}. If any of the inner {@code Publisher}s doesn't honor + * backpressure, that <em>may</em> throw an {@link IllegalStateException} when that + * {@code Publisher} completes.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code concatMapDelayError} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <R> the result value type + * @param mapper the function that maps the items of this {@code Publisher} into the inner {@code Publisher}s. + * @return the new {@code Flowable} instance with the concatenation behavior + * @throws NullPointerException if {@code mapper} is {@code null} + * @see #concatMapDelayError(Function, boolean, int, Scheduler) + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull R> Flowable<R> concatMapDelayError(@NonNull Function<? super T, @NonNull ? extends Publisher<? extends R>> mapper) { + return concatMapDelayError(mapper, true, 2); + } + + /** + * Maps each of the items into a {@link Publisher}, subscribes to them one after the other, + * one at a time and emits their values in order + * while delaying any error from either this or any of the inner {@code Publisher}s + * till all of them terminate. + * <p> + * Note that there is no guarantee where the given {@code mapper} function will be executed; it could be on the subscribing thread, + * on the upstream thread signaling the new item to be mapped or on the thread where the inner source terminates. To ensure + * the {@code mapper} function is confined to a known thread, use the {@link #concatMapDelayError(Function, boolean, int, Scheduler)} overload. + * + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream. Both this and the inner {@code Publisher}s are + * expected to honor backpressure as well. If the current {@code Flowable} violates the rule, the operator will + * signal a {@link MissingBackpressureException}. If any of the inner {@code Publisher}s doesn't honor + * backpressure, that <em>may</em> throw an {@link IllegalStateException} when that + * {@code Publisher} completes.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code concatMapDelayError} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <R> the result value type + * @param mapper the function that maps the items of this {@code Publisher} into the inner {@code Publisher}s. + * @param tillTheEnd + * if {@code true}, all errors from the outer and inner {@code Publisher} sources are delayed until the end, + * if {@code false}, an error from the main source is signaled when the current inner {@code Publisher} source terminates + * @param prefetch + * the number of elements to prefetch from the current {@code Flowable} + * @return the new {@code Flowable} instance with the concatenation behavior + * @throws NullPointerException if {@code mapper} is {@code null} + * @throws IllegalArgumentException if {@code prefetch} is non-positive + * @see #concatMapDelayError(Function, boolean, int, Scheduler) + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + public final <@NonNull R> Flowable<R> concatMapDelayError(@NonNull Function<? super T, @NonNull ? extends Publisher<? extends R>> mapper, + boolean tillTheEnd, int prefetch) { + Objects.requireNonNull(mapper, "mapper is null"); + ObjectHelper.verifyPositive(prefetch, "prefetch"); + if (this instanceof ScalarSupplier) { + @SuppressWarnings("unchecked") + T v = ((ScalarSupplier<T>)this).get(); + if (v == null) { + return empty(); + } + return FlowableScalarXMap.scalarXMap(v, mapper); + } + return RxJavaPlugins.onAssembly(new FlowableConcatMap<>(this, mapper, prefetch, tillTheEnd ? ErrorMode.END : ErrorMode.BOUNDARY)); + } + + /** + * Maps each of the upstream items into a {@link Publisher}, subscribes to them one after the other, + * one at a time and emits their values in order + * while executing the mapper function on the designated scheduler, delaying any error from either this or any of the + * inner {@code Publisher}s till all of them terminate. + * <p> + * The difference between {@link #concatMapDelayError(Function, boolean, int)} and this operator is that this operator guarantees the {@code mapper} + * function is executed on the specified scheduler. + * + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream. Both this and the inner {@code Publisher}s are + * expected to honor backpressure as well. If the current {@code Flowable} violates the rule, the operator will + * signal a {@link MissingBackpressureException}. If any of the inner {@code Publisher}s doesn't honor + * backpressure, that <em>may</em> throw an {@link IllegalStateException} when that + * {@code Publisher} completes.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code concatMapDelayError} executes the given {@code mapper} function on the provided {@link Scheduler}.</dd> + * </dl> + * + * @param <R> the result value type + * @param mapper the function that maps the items of this {@code Publisher} into the inner {@code Publisher}s. + * @param tillTheEnd + * if {@code true}, all errors from the outer and inner {@code Publisher} sources are delayed until the end, + * if {@code false}, an error from the main source is signaled when the current inner {@code Publisher} source terminates + * @param prefetch + * the number of elements to prefetch from the current {@code Flowable} + * @param scheduler + * the scheduler where the {@code mapper} function will be executed + * @return the new {@code Flowable} instance with the concatenation behavior + * @throws NullPointerException if {@code mapper} or {@code scheduler} is {@code null} + * @throws IllegalArgumentException if {@code prefetch} is non-positive + * @see #concatMapDelayError(Function, boolean, int) + * @since 3.0.0 + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.CUSTOM) + public final <@NonNull R> Flowable<R> concatMapDelayError(@NonNull Function<? super T, @NonNull ? extends Publisher<? extends R>> mapper, + boolean tillTheEnd, int prefetch, @NonNull Scheduler scheduler) { + Objects.requireNonNull(mapper, "mapper is null"); + ObjectHelper.verifyPositive(prefetch, "prefetch"); + Objects.requireNonNull(scheduler, "scheduler is null"); + return RxJavaPlugins.onAssembly(new FlowableConcatMapScheduler<>(this, mapper, prefetch, tillTheEnd ? ErrorMode.END : ErrorMode.BOUNDARY, scheduler)); + } + + /** + * Maps a sequence of values into {@link Publisher}s and concatenates these {@code Publisher}s eagerly into a single + * {@code Publisher}. + * <p> + * Eager concatenation means that once a subscriber subscribes, this operator subscribes to all of the + * inner {@code Publisher}s. The operator buffers the values emitted by these {@code Publisher}s and then drains them in + * order, each one after the previous one completes. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>Backpressure is honored towards the downstream, however, due to the eagerness requirement, sources + * are subscribed to in unbounded mode and their values are queued up in an unbounded buffer.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>This method does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <R> the value type + * @param mapper the function that maps a sequence of values into a sequence of {@code Publisher}s that will be + * eagerly concatenated + * @return the new {@code Flowable} instance with the specified concatenation behavior + * @throws NullPointerException if {@code mapper} is {@code null} + * @since 2.0 + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull R> Flowable<R> concatMapEager(@NonNull Function<? super T, @NonNull ? extends Publisher<? extends R>> mapper) { + return concatMapEager(mapper, bufferSize(), bufferSize()); + } + + /** + * Maps a sequence of values into {@link Publisher}s and concatenates these {@code Publisher}s eagerly into a single + * {@code Publisher}. + * <p> + * Eager concatenation means that once a subscriber subscribes, this operator subscribes to all of the + * inner {@code Publisher}s. The operator buffers the values emitted by these {@code Publisher}s and then drains them in + * order, each one after the previous one completes. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>Backpressure is honored towards the downstream, however, due to the eagerness requirement, sources + * are subscribed to in unbounded mode and their values are queued up in an unbounded buffer.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>This method does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <R> the value type + * @param mapper the function that maps a sequence of values into a sequence of {@code Publisher}s that will be + * eagerly concatenated + * @param maxConcurrency the maximum number of concurrent subscribed {@code Publisher}s + * @param prefetch hints about the number of expected values from each inner {@code Publisher}, must be positive + * @return the new {@code Flowable} instance with the specified concatenation behavior + * @throws NullPointerException if {@code mapper} is {@code null} + * @throws IllegalArgumentException if {@code maxConcurrency} or {@code prefetch} is non-positive + * @since 2.0 + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + public final <@NonNull R> Flowable<R> concatMapEager(@NonNull Function<? super T, @NonNull ? extends Publisher<? extends R>> mapper, + int maxConcurrency, int prefetch) { + Objects.requireNonNull(mapper, "mapper is null"); + ObjectHelper.verifyPositive(maxConcurrency, "maxConcurrency"); + ObjectHelper.verifyPositive(prefetch, "prefetch"); + return RxJavaPlugins.onAssembly(new FlowableConcatMapEager<>(this, mapper, maxConcurrency, prefetch, ErrorMode.IMMEDIATE)); + } + + /** + * Maps a sequence of values into {@link Publisher}s and concatenates these {@code Publisher}s eagerly into a single + * {@code Publisher}. + * <p> + * Eager concatenation means that once a subscriber subscribes, this operator subscribes to all of the + * inner {@code Publisher}s. The operator buffers the values emitted by these {@code Publisher}s and then drains them in + * order, each one after the previous one completes. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>Backpressure is honored towards the downstream, however, due to the eagerness requirement, sources + * are subscribed to in unbounded mode and their values are queued up in an unbounded buffer.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>This method does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <R> the value type + * @param mapper the function that maps a sequence of values into a sequence of {@code Publisher}s that will be + * eagerly concatenated + * @param tillTheEnd + * if {@code true}, all errors from the outer and inner {@code Publisher} sources are delayed until the end, + * if {@code false}, an error from the main source is signaled when the current inner {@code Publisher} source terminates + * @return the new {@code Flowable} instance with the specified concatenation behavior + * @throws NullPointerException if {@code mapper} is {@code null} + * @since 2.0 + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull R> Flowable<R> concatMapEagerDelayError(@NonNull Function<? super T, @NonNull ? extends Publisher<? extends R>> mapper, + boolean tillTheEnd) { + return concatMapEagerDelayError(mapper, tillTheEnd, bufferSize(), bufferSize()); + } + + /** + * Maps a sequence of values into {@link Publisher}s and concatenates these {@code Publisher}s eagerly into a single + * {@code Flowable} sequence. + * <p> + * Eager concatenation means that once a subscriber subscribes, this operator subscribes to all of the + * inner {@code Publisher}s. The operator buffers the values emitted by these {@code Publisher}s and then drains them in + * order, each one after the previous one completes. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>Backpressure is honored towards the downstream, however, due to the eagerness requirement, sources + * are subscribed to in unbounded mode and their values are queued up in an unbounded buffer.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>This method does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <R> the value type + * @param mapper the function that maps a sequence of values into a sequence of {@code Publisher}s that will be + * eagerly concatenated + * @param tillTheEnd + * if {@code true}, exceptions from the current {@code Flowable} and all the inner {@code Publisher}s are delayed until + * all of them terminate, if {@code false}, exception from the current {@code Flowable} is delayed until the + * currently running {@code Publisher} terminates + * @param maxConcurrency the maximum number of concurrent subscribed {@code Publisher}s + * @param prefetch + * the number of elements to prefetch from each source {@code Publisher} + * @return the new {@code Flowable} instance with the specified concatenation behavior + * @throws NullPointerException if {@code mapper} is {@code null} + * @throws IllegalArgumentException if {@code maxConcurrency} or {@code prefetch} is non-positive + * @since 2.0 + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + public final <@NonNull R> Flowable<R> concatMapEagerDelayError(@NonNull Function<? super T, @NonNull ? extends Publisher<? extends R>> mapper, + boolean tillTheEnd, int maxConcurrency, int prefetch) { + Objects.requireNonNull(mapper, "mapper is null"); + ObjectHelper.verifyPositive(maxConcurrency, "maxConcurrency"); + ObjectHelper.verifyPositive(prefetch, "prefetch"); + return RxJavaPlugins.onAssembly(new FlowableConcatMapEager<>(this, mapper, maxConcurrency, prefetch, tillTheEnd ? ErrorMode.END : ErrorMode.BOUNDARY)); + } + + /** + * Returns a {@code Flowable} that concatenate each item emitted by the current {@code Flowable} with the values in an + * {@link Iterable} corresponding to that item that is generated by a selector. + * + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream. The current {@code Flowable}s is + * expected to honor backpressure as well. If the current {@code Flowable} violates the rule, the operator will + * signal a {@link MissingBackpressureException}.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code concatMapIterable} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <U> + * the type of item emitted by the resulting {@code Flowable} + * @param mapper + * a function that returns an {@code Iterable} sequence of values for when given an item emitted by the + * current {@code Flowable} + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code mapper} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/flatmap.html">ReactiveX operators documentation: FlatMap</a> + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull U> Flowable<U> concatMapIterable(@NonNull Function<? super T, @NonNull ? extends Iterable<? extends U>> mapper) { + return concatMapIterable(mapper, 2); + } + + /** + * Returns a {@code Flowable} that concatenate each item emitted by the current {@code Flowable} with the values in an + * {@link Iterable} corresponding to that item that is generated by a selector. + * + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream. The current {@code Flowable} is + * expected to honor backpressure as well. If the current {@code Flowable} violates the rule, the operator will + * signal a {@link MissingBackpressureException}.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code concatMapIterable} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <U> + * the type of item emitted by the resulting {@code Flowable} + * @param mapper + * a function that returns an {@code Iterable} sequence of values for when given an item emitted by the + * current {@code Flowable} + * @param prefetch + * the number of elements to prefetch from the current {@code Flowable} + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code mapper} is {@code null} + * @throws IllegalArgumentException if {@code prefetch} is non-positive + * @see <a href="http://reactivex.io/documentation/operators/flatmap.html">ReactiveX operators documentation: FlatMap</a> + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + public final <@NonNull U> Flowable<U> concatMapIterable(@NonNull Function<? super T, @NonNull ? extends Iterable<? extends U>> mapper, int prefetch) { + Objects.requireNonNull(mapper, "mapper is null"); + ObjectHelper.verifyPositive(prefetch, "prefetch"); + return RxJavaPlugins.onAssembly(new FlowableFlattenIterable<>(this, mapper, prefetch)); + } + + /** + * Maps the upstream items into {@link MaybeSource}s and subscribes to them one after the + * other succeeds or completes, emits their success value if available or terminates immediately if + * either this {@code Flowable} or the current inner {@code MaybeSource} fail. + * <p> + * <img width="640" height="310" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/concatMapMaybe.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator expects the upstream to support backpressure and honors + * the backpressure from downstream. If this {@code Flowable} violates the rule, the operator will + * signal a {@link MissingBackpressureException}.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code concatMapMaybe} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * <p>History: 2.1.11 - experimental + * @param <R> the result type of the inner {@code MaybeSource}s + * @param mapper the function called with the upstream item and should return + * a {@code MaybeSource} to become the next source to + * be subscribed to + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code mapper} is {@code null} + * @see #concatMapMaybeDelayError(Function) + * @see #concatMapMaybe(Function, int) + * @since 2.2 + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull R> Flowable<R> concatMapMaybe(@NonNull Function<? super T, ? extends MaybeSource<? extends R>> mapper) { + return concatMapMaybe(mapper, 2); + } + + /** + * Maps the upstream items into {@link MaybeSource}s and subscribes to them one after the + * other succeeds or completes, emits their success value if available or terminates immediately if + * either this {@code Flowable} or the current inner {@code MaybeSource} fail. + * <p> + * <img width="640" height="310" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/concatMapMaybe.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator expects the upstream to support backpressure and honors + * the backpressure from downstream. If this {@code Flowable} violates the rule, the operator will + * signal a {@link MissingBackpressureException}.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code concatMapMaybe} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * <p>History: 2.1.11 - experimental + * @param <R> the result type of the inner {@code MaybeSource}s + * @param mapper the function called with the upstream item and should return + * a {@code MaybeSource} to become the next source to + * be subscribed to + * @param prefetch The number of upstream items to prefetch so that fresh items are + * ready to be mapped when a previous {@code MaybeSource} terminates. + * The operator replenishes after half of the prefetch amount has been consumed + * and turned into {@code MaybeSource}s. + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code mapper} is {@code null} + * @throws IllegalArgumentException if {@code prefetch} is non-positive + * @see #concatMapMaybe(Function) + * @see #concatMapMaybeDelayError(Function, boolean, int) + * @since 2.2 + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + public final <@NonNull R> Flowable<R> concatMapMaybe(@NonNull Function<? super T, ? extends MaybeSource<? extends R>> mapper, int prefetch) { + Objects.requireNonNull(mapper, "mapper is null"); + ObjectHelper.verifyPositive(prefetch, "prefetch"); + return RxJavaPlugins.onAssembly(new FlowableConcatMapMaybe<>(this, mapper, ErrorMode.IMMEDIATE, prefetch)); + } + + /** + * Maps the upstream items into {@link MaybeSource}s and subscribes to them one after the + * other terminates, emits their success value if available and delaying all errors + * till both this {@code Flowable} and all inner {@code MaybeSource}s terminate. + * <p> + * <img width="640" height="310" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/concatMapMaybeDelayError.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator expects the upstream to support backpressure and honors + * the backpressure from downstream. If this {@code Flowable} violates the rule, the operator will + * signal a {@link MissingBackpressureException}.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code concatMapMaybeDelayError} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * <p>History: 2.1.11 - experimental + * @param <R> the result type of the inner {@code MaybeSource}s + * @param mapper the function called with the upstream item and should return + * a {@code MaybeSource} to become the next source to + * be subscribed to + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code mapper} is {@code null} + * @see #concatMapMaybe(Function) + * @see #concatMapMaybeDelayError(Function, boolean) + * @since 2.2 + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull R> Flowable<R> concatMapMaybeDelayError(@NonNull Function<? super T, ? extends MaybeSource<? extends R>> mapper) { + return concatMapMaybeDelayError(mapper, true, 2); + } + + /** + * Maps the upstream items into {@link MaybeSource}s and subscribes to them one after the + * other terminates, emits their success value if available and optionally delaying all errors + * till both this {@code Flowable} and all inner {@code MaybeSource}s terminate. + * <p> + * <img width="640" height="310" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/concatMapMaybeDelayError.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator expects the upstream to support backpressure and honors + * the backpressure from downstream. If this {@code Flowable} violates the rule, the operator will + * signal a {@link MissingBackpressureException}.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code concatMapMaybeDelayError} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * <p>History: 2.1.11 - experimental + * @param <R> the result type of the inner {@code MaybeSource}s + * @param mapper the function called with the upstream item and should return + * a {@code MaybeSource} to become the next source to + * be subscribed to + * @param tillTheEnd If {@code true}, errors from this {@code Flowable} or any of the + * inner {@code MaybeSource}s are delayed until all + * of them terminate. If {@code false}, an error from this + * {@code Flowable} is delayed until the current inner + * {@code MaybeSource} terminates and only then is + * it emitted to the downstream. + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code mapper} is {@code null} + * @see #concatMapMaybe(Function, int) + * @see #concatMapMaybeDelayError(Function, boolean, int) + * @since 2.2 + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull R> Flowable<R> concatMapMaybeDelayError(@NonNull Function<? super T, ? extends MaybeSource<? extends R>> mapper, boolean tillTheEnd) { + return concatMapMaybeDelayError(mapper, tillTheEnd, 2); + } + + /** + * Maps the upstream items into {@link MaybeSource}s and subscribes to them one after the + * other terminates, emits their success value if available and optionally delaying all errors + * till both this {@code Flowable} and all inner {@code MaybeSource}s terminate. + * <p> + * <img width="640" height="310" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/concatMapMaybeDelayError.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator expects the upstream to support backpressure and honors + * the backpressure from downstream. If this {@code Flowable} violates the rule, the operator will + * signal a {@link MissingBackpressureException}.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code concatMapMaybeDelayError} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * <p>History: 2.1.11 - experimental + * @param <R> the result type of the inner {@code MaybeSource}s + * @param mapper the function called with the upstream item and should return + * a {@code MaybeSource} to become the next source to + * be subscribed to + * @param tillTheEnd If {@code true}, errors from this {@code Flowable} or any of the + * inner {@code MaybeSource}s are delayed until all + * of them terminate. If {@code false}, an error from this + * {@code Flowable} is delayed until the current inner + * {@code MaybeSource} terminates and only then is + * it emitted to the downstream. + * @param prefetch The number of upstream items to prefetch so that fresh items are + * ready to be mapped when a previous {@code MaybeSource} terminates. + * The operator replenishes after half of the prefetch amount has been consumed + * and turned into {@code MaybeSource}s. + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code mapper} is {@code null} + * @throws IllegalArgumentException if {@code prefetch} is non-positive + * @see #concatMapMaybe(Function, int) + * @since 2.2 + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + public final <@NonNull R> Flowable<R> concatMapMaybeDelayError(@NonNull Function<? super T, ? extends MaybeSource<? extends R>> mapper, boolean tillTheEnd, int prefetch) { + Objects.requireNonNull(mapper, "mapper is null"); + ObjectHelper.verifyPositive(prefetch, "prefetch"); + return RxJavaPlugins.onAssembly(new FlowableConcatMapMaybe<>(this, mapper, tillTheEnd ? ErrorMode.END : ErrorMode.BOUNDARY, prefetch)); + } + + /** + * Maps the upstream items into {@link SingleSource}s and subscribes to them one after the + * other succeeds, emits their success values or terminates immediately if + * either this {@code Flowable} or the current inner {@code SingleSource} fail. + * <p> + * <img width="640" height="310" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/concatMapSingle.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator expects the upstream to support backpressure and honors + * the backpressure from downstream. If this {@code Flowable} violates the rule, the operator will + * signal a {@link MissingBackpressureException}.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code concatMapSingle} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * <p>History: 2.1.11 - experimental + * @param <R> the result type of the inner {@code SingleSource}s + * @param mapper the function called with the upstream item and should return + * a {@code SingleSource} to become the next source to + * be subscribed to + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code mapper} is {@code null} + * @see #concatMapSingleDelayError(Function) + * @see #concatMapSingle(Function, int) + * @since 2.2 + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull R> Flowable<R> concatMapSingle(@NonNull Function<? super T, ? extends SingleSource<? extends R>> mapper) { + return concatMapSingle(mapper, 2); + } + + /** + * Maps the upstream items into {@link SingleSource}s and subscribes to them one after the + * other succeeds, emits their success values or terminates immediately if + * either this {@code Flowable} or the current inner {@code SingleSource} fail. + * <p> + * <img width="640" height="310" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/concatMapSingle.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator expects the upstream to support backpressure and honors + * the backpressure from downstream. If this {@code Flowable} violates the rule, the operator will + * signal a {@link MissingBackpressureException}.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code concatMapSingle} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * <p>History: 2.1.11 - experimental + * @param <R> the result type of the inner {@code SingleSource}s + * @param mapper the function called with the upstream item and should return + * a {@code SingleSource} to become the next source to + * be subscribed to + * @param prefetch The number of upstream items to prefetch so that fresh items are + * ready to be mapped when a previous {@code SingleSource} terminates. + * The operator replenishes after half of the prefetch amount has been consumed + * and turned into {@code SingleSource}s. + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code mapper} is {@code null} + * @throws IllegalArgumentException if {@code prefetch} is non-positive + * @see #concatMapSingle(Function) + * @see #concatMapSingleDelayError(Function, boolean, int) + * @since 2.2 + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + public final <@NonNull R> Flowable<R> concatMapSingle(@NonNull Function<? super T, ? extends SingleSource<? extends R>> mapper, int prefetch) { + Objects.requireNonNull(mapper, "mapper is null"); + ObjectHelper.verifyPositive(prefetch, "prefetch"); + return RxJavaPlugins.onAssembly(new FlowableConcatMapSingle<>(this, mapper, ErrorMode.IMMEDIATE, prefetch)); + } + + /** + * Maps the upstream items into {@link SingleSource}s and subscribes to them one after the + * other succeeds or fails, emits their success values and delays all errors + * till both this {@code Flowable} and all inner {@code SingleSource}s terminate. + * <p> + * <img width="640" height="310" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/concatMapSingleDelayError.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator expects the upstream to support backpressure and honors + * the backpressure from downstream. If this {@code Flowable} violates the rule, the operator will + * signal a {@link MissingBackpressureException}.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code concatMapSingleDelayError} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * <p>History: 2.1.11 - experimental + * @param <R> the result type of the inner {@code SingleSource}s + * @param mapper the function called with the upstream item and should return + * a {@code SingleSource} to become the next source to + * be subscribed to + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code mapper} is {@code null} + * @see #concatMapSingle(Function) + * @see #concatMapSingleDelayError(Function, boolean) + * @since 2.2 + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull R> Flowable<R> concatMapSingleDelayError(@NonNull Function<? super T, ? extends SingleSource<? extends R>> mapper) { + return concatMapSingleDelayError(mapper, true, 2); + } + + /** + * Maps the upstream items into {@link SingleSource}s and subscribes to them one after the + * other succeeds or fails, emits their success values and optionally delays all errors + * till both this {@code Flowable} and all inner {@code SingleSource}s terminate. + * <p> + * <img width="640" height="310" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/concatMapSingleDelayError.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator expects the upstream to support backpressure and honors + * the backpressure from downstream. If this {@code Flowable} violates the rule, the operator will + * signal a {@link MissingBackpressureException}.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code concatMapSingleDelayError} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * <p>History: 2.1.11 - experimental + * @param <R> the result type of the inner {@code SingleSource}s + * @param mapper the function called with the upstream item and should return + * a {@code SingleSource} to become the next source to + * be subscribed to + * @param tillTheEnd If {@code true}, errors from this {@code Flowable} or any of the + * inner {@code SingleSource}s are delayed until all + * of them terminate. If {@code false}, an error from this + * {@code Flowable} is delayed until the current inner + * {@code SingleSource} terminates and only then is + * it emitted to the downstream. + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code mapper} is {@code null} + * @see #concatMapSingle(Function, int) + * @see #concatMapSingleDelayError(Function, boolean, int) + * @since 2.2 + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull R> Flowable<R> concatMapSingleDelayError(@NonNull Function<? super T, ? extends SingleSource<? extends R>> mapper, boolean tillTheEnd) { + return concatMapSingleDelayError(mapper, tillTheEnd, 2); + } + + /** + * Maps the upstream items into {@link SingleSource}s and subscribes to them one after the + * other succeeds or fails, emits their success values and optionally delays errors + * till both this {@code Flowable} and all inner {@code SingleSource}s terminate. + * <p> + * <img width="640" height="310" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/concatMapSingleDelayError.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator expects the upstream to support backpressure and honors + * the backpressure from downstream. If this {@code Flowable} violates the rule, the operator will + * signal a {@link MissingBackpressureException}.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code concatMapSingleDelayError} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * <p>History: 2.1.11 - experimental + * @param <R> the result type of the inner {@code SingleSource}s + * @param mapper the function called with the upstream item and should return + * a {@code SingleSource} to become the next source to + * be subscribed to + * @param tillTheEnd If {@code true}, errors from this {@code Flowable} or any of the + * inner {@code SingleSource}s are delayed until all + * of them terminate. If {@code false}, an error from this + * {@code Flowable} is delayed until the current inner + * {@code SingleSource} terminates and only then is + * it emitted to the downstream. + * @param prefetch The number of upstream items to prefetch so that fresh items are + * ready to be mapped when a previous {@code SingleSource} terminates. + * The operator replenishes after half of the prefetch amount has been consumed + * and turned into {@code SingleSource}s. + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code mapper} is {@code null} + * @throws IllegalArgumentException if {@code prefetch} is non-positive + * @see #concatMapSingle(Function, int) + * @since 2.2 + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + public final <@NonNull R> Flowable<R> concatMapSingleDelayError(@NonNull Function<? super T, ? extends SingleSource<? extends R>> mapper, boolean tillTheEnd, int prefetch) { + Objects.requireNonNull(mapper, "mapper is null"); + ObjectHelper.verifyPositive(prefetch, "prefetch"); + return RxJavaPlugins.onAssembly(new FlowableConcatMapSingle<>(this, mapper, tillTheEnd ? ErrorMode.END : ErrorMode.BOUNDARY, prefetch)); + } + + /** + * Returns a {@code Flowable} that emits the items emitted from the current {@code Flowable}, then the next, one after + * the other, without interleaving them. + * <p> + * <img width="640" height="380" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/concat.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream. Both this and the {@code other} {@link Publisher}s + * are expected to honor backpressure as well. If any of then violates this rule, it <em>may</em> throw an + * {@link IllegalStateException} when the current {@code Flowable} completes.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code concatWith} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param other + * a {@code Publisher} to be concatenated after the current + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code other} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/concat.html">ReactiveX operators documentation: Concat</a> + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + public final Flowable<T> concatWith(@NonNull Publisher<? extends T> other) { + Objects.requireNonNull(other, "other is null"); + return concat(this, other); + } + + /** + * Returns a {@code Flowable} that emits the items from this {@code Flowable} followed by the success item or error event + * of the other {@link SingleSource}. + * <p> + * <img width="640" height="380" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/concat.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator supports backpressure and makes sure the success item of the other {@code SingleSource} + * is only emitted when there is a demand for it.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code concatWith} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * <p>History: 2.1.10 - experimental + * @param other the {@code SingleSource} whose signal should be emitted after this {@code Flowable} completes normally. + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code other} is {@code null} + * @since 2.2 + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Flowable<T> concatWith(@NonNull SingleSource<? extends T> other) { + Objects.requireNonNull(other, "other is null"); + return RxJavaPlugins.onAssembly(new FlowableConcatWithSingle<>(this, other)); + } + + /** + * Returns a {@code Flowable} that emits the items from this {@code Flowable} followed by the success item or terminal events + * of the other {@link MaybeSource}. + * <p> + * <img width="640" height="380" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/concat.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator supports backpressure and makes sure the success item of the other {@code MaybeSource} + * is only emitted when there is a demand for it.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code concatWith} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * <p>History: 2.1.10 - experimental + * @param other the {@code MaybeSource} whose signal should be emitted after this {@code Flowable} completes normally. + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code other} is {@code null} + * @since 2.2 + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Flowable<T> concatWith(@NonNull MaybeSource<? extends T> other) { + Objects.requireNonNull(other, "other is null"); + return RxJavaPlugins.onAssembly(new FlowableConcatWithMaybe<>(this, other)); + } + + /** + * Returns a {@code Flowable} that emits items from this {@code Flowable} and when it completes normally, the + * other {@link CompletableSource} is subscribed to and the returned {@code Flowable} emits its terminal events. + * <p> + * <img width="640" height="380" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/concat.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator does not interfere with backpressure between the current {@code Flowable} and the + * downstream consumer (i.e., acts as pass-through). When the operator switches to the + * {@link Completable}, backpressure is no longer present because {@code Completable} doesn't + * have items to apply backpressure to.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code concatWith} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * <p>History: 2.1.10 - experimental + * @param other the {@code CompletableSource} to subscribe to once the current {@code Flowable} completes normally + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code other} is {@code null} + * @since 2.2 + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.PASS_THROUGH) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Flowable<T> concatWith(@NonNull CompletableSource other) { + Objects.requireNonNull(other, "other is null"); + return RxJavaPlugins.onAssembly(new FlowableConcatWithCompletable<>(this, other)); + } + + /** + * Returns a {@link Single} that emits a {@link Boolean} that indicates whether the current {@code Flowable} emitted a + * specified item. + * <p> + * <img width="640" height="320" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/contains.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream and consumes the current {@code Flowable} in an + * unbounded manner (i.e., without applying backpressure).</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code contains} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param item + * the item to search for in the emissions from the current {@code Flowable} + * @return the new {@code Single} instance + * @throws NullPointerException if {@code item} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/contains.html">ReactiveX operators documentation: Contains</a> + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.UNBOUNDED_IN) + @SchedulerSupport(SchedulerSupport.NONE) + public final Single<Boolean> contains(@NonNull Object item) { + Objects.requireNonNull(item, "item is null"); + return any(Functions.equalsWith(item)); + } + + /** + * Returns a {@link Single} that counts the total number of items emitted by the current {@code Flowable} and emits + * this count as a 64-bit {@link Long}. + * <p> + * <img width="640" height="310" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/longCount.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream and consumes the current {@code Flowable} in an + * unbounded manner (i.e., without applying backpressure).</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code count} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @return the new {@code Single} instance + * @see <a href="http://reactivex.io/documentation/operators/count.html">ReactiveX operators documentation: Count</a> + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.UNBOUNDED_IN) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Single<Long> count() { + return RxJavaPlugins.onAssembly(new FlowableCountSingle<>(this)); + } + + /** + * Returns a {@code Flowable} that mirrors the current {@code Flowable}, except that it drops items emitted by the + * current {@code Flowable} that are followed by another item within a computed debounce duration. + * <p> + * <img width="640" height="425" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/debounce.f.v3.png" alt=""> + * <p> + * The delivery of the item happens on the thread of the first {@code onNext} or {@code onComplete} + * signal of the generated {@link Publisher} sequence, + * which if takes too long, a newer item may arrive from the upstream, causing the + * generated sequence to get cancelled, which may also interrupt any downstream blocking operation + * (yielding an {@code InterruptedException}). It is recommended processing items + * that may take long time to be moved to another thread via {@link #observeOn} applied after + * {@code debounce} itself. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>This operator does not support backpressure as it uses the {@code debounceSelector} to mark + * boundaries.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>This version of {@code debounce} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <U> + * the debounce value type (ignored) + * @param debounceIndicator + * function to retrieve a sequence that indicates the throttle duration for each item + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code debounceIndicator} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/debounce.html">ReactiveX operators documentation: Debounce</a> + * @see <a href="https://github.com/ReactiveX/RxJava/wiki/Backpressure">RxJava wiki: Backpressure</a> + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.ERROR) + @SchedulerSupport(SchedulerSupport.NONE) + public final <@NonNull U> Flowable<T> debounce(@NonNull Function<? super T, @NonNull ? extends Publisher<U>> debounceIndicator) { + Objects.requireNonNull(debounceIndicator, "debounceIndicator is null"); + return RxJavaPlugins.onAssembly(new FlowableDebounce<>(this, debounceIndicator)); + } + + /** + * Returns a {@code Flowable} that mirrors the current {@code Flowable}, except that it drops items emitted by the + * current {@code Flowable} that are followed by newer items before a timeout value expires. The timer resets on + * each emission. + * <p> + * <em>Note:</em> If items keep being emitted by the current {@code Flowable} faster than the timeout then no items + * will be emitted by the resulting {@code Flowable}. + * <p> + * <img width="640" height="310" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/debounce.v3.png" alt=""> + * <p> + * Delivery of the item after the grace period happens on the {@code computation} {@link Scheduler}'s + * {@code Worker} which if takes too long, a newer item may arrive from the upstream, causing the + * {@code Worker}'s task to get disposed, which may also interrupt any downstream blocking operation + * (yielding an {@code InterruptedException}). It is recommended processing items + * that may take long time to be moved to another thread via {@link #observeOn} applied after + * {@code debounce} itself. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>This operator does not support backpressure as it uses time to control data flow.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code debounce} operates by default on the {@code computation} {@code Scheduler}.</dd> + * </dl> + * + * @param timeout + * the length of the window of time that must pass after the emission of an item from the current + * {@code Flowable} in which it emits no items in order for the item to be emitted by the + * resulting {@code Flowable} + * @param unit + * the unit of time for the specified {@code timeout} + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code unit} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/debounce.html">ReactiveX operators documentation: Debounce</a> + * @see <a href="https://github.com/ReactiveX/RxJava/wiki/Backpressure">RxJava wiki: Backpressure</a> + * @see #throttleWithTimeout(long, TimeUnit) + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.ERROR) + @SchedulerSupport(SchedulerSupport.COMPUTATION) + @NonNull + public final Flowable<T> debounce(long timeout, @NonNull TimeUnit unit) { + return debounce(timeout, unit, Schedulers.computation()); + } + + /** + * Returns a {@code Flowable} that mirrors the current {@code Flowable}, except that it drops items emitted by the + * current {@code Flowable} that are followed by newer items before a timeout value expires on a specified + * {@link Scheduler}. The timer resets on each emission. + * <p> + * <em>Note:</em> If items keep being emitted by the current {@code Flowable} faster than the timeout then no items + * will be emitted by the resulting {@code Flowable}. + * <p> + * <img width="640" height="310" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/debounce.s.v3.png" alt=""> + * <p> + * Delivery of the item after the grace period happens on the given {@code Scheduler}'s + * {@code Worker} which if takes too long, a newer item may arrive from the upstream, causing the + * {@code Worker}'s task to get disposed, which may also interrupt any downstream blocking operation + * (yielding an {@code InterruptedException}). It is recommended processing items + * that may take long time to be moved to another thread via {@link #observeOn} applied after + * {@code debounce} itself. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>This operator does not support backpressure as it uses time to control data flow.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>You specify which {@code Scheduler} this operator will use.</dd> + * </dl> + * + * @param timeout + * the time each item has to be "the most recent" of those emitted by the current {@code Flowable} to + * ensure that it's not dropped + * @param unit + * the unit of time for the specified {@code timeout} + * @param scheduler + * the {@code Scheduler} to use internally to manage the timers that handle the timeout for each + * item + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code unit} or {@code scheduler} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/debounce.html">ReactiveX operators documentation: Debounce</a> + * @see <a href="https://github.com/ReactiveX/RxJava/wiki/Backpressure">RxJava wiki: Backpressure</a> + * @see #throttleWithTimeout(long, TimeUnit, Scheduler) + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.ERROR) + @SchedulerSupport(SchedulerSupport.CUSTOM) + public final Flowable<T> debounce(long timeout, @NonNull TimeUnit unit, @NonNull Scheduler scheduler) { + Objects.requireNonNull(unit, "unit is null"); + Objects.requireNonNull(scheduler, "scheduler is null"); + return RxJavaPlugins.onAssembly(new FlowableDebounceTimed<>(this, timeout, unit, scheduler, null)); + } + + /** + * Returns a {@code Flowable} that mirrors the current {@code Flowable}, except that it drops items emitted by the + * current {@code Flowable} that are followed by newer items before a timeout value expires on a specified + * {@link Scheduler}. The timer resets on each emission. + * <p> + * <em>Note:</em> If items keep being emitted by the current {@code Flowable} faster than the timeout then no items + * will be emitted by the resulting {@code Flowable}. + * <p> + * <img width="640" height="310" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/debounce.s.v3.png" alt=""> + * <p> + * Delivery of the item after the grace period happens on the given {@code Scheduler}'s + * {@code Worker} which if takes too long, a newer item may arrive from the upstream, causing the + * {@code Worker}'s task to get disposed, which may also interrupt any downstream blocking operation + * (yielding an {@code InterruptedException}). It is recommended processing items + * that may take long time to be moved to another thread via {@link #observeOn} applied after + * {@code debounce} itself. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>This operator does not support backpressure as it uses time to control data flow.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>You specify which {@code Scheduler} this operator will use.</dd> + * </dl> + * + * @param timeout + * the time each item has to be "the most recent" of those emitted by the current {@code Flowable} to + * ensure that it's not dropped + * @param unit + * the unit of time for the specified {@code timeout} + * @param scheduler + * the {@code Scheduler} to use internally to manage the timers that handle the timeout for each + * item + * @param onDropped + * called with the current entry when it has been replaced by a new one + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code unit} or {@code scheduler} is {@code null} or {@code onDropped} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/debounce.html">ReactiveX operators documentation: Debounce</a> + * @see <a href="https://github.com/ReactiveX/RxJava/wiki/Backpressure">RxJava wiki: Backpressure</a> + * @see #throttleWithTimeout(long, TimeUnit, Scheduler, Consumer) + * @since 3.1.6 - Experimental + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.ERROR) + @SchedulerSupport(SchedulerSupport.CUSTOM) + @Experimental + public final Flowable<T> debounce(long timeout, @NonNull TimeUnit unit, @NonNull Scheduler scheduler, @NonNull Consumer<? super T> onDropped) { + Objects.requireNonNull(unit, "unit is null"); + Objects.requireNonNull(scheduler, "scheduler is null"); + Objects.requireNonNull(onDropped, "onDropped is null"); + return RxJavaPlugins.onAssembly(new FlowableDebounceTimed<>(this, timeout, unit, scheduler, onDropped)); + } + + /** + * Returns a {@code Flowable} that emits the items emitted by the current {@code Flowable} or a specified default item + * if the current {@code Flowable} is empty. + * <p> + * <img width="640" height="305" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/defaultIfEmpty.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>If the current {@code Flowable} is empty, this operator is guaranteed to honor backpressure from downstream. + * If the current {@code Flowable} is non-empty, it is expected to honor backpressure as well; if the rule is violated, + * a {@link MissingBackpressureException} <em>may</em> get signaled somewhere downstream. + * </dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code defaultIfEmpty} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param defaultItem + * the item to emit if the current {@code Flowable} emits no items + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code defaultItem} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/defaultifempty.html">ReactiveX operators documentation: DefaultIfEmpty</a> + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + public final Flowable<T> defaultIfEmpty(@NonNull T defaultItem) { + Objects.requireNonNull(defaultItem, "defaultItem is null"); + return switchIfEmpty(just(defaultItem)); + } + + /** + * Returns a {@code Flowable} that delays the emissions of the current {@code Flowable} via another {@link Publisher} on a + * per-item basis. + * <p> + * <img width="640" height="450" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/delay.o.v3.png" alt=""> + * <p> + * <em>Note:</em> the resulting {@code Flowable} will immediately propagate any {@code onError} notification + * from the current {@code Flowable}. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator doesn't interfere with the backpressure behavior which is determined by the current {@code Flowable}. + * All of the other {@code Publisher}s supplied by the function are consumed + * in an unbounded manner (i.e., no backpressure applied to them).</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>This version of {@code delay} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <U> + * the item delay value type (ignored) + * @param itemDelayIndicator + * a function that returns a {@code Publisher} for each item emitted by the current {@code Flowable}, which is + * then used to delay the emission of that item by the resulting {@code Flowable} until the {@code Publisher} + * returned from {@code itemDelay} emits an item + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code itemDelayIndicator} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/delay.html">ReactiveX operators documentation: Delay</a> + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + public final <@NonNull U> Flowable<T> delay(@NonNull Function<? super T, @NonNull ? extends Publisher<U>> itemDelayIndicator) { + Objects.requireNonNull(itemDelayIndicator, "itemDelayIndicator is null"); + return flatMap(FlowableInternalHelper.itemDelay(itemDelayIndicator)); + } + + /** + * Returns a {@code Flowable} that emits the items emitted by the current {@code Flowable} shifted forward in time by a + * specified delay. The {@code onError} notification from the current {@code Flowable} is not delayed. + * <p> + * <img width="640" height="310" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/delay.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator doesn't interfere with the backpressure behavior which is determined by the current {@code Flowable}.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>This version of {@code delay} operates by default on the {@code computation} {@link Scheduler}.</dd> + * </dl> + * + * @param time + * the delay to shift the source by + * @param unit + * the {@link TimeUnit} in which {@code period} is defined + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code unit} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/delay.html">ReactiveX operators documentation: Delay</a> + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.COMPUTATION) + @NonNull + public final Flowable<T> delay(long time, @NonNull TimeUnit unit) { + return delay(time, unit, Schedulers.computation(), false); + } + + /** + * Returns a {@code Flowable} that emits the items emitted by the current {@code Flowable} shifted forward in time by a + * specified delay. If {@code delayError} is {@code true}, error notifications will also be delayed. + * <p> + * <img width="640" height="310" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/delay.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator doesn't interfere with the backpressure behavior which is determined by the current {@code Flowable}.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>This version of {@code delay} operates by default on the {@code computation} {@link Scheduler}.</dd> + * </dl> + * + * @param time + * the delay to shift the source by + * @param unit + * the {@link TimeUnit} in which {@code period} is defined + * @param delayError + * if {@code true}, the upstream exception is signaled with the given delay, after all preceding normal elements, + * if {@code false}, the upstream exception is signaled immediately + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code unit} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/delay.html">ReactiveX operators documentation: Delay</a> + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.COMPUTATION) + @NonNull + public final Flowable<T> delay(long time, @NonNull TimeUnit unit, boolean delayError) { + return delay(time, unit, Schedulers.computation(), delayError); + } + + /** + * Returns a {@code Flowable} that emits the items emitted by the current {@code Flowable} shifted forward in time by a + * specified delay. The {@code onError} notification from the current {@code Flowable} is not delayed. + * <p> + * <img width="640" height="310" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/delay.s.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator doesn't interfere with the backpressure behavior which is determined by the current {@code Flowable}.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>You specify which {@link Scheduler} this operator will use.</dd> + * </dl> + * + * @param time + * the delay to shift the source by + * @param unit + * the time unit of {@code delay} + * @param scheduler + * the {@code Scheduler} to use for delaying + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code unit} or {@code scheduler} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/delay.html">ReactiveX operators documentation: Delay</a> + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.CUSTOM) + @NonNull + public final Flowable<T> delay(long time, @NonNull TimeUnit unit, @NonNull Scheduler scheduler) { + return delay(time, unit, scheduler, false); + } + + /** + * Returns a {@code Flowable} that emits the items emitted by the current {@code Flowable} shifted forward in time by a + * specified delay. If {@code delayError} is {@code true}, error notifications will also be delayed. + * <p> + * <img width="640" height="310" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/delay.s.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator doesn't interfere with the backpressure behavior which is determined by the current {@code Flowable}.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>You specify which {@link Scheduler} this operator will use.</dd> + * </dl> + * + * @param time + * the delay to shift the source by + * @param unit + * the time unit of {@code delay} + * @param scheduler + * the {@code Scheduler} to use for delaying + * @param delayError + * if {@code true}, the upstream exception is signaled with the given delay, after all preceding normal elements, + * if {@code false}, the upstream exception is signaled immediately + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code unit} or {@code scheduler} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/delay.html">ReactiveX operators documentation: Delay</a> + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.CUSTOM) + public final Flowable<T> delay(long time, @NonNull TimeUnit unit, @NonNull Scheduler scheduler, boolean delayError) { + Objects.requireNonNull(unit, "unit is null"); + Objects.requireNonNull(scheduler, "scheduler is null"); + + return RxJavaPlugins.onAssembly(new FlowableDelay<>(this, Math.max(0L, time), unit, scheduler, delayError)); + } + + /** + * Returns a {@code Flowable} that delays the subscription to and emissions from the current {@code Flowable} via another + * {@link Publisher} on a per-item basis. + * <p> + * <img width="640" height="450" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/delay.oo.v3.png" alt=""> + * <p> + * <em>Note:</em> the resulting {@code Flowable} will immediately propagate any {@code onError} notification + * from the current {@code Flowable}. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator doesn't interfere with the backpressure behavior which is determined by the current {@code Flowable}. + * All of the other {@code Publisher}s supplied by the functions are consumed + * in an unbounded manner (i.e., no backpressure applied to them).</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>This version of {@code delay} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <U> + * the subscription delay value type (ignored) + * @param <V> + * the item delay value type (ignored) + * @param subscriptionIndicator + * a function that returns a {@code Publisher} that triggers the subscription to the current {@code Flowable} + * once it emits any item + * @param itemDelayIndicator + * a function that returns a {@code Publisher} for each item emitted by the current {@code Flowable}, which is + * then used to delay the emission of that item by the resulting {@code Flowable} until the {@code Publisher} + * returned from {@code itemDelay} emits an item + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code subscriptionIndicator} and {@code itemDelayIndicator} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/delay.html">ReactiveX operators documentation: Delay</a> + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull U, @NonNull V> Flowable<T> delay(@NonNull Publisher<U> subscriptionIndicator, + @NonNull Function<? super T, @NonNull ? extends Publisher<V>> itemDelayIndicator) { + return delaySubscription(subscriptionIndicator).delay(itemDelayIndicator); + } + + /** + * Returns a {@code Flowable} that delays the subscription to this {@link Publisher} + * until the other {@code Publisher} emits an element or completes normally. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator forwards the backpressure requests to this {@code Publisher} once + * the subscription happens and requests {@link Long#MAX_VALUE} from the other {@code Publisher}</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>This method does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <U> the value type of the other {@code Publisher}, irrelevant + * @param subscriptionIndicator the other {@code Publisher} that should trigger the subscription + * to this {@code Publisher}. + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code subscriptionIndicator} is {@code null} + * @since 2.0 + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + public final <@NonNull U> Flowable<T> delaySubscription(@NonNull Publisher<U> subscriptionIndicator) { + Objects.requireNonNull(subscriptionIndicator, "subscriptionIndicator is null"); + return RxJavaPlugins.onAssembly(new FlowableDelaySubscriptionOther<>(this, subscriptionIndicator)); + } + + /** + * Returns a {@code Flowable} that delays the subscription to the current {@code Flowable} by a given amount of time. + * <p> + * <img width="640" height="310" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/delaySubscription.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator doesn't interfere with the backpressure behavior which is determined by the current {@code Flowable}.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>This version of {@code delaySubscription} operates by default on the {@code computation} {@link Scheduler}.</dd> + * </dl> + * + * @param time + * the time to delay the subscription + * @param unit + * the time unit of {@code delay} + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code unit} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/delay.html">ReactiveX operators documentation: Delay</a> + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.COMPUTATION) + @NonNull + public final Flowable<T> delaySubscription(long time, @NonNull TimeUnit unit) { + return delaySubscription(time, unit, Schedulers.computation()); + } + + /** + * Returns a {@code Flowable} that delays the subscription to the current {@code Flowable} by a given amount of time, + * both waiting and subscribing on a given {@link Scheduler}. + * <p> + * <img width="640" height="310" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/delaySubscription.s.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator doesn't interfere with the backpressure behavior which is determined by the current {@code Flowable}.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>You specify which {@code Scheduler} this operator will use.</dd> + * </dl> + * + * @param time + * the time to delay the subscription + * @param unit + * the time unit of {@code delay} + * @param scheduler + * the {@code Scheduler} on which the waiting and subscription will happen + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code unit} or {@code scheduler} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/delay.html">ReactiveX operators documentation: Delay</a> + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.CUSTOM) + @NonNull + public final Flowable<T> delaySubscription(long time, @NonNull TimeUnit unit, @NonNull Scheduler scheduler) { + return delaySubscription(timer(time, unit, scheduler)); + } + + /** + * Returns a {@code Flowable} that reverses the effect of {@link #materialize materialize} by transforming the + * {@link Notification} objects extracted from the source items via a selector function + * into their respective {@link Subscriber} signal types. + * <p> + * <img width="640" height="335" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/dematerialize.v3.png" alt=""> + * <p> + * The intended use of the {@code selector} function is to perform a + * type-safe identity mapping (see example) on a source that is already of type + * {@code Notification<T>}. The Java language doesn't allow + * limiting instance methods to a certain generic argument shape, therefore, + * a function is used to ensure the conversion remains type safe. + * <p> + * When the upstream signals an {@link Notification#createOnError(Throwable) onError} or + * {@link Notification#createOnComplete() onComplete} item, the + * returned {@code Flowable} cancels of the flow and terminates with that type of terminal event: + * <pre><code> + * Flowable.just(createOnNext(1), createOnComplete(), createOnNext(2)) + * .doOnCancel(() -> System.out.println("Canceled!")); + * .dematerialize(notification -> notification) + * .test() + * .assertResult(1); + * </code></pre> + * If the upstream signals {@code onError} or {@code onComplete} directly, the flow is terminated + * with the same event. + * <pre><code> + * Flowable.just(createOnNext(1), createOnNext(2)) + * .dematerialize(notification -> notification) + * .test() + * .assertResult(1, 2); + * </code></pre> + * If this behavior is not desired, the completion can be suppressed by applying {@link #concatWith(Publisher)} + * with a {@link #never()} source. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator doesn't interfere with backpressure which is determined by the current {@code Flowable}'s + * backpressure behavior.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code dematerialize} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * <p>History: 2.2.4 - experimental + * + * @param <R> the output value type + * @param selector function that returns the upstream item and should return a {@code Notification} to signal + * the corresponding {@code Subscriber} event to the downstream. + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code selector} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/materialize-dematerialize.html">ReactiveX operators documentation: Dematerialize</a> + * @since 3.0.0 + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + @BackpressureSupport(BackpressureKind.PASS_THROUGH) + public final <@NonNull R> Flowable<R> dematerialize(@NonNull Function<@NonNull ? super T, @NonNull Notification<R>> selector) { + Objects.requireNonNull(selector, "selector is null"); + return RxJavaPlugins.onAssembly(new FlowableDematerialize<>(this, selector)); + } + + /** + * Returns a {@code Flowable} that emits all items emitted by the current {@code Flowable} that are distinct + * based on {@link Object#equals(Object)} comparison. + * <p> + * <img width="640" height="310" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/distinct.v3.png" alt=""> + * <p> + * It is recommended the elements' class {@code T} in the flow overrides the default {@code Object.equals()} and {@link Object#hashCode()} to provide + * a meaningful comparison between items as the default Java implementation only considers reference equivalence. + * <p> + * By default, {@code distinct()} uses an internal {@link java.util.HashSet} per {@link Subscriber} to remember + * previously seen items and uses {@link java.util.Set#add(Object)} returning {@code false} as the + * indicator for duplicates. + * <p> + * Note that this internal {@link HashSet} may grow unbounded as items won't be removed from it by + * the operator. Therefore, using very long or infinite upstream (with very distinct elements) may lead + * to {@link OutOfMemoryError}. + * <p> + * Customizing the retention policy can happen only by providing a custom {@link java.util.Collection} implementation + * to the {@link #distinct(Function, Supplier)} overload. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator doesn't interfere with backpressure which is determined by the current {@code Flowable}'s + * backpressure behavior.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code distinct} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @return the new {@code Flowable} instance + * @see <a href="http://reactivex.io/documentation/operators/distinct.html">ReactiveX operators documentation: Distinct</a> + * @see #distinct(Function) + * @see #distinct(Function, Supplier) + */ + @SuppressWarnings({ "rawtypes", "unchecked" }) + @CheckReturnValue + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Flowable<T> distinct() { + return distinct((Function)Functions.identity(), Functions.<T>createHashSet()); + } + + /** + * Returns a {@code Flowable} that emits all items emitted by the current {@code Flowable} that are distinct according + * to a key selector function and based on {@link Object#equals(Object)} comparison of the objects + * returned by the key selector function. + * <p> + * <img width="640" height="310" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/distinct.key.v3.png" alt=""> + * <p> + * It is recommended the keys' class {@code K} overrides the default {@code Object.equals()} and {@link Object#hashCode()} to provide + * a meaningful comparison between the key objects as the default Java implementation only considers reference equivalence. + * <p> + * By default, {@code distinct()} uses an internal {@link java.util.HashSet} per {@link Subscriber} to remember + * previously seen keys and uses {@link java.util.Set#add(Object)} returning {@code false} as the + * indicator for duplicates. + * <p> + * Note that this internal {@link HashSet} may grow unbounded as keys won't be removed from it by + * the operator. Therefore, using very long or infinite upstream (with very distinct keys) may lead + * to {@link OutOfMemoryError}. + * <p> + * Customizing the retention policy can happen only by providing a custom {@link java.util.Collection} implementation + * to the {@link #distinct(Function, Supplier)} overload. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator doesn't interfere with backpressure which is determined by the current {@code Flowable}'s + * backpressure behavior.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code distinct} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <K> the key type + * @param keySelector + * a function that projects an emitted item to a key value that is used to decide whether an item + * is distinct from another one or not + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code keySelector} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/distinct.html">ReactiveX operators documentation: Distinct</a> + * @see #distinct(Function, Supplier) + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull K> Flowable<T> distinct(@NonNull Function<? super T, K> keySelector) { + return distinct(keySelector, Functions.createHashSet()); + } + + /** + * Returns a {@code Flowable} that emits all items emitted by the current {@code Flowable} that are distinct according + * to a key selector function and based on {@link Object#equals(Object)} comparison of the objects + * returned by the key selector function. + * <p> + * <img width="640" height="310" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/distinct.key.v3.png" alt=""> + * <p> + * It is recommended the keys' class {@code K} overrides the default {@code Object.equals()} and {@link Object#hashCode()} to provide + * a meaningful comparison between the key objects as the default Java implementation only considers reference equivalence. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator doesn't interfere with backpressure which is determined by the current {@code Flowable}'s + * backpressure behavior.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code distinct} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <K> the key type + * @param keySelector + * a function that projects an emitted item to a key value that is used to decide whether an item + * is distinct from another one or not + * @param collectionSupplier + * function called for each individual {@link Subscriber} to return a {@link Collection} subtype for holding the extracted + * keys and whose add() method's return indicates uniqueness. + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code keySelector} or {@code collectionSupplier} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/distinct.html">ReactiveX operators documentation: Distinct</a> + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull K> Flowable<T> distinct(@NonNull Function<? super T, K> keySelector, + @NonNull Supplier<? extends Collection<? super K>> collectionSupplier) { + Objects.requireNonNull(keySelector, "keySelector is null"); + Objects.requireNonNull(collectionSupplier, "collectionSupplier is null"); + return RxJavaPlugins.onAssembly(new FlowableDistinct<>(this, keySelector, collectionSupplier)); + } + + /** + * Returns a {@code Flowable} that emits all items emitted by the current {@code Flowable} that are distinct from their + * immediate predecessors based on {@link Object#equals(Object)} comparison. + * <p> + * <img width="640" height="310" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/distinctUntilChanged.v3.png" alt=""> + * <p> + * It is recommended the elements' class {@code T} in the flow overrides the default {@code Object.equals()} to provide + * a meaningful comparison between items as the default Java implementation only considers reference equivalence. + * Alternatively, use the {@link #distinctUntilChanged(BiPredicate)} overload and provide a comparison function + * in case the class {@code T} can't be overridden with custom {@code equals()} or the comparison itself + * should happen on different terms or properties of the class {@code T}. + * <p> + * Note that the operator always retains the latest item from upstream regardless of the comparison result + * and uses it in the next comparison with the next upstream item. + * <p> + * Note that if element type {@code T} in the flow is mutable, the comparison of the previous and current + * item may yield unexpected results if the items are mutated externally. Common cases are mutable + * {@link CharSequence}s or {@link List}s where the objects will actually have the same + * references when they are modified and {@code distinctUntilChanged} will evaluate subsequent items as same. + * To avoid such situation, it is recommended that mutable data is converted to an immutable one, + * for example using {@code map(CharSequence::toString)} or {@code map(list -> Collections.unmodifiableList(new ArrayList<>(list)))}. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator doesn't interfere with backpressure which is determined by the current {@code Flowable}'s + * backpressure behavior.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code distinctUntilChanged} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @return the new {@code Flowable} instance + * @see <a href="http://reactivex.io/documentation/operators/distinct.html">ReactiveX operators documentation: Distinct</a> + * @see #distinctUntilChanged(BiPredicate) + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Flowable<T> distinctUntilChanged() { + return distinctUntilChanged(Functions.identity()); + } + + /** + * Returns a {@code Flowable} that emits all items emitted by the current {@code Flowable} that are distinct from their + * immediate predecessors, according to a key selector function and based on {@link Object#equals(Object)} comparison + * of those objects returned by the key selector function. + * <p> + * <img width="640" height="310" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/distinctUntilChanged.key.v3.png" alt=""> + * <p> + * It is recommended the keys' class {@code K} overrides the default {@code Object.equals()} to provide + * a meaningful comparison between the key objects as the default Java implementation only considers reference equivalence. + * Alternatively, use the {@link #distinctUntilChanged(BiPredicate)} overload and provide a comparison function + * in case the class {@code K} can't be overridden with custom {@code equals()} or the comparison itself + * should happen on different terms or properties of the item class {@code T} (for which the keys can be + * derived via a similar selector). + * <p> + * Note that the operator always retains the latest key from upstream regardless of the comparison result + * and uses it in the next comparison with the next key derived from the next upstream item. + * <p> + * Note that if element type {@code T} in the flow is mutable, the comparison of the previous and current + * item may yield unexpected results if the items are mutated externally. Common cases are mutable + * {@link CharSequence}s or {@link List}s where the objects will actually have the same + * references when they are modified and {@code distinctUntilChanged} will evaluate subsequent items as same. + * To avoid such situation, it is recommended that mutable data is converted to an immutable one, + * for example using {@code map(CharSequence::toString)} or {@code map(list -> Collections.unmodifiableList(new ArrayList<>(list)))}. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator doesn't interfere with backpressure which is determined by the current {@code Flowable}'s + * backpressure behavior.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code distinctUntilChanged} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <K> the key type + * @param keySelector + * a function that projects an emitted item to a key value that is used to decide whether an item + * is distinct from another one or not + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code keySelector} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/distinct.html">ReactiveX operators documentation: Distinct</a> + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull K> Flowable<T> distinctUntilChanged(@NonNull Function<? super T, K> keySelector) { + Objects.requireNonNull(keySelector, "keySelector is null"); + return RxJavaPlugins.onAssembly(new FlowableDistinctUntilChanged<>(this, keySelector, ObjectHelper.equalsPredicate())); + } + + /** + * Returns a {@code Flowable} that emits all items emitted by the current {@code Flowable} that are distinct from their + * immediate predecessors when compared with each other via the provided comparator function. + * <p> + * <img width="640" height="310" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/distinctUntilChanged.v3.png" alt=""> + * <p> + * Note that the operator always retains the latest item from upstream regardless of the comparison result + * and uses it in the next comparison with the next upstream item. + * <p> + * Note that if element type {@code T} in the flow is mutable, the comparison of the previous and current + * item may yield unexpected results if the items are mutated externally. Common cases are mutable + * {@link CharSequence}s or {@link List}s where the objects will actually have the same + * references when they are modified and {@code distinctUntilChanged} will evaluate subsequent items as same. + * To avoid such situation, it is recommended that mutable data is converted to an immutable one, + * for example using {@code map(CharSequence::toString)} or {@code map(list -> Collections.unmodifiableList(new ArrayList<>(list)))}. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator doesn't interfere with backpressure which is determined by the current {@code Flowable}'s + * backpressure behavior.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code distinctUntilChanged} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param comparer the function that receives the previous item and the current item and is + * expected to return {@code true} if the two are equal, thus skipping the current value. + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code comparer} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/distinct.html">ReactiveX operators documentation: Distinct</a> + * @since 2.0 + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Flowable<T> distinctUntilChanged(@NonNull BiPredicate<? super T, ? super T> comparer) { + Objects.requireNonNull(comparer, "comparer is null"); + return RxJavaPlugins.onAssembly(new FlowableDistinctUntilChanged<>(this, Functions.identity(), comparer)); + } + + /** + * Calls the specified action after this {@code Flowable} signals {@code onError} or {@code onComplete} or gets canceled by + * the downstream. + * <p>In case of a race between a terminal event and a cancellation, the provided {@code onFinally} action + * is executed once per subscription. + * <p>Note that the {@code onFinally} action is shared between subscriptions and as such + * should be thread-safe. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator doesn't interfere with backpressure which is determined by the current {@code Flowable}'s backpressure + * behavior.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code doFinally} does not operate by default on a particular {@link Scheduler}.</dd> + * <dt><b>Operator-fusion:</b></dt> + * <dd>This operator supports normal and conditional {@link Subscriber}s as well as boundary-limited + * synchronous or asynchronous queue-fusion.</dd> + * </dl> + * <p>History: 2.0.1 - experimental + * @param onFinally the action called when this {@code Flowable} terminates or gets canceled + * @throws NullPointerException if {@code onFinally} is {@code null} + * @return the new {@code Flowable} instance + * @since 2.1 + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.PASS_THROUGH) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Flowable<T> doFinally(@NonNull Action onFinally) { + Objects.requireNonNull(onFinally, "onFinally is null"); + return RxJavaPlugins.onAssembly(new FlowableDoFinally<>(this, onFinally)); + } + + /** + * Calls the specified consumer with the current item after this item has been emitted to the downstream. + * <p>Note that the {@code onAfterNext} action is shared between subscriptions and as such + * should be thread-safe. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator doesn't interfere with backpressure which is determined by the current {@code Flowable}'s backpressure + * behavior.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code doAfterNext} does not operate by default on a particular {@link Scheduler}.</dd> + * <dt><b>Operator-fusion:</b></dt> + * <dd>This operator supports normal and conditional {@link Subscriber}s as well as boundary-limited + * synchronous or asynchronous queue-fusion.</dd> + * </dl> + * <p>History: 2.0.1 - experimental + * @param onAfterNext the {@link Consumer} that will be called after emitting an item from upstream to the downstream + * @throws NullPointerException if {@code onAfterNext} is {@code null} + * @return the new {@code Flowable} instance + * @since 2.1 + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.PASS_THROUGH) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Flowable<T> doAfterNext(@NonNull Consumer<? super T> onAfterNext) { + Objects.requireNonNull(onAfterNext, "onAfterNext is null"); + return RxJavaPlugins.onAssembly(new FlowableDoAfterNext<>(this, onAfterNext)); + } + + /** + * Registers an {@link Action} to be called when this {@link Publisher} invokes either + * {@link Subscriber#onComplete onComplete} or {@link Subscriber#onError onError}. + * <p> + * <img width="640" height="310" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/finallyDo.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator doesn't interfere with backpressure which is determined by the current {@code Flowable}'s backpressure + * behavior.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code doAfterTerminate} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param onAfterTerminate + * an {@code Action} to be invoked when the current {@code Flowable} finishes + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code onAfterTerminate} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/do.html">ReactiveX operators documentation: Do</a> + * @see #doOnTerminate(Action) + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.PASS_THROUGH) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Flowable<T> doAfterTerminate(@NonNull Action onAfterTerminate) { + return doOnEach(Functions.emptyConsumer(), Functions.emptyConsumer(), + Functions.EMPTY_ACTION, onAfterTerminate); + } + + /** + * Calls the cancel {@link Action} if the downstream cancels the sequence. + * <p> + * <img width="640" height="310" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/doOnUnsubscribe.v3.png" alt=""> + * <p> + * The action is shared between subscriptions and thus may be called concurrently from multiple + * threads; the action must be thread-safe. + * <p> + * If the action throws a runtime exception, that exception is rethrown by the {@code onCancel()} call, + * sometimes as a {@link CompositeException} if there were multiple exceptions along the way. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>{@code doOnCancel} does not interact with backpressure requests or value delivery; backpressure + * behavior is preserved between its upstream and its downstream.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code doOnCancel} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param onCancel + * the action that gets called when the current {@code Flowable}'s {@link Subscription} is canceled + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code onCancel} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/do.html">ReactiveX operators documentation: Do</a> + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.PASS_THROUGH) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Flowable<T> doOnCancel(@NonNull Action onCancel) { + return doOnLifecycle(Functions.emptyConsumer(), Functions.EMPTY_LONG_CONSUMER, onCancel); + } + + /** + * Invokes an {@link Action} just before the current {@code Flowable} calls {@code onComplete}. + * <p> + * <img width="640" height="305" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/doOnComplete.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator doesn't interfere with backpressure which is determined by the current {@code Flowable}'s + * backpressure behavior.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code doOnComplete} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param onComplete + * the action to invoke when the current {@code Flowable} calls {@code onComplete} + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code onComplete} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/do.html">ReactiveX operators documentation: Do</a> + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.PASS_THROUGH) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Flowable<T> doOnComplete(@NonNull Action onComplete) { + return doOnEach(Functions.emptyConsumer(), Functions.emptyConsumer(), + onComplete, Functions.EMPTY_ACTION); + } + + /** + * Calls the appropriate onXXX consumer (shared between all subscribers) whenever a signal with the same type + * passes through, before forwarding them to downstream. + * <p> + * <img width="640" height="310" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/doOnEach.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator doesn't interfere with backpressure which is determined by the current {@code Flowable}'s + * backpressure behavior.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code doOnEach} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param onNext the {@link Consumer} to invoke when the current {@code Flowable} calls {@code onNext} + * @param onError the {@code Consumer} to invoke when the current {@code Flowable} calls {@code onError} + * @param onComplete the {@link Action} to invoke when the current {@code Flowable} calls {@code onComplete} + * @param onAfterTerminate the {@code Action} to invoke when the current {@code Flowable} calls {@code onAfterTerminate} + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code onNext}, {@code onError}, {@code onComplete} or {@code onAfterTerminate} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/do.html">ReactiveX operators documentation: Do</a> + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.PASS_THROUGH) + @SchedulerSupport(SchedulerSupport.NONE) + private Flowable<T> doOnEach(@NonNull Consumer<? super T> onNext, @NonNull Consumer<? super Throwable> onError, + Action onComplete, Action onAfterTerminate) { + Objects.requireNonNull(onNext, "onNext is null"); + Objects.requireNonNull(onError, "onError is null"); + Objects.requireNonNull(onComplete, "onComplete is null"); + Objects.requireNonNull(onAfterTerminate, "onAfterTerminate is null"); + return RxJavaPlugins.onAssembly(new FlowableDoOnEach<>(this, onNext, onError, onComplete, onAfterTerminate)); + } + + /** + * Invokes a {@link Consumer} with a {@link Notification} instances matching the signals emitted by the current {@code Flowable} + * before they are forwarded to the downstream. + * <p> + * <img width="640" height="310" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/doOnEach.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator doesn't interfere with backpressure which is determined by the current {@code Flowable}'s + * backpressure behavior.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code doOnEach} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param onNotification + * the action to invoke for each item emitted by the current {@code Flowable} + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code onNotification} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/do.html">ReactiveX operators documentation: Do</a> + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.PASS_THROUGH) + @SchedulerSupport(SchedulerSupport.NONE) + public final Flowable<T> doOnEach(@NonNull Consumer<@NonNull ? super Notification<T>> onNotification) { + Objects.requireNonNull(onNotification, "onNotification is null"); + return doOnEach( + Functions.notificationOnNext(onNotification), + Functions.notificationOnError(onNotification), + Functions.notificationOnComplete(onNotification), + Functions.EMPTY_ACTION + ); + } + + /** + * Calls the appropriate methods of the given {@link Subscriber} when the current {@code Flowable} signals events before forwarding it + * to the downstream. + * <p> + * In case the {@code onError} of the supplied {@code Subscriber} throws, the downstream will receive a composite + * exception containing the original exception and the exception thrown by {@code onError}. If either the + * {@code onNext} or the {@code onComplete} method of the supplied {@code Subscriber} throws, the downstream will be + * terminated and will receive this thrown exception. + * <p> + * <img width="640" height="310" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/doOnEach.o.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator doesn't interfere with backpressure which is determined by the current {@code Flowable}'s + * backpressure behavior.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code doOnEach} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param subscriber + * the {@code Subscriber} to be notified about {@code onNext}, {@code onError} and {@code onComplete} events on its + * respective methods before the actual downstream {@code Subscriber} gets notified. + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code subscriber} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/do.html">ReactiveX operators documentation: Do</a> + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.PASS_THROUGH) + @SchedulerSupport(SchedulerSupport.NONE) + public final Flowable<T> doOnEach(@NonNull Subscriber<? super T> subscriber) { + Objects.requireNonNull(subscriber, "subscriber is null"); + return doOnEach( + FlowableInternalHelper.subscriberOnNext(subscriber), + FlowableInternalHelper.subscriberOnError(subscriber), + FlowableInternalHelper.subscriberOnComplete(subscriber), + Functions.EMPTY_ACTION); + } + + /** + * Calls the given {@link Consumer} with the error {@link Throwable} if the current {@code Flowable} failed before forwarding it to + * the downstream. + * <p> + * In case the {@code onError} action throws, the downstream will receive a composite exception containing + * the original exception and the exception thrown by {@code onError}. + * <p> + * <img width="640" height="305" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/doOnError.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator doesn't interfere with backpressure which is determined by the current {@code Flowable}'s + * backpressure behavior.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code doOnError} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param onError + * the action to invoke if the current {@code Flowable} calls {@code onError} + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code onError} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/do.html">ReactiveX operators documentation: Do</a> + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.PASS_THROUGH) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Flowable<T> doOnError(@NonNull Consumer<? super Throwable> onError) { + return doOnEach(Functions.emptyConsumer(), onError, + Functions.EMPTY_ACTION, Functions.EMPTY_ACTION); + } + + /** + * Calls the appropriate {@code onXXX} method (shared between all {@link Subscriber}s) for the lifecycle events of + * the sequence (subscription, cancellation, requesting). + * <p> + * <img width="640" height="310" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/doOnNext.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator doesn't interfere with backpressure which is determined by the current {@code Flowable}'s + * backpressure behavior.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code doOnLifecycle} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param onSubscribe + * a {@link Consumer} called with the {@link Subscription} sent via {@link Subscriber#onSubscribe(Subscription)} + * @param onRequest + * a {@link LongConsumer} called with the request amount sent via {@link Subscription#request(long)} + * @param onCancel + * called when the downstream cancels the {@code Subscription} via {@link Subscription#cancel()} + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code onSubscribe}, {@code onRequest} or {@code onCancel} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/do.html">ReactiveX operators documentation: Do</a> + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.PASS_THROUGH) + @SchedulerSupport(SchedulerSupport.NONE) + public final Flowable<T> doOnLifecycle(@NonNull Consumer<? super Subscription> onSubscribe, + @NonNull LongConsumer onRequest, @NonNull Action onCancel) { + Objects.requireNonNull(onSubscribe, "onSubscribe is null"); + Objects.requireNonNull(onRequest, "onRequest is null"); + Objects.requireNonNull(onCancel, "onCancel is null"); + return RxJavaPlugins.onAssembly(new FlowableDoOnLifecycle<>(this, onSubscribe, onRequest, onCancel)); + } + + /** + * Calls the given {@link Consumer} with the value emitted by the current {@code Flowable} before forwarding it to the downstream. + * <p> + * <img width="640" height="310" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/doOnNext.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator doesn't interfere with backpressure which is determined by the current {@code Flowable}'s + * backpressure behavior.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code doOnNext} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param onNext + * the action to invoke when the current {@code Flowable} calls {@code onNext} + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code onNext} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/do.html">ReactiveX operators documentation: Do</a> + * @see #doAfterNext(Consumer) + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.PASS_THROUGH) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Flowable<T> doOnNext(@NonNull Consumer<? super T> onNext) { + return doOnEach(onNext, Functions.emptyConsumer(), + Functions.EMPTY_ACTION, Functions.EMPTY_ACTION); + } + + /** + * Calls the given {@link LongConsumer} with the request amount from the downstream before forwarding it + * to the current {@code Flowable}. + * <p> + * <b>Note:</b> This operator is for tracing the internal behavior of back-pressure request + * patterns and generally intended for debugging use. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator doesn't interfere with backpressure which is determined by the current {@code Flowable}'s + * backpressure behavior.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code doOnRequest} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param onRequest + * the action that gets called when a {@link Subscriber} requests items from the current + * {@code Flowable} + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code onRequest} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/do.html">ReactiveX operators + * documentation: Do</a> + * @since 2.0 + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.PASS_THROUGH) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Flowable<T> doOnRequest(@NonNull LongConsumer onRequest) { + return doOnLifecycle(Functions.emptyConsumer(), onRequest, Functions.EMPTY_ACTION); + } + + /** + * Calls the given {@link Consumer} with the {@link Subscription} provided by the current {@code Flowable} upon + * subscription from the downstream before forwarding it to the subscriber's + * {@link Subscriber#onSubscribe(Subscription) onSubscribe} method. + * <p> + * <img width="640" height="390" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/doOnSubscribe.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator doesn't interfere with backpressure which is determined by the current {@code Flowable}'s + * backpressure behavior.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code doOnSubscribe} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param onSubscribe + * the {@code Consumer} that gets called when a {@link Subscriber} subscribes to the current {@code Flowable} + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code onSubscribe} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/do.html">ReactiveX operators documentation: Do</a> + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.PASS_THROUGH) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Flowable<T> doOnSubscribe(@NonNull Consumer<? super Subscription> onSubscribe) { + return doOnLifecycle(onSubscribe, Functions.EMPTY_LONG_CONSUMER, Functions.EMPTY_ACTION); + } + + /** + * Calls the given {@link Action} when the current {@code Flowable} completes normally or with an error before those signals + * are forwarded to the downstream. + * <p> + * <img width="640" height="305" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/doOnTerminate.v3.png" alt=""> + * <p> + * This differs from {@code doAfterTerminate} in that this happens <em>before</em> the {@code onComplete} or + * {@code onError} notification. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator doesn't interfere with backpressure which is determined by the current {@code Flowable}'s + * backpressure behavior.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code doOnTerminate} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param onTerminate + * the action to invoke when the current {@code Flowable} calls {@code onComplete} or {@code onError} + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code onTerminate} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/do.html">ReactiveX operators documentation: Do</a> + * @see #doAfterTerminate(Action) + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.PASS_THROUGH) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Flowable<T> doOnTerminate(@NonNull Action onTerminate) { + return doOnEach(Functions.emptyConsumer(), Functions.actionConsumer(onTerminate), + onTerminate, Functions.EMPTY_ACTION); + } + + /** + * Returns a {@link Maybe} that emits the single item at a specified index in a sequence of emissions from + * this {@code Flowable} or completes if this {@code Flowable} sequence has fewer elements than index. + * <p> + * <img width="640" height="310" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/elementAt.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream and consumes the current {@code Flowable} in a bounded manner.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code elementAt} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param index + * the zero-based index of the item to retrieve + * @return the new {@code Maybe} instance + * @throws IndexOutOfBoundsException if {@code index} is negative + * @see <a href="http://reactivex.io/documentation/operators/elementat.html">ReactiveX operators documentation: ElementAt</a> + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Maybe<T> elementAt(long index) { + if (index < 0) { + throw new IndexOutOfBoundsException("index >= 0 required but it was " + index); + } + return RxJavaPlugins.onAssembly(new FlowableElementAtMaybe<>(this, index)); + } + + /** + * Returns a {@link Single} that emits the item found at a specified index in a sequence of emissions from + * this {@code Flowable}, or a default item if that index is out of range. + * <p> + * <img width="640" height="310" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/elementAtOrDefault.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream and consumes the current {@code Flowable} in a bounded manner.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code elementAt} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param index + * the zero-based index of the item to retrieve + * @param defaultItem + * the default item + * @return the new {@code Single} instance + * @throws NullPointerException if {@code defaultItem} is {@code null} + * @throws IndexOutOfBoundsException + * if {@code index} is negative + * @see <a href="http://reactivex.io/documentation/operators/elementat.html">ReactiveX operators documentation: ElementAt</a> + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + public final Single<T> elementAt(long index, @NonNull T defaultItem) { + if (index < 0) { + throw new IndexOutOfBoundsException("index >= 0 required but it was " + index); + } + Objects.requireNonNull(defaultItem, "defaultItem is null"); + return RxJavaPlugins.onAssembly(new FlowableElementAtSingle<>(this, index, defaultItem)); + } + + /** + * Returns a {@link Single} that emits the item found at a specified index in a sequence of emissions from + * this {@code Flowable} or signals a {@link NoSuchElementException} if this {@code Flowable} has fewer elements than index. + * <p> + * <img width="640" height="310" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/elementAtOrDefault.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream and consumes the current {@code Flowable} in a bounded manner.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code elementAtOrError} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param index + * the zero-based index of the item to retrieve + * @return the new {@code Single} instance + * @throws IndexOutOfBoundsException + * if {@code index} is less than 0 + * @see <a href="http://reactivex.io/documentation/operators/elementat.html">ReactiveX operators documentation: ElementAt</a> + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Single<T> elementAtOrError(long index) { + if (index < 0) { + throw new IndexOutOfBoundsException("index >= 0 required but it was " + index); + } + return RxJavaPlugins.onAssembly(new FlowableElementAtSingle<>(this, index, null)); + } + + /** + * Filters items emitted by the current {@code Flowable} by only emitting those that satisfy a specified predicate. + * <p> + * <img width="640" height="310" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/filter.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator doesn't interfere with backpressure which is determined by the current {@code Flowable}'s backpressure + * behavior.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code filter} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param predicate + * a function that evaluates each item emitted by the current {@code Flowable}, returning {@code true} + * if it passes the filter + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code predicate} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/filter.html">ReactiveX operators documentation: Filter</a> + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.PASS_THROUGH) + @SchedulerSupport(SchedulerSupport.NONE) + public final Flowable<T> filter(@NonNull Predicate<? super T> predicate) { + Objects.requireNonNull(predicate, "predicate is null"); + return RxJavaPlugins.onAssembly(new FlowableFilter<>(this, predicate)); + } + + /** + * Returns a {@link Maybe} that emits only the very first item emitted by this {@code Flowable} or + * completes if this {@code Flowable} is empty. + * <p> + * <img width="640" height="286" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/firstElement.m.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream and consumes the current {@code Flowable} in a bounded manner.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code firstElement} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @return the new {@code Maybe} instance + * @see <a href="http://reactivex.io/documentation/operators/first.html">ReactiveX operators documentation: First</a> + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Maybe<T> firstElement() { + return elementAt(0); + } + + /** + * Returns a {@link Single} that emits only the very first item emitted by this {@code Flowable}, or a default + * item if this {@code Flowable} completes without emitting anything. + * <p> + * <img width="640" height="298" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Flowable.first.s.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream and consumes the current {@code Flowable} in a bounded manner.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code first} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param defaultItem + * the default item to emit if the current {@code Flowable} doesn't emit anything + * @return the new {@code Single} instance + * @throws NullPointerException if {@code defaultItem} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/first.html">ReactiveX operators documentation: First</a> + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Single<T> first(@NonNull T defaultItem) { + return elementAt(0, defaultItem); + } + + /** + * Returns a {@link Single} that emits only the very first item emitted by this {@code Flowable} or + * signals a {@link NoSuchElementException} if this {@code Flowable} is empty. + * <p> + * <img width="640" height="237" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/firstOrError.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream and consumes the current {@code Flowable} in a bounded manner.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code firstOrError} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @return the new {@code Single} instance + * @see <a href="http://reactivex.io/documentation/operators/first.html">ReactiveX operators documentation: First</a> + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.FULL) // take may trigger UNBOUNDED_IN + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Single<T> firstOrError() { + return elementAtOrError(0); + } + + /** + * Returns a {@code Flowable} that emits items based on applying a function that you supply to each item emitted + * by the current {@code Flowable}, where that function returns a {@link Publisher}, and then merging those resulting + * {@code Publisher}s and emitting the results of this merger. + * <p> + * <img width="640" height="310" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/flatMap.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream. The upstream {@code Flowable} is consumed + * in a bounded manner (up to {@link #bufferSize()} outstanding request amount for items). + * The inner {@code Publisher}s are expected to honor backpressure; if violated, + * the operator <em>may</em> signal {@link MissingBackpressureException}.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code flatMap} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <R> the value type of the inner {@code Publisher}s and the output type + * @param mapper + * a function that, when applied to an item emitted by the current {@code Flowable}, returns a + * {@code Publisher} + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code mapper} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/flatmap.html">ReactiveX operators documentation: FlatMap</a> + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull R> Flowable<R> flatMap(@NonNull Function<? super T, @NonNull ? extends Publisher<? extends R>> mapper) { + return flatMap(mapper, false, bufferSize(), bufferSize()); + } + + /** + * Returns a {@code Flowable} that emits items based on applying a function that you supply to each item emitted + * by the current {@code Flowable}, where that function returns a {@link Publisher}, and then merging those resulting + * {@code Publisher}s and emitting the results of this merger. + * <p> + * <img width="640" height="310" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/flatMap.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream. The upstream {@code Flowable} is consumed + * in a bounded manner (up to {@link #bufferSize()} outstanding request amount for items). + * The inner {@code Publisher}s are expected to honor backpressure; if violated, + * the operator <em>may</em> signal {@link MissingBackpressureException}.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code flatMap} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <R> the value type of the inner {@code Publisher}s and the output type + * @param mapper + * a function that, when applied to an item emitted by the current {@code Flowable}, returns a + * {@code Publisher} + * @param delayErrors + * if {@code true}, exceptions from the current {@code Flowable} and all inner {@code Publisher}s are delayed until all of them terminate + * if {@code false}, the first one signaling an exception will terminate the whole sequence immediately + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code mapper} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/flatmap.html">ReactiveX operators documentation: FlatMap</a> + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull R> Flowable<R> flatMap(@NonNull Function<? super T, @NonNull ? extends Publisher<? extends R>> mapper, boolean delayErrors) { + return flatMap(mapper, delayErrors, bufferSize(), bufferSize()); + } + + /** + * Returns a {@code Flowable} that emits items based on applying a function that you supply to each item emitted + * by the current {@code Flowable}, where that function returns a {@link Publisher}, and then merging those resulting + * {@code Publisher}s and emitting the results of this merger, while limiting the maximum number of concurrent + * subscriptions to these {@code Publisher}s. + * <!-- <p> --> + * <!-- <img width="640" height="310" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/flatMap.v3.png" alt=""> --> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream. The upstream {@code Flowable} is consumed + * in a bounded manner (up to {@code maxConcurrency} outstanding request amount for items). + * The inner {@code Publisher}s are expected to honor backpressure; if violated, + * the operator <em>may</em> signal {@link MissingBackpressureException}.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code flatMap} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <R> the value type of the inner {@code Publisher}s and the output type + * @param mapper + * a function that, when applied to an item emitted by the current {@code Flowable}, returns a + * {@code Publisher} + * @param maxConcurrency + * the maximum number of {@code Publisher}s that may be subscribed to concurrently + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code mapper} is {@code null} + * @throws IllegalArgumentException if {@code maxConcurrency} is non-positive + * @see <a href="http://reactivex.io/documentation/operators/flatmap.html">ReactiveX operators documentation: FlatMap</a> + * @since 2.0 + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull R> Flowable<R> flatMap(@NonNull Function<? super T, @NonNull ? extends Publisher<? extends R>> mapper, int maxConcurrency) { + return flatMap(mapper, false, maxConcurrency, bufferSize()); + } + + /** + * Returns a {@code Flowable} that emits items based on applying a function that you supply to each item emitted + * by the current {@code Flowable}, where that function returns a {@link Publisher}, and then merging those resulting + * {@code Publisher}s and emitting the results of this merger, while limiting the maximum number of concurrent + * subscriptions to these {@code Publisher}s. + * <!-- <p> --> + * <!-- <img width="640" height="310" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/flatMap.v3.png" alt=""> --> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream. The upstream {@code Flowable} is consumed + * in a bounded manner (up to {@code maxConcurrency} outstanding request amount for items). + * The inner {@code Publisher}s are expected to honor backpressure; if violated, + * the operator <em>may</em> signal {@link MissingBackpressureException}.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code flatMap} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <R> the value type of the inner {@code Publisher}s and the output type + * @param mapper + * a function that, when applied to an item emitted by the current {@code Flowable}, returns a + * {@code Publisher} + * @param maxConcurrency + * the maximum number of {@code Publisher}s that may be subscribed to concurrently + * @param delayErrors + * if {@code true}, exceptions from the current {@code Flowable} and all inner {@code Publisher}s are delayed until all of them terminate + * if {@code false}, the first one signaling an exception will terminate the whole sequence immediately + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code mapper} is {@code null} + * @throws IllegalArgumentException if {@code maxConcurrency} is non-positive + * @see <a href="http://reactivex.io/documentation/operators/flatmap.html">ReactiveX operators documentation: FlatMap</a> + * @since 2.0 + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull R> Flowable<R> flatMap(@NonNull Function<? super T, @NonNull ? extends Publisher<? extends R>> mapper, boolean delayErrors, int maxConcurrency) { + return flatMap(mapper, delayErrors, maxConcurrency, bufferSize()); + } + + /** + * Returns a {@code Flowable} that emits items based on applying a function that you supply to each item emitted + * by the current {@code Flowable}, where that function returns a {@link Publisher}, and then merging those resulting + * {@code Publisher}s and emitting the results of this merger, while limiting the maximum number of concurrent + * subscriptions to these {@code Publisher}s. + * <!-- <p> --> + * <!-- <img width="640" height="310" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/flatMap.v3.png" alt=""> --> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream. The upstream {@code Flowable} is consumed + * in a bounded manner (up to {@code maxConcurrency} outstanding request amount for items). + * The inner {@code Publisher}s are expected to honor backpressure; if violated, + * the operator <em>may</em> signal {@link MissingBackpressureException}.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code flatMap} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <R> the value type of the inner {@code Publisher}s and the output type + * @param mapper + * a function that, when applied to an item emitted by the current {@code Flowable}, returns a + * {@code Publisher} + * @param maxConcurrency + * the maximum number of {@code Publisher}s that may be subscribed to concurrently + * @param delayErrors + * if {@code true}, exceptions from the current {@code Flowable} and all inner {@code Publisher}s are delayed until all of them terminate + * if {@code false}, the first one signaling an exception will terminate the whole sequence immediately + * @param bufferSize + * the number of elements to prefetch from each inner {@code Publisher} + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code mapper} is {@code null} + * @throws IllegalArgumentException if {@code maxConcurrency} or {@code bufferSize} is non-positive + * @see <a href="http://reactivex.io/documentation/operators/flatmap.html">ReactiveX operators documentation: FlatMap</a> + * @since 2.0 + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + public final <@NonNull R> Flowable<R> flatMap(@NonNull Function<? super T, @NonNull ? extends Publisher<? extends R>> mapper, + boolean delayErrors, int maxConcurrency, int bufferSize) { + Objects.requireNonNull(mapper, "mapper is null"); + ObjectHelper.verifyPositive(maxConcurrency, "maxConcurrency"); + ObjectHelper.verifyPositive(bufferSize, "bufferSize"); + if (this instanceof ScalarSupplier) { + @SuppressWarnings("unchecked") + T v = ((ScalarSupplier<T>)this).get(); + if (v == null) { + return empty(); + } + return FlowableScalarXMap.scalarXMap(v, mapper); + } + return RxJavaPlugins.onAssembly(new FlowableFlatMap<>(this, mapper, delayErrors, maxConcurrency, bufferSize)); + } + + /** + * Returns a {@code Flowable} that applies a function to each item emitted or notification raised by the current + * {@code Flowable} and then flattens the {@link Publisher}s returned from these functions and emits the resulting items. + * <p> + * <img width="640" height="410" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/mergeMap.nce.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream. The upstream {@code Flowable} is consumed + * in a bounded manner (up to {@link #bufferSize()} outstanding request amount for items). + * The inner {@code Publisher}s are expected to honor backpressure; if violated, + * the operator <em>may</em> signal {@link MissingBackpressureException}.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code flatMap} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <R> + * the result type + * @param onNextMapper + * a function that returns a {@code Publisher} to merge for each item emitted by the current {@code Flowable} + * @param onErrorMapper + * a function that returns a {@code Publisher} to merge for an {@code onError} notification from the current + * {@code Flowable} + * @param onCompleteSupplier + * a function that returns a {@code Publisher} to merge for an {@code onComplete} notification from the current + * {@code Flowable} + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code onNextMapper}, {@code onErrorMapper} or {@code onCompleteSupplier} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/flatmap.html">ReactiveX operators documentation: FlatMap</a> + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + public final <@NonNull R> Flowable<R> flatMap( + @NonNull Function<? super T, @NonNull ? extends Publisher<? extends R>> onNextMapper, + @NonNull Function<? super Throwable, @NonNull ? extends Publisher<? extends R>> onErrorMapper, + @NonNull Supplier<? extends Publisher<? extends R>> onCompleteSupplier) { + Objects.requireNonNull(onNextMapper, "onNextMapper is null"); + Objects.requireNonNull(onErrorMapper, "onErrorMapper is null"); + Objects.requireNonNull(onCompleteSupplier, "onCompleteSupplier is null"); + return merge(new FlowableMapNotification<>(this, onNextMapper, onErrorMapper, onCompleteSupplier)); + } + + /** + * Returns a {@code Flowable} that applies a function to each item emitted or notification raised by the current + * {@code Flowable} and then flattens the {@link Publisher}s returned from these functions and emits the resulting items, + * while limiting the maximum number of concurrent subscriptions to these {@code Publisher}s. + * <!-- <p> --> + * <!-- <img width="640" height="410" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/mergeMap.nce.v3.png" alt=""> --> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream. The upstream {@code Flowable} is consumed + * in a bounded manner (up to {@code maxConcurrency} outstanding request amount for items). + * The inner {@code Publisher}s are expected to honor backpressure; if violated, + * the operator <em>may</em> signal {@link MissingBackpressureException}.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code flatMap} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <R> + * the result type + * @param onNextMapper + * a function that returns a {@code Publisher} to merge for each item emitted by the current {@code Flowable} + * @param onErrorMapper + * a function that returns a {@code Publisher} to merge for an {@code onError} notification from the current + * {@code Flowable} + * @param onCompleteSupplier + * a function that returns a {@code Publisher} to merge for an {@code onComplete} notification from the current + * {@code Flowable} + * @param maxConcurrency + * the maximum number of {@code Publisher}s that may be subscribed to concurrently + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code onNextMapper}, {@code onErrorMapper} or {@code onCompleteSupplier} is {@code null} + * @throws IllegalArgumentException if {@code maxConcurrency} is non-positive + * @see <a href="http://reactivex.io/documentation/operators/flatmap.html">ReactiveX operators documentation: FlatMap</a> + * @since 2.0 + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + public final <@NonNull R> Flowable<R> flatMap( + @NonNull Function<? super T, @NonNull ? extends Publisher<? extends R>> onNextMapper, + @NonNull Function<Throwable, @NonNull ? extends Publisher<? extends R>> onErrorMapper, + @NonNull Supplier<? extends Publisher<? extends R>> onCompleteSupplier, + int maxConcurrency) { + Objects.requireNonNull(onNextMapper, "onNextMapper is null"); + Objects.requireNonNull(onErrorMapper, "onErrorMapper is null"); + Objects.requireNonNull(onCompleteSupplier, "onCompleteSupplier is null"); + return merge(new FlowableMapNotification<>( + this, onNextMapper, onErrorMapper, onCompleteSupplier), maxConcurrency); + } + + /** + * Returns a {@code Flowable} that emits the results of a specified function to the pair of values emitted by the + * current {@code Flowable} and a specified collection {@link Publisher}. + * <p> + * <img width="640" height="390" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/mergeMap.r.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream. The upstream {@code Flowable} is consumed + * in a bounded manner (up to {@code maxConcurrency} outstanding request amount for items). + * The inner {@code Publisher}s are expected to honor backpressure; if violated, + * the operator <em>may</em> signal {@link MissingBackpressureException}.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code flatMap} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <U> + * the type of items emitted by the inner {@code Publisher}s + * @param <R> + * the type of items emitted by the combiner function + * @param mapper + * a function that returns a {@code Publisher} for each item emitted by the current {@code Flowable} + * @param combiner + * a function that combines one item emitted by each of the source and collection {@code Publisher}s and + * returns an item to be emitted by the resulting {@code Flowable} + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code mapper} or {@code combiner} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/flatmap.html">ReactiveX operators documentation: FlatMap</a> + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull U, @NonNull R> Flowable<R> flatMap(@NonNull Function<? super T, @NonNull ? extends Publisher<? extends U>> mapper, + @NonNull BiFunction<? super T, ? super U, ? extends R> combiner) { + return flatMap(mapper, combiner, false, bufferSize(), bufferSize()); + } + + /** + * Returns a {@code Flowable} that emits the results of a specified function to the pair of values emitted by the + * current {@code Flowable} and a specified inner {@link Publisher}. + * <p> + * <img width="640" height="390" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/mergeMap.r.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream. The upstream {@code Flowable} is consumed + * in a bounded manner (up to {@link #bufferSize()} outstanding request amount for items). + * The inner {@code Publisher}s are expected to honor backpressure; if violated, + * the operator <em>may</em> signal {@link MissingBackpressureException}.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code flatMap} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <U> + * the type of items emitted by the inner {@code Publisher}s + * @param <R> + * the type of items emitted by the combiner functions + * @param mapper + * a function that returns a {@code Publisher} for each item emitted by the current {@code Flowable} + * @param combiner + * a function that combines one item emitted by each of the source and collection {@code Publisher}s and + * returns an item to be emitted by the resulting {@code Flowable} + * @param delayErrors + * if {@code true}, exceptions from the current {@code Flowable} and all inner {@code Publisher}s are delayed until all of them terminate + * if {@code false}, the first one signaling an exception will terminate the whole sequence immediately + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code mapper} or {@code combiner} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/flatmap.html">ReactiveX operators documentation: FlatMap</a> + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull U, @NonNull R> Flowable<R> flatMap(@NonNull Function<? super T, @NonNull ? extends Publisher<? extends U>> mapper, + @NonNull BiFunction<? super T, ? super U, ? extends R> combiner, boolean delayErrors) { + return flatMap(mapper, combiner, delayErrors, bufferSize(), bufferSize()); + } + + /** + * Returns a {@code Flowable} that emits the results of a specified function to the pair of values emitted by the + * current {@code Flowable} and a specified collection {@link Publisher}, while limiting the maximum number of concurrent + * subscriptions to these {@code Publisher}s. + * <!-- <p> --> + * <!-- <img width="640" height="390" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/mergeMap.r.v3.png" alt=""> --> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream. The upstream {@code Flowable} is consumed + * in a bounded manner (up to {@code maxConcurrency} outstanding request amount for items). + * The inner {@code Publisher}s are expected to honor backpressure; if violated, + * the operator <em>may</em> signal {@link MissingBackpressureException}.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code flatMap} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <U> + * the type of items emitted by the inner {@code Publisher}s + * @param <R> + * the type of items emitted by the combiner function + * @param mapper + * a function that returns a {@code Publisher} for each item emitted by the current {@code Flowable} + * @param combiner + * a function that combines one item emitted by each of the source and collection {@code Publisher}s and + * returns an item to be emitted by the resulting {@code Flowable} + * @param maxConcurrency + * the maximum number of {@code Publisher}s that may be subscribed to concurrently + * @param delayErrors + * if {@code true}, exceptions from the current {@code Flowable} and all inner {@code Publisher}s are delayed until all of them terminate + * if {@code false}, the first one signaling an exception will terminate the whole sequence immediately + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code mapper} or {@code combiner} is {@code null} + * @throws IllegalArgumentException if {@code maxConcurrency} is non-positive + * @see <a href="http://reactivex.io/documentation/operators/flatmap.html">ReactiveX operators documentation: FlatMap</a> + * @since 2.0 + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull U, @NonNull R> Flowable<R> flatMap(@NonNull Function<? super T, @NonNull ? extends Publisher<? extends U>> mapper, + @NonNull BiFunction<? super T, ? super U, ? extends R> combiner, boolean delayErrors, int maxConcurrency) { + return flatMap(mapper, combiner, delayErrors, maxConcurrency, bufferSize()); + } + + /** + * Returns a {@code Flowable} that emits the results of a specified function to the pair of values emitted by the + * current {@code Flowable} and a specified collection {@link Publisher}, while limiting the maximum number of concurrent + * subscriptions to these {@code Publisher}s. + * <!-- <p> --> + * <!-- <img width="640" height="390" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/mergeMap.r.v3.png" alt=""> --> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream. The upstream {@code Flowable} is consumed + * in a bounded manner (up to {@code maxConcurrency} outstanding request amount for items). + * The inner {@code Publisher}s are expected to honor backpressure; if violated, + * the operator <em>may</em> signal {@link MissingBackpressureException}.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code flatMap} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <U> + * the type of items emitted by the inner {@code Publisher}s + * @param <R> + * the type of items emitted by the combiner function + * @param mapper + * a function that returns a {@code Publisher} for each item emitted by the current {@code Flowable} + * @param combiner + * a function that combines one item emitted by each of the source and collection {@code Publisher}s and + * returns an item to be emitted by the resulting {@code Flowable} + * @param maxConcurrency + * the maximum number of {@code Publisher}s that may be subscribed to concurrently + * @param delayErrors + * if {@code true}, exceptions from the current {@code Flowable} and all inner {@code Publisher}s are delayed until all of them terminate + * if {@code false}, the first one signaling an exception will terminate the whole sequence immediately + * @param bufferSize + * the number of elements to prefetch from the inner {@code Publisher}s. + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code mapper} or {@code combiner} is {@code null} + * @throws IllegalArgumentException if {@code maxConcurrency} or {@code bufferSize} is non-positive + * @see <a href="http://reactivex.io/documentation/operators/flatmap.html">ReactiveX operators documentation: FlatMap</a> + * @since 2.0 + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + public final <@NonNull U, @NonNull R> Flowable<R> flatMap(@NonNull Function<? super T, @NonNull ? extends Publisher<? extends U>> mapper, + @NonNull BiFunction<? super T, ? super U, ? extends R> combiner, boolean delayErrors, int maxConcurrency, int bufferSize) { + Objects.requireNonNull(mapper, "mapper is null"); + Objects.requireNonNull(combiner, "combiner is null"); + ObjectHelper.verifyPositive(maxConcurrency, "maxConcurrency"); + ObjectHelper.verifyPositive(bufferSize, "bufferSize"); + return flatMap(FlowableInternalHelper.flatMapWithCombiner(mapper, combiner), delayErrors, maxConcurrency, bufferSize); + } + + /** + * Returns a {@code Flowable} that emits the results of a specified function to the pair of values emitted by the + * current {@code Flowable} and a specified collection {@link Publisher}, while limiting the maximum number of concurrent + * subscriptions to these {@code Publisher}s. + * <!-- <p> --> + * <!-- <img width="640" height="390" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/mergeMap.r.v3.png" alt=""> --> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream. The upstream {@code Flowable} is consumed + * in a bounded manner (up to {@link #bufferSize()} outstanding request amount for items). + * The inner {@code Publisher}s are expected to honor backpressure; if violated, + * the operator <em>may</em> signal {@link MissingBackpressureException}.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code flatMap} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <U> + * the type of items emitted by the inner {@code Publisher}s + * @param <R> + * the type of items emitted by the combiner function + * @param mapper + * a function that returns a {@code Publisher} for each item emitted by the current {@code Flowable} + * @param combiner + * a function that combines one item emitted by each of the source and collection {@code Publisher}s and + * returns an item to be emitted by the resulting {@code Flowable} + * @param maxConcurrency + * the maximum number of {@code Publisher}s that may be subscribed to concurrently + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code mapper} or {@code combiner} is {@code null} + * @throws IllegalArgumentException if {@code maxConcurrency} is non-positive + * @see <a href="http://reactivex.io/documentation/operators/flatmap.html">ReactiveX operators documentation: FlatMap</a> + * @since 2.0 + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull U, @NonNull R> Flowable<R> flatMap(@NonNull Function<? super T, @NonNull ? extends Publisher<? extends U>> mapper, + @NonNull BiFunction<? super T, ? super U, ? extends R> combiner, int maxConcurrency) { + return flatMap(mapper, combiner, false, maxConcurrency, bufferSize()); + } + + /** + * Maps each element of the upstream {@code Flowable} into {@link CompletableSource}s, subscribes to them and + * waits until the upstream and all {@code CompletableSource}s complete. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator consumes the upstream in an unbounded manner.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code flatMapCompletable} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param mapper the function that received each source value and transforms them into {@code CompletableSource}s. + * @return the new {@link Completable} instance + * @throws NullPointerException if {@code mapper} is {@code null} + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.UNBOUNDED_IN) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Completable flatMapCompletable(@NonNull Function<? super T, ? extends CompletableSource> mapper) { + return flatMapCompletable(mapper, false, Integer.MAX_VALUE); + } + + /** + * Maps each element of the upstream {@code Flowable} into {@link CompletableSource}s, subscribes to them and + * waits until the upstream and all {@code CompletableSource}s complete, optionally delaying all errors. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>If {@code maxConcurrency == }{@link Integer#MAX_VALUE} the operator consumes the upstream in an unbounded manner. + * Otherwise, the operator expects the upstream to honor backpressure. If the upstream doesn't support backpressure + * the operator behaves as if {@code maxConcurrency == }{@link Integer#MAX_VALUE} was used.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code flatMapCompletable} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param mapper the function that received each source value and transforms them into {@code CompletableSource}s. + * @param delayErrors if {@code true}, errors from the upstream and inner {@code CompletableSource}s are delayed until each of them + * terminates. + * @param maxConcurrency the maximum number of active subscriptions to the {@code CompletableSource}s. + * @return the new {@link Completable} instance + * @throws NullPointerException if {@code mapper} is {@code null} + * @throws IllegalArgumentException if {@code maxConcurrency} is non-positive + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.UNBOUNDED_IN) + @SchedulerSupport(SchedulerSupport.NONE) + public final Completable flatMapCompletable(@NonNull Function<? super T, ? extends CompletableSource> mapper, boolean delayErrors, int maxConcurrency) { + Objects.requireNonNull(mapper, "mapper is null"); + ObjectHelper.verifyPositive(maxConcurrency, "maxConcurrency"); + return RxJavaPlugins.onAssembly(new FlowableFlatMapCompletableCompletable<>(this, mapper, delayErrors, maxConcurrency)); + } + + /** + * Merges {@link Iterable}s generated by a mapper {@link Function} for each individual item emitted by + * the current {@code Flowable} into a single {@code Flowable} sequence. + * <p> + * <img width="640" height="342" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/flatMapIterable.f.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream. The current {@code Flowable}s is + * expected to honor backpressure as well. If the current {@code Flowable} violates the rule, the operator will + * signal a {@link MissingBackpressureException}.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code flatMapIterable} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <U> + * the output type and the element type of the {@code Iterable}s + * @param mapper + * a function that returns an {@code Iterable} sequence of values for when given an item emitted by the + * current {@code Flowable} + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code mapper} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/flatmap.html">ReactiveX operators documentation: FlatMap</a> + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull U> Flowable<U> flatMapIterable(@NonNull Function<? super T, @NonNull ? extends Iterable<? extends U>> mapper) { + return flatMapIterable(mapper, bufferSize()); + } + + /** + * Merges {@link Iterable}s generated by a mapper {@link Function} for each individual item emitted by + * the current {@code Flowable} into a single {@code Flowable} sequence. + * <p> + * <img width="640" height="342" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/flatMapIterable.f.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream. The current {@code Flowable}s is + * expected to honor backpressure as well. If the current {@code Flowable} violates the rule, the operator will + * signal a {@link MissingBackpressureException}.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code flatMapIterable} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <U> + * the type of item emitted by the resulting {@code Iterable} + * @param mapper + * a function that returns an {@code Iterable} sequence of values for when given an item emitted by the + * current {@code Flowable} + * @param bufferSize + * the number of elements to prefetch from the current {@code Flowable} + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code mapper} is {@code null} + * @throws IllegalArgumentException if {@code bufferSize} is non-positive + * @see <a href="http://reactivex.io/documentation/operators/flatmap.html">ReactiveX operators documentation: FlatMap</a> + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + public final <@NonNull U> Flowable<U> flatMapIterable(@NonNull Function<? super T, @NonNull ? extends Iterable<? extends U>> mapper, int bufferSize) { + Objects.requireNonNull(mapper, "mapper is null"); + ObjectHelper.verifyPositive(bufferSize, "bufferSize"); + return RxJavaPlugins.onAssembly(new FlowableFlattenIterable<>(this, mapper, bufferSize)); + } + + /** + * Merges {@link Iterable}s generated by a mapper {@link Function} for each individual item emitted by + * the current {@code Flowable} into a single {@code Flowable} sequence where the resulting items will + * be the combination of the original item and each inner item of the respective {@code Iterable} as returned + * by the {@code resultSelector} {@link BiFunction}. + * <p> + * <img width="640" height="410" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/flatMapIterable.f.r.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream and the current {@code Flowable}s is + * consumed in a bounded manner (requesting {@link #bufferSize()} items upfront, then 75% of it after 75% received).</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code flatMapIterable} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <U> + * the element type of the {@code Iterable}s + * @param <V> + * the output type as determined by the {@code resultSelector} function + * @param mapper + * a function that returns an {@code Iterable} sequence of values for each item emitted by the current + * {@code Flowable} + * @param combiner + * a function that returns an item based on the item emitted by the current {@code Flowable} and the + * {@code Iterable} returned for that item by the {@code collectionSelector} + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code mapper} or {@code combiner} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/flatmap.html">ReactiveX operators documentation: FlatMap</a> + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + public final <@NonNull U, @NonNull V> Flowable<V> flatMapIterable(@NonNull Function<? super T, @NonNull ? extends Iterable<? extends U>> mapper, + @NonNull BiFunction<? super T, ? super U, ? extends V> combiner) { + Objects.requireNonNull(mapper, "mapper is null"); + Objects.requireNonNull(combiner, "combiner is null"); + return flatMap(FlowableInternalHelper.flatMapIntoIterable(mapper), combiner, false, bufferSize(), bufferSize()); + } + + /** + * Merges {@link Iterable}s generated by a mapper {@link Function} for each individual item emitted by + * the current {@code Flowable} into a single {@code Flowable} sequence where the resulting items will + * be the combination of the original item and each inner item of the respective {@code Iterable} as returned + * by the {@code resultSelector} {@link BiFunction}. + * <p> + * <img width="640" height="410" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/flatMapIterable.f.r.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream. The current {@code Flowable}s is + * expected to honor backpressure as well. If the current {@code Flowable} violates the rule, the operator will + * signal a {@link MissingBackpressureException}.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code flatMapIterable} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <U> + * the element type of the inner {@code Iterable} sequences + * @param <V> + * the type of item emitted by the resulting {@code Flowable} + * @param mapper + * a function that returns an {@code Iterable} sequence of values for when given an item emitted by the + * current {@code Flowable} + * @param combiner + * a function that returns an item based on the item emitted by the current {@code Flowable} and the + * {@code Iterable} returned for that item by the {@code collectionSelector} + * @param prefetch + * the number of elements to prefetch from the current {@code Flowable} + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code mapper} or {@code combiner} is {@code null} + * @throws IllegalArgumentException if {@code prefetch} is non-positive + * @see <a href="http://reactivex.io/documentation/operators/flatmap.html">ReactiveX operators documentation: FlatMap</a> + * @since 2.0 + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + public final <@NonNull U, @NonNull V> Flowable<V> flatMapIterable(@NonNull Function<? super T, @NonNull ? extends Iterable<? extends U>> mapper, + @NonNull BiFunction<? super T, ? super U, ? extends V> combiner, int prefetch) { + Objects.requireNonNull(mapper, "mapper is null"); + Objects.requireNonNull(combiner, "combiner is null"); + return flatMap(FlowableInternalHelper.flatMapIntoIterable(mapper), combiner, false, bufferSize(), prefetch); + } + + /** + * Maps each element of the upstream {@code Flowable} into {@link MaybeSource}s, subscribes to all of them + * and merges their {@code onSuccess} values, in no particular order, into a single {@code Flowable} sequence. + * <p> + * <img width="640" height="310" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/flatMapMaybe.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator consumes the upstream in an unbounded manner.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code flatMapMaybe} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <R> the result value type + * @param mapper the function that received each source value and transforms them into {@code MaybeSource}s. + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code mapper} is {@code null} + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.UNBOUNDED_IN) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull R> Flowable<R> flatMapMaybe(@NonNull Function<? super T, ? extends MaybeSource<? extends R>> mapper) { + return flatMapMaybe(mapper, false, Integer.MAX_VALUE); + } + + /** + * Maps each element of the upstream {@code Flowable} into {@link MaybeSource}s, subscribes to at most + * {@code maxConcurrency} {@code MaybeSource}s at a time and merges their {@code onSuccess} values, + * in no particular order, into a single {@code Flowable} sequence, optionally delaying all errors. + * <p> + * <img width="640" height="310" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/flatMapMaybe.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>If {@code maxConcurrency == }{@link Integer#MAX_VALUE} the operator consumes the upstream in an unbounded manner. + * Otherwise, the operator expects the upstream to honor backpressure. If the upstream doesn't support backpressure + * the operator behaves as if {@code maxConcurrency == }{@link Integer#MAX_VALUE} was used.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code flatMapMaybe} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <R> the result value type + * @param mapper the function that received each source value and transforms them into {@code MaybeSource}s. + * @param delayErrors if {@code true}, errors from the upstream and inner {@code MaybeSource}s are delayed until each of them + * terminates. + * @param maxConcurrency the maximum number of active subscriptions to the {@code MaybeSource}s. + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code mapper} is {@code null} + * @throws IllegalArgumentException if {@code maxConcurrency} is non-positive + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.UNBOUNDED_IN) + @SchedulerSupport(SchedulerSupport.NONE) + public final <@NonNull R> Flowable<R> flatMapMaybe(@NonNull Function<? super T, ? extends MaybeSource<? extends R>> mapper, boolean delayErrors, int maxConcurrency) { + Objects.requireNonNull(mapper, "mapper is null"); + ObjectHelper.verifyPositive(maxConcurrency, "maxConcurrency"); + return RxJavaPlugins.onAssembly(new FlowableFlatMapMaybe<>(this, mapper, delayErrors, maxConcurrency)); + } + + /** + * Maps each element of the upstream {@code Flowable} into {@link SingleSource}s, subscribes to all of them + * and merges their {@code onSuccess} values, in no particular order, into a single {@code Flowable} sequence. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator consumes the upstream in an unbounded manner.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code flatMapSingle} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <R> the result value type + * @param mapper the function that received each source value and transforms them into {@code SingleSource}s. + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code mapper} is {@code null} + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.UNBOUNDED_IN) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull R> Flowable<R> flatMapSingle(@NonNull Function<? super T, ? extends SingleSource<? extends R>> mapper) { + return flatMapSingle(mapper, false, Integer.MAX_VALUE); + } + + /** + * Maps each element of the upstream {@code Flowable} into {@link SingleSource}s, subscribes to at most + * {@code maxConcurrency} {@code SingleSource}s at a time and merges their {@code onSuccess} values, + * in no particular order, into a single {@code Flowable} sequence, optionally delaying all errors. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>If {@code maxConcurrency == }{@link Integer#MAX_VALUE} the operator consumes the upstream in an unbounded manner. + * Otherwise, the operator expects the upstream to honor backpressure. If the upstream doesn't support backpressure + * the operator behaves as if {@code maxConcurrency == }{@link Integer#MAX_VALUE} was used.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code flatMapSingle} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <R> the result value type + * @param mapper the function that received each source value and transforms them into {@code SingleSource}s. + * @param delayErrors if {@code true}, errors from the upstream and inner {@code SingleSources} are delayed until each of them + * terminates. + * @param maxConcurrency the maximum number of active subscriptions to the {@code SingleSource}s. + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code mapper} is {@code null} + * @throws IllegalArgumentException if {@code maxConcurrency} is non-positive + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.UNBOUNDED_IN) + @SchedulerSupport(SchedulerSupport.NONE) + public final <@NonNull R> Flowable<R> flatMapSingle(@NonNull Function<? super T, ? extends SingleSource<? extends R>> mapper, boolean delayErrors, int maxConcurrency) { + Objects.requireNonNull(mapper, "mapper is null"); + ObjectHelper.verifyPositive(maxConcurrency, "maxConcurrency"); + return RxJavaPlugins.onAssembly(new FlowableFlatMapSingle<>(this, mapper, delayErrors, maxConcurrency)); + } + + /** + * Subscribes to the current {@code Flowable} and receives notifications for each element. + * <p> + * Alias to {@link #subscribe(Consumer)} + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator consumes the current {@code Flowable} in an unbounded manner (i.e., no + * backpressure is applied to it).</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code forEach} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param onNext + * {@link Consumer} to execute for each item. + * @return + * a {@link Disposable} that allows canceling an asynchronous sequence + * @throws NullPointerException + * if {@code onNext} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/subscribe.html">ReactiveX operators documentation: Subscribe</a> + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.NONE) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Disposable forEach(@NonNull Consumer<? super T> onNext) { + return subscribe(onNext); + } + + /** + * Subscribes to the current {@code Flowable} and receives notifications for each element until the + * {@code onNext} Predicate returns {@code false}. + * <p> + * If the {@code Flowable} emits an error, it is wrapped into an + * {@link io.reactivex.rxjava3.exceptions.OnErrorNotImplementedException OnErrorNotImplementedException} + * and routed to the {@link RxJavaPlugins#onError(Throwable)} handler. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator consumes the current {@code Flowable} in an unbounded manner (i.e., no + * backpressure is applied to it).</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code forEachWhile} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param onNext + * {@link Predicate} to execute for each item. + * @return + * a {@link Disposable} that allows canceling an asynchronous sequence + * @throws NullPointerException + * if {@code onNext} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/subscribe.html">ReactiveX operators documentation: Subscribe</a> + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.NONE) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Disposable forEachWhile(@NonNull Predicate<? super T> onNext) { + return forEachWhile(onNext, Functions.ON_ERROR_MISSING, Functions.EMPTY_ACTION); + } + + /** + * Subscribes to the current {@code Flowable} and receives notifications for each element and error events until the + * {@code onNext} Predicate returns {@code false}. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator consumes the current {@code Flowable} in an unbounded manner (i.e., no + * backpressure is applied to it).</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code forEachWhile} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param onNext + * {@link Predicate} to execute for each item. + * @param onError + * {@link Consumer} to execute when an error is emitted. + * @return + * a {@link Disposable} that allows canceling an asynchronous sequence + * @throws NullPointerException + * if {@code onNext} or {@code onError} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/subscribe.html">ReactiveX operators documentation: Subscribe</a> + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.NONE) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Disposable forEachWhile(@NonNull Predicate<? super T> onNext, @NonNull Consumer<? super Throwable> onError) { + return forEachWhile(onNext, onError, Functions.EMPTY_ACTION); + } + + /** + * Subscribes to the current {@code Flowable} and receives notifications for each element and the terminal events until the + * {@code onNext} Predicate returns {@code false}. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator consumes the current {@code Flowable} in an unbounded manner (i.e., no + * backpressure is applied to it).</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code forEachWhile} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param onNext + * {@link Predicate} to execute for each item. + * @param onError + * {@link Consumer} to execute when an error is emitted. + * @param onComplete + * {@link Action} to execute when completion is signaled. + * @return + * a {@link Disposable} that allows canceling an asynchronous sequence + * @throws NullPointerException + * if {@code onNext}, {@code onError} or {@code onComplete} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/subscribe.html">ReactiveX operators documentation: Subscribe</a> + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.NONE) + @SchedulerSupport(SchedulerSupport.NONE) + public final Disposable forEachWhile(@NonNull Predicate<? super T> onNext, @NonNull Consumer<? super Throwable> onError, + @NonNull Action onComplete) { + Objects.requireNonNull(onNext, "onNext is null"); + Objects.requireNonNull(onError, "onError is null"); + Objects.requireNonNull(onComplete, "onComplete is null"); + + ForEachWhileSubscriber<T> s = new ForEachWhileSubscriber<>(onNext, onError, onComplete); + subscribe(s); + return s; + } + + /** + * Groups the items emitted by the current {@code Flowable} according to a specified criterion, and emits these + * grouped items as {@link GroupedFlowable}s. The emitted {@code GroupedFlowable} allows only a single + * {@link Subscriber} during its lifetime and if this {@code Subscriber} cancels before the + * source terminates, the next emission by the source having the same key will trigger a new + * {@code GroupedFlowable} emission. + * <p> + * <img width="640" height="360" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/groupBy.v3.png" alt=""> + * <p> + * <em>Note:</em> A {@code GroupedFlowable} will cache the items it is to emit until such time as it + * is subscribed to. For this reason, in order to avoid memory leaks, you should not simply ignore those + * {@code GroupedFlowable}s that do not concern you. Instead, you can signal to them that they may + * discard their buffers by applying an operator like {@link #ignoreElements} to them. + * <p> + * Note that the {@code GroupedFlowable}s should be subscribed to as soon as possible, otherwise, + * the unconsumed groups may starve other groups due to the internal backpressure + * coordination of the {@code groupBy} operator. Such hangs can be usually avoided by using + * {@link #flatMap(Function, int)} or {@link #concatMapEager(Function, int, int)} and overriding the default maximum concurrency + * value to be greater or equal to the expected number of groups, possibly using + * {@link Integer#MAX_VALUE} if the number of expected groups is unknown. + * <p> + * Note also that ignoring groups or subscribing later (i.e., on another thread) will result in + * so-called group abandonment where a group will only contain one element and the group will be + * re-created over and over as new upstream items trigger a new group. The behavior is + * a trade-off between no-dataloss, upstream cancellation and excessive group creation. + * + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The consumer of the returned {@code Flowable} has to be ready to receive new {@code GroupedFlowable}s or else + * this operator will signal {@link MissingBackpressureException}. To avoid this exception, make + * sure a combining operator (such as {@code flatMap}) has adequate amount of buffering/prefetch configured. + * The inner {@code GroupedFlowable}s honor backpressure but due to the single-source multiple consumer + * nature of this operator, each group must be consumed so the whole operator can make progress and not hang.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code groupBy} does not operate by default on a particular {@link Scheduler}.</dd> + * <dt><b>Error handling:</b></dt> + * <dd>If the upstream signals or the callback(s) throw an exception, the returned {@code Flowable} and + * all active inner {@code GroupedFlowable}s will signal the same exception.</dd> + * </dl> + * + * @param keySelector + * a function that extracts the key for each item + * @param <K> + * the key type + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code keySelector} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/groupby.html">ReactiveX operators documentation: GroupBy</a> + * @see #groupBy(Function, boolean) + * @see #groupBy(Function, Function) + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.ERROR) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull K> Flowable<GroupedFlowable<K, T>> groupBy(@NonNull Function<? super T, ? extends K> keySelector) { + return groupBy(keySelector, Functions.identity(), false, bufferSize()); + } + + /** + * Groups the items emitted by the current {@code Flowable} according to a specified criterion, and emits these + * grouped items as {@link GroupedFlowable}s. The emitted {@code GroupedFlowable} allows only a single + * {@link Subscriber} during its lifetime and if this {@code Subscriber} cancels before the + * source terminates, the next emission by the source having the same key will trigger a new + * {@code GroupedFlowable} emission. + * <p> + * <img width="640" height="360" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/groupBy.v3.png" alt=""> + * <p> + * <em>Note:</em> A {@code GroupedFlowable} will cache the items it is to emit until such time as it + * is subscribed to. For this reason, in order to avoid memory leaks, you should not simply ignore those + * {@code GroupedFlowable}s that do not concern you. Instead, you can signal to them that they may + * discard their buffers by applying an operator like {@link #ignoreElements} to them. + * <p> + * Note that the {@code GroupedFlowable}s should be subscribed to as soon as possible, otherwise, + * the unconsumed groups may starve other groups due to the internal backpressure + * coordination of the {@code groupBy} operator. Such hangs can be usually avoided by using + * {@link #flatMap(Function, int)} or {@link #concatMapEager(Function, int, int)} and overriding the default maximum concurrency + * value to be greater or equal to the expected number of groups, possibly using + * {@link Integer#MAX_VALUE} if the number of expected groups is unknown. + * <p> + * Note also that ignoring groups or subscribing later (i.e., on another thread) will result in + * so-called group abandonment where a group will only contain one element and the group will be + * re-created over and over as new upstream items trigger a new group. The behavior is + * a trade-off between no-dataloss, upstream cancellation and excessive group creation. + * + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The consumer of the returned {@code Flowable} has to be ready to receive new {@code GroupedFlowable}s or else + * this operator will signal {@link MissingBackpressureException}. To avoid this exception, make + * sure a combining operator (such as {@code flatMap}) has adequate amount of buffering/prefetch configured. + * The inner {@code GroupedFlowable}s honor backpressure but due to the single-source multiple consumer + * nature of this operator, each group must be consumed so the whole operator can make progress and not hang.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code groupBy} does not operate by default on a particular {@link Scheduler}.</dd> + * <dt><b>Error handling:</b></dt> + * <dd>If the upstream signals or the callback(s) throw an exception, the returned {@code Flowable} and + * all active inner {@code GroupedFlowable}s will signal the same exception.</dd> + * </dl> + * + * @param keySelector + * a function that extracts the key for each item + * @param <K> + * the key type + * @param delayError + * if {@code true}, the exception from the current {@code Flowable} is delayed in each group until that specific group emitted + * the normal values; if {@code false}, the exception bypasses values in the groups and is reported immediately. + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code keySelector} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/groupby.html">ReactiveX operators documentation: GroupBy</a> + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.ERROR) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull K> Flowable<GroupedFlowable<K, T>> groupBy(@NonNull Function<? super T, ? extends K> keySelector, boolean delayError) { + return groupBy(keySelector, Functions.identity(), delayError, bufferSize()); + } + + /** + * Groups the items emitted by the current {@code Flowable} according to a specified criterion, and emits these + * grouped items as {@link GroupedFlowable}s. The emitted {@code GroupedFlowable} allows only a single + * {@link Subscriber} during its lifetime and if this {@code Subscriber} cancels before the + * source terminates, the next emission by the source having the same key will trigger a new + * {@code GroupedFlowable} emission. + * <p> + * <img width="640" height="360" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/groupBy.v3.png" alt=""> + * <p> + * <em>Note:</em> A {@code GroupedFlowable} will cache the items it is to emit until such time as it + * is subscribed to. For this reason, in order to avoid memory leaks, you should not simply ignore those + * {@code GroupedFlowable}s that do not concern you. Instead, you can signal to them that they may + * discard their buffers by applying an operator like {@link #ignoreElements} to them. + * <p> + * Note that the {@code GroupedFlowable}s should be subscribed to as soon as possible, otherwise, + * the unconsumed groups may starve other groups due to the internal backpressure + * coordination of the {@code groupBy} operator. Such hangs can be usually avoided by using + * {@link #flatMap(Function, int)} or {@link #concatMapEager(Function, int, int)} and overriding the default maximum concurrency + * value to be greater or equal to the expected number of groups, possibly using + * {@link Integer#MAX_VALUE} if the number of expected groups is unknown. + * <p> + * Note also that ignoring groups or subscribing later (i.e., on another thread) will result in + * so-called group abandonment where a group will only contain one element and the group will be + * re-created over and over as new upstream items trigger a new group. The behavior is + * a trade-off between no-dataloss, upstream cancellation and excessive group creation. + * + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The consumer of the returned {@code Flowable} has to be ready to receive new {@code GroupedFlowable}s or else + * this operator will signal {@link MissingBackpressureException}. To avoid this exception, make + * sure a combining operator (such as {@code flatMap}) has adequate amount of buffering/prefetch configured. + * The inner {@code GroupedFlowable}s honor backpressure but due to the single-source multiple consumer + * nature of this operator, each group must be consumed so the whole operator can make progress and not hang.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code groupBy} does not operate by default on a particular {@link Scheduler}.</dd> + * <dt><b>Error handling:</b></dt> + * <dd>If the upstream signals or the callback(s) throw an exception, the returned {@code Flowable} and + * all active inner {@code GroupedFlowable}s will signal the same exception.</dd> + * </dl> + * + * @param keySelector + * a function that extracts the key for each item + * @param valueSelector + * a function that extracts the return element for each item + * @param <K> + * the key type + * @param <V> + * the element type + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code keySelector} or {@code valueSelector} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/groupby.html">ReactiveX operators documentation: GroupBy</a> + * @see #groupBy(Function, Function, boolean) + * @see #groupBy(Function, Function, boolean, int) + * @see #groupBy(Function, Function, boolean, int, Function) + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.ERROR) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull K, @NonNull V> Flowable<GroupedFlowable<K, V>> groupBy(@NonNull Function<? super T, ? extends K> keySelector, + @NonNull Function<? super T, ? extends V> valueSelector) { + return groupBy(keySelector, valueSelector, false, bufferSize()); + } + + /** + * Groups the items emitted by the current {@code Flowable} according to a specified criterion, and emits these + * grouped items as {@link GroupedFlowable}s. The emitted {@code GroupedFlowable} allows only a single + * {@link Subscriber} during its lifetime and if this {@code Subscriber} cancels before the + * source terminates, the next emission by the source having the same key will trigger a new + * {@code GroupedFlowable} emission. + * <p> + * <img width="640" height="360" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/groupBy.v3.png" alt=""> + * <p> + * <em>Note:</em> A {@code GroupedFlowable} will cache the items it is to emit until such time as it + * is subscribed to. For this reason, in order to avoid memory leaks, you should not simply ignore those + * {@code GroupedFlowable}s that do not concern you. Instead, you can signal to them that they may + * discard their buffers by applying an operator like {@link #ignoreElements} to them. + * <p> + * Note that the {@code GroupedFlowable}s should be subscribed to as soon as possible, otherwise, + * the unconsumed groups may starve other groups due to the internal backpressure + * coordination of the {@code groupBy} operator. Such hangs can be usually avoided by using + * {@link #flatMap(Function, int)} or {@link #concatMapEager(Function, int, int)} and overriding the default maximum concurrency + * value to be greater or equal to the expected number of groups, possibly using + * {@link Integer#MAX_VALUE} if the number of expected groups is unknown. + * <p> + * Note also that ignoring groups or subscribing later (i.e., on another thread) will result in + * so-called group abandonment where a group will only contain one element and the group will be + * re-created over and over as new upstream items trigger a new group. The behavior is + * a trade-off between no-dataloss, upstream cancellation and excessive group creation. + * + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The consumer of the returned {@code Flowable} has to be ready to receive new {@code GroupedFlowable}s or else + * this operator will signal {@link MissingBackpressureException}. To avoid this exception, make + * sure a combining operator (such as {@code flatMap}) has adequate amount of buffering/prefetch configured. + * The inner {@code GroupedFlowable}s honor backpressure but due to the single-source multiple consumer + * nature of this operator, each group must be consumed so the whole operator can make progress and not hang.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code groupBy} does not operate by default on a particular {@link Scheduler}.</dd> + * <dt><b>Error handling:</b></dt> + * <dd>If the upstream signals or the callback(s) throw an exception, the returned {@code Flowable} and + * all active inner {@code GroupedFlowable}s will signal the same exception.</dd> + * </dl> + * + * @param keySelector + * a function that extracts the key for each item + * @param valueSelector + * a function that extracts the return element for each item + * @param <K> + * the key type + * @param <V> + * the element type + * @param delayError + * if {@code true}, the exception from the current {@code Flowable} is delayed in each group until that specific group emitted + * the normal values; if {@code false}, the exception bypasses values in the groups and is reported immediately. + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code keySelector} or {@code valueSelector} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/groupby.html">ReactiveX operators documentation: GroupBy</a> + * @see #groupBy(Function, Function, boolean, int) + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.ERROR) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull K, @NonNull V> Flowable<GroupedFlowable<K, V>> groupBy(@NonNull Function<? super T, ? extends K> keySelector, + @NonNull Function<? super T, ? extends V> valueSelector, boolean delayError) { + return groupBy(keySelector, valueSelector, delayError, bufferSize()); + } + + /** + * Groups the items emitted by the current {@code Flowable} according to a specified criterion, and emits these + * grouped items as {@link GroupedFlowable}s. The emitted {@code GroupedFlowable} allows only a single + * {@link Subscriber} during its lifetime and if this {@code Subscriber} cancels before the + * source terminates, the next emission by the source having the same key will trigger a new + * {@code GroupedFlowable} emission. + * <p> + * <img width="640" height="360" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/groupBy.v3.png" alt=""> + * <p> + * <em>Note:</em> A {@code GroupedFlowable} will cache the items it is to emit until such time as it + * is subscribed to. For this reason, in order to avoid memory leaks, you should not simply ignore those + * {@code GroupedFlowable}s that do not concern you. Instead, you can signal to them that they may + * discard their buffers by applying an operator like {@link #ignoreElements} to them. + * <p> + * Note that the {@code GroupedFlowable}s should be subscribed to as soon as possible, otherwise, + * the unconsumed groups may starve other groups due to the internal backpressure + * coordination of the {@code groupBy} operator. Such hangs can be usually avoided by using + * {@link #flatMap(Function, int)} or {@link #concatMapEager(Function, int, int)} and overriding the default maximum concurrency + * value to be greater or equal to the expected number of groups, possibly using + * {@link Integer#MAX_VALUE} if the number of expected groups is unknown. + * <p> + * Note also that ignoring groups or subscribing later (i.e., on another thread) will result in + * so-called group abandonment where a group will only contain one element and the group will be + * re-created over and over as new upstream items trigger a new group. The behavior is + * a trade-off between no-dataloss, upstream cancellation and excessive group creation. + * + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The consumer of the returned {@code Flowable} has to be ready to receive new {@code GroupedFlowable}s or else + * this operator will signal {@link MissingBackpressureException}. To avoid this exception, make + * sure a combining operator (such as {@code flatMap}) has adequate amount of buffering/prefetch configured. + * The inner {@code GroupedFlowable}s honor backpressure but due to the single-source multiple consumer + * nature of this operator, each group must be consumed so the whole operator can make progress and not hang.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code groupBy} does not operate by default on a particular {@link Scheduler}.</dd> + * <dt><b>Error handling:</b></dt> + * <dd>If the upstream signals or the callback(s) throw an exception, the returned {@code Flowable} and + * all active inner {@code GroupedFlowable}s will signal the same exception.</dd> + * </dl> + * + * @param keySelector + * a function that extracts the key for each item + * @param valueSelector + * a function that extracts the return element for each item + * @param delayError + * if {@code true}, the exception from the current {@code Flowable} is delayed in each group until that specific group emitted + * the normal values; if {@code false}, the exception bypasses values in the groups and is reported immediately. + * @param bufferSize + * the hint for how many {@code GroupedFlowable}s and element in each {@code GroupedFlowable} should be buffered + * @param <K> + * the key type + * @param <V> + * the element type + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code keySelector} or {@code valueSelector} is {@code null} + * @throws IllegalArgumentException if {@code bufferSize} is non-positive + * @see <a href="http://reactivex.io/documentation/operators/groupby.html">ReactiveX operators documentation: GroupBy</a> + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.SPECIAL) + @SchedulerSupport(SchedulerSupport.NONE) + public final <@NonNull K, @NonNull V> Flowable<GroupedFlowable<K, V>> groupBy(@NonNull Function<? super T, ? extends K> keySelector, + @NonNull Function<? super T, ? extends V> valueSelector, + boolean delayError, int bufferSize) { + Objects.requireNonNull(keySelector, "keySelector is null"); + Objects.requireNonNull(valueSelector, "valueSelector is null"); + ObjectHelper.verifyPositive(bufferSize, "bufferSize"); + + return RxJavaPlugins.onAssembly(new FlowableGroupBy<>(this, keySelector, valueSelector, bufferSize, delayError, null)); + } + + /** + * Groups the items emitted by the current {@code Flowable} according to a specified criterion, and emits these + * grouped items as {@link GroupedFlowable}s. The emitted {@code GroupedFlowable} allows only a single + * {@link Subscriber} during its lifetime and if this {@code Subscriber} cancels before the + * source terminates, the next emission by the source having the same key will trigger a new + * {@code GroupedFlowable} emission. The {@code evictingMapFactory} is used to create a map that will + * be used to hold the {@code GroupedFlowable}s by key. The evicting map created by this factory must + * notify the provided {@code Consumer<Object>} with the entry value (not the key!) when an entry in this + * map has been evicted. The next source emission will bring about the completion of the evicted + * {@code GroupedFlowable}s and the arrival of an item with the same key as a completed {@code GroupedFlowable} + * will prompt the creation and emission of a new {@code GroupedFlowable} with that key. + * + * <p>A use case for specifying an {@code evictingMapFactory} is where the source is infinite and fast and + * over time the number of keys grows enough to be a concern in terms of the memory footprint of the + * internal hash map containing the {@code GroupedFlowable}s. + * + * <p>The map created by an {@code evictingMapFactory} must be thread-safe. + * + * <p>An example of an {@code evictingMapFactory} using <a href="https://google.github.io/guava/releases/24.0-jre/api/docs/com/google/common/cache/CacheBuilder.html">CacheBuilder</a> from the Guava library is below: + * + * <pre><code> + * Function<Consumer<Object>, Map<Integer, Object>> evictingMapFactory = + * notify -> + * CacheBuilder + * .newBuilder() + * .maximumSize(3) + * .removalListener(entry -> { + * try { + * // emit the value not the key! + * notify.accept(entry.getValue()); + * } catch (Exception e) { + * throw new RuntimeException(e); + * } + * }) + * .<Integer, Object> build() + * .asMap(); + * + * // Emit 1000 items but ensure that the + * // internal map never has more than 3 items in it + * Flowable + * .range(1, 1000) + * // note that number of keys is 10 + * .groupBy(x -> x % 10, x -> x, true, 16, evictingMapFactory) + * .flatMap(g -> g) + * .forEach(System.out::println); + * </code></pre> + * + * <p> + * <img width="640" height="360" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/groupBy.v3.png" alt=""> + * <p> + * <em>Note:</em> A {@code GroupedFlowable} will cache the items it is to emit until such time as it + * is subscribed to. For this reason, in order to avoid memory leaks, you should not simply ignore those + * {@code GroupedFlowable}s that do not concern you. Instead, you can signal to them that they may + * discard their buffers by applying an operator like {@link #ignoreElements} to them. + * <p> + * Note that the {@code GroupedFlowable}s should be subscribed to as soon as possible, otherwise, + * the unconsumed groups may starve other groups due to the internal backpressure + * coordination of the {@code groupBy} operator. Such hangs can be usually avoided by using + * {@link #flatMap(Function, int)} or {@link #concatMapEager(Function, int, int)} and overriding the default maximum concurrency + * value to be greater or equal to the expected number of groups, possibly using + * {@link Integer#MAX_VALUE} if the number of expected groups is unknown. + * <p> + * Note also that ignoring groups or subscribing later (i.e., on another thread) will result in + * so-called group abandonment where a group will only contain one element and the group will be + * re-created over and over as new upstream items trigger a new group. The behavior is + * a trade-off between no-dataloss, upstream cancellation and excessive group creation. + * + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The consumer of the returned {@code Flowable} has to be ready to receive new {@code GroupedFlowable}s or else + * this operator will signal {@link MissingBackpressureException}. To avoid this exception, make + * sure a combining operator (such as {@code flatMap}) has adequate amount of buffering/prefetch configured. + * The inner {@code GroupedFlowable}s honor backpressure but due to the single-source multiple consumer + * nature of this operator, each group must be consumed so the whole operator can make progress and not hang.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code groupBy} does not operate by default on a particular {@link Scheduler}.</dd> + * <dt><b>Error handling:</b></dt> + * <dd>If the upstream signals or the callback(s) throw an exception, the returned {@code Flowable} and + * all active inner {@code GroupedFlowable}s will signal the same exception.</dd> + * </dl> + * <p>History: 2.1.10 - beta + * @param keySelector + * a function that extracts the key for each item + * @param valueSelector + * a function that extracts the return element for each item + * @param delayError + * if {@code true}, the exception from the current {@code Flowable} is delayed in each group until that specific group emitted + * the normal values; if {@code false}, the exception bypasses values in the groups and is reported immediately. + * @param bufferSize + * the hint for how many {@code GroupedFlowable}s and element in each {@code GroupedFlowable} should be buffered + * @param evictingMapFactory + * The factory used to create a map that will be used by the implementation to hold the + * {@code GroupedFlowable}s. The evicting map created by this factory must + * notify the provided {@code Consumer<Object>} with the entry value (not the key!) when + * an entry in this map has been evicted. The next source emission will bring about the + * completion of the evicted {@code GroupedFlowable}s. See example above. + * @param <K> + * the key type + * @param <V> + * the element type + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code keySelector}, {@code valueSelector} or {@code evictingMapFactory} is {@code null} + * @throws IllegalArgumentException if {@code bufferSize} is non-positive + * @see <a href="http://reactivex.io/documentation/operators/groupby.html">ReactiveX operators documentation: GroupBy</a> + * + * @since 2.2 + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.SPECIAL) + @SchedulerSupport(SchedulerSupport.NONE) + public final <@NonNull K, @NonNull V> Flowable<GroupedFlowable<K, V>> groupBy(@NonNull Function<? super T, ? extends K> keySelector, + @NonNull Function<? super T, ? extends V> valueSelector, + boolean delayError, int bufferSize, + @NonNull Function<? super Consumer<Object>, ? extends Map<K, Object>> evictingMapFactory) { + Objects.requireNonNull(keySelector, "keySelector is null"); + Objects.requireNonNull(valueSelector, "valueSelector is null"); + ObjectHelper.verifyPositive(bufferSize, "bufferSize"); + Objects.requireNonNull(evictingMapFactory, "evictingMapFactory is null"); + + return RxJavaPlugins.onAssembly(new FlowableGroupBy<>(this, keySelector, valueSelector, bufferSize, delayError, evictingMapFactory)); + } + + /** + * Returns a {@code Flowable} that correlates two {@link Publisher}s when they overlap in time and groups the results. + * <p> + * There are no guarantees in what order the items get combined when multiple + * items from one or both source {@code Publisher}s overlap. + * <p> + * <img width="640" height="380" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/groupJoin.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator doesn't support backpressure and consumes all participating {@code Publisher}s in + * an unbounded mode (i.e., not applying any backpressure to them).</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code groupJoin} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <TRight> the value type of the right {@code Publisher} source + * @param <TLeftEnd> the element type of the left duration {@code Publisher}s + * @param <TRightEnd> the element type of the right duration {@code Publisher}s + * @param <R> the result type + * @param other + * the other {@code Publisher} to correlate items from the current {@code Flowable} with + * @param leftEnd + * a function that returns a {@code Publisher} whose emissions indicate the duration of the values of + * the current {@code Flowable} + * @param rightEnd + * a function that returns a {@code Publisher} whose emissions indicate the duration of the values of + * the {@code right} {@code Publisher} + * @param resultSelector + * a function that takes an item emitted by each {@code Publisher} and returns the value to be emitted + * by the resulting {@code Flowable} + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code other}, {@code leftEnd}, {@code rightEnd} or {@code resultSelector} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/join.html">ReactiveX operators documentation: Join</a> + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.ERROR) + @SchedulerSupport(SchedulerSupport.NONE) + public final <@NonNull TRight, @NonNull TLeftEnd, @NonNull TRightEnd, @NonNull R> Flowable<R> groupJoin( + @NonNull Publisher<? extends TRight> other, + @NonNull Function<? super T, @NonNull ? extends Publisher<TLeftEnd>> leftEnd, + @NonNull Function<? super TRight, @NonNull ? extends Publisher<TRightEnd>> rightEnd, + @NonNull BiFunction<? super T, ? super Flowable<TRight>, ? extends R> resultSelector) { + Objects.requireNonNull(other, "other is null"); + Objects.requireNonNull(leftEnd, "leftEnd is null"); + Objects.requireNonNull(rightEnd, "rightEnd is null"); + Objects.requireNonNull(resultSelector, "resultSelector is null"); + return RxJavaPlugins.onAssembly(new FlowableGroupJoin<>( + this, other, leftEnd, rightEnd, resultSelector)); + } + + /** + * Hides the identity of this {@code Flowable} and its {@link Subscription}. + * <p>Allows hiding extra features such as {@link Processor}'s + * {@link Subscriber} methods or preventing certain identity-based + * optimizations (fusion). + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator is a pass-through for backpressure, the behavior is determined by the upstream's + * backpressure behavior.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code hide} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @return the new {@code Flowable} instance + * + * @since 2.0 + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.PASS_THROUGH) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Flowable<T> hide() { + return RxJavaPlugins.onAssembly(new FlowableHide<>(this)); + } + + /** + * Ignores all items emitted by the current {@code Flowable} and only calls {@code onComplete} or {@code onError}. + * <p> + * <img width="640" height="305" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/ignoreElements.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>This operator ignores backpressure as it doesn't emit any elements and consumes the current {@code Flowable} + * in an unbounded manner (i.e., no backpressure is applied to it).</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code ignoreElements} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @return the new {@link Completable} instance + * @see <a href="http://reactivex.io/documentation/operators/ignoreelements.html">ReactiveX operators documentation: IgnoreElements</a> + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.UNBOUNDED_IN) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Completable ignoreElements() { + return RxJavaPlugins.onAssembly(new FlowableIgnoreElementsCompletable<>(this)); + } + + /** + * Returns a {@link Single} that emits {@code true} if the current {@code Flowable} is empty, otherwise {@code false}. + * <p> + * In Rx.Net this is negated as the {@code any} {@link Subscriber} but we renamed this in RxJava to better match Java + * naming idioms. + * <p> + * <img width="640" height="320" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/isEmpty.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream and consumes the current {@code Flowable} in an + * unbounded manner (i.e., without applying backpressure).</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code isEmpty} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @return the new {@code Single} instance + * @see <a href="http://reactivex.io/documentation/operators/contains.html">ReactiveX operators documentation: Contains</a> + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.UNBOUNDED_IN) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Single<Boolean> isEmpty() { + return all(Functions.alwaysFalse()); + } + + /** + * Correlates the items emitted by two {@link Publisher}s based on overlapping durations. + * <p> + * There are no guarantees in what order the items get combined when multiple + * items from one or both source {@code Publisher}s overlap. + * <p> + * <img width="640" height="380" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/join_.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator doesn't support backpressure and consumes all participating {@code Publisher}s in + * an unbounded mode (i.e., not applying any backpressure to them).</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code join} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <TRight> the value type of the right {@code Publisher} source + * @param <TLeftEnd> the element type of the left duration {@code Publisher}s + * @param <TRightEnd> the element type of the right duration {@code Publisher}s + * @param <R> the result type + * @param other + * the second {@code Publisher} to join items from + * @param leftEnd + * a function to select a duration for each item emitted by the current {@code Flowable}, used to + * determine overlap + * @param rightEnd + * a function to select a duration for each item emitted by the {@code right} {@code Publisher}, used to + * determine overlap + * @param resultSelector + * a function that computes an item to be emitted by the resulting {@code Flowable} for any two + * overlapping items emitted by the two {@code Publisher}s + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code other}, {@code leftEnd}, {@code rightEnd} or {@code resultSelector} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/join.html">ReactiveX operators documentation: Join</a> + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.ERROR) + @SchedulerSupport(SchedulerSupport.NONE) + public final <@NonNull TRight, @NonNull TLeftEnd, @NonNull TRightEnd, @NonNull R> Flowable<R> join( + @NonNull Publisher<? extends TRight> other, + @NonNull Function<? super T, @NonNull ? extends Publisher<TLeftEnd>> leftEnd, + @NonNull Function<? super TRight, @NonNull ? extends Publisher<TRightEnd>> rightEnd, + @NonNull BiFunction<? super T, ? super TRight, ? extends R> resultSelector) { + Objects.requireNonNull(other, "other is null"); + Objects.requireNonNull(leftEnd, "leftEnd is null"); + Objects.requireNonNull(rightEnd, "rightEnd is null"); + Objects.requireNonNull(resultSelector, "resultSelector is null"); + return RxJavaPlugins.onAssembly(new FlowableJoin<T, TRight, TLeftEnd, TRightEnd, R>( + this, other, leftEnd, rightEnd, resultSelector)); + } + + /** + * Returns a {@link Maybe} that emits the last item emitted by this {@code Flowable} or completes if + * this {@code Flowable} is empty. + * <p> + * <img width="640" height="305" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/last.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream and consumes the current {@code Flowable} in an + * unbounded manner (i.e., without applying backpressure).</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code lastElement} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @return the new {@code Maybe} instance + * @see <a href="http://reactivex.io/documentation/operators/last.html">ReactiveX operators documentation: Last</a> + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.UNBOUNDED_IN) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Maybe<T> lastElement() { + return RxJavaPlugins.onAssembly(new FlowableLastMaybe<>(this)); + } + + /** + * Returns a {@link Single} that emits only the last item emitted by this {@code Flowable}, or a default item + * if this {@code Flowable} completes without emitting any items. + * <p> + * <img width="640" height="305" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/lastOrDefault.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream and consumes the current {@code Flowable} in an + * unbounded manner (i.e., without applying backpressure).</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code last} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param defaultItem + * the default item to emit if the current {@code Flowable} is empty + * @return the new {@code Single} instance + * @throws NullPointerException if {@code defaultItem} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/last.html">ReactiveX operators documentation: Last</a> + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.UNBOUNDED_IN) + @SchedulerSupport(SchedulerSupport.NONE) + public final Single<T> last(@NonNull T defaultItem) { + Objects.requireNonNull(defaultItem, "defaultItem is null"); + return RxJavaPlugins.onAssembly(new FlowableLastSingle<>(this, defaultItem)); + } + + /** + * Returns a {@link Single} that emits only the last item emitted by this {@code Flowable} or signals + * a {@link NoSuchElementException} if this {@code Flowable} is empty. + * <p> + * <img width="640" height="236" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/lastOrError.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream and consumes the current {@code Flowable} in an + * unbounded manner (i.e., without applying backpressure).</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code lastOrError} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @return the new {@code Single} instance + * @see <a href="http://reactivex.io/documentation/operators/last.html">ReactiveX operators documentation: Last</a> + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.UNBOUNDED_IN) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Single<T> lastOrError() { + return RxJavaPlugins.onAssembly(new FlowableLastSingle<>(this, null)); + } + + /** + * <strong>This method requires advanced knowledge about building operators, please consider + * other standard composition methods first;</strong> + * Returns a {@code Flowable} which, when subscribed to, invokes the {@link FlowableOperator#apply(Subscriber) apply(Subscriber)} method + * of the provided {@link FlowableOperator} for each individual downstream {@link Subscriber} and allows the + * insertion of a custom operator by accessing the downstream's {@code Subscriber} during this subscription phase + * and providing a new {@code Subscriber}, containing the custom operator's intended business logic, that will be + * used in the subscription process going further upstream. + * <p> + * Generally, such a new {@code Subscriber} will wrap the downstream's {@code Subscriber} and forwards the + * {@code onNext}, {@code onError} and {@code onComplete} events from the upstream directly or according to the + * emission pattern the custom operator's business logic requires. In addition, such operator can intercept the + * flow control calls of {@code cancel} and {@code request} that would have traveled upstream and perform + * additional actions depending on the same business logic requirements. + * <p> + * Example: + * <pre><code> + * // Step 1: Create the consumer type that will be returned by the FlowableOperator.apply(): + * + * public final class CustomSubscriber<T> implements FlowableSubscriber<T>, Subscription { + * + * // The downstream's Subscriber that will receive the onXXX events + * final Subscriber<? super String> downstream; + * + * // The connection to the upstream source that will call this class' onXXX methods + * Subscription upstream; + * + * // The constructor takes the downstream subscriber and usually any other parameters + * public CustomSubscriber(Subscriber<? super String> downstream) { + * this.downstream = downstream; + * } + * + * // In the subscription phase, the upstream sends a Subscription to this class + * // and subsequently this class has to send a Subscription to the downstream. + * // Note that relaying the upstream's Subscription instance directly is not allowed in RxJava + * @Override + * public void onSubscribe(Subscription s) { + * if (upstream != null) { + * s.cancel(); + * } else { + * upstream = s; + * downstream.onSubscribe(this); + * } + * } + * + * // The upstream calls this with the next item and the implementation's + * // responsibility is to emit an item to the downstream based on the intended + * // business logic, or if it can't do so for the particular item, + * // request more from the upstream + * @Override + * public void onNext(T item) { + * String str = item.toString(); + * if (str.length() < 2) { + * downstream.onNext(str); + * } else { + * upstream.request(1); + * } + * } + * + * // Some operators may handle the upstream's error while others + * // could just forward it to the downstream. + * @Override + * public void onError(Throwable throwable) { + * downstream.onError(throwable); + * } + * + * // When the upstream completes, usually the downstream should complete as well. + * @Override + * public void onComplete() { + * downstream.onComplete(); + * } + * + * // Some operators have to intercept the downstream's request calls to trigger + * // the emission of queued items while others can simply forward the request + * // amount as is. + * @Override + * public void request(long n) { + * upstream.request(n); + * } + * + * // Some operators may use their own resources which should be cleaned up if + * // the downstream cancels the flow before it completed. Operators without + * // resources can simply forward the cancellation to the upstream. + * // In some cases, a canceled flag may be set by this method so that other parts + * // of this class may detect the cancellation and stop sending events + * // to the downstream. + * @Override + * public void cancel() { + * upstream.cancel(); + * } + * } + * + * // Step 2: Create a class that implements the FlowableOperator interface and + * // returns the custom consumer type from above in its apply() method. + * // Such class may define additional parameters to be submitted to + * // the custom consumer type. + * + * final class CustomOperator<T> implements FlowableOperator<String> { + * @Override + * public Subscriber<? super String> apply(Subscriber<? super T> upstream) { + * return new CustomSubscriber<T>(upstream); + * } + * } + * + * // Step 3: Apply the custom operator via lift() in a flow by creating an instance of it + * // or reusing an existing one. + * + * Flowable.range(5, 10) + * .lift(new CustomOperator<Integer>()) + * .test() + * .assertResult("5", "6", "7", "8", "9"); + * </code></pre> + * <p> + * Creating custom operators can be complicated and it is recommended one consults the + * <a href="https://github.com/ReactiveX/RxJava/wiki/Writing-operators-for-2.0">RxJava wiki: Writing operators</a> page about + * the tools, requirements, rules, considerations and pitfalls of implementing them. + * <p> + * Note that implementing custom operators via this {@code lift()} method adds slightly more overhead by requiring + * an additional allocation and indirection per assembled flows. Instead, extending the abstract {@code Flowable} + * class and creating a {@link FlowableTransformer} with it is recommended. + * <p> + * Note also that it is not possible to stop the subscription phase in {@code lift()} as the {@code apply()} method + * requires a non-{@code null} {@code Subscriber} instance to be returned, which is then unconditionally subscribed to + * the upstream {@code Flowable}. For example, if the operator decided there is no reason to subscribe to the + * upstream source because of some optimization possibility or a failure to prepare the operator, it still has to + * return a {@code Subscriber} that should immediately cancel the upstream's {@link Subscription} in its + * {@code onSubscribe} method. Again, using a {@code FlowableTransformer} and extending the {@code Flowable} is + * a better option as {@link #subscribeActual} can decide to not subscribe to its upstream after all. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The {@code Subscriber} instance returned by the {@code FlowableOperator} is responsible to be + * backpressure-aware or document the fact that the consumer of the returned {@link Publisher} has to apply one of + * the {@code onBackpressureXXX} operators.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code lift} does not operate by default on a particular {@link Scheduler}, however, the + * {@code FlowableOperator} may use a {@code Scheduler} to support its own asynchronous behavior.</dd> + * </dl> + * + * @param <R> the output value type + * @param lifter the {@code FlowableOperator} that receives the downstream's {@code Subscriber} and should return + * a {@code Subscriber} with custom behavior to be used as the consumer for the current + * {@code Flowable}. + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code lifter} is {@code null} + * @see <a href="https://github.com/ReactiveX/RxJava/wiki/Writing-operators-for-2.0">RxJava wiki: Writing operators</a> + * @see #compose(FlowableTransformer) + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.SPECIAL) + @SchedulerSupport(SchedulerSupport.NONE) + public final <@NonNull R> Flowable<R> lift(@NonNull FlowableOperator<? extends R, ? super T> lifter) { + Objects.requireNonNull(lifter, "lifter is null"); + return RxJavaPlugins.onAssembly(new FlowableLift<>(this, lifter)); + } + + /** + * Returns a {@code Flowable} that applies a specified function to each item emitted by the current {@code Flowable} and + * emits the results of these function applications. + * <p> + * <img width="640" height="305" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/map.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator doesn't interfere with backpressure which is determined by the current {@code Flowable}'s backpressure + * behavior.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code map} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <R> the output type + * @param mapper + * a function to apply to each item emitted by the current {@code Flowable} + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code mapper} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/map.html">ReactiveX operators documentation: Map</a> + * @see #mapOptional(Function) + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.PASS_THROUGH) + @SchedulerSupport(SchedulerSupport.NONE) + public final <@NonNull R> Flowable<R> map(@NonNull Function<? super T, ? extends R> mapper) { + Objects.requireNonNull(mapper, "mapper is null"); + return RxJavaPlugins.onAssembly(new FlowableMap<>(this, mapper)); + } + + /** + * Returns a {@code Flowable} that represents all of the emissions <em>and</em> notifications from the current + * {@code Flowable} into emissions marked with their original types within {@link Notification} objects. + * <p> + * <img width="640" height="315" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/materialize.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream and expects it from the current {@code Flowable}. + * If this expectation is violated, the operator <em>may</em> throw an {@link IllegalStateException}.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code materialize} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @return the new {@code Flowable} instance + * @see <a href="http://reactivex.io/documentation/operators/materialize-dematerialize.html">ReactiveX operators documentation: Materialize</a> + * @see #dematerialize(Function) + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Flowable<Notification<T>> materialize() { + return RxJavaPlugins.onAssembly(new FlowableMaterialize<>(this)); + } + + /** + * Flattens this and another {@link Publisher} into a single {@code Publisher}, without any transformation. + * <p> + * <img width="640" height="380" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/merge.v3.png" alt=""> + * <p> + * You can combine items emitted by multiple {@code Publisher}s so that they appear as a single {@code Publisher}, by + * using the {@code mergeWith} method. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream. This and the other {@code Publisher}s are expected to honor + * backpressure; if violated, the operator <em>may</em> signal {@link MissingBackpressureException}.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code mergeWith} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param other + * a {@code Publisher} to be merged + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code other} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/merge.html">ReactiveX operators documentation: Merge</a> + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + public final Flowable<T> mergeWith(@NonNull Publisher<? extends T> other) { + Objects.requireNonNull(other, "other is null"); + return merge(this, other); + } + + /** + * Merges the sequence of items of this {@code Flowable} with the success value of the other {@link SingleSource}. + * <p> + * <img width="640" height="380" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/merge.v3.png" alt=""> + * <p> + * The success value of the other {@code SingleSource} can get interleaved at any point of this + * {@code Flowable} sequence. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream and ensures the success item from the + * {@code SingleSource} is emitted only when there is a downstream demand.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code mergeWith} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * <p>History: 2.1.10 - experimental + * @param other the {@code SingleSource} whose success value to merge with + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code other} is {@code null} + * @since 2.2 + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + public final Flowable<T> mergeWith(@NonNull SingleSource<? extends T> other) { + Objects.requireNonNull(other, "other is null"); + return RxJavaPlugins.onAssembly(new FlowableMergeWithSingle<>(this, other)); + } + + /** + * Merges the sequence of items of this {@code Flowable} with the success value of the other {@link MaybeSource} + * or waits for both to complete normally if the {@code MaybeSource} is empty. + * <p> + * <img width="640" height="380" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/merge.v3.png" alt=""> + * <p> + * The success value of the other {@code MaybeSource} can get interleaved at any point of this + * {@code Flowable} sequence. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream and ensures the success item from the + * {@code MaybeSource} is emitted only when there is a downstream demand.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code mergeWith} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * <p>History: 2.1.10 - experimental + * @param other the {@code MaybeSource} which provides a success value to merge with or completes + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code other} is {@code null} + * @since 2.2 + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + public final Flowable<T> mergeWith(@NonNull MaybeSource<? extends T> other) { + Objects.requireNonNull(other, "other is null"); + return RxJavaPlugins.onAssembly(new FlowableMergeWithMaybe<>(this, other)); + } + + /** + * Relays the items of this {@code Flowable} and completes only when the other {@link CompletableSource} completes + * as well. + * <p> + * <img width="640" height="380" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/merge.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator doesn't interfere with backpressure which is determined by the current {@code Flowable}'s backpressure + * behavior.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code mergeWith} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * <p>History: 2.1.10 - experimental + * @param other the {@code CompletableSource} to await for completion + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code other} is {@code null} + * @since 2.2 + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.PASS_THROUGH) + @SchedulerSupport(SchedulerSupport.NONE) + public final Flowable<T> mergeWith(@NonNull CompletableSource other) { + Objects.requireNonNull(other, "other is null"); + return RxJavaPlugins.onAssembly(new FlowableMergeWithCompletable<>(this, other)); + } + + /** + * Signals the items and terminal signals of the current {@code Flowable} on the specified {@link Scheduler}, + * asynchronously with a bounded buffer of {@link #bufferSize()} slots. + * + * <p>Note that {@code onError} notifications will cut ahead of {@code onNext} notifications on the emission thread if {@code Scheduler} is truly + * asynchronous. If strict event ordering is required, consider using the {@link #observeOn(Scheduler, boolean)} overload. + * <p> + * <img width="640" height="308" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/observeOn.v3.png" alt=""> + * <p> + * This operator keeps emitting as many signals as it can on the given {@code Scheduler}'s Worker thread, + * which may result in a longer than expected occupation of this thread. In other terms, + * it does not allow per-signal fairness in case the worker runs on a shared underlying thread. + * If such fairness and signal/work interleaving is preferred, use the delay operator with zero time instead. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>This operator honors backpressure from downstream and expects it from the current {@code Flowable}. Violating this + * expectation will lead to {@link MissingBackpressureException}. This is the most common operator where the exception + * pops up; look for sources up the chain that don't support backpressure, + * such as {@link #interval(long, TimeUnit)}, {@link #timer(long, TimeUnit)}, + * {@link io.reactivex.rxjava3.processors.PublishProcessor PublishProcessor} or + * {@link io.reactivex.rxjava3.processors.BehaviorProcessor BehaviorProcessor} and apply any + * of the {@code onBackpressureXXX} operators <strong>before</strong> applying {@code observeOn} itself. + * Note also that request amounts are not preserved between the immediate downstream and the + * immediate upstream. The operator always requests the default {@link #bufferSize()} amount first, then after + * every 75% of that amount delivered, another 75% of this default value. If preserving the request amounts + * is to be preferred over potential excess scheduler infrastructure use, consider applying + * {@link #delay(long, TimeUnit, Scheduler)} with zero time instead. + * </dd> + * <dt><b>Scheduler:</b></dt> + * <dd>You specify which {@code Scheduler} this operator will use.</dd> + * </dl> + * + * @param scheduler + * the {@code Scheduler} to notify {@link Subscriber}s on + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code scheduler} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/observeon.html">ReactiveX operators documentation: ObserveOn</a> + * @see <a href="http://www.grahamlea.com/2014/07/rxjava-threading-examples/">RxJava Threading Examples</a> + * @see #subscribeOn + * @see #observeOn(Scheduler, boolean) + * @see #observeOn(Scheduler, boolean, int) + * @see #delay(long, TimeUnit, Scheduler) + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.CUSTOM) + @NonNull + public final Flowable<T> observeOn(@NonNull Scheduler scheduler) { + return observeOn(scheduler, false, bufferSize()); + } + + /** + * Signals the items and terminal signals of the current {@code Flowable} on the specified {@link Scheduler}, + * asynchronously with a bounded buffer and optionally delays {@code onError} notifications. + * <p> + * <img width="640" height="308" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/observeOn.v3.png" alt=""> + * <p> + * This operator keeps emitting as many signals as it can on the given {@code Scheduler}'s Worker thread, + * which may result in a longer than expected occupation of this thread. In other terms, + * it does not allow per-signal fairness in case the worker runs on a shared underlying thread. + * If such fairness and signal/work interleaving is preferred, use the delay operator with zero time instead. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>This operator honors backpressure from downstream and expects it from the current {@code Flowable}. Violating this + * expectation will lead to {@link MissingBackpressureException}. This is the most common operator where the exception + * pops up; look for sources up the chain that don't support backpressure, + * such as {@link #interval(long, TimeUnit)}, {@link #timer(long, TimeUnit)}, + * {@link io.reactivex.rxjava3.processors.PublishProcessor PublishProcessor} or + * {@link io.reactivex.rxjava3.processors.BehaviorProcessor BehaviorProcessor} and apply any + * of the {@code onBackpressureXXX} operators <strong>before</strong> applying {@code observeOn} itself. + * Note also that request amounts are not preserved between the immediate downstream and the + * immediate upstream. The operator always requests the default {@link #bufferSize()} amount first, then after + * every 75% of that amount delivered, another 75% of this default value. If preserving the request amounts + * is to be preferred over potential excess scheduler infrastructure use, consider applying + * {@link #delay(long, TimeUnit, Scheduler, boolean)} with zero time instead. + * </dd> + * <dt><b>Scheduler:</b></dt> + * <dd>You specify which {@code Scheduler} this operator will use.</dd> + * </dl> + * + * @param scheduler + * the {@code Scheduler} to notify {@link Subscriber}s on + * @param delayError + * indicates if the {@code onError} notification may not cut ahead of {@code onNext} notification on the other side of the + * scheduling boundary. If {@code true}, a sequence ending in {@code onError} will be replayed in the same order as was received + * from upstream + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code scheduler} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/observeon.html">ReactiveX operators documentation: ObserveOn</a> + * @see <a href="http://www.grahamlea.com/2014/07/rxjava-threading-examples/">RxJava Threading Examples</a> + * @see #subscribeOn + * @see #observeOn(Scheduler) + * @see #observeOn(Scheduler, boolean, int) + * @see #delay(long, TimeUnit, Scheduler, boolean) + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.CUSTOM) + @NonNull + public final Flowable<T> observeOn(@NonNull Scheduler scheduler, boolean delayError) { + return observeOn(scheduler, delayError, bufferSize()); + } + + /** + * Signals the items and terminal signals of the current {@code Flowable} on the specified {@link Scheduler}, + * asynchronously with a bounded buffer of configurable size and optionally delays {@code onError} notifications. + * <p> + * <img width="640" height="308" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/observeOn.v3.png" alt=""> + * <p> + * This operator keeps emitting as many signals as it can on the given {@code Scheduler}'s Worker thread, + * which may result in a longer than expected occupation of this thread. In other terms, + * it does not allow per-signal fairness in case the worker runs on a shared underlying thread. + * If such fairness and signal/work interleaving is preferred, use the delay operator with zero time instead. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>This operator honors backpressure from downstream and expects it from the current {@code Flowable}. Violating this + * expectation will lead to {@link MissingBackpressureException}. This is the most common operator where the exception + * pops up; look for sources up the chain that don't support backpressure, + * such as {@link #interval(long, TimeUnit)}, {@link #timer(long, TimeUnit)}, + * {@link io.reactivex.rxjava3.processors.PublishProcessor PublishProcessor} or + * {@link io.reactivex.rxjava3.processors.BehaviorProcessor BehaviorProcessor} and apply any + * of the {@code onBackpressureXXX} operators <strong>before</strong> applying {@code observeOn} itself. + * Note also that request amounts are not preserved between the immediate downstream and the + * immediate upstream. The operator always requests the specified {@code bufferSize} amount first, then after + * every 75% of that amount delivered, another 75% of this specified value. If preserving the request amounts + * is to be preferred over potential excess scheduler infrastructure use, consider applying + * {@link #delay(long, TimeUnit, Scheduler, boolean)} with zero time instead. + * </dd> + * <dt><b>Scheduler:</b></dt> + * <dd>You specify which {@code Scheduler} this operator will use.</dd> + * </dl> + * + * @param scheduler + * the {@code Scheduler} to notify {@link Subscriber}s on + * @param delayError + * indicates if the {@code onError} notification may not cut ahead of {@code onNext} notification on the other side of the + * scheduling boundary. If {@code true}, a sequence ending in {@code onError} will be replayed in the same order as was received + * from upstream + * @param bufferSize the size of the buffer. + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code scheduler} is {@code null} + * @throws IllegalArgumentException if {@code bufferSize} is non-positive + * @see <a href="http://reactivex.io/documentation/operators/observeon.html">ReactiveX operators documentation: ObserveOn</a> + * @see <a href="http://www.grahamlea.com/2014/07/rxjava-threading-examples/">RxJava Threading Examples</a> + * @see #subscribeOn + * @see #observeOn(Scheduler) + * @see #observeOn(Scheduler, boolean) + * @see #delay(long, TimeUnit, Scheduler, boolean) + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.CUSTOM) + public final Flowable<T> observeOn(@NonNull Scheduler scheduler, boolean delayError, int bufferSize) { + Objects.requireNonNull(scheduler, "scheduler is null"); + ObjectHelper.verifyPositive(bufferSize, "bufferSize"); + return RxJavaPlugins.onAssembly(new FlowableObserveOn<>(this, scheduler, delayError, bufferSize)); + } + + /** + * Filters the items emitted by the current {@code Flowable}, only emitting those of the specified type. + * <p> + * <img width="640" height="310" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/ofClass.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator doesn't interfere with backpressure which is determined by the current {@code Flowable}'s backpressure + * behavior.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code ofType} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <U> the output type + * @param clazz + * the class type to filter the items emitted by the current {@code Flowable} + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code clazz} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/filter.html">ReactiveX operators documentation: Filter</a> + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.PASS_THROUGH) + @SchedulerSupport(SchedulerSupport.NONE) + public final <@NonNull U> Flowable<U> ofType(@NonNull Class<U> clazz) { + Objects.requireNonNull(clazz, "clazz is null"); + return filter(Functions.isInstanceOf(clazz)).cast(clazz); + } + + /** + * Buffers an unlimited number of items from the current {@code Flowable} and allows it to emit as fast it can while allowing the + * downstream to consume the items at its own place. + * <p> + * <img width="640" height="300" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/bp.obp.buffer.v3.png" alt=""> + * <p> + * An error from the current {@code Flowable} will cut ahead of any unconsumed item. Use {@link #onBackpressureBuffer(boolean)} + * to have the operator keep the original signal order. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream and consumes the current {@code Flowable} in an unbounded + * manner (i.e., not applying backpressure to it).</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code onBackpressureBuffer} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @return the new {@code Flowable} instance + * @see <a href="http://reactivex.io/documentation/operators/backpressure.html">ReactiveX operators documentation: backpressure operators</a> + * @see #onBackpressureBuffer(boolean) + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.UNBOUNDED_IN) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Flowable<T> onBackpressureBuffer() { + return onBackpressureBuffer(bufferSize(), false, true); + } + + /** + * Buffers an unlimited number of items from the current {@code Flowable} and allows it to emit as fast it can while allowing the + * downstream to consume the items at its own place, optionally delaying an error until all buffered items have been consumed. + * <p> + * <img width="640" height="300" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/bp.obp.buffer.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream and consumes the current {@code Flowable} in an unbounded + * manner (i.e., not applying backpressure to it).</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code onBackpressureBuffer} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param delayError + * if {@code true}, an exception from the current {@code Flowable} is delayed until all buffered elements have been + * consumed by the downstream; if {@code false}, an exception is immediately signaled to the downstream, skipping + * any buffered element + * @return the new {@code Flowable} instance + * @see <a href="http://reactivex.io/documentation/operators/backpressure.html">ReactiveX operators documentation: backpressure operators</a> + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.UNBOUNDED_IN) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Flowable<T> onBackpressureBuffer(boolean delayError) { + return onBackpressureBuffer(bufferSize(), delayError, true); + } + + /** + * Buffers an limited number of items from the current {@code Flowable} and allows it to emit as fast it can while allowing the + * downstream to consume the items at its own place, however, the resulting {@code Flowable} will signal a + * {@link MissingBackpressureException} via {@code onError} as soon as the buffer's capacity is exceeded, dropping all undelivered + * items, and canceling the flow. + * <p> + * <img width="640" height="300" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/bp.obp.buffer.v3.png" alt=""> + * <p> + * An error from the current {@code Flowable} will cut ahead of any unconsumed item. Use {@link #onBackpressureBuffer(int, boolean)} + * to have the operator keep the original signal order. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream and consumes the current {@code Flowable} in an unbounded + * manner (i.e., not applying backpressure to it).</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code onBackpressureBuffer} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param capacity number of slots available in the buffer. + * @return the new {@code Flowable} instance + * @throws IllegalArgumentException if {@code capacity} is non-positive + * @see <a href="http://reactivex.io/documentation/operators/backpressure.html">ReactiveX operators documentation: backpressure operators</a> + * @since 1.1.0 + * @see #onBackpressureBuffer(long, Action, BackpressureOverflowStrategy) + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.ERROR) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Flowable<T> onBackpressureBuffer(int capacity) { + return onBackpressureBuffer(capacity, false, false); + } + + /** + * Buffers an limited number of items from the current {@code Flowable} and allows it to emit as fast it can while allowing the + * downstream to consume the items at its own place, however, the resulting {@code Flowable} will signal a + * {@link MissingBackpressureException} via {@code onError} as soon as the buffer's capacity is exceeded, dropping all undelivered + * items, and canceling the flow. + * <p> + * <img width="640" height="300" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/bp.obp.buffer.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream and consumes the current {@code Flowable} in an unbounded + * manner (i.e., not applying backpressure to it).</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code onBackpressureBuffer} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param capacity number of slots available in the buffer. + * @param delayError + * if {@code true}, an exception from the current {@code Flowable} is delayed until all buffered elements have been + * consumed by the downstream; if {@code false}, an exception is immediately signaled to the downstream, skipping + * any buffered element + * @return the new {@code Flowable} instance + * @throws IllegalArgumentException if {@code capacity} is non-positive + * @see <a href="http://reactivex.io/documentation/operators/backpressure.html">ReactiveX operators documentation: backpressure operators</a> + * @since 1.1.0 + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.ERROR) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Flowable<T> onBackpressureBuffer(int capacity, boolean delayError) { + return onBackpressureBuffer(capacity, delayError, false); + } + + /** + * Buffers an optionally unlimited number of items from the current {@code Flowable} and allows it to emit as fast it can while allowing the + * downstream to consume the items at its own place. + * If {@code unbounded} is {@code true}, the resulting {@code Flowable} will signal a + * {@link MissingBackpressureException} via {@code onError} as soon as the buffer's capacity is exceeded, dropping all undelivered + * items, and canceling the flow. + * <p> + * <img width="640" height="300" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/bp.obp.buffer.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream and consumes the current {@code Flowable} in an unbounded + * manner (i.e., not applying backpressure to it).</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code onBackpressureBuffer} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param capacity number of slots available in the buffer. + * @param delayError + * if {@code true}, an exception from the current {@code Flowable} is delayed until all buffered elements have been + * consumed by the downstream; if {@code false}, an exception is immediately signaled to the downstream, skipping + * any buffered element + * @param unbounded + * if {@code true}, the capacity value is interpreted as the internal "island" size of the unbounded buffer + * @return the new {@code Flowable} instance + * @throws IllegalArgumentException if {@code capacity} is non-positive + * @see <a href="http://reactivex.io/documentation/operators/backpressure.html">ReactiveX operators documentation: backpressure operators</a> + * @since 1.1.0 + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.SPECIAL) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Flowable<T> onBackpressureBuffer(int capacity, boolean delayError, boolean unbounded) { + ObjectHelper.verifyPositive(capacity, "capacity"); + return RxJavaPlugins.onAssembly(new FlowableOnBackpressureBuffer<>(this, capacity, unbounded, delayError, Functions.EMPTY_ACTION, Functions.emptyConsumer())); + } + + /** + * Buffers an optionally unlimited number of items from the current {@code Flowable} and allows it to emit as fast it can while allowing the + * downstream to consume the items at its own place. + * If {@code unbounded} is {@code true}, the resulting {@code Flowable} will signal a + * {@link MissingBackpressureException} via {@code onError} as soon as the buffer's capacity is exceeded, dropping all undelivered + * items, canceling the flow and calling the {@code onOverflow} action. + * <p> + * <img width="640" height="300" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/bp.obp.buffer.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream and consumes the current {@code Flowable} in an unbounded + * manner (i.e., not applying backpressure to it).</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code onBackpressureBuffer} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param capacity number of slots available in the buffer. + * @param delayError + * if {@code true}, an exception from the current {@code Flowable} is delayed until all buffered elements have been + * consumed by the downstream; if {@code false}, an exception is immediately signaled to the downstream, skipping + * any buffered element + * @param unbounded + * if {@code true}, the capacity value is interpreted as the internal "island" size of the unbounded buffer + * @param onOverflow action to execute if an item needs to be buffered, but there are no available slots. + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code onOverflow} is {@code null} + * @throws IllegalArgumentException if {@code capacity} is non-positive + * @see <a href="http://reactivex.io/documentation/operators/backpressure.html">ReactiveX operators documentation: backpressure operators</a> + * @see #onBackpressureBuffer(int, boolean, boolean, Action, Consumer) + * @since 1.1.0 + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.SPECIAL) + @SchedulerSupport(SchedulerSupport.NONE) + public final Flowable<T> onBackpressureBuffer(int capacity, boolean delayError, boolean unbounded, + @NonNull Action onOverflow) { + Objects.requireNonNull(onOverflow, "onOverflow is null"); + ObjectHelper.verifyPositive(capacity, "capacity"); + return RxJavaPlugins.onAssembly(new FlowableOnBackpressureBuffer<>(this, capacity, unbounded, delayError, onOverflow, Functions.emptyConsumer())); + } + + /** + * Buffers an optionally unlimited number of items from the current {@code Flowable} and allows it to emit as fast it can while allowing the + * downstream to consume the items at its own place. + * If {@code unbounded} is {@code true}, the resulting {@code Flowable} will signal a + * {@link MissingBackpressureException} via {@code onError} as soon as the buffer's capacity is exceeded, dropping all undelivered + * items, canceling the flow and calling the {@code onOverflow} action. + * <p> + * <img width="640" height="300" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/bp.obp.buffer.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream and consumes the current {@code Flowable} in an unbounded + * manner (i.e., not applying backpressure to it).</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code onBackpressureBuffer} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param capacity number of slots available in the buffer. + * @param delayError + * if {@code true}, an exception from the current {@code Flowable} is delayed until all buffered elements have been + * consumed by the downstream; if {@code false}, an exception is immediately signaled to the downstream, skipping + * any buffered element + * @param unbounded + * if {@code true}, the capacity value is interpreted as the internal "island" size of the unbounded buffer + * @param onOverflow action to execute if an item needs to be buffered, but there are no available slots. + * @param onDropped the {@link Consumer} to be called with the item that could not be buffered due to capacity constraints. + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code onOverflow} or {@code onDropped} is {@code null} + * @throws IllegalArgumentException if {@code capacity} is non-positive + * @see <a href="http://reactivex.io/documentation/operators/backpressure.html">ReactiveX operators documentation: backpressure operators</a> + * @since 3.1.7 + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.SPECIAL) + @SchedulerSupport(SchedulerSupport.NONE) + @Experimental + public final Flowable<T> onBackpressureBuffer(int capacity, boolean delayError, boolean unbounded, + @NonNull Action onOverflow, @NonNull Consumer<? super T> onDropped) { + Objects.requireNonNull(onOverflow, "onOverflow is null"); + Objects.requireNonNull(onDropped, "onDropped is null"); + ObjectHelper.verifyPositive(capacity, "capacity"); + return RxJavaPlugins.onAssembly(new FlowableOnBackpressureBuffer<>(this, capacity, unbounded, delayError, onOverflow, onDropped)); + } + + /** + * Buffers an limited number of items from the current {@code Flowable} and allows it to emit as fast it can while allowing the + * downstream to consume the items at its own place, however, the resulting {@code Flowable} will signal a + * {@link MissingBackpressureException} via {@code onError} as soon as the buffer's capacity is exceeded, dropping all undelivered + * items, canceling the flow and calling the {@code onOverflow} action. + * <p> + * <img width="640" height="300" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/bp.obp.buffer.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream and consumes the current {@code Flowable} in an unbounded + * manner (i.e., not applying backpressure to it).</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code onBackpressureBuffer} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param capacity number of slots available in the buffer. + * @param onOverflow action to execute if an item needs to be buffered, but there are no available slots. Null is allowed. + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code onOverflow} is {@code null} + * @throws IllegalArgumentException if {@code capacity} is non-positive + * @see <a href="http://reactivex.io/documentation/operators/backpressure.html">ReactiveX operators documentation: backpressure operators</a> + * @since 1.1.0 + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.ERROR) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Flowable<T> onBackpressureBuffer(int capacity, @NonNull Action onOverflow) { + return onBackpressureBuffer(capacity, false, false, onOverflow); + } + + /** + * Buffers an optionally unlimited number of items from the current {@code Flowable} and allows it to emit as fast it can while allowing the + * downstream to consume the items at its own place. + * The resulting {@code Flowable} will behave as determined by {@code overflowStrategy} if the buffer capacity is exceeded: + * <ul> + * <li>{@link BackpressureOverflowStrategy#ERROR} (default) will call {@code onError} dropping all undelivered items, + * canceling the source, and notifying the producer with {@code onOverflow}. </li> + * <li>{@link BackpressureOverflowStrategy#DROP_LATEST} will drop any new items emitted by the producer while + * the buffer is full, without generating any {@code onError}. Each drop will, however, invoke {@code onOverflow} + * to signal the overflow to the producer.</li> + * <li>{@link BackpressureOverflowStrategy#DROP_OLDEST} will drop the oldest items in the buffer in order to make + * room for newly emitted ones. Overflow will not generate an {@code onError}, but each drop will invoke + * {@code onOverflow} to signal the overflow to the producer.</li> + * </ul> + * + * <p> + * <img width="640" height="300" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/bp.obp.buffer.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream and consumes the current {@code Flowable} in an unbounded + * manner (i.e., not applying backpressure to it).</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code onBackpressureBuffer} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param capacity number of slots available in the buffer. + * @param onOverflow action to execute if an item needs to be buffered, but there are no available slots, {@code null} is allowed. + * @param overflowStrategy how should the resulting {@code Flowable} react to buffer overflows, {@code null} is not allowed. + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code onOverflow} or {@code overflowStrategy} is {@code null} + * @throws IllegalArgumentException if {@code capacity} is non-positive + * @see <a href="http://reactivex.io/documentation/operators/backpressure.html">ReactiveX operators documentation: backpressure operators</a> + * @see #onBackpressureBuffer(long, Action, BackpressureOverflowStrategy) + * @since 2.0 + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.SPECIAL) + @SchedulerSupport(SchedulerSupport.NONE) + public final Flowable<T> onBackpressureBuffer(long capacity, @Nullable Action onOverflow, @NonNull BackpressureOverflowStrategy overflowStrategy) { + Objects.requireNonNull(overflowStrategy, "overflowStrategy is null"); + ObjectHelper.verifyPositive(capacity, "capacity"); + return RxJavaPlugins.onAssembly(new FlowableOnBackpressureBufferStrategy<>(this, capacity, onOverflow, overflowStrategy, null)); + } + + /** + * Buffers an optionally unlimited number of items from the current {@code Flowable} and allows it to emit as fast it can while allowing the + * downstream to consume the items at its own place. + * The resulting {@code Flowable} will behave as determined by {@code overflowStrategy} if the buffer capacity is exceeded: + * <ul> + * <li>{@link BackpressureOverflowStrategy#ERROR} (default) will call {@code onError} dropping all undelivered items, + * canceling the source, and notifying the producer with {@code onOverflow}. </li> + * <li>{@link BackpressureOverflowStrategy#DROP_LATEST} will drop any new items emitted by the producer while + * the buffer is full, without generating any {@code onError}. Each drop will, however, invoke {@code onOverflow} + * to signal the overflow to the producer.</li> + * <li>{@link BackpressureOverflowStrategy#DROP_OLDEST} will drop the oldest items in the buffer in order to make + * room for newly emitted ones. Overflow will not generate an {@code onError}, but each drop will invoke + * {@code onOverflow} to signal the overflow to the producer.</li> + * </ul> + * + * <p> + * <img width="640" height="300" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/bp.obp.buffer.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream and consumes the current {@code Flowable} in an unbounded + * manner (i.e., not applying backpressure to it).</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code onBackpressureBuffer} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param capacity number of slots available in the buffer. + * @param onOverflow action to execute if an item needs to be buffered, but there are no available slots, {@code null} is allowed. + * @param overflowStrategy how should the resulting {@code Flowable} react to buffer overflows, {@code null} is not allowed. + * @param onDropped the {@link Consumer} to be called with the item that could not be buffered due to capacity constraints. + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code onOverflow}, {@code overflowStrategy} or {@code onDropped} is {@code null} + * @throws IllegalArgumentException if {@code capacity} is non-positive + * @see <a href="http://reactivex.io/documentation/operators/backpressure.html">ReactiveX operators documentation: backpressure operators</a> + * @since 3.1.7 + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.SPECIAL) + @SchedulerSupport(SchedulerSupport.NONE) + @Experimental + public final Flowable<T> onBackpressureBuffer(long capacity, @Nullable Action onOverflow, @NonNull BackpressureOverflowStrategy overflowStrategy, @NonNull Consumer<? super T> onDropped) { + Objects.requireNonNull(overflowStrategy, "overflowStrategy is null"); + Objects.requireNonNull(onDropped, "onDropped is null"); + ObjectHelper.verifyPositive(capacity, "capacity"); + return RxJavaPlugins.onAssembly(new FlowableOnBackpressureBufferStrategy<>(this, capacity, onOverflow, overflowStrategy, onDropped)); + } + /** + * Drops items from the current {@code Flowable} if the downstream is not ready to receive new items (indicated + * by a lack of {@link Subscription#request(long)} calls from it). + * <p> + * <img width="640" height="245" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/bp.obp.drop.v3.png" alt=""> + * <p> + * If the downstream request count hits 0 then the resulting {@code Flowable} will refrain from calling {@code onNext} until + * the {@link Subscriber} invokes {@code request(n)} again to increase the request count. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream and consumes the current {@code Flowable} in an unbounded + * manner (i.e., not applying backpressure to it).</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code onBackpressureDrop} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @return the new {@code Flowable} instance + * @see <a href="http://reactivex.io/documentation/operators/backpressure.html">ReactiveX operators documentation: backpressure operators</a> + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.UNBOUNDED_IN) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Flowable<T> onBackpressureDrop() { + return RxJavaPlugins.onAssembly(new FlowableOnBackpressureDrop<>(this)); + } + + /** + * Drops items from the current {@code Flowable} if the downstream is not ready to receive new items (indicated + * by a lack of {@link Subscription#request(long)} calls from it) and calls the given {@link Consumer} with such + * dropped items. + * <p> + * <img width="640" height="245" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/bp.obp.drop.v3.png" alt=""> + * <p> + * If the downstream request count hits 0 then the resulting {@code Flowable} will refrain from calling {@code onNext} until + * the {@link Subscriber} invokes {@code request(n)} again to increase the request count. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream and consumes the current {@code Flowable} in an unbounded + * manner (i.e., not applying backpressure to it).</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code onBackpressureDrop} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param onDrop the action to invoke for each item dropped, should be fast and should never block. + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code onDrop} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/backpressure.html">ReactiveX operators documentation: backpressure operators</a> + * @since 1.1.0 + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.UNBOUNDED_IN) + @SchedulerSupport(SchedulerSupport.NONE) + public final Flowable<T> onBackpressureDrop(@NonNull Consumer<? super T> onDrop) { + Objects.requireNonNull(onDrop, "onDrop is null"); + return RxJavaPlugins.onAssembly(new FlowableOnBackpressureDrop<>(this, onDrop)); + } + + /** + * Drops all but the latest item emitted by the current {@code Flowable} if the downstream is not ready to receive + * new items (indicated by a lack of {@link Subscription#request(long)} calls from it) and emits this latest + * item when the downstream becomes ready. + * <p> + * <img width="640" height="245" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/bp.obp.latest.v3.png" alt=""> + * <p> + * Its behavior is logically equivalent to {@code blockingLatest()} with the exception that + * the downstream is not blocking while requesting more values. + * <p> + * Note that if the current {@code Flowable} does support backpressure, this operator ignores that capability + * and doesn't propagate any backpressure requests from downstream. + * <p> + * Note that due to the nature of how backpressure requests are propagated through subscribeOn/observeOn, + * requesting more than 1 from downstream doesn't guarantee a continuous delivery of {@code onNext} events. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream and consumes the current {@code Flowable} in an unbounded + * manner (i.e., not applying backpressure to it).</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code onBackpressureLatest} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @return the new {@code Flowable} instance + * @since 1.1.0 + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.UNBOUNDED_IN) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Flowable<T> onBackpressureLatest() { + return RxJavaPlugins.onAssembly(new FlowableOnBackpressureLatest<>(this, null)); + } + + /** + * Drops all but the latest item emitted by the current {@code Flowable} if the downstream is not ready to receive + * new items (indicated by a lack of {@link Subscription#request(long)} calls from it) and emits this latest + * item when the downstream becomes ready. + * <p> + * <img width="640" height="245" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/bp.obp.latest.v3.png" alt=""> + * <p> + * Its behavior is logically equivalent to {@code blockingLatest()} with the exception that + * the downstream is not blocking while requesting more values. + * <p> + * Note that if the current {@code Flowable} does support backpressure, this operator ignores that capability + * and doesn't propagate any backpressure requests from downstream. + * <p> + * Note that due to the nature of how backpressure requests are propagated through subscribeOn/observeOn, + * requesting more than 1 from downstream doesn't guarantee a continuous delivery of {@code onNext} events. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream and consumes the current {@code Flowable} in an unbounded + * manner (i.e., not applying backpressure to it).</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code onBackpressureLatest} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param onDropped + * called with the current entry when it has been replaced by a new one + * @throws NullPointerException if {@code onDropped} is {@code null} + * @return the new {@code Flowable} instance + * @since 3.1.7 + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.UNBOUNDED_IN) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + @Experimental + public final Flowable<T> onBackpressureLatest(@NonNull Consumer<? super T> onDropped) { + Objects.requireNonNull(onDropped, "onDropped is null"); + return RxJavaPlugins.onAssembly(new FlowableOnBackpressureLatest<>(this, onDropped)); + } + + /** + * Reduces a sequence of two not emitted values via a function into a single value if the downstream is not ready to receive + * new items (indicated by a lack of {@link Subscription#request(long)} calls from it) and emits this latest + * item when the downstream becomes ready. + * <p> + * <img width="640" height="354" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Flowable.onBackpressureReduce.png" alt=""> + * <p> + * Note that if the current {@code Flowable} does support backpressure, this operator ignores that capability + * and doesn't propagate any backpressure requests from downstream. + * <p> + * Note that due to the nature of how backpressure requests are propagated through subscribeOn/observeOn, + * requesting more than 1 from downstream doesn't guarantee a continuous delivery of {@code onNext} events. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream and consumes the current {@code Flowable} in an unbounded + * manner (i.e., not applying backpressure to it).</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code onBackpressureReduce} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * <p>History: 3.0.9 - experimental + * @param reducer the bi-function to call when there is more than one non-emitted value to downstream, + * the first argument of the bi-function is previous item and the second one is currently + * emitting from upstream + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code reducer} is {@code null} + * @since 3.1.0 + * @see #onBackpressureReduce(Supplier, BiFunction) + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.UNBOUNDED_IN) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Flowable<T> onBackpressureReduce(@NonNull BiFunction<T, T, T> reducer) { + Objects.requireNonNull(reducer, "reducer is null"); + return RxJavaPlugins.onAssembly(new FlowableOnBackpressureReduce<>(this, reducer)); + } + + /** + * Reduces upstream values into an aggregate value, provided by a supplier and combined via a reducer function, + * while the downstream is not ready to receive items, then emits this aggregate value when the downstream becomes ready. + * <p> + * <img width="640" height="315" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Flowable.onBackpressureReduce.ff.png" alt=""> + * <p> + * Note that even if the downstream is ready to receive an item, the upstream item will always be aggregated into the output type, + * calling both the supplier and the reducer to produce the output value. + * <p> + * Note that if the current {@code Flowable} does support backpressure, this operator ignores that capability + * and doesn't propagate any backpressure requests from downstream. + * <p> + * Note that due to the nature of how backpressure requests are propagated through subscribeOn/observeOn, + * requesting more than 1 from downstream doesn't guarantee a continuous delivery of {@code onNext} events. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream and consumes the current {@code Flowable} in an unbounded + * manner (i.e., not applying backpressure to it).</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code onBackpressureReduce} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * <p>History: 3.0.9 - experimental + * @param <R> the aggregate type emitted when the downstream requests more items + * @param supplier the factory to call to create new item of type R to pass it as the first argument to {@code reducer}. + * It is called when previous returned value by {@code reducer} already sent to + * downstream or the very first update from upstream received. + * @param reducer the bi-function to call to reduce excessive updates which downstream is not ready to receive. + * The first argument of type R is the object returned by {@code supplier} or result of previous + * {@code reducer} invocation. The second argument of type T is the current update from upstream. + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code supplier} or {@code reducer} is {@code null} + * @see #onBackpressureReduce(BiFunction) + * @since 3.1.0 + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.UNBOUNDED_IN) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull R> Flowable<R> onBackpressureReduce(@NonNull Supplier<R> supplier, @NonNull BiFunction<R, ? super T, R> reducer) { + Objects.requireNonNull(supplier, "supplier is null"); + Objects.requireNonNull(reducer, "reducer is null"); + return RxJavaPlugins.onAssembly(new FlowableOnBackpressureReduceWith<>(this, supplier, reducer)); + } + + /** + * Returns a {@code Flowable} instance that if the current {@code Flowable} emits an error, it will emit an {@code onComplete} + * and swallow the throwable. + * <p> + * <img width="640" height="372" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Flowable.onErrorComplete.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator doesn't interfere with backpressure which is determined by the current {@code Flowable}'s backpressure + * behavior.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code onErrorComplete} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @return the new {@code Flowable} instance + * @since 3.0.0 + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @BackpressureSupport(BackpressureKind.PASS_THROUGH) + @NonNull + public final Flowable<T> onErrorComplete() { + return onErrorComplete(Functions.alwaysTrue()); + } + + /** + * Returns a {@code Flowable} instance that if the current {@code Flowable} emits an error and the predicate returns + * {@code true}, it will emit an {@code onComplete} and swallow the throwable. + * <p> + * <img width="640" height="201" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Flowable.onErrorComplete.f.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator doesn't interfere with backpressure which is determined by the current {@code Flowable}'s backpressure + * behavior.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code onErrorComplete} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param predicate the predicate to call when an {@link Throwable} is emitted which should return {@code true} + * if the {@code Throwable} should be swallowed and replaced with an {@code onComplete}. + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code predicate} is {@code null} + * @since 3.0.0 + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.PASS_THROUGH) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Flowable<T> onErrorComplete(@NonNull Predicate<? super Throwable> predicate) { + Objects.requireNonNull(predicate, "predicate is null"); + + return RxJavaPlugins.onAssembly(new FlowableOnErrorComplete<>(this, predicate)); + } + + /** + * Resumes the flow with a {@link Publisher} returned for the failure {@link Throwable} of the current {@code Flowable} by a + * function instead of signaling the error via {@code onError}. + * <p> + * <img width="640" height="310" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/onErrorResumeNext.v3.png" alt=""> + * <p> + * By default, when a {@code Publisher} encounters an error that prevents it from emitting the expected item to + * its {@link Subscriber}, the {@code Publisher} invokes its {@code Subscriber}'s {@code onError} method, and then quits + * without invoking any more of its {@code Subscriber}'s methods. The {@code onErrorResumeNext} method changes this + * behavior. If you pass a function that returns a {@code Publisher} ({@code resumeFunction}) to + * {@code onErrorResumeNext}, if the original {@code Publisher} encounters an error, instead of invoking its + * {@code Subscriber}'s {@code onError} method, it will instead relinquish control to the {@code Publisher} returned from + * {@code resumeFunction}, which will invoke the {@code Subscriber}'s {@link Subscriber#onNext onNext} method if it is + * able to do so. In such a case, because no {@code Publisher} necessarily invokes {@code onError}, the {@code Subscriber} + * may never know that an error happened. + * <p> + * You can use this to prevent errors from propagating or to supply fallback data should errors be + * encountered. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream. This and the resuming {@code Publisher}s + * are expected to honor backpressure as well. + * If any of them violate this expectation, the operator <em>may</em> throw an + * {@link IllegalStateException} when the current {@code Flowable} completes or + * a {@link MissingBackpressureException} is signaled somewhere downstream.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code onErrorResumeNext} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param fallbackSupplier + * a function that returns a {@code Publisher} that will take over if the current {@code Flowable} encounters + * an error + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code fallbackSupplier} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/catch.html">ReactiveX operators documentation: Catch</a> + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + public final Flowable<T> onErrorResumeNext(@NonNull Function<? super Throwable, @NonNull ? extends Publisher<? extends T>> fallbackSupplier) { + Objects.requireNonNull(fallbackSupplier, "fallbackSupplier is null"); + return RxJavaPlugins.onAssembly(new FlowableOnErrorNext<>(this, fallbackSupplier)); + } + + /** + * Resumes the flow with the given {@link Publisher} when the current {@code Flowable} fails instead of + * signaling the error via {@code onError}. + * <p> + * <img width="640" height="310" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/onErrorResumeWith.v3.png" alt=""> + * <p> + * By default, when a {@code Publisher} encounters an error that prevents it from emitting the expected item to + * its {@link Subscriber}, the {@code Publisher} invokes its {@code Subscriber}'s {@code onError} method, and then quits + * without invoking any more of its {@code Subscriber}'s methods. The {@code onErrorResumeWith} method changes this + * behavior. If you pass another {@code Publisher} ({@code resumeSequence}) to a {@code Publisher}'s + * {@code onErrorResumeWith} method, if the original {@code Publisher} encounters an error, instead of invoking its + * {@code Subscriber}'s {@code onError} method, it will instead relinquish control to {@code resumeSequence} which + * will invoke the {@code Subscriber}'s {@link Subscriber#onNext onNext} method if it is able to do so. In such a case, + * because no {@code Publisher} necessarily invokes {@code onError}, the {@code Subscriber} may never know that an error + * happened. + * <p> + * You can use this to prevent errors from propagating or to supply fallback data should errors be + * encountered. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream. This and the resuming {@code Publisher}s + * are expected to honor backpressure as well. + * If any of them violate this expectation, the operator <em>may</em> throw an + * {@link IllegalStateException} when the current {@code Flowable} completes or + * {@link MissingBackpressureException} is signaled somewhere downstream.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code onErrorResumeWith} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param fallback + * the next {@code Publisher} source that will take over if the current {@code Flowable} encounters + * an error + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code fallback} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/catch.html">ReactiveX operators documentation: Catch</a> + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + public final Flowable<T> onErrorResumeWith(@NonNull Publisher<? extends T> fallback) { + Objects.requireNonNull(fallback, "fallback is null"); + return onErrorResumeNext(Functions.justFunction(fallback)); + } + + /** + * Ends the flow with a last item returned by a function for the {@link Throwable} error signaled by the current + * {@code Flowable} instead of signaling the error via {@code onError}. + * <p> + * <img width="640" height="310" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/onErrorReturn.v3.png" alt=""> + * <p> + * By default, when a {@link Publisher} encounters an error that prevents it from emitting the expected item to + * its {@link Subscriber}, the {@code Publisher} invokes its {@code Subscriber}'s {@code onError} method, and then quits + * without invoking any more of its {@code Subscriber}'s methods. The {@code onErrorReturn} method changes this + * behavior. If you pass a function ({@code resumeFunction}) to a {@code Publisher}'s {@code onErrorReturn} + * method, if the original {@code Publisher} encounters an error, instead of invoking its {@code Subscriber}'s + * {@code onError} method, it will instead emit the return value of {@code resumeFunction}. + * <p> + * You can use this to prevent errors from propagating or to supply fallback data should errors be + * encountered. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream. The current {@code Flowable} is expected to honor + * backpressure as well. If it this expectation is violated, the operator <em>may</em> throw + * {@link IllegalStateException} when the current {@code Flowable} completes or + * {@link MissingBackpressureException} is signaled somewhere downstream.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code onErrorReturn} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param itemSupplier + * a function that returns a single value that will be emitted along with a regular {@code onComplete} in case + * the current {@code Flowable} signals an {@code onError} event + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code itemSupplier} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/catch.html">ReactiveX operators documentation: Catch</a> + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + public final Flowable<T> onErrorReturn(@NonNull Function<? super Throwable, ? extends T> itemSupplier) { + Objects.requireNonNull(itemSupplier, "itemSupplier is null"); + return RxJavaPlugins.onAssembly(new FlowableOnErrorReturn<>(this, itemSupplier)); + } + + /** + * Ends the flow with the given last item when the current {@code Flowable} fails instead of signaling the error via {@code onError}. + * <p> + * <img width="640" height="310" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/onErrorReturn.v3.png" alt=""> + * <p> + * By default, when a {@link Publisher} encounters an error that prevents it from emitting the expected item to + * its {@link Subscriber}, the {@code Publisher} invokes its {@code Subscriber}'s {@code onError} method, and then quits + * without invoking any more of its {@code Subscriber}'s methods. The {@code onErrorReturn} method changes this + * behavior. If you pass a function ({@code resumeFunction}) to a {@code Publisher}'s {@code onErrorReturn} + * method, if the original {@code Publisher} encounters an error, instead of invoking its {@code Subscriber}'s + * {@code onError} method, it will instead emit the return value of {@code resumeFunction}. + * <p> + * You can use this to prevent errors from propagating or to supply fallback data should errors be + * encountered. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream. The current {@code Flowable} is expected to honor + * backpressure as well. If it this expectation is violated, the operator <em>may</em> throw + * {@link IllegalStateException} when the current {@code Flowable} completes or + * {@link MissingBackpressureException} is signaled somewhere downstream.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code onErrorReturnItem} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param item + * the value that is emitted along with a regular {@code onComplete} in case the current + * {@code Flowable} signals an exception + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code item} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/catch.html">ReactiveX operators documentation: Catch</a> + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + public final Flowable<T> onErrorReturnItem(@NonNull T item) { + Objects.requireNonNull(item, "item is null"); + return onErrorReturn(Functions.justFunction(item)); + } + + /** + * Nulls out references to the upstream producer and downstream {@link Subscriber} if + * the sequence is terminated or downstream cancels. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator doesn't interfere with backpressure which is determined by the current {@code Flowable}'s backpressure + * behavior.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code onTerminateDetach} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @return the new {@code Flowable} instance + * the sequence is terminated or downstream cancels + * @since 2.0 + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.PASS_THROUGH) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Flowable<T> onTerminateDetach() { + return RxJavaPlugins.onAssembly(new FlowableDetach<>(this)); + } + + /** + * Parallelizes the flow by creating multiple 'rails' (equal to the number of CPUs) + * and dispatches the upstream items to them in a round-robin fashion. + * <p> + * Note that the rails don't execute in parallel on their own and one needs to + * apply {@link ParallelFlowable#runOn(Scheduler)} to specify the {@link Scheduler} where + * each rail will execute. + * <p> + * To merge the parallel 'rails' back into a single sequence, use {@link ParallelFlowable#sequential()}. + * <p> + * <img width="640" height="547" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/flowable.parallel.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator requires the upstream to honor backpressure and each 'rail' honors backpressure + * as well.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code parallel} does not operate by default on a particular {@code Scheduler}.</dd> + * </dl> + * <p>History: 2.0.5 - experimental; 2.1 - beta + * @return the new {@link ParallelFlowable} instance + * @since 2.2 + */ + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + @CheckReturnValue + @NonNull + public final ParallelFlowable<T> parallel() { + return ParallelFlowable.from(this); + } + + /** + * Parallelizes the flow by creating the specified number of 'rails' + * and dispatches the upstream items to them in a round-robin fashion. + * <p> + * Note that the rails don't execute in parallel on their own and one needs to + * apply {@link ParallelFlowable#runOn(Scheduler)} to specify the {@link Scheduler} where + * each rail will execute. + * <p> + * To merge the parallel 'rails' back into a single sequence, use {@link ParallelFlowable#sequential()}. + * <p> + * <img width="640" height="547" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/flowable.parallel.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator requires the upstream to honor backpressure and each 'rail' honors backpressure + * as well.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code parallel} does not operate by default on a particular {@code Scheduler}.</dd> + * </dl> + * <p>History: 2.0.5 - experimental; 2.1 - beta + * @param parallelism the number of 'rails' to use + * @return the new {@link ParallelFlowable} instance + * @throws IllegalArgumentException if {@code parallelism} is non-positive + * @since 2.2 + */ + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + @CheckReturnValue + @NonNull + public final ParallelFlowable<T> parallel(int parallelism) { + return ParallelFlowable.from(this, parallelism); + } + + /** + * Parallelizes the flow by creating the specified number of 'rails' + * and dispatches the upstream items to them in a round-robin fashion and + * uses the defined per-'rail' prefetch amount. + * <p> + * Note that the rails don't execute in parallel on their own and one needs to + * apply {@link ParallelFlowable#runOn(Scheduler)} to specify the {@link Scheduler} where + * each rail will execute. + * <p> + * To merge the parallel 'rails' back into a single sequence, use {@link ParallelFlowable#sequential()}. + * <p> + * <img width="640" height="547" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/flowable.parallel.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator requires the upstream to honor backpressure and each 'rail' honors backpressure + * as well.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code parallel} does not operate by default on a particular {@code Scheduler}.</dd> + * </dl> + * <p>History: 2.0.5 - experimental; 2.1 - beta + * @param parallelism the number of 'rails' to use + * @param prefetch the number of items each 'rail' should prefetch + * @return the new {@link ParallelFlowable} instance + * @throws IllegalArgumentException if {@code parallelism} or {@code prefetch} is non-positive + * @since 2.2 + */ + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + @CheckReturnValue + @NonNull + public final ParallelFlowable<T> parallel(int parallelism, int prefetch) { + return ParallelFlowable.from(this, parallelism, prefetch); + } + + /** + * Returns a {@link ConnectableFlowable}, which is a variety of {@link Publisher} that waits until its + * {@link ConnectableFlowable#connect connect} method is called before it begins emitting items to those + * {@link Subscriber}s that have subscribed to it. + * <p> + * <img width="640" height="510" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/publishConnect.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The returned {@code ConnectableFlowable} honors backpressure for each of its {@code Subscriber}s + * and expects the current {@code Flowable} to honor backpressure as well. If this expectation is violated, + * the operator will signal a {@link MissingBackpressureException} to its {@code Subscriber}s and disconnect.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code publish} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @return the new {@code ConnectableFlowable} instance + * @see <a href="http://reactivex.io/documentation/operators/publish.html">ReactiveX operators documentation: Publish</a> + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final ConnectableFlowable<T> publish() { + return publish(bufferSize()); + } + + /** + * Returns a {@code Flowable} that emits the results of invoking a specified selector on items emitted by a + * {@link ConnectableFlowable} that shares a single subscription to the underlying sequence. + * <p> + * <img width="640" height="510" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/publishConnect.f.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator expects the current {@code Flowable} to honor backpressure and if this expectation is + * violated, the operator will signal a {@link MissingBackpressureException} through the {@code Flowable} + * provided to the function. Since the {@link Publisher} returned by the {@code selector} may be + * independent of the provided {@code Flowable} to the function, the output's backpressure behavior + * is determined by this returned {@code Publisher}.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code publish} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <R> + * the type of items emitted by the resulting {@code Flowable} + * @param selector + * a function that can use the multicasted source sequence as many times as needed, without + * causing multiple subscriptions to the source sequence. {@link Subscriber}s to the given source will + * receive all notifications of the source from the time of the subscription forward. + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code selector} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/publish.html">ReactiveX operators documentation: Publish</a> + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull R> Flowable<R> publish(@NonNull Function<? super Flowable<T>, @NonNull ? extends Publisher<R>> selector) { + return publish(selector, bufferSize()); + } + + /** + * Returns a {@code Flowable} that emits the results of invoking a specified selector on items emitted by a + * {@link ConnectableFlowable} that shares a single subscription to the underlying sequence. + * <p> + * <img width="640" height="510" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/publishConnect.f.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator expects the current {@code Flowable} to honor backpressure and if this expectation is + * violated, the operator will signal a {@link MissingBackpressureException} through the {@code Flowable} + * provided to the function. Since the {@link Publisher} returned by the {@code selector} may be + * independent of the provided {@code Flowable} to the function, the output's backpressure behavior + * is determined by this returned {@code Publisher}.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code publish} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <R> + * the type of items emitted by the resulting {@code Flowable} + * @param selector + * a function that can use the multicasted source sequence as many times as needed, without + * causing multiple subscriptions to the source sequence. {@link Subscriber}s to the given source will + * receive all notifications of the source from the time of the subscription forward. + * @param prefetch + * the number of elements to prefetch from the current {@code Flowable} + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code selector} is {@code null} + * @throws IllegalArgumentException if {@code prefetch} is non-positive + * @see <a href="http://reactivex.io/documentation/operators/publish.html">ReactiveX operators documentation: Publish</a> + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + public final <@NonNull R> Flowable<R> publish(@NonNull Function<? super Flowable<T>, @NonNull ? extends Publisher<? extends R>> selector, int prefetch) { + Objects.requireNonNull(selector, "selector is null"); + ObjectHelper.verifyPositive(prefetch, "prefetch"); + return RxJavaPlugins.onAssembly(new FlowablePublishMulticast<>(this, selector, prefetch, false)); + } + + /** + * Returns a {@link ConnectableFlowable}, which is a variety of {@link Publisher} that waits until its + * {@link ConnectableFlowable#connect connect} method is called before it begins emitting items to those + * {@link Subscriber}s that have subscribed to it. + * <p> + * <img width="640" height="510" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/publishConnect.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The returned {@code ConnectableFlowable} honors backpressure for each of its {@code Subscriber}s + * and expects the current {@code Flowable} to honor backpressure as well. If this expectation is violated, + * the operator will signal a {@link MissingBackpressureException} to its {@code Subscriber}s and disconnect.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code publish} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param bufferSize + * the number of elements to prefetch from the current {@code Flowable} + * @return the new {@code ConnectableFlowable} instance + * @throws IllegalArgumentException if {@code bufferSize} is non-positive + * @see <a href="http://reactivex.io/documentation/operators/publish.html">ReactiveX operators documentation: Publish</a> + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final ConnectableFlowable<T> publish(int bufferSize) { + ObjectHelper.verifyPositive(bufferSize, "bufferSize"); + return RxJavaPlugins.onAssembly(new FlowablePublish<>(this, bufferSize)); + } + + /** + * Requests {@code n} initially from the upstream and then 75% of {@code n} subsequently + * after 75% of {@code n} values have been emitted to the downstream. + * + * <p>This operator allows preventing the downstream to trigger unbounded mode via {@code request(}{@link Long#MAX_VALUE}{@code )} + * or compensate for the per-item overhead of small and frequent requests. + * + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator expects backpressure from upstream and honors backpressure from downstream.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code rebatchRequests} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param n the initial request amount, further request will happen after 75% of this value + * @return the new {@code Flowable} instance + * @throws IllegalArgumentException if {@code n} is non-positive + * @since 2.0 + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Flowable<T> rebatchRequests(int n) { + return observeOn(ImmediateThinScheduler.INSTANCE, true, n); + } + + /** + * Returns a {@link Maybe} that applies a specified accumulator function to the first item emitted by the current + * {@code Flowable}, then feeds the result of that function along with the second item emitted by the current + * {@code Flowable} into the same function, and so on until all items have been emitted by the current and finite {@code Flowable}, + * and emits the final result from the final call to your function as its sole item. + * <p> + * <img width="640" height="320" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/reduce.v3.png" alt=""> + * <p> + * This technique, which is called "reduce" here, is sometimes called "aggregate," "fold," "accumulate," + * "compress," or "inject" in other programming contexts. Groovy, for instance, has an {@code inject} method + * that does a similar operation on lists. + * <p> + * Note that this operator requires the upstream to signal {@code onComplete} for the accumulator object to + * be emitted. Sources that are infinite and never complete will never emit anything through this + * operator and an infinite source may lead to a fatal {@link OutOfMemoryError}. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure of its downstream consumer and consumes the + * upstream source in unbounded mode.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code reduce} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param reducer + * an accumulator function to be invoked on each item emitted by the current {@code Flowable}, whose + * result will be used in the next accumulator call + * @return the new {@code Maybe} instance + * @throws NullPointerException if {@code reducer} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/reduce.html">ReactiveX operators documentation: Reduce</a> + * @see <a href="http://en.wikipedia.org/wiki/Fold_(higher-order_function)">Wikipedia: Fold (higher-order function)</a> + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.UNBOUNDED_IN) + @SchedulerSupport(SchedulerSupport.NONE) + public final Maybe<T> reduce(@NonNull BiFunction<T, T, T> reducer) { + Objects.requireNonNull(reducer, "reducer is null"); + return RxJavaPlugins.onAssembly(new FlowableReduceMaybe<>(this, reducer)); + } + + /** + * Returns a {@link Single} that applies a specified accumulator function to the first item emitted by the current + * {@code Flowable} and a specified seed value, then feeds the result of that function along with the second item + * emitted by the current {@code Flowable} into the same function, and so on until all items have been emitted by the + * current and finite {@code Flowable}, emitting the final result from the final call to your function as its sole item. + * <p> + * <img width="640" height="325" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/reduceSeed.v3.png" alt=""> + * <p> + * This technique, which is called "reduce" here, is sometimes called "aggregate," "fold," "accumulate," + * "compress," or "inject" in other programming contexts. Groovy, for instance, has an {@code inject} method + * that does a similar operation on lists. + * <p> + * Note that the {@code seed} is shared among all subscribers to the resulting {@code Flowable} + * and may cause problems if it is mutable. To make sure each subscriber gets its own value, defer + * the application of this operator via {@link #defer(Supplier)}: + * <pre><code> + * Flowable<T> source = ... + * Single.defer(() -> source.reduce(new ArrayList<>(), (list, item) -> list.add(item))); + * + * // alternatively, by using compose to stay fluent + * + * source.compose(o -> + * Flowable.defer(() -> o.reduce(new ArrayList<>(), (list, item) -> list.add(item)).toFlowable()) + * ).firstOrError(); + * + * // or, by using reduceWith instead of reduce + * + * source.reduceWith(() -> new ArrayList<>(), (list, item) -> list.add(item))); + * </code></pre> + * <p> + * Note that this operator requires the upstream to signal {@code onComplete} for the accumulator object to + * be emitted. Sources that are infinite and never complete will never emit anything through this + * operator and an infinite source may lead to a fatal {@link OutOfMemoryError}. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure of its downstream consumer and consumes the + * upstream source in unbounded mode.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code reduce} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <R> the accumulator and output value type + * @param seed + * the initial (seed) accumulator value + * @param reducer + * an accumulator function to be invoked on each item emitted by the current {@code Flowable}, the + * result of which will be used in the next accumulator call + * @return the new {@code Single} instance + * @throws NullPointerException if {@code seed} or {@code reducer} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/reduce.html">ReactiveX operators documentation: Reduce</a> + * @see <a href="http://en.wikipedia.org/wiki/Fold_(higher-order_function)">Wikipedia: Fold (higher-order function)</a> + * @see #reduceWith(Supplier, BiFunction) + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.UNBOUNDED_IN) + @SchedulerSupport(SchedulerSupport.NONE) + public final <@NonNull R> Single<R> reduce(R seed, @NonNull BiFunction<R, ? super T, R> reducer) { + Objects.requireNonNull(seed, "seed is null"); + Objects.requireNonNull(reducer, "reducer is null"); + return RxJavaPlugins.onAssembly(new FlowableReduceSeedSingle<>(this, seed, reducer)); + } + + /** + * Returns a {@link Single} that applies a specified accumulator function to the first item emitted by the current + * {@code Flowable} and a seed value derived from calling a specified {@code seedSupplier}, then feeds the result + * of that function along with the second item emitted by the current {@code Flowable} into the same function, and so on until + * all items have been emitted by the current and finite {@code Flowable}, emitting the final result from the final call to your + * function as its sole item. + * <p> + * <img width="640" height="325" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/reduceSeed.v3.png" alt=""> + * <p> + * This technique, which is called "reduce" here, is sometimes called "aggregate", "fold", "accumulate", + * "compress", or "inject" in other programming contexts. Groovy, for instance, has an {@code inject} method + * that does a similar operation on lists. + * <p> + * Note that this operator requires the upstream to signal {@code onComplete} for the accumulator object to + * be emitted. Sources that are infinite and never complete will never emit anything through this + * operator and an infinite source may lead to a fatal {@link OutOfMemoryError}. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure of its downstream consumer and consumes the + * upstream source in unbounded mode.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code reduceWith} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <R> the accumulator and output value type + * @param seedSupplier + * the {@link Supplier} that provides the initial (seed) accumulator value for each individual {@link Subscriber} + * @param reducer + * an accumulator function to be invoked on each item emitted by the current {@code Flowable}, the + * result of which will be used in the next accumulator call + * @return the new {@code Single} instance + * @throws NullPointerException if {@code seedSupplier} or {@code reducer} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/reduce.html">ReactiveX operators documentation: Reduce</a> + * @see <a href="http://en.wikipedia.org/wiki/Fold_(higher-order_function)">Wikipedia: Fold (higher-order function)</a> + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.UNBOUNDED_IN) + @SchedulerSupport(SchedulerSupport.NONE) + public final <@NonNull R> Single<R> reduceWith(@NonNull Supplier<R> seedSupplier, @NonNull BiFunction<R, ? super T, R> reducer) { + Objects.requireNonNull(seedSupplier, "seedSupplier is null"); + Objects.requireNonNull(reducer, "reducer is null"); + return RxJavaPlugins.onAssembly(new FlowableReduceWithSingle<>(this, seedSupplier, reducer)); + } + + /** + * Returns a {@code Flowable} that repeats the sequence of items emitted by the current {@code Flowable} indefinitely. + * <p> + * <img width="640" height="309" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/repeat.o.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors downstream backpressure and expects the current {@code Flowable} to honor backpressure as well. + * If this expectation is violated, the operator <em>may</em> throw an {@link IllegalStateException}.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code repeat} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @return the new {@code Flowable} instance + * @see <a href="http://reactivex.io/documentation/operators/repeat.html">ReactiveX operators documentation: Repeat</a> + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Flowable<T> repeat() { + return repeat(Long.MAX_VALUE); + } + + /** + * Returns a {@code Flowable} that repeats the sequence of items emitted by the current {@code Flowable} at most + * {@code count} times. + * <p> + * <img width="640" height="310" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/repeat.on.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors downstream backpressure and expects the current {@code Flowable} to honor backpressure as well. + * If this expectation is violated, the operator <em>may</em> throw an {@link IllegalStateException}.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code repeat} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param times + * the number of times the current {@code Flowable} items are repeated, a count of 0 will yield an empty + * sequence + * @return the new {@code Flowable} instance + * @throws IllegalArgumentException + * if {@code times} is less than zero + * @see <a href="http://reactivex.io/documentation/operators/repeat.html">ReactiveX operators documentation: Repeat</a> + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Flowable<T> repeat(long times) { + if (times < 0) { + throw new IllegalArgumentException("times >= 0 required but it was " + times); + } + if (times == 0) { + return empty(); + } + return RxJavaPlugins.onAssembly(new FlowableRepeat<>(this, times)); + } + + /** + * Returns a {@code Flowable} that repeats the sequence of items emitted by the current {@code Flowable} until + * the provided stop function returns {@code true}. + * <p> + * <img width="640" height="310" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/repeat.on.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors downstream backpressure and expects the current {@code Flowable} to honor backpressure as well. + * If this expectation is violated, the operator <em>may</em> throw an {@link IllegalStateException}.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code repeatUntil} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param stop + * a boolean supplier that is called when the current {@code Flowable} completes and unless it returns + * {@code false}, the current {@code Flowable} is resubscribed + * @return the new {@code Flowable} instance + * @throws NullPointerException + * if {@code stop} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/repeat.html">ReactiveX operators documentation: Repeat</a> + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + public final Flowable<T> repeatUntil(@NonNull BooleanSupplier stop) { + Objects.requireNonNull(stop, "stop is null"); + return RxJavaPlugins.onAssembly(new FlowableRepeatUntil<>(this, stop)); + } + + /** + * Returns a {@code Flowable} that emits the same values as the current {@code Flowable} with the exception of an + * {@code onComplete}. An {@code onComplete} notification from the source will result in the emission of + * a {@code void} item to the {@code Flowable} provided as an argument to the {@code notificationHandler} + * function. If that {@link Publisher} calls {@code onComplete} or {@code onError} then {@code repeatWhen} will + * call {@code onComplete} or {@code onError} on the child subscription. Otherwise, this {@code Publisher} will + * resubscribe to the current {@code Flowable}. + * <p> + * <img width="640" height="430" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/repeatWhen.f.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors downstream backpressure and expects the current {@code Flowable} to honor backpressure as well. + * If this expectation is violated, the operator <em>may</em> throw an {@link IllegalStateException}.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code repeatWhen} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param handler + * receives a {@code Publisher} of notifications with which a user can complete or error, aborting the repeat. + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code handler} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/repeat.html">ReactiveX operators documentation: Repeat</a> + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + public final Flowable<T> repeatWhen(@NonNull Function<? super Flowable<Object>, @NonNull ? extends Publisher<@NonNull ?>> handler) { + Objects.requireNonNull(handler, "handler is null"); + return RxJavaPlugins.onAssembly(new FlowableRepeatWhen<>(this, handler)); + } + + /** + * Returns a {@link ConnectableFlowable} that shares a single subscription to the underlying {@link Publisher} + * that will replay all of its items and notifications to any future {@link Subscriber}. A connectable + * {@code Flowable} resembles an ordinary {@code Flowable}, except that it does not begin emitting items when it is + * subscribed to, but only when its {@code connect} method is called. + * <p> + * <img width="640" height="515" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/replay.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>This operator supports backpressure. Note that the upstream requests are determined by the child + * {@code Subscriber} which requests the largest amount: i.e., two child {@code Subscriber}s with requests of 10 and 100 will + * request 100 elements from the current {@code Flowable} sequence.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>This version of {@code replay} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @return the new {@code ConnectableFlowable} instance + * @see <a href="http://reactivex.io/documentation/operators/replay.html">ReactiveX operators documentation: Replay</a> + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final ConnectableFlowable<T> replay() { + return FlowableReplay.createFrom(this); + } + + /** + * Returns a {@code Flowable} that emits items that are the results of invoking a specified selector on the items + * emitted by a {@link ConnectableFlowable} that shares a single subscription to the current {@code Flowable}. + * <p> + * <img width="640" height="450" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/replay.f.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>This operator supports backpressure. Note that the upstream requests are determined by the child + * {@link Subscriber} which requests the largest amount: i.e., two child {@code Subscriber}s with requests of 10 and 100 will + * request 100 elements from the current {@code Flowable} sequence.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>This version of {@code replay} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <R> + * the type of items emitted by the resulting {@code Flowable} + * @param selector + * the selector function, which can use the multicasted sequence as many times as needed, without + * causing multiple subscriptions to the current {@code Flowable} + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code selector} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/replay.html">ReactiveX operators documentation: Replay</a> + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + public final <@NonNull R> Flowable<R> replay(@NonNull Function<? super Flowable<T>, @NonNull ? extends Publisher<R>> selector) { + Objects.requireNonNull(selector, "selector is null"); + return FlowableReplay.multicastSelector(FlowableInternalHelper.replaySupplier(this), selector); + } + + /** + * Returns a {@code Flowable} that emits items that are the results of invoking a specified selector on items + * emitted by a {@link ConnectableFlowable} that shares a single subscription to the current {@code Flowable}, + * replaying {@code bufferSize} notifications. + * <p> + * Note that due to concurrency requirements, {@code replay(bufferSize)} may hold strong references to more than + * {@code bufferSize} source emissions. + * <p> + * <img width="640" height="440" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/replay.fn.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>This operator supports backpressure. Note that the upstream requests are determined by the child + * {@link Subscriber} which requests the largest amount: i.e., two child {@code Subscriber}s with requests of 10 and 100 will + * request 100 elements from the current {@code Flowable} sequence.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>This version of {@code replay} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <R> + * the type of items emitted by the resulting {@code Flowable} + * @param selector + * the selector function, which can use the multicasted sequence as many times as needed, without + * causing multiple subscriptions to the current {@code Flowable} + * @param bufferSize + * the buffer size that limits the number of items the operator can replay + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code selector} is {@code null} + * @throws IllegalArgumentException if {@code bufferSize} is non-positive + * @see <a href="http://reactivex.io/documentation/operators/replay.html">ReactiveX operators documentation: Replay</a> + * @see #replay(Function, int, boolean) + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + public final <@NonNull R> Flowable<R> replay(@NonNull Function<? super Flowable<T>, @NonNull ? extends Publisher<R>> selector, int bufferSize) { + Objects.requireNonNull(selector, "selector is null"); + ObjectHelper.verifyPositive(bufferSize, "bufferSize"); + return FlowableReplay.multicastSelector(FlowableInternalHelper.replaySupplier(this, bufferSize, false), selector); + } + + /** + * Returns a {@code Flowable} that emits items that are the results of invoking a specified selector on items + * emitted by a {@link ConnectableFlowable} that shares a single subscription to the current {@code Flowable}, + * replaying {@code bufferSize} notifications. + * <p> + * Note that due to concurrency requirements, {@code replay(bufferSize)} may hold strong references to more than + * {@code bufferSize} source emissions. + * <p> + * <img width="640" height="440" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/replay.fn.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>This operator supports backpressure. Note that the upstream requests are determined by the child + * {@link Subscriber} which requests the largest amount: i.e., two child {@code Subscriber}s with requests of 10 and 100 will + * request 100 elements from the current {@code Flowable} sequence.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>This version of {@code replay} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <R> + * the type of items emitted by the resulting {@code Flowable} + * @param selector + * the selector function, which can use the multicasted sequence as many times as needed, without + * causing multiple subscriptions to the current {@code Flowable} + * @param bufferSize + * the buffer size that limits the number of items the operator can replay + * @param eagerTruncate + * if {@code true}, whenever the internal buffer is truncated to the given bufferSize, the + * oldest item will be guaranteed dereferenced, thus avoiding unexpected retention + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code selector} is {@code null} + * @throws IllegalArgumentException if {@code bufferSize} is non-positive + * @see <a href="http://reactivex.io/documentation/operators/replay.html">ReactiveX operators documentation: Replay</a> + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + public final <@NonNull R> Flowable<R> replay(@NonNull Function<? super Flowable<T>, @NonNull ? extends Publisher<R>> selector, int bufferSize, boolean eagerTruncate) { + Objects.requireNonNull(selector, "selector is null"); + ObjectHelper.verifyPositive(bufferSize, "bufferSize"); + return FlowableReplay.multicastSelector(FlowableInternalHelper.replaySupplier(this, bufferSize, eagerTruncate), selector); + } + + /** + * Returns a {@code Flowable} that emits items that are the results of invoking a specified selector on items + * emitted by a {@link ConnectableFlowable} that shares a single subscription to the current {@code Flowable}, + * replaying no more than {@code bufferSize} items that were emitted within a specified time window. + * <p> + * Note that due to concurrency requirements, {@code replay(bufferSize)} may hold strong references to more than + * {@code bufferSize} source emissions. + * <p> + * <img width="640" height="445" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/replay.fnt.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>This operator supports backpressure. Note that the upstream requests are determined by the child + * {@link Subscriber} which requests the largest amount: i.e., two child {@code Subscriber}s with requests of 10 and 100 will + * request 100 elements from the current {@code Flowable} sequence.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>This version of {@code replay} operates by default on the {@code computation} {@link Scheduler}.</dd> + * </dl> + * + * @param <R> + * the type of items emitted by the resulting {@code Flowable} + * @param selector + * a selector function, which can use the multicasted sequence as many times as needed, without + * causing multiple subscriptions to the current {@code Flowable} + * @param bufferSize + * the buffer size that limits the number of items the operator can replay + * @param time + * the duration of the window in which the replayed items must have been emitted + * @param unit + * the time unit of {@code time} + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code selector} or {@code unit} is {@code null} + * @throws IllegalArgumentException if {@code bufferSize} is non-positive + * @see <a href="http://reactivex.io/documentation/operators/replay.html">ReactiveX operators documentation: Replay</a> + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.COMPUTATION) + @NonNull + public final <@NonNull R> Flowable<R> replay(@NonNull Function<? super Flowable<T>, @NonNull ? extends Publisher<R>> selector, int bufferSize, long time, @NonNull TimeUnit unit) { + return replay(selector, bufferSize, time, unit, Schedulers.computation()); + } + + /** + * Returns a {@code Flowable} that emits items that are the results of invoking a specified selector on items + * emitted by a {@link ConnectableFlowable} that shares a single subscription to the current {@code Flowable}, + * replaying no more than {@code bufferSize} items that were emitted within a specified time window. + * <p> + * Note that due to concurrency requirements, {@code replay(bufferSize)} may hold strong references to more than + * {@code bufferSize} source emissions. + * <p> + * <img width="640" height="445" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/replay.fnts.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>This operator supports backpressure. Note that the upstream requests are determined by the child + * {@link Subscriber} which requests the largest amount: i.e., two child {@code Subscriber}s with requests of 10 and 100 will + * request 100 elements from the current {@code Flowable} sequence.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>You specify which {@link Scheduler} this operator will use.</dd> + * </dl> + * + * @param <R> + * the type of items emitted by the resulting {@code Flowable} + * @param selector + * a selector function, which can use the multicasted sequence as many times as needed, without + * causing multiple subscriptions to the current {@code Flowable} + * @param bufferSize + * the buffer size that limits the number of items the operator can replay + * @param time + * the duration of the window in which the replayed items must have been emitted + * @param unit + * the time unit of {@code time} + * @param scheduler + * the {@code Scheduler} that is the time source for the window + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code selector}, {@code unit} or {@code scheduler} is {@code null} + * @throws IllegalArgumentException + * if {@code bufferSize} is non-positive + * @see <a href="http://reactivex.io/documentation/operators/replay.html">ReactiveX operators documentation: Replay</a> + * @see #replay(Function, int, long, TimeUnit, Scheduler, boolean) + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.CUSTOM) + public final <@NonNull R> Flowable<R> replay(@NonNull Function<? super Flowable<T>, @NonNull ? extends Publisher<R>> selector, int bufferSize, long time, @NonNull TimeUnit unit, @NonNull Scheduler scheduler) { + Objects.requireNonNull(selector, "selector is null"); + Objects.requireNonNull(unit, "unit is null"); + ObjectHelper.verifyPositive(bufferSize, "bufferSize"); + Objects.requireNonNull(scheduler, "scheduler is null"); + return FlowableReplay.multicastSelector( + FlowableInternalHelper.replaySupplier(this, bufferSize, time, unit, scheduler, false), selector); + } + + /** + * Returns a {@code Flowable} that emits items that are the results of invoking a specified selector on items + * emitted by a {@link ConnectableFlowable} that shares a single subscription to the current {@code Flowable}, + * replaying no more than {@code bufferSize} items that were emitted within a specified time window. + * <p> + * Note that due to concurrency requirements, {@code replay(bufferSize)} may hold strong references to more than + * {@code bufferSize} source emissions. + * <p> + * <img width="640" height="445" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/replay.fnts.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>This operator supports backpressure. Note that the upstream requests are determined by the child + * {@link Subscriber} which requests the largest amount: i.e., two child {@code Subscriber}s with requests of 10 and 100 will + * request 100 elements from the current {@code Flowable} sequence.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>You specify which {@link Scheduler} this operator will use.</dd> + * </dl> + * + * @param <R> + * the type of items emitted by the resulting {@code Flowable} + * @param selector + * a selector function, which can use the multicasted sequence as many times as needed, without + * causing multiple subscriptions to the current {@code Flowable} + * @param bufferSize + * the buffer size that limits the number of items the operator can replay + * @param time + * the duration of the window in which the replayed items must have been emitted + * @param unit + * the time unit of {@code time} + * @param scheduler + * the {@code Scheduler} that is the time source for the window + * @param eagerTruncate + * if {@code true}, whenever the internal buffer is truncated to the given bufferSize/age, the + * oldest item will be guaranteed dereferenced, thus avoiding unexpected retention + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code selector}, {@code unit} or {@code scheduler} is {@code null} + * @throws IllegalArgumentException + * if {@code bufferSize} is less than zero + * @see <a href="http://reactivex.io/documentation/operators/replay.html">ReactiveX operators documentation: Replay</a> + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.CUSTOM) + public final <@NonNull R> Flowable<R> replay(@NonNull Function<? super Flowable<T>, @NonNull ? extends Publisher<R>> selector, int bufferSize, long time, @NonNull TimeUnit unit, @NonNull Scheduler scheduler, boolean eagerTruncate) { + Objects.requireNonNull(selector, "selector is null"); + Objects.requireNonNull(unit, "unit is null"); + ObjectHelper.verifyPositive(bufferSize, "bufferSize"); + Objects.requireNonNull(scheduler, "scheduler is null"); + return FlowableReplay.multicastSelector( + FlowableInternalHelper.replaySupplier(this, bufferSize, time, unit, scheduler, eagerTruncate), selector); + } + + /** + * Returns a {@code Flowable} that emits items that are the results of invoking a specified selector on items + * emitted by a {@link ConnectableFlowable} that shares a single subscription to the current {@code Flowable}, + * replaying all items that were emitted within a specified time window. + * <p> + * <img width="640" height="435" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/replay.ft.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>This operator supports backpressure. Note that the upstream requests are determined by the child + * {@link Subscriber} which requests the largest amount: i.e., two child {@code Subscriber}s with requests of 10 and 100 will + * request 100 elements from the current {@code Flowable} sequence.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>This version of {@code replay} operates by default on the {@code computation} {@link Scheduler}.</dd> + * </dl> + * + * @param <R> + * the type of items emitted by the resulting {@code Flowable} + * @param selector + * a selector function, which can use the multicasted sequence as many times as needed, without + * causing multiple subscriptions to the current {@code Flowable} + * @param time + * the duration of the window in which the replayed items must have been emitted + * @param unit + * the time unit of {@code time} + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code selector} or {@code unit} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/replay.html">ReactiveX operators documentation: Replay</a> + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.COMPUTATION) + @NonNull + public final <@NonNull R> Flowable<R> replay(@NonNull Function<? super Flowable<T>, @NonNull ? extends Publisher<R>> selector, long time, @NonNull TimeUnit unit) { + return replay(selector, time, unit, Schedulers.computation()); + } + + /** + * Returns a {@code Flowable} that emits items that are the results of invoking a specified selector on items + * emitted by a {@link ConnectableFlowable} that shares a single subscription to the current {@code Flowable}, + * replaying all items that were emitted within a specified time window. + * <p> + * <img width="640" height="440" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/replay.fts.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>This operator supports backpressure. Note that the upstream requests are determined by the child + * {@link Subscriber} which requests the largest amount: i.e., two child {@code Subscriber}s with requests of 10 and 100 will + * request 100 elements from the current {@code Flowable} sequence.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>You specify which {@link Scheduler} this operator will use.</dd> + * </dl> + * + * @param <R> + * the type of items emitted by the resulting {@code Flowable} + * @param selector + * a selector function, which can use the multicasted sequence as many times as needed, without + * causing multiple subscriptions to the current {@code Flowable} + * @param time + * the duration of the window in which the replayed items must have been emitted + * @param unit + * the time unit of {@code time} + * @param scheduler + * the scheduler that is the time source for the window + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code selector}, {@code unit} or {@code scheduler} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/replay.html">ReactiveX operators documentation: Replay</a> + * @see #replay(Function, long, TimeUnit, Scheduler, boolean) + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.CUSTOM) + public final <@NonNull R> Flowable<R> replay(@NonNull Function<? super Flowable<T>, @NonNull ? extends Publisher<R>> selector, long time, @NonNull TimeUnit unit, @NonNull Scheduler scheduler) { + Objects.requireNonNull(selector, "selector is null"); + Objects.requireNonNull(unit, "unit is null"); + Objects.requireNonNull(scheduler, "scheduler is null"); + return FlowableReplay.multicastSelector(FlowableInternalHelper.replaySupplier(this, time, unit, scheduler, false), selector); + } + + /** + * Returns a {@code Flowable} that emits items that are the results of invoking a specified selector on items + * emitted by a {@link ConnectableFlowable} that shares a single subscription to the current {@code Flowable}, + * replaying all items that were emitted within a specified time window. + * <p> + * <img width="640" height="440" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/replay.fts.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>This operator supports backpressure. Note that the upstream requests are determined by the child + * {@link Subscriber} which requests the largest amount: i.e., two child {@code Subscriber}s with requests of 10 and 100 will + * request 100 elements from the current {@code Flowable} sequence.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>You specify which {@link Scheduler} this operator will use.</dd> + * </dl> + * + * @param <R> + * the type of items emitted by the resulting {@code Flowable} + * @param selector + * a selector function, which can use the multicasted sequence as many times as needed, without + * causing multiple subscriptions to the current {@code Flowable} + * @param time + * the duration of the window in which the replayed items must have been emitted + * @param unit + * the time unit of {@code time} + * @param scheduler + * the scheduler that is the time source for the window + * @param eagerTruncate + * if {@code true}, whenever the internal buffer is truncated to the given age, the + * oldest item will be guaranteed dereferenced, thus avoiding unexpected retention + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code selector}, {@code unit} or {@code scheduler} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/replay.html">ReactiveX operators documentation: Replay</a> + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.CUSTOM) + public final <@NonNull R> Flowable<R> replay(@NonNull Function<? super Flowable<T>, @NonNull ? extends Publisher<R>> selector, long time, @NonNull TimeUnit unit, @NonNull Scheduler scheduler, boolean eagerTruncate) { + Objects.requireNonNull(selector, "selector is null"); + Objects.requireNonNull(unit, "unit is null"); + Objects.requireNonNull(scheduler, "scheduler is null"); + return FlowableReplay.multicastSelector(FlowableInternalHelper.replaySupplier(this, time, unit, scheduler, eagerTruncate), selector); + } + + /** + * Returns a {@link ConnectableFlowable} that shares a single subscription to the current {@code Flowable} and + * replays at most {@code bufferSize} items to late {@link Subscriber}s. A Connectable {@code Flowable} resembles + * an ordinary {@code Flowable}, except that it does not begin emitting items when it is subscribed to, but only + * when its {@code connect} method is called. + * <p> + * Note that due to concurrency requirements, {@code replay(bufferSize)} may hold strong references to more than + * {@code bufferSize} source emissions. + * To ensure no beyond-bufferSize items are referenced, + * use the {@link #replay(int, boolean)} overload with {@code eagerTruncate = true}. + * <p> + * <img width="640" height="515" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/replay.n.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>This operator supports backpressure. Note that the upstream requests are determined by the child + * {@code Subscriber} which requests the largest amount: i.e., two child {@code Subscriber}s with requests of 10 and 100 will + * request 100 elements from the current {@code Flowable} sequence.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>This version of {@code replay} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param bufferSize + * the buffer size that limits the number of items that can be replayed + * @return the new {@code ConnectableFlowable} instance + * @throws IllegalArgumentException if {@code bufferSize} is non-positive + * @see <a href="http://reactivex.io/documentation/operators/replay.html">ReactiveX operators documentation: Replay</a> + * @see #replay(int, boolean) + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final ConnectableFlowable<T> replay(int bufferSize) { + ObjectHelper.verifyPositive(bufferSize, "bufferSize"); + return FlowableReplay.create(this, bufferSize, false); + } + /** + * Returns a {@link ConnectableFlowable} that shares a single subscription to the current {@code Flowable} and + * replays at most {@code bufferSize} items to late {@link Subscriber}s. A connectable {@code Flowable} resembles + * an ordinary {@code Flowable}, except that it does not begin emitting items when it is subscribed to, but only + * when its {@code connect} method is called. + * <p> + * <img width="640" height="515" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/replay.n.v3.png" alt=""> + * <p> + * Note that due to concurrency requirements, {@code replay(bufferSize)} may hold strong references to more than + * {@code bufferSize} source emissions. + * To ensure no beyond-bufferSize items are referenced, set {@code eagerTruncate = true}. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>This operator supports backpressure. Note that the upstream requests are determined by the child + * {@code Subscriber} which requests the largest amount: i.e., two child {@code Subscriber}s with requests of 10 and 100 will + * request 100 elements from the current {@code Flowable} sequence.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>This version of {@code replay} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param bufferSize + * the buffer size that limits the number of items that can be replayed + * @param eagerTruncate + * if {@code true}, whenever the internal buffer is truncated to the given bufferSize, the + * oldest item will be guaranteed dereferenced, thus avoiding unexpected retention + * @return the new {@code ConnectableFlowable} instance + * @throws IllegalArgumentException if {@code bufferSize} is non-positive + * @see <a href="http://reactivex.io/documentation/operators/replay.html">ReactiveX operators documentation: Replay</a> + * @since 3.0.0 + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final ConnectableFlowable<T> replay(int bufferSize, boolean eagerTruncate) { + ObjectHelper.verifyPositive(bufferSize, "bufferSize"); + return FlowableReplay.create(this, bufferSize, eagerTruncate); + } + + /** + * Returns a {@link ConnectableFlowable} that shares a single subscription to the current {@code Flowable} and + * replays at most {@code bufferSize} items that were emitted during a specified time window. A connectable + * {@code Flowable} resembles an ordinary {@code Flowable}, except that it does not begin emitting items when it is + * subscribed to, but only when its {@code connect} method is called. + * <p> + * <img width="640" height="515" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/replay.nt.v3.png" alt=""> + * <p> + * Note that due to concurrency requirements, {@code replay(bufferSize)} may hold strong references to more than + * {@code bufferSize} source emissions. + * To ensure no out-of-date or beyond-bufferSize items are referenced, + * use the {@link #replay(int, long, TimeUnit, Scheduler, boolean)} overload with {@code eagerTruncate = true}. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>This operator supports backpressure. Note that the upstream requests are determined by the child + * {@link Subscriber} which requests the largest amount: i.e., two child {@code Subscriber}s with requests of 10 and 100 will + * request 100 elements from the current {@code Flowable} sequence.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>This version of {@code replay} operates by default on the {@code computation} {@link Scheduler}.</dd> + * </dl> + * + * @param bufferSize + * the buffer size that limits the number of items that can be replayed + * @param time + * the duration of the window in which the replayed items must have been emitted + * @param unit + * the time unit of {@code time} + * @return the new {@code ConnectableFlowable} instance + * @throws NullPointerException if {@code unit} is {@code null} + * @throws IllegalArgumentException if {@code bufferSize} is non-positive + * @see <a href="http://reactivex.io/documentation/operators/replay.html">ReactiveX operators documentation: Replay</a> + * @see #replay(int, long, TimeUnit, Scheduler, boolean) + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.COMPUTATION) + @NonNull + public final ConnectableFlowable<T> replay(int bufferSize, long time, @NonNull TimeUnit unit) { + return replay(bufferSize, time, unit, Schedulers.computation()); + } + + /** + * Returns a {@link ConnectableFlowable} that shares a single subscription to the current {@code Flowable} and + * replays a maximum of {@code bufferSize} items that are emitted within a specified time window to late {@link Subscriber}s. A + * connectable {@code Flowable} resembles an ordinary {@code Flowable}, except that it does not begin emitting items + * when it is subscribed to, but only when its {@code connect} method is called. + * <p> + * <img width="640" height="515" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/replay.nts.v3.png" alt=""> + * <p> + * Note that due to concurrency requirements, {@code replay(bufferSize)} may hold strong references to more than + * {@code bufferSize} source emissions. + * To ensure no out-of-date or beyond-bufferSize items are referenced, + * use the {@link #replay(int, long, TimeUnit, Scheduler, boolean)} overload with {@code eagerTruncate = true}. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>This operator supports backpressure. Note that the upstream requests are determined by the child + * {@code Subscriber} which requests the largest amount: i.e., two child {@code Subscriber}s with requests of 10 and 100 will + * request 100 elements from the current {@code Flowable} sequence.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>You specify which {@link Scheduler} this operator will use.</dd> + * </dl> + * + * @param bufferSize + * the buffer size that limits the number of items that can be replayed + * @param time + * the duration of the window in which the replayed items must have been emitted + * @param unit + * the time unit of {@code time} + * @param scheduler + * the scheduler that is used as a time source for the window + * @return the new {@code ConnectableFlowable} instance + * @throws NullPointerException if {@code unit} or {@code scheduler} is {@code null} + * @throws IllegalArgumentException if {@code bufferSize} is non-positive + * @see <a href="http://reactivex.io/documentation/operators/replay.html">ReactiveX operators documentation: Replay</a> + * @see #replay(int, long, TimeUnit, Scheduler, boolean) + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.CUSTOM) + @NonNull + public final ConnectableFlowable<T> replay(int bufferSize, long time, @NonNull TimeUnit unit, @NonNull Scheduler scheduler) { + Objects.requireNonNull(unit, "unit is null"); + Objects.requireNonNull(scheduler, "scheduler is null"); + ObjectHelper.verifyPositive(bufferSize, "bufferSize"); + return FlowableReplay.create(this, time, unit, scheduler, bufferSize, false); + } + + /** + * Returns a {@link ConnectableFlowable} that shares a single subscription to the current {@code Flowable} and + * replays a maximum of {@code bufferSize} items that are emitted within a specified time window to late {@link Subscriber}s. A + * connectable {@code Flowable} resembles an ordinary {@code Flowable}, except that it does not begin emitting items + * when it is subscribed to, but only when its {@code connect} method is called. + * <p> + * Note that due to concurrency requirements, {@code replay(bufferSize)} may hold strong references to more than + * {@code bufferSize} source emissions. To ensure no out-of-date or beyond-bufferSize items + * are referenced, set {@code eagerTruncate = true}. + * <p> + * <img width="640" height="515" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/replay.nts.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>This operator supports backpressure. Note that the upstream requests are determined by the child + * {@code Subscriber} which requests the largest amount: i.e., two child {@code Subscriber}s with requests of 10 and 100 will + * request 100 elements from the current {@code Flowable} sequence.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>You specify which {@link Scheduler} this operator will use.</dd> + * </dl> + * + * @param bufferSize + * the buffer size that limits the number of items that can be replayed + * @param time + * the duration of the window in which the replayed items must have been emitted + * @param unit + * the time unit of {@code time} + * @param scheduler + * the scheduler that is used as a time source for the window + * @param eagerTruncate + * if {@code true}, whenever the internal buffer is truncated to the given bufferSize/age, the + * oldest item will be guaranteed dereferenced, thus avoiding unexpected retention + * @return the new {@code ConnectableFlowable} instance + * @throws NullPointerException if {@code unit} or {@code scheduler} is {@code null} + * @throws IllegalArgumentException if {@code bufferSize} is non-positive + * @see <a href="http://reactivex.io/documentation/operators/replay.html">ReactiveX operators documentation: Replay</a> + * @since 3.0.0 + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.CUSTOM) + @NonNull + public final ConnectableFlowable<T> replay(int bufferSize, long time, @NonNull TimeUnit unit, @NonNull Scheduler scheduler, boolean eagerTruncate) { + Objects.requireNonNull(unit, "unit is null"); + Objects.requireNonNull(scheduler, "scheduler is null"); + ObjectHelper.verifyPositive(bufferSize, "bufferSize"); + return FlowableReplay.create(this, time, unit, scheduler, bufferSize, eagerTruncate); + } + + /** + * Returns a {@link ConnectableFlowable} that shares a single subscription to the current {@code Flowable} and + * replays all items emitted by it within a specified time window to late {@link Subscriber}s. A connectable {@code Flowable} + * resembles an ordinary {@code Flowable}, except that it does not begin emitting items when it is subscribed to, + * but only when its {@code connect} method is called. + * <p> + * <img width="640" height="515" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/replay.t.v3.png" alt=""> + * <p> + * Note that the internal buffer may retain strong references to the oldest item. To ensure no out-of-date items + * are referenced, use the {@link #replay(long, TimeUnit, Scheduler, boolean)} overload with {@code eagerTruncate = true}. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>This operator supports backpressure. Note that the upstream requests are determined by the child + * {@code Subscriber} which requests the largest amount: i.e., two child {@code Subscriber}s with requests of 10 and 100 will + * request 100 elements from the current {@code Flowable} sequence.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>This version of {@code replay} operates by default on the {@code computation} {@link Scheduler}.</dd> + * </dl> + * + * @param time + * the duration of the window in which the replayed items must have been emitted + * @param unit + * the time unit of {@code time} + * @return the new {@code ConnectableFlowable} instance + * @throws NullPointerException if {@code unit} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/replay.html">ReactiveX operators documentation: Replay</a> + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.COMPUTATION) + @NonNull + public final ConnectableFlowable<T> replay(long time, @NonNull TimeUnit unit) { + return replay(time, unit, Schedulers.computation()); + } + + /** + * Returns a {@link ConnectableFlowable} that shares a single subscription to the current {@code Flowable} and + * replays all items emitted by it within a specified time window to late {@link Subscriber}s. A connectable {@code Flowable} + * resembles an ordinary {@code Flowable}, except that it does not begin emitting items when it is subscribed to, + * but only when its {@code connect} method is called. + * <p> + * <img width="640" height="515" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/replay.ts.v3.png" alt=""> + * <p> + * Note that the internal buffer may retain strong references to the oldest item. To ensure no out-of-date items + * are referenced, use the {@link #replay(long, TimeUnit, Scheduler, boolean)} overload with {@code eagerTruncate = true}. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>This operator supports backpressure. Note that the upstream requests are determined by the child + * {@code Subscriber} which requests the largest amount: i.e., two child {@code Subscriber}s with requests of 10 and 100 will + * request 100 elements from the current {@code Flowable} sequence.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>You specify which {@link Scheduler} this operator will use.</dd> + * </dl> + * + * @param time + * the duration of the window in which the replayed items must have been emitted + * @param unit + * the time unit of {@code time} + * @param scheduler + * the {@code Scheduler} that is the time source for the window + * @return the new {@code ConnectableFlowable} instance + * @throws NullPointerException if {@code unit} or {@code scheduler} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/replay.html">ReactiveX operators documentation: Replay</a> + * @see #replay(long, TimeUnit, Scheduler, boolean) + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.CUSTOM) + @NonNull + public final ConnectableFlowable<T> replay(long time, @NonNull TimeUnit unit, @NonNull Scheduler scheduler) { + Objects.requireNonNull(unit, "unit is null"); + Objects.requireNonNull(scheduler, "scheduler is null"); + return FlowableReplay.create(this, time, unit, scheduler, false); + } + + /** + * Returns a {@link ConnectableFlowable} that shares a single subscription to the current {@code Flowable} and + * replays all items emitted by it within a specified time window to late {@link Subscriber}s. A connectable {@code Flowable} + * resembles an ordinary {@code Flowable}, except that it does not begin emitting items when it is subscribed to, + * but only when its {@code connect} method is called. + * <p> + * <img width="640" height="515" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/replay.ts.v3.png" alt=""> + * <p> + * Note that the internal buffer may retain strong references to the oldest item. To ensure no out-of-date items + * are referenced, set {@code eagerTruncate = true}. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>This operator supports backpressure. Note that the upstream requests are determined by the child + * {@code Subscriber} which requests the largest amount: i.e., two child {@code Subscriber}s with requests of 10 and 100 will + * request 100 elements from the current {@code Flowable} sequence.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>You specify which {@link Scheduler} this operator will use.</dd> + * </dl> + * + * @param time + * the duration of the window in which the replayed items must have been emitted + * @param unit + * the time unit of {@code time} + * @param scheduler + * the {@code Scheduler} that is the time source for the window + * @param eagerTruncate + * if {@code true}, whenever the internal buffer is truncated to the given bufferSize/age, the + * oldest item will be guaranteed dereferenced, thus avoiding unexpected retention + * @return the new {@code ConnectableFlowable} instance + * @throws NullPointerException if {@code unit} or {@code scheduler} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/replay.html">ReactiveX operators documentation: Replay</a> + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.CUSTOM) + @NonNull + public final ConnectableFlowable<T> replay(long time, @NonNull TimeUnit unit, @NonNull Scheduler scheduler, boolean eagerTruncate) { + Objects.requireNonNull(unit, "unit is null"); + Objects.requireNonNull(scheduler, "scheduler is null"); + return FlowableReplay.create(this, time, unit, scheduler, eagerTruncate); + } + + /** + * Returns a {@code Flowable} that mirrors the current {@code Flowable}, resubscribing to it if it calls {@code onError} + * (infinite retry count). + * <p> + * <img width="640" height="315" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/retry.v3.png" alt=""> + * <p> + * If the current {@code Flowable} calls {@link Subscriber#onError}, this method will resubscribe to the current + * {@code Flowable} rather than propagating the {@code onError} call. + * <p> + * Any and all items emitted by the current {@code Flowable} will be emitted by the resulting {@code Flowable}, even + * those emitted during failed subscriptions. For example, if the current {@code Flowable} fails at first but emits + * {@code [1, 2]} then succeeds the second time and emits {@code [1, 2, 3, 4, 5]} then the complete sequence + * of emissions and notifications would be {@code [1, 2, 1, 2, 3, 4, 5, onComplete]}. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors downstream backpressure and expects the current {@code Flowable} to honor backpressure as well. + * If this expectation is violated, the operator <em>may</em> throw an {@link IllegalStateException}.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code retry} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @return the new {@code Flowable} instance + * @see <a href="http://reactivex.io/documentation/operators/retry.html">ReactiveX operators documentation: Retry</a> + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Flowable<T> retry() { + return retry(Long.MAX_VALUE, Functions.alwaysTrue()); + } + + /** + * Returns a {@code Flowable} that mirrors the current {@code Flowable}, resubscribing to it if it calls {@code onError} + * and the predicate returns {@code true} for that specific exception and retry count. + * <p> + * <img width="640" height="315" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/retry.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors downstream backpressure and expects the current {@code Flowable} to honor backpressure as well. + * If this expectation is violated, the operator <em>may</em> throw an {@link IllegalStateException}.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code retry} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param predicate + * the predicate that determines if a resubscription may happen in case of a specific exception + * and retry count + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code predicate} is {@code null} + * @see #retry() + * @see <a href="http://reactivex.io/documentation/operators/retry.html">ReactiveX operators documentation: Retry</a> + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + public final Flowable<T> retry(@NonNull BiPredicate<@NonNull ? super Integer, @NonNull ? super Throwable> predicate) { + Objects.requireNonNull(predicate, "predicate is null"); + + return RxJavaPlugins.onAssembly(new FlowableRetryBiPredicate<>(this, predicate)); + } + + /** + * Returns a {@code Flowable} that mirrors the current {@code Flowable}, resubscribing to it if it calls {@code onError} + * up to a specified number of retries. + * <p> + * <img width="640" height="315" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/retry.v3.png" alt=""> + * <p> + * If the current {@code Flowable} calls {@link Subscriber#onError}, this method will resubscribe to the current + * {@code Flowable} for a maximum of {@code count} resubscriptions rather than propagating the + * {@code onError} call. + * <p> + * Any and all items emitted by the current {@code Flowable} will be emitted by the resulting {@code Flowable}, even + * those emitted during failed subscriptions. For example, if the current {@code Flowable} fails at first but emits + * {@code [1, 2]} then succeeds the second time and emits {@code [1, 2, 3, 4, 5]} then the complete sequence + * of emissions and notifications would be {@code [1, 2, 1, 2, 3, 4, 5, onComplete]}. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors downstream backpressure and expects the current {@code Flowable} to honor backpressure as well. + * If this expectation is violated, the operator <em>may</em> throw an {@link IllegalStateException}.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code retry} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param times + * the number of times to resubscribe if the current {@code Flowable} fails + * @return the new {@code Flowable} instance + * @throws IllegalArgumentException if {@code times} is negative + * @see <a href="http://reactivex.io/documentation/operators/retry.html">ReactiveX operators documentation: Retry</a> + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Flowable<T> retry(long times) { + return retry(times, Functions.alwaysTrue()); + } + + /** + * Retries at most times or until the predicate returns {@code false}, whichever happens first. + * + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors downstream backpressure and expects the current {@code Flowable} to honor backpressure as well. + * If this expectation is violated, the operator <em>may</em> throw an {@link IllegalStateException}.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code retry} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param times the number of times to resubscribe if the current {@code Flowable} fails + * @param predicate the predicate called with the failure {@link Throwable} and should return {@code true} to trigger a retry. + * @throws NullPointerException if {@code predicate} is {@code null} + * @throws IllegalArgumentException if {@code times} is negative + * @return the new {@code Flowable} instance + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + public final Flowable<T> retry(long times, @NonNull Predicate<@NonNull ? super Throwable> predicate) { + if (times < 0) { + throw new IllegalArgumentException("times >= 0 required but it was " + times); + } + Objects.requireNonNull(predicate, "predicate is null"); + + return RxJavaPlugins.onAssembly(new FlowableRetryPredicate<>(this, times, predicate)); + } + + /** + * Retries the current {@code Flowable} if the predicate returns {@code true}. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors downstream backpressure and expects the current {@code Flowable} to honor backpressure as well. + * If this expectation is violated, the operator <em>may</em> throw an {@link IllegalStateException}.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code retry} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param predicate the predicate that receives the failure {@link Throwable} and should return {@code true} to trigger a retry. + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code predicate} is {@code null} + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Flowable<T> retry(@NonNull Predicate<@NonNull ? super Throwable> predicate) { + return retry(Long.MAX_VALUE, predicate); + } + + /** + * Retries until the given stop function returns {@code true}. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors downstream backpressure and expects the current {@code Flowable} to honor backpressure as well. + * If this expectation is violated, the operator <em>may</em> throw an {@link IllegalStateException}.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code retryUntil} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param stop the function that should return {@code true} to stop retrying + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code stop} is {@code null} + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + public final Flowable<T> retryUntil(@NonNull BooleanSupplier stop) { + Objects.requireNonNull(stop, "stop is null"); + return retry(Long.MAX_VALUE, Functions.predicateReverseFor(stop)); + } + + /** + * Returns a {@code Flowable} that emits the same values as the current {@code Flowable} with the exception of an + * {@code onError}. An {@code onError} notification from the source will result in the emission of a + * {@link Throwable} item to the {@code Flowable} provided as an argument to the {@code notificationHandler} + * function. If that {@link Publisher} calls {@code onComplete} or {@code onError} then {@code retry} will call + * {@code onComplete} or {@code onError} on the child subscription. Otherwise, this {@code Publisher} will + * resubscribe to the current {@code Flowable}. + * <p> + * <img width="640" height="430" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/retryWhen.f.v3.png" alt=""> + * <p> + * Example: + * + * This retries 3 times, each time incrementing the number of seconds it waits. + * + * <pre><code> + * Flowable.create((FlowableEmitter<? super String> s) -> { + * System.out.println("subscribing"); + * s.onError(new RuntimeException("always fails")); + * }, BackpressureStrategy.BUFFER).retryWhen(attempts -> { + * return attempts.zipWith(Flowable.range(1, 3), (n, i) -> i).flatMap(i -> { + * System.out.println("delay retry by " + i + " second(s)"); + * return Flowable.timer(i, TimeUnit.SECONDS); + * }); + * }).blockingForEach(System.out::println); + * </code></pre> + * + * Output is: + * + * <pre> {@code + * subscribing + * delay retry by 1 second(s) + * subscribing + * delay retry by 2 second(s) + * subscribing + * delay retry by 3 second(s) + * subscribing + * } </pre> + * <p> + * Note that the inner {@code Publisher} returned by the handler function should signal + * either {@code onNext}, {@code onError} or {@code onComplete} in response to the received + * {@code Throwable} to indicate the operator should retry or terminate. If the upstream to + * the operator is asynchronous, signaling {@code onNext} followed by {@code onComplete} immediately may + * result in the sequence to be completed immediately. Similarly, if this inner + * {@code Publisher} signals {@code onError} or {@code onComplete} while the upstream is + * active, the sequence is terminated with the same signal immediately. + * <p> + * The following example demonstrates how to retry an asynchronous source with a delay: + * <pre><code> + * Flowable.timer(1, TimeUnit.SECONDS) + * .doOnSubscribe(s -> System.out.println("subscribing")) + * .map(v -> { throw new RuntimeException(); }) + * .retryWhen(errors -> { + * AtomicInteger counter = new AtomicInteger(); + * return errors + * .takeWhile(e -> counter.getAndIncrement() != 3) + * .flatMap(e -> { + * System.out.println("delay retry by " + counter.get() + " second(s)"); + * return Flowable.timer(counter.get(), TimeUnit.SECONDS); + * }); + * }) + * .blockingSubscribe(System.out::println, System.out::println); + * </code></pre> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors downstream backpressure and expects both the source + * and inner {@code Publisher}s to honor backpressure as well. + * If this expectation is violated, the operator <em>may</em> throw an {@link IllegalStateException}.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code retryWhen} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param handler + * receives a {@code Publisher} of notifications with which a user can complete or error, aborting the + * retry + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code handler} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/retry.html">ReactiveX operators documentation: Retry</a> + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + public final Flowable<T> retryWhen( + @NonNull Function<? super Flowable<Throwable>, @NonNull ? extends Publisher<@NonNull ?>> handler) { + Objects.requireNonNull(handler, "handler is null"); + + return RxJavaPlugins.onAssembly(new FlowableRetryWhen<>(this, handler)); + } + + /** + * Subscribes to the current {@code Flowable} and wraps the given {@link Subscriber} into a {@link SafeSubscriber} + * (if not already a {@code SafeSubscriber}) that + * deals with exceptions thrown by a misbehaving {@code Subscriber} (that doesn't follow the + * <em>Reactive Streams</em> specification). + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>This operator leaves the reactive world and the backpressure behavior depends on the {@code Subscriber}'s behavior.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code safeSubscribe} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param subscriber the incoming {@code Subscriber} instance + * @throws NullPointerException if {@code subscriber} is {@code null} + */ + @BackpressureSupport(BackpressureKind.PASS_THROUGH) + @SchedulerSupport(SchedulerSupport.NONE) + public final void safeSubscribe(@NonNull Subscriber<? super T> subscriber) { + Objects.requireNonNull(subscriber, "subscriber is null"); + if (subscriber instanceof SafeSubscriber) { + subscribe((SafeSubscriber<? super T>)subscriber); + } else { + subscribe(new SafeSubscriber<>(subscriber)); + } + } + + /** + * Returns a {@code Flowable} that emits the most recently emitted item (if any) emitted by the current {@code Flowable} + * within periodic time intervals. + * <p> + * <img width="640" height="305" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/sample.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>This operator does not support backpressure as it uses time to control data flow.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code sample} operates by default on the {@code computation} {@link Scheduler}.</dd> + * </dl> + * + * @param period + * the sampling rate + * @param unit + * the {@link TimeUnit} in which {@code period} is defined + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code unit} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/sample.html">ReactiveX operators documentation: Sample</a> + * @see <a href="https://github.com/ReactiveX/RxJava/wiki/Backpressure">RxJava wiki: Backpressure</a> + * @see #throttleLast(long, TimeUnit) + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.ERROR) + @SchedulerSupport(SchedulerSupport.COMPUTATION) + @NonNull + public final Flowable<T> sample(long period, @NonNull TimeUnit unit) { + return sample(period, unit, Schedulers.computation()); + } + + /** + * Returns a {@code Flowable} that emits the most recently emitted item (if any) emitted by the current {@code Flowable} + * within periodic time intervals and optionally emit the very last upstream item when the upstream completes. + * <p> + * <img width="640" height="277" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/sample.emitlast.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>This operator does not support backpressure as it uses time to control data flow.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code sample} operates by default on the {@code computation} {@link Scheduler}.</dd> + * </dl> + * + * <p>History: 2.0.5 - experimental + * @param period + * the sampling rate + * @param unit + * the {@link TimeUnit} in which {@code period} is defined + * @param emitLast + * if {@code true}, and the upstream completes while there is still an unsampled item available, + * that item is emitted to downstream before completion + * if {@code false}, an unsampled last item is ignored. + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code unit} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/sample.html">ReactiveX operators documentation: Sample</a> + * @see <a href="https://github.com/ReactiveX/RxJava/wiki/Backpressure">RxJava wiki: Backpressure</a> + * @see #throttleLast(long, TimeUnit) + * @since 2.1 + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.ERROR) + @SchedulerSupport(SchedulerSupport.COMPUTATION) + @NonNull + public final Flowable<T> sample(long period, @NonNull TimeUnit unit, boolean emitLast) { + return sample(period, unit, Schedulers.computation(), emitLast); + } + + /** + * Returns a {@code Flowable} that emits the most recently emitted item (if any) emitted by the current {@code Flowable} + * within periodic time intervals, where the intervals are defined on a particular {@link Scheduler}. + * <p> + * <img width="640" height="305" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/sample.s.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>This operator does not support backpressure as it uses time to control data flow.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>You specify which {@code Scheduler} this operator will use.</dd> + * </dl> + * + * @param period + * the sampling rate + * @param unit + * the {@link TimeUnit} in which {@code period} is defined + * @param scheduler + * the {@code Scheduler} to use when sampling + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code unit} or {@code scheduler} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/sample.html">ReactiveX operators documentation: Sample</a> + * @see <a href="https://github.com/ReactiveX/RxJava/wiki/Backpressure">RxJava wiki: Backpressure</a> + * @see #throttleLast(long, TimeUnit, Scheduler) + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.ERROR) + @SchedulerSupport(SchedulerSupport.CUSTOM) + public final Flowable<T> sample(long period, @NonNull TimeUnit unit, @NonNull Scheduler scheduler) { + Objects.requireNonNull(unit, "unit is null"); + Objects.requireNonNull(scheduler, "scheduler is null"); + return RxJavaPlugins.onAssembly(new FlowableSampleTimed<>(this, period, unit, scheduler, false, null)); + } + + /** + * Returns a {@code Flowable} that emits the most recently emitted item (if any) emitted by the current {@code Flowable} + * within periodic time intervals, where the intervals are defined on a particular {@link Scheduler} + * and optionally emit the very last upstream item when the upstream completes. + * <p> + * <img width="640" height="277" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/sample.s.emitlast.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>This operator does not support backpressure as it uses time to control data flow.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>You specify which {@code Scheduler} this operator will use.</dd> + * </dl> + * + * <p>History: 2.0.5 - experimental + * @param period + * the sampling rate + * @param unit + * the {@link TimeUnit} in which {@code period} is defined + * @param scheduler + * the {@code Scheduler} to use when sampling + * @param emitLast + * if {@code true} and the upstream completes while there is still an unsampled item available, + * that item is emitted to downstream before completion + * if {@code false}, an unsampled last item is ignored. + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code unit} or {@code scheduler} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/sample.html">ReactiveX operators documentation: Sample</a> + * @see <a href="https://github.com/ReactiveX/RxJava/wiki/Backpressure">RxJava wiki: Backpressure</a> + * @see #throttleLast(long, TimeUnit, Scheduler) + * @since 2.1 + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.ERROR) + @SchedulerSupport(SchedulerSupport.CUSTOM) + public final Flowable<T> sample(long period, @NonNull TimeUnit unit, @NonNull Scheduler scheduler, boolean emitLast) { + Objects.requireNonNull(unit, "unit is null"); + Objects.requireNonNull(scheduler, "scheduler is null"); + return RxJavaPlugins.onAssembly(new FlowableSampleTimed<>(this, period, unit, scheduler, emitLast, null)); + } + + /** + * Returns a {@code Flowable} that emits the most recently emitted item (if any) emitted by the current {@code Flowable} + * within periodic time intervals, where the intervals are defined on a particular {@link Scheduler} + * and optionally emit the very last upstream item when the upstream completes. + * <p> + * <img width="640" height="277" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/sample.s.emitlast.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>This operator does not support backpressure as it uses time to control data flow.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>You specify which {@code Scheduler} this operator will use.</dd> + * </dl> + * + * @param period + * the sampling rate + * @param unit + * the {@link TimeUnit} in which {@code period} is defined + * @param scheduler + * the {@code Scheduler} to use when sampling + * @param emitLast + * if {@code true} and the upstream completes while there is still an unsampled item available, + * that item is emitted to downstream before completion + * if {@code false}, an unsampled last item is ignored. + * @param onDropped + * called with the current entry when it has been replaced by a new one + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code unit} or {@code scheduler} is {@code null} or {@code onDropped} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/sample.html">ReactiveX operators documentation: Sample</a> + * @see <a href="https://github.com/ReactiveX/RxJava/wiki/Backpressure">RxJava wiki: Backpressure</a> + * @see #throttleLast(long, TimeUnit, Scheduler) + * @since 3.1.6 - Experimental + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.ERROR) + @SchedulerSupport(SchedulerSupport.CUSTOM) + @Experimental + public final Flowable<T> sample(long period, @NonNull TimeUnit unit, @NonNull Scheduler scheduler, boolean emitLast, @NonNull Consumer<? super T> onDropped) { + Objects.requireNonNull(unit, "unit is null"); + Objects.requireNonNull(scheduler, "scheduler is null"); + Objects.requireNonNull(onDropped, "onDropped is null"); + return RxJavaPlugins.onAssembly(new FlowableSampleTimed<>(this, period, unit, scheduler, emitLast, onDropped)); + } + + /** + * Returns a {@code Flowable} that, when the specified {@code sampler} {@link Publisher} emits an item or completes, + * emits the most recently emitted item (if any) emitted by the current {@code Flowable} since the previous + * emission from the {@code sampler} {@code Publisher}. + * <p> + * <img width="640" height="290" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/sample.o.nolast.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>This operator does not support backpressure as it uses the emissions of the {@code sampler} + * {@code Publisher} to control data flow.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>This version of {@code sample} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <U> the element type of the sampler {@code Publisher} + * @param sampler + * the {@code Publisher} to use for sampling the current {@code Flowable} + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code sampler} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/sample.html">ReactiveX operators documentation: Sample</a> + * @see <a href="https://github.com/ReactiveX/RxJava/wiki/Backpressure">RxJava wiki: Backpressure</a> + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.ERROR) + @SchedulerSupport(SchedulerSupport.NONE) + public final <@NonNull U> Flowable<T> sample(@NonNull Publisher<U> sampler) { + Objects.requireNonNull(sampler, "sampler is null"); + return RxJavaPlugins.onAssembly(new FlowableSamplePublisher<>(this, sampler, false)); + } + + /** + * Returns a {@code Flowable} that, when the specified {@code sampler} {@link Publisher} emits an item or completes, + * emits the most recently emitted item (if any) emitted by the current {@code Flowable} since the previous + * emission from the {@code sampler} {@code Publisher} + * and optionally emit the very last upstream item when the upstream or other {@code Publisher} complete. + * <p> + * <img width="640" height="290" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/sample.o.emitlast.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>This operator does not support backpressure as it uses the emissions of the {@code sampler} + * {@code Publisher} to control data flow.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>This version of {@code sample} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * <p>History: 2.0.5 - experimental + * @param <U> the element type of the sampler {@code Publisher} + * @param sampler + * the {@code Publisher} to use for sampling the current {@code Flowable} + * @param emitLast + * if {@code true} and the upstream completes while there is still an unsampled item available, + * that item is emitted to downstream before completion + * if {@code false}, an unsampled last item is ignored. + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code sampler} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/sample.html">ReactiveX operators documentation: Sample</a> + * @see <a href="https://github.com/ReactiveX/RxJava/wiki/Backpressure">RxJava wiki: Backpressure</a> + * @since 2.1 + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.ERROR) + @SchedulerSupport(SchedulerSupport.NONE) + public final <@NonNull U> Flowable<T> sample(@NonNull Publisher<U> sampler, boolean emitLast) { + Objects.requireNonNull(sampler, "sampler is null"); + return RxJavaPlugins.onAssembly(new FlowableSamplePublisher<>(this, sampler, emitLast)); + } + + /** + * Returns a {@code Flowable} that emits the first value emitted by the current {@code Flowable}, then emits one value + * for each subsequent value emitted by the current {@code Flowable}. Each emission after the first is the result of + * applying the specified accumulator function to the previous emission and the corresponding value from the current {@code Flowable}. + * <p> + * <img width="640" height="320" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/scan.v3.png" alt=""> + * <p> + * This sort of function is sometimes called an accumulator. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors downstream backpressure and expects the current {@code Flowable} to honor backpressure as well. + * Violating this expectation, a {@link MissingBackpressureException} <em>may</em> get signaled somewhere downstream.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code scan} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param accumulator + * an accumulator function to be invoked on each item emitted by the current {@code Flowable}, whose + * result will be emitted to {@link Subscriber}s via {@link Subscriber#onNext onNext} and used in the + * next accumulator call + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code accumulator} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/scan.html">ReactiveX operators documentation: Scan</a> + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + public final Flowable<T> scan(@NonNull BiFunction<T, T, T> accumulator) { + Objects.requireNonNull(accumulator, "accumulator is null"); + return RxJavaPlugins.onAssembly(new FlowableScan<>(this, accumulator)); + } + + /** + * Returns a {@code Flowable} that emits the provided initial (seed) value, then emits one value for each value emitted + * by the current {@code Flowable}. Each emission after the first is the result of applying the specified accumulator + * function to the previous emission and the corresponding value from the current {@code Flowable}. + * <p> + * <img width="640" height="320" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/scanSeed.v3.png" alt=""> + * <p> + * This sort of function is sometimes called an accumulator. + * <p> + * Note that the {@code Flowable} that results from this method will emit {@code initialValue} as its first + * emitted item. + * <p> + * Note that the {@code initialValue} is shared among all subscribers to the resulting {@code Flowable} + * and may cause problems if it is mutable. To make sure each subscriber gets its own value, defer + * the application of this operator via {@link #defer(Supplier)}: + * <pre><code> + * Publisher<T> source = ... + * Flowable.defer(() -> source.scan(new ArrayList<>(), (list, item) -> list.add(item))); + * + * // alternatively, by using compose to stay fluent + * + * source.compose(o -> + * Flowable.defer(() -> o.scan(new ArrayList<>(), (list, item) -> list.add(item))) + * ); + * </code></pre> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors downstream backpressure and expects the current {@code Flowable} to honor backpressure as well. + * Violating this expectation, a {@link MissingBackpressureException} <em>may</em> get signaled somewhere downstream. + * The downstream request pattern is not preserved across this operator. + * The upstream is requested {@link #bufferSize()} - 1 upfront and 75% of {@link #bufferSize()} thereafter.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code scan} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <R> the initial, accumulator and result type + * @param initialValue + * the initial (seed) accumulator item + * @param accumulator + * an accumulator function to be invoked on each item emitted by the current {@code Flowable}, whose + * result will be emitted to {@link Subscriber}s via {@link Subscriber#onNext onNext} and used in the + * next accumulator call + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code initialValue} or {@code accumulator} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/scan.html">ReactiveX operators documentation: Scan</a> + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + public final <@NonNull R> Flowable<R> scan(R initialValue, @NonNull BiFunction<R, ? super T, R> accumulator) { + Objects.requireNonNull(initialValue, "initialValue is null"); + return scanWith(Functions.justSupplier(initialValue), accumulator); + } + + /** + * Returns a {@code Flowable} that emits the provided initial (seed) value, then emits one value for each value emitted + * by the current {@code Flowable}. Each emission after the first is the result of applying the specified accumulator + * function to the previous emission and the corresponding value from the current {@code Flowable}. + * <p> + * <img width="640" height="320" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/scanSeed.v3.png" alt=""> + * <p> + * This sort of function is sometimes called an accumulator. + * <p> + * Note that the {@code Flowable} that results from this method will emit the value returned by + * the {@code seedSupplier} as its first item. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors downstream backpressure and expects the current {@code Flowable} to honor backpressure as well. + * Violating this expectation, a {@link MissingBackpressureException} <em>may</em> get signaled somewhere downstream. + * The downstream request pattern is not preserved across this operator. + * The upstream is requested {@link #bufferSize()} - 1 upfront and 75% of {@link #bufferSize()} thereafter.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code scanWith} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <R> the initial, accumulator and result type + * @param seedSupplier + * a {@link Supplier} that returns the initial (seed) accumulator item for each individual {@link Subscriber} + * @param accumulator + * an accumulator function to be invoked on each item emitted by the current {@code Flowable}, whose + * result will be emitted to {@code Subscriber}s via {@link Subscriber#onNext onNext} and used in the + * next accumulator call + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code seedSupplier} or {@code accumulator} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/scan.html">ReactiveX operators documentation: Scan</a> + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + public final <@NonNull R> Flowable<R> scanWith(@NonNull Supplier<R> seedSupplier, @NonNull BiFunction<R, ? super T, R> accumulator) { + Objects.requireNonNull(seedSupplier, "seedSupplier is null"); + Objects.requireNonNull(accumulator, "accumulator is null"); + return RxJavaPlugins.onAssembly(new FlowableScanSeed<>(this, seedSupplier, accumulator)); + } + + /** + * Forces the current {@code Flowable}'s emissions and notifications to be serialized and for it to obey + * <a href="http://reactivex.io/documentation/contract.html">the {@code Publisher} contract</a> in other ways. + * <p> + * It is possible for a {@link Publisher} to invoke its {@link Subscriber}s' methods asynchronously, perhaps from + * different threads. This could make such a {@code Publisher} poorly-behaved, in that it might try to invoke + * {@code onComplete} or {@code onError} before one of its {@code onNext} invocations, or it might call + * {@code onNext} from two different threads concurrently. You can force such a {@code Publisher} to be + * well-behaved and sequential by applying the {@code serialize} method to it. + * <p> + * <img width="640" height="400" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/synchronize.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator doesn't interfere with backpressure which is determined by the current {@code Flowable}'s backpressure + * behavior.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code serialize} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @return the new {@code Flowable} instance + * @see <a href="http://reactivex.io/documentation/operators/serialize.html">ReactiveX operators documentation: Serialize</a> + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.PASS_THROUGH) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Flowable<T> serialize() { + return RxJavaPlugins.onAssembly(new FlowableSerialized<>(this)); + } + + /** + * Returns a new {@code Flowable} that multicasts (and shares a single subscription to) the current {@code Flowable}. As long as + * there is at least one {@link Subscriber}, the current {@code Flowable} will be subscribed and emitting data. + * When all subscribers have canceled it will cancel the current {@code Flowable}. + * <p> + * This is an alias for {@link #publish()}.{@link ConnectableFlowable#refCount() refCount()}. + * <p> + * <img width="640" height="510" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/publishRefCount.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure and expects the current {@code Flowable} to honor backpressure as well. + * If this expectation is violated, the operator will signal a {@link MissingBackpressureException} to + * its {@code Subscriber}s.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code share} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @return the new {@code Flowable} instance + * @see <a href="http://reactivex.io/documentation/operators/refcount.html">ReactiveX operators documentation: RefCount</a> + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Flowable<T> share() { + return publish().refCount(); + } + + /** + * Returns a {@link Maybe} that completes if this {@code Flowable} is empty, signals one item if this {@code Flowable} + * signals exactly one item or signals an {@link IllegalArgumentException} if this {@code Flowable} signals + * more than one item. + * <p> + * <img width="640" height="315" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/single.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream and consumes the current {@code Flowable} in an + * unbounded manner (i.e., without applying backpressure).</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code singleElement} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @return the new {@code Maybe} instance + * @see <a href="http://reactivex.io/documentation/operators/first.html">ReactiveX operators documentation: First</a> + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.UNBOUNDED_IN) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Maybe<T> singleElement() { + return RxJavaPlugins.onAssembly(new FlowableSingleMaybe<>(this)); + } + + /** + * Returns a {@link Single} that emits the single item emitted by the current {@code Flowable} if it + * emits only a single item, or a default item if the current {@code Flowable} emits no items. If the current + * {@code Flowable} emits more than one item, an {@link IllegalArgumentException} is signaled instead. + * <p> + * <img width="640" height="315" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/singleOrDefault.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream and consumes the current {@code Flowable} in an + * unbounded manner (i.e., without applying backpressure).</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code single} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param defaultItem + * a default value to emit if the current {@code Flowable} emits no item + * @return the new {@code Single} instance + * @throws NullPointerException if {@code defaultItem} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/first.html">ReactiveX operators documentation: First</a> + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.UNBOUNDED_IN) + @SchedulerSupport(SchedulerSupport.NONE) + public final Single<T> single(@NonNull T defaultItem) { + Objects.requireNonNull(defaultItem, "defaultItem is null"); + return RxJavaPlugins.onAssembly(new FlowableSingleSingle<>(this, defaultItem)); + } + + /** + * Returns a {@link Single} that emits the single item emitted by this {@code Flowable}, if this {@code Flowable} + * emits only a single item, otherwise + * if this {@code Flowable} completes without emitting any items a {@link NoSuchElementException} will be signaled and + * if this {@code Flowable} emits more than one item, an {@link IllegalArgumentException} will be signaled. + * <p> + * <img width="640" height="206" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/singleOrError.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream and consumes the current {@code Flowable} in an + * unbounded manner (i.e., without applying backpressure).</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code singleOrError} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @return the new {@code Single} instance + * @see <a href="http://reactivex.io/documentation/operators/first.html">ReactiveX operators documentation: First</a> + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.UNBOUNDED_IN) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Single<T> singleOrError() { + return RxJavaPlugins.onAssembly(new FlowableSingleSingle<>(this, null)); + } + + /** + * Returns a {@code Flowable} that skips the first {@code count} items emitted by the current {@code Flowable} and emits + * the remainder. + * <p> + * <img width="640" height="305" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/skip.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator doesn't interfere with backpressure which is determined by the current {@code Flowable}'s backpressure + * behavior.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>This version of {@code skip} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param count + * the number of items to skip + * @return the new {@code Flowable} instance + * @throws IllegalArgumentException if {@code count} is negative + * @see <a href="http://reactivex.io/documentation/operators/skip.html">ReactiveX operators documentation: Skip</a> + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Flowable<T> skip(long count) { + if (count < 0) { + throw new IllegalArgumentException("count >= 0 expected but it was " + count); + } + if (count == 0) { + return RxJavaPlugins.onAssembly(this); + } + return RxJavaPlugins.onAssembly(new FlowableSkip<>(this, count)); + } + + /** + * Returns a {@code Flowable} that skips values emitted by the current {@code Flowable} before a specified time window + * elapses. + * <p> + * <img width="640" height="305" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/skip.t.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator doesn't support backpressure as it uses time to skip an arbitrary number of elements and + * thus has to consume the current {@code Flowable} in an unbounded manner (i.e., no backpressure applied to it).</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code skip} does not operate on any particular scheduler but uses the current time + * from the {@code computation} {@link Scheduler}.</dd> + * </dl> + * + * @param time + * the length of the time window to skip + * @param unit + * the time unit of {@code time} + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code unit} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/skip.html">ReactiveX operators documentation: Skip</a> + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Flowable<T> skip(long time, @NonNull TimeUnit unit) { + return skipUntil(timer(time, unit)); + } + + /** + * Returns a {@code Flowable} that skips values emitted by the current {@code Flowable} before a specified time window + * on a specified {@link Scheduler} elapses. + * <p> + * <img width="640" height="305" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/skip.ts.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator doesn't support backpressure as it uses time to skip an arbitrary number of elements and + * thus has to consume the current {@code Flowable} in an unbounded manner (i.e., no backpressure applied to it).</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>You specify which {@code Scheduler} this operator will use for the timed skipping</dd> + * </dl> + * + * @param time + * the length of the time window to skip + * @param unit + * the time unit of {@code time} + * @param scheduler + * the {@code Scheduler} on which the timed wait happens + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code unit} or {@code scheduler} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/skip.html">ReactiveX operators documentation: Skip</a> + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.CUSTOM) + @NonNull + public final Flowable<T> skip(long time, @NonNull TimeUnit unit, @NonNull Scheduler scheduler) { + return skipUntil(timer(time, unit, scheduler)); + } + + /** + * Returns a {@code Flowable} that drops a specified number of items from the end of the sequence emitted by the + * current {@code Flowable}. + * <p> + * <img width="640" height="305" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/skipLast.v3.png" alt=""> + * <p> + * This {@link Subscriber} accumulates a queue long enough to store the first {@code count} items. As more items are + * received, items are taken from the front of the queue and emitted by the resulting {@code Flowable}. This causes + * such items to be delayed. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator doesn't interfere with backpressure which is determined by the current {@code Flowable}'s backpressure + * behavior.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>This version of {@code skipLast} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param count + * number of items to drop from the end of the source sequence + * @return the new {@code Flowable} instance + * @throws IllegalArgumentException + * if {@code count} is less than zero + * @see <a href="http://reactivex.io/documentation/operators/skiplast.html">ReactiveX operators documentation: SkipLast</a> + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Flowable<T> skipLast(int count) { + if (count < 0) { + throw new IllegalArgumentException("count >= 0 required but it was " + count); + } + if (count == 0) { + return RxJavaPlugins.onAssembly(this); + } + return RxJavaPlugins.onAssembly(new FlowableSkipLast<>(this, count)); + } + + /** + * Returns a {@code Flowable} that drops items emitted by the current {@code Flowable} during a specified time window + * before the source completes. + * <p> + * <img width="640" height="305" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/skipLast.t.v3.png" alt=""> + * <p> + * Note: this action will cache the latest items arriving in the specified time window. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator doesn't support backpressure as it uses time to skip an arbitrary number of elements and + * thus has to consume the current {@code Flowable} in an unbounded manner (i.e., no backpressure applied to it).</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code skipLast} does not operate on any particular scheduler but uses the current time + * from the {@code computation} {@link Scheduler}.</dd> + * </dl> + * + * @param time + * the length of the time window + * @param unit + * the time unit of {@code time} + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code unit} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/skiplast.html">ReactiveX operators documentation: SkipLast</a> + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.UNBOUNDED_IN) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Flowable<T> skipLast(long time, @NonNull TimeUnit unit) { + return skipLast(time, unit, Schedulers.computation(), false, bufferSize()); + } + + /** + * Returns a {@code Flowable} that drops items emitted by the current {@code Flowable} during a specified time window + * before the source completes. + * <p> + * <img width="640" height="305" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/skipLast.t.v3.png" alt=""> + * <p> + * Note: this action will cache the latest items arriving in the specified time window. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator doesn't support backpressure as it uses time to skip an arbitrary number of elements and + * thus has to consume the current {@code Flowable} in an unbounded manner (i.e., no backpressure applied to it).</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code skipLast} does not operate on any particular scheduler but uses the current time + * from the {@code computation} {@link Scheduler}.</dd> + * </dl> + * + * @param time + * the length of the time window + * @param unit + * the time unit of {@code time} + * @param delayError + * if {@code true}, an exception signaled by the current {@code Flowable} is delayed until the regular elements are consumed + * by the downstream; if {@code false}, an exception is immediately signaled and all regular elements dropped + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code unit} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/skiplast.html">ReactiveX operators documentation: SkipLast</a> + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.UNBOUNDED_IN) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Flowable<T> skipLast(long time, @NonNull TimeUnit unit, boolean delayError) { + return skipLast(time, unit, Schedulers.computation(), delayError, bufferSize()); + } + + /** + * Returns a {@code Flowable} that drops items emitted by the current {@code Flowable} during a specified time window + * (defined on a specified scheduler) before the source completes. + * <p> + * <img width="640" height="340" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/skipLast.ts.v3.png" alt=""> + * <p> + * Note: this action will cache the latest items arriving in the specified time window. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator doesn't support backpressure as it uses time to skip an arbitrary number of elements and + * thus has to consume the current {@code Flowable} in an unbounded manner (i.e., no backpressure applied to it).</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>You specify which {@link Scheduler} this operator will use for tracking the current time</dd> + * </dl> + * + * @param time + * the length of the time window + * @param unit + * the time unit of {@code time} + * @param scheduler + * the scheduler used as the time source + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code unit} or {@code scheduler} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/skiplast.html">ReactiveX operators documentation: SkipLast</a> + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.UNBOUNDED_IN) + @SchedulerSupport(SchedulerSupport.CUSTOM) + @NonNull + public final Flowable<T> skipLast(long time, @NonNull TimeUnit unit, @NonNull Scheduler scheduler) { + return skipLast(time, unit, scheduler, false, bufferSize()); + } + + /** + * Returns a {@code Flowable} that drops items emitted by the current {@code Flowable} during a specified time window + * (defined on a specified scheduler) before the source completes. + * <p> + * <img width="640" height="340" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/skipLast.ts.v3.png" alt=""> + * <p> + * Note: this action will cache the latest items arriving in the specified time window. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator doesn't support backpressure as it uses time to skip an arbitrary number of elements and + * thus has to consume the current {@code Flowable} in an unbounded manner (i.e., no backpressure applied to it).</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>You specify which {@link Scheduler} this operator will use to track the current time</dd> + * </dl> + * + * @param time + * the length of the time window + * @param unit + * the time unit of {@code time} + * @param scheduler + * the scheduler used as the time source + * @param delayError + * if {@code true}, an exception signaled by the current {@code Flowable} is delayed until the regular elements are consumed + * by the downstream; if {@code false}, an exception is immediately signaled and all regular elements dropped + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code unit} or {@code scheduler} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/skiplast.html">ReactiveX operators documentation: SkipLast</a> + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.UNBOUNDED_IN) + @SchedulerSupport(SchedulerSupport.CUSTOM) + @NonNull + public final Flowable<T> skipLast(long time, @NonNull TimeUnit unit, @NonNull Scheduler scheduler, boolean delayError) { + return skipLast(time, unit, scheduler, delayError, bufferSize()); + } + + /** + * Returns a {@code Flowable} that drops items emitted by the current {@code Flowable} during a specified time window + * (defined on a specified scheduler) before the source completes. + * <p> + * <img width="640" height="340" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/skipLast.ts.v3.png" alt=""> + * <p> + * Note: this action will cache the latest items arriving in the specified time window. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator doesn't support backpressure as it uses time to skip an arbitrary number of elements and + * thus has to consume the current {@code Flowable} in an unbounded manner (i.e., no backpressure applied to it).</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>You specify which {@link Scheduler} this operator will use.</dd> + * </dl> + * + * @param time + * the length of the time window + * @param unit + * the time unit of {@code time} + * @param scheduler + * the scheduler used as the time source + * @param delayError + * if {@code true}, an exception signaled by the current {@code Flowable} is delayed until the regular elements are consumed + * by the downstream; if {@code false}, an exception is immediately signaled and all regular elements dropped + * @param bufferSize + * the hint about how many elements to expect to be skipped + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code unit} or {@code scheduler} is {@code null} + * @throws IllegalArgumentException if {@code bufferSize} is non-positive + * @see <a href="http://reactivex.io/documentation/operators/skiplast.html">ReactiveX operators documentation: SkipLast</a> + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.UNBOUNDED_IN) + @SchedulerSupport(SchedulerSupport.CUSTOM) + public final Flowable<T> skipLast(long time, @NonNull TimeUnit unit, @NonNull Scheduler scheduler, boolean delayError, int bufferSize) { + Objects.requireNonNull(unit, "unit is null"); + Objects.requireNonNull(scheduler, "scheduler is null"); + ObjectHelper.verifyPositive(bufferSize, "bufferSize"); + // the internal buffer holds pairs of (timestamp, value) so double the default buffer size + int s = bufferSize << 1; + return RxJavaPlugins.onAssembly(new FlowableSkipLastTimed<>(this, time, unit, scheduler, s, delayError)); + } + + /** + * Returns a {@code Flowable} that skips items emitted by the current {@code Flowable} until a second {@link Publisher} emits + * an item. + * <p> + * <img width="640" height="375" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/skipUntil.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator doesn't interfere with backpressure which is determined by the current {@code Flowable}'s backpressure + * behavior.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code skipUntil} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <U> the element type of the other {@code Publisher} + * @param other + * the second {@code Publisher} that has to emit an item before the current {@code Flowable}'s elements begin + * to be mirrored by the resulting {@code Flowable} + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code other} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/skipuntil.html">ReactiveX operators documentation: SkipUntil</a> + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + public final <@NonNull U> Flowable<T> skipUntil(@NonNull Publisher<U> other) { + Objects.requireNonNull(other, "other is null"); + return RxJavaPlugins.onAssembly(new FlowableSkipUntil<>(this, other)); + } + + /** + * Returns a {@code Flowable} that skips all items emitted by the current {@code Flowable} as long as a specified + * condition holds {@code true}, but emits all further source items as soon as the condition becomes {@code false}. + * <p> + * <img width="640" height="305" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/skipWhile.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator doesn't interfere with backpressure which is determined by the current {@code Flowable}'s backpressure + * behavior.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code skipWhile} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param predicate + * a function to test each item emitted from the current {@code Flowable} + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code predicate} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/skipwhile.html">ReactiveX operators documentation: SkipWhile</a> + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + public final Flowable<T> skipWhile(@NonNull Predicate<? super T> predicate) { + Objects.requireNonNull(predicate, "predicate is null"); + return RxJavaPlugins.onAssembly(new FlowableSkipWhile<>(this, predicate)); + } + /** + * Returns a {@code Flowable} that emits the events emitted by source {@link Publisher}, in a + * sorted order. Each item emitted by the {@code Publisher} must implement {@link Comparable} with respect to all + * other items in the sequence. + * + * <p>If any item emitted by this {@code Flowable} does not implement {@code Comparable} with respect to + * all other items emitted by this {@code Flowable}, no items will be emitted and the + * sequence is terminated with a {@link ClassCastException}. + * <p>Note that calling {@code sorted} with long, non-terminating or infinite sources + * might cause {@link OutOfMemoryError} + * + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream and consumes the current {@code Flowable} in an + * unbounded manner (i.e., without applying backpressure to it).</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code sorted} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @return the new {@code Flowable} instance + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Flowable<T> sorted() { + return toList().toFlowable().map(Functions.listSorter(Functions.naturalComparator())).flatMapIterable(Functions.identity()); + } + + /** + * Returns a {@code Flowable} that emits the events emitted by source {@link Publisher}, in a + * sorted order based on a specified comparison function. + * + * <p>Note that calling {@code sorted} with long, non-terminating or infinite sources + * might cause {@link OutOfMemoryError} + * + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream and consumes the current {@code Flowable} in an + * unbounded manner (i.e., without applying backpressure to it).</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code sorted} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param comparator + * a function that compares two items emitted by the current {@code Flowable} and returns an {@link Integer} + * that indicates their sort order + * @throws NullPointerException if {@code comparator} is {@code null} + * @return the new {@code Flowable} instance + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + public final Flowable<T> sorted(@NonNull Comparator<@NonNull ? super T> comparator) { + Objects.requireNonNull(comparator, "comparator is null"); + return toList().toFlowable().map(Functions.listSorter(comparator)).flatMapIterable(Functions.identity()); + } + + /** + * Returns a {@code Flowable} that emits the items in a specified {@link Iterable} before it begins to emit items + * emitted by the current {@code Flowable}. + * <p> + * <img width="640" height="315" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/startWith.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream. The Current {@code Flowable} + * is expected to honor backpressure as well. If it violates this rule, it <em>may</em> throw an + * {@link IllegalStateException} when the current {@code Flowable} completes.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code startWithIterable} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param items + * an {@code Iterable} that contains the items you want the resulting {@code Flowable} to emit first + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code items} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/startwith.html">ReactiveX operators documentation: StartWith</a> + * @see #startWithArray(Object...) + * @see #startWithItem(Object) + * @since 3.0.0 + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Flowable<T> startWithIterable(@NonNull Iterable<? extends T> items) { + return concatArray(fromIterable(items), this); + } + + /** + * Returns a {@code Flowable} which first runs the other {@link CompletableSource} + * then the current {@code Flowable} if the other completed normally. + * <p> + * <img width="640" height="268" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Flowable.startWith.c.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The returned {@code Flowable} honors the backpressure of the downstream consumer.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code startWith} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param other the other {@code CompletableSource} to run first + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code other} is {@code null} + * @since 3.0.0 + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + @BackpressureSupport(BackpressureKind.FULL) + public final Flowable<T> startWith(@NonNull CompletableSource other) { + Objects.requireNonNull(other, "other is null"); + return Flowable.concat(Completable.wrap(other).<T>toFlowable(), this); + } + + /** + * Returns a {@code Flowable} which first runs the other {@link SingleSource} + * then the current {@code Flowable} if the other succeeded normally. + * <p> + * <img width="640" height="248" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Flowable.startWith.s.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The returned {@code Flowable} honors the backpressure of the downstream consumer.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code startWith} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param other the other {@code SingleSource} to run first + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code other} is {@code null} + * @since 3.0.0 + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + @BackpressureSupport(BackpressureKind.FULL) + public final Flowable<T> startWith(@NonNull SingleSource<T> other) { + Objects.requireNonNull(other, "other is null"); + return Flowable.concat(Single.wrap(other).toFlowable(), this); + } + + /** + * Returns a {@code Flowable} which first runs the other {@link MaybeSource} + * then the current {@code Flowable} if the other succeeded or completed normally. + * <p> + * <img width="640" height="168" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Flowable.startWith.m.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The returned {@code Flowable} honors the backpressure of the downstream consumer.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code startWith} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param other the other {@code MaybeSource} to run first + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code other} is {@code null} + * @since 3.0.0 + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + @BackpressureSupport(BackpressureKind.FULL) + public final Flowable<T> startWith(@NonNull MaybeSource<T> other) { + Objects.requireNonNull(other, "other is null"); + return Flowable.concat(Maybe.wrap(other).toFlowable(), this); + } + + /** + * Returns a {@code Flowable} that emits the items in a specified {@link Publisher} before it begins to emit + * items emitted by the current {@code Flowable}. + * <p> + * <img width="640" height="315" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/startWith.o.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream. Both this and the {@code other} {@code Publisher}s + * are expected to honor backpressure as well. If any of then violates this rule, it <em>may</em> throw an + * {@link IllegalStateException} when the current {@code Flowable} completes.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code startWith} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param other + * a {@code Publisher} that contains the items you want the modified {@code Publisher} to emit first + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code other} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/startwith.html">ReactiveX operators documentation: StartWith</a> + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + public final Flowable<T> startWith(@NonNull Publisher<? extends T> other) { + Objects.requireNonNull(other, "other is null"); + return concatArray(other, this); + } + + /** + * Returns a {@code Flowable} that emits a specified item before it begins to emit items emitted by the current + * {@code Flowable}. + * <p> + * <img width="640" height="315" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/startWith.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream. The current {@code Flowable} + * is expected to honor backpressure as well. If it violates this rule, it <em>may</em> throw an + * {@link IllegalStateException} when the current {@code Flowable} completes.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code startWithItem} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param item + * the item to emit first + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code item} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/startwith.html">ReactiveX operators documentation: StartWith</a> + * @see #startWithArray(Object...) + * @see #startWithIterable(Iterable) + * @since 3.0.0 + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + public final Flowable<T> startWithItem(@NonNull T item) { + Objects.requireNonNull(item, "item is null"); + return concatArray(just(item), this); + } + + /** + * Returns a {@code Flowable} that emits the specified items before it begins to emit items emitted by the current + * {@code Flowable}. + * <p> + * <img width="640" height="315" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/startWith.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream. The current {@code Flowable} + * is expected to honor backpressure as well. If it violates this rule, it <em>may</em> throw an + * {@link IllegalStateException} when the current {@code Flowable} completes.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code startWithArray} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param items + * the array of values to emit first + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code items} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/startwith.html">ReactiveX operators documentation: StartWith</a> + * @see #startWithItem(Object) + * @see #startWithIterable(Iterable) + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + @SafeVarargs + @NonNull + public final Flowable<T> startWithArray(@NonNull T... items) { + Flowable<T> fromArray = fromArray(items); + if (fromArray == empty()) { + return RxJavaPlugins.onAssembly(this); + } + return concatArray(fromArray, this); + } + + /** + * Subscribes to the current {@code Flowable} and ignores {@code onNext} and {@code onComplete} emissions. + * <p> + * If the {@code Flowable} emits an error, it is wrapped into an + * {@link io.reactivex.rxjava3.exceptions.OnErrorNotImplementedException OnErrorNotImplementedException} + * and routed to the {@link RxJavaPlugins#onError(Throwable)} handler. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator consumes the current {@code Flowable} in an unbounded manner (i.e., no + * backpressure is applied to it).</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code subscribe} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @return the new {@link Disposable} instance that allows cancelling the flow + * @see <a href="http://reactivex.io/documentation/operators/subscribe.html">ReactiveX operators documentation: Subscribe</a> + * @see #subscribe(Consumer, Consumer, Action, DisposableContainer) + */ + @BackpressureSupport(BackpressureKind.UNBOUNDED_IN) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Disposable subscribe() { + return subscribe(Functions.emptyConsumer(), Functions.ON_ERROR_MISSING, Functions.EMPTY_ACTION); + } + + /** + * Subscribes to the current {@code Flowable} and provides a callback to handle the items it emits. + * <p> + * If the {@code Flowable} emits an error, it is wrapped into an + * {@link io.reactivex.rxjava3.exceptions.OnErrorNotImplementedException OnErrorNotImplementedException} + * and routed to the {@link RxJavaPlugins#onError(Throwable)} handler. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator consumes the current {@code Flowable} in an unbounded manner (i.e., no + * backpressure is applied to it).</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code subscribe} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param onNext + * the {@code Consumer<T>} you have designed to accept emissions from the current {@code Flowable} + * @return the new {@link Disposable} instance that allows cancelling the flow + * @throws NullPointerException + * if {@code onNext} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/subscribe.html">ReactiveX operators documentation: Subscribe</a> + * @see #subscribe(Consumer, Consumer, Action, DisposableContainer) + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.UNBOUNDED_IN) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Disposable subscribe(@NonNull Consumer<? super T> onNext) { + return subscribe(onNext, Functions.ON_ERROR_MISSING, Functions.EMPTY_ACTION); + } + + /** + * Subscribes to the current {@code Flowable} and provides callbacks to handle the items it emits and any error + * notification it issues. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator consumes the current {@code Flowable} in an unbounded manner (i.e., no + * backpressure is applied to it).</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code subscribe} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param onNext + * the {@code Consumer<T>} you have designed to accept emissions from the current {@code Flowable} + * @param onError + * the {@code Consumer<Throwable>} you have designed to accept any error notification from the + * current {@code Flowable} + * @return the new {@link Disposable} instance that allows cancelling the flow + * @throws NullPointerException + * if {@code onNext} or {@code onError} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/subscribe.html">ReactiveX operators documentation: Subscribe</a> + * @see #subscribe(Consumer, Consumer, Action, DisposableContainer) + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.UNBOUNDED_IN) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Disposable subscribe(@NonNull Consumer<? super T> onNext, @NonNull Consumer<? super Throwable> onError) { + return subscribe(onNext, onError, Functions.EMPTY_ACTION); + } + + /** + * Subscribes to the current {@code Flowable} and provides callbacks to handle the items it emits and any error or + * completion notification it issues. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator consumes the current {@code Flowable} in an unbounded manner (i.e., no + * backpressure is applied to it).</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code subscribe} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param onNext + * the {@code Consumer<T>} you have designed to accept emissions from the current {@code Flowable} + * @param onError + * the {@code Consumer<Throwable>} you have designed to accept any error notification from the + * current {@code Flowable} + * @param onComplete + * the {@link Action} you have designed to accept a completion notification from the + * the current {@code Flowable} + * @return the new {@link Disposable} instance that allows cancelling the flow + * @throws NullPointerException + * if {@code onNext}, {@code onError} or {@code onComplete} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/subscribe.html">ReactiveX operators documentation: Subscribe</a> + * @see #subscribe(Consumer, Consumer, Action, DisposableContainer) + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.UNBOUNDED_IN) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Disposable subscribe(@NonNull Consumer<? super T> onNext, @NonNull Consumer<? super Throwable> onError, + @NonNull Action onComplete) { + Objects.requireNonNull(onNext, "onNext is null"); + Objects.requireNonNull(onError, "onError is null"); + Objects.requireNonNull(onComplete, "onComplete is null"); + + LambdaSubscriber<T> ls = new LambdaSubscriber<>(onNext, onError, onComplete, FlowableInternalHelper.RequestMax.INSTANCE); + + subscribe(ls); + + return ls; + } + + /** + * Wraps the given onXXX callbacks into a {@link Disposable} {@link Subscriber}, + * adds it to the given {@link DisposableContainer} and ensures, that if the upstream + * terminates or this particular {@code Disposable} is disposed, the {@code Subscriber} is removed + * from the given container. + * <p> + * The {@code Subscriber} will be removed after the callback for the terminal event has been invoked. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator consumes the current {@code Flowable} in an unbounded manner (i.e., no + * backpressure is applied to it).</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code subscribe} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param onNext the callback for upstream items + * @param onError the callback for an upstream error if any + * @param onComplete the callback for the upstream completion if any + * @param container the {@code DisposableContainer} (such as {@link CompositeDisposable}) to add and remove the + * created {@code Disposable} {@code Subscriber} + * @return the {@code Disposable} that allows disposing the particular subscription. + * @throws NullPointerException + * if {@code onNext}, {@code onError}, + * {@code onComplete} or {@code container} is {@code null} + * @since 3.1.0 + */ + @BackpressureSupport(BackpressureKind.SPECIAL) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Disposable subscribe( + @NonNull Consumer<? super T> onNext, + @NonNull Consumer<? super Throwable> onError, + @NonNull Action onComplete, + @NonNull DisposableContainer container) { + Objects.requireNonNull(onNext, "onNext is null"); + Objects.requireNonNull(onError, "onError is null"); + Objects.requireNonNull(onComplete, "onComplete is null"); + Objects.requireNonNull(container, "container is null"); + + DisposableAutoReleaseSubscriber<T> subscriber = new DisposableAutoReleaseSubscriber<>( + container, onNext, onError, onComplete); + container.add(subscriber); + subscribe(subscriber); + return subscriber; + } + + @BackpressureSupport(BackpressureKind.SPECIAL) + @SchedulerSupport(SchedulerSupport.NONE) + @Override + public final void subscribe(@NonNull Subscriber<? super T> subscriber) { + if (subscriber instanceof FlowableSubscriber) { + subscribe((FlowableSubscriber<? super T>)subscriber); + } else { + Objects.requireNonNull(subscriber, "subscriber is null"); + subscribe(new StrictSubscriber<>(subscriber)); + } + } + + /** + * Establish a connection between this {@code Flowable} and the given {@link FlowableSubscriber} and + * start streaming events based on the demand of the {@code FlowableSubscriber}. + * <p> + * This is a "factory method" and can be called multiple times, each time starting a new {@link Subscription}. + * <p> + * Each {@code Subscription} will work for only a single {@code FlowableSubscriber}. + * <p> + * If the same {@code FlowableSubscriber} instance is subscribed to multiple {@code Flowable}s and/or the + * same {@code Flowable} multiple times, it must ensure the serialization over its {@code onXXX} + * methods manually. + * <p> + * If the {@code Flowable} rejects the subscription attempt or otherwise fails it will signal + * the error via {@link FlowableSubscriber#onError(Throwable)}. + * <p> + * This subscribe method relaxes the following <em>Reactive Streams</em> rules: + * <ul> + * <li>§1.3: {@code onNext} should not be called concurrently until {@code onSubscribe} returns. + * <b>{@link FlowableSubscriber#onSubscribe(Subscription)} should make sure a sync or async call triggered by request() is safe.</b></li> + * <li>§2.3: {@code onError} or {@code onComplete} must not call cancel. + * <b>Calling request() or cancel() is NOP at this point.</b></li> + * <li>§2.12: {@code onSubscribe} must be called at most once on the same instance. + * <b>{@code FlowableSubscriber} reuse is not checked and if happens, it is the responsibility of + * the {@code FlowableSubscriber} to ensure proper serialization of its onXXX methods.</b></li> + * <li>§3.9: negative requests should emit an {@code onError(IllegalArgumentException)}. + * <b>Non-positive requests signal via {@link RxJavaPlugins#onError(Throwable)} and the stream is not affected.</b></li> + * </ul> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The backpressure behavior/expectation is determined by the supplied {@code FlowableSubscriber}.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code subscribe} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * <p>History: 2.0.7 - experimental; 2.1 - beta + * @param subscriber the {@code FlowableSubscriber} that will consume signals from this {@code Flowable} + * @throws NullPointerException if {@code subscriber} is {@code null} + * @since 2.2 + */ + @BackpressureSupport(BackpressureKind.SPECIAL) + @SchedulerSupport(SchedulerSupport.NONE) + public final void subscribe(@NonNull FlowableSubscriber<? super T> subscriber) { + Objects.requireNonNull(subscriber, "subscriber is null"); + try { + Subscriber<? super T> flowableSubscriber = RxJavaPlugins.onSubscribe(this, subscriber); + + Objects.requireNonNull(flowableSubscriber, "The RxJavaPlugins.onSubscribe hook returned a null FlowableSubscriber. Please check the handler provided to RxJavaPlugins.setOnFlowableSubscribe for invalid null returns. Further reading: https://github.com/ReactiveX/RxJava/wiki/Plugins"); + + subscribeActual(flowableSubscriber); + } catch (NullPointerException e) { // NOPMD + throw e; + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + // can't call onError because no way to know if a Subscription has been set or not + // can't call onSubscribe because the call might have set a Subscription already + RxJavaPlugins.onError(e); + + NullPointerException npe = new NullPointerException("Actually not, but can't throw other exceptions due to RS"); + npe.initCause(e); + throw npe; + } + } + + /** + * Operator implementations (both source and intermediate) should implement this method that + * performs the necessary business logic and handles the incoming {@link Subscriber}s. + * <p>There is no need to call any of the plugin hooks on the current {@code Flowable} instance or + * the {@code Subscriber}; all hooks and basic safeguards have been + * applied by {@link #subscribe(Subscriber)} before this method gets called. + * @param subscriber the incoming {@code Subscriber}, never {@code null} + */ + protected abstract void subscribeActual(@NonNull Subscriber<? super T> subscriber); + + /** + * Subscribes a given {@link Subscriber} (subclass) to this {@code Flowable} and returns the given + * {@code Subscriber} as is. + * <p>Usage example: + * <pre><code> + * Flowable<Integer> source = Flowable.range(1, 10); + * CompositeDisposable composite = new CompositeDisposable(); + * + * ResourceSubscriber<Integer> rs = new ResourceSubscriber<>() { + * // ... + * }; + * + * composite.add(source.subscribeWith(rs)); + * </code></pre> + * + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The backpressure behavior/expectation is determined by the supplied {@code Subscriber}.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code subscribeWith} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <E> the type of the {@code Subscriber} to use and return + * @param subscriber the {@code Subscriber} (subclass) to use and return, not {@code null} + * @return the input {@code subscriber} + * @throws NullPointerException if {@code subscriber} is {@code null} + * @since 2.0 + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.SPECIAL) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull E extends Subscriber<? super T>> E subscribeWith(E subscriber) { + subscribe(subscriber); + return subscriber; + } + + /** + * Asynchronously subscribes {@link Subscriber}s to the current {@code Flowable} on the specified {@link Scheduler}. + * <p> + * If there is a {@link #create(FlowableOnSubscribe, BackpressureStrategy)} type source up in the + * chain, it is recommended to use {@code subscribeOn(scheduler, false)} instead + * to avoid same-pool deadlock because requests may pile up behind an eager/blocking emitter. + * <p> + * <img width="640" height="305" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/subscribeOn.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator doesn't interfere with backpressure which is determined by the current {@code Flowable}'s backpressure + * behavior.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>You specify which {@code Scheduler} this operator will use.</dd> + * </dl> + * + * @param scheduler + * the {@code Scheduler} to perform subscription actions on + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code scheduler} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/subscribeon.html">ReactiveX operators documentation: SubscribeOn</a> + * @see <a href="http://www.grahamlea.com/2014/07/rxjava-threading-examples/">RxJava Threading Examples</a> + * @see #observeOn + * @see #subscribeOn(Scheduler, boolean) + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.PASS_THROUGH) + @SchedulerSupport(SchedulerSupport.CUSTOM) + public final Flowable<T> subscribeOn(@NonNull Scheduler scheduler) { + Objects.requireNonNull(scheduler, "scheduler is null"); + return subscribeOn(scheduler, !(this instanceof FlowableCreate)); + } + + /** + * Asynchronously subscribes {@link Subscriber}s to the current {@code Flowable} on the specified {@link Scheduler} + * optionally reroutes requests from other threads to the same {@code Scheduler} thread. + * <p> + * If there is a {@link #create(FlowableOnSubscribe, BackpressureStrategy)} type source up in the + * chain, it is recommended to have {@code requestOn} {@code false} to avoid same-pool deadlock + * because requests may pile up behind an eager/blocking emitter. + * <p> + * <img width="640" height="305" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/subscribeOn.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator doesn't interfere with backpressure which is determined by the current {@code Flowable}'s backpressure + * behavior.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>You specify which {@code Scheduler} this operator will use.</dd> + * </dl> + * <p>History: 2.1.1 - experimental + * @param scheduler + * the {@code Scheduler} to perform subscription actions on + * @param requestOn if {@code true}, requests are rerouted to the given {@code Scheduler} as well (strong pipelining) + * if {@code false}, requests coming from any thread are simply forwarded to + * the upstream on the same thread (weak pipelining) + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code scheduler} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/subscribeon.html">ReactiveX operators documentation: SubscribeOn</a> + * @see <a href="http://www.grahamlea.com/2014/07/rxjava-threading-examples/">RxJava Threading Examples</a> + * @see #observeOn + * @since 2.2 + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.PASS_THROUGH) + @SchedulerSupport(SchedulerSupport.CUSTOM) + public final Flowable<T> subscribeOn(@NonNull Scheduler scheduler, boolean requestOn) { + Objects.requireNonNull(scheduler, "scheduler is null"); + return RxJavaPlugins.onAssembly(new FlowableSubscribeOn<>(this, scheduler, requestOn)); + } + + /** + * Returns a {@code Flowable} that emits the items emitted by the current {@code Flowable} or the items of an alternate + * {@link Publisher} if the current {@code Flowable} is empty. + * <p> + * <img width="640" height="256" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/switchifempty.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>If the current {@code Flowable} is empty, the alternate {@code Publisher} is expected to honor backpressure. + * If the current {@code Flowable} is non-empty, it is expected to honor backpressure as instead. + * In either case, if violated, a {@link MissingBackpressureException} <em>may</em> get + * signaled somewhere downstream. + * </dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code switchIfEmpty} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param other + * the alternate {@code Publisher} to subscribe to if the source does not emit any items + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code other} is {@code null} + * @since 1.1.0 + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + public final Flowable<T> switchIfEmpty(@NonNull Publisher<? extends T> other) { + Objects.requireNonNull(other, "other is null"); + return RxJavaPlugins.onAssembly(new FlowableSwitchIfEmpty<>(this, other)); + } + + /** + * Returns a new {@code Flowable} by applying a function that you supply to each item emitted by the current + * {@code Flowable} that returns a {@link Publisher}, and then emitting the items emitted by the most recently emitted + * of these {@code Publisher}s. + * <p> + * The resulting {@code Flowable} completes if both the current {@code Flowable} and the last inner {@code Publisher}, if any, complete. + * If the current {@code Flowable} signals an {@code onError}, the inner {@code Publisher} is canceled and the error delivered in-sequence. + * <p> + * <img width="640" height="350" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/switchMap.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream. The outer {@code Publisher} is consumed in an + * unbounded manner (i.e., without backpressure) and the inner {@code Publisher}s are expected to honor + * backpressure but it is not enforced; the operator won't signal a {@link MissingBackpressureException} + * but the violation <em>may</em> lead to {@link OutOfMemoryError} due to internal buffer bloat.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code switchMap} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <R> the element type of the inner {@code Publisher}s and the output + * @param mapper + * a function that, when applied to an item emitted by the current {@code Flowable}, returns a + * {@code Publisher} + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code mapper} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/flatmap.html">ReactiveX operators documentation: FlatMap</a> + * @see #switchMapDelayError(Function) + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull R> Flowable<R> switchMap(@NonNull Function<? super T, @NonNull ? extends Publisher<? extends R>> mapper) { + return switchMap(mapper, bufferSize()); + } + + /** + * Returns a new {@code Flowable} by applying a function that you supply to each item emitted by the current + * {@code Flowable} that returns a {@link Publisher}, and then emitting the items emitted by the most recently emitted + * of these {@code Publisher}s. + * <p> + * The resulting {@code Flowable} completes if both the current {@code Flowable} and the last inner {@code Publisher}, if any, complete. + * If the current {@code Flowable} signals an {@code onError}, the inner {@code Publisher} is canceled and the error delivered in-sequence. + * <p> + * <img width="640" height="350" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/switchMap.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream. The outer {@code Publisher} is consumed in an + * unbounded manner (i.e., without backpressure) and the inner {@code Publisher}s are expected to honor + * backpressure but it is not enforced; the operator won't signal a {@link MissingBackpressureException} + * but the violation <em>may</em> lead to {@link OutOfMemoryError} due to internal buffer bloat.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code switchMap} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <R> the element type of the inner {@code Publisher}s and the output + * @param mapper + * a function that, when applied to an item emitted by the current {@code Flowable}, returns a + * {@code Publisher} + * @param bufferSize + * the number of elements to prefetch from the current active inner {@code Publisher} + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code mapper} is {@code null} + * @throws IllegalArgumentException if {@code bufferSize} is non-positive + * @see <a href="http://reactivex.io/documentation/operators/flatmap.html">ReactiveX operators documentation: FlatMap</a> + * @see #switchMapDelayError(Function, int) + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull R> Flowable<R> switchMap(@NonNull Function<? super T, @NonNull ? extends Publisher<? extends R>> mapper, int bufferSize) { + return switchMap0(mapper, bufferSize, false); + } + + /** + * Maps the upstream values into {@link CompletableSource}s, subscribes to the newer one while + * disposing the subscription to the previous {@code CompletableSource}, thus keeping at most one + * active {@code CompletableSource} running. + * <p> + * <img width="640" height="522" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/switchMapCompletable.f.png" alt=""> + * <p> + * Since a {@code CompletableSource} doesn't produce any items, the resulting reactive type of + * this operator is a {@link Completable} that can only indicate successful completion or + * a failure in any of the inner {@code CompletableSource}s or the failure of the current + * {@code Flowable}. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator consumes the current {@code Flowable} in an unbounded manner and otherwise + * does not have backpressure in its return type because no items are ever produced.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code switchMapCompletable} does not operate by default on a particular {@link Scheduler}.</dd> + * <dt><b>Error handling:</b></dt> + * <dd>If either this {@code Flowable} or the active {@code CompletableSource} signals an {@code onError}, + * the resulting {@code Completable} is terminated immediately with that {@link Throwable}. + * Use the {@link #switchMapCompletableDelayError(Function)} to delay such inner failures until + * every inner {@code CompletableSource}s and the main {@code Flowable} terminates in some fashion. + * If they fail concurrently, the operator may combine the {@code Throwable}s into a + * {@link io.reactivex.rxjava3.exceptions.CompositeException CompositeException} + * and signal it to the downstream instead. If any inactivated (switched out) {@code CompletableSource} + * signals an {@code onError} late, the {@code Throwable}s will be signaled to the global error handler via + * {@link RxJavaPlugins#onError(Throwable)} method as {@link UndeliverableException} errors. + * </dd> + * </dl> + * <p>History: 2.1.11 - experimental + * @param mapper the function called with each upstream item and should return a + * {@code CompletableSource} to be subscribed to and awaited for + * (non blockingly) for its terminal event + * @return the new {@code Completable} instance + * @throws NullPointerException if {@code mapper} is {@code null} + * @see #switchMapCompletableDelayError(Function) + * @since 2.2 + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.UNBOUNDED_IN) + @SchedulerSupport(SchedulerSupport.NONE) + public final Completable switchMapCompletable(@NonNull Function<? super T, ? extends CompletableSource> mapper) { + Objects.requireNonNull(mapper, "mapper is null"); + return RxJavaPlugins.onAssembly(new FlowableSwitchMapCompletable<>(this, mapper, false)); + } + + /** + * Maps the upstream values into {@link CompletableSource}s, subscribes to the newer one while + * disposing the subscription to the previous {@code CompletableSource}, thus keeping at most one + * active {@code CompletableSource} running and delaying any main or inner errors until all + * of them terminate. + * <p> + * <img width="640" height="453" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/switchMapCompletableDelayError.f.png" alt=""> + * <p> + * Since a {@code CompletableSource} doesn't produce any items, the resulting reactive type of + * this operator is a {@link Completable} that can only indicate successful completion or + * a failure in any of the inner {@code CompletableSource}s or the failure of the current + * {@code Flowable}. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator consumes the current {@code Flowable} in an unbounded manner and otherwise + * does not have backpressure in its return type because no items are ever produced.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code switchMapCompletableDelayError} does not operate by default on a particular {@link Scheduler}.</dd> + * <dt><b>Error handling:</b></dt> + * <dd>The errors of this {@code Flowable} and all the {@code CompletableSource}s, who had the chance + * to run to their completion, are delayed until + * all of them terminate in some fashion. At this point, if there was only one failure, the respective + * {@link Throwable} is emitted to the downstream. If there was more than one failure, the + * operator combines all {@code Throwable}s into a {@link io.reactivex.rxjava3.exceptions.CompositeException CompositeException} + * and signals that to the downstream. + * If any inactivated (switched out) {@code CompletableSource} + * signals an {@code onError} late, the {@code Throwable}s will be signaled to the global error handler via + * {@link RxJavaPlugins#onError(Throwable)} method as {@link UndeliverableException} errors. + * </dd> + * </dl> + * <p>History: 2.1.11 - experimental + * @param mapper the function called with each upstream item and should return a + * {@code CompletableSource} to be subscribed to and awaited for + * (non blockingly) for its terminal event + * @return the new {@code Completable} instance + * @throws NullPointerException if {@code mapper} is {@code null} + * @see #switchMapCompletable(Function) + * @since 2.2 + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.UNBOUNDED_IN) + @SchedulerSupport(SchedulerSupport.NONE) + public final Completable switchMapCompletableDelayError(@NonNull Function<? super T, ? extends CompletableSource> mapper) { + Objects.requireNonNull(mapper, "mapper is null"); + return RxJavaPlugins.onAssembly(new FlowableSwitchMapCompletable<>(this, mapper, true)); + } + + /** + * Returns a new {@code Flowable} by applying a function that you supply to each item emitted by the current + * {@code Flowable} that returns a {@link Publisher}, and then emitting the items emitted by the most recently emitted + * of these {@code Publisher}s and delays any error until all {@code Publisher}s terminate. + * <p> + * The resulting {@code Flowable} completes if both the current {@code Flowable} and the last inner {@code Publisher}, if any, complete. + * If the current {@code Flowable} signals an {@code onError}, the termination of the last inner {@code Publisher} will emit that error as is + * or wrapped into a {@link CompositeException} along with the other possible errors the former inner {@code Publisher}s signaled. + * <p> + * <img width="640" height="350" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/switchMap.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream. The outer {@code Publisher} is consumed in an + * unbounded manner (i.e., without backpressure) and the inner {@code Publisher}s are expected to honor + * backpressure but it is not enforced; the operator won't signal a {@link MissingBackpressureException} + * but the violation <em>may</em> lead to {@link OutOfMemoryError} due to internal buffer bloat.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code switchMapDelayError} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <R> the element type of the inner {@code Publisher}s and the output + * @param mapper + * a function that, when applied to an item emitted by the current {@code Flowable}, returns a + * {@code Publisher} + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code mapper} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/flatmap.html">ReactiveX operators documentation: FlatMap</a> + * @see #switchMap(Function) + * @since 2.0 + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.SPECIAL) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull R> Flowable<R> switchMapDelayError(@NonNull Function<? super T, @NonNull ? extends Publisher<? extends R>> mapper) { + return switchMapDelayError(mapper, bufferSize()); + } + + /** + * Returns a new {@code Flowable} by applying a function that you supply to each item emitted by the current + * {@code Flowable} that returns a {@link Publisher}, and then emitting the items emitted by the most recently emitted + * of these {@code Publisher}s and delays any error until all {@code Publisher}s terminate. + * <p> + * The resulting {@code Flowable} completes if both the current {@code Flowable} and the last inner {@code Publisher}, if any, complete. + * If the current {@code Flowable} signals an {@code onError}, the termination of the last inner {@code Publisher} will emit that error as is + * or wrapped into a {@link CompositeException} along with the other possible errors the former inner {@code Publisher}s signaled. + * <p> + * <img width="640" height="350" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/switchMap.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream. The outer {@code Publisher} is consumed in an + * unbounded manner (i.e., without backpressure) and the inner {@code Publisher}s are expected to honor + * backpressure but it is not enforced; the operator won't signal a {@link MissingBackpressureException} + * but the violation <em>may</em> lead to {@link OutOfMemoryError} due to internal buffer bloat.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code switchMapDelayError} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <R> the element type of the inner {@code Publisher}s and the output + * @param mapper + * a function that, when applied to an item emitted by the current {@code Flowable}, returns a + * {@code Publisher} + * @param bufferSize + * the number of elements to prefetch from the current active inner {@code Publisher} + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code mapper} is {@code null} + * @throws IllegalArgumentException if {@code bufferSize} is non-positive + * @see <a href="http://reactivex.io/documentation/operators/flatmap.html">ReactiveX operators documentation: FlatMap</a> + * @see #switchMap(Function, int) + * @since 2.0 + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.SPECIAL) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull R> Flowable<R> switchMapDelayError(@NonNull Function<? super T, @NonNull ? extends Publisher<? extends R>> mapper, int bufferSize) { + return switchMap0(mapper, bufferSize, true); + } + + <R> Flowable<R> switchMap0(Function<? super T, @NonNull ? extends Publisher<? extends R>> mapper, int bufferSize, boolean delayError) { + Objects.requireNonNull(mapper, "mapper is null"); + ObjectHelper.verifyPositive(bufferSize, "bufferSize"); + if (this instanceof ScalarSupplier) { + @SuppressWarnings("unchecked") + T v = ((ScalarSupplier<T>)this).get(); + if (v == null) { + return empty(); + } + return FlowableScalarXMap.scalarXMap(v, mapper); + } + return RxJavaPlugins.onAssembly(new FlowableSwitchMap<>(this, mapper, bufferSize, delayError)); + } + + /** + * Maps the upstream items into {@link MaybeSource}s and switches (subscribes) to the newer ones + * while disposing the older ones (and ignoring their signals) and emits the latest success value of the current one if + * available while failing immediately if this {@code Flowable} or any of the + * active inner {@code MaybeSource}s fail. + * <p> + * <img width="640" height="350" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/switchMap.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream. The main {@code Flowable} is consumed in an + * unbounded manner (i.e., without backpressure).</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code switchMapMaybe} does not operate by default on a particular {@link Scheduler}.</dd> + * <dt><b>Error handling:</b></dt> + * <dd>This operator terminates with an {@code onError} if this {@code Flowable} or any of + * the inner {@code MaybeSource}s fail while they are active. When this happens concurrently, their + * individual {@link Throwable} errors may get combined and emitted as a single + * {@link CompositeException}. Otherwise, a late + * (i.e., inactive or switched out) {@code onError} from this {@code Flowable} or from any of + * the inner {@code MaybeSource}s will be forwarded to the global error handler via + * {@link io.reactivex.rxjava3.plugins.RxJavaPlugins#onError(Throwable)} as + * {@link io.reactivex.rxjava3.exceptions.UndeliverableException UndeliverableException}</dd> + * </dl> + * <p>History: 2.1.11 - experimental + * @param <R> the output value type + * @param mapper the function called with the current upstream event and should + * return a {@code MaybeSource} to replace the current active inner source + * and get subscribed to. + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code mapper} is {@code null} + * @see #switchMapMaybeDelayError(Function) + * @since 2.2 + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.UNBOUNDED_IN) + @SchedulerSupport(SchedulerSupport.NONE) + public final <@NonNull R> Flowable<R> switchMapMaybe(@NonNull Function<? super T, ? extends MaybeSource<? extends R>> mapper) { + Objects.requireNonNull(mapper, "mapper is null"); + return RxJavaPlugins.onAssembly(new FlowableSwitchMapMaybe<>(this, mapper, false)); + } + + /** + * Maps the upstream items into {@link MaybeSource}s and switches (subscribes) to the newer ones + * while disposing the older ones (and ignoring their signals) and emits the latest success value of the current one if + * available, delaying errors from this {@code Flowable} or the inner {@code MaybeSource}s until all terminate. + * <p> + * <img width="640" height="350" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/switchMap.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream. The main {@code Flowable} is consumed in an + * unbounded manner (i.e., without backpressure).</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code switchMapMaybeDelayError} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * <p>History: 2.1.11 - experimental + * @param <R> the output value type + * @param mapper the function called with the current upstream event and should + * return a {@code MaybeSource} to replace the current active inner source + * and get subscribed to. + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code mapper} is {@code null} + * @see #switchMapMaybe(Function) + * @since 2.2 + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.UNBOUNDED_IN) + @SchedulerSupport(SchedulerSupport.NONE) + public final <@NonNull R> Flowable<R> switchMapMaybeDelayError(@NonNull Function<? super T, ? extends MaybeSource<? extends R>> mapper) { + Objects.requireNonNull(mapper, "mapper is null"); + return RxJavaPlugins.onAssembly(new FlowableSwitchMapMaybe<>(this, mapper, true)); + } + + /** + * Maps the upstream items into {@link SingleSource}s and switches (subscribes) to the newer ones + * while disposing the older ones (and ignoring their signals) and emits the latest success value of the current one + * while failing immediately if this {@code Flowable} or any of the + * active inner {@code SingleSource}s fail. + * <p> + * <img width="640" height="350" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/switchMap.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream. The main {@code Flowable} is consumed in an + * unbounded manner (i.e., without backpressure).</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code switchMapSingle} does not operate by default on a particular {@link Scheduler}.</dd> + * <dt><b>Error handling:</b></dt> + * <dd>This operator terminates with an {@code onError} if this {@code Flowable} or any of + * the inner {@code SingleSource}s fail while they are active. When this happens concurrently, their + * individual {@link Throwable} errors may get combined and emitted as a single + * {@link CompositeException}. Otherwise, a late + * (i.e., inactive or switched out) {@code onError} from this {@code Flowable} or from any of + * the inner {@code SingleSource}s will be forwarded to the global error handler via + * {@link io.reactivex.rxjava3.plugins.RxJavaPlugins#onError(Throwable)} as + * {@link io.reactivex.rxjava3.exceptions.UndeliverableException UndeliverableException}</dd> + * </dl> + * <p>History: 2.1.11 - experimental + * @param <R> the output value type + * @param mapper the function called with the current upstream event and should + * return a {@code SingleSource} to replace the current active inner source + * and get subscribed to. + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code mapper} is {@code null} + * @see #switchMapSingleDelayError(Function) + * @since 2.2 + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.UNBOUNDED_IN) + @SchedulerSupport(SchedulerSupport.NONE) + public final <@NonNull R> Flowable<R> switchMapSingle(@NonNull Function<? super T, ? extends SingleSource<? extends R>> mapper) { + Objects.requireNonNull(mapper, "mapper is null"); + return RxJavaPlugins.onAssembly(new FlowableSwitchMapSingle<>(this, mapper, false)); + } + + /** + * Maps the upstream items into {@link SingleSource}s and switches (subscribes) to the newer ones + * while disposing the older ones (and ignoring their signals) and emits the latest success value of the current one, + * delaying errors from this {@code Flowable} or the inner {@code SingleSource}s until all terminate. + * <p> + * <img width="640" height="350" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/switchMap.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream. The main {@code Flowable} is consumed in an + * unbounded manner (i.e., without backpressure).</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code switchMapSingleDelayError} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * <p>History: 2.1.11 - experimental + * @param <R> the output value type + * @param mapper the function called with the current upstream event and should + * return a {@code SingleSource} to replace the current active inner source + * and get subscribed to. + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code mapper} is {@code null} + * @see #switchMapSingle(Function) + * @since 2.2 + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.UNBOUNDED_IN) + @SchedulerSupport(SchedulerSupport.NONE) + public final <@NonNull R> Flowable<R> switchMapSingleDelayError(@NonNull Function<? super T, ? extends SingleSource<? extends R>> mapper) { + Objects.requireNonNull(mapper, "mapper is null"); + return RxJavaPlugins.onAssembly(new FlowableSwitchMapSingle<>(this, mapper, true)); + } + + /** + * Returns a {@code Flowable} that emits only the first {@code count} items emitted by the current {@code Flowable}. + * If the source emits fewer than {@code count} items then all of its items are emitted. + * <p> + * <img width="640" height="305" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/take.v3.png" alt=""> + * <p> + * This method returns a {@code Flowable} that will invoke a subscribing {@link Subscriber}'s + * {@link Subscriber#onNext onNext} function a maximum of {@code count} times before invoking + * {@link Subscriber#onComplete onComplete}. + * <p> + * Limits both the number of upstream items (after which the sequence completes) + * and the total downstream request amount requested from the upstream to + * possibly prevent the creation of excess items by the upstream. + * <p> + * The operator requests at most the given {@code count} of items from upstream even + * if the downstream requests more than that. For example, given a {@code take(5)}, + * if the downstream requests 1, a request of 1 is submitted to the upstream + * and the operator remembers that only 4 items can be requested now on. A request + * of 5 at this point will request 4 from the upstream and any subsequent requests will + * be ignored. + * <p> + * Note that requests are negotiated on an operator boundary and {@code take}'s amount + * may not be preserved further upstream. For example, + * {@code source.observeOn(Schedulers.computation()).take(5)} will still request the + * default (128) elements from the given {@code source}. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The current {@code Flowable} is consumed in a bounded manner.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>This version of {@code take} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param count + * the maximum number of items and the total request amount, non-negative. + * Zero will immediately cancel the upstream on subscription and complete + * the downstream. + * @return the new {@code Flowable} instance + * @throws IllegalArgumentException if {@code count} is negative + * @see <a href="http://reactivex.io/documentation/operators/take.html">ReactiveX operators documentation: Take</a> + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Flowable<T> take(long count) { + if (count < 0) { + throw new IllegalArgumentException("count >= 0 required but it was " + count); + } + return RxJavaPlugins.onAssembly(new FlowableTake<>(this, count)); + } + + /** + * Returns a {@code Flowable} that emits those items emitted by source {@link Publisher} before a specified time runs + * out. + * <p> + * If time runs out before the {@code Flowable} completes normally, the {@code onComplete} event will be + * signaled on the default {@code computation} {@link Scheduler}. + * <p> + * <img width="640" height="305" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/take.t.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator doesn't interfere with backpressure which is determined by the current {@code Flowable}'s backpressure + * behavior.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>This version of {@code take} operates by default on the {@code computation} {@code Scheduler}.</dd> + * </dl> + * + * @param time + * the length of the time window + * @param unit + * the time unit of {@code time} + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code unit} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/take.html">ReactiveX operators documentation: Take</a> + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.PASS_THROUGH) + @SchedulerSupport(SchedulerSupport.COMPUTATION) + @NonNull + public final Flowable<T> take(long time, @NonNull TimeUnit unit) { + return takeUntil(timer(time, unit)); + } + + /** + * Returns a {@code Flowable} that emits those items emitted by source {@link Publisher} before a specified time (on a + * specified {@link Scheduler}) runs out. + * <p> + * If time runs out before the {@code Flowable} completes normally, the {@code onComplete} event will be + * signaled on the provided {@code Scheduler}. + * <p> + * <img width="640" height="305" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/take.ts.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator doesn't interfere with backpressure which is determined by the current {@code Flowable}'s backpressure + * behavior.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>You specify which {@code Scheduler} this operator will use.</dd> + * </dl> + * + * @param time + * the length of the time window + * @param unit + * the time unit of {@code time} + * @param scheduler + * the {@code Scheduler} used for time source + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code unit} or {@code scheduler} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/take.html">ReactiveX operators documentation: Take</a> + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.PASS_THROUGH) + @SchedulerSupport(SchedulerSupport.CUSTOM) + @NonNull + public final Flowable<T> take(long time, @NonNull TimeUnit unit, @NonNull Scheduler scheduler) { + return takeUntil(timer(time, unit, scheduler)); + } + + /** + * Returns a {@code Flowable} that emits at most the last {@code count} items emitted by the current {@code Flowable}. If the source emits fewer than + * {@code count} items then all of its items are emitted. + * <p> + * <img width="640" height="310" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/takeLast.n.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream if the {@code count} is non-zero; ignores + * backpressure if the {@code count} is zero as it doesn't signal any values.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>This version of {@code takeLast} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param count + * the maximum number of items to emit from the end of the sequence of items emitted by the current + * {@code Flowable} + * @return the new {@code Flowable} instance + * @throws IllegalArgumentException + * if {@code count} is negative + * @see <a href="http://reactivex.io/documentation/operators/takelast.html">ReactiveX operators documentation: TakeLast</a> + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Flowable<T> takeLast(int count) { + if (count < 0) { + throw new IllegalArgumentException("count >= 0 required but it was " + count); + } else + if (count == 0) { + return RxJavaPlugins.onAssembly(new FlowableIgnoreElements<>(this)); + } else + if (count == 1) { + return RxJavaPlugins.onAssembly(new FlowableTakeLastOne<>(this)); + } + return RxJavaPlugins.onAssembly(new FlowableTakeLast<>(this, count)); + } + + /** + * Returns a {@code Flowable} that emits at most a specified number of items from the current {@code Flowable} that were + * emitted in a specified window of time before the current {@code Flowable} completed. + * <p> + * <img width="640" height="310" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/takeLast.tn.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream and consumes the current {@code Flowable} in an + * unbounded manner (i.e., no backpressure is applied to it).</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code takeLast} does not operate on any particular scheduler but uses the current time + * from the {@code computation} {@link Scheduler}.</dd> + * </dl> + * + * @param count + * the maximum number of items to emit + * @param time + * the length of the time window + * @param unit + * the time unit of {@code time} + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code unit} is {@code null} + * @throws IllegalArgumentException if {@code count} is negative + * @see <a href="http://reactivex.io/documentation/operators/takelast.html">ReactiveX operators documentation: TakeLast</a> + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Flowable<T> takeLast(long count, long time, @NonNull TimeUnit unit) { + return takeLast(count, time, unit, Schedulers.computation(), false, bufferSize()); + } + + /** + * Returns a {@code Flowable} that emits at most a specified number of items from the current {@code Flowable} that were + * emitted in a specified window of time before the current {@code Flowable} completed, where the timing information is + * provided by a given {@link Scheduler}. + * <p> + * <img width="640" height="310" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/takeLast.tns.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream and consumes the current {@code Flowable} in an + * unbounded manner (i.e., no backpressure is applied to it).</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>You specify which {@code Scheduler} this operator will use for tracking the current time</dd> + * </dl> + * + * @param count + * the maximum number of items to emit + * @param time + * the length of the time window + * @param unit + * the time unit of {@code time} + * @param scheduler + * the {@code Scheduler} that provides the timestamps for the observed items + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code unit} or {@code scheduler} is {@code null} + * @throws IllegalArgumentException + * if {@code count} is less than zero + * @see <a href="http://reactivex.io/documentation/operators/takelast.html">ReactiveX operators documentation: TakeLast</a> + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.CUSTOM) + @NonNull + public final Flowable<T> takeLast(long count, long time, @NonNull TimeUnit unit, @NonNull Scheduler scheduler) { + return takeLast(count, time, unit, scheduler, false, bufferSize()); + } + + /** + * Returns a {@code Flowable} that emits at most a specified number of items from the current {@code Flowable} that were + * emitted in a specified window of time before the current {@code Flowable} completed, where the timing information is + * provided by a given {@link Scheduler}. + * <p> + * <img width="640" height="310" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/takeLast.tns.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream and consumes the current {@code Flowable} in an + * unbounded manner (i.e., no backpressure is applied to it).</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>You specify which {@code Scheduler} this operator will use for tracking the current time</dd> + * </dl> + * + * @param count + * the maximum number of items to emit + * @param time + * the length of the time window + * @param unit + * the time unit of {@code time} + * @param scheduler + * the {@code Scheduler} that provides the timestamps for the observed items + * @param delayError + * if {@code true}, an exception signaled by the current {@code Flowable} is delayed until the regular elements are consumed + * by the downstream; if {@code false}, an exception is immediately signaled and all regular elements dropped + * @param bufferSize + * the hint about how many elements to expect to be last + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code unit} or {@code scheduler} is {@code null} + * @throws IllegalArgumentException + * if {@code count} is negative or {@code bufferSize} is non-positive + * @see <a href="http://reactivex.io/documentation/operators/takelast.html">ReactiveX operators documentation: TakeLast</a> + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.CUSTOM) + public final Flowable<T> takeLast(long count, long time, @NonNull TimeUnit unit, @NonNull Scheduler scheduler, boolean delayError, int bufferSize) { + Objects.requireNonNull(unit, "unit is null"); + Objects.requireNonNull(scheduler, "scheduler is null"); + ObjectHelper.verifyPositive(bufferSize, "bufferSize"); + if (count < 0) { + throw new IllegalArgumentException("count >= 0 required but it was " + count); + } + return RxJavaPlugins.onAssembly(new FlowableTakeLastTimed<>(this, count, time, unit, scheduler, bufferSize, delayError)); + } + + /** + * Returns a {@code Flowable} that emits the items from the current {@code Flowable} that were emitted in a specified + * window of time before the current {@code Flowable} completed. + * <p> + * <img width="640" height="310" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/takeLast.t.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream and consumes the current {@code Flowable} in an + * unbounded manner (i.e., no backpressure is applied to it) but note that this <em>may</em> + * lead to {@link OutOfMemoryError} due to internal buffer bloat. + * Consider using {@link #takeLast(long, long, TimeUnit)} in this case.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>This version of {@code takeLast} operates by default on the {@code computation} {@link Scheduler}.</dd> + * </dl> + * + * @param time + * the length of the time window + * @param unit + * the time unit of {@code time} + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code unit} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/takelast.html">ReactiveX operators documentation: TakeLast</a> + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.COMPUTATION) + @NonNull + public final Flowable<T> takeLast(long time, @NonNull TimeUnit unit) { + return takeLast(time, unit, Schedulers.computation(), false, bufferSize()); + } + + /** + * Returns a {@code Flowable} that emits the items from the current {@code Flowable} that were emitted in a specified + * window of time before the current {@code Flowable} completed. + * <p> + * <img width="640" height="310" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/takeLast.t.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream and consumes the current {@code Flowable} in an + * unbounded manner (i.e., no backpressure is applied to it) but note that this <em>may</em> + * lead to {@link OutOfMemoryError} due to internal buffer bloat. + * Consider using {@link #takeLast(long, long, TimeUnit)} in this case.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>This version of {@code takeLast} operates by default on the {@code computation} {@link Scheduler}.</dd> + * </dl> + * + * @param time + * the length of the time window + * @param unit + * the time unit of {@code time} + * @param delayError + * if {@code true}, an exception signaled by the current {@code Flowable} is delayed until the regular elements are consumed + * by the downstream; if {@code false}, an exception is immediately signaled and all regular elements dropped + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code unit} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/takelast.html">ReactiveX operators documentation: TakeLast</a> + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.COMPUTATION) + @NonNull + public final Flowable<T> takeLast(long time, @NonNull TimeUnit unit, boolean delayError) { + return takeLast(time, unit, Schedulers.computation(), delayError, bufferSize()); + } + + /** + * Returns a {@code Flowable} that emits the items from the current {@code Flowable} that were emitted in a specified + * window of time before the current {@code Flowable} completed, where the timing information is provided by a specified + * {@link Scheduler}. + * <p> + * <img width="640" height="310" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/takeLast.ts.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream and consumes the current {@code Flowable} in an + * unbounded manner (i.e., no backpressure is applied to it) but note that this <em>may</em> + * lead to {@link OutOfMemoryError} due to internal buffer bloat. + * Consider using {@link #takeLast(long, long, TimeUnit, Scheduler)} in this case.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>You specify which {@code Scheduler} this operator will use.</dd> + * </dl> + * + * @param time + * the length of the time window + * @param unit + * the time unit of {@code time} + * @param scheduler + * the {@code Scheduler} that provides the timestamps for the observed items + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code unit} or {@code scheduler} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/takelast.html">ReactiveX operators documentation: TakeLast</a> + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.CUSTOM) + @NonNull + public final Flowable<T> takeLast(long time, @NonNull TimeUnit unit, @NonNull Scheduler scheduler) { + return takeLast(time, unit, scheduler, false, bufferSize()); + } + + /** + * Returns a {@code Flowable} that emits the items from the current {@code Flowable} that were emitted in a specified + * window of time before the current {@code Flowable} completed, where the timing information is provided by a specified + * {@link Scheduler}. + * <p> + * <img width="640" height="310" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/takeLast.ts.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream and consumes the current {@code Flowable} in an + * unbounded manner (i.e., no backpressure is applied to it) but note that this <em>may</em> + * lead to {@link OutOfMemoryError} due to internal buffer bloat. + * Consider using {@link #takeLast(long, long, TimeUnit, Scheduler)} in this case.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>You specify which {@code Scheduler} this operator will use.</dd> + * </dl> + * + * @param time + * the length of the time window + * @param unit + * the time unit of {@code time} + * @param scheduler + * the {@code Scheduler} that provides the timestamps for the observed items + * @param delayError + * if {@code true}, an exception signaled by the current {@code Flowable} is delayed until the regular elements are consumed + * by the downstream; if {@code false}, an exception is immediately signaled and all regular elements dropped + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code unit} or {@code scheduler} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/takelast.html">ReactiveX operators documentation: TakeLast</a> + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.CUSTOM) + @NonNull + public final Flowable<T> takeLast(long time, @NonNull TimeUnit unit, @NonNull Scheduler scheduler, boolean delayError) { + return takeLast(time, unit, scheduler, delayError, bufferSize()); + } + + /** + * Returns a {@code Flowable} that emits the items from the current {@code Flowable} that were emitted in a specified + * window of time before the current {@code Flowable} completed, where the timing information is provided by a specified + * {@link Scheduler}. + * <p> + * <img width="640" height="310" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/takeLast.ts.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream and consumes the current {@code Flowable} in an + * unbounded manner (i.e., no backpressure is applied to it) but note that this <em>may</em> + * lead to {@link OutOfMemoryError} due to internal buffer bloat. + * Consider using {@link #takeLast(long, long, TimeUnit, Scheduler)} in this case.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>You specify which {@code Scheduler} this operator will use.</dd> + * </dl> + * + * @param time + * the length of the time window + * @param unit + * the time unit of {@code time} + * @param scheduler + * the {@code Scheduler} that provides the timestamps for the observed items + * @param delayError + * if {@code true}, an exception signaled by the current {@code Flowable} is delayed until the regular elements are consumed + * by the downstream; if {@code false}, an exception is immediately signaled and all regular elements dropped + * @param bufferSize + * the hint about how many elements to expect to be last + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code unit} or {@code scheduler} is {@code null} + * @throws IllegalArgumentException if {@code bufferSize} is non-positive + * @see <a href="http://reactivex.io/documentation/operators/takelast.html">ReactiveX operators documentation: TakeLast</a> + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.CUSTOM) + @NonNull + public final Flowable<T> takeLast(long time, @NonNull TimeUnit unit, @NonNull Scheduler scheduler, boolean delayError, int bufferSize) { + return takeLast(Long.MAX_VALUE, time, unit, scheduler, delayError, bufferSize); + } + + /** + * Returns a {@code Flowable} that emits items emitted by the current {@code Flowable}, checks the specified predicate + * for each item, and then completes when the condition is satisfied. + * <p> + * <img width="640" height="305" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/takeUntil.p.v3.png" alt=""> + * <p> + * The difference between this operator and {@link #takeWhile(Predicate)} is that here, the condition is + * evaluated <em>after</em> the item is emitted. + * + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator is a pass-through for backpressure; the backpressure behavior is determined by the upstream + * source and the downstream consumer.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code takeUntil} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param stopPredicate + * a function that evaluates an item emitted by the current {@code Flowable} and returns a {@link Boolean} + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code stopPredicate} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/takeuntil.html">ReactiveX operators documentation: TakeUntil</a> + * @see Flowable#takeWhile(Predicate) + * @since 1.1.0 + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.PASS_THROUGH) + @SchedulerSupport(SchedulerSupport.NONE) + public final Flowable<T> takeUntil(@NonNull Predicate<? super T> stopPredicate) { + Objects.requireNonNull(stopPredicate, "stopPredicate is null"); + return RxJavaPlugins.onAssembly(new FlowableTakeUntilPredicate<>(this, stopPredicate)); + } + + /** + * Returns a {@code Flowable} that emits the items emitted by the current {@code Flowable} until a second {@link Publisher} + * emits an item or completes. + * <p> + * <img width="640" height="188" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Flowable.takeUntil.p.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator doesn't interfere with backpressure which is determined by the current {@code Flowable}'s backpressure + * behavior.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code takeUntil} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param other + * the {@code Publisher} whose first emitted item or completion will cause {@code takeUntil} to stop emitting items + * from the current {@code Flowable} + * @param <U> + * the type of items emitted by {@code other} + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code other} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/takeuntil.html">ReactiveX operators documentation: TakeUntil</a> + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.PASS_THROUGH) + @SchedulerSupport(SchedulerSupport.NONE) + public final <@NonNull U> Flowable<T> takeUntil(@NonNull Publisher<U> other) { + Objects.requireNonNull(other, "other is null"); + return RxJavaPlugins.onAssembly(new FlowableTakeUntil<>(this, other)); + } + + /** + * Returns a {@code Flowable} that emits items emitted by the current {@code Flowable} so long as each item satisfied a + * specified condition, and then completes as soon as this condition is not satisfied. + * <p> + * <img width="640" height="305" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/takeWhile.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator doesn't interfere with backpressure which is determined by the current {@code Flowable}'s backpressure + * behavior.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code takeWhile} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param predicate + * a function that evaluates an item emitted by the current {@code Flowable} and returns a {@link Boolean} + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code predicate} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/takewhile.html">ReactiveX operators documentation: TakeWhile</a> + * @see Flowable#takeUntil(Predicate) + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.PASS_THROUGH) + @SchedulerSupport(SchedulerSupport.NONE) + public final Flowable<T> takeWhile(@NonNull Predicate<? super T> predicate) { + Objects.requireNonNull(predicate, "predicate is null"); + return RxJavaPlugins.onAssembly(new FlowableTakeWhile<>(this, predicate)); + } + + /** + * Returns a {@code Flowable} that emits only the first item emitted by the current {@code Flowable} during sequential + * time windows of a specified duration. + * <p> + * This differs from {@link #throttleLast} in that this only tracks the passage of time whereas + * {@link #throttleLast} ticks at scheduled intervals. + * <p> + * <img width="640" height="305" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/throttleFirst.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>This operator does not support backpressure as it uses time to control data flow.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code throttleFirst} operates by default on the {@code computation} {@link Scheduler}.</dd> + * </dl> + * + * @param windowDuration + * time to wait before emitting another item after emitting the last item + * @param unit + * the unit of time of {@code windowDuration} + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code unit} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/sample.html">ReactiveX operators documentation: Sample</a> + * @see <a href="https://github.com/ReactiveX/RxJava/wiki/Backpressure">RxJava wiki: Backpressure</a> + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.ERROR) + @SchedulerSupport(SchedulerSupport.COMPUTATION) + @NonNull + public final Flowable<T> throttleFirst(long windowDuration, @NonNull TimeUnit unit) { + return throttleFirst(windowDuration, unit, Schedulers.computation()); + } + + /** + * Returns a {@code Flowable} that emits only the first item emitted by the current {@code Flowable} during sequential + * time windows of a specified duration, where the windows are managed by a specified {@link Scheduler}. + * <p> + * This differs from {@link #throttleLast} in that this only tracks the passage of time whereas + * {@link #throttleLast} ticks at scheduled intervals. + * <p> + * <img width="640" height="305" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/throttleFirst.s.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>This operator does not support backpressure as it uses time to control data flow.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>You specify which {@code Scheduler} this operator will use.</dd> + * </dl> + * + * @param skipDuration + * time to wait before emitting another item after emitting the last item + * @param unit + * the unit of time of {@code skipDuration} + * @param scheduler + * the {@code Scheduler} to use internally to manage the timers that handle timeout for each + * event + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code unit} or {@code scheduler} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/sample.html">ReactiveX operators documentation: Sample</a> + * @see <a href="https://github.com/ReactiveX/RxJava/wiki/Backpressure">RxJava wiki: Backpressure</a> + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.ERROR) + @SchedulerSupport(SchedulerSupport.CUSTOM) + public final Flowable<T> throttleFirst(long skipDuration, @NonNull TimeUnit unit, @NonNull Scheduler scheduler) { + Objects.requireNonNull(unit, "unit is null"); + Objects.requireNonNull(scheduler, "scheduler is null"); + return RxJavaPlugins.onAssembly(new FlowableThrottleFirstTimed<>(this, skipDuration, unit, scheduler, null)); + } + + /** + * Returns a {@code Flowable} that emits only the first item emitted by the current {@code Flowable} during sequential + * time windows of a specified duration, where the windows are managed by a specified {@link Scheduler}. + * <p> + * This differs from {@link #throttleLast} in that this only tracks the passage of time whereas + * {@link #throttleLast} ticks at scheduled intervals. + * <p> + * <img width="640" height="305" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/throttleFirst.s.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>This operator does not support backpressure as it uses time to control data flow.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>You specify which {@code Scheduler} this operator will use.</dd> + * </dl> + * + * @param skipDuration + * time to wait before emitting another item after emitting the last item + * @param unit + * the unit of time of {@code skipDuration} + * @param scheduler + * the {@code Scheduler} to use internally to manage the timers that handle timeout for each + * event + * @param onDropped + * called when an item doesn't get delivered to the downstream + * + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code unit} or {@code scheduler} or {@code onDropped} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/sample.html">ReactiveX operators documentation: Sample</a> + * @see <a href="https://github.com/ReactiveX/RxJava/wiki/Backpressure">RxJava wiki: Backpressure</a> + * @since 3.1.6 - Experimental + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.ERROR) + @SchedulerSupport(SchedulerSupport.CUSTOM) + @Experimental + public final Flowable<T> throttleFirst(long skipDuration, @NonNull TimeUnit unit, @NonNull Scheduler scheduler, @NonNull Consumer<? super T> onDropped) { + Objects.requireNonNull(unit, "unit is null"); + Objects.requireNonNull(scheduler, "scheduler is null"); + Objects.requireNonNull(onDropped, "onDropped is null"); + return RxJavaPlugins.onAssembly(new FlowableThrottleFirstTimed<>(this, skipDuration, unit, scheduler, onDropped)); + } + + /** + * Returns a {@code Flowable} that emits only the last item emitted by the current {@code Flowable} during sequential + * time windows of a specified duration. + * <p> + * This differs from {@link #throttleFirst} in that this ticks along at a scheduled interval whereas + * {@link #throttleFirst} does not tick, it just tracks the passage of time. + * <p> + * <img width="640" height="305" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/throttleLast.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>This operator does not support backpressure as it uses time to control data flow.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code throttleLast} operates by default on the {@code computation} {@link Scheduler}.</dd> + * </dl> + * + * @param intervalDuration + * duration of windows within which the last item emitted by the current {@code Flowable} will be + * emitted + * @param unit + * the unit of time of {@code intervalDuration} + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code unit} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/sample.html">ReactiveX operators documentation: Sample</a> + * @see <a href="https://github.com/ReactiveX/RxJava/wiki/Backpressure">RxJava wiki: Backpressure</a> + * @see #sample(long, TimeUnit) + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.ERROR) + @SchedulerSupport(SchedulerSupport.COMPUTATION) + @NonNull + public final Flowable<T> throttleLast(long intervalDuration, @NonNull TimeUnit unit) { + return sample(intervalDuration, unit); + } + + /** + * Returns a {@code Flowable} that emits only the last item emitted by the current {@code Flowable} during sequential + * time windows of a specified duration, where the duration is governed by a specified {@link Scheduler}. + * <p> + * This differs from {@link #throttleFirst(long, TimeUnit, Scheduler)} in that this ticks along at a scheduled interval whereas + * {@code throttleFirst} does not tick, it just tracks the passage of time. + * <p> + * <img width="640" height="305" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/throttleLast.s.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>This operator does not support backpressure as it uses time to control data flow.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>You specify which {@code Scheduler} this operator will use.</dd> + * </dl> + * + * @param intervalDuration + * duration of windows within which the last item emitted by the current {@code Flowable} will be + * emitted + * @param unit + * the unit of time of {@code intervalDuration} + * @param scheduler + * the {@code Scheduler} to use internally to manage the timers that handle timeout for each + * event + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code unit} or {@code scheduler} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/sample.html">ReactiveX operators documentation: Sample</a> + * @see <a href="https://github.com/ReactiveX/RxJava/wiki/Backpressure">RxJava wiki: Backpressure</a> + * @see #sample(long, TimeUnit, Scheduler) + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.ERROR) + @SchedulerSupport(SchedulerSupport.CUSTOM) + @NonNull + public final Flowable<T> throttleLast(long intervalDuration, @NonNull TimeUnit unit, @NonNull Scheduler scheduler) { + return sample(intervalDuration, unit, scheduler); + } + + /** + * Returns a {@code Flowable} that emits only the last item emitted by the current {@code Flowable} during sequential + * time windows of a specified duration, where the duration is governed by a specified {@link Scheduler}. + * <p> + * This differs from {@link #throttleFirst(long, TimeUnit, Scheduler)} in that this ticks along at a scheduled interval whereas + * {@code throttleFirst} does not tick, it just tracks the passage of time. + * <p> + * <img width="640" height="305" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/throttleLast.s.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>This operator does not support backpressure as it uses time to control data flow.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>You specify which {@code Scheduler} this operator will use.</dd> + * </dl> + * + * @param intervalDuration + * duration of windows within which the last item emitted by the current {@code Flowable} will be + * emitted + * @param unit + * the unit of time of {@code intervalDuration} + * @param scheduler + * the {@code Scheduler} to use internally to manage the timers that handle timeout for each + * event + * @param onDropped + * called with the current entry when it has been replaced by a new one + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code unit} or {@code scheduler} is {@code null} or {@code onDropped} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/sample.html">ReactiveX operators documentation: Sample</a> + * @see <a href="https://github.com/ReactiveX/RxJava/wiki/Backpressure">RxJava wiki: Backpressure</a> + * @see #sample(long, TimeUnit, Scheduler) + * @since 3.1.6 - Experimental + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.ERROR) + @SchedulerSupport(SchedulerSupport.CUSTOM) + @NonNull + @Experimental + public final Flowable<T> throttleLast(long intervalDuration, @NonNull TimeUnit unit, @NonNull Scheduler scheduler, @NonNull Consumer<? super T> onDropped) { + return sample(intervalDuration, unit, scheduler, false, onDropped); + } + + /** + * Throttles items from the upstream {@code Flowable} by first emitting the next + * item from upstream, then periodically emitting the latest item (if any) when + * the specified timeout elapses between them. + * <p> + * <img width="640" height="326" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/throttleLatest.png" alt=""> + * <p> + * Unlike the option with {@link #throttleLatest(long, TimeUnit, boolean)}, the very last item being held back + * (if any) is not emitted when the upstream completes. + * <p> + * If no items were emitted from the upstream during this timeout phase, the next + * upstream item is emitted immediately and the timeout window starts from then. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>This operator does not support backpressure as it uses time to control data flow. + * If the downstream is not ready to receive items, a + * {@link io.reactivex.rxjava3.exceptions.MissingBackpressureException MissingBackpressureException} + * will be signaled.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code throttleLatest} operates by default on the {@code computation} {@link Scheduler}.</dd> + * </dl> + * <p>History: 2.1.14 - experimental + * @param timeout the time to wait after an item emission towards the downstream + * before trying to emit the latest item from upstream again + * @param unit the time unit + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code unit} is {@code null} + * @since 2.2 + * @see #throttleLatest(long, TimeUnit, boolean) + * @see #throttleLatest(long, TimeUnit, Scheduler) + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.ERROR) + @SchedulerSupport(SchedulerSupport.COMPUTATION) + @NonNull + public final Flowable<T> throttleLatest(long timeout, @NonNull TimeUnit unit) { + return throttleLatest(timeout, unit, Schedulers.computation(), false); + } + + /** + * Throttles items from the upstream {@code Flowable} by first emitting the next + * item from upstream, then periodically emitting the latest item (if any) when + * the specified timeout elapses between them. + * <p> + * <img width="640" height="326" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/throttleLatest.e.png" alt=""> + * <p> + * If no items were emitted from the upstream during this timeout phase, the next + * upstream item is emitted immediately and the timeout window starts from then. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>This operator does not support backpressure as it uses time to control data flow. + * If the downstream is not ready to receive items, a + * {@link io.reactivex.rxjava3.exceptions.MissingBackpressureException MissingBackpressureException} + * will be signaled.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code throttleLatest} operates by default on the {@code computation} {@link Scheduler}.</dd> + * </dl> + * <p>History: 2.1.14 - experimental + * @param timeout the time to wait after an item emission towards the downstream + * before trying to emit the latest item from upstream again + * @param unit the time unit + * @param emitLast If {@code true}, the very last item from the upstream will be emitted + * immediately when the upstream completes, regardless if there is + * a timeout window active or not. If {@code false}, the very last + * upstream item is ignored and the flow terminates. + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code unit} is {@code null} + * @see #throttleLatest(long, TimeUnit, Scheduler, boolean) + * @since 2.2 + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.ERROR) + @SchedulerSupport(SchedulerSupport.COMPUTATION) + @NonNull + public final Flowable<T> throttleLatest(long timeout, @NonNull TimeUnit unit, boolean emitLast) { + return throttleLatest(timeout, unit, Schedulers.computation(), emitLast); + } + + /** + * Throttles items from the upstream {@code Flowable} by first emitting the next + * item from upstream, then periodically emitting the latest item (if any) when + * the specified timeout elapses between them. + * <p> + * <img width="640" height="326" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/throttleLatest.s.png" alt=""> + * <p> + * Unlike the option with {@link #throttleLatest(long, TimeUnit, Scheduler, boolean)}, the very last item being held back + * (if any) is not emitted when the upstream completes. + * <p> + * If no items were emitted from the upstream during this timeout phase, the next + * upstream item is emitted immediately and the timeout window starts from then. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>This operator does not support backpressure as it uses time to control data flow. + * If the downstream is not ready to receive items, a + * {@link io.reactivex.rxjava3.exceptions.MissingBackpressureException MissingBackpressureException} + * will be signaled.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>You specify which {@link Scheduler} this operator will use.</dd> + * </dl> + * <p>History: 2.1.14 - experimental + * @param timeout the time to wait after an item emission towards the downstream + * before trying to emit the latest item from upstream again + * @param unit the time unit + * @param scheduler the {@code Scheduler} where the timed wait and latest item + * emission will be performed + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code unit} or {@code scheduler} is {@code null} + * @see #throttleLatest(long, TimeUnit, Scheduler, boolean) + * @since 2.2 + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.ERROR) + @SchedulerSupport(SchedulerSupport.CUSTOM) + @NonNull + public final Flowable<T> throttleLatest(long timeout, @NonNull TimeUnit unit, @NonNull Scheduler scheduler) { + return throttleLatest(timeout, unit, scheduler, false); + } + + /** + * Throttles items from the upstream {@code Flowable} by first emitting the next + * item from upstream, then periodically emitting the latest item (if any) when + * the specified timeout elapses between them. + * <p> + * <img width="640" height="326" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/throttleLatest.se.png" alt=""> + * <p> + * If no items were emitted from the upstream during this timeout phase, the next + * upstream item is emitted immediately and the timeout window starts from then. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>This operator does not support backpressure as it uses time to control data flow. + * If the downstream is not ready to receive items, a + * {@link io.reactivex.rxjava3.exceptions.MissingBackpressureException MissingBackpressureException} + * will be signaled.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>You specify which {@link Scheduler} this operator will use.</dd> + * </dl> + * <p>History: 2.1.14 - experimental + * @param timeout the time to wait after an item emission towards the downstream + * before trying to emit the latest item from upstream again + * @param unit the time unit + * @param scheduler the {@code Scheduler} where the timed wait and latest item + * emission will be performed + * @param emitLast If {@code true}, the very last item from the upstream will be emitted + * immediately when the upstream completes, regardless if there is + * a timeout window active or not. If {@code false}, the very last + * upstream item is ignored and the flow terminates. + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code unit} or {@code scheduler} is {@code null} + * @since 2.2 + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.ERROR) + @SchedulerSupport(SchedulerSupport.CUSTOM) + public final Flowable<T> throttleLatest(long timeout, @NonNull TimeUnit unit, @NonNull Scheduler scheduler, boolean emitLast) { + Objects.requireNonNull(unit, "unit is null"); + Objects.requireNonNull(scheduler, "scheduler is null"); + return RxJavaPlugins.onAssembly(new FlowableThrottleLatest<>(this, timeout, unit, scheduler, emitLast, null)); + } + + /** + * Throttles items from the upstream {@code Flowable} by first emitting the next + * item from upstream, then periodically emitting the latest item (if any) when + * the specified timeout elapses between them, invoking the consumer for any dropped item. + * <p> + * <img width="640" height="326" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/throttleLatest.se.png" alt=""> + * <p> + * If no items were emitted from the upstream during this timeout phase, the next + * upstream item is emitted immediately and the timeout window starts from then. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>This operator does not support backpressure as it uses time to control data flow. + * If the downstream is not ready to receive items, a + * {@link io.reactivex.rxjava3.exceptions.MissingBackpressureException MissingBackpressureException} + * will be signaled.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>You specify which {@link Scheduler} this operator will use.</dd> + * <dt><b>Error handling:</b></dt> + * <dd> + * If the upstream signals an {@code onError} or {@code onDropped} callback crashes, + * the error is delivered immediately to the downstream. If both happen, a {@link CompositeException} + * is created, containing both the upstream and the callback error. + * If the {@code onDropped} callback crashes during cancellation, the exception is forwarded + * to the global error handler via {@link RxJavaPlugins#onError(Throwable)}. + * </dd> + * </dl> + * @param timeout the time to wait after an item emission towards the downstream + * before trying to emit the latest item from upstream again + * @param unit the time unit + * @param scheduler the {@code Scheduler} where the timed wait and latest item + * emission will be performed + * @param emitLast If {@code true}, the very last item from the upstream will be emitted + * immediately when the upstream completes, regardless if there is + * a timeout window active or not. If {@code false}, the very last + * upstream item is ignored and the flow terminates. + * @param onDropped called when an item is replaced by a newer item that doesn't get delivered + * to the downstream, including the very last item if {@code emitLast} is {@code false} + * and the current undelivered item when the sequence gets canceled. + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code unit}, {@code scheduler} or {@code onDropped} is {@code null} + * @since 3.1.6 - Experimental + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.ERROR) + @SchedulerSupport(SchedulerSupport.CUSTOM) + @Experimental + public final Flowable<T> throttleLatest(long timeout, @NonNull TimeUnit unit, @NonNull Scheduler scheduler, boolean emitLast, @NonNull Consumer<? super T> onDropped) { + Objects.requireNonNull(unit, "unit is null"); + Objects.requireNonNull(scheduler, "scheduler is null"); + Objects.requireNonNull(onDropped, "onDropped is null"); + return RxJavaPlugins.onAssembly(new FlowableThrottleLatest<>(this, timeout, unit, scheduler, emitLast, onDropped)); + } + + /** + * Returns a {@code Flowable} that mirrors the current {@code Flowable}, except that it drops items emitted by the + * current {@code Flowable} that are followed by newer items before a timeout value expires. The timer resets on + * each emission (alias to {@link #debounce(long, TimeUnit)}). + * <p> + * <em>Note:</em> If items keep being emitted by the current {@code Flowable} faster than the timeout then no items + * will be emitted by the resulting {@code Flowable}. + * <p> + * <img width="640" height="305" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/throttleWithTimeout.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>This operator does not support backpressure as it uses time to control data flow.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code throttleWithTimeout} operates by default on the {@code computation} {@link Scheduler}.</dd> + * </dl> + * + * @param timeout + * the length of the window of time that must pass after the emission of an item from the current + * {@code Flowable} in which it emits no items in order for the item to be emitted by the + * resulting {@code Flowable} + * @param unit + * the unit of time for the specified {@code timeout} + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code unit} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/debounce.html">ReactiveX operators documentation: Debounce</a> + * @see <a href="https://github.com/ReactiveX/RxJava/wiki/Backpressure">RxJava wiki: Backpressure</a> + * @see #debounce(long, TimeUnit) + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.ERROR) + @SchedulerSupport(SchedulerSupport.COMPUTATION) + @NonNull + public final Flowable<T> throttleWithTimeout(long timeout, @NonNull TimeUnit unit) { + return debounce(timeout, unit); + } + + /** + * Returns a {@code Flowable} that mirrors the current {@code Flowable}, except that it drops items emitted by the + * current {@code Flowable} that are followed by newer items before a timeout value expires on a specified + * {@link Scheduler}. The timer resets on each emission (alias to {@link #debounce(long, TimeUnit, Scheduler)}). + * <p> + * <em>Note:</em> If items keep being emitted by the current {@code Flowable} faster than the timeout then no items + * will be emitted by the resulting {@code Flowable}. + * <p> + * <img width="640" height="305" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/throttleWithTimeout.s.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>This operator does not support backpressure as it uses time to control data flow.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>You specify which {@code Scheduler} this operator will use.</dd> + * </dl> + * + * @param timeout + * the length of the window of time that must pass after the emission of an item from the current + * {@code Flowable} in which it emits no items in order for the item to be emitted by the + * resulting {@code Flowable} + * @param unit + * the unit of time for the specified {@code timeout} + * @param scheduler + * the {@code Scheduler} to use internally to manage the timers that handle the timeout for each + * item + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code unit} or {@code scheduler} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/debounce.html">ReactiveX operators documentation: Debounce</a> + * @see <a href="https://github.com/ReactiveX/RxJava/wiki/Backpressure">RxJava wiki: Backpressure</a> + * @see #debounce(long, TimeUnit, Scheduler) + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.ERROR) + @SchedulerSupport(SchedulerSupport.CUSTOM) + @NonNull + public final Flowable<T> throttleWithTimeout(long timeout, @NonNull TimeUnit unit, @NonNull Scheduler scheduler) { + return debounce(timeout, unit, scheduler); + } + + /** + * Returns a {@code Flowable} that mirrors the current {@code Flowable}, except that it drops items emitted by the + * current {@code Flowable} that are followed by newer items before a timeout value expires on a specified + * {@link Scheduler}. The timer resets on each emission (alias to {@link #debounce(long, TimeUnit, Scheduler, Consumer)}). + * <p> + * <em>Note:</em> If items keep being emitted by the current {@code Flowable} faster than the timeout then no items + * will be emitted by the resulting {@code Flowable}. + * <p> + * <img width="640" height="305" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/throttleWithTimeout.s.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>This operator does not support backpressure as it uses time to control data flow.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>You specify which {@code Scheduler} this operator will use.</dd> + * </dl> + * + * @param timeout + * the length of the window of time that must pass after the emission of an item from the current + * {@code Flowable} in which it emits no items in order for the item to be emitted by the + * resulting {@code Flowable} + * @param unit + * the unit of time for the specified {@code timeout} + * @param scheduler + * the {@code Scheduler} to use internally to manage the timers that handle the timeout for each + * item + * @param onDropped + * called with the current entry when it has been replaced by a new one + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code unit} or {@code scheduler} is {@code null} or {@code onDropped} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/debounce.html">ReactiveX operators documentation: Debounce</a> + * @see <a href="https://github.com/ReactiveX/RxJava/wiki/Backpressure">RxJava wiki: Backpressure</a> + * @see #debounce(long, TimeUnit, Scheduler, Consumer) + * @since 3.1.6 - Experimental + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.ERROR) + @SchedulerSupport(SchedulerSupport.CUSTOM) + @NonNull + @Experimental + public final Flowable<T> throttleWithTimeout(long timeout, @NonNull TimeUnit unit, @NonNull Scheduler scheduler, @NonNull Consumer<? super T> onDropped) { + return debounce(timeout, unit, scheduler, onDropped); + } + + /** + * Returns a {@code Flowable} that emits records of the time interval between consecutive items emitted by the + * current {@code Flowable}. + * <p> + * <img width="640" height="310" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/timeInterval.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator doesn't interfere with backpressure which is determined by the current {@code Flowable}'s backpressure + * behavior.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code timeInterval} does not operate on any particular scheduler but uses the current time + * from the {@code computation} {@link Scheduler}.</dd> + * </dl> + * + * @return the new {@code Flowable} instance + * @see <a href="http://reactivex.io/documentation/operators/timeinterval.html">ReactiveX operators documentation: TimeInterval</a> + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.PASS_THROUGH) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Flowable<Timed<T>> timeInterval() { + return timeInterval(TimeUnit.MILLISECONDS, Schedulers.computation()); + } + + /** + * Returns a {@code Flowable} that emits records of the time interval between consecutive items emitted by the + * current {@code Flowable}, where this interval is computed on a specified {@link Scheduler}. + * <p> + * <img width="640" height="315" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/timeInterval.s.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator doesn't interfere with backpressure which is determined by the current {@code Flowable}'s backpressure + * behavior.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code timeInterval} does not operate on any particular scheduler but uses the current time + * from the specified {@code Scheduler}.</dd> + * </dl> + * + * @param scheduler + * the {@code Scheduler} used to compute time intervals + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code scheduler} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/timeinterval.html">ReactiveX operators documentation: TimeInterval</a> + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.PASS_THROUGH) + @SchedulerSupport(SchedulerSupport.NONE) // Supplied scheduler is only used for creating timestamps. + @NonNull + public final Flowable<Timed<T>> timeInterval(@NonNull Scheduler scheduler) { + return timeInterval(TimeUnit.MILLISECONDS, scheduler); + } + + /** + * Returns a {@code Flowable} that emits records of the time interval between consecutive items emitted by the + * current {@code Flowable}. + * <p> + * <img width="640" height="310" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/timeInterval.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator doesn't interfere with backpressure which is determined by the current {@code Flowable}'s backpressure + * behavior.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code timeInterval} does not operate on any particular scheduler but uses the current time + * from the {@code computation} {@link Scheduler}.</dd> + * </dl> + * + * @param unit the time unit for the current time + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code unit} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/timeinterval.html">ReactiveX operators documentation: TimeInterval</a> + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.PASS_THROUGH) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Flowable<Timed<T>> timeInterval(@NonNull TimeUnit unit) { + return timeInterval(unit, Schedulers.computation()); + } + + /** + * Returns a {@code Flowable} that emits records of the time interval between consecutive items emitted by the + * current {@code Flowable}, where this interval is computed on a specified {@link Scheduler}. + * <p> + * <img width="640" height="315" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/timeInterval.s.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator doesn't interfere with backpressure which is determined by the current {@code Flowable}'s backpressure + * behavior.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code timeInterval} does not operate on any particular scheduler but uses the current time + * from the specified {@code Scheduler}.</dd> + * </dl> + * + * @param unit the time unit for the current time + * @param scheduler + * the {@code Scheduler} used to compute time intervals + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code unit} or {@code scheduler} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/timeinterval.html">ReactiveX operators documentation: TimeInterval</a> + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.PASS_THROUGH) + @SchedulerSupport(SchedulerSupport.NONE) // Supplied scheduler is only used for creating timestamps. + @NonNull + public final Flowable<Timed<T>> timeInterval(@NonNull TimeUnit unit, @NonNull Scheduler scheduler) { + Objects.requireNonNull(unit, "unit is null"); + Objects.requireNonNull(scheduler, "scheduler is null"); + return RxJavaPlugins.onAssembly(new FlowableTimeInterval<>(this, unit, scheduler)); + } + + /** + * Returns a {@code Flowable} that mirrors the current {@code Flowable}, but notifies {@link Subscriber}s of a + * {@link TimeoutException} if an item emitted by the current {@code Flowable} doesn't arrive within a window of + * time after the emission of the previous item, where that period of time is measured by a {@link Publisher} that + * is a function of the previous item. + * <p> + * <img width="640" height="400" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/timeout3.v3.png" alt=""> + * <p> + * Note: The arrival of the first source item is never timed out. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream. The {@code Publisher} + * sources are expected to honor backpressure as well. + * If any of the current {@code Flowable}s violate this, it <em>may</em> throw an + * {@link IllegalStateException} when the current {@code Flowable} completes.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>This version of {@code timeout} operates by default on the {@code immediate} {@link Scheduler}.</dd> + * </dl> + * + * @param <V> + * the timeout value type (ignored) + * @param itemTimeoutIndicator + * a function that returns a {@code Publisher} for each item emitted by the current + * {@code Flowable} and that determines the timeout window for the subsequent item + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code itemTimeoutIndicator} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/timeout.html">ReactiveX operators documentation: Timeout</a> + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.PASS_THROUGH) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull V> Flowable<T> timeout(@NonNull Function<? super T, @NonNull ? extends Publisher<V>> itemTimeoutIndicator) { + return timeout0(null, itemTimeoutIndicator, null); + } + + /** + * Returns a {@code Flowable} that mirrors the current {@code Flowable}, but that switches to a fallback {@link Publisher} if + * an item emitted by the current {@code Flowable} doesn't arrive within a window of time after the emission of the + * previous item, where that period of time is measured by a {@code Publisher} that is a function of the previous + * item. + * <p> + * <img width="640" height="400" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/timeout4.v3.png" alt=""> + * <p> + * Note: The arrival of the first source item is never timed out. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream. The {@code Publisher} + * sources are expected to honor backpressure as well. + * If any of the current {@code Flowable}s violate this, it <em>may</em> throw an + * {@link IllegalStateException} when the current {@code Flowable} completes.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>This version of {@code timeout} operates by default on the {@code immediate} {@link Scheduler}.</dd> + * </dl> + * + * @param <V> + * the timeout value type (ignored) + * @param itemTimeoutIndicator + * a function that returns a {@code Publisher}, for each item emitted by the current {@code Flowable}, that + * determines the timeout window for the subsequent item + * @param fallback + * the fallback {@code Publisher} to switch to if the current {@code Flowable} times out + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code itemTimeoutIndicator} or {@code fallback} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/timeout.html">ReactiveX operators documentation: Timeout</a> + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + public final <@NonNull V> Flowable<T> timeout(@NonNull Function<? super T, @NonNull ? extends Publisher<V>> itemTimeoutIndicator, @NonNull Publisher<? extends T> fallback) { + Objects.requireNonNull(fallback, "fallback is null"); + return timeout0(null, itemTimeoutIndicator, fallback); + } + + /** + * Returns a {@code Flowable} that mirrors the current {@code Flowable} but applies a timeout policy for each emitted + * item. If the next item isn't emitted within the specified timeout duration starting from its predecessor, + * the resulting {@code Flowable} terminates and notifies {@link Subscriber}s of a {@link TimeoutException}. + * <p> + * <img width="640" height="305" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/timeout.1.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator doesn't interfere with backpressure which is determined by the current {@code Flowable}'s backpressure + * behavior.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>This version of {@code timeout} operates by default on the {@code computation} {@link Scheduler}.</dd> + * </dl> + * + * @param timeout + * maximum duration between emitted items before a timeout occurs + * @param unit + * the unit of time that applies to the {@code timeout} argument. + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code unit} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/timeout.html">ReactiveX operators documentation: Timeout</a> + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.PASS_THROUGH) + @SchedulerSupport(SchedulerSupport.COMPUTATION) + @NonNull + public final Flowable<T> timeout(long timeout, @NonNull TimeUnit unit) { + return timeout0(timeout, unit, null, Schedulers.computation()); + } + + /** + * Returns a {@code Flowable} that mirrors the current {@code Flowable} but applies a timeout policy for each emitted + * item. If the next item isn't emitted within the specified timeout duration starting from its predecessor, + * the current {@code Flowable} is disposed and the resulting {@code Flowable} begins instead to mirror a fallback {@link Publisher}. + * <p> + * <img width="640" height="305" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/timeout.2.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream. The {@code Publisher} + * sources are expected to honor backpressure as well. + * If any of the current {@code Flowable}s violate this, it <em>may</em> throw an + * {@link IllegalStateException} when the current {@code Flowable} completes.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>This version of {@code timeout} operates by default on the {@code computation} {@link Scheduler}.</dd> + * </dl> + * + * @param timeout + * maximum duration between items before a timeout occurs + * @param unit + * the unit of time that applies to the {@code timeout} argument + * @param fallback + * the fallback {@code Publisher} to use in case of a timeout + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code unit} or {@code fallback} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/timeout.html">ReactiveX operators documentation: Timeout</a> + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.COMPUTATION) + public final Flowable<T> timeout(long timeout, @NonNull TimeUnit unit, @NonNull Publisher<? extends T> fallback) { + Objects.requireNonNull(fallback, "fallback is null"); + return timeout0(timeout, unit, fallback, Schedulers.computation()); + } + + /** + * Returns a {@code Flowable} that mirrors the current {@code Flowable} but applies a timeout policy for each emitted + * item using a specified {@link Scheduler}. If the next item isn't emitted within the specified timeout duration + * starting from its predecessor, the current {@code Flowable} is disposed and the resulting {@code Flowable} begins + * instead to mirror a fallback {@link Publisher}. + * <p> + * <img width="640" height="305" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/timeout.2s.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream. The {@code Publisher} + * sources are expected to honor backpressure as well. + * If any of the current {@code Flowable}s violate this, it <em>may</em> throw an + * {@link IllegalStateException} when the current {@code Flowable} completes.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>You specify which {@code Scheduler} this operator will use.</dd> + * </dl> + * + * @param timeout + * maximum duration between items before a timeout occurs + * @param unit + * the unit of time that applies to the {@code timeout} argument + * @param scheduler + * the {@code Scheduler} to run the timeout timers on + * @param fallback + * the {@code Publisher} to use as the fallback in case of a timeout + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code unit}, {@code scheduler} or {@code fallback} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/timeout.html">ReactiveX operators documentation: Timeout</a> + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.CUSTOM) + public final Flowable<T> timeout(long timeout, @NonNull TimeUnit unit, @NonNull Scheduler scheduler, @NonNull Publisher<? extends T> fallback) { + Objects.requireNonNull(fallback, "fallback is null"); + return timeout0(timeout, unit, fallback, scheduler); + } + + /** + * Returns a {@code Flowable} that mirrors the current {@code Flowable} but applies a timeout policy for each emitted + * item, where this policy is governed by a specified {@link Scheduler}. If the next item isn't emitted within the + * specified timeout duration starting from its predecessor, the resulting {@code Flowable} terminates and + * notifies {@link Subscriber}s of a {@link TimeoutException}. + * <p> + * <img width="640" height="305" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/timeout.1s.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator doesn't interfere with backpressure which is determined by the current {@code Flowable}'s backpressure + * behavior.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>You specify which {@code Scheduler} this operator will use.</dd> + * </dl> + * + * @param timeout + * maximum duration between items before a timeout occurs + * @param unit + * the unit of time that applies to the {@code timeout} argument + * @param scheduler + * the {@code Scheduler} to run the timeout timers on + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code unit} or {@code scheduler} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/timeout.html">ReactiveX operators documentation: Timeout</a> + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.PASS_THROUGH) + @SchedulerSupport(SchedulerSupport.CUSTOM) + @NonNull + public final Flowable<T> timeout(long timeout, @NonNull TimeUnit unit, @NonNull Scheduler scheduler) { + return timeout0(timeout, unit, null, scheduler); + } + + /** + * Returns a {@code Flowable} that mirrors the current {@code Flowable}, but notifies {@link Subscriber}s of a + * {@link TimeoutException} if either the first item emitted by the current {@code Flowable} or any subsequent item + * doesn't arrive within time windows defined by other {@link Publisher}s. + * <p> + * <img width="640" height="400" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/timeout5.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream. Both this and the returned {@code Publisher}s + * are expected to honor backpressure as well. If any of then violates this rule, it <em>may</em> throw an + * {@link IllegalStateException} when the {@code Publisher} completes.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code timeout} does not operate by default on any {@link Scheduler}.</dd> + * </dl> + * + * @param <U> + * the first timeout value type (ignored) + * @param <V> + * the subsequent timeout value type (ignored) + * @param firstTimeoutIndicator + * a function that returns a {@code Publisher} that determines the timeout window for the first source + * item + * @param itemTimeoutIndicator + * a function that returns a {@code Publisher} for each item emitted by the current {@code Flowable} and that + * determines the timeout window in which the subsequent source item must arrive in order to + * continue the sequence + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code firstTimeoutIndicator} or {@code itemTimeoutIndicator} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/timeout.html">ReactiveX operators documentation: Timeout</a> + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.PASS_THROUGH) + @SchedulerSupport(SchedulerSupport.NONE) + public final <@NonNull U, @NonNull V> Flowable<T> timeout(@NonNull Publisher<U> firstTimeoutIndicator, + @NonNull Function<? super T, @NonNull ? extends Publisher<V>> itemTimeoutIndicator) { + Objects.requireNonNull(firstTimeoutIndicator, "firstTimeoutIndicator is null"); + return timeout0(firstTimeoutIndicator, itemTimeoutIndicator, null); + } + + /** + * Returns a {@code Flowable} that mirrors the current {@code Flowable}, but switches to a fallback {@link Publisher} if either + * the first item emitted by the current {@code Flowable} or any subsequent item doesn't arrive within time windows + * defined by other {@code Publisher}s. + * <p> + * <img width="640" height="400" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/timeout6.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream. The {@code Publisher} + * sources are expected to honor backpressure as well. + * If any of the current {@code Flowable}s violate this, it <em>may</em> throw an + * {@link IllegalStateException} when the current {@code Flowable} completes.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code timeout} does not operate by default on any {@link Scheduler}.</dd> + * </dl> + * + * @param <U> + * the first timeout value type (ignored) + * @param <V> + * the subsequent timeout value type (ignored) + * @param firstTimeoutIndicator + * a function that returns a {@code Publisher} which determines the timeout window for the first source + * item + * @param itemTimeoutIndicator + * a function that returns a {@code Publisher} for each item emitted by the current {@code Flowable} and that + * determines the timeout window in which the subsequent source item must arrive in order to + * continue the sequence + * @param fallback + * the fallback {@code Publisher} to switch to if the current {@code Flowable} times out + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code firstTimeoutIndicator}, {@code itemTimeoutIndicator} or {@code fallback} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/timeout.html">ReactiveX operators documentation: Timeout</a> + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + public final <@NonNull U, @NonNull V> Flowable<T> timeout( + @NonNull Publisher<U> firstTimeoutIndicator, + @NonNull Function<? super T, @NonNull ? extends Publisher<V>> itemTimeoutIndicator, + @NonNull Publisher<? extends T> fallback) { + Objects.requireNonNull(firstTimeoutIndicator, "firstTimeoutIndicator is null"); + Objects.requireNonNull(fallback, "fallback is null"); + return timeout0(firstTimeoutIndicator, itemTimeoutIndicator, fallback); + } + + private Flowable<T> timeout0(long timeout, TimeUnit unit, Publisher<? extends T> fallback, + Scheduler scheduler) { + Objects.requireNonNull(unit, "unit is null"); + Objects.requireNonNull(scheduler, "scheduler is null"); + return RxJavaPlugins.onAssembly(new FlowableTimeoutTimed<>(this, timeout, unit, scheduler, fallback)); + } + + private <@NonNull U, @NonNull V> Flowable<T> timeout0( + Publisher<U> firstTimeoutIndicator, + Function<? super T, @NonNull ? extends Publisher<V>> itemTimeoutIndicator, + Publisher<? extends T> fallback) { + Objects.requireNonNull(itemTimeoutIndicator, "itemTimeoutIndicator is null"); + return RxJavaPlugins.onAssembly(new FlowableTimeout<>(this, firstTimeoutIndicator, itemTimeoutIndicator, fallback)); + } + + /** + * Returns a {@code Flowable} that emits each item emitted by the current {@code Flowable}, wrapped in a + * {@link Timed} object. + * <p> + * <img width="640" height="310" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/timestamp.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator doesn't interfere with backpressure which is determined by the current {@code Flowable}'s backpressure + * behavior.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code timestamp} does not operate on any particular scheduler but uses the current time + * from the {@code computation} {@link Scheduler}.</dd> + * </dl> + * + * @return the new {@code Flowable} instance + * @see <a href="http://reactivex.io/documentation/operators/timestamp.html">ReactiveX operators documentation: Timestamp</a> + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.PASS_THROUGH) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Flowable<Timed<T>> timestamp() { + return timestamp(TimeUnit.MILLISECONDS, Schedulers.computation()); + } + + /** + * Returns a {@code Flowable} that emits each item emitted by the current {@code Flowable}, wrapped in a + * {@link Timed} object whose timestamps are provided by a specified {@link Scheduler}. + * <p> + * <img width="640" height="310" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/timestamp.s.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator doesn't interfere with backpressure which is determined by the current {@code Flowable}'s backpressure + * behavior.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>This operator does not operate on any particular scheduler but uses the current time + * from the specified {@code Scheduler}.</dd> + * </dl> + * + * @param scheduler + * the {@code Scheduler} to use as a time source + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code scheduler} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/timestamp.html">ReactiveX operators documentation: Timestamp</a> + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.PASS_THROUGH) + @SchedulerSupport(SchedulerSupport.NONE) // Supplied scheduler is only used for creating timestamps. + @NonNull + public final Flowable<Timed<T>> timestamp(@NonNull Scheduler scheduler) { + return timestamp(TimeUnit.MILLISECONDS, scheduler); + } + + /** + * Returns a {@code Flowable} that emits each item emitted by the current {@code Flowable}, wrapped in a + * {@link Timed} object. + * <p> + * <img width="640" height="310" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/timestamp.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator doesn't interfere with backpressure which is determined by the current {@code Flowable}'s backpressure + * behavior.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code timestamp} does not operate on any particular scheduler but uses the current time + * from the {@code computation} {@link Scheduler}.</dd> + * </dl> + * + * @param unit the time unit for the current time + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code unit} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/timestamp.html">ReactiveX operators documentation: Timestamp</a> + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.PASS_THROUGH) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Flowable<Timed<T>> timestamp(@NonNull TimeUnit unit) { + return timestamp(unit, Schedulers.computation()); + } + + /** + * Returns a {@code Flowable} that emits each item emitted by the current {@code Flowable}, wrapped in a + * {@link Timed} object whose timestamps are provided by a specified {@link Scheduler}. + * <p> + * <img width="640" height="310" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/timestamp.s.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator doesn't interfere with backpressure which is determined by the current {@code Flowable}'s backpressure + * behavior.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>This operator does not operate on any particular scheduler but uses the current time + * from the specified {@code Scheduler}.</dd> + * </dl> + * + * @param unit the time unit for the current time + * @param scheduler + * the {@code Scheduler} to use as a time source + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code unit} or {@code scheduler} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/timestamp.html">ReactiveX operators documentation: Timestamp</a> + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.PASS_THROUGH) + @SchedulerSupport(SchedulerSupport.NONE) // Supplied scheduler is only used for creating timestamps. + public final Flowable<Timed<T>> timestamp(@NonNull TimeUnit unit, @NonNull Scheduler scheduler) { + Objects.requireNonNull(unit, "unit is null"); + Objects.requireNonNull(scheduler, "scheduler is null"); + return map(Functions.timestampWith(unit, scheduler)); + } + + /** + * Calls the specified converter function during assembly time and returns its resulting value. + * <p> + * This allows fluent conversion to any other type. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The backpressure behavior depends on what happens in the {@code converter} function.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code to} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * <p>History: 2.1.7 - experimental + * @param <R> the resulting object type + * @param converter the function that receives the current {@code Flowable} instance and returns a value + * @return the converted value + * @throws NullPointerException if {@code converter} is {@code null} + * @since 2.2 + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.SPECIAL) + @SchedulerSupport(SchedulerSupport.NONE) + public final <@NonNull R> R to(@NonNull FlowableConverter<T, ? extends R> converter) { + return Objects.requireNonNull(converter, "converter is null").apply(this); + } + + /** + * Returns a {@link Single} that emits a single item, a list composed of all the items emitted by the + * finite upstream source {@link Publisher}. + * <p> + * <img width="640" height="305" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/toList.v3.png" alt=""> + * <p> + * Normally, a {@code Publisher} that returns multiple items will do so by invoking its {@link Subscriber}'s + * {@link Subscriber#onNext onNext} method for each such item. You can change this behavior by having the + * operator compose a list of all of these items and then to invoke the {@link SingleObserver}'s {@code onSuccess} + * method once, passing it the entire list, by calling the {@code Flowable}'s {@code toList} method prior to + * calling its {@link #subscribe} method. + * <p> + * Note that this operator requires the upstream to signal {@code onComplete} for the accumulated list to + * be emitted. Sources that are infinite and never complete will never emit anything through this + * operator and an infinite source may lead to a fatal {@link OutOfMemoryError}. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream and consumes the current {@code Flowable} in an + * unbounded manner (i.e., without applying backpressure to it).</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code toList} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @return the new {@code Single} instance + * @see <a href="http://reactivex.io/documentation/operators/to.html">ReactiveX operators documentation: To</a> + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.UNBOUNDED_IN) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Single<List<T>> toList() { + return RxJavaPlugins.onAssembly(new FlowableToListSingle<>(this)); + } + + /** + * Returns a {@link Single} that emits a single item, a list composed of all the items emitted by the + * finite source {@link Publisher}. + * <p> + * <img width="640" height="305" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/toList.v3.png" alt=""> + * <p> + * Normally, a {@code Publisher} that returns multiple items will do so by invoking its {@link Subscriber}'s + * {@link Subscriber#onNext onNext} method for each such item. You can change this behavior by having the + * operator compose a list of all of these items and then to invoke the {@link SingleObserver}'s {@code onSuccess} + * method once, passing it the entire list, by calling the {@code Flowable}'s {@code toList} method prior to + * calling its {@link #subscribe} method. + * <p> + * Note that this operator requires the upstream to signal {@code onComplete} for the accumulated list to + * be emitted. Sources that are infinite and never complete will never emit anything through this + * operator and an infinite source may lead to a fatal {@link OutOfMemoryError}. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream and consumes the current {@code Flowable} in an + * unbounded manner (i.e., without applying backpressure to it).</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code toList} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param capacityHint + * the number of elements expected from the current {@code Flowable} + * @return the new {@code Single} instance + * @throws IllegalArgumentException if {@code capacityHint} is non-positive + * @see <a href="http://reactivex.io/documentation/operators/to.html">ReactiveX operators documentation: To</a> + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.UNBOUNDED_IN) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Single<List<T>> toList(int capacityHint) { + ObjectHelper.verifyPositive(capacityHint, "capacityHint"); + return RxJavaPlugins.onAssembly(new FlowableToListSingle<>(this, Functions.createArrayList(capacityHint))); + } + + /** + * Returns a {@link Single} that emits a single item, a list composed of all the items emitted by the + * finite source {@link Publisher}. + * <p> + * <img width="640" height="305" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/toList.v3.png" alt=""> + * <p> + * Normally, a {@code Publisher} that returns multiple items will do so by invoking its {@link Subscriber}'s + * {@link Subscriber#onNext onNext} method for each such item. You can change this behavior by having the + * operator compose a collection of all of these items and then to invoke the {@link SingleObserver}'s {@code onSuccess} + * method once, passing it the entire collection, by calling the {@code Flowable}'s {@code toList} method prior to + * calling its {@link #subscribe} method. + * <p> + * Note that this operator requires the upstream to signal {@code onComplete} for the accumulated collection to + * be emitted. Sources that are infinite and never complete will never emit anything through this + * operator and an infinite source may lead to a fatal {@link OutOfMemoryError}. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream and consumes the current {@code Flowable} in an + * unbounded manner (i.e., without applying backpressure to it).</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code toList} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <U> the subclass of a collection of Ts + * @param collectionSupplier + * the {@link Supplier} returning the collection (for each individual {@code Subscriber}) to be filled in + * @return the new {@code Single} instance + * @throws NullPointerException if {@code collectionSupplier} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/to.html">ReactiveX operators documentation: To</a> + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.UNBOUNDED_IN) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull U extends Collection<? super T>> Single<U> toList(@NonNull Supplier<U> collectionSupplier) { + Objects.requireNonNull(collectionSupplier, "collectionSupplier is null"); + return RxJavaPlugins.onAssembly(new FlowableToListSingle<>(this, collectionSupplier)); + } + + /** + * Returns a {@link Single} that emits a single {@link HashMap} containing all items emitted by the finite source {@link Publisher}, + * mapped by the keys returned by a specified {@code keySelector} function. + * <p> + * <img width="640" height="305" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/toMap.v3.png" alt=""> + * <p> + * If more than one source item maps to the same key, the {@code HashMap} will contain the latest of those items. + * <p> + * Note that this operator requires the upstream to signal {@code onComplete} for the accumulated map to + * be emitted. Sources that are infinite and never complete will never emit anything through this + * operator and an infinite source may lead to a fatal {@link OutOfMemoryError}. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream and consumes the current {@code Flowable} in an + * unbounded manner (i.e., without applying backpressure to it).</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code toMap} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <K> the key type of the Map + * @param keySelector + * the function that extracts the key from a source item to be used in the {@code HashMap} + * @return the new {@code Single} instance + * @throws NullPointerException if {@code keySelector} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/to.html">ReactiveX operators documentation: To</a> + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.UNBOUNDED_IN) + @SchedulerSupport(SchedulerSupport.NONE) + public final <@NonNull K> Single<Map<K, T>> toMap(@NonNull Function<? super T, ? extends K> keySelector) { + Objects.requireNonNull(keySelector, "keySelector is null"); + return collect(HashMapSupplier.asSupplier(), Functions.toMapKeySelector(keySelector)); + } + + /** + * Returns a {@link Single} that emits a single {@link HashMap} containing values corresponding to items emitted by the + * finite source {@link Publisher}, mapped by the keys returned by a specified {@code keySelector} function. + * <p> + * <img width="640" height="305" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/toMap.v3.png" alt=""> + * <p> + * If more than one source item maps to the same key, the {@code HashMap} will contain a single entry that + * corresponds to the latest of those items. + * <p> + * Note that this operator requires the upstream to signal {@code onComplete} for the accumulated map to + * be emitted. Sources that are infinite and never complete will never emit anything through this + * operator and an infinite source may lead to a fatal {@link OutOfMemoryError}. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream and consumes the current {@code Flowable} in an + * unbounded manner (i.e., without applying backpressure to it).</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code toMap} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <K> the key type of the Map + * @param <V> the value type of the Map + * @param keySelector + * the function that extracts the key from a source item to be used in the {@code HashMap} + * @param valueSelector + * the function that extracts the value from a source item to be used in the {@code HashMap} + * @return the new {@code Single} instance + * @throws NullPointerException if {@code keySelector} or {@code valueSelector} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/to.html">ReactiveX operators documentation: To</a> + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.UNBOUNDED_IN) + @SchedulerSupport(SchedulerSupport.NONE) + public final <@NonNull K, @NonNull V> Single<Map<K, V>> toMap(@NonNull Function<? super T, ? extends K> keySelector, @NonNull Function<? super T, ? extends V> valueSelector) { + Objects.requireNonNull(keySelector, "keySelector is null"); + Objects.requireNonNull(valueSelector, "valueSelector is null"); + return collect(HashMapSupplier.asSupplier(), Functions.toMapKeyValueSelector(keySelector, valueSelector)); + } + + /** + * Returns a {@link Single} that emits a single {@link Map}, returned by a specified {@code mapFactory} function, that + * contains keys and values extracted from the items emitted by the finite source {@link Publisher}. + * <p> + * <img width="640" height="305" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/toMap.v3.png" alt=""> + * <p> + * Note that this operator requires the upstream to signal {@code onComplete} for the accumulated map to + * be emitted. Sources that are infinite and never complete will never emit anything through this + * operator and an infinite source may lead to a fatal {@link OutOfMemoryError}. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream and consumes the current {@code Flowable} in an + * unbounded manner (i.e., without applying backpressure to it).</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code toMap} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <K> the key type of the Map + * @param <V> the value type of the Map + * @param keySelector + * the function that extracts the key from a source item to be used in the Map + * @param valueSelector + * the function that extracts the value from the source items to be used as value in the Map + * @param mapSupplier + * the function that returns a {@code Map} instance to be used + * @return the new {@code Single} instance + * @throws NullPointerException if {@code keySelector}, {@code valueSelector} or {@code mapSupplier} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/to.html">ReactiveX operators documentation: To</a> + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.UNBOUNDED_IN) + @SchedulerSupport(SchedulerSupport.NONE) + public final <@NonNull K, @NonNull V> Single<Map<K, V>> toMap(@NonNull Function<? super T, ? extends K> keySelector, + @NonNull Function<? super T, ? extends V> valueSelector, + @NonNull Supplier<? extends Map<K, V>> mapSupplier) { + Objects.requireNonNull(keySelector, "keySelector is null"); + Objects.requireNonNull(valueSelector, "valueSelector is null"); + return collect(mapSupplier, Functions.toMapKeyValueSelector(keySelector, valueSelector)); + } + + /** + * Returns a {@link Single} that emits a single {@link HashMap} that contains an {@link ArrayList} of items emitted by the + * finite source {@link Publisher} keyed by a specified {@code keySelector} function. + * <p> + * <img width="640" height="305" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/toMultiMap.v3.png" alt=""> + * <p> + * Note that this operator requires the upstream to signal {@code onComplete} for the accumulated map to + * be emitted. Sources that are infinite and never complete will never emit anything through this + * operator and an infinite source may lead to a fatal {@link OutOfMemoryError}. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>This operator does not support backpressure as by intent it is requesting and buffering everything.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code toMultimap} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <K> the key type of the Map + * @param keySelector + * the function that extracts the key from the source items to be used as key in the {@code HashMap} + * @return the new {@code Single} instance + * @throws NullPointerException if {@code keySelector} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/to.html">ReactiveX operators documentation: To</a> + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.UNBOUNDED_IN) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull K> Single<Map<K, Collection<T>>> toMultimap(@NonNull Function<? super T, ? extends K> keySelector) { + Function<T, T> valueSelector = Functions.identity(); + Supplier<Map<K, Collection<T>>> mapSupplier = HashMapSupplier.asSupplier(); + Function<K, List<T>> collectionFactory = ArrayListSupplier.asFunction(); + return toMultimap(keySelector, valueSelector, mapSupplier, collectionFactory); + } + + /** + * Returns a {@link Single} that emits a single {@link HashMap} that contains an {@link ArrayList} of values extracted by a + * specified {@code valueSelector} function from items emitted by the finite source {@link Publisher}, keyed by a + * specified {@code keySelector} function. + * <p> + * <img width="640" height="305" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/toMultiMap.v3.png" alt=""> + * <p> + * Note that this operator requires the upstream to signal {@code onComplete} for the accumulated map to + * be emitted. Sources that are infinite and never complete will never emit anything through this + * operator and an infinite source may lead to a fatal {@link OutOfMemoryError}. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream and consumes the current {@code Flowable} in an + * unbounded manner (i.e., without applying backpressure to it).</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code toMultimap} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <K> the key type of the Map + * @param <V> the value type of the Map + * @param keySelector + * the function that extracts a key from the source items to be used as key in the {@code HashMap} + * @param valueSelector + * the function that extracts a value from the source items to be used as value in the {@code HashMap} + * @return the new {@code Single} instance + * @throws NullPointerException if {@code keySelector} or {@code valueSelector} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/to.html">ReactiveX operators documentation: To</a> + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.UNBOUNDED_IN) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull K, @NonNull V> Single<Map<K, Collection<V>>> toMultimap(@NonNull Function<? super T, ? extends K> keySelector, @NonNull Function<? super T, ? extends V> valueSelector) { + Supplier<Map<K, Collection<V>>> mapSupplier = HashMapSupplier.asSupplier(); + Function<K, List<V>> collectionFactory = ArrayListSupplier.asFunction(); + return toMultimap(keySelector, valueSelector, mapSupplier, collectionFactory); + } + + /** + * Returns a {@link Single} that emits a single {@link Map}, returned by a specified {@code mapFactory} function, that + * contains a custom collection of values, extracted by a specified {@code valueSelector} function from + * items emitted by the finite source {@link Publisher}, and keyed by the {@code keySelector} function. + * <p> + * <img width="640" height="305" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/toMultiMap.v3.png" alt=""> + * <p> + * Note that this operator requires the upstream to signal {@code onComplete} for the accumulated map to + * be emitted. Sources that are infinite and never complete will never emit anything through this + * operator and an infinite source may lead to a fatal {@link OutOfMemoryError}. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream and consumes the current {@code Flowable} in an + * unbounded manner (i.e., without applying backpressure to it).</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code toMultimap} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <K> the key type of the Map + * @param <V> the value type of the Map + * @param keySelector + * the function that extracts a key from the source items to be used as the key in the Map + * @param valueSelector + * the function that extracts a value from the source items to be used as the value in the {@code Map} + * @param mapSupplier + * the function that returns a Map instance to be used + * @param collectionFactory + * the function that returns a {@link Collection} instance for a particular key to be used in the {@code Map} + * @return the new {@code Single} instance + * @throws NullPointerException if {@code keySelector}, {@code valueSelector}, {@code mapSupplier} or {@code collectionFactory} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/to.html">ReactiveX operators documentation: To</a> + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.UNBOUNDED_IN) + @SchedulerSupport(SchedulerSupport.NONE) + public final <@NonNull K, @NonNull V> Single<Map<K, Collection<V>>> toMultimap( + @NonNull Function<? super T, ? extends K> keySelector, + @NonNull Function<? super T, ? extends V> valueSelector, + @NonNull Supplier<? extends Map<K, Collection<V>>> mapSupplier, + @NonNull Function<? super K, ? extends Collection<? super V>> collectionFactory) { + Objects.requireNonNull(keySelector, "keySelector is null"); + Objects.requireNonNull(valueSelector, "valueSelector is null"); + Objects.requireNonNull(mapSupplier, "mapSupplier is null"); + Objects.requireNonNull(collectionFactory, "collectionFactory is null"); + return collect(mapSupplier, Functions.toMultimapKeyValueSelector(keySelector, valueSelector, collectionFactory)); + } + + /** + * Returns a {@link Single} that emits a single {@link Map}, returned by a specified {@code mapFactory} function, that + * contains an {@link ArrayList} of values, extracted by a specified {@code valueSelector} function from items + * emitted by the finite source {@link Publisher} and keyed by the {@code keySelector} function. + * <p> + * <img width="640" height="305" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/toMultiMap.v3.png" alt=""> + * <p> + * Note that this operator requires the upstream to signal {@code onComplete} for the accumulated map to + * be emitted. Sources that are infinite and never complete will never emit anything through this + * operator and an infinite source may lead to a fatal {@link OutOfMemoryError}. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream and consumes the current {@code Flowable} in an + * unbounded manner (i.e., without applying backpressure to it).</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code toMultimap} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <K> the key type of the {@code Map} + * @param <V> the value type of the {@code Map} + * @param keySelector + * the function that extracts a key from the source items to be used as the key in the {@code Map} + * @param valueSelector + * the function that extracts a value from the source items to be used as the value in the {@code Map} + * @param mapSupplier + * the function that returns a {@code Map} instance to be used + * @return the new {@code Single} instance + * @throws NullPointerException if {@code keySelector}, {@code valueSelector} or {@code mapSupplier} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/to.html">ReactiveX operators documentation: To</a> + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.UNBOUNDED_IN) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull K, @NonNull V> Single<Map<K, Collection<V>>> toMultimap( + @NonNull Function<? super T, ? extends K> keySelector, + @NonNull Function<? super T, ? extends V> valueSelector, + @NonNull Supplier<Map<K, Collection<V>>> mapSupplier + ) { + return toMultimap(keySelector, valueSelector, mapSupplier, ArrayListSupplier.asFunction()); + } + + /** + * Converts the current {@code Flowable} into a non-backpressured {@link Observable}. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>{@code Observable}s don't support backpressure thus the current {@code Flowable} is consumed in an unbounded + * manner (by requesting {@link Long#MAX_VALUE}).</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code toObservable} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @return the new {@code Observable} instance + * @since 2.0 + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.UNBOUNDED_IN) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Observable<T> toObservable() { + return RxJavaPlugins.onAssembly(new ObservableFromPublisher<>(this)); + } + + /** + * Returns a {@link Single} that emits a {@link List} that contains the items emitted by the finite source {@link Publisher}, in a + * sorted order. Each item emitted by the {@code Publisher} must implement {@link Comparable} with respect to all + * other items in the sequence. + * + * <p>If any item emitted by this {@code Flowable} does not implement {@code Comparable} with respect to + * all other items emitted by this {@code Flowable}, no items will be emitted and the + * sequence is terminated with a {@link ClassCastException}. + * <p> + * <img width="640" height="310" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/toSortedList.v3.png" alt=""> + * <p> + * Note that this operator requires the upstream to signal {@code onComplete} for the accumulated list to + * be emitted. Sources that are infinite and never complete will never emit anything through this + * operator and an infinite source may lead to a fatal {@link OutOfMemoryError}. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream and consumes the current {@code Flowable} in an + * unbounded manner (i.e., without applying backpressure to it).</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code toSortedList} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @return the new {@code Single} instance + * @see <a href="http://reactivex.io/documentation/operators/to.html">ReactiveX operators documentation: To</a> + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.UNBOUNDED_IN) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Single<List<T>> toSortedList() { + return toSortedList(Functions.naturalComparator()); + } + + /** + * Returns a {@link Single} that emits a {@link List} that contains the items emitted by the finite source {@link Publisher}, in a + * sorted order based on a specified comparison function. + * <p> + * <img width="640" height="310" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/toSortedList.f.v3.png" alt=""> + * <p> + * Note that this operator requires the upstream to signal {@code onComplete} for the accumulated list to + * be emitted. Sources that are infinite and never complete will never emit anything through this + * operator and an infinite source may lead to a fatal {@link OutOfMemoryError}. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream and consumes the current {@code Flowable} in an + * unbounded manner (i.e., without applying backpressure to it).</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code toSortedList} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param comparator + * a function that compares two items emitted by the current {@code Flowable} and returns an {@code int} + * that indicates their sort order + * @return the new {@code Single} instance + * @throws NullPointerException if {@code comparator} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/to.html">ReactiveX operators documentation: To</a> + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.UNBOUNDED_IN) + @SchedulerSupport(SchedulerSupport.NONE) + public final Single<List<T>> toSortedList(@NonNull Comparator<? super T> comparator) { + Objects.requireNonNull(comparator, "comparator is null"); + return toList().map(Functions.listSorter(comparator)); + } + + /** + * Returns a {@link Single} that emits a {@link List} that contains the items emitted by the finite source {@link Publisher}, in a + * sorted order based on a specified comparison function. + * <p> + * <img width="640" height="310" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/toSortedList.f.v3.png" alt=""> + * <p> + * Note that this operator requires the upstream to signal {@code onComplete} for the accumulated list to + * be emitted. Sources that are infinite and never complete will never emit anything through this + * operator and an infinite source may lead to a fatal {@link OutOfMemoryError}. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream and consumes the current {@code Flowable} in an + * unbounded manner (i.e., without applying backpressure to it).</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code toSortedList} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param comparator + * a function that compares two items emitted by the current {@code Flowable} and returns an {@code int} + * that indicates their sort order + * @param capacityHint + * the initial capacity of the {@link ArrayList} used to accumulate items before sorting + * @return the new {@code Single} instance + * @throws NullPointerException if {@code comparator} is {@code null} + * @throws IllegalArgumentException if {@code capacityHint} is non-positive + * @see <a href="http://reactivex.io/documentation/operators/to.html">ReactiveX operators documentation: To</a> + * @since 2.0 + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.UNBOUNDED_IN) + @SchedulerSupport(SchedulerSupport.NONE) + public final Single<List<T>> toSortedList(@NonNull Comparator<? super T> comparator, int capacityHint) { + Objects.requireNonNull(comparator, "comparator is null"); + return toList(capacityHint).map(Functions.listSorter(comparator)); + } + + /** + * Returns a {@link Single} that emits a {@link List} that contains the items emitted by the finite source {@link Publisher}, in a + * sorted order. Each item emitted by the {@code Publisher} must implement {@link Comparable} with respect to all + * other items in the sequence. + * + * <p>If any item emitted by this {@code Flowable} does not implement {@code Comparable} with respect to + * all other items emitted by this {@code Flowable}, no items will be emitted and the + * sequence is terminated with a {@link ClassCastException}. + * <p> + * <img width="640" height="310" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/toSortedList.v3.png" alt=""> + * <p> + * Note that this operator requires the upstream to signal {@code onComplete} for the accumulated list to + * be emitted. Sources that are infinite and never complete will never emit anything through this + * operator and an infinite source may lead to a fatal {@link OutOfMemoryError}. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream and consumes the current {@code Flowable} in an + * unbounded manner (i.e., without applying backpressure to it).</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code toSortedList} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param capacityHint + * the initial capacity of the {@link ArrayList} used to accumulate items before sorting + * @return the new {@code Single} instance + * @throws IllegalArgumentException if {@code capacityHint} is non-positive + * @see <a href="http://reactivex.io/documentation/operators/to.html">ReactiveX operators documentation: To</a> + * @since 2.0 + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.UNBOUNDED_IN) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Single<List<T>> toSortedList(int capacityHint) { + return toSortedList(Functions.naturalComparator(), capacityHint); + } + + /** + * Cancels the current {@code Flowable} asynchronously by invoking {@link Subscription#cancel()} + * on the specified {@link Scheduler}. + * <p> + * The operator suppresses signals from the current {@code Flowable} immediately when the + * downstream cancels the flow because the actual cancellation itself could take an arbitrary amount of time + * to take effect and make the flow stop producing items. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator doesn't interfere with backpressure which is determined by the current {@code Flowable}'s backpressure + * behavior.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>You specify which {@code Scheduler} this operator will use.</dd> + * </dl> + * + * @param scheduler + * the {@code Scheduler} to perform cancellation actions on + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code scheduler} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/subscribeon.html">ReactiveX operators documentation: SubscribeOn</a> + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.PASS_THROUGH) + @SchedulerSupport(SchedulerSupport.CUSTOM) + public final Flowable<T> unsubscribeOn(@NonNull Scheduler scheduler) { + Objects.requireNonNull(scheduler, "scheduler is null"); + return RxJavaPlugins.onAssembly(new FlowableUnsubscribeOn<>(this, scheduler)); + } + + /** + * Returns a {@code Flowable} that emits windows of items it collects from the current {@code Flowable}. The resulting + * {@code Flowable} emits connected, non-overlapping windows, each containing {@code count} items. When the current + * {@code Flowable} completes or encounters an error, the resulting {@code Flowable} emits the current window and + * propagates the notification from the current {@code Flowable}. + * <p> + * <img width="640" height="400" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/window3.v3.png" alt=""> + * <p> + * Note that ignoring windows or subscribing later (i.e., on another thread) will result in + * so-called window abandonment where a window will only contain one element. The behavior is + * a trade-off between no-dataloss and ensuring upstream cancellation can happen. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure of its inner and outer subscribers, however, the inner {@code Flowable} uses an + * unbounded buffer that may hold at most {@code count} elements.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>This version of {@code window} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param count + * the maximum size of each window before it should be emitted + * @return the new {@code Flowable} instance + * @throws IllegalArgumentException if {@code count} is non-positive + * @see <a href="http://reactivex.io/documentation/operators/window.html">ReactiveX operators documentation: Window</a> + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Flowable<Flowable<T>> window(long count) { + return window(count, count, bufferSize()); + } + + /** + * Returns a {@code Flowable} that emits windows of items it collects from the current {@code Flowable}. The resulting + * {@code Flowable} emits windows every {@code skip} items, each containing no more than {@code count} items. When + * the current {@code Flowable} completes or encounters an error, the resulting {@code Flowable} emits the current window + * and propagates the notification from the current {@code Flowable}. + * <p> + * <img width="640" height="365" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/window4.v3.png" alt=""> + * <p> + * Note that ignoring windows or subscribing later (i.e., on another thread) will result in + * so-called window abandonment where a window may not contain any elements. In this case, subsequent + * elements will be dropped until the condition for the next window boundary is satisfied. The behavior is + * a trade-off between no-dataloss and ensuring upstream cancellation can happen under some race conditions. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure of its inner and outer subscribers, however, the inner {@code Flowable} uses an + * unbounded buffer that may hold at most {@code count} elements.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>This version of {@code window} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param count + * the maximum size of each window before it should be emitted + * @param skip + * how many items need to be skipped before starting a new window. Note that if {@code skip} and + * {@code count} are equal this is the same operation as {@link #window(long)}. + * @return the new {@code Flowable} instance + * @throws IllegalArgumentException if {@code count} or {@code skip} is non-positive + * @see <a href="http://reactivex.io/documentation/operators/window.html">ReactiveX operators documentation: Window</a> + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Flowable<Flowable<T>> window(long count, long skip) { + return window(count, skip, bufferSize()); + } + + /** + * Returns a {@code Flowable} that emits windows of items it collects from the current {@code Flowable}. The resulting + * {@code Flowable} emits windows every {@code skip} items, each containing no more than {@code count} items. When + * the current {@code Flowable} completes or encounters an error, the resulting {@code Flowable} emits the current window + * and propagates the notification from the current {@code Flowable}. + * <p> + * <img width="640" height="365" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/window4.v3.png" alt=""> + * <p> + * Note that ignoring windows or subscribing later (i.e., on another thread) will result in + * so-called window abandonment where a window may not contain any elements. In this case, subsequent + * elements will be dropped until the condition for the next window boundary is satisfied. The behavior is + * a trade-off between no-dataloss and ensuring upstream cancellation can happen under some race conditions. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure of its inner and outer subscribers, however, the inner {@code Flowable} uses an + * unbounded buffer that may hold at most {@code count} elements.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>This version of {@code window} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param count + * the maximum size of each window before it should be emitted + * @param skip + * how many items need to be skipped before starting a new window. Note that if {@code skip} and + * {@code count} are equal this is the same operation as {@link #window(long)}. + * @param bufferSize + * the capacity hint for the buffer in the inner windows + * @return the new {@code Flowable} instance + * @throws IllegalArgumentException if {@code count}, {@code skip} or {@code bufferSize} is non-positive + * @see <a href="http://reactivex.io/documentation/operators/window.html">ReactiveX operators documentation: Window</a> + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Flowable<Flowable<T>> window(long count, long skip, int bufferSize) { + ObjectHelper.verifyPositive(skip, "skip"); + ObjectHelper.verifyPositive(count, "count"); + ObjectHelper.verifyPositive(bufferSize, "bufferSize"); + return RxJavaPlugins.onAssembly(new FlowableWindow<>(this, count, skip, bufferSize)); + } + + /** + * Returns a {@code Flowable} that emits windows of items it collects from the current {@code Flowable}. The resulting + * {@code Flowable} starts a new window periodically, as determined by the {@code timeskip} argument. It emits + * each window after a fixed timespan, specified by the {@code timespan} argument. When the current + * {@code Flowable} completes or encounters an error, the resulting {@code Flowable} emits the + * current window and propagates the notification from the current {@code Flowable}. + * <p> + * <img width="640" height="335" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/window7.v3.png" alt=""> + * <p> + * Note that ignoring windows or subscribing later (i.e., on another thread) will result in + * so-called window abandonment where a window may not contain any elements. In this case, subsequent + * elements will be dropped until the condition for the next window boundary is satisfied. The behavior is + * a trade-off for ensuring upstream cancellation can happen under some race conditions. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator consumes the current {@code Flowable} in an unbounded manner. + * The resulting {@code Flowable} doesn't support backpressure as it uses + * time to control the creation of windows. The emitted inner {@code Flowable}s honor + * backpressure but have an unbounded inner buffer that <em>may</em> lead to {@link OutOfMemoryError} + * if left unconsumed.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>This version of {@code window} operates by default on the {@code computation} {@link Scheduler}.</dd> + * </dl> + * + * @param timespan + * the period of time each window collects items before it should be emitted + * @param timeskip + * the period of time after which a new window will be created + * @param unit + * the unit of time that applies to the {@code timespan} and {@code timeskip} arguments + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code unit} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/window.html">ReactiveX operators documentation: Window</a> + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.ERROR) + @SchedulerSupport(SchedulerSupport.COMPUTATION) + @NonNull + public final Flowable<Flowable<T>> window(long timespan, long timeskip, @NonNull TimeUnit unit) { + return window(timespan, timeskip, unit, Schedulers.computation(), bufferSize()); + } + + /** + * Returns a {@code Flowable} that emits windows of items it collects from the current {@code Flowable}. The resulting + * {@code Flowable} starts a new window periodically, as determined by the {@code timeskip} argument. It emits + * each window after a fixed timespan, specified by the {@code timespan} argument. When the current + * {@code Flowable} completes or encounters an error, the resulting {@code Flowable} emits the + * current window and propagates the notification from the current {@code Flowable}. + * <p> + * <img width="640" height="335" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/window7.s.v3.png" alt=""> + * <p> + * Note that ignoring windows or subscribing later (i.e., on another thread) will result in + * so-called window abandonment where a window may not contain any elements. In this case, subsequent + * elements will be dropped until the condition for the next window boundary is satisfied. The behavior is + * a trade-off for ensuring upstream cancellation can happen under some race conditions. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator consumes the current {@code Flowable} in an unbounded manner. + * The resulting {@code Flowable} doesn't support backpressure as it uses + * time to control the creation of windows. The returned inner {@code Flowable}s honor + * backpressure but have an unbounded inner buffer that <em>may</em> lead to {@link OutOfMemoryError} + * if left unconsumed.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>You specify which {@link Scheduler} this operator will use.</dd> + * </dl> + * + * @param timespan + * the period of time each window collects items before it should be emitted + * @param timeskip + * the period of time after which a new window will be created + * @param unit + * the unit of time that applies to the {@code timespan} and {@code timeskip} arguments + * @param scheduler + * the {@code Scheduler} to use when determining the end and start of a window + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code unit} or {@code scheduler} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/window.html">ReactiveX operators documentation: Window</a> + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.ERROR) + @SchedulerSupport(SchedulerSupport.CUSTOM) + @NonNull + public final Flowable<Flowable<T>> window(long timespan, long timeskip, @NonNull TimeUnit unit, @NonNull Scheduler scheduler) { + return window(timespan, timeskip, unit, scheduler, bufferSize()); + } + + /** + * Returns a {@code Flowable} that emits windows of items it collects from the current {@code Flowable}. The resulting + * {@code Flowable} starts a new window periodically, as determined by the {@code timeskip} argument. It emits + * each window after a fixed timespan, specified by the {@code timespan} argument. When the current + * {@code Flowable} completes or encounters an error, the resulting {@code Flowable} emits the + * current window and propagates the notification from the current {@code Flowable}. + * <p> + * <img width="640" height="335" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/window7.s.v3.png" alt=""> + * <p> + * Note that ignoring windows or subscribing later (i.e., on another thread) will result in + * so-called window abandonment where a window may not contain any elements. In this case, subsequent + * elements will be dropped until the condition for the next window boundary is satisfied. The behavior is + * a trade-off for ensuring upstream cancellation can happen under some race conditions. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator consumes the current {@code Flowable} in an unbounded manner. + * The resulting {@code Flowable} doesn't support backpressure as it uses + * time to control the creation of windows. The returned inner {@code Flowable}s honor + * backpressure but have an unbounded inner buffer that <em>may</em> lead to {@link OutOfMemoryError} + * if left unconsumed.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>You specify which {@link Scheduler} this operator will use.</dd> + * </dl> + * + * @param timespan + * the period of time each window collects items before it should be emitted + * @param timeskip + * the period of time after which a new window will be created + * @param unit + * the unit of time that applies to the {@code timespan} and {@code timeskip} arguments + * @param scheduler + * the {@code Scheduler} to use when determining the end and start of a window + * @param bufferSize + * the capacity hint for the buffer in the inner windows + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code unit} or {@code scheduler} is {@code null} + * @throws IllegalArgumentException if {@code timespan}, {@code timeskip} or {@code bufferSize} is non-positive + * @see <a href="http://reactivex.io/documentation/operators/window.html">ReactiveX operators documentation: Window</a> + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.ERROR) + @SchedulerSupport(SchedulerSupport.CUSTOM) + public final Flowable<Flowable<T>> window(long timespan, long timeskip, @NonNull TimeUnit unit, @NonNull Scheduler scheduler, int bufferSize) { + ObjectHelper.verifyPositive(bufferSize, "bufferSize"); + ObjectHelper.verifyPositive(timespan, "timespan"); + ObjectHelper.verifyPositive(timeskip, "timeskip"); + Objects.requireNonNull(scheduler, "scheduler is null"); + Objects.requireNonNull(unit, "unit is null"); + return RxJavaPlugins.onAssembly(new FlowableWindowTimed<>(this, timespan, timeskip, unit, scheduler, Long.MAX_VALUE, bufferSize, false)); + } + + /** + * Returns a {@code Flowable} that emits windows of items it collects from the current {@code Flowable}. The resulting + * {@code Flowable} emits connected, non-overlapping windows, each of a fixed duration specified by the + * {@code timespan} argument. When the current {@code Flowable} completes or encounters an error, the resulting + * {@code Flowable} emits the current window and propagates the notification from the current {@code Flowable}. + * <p> + * <img width="640" height="375" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/window5.v3.png" alt=""> + * <p> + * Note that ignoring windows or subscribing later (i.e., on another thread) will result in + * so-called window abandonment where a window may not contain any elements. In this case, subsequent + * elements will be dropped until the condition for the next window boundary is satisfied. The behavior is + * a trade-off for ensuring upstream cancellation can happen under some race conditions. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator consumes the current {@code Flowable} in an unbounded manner. + * The resulting {@code Flowable} doesn't support backpressure as it uses + * time to control the creation of windows. The emitted inner {@code Flowable}s honor + * backpressure and may hold up to {@code count} elements at most.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>This version of {@code window} operates by default on the {@code computation} {@link Scheduler}.</dd> + * </dl> + * + * @param timespan + * the period of time each window collects items before it should be emitted and replaced with a + * new window + * @param unit + * the unit of time that applies to the {@code timespan} argument + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code unit} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/window.html">ReactiveX operators documentation: Window</a> + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.ERROR) + @SchedulerSupport(SchedulerSupport.COMPUTATION) + @NonNull + public final Flowable<Flowable<T>> window(long timespan, @NonNull TimeUnit unit) { + return window(timespan, unit, Schedulers.computation(), Long.MAX_VALUE, false); + } + + /** + * Returns a {@code Flowable} that emits windows of items it collects from the current {@code Flowable}. The resulting + * {@code Flowable} emits connected, non-overlapping windows, each of a fixed duration as specified by the + * {@code timespan} argument or a maximum size as specified by the {@code count} argument (whichever is + * reached first). When the current {@code Flowable} completes or encounters an error, the resulting {@code Flowable} + * emits the current window and propagates the notification from the current {@code Flowable}. + * <p> + * <img width="640" height="370" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/window6.v3.png" alt=""> + * <p> + * Note that ignoring windows or subscribing later (i.e., on another thread) will result in + * so-called window abandonment where a window may not contain any elements. In this case, subsequent + * elements will be dropped until the condition for the next window boundary is satisfied. The behavior is + * a trade-off for ensuring upstream cancellation can happen under some race conditions. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator consumes the current {@code Flowable} in an unbounded manner. + * The resulting {@code Flowable} doesn't support backpressure as it uses + * time to control the creation of windows. The emitted inner {@code Flowable}s honor + * backpressure and may hold up to {@code count} elements at most.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>This version of {@code window} operates by default on the {@code computation} {@link Scheduler}.</dd> + * </dl> + * + * @param timespan + * the period of time each window collects items before it should be emitted and replaced with a + * new window + * @param unit + * the unit of time that applies to the {@code timespan} argument + * @param count + * the maximum size of each window before it should be emitted + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code unit} is {@code null} + * @throws IllegalArgumentException if {@code count} is non-positive + * @see <a href="http://reactivex.io/documentation/operators/window.html">ReactiveX operators documentation: Window</a> + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.ERROR) + @SchedulerSupport(SchedulerSupport.COMPUTATION) + @NonNull + public final Flowable<Flowable<T>> window(long timespan, @NonNull TimeUnit unit, + long count) { + return window(timespan, unit, Schedulers.computation(), count, false); + } + + /** + * Returns a {@code Flowable} that emits windows of items it collects from the current {@code Flowable}. The resulting + * {@code Flowable} emits connected, non-overlapping windows, each of a fixed duration as specified by the + * {@code timespan} argument or a maximum size as specified by the {@code count} argument (whichever is + * reached first). When the current {@code Flowable} completes or encounters an error, the resulting {@code Flowable} + * emits the current window and propagates the notification from the current {@code Flowable}. + * <p> + * <img width="640" height="370" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/window6.v3.png" alt=""> + * <p> + * Note that ignoring windows or subscribing later (i.e., on another thread) will result in + * so-called window abandonment where a window may not contain any elements. In this case, subsequent + * elements will be dropped until the condition for the next window boundary is satisfied. The behavior is + * a trade-off for ensuring upstream cancellation can happen under some race conditions. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator consumes the current {@code Flowable} in an unbounded manner. + * The resulting {@code Flowable} doesn't support backpressure as it uses + * time to control the creation of windows. The emitted inner {@code Flowable}s honor + * backpressure and may hold up to {@code count} elements at most.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>This version of {@code window} operates by default on the {@code computation} {@link Scheduler}.</dd> + * </dl> + * + * @param timespan + * the period of time each window collects items before it should be emitted and replaced with a + * new window + * @param unit + * the unit of time that applies to the {@code timespan} argument + * @param count + * the maximum size of each window before it should be emitted + * @param restart + * if {@code true}, when a window reaches the capacity limit, the timer is restarted as well + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code unit} is {@code null} + * @throws IllegalArgumentException if {@code count} is non-positive + * @see <a href="http://reactivex.io/documentation/operators/window.html">ReactiveX operators documentation: Window</a> + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.ERROR) + @SchedulerSupport(SchedulerSupport.COMPUTATION) + @NonNull + public final Flowable<Flowable<T>> window(long timespan, @NonNull TimeUnit unit, + long count, boolean restart) { + return window(timespan, unit, Schedulers.computation(), count, restart); + } + + /** + * Returns a {@code Flowable} that emits windows of items it collects from the current {@code Flowable}. The resulting + * {@code Flowable} emits connected, non-overlapping windows, each of a fixed duration as specified by the + * {@code timespan} argument. When the current {@code Flowable} completes or encounters an error, the resulting + * {@code Flowable} emits the current window and propagates the notification from the current {@code Flowable}. + * <p> + * <img width="640" height="375" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/window5.s.v3.png" alt=""> + * <p> + * Note that ignoring windows or subscribing later (i.e., on another thread) will result in + * so-called window abandonment where a window may not contain any elements. In this case, subsequent + * elements will be dropped until the condition for the next window boundary is satisfied. The behavior is + * a trade-off for ensuring upstream cancellation can happen under some race conditions. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator consumes the current {@code Flowable} in an unbounded manner. + * The resulting {@code Flowable} doesn't support backpressure as it uses + * time to control the creation of windows. The emitted inner {@code Flowable}s honor + * backpressure but have an unbounded inner buffer that <em>may</em> lead to {@link OutOfMemoryError} + * if left unconsumed.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>You specify which {@link Scheduler} this operator will use.</dd> + * </dl> + * + * @param timespan + * the period of time each window collects items before it should be emitted and replaced with a + * new window + * @param unit + * the unit of time which applies to the {@code timespan} argument + * @param scheduler + * the {@code Scheduler} to use when determining the end and start of a window + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code unit} or {@code scheduler} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/window.html">ReactiveX operators documentation: Window</a> + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.ERROR) + @SchedulerSupport(SchedulerSupport.CUSTOM) + @NonNull + public final Flowable<Flowable<T>> window(long timespan, @NonNull TimeUnit unit, + @NonNull Scheduler scheduler) { + return window(timespan, unit, scheduler, Long.MAX_VALUE, false); + } + + /** + * Returns a {@code Flowable} that emits windows of items it collects from the current {@code Flowable}. The resulting + * {@code Flowable} emits connected, non-overlapping windows, each of a fixed duration specified by the + * {@code timespan} argument or a maximum size specified by the {@code count} argument (whichever is reached + * first). When the current {@code Flowable} completes or encounters an error, the resulting {@code Flowable} emits the + * current window and propagates the notification from the current {@code Flowable}. + * <p> + * <img width="640" height="370" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/window6.s.v3.png" alt=""> + * <p> + * Note that ignoring windows or subscribing later (i.e., on another thread) will result in + * so-called window abandonment where a window may not contain any elements. In this case, subsequent + * elements will be dropped until the condition for the next window boundary is satisfied. The behavior is + * a trade-off for ensuring upstream cancellation can happen under some race conditions. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator consumes the current {@code Flowable} in an unbounded manner. + * The resulting {@code Flowable} doesn't support backpressure as it uses + * time to control the creation of windows. The emitted inner {@code Flowable}s honor + * backpressure and may hold up to {@code count} elements at most.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>You specify which {@link Scheduler} this operator will use.</dd> + * </dl> + * + * @param timespan + * the period of time each window collects items before it should be emitted and replaced with a + * new window + * @param unit + * the unit of time which applies to the {@code timespan} argument + * @param count + * the maximum size of each window before it should be emitted + * @param scheduler + * the {@code Scheduler} to use when determining the end and start of a window + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code unit} or {@code scheduler} is {@code null} + * @throws IllegalArgumentException if {@code count} is non-positive + * @see <a href="http://reactivex.io/documentation/operators/window.html">ReactiveX operators documentation: Window</a> + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.ERROR) + @SchedulerSupport(SchedulerSupport.CUSTOM) + @NonNull + public final Flowable<Flowable<T>> window(long timespan, @NonNull TimeUnit unit, + @NonNull Scheduler scheduler, long count) { + return window(timespan, unit, scheduler, count, false); + } + + /** + * Returns a {@code Flowable} that emits windows of items it collects from the current {@code Flowable}. The resulting + * {@code Flowable} emits connected, non-overlapping windows, each of a fixed duration specified by the + * {@code timespan} argument or a maximum size specified by the {@code count} argument (whichever is reached + * first). When the current {@code Flowable} completes or encounters an error, the resulting {@code Flowable} emits the + * current window and propagates the notification from the current {@code Flowable}. + * <p> + * <img width="640" height="370" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/window6.s.v3.png" alt=""> + * <p> + * Note that ignoring windows or subscribing later (i.e., on another thread) will result in + * so-called window abandonment where a window may not contain any elements. In this case, subsequent + * elements will be dropped until the condition for the next window boundary is satisfied. The behavior is + * a trade-off for ensuring upstream cancellation can happen under some race conditions. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator consumes the current {@code Flowable} in an unbounded manner. + * The resulting {@code Flowable} doesn't support backpressure as it uses + * time to control the creation of windows. The emitted inner {@code Flowable}s honor + * backpressure and may hold up to {@code count} elements at most.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>You specify which {@link Scheduler} this operator will use.</dd> + * </dl> + * + * @param timespan + * the period of time each window collects items before it should be emitted and replaced with a + * new window + * @param unit + * the unit of time which applies to the {@code timespan} argument + * @param count + * the maximum size of each window before it should be emitted + * @param scheduler + * the {@code Scheduler} to use when determining the end and start of a window + * @param restart + * if {@code true}, when a window reaches the capacity limit, the timer is restarted as well + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code unit} or {@code scheduler} is {@code null} + * @throws IllegalArgumentException if {@code count} is non-positive + * @see <a href="http://reactivex.io/documentation/operators/window.html">ReactiveX operators documentation: Window</a> + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.ERROR) + @SchedulerSupport(SchedulerSupport.CUSTOM) + @NonNull + public final Flowable<Flowable<T>> window(long timespan, @NonNull TimeUnit unit, + @NonNull Scheduler scheduler, long count, boolean restart) { + return window(timespan, unit, scheduler, count, restart, bufferSize()); + } + + /** + * Returns a {@code Flowable} that emits windows of items it collects from the current {@code Flowable}. The resulting + * {@code Flowable} emits connected, non-overlapping windows, each of a fixed duration specified by the + * {@code timespan} argument or a maximum size specified by the {@code count} argument (whichever is reached + * first). When the current {@code Flowable} completes or encounters an error, the resulting {@code Flowable} emits the + * current window and propagates the notification from the current {@code Flowable}. + * <p> + * <img width="640" height="370" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/window6.s.v3.png" alt=""> + * <p> + * Note that ignoring windows or subscribing later (i.e., on another thread) will result in + * so-called window abandonment where a window may not contain any elements. In this case, subsequent + * elements will be dropped until the condition for the next window boundary is satisfied. The behavior is + * a trade-off for ensuring upstream cancellation can happen under some race conditions. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator consumes the current {@code Flowable} in an unbounded manner. + * The resulting {@code Flowable} doesn't support backpressure as it uses + * time to control the creation of windows. The emitted inner {@code Flowable}s honor + * backpressure and may hold up to {@code count} elements at most.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>You specify which {@link Scheduler} this operator will use.</dd> + * </dl> + * + * @param timespan + * the period of time each window collects items before it should be emitted and replaced with a + * new window + * @param unit + * the unit of time which applies to the {@code timespan} argument + * @param count + * the maximum size of each window before it should be emitted + * @param scheduler + * the {@code Scheduler} to use when determining the end and start of a window + * @param restart + * if {@code true}, when a window reaches the capacity limit, the timer is restarted as well + * @param bufferSize + * the capacity hint for the buffer in the inner windows + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code unit} or {@code scheduler} is {@code null} + * @throws IllegalArgumentException if {@code count}, {@code timespan} or {@code bufferSize} is non-positive + * @see <a href="http://reactivex.io/documentation/operators/window.html">ReactiveX operators documentation: Window</a> + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.ERROR) + @SchedulerSupport(SchedulerSupport.CUSTOM) + public final Flowable<Flowable<T>> window( + long timespan, @NonNull TimeUnit unit, @NonNull Scheduler scheduler, + long count, boolean restart, int bufferSize) { + ObjectHelper.verifyPositive(bufferSize, "bufferSize"); + Objects.requireNonNull(scheduler, "scheduler is null"); + Objects.requireNonNull(unit, "unit is null"); + ObjectHelper.verifyPositive(count, "count"); + return RxJavaPlugins.onAssembly(new FlowableWindowTimed<>(this, timespan, timespan, unit, scheduler, count, bufferSize, restart)); + } + + /** + * Returns a {@code Flowable} that emits non-overlapping windows of items it collects from the current {@code Flowable} + * where the boundary of each window is determined by the items emitted from a specified boundary-governing + * {@link Publisher}. + * <p> + * <img width="640" height="475" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/window8.v3.png" alt=""> + * <p> + * Note that ignoring windows or subscribing later (i.e., on another thread) will result in + * so-called window abandonment where a window may not contain any elements. In this case, subsequent + * elements will be dropped until the condition for the next window boundary is satisfied. The behavior is + * a trade-off for ensuring upstream cancellation can happen under some race conditions. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The outer {@code Publisher} of this operator does not support backpressure as it uses a {@code boundary} {@code Publisher} to control data + * flow. The inner {@code Publisher}s honor backpressure and buffer everything until the boundary signals the next element.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>This version of {@code window} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <B> + * the window element type (ignored) + * @param boundaryIndicator + * a {@code Publisher} whose emitted items close and open windows + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code boundaryIndicator} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/window.html">ReactiveX operators documentation: Window</a> + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.ERROR) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull B> Flowable<Flowable<T>> window(@NonNull Publisher<B> boundaryIndicator) { + return window(boundaryIndicator, bufferSize()); + } + + /** + * Returns a {@code Flowable} that emits non-overlapping windows of items it collects from the current {@code Flowable} + * where the boundary of each window is determined by the items emitted from a specified boundary-governing + * {@link Publisher}. + * <p> + * <img width="640" height="475" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/window8.v3.png" alt=""> + * <p> + * Note that ignoring windows or subscribing later (i.e., on another thread) will result in + * so-called window abandonment where a window may not contain any elements. In this case, subsequent + * elements will be dropped until the condition for the next window boundary is satisfied. The behavior is + * a trade-off for ensuring upstream cancellation can happen under some race conditions. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The outer {@code Publisher} of this operator does not support backpressure as it uses a {@code boundary} {@code Publisher} to control data + * flow. The inner {@code Publisher}s honor backpressure and buffer everything until the boundary signals the next element.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>This version of {@code window} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <B> + * the window element type (ignored) + * @param boundaryIndicator + * a {@code Publisher} whose emitted items close and open windows + * @param bufferSize + * the capacity hint for the buffer in the inner windows + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code boundaryIndicator} is {@code null} + * @throws IllegalArgumentException if {@code bufferSize} is non-positive + * @see <a href="http://reactivex.io/documentation/operators/window.html">ReactiveX operators documentation: Window</a> + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.ERROR) + @SchedulerSupport(SchedulerSupport.NONE) + public final <@NonNull B> Flowable<Flowable<T>> window(@NonNull Publisher<B> boundaryIndicator, int bufferSize) { + Objects.requireNonNull(boundaryIndicator, "boundaryIndicator is null"); + ObjectHelper.verifyPositive(bufferSize, "bufferSize"); + return RxJavaPlugins.onAssembly(new FlowableWindowBoundary<>(this, boundaryIndicator, bufferSize)); + } + + /** + * Returns a {@code Flowable} that emits windows of items it collects from the current {@code Flowable}. The resulting + * {@code Flowable} emits windows that contain those items emitted by the current {@code Flowable} between the time when + * the {@code windowOpenings} {@link Publisher} emits an item and when the {@code Publisher} returned by + * {@code closingSelector} emits an item. + * <p> + * <img width="640" height="550" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/window2.v3.png" alt=""> + * <p> + * Note that ignoring windows or subscribing later (i.e., on another thread) will result in + * so-called window abandonment where a window may not contain any elements. In this case, subsequent + * elements will be dropped until the condition for the next window boundary is satisfied. The behavior is + * a trade-off for ensuring upstream cancellation can happen under some race conditions. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The outer {@code Publisher} of this operator doesn't support backpressure because the emission of new + * inner {@code Publisher}s are controlled by the {@code windowOpenings} {@code Publisher}. + * The inner {@code Publisher}s honor backpressure and buffer everything until the associated closing + * {@code Publisher} signals or completes.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>This version of {@code window} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <U> the element type of the window-opening {@code Publisher} + * @param <V> the element type of the window-closing {@code Publisher}s + * @param openingIndicator + * a {@code Publisher} that, when it emits an item, causes another window to be created + * @param closingIndicator + * a {@link Function} that produces a {@code Publisher} for every window created. When this {@code Publisher} + * emits an item, the associated window is closed and emitted + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code openingIndicator} or {@code closingIndicator} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/window.html">ReactiveX operators documentation: Window</a> + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.ERROR) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull U, @NonNull V> Flowable<Flowable<T>> window( + @NonNull Publisher<U> openingIndicator, + @NonNull Function<? super U, @NonNull ? extends Publisher<V>> closingIndicator) { + return window(openingIndicator, closingIndicator, bufferSize()); + } + + /** + * Returns a {@code Flowable} that emits windows of items it collects from the current {@code Flowable}. The resulting + * {@code Flowable} emits windows that contain those items emitted by the current {@code Flowable} between the time when + * the {@code windowOpenings} {@link Publisher} emits an item and when the {@code Publisher} returned by + * {@code closingSelector} emits an item. + * <p> + * <img width="640" height="550" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/window2.v3.png" alt=""> + * <p> + * Note that ignoring windows or subscribing later (i.e., on another thread) will result in + * so-called window abandonment where a window may not contain any elements. In this case, subsequent + * elements will be dropped until the condition for the next window boundary is satisfied. The behavior is + * a trade-off for ensuring upstream cancellation can happen under some race conditions. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The outer {@code Publisher} of this operator doesn't support backpressure because the emission of new + * inner {@code Publisher}s are controlled by the {@code windowOpenings} {@code Publisher}. + * The inner {@code Publisher}s honor backpressure and buffer everything until the associated closing + * {@code Publisher} signals or completes.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>This version of {@code window} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <U> the element type of the window-opening {@code Publisher} + * @param <V> the element type of the window-closing {@code Publisher}s + * @param openingIndicator + * a {@code Publisher} that, when it emits an item, causes another window to be created + * @param closingIndicator + * a {@link Function} that produces a {@code Publisher} for every window created. When this {@code Publisher} + * emits an item, the associated window is closed and emitted + * @param bufferSize + * the capacity hint for the buffer in the inner windows + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code openingIndicator} or {@code closingIndicator} is {@code null} + * @throws IllegalArgumentException if {@code bufferSize} is non-positive + * @see <a href="http://reactivex.io/documentation/operators/window.html">ReactiveX operators documentation: Window</a> + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.ERROR) + @SchedulerSupport(SchedulerSupport.NONE) + public final <@NonNull U, @NonNull V> Flowable<Flowable<T>> window( + @NonNull Publisher<U> openingIndicator, + @NonNull Function<? super U, @NonNull ? extends Publisher<V>> closingIndicator, int bufferSize) { + Objects.requireNonNull(openingIndicator, "openingIndicator is null"); + Objects.requireNonNull(closingIndicator, "closingIndicator is null"); + ObjectHelper.verifyPositive(bufferSize, "bufferSize"); + return RxJavaPlugins.onAssembly(new FlowableWindowBoundarySelector<>(this, openingIndicator, closingIndicator, bufferSize)); + } + + /** + * Merges the specified {@link Publisher} into the current {@code Flowable} sequence by using the {@code resultSelector} + * function only when the current {@code Flowable} (this instance) emits an item. + * + * <p>Note that this operator doesn't emit anything until the other source has produced at + * least one value. The resulting emission only happens when the current {@code Flowable} emits (and + * not when the other source emits, unlike combineLatest). + * If the other source doesn't produce any value and just completes, the sequence is completed immediately. + * If the upstream completes before the other source has produced at least one value, the sequence completes + * without emission. + * <p> + * <img width="640" height="380" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/withLatestFrom.v3.png" alt=""> + * + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator is a pass-through for backpressure: the backpressure support + * depends on the upstream and downstream's backpressure behavior. The other {@code Publisher} + * is consumed in an unbounded fashion.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>This operator, by default, doesn't run any particular {@link Scheduler}.</dd> + * </dl> + * + * @param <U> the element type of the other {@code Publisher} + * @param <R> the result type of the combination + * @param other + * the other {@code Publisher} + * @param combiner + * the function to call when the current {@code Flowable} emits an item and the other {@code Publisher} has already + * emitted an item, to generate the item to be emitted by the resulting {@code Flowable} + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code other} or {@code combiner} is {@code null} + * @since 2.0 + * @see <a href="http://reactivex.io/documentation/operators/combinelatest.html">ReactiveX operators documentation: CombineLatest</a> + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.PASS_THROUGH) + @SchedulerSupport(SchedulerSupport.NONE) + public final <@NonNull U, @NonNull R> Flowable<R> withLatestFrom(@NonNull Publisher<? extends U> other, + @NonNull BiFunction<? super T, ? super U, ? extends R> combiner) { + Objects.requireNonNull(other, "other is null"); + Objects.requireNonNull(combiner, "combiner is null"); + + return RxJavaPlugins.onAssembly(new FlowableWithLatestFrom<T, U, R>(this, combiner, other)); + } + + /** + * Combines the value emission from the current {@code Flowable} with the latest emissions from the + * other {@link Publisher}s via a function to produce the output item. + * + * <p>Note that this operator doesn't emit anything until all other sources have produced at + * least one value. The resulting emission only happens when the current {@code Flowable} emits (and + * not when any of the other sources emit, unlike combineLatest). + * If a source doesn't produce any value and just completes, the sequence is completed immediately. + * If the upstream completes before all other sources have produced at least one value, the sequence completes + * without emission. + * + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>This operator is a pass-through for backpressure behavior between the current {@code Flowable} + * and the downstream {@link Subscriber}. The other {@code Publisher}s are consumed in an unbounded manner.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>This operator does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T1> the first other source's value type + * @param <T2> the second other source's value type + * @param <R> the result value type + * @param source1 the first other {@code Publisher} + * @param source2 the second other {@code Publisher} + * @param combiner the function called with an array of values from each participating {@code Publisher} + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code source1}, {@code source2} or {@code combiner} is {@code null} + * @since 2.0 + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.PASS_THROUGH) + @SchedulerSupport(SchedulerSupport.NONE) + public final <@NonNull T1, @NonNull T2, @NonNull R> Flowable<R> withLatestFrom(@NonNull Publisher<T1> source1, @NonNull Publisher<T2> source2, + @NonNull Function3<? super T, ? super T1, ? super T2, R> combiner) { + Objects.requireNonNull(source1, "source1 is null"); + Objects.requireNonNull(source2, "source2 is null"); + Objects.requireNonNull(combiner, "combiner is null"); + Function<Object[], R> f = Functions.toFunction(combiner); + return withLatestFrom(new Publisher[] { source1, source2 }, f); + } + + /** + * Combines the value emission from the current {@code Flowable} with the latest emissions from the + * other {@link Publisher}s via a function to produce the output item. + * + * <p>Note that this operator doesn't emit anything until all other sources have produced at + * least one value. The resulting emission only happens when the current {@code Flowable} emits (and + * not when any of the other sources emit, unlike combineLatest). + * If a source doesn't produce any value and just completes, the sequence is completed immediately. + * If the upstream completes before all other sources have produced at least one value, the sequence completes + * without emission. + * + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>This operator is a pass-through for backpressure behavior between the current {@code Flowable} + * and the downstream {@link Subscriber}. The other {@code Publisher}s are consumed in an unbounded manner.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>This operator does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T1> the first other source's value type + * @param <T2> the second other source's value type + * @param <T3> the third other source's value type + * @param <R> the result value type + * @param source1 the first other {@code Publisher} + * @param source2 the second other {@code Publisher} + * @param source3 the third other {@code Publisher} + * @param combiner the function called with an array of values from each participating {@code Publisher} + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code source1}, {@code source2}, {@code source3} or {@code combiner} is {@code null} + * @since 2.0 + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.PASS_THROUGH) + @SchedulerSupport(SchedulerSupport.NONE) + public final <@NonNull T1, @NonNull T2, @NonNull T3, @NonNull R> Flowable<R> withLatestFrom( + @NonNull Publisher<T1> source1, @NonNull Publisher<T2> source2, + @NonNull Publisher<T3> source3, + @NonNull Function4<? super T, ? super T1, ? super T2, ? super T3, R> combiner) { + Objects.requireNonNull(source1, "source1 is null"); + Objects.requireNonNull(source2, "source2 is null"); + Objects.requireNonNull(source3, "source3 is null"); + Objects.requireNonNull(combiner, "combiner is null"); + Function<Object[], R> f = Functions.toFunction(combiner); + return withLatestFrom(new Publisher[] { source1, source2, source3 }, f); + } + + /** + * Combines the value emission from the current {@code Flowable} with the latest emissions from the + * other {@link Publisher}s via a function to produce the output item. + * + * <p>Note that this operator doesn't emit anything until all other sources have produced at + * least one value. The resulting emission only happens when the current {@code Flowable} emits (and + * not when any of the other sources emit, unlike combineLatest). + * If a source doesn't produce any value and just completes, the sequence is completed immediately. + * If the upstream completes before all other sources have produced at least one value, the sequence completes + * without emission. + * + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>This operator is a pass-through for backpressure behavior between the current {@code Flowable} + * and the downstream {@link Subscriber}. The other {@code Publisher}s are consumed in an unbounded manner.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>This operator does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T1> the first other source's value type + * @param <T2> the second other source's value type + * @param <T3> the third other source's value type + * @param <T4> the fourth other source's value type + * @param <R> the result value type + * @param source1 the first other {@code Publisher} + * @param source2 the second other {@code Publisher} + * @param source3 the third other {@code Publisher} + * @param source4 the fourth other {@code Publisher} + * @param combiner the function called with an array of values from each participating {@code Publisher} + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code source1}, {@code source2}, {@code source3}, + * {@code source4} or {@code combiner} is {@code null} + * @since 2.0 + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.PASS_THROUGH) + @SchedulerSupport(SchedulerSupport.NONE) + public final <@NonNull T1, @NonNull T2, @NonNull T3, @NonNull T4, @NonNull R> Flowable<R> withLatestFrom( + @NonNull Publisher<T1> source1, @NonNull Publisher<T2> source2, + @NonNull Publisher<T3> source3, @NonNull Publisher<T4> source4, + @NonNull Function5<? super T, ? super T1, ? super T2, ? super T3, ? super T4, R> combiner) { + Objects.requireNonNull(source1, "source1 is null"); + Objects.requireNonNull(source2, "source2 is null"); + Objects.requireNonNull(source3, "source3 is null"); + Objects.requireNonNull(source4, "source4 is null"); + Objects.requireNonNull(combiner, "combiner is null"); + Function<Object[], R> f = Functions.toFunction(combiner); + return withLatestFrom(new Publisher[] { source1, source2, source3, source4 }, f); + } + + /** + * Combines the value emission from the current {@code Flowable} with the latest emissions from the + * other {@link Publisher}s via a function to produce the output item. + * + * <p>Note that this operator doesn't emit anything until all other sources have produced at + * least one value. The resulting emission only happens when the current {@code Flowable} emits (and + * not when any of the other sources emit, unlike combineLatest). + * If a source doesn't produce any value and just completes, the sequence is completed immediately. + * If the upstream completes before all other sources have produced at least one value, the sequence completes + * without emission. + * + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>This operator is a pass-through for backpressure behavior between the current {@code Flowable} + * and the downstream {@link Subscriber}. The other {@code Publisher}s are consumed in an unbounded manner.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>This operator does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <R> the result value type + * @param others the array of other sources + * @param combiner the function called with an array of values from each participating {@code Publisher} + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code others} or {@code combiner} is {@code null} + * @since 2.0 + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.PASS_THROUGH) + @SchedulerSupport(SchedulerSupport.NONE) + public final <@NonNull R> Flowable<R> withLatestFrom(@NonNull Publisher<@NonNull ?>[] others, @NonNull Function<? super Object[], R> combiner) { + Objects.requireNonNull(others, "others is null"); + Objects.requireNonNull(combiner, "combiner is null"); + return RxJavaPlugins.onAssembly(new FlowableWithLatestFromMany<>(this, others, combiner)); + } + + /** + * Combines the value emission from the current {@code Flowable} with the latest emissions from the + * other {@link Publisher}s via a function to produce the output item. + * + * <p>Note that this operator doesn't emit anything until all other sources have produced at + * least one value. The resulting emission only happens when the current {@code Flowable} emits (and + * not when any of the other sources emit, unlike combineLatest). + * If a source doesn't produce any value and just completes, the sequence is completed immediately. + * If the upstream completes before all other sources have produced at least one value, the sequence completes + * without emission. + * + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>This operator is a pass-through for backpressure behavior between the current {@code Flowable} + * and the downstream {@link Subscriber}. The other {@code Publisher}s are consumed in an unbounded manner.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>This operator does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <R> the result value type + * @param others the iterable of other sources + * @param combiner the function called with an array of values from each participating {@code Publisher} + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code others} or {@code combiner} is {@code null} + * @since 2.0 + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.PASS_THROUGH) + @SchedulerSupport(SchedulerSupport.NONE) + public final <@NonNull R> Flowable<R> withLatestFrom(@NonNull Iterable<@NonNull ? extends Publisher<@NonNull ?>> others, @NonNull Function<? super Object[], R> combiner) { + Objects.requireNonNull(others, "others is null"); + Objects.requireNonNull(combiner, "combiner is null"); + return RxJavaPlugins.onAssembly(new FlowableWithLatestFromMany<>(this, others, combiner)); + } + + /** + * Returns a {@code Flowable} that emits items that are the result of applying a specified function to pairs of + * values, one each from the current {@code Flowable} and a specified {@link Iterable} sequence. + * <p> + * <img width="640" height="380" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/zip.i.v3.png" alt=""> + * <p> + * Note that the {@code other} {@code Iterable} is evaluated as items are observed from the current {@code Flowable}; it is + * not pre-consumed. This allows you to zip infinite streams on either side. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator expects backpressure from the sources and honors backpressure from the downstream. + * (I.e., zipping with {@link #interval(long, TimeUnit)} may result in {@link MissingBackpressureException}, use + * one of the {@code onBackpressureX} to handle similar, backpressure-ignoring sources.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code zipWith} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <U> + * the type of items in the {@code other} {@code Iterable} + * @param <R> + * the type of items emitted by the resulting {@code Flowable} + * @param other + * the {@code Iterable} sequence + * @param zipper + * a function that combines the pairs of items from the current {@code Flowable} and the {@code Iterable} to generate + * the items to be emitted by the resulting {@code Flowable} + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code other} or {@code zipper} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/zip.html">ReactiveX operators documentation: Zip</a> + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + public final <@NonNull U, @NonNull R> Flowable<R> zipWith(@NonNull Iterable<U> other, @NonNull BiFunction<? super T, ? super U, ? extends R> zipper) { + Objects.requireNonNull(other, "other is null"); + Objects.requireNonNull(zipper, "zipper is null"); + return RxJavaPlugins.onAssembly(new FlowableZipIterable<>(this, other, zipper)); + } + + /** + * Returns a {@code Flowable} that emits items that are the result of applying a specified function to pairs of + * values, one each from the current {@code Flowable} and another specified {@link Publisher}. + * <p> + * The operator subscribes to its sources in the order they are specified and completes eagerly if + * one of the sources is shorter than the rest while canceling the other sources. Therefore, it + * is possible those other sources will never be able to run to completion (and thus not calling + * {@code doOnComplete()}). This can also happen if the sources are exactly the same length; if + * source A completes and B has been consumed and is about to complete, the operator detects A won't + * be sending further values and it will cancel B immediately. For example: + * <pre><code>range(1, 5).doOnComplete(action1).zipWith(range(6, 5).doOnComplete(action2), (a, b) -> a + b)</code></pre> + * {@code action1} will be called but {@code action2} won't. + * <br>To work around this termination property, + * use {@link #doOnCancel(Action)} as well or use {@code using()} to do cleanup in case of completion + * or cancellation. + * <p> + * <img width="640" height="380" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/zip.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator expects backpressure from the sources and honors backpressure from the downstream. + * (I.e., zipping with {@link #interval(long, TimeUnit)} may result in {@link MissingBackpressureException}, use + * one of the {@code onBackpressureX} to handle similar, backpressure-ignoring sources.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code zipWith} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <U> + * the type of items emitted by the {@code other} {@code Publisher} + * @param <R> + * the type of items emitted by the resulting {@code Flowable} + * @param other + * the other {@code Publisher} + * @param zipper + * a function that combines the pairs of items from the two {@code Publisher}s to generate the items to + * be emitted by the resulting {@code Flowable} + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code other} or {@code zipper} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/zip.html">ReactiveX operators documentation: Zip</a> + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + public final <@NonNull U, @NonNull R> Flowable<R> zipWith(@NonNull Publisher<? extends U> other, @NonNull BiFunction<? super T, ? super U, ? extends R> zipper) { + Objects.requireNonNull(other, "other is null"); + return zip(this, other, zipper); + } + + /** + * Returns a {@code Flowable} that emits items that are the result of applying a specified function to pairs of + * values, one each from the current {@code Flowable} and another specified {@link Publisher}. + * <p> + * The operator subscribes to its sources in the order they are specified and completes eagerly if + * one of the sources is shorter than the rest while canceling the other sources. Therefore, it + * is possible those other sources will never be able to run to completion (and thus not calling + * {@code doOnComplete()}). This can also happen if the sources are exactly the same length; if + * source A completes and B has been consumed and is about to complete, the operator detects A won't + * be sending further values and it will cancel B immediately. For example: + * <pre><code>range(1, 5).doOnComplete(action1).zipWith(range(6, 5).doOnComplete(action2), (a, b) -> a + b)</code></pre> + * {@code action1} will be called but {@code action2} won't. + * <br>To work around this termination property, + * use {@link #doOnCancel(Action)} as well or use {@code using()} to do cleanup in case of completion + * or cancellation. + * <p> + * <img width="640" height="380" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/zip.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator expects backpressure from the sources and honors backpressure from the downstream. + * (I.e., zipping with {@link #interval(long, TimeUnit)} may result in {@link MissingBackpressureException}, use + * one of the {@code onBackpressureX} to handle similar, backpressure-ignoring sources.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code zipWith} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <U> + * the type of items emitted by the {@code other} {@code Publisher} + * @param <R> + * the type of items emitted by the resulting {@code Flowable} + * @param other + * the other {@code Publisher} + * @param zipper + * a function that combines the pairs of items from the two {@code Publisher}s to generate the items to + * be emitted by the resulting {@code Flowable} + * @param delayError + * if {@code true}, errors from the current {@code Flowable} or the other {@code Publisher} is delayed until both terminate + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code other} or {@code zipper} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/zip.html">ReactiveX operators documentation: Zip</a> + * @since 2.0 + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull U, @NonNull R> Flowable<R> zipWith(@NonNull Publisher<? extends U> other, + @NonNull BiFunction<? super T, ? super U, ? extends R> zipper, boolean delayError) { + return zip(this, other, zipper, delayError); + } + + /** + * Returns a {@code Flowable} that emits items that are the result of applying a specified function to pairs of + * values, one each from the current {@code Flowable} and another specified {@link Publisher}. + * <p> + * The operator subscribes to its sources in the order they are specified and completes eagerly if + * one of the sources is shorter than the rest while canceling the other sources. Therefore, it + * is possible those other sources will never be able to run to completion (and thus not calling + * {@code doOnComplete()}). This can also happen if the sources are exactly the same length; if + * source A completes and B has been consumed and is about to complete, the operator detects A won't + * be sending further values and it will cancel B immediately. For example: + * <pre><code>range(1, 5).doOnComplete(action1).zipWith(range(6, 5).doOnComplete(action2), (a, b) -> a + b)</code></pre> + * {@code action1} will be called but {@code action2} won't. + * <br>To work around this termination property, + * use {@link #doOnCancel(Action)} as well or use {@code using()} to do cleanup in case of completion + * or cancellation. + * <p> + * <img width="640" height="380" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/zip.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator expects backpressure from the sources and honors backpressure from the downstream. + * (I.e., zipping with {@link #interval(long, TimeUnit)} may result in {@link MissingBackpressureException}, use + * one of the {@code onBackpressureX} to handle similar, backpressure-ignoring sources.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code zipWith} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <U> + * the type of items emitted by the {@code other} {@code Publisher} + * @param <R> + * the type of items emitted by the resulting {@code Flowable} + * @param other + * the other {@code Publisher} + * @param zipper + * a function that combines the pairs of items from the two {@code Publisher}s to generate the items to + * be emitted by the resulting {@code Flowable} + * @param bufferSize + * the capacity hint for the buffer in the inner windows + * @param delayError + * if {@code true}, errors from the current {@code Flowable} or the other {@code Publisher} is delayed until both terminate + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code other} or {@code zipper} is {@code null} + * @throws IllegalArgumentException if {@code bufferSize} is non-positive + * @see <a href="http://reactivex.io/documentation/operators/zip.html">ReactiveX operators documentation: Zip</a> + * @since 2.0 + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull U, @NonNull R> Flowable<R> zipWith(@NonNull Publisher<? extends U> other, + @NonNull BiFunction<? super T, ? super U, ? extends R> zipper, boolean delayError, int bufferSize) { + return zip(this, other, zipper, delayError, bufferSize); + } + + // ------------------------------------------------------------------------- + // Fluent test support, super handy and reduces test preparation boilerplate + // ------------------------------------------------------------------------- + /** + * Creates a {@link TestSubscriber} that requests {@link Long#MAX_VALUE} and subscribes + * it to this {@code Flowable}. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The returned {@code TestSubscriber} consumes this {@code Flowable} in an unbounded fashion.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code test} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @return the new {@code TestSubscriber} instance + * @since 2.0 + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.UNBOUNDED_IN) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final TestSubscriber<T> test() { // NoPMD + TestSubscriber<T> ts = new TestSubscriber<>(); + subscribe(ts); + return ts; + } + + /** + * Creates a {@link TestSubscriber} with the given initial request amount and subscribes + * it to this {@code Flowable}. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The returned {@code TestSubscriber} requests the given {@code initialRequest} amount upfront.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code test} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param initialRequest the initial request amount, positive + * @return the new {@code TestSubscriber} instance + * @since 2.0 + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final TestSubscriber<T> test(long initialRequest) { // NoPMD + TestSubscriber<T> ts = new TestSubscriber<>(initialRequest); + subscribe(ts); + return ts; + } + + /** + * Creates a {@link TestSubscriber} with the given initial request amount, + * optionally cancels it before the subscription and subscribes + * it to this {@code Flowable}. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The returned {@code TestSubscriber} requests the given {@code initialRequest} amount upfront.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code test} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param initialRequest the initial request amount, positive + * @param cancel should the {@code TestSubscriber} be canceled before the subscription? + * @return the new {@code TestSubscriber} instance + * @since 2.0 + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final TestSubscriber<T> test(long initialRequest, boolean cancel) { // NoPMD + TestSubscriber<T> ts = new TestSubscriber<>(initialRequest); + if (cancel) { + ts.cancel(); + } + subscribe(ts); + return ts; + } + + // ------------------------------------------------------------------------- + // JDK 8 Support + // ------------------------------------------------------------------------- + + /** + * Converts the existing value of the provided optional into a {@link #just(Object)} + * or an empty optional into an {@link #empty()} {@code Flowable} instance. + * <p> + * <img width="640" height="335" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/fromOptional.f.png" alt=""> + * <p> + * Note that the operator takes an already instantiated optional reference and does not + * by any means create this original optional. If the optional is to be created per + * consumer upon subscription, use {@link #defer(Supplier)} around {@code fromOptional}: + * <pre><code> + * Flowable.defer(() -> Flowable.fromOptional(createOptional())); + * </code></pre> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The returned {@code Flowable} supports backpressure.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code fromOptional} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <T> the element type of the optional value + * @param optional the optional value to convert into a {@code Flowable} + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code optional} is {@code null} + * @since 3.0.0 + * @see #just(Object) + * @see #empty() + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public static <@NonNull T> Flowable<@NonNull T> fromOptional(@NonNull Optional<T> optional) { + Objects.requireNonNull(optional, "optional is null"); + return optional.map(Flowable::just).orElseGet(Flowable::empty); + } + + /** + * Signals the completion value or error of the given (hot) {@link CompletionStage}-based asynchronous calculation. + * <p> + * <img width="640" height="262" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/fromCompletionStage.f.png" alt=""> + * <p> + * Note that the operator takes an already instantiated, running or terminated {@code CompletionStage}. + * If the {@code CompletionStage} is to be created per consumer upon subscription, use {@link #defer(Supplier)} + * around {@code fromCompletionStage}: + * <pre><code> + * Flowable.defer(() -> Flowable.fromCompletionStage(createCompletionStage())); + * </code></pre> + * <p> + * If the {@code CompletionStage} completes with {@code null}, a {@link NullPointerException} is signaled. + * <p> + * Canceling the flow can't cancel the execution of the {@code CompletionStage} because {@code CompletionStage} + * itself doesn't support cancellation. Instead, the operator detaches from the {@code CompletionStage}. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The returned {@code Flowable} supports backpressure and caches the completion value until the + * downstream is ready to receive it.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code fromCompletionStage} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <T> the element type of the {@code CompletionStage} + * @param stage the {@code CompletionStage} to convert to {@code Flowable} and signal its terminal value or error + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code stage} is {@code null} + * @since 3.0.0 + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public static <@NonNull T> Flowable<@NonNull T> fromCompletionStage(@NonNull CompletionStage<T> stage) { + Objects.requireNonNull(stage, "stage is null"); + return RxJavaPlugins.onAssembly(new FlowableFromCompletionStage<>(stage)); + } + + /** + * Converts a {@link Stream} into a finite {@code Flowable} and emits its items in the sequence. + * <p> + * <img width="640" height="347" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/fromStream.f.png" alt=""> + * <p> + * The operator closes the {@code Stream} upon cancellation and when it terminates. Any exceptions raised when + * closing a {@code Stream} are routed to the global error handler ({@link RxJavaPlugins#onError(Throwable)}. + * If a {@code Stream} should not be closed, turn it into an {@link Iterable} and use {@link #fromIterable(Iterable)}: + * <pre><code> + * Stream<T> stream = ... + * Flowable.fromIterable(stream::iterator); + * </code></pre> + * <p> + * Note that {@code Stream}s can be consumed only once; any subsequent attempt to consume a {@code Stream} + * will result in an {@link IllegalStateException}. + * <p> + * Primitive streams are not supported and items have to be boxed manually (e.g., via {@link IntStream#boxed()}): + * <pre><code> + * IntStream intStream = IntStream.rangeClosed(1, 10); + * Flowable.fromStream(intStream.boxed()); + * </code></pre> + * <p> + * {@code Stream} does not support concurrent usage so creating and/or consuming the same instance multiple times + * from multiple threads can lead to undefined behavior. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream and iterates the given {@code Stream} + * on demand (i.e., when requested).</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code fromStream} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <T> the element type of the source {@code Stream} + * @param stream the {@code Stream} of values to emit + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code stream} is {@code null} + * @since 3.0.0 + * @see #fromIterable(Iterable) + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public static <@NonNull T> Flowable<@NonNull T> fromStream(@NonNull Stream<T> stream) { + Objects.requireNonNull(stream, "stream is null"); + return RxJavaPlugins.onAssembly(new FlowableFromStream<>(stream)); + } + + /** + * Maps each upstream value into an {@link Optional} and emits the contained item if not empty. + * <p> + * <img width="640" height="308" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/mapOptional.f.png" alt=""> + * + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator is a pass-through for downstream requests but issues {@code request(1)} whenever the + * mapped {@code Optional} is empty.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code mapOptional} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <R> the non-{@code null} output type + * @param mapper the function that receives the upstream item and should return a <em>non-empty</em> {@code Optional} + * to emit as the output or an <em>empty</em> {@code Optional} to skip to the next upstream value + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code mapper} is {@code null} + * @since 3.0.0 + * @see #map(Function) + * @see #filter(Predicate) + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull R> Flowable<R> mapOptional(@NonNull Function<? super T, @NonNull Optional<? extends R>> mapper) { + Objects.requireNonNull(mapper, "mapper is null"); + return RxJavaPlugins.onAssembly(new FlowableMapOptional<>(this, mapper)); + } + + /** + * Collects the finite upstream's values into a container via a {@link Stream} {@link Collector} callback set and emits + * it as the success result. + * <p> + * <img width="640" height="360" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/collector.f.png" alt=""> + * + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator consumes the upstream in an unbounded manner.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code collect} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <R> the non-{@code null} result type + * @param <A> the intermediate container type used for the accumulation + * @param collector the interface defining the container supplier, accumulator and finisher functions; + * see {@link Collectors} for some standard implementations + * @return the new {@link Single} instance + * @throws NullPointerException if {@code collector} is {@code null} + * @since 3.0.0 + * @see Collectors + * @see #collect(Supplier, BiConsumer) + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.UNBOUNDED_IN) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull R, @Nullable A> Single<R> collect(@NonNull Collector<? super T, A, R> collector) { + Objects.requireNonNull(collector, "collector is null"); + return RxJavaPlugins.onAssembly(new FlowableCollectWithCollectorSingle<>(this, collector)); + } + + /** + * Signals the first upstream item (or the default item if the upstream is empty) via + * a {@link CompletionStage}. + * <p> + * <img width="640" height="315" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/firstStage.f.png" alt=""> + * <p> + * The upstream can be canceled by converting the resulting {@code CompletionStage} into + * {@link CompletableFuture} via {@link CompletionStage#toCompletableFuture()} and + * calling {@link CompletableFuture#cancel(boolean)} on it. + * The upstream will be also cancelled if the resulting {@code CompletionStage} is converted to and + * completed manually by {@link CompletableFuture#complete(Object)} or {@link CompletableFuture#completeExceptionally(Throwable)}. + * <p> + * {@code CompletionStage}s don't have a notion of emptiness and allow {@code null}s, therefore, one can either use + * a {@code defaultItem} of {@code null} or turn the flow into a sequence of {@link Optional}s and default to {@link Optional#empty()}: + * <pre><code> + * CompletionStage<Optional<T>> stage = source.map(Optional::of).firstStage(Optional.empty()); + * </code></pre> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator requests one item from upstream and then when received, cancels the upstream.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code firstStage} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param defaultItem the item to signal if the upstream is empty + * @return the new {@code CompletionStage} instance + * @since 3.0.0 + * @see #firstOrErrorStage() + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final CompletionStage<T> firstStage(@Nullable T defaultItem) { + return subscribeWith(new FlowableFirstStageSubscriber<>(true, defaultItem)); + } + + /** + * Signals the only expected upstream item (or the default item if the upstream is empty) + * or signals {@link IllegalArgumentException} if the upstream has more than one item + * via a {@link CompletionStage}. + * <p> + * <img width="640" height="229" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/singleStage.f.png" alt=""> + * <p> + * The upstream can be canceled by converting the resulting {@code CompletionStage} into + * {@link CompletableFuture} via {@link CompletionStage#toCompletableFuture()} and + * calling {@link CompletableFuture#cancel(boolean)} on it. + * The upstream will be also cancelled if the resulting {@code CompletionStage} is converted to and + * completed manually by {@link CompletableFuture#complete(Object)} or {@link CompletableFuture#completeExceptionally(Throwable)}. + * <p> + * {@code CompletionStage}s don't have a notion of emptiness and allow {@code null}s, therefore, one can either use + * a {@code defaultItem} of {@code null} or turn the flow into a sequence of {@link Optional}s and default to {@link Optional#empty()}: + * <pre><code> + * CompletionStage<Optional<T>> stage = source.map(Optional::of).singleStage(Optional.empty()); + * </code></pre> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator requests two items from upstream and then when more than one item is received, cancels the upstream.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code singleStage} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param defaultItem the item to signal if the upstream is empty + * @return the new {@code CompletionStage} instance + * @since 3.0.0 + * @see #singleOrErrorStage() + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final CompletionStage<T> singleStage(@Nullable T defaultItem) { + return subscribeWith(new FlowableSingleStageSubscriber<>(true, defaultItem)); + } + + /** + * Signals the last upstream item (or the default item if the upstream is empty) via + * a {@link CompletionStage}. + * <p> + * <img width="640" height="315" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/lastStage.f.png" alt=""> + * <p> + * The upstream can be canceled by converting the resulting {@code CompletionStage} into + * {@link CompletableFuture} via {@link CompletionStage#toCompletableFuture()} and + * calling {@link CompletableFuture#cancel(boolean)} on it. + * The upstream will be also cancelled if the resulting {@code CompletionStage} is converted to and + * completed manually by {@link CompletableFuture#complete(Object)} or {@link CompletableFuture#completeExceptionally(Throwable)}. + * <p> + * {@code CompletionStage}s don't have a notion of emptiness and allow {@code null}s, therefore, one can either use + * a {@code defaultItem} of {@code null} or turn the flow into a sequence of {@link Optional}s and default to {@link Optional#empty()}: + * <pre><code> + * CompletionStage<Optional<T>> stage = source.map(Optional::of).lastStage(Optional.empty()); + * </code></pre> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator requests an unbounded number of items from the upstream.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code lastStage} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param defaultItem the item to signal if the upstream is empty + * @return the new {@code CompletionStage} instance + * @since 3.0.0 + * @see #lastOrErrorStage() + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.UNBOUNDED_IN) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final CompletionStage<T> lastStage(@Nullable T defaultItem) { + return subscribeWith(new FlowableLastStageSubscriber<>(true, defaultItem)); + } + + /** + * Signals the first upstream item or a {@link NoSuchElementException} if the upstream is empty via + * a {@link CompletionStage}. + * <p> + * <img width="640" height="343" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/firstOrErrorStage.f.png" alt=""> + * <p> + * The upstream can be canceled by converting the resulting {@code CompletionStage} into + * {@link CompletableFuture} via {@link CompletionStage#toCompletableFuture()} and + * calling {@link CompletableFuture#cancel(boolean)} on it. + * The upstream will be also cancelled if the resulting {@code CompletionStage} is converted to and + * completed manually by {@link CompletableFuture#complete(Object)} or {@link CompletableFuture#completeExceptionally(Throwable)}. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator requests one item from upstream and then when received, cancels the upstream.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code firstOrErrorStage} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @return the new {@code CompletionStage} instance + * @since 3.0.0 + * @see #firstStage(Object) + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final CompletionStage<T> firstOrErrorStage() { + return subscribeWith(new FlowableFirstStageSubscriber<>(false, null)); + } + + /** + * Signals the only expected upstream item, a {@link NoSuchElementException} if the upstream is empty + * or signals {@link IllegalArgumentException} if the upstream has more than one item + * via a {@link CompletionStage}. + * <p> + * <img width="640" height="229" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/singleOrErrorStage.f.png" alt=""> + * <p> + * The upstream can be canceled by converting the resulting {@code CompletionStage} into + * {@link CompletableFuture} via {@link CompletionStage#toCompletableFuture()} and + * calling {@link CompletableFuture#cancel(boolean)} on it. + * The upstream will be also cancelled if the resulting {@code CompletionStage} is converted to and + * completed manually by {@link CompletableFuture#complete(Object)} or {@link CompletableFuture#completeExceptionally(Throwable)}. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator requests two items from upstream and then when more than one item is received, cancels the upstream.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code singleOrErrorStage} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @return the new {@code CompletionStage} instance + * @since 3.0.0 + * @see #singleStage(Object) + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final CompletionStage<T> singleOrErrorStage() { + return subscribeWith(new FlowableSingleStageSubscriber<>(false, null)); + } + + /** + * Signals the last upstream item or a {@link NoSuchElementException} if the upstream is empty via + * a {@link CompletionStage}. + * <p> + * <img width="640" height="346" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/lastOrErrorStage.f.png" alt=""> + * <p> + * The upstream can be canceled by converting the resulting {@code CompletionStage} into + * {@link CompletableFuture} via {@link CompletionStage#toCompletableFuture()} and + * calling {@link CompletableFuture#cancel(boolean)} on it. + * The upstream will be also cancelled if the resulting {@code CompletionStage} is converted to and + * completed manually by {@link CompletableFuture#complete(Object)} or {@link CompletableFuture#completeExceptionally(Throwable)}. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator requests an unbounded number of items from the upstream.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code lastOrErrorStage} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @return the new {@code CompletionStage} instance + * @since 3.0.0 + * @see #lastStage(Object) + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final CompletionStage<T> lastOrErrorStage() { + return subscribeWith(new FlowableLastStageSubscriber<>(false, null)); + } + + /** + * Creates a sequential {@link Stream} to consume or process this {@code Flowable} in a blocking manner via + * the Java {@code Stream} API. + * <p> + * <img width="640" height="446" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/blockingStream.f.png" alt=""> + * <p> + * Cancellation of the upstream is done via {@link Stream#close()}, therefore, it is strongly recommended the + * consumption is performed within a try-with-resources construct: + * <pre><code> + * Flowable<Integer> source = Flowable.range(1, 10) + * .subscribeOn(Schedulers.computation()); + * + * try (Stream<Integer> stream = source.blockingStream()) { + * stream.limit(3).forEach(System.out::println); + * } + * </code></pre> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator requests {@link #bufferSize()} amount upfront and 75% of it after each 75% of the amount received.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code blockingStream} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @return the new {@code Stream} instance + * @since 3.0.0 + * @see #blockingStream(int) + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Stream<T> blockingStream() { + return blockingStream(bufferSize()); + } + + /** + * Creates a sequential {@link Stream} to consume or process this {@code Flowable} in a blocking manner via + * the Java {@code Stream} API. + * <p> + * <img width="640" height="410" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/blockingStream.fi.png" alt=""> + * <p> + * Cancellation of the upstream is done via {@link Stream#close()}, therefore, it is strongly recommended the + * consumption is performed within a try-with-resources construct: + * <pre><code> + * Flowable<Integer> source = Flowable.range(1, 10) + * .subscribeOn(Schedulers.computation()); + * + * try (Stream<Integer> stream = source.blockingStream(4)) { + * stream.limit(3).forEach(System.out::println); + * } + * </code></pre> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator requests the given {@code prefetch} amount upfront and 75% of it after each 75% of the amount received.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code blockingStream} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param prefetch the number of items to request from the upstream to limit the number of + * in-flight items and item generation. + * @return the new {@code Stream} instance + * @throws IllegalArgumentException if {@code prefetch} is non-positive + * @since 3.0.0 + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Stream<T> blockingStream(int prefetch) { + Iterator<T> iterator = blockingIterable(prefetch).iterator(); + return StreamSupport.stream(Spliterators.spliteratorUnknownSize(iterator, 0), false) + .onClose(((Disposable) iterator)::dispose); + } + + /** + * Maps each upstream item into a {@link Stream} and emits the {@code Stream}'s items to the downstream in a sequential fashion. + * <p> + * <img width="640" height="327" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/concatMapStream.f.png" alt=""> + * <p> + * Due to the blocking and sequential nature of Java {@code Stream}s, the streams are mapped and consumed in a sequential fashion + * without interleaving (unlike a more general {@link #flatMap(Function)}). Therefore, {@code flatMapStream} and + * {@code concatMapStream} are identical operators and are provided as aliases. + * <p> + * The operator closes the {@code Stream} upon cancellation and when it terminates. Any exceptions raised when + * closing a {@code Stream} are routed to the global error handler ({@link RxJavaPlugins#onError(Throwable)}. + * If a {@code Stream} should not be closed, turn it into an {@link Iterable} and use {@link #concatMapIterable(Function)}: + * <pre><code> + * source.concatMapIterable(v -> createStream(v)::iterator); + * </code></pre> + * <p> + * Note that {@code Stream}s can be consumed only once; any subsequent attempt to consume a {@code Stream} + * will result in an {@link IllegalStateException}. + * <p> + * Primitive streams are not supported and items have to be boxed manually (e.g., via {@link IntStream#boxed()}): + * <pre><code> + * source.concatMapStream(v -> IntStream.rangeClosed(v + 1, v + 10).boxed()); + * </code></pre> + * <p> + * {@code Stream} does not support concurrent usage so creating and/or consuming the same instance multiple times + * from multiple threads can lead to undefined behavior. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors the downstream backpressure and consumes the inner stream only on demand. The operator + * prefetches {@link #bufferSize} items of the upstream (then 75% of it after the 75% received) + * and caches them until they are ready to be mapped into {@code Stream}s + * after the current {@code Stream} has been consumed.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code concatMapStream} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <R> the element type of the {@code Stream}s and the result + * @param mapper the function that receives an upstream item and should return a {@code Stream} whose elements + * will be emitted to the downstream + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code mapper} is {@code null} + * @since 3.0.0 + * @see #concatMap(Function) + * @see #concatMapIterable(Function) + * @see #concatMapStream(Function, int) + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull R> Flowable<R> concatMapStream(@NonNull Function<? super T, @NonNull ? extends Stream<? extends R>> mapper) { + return flatMapStream(mapper, bufferSize()); + } + + /** + * Maps each upstream item into a {@link Stream} and emits the {@code Stream}'s items to the downstream in a sequential fashion. + * <p> + * <img width="640" height="270" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/concatMapStream.fi.png" alt=""> + * <p> + * Due to the blocking and sequential nature of Java {@code Stream}s, the streams are mapped and consumed in a sequential fashion + * without interleaving (unlike a more general {@link #flatMap(Function)}). Therefore, {@code flatMapStream} and + * {@code concatMapStream} are identical operators and are provided as aliases. + * <p> + * The operator closes the {@code Stream} upon cancellation and when it terminates. Any exceptions raised when + * closing a {@code Stream} are routed to the global error handler ({@link RxJavaPlugins#onError(Throwable)}. + * If a {@code Stream} should not be closed, turn it into an {@link Iterable} and use {@link #concatMapIterable(Function, int)}: + * <pre><code> + * source.concatMapIterable(v -> createStream(v)::iterator, 32); + * </code></pre> + * <p> + * Note that {@code Stream}s can be consumed only once; any subsequent attempt to consume a {@code Stream} + * will result in an {@link IllegalStateException}. + * <p> + * Primitive streams are not supported and items have to be boxed manually (e.g., via {@link IntStream#boxed()}): + * <pre><code> + * source.concatMapStream(v -> IntStream.rangeClosed(v + 1, v + 10).boxed(), 32); + * </code></pre> + * <p> + * {@code Stream} does not support concurrent usage so creating and/or consuming the same instance multiple times + * from multiple threads can lead to undefined behavior. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors the downstream backpressure and consumes the inner stream only on demand. The operator + * prefetches the given amount of upstream items and caches them until they are ready to be mapped into {@code Stream}s + * after the current {@code Stream} has been consumed.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code concatMapStream} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <R> the element type of the {@code Stream}s and the result + * @param mapper the function that receives an upstream item and should return a {@code Stream} whose elements + * will be emitted to the downstream + * @param prefetch the number of upstream items to request upfront, then 75% of this amount after each 75% upstream items received + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code mapper} is {@code null} + * @throws IllegalArgumentException if {@code prefetch} is non-positive + * @since 3.0.0 + * @see #concatMap(Function, int) + * @see #concatMapIterable(Function, int) + * @see #flatMapStream(Function, int) + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull R> Flowable<R> concatMapStream(@NonNull Function<? super T, @NonNull ? extends Stream<? extends R>> mapper, int prefetch) { + Objects.requireNonNull(mapper, "mapper is null"); + ObjectHelper.verifyPositive(prefetch, "prefetch"); + return RxJavaPlugins.onAssembly(new FlowableFlatMapStream<>(this, mapper, prefetch)); + } + + /** + * Maps each upstream item into a {@link Stream} and emits the {@code Stream}'s items to the downstream in a sequential fashion. + * <p> + * <img width="640" height="328" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/flatMapStream.f.png" alt=""> + * <p> + * Due to the blocking and sequential nature of Java {@code Stream}s, the streams are mapped and consumed in a sequential fashion + * without interleaving (unlike a more general {@link #flatMap(Function)}). Therefore, {@code flatMapStream} and + * {@code concatMapStream} are identical operators and are provided as aliases. + * <p> + * The operator closes the {@code Stream} upon cancellation and when it terminates. Any exceptions raised when + * closing a {@code Stream} are routed to the global error handler ({@link RxJavaPlugins#onError(Throwable)}. + * If a {@code Stream} should not be closed, turn it into an {@link Iterable} and use {@link #flatMapIterable(Function)}: + * <pre><code> + * source.flatMapIterable(v -> createStream(v)::iterator); + * </code></pre> + * <p> + * Note that {@code Stream}s can be consumed only once; any subsequent attempt to consume a {@code Stream} + * will result in an {@link IllegalStateException}. + * <p> + * Primitive streams are not supported and items have to be boxed manually (e.g., via {@link IntStream#boxed()}): + * <pre><code> + * source.flatMapStream(v -> IntStream.rangeClosed(v + 1, v + 10).boxed()); + * </code></pre> + * <p> + * {@code Stream} does not support concurrent usage so creating and/or consuming the same instance multiple times + * from multiple threads can lead to undefined behavior. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors the downstream backpressure and consumes the inner stream only on demand. The operator + * prefetches {@link #bufferSize} items of the upstream (then 75% of it after the 75% received) + * and caches them until they are ready to be mapped into {@code Stream}s + * after the current {@code Stream} has been consumed.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code flatMapStream} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <R> the element type of the {@code Stream}s and the result + * @param mapper the function that receives an upstream item and should return a {@code Stream} whose elements + * will be emitted to the downstream + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code mapper} is {@code null} + * @since 3.0.0 + * @see #flatMap(Function) + * @see #flatMapIterable(Function) + * @see #flatMapStream(Function, int) + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull R> Flowable<R> flatMapStream(@NonNull Function<? super T, @NonNull ? extends Stream<? extends R>> mapper) { + return flatMapStream(mapper, bufferSize()); + } + + /** + * Maps each upstream item into a {@link Stream} and emits the {@code Stream}'s items to the downstream in a sequential fashion. + * <p> + * <img width="640" height="270" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/flatMapStream.fi.png" alt=""> + * <p> + * Due to the blocking and sequential nature of Java {@code Stream}s, the streams are mapped and consumed in a sequential fashion + * without interleaving (unlike a more general {@link #flatMap(Function)}). Therefore, {@code flatMapStream} and + * {@code concatMapStream} are identical operators and are provided as aliases. + * <p> + * The operator closes the {@code Stream} upon cancellation and when it terminates. Any exceptions raised when + * closing a {@code Stream} are routed to the global error handler ({@link RxJavaPlugins#onError(Throwable)}. + * If a {@code Stream} should not be closed, turn it into an {@link Iterable} and use {@link #flatMapIterable(Function, int)}: + * <pre><code> + * source.flatMapIterable(v -> createStream(v)::iterator, 32); + * </code></pre> + * <p> + * Note that {@code Stream}s can be consumed only once; any subsequent attempt to consume a {@code Stream} + * will result in an {@link IllegalStateException}. + * <p> + * Primitive streams are not supported and items have to be boxed manually (e.g., via {@link IntStream#boxed()}): + * <pre><code> + * source.flatMapStream(v -> IntStream.rangeClosed(v + 1, v + 10).boxed(), 32); + * </code></pre> + * <p> + * {@code Stream} does not support concurrent usage so creating and/or consuming the same instance multiple times + * from multiple threads can lead to undefined behavior. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors the downstream backpressure and consumes the inner stream only on demand. The operator + * prefetches the given amount of upstream items and caches them until they are ready to be mapped into {@code Stream}s + * after the current {@code Stream} has been consumed.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code flatMapStream} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <R> the element type of the {@code Stream}s and the result + * @param mapper the function that receives an upstream item and should return a {@code Stream} whose elements + * will be emitted to the downstream + * @param prefetch the number of upstream items to request upfront, then 75% of this amount after each 75% upstream items received + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code mapper} is {@code null} + * @throws IllegalArgumentException if {@code prefetch} is non-positive + * @since 3.0.0 + * @see #flatMap(Function, int) + * @see #flatMapIterable(Function, int) + * @see #concatMapStream(Function, int) + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull R> Flowable<R> flatMapStream(@NonNull Function<? super T, @NonNull ? extends Stream<? extends R>> mapper, int prefetch) { + Objects.requireNonNull(mapper, "mapper is null"); + ObjectHelper.verifyPositive(prefetch, "prefetch"); + return RxJavaPlugins.onAssembly(new FlowableFlatMapStream<>(this, mapper, prefetch)); + } +} diff --git a/src/main/java/io/reactivex/rxjava3/core/FlowableConverter.java b/src/main/java/io/reactivex/rxjava3/core/FlowableConverter.java new file mode 100644 index 0000000000..1db19b9144 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/core/FlowableConverter.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.core; + +import io.reactivex.rxjava3.annotations.NonNull; + +/** + * Convenience interface and callback used by the {@link Flowable#to} operator to turn a {@link Flowable} into another + * value fluently. + * <p>History: 2.1.7 - experimental + * @param <T> the upstream type + * @param <R> the output type + * @since 2.2 + */ +@FunctionalInterface +public interface FlowableConverter<@NonNull T, @NonNull R> { + /** + * Applies a function to the upstream {@link Flowable} and returns a converted value of type {@code R}. + * + * @param upstream the upstream {@code Flowable} instance + * @return the converted value + */ + R apply(@NonNull Flowable<T> upstream); +} diff --git a/src/main/java/io/reactivex/rxjava3/core/FlowableEmitter.java b/src/main/java/io/reactivex/rxjava3/core/FlowableEmitter.java new file mode 100644 index 0000000000..5ad6850389 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/core/FlowableEmitter.java @@ -0,0 +1,106 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.core; + +import io.reactivex.rxjava3.annotations.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.functions.*; + +/** + * Abstraction over a Reactive Streams {@link org.reactivestreams.Subscriber} that allows associating + * a resource with it and exposes the current number of downstream + * requested amount. + * <p> + * The {@link #onNext(Object)}, {@link #onError(Throwable)}, {@link #tryOnError(Throwable)} + * and {@link #onComplete()} methods should be called in a sequential manner, just like + * the {@link org.reactivestreams.Subscriber Subscriber}'s methods. + * Use the {@code FlowableEmitter} the {@link #serialize()} method returns instead of the original + * {@code FlowableEmitter} instance provided by the generator routine if you want to ensure this. + * The other methods are thread-safe. + * <p> + * The emitter allows the registration of a single resource, in the form of a {@link Disposable} + * or {@link Cancellable} via {@link #setDisposable(Disposable)} or {@link #setCancellable(Cancellable)} + * respectively. The emitter implementations will dispose/cancel this instance when the + * downstream cancels the flow or after the event generator logic calls {@link #onError(Throwable)}, + * {@link #onComplete()} or when {@link #tryOnError(Throwable)} succeeds. + * <p> + * Only one {@code Disposable} or {@code Cancellable} object can be associated with the emitter at + * a time. Calling either {@code set} method will dispose/cancel any previous object. If there + * is a need for handling multiple resources, one can create a {@link io.reactivex.rxjava3.disposables.CompositeDisposable} + * and associate that with the emitter instead. + * <p> + * The {@link Cancellable} is logically equivalent to {@code Disposable} but allows using cleanup logic that can + * throw a checked exception (such as many {@code close()} methods on Java IO components). Since + * the release of resources happens after the terminal events have been delivered or the sequence gets + * cancelled, exceptions throw within {@code Cancellable} are routed to the global error handler via + * {@link io.reactivex.rxjava3.plugins.RxJavaPlugins#onError(Throwable)}. + * + * @param <T> the value type to emit + */ +public interface FlowableEmitter<@NonNull T> extends Emitter<T> { + + /** + * Sets a Disposable on this emitter; any previous {@link Disposable} + * or {@link Cancellable} will be disposed/cancelled. + * <p>This method is thread-safe. + * @param d the disposable, {@code null} is allowed + */ + void setDisposable(@Nullable Disposable d); + + /** + * Sets a {@link Cancellable} on this emitter; any previous {@link Disposable} + * or {@code Cancellable} will be disposed/cancelled. + * <p>This method is thread-safe. + * @param c the {@code Cancellable} resource, {@code null} is allowed + */ + void setCancellable(@Nullable Cancellable c); + + /** + * The current outstanding request amount. + * <p>This method is thread-safe. + * @return the current outstanding request amount + */ + long requested(); + + /** + * Returns true if the downstream cancelled the sequence or the + * emitter was terminated via {@link #onError(Throwable)}, {@link #onComplete} or a + * successful {@link #tryOnError(Throwable)}. + * <p>This method is thread-safe. + * @return true if the downstream cancelled the sequence or the emitter was terminated + */ + boolean isCancelled(); + + /** + * Ensures that calls to {@code onNext}, {@code onError} and {@code onComplete} are properly serialized. + * @return the serialized {@link FlowableEmitter} + */ + @NonNull + FlowableEmitter<T> serialize(); + + /** + * Attempts to emit the specified {@link Throwable} error if the downstream + * hasn't cancelled the sequence or is otherwise terminated, returning false + * if the emission is not allowed to happen due to lifecycle restrictions. + * <p> + * Unlike {@link #onError(Throwable)}, the {@link io.reactivex.rxjava3.plugins.RxJavaPlugins#onError(Throwable) RxjavaPlugins.onError} + * is not called if the error could not be delivered. + * <p>History: 2.1.1 - experimental + * @param t the throwable error to signal if possible + * @return true if successful, false if the downstream is not able to accept further + * events + * @since 2.2 + */ + boolean tryOnError(@NonNull Throwable t); +} diff --git a/src/main/java/io/reactivex/rxjava3/core/FlowableOnSubscribe.java b/src/main/java/io/reactivex/rxjava3/core/FlowableOnSubscribe.java new file mode 100644 index 0000000000..6c5263b779 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/core/FlowableOnSubscribe.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.core; + +import io.reactivex.rxjava3.annotations.NonNull; + +/** + * A functional interface that has a {@code subscribe()} method that receives + * a {@link FlowableEmitter} instance that allows pushing + * events in a backpressure-safe and cancellation-safe manner. + * + * @param <T> the value type pushed + */ +@FunctionalInterface +public interface FlowableOnSubscribe<@NonNull T> { + + /** + * Called for each {@link org.reactivestreams.Subscriber Subscriber} that subscribes. + * @param emitter the safe emitter instance, never {@code null} + * @throws Throwable on error + */ + void subscribe(@NonNull FlowableEmitter<T> emitter) throws Throwable; +} + diff --git a/src/main/java/io/reactivex/rxjava3/core/FlowableOperator.java b/src/main/java/io/reactivex/rxjava3/core/FlowableOperator.java new file mode 100644 index 0000000000..aa2d5ff0e7 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/core/FlowableOperator.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.core; + +import org.reactivestreams.Subscriber; + +import io.reactivex.rxjava3.annotations.NonNull; + +/** + * Interface to map/wrap a downstream {@link Subscriber} to an upstream {@code Subscriber}. + * + * @param <Downstream> the value type of the downstream + * @param <Upstream> the value type of the upstream + */ +@FunctionalInterface +public interface FlowableOperator<@NonNull Downstream, @NonNull Upstream> { + /** + * Applies a function to the child {@link Subscriber} and returns a new parent {@code Subscriber}. + * @param subscriber the child {@code Subscriber} instance + * @return the parent {@code Subscriber} instance + * @throws Throwable on failure + */ + @NonNull + Subscriber<? super Upstream> apply(@NonNull Subscriber<? super Downstream> subscriber) throws Throwable; +} diff --git a/src/main/java/io/reactivex/rxjava3/core/FlowableSubscriber.java b/src/main/java/io/reactivex/rxjava3/core/FlowableSubscriber.java new file mode 100644 index 0000000000..f97eadbd37 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/core/FlowableSubscriber.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.core; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.annotations.NonNull; + +/** + * Represents a Reactive-Streams inspired {@link Subscriber} that is RxJava 3 only + * and weakens the Reactive Streams rules <a href='https://github.com/reactive-streams/reactive-streams-jvm#1.3'>§1.3</a> + * and <a href='https://github.com/reactive-streams/reactive-streams-jvm#3.9'>§3.9</a> of the specification + * for gaining performance. + * + * <p>History: 2.0.7 - experimental; 2.1 - beta + * @param <T> the value type + * @since 2.2 + */ +public interface FlowableSubscriber<@NonNull T> extends Subscriber<T> { + + /** + * Implementors of this method should make sure everything that needs + * to be visible in {@link #onNext(Object)} is established before + * calling {@link Subscription#request(long)}. In practice this means + * no initialization should happen after the {@code request()} call and + * additional behavior is thread safe in respect to {@code onNext}. + * + * {@inheritDoc} + */ + @Override + void onSubscribe(@NonNull Subscription s); +} diff --git a/src/main/java/io/reactivex/rxjava3/core/FlowableTransformer.java b/src/main/java/io/reactivex/rxjava3/core/FlowableTransformer.java new file mode 100644 index 0000000000..fe51a2d5d7 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/core/FlowableTransformer.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.core; + +import org.reactivestreams.Publisher; + +import io.reactivex.rxjava3.annotations.NonNull; + +/** + * Interface to compose {@link Flowable}s. + * + * @param <Upstream> the upstream value type + * @param <Downstream> the downstream value type + */ +@FunctionalInterface +public interface FlowableTransformer<@NonNull Upstream, @NonNull Downstream> { + /** + * Applies a function to the upstream {@link Flowable} and returns a {@link Publisher} with + * optionally different element type. + * @param upstream the upstream {@code Flowable} instance + * @return the transformed {@code Publisher} instance + */ + @NonNull + Publisher<Downstream> apply(@NonNull Flowable<Upstream> upstream); +} diff --git a/src/main/java/io/reactivex/rxjava3/core/Maybe.java b/src/main/java/io/reactivex/rxjava3/core/Maybe.java new file mode 100644 index 0000000000..8ae4137c83 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/core/Maybe.java @@ -0,0 +1,6353 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.core; + +import java.util.*; +import java.util.concurrent.*; +import java.util.stream.*; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.annotations.*; +import io.reactivex.rxjava3.disposables.*; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.functions.*; +import io.reactivex.rxjava3.internal.fuseable.*; +import io.reactivex.rxjava3.internal.jdk8.*; +import io.reactivex.rxjava3.internal.observers.*; +import io.reactivex.rxjava3.internal.operators.flowable.*; +import io.reactivex.rxjava3.internal.operators.maybe.*; +import io.reactivex.rxjava3.internal.operators.mixed.*; +import io.reactivex.rxjava3.internal.operators.observable.ObservableElementAtMaybe; +import io.reactivex.rxjava3.internal.util.ErrorMode; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.schedulers.*; + +/** + * The {@code Maybe} class represents a deferred computation and emission of a single value, no value at all or an exception. + * <p> + * The {@code Maybe} class implements the {@link MaybeSource} base interface and the default consumer + * type it interacts with is the {@link MaybeObserver} via the {@link #subscribe(MaybeObserver)} method. + * <p> + * The {@code Maybe} operates with the following sequential protocol: + * <pre><code> + * onSubscribe (onSuccess | onError | onComplete)? + * </code></pre> + * <p> + * Note that {@code onSuccess}, {@code onError} and {@code onComplete} are mutually exclusive events; unlike {@link Observable}, + * {@code onSuccess} is never followed by {@code onError} or {@code onComplete}. + * <p> + * Like {@code Observable}, a running {@code Maybe} can be stopped through the {@link Disposable} instance + * provided to consumers through {@link MaybeObserver#onSubscribe}. + * <p> + * Like an {@code Observable}, a {@code Maybe} is lazy, can be either "hot" or "cold", synchronous or + * asynchronous. {@code Maybe} instances returned by the methods of this class are <em>cold</em> + * and there is a standard <em>hot</em> implementation in the form of a subject: + * {@link io.reactivex.rxjava3.subjects.MaybeSubject MaybeSubject}. + * <p> + * The documentation for this class makes use of marble diagrams. The following legend explains these diagrams: + * <p> + * <img width="640" height="370" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/maybe.png" alt=""> + * <p> + * See {@link Flowable} or {@code Observable} for the + * implementation of the Reactive Pattern for a stream or vector of values. + * <p> + * Example: + * <pre><code> + * Disposable d = Maybe.just("Hello World") + * .delay(10, TimeUnit.SECONDS, Schedulers.io()) + * .subscribeWith(new DisposableMaybeObserver<String>() { + * @Override + * public void onStart() { + * System.out.println("Started"); + * } + * + * @Override + * public void onSuccess(String value) { + * System.out.println("Success: " + value); + * } + * + * @Override + * public void onError(Throwable error) { + * error.printStackTrace(); + * } + * + * @Override + * public void onComplete() { + * System.out.println("Done!"); + * } + * }); + * + * Thread.sleep(5000); + * + * d.dispose(); + * </code></pre> + * <p> + * Note that by design, subscriptions via {@link #subscribe(MaybeObserver)} can't be disposed + * from the outside (hence the + * {@code void} return of the {@link #subscribe(MaybeObserver)} method) and it is the + * responsibility of the implementor of the {@code MaybeObserver} to allow this to happen. + * RxJava supports such usage with the standard + * {@link io.reactivex.rxjava3.observers.DisposableMaybeObserver DisposableMaybeObserver} instance. + * For convenience, the {@link #subscribeWith(MaybeObserver)} method is provided as well to + * allow working with a {@code MaybeObserver} (or subclass) instance to be applied with in + * a fluent manner (such as in the example above). + * + * @param <T> the value type + * @since 2.0 + * @see io.reactivex.rxjava3.observers.DisposableMaybeObserver + */ +public abstract class Maybe<@NonNull T> implements MaybeSource<T> { + + /** + * Runs multiple {@link MaybeSource}s provided by an {@link Iterable} sequence and + * signals the events of the first one that signals (disposing the rest). + * <p> + * <img width="640" height="518" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.amb.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code amb} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <T> the value type + * @param sources the {@code Iterable} sequence of sources. A subscription to each source will + * occur in the same order as in the {@code Iterable}. + * @return the new {@code Maybe} instance + * @throws NullPointerException if {@code sources} is {@code null} + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T> Maybe<T> amb(@NonNull Iterable<@NonNull ? extends MaybeSource<? extends T>> sources) { + Objects.requireNonNull(sources, "sources is null"); + return RxJavaPlugins.onAssembly(new MaybeAmb<>(null, sources)); + } + + /** + * Runs multiple {@link MaybeSource}s and signals the events of the first one that signals (disposing + * the rest). + * <p> + * <img width="640" height="519" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.ambArray.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code ambArray} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <T> the value type + * @param sources the array of sources. A subscription to each source will + * occur in the same order as in the array. + * @return the new {@code Maybe} instance + * @throws NullPointerException if {@code sources} is {@code null} + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + @SafeVarargs + public static <@NonNull T> Maybe<T> ambArray(@NonNull MaybeSource<? extends T>... sources) { + Objects.requireNonNull(sources, "sources is null"); + if (sources.length == 0) { + return empty(); + } + if (sources.length == 1) { + @SuppressWarnings("unchecked") + MaybeSource<T> source = (MaybeSource<T>)sources[0]; + return wrap(source); + } + return RxJavaPlugins.onAssembly(new MaybeAmb<>(sources, null)); + } + + /** + * Concatenate the single values, in a non-overlapping fashion, of the {@link MaybeSource} sources provided by + * an {@link Iterable} sequence as a {@link Flowable} sequence. + * <p> + * <img width="640" height="526" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.concat.i.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The returned {@code Flowable} honors the backpressure of the downstream consumer.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code concat} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <T> the value type + * @param sources the {@code Iterable} sequence of {@code MaybeSource} instances + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code sources} is {@code null} + */ + @BackpressureSupport(BackpressureKind.FULL) + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T> Flowable<T> concat(@NonNull Iterable<@NonNull ? extends MaybeSource<? extends T>> sources) { + Objects.requireNonNull(sources, "sources is null"); + return RxJavaPlugins.onAssembly(new MaybeConcatIterable<>(sources)); + } + + /** + * Returns a {@link Flowable} that emits the items emitted by two {@link MaybeSource}s, one after the other. + * <p> + * <img width="640" height="423" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.concat.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The returned {@code Flowable} honors the backpressure of the downstream consumer.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code concat} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T> the common value type + * @param source1 + * a {@code MaybeSource} to be concatenated + * @param source2 + * a {@code MaybeSource} to be concatenated + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code source1} or {@code source2} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/concat.html">ReactiveX operators documentation: Concat</a> + */ + @BackpressureSupport(BackpressureKind.FULL) + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T> Flowable<T> concat(@NonNull MaybeSource<? extends T> source1, @NonNull MaybeSource<? extends T> source2) { + Objects.requireNonNull(source1, "source1 is null"); + Objects.requireNonNull(source2, "source2 is null"); + return concatArray(source1, source2); + } + + /** + * Returns a {@link Flowable} that emits the items emitted by three {@link MaybeSource}s, one after the other. + * <p> + * <img width="640" height="423" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.concat.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The returned {@code Flowable} honors the backpressure of the downstream consumer.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code concat} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T> the common value type + * @param source1 + * a {@code MaybeSource} to be concatenated + * @param source2 + * a {@code MaybeSource} to be concatenated + * @param source3 + * a {@code MaybeSource} to be concatenated + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code source1}, {@code source2} or {@code source3} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/concat.html">ReactiveX operators documentation: Concat</a> + */ + @BackpressureSupport(BackpressureKind.FULL) + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T> Flowable<T> concat( + @NonNull MaybeSource<? extends T> source1, @NonNull MaybeSource<? extends T> source2, @NonNull MaybeSource<? extends T> source3) { + Objects.requireNonNull(source1, "source1 is null"); + Objects.requireNonNull(source2, "source2 is null"); + Objects.requireNonNull(source3, "source3 is null"); + return concatArray(source1, source2, source3); + } + + /** + * Returns a {@link Flowable} that emits the items emitted by four {@link MaybeSource}s, one after the other. + * <p> + * <img width="640" height="423" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.concat.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The returned {@code Flowable} honors the backpressure of the downstream consumer.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code concat} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T> the common value type + * @param source1 + * a {@code MaybeSource} to be concatenated + * @param source2 + * a {@code MaybeSource} to be concatenated + * @param source3 + * a {@code MaybeSource} to be concatenated + * @param source4 + * a {@code MaybeSource} to be concatenated + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code source1}, {@code source2}, {@code source3} or {@code source4} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/concat.html">ReactiveX operators documentation: Concat</a> + */ + @BackpressureSupport(BackpressureKind.FULL) + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T> Flowable<T> concat( + @NonNull MaybeSource<? extends T> source1, @NonNull MaybeSource<? extends T> source2, @NonNull MaybeSource<? extends T> source3, @NonNull MaybeSource<? extends T> source4) { + Objects.requireNonNull(source1, "source1 is null"); + Objects.requireNonNull(source2, "source2 is null"); + Objects.requireNonNull(source3, "source3 is null"); + Objects.requireNonNull(source4, "source4 is null"); + return concatArray(source1, source2, source3, source4); + } + + /** + * Concatenate the single values, in a non-overlapping fashion, of the {@link MaybeSource} sources provided by + * a {@link Publisher} sequence as a {@link Flowable} sequence. + * <p> + * <img width="640" height="416" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.concat.p.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The returned {@code Flowable} honors the backpressure of the downstream consumer and + * expects the {@code Publisher} to honor backpressure as well. If the sources {@code Publisher} + * violates this, a {@link io.reactivex.rxjava3.exceptions.MissingBackpressureException} is signaled.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code concat} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <T> the value type + * @param sources the {@code Publisher} of {@code MaybeSource} instances + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code sources} is {@code null} + */ + @BackpressureSupport(BackpressureKind.FULL) + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public static <@NonNull T> Flowable<T> concat(@NonNull Publisher<@NonNull ? extends MaybeSource<? extends T>> sources) { + return concat(sources, 2); + } + + /** + * Concatenate the single values, in a non-overlapping fashion, of the {@link MaybeSource} sources provided by + * a {@link Publisher} sequence as a {@link Flowable} sequence. + * <p> + * <img width="640" height="416" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.concat.pn.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The returned {@code Flowable} honors the backpressure of the downstream consumer and + * expects the {@code Publisher} to honor backpressure as well. If the sources {@code Publisher} + * violates this, a {@link io.reactivex.rxjava3.exceptions.MissingBackpressureException} is signaled.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code concat} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <T> the value type + * @param sources the {@code Publisher} of {@code MaybeSource} instances + * @param prefetch the number of {@code MaybeSource}s to prefetch from the {@code Publisher} + * @throws NullPointerException if {@code sources} is {@code null} + * @throws IllegalArgumentException if {@code prefetch} is non-positive + * @return the new {@code Flowable} instance + */ + @BackpressureSupport(BackpressureKind.FULL) + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T> Flowable<T> concat(@NonNull Publisher<@NonNull ? extends MaybeSource<? extends T>> sources, int prefetch) { + Objects.requireNonNull(sources, "sources is null"); + ObjectHelper.verifyPositive(prefetch, "prefetch"); + return RxJavaPlugins.onAssembly(new FlowableConcatMapMaybePublisher<>(sources, Functions.identity(), ErrorMode.IMMEDIATE, prefetch)); + } + + /** + * Concatenate the single values, in a non-overlapping fashion, of the {@link MaybeSource} sources in the array + * as a {@link Flowable} sequence. + * <p> + * <img width="640" height="526" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.concatArray.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The returned {@code Flowable} honors the backpressure of the downstream consumer.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code concatArray} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <T> the value type + * @param sources the array of {@code MaybeSource} instances + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code sources} is {@code null} + */ + @BackpressureSupport(BackpressureKind.FULL) + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + @SafeVarargs + public static <@NonNull T> Flowable<T> concatArray(@NonNull MaybeSource<? extends T>... sources) { + Objects.requireNonNull(sources, "sources is null"); + if (sources.length == 0) { + return Flowable.empty(); + } + if (sources.length == 1) { + @SuppressWarnings("unchecked") + MaybeSource<T> source = (MaybeSource<T>)sources[0]; + return RxJavaPlugins.onAssembly(new MaybeToFlowable<>(source)); + } + return RxJavaPlugins.onAssembly(new MaybeConcatArray<>(sources)); + } + + /** + * Concatenates a variable number of {@link MaybeSource} sources and delays errors from any of them + * till all terminate as a {@link Flowable} sequence. + * <p> + * <img width="640" height="425" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.concatArrayDelayError.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code concatArrayDelayError} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param sources the array of sources + * @param <T> the common base value type + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code sources} is {@code null} + */ + @BackpressureSupport(BackpressureKind.FULL) + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @SafeVarargs + @NonNull + public static <@NonNull T> Flowable<T> concatArrayDelayError(@NonNull MaybeSource<? extends T>... sources) { + Objects.requireNonNull(sources, "sources is null"); + if (sources.length == 0) { + return Flowable.empty(); + } else + if (sources.length == 1) { + @SuppressWarnings("unchecked") + MaybeSource<T> source = (MaybeSource<T>)sources[0]; + return RxJavaPlugins.onAssembly(new MaybeToFlowable<>(source)); + } + return RxJavaPlugins.onAssembly(new MaybeConcatArrayDelayError<>(sources)); + } + + /** + * Concatenates a sequence of {@link MaybeSource} eagerly into a {@link Flowable} sequence. + * <p> + * Eager concatenation means that once an observer subscribes, this operator subscribes to all of the + * source {@code MaybeSource}s. The operator buffers the value emitted by these {@code MaybeSource}s and then drains them + * in order, each one after the previous one completes. + * <p> + * <img width="640" height="490" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.concatArrayEager.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>This method does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <T> the value type + * @param sources a sequence of {@code MaybeSource}s that need to be eagerly concatenated + * @return the new {@code Flowable} instance with the specified concatenation behavior + * @throws NullPointerException if {@code sources} is {@code null} + */ + @SuppressWarnings({ "rawtypes", "unchecked" }) + @BackpressureSupport(BackpressureKind.FULL) + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + @SafeVarargs + public static <@NonNull T> Flowable<T> concatArrayEager(@NonNull MaybeSource<? extends T>... sources) { + return Flowable.fromArray(sources).concatMapEager((Function)MaybeToPublisher.instance()); + } + /** + * Concatenates a sequence of {@link MaybeSource} eagerly into a {@link Flowable} sequence. + * <p> + * Eager concatenation means that once an observer subscribes, this operator subscribes to all of the + * source {@code MaybeSource}s. The operator buffers the value emitted by these {@code MaybeSource}s and then drains them + * in order, each one after the previous one completes. + * <p> + * <img width="640" height="428" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.concatArrayEagerDelayError.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>This method does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <T> the value type + * @param sources a sequence of {@code MaybeSource}s that need to be eagerly concatenated + * @return the new {@code Flowable} instance with the specified concatenation behavior + * @throws NullPointerException if {@code sources} is {@code null} + * @since 3.0.0 + */ + @SuppressWarnings({ "rawtypes", "unchecked" }) + @BackpressureSupport(BackpressureKind.FULL) + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + @SafeVarargs + public static <@NonNull T> Flowable<T> concatArrayEagerDelayError(@NonNull MaybeSource<? extends T>... sources) { + return Flowable.fromArray(sources).concatMapEagerDelayError((Function)MaybeToPublisher.instance(), true); + } + + /** + * Concatenates the {@link Iterable} sequence of {@link MaybeSource}s into a single sequence by subscribing to each {@code MaybeSource}, + * one after the other, one at a time and delays any errors till the all inner {@code MaybeSource}s terminate + * as a {@link Flowable} sequence. + * <p> + * <img width="640" height="451" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.concatDelayError3.i.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code concatDelayError} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T> the common element base type + * @param sources the {@code Iterable} sequence of {@code MaybeSource}s + * @return the new {@code Flowable} with the concatenating behavior + * @throws NullPointerException if {@code sources} is {@code null} + */ + @BackpressureSupport(BackpressureKind.FULL) + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T> Flowable<T> concatDelayError(@NonNull Iterable<@NonNull ? extends MaybeSource<? extends T>> sources) { + return Flowable.fromIterable(sources).concatMapMaybeDelayError(Functions.identity()); + } + + /** + * Concatenates the {@link Publisher} sequence of {@link MaybeSource}s into a single sequence by subscribing to each inner {@code MaybeSource}, + * one after the other, one at a time and delays any errors till the all inner and the outer {@code Publisher} terminate + * as a {@link Flowable} sequence. + * <p> + * <img width="640" height="360" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.concatDelayError.p.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>{@code concatDelayError} fully supports backpressure.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code concatDelayError} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T> the common element base type + * @param sources the {@code Publisher} sequence of {@code MaybeSource}s + * @return the new {@code Flowable} with the concatenating behavior + * @throws NullPointerException if {@code sources} is {@code null} + */ + @BackpressureSupport(BackpressureKind.FULL) + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public static <@NonNull T> Flowable<T> concatDelayError(@NonNull Publisher<@NonNull ? extends MaybeSource<? extends T>> sources) { + return Flowable.fromPublisher(sources).concatMapMaybeDelayError(Functions.identity()); + } + /** + * Concatenates the {@link Publisher} sequence of {@link MaybeSource}s into a single sequence by subscribing to each inner {@code MaybeSource}, + * one after the other, one at a time and delays any errors till the all inner and the outer {@code Publisher} terminate + * as a {@link Flowable} sequence. + * <p> + * <img width="640" height="299" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.concatDelayError.pn.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>{@code concatDelayError} fully supports backpressure.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code concatDelayError} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T> the common element base type + * @param sources the {@code Publisher} sequence of {@code MaybeSource}s + * @param prefetch The number of upstream items to prefetch so that fresh items are + * ready to be mapped when a previous {@code MaybeSource} terminates. + * The operator replenishes after half of the prefetch amount has been consumed + * and turned into {@code MaybeSource}s. + * @return the new {@code Flowable} with the concatenating behavior + * @throws NullPointerException if {@code sources} is {@code null} + * @throws IllegalArgumentException if {@code prefetch} is non-positive + * @since 3.0.0 + */ + @BackpressureSupport(BackpressureKind.FULL) + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public static <@NonNull T> Flowable<T> concatDelayError(@NonNull Publisher<@NonNull ? extends MaybeSource<? extends T>> sources, int prefetch) { + return Flowable.fromPublisher(sources).concatMapMaybeDelayError(Functions.identity(), true, prefetch); + } + + /** + * Concatenates a sequence of {@link MaybeSource}s eagerly into a {@link Flowable} sequence. + * <p> + * <img width="640" height="526" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.concatEager.i.png" alt=""> + * <p> + * Eager concatenation means that once an observer subscribes, this operator subscribes to all of the + * source {@code MaybeSource}s. The operator buffers the values emitted by these {@code MaybeSource}s and then drains them + * in order, each one after the previous one completes. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>Backpressure is honored towards the downstream.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>This method does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <T> the value type + * @param sources a sequence of {@code MaybeSource} that need to be eagerly concatenated + * @return the new {@code Flowable} instance with the specified concatenation behavior + * @throws NullPointerException if {@code sources} is {@code null} + */ + @SuppressWarnings({ "rawtypes", "unchecked" }) + @BackpressureSupport(BackpressureKind.FULL) + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public static <@NonNull T> Flowable<T> concatEager(@NonNull Iterable<@NonNull ? extends MaybeSource<? extends T>> sources) { + return Flowable.fromIterable(sources).concatMapEagerDelayError((Function)MaybeToPublisher.instance(), false); + } + + /** + * Concatenates a sequence of {@link MaybeSource}s eagerly into a {@link Flowable} sequence and + * runs a limited number of the inner sequences at once. + * <p> + * <img width="640" height="439" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.concatEager.in.png" alt=""> + * <p> + * Eager concatenation means that once an observer subscribes, this operator subscribes to all of the + * source {@code MaybeSource}s. The operator buffers the values emitted by these {@code MaybeSource}s and then drains them + * in order, each one after the previous one completes. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>Backpressure is honored towards the downstream.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>This method does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <T> the value type + * @param sources a sequence of {@code MaybeSource} that need to be eagerly concatenated + * @param maxConcurrency the maximum number of concurrently running inner {@code MaybeSource}s; {@link Integer#MAX_VALUE} + * is interpreted as all inner {@code MaybeSource}s can be active at the same time + * @return the new {@code Flowable} instance with the specified concatenation behavior + * @throws NullPointerException if {@code sources} is {@code null} + * @throws IllegalArgumentException if {@code maxConcurrency} is non-positive + * @since 3.0.0 + */ + @SuppressWarnings({ "rawtypes", "unchecked" }) + @BackpressureSupport(BackpressureKind.FULL) + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public static <@NonNull T> Flowable<T> concatEager(@NonNull Iterable<@NonNull ? extends MaybeSource<? extends T>> sources, int maxConcurrency) { + return Flowable.fromIterable(sources).concatMapEagerDelayError((Function)MaybeToPublisher.instance(), false, maxConcurrency, 1); + } + + /** + * Concatenates a {@link Publisher} sequence of {@link MaybeSource}s eagerly into a {@link Flowable} sequence. + * <p> + * Eager concatenation means that once a subscriber subscribes, this operator subscribes to all of the + * emitted source {@code MaybeSource}s as they are observed. The operator buffers the values emitted by these + * {@code MaybeSource}s and then drains them in order, each one after the previous one completes. + * <p> + * <img width="640" height="511" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.concatEager.p.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>Backpressure is honored towards the downstream and the outer {@code Publisher} is + * expected to support backpressure. Violating this assumption, the operator will + * signal {@link io.reactivex.rxjava3.exceptions.MissingBackpressureException}.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>This method does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <T> the value type + * @param sources a sequence of {@code MaybeSource}s that need to be eagerly concatenated + * @return the new {@code Flowable} instance with the specified concatenation behavior + * @throws NullPointerException if {@code sources} is {@code null} + */ + @SuppressWarnings({ "rawtypes", "unchecked" }) + @BackpressureSupport(BackpressureKind.FULL) + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public static <@NonNull T> Flowable<T> concatEager(@NonNull Publisher<@NonNull ? extends MaybeSource<? extends T>> sources) { + return Flowable.fromPublisher(sources).concatMapEager((Function)MaybeToPublisher.instance()); + } + + /** + * Concatenates a {@link Publisher} sequence of {@link MaybeSource}s eagerly into a {@link Flowable} sequence, + * running at most the given number of inner {@code MaybeSource}s at once. + * <p> + * <img width="640" height="425" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.concatEager.pn.png" alt=""> + * <p> + * Eager concatenation means that once a subscriber subscribes, this operator subscribes to all of the + * emitted source {@code MaybeSource}s as they are observed. The operator buffers the values emitted by these + * {@code MaybeSource}s and then drains them in order, each one after the previous one completes. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>Backpressure is honored towards the downstream and the outer {@code Publisher} is + * expected to support backpressure. Violating this assumption, the operator will + * signal {@link io.reactivex.rxjava3.exceptions.MissingBackpressureException}.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>This method does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <T> the value type + * @param sources a sequence of {@code MaybeSource}s that need to be eagerly concatenated + * @param maxConcurrency the maximum number of concurrently running inner {@code MaybeSource}s; {@link Integer#MAX_VALUE} + * is interpreted as all inner {@code MaybeSource}s can be active at the same time + * @return the new {@code Flowable} instance with the specified concatenation behavior + * @throws NullPointerException if {@code sources} is {@code null} + * @throws IllegalArgumentException if {@code maxConcurrency} is non-positive + * @since 3.0.0 + */ + @SuppressWarnings({ "rawtypes", "unchecked" }) + @BackpressureSupport(BackpressureKind.FULL) + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public static <@NonNull T> Flowable<T> concatEager(@NonNull Publisher<@NonNull ? extends MaybeSource<? extends T>> sources, int maxConcurrency) { + return Flowable.fromPublisher(sources).concatMapEager((Function)MaybeToPublisher.instance(), maxConcurrency, 1); + } + + /** + * Concatenates a sequence of {@link MaybeSource}s eagerly into a {@link Flowable} sequence, + * delaying errors until all inner {@code MaybeSource}s terminate. + * <p> + * <img width="640" height="428" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.concatEagerDelayError.i.png" alt=""> + * <p> + * Eager concatenation means that once an observer subscribes, this operator subscribes to all of the + * source {@code MaybeSource}s. The operator buffers the values emitted by these {@code MaybeSource}s and then drains them + * in order, each one after the previous one completes. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>Backpressure is honored towards the downstream.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>This method does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <T> the value type + * @param sources a sequence of {@code MaybeSource} that need to be eagerly concatenated + * @return the new {@code Flowable} instance with the specified concatenation behavior + * @throws NullPointerException if {@code sources} is {@code null} + * @since 3.0.0 + */ + @SuppressWarnings({ "rawtypes", "unchecked" }) + @BackpressureSupport(BackpressureKind.FULL) + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public static <@NonNull T> Flowable<T> concatEagerDelayError(@NonNull Iterable<@NonNull ? extends MaybeSource<? extends T>> sources) { + return Flowable.fromIterable(sources).concatMapEagerDelayError((Function)MaybeToPublisher.instance(), true); + } + + /** + * Concatenates a sequence of {@link MaybeSource}s eagerly into a {@link Flowable} sequence, + * delaying errors until all inner {@code MaybeSource}s terminate and + * runs a limited number of inner {@code MaybeSource}s at once. + * <p> + * <img width="640" height="379" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.concatEagerDelayError.in.png" alt=""> + * <p> + * Eager concatenation means that once an observer subscribes, this operator subscribes to all of the + * source {@code MaybeSource}s. The operator buffers the values emitted by these {@code MaybeSource}s and then drains them + * in order, each one after the previous one completes. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>Backpressure is honored towards the downstream.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>This method does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <T> the value type + * @param sources a sequence of {@code MaybeSource} that need to be eagerly concatenated + * @param maxConcurrency the maximum number of concurrently running inner {@code MaybeSource}s; {@link Integer#MAX_VALUE} + * is interpreted as all inner {@code MaybeSource}s can be active at the same time + * @return the new {@code Flowable} instance with the specified concatenation behavior + * @throws NullPointerException if {@code sources} is {@code null} + * @throws IllegalArgumentException if {@code maxConcurrency} is non-positive + * @since 3.0.0 + */ + @SuppressWarnings({ "rawtypes", "unchecked" }) + @BackpressureSupport(BackpressureKind.FULL) + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public static <@NonNull T> Flowable<T> concatEagerDelayError(@NonNull Iterable<@NonNull ? extends MaybeSource<? extends T>> sources, int maxConcurrency) { + return Flowable.fromIterable(sources).concatMapEagerDelayError((Function)MaybeToPublisher.instance(), true, maxConcurrency, 1); + } + + /** + * Concatenates a {@link Publisher} sequence of {@link MaybeSource}s eagerly into a {@link Flowable} sequence, + * delaying errors until all the inner and the outer sequence terminate. + * <p> + * <img width="640" height="495" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.concatEagerDelayError.p.png" alt=""> + * <p> + * Eager concatenation means that once a subscriber subscribes, this operator subscribes to all of the + * emitted source {@code MaybeSource}s as they are observed. The operator buffers the values emitted by these + * {@code MaybeSource}s and then drains them in order, each one after the previous one completes. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>Backpressure is honored towards the downstream and the outer {@code Publisher} is + * expected to support backpressure. Violating this assumption, the operator will + * signal {@link io.reactivex.rxjava3.exceptions.MissingBackpressureException}.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>This method does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <T> the value type + * @param sources a sequence of {@code MaybeSource}s that need to be eagerly concatenated + * @return the new {@code Flowable} instance with the specified concatenation behavior + * @throws NullPointerException if {@code sources} is {@code null} + * @since 3.0.0 + */ + @SuppressWarnings({ "rawtypes", "unchecked" }) + @BackpressureSupport(BackpressureKind.FULL) + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public static <@NonNull T> Flowable<T> concatEagerDelayError(@NonNull Publisher<@NonNull ? extends MaybeSource<? extends T>> sources) { + return Flowable.fromPublisher(sources).concatMapEagerDelayError((Function)MaybeToPublisher.instance(), true); + } + + /** + * Concatenates a {@link Publisher} sequence of {@link MaybeSource}s eagerly into a {@link Flowable} sequence, + * delaying errors until all the inner and the outer sequence terminate and + * runs a limited number of the inner {@code MaybeSource}s at once. + * <p> + * <img width="640" height="421" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.concatEagerDelayError.pn.png" alt=""> + * <p> + * Eager concatenation means that once a subscriber subscribes, this operator subscribes to all of the + * emitted source {@code MaybeSource}s as they are observed. The operator buffers the values emitted by these + * {@code MaybeSource}s and then drains them in order, each one after the previous one completes. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>Backpressure is honored towards the downstream and the outer {@code Publisher} is + * expected to support backpressure. Violating this assumption, the operator will + * signal {@link io.reactivex.rxjava3.exceptions.MissingBackpressureException}.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>This method does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <T> the value type + * @param sources a sequence of {@code MaybeSource}s that need to be eagerly concatenated + * @param maxConcurrency the maximum number of concurrently running inner {@code MaybeSource}s; {@link Integer#MAX_VALUE} + * is interpreted as all inner {@code MaybeSource}s can be active at the same time + * @return the new {@code Flowable} instance with the specified concatenation behavior + * @throws NullPointerException if {@code sources} is {@code null} + * @throws IllegalArgumentException if {@code maxConcurrency} is non-positive + * @since 3.0.0 + */ + @SuppressWarnings({ "rawtypes", "unchecked" }) + @BackpressureSupport(BackpressureKind.FULL) + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public static <@NonNull T> Flowable<T> concatEagerDelayError(@NonNull Publisher<@NonNull ? extends MaybeSource<? extends T>> sources, int maxConcurrency) { + return Flowable.fromPublisher(sources).concatMapEagerDelayError((Function)MaybeToPublisher.instance(), true, maxConcurrency, 1); + } + + /** + * Provides an API (via a cold {@code Maybe}) that bridges the reactive world with the callback-style world. + * <p> + * <img width="640" height="499" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.create.png" alt=""> + * <p> + * Example: + * <pre><code> + * Maybe.<Event>create(emitter -> { + * Callback listener = new Callback() { + * @Override + * public void onEvent(Event e) { + * if (e.isNothing()) { + * emitter.onComplete(); + * } else { + * emitter.onSuccess(e); + * } + * } + * + * @Override + * public void onFailure(Exception e) { + * emitter.onError(e); + * } + * }; + * + * AutoCloseable c = api.someMethod(listener); + * + * emitter.setCancellable(c::close); + * + * }); + * </code></pre> + * <p> + * Whenever a {@link MaybeObserver} subscribes to the returned {@code Maybe}, the provided + * {@link MaybeOnSubscribe} callback is invoked with a fresh instance of a {@link MaybeEmitter} + * that will interact only with that specific {@code MaybeObserver}. If this {@code MaybeObserver} + * disposes the flow (making {@link MaybeEmitter#isDisposed} return {@code true}), + * other observers subscribed to the same returned {@code Maybe} are not affected. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code create} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <T> the value type + * @param onSubscribe the emitter that is called when a {@code MaybeObserver} subscribes to the returned {@code Maybe} + * @return the new {@code Maybe} instance + * @throws NullPointerException if {@code onSubscribe} is {@code null} + * @see MaybeOnSubscribe + * @see Cancellable + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T> Maybe<T> create(@NonNull MaybeOnSubscribe<T> onSubscribe) { + Objects.requireNonNull(onSubscribe, "onSubscribe is null"); + return RxJavaPlugins.onAssembly(new MaybeCreate<>(onSubscribe)); + } + + /** + * Calls a {@link Supplier} for each individual {@link MaybeObserver} to return the actual {@link MaybeSource} source to + * be subscribed to. + * <p> + * <img width="640" height="498" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.defer.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code defer} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <T> the value type + * @param supplier the {@code Supplier} that is called for each individual {@code MaybeObserver} and + * returns a {@code MaybeSource} instance to subscribe to + * @return the new {@code Maybe} instance + * @throws NullPointerException if {@code supplier} is {@code null} + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T> Maybe<T> defer(@NonNull Supplier<? extends @NonNull MaybeSource<? extends T>> supplier) { + Objects.requireNonNull(supplier, "supplier is null"); + return RxJavaPlugins.onAssembly(new MaybeDefer<>(supplier)); + } + + /** + * Returns a (singleton) {@code Maybe} instance that calls {@link MaybeObserver#onComplete onComplete} + * immediately. + * <p> + * <img width="640" height="190" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/empty.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code empty} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <T> the value type + * @return the shared {@code Maybe} instance + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @SuppressWarnings("unchecked") + @NonNull + public static <@NonNull T> Maybe<T> empty() { + return RxJavaPlugins.onAssembly((Maybe<T>)MaybeEmpty.INSTANCE); + } + + /** + * Returns a {@code Maybe} that invokes a subscriber's {@link MaybeObserver#onError onError} method when the + * subscriber subscribes to it. + * <p> + * <img width="640" height="447" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.error.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code error} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param throwable + * the particular {@link Throwable} to pass to {@link MaybeObserver#onError onError} + * @param <T> + * the type of the item (ostensibly) emitted by the {@code Maybe} + * @return the new {@code Maybe} instance + * @throws NullPointerException if {@code throwable} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/empty-never-throw.html">ReactiveX operators documentation: Throw</a> + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T> Maybe<T> error(@NonNull Throwable throwable) { + Objects.requireNonNull(throwable, "throwable is null"); + return RxJavaPlugins.onAssembly(new MaybeError<>(throwable)); + } + + /** + * Returns a {@code Maybe} that invokes a {@link MaybeObserver}'s {@link MaybeObserver#onError onError} method when the + * {@code MaybeObserver} subscribes to it. + * <p> + * <img width="640" height="440" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.error.f.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code error} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param supplier + * a {@link Supplier} factory to return a {@link Throwable} for each individual {@code MaybeObserver} + * @param <T> + * the type of the items (ostensibly) emitted by the {@code Maybe} + * @return the new {@code Maybe} instance + * @throws NullPointerException if {@code supplier} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/empty-never-throw.html">ReactiveX operators documentation: Throw</a> + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T> Maybe<T> error(@NonNull Supplier<? extends @NonNull Throwable> supplier) { + Objects.requireNonNull(supplier, "supplier is null"); + return RxJavaPlugins.onAssembly(new MaybeErrorCallable<>(supplier)); + } + + /** + * Returns a {@code Maybe} instance that runs the given {@link Action} for each {@link MaybeObserver} and + * emits either its exception or simply completes. + * <p> + * <img width="640" height="287" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.fromAction.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code fromAction} does not operate by default on a particular {@link Scheduler}.</dd> + * <dt><b>Error handling:</b></dt> + * <dd> If the {@code Action} throws an exception, the respective {@link Throwable} is + * delivered to the downstream via {@link MaybeObserver#onError(Throwable)}, + * except when the downstream has disposed the resulting {@code Maybe} source. + * In this latter case, the {@code Throwable} is delivered to the global error handler via + * {@link RxJavaPlugins#onError(Throwable)} as an {@link io.reactivex.rxjava3.exceptions.UndeliverableException UndeliverableException}. + * </dd> + * </dl> + * @param <T> the target type + * @param action the {@code Action} to run for each {@code MaybeObserver} + * @return the new {@code Maybe} instance + * @throws NullPointerException if {@code action} is {@code null} + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T> Maybe<T> fromAction(@NonNull Action action) { + Objects.requireNonNull(action, "action is null"); + return RxJavaPlugins.onAssembly(new MaybeFromAction<>(action)); + } + + /** + * Wraps a {@link CompletableSource} into a {@code Maybe}. + * <p> + * <img width="640" height="280" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.fromCompletable.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code fromCompletable} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <T> the target type + * @param completableSource the {@code CompletableSource} to convert from + * @return the new {@code Maybe} instance + * @throws NullPointerException if {@code completableSource} is {@code null} + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T> Maybe<T> fromCompletable(@NonNull CompletableSource completableSource) { + Objects.requireNonNull(completableSource, "completableSource is null"); + return RxJavaPlugins.onAssembly(new MaybeFromCompletable<>(completableSource)); + } + + /** + * Wraps a {@link SingleSource} into a {@code Maybe}. + * <p> + * <img width="640" height="344" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.fromSingle.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code fromSingle} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <T> the target type + * @param single the {@code SingleSource} to convert from + * @return the new {@code Maybe} instance + * @throws NullPointerException if {@code single} is {@code null} + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T> Maybe<T> fromSingle(@NonNull SingleSource<T> single) { + Objects.requireNonNull(single, "single is null"); + return RxJavaPlugins.onAssembly(new MaybeFromSingle<>(single)); + } + + /** + * Returns a {@code Maybe} that invokes the given {@link Callable} for each individual {@link MaybeObserver} that + * subscribes and emits the resulting non-{@code null} item via {@code onSuccess} while + * considering a {@code null} result from the {@code Callable} as indication for valueless completion + * via {@code onComplete}. + * <p> + * <img width="640" height="183" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.fromCallable.png" alt=""> + * <p> + * This operator allows you to defer the execution of the given {@code Callable} until a {@code MaybeObserver} + * subscribes to the returned {@code Maybe}. In other terms, this source operator evaluates the given + * {@code Callable} "lazily". + * <p> + * Note that the {@code null} handling of this operator differs from the similar source operators in the other + * {@link io.reactivex.rxjava3.core base reactive classes}. Those operators signal a {@link NullPointerException} if the value returned by their + * {@code Callable} is {@code null} while this {@code fromCallable} considers it to indicate the + * returned {@code Maybe} is empty. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code fromCallable} does not operate by default on a particular {@link Scheduler}.</dd> + * <dt><b>Error handling:</b></dt> + * <dd>Any non-fatal exception thrown by {@link Callable#call()} will be forwarded to {@code onError}, + * except if the {@code MaybeObserver} disposed the subscription in the meantime. In this latter case, + * the exception is forwarded to the global error handler via + * {@link io.reactivex.rxjava3.plugins.RxJavaPlugins#onError(Throwable)} wrapped into a + * {@link io.reactivex.rxjava3.exceptions.UndeliverableException UndeliverableException}. + * Fatal exceptions are rethrown and usually will end up in the executing thread's + * {@link java.lang.Thread.UncaughtExceptionHandler#uncaughtException(Thread, Throwable)} handler.</dd> + * </dl> + * + * @param callable + * a {@code Callable} instance whose execution should be deferred and performed for each individual + * {@code MaybeObserver} that subscribes to the returned {@code Maybe}. + * @param <T> + * the type of the item emitted by the {@code Maybe}. + * @return the new {@code Maybe} instance + * @throws NullPointerException if {@code callable} is {@code null} + * @see #defer(Supplier) + * @see #fromSupplier(Supplier) + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public static <T> Maybe<@NonNull T> fromCallable(@NonNull Callable<? extends @Nullable T> callable) { + Objects.requireNonNull(callable, "callable is null"); + return RxJavaPlugins.onAssembly(new MaybeFromCallable<>(callable)); + } + + /** + * Converts a {@link Future} into a {@code Maybe}, treating a {@code null} result as an indication of emptiness. + * <p> + * <img width="640" height="204" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.fromFuture.png" alt=""> + * <p> + * The operator calls {@link Future#get()}, which is a blocking method, on the subscription thread. + * It is recommended applying {@link #subscribeOn(Scheduler)} to move this blocking wait to a + * background thread, and if the {@link Scheduler} supports it, interrupt the wait when the flow + * is disposed. + * <p> + * Unlike 1.x, disposing the {@code Maybe} won't cancel the future. If necessary, one can use composition to achieve the + * cancellation effect: {@code futureMaybe.doOnDispose(() -> future.cancel(true));}. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code fromFuture} does not operate by default on a particular {@code Scheduler}.</dd> + * </dl> + * + * @param future + * the source {@code Future} + * @param <T> + * the type of object that the {@code Future} returns, and also the type of item to be emitted by + * the resulting {@code Maybe} + * @return the new {@code Maybe} instance + * @throws NullPointerException if {@code future} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/from.html">ReactiveX operators documentation: From</a> + * @see #fromCompletionStage(CompletionStage) + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T> Maybe<T> fromFuture(@NonNull Future<? extends T> future) { + Objects.requireNonNull(future, "future is null"); + return RxJavaPlugins.onAssembly(new MaybeFromFuture<>(future, 0L, null)); + } + + /** + * Converts a {@link Future} into a {@code Maybe}, with a timeout on the {@code Future}. + * <p> + * <img width="640" height="176" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.fromFuture.t.png" alt=""> + * <p> + * The operator calls {@link Future#get(long, TimeUnit)}, which is a blocking method, on the subscription thread. + * It is recommended applying {@link #subscribeOn(Scheduler)} to move this blocking wait to a + * background thread, and if the {@link Scheduler} supports it, interrupt the wait when the flow + * is disposed. + * <p> + * Unlike 1.x, disposing the {@code Maybe} won't cancel the future. If necessary, one can use composition to achieve the + * cancellation effect: {@code futureMaybe.doOnCancel(() -> future.cancel(true));}. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code fromFuture} does not operate by default on a particular {@code Scheduler}.</dd> + * </dl> + * + * @param future + * the source {@code Future} + * @param timeout + * the maximum time to wait before calling {@code get} + * @param unit + * the {@link TimeUnit} of the {@code timeout} argument + * @param <T> + * the type of object that the {@code Future} returns, and also the type of item to be emitted by + * the resulting {@code Maybe} + * @return the new {@code Maybe} instance + * @throws NullPointerException if {@code future} or {@code unit} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/from.html">ReactiveX operators documentation: From</a> + * @see #fromCompletionStage(CompletionStage) + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T> Maybe<T> fromFuture(@NonNull Future<? extends T> future, long timeout, @NonNull TimeUnit unit) { + Objects.requireNonNull(future, "future is null"); + Objects.requireNonNull(unit, "unit is null"); + return RxJavaPlugins.onAssembly(new MaybeFromFuture<>(future, timeout, unit)); + } + + /** + * Wraps an {@link ObservableSource} into a {@code Maybe} and emits the very first item + * or completes if the source is empty. + * <p> + * <img width="640" height="276" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.fromObservable.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code fromObservable} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <T> the target type + * @param source the {@code ObservableSource} to convert from + * @return the new {@code Maybe} instance + * @throws NullPointerException if {@code source} is {@code null} + * @since 3.0.0 + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T> Maybe<T> fromObservable(@NonNull ObservableSource<T> source) { + Objects.requireNonNull(source, "source is null"); + return RxJavaPlugins.onAssembly(new ObservableElementAtMaybe<>(source, 0L)); + } + + /** + * Wraps a {@link Publisher} into a {@code Maybe} and emits the very first item + * or completes if the source is empty. + * <p> + * <img width="640" height="309" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.fromPublisher.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator consumes the given {@code Publisher} in an unbounded manner + * (requesting {@link Long#MAX_VALUE}) but cancels it after one item received.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code fromPublisher} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <T> the target type + * @param source the {@code Publisher} to convert from + * @return the new {@code Maybe} instance + * @throws NullPointerException if {@code source} is {@code null} + * @since 3.0.0 + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + @BackpressureSupport(BackpressureKind.UNBOUNDED_IN) + public static <@NonNull T> Maybe<T> fromPublisher(@NonNull Publisher<T> source) { + Objects.requireNonNull(source, "source is null"); + return RxJavaPlugins.onAssembly(new FlowableElementAtMaybePublisher<>(source, 0L)); + } + + /** + * Returns a {@code Maybe} instance that runs the given {@link Runnable} for each {@link MaybeObserver} and + * emits either its unchecked exception or simply completes. + * <p> + * <img width="640" height="287" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.fromRunnable.png" alt=""> + * <p> + * If the code to be wrapped needs to throw a checked or more broader {@link Throwable} exception, that + * exception has to be converted to an unchecked exception by the wrapped code itself. Alternatively, + * use the {@link #fromAction(Action)} method which allows the wrapped code to throw any {@code Throwable} + * exception and will signal it to observers as-is. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code fromRunnable} does not operate by default on a particular {@link Scheduler}.</dd> + * <dt><b>Error handling:</b></dt> + * <dd> If the {@code Runnable} throws an exception, the respective {@code Throwable} is + * delivered to the downstream via {@link MaybeObserver#onError(Throwable)}, + * except when the downstream has disposed this {@code Maybe} source. + * In this latter case, the {@code Throwable} is delivered to the global error handler via + * {@link RxJavaPlugins#onError(Throwable)} as an {@link io.reactivex.rxjava3.exceptions.UndeliverableException UndeliverableException}. + * </dd> + * </dl> + * @param <T> the target type + * @param run the {@code Runnable} to run for each {@code MaybeObserver} + * @return the new {@code Maybe} instance + * @throws NullPointerException if {@code run} is {@code null} + * @see #fromAction(Action) + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T> Maybe<T> fromRunnable(@NonNull Runnable run) { + Objects.requireNonNull(run, "run is null"); + return RxJavaPlugins.onAssembly(new MaybeFromRunnable<>(run)); + } + + /** + * Returns a {@code Maybe} that invokes the given {@link Supplier} for each individual {@link MaybeObserver} that + * subscribes and emits the resulting non-{@code null} item via {@code onSuccess} while + * considering a {@code null} result from the {@code Supplier} as indication for valueless completion + * via {@code onComplete}. + * <p> + * This operator allows you to defer the execution of the given {@code Supplier} until a {@code MaybeObserver} + * subscribes to the returned {@code Maybe}. In other terms, this source operator evaluates the given + * {@code Supplier} "lazily". + * <p> + * <img width="640" height="311" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.fromSupplier.v3.png" alt=""> + * <p> + * Note that the {@code null} handling of this operator differs from the similar source operators in the other + * {@link io.reactivex.rxjava3.core base reactive classes}. Those operators signal a {@link NullPointerException} if the value returned by their + * {@code Supplier} is {@code null} while this {@code fromSupplier} considers it to indicate the + * returned {@code Maybe} is empty. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code fromSupplier} does not operate by default on a particular {@link Scheduler}.</dd> + * <dt><b>Error handling:</b></dt> + * <dd>Any non-fatal exception thrown by {@link Supplier#get()} will be forwarded to {@code onError}, + * except if the {@code MaybeObserver} disposed the subscription in the meantime. In this latter case, + * the exception is forwarded to the global error handler via + * {@link io.reactivex.rxjava3.plugins.RxJavaPlugins#onError(Throwable)} wrapped into a + * {@link io.reactivex.rxjava3.exceptions.UndeliverableException UndeliverableException}. + * Fatal exceptions are rethrown and usually will end up in the executing thread's + * {@link java.lang.Thread.UncaughtExceptionHandler#uncaughtException(Thread, Throwable)} handler.</dd> + * </dl> + * + * @param supplier + * a {@code Supplier} instance whose execution should be deferred and performed for each individual + * {@code MaybeObserver} that subscribes to the returned {@code Maybe}. + * @param <T> + * the type of the item emitted by the {@code Maybe}. + * @return the new {@code Maybe} instance + * @throws NullPointerException if {@code supplier} is {@code null} + * @see #defer(Supplier) + * @see #fromCallable(Callable) + * @since 3.0.0 + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public static <T> Maybe<@NonNull T> fromSupplier(@NonNull Supplier<? extends @Nullable T> supplier) { + Objects.requireNonNull(supplier, "supplier is null"); + return RxJavaPlugins.onAssembly(new MaybeFromSupplier<>(supplier)); + } + + /** + * Returns a {@code Maybe} that emits a specified item. + * <p> + * <img width="640" height="485" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.just.png" alt=""> + * <p> + * To convert any object into a {@code Maybe} that emits that object, pass that object into the + * {@code just} method. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code just} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param item + * the item to emit + * @param <T> + * the type of that item + * @return the new {@code Maybe} instance + * @throws NullPointerException if {@code item} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/just.html">ReactiveX operators documentation: Just</a> + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T> Maybe<T> just(T item) { + Objects.requireNonNull(item, "item is null"); + return RxJavaPlugins.onAssembly(new MaybeJust<>(item)); + } + + /** + * Merges an {@link Iterable} sequence of {@link MaybeSource} instances into a single {@link Flowable} sequence, + * running all {@code MaybeSource}s at once. + * <p> + * <img width="640" height="301" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.merge.i.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code merge} does not operate by default on a particular {@link Scheduler}.</dd> + * <dt><b>Error handling:</b></dt> + * <dd>If any of the source {@code MaybeSource}s signal a {@link Throwable} via {@code onError}, the resulting + * {@code Flowable} terminates with that {@code Throwable} and all other source {@code MaybeSource}s are disposed. + * If more than one {@code MaybeSource} signals an error, the resulting {@code Flowable} may terminate with the + * first one's error or, depending on the concurrency of the sources, may terminate with a + * {@link CompositeException} containing two or more of the various error signals. + * {@code Throwable}s that didn't make into the composite will be sent (individually) to the global error handler via + * {@link RxJavaPlugins#onError(Throwable)} method as {@link UndeliverableException} errors. Similarly, {@code Throwable}s + * signaled by source(s) after the returned {@code Flowable} has been cancelled or terminated with a + * (composite) error will be sent to the same global error handler. + * Use {@link #mergeDelayError(Iterable)} to merge sources and terminate only when all source {@code MaybeSource}s + * have completed or failed with an error. + * </dd> + * </dl> + * @param <T> the common and resulting value type + * @param sources the {@code Iterable} sequence of {@code MaybeSource} sources + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code sources} is {@code null} + * @see #mergeDelayError(Iterable) + */ + @BackpressureSupport(BackpressureKind.FULL) + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public static <@NonNull T> Flowable<T> merge(@NonNull Iterable<@NonNull ? extends MaybeSource<? extends T>> sources) { + return Flowable.fromIterable(sources).flatMapMaybe(Functions.identity(), false, Integer.MAX_VALUE); + } + + /** + * Merges a {@link Publisher} sequence of {@link MaybeSource} instances into a single {@link Flowable} sequence, + * running all {@code MaybeSource}s at once. + * <p> + * <img width="640" height="325" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.merge.p.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code merge} does not operate by default on a particular {@link Scheduler}.</dd> + * <dt><b>Error handling:</b></dt> + * <dd>If any of the source {@code MaybeSource}s signal a {@link Throwable} via {@code onError}, the resulting + * {@code Flowable} terminates with that {@code Throwable} and all other source {@code MaybeSource}s are disposed. + * If more than one {@code MaybeSource} signals an error, the resulting {@code Flowable} may terminate with the + * first one's error or, depending on the concurrency of the sources, may terminate with a + * {@link CompositeException} containing two or more of the various error signals. + * {@code Throwable}s that didn't make into the composite will be sent (individually) to the global error handler via + * {@link RxJavaPlugins#onError(Throwable)} method as {@link UndeliverableException} errors. Similarly, {@code Throwable}s + * signaled by source(s) after the returned {@code Flowable} has been cancelled or terminated with a + * (composite) error will be sent to the same global error handler. + * Use {@link #mergeDelayError(Publisher)} to merge sources and terminate only when all source {@code MaybeSource}s + * have completed or failed with an error. + * </dd> + * </dl> + * @param <T> the common and resulting value type + * @param sources the {@code Flowable} sequence of {@code MaybeSource} sources + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code sources} is {@code null} + * @see #mergeDelayError(Publisher) + */ + @BackpressureSupport(BackpressureKind.FULL) + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public static <@NonNull T> Flowable<T> merge(@NonNull Publisher<@NonNull ? extends MaybeSource<? extends T>> sources) { + return merge(sources, Integer.MAX_VALUE); + } + + /** + * Merges a {@link Publisher} sequence of {@link MaybeSource} instances into a single {@link Flowable} sequence, + * running at most maxConcurrency {@code MaybeSource}s at once. + * <p> + * <img width="640" height="260" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.merge.pn.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code merge} does not operate by default on a particular {@link Scheduler}.</dd> + * <dt><b>Error handling:</b></dt> + * <dd>If any of the source {@code MaybeSource}s signal a {@link Throwable} via {@code onError}, the resulting + * {@code Flowable} terminates with that {@code Throwable} and all other source {@code MaybeSource}s are disposed. + * If more than one {@code MaybeSource} signals an error, the resulting {@code Flowable} may terminate with the + * first one's error or, depending on the concurrency of the sources, may terminate with a + * {@link CompositeException} containing two or more of the various error signals. + * {@code Throwable}s that didn't make into the composite will be sent (individually) to the global error handler via + * {@link RxJavaPlugins#onError(Throwable)} method as {@link UndeliverableException} errors. Similarly, {@code Throwable}s + * signaled by source(s) after the returned {@code Flowable} has been cancelled or terminated with a + * (composite) error will be sent to the same global error handler. + * Use {@link #mergeDelayError(Publisher, int)} to merge sources and terminate only when all source {@code MaybeSource}s + * have completed or failed with an error. + * </dd> + * </dl> + * @param <T> the common and resulting value type + * @param sources the {@code Flowable} sequence of {@code MaybeSource} sources + * @param maxConcurrency the maximum number of concurrently running {@code MaybeSource}s + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code sources} is {@code null} + * @throws IllegalArgumentException if {@code maxConcurrency} is non-positive + * @see #mergeDelayError(Publisher, int) + */ + @BackpressureSupport(BackpressureKind.FULL) + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T> Flowable<T> merge(@NonNull Publisher<@NonNull ? extends MaybeSource<? extends T>> sources, int maxConcurrency) { + Objects.requireNonNull(sources, "sources is null"); + ObjectHelper.verifyPositive(maxConcurrency, "maxConcurrency"); + return RxJavaPlugins.onAssembly(new FlowableFlatMapMaybePublisher<>(sources, Functions.identity(), false, maxConcurrency)); + } + + /** + * Flattens a {@link MaybeSource} that emits a {@code MaybeSource} into a single {@code MaybeSource} that emits the item + * emitted by the nested {@code MaybeSource}, without any transformation. + * <p> + * <img width="640" height="394" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.merge.oo.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code merge} does not operate by default on a particular {@link Scheduler}.</dd> + * <dt><b>Error handling:</b></dt> + * <dd>The resulting {@code Maybe} emits the outer source's or the inner {@code MaybeSource}'s {@link Throwable} as is. + * Unlike the other {@code merge()} operators, this operator won't and can't produce a {@link CompositeException} because there is + * only one possibility for the outer or the inner {@code MaybeSource} to emit an {@code onError} signal. + * Therefore, there is no need for a {@code mergeDelayError(MaybeSource<MaybeSource<T>>)} operator. + * </dd> + * </dl> + * + * @param <T> the value type of the sources and the output + * @param source + * a {@code MaybeSource} that emits a {@code MaybeSource} + * @return the new {@code Maybe} instance + * @throws NullPointerException if {@code source} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/merge.html">ReactiveX operators documentation: Merge</a> + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + @SuppressWarnings({ "unchecked", "rawtypes" }) + public static <@NonNull T> Maybe<T> merge(@NonNull MaybeSource<? extends MaybeSource<? extends T>> source) { + Objects.requireNonNull(source, "source is null"); + return RxJavaPlugins.onAssembly(new MaybeFlatten(source, Functions.identity())); + } + + /** + * Flattens two {@link MaybeSource}s into a single {@link Flowable}, without any transformation. + * <p> + * <img width="640" height="279" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.merge.2.png" alt=""> + * <p> + * You can combine items emitted by multiple {@code MaybeSource}s so that they appear as a single {@code Flowable}, by + * using the {@code merge} method. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code merge} does not operate by default on a particular {@link Scheduler}.</dd> + * <dt><b>Error handling:</b></dt> + * <dd>If any of the source {@code MaybeSource}s signal a {@link Throwable} via {@code onError}, the resulting + * {@code Flowable} terminates with that {@code Throwable} and all other source {@code MaybeSource}s are disposed. + * If more than one {@code MaybeSource} signals an error, the resulting {@code Flowable} may terminate with the + * first one's error or, depending on the concurrency of the sources, may terminate with a + * {@link CompositeException} containing two or more of the various error signals. + * {@code Throwable}s that didn't make into the composite will be sent (individually) to the global error handler via + * {@link RxJavaPlugins#onError(Throwable)} method as {@link UndeliverableException} errors. Similarly, {@code Throwable}s + * signaled by source(s) after the returned {@code Flowable} has been cancelled or terminated with a + * (composite) error will be sent to the same global error handler. + * Use {@link #mergeDelayError(MaybeSource, MaybeSource)} to merge sources and terminate only when all source {@code MaybeSource}s + * have completed or failed with an error. + * </dd> + * </dl> + * + * @param <T> the common value type + * @param source1 + * a {@code MaybeSource} to be merged + * @param source2 + * a {@code MaybeSource} to be merged + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code source1} or {@code source2} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/merge.html">ReactiveX operators documentation: Merge</a> + * @see #mergeDelayError(MaybeSource, MaybeSource) + */ + @BackpressureSupport(BackpressureKind.FULL) + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T> Flowable<T> merge( + @NonNull MaybeSource<? extends T> source1, @NonNull MaybeSource<? extends T> source2 + ) { + Objects.requireNonNull(source1, "source1 is null"); + Objects.requireNonNull(source2, "source2 is null"); + return mergeArray(source1, source2); + } + + /** + * Flattens three {@link MaybeSource}s into a single {@link Flowable}, without any transformation. + * <p> + * <img width="640" height="301" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.merge.3.png" alt=""> + * <p> + * You can combine items emitted by multiple {@code MaybeSource}s so that they appear as a single {@code Flowable}, by using + * the {@code merge} method. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code merge} does not operate by default on a particular {@link Scheduler}.</dd> + * <dt><b>Error handling:</b></dt> + * <dd>If any of the source {@code MaybeSource}s signal a {@link Throwable} via {@code onError}, the resulting + * {@code Flowable} terminates with that {@code Throwable} and all other source {@code MaybeSource}s are disposed. + * If more than one {@code MaybeSource} signals an error, the resulting {@code Flowable} may terminate with the + * first one's error or, depending on the concurrency of the sources, may terminate with a + * {@link CompositeException} containing two or more of the various error signals. + * {@code Throwable}s that didn't make into the composite will be sent (individually) to the global error handler via + * {@link RxJavaPlugins#onError(Throwable)} method as {@link UndeliverableException} errors. Similarly, {@code Throwable}s + * signaled by source(s) after the returned {@code Flowable} has been cancelled or terminated with a + * (composite) error will be sent to the same global error handler. + * Use {@link #mergeDelayError(MaybeSource, MaybeSource, MaybeSource)} to merge sources and terminate only when all source {@code MaybeSource}s + * have completed or failed with an error. + * </dd> + * </dl> + * + * @param <T> the common value type + * @param source1 + * a {@code MaybeSource} to be merged + * @param source2 + * a {@code MaybeSource} to be merged + * @param source3 + * a {@code MaybeSource} to be merged + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code source1}, {@code source2} or {@code source3} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/merge.html">ReactiveX operators documentation: Merge</a> + * @see #mergeDelayError(MaybeSource, MaybeSource, MaybeSource) + */ + @BackpressureSupport(BackpressureKind.FULL) + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T> Flowable<T> merge( + @NonNull MaybeSource<? extends T> source1, @NonNull MaybeSource<? extends T> source2, + @NonNull MaybeSource<? extends T> source3 + ) { + Objects.requireNonNull(source1, "source1 is null"); + Objects.requireNonNull(source2, "source2 is null"); + Objects.requireNonNull(source3, "source3 is null"); + return mergeArray(source1, source2, source3); + } + + /** + * Flattens four {@link MaybeSource}s into a single {@link Flowable}, without any transformation. + * <p> + * <img width="640" height="289" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.merge.4.png" alt=""> + * <p> + * You can combine items emitted by multiple {@code MaybeSource}s so that they appear as a single {@code Flowable}, by using + * the {@code merge} method. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code merge} does not operate by default on a particular {@link Scheduler}.</dd> + * <dt><b>Error handling:</b></dt> + * <dd>If any of the source {@code MaybeSource}s signal a {@link Throwable} via {@code onError}, the resulting + * {@code Flowable} terminates with that {@code Throwable} and all other source {@code MaybeSource}s are disposed. + * If more than one {@code MaybeSource} signals an error, the resulting {@code Flowable} may terminate with the + * first one's error or, depending on the concurrency of the sources, may terminate with a + * {@link CompositeException} containing two or more of the various error signals. + * {@code Throwable}s that didn't make into the composite will be sent (individually) to the global error handler via + * {@link RxJavaPlugins#onError(Throwable)} method as {@link UndeliverableException} errors. Similarly, {@code Throwable}s + * signaled by source(s) after the returned {@code Flowable} has been cancelled or terminated with a + * (composite) error will be sent to the same global error handler. + * Use {@link #mergeDelayError(MaybeSource, MaybeSource, MaybeSource, MaybeSource)} to merge sources and terminate only when all source {@code MaybeSource}s + * have completed or failed with an error. + * </dd> + * </dl> + * + * @param <T> the common value type + * @param source1 + * a {@code MaybeSource} to be merged + * @param source2 + * a {@code MaybeSource} to be merged + * @param source3 + * a {@code MaybeSource} to be merged + * @param source4 + * a {@code MaybeSource} to be merged + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code source1}, {@code source2}, {@code source3} or {@code source4} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/merge.html">ReactiveX operators documentation: Merge</a> + * @see #mergeDelayError(MaybeSource, MaybeSource, MaybeSource, MaybeSource) + */ + @BackpressureSupport(BackpressureKind.FULL) + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T> Flowable<T> merge( + @NonNull MaybeSource<? extends T> source1, @NonNull MaybeSource<? extends T> source2, + @NonNull MaybeSource<? extends T> source3, @NonNull MaybeSource<? extends T> source4 + ) { + Objects.requireNonNull(source1, "source1 is null"); + Objects.requireNonNull(source2, "source2 is null"); + Objects.requireNonNull(source3, "source3 is null"); + Objects.requireNonNull(source4, "source4 is null"); + return mergeArray(source1, source2, source3, source4); + } + + /** + * Merges an array of {@link MaybeSource} instances into a single {@link Flowable} sequence, + * running all {@code MaybeSource}s at once. + * <p> + * <img width="640" height="272" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.mergeArray.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code mergeArray} does not operate by default on a particular {@link Scheduler}.</dd> + * <dt><b>Error handling:</b></dt> + * <dd>If any of the source {@code MaybeSource}s signal a {@link Throwable} via {@code onError}, the resulting + * {@code Flowable} terminates with that {@code Throwable} and all other source {@code MaybeSource}s are disposed. + * If more than one {@code MaybeSource} signals an error, the resulting {@code Flowable} may terminate with the + * first one's error or, depending on the concurrency of the sources, may terminate with a + * {@link CompositeException} containing two or more of the various error signals. + * {@code Throwable}s that didn't make into the composite will be sent (individually) to the global error handler via + * {@link RxJavaPlugins#onError(Throwable)} method as {@link UndeliverableException} errors. Similarly, {@code Throwable}s + * signaled by source(s) after the returned {@code Flowable} has been cancelled or terminated with a + * (composite) error will be sent to the same global error handler. + * Use {@link #mergeArrayDelayError(MaybeSource...)} to merge sources and terminate only when all source {@code MaybeSource}s + * have completed or failed with an error. + * </dd> + * </dl> + * @param <T> the common and resulting value type + * @param sources the array sequence of {@code MaybeSource} sources + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code sources} is {@code null} + * @see #mergeArrayDelayError(MaybeSource...) + */ + @BackpressureSupport(BackpressureKind.FULL) + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + @SafeVarargs + public static <@NonNull T> Flowable<T> mergeArray(MaybeSource<? extends T>... sources) { + Objects.requireNonNull(sources, "sources is null"); + if (sources.length == 0) { + return Flowable.empty(); + } + if (sources.length == 1) { + @SuppressWarnings("unchecked") + MaybeSource<T> source = (MaybeSource<T>)sources[0]; + return RxJavaPlugins.onAssembly(new MaybeToFlowable<>(source)); + } + return RxJavaPlugins.onAssembly(new MaybeMergeArray<>(sources)); + } + + /** + * Flattens an array of {@link MaybeSource}s into one {@link Flowable}, in a way that allows a subscriber to receive all + * successfully emitted items from each of the source {@code MaybeSource}s without being interrupted by an error + * notification from one of them. + * <p> + * <img width="640" height="422" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.mergeArrayDelayError.png" alt=""> + * <p> + * This behaves like {@link #merge(Publisher)} except that if any of the merged {@code MaybeSource}s notify of an + * error via {@link Subscriber#onError onError}, {@code mergeArrayDelayError} will refrain from propagating that + * error notification until all of the merged {@code MaybeSource}s have finished emitting items. + * <p> + * Even if multiple merged {@code MaybeSource}s send {@code onError} notifications, {@code mergeArrayDelayError} will only + * invoke the {@code onError} method of its subscribers once. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code mergeArrayDelayError} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T> the common element base type + * @param sources + * the array of {@code MaybeSource}s + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code sources} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/merge.html">ReactiveX operators documentation: Merge</a> + */ + @BackpressureSupport(BackpressureKind.FULL) + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @SafeVarargs + @NonNull + public static <@NonNull T> Flowable<T> mergeArrayDelayError(@NonNull MaybeSource<? extends T>... sources) { + Objects.requireNonNull(sources, "sources is null"); + return Flowable.fromArray(sources).flatMapMaybe(Functions.identity(), true, Math.max(1, sources.length)); + } + + /** + * Flattens an {@link Iterable} sequence of {@link MaybeSource}s into one {@link Flowable}, in a way that allows a subscriber to receive all + * successfully emitted items from each of the source {@code MaybeSource}s without being interrupted by an error + * notification from one of them. + * <p> + * <img width="640" height="467" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.mergeDelayError.i.png" alt=""> + * <p> + * This behaves like {@link #merge(Publisher)} except that if any of the merged {@code MaybeSource}s notify of an + * error via {@link Subscriber#onError onError}, {@code mergeDelayError} will refrain from propagating that + * error notification until all of the merged {@code MaybeSource}s have finished emitting items. + * <p> + * <img width="640" height="467" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.mergeDelayError.i.png" alt=""> + * <p> + * Even if multiple merged {@code MaybeSource}s send {@code onError} notifications, {@code mergeDelayError} will only + * invoke the {@code onError} method of its subscribers once. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code mergeDelayError} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T> the common element base type + * @param sources + * the {@code Iterable} of {@code MaybeSource}s + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code sources} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/merge.html">ReactiveX operators documentation: Merge</a> + */ + @BackpressureSupport(BackpressureKind.FULL) + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public static <@NonNull T> Flowable<T> mergeDelayError(@NonNull Iterable<@NonNull ? extends MaybeSource<? extends T>> sources) { + return Flowable.fromIterable(sources).flatMapMaybe(Functions.identity(), true, Integer.MAX_VALUE); + } + + /** + * Flattens a {@link Publisher} that emits {@link MaybeSource}s into one {@link Flowable}, in a way that allows a subscriber to + * receive all successfully emitted items from all of the source {@code MaybeSource}s without being interrupted by + * an error notification from one of them or even the main {@code Publisher}. + * <p> + * <img width="640" height="456" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.mergeDelayError.p.png" alt=""> + * <p> + * This behaves like {@link #merge(Publisher)} except that if any of the merged {@code MaybeSource}s notify of an + * error via {@link Subscriber#onError onError}, {@code mergeDelayError} will refrain from propagating that + * error notification until all of the merged {@code MaybeSource}s and the main {@code Publisher} have finished emitting items. + * <p> + * Even if multiple merged {@code MaybeSource}s send {@code onError} notifications, {@code mergeDelayError} will only + * invoke the {@code onError} method of its subscribers once. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream. The outer {@code Publisher} is consumed + * in unbounded mode (i.e., no backpressure is applied to it).</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code mergeDelayError} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T> the common element base type + * @param sources + * a {@code Publisher} that emits {@code MaybeSource}s + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code sources} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/merge.html">ReactiveX operators documentation: Merge</a> + */ + @BackpressureSupport(BackpressureKind.FULL) + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public static <@NonNull T> Flowable<T> mergeDelayError(@NonNull Publisher<@NonNull ? extends MaybeSource<? extends T>> sources) { + return mergeDelayError(sources, Integer.MAX_VALUE); + } + + /** + * Flattens a {@link Publisher} that emits {@link MaybeSource}s into one {@link Flowable}, in a way that allows a subscriber to + * receive all successfully emitted items from all of the source {@code MaybeSource}s without being interrupted by + * an error notification from one of them or even the main {@code Publisher} as well as limiting the total number of active {@code MaybeSource}s. + * <p> + * <img width="640" height="429" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.mergeDelayError.pn.png" alt=""> + * <p> + * This behaves like {@link #merge(Publisher, int)} except that if any of the merged {@code MaybeSource}s notify of an + * error via {@link Subscriber#onError onError}, {@code mergeDelayError} will refrain from propagating that + * error notification until all of the merged {@code MaybeSource}s and the main {@code Publisher} have finished emitting items. + * <p> + * Even if multiple merged {@code MaybeSource}s send {@code onError} notifications, {@code mergeDelayError} will only + * invoke the {@code onError} method of its subscribers once. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream. The outer {@code Publisher} is consumed + * in unbounded mode (i.e., no backpressure is applied to it).</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code mergeDelayError} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * <p>History: 2.1.9 - experimental + * @param <T> the common element base type + * @param sources + * a {@code Publisher} that emits {@code MaybeSource}s + * @param maxConcurrency the maximum number of active inner {@code MaybeSource}s to be merged at a time + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code sources} is {@code null} + * @throws IllegalArgumentException if {@code maxConcurrency} is non-positive + * @see <a href="http://reactivex.io/documentation/operators/merge.html">ReactiveX operators documentation: Merge</a> + * @since 2.2 + */ + @BackpressureSupport(BackpressureKind.FULL) + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T> Flowable<T> mergeDelayError(@NonNull Publisher<@NonNull ? extends MaybeSource<? extends T>> sources, int maxConcurrency) { + Objects.requireNonNull(sources, "sources is null"); + ObjectHelper.verifyPositive(maxConcurrency, "maxConcurrency"); + return RxJavaPlugins.onAssembly(new FlowableFlatMapMaybePublisher<>(sources, Functions.identity(), true, maxConcurrency)); + } + + /** + * Flattens two {@link MaybeSource}s into one {@link Flowable}, in a way that allows a subscriber to receive all + * successfully emitted items from each of the source {@code MaybeSource}s without being interrupted by an error + * notification from one of them. + * <p> + * <img width="640" height="414" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.mergeDelayError.2.png" alt=""> + * <p> + * This behaves like {@link #merge(MaybeSource, MaybeSource)} except that if any of the merged {@code MaybeSource}s + * notify of an error via {@link Subscriber#onError onError}, {@code mergeDelayError} will refrain from + * propagating that error notification until all of the merged {@code MaybeSource}s have finished emitting items. + * <p> + * Even if both merged {@code MaybeSource}s send {@code onError} notifications, {@code mergeDelayError} will only + * invoke the {@code onError} method of its subscribers once. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code mergeDelayError} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T> the common element base type + * @param source1 + * a {@code MaybeSource} to be merged + * @param source2 + * a {@code MaybeSource} to be merged + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code source1} or {@code source2} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/merge.html">ReactiveX operators documentation: Merge</a> + */ + @BackpressureSupport(BackpressureKind.FULL) + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T> Flowable<T> mergeDelayError(@NonNull MaybeSource<? extends T> source1, @NonNull MaybeSource<? extends T> source2) { + Objects.requireNonNull(source1, "source1 is null"); + Objects.requireNonNull(source2, "source2 is null"); + return mergeArrayDelayError(source1, source2); + } + + /** + * Flattens three {@link MaybeSource} into one {@link Flowable}, in a way that allows a subscriber to receive all + * successfully emitted items from all of the source {@code MaybeSource}s without being interrupted by an error + * notification from one of them. + * <p> + * <img width="640" height="467" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.mergeDelayError.3.png" alt=""> + * <p> + * This behaves like {@link #merge(MaybeSource, MaybeSource, MaybeSource)} except that if any of the merged + * {@code MaybeSource}s notify of an error via {@link Subscriber#onError onError}, {@code mergeDelayError} will refrain + * from propagating that error notification until all of the merged {@code MaybeSource}s have finished emitting + * items. + * <p> + * Even if multiple merged {@code MaybeSource}s send {@code onError} notifications, {@code mergeDelayError} will only + * invoke the {@code onError} method of its subscribers once. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code mergeDelayError} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T> the common element base type + * @param source1 + * a {@code MaybeSource} to be merged + * @param source2 + * a {@code MaybeSource} to be merged + * @param source3 + * a {@code MaybeSource} to be merged + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code source1}, {@code source2} or {@code source3} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/merge.html">ReactiveX operators documentation: Merge</a> + */ + @BackpressureSupport(BackpressureKind.FULL) + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T> Flowable<T> mergeDelayError(@NonNull MaybeSource<? extends T> source1, + @NonNull MaybeSource<? extends T> source2, @NonNull MaybeSource<? extends T> source3) { + Objects.requireNonNull(source1, "source1 is null"); + Objects.requireNonNull(source2, "source2 is null"); + Objects.requireNonNull(source3, "source3 is null"); + return mergeArrayDelayError(source1, source2, source3); + } + + /** + * Flattens four {@link MaybeSource}s into one {@link Flowable}, in a way that allows a subscriber to receive all + * successfully emitted items from all of the source {@code MaybeSource}s without being interrupted by an error + * notification from one of them. + * <p> + * <img width="640" height="461" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.mergeDelayError.4.png" alt=""> + * <p> + * This behaves like {@link #merge(MaybeSource, MaybeSource, MaybeSource, MaybeSource)} except that if any of + * the merged {@code MaybeSource}s notify of an error via {@link Subscriber#onError onError}, {@code mergeDelayError} + * will refrain from propagating that error notification until all of the merged {@code MaybeSource}s have finished + * emitting items. + * <p> + * Even if multiple merged {@code MaybeSource}s send {@code onError} notifications, {@code mergeDelayError} will only + * invoke the {@code onError} method of its subscribers once. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code mergeDelayError} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T> the common element base type + * @param source1 + * a {@code MaybeSource} to be merged + * @param source2 + * a {@code MaybeSource} to be merged + * @param source3 + * a {@code MaybeSource} to be merged + * @param source4 + * a {@code MaybeSource} to be merged + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code source1}, {@code source2}, {@code source3} or {@code source4} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/merge.html">ReactiveX operators documentation: Merge</a> + */ + @BackpressureSupport(BackpressureKind.FULL) + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T> Flowable<T> mergeDelayError( + @NonNull MaybeSource<? extends T> source1, @NonNull MaybeSource<? extends T> source2, + @NonNull MaybeSource<? extends T> source3, @NonNull MaybeSource<? extends T> source4) { + Objects.requireNonNull(source1, "source1 is null"); + Objects.requireNonNull(source2, "source2 is null"); + Objects.requireNonNull(source3, "source3 is null"); + Objects.requireNonNull(source4, "source4 is null"); + return mergeArrayDelayError(source1, source2, source3, source4); + } + + /** + * Returns a {@code Maybe} that never sends any items or notifications to a {@link MaybeObserver}. + * <p> + * <img width="640" height="185" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/never.v3.png" alt=""> + * <p> + * This {@code Maybe} is useful primarily for testing purposes. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code never} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T> + * the type of items (not) emitted by the {@code Maybe} + * @return the shared {@code Maybe} instance + * @see <a href="http://reactivex.io/documentation/operators/empty-never-throw.html">ReactiveX operators documentation: Never</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @SuppressWarnings("unchecked") + @NonNull + public static <@NonNull T> Maybe<T> never() { + return RxJavaPlugins.onAssembly((Maybe<T>)MaybeNever.INSTANCE); + } + + /** + * Returns a {@link Single} that emits a {@link Boolean} value that indicates whether two {@link MaybeSource} sequences are the + * same by comparing the items emitted by each {@code MaybeSource} pairwise. + * <p> + * <img width="640" height="187" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.sequenceEqual.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code sequenceEqual} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param source1 + * the first {@code MaybeSource} to compare + * @param source2 + * the second {@code MaybeSource} to compare + * @param <T> + * the type of items emitted by each {@code MaybeSource} + * @return the new {@code Single} instance + * @throws NullPointerException if {@code source1} or {@code source2} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/sequenceequal.html">ReactiveX operators documentation: SequenceEqual</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public static <@NonNull T> Single<Boolean> sequenceEqual(@NonNull MaybeSource<? extends T> source1, @NonNull MaybeSource<? extends T> source2) { + return sequenceEqual(source1, source2, ObjectHelper.equalsPredicate()); + } + + /** + * Returns a {@link Single} that emits a {@link Boolean} value that indicates whether two {@link MaybeSource}s are the + * same by comparing the items emitted by each {@code MaybeSource} pairwise based on the results of a specified + * equality function. + * <p> + * <img width="640" height="247" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.sequenceEqual.f.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code sequenceEqual} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param source1 + * the first {@code MaybeSource} to compare + * @param source2 + * the second {@code MaybeSource} to compare + * @param isEqual + * a function used to compare items emitted by each {@code MaybeSource} + * @param <T> + * the type of items emitted by each {@code MaybeSource} + * @return the new {@code Single} instance + * @throws NullPointerException if {@code source1}, {@code source2} or {@code isEqual} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/sequenceequal.html">ReactiveX operators documentation: SequenceEqual</a> + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T> Single<Boolean> sequenceEqual(@NonNull MaybeSource<? extends T> source1, @NonNull MaybeSource<? extends T> source2, + @NonNull BiPredicate<? super T, ? super T> isEqual) { + Objects.requireNonNull(source1, "source1 is null"); + Objects.requireNonNull(source2, "source2 is null"); + Objects.requireNonNull(isEqual, "isEqual is null"); + return RxJavaPlugins.onAssembly(new MaybeEqualSingle<>(source1, source2, isEqual)); + } + + /** + * Switches between {@link MaybeSource}s emitted by the source {@link Publisher} whenever + * a new {@code MaybeSource} is emitted, disposing the previously running {@code MaybeSource}, + * exposing the success items as a {@link Flowable} sequence. + * <p> + * <img width="640" height="521" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.switchOnNext.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The {@code sources} {@code Publisher} is consumed in an unbounded manner (requesting {@link Long#MAX_VALUE}). + * The returned {@code Flowable} respects the backpressure from the downstream.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code switchOnNext} does not operate by default on a particular {@link Scheduler}.</dd> + * <dt><b>Error handling:</b></dt> + * <dd>The returned sequence fails with the first error signaled by the {@code sources} {@code Publisher} + * or the currently running {@code MaybeSource}, disposing the rest. Late errors are + * forwarded to the global error handler via {@link RxJavaPlugins#onError(Throwable)}.</dd> + * </dl> + * @param <T> the element type of the {@code MaybeSource}s + * @param sources the {@code Publisher} sequence of inner {@code MaybeSource}s to switch between + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code sources} is {@code null} + * @since 3.0.0 + * @see #switchOnNextDelayError(Publisher) + * @see <a href="http://reactivex.io/documentation/operators/switch.html">ReactiveX operators documentation: Switch</a> + */ + @BackpressureSupport(BackpressureKind.UNBOUNDED_IN) + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T> Flowable<T> switchOnNext(@NonNull Publisher<@NonNull ? extends MaybeSource<? extends T>> sources) { + Objects.requireNonNull(sources, "sources is null"); + return RxJavaPlugins.onAssembly(new FlowableSwitchMapMaybePublisher<>(sources, Functions.identity(), false)); + } + + /** + * Switches between {@link MaybeSource}s emitted by the source {@link Publisher} whenever + * a new {@code MaybeSource} is emitted, disposing the previously running {@code MaybeSource}, + * exposing the success items as a {@link Flowable} sequence and delaying all errors from + * all of them until all terminate. + * <p> + * <img width="640" height="423" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.switchOnNextDelayError.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The {@code sources} {@code Publisher} is consumed in an unbounded manner (requesting {@link Long#MAX_VALUE}). + * The returned {@code Flowable} respects the backpressure from the downstream.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code switchOnNextDelayError} does not operate by default on a particular {@link Scheduler}.</dd> + * <dt><b>Error handling:</b></dt> + * <dd>The returned {@code Flowable} collects all errors emitted by either the {@code sources} + * {@code Publisher} or any inner {@code MaybeSource} and emits them as a {@link CompositeException} + * when all sources terminate. If only one source ever failed, its error is emitted as-is at the end.</dd> + * </dl> + * @param <T> the element type of the {@code MaybeSource}s + * @param sources the {@code Publisher} sequence of inner {@code MaybeSource}s to switch between + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code sources} is {@code null} + * @since 3.0.0 + * @see #switchOnNext(Publisher) + * @see <a href="http://reactivex.io/documentation/operators/switch.html">ReactiveX operators documentation: Switch</a> + */ + @BackpressureSupport(BackpressureKind.FULL) + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T> Flowable<T> switchOnNextDelayError(@NonNull Publisher<@NonNull ? extends MaybeSource<? extends T>> sources) { + Objects.requireNonNull(sources, "sources is null"); + return RxJavaPlugins.onAssembly(new FlowableSwitchMapMaybePublisher<>(sources, Functions.identity(), true)); + } + + /** + * Returns a {@code Maybe} that emits {@code 0L} after a specified delay. + * <p> + * <img width="640" height="391" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.timer.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code timer} operates by default on the {@code computation} {@link Scheduler}.</dd> + * </dl> + * + * @param delay + * the initial delay before emitting a single {@code 0L} + * @param unit + * time units to use for {@code delay} + * @return the new {@code Maybe} instance + * @throws NullPointerException if {@code unit} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/timer.html">ReactiveX operators documentation: Timer</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.COMPUTATION) + @NonNull + public static Maybe<Long> timer(long delay, @NonNull TimeUnit unit) { + return timer(delay, unit, Schedulers.computation()); + } + + /** + * Returns a {@code Maybe} that emits {@code 0L} after a specified delay on a specified {@link Scheduler}. + * <p> + * <img width="640" height="392" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.timer.s.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>You specify which {@code Scheduler} this operator will use.</dd> + * </dl> + * + * @param delay + * the initial delay before emitting a single 0L + * @param unit + * time units to use for {@code delay} + * @param scheduler + * the {@code Scheduler} to use for scheduling the item + * @return the new {@code Maybe} instance + * @throws NullPointerException if {@code unit} or {@code scheduler} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/timer.html">ReactiveX operators documentation: Timer</a> + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.CUSTOM) + public static Maybe<Long> timer(long delay, @NonNull TimeUnit unit, @NonNull Scheduler scheduler) { + Objects.requireNonNull(unit, "unit is null"); + Objects.requireNonNull(scheduler, "scheduler is null"); + + return RxJavaPlugins.onAssembly(new MaybeTimer(Math.max(0L, delay), unit, scheduler)); + } + + /** + * <strong>Advanced use only:</strong> creates a {@code Maybe} instance without + * any safeguards by using a callback that is called with a {@link MaybeObserver}. + * <p> + * <img width="640" height="262" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.unsafeCreate.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code unsafeCreate} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <T> the value type + * @param onSubscribe the function that is called with the subscribing {@code MaybeObserver} + * @return the new {@code Maybe} instance + * @throws IllegalArgumentException if {@code onSubscribe} is a {@code Maybe} + * @throws NullPointerException if {@code onSubscribe} is {@code null} + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T> Maybe<T> unsafeCreate(@NonNull MaybeSource<T> onSubscribe) { + if (onSubscribe instanceof Maybe) { + throw new IllegalArgumentException("unsafeCreate(Maybe) should be upgraded"); + } + Objects.requireNonNull(onSubscribe, "onSubscribe is null"); + return RxJavaPlugins.onAssembly(new MaybeUnsafeCreate<>(onSubscribe)); + } + + /** + * Constructs a {@code Maybe} that creates a dependent resource object which is disposed of when the + * generated {@link MaybeSource} terminates or the downstream calls dispose(). + * <p> + * <img width="640" height="378" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.using.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code using} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T> the element type of the generated {@code MaybeSource} + * @param <D> the type of the resource associated with the output sequence + * @param resourceSupplier + * the factory function to create a resource object that depends on the {@code Maybe} + * @param sourceSupplier + * the factory function to create a {@code MaybeSource} + * @param resourceCleanup + * the function that will dispose of the resource + * @return the new {@code Maybe} instance + * @throws NullPointerException if {@code resourceSupplier}, {@code sourceSupplier} or {@code resourceCleanup} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/using.html">ReactiveX operators documentation: Using</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public static <@NonNull T, @NonNull D> Maybe<T> using(@NonNull Supplier<? extends D> resourceSupplier, + @NonNull Function<? super D, ? extends MaybeSource<? extends T>> sourceSupplier, + @NonNull Consumer<? super D> resourceCleanup) { + return using(resourceSupplier, sourceSupplier, resourceCleanup, true); + } + + /** + * Constructs a {@code Maybe} that creates a dependent resource object which is disposed first ({code eager == true}) + * when the generated {@link MaybeSource} terminates or the downstream disposes; or after ({code eager == false}). + * <p> + * <img width="640" height="323" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.using.b.png" alt=""> + * <p> + * Eager disposal is particularly appropriate for a synchronous {@code Maybe} that reuses resources. {@code disposeAction} will + * only be called once per subscription. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code using} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T> the element type of the generated {@code MaybeSource} + * @param <D> the type of the resource associated with the output sequence + * @param resourceSupplier + * the factory function to create a resource object that depends on the {@code Maybe} + * @param sourceSupplier + * the factory function to create a {@code MaybeSource} + * @param resourceCleanup + * the function that will dispose of the resource + * @param eager + * If {@code true} then resource disposal will happen either on a {@code dispose()} call before the upstream is disposed + * or just before the emission of a terminal event ({@code onSuccess}, {@code onComplete} or {@code onError}). + * If {@code false} the resource disposal will happen either on a {@code dispose()} call after the upstream is disposed + * or just after the emission of a terminal event ({@code onSuccess}, {@code onComplete} or {@code onError}). + * @return the new {@code Maybe} instance + * @throws NullPointerException if {@code resourceSupplier}, {@code sourceSupplier} or {@code resourceCleanup} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/using.html">ReactiveX operators documentation: Using</a> + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T, @NonNull D> Maybe<T> using(@NonNull Supplier<? extends D> resourceSupplier, + @NonNull Function<? super D, ? extends MaybeSource<? extends T>> sourceSupplier, + @NonNull Consumer<? super D> resourceCleanup, boolean eager) { + Objects.requireNonNull(resourceSupplier, "resourceSupplier is null"); + Objects.requireNonNull(sourceSupplier, "sourceSupplier is null"); + Objects.requireNonNull(resourceCleanup, "resourceCleanup is null"); + return RxJavaPlugins.onAssembly(new MaybeUsing<T, D>(resourceSupplier, sourceSupplier, resourceCleanup, eager)); + } + + /** + * Wraps a {@link MaybeSource} instance into a new {@code Maybe} instance if not already a {@code Maybe} + * instance. + * <p> + * <img width="640" height="232" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.wrap.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code wrap} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <T> the value type + * @param source the source to wrap + * @return the new wrapped or cast {@code Maybe} instance + * @throws NullPointerException if {@code source} is {@code null} + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T> Maybe<T> wrap(@NonNull MaybeSource<T> source) { + if (source instanceof Maybe) { + return RxJavaPlugins.onAssembly((Maybe<T>)source); + } + Objects.requireNonNull(source, "source is null"); + return RxJavaPlugins.onAssembly(new MaybeUnsafeCreate<>(source)); + } + + /** + * Returns a {@code Maybe} that emits the results of a specified combiner function applied to combinations of + * items emitted, in sequence, by an {@link Iterable} of other {@link MaybeSource}s. + * <p> + * <img width="640" height="341" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.zip.i.png" alt=""> + * <p> + * Note on method signature: since Java doesn't allow creating a generic array with {@code new T[]}, the + * implementation of this operator has to create an {@code Object[]} instead. Unfortunately, a + * {@code Function<Integer[], R>} passed to the method would trigger a {@link ClassCastException}. + * <p> + * This operator terminates eagerly if any of the source {@code MaybeSource}s signal an {@code onError} or {@code onComplete}. This + * also means it is possible some sources may not get subscribed to at all. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code zip} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T> the common value type + * @param <R> the zipped result type + * @param sources + * an {@code Iterable} of source {@code MaybeSource}s + * @param zipper + * a function that, when applied to an item emitted by each of the source {@code MaybeSource}s, results in + * an item that will be emitted by the resulting {@code Maybe} + * @return the new {@code Maybe} instance + * @throws NullPointerException if {@code zipper} or {@code sources} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/zip.html">ReactiveX operators documentation: Zip</a> + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T, @NonNull R> Maybe<R> zip(@NonNull Iterable<@NonNull ? extends MaybeSource<? extends T>> sources, @NonNull Function<? super Object[], ? extends R> zipper) { + Objects.requireNonNull(zipper, "zipper is null"); + Objects.requireNonNull(sources, "sources is null"); + return RxJavaPlugins.onAssembly(new MaybeZipIterable<>(sources, zipper)); + } + + /** + * Returns a {@code Maybe} that emits the results of a specified combiner function applied to combinations of + * two items emitted, in sequence, by two other {@link MaybeSource}s. + * <p> + * <img width="640" height="434" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.zip.n.png" alt=""> + * <p> + * This operator terminates eagerly if any of the source {@code MaybeSource}s signal an {@code onError} or {@code onComplete}. This + * also means it is possible some sources may not get subscribed to at all. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code zip} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T1> the value type of the first source + * @param <T2> the value type of the second source + * @param <R> the zipped result type + * @param source1 + * the first source {@code MaybeSource} + * @param source2 + * a second source {@code MaybeSource} + * @param zipper + * a function that, when applied to an item emitted by each of the source {@code MaybeSource}s, results + * in an item that will be emitted by the resulting {@code Maybe} + * @return the new {@code Maybe} instance + * @throws NullPointerException if {@code source1}, {@code source2} or {@code zipper} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/zip.html">ReactiveX operators documentation: Zip</a> + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T1, @NonNull T2, @NonNull R> Maybe<R> zip( + @NonNull MaybeSource<? extends T1> source1, @NonNull MaybeSource<? extends T2> source2, + @NonNull BiFunction<? super T1, ? super T2, ? extends R> zipper) { + Objects.requireNonNull(source1, "source1 is null"); + Objects.requireNonNull(source2, "source2 is null"); + Objects.requireNonNull(zipper, "zipper is null"); + return zipArray(Functions.toFunction(zipper), source1, source2); + } + + /** + * Returns a {@code Maybe} that emits the results of a specified combiner function applied to combinations of + * three items emitted, in sequence, by three other {@link MaybeSource}s. + * <p> + * <img width="640" height="434" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.zip.n.png" alt=""> + * <p> + * This operator terminates eagerly if any of the source {@code MaybeSource}s signal an {@code onError} or {@code onComplete}. This + * also means it is possible some sources may not get subscribed to at all. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code zip} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T1> the value type of the first source + * @param <T2> the value type of the second source + * @param <T3> the value type of the third source + * @param <R> the zipped result type + * @param source1 + * the first source {@code MaybeSource} + * @param source2 + * a second source {@code MaybeSource} + * @param source3 + * a third source {@code MaybeSource} + * @param zipper + * a function that, when applied to an item emitted by each of the source {@code MaybeSource}s, results in + * an item that will be emitted by the resulting {@code Maybe} + * @return the new {@code Maybe} instance + * @throws NullPointerException if {@code source1}, {@code source2}, {@code source3} or {@code zipper} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/zip.html">ReactiveX operators documentation: Zip</a> + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T1, @NonNull T2, @NonNull T3, @NonNull R> Maybe<R> zip( + @NonNull MaybeSource<? extends T1> source1, @NonNull MaybeSource<? extends T2> source2, @NonNull MaybeSource<? extends T3> source3, + @NonNull Function3<? super T1, ? super T2, ? super T3, ? extends R> zipper) { + Objects.requireNonNull(source1, "source1 is null"); + Objects.requireNonNull(source2, "source2 is null"); + Objects.requireNonNull(source3, "source3 is null"); + Objects.requireNonNull(zipper, "zipper is null"); + return zipArray(Functions.toFunction(zipper), source1, source2, source3); + } + + /** + * Returns a {@code Maybe} that emits the results of a specified combiner function applied to combinations of + * four items emitted, in sequence, by four other {@link MaybeSource}s. + * <p> + * <img width="640" height="434" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.zip.n.png" alt=""> + * <p> + * This operator terminates eagerly if any of the source {@code MaybeSource}s signal an {@code onError} or {@code onComplete}. This + * also means it is possible some sources may not get subscribed to at all. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code zip} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T1> the value type of the first source + * @param <T2> the value type of the second source + * @param <T3> the value type of the third source + * @param <T4> the value type of the fourth source + * @param <R> the zipped result type + * @param source1 + * the first source {@code MaybeSource} + * @param source2 + * a second source {@code MaybeSource} + * @param source3 + * a third source {@code MaybeSource} + * @param source4 + * a fourth source {@code MaybeSource} + * @param zipper + * a function that, when applied to an item emitted by each of the source {@code MaybeSource}s, results in + * an item that will be emitted by the resulting {@code Maybe} + * @return the new {@code Maybe} instance + * @throws NullPointerException if {@code source1}, {@code source2}, {@code source3}, + * {@code source4} or {@code zipper} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/zip.html">ReactiveX operators documentation: Zip</a> + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T1, @NonNull T2, @NonNull T3, @NonNull T4, @NonNull R> Maybe<R> zip( + @NonNull MaybeSource<? extends T1> source1, @NonNull MaybeSource<? extends T2> source2, @NonNull MaybeSource<? extends T3> source3, + @NonNull MaybeSource<? extends T4> source4, + @NonNull Function4<? super T1, ? super T2, ? super T3, ? super T4, ? extends R> zipper) { + Objects.requireNonNull(source1, "source1 is null"); + Objects.requireNonNull(source2, "source2 is null"); + Objects.requireNonNull(source3, "source3 is null"); + Objects.requireNonNull(source4, "source4 is null"); + Objects.requireNonNull(zipper, "zipper is null"); + return zipArray(Functions.toFunction(zipper), source1, source2, source3, source4); + } + + /** + * Returns a {@code Maybe} that emits the results of a specified combiner function applied to combinations of + * five items emitted, in sequence, by five other {@link MaybeSource}s. + * <p> + * <img width="640" height="434" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.zip.n.png" alt=""> + * <p> + * This operator terminates eagerly if any of the source {@code MaybeSource}s signal an {@code onError} or {@code onComplete}. This + * also means it is possible some sources may not get subscribed to at all. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code zip} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T1> the value type of the first source + * @param <T2> the value type of the second source + * @param <T3> the value type of the third source + * @param <T4> the value type of the fourth source + * @param <T5> the value type of the fifth source + * @param <R> the zipped result type + * @param source1 + * the first source {@code MaybeSource} + * @param source2 + * a second source {@code MaybeSource} + * @param source3 + * a third source {@code MaybeSource} + * @param source4 + * a fourth source {@code MaybeSource} + * @param source5 + * a fifth source {@code MaybeSource} + * @param zipper + * a function that, when applied to an item emitted by each of the source {@code MaybeSource}s, results in + * an item that will be emitted by the resulting {@code Maybe} + * @return the new {@code Maybe} instance + * @throws NullPointerException if {@code source1}, {@code source2}, {@code source3}, + * {@code source4}, {@code source5} or {@code zipper} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/zip.html">ReactiveX operators documentation: Zip</a> + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T1, @NonNull T2, @NonNull T3, @NonNull T4, @NonNull T5, @NonNull R> Maybe<R> zip( + @NonNull MaybeSource<? extends T1> source1, @NonNull MaybeSource<? extends T2> source2, @NonNull MaybeSource<? extends T3> source3, + @NonNull MaybeSource<? extends T4> source4, @NonNull MaybeSource<? extends T5> source5, + @NonNull Function5<? super T1, ? super T2, ? super T3, ? super T4, ? super T5, ? extends R> zipper) { + Objects.requireNonNull(source1, "source1 is null"); + Objects.requireNonNull(source2, "source2 is null"); + Objects.requireNonNull(source3, "source3 is null"); + Objects.requireNonNull(source4, "source4 is null"); + Objects.requireNonNull(source5, "source5 is null"); + Objects.requireNonNull(zipper, "zipper is null"); + return zipArray(Functions.toFunction(zipper), source1, source2, source3, source4, source5); + } + + /** + * Returns a {@code Maybe} that emits the results of a specified combiner function applied to combinations of + * six items emitted, in sequence, by six other {@link MaybeSource}s. + * <p> + * <img width="640" height="434" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.zip.n.png" alt=""> + * <p> + * This operator terminates eagerly if any of the source {@code MaybeSource}s signal an {@code onError} or {@code onComplete}. This + * also means it is possible some sources may not get subscribed to at all. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code zip} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T1> the value type of the first source + * @param <T2> the value type of the second source + * @param <T3> the value type of the third source + * @param <T4> the value type of the fourth source + * @param <T5> the value type of the fifth source + * @param <T6> the value type of the sixth source + * @param <R> the zipped result type + * @param source1 + * the first source {@code MaybeSource} + * @param source2 + * a second source {@code MaybeSource} + * @param source3 + * a third source {@code MaybeSource} + * @param source4 + * a fourth source {@code MaybeSource} + * @param source5 + * a fifth source {@code MaybeSource} + * @param source6 + * a sixth source {@code MaybeSource} + * @param zipper + * a function that, when applied to an item emitted by each of the source {@code MaybeSource}s, results in + * an item that will be emitted by the resulting {@code Maybe} + * @return the new {@code Maybe} instance + * @throws NullPointerException if {@code source1}, {@code source2}, {@code source3}, + * {@code source4}, {@code source5}, {@code source6} or {@code zipper} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/zip.html">ReactiveX operators documentation: Zip</a> + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T1, @NonNull T2, @NonNull T3, @NonNull T4, @NonNull T5, @NonNull T6, @NonNull R> Maybe<R> zip( + @NonNull MaybeSource<? extends T1> source1, @NonNull MaybeSource<? extends T2> source2, @NonNull MaybeSource<? extends T3> source3, + @NonNull MaybeSource<? extends T4> source4, @NonNull MaybeSource<? extends T5> source5, @NonNull MaybeSource<? extends T6> source6, + @NonNull Function6<? super T1, ? super T2, ? super T3, ? super T4, ? super T5, ? super T6, ? extends R> zipper) { + Objects.requireNonNull(source1, "source1 is null"); + Objects.requireNonNull(source2, "source2 is null"); + Objects.requireNonNull(source3, "source3 is null"); + Objects.requireNonNull(source4, "source4 is null"); + Objects.requireNonNull(source5, "source5 is null"); + Objects.requireNonNull(source6, "source6 is null"); + Objects.requireNonNull(zipper, "zipper is null"); + return zipArray(Functions.toFunction(zipper), source1, source2, source3, source4, source5, source6); + } + + /** + * Returns a {@code Maybe} that emits the results of a specified combiner function applied to combinations of + * seven items emitted, in sequence, by seven other {@link MaybeSource}s. + * <p> + * <img width="640" height="434" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.zip.n.png" alt=""> + * <p> + * This operator terminates eagerly if any of the source {@code MaybeSource}s signal an {@code onError} or {@code onComplete}. This + * also means it is possible some sources may not get subscribed to at all. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code zip} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T1> the value type of the first source + * @param <T2> the value type of the second source + * @param <T3> the value type of the third source + * @param <T4> the value type of the fourth source + * @param <T5> the value type of the fifth source + * @param <T6> the value type of the sixth source + * @param <T7> the value type of the seventh source + * @param <R> the zipped result type + * @param source1 + * the first source {@code MaybeSource} + * @param source2 + * a second source {@code MaybeSource} + * @param source3 + * a third source {@code MaybeSource} + * @param source4 + * a fourth source {@code MaybeSource} + * @param source5 + * a fifth source {@code MaybeSource} + * @param source6 + * a sixth source {@code MaybeSource} + * @param source7 + * a seventh source {@code MaybeSource} + * @param zipper + * a function that, when applied to an item emitted by each of the source {@code MaybeSource}s, results in + * an item that will be emitted by the resulting {@code Maybe} + * @throws NullPointerException if {@code source1}, {@code source2}, {@code source3}, + * {@code source4}, {@code source5}, {@code source6}, + * {@code source7} or {@code zipper} is {@code null} + * @return the new {@code Maybe} instance + * @see <a href="http://reactivex.io/documentation/operators/zip.html">ReactiveX operators documentation: Zip</a> + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T1, @NonNull T2, @NonNull T3, @NonNull T4, @NonNull T5, @NonNull T6, @NonNull T7, @NonNull R> Maybe<R> zip( + @NonNull MaybeSource<? extends T1> source1, @NonNull MaybeSource<? extends T2> source2, @NonNull MaybeSource<? extends T3> source3, + @NonNull MaybeSource<? extends T4> source4, @NonNull MaybeSource<? extends T5> source5, @NonNull MaybeSource<? extends T6> source6, + @NonNull MaybeSource<? extends T7> source7, + @NonNull Function7<? super T1, ? super T2, ? super T3, ? super T4, ? super T5, ? super T6, ? super T7, ? extends R> zipper) { + Objects.requireNonNull(source1, "source1 is null"); + Objects.requireNonNull(source2, "source2 is null"); + Objects.requireNonNull(source3, "source3 is null"); + Objects.requireNonNull(source4, "source4 is null"); + Objects.requireNonNull(source5, "source5 is null"); + Objects.requireNonNull(source6, "source6 is null"); + Objects.requireNonNull(source7, "source7 is null"); + Objects.requireNonNull(zipper, "zipper is null"); + return zipArray(Functions.toFunction(zipper), source1, source2, source3, source4, source5, source6, source7); + } + + /** + * Returns a {@code Maybe} that emits the results of a specified combiner function applied to combinations of + * eight items emitted, in sequence, by eight other {@link MaybeSource}s. + * <p> + * <img width="640" height="434" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.zip.n.png" alt=""> + * <p> + * This operator terminates eagerly if any of the source {@code MaybeSource}s signal an {@code onError} or {@code onComplete}. This + * also means it is possible some sources may not get subscribed to at all. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code zip} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T1> the value type of the first source + * @param <T2> the value type of the second source + * @param <T3> the value type of the third source + * @param <T4> the value type of the fourth source + * @param <T5> the value type of the fifth source + * @param <T6> the value type of the sixth source + * @param <T7> the value type of the seventh source + * @param <T8> the value type of the eighth source + * @param <R> the zipped result type + * @param source1 + * the first source {@code MaybeSource} + * @param source2 + * a second source {@code MaybeSource} + * @param source3 + * a third source {@code MaybeSource} + * @param source4 + * a fourth source {@code MaybeSource} + * @param source5 + * a fifth source {@code MaybeSource} + * @param source6 + * a sixth source {@code MaybeSource} + * @param source7 + * a seventh source {@code MaybeSource} + * @param source8 + * an eighth source {@code MaybeSource} + * @param zipper + * a function that, when applied to an item emitted by each of the source {@code MaybeSource}s, results in + * an item that will be emitted by the resulting {@code Maybe} + * @return the new {@code Maybe} instance + * @throws NullPointerException if {@code source1}, {@code source2}, {@code source3}, + * {@code source4}, {@code source5}, {@code source6}, + * {@code source7}, {@code source8} or {@code zipper} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/zip.html">ReactiveX operators documentation: Zip</a> + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T1, @NonNull T2, @NonNull T3, @NonNull T4, @NonNull T5, @NonNull T6, @NonNull T7, @NonNull T8, @NonNull R> Maybe<R> zip( + @NonNull MaybeSource<? extends T1> source1, @NonNull MaybeSource<? extends T2> source2, @NonNull MaybeSource<? extends T3> source3, + @NonNull MaybeSource<? extends T4> source4, @NonNull MaybeSource<? extends T5> source5, @NonNull MaybeSource<? extends T6> source6, + @NonNull MaybeSource<? extends T7> source7, @NonNull MaybeSource<? extends T8> source8, + @NonNull Function8<? super T1, ? super T2, ? super T3, ? super T4, ? super T5, ? super T6, ? super T7, ? super T8, ? extends R> zipper) { + Objects.requireNonNull(source1, "source1 is null"); + Objects.requireNonNull(source2, "source2 is null"); + Objects.requireNonNull(source3, "source3 is null"); + Objects.requireNonNull(source4, "source4 is null"); + Objects.requireNonNull(source5, "source5 is null"); + Objects.requireNonNull(source6, "source6 is null"); + Objects.requireNonNull(source7, "source7 is null"); + Objects.requireNonNull(source8, "source8 is null"); + Objects.requireNonNull(zipper, "zipper is null"); + return zipArray(Functions.toFunction(zipper), source1, source2, source3, source4, source5, source6, source7, source8); + } + + /** + * Returns a {@code Maybe} that emits the results of a specified combiner function applied to combinations of + * nine items emitted, in sequence, by nine other {@link MaybeSource}s. + * <p> + * <img width="640" height="434" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.zip.n.png" alt=""> + * <p> + * This operator terminates eagerly if any of the source {@code MaybeSource}s signal an {@code onError} or {@code onComplete}. This + * also means it is possible some sources may not get subscribed to at all. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code zip} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <T1> the value type of the first source + * @param <T2> the value type of the second source + * @param <T3> the value type of the third source + * @param <T4> the value type of the fourth source + * @param <T5> the value type of the fifth source + * @param <T6> the value type of the sixth source + * @param <T7> the value type of the seventh source + * @param <T8> the value type of the eighth source + * @param <T9> the value type of the ninth source + * @param <R> the zipped result type + * @param source1 + * the first source {@code MaybeSource} + * @param source2 + * a second source {@code MaybeSource} + * @param source3 + * a third source {@code MaybeSource} + * @param source4 + * a fourth source {@code MaybeSource} + * @param source5 + * a fifth source {@code MaybeSource} + * @param source6 + * a sixth source {@code MaybeSource} + * @param source7 + * a seventh source {@code MaybeSource} + * @param source8 + * an eighth source {@code MaybeSource} + * @param source9 + * a ninth source {@code MaybeSource} + * @param zipper + * a function that, when applied to an item emitted by each of the source {@code MaybeSource}s, results in + * an item that will be emitted by the resulting {@code Maybe} + * @return the new {@code Maybe} instance + * @throws NullPointerException if {@code source1}, {@code source2}, {@code source3}, + * {@code source4}, {@code source5}, {@code source6}, + * {@code source7}, {@code source8}, {@code source9} or {@code zipper} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/zip.html">ReactiveX operators documentation: Zip</a> + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T1, @NonNull T2, @NonNull T3, @NonNull T4, @NonNull T5, @NonNull T6, @NonNull T7, @NonNull T8, @NonNull T9, @NonNull R> Maybe<R> zip( + @NonNull MaybeSource<? extends T1> source1, @NonNull MaybeSource<? extends T2> source2, @NonNull MaybeSource<? extends T3> source3, + @NonNull MaybeSource<? extends T4> source4, @NonNull MaybeSource<? extends T5> source5, @NonNull MaybeSource<? extends T6> source6, + @NonNull MaybeSource<? extends T7> source7, @NonNull MaybeSource<? extends T8> source8, @NonNull MaybeSource<? extends T9> source9, + @NonNull Function9<? super T1, ? super T2, ? super T3, ? super T4, ? super T5, ? super T6, ? super T7, ? super T8, ? super T9, ? extends R> zipper) { + + Objects.requireNonNull(source1, "source1 is null"); + Objects.requireNonNull(source2, "source2 is null"); + Objects.requireNonNull(source3, "source3 is null"); + Objects.requireNonNull(source4, "source4 is null"); + Objects.requireNonNull(source5, "source5 is null"); + Objects.requireNonNull(source6, "source6 is null"); + Objects.requireNonNull(source7, "source7 is null"); + Objects.requireNonNull(source8, "source8 is null"); + Objects.requireNonNull(source9, "source9 is null"); + Objects.requireNonNull(zipper, "zipper is null"); + return zipArray(Functions.toFunction(zipper), source1, source2, source3, source4, source5, source6, source7, source8, source9); + } + + /** + * Returns a {@code Maybe} that emits the results of a specified combiner function applied to combinations of + * items emitted, in sequence, by an array of other {@link MaybeSource}s. + * <p> + * Note on method signature: since Java doesn't allow creating a generic array with {@code new T[]}, the + * implementation of this operator has to create an {@code Object[]} instead. Unfortunately, a + * {@code Function<Integer[], R>} passed to the method would trigger a {@link ClassCastException}. + * + * <p> + * <img width="640" height="340" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.zipArray.png" alt=""> + * <p>This operator terminates eagerly if any of the source {@code MaybeSource}s signal an {@code onError} or {@code onComplete}. This + * also means it is possible some sources may not get subscribed to at all. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code zipArray} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T> the common element type + * @param <R> the result type + * @param sources + * an array of source {@code MaybeSource}s + * @param zipper + * a function that, when applied to an item emitted by each of the source {@code MaybeSource}s, results in + * an item that will be emitted by the resulting {@code Maybe} + * @return the new {@code Maybe} instance + * @throws NullPointerException if {@code sources} or {@code zipper} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/zip.html">ReactiveX operators documentation: Zip</a> + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + @SafeVarargs + public static <@NonNull T, @NonNull R> Maybe<R> zipArray(@NonNull Function<? super Object[], ? extends R> zipper, + @NonNull MaybeSource<? extends T>... sources) { + Objects.requireNonNull(sources, "sources is null"); + if (sources.length == 0) { + return empty(); + } + Objects.requireNonNull(zipper, "zipper is null"); + return RxJavaPlugins.onAssembly(new MaybeZipArray<>(sources, zipper)); + } + + // ------------------------------------------------------------------ + // Instance methods + // ------------------------------------------------------------------ + + /** + * Mirrors the {@link MaybeSource} (current or provided) that first signals an event. + * <p> + * <img width="640" height="310" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.ambWith.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code ambWith} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param other + * a {@code MaybeSource} competing to react first. A subscription to this provided source will occur after + * subscribing to the current source. + * @return the new {@code Maybe} instance + * @throws NullPointerException if {@code other} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/amb.html">ReactiveX operators documentation: Amb</a> + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public final Maybe<T> ambWith(@NonNull MaybeSource<? extends T> other) { + Objects.requireNonNull(other, "other is null"); + return ambArray(this, other); + } + + /** + * Waits in a blocking fashion until the current {@code Maybe} signals a success value (which is returned), + * {@code null} if completed or an exception (which is propagated). + * <p> + * <img width="640" height="285" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.blockingGet.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code blockingGet} does not operate by default on a particular {@link Scheduler}.</dd> + * <dt><b>Error handling:</b></dt> + * <dd>If the source signals an error, the operator wraps a checked {@link Exception} + * into {@link RuntimeException} and throws that. Otherwise, {@code RuntimeException}s and + * {@link Error}s are rethrown as they are.</dd> + * </dl> + * @return the success value + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @Nullable + public final T blockingGet() { + BlockingMultiObserver<T> observer = new BlockingMultiObserver<>(); + subscribe(observer); + return observer.blockingGet(); + } + + /** + * Waits in a blocking fashion until the current {@code Maybe} signals a success value (which is returned), + * defaultValue if completed or an exception (which is propagated). + * <p> + * <img width="640" height="297" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.blockingGet.v.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code blockingGet} does not operate by default on a particular {@link Scheduler}.</dd> + * <dt><b>Error handling:</b></dt> + * <dd>If the source signals an error, the operator wraps a checked {@link Exception} + * into {@link RuntimeException} and throws that. Otherwise, {@code RuntimeException}s and + * {@link Error}s are rethrown as they are.</dd> + * </dl> + * @param defaultValue the default item to return if this {@code Maybe} is empty + * @return the success value + * @throws NullPointerException if {@code defaultValue} is {@code null} + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final T blockingGet(@NonNull T defaultValue) { + Objects.requireNonNull(defaultValue, "defaultValue is null"); + BlockingMultiObserver<T> observer = new BlockingMultiObserver<>(); + subscribe(observer); + return observer.blockingGet(defaultValue); + } + + /** + * Subscribes to the current {@code Maybe} and <em>blocks the current thread</em> until it terminates. + * <p> + * <img width="640" height="238" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.blockingSubscribe.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code blockingSubscribe} does not operate by default on a particular {@link Scheduler}.</dd> + * <dt><b>Error handling:</b></dt> + * <dd>If the current {@code Maybe} signals an error, + * the {@link Throwable} is routed to the global error handler via {@link RxJavaPlugins#onError(Throwable)}. + * If the current thread is interrupted, an {@link InterruptedException} is routed to the same global error handler. + * </dd> + * </dl> + * @since 3.0.0 + * @see #blockingSubscribe(Consumer) + * @see #blockingSubscribe(Consumer, Consumer) + * @see #blockingSubscribe(Consumer, Consumer, Action) + */ + @SchedulerSupport(SchedulerSupport.NONE) + public final void blockingSubscribe() { + blockingSubscribe(Functions.emptyConsumer(), Functions.ERROR_CONSUMER, Functions.EMPTY_ACTION); + } + + /** + * Subscribes to the current {@code Maybe} and calls given {@code onSuccess} callback on the <em>current thread</em> + * when it completes normally. + * <p> + * <img width="640" height="245" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.blockingSubscribe.c.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code blockingSubscribe} does not operate by default on a particular {@link Scheduler}.</dd> + * <dt><b>Error handling:</b></dt> + * <dd>If either the current {@code Maybe} signals an error or {@code onSuccess} throws, + * the respective {@link Throwable} is routed to the global error handler via {@link RxJavaPlugins#onError(Throwable)}. + * If the current thread is interrupted, an {@link InterruptedException} is routed to the same global error handler. + * </dd> + * </dl> + * @param onSuccess the {@link Consumer} to call if the current {@code Maybe} succeeds + * @throws NullPointerException if {@code onSuccess} is {@code null} + * @since 3.0.0 + * @see #blockingSubscribe(Consumer, Consumer) + * @see #blockingSubscribe(Consumer, Consumer, Action) + */ + @SchedulerSupport(SchedulerSupport.NONE) + public final void blockingSubscribe(@NonNull Consumer<? super T> onSuccess) { + blockingSubscribe(onSuccess, Functions.ERROR_CONSUMER, Functions.EMPTY_ACTION); + } + + /** + * Subscribes to the current {@code Maybe} and calls the appropriate callback on the <em>current thread</em> + * when it terminates. + * <p> + * <img width="640" height="256" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.blockingSubscribe.cc.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code blockingSubscribe} does not operate by default on a particular {@link Scheduler}.</dd> + * <dt><b>Error handling:</b></dt> + * <dd>If either {@code onSuccess} or {@code onError} throw, the {@link Throwable} is routed to the + * global error handler via {@link RxJavaPlugins#onError(Throwable)}. + * If the current thread is interrupted, the {@code onError} consumer is called with an {@link InterruptedException}. + * </dd> + * </dl> + * @param onSuccess the {@link Consumer} to call if the current {@code Maybe} succeeds + * @param onError the {@code Consumer} to call if the current {@code Maybe} signals an error + * @throws NullPointerException if {@code onSuccess} or {@code onError} is {@code null} + * @since 3.0.0 + * @see #blockingSubscribe(Consumer, Consumer, Action) + */ + @SchedulerSupport(SchedulerSupport.NONE) + public final void blockingSubscribe(@NonNull Consumer<? super T> onSuccess, @NonNull Consumer<? super Throwable> onError) { + blockingSubscribe(onSuccess, onError, Functions.EMPTY_ACTION); + } + + /** + * Subscribes to the current {@code Maybe} and calls the appropriate callback on the <em>current thread</em> + * when it terminates. + * <p> + * <img width="640" height="251" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.blockingSubscribe.cca.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code blockingSubscribe} does not operate by default on a particular {@link Scheduler}.</dd> + * <dt><b>Error handling:</b></dt> + * <dd>If either {@code onSuccess}, {@code onError} or {@code onComplete} throw, the {@link Throwable} is routed to the + * global error handler via {@link RxJavaPlugins#onError(Throwable)}. + * If the current thread is interrupted, the {@code onError} consumer is called with an {@link InterruptedException}. + * </dd> + * </dl> + * @param onSuccess the {@link Consumer} to call if the current {@code Maybe} succeeds + * @param onError the {@code Consumer} to call if the current {@code Maybe} signals an error + * @param onComplete the {@link Action} to call if the current {@code Maybe} completes without a value + * @throws NullPointerException if {@code onSuccess}, {@code onError} or {@code onComplete} is {@code null} + * @since 3.0.0 + */ + @SchedulerSupport(SchedulerSupport.NONE) + public final void blockingSubscribe(@NonNull Consumer<? super T> onSuccess, @NonNull Consumer<? super Throwable> onError, @NonNull Action onComplete) { + Objects.requireNonNull(onSuccess, "onSuccess is null"); + Objects.requireNonNull(onError, "onError is null"); + Objects.requireNonNull(onComplete, "onComplete is null"); + BlockingMultiObserver<T> observer = new BlockingMultiObserver<>(); + subscribe(observer); + observer.blockingConsume(onSuccess, onError, onComplete); + } + + /** + * Subscribes to the current {@code Maybe} and calls the appropriate {@link MaybeObserver} method on the <em>current thread</em>. + * <p> + * <img width="640" height="398" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.blockingSubscribe.o.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code blockingSubscribe} does not operate by default on a particular {@link Scheduler}.</dd> + * <dt><b>Error handling:</b></dt> + * <dd>An {@code onError} signal is delivered to the {@link MaybeObserver#onError(Throwable)} method. + * If any of the {@code MaybeObserver}'s methods throw, the {@link RuntimeException} is propagated to the caller of this method. + * If the current thread is interrupted, an {@link InterruptedException} is delivered to {@code observer.onError}. + * </dd> + * </dl> + * @param observer the {@code MaybeObserver} to call methods on the current thread + * @throws NullPointerException if {@code observer} is {@code null} + * @since 3.0.0 + */ + @SchedulerSupport(SchedulerSupport.NONE) + public final void blockingSubscribe(@NonNull MaybeObserver<? super T> observer) { + Objects.requireNonNull(observer, "observer is null"); + BlockingDisposableMultiObserver<T> blockingObserver = new BlockingDisposableMultiObserver<>(); + observer.onSubscribe(blockingObserver); + subscribe(blockingObserver); + blockingObserver.blockingConsume(observer); + } + + /** + * Returns a {@code Maybe} that subscribes to this {@code Maybe} lazily, caches its event + * and replays it, to all the downstream subscribers. + * <p> + * <img width="640" height="244" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.cache.png" alt=""> + * <p> + * The operator subscribes only when the first downstream subscriber subscribes and maintains + * a single subscription towards this {@code Maybe}. + * <p> + * <em>Note:</em> You sacrifice the ability to dispose the origin when you use the {@code cache}. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code cache} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @return the new {@code Maybe} instance + * @see <a href="http://reactivex.io/documentation/operators/replay.html">ReactiveX operators documentation: Replay</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Maybe<T> cache() { + return RxJavaPlugins.onAssembly(new MaybeCache<>(this)); + } + + /** + * Casts the success value of the current {@code Maybe} into the target type or signals a + * {@link ClassCastException} if not compatible. + * <p> + * <img width="640" height="318" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.cast.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code cast} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <U> the target type + * @param clazz the type token to use for casting the success result from the current {@code Maybe} + * @return the new {@code Maybe} instance + * @throws NullPointerException if {@code clazz} is {@code null} + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public final <@NonNull U> Maybe<U> cast(@NonNull Class<? extends U> clazz) { + Objects.requireNonNull(clazz, "clazz is null"); + return map(Functions.castFunction(clazz)); + } + + /** + * Transform a {@code Maybe} by applying a particular {@link MaybeTransformer} function to it. + * <p> + * <img width="640" height="615" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.compose.png" alt=""> + * <p> + * This method operates on the {@code Maybe} itself whereas {@link #lift} operates on the {@code Maybe}'s {@link MaybeObserver}s. + * <p> + * If the operator you are creating is designed to act on the individual item emitted by a {@code Maybe}, use + * {@link #lift}. If your operator is designed to transform the current {@code Maybe} as a whole (for instance, by + * applying a particular set of existing RxJava operators to it) use {@code compose}. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code compose} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <R> the value type of the {@code Maybe} returned by the transformer function + * @param transformer the transformer function, not {@code null} + * @return the new {@code Maybe} instance + * @throws NullPointerException if {@code transformer} is {@code null} + * @see <a href="https://github.com/ReactiveX/RxJava/wiki/Implementing-Your-Own-Operators">RxJava wiki: Implementing Your Own Operators</a> + */ + @SuppressWarnings("unchecked") + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull R> Maybe<R> compose(@NonNull MaybeTransformer<? super T, ? extends R> transformer) { + return wrap(((MaybeTransformer<T, R>) Objects.requireNonNull(transformer, "transformer is null")).apply(this)); + } + + /** + * Returns a {@code Maybe} that is based on applying a specified function to the item emitted by the current {@code Maybe}, + * where that function returns a {@link MaybeSource}. + * <p> + * <img width="640" height="216" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.concatMap.png" alt=""> + * <p> + * Note that flatMap and concatMap for {@code Maybe} is the same operation. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code concatMap} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <R> the result value type + * @param mapper + * a function that, when applied to the item emitted by the current {@code Maybe}, returns a {@code MaybeSource} + * @return the new {@code Maybe} instance + * @see <a href="http://reactivex.io/documentation/operators/flatmap.html">ReactiveX operators documentation: FlatMap</a> + * @throws NullPointerException if {@code mapper} is {@code null} + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public final <@NonNull R> Maybe<R> concatMap(@NonNull Function<? super T, ? extends MaybeSource<? extends R>> mapper) { + return flatMap(mapper); + } + + /** + * Returns a {@link Completable} that completes based on applying a specified function to the item emitted by the + * current {@code Maybe}, where that function returns a {@code Completable}. + * <p> + * <img width="640" height="304" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.concatMapCompletable.png" alt=""> + * <p> + * This operator is an alias for {@link #flatMapCompletable(Function)}. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code concatMapCompletable} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param mapper + * a function that, when applied to the item emitted by the current {@code Maybe}, returns a + * {@code Completable} + * @return the new {@code Completable} instance + * @throws NullPointerException if {@code mapper} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/flatmap.html">ReactiveX operators documentation: FlatMap</a> + * @since 3.0.0 + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public final Completable concatMapCompletable(@NonNull Function<? super T, ? extends CompletableSource> mapper) { + return flatMapCompletable(mapper); + } + + /** + * Returns a {@code Maybe} based on applying a specified function to the item emitted by the + * current {@code Maybe}, where that function returns a {@link Single}. + * When this {@code Maybe} just completes the resulting {@code Maybe} completes as well. + * <p> + * <img width="640" height="315" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.concatMapSingle.png" alt=""> + * <p> + * This operator is an alias for {@link #flatMapSingle(Function)}. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code concatMapSingle} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <R> the result value type + * @param mapper + * a function that, when applied to the item emitted by the current {@code Maybe}, returns a + * {@code Single} + * @return the new {@code Maybe} instance + * @throws NullPointerException if {@code mapper} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/flatmap.html">ReactiveX operators documentation: FlatMap</a> + * @since 3.0.0 + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public final <@NonNull R> Maybe<R> concatMapSingle(@NonNull Function<? super T, ? extends SingleSource<? extends R>> mapper) { + return flatMapSingle(mapper); + } + + /** + * Returns a {@link Flowable} that emits the items emitted from the current {@code Maybe}, then the {@code other} {@link MaybeSource}, one after + * the other, without interleaving them. + * <p> + * <img width="640" height="172" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.concatWith.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code concatWith} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param other + * a {@code MaybeSource} to be concatenated after the current + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code other} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/concat.html">ReactiveX operators documentation: Concat</a> + */ + @BackpressureSupport(BackpressureKind.FULL) + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public final Flowable<T> concatWith(@NonNull MaybeSource<? extends T> other) { + Objects.requireNonNull(other, "other is null"); + return concat(this, other); + } + + /** + * Returns a {@link Single} that emits a {@link Boolean} that indicates whether the current {@code Maybe} emitted a + * specified item. + * <p> + * <img width="640" height="310" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.contains.o.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code contains} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param item + * the item to search for in the emissions from the current {@code Maybe}, not {@code null} + * @return the new {@code Single} instance + * @throws NullPointerException if {@code item} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/contains.html">ReactiveX operators documentation: Contains</a> + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public final Single<Boolean> contains(@NonNull Object item) { + Objects.requireNonNull(item, "item is null"); + return RxJavaPlugins.onAssembly(new MaybeContains<>(this, item)); + } + + /** + * Returns a {@link Single} that counts the total number of items emitted (0 or 1) by the current {@code Maybe} and emits + * this count as a 64-bit {@link Long}. + * <p> + * <img width="640" height="434" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.count.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code count} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @return the new {@code Single} instance + * @see <a href="http://reactivex.io/documentation/operators/count.html">ReactiveX operators documentation: Count</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Single<Long> count() { + return RxJavaPlugins.onAssembly(new MaybeCount<>(this)); + } + + /** + * Returns a {@link Single} that emits the item emitted by the current {@code Maybe} or a specified default item + * if the current {@code Maybe} is empty. + * <p> + * <img width="640" height="390" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.defaultIfEmpty.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code defaultIfEmpty} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param defaultItem + * the item to emit if the current {@code Maybe} emits no items + * @return the new {@code Single} instance + * @throws NullPointerException if {@code defaultItem} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/defaultifempty.html">ReactiveX operators documentation: DefaultIfEmpty</a> + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public final Single<T> defaultIfEmpty(@NonNull T defaultItem) { + Objects.requireNonNull(defaultItem, "defaultItem is null"); + return RxJavaPlugins.onAssembly(new MaybeToSingle<>(this, defaultItem)); + } + + /** + * Maps the {@link Notification} success value of the current {@code Maybe} back into normal + * {@code onSuccess}, {@code onError} or {@code onComplete} signals. + * <p> + * <img width="640" height="268" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.dematerialize.png" alt=""> + * <p> + * The intended use of the {@code selector} function is to perform a + * type-safe identity mapping (see example) on a source that is already of type + * {@code Notification<T>}. The Java language doesn't allow + * limiting instance methods to a certain generic argument shape, therefore, + * a function is used to ensure the conversion remains type safe. + * <p> + * Regular {@code onError} or {@code onComplete} signals from the current {@code Maybe} are passed along to the downstream. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code dematerialize} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * <p> + * Example: + * <pre><code> + * Maybe.just(Notification.createOnNext(1)) + * .dematerialize(notification -> notification) + * .test() + * .assertResult(1); + * </code></pre> + * @param <R> the result type + * @param selector the function called with the success item and should + * return a {@code Notification} instance. + * @return the new {@code Maybe} instance + * @throws NullPointerException if {@code selector} is {@code null} + * @since 3.0.0 + * @see #materialize() + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public final <@NonNull R> Maybe<R> dematerialize(@NonNull Function<? super T, @NonNull Notification<R>> selector) { + Objects.requireNonNull(selector, "selector is null"); + return RxJavaPlugins.onAssembly(new MaybeDematerialize<>(this, selector)); + } + + /** + * Returns a {@code Maybe} that signals the events emitted by the current {@code Maybe} shifted forward in time by a + * specified delay. + * An error signal will not be delayed. + * <p> + * <img width="640" height="434" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.delay.t.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>This version of {@code delay} operates by default on the {@code computation} {@link Scheduler}.</dd> + * </dl> + * + * @param time + * the delay to shift the source by + * @param unit + * the {@link TimeUnit} in which {@code time} is defined + * @return the new {@code Maybe} instance + * @throws NullPointerException if {@code unit} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/delay.html">ReactiveX operators documentation: Delay</a> + * @see #delay(long, TimeUnit, Scheduler, boolean) + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.COMPUTATION) + @NonNull + public final Maybe<T> delay(long time, @NonNull TimeUnit unit) { + return delay(time, unit, Schedulers.computation(), false); + } + + /** + * Returns a {@code Maybe} that signals the events emitted by the current {@code Maybe} shifted forward in time by a + * specified delay. + * <p> + * <img width="640" height="340" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.delay.tb.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>This version of {@code delay} operates by default on the {@code computation} {@link Scheduler}.</dd> + * </dl> + * + * @param time the delay to shift the source by + * @param unit the {@link TimeUnit} in which {@code time} is defined + * @param delayError if {@code true}, both success and error signals are delayed. if {@code false}, only success signals are delayed. + * @return the new {@code Maybe} instance + * @throws NullPointerException if {@code unit} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/delay.html">ReactiveX operators documentation: Delay</a> + * @see #delay(long, TimeUnit, Scheduler, boolean) + * @since 3.0.0 + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.COMPUTATION) + @NonNull + public final Maybe<T> delay(long time, @NonNull TimeUnit unit, boolean delayError) { + return delay(time, unit, Schedulers.computation(), delayError); + } + + /** + * Returns a {@code Maybe} that signals the events emitted by the current {@code Maybe} shifted forward in time by a + * specified delay. + * An error signal will not be delayed. + * <p> + * <img width="640" height="434" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.delay.ts.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>you specify the {@link Scheduler} where the non-blocking wait and emission happens</dd> + * </dl> + * + * @param time the delay to shift the source by + * @param unit the {@link TimeUnit} in which {@code time} is defined + * @param scheduler the {@code Scheduler} to use for delaying + * @return the new {@code Maybe} instance + * @throws NullPointerException if {@code unit} or {@code scheduler} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/delay.html">ReactiveX operators documentation: Delay</a> + * @see #delay(long, TimeUnit, Scheduler, boolean) + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.CUSTOM) + @NonNull + public final Maybe<T> delay(long time, @NonNull TimeUnit unit, @NonNull Scheduler scheduler) { + return delay(time, unit, scheduler, false); + } + + /** + * Returns a {@code Maybe} that signals the events emitted by the current {@code Maybe} shifted forward in time by a + * specified delay running on the specified {@link Scheduler}. + * <p> + * <img width="640" height="352" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.delay.tsb.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>you specify which {@code Scheduler} this operator will use.</dd> + * </dl> + * + * @param time + * the delay to shift the source by + * @param unit + * the {@link TimeUnit} in which {@code time} is defined + * @param scheduler + * the {@code Scheduler} to use for delaying + * @param delayError if {@code true}, both success and error signals are delayed. if {@code false}, only success signals are delayed. + * @return the new {@code Maybe} instance + * @throws NullPointerException if {@code unit} or {@code scheduler} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/delay.html">ReactiveX operators documentation: Delay</a> + * @since 3.0.0 + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.CUSTOM) + public final Maybe<T> delay(long time, @NonNull TimeUnit unit, @NonNull Scheduler scheduler, boolean delayError) { + Objects.requireNonNull(unit, "unit is null"); + Objects.requireNonNull(scheduler, "scheduler is null"); + return RxJavaPlugins.onAssembly(new MaybeDelay<>(this, Math.max(0L, time), unit, scheduler, delayError)); + } + + /** + * Delays the emission of this {@code Maybe} until the given {@link Publisher} signals an item or completes. + * <p> + * <img width="640" height="175" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.delay.p.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The {@code delayIndicator} is consumed in an unbounded manner but is cancelled after + * the first item it produces.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>This version of {@code delay} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <U> + * the subscription delay value type (ignored) + * @param delayIndicator + * the {@code Publisher} that gets subscribed to when this {@code Maybe} signals an event and that + * signal is emitted when the {@code Publisher} signals an item or completes + * @return the new {@code Maybe} instance + * @throws NullPointerException if {@code delayIndicator} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/delay.html">ReactiveX operators documentation: Delay</a> + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + @BackpressureSupport(BackpressureKind.UNBOUNDED_IN) + public final <@NonNull U> Maybe<T> delay(@NonNull Publisher<U> delayIndicator) { + Objects.requireNonNull(delayIndicator, "delayIndicator is null"); + return RxJavaPlugins.onAssembly(new MaybeDelayOtherPublisher<>(this, delayIndicator)); + } + + /** + * Returns a {@code Maybe} that delays the subscription to this {@code Maybe} + * until the other {@link Publisher} emits an element or completes normally. + * <p> + * <img width="640" height="214" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.delaySubscription.p.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The {@code Publisher} source is consumed in an unbounded fashion (without applying backpressure).</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>This method does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <U> the value type of the other {@code Publisher}, irrelevant + * @param subscriptionIndicator the other {@code Publisher} that should trigger the subscription + * to this {@code Publisher}. + * @throws NullPointerException if {@code subscriptionIndicator} is {@code null} + * @return the new {@code Maybe} instance + */ + @BackpressureSupport(BackpressureKind.UNBOUNDED_IN) + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public final <@NonNull U> Maybe<T> delaySubscription(@NonNull Publisher<U> subscriptionIndicator) { + Objects.requireNonNull(subscriptionIndicator, "subscriptionIndicator is null"); + return RxJavaPlugins.onAssembly(new MaybeDelaySubscriptionOtherPublisher<>(this, subscriptionIndicator)); + } + + /** + * Returns a {@code Maybe} that delays the subscription to the current {@code Maybe} by a given amount of time. + * <p> + * <img width="640" height="471" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.delaySubscription.t.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>This version of {@code delaySubscription} operates by default on the {@code computation} {@link Scheduler}.</dd> + * </dl> + * + * @param time + * the time to delay the subscription + * @param unit + * the time unit of {@code delay} + * @return the new {@code Maybe} instance + * @throws NullPointerException if {@code unit} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/delay.html">ReactiveX operators documentation: Delay</a> + * @see #delaySubscription(long, TimeUnit, Scheduler) + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.COMPUTATION) + @NonNull + public final Maybe<T> delaySubscription(long time, @NonNull TimeUnit unit) { + return delaySubscription(time, unit, Schedulers.computation()); + } + + /** + * Returns a {@code Maybe} that delays the subscription to the current {@code Maybe} by a given amount of time, + * both waiting and subscribing on a given {@link Scheduler}. + * <p> + * <img width="640" height="420" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.delaySubscription.ts.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>You specify which {@code Scheduler} this operator will use.</dd> + * </dl> + * + * @param time + * the time to delay the subscription + * @param unit + * the time unit of {@code delay} + * @param scheduler + * the {@code Scheduler} on which the waiting and subscription will happen + * @return the new {@code Maybe} instance + * @throws NullPointerException if {@code unit} or {@code scheduler} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/delay.html">ReactiveX operators documentation: Delay</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.CUSTOM) + @NonNull + public final Maybe<T> delaySubscription(long time, @NonNull TimeUnit unit, @NonNull Scheduler scheduler) { + return delaySubscription(Flowable.timer(time, unit, scheduler)); + } + + /** + * Calls the specified {@link Consumer} with the success item after this item has been emitted to the downstream. + * <p>Note that the {@code onAfterSuccess} action is shared between subscriptions and as such + * should be thread-safe. + * <p> + * <img width="640" height="527" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.doAfterSuccess.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code doAfterSuccess} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * <p>History: 2.0.1 - experimental + * @param onAfterSuccess the {@code Consumer} that will be called after emitting an item from upstream to the downstream + * @return the new {@code Maybe} instance + * @throws NullPointerException if {@code onAfterSuccess} is {@code null} + * @since 2.1 + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public final Maybe<T> doAfterSuccess(@NonNull Consumer<? super T> onAfterSuccess) { + Objects.requireNonNull(onAfterSuccess, "onAfterSuccess is null"); + return RxJavaPlugins.onAssembly(new MaybeDoAfterSuccess<>(this, onAfterSuccess)); + } + + /** + * Registers an {@link Action} to be called when this {@code Maybe} invokes either + * {@link MaybeObserver#onComplete onSuccess}, + * {@link MaybeObserver#onComplete onComplete} or {@link MaybeObserver#onError onError}. + * <p> + * <img width="640" height="249" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.doAfterTerminate.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code doAfterTerminate} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param onAfterTerminate + * an {@code Action} to be invoked when the current {@code Maybe} finishes + * @return the new {@code Maybe} instance + * @throws NullPointerException if {@code onAfterTerminate} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/do.html">ReactiveX operators documentation: Do</a> + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public final Maybe<T> doAfterTerminate(@NonNull Action onAfterTerminate) { + return RxJavaPlugins.onAssembly(new MaybePeek<>(this, + Functions.emptyConsumer(), // onSubscribe + Functions.emptyConsumer(), // onSuccess + Functions.emptyConsumer(), // onError + Functions.EMPTY_ACTION, // onComplete + Objects.requireNonNull(onAfterTerminate, "onAfterTerminate is null"), + Functions.EMPTY_ACTION // dispose + )); + } + + /** + * Calls the specified action after this {@code Maybe} signals {@code onSuccess}, {@code onError} or {@code onComplete} or gets disposed by + * the downstream. + * <p> + * <img width="640" height="247" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.doFinally.png" alt=""> + * <p> + * In case of a race between a terminal event and a dispose call, the provided {@code onFinally} action + * is executed once per subscription. + * <p>Note that the {@code onFinally} action is shared between subscriptions and as such + * should be thread-safe. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code doFinally} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * <p>History: 2.0.1 - experimental + * @param onFinally the action called when this {@code Maybe} terminates or gets disposed + * @return the new {@code Maybe} instance + * @throws NullPointerException if {@code onFinally} is {@code null} + * @since 2.1 + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public final Maybe<T> doFinally(@NonNull Action onFinally) { + Objects.requireNonNull(onFinally, "onFinally is null"); + return RxJavaPlugins.onAssembly(new MaybeDoFinally<>(this, onFinally)); + } + + /** + * Calls the shared {@link Action} if a {@link MaybeObserver} subscribed to the current {@code Maybe} + * disposes the common {@link Disposable} it received via {@code onSubscribe}. + * <p> + * <img width="640" height="277" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.doOnDispose.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code doOnDispose} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param onDispose the action called when the subscription is disposed + * @throws NullPointerException if {@code onDispose} is {@code null} + * @return the new {@code Maybe} instance + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public final Maybe<T> doOnDispose(@NonNull Action onDispose) { + return RxJavaPlugins.onAssembly(new MaybePeek<>(this, + Functions.emptyConsumer(), // onSubscribe + Functions.emptyConsumer(), // onSuccess + Functions.emptyConsumer(), // onError + Functions.EMPTY_ACTION, // onComplete + Functions.EMPTY_ACTION, // (onSuccess | onError | onComplete) after + Objects.requireNonNull(onDispose, "onDispose is null") + )); + } + + /** + * Invokes an {@link Action} just before the current {@code Maybe} calls {@code onComplete}. + * <p> + * <img width="640" height="358" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/doOnComplete.m.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code doOnComplete} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param onComplete + * the action to invoke when the current {@code Maybe} calls {@code onComplete} + * @return the new {@code Maybe} with the side-effecting behavior applied + * @throws NullPointerException if {@code onComplete} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/do.html">ReactiveX operators documentation: Do</a> + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public final Maybe<T> doOnComplete(@NonNull Action onComplete) { + return RxJavaPlugins.onAssembly(new MaybePeek<>(this, + Functions.emptyConsumer(), // onSubscribe + Functions.emptyConsumer(), // onSuccess + Functions.emptyConsumer(), // onError + Objects.requireNonNull(onComplete, "onComplete is null"), + Functions.EMPTY_ACTION, // (onSuccess | onError | onComplete) + Functions.EMPTY_ACTION // dispose + )); + } + + /** + * Calls the shared {@link Consumer} with the error sent via {@code onError} for each + * {@link MaybeObserver} that subscribes to the current {@code Maybe}. + * <p> + * <img width="640" height="358" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/doOnError.m.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code doOnError} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param onError the consumer called with the success value of {@code onError} + * @return the new {@code Maybe} instance + * @throws NullPointerException if {@code onError} is {@code null} + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public final Maybe<T> doOnError(@NonNull Consumer<? super Throwable> onError) { + return RxJavaPlugins.onAssembly(new MaybePeek<>(this, + Functions.emptyConsumer(), // onSubscribe + Functions.emptyConsumer(), // onSuccess + Objects.requireNonNull(onError, "onError is null"), + Functions.EMPTY_ACTION, // onComplete + Functions.EMPTY_ACTION, // (onSuccess | onError | onComplete) + Functions.EMPTY_ACTION // dispose + )); + } + + /** + * Calls the given {@code onEvent} callback with the (success value, {@code null}) for an {@code onSuccess}, ({@code null}, throwable) for + * an {@code onError} or ({@code null}, {@code null}) for an {@code onComplete} signal from this {@code Maybe} before delivering said + * signal to the downstream. + * <p> + * <img width="640" height="297" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.doOnEvent.png" alt=""> + * <p> + * The exceptions thrown from the callback will override the event so the downstream receives the + * error instead of the original signal. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code doOnEvent} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param onEvent the callback to call with the success value or the exception, whichever is not {@code null} + * @return the new {@code Maybe} instance + * @throws NullPointerException if {@code onEvent} is {@code null} + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Maybe<T> doOnEvent(@NonNull BiConsumer<@Nullable ? super T, @Nullable ? super Throwable> onEvent) { + Objects.requireNonNull(onEvent, "onEvent is null"); + return RxJavaPlugins.onAssembly(new MaybeDoOnEvent<>(this, onEvent)); + } + + /** + * Calls the appropriate {@code onXXX} method (shared between all {@link MaybeObserver}s) for the lifecycle events of + * the sequence (subscription, disposal). + * <p> + * <img width="640" height="183" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.doOnLifecycle.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code doOnLifecycle} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param onSubscribe + * a {@link Consumer} called with the {@link Disposable} sent via {@link MaybeObserver#onSubscribe(Disposable)} + * @param onDispose + * called when the downstream disposes the {@code Disposable} via {@code dispose()} + * @return the new {@code Maybe} instance + * @throws NullPointerException if {@code onSubscribe} or {@code onDispose} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/do.html">ReactiveX operators documentation: Do</a> + * @since 3.0.0 + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Maybe<T> doOnLifecycle(@NonNull Consumer<? super Disposable> onSubscribe, @NonNull Action onDispose) { + Objects.requireNonNull(onSubscribe, "onSubscribe is null"); + Objects.requireNonNull(onDispose, "onDispose is null"); + return RxJavaPlugins.onAssembly(new MaybeDoOnLifecycle<>(this, onSubscribe, onDispose)); + } + + /** + * Calls the shared {@link Consumer} with the {@link Disposable} sent through the {@code onSubscribe} for each + * {@link MaybeObserver} that subscribes to the current {@code Maybe}. + * <p> + * <img width="640" height="506" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.doOnSubscribe.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code doOnSubscribe} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param onSubscribe the {@code Consumer} called with the {@code Disposable} sent via {@code onSubscribe} + * @return the new {@code Maybe} instance + * @throws NullPointerException if {@code onSubscribe} is {@code null} + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public final Maybe<T> doOnSubscribe(@NonNull Consumer<? super Disposable> onSubscribe) { + return RxJavaPlugins.onAssembly(new MaybePeek<>(this, + Objects.requireNonNull(onSubscribe, "onSubscribe is null"), + Functions.emptyConsumer(), // onSuccess + Functions.emptyConsumer(), // onError + Functions.EMPTY_ACTION, // onComplete + Functions.EMPTY_ACTION, // (onSuccess | onError | onComplete) + Functions.EMPTY_ACTION // dispose + )); + } + + /** + * Returns a {@code Maybe} instance that calls the given onTerminate callback + * just before this {@code Maybe} completes normally or with an exception. + * <p> + * <img width="640" height="249" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.doOnTerminate.png" alt=""> + * <p> + * This differs from {@code doAfterTerminate} in that this happens <em>before</em> the {@code onComplete} or + * {@code onError} notification. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code doOnTerminate} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * <p>History: 2.2.7 - experimental + * @param onTerminate the action to invoke when the consumer calls {@code onComplete} or {@code onError} + * @return the new {@code Maybe} instance + * @throws NullPointerException if {@code onTerminate} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/do.html">ReactiveX operators documentation: Do</a> + * @see #doOnTerminate(Action) + * @since 3.0.0 + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public final Maybe<T> doOnTerminate(@NonNull Action onTerminate) { + Objects.requireNonNull(onTerminate, "onTerminate is null"); + return RxJavaPlugins.onAssembly(new MaybeDoOnTerminate<>(this, onTerminate)); + } + + /** + * Calls the shared {@link Consumer} with the success value sent via {@code onSuccess} for each + * {@link MaybeObserver} that subscribes to the current {@code Maybe}. + * <p> + * <img width="640" height="358" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/doOnSuccess.m.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code doOnSuccess} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param onSuccess the {@code Consumer} called with the success value of the upstream + * @return the new {@code Maybe} instance + * @throws NullPointerException if {@code onSuccess} is {@code null} + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public final Maybe<T> doOnSuccess(@NonNull Consumer<? super T> onSuccess) { + return RxJavaPlugins.onAssembly(new MaybePeek<>(this, + Functions.emptyConsumer(), // onSubscribe + Objects.requireNonNull(onSuccess, "onSuccess is null"), + Functions.emptyConsumer(), // onError + Functions.EMPTY_ACTION, // onComplete + Functions.EMPTY_ACTION, // (onSuccess | onError | onComplete) + Functions.EMPTY_ACTION // dispose + )); + } + + /** + * Filters the success item of the {@code Maybe} via a predicate function and emitting it if the predicate + * returns {@code true}, completing otherwise. + * <p> + * <img width="640" height="291" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.filter.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code filter} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param predicate + * a function that evaluates the item emitted by the current {@code Maybe}, returning {@code true} + * if it passes the filter + * @return the new {@code Maybe} instance + * @throws NullPointerException if {@code predicate} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/filter.html">ReactiveX operators documentation: Filter</a> + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public final Maybe<T> filter(@NonNull Predicate<? super T> predicate) { + Objects.requireNonNull(predicate, "predicate is null"); + return RxJavaPlugins.onAssembly(new MaybeFilter<>(this, predicate)); + } + + /** + * Returns a {@code Maybe} that is based on applying a specified function to the item emitted by the current {@code Maybe}, + * where that function returns a {@link MaybeSource}. + * <p> + * <img width="640" height="357" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.flatMap.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code flatMap} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * <p>Note that flatMap and concatMap for {@code Maybe} is the same operation. + * + * @param <R> the result value type + * @param mapper + * a function that, when applied to the item emitted by the current {@code Maybe}, returns a {@code MaybeSource} + * @return the new {@code Maybe} instance + * @throws NullPointerException if {@code mapper} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/flatmap.html">ReactiveX operators documentation: FlatMap</a> + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public final <@NonNull R> Maybe<R> flatMap(@NonNull Function<? super T, ? extends MaybeSource<? extends R>> mapper) { + Objects.requireNonNull(mapper, "mapper is null"); + return RxJavaPlugins.onAssembly(new MaybeFlatten<>(this, mapper)); + } + + /** + * Maps the {@code onSuccess}, {@code onError} or {@code onComplete} signals of the current {@code Maybe} into a {@link MaybeSource} and emits that + * {@code MaybeSource}'s signals. + * <p> + * <img width="640" height="354" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.flatMap.mmm.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code flatMap} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <R> + * the result type + * @param onSuccessMapper + * a function that returns a {@code MaybeSource} to merge for the {@code onSuccess} item emitted by this {@code Maybe} + * @param onErrorMapper + * a function that returns a {@code MaybeSource} to merge for an {@code onError} notification from this {@code Maybe} + * @param onCompleteSupplier + * a function that returns a {@code MaybeSource} to merge for an {@code onComplete} notification this {@code Maybe} + * @return the new {@code Maybe} instance + * @throws NullPointerException if {@code onSuccessMapper}, {@code onErrorMapper} or {@code onCompleteSupplier} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/flatmap.html">ReactiveX operators documentation: FlatMap</a> + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public final <@NonNull R> Maybe<R> flatMap( + @NonNull Function<? super T, ? extends MaybeSource<? extends R>> onSuccessMapper, + @NonNull Function<? super Throwable, ? extends MaybeSource<? extends R>> onErrorMapper, + @NonNull Supplier<? extends MaybeSource<? extends R>> onCompleteSupplier) { + Objects.requireNonNull(onSuccessMapper, "onSuccessMapper is null"); + Objects.requireNonNull(onErrorMapper, "onErrorMapper is null"); + Objects.requireNonNull(onCompleteSupplier, "onCompleteSupplier is null"); + return RxJavaPlugins.onAssembly(new MaybeFlatMapNotification<>(this, onSuccessMapper, onErrorMapper, onCompleteSupplier)); + } + + /** + * Returns a {@code Maybe} that emits the results of a specified function to the pair of values emitted by the + * current {@code Maybe} and a specified mapped {@link MaybeSource}. + * <p> + * <img width="640" height="268" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.flatMap.combiner.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code flatMap} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <U> + * the type of items emitted by the {@code MaybeSource} returned by the {@code mapper} function + * @param <R> + * the type of items emitted by the resulting {@code Maybe} + * @param mapper + * a function that returns a {@code MaybeSource} for the item emitted by the current {@code Maybe} + * @param combiner + * a function that combines one item emitted by each of the source and collection {@code MaybeSource} and + * returns an item to be emitted by the resulting {@code MaybeSource} + * @return the new {@code Maybe} instance + * @throws NullPointerException if {@code mapper} or {@code combiner} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/flatmap.html">ReactiveX operators documentation: FlatMap</a> + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public final <@NonNull U, @NonNull R> Maybe<R> flatMap(@NonNull Function<? super T, ? extends MaybeSource<? extends U>> mapper, + @NonNull BiFunction<? super T, ? super U, ? extends R> combiner) { + Objects.requireNonNull(mapper, "mapper is null"); + Objects.requireNonNull(combiner, "combiner is null"); + return RxJavaPlugins.onAssembly(new MaybeFlatMapBiSelector<>(this, mapper, combiner)); + } + + /** + * Maps the success value of the current {@code Maybe} into an {@link Iterable} and emits its items as a + * {@link Flowable} sequence. + * <p> + * <img width="640" height="373" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/flattenAsFlowable.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code flattenAsFlowable} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <U> + * the type of item emitted by the inner {@code Iterable} + * @param mapper + * a function that returns an {@code Iterable} sequence of values for when given an item emitted by the + * current {@code Maybe} + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code mapper} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/flatmap.html">ReactiveX operators documentation: FlatMap</a> + * @see #flattenStreamAsFlowable(Function) + */ + @BackpressureSupport(BackpressureKind.FULL) + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public final <@NonNull U> Flowable<U> flattenAsFlowable(@NonNull Function<? super T, @NonNull ? extends Iterable<? extends U>> mapper) { + Objects.requireNonNull(mapper, "mapper is null"); + return RxJavaPlugins.onAssembly(new MaybeFlatMapIterableFlowable<>(this, mapper)); + } + + /** + * Maps the success value of the current {@code Maybe} into an {@link Iterable} and emits its items as an + * {@link Observable} sequence. + * <p> + * <img width="640" height="373" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/flattenAsObservable.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code flattenAsObservable} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <U> + * the type of item emitted by the resulting {@code Iterable} + * @param mapper + * a function that returns an {@code Iterable} sequence of values for when given an item emitted by the + * current {@code Maybe} + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code mapper} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/flatmap.html">ReactiveX operators documentation: FlatMap</a> + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public final <@NonNull U> Observable<U> flattenAsObservable(@NonNull Function<? super T, @NonNull ? extends Iterable<? extends U>> mapper) { + Objects.requireNonNull(mapper, "mapper is null"); + return RxJavaPlugins.onAssembly(new MaybeFlatMapIterableObservable<>(this, mapper)); + } + + /** + * Returns an {@link Observable} that is based on applying a specified function to the item emitted by the current {@code Maybe}, + * where that function returns an {@link ObservableSource}. + * <p> + * <img width="640" height="302" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.flatMapObservable.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code flatMapObservable} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <R> the result value type + * @param mapper + * a function that, when applied to the item emitted by the current {@code Maybe}, returns an {@code ObservableSource} + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code mapper} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/flatmap.html">ReactiveX operators documentation: FlatMap</a> + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public final <@NonNull R> Observable<R> flatMapObservable(@NonNull Function<? super T, ? extends ObservableSource<? extends R>> mapper) { + Objects.requireNonNull(mapper, "mapper is null"); + return RxJavaPlugins.onAssembly(new MaybeFlatMapObservable<>(this, mapper)); + } + + /** + * Returns a {@link Flowable} that emits items based on applying a specified function to the item emitted by the + * current {@code Maybe}, where that function returns a {@link Publisher}. + * <p> + * <img width="640" height="312" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.flatMapPublisher.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The returned {@code Flowable} honors the downstream backpressure.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code flatMapPublisher} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <R> the result value type + * @param mapper + * a function that, when applied to the item emitted by the current {@code Maybe}, returns a + * {@code Flowable} + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code mapper} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/flatmap.html">ReactiveX operators documentation: FlatMap</a> + */ + @BackpressureSupport(BackpressureKind.FULL) + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public final <@NonNull R> Flowable<R> flatMapPublisher(@NonNull Function<? super T, @NonNull ? extends Publisher<? extends R>> mapper) { + Objects.requireNonNull(mapper, "mapper is null"); + return RxJavaPlugins.onAssembly(new MaybeFlatMapPublisher<>(this, mapper)); + } + + /** + * Returns a {@code Maybe} based on applying a specified function to the item emitted by the + * current {@code Maybe}, where that function returns a {@link Single}. + * When this {@code Maybe} just completes the resulting {@code Maybe} completes as well. + * <p> + * <img width="640" height="357" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.flatMapSingle.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code flatMapSingle} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * <p>History: 2.0.2 - experimental + * @param <R> the result value type + * @param mapper + * a function that, when applied to the item emitted by the current {@code Maybe}, returns a + * {@code Single} + * @return the new {@code Maybe} instance + * @throws NullPointerException if {@code mapper} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/flatmap.html">ReactiveX operators documentation: FlatMap</a> + * @since 2.1 + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public final <@NonNull R> Maybe<R> flatMapSingle(@NonNull Function<? super T, ? extends SingleSource<? extends R>> mapper) { + Objects.requireNonNull(mapper, "mapper is null"); + return RxJavaPlugins.onAssembly(new MaybeFlatMapSingle<>(this, mapper)); + } + + /** + * Returns a {@link Completable} that completes based on applying a specified function to the item emitted by the + * current {@code Maybe}, where that function returns a {@code Completable}. + * <p> + * <img width="640" height="303" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.flatMapCompletable3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code flatMapCompletable} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param mapper + * a function that, when applied to the item emitted by the current {@code Maybe}, returns a + * {@code Completable} + * @return the new {@code Completable} instance + * @throws NullPointerException if {@code mapper} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/flatmap.html">ReactiveX operators documentation: FlatMap</a> + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public final Completable flatMapCompletable(@NonNull Function<? super T, ? extends CompletableSource> mapper) { + Objects.requireNonNull(mapper, "mapper is null"); + return RxJavaPlugins.onAssembly(new MaybeFlatMapCompletable<>(this, mapper)); + } + + /** + * Hides the identity of this {@code Maybe} and its {@link Disposable}. + * <p> + * <img width="640" height="300" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.hide.png" alt=""> + * <p>Allows preventing certain identity-based + * optimizations (fusion). + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code hide} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @return the new {@code Maybe} instance + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Maybe<T> hide() { + return RxJavaPlugins.onAssembly(new MaybeHide<>(this)); + } + + /** + * Returns a {@link Completable} that ignores the item emitted by the current {@code Maybe} and only calls {@code onComplete} or {@code onError}. + * <p> + * <img width="640" height="390" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.ignoreElement.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code ignoreElement} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @return the new {@code Completable} instance + * @see <a href="http://reactivex.io/documentation/operators/ignoreelements.html">ReactiveX operators documentation: IgnoreElements</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Completable ignoreElement() { + return RxJavaPlugins.onAssembly(new MaybeIgnoreElementCompletable<>(this)); + } + + /** + * Returns a {@link Single} that emits {@code true} if the current {@code Maybe} is empty, otherwise {@code false}. + * <p> + * <img width="640" height="444" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.isEmpty.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code isEmpty} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @return the new {@code Single} instance + * @see <a href="http://reactivex.io/documentation/operators/contains.html">ReactiveX operators documentation: Contains</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Single<Boolean> isEmpty() { + return RxJavaPlugins.onAssembly(new MaybeIsEmptySingle<>(this)); + } + + /** + * <strong>This method requires advanced knowledge about building operators, please consider + * other standard composition methods first;</strong> + * Returns a {@code Maybe} which, when subscribed to, invokes the {@link MaybeOperator#apply(MaybeObserver) apply(MaybeObserver)} method + * of the provided {@link MaybeOperator} for each individual downstream {@code Maybe} and allows the + * insertion of a custom operator by accessing the downstream's {@link MaybeObserver} during this subscription phase + * and providing a new {@code MaybeObserver}, containing the custom operator's intended business logic, that will be + * used in the subscription process going further upstream. + * <p> + * <img width="640" height="352" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.lift.png" alt=""> + * <p> + * Generally, such a new {@code MaybeObserver} will wrap the downstream's {@code MaybeObserver} and forwards the + * {@code onSuccess}, {@code onError} and {@code onComplete} events from the upstream directly or according to the + * emission pattern the custom operator's business logic requires. In addition, such operator can intercept the + * flow control calls of {@code dispose} and {@code isDisposed} that would have traveled upstream and perform + * additional actions depending on the same business logic requirements. + * <p> + * Example: + * <pre><code> + * // Step 1: Create the consumer type that will be returned by the MaybeOperator.apply(): + * + * public final class CustomMaybeObserver<T> implements MaybeObserver<T>, Disposable { + * + * // The downstream's MaybeObserver that will receive the onXXX events + * final MaybeObserver<? super String> downstream; + * + * // The connection to the upstream source that will call this class' onXXX methods + * Disposable upstream; + * + * // The constructor takes the downstream subscriber and usually any other parameters + * public CustomMaybeObserver(MaybeObserver<? super String> downstream) { + * this.downstream = downstream; + * } + * + * // In the subscription phase, the upstream sends a Disposable to this class + * // and subsequently this class has to send a Disposable to the downstream. + * // Note that relaying the upstream's Disposable directly is not allowed in RxJava + * @Override + * public void onSubscribe(Disposable d) { + * if (upstream != null) { + * d.dispose(); + * } else { + * upstream = d; + * downstream.onSubscribe(this); + * } + * } + * + * // The upstream calls this with the next item and the implementation's + * // responsibility is to emit an item to the downstream based on the intended + * // business logic, or if it can't do so for the particular item, + * // request more from the upstream + * @Override + * public void onSuccess(T item) { + * String str = item.toString(); + * if (str.length() < 2) { + * downstream.onSuccess(str); + * } else { + * // Maybe is expected to produce one of the onXXX events only + * downstream.onComplete(); + * } + * } + * + * // Some operators may handle the upstream's error while others + * // could just forward it to the downstream. + * @Override + * public void onError(Throwable throwable) { + * downstream.onError(throwable); + * } + * + * // When the upstream completes, usually the downstream should complete as well. + * @Override + * public void onComplete() { + * downstream.onComplete(); + * } + * + * // Some operators may use their own resources which should be cleaned up if + * // the downstream disposes the flow before it completed. Operators without + * // resources can simply forward the dispose to the upstream. + * // In some cases, a disposed flag may be set by this method so that other parts + * // of this class may detect the dispose and stop sending events + * // to the downstream. + * @Override + * public void dispose() { + * upstream.dispose(); + * } + * + * // Some operators may simply forward the call to the upstream while others + * // can return the disposed flag set in dispose(). + * @Override + * public boolean isDisposed() { + * return upstream.isDisposed(); + * } + * } + * + * // Step 2: Create a class that implements the MaybeOperator interface and + * // returns the custom consumer type from above in its apply() method. + * // Such class may define additional parameters to be submitted to + * // the custom consumer type. + * + * final class CustomMaybeOperator<T> implements MaybeOperator<String> { + * @Override + * public MaybeObserver<? super String> apply(MaybeObserver<? super T> upstream) { + * return new CustomMaybeObserver<T>(upstream); + * } + * } + * + * // Step 3: Apply the custom operator via lift() in a flow by creating an instance of it + * // or reusing an existing one. + * + * Maybe.just(5) + * .lift(new CustomMaybeOperator<Integer>()) + * .test() + * .assertResult("5"); + * + * Maybe.just(15) + * .lift(new CustomMaybeOperator<Integer>()) + * .test() + * .assertResult(); + * </code></pre> + * <p> + * Creating custom operators can be complicated and it is recommended one consults the + * <a href="https://github.com/ReactiveX/RxJava/wiki/Writing-operators-for-2.0">RxJava wiki: Writing operators</a> page about + * the tools, requirements, rules, considerations and pitfalls of implementing them. + * <p> + * Note that implementing custom operators via this {@code lift()} method adds slightly more overhead by requiring + * an additional allocation and indirection per assembled flows. Instead, extending the abstract {@code Maybe} + * class and creating a {@link MaybeTransformer} with it is recommended. + * <p> + * Note also that it is not possible to stop the subscription phase in {@code lift()} as the {@code apply()} method + * requires a non-{@code null} {@code MaybeObserver} instance to be returned, which is then unconditionally subscribed to + * the current {@code Maybe}. For example, if the operator decided there is no reason to subscribe to the + * upstream source because of some optimization possibility or a failure to prepare the operator, it still has to + * return a {@code MaybeObserver} that should immediately dispose the upstream's {@link Disposable} in its + * {@code onSubscribe} method. Again, using a {@code MaybeTransformer} and extending the {@code Maybe} is + * a better option as {@link #subscribeActual} can decide to not subscribe to its upstream after all. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code lift} does not operate by default on a particular {@link Scheduler}, however, the + * {@code MaybeOperator} may use a {@code Scheduler} to support its own asynchronous behavior.</dd> + * </dl> + * + * @param <R> the output value type + * @param lift the {@code MaybeOperator} that receives the downstream's {@code MaybeObserver} and should return + * a {@code MaybeObserver} with custom behavior to be used as the consumer for the current + * {@code Maybe}. + * @return the new {@code Maybe} instance + * @throws NullPointerException if {@code lift} is {@code null} + * @see <a href="https://github.com/ReactiveX/RxJava/wiki/Writing-operators-for-2.0">RxJava wiki: Writing operators</a> + * @see #compose(MaybeTransformer) + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public final <@NonNull R> Maybe<R> lift(@NonNull MaybeOperator<? extends R, ? super T> lift) { + Objects.requireNonNull(lift, "lift is null"); + return RxJavaPlugins.onAssembly(new MaybeLift<>(this, lift)); + } + + /** + * Returns a {@code Maybe} that applies a specified function to the item emitted by the current {@code Maybe} and + * emits the result of this function application. + * <p> + * <img width="640" height="515" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.map.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code map} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <R> the result value type + * @param mapper + * a function to apply to the item emitted by the {@code Maybe} + * @return the new {@code Maybe} instance + * @throws NullPointerException if {@code mapper} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/map.html">ReactiveX operators documentation: Map</a> + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public final <@NonNull R> Maybe<R> map(@NonNull Function<? super T, ? extends R> mapper) { + Objects.requireNonNull(mapper, "mapper is null"); + return RxJavaPlugins.onAssembly(new MaybeMap<>(this, mapper)); + } + + /** + * Maps the signal types of this {@code Maybe} into a {@link Notification} of the same kind + * and emits it as a {@link Single}'s {@code onSuccess} value to downstream. + * <p> + * <img width="640" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/materialize.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code materialize} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * <p>History: 2.2.4 - experimental + * @return the new {@code Single} instance + * @since 3.0.0 + * @see Single#dematerialize(Function) + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Single<Notification<T>> materialize() { + return RxJavaPlugins.onAssembly(new MaybeMaterialize<>(this)); + } + + /** + * Flattens this {@code Maybe} and another {@link MaybeSource} into a single {@link Flowable}, without any transformation. + * <p> + * <img width="640" height="218" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.mergeWith.png" alt=""> + * <p> + * You can combine items emitted by multiple {@code Maybe}s so that they appear as a single {@code Flowable}, by + * using the {@code mergeWith} method. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code mergeWith} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param other + * a {@code MaybeSource} to be merged + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code other} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/merge.html">ReactiveX operators documentation: Merge</a> + */ + @BackpressureSupport(BackpressureKind.FULL) + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public final Flowable<T> mergeWith(@NonNull MaybeSource<? extends T> other) { + Objects.requireNonNull(other, "other is null"); + return merge(this, other); + } + + /** + * Wraps a {@code Maybe} to emit its item (or notify of its error) on a specified {@link Scheduler}, + * asynchronously. + * <p> + * <img width="640" height="183" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.observeOn.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>you specify which {@code Scheduler} this operator will use.</dd> + * </dl> + * + * @param scheduler + * the {@code Scheduler} to notify subscribers on + * @return the new {@code Maybe} instance that its subscribers are notified on the specified + * {@code Scheduler} + * @throws NullPointerException if {@code scheduler} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/observeon.html">ReactiveX operators documentation: ObserveOn</a> + * @see <a href="http://www.grahamlea.com/2014/07/rxjava-threading-examples/">RxJava Threading Examples</a> + * @see #subscribeOn + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.CUSTOM) + public final Maybe<T> observeOn(@NonNull Scheduler scheduler) { + Objects.requireNonNull(scheduler, "scheduler is null"); + return RxJavaPlugins.onAssembly(new MaybeObserveOn<>(this, scheduler)); + } + + /** + * Filters the items emitted by the current {@code Maybe}, only emitting its success value if that + * is an instance of the supplied {@link Class}. + * <p> + * <img width="640" height="291" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.ofType.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code ofType} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <U> the output type + * @param clazz + * the class type to filter the items emitted by the current {@code Maybe} + * @return the new {@code Maybe} instance + * @throws NullPointerException if {@code clazz} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/filter.html">ReactiveX operators documentation: Filter</a> + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public final <@NonNull U> Maybe<U> ofType(@NonNull Class<U> clazz) { + Objects.requireNonNull(clazz, "clazz is null"); + return filter(Functions.isInstanceOf(clazz)).cast(clazz); + } + + /** + * Calls the specified converter function during assembly time and returns its resulting value. + * <p> + * <img width="640" height="731" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.to.png" alt=""> + * <p> + * This allows fluent conversion to any other type. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code to} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * <p>History: 2.1.7 - experimental + * @param <R> the resulting object type + * @param converter the function that receives the current {@code Maybe} instance and returns a value + * @return the converted value + * @throws NullPointerException if {@code converter} is {@code null} + * @since 2.2 + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + public final <R> R to(@NonNull MaybeConverter<T, ? extends R> converter) { + return Objects.requireNonNull(converter, "converter is null").apply(this); + } + + /** + * Converts this {@code Maybe} into a backpressure-aware {@link Flowable} instance composing cancellation + * through. + * <p> + * <img width="640" height="346" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.toFlowable.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The returned {@code Flowable} honors the backpressure of the downstream.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code toFlowable} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @return the new {@code Flowable} instance + */ + @SuppressWarnings("unchecked") + @BackpressureSupport(BackpressureKind.FULL) + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Flowable<T> toFlowable() { + if (this instanceof FuseToFlowable) { + return ((FuseToFlowable<T>)this).fuseToFlowable(); + } + return RxJavaPlugins.onAssembly(new MaybeToFlowable<>(this)); + } + + /** + * Returns a {@link Future} representing the single value emitted by the current {@code Maybe} + * or {@code null} if the current {@code Maybe} is empty. + * <p> + * <img width="640" height="292" src="https://github.com/ReactiveX/RxJava/wiki/images/rx-operators/Maybe.toFuture.png" alt=""> + * <p> + * Cancelling the {@code Future} will cancel the subscription to the current {@code Maybe}. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code toFuture} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @return the new {@code Future} instance + * @see <a href="http://reactivex.io/documentation/operators/to.html">ReactiveX documentation: To</a> + * @since 3.0.0 + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Future<T> toFuture() { + return subscribeWith(new FutureMultiObserver<>()); + } + + /** + * Converts this {@code Maybe} into an {@link Observable} instance composing disposal + * through. + * <p> + * <img width="640" height="346" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.toObservable.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code toObservable} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @return the new {@code Observable} instance + */ + @SuppressWarnings("unchecked") + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Observable<T> toObservable() { + if (this instanceof FuseToObservable) { + return ((FuseToObservable<T>)this).fuseToObservable(); + } + return RxJavaPlugins.onAssembly(new MaybeToObservable<>(this)); + } + + /** + * Converts this {@code Maybe} into a {@link Single} instance composing disposal + * through and turning an empty {@code Maybe} into a signal of {@link NoSuchElementException}. + * <p> + * <img width="640" height="361" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.toSingle.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code toSingle} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @return the new {@code Single} instance + * @see #defaultIfEmpty(Object) + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Single<T> toSingle() { + return RxJavaPlugins.onAssembly(new MaybeToSingle<>(this, null)); + } + + /** + * Returns a {@code Maybe} instance that if this {@code Maybe} emits an error, it will emit an {@code onComplete} + * and swallow the throwable. + * <p> + * <img width="640" height="372" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.onErrorComplete.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code onErrorComplete} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @return the new {@code Maybe} instance + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Maybe<T> onErrorComplete() { + return onErrorComplete(Functions.alwaysTrue()); + } + + /** + * Returns a {@code Maybe} instance that if this {@code Maybe} emits an error and the predicate returns + * {@code true}, it will emit an {@code onComplete} and swallow the throwable. + * <p> + * <img width="640" height="220" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.onErrorComplete.f.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code onErrorComplete} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param predicate the predicate to call when an {@link Throwable} is emitted which should return {@code true} + * if the {@code Throwable} should be swallowed and replaced with an {@code onComplete}. + * @return the new {@code Maybe} instance + * @throws NullPointerException if {@code predicate} is {@code null} + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public final Maybe<T> onErrorComplete(@NonNull Predicate<? super Throwable> predicate) { + Objects.requireNonNull(predicate, "predicate is null"); + + return RxJavaPlugins.onAssembly(new MaybeOnErrorComplete<>(this, predicate)); + } + + /** + * Resumes the flow with the given {@link MaybeSource} when the current {@code Maybe} fails instead of + * signaling the error via {@code onError}. + * <p> + * <img width="640" height="298" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.onErrorResumeWith.png" alt=""> + * <p> + * You can use this to prevent errors from propagating or to supply fallback data should errors be + * encountered. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code onErrorResumeWith} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param fallback + * the next {@code MaybeSource} that will take over if the current {@code Maybe} encounters + * an error + * @return the new {@code Maybe} instance + * @throws NullPointerException if {@code fallback} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/catch.html">ReactiveX operators documentation: Catch</a> + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public final Maybe<T> onErrorResumeWith(@NonNull MaybeSource<? extends T> fallback) { + Objects.requireNonNull(fallback, "fallback is null"); + return onErrorResumeNext(Functions.justFunction(fallback)); + } + + /** + * Resumes the flow with a {@link MaybeSource} returned for the failure {@link Throwable} of the current {@code Maybe} by a + * function instead of signaling the error via {@code onError}. + * <p> + * <img width="640" height="298" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.onErrorResumeNext.png" alt=""> + * <p> + * You can use this to prevent errors from propagating or to supply fallback data should errors be + * encountered. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code onErrorResumeNext} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param fallbackSupplier + * a function that returns a {@code MaybeSource} that will take over if the current {@code Maybe} encounters + * an error + * @return the new {@code Maybe} instance + * @throws NullPointerException if {@code fallbackSupplier} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/catch.html">ReactiveX operators documentation: Catch</a> + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public final Maybe<T> onErrorResumeNext(@NonNull Function<? super Throwable, ? extends MaybeSource<? extends T>> fallbackSupplier) { + Objects.requireNonNull(fallbackSupplier, "fallbackSupplier is null"); + return RxJavaPlugins.onAssembly(new MaybeOnErrorNext<>(this, fallbackSupplier)); + } + + /** + * Ends the flow with a success item returned by a function for the {@link Throwable} error signaled by the current + * {@code Maybe} instead of signaling the error via {@code onError}. + * <p> + * <img width="640" height="377" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.onErrorReturn.png" alt=""> + * <p> + * You can use this to prevent errors from propagating or to supply fallback data should errors be + * encountered. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code onErrorReturn} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param itemSupplier + * a function that returns a single value that will be emitted as success value + * the current {@code Maybe} signals an {@code onError} event + * @return the new {@code Maybe} instance + * @throws NullPointerException if {@code itemSupplier} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/catch.html">ReactiveX operators documentation: Catch</a> + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public final Maybe<T> onErrorReturn(@NonNull Function<? super Throwable, ? extends T> itemSupplier) { + Objects.requireNonNull(itemSupplier, "itemSupplier is null"); + return RxJavaPlugins.onAssembly(new MaybeOnErrorReturn<>(this, itemSupplier)); + } + + /** + * Ends the flow with the given success item when the current {@code Maybe} fails instead of signaling the error via {@code onError}. + * <p> + * <img width="640" height="377" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.onErrorReturnItem.png" alt=""> + * <p> + * You can use this to prevent errors from propagating or to supply fallback data should errors be + * encountered. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code onErrorReturnItem} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param item + * the value that is emitted as {@code onSuccess} in case the current {@code Maybe} signals an {@code onError} + * @return the new {@code Maybe} instance + * @throws NullPointerException if {@code item} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/catch.html">ReactiveX operators documentation: Catch</a> + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public final Maybe<T> onErrorReturnItem(@NonNull T item) { + Objects.requireNonNull(item, "item is null"); + return onErrorReturn(Functions.justFunction(item)); + } + + /** + * Nulls out references to the upstream producer and downstream {@link MaybeObserver} if + * the sequence is terminated or downstream calls {@code dispose()}. + * <p> + * <img width="640" height="263" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.onTerminateDetach.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code onTerminateDetach} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @return the new {@code Maybe} instance + * the sequence is terminated or downstream calls {@code dispose()} + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Maybe<T> onTerminateDetach() { + return RxJavaPlugins.onAssembly(new MaybeDetach<>(this)); + } + + /** + * Returns a {@link Flowable} that repeats the sequence of items emitted by the current {@code Maybe} indefinitely. + * <p> + * <img width="640" height="276" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.repeat.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors downstream backpressure.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code repeat} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @return the new {@code Flowable} instance + * @see <a href="http://reactivex.io/documentation/operators/repeat.html">ReactiveX operators documentation: Repeat</a> + */ + @BackpressureSupport(BackpressureKind.FULL) + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Flowable<T> repeat() { + return repeat(Long.MAX_VALUE); + } + + /** + * Returns a {@link Flowable} that repeats the sequence of items emitted by the current {@code Maybe} at most + * {@code count} times. + * <p> + * <img width="640" height="294" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.repeat.n.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>This operator honors downstream backpressure.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code repeat} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param times + * the number of times the current {@code Maybe} items are repeated, a count of 0 will yield an empty + * sequence + * @return the new {@code Flowable} instance + * @throws IllegalArgumentException + * if {@code times} is negative + * @see <a href="http://reactivex.io/documentation/operators/repeat.html">ReactiveX operators documentation: Repeat</a> + */ + @BackpressureSupport(BackpressureKind.FULL) + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Flowable<T> repeat(long times) { + return toFlowable().repeat(times); + } + + /** + * Returns a {@link Flowable} that repeats the sequence of items emitted by the current {@code Maybe} until + * the provided stop function returns {@code true}. + * <p> + * <img width="640" height="329" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.repeatUntil.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>This operator honors downstream backpressure.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code repeatUntil} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param stop + * a boolean supplier that is called when the current {@code Flowable} completes and unless it returns + * {@code false}, the current {@code Flowable} is resubscribed + * @return the new {@code Flowable} instance + * @throws NullPointerException + * if {@code stop} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/repeat.html">ReactiveX operators documentation: Repeat</a> + */ + @BackpressureSupport(BackpressureKind.FULL) + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Flowable<T> repeatUntil(@NonNull BooleanSupplier stop) { + return toFlowable().repeatUntil(stop); + } + + /** + * Returns a {@link Flowable} that emits the same values as the current {@code Maybe} with the exception of an + * {@code onComplete}. An {@code onComplete} notification from the source will result in the emission of + * a {@code void} item to the {@code Flowable} provided as an argument to the {@code notificationHandler} + * function. If that {@link Publisher} calls {@code onComplete} or {@code onError} then {@code repeatWhen} will + * call {@code onComplete} or {@code onError} on the child observer. Otherwise, this operator will + * resubscribe to the current {@code Maybe}. + * <p> + * <img width="640" height="562" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.repeatWhen.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors downstream backpressure and expects the source {@code Publisher} to honor backpressure as well. + * If this expectation is violated, the operator <em>may</em> throw an {@link IllegalStateException}.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code repeatWhen} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param handler + * receives a {@code Publisher} of notifications with which a user can complete or error, aborting the repeat. + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code handler} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/repeat.html">ReactiveX operators documentation: Repeat</a> + */ + @BackpressureSupport(BackpressureKind.FULL) + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Flowable<T> repeatWhen(@NonNull Function<? super Flowable<Object>, @NonNull ? extends Publisher<@NonNull ?>> handler) { + return toFlowable().repeatWhen(handler); + } + + /** + * Returns a {@code Maybe} that mirrors the current {@code Maybe}, resubscribing to it if it calls {@code onError} + * (infinite retry count). + * <p> + * <img width="640" height="393" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.retry.png" alt=""> + * <p> + * If the current {@code Maybe} calls {@link MaybeObserver#onError}, this operator will resubscribe to the current + * {@code Maybe} rather than propagating the {@code onError} call. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code retry} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @return the new {@code Maybe} instance + * @see <a href="http://reactivex.io/documentation/operators/retry.html">ReactiveX operators documentation: Retry</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Maybe<T> retry() { + return retry(Long.MAX_VALUE, Functions.alwaysTrue()); + } + + /** + * Returns a {@code Maybe} that mirrors the current {@code Maybe}, resubscribing to it if it calls {@code onError} + * and the predicate returns {@code true} for that specific exception and retry count. + * <p> + * <img width="640" height="230" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.retry.f.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code retry} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param predicate + * the predicate that determines if a resubscription may happen in case of a specific exception + * and retry count + * @return the new {@code Maybe} instance + * @throws NullPointerException if {@code predicate} is {@code null} + * @see #retry() + * @see <a href="http://reactivex.io/documentation/operators/retry.html">ReactiveX operators documentation: Retry</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Maybe<T> retry(@NonNull BiPredicate<? super Integer, ? super Throwable> predicate) { + return toFlowable().retry(predicate).singleElement(); + } + + /** + * Returns a {@code Maybe} that mirrors the current {@code Maybe}, resubscribing to it if it calls {@code onError} + * up to a specified number of retries. + * <p> + * <img width="640" height="329" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.retry.n.png" alt=""> + * <p> + * If the current {@code Maybe} calls {@link MaybeObserver#onError}, this operator will resubscribe to the current + * {@code Maybe} for a maximum of {@code count} resubscriptions rather than propagating the + * {@code onError} call. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code retry} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param times + * the number of times to resubscribe if the current {@code Maybe} fails + * @return the new {@code Maybe} instance + * @throws IllegalArgumentException if {@code times} is negative + * @see <a href="http://reactivex.io/documentation/operators/retry.html">ReactiveX operators documentation: Retry</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Maybe<T> retry(long times) { + return retry(times, Functions.alwaysTrue()); + } + + /** + * Retries at most {@code times} or until the predicate returns {@code false}, whichever happens first. + * <p> + * <img width="640" height="259" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.retry.nf.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code retry} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param times the number of times to resubscribe if the current {@code Maybe} fails + * @param predicate the predicate called with the failure {@link Throwable} and should return {@code true} to trigger a retry. + * @return the new {@code Maybe} instance + * @throws NullPointerException if {@code predicate} is {@code null} + * @throws IllegalArgumentException if {@code times} is negative + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Maybe<T> retry(long times, @NonNull Predicate<? super Throwable> predicate) { + return toFlowable().retry(times, predicate).singleElement(); + } + + /** + * Retries the current {@code Maybe} if it fails and the predicate returns {@code true}. + * <p> + * <img width="640" height="240" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.retry.g.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code retry} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param predicate the predicate that receives the failure {@link Throwable} and should return {@code true} to trigger a retry. + * @return the new {@code Maybe} instance + * @throws NullPointerException if {@code predicate} is {@code null} + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Maybe<T> retry(@NonNull Predicate<? super Throwable> predicate) { + return retry(Long.MAX_VALUE, predicate); + } + + /** + * Retries until the given stop function returns {@code true}. + * <p> + * <img width="640" height="285" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.retryUntil.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code retryUntil} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param stop the function that should return {@code true} to stop retrying + * @return the new {@code Maybe} instance + * @throws NullPointerException if {@code stop} is {@code null} + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public final Maybe<T> retryUntil(@NonNull BooleanSupplier stop) { + Objects.requireNonNull(stop, "stop is null"); + return retry(Long.MAX_VALUE, Functions.predicateReverseFor(stop)); + } + + /** + * Returns a {@code Maybe} that emits the same values as the current {@code Maybe} with the exception of an + * {@code onError}. An {@code onError} notification from the source will result in the emission of a + * {@link Throwable} item to the {@link Flowable} provided as an argument to the {@code notificationHandler} + * function. If the returned {@link Publisher} calls {@code onComplete} or {@code onError} then {@code retry} will call + * {@code onComplete} or {@code onError} on the child subscription. Otherwise, this operator will + * resubscribe to the current {@code Maybe}. + * <p> + * <img width="640" height="405" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.retryWhen.png" alt=""> + * <p> + * Example: + * + * This retries 3 times, each time incrementing the number of seconds it waits. + * + * <pre><code> + * Maybe.create((MaybeEmitter<? super String> s) -> { + * System.out.println("subscribing"); + * s.onError(new RuntimeException("always fails")); + * }, BackpressureStrategy.BUFFER).retryWhen(attempts -> { + * return attempts.zipWith(Publisher.range(1, 3), (n, i) -> i).flatMap(i -> { + * System.out.println("delay retry by " + i + " second(s)"); + * return Flowable.timer(i, TimeUnit.SECONDS); + * }); + * }).blockingForEach(System.out::println); + * </code></pre> + * + * Output is: + * + * <pre> {@code + * subscribing + * delay retry by 1 second(s) + * subscribing + * delay retry by 2 second(s) + * subscribing + * delay retry by 3 second(s) + * subscribing + * } </pre> + * <p> + * Note that the inner {@code Publisher} returned by the handler function should signal + * either {@code onNext}, {@code onError} or {@code onComplete} in response to the received + * {@code Throwable} to indicate the operator should retry or terminate. If the upstream to + * the operator is asynchronous, signalling {@code onNext} followed by {@code onComplete} immediately may + * result in the sequence to be completed immediately. Similarly, if this inner + * {@code Publisher} signals {@code onError} or {@code onComplete} while the upstream is + * active, the sequence is terminated with the same signal immediately. + * <p> + * The following example demonstrates how to retry an asynchronous source with a delay: + * <pre><code> + * Maybe.timer(1, TimeUnit.SECONDS) + * .doOnSubscribe(s -> System.out.println("subscribing")) + * .map(v -> { throw new RuntimeException(); }) + * .retryWhen(errors -> { + * AtomicInteger counter = new AtomicInteger(); + * return errors + * .takeWhile(e -> counter.getAndIncrement() != 3) + * .flatMap(e -> { + * System.out.println("delay retry by " + counter.get() + " second(s)"); + * return Flowable.timer(counter.get(), TimeUnit.SECONDS); + * }); + * }) + * .blockingGet(); + * </code></pre> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code retryWhen} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param handler + * receives a {@code Publisher} of notifications with which a user can complete or error, aborting the + * retry + * @return the new {@code Maybe} instance + * @throws NullPointerException if {@code handler} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/retry.html">ReactiveX operators documentation: Retry</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Maybe<T> retryWhen( + @NonNull Function<? super Flowable<Throwable>, @NonNull ? extends Publisher<@NonNull ?>> handler) { + return toFlowable().retryWhen(handler).singleElement(); + } + + /** + * Wraps the given {@link MaybeObserver}, catches any {@link RuntimeException}s thrown by its + * {@link MaybeObserver#onSubscribe(Disposable)}, {@link MaybeObserver#onSuccess(Object)}, + * {@link MaybeObserver#onError(Throwable)} or {@link MaybeObserver#onComplete()} methods + * and routes those to the global error handler via {@link RxJavaPlugins#onError(Throwable)}. + * <p> + * By default, the {@code Maybe} protocol forbids the {@code onXXX} methods to throw, but some + * {@code MaybeObserver} implementation may do it anyway, causing undefined behavior in the + * upstream. This method and the underlying safe wrapper ensures such misbehaving consumers don't + * disrupt the protocol. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code safeSubscribe} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param observer the potentially misbehaving {@code MaybeObserver} + * @throws NullPointerException if {@code observer} is {@code null} + * @see #subscribe(Consumer,Consumer, Action) + * @since 3.0.0 + */ + @SchedulerSupport(SchedulerSupport.NONE) + public final void safeSubscribe(@NonNull MaybeObserver<? super T> observer) { + Objects.requireNonNull(observer, "observer is null"); + subscribe(new SafeMaybeObserver<>(observer)); + } + + /** + * Returns a {@link Flowable} which first runs the other {@link CompletableSource} + * then the current {@code Maybe} if the other completed normally. + * <p> + * <img width="640" height="268" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.startWith.c.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The returned {@code Flowable} honors the backpressure of the downstream consumer.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code startWith} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param other the other {@code CompletableSource} to run first + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code other} is {@code null} + * @since 3.0.0 + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + @BackpressureSupport(BackpressureKind.FULL) + public final Flowable<T> startWith(@NonNull CompletableSource other) { + Objects.requireNonNull(other, "other is null"); + return Flowable.concat(Completable.wrap(other).<T>toFlowable(), toFlowable()); + } + + /** + * Returns a {@link Flowable} which first runs the other {@link SingleSource} + * then the current {@code Maybe} if the other succeeded normally. + * <p> + * <img width="640" height="237" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.startWith.s.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The returned {@code Flowable} honors the backpressure of the downstream consumer.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code startWith} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param other the other {@code SingleSource} to run first + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code other} is {@code null} + * @since 3.0.0 + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + @BackpressureSupport(BackpressureKind.FULL) + public final Flowable<T> startWith(@NonNull SingleSource<T> other) { + Objects.requireNonNull(other, "other is null"); + return Flowable.concat(Single.wrap(other).toFlowable(), toFlowable()); + } + + /** + * Returns a {@link Flowable} which first runs the other {@link MaybeSource} + * then the current {@code Maybe} if the other succeeded or completed normally. + * <p> + * <img width="640" height="178" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.startWith.m.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The returned {@code Flowable} honors the backpressure of the downstream consumer.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code startWith} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param other the other {@code MaybeSource} to run first + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code other} is {@code null} + * @since 3.0.0 + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + @BackpressureSupport(BackpressureKind.FULL) + public final Flowable<T> startWith(@NonNull MaybeSource<T> other) { + Objects.requireNonNull(other, "other is null"); + return Flowable.concat(Maybe.wrap(other).toFlowable(), toFlowable()); + } + + /** + * Returns an {@link Observable} which first delivers the events + * of the other {@link ObservableSource} then runs the current {@code Maybe}. + * <p> + * <img width="640" height="179" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.startWith.o.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code startWith} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param other the other {@code ObservableSource} to run first + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code other} is {@code null} + * @since 3.0.0 + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public final Observable<T> startWith(@NonNull ObservableSource<T> other) { + Objects.requireNonNull(other, "other is null"); + return Observable.wrap(other).concatWith(this.toObservable()); + } + + /** + * Returns a {@link Flowable} which first delivers the events + * of the other {@link Publisher} then runs the current {@code Maybe}. + * <p> + * <img width="640" height="179" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.startWith.p.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The returned {@code Flowable} honors the backpressure of the downstream consumer + * and expects the other {@code Publisher} to honor it as well.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code startWith} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param other the other {@code Publisher} to run first + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code other} is {@code null} + * @since 3.0.0 + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + public final Flowable<T> startWith(@NonNull Publisher<T> other) { + Objects.requireNonNull(other, "other is null"); + return toFlowable().startWith(other); + } + + /** + * Subscribes to a {@code Maybe} and ignores {@code onSuccess} and {@code onComplete} emissions. + * <p> + * If the {@code Maybe} emits an error, it is wrapped into an + * {@link io.reactivex.rxjava3.exceptions.OnErrorNotImplementedException OnErrorNotImplementedException} + * and routed to the {@link RxJavaPlugins#onError(Throwable)} handler. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code subscribe} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @return the new {@link Disposable} instance that can be used for disposing the subscription at any time + * @see <a href="http://reactivex.io/documentation/operators/subscribe.html">ReactiveX operators documentation: Subscribe</a> + * @see #subscribe(Consumer, Consumer, Action, DisposableContainer) + */ + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Disposable subscribe() { + return subscribe(Functions.emptyConsumer(), Functions.ON_ERROR_MISSING, Functions.EMPTY_ACTION); + } + + /** + * Subscribes to a {@code Maybe} and provides a callback to handle the items it emits. + * <p> + * If the {@code Maybe} emits an error, it is wrapped into an + * {@link io.reactivex.rxjava3.exceptions.OnErrorNotImplementedException OnErrorNotImplementedException} + * and routed to the {@link RxJavaPlugins#onError(Throwable)} handler. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code subscribe} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param onSuccess + * the {@code Consumer<T>} you have designed to accept a success value from the {@code Maybe} + * @return the new {@link Disposable} instance that can be used for disposing the subscription at any time + * @throws NullPointerException + * if {@code onSuccess} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/subscribe.html">ReactiveX operators documentation: Subscribe</a> + * @see #subscribe(Consumer, Consumer, Action, DisposableContainer) + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Disposable subscribe(@NonNull Consumer<? super T> onSuccess) { + return subscribe(onSuccess, Functions.ON_ERROR_MISSING, Functions.EMPTY_ACTION); + } + + /** + * Subscribes to a {@code Maybe} and provides callbacks to handle the items it emits and any error + * notification it issues. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code subscribe} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param onSuccess + * the {@code Consumer<T>} you have designed to accept a success value from the {@code Maybe} + * @param onError + * the {@code Consumer<Throwable>} you have designed to accept any error notification from the + * {@code Maybe} + * @return the new {@link Disposable} instance that can be used for disposing the subscription at any time + * @throws NullPointerException + * if {@code onSuccess} is {@code null}, or + * if {@code onError} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/subscribe.html">ReactiveX operators documentation: Subscribe</a> + * @see #subscribe(Consumer, Consumer, Action, DisposableContainer) + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Disposable subscribe(@NonNull Consumer<? super T> onSuccess, @NonNull Consumer<? super Throwable> onError) { + return subscribe(onSuccess, onError, Functions.EMPTY_ACTION); + } + + /** + * Subscribes to a {@code Maybe} and provides callbacks to handle the items it emits and any error or + * completion notification it issues. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code subscribe} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param onSuccess + * the {@code Consumer<T>} you have designed to accept a success value from the {@code Maybe} + * @param onError + * the {@code Consumer<Throwable>} you have designed to accept any error notification from the + * {@code Maybe} + * @param onComplete + * the {@link Action} you have designed to accept a completion notification from the + * {@code Maybe} + * @return the new {@link Disposable} instance that can be used for disposing the subscription at any time + * @throws NullPointerException + * if {@code onSuccess}, {@code onError} or + * {@code onComplete} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/subscribe.html">ReactiveX operators documentation: Subscribe</a> + * @see #subscribe(Consumer, Consumer, Action, DisposableContainer) + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public final Disposable subscribe(@NonNull Consumer<? super T> onSuccess, @NonNull Consumer<? super Throwable> onError, + @NonNull Action onComplete) { + Objects.requireNonNull(onSuccess, "onSuccess is null"); + Objects.requireNonNull(onError, "onError is null"); + Objects.requireNonNull(onComplete, "onComplete is null"); + return subscribeWith(new MaybeCallbackObserver<>(onSuccess, onError, onComplete)); + } + + /** + * Wraps the given onXXX callbacks into a {@link Disposable} {@link MaybeObserver}, + * adds it to the given {@link DisposableContainer} and ensures, that if the upstream + * terminates or this particular {@code Disposable} is disposed, the {@code MaybeObserver} is removed + * from the given composite. + * <p> + * The {@code MaybeObserver} will be removed after the callback for the terminal event has been invoked. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code subscribe} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param onSuccess the callback for upstream items + * @param onError the callback for an upstream error + * @param onComplete the callback for an upstream completion without any value or error + * @param container the {@code DisposableContainer} (such as {@link CompositeDisposable}) to add and remove the + * created {@code Disposable} {@code MaybeObserver} + * @return the {@code Disposable} that allows disposing the particular subscription. + * @throws NullPointerException + * if {@code onSuccess}, {@code onError}, + * {@code onComplete} or {@code container} is {@code null} + * @since 3.1.0 + */ + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Disposable subscribe( + @NonNull Consumer<? super T> onSuccess, + @NonNull Consumer<? super Throwable> onError, + @NonNull Action onComplete, + @NonNull DisposableContainer container) { + Objects.requireNonNull(onSuccess, "onSuccess is null"); + Objects.requireNonNull(onError, "onError is null"); + Objects.requireNonNull(onComplete, "onComplete is null"); + Objects.requireNonNull(container, "container is null"); + + DisposableAutoReleaseMultiObserver<T> observer = new DisposableAutoReleaseMultiObserver<>( + container, onSuccess, onError, onComplete); + container.add(observer); + subscribe(observer); + return observer; + } + + @SchedulerSupport(SchedulerSupport.NONE) + @Override + public final void subscribe(@NonNull MaybeObserver<? super T> observer) { + Objects.requireNonNull(observer, "observer is null"); + + observer = RxJavaPlugins.onSubscribe(this, observer); + + Objects.requireNonNull(observer, "The RxJavaPlugins.onSubscribe hook returned a null MaybeObserver. Please check the handler provided to RxJavaPlugins.setOnMaybeSubscribe for invalid null returns. Further reading: https://github.com/ReactiveX/RxJava/wiki/Plugins"); + + try { + subscribeActual(observer); + } catch (NullPointerException ex) { + throw ex; + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + NullPointerException npe = new NullPointerException("subscribeActual failed"); + npe.initCause(ex); + throw npe; + } + } + + /** + * Implement this method in subclasses to handle the incoming {@link MaybeObserver}s. + * <p>There is no need to call any of the plugin hooks on the current {@code Maybe} instance or + * the {@code MaybeObserver}; all hooks and basic safeguards have been + * applied by {@link #subscribe(MaybeObserver)} before this method gets called. + * @param observer the {@code MaybeObserver} to handle, not {@code null} + */ + protected abstract void subscribeActual(@NonNull MaybeObserver<? super T> observer); + + /** + * Asynchronously subscribes subscribers to this {@code Maybe} on the specified {@link Scheduler}. + * <p> + * <img width="640" height="753" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.subscribeOn.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>you specify which {@code Scheduler} this operator will use.</dd> + * </dl> + * + * @param scheduler + * the {@code Scheduler} to perform subscription actions on + * @return the new {@code Maybe} instance + * @throws NullPointerException if {@code scheduler} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/subscribeon.html">ReactiveX operators documentation: SubscribeOn</a> + * @see <a href="http://www.grahamlea.com/2014/07/rxjava-threading-examples/">RxJava Threading Examples</a> + * @see #observeOn + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.CUSTOM) + public final Maybe<T> subscribeOn(@NonNull Scheduler scheduler) { + Objects.requireNonNull(scheduler, "scheduler is null"); + return RxJavaPlugins.onAssembly(new MaybeSubscribeOn<>(this, scheduler)); + } + + /** + * Subscribes a given {@link MaybeObserver} (subclass) to this {@code Maybe} and returns the given + * {@code MaybeObserver} as is. + * <p>Usage example: + * <pre><code> + * Maybe<Integer> source = Maybe.just(1); + * CompositeDisposable composite = new CompositeDisposable(); + * + * DisposableMaybeObserver<Integer> ds = new DisposableMaybeObserver<>() { + * // ... + * }; + * + * composite.add(source.subscribeWith(ds)); + * </code></pre> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code subscribeWith} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <E> the type of the {@code MaybeObserver} to use and return + * @param observer the {@code MaybeObserver} (subclass) to use and return, not {@code null} + * @return the input {@code observer} + * @throws NullPointerException if {@code observer} is {@code null} + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull E extends MaybeObserver<? super T>> E subscribeWith(E observer) { + subscribe(observer); + return observer; + } + + /** + * Returns a {@code Maybe} that emits the items emitted by the current {@code Maybe} or the items of an alternate + * {@link MaybeSource} if the current {@code Maybe} is empty. + * <p> + * <img width="640" height="222" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.switchIfEmpty.m.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code switchIfEmpty} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param other + * the alternate {@code MaybeSource} to subscribe to if the main does not emit any items + * @return the new {@code Maybe} instance + * @throws NullPointerException if {@code other} is {@code null} + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public final Maybe<T> switchIfEmpty(@NonNull MaybeSource<? extends T> other) { + Objects.requireNonNull(other, "other is null"); + return RxJavaPlugins.onAssembly(new MaybeSwitchIfEmpty<>(this, other)); + } + + /** + * Returns a {@link Single} that emits the items emitted by the current {@code Maybe} or the item of an alternate + * {@link SingleSource} if the current {@code Maybe} is empty. + * <p> + * <img width="640" height="312" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.switchIfEmpty.s.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code switchIfEmpty} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * <p>History: 2.1.4 - experimental + * @param other + * the alternate {@code SingleSource} to subscribe to if the main does not emit any items + * @return the new {@code Single} instance + * @throws NullPointerException if {@code other} is {@code null} + * @since 2.2 + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public final Single<T> switchIfEmpty(@NonNull SingleSource<? extends T> other) { + Objects.requireNonNull(other, "other is null"); + return RxJavaPlugins.onAssembly(new MaybeSwitchIfEmptySingle<>(this, other)); + } + + /** + * Returns a {@code Maybe} that emits the items emitted by the current {@code Maybe} until a second {@link MaybeSource} + * emits an item. + * <p> + * <img width="640" height="219" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.takeUntil.m.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code takeUntil} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param other + * the {@code MaybeSource} whose first emitted item will cause {@code takeUntil} to stop emitting items + * from the current {@code Maybe} + * @param <U> + * the type of items emitted by {@code other} + * @return the new {@code Maybe} instance + * @throws NullPointerException if {@code other} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/takeuntil.html">ReactiveX operators documentation: TakeUntil</a> + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public final <@NonNull U> Maybe<T> takeUntil(@NonNull MaybeSource<U> other) { + Objects.requireNonNull(other, "other is null"); + return RxJavaPlugins.onAssembly(new MaybeTakeUntilMaybe<>(this, other)); + } + + /** + * Returns a {@code Maybe} that emits the item emitted by the current {@code Maybe} until a second {@link Publisher} + * emits an item. + * <p> + * <img width="640" height="199" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.takeUntil.p.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The {@code Publisher} is consumed in an unbounded fashion and is cancelled after the first item + * emitted.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code takeUntil} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param other + * the {@code Publisher} whose first emitted item will cause {@code takeUntil} to stop emitting items + * from the source {@code Publisher} + * @param <U> + * the type of items emitted by {@code other} + * @return the new {@code Maybe} instance + * @throws NullPointerException if {@code other} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/takeuntil.html">ReactiveX operators documentation: TakeUntil</a> + */ + @BackpressureSupport(BackpressureKind.UNBOUNDED_IN) + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public final <@NonNull U> Maybe<T> takeUntil(@NonNull Publisher<U> other) { + Objects.requireNonNull(other, "other is null"); + return RxJavaPlugins.onAssembly(new MaybeTakeUntilPublisher<>(this, other)); + } + + /** + * Measures the time (in milliseconds) between the subscription and success item emission + * of the current {@code Maybe} and signals it as a tuple ({@link Timed}) + * success value. + * <p> + * <img width="640" height="352" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.timeInterval.png" alt=""> + * <p> + * If the current {@code Maybe} is empty or fails, the resulting {@code Maybe} will + * pass along the signals to the downstream. To measure the time to termination, + * use {@link #materialize()} and apply {@link Single#timeInterval()}. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code timeInterval} uses the {@code computation} {@link Scheduler} + * for determining the current time upon subscription and upon receiving the + * success item from the current {@code Maybe}.</dd> + * </dl> + * @return the new {@code Maybe} instance + * @since 3.0.0 + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.COMPUTATION) + public final Maybe<Timed<T>> timeInterval() { + return timeInterval(TimeUnit.MILLISECONDS, Schedulers.computation()); + } + + /** + * Measures the time (in milliseconds) between the subscription and success item emission + * of the current {@code Maybe} and signals it as a tuple ({@link Timed}) + * success value. + * <p> + * <img width="640" height="355" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.timeInterval.s.png" alt=""> + * <p> + * If the current {@code Maybe} is empty or fails, the resulting {@code Maybe} will + * pass along the signals to the downstream. To measure the time to termination, + * use {@link #materialize()} and apply {@link Single#timeInterval(Scheduler)}. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code timeInterval} uses the provided {@link Scheduler} + * for determining the current time upon subscription and upon receiving the + * success item from the current {@code Maybe}.</dd> + * </dl> + * @param scheduler the {@code Scheduler} used for providing the current time + * @return the new {@code Maybe} instance + * @throws NullPointerException if {@code scheduler} is {@code null} + * @since 3.0.0 + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.CUSTOM) + public final Maybe<Timed<T>> timeInterval(@NonNull Scheduler scheduler) { + return timeInterval(TimeUnit.MILLISECONDS, scheduler); + } + + /** + * Measures the time between the subscription and success item emission + * of the current {@code Maybe} and signals it as a tuple ({@link Timed}) + * success value. + * <p> + * <img width="640" height="352" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.timeInterval.png" alt=""> + * <p> + * If the current {@code Maybe} is empty or fails, the resulting {@code Maybe} will + * pass along the signals to the downstream. To measure the time to termination, + * use {@link #materialize()} and apply {@link Single#timeInterval(TimeUnit)}. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code timeInterval} uses the {@code computation} {@link Scheduler} + * for determining the current time upon subscription and upon receiving the + * success item from the current {@code Maybe}.</dd> + * </dl> + * @param unit the time unit for measurement + * @return the new {@code Maybe} instance + * @throws NullPointerException if {@code unit} is {@code null} + * @since 3.0.0 + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.COMPUTATION) + public final Maybe<Timed<T>> timeInterval(@NonNull TimeUnit unit) { + return timeInterval(unit, Schedulers.computation()); + } + + /** + * Measures the time between the subscription and success item emission + * of the current {@code Maybe} and signals it as a tuple ({@link Timed}) + * success value. + * <p> + * <img width="640" height="355" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.timeInterval.s.png" alt=""> + * <p> + * If the current {@code Maybe} is empty or fails, the resulting {@code Maybe} will + * pass along the signals to the downstream. To measure the time to termination, + * use {@link #materialize()} and apply {@link Single#timeInterval(TimeUnit, Scheduler)}. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code timeInterval} uses the provided {@link Scheduler} + * for determining the current time upon subscription and upon receiving the + * success item from the current {@code Maybe}.</dd> + * </dl> + * @param unit the time unit for measurement + * @param scheduler the {@code Scheduler} used for providing the current time + * @return the new {@code Maybe} instance + * @throws NullPointerException if {@code unit} or {@code scheduler} is {@code null} + * @since 3.0.0 + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.CUSTOM) + public final Maybe<Timed<T>> timeInterval(@NonNull TimeUnit unit, @NonNull Scheduler scheduler) { + Objects.requireNonNull(unit, "unit is null"); + Objects.requireNonNull(scheduler, "scheduler is null"); + return RxJavaPlugins.onAssembly(new MaybeTimeInterval<>(this, unit, scheduler, true)); + } + + /** + * Combines the success value from the current {@code Maybe} with the current time (in milliseconds) of + * its reception, using the {@code computation} {@link Scheduler} as time source, + * then signals them as a {@link Timed} instance. + * <p> + * <img width="640" height="352" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.timestamp.png" alt=""> + * <p> + * If the current {@code Maybe} is empty or fails, the resulting {@code Maybe} will + * pass along the signals to the downstream. To measure the time to termination, + * use {@link #materialize()} and apply {@link Single#timestamp()}. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code timestamp} uses the {@code computation} {@code Scheduler} + * for determining the current time upon receiving the + * success item from the current {@code Maybe}.</dd> + * </dl> + * @return the new {@code Maybe} instance + * @since 3.0.0 + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.COMPUTATION) + public final Maybe<Timed<T>> timestamp() { + return timestamp(TimeUnit.MILLISECONDS, Schedulers.computation()); + } + + /** + * Combines the success value from the current {@code Maybe} with the current time (in milliseconds) of + * its reception, using the given {@link Scheduler} as time source, + * then signals them as a {@link Timed} instance. + * <p> + * <img width="640" height="355" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.timestamp.s.png" alt=""> + * <p> + * If the current {@code Maybe} is empty or fails, the resulting {@code Maybe} will + * pass along the signals to the downstream. To measure the time to termination, + * use {@link #materialize()} and apply {@link Single#timestamp(Scheduler)}. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code timestamp} uses the provided {@code Scheduler} + * for determining the current time upon receiving the + * success item from the current {@code Maybe}.</dd> + * </dl> + * @param scheduler the {@code Scheduler} used for providing the current time + * @return the new {@code Maybe} instance + * @throws NullPointerException if {@code scheduler} is {@code null} + * @since 3.0.0 + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.CUSTOM) + public final Maybe<Timed<T>> timestamp(@NonNull Scheduler scheduler) { + return timestamp(TimeUnit.MILLISECONDS, scheduler); + } + + /** + * Combines the success value from the current {@code Maybe} with the current time of + * its reception, using the {@code computation} {@link Scheduler} as time source, + * then signals it as a {@link Timed} instance. + * <p> + * <img width="640" height="352" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.timestamp.png" alt=""> + * <p> + * If the current {@code Maybe} is empty or fails, the resulting {@code Maybe} will + * pass along the signals to the downstream. To measure the time to termination, + * use {@link #materialize()} and apply {@link Single#timestamp(TimeUnit)}. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code timestamp} uses the {@code computation} {@code Scheduler}, + * for determining the current time upon receiving the + * success item from the current {@code Maybe}.</dd> + * </dl> + * @param unit the time unit for measurement + * @return the new {@code Maybe} instance + * @throws NullPointerException if {@code unit} is {@code null} + * @since 3.0.0 + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.COMPUTATION) + public final Maybe<Timed<T>> timestamp(@NonNull TimeUnit unit) { + return timestamp(unit, Schedulers.computation()); + } + + /** + * Combines the success value from the current {@code Maybe} with the current time of + * its reception, using the given {@link Scheduler} as time source, + * then signals it as a {@link Timed} instance. + * <p> + * <img width="640" height="355" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.timestamp.s.png" alt=""> + * <p> + * If the current {@code Maybe} is empty or fails, the resulting {@code Maybe} will + * pass along the signals to the downstream. To measure the time to termination, + * use {@link #materialize()} and apply {@link Single#timestamp(TimeUnit, Scheduler)}. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code timestamp} uses the provided {@code Scheduler}, + * which is used for determining the current time upon receiving the + * success item from the current {@code Maybe}.</dd> + * </dl> + * @param unit the time unit for measurement + * @param scheduler the {@code Scheduler} used for providing the current time + * @return the new {@code Maybe} instance + * @throws NullPointerException if {@code unit} or {@code scheduler} is {@code null} + * @since 3.0.0 + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.CUSTOM) + public final Maybe<Timed<T>> timestamp(@NonNull TimeUnit unit, @NonNull Scheduler scheduler) { + Objects.requireNonNull(unit, "unit is null"); + Objects.requireNonNull(scheduler, "scheduler is null"); + return RxJavaPlugins.onAssembly(new MaybeTimeInterval<>(this, unit, scheduler, false)); + } + + /** + * Returns a {@code Maybe} that mirrors the current {@code Maybe} but applies a timeout policy for each emitted + * item. If the next item isn't emitted within the specified timeout duration starting from its predecessor, + * the resulting {@code Maybe} terminates and notifies {@link MaybeObserver}s of a {@link TimeoutException}. + * <p> + * <img width="640" height="261" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.timeout.t.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>This version of {@code timeout} operates by default on the {@code computation} {@link Scheduler}.</dd> + * </dl> + * + * @param timeout + * maximum duration between emitted items before a timeout occurs + * @param unit + * the unit of time that applies to the {@code timeout} argument. + * @return the new {@code Maybe} instance + * @throws NullPointerException if {@code unit} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/timeout.html">ReactiveX operators documentation: Timeout</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.COMPUTATION) + @NonNull + public final Maybe<T> timeout(long timeout, @NonNull TimeUnit unit) { + return timeout(timeout, unit, Schedulers.computation()); + } + + /** + * Returns a {@code Maybe} that mirrors the current {@code Maybe} but applies a timeout policy for each emitted + * item. If the next item isn't emitted within the specified timeout duration starting from its predecessor, + * the current {@code Maybe} is disposed and resulting {@code Maybe} begins instead to mirror a fallback {@link MaybeSource}. + * <p> + * <img width="640" height="226" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.timeout.tm.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>This version of {@code timeout} operates by default on the {@code computation} {@link Scheduler}.</dd> + * </dl> + * + * @param timeout + * maximum duration between items before a timeout occurs + * @param unit + * the unit of time that applies to the {@code timeout} argument + * @param fallback + * the fallback {@code MaybeSource} to use in case of a timeout + * @return the new {@code Maybe} instance + * @throws NullPointerException if {@code unit} or {@code fallback} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/timeout.html">ReactiveX operators documentation: Timeout</a> + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.COMPUTATION) + public final Maybe<T> timeout(long timeout, @NonNull TimeUnit unit, @NonNull MaybeSource<? extends T> fallback) { + Objects.requireNonNull(fallback, "fallback is null"); + return timeout(timeout, unit, Schedulers.computation(), fallback); + } + + /** + * Returns a {@code Maybe} that mirrors the current {@code Maybe} but applies a timeout policy for each emitted + * item using a specified {@link Scheduler}. If the next item isn't emitted within the specified timeout duration + * starting from its predecessor, the current {@code Maybe} is disposed and resulting {@code Maybe} begins instead + * to mirror a fallback {@link MaybeSource}. + * <p> + * <img width="640" height="227" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.timeout.tsm.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>You specify which {@code Scheduler} this operator will use.</dd> + * </dl> + * + * @param timeout + * maximum duration between items before a timeout occurs + * @param unit + * the unit of time that applies to the {@code timeout} argument + * @param fallback + * the {@code MaybeSource} to use as the fallback in case of a timeout + * @param scheduler + * the {@code Scheduler} to run the timeout timers on + * @return the new {@code Maybe} instance + * @throws NullPointerException if {@code fallback}, {@code unit} or {@code scheduler} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/timeout.html">ReactiveX operators documentation: Timeout</a> + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.CUSTOM) + public final Maybe<T> timeout(long timeout, @NonNull TimeUnit unit, @NonNull Scheduler scheduler, @NonNull MaybeSource<? extends T> fallback) { + Objects.requireNonNull(fallback, "fallback is null"); + return timeout(timer(timeout, unit, scheduler), fallback); + } + + /** + * Returns a {@code Maybe} that mirrors the current {@code Maybe} but applies a timeout policy for each emitted + * item, where this policy is governed on a specified {@link Scheduler}. If the next item isn't emitted within the + * specified timeout duration starting from its predecessor, the resulting {@code Maybe} terminates and + * notifies {@link MaybeObserver}s of a {@link TimeoutException}. + * <p> + * <img width="640" height="261" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.timeout.ts.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>You specify which {@code Scheduler} this operator will use.</dd> + * </dl> + * + * @param timeout + * maximum duration between items before a timeout occurs + * @param unit + * the unit of time that applies to the {@code timeout} argument + * @param scheduler + * the {@code Scheduler} to run the timeout timers on + * @return the new {@code Maybe} instance + * @see <a href="http://reactivex.io/documentation/operators/timeout.html">ReactiveX operators documentation: Timeout</a> + * @throws NullPointerException if {@code unit} or {@code scheduler} is {@code null} + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.CUSTOM) + @NonNull + public final Maybe<T> timeout(long timeout, @NonNull TimeUnit unit, @NonNull Scheduler scheduler) { + return timeout(timer(timeout, unit, scheduler)); + } + + /** + * If the current {@code Maybe} didn't signal an event before the {@code timeoutIndicator} {@link MaybeSource} signals, a + * {@link TimeoutException} is signaled instead. + * <p> + * <img width="640" height="235" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.timeout.m.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code timeout} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <U> the value type of the + * @param timeoutIndicator the {@code MaybeSource} that indicates the timeout by signaling {@code onSuccess} + * or {@code onComplete}. + * @return the new {@code Maybe} instance + * @throws NullPointerException if {@code timeoutIndicator} is {@code null} + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public final <@NonNull U> Maybe<T> timeout(@NonNull MaybeSource<U> timeoutIndicator) { + Objects.requireNonNull(timeoutIndicator, "timeoutIndicator is null"); + return RxJavaPlugins.onAssembly(new MaybeTimeoutMaybe<>(this, timeoutIndicator, null)); + } + + /** + * If the current {@code Maybe} didn't signal an event before the {@code timeoutIndicator} {@link MaybeSource} signals, + * the current {@code Maybe} is disposed and the {@code fallback} {@code MaybeSource} subscribed to + * as a continuation. + * <p> + * <img width="640" height="194" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.timeout.mm.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code timeout} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <U> the value type of the + * @param timeoutIndicator the {@code MaybeSource} that indicates the timeout by signaling {@code onSuccess} + * or {@code onComplete}. + * @param fallback the {@code MaybeSource} that is subscribed to if the current {@code Maybe} times out + * @return the new {@code Maybe} instance + * @throws NullPointerException if {@code timeoutIndicator} or {@code fallback} is {@code null} + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public final <@NonNull U> Maybe<T> timeout(@NonNull MaybeSource<U> timeoutIndicator, @NonNull MaybeSource<? extends T> fallback) { + Objects.requireNonNull(timeoutIndicator, "timeoutIndicator is null"); + Objects.requireNonNull(fallback, "fallback is null"); + return RxJavaPlugins.onAssembly(new MaybeTimeoutMaybe<>(this, timeoutIndicator, fallback)); + } + + /** + * If the current {@code Maybe} source didn't signal an event before the {@code timeoutIndicator} {@link Publisher} signals, a + * {@link TimeoutException} is signaled instead. + * <p> + * <img width="640" height="212" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.timeout.p.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The {@code timeoutIndicator} {@code Publisher} is consumed in an unbounded manner and + * is cancelled after its first item.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code timeout} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <U> the value type of the + * @param timeoutIndicator the {@code Publisher} that indicates the timeout by signaling {@code onSuccess} + * or {@code onComplete}. + * @return the new {@code Maybe} instance + * @throws NullPointerException if {@code timeoutIndicator} is {@code null} + */ + @BackpressureSupport(BackpressureKind.UNBOUNDED_IN) + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public final <@NonNull U> Maybe<T> timeout(@NonNull Publisher<U> timeoutIndicator) { + Objects.requireNonNull(timeoutIndicator, "timeoutIndicator is null"); + return RxJavaPlugins.onAssembly(new MaybeTimeoutPublisher<>(this, timeoutIndicator, null)); + } + + /** + * If the current {@code Maybe} didn't signal an event before the {@code timeoutIndicator} {@link Publisher} signals, + * the current {@code Maybe} is disposed and the {@code fallback} {@link MaybeSource} subscribed to + * as a continuation. + * <p> + * <img width="640" height="169" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.timeout.pm.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The {@code timeoutIndicator} {@code Publisher} is consumed in an unbounded manner and + * is cancelled after its first item.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code timeout} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <U> the value type of the + * @param timeoutIndicator the {@code MaybeSource} that indicates the timeout by signaling {@code onSuccess} + * or {@code onComplete} + * @param fallback the {@code MaybeSource} that is subscribed to if the current {@code Maybe} times out + * @return the new {@code Maybe} instance + * @throws NullPointerException if {@code timeoutIndicator} or {@code fallback} is {@code null} + */ + @BackpressureSupport(BackpressureKind.UNBOUNDED_IN) + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public final <@NonNull U> Maybe<T> timeout(@NonNull Publisher<U> timeoutIndicator, @NonNull MaybeSource<? extends T> fallback) { + Objects.requireNonNull(timeoutIndicator, "timeoutIndicator is null"); + Objects.requireNonNull(fallback, "fallback is null"); + return RxJavaPlugins.onAssembly(new MaybeTimeoutPublisher<>(this, timeoutIndicator, fallback)); + } + + /** + * Returns a {@code Maybe} which makes sure when a {@link MaybeObserver} disposes the {@link Disposable}, + * that call is propagated up on the specified {@link Scheduler}. + * <p> + * <img width="640" height="693" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.unsubscribeOn.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code unsubscribeOn} calls {@code dispose()} of the upstream on the {@code Scheduler} you specify.</dd> + * </dl> + * @param scheduler the target scheduler where to execute the disposal + * @return the new {@code Maybe} instance + * @throws NullPointerException if {@code scheduler} is {@code null} + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.CUSTOM) + public final Maybe<T> unsubscribeOn(@NonNull Scheduler scheduler) { + Objects.requireNonNull(scheduler, "scheduler is null"); + return RxJavaPlugins.onAssembly(new MaybeUnsubscribeOn<>(this, scheduler)); + } + + /** + * Waits until this and the other {@link MaybeSource} signal a success value then applies the given {@link BiFunction} + * to those values and emits the {@code BiFunction}'s resulting value to downstream. + * + * <img width="640" height="451" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.zipWith.png" alt=""> + * + * <p>If either this or the other {@code MaybeSource} is empty or signals an error, the resulting {@code Maybe} will + * terminate immediately and dispose the other source. + * + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code zipWith} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <U> + * the type of items emitted by the {@code other} {@code MaybeSource} + * @param <R> + * the type of items emitted by the resulting {@code Maybe} + * @param other + * the other {@code MaybeSource} + * @param zipper + * a function that combines the pairs of items from the two {@code MaybeSource}s to generate the items to + * be emitted by the resulting {@code Maybe} + * @return the new {@code Maybe} instance + * @throws NullPointerException if {@code other} or {@code zipper} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/zip.html">ReactiveX operators documentation: Zip</a> + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public final <@NonNull U, @NonNull R> Maybe<R> zipWith(@NonNull MaybeSource<? extends U> other, @NonNull BiFunction<? super T, ? super U, ? extends R> zipper) { + Objects.requireNonNull(other, "other is null"); + return zip(this, other, zipper); + } + + // ------------------------------------------------------------------ + // Test helper + // ------------------------------------------------------------------ + + /** + * Creates a {@link TestObserver} and subscribes + * it to this {@code Maybe}. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code test} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @return the new {@code TestObserver} instance + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final TestObserver<T> test() { + TestObserver<T> to = new TestObserver<>(); + subscribe(to); + return to; + } + + /** + * Creates a {@link TestObserver} optionally in cancelled state, then subscribes it to this {@code Maybe}. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code test} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param dispose if {@code true}, the {@code TestObserver} will be disposed before subscribing to this + * {@code Maybe}. + * @return the new {@code TestObserver} instance + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final TestObserver<T> test(boolean dispose) { + TestObserver<T> to = new TestObserver<>(); + + if (dispose) { + to.dispose(); + } + + subscribe(to); + return to; + } + + // ------------------------------------------------------------------------- + // JDK 8 Support + // ------------------------------------------------------------------------- + + /** + * Converts the existing value of the provided optional into a {@link #just(Object)} + * or an empty optional into an {@link #empty()} {@code Maybe} instance. + * <p> + * <img width="640" height="335" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/fromOptional.m.png" alt=""> + * <p> + * Note that the operator takes an already instantiated optional reference and does not + * by any means create this original optional. If the optional is to be created per + * consumer upon subscription, use {@link #defer(Supplier)} around {@code fromOptional}: + * <pre><code> + * Maybe.defer(() -> Maybe.fromOptional(createOptional())); + * </code></pre> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code fromOptional} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <T> the element type of the optional value + * @param optional the optional value to convert into a {@code Maybe} + * @return the new {@code Maybe} instance + * @throws NullPointerException if {@code optional} is {@code null} + * @since 3.0.0 + * @see #just(Object) + * @see #empty() + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public static <@NonNull T> Maybe<@NonNull T> fromOptional(@NonNull Optional<T> optional) { + Objects.requireNonNull(optional, "optional is null"); + return optional.map(Maybe::just).orElseGet(Maybe::empty); + } + + /** + * Signals the completion value or error of the given (hot) {@link CompletionStage}-based asynchronous calculation. + * <p> + * <img width="640" height="262" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/fromCompletionStage.s.png" alt=""> + * <p> + * Note that the operator takes an already instantiated, running or terminated {@code CompletionStage}. + * If the {@code CompletionStage} is to be created per consumer upon subscription, use {@link #defer(Supplier)} + * around {@code fromCompletionStage}: + * <pre><code> + * Maybe.defer(() -> Maybe.fromCompletionStage(createCompletionStage())); + * </code></pre> + * <p> + * If the {@code CompletionStage} completes with {@code null}, the resulting {@code Maybe} is completed via {@code onComplete}. + * <p> + * Canceling the flow can't cancel the execution of the {@code CompletionStage} because {@code CompletionStage} + * itself doesn't support cancellation. Instead, the operator detaches from the {@code CompletionStage}. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code fromCompletionStage} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <T> the element type of the {@code CompletionStage} + * @param stage the {@code CompletionStage} to convert to {@code Maybe} and signal its terminal value or error + * @return the new {@code Maybe} instance + * @throws NullPointerException if {@code stage} is {@code null} + * @since 3.0.0 + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public static <@NonNull T> Maybe<@NonNull T> fromCompletionStage(@NonNull CompletionStage<T> stage) { + Objects.requireNonNull(stage, "stage is null"); + return RxJavaPlugins.onAssembly(new MaybeFromCompletionStage<>(stage)); + } + + /** + * Maps the upstream success value into an {@link Optional} and emits the contained item if not empty. + * <p> + * <img width="640" height="323" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/mapOptional.m.png" alt=""> + * + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code mapOptional} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <R> the non-{@code null} output type + * @param mapper the function that receives the upstream success item and should return a <em>non-empty</em> {@code Optional} + * to emit as the success output or an <em>empty</em> {@code Optional} to complete the {@code Maybe} + * @return the new {@code Maybe} instance + * @throws NullPointerException if {@code mapper} is {@code null} + * @since 3.0.0 + * @see #map(Function) + * @see #filter(Predicate) + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull R> Maybe<R> mapOptional(@NonNull Function<? super T, @NonNull Optional<? extends R>> mapper) { + Objects.requireNonNull(mapper, "mapper is null"); + return RxJavaPlugins.onAssembly(new MaybeMapOptional<>(this, mapper)); + } + + /** + * Signals the upstream success item (or a {@link NoSuchElementException} if the upstream is empty) via + * a {@link CompletionStage}. + * <p> + * <img width="640" height="349" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/toCompletionStage.m.png" alt=""> + * <p> + * The upstream can be canceled by converting the resulting {@code CompletionStage} into + * {@link CompletableFuture} via {@link CompletionStage#toCompletableFuture()} and + * calling {@link CompletableFuture#cancel(boolean)} on it. + * The upstream will be also cancelled if the resulting {@code CompletionStage} is converted to and + * completed manually by {@link CompletableFuture#complete(Object)} or {@link CompletableFuture#completeExceptionally(Throwable)}. + * <p> + * {@code CompletionStage}s don't have a notion of emptiness and allow {@code null}s, therefore, one can either use + * {@link #toCompletionStage(Object)} with {@code null} or turn the upstream into a sequence of {@link Optional}s and + * default to {@link Optional#empty()}: + * <pre><code> + * CompletionStage<Optional<T>> stage = source.map(Optional::of).toCompletionStage(Optional.empty()); + * </code></pre> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code toCompletionStage} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @return the new {@code CompletionStage} instance + * @since 3.0.0 + * @see #toCompletionStage(Object) + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final CompletionStage<T> toCompletionStage() { + return subscribeWith(new CompletionStageConsumer<>(false, null)); + } + + /** + * Signals the upstream success item (or the default item if the upstream is empty) via + * a {@link CompletionStage}. + * <p> + * <img width="640" height="323" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/toCompletionStage.mv.png" alt=""> + * <p> + * The upstream can be canceled by converting the resulting {@code CompletionStage} into + * {@link CompletableFuture} via {@link CompletionStage#toCompletableFuture()} and + * calling {@link CompletableFuture#cancel(boolean)} on it. + * The upstream will be also cancelled if the resulting {@code CompletionStage} is converted to and + * completed manually by {@link CompletableFuture#complete(Object)} or {@link CompletableFuture#completeExceptionally(Throwable)}. + * <p> + * {@code CompletionStage}s don't have a notion of emptiness and allow {@code null}s, therefore, one can either use + * a {@code defaultItem} of {@code null} or turn the flow into a sequence of {@link Optional}s and default to {@link Optional#empty()}: + * <pre><code> + * CompletionStage<Optional<T>> stage = source.map(Optional::of).toCompletionStage(Optional.empty()); + * </code></pre> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code toCompletionStage} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param defaultItem the item to signal if the upstream is empty + * @return the new {@code CompletionStage} instance + * @since 3.0.0 + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final CompletionStage<T> toCompletionStage(@Nullable T defaultItem) { + return subscribeWith(new CompletionStageConsumer<>(true, defaultItem)); + } + + /** + * Maps the upstream succecss value into a Java {@link Stream} and emits its + * items to the downstream consumer as a {@link Flowable}. + * <p> + * <img width="640" height="246" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/flattenStreamAsFlowable.m.png" alt=""> + * <p> + * The operator closes the {@code Stream} upon cancellation and when it terminates. The exceptions raised when + * closing a {@code Stream} are routed to the global error handler ({@link RxJavaPlugins#onError(Throwable)}. + * If a {@code Stream} should not be closed, turn it into an {@link Iterable} and use {@link #flattenAsFlowable(Function)}: + * <pre><code> + * source.flattenAsFlowable(item -> createStream(item)::iterator); + * </code></pre> + * <p> + * Primitive streams are not supported and items have to be boxed manually (e.g., via {@link IntStream#boxed()}): + * <pre><code> + * source.flattenStreamAsFlowable(item -> IntStream.rangeClosed(1, 10).boxed()); + * </code></pre> + * <p> + * {@code Stream} does not support concurrent usage so creating and/or consuming the same instance multiple times + * from multiple threads can lead to undefined behavior. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream and iterates the given {@code Stream} + * on demand (i.e., when requested).</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code flattenStreamAsFlowable} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <R> the element type of the {@code Stream} and the output {@code Flowable} + * @param mapper the function that receives the upstream success item and should + * return a {@code Stream} of values to emit. + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code mapper} is {@code null} + * @since 3.0.0 + * @see #flattenAsFlowable(Function) + * @see #flattenStreamAsObservable(Function) + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @BackpressureSupport(BackpressureKind.FULL) + @NonNull + public final <@NonNull R> Flowable<R> flattenStreamAsFlowable(@NonNull Function<? super T, @NonNull ? extends Stream<? extends R>> mapper) { + Objects.requireNonNull(mapper, "mapper is null"); + return RxJavaPlugins.onAssembly(new MaybeFlattenStreamAsFlowable<>(this, mapper)); + } + + /** + * Maps the upstream succecss value into a Java {@link Stream} and emits its + * items to the downstream consumer as an {@link Observable}. + * <img width="640" height="241" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/flattenStreamAsObservable.m.png" alt=""> + * <p> + * The operator closes the {@code Stream} upon cancellation and when it terminates. The exceptions raised when + * closing a {@code Stream} are routed to the global error handler ({@link RxJavaPlugins#onError(Throwable)}. + * If a {@code Stream} should not be closed, turn it into an {@link Iterable} and use {@link #flattenAsObservable(Function)}: + * <pre><code> + * source.flattenAsObservable(item -> createStream(item)::iterator); + * </code></pre> + * <p> + * Primitive streams are not supported and items have to be boxed manually (e.g., via {@link IntStream#boxed()}): + * <pre><code> + * source.flattenStreamAsObservable(item -> IntStream.rangeClosed(1, 10).boxed()); + * </code></pre> + * <p> + * {@code Stream} does not support concurrent usage so creating and/or consuming the same instance multiple times + * from multiple threads can lead to undefined behavior. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code flattenStreamAsObservable} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <R> the element type of the {@code Stream} and the output {@code Observable} + * @param mapper the function that receives the upstream success item and should + * return a {@code Stream} of values to emit. + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code mapper} is {@code null} + * @since 3.0.0 + * @see #flattenAsObservable(Function) + * @see #flattenStreamAsFlowable(Function) + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull R> Observable<R> flattenStreamAsObservable(@NonNull Function<? super T, @NonNull ? extends Stream<? extends R>> mapper) { + Objects.requireNonNull(mapper, "mapper is null"); + return RxJavaPlugins.onAssembly(new MaybeFlattenStreamAsObservable<>(this, mapper)); + } +} diff --git a/src/main/java/io/reactivex/rxjava3/core/MaybeConverter.java b/src/main/java/io/reactivex/rxjava3/core/MaybeConverter.java new file mode 100644 index 0000000000..6ef529de7e --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/core/MaybeConverter.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.core; + +import io.reactivex.rxjava3.annotations.NonNull; + +/** + * Convenience interface and callback used by the {@link Maybe#to} operator to turn a {@link Maybe} into another + * value fluently. + * <p>History: 2.1.7 - experimental + * @param <T> the upstream type + * @param <R> the output type + * @since 2.2 + */ +@FunctionalInterface +public interface MaybeConverter<@NonNull T, @NonNull R> { + /** + * Applies a function to the upstream {@link Maybe} and returns a converted value of type {@code R}. + * + * @param upstream the upstream {@code Maybe} instance + * @return the converted value + */ + @NonNull + R apply(@NonNull Maybe<T> upstream); +} diff --git a/src/main/java/io/reactivex/rxjava3/core/MaybeEmitter.java b/src/main/java/io/reactivex/rxjava3/core/MaybeEmitter.java new file mode 100644 index 0000000000..57d7f5a219 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/core/MaybeEmitter.java @@ -0,0 +1,109 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.core; + +import io.reactivex.rxjava3.annotations.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.functions.*; + +/** + * Abstraction over an RxJava {@link MaybeObserver} that allows associating + * a resource with it. + * <p> + * All methods are safe to call from multiple threads, but note that there is no guarantee + * whose terminal event will win and get delivered to the downstream. + * <p> + * Calling {@link #onSuccess(Object)} or {@link #onComplete()} multiple times has no effect. + * Calling {@link #onError(Throwable)} multiple times or after the other two will route the + * exception into the global error handler via {@link io.reactivex.rxjava3.plugins.RxJavaPlugins#onError(Throwable)}. + * <p> + * The emitter allows the registration of a single resource, in the form of a {@link Disposable} + * or {@link Cancellable} via {@link #setDisposable(Disposable)} or {@link #setCancellable(Cancellable)} + * respectively. The emitter implementations will dispose/cancel this instance when the + * downstream cancels the flow or after the event generator logic calls {@link #onSuccess(Object)}, + * {@link #onError(Throwable)}, {@link #onComplete()} or when {@link #tryOnError(Throwable)} succeeds. + * <p> + * Only one {@code Disposable} or {@code Cancellable} object can be associated with the emitter at + * a time. Calling either {@code set} method will dispose/cancel any previous object. If there + * is a need for handling multiple resources, one can create a {@link io.reactivex.rxjava3.disposables.CompositeDisposable} + * and associate that with the emitter instead. + * <p> + * The {@link Cancellable} is logically equivalent to {@code Disposable} but allows using cleanup logic that can + * throw a checked exception (such as many {@code close()} methods on Java IO components). Since + * the release of resources happens after the terminal events have been delivered or the sequence gets + * cancelled, exceptions throw within {@code Cancellable} are routed to the global error handler via + * {@link io.reactivex.rxjava3.plugins.RxJavaPlugins#onError(Throwable)}. + * + * @param <T> the value type to emit + */ +public interface MaybeEmitter<@NonNull T> { + + /** + * Signal a success value. + * @param t the value, not null + */ + void onSuccess(@NonNull T t); + + /** + * Signal an exception. + * @param t the exception, not {@code null} + */ + void onError(@NonNull Throwable t); + + /** + * Signal the completion. + */ + void onComplete(); + + /** + * Sets a {@link Disposable} on this emitter; any previous {@code Disposable} + * or {@link Cancellable} will be disposed/cancelled. + * <p>This method is thread-safe. + * @param d the disposable, {@code null} is allowed + */ + void setDisposable(@Nullable Disposable d); + + /** + * Sets a {@link Cancellable} on this emitter; any previous {@link Disposable} + * or {@code Cancellable} will be disposed/cancelled. + * <p>This method is thread-safe. + * @param c the {@code Cancellable} resource, {@code null} is allowed + */ + void setCancellable(@Nullable Cancellable c); + + /** + * Returns true if the downstream disposed the sequence or the + * emitter was terminated via {@link #onSuccess(Object)}, {@link #onError(Throwable)}, + * {@link #onComplete} or a + * successful {@link #tryOnError(Throwable)}. + * <p>This method is thread-safe. + * @return true if the downstream disposed the sequence or the emitter was terminated + */ + boolean isDisposed(); + + /** + * Attempts to emit the specified {@link Throwable} error if the downstream + * hasn't cancelled the sequence or is otherwise terminated, returning false + * if the emission is not allowed to happen due to lifecycle restrictions. + * <p> + * Unlike {@link #onError(Throwable)}, the {@link io.reactivex.rxjava3.plugins.RxJavaPlugins#onError(Throwable) RxjavaPlugins.onError} + * is not called if the error could not be delivered. + * <p>History: 2.1.1 - experimental + * @param t the {@code Throwable} error to signal if possible + * @return true if successful, false if the downstream is not able to accept further + * events + * @since 2.2 + */ + boolean tryOnError(@NonNull Throwable t); +} diff --git a/src/main/java/io/reactivex/rxjava3/core/MaybeObserver.java b/src/main/java/io/reactivex/rxjava3/core/MaybeObserver.java new file mode 100644 index 0000000000..f6567dc94e --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/core/MaybeObserver.java @@ -0,0 +1,92 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.core; + +import io.reactivex.rxjava3.annotations.NonNull; +import io.reactivex.rxjava3.disposables.Disposable; + +/** + * Provides a mechanism for receiving push-based notification of a single value, an error or completion without any value. + * <p> + * When a {@code MaybeObserver} is subscribed to a {@link MaybeSource} through the {@link MaybeSource#subscribe(MaybeObserver)} method, + * the {@code MaybeSource} calls {@link #onSubscribe(Disposable)} with a {@link Disposable} that allows + * disposing the sequence at any time. A well-behaved + * {@code MaybeSource} will call a {@code MaybeObserver}'s {@link #onSuccess(Object)}, {@link #onError(Throwable)} + * or {@link #onComplete()} method exactly once as they are considered mutually exclusive <strong>terminal signals</strong>. + * <p> + * Calling the {@code MaybeObserver}'s method must happen in a serialized fashion, that is, they must not + * be invoked concurrently by multiple threads in an overlapping fashion and the invocation pattern must + * adhere to the following protocol: + * <pre><code> onSubscribe (onSuccess | onError | onComplete)?</code></pre> + * <p> + * Note that unlike with the {@code Observable} protocol, {@link #onComplete()} is not called after the success item has been + * signalled via {@link #onSuccess(Object)}. + * <p> + * Subscribing a {@code MaybeObserver} to multiple {@code MaybeSource}s is not recommended. If such reuse + * happens, it is the duty of the {@code MaybeObserver} implementation to be ready to receive multiple calls to + * its methods and ensure proper concurrent behavior of its business logic. + * <p> + * Calling {@link #onSubscribe(Disposable)}, {@link #onSuccess(Object)} or {@link #onError(Throwable)} with a + * {@code null} argument is forbidden. + * <p> + * The implementations of the {@code onXXX} methods should avoid throwing runtime exceptions other than the following cases: + * <ul> + * <li>If the argument is {@code null}, the methods can throw a {@code NullPointerException}. + * Note though that RxJava prevents {@code null}s to enter into the flow and thus there is generally no + * need to check for nulls in flows assembled from standard sources and intermediate operators. + * </li> + * <li>If there is a fatal error (such as {@code VirtualMachineError}).</li> + * </ul> + * @see <a href="http://reactivex.io/documentation/observable.html">ReactiveX documentation: Observable</a> + * @param <T> + * the type of item the MaybeObserver expects to observe + * @since 2.0 + */ +public interface MaybeObserver<@NonNull T> { + + /** + * Provides the {@link MaybeObserver} with the means of cancelling (disposing) the + * connection (channel) with the {@link Maybe} in both + * synchronous (from within {@code onSubscribe(Disposable)} itself) and asynchronous manner. + * @param d the {@link Disposable} instance whose {@link Disposable#dispose()} can + * be called anytime to cancel the connection + */ + void onSubscribe(@NonNull Disposable d); + + /** + * Notifies the {@link MaybeObserver} with one item and that the {@link Maybe} has finished sending + * push-based notifications. + * <p> + * The {@link Maybe} will not call this method if it calls {@link #onError}. + * + * @param t + * the item emitted by the {@code Maybe} + */ + void onSuccess(@NonNull T t); + + /** + * Notifies the {@link MaybeObserver} that the {@link Maybe} has experienced an error condition. + * <p> + * If the {@link Maybe} calls this method, it will not thereafter call {@link #onSuccess}. + * + * @param e + * the exception encountered by the {@code Maybe} + */ + void onError(@NonNull Throwable e); + + /** + * Called once the deferred computation completes normally. + */ + void onComplete(); +} diff --git a/src/main/java/io/reactivex/rxjava3/core/MaybeOnSubscribe.java b/src/main/java/io/reactivex/rxjava3/core/MaybeOnSubscribe.java new file mode 100644 index 0000000000..67994d3d40 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/core/MaybeOnSubscribe.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.core; + +import io.reactivex.rxjava3.annotations.NonNull; + +/** + * A functional interface that has a {@code subscribe()} method that receives + * a {@link MaybeEmitter} instance that allows pushing + * an event in a cancellation-safe manner. + * + * @param <T> the value type pushed + */ +@FunctionalInterface +public interface MaybeOnSubscribe<@NonNull T> { + + /** + * Called for each {@link MaybeObserver} that subscribes. + * @param emitter the safe emitter instance, never {@code null} + * @throws Throwable on error + */ + void subscribe(@NonNull MaybeEmitter<T> emitter) throws Throwable; +} + diff --git a/src/main/java/io/reactivex/rxjava3/core/MaybeOperator.java b/src/main/java/io/reactivex/rxjava3/core/MaybeOperator.java new file mode 100644 index 0000000000..7cea758dd9 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/core/MaybeOperator.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.core; + +import io.reactivex.rxjava3.annotations.NonNull; + +/** + * Interface to map/wrap a downstream {@link MaybeObserver} to an upstream {@code MaybeObserver}. + * + * @param <Downstream> the value type of the downstream + * @param <Upstream> the value type of the upstream + */ +@FunctionalInterface +public interface MaybeOperator<@NonNull Downstream, @NonNull Upstream> { + /** + * Applies a function to the child {@link MaybeObserver} and returns a new parent {@code MaybeObserver}. + * @param observer the child {@code MaybeObserver} instance + * @return the parent {@code MaybeObserver} instance + * @throws Throwable on failure + */ + @NonNull + MaybeObserver<? super Upstream> apply(@NonNull MaybeObserver<? super Downstream> observer) throws Throwable; +} diff --git a/src/main/java/io/reactivex/rxjava3/core/MaybeSource.java b/src/main/java/io/reactivex/rxjava3/core/MaybeSource.java new file mode 100644 index 0000000000..a15ea2c89d --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/core/MaybeSource.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.core; + +import io.reactivex.rxjava3.annotations.NonNull; + +/** + * Represents a basic {@link Maybe} source base interface, + * consumable via an {@link MaybeObserver}. + * <p> + * This class also serves the base type for custom operators wrapped into + * Maybe via {@link Maybe#create(MaybeOnSubscribe)}. + * + * @param <T> the element type + * @since 2.0 + */ +@FunctionalInterface +public interface MaybeSource<@NonNull T> { + + /** + * Subscribes the given {@link MaybeObserver} to this {@link MaybeSource} instance. + * @param observer the {@code MaybeObserver}, not {@code null} + * @throws NullPointerException if {@code observer} is {@code null} + */ + void subscribe(@NonNull MaybeObserver<? super T> observer); +} diff --git a/src/main/java/io/reactivex/rxjava3/core/MaybeTransformer.java b/src/main/java/io/reactivex/rxjava3/core/MaybeTransformer.java new file mode 100644 index 0000000000..770497fe3c --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/core/MaybeTransformer.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.core; + +import io.reactivex.rxjava3.annotations.NonNull; + +/** + * Interface to compose {@link Maybe}s. + * + * @param <Upstream> the upstream value type + * @param <Downstream> the downstream value type + */ +@FunctionalInterface +public interface MaybeTransformer<@NonNull Upstream, @NonNull Downstream> { + /** + * Applies a function to the upstream {@link Maybe} and returns a {@link MaybeSource} with + * optionally different element type. + * @param upstream the upstream {@code Maybe} instance + * @return the transformed {@code MaybeSource} instance + */ + @NonNull + MaybeSource<Downstream> apply(@NonNull Maybe<Upstream> upstream); +} diff --git a/src/main/java/io/reactivex/rxjava3/core/Notification.java b/src/main/java/io/reactivex/rxjava3/core/Notification.java new file mode 100644 index 0000000000..7f5896209f --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/core/Notification.java @@ -0,0 +1,163 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.core; + +import io.reactivex.rxjava3.annotations.*; +import io.reactivex.rxjava3.internal.util.NotificationLite; +import java.util.Objects; + +/** + * Represents the reactive signal types: {@code onNext}, {@code onError} and {@code onComplete} and + * holds their parameter values (a value, a {@link Throwable}, nothing). + * @param <T> the value type + */ +public final class Notification<T> { + + final Object value; + + /** Not meant to be implemented externally. + * @param value the value to carry around in the notification, not {@code null} + */ + private Notification(@Nullable Object value) { + this.value = value; + } + + /** + * Returns true if this notification is an {@code onComplete} signal. + * @return true if this notification is an {@code onComplete} signal + */ + public boolean isOnComplete() { + return value == null; + } + + /** + * Returns true if this notification is an {@code onError} signal and + * {@link #getError()} returns the contained {@link Throwable}. + * @return true if this notification is an {@code onError} signal + * @see #getError() + */ + public boolean isOnError() { + return NotificationLite.isError(value); + } + + /** + * Returns true if this notification is an {@code onNext} signal and + * {@link #getValue()} returns the contained value. + * @return true if this notification is an {@code onNext} signal + * @see #getValue() + */ + public boolean isOnNext() { + Object o = value; + return o != null && !NotificationLite.isError(o); + } + + /** + * Returns the contained value if this notification is an {@code onNext} + * signal, null otherwise. + * @return the value contained or null + * @see #isOnNext() + */ + @SuppressWarnings("unchecked") + @Nullable + public T getValue() { + Object o = value; + if (o != null && !NotificationLite.isError(o)) { + return (T)value; + } + return null; + } + + /** + * Returns the container {@link Throwable} error if this notification is an {@code onError} + * signal, null otherwise. + * @return the {@code Throwable} error contained or {@code null} + * @see #isOnError() + */ + @Nullable + public Throwable getError() { + Object o = value; + if (NotificationLite.isError(o)) { + return NotificationLite.getError(o); + } + return null; + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof Notification) { + Notification<?> n = (Notification<?>) obj; + return Objects.equals(value, n.value); + } + return false; + } + + @Override + public int hashCode() { + Object o = value; + return o != null ? o.hashCode() : 0; + } + + @Override + public String toString() { + Object o = value; + if (o == null) { + return "OnCompleteNotification"; + } + if (NotificationLite.isError(o)) { + return "OnErrorNotification[" + NotificationLite.getError(o) + "]"; + } + return "OnNextNotification[" + value + "]"; + } + + /** + * Constructs an onNext notification containing the given value. + * @param <T> the value type + * @param value the value to carry around in the notification, not {@code null} + * @return the new Notification instance + * @throws NullPointerException if value is {@code null} + */ + @NonNull + public static <@NonNull T> Notification<T> createOnNext(T value) { + Objects.requireNonNull(value, "value is null"); + return new Notification<>(value); + } + + /** + * Constructs an onError notification containing the error. + * @param <T> the value type + * @param error the error Throwable to carry around in the notification, not null + * @return the new Notification instance + * @throws NullPointerException if error is {@code null} + */ + @NonNull + public static <T> Notification<T> createOnError(@NonNull Throwable error) { + Objects.requireNonNull(error, "error is null"); + return new Notification<>(NotificationLite.error(error)); + } + + /** + * Returns the empty and stateless shared instance of a notification representing + * an {@code onComplete} signal. + * @param <T> the target value type + * @return the shared Notification instance representing an {@code onComplete} signal + */ + @SuppressWarnings("unchecked") + @NonNull + public static <T> Notification<T> createOnComplete() { + return (Notification<T>)COMPLETE; + } + + /** The singleton instance for createOnComplete. */ + static final Notification<Object> COMPLETE = new Notification<>(null); +} diff --git a/src/main/java/io/reactivex/rxjava3/core/Observable.java b/src/main/java/io/reactivex/rxjava3/core/Observable.java new file mode 100644 index 0000000000..fcf809cdf6 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/core/Observable.java @@ -0,0 +1,17338 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.core; + +import java.util.*; +import java.util.concurrent.*; +import java.util.stream.*; + +import org.reactivestreams.Publisher; + +import io.reactivex.rxjava3.annotations.*; +import io.reactivex.rxjava3.disposables.*; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.functions.*; +import io.reactivex.rxjava3.internal.jdk8.*; +import io.reactivex.rxjava3.internal.observers.*; +import io.reactivex.rxjava3.internal.operators.flowable.*; +import io.reactivex.rxjava3.internal.operators.maybe.MaybeToObservable; +import io.reactivex.rxjava3.internal.operators.mixed.*; +import io.reactivex.rxjava3.internal.operators.observable.*; +import io.reactivex.rxjava3.internal.operators.single.SingleToObservable; +import io.reactivex.rxjava3.internal.util.*; +import io.reactivex.rxjava3.observables.*; +import io.reactivex.rxjava3.observers.*; +import io.reactivex.rxjava3.operators.ScalarSupplier; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.schedulers.*; + +/** + * The {@code Observable} class is the non-backpressured, optionally multi-valued base reactive class that + * offers factory methods, intermediate operators and the ability to consume synchronous + * and/or asynchronous reactive dataflows. + * <p> + * Many operators in the class accept {@link ObservableSource}(s), the base reactive interface + * for such non-backpressured flows, which {@code Observable} itself implements as well. + * <p> + * The {@code Observable}'s operators, by default, run with a buffer size of 128 elements (see {@link Flowable#bufferSize()}), + * that can be overridden globally via the system parameter {@code rx3.buffer-size}. Most operators, however, have + * overloads that allow setting their internal buffer size explicitly. + * <p> + * The documentation for this class makes use of marble diagrams. The following legend explains these diagrams: + * <p> + * <img width="640" height="317" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/legend.v3.png" alt=""> + * <p> + * The design of this class was derived from the + * <a href="https://github.com/reactive-streams/reactive-streams-jvm">Reactive-Streams design and specification</a> + * by removing any backpressure-related infrastructure and implementation detail, replacing the + * {@code org.reactivestreams.Subscription} with {@link Disposable} as the primary means to dispose of + * a flow. + * <p> + * The {@code Observable} follows the protocol + * <pre><code> + * onSubscribe onNext* (onError | onComplete)? + * </code></pre> + * where + * the stream can be disposed through the {@code Disposable} instance provided to consumers through + * {@code Observer.onSubscribe}. + * <p> + * Unlike the {@code Observable} of version 1.x, {@link #subscribe(Observer)} does not allow external disposal + * of a subscription and the {@link Observer} instance is expected to expose such capability. + * <p>Example: + * <pre><code> + * Disposable d = Observable.just("Hello world!") + * .delay(1, TimeUnit.SECONDS) + * .subscribeWith(new DisposableObserver<String>() { + * @Override public void onStart() { + * System.out.println("Start!"); + * } + * @Override public void onNext(String t) { + * System.out.println(t); + * } + * @Override public void onError(Throwable t) { + * t.printStackTrace(); + * } + * @Override public void onComplete() { + * System.out.println("Done!"); + * } + * }); + * + * Thread.sleep(500); + * // the sequence can now be disposed via dispose() + * d.dispose(); + * </code></pre> + * + * @param <T> + * the type of the items emitted by the {@code Observable} + * @see Flowable + * @see io.reactivex.rxjava3.observers.DisposableObserver + */ +public abstract class Observable<@NonNull T> implements ObservableSource<T> { + + /** + * Mirrors the one {@link ObservableSource} in an {@link Iterable} of several {@code ObservableSource}s that first either emits an item or sends + * a termination notification. + * <p> + * <img width="640" height="505" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Observable.amb.png" alt=""> + * <p> + * When one of the {@code ObservableSource}s signal an item or terminates first, all subscriptions to the other + * {@code ObservableSource}s are disposed. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code amb} does not operate by default on a particular {@link Scheduler}.</dd> + * <dt><b>Error handling:</b></dt> + * <dd> + * If any of the losing {@code ObservableSource}s signals an error, the error is routed to the global + * error handler via {@link RxJavaPlugins#onError(Throwable)}. + * </dd> + * </dl> + * + * @param <T> the common element type + * @param sources + * an {@code Iterable} of {@code ObservableSource} sources competing to react first. A subscription to each source will + * occur in the same order as in the {@code Iterable}. + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code sources} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/amb.html">ReactiveX operators documentation: Amb</a> + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T> Observable<T> amb(@NonNull Iterable<@NonNull ? extends ObservableSource<? extends T>> sources) { + Objects.requireNonNull(sources, "sources is null"); + return RxJavaPlugins.onAssembly(new ObservableAmb<>(null, sources)); + } + + /** + * Mirrors the one {@link ObservableSource} in an array of several {@code ObservableSource}s that first either emits an item or sends + * a termination notification. + * <p> + * <img width="640" height="505" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Observable.ambArray.png" alt=""> + * <p> + * When one of the {@code ObservableSource}s signal an item or terminates first, all subscriptions to the other + * {@code ObservableSource}s are disposed. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code ambArray} does not operate by default on a particular {@link Scheduler}.</dd> + * <dt><b>Error handling:</b></dt> + * <dd> + * If any of the losing {@code ObservableSource}s signals an error, the error is routed to the global + * error handler via {@link RxJavaPlugins#onError(Throwable)}. + * </dd> + * </dl> + * + * @param <T> the common element type + * @param sources + * an array of {@code ObservableSource} sources competing to react first. A subscription to each source will + * occur in the same order as in the array. + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code sources} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/amb.html">ReactiveX operators documentation: Amb</a> + */ + @SuppressWarnings("unchecked") + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + @SafeVarargs + public static <@NonNull T> Observable<T> ambArray(@NonNull ObservableSource<? extends T>... sources) { + Objects.requireNonNull(sources, "sources is null"); + int len = sources.length; + if (len == 0) { + return empty(); + } + if (len == 1) { + return (Observable<T>)wrap(sources[0]); + } + return RxJavaPlugins.onAssembly(new ObservableAmb<>(sources, null)); + } + + /** + * Returns the default 'island' size or capacity-increment hint for unbounded buffers. + * <p>Delegates to {@link Flowable#bufferSize} but is public for convenience. + * <p>The value can be overridden via system parameter {@code rx3.buffer-size} + * <em>before</em> the {@link Flowable} class is loaded. + * @return the default 'island' size or capacity-increment hint + */ + @CheckReturnValue + public static int bufferSize() { + return Flowable.bufferSize(); + } + + /** + * Combines a collection of source {@link ObservableSource}s by emitting an item that aggregates the latest values of each of + * the returned {@code ObservableSource}s each time an item is received from any of the returned {@code ObservableSource}s, where this + * aggregation is defined by a specified function. + * <p> + * Note on method signature: since Java doesn't allow creating a generic array with {@code new T[]}, the + * implementation of this operator has to create an {@code Object[]} instead. Unfortunately, a + * {@code Function<Integer[], R>} passed to the method would trigger a {@link ClassCastException}. + * <p> + * If any of the sources never produces an item but only terminates (normally or with an error), the + * resulting sequence terminates immediately (normally or with all the errors accumulated till that point). + * If that input source is also synchronous, other sources after it will not be subscribed to. + * <p> + * If the provided iterable of {@code ObservableSource}s is empty, the resulting sequence completes immediately without emitting + * any items and without any calls to the combiner function. + * + * <p> + * <img width="640" height="380" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/combineLatest.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code combineLatest} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T> + * the common base type of source values + * @param <R> + * the result type + * @param sources + * the collection of source {@code ObservableSource}s + * @param combiner + * the aggregation function used to combine the items emitted by the returned {@code ObservableSource}s + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code sources} or {@code combiner} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/combinelatest.html">ReactiveX operators documentation: CombineLatest</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public static <@NonNull T, @NonNull R> Observable<R> combineLatest( + @NonNull Iterable<@NonNull ? extends ObservableSource<? extends T>> sources, + @NonNull Function<? super Object[], ? extends R> combiner) { + return combineLatest(sources, combiner, bufferSize()); + } + + /** + * Combines an {@link Iterable} of source {@link ObservableSource}s by emitting an item that aggregates the latest values of each of + * the returned {@code ObservableSource}s each time an item is received from any of the returned {@code ObservableSource}s, where this + * aggregation is defined by a specified function. + * <p> + * Note on method signature: since Java doesn't allow creating a generic array with {@code new T[]}, the + * implementation of this operator has to create an {@code Object[]} instead. Unfortunately, a + * {@code Function<Integer[], R>} passed to the method would trigger a {@link ClassCastException}. + * <p> + * If any of the sources never produces an item but only terminates (normally or with an error), the + * resulting sequence terminates immediately (normally or with all the errors accumulated till that point). + * If that input source is also synchronous, other sources after it will not be subscribed to. + * <p> + * If the provided {@code Iterable} of {@code ObservableSource}s is empty, the resulting sequence completes immediately without emitting + * any items and without any calls to the combiner function. + * + * <p> + * <img width="640" height="380" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/combineLatest.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code combineLatest} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T> + * the common base type of source values + * @param <R> + * the result type + * @param sources + * the collection of source {@code ObservableSource}s + * @param combiner + * the aggregation function used to combine the items emitted by the returned {@code ObservableSource}s + * @param bufferSize + * the expected number of row combination items to be buffered internally + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code sources} or {@code combiner} is {@code null} + * @throws IllegalArgumentException if {@code bufferSize} is non-positive + * @see <a href="http://reactivex.io/documentation/operators/combinelatest.html">ReactiveX operators documentation: CombineLatest</a> + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T, @NonNull R> Observable<R> combineLatest( + @NonNull Iterable<@NonNull ? extends ObservableSource<? extends T>> sources, + @NonNull Function<? super Object[], ? extends R> combiner, int bufferSize) { + Objects.requireNonNull(sources, "sources is null"); + Objects.requireNonNull(combiner, "combiner is null"); + ObjectHelper.verifyPositive(bufferSize, "bufferSize"); + + // the queue holds a pair of values so we need to double the capacity + int s = bufferSize << 1; + return RxJavaPlugins.onAssembly(new ObservableCombineLatest<>(null, sources, combiner, s, false)); + } + + /** + * Combines an array of source {@link ObservableSource}s by emitting an item that aggregates the latest values of each of + * the {@code ObservableSource}s each time an item is received from any of the returned {@code ObservableSource}s, where this + * aggregation is defined by a specified function. + * <p> + * Note on method signature: since Java doesn't allow creating a generic array with {@code new T[]}, the + * implementation of this operator has to create an {@code Object[]} instead. Unfortunately, a + * {@code Function<Integer[], R>} passed to the method would trigger a {@link ClassCastException}. + * <p> + * If any of the sources never produces an item but only terminates (normally or with an error), the + * resulting sequence terminates immediately (normally or with all the errors accumulated till that point). + * If that input source is also synchronous, other sources after it will not be subscribed to. + * <p> + * If the provided array of {@code ObservableSource}s is empty, the resulting sequence completes immediately without emitting + * any items and without any calls to the combiner function. + * + * <p> + * <img width="640" height="380" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/combineLatest.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code combineLatestArray} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T> + * the common base type of source values + * @param <R> + * the result type + * @param sources + * the collection of source {@code ObservableSource}s + * @param combiner + * the aggregation function used to combine the items emitted by the {@code ObservableSource}s + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code sources} or {@code combiner} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/combinelatest.html">ReactiveX operators documentation: CombineLatest</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public static <@NonNull T, @NonNull R> Observable<R> combineLatestArray( + @NonNull ObservableSource<? extends T>[] sources, + @NonNull Function<? super Object[], ? extends R> combiner) { + return combineLatestArray(sources, combiner, bufferSize()); + } + + /** + * Combines an array of source {@link ObservableSource}s by emitting an item that aggregates the latest values of each of + * the {@code ObservableSource}s each time an item is received from any of the {@code ObservableSource}s, where this + * aggregation is defined by a specified function. + * <p> + * Note on method signature: since Java doesn't allow creating a generic array with {@code new T[]}, the + * implementation of this operator has to create an {@code Object[]} instead. Unfortunately, a + * {@code Function<Integer[], R>} passed to the method would trigger a {@link ClassCastException}. + * <p> + * If any of the sources never produces an item but only terminates (normally or with an error), the + * resulting sequence terminates immediately (normally or with all the errors accumulated till that point). + * If that input source is also synchronous, other sources after it will not be subscribed to. + * <p> + * If the provided array of {@code ObservableSource}s is empty, the resulting sequence completes immediately without emitting + * any items and without any calls to the combiner function. + * + * <p> + * <img width="640" height="380" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/combineLatest.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code combineLatestArray} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T> + * the common base type of source values + * @param <R> + * the result type + * @param sources + * the collection of source {@code ObservableSource}s + * @param combiner + * the aggregation function used to combine the items emitted by the {@code ObservableSource}s + * @param bufferSize + * the expected number of row combination items to be buffered internally + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code sources} or {@code combiner} is {@code null} + * @throws IllegalArgumentException if {@code bufferSize} is non-positive + * @see <a href="http://reactivex.io/documentation/operators/combinelatest.html">ReactiveX operators documentation: CombineLatest</a> + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T, @NonNull R> Observable<R> combineLatestArray( + @NonNull ObservableSource<? extends T>[] sources, + @NonNull Function<? super Object[], ? extends R> combiner, int bufferSize) { + Objects.requireNonNull(sources, "sources is null"); + if (sources.length == 0) { + return empty(); + } + Objects.requireNonNull(combiner, "combiner is null"); + ObjectHelper.verifyPositive(bufferSize, "bufferSize"); + + // the queue holds a pair of values so we need to double the capacity + int s = bufferSize << 1; + return RxJavaPlugins.onAssembly(new ObservableCombineLatest<>(sources, null, combiner, s, false)); + } + + /** + * Combines two source {@link ObservableSource}s by emitting an item that aggregates the latest values of each of the + * {@code ObservableSource}s each time an item is received from either of the {@code ObservableSource}s, where this + * aggregation is defined by a specified function. + * <p> + * If any of the sources never produces an item but only terminates (normally or with an error), the + * resulting sequence terminates immediately (normally or with all the errors accumulated till that point). + * If that input source is also synchronous, other sources after it will not be subscribed to. + * <p> + * <img width="640" height="380" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/combineLatest.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code combineLatest} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T1> the element type of the first source + * @param <T2> the element type of the second source + * @param <R> the combined output type + * @param source1 + * the first source {@code ObservableSource} + * @param source2 + * the second source {@code ObservableSource} + * @param combiner + * the aggregation function used to combine the items emitted by the {@code ObservableSource}s + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code source1}, {@code source2} or {@code combiner} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/combinelatest.html">ReactiveX operators documentation: CombineLatest</a> + */ + @SuppressWarnings("unchecked") + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T1, @NonNull T2, @NonNull R> Observable<R> combineLatest( + @NonNull ObservableSource<? extends T1> source1, @NonNull ObservableSource<? extends T2> source2, + @NonNull BiFunction<? super T1, ? super T2, ? extends R> combiner) { + Objects.requireNonNull(source1, "source1 is null"); + Objects.requireNonNull(source2, "source2 is null"); + Objects.requireNonNull(combiner, "combiner is null"); + return combineLatestArray(new ObservableSource[] { source1, source2 }, Functions.toFunction(combiner), bufferSize()); + } + + /** + * Combines three source {@link ObservableSource}s by emitting an item that aggregates the latest values of each of the + * {@code ObservableSource}s each time an item is received from any of the {@code ObservableSource}s, where this + * aggregation is defined by a specified function. + * <p> + * If any of the sources never produces an item but only terminates (normally or with an error), the + * resulting sequence terminates immediately (normally or with all the errors accumulated till that point). + * If that input source is also synchronous, other sources after it will not be subscribed to. + * <p> + * <img width="640" height="380" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/combineLatest.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code combineLatest} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T1> the element type of the first source + * @param <T2> the element type of the second source + * @param <T3> the element type of the third source + * @param <R> the combined output type + * @param source1 + * the first source {@code ObservableSource} + * @param source2 + * the second source {@code ObservableSource} + * @param source3 + * the third source {@code ObservableSource} + * @param combiner + * the aggregation function used to combine the items emitted by the {@code ObservableSource}s + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code source1}, {@code source2}, {@code source3} or {@code combiner} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/combinelatest.html">ReactiveX operators documentation: CombineLatest</a> + */ + @SuppressWarnings("unchecked") + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T1, @NonNull T2, @NonNull T3, @NonNull R> Observable<R> combineLatest( + @NonNull ObservableSource<? extends T1> source1, @NonNull ObservableSource<? extends T2> source2, + @NonNull ObservableSource<? extends T3> source3, + @NonNull Function3<? super T1, ? super T2, ? super T3, ? extends R> combiner) { + Objects.requireNonNull(source1, "source1 is null"); + Objects.requireNonNull(source2, "source2 is null"); + Objects.requireNonNull(source3, "source3 is null"); + Objects.requireNonNull(combiner, "combiner is null"); + return combineLatestArray(new ObservableSource[] { source1, source2, source3 }, Functions.toFunction(combiner), bufferSize()); + } + + /** + * Combines four source {@link ObservableSource}s by emitting an item that aggregates the latest values of each of the + * {@code ObservableSource}s each time an item is received from any of the {@code ObservableSource}s, where this + * aggregation is defined by a specified function. + * <p> + * If any of the sources never produces an item but only terminates (normally or with an error), the + * resulting sequence terminates immediately (normally or with all the errors accumulated till that point). + * If that input source is also synchronous, other sources after it will not be subscribed to. + * <p> + * <img width="640" height="380" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/combineLatest.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code combineLatest} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T1> the element type of the first source + * @param <T2> the element type of the second source + * @param <T3> the element type of the third source + * @param <T4> the element type of the fourth source + * @param <R> the combined output type + * @param source1 + * the first source {@code ObservableSource} + * @param source2 + * the second source {@code ObservableSource} + * @param source3 + * the third source {@code ObservableSource} + * @param source4 + * the fourth source {@code ObservableSource} + * @param combiner + * the aggregation function used to combine the items emitted by the {@code ObservableSource}s + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code source1}, {@code source2}, {@code source3}, + * {@code source4} or {@code combiner} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/combinelatest.html">ReactiveX operators documentation: CombineLatest</a> + */ + @SuppressWarnings("unchecked") + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T1, @NonNull T2, @NonNull T3, @NonNull T4, @NonNull R> Observable<R> combineLatest( + @NonNull ObservableSource<? extends T1> source1, @NonNull ObservableSource<? extends T2> source2, + @NonNull ObservableSource<? extends T3> source3, @NonNull ObservableSource<? extends T4> source4, + @NonNull Function4<? super T1, ? super T2, ? super T3, ? super T4, ? extends R> combiner) { + Objects.requireNonNull(source1, "source1 is null"); + Objects.requireNonNull(source2, "source2 is null"); + Objects.requireNonNull(source3, "source3 is null"); + Objects.requireNonNull(source4, "source4 is null"); + Objects.requireNonNull(combiner, "combiner is null"); + return combineLatestArray(new ObservableSource[] { source1, source2, source3, source4 }, Functions.toFunction(combiner), bufferSize()); + } + + /** + * Combines five source {@link ObservableSource}s by emitting an item that aggregates the latest values of each of the + * {@code ObservableSource}s each time an item is received from any of the {@code ObservableSource}s, where this + * aggregation is defined by a specified function. + * <p> + * If any of the sources never produces an item but only terminates (normally or with an error), the + * resulting sequence terminates immediately (normally or with all the errors accumulated till that point). + * If that input source is also synchronous, other sources after it will not be subscribed to. + * <p> + * <img width="640" height="380" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/combineLatest.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code combineLatest} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T1> the element type of the first source + * @param <T2> the element type of the second source + * @param <T3> the element type of the third source + * @param <T4> the element type of the fourth source + * @param <T5> the element type of the fifth source + * @param <R> the combined output type + * @param source1 + * the first source {@code ObservableSource} + * @param source2 + * the second source {@code ObservableSource} + * @param source3 + * the third source {@code ObservableSource} + * @param source4 + * the fourth source {@code ObservableSource} + * @param source5 + * the fifth source {@code ObservableSource} + * @param combiner + * the aggregation function used to combine the items emitted by the {@code ObservableSource}s + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code source1}, {@code source2}, {@code source3}, + * {@code source4}, {@code source5} or {@code combiner} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/combinelatest.html">ReactiveX operators documentation: CombineLatest</a> + */ + @SuppressWarnings("unchecked") + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T1, @NonNull T2, @NonNull T3, @NonNull T4, @NonNull T5, @NonNull R> Observable<R> combineLatest( + @NonNull ObservableSource<? extends T1> source1, @NonNull ObservableSource<? extends T2> source2, + @NonNull ObservableSource<? extends T3> source3, @NonNull ObservableSource<? extends T4> source4, + @NonNull ObservableSource<? extends T5> source5, + @NonNull Function5<? super T1, ? super T2, ? super T3, ? super T4, ? super T5, ? extends R> combiner) { + Objects.requireNonNull(source1, "source1 is null"); + Objects.requireNonNull(source2, "source2 is null"); + Objects.requireNonNull(source3, "source3 is null"); + Objects.requireNonNull(source4, "source4 is null"); + Objects.requireNonNull(source5, "source5 is null"); + Objects.requireNonNull(combiner, "combiner is null"); + return combineLatestArray(new ObservableSource[] { source1, source2, source3, source4, source5 }, Functions.toFunction(combiner), bufferSize()); + } + + /** + * Combines six source {@link ObservableSource}s by emitting an item that aggregates the latest values of each of the + * {@code ObservableSource}s each time an item is received from any of the {@code ObservableSource}s, where this + * aggregation is defined by a specified function. + * <p> + * If any of the sources never produces an item but only terminates (normally or with an error), the + * resulting sequence terminates immediately (normally or with all the errors accumulated till that point). + * If that input source is also synchronous, other sources after it will not be subscribed to. + * <p> + * <img width="640" height="380" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/combineLatest.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code combineLatest} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T1> the element type of the first source + * @param <T2> the element type of the second source + * @param <T3> the element type of the third source + * @param <T4> the element type of the fourth source + * @param <T5> the element type of the fifth source + * @param <T6> the element type of the sixth source + * @param <R> the combined output type + * @param source1 + * the first source {@code ObservableSource} + * @param source2 + * the second source {@code ObservableSource} + * @param source3 + * the third source {@code ObservableSource} + * @param source4 + * the fourth source {@code ObservableSource} + * @param source5 + * the fifth source {@code ObservableSource} + * @param source6 + * the sixth source {@code ObservableSource} + * @param combiner + * the aggregation function used to combine the items emitted by the {@code ObservableSource}s + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code source1}, {@code source2}, {@code source3}, + * {@code source4}, {@code source5}, {@code source6} or {@code combiner} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/combinelatest.html">ReactiveX operators documentation: CombineLatest</a> + */ + @SuppressWarnings("unchecked") + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T1, @NonNull T2, @NonNull T3, @NonNull T4, @NonNull T5, @NonNull T6, @NonNull R> Observable<R> combineLatest( + @NonNull ObservableSource<? extends T1> source1, @NonNull ObservableSource<? extends T2> source2, + @NonNull ObservableSource<? extends T3> source3, @NonNull ObservableSource<? extends T4> source4, + @NonNull ObservableSource<? extends T5> source5, @NonNull ObservableSource<? extends T6> source6, + @NonNull Function6<? super T1, ? super T2, ? super T3, ? super T4, ? super T5, ? super T6, ? extends R> combiner) { + Objects.requireNonNull(source1, "source1 is null"); + Objects.requireNonNull(source2, "source2 is null"); + Objects.requireNonNull(source3, "source3 is null"); + Objects.requireNonNull(source4, "source4 is null"); + Objects.requireNonNull(source5, "source5 is null"); + Objects.requireNonNull(source6, "source6 is null"); + Objects.requireNonNull(combiner, "combiner is null"); + return combineLatestArray(new ObservableSource[] { source1, source2, source3, source4, source5, source6 }, Functions.toFunction(combiner), bufferSize()); + } + + /** + * Combines seven source {@link ObservableSource}s by emitting an item that aggregates the latest values of each of the + * {@code ObservableSource}s each time an item is received from any of the {@code ObservableSource}s, where this + * aggregation is defined by a specified function. + * <p> + * If any of the sources never produces an item but only terminates (normally or with an error), the + * resulting sequence terminates immediately (normally or with all the errors accumulated till that point). + * If that input source is also synchronous, other sources after it will not be subscribed to. + * <p> + * <img width="640" height="380" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/combineLatest.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code combineLatest} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T1> the element type of the first source + * @param <T2> the element type of the second source + * @param <T3> the element type of the third source + * @param <T4> the element type of the fourth source + * @param <T5> the element type of the fifth source + * @param <T6> the element type of the sixth source + * @param <T7> the element type of the seventh source + * @param <R> the combined output type + * @param source1 + * the first source {@code ObservableSource} + * @param source2 + * the second source {@code ObservableSource} + * @param source3 + * the third source {@code ObservableSource} + * @param source4 + * the fourth source {@code ObservableSource} + * @param source5 + * the fifth source {@code ObservableSource} + * @param source6 + * the sixth source {@code ObservableSource} + * @param source7 + * the seventh source {@code ObservableSource} + * @param combiner + * the aggregation function used to combine the items emitted by the {@code ObservableSource}s + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code source1}, {@code source2}, {@code source3}, + * {@code source4}, {@code source5}, {@code source6}, + * {@code source7} or {@code combiner} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/combinelatest.html">ReactiveX operators documentation: CombineLatest</a> + */ + @SuppressWarnings("unchecked") + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T1, @NonNull T2, @NonNull T3, @NonNull T4, @NonNull T5, @NonNull T6, @NonNull T7, @NonNull R> Observable<R> combineLatest( + @NonNull ObservableSource<? extends T1> source1, @NonNull ObservableSource<? extends T2> source2, + @NonNull ObservableSource<? extends T3> source3, @NonNull ObservableSource<? extends T4> source4, + @NonNull ObservableSource<? extends T5> source5, @NonNull ObservableSource<? extends T6> source6, + @NonNull ObservableSource<? extends T7> source7, + @NonNull Function7<? super T1, ? super T2, ? super T3, ? super T4, ? super T5, ? super T6, ? super T7, ? extends R> combiner) { + Objects.requireNonNull(source1, "source1 is null"); + Objects.requireNonNull(source2, "source2 is null"); + Objects.requireNonNull(source3, "source3 is null"); + Objects.requireNonNull(source4, "source4 is null"); + Objects.requireNonNull(source5, "source5 is null"); + Objects.requireNonNull(source6, "source6 is null"); + Objects.requireNonNull(source7, "source7 is null"); + Objects.requireNonNull(combiner, "combiner is null"); + return combineLatestArray(new ObservableSource[] { source1, source2, source3, source4, source5, source6, source7 }, Functions.toFunction(combiner), bufferSize()); + } + + /** + * Combines eight source {@link ObservableSource}s by emitting an item that aggregates the latest values of each of the + * {@code ObservableSource}s each time an item is received from any of the {@code ObservableSource}s, where this + * aggregation is defined by a specified function. + * <p> + * If any of the sources never produces an item but only terminates (normally or with an error), the + * resulting sequence terminates immediately (normally or with all the errors accumulated till that point). + * If that input source is also synchronous, other sources after it will not be subscribed to. + * <p> + * <img width="640" height="380" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/combineLatest.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code combineLatest} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T1> the element type of the first source + * @param <T2> the element type of the second source + * @param <T3> the element type of the third source + * @param <T4> the element type of the fourth source + * @param <T5> the element type of the fifth source + * @param <T6> the element type of the sixth source + * @param <T7> the element type of the seventh source + * @param <T8> the element type of the eighth source + * @param <R> the combined output type + * @param source1 + * the first source {@code ObservableSource} + * @param source2 + * the second source {@code ObservableSource} + * @param source3 + * the third source {@code ObservableSource} + * @param source4 + * the fourth source {@code ObservableSource} + * @param source5 + * the fifth source {@code ObservableSource} + * @param source6 + * the sixth source {@code ObservableSource} + * @param source7 + * the seventh source {@code ObservableSource} + * @param source8 + * the eighth source {@code ObservableSource} + * @param combiner + * the aggregation function used to combine the items emitted by the {@code ObservableSource}s + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code source1}, {@code source2}, {@code source3}, + * {@code source4}, {@code source5}, {@code source6}, + * {@code source7}, {@code source8} or {@code combiner} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/combinelatest.html">ReactiveX operators documentation: CombineLatest</a> + */ + @SuppressWarnings("unchecked") + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T1, @NonNull T2, @NonNull T3, @NonNull T4, @NonNull T5, @NonNull T6, @NonNull T7, @NonNull T8, @NonNull R> Observable<R> combineLatest( + @NonNull ObservableSource<? extends T1> source1, @NonNull ObservableSource<? extends T2> source2, + @NonNull ObservableSource<? extends T3> source3, @NonNull ObservableSource<? extends T4> source4, + @NonNull ObservableSource<? extends T5> source5, @NonNull ObservableSource<? extends T6> source6, + @NonNull ObservableSource<? extends T7> source7, @NonNull ObservableSource<? extends T8> source8, + @NonNull Function8<? super T1, ? super T2, ? super T3, ? super T4, ? super T5, ? super T6, ? super T7, ? super T8, ? extends R> combiner) { + Objects.requireNonNull(source1, "source1 is null"); + Objects.requireNonNull(source2, "source2 is null"); + Objects.requireNonNull(source3, "source3 is null"); + Objects.requireNonNull(source4, "source4 is null"); + Objects.requireNonNull(source5, "source5 is null"); + Objects.requireNonNull(source6, "source6 is null"); + Objects.requireNonNull(source7, "source7 is null"); + Objects.requireNonNull(source8, "source8 is null"); + Objects.requireNonNull(combiner, "combiner is null"); + return combineLatestArray(new ObservableSource[] { source1, source2, source3, source4, source5, source6, source7, source8 }, Functions.toFunction(combiner), bufferSize()); + } + + /** + * Combines nine source {@link ObservableSource}s by emitting an item that aggregates the latest values of each of the + * {@code ObservableSource}s each time an item is received from any of the {@code ObservableSource}s, where this + * aggregation is defined by a specified function. + * <p> + * If any of the sources never produces an item but only terminates (normally or with an error), the + * resulting sequence terminates immediately (normally or with all the errors accumulated till that point). + * If that input source is also synchronous, other sources after it will not be subscribed to. + * <p> + * <img width="640" height="380" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/combineLatest.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code combineLatest} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T1> the element type of the first source + * @param <T2> the element type of the second source + * @param <T3> the element type of the third source + * @param <T4> the element type of the fourth source + * @param <T5> the element type of the fifth source + * @param <T6> the element type of the sixth source + * @param <T7> the element type of the seventh source + * @param <T8> the element type of the eighth source + * @param <T9> the element type of the ninth source + * @param <R> the combined output type + * @param source1 + * the first source {@code ObservableSource} + * @param source2 + * the second source {@code ObservableSource} + * @param source3 + * the third source {@code ObservableSource} + * @param source4 + * the fourth source {@code ObservableSource} + * @param source5 + * the fifth source {@code ObservableSource} + * @param source6 + * the sixth source {@code ObservableSource} + * @param source7 + * the seventh source {@code ObservableSource} + * @param source8 + * the eighth source {@code ObservableSource} + * @param source9 + * the ninth source {@code ObservableSource} + * @param combiner + * the aggregation function used to combine the items emitted by the {@code ObservableSource}s + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code source1}, {@code source2}, {@code source3}, + * {@code source4}, {@code source5}, {@code source6}, + * {@code source7}, {@code source8}, {@code source9} or {@code combiner} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/combinelatest.html">ReactiveX operators documentation: CombineLatest</a> + */ + @SuppressWarnings("unchecked") + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T1, @NonNull T2, @NonNull T3, @NonNull T4, @NonNull T5, @NonNull T6, @NonNull T7, @NonNull T8, @NonNull T9, @NonNull R> Observable<R> combineLatest( + @NonNull ObservableSource<? extends T1> source1, @NonNull ObservableSource<? extends T2> source2, + @NonNull ObservableSource<? extends T3> source3, @NonNull ObservableSource<? extends T4> source4, + @NonNull ObservableSource<? extends T5> source5, @NonNull ObservableSource<? extends T6> source6, + @NonNull ObservableSource<? extends T7> source7, @NonNull ObservableSource<? extends T8> source8, + @NonNull ObservableSource<? extends T9> source9, + @NonNull Function9<? super T1, ? super T2, ? super T3, ? super T4, ? super T5, ? super T6, ? super T7, ? super T8, ? super T9, ? extends R> combiner) { + Objects.requireNonNull(source1, "source1 is null"); + Objects.requireNonNull(source2, "source2 is null"); + Objects.requireNonNull(source3, "source3 is null"); + Objects.requireNonNull(source4, "source4 is null"); + Objects.requireNonNull(source5, "source5 is null"); + Objects.requireNonNull(source6, "source6 is null"); + Objects.requireNonNull(source7, "source7 is null"); + Objects.requireNonNull(source8, "source8 is null"); + Objects.requireNonNull(source9, "source9 is null"); + Objects.requireNonNull(combiner, "combiner is null"); + return combineLatestArray(new ObservableSource[] { source1, source2, source3, source4, source5, source6, source7, source8, source9 }, Functions.toFunction(combiner), bufferSize()); + } + + /** + * Combines an array of {@link ObservableSource}s by emitting an item that aggregates the latest values of each of + * the {@code ObservableSource}s each time an item is received from any of the {@code ObservableSource}s, where this + * aggregation is defined by a specified function. + * <p> + * <img width="640" height="380" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/combineLatestDelayError.v3.png" alt=""> + * <p> + * Note on method signature: since Java doesn't allow creating a generic array with {@code new T[]}, the + * implementation of this operator has to create an {@code Object[]} instead. Unfortunately, a + * {@code Function<Integer[], R>} passed to the method would trigger a {@link ClassCastException}. + * <p> + * If any of the sources never produces an item but only terminates (normally or with an error), the + * resulting sequence terminates immediately (normally or with all the errors accumulated till that point). + * If that input source is also synchronous, other sources after it will not be subscribed to. + * <p> + * If the provided array of {@code ObservableSource}s is empty, the resulting sequence completes immediately without emitting + * any items and without any calls to the combiner function. + * + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code combineLatestArrayDelayError} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T> + * the common base type of source values + * @param <R> + * the result type + * @param sources + * the collection of source {@code ObservableSource}s + * @param combiner + * the aggregation function used to combine the items emitted by the {@code ObservableSource}s + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code sources} or {@code combiner} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/combinelatest.html">ReactiveX operators documentation: CombineLatest</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public static <@NonNull T, @NonNull R> Observable<R> combineLatestArrayDelayError( + @NonNull ObservableSource<? extends T>[] sources, + @NonNull Function<? super Object[], ? extends R> combiner) { + return combineLatestArrayDelayError(sources, combiner, bufferSize()); + } + + /** + * Combines an array of {@link ObservableSource}s by emitting an item that aggregates the latest values of each of + * the {@code ObservableSource}s each time an item is received from any of the {@code ObservableSource}s, where this + * aggregation is defined by a specified function and delays any error from the sources until + * all source {@code ObservableSource}s terminate. + * <p> + * Note on method signature: since Java doesn't allow creating a generic array with {@code new T[]}, the + * implementation of this operator has to create an {@code Object[]} instead. Unfortunately, a + * {@code Function<Integer[], R>} passed to the method would trigger a {@link ClassCastException}. + * <p> + * If any of the sources never produces an item but only terminates (normally or with an error), the + * resulting sequence terminates immediately (normally or with all the errors accumulated till that point). + * If that input source is also synchronous, other sources after it will not be subscribed to. + * <p> + * If the provided array of {@code ObservableSource}s is empty, the resulting sequence completes immediately without emitting + * any items and without any calls to the combiner function. + * + * <p> + * <img width="640" height="380" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/combineLatestDelayError.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code combineLatestArrayDelayError} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T> + * the common base type of source values + * @param <R> + * the result type + * @param sources + * the collection of source {@code ObservableSource}s + * @param combiner + * the aggregation function used to combine the items emitted by the {@code ObservableSource}s + * @param bufferSize + * the expected number of row combination items to be buffered internally + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code sources} or {@code combiner} is {@code null} + * @throws IllegalArgumentException if {@code bufferSize} is non-positive + * @see <a href="http://reactivex.io/documentation/operators/combinelatest.html">ReactiveX operators documentation: CombineLatest</a> + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T, @NonNull R> Observable<R> combineLatestArrayDelayError(@NonNull ObservableSource<? extends T>[] sources, + @NonNull Function<? super Object[], ? extends R> combiner, int bufferSize) { + Objects.requireNonNull(sources, "sources is null"); + Objects.requireNonNull(combiner, "combiner is null"); + ObjectHelper.verifyPositive(bufferSize, "bufferSize"); + if (sources.length == 0) { + return empty(); + } + // the queue holds a pair of values so we need to double the capacity + int s = bufferSize << 1; + return RxJavaPlugins.onAssembly(new ObservableCombineLatest<>(sources, null, combiner, s, true)); + } + + /** + * Combines an {@link Iterable} of {@link ObservableSource}s by emitting an item that aggregates the latest values of each of + * the {@code ObservableSource}s each time an item is received from any of the {@code ObservableSource}s, where this + * aggregation is defined by a specified function and delays any error from the sources until + * all source {@code ObservableSource}s terminate. + * <p> + * Note on method signature: since Java doesn't allow creating a generic array with {@code new T[]}, the + * implementation of this operator has to create an {@code Object[]} instead. Unfortunately, a + * {@code Function<Integer[], R>} passed to the method would trigger a {@link ClassCastException}. + * <p> + * If any of the sources never produces an item but only terminates (normally or with an error), the + * resulting sequence terminates immediately (normally or with all the errors accumulated till that point). + * If that input source is also synchronous, other sources after it will not be subscribed to. + * <p> + * If the provided iterable of {@code ObservableSource}s is empty, the resulting sequence completes immediately without emitting + * any items and without any calls to the combiner function. + * + * <p> + * <img width="640" height="380" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/combineLatestDelayError.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code combineLatestDelayError} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T> + * the common base type of source values + * @param <R> + * the result type + * @param sources + * the {@code Iterable} of source {@code ObservableSource}s + * @param combiner + * the aggregation function used to combine the items emitted by the {@code ObservableSource}s + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code sources} or {@code combiner} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/combinelatest.html">ReactiveX operators documentation: CombineLatest</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public static <@NonNull T, @NonNull R> Observable<R> combineLatestDelayError(@NonNull Iterable<@NonNull ? extends ObservableSource<? extends T>> sources, + @NonNull Function<? super Object[], ? extends R> combiner) { + return combineLatestDelayError(sources, combiner, bufferSize()); + } + + /** + * Combines an {@link Iterable} of {@link ObservableSource}s by emitting an item that aggregates the latest values of each of + * the {@code ObservableSource}s each time an item is received from any of the {@code ObservableSource}s, where this + * aggregation is defined by a specified function and delays any error from the sources until + * all source {@code ObservableSource}s terminate. + * <p> + * Note on method signature: since Java doesn't allow creating a generic array with {@code new T[]}, the + * implementation of this operator has to create an {@code Object[]} instead. Unfortunately, a + * {@code Function<Integer[], R>} passed to the method would trigger a {@link ClassCastException}. + * <p> + * If any of the sources never produces an item but only terminates (normally or with an error), the + * resulting sequence terminates immediately (normally or with all the errors accumulated till that point). + * If that input source is also synchronous, other sources after it will not be subscribed to. + * <p> + * If the provided iterable of {@code ObservableSource}s is empty, the resulting sequence completes immediately without emitting + * any items and without any calls to the combiner function. + * + * <p> + * <img width="640" height="380" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/combineLatestDelayError.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code combineLatestDelayError} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T> + * the common base type of source values + * @param <R> + * the result type + * @param sources + * the collection of source {@code ObservableSource}s + * @param combiner + * the aggregation function used to combine the items emitted by the {@code ObservableSource}s + * @param bufferSize + * the expected number of row combination items to be buffered internally + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code sources} or {@code combiner} is {@code null} + * @throws IllegalArgumentException if {@code bufferSize} is non-positive + * @see <a href="http://reactivex.io/documentation/operators/combinelatest.html">ReactiveX operators documentation: CombineLatest</a> + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T, @NonNull R> Observable<R> combineLatestDelayError(@NonNull Iterable<@NonNull ? extends ObservableSource<? extends T>> sources, + @NonNull Function<? super Object[], ? extends R> combiner, int bufferSize) { + Objects.requireNonNull(sources, "sources is null"); + Objects.requireNonNull(combiner, "combiner is null"); + ObjectHelper.verifyPositive(bufferSize, "bufferSize"); + + // the queue holds a pair of values so we need to double the capacity + int s = bufferSize << 1; + return RxJavaPlugins.onAssembly(new ObservableCombineLatest<>(null, sources, combiner, s, true)); + } + + /** + * Concatenates elements of each {@link ObservableSource} provided via an {@link Iterable} sequence into a single sequence + * of elements without interleaving them. + * <p> + * <img width="640" height="380" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/concat.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code concat} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <T> the common value type of the sources + * @param sources the {@code Iterable} sequence of {@code ObservableSource}s + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code sources} is {@code null} + */ + @SuppressWarnings({ "unchecked", "rawtypes" }) + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T> Observable<T> concat(@NonNull Iterable<@NonNull ? extends ObservableSource<? extends T>> sources) { + Objects.requireNonNull(sources, "sources is null"); + return fromIterable(sources).concatMapDelayError((Function)Functions.identity(), false, bufferSize()); + } + + /** + * Returns an {@code Observable} that emits the items emitted by each of the {@link ObservableSource}s emitted by the + * {@code ObservableSource}, one after the other, without interleaving them. + * <p> + * <img width="640" height="380" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/concat.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code concat} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T> the common element base type + * @param sources + * an {@code ObservableSource} that emits {@code ObservableSource}s + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code sources} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/concat.html">ReactiveX operators documentation: Concat</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public static <@NonNull T> Observable<T> concat(@NonNull ObservableSource<? extends ObservableSource<? extends T>> sources) { + return concat(sources, bufferSize()); + } + + /** + * Returns an {@code Observable} that emits the items emitted by each of the {@link ObservableSource}s emitted by the outer + * {@code ObservableSource}, one after the other, without interleaving them. + * <p> + * <img width="640" height="380" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/concat.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code concat} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T> the common element base type + * @param sources + * an {@code ObservableSource} that emits {@code ObservableSource}s + * @param bufferSize + * the number of inner {@code ObservableSource}s expected to be buffered. + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code sources} is {@code null} + * @throws IllegalArgumentException if {@code bufferSize} is non-positive + * @see <a href="http://reactivex.io/documentation/operators/concat.html">ReactiveX operators documentation: Concat</a> + */ + @SuppressWarnings({ "unchecked", "rawtypes" }) + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T> Observable<T> concat(@NonNull ObservableSource<? extends ObservableSource<? extends T>> sources, int bufferSize) { + Objects.requireNonNull(sources, "sources is null"); + ObjectHelper.verifyPositive(bufferSize, "bufferSize"); + return RxJavaPlugins.onAssembly(new ObservableConcatMap(sources, Functions.identity(), bufferSize, ErrorMode.IMMEDIATE)); + } + + /** + * Returns an {@code Observable} that emits the items emitted by two {@link ObservableSource}s, one after the other, without + * interleaving them. + * <p> + * <img width="640" height="380" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/concat.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code concat} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T> the common element base type + * @param source1 + * an {@code ObservableSource} to be concatenated + * @param source2 + * an {@code ObservableSource} to be concatenated + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code source1} or {@code source2} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/concat.html">ReactiveX operators documentation: Concat</a> + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T> Observable<T> concat(@NonNull ObservableSource<? extends T> source1, ObservableSource<? extends T> source2) { + Objects.requireNonNull(source1, "source1 is null"); + Objects.requireNonNull(source2, "source2 is null"); + return concatArray(source1, source2); + } + + /** + * Returns an {@code Observable} that emits the items emitted by three {@link ObservableSource}s, one after the other, without + * interleaving them. + * <p> + * <img width="640" height="380" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/concat.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code concat} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T> the common element base type + * @param source1 + * an {@code ObservableSource} to be concatenated + * @param source2 + * an {@code ObservableSource} to be concatenated + * @param source3 + * an {@code ObservableSource} to be concatenated + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code source1}, {@code source2} or {@code source3} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/concat.html">ReactiveX operators documentation: Concat</a> + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T> Observable<T> concat( + @NonNull ObservableSource<? extends T> source1, @NonNull ObservableSource<? extends T> source2, + @NonNull ObservableSource<? extends T> source3) { + Objects.requireNonNull(source1, "source1 is null"); + Objects.requireNonNull(source2, "source2 is null"); + Objects.requireNonNull(source3, "source3 is null"); + return concatArray(source1, source2, source3); + } + + /** + * Returns an {@code Observable} that emits the items emitted by four {@link ObservableSource}s, one after the other, without + * interleaving them. + * <p> + * <img width="640" height="380" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/concat.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code concat} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T> the common element base type + * @param source1 + * an {@code ObservableSource} to be concatenated + * @param source2 + * an {@code ObservableSource} to be concatenated + * @param source3 + * an {@code ObservableSource} to be concatenated + * @param source4 + * an {@code ObservableSource} to be concatenated + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code source1}, {@code source2}, {@code source3} or {@code source4} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/concat.html">ReactiveX operators documentation: Concat</a> + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T> Observable<T> concat( + @NonNull ObservableSource<? extends T> source1, @NonNull ObservableSource<? extends T> source2, + @NonNull ObservableSource<? extends T> source3, @NonNull ObservableSource<? extends T> source4) { + Objects.requireNonNull(source1, "source1 is null"); + Objects.requireNonNull(source2, "source2 is null"); + Objects.requireNonNull(source3, "source3 is null"); + Objects.requireNonNull(source4, "source4 is null"); + return concatArray(source1, source2, source3, source4); + } + + /** + * Concatenates a variable number of {@link ObservableSource} sources. + * <p> + * Note: named this way because of overload conflict with {@code concat(ObservableSource<ObservableSource>)} + * <p> + * <img width="640" height="290" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/concatArray.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code concatArray} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param sources the array of sources + * @param <T> the common base value type + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code sources} is {@code null} + */ + @SuppressWarnings({ "unchecked", "rawtypes" }) + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + @SafeVarargs + public static <@NonNull T> Observable<T> concatArray(@NonNull ObservableSource<? extends T>... sources) { + Objects.requireNonNull(sources, "sources is null"); + if (sources.length == 0) { + return empty(); + } + if (sources.length == 1) { + return wrap((ObservableSource<T>)sources[0]); + } + return RxJavaPlugins.onAssembly(new ObservableConcatMap(fromArray(sources), Functions.identity(), bufferSize(), ErrorMode.BOUNDARY)); + } + + /** + * Concatenates a variable number of {@link ObservableSource} sources and delays errors from any of them + * till all terminate. + * <p> + * <img width="640" height="290" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/concatArray.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code concatArrayDelayError} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param sources the array of sources + * @param <T> the common base value type + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code sources} is {@code null} + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + @SafeVarargs + public static <@NonNull T> Observable<T> concatArrayDelayError(@NonNull ObservableSource<? extends T>... sources) { + Objects.requireNonNull(sources, "sources is null"); + if (sources.length == 0) { + return empty(); + } + if (sources.length == 1) { + @SuppressWarnings("unchecked") + Observable<T> source = (Observable<T>)wrap(sources[0]); + return source; + } + return concatDelayError(fromArray(sources)); + } + + /** + * Concatenates an array of {@link ObservableSource}s eagerly into a single stream of values. + * <p> + * <img width="640" height="411" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/concatArrayEager.png" alt=""> + * <p> + * Eager concatenation means that once a subscriber subscribes, this operator subscribes to all of the + * {@code ObservableSource}s. The operator buffers the values emitted by these {@code ObservableSource}s and then drains them + * in order, each one after the previous one completes. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>This method does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <T> the value type + * @param sources an array of {@code ObservableSource}s that need to be eagerly concatenated + * @return the new {@code Observable} instance with the specified concatenation behavior + * @throws NullPointerException if {@code sources} is {@code null} + * @since 2.0 + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @SafeVarargs + @NonNull + public static <@NonNull T> Observable<T> concatArrayEager(@NonNull ObservableSource<? extends T>... sources) { + return concatArrayEager(bufferSize(), bufferSize(), sources); + } + + /** + * Concatenates an array of {@link ObservableSource}s eagerly into a single stream of values. + * <p> + * <img width="640" height="495" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/concatArrayEager.nn.png" alt=""> + * <p> + * Eager concatenation means that once a subscriber subscribes, this operator subscribes to all of the + * {@code ObservableSource}s. The operator buffers the values emitted by these {@code ObservableSource}s and then drains them + * in order, each one after the previous one completes. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>This method does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <T> the value type + * @param sources an array of {@code ObservableSource}s that need to be eagerly concatenated + * @param maxConcurrency the maximum number of concurrent subscriptions at a time, {@link Integer#MAX_VALUE} + * is interpreted as indication to subscribe to all sources at once + * @param bufferSize the number of elements expected from each {@code ObservableSource} to be buffered + * @return the new {@code Observable} instance with the specified concatenation behavior + * @throws NullPointerException if {@code sources} is {@code null} + * @throws IllegalArgumentException if {@code maxConcurrency} or {@code bufferSize} is non-positive + * @since 2.0 + */ + @SuppressWarnings({ "rawtypes", "unchecked" }) + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + @SafeVarargs + public static <@NonNull T> Observable<T> concatArrayEager(int maxConcurrency, int bufferSize, @NonNull ObservableSource<? extends T>... sources) { + return fromArray(sources).concatMapEagerDelayError((Function)Functions.identity(), false, maxConcurrency, bufferSize); + } + + /** + * Concatenates an array of {@link ObservableSource}s eagerly into a single stream of values + * and delaying any errors until all sources terminate. + * <p> + * <img width="640" height="354" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/concatArrayEagerDelayError.png" alt=""> + * <p> + * Eager concatenation means that once a subscriber subscribes, this operator subscribes to all of the + * {@code ObservableSource}s. The operator buffers the values emitted by these {@code ObservableSource}s + * and then drains them in order, each one after the previous one completes. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>This method does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <T> the value type + * @param sources an array of {@code ObservableSource}s that need to be eagerly concatenated + * @return the new {@code Observable} instance with the specified concatenation behavior + * @throws NullPointerException if {@code sources} is {@code null} + * @since 2.2.1 - experimental + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @SafeVarargs + @NonNull + public static <@NonNull T> Observable<T> concatArrayEagerDelayError(@NonNull ObservableSource<? extends T>... sources) { + return concatArrayEagerDelayError(bufferSize(), bufferSize(), sources); + } + + /** + * Concatenates an array of {@link ObservableSource}s eagerly into a single stream of values + * and delaying any errors until all sources terminate. + * <p> + * <img width="640" height="460" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/concatArrayEagerDelayError.nn.png" alt=""> + * <p> + * Eager concatenation means that once a subscriber subscribes, this operator subscribes to all of the + * {@code ObservableSource}s. The operator buffers the values emitted by these {@code ObservableSource}s + * and then drains them in order, each one after the previous one completes. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>This method does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <T> the value type + * @param sources an array of {@code ObservableSource}s that need to be eagerly concatenated + * @param maxConcurrency the maximum number of concurrent subscriptions at a time, {@link Integer#MAX_VALUE} + * is interpreted as indication to subscribe to all sources at once + * @param bufferSize the number of elements expected from each {@code ObservableSource} to be buffered + * @return the new {@code Observable} instance with the specified concatenation behavior + * @throws NullPointerException if {@code sources} is {@code null} + * @throws IllegalArgumentException if {@code maxConcurrency} or {@code bufferSize} is non-positive + * @since 2.2.1 - experimental + */ + @SuppressWarnings({ "rawtypes", "unchecked" }) + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + @SafeVarargs + public static <@NonNull T> Observable<T> concatArrayEagerDelayError(int maxConcurrency, int bufferSize, @NonNull ObservableSource<? extends T>... sources) { + return fromArray(sources).concatMapEagerDelayError((Function)Functions.identity(), true, maxConcurrency, bufferSize); + } + + /** + * Concatenates the {@link Iterable} sequence of {@link ObservableSource}s into a single {@code Observable} sequence + * by subscribing to each {@code ObservableSource}, one after the other, one at a time and delays any errors till + * the all inner {@code ObservableSource}s terminate. + * <p> + * <img width="640" height="380" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/concatDelayError.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code concatDelayError} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T> the common element base type + * @param sources the {@code Iterable} sequence of {@code ObservableSource}s + * @return the new {@code Observable} with the concatenating behavior + * @throws NullPointerException if {@code sources} is {@code null} + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T> Observable<T> concatDelayError(@NonNull Iterable<@NonNull ? extends ObservableSource<? extends T>> sources) { + Objects.requireNonNull(sources, "sources is null"); + return concatDelayError(fromIterable(sources)); + } + + /** + * Concatenates the {@link ObservableSource} sequence of {@code ObservableSource}s into a single {@code Observable} sequence + * by subscribing to each inner {@code ObservableSource}, one after the other, one at a time and delays any errors till the + * all inner and the outer {@code ObservableSource}s terminate. + * <p> + * <img width="640" height="380" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/concatDelayError.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code concatDelayError} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T> the common element base type + * @param sources the {@code ObservableSource} sequence of {@code ObservableSource}s + * @return the new {@code Observable} with the concatenating behavior + * @throws NullPointerException if {@code sources} is {@code null} + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public static <@NonNull T> Observable<T> concatDelayError(@NonNull ObservableSource<? extends ObservableSource<? extends T>> sources) { + return concatDelayError(sources, bufferSize(), true); + } + + /** + * Concatenates the {@link ObservableSource} sequence of {@code ObservableSource}s into a single sequence by subscribing to each inner {@code ObservableSource}, + * one after the other, one at a time and delays any errors till the all inner and the outer {@code ObservableSource}s terminate. + * <p> + * <img width="640" height="380" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/concatDelayError.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code concatDelayError} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T> the common element base type + * @param sources the {@code ObservableSource} sequence of {@code ObservableSource}s + * @param bufferSize the number of inner {@code ObservableSource}s expected to be buffered + * @param tillTheEnd if {@code true}, exceptions from the outer and all inner {@code ObservableSource}s are delayed to the end + * if {@code false}, exception from the outer {@code ObservableSource} is delayed till the active {@code ObservableSource} terminates + * @return the new {@code Observable} with the concatenating behavior + * @throws NullPointerException if {@code sources} is {@code null} + * @throws IllegalArgumentException if {@code bufferSize} is non-positive + */ + @SuppressWarnings({ "rawtypes", "unchecked" }) + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T> Observable<T> concatDelayError(@NonNull ObservableSource<? extends ObservableSource<? extends T>> sources, int bufferSize, boolean tillTheEnd) { + Objects.requireNonNull(sources, "sources is null"); + ObjectHelper.verifyPositive(bufferSize, "bufferSize is null"); + return RxJavaPlugins.onAssembly(new ObservableConcatMap(sources, Functions.identity(), bufferSize, tillTheEnd ? ErrorMode.END : ErrorMode.BOUNDARY)); + } + + /** + * Concatenates a sequence of {@link ObservableSource}s eagerly into a single stream of values. + * <p> + * <img width="640" height="422" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Observable.concatEager.i.png" alt=""> + * <p> + * Eager concatenation means that once a subscriber subscribes, this operator subscribes to all of the + * {@code ObservableSource}s. The operator buffers the values emitted by these {@code ObservableSource}s and then drains them + * in order, each one after the previous one completes. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>This method does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <T> the value type + * @param sources a sequence of {@code ObservableSource}s that need to be eagerly concatenated + * @return the new {@code Observable} instance with the specified concatenation behavior + * @throws NullPointerException if {@code sources} is {@code null} + * @since 2.0 + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public static <@NonNull T> Observable<T> concatEager(@NonNull Iterable<@NonNull ? extends ObservableSource<? extends T>> sources) { + return concatEager(sources, bufferSize(), bufferSize()); + } + + /** + * Concatenates a sequence of {@link ObservableSource}s eagerly into a single stream of values and + * runs a limited number of inner sequences at once. + * <p> + * <img width="640" height="379" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Observable.concatEager.in.png" alt=""> + * <p> + * Eager concatenation means that once a subscriber subscribes, this operator subscribes to all of the + * {@code ObservableSource}s. The operator buffers the values emitted by these {@code ObservableSource}s and then drains them + * in order, each one after the previous one completes. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>This method does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <T> the value type + * @param sources a sequence of {@code ObservableSource}s that need to be eagerly concatenated + * @param maxConcurrency the maximum number of concurrently running inner {@code ObservableSource}s; {@link Integer#MAX_VALUE} + * is interpreted as all inner {@code ObservableSource}s can be active at the same time + * @param bufferSize the number of elements expected from each inner {@code ObservableSource} to be buffered + * @return the new {@code Observable} instance with the specified concatenation behavior + * @throws NullPointerException if {@code sources} is {@code null} + * @throws IllegalArgumentException if {@code maxConcurrency} or {@code bufferSize} is non-positive + * @since 2.0 + */ + @SuppressWarnings({ "unchecked", "rawtypes" }) + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public static <@NonNull T> Observable<T> concatEager(@NonNull Iterable<@NonNull ? extends ObservableSource<? extends T>> sources, int maxConcurrency, int bufferSize) { + return fromIterable(sources).concatMapEagerDelayError((Function)Functions.identity(), false, maxConcurrency, bufferSize); + } + + /** + * Concatenates an {@link ObservableSource} sequence of {@code ObservableSource}s eagerly into a single stream of values. + * <p> + * <img width="640" height="495" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Observable.concatEager.o.png" alt=""> + * <p> + * Eager concatenation means that once a subscriber subscribes, this operator subscribes to all of the + * emitted source {@code ObservableSource}s as they are observed. The operator buffers the values emitted by these + * {@code ObservableSource}s and then drains them in order, each one after the previous one completes. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>This method does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <T> the value type + * @param sources a sequence of {@code ObservableSource}s that need to be eagerly concatenated + * @return the new {@code Observable} instance with the specified concatenation behavior + * @throws NullPointerException if {@code sources} is {@code null} + * @since 2.0 + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public static <@NonNull T> Observable<T> concatEager(@NonNull ObservableSource<? extends ObservableSource<? extends T>> sources) { + return concatEager(sources, bufferSize(), bufferSize()); + } + + /** + * Concatenates an {@link ObservableSource} sequence of {@code ObservableSource}s eagerly into a single stream of values + * and runs a limited number of inner sequences at once. + * + * <p> + * <img width="640" height="442" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Observable.concatEager.on.png" alt=""> + * <p> + * Eager concatenation means that once a subscriber subscribes, this operator subscribes to all of the + * emitted source {@code ObservableSource}s as they are observed. The operator buffers the values emitted by these + * {@code ObservableSource}s and then drains them in order, each one after the previous one completes. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>This method does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <T> the value type + * @param sources a sequence of {@code ObservableSource}s that need to be eagerly concatenated + * @param maxConcurrency the maximum number of concurrently running inner {@code ObservableSource}s; {@link Integer#MAX_VALUE} + * is interpreted as all inner {@code ObservableSource}s can be active at the same time + * @param bufferSize the number of inner {@code ObservableSource} expected to be buffered + * @return the new {@code Observable} instance with the specified concatenation behavior + * @throws NullPointerException if {@code sources} is {@code null} + * @throws IllegalArgumentException if {@code maxConcurrency} or {@code bufferSize} is non-positive + * @since 2.0 + */ + @SuppressWarnings({ "unchecked", "rawtypes" }) + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public static <@NonNull T> Observable<T> concatEager(@NonNull ObservableSource<? extends ObservableSource<? extends T>> sources, int maxConcurrency, int bufferSize) { + return wrap(sources).concatMapEager((Function)Functions.identity(), maxConcurrency, bufferSize); + } + + /** + * Concatenates a sequence of {@link ObservableSource}s eagerly into a single stream of values, + * delaying errors until all the inner sequences terminate. + * <p> + * <img width="640" height="428" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Observable.concatEagerDelayError.i.png" alt=""> + * <p> + * Eager concatenation means that once a subscriber subscribes, this operator subscribes to all of the + * {@code ObservableSource}s. The operator buffers the values emitted by these {@code ObservableSource}s and then drains them + * in order, each one after the previous one completes. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>This method does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <T> the value type + * @param sources a sequence of {@code ObservableSource}s that need to be eagerly concatenated + * @return the new {@code Observable} instance with the specified concatenation behavior + * @throws NullPointerException if {@code sources} is {@code null} + * @since 3.0.0 + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public static <@NonNull T> Observable<T> concatEagerDelayError(@NonNull Iterable<@NonNull ? extends ObservableSource<? extends T>> sources) { + return concatEagerDelayError(sources, bufferSize(), bufferSize()); + } + + /** + * Concatenates a sequence of {@link ObservableSource}s eagerly into a single stream of values, + * delaying errors until all the inner sequences terminate and runs a limited number of inner + * sequences at once. + * <p> + * <img width="640" height="380" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Observable.concatEagerDelayError.in.png" alt=""> + * <p> + * Eager concatenation means that once a subscriber subscribes, this operator subscribes to all of the + * {@code ObservableSource}s. The operator buffers the values emitted by these {@code ObservableSource}s and then drains them + * in order, each one after the previous one completes. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>This method does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <T> the value type + * @param sources a sequence of {@code ObservableSource}s that need to be eagerly concatenated + * @param maxConcurrency the maximum number of concurrently running inner {@code ObservableSource}s; {@link Integer#MAX_VALUE} + * is interpreted as all inner {@code ObservableSource}s can be active at the same time + * @param bufferSize the number of elements expected from each inner {@code ObservableSource} to be buffered + * @return the new {@code Observable} instance with the specified concatenation behavior + * @throws NullPointerException if {@code sources} is {@code null} + * @throws IllegalArgumentException if {@code maxConcurrency} or {@code bufferSize} is non-positive + * @since 3.0.0 + */ + @SuppressWarnings({ "unchecked", "rawtypes" }) + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public static <@NonNull T> Observable<T> concatEagerDelayError(@NonNull Iterable<@NonNull ? extends ObservableSource<? extends T>> sources, int maxConcurrency, int bufferSize) { + return fromIterable(sources).concatMapEagerDelayError((Function)Functions.identity(), true, maxConcurrency, bufferSize); + } + + /** + * Concatenates an {@link ObservableSource} sequence of {@code ObservableSource}s eagerly into a single stream of values, + * delaying errors until all the inner and the outer sequence terminate. + * <p> + * <img width="640" height="496" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Observable.concatEagerDelayError.o.png" alt=""> + * <p> + * Eager concatenation means that once a subscriber subscribes, this operator subscribes to all of the + * emitted source {@code ObservableSource}s as they are observed. The operator buffers the values emitted by these + * {@code ObservableSource}s and then drains them in order, each one after the previous one completes. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>This method does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <T> the value type + * @param sources a sequence of {@code ObservableSource}s that need to be eagerly concatenated + * @return the new {@code Observable} instance with the specified concatenation behavior + * @throws NullPointerException if {@code sources} is {@code null} + * @since 3.0.0 + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public static <@NonNull T> Observable<T> concatEagerDelayError(@NonNull ObservableSource<? extends ObservableSource<? extends T>> sources) { + return concatEagerDelayError(sources, bufferSize(), bufferSize()); + } + + /** + * Concatenates an {@link ObservableSource} sequence of {@code ObservableSource}s eagerly into a single stream of values, + * delaying errors until all the inner and the outer sequence terminate and runs a limited number of inner sequences at once. + * <p> + * <img width="640" height="421" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Observable.concatEagerDelayError.on.png" alt=""> + * <p> + * Eager concatenation means that once a subscriber subscribes, this operator subscribes to all of the + * emitted source {@code ObservableSource}s as they are observed. The operator buffers the values emitted by these + * {@code ObservableSource}s and then drains them in order, each one after the previous one completes. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>This method does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <T> the value type + * @param sources a sequence of {@code ObservableSource}s that need to be eagerly concatenated + * @param maxConcurrency the maximum number of concurrently running inner {@code ObservableSource}s; {@link Integer#MAX_VALUE} + * is interpreted as all inner {@code ObservableSource}s can be active at the same time + * @param bufferSize the number of inner {@code ObservableSource} expected to be buffered + * @return the new {@code Observable} instance with the specified concatenation behavior + * @throws NullPointerException if {@code sources} is {@code null} + * @throws IllegalArgumentException if {@code maxConcurrency} or {@code bufferSize} is non-positive + * @since 3.0.0 + */ + @SuppressWarnings({ "unchecked", "rawtypes" }) + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public static <@NonNull T> Observable<T> concatEagerDelayError(@NonNull ObservableSource<? extends ObservableSource<? extends T>> sources, int maxConcurrency, int bufferSize) { + return wrap(sources).concatMapEagerDelayError((Function)Functions.identity(), true, maxConcurrency, bufferSize); + } + + /** + * Provides an API (via a cold {@code Observable}) that bridges the reactive world with the callback-style world. + * <p> + * Example: + * <pre><code> + * Observable.<Event>create(emitter -> { + * Callback listener = new Callback() { + * @Override + * public void onEvent(Event e) { + * emitter.onNext(e); + * if (e.isLast()) { + * emitter.onComplete(); + * } + * } + * + * @Override + * public void onFailure(Exception e) { + * emitter.onError(e); + * } + * }; + * + * AutoCloseable c = api.someMethod(listener); + * + * emitter.setCancellable(c::close); + * + * }); + * </code></pre> + * <p> + * Whenever an {@link Observer} subscribes to the returned {@code Observable}, the provided + * {@link ObservableOnSubscribe} callback is invoked with a fresh instance of an {@link ObservableEmitter} + * that will interact only with that specific {@code Observer}. If this {@code Observer} + * disposes the flow (making {@link ObservableEmitter#isDisposed} return {@code true}), + * other observers subscribed to the same returned {@code Observable} are not affected. + * <p> + * <img width="640" height="200" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/create.v3.png" alt=""> + * <p> + * You should call the {@code ObservableEmitter}'s {@code onNext}, {@code onError} and {@code onComplete} methods in a serialized fashion. The + * rest of its methods are thread-safe. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code create} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T> the element type + * @param source the emitter that is called when an {@code Observer} subscribes to the returned {@code Observable} + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code source} is {@code null} + * @see ObservableOnSubscribe + * @see ObservableEmitter + * @see Cancellable + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T> Observable<T> create(@NonNull ObservableOnSubscribe<T> source) { + Objects.requireNonNull(source, "source is null"); + return RxJavaPlugins.onAssembly(new ObservableCreate<>(source)); + } + + /** + * Returns an {@code Observable} that calls an {@link ObservableSource} factory to create an {@code ObservableSource} for each new {@link Observer} + * that subscribes. That is, for each subscriber, the actual {@code ObservableSource} that subscriber observes is + * determined by the factory function. + * <p> + * <img width="640" height="340" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/defer.v3.png" alt=""> + * <p> + * The {@code defer} operator allows you to defer or delay emitting items from an {@code ObservableSource} until such time as an + * {@code Observer} subscribes to the {@code ObservableSource}. This allows an {@code Observer} to easily obtain updates or a + * refreshed version of the sequence. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code defer} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param supplier + * the {@code ObservableSource} factory function to invoke for each {@code Observer} that subscribes to the + * resulting {@code Observable} + * @param <T> + * the type of the items emitted by the {@code ObservableSource} + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code supplier} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/defer.html">ReactiveX operators documentation: Defer</a> + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T> Observable<T> defer(@NonNull Supplier<? extends @NonNull ObservableSource<? extends T>> supplier) { + Objects.requireNonNull(supplier, "supplier is null"); + return RxJavaPlugins.onAssembly(new ObservableDefer<>(supplier)); + } + + /** + * Returns an {@code Observable} that emits no items to the {@link Observer} and immediately invokes its + * {@link Observer#onComplete onComplete} method. + * <p> + * <img width="640" height="190" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/empty.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code empty} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T> + * the type of the items (ostensibly) emitted by the {@code Observable} + * @return the shared {@code Observable} instance + * @see <a href="http://reactivex.io/documentation/operators/empty-never-throw.html">ReactiveX operators documentation: Empty</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @SuppressWarnings("unchecked") + @NonNull + public static <@NonNull T> Observable<T> empty() { + return RxJavaPlugins.onAssembly((Observable<T>) ObservableEmpty.INSTANCE); + } + + /** + * Returns an {@code Observable} that invokes an {@link Observer}'s {@link Observer#onError onError} method when the + * {@code Observer} subscribes to it. + * <p> + * <img width="640" height="221" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/error.supplier.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code error} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param supplier + * a {@link Supplier} factory to return a {@link Throwable} for each individual {@code Observer} + * @param <T> + * the type of the items (ostensibly) emitted by the {@code Observable} + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code supplier} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/empty-never-throw.html">ReactiveX operators documentation: Throw</a> + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T> Observable<T> error(@NonNull Supplier<? extends @NonNull Throwable> supplier) { + Objects.requireNonNull(supplier, "supplier is null"); + return RxJavaPlugins.onAssembly(new ObservableError<>(supplier)); + } + + /** + * Returns an {@code Observable} that invokes an {@link Observer}'s {@link Observer#onError onError} method when the + * {@code Observer} subscribes to it. + * <p> + * <img width="640" height="221" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/error.item.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code error} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param throwable + * the particular {@link Throwable} to pass to {@link Observer#onError onError} + * @param <T> + * the type of the items (ostensibly) emitted by the {@code Observable} + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code throwable} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/empty-never-throw.html">ReactiveX operators documentation: Throw</a> + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T> Observable<T> error(@NonNull Throwable throwable) { + Objects.requireNonNull(throwable, "throwable is null"); + return error(Functions.justSupplier(throwable)); + } + + /** + * Returns an {@code Observable} instance that runs the given {@link Action} for each {@link Observer} and + * emits either its exception or simply completes. + * <p> + * <img width="640" height="287" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Observable.fromAction.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code fromAction} does not operate by default on a particular {@link Scheduler}.</dd> + * <dt><b>Error handling:</b></dt> + * <dd> If the {@code Action} throws an exception, the respective {@link Throwable} is + * delivered to the downstream via {@link Observer#onError(Throwable)}, + * except when the downstream has canceled the resulting {@code Observable} source. + * In this latter case, the {@code Throwable} is delivered to the global error handler via + * {@link RxJavaPlugins#onError(Throwable)} as an {@link io.reactivex.rxjava3.exceptions.UndeliverableException UndeliverableException}. + * </dd> + * </dl> + * @param <T> the target type + * @param action the {@code Action} to run for each {@code Observer} + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code action} is {@code null} + * @since 3.0.0 + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T> Observable<T> fromAction(@NonNull Action action) { + Objects.requireNonNull(action, "action is null"); + return RxJavaPlugins.onAssembly(new ObservableFromAction<>(action)); + } + + /** + * Converts an array into an {@link ObservableSource} that emits the items in the array. + * <p> + * <img width="640" height="315" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/from.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code fromArray} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param items + * the array of elements + * @param <T> + * the type of items in the array and the type of items to be emitted by the resulting {@code Observable} + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code items} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/from.html">ReactiveX operators documentation: From</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + @SafeVarargs + public static <@NonNull T> Observable<T> fromArray(@NonNull T... items) { + Objects.requireNonNull(items, "items is null"); + if (items.length == 0) { + return empty(); + } + if (items.length == 1) { + return just(items[0]); + } + return RxJavaPlugins.onAssembly(new ObservableFromArray<>(items)); + } + + /** + * Returns an {@code Observable} that, when an observer subscribes to it, invokes a function you specify and then + * emits the value returned from that function. + * <p> + * <img width="640" height="310" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/fromCallable.v3.png" alt=""> + * <p> + * This allows you to defer the execution of the function you specify until an observer subscribes to the + * {@code Observable}. That is to say, it makes the function "lazy." + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code fromCallable} does not operate by default on a particular {@link Scheduler}.</dd> + * <dt><b>Error handling:</b></dt> + * <dd> If the {@link Callable} throws an exception, the respective {@link Throwable} is + * delivered to the downstream via {@link Observer#onError(Throwable)}, + * except when the downstream has disposed the current {@code Observable} source. + * In this latter case, the {@code Throwable} is delivered to the global error handler via + * {@link RxJavaPlugins#onError(Throwable)} as an {@link UndeliverableException}. + * </dd> + * </dl> + * @param callable + * a function, the execution of which should be deferred; {@code fromCallable} will invoke this + * function only when an observer subscribes to the {@code Observable} that {@code fromCallable} returns + * @param <T> + * the type of the item returned by the {@code Callable} and emitted by the {@code Observable} + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code callable} is {@code null} + * @see #defer(Supplier) + * @see #fromSupplier(Supplier) + * @since 2.0 + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T> Observable<T> fromCallable(@NonNull Callable<? extends T> callable) { + Objects.requireNonNull(callable, "callable is null"); + return RxJavaPlugins.onAssembly(new ObservableFromCallable<>(callable)); + } + + /** + * Wraps a {@link CompletableSource} into an {@code Observable}. + * <p> + * <img width="640" height="278" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Observable.fromCompletable.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code fromCompletable} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <T> the target type + * @param completableSource the {@code CompletableSource} to convert from + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code completableSource} is {@code null} + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T> Observable<T> fromCompletable(@NonNull CompletableSource completableSource) { + Objects.requireNonNull(completableSource, "completableSource is null"); + return RxJavaPlugins.onAssembly(new ObservableFromCompletable<>(completableSource)); + } + + /** + * Converts a {@link Future} into an {@code Observable}. + * <p> + * <img width="640" height="284" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/fromFuture.noarg.png" alt=""> + * <p> + * The operator calls {@link Future#get()}, which is a blocking method, on the subscription thread. + * It is recommended applying {@link #subscribeOn(Scheduler)} to move this blocking wait to a + * background thread, and if the {@link Scheduler} supports it, interrupt the wait when the flow + * is disposed. + * <p> + * Unlike 1.x, disposing the {@code Observable} won't cancel the future. If necessary, one can use composition to achieve the + * cancellation effect: {@code futureObservableSource.doOnDispose(() -> future.cancel(true));}. + * <p> + * Also note that this operator will consume a {@link CompletionStage}-based {@code Future} subclass (such as + * {@link CompletableFuture}) in a blocking manner as well. Use the {@link #fromCompletionStage(CompletionStage)} + * operator to convert and consume such sources in a non-blocking fashion instead. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code fromFuture} does not operate by default on a particular {@code Scheduler}.</dd> + * </dl> + * + * @param future + * the source {@code Future} + * @param <T> + * the type of object that the {@code Future} returns, and also the type of item to be emitted by + * the resulting {@code Observable} + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code future} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/from.html">ReactiveX operators documentation: From</a> + * @see #fromCompletionStage(CompletionStage) + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T> Observable<T> fromFuture(@NonNull Future<? extends T> future) { + Objects.requireNonNull(future, "future is null"); + return RxJavaPlugins.onAssembly(new ObservableFromFuture<>(future, 0L, null)); + } + + /** + * Converts a {@link Future} into an {@code Observable}, with a timeout on the {@code Future}. + * <p> + * <img width="640" height="287" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/fromFuture.timeout.png" alt=""> + * <p> + * The operator calls {@link Future#get(long, TimeUnit)}, which is a blocking method, on the subscription thread. + * It is recommended applying {@link #subscribeOn(Scheduler)} to move this blocking wait to a + * background thread, and if the {@link Scheduler} supports it, interrupt the wait when the flow + * is disposed. + * <p> + * Unlike 1.x, disposing the {@code Observable} won't cancel the future. If necessary, one can use composition to achieve the + * cancellation effect: {@code futureObservableSource.doOnDispose(() -> future.cancel(true));}. + * <p> + * Also note that this operator will consume a {@link CompletionStage}-based {@code Future} subclass (such as + * {@link CompletableFuture}) in a blocking manner as well. Use the {@link #fromCompletionStage(CompletionStage)} + * operator to convert and consume such sources in a non-blocking fashion instead. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code fromFuture} does not operate by default on a particular {@code Scheduler}.</dd> + * </dl> + * + * @param future + * the source {@code Future} + * @param timeout + * the maximum time to wait before calling {@code get} + * @param unit + * the {@link TimeUnit} of the {@code timeout} argument + * @param <T> + * the type of object that the {@code Future} returns, and also the type of item to be emitted by + * the resulting {@code Observable} + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code future} or {@code unit} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/from.html">ReactiveX operators documentation: From</a> + * @see #fromCompletionStage(CompletionStage) + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T> Observable<T> fromFuture(@NonNull Future<? extends T> future, long timeout, @NonNull TimeUnit unit) { + Objects.requireNonNull(future, "future is null"); + Objects.requireNonNull(unit, "unit is null"); + return RxJavaPlugins.onAssembly(new ObservableFromFuture<>(future, timeout, unit)); + } + + /** + * Converts an {@link Iterable} sequence into an {@code Observable} that emits the items in the sequence. + * <p> + * <img width="640" height="187" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/fromIterable.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code fromIterable} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param source + * the source {@code Iterable} sequence + * @param <T> + * the type of items in the {@code Iterable} sequence and the type of items to be emitted by the + * resulting {@code Observable} + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code source} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/from.html">ReactiveX operators documentation: From</a> + * @see #fromStream(Stream) + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T> Observable<T> fromIterable(@NonNull Iterable<? extends T> source) { + Objects.requireNonNull(source, "source is null"); + return RxJavaPlugins.onAssembly(new ObservableFromIterable<>(source)); + } + + /** + * Returns an {@code Observable} instance that when subscribed to, subscribes to the {@link MaybeSource} instance and + * emits {@code onSuccess} as a single item or forwards any {@code onComplete} or + * {@code onError} signal. + * <p> + * <img width="640" height="226" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Observable.fromMaybe.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code fromMaybe} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <T> the value type of the {@code MaybeSource} element + * @param maybe the {@code MaybeSource} instance to subscribe to, not {@code null} + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code maybe} is {@code null} + * @since 3.0.0 + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T> Observable<T> fromMaybe(@NonNull MaybeSource<T> maybe) { + Objects.requireNonNull(maybe, "maybe is null"); + return RxJavaPlugins.onAssembly(new MaybeToObservable<>(maybe)); + } + + /** + * Converts an arbitrary <em>Reactive Streams</em> {@link Publisher} into an {@code Observable}. + * <p> + * <img width="640" height="344" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/fromPublisher.o.png" alt=""> + * <p> + * The {@code Publisher} must follow the + * <a href="https://github.com/reactive-streams/reactive-streams-jvm#reactive-streams">Reactive-Streams specification</a>. + * Violating the specification may result in undefined behavior. + * <p> + * If possible, use {@link #create(ObservableOnSubscribe)} to create a + * source-like {@code Observable} instead. + * <p> + * Note that even though {@code Publisher} appears to be a functional interface, it + * is not recommended to implement it through a lambda as the specification requires + * state management that is not achievable with a stateless lambda. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The source {@code publisher} is consumed in an unbounded fashion without applying any + * backpressure to it.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code fromPublisher} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <T> the value type of the flow + * @param publisher the {@code Publisher} to convert + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code publisher} is {@code null} + * @see #create(ObservableOnSubscribe) + */ + @BackpressureSupport(BackpressureKind.UNBOUNDED_IN) + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T> Observable<T> fromPublisher(@NonNull Publisher<? extends T> publisher) { + Objects.requireNonNull(publisher, "publisher is null"); + return RxJavaPlugins.onAssembly(new ObservableFromPublisher<>(publisher)); + } + + /** + * Returns an {@code Observable} instance that runs the given {@link Runnable} for each {@link Observer} and + * emits either its unchecked exception or simply completes. + * <p> + * <img width="640" height="286" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Observable.fromRunnable.png" alt=""> + * <p> + * If the code to be wrapped needs to throw a checked or more broader {@link Throwable} exception, that + * exception has to be converted to an unchecked exception by the wrapped code itself. Alternatively, + * use the {@link #fromAction(Action)} method which allows the wrapped code to throw any {@code Throwable} + * exception and will signal it to observers as-is. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code fromRunnable} does not operate by default on a particular {@link Scheduler}.</dd> + * <dt><b>Error handling:</b></dt> + * <dd> If the {@code Runnable} throws an exception, the respective {@code Throwable} is + * delivered to the downstream via {@link Observer#onError(Throwable)}, + * except when the downstream has canceled the resulting {@code Observable} source. + * In this latter case, the {@code Throwable} is delivered to the global error handler via + * {@link RxJavaPlugins#onError(Throwable)} as an {@link io.reactivex.rxjava3.exceptions.UndeliverableException UndeliverableException}. + * </dd> + * </dl> + * @param <T> the target type + * @param run the {@code Runnable} to run for each {@code Observer} + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code run} is {@code null} + * @since 3.0.0 + * @see #fromAction(Action) + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T> Observable<T> fromRunnable(@NonNull Runnable run) { + Objects.requireNonNull(run, "run is null"); + return RxJavaPlugins.onAssembly(new ObservableFromRunnable<>(run)); + } + + /** + * Returns an {@code Observable} instance that when subscribed to, subscribes to the {@link SingleSource} instance and + * emits {@code onSuccess} as a single item or forwards the {@code onError} signal. + * <p> + * <img width="640" height="341" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Observable.fromSingle.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code fromSingle} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <T> the value type of the {@code SingleSource} element + * @param source the {@code SingleSource} instance to subscribe to, not {@code null} + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code source} is {@code null} + * @since 3.0.0 + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T> Observable<T> fromSingle(@NonNull SingleSource<T> source) { + Objects.requireNonNull(source, "source is null"); + return RxJavaPlugins.onAssembly(new SingleToObservable<>(source)); + } + + /** + * Returns an {@code Observable} that, when an observer subscribes to it, invokes a supplier function you specify and then + * emits the value returned from that function. + * <p> + * <img width="640" height="310" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Observable.fromSupplier.v3.png" alt=""> + * <p> + * This allows you to defer the execution of the function you specify until an observer subscribes to the + * {@code Observable}. That is to say, it makes the function "lazy." + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code fromSupplier} does not operate by default on a particular {@link Scheduler}.</dd> + * <dt><b>Error handling:</b></dt> + * <dd> If the {@link Supplier} throws an exception, the respective {@link Throwable} is + * delivered to the downstream via {@link Observer#onError(Throwable)}, + * except when the downstream has disposed the current {@code Observable} source. + * In this latter case, the {@code Throwable} is delivered to the global error handler via + * {@link RxJavaPlugins#onError(Throwable)} as an {@link UndeliverableException}. + * </dd> + * </dl> + * @param supplier + * a function, the execution of which should be deferred; {@code fromSupplier} will invoke this + * function only when an observer subscribes to the {@code Observable} that {@code fromSupplier} returns + * @param <T> + * the type of the item emitted by the {@code Observable} + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code supplier} is {@code null} + * @see #defer(Supplier) + * @see #fromCallable(Callable) + * @since 3.0.0 + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T> Observable<T> fromSupplier(@NonNull Supplier<? extends T> supplier) { + Objects.requireNonNull(supplier, "supplier is null"); + return RxJavaPlugins.onAssembly(new ObservableFromSupplier<>(supplier)); + } + + /** + * Returns a cold, synchronous and stateless generator of values. + * <p> + * <img width="640" height="315" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/generate.2.v3.png" alt=""> + * <p> + * Note that the {@link Emitter#onNext}, {@link Emitter#onError} and + * {@link Emitter#onComplete} methods provided to the function via the {@link Emitter} instance should be called synchronously, + * never concurrently and only while the function body is executing. Calling them from multiple threads + * or outside the function call is not supported and leads to an undefined behavior. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code generate} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T> the generated value type + * @param generator the {@link Consumer} called in a loop after a downstream {@link Observer} has + * subscribed. The callback then should call {@code onNext}, {@code onError} or + * {@code onComplete} to signal a value or a terminal event. Signaling multiple {@code onNext} + * in a call will make the operator signal {@link IllegalStateException}. + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code generator} is {@code null} + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T> Observable<T> generate(@NonNull Consumer<Emitter<T>> generator) { + Objects.requireNonNull(generator, "generator is null"); + return generate(Functions.nullSupplier(), + ObservableInternalHelper.simpleGenerator(generator), Functions.emptyConsumer()); + } + + /** + * Returns a cold, synchronous and stateful generator of values. + * <p> + * <img width="640" height="315" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/generate.2.v3.png" alt=""> + * <p> + * Note that the {@link Emitter#onNext}, {@link Emitter#onError} and + * {@link Emitter#onComplete} methods provided to the function via the {@link Emitter} instance should be called synchronously, + * never concurrently and only while the function body is executing. Calling them from multiple threads + * or outside the function call is not supported and leads to an undefined behavior. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code generate} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <S> the type of the per-{@link Observer} state + * @param <T> the generated value type + * @param initialState the {@link Supplier} to generate the initial state for each {@code Observer} + * @param generator the {@link BiConsumer} called in a loop after a downstream {@code Observer} has + * subscribed. The callback then should call {@code onNext}, {@code onError} or + * {@code onComplete} to signal a value or a terminal event. Signaling multiple {@code onNext} + * in a call will make the operator signal {@link IllegalStateException}. + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code initialState} or {@code generator} is {@code null} + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T, @NonNull S> Observable<T> generate(@NonNull Supplier<S> initialState, @NonNull BiConsumer<S, Emitter<T>> generator) { + Objects.requireNonNull(generator, "generator is null"); + return generate(initialState, ObservableInternalHelper.simpleBiGenerator(generator), Functions.emptyConsumer()); + } + + /** + * Returns a cold, synchronous and stateful generator of values. + * <p> + * <img width="640" height="315" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/generate.2.v3.png" alt=""> + * <p> + * Note that the {@link Emitter#onNext}, {@link Emitter#onError} and + * {@link Emitter#onComplete} methods provided to the function via the {@link Emitter} instance should be called synchronously, + * never concurrently and only while the function body is executing. Calling them from multiple threads + * or outside the function call is not supported and leads to an undefined behavior. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code generate} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <S> the type of the per-{@link Observer} state + * @param <T> the generated value type + * @param initialState the {@link Supplier} to generate the initial state for each {@code Observer} + * @param generator the {@link BiConsumer} called in a loop after a downstream {@code Observer} has + * subscribed. The callback then should call {@code onNext}, {@code onError} or + * {@code onComplete} to signal a value or a terminal event. Signaling multiple {@code onNext} + * in a call will make the operator signal {@link IllegalStateException}. + * @param disposeState the {@link Consumer} that is called with the current state when the generator + * terminates the sequence or it gets disposed + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code initialState}, {@code generator} or {@code disposeState} is {@code null} + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T, @NonNull S> Observable<T> generate( + @NonNull Supplier<S> initialState, + @NonNull BiConsumer<S, Emitter<T>> generator, + @NonNull Consumer<? super S> disposeState) { + Objects.requireNonNull(generator, "generator is null"); + return generate(initialState, ObservableInternalHelper.simpleBiGenerator(generator), disposeState); + } + + /** + * Returns a cold, synchronous and stateful generator of values. + * <p> + * <img width="640" height="315" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/generate.2.v3.png" alt=""> + * <p> + * Note that the {@link Emitter#onNext}, {@link Emitter#onError} and + * {@link Emitter#onComplete} methods provided to the function via the {@link Emitter} instance should be called synchronously, + * never concurrently and only while the function body is executing. Calling them from multiple threads + * or outside the function call is not supported and leads to an undefined behavior. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code generate} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <S> the type of the per-{@link Observer} state + * @param <T> the generated value type + * @param initialState the {@link Supplier} to generate the initial state for each {@code Observer} + * @param generator the {@link BiConsumer} called in a loop after a downstream {@code Observer} has + * subscribed. The callback then should call {@code onNext}, {@code onError} or + * {@code onComplete} to signal a value or a terminal event and should return a (new) state for + * the next invocation. Signaling multiple {@code onNext} + * in a call will make the operator signal {@link IllegalStateException}. + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code initialState} or {@code generator} is {@code null} + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public static <@NonNull T, @NonNull S> Observable<T> generate(@NonNull Supplier<S> initialState, @NonNull BiFunction<S, Emitter<T>, S> generator) { + return generate(initialState, generator, Functions.emptyConsumer()); + } + + /** + * Returns a cold, synchronous and stateful generator of values. + * <p> + * <img width="640" height="315" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/generate.2.v3.png" alt=""> + * <p> + * Note that the {@link Emitter#onNext}, {@link Emitter#onError} and + * {@link Emitter#onComplete} methods provided to the function via the {@link Emitter} instance should be called synchronously, + * never concurrently and only while the function body is executing. Calling them from multiple threads + * or outside the function call is not supported and leads to an undefined behavior. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code generate} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <S> the type of the per-{@link Observer} state + * @param <T> the generated value type + * @param initialState the {@link Supplier} to generate the initial state for each {@code Observer} + * @param generator the {@link BiConsumer} called in a loop after a downstream {@code Observer} has + * subscribed. The callback then should call {@code onNext}, {@code onError} or + * {@code onComplete} to signal a value or a terminal event and should return a (new) state for + * the next invocation. Signaling multiple {@code onNext} + * in a call will make the operator signal {@link IllegalStateException}. + * @param disposeState the {@link Consumer} that is called with the current state when the generator + * terminates the sequence or it gets disposed + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code initialState}, {@code generator} or {@code disposeState} is {@code null} + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T, @NonNull S> Observable<T> generate(@NonNull Supplier<S> initialState, @NonNull BiFunction<S, Emitter<T>, S> generator, + @NonNull Consumer<? super S> disposeState) { + Objects.requireNonNull(initialState, "initialState is null"); + Objects.requireNonNull(generator, "generator is null"); + Objects.requireNonNull(disposeState, "disposeState is null"); + return RxJavaPlugins.onAssembly(new ObservableGenerate<>(initialState, generator, disposeState)); + } + + /** + * Returns an {@code Observable} that emits a {@code 0L} after the {@code initialDelay} and ever increasing numbers + * after each {@code period} of time thereafter. + * <p> + * <img width="640" height="200" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/timer.p.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code interval} operates by default on the {@code computation} {@link Scheduler}.</dd> + * </dl> + * + * @param initialDelay + * the initial delay time to wait before emitting the first value of 0L + * @param period + * the period of time between emissions of the subsequent numbers + * @param unit + * the time unit for both {@code initialDelay} and {@code period} + * @return the new {@code Observable} instance + * @see <a href="http://reactivex.io/documentation/operators/interval.html">ReactiveX operators documentation: Interval</a> + * @throws NullPointerException if {@code unit} is {@code null} + * @since 1.0.12 + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.COMPUTATION) + @NonNull + public static Observable<Long> interval(long initialDelay, long period, @NonNull TimeUnit unit) { + return interval(initialDelay, period, unit, Schedulers.computation()); + } + + /** + * Returns an {@code Observable} that emits a {@code 0L} after the {@code initialDelay} and ever increasing numbers + * after each {@code period} of time thereafter, on a specified {@link Scheduler}. + * <p> + * <img width="640" height="200" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/timer.ps.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>You specify which {@code Scheduler} this operator will use.</dd> + * </dl> + * + * @param initialDelay + * the initial delay time to wait before emitting the first value of 0L + * @param period + * the period of time between emissions of the subsequent numbers + * @param unit + * the time unit for both {@code initialDelay} and {@code period} + * @param scheduler + * the {@code Scheduler} on which the waiting happens and items are emitted + * @return the new {@code Observable} instance + * @see <a href="http://reactivex.io/documentation/operators/interval.html">ReactiveX operators documentation: Interval</a> + * @since 1.0.12 + * @throws NullPointerException if {@code unit} or {@code scheduler} is {@code null} + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.CUSTOM) + public static Observable<Long> interval(long initialDelay, long period, @NonNull TimeUnit unit, @NonNull Scheduler scheduler) { + Objects.requireNonNull(unit, "unit is null"); + Objects.requireNonNull(scheduler, "scheduler is null"); + + return RxJavaPlugins.onAssembly(new ObservableInterval(Math.max(0L, initialDelay), Math.max(0L, period), unit, scheduler)); + } + + /** + * Returns an {@code Observable} that emits a sequential number every specified interval of time. + * <p> + * <img width="640" height="195" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/interval.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code interval} operates by default on the {@code computation} {@link Scheduler}.</dd> + * </dl> + * + * @param period + * the period size in time units (see below) + * @param unit + * time units to use for the interval size + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code unit} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/interval.html">ReactiveX operators documentation: Interval</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.COMPUTATION) + @NonNull + public static Observable<Long> interval(long period, @NonNull TimeUnit unit) { + return interval(period, period, unit, Schedulers.computation()); + } + + /** + * Returns an {@code Observable} that emits a sequential number every specified interval of time, on a + * specified {@link Scheduler}. + * <p> + * <img width="640" height="200" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/interval.s.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>You specify which {@code Scheduler} this operator will use.</dd> + * </dl> + * + * @param period + * the period size in time units (see below) + * @param unit + * time units to use for the interval size + * @param scheduler + * the {@code Scheduler} to use for scheduling the items + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code unit} or {@code scheduler} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/interval.html">ReactiveX operators documentation: Interval</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.CUSTOM) + @NonNull + public static Observable<Long> interval(long period, @NonNull TimeUnit unit, @NonNull Scheduler scheduler) { + return interval(period, period, unit, scheduler); + } + + /** + * Signals a range of long values, the first after some initial delay and the rest periodically after. + * <p> + * The sequence completes immediately after the last value (start + count - 1) has been reached. + * <p> + * <img width="640" height="195" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/intervalRange.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code intervalRange} by default operates on the {@link Schedulers#computation() computation} {@link Scheduler}.</dd> + * </dl> + * @param start that start value of the range + * @param count the number of values to emit in total, if zero, the operator emits an {@code onComplete} after the initial delay. + * @param initialDelay the initial delay before signaling the first value (the start) + * @param period the period between subsequent values + * @param unit the unit of measure of the {@code initialDelay} and {@code period} amounts + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code unit} is {@code null} + * @throws IllegalArgumentException + * if {@code count} is negative, or if {@code start} + {@code count} − 1 exceeds + * {@link Long#MAX_VALUE} + * @see #range(int, int) + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.COMPUTATION) + public static Observable<Long> intervalRange(long start, long count, long initialDelay, long period, @NonNull TimeUnit unit) { + return intervalRange(start, count, initialDelay, period, unit, Schedulers.computation()); + } + + /** + * Signals a range of long values, the first after some initial delay and the rest periodically after. + * <p> + * The sequence completes immediately after the last value (start + count - 1) has been reached. + * <p> + * <img width="640" height="195" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/intervalRange.s.v3.png" alt=""> * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>you provide the {@link Scheduler}.</dd> + * </dl> + * @param start that start value of the range + * @param count the number of values to emit in total, if zero, the operator emits an {@code onComplete} after the initial delay. + * @param initialDelay the initial delay before signaling the first value (the start) + * @param period the period between subsequent values + * @param unit the unit of measure of the {@code initialDelay} and {@code period} amounts + * @param scheduler the target scheduler where the values and terminal signals will be emitted + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code unit} or {@code scheduler} is {@code null} + * @throws IllegalArgumentException + * if {@code count} is negative, or if {@code start} + {@code count} − 1 exceeds + * {@link Long#MAX_VALUE} + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.CUSTOM) + public static Observable<Long> intervalRange(long start, long count, long initialDelay, long period, @NonNull TimeUnit unit, @NonNull Scheduler scheduler) { + if (count < 0) { + throw new IllegalArgumentException("count >= 0 required but it was " + count); + } + + if (count == 0L) { + return Observable.<Long>empty().delay(initialDelay, unit, scheduler); + } + + long end = start + (count - 1); + if (start > 0 && end < 0) { + throw new IllegalArgumentException("Overflow! start + count is bigger than Long.MAX_VALUE"); + } + Objects.requireNonNull(unit, "unit is null"); + Objects.requireNonNull(scheduler, "scheduler is null"); + + return RxJavaPlugins.onAssembly(new ObservableIntervalRange(start, end, Math.max(0L, initialDelay), Math.max(0L, period), unit, scheduler)); + } + + /** + * Returns an {@code Observable} that signals the given (constant reference) item and then completes. + * <p> + * <img width="640" height="290" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/just.item.png" alt=""> + * <p> + * Note that the item is taken and re-emitted as is and not computed by any means by {@code just}. Use {@link #fromCallable(Callable)} + * to generate a single item on demand (when {@link Observer}s subscribe to it). + * <p> + * See the multi-parameter overloads of {@code just} to emit more than one (constant reference) items one after the other. + * Use {@link #fromArray(Object...)} to emit an arbitrary number of items that are known upfront. + * <p> + * To emit the items of an {@link Iterable} sequence (such as a {@link java.util.List}), use {@link #fromIterable(Iterable)}. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code just} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param item + * the item to emit + * @param <T> + * the type of that item + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code item} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/just.html">ReactiveX operators documentation: Just</a> + * @see #just(Object, Object) + * @see #fromCallable(Callable) + * @see #fromArray(Object...) + * @see #fromIterable(Iterable) + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T> Observable<T> just(@NonNull T item) { + Objects.requireNonNull(item, "item is null"); + return RxJavaPlugins.onAssembly(new ObservableJust<>(item)); + } + + /** + * Converts two items into an {@code Observable} that emits those items. + * <p> + * <img width="640" height="186" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/just.2.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code just} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param item1 + * first item + * @param item2 + * second item + * @param <T> + * the type of these items + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code item1} or {@code item2} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/just.html">ReactiveX operators documentation: Just</a> + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T> Observable<T> just(@NonNull T item1, @NonNull T item2) { + Objects.requireNonNull(item1, "item1 is null"); + Objects.requireNonNull(item2, "item2 is null"); + + return fromArray(item1, item2); + } + + /** + * Converts three items into an {@code Observable} that emits those items. + * <p> + * <img width="640" height="186" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/just.3.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code just} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param item1 + * first item + * @param item2 + * second item + * @param item3 + * third item + * @param <T> + * the type of these items + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code item1}, {@code item2} or {@code item3} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/just.html">ReactiveX operators documentation: Just</a> + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T> Observable<T> just(@NonNull T item1, @NonNull T item2, @NonNull T item3) { + Objects.requireNonNull(item1, "item1 is null"); + Objects.requireNonNull(item2, "item2 is null"); + Objects.requireNonNull(item3, "item3 is null"); + + return fromArray(item1, item2, item3); + } + + /** + * Converts four items into an {@code Observable} that emits those items. + * <p> + * <img width="640" height="186" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/just.4.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code just} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param item1 + * first item + * @param item2 + * second item + * @param item3 + * third item + * @param item4 + * fourth item + * @param <T> + * the type of these items + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code item1}, {@code item2}, {@code item3} or {@code item4} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/just.html">ReactiveX operators documentation: Just</a> + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T> Observable<T> just(@NonNull T item1, @NonNull T item2, @NonNull T item3, @NonNull T item4) { + Objects.requireNonNull(item1, "item1 is null"); + Objects.requireNonNull(item2, "item2 is null"); + Objects.requireNonNull(item3, "item3 is null"); + Objects.requireNonNull(item4, "item4 is null"); + + return fromArray(item1, item2, item3, item4); + } + + /** + * Converts five items into an {@code Observable} that emits those items. + * <p> + * <img width="640" height="186" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/just.5.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code just} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param item1 + * first item + * @param item2 + * second item + * @param item3 + * third item + * @param item4 + * fourth item + * @param item5 + * fifth item + * @param <T> + * the type of these items + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code item1}, {@code item2}, {@code item3}, + * {@code item4} or {@code item5} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/just.html">ReactiveX operators documentation: Just</a> + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T> Observable<T> just(@NonNull T item1, @NonNull T item2, @NonNull T item3, @NonNull T item4, @NonNull T item5) { + Objects.requireNonNull(item1, "item1 is null"); + Objects.requireNonNull(item2, "item2 is null"); + Objects.requireNonNull(item3, "item3 is null"); + Objects.requireNonNull(item4, "item4 is null"); + Objects.requireNonNull(item5, "item5 is null"); + + return fromArray(item1, item2, item3, item4, item5); + } + + /** + * Converts six items into an {@code Observable} that emits those items. + * <p> + * <img width="640" height="186" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/just.6.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code just} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param item1 + * first item + * @param item2 + * second item + * @param item3 + * third item + * @param item4 + * fourth item + * @param item5 + * fifth item + * @param item6 + * sixth item + * @param <T> + * the type of these items + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code item1}, {@code item2}, {@code item3}, + * {@code item4}, {@code item5} or {@code item6} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/just.html">ReactiveX operators documentation: Just</a> + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T> Observable<T> just(@NonNull T item1, @NonNull T item2, @NonNull T item3, @NonNull T item4, @NonNull T item5, @NonNull T item6) { + Objects.requireNonNull(item1, "item1 is null"); + Objects.requireNonNull(item2, "item2 is null"); + Objects.requireNonNull(item3, "item3 is null"); + Objects.requireNonNull(item4, "item4 is null"); + Objects.requireNonNull(item5, "item5 is null"); + Objects.requireNonNull(item6, "item6 is null"); + + return fromArray(item1, item2, item3, item4, item5, item6); + } + + /** + * Converts seven items into an {@code Observable} that emits those items. + * <p> + * <img width="640" height="186" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/just.7.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code just} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param item1 + * first item + * @param item2 + * second item + * @param item3 + * third item + * @param item4 + * fourth item + * @param item5 + * fifth item + * @param item6 + * sixth item + * @param item7 + * seventh item + * @param <T> + * the type of these items + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code item1}, {@code item2}, {@code item3}, + * {@code item4}, {@code item5}, {@code item6} + * or {@code item7} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/just.html">ReactiveX operators documentation: Just</a> + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T> Observable<T> just(@NonNull T item1, @NonNull T item2, @NonNull T item3, @NonNull T item4, @NonNull T item5, @NonNull T item6, @NonNull T item7) { + Objects.requireNonNull(item1, "item1 is null"); + Objects.requireNonNull(item2, "item2 is null"); + Objects.requireNonNull(item3, "item3 is null"); + Objects.requireNonNull(item4, "item4 is null"); + Objects.requireNonNull(item5, "item5 is null"); + Objects.requireNonNull(item6, "item6 is null"); + Objects.requireNonNull(item7, "item7 is null"); + + return fromArray(item1, item2, item3, item4, item5, item6, item7); + } + + /** + * Converts eight items into an {@code Observable} that emits those items. + * <p> + * <img width="640" height="186" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/just.8.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code just} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param item1 + * first item + * @param item2 + * second item + * @param item3 + * third item + * @param item4 + * fourth item + * @param item5 + * fifth item + * @param item6 + * sixth item + * @param item7 + * seventh item + * @param item8 + * eighth item + * @param <T> + * the type of these items + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code item1}, {@code item2}, {@code item3}, + * {@code item4}, {@code item5}, {@code item6} + * {@code item7} or {@code item8} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/just.html">ReactiveX operators documentation: Just</a> + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T> Observable<T> just(@NonNull T item1, @NonNull T item2, @NonNull T item3, @NonNull T item4, @NonNull T item5, @NonNull T item6, @NonNull T item7, @NonNull T item8) { + Objects.requireNonNull(item1, "item1 is null"); + Objects.requireNonNull(item2, "item2 is null"); + Objects.requireNonNull(item3, "item3 is null"); + Objects.requireNonNull(item4, "item4 is null"); + Objects.requireNonNull(item5, "item5 is null"); + Objects.requireNonNull(item6, "item6 is null"); + Objects.requireNonNull(item7, "item7 is null"); + Objects.requireNonNull(item8, "item8 is null"); + + return fromArray(item1, item2, item3, item4, item5, item6, item7, item8); + } + + /** + * Converts nine items into an {@code Observable} that emits those items. + * <p> + * <img width="640" height="186" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/just.9.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code just} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param item1 + * first item + * @param item2 + * second item + * @param item3 + * third item + * @param item4 + * fourth item + * @param item5 + * fifth item + * @param item6 + * sixth item + * @param item7 + * seventh item + * @param item8 + * eighth item + * @param item9 + * ninth item + * @param <T> + * the type of these items + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code item1}, {@code item2}, {@code item3}, + * {@code item4}, {@code item5}, {@code item6} + * {@code item7}, {@code item8} or {@code item9} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/just.html">ReactiveX operators documentation: Just</a> + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T> Observable<T> just(@NonNull T item1, @NonNull T item2, @NonNull T item3, @NonNull T item4, @NonNull T item5, @NonNull T item6, @NonNull T item7, @NonNull T item8, @NonNull T item9) { + Objects.requireNonNull(item1, "item1 is null"); + Objects.requireNonNull(item2, "item2 is null"); + Objects.requireNonNull(item3, "item3 is null"); + Objects.requireNonNull(item4, "item4 is null"); + Objects.requireNonNull(item5, "item5 is null"); + Objects.requireNonNull(item6, "item6 is null"); + Objects.requireNonNull(item7, "item7 is null"); + Objects.requireNonNull(item8, "item8 is null"); + Objects.requireNonNull(item9, "item9 is null"); + + return fromArray(item1, item2, item3, item4, item5, item6, item7, item8, item9); + } + + /** + * Converts ten items into an {@code Observable} that emits those items. + * <p> + * <img width="640" height="186" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/just.10.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code just} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param item1 + * first item + * @param item2 + * second item + * @param item3 + * third item + * @param item4 + * fourth item + * @param item5 + * fifth item + * @param item6 + * sixth item + * @param item7 + * seventh item + * @param item8 + * eighth item + * @param item9 + * ninth item + * @param item10 + * tenth item + * @param <T> + * the type of these items + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code item1}, {@code item2}, {@code item3}, + * {@code item4}, {@code item5}, {@code item6} + * {@code item7}, {@code item8}, {@code item9} + * or {@code item10} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/just.html">ReactiveX operators documentation: Just</a> + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T> Observable<T> just(@NonNull T item1, @NonNull T item2, @NonNull T item3, @NonNull T item4, @NonNull T item5, @NonNull T item6, @NonNull T item7, @NonNull T item8, @NonNull T item9, @NonNull T item10) { + Objects.requireNonNull(item1, "item1 is null"); + Objects.requireNonNull(item2, "item2 is null"); + Objects.requireNonNull(item3, "item3 is null"); + Objects.requireNonNull(item4, "item4 is null"); + Objects.requireNonNull(item5, "item5 is null"); + Objects.requireNonNull(item6, "item6 is null"); + Objects.requireNonNull(item7, "item7 is null"); + Objects.requireNonNull(item8, "item8 is null"); + Objects.requireNonNull(item9, "item9 is null"); + Objects.requireNonNull(item10, "item10 is null"); + + return fromArray(item1, item2, item3, item4, item5, item6, item7, item8, item9, item10); + } + + /** + * Flattens an {@link Iterable} of {@link ObservableSource}s into one {@code Observable}, without any transformation, while limiting the + * number of concurrent subscriptions to these {@code ObservableSource}s. + * <p> + * <img width="640" height="380" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/merge.v3.png" alt=""> + * <p> + * You can combine the items emitted by multiple {@code ObservableSource}s so that they appear as a single {@code ObservableSource}, by + * using the {@code merge} method. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code merge} does not operate by default on a particular {@link Scheduler}.</dd> + * <dt><b>Error handling:</b></dt> + * <dd>If any of the returned {@code ObservableSource}s signal a {@link Throwable} via {@code onError}, the resulting + * {@code Observable} terminates with that {@code Throwable} and all other source {@code ObservableSource}s are disposed. + * If more than one {@code ObservableSource} signals an error, the resulting {@code Observable} may terminate with the + * first one's error or, depending on the concurrency of the sources, may terminate with a + * {@link CompositeException} containing two or more of the various error signals. + * {@code Throwable}s that didn't make into the composite will be sent (individually) to the global error handler via + * {@link RxJavaPlugins#onError(Throwable)} method as {@link UndeliverableException} errors. Similarly, {@code Throwable}s + * signaled by source(s) after the returned {@code Observable} has been disposed or terminated with a + * (composite) error will be sent to the same global error handler. + * Use {@link #mergeDelayError(Iterable, int, int)} to merge sources and terminate only when all source {@code ObservableSource}s + * have completed or failed with an error. + * </dd> + * </dl> + * + * @param <T> the common element base type + * @param sources + * the {@code Iterable} of {@code ObservableSource}s + * @param maxConcurrency + * the maximum number of {@code ObservableSource}s that may be subscribed to concurrently + * @param bufferSize + * the number of items expected from each inner {@code ObservableSource} to be buffered + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code sources} is {@code null} + * @throws IllegalArgumentException + * if {@code maxConcurrency} or {@code bufferSize} is non-positive + * @see <a href="http://reactivex.io/documentation/operators/merge.html">ReactiveX operators documentation: Merge</a> + * @see #mergeDelayError(Iterable, int, int) + */ + @SuppressWarnings({ "unchecked", "rawtypes" }) + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public static <@NonNull T> Observable<T> merge(@NonNull Iterable<@NonNull ? extends ObservableSource<? extends T>> sources, int maxConcurrency, int bufferSize) { + return fromIterable(sources).flatMap((Function)Functions.identity(), false, maxConcurrency, bufferSize); + } + + /** + * Flattens an array of {@link ObservableSource}s into one {@code Observable}, without any transformation, while limiting the + * number of concurrent subscriptions to these {@code ObservableSource}s. + * <p> + * <img width="640" height="380" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/merge.v3.png" alt=""> + * <p> + * You can combine the items emitted by multiple {@code ObservableSource}s so that they appear as a single {@code ObservableSource}, by + * using the {@code merge} method. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code mergeArray} does not operate by default on a particular {@link Scheduler}.</dd> + * <dt><b>Error handling:</b></dt> + * <dd>If any of the {@code ObservableSource}s signal a {@link Throwable} via {@code onError}, the resulting + * {@code Observable} terminates with that {@code Throwable} and all other source {@code ObservableSource}s are disposed. + * If more than one {@code ObservableSource} signals an error, the resulting {@code Observable} may terminate with the + * first one's error or, depending on the concurrency of the sources, may terminate with a + * {@link CompositeException} containing two or more of the various error signals. + * {@code Throwable}s that didn't make into the composite will be sent (individually) to the global error handler via + * {@link RxJavaPlugins#onError(Throwable)} method as {@link UndeliverableException} errors. Similarly, {@code Throwable}s + * signaled by source(s) after the returned {@code Observable} has been disposed or terminated with a + * (composite) error will be sent to the same global error handler. + * Use {@link #mergeArrayDelayError(int, int, ObservableSource...)} to merge sources and terminate only when all source {@code ObservableSource}s + * have completed or failed with an error. + * </dd> + * </dl> + * + * @param <T> the common element base type + * @param sources + * the array of {@code ObservableSource}s + * @param maxConcurrency + * the maximum number of {@code ObservableSource}s that may be subscribed to concurrently + * @param bufferSize + * the number of items expected from each inner {@code ObservableSource} to be buffered + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code sources} is {@code null} + * @throws IllegalArgumentException + * if {@code maxConcurrency} or {@code bufferSize} is non-positive + * @see <a href="http://reactivex.io/documentation/operators/merge.html">ReactiveX operators documentation: Merge</a> + * @see #mergeArrayDelayError(int, int, ObservableSource...) + */ + @SuppressWarnings({ "unchecked", "rawtypes" }) + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + @SafeVarargs + public static <@NonNull T> Observable<T> mergeArray(int maxConcurrency, int bufferSize, @NonNull ObservableSource<? extends T>... sources) { + return fromArray(sources).flatMap((Function)Functions.identity(), false, maxConcurrency, bufferSize); + } + + /** + * Flattens an {@link Iterable} of {@link ObservableSource}s into one {@code Observable}, without any transformation. + * <p> + * <img width="640" height="380" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/merge.v3.png" alt=""> + * <p> + * You can combine the items emitted by multiple {@code ObservableSource}s so that they appear as a single {@code ObservableSource}, by + * using the {@code merge} method. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code merge} does not operate by default on a particular {@link Scheduler}.</dd> + * <dt><b>Error handling:</b></dt> + * <dd>If any of the returned {@code ObservableSource}s signal a {@link Throwable} via {@code onError}, the resulting + * {@code Observable} terminates with that {@code Throwable} and all other source {@code ObservableSource}s are disposed. + * If more than one {@code ObservableSource} signals an error, the resulting {@code Observable} may terminate with the + * first one's error or, depending on the concurrency of the sources, may terminate with a + * {@link CompositeException} containing two or more of the various error signals. + * {@code Throwable}s that didn't make into the composite will be sent (individually) to the global error handler via + * {@link RxJavaPlugins#onError(Throwable)} method as {@link UndeliverableException} errors. Similarly, {@code Throwable}s + * signaled by source(s) after the returned {@code Observable} has been disposed or terminated with a + * (composite) error will be sent to the same global error handler. + * Use {@link #mergeDelayError(Iterable)} to merge sources and terminate only when all source {@code ObservableSource}s + * have completed or failed with an error. + * </dd> + * </dl> + * + * @param <T> the common element base type + * @param sources + * the {@code Iterable} of {@code ObservableSource}s + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code sources} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/merge.html">ReactiveX operators documentation: Merge</a> + * @see #mergeDelayError(Iterable) + */ + @SuppressWarnings({ "unchecked", "rawtypes" }) + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public static <@NonNull T> Observable<T> merge(@NonNull Iterable<@NonNull ? extends ObservableSource<? extends T>> sources) { + return fromIterable(sources).flatMap((Function)Functions.identity()); + } + + /** + * Flattens an {@link Iterable} of {@link ObservableSource}s into one {@code Observable}, without any transformation, while limiting the + * number of concurrent subscriptions to these {@code ObservableSource}s. + * <p> + * <img width="640" height="380" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/merge.v3.png" alt=""> + * <p> + * You can combine the items emitted by multiple {@code ObservableSource}s so that they appear as a single {@code ObservableSource}, by + * using the {@code merge} method. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code merge} does not operate by default on a particular {@link Scheduler}.</dd> + * <dt><b>Error handling:</b></dt> + * <dd>If any of the returned {@code ObservableSource}s signal a {@link Throwable} via {@code onError}, the resulting + * {@code Observable} terminates with that {@code Throwable} and all other source {@code ObservableSource}s are disposed. + * If more than one {@code ObservableSource} signals an error, the resulting {@code Observable} may terminate with the + * first one's error or, depending on the concurrency of the sources, may terminate with a + * {@link CompositeException} containing two or more of the various error signals. + * {@code Throwable}s that didn't make into the composite will be sent (individually) to the global error handler via + * {@link RxJavaPlugins#onError(Throwable)} method as {@link UndeliverableException} errors. Similarly, {@code Throwable}s + * signaled by source(s) after the returned {@code Observable} has been disposed or terminated with a + * (composite) error will be sent to the same global error handler. + * Use {@link #mergeDelayError(Iterable, int)} to merge sources and terminate only when all source {@code ObservableSource}s + * have completed or failed with an error. + * </dd> + * </dl> + * + * @param <T> the common element base type + * @param sources + * the {@code Iterable} of {@code ObservableSource}s + * @param maxConcurrency + * the maximum number of {@code ObservableSource}s that may be subscribed to concurrently + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code sources} is {@code null} + * @throws IllegalArgumentException + * if {@code maxConcurrency} is less than or equal to 0 + * @see <a href="http://reactivex.io/documentation/operators/merge.html">ReactiveX operators documentation: Merge</a> + * @see #mergeDelayError(Iterable, int) + */ + @SuppressWarnings({ "unchecked", "rawtypes" }) + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public static <@NonNull T> Observable<T> merge(@NonNull Iterable<@NonNull ? extends ObservableSource<? extends T>> sources, int maxConcurrency) { + return fromIterable(sources).flatMap((Function)Functions.identity(), maxConcurrency); + } + + /** + * Flattens an {@link ObservableSource} that emits {@code ObservableSource}s into a single {@code Observable} that emits the items emitted by + * those {@code ObservableSource}s, without any transformation. + * <p> + * <img width="640" height="370" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/merge.oo.v3.png" alt=""> + * <p> + * You can combine the items emitted by multiple {@code ObservableSource}s so that they appear as a single {@code ObservableSource}, by + * using the {@code merge} method. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code merge} does not operate by default on a particular {@link Scheduler}.</dd> + * <dt><b>Error handling:</b></dt> + * <dd>If any of the returned {@code ObservableSource}s signal a {@link Throwable} via {@code onError}, the resulting + * {@code Observable} terminates with that {@code Throwable} and all other source {@code ObservableSource}s are disposed. + * If more than one {@code ObservableSource} signals an error, the resulting {@code Observable} may terminate with the + * first one's error or, depending on the concurrency of the sources, may terminate with a + * {@link CompositeException} containing two or more of the various error signals. + * {@code Throwable}s that didn't make into the composite will be sent (individually) to the global error handler via + * {@link RxJavaPlugins#onError(Throwable)} method as {@link UndeliverableException} errors. Similarly, {@code Throwable}s + * signaled by source(s) after the returned {@code Observable} has been disposed or terminated with a + * (composite) error will be sent to the same global error handler. + * Use {@link #mergeDelayError(ObservableSource)} to merge sources and terminate only when all source {@code ObservableSource}s + * have completed or failed with an error. + * </dd> + * </dl> + * + * @param <T> the common element base type + * @param sources + * an {@code ObservableSource} that emits {@code ObservableSource}s + * @return the new {@code Observable} instance + * @see <a href="http://reactivex.io/documentation/operators/merge.html">ReactiveX operators documentation: Merge</a> + * @throws NullPointerException if {@code sources} is {@code null} + * @see #mergeDelayError(ObservableSource) + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @SuppressWarnings({ "unchecked", "rawtypes" }) + @NonNull + public static <@NonNull T> Observable<T> merge(@NonNull ObservableSource<? extends ObservableSource<? extends T>> sources) { + Objects.requireNonNull(sources, "sources is null"); + return RxJavaPlugins.onAssembly(new ObservableFlatMap(sources, Functions.identity(), false, Integer.MAX_VALUE, bufferSize())); + } + + /** + * Flattens an {@link ObservableSource} that emits {@code ObservableSource}s into a single {@code Observable} that emits the items emitted by + * those {@code ObservableSource}s, without any transformation, while limiting the maximum number of concurrent + * subscriptions to these {@code ObservableSource}s. + * <p> + * <img width="640" height="370" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/merge.oo.v3.png" alt=""> + * <p> + * You can combine the items emitted by multiple {@code ObservableSource}s so that they appear as a single {@code ObservableSource}, by + * using the {@code merge} method. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code merge} does not operate by default on a particular {@link Scheduler}.</dd> + * <dt><b>Error handling:</b></dt> + * <dd>If any of the returned {@code ObservableSource}s signal a {@link Throwable} via {@code onError}, the resulting + * {@code Observable} terminates with that {@code Throwable} and all other source {@code ObservableSource}s are disposed. + * If more than one {@code ObservableSource} signals an error, the resulting {@code Observable} may terminate with the + * first one's error or, depending on the concurrency of the sources, may terminate with a + * {@link CompositeException} containing two or more of the various error signals. + * {@code Throwable}s that didn't make into the composite will be sent (individually) to the global error handler via + * {@link RxJavaPlugins#onError(Throwable)} method as {@link UndeliverableException} errors. Similarly, {@code Throwable}s + * signaled by source(s) after the returned {@code Observable} has been disposed or terminated with a + * (composite) error will be sent to the same global error handler. + * Use {@link #mergeDelayError(ObservableSource, int)} to merge sources and terminate only when all source {@code ObservableSource}s + * have completed or failed with an error. + * </dd> + * </dl> + * + * @param <T> the common element base type + * @param sources + * an {@code ObservableSource} that emits {@code ObservableSource}s + * @param maxConcurrency + * the maximum number of {@code ObservableSource}s that may be subscribed to concurrently + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code sources} is {@code null} + * @throws IllegalArgumentException + * if {@code maxConcurrency} is non-positive + * @see <a href="http://reactivex.io/documentation/operators/merge.html">ReactiveX operators documentation: Merge</a> + * @since 1.1.0 + * @see #mergeDelayError(ObservableSource, int) + */ + @SuppressWarnings({ "unchecked", "rawtypes" }) + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public static <@NonNull T> Observable<T> merge(@NonNull ObservableSource<? extends ObservableSource<? extends T>> sources, int maxConcurrency) { + Objects.requireNonNull(sources, "sources is null"); + ObjectHelper.verifyPositive(maxConcurrency, "maxConcurrency"); + return RxJavaPlugins.onAssembly(new ObservableFlatMap(sources, Functions.identity(), false, maxConcurrency, bufferSize())); + } + + /** + * Flattens two {@link ObservableSource}s into a single {@code Observable}, without any transformation. + * <p> + * <img width="640" height="380" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/merge.v3.png" alt=""> + * <p> + * You can combine items emitted by multiple {@code ObservableSource}s so that they appear as a single {@code ObservableSource}, by + * using the {@code merge} method. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code merge} does not operate by default on a particular {@link Scheduler}.</dd> + * <dt><b>Error handling:</b></dt> + * <dd>If any of the {@code ObservableSource}s signal a {@link Throwable} via {@code onError}, the resulting + * {@code Observable} terminates with that {@code Throwable} and all other source {@code ObservableSource}s are disposed. + * If more than one {@code ObservableSource} signals an error, the resulting {@code Observable} may terminate with the + * first one's error or, depending on the concurrency of the sources, may terminate with a + * {@link CompositeException} containing two or more of the various error signals. + * {@code Throwable}s that didn't make into the composite will be sent (individually) to the global error handler via + * {@link RxJavaPlugins#onError(Throwable)} method as {@link UndeliverableException} errors. Similarly, {@code Throwable}s + * signaled by source(s) after the returned {@code Observable} has been disposed or terminated with a + * (composite) error will be sent to the same global error handler. + * Use {@link #mergeDelayError(ObservableSource, ObservableSource)} to merge sources and terminate only when all source {@code ObservableSource}s + * have completed or failed with an error. + * </dd> + * </dl> + * + * @param <T> the common element base type + * @param source1 + * an {@code ObservableSource} to be merged + * @param source2 + * an {@code ObservableSource} to be merged + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code source1} or {@code source2} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/merge.html">ReactiveX operators documentation: Merge</a> + * @see #mergeDelayError(ObservableSource, ObservableSource) + */ + @SuppressWarnings({ "unchecked", "rawtypes" }) + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public static <@NonNull T> Observable<T> merge(@NonNull ObservableSource<? extends T> source1, @NonNull ObservableSource<? extends T> source2) { + Objects.requireNonNull(source1, "source1 is null"); + Objects.requireNonNull(source2, "source2 is null"); + return fromArray(source1, source2).flatMap((Function)Functions.identity(), false, 2); + } + + /** + * Flattens three {@link ObservableSource}s into a single {@code Observable}, without any transformation. + * <p> + * <img width="640" height="380" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/merge.v3.png" alt=""> + * <p> + * You can combine items emitted by multiple {@code ObservableSource}s so that they appear as a single {@code ObservableSource}, by + * using the {@code merge} method. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code merge} does not operate by default on a particular {@link Scheduler}.</dd> + * <dt><b>Error handling:</b></dt> + * <dd>If any of the {@code ObservableSource}s signal a {@link Throwable} via {@code onError}, the resulting + * {@code Observable} terminates with that {@code Throwable} and all other source {@code ObservableSource}s are disposed. + * If more than one {@code ObservableSource} signals an error, the resulting {@code Observable} may terminate with the + * first one's error or, depending on the concurrency of the sources, may terminate with a + * {@link CompositeException} containing two or more of the various error signals. + * {@code Throwable}s that didn't make into the composite will be sent (individually) to the global error handler via + * {@link RxJavaPlugins#onError(Throwable)} method as {@link UndeliverableException} errors. Similarly, {@code Throwable}s + * signaled by source(s) after the returned {@code Observable} has been disposed or terminated with a + * (composite) error will be sent to the same global error handler. + * Use {@link #mergeDelayError(ObservableSource, ObservableSource, ObservableSource)} to merge sources and terminate only when all source {@code ObservableSource}s + * have completed or failed with an error. + * </dd> + * </dl> + * + * @param <T> the common element base type + * @param source1 + * an {@code ObservableSource} to be merged + * @param source2 + * an {@code ObservableSource} to be merged + * @param source3 + * an {@code ObservableSource} to be merged + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code source1}, {@code source2} or {@code source3} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/merge.html">ReactiveX operators documentation: Merge</a> + * @see #mergeDelayError(ObservableSource, ObservableSource, ObservableSource) + */ + @SuppressWarnings({ "unchecked", "rawtypes" }) + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public static <@NonNull T> Observable<T> merge( + @NonNull ObservableSource<? extends T> source1, @NonNull ObservableSource<? extends T> source2, + @NonNull ObservableSource<? extends T> source3) { + Objects.requireNonNull(source1, "source1 is null"); + Objects.requireNonNull(source2, "source2 is null"); + Objects.requireNonNull(source3, "source3 is null"); + return fromArray(source1, source2, source3).flatMap((Function)Functions.identity(), false, 3); + } + + /** + * Flattens four {@link ObservableSource}s into a single {@code Observable}, without any transformation. + * <p> + * <img width="640" height="380" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/merge.v3.png" alt=""> + * <p> + * You can combine items emitted by multiple {@code ObservableSource}s so that they appear as a single {@code ObservableSource}, by + * using the {@code merge} method. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code merge} does not operate by default on a particular {@link Scheduler}.</dd> + * <dt><b>Error handling:</b></dt> + * <dd>If any of the {@code ObservableSource}s signal a {@link Throwable} via {@code onError}, the resulting + * {@code Observable} terminates with that {@code Throwable} and all other source {@code ObservableSource}s are disposed. + * If more than one {@code ObservableSource} signals an error, the resulting {@code Observable} may terminate with the + * first one's error or, depending on the concurrency of the sources, may terminate with a + * {@link CompositeException} containing two or more of the various error signals. + * {@code Throwable}s that didn't make into the composite will be sent (individually) to the global error handler via + * {@link RxJavaPlugins#onError(Throwable)} method as {@link UndeliverableException} errors. Similarly, {@code Throwable}s + * signaled by source(s) after the returned {@code Observable} has been disposed or terminated with a + * (composite) error will be sent to the same global error handler. + * Use {@link #mergeDelayError(ObservableSource, ObservableSource, ObservableSource, ObservableSource)} to merge sources and terminate only when all source {@code ObservableSource}s + * have completed or failed with an error. + * </dd> + * </dl> + * + * @param <T> the common element base type + * @param source1 + * an {@code ObservableSource} to be merged + * @param source2 + * an {@code ObservableSource} to be merged + * @param source3 + * an {@code ObservableSource} to be merged + * @param source4 + * an {@code ObservableSource} to be merged + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code source1}, {@code source2}, {@code source3} or {@code source4} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/merge.html">ReactiveX operators documentation: Merge</a> + * @see #mergeDelayError(ObservableSource, ObservableSource, ObservableSource, ObservableSource) + */ + @SuppressWarnings({ "unchecked", "rawtypes" }) + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public static <@NonNull T> Observable<T> merge( + @NonNull ObservableSource<? extends T> source1, @NonNull ObservableSource<? extends T> source2, + @NonNull ObservableSource<? extends T> source3, @NonNull ObservableSource<? extends T> source4) { + Objects.requireNonNull(source1, "source1 is null"); + Objects.requireNonNull(source2, "source2 is null"); + Objects.requireNonNull(source3, "source3 is null"); + Objects.requireNonNull(source4, "source4 is null"); + return fromArray(source1, source2, source3, source4).flatMap((Function)Functions.identity(), false, 4); + } + + /** + * Flattens an array of {@link ObservableSource}s into one {@code Observable}, without any transformation. + * <p> + * <img width="640" height="370" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/merge.io.v3.png" alt=""> + * <p> + * You can combine items emitted by multiple {@code ObservableSource}s so that they appear as a single {@code ObservableSource}, by + * using the {@code merge} method. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code mergeArray} does not operate by default on a particular {@link Scheduler}.</dd> + * <dt><b>Error handling:</b></dt> + * <dd>If any of the {@code ObservableSource}s signal a {@link Throwable} via {@code onError}, the resulting + * {@code Observable} terminates with that {@code Throwable} and all other source {@code ObservableSource}s are disposed. + * If more than one {@code ObservableSource} signals an error, the resulting {@code Observable} may terminate with the + * first one's error or, depending on the concurrency of the sources, may terminate with a + * {@link CompositeException} containing two or more of the various error signals. + * {@code Throwable}s that didn't make into the composite will be sent (individually) to the global error handler via + * {@link RxJavaPlugins#onError(Throwable)} method as {@link UndeliverableException} errors. Similarly, {@code Throwable}s + * signaled by source(s) after the returned {@code Observable} has been disposed or terminated with a + * (composite) error will be sent to the same global error handler. + * Use {@link #mergeArrayDelayError(ObservableSource...)} to merge sources and terminate only when all source {@code ObservableSource}s + * have completed or failed with an error. + * </dd> + * </dl> + * + * @param <T> the common element base type + * @param sources + * the array of {@code ObservableSource}s + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code sources} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/merge.html">ReactiveX operators documentation: Merge</a> + * @see #mergeArrayDelayError(ObservableSource...) + */ + @SuppressWarnings({ "unchecked", "rawtypes" }) + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + @SafeVarargs + public static <@NonNull T> Observable<T> mergeArray(@NonNull ObservableSource<? extends T>... sources) { + return fromArray(sources).flatMap((Function)Functions.identity(), sources.length); + } + + /** + * Flattens an {@link Iterable} of {@link ObservableSource}s into one {@code Observable}, in a way that allows an {@link Observer} to receive all + * successfully emitted items from each of the returned {@code ObservableSource}s without being interrupted by an error + * notification from one of them. + * <p> + * This behaves like {@link #merge(ObservableSource)} except that if any of the merged {@code ObservableSource}s notify of an + * error via {@link Observer#onError onError}, {@code mergeDelayError} will refrain from propagating that + * error notification until all of the merged {@code ObservableSource}s have finished emitting items. + * <p> + * <img width="640" height="380" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/mergeDelayError.v3.png" alt=""> + * <p> + * Even if multiple merged {@code ObservableSource}s send {@code onError} notifications, {@code mergeDelayError} will only + * invoke the {@code onError} method of its {@code Observer}s once. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code mergeDelayError} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T> the common element base type + * @param sources + * the {@code Iterable} of {@code ObservableSource}s + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code sources} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/merge.html">ReactiveX operators documentation: Merge</a> + */ + @SuppressWarnings({ "unchecked", "rawtypes" }) + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public static <@NonNull T> Observable<T> mergeDelayError(@NonNull Iterable<@NonNull ? extends ObservableSource<? extends T>> sources) { + return fromIterable(sources).flatMap((Function)Functions.identity(), true); + } + + /** + * Flattens an {@link Iterable} of {@link ObservableSource}s into one {@code Observable}, in a way that allows an {@link Observer} to receive all + * successfully emitted items from each of the returned {@code ObservableSource}s without being interrupted by an error + * notification from one of them, while limiting the number of concurrent subscriptions to these {@code ObservableSource}s. + * <p> + * This behaves like {@link #merge(ObservableSource)} except that if any of the merged {@code ObservableSource}s notify of an + * error via {@link Observer#onError onError}, {@code mergeDelayError} will refrain from propagating that + * error notification until all of the merged {@code ObservableSource}s have finished emitting items. + * <p> + * <img width="640" height="380" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/mergeDelayError.v3.png" alt=""> + * <p> + * Even if multiple merged {@code ObservableSource}s send {@code onError} notifications, {@code mergeDelayError} will only + * invoke the {@code onError} method of its {@code Observer}s once. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code mergeDelayError} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T> the common element base type + * @param sources + * the {@code Iterable} of {@code ObservableSource}s + * @param maxConcurrency + * the maximum number of {@code ObservableSource}s that may be subscribed to concurrently + * @param bufferSize + * the number of items expected from each inner {@code ObservableSource} to be buffered + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code sources} is {@code null} + * @throws IllegalArgumentException if {@code maxConcurrency} or {@code bufferSize} is non-positive + * @see <a href="http://reactivex.io/documentation/operators/merge.html">ReactiveX operators documentation: Merge</a> + */ + @SuppressWarnings({ "unchecked", "rawtypes" }) + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public static <@NonNull T> Observable<T> mergeDelayError(@NonNull Iterable<@NonNull ? extends ObservableSource<? extends T>> sources, int maxConcurrency, int bufferSize) { + return fromIterable(sources).flatMap((Function)Functions.identity(), true, maxConcurrency, bufferSize); + } + + /** + * Flattens an array of {@link ObservableSource}s into one {@code Observable}, in a way that allows an {@link Observer} to receive all + * successfully emitted items from each of the {@code ObservableSource}s without being interrupted by an error + * notification from one of them, while limiting the number of concurrent subscriptions to these {@code ObservableSource}s. + * <p> + * This behaves like {@link #merge(ObservableSource)} except that if any of the merged {@code ObservableSource}s notify of an + * error via {@link Observer#onError onError}, {@code mergeDelayError} will refrain from propagating that + * error notification until all of the merged {@code ObservableSource}s have finished emitting items. + * <p> + * <img width="640" height="380" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/mergeDelayError.v3.png" alt=""> + * <p> + * Even if multiple merged {@code ObservableSource}s send {@code onError} notifications, {@code mergeDelayError} will only + * invoke the {@code onError} method of its {@code Observer}s once. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code mergeArrayDelayError} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T> the common element base type + * @param sources + * the array of {@code ObservableSource}s + * @param maxConcurrency + * the maximum number of {@code ObservableSource}s that may be subscribed to concurrently + * @param bufferSize + * the number of items expected from each inner {@code ObservableSource} to be buffered + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code sources} is {@code null} + * @throws IllegalArgumentException if {@code maxConcurrency} or {@code bufferSize} is non-positive + * @see <a href="http://reactivex.io/documentation/operators/merge.html">ReactiveX operators documentation: Merge</a> + */ + @SuppressWarnings({ "unchecked", "rawtypes" }) + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + @SafeVarargs + public static <@NonNull T> Observable<T> mergeArrayDelayError(int maxConcurrency, int bufferSize, @NonNull ObservableSource<? extends T>... sources) { + return fromArray(sources).flatMap((Function)Functions.identity(), true, maxConcurrency, bufferSize); + } + + /** + * Flattens an {@link Iterable} of {@link ObservableSource}s into one {@code Observable}, in a way that allows an {@link Observer} to receive all + * successfully emitted items from each of the returned {@code ObservableSource}s without being interrupted by an error + * notification from one of them, while limiting the number of concurrent subscriptions to these {@code ObservableSource}s. + * <p> + * This behaves like {@link #merge(ObservableSource)} except that if any of the merged {@code ObservableSource}s notify of an + * error via {@link Observer#onError onError}, {@code mergeDelayError} will refrain from propagating that + * error notification until all of the merged {@code ObservableSource}s have finished emitting items. + * <p> + * <img width="640" height="380" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/mergeDelayError.v3.png" alt=""> + * <p> + * Even if multiple merged {@code ObservableSource}s send {@code onError} notifications, {@code mergeDelayError} will only + * invoke the {@code onError} method of its {@code Observer}s once. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code mergeDelayError} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T> the common element base type + * @param sources + * the {@code Iterable} of {@code ObservableSource}s + * @param maxConcurrency + * the maximum number of {@code ObservableSource}s that may be subscribed to concurrently + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code sources} is {@code null} + * @throws IllegalArgumentException if {@code maxConcurrency} is non-positive + * @see <a href="http://reactivex.io/documentation/operators/merge.html">ReactiveX operators documentation: Merge</a> + */ + @SuppressWarnings({ "unchecked", "rawtypes" }) + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public static <@NonNull T> Observable<T> mergeDelayError(@NonNull Iterable<@NonNull ? extends ObservableSource<? extends T>> sources, int maxConcurrency) { + return fromIterable(sources).flatMap((Function)Functions.identity(), true, maxConcurrency); + } + + /** + * Flattens an {@link ObservableSource} that emits {@code ObservableSource}s into one {@code Observable}, in a way that allows an {@link Observer} to + * receive all successfully emitted items from all of the emitted {@code ObservableSource}s without being interrupted by + * an error notification from one of them. + * <p> + * This behaves like {@link #merge(ObservableSource)} except that if any of the merged {@code ObservableSource}s notify of an + * error via {@link Observer#onError onError}, {@code mergeDelayError} will refrain from propagating that + * error notification until all of the merged {@code ObservableSource}s have finished emitting items. + * <p> + * <img width="640" height="380" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/mergeDelayError.v3.png" alt=""> + * <p> + * Even if multiple merged {@code ObservableSource}s send {@code onError} notifications, {@code mergeDelayError} will only + * invoke the {@code onError} method of its {@code Observer}s once. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code mergeDelayError} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T> the common element base type + * @param sources + * an {@code ObservableSource} that emits {@code ObservableSource}s + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code sources} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/merge.html">ReactiveX operators documentation: Merge</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @SuppressWarnings({ "unchecked", "rawtypes" }) + @NonNull + public static <@NonNull T> Observable<T> mergeDelayError(@NonNull ObservableSource<? extends ObservableSource<? extends T>> sources) { + Objects.requireNonNull(sources, "sources is null"); + return RxJavaPlugins.onAssembly(new ObservableFlatMap(sources, Functions.identity(), true, Integer.MAX_VALUE, bufferSize())); + } + + /** + * Flattens an {@link ObservableSource} that emits {@code ObservableSource}s into one {@code Observable}, in a way that allows an {@link Observer} to + * receive all successfully emitted items from all of the emitted {@code ObservableSource}s without being interrupted by + * an error notification from one of them, while limiting the + * number of concurrent subscriptions to these {@code ObservableSource}s. + * <p> + * This behaves like {@link #merge(ObservableSource)} except that if any of the merged {@code ObservableSource}s notify of an + * error via {@link Observer#onError onError}, {@code mergeDelayError} will refrain from propagating that + * error notification until all of the merged {@code ObservableSource}s have finished emitting items. + * <p> + * <img width="640" height="380" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/mergeDelayError.v3.png" alt=""> + * <p> + * Even if multiple merged {@code ObservableSource}s send {@code onError} notifications, {@code mergeDelayError} will only + * invoke the {@code onError} method of its {@code Observer}s once. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code mergeDelayError} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T> the common element base type + * @param sources + * an {@code ObservableSource} that emits {@code ObservableSource}s + * @param maxConcurrency + * the maximum number of {@code ObservableSource}s that may be subscribed to concurrently + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code sources} is {@code null} + * @throws IllegalArgumentException if {@code maxConcurrency} is non-positive + * @see <a href="http://reactivex.io/documentation/operators/merge.html">ReactiveX operators documentation: Merge</a> + * @since 2.0 + */ + @SuppressWarnings({ "unchecked", "rawtypes" }) + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public static <@NonNull T> Observable<T> mergeDelayError(@NonNull ObservableSource<? extends ObservableSource<? extends T>> sources, int maxConcurrency) { + Objects.requireNonNull(sources, "sources is null"); + ObjectHelper.verifyPositive(maxConcurrency, "maxConcurrency"); + return RxJavaPlugins.onAssembly(new ObservableFlatMap(sources, Functions.identity(), true, maxConcurrency, bufferSize())); + } + + /** + * Flattens two {@link ObservableSource}s into one {@code Observable}, in a way that allows an {@link Observer} to receive all + * successfully emitted items from each of the {@code ObservableSource}s without being interrupted by an error + * notification from one of them. + * <p> + * This behaves like {@link #merge(ObservableSource, ObservableSource)} except that if any of the merged {@code ObservableSource}s + * notify of an error via {@link Observer#onError onError}, {@code mergeDelayError} will refrain from + * propagating that error notification until all of the merged {@code ObservableSource}s have finished emitting items. + * <p> + * <img width="640" height="380" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/mergeDelayError.v3.png" alt=""> + * <p> + * Even if both merged {@code ObservableSource}s send {@code onError} notifications, {@code mergeDelayError} will only + * invoke the {@code onError} method of its {@code Observer}s once. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code mergeDelayError} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T> the common element base type + * @param source1 + * an {@code ObservableSource} to be merged + * @param source2 + * an {@code ObservableSource} to be merged + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code source1} or {@code source2} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/merge.html">ReactiveX operators documentation: Merge</a> + */ + @SuppressWarnings({ "unchecked", "rawtypes" }) + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public static <@NonNull T> Observable<T> mergeDelayError( + @NonNull ObservableSource<? extends T> source1, @NonNull ObservableSource<? extends T> source2) { + Objects.requireNonNull(source1, "source1 is null"); + Objects.requireNonNull(source2, "source2 is null"); + return fromArray(source1, source2).flatMap((Function)Functions.identity(), true, 2); + } + + /** + * Flattens three {@link ObservableSource}s into one {@code Observable}, in a way that allows an {@link Observer} to receive all + * successfully emitted items from all of the {@code ObservableSource}s without being interrupted by an error + * notification from one of them. + * <p> + * This behaves like {@link #merge(ObservableSource, ObservableSource, ObservableSource)} except that if any of the merged + * {@code ObservableSource}s notify of an error via {@link Observer#onError onError}, {@code mergeDelayError} will refrain + * from propagating that error notification until all of the merged {@code ObservableSource}s have finished emitting + * items. + * <p> + * <img width="640" height="380" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/mergeDelayError.v3.png" alt=""> + * <p> + * Even if multiple merged {@code ObservableSource}s send {@code onError} notifications, {@code mergeDelayError} will only + * invoke the {@code onError} method of its {@code Observer}s once. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code mergeDelayError} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T> the common element base type + * @param source1 + * an {@code ObservableSource} to be merged + * @param source2 + * an {@code ObservableSource} to be merged + * @param source3 + * an {@code ObservableSource} to be merged + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code source1}, {@code source2} or {@code source3} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/merge.html">ReactiveX operators documentation: Merge</a> + */ + @SuppressWarnings({ "unchecked", "rawtypes" }) + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public static <@NonNull T> Observable<T> mergeDelayError( + @NonNull ObservableSource<? extends T> source1, @NonNull ObservableSource<? extends T> source2, + @NonNull ObservableSource<? extends T> source3) { + Objects.requireNonNull(source1, "source1 is null"); + Objects.requireNonNull(source2, "source2 is null"); + Objects.requireNonNull(source3, "source3 is null"); + return fromArray(source1, source2, source3).flatMap((Function)Functions.identity(), true, 3); + } + + /** + * Flattens four {@link ObservableSource}s into one {@code Observable}, in a way that allows an {@link Observer} to receive all + * successfully emitted items from all of the {@code ObservableSource}s without being interrupted by an error + * notification from one of them. + * <p> + * This behaves like {@link #merge(ObservableSource, ObservableSource, ObservableSource, ObservableSource)} except that if any of + * the merged {@code ObservableSource}s notify of an error via {@link Observer#onError onError}, {@code mergeDelayError} + * will refrain from propagating that error notification until all of the merged {@code ObservableSource}s have finished + * emitting items. + * <p> + * <img width="640" height="380" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/mergeDelayError.v3.png" alt=""> + * <p> + * Even if multiple merged {@code ObservableSource}s send {@code onError} notifications, {@code mergeDelayError} will only + * invoke the {@code onError} method of its {@code Observer}s once. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code mergeDelayError} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T> the common element base type + * @param source1 + * an {@code ObservableSource} to be merged + * @param source2 + * an {@code ObservableSource} to be merged + * @param source3 + * an {@code ObservableSource} to be merged + * @param source4 + * an {@code ObservableSource} to be merged + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code source1}, {@code source2}, {@code source3} or {@code source4} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/merge.html">ReactiveX operators documentation: Merge</a> + */ + @SuppressWarnings({ "unchecked", "rawtypes" }) + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public static <@NonNull T> Observable<T> mergeDelayError( + @NonNull ObservableSource<? extends T> source1, @NonNull ObservableSource<? extends T> source2, + @NonNull ObservableSource<? extends T> source3, @NonNull ObservableSource<? extends T> source4) { + Objects.requireNonNull(source1, "source1 is null"); + Objects.requireNonNull(source2, "source2 is null"); + Objects.requireNonNull(source3, "source3 is null"); + Objects.requireNonNull(source4, "source4 is null"); + return fromArray(source1, source2, source3, source4).flatMap((Function)Functions.identity(), true, 4); + } + + /** + * Flattens an array of {@link ObservableSource}s into one {@code Observable}, in a way that allows an {@link Observer} to receive all + * successfully emitted items from each of the {@code ObservableSource}s without being interrupted by an error + * notification from one of them. + * <p> + * This behaves like {@link #merge(ObservableSource)} except that if any of the merged {@code ObservableSource}s notify of an + * error via {@link Observer#onError onError}, {@code mergeDelayError} will refrain from propagating that + * error notification until all of the merged {@code ObservableSource}s have finished emitting items. + * <p> + * <img width="640" height="380" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/mergeDelayError.v3.png" alt=""> + * <p> + * Even if multiple merged {@code ObservableSource}s send {@code onError} notifications, {@code mergeDelayError} will only + * invoke the {@code onError} method of its {@code Observer}s once. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code mergeArrayDelayError} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T> the common element base type + * @param sources + * the array of {@code ObservableSource}s + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code sources} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/merge.html">ReactiveX operators documentation: Merge</a> + */ + @SuppressWarnings({ "unchecked", "rawtypes" }) + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + @SafeVarargs + public static <@NonNull T> Observable<T> mergeArrayDelayError(@NonNull ObservableSource<? extends T>... sources) { + return fromArray(sources).flatMap((Function)Functions.identity(), true, sources.length); + } + + /** + * Returns an {@code Observable} that never sends any items or notifications to an {@link Observer}. + * <p> + * <img width="640" height="185" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/never.v3.png" alt=""> + * <p> + * The returned {@code Observable} is useful primarily for testing purposes. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code never} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T> + * the type of items (not) emitted by the {@code Observable} + * @return the shared {@code Observable} instance + * @see <a href="http://reactivex.io/documentation/operators/empty-never-throw.html">ReactiveX operators documentation: Never</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @SuppressWarnings("unchecked") + @NonNull + public static <@NonNull T> Observable<T> never() { + return RxJavaPlugins.onAssembly((Observable<T>) ObservableNever.INSTANCE); + } + + /** + * Returns an {@code Observable} that emits a sequence of {@link Integer}s within a specified range. + * <p> + * <img width="640" height="195" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/range.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code range} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param start + * the value of the first {@code Integer} in the sequence + * @param count + * the number of sequential {@code Integer}s to generate + * @return the new {@code Observable} instance + * @throws IllegalArgumentException + * if {@code count} is negative, or if {@code start} + {@code count} − 1 exceeds + * {@link Integer#MAX_VALUE} + * @see <a href="http://reactivex.io/documentation/operators/range.html">ReactiveX operators documentation: Range</a> + * @see #rangeLong(long, long) + * @see #intervalRange(long, long, long, long, TimeUnit) + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public static Observable<Integer> range(int start, int count) { + if (count < 0) { + throw new IllegalArgumentException("count >= 0 required but it was " + count); + } + if (count == 0) { + return empty(); + } + if (count == 1) { + return just(start); + } + if ((long)start + (count - 1) > Integer.MAX_VALUE) { + throw new IllegalArgumentException("Integer overflow"); + } + return RxJavaPlugins.onAssembly(new ObservableRange(start, count)); + } + + /** + * Returns an {@code Observable} that emits a sequence of {@link Long}s within a specified range. + * <p> + * <img width="640" height="195" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/rangeLong.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code rangeLong} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param start + * the value of the first {@code Long} in the sequence + * @param count + * the number of sequential {@code Long}s to generate + * @return the new {@code Observable} instance + * @throws IllegalArgumentException + * if {@code count} is negative, or if {@code start} + {@code count} − 1 exceeds + * {@link Long#MAX_VALUE} + * @see <a href="http://reactivex.io/documentation/operators/range.html">ReactiveX operators documentation: Range</a> + * @see #intervalRange(long, long, long, long, TimeUnit) + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public static Observable<Long> rangeLong(long start, long count) { + if (count < 0) { + throw new IllegalArgumentException("count >= 0 required but it was " + count); + } + + if (count == 0) { + return empty(); + } + + if (count == 1) { + return just(start); + } + + long end = start + (count - 1); + if (start > 0 && end < 0) { + throw new IllegalArgumentException("Overflow! start + count is bigger than Long.MAX_VALUE"); + } + + return RxJavaPlugins.onAssembly(new ObservableRangeLong(start, count)); + } + + /** + * Returns a {@link Single} that emits a {@link Boolean} value that indicates whether two {@link ObservableSource} sequences are the + * same by comparing the items emitted by each {@code ObservableSource} pairwise. + * <p> + * <img width="640" height="380" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/sequenceEqual.2.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code sequenceEqual} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param source1 + * the first {@code ObservableSource} to compare + * @param source2 + * the second {@code ObservableSource} to compare + * @param <T> + * the type of items emitted by each {@code ObservableSource} + * @return the new {@code Single} instance + * @throws NullPointerException if {@code source1} or {@code source2} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/sequenceequal.html">ReactiveX operators documentation: SequenceEqual</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public static <@NonNull T> Single<Boolean> sequenceEqual(@NonNull ObservableSource<? extends T> source1, @NonNull ObservableSource<? extends T> source2) { + return sequenceEqual(source1, source2, ObjectHelper.equalsPredicate(), bufferSize()); + } + + /** + * Returns a {@link Single} that emits a {@link Boolean} value that indicates whether two {@link ObservableSource} sequences are the + * same by comparing the items emitted by each {@code ObservableSource} pairwise based on the results of a specified + * equality function. + * <p> + * <img width="640" height="380" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/sequenceEqual.2.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code sequenceEqual} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param source1 + * the first {@code ObservableSource} to compare + * @param source2 + * the second {@code ObservableSource} to compare + * @param isEqual + * a function used to compare items emitted by each {@code ObservableSource} + * @param <T> + * the type of items emitted by each {@code ObservableSource} + * @return the new {@code Single} instance + * @throws NullPointerException if {@code source1}, {@code source2} or {@code isEqual} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/sequenceequal.html">ReactiveX operators documentation: SequenceEqual</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public static <@NonNull T> Single<Boolean> sequenceEqual( + @NonNull ObservableSource<? extends T> source1, @NonNull ObservableSource<? extends T> source2, + @NonNull BiPredicate<? super T, ? super T> isEqual) { + return sequenceEqual(source1, source2, isEqual, bufferSize()); + } + + /** + * Returns a {@link Single} that emits a {@link Boolean} value that indicates whether two {@link ObservableSource} sequences are the + * same by comparing the items emitted by each {@code ObservableSource} pairwise based on the results of a specified + * equality function. + * <p> + * <img width="640" height="380" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/sequenceEqual.2.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code sequenceEqual} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param source1 + * the first {@code ObservableSource} to compare + * @param source2 + * the second {@code ObservableSource} to compare + * @param isEqual + * a function used to compare items emitted by each {@code ObservableSource} + * @param bufferSize + * the number of items expected from the first and second source {@code ObservableSource} to be buffered + * @param <T> + * the type of items emitted by each {@code ObservableSource} + * @return the new {@code Single} instance + * @throws NullPointerException if {@code source1}, {@code source2} or {@code isEqual} is {@code null} + * @throws IllegalArgumentException if {@code bufferSize} is non-positive + * @see <a href="http://reactivex.io/documentation/operators/sequenceequal.html">ReactiveX operators documentation: SequenceEqual</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public static <@NonNull T> Single<Boolean> sequenceEqual( + @NonNull ObservableSource<? extends T> source1, @NonNull ObservableSource<? extends T> source2, + @NonNull BiPredicate<? super T, ? super T> isEqual, int bufferSize) { + Objects.requireNonNull(source1, "source1 is null"); + Objects.requireNonNull(source2, "source2 is null"); + Objects.requireNonNull(isEqual, "isEqual is null"); + ObjectHelper.verifyPositive(bufferSize, "bufferSize"); + return RxJavaPlugins.onAssembly(new ObservableSequenceEqualSingle<>(source1, source2, isEqual, bufferSize)); + } + + /** + * Returns a {@link Single} that emits a {@link Boolean} value that indicates whether two {@link ObservableSource} sequences are the + * same by comparing the items emitted by each {@code ObservableSource} pairwise. + * <p> + * <img width="640" height="380" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/sequenceEqual.2.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code sequenceEqual} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param source1 + * the first {@code ObservableSource} to compare + * @param source2 + * the second {@code ObservableSource} to compare + * @param bufferSize + * the number of items expected from the first and second source {@code ObservableSource} to be buffered + * @param <T> + * the type of items emitted by each {@code ObservableSource} + * @return the new {@code Single} instance + * @throws NullPointerException if {@code source1} or {@code source2} is {@code null} + * @throws IllegalArgumentException if {@code bufferSize} is non-positive + * @see <a href="http://reactivex.io/documentation/operators/sequenceequal.html">ReactiveX operators documentation: SequenceEqual</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public static <@NonNull T> Single<Boolean> sequenceEqual(@NonNull ObservableSource<? extends T> source1, @NonNull ObservableSource<? extends T> source2, + int bufferSize) { + return sequenceEqual(source1, source2, ObjectHelper.equalsPredicate(), bufferSize); + } + + /** + * Converts an {@link ObservableSource} that emits {@code ObservableSource}s into an {@code Observable} that emits the items emitted by the + * most recently emitted of those {@code ObservableSource}s. + * <p> + * <img width="640" height="370" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/switchDo.v3.png" alt=""> + * <p> + * {@code switchOnNext} subscribes to an {@code ObservableSource} that emits {@code ObservableSource}s. Each time it observes one of + * these emitted {@code ObservableSource}s, the {@code ObservableSource} returned by {@code switchOnNext} begins emitting the items + * emitted by that {@code ObservableSource}. When a new inner {@code ObservableSource} is emitted, {@code switchOnNext} stops emitting items + * from the earlier-emitted {@code ObservableSource} and begins emitting items from the new one. + * <p> + * The resulting {@code Observable} completes if both the outer {@code ObservableSource} and the last inner {@code ObservableSource}, if any, complete. + * If the outer {@code ObservableSource} signals an {@code onError}, the inner {@code ObservableSource} is disposed and the error delivered in-sequence. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code switchOnNext} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T> the item type + * @param sources + * the {@code ObservableSource} that emits {@code ObservableSource}s + * @param bufferSize + * the expected number of items to cache from the inner {@code ObservableSource}s + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code sources} is {@code null} + * @throws IllegalArgumentException if {@code bufferSize} is non-positive + * @see <a href="http://reactivex.io/documentation/operators/switch.html">ReactiveX operators documentation: Switch</a> + */ + @SuppressWarnings({ "rawtypes", "unchecked" }) + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public static <@NonNull T> Observable<T> switchOnNext(@NonNull ObservableSource<? extends ObservableSource<? extends T>> sources, int bufferSize) { + Objects.requireNonNull(sources, "sources is null"); + ObjectHelper.verifyPositive(bufferSize, "bufferSize"); + return RxJavaPlugins.onAssembly(new ObservableSwitchMap(sources, Functions.identity(), bufferSize, false)); + } + + /** + * Converts an {@link ObservableSource} that emits {@code ObservableSource}s into an {@code Observable} that emits the items emitted by the + * most recently emitted of those {@code ObservableSource}s. + * <p> + * <img width="640" height="370" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/switchDo.v3.png" alt=""> + * <p> + * {@code switchOnNext} subscribes to an {@code ObservableSource} that emits {@code ObservableSource}s. Each time it observes one of + * these emitted {@code ObservableSource}s, the {@code ObservableSource} returned by {@code switchOnNext} begins emitting the items + * emitted by that {@code ObservableSource}. When a new inner {@code ObservableSource} is emitted, {@code switchOnNext} stops emitting items + * from the earlier-emitted {@code ObservableSource} and begins emitting items from the new one. + * <p> + * The resulting {@code Observable} completes if both the outer {@code ObservableSource} and the last inner {@code ObservableSource}, if any, complete. + * If the outer {@code ObservableSource} signals an {@code onError}, the inner {@code ObservableSource} is disposed and the error delivered in-sequence. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code switchOnNext} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T> the item type + * @param sources + * the {@code ObservableSource} that emits {@code ObservableSource}s + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code sources} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/switch.html">ReactiveX operators documentation: Switch</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public static <@NonNull T> Observable<T> switchOnNext(@NonNull ObservableSource<? extends ObservableSource<? extends T>> sources) { + return switchOnNext(sources, bufferSize()); + } + + /** + * Converts an {@link ObservableSource} that emits {@code ObservableSource}s into an {@code Observable} that emits the items emitted by the + * most recently emitted of those {@code ObservableSource}s and delays any exception until all {@code ObservableSource}s terminate. + * <p> + * <img width="640" height="370" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/switchOnNextDelayError.v3.png" alt=""> + * <p> + * {@code switchOnNext} subscribes to an {@code ObservableSource} that emits {@code ObservableSource}s. Each time it observes one of + * these emitted {@code ObservableSource}s, the {@code ObservableSource} returned by {@code switchOnNext} begins emitting the items + * emitted by that {@code ObservableSource}. When a new inner {@code ObservableSource} is emitted, {@code switchOnNext} stops emitting items + * from the earlier-emitted {@code ObservableSource} and begins emitting items from the new one. + * <p> + * The resulting {@code Observable} completes if both the main {@code ObservableSource} and the last inner {@code ObservableSource}, if any, complete. + * If the main {@code ObservableSource} signals an {@code onError}, the termination of the last inner {@code ObservableSource} will emit that error as is + * or wrapped into a {@link CompositeException} along with the other possible errors the former inner {@code ObservableSource}s signaled. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code switchOnNextDelayError} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T> the item type + * @param sources + * the {@code ObservableSource} that emits {@code ObservableSource}s + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code sources} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/switch.html">ReactiveX operators documentation: Switch</a> + * @since 2.0 + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public static <@NonNull T> Observable<T> switchOnNextDelayError(@NonNull ObservableSource<? extends ObservableSource<? extends T>> sources) { + return switchOnNextDelayError(sources, bufferSize()); + } + + /** + * Converts an {@link ObservableSource} that emits {@code ObservableSource}s into an {@code Observable} that emits the items emitted by the + * most recently emitted of those {@code ObservableSource}s and delays any exception until all {@code ObservableSource}s terminate. + * <p> + * <img width="640" height="370" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/switchOnNextDelayError.v3.png" alt=""> + * <p> + * {@code switchOnNext} subscribes to an {@code ObservableSource} that emits {@code ObservableSource}s. Each time it observes one of + * these emitted {@code ObservableSource}s, the {@code ObservableSource} returned by {@code switchOnNext} begins emitting the items + * emitted by that {@code ObservableSource}. When a new inner {@code ObservableSource} is emitted, {@code switchOnNext} stops emitting items + * from the earlier-emitted {@code ObservableSource} and begins emitting items from the new one. + * <p> + * The resulting {@code Observable} completes if both the main {@code ObservableSource} and the last inner {@code ObservableSource}, if any, complete. + * If the main {@code ObservableSource} signals an {@code onError}, the termination of the last inner {@code ObservableSource} will emit that error as is + * or wrapped into a {@link CompositeException} along with the other possible errors the former inner {@code ObservableSource}s signaled. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code switchOnNextDelayError} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T> the item type + * @param sources + * the {@code ObservableSource} that emits {@code ObservableSource}s + * @param bufferSize + * the expected number of items to cache from the inner {@code ObservableSource}s + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code sources} is {@code null} + * @throws IllegalArgumentException if {@code bufferSize} is non-positive + * @see <a href="http://reactivex.io/documentation/operators/switch.html">ReactiveX operators documentation: Switch</a> + * @since 2.0 + */ + @SuppressWarnings({ "rawtypes", "unchecked" }) + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public static <@NonNull T> Observable<T> switchOnNextDelayError(@NonNull ObservableSource<? extends ObservableSource<? extends T>> sources, int bufferSize) { + Objects.requireNonNull(sources, "sources is null"); + ObjectHelper.verifyPositive(bufferSize, "bufferSize"); + return RxJavaPlugins.onAssembly(new ObservableSwitchMap(sources, Functions.identity(), bufferSize, true)); + } + + /** + * Returns an {@code Observable} that emits {@code 0L} after a specified delay, and then completes. + * <p> + * <img width="640" height="200" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/timer.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code timer} operates by default on the {@code computation} {@link Scheduler}.</dd> + * </dl> + * + * @param delay + * the initial delay before emitting a single {@code 0L} + * @param unit + * time units to use for {@code delay} + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code unit} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/timer.html">ReactiveX operators documentation: Timer</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.COMPUTATION) + @NonNull + public static Observable<Long> timer(long delay, @NonNull TimeUnit unit) { + return timer(delay, unit, Schedulers.computation()); + } + + /** + * Returns an {@code Observable} that emits {@code 0L} after a specified delay, on a specified {@link Scheduler}, and then + * completes. + * <p> + * <img width="640" height="200" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/timer.s.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>You specify which {@code Scheduler} this operator will use.</dd> + * </dl> + * + * @param delay + * the initial delay before emitting a single 0L + * @param unit + * time units to use for {@code delay} + * @param scheduler + * the {@code Scheduler} to use for scheduling the item + * @throws NullPointerException + * if {@code unit} or {@code scheduler} is {@code null} + * @return the new {@code Observable} instance + * @see <a href="http://reactivex.io/documentation/operators/timer.html">ReactiveX operators documentation: Timer</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.CUSTOM) + @NonNull + public static Observable<Long> timer(long delay, @NonNull TimeUnit unit, @NonNull Scheduler scheduler) { + Objects.requireNonNull(unit, "unit is null"); + Objects.requireNonNull(scheduler, "scheduler is null"); + + return RxJavaPlugins.onAssembly(new ObservableTimer(Math.max(delay, 0L), unit, scheduler)); + } + + /** + * Create an {@code Observable} by wrapping an {@link ObservableSource} <em>which has to be implemented according + * to the {@code Observable} specification derived from the <b>Reactive Streams</b> specification by handling + * disposal correctly; no safeguards are provided by the {@code Observable} itself</em>. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code unsafeCreate} by default doesn't operate on any particular {@link Scheduler}.</dd> + * </dl> + * @param <T> the value type emitted + * @param onSubscribe the {@code ObservableSource} instance to wrap + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code onSubscribe} is {@code null} + * @throws IllegalArgumentException if the {@code onSubscribe} is already an {@code Observable}, use + * {@link #wrap(ObservableSource)} in this case + * @see #wrap(ObservableSource) + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public static <@NonNull T> Observable<T> unsafeCreate(@NonNull ObservableSource<T> onSubscribe) { + Objects.requireNonNull(onSubscribe, "onSubscribe is null"); + if (onSubscribe instanceof Observable) { + throw new IllegalArgumentException("unsafeCreate(Observable) should be upgraded"); + } + return RxJavaPlugins.onAssembly(new ObservableFromUnsafeSource<>(onSubscribe)); + } + + /** + * Constructs an {@code Observable} that creates a dependent resource object, an {@link ObservableSource} with + * that resource and calls the provided {@code resourceDisposer} function if this inner source terminates or the + * downstream disposes the flow. + * <p> + * <img width="640" height="400" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/using.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code using} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T> the element type of the generated {@code Observable} + * @param <D> the type of the resource associated with the output sequence + * @param resourceSupplier + * the factory function to create a resource object that depends on the {@code ObservableSource} + * @param sourceSupplier + * the factory function to create an {@code ObservableSource} + * @param resourceCleanup + * the function that will dispose of the resource + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code resourceSupplier}, {@code sourceSupplier} or {@code resourceCleanup} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/using.html">ReactiveX operators documentation: Using</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public static <@NonNull T, @NonNull D> Observable<T> using( + @NonNull Supplier<? extends D> resourceSupplier, + @NonNull Function<? super D, ? extends ObservableSource<? extends T>> sourceSupplier, + @NonNull Consumer<? super D> resourceCleanup) { + return using(resourceSupplier, sourceSupplier, resourceCleanup, true); + } + + /** + * Constructs an {@code Observable} that creates a dependent resource object, an {@link ObservableSource} with + * that resource and calls the provided {@code disposer} function if this inner source terminates or the + * downstream disposes the flow; doing it before these end-states have been reached if {@code eager == true}, after otherwise. + * <p> + * <img width="640" height="400" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/using.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code using} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T> the element type of the generated {@code ObservableSource} + * @param <D> the type of the resource associated with the output sequence + * @param resourceSupplier + * the factory function to create a resource object that depends on the {@code ObservableSource} + * @param sourceSupplier + * the factory function to create an {@code ObservableSource} + * @param resourceCleanup + * the function that will dispose of the resource + * @param eager + * If {@code true}, the resource disposal will happen either on a {@code dispose()} call before the upstream is disposed + * or just before the emission of a terminal event ({@code onComplete} or {@code onError}). + * If {@code false}, the resource disposal will happen either on a {@code dispose()} call after the upstream is disposed + * or just after the emission of a terminal event ({@code onComplete} or {@code onError}). + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code resourceSupplier}, {@code sourceSupplier} and {@code resourceCleanup} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/using.html">ReactiveX operators documentation: Using</a> + * @since 2.0 + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public static <@NonNull T, @NonNull D> Observable<T> using( + @NonNull Supplier<? extends D> resourceSupplier, + @NonNull Function<? super D, ? extends ObservableSource<? extends T>> sourceSupplier, + @NonNull Consumer<? super D> resourceCleanup, boolean eager) { + Objects.requireNonNull(resourceSupplier, "resourceSupplier is null"); + Objects.requireNonNull(sourceSupplier, "sourceSupplier is null"); + Objects.requireNonNull(resourceCleanup, "resourceCleanup is null"); + return RxJavaPlugins.onAssembly(new ObservableUsing<T, D>(resourceSupplier, sourceSupplier, resourceCleanup, eager)); + } + + /** + * Wraps an {@link ObservableSource} into an {@code Observable} if not already an {@code Observable}. + * + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code wrap} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T> the value type + * @param source the {@code ObservableSource} instance to wrap or cast to {@code Observable} + * @return the new {@code Observable} instance or the same as the source + * @throws NullPointerException if {@code source} is {@code null} + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public static <@NonNull T> Observable<T> wrap(@NonNull ObservableSource<T> source) { + Objects.requireNonNull(source, "source is null"); + if (source instanceof Observable) { + return RxJavaPlugins.onAssembly((Observable<T>)source); + } + return RxJavaPlugins.onAssembly(new ObservableFromUnsafeSource<>(source)); + } + + /** + * Returns an {@code Observable} that emits the results of a specified combiner function applied to combinations of + * items emitted, in sequence, by an {@link Iterable} of other {@link ObservableSource}s. + * <p> + * {@code zip} applies this function in strict sequence, so the first item emitted by the resulting {@code Observable} + * will be the result of the function applied to the first item emitted by each of the {@code ObservableSource}s; + * the second item emitted by the resulting {@code Observable} will be the result of the function applied to the second + * item emitted by each of those {@code ObservableSource}s; and so forth. + * <p> + * The resulting {@code Observable<R>} returned from {@code zip} will invoke {@code onNext} as many times as + * the number of {@code onNext} invocations of the {@code ObservableSource} that emits the fewest items. + * <p> + * The operator subscribes to its sources in order they are specified and completes eagerly if + * one of the sources is shorter than the rest while disposing the other sources. Therefore, it + * is possible those other sources will never be able to run to completion (and thus not calling + * {@code doOnComplete()}). This can also happen if the sources are exactly the same length; if + * source A completes and B has been consumed and is about to complete, the operator detects A won't + * be sending further values and it will dispose B immediately. For example: + * <pre><code>zip(Arrays.asList(range(1, 5).doOnComplete(action1), range(6, 5).doOnComplete(action2)), (a) -> a)</code></pre> + * {@code action1} will be called but {@code action2} won't. + * <br>To work around this termination property, + * use {@link #doOnDispose(Action)} as well or use {@code using()} to do cleanup in case of completion + * or a dispose() call. + * <p> + * Note on method signature: since Java doesn't allow creating a generic array with {@code new T[]}, the + * implementation of this operator has to create an {@code Object[]} instead. Unfortunately, a + * {@code Function<Integer[], R>} passed to the method would trigger a {@link ClassCastException}. + * + * <p> + * <img width="640" height="380" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/zip.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code zip} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T> the common value type + * @param <R> the zipped result type + * @param sources + * an {@code Iterable} of source {@code ObservableSource}s + * @param zipper + * a function that, when applied to an item emitted by each of the {@code ObservableSource}s, results in + * an item that will be emitted by the resulting {@code Observable} + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code sources} or {@code zipper} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/zip.html">ReactiveX operators documentation: Zip</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public static <@NonNull T, @NonNull R> Observable<R> zip(@NonNull Iterable<@NonNull ? extends ObservableSource<? extends T>> sources, @NonNull Function<? super Object[], ? extends R> zipper) { + Objects.requireNonNull(zipper, "zipper is null"); + Objects.requireNonNull(sources, "sources is null"); + return RxJavaPlugins.onAssembly(new ObservableZip<>(null, sources, zipper, bufferSize(), false)); + } + + /** + * Returns an {@code Observable} that emits the results of a specified combiner function applied to combinations of + * items emitted, in sequence, by an {@link Iterable} of other {@link ObservableSource}s. + * <p> + * {@code zip} applies this function in strict sequence, so the first item emitted by the resulting {@code Observable} + * will be the result of the function applied to the first item emitted by each of the {@code ObservableSource}s; + * the second item emitted by the resulting {@code Observable} will be the result of the function applied to the second + * item emitted by each of those {@code ObservableSource}s; and so forth. + * <p> + * The resulting {@code Observable<R>} returned from {@code zip} will invoke {@code onNext} as many times as + * the number of {@code onNext} invocations of the {@code ObservableSource} that emits the fewest items. + * <p> + * The operator subscribes to its sources in order they are specified and completes eagerly if + * one of the sources is shorter than the rest while disposing the other sources. Therefore, it + * is possible those other sources will never be able to run to completion (and thus not calling + * {@code doOnComplete()}). This can also happen if the sources are exactly the same length; if + * source A completes and B has been consumed and is about to complete, the operator detects A won't + * be sending further values and it will dispose B immediately. For example: + * <pre><code>zip(Arrays.asList(range(1, 5).doOnComplete(action1), range(6, 5).doOnComplete(action2)), (a) -> a)</code></pre> + * {@code action1} will be called but {@code action2} won't. + * <br>To work around this termination property, + * use {@link #doOnDispose(Action)} as well or use {@code using()} to do cleanup in case of completion + * or a dispose() call. + * <p> + * Note on method signature: since Java doesn't allow creating a generic array with {@code new T[]}, the + * implementation of this operator has to create an {@code Object[]} instead. Unfortunately, a + * {@code Function<Integer[], R>} passed to the method would trigger a {@link ClassCastException}. + * + * <p> + * <img width="640" height="380" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/zipIterable.o.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code zip} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * + * @param sources + * an {@code Iterable} of source {@code ObservableSource}s + * @param zipper + * a function that, when applied to an item emitted by each of the {@code ObservableSource}s, results in + * an item that will be emitted by the resulting {@code Observable} + * @param delayError + * delay errors signaled by any of the {@code ObservableSource} until all {@code ObservableSource}s terminate + * @param bufferSize + * the number of elements expected from each source {@code ObservableSource} to be buffered + * @param <T> the common source value type + * @param <R> the zipped result type + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code sources} or {@code zipper} is {@code null} + * @throws IllegalArgumentException if {@code bufferSize} is non-positive + * @see <a href="http://reactivex.io/documentation/operators/zip.html">ReactiveX operators documentation: Zip</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public static <@NonNull T, @NonNull R> Observable<R> zip(@NonNull Iterable<@NonNull ? extends ObservableSource<? extends T>> sources, + @NonNull Function<? super Object[], ? extends R> zipper, boolean delayError, + int bufferSize) { + Objects.requireNonNull(zipper, "zipper is null"); + Objects.requireNonNull(sources, "sources is null"); + ObjectHelper.verifyPositive(bufferSize, "bufferSize"); + return RxJavaPlugins.onAssembly(new ObservableZip<>(null, sources, zipper, bufferSize, delayError)); + } + + /** + * Returns an {@code Observable} that emits the results of a specified combiner function applied to combinations of + * two items emitted, in sequence, by two other {@link ObservableSource}s. + * <p> + * <img width="640" height="380" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/zip.v3.png" alt=""> + * <p> + * {@code zip} applies this function in strict sequence, so the first item emitted by the resulting {@code Observable} + * will be the result of the function applied to the first item emitted by {@code o1} and the first item + * emitted by {@code o2}; the second item emitted by the resulting {@code Observable} will be the result of the function + * applied to the second item emitted by {@code o1} and the second item emitted by {@code o2}; and so forth. + * <p> + * The resulting {@code Observable<R>} returned from {@code zip} will invoke {@link Observer#onNext onNext} + * as many times as the number of {@code onNext} invocations of the {@code ObservableSource} that emits the fewest + * items. + * <p> + * The operator subscribes to its sources in order they are specified and completes eagerly if + * one of the sources is shorter than the rest while disposing the other sources. Therefore, it + * is possible those other sources will never be able to run to completion (and thus not calling + * {@code doOnComplete()}). This can also happen if the sources are exactly the same length; if + * source A completes and B has been consumed and is about to complete, the operator detects A won't + * be sending further values and it will dispose B immediately. For example: + * <pre><code>zip(range(1, 5).doOnComplete(action1), range(6, 5).doOnComplete(action2), (a, b) -> a + b)</code></pre> + * {@code action1} will be called but {@code action2} won't. + * <br>To work around this termination property, + * use {@link #doOnDispose(Action)} as well or use {@code using()} to do cleanup in case of completion + * or a dispose() call. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code zip} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T1> the value type of the first source + * @param <T2> the value type of the second source + * @param <R> the zipped result type + * @param source1 + * the first source {@code ObservableSource} + * @param source2 + * a second source {@code ObservableSource} + * @param zipper + * a function that, when applied to an item emitted by each of the {@code ObservableSource}s, results + * in an item that will be emitted by the resulting {@code Observable} + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code source1}, {@code source2} or {@code zipper} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/zip.html">ReactiveX operators documentation: Zip</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public static <@NonNull T1, @NonNull T2, @NonNull R> Observable<R> zip( + @NonNull ObservableSource<? extends T1> source1, @NonNull ObservableSource<? extends T2> source2, + @NonNull BiFunction<? super T1, ? super T2, ? extends R> zipper) { + Objects.requireNonNull(source1, "source1 is null"); + Objects.requireNonNull(source2, "source2 is null"); + Objects.requireNonNull(zipper, "zipper is null"); + return zipArray(Functions.toFunction(zipper), false, bufferSize(), source1, source2); + } + + /** + * Returns an {@code Observable} that emits the results of a specified combiner function applied to combinations of + * two items emitted, in sequence, by two other {@link ObservableSource}s. + * <p> + * <img width="640" height="380" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/zip.v3.png" alt=""> + * <p> + * {@code zip} applies this function in strict sequence, so the first item emitted by the resulting {@code Observable} + * will be the result of the function applied to the first item emitted by {@code o1} and the first item + * emitted by {@code o2}; the second item emitted by the resulting {@code Observable} will be the result of the function + * applied to the second item emitted by {@code o1} and the second item emitted by {@code o2}; and so forth. + * <p> + * The resulting {@code Observable<R>} returned from {@code zip} will invoke {@link Observer#onNext onNext} + * as many times as the number of {@code onNext} invocations of the {@code ObservableSource} that emits the fewest + * items. + * <p> + * The operator subscribes to its sources in order they are specified and completes eagerly if + * one of the sources is shorter than the rest while disposing the other sources. Therefore, it + * is possible those other sources will never be able to run to completion (and thus not calling + * {@code doOnComplete()}). This can also happen if the sources are exactly the same length; if + * source A completes and B has been consumed and is about to complete, the operator detects A won't + * be sending further values and it will dispose B immediately. For example: + * <pre><code>zip(range(1, 5).doOnComplete(action1), range(6, 5).doOnComplete(action2), (a, b) -> a + b)</code></pre> + * {@code action1} will be called but {@code action2} won't. + * <br>To work around this termination property, + * use {@link #doOnDispose(Action)} as well or use {@code using()} to do cleanup in case of completion + * or a dispose() call. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code zip} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T1> the value type of the first source + * @param <T2> the value type of the second source + * @param <R> the zipped result type + * @param source1 + * the first source {@code ObservableSource} + * @param source2 + * a second source {@code ObservableSource} + * @param zipper + * a function that, when applied to an item emitted by each of the {@code ObservableSource}s, results + * in an item that will be emitted by the resulting {@code Observable} + * @param delayError delay errors from any of the {@code ObservableSource}s till the other terminates + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code source1}, {@code source2} or {@code zipper} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/zip.html">ReactiveX operators documentation: Zip</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public static <@NonNull T1, @NonNull T2, @NonNull R> Observable<R> zip( + @NonNull ObservableSource<? extends T1> source1, @NonNull ObservableSource<? extends T2> source2, + @NonNull BiFunction<? super T1, ? super T2, ? extends R> zipper, boolean delayError) { + Objects.requireNonNull(source1, "source1 is null"); + Objects.requireNonNull(source2, "source2 is null"); + Objects.requireNonNull(zipper, "zipper is null"); + return zipArray(Functions.toFunction(zipper), delayError, bufferSize(), source1, source2); + } + + /** + * Returns an {@code Observable} that emits the results of a specified combiner function applied to combinations of + * two items emitted, in sequence, by two other {@link ObservableSource}s. + * <p> + * <img width="640" height="380" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/zip.v3.png" alt=""> + * <p> + * {@code zip} applies this function in strict sequence, so the first item emitted by the resulting {@code Observable} + * will be the result of the function applied to the first item emitted by {@code o1} and the first item + * emitted by {@code o2}; the second item emitted by the resulting {@code Observable} will be the result of the function + * applied to the second item emitted by {@code o1} and the second item emitted by {@code o2}; and so forth. + * <p> + * The resulting {@code Observable<R>} returned from {@code zip} will invoke {@link Observer#onNext onNext} + * as many times as the number of {@code onNext} invocations of the {@code ObservableSource} that emits the fewest + * items. + * <p> + * The operator subscribes to its sources in order they are specified and completes eagerly if + * one of the sources is shorter than the rest while disposing the other sources. Therefore, it + * is possible those other sources will never be able to run to completion (and thus not calling + * {@code doOnComplete()}). This can also happen if the sources are exactly the same length; if + * source A completes and B has been consumed and is about to complete, the operator detects A won't + * be sending further values and it will dispose B immediately. For example: + * <pre><code>zip(range(1, 5).doOnComplete(action1), range(6, 5).doOnComplete(action2), (a, b) -> a + b)</code></pre> + * {@code action1} will be called but {@code action2} won't. + * <br>To work around this termination property, + * use {@link #doOnDispose(Action)} as well or use {@code using()} to do cleanup in case of completion + * or a dispose() call. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code zip} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T1> the value type of the first source + * @param <T2> the value type of the second source + * @param <R> the zipped result type + * @param source1 + * the first source {@code ObservableSource} + * @param source2 + * a second source {@code ObservableSource} + * @param zipper + * a function that, when applied to an item emitted by each of the {@code ObservableSource}s, results + * in an item that will be emitted by the resulting {@code Observable} + * @param delayError delay errors from any of the {@code ObservableSource}s till the other terminates + * @param bufferSize the number of elements expected from each source {@code ObservableSource} to be buffered + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code source1}, {@code source2} or {@code zipper} is {@code null} + * @throws IllegalArgumentException if {@code bufferSize} is non-positive + * @see <a href="http://reactivex.io/documentation/operators/zip.html">ReactiveX operators documentation: Zip</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public static <@NonNull T1, @NonNull T2, @NonNull R> Observable<R> zip( + @NonNull ObservableSource<? extends T1> source1, @NonNull ObservableSource<? extends T2> source2, + @NonNull BiFunction<? super T1, ? super T2, ? extends R> zipper, boolean delayError, int bufferSize) { + Objects.requireNonNull(source1, "source1 is null"); + Objects.requireNonNull(source2, "source2 is null"); + Objects.requireNonNull(zipper, "zipper is null"); + return zipArray(Functions.toFunction(zipper), delayError, bufferSize, source1, source2); + } + + /** + * Returns an {@code Observable} that emits the results of a specified combiner function applied to combinations of + * three items emitted, in sequence, by three other {@link ObservableSource}s. + * <p> + * <img width="640" height="380" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/zip.v3.png" alt=""> + * <p> + * {@code zip} applies this function in strict sequence, so the first item emitted by the resulting {@code Observable} + * will be the result of the function applied to the first item emitted by {@code o1}, the first item + * emitted by {@code o2}, and the first item emitted by {@code o3}; the second item emitted by the resulting + * {@code Observable} will be the result of the function applied to the second item emitted by {@code o1}, the + * second item emitted by {@code o2}, and the second item emitted by {@code o3}; and so forth. + * <p> + * The resulting {@code Observable<R>} returned from {@code zip} will invoke {@link Observer#onNext onNext} + * as many times as the number of {@code onNext} invocations of the {@code ObservableSource} that emits the fewest + * items. + * <p> + * The operator subscribes to its sources in order they are specified and completes eagerly if + * one of the sources is shorter than the rest while disposing the other sources. Therefore, it + * is possible those other sources will never be able to run to completion (and thus not calling + * {@code doOnComplete()}). This can also happen if the sources are exactly the same length; if + * source A completes and B has been consumed and is about to complete, the operator detects A won't + * be sending further values and it will dispose B immediately. For example: + * <pre><code>zip(range(1, 5).doOnComplete(action1), range(6, 5).doOnComplete(action2), ..., (a, b, c) -> a + b)</code></pre> + * {@code action1} will be called but {@code action2} won't. + * <br>To work around this termination property, + * use {@link #doOnDispose(Action)} as well or use {@code using()} to do cleanup in case of completion + * or a dispose() call. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code zip} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T1> the value type of the first source + * @param <T2> the value type of the second source + * @param <T3> the value type of the third source + * @param <R> the zipped result type + * @param source1 + * the first source {@code ObservableSource} + * @param source2 + * a second source {@code ObservableSource} + * @param source3 + * a third source {@code ObservableSource} + * @param zipper + * a function that, when applied to an item emitted by each of the {@code ObservableSource}s, results in + * an item that will be emitted by the resulting {@code Observable} + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code source1}, {@code source2}, {@code source3} or {@code zipper} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/zip.html">ReactiveX operators documentation: Zip</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public static <@NonNull T1, @NonNull T2, @NonNull T3, @NonNull R> Observable<R> zip( + @NonNull ObservableSource<? extends T1> source1, @NonNull ObservableSource<? extends T2> source2, + @NonNull ObservableSource<? extends T3> source3, + @NonNull Function3<? super T1, ? super T2, ? super T3, ? extends R> zipper) { + Objects.requireNonNull(source1, "source1 is null"); + Objects.requireNonNull(source2, "source2 is null"); + Objects.requireNonNull(source3, "source3 is null"); + Objects.requireNonNull(zipper, "zipper is null"); + return zipArray(Functions.toFunction(zipper), false, bufferSize(), source1, source2, source3); + } + + /** + * Returns an {@code Observable} that emits the results of a specified combiner function applied to combinations of + * four items emitted, in sequence, by four other {@link ObservableSource}s. + * <p> + * <img width="640" height="380" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/zip.v3.png" alt=""> + * <p> + * {@code zip} applies this function in strict sequence, so the first item emitted by the resulting {@code Observable} + * will be the result of the function applied to the first item emitted by {@code o1}, the first item + * emitted by {@code o2}, the first item emitted by {@code o3}, and the first item emitted by {@code 04}; + * the second item emitted by the resulting {@code Observable} will be the result of the function applied to the second + * item emitted by each of those {@code ObservableSource}s; and so forth. + * <p> + * The resulting {@code Observable<R>} returned from {@code zip} will invoke {@link Observer#onNext onNext} + * as many times as the number of {@code onNext} invocations of the {@code ObservableSource} that emits the fewest + * items. + * <p> + * The operator subscribes to its sources in order they are specified and completes eagerly if + * one of the sources is shorter than the rest while disposing the other sources. Therefore, it + * is possible those other sources will never be able to run to completion (and thus not calling + * {@code doOnComplete()}). This can also happen if the sources are exactly the same length; if + * source A completes and B has been consumed and is about to complete, the operator detects A won't + * be sending further values and it will dispose B immediately. For example: + * <pre><code>zip(range(1, 5).doOnComplete(action1), range(6, 5).doOnComplete(action2), ..., (a, b, c, d) -> a + b)</code></pre> + * {@code action1} will be called but {@code action2} won't. + * <br>To work around this termination property, + * use {@link #doOnDispose(Action)} as well or use {@code using()} to do cleanup in case of completion + * or a dispose() call. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code zip} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T1> the value type of the first source + * @param <T2> the value type of the second source + * @param <T3> the value type of the third source + * @param <T4> the value type of the fourth source + * @param <R> the zipped result type + * @param source1 + * the first source {@code ObservableSource} + * @param source2 + * a second source {@code ObservableSource} + * @param source3 + * a third source {@code ObservableSource} + * @param source4 + * a fourth source {@code ObservableSource} + * @param zipper + * a function that, when applied to an item emitted by each of the {@code ObservableSource}s, results in + * an item that will be emitted by the resulting {@code Observable} + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code source1}, {@code source2}, {@code source3}, + * {@code source4} or {@code zipper} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/zip.html">ReactiveX operators documentation: Zip</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public static <@NonNull T1, @NonNull T2, @NonNull T3, @NonNull T4, @NonNull R> Observable<R> zip( + @NonNull ObservableSource<? extends T1> source1, @NonNull ObservableSource<? extends T2> source2, + @NonNull ObservableSource<? extends T3> source3, @NonNull ObservableSource<? extends T4> source4, + @NonNull Function4<? super T1, ? super T2, ? super T3, ? super T4, ? extends R> zipper) { + Objects.requireNonNull(source1, "source1 is null"); + Objects.requireNonNull(source2, "source2 is null"); + Objects.requireNonNull(source3, "source3 is null"); + Objects.requireNonNull(source4, "source4 is null"); + Objects.requireNonNull(zipper, "zipper is null"); + return zipArray(Functions.toFunction(zipper), false, bufferSize(), source1, source2, source3, source4); + } + + /** + * Returns an {@code Observable} that emits the results of a specified combiner function applied to combinations of + * five items emitted, in sequence, by five other {@link ObservableSource}s. + * <p> + * <img width="640" height="380" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/zip.v3.png" alt=""> + * <p> + * {@code zip} applies this function in strict sequence, so the first item emitted by the resulting {@code Observable} + * will be the result of the function applied to the first item emitted by {@code o1}, the first item + * emitted by {@code o2}, the first item emitted by {@code o3}, the first item emitted by {@code o4}, and + * the first item emitted by {@code o5}; the second item emitted by the resulting {@code Observable} will be the result of + * the function applied to the second item emitted by each of those {@code ObservableSource}s; and so forth. + * <p> + * The resulting {@code Observable<R>} returned from {@code zip} will invoke {@link Observer#onNext onNext} + * as many times as the number of {@code onNext} invocations of the {@code ObservableSource} that emits the fewest + * items. + * <p> + * The operator subscribes to its sources in order they are specified and completes eagerly if + * one of the sources is shorter than the rest while disposing the other sources. Therefore, it + * is possible those other sources will never be able to run to completion (and thus not calling + * {@code doOnComplete()}). This can also happen if the sources are exactly the same length; if + * source A completes and B has been consumed and is about to complete, the operator detects A won't + * be sending further values and it will dispose B immediately. For example: + * <pre><code>zip(range(1, 5).doOnComplete(action1), range(6, 5).doOnComplete(action2), ..., (a, b, c, d, e) -> a + b)</code></pre> + * {@code action1} will be called but {@code action2} won't. + * <br>To work around this termination property, + * use {@link #doOnDispose(Action)} as well or use {@code using()} to do cleanup in case of completion + * or a dispose() call. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code zip} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T1> the value type of the first source + * @param <T2> the value type of the second source + * @param <T3> the value type of the third source + * @param <T4> the value type of the fourth source + * @param <T5> the value type of the fifth source + * @param <R> the zipped result type + * @param source1 + * the first source {@code ObservableSource} + * @param source2 + * a second source {@code ObservableSource} + * @param source3 + * a third source {@code ObservableSource} + * @param source4 + * a fourth source {@code ObservableSource} + * @param source5 + * a fifth source {@code ObservableSource} + * @param zipper + * a function that, when applied to an item emitted by each of the {@code ObservableSource}s, results in + * an item that will be emitted by the resulting {@code Observable} + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code source1}, {@code source2}, {@code source3}, + * {@code source4}, {@code source5} or {@code zipper} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/zip.html">ReactiveX operators documentation: Zip</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public static <@NonNull T1, @NonNull T2, @NonNull T3, @NonNull T4, @NonNull T5, @NonNull R> Observable<R> zip( + @NonNull ObservableSource<? extends T1> source1, @NonNull ObservableSource<? extends T2> source2, @NonNull ObservableSource<? extends T3> source3, + @NonNull ObservableSource<? extends T4> source4, @NonNull ObservableSource<? extends T5> source5, + @NonNull Function5<? super T1, ? super T2, ? super T3, ? super T4, ? super T5, ? extends R> zipper) { + Objects.requireNonNull(source1, "source1 is null"); + Objects.requireNonNull(source2, "source2 is null"); + Objects.requireNonNull(source3, "source3 is null"); + Objects.requireNonNull(source4, "source4 is null"); + Objects.requireNonNull(source5, "source5 is null"); + Objects.requireNonNull(zipper, "zipper is null"); + return zipArray(Functions.toFunction(zipper), false, bufferSize(), source1, source2, source3, source4, source5); + } + + /** + * Returns an {@code Observable} that emits the results of a specified combiner function applied to combinations of + * six items emitted, in sequence, by six other {@link ObservableSource}s. + * <p> + * <img width="640" height="380" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/zip.v3.png" alt=""> + * <p> + * {@code zip} applies this function in strict sequence, so the first item emitted by the resulting {@code Observable} + * will be the result of the function applied to the first item emitted by each source {@code ObservableSource}, the + * second item emitted by the resulting {@code Observable} will be the result of the function applied to the second item + * emitted by each of those {@code ObservableSource}s, and so forth. + * <p> + * The resulting {@code Observable<R>} returned from {@code zip} will invoke {@link Observer#onNext onNext} + * as many times as the number of {@code onNext} invocations of the {@code ObservableSource} that emits the fewest + * items. + * <p> + * The operator subscribes to its sources in order they are specified and completes eagerly if + * one of the sources is shorter than the rest while disposing the other sources. Therefore, it + * is possible those other sources will never be able to run to completion (and thus not calling + * {@code doOnComplete()}). This can also happen if the sources are exactly the same length; if + * source A completes and B has been consumed and is about to complete, the operator detects A won't + * be sending further values and it will dispose B immediately. For example: + * <pre><code>zip(range(1, 5).doOnComplete(action1), range(6, 5).doOnComplete(action2), ..., (a, b, c, d, e, f) -> a + b)</code></pre> + * {@code action1} will be called but {@code action2} won't. + * <br>To work around this termination property, + * use {@link #doOnDispose(Action)} as well or use {@code using()} to do cleanup in case of completion + * or a dispose() call. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code zip} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T1> the value type of the first source + * @param <T2> the value type of the second source + * @param <T3> the value type of the third source + * @param <T4> the value type of the fourth source + * @param <T5> the value type of the fifth source + * @param <T6> the value type of the sixth source + * @param <R> the zipped result type + * @param source1 + * the first source {@code ObservableSource} + * @param source2 + * a second source {@code ObservableSource} + * @param source3 + * a third source {@code ObservableSource} + * @param source4 + * a fourth source {@code ObservableSource} + * @param source5 + * a fifth source {@code ObservableSource} + * @param source6 + * a sixth source {@code ObservableSource} + * @param zipper + * a function that, when applied to an item emitted by each of the {@code ObservableSource}s, results in + * an item that will be emitted by the resulting {@code Observable} + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code source1}, {@code source2}, {@code source3}, + * {@code source4}, {@code source5}, {@code source6} or {@code zipper} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/zip.html">ReactiveX operators documentation: Zip</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public static <@NonNull T1, @NonNull T2, @NonNull T3, @NonNull T4, @NonNull T5, @NonNull T6, @NonNull R> Observable<R> zip( + @NonNull ObservableSource<? extends T1> source1, @NonNull ObservableSource<? extends T2> source2, @NonNull ObservableSource<? extends T3> source3, + @NonNull ObservableSource<? extends T4> source4, @NonNull ObservableSource<? extends T5> source5, @NonNull ObservableSource<? extends T6> source6, + @NonNull Function6<? super T1, ? super T2, ? super T3, ? super T4, ? super T5, ? super T6, ? extends R> zipper) { + Objects.requireNonNull(source1, "source1 is null"); + Objects.requireNonNull(source2, "source2 is null"); + Objects.requireNonNull(source3, "source3 is null"); + Objects.requireNonNull(source4, "source4 is null"); + Objects.requireNonNull(source5, "source5 is null"); + Objects.requireNonNull(source6, "source6 is null"); + Objects.requireNonNull(zipper, "zipper is null"); + return zipArray(Functions.toFunction(zipper), false, bufferSize(), source1, source2, source3, source4, source5, source6); + } + + /** + * Returns an {@code Observable} that emits the results of a specified combiner function applied to combinations of + * seven items emitted, in sequence, by seven other {@link ObservableSource}s. + * <p> + * <img width="640" height="380" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/zip.v3.png" alt=""> + * <p> + * {@code zip} applies this function in strict sequence, so the first item emitted by the resulting {@code Observable} + * will be the result of the function applied to the first item emitted by each source {@code ObservableSource}, the + * second item emitted by the resulting {@code Observable} will be the result of the function applied to the second item + * emitted by each of those {@code ObservableSource}s, and so forth. + * <p> + * The resulting {@code Observable<R>} returned from {@code zip} will invoke {@link Observer#onNext onNext} + * as many times as the number of {@code onNext} invocations of the {@code ObservableSource} that emits the fewest + * items. + * <p> + * The operator subscribes to its sources in order they are specified and completes eagerly if + * one of the sources is shorter than the rest while disposing the other sources. Therefore, it + * is possible those other sources will never be able to run to completion (and thus not calling + * {@code doOnComplete()}). This can also happen if the sources are exactly the same length; if + * source A completes and B has been consumed and is about to complete, the operator detects A won't + * be sending further values and it will dispose B immediately. For example: + * <pre><code>zip(range(1, 5).doOnComplete(action1), range(6, 5).doOnComplete(action2), ..., (a, b, c, d, e, f, g) -> a + b)</code></pre> + * {@code action1} will be called but {@code action2} won't. + * <br>To work around this termination property, + * use {@link #doOnDispose(Action)} as well or use {@code using()} to do cleanup in case of completion + * or a dispose() call. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code zip} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T1> the value type of the first source + * @param <T2> the value type of the second source + * @param <T3> the value type of the third source + * @param <T4> the value type of the fourth source + * @param <T5> the value type of the fifth source + * @param <T6> the value type of the sixth source + * @param <T7> the value type of the seventh source + * @param <R> the zipped result type + * @param source1 + * the first source {@code ObservableSource} + * @param source2 + * a second source {@code ObservableSource} + * @param source3 + * a third source {@code ObservableSource} + * @param source4 + * a fourth source {@code ObservableSource} + * @param source5 + * a fifth source {@code ObservableSource} + * @param source6 + * a sixth source {@code ObservableSource} + * @param source7 + * a seventh source {@code ObservableSource} + * @param zipper + * a function that, when applied to an item emitted by each of the {@code ObservableSource}s, results in + * an item that will be emitted by the resulting {@code Observable} + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code source1}, {@code source2}, {@code source3}, + * {@code source4}, {@code source5}, {@code source6}, + * {@code source7} or {@code zipper} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/zip.html">ReactiveX operators documentation: Zip</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public static <@NonNull T1, @NonNull T2, @NonNull T3, @NonNull T4, @NonNull T5, @NonNull T6, @NonNull T7, @NonNull R> Observable<R> zip( + @NonNull ObservableSource<? extends T1> source1, @NonNull ObservableSource<? extends T2> source2, @NonNull ObservableSource<? extends T3> source3, + @NonNull ObservableSource<? extends T4> source4, @NonNull ObservableSource<? extends T5> source5, @NonNull ObservableSource<? extends T6> source6, + @NonNull ObservableSource<? extends T7> source7, + @NonNull Function7<? super T1, ? super T2, ? super T3, ? super T4, ? super T5, ? super T6, ? super T7, ? extends R> zipper) { + Objects.requireNonNull(source1, "source1 is null"); + Objects.requireNonNull(source2, "source2 is null"); + Objects.requireNonNull(source3, "source3 is null"); + Objects.requireNonNull(source4, "source4 is null"); + Objects.requireNonNull(source5, "source5 is null"); + Objects.requireNonNull(source6, "source6 is null"); + Objects.requireNonNull(source7, "source7 is null"); + Objects.requireNonNull(zipper, "zipper is null"); + return zipArray(Functions.toFunction(zipper), false, bufferSize(), source1, source2, source3, source4, source5, source6, source7); + } + + /** + * Returns an {@code Observable} that emits the results of a specified combiner function applied to combinations of + * eight items emitted, in sequence, by eight other {@link ObservableSource}s. + * <p> + * <img width="640" height="380" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/zip.v3.png" alt=""> + * <p> + * {@code zip} applies this function in strict sequence, so the first item emitted by the resulting {@code Observable} + * will be the result of the function applied to the first item emitted by each source {@code ObservableSource}, the + * second item emitted by the resulting {@code Observable} will be the result of the function applied to the second item + * emitted by each of those {@code ObservableSource}s, and so forth. + * <p> + * The resulting {@code Observable<R>} returned from {@code zip} will invoke {@link Observer#onNext onNext} + * as many times as the number of {@code onNext} invocations of the {@code ObservableSource} that emits the fewest + * items. + * <p> + * The operator subscribes to its sources in order they are specified and completes eagerly if + * one of the sources is shorter than the rest while disposing the other sources. Therefore, it + * is possible those other sources will never be able to run to completion (and thus not calling + * {@code doOnComplete()}). This can also happen if the sources are exactly the same length; if + * source A completes and B has been consumed and is about to complete, the operator detects A won't + * be sending further values and it will dispose B immediately. For example: + * <pre><code>zip(range(1, 5).doOnComplete(action1), range(6, 5).doOnComplete(action2), ..., (a, b, c, d, e, f, g, h) -> a + b)</code></pre> + * {@code action1} will be called but {@code action2} won't. + * <br>To work around this termination property, + * use {@link #doOnDispose(Action)} as well or use {@code using()} to do cleanup in case of completion + * or a dispose() call. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code zip} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T1> the value type of the first source + * @param <T2> the value type of the second source + * @param <T3> the value type of the third source + * @param <T4> the value type of the fourth source + * @param <T5> the value type of the fifth source + * @param <T6> the value type of the sixth source + * @param <T7> the value type of the seventh source + * @param <T8> the value type of the eighth source + * @param <R> the zipped result type + * @param source1 + * the first source {@code ObservableSource} + * @param source2 + * a second source {@code ObservableSource} + * @param source3 + * a third source {@code ObservableSource} + * @param source4 + * a fourth source {@code ObservableSource} + * @param source5 + * a fifth source {@code ObservableSource} + * @param source6 + * a sixth source {@code ObservableSource} + * @param source7 + * a seventh source {@code ObservableSource} + * @param source8 + * an eighth source {@code ObservableSource} + * @param zipper + * a function that, when applied to an item emitted by each of the {@code ObservableSource}s, results in + * an item that will be emitted by the resulting {@code Observable} + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code source1}, {@code source2}, {@code source3}, + * {@code source4}, {@code source5}, {@code source6}, + * {@code source7}, {@code source8} or {@code zipper} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/zip.html">ReactiveX operators documentation: Zip</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public static <@NonNull T1, @NonNull T2, @NonNull T3, @NonNull T4, @NonNull T5, @NonNull T6, @NonNull T7, @NonNull T8, @NonNull R> Observable<R> zip( + @NonNull ObservableSource<? extends T1> source1, @NonNull ObservableSource<? extends T2> source2, @NonNull ObservableSource<? extends T3> source3, + @NonNull ObservableSource<? extends T4> source4, @NonNull ObservableSource<? extends T5> source5, @NonNull ObservableSource<? extends T6> source6, + @NonNull ObservableSource<? extends T7> source7, @NonNull ObservableSource<? extends T8> source8, + @NonNull Function8<? super T1, ? super T2, ? super T3, ? super T4, ? super T5, ? super T6, ? super T7, ? super T8, ? extends R> zipper) { + Objects.requireNonNull(source1, "source1 is null"); + Objects.requireNonNull(source2, "source2 is null"); + Objects.requireNonNull(source3, "source3 is null"); + Objects.requireNonNull(source4, "source4 is null"); + Objects.requireNonNull(source5, "source5 is null"); + Objects.requireNonNull(source6, "source6 is null"); + Objects.requireNonNull(source7, "source7 is null"); + Objects.requireNonNull(source8, "source8 is null"); + Objects.requireNonNull(zipper, "zipper is null"); + return zipArray(Functions.toFunction(zipper), false, bufferSize(), source1, source2, source3, source4, source5, source6, source7, source8); + } + + /** + * Returns an {@code Observable} that emits the results of a specified combiner function applied to combinations of + * nine items emitted, in sequence, by nine other {@link ObservableSource}s. + * <p> + * <img width="640" height="380" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/zip.v3.png" alt=""> + * <p> + * {@code zip} applies this function in strict sequence, so the first item emitted by the resulting {@code Observable} + * will be the result of the function applied to the first item emitted by each source {@code ObservableSource}, the + * second item emitted by the resulting {@code Observable} will be the result of the function applied to the second item + * emitted by each of those {@code ObservableSource}s, and so forth. + * <p> + * The resulting {@code Observable<R>} returned from {@code zip} will invoke {@link Observer#onNext onNext} + * as many times as the number of {@code onNext} invocations of the {@code ObservableSource} that emits the fewest + * items. + * <p> + * The operator subscribes to its sources in order they are specified and completes eagerly if + * one of the sources is shorter than the rest while disposing the other sources. Therefore, it + * is possible those other sources will never be able to run to completion (and thus not calling + * {@code doOnComplete()}). This can also happen if the sources are exactly the same length; if + * source A completes and B has been consumed and is about to complete, the operator detects A won't + * be sending further values and it will dispose B immediately. For example: + * <pre><code>zip(range(1, 5).doOnComplete(action1), range(6, 5).doOnComplete(action2), ..., (a, b, c, d, e, f, g, h, i) -> a + b)</code></pre> + * {@code action1} will be called but {@code action2} won't. + * <br>To work around this termination property, + * use {@link #doOnDispose(Action)} as well or use {@code using()} to do cleanup in case of completion + * or a dispose() call. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code zip} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T1> the value type of the first source + * @param <T2> the value type of the second source + * @param <T3> the value type of the third source + * @param <T4> the value type of the fourth source + * @param <T5> the value type of the fifth source + * @param <T6> the value type of the sixth source + * @param <T7> the value type of the seventh source + * @param <T8> the value type of the eighth source + * @param <T9> the value type of the ninth source + * @param <R> the zipped result type + * @param source1 + * the first source {@code ObservableSource} + * @param source2 + * a second source {@code ObservableSource} + * @param source3 + * a third source {@code ObservableSource} + * @param source4 + * a fourth source {@code ObservableSource} + * @param source5 + * a fifth source {@code ObservableSource} + * @param source6 + * a sixth source {@code ObservableSource} + * @param source7 + * a seventh source {@code ObservableSource} + * @param source8 + * an eighth source {@code ObservableSource} + * @param source9 + * a ninth source {@code ObservableSource} + * @param zipper + * a function that, when applied to an item emitted by each of the {@code ObservableSource}s, results in + * an item that will be emitted by the resulting {@code Observable} + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code source1}, {@code source2}, {@code source3}, + * {@code source4}, {@code source5}, {@code source6}, + * {@code source7}, {@code source8}, {@code source9} or {@code zipper} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/zip.html">ReactiveX operators documentation: Zip</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public static <@NonNull T1, @NonNull T2, @NonNull T3, @NonNull T4, @NonNull T5, @NonNull T6, @NonNull T7, @NonNull T8, @NonNull T9, @NonNull R> Observable<R> zip( + @NonNull ObservableSource<? extends T1> source1, @NonNull ObservableSource<? extends T2> source2, @NonNull ObservableSource<? extends T3> source3, + @NonNull ObservableSource<? extends T4> source4, @NonNull ObservableSource<? extends T5> source5, @NonNull ObservableSource<? extends T6> source6, + @NonNull ObservableSource<? extends T7> source7, @NonNull ObservableSource<? extends T8> source8, @NonNull ObservableSource<? extends T9> source9, + @NonNull Function9<? super T1, ? super T2, ? super T3, ? super T4, ? super T5, ? super T6, ? super T7, ? super T8, ? super T9, ? extends R> zipper) { + Objects.requireNonNull(source1, "source1 is null"); + Objects.requireNonNull(source2, "source2 is null"); + Objects.requireNonNull(source3, "source3 is null"); + Objects.requireNonNull(source4, "source4 is null"); + Objects.requireNonNull(source5, "source5 is null"); + Objects.requireNonNull(source6, "source6 is null"); + Objects.requireNonNull(source7, "source7 is null"); + Objects.requireNonNull(source8, "source8 is null"); + Objects.requireNonNull(source9, "source9 is null"); + Objects.requireNonNull(zipper, "zipper is null"); + return zipArray(Functions.toFunction(zipper), false, bufferSize(), source1, source2, source3, source4, source5, source6, source7, source8, source9); + } + + /** + * Returns an {@code Observable} that emits the results of a specified combiner function applied to combinations of + * items emitted, in sequence, by an array of other {@link ObservableSource}s. + * <p> + * {@code zip} applies this function in strict sequence, so the first item emitted by the resulting {@code Observable} + * will be the result of the function applied to the first item emitted by each of the {@code ObservableSource}s; + * the second item emitted by the resulting {@code Observable} will be the result of the function applied to the second + * item emitted by each of those {@code ObservableSource}s; and so forth. + * <p> + * The resulting {@code Observable<R>} returned from {@code zip} will invoke {@code onNext} as many times as + * the number of {@code onNext} invocations of the {@code ObservableSource} that emits the fewest items. + * <p> + * The operator subscribes to its sources in order they are specified and completes eagerly if + * one of the sources is shorter than the rest while disposing the other sources. Therefore, it + * is possible those other sources will never be able to run to completion (and thus not calling + * {@code doOnComplete()}). This can also happen if the sources are exactly the same length; if + * source A completes and B has been consumed and is about to complete, the operator detects A won't + * be sending further values and it will dispose B immediately. For example: + * <pre><code>zip(new ObservableSource[]{range(1, 5).doOnComplete(action1), range(6, 5).doOnComplete(action2)}, (a) -> + * a)</code></pre> + * {@code action1} will be called but {@code action2} won't. + * <br>To work around this termination property, + * use {@link #doOnDispose(Action)} as well or use {@code using()} to do cleanup in case of completion + * or a dispose() call. + * <p> + * Note on method signature: since Java doesn't allow creating a generic array with {@code new T[]}, the + * implementation of this operator has to create an {@code Object[]} instead. Unfortunately, a + * {@code Function<Integer[], R>} passed to the method would trigger a {@link ClassCastException}. + * + * <p> + * <img width="640" height="380" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/zipArray.o.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code zipArray} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T> the common element type + * @param <R> the result type + * @param sources + * an array of source {@code ObservableSource}s + * @param zipper + * a function that, when applied to an item emitted by each of the {@code ObservableSource}s, results in + * an item that will be emitted by the resulting {@code Observable} + * @param delayError + * delay errors signaled by any of the {@code ObservableSource} until all {@code ObservableSource}s terminate + * @param bufferSize + * the number of elements expected from each source {@code ObservableSource} to be buffered + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code sources} or {@code zipper} is {@code null} + * @throws IllegalArgumentException if {@code bufferSize} is non-positive + * @see <a href="http://reactivex.io/documentation/operators/zip.html">ReactiveX operators documentation: Zip</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @SafeVarargs + @NonNull + public static <@NonNull T, @NonNull R> Observable<R> zipArray( + @NonNull Function<? super Object[], ? extends R> zipper, + boolean delayError, int bufferSize, + @NonNull ObservableSource<? extends T>... sources) { + Objects.requireNonNull(sources, "sources is null"); + if (sources.length == 0) { + return empty(); + } + Objects.requireNonNull(zipper, "zipper is null"); + ObjectHelper.verifyPositive(bufferSize, "bufferSize"); + return RxJavaPlugins.onAssembly(new ObservableZip<>(sources, null, zipper, bufferSize, delayError)); + } + + // *************************************************************************************************** + // Instance operators + // *************************************************************************************************** + + /** + * Returns a {@link Single} that emits a {@link Boolean} that indicates whether all of the items emitted by the current + * {@code Observable} satisfy a condition. + * <p> + * <img width="640" height="265" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/all.o.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code all} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param predicate + * a function that evaluates an item and returns a {@code Boolean} + * @return the new {@code Single} instance + * @throws NullPointerException if {@code predicate} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/all.html">ReactiveX operators documentation: All</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Single<Boolean> all(@NonNull Predicate<? super T> predicate) { + Objects.requireNonNull(predicate, "predicate is null"); + return RxJavaPlugins.onAssembly(new ObservableAllSingle<>(this, predicate)); + } + + /** + * Mirrors the current {@code Observable} or the other {@link ObservableSource} provided of which the first either emits an item or sends a termination + * notification. + * <p> + * <img width="640" height="448" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Observable.ambWith.png" alt=""> + * <p> + * When the current {@code Observable} signals an item or terminates first, the subscription to the other + * {@code ObservableSource} is disposed. If the other {@code ObservableSource} signals an item or terminates first, + * the subscription to the current {@code Observable} is disposed. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code ambWith} does not operate by default on a particular {@link Scheduler}.</dd> + * <dt><b>Error handling:</b></dt> + * <dd> + * If the losing {@code ObservableSource} signals an error, the error is routed to the global + * error handler via {@link RxJavaPlugins#onError(Throwable)}. + * </dd> + * </dl> + * + * @param other + * an {@code ObservableSource} competing to react first. A subscription to this provided source will occur after + * subscribing to the current source. + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code other} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/amb.html">ReactiveX operators documentation: Amb</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Observable<T> ambWith(@NonNull ObservableSource<? extends T> other) { + Objects.requireNonNull(other, "other is null"); + return ambArray(this, other); + } + + /** + * Returns a {@link Single} that emits {@code true} if any item emitted by the current {@code Observable} satisfies a + * specified condition, otherwise {@code false}. <em>Note:</em> this always emits {@code false} if the + * current {@code Observable} is empty. + * <p> + * <img width="640" height="320" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/any.2.v3.png" alt=""> + * <p> + * In Rx.Net this is the {@code any} {@link Observer} but we renamed it in RxJava to better match Java naming + * idioms. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code any} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param predicate + * the condition to test items emitted by the current {@code Observable} + * @return the new {@code Single} instance + * @throws NullPointerException if {@code predicate} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/contains.html">ReactiveX operators documentation: Contains</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Single<Boolean> any(@NonNull Predicate<? super T> predicate) { + Objects.requireNonNull(predicate, "predicate is null"); + return RxJavaPlugins.onAssembly(new ObservableAnySingle<>(this, predicate)); + } + + /** + * Returns the first item emitted by the current {@code Observable}, or throws + * {@link NoSuchElementException} if it emits no items. + * <p> + * <img width="640" height="413" src="https://github.com/ReactiveX/RxJava/wiki/images/rx-operators/blockingFirst.o.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code blockingFirst} does not operate by default on a particular {@link Scheduler}.</dd> + * <dt><b>Error handling:</b></dt> + * <dd>If the source signals an error, the operator wraps a checked {@link Exception} + * into {@link RuntimeException} and throws that. Otherwise, {@code RuntimeException}s and + * {@link Error}s are rethrown as they are.</dd> + * </dl> + * + * @return the first item emitted by the current {@code Observable} + * @throws NoSuchElementException + * if the current {@code Observable} emits no items + * @see <a href="http://reactivex.io/documentation/operators/first.html">ReactiveX documentation: First</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final T blockingFirst() { + BlockingFirstObserver<T> observer = new BlockingFirstObserver<>(); + subscribe(observer); + T v = observer.blockingGet(); + if (v != null) { + return v; + } + throw new NoSuchElementException(); + } + + /** + * Returns the first item emitted by the current {@code Observable}, or a default value if it emits no + * items. + * <p> + * <img width="640" height="329" src="https://github.com/ReactiveX/RxJava/wiki/images/rx-operators/blockingFirst.o.default.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code blockingFirst} does not operate by default on a particular {@link Scheduler}.</dd> + * <dt><b>Error handling:</b></dt> + * <dd>If the source signals an error, the operator wraps a checked {@link Exception} + * into {@link RuntimeException} and throws that. Otherwise, {@code RuntimeException}s and + * {@link Error}s are rethrown as they are.</dd> + * </dl> + * + * @param defaultItem + * a default value to return if the current {@code Observable} emits no items + * @return the first item emitted by the current {@code Observable}, or the default value if it emits no + * items + * @throws NullPointerException if {@code defaultItem} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/first.html">ReactiveX documentation: First</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final T blockingFirst(@NonNull T defaultItem) { + Objects.requireNonNull(defaultItem, "defaultItem is null"); + BlockingFirstObserver<T> observer = new BlockingFirstObserver<>(); + subscribe(observer); + T v = observer.blockingGet(); + return v != null ? v : defaultItem; + } + + /** + * Consumes the current {@code Observable} in a blocking fashion and invokes the given + * {@link Consumer} with each upstream item on the <em>current thread</em> until the + * upstream terminates. + * <p> + * <img width="640" height="330" src="https://github.com/ReactiveX/RxJava/wiki/images/rx-operators/blockingForEach.o.v3.png" alt=""> + * <p> + * <em>Note:</em> the method will only return if the upstream terminates or the current + * thread is interrupted. + * <p> + * This method executes the {@code Consumer} on the current thread while + * {@link #subscribe(Consumer)} executes the consumer on the original caller thread of the + * sequence. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code blockingForEach} does not operate by default on a particular {@link Scheduler}.</dd> + * <dt><b>Error handling:</b></dt> + * <dd>If the source signals an error, the operator wraps a checked {@link Exception} + * into {@link RuntimeException} and throws that. Otherwise, {@code RuntimeException}s and + * {@link Error}s are rethrown as they are.</dd> + * </dl> + * + * @param onNext + * the {@code Consumer} to invoke for each item emitted by the {@code Observable} + * @throws NullPointerException if {@code onNext} is {@code null} + * @throws RuntimeException + * if an error occurs + * @see <a href="http://reactivex.io/documentation/operators/subscribe.html">ReactiveX documentation: Subscribe</a> + * @see #subscribe(Consumer) + * @see #blockingForEach(Consumer, int) + */ + @SchedulerSupport(SchedulerSupport.NONE) + public final void blockingForEach(@NonNull Consumer<? super T> onNext) { + blockingForEach(onNext, bufferSize()); + } + + /** + * Consumes the current {@code Observable} in a blocking fashion and invokes the given + * {@link Consumer} with each upstream item on the <em>current thread</em> until the + * upstream terminates. + * <p> + * <img width="640" height="330" src="https://github.com/ReactiveX/RxJava/wiki/images/rx-operators/blockingForEach.o.v3.png" alt=""> + * <p> + * <em>Note:</em> the method will only return if the upstream terminates or the current + * thread is interrupted. + * <p> + * This method executes the {@code Consumer} on the current thread while + * {@link #subscribe(Consumer)} executes the consumer on the original caller thread of the + * sequence. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code blockingForEach} does not operate by default on a particular {@link Scheduler}.</dd> + * <dt><b>Error handling:</b></dt> + * <dd>If the source signals an error, the operator wraps a checked {@link Exception} + * into {@link RuntimeException} and throws that. Otherwise, {@code RuntimeException}s and + * {@link Error}s are rethrown as they are.</dd> + * </dl> + * + * @param onNext + * the {@code Consumer} to invoke for each item emitted by the {@code Observable} + * @param capacityHint + * the number of items expected to be buffered (allows reducing buffer reallocations) + * @throws NullPointerException if {@code onNext} is {@code null} + * @throws IllegalArgumentException if {@code capacityHint} is non-positive + * @throws RuntimeException + * if an error occurs; {@code Error}s and {@code RuntimeException}s are rethrown + * as they are, checked {@code Exception}s are wrapped into {@code RuntimeException}s + * @see <a href="http://reactivex.io/documentation/operators/subscribe.html">ReactiveX documentation: Subscribe</a> + * @see #subscribe(Consumer) + */ + @SchedulerSupport(SchedulerSupport.NONE) + public final void blockingForEach(@NonNull Consumer<? super T> onNext, int capacityHint) { + Objects.requireNonNull(onNext, "onNext is null"); + Iterator<T> it = blockingIterable(capacityHint).iterator(); + while (it.hasNext()) { + try { + onNext.accept(it.next()); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + ((Disposable)it).dispose(); + throw ExceptionHelper.wrapOrThrow(e); + } + } + } + + /** + * Exposes the current {@code Observable} as an {@link Iterable} which, when iterated, + * subscribes to the current {@code Observable} and blocks + * until the current {@code Observable} emits items or terminates. + * <p> + * <img width="640" height="315" src="https://github.com/ReactiveX/RxJava/wiki/images/rx-operators/blockingIterable.o.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code blockingIterable} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @return the new {@code Iterable} instance + * @see <a href="http://reactivex.io/documentation/operators/to.html">ReactiveX documentation: To</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Iterable<T> blockingIterable() { + return blockingIterable(bufferSize()); + } + + /** + * Exposes the current {@code Observable} as an {@link Iterable} which, when iterated, + * subscribes to the current {@code Observable} and blocks + * until the current {@code Observable} emits items or terminates. + * <p> + * <img width="640" height="315" src="https://github.com/ReactiveX/RxJava/wiki/images/rx-operators/blockingIterable.o.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code blockingIterable} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param capacityHint the expected number of items to be buffered + * @return the new {@code Iterable} instance + * @throws IllegalArgumentException if {@code capacityHint} is non-positive + * @see <a href="http://reactivex.io/documentation/operators/to.html">ReactiveX documentation: To</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Iterable<T> blockingIterable(int capacityHint) { + ObjectHelper.verifyPositive(capacityHint, "capacityHint"); + return new BlockingObservableIterable<>(this, capacityHint); + } + + /** + * Returns the last item emitted by the current {@code Observable}, or throws + * {@link NoSuchElementException} if the current {@code Observable} emits no items. + * <p> + * <img width="640" height="315" src="https://github.com/ReactiveX/RxJava/wiki/images/rx-operators/blockingLast.o.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code blockingLast} does not operate by default on a particular {@link Scheduler}.</dd> + * <dt><b>Error handling:</b></dt> + * <dd>If the source signals an error, the operator wraps a checked {@link Exception} + * into {@link RuntimeException} and throws that. Otherwise, {@code RuntimeException}s and + * {@link Error}s are rethrown as they are.</dd> + * </dl> + * + * @return the last item emitted by the current {@code Observable} + * @throws NoSuchElementException + * if the current {@code Observable} emits no items + * @see <a href="http://reactivex.io/documentation/operators/last.html">ReactiveX documentation: Last</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final T blockingLast() { + BlockingLastObserver<T> observer = new BlockingLastObserver<>(); + subscribe(observer); + T v = observer.blockingGet(); + if (v != null) { + return v; + } + throw new NoSuchElementException(); + } + + /** + * Returns the last item emitted by the current {@code Observable}, or a default value if it emits no + * items. + * <p> + * <img width="640" height="310" src="https://github.com/ReactiveX/RxJava/wiki/images/rx-operators/blockingLastDefault.o.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code blockingLast} does not operate by default on a particular {@link Scheduler}.</dd> + * <dt><b>Error handling:</b></dt> + * <dd>If the source signals an error, the operator wraps a checked {@link Exception} + * into {@link RuntimeException} and throws that. Otherwise, {@code RuntimeException}s and + * {@link Error}s are rethrown as they are.</dd> + * </dl> + * + * @param defaultItem + * a default value to return if the current {@code Observable} emits no items + * @return the last item emitted by the {@code Observable}, or the default value if it emits no + * items + * @throws NullPointerException if {@code defaultItem} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/last.html">ReactiveX documentation: Last</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final T blockingLast(@NonNull T defaultItem) { + Objects.requireNonNull(defaultItem, "defaultItem is null"); + BlockingLastObserver<T> observer = new BlockingLastObserver<>(); + subscribe(observer); + T v = observer.blockingGet(); + return v != null ? v : defaultItem; + } + + /** + * Returns an {@link Iterable} that returns the latest item emitted by the current {@code Observable}, + * waiting if necessary for one to become available. + * <p> + * <img width="640" height="350" src="https://github.com/ReactiveX/RxJava/wiki/images/rx-operators/blockingLatest.o.png" alt=""> + * <p> + * If the current {@code Observable} produces items faster than {@code Iterator.next} takes them, + * {@code onNext} events might be skipped, but {@code onError} or {@code onComplete} events are not. + * <p> + * Note also that an {@code onNext} directly followed by {@code onComplete} might hide the {@code onNext} + * event. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code blockingLatest} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @return the new {@code Iterable} instance + * @see <a href="http://reactivex.io/documentation/operators/first.html">ReactiveX documentation: First</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Iterable<T> blockingLatest() { + return new BlockingObservableLatest<>(this); + } + + /** + * Returns an {@link Iterable} that always returns the item most recently emitted by the current + * {@code Observable}. + * <p> + * <img width="640" height="426" src="https://github.com/ReactiveX/RxJava/wiki/images/rx-operators/blockingMostRecent.o.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code blockingMostRecent} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param initialItem + * the initial value that the {@code Iterable} sequence will yield if the current + * {@code Observable} has not yet emitted an item + * @return the new {@code Iterable} instance + * @throws NullPointerException if {@code initialItem} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/first.html">ReactiveX documentation: First</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Iterable<T> blockingMostRecent(@NonNull T initialItem) { + Objects.requireNonNull(initialItem, "initialItem is null"); + return new BlockingObservableMostRecent<>(this, initialItem); + } + + /** + * Returns an {@link Iterable} that blocks until the current {@code Observable} emits another item, then + * returns that item. + * <p> + * <img width="640" height="427" src="https://github.com/ReactiveX/RxJava/wiki/images/rx-operators/blockingNext.o.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code blockingNext} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @return the new {@code Iterable} instance + * @see <a href="http://reactivex.io/documentation/operators/takelast.html">ReactiveX documentation: TakeLast</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Iterable<T> blockingNext() { + return new BlockingObservableNext<>(this); + } + + /** + * If the current {@code Observable} completes after emitting a single item, return that item, otherwise + * throw a {@link NoSuchElementException}. + * <p> + * <img width="640" height="315" src="https://github.com/ReactiveX/RxJava/wiki/images/rx-operators/blockingSingle.o.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code blockingSingle} does not operate by default on a particular {@link Scheduler}.</dd> + * <dt><b>Error handling:</b></dt> + * <dd>If the source signals an error, the operator wraps a checked {@link Exception} + * into {@link RuntimeException} and throws that. Otherwise, {@code RuntimeException}s and + * {@link Error}s are rethrown as they are.</dd> + * </dl> + * + * @return the single item emitted by the current {@code Observable} + * @see <a href="http://reactivex.io/documentation/operators/first.html">ReactiveX documentation: First</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final T blockingSingle() { + T v = singleElement().blockingGet(); + if (v == null) { + throw new NoSuchElementException(); + } + return v; + } + + /** + * If the current {@code Observable} completes after emitting a single item, return that item; if it emits + * more than one item, throw an {@link IllegalArgumentException}; if it emits no items, return a default + * value. + * <p> + * <img width="640" height="315" src="https://github.com/ReactiveX/RxJava/wiki/images/rx-operators/blockingSingleDefault.o.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code blockingSingle} does not operate by default on a particular {@link Scheduler}.</dd> + * <dt><b>Error handling:</b></dt> + * <dd>If the source signals an error, the operator wraps a checked {@link Exception} + * into {@link RuntimeException} and throws that. Otherwise, {@code RuntimeException}s and + * {@link Error}s are rethrown as they are.</dd> + * </dl> + * + * @param defaultItem + * a default value to return if the current {@code Observable} emits no items + * @return the single item emitted by the current {@code Observable}, or the default value if it emits no + * items + * @throws NullPointerException if {@code defaultItem} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/first.html">ReactiveX documentation: First</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final T blockingSingle(@NonNull T defaultItem) { + return single(defaultItem).blockingGet(); + } + + /** + * Returns a {@link Future} representing the only value emitted by the current {@code Observable}. + * <p> + * <img width="640" height="299" src="https://github.com/ReactiveX/RxJava/wiki/images/rx-operators/toFuture.o.png" alt=""> + * <p> + * If the {@code Observable} emits more than one item, {@code Future} will receive an + * {@link IndexOutOfBoundsException}. If the {@code Observable} is empty, {@code Future} + * will receive an {@link NoSuchElementException}. The {@code Observable} source has to terminate in order + * for the returned {@code Future} to terminate as well. + * <p> + * If the {@code Observable} may emit more than one item, use {@code Observable.toList().toFuture()}. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code toFuture} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @return the new {@code Future} instance + * @see <a href="http://reactivex.io/documentation/operators/to.html">ReactiveX documentation: To</a> + * @see #singleOrErrorStage() + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Future<T> toFuture() { + return subscribeWith(new FutureObserver<>()); + } + + /** + * Runs the current {@code Observable} to a terminal event, ignoring any values and rethrowing any exception. + * <p> + * <img width="640" height="270" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/blockingSubscribe.o.0.png" alt=""> + * <p> + * Note that calling this method will block the caller thread until the upstream terminates + * normally or with an error. Therefore, calling this method from special threads such as the + * Android Main Thread or the Swing Event Dispatch Thread is not recommended. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code blockingSubscribe} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @since 2.0 + * @see #blockingSubscribe(Consumer) + * @see #blockingSubscribe(Consumer, Consumer) + * @see #blockingSubscribe(Consumer, Consumer, Action) + */ + @SchedulerSupport(SchedulerSupport.NONE) + public final void blockingSubscribe() { + ObservableBlockingSubscribe.subscribe(this); + } + + /** + * Subscribes to the source and calls the given callbacks <strong>on the current thread</strong>. + * <p> + * <img width="640" height="394" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/blockingSubscribe.o.1.png" alt=""> + * <p> + * If the {@code Observable} emits an error, it is wrapped into an + * {@link OnErrorNotImplementedException} + * and routed to the {@link RxJavaPlugins#onError(Throwable)} handler. + * Using the overloads {@link #blockingSubscribe(Consumer, Consumer)} + * or {@link #blockingSubscribe(Consumer, Consumer, Action)} instead is recommended. + * <p> + * Note that calling this method will block the caller thread until the upstream terminates + * normally or with an error. Therefore, calling this method from special threads such as the + * Android Main Thread or the Swing Event Dispatch Thread is not recommended. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code blockingSubscribe} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param onNext the callback action for each source value + * @throws NullPointerException if {@code onNext} is {@code null} + * @since 2.0 + * @see #blockingSubscribe(Consumer, Consumer) + * @see #blockingSubscribe(Consumer, Consumer, Action) + */ + @SchedulerSupport(SchedulerSupport.NONE) + public final void blockingSubscribe(@NonNull Consumer<? super T> onNext) { + ObservableBlockingSubscribe.subscribe(this, onNext, Functions.ON_ERROR_MISSING, Functions.EMPTY_ACTION); + } + + /** + * Subscribes to the source and calls the given callbacks <strong>on the current thread</strong>. + * <p> + * <img width="640" height="397" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/blockingSubscribe.o.2.png" alt=""> + * <p> + * Note that calling this method will block the caller thread until the upstream terminates + * normally or with an error. Therefore, calling this method from special threads such as the + * Android Main Thread or the Swing Event Dispatch Thread is not recommended. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code blockingSubscribe} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param onNext the callback action for each source value + * @param onError the callback action for an error event + * @throws NullPointerException if {@code onNext} or {@code onError} is {@code null} + * @since 2.0 + * @see #blockingSubscribe(Consumer, Consumer, Action) + */ + @SchedulerSupport(SchedulerSupport.NONE) + public final void blockingSubscribe(@NonNull Consumer<? super T> onNext, @NonNull Consumer<? super Throwable> onError) { + ObservableBlockingSubscribe.subscribe(this, onNext, onError, Functions.EMPTY_ACTION); + } + + /** + * Subscribes to the source and calls the given callbacks <strong>on the current thread</strong>. + * <p> + * <img width="640" height="394" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/blockingSubscribe.o.png" alt=""> + * <p> + * Note that calling this method will block the caller thread until the upstream terminates + * normally or with an error. Therefore, calling this method from special threads such as the + * Android Main Thread or the Swing Event Dispatch Thread is not recommended. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code blockingSubscribe} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param onNext the callback action for each source value + * @param onError the callback action for an error event + * @param onComplete the callback action for the completion event. + * @throws NullPointerException if {@code onNext}, {@code onError} or {@code onComplete} is {@code null} + * @since 2.0 + */ + @SchedulerSupport(SchedulerSupport.NONE) + public final void blockingSubscribe(@NonNull Consumer<? super T> onNext, @NonNull Consumer<? super Throwable> onError, @NonNull Action onComplete) { + ObservableBlockingSubscribe.subscribe(this, onNext, onError, onComplete); + } + + /** + * Subscribes to the source and calls the {@link Observer} methods <strong>on the current thread</strong>. + * <p> + * Note that calling this method will block the caller thread until the upstream terminates + * normally, with an error or the {@code Observer} disposes the {@link Disposable} it receives via + * {@link Observer#onSubscribe(Disposable)}. + * Therefore, calling this method from special threads such as the + * Android Main Thread or the Swing Event Dispatch Thread is not recommended. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code blockingSubscribe} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * The a dispose() call is composed through. + * @param observer the {@code Observer} instance to forward events and calls to in the current thread + * @throws NullPointerException if {@code observer} is {@code null} + * @since 2.0 + */ + @SchedulerSupport(SchedulerSupport.NONE) + public final void blockingSubscribe(@NonNull Observer<? super T> observer) { + Objects.requireNonNull(observer, "observer is null"); + ObservableBlockingSubscribe.subscribe(this, observer); + } + + /** + * Returns an {@code Observable} that emits buffers of items it collects from the current {@code Observable}. The resulting + * {@code Observable} emits connected, non-overlapping buffers, each containing {@code count} items. When the current + * {@code Observable} completes, the resulting {@code Observable} emits the current buffer and propagates the notification + * from the current {@code Observable}. Note that if the current {@code Observable} issues an {@code onError} notification + * the event is passed on immediately without first emitting the buffer it is in the process of assembling. + * <p> + * <img width="640" height="320" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/buffer3.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>This version of {@code buffer} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param count + * the maximum number of items in each buffer before it should be emitted + * @return the new {@code Observable} instance + * @throws IllegalArgumentException if {@code count} is non-positive + * @see <a href="http://reactivex.io/documentation/operators/buffer.html">ReactiveX operators documentation: Buffer</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Observable<@NonNull List<T>> buffer(int count) { + return buffer(count, count); + } + + /** + * Returns an {@code Observable} that emits buffers of items it collects from the current {@code Observable}. The resulting + * {@code Observable} emits buffers every {@code skip} items, each containing {@code count} items. When the current + * {@code Observable} completes, the resulting {@code Observable} emits the current buffer and propagates the notification + * from the current {@code Observable}. Note that if the current {@code Observable} issues an {@code onError} notification + * the event is passed on immediately without first emitting the buffer it is in the process of assembling. + * <p> + * <img width="640" height="320" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/buffer4.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>This version of {@code buffer} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param count + * the maximum size of each buffer before it should be emitted + * @param skip + * how many items emitted by the current {@code Observable} should be skipped before starting a new + * buffer. Note that when {@code skip} and {@code count} are equal, this is the same operation as + * {@link #buffer(int)}. + * @return the new {@code Observable} instance + * @throws IllegalArgumentException if {@code count} or {@code skip} is non-positive + * @see <a href="http://reactivex.io/documentation/operators/buffer.html">ReactiveX operators documentation: Buffer</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Observable<@NonNull List<T>> buffer(int count, int skip) { + return buffer(count, skip, ArrayListSupplier.asSupplier()); + } + + /** + * Returns an {@code Observable} that emits buffers of items it collects from the current {@code Observable}. The resulting + * {@code Observable} emits buffers every {@code skip} items, each containing {@code count} items. When the current + * {@code Observable} completes, the resulting {@code Observable} emits the current buffer and propagates the notification + * from the current {@code Observable}. Note that if the current {@code Observable} issues an {@code onError} notification + * the event is passed on immediately without first emitting the buffer it is in the process of assembling. + * <p> + * <img width="640" height="320" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/buffer4.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>This version of {@code buffer} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <U> the collection subclass type to buffer into + * @param count + * the maximum size of each buffer before it should be emitted + * @param skip + * how many items emitted by the current {@code Observable} should be skipped before starting a new + * buffer. Note that when {@code skip} and {@code count} are equal, this is the same operation as + * {@link #buffer(int)}. + * @param bufferSupplier + * a factory function that returns an instance of the collection subclass to be used and returned + * as the buffer + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code bufferSupplier} is {@code null} + * @throws IllegalArgumentException if {@code count} or {@code skip} is non-positive + * @see <a href="http://reactivex.io/documentation/operators/buffer.html">ReactiveX operators documentation: Buffer</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull U extends Collection<? super T>> Observable<U> buffer(int count, int skip, @NonNull Supplier<U> bufferSupplier) { + ObjectHelper.verifyPositive(count, "count"); + ObjectHelper.verifyPositive(skip, "skip"); + Objects.requireNonNull(bufferSupplier, "bufferSupplier is null"); + return RxJavaPlugins.onAssembly(new ObservableBuffer<>(this, count, skip, bufferSupplier)); + } + + /** + * Returns an {@code Observable} that emits buffers of items it collects from the current {@code Observable}. The resulting + * {@code Observable} emits connected, non-overlapping buffers, each containing {@code count} items. When the current + * {@code Observable} completes, the resulting {@code Observable} emits the current buffer and propagates the notification + * from the current {@code Observable}. Note that if the current {@code Observable} issues an {@code onError} notification + * the event is passed on immediately without first emitting the buffer it is in the process of assembling. + * <p> + * <img width="640" height="320" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/buffer3.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>This version of {@code buffer} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <U> the collection subclass type to buffer into + * @param count + * the maximum number of items in each buffer before it should be emitted + * @param bufferSupplier + * a factory function that returns an instance of the collection subclass to be used and returned + * as the buffer + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code bufferSupplier} is {@code null} + * @throws IllegalArgumentException if {@code count} is non-positive + * @see <a href="http://reactivex.io/documentation/operators/buffer.html">ReactiveX operators documentation: Buffer</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull U extends Collection<? super T>> Observable<U> buffer(int count, @NonNull Supplier<U> bufferSupplier) { + return buffer(count, count, bufferSupplier); + } + + /** + * Returns an {@code Observable} that emits buffers of items it collects from the current {@code Observable}. The resulting + * {@code Observable} starts a new buffer periodically, as determined by the {@code timeskip} argument. It emits + * each buffer after a fixed timespan, specified by the {@code timespan} argument. When the current + * {@code Observable} completes, the resulting {@code Observable} emits the current buffer and propagates the notification + * from the current {@code Observable}. Note that if the current {@code Observable} issues an {@code onError} notification + * the event is passed on immediately without first emitting the buffer it is in the process of assembling. + * <p> + * <img width="640" height="320" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/buffer7.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>This version of {@code buffer} operates by default on the {@code computation} {@link Scheduler}.</dd> + * </dl> + * + * @param timespan + * the period of time each buffer collects items before it is emitted + * @param timeskip + * the period of time after which a new buffer will be created + * @param unit + * the unit of time that applies to the {@code timespan} and {@code timeskip} arguments + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code unit} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/buffer.html">ReactiveX operators documentation: Buffer</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.COMPUTATION) + @NonNull + public final Observable<@NonNull List<T>> buffer(long timespan, long timeskip, @NonNull TimeUnit unit) { + return buffer(timespan, timeskip, unit, Schedulers.computation(), ArrayListSupplier.asSupplier()); + } + + /** + * Returns an {@code Observable} that emits buffers of items it collects from the current {@code Observable}. The resulting + * {@code Observable} starts a new buffer periodically, as determined by the {@code timeskip} argument, and on the + * specified {@code scheduler}. It emits each buffer after a fixed timespan, specified by the + * {@code timespan} argument. When the current {@code Observable} completes, the resulting {@code Observable} emits the + * current buffer and propagates the notification from the current {@code Observable}. Note that if the current + * {@code Observable} issues an {@code onError} notification the event is passed on immediately without first emitting the + * buffer it is in the process of assembling. + * <p> + * <img width="640" height="320" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/buffer7.s.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>You specify which {@link Scheduler} this operator will use.</dd> + * </dl> + * + * @param timespan + * the period of time each buffer collects items before it is emitted + * @param timeskip + * the period of time after which a new buffer will be created + * @param unit + * the unit of time that applies to the {@code timespan} and {@code timeskip} arguments + * @param scheduler + * the {@code Scheduler} to use when determining the end and start of a buffer + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code unit} or {@code scheduler} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/buffer.html">ReactiveX operators documentation: Buffer</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.CUSTOM) + @NonNull + public final Observable<@NonNull List<T>> buffer(long timespan, long timeskip, @NonNull TimeUnit unit, @NonNull Scheduler scheduler) { + return buffer(timespan, timeskip, unit, scheduler, ArrayListSupplier.asSupplier()); + } + + /** + * Returns an {@code Observable} that emits buffers of items it collects from the current {@code Observable}. The resulting + * {@code Observable} starts a new buffer periodically, as determined by the {@code timeskip} argument, and on the + * specified {@code scheduler}. It emits each buffer after a fixed timespan, specified by the + * {@code timespan} argument. When the current {@code Observable} completes, the resulting {@code Observable} emits the + * current buffer and propagates the notification from the current {@code Observable}. Note that if the current + * {@code Observable} issues an {@code onError} notification the event is passed on immediately without first emitting the + * buffer it is in the process of assembling. + * <p> + * <img width="640" height="320" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/buffer7.s.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>You specify which {@link Scheduler} this operator will use.</dd> + * </dl> + * + * @param <U> the collection subclass type to buffer into + * @param timespan + * the period of time each buffer collects items before it is emitted + * @param timeskip + * the period of time after which a new buffer will be created + * @param unit + * the unit of time that applies to the {@code timespan} and {@code timeskip} arguments + * @param scheduler + * the {@code Scheduler} to use when determining the end and start of a buffer + * @param bufferSupplier + * a factory function that returns an instance of the collection subclass to be used and returned + * as the buffer + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code unit}, {@code scheduler} or {@code bufferSupplier} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/buffer.html">ReactiveX operators documentation: Buffer</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.CUSTOM) + @NonNull + public final <@NonNull U extends Collection<? super T>> Observable<U> buffer(long timespan, long timeskip, @NonNull TimeUnit unit, @NonNull Scheduler scheduler, @NonNull Supplier<U> bufferSupplier) { + Objects.requireNonNull(unit, "unit is null"); + Objects.requireNonNull(scheduler, "scheduler is null"); + Objects.requireNonNull(bufferSupplier, "bufferSupplier is null"); + return RxJavaPlugins.onAssembly(new ObservableBufferTimed<>(this, timespan, timeskip, unit, scheduler, bufferSupplier, Integer.MAX_VALUE, false)); + } + + /** + * Returns an {@code Observable} that emits buffers of items it collects from the current {@code Observable}. The resulting + * {@code Observable} emits connected, non-overlapping buffers, each of a fixed duration specified by the + * {@code timespan} argument. When the current {@code Observable} completes, the resulting {@code Observable} emits the + * current buffer and propagates the notification from the current {@code Observable}. Note that if the current + * {@code Observable} issues an {@code onError} notification the event is passed on immediately without first emitting the + * buffer it is in the process of assembling. + * <p> + * <img width="640" height="320" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/buffer5.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>This version of {@code buffer} operates by default on the {@code computation} {@link Scheduler}.</dd> + * </dl> + * + * @param timespan + * the period of time each buffer collects items before it is emitted and replaced with a new + * buffer + * @param unit + * the unit of time that applies to the {@code timespan} argument + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code unit} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/buffer.html">ReactiveX operators documentation: Buffer</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.COMPUTATION) + @NonNull + public final Observable<@NonNull List<T>> buffer(long timespan, @NonNull TimeUnit unit) { + return buffer(timespan, unit, Schedulers.computation(), Integer.MAX_VALUE); + } + + /** + * Returns an {@code Observable} that emits buffers of items it collects from the current {@code Observable}. The resulting + * {@code Observable} emits connected, non-overlapping buffers, each of a fixed duration specified by the + * {@code timespan} argument or a maximum size specified by the {@code count} argument (whichever is reached + * first). When the current {@code Observable} completes, the resulting {@code Observable} emits the current buffer and + * propagates the notification from the current {@code Observable}. Note that if the current {@code Observable} issues an + * {@code onError} notification the event is passed on immediately without first emitting the buffer it is in the process of + * assembling. + * <p> + * <img width="640" height="320" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/buffer6.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>This version of {@code buffer} operates by default on the {@code computation} {@link Scheduler}.</dd> + * </dl> + * + * @param timespan + * the period of time each buffer collects items before it is emitted and replaced with a new + * buffer + * @param unit + * the unit of time which applies to the {@code timespan} argument + * @param count + * the maximum size of each buffer before it is emitted + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code unit} is {@code null} + * @throws IllegalArgumentException if {@code count} is non-positive + * @see <a href="http://reactivex.io/documentation/operators/buffer.html">ReactiveX operators documentation: Buffer</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.COMPUTATION) + @NonNull + public final Observable<@NonNull List<T>> buffer(long timespan, @NonNull TimeUnit unit, int count) { + return buffer(timespan, unit, Schedulers.computation(), count); + } + + /** + * Returns an {@code Observable} that emits buffers of items it collects from the current {@code Observable}. The resulting + * {@code Observable} emits connected, non-overlapping buffers, each of a fixed duration specified by the + * {@code timespan} argument as measured on the specified {@code scheduler}, or a maximum size specified by + * the {@code count} argument (whichever is reached first). When the current {@code Observable} completes, the resulting + * {@code Observable} emits the current buffer and propagates the notification from the current {@code Observable}. Note + * that if the current {@code Observable} issues an {@code onError} notification the event is passed on immediately without + * first emitting the buffer it is in the process of assembling. + * <p> + * <img width="640" height="320" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/buffer6.s.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>You specify which {@link Scheduler} this operator will use.</dd> + * </dl> + * + * @param timespan + * the period of time each buffer collects items before it is emitted and replaced with a new + * buffer + * @param unit + * the unit of time which applies to the {@code timespan} argument + * @param scheduler + * the {@code Scheduler} to use when determining the end and start of a buffer + * @param count + * the maximum size of each buffer before it is emitted + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code unit} or {@code scheduler} is {@code null} + * @throws IllegalArgumentException if {@code count} is non-positive + * @see <a href="http://reactivex.io/documentation/operators/buffer.html">ReactiveX operators documentation: Buffer</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.CUSTOM) + @NonNull + public final Observable<@NonNull List<T>> buffer(long timespan, @NonNull TimeUnit unit, @NonNull Scheduler scheduler, int count) { + return buffer(timespan, unit, scheduler, count, ArrayListSupplier.asSupplier(), false); + } + + /** + * Returns an {@code Observable} that emits buffers of items it collects from the current {@code Observable}. The resulting + * {@code Observable} emits connected, non-overlapping buffers, each of a fixed duration specified by the + * {@code timespan} argument as measured on the specified {@code scheduler}, or a maximum size specified by + * the {@code count} argument (whichever is reached first). When the current {@code Observable} completes, the resulting + * {@code Observable} emits the current buffer and propagates the notification from the current {@code Observable}. Note + * that if the current {@code Observable} issues an {@code onError} notification the event is passed on immediately without + * first emitting the buffer it is in the process of assembling. + * <p> + * <img width="640" height="320" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/buffer6.s.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>You specify which {@link Scheduler} this operator will use.</dd> + * </dl> + * + * @param <U> the collection subclass type to buffer into + * @param timespan + * the period of time each buffer collects items before it is emitted and replaced with a new + * buffer + * @param unit + * the unit of time which applies to the {@code timespan} argument + * @param scheduler + * the {@code Scheduler} to use when determining the end and start of a buffer + * @param count + * the maximum size of each buffer before it is emitted + * @param bufferSupplier + * a factory function that returns an instance of the collection subclass to be used and returned + * as the buffer + * @param restartTimerOnMaxSize if {@code true}, the time window is restarted when the max capacity of the current buffer + * is reached + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code unit}, {@code scheduler} or {@code bufferSupplier} is {@code null} + * @throws IllegalArgumentException if {@code count} is non-positive + * @see <a href="http://reactivex.io/documentation/operators/buffer.html">ReactiveX operators documentation: Buffer</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.CUSTOM) + @NonNull + public final <@NonNull U extends Collection<? super T>> Observable<U> buffer( + long timespan, @NonNull TimeUnit unit, + @NonNull Scheduler scheduler, int count, + @NonNull Supplier<U> bufferSupplier, + boolean restartTimerOnMaxSize) { + Objects.requireNonNull(unit, "unit is null"); + Objects.requireNonNull(scheduler, "scheduler is null"); + Objects.requireNonNull(bufferSupplier, "bufferSupplier is null"); + ObjectHelper.verifyPositive(count, "count"); + return RxJavaPlugins.onAssembly(new ObservableBufferTimed<>(this, timespan, timespan, unit, scheduler, bufferSupplier, count, restartTimerOnMaxSize)); + } + + /** + * Returns an {@code Observable} that emits buffers of items it collects from the current {@code Observable}. The resulting + * {@code Observable} emits connected, non-overlapping buffers, each of a fixed duration specified by the + * {@code timespan} argument and on the specified {@code scheduler}. When the current {@code Observable} completes, + * the resulting {@code Observable} emits the current buffer and propagates the notification from the current + * {@code Observable}. Note that if the current {@code Observable} issues an {@code onError} notification the event is passed on + * immediately without first emitting the buffer it is in the process of assembling. + * <p> + * <img width="640" height="320" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/buffer5.s.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>You specify which {@link Scheduler} this operator will use.</dd> + * </dl> + * + * @param timespan + * the period of time each buffer collects items before it is emitted and replaced with a new + * buffer + * @param unit + * the unit of time which applies to the {@code timespan} argument + * @param scheduler + * the {@code Scheduler} to use when determining the end and start of a buffer + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code unit} or {@code scheduler} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/buffer.html">ReactiveX operators documentation: Buffer</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.CUSTOM) + @NonNull + public final Observable<@NonNull List<T>> buffer(long timespan, @NonNull TimeUnit unit, @NonNull Scheduler scheduler) { + return buffer(timespan, unit, scheduler, Integer.MAX_VALUE, ArrayListSupplier.asSupplier(), false); + } + + /** + * Returns an {@code Observable} that emits buffers of items it collects from the current {@code Observable}. The resulting + * {@code Observable} emits buffers that it creates when the specified {@code openingIndicator} {@link ObservableSource} emits an + * item, and closes when the {@code ObservableSource} returned from {@code closingIndicator} emits an item. If any of the + * current {@code Observable}, {@code openingIndicator} or {@code closingIndicator} issues an {@code onError} notification the + * event is passed on immediately without first emitting the buffer it is in the process of assembling. + * <p> + * <img width="640" height="470" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/buffer2.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>This version of {@code buffer} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <TOpening> the element type of the buffer-opening {@code ObservableSource} + * @param <TClosing> the element type of the individual buffer-closing {@code ObservableSource}s + * @param openingIndicator + * the {@code ObservableSource} that, when it emits an item, causes a new buffer to be created + * @param closingIndicator + * the {@link Function} that is used to produce an {@code ObservableSource} for every buffer created. When this indicator + * {@code ObservableSource} emits an item, the associated buffer is emitted. + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code openingIndicator} or {@code closingIndicator} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/buffer.html">ReactiveX operators documentation: Buffer</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull TOpening, @NonNull TClosing> Observable<@NonNull List<T>> buffer( + @NonNull ObservableSource<? extends TOpening> openingIndicator, + @NonNull Function<? super TOpening, ? extends ObservableSource<? extends TClosing>> closingIndicator) { + return buffer(openingIndicator, closingIndicator, ArrayListSupplier.asSupplier()); + } + + /** + * Returns an {@code Observable} that emits buffers of items it collects from the current {@code Observable}. The resulting + * {@code Observable} emits buffers that it creates when the specified {@code openingIndicator} {@link ObservableSource} emits an + * item, and closes when the {@code ObservableSource} returned from {@code closingIndicator} emits an item. If any of the + * current {@code Observable}, {@code openingIndicator} or {@code closingIndicator} issues an {@code onError} notification the + * event is passed on immediately without first emitting the buffer it is in the process of assembling. + * <p> + * <img width="640" height="470" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/buffer2.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>This version of {@code buffer} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <U> the collection subclass type to buffer into + * @param <TOpening> the element type of the buffer-opening {@code ObservableSource} + * @param <TClosing> the element type of the individual buffer-closing {@code ObservableSource}s + * @param openingIndicator + * the {@code ObservableSource} that, when it emits an item, causes a new buffer to be created + * @param closingIndicator + * the {@link Function} that is used to produce an {@code ObservableSource} for every buffer created. When this indicator + * {@code ObservableSource} emits an item, the associated buffer is emitted. + * @param bufferSupplier + * a factory function that returns an instance of the collection subclass to be used and returned + * as the buffer + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code openingIndicator}, {@code closingIndicator} or {@code bufferSupplier} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/buffer.html">ReactiveX operators documentation: Buffer</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull TOpening, @NonNull TClosing, @NonNull U extends Collection<? super T>> Observable<U> buffer( + @NonNull ObservableSource<? extends TOpening> openingIndicator, + @NonNull Function<? super TOpening, ? extends ObservableSource<? extends TClosing>> closingIndicator, + @NonNull Supplier<U> bufferSupplier) { + Objects.requireNonNull(openingIndicator, "openingIndicator is null"); + Objects.requireNonNull(closingIndicator, "closingIndicator is null"); + Objects.requireNonNull(bufferSupplier, "bufferSupplier is null"); + return RxJavaPlugins.onAssembly(new ObservableBufferBoundary<T, U, TOpening, TClosing>(this, openingIndicator, closingIndicator, bufferSupplier)); + } + + /** + * Returns an {@code Observable} that emits non-overlapping buffered items from the current {@code Observable} each time the + * specified boundary {@link ObservableSource} emits an item. + * <p> + * <img width="640" height="395" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/buffer8.v3.png" alt=""> + * <p> + * Completion of either the source or the boundary {@code ObservableSource} causes the returned {@code ObservableSource} to emit the + * latest buffer and complete. If either the current {@code Observable} or the boundary {@code ObservableSource} issues an + * {@code onError} notification the event is passed on immediately without first emitting the buffer it is in the process of + * assembling. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>This version of {@code buffer} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <B> + * the boundary value type (ignored) + * @param boundaryIndicator + * the boundary {@code ObservableSource} + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code boundaryIndicator} is {@code null} + * @see #buffer(ObservableSource, int) + * @see <a href="http://reactivex.io/documentation/operators/buffer.html">ReactiveX operators documentation: Buffer</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull B> Observable<@NonNull List<T>> buffer(@NonNull ObservableSource<B> boundaryIndicator) { + return buffer(boundaryIndicator, ArrayListSupplier.asSupplier()); + } + + /** + * Returns an {@code Observable} that emits non-overlapping buffered items from the current {@code Observable} each time the + * specified boundary {@link ObservableSource} emits an item. + * <p> + * <img width="640" height="395" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/buffer8.v3.png" alt=""> + * <p> + * Completion of either the source or the boundary {@code ObservableSource} causes the returned {@code ObservableSource} to emit the + * latest buffer and complete. If either the current {@code Observable} or the boundary {@code ObservableSource} issues an + * {@code onError} notification the event is passed on immediately without first emitting the buffer it is in the process of + * assembling. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>This version of {@code buffer} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <B> + * the boundary value type (ignored) + * @param boundaryIndicator + * the boundary {@code ObservableSource} + * @param initialCapacity + * the initial capacity of each buffer chunk + * @return the new {@code Observable} instance + * @see <a href="http://reactivex.io/documentation/operators/buffer.html">ReactiveX operators documentation: Buffer</a> + * @throws NullPointerException if {@code boundaryIndicator} is {@code null} + * @throws IllegalArgumentException if {@code initialCapacity} is non-positive + * @see #buffer(ObservableSource) + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull B> Observable<@NonNull List<T>> buffer(@NonNull ObservableSource<B> boundaryIndicator, int initialCapacity) { + ObjectHelper.verifyPositive(initialCapacity, "initialCapacity"); + return buffer(boundaryIndicator, Functions.createArrayList(initialCapacity)); + } + + /** + * Returns an {@code Observable} that emits non-overlapping buffered items from the current {@code Observable} each time the + * specified boundary {@link ObservableSource} emits an item. + * <p> + * <img width="640" height="395" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/buffer8.v3.png" alt=""> + * <p> + * Completion of either the source or the boundary {@code ObservableSource} causes the returned {@code ObservableSource} to emit the + * latest buffer and complete. If either the current {@code Observable} or the boundary {@code ObservableSource} issues an + * {@code onError} notification the event is passed on immediately without first emitting the buffer it is in the process of + * assembling. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>This version of {@code buffer} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <U> the collection subclass type to buffer into + * @param <B> + * the boundary value type (ignored) + * @param boundaryIndicator + * the boundary {@code ObservableSource} + * @param bufferSupplier + * a factory function that returns an instance of the collection subclass to be used and returned + * as the buffer + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code boundaryIndicator} or {@code bufferSupplier} is {@code null} + * @see #buffer(ObservableSource, int) + * @see <a href="http://reactivex.io/documentation/operators/buffer.html">ReactiveX operators documentation: Buffer</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull B, @NonNull U extends Collection<? super T>> Observable<U> buffer(@NonNull ObservableSource<B> boundaryIndicator, @NonNull Supplier<U> bufferSupplier) { + Objects.requireNonNull(boundaryIndicator, "boundaryIndicator is null"); + Objects.requireNonNull(bufferSupplier, "bufferSupplier is null"); + return RxJavaPlugins.onAssembly(new ObservableBufferExactBoundary<>(this, boundaryIndicator, bufferSupplier)); + } + + /** + * Returns an {@code Observable} that subscribes to the current {@code Observable} lazily, caches all of its events + * and replays them, in the same order as received, to all the downstream observers. + * <p> + * <img width="640" height="410" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/cache.v3.png" alt=""> + * <p> + * This is useful when you want an {@code Observable} to cache responses and you can't control the + * subscribe/dispose behavior of all the {@link Observer}s. + * <p> + * The operator subscribes only when the first downstream observer subscribes and maintains + * a single subscription towards the current {@code Observable}. In contrast, the operator family of {@link #replay()} + * that return a {@link ConnectableObservable} require an explicit call to {@link ConnectableObservable#connect()}. + * <p> + * <em>Note:</em> You sacrifice the ability to dispose the origin when you use the {@code cache} + * operator so be careful not to use this operator on {@code Observable}s that emit an infinite or very large number + * of items that will use up memory. + * A possible workaround is to apply {@code takeUntil} with a predicate or + * another source before (and perhaps after) the application of {@code cache()}. + * <pre><code> + * AtomicBoolean shouldStop = new AtomicBoolean(); + * + * source.takeUntil(v -> shouldStop.get()) + * .cache() + * .takeUntil(v -> shouldStop.get()) + * .subscribe(...); + * </code></pre> + * Since the operator doesn't allow clearing the cached values either, the possible workaround is + * to forget all references to it via {@link #onTerminateDetach()} applied along with the previous + * workaround: + * <pre><code> + * AtomicBoolean shouldStop = new AtomicBoolean(); + * + * source.takeUntil(v -> shouldStop.get()) + * .onTerminateDetach() + * .cache() + * .takeUntil(v -> shouldStop.get()) + * .onTerminateDetach() + * .subscribe(...); + * </code></pre> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code cache} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @return the new {@code Observable} instance + * @see <a href="http://reactivex.io/documentation/operators/replay.html">ReactiveX operators documentation: Replay</a> + * @see #takeUntil(Predicate) + * @see #takeUntil(ObservableSource) + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Observable<T> cache() { + return cacheWithInitialCapacity(16); + } + + /** + * Returns an {@code Observable} that subscribes to the current {@code Observable} lazily, caches all of its events + * and replays them, in the same order as received, to all the downstream observers. + * <p> + * <img width="640" height="410" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/cacheWithInitialCapacity.o.v3.png" alt=""> + * <p> + * This is useful when you want an {@code Observable} to cache responses and you can't control the + * subscribe/dispose behavior of all the {@link Observer}s. + * <p> + * The operator subscribes only when the first downstream observer subscribes and maintains + * a single subscription towards the current {@code Observable}. In contrast, the operator family of {@link #replay()} + * that return a {@link ConnectableObservable} require an explicit call to {@link ConnectableObservable#connect()}. + * <p> + * <em>Note:</em> You sacrifice the ability to dispose the origin when you use the {@code cache} + * operator so be careful not to use this operator on {@code Observable}s that emit an infinite or very large number + * of items that will use up memory. + * A possible workaround is to apply `takeUntil` with a predicate or + * another source before (and perhaps after) the application of {@code cache()}. + * <pre><code> + * AtomicBoolean shouldStop = new AtomicBoolean(); + * + * source.takeUntil(v -> shouldStop.get()) + * .cache() + * .takeUntil(v -> shouldStop.get()) + * .subscribe(...); + * </code></pre> + * Since the operator doesn't allow clearing the cached values either, the possible workaround is + * to forget all references to it via {@link #onTerminateDetach()} applied along with the previous + * workaround: + * <pre><code> + * AtomicBoolean shouldStop = new AtomicBoolean(); + * + * source.takeUntil(v -> shouldStop.get()) + * .onTerminateDetach() + * .cache() + * .takeUntil(v -> shouldStop.get()) + * .onTerminateDetach() + * .subscribe(...); + * </code></pre> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code cacheWithInitialCapacity} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * <p> + * <em>Note:</em> The capacity hint is not an upper bound on cache size. For that, consider + * {@link #replay(int)} in combination with {@link ConnectableObservable#autoConnect()} or similar. + * + * @param initialCapacity hint for number of items to cache (for optimizing underlying data structure) + * @return the new {@code Observable} instance + * @throws IllegalArgumentException if {@code initialCapacity} is non-positive + * @see <a href="http://reactivex.io/documentation/operators/replay.html">ReactiveX operators documentation: Replay</a> + * @see #takeUntil(Predicate) + * @see #takeUntil(ObservableSource) + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Observable<T> cacheWithInitialCapacity(int initialCapacity) { + ObjectHelper.verifyPositive(initialCapacity, "initialCapacity"); + return RxJavaPlugins.onAssembly(new ObservableCache<>(this, initialCapacity)); + } + + /** + * Returns an {@code Observable} that emits the upstream items while + * they can be cast via {@link Class#cast(Object)} until the upstream terminates, + * or until the upstream signals an item which can't be cast, + * resulting in a {@link ClassCastException} to be signaled to the downstream. + * <p> + * <img width="640" height="305" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/cast.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code cast} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <U> the output value type cast to + * @param clazz + * the target class to use to try and cast the upstream items into + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code clazz} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/map.html">ReactiveX operators documentation: Map</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull U> Observable<U> cast(@NonNull Class<U> clazz) { + Objects.requireNonNull(clazz, "clazz is null"); + return map(Functions.castFunction(clazz)); + } + + /** + * Collects items emitted by the finite source {@code Observable} into a single mutable data structure and returns + * a {@link Single} that emits this structure. + * <p> + * <img width="640" height="330" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/collect.2.v3.png" alt=""> + * <p> + * This is a simplified version of {@code reduce} that does not need to return the state on each pass. + * <p> + * Note that this operator requires the upstream to signal {@code onComplete} for the accumulator object to + * be emitted. Sources that are infinite and never complete will never emit anything through this + * operator and an infinite source may lead to a fatal {@link OutOfMemoryError}. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code collect} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <U> the accumulator and output type + * @param initialItemSupplier + * the mutable data structure that will collect the items + * @param collector + * a function that accepts the {@code state} and an emitted item, and modifies the accumulator accordingly + * accordingly + * @return the new {@code Single} instance + * @throws NullPointerException if {@code initialItemSupplier} or {@code collector} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/reduce.html">ReactiveX operators documentation: Reduce</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull U> Single<U> collect(@NonNull Supplier<? extends U> initialItemSupplier, @NonNull BiConsumer<? super U, ? super T> collector) { + Objects.requireNonNull(initialItemSupplier, "initialItemSupplier is null"); + Objects.requireNonNull(collector, "collector is null"); + return RxJavaPlugins.onAssembly(new ObservableCollectSingle<>(this, initialItemSupplier, collector)); + } + + /** + * Collects items emitted by the finite source {@code Observable} into a single mutable data structure and returns + * a {@link Single} that emits this structure. + * <p> + * <img width="640" height="330" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/collectInto.o.v3.png" alt=""> + * <p> + * This is a simplified version of {@code reduce} that does not need to return the state on each pass. + * <p> + * Note that this operator requires the upstream to signal {@code onComplete} for the accumulator object to + * be emitted. Sources that are infinite and never complete will never emit anything through this + * operator and an infinite source may lead to a fatal {@link OutOfMemoryError}. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code collectInto} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <U> the accumulator and output type + * @param initialItem + * the mutable data structure that will collect the items + * @param collector + * a function that accepts the {@code state} and an emitted item, and modifies the accumulator accordingly + * accordingly + * @return the new {@code Single} instance + * @throws NullPointerException if {@code initialItem} or {@code collector} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/reduce.html">ReactiveX operators documentation: Reduce</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull U> Single<U> collectInto(@NonNull U initialItem, @NonNull BiConsumer<? super U, ? super T> collector) { + Objects.requireNonNull(initialItem, "initialItem is null"); + return collect(Functions.justSupplier(initialItem), collector); + } + + /** + * Transform the current {@code Observable} by applying a particular {@link ObservableTransformer} function to it. + * <p> + * This method operates on the {@code Observable} itself whereas {@link #lift} operates on the {@link ObservableSource}'s + * {@link Observer}s. + * <p> + * If the operator you are creating is designed to act on the individual items emitted by the current + * {@code Observable}, use {@link #lift}. If your operator is designed to transform the current {@code Observable} as a whole + * (for instance, by applying a particular set of existing RxJava operators to it) use {@code compose}. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code compose} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <R> the value type of the output {@code ObservableSource} + * @param composer implements the function that transforms the current {@code Observable} + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code composer} is {@code null} + * @see <a href="https://github.com/ReactiveX/RxJava/wiki/Implementing-Your-Own-Operators">RxJava wiki: Implementing Your Own Operators</a> + */ + @SuppressWarnings("unchecked") + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull R> Observable<R> compose(@NonNull ObservableTransformer<? super T, ? extends R> composer) { + return wrap(((ObservableTransformer<T, R>) Objects.requireNonNull(composer, "composer is null")).apply(this)); + } + + /** + * Returns a new {@code Observable} that emits items resulting from applying a function that you supply to each item + * emitted by the current {@code Observable}, where that function returns an {@link ObservableSource}, and then emitting the items + * that result from concatenating those returned {@code ObservableSource}s. + * <p> + * <img width="640" height="305" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/concatMap.v3.png" alt=""> + * <p> + * Note that there is no guarantee where the given {@code mapper} function will be executed; it could be on the subscribing thread, + * on the upstream thread signaling the new item to be mapped or on the thread where the inner source terminates. To ensure + * the {@code mapper} function is confined to a known thread, use the {@link #concatMap(Function, int, Scheduler)} overload. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code concatMap} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <R> the type of the inner {@code ObservableSource} sources and thus the output type + * @param mapper + * a function that, when applied to an item emitted by the current {@code Observable}, returns an + * {@code ObservableSource} + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code mapper} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/flatmap.html">ReactiveX operators documentation: FlatMap</a> + * @see #concatMap(Function, int, Scheduler) + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull R> Observable<R> concatMap(@NonNull Function<? super T, ? extends ObservableSource<? extends R>> mapper) { + return concatMap(mapper, 2); + } + + /** + * Returns a new {@code Observable} that emits items resulting from applying a function that you supply to each item + * emitted by the current {@code Observable}, where that function returns an {@link ObservableSource}, and then emitting the items + * that result from concatenating those returned {@code ObservableSource}s. + * <p> + * <img width="640" height="305" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/concatMap.v3.png" alt=""> + * <p> + * Note that there is no guarantee where the given {@code mapper} function will be executed; it could be on the subscribing thread, + * on the upstream thread signaling the new item to be mapped or on the thread where the inner source terminates. To ensure + * the {@code mapper} function is confined to a known thread, use the {@link #concatMap(Function, int, Scheduler)} overload. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code concatMap} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <R> the type of the inner {@code ObservableSource} sources and thus the output type + * @param mapper + * a function that, when applied to an item emitted by the current {@code Observable}, returns an + * {@code ObservableSource} + * @param bufferSize + * the number of elements expected from the current {@code Observable} to be buffered + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code mapper} is {@code null} + * @throws IllegalArgumentException if {@code bufferSize} is non-positive + * @see <a href="http://reactivex.io/documentation/operators/flatmap.html">ReactiveX operators documentation: FlatMap</a> + * @see #concatMap(Function, int, Scheduler) + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull R> Observable<R> concatMap(@NonNull Function<? super T, ? extends ObservableSource<? extends R>> mapper, int bufferSize) { + Objects.requireNonNull(mapper, "mapper is null"); + ObjectHelper.verifyPositive(bufferSize, "bufferSize"); + if (this instanceof ScalarSupplier) { + @SuppressWarnings("unchecked") + T v = ((ScalarSupplier<T>)this).get(); + if (v == null) { + return empty(); + } + return ObservableScalarXMap.scalarXMap(v, mapper); + } + return RxJavaPlugins.onAssembly(new ObservableConcatMap<>(this, mapper, bufferSize, ErrorMode.IMMEDIATE)); + } + + /** + * Returns a new {@code Observable} that emits items resulting from applying a function that you supply to each item + * emitted by the current {@code Observable}, where that function returns an {@link ObservableSource}, and then emitting the items + * that result from concatenating those returned {@code ObservableSource}s. + * <p> + * <img width="640" height="305" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/concatMap.v3.png" alt=""> + * <p> + * The difference between {@link #concatMap(Function, int)} and this operator is that this operator guarantees the {@code mapper} + * function is executed on the specified scheduler. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code concatMap} executes the given {@code mapper} function on the provided {@link Scheduler}.</dd> + * </dl> + * + * @param <R> the type of the inner {@code ObservableSource} sources and thus the output type + * @param mapper + * a function that, when applied to an item emitted by the current {@code Observable}, returns an + * {@code ObservableSource} + * @param bufferSize + * the number of elements expected from the current {@code Observable} to be buffered + * @param scheduler + * the scheduler where the {@code mapper} function will be executed + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code mapper} or {@code scheduler} is {@code null} + * @throws IllegalArgumentException if {@code bufferSize} is non-positive + * @since 3.0.0 + * @see <a href="http://reactivex.io/documentation/operators/flatmap.html">ReactiveX operators documentation: FlatMap</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.CUSTOM) + @NonNull + public final <@NonNull R> Observable<R> concatMap(@NonNull Function<? super T, ? extends ObservableSource<? extends R>> mapper, int bufferSize, @NonNull Scheduler scheduler) { + Objects.requireNonNull(mapper, "mapper is null"); + ObjectHelper.verifyPositive(bufferSize, "bufferSize"); + Objects.requireNonNull(scheduler, "scheduler is null"); + return RxJavaPlugins.onAssembly(new ObservableConcatMapScheduler<>(this, mapper, bufferSize, ErrorMode.IMMEDIATE, scheduler)); + } + + /** + * Maps each of the items into an {@link ObservableSource}, subscribes to them one after the other, + * one at a time and emits their values in order + * while delaying any error from either this or any of the inner {@code ObservableSource}s + * till all of them terminate. + * <p> + * <img width="640" height="348" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/concatMapDelayError.o.png" alt=""> + * <p> + * Note that there is no guarantee where the given {@code mapper} function will be executed; it could be on the subscribing thread, + * on the upstream thread signaling the new item to be mapped or on the thread where the inner source terminates. To ensure + * the {@code mapper} function is confined to a known thread, use the {@link #concatMapDelayError(Function, boolean, int, Scheduler)} overload. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code concatMapDelayError} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <R> the result value type + * @param mapper the function that maps the items of the current {@code Observable} into the inner {@code ObservableSource}s. + * @return the new {@code Observable} instance with the concatenation behavior + * @throws NullPointerException if {@code mapper} is {@code null} + * @see #concatMapDelayError(Function, boolean, int, Scheduler) + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull R> Observable<R> concatMapDelayError(@NonNull Function<? super T, ? extends ObservableSource<? extends R>> mapper) { + return concatMapDelayError(mapper, true, bufferSize()); + } + + /** + * Maps each of the items into an {@link ObservableSource}, subscribes to them one after the other, + * one at a time and emits their values in order + * while delaying any error from either this or any of the inner {@code ObservableSource}s + * till all of them terminate. + * <p> + * <img width="640" height="348" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/concatMapDelayError.o.png" alt=""> + * <p> + * Note that there is no guarantee where the given {@code mapper} function will be executed; it could be on the subscribing thread, + * on the upstream thread signaling the new item to be mapped or on the thread where the inner source terminates. To ensure + * the {@code mapper} function is confined to a known thread, use the {@link #concatMapDelayError(Function, boolean, int, Scheduler)} overload. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code concatMapDelayError} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <R> the result value type + * @param mapper the function that maps the items of the current {@code Observable} into the inner {@code ObservableSource}s. + * @param tillTheEnd + * if {@code true}, all errors from the outer and inner {@code ObservableSource} sources are delayed until the end, + * if {@code false}, an error from the main source is signaled when the current {@code Observable} source terminates + * @param bufferSize + * the number of elements expected from the current {@code Observable} to be buffered + * @return the new {@code Observable} instance with the concatenation behavior + * @throws NullPointerException if {@code mapper} is {@code null} + * @throws IllegalArgumentException if {@code bufferSize} is non-positive + * @see #concatMapDelayError(Function, boolean, int, Scheduler) + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull R> Observable<R> concatMapDelayError(@NonNull Function<? super T, ? extends ObservableSource<? extends R>> mapper, + boolean tillTheEnd, int bufferSize) { + Objects.requireNonNull(mapper, "mapper is null"); + ObjectHelper.verifyPositive(bufferSize, "bufferSize"); + if (this instanceof ScalarSupplier) { + @SuppressWarnings("unchecked") + T v = ((ScalarSupplier<T>)this).get(); + if (v == null) { + return empty(); + } + return ObservableScalarXMap.scalarXMap(v, mapper); + } + return RxJavaPlugins.onAssembly(new ObservableConcatMap<>(this, mapper, bufferSize, tillTheEnd ? ErrorMode.END : ErrorMode.BOUNDARY)); + } + + /** + * Maps each of the items into an {@link ObservableSource}, subscribes to them one after the other, + * one at a time and emits their values in order + * while delaying any error from either this or any of the inner {@code ObservableSource}s + * till all of them terminate. + * <p> + * <img width="640" height="348" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/concatMapDelayError.o.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code concatMapDelayError} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <R> the result value type + * @param mapper the function that maps the items of the current {@code Observable} into the inner {@code ObservableSource}s. + * @param tillTheEnd + * if {@code true}, all errors from the outer and inner {@code ObservableSource} sources are delayed until the end, + * if {@code false}, an error from the main source is signaled when the current {@code Observable} source terminates + * @param bufferSize + * the number of elements expected from the current {@code Observable} to be buffered + * @param scheduler + * the scheduler where the {@code mapper} function will be executed + * @return the new {@code Observable} instance with the concatenation behavior + * @throws NullPointerException if {@code mapper} or {@code scheduler} is {@code null} + * @throws IllegalArgumentException if {@code bufferSize} is non-positive + * @see #concatMapDelayError(Function, boolean, int) + * @since 3.0.0 + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.CUSTOM) + @NonNull + public final <@NonNull R> Observable<R> concatMapDelayError(@NonNull Function<? super T, ? extends ObservableSource<? extends R>> mapper, + boolean tillTheEnd, int bufferSize, @NonNull Scheduler scheduler) { + Objects.requireNonNull(mapper, "mapper is null"); + ObjectHelper.verifyPositive(bufferSize, "bufferSize"); + Objects.requireNonNull(scheduler, "scheduler is null"); + return RxJavaPlugins.onAssembly(new ObservableConcatMapScheduler<>(this, mapper, bufferSize, tillTheEnd ? ErrorMode.END : ErrorMode.BOUNDARY, scheduler)); + } + + /** + * Maps a sequence of values into {@link ObservableSource}s and concatenates these {@code ObservableSource}s eagerly into a single + * {@code Observable} sequence. + * <p> + * Eager concatenation means that once a subscriber subscribes, this operator subscribes to all of the + * current {@code Observable}s. The operator buffers the values emitted by these {@code ObservableSource}s and then drains them in + * order, each one after the previous one completes. + * <p> + * <img width="640" height="361" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/concatMapEager.o.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>This method does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <R> the value type + * @param mapper the function that maps a sequence of values into a sequence of {@code ObservableSource}s that will be + * eagerly concatenated + * @return the new {@code Observable} instance with the specified concatenation behavior + * @throws NullPointerException if {@code mapper} is {@code null} + * @since 2.0 + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull R> Observable<R> concatMapEager(@NonNull Function<? super T, ? extends ObservableSource<? extends R>> mapper) { + return concatMapEager(mapper, Integer.MAX_VALUE, bufferSize()); + } + + /** + * Maps a sequence of values into {@link ObservableSource}s and concatenates these {@code ObservableSource}s eagerly into a single + * {@code Observable} sequence. + * <p> + * Eager concatenation means that once a subscriber subscribes, this operator subscribes to all of the + * current {@code Observable}s. The operator buffers the values emitted by these {@code ObservableSource}s and then drains them in + * order, each one after the previous one completes. + * <p> + * <img width="640" height="361" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/concatMapEager.o.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>This method does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <R> the value type + * @param mapper the function that maps a sequence of values into a sequence of {@code ObservableSource}s that will be + * eagerly concatenated + * @param maxConcurrency the maximum number of concurrent subscribed {@code ObservableSource}s + * @param bufferSize hints about the number of expected items from each inner {@code ObservableSource}, must be positive + * @return the new {@code Observable} instance with the specified concatenation behavior + * @throws NullPointerException if {@code mapper} is {@code null} + * @throws IllegalArgumentException if {@code maxConcurrency} or {@code bufferSize} is non-positive + * @since 2.0 + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull R> Observable<R> concatMapEager(@NonNull Function<? super T, ? extends ObservableSource<? extends R>> mapper, + int maxConcurrency, int bufferSize) { + Objects.requireNonNull(mapper, "mapper is null"); + ObjectHelper.verifyPositive(maxConcurrency, "maxConcurrency"); + ObjectHelper.verifyPositive(bufferSize, "bufferSize"); + return RxJavaPlugins.onAssembly(new ObservableConcatMapEager<>(this, mapper, ErrorMode.IMMEDIATE, maxConcurrency, bufferSize)); + } + + /** + * Maps a sequence of values into {@link ObservableSource}s and concatenates these {@code ObservableSource}s eagerly into a single + * {@code Observable} sequence. + * <p> + * Eager concatenation means that once a subscriber subscribes, this operator subscribes to all of the + * current {@code Observable}s. The operator buffers the values emitted by these {@code ObservableSource}s and then drains them in + * order, each one after the previous one completes. + * <p> + * <img width="640" height="390" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/concatMapEagerDelayError.o.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>This method does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <R> the value type + * @param mapper the function that maps a sequence of values into a sequence of {@code ObservableSource}s that will be + * eagerly concatenated + * @param tillTheEnd + * if {@code true}, all errors from the outer and inner {@code ObservableSource} sources are delayed until the end, + * if {@code false}, an error from the main source is signaled when the current {@code Observable} source terminates + * @return the new {@code Observable} instance with the specified concatenation behavior + * @throws NullPointerException if {@code mapper} is {@code null} + * @since 2.0 + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull R> Observable<R> concatMapEagerDelayError(@NonNull Function<? super T, ? extends ObservableSource<? extends R>> mapper, + boolean tillTheEnd) { + return concatMapEagerDelayError(mapper, tillTheEnd, Integer.MAX_VALUE, bufferSize()); + } + + /** + * Maps a sequence of values into {@link ObservableSource}s and concatenates these {@code ObservableSource}s eagerly into a single + * {@code Observable} sequence. + * <p> + * Eager concatenation means that once a subscriber subscribes, this operator subscribes to all of the + * current {@code Observable}s. The operator buffers the values emitted by these {@code ObservableSource}s and then drains them in + * order, each one after the previous one completes. + * <p> + * <img width="640" height="390" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/concatMapEagerDelayError.o.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>This method does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <R> the value type + * @param mapper the function that maps a sequence of values into a sequence of {@code ObservableSource}s that will be + * eagerly concatenated + * @param tillTheEnd + * if {@code true}, exceptions from the current {@code Observable} and all the inner {@code ObservableSource}s are delayed until + * all of them terminate, if {@code false}, exception from the current {@code Observable} is delayed until the + * currently running {@code ObservableSource} terminates + * @param maxConcurrency the maximum number of concurrent subscribed {@code ObservableSource}s + * @param bufferSize + * the number of elements expected from the current {@code Observable} and each inner {@code ObservableSource} to be buffered + * @return the new {@code Observable} instance with the specified concatenation behavior + * @throws NullPointerException if {@code mapper} is {@code null} + * @throws IllegalArgumentException if {@code maxConcurrency} or {@code bufferSize} is non-positive + * @since 2.0 + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull R> Observable<R> concatMapEagerDelayError(@NonNull Function<? super T, ? extends ObservableSource<? extends R>> mapper, + boolean tillTheEnd, int maxConcurrency, int bufferSize) { + Objects.requireNonNull(mapper, "mapper is null"); + ObjectHelper.verifyPositive(maxConcurrency, "maxConcurrency"); + ObjectHelper.verifyPositive(bufferSize, "bufferSize"); + return RxJavaPlugins.onAssembly(new ObservableConcatMapEager<>(this, mapper, tillTheEnd ? ErrorMode.END : ErrorMode.BOUNDARY, maxConcurrency, bufferSize)); + } + + /** + * Maps each element of the current {@code Observable} into {@link CompletableSource}s, subscribes to them one at a time in + * order and waits until the upstream and all {@code CompletableSource}s complete. + * <p> + * <img width="640" height="506" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/concatMapCompletable.o.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code concatMapCompletable} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * <p>History: 2.1.6 - experimental + * @param mapper + * a function that, when applied to an item emitted by the current {@code Observable}, returns a {@code CompletableSource} + * @return the new {@link Completable} instance + * @throws NullPointerException if {@code mapper} is {@code null} + * @since 2.2 + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Completable concatMapCompletable(@NonNull Function<? super T, ? extends CompletableSource> mapper) { + return concatMapCompletable(mapper, 2); + } + + /** + * Maps each element of the current {@code Observable} into {@link CompletableSource}s, subscribes to them one at a time in + * order and waits until the upstream and all {@code CompletableSource}s complete. + * <p> + * <img width="640" height="506" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/concatMapCompletable.o.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code concatMapCompletable} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * <p>History: 2.1.6 - experimental + * @param mapper + * a function that, when applied to an item emitted by the current {@code Observable}, returns a {@code CompletableSource} + * + * @param capacityHint + * the number of upstream items expected to be buffered until the current {@code CompletableSource}, mapped from + * the current item, completes. + * @return the new {@link Completable} instance + * @throws NullPointerException if {@code mapper} is {@code null} + * @throws IllegalArgumentException if {@code capacityHint} is non-positive + * @since 2.2 + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Completable concatMapCompletable(@NonNull Function<? super T, ? extends CompletableSource> mapper, int capacityHint) { + Objects.requireNonNull(mapper, "mapper is null"); + ObjectHelper.verifyPositive(capacityHint, "capacityHint"); + return RxJavaPlugins.onAssembly(new ObservableConcatMapCompletable<>(this, mapper, ErrorMode.IMMEDIATE, capacityHint)); + } + + /** + * Maps the upstream items into {@link CompletableSource}s and subscribes to them one after the + * other terminates, delaying all errors till both the current {@code Observable} and all + * inner {@code CompletableSource}s terminate. + * <p> + * <img width="640" height="305" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/concatMap.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code concatMapCompletableDelayError} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * <p>History: 2.1.11 - experimental + * @param mapper the function called with the upstream item and should return + * a {@code CompletableSource} to become the next source to + * be subscribed to + * @return the new {@link Completable} instance + * @throws NullPointerException if {@code mapper} is {@code null} + * @see #concatMapCompletable(Function, int) + * @since 2.2 + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Completable concatMapCompletableDelayError(@NonNull Function<? super T, ? extends CompletableSource> mapper) { + return concatMapCompletableDelayError(mapper, true, 2); + } + + /** + * Maps the upstream items into {@link CompletableSource}s and subscribes to them one after the + * other terminates, optionally delaying all errors till both the current {@code Observable} and all + * inner {@code CompletableSource}s terminate. + * <p> + * <img width="640" height="305" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/concatMap.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code concatMapCompletableDelayError} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * <p>History: 2.1.11 - experimental + * @param mapper the function called with the upstream item and should return + * a {@code CompletableSource} to become the next source to + * be subscribed to + * @param tillTheEnd If {@code true}, errors from the current {@code Observable} or any of the + * inner {@code CompletableSource}s are delayed until all + * of them terminate. If {@code false}, an error from the current + * {@code Observable} is delayed until the current inner + * {@code CompletableSource} terminates and only then is + * it emitted to the downstream. + * @return the new {@link Completable} instance + * @throws NullPointerException if {@code mapper} is {@code null} + * @see #concatMapCompletable(Function) + * @since 2.2 + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Completable concatMapCompletableDelayError(@NonNull Function<? super T, ? extends CompletableSource> mapper, boolean tillTheEnd) { + return concatMapCompletableDelayError(mapper, tillTheEnd, 2); + } + + /** + * Maps the upstream items into {@link CompletableSource}s and subscribes to them one after the + * other terminates, optionally delaying all errors till both the current {@code Observable} and all + * inner {@code CompletableSource}s terminate. + * <p> + * <img width="640" height="305" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/concatMap.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code concatMapCompletableDelayError} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * <p>History: 2.1.11 - experimental + * @param mapper the function called with the upstream item and should return + * a {@code CompletableSource} to become the next source to + * be subscribed to + * @param tillTheEnd If {@code true}, errors from the current {@code Observable} or any of the + * inner {@code CompletableSource}s are delayed until all + * of them terminate. If {@code false}, an error from the current + * {@code Observable} is delayed until the current inner + * {@code CompletableSource} terminates and only then is + * it emitted to the downstream. + * @param bufferSize The number of upstream items expected to be buffered so that fresh items are + * ready to be mapped when a previous {@code CompletableSource} terminates. + * @return the new {@link Completable} instance + * @throws NullPointerException if {@code mapper} is {@code null} + * @throws IllegalArgumentException if {@code bufferSize} is non-positive + * @see #concatMapCompletable(Function, int) + * @since 2.2 + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Completable concatMapCompletableDelayError(@NonNull Function<? super T, ? extends CompletableSource> mapper, boolean tillTheEnd, int bufferSize) { + Objects.requireNonNull(mapper, "mapper is null"); + ObjectHelper.verifyPositive(bufferSize, "bufferSize"); + return RxJavaPlugins.onAssembly(new ObservableConcatMapCompletable<>(this, mapper, tillTheEnd ? ErrorMode.END : ErrorMode.BOUNDARY, bufferSize)); + } + + /** + * Returns an {@code Observable} that concatenate each item emitted by the current {@code Observable} with the values in an + * {@link Iterable} corresponding to that item that is generated by a selector. + * <p> + * <img width="640" height="275" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/concatMapIterable.o.png" alt=""> + * + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code concatMapIterable} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <U> + * the type of item emitted by the resulting {@code Observable} + * @param mapper + * a function that returns an {@code Iterable} sequence of values for when given an item emitted by the + * current {@code Observable} + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code mapper} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/flatmap.html">ReactiveX operators documentation: FlatMap</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull U> Observable<U> concatMapIterable(@NonNull Function<? super T, @NonNull ? extends Iterable<? extends U>> mapper) { + Objects.requireNonNull(mapper, "mapper is null"); + return RxJavaPlugins.onAssembly(new ObservableFlattenIterable<>(this, mapper)); + } + + /** + * Maps the upstream items into {@link MaybeSource}s and subscribes to them one after the + * other succeeds or completes, emits their success value if available or terminates immediately if + * either the current {@code Observable} or the current inner {@code MaybeSource} fail. + * <p> + * <img width="640" height="310" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/concatMapMaybe.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code concatMapMaybe} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * <p>History: 2.1.11 - experimental + * @param <R> the result type of the inner {@code MaybeSource}s + * @param mapper the function called with the upstream item and should return + * a {@code MaybeSource} to become the next source to + * be subscribed to + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code mapper} is {@code null} + * @see #concatMapMaybeDelayError(Function) + * @see #concatMapMaybe(Function, int) + * @since 2.2 + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull R> Observable<R> concatMapMaybe(@NonNull Function<? super T, ? extends MaybeSource<? extends R>> mapper) { + return concatMapMaybe(mapper, 2); + } + + /** + * Maps the upstream items into {@link MaybeSource}s and subscribes to them one after the + * other succeeds or completes, emits their success value if available or terminates immediately if + * either the current {@code Observable} or the current inner {@code MaybeSource} fail. + * <p> + * <img width="640" height="310" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/concatMapMaybe.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code concatMapMaybe} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * <p>History: 2.1.11 - experimental + * @param <R> the result type of the inner {@code MaybeSource}s + * @param mapper the function called with the upstream item and should return + * a {@code MaybeSource} to become the next source to + * be subscribed to + * @param bufferSize The number of upstream items expected to be buffered so that fresh items are + * ready to be mapped when a previous {@code MaybeSource} terminates. + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code mapper} is {@code null} + * @throws IllegalArgumentException if {@code bufferSize} is non-positive + * @see #concatMapMaybe(Function) + * @see #concatMapMaybeDelayError(Function, boolean, int) + * @since 2.2 + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull R> Observable<R> concatMapMaybe(@NonNull Function<? super T, ? extends MaybeSource<? extends R>> mapper, int bufferSize) { + Objects.requireNonNull(mapper, "mapper is null"); + ObjectHelper.verifyPositive(bufferSize, "bufferSize"); + return RxJavaPlugins.onAssembly(new ObservableConcatMapMaybe<>(this, mapper, ErrorMode.IMMEDIATE, bufferSize)); + } + + /** + * Maps the upstream items into {@link MaybeSource}s and subscribes to them one after the + * other terminates, emits their success value if available and delaying all errors + * till both the current {@code Observable} and all inner {@code MaybeSource}s terminate. + * <p> + * <img width="640" height="310" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/concatMapMaybeDelayError.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code concatMapMaybeDelayError} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * <p>History: 2.1.11 - experimental + * @param <R> the result type of the inner {@code MaybeSource}s + * @param mapper the function called with the upstream item and should return + * a {@code MaybeSource} to become the next source to + * be subscribed to + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code mapper} is {@code null} + * @see #concatMapMaybe(Function) + * @see #concatMapMaybeDelayError(Function, boolean) + * @since 2.2 + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull R> Observable<R> concatMapMaybeDelayError(@NonNull Function<? super T, ? extends MaybeSource<? extends R>> mapper) { + return concatMapMaybeDelayError(mapper, true, 2); + } + + /** + * Maps the upstream items into {@link MaybeSource}s and subscribes to them one after the + * other terminates, emits their success value if available and optionally delaying all errors + * till both the current {@code Observable} and all inner {@code MaybeSource}s terminate. + * <p> + * <img width="640" height="310" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/concatMapMaybeDelayError.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code concatMapMaybeDelayError} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * <p>History: 2.1.11 - experimental + * @param <R> the result type of the inner {@code MaybeSource}s + * @param mapper the function called with the upstream item and should return + * a {@code MaybeSource} to become the next source to + * be subscribed to + * @param tillTheEnd If {@code true}, errors from the current {@code Observable} or any of the + * inner {@code MaybeSource}s are delayed until all + * of them terminate. If {@code false}, an error from the current + * {@code Observable} is delayed until the current inner + * {@code MaybeSource} terminates and only then is + * it emitted to the downstream. + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code mapper} is {@code null} + * @see #concatMapMaybe(Function, int) + * @see #concatMapMaybeDelayError(Function, boolean, int) + * @since 2.2 + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull R> Observable<R> concatMapMaybeDelayError(@NonNull Function<? super T, ? extends MaybeSource<? extends R>> mapper, boolean tillTheEnd) { + return concatMapMaybeDelayError(mapper, tillTheEnd, 2); + } + + /** + * Maps the upstream items into {@link MaybeSource}s and subscribes to them one after the + * other terminates, emits their success value if available and optionally delaying all errors + * till both the current {@code Observable} and all inner {@code MaybeSource}s terminate. + * <p> + * <img width="640" height="310" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/concatMapMaybeDelayError.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code concatMapMaybeDelayError} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * <p>History: 2.1.11 - experimental + * @param <R> the result type of the inner {@code MaybeSource}s + * @param mapper the function called with the upstream item and should return + * a {@code MaybeSource} to become the next source to + * be subscribed to + * @param tillTheEnd If {@code true}, errors from the current {@code Observable} or any of the + * inner {@code MaybeSource}s are delayed until all + * of them terminate. If {@code false}, an error from the current + * {@code Observable} is delayed until the current inner + * {@code MaybeSource} terminates and only then is + * it emitted to the downstream. + * @param bufferSize The number of upstream items expected to be buffered so that fresh items are + * ready to be mapped when a previous {@code MaybeSource} terminates. + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code mapper} is {@code null} + * @throws IllegalArgumentException if {@code bufferSize} is non-positive + * @see #concatMapMaybe(Function, int) + * @since 2.2 + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull R> Observable<R> concatMapMaybeDelayError(@NonNull Function<? super T, ? extends MaybeSource<? extends R>> mapper, boolean tillTheEnd, int bufferSize) { + Objects.requireNonNull(mapper, "mapper is null"); + ObjectHelper.verifyPositive(bufferSize, "bufferSize"); + return RxJavaPlugins.onAssembly(new ObservableConcatMapMaybe<>(this, mapper, tillTheEnd ? ErrorMode.END : ErrorMode.BOUNDARY, bufferSize)); + } + + /** + * Maps the upstream items into {@link SingleSource}s and subscribes to them one after the + * other succeeds, emits their success values or terminates immediately if + * either the current {@code Observable} or the current inner {@code SingleSource} fail. + * <p> + * <img width="640" height="310" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/concatMapSingle.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code concatMapSingle} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * <p>History: 2.1.11 - experimental + * @param <R> the result type of the inner {@code SingleSource}s + * @param mapper the function called with the upstream item and should return + * a {@code SingleSource} to become the next source to + * be subscribed to + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code mapper} is {@code null} + * @see #concatMapSingleDelayError(Function) + * @see #concatMapSingle(Function, int) + * @since 2.2 + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull R> Observable<R> concatMapSingle(@NonNull Function<? super T, ? extends SingleSource<? extends R>> mapper) { + return concatMapSingle(mapper, 2); + } + + /** + * Maps the upstream items into {@link SingleSource}s and subscribes to them one after the + * other succeeds, emits their success values or terminates immediately if + * either the current {@code Observable} or the current inner {@code SingleSource} fail. + * <p> + * <img width="640" height="310" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/concatMapSingle.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code concatMapSingle} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * <p>History: 2.1.11 - experimental + * @param <R> the result type of the inner {@code SingleSource}s + * @param mapper the function called with the upstream item and should return + * a {@code SingleSource} to become the next source to + * be subscribed to + * @param bufferSize The number of upstream items expected to be buffered so that fresh items are + * ready to be mapped when a previous {@code SingleSource} terminates. + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code mapper} is {@code null} + * @throws IllegalArgumentException if {@code bufferSize} is non-positive + * @see #concatMapSingle(Function) + * @see #concatMapSingleDelayError(Function, boolean, int) + * @since 2.2 + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull R> Observable<R> concatMapSingle(@NonNull Function<? super T, ? extends SingleSource<? extends R>> mapper, int bufferSize) { + Objects.requireNonNull(mapper, "mapper is null"); + ObjectHelper.verifyPositive(bufferSize, "bufferSize"); + return RxJavaPlugins.onAssembly(new ObservableConcatMapSingle<>(this, mapper, ErrorMode.IMMEDIATE, bufferSize)); + } + + /** + * Maps the upstream items into {@link SingleSource}s and subscribes to them one after the + * other succeeds or fails, emits their success values and delays all errors + * till both the current {@code Observable} and all inner {@code SingleSource}s terminate. + * <p> + * <img width="640" height="310" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/concatMapSingleDelayError.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code concatMapSingleDelayError} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * <p>History: 2.1.11 - experimental + * @param <R> the result type of the inner {@code SingleSource}s + * @param mapper the function called with the upstream item and should return + * a {@code SingleSource} to become the next source to + * be subscribed to + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code mapper} is {@code null} + * @see #concatMapSingle(Function) + * @see #concatMapSingleDelayError(Function, boolean) + * @since 2.2 + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull R> Observable<R> concatMapSingleDelayError(@NonNull Function<? super T, ? extends SingleSource<? extends R>> mapper) { + return concatMapSingleDelayError(mapper, true, 2); + } + + /** + * Maps the upstream items into {@link SingleSource}s and subscribes to them one after the + * other succeeds or fails, emits their success values and optionally delays all errors + * till both the current {@code Observable} and all inner {@code SingleSource}s terminate. + * <p> + * <img width="640" height="310" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/concatMapSingleDelayError.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code concatMapSingleDelayError} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * <p>History: 2.1.11 - experimental + * @param <R> the result type of the inner {@code SingleSource}s + * @param mapper the function called with the upstream item and should return + * a {@code SingleSource} to become the next source to + * be subscribed to + * @param tillTheEnd If {@code true}, errors from the current {@code Observable} or any of the + * inner {@code SingleSource}s are delayed until all + * of them terminate. If {@code false}, an error from the current + * {@code Observable} is delayed until the current inner + * {@code SingleSource} terminates and only then is + * it emitted to the downstream. + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code mapper} is {@code null} + * @see #concatMapSingle(Function, int) + * @see #concatMapSingleDelayError(Function, boolean, int) + * @since 2.2 + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull R> Observable<R> concatMapSingleDelayError(@NonNull Function<? super T, ? extends SingleSource<? extends R>> mapper, boolean tillTheEnd) { + return concatMapSingleDelayError(mapper, tillTheEnd, 2); + } + + /** + * Maps the upstream items into {@link SingleSource}s and subscribes to them one after the + * other succeeds or fails, emits their success values and optionally delays errors + * till both the current {@code Observable} and all inner {@code SingleSource}s terminate. + * <p> + * <img width="640" height="310" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/concatMapSingleDelayError.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code concatMapSingleDelayError} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * <p>History: 2.1.11 - experimental + * @param <R> the result type of the inner {@code SingleSource}s + * @param mapper the function called with the upstream item and should return + * a {@code SingleSource} to become the next source to + * be subscribed to + * @param tillTheEnd If {@code true}, errors from the current {@code Observable} or any of the + * inner {@code SingleSource}s are delayed until all + * of them terminate. If {@code false}, an error from the current + * {@code Observable} is delayed until the current inner + * {@code SingleSource} terminates and only then is + * it emitted to the downstream. + * @param bufferSize The number of upstream items expected to be buffered so that fresh items are + * ready to be mapped when a previous {@code SingleSource} terminates. + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code mapper} is {@code null} + * @throws IllegalArgumentException if {@code bufferSize} is non-positive + * @see #concatMapSingle(Function, int) + * @since 2.2 + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull R> Observable<R> concatMapSingleDelayError(@NonNull Function<? super T, ? extends SingleSource<? extends R>> mapper, boolean tillTheEnd, int bufferSize) { + Objects.requireNonNull(mapper, "mapper is null"); + ObjectHelper.verifyPositive(bufferSize, "bufferSize"); + return RxJavaPlugins.onAssembly(new ObservableConcatMapSingle<>(this, mapper, tillTheEnd ? ErrorMode.END : ErrorMode.BOUNDARY, bufferSize)); + } + + /** + * Returns an {@code Observable} that first emits the items emitted from the current {@code Observable}, then items + * from the {@code other} {@link ObservableSource} without interleaving them. + * <p> + * <img width="640" height="380" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/concat.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code concatWith} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param other + * an {@code ObservableSource} to be concatenated after the current + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code other} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/concat.html">ReactiveX operators documentation: Concat</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Observable<T> concatWith(@NonNull ObservableSource<? extends T> other) { + Objects.requireNonNull(other, "other is null"); + return concat(this, other); + } + + /** + * Returns an {@code Observable} that emits the items from the current {@code Observable} followed by the success item or error event + * of the {@code other} {@link SingleSource}. + * <p> + * <img width="640" height="380" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/concat.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code concatWith} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * <p>History: 2.1.10 - experimental + * @param other the {@code SingleSource} whose signal should be emitted after the current {@code Observable} completes normally. + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code other} is {@code null} + * @since 2.2 + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Observable<T> concatWith(@NonNull SingleSource<? extends T> other) { + Objects.requireNonNull(other, "other is null"); + return RxJavaPlugins.onAssembly(new ObservableConcatWithSingle<>(this, other)); + } + + /** + * Returns an {@code Observable} that emits the items from the current {@code Observable} followed by the success item or terminal events + * of the other {@link MaybeSource}. + * <p> + * <img width="640" height="380" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/concat.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code concatWith} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * <p>History: 2.1.10 - experimental + * @param other the {@code MaybeSource} whose signal should be emitted after the current {@code Observable} completes normally. + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code other} is {@code null} + * @since 2.2 + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Observable<T> concatWith(@NonNull MaybeSource<? extends T> other) { + Objects.requireNonNull(other, "other is null"); + return RxJavaPlugins.onAssembly(new ObservableConcatWithMaybe<>(this, other)); + } + + /** + * Returns an {@code Observable} that emits items from the current {@code Observable} and when it completes normally, the + * other {@link CompletableSource} is subscribed to and the returned {@code Observable} emits its terminal events. + * <p> + * <img width="640" height="380" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/concat.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code concatWith} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * <p>History: 2.1.10 - experimental + * @param other the {@code CompletableSource} to subscribe to once the current {@code Observable} completes normally + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code other} is {@code null} + * @since 2.2 + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Observable<T> concatWith(@NonNull CompletableSource other) { + Objects.requireNonNull(other, "other is null"); + return RxJavaPlugins.onAssembly(new ObservableConcatWithCompletable<>(this, other)); + } + + /** + * Returns a {@link Single} that emits a {@link Boolean} that indicates whether the current {@code Observable} emitted a + * specified item. + * <p> + * <img width="640" height="320" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/contains.2.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code contains} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param item + * the item to search for in the emissions from the current {@code Observable} + * @return the new {@code Single} instance + * @throws NullPointerException if {@code item} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/contains.html">ReactiveX operators documentation: Contains</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Single<Boolean> contains(@NonNull Object item) { + Objects.requireNonNull(item, "item is null"); + return any(Functions.equalsWith(item)); + } + + /** + * Returns a {@link Single} that counts the total number of items emitted by the current {@code Observable} and emits + * this count as a 64-bit {@link Long}. + * <p> + * <img width="640" height="310" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/count.2.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code count} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @return the new {@code Single} instance + * @see <a href="http://reactivex.io/documentation/operators/count.html">ReactiveX operators documentation: Count</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Single<Long> count() { + return RxJavaPlugins.onAssembly(new ObservableCountSingle<>(this)); + } + + /** + * Returns an {@code Observable} that mirrors the current {@code Observable}, except that it drops items emitted by the + * current {@code Observable} that are followed by another item within a computed debounce duration + * denoted by an item emission or completion from a generated inner {@link ObservableSource} for that original item. + * <p> + * <img width="640" height="425" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/debounce.f.v3.png" alt=""> + * <p> + * The delivery of the item happens on the thread of the first {@code onNext} or {@code onComplete} + * signal of the generated {@code ObservableSource} sequence, + * which if takes too long, a newer item may arrive from the upstream, causing the + * generated sequence to get disposed, which may also interrupt any downstream blocking operation + * (yielding an {@code InterruptedException}). It is recommended processing items + * that may take long time to be moved to another thread via {@link #observeOn} applied after + * {@code debounce} itself. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>This version of {@code debounce} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <U> + * the debounce value type (ignored) + * @param debounceIndicator + * function to return a sequence that indicates the throttle duration for each item via its own emission or completion + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code debounceIndicator} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/debounce.html">ReactiveX operators documentation: Debounce</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull U> Observable<T> debounce(@NonNull Function<? super T, ? extends ObservableSource<U>> debounceIndicator) { + Objects.requireNonNull(debounceIndicator, "debounceIndicator is null"); + return RxJavaPlugins.onAssembly(new ObservableDebounce<>(this, debounceIndicator)); + } + + /** + * Returns an {@code Observable} that mirrors the current {@code Observable}, except that it drops items emitted by the + * current {@code Observable} that are followed by newer items before a timeout value expires. The timer resets on + * each emission. + * <p> + * <em>Note:</em> If items keep being emitted by the current {@code Observable} faster than the timeout then no items + * will be emitted by the resulting {@code Observable}. + * <p> + * <img width="640" height="310" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/debounce.v3.png" alt=""> + * <p> + * Delivery of the item after the grace period happens on the {@code computation} {@link Scheduler}'s + * {@code Worker} which if takes too long, a newer item may arrive from the upstream, causing the + * {@code Worker}'s task to get disposed, which may also interrupt any downstream blocking operation + * (yielding an {@code InterruptedException}). It is recommended processing items + * that may take long time to be moved to another thread via {@link #observeOn} applied after + * {@code debounce} itself. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code debounce} operates by default on the {@code computation} {@code Scheduler}.</dd> + * </dl> + * + * @param timeout + * the length of the window of time that must pass after the emission of an item from the current + * {@code Observable} in which the {@code Observable} emits no items in order for the item to be emitted by the + * resulting {@code Observable} + * @param unit + * the unit of time for the specified {@code timeout} + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code unit} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/debounce.html">ReactiveX operators documentation: Debounce</a> + * @see #throttleWithTimeout(long, TimeUnit) + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.COMPUTATION) + @NonNull + public final Observable<T> debounce(long timeout, @NonNull TimeUnit unit) { + return debounce(timeout, unit, Schedulers.computation()); + } + + /** + * Returns an {@code Observable} that mirrors the current {@code Observable}, except that it drops items emitted by the + * current {@code Observable} that are followed by newer items before a timeout value expires on a specified + * {@link Scheduler}. The timer resets on each emission. + * <p> + * <em>Note:</em> If items keep being emitted by the current {@code Observable} faster than the timeout then no items + * will be emitted by the resulting {@code Observable}. + * <p> + * <img width="640" height="310" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/debounce.s.v3.png" alt=""> + * <p> + * Delivery of the item after the grace period happens on the given {@code Scheduler}'s + * {@code Worker} which if takes too long, a newer item may arrive from the upstream, causing the + * {@code Worker}'s task to get disposed, which may also interrupt any downstream blocking operation + * (yielding an {@code InterruptedException}). It is recommended processing items + * that may take long time to be moved to another thread via {@link #observeOn} applied after + * {@code debounce} itself. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>You specify which {@code Scheduler} this operator will use.</dd> + * </dl> + * + * @param timeout + * the time each item has to be "the most recent" of those emitted by the current {@code Observable} to + * ensure that it's not dropped + * @param unit + * the unit of time for the specified {@code timeout} + * @param scheduler + * the {@code Scheduler} to use internally to manage the timers that handle the timeout for each + * item + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code unit} or {@code scheduler} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/debounce.html">ReactiveX operators documentation: Debounce</a> + * @see #throttleWithTimeout(long, TimeUnit, Scheduler) + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.CUSTOM) + @NonNull + public final Observable<T> debounce(long timeout, @NonNull TimeUnit unit, @NonNull Scheduler scheduler) { + Objects.requireNonNull(unit, "unit is null"); + Objects.requireNonNull(scheduler, "scheduler is null"); + return RxJavaPlugins.onAssembly(new ObservableDebounceTimed<>(this, timeout, unit, scheduler, null)); + } + + /** + * Returns an {@code Observable} that mirrors the current {@code Observable}, except that it drops items emitted by the + * current {@code Observable} that are followed by newer items before a timeout value expires on a specified + * {@link Scheduler}. The timer resets on each emission. + * <p> + * <em>Note:</em> If items keep being emitted by the current {@code Observable} faster than the timeout then no items + * will be emitted by the resulting {@code Observable}. + * <p> + * <img width="640" height="310" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/debounce.s.v3.png" alt=""> + * <p> + * Delivery of the item after the grace period happens on the given {@code Scheduler}'s + * {@code Worker} which if takes too long, a newer item may arrive from the upstream, causing the + * {@code Worker}'s task to get disposed, which may also interrupt any downstream blocking operation + * (yielding an {@code InterruptedException}). It is recommended processing items + * that may take long time to be moved to another thread via {@link #observeOn} applied after + * {@code debounce} itself. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>You specify which {@code Scheduler} this operator will use.</dd> + * </dl> + * + * @param timeout + * the time each item has to be "the most recent" of those emitted by the current {@code Observable} to + * ensure that it's not dropped + * @param unit + * the unit of time for the specified {@code timeout} + * @param scheduler + * the {@code Scheduler} to use internally to manage the timers that handle the timeout for each + * item + * @param onDropped + * called with the current entry when it has been replaced by a new one + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code unit} or {@code scheduler} is {@code null} } or {@code onDropped} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/debounce.html">ReactiveX operators documentation: Debounce</a> + * @see #throttleWithTimeout(long, TimeUnit, Scheduler, Consumer) + * @since 3.1.6 - Experimental + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.CUSTOM) + @NonNull + @Experimental + public final Observable<T> debounce(long timeout, @NonNull TimeUnit unit, @NonNull Scheduler scheduler, @NonNull Consumer<? super T> onDropped) { + Objects.requireNonNull(unit, "unit is null"); + Objects.requireNonNull(scheduler, "scheduler is null"); + Objects.requireNonNull(onDropped, "onDropped is null"); + return RxJavaPlugins.onAssembly(new ObservableDebounceTimed<>(this, timeout, unit, scheduler, onDropped)); + } + + /** + * Returns an {@code Observable} that emits the items emitted by the current {@code Observable} or a specified default item + * if the current {@code Observable} is empty. + * <p> + * <img width="640" height="305" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/defaultIfEmpty.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code defaultIfEmpty} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param defaultItem + * the item to emit if the current {@code Observable} emits no items + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code defaultItem} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/defaultifempty.html">ReactiveX operators documentation: DefaultIfEmpty</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Observable<T> defaultIfEmpty(@NonNull T defaultItem) { + Objects.requireNonNull(defaultItem, "defaultItem is null"); + return switchIfEmpty(just(defaultItem)); + } + + /** + * Returns an {@code Observable} that delays the emissions of the current {@code Observable} via + * a per-item derived {@link ObservableSource}'s item emission or termination, on a per source item basis. + * <p> + * <img width="640" height="450" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/delay.o.v3.png" alt=""> + * <p> + * <em>Note:</em> the resulting {@code Observable} will immediately propagate any {@code onError} notification + * from the current {@code Observable}. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>This version of {@code delay} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <U> + * the item delay value type (ignored) + * @param itemDelayIndicator + * a function that returns an {@code ObservableSource} for each item emitted by the current {@code Observable}, which is + * then used to delay the emission of that item by the resulting {@code Observable} until the {@code ObservableSource} + * returned from {@code itemDelay} emits an item + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code itemDelayIndicator} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/delay.html">ReactiveX operators documentation: Delay</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull U> Observable<T> delay(@NonNull Function<? super T, ? extends ObservableSource<U>> itemDelayIndicator) { + Objects.requireNonNull(itemDelayIndicator, "itemDelayIndicator is null"); + return flatMap(ObservableInternalHelper.itemDelay(itemDelayIndicator)); + } + + /** + * Returns an {@code Observable} that emits the items emitted by the current {@code Observable} shifted forward in time by a + * specified delay. An error notification from the current {@code Observable} is not delayed. + * <p> + * <img width="640" height="310" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/delay.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>This version of {@code delay} operates by default on the {@code computation} {@link Scheduler}.</dd> + * </dl> + * + * @param time + * the delay to shift the source by + * @param unit + * the {@link TimeUnit} in which {@code period} is defined + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code unit} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/delay.html">ReactiveX operators documentation: Delay</a> + * @see #delay(long, TimeUnit, boolean) + * @see #delay(long, TimeUnit, Scheduler) + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.COMPUTATION) + @NonNull + public final Observable<T> delay(long time, @NonNull TimeUnit unit) { + return delay(time, unit, Schedulers.computation(), false); + } + + /** + * Returns an {@code Observable} that emits the items emitted by the current {@code Observable} shifted forward in time by a + * specified delay. If {@code delayError} is {@code true}, error notifications will also be delayed. + * <p> + * <img width="640" height="310" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/delay.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>This version of {@code delay} operates by default on the {@code computation} {@link Scheduler}.</dd> + * </dl> + * + * @param time + * the delay to shift the source by + * @param unit + * the {@link TimeUnit} in which {@code period} is defined + * @param delayError + * if {@code true}, the upstream exception is signaled with the given delay, after all preceding normal elements, + * if {@code false}, the upstream exception is signaled immediately + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code unit} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/delay.html">ReactiveX operators documentation: Delay</a> + * @see #delay(long, TimeUnit, Scheduler, boolean) + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.COMPUTATION) + @NonNull + public final Observable<T> delay(long time, @NonNull TimeUnit unit, boolean delayError) { + return delay(time, unit, Schedulers.computation(), delayError); + } + + /** + * Returns an {@code Observable} that emits the items emitted by the current {@code Observable} shifted forward in time by a + * specified delay. An error notification from the current {@code Observable} is not delayed. + * <p> + * <img width="640" height="310" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/delay.s.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>You specify which {@link Scheduler} this operator will use.</dd> + * </dl> + * + * @param time + * the delay to shift the source by + * @param unit + * the time unit of {@code delay} + * @param scheduler + * the {@code Scheduler} to use for delaying + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code unit} or {@code scheduler} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/delay.html">ReactiveX operators documentation: Delay</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.CUSTOM) + @NonNull + public final Observable<T> delay(long time, @NonNull TimeUnit unit, @NonNull Scheduler scheduler) { + return delay(time, unit, scheduler, false); + } + + /** + * Returns an {@code Observable} that emits the items emitted by the current {@code Observable} shifted forward in time by a + * specified delay. If {@code delayError} is {@code true}, error notifications will also be delayed. + * <p> + * <img width="640" height="310" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/delay.s.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>You specify which {@link Scheduler} this operator will use.</dd> + * </dl> + * + * @param time + * the delay to shift the source by + * @param unit + * the time unit of {@code delay} + * @param scheduler + * the {@code Scheduler} to use for delaying + * @param delayError + * if {@code true}, the upstream exception is signaled with the given delay, after all preceding normal elements, + * if {@code false}, the upstream exception is signaled immediately + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code unit} or {@code scheduler} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/delay.html">ReactiveX operators documentation: Delay</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.CUSTOM) + @NonNull + public final Observable<T> delay(long time, @NonNull TimeUnit unit, @NonNull Scheduler scheduler, boolean delayError) { + Objects.requireNonNull(unit, "unit is null"); + Objects.requireNonNull(scheduler, "scheduler is null"); + + return RxJavaPlugins.onAssembly(new ObservableDelay<>(this, time, unit, scheduler, delayError)); + } + + /** + * Returns an {@code Observable} that delays the subscription to and emissions from the current {@code Observable} via + * {@link ObservableSource}s for the subscription itself and on a per-item basis. + * <p> + * <img width="640" height="450" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/delay.oo.v3.png" alt=""> + * <p> + * <em>Note:</em> the resulting {@code Observable} will immediately propagate any {@code onError} notification + * from the current {@code Observable}. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>This version of {@code delay} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <U> + * the subscription delay value type (ignored) + * @param <V> + * the item delay value type (ignored) + * @param subscriptionIndicator + * a function that returns an {@code ObservableSource} that triggers the subscription to the current {@code Observable} + * once it emits any item + * @param itemDelayIndicator + * a function that returns an {@code ObservableSource} for each item emitted by the current {@code Observable}, which is + * then used to delay the emission of that item by the resulting {@code Observable} until the {@code ObservableSource} + * returned from {@code itemDelay} emits an item + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code subscriptionIndicator} or {@code itemDelayIndicator} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/delay.html">ReactiveX operators documentation: Delay</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull U, @NonNull V> Observable<T> delay(@NonNull ObservableSource<U> subscriptionIndicator, + @NonNull Function<? super T, ? extends ObservableSource<V>> itemDelayIndicator) { + return delaySubscription(subscriptionIndicator).delay(itemDelayIndicator); + } + + /** + * Returns an {@code Observable} that delays the subscription to the current {@code Observable} + * until the other {@link ObservableSource} emits an element or completes normally. + * <p> + * <img width="640" height="310" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/delaySubscription.o.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>This method does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <U> the value type of the other {@code Observable}, irrelevant + * @param subscriptionIndicator the other {@code ObservableSource} that should trigger the subscription + * to the current {@code Observable}. + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code subscriptionIndicator} is {@code null} + * @since 2.0 + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull U> Observable<T> delaySubscription(@NonNull ObservableSource<U> subscriptionIndicator) { + Objects.requireNonNull(subscriptionIndicator, "subscriptionIndicator is null"); + return RxJavaPlugins.onAssembly(new ObservableDelaySubscriptionOther<>(this, subscriptionIndicator)); + } + + /** + * Returns an {@code Observable} that delays the subscription to the current {@code Observable} by a given amount of time. + * <p> + * <img width="640" height="310" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/delaySubscription.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>This version of {@code delaySubscription} operates by default on the {@code computation} {@link Scheduler}.</dd> + * </dl> + * + * @param time + * the time to delay the subscription + * @param unit + * the time unit of {@code delay} + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code unit} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/delay.html">ReactiveX operators documentation: Delay</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.COMPUTATION) + @NonNull + public final Observable<T> delaySubscription(long time, @NonNull TimeUnit unit) { + return delaySubscription(time, unit, Schedulers.computation()); + } + + /** + * Returns an {@code Observable} that delays the subscription to the current {@code Observable} by a given amount of time, + * both waiting and subscribing on a given {@link Scheduler}. + * <p> + * <img width="640" height="310" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/delaySubscription.s.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>You specify which {@code Scheduler} this operator will use.</dd> + * </dl> + * + * @param time + * the time to delay the subscription + * @param unit + * the time unit of {@code delay} + * @param scheduler + * the {@code Scheduler} on which the waiting and subscription will happen + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code unit} or {@code scheduler} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/delay.html">ReactiveX operators documentation: Delay</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.CUSTOM) + @NonNull + public final Observable<T> delaySubscription(long time, @NonNull TimeUnit unit, @NonNull Scheduler scheduler) { + return delaySubscription(timer(time, unit, scheduler)); + } + + /** + * Returns an {@code Observable} that reverses the effect of {@link #materialize materialize} by transforming the + * {@link Notification} objects extracted from the source items via a selector function + * into their respective {@link Observer} signal types. + * <p> + * <img width="640" height="335" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/dematerialize.v3.png" alt=""> + * <p> + * The intended use of the {@code selector} function is to perform a + * type-safe identity mapping (see example) on a source that is already of type + * {@code Notification<T>}. The Java language doesn't allow + * limiting instance methods to a certain generic argument shape, therefore, + * a function is used to ensure the conversion remains type safe. + * <p> + * When the upstream signals an {@link Notification#createOnError(Throwable) onError} or + * {@link Notification#createOnComplete() onComplete} item, the + * returned {@code Observable} disposes of the flow and terminates with that type of terminal event: + * <pre><code> + * Observable.just(createOnNext(1), createOnComplete(), createOnNext(2)) + * .doOnDispose(() -> System.out.println("Disposed!")); + * .dematerialize(notification -> notification) + * .test() + * .assertResult(1); + * </code></pre> + * If the upstream signals {@code onError} or {@code onComplete} directly, the flow is terminated + * with the same event. + * <pre><code> + * Observable.just(createOnNext(1), createOnNext(2)) + * .dematerialize(notification -> notification) + * .test() + * .assertResult(1, 2); + * </code></pre> + * If this behavior is not desired, the completion can be suppressed by applying {@link #concatWith(ObservableSource)} + * with a {@link #never()} source. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code dematerialize} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * <p>History: 2.2.4 - experimental + * + * @param <R> the output value type + * @param selector function that returns the upstream item and should return a {@code Notification} to signal + * the corresponding {@code Observer} event to the downstream. + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code selector} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/materialize-dematerialize.html">ReactiveX operators documentation: Dematerialize</a> + * @since 3.0.0 + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull R> Observable<R> dematerialize(@NonNull Function<? super T, Notification<R>> selector) { + Objects.requireNonNull(selector, "selector is null"); + return RxJavaPlugins.onAssembly(new ObservableDematerialize<>(this, selector)); + } + + /** + * Returns an {@code Observable} that emits all items emitted by the current {@code Observable} that are distinct + * based on {@link Object#equals(Object)} comparison. + * <p> + * <img width="640" height="310" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/distinct.v3.png" alt=""> + * <p> + * It is recommended the elements' class {@code T} in the flow overrides the default {@code Object.equals()} + * and {@link Object#hashCode()} to provide meaningful comparison between items as the default Java + * implementation only considers reference equivalence. + * <p> + * By default, {@code distinct()} uses an internal {@link HashSet} per {@link Observer} to remember + * previously seen items and uses {@link java.util.Set#add(Object)} returning {@code false} as the + * indicator for duplicates. + * <p> + * Note that this internal {@code HashSet} may grow unbounded as items won't be removed from it by + * the operator. Therefore, using very long or infinite upstream (with very distinct elements) may lead + * to {@link OutOfMemoryError}. + * <p> + * Customizing the retention policy can happen only by providing a custom {@link java.util.Collection} implementation + * to the {@link #distinct(Function, Supplier)} overload. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code distinct} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @return the new {@code Observable} instance + * @see <a href="http://reactivex.io/documentation/operators/distinct.html">ReactiveX operators documentation: Distinct</a> + * @see #distinct(Function) + * @see #distinct(Function, Supplier) + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Observable<T> distinct() { + return distinct(Functions.identity(), Functions.createHashSet()); + } + + /** + * Returns an {@code Observable} that emits all items emitted by the current {@code Observable} that are distinct according + * to a key selector function and based on {@link Object#equals(Object)} comparison of the objects + * returned by the key selector function. + * <p> + * <img width="640" height="310" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/distinct.key.v3.png" alt=""> + * <p> + * It is recommended the keys' class {@code K} overrides the default {@code Object.equals()} + * and {@link Object#hashCode()} to provide meaningful comparison between the key objects as the default + * Java implementation only considers reference equivalence. + * <p> + * By default, {@code distinct()} uses an internal {@link HashSet} per {@link Observer} to remember + * previously seen keys and uses {@link java.util.Set#add(Object)} returning {@code false} as the + * indicator for duplicates. + * <p> + * Note that this internal {@code HashSet} may grow unbounded as keys won't be removed from it by + * the operator. Therefore, using very long or infinite upstream (with very distinct keys) may lead + * to {@link OutOfMemoryError}. + * <p> + * Customizing the retention policy can happen only by providing a custom {@link java.util.Collection} implementation + * to the {@link #distinct(Function, Supplier)} overload. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code distinct} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <K> the key type + * @param keySelector + * a function that projects an emitted item to a key value that is used to decide whether an item + * is distinct from another one or not + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code keySelector} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/distinct.html">ReactiveX operators documentation: Distinct</a> + * @see #distinct(Function, Supplier) + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull K> Observable<T> distinct(@NonNull Function<? super T, K> keySelector) { + return distinct(keySelector, Functions.createHashSet()); + } + + /** + * Returns an {@code Observable} that emits all items emitted by the current {@code Observable} that are distinct according + * to a key selector function and based on {@link Object#equals(Object)} comparison of the objects + * returned by the key selector function. + * <p> + * <img width="640" height="310" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/distinct.key.v3.png" alt=""> + * <p> + * It is recommended the keys' class {@code K} overrides the default {@code Object.equals()} + * and {@link Object#hashCode()} to provide meaningful comparison between the key objects as + * the default Java implementation only considers reference equivalence. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code distinct} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <K> the key type + * @param keySelector + * a function that projects an emitted item to a key value that is used to decide whether an item + * is distinct from another one or not + * @param collectionSupplier + * function called for each individual {@link Observer} to return a {@link Collection} subtype for holding the extracted + * keys and whose {@code add()} method's return indicates uniqueness. + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code keySelector} or {@code collectionSupplier} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/distinct.html">ReactiveX operators documentation: Distinct</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull K> Observable<T> distinct(@NonNull Function<? super T, K> keySelector, @NonNull Supplier<? extends Collection<? super K>> collectionSupplier) { + Objects.requireNonNull(keySelector, "keySelector is null"); + Objects.requireNonNull(collectionSupplier, "collectionSupplier is null"); + return RxJavaPlugins.onAssembly(new ObservableDistinct<>(this, keySelector, collectionSupplier)); + } + + /** + * Returns an {@code Observable} that emits all items emitted by the current {@code Observable} that are distinct from their + * immediate predecessors based on {@link Object#equals(Object)} comparison. + * <p> + * <img width="640" height="310" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/distinctUntilChanged.v3.png" alt=""> + * <p> + * It is recommended the elements' class {@code T} in the flow overrides the default {@code Object.equals()} to provide + * meaningful comparison between items as the default Java implementation only considers reference equivalence. + * Alternatively, use the {@link #distinctUntilChanged(BiPredicate)} overload and provide a comparison function + * in case the class {@code T} can't be overridden with custom {@code equals()} or the comparison itself + * should happen on different terms or properties of the class {@code T}. + * <p> + * Note that the operator always retains the latest item from upstream regardless of the comparison result + * and uses it in the next comparison with the next upstream item. + * <p> + * Note that if element type {@code T} in the flow is mutable, the comparison of the previous and current + * item may yield unexpected results if the items are mutated externally. Common cases are mutable + * {@link CharSequence}s or {@link List}s where the objects will actually have the same + * references when they are modified and {@code distinctUntilChanged} will evaluate subsequent items as same. + * To avoid such situation, it is recommended that mutable data is converted to an immutable one, + * for example using {@code map(CharSequence::toString)} or {@code map(list -> Collections.unmodifiableList(new ArrayList<>(list)))}. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code distinctUntilChanged} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @return the new {@code Observable} instance + * @see <a href="http://reactivex.io/documentation/operators/distinct.html">ReactiveX operators documentation: Distinct</a> + * @see #distinctUntilChanged(BiPredicate) + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Observable<T> distinctUntilChanged() { + return distinctUntilChanged(Functions.identity()); + } + + /** + * Returns an {@code Observable} that emits all items emitted by the current {@code Observable} that are distinct from their + * immediate predecessors, according to a key selector function and based on {@link Object#equals(Object)} comparison + * of those objects returned by the key selector function. + * <p> + * <img width="640" height="310" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/distinctUntilChanged.key.v3.png" alt=""> + * <p> + * It is recommended the keys' class {@code K} overrides the default {@code Object.equals()} to provide + * meaningful comparison between the key objects as the default Java implementation only considers reference equivalence. + * Alternatively, use the {@link #distinctUntilChanged(BiPredicate)} overload and provide a comparison function + * in case the class {@code K} can't be overridden with custom {@code equals()} or the comparison itself + * should happen on different terms or properties of the item class {@code T} (for which the keys can be + * derived via a similar selector). + * <p> + * Note that the operator always retains the latest key from upstream regardless of the comparison result + * and uses it in the next comparison with the next key derived from the next upstream item. + * <p> + * Note that if element type {@code T} in the flow is mutable, the comparison of the previous and current + * item may yield unexpected results if the items are mutated externally. Common cases are mutable + * {@link CharSequence}s or {@link List}s where the objects will actually have the same + * references when they are modified and {@code distinctUntilChanged} will evaluate subsequent items as same. + * To avoid such situation, it is recommended that mutable data is converted to an immutable one, + * for example using {@code map(CharSequence::toString)} or {@code map(list -> Collections.unmodifiableList(new ArrayList<>(list)))}. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code distinctUntilChanged} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <K> the key type + * @param keySelector + * a function that projects an emitted item to a key value that is used to decide whether an item + * is distinct from another one or not + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code keySelector} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/distinct.html">ReactiveX operators documentation: Distinct</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull K> Observable<T> distinctUntilChanged(@NonNull Function<? super T, K> keySelector) { + Objects.requireNonNull(keySelector, "keySelector is null"); + return RxJavaPlugins.onAssembly(new ObservableDistinctUntilChanged<>(this, keySelector, ObjectHelper.equalsPredicate())); + } + + /** + * Returns an {@code Observable} that emits all items emitted by the current {@code Observable} that are distinct from their + * immediate predecessors when compared with each other via the provided comparator function. + * <p> + * <img width="640" height="310" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/distinctUntilChanged.v3.png" alt=""> + * <p> + * Note that the operator always retains the latest item from upstream regardless of the comparison result + * and uses it in the next comparison with the next upstream item. + * <p> + * Note that if element type {@code T} in the flow is mutable, the comparison of the previous and current + * item may yield unexpected results if the items are mutated externally. Common cases are mutable + * {@link CharSequence}s or {@link List}s where the objects will actually have the same + * references when they are modified and {@code distinctUntilChanged} will evaluate subsequent items as same. + * To avoid such situation, it is recommended that mutable data is converted to an immutable one, + * for example using {@code map(CharSequence::toString)} or {@code map(list -> Collections.unmodifiableList(new ArrayList<>(list)))}. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code distinctUntilChanged} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param comparer the function that receives the previous item and the current item and is + * expected to return {@code true} if the two are equal, thus skipping the current value. + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code comparer} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/distinct.html">ReactiveX operators documentation: Distinct</a> + * @since 2.0 + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Observable<T> distinctUntilChanged(@NonNull BiPredicate<? super T, ? super T> comparer) { + Objects.requireNonNull(comparer, "comparer is null"); + return RxJavaPlugins.onAssembly(new ObservableDistinctUntilChanged<>(this, Functions.identity(), comparer)); + } + + /** + * Calls the specified {@link Consumer} with the current item after this item has been emitted to the downstream. + * <p> + * Note that the {@code onAfterNext} action is shared between subscriptions and as such + * should be thread-safe. + * <p> + * <img width="640" height="360" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/doAfterNext.o.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code doAfterNext} does not operate by default on a particular {@link Scheduler}.</dd> + * <dt><b>Operator-fusion:</b></dt> + * <dd>This operator supports boundary-limited synchronous or asynchronous queue-fusion.</dd> + * </dl> + * <p>History: 2.0.1 - experimental + * @param onAfterNext the {@code Consumer} that will be called after emitting an item from upstream to the downstream + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code onAfterNext} is {@code null} + * @since 2.1 + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Observable<T> doAfterNext(@NonNull Consumer<? super T> onAfterNext) { + Objects.requireNonNull(onAfterNext, "onAfterNext is null"); + return RxJavaPlugins.onAssembly(new ObservableDoAfterNext<>(this, onAfterNext)); + } + + /** + * Registers an {@link Action} to be called when the current {@code Observable} invokes either + * {@link Observer#onComplete onComplete} or {@link Observer#onError onError}. + * <p> + * <img width="640" height="310" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/doAfterTerminate.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code doAfterTerminate} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param onAfterTerminate + * an {@code Action} to be invoked after the current {@code Observable} finishes + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code onAfterTerminate} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/do.html">ReactiveX operators documentation: Do</a> + * @see #doOnTerminate(Action) + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Observable<T> doAfterTerminate(@NonNull Action onAfterTerminate) { + Objects.requireNonNull(onAfterTerminate, "onAfterTerminate is null"); + return doOnEach(Functions.emptyConsumer(), Functions.emptyConsumer(), Functions.EMPTY_ACTION, onAfterTerminate); + } + + /** + * Calls the specified action after the current {@code Observable} signals {@code onError} or {@code onCompleted} or gets disposed by + * the downstream. + * <p>In case of a race between a terminal event and a dispose call, the provided {@code onFinally} action + * is executed once per subscription. + * <p>Note that the {@code onFinally} action is shared between subscriptions and as such + * should be thread-safe. + * <p> + * <img width="640" height="282" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/doFinally.o.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code doFinally} does not operate by default on a particular {@link Scheduler}.</dd> + * <dt><b>Operator-fusion:</b></dt> + * <dd>This operator supports boundary-limited synchronous or asynchronous queue-fusion.</dd> + * </dl> + * <p>History: 2.0.1 - experimental + * @param onFinally the action called when the current {@code Observable} terminates or gets disposed + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code onFinally} is {@code null} + * @since 2.1 + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Observable<T> doFinally(@NonNull Action onFinally) { + Objects.requireNonNull(onFinally, "onFinally is null"); + return RxJavaPlugins.onAssembly(new ObservableDoFinally<>(this, onFinally)); + } + + /** + * Calls the given shared {@link Action} if the downstream disposes the sequence. + * <p> + * The action is shared between subscriptions and thus may be called concurrently from multiple + * threads; the action must be thread safe. + * <p> + * If the action throws a runtime exception, that exception is rethrown by the {@code dispose()} call, + * sometimes as a {@link CompositeException} if there were multiple exceptions along the way. + * <p> + * <img width="640" height="310" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/doOnDispose.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code doOnDispose} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param onDispose + * the action that gets called when the current {@code Observable}'s {@link Disposable} is disposed + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code onDispose} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/do.html">ReactiveX operators documentation: Do</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Observable<T> doOnDispose(@NonNull Action onDispose) { + return doOnLifecycle(Functions.emptyConsumer(), onDispose); + } + + /** + * Returns an {@code Observable} that invokes an {@link Action} when the current {@code Observable} calls {@code onComplete}. + * <p> + * <img width="640" height="358" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/doOnComplete.o.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code doOnComplete} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param onComplete + * the action to invoke when the current {@code Observable} calls {@code onComplete} + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code onComplete} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/do.html">ReactiveX operators documentation: Do</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Observable<T> doOnComplete(@NonNull Action onComplete) { + return doOnEach(Functions.emptyConsumer(), Functions.emptyConsumer(), onComplete, Functions.EMPTY_ACTION); + } + + /** + * Calls the appropriate {@code onXXX} consumer (shared between all {@link Observer}s) whenever a signal with the same type + * passes through, before forwarding them to the downstream. + * <p> + * <img width="640" height="310" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/doOnEach.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code doOnEach} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param onNext the {@link Consumer} to invoke when the current {@code Observable} calls {@code onNext} + * @param onError the {@code Consumer} to invoke when the current {@code Observable} calls {@code onError} + * @param onComplete the {@link Action} to invoke when the current {@code Observable} calls {@code onComplete} + * @param onAfterTerminate the {@code Action} to invoke when the current {@code Observable} calls {@code onAfterTerminate} + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code onNext}, {@code onError}, {@code onComplete} or {@code onAfterTerminate} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/do.html">ReactiveX operators documentation: Do</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + private Observable<T> doOnEach(@NonNull Consumer<? super T> onNext, @NonNull Consumer<? super Throwable> onError, @NonNull Action onComplete, @NonNull Action onAfterTerminate) { + Objects.requireNonNull(onNext, "onNext is null"); + Objects.requireNonNull(onError, "onError is null"); + Objects.requireNonNull(onComplete, "onComplete is null"); + Objects.requireNonNull(onAfterTerminate, "onAfterTerminate is null"); + return RxJavaPlugins.onAssembly(new ObservableDoOnEach<>(this, onNext, onError, onComplete, onAfterTerminate)); + } + + /** + * Returns an {@code Observable} that invokes a {@link Consumer} with the appropriate {@link Notification} + * object when the current {@code Observable} signals an item or terminates. + * <p> + * <img width="640" height="310" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/doOnEach.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code doOnEach} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param onNotification + * the action to invoke for each item emitted by the current {@code Observable} + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code onNotification} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/do.html">ReactiveX operators documentation: Do</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Observable<T> doOnEach(@NonNull Consumer<? super Notification<T>> onNotification) { + Objects.requireNonNull(onNotification, "onNotification is null"); + return doOnEach( + Functions.notificationOnNext(onNotification), + Functions.notificationOnError(onNotification), + Functions.notificationOnComplete(onNotification), + Functions.EMPTY_ACTION + ); + } + + /** + * Returns an {@code Observable} that forwards the items and terminal events of the current + * {@code Observable} to its {@link Observer}s and to the given shared {@code Observer} instance. + * <p> + * In case the {@code onError} of the supplied observer throws, the downstream will receive a composite + * exception containing the original exception and the exception thrown by {@code onError}. If either the + * {@code onNext} or the {@code onComplete} method of the supplied observer throws, the downstream will be + * terminated and will receive this thrown exception. + * <p> + * <img width="640" height="310" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/doOnEach.o.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code doOnEach} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param observer + * the observer to be notified about {@code onNext}, {@code onError} and {@code onComplete} events on its + * respective methods before the actual downstream {@code Observer} gets notified. + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code observer} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/do.html">ReactiveX operators documentation: Do</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Observable<T> doOnEach(@NonNull Observer<? super T> observer) { + Objects.requireNonNull(observer, "observer is null"); + return doOnEach( + ObservableInternalHelper.observerOnNext(observer), + ObservableInternalHelper.observerOnError(observer), + ObservableInternalHelper.observerOnComplete(observer), + Functions.EMPTY_ACTION); + } + + /** + * Calls the given {@link Consumer} with the error {@link Throwable} if the current {@code Observable} failed before forwarding it to + * the downstream. + * <p> + * In case the {@code onError} action throws, the downstream will receive a composite exception containing + * the original exception and the exception thrown by {@code onError}. + * <p> + * <img width="640" height="355" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/doOnError.o.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code doOnError} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param onError + * the action to invoke if the current {@code Observable} calls {@code onError} + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code onError} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/do.html">ReactiveX operators documentation: Do</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Observable<T> doOnError(@NonNull Consumer<? super Throwable> onError) { + return doOnEach(Functions.emptyConsumer(), onError, Functions.EMPTY_ACTION, Functions.EMPTY_ACTION); + } + + /** + * Calls the appropriate {@code onXXX} method (shared between all {@link Observer}s) for the lifecycle events of + * the sequence (subscription, disposal). + * <p> + * <img width="640" height="310" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/doOnLifecycle.o.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code doOnLifecycle} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param onSubscribe + * a {@link Consumer} called with the {@link Disposable} sent via {@link Observer#onSubscribe(Disposable)} + * @param onDispose + * called when the downstream disposes the {@code Disposable} via {@code dispose()} + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code onSubscribe} or {@code onDispose} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/do.html">ReactiveX operators documentation: Do</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Observable<T> doOnLifecycle(@NonNull Consumer<? super Disposable> onSubscribe, @NonNull Action onDispose) { + Objects.requireNonNull(onSubscribe, "onSubscribe is null"); + Objects.requireNonNull(onDispose, "onDispose is null"); + return RxJavaPlugins.onAssembly(new ObservableDoOnLifecycle<>(this, onSubscribe, onDispose)); + } + + /** + * Calls the given {@link Consumer} with the value emitted by the current {@code Observable} before forwarding it to the downstream. + * <p> + * <img width="640" height="360" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/doOnNext.o.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code doOnNext} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param onNext + * the action to invoke when the current {@code Observable} calls {@code onNext} + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code onNext} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/do.html">ReactiveX operators documentation: Do</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Observable<T> doOnNext(@NonNull Consumer<? super T> onNext) { + return doOnEach(onNext, Functions.emptyConsumer(), Functions.EMPTY_ACTION, Functions.EMPTY_ACTION); + } + + /** + * Returns an {@code Observable} so that it invokes the given {@link Consumer} when the current {@code Observable} is subscribed from + * its {@link Observer}s. Each subscription will result in an invocation of the given action except when the + * current {@code Observable} is reference counted, in which case the current {@code Observable} will invoke + * the given action for the first subscription. + * <p> + * <img width="640" height="390" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/doOnSubscribe.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code doOnSubscribe} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param onSubscribe + * the {@code Consumer} that gets called when an {@code Observer} subscribes to the current {@code Observable} + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code onSubscribe} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/do.html">ReactiveX operators documentation: Do</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Observable<T> doOnSubscribe(@NonNull Consumer<? super Disposable> onSubscribe) { + return doOnLifecycle(onSubscribe, Functions.EMPTY_ACTION); + } + + /** + * Returns an {@code Observable} so that it invokes an action when the current {@code Observable} calls {@code onComplete} or + * {@code onError}. + * <p> + * <img width="640" height="327" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/doOnTerminate.o.png" alt=""> + * <p> + * This differs from {@code doAfterTerminate} in that this happens <em>before</em> the {@code onComplete} or + * {@code onError} notification. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code doOnTerminate} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param onTerminate + * the action to invoke when the current {@code Observable} calls {@code onComplete} or {@code onError} + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code onTerminate} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/do.html">ReactiveX operators documentation: Do</a> + * @see #doAfterTerminate(Action) + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Observable<T> doOnTerminate(@NonNull Action onTerminate) { + Objects.requireNonNull(onTerminate, "onTerminate is null"); + return doOnEach(Functions.emptyConsumer(), + Functions.actionConsumer(onTerminate), onTerminate, + Functions.EMPTY_ACTION); + } + + /** + * Returns a {@link Maybe} that emits the single item at a specified index in a sequence of emissions from + * the current {@code Observable} or completes if the current {@code Observable} signals fewer elements than index. + * <p> + * <img width="640" height="363" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/elementAt.o.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code elementAt} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param index + * the zero-based index of the item to retrieve + * @return the new {@code Maybe} instance + * @throws IndexOutOfBoundsException + * if {@code index} is negative + * @see <a href="http://reactivex.io/documentation/operators/elementat.html">ReactiveX operators documentation: ElementAt</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Maybe<T> elementAt(long index) { + if (index < 0) { + throw new IndexOutOfBoundsException("index >= 0 required but it was " + index); + } + return RxJavaPlugins.onAssembly(new ObservableElementAtMaybe<>(this, index)); + } + + /** + * Returns a {@link Single} that emits the item found at a specified index in a sequence of emissions from + * the current {@code Observable}, or a default item if that index is out of range. + * <p> + * <img width="640" height="354" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/elementAtDefault.o.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code elementAt} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param index + * the zero-based index of the item to retrieve + * @param defaultItem + * the default item + * @return the new {@code Single} instance + * @throws NullPointerException if {@code defaultItem} is {@code null} + * @throws IndexOutOfBoundsException + * if {@code index} is negative + * @see <a href="http://reactivex.io/documentation/operators/elementat.html">ReactiveX operators documentation: ElementAt</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Single<T> elementAt(long index, @NonNull T defaultItem) { + if (index < 0) { + throw new IndexOutOfBoundsException("index >= 0 required but it was " + index); + } + Objects.requireNonNull(defaultItem, "defaultItem is null"); + return RxJavaPlugins.onAssembly(new ObservableElementAtSingle<>(this, index, defaultItem)); + } + + /** + * Returns a {@link Single} that emits the item found at a specified index in a sequence of emissions from the current {@code Observable} + * or signals a {@link NoSuchElementException} if the current {@code Observable} signals fewer elements than index. + * <p> + * <img width="640" height="362" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/elementAtOrError.o.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code elementAtOrError} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param index + * the zero-based index of the item to retrieve + * @return the new {@code Single} instance + * @throws IndexOutOfBoundsException + * if {@code index} is negative + * @see <a href="http://reactivex.io/documentation/operators/elementat.html">ReactiveX operators documentation: ElementAt</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Single<T> elementAtOrError(long index) { + if (index < 0) { + throw new IndexOutOfBoundsException("index >= 0 required but it was " + index); + } + return RxJavaPlugins.onAssembly(new ObservableElementAtSingle<>(this, index, null)); + } + + /** + * Filters items emitted by the current {@code Observable} by only emitting those that satisfy a specified {@link Predicate}. + * <p> + * <img width="640" height="310" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/filter.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code filter} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param predicate + * a function that evaluates each item emitted by the current {@code Observable}, returning {@code true} + * if it passes the filter + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code predicate} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/filter.html">ReactiveX operators documentation: Filter</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Observable<T> filter(@NonNull Predicate<? super T> predicate) { + Objects.requireNonNull(predicate, "predicate is null"); + return RxJavaPlugins.onAssembly(new ObservableFilter<>(this, predicate)); + } + + /** + * Returns a {@link Maybe} that emits only the very first item emitted by the current {@code Observable}, or + * completes if the current {@code Observable} is empty. + * <p> + * <img width="640" height="286" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/firstElement.m.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code firstElement} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @return the new {@code Maybe} instance + * @see <a href="http://reactivex.io/documentation/operators/first.html">ReactiveX operators documentation: First</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Maybe<T> firstElement() { + return elementAt(0L); + } + + /** + * Returns a {@link Single} that emits only the very first item emitted by the current {@code Observable}, or a default item + * if the current {@code Observable} completes without emitting any items. + * <p> + * <img width="640" height="283" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/first.s.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code first} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param defaultItem + * the default item to emit if the current {@code Observable} doesn't emit anything + * @return the new {@code Single} instance + * @throws NullPointerException if {@code defaultItem} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/first.html">ReactiveX operators documentation: First</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Single<T> first(@NonNull T defaultItem) { + return elementAt(0L, defaultItem); + } + + /** + * Returns a {@link Single} that emits only the very first item emitted by the current {@code Observable} or + * signals a {@link NoSuchElementException} if the current {@code Observable} is empty. + * <p> + * <img width="640" height="435" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/firstOrError.o.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code firstOrError} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @return the new {@code Single} instance + * @see <a href="http://reactivex.io/documentation/operators/first.html">ReactiveX operators documentation: First</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Single<T> firstOrError() { + return elementAtOrError(0L); + } + + /** + * Returns an {@code Observable} that emits items based on applying a function that you supply to each item emitted + * by the current {@code Observable}, where that function returns an {@link ObservableSource}, and then merging those returned + * {@code ObservableSource}s and emitting the results of this merger. + * <p> + * <img width="640" height="310" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/flatMap.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code flatMap} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <R> the value type of the inner {@code ObservableSource}s and the output type + * @param mapper + * a function that, when applied to an item emitted by the current {@code Observable}, returns an + * {@code ObservableSource} + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code mapper} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/flatmap.html">ReactiveX operators documentation: FlatMap</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull R> Observable<R> flatMap(@NonNull Function<? super T, ? extends ObservableSource<? extends R>> mapper) { + return flatMap(mapper, false); + } + + /** + * Returns an {@code Observable} that emits items based on applying a function that you supply to each item emitted + * by the current {@code Observable}, where that function returns an {@link ObservableSource}, and then merging those returned + * {@code ObservableSource}s and emitting the results of this merger. + * <p> + * <img width="640" height="356" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/flatMapDelayError.o.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code flatMap} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <R> the value type of the inner {@code ObservableSource}s and the output type + * @param mapper + * a function that, when applied to an item emitted by the current {@code Observable}, returns an + * {@code ObservableSource} + * @param delayErrors + * if {@code true}, exceptions from the current {@code Observable} and all inner {@code ObservableSource}s are delayed until all of them terminate + * if {@code false}, the first one signaling an exception will terminate the whole sequence immediately + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code mapper} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/flatmap.html">ReactiveX operators documentation: FlatMap</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull R> Observable<R> flatMap(@NonNull Function<? super T, ? extends ObservableSource<? extends R>> mapper, boolean delayErrors) { + return flatMap(mapper, delayErrors, Integer.MAX_VALUE); + } + + /** + * Returns an {@code Observable} that emits items based on applying a function that you supply to each item emitted + * by the current {@code Observable}, where that function returns an {@link ObservableSource}, and then merging those returned + * {@code ObservableSource}s and emitting the results of this merger, while limiting the maximum number of concurrent + * subscriptions to these {@code ObservableSource}s. + * <p> + * <img width="640" height="442" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/flatMapMaxConcurrency.o.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code flatMap} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <R> the value type of the inner {@code ObservableSource}s and the output type + * @param mapper + * a function that, when applied to an item emitted by the current {@code Observable}, returns an + * {@code ObservableSource} + * @param maxConcurrency + * the maximum number of {@code ObservableSource}s that may be subscribed to concurrently + * @param delayErrors + * if {@code true}, exceptions from the current {@code Observable} and all inner {@code ObservableSource}s are delayed until all of them terminate + * if {@code false}, the first one signaling an exception will terminate the whole sequence immediately + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code mapper} is {@code null} + * @throws IllegalArgumentException if {@code maxConcurrency} is non-positive + * @see <a href="http://reactivex.io/documentation/operators/flatmap.html">ReactiveX operators documentation: FlatMap</a> + * @since 2.0 + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull R> Observable<R> flatMap(@NonNull Function<? super T, ? extends ObservableSource<? extends R>> mapper, boolean delayErrors, int maxConcurrency) { + return flatMap(mapper, delayErrors, maxConcurrency, bufferSize()); + } + + /** + * Returns an {@code Observable} that emits items based on applying a function that you supply to each item emitted + * by the current {@code Observable}, where that function returns an {@link ObservableSource}, and then merging those returned + * {@code ObservableSource}s and emitting the results of this merger, while limiting the maximum number of concurrent + * subscriptions to these {@code ObservableSource}s. + * <p> + * <img width="640" height="442" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/flatMapMaxConcurrency.o.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code flatMap} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <R> the value type of the inner {@code ObservableSource}s and the output type + * @param mapper + * a function that, when applied to an item emitted by the current {@code Observable}, returns an + * {@code ObservableSource} + * @param maxConcurrency + * the maximum number of {@code ObservableSource}s that may be subscribed to concurrently + * @param delayErrors + * if {@code true}, exceptions from the current {@code Observable} and all inner {@code ObservableSource}s are delayed until all of them terminate + * if {@code false}, the first one signaling an exception will terminate the whole sequence immediately + * @param bufferSize + * the number of elements expected from each inner {@code ObservableSource} to be buffered + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code mapper} is {@code null} + * @throws IllegalArgumentException if {@code maxConcurrency} or {@code bufferSize} is non-positive + * @see <a href="http://reactivex.io/documentation/operators/flatmap.html">ReactiveX operators documentation: FlatMap</a> + * @since 2.0 + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull R> Observable<R> flatMap(@NonNull Function<? super T, ? extends ObservableSource<? extends R>> mapper, + boolean delayErrors, int maxConcurrency, int bufferSize) { + Objects.requireNonNull(mapper, "mapper is null"); + ObjectHelper.verifyPositive(maxConcurrency, "maxConcurrency"); + ObjectHelper.verifyPositive(bufferSize, "bufferSize"); + if (this instanceof ScalarSupplier) { + @SuppressWarnings("unchecked") + T v = ((ScalarSupplier<T>)this).get(); + if (v == null) { + return empty(); + } + return ObservableScalarXMap.scalarXMap(v, mapper); + } + return RxJavaPlugins.onAssembly(new ObservableFlatMap<>(this, mapper, delayErrors, maxConcurrency, bufferSize)); + } + + /** + * Returns an {@code Observable} that applies a function to each item emitted or notification raised by the current + * {@code Observable} and then flattens the {@link ObservableSource}s returned from these functions and emits the resulting items. + * <p> + * <img width="640" height="410" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/mergeMap.nce.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code flatMap} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <R> + * the result type + * @param onNextMapper + * a function that returns an {@code ObservableSource} to merge for each item emitted by the current {@code Observable} + * @param onErrorMapper + * a function that returns an {@code ObservableSource} to merge for an {@code onError} notification from the current + * {@code Observable} + * @param onCompleteSupplier + * a function that returns an {@code ObservableSource} to merge for an {@code onComplete} notification from the current + * {@code Observable} + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code onNextMapper} or {@code onErrorMapper} or {@code onCompleteSupplier} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/flatmap.html">ReactiveX operators documentation: FlatMap</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull R> Observable<R> flatMap( + @NonNull Function<? super T, ? extends ObservableSource<? extends R>> onNextMapper, + @NonNull Function<? super Throwable, ? extends ObservableSource<? extends R>> onErrorMapper, + @NonNull Supplier<? extends ObservableSource<? extends R>> onCompleteSupplier) { + Objects.requireNonNull(onNextMapper, "onNextMapper is null"); + Objects.requireNonNull(onErrorMapper, "onErrorMapper is null"); + Objects.requireNonNull(onCompleteSupplier, "onCompleteSupplier is null"); + return merge(new ObservableMapNotification<>(this, onNextMapper, onErrorMapper, onCompleteSupplier)); + } + + /** + * Returns an {@code Observable} that applies a function to each item emitted or notification raised by the current + * {@code Observable} and then flattens the {@link ObservableSource}s returned from these functions and emits the resulting items, + * while limiting the maximum number of concurrent subscriptions to these {@code ObservableSource}s. + * <p> + * <img width="640" height="410" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/mergeMap.nce.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code flatMap} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <R> + * the result type + * @param onNextMapper + * a function that returns an {@code ObservableSource} to merge for each item emitted by the current {@code Observable} + * @param onErrorMapper + * a function that returns an {@code ObservableSource} to merge for an {@code onError} notification from the current + * {@code Observable} + * @param onCompleteSupplier + * a function that returns an {@code ObservableSource} to merge for an {@code onComplete} notification from the current + * {@code Observable} + * @param maxConcurrency + * the maximum number of {@code ObservableSource}s that may be subscribed to concurrently + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code onNextMapper} or {@code onErrorMapper} or {@code onCompleteSupplier} is {@code null} + * @throws IllegalArgumentException if {@code maxConcurrency} is non-positive + * @see <a href="http://reactivex.io/documentation/operators/flatmap.html">ReactiveX operators documentation: FlatMap</a> + * @since 2.0 + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull R> Observable<R> flatMap( + @NonNull Function<? super T, ? extends ObservableSource<? extends R>> onNextMapper, + @NonNull Function<Throwable, ? extends ObservableSource<? extends R>> onErrorMapper, + @NonNull Supplier<? extends ObservableSource<? extends R>> onCompleteSupplier, + int maxConcurrency) { + Objects.requireNonNull(onNextMapper, "onNextMapper is null"); + Objects.requireNonNull(onErrorMapper, "onErrorMapper is null"); + Objects.requireNonNull(onCompleteSupplier, "onCompleteSupplier is null"); + return merge(new ObservableMapNotification<>(this, onNextMapper, onErrorMapper, onCompleteSupplier), maxConcurrency); + } + + /** + * Returns an {@code Observable} that emits items based on applying a function that you supply to each item emitted + * by the current {@code Observable}, where that function returns an {@link ObservableSource}, and then merging those returned + * {@code ObservableSource}s and emitting the results of this merger, while limiting the maximum number of concurrent + * subscriptions to these {@code ObservableSource}s. + * <p> + * <img width="640" height="442" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/flatMapMaxConcurrency.o.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code flatMap} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <R> the value type of the inner {@code ObservableSource}s and the output type + * @param mapper + * a function that, when applied to an item emitted by the current {@code Observable}, returns an + * {@code ObservableSource} + * @param maxConcurrency + * the maximum number of {@code ObservableSource}s that may be subscribed to concurrently + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code mapper} is {@code null} + * @throws IllegalArgumentException if {@code maxConcurrency} is non-positive + * @see <a href="http://reactivex.io/documentation/operators/flatmap.html">ReactiveX operators documentation: FlatMap</a> + * @since 2.0 + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull R> Observable<R> flatMap(@NonNull Function<? super T, ? extends ObservableSource<? extends R>> mapper, int maxConcurrency) { + return flatMap(mapper, false, maxConcurrency, bufferSize()); + } + + /** + * Returns an {@code Observable} that emits the results of a specified function to the pair of values emitted by the + * current {@code Observable} and the mapped inner {@link ObservableSource}. + * <p> + * <img width="640" height="390" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/mergeMap.r.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code flatMap} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <U> + * the type of items emitted by the collection {@code ObservableSource} + * @param <R> + * the type of items emitted by the resulting {@code Observable} + * @param mapper + * a function that returns an {@code ObservableSource} for each item emitted by the current {@code Observable} + * @param combiner + * a function that combines one item emitted by each of the source and collection {@code ObservableSource}s and + * returns an item to be emitted by the resulting {@code Observable} + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code mapper} or {@code combiner} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/flatmap.html">ReactiveX operators documentation: FlatMap</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull U, @NonNull R> Observable<R> flatMap(@NonNull Function<? super T, ? extends ObservableSource<? extends U>> mapper, + @NonNull BiFunction<? super T, ? super U, ? extends R> combiner) { + return flatMap(mapper, combiner, false, bufferSize(), bufferSize()); + } + + /** + * Returns an {@code Observable} that emits the results of a specified function to the pair of values emitted by the + * current {@code Observable} and the mapped inner {@link ObservableSource}. + * <p> + * <img width="640" height="390" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/mergeMap.r.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code flatMap} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <U> + * the type of items emitted by the collection {@code ObservableSource} + * @param <R> + * the type of items emitted by the resulting {@code Observable} + * @param mapper + * a function that returns an {@code ObservableSource} for each item emitted by the current {@code Observable} + * @param combiner + * a function that combines one item emitted by each of the source and collection {@code ObservableSource}s and + * returns an item to be emitted by the resulting {@code Observable} + * @param delayErrors + * if {@code true}, exceptions from the current {@code Observable} and all inner {@code ObservableSource}s are delayed until all of them terminate + * if {@code false}, the first one signaling an exception will terminate the whole sequence immediately + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code mapper} or {@code combiner} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/flatmap.html">ReactiveX operators documentation: FlatMap</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull U, @NonNull R> Observable<R> flatMap(@NonNull Function<? super T, ? extends ObservableSource<? extends U>> mapper, + @NonNull BiFunction<? super T, ? super U, ? extends R> combiner, boolean delayErrors) { + return flatMap(mapper, combiner, delayErrors, bufferSize(), bufferSize()); + } + + /** + * Returns an {@code Observable} that emits the results of a specified function to the pair of values emitted by the + * current {@code Observable} and the mapped inner {@link ObservableSource}, while limiting the maximum number of concurrent + * subscriptions to these {@code ObservableSource}s. + * <p> + * <img width="640" height="390" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/mergeMap.r.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code flatMap} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <U> + * the type of items emitted by the collection {@code ObservableSource} + * @param <R> + * the type of items emitted by the resulting {@code Observable} + * @param mapper + * a function that returns an {@code ObservableSource} for each item emitted by the current {@code Observable} + * @param combiner + * a function that combines one item emitted by each of the source and collection {@code ObservableSource}s and + * returns an item to be emitted by the resulting {@code Observable} + * @param maxConcurrency + * the maximum number of {@code ObservableSource}s that may be subscribed to concurrently + * @param delayErrors + * if {@code true}, exceptions from the current {@code Observable} and all inner {@code ObservableSource}s are delayed until all of them terminate + * if {@code false}, the first one signaling an exception will terminate the whole sequence immediately + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code mapper} or {@code combiner} is {@code null} + * @throws IllegalArgumentException if {@code maxConcurrency} is non-positive + * @see <a href="http://reactivex.io/documentation/operators/flatmap.html">ReactiveX operators documentation: FlatMap</a> + * @since 2.0 + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull U, @NonNull R> Observable<R> flatMap(@NonNull Function<? super T, ? extends ObservableSource<? extends U>> mapper, + @NonNull BiFunction<? super T, ? super U, ? extends R> combiner, boolean delayErrors, int maxConcurrency) { + return flatMap(mapper, combiner, delayErrors, maxConcurrency, bufferSize()); + } + + /** + * Returns an {@code Observable} that emits the results of a specified function to the pair of values emitted by the + * current {@code Observable} and the mapped inner {@link ObservableSource}, while limiting the maximum number of concurrent + * subscriptions to these {@code ObservableSource}s. + * <p> + * <img width="640" height="390" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/mergeMap.r.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code flatMap} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <U> + * the type of items emitted by the collection {@code ObservableSource} + * @param <R> + * the type of items emitted by the resulting {@code Observable} + * @param mapper + * a function that returns an {@code ObservableSource} for each item emitted by the current {@code Observable} + * @param combiner + * a function that combines one item emitted by each of the source and collection {@code ObservableSource}s and + * returns an item to be emitted by the resulting {@code Observable} + * @param maxConcurrency + * the maximum number of {@code ObservableSource}s that may be subscribed to concurrently + * @param delayErrors + * if {@code true}, exceptions from the current {@code Observable} and all inner {@code ObservableSource}s are delayed until all of them terminate + * if {@code false}, the first one signaling an exception will terminate the whole sequence immediately + * @param bufferSize + * the number of elements expected from the inner {@code ObservableSource} to be buffered + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code mapper} or {@code combiner} is {@code null} + * @throws IllegalArgumentException if {@code maxConcurrency} or {@code bufferSize} is non-positive + * @see <a href="http://reactivex.io/documentation/operators/flatmap.html">ReactiveX operators documentation: FlatMap</a> + * @since 2.0 + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull U, @NonNull R> Observable<R> flatMap(@NonNull Function<? super T, ? extends ObservableSource<? extends U>> mapper, + @NonNull BiFunction<? super T, ? super U, ? extends R> combiner, boolean delayErrors, int maxConcurrency, int bufferSize) { + Objects.requireNonNull(mapper, "mapper is null"); + Objects.requireNonNull(combiner, "combiner is null"); + return flatMap(ObservableInternalHelper.flatMapWithCombiner(mapper, combiner), delayErrors, maxConcurrency, bufferSize); + } + + /** + * Returns an {@code Observable} that emits the results of a specified function to the pair of values emitted by the + * current {@code Observable} and the mapped inner {@link ObservableSource}, while limiting the maximum number of concurrent + * subscriptions to these {@code ObservableSource}s. + * <p> + * <img width="640" height="390" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/mergeMap.r.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code flatMap} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <U> + * the type of items emitted by the collection {@code ObservableSource} + * @param <R> + * the type of items emitted by the resulting {@code Observable} + * @param mapper + * a function that returns an {@code ObservableSource} for each item emitted by the current {@code Observable} + * @param combiner + * a function that combines one item emitted by each of the source and collection {@code ObservableSource}s and + * returns an item to be emitted by the resulting {@code Observable} + * @param maxConcurrency + * the maximum number of {@code ObservableSource}s that may be subscribed to concurrently + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code mapper} or {@code combiner} is {@code null} + * @throws IllegalArgumentException if {@code maxConcurrency} is non-positive + * @see <a href="http://reactivex.io/documentation/operators/flatmap.html">ReactiveX operators documentation: FlatMap</a> + * @since 2.0 + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull U, @NonNull R> Observable<R> flatMap(@NonNull Function<? super T, ? extends ObservableSource<? extends U>> mapper, + @NonNull BiFunction<? super T, ? super U, ? extends R> combiner, int maxConcurrency) { + return flatMap(mapper, combiner, false, maxConcurrency, bufferSize()); + } + + /** + * Maps each element of the current {@code Observable} into {@link CompletableSource}s, subscribes to them and + * waits until the upstream and all {@code CompletableSource}s complete. + * <p> + * <img width="640" height="424" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/flatMapCompletable.o.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code flatMapCompletable} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param mapper the function that received each source value and transforms them into {@code CompletableSource}s. + * @throws NullPointerException if {@code mapper} is {@code null} + * @return the new {@link Completable} instance + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Completable flatMapCompletable(@NonNull Function<? super T, ? extends CompletableSource> mapper) { + return flatMapCompletable(mapper, false); + } + + /** + * Maps each element of the current {@code Observable} into {@link CompletableSource}s, subscribes to them and + * waits until the upstream and all {@code CompletableSource}s complete, optionally delaying all errors. + * <p> + * <img width="640" height="362" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/flatMapCompletableDelayError.o.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code flatMapCompletable} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param mapper the function that received each source value and transforms them into {@code CompletableSource}s. + * @param delayErrors if {@code true}, errors from the upstream and inner {@code CompletableSource}s are delayed until all of them + * terminate. + * @return the new {@link Completable} instance + * @throws NullPointerException if {@code mapper} is {@code null} + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Completable flatMapCompletable(@NonNull Function<? super T, ? extends CompletableSource> mapper, boolean delayErrors) { + Objects.requireNonNull(mapper, "mapper is null"); + return RxJavaPlugins.onAssembly(new ObservableFlatMapCompletableCompletable<>(this, mapper, delayErrors)); + } + + /** + * Merges {@link Iterable}s generated by a mapper {@link Function} for each individual item emitted by + * the current {@code Observable} into a single {@code Observable} sequence. + * <p> + * <img width="640" height="343" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/flatMapIterable.o.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code flatMapIterable} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <U> + * the output type and the element type of the {@code Iterable}s + * @param mapper + * a function that returns an {@code Iterable} sequence of values for when given an item emitted by the + * current {@code Observable} + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code mapper} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/flatmap.html">ReactiveX operators documentation: FlatMap</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull U> Observable<U> flatMapIterable(@NonNull Function<? super T, @NonNull ? extends Iterable<? extends U>> mapper) { + Objects.requireNonNull(mapper, "mapper is null"); + return RxJavaPlugins.onAssembly(new ObservableFlattenIterable<>(this, mapper)); + } + + /** + * Merges {@link Iterable}s generated by a mapper {@link Function} for each individual item emitted by + * the current {@code Observable} into a single {@code Observable} sequence where the resulting items will + * be the combination of the original item and each inner item of the respective {@code Iterable} as returned + * by the {@code resultSelector} {@link BiFunction}. + * <p> + * <img width="640" height="410" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/flatMapIterable.o.r.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code flatMapIterable} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <U> + * the element type of the {@code Iterable}s + * @param <V> + * the output type as determined by the {@code resultSelector} function + * @param mapper + * a function that returns an {@code Iterable} sequence of values for each item emitted by the current + * {@code Observable} + * @param combiner + * a function that returns an item based on the item emitted by the current {@code Observable} and the + * next item of the {@code Iterable} returned for that original item by the {@code mapper} + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code mapper} or {@code combiner} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/flatmap.html">ReactiveX operators documentation: FlatMap</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull U, @NonNull V> Observable<V> flatMapIterable(@NonNull Function<? super T, @NonNull ? extends Iterable<? extends U>> mapper, + @NonNull BiFunction<? super T, ? super U, ? extends V> combiner) { + Objects.requireNonNull(mapper, "mapper is null"); + Objects.requireNonNull(combiner, "combiner is null"); + return flatMap(ObservableInternalHelper.flatMapIntoIterable(mapper), combiner, false, bufferSize(), bufferSize()); + } + + /** + * Maps each element of the current {@code Observable} into {@link MaybeSource}s, subscribes to all of them + * and merges their {@code onSuccess} values, in no particular order, into a single {@code Observable} sequence. + * <p> + * <img width="640" height="310" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/flatMapMaybe.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code flatMapMaybe} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <R> the result value type + * @param mapper the function that received each source value and transforms them into {@code MaybeSource}s. + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code mapper} is {@code null} + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull R> Observable<R> flatMapMaybe(@NonNull Function<? super T, ? extends MaybeSource<? extends R>> mapper) { + return flatMapMaybe(mapper, false); + } + + /** + * Maps each element of the current {@code Observable} into {@link MaybeSource}s, subscribes to them + * and merges their {@code onSuccess} values, in no particular order, into a single {@code Observable} sequence, + * optionally delaying all errors. + * <p> + * <img width="640" height="310" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/flatMapMaybe.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code flatMapMaybe} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <R> the result value type + * @param mapper the function that received each source value and transforms them into {@code MaybeSource}s. + * @param delayErrors if {@code true}, errors from the upstream and inner {@code MaybeSource}s are delayed until all of them + * terminate. + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code mapper} is {@code null} + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull R> Observable<R> flatMapMaybe(@NonNull Function<? super T, ? extends MaybeSource<? extends R>> mapper, boolean delayErrors) { + Objects.requireNonNull(mapper, "mapper is null"); + return RxJavaPlugins.onAssembly(new ObservableFlatMapMaybe<>(this, mapper, delayErrors)); + } + + /** + * Maps each element of the current {@code Observable} into {@link SingleSource}s, subscribes to all of them + * and merges their {@code onSuccess} values, in no particular order, into a single {@code Observable} sequence. + * <p> + * <img width="640" height="310" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/flatMapSingle.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code flatMapSingle} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <R> the result value type + * @param mapper the function that received each source value and transforms them into {@code SingleSource}s. + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code mapper} is {@code null} + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull R> Observable<R> flatMapSingle(@NonNull Function<? super T, ? extends SingleSource<? extends R>> mapper) { + return flatMapSingle(mapper, false); + } + + /** + * Maps each element of the current {@code Observable} into {@link SingleSource}s, subscribes to them + * and merges their {@code onSuccess} values, in no particular order, into a single {@code Observable} sequence, + * optionally delaying all errors. + * <p> + * <img width="640" height="310" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/flatMapSingle.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code flatMapSingle} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <R> the result value type + * @param mapper the function that received each source value and transforms them into {@code SingleSource}s. + * @param delayErrors if {@code true}, errors from the upstream and inner {@code SingleSource}s are delayed until each of them + * terminates. + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code mapper} is {@code null} + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull R> Observable<R> flatMapSingle(@NonNull Function<? super T, ? extends SingleSource<? extends R>> mapper, boolean delayErrors) { + Objects.requireNonNull(mapper, "mapper is null"); + return RxJavaPlugins.onAssembly(new ObservableFlatMapSingle<>(this, mapper, delayErrors)); + } + + /** + * Subscribes to the {@link ObservableSource} and calls a {@link Consumer} for each item of the current {@code Observable} + * on its emission thread. + * <p> + * <img width="640" height="264" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/forEach.o.png" alt=""> + * <p> + * Alias to {@link #subscribe(Consumer)} + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code forEach} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param onNext + * the {@code Consumer} to execute for each item. + * @return + * a {@link Disposable} that allows disposing the sequence if the current {@code Observable} runs asynchronously + * @throws NullPointerException + * if {@code onNext} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/subscribe.html">ReactiveX operators documentation: Subscribe</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Disposable forEach(@NonNull Consumer<? super T> onNext) { + return subscribe(onNext); + } + + /** + * Subscribes to the {@link ObservableSource} and calls a {@link Predicate} for each item of the current {@code Observable}, + * on its emission thread, until the predicate returns {@code false}. + * <p> + * <img width="640" height="273" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/forEachWhile.o.png" alt=""> + * <p> + * If the {@code Observable} emits an error, it is wrapped into an + * {@link OnErrorNotImplementedException} + * and routed to the {@link RxJavaPlugins#onError(Throwable)} handler. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code forEachWhile} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param onNext + * the {@code Predicate} to execute for each item. + * @return + * a {@link Disposable} that allows disposing the sequence if the current {@code Observable} runs asynchronously + * @throws NullPointerException + * if {@code onNext} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/subscribe.html">ReactiveX operators documentation: Subscribe</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Disposable forEachWhile(@NonNull Predicate<? super T> onNext) { + return forEachWhile(onNext, Functions.ON_ERROR_MISSING, Functions.EMPTY_ACTION); + } + + /** + * Subscribes to the {@link ObservableSource} and calls a {@link Predicate} for each item or a {@link Consumer} with the error + * of the current {@code Observable}, on their original emission threads, until the predicate returns {@code false}. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code forEachWhile} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param onNext + * the {@code Predicate} to execute for each item. + * @param onError + * the {@code Consumer} to execute when an error is emitted. + * @return + * a {@link Disposable} that allows disposing the sequence if the current {@code Observable} runs asynchronously + * @throws NullPointerException + * if {@code onNext} or {@code onError} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/subscribe.html">ReactiveX operators documentation: Subscribe</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Disposable forEachWhile(@NonNull Predicate<? super T> onNext, @NonNull Consumer<? super Throwable> onError) { + return forEachWhile(onNext, onError, Functions.EMPTY_ACTION); + } + + /** + * Subscribes to the {@link ObservableSource} and calls a {@link Predicate} for each item, a {@link Consumer} with the error + * or an {@link Action} upon completion of the current {@code Observable}, on their original emission threads, + * until the predicate returns {@code false}. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code forEachWhile} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param onNext + * the {@code Predicate} to execute for each item. + * @param onError + * the {@code Consumer} to execute when an error is emitted. + * @param onComplete + * the {@code Action} to execute when completion is signaled. + * @return + * a {@link Disposable} that allows disposing the sequence if the current {@code Observable} runs asynchronously + * @throws NullPointerException + * if {@code onNext} or {@code onError} or {@code onComplete} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/subscribe.html">ReactiveX operators documentation: Subscribe</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Disposable forEachWhile(@NonNull Predicate<? super T> onNext, @NonNull Consumer<? super Throwable> onError, + @NonNull Action onComplete) { + Objects.requireNonNull(onNext, "onNext is null"); + Objects.requireNonNull(onError, "onError is null"); + Objects.requireNonNull(onComplete, "onComplete is null"); + + ForEachWhileObserver<T> o = new ForEachWhileObserver<>(onNext, onError, onComplete); + subscribe(o); + return o; + } + + /** + * Groups the items emitted by the current {@code Observable} according to a specified criterion, and emits these + * grouped items as {@link GroupedObservable}s. + * <p> + * <img width="640" height="360" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/groupBy.v3.png" alt=""> + * <p> + * Each emitted {@code GroupedObservable} allows only a single {@link Observer} to subscribe to it during its + * lifetime and if this {@code Observer} calls {@code dispose()} before the + * source terminates, the next emission by the source having the same key will trigger a new + * {@code GroupedObservable} emission. + * <p> + * <em>Note:</em> A {@code GroupedObservable} will cache the items it is to emit until such time as it + * is subscribed to. For this reason, in order to avoid memory leaks, you should not simply ignore those + * {@code GroupedObservable}s that do not concern you. Instead, you can signal to them that they may + * discard their buffers by applying an operator like {@link #ignoreElements} to them. + * <p> + * Note also that ignoring groups or subscribing later (i.e., on another thread) will result in + * so-called group abandonment where a group will only contain one element and the group will be + * re-created over and over as new upstream items trigger a new group. The behavior is + * a trade-off between no-dataloss, upstream cancellation and excessive group creation. + * + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code groupBy} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param keySelector + * a function that extracts the key for each item + * @param <K> + * the key type + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code keySelector} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/groupby.html">ReactiveX operators documentation: GroupBy</a> + */ + @SuppressWarnings({ "unchecked", "rawtypes" }) + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull K> Observable<GroupedObservable<K, T>> groupBy(@NonNull Function<? super T, ? extends K> keySelector) { + return groupBy(keySelector, (Function)Functions.identity(), false, bufferSize()); + } + + /** + * Groups the items emitted by the current {@code Observable} according to a specified criterion, and emits these + * grouped items as {@link GroupedObservable}s. + * <p> + * <img width="640" height="360" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/groupBy.v3.png" alt=""> + * <p> + * Each emitted {@code GroupedObservable} allows only a single {@link Observer} to subscribe to it during its + * lifetime and if this {@code Observer} calls {@code dispose()} before the + * source terminates, the next emission by the source having the same key will trigger a new + * {@code GroupedObservable} emission. + * <p> + * <em>Note:</em> A {@code GroupedObservable} will cache the items it is to emit until such time as it + * is subscribed to. For this reason, in order to avoid memory leaks, you should not simply ignore those + * {@code GroupedObservable}s that do not concern you. Instead, you can signal to them that they may + * discard their buffers by applying an operator like {@link #ignoreElements} to them. + * <p> + * Note also that ignoring groups or subscribing later (i.e., on another thread) will result in + * so-called group abandonment where a group will only contain one element and the group will be + * re-created over and over as new upstream items trigger a new group. The behavior is + * a trade-off between no-dataloss, upstream cancellation and excessive group creation. + * + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code groupBy} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param keySelector + * a function that extracts the key for each item + * @param <K> + * the key type + * @param delayError + * if {@code true}, the exception from the current {@code Observable} is delayed in each group until that specific group emitted + * the normal values; if {@code false}, the exception bypasses values in the groups and is reported immediately. + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code keySelector} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/groupby.html">ReactiveX operators documentation: GroupBy</a> + */ + @SuppressWarnings({ "unchecked", "rawtypes" }) + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull K> Observable<GroupedObservable<K, T>> groupBy(@NonNull Function<? super T, ? extends K> keySelector, boolean delayError) { + return groupBy(keySelector, (Function)Functions.identity(), delayError, bufferSize()); + } + + /** + * Groups the items emitted by the current {@code Observable} according to a specified criterion, and emits these + * grouped items as {@link GroupedObservable}s. + * <p> + * <img width="640" height="360" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/groupBy.v3.png" alt=""> + * <p> + * Each emitted {@code GroupedObservable} allows only a single {@link Observer} to subscribe to it during its + * lifetime and if this {@code Observer} calls {@code dispose()} before the + * source terminates, the next emission by the source having the same key will trigger a new + * {@code GroupedObservable} emission. + * <p> + * <em>Note:</em> A {@code GroupedObservable} will cache the items it is to emit until such time as it + * is subscribed to. For this reason, in order to avoid memory leaks, you should not simply ignore those + * {@code GroupedObservable}s that do not concern you. Instead, you can signal to them that they may + * discard their buffers by applying an operator like {@link #ignoreElements} to them. + * <p> + * Note also that ignoring groups or subscribing later (i.e., on another thread) will result in + * so-called group abandonment where a group will only contain one element and the group will be + * re-created over and over as new upstream items trigger a new group. The behavior is + * a trade-off between no-dataloss, upstream cancellation and excessive group creation. + * + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code groupBy} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param keySelector + * a function that extracts the key for each item + * @param valueSelector + * a function that extracts the return element for each item + * @param <K> + * the key type + * @param <V> + * the element type + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code keySelector} or {@code valueSelector} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/groupby.html">ReactiveX operators documentation: GroupBy</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull K, @NonNull V> Observable<GroupedObservable<K, V>> groupBy(@NonNull Function<? super T, ? extends K> keySelector, + Function<? super T, ? extends V> valueSelector) { + return groupBy(keySelector, valueSelector, false, bufferSize()); + } + + /** + * Groups the items emitted by the current {@code Observable} according to a specified criterion, and emits these + * grouped items as {@link GroupedObservable}s. + * <p> + * <img width="640" height="360" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/groupBy.v3.png" alt=""> + * <p> + * Each emitted {@code GroupedObservable} allows only a single {@link Observer} to subscribe to it during its + * lifetime and if this {@code Observer} calls {@code dispose()} before the + * source terminates, the next emission by the source having the same key will trigger a new + * {@code GroupedObservable} emission. + * <p> + * <em>Note:</em> A {@code GroupedObservable} will cache the items it is to emit until such time as it + * is subscribed to. For this reason, in order to avoid memory leaks, you should not simply ignore those + * {@code GroupedObservable}s that do not concern you. Instead, you can signal to them that they may + * discard their buffers by applying an operator like {@link #ignoreElements} to them. + * <p> + * Note also that ignoring groups or subscribing later (i.e., on another thread) will result in + * so-called group abandonment where a group will only contain one element and the group will be + * re-created over and over as new upstream items trigger a new group. The behavior is + * a trade-off between no-dataloss, upstream cancellation and excessive group creation. + * + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code groupBy} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param keySelector + * a function that extracts the key for each item + * @param valueSelector + * a function that extracts the return element for each item + * @param <K> + * the key type + * @param <V> + * the element type + * @param delayError + * if {@code true}, the exception from the current {@code Observable} is delayed in each group until that specific group emitted + * the normal values; if {@code false}, the exception bypasses values in the groups and is reported immediately. + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code keySelector} or {@code valueSelector} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/groupby.html">ReactiveX operators documentation: GroupBy</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull K, @NonNull V> Observable<GroupedObservable<K, V>> groupBy(@NonNull Function<? super T, ? extends K> keySelector, + @NonNull Function<? super T, ? extends V> valueSelector, boolean delayError) { + return groupBy(keySelector, valueSelector, delayError, bufferSize()); + } + + /** + * Groups the items emitted by the current {@code Observable} according to a specified criterion, and emits these + * grouped items as {@link GroupedObservable}s. + * <p> + * <img width="640" height="360" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/groupBy.v3.png" alt=""> + * <p> + * Each emitted {@code GroupedObservable} allows only a single {@link Observer} to subscribe to it during its + * lifetime and if this {@code Observer} calls {@code dispose()} before the + * source terminates, the next emission by the source having the same key will trigger a new + * {@code GroupedObservable} emission. + * <p> + * <em>Note:</em> A {@code GroupedObservable} will cache the items it is to emit until such time as it + * is subscribed to. For this reason, in order to avoid memory leaks, you should not simply ignore those + * {@code GroupedObservable}s that do not concern you. Instead, you can signal to them that they may + * discard their buffers by applying an operator like {@link #ignoreElements} to them. + * <p> + * Note also that ignoring groups or subscribing later (i.e., on another thread) will result in + * so-called group abandonment where a group will only contain one element and the group will be + * re-created over and over as new upstream items trigger a new group. The behavior is + * a trade-off between no-dataloss, upstream cancellation and excessive group creation. + * + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code groupBy} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param keySelector + * a function that extracts the key for each item + * @param valueSelector + * a function that extracts the return element for each item + * @param delayError + * if {@code true}, the exception from the current {@code Observable} is delayed in each group until that specific group emitted + * the normal values; if {@code false}, the exception bypasses values in the groups and is reported immediately. + * @param bufferSize + * the hint for how many {@code GroupedObservable}s and element in each {@code GroupedObservable} should be buffered + * @param <K> + * the key type + * @param <V> + * the element type + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code keySelector} or {@code valueSelector} is {@code null} + * @throws IllegalArgumentException if {@code bufferSize} is non-positive + * @see <a href="http://reactivex.io/documentation/operators/groupby.html">ReactiveX operators documentation: GroupBy</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull K, @NonNull V> Observable<GroupedObservable<K, V>> groupBy(@NonNull Function<? super T, ? extends K> keySelector, + @NonNull Function<? super T, ? extends V> valueSelector, + boolean delayError, int bufferSize) { + Objects.requireNonNull(keySelector, "keySelector is null"); + Objects.requireNonNull(valueSelector, "valueSelector is null"); + ObjectHelper.verifyPositive(bufferSize, "bufferSize"); + + return RxJavaPlugins.onAssembly(new ObservableGroupBy<>(this, keySelector, valueSelector, bufferSize, delayError)); + } + + /** + * Returns an {@code Observable} that correlates two {@link ObservableSource}s when they overlap in time and groups the results. + * <p> + * There are no guarantees in what order the items get combined when multiple + * items from one or both source {@code ObservableSource}s overlap. + * <p> + * <img width="640" height="380" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/groupJoin.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code groupJoin} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <TRight> the value type of the right {@code ObservableSource} source + * @param <TLeftEnd> the element type of the left duration {@code ObservableSource}s + * @param <TRightEnd> the element type of the right duration {@code ObservableSource}s + * @param <R> the result type + * @param other + * the other {@code ObservableSource} to correlate items from the current {@code Observable} with + * @param leftEnd + * a function that returns an {@code ObservableSource} whose emissions indicate the duration of the values of + * the current {@code Observable} + * @param rightEnd + * a function that returns an {@code ObservableSource} whose emissions indicate the duration of the values of + * the {@code right} {@code ObservableSource} + * @param resultSelector + * a function that takes an item emitted by each {@code ObservableSource} and returns the value to be emitted + * by the resulting {@code Observable} + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code other}, {@code leftEnd}, {@code rightEnd} or {@code resultSelector} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/join.html">ReactiveX operators documentation: Join</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull TRight, @NonNull TLeftEnd, @NonNull TRightEnd, @NonNull R> Observable<R> groupJoin( + @NonNull ObservableSource<? extends TRight> other, + @NonNull Function<? super T, ? extends ObservableSource<TLeftEnd>> leftEnd, + @NonNull Function<? super TRight, ? extends ObservableSource<TRightEnd>> rightEnd, + @NonNull BiFunction<? super T, ? super Observable<TRight>, ? extends R> resultSelector + ) { + Objects.requireNonNull(other, "other is null"); + Objects.requireNonNull(leftEnd, "leftEnd is null"); + Objects.requireNonNull(rightEnd, "rightEnd is null"); + Objects.requireNonNull(resultSelector, "resultSelector is null"); + return RxJavaPlugins.onAssembly(new ObservableGroupJoin<>( + this, other, leftEnd, rightEnd, resultSelector)); + } + + /** + * Hides the identity of the current {@code Observable} and its {@link Disposable}. + * <p> + * Allows hiding extra features such as {@link io.reactivex.rxjava3.subjects.Subject}'s + * {@link Observer} methods or preventing certain identity-based + * optimizations (fusion). + * <p> + * <img width="640" height="283" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/hide.o.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code hide} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @return the new {@code Observable} instance + * + * @since 2.0 + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Observable<T> hide() { + return RxJavaPlugins.onAssembly(new ObservableHide<>(this)); + } + + /** + * Ignores all items emitted by the current {@code Observable} and only calls {@code onComplete} or {@code onError}. + * <p> + * <img width="640" height="305" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/ignoreElements.2.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code ignoreElements} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @return the new {@link Completable} instance + * @see <a href="http://reactivex.io/documentation/operators/ignoreelements.html">ReactiveX operators documentation: IgnoreElements</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Completable ignoreElements() { + return RxJavaPlugins.onAssembly(new ObservableIgnoreElementsCompletable<>(this)); + } + + /** + * Returns a {@link Single} that emits {@code true} if the current {@code Observable} is empty, otherwise {@code false}. + * <p> + * In Rx.Net this is negated as the {@code any} {@link Observer} but we renamed this in RxJava to better match Java + * naming idioms. + * <p> + * <img width="640" height="320" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/isEmpty.2.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code isEmpty} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @return the new {@code Single} instance + * @see <a href="http://reactivex.io/documentation/operators/contains.html">ReactiveX operators documentation: Contains</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Single<Boolean> isEmpty() { + return all(Functions.alwaysFalse()); + } + + /** + * Correlates the items emitted by two {@link ObservableSource}s based on overlapping durations. + * <p> + * There are no guarantees in what order the items get combined when multiple + * items from one or both source {@code ObservableSource}s overlap. + * <p> + * <img width="640" height="380" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/join_.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code join} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <TRight> the value type of the right {@code ObservableSource} source + * @param <TLeftEnd> the element type of the left duration {@code ObservableSource}s + * @param <TRightEnd> the element type of the right duration {@code ObservableSource}s + * @param <R> the result type + * @param other + * the second {@code ObservableSource} to join items from + * @param leftEnd + * a function to select a duration for each item emitted by the current {@code Observable}, used to + * determine overlap + * @param rightEnd + * a function to select a duration for each item emitted by the {@code right} {@code ObservableSource}, used to + * determine overlap + * @param resultSelector + * a function that computes an item to be emitted by the resulting {@code Observable} for any two + * overlapping items emitted by the two {@code ObservableSource}s + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code other}, {@code leftEnd}, {@code rightEnd} or {@code resultSelector} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/join.html">ReactiveX operators documentation: Join</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull TRight, @NonNull TLeftEnd, @NonNull TRightEnd, @NonNull R> Observable<R> join( + @NonNull ObservableSource<? extends TRight> other, + @NonNull Function<? super T, ? extends ObservableSource<TLeftEnd>> leftEnd, + @NonNull Function<? super TRight, ? extends ObservableSource<TRightEnd>> rightEnd, + @NonNull BiFunction<? super T, ? super TRight, ? extends R> resultSelector + ) { + Objects.requireNonNull(other, "other is null"); + Objects.requireNonNull(leftEnd, "leftEnd is null"); + Objects.requireNonNull(rightEnd, "rightEnd is null"); + Objects.requireNonNull(resultSelector, "resultSelector is null"); + return RxJavaPlugins.onAssembly(new ObservableJoin<T, TRight, TLeftEnd, TRightEnd, R>( + this, other, leftEnd, rightEnd, resultSelector)); + } + + /** + * Returns a {@link Maybe} that emits the last item emitted by the current {@code Observable} or + * completes if the current {@code Observable} is empty. + * <p> + * <img width="640" height="305" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/lastElement.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code lastElement} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @return the new {@code Maybe} instance + * @see <a href="http://reactivex.io/documentation/operators/last.html">ReactiveX operators documentation: Last</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Maybe<T> lastElement() { + return RxJavaPlugins.onAssembly(new ObservableLastMaybe<>(this)); + } + + /** + * Returns a {@link Single} that emits only the last item emitted by the current {@code Observable}, or a default item + * if the current {@code Observable} completes without emitting any items. + * <p> + * <img width="640" height="305" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/last.2.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code last} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param defaultItem + * the default item to emit if the current {@code Observable} is empty + * @return the new {@code Single} instance + * @throws NullPointerException if {@code defaultItem} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/last.html">ReactiveX operators documentation: Last</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Single<T> last(@NonNull T defaultItem) { + Objects.requireNonNull(defaultItem, "defaultItem is null"); + return RxJavaPlugins.onAssembly(new ObservableLastSingle<>(this, defaultItem)); + } + + /** + * Returns a {@link Single} that emits only the last item emitted by the current {@code Observable} or + * signals a {@link NoSuchElementException} if the current {@code Observable} is empty. + * <p> + * <img width="640" height="236" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/lastOrError.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code lastOrError} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @return the new {@code Single} instance + * @see <a href="http://reactivex.io/documentation/operators/last.html">ReactiveX operators documentation: Last</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Single<T> lastOrError() { + return RxJavaPlugins.onAssembly(new ObservableLastSingle<>(this, null)); + } + + /** + * <strong>This method requires advanced knowledge about building operators, please consider + * other standard composition methods first;</strong> + * Returns an {@code Observable} which, when subscribed to, invokes the {@link ObservableOperator#apply(Observer) apply(Observer)} method + * of the provided {@link ObservableOperator} for each individual downstream {@link Observer} and allows the + * insertion of a custom operator by accessing the downstream's {@code Observer} during this subscription phase + * and providing a new {@code Observer}, containing the custom operator's intended business logic, that will be + * used in the subscription process going further upstream. + * <p> + * Generally, such a new {@code Observer} will wrap the downstream's {@code Observer} and forwards the + * {@code onNext}, {@code onError} and {@code onComplete} events from the upstream directly or according to the + * emission pattern the custom operator's business logic requires. In addition, such operator can intercept the + * flow control calls of {@code dispose} and {@code isDisposed} that would have traveled upstream and perform + * additional actions depending on the same business logic requirements. + * <p> + * Example: + * <pre><code> + * // Step 1: Create the consumer type that will be returned by the ObservableOperator.apply(): + * + * public final class CustomObserver<T> implements Observer<T>, Disposable { + * + * // The downstream's Observer that will receive the onXXX events + * final Observer<? super String> downstream; + * + * // The connection to the upstream source that will call this class' onXXX methods + * Disposable upstream; + * + * // The constructor takes the downstream subscriber and usually any other parameters + * public CustomObserver(Observer<? super String> downstream) { + * this.downstream = downstream; + * } + * + * // In the subscription phase, the upstream sends a Disposable to this class + * // and subsequently this class has to send a Disposable to the downstream. + * // Note that relaying the upstream's Disposable directly is not allowed in RxJava + * @Override + * public void onSubscribe(Disposable d) { + * if (upstream != null) { + * d.dispose(); + * } else { + * upstream = d; + * downstream.onSubscribe(this); + * } + * } + * + * // The upstream calls this with the next item and the implementation's + * // responsibility is to emit an item to the downstream based on the intended + * // business logic, or if it can't do so for the particular item, + * // request more from the upstream + * @Override + * public void onNext(T item) { + * String str = item.toString(); + * if (str.length() < 2) { + * downstream.onNext(str); + * } + * // Observable doesn't support backpressure, therefore, there is no + * // need or opportunity to call upstream.request(1) if an item + * // is not produced to the downstream + * } + * + * // Some operators may handle the upstream's error while others + * // could just forward it to the downstream. + * @Override + * public void onError(Throwable throwable) { + * downstream.onError(throwable); + * } + * + * // When the upstream completes, usually the downstream should complete as well. + * @Override + * public void onComplete() { + * downstream.onComplete(); + * } + * + * // Some operators may use their own resources which should be cleaned up if + * // the downstream disposes the flow before it completed. Operators without + * // resources can simply forward the dispose to the upstream. + * // In some cases, a disposed flag may be set by this method so that other parts + * // of this class may detect the dispose and stop sending events + * // to the downstream. + * @Override + * public void dispose() { + * upstream.dispose(); + * } + * + * // Some operators may simply forward the call to the upstream while others + * // can return the disposed flag set in dispose(). + * @Override + * public boolean isDisposed() { + * return upstream.isDisposed(); + * } + * } + * + * // Step 2: Create a class that implements the ObservableOperator interface and + * // returns the custom consumer type from above in its apply() method. + * // Such class may define additional parameters to be submitted to + * // the custom consumer type. + * + * final class CustomOperator<T> implements ObservableOperator<String, T> { + * @Override + * public Observer<T> apply(Observer<? super String> downstream) { + * return new CustomObserver<T>(downstream); + * } + * } + * + * // Step 3: Apply the custom operator via lift() in a flow by creating an instance of it + * // or reusing an existing one. + * + * Observable.range(5, 10) + * .lift(new CustomOperator<Integer>()) + * .test() + * .assertResult("5", "6", "7", "8", "9"); + * </code></pre> + * <p> + * Creating custom operators can be complicated and it is recommended one consults the + * <a href="https://github.com/ReactiveX/RxJava/wiki/Writing-operators-for-2.0">RxJava wiki: Writing operators</a> page about + * the tools, requirements, rules, considerations and pitfalls of implementing them. + * <p> + * Note that implementing custom operators via this {@code lift()} method adds slightly more overhead by requiring + * an additional allocation and indirection per assembled flows. Instead, extending the abstract {@code Observable} + * class and creating an {@link ObservableTransformer} with it is recommended. + * <p> + * Note also that it is not possible to stop the subscription phase in {@code lift()} as the {@code apply()} method + * requires a non-{@code null} {@code Observer} instance to be returned, which is then unconditionally subscribed to + * the current {@code Observable}. For example, if the operator decided there is no reason to subscribe to the + * upstream source because of some optimization possibility or a failure to prepare the operator, it still has to + * return an {@code Observer} that should immediately dispose the upstream's {@link Disposable} in its + * {@code onSubscribe} method. Again, using an {@code ObservableTransformer} and extending the {@code Observable} is + * a better option as {@link #subscribeActual} can decide to not subscribe to its upstream after all. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code lift} does not operate by default on a particular {@link Scheduler}, however, the + * {@code ObservableOperator} may use a {@code Scheduler} to support its own asynchronous behavior.</dd> + * </dl> + * + * @param <R> the output value type + * @param lifter the {@code ObservableOperator} that receives the downstream's {@code Observer} and should return + * an {@code Observer} with custom behavior to be used as the consumer for the current + * {@code Observable}. + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code lifter} is {@code null} + * @see <a href="https://github.com/ReactiveX/RxJava/wiki/Writing-operators-for-2.0">RxJava wiki: Writing operators</a> + * @see #compose(ObservableTransformer) + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull R> Observable<R> lift(@NonNull ObservableOperator<? extends R, ? super T> lifter) { + Objects.requireNonNull(lifter, "lifter is null"); + return RxJavaPlugins.onAssembly(new ObservableLift<>(this, lifter)); + } + + /** + * Returns an {@code Observable} that applies a specified function to each item emitted by the current {@code Observable} and + * emits the results of these function applications. + * <p> + * <img width="640" height="305" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/map.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code map} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <R> the output type + * @param mapper + * a function to apply to each item emitted by the current {@code Observable} + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code mapper} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/map.html">ReactiveX operators documentation: Map</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull R> Observable<R> map(@NonNull Function<? super T, ? extends R> mapper) { + Objects.requireNonNull(mapper, "mapper is null"); + return RxJavaPlugins.onAssembly(new ObservableMap<>(this, mapper)); + } + + /** + * Returns an {@code Observable} that represents all of the emissions <em>and</em> notifications from the current + * {@code Observable} into emissions marked with their original types within {@link Notification} objects. + * <p> + * <img width="640" height="315" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/materialize.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code materialize} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @return the new {@code Observable} instance + * @see <a href="http://reactivex.io/documentation/operators/materialize-dematerialize.html">ReactiveX operators documentation: Materialize</a> + * @see #dematerialize(Function) + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Observable<Notification<T>> materialize() { + return RxJavaPlugins.onAssembly(new ObservableMaterialize<>(this)); + } + + /** + * Flattens the current {@code Observable} and another {@link ObservableSource} into a single {@code Observable} sequence, without any transformation. + * <p> + * <img width="640" height="380" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/merge.v3.png" alt=""> + * <p> + * You can combine items emitted by multiple {@code ObservableSource}s so that they appear as a single {@code ObservableSource}, by + * using the {@code mergeWith} method. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code mergeWith} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param other + * an {@code ObservableSource} to be merged + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code other} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/merge.html">ReactiveX operators documentation: Merge</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Observable<T> mergeWith(@NonNull ObservableSource<? extends T> other) { + Objects.requireNonNull(other, "other is null"); + return merge(this, other); + } + + /** + * Merges the sequence of items of the current {@code Observable} with the success value of the other {@link SingleSource}. + * <p> + * <img width="640" height="380" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/merge.v3.png" alt=""> + * <p> + * The success value of the other {@code SingleSource} can get interleaved at any point of the current + * {@code Observable} sequence. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code mergeWith} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * <p>History: 2.1.10 - experimental + * @param other the {@code SingleSource} whose success value to merge with + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code other} is {@code null} + * @since 2.2 + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Observable<T> mergeWith(@NonNull SingleSource<? extends T> other) { + Objects.requireNonNull(other, "other is null"); + return RxJavaPlugins.onAssembly(new ObservableMergeWithSingle<>(this, other)); + } + + /** + * Merges the sequence of items of the current {@code Observable} with the success value of the other {@link MaybeSource} + * or waits both to complete normally if the {@code MaybeSource} is empty. + * <p> + * <img width="640" height="380" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/merge.v3.png" alt=""> + * <p> + * The success value of the other {@code MaybeSource} can get interleaved at any point of the current + * {@code Observable} sequence. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code mergeWith} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * <p>History: 2.1.10 - experimental + * @param other the {@code MaybeSource} which provides a success value to merge with or completes + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code other} is {@code null} + * @since 2.2 + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Observable<T> mergeWith(@NonNull MaybeSource<? extends T> other) { + Objects.requireNonNull(other, "other is null"); + return RxJavaPlugins.onAssembly(new ObservableMergeWithMaybe<>(this, other)); + } + + /** + * Relays the items of the current {@code Observable} and completes only when the other {@link CompletableSource} completes + * as well. + * <p> + * <img width="640" height="380" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/merge.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code mergeWith} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * <p>History: 2.1.10 - experimental + * @param other the {@code CompletableSource} to await for completion + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code other} is {@code null} + * @since 2.2 + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Observable<T> mergeWith(@NonNull CompletableSource other) { + Objects.requireNonNull(other, "other is null"); + return RxJavaPlugins.onAssembly(new ObservableMergeWithCompletable<>(this, other)); + } + + /** + * Returns an {@code Observable} to perform the current {@code Observable}'s emissions and notifications on a specified {@link Scheduler}, + * asynchronously with an unbounded buffer with {@link Flowable#bufferSize()} "island size". + * + * <p>Note that {@code onError} notifications will cut ahead of {@code onNext} notifications on the emission thread if {@code Scheduler} is truly + * asynchronous. If strict event ordering is required, consider using the {@link #observeOn(Scheduler, boolean)} overload. + * <p> + * <img width="640" height="308" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/observeOn.v3.png" alt=""> + * <p> + * This operator keeps emitting as many signals as it can on the given {@code Scheduler}'s worker thread, + * which may result in a longer than expected occupation of this thread. In other terms, + * it does not allow per-signal fairness in case the worker runs on a shared underlying thread. + * If such fairness and signal/work interleaving is preferred, use the delay operator with zero time instead. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>You specify which {@code Scheduler} this operator will use.</dd> + * </dl> + * <p>"Island size" indicates how large chunks the unbounded buffer allocates to store the excess elements waiting to be consumed + * on the other side of the asynchronous boundary. + * + * @param scheduler + * the {@code Scheduler} to notify {@link Observer}s on + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code scheduler} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/observeon.html">ReactiveX operators documentation: ObserveOn</a> + * @see <a href="http://www.grahamlea.com/2014/07/rxjava-threading-examples/">RxJava Threading Examples</a> + * @see #subscribeOn + * @see #observeOn(Scheduler, boolean) + * @see #observeOn(Scheduler, boolean, int) + * @see #delay(long, TimeUnit, Scheduler) + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.CUSTOM) + @NonNull + public final Observable<T> observeOn(@NonNull Scheduler scheduler) { + return observeOn(scheduler, false, bufferSize()); + } + + /** + * Returns an {@code Observable} to perform the current {@code Observable}'s emissions and notifications on a specified {@link Scheduler}, + * asynchronously with an unbounded buffer with {@link Flowable#bufferSize()} "island size" and optionally delays {@code onError} notifications. + * <p> + * <img width="640" height="308" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/observeOn.v3.png" alt=""> + * <p> + * This operator keeps emitting as many signals as it can on the given {@code Scheduler}'s worker thread, + * which may result in a longer than expected occupation of this thread. In other terms, + * it does not allow per-signal fairness in case the worker runs on a shared underlying thread. + * If such fairness and signal/work interleaving is preferred, use the delay operator with zero time instead. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>You specify which {@code Scheduler} this operator will use.</dd> + * </dl> + * <p>"Island size" indicates how large chunks the unbounded buffer allocates to store the excess elements waiting to be consumed + * on the other side of the asynchronous boundary. + * + * @param scheduler + * the {@code Scheduler} to notify {@link Observer}s on + * @param delayError + * indicates if the {@code onError} notification may not cut ahead of {@code onNext} notification on the other side of the + * scheduling boundary. If {@code true}, a sequence ending in {@code onError} will be replayed in the same order as was received + * from the current {@code Observable} + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code scheduler} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/observeon.html">ReactiveX operators documentation: ObserveOn</a> + * @see <a href="http://www.grahamlea.com/2014/07/rxjava-threading-examples/">RxJava Threading Examples</a> + * @see #subscribeOn + * @see #observeOn(Scheduler) + * @see #observeOn(Scheduler, boolean, int) + * @see #delay(long, TimeUnit, Scheduler, boolean) + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.CUSTOM) + @NonNull + public final Observable<T> observeOn(@NonNull Scheduler scheduler, boolean delayError) { + return observeOn(scheduler, delayError, bufferSize()); + } + + /** + * Returns an {@code Observable} to perform the current {@code Observable}'s emissions and notifications on a specified {@link Scheduler}, + * asynchronously with an unbounded buffer of configurable "island size" and optionally delays {@code onError} notifications. + * <p> + * <img width="640" height="308" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/observeOn.v3.png" alt=""> + * <p> + * This operator keeps emitting as many signals as it can on the given {@code Scheduler}'s worker thread, + * which may result in a longer than expected occupation of this thread. In other terms, + * it does not allow per-signal fairness in case the worker runs on a shared underlying thread. + * If such fairness and signal/work interleaving is preferred, use the delay operator with zero time instead. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>You specify which {@code Scheduler} this operator will use.</dd> + * </dl> + * <p>"Island size" indicates how large chunks the unbounded buffer allocates to store the excess elements waiting to be consumed + * on the other side of the asynchronous boundary. Values below 16 are not recommended in performance sensitive scenarios. + * + * @param scheduler + * the {@code Scheduler} to notify {@link Observer}s on + * @param delayError + * indicates if the {@code onError} notification may not cut ahead of {@code onNext} notification on the other side of the + * scheduling boundary. If {@code true} a sequence ending in {@code onError} will be replayed in the same order as was received + * from upstream + * @param bufferSize the size of the buffer. + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code scheduler} is {@code null} + * @throws IllegalArgumentException if {@code bufferSize} is non-positive + * @see <a href="http://reactivex.io/documentation/operators/observeon.html">ReactiveX operators documentation: ObserveOn</a> + * @see <a href="http://www.grahamlea.com/2014/07/rxjava-threading-examples/">RxJava Threading Examples</a> + * @see #subscribeOn + * @see #observeOn(Scheduler) + * @see #observeOn(Scheduler, boolean) + * @see #delay(long, TimeUnit, Scheduler, boolean) + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.CUSTOM) + @NonNull + public final Observable<T> observeOn(@NonNull Scheduler scheduler, boolean delayError, int bufferSize) { + Objects.requireNonNull(scheduler, "scheduler is null"); + ObjectHelper.verifyPositive(bufferSize, "bufferSize"); + return RxJavaPlugins.onAssembly(new ObservableObserveOn<>(this, scheduler, delayError, bufferSize)); + } + + /** + * Filters the items emitted by the current {@code Observable}, only emitting those of the specified type. + * <p> + * <img width="640" height="310" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/ofClass.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code ofType} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <U> the output type + * @param clazz + * the class type to filter the items emitted by the current {@code Observable} + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code clazz} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/filter.html">ReactiveX operators documentation: Filter</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull U> Observable<U> ofType(@NonNull Class<U> clazz) { + Objects.requireNonNull(clazz, "clazz is null"); + return filter(Functions.isInstanceOf(clazz)).cast(clazz); + } + + /** + * Returns an {@code Observable} instance that if the current {@code Observable} emits an error, it will emit an {@code onComplete} + * and swallow the throwable. + * <p> + * <img width="640" height="373" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Observable.onErrorComplete.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code onErrorComplete} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @return the new {@code Observable} instance + * @since 3.0.0 + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Observable<T> onErrorComplete() { + return onErrorComplete(Functions.alwaysTrue()); + } + + /** + * Returns an {@code Observable} instance that if the current {@code Observable} emits an error and the predicate returns + * {@code true}, it will emit an {@code onComplete} and swallow the throwable. + * <p> + * <img width="640" height="215" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Observable.onErrorComplete.f.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code onErrorComplete} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param predicate the predicate to call when an {@link Throwable} is emitted which should return {@code true} + * if the {@code Throwable} should be swallowed and replaced with an {@code onComplete}. + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code predicate} is {@code null} + * @since 3.0.0 + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public final Observable<T> onErrorComplete(@NonNull Predicate<? super Throwable> predicate) { + Objects.requireNonNull(predicate, "predicate is null"); + + return RxJavaPlugins.onAssembly(new ObservableOnErrorComplete<>(this, predicate)); + } + + /** + * Resumes the flow with an {@link ObservableSource} returned for the failure {@link Throwable} of the current {@code Observable} by a + * function instead of signaling the error via {@code onError}. + * <p> + * <img width="640" height="310" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/onErrorResumeNext.v3.png" alt=""> + * <p> + * By default, when an {@code ObservableSource} encounters an error that prevents it from emitting the expected item to + * its {@link Observer}, the {@code ObservableSource} invokes its {@code Observer}'s {@code onError} method, and then quits + * without invoking any more of its {@code Observer}'s methods. The {@code onErrorResumeNext} method changes this + * behavior. If you pass a function that returns an {@code ObservableSource} ({@code resumeFunction}) to + * {@code onErrorResumeNext}, if the original {@code ObservableSource} encounters an error, instead of invoking its + * {@code Observer}'s {@code onError} method, it will instead relinquish control to the {@code ObservableSource} returned from + * {@code resumeFunction}, which will invoke the {@code Observer}'s {@link Observer#onNext onNext} method if it is + * able to do so. In such a case, because no {@code ObservableSource} necessarily invokes {@code onError}, the {@code Observer} + * may never know that an error happened. + * <p> + * You can use this to prevent errors from propagating or to supply fallback data should errors be + * encountered. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code onErrorResumeNext} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param fallbackSupplier + * a function that returns an {@code ObservableSource} that will take over if the current {@code Observable} encounters + * an error + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code fallbackSupplier} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/catch.html">ReactiveX operators documentation: Catch</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Observable<T> onErrorResumeNext(@NonNull Function<? super Throwable, ? extends ObservableSource<? extends T>> fallbackSupplier) { + Objects.requireNonNull(fallbackSupplier, "fallbackSupplier is null"); + return RxJavaPlugins.onAssembly(new ObservableOnErrorNext<>(this, fallbackSupplier)); + } + + /** + * Resumes the flow with the given {@link ObservableSource} when the current {@code Observable} fails instead of + * signaling the error via {@code onError}. + * <p> + * <img width="640" height="310" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/onErrorResumeWith.v3.png" alt=""> + * <p> + * By default, when an {@code ObservableSource} encounters an error that prevents it from emitting the expected item to + * its {@link Observer}, the {@code ObservableSource} invokes its {@code Observer}'s {@code onError} method, and then quits + * without invoking any more of its {@code Observer}'s methods. The {@code onErrorResumeWith} method changes this + * behavior. If you pass another {@code ObservableSource} ({@code next}) to an {@code ObservableSource}'s + * {@code onErrorResumeWith} method, if the original {@code ObservableSource} encounters an error, instead of invoking its + * {@code Observer}'s {@code onError} method, it will instead relinquish control to {@code next} which + * will invoke the {@code Observer}'s {@link Observer#onNext onNext} method if it is able to do so. In such a case, + * because no {@code ObservableSource} necessarily invokes {@code onError}, the {@code Observer} may never know that an error + * happened. + * <p> + * You can use this to prevent errors from propagating or to supply fallback data should errors be + * encountered. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code onErrorResumeWith} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param fallback + * the next {@code ObservableSource} source that will take over if the current {@code Observable} encounters + * an error + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code fallback} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/catch.html">ReactiveX operators documentation: Catch</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Observable<T> onErrorResumeWith(@NonNull ObservableSource<? extends T> fallback) { + Objects.requireNonNull(fallback, "fallback is null"); + return onErrorResumeNext(Functions.justFunction(fallback)); + } + + /** + * Ends the flow with a last item returned by a function for the {@link Throwable} error signaled by the current + * {@code Observable} instead of signaling the error via {@code onError}. + * <p> + * <img width="640" height="310" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/onErrorReturn.o.v3.png" alt=""> + * <p> + * By default, when an {@link ObservableSource} encounters an error that prevents it from emitting the expected item to + * its {@link Observer}, the {@code ObservableSource} invokes its {@code Observer}'s {@code onError} method, and then quits + * without invoking any more of its {@code Observer}'s methods. The {@code onErrorReturn} method changes this + * behavior. If you pass a function ({@code resumeFunction}) to an {@code ObservableSource}'s {@code onErrorReturn} + * method, if the original {@code ObservableSource} encounters an error, instead of invoking its {@code Observer}'s + * {@code onError} method, it will instead emit the return value of {@code resumeFunction}. + * <p> + * You can use this to prevent errors from propagating or to supply fallback data should errors be + * encountered. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code onErrorReturn} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param itemSupplier + * a function that returns a single value that will be emitted along with a regular {@code onComplete} in case + * the current {@code Observable} signals an {@code onError} event + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code itemSupplier} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/catch.html">ReactiveX operators documentation: Catch</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Observable<T> onErrorReturn(@NonNull Function<? super Throwable, ? extends T> itemSupplier) { + Objects.requireNonNull(itemSupplier, "itemSupplier is null"); + return RxJavaPlugins.onAssembly(new ObservableOnErrorReturn<>(this, itemSupplier)); + } + + /** + * Ends the flow with the given last item when the current {@code Observable} fails instead of signaling the error via {@code onError}. + * <p> + * <img width="640" height="310" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/onErrorReturnItem.o.v3.png" alt=""> + * <p> + * By default, when an {@link ObservableSource} encounters an error that prevents it from emitting the expected item to + * its {@link Observer}, the {@code ObservableSource} invokes its {@code Observer}'s {@code onError} method, and then quits + * without invoking any more of its {@code Observer}'s methods. The {@code onErrorReturn} method changes this + * behavior. If you pass a function ({@code resumeFunction}) to an {@code ObservableSource}'s {@code onErrorReturn} + * method, if the original {@code ObservableSource} encounters an error, instead of invoking its {@code Observer}'s + * {@code onError} method, it will instead emit the return value of {@code resumeFunction}. + * <p> + * You can use this to prevent errors from propagating or to supply fallback data should errors be + * encountered. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code onErrorReturnItem} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param item + * the value that is emitted along with a regular {@code onComplete} in case the current + * {@code Observable} signals an exception + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code item} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/catch.html">ReactiveX operators documentation: Catch</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Observable<T> onErrorReturnItem(@NonNull T item) { + Objects.requireNonNull(item, "item is null"); + return onErrorReturn(Functions.justFunction(item)); + } + + /** + * Nulls out references to the upstream producer and downstream {@link Observer} if + * the sequence is terminated or downstream calls {@code dispose()}. + * <p> + * <img width="640" height="247" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/onTerminateDetach.o.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code onTerminateDetach} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @return the new {@code Observable} instance + * the sequence is terminated or downstream calls {@code dispose()} + * @since 2.0 + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Observable<T> onTerminateDetach() { + return RxJavaPlugins.onAssembly(new ObservableDetach<>(this)); + } + + /** + * Returns a {@link ConnectableObservable}, which is a variety of {@link ObservableSource} that waits until its + * {@link ConnectableObservable#connect connect} method is called before it begins emitting items to those + * {@link Observer}s that have subscribed to it. + * <p> + * <img width="640" height="510" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/publishConnect.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code publish} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @return the new {@code ConnectableObservable} instance + * @see <a href="http://reactivex.io/documentation/operators/publish.html">ReactiveX operators documentation: Publish</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final ConnectableObservable<T> publish() { + return RxJavaPlugins.onAssembly(new ObservablePublish<>(this)); + } + + /** + * Returns an {@code Observable} that emits the results of invoking a specified selector on items emitted by a + * {@link ConnectableObservable} that shares a single subscription to the current {@code Observable} sequence. + * <p> + * <img width="640" height="647" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/publishFunction.o.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code publish} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <R> + * the type of items emitted by the resulting {@code Observable} + * @param selector + * a function that can use the multicasted source sequence as many times as needed, without + * causing multiple subscriptions to the source sequence. {@link Observer}s to the given source will + * receive all notifications of the source from the time of the subscription forward. + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code selector} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/publish.html">ReactiveX operators documentation: Publish</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull R> Observable<R> publish(@NonNull Function<? super Observable<T>, ? extends ObservableSource<R>> selector) { + Objects.requireNonNull(selector, "selector is null"); + return RxJavaPlugins.onAssembly(new ObservablePublishSelector<>(this, selector)); + } + + /** + * Returns a {@link Maybe} that applies a specified accumulator function to the first item emitted by the current + * {@code Observable}, then feeds the result of that function along with the second item emitted by the current + * {@code Observable} into the same function, and so on until all items have been emitted by the current and finite {@code Observable}, + * and emits the final result from the final call to your function as its sole item. + * <p> + * <img width="640" height="320" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/reduce.2.v3.png" alt=""> + * <p> + * This technique, which is called "reduce" here, is sometimes called "aggregate," "fold," "accumulate," + * "compress," or "inject" in other programming contexts. Groovy, for instance, has an {@code inject} method + * that does a similar operation on lists. + * <p> + * Note that this operator requires the upstream to signal {@code onComplete} for the accumulator object to + * be emitted. Sources that are infinite and never complete will never emit anything through this + * operator and an infinite source may lead to a fatal {@link OutOfMemoryError}. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code reduce} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param reducer + * an accumulator function to be invoked on each item emitted by the current {@code Observable}, whose + * result will be used in the next accumulator call + * @return the new {@code Maybe} instance + * @throws NullPointerException if {@code reducer} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/reduce.html">ReactiveX operators documentation: Reduce</a> + * @see <a href="http://en.wikipedia.org/wiki/Fold_(higher-order_function)">Wikipedia: Fold (higher-order function)</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Maybe<T> reduce(@NonNull BiFunction<T, T, T> reducer) { + Objects.requireNonNull(reducer, "reducer is null"); + return RxJavaPlugins.onAssembly(new ObservableReduceMaybe<>(this, reducer)); + } + + /** + * Returns a {@link Single} that applies a specified accumulator function to the first item emitted by the current + * {@code Observable} and a specified seed value, then feeds the result of that function along with the second item + * emitted by the current {@code Observable} into the same function, and so on until all items have been emitted by the + * current and finite {@code Observable}, emitting the final result from the final call to your function as its sole item. + * <p> + * <img width="640" height="325" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/reduceSeed.o.v3.png" alt=""> + * <p> + * This technique, which is called "reduce" here, is sometimes called "aggregate," "fold," "accumulate," + * "compress," or "inject" in other programming contexts. Groovy, for instance, has an {@code inject} method + * that does a similar operation on lists. + * <p> + * Note that the {@code seed} is shared among all subscribers to the resulting {@code Observable} + * and may cause problems if it is mutable. To make sure each subscriber gets its own value, defer + * the application of this operator via {@link #defer(Supplier)}: + * <pre><code> + * ObservableSource<T> source = ... + * Single.defer(() -> source.reduce(new ArrayList<>(), (list, item) -> list.add(item))); + * + * // alternatively, by using compose to stay fluent + * + * source.compose(o -> + * Observable.defer(() -> o.reduce(new ArrayList<>(), (list, item) -> list.add(item)).toObservable()) + * ).firstOrError(); + * + * // or, by using reduceWith instead of reduce + * + * source.reduceWith(() -> new ArrayList<>(), (list, item) -> list.add(item))); + * </code></pre> + * <p> + * Note that this operator requires the upstream to signal {@code onComplete} for the accumulator object to + * be emitted. Sources that are infinite and never complete will never emit anything through this + * operator and an infinite source may lead to a fatal {@link OutOfMemoryError}. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code reduce} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <R> the accumulator and output value type + * @param seed + * the initial (seed) accumulator value + * @param reducer + * an accumulator function to be invoked on each item emitted by the current {@code Observable}, the + * result of which will be used in the next accumulator call + * @return the new {@code Single} instance + * @throws NullPointerException if {@code seed} or {@code reducer} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/reduce.html">ReactiveX operators documentation: Reduce</a> + * @see <a href="http://en.wikipedia.org/wiki/Fold_(higher-order_function)">Wikipedia: Fold (higher-order function)</a> + * @see #reduceWith(Supplier, BiFunction) + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull R> Single<R> reduce(R seed, @NonNull BiFunction<R, ? super T, R> reducer) { + Objects.requireNonNull(seed, "seed is null"); + Objects.requireNonNull(reducer, "reducer is null"); + return RxJavaPlugins.onAssembly(new ObservableReduceSeedSingle<>(this, seed, reducer)); + } + + /** + * Returns a {@link Single} that applies a specified accumulator function to the first item emitted by the current + * {@code Observable} and a seed value derived from calling a specified {@code seedSupplier}, then feeds the result + * of that function along with the second item emitted by the current {@code Observable} into the same function, + * and so on until all items have been emitted by the current and finite {@code Observable}, emitting the final result + * from the final call to your function as its sole item. + * <p> + * <img width="640" height="325" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/reduceWith.o.v3.png" alt=""> + * <p> + * This technique, which is called "reduce" here, is sometimes called "aggregate," "fold," "accumulate," + * "compress," or "inject" in other programming contexts. Groovy, for instance, has an {@code inject} method + * that does a similar operation on lists. + * <p> + * Note that this operator requires the upstream to signal {@code onComplete} for the accumulator object to + * be emitted. Sources that are infinite and never complete will never emit anything through this + * operator and an infinite source may lead to a fatal {@link OutOfMemoryError}. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code reduceWith} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <R> the accumulator and output value type + * @param seedSupplier + * the {@link Supplier} that provides the initial (seed) accumulator value for each individual {@link Observer} + * @param reducer + * an accumulator function to be invoked on each item emitted by the current {@code Observable}, the + * result of which will be used in the next accumulator call + * @return the new {@code Single} instance + * @throws NullPointerException if {@code seedSupplier} or {@code reducer} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/reduce.html">ReactiveX operators documentation: Reduce</a> + * @see <a href="http://en.wikipedia.org/wiki/Fold_(higher-order_function)">Wikipedia: Fold (higher-order function)</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull R> Single<R> reduceWith(@NonNull Supplier<R> seedSupplier, @NonNull BiFunction<R, ? super T, R> reducer) { + Objects.requireNonNull(seedSupplier, "seedSupplier is null"); + Objects.requireNonNull(reducer, "reducer is null"); + return RxJavaPlugins.onAssembly(new ObservableReduceWithSingle<>(this, seedSupplier, reducer)); + } + + /** + * Returns an {@code Observable} that repeats the sequence of items emitted by the current {@code Observable} indefinitely. + * <p> + * <img width="640" height="287" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/repeatInf.o.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code repeat} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @return the new {@code Observable} instance + * @see <a href="http://reactivex.io/documentation/operators/repeat.html">ReactiveX operators documentation: Repeat</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Observable<T> repeat() { + return repeat(Long.MAX_VALUE); + } + + /** + * Returns an {@code Observable} that repeats the sequence of items emitted by the current {@code Observable} at most + * {@code count} times. + * <p> + * <img width="640" height="336" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/repeatCount.o.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code repeat} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param times + * the number of times the current {@code Observable} items are repeated, a count of 0 will yield an empty + * sequence + * @return the new {@code Observable} instance + * @throws IllegalArgumentException + * if {@code times} is negative + * @see <a href="http://reactivex.io/documentation/operators/repeat.html">ReactiveX operators documentation: Repeat</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Observable<T> repeat(long times) { + if (times < 0) { + throw new IllegalArgumentException("times >= 0 required but it was " + times); + } + if (times == 0) { + return empty(); + } + return RxJavaPlugins.onAssembly(new ObservableRepeat<>(this, times)); + } + + /** + * Returns an {@code Observable} that repeats the sequence of items emitted by the current {@code Observable} until + * the provided stop function returns {@code true}. + * <p> + * <img width="640" height="263" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/repeatUntil.o.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code repeatUntil} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param stop + * a boolean supplier that is called when the current {@code Observable} completes; + * if it returns {@code true}, the returned {@code Observable} completes; if it returns {@code false}, + * the current {@code Observable} is resubscribed. + * @return the new {@code Observable} instance + * @throws NullPointerException + * if {@code stop} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/repeat.html">ReactiveX operators documentation: Repeat</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Observable<T> repeatUntil(@NonNull BooleanSupplier stop) { + Objects.requireNonNull(stop, "stop is null"); + return RxJavaPlugins.onAssembly(new ObservableRepeatUntil<>(this, stop)); + } + + /** + * Returns an {@code Observable} that emits the same values as the current {@code Observable} with the exception of an + * {@code onComplete}. An {@code onComplete} notification from the source will result in the emission of + * a {@code void} item to the {@link ObservableSource} provided as an argument to the {@code notificationHandler} + * function. If that {@code ObservableSource} calls {@code onComplete} or {@code onError} then {@code repeatWhen} will + * call {@code onComplete} or {@code onError} on the child subscription. Otherwise, the current {@code Observable} + * will be resubscribed. + * <p> + * <img width="640" height="430" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/repeatWhen.f.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code repeatWhen} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param handler + * receives an {@code ObservableSource} of notifications with which a user can complete or error, aborting the repeat. + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code handler} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/repeat.html">ReactiveX operators documentation: Repeat</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Observable<T> repeatWhen(@NonNull Function<? super Observable<Object>, ? extends ObservableSource<?>> handler) { + Objects.requireNonNull(handler, "handler is null"); + return RxJavaPlugins.onAssembly(new ObservableRepeatWhen<>(this, handler)); + } + + /** + * Returns a {@link ConnectableObservable} that shares a single subscription to the current {@code Observable} + * that will replay all of its items and notifications to any future {@link Observer}. A connectable + * {@code Observable} resembles an ordinary {@code Observable}, except that it does not begin emitting items when it is + * subscribed to, but only when its {@code connect} method is called. + * <p> + * <img width="640" height="445" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/replay.o.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>This version of {@code replay} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @return the new {@code ConnectableObservable} instance + * @see <a href="http://reactivex.io/documentation/operators/replay.html">ReactiveX operators documentation: Replay</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final ConnectableObservable<T> replay() { + return ObservableReplay.createFrom(this); + } + + /** + * Returns an {@code Observable} that emits items that are the results of invoking a specified selector on the items + * emitted by a {@link ConnectableObservable} that shares a single subscription to the current {@code Observable}. + * <p> + * <img width="640" height="449" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/replay.o.f.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>This version of {@code replay} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <R> + * the type of items emitted by the resulting {@code Observable} + * @param selector + * the selector function, which can use the multicasted sequence as many times as needed, without + * causing multiple subscriptions to the current {@code Observable} + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code selector} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/replay.html">ReactiveX operators documentation: Replay</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull R> Observable<R> replay(@NonNull Function<? super Observable<T>, ? extends ObservableSource<R>> selector) { + Objects.requireNonNull(selector, "selector is null"); + return ObservableReplay.multicastSelector(ObservableInternalHelper.replaySupplier(this), selector); + } + + /** + * Returns an {@code Observable} that emits items that are the results of invoking a specified selector on items + * emitted by a {@link ConnectableObservable} that shares a single subscription to the current {@code Observable}, + * replaying {@code bufferSize} notifications. + * <p> + * Note that due to concurrency requirements, {@code replay(bufferSize)} may hold strong references to more than + * {@code bufferSize} source emissions. + * <p> + * <img width="640" height="392" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/replay.o.fn.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>This version of {@code replay} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <R> + * the type of items emitted by the resulting {@code Observable} + * @param selector + * the selector function, which can use the multicasted sequence as many times as needed, without + * causing multiple subscriptions to the current {@code Observable} + * @param bufferSize + * the buffer size that limits the number of items the connectable {@code Observable} can replay + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code selector} is {@code null} + * @throws IllegalArgumentException if {@code bufferSize} is non-positive + * @see <a href="http://reactivex.io/documentation/operators/replay.html">ReactiveX operators documentation: Replay</a> + * @see #replay(Function, int, boolean) + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull R> Observable<R> replay(@NonNull Function<? super Observable<T>, ? extends ObservableSource<R>> selector, int bufferSize) { + Objects.requireNonNull(selector, "selector is null"); + ObjectHelper.verifyPositive(bufferSize, "bufferSize"); + return ObservableReplay.multicastSelector(ObservableInternalHelper.replaySupplier(this, bufferSize, false), selector); + } + + /** + * Returns an {@code Observable} that emits items that are the results of invoking a specified selector on items + * emitted by a {@link ConnectableObservable} that shares a single subscription to the current {@code Observable}, + * replaying {@code bufferSize} notifications. + * <p> + * Note that due to concurrency requirements, {@code replay(bufferSize)} may hold strong references to more than + * {@code bufferSize} source emissions. + * <p> + * <img width="640" height="392" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/replay.o.fn.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>This version of {@code replay} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <R> + * the type of items emitted by the resulting {@code Observable} + * @param selector + * the selector function, which can use the multicasted sequence as many times as needed, without + * causing multiple subscriptions to the current {@code Observable} + * @param bufferSize + * the buffer size that limits the number of items the connectable {@code Observable} can replay + * @param eagerTruncate + * if {@code true}, whenever the internal buffer is truncated to the given bufferSize, the + * oldest item will be guaranteed dereferenced, thus avoiding unexpected retention + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code selector} is {@code null} + * @throws IllegalArgumentException if {@code bufferSize} is non-positive + * @see <a href="http://reactivex.io/documentation/operators/replay.html">ReactiveX operators documentation: Replay</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull R> Observable<R> replay(@NonNull Function<? super Observable<T>, ? extends ObservableSource<R>> selector, int bufferSize, boolean eagerTruncate) { + Objects.requireNonNull(selector, "selector is null"); + ObjectHelper.verifyPositive(bufferSize, "bufferSize"); + return ObservableReplay.multicastSelector(ObservableInternalHelper.replaySupplier(this, bufferSize, eagerTruncate), selector); + } + + /** + * Returns an {@code Observable} that emits items that are the results of invoking a specified selector on items + * emitted by a {@link ConnectableObservable} that shares a single subscription to the current {@code Observable}, + * replaying no more than {@code bufferSize} items that were emitted within a specified time window. + * <p> + * Note that due to concurrency requirements, {@code replay(bufferSize)} may hold strong references to more than + * {@code bufferSize} source emissions. + * <p> + * <img width="640" height="350" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/replay.o.fnt.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>This version of {@code replay} operates by default on the {@code computation} {@link Scheduler}.</dd> + * </dl> + * + * @param <R> + * the type of items emitted by the resulting {@code Observable} + * @param selector + * a selector function, which can use the multicasted sequence as many times as needed, without + * causing multiple subscriptions to the current {@code Observable} + * @param bufferSize + * the buffer size that limits the number of items the connectable {@code Observable} can replay + * @param time + * the duration of the window in which the replayed items must have been emitted + * @param unit + * the time unit of {@code time} + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code selector} or {@code unit} is {@code null} + * @throws IllegalArgumentException if {@code bufferSize} is non-positive + * @see <a href="http://reactivex.io/documentation/operators/replay.html">ReactiveX operators documentation: Replay</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.COMPUTATION) + @NonNull + public final <@NonNull R> Observable<R> replay(@NonNull Function<? super Observable<T>, ? extends ObservableSource<R>> selector, int bufferSize, long time, @NonNull TimeUnit unit) { + return replay(selector, bufferSize, time, unit, Schedulers.computation()); + } + + /** + * Returns an {@code Observable} that emits items that are the results of invoking a specified selector on items + * emitted by a {@link ConnectableObservable} that shares a single subscription to the current {@code Observable}, + * replaying no more than {@code bufferSize} items that were emitted within a specified time window. + * <p> + * Note that due to concurrency requirements, {@code replay(bufferSize)} may hold strong references to more than + * {@code bufferSize} source emissions. + * <p> + * <img width="640" height="329" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/replay.o.fnts.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>You specify which {@link Scheduler} this operator will use.</dd> + * </dl> + * + * @param <R> + * the type of items emitted by the resulting {@code Observable} + * @param selector + * a selector function, which can use the multicasted sequence as many times as needed, without + * causing multiple subscriptions to the current {@code Observable} + * @param bufferSize + * the buffer size that limits the number of items the connectable {@code Observable} can replay + * @param time + * the duration of the window in which the replayed items must have been emitted + * @param unit + * the time unit of {@code time} + * @param scheduler + * the {@code Scheduler} that is the time source for the window + * @return the new {@code Observable} instance + * @throws IllegalArgumentException + * if {@code bufferSize} is non-positive + * @throws NullPointerException if {@code selector}, {@code unit} or {@code scheduler} is {@code null} + * @throws IllegalArgumentException if {@code bufferSize} is non-positive + * @see <a href="http://reactivex.io/documentation/operators/replay.html">ReactiveX operators documentation: Replay</a> + * @see #replay(Function, int, long, TimeUnit, Scheduler, boolean) + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.CUSTOM) + @NonNull + public final <@NonNull R> Observable<R> replay(@NonNull Function<? super Observable<T>, ? extends ObservableSource<R>> selector, int bufferSize, long time, @NonNull TimeUnit unit, @NonNull Scheduler scheduler) { + Objects.requireNonNull(selector, "selector is null"); + ObjectHelper.verifyPositive(bufferSize, "bufferSize"); + Objects.requireNonNull(unit, "unit is null"); + Objects.requireNonNull(scheduler, "scheduler is null"); + return ObservableReplay.multicastSelector( + ObservableInternalHelper.replaySupplier(this, bufferSize, time, unit, scheduler, false), selector); + } + + /** + * Returns an {@code Observable} that emits items that are the results of invoking a specified selector on items + * emitted by a {@link ConnectableObservable} that shares a single subscription to the current {@code Observable}, + * replaying no more than {@code bufferSize} items that were emitted within a specified time window. + * <p> + * Note that due to concurrency requirements, {@code replay(bufferSize)} may hold strong references to more than + * {@code bufferSize} source emissions. + * <p> + * <img width="640" height="329" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/replay.o.fnts.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>You specify which {@link Scheduler} this operator will use.</dd> + * </dl> + * + * @param <R> + * the type of items emitted by the resulting {@code Observable} + * @param selector + * a selector function, which can use the multicasted sequence as many times as needed, without + * causing multiple subscriptions to the current {@code Observable} + * @param bufferSize + * the buffer size that limits the number of items the connectable {@code Observable} can replay + * @param time + * the duration of the window in which the replayed items must have been emitted + * @param unit + * the time unit of {@code time} + * @param scheduler + * the {@code Scheduler} that is the time source for the window + * @param eagerTruncate + * if {@code true}, whenever the internal buffer is truncated to the given bufferSize/age, the + * oldest item will be guaranteed dereferenced, thus avoiding unexpected retention + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code selector}, {@code unit} or {@code scheduler} is {@code null} + * @throws IllegalArgumentException + * if {@code bufferSize} is non-positive + * @see <a href="http://reactivex.io/documentation/operators/replay.html">ReactiveX operators documentation: Replay</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.CUSTOM) + @NonNull + public final <@NonNull R> Observable<R> replay(@NonNull Function<? super Observable<T>, ? extends ObservableSource<R>> selector, int bufferSize, long time, @NonNull TimeUnit unit, @NonNull Scheduler scheduler, boolean eagerTruncate) { + Objects.requireNonNull(selector, "selector is null"); + ObjectHelper.verifyPositive(bufferSize, "bufferSize"); + Objects.requireNonNull(unit, "unit is null"); + Objects.requireNonNull(scheduler, "scheduler is null"); + return ObservableReplay.multicastSelector( + ObservableInternalHelper.replaySupplier(this, bufferSize, time, unit, scheduler, eagerTruncate), selector); + } + + /** + * Returns an {@code Observable} that emits items that are the results of invoking a specified selector on items + * emitted by a {@link ConnectableObservable} that shares a single subscription to the current {@code Observable}, + * replaying all items that were emitted within a specified time window. + * <p> + * <img width="640" height="394" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/replay.o.ft.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>This version of {@code replay} operates by default on the {@code computation} {@link Scheduler}.</dd> + * </dl> + * + * @param <R> + * the type of items emitted by the resulting {@code Observable} + * @param selector + * a selector function, which can use the multicasted sequence as many times as needed, without + * causing multiple subscriptions to the current {@code Observable} + * @param time + * the duration of the window in which the replayed items must have been emitted + * @param unit + * the time unit of {@code time} + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code selector} or {@code unit} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/replay.html">ReactiveX operators documentation: Replay</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.COMPUTATION) + @NonNull + public final <@NonNull R> Observable<R> replay(@NonNull Function<? super Observable<T>, ? extends ObservableSource<R>> selector, long time, @NonNull TimeUnit unit) { + return replay(selector, time, unit, Schedulers.computation()); + } + + /** + * Returns an {@code Observable} that emits items that are the results of invoking a specified selector on items + * emitted by a {@link ConnectableObservable} that shares a single subscription to the current {@code Observable}, + * replaying all items that were emitted within a specified time window. + * <p> + * <img width="640" height="367" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/replay.o.fts.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>You specify which {@link Scheduler} this operator will use.</dd> + * </dl> + * + * @param <R> + * the type of items emitted by the resulting {@code Observable} + * @param selector + * a selector function, which can use the multicasted sequence as many times as needed, without + * causing multiple subscriptions to the current {@code Observable} + * @param time + * the duration of the window in which the replayed items must have been emitted + * @param unit + * the time unit of {@code time} + * @param scheduler + * the scheduler that is the time source for the window + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code selector}, {@code unit} or {@code scheduler} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/replay.html">ReactiveX operators documentation: Replay</a> + * @see #replay(Function, long, TimeUnit, Scheduler, boolean) + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.CUSTOM) + @NonNull + public final <@NonNull R> Observable<R> replay(@NonNull Function<? super Observable<T>, ? extends ObservableSource<R>> selector, long time, @NonNull TimeUnit unit, @NonNull Scheduler scheduler) { + Objects.requireNonNull(selector, "selector is null"); + Objects.requireNonNull(unit, "unit is null"); + Objects.requireNonNull(scheduler, "scheduler is null"); + return ObservableReplay.multicastSelector(ObservableInternalHelper.replaySupplier(this, time, unit, scheduler, false), selector); + } + + /** + * Returns an {@code Observable} that emits items that are the results of invoking a specified selector on items + * emitted by a {@link ConnectableObservable} that shares a single subscription to the current {@code Observable}, + * replaying all items that were emitted within a specified time window. + * <p> + * <img width="640" height="367" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/replay.o.fts.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>You specify which {@link Scheduler} this operator will use.</dd> + * </dl> + * + * @param <R> + * the type of items emitted by the resulting {@code Observable} + * @param selector + * a selector function, which can use the multicasted sequence as many times as needed, without + * causing multiple subscriptions to the current {@code Observable} + * @param time + * the duration of the window in which the replayed items must have been emitted + * @param unit + * the time unit of {@code time} + * @param scheduler + * the scheduler that is the time source for the window + * @param eagerTruncate + * if {@code true}, whenever the internal buffer is truncated to the given age, the + * oldest item will be guaranteed dereferenced, thus avoiding unexpected retention + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code selector}, {@code unit} or {@code scheduler} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/replay.html">ReactiveX operators documentation: Replay</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.CUSTOM) + @NonNull + public final <@NonNull R> Observable<R> replay(@NonNull Function<? super Observable<T>, ? extends ObservableSource<R>> selector, long time, @NonNull TimeUnit unit, @NonNull Scheduler scheduler, boolean eagerTruncate) { + Objects.requireNonNull(selector, "selector is null"); + Objects.requireNonNull(unit, "unit is null"); + Objects.requireNonNull(scheduler, "scheduler is null"); + return ObservableReplay.multicastSelector(ObservableInternalHelper.replaySupplier(this, time, unit, scheduler, eagerTruncate), selector); + } + + /** + * Returns a {@link ConnectableObservable} that shares a single subscription to the current {@code Observable} that + * replays at most {@code bufferSize} items emitted by the current {@code Observable}. A connectable {@code Observable} resembles + * an ordinary {@code Observable}, except that it does not begin emitting items when it is subscribed to, but only + * when its {@code connect} method is called. + * <p> + * <img width="640" height="445" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/replay.o.n.v3.png" alt=""> + * <p> + * Note that due to concurrency requirements, {@code replay(bufferSize)} may hold strong references to more than + * {@code bufferSize} source emissions. + * To ensure no beyond-bufferSize items are referenced, + * use the {@link #replay(int, boolean)} overload with {@code eagerTruncate = true}. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>This version of {@code replay} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param bufferSize + * the buffer size that limits the number of items that can be replayed + * @return the new {@code ConnectableObservable} instance + * @throws IllegalArgumentException if {@code bufferSize} is non-positive + * @see <a href="http://reactivex.io/documentation/operators/replay.html">ReactiveX operators documentation: Replay</a> + * @see #replay(int, boolean) + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final ConnectableObservable<T> replay(int bufferSize) { + ObjectHelper.verifyPositive(bufferSize, "bufferSize"); + return ObservableReplay.create(this, bufferSize, false); + } + + /** + * Returns a {@link ConnectableObservable} that shares a single subscription to the current {@code Observable} that + * replays at most {@code bufferSize} items emitted by the current {@code Observable}. A connectable {@code Observable} resembles + * an ordinary {@code Observable}, except that it does not begin emitting items when it is subscribed to, but only + * when its {@code connect} method is called. + * <p> + * <img width="640" height="445" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/replay.o.n.v3.png" alt=""> + * <p> + * Note that due to concurrency requirements, {@code replay(bufferSize)} may hold strong references to more than + * {@code bufferSize} source emissions. + * To ensure no beyond-bufferSize items are referenced, set {@code eagerTruncate = true}. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>This version of {@code replay} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param bufferSize + * the buffer size that limits the number of items that can be replayed + * @param eagerTruncate + * if {@code true}, whenever the internal buffer is truncated to the given bufferSize/age, the + * oldest item will be guaranteed dereferenced, thus avoiding unexpected retention + * @return the new {@code ConnectableObservable} instance + * @throws IllegalArgumentException if {@code bufferSize} is non-positive + * @see <a href="http://reactivex.io/documentation/operators/replay.html">ReactiveX operators documentation: Replay</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final ConnectableObservable<T> replay(int bufferSize, boolean eagerTruncate) { + ObjectHelper.verifyPositive(bufferSize, "bufferSize"); + return ObservableReplay.create(this, bufferSize, eagerTruncate); + } + + /** + * Returns a {@link ConnectableObservable} that shares a single subscription to the current {@code Observable} and + * replays at most {@code bufferSize} items that were emitted during a specified time window. A connectable + * {@code Observable} resembles an ordinary {@code Observable}, except that it does not begin emitting items when it is + * subscribed to, but only when its {@code connect} method is called. + * <p> + * <img width="640" height="445" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/replay.o.nt.v3.png" alt=""> + * <p> + * Note that due to concurrency requirements, {@code replay(bufferSize)} may hold strong references to more than + * {@code bufferSize} source emissions. + * To ensure no out-of-date or beyond-bufferSize items are referenced, + * use the {@link #replay(int, long, TimeUnit, Scheduler, boolean)} overload with {@code eagerTruncate = true}. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>This version of {@code replay} operates by default on the {@code computation} {@link Scheduler}.</dd> + * </dl> + * + * @param bufferSize + * the buffer size that limits the number of items that can be replayed + * @param time + * the duration of the window in which the replayed items must have been emitted + * @param unit + * the time unit of {@code time} + * @return the new {@code ConnectableObservable} instance + * @throws NullPointerException if {@code unit} is {@code null} + * @throws IllegalArgumentException if {@code bufferSize} is non-positive + * @see <a href="http://reactivex.io/documentation/operators/replay.html">ReactiveX operators documentation: Replay</a> + * @see #replay(int, long, TimeUnit, Scheduler, boolean) + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.COMPUTATION) + @NonNull + public final ConnectableObservable<T> replay(int bufferSize, long time, @NonNull TimeUnit unit) { + return replay(bufferSize, time, unit, Schedulers.computation()); + } + + /** + * Returns a {@link ConnectableObservable} that shares a single subscription to the current {@code Observable} and + * that replays a maximum of {@code bufferSize} items that are emitted within a specified time window. A + * connectable {@code Observable} resembles an ordinary {@code Observable}, except that it does not begin emitting items + * when it is subscribed to, but only when its {@code connect} method is called. + * <p> + * Note that due to concurrency requirements, {@code replay(bufferSize)} may hold strong references to more than + * {@code bufferSize} source emissions. + * To ensure no out-of-date or beyond-bufferSize items are referenced, + * use the {@link #replay(int, long, TimeUnit, Scheduler, boolean)} overload with {@code eagerTruncate = true}. + * <p> + * <img width="640" height="445" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/replay.o.nts.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>You specify which {@link Scheduler} this operator will use.</dd> + * </dl> + * + * @param bufferSize + * the buffer size that limits the number of items that can be replayed + * @param time + * the duration of the window in which the replayed items must have been emitted + * @param unit + * the time unit of {@code time} + * @param scheduler + * the scheduler that is used as a time source for the window + * @return the new {@code ConnectableObservable} instance + * @throws NullPointerException if {@code unit} or {@code scheduler} is {@code null} + * @throws IllegalArgumentException + * if {@code bufferSize} is non-positive + * @see <a href="http://reactivex.io/documentation/operators/replay.html">ReactiveX operators documentation: Replay</a> + * @see #replay(int, long, TimeUnit, Scheduler, boolean) + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.CUSTOM) + @NonNull + public final ConnectableObservable<T> replay(int bufferSize, long time, @NonNull TimeUnit unit, @NonNull Scheduler scheduler) { + ObjectHelper.verifyPositive(bufferSize, "bufferSize"); + Objects.requireNonNull(unit, "unit is null"); + Objects.requireNonNull(scheduler, "scheduler is null"); + return ObservableReplay.create(this, time, unit, scheduler, bufferSize, false); + } + + /** + * Returns a {@link ConnectableObservable} that shares a single subscription to the current {@code Observable} and + * that replays a maximum of {@code bufferSize} items that are emitted within a specified time window. A + * connectable {@code Observable} resembles an ordinary {@code Observable}, except that it does not begin emitting items + * when it is subscribed to, but only when its {@code connect} method is called. + * <p> + * <img width="640" height="445" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/replay.o.nts.v3.png" alt=""> + * <p> + * Note that due to concurrency requirements, {@code replay(bufferSize)} may hold strong references to more than + * {@code bufferSize} source emissions. + * To ensure no out-of-date or beyond-bufferSize items + * are referenced, set {@code eagerTruncate = true}. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>You specify which {@link Scheduler} this operator will use.</dd> + * </dl> + * + * @param bufferSize + * the buffer size that limits the number of items that can be replayed + * @param time + * the duration of the window in which the replayed items must have been emitted + * @param unit + * the time unit of {@code time} + * @param scheduler + * the scheduler that is used as a time source for the window + * @return the new {@code ConnectableObservable} instance + * @param eagerTruncate + * if {@code true}, whenever the internal buffer is truncated to the given bufferSize/age, the + * oldest item will be guaranteed dereferenced, thus avoiding unexpected retention + * @throws NullPointerException if {@code unit} or {@code scheduler} is {@code null} + * @throws IllegalArgumentException + * if {@code bufferSize} is non-positive + * @see <a href="http://reactivex.io/documentation/operators/replay.html">ReactiveX operators documentation: Replay</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.CUSTOM) + @NonNull + public final ConnectableObservable<T> replay(int bufferSize, long time, @NonNull TimeUnit unit, @NonNull Scheduler scheduler, boolean eagerTruncate) { + ObjectHelper.verifyPositive(bufferSize, "bufferSize"); + Objects.requireNonNull(unit, "unit is null"); + Objects.requireNonNull(scheduler, "scheduler is null"); + return ObservableReplay.create(this, time, unit, scheduler, bufferSize, eagerTruncate); + } + + /** + * Returns a {@link ConnectableObservable} that shares a single subscription to the current {@code Observable} and + * replays all items emitted by the current {@code Observable} within a specified time window. A connectable {@code Observable} + * resembles an ordinary {@code Observable}, except that it does not begin emitting items when it is subscribed to, + * but only when its {@code connect} method is called. + * <p> + * <img width="640" height="445" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/replay.o.t.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>This version of {@code replay} operates by default on the {@code computation} {@link Scheduler}.</dd> + * </dl> + * + * @param time + * the duration of the window in which the replayed items must have been emitted + * @param unit + * the time unit of {@code time} + * @return the new {@code ConnectableObservable} instance + * @throws NullPointerException if {@code unit} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/replay.html">ReactiveX operators documentation: Replay</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.COMPUTATION) + @NonNull + public final ConnectableObservable<T> replay(long time, @NonNull TimeUnit unit) { + return replay(time, unit, Schedulers.computation()); + } + + /** + * Returns a {@link ConnectableObservable} that shares a single subscription to the current {@code Observable} and + * replays all items emitted by the current {@code Observable} within a specified time window. A connectable {@code Observable} + * resembles an ordinary {@code Observable}, except that it does not begin emitting items when it is subscribed to, + * but only when its {@code connect} method is called. + * <p> + * <img width="640" height="445" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/replay.o.ts.v3.png" alt=""> + * <p> + * Note that the internal buffer may retain strong references to the oldest item. To ensure no out-of-date items + * are referenced, use the {@link #replay(long, TimeUnit, Scheduler, boolean)} overload with {@code eagerTruncate = true}. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>You specify which {@link Scheduler} this operator will use.</dd> + * </dl> + * + * @param time + * the duration of the window in which the replayed items must have been emitted + * @param unit + * the time unit of {@code time} + * @param scheduler + * the {@code Scheduler} that is the time source for the window + * @return the new {@code ConnectableObservable} instance + * @throws NullPointerException if {@code unit} or {@code scheduler} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/replay.html">ReactiveX operators documentation: Replay</a> + * @see #replay(long, TimeUnit, Scheduler, boolean) + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.CUSTOM) + @NonNull + public final ConnectableObservable<T> replay(long time, @NonNull TimeUnit unit, @NonNull Scheduler scheduler) { + Objects.requireNonNull(unit, "unit is null"); + Objects.requireNonNull(scheduler, "scheduler is null"); + return ObservableReplay.create(this, time, unit, scheduler, false); + } + + /** + * Returns a {@link ConnectableObservable} that shares a single subscription to the current {@code Observable} and + * replays all items emitted by the current {@code Observable} within a specified time window. A connectable {@code Observable} + * resembles an ordinary {@code Observable}, except that it does not begin emitting items when it is subscribed to, + * but only when its {@code connect} method is called. + * <p> + * <img width="640" height="445" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/replay.o.ts.v3.png" alt=""> + * <p> + * Note that the internal buffer may retain strong references to the oldest item. To ensure no out-of-date items + * are referenced, set {@code eagerTruncate = true}. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>You specify which {@link Scheduler} this operator will use.</dd> + * </dl> + * + * @param time + * the duration of the window in which the replayed items must have been emitted + * @param unit + * the time unit of {@code time} + * @param scheduler + * the {@code Scheduler} that is the time source for the window + * @param eagerTruncate + * if {@code true}, whenever the internal buffer is truncated to the given bufferSize/age, the + * oldest item will be guaranteed dereferenced, thus avoiding unexpected retention + * @return the new {@code ConnectableObservable} instance + * @throws NullPointerException if {@code unit} or {@code scheduler} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/replay.html">ReactiveX operators documentation: Replay</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.CUSTOM) + @NonNull + public final ConnectableObservable<T> replay(long time, @NonNull TimeUnit unit, @NonNull Scheduler scheduler, boolean eagerTruncate) { + Objects.requireNonNull(unit, "unit is null"); + Objects.requireNonNull(scheduler, "scheduler is null"); + return ObservableReplay.create(this, time, unit, scheduler, eagerTruncate); + } + + /** + * Returns an {@code Observable} that mirrors the current {@code Observable}, resubscribing to it if it calls {@code onError} + * (infinite retry count). + * <p> + * <img width="640" height="315" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/retry.v3.png" alt=""> + * <p> + * If the current {@code Observable} calls {@link Observer#onError}, this method will resubscribe to the current + * {@code Observable} rather than propagating the {@code onError} call. + * <p> + * Any and all items emitted by the current {@code Observable} will be emitted by the resulting {@code Observable}, even + * those emitted during failed subscriptions. For example, if the current {@code Observable} fails at first but emits + * {@code [1, 2]} then succeeds the second time and emits {@code [1, 2, 3, 4, 5]} then the complete sequence + * of emissions and notifications would be {@code [1, 2, 1, 2, 3, 4, 5, onComplete]}. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code retry} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @return the new {@code Observable} instance + * @see <a href="http://reactivex.io/documentation/operators/retry.html">ReactiveX operators documentation: Retry</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Observable<T> retry() { + return retry(Long.MAX_VALUE, Functions.alwaysTrue()); + } + + /** + * Returns an {@code Observable} that mirrors the current {@code Observable}, resubscribing to it if it calls {@code onError} + * and the predicate returns {@code true} for that specific exception and retry count. + * <p> + * <img width="640" height="236" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/retry.o.ne.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code retry} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param predicate + * the predicate that determines if a resubscription may happen in case of a specific exception + * and retry count + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code predicate} is {@code null} + * @see #retry() + * @see <a href="http://reactivex.io/documentation/operators/retry.html">ReactiveX operators documentation: Retry</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Observable<T> retry(@NonNull BiPredicate<? super Integer, ? super Throwable> predicate) { + Objects.requireNonNull(predicate, "predicate is null"); + + return RxJavaPlugins.onAssembly(new ObservableRetryBiPredicate<>(this, predicate)); + } + + /** + * Returns an {@code Observable} that mirrors the current {@code Observable}, resubscribing to it if it calls {@code onError} + * up to a specified number of retries. + * <p> + * <img width="640" height="327" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/retry.o.n.v3.png" alt=""> + * <p> + * If the current {@code Observable} calls {@link Observer#onError}, this method will resubscribe to the current + * {@code Observable} for a maximum of {@code count} resubscriptions rather than propagating the + * {@code onError} call. + * <p> + * Any and all items emitted by the current {@code Observable} will be emitted by the resulting {@code Observable}, even + * those emitted during failed subscriptions. For example, if the current {@code Observable} fails at first but emits + * {@code [1, 2]} then succeeds the second time and emits {@code [1, 2, 3, 4, 5]} then the complete sequence + * of emissions and notifications would be {@code [1, 2, 1, 2, 3, 4, 5, onComplete]}. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code retry} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param times + * the number of times to resubscribe if the current {@code Observable} fails + * @return the new {@code Observable} instance + * @throws IllegalArgumentException if {@code times} is negative + * @see <a href="http://reactivex.io/documentation/operators/retry.html">ReactiveX operators documentation: Retry</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Observable<T> retry(long times) { + return retry(times, Functions.alwaysTrue()); + } + + /** + * Retries at most times or until the predicate returns {@code false}, whichever happens first. + * <p> + * <img width="640" height="270" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/retry.o.nfe.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code retry} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param times the number of times to resubscribe if the current {@code Observable} fails + * @param predicate the predicate called with the failure {@link Throwable} and should return {@code true} to trigger a retry. + * @throws NullPointerException if {@code predicate} is {@code null} + * @throws IllegalArgumentException if {@code times} is negative + * @return the new {@code Observable} instance + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Observable<T> retry(long times, @NonNull Predicate<? super Throwable> predicate) { + if (times < 0) { + throw new IllegalArgumentException("times >= 0 required but it was " + times); + } + Objects.requireNonNull(predicate, "predicate is null"); + + return RxJavaPlugins.onAssembly(new ObservableRetryPredicate<>(this, times, predicate)); + } + + /** + * Retries the current {@code Observable} if the predicate returns {@code true}. + * <p> + * <img width="640" height="249" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/retry.o.e.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code retry} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param predicate the predicate that receives the failure {@link Throwable} and should return {@code true} to trigger a retry. + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code predicate} is {@code null} + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Observable<T> retry(@NonNull Predicate<? super Throwable> predicate) { + return retry(Long.MAX_VALUE, predicate); + } + + /** + * Retries until the given stop function returns {@code true}. + * <p> + * <img width="640" height="262" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/retryUntil.o.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code retryUntil} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param stop the function that should return {@code true} to stop retrying + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code stop} is {@code null} + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Observable<T> retryUntil(@NonNull BooleanSupplier stop) { + Objects.requireNonNull(stop, "stop is null"); + return retry(Long.MAX_VALUE, Functions.predicateReverseFor(stop)); + } + + /** + * Returns an {@code Observable} that emits the same values as the current {@code Observable} with the exception of an + * {@code onError}. An {@code onError} notification from the source will result in the emission of a + * {@link Throwable} item to the {@code Observable} provided as an argument to the {@code notificationHandler} + * function. If that {@code Observable} calls {@code onComplete} or {@code onError} then {@code retry} will call + * {@code onComplete} or {@code onError} on the child subscription. Otherwise, the current {@code Observable} + * will be resubscribed. + * <p> + * <img width="640" height="430" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/retryWhen.f.v3.png" alt=""> + * <p> + * Example: + * + * This retries 3 times, each time incrementing the number of seconds it waits. + * + * <pre><code> + * Observable.create((ObservableEmitter<? super String> s) -> { + * System.out.println("subscribing"); + * s.onError(new RuntimeException("always fails")); + * }).retryWhen(attempts -> { + * return attempts.zipWith(Observable.range(1, 3), (n, i) -> i).flatMap(i -> { + * System.out.println("delay retry by " + i + " second(s)"); + * return Observable.timer(i, TimeUnit.SECONDS); + * }); + * }).blockingForEach(System.out::println); + * </code></pre> + * + * Output is: + * + * <pre> {@code + * subscribing + * delay retry by 1 second(s) + * subscribing + * delay retry by 2 second(s) + * subscribing + * delay retry by 3 second(s) + * subscribing + * } </pre> + * <p> + * Note that the inner {@link ObservableSource} returned by the handler function should signal + * either {@code onNext}, {@code onError} or {@code onComplete} in response to the received + * {@code Throwable} to indicate the operator should retry or terminate. If the upstream to + * the operator is asynchronous, signaling {@code onNext} followed by {@code onComplete} immediately may + * result in the sequence to be completed immediately. Similarly, if this inner + * {@code ObservableSource} signals {@code onError} or {@code onComplete} while the upstream is + * active, the sequence is terminated with the same signal immediately. + * <p> + * The following example demonstrates how to retry an asynchronous source with a delay: + * <pre><code> + * Observable.timer(1, TimeUnit.SECONDS) + * .doOnSubscribe(s -> System.out.println("subscribing")) + * .map(v -> { throw new RuntimeException(); }) + * .retryWhen(errors -> { + * AtomicInteger counter = new AtomicInteger(); + * return errors + * .takeWhile(e -> counter.getAndIncrement() != 3) + * .flatMap(e -> { + * System.out.println("delay retry by " + counter.get() + " second(s)"); + * return Observable.timer(counter.get(), TimeUnit.SECONDS); + * }); + * }) + * .blockingSubscribe(System.out::println, System.out::println); + * </code></pre> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code retryWhen} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param handler + * receives an {@code Observable} of notifications with which a user can complete or error, aborting the + * retry + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code handler} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/retry.html">ReactiveX operators documentation: Retry</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Observable<T> retryWhen( + @NonNull Function<? super Observable<Throwable>, ? extends ObservableSource<?>> handler) { + Objects.requireNonNull(handler, "handler is null"); + return RxJavaPlugins.onAssembly(new ObservableRetryWhen<>(this, handler)); + } + + /** + * Subscribes to the current {@code Observable} and wraps the given {@link Observer} into a {@link SafeObserver} + * (if not already a {@code SafeObserver}) that + * deals with exceptions thrown by a misbehaving {@code Observer} (that doesn't follow the + * <em>Reactive Streams</em> specification). + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code safeSubscribe} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param observer the incoming {@code Observer} instance + * @throws NullPointerException if {@code observer} is {@code null} + */ + @SchedulerSupport(SchedulerSupport.NONE) + public final void safeSubscribe(@NonNull Observer<? super T> observer) { + Objects.requireNonNull(observer, "observer is null"); + if (observer instanceof SafeObserver) { + subscribe(observer); + } else { + subscribe(new SafeObserver<>(observer)); + } + } + + /** + * Returns an {@code Observable} that emits the most recently emitted item (if any) emitted by the current {@code Observable} + * within periodic time intervals. + * <p> + * <img width="640" height="305" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/sample.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code sample} operates by default on the {@code computation} {@link Scheduler}.</dd> + * </dl> + * + * @param period + * the sampling rate + * @param unit + * the {@link TimeUnit} in which {@code period} is defined + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code unit} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/sample.html">ReactiveX operators documentation: Sample</a> + * @see #throttleLast(long, TimeUnit) + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.COMPUTATION) + @NonNull + public final Observable<T> sample(long period, @NonNull TimeUnit unit) { + return sample(period, unit, Schedulers.computation()); + } + + /** + * Returns an {@code Observable} that emits the most recently emitted item (if any) emitted by the current {@code Observable} + * within periodic time intervals and optionally emit the very last upstream item when the upstream completes. + * <p> + * <img width="640" height="277" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/sample.emitlast.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code sample} operates by default on the {@code computation} {@link Scheduler}.</dd> + * </dl> + * + * <p>History: 2.0.5 - experimental + * @param period + * the sampling rate + * @param unit + * the {@link TimeUnit} in which {@code period} is defined + * @param emitLast + * if {@code true} and the upstream completes while there is still an unsampled item available, + * that item is emitted to downstream before completion + * if {@code false}, an unsampled last item is ignored. + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code unit} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/sample.html">ReactiveX operators documentation: Sample</a> + * @see #throttleLast(long, TimeUnit) + * @since 2.1 + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.COMPUTATION) + @NonNull + public final Observable<T> sample(long period, @NonNull TimeUnit unit, boolean emitLast) { + return sample(period, unit, Schedulers.computation(), emitLast); + } + + /** + * Returns an {@code Observable} that emits the most recently emitted item (if any) emitted by the current {@code Observable} + * within periodic time intervals, where the intervals are defined on a particular {@link Scheduler}. + * <p> + * <img width="640" height="305" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/sample.s.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>You specify which {@code Scheduler} this operator will use.</dd> + * </dl> + * + * @param period + * the sampling rate + * @param unit + * the {@link TimeUnit} in which {@code period} is defined + * @param scheduler + * the {@code Scheduler} to use when sampling + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code unit} or {@code scheduler} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/sample.html">ReactiveX operators documentation: Sample</a> + * @see #throttleLast(long, TimeUnit, Scheduler) + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.CUSTOM) + @NonNull + public final Observable<T> sample(long period, @NonNull TimeUnit unit, @NonNull Scheduler scheduler) { + Objects.requireNonNull(unit, "unit is null"); + Objects.requireNonNull(scheduler, "scheduler is null"); + return RxJavaPlugins.onAssembly(new ObservableSampleTimed<>(this, period, unit, scheduler, false, null)); + } + + /** + * Returns an {@code Observable} that emits the most recently emitted item (if any) emitted by the current {@code Observable} + * within periodic time intervals, where the intervals are defined on a particular {@link Scheduler} + * and optionally emit the very last upstream item when the upstream completes. + * <p> + * <img width="640" height="277" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/sample.s.emitlast.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>You specify which {@code Scheduler} this operator will use.</dd> + * </dl> + * + * <p>History: 2.0.5 - experimental + * @param period + * the sampling rate + * @param unit + * the {@link TimeUnit} in which {@code period} is defined + * @param scheduler + * the {@code Scheduler} to use when sampling + * @param emitLast + * if {@code true} and the upstream completes while there is still an unsampled item available, + * that item is emitted to downstream before completion + * if {@code false}, an unsampled last item is ignored. + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code unit} or {@code scheduler} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/sample.html">ReactiveX operators documentation: Sample</a> + * @see #throttleLast(long, TimeUnit, Scheduler) + * @since 2.1 + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.CUSTOM) + @NonNull + public final Observable<T> sample(long period, @NonNull TimeUnit unit, @NonNull Scheduler scheduler, boolean emitLast) { + Objects.requireNonNull(unit, "unit is null"); + Objects.requireNonNull(scheduler, "scheduler is null"); + return RxJavaPlugins.onAssembly(new ObservableSampleTimed<>(this, period, unit, scheduler, emitLast, null)); + } + + /** + * Returns an {@code Observable} that emits the most recently emitted item (if any) emitted by the current {@code Observable} + * within periodic time intervals, where the intervals are defined on a particular {@link Scheduler}. + * <p> + * <img width="640" height="305" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/sample.s.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>You specify which {@code Scheduler} this operator will use.</dd> + * </dl> + * + * @param period + * the sampling rate + * @param unit + * the {@link TimeUnit} in which {@code period} is defined + * @param scheduler + * the {@code Scheduler} to use when sampling + * @param emitLast + * if {@code true} and the upstream completes while there is still an unsampled item available, + * that item is emitted to downstream before completion + * if {@code false}, an unsampled last item is ignored. + * @param onDropped + * called with the current entry when it has been replaced by a new one + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code unit} or {@code scheduler} is {@code null} or {@code onDropped} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/sample.html">ReactiveX operators documentation: Sample</a> + * @see #throttleLast(long, TimeUnit, Scheduler) + * @since 3.1.6 - Experimental + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.CUSTOM) + @NonNull + @Experimental + public final Observable<T> sample(long period, @NonNull TimeUnit unit, @NonNull Scheduler scheduler, boolean emitLast, @NonNull Consumer<? super T> onDropped) { + Objects.requireNonNull(unit, "unit is null"); + Objects.requireNonNull(scheduler, "scheduler is null"); + Objects.requireNonNull(onDropped, "onDropped is null"); + return RxJavaPlugins.onAssembly(new ObservableSampleTimed<>(this, period, unit, scheduler, emitLast, onDropped)); + } + + /** + * Returns an {@code Observable} that, when the specified {@code sampler} {@link ObservableSource} emits an item or completes, + * emits the most recently emitted item (if any) emitted by the current {@code Observable} since the previous + * emission from the {@code sampler} {@code ObservableSource}. + * <p> + * <img width="640" height="290" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/sample.o.nolast.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>This version of {@code sample} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <U> the element type of the sampler {@code ObservableSource} + * @param sampler + * the {@code ObservableSource} to use for sampling the current {@code Observable} + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code sampler} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/sample.html">ReactiveX operators documentation: Sample</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull U> Observable<T> sample(@NonNull ObservableSource<U> sampler) { + Objects.requireNonNull(sampler, "sampler is null"); + return RxJavaPlugins.onAssembly(new ObservableSampleWithObservable<>(this, sampler, false)); + } + + /** + * Returns an {@code Observable} that, when the specified {@code sampler} {@link ObservableSource} emits an item or completes, + * emits the most recently emitted item (if any) emitted by the current {@code Observable} since the previous + * emission from the {@code sampler} {@code ObservableSource} + * and optionally emit the very last upstream item when the upstream or other {@code ObservableSource} complete. + * <p> + * <img width="640" height="290" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/sample.o.emitlast.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>This version of {@code sample} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * <p>History: 2.0.5 - experimental + * @param <U> the element type of the sampler {@code ObservableSource} + * @param sampler + * the {@code ObservableSource} to use for sampling the current {@code Observable} + * @param emitLast + * if {@code true} and the upstream completes while there is still an unsampled item available, + * that item is emitted to downstream before completion + * if {@code false}, an unsampled last item is ignored. + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code sampler} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/sample.html">ReactiveX operators documentation: Sample</a> + * @since 2.1 + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull U> Observable<T> sample(@NonNull ObservableSource<U> sampler, boolean emitLast) { + Objects.requireNonNull(sampler, "sampler is null"); + return RxJavaPlugins.onAssembly(new ObservableSampleWithObservable<>(this, sampler, emitLast)); + } + + /** + * Returns an {@code Observable} that emits the first value emitted by the current {@code Observable}, then emits one value + * for each subsequent value emitted by the current {@code Observable}. Each emission after the first is the result of + * applying the specified accumulator function to the previous emission and the corresponding value from the current {@code Observable}. + * <p> + * <img width="640" height="320" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/scan.v3.png" alt=""> + * <p> + * This sort of function is sometimes called an accumulator. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code scan} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param accumulator + * an accumulator function to be invoked on each item emitted by the current {@code Observable}, whose + * result will be emitted to {@link Observer}s via {@link Observer#onNext onNext} and used in the + * next accumulator call + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code accumulator} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/scan.html">ReactiveX operators documentation: Scan</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Observable<T> scan(@NonNull BiFunction<T, T, T> accumulator) { + Objects.requireNonNull(accumulator, "accumulator is null"); + return RxJavaPlugins.onAssembly(new ObservableScan<>(this, accumulator)); + } + + /** + * Returns an {@code Observable} that emits the provided initial (seed) value, then emits one value for each value emitted + * by the current {@code Observable}. Each emission after the first is the result of applying the specified accumulator + * function to the previous emission and the corresponding value from the current {@code Observable}. + * <p> + * <img width="640" height="320" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/scanSeed.v3.png" alt=""> + * <p> + * This sort of function is sometimes called an accumulator. + * <p> + * Note that the {@code Observable} that results from this method will emit {@code initialValue} as its first + * emitted item. + * <p> + * Note that the {@code initialValue} is shared among all subscribers to the resulting {@code Observable} + * and may cause problems if it is mutable. To make sure each subscriber gets its own value, defer + * the application of this operator via {@link #defer(Supplier)}: + * <pre><code> + * ObservableSource<T> source = ... + * Observable.defer(() -> source.scan(new ArrayList<>(), (list, item) -> list.add(item))); + * + * // alternatively, by using compose to stay fluent + * + * source.compose(o -> + * Observable.defer(() -> o.scan(new ArrayList<>(), (list, item) -> list.add(item))) + * ); + * </code></pre> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code scan} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <R> the initial, accumulator and result type + * @param initialValue + * the initial (seed) accumulator item + * @param accumulator + * an accumulator function to be invoked on each item emitted by the current {@code Observable}, whose + * result will be emitted to {@link Observer}s via {@link Observer#onNext onNext} and used in the + * next accumulator call + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code initialValue} or {@code accumulator} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/scan.html">ReactiveX operators documentation: Scan</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull R> Observable<R> scan(@NonNull R initialValue, @NonNull BiFunction<R, ? super T, R> accumulator) { + Objects.requireNonNull(initialValue, "initialValue is null"); + return scanWith(Functions.justSupplier(initialValue), accumulator); + } + + /** + * Returns an {@code Observable} that emits the provided initial (seed) value, then emits one value for each value emitted + * by the current {@code Observable}. Each emission after the first is the result of applying the specified accumulator + * function to the previous emission and the corresponding value from the current {@code Observable}. + * <p> + * <img width="640" height="320" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/scanSeed.v3.png" alt=""> + * <p> + * This sort of function is sometimes called an accumulator. + * <p> + * Note that the {@code Observable} that results from this method will emit the value returned + * by the {@code seedSupplier} as its first item. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code scanWith} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <R> the initial, accumulator and result type + * @param seedSupplier + * a {@link Supplier} that returns the initial (seed) accumulator item for each individual {@link Observer} + * @param accumulator + * an accumulator function to be invoked on each item emitted by the current {@code Observable}, whose + * result will be emitted to {@code Observer}s via {@link Observer#onNext onNext} and used in the + * next accumulator call + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code seedSupplier} or {@code accumulator} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/scan.html">ReactiveX operators documentation: Scan</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull R> Observable<R> scanWith(@NonNull Supplier<R> seedSupplier, @NonNull BiFunction<R, ? super T, R> accumulator) { + Objects.requireNonNull(seedSupplier, "seedSupplier is null"); + Objects.requireNonNull(accumulator, "accumulator is null"); + return RxJavaPlugins.onAssembly(new ObservableScanSeed<>(this, seedSupplier, accumulator)); + } + + /** + * Forces the current {@code Observable}'s emissions and notifications to be serialized and for it to obey + * <a href="http://reactivex.io/documentation/contract.html">the {@code ObservableSource} contract</a> in other ways. + * <p> + * It is possible for an {@code Observable} to invoke its {@link Observer}s' methods asynchronously, perhaps from + * different threads. This could make such an {@code Observable} poorly-behaved, in that it might try to invoke + * {@code onComplete} or {@code onError} before one of its {@code onNext} invocations, or it might call + * {@code onNext} from two different threads concurrently. You can force such an {@code Observable} to be + * well-behaved and sequential by applying the {@code serialize} method to it. + * <p> + * <img width="640" height="400" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/synchronize.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code serialize} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @return the new {@code Observable} instance + * @see <a href="http://reactivex.io/documentation/operators/serialize.html">ReactiveX operators documentation: Serialize</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Observable<T> serialize() { + return RxJavaPlugins.onAssembly(new ObservableSerialized<>(this)); + } + + /** + * Returns a new {@code Observable} that multicasts (and shares a single subscription to) the current {@code Observable}. As long as + * there is at least one {@link Observer}, the current {@code Observable} will stay subscribed and keep emitting signals. + * When all observers have disposed, the operator will dispose the subscription to the current {@code Observable}. + * <p> + * This is an alias for {@link #publish()}.{@link ConnectableObservable#refCount() refCount()}. + * <p> + * <img width="640" height="510" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/publishRefCount.o.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code share} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @return the new {@code Observable} instance + * @see <a href="http://reactivex.io/documentation/operators/refcount.html">ReactiveX operators documentation: RefCount</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Observable<T> share() { + return publish().refCount(); + } + + /** + * Returns a {@link Maybe} that completes if the current {@code Observable} is empty or emits the single item + * emitted by the current {@code Observable}, or signals an {@link IllegalArgumentException} if the current + * {@code Observable} emits more than one item. + * <p> + * <img width="640" height="217" src="https://raw.githubusercontent.com/wiki/ReactiveX/RxJava/images/rx-operators/singleElement.o.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code singleElement} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @return the new {@code Maybe} instance + * @see <a href="http://reactivex.io/documentation/operators/first.html">ReactiveX operators documentation: First</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Maybe<T> singleElement() { + return RxJavaPlugins.onAssembly(new ObservableSingleMaybe<>(this)); + } + + /** + * Returns a {@link Single} that emits the single item emitted by the current {@code Observable}, if the current {@code Observable} + * emits only a single item, or a default item if the current {@code Observable} emits no items. If the current + * {@code Observable} emits more than one item, an {@link IllegalArgumentException} is signaled instead. + * <p> + * <img width="640" height="315" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/single.2.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code single} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param defaultItem + * a default value to emit if the current {@code Observable} emits no item + * @return the new {@code Single} instance + * @throws NullPointerException if {@code defaultItem} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/first.html">ReactiveX operators documentation: First</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Single<T> single(@NonNull T defaultItem) { + Objects.requireNonNull(defaultItem, "defaultItem is null"); + return RxJavaPlugins.onAssembly(new ObservableSingleSingle<>(this, defaultItem)); + } + + /** + * Returns a {@link Single} that emits the single item emitted by the current {@code Observable} if it + * emits only a single item, otherwise + * if the current {@code Observable} completes without emitting any items or emits more than one item a + * {@link NoSuchElementException} or {@link IllegalArgumentException} will be signaled respectively. + * <p> + * <img width="640" height="206" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/singleOrError.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code singleOrError} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @return the new {@code Single} instance + * @see <a href="http://reactivex.io/documentation/operators/first.html">ReactiveX operators documentation: First</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Single<T> singleOrError() { + return RxJavaPlugins.onAssembly(new ObservableSingleSingle<>(this, null)); + } + + /** + * Returns an {@code Observable} that skips the first {@code count} items emitted by the current {@code Observable} and emits + * the remainder. + * <p> + * <img width="640" height="305" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/skip.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>This version of {@code skip} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param count + * the number of items to skip + * @return the new {@code Observable} instance + * @throws IllegalArgumentException if {@code count} is negative + * @see <a href="http://reactivex.io/documentation/operators/skip.html">ReactiveX operators documentation: Skip</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Observable<T> skip(long count) { + if (count < 0) { + throw new IllegalArgumentException("count >= 0 expected but it was " + count); + } + if (count == 0) { + return RxJavaPlugins.onAssembly(this); + } + return RxJavaPlugins.onAssembly(new ObservableSkip<>(this, count)); + } + + /** + * Returns an {@code Observable} that skips values emitted by the current {@code Observable} before a specified time window + * elapses. + * <p> + * <img width="640" height="305" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/skip.t.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code skip} does not operate on any particular scheduler but uses the current time + * from the {@code computation} {@link Scheduler}.</dd> + * </dl> + * + * @param time + * the length of the time window to skip + * @param unit + * the time unit of {@code time} + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code unit} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/skip.html">ReactiveX operators documentation: Skip</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.COMPUTATION) + @NonNull + public final Observable<T> skip(long time, @NonNull TimeUnit unit) { + return skipUntil(timer(time, unit)); + } + + /** + * Returns an {@code Observable} that skips values emitted by the current {@code Observable} before a specified time window + * on a specified {@link Scheduler} elapses. + * <p> + * <img width="640" height="305" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/skip.ts.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>You specify which {@code Scheduler} this operator will use for the timed skipping</dd> + * </dl> + * + * @param time + * the length of the time window to skip + * @param unit + * the time unit of {@code time} + * @param scheduler + * the {@code Scheduler} on which the timed wait happens + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code unit} or {@code scheduler} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/skip.html">ReactiveX operators documentation: Skip</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.CUSTOM) + @NonNull + public final Observable<T> skip(long time, @NonNull TimeUnit unit, @NonNull Scheduler scheduler) { + return skipUntil(timer(time, unit, scheduler)); + } + + /** + * Returns an {@code Observable} that drops a specified number of items from the end of the sequence emitted by the + * current {@code Observable}. + * <p> + * <img width="640" height="305" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/skipLast.v3.png" alt=""> + * <p> + * This {@link Observer} accumulates a queue long enough to store the first {@code count} items. As more items are + * received, items are taken from the front of the queue and emitted by the returned {@code Observable}. This causes + * such items to be delayed. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>This version of {@code skipLast} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param count + * number of items to drop from the end of the source sequence + * @return the new {@code Observable} instance + * @throws IllegalArgumentException + * if {@code count} is negative + * @see <a href="http://reactivex.io/documentation/operators/skiplast.html">ReactiveX operators documentation: SkipLast</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Observable<T> skipLast(int count) { + if (count < 0) { + throw new IllegalArgumentException("count >= 0 required but it was " + count); + } + if (count == 0) { + return RxJavaPlugins.onAssembly(this); + } + return RxJavaPlugins.onAssembly(new ObservableSkipLast<>(this, count)); + } + + /** + * Returns an {@code Observable} that drops items emitted by the current {@code Observable} during a specified time window + * before the source completes. + * <p> + * <img width="640" height="305" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/skipLast.t.v3.png" alt=""> + * <p> + * Note: this action will cache the latest items arriving in the specified time window. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code skipLast} does not operate on any particular scheduler but uses the current time + * from the {@code trampoline} {@link Scheduler}.</dd> + * </dl> + * + * @param time + * the length of the time window + * @param unit + * the time unit of {@code time} + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code unit} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/skiplast.html">ReactiveX operators documentation: SkipLast</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.TRAMPOLINE) + @NonNull + public final Observable<T> skipLast(long time, @NonNull TimeUnit unit) { + return skipLast(time, unit, Schedulers.trampoline(), false, bufferSize()); + } + + /** + * Returns an {@code Observable} that drops items emitted by the current {@code Observable} during a specified time window + * before the source completes. + * <p> + * <img width="640" height="305" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/skipLast.t.v3.png" alt=""> + * <p> + * Note: this action will cache the latest items arriving in the specified time window. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code skipLast} does not operate on any particular scheduler but uses the current time + * from the {@code computation} {@link Scheduler}.</dd> + * </dl> + * + * @param time + * the length of the time window + * @param unit + * the time unit of {@code time} + * @param delayError + * if {@code true}, an exception signaled by the current {@code Observable} is delayed until the regular elements are consumed + * by the downstream; if {@code false}, an exception is immediately signaled and all regular elements dropped + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code unit} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/skiplast.html">ReactiveX operators documentation: SkipLast</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.TRAMPOLINE) + @NonNull + public final Observable<T> skipLast(long time, @NonNull TimeUnit unit, boolean delayError) { + return skipLast(time, unit, Schedulers.trampoline(), delayError, bufferSize()); + } + + /** + * Returns an {@code Observable} that drops items emitted by the current {@code Observable} during a specified time window + * (defined on a specified scheduler) before the source completes. + * <p> + * <img width="640" height="340" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/skipLast.ts.v3.png" alt=""> + * <p> + * Note: this action will cache the latest items arriving in the specified time window. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>You specify which {@link Scheduler} this operator will use for tracking the current time</dd> + * </dl> + * + * @param time + * the length of the time window + * @param unit + * the time unit of {@code time} + * @param scheduler + * the scheduler used as the time source + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code unit} or {@code scheduler} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/skiplast.html">ReactiveX operators documentation: SkipLast</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.CUSTOM) + @NonNull + public final Observable<T> skipLast(long time, @NonNull TimeUnit unit, @NonNull Scheduler scheduler) { + return skipLast(time, unit, scheduler, false, bufferSize()); + } + + /** + * Returns an {@code Observable} that drops items emitted by the current {@code Observable} during a specified time window + * (defined on a specified scheduler) before the source completes. + * <p> + * <img width="640" height="340" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/skipLast.ts.v3.png" alt=""> + * <p> + * Note: this action will cache the latest items arriving in the specified time window. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>You specify which {@link Scheduler} this operator will use to track the current time</dd> + * </dl> + * + * @param time + * the length of the time window + * @param unit + * the time unit of {@code time} + * @param scheduler + * the scheduler used as the time source + * @param delayError + * if {@code true}, an exception signaled by the current {@code Observable} is delayed until the regular elements are consumed + * by the downstream; if {@code false}, an exception is immediately signaled and all regular elements dropped + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code unit} or {@code scheduler} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/skiplast.html">ReactiveX operators documentation: SkipLast</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.CUSTOM) + @NonNull + public final Observable<T> skipLast(long time, @NonNull TimeUnit unit, @NonNull Scheduler scheduler, boolean delayError) { + return skipLast(time, unit, scheduler, delayError, bufferSize()); + } + + /** + * Returns an {@code Observable} that drops items emitted by the current {@code Observable} during a specified time window + * (defined on a specified scheduler) before the source completes. + * <p> + * <img width="640" height="340" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/skipLast.ts.v3.png" alt=""> + * <p> + * Note: this action will cache the latest items arriving in the specified time window. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>You specify which {@link Scheduler} this operator will use.</dd> + * </dl> + * + * @param time + * the length of the time window + * @param unit + * the time unit of {@code time} + * @param scheduler + * the scheduler used as the time source + * @param delayError + * if {@code true}, an exception signaled by the current {@code Observable} is delayed until the regular elements are consumed + * by the downstream; if {@code false}, an exception is immediately signaled and all regular elements dropped + * @param bufferSize + * the hint about how many elements to expect to be skipped + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code unit} or {@code scheduler} is {@code null} + * @throws IllegalArgumentException if {@code bufferSize} is non-positive + * @see <a href="http://reactivex.io/documentation/operators/skiplast.html">ReactiveX operators documentation: SkipLast</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.CUSTOM) + @NonNull + public final Observable<T> skipLast(long time, @NonNull TimeUnit unit, @NonNull Scheduler scheduler, boolean delayError, int bufferSize) { + Objects.requireNonNull(unit, "unit is null"); + Objects.requireNonNull(scheduler, "scheduler is null"); + ObjectHelper.verifyPositive(bufferSize, "bufferSize"); + // the internal buffer holds pairs of (timestamp, value) so double the default buffer size + int s = bufferSize << 1; + return RxJavaPlugins.onAssembly(new ObservableSkipLastTimed<>(this, time, unit, scheduler, s, delayError)); + } + + /** + * Returns an {@code Observable} that skips items emitted by the current {@code Observable} until a second {@link ObservableSource} emits + * an item. + * <p> + * <img width="640" height="375" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/skipUntil.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code skipUntil} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <U> the element type of the other {@code ObservableSource} + * @param other + * the second {@code ObservableSource} that has to emit an item before the current {@code Observable}'s elements begin + * to be mirrored by the resulting {@code Observable} + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code other} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/skipuntil.html">ReactiveX operators documentation: SkipUntil</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull U> Observable<T> skipUntil(@NonNull ObservableSource<U> other) { + Objects.requireNonNull(other, "other is null"); + return RxJavaPlugins.onAssembly(new ObservableSkipUntil<>(this, other)); + } + + /** + * Returns an {@code Observable} that skips all items emitted by the current {@code Observable} as long as a specified + * condition holds {@code true}, but emits all further source items as soon as the condition becomes {@code false}. + * <p> + * <img width="640" height="305" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/skipWhile.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code skipWhile} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param predicate + * a function to test each item emitted from the current {@code Observable} + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code predicate} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/skipwhile.html">ReactiveX operators documentation: SkipWhile</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Observable<T> skipWhile(@NonNull Predicate<? super T> predicate) { + Objects.requireNonNull(predicate, "predicate is null"); + return RxJavaPlugins.onAssembly(new ObservableSkipWhile<>(this, predicate)); + } + + /** + * Returns an {@code Observable} that emits the events emitted by the current {@code Observable}, in a + * sorted order. Each item emitted by the current {@code Observable} must implement {@link Comparable} with respect to all + * other items in the sequence. + * <p> + * <img width="640" height="315" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/sorted.png" alt=""> + * <p> + * If any item emitted by the current {@code Observable} does not implement {@code Comparable} with respect to + * all other items emitted by the current {@code Observable}, no items will be emitted and the + * sequence is terminated with a {@link ClassCastException}. + * + * <p>Note that calling {@code sorted} with long, non-terminating or infinite sources + * might cause {@link OutOfMemoryError} + * + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code sorted} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @return the new {@code Observable} instance + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Observable<T> sorted() { + return toList().toObservable().map(Functions.listSorter(Functions.naturalComparator())).flatMapIterable(Functions.identity()); + } + + /** + * Returns an {@code Observable} that emits the events emitted by the current {@code Observable}, in a + * sorted order based on a specified comparison function. + * + * <p>Note that calling {@code sorted} with long, non-terminating or infinite sources + * might cause {@link OutOfMemoryError} + * + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code sorted} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param comparator + * a function that compares two items emitted by the current {@code Observable} and returns an {@code int} + * that indicates their sort order + * @throws NullPointerException if {@code comparator} is {@code null} + * @return the new {@code Observable} instance + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Observable<T> sorted(@NonNull Comparator<? super T> comparator) { + Objects.requireNonNull(comparator, "comparator is null"); + return toList().toObservable().map(Functions.listSorter(comparator)).flatMapIterable(Functions.identity()); + } + + /** + * Returns an {@code Observable} that emits the items in a specified {@link Iterable} before it begins to emit items + * emitted by the current {@code Observable}. + * <p> + * <img width="640" height="315" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/startWith.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code startWithIterable} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param items + * an {@code Iterable} that contains the items you want the resulting {@code Observable} to emit first + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code items} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/startwith.html">ReactiveX operators documentation: StartWith</a> + * @since 3.0.0 + * @see #startWithItem(Object) + * @see #startWithArray(Object...) + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Observable<T> startWithIterable(@NonNull Iterable<? extends T> items) { + return concatArray(fromIterable(items), this); + } + + /** + * Returns an {@code Observable} which first runs the other {@link CompletableSource} + * then the current {@code Observable} if the other completed normally. + * <p> + * <img width="640" height="268" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Observable.startWith.c.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code startWith} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param other the other {@code CompletableSource} to run first + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code other} is {@code null} + * @since 3.0.0 + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public final Observable<T> startWith(@NonNull CompletableSource other) { + Objects.requireNonNull(other, "other is null"); + return Observable.concat(Completable.wrap(other).<T>toObservable(), this); + } + + /** + * Returns an {@code Observable} which first runs the other {@link SingleSource} + * then the current {@code Observable} if the other succeeded normally. + * <p> + * <img width="640" height="248" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Observable.startWith.s.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code startWith} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param other the other {@code SingleSource} to run first + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code other} is {@code null} + * @since 3.0.0 + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public final Observable<T> startWith(@NonNull SingleSource<T> other) { + Objects.requireNonNull(other, "other is null"); + return Observable.concat(Single.wrap(other).toObservable(), this); + } + + /** + * Returns an {@code Observable} which first runs the other {@link MaybeSource} + * then the current {@code Observable} if the other succeeded or completed normally. + * <p> + * <img width="640" height="168" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Observable.startWith.m.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code startWith} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param other the other {@code MaybeSource} to run first + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code other} is {@code null} + * @since 3.0.0 + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public final Observable<T> startWith(@NonNull MaybeSource<T> other) { + Objects.requireNonNull(other, "other is null"); + return Observable.concat(Maybe.wrap(other).toObservable(), this); + } + + /** + * Returns an {@code Observable} that emits the items in a specified {@link ObservableSource} before it begins to emit + * items emitted by the current {@code Observable}. + * <p> + * <img width="640" height="315" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/startWith.o.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code startWith} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param other + * an {@code ObservableSource} that contains the items you want the modified {@code ObservableSource} to emit first + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code other} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/startwith.html">ReactiveX operators documentation: StartWith</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Observable<T> startWith(@NonNull ObservableSource<? extends T> other) { + Objects.requireNonNull(other, "other is null"); + return concatArray(other, this); + } + + /** + * Returns an {@code Observable} that emits a specified item before it begins to emit items emitted by the current + * {@code Observable}. + * <p> + * <img width="640" height="315" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/startWith.item.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code startWithItem} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param item + * the item to emit first + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code item} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/startwith.html">ReactiveX operators documentation: StartWith</a> + * @see #startWithArray(Object...) + * @see #startWithIterable(Iterable) + * @since 3.0.0 + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Observable<T> startWithItem(@NonNull T item) { + return concatArray(just(item), this); + } + + /** + * Returns an {@code Observable} that emits the specified items before it begins to emit items emitted by the current + * {@code Observable}. + * <p> + * <img width="640" height="315" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/startWithArray.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code startWithArray} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param items + * the array of values to emit first + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code items} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/startwith.html">ReactiveX operators documentation: StartWith</a> + * @see #startWithItem(Object) + * @see #startWithIterable(Iterable) + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @SafeVarargs + @NonNull + public final Observable<T> startWithArray(@NonNull T... items) { + Observable<T> fromArray = fromArray(items); + if (fromArray == empty()) { + return RxJavaPlugins.onAssembly(this); + } + return concatArray(fromArray, this); + } + + /** + * Subscribes to the current {@code Observable} and ignores {@code onNext} and {@code onComplete} emissions. + * <p> + * If the {@code Observable} emits an error, it is wrapped into an + * {@link OnErrorNotImplementedException} + * and routed to the {@link RxJavaPlugins#onError(Throwable)} handler. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code subscribe} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @return the new {@link Disposable} instance that can be used to dispose the subscription at any time + * @see <a href="http://reactivex.io/documentation/operators/subscribe.html">ReactiveX operators documentation: Subscribe</a> + * @see #subscribe(Consumer, Consumer, Action, DisposableContainer) + */ + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Disposable subscribe() { + return subscribe(Functions.emptyConsumer(), Functions.ON_ERROR_MISSING, Functions.EMPTY_ACTION); + } + + /** + * Subscribes to the current {@code Observable} and provides a callback to handle the items it emits. + * <p> + * If the {@code Observable} emits an error, it is wrapped into an + * {@link OnErrorNotImplementedException} + * and routed to the {@link RxJavaPlugins#onError(Throwable)} handler. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code subscribe} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param onNext + * the {@code Consumer<T>} you have designed to accept emissions from the current {@code Observable} + * @return the new {@link Disposable} instance that can be used to dispose the subscription at any time + * @throws NullPointerException + * if {@code onNext} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/subscribe.html">ReactiveX operators documentation: Subscribe</a> + * @see #subscribe(Consumer, Consumer, Action, DisposableContainer) + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Disposable subscribe(@NonNull Consumer<? super T> onNext) { + return subscribe(onNext, Functions.ON_ERROR_MISSING, Functions.EMPTY_ACTION); + } + + /** + * Subscribes to the current {@code Observable} and provides callbacks to handle the items it emits and any error + * notification it signals. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code subscribe} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param onNext + * the {@code Consumer<T>} you have designed to accept emissions from the current {@code Observable} + * @param onError + * the {@code Consumer<Throwable>} you have designed to accept any error notification from the current + * {@code Observable} + * @return the new {@link Disposable} instance that can be used to dispose the subscription at any time + * @throws NullPointerException + * if {@code onNext} or {@code onError} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/subscribe.html">ReactiveX operators documentation: Subscribe</a> + * @see #subscribe(Consumer, Consumer, Action, DisposableContainer) + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Disposable subscribe(@NonNull Consumer<? super T> onNext, @NonNull Consumer<? super Throwable> onError) { + return subscribe(onNext, onError, Functions.EMPTY_ACTION); + } + + /** + * Subscribes to the current {@code Observable} and provides callbacks to handle the items it emits and any error or + * completion notification it signals. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code subscribe} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param onNext + * the {@code Consumer<T>} you have designed to accept emissions from the current {@code Observable} + * @param onError + * the {@code Consumer<Throwable>} you have designed to accept any error notification from the current + * {@code Observable} + * @param onComplete + * the {@link Action} you have designed to accept a completion notification from the current + * {@code Observable} + * @return the new {@link Disposable} instance that can be used to dispose the subscription at any time + * @throws NullPointerException + * if {@code onNext}, {@code onError} or {@code onComplete} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/subscribe.html">ReactiveX operators documentation: Subscribe</a> + * @see #subscribe(Consumer, Consumer, Action, DisposableContainer) + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Disposable subscribe(@NonNull Consumer<? super T> onNext, @NonNull Consumer<? super Throwable> onError, + @NonNull Action onComplete) { + Objects.requireNonNull(onNext, "onNext is null"); + Objects.requireNonNull(onError, "onError is null"); + Objects.requireNonNull(onComplete, "onComplete is null"); + + LambdaObserver<T> ls = new LambdaObserver<>(onNext, onError, onComplete, Functions.emptyConsumer()); + + subscribe(ls); + + return ls; + } + + /** + * Wraps the given onXXX callbacks into a {@link Disposable} {@link Observer}, + * adds it to the given {@code DisposableContainer} and ensures, that if the upstream + * terminates or this particular {@code Disposable} is disposed, the {@code Observer} is removed + * from the given container. + * <p> + * The {@code Observer} will be removed after the callback for the terminal event has been invoked. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code subscribe} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param onNext the callback for upstream items + * @param onError the callback for an upstream error if any + * @param onComplete the callback for the upstream completion if any + * @param container the {@code DisposableContainer} (such as {@link CompositeDisposable}) to add and remove the + * created {@code Disposable} {@code Observer} + * @return the {@code Disposable} that allows disposing the particular subscription. + * @throws NullPointerException + * if {@code onNext}, {@code onError}, + * {@code onComplete} or {@code container} is {@code null} + * @since 3.1.0 + */ + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Disposable subscribe( + @NonNull Consumer<? super T> onNext, + @NonNull Consumer<? super Throwable> onError, + @NonNull Action onComplete, + @NonNull DisposableContainer container) { + Objects.requireNonNull(onNext, "onNext is null"); + Objects.requireNonNull(onError, "onError is null"); + Objects.requireNonNull(onComplete, "onComplete is null"); + Objects.requireNonNull(container, "container is null"); + + DisposableAutoReleaseObserver<T> observer = new DisposableAutoReleaseObserver<>( + container, onNext, onError, onComplete); + container.add(observer); + subscribe(observer); + return observer; + } + + @SchedulerSupport(SchedulerSupport.NONE) + @Override + public final void subscribe(@NonNull Observer<? super T> observer) { + Objects.requireNonNull(observer, "observer is null"); + try { + observer = RxJavaPlugins.onSubscribe(this, observer); + + Objects.requireNonNull(observer, "The RxJavaPlugins.onSubscribe hook returned a null Observer. Please change the handler provided to RxJavaPlugins.setOnObservableSubscribe for invalid null returns. Further reading: https://github.com/ReactiveX/RxJava/wiki/Plugins"); + + subscribeActual(observer); + } catch (NullPointerException e) { // NOPMD + throw e; + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + // can't call onError because no way to know if a Disposable has been set or not + // can't call onSubscribe because the call might have set a Subscription already + RxJavaPlugins.onError(e); + + NullPointerException npe = new NullPointerException("Actually not, but can't throw other exceptions due to RS"); + npe.initCause(e); + throw npe; + } + } + + /** + * Operator implementations (both source and intermediate) should implement this method that + * performs the necessary business logic and handles the incoming {@link Observer}s. + * <p>There is no need to call any of the plugin hooks on the current {@code Observable} instance or + * the {@code Observer}; all hooks and basic safeguards have been + * applied by {@link #subscribe(Observer)} before this method gets called. + * @param observer the incoming {@code Observer}, never {@code null} + */ + protected abstract void subscribeActual(@NonNull Observer<? super T> observer); + + /** + * Subscribes a given {@link Observer} (subclass) to the current {@code Observable} and returns the given + * {@code Observer} instance as is. + * <p>Usage example: + * <pre><code> + * Observable<Integer> source = Observable.range(1, 10); + * CompositeDisposable composite = new CompositeDisposable(); + * + * DisposableObserver<Integer> ds = new DisposableObserver<>() { + * // ... + * }; + * + * composite.add(source.subscribeWith(ds)); + * </code></pre> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code subscribeWith} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <E> the type of the {@code Observer} to use and return + * @param observer the {@code Observer} (subclass) to use and return, not {@code null} + * @return the input {@code observer} + * @throws NullPointerException if {@code observer} is {@code null} + * @since 2.0 + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull E extends Observer<? super T>> E subscribeWith(E observer) { + subscribe(observer); + return observer; + } + + /** + * Asynchronously subscribes {@link Observer}s to the current {@code Observable} on the specified {@link Scheduler}. + * <p> + * <img width="640" height="305" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/subscribeOn.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>You specify which {@code Scheduler} this operator will use.</dd> + * </dl> + * + * @param scheduler + * the {@code Scheduler} to perform subscription actions on + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code scheduler} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/subscribeon.html">ReactiveX operators documentation: SubscribeOn</a> + * @see <a href="http://www.grahamlea.com/2014/07/rxjava-threading-examples/">RxJava Threading Examples</a> + * @see #observeOn + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.CUSTOM) + @NonNull + public final Observable<T> subscribeOn(@NonNull Scheduler scheduler) { + Objects.requireNonNull(scheduler, "scheduler is null"); + return RxJavaPlugins.onAssembly(new ObservableSubscribeOn<>(this, scheduler)); + } + + /** + * Returns an {@code Observable} that emits the items emitted by the current {@code Observable} or the items of an alternate + * {@link ObservableSource} if the current {@code Observable} is empty. + * <p> + * <img width="640" height="256" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/switchifempty.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code switchIfEmpty} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param other + * the alternate {@code ObservableSource} to subscribe to if the source does not emit any items + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code other} is {@code null} + * @since 1.1.0 + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Observable<T> switchIfEmpty(@NonNull ObservableSource<? extends T> other) { + Objects.requireNonNull(other, "other is null"); + return RxJavaPlugins.onAssembly(new ObservableSwitchIfEmpty<>(this, other)); + } + + /** + * Returns a new {@code Observable} by applying a function that you supply to each item emitted by the current + * {@code Observable} that returns an {@link ObservableSource}, and then emitting the items emitted by the most recently emitted + * of these {@code ObservableSource}s. + * <p> + * The resulting {@code Observable} completes if both the current {@code Observable} and the last inner {@code ObservableSource}, if any, complete. + * If the current {@code Observable} signals an {@code onError}, the inner {@code ObservableSource} is disposed and the error delivered in-sequence. + * <p> + * <img width="640" height="350" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/switchMap.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code switchMap} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <R> the element type of the inner {@code ObservableSource}s and the output + * @param mapper + * a function that, when applied to an item emitted by the current {@code Observable}, returns an + * {@code ObservableSource} + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code mapper} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/flatmap.html">ReactiveX operators documentation: FlatMap</a> + * @see #switchMapDelayError(Function) + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull R> Observable<R> switchMap(@NonNull Function<? super T, ? extends ObservableSource<? extends R>> mapper) { + return switchMap(mapper, bufferSize()); + } + + /** + * Returns a new {@code Observable} by applying a function that you supply to each item emitted by the current + * {@code Observable} that returns an {@link ObservableSource}, and then emitting the items emitted by the most recently emitted + * of these {@code ObservableSource}s. + * <p> + * The resulting {@code Observable} completes if both the current {@code Observable} and the last inner {@code ObservableSource}, if any, complete. + * If the current {@code Observable} signals an {@code onError}, the inner {@code ObservableSource} is disposed and the error delivered in-sequence. + * <p> + * <img width="640" height="350" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/switchMap.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code switchMap} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <R> the element type of the inner {@code ObservableSource}s and the output + * @param mapper + * a function that, when applied to an item emitted by the current {@code Observable}, returns an + * {@code ObservableSource} + * @param bufferSize + * the number of elements expected from the current active inner {@code ObservableSource} to be buffered + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code mapper} is {@code null} + * @throws IllegalArgumentException if {@code bufferSize} is non-positive + * @see <a href="http://reactivex.io/documentation/operators/flatmap.html">ReactiveX operators documentation: FlatMap</a> + * @see #switchMapDelayError(Function, int) + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull R> Observable<R> switchMap(@NonNull Function<? super T, ? extends ObservableSource<? extends R>> mapper, int bufferSize) { + Objects.requireNonNull(mapper, "mapper is null"); + ObjectHelper.verifyPositive(bufferSize, "bufferSize"); + if (this instanceof ScalarSupplier) { + @SuppressWarnings("unchecked") + T v = ((ScalarSupplier<T>)this).get(); + if (v == null) { + return empty(); + } + return ObservableScalarXMap.scalarXMap(v, mapper); + } + return RxJavaPlugins.onAssembly(new ObservableSwitchMap<>(this, mapper, bufferSize, false)); + } + + /** + * Maps the items of the current {@code Observable} into {@link CompletableSource}s, subscribes to the newer one while + * disposing the subscription to the previous {@code CompletableSource}, thus keeping at most one + * active {@code CompletableSource} running. + * <p> + * <img width="640" height="522" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/switchMapCompletable.f.png" alt=""> + * <p> + * Since a {@code CompletableSource} doesn't produce any items, the resulting reactive type of + * this operator is a {@link Completable} that can only indicate successful completion or + * a failure in any of the inner {@code CompletableSource}s or the failure of the current + * {@code Observable}. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code switchMapCompletable} does not operate by default on a particular {@link Scheduler}.</dd> + * <dt><b>Error handling:</b></dt> + * <dd>If either the current {@code Observable} or the active {@code CompletableSource} signals an {@code onError}, + * the resulting {@code Completable} is terminated immediately with that {@link Throwable}. + * Use the {@link #switchMapCompletableDelayError(Function)} to delay such inner failures until + * every inner {@code CompletableSource}s and the main {@code Observable} terminates in some fashion. + * If they fail concurrently, the operator may combine the {@code Throwable}s into a + * {@link CompositeException} + * and signal it to the downstream instead. If any inactivated (switched out) {@code CompletableSource} + * signals an {@code onError} late, the {@code Throwable}s will be signaled to the global error handler via + * {@link RxJavaPlugins#onError(Throwable)} method as {@link UndeliverableException} errors. + * </dd> + * </dl> + * <p>History: 2.1.11 - experimental + * @param mapper the function called with each upstream item and should return a + * {@code CompletableSource} to be subscribed to and awaited for + * (non blockingly) for its terminal event + * @return the new {@code Completable} instance + * @throws NullPointerException if {@code mapper} is {@code null} + * @see #switchMapCompletableDelayError(Function) + * @since 2.2 + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Completable switchMapCompletable(@NonNull Function<? super T, ? extends CompletableSource> mapper) { + Objects.requireNonNull(mapper, "mapper is null"); + return RxJavaPlugins.onAssembly(new ObservableSwitchMapCompletable<>(this, mapper, false)); + } + + /** + * Maps the upstream values into {@link CompletableSource}s, subscribes to the newer one while + * disposing the subscription to the previous {@code CompletableSource}, thus keeping at most one + * active {@code CompletableSource} running and delaying any main or inner errors until all + * of them terminate. + * <p> + * <img width="640" height="453" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/switchMapCompletableDelayError.f.png" alt=""> + * <p> + * Since a {@code CompletableSource} doesn't produce any items, the resulting reactive type of + * this operator is a {@link Completable} that can only indicate successful completion or + * a failure in any of the inner {@code CompletableSource}s or the failure of the current + * {@code Observable}. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code switchMapCompletableDelayError} does not operate by default on a particular {@link Scheduler}.</dd> + * <dt><b>Error handling:</b></dt> + * <dd>The errors of the current {@code Observable} and all the {@code CompletableSource}s, who had the chance + * to run to their completion, are delayed until + * all of them terminate in some fashion. At this point, if there was only one failure, the respective + * {@link Throwable} is emitted to the downstream. It there were more than one failures, the + * operator combines all {@code Throwable}s into a {@link CompositeException} + * and signals that to the downstream. + * If any inactivated (switched out) {@code CompletableSource} + * signals an {@code onError} late, the {@code Throwable}s will be signaled to the global error handler via + * {@link RxJavaPlugins#onError(Throwable)} method as {@link UndeliverableException} errors. + * </dd> + * </dl> + * <p>History: 2.1.11 - experimental + * @param mapper the function called with each upstream item and should return a + * {@code CompletableSource} to be subscribed to and awaited for + * (non blockingly) for its terminal event + * @return the new {@code Completable} instance + * @throws NullPointerException if {@code mapper} is {@code null} + * @see #switchMapCompletable(Function) + * @since 2.2 + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Completable switchMapCompletableDelayError(@NonNull Function<? super T, ? extends CompletableSource> mapper) { + Objects.requireNonNull(mapper, "mapper is null"); + return RxJavaPlugins.onAssembly(new ObservableSwitchMapCompletable<>(this, mapper, true)); + } + + /** + * Maps the items of the current {@code Observable} into {@link MaybeSource}s and switches (subscribes) to the newer ones + * while disposing the older ones (and ignoring their signals) and emits the latest success value of the current one if + * available while failing immediately if the current {@code Observable} or any of the + * active inner {@code MaybeSource}s fail. + * <p> + * <img width="640" height="531" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/switchMapMaybe.o.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code switchMapMaybe} does not operate by default on a particular {@link Scheduler}.</dd> + * <dt><b>Error handling:</b></dt> + * <dd>This operator terminates with an {@code onError} if the current {@code Observable} or any of + * the inner {@code MaybeSource}s fail while they are active. When this happens concurrently, their + * individual {@link Throwable} errors may get combined and emitted as a single + * {@link CompositeException}. Otherwise, a late + * (i.e., inactive or switched out) {@code onError} from the current {@code Observable} or from any of + * the inner {@code MaybeSource}s will be forwarded to the global error handler via + * {@link RxJavaPlugins#onError(Throwable)} as + * {@link UndeliverableException}</dd> + * </dl> + * <p>History: 2.1.11 - experimental + * @param <R> the output value type + * @param mapper the function called with the current upstream event and should + * return a {@code MaybeSource} to replace the current active inner source + * and get subscribed to. + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code mapper} is {@code null} + * @see #switchMapMaybeDelayError(Function) + * @since 2.2 + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull R> Observable<R> switchMapMaybe(@NonNull Function<? super T, ? extends MaybeSource<? extends R>> mapper) { + Objects.requireNonNull(mapper, "mapper is null"); + return RxJavaPlugins.onAssembly(new ObservableSwitchMapMaybe<>(this, mapper, false)); + } + + /** + * Maps the upstream items into {@link MaybeSource}s and switches (subscribes) to the newer ones + * while disposing the older ones (and ignoring their signals) and emits the latest success value of the current one if + * available, delaying errors from the current {@code Observable} or the inner {@code MaybeSource}s until all terminate. + * <p> + * <img width="640" height="469" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/switchMapMaybeDelayError.o.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code switchMapMaybeDelayError} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * <p>History: 2.1.11 - experimental + * @param <R> the output value type + * @param mapper the function called with the current upstream event and should + * return a {@code MaybeSource} to replace the current active inner source + * and get subscribed to. + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code mapper} is {@code null} + * @see #switchMapMaybe(Function) + * @since 2.2 + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull R> Observable<R> switchMapMaybeDelayError(@NonNull Function<? super T, ? extends MaybeSource<? extends R>> mapper) { + Objects.requireNonNull(mapper, "mapper is null"); + return RxJavaPlugins.onAssembly(new ObservableSwitchMapMaybe<>(this, mapper, true)); + } + + /** + * Returns a new {@code Observable} by applying a function that you supply to each item emitted by the current + * {@code Observable} that returns a {@link SingleSource}, and then emitting the item emitted by the most recently emitted + * of these {@code SingleSource}s. + * <p> + * The resulting {@code Observable} completes if both the current {@code Observable} and the last inner {@code SingleSource}, if any, complete. + * If the current {@code Observable} signals an {@code onError}, the inner {@code SingleSource} is disposed and the error delivered in-sequence. + * <p> + * <img width="640" height="532" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/switchMapSingle.o.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code switchMapSingle} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * <p>History: 2.0.8 - experimental + * @param <R> the element type of the inner {@code SingleSource}s and the output + * @param mapper + * a function that, when applied to an item emitted by the current {@code Observable}, returns a + * {@code SingleSource} + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code mapper} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/flatmap.html">ReactiveX operators documentation: FlatMap</a> + * @see #switchMapSingleDelayError(Function) + * @since 2.2 + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull R> Observable<R> switchMapSingle(@NonNull Function<? super T, ? extends SingleSource<? extends R>> mapper) { + Objects.requireNonNull(mapper, "mapper is null"); + return RxJavaPlugins.onAssembly(new ObservableSwitchMapSingle<>(this, mapper, false)); + } + + /** + * Returns a new {@code Observable} by applying a function that you supply to each item emitted by the current + * {@code Observable} that returns a {@link SingleSource}, and then emitting the item emitted by the most recently emitted + * of these {@code SingleSource}s and delays any error until all {@code SingleSource}s terminate. + * <p> + * The resulting {@code Observable} completes if both the current {@code Observable} and the last inner {@code SingleSource}, if any, complete. + * If the current {@code Observable} signals an {@code onError}, the termination of the last inner {@code SingleSource} will emit that error as is + * or wrapped into a {@link CompositeException} along with the other possible errors the former inner {@code SingleSource}s signaled. + * <p> + * <img width="640" height="467" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/switchMapSingleDelayError.o.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code switchMapSingleDelayError} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * <p>History: 2.0.8 - experimental + * @param <R> the element type of the inner {@code SingleSource}s and the output + * @param mapper + * a function that, when applied to an item emitted by the current {@code Observable}, returns a + * {@code SingleSource} + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code mapper} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/flatmap.html">ReactiveX operators documentation: FlatMap</a> + * @see #switchMapSingle(Function) + * @since 2.2 + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull R> Observable<R> switchMapSingleDelayError(@NonNull Function<? super T, ? extends SingleSource<? extends R>> mapper) { + Objects.requireNonNull(mapper, "mapper is null"); + return RxJavaPlugins.onAssembly(new ObservableSwitchMapSingle<>(this, mapper, true)); + } + + /** + * Returns a new {@code Observable} by applying a function that you supply to each item emitted by the current + * {@code Observable} that returns an {@link ObservableSource}, and then emitting the items emitted by the most recently emitted + * of these {@code ObservableSource}s and delays any error until all {@code ObservableSource}s terminate. + * <p> + * The resulting {@code Observable} completes if both the current {@code Observable} and the last inner {@code ObservableSource}, if any, complete. + * If the current {@code Observable} signals an {@code onError}, the termination of the last inner {@code ObservableSource} will emit that error as is + * or wrapped into a {@link CompositeException} along with the other possible errors the former inner {@code ObservableSource}s signaled. + * <p> + * <img width="640" height="350" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/switchMap.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code switchMapDelayError} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <R> the element type of the inner {@code ObservableSource}s and the output + * @param mapper + * a function that, when applied to an item emitted by the current {@code Observable}, returns an + * {@code ObservableSource} + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code mapper} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/flatmap.html">ReactiveX operators documentation: FlatMap</a> + * @see #switchMap(Function) + * @since 2.0 + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull R> Observable<R> switchMapDelayError(@NonNull Function<? super T, ? extends ObservableSource<? extends R>> mapper) { + return switchMapDelayError(mapper, bufferSize()); + } + + /** + * Returns a new {@code Observable} by applying a function that you supply to each item emitted by the current + * {@code Observable} that returns an {@link ObservableSource}, and then emitting the items emitted by the most recently emitted + * of these {@code ObservableSource}s and delays any error until all {@code ObservableSource}s terminate. + * <p> + * The resulting {@code Observable} completes if both the current {@code Observable} and the last inner {@code ObservableSource}, if any, complete. + * If the current {@code Observable} signals an {@code onError}, the termination of the last inner {@code ObservableSource} will emit that error as is + * or wrapped into a {@link CompositeException} along with the other possible errors the former inner {@code ObservableSource}s signaled. + * <p> + * <img width="640" height="350" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/switchMap.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code switchMapDelayError} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <R> the element type of the inner {@code ObservableSource}s and the output + * @param mapper + * a function that, when applied to an item emitted by the current {@code Observable}, returns an + * {@code ObservableSource} + * @param bufferSize + * the number of elements expected from the current active inner {@code ObservableSource} to be buffered + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code mapper} is {@code null} + * @throws IllegalArgumentException if {@code bufferSize} is non-positive + * @see <a href="http://reactivex.io/documentation/operators/flatmap.html">ReactiveX operators documentation: FlatMap</a> + * @see #switchMap(Function, int) + * @since 2.0 + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull R> Observable<R> switchMapDelayError(@NonNull Function<? super T, ? extends ObservableSource<? extends R>> mapper, int bufferSize) { + Objects.requireNonNull(mapper, "mapper is null"); + ObjectHelper.verifyPositive(bufferSize, "bufferSize"); + if (this instanceof ScalarSupplier) { + @SuppressWarnings("unchecked") + T v = ((ScalarSupplier<T>)this).get(); + if (v == null) { + return empty(); + } + return ObservableScalarXMap.scalarXMap(v, mapper); + } + return RxJavaPlugins.onAssembly(new ObservableSwitchMap<>(this, mapper, bufferSize, true)); + } + + /** + * Returns an {@code Observable} that emits only the first {@code count} items emitted by the current {@code Observable}. + * If the source emits fewer than {@code count} items then all of its items are emitted. + * <p> + * <img width="640" height="305" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/take.v3.png" alt=""> + * <p> + * This method returns an {@code Observable} that will invoke a subscribing {@link Observer}'s + * {@link Observer#onNext onNext} function a maximum of {@code count} times before invoking + * {@link Observer#onComplete onComplete}. + * <p> + * Taking {@code 0} items from the current {@code Observable} will still subscribe to it, allowing the + * subscription-time side-effects to happen there, but will be immediately disposed and the downstream completed + * without any item emission. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>This version of {@code take} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param count + * the maximum number of items to emit + * @return the new {@code Observable} instance + * @throws IllegalArgumentException if {@code count} is negative + * @see <a href="http://reactivex.io/documentation/operators/take.html">ReactiveX operators documentation: Take</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Observable<T> take(long count) { + if (count < 0) { + throw new IllegalArgumentException("count >= 0 required but it was " + count); + } + return RxJavaPlugins.onAssembly(new ObservableTake<>(this, count)); + } + + /** + * Returns an {@code Observable} that emits those items emitted by the current {@code Observable} before a specified time runs + * out. + * <p> + * If time runs out before the {@code Observable} completes normally, the {@code onComplete} event will be + * signaled on the default {@code computation} {@link Scheduler}. + * <p> + * <img width="640" height="305" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/take.t.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>This version of {@code take} operates by default on the {@code computation} {@code Scheduler}.</dd> + * </dl> + * + * @param time + * the length of the time window + * @param unit + * the time unit of {@code time} + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code unit} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/take.html">ReactiveX operators documentation: Take</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Observable<T> take(long time, @NonNull TimeUnit unit) { + return takeUntil(timer(time, unit)); + } + + /** + * Returns an {@code Observable} that emits those items emitted by the current {@code Observable} before a specified time (on a + * specified {@link Scheduler}) runs out. + * <p> + * If time runs out before the {@code Observable} completes normally, the {@code onComplete} event will be + * signaled on the provided {@code Scheduler}. + * <p> + * <img width="640" height="305" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/take.ts.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>You specify which {@code Scheduler} this operator will use.</dd> + * </dl> + * + * @param time + * the length of the time window + * @param unit + * the time unit of {@code time} + * @param scheduler + * the {@code Scheduler} used for time source + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code unit} or {@code scheduler} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/take.html">ReactiveX operators documentation: Take</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.CUSTOM) + @NonNull + public final Observable<T> take(long time, @NonNull TimeUnit unit, @NonNull Scheduler scheduler) { + return takeUntil(timer(time, unit, scheduler)); + } + + /** + * Returns an {@code Observable} that emits at most the last {@code count} items emitted by the current {@code Observable}. + * If the source emits fewer than {@code count} items then all of its items are emitted. + * <p> + * <img width="640" height="310" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/takeLast.n.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>This version of {@code takeLast} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param count + * the maximum number of items to emit from the end of the sequence of items emitted by the current + * {@code Observable} + * @return the new {@code Observable} instance + * @throws IllegalArgumentException + * if {@code count} is negative + * @see <a href="http://reactivex.io/documentation/operators/takelast.html">ReactiveX operators documentation: TakeLast</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Observable<T> takeLast(int count) { + if (count < 0) { + throw new IllegalArgumentException("count >= 0 required but it was " + count); + } + if (count == 0) { + return RxJavaPlugins.onAssembly(new ObservableIgnoreElements<>(this)); + } + if (count == 1) { + return RxJavaPlugins.onAssembly(new ObservableTakeLastOne<>(this)); + } + return RxJavaPlugins.onAssembly(new ObservableTakeLast<>(this, count)); + } + + /** + * Returns an {@code Observable} that emits at most a specified number of items from the current {@code Observable} that were + * emitted in a specified window of time before the current {@code Observable} completed. + * <p> + * <img width="640" height="310" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/takeLast.tn.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code takeLast} does not operate on any particular scheduler but uses the current time + * from the {@code trampoline} {@link Scheduler}.</dd> + * </dl> + * + * @param count + * the maximum number of items to emit + * @param time + * the length of the time window + * @param unit + * the time unit of {@code time} + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code unit} is {@code null} + * @throws IllegalArgumentException if {@code count} is negative + * @see <a href="http://reactivex.io/documentation/operators/takelast.html">ReactiveX operators documentation: TakeLast</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.TRAMPOLINE) + @NonNull + public final Observable<T> takeLast(long count, long time, @NonNull TimeUnit unit) { + return takeLast(count, time, unit, Schedulers.trampoline(), false, bufferSize()); + } + + /** + * Returns an {@code Observable} that emits at most a specified number of items from the current {@code Observable} that were + * emitted in a specified window of time before the current {@code Observable} completed, where the timing information is + * provided by a given {@link Scheduler}. + * <p> + * <img width="640" height="310" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/takeLast.tns.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>You specify which {@code Scheduler} this operator will use for tracking the current time</dd> + * </dl> + * + * @param count + * the maximum number of items to emit + * @param time + * the length of the time window + * @param unit + * the time unit of {@code time} + * @param scheduler + * the {@code Scheduler} that provides the timestamps for the observed items + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code unit} or {@code scheduler} is {@code null} + * @throws IllegalArgumentException + * if {@code count} is negative + * @see <a href="http://reactivex.io/documentation/operators/takelast.html">ReactiveX operators documentation: TakeLast</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.CUSTOM) + @NonNull + public final Observable<T> takeLast(long count, long time, @NonNull TimeUnit unit, @NonNull Scheduler scheduler) { + return takeLast(count, time, unit, scheduler, false, bufferSize()); + } + + /** + * Returns an {@code Observable} that emits at most a specified number of items from the current {@code Observable} that were + * emitted in a specified window of time before the current {@code Observable} completed, where the timing information is + * provided by a given {@link Scheduler}. + * <p> + * <img width="640" height="310" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/takeLast.tns.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>You specify which {@code Scheduler} this operator will use for tracking the current time</dd> + * </dl> + * + * @param count + * the maximum number of items to emit + * @param time + * the length of the time window + * @param unit + * the time unit of {@code time} + * @param scheduler + * the {@code Scheduler} that provides the timestamps for the observed items + * @param delayError + * if {@code true}, an exception signaled by the current {@code Observable} is delayed until the regular elements are consumed + * by the downstream; if {@code false}, an exception is immediately signaled and all regular elements dropped + * @param bufferSize + * the hint about how many elements to expect to be last + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code unit} or {@code scheduler} is {@code null} + * @throws IllegalArgumentException + * if {@code count} is negative or {@code bufferSize} is non-positive + * @see <a href="http://reactivex.io/documentation/operators/takelast.html">ReactiveX operators documentation: TakeLast</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.CUSTOM) + @NonNull + public final Observable<T> takeLast(long count, long time, @NonNull TimeUnit unit, @NonNull Scheduler scheduler, boolean delayError, int bufferSize) { + Objects.requireNonNull(unit, "unit is null"); + Objects.requireNonNull(scheduler, "scheduler is null"); + ObjectHelper.verifyPositive(bufferSize, "bufferSize"); + if (count < 0) { + throw new IllegalArgumentException("count >= 0 required but it was " + count); + } + return RxJavaPlugins.onAssembly(new ObservableTakeLastTimed<>(this, count, time, unit, scheduler, bufferSize, delayError)); + } + + /** + * Returns an {@code Observable} that emits the items from the current {@code Observable} that were emitted in a specified + * window of time before the current {@code Observable} completed. + * <p> + * <img width="640" height="310" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/takeLast.t.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code takeLast} does not operate on any particular scheduler but uses the current time + * from the {@code trampoline} {@link Scheduler}.</dd> + * </dl> + * + * @param time + * the length of the time window + * @param unit + * the time unit of {@code time} + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code unit} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/takelast.html">ReactiveX operators documentation: TakeLast</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.TRAMPOLINE) + @NonNull + public final Observable<T> takeLast(long time, @NonNull TimeUnit unit) { + return takeLast(time, unit, Schedulers.trampoline(), false, bufferSize()); + } + + /** + * Returns an {@code Observable} that emits the items from the current {@code Observable} that were emitted in a specified + * window of time before the current {@code Observable} completed. + * <p> + * <img width="640" height="310" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/takeLast.t.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code takeLast} does not operate on any particular scheduler but uses the current time + * from the {@code trampoline} {@link Scheduler}.</dd> + * </dl> + * + * @param time + * the length of the time window + * @param unit + * the time unit of {@code time} + * @param delayError + * if {@code true}, an exception signaled by the current {@code Observable} is delayed until the regular elements are consumed + * by the downstream; if {@code false}, an exception is immediately signaled and all regular elements dropped + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code unit} is {@code null} + * @throws IllegalArgumentException if {@code count} is non-positive + * @see <a href="http://reactivex.io/documentation/operators/takelast.html">ReactiveX operators documentation: TakeLast</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.TRAMPOLINE) + @NonNull + public final Observable<T> takeLast(long time, @NonNull TimeUnit unit, boolean delayError) { + return takeLast(time, unit, Schedulers.trampoline(), delayError, bufferSize()); + } + + /** + * Returns an {@code Observable} that emits the items from the current {@code Observable} that were emitted in a specified + * window of time before the current {@code Observable} completed, where the timing information is provided by a specified + * {@link Scheduler}. + * <p> + * <img width="640" height="310" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/takeLast.ts.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>You specify which {@code Scheduler} this operator will use.</dd> + * </dl> + * + * @param time + * the length of the time window + * @param unit + * the time unit of {@code time} + * @param scheduler + * the {@code Scheduler} that provides the timestamps for the observed items + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code unit} or {@code scheduler} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/takelast.html">ReactiveX operators documentation: TakeLast</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.CUSTOM) + @NonNull + public final Observable<T> takeLast(long time, @NonNull TimeUnit unit, @NonNull Scheduler scheduler) { + return takeLast(time, unit, scheduler, false, bufferSize()); + } + + /** + * Returns an {@code Observable} that emits the items from the current {@code Observable} that were emitted in a specified + * window of time before the current {@code Observable} completed, where the timing information is provided by a specified + * {@link Scheduler}. + * <p> + * <img width="640" height="310" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/takeLast.ts.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>You specify which {@code Scheduler} this operator will use.</dd> + * </dl> + * + * @param time + * the length of the time window + * @param unit + * the time unit of {@code time} + * @param scheduler + * the {@code Scheduler} that provides the timestamps for the observed items + * @param delayError + * if {@code true}, an exception signaled by the current {@code Observable} is delayed until the regular elements are consumed + * by the downstream; if {@code false}, an exception is immediately signaled and all regular elements dropped + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code unit} or {@code scheduler} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/takelast.html">ReactiveX operators documentation: TakeLast</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.CUSTOM) + @NonNull + public final Observable<T> takeLast(long time, @NonNull TimeUnit unit, @NonNull Scheduler scheduler, boolean delayError) { + return takeLast(time, unit, scheduler, delayError, bufferSize()); + } + + /** + * Returns an {@code Observable} that emits the items from the current {@code Observable} that were emitted in a specified + * window of time before the current {@code Observable} completed, where the timing information is provided by a specified + * {@link Scheduler}. + * <p> + * <img width="640" height="310" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/takeLast.ts.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>You specify which {@code Scheduler} this operator will use.</dd> + * </dl> + * + * @param time + * the length of the time window + * @param unit + * the time unit of {@code time} + * @param scheduler + * the {@code Scheduler} that provides the timestamps for the observed items + * @param delayError + * if {@code true}, an exception signaled by the current {@code Observable} is delayed until the regular elements are consumed + * by the downstream; if {@code false}, an exception is immediately signaled and all regular elements dropped + * @param bufferSize + * the hint about how many elements to expect to be last + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code unit} or {@code scheduler} is {@code null} + * @throws IllegalArgumentException if {@code bufferSize} is non-positive + * @see <a href="http://reactivex.io/documentation/operators/takelast.html">ReactiveX operators documentation: TakeLast</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.CUSTOM) + @NonNull + public final Observable<T> takeLast(long time, @NonNull TimeUnit unit, @NonNull Scheduler scheduler, boolean delayError, int bufferSize) { + return takeLast(Long.MAX_VALUE, time, unit, scheduler, delayError, bufferSize); + } + + /** + * Returns an {@code Observable} that emits the items emitted by the current {@code Observable} until a second {@link ObservableSource} + * emits an item or completes. + * <p> + * <img width="640" height="213" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Observable.takeUntil.o.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code takeUntil} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param other + * the {@code ObservableSource} whose first emitted item or completion will cause {@code takeUntil} to stop emitting items + * from the current {@code Observable} + * @param <U> + * the type of items emitted by {@code other} + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code other} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/takeuntil.html">ReactiveX operators documentation: TakeUntil</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull U> Observable<T> takeUntil(@NonNull ObservableSource<U> other) { + Objects.requireNonNull(other, "other is null"); + return RxJavaPlugins.onAssembly(new ObservableTakeUntil<>(this, other)); + } + + /** + * Returns an {@code Observable} that emits items emitted by the current {@code Observable}, checks the specified predicate + * for each item, and then completes when the condition is satisfied. + * <p> + * <img width="640" height="305" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/takeUntil.p.v3.png" alt=""> + * <p> + * The difference between this operator and {@link #takeWhile(Predicate)} is that here, the condition is + * evaluated <em>after</em> the item is emitted. + * + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code takeUntil} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param stopPredicate + * a function that evaluates an item emitted by the current {@code Observable} and returns a {@link Boolean} + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code stopPredicate} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/takeuntil.html">ReactiveX operators documentation: TakeUntil</a> + * @see Observable#takeWhile(Predicate) + * @since 1.1.0 + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Observable<T> takeUntil(@NonNull Predicate<? super T> stopPredicate) { + Objects.requireNonNull(stopPredicate, "stopPredicate is null"); + return RxJavaPlugins.onAssembly(new ObservableTakeUntilPredicate<>(this, stopPredicate)); + } + + /** + * Returns an {@code Observable} that emits items emitted by the current {@code Observable} so long as each item satisfied a + * specified condition, and then completes as soon as this condition is not satisfied. + * <p> + * <img width="640" height="305" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/takeWhile.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code takeWhile} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param predicate + * a function that evaluates an item emitted by the current {@code Observable} and returns a {@link Boolean} + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code predicate} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/takewhile.html">ReactiveX operators documentation: TakeWhile</a> + * @see Observable#takeUntil(Predicate) + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Observable<T> takeWhile(@NonNull Predicate<? super T> predicate) { + Objects.requireNonNull(predicate, "predicate is null"); + return RxJavaPlugins.onAssembly(new ObservableTakeWhile<>(this, predicate)); + } + + /** + * Returns an {@code Observable} that emits only the first item emitted by the current {@code Observable} during sequential + * time windows of a specified duration. + * <p> + * This differs from {@link #throttleLast} in that this only tracks passage of time whereas + * {@code throttleLast} ticks at scheduled intervals. + * <p> + * <img width="640" height="305" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/throttleFirst.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code throttleFirst} operates by default on the {@code computation} {@link Scheduler}.</dd> + * </dl> + * + * @param windowDuration + * time to wait before emitting another item after emitting the last item + * @param unit + * the unit of time of {@code windowDuration} + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code unit} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/sample.html">ReactiveX operators documentation: Sample</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.COMPUTATION) + @NonNull + public final Observable<T> throttleFirst(long windowDuration, @NonNull TimeUnit unit) { + return throttleFirst(windowDuration, unit, Schedulers.computation()); + } + + /** + * Returns an {@code Observable} that emits only the first item emitted by the current {@code Observable} during sequential + * time windows of a specified duration, where the windows are managed by a specified {@link Scheduler}. + * <p> + * This differs from {@link #throttleLast} in that this only tracks passage of time whereas + * {@code throttleLast} ticks at scheduled intervals. + * <p> + * <img width="640" height="305" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/throttleFirst.s.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>You specify which {@code Scheduler} this operator will use.</dd> + * </dl> + * + * @param skipDuration + * time to wait before emitting another item after emitting the last item + * @param unit + * the unit of time of {@code skipDuration} + * @param scheduler + * the {@code Scheduler} to use internally to manage the timers that handle timeout for each + * event + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code unit} or {@code scheduler} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/sample.html">ReactiveX operators documentation: Sample</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.CUSTOM) + @NonNull + public final Observable<T> throttleFirst(long skipDuration, @NonNull TimeUnit unit, @NonNull Scheduler scheduler) { + Objects.requireNonNull(unit, "unit is null"); + Objects.requireNonNull(scheduler, "scheduler is null"); + return RxJavaPlugins.onAssembly(new ObservableThrottleFirstTimed<>(this, skipDuration, unit, scheduler, null)); + } + + /** + * Returns an {@code Observable} that emits only the first item emitted by the current {@code Observable} during sequential + * time windows of a specified duration, where the windows are managed by a specified {@link Scheduler}. + * <p> + * This differs from {@link #throttleLast} in that this only tracks passage of time whereas + * {@code throttleLast} ticks at scheduled intervals. + * <p> + * <img width="640" height="305" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/throttleFirst.s.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>You specify which {@code Scheduler} this operator will use.</dd> + * </dl> + * + * @param skipDuration + * time to wait before emitting another item after emitting the last item + * @param unit + * the unit of time of {@code skipDuration} + * @param scheduler + * the {@code Scheduler} to use internally to manage the timers that handle timeout for each + * event + * @param onDropped + * called when an item doesn't get delivered to the downstream + * + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code unit} or {@code scheduler} or {@code onDropped} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/sample.html">ReactiveX operators documentation: Sample</a> + * @since 3.1.6 - Experimental + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.CUSTOM) + @NonNull + @Experimental + public final Observable<T> throttleFirst(long skipDuration, @NonNull TimeUnit unit, @NonNull Scheduler scheduler, @NonNull Consumer<? super T> onDropped) { + Objects.requireNonNull(unit, "unit is null"); + Objects.requireNonNull(scheduler, "scheduler is null"); + Objects.requireNonNull(onDropped, "onDropped is null"); + return RxJavaPlugins.onAssembly(new ObservableThrottleFirstTimed<>(this, skipDuration, unit, scheduler, onDropped)); + } + + /** + * Returns an {@code Observable} that emits only the last item emitted by the current {@code Observable} during sequential + * time windows of a specified duration. + * <p> + * This differs from {@link #throttleFirst} in that this ticks along at a scheduled interval whereas + * {@code throttleFirst} does not tick, it just tracks passage of time. + * <p> + * <img width="640" height="305" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/throttleLast.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code throttleLast} operates by default on the {@code computation} {@link Scheduler}.</dd> + * </dl> + * + * @param intervalDuration + * duration of windows within which the last item emitted by the current {@code Observable} will be + * emitted + * @param unit + * the unit of time of {@code intervalDuration} + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code unit} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/sample.html">ReactiveX operators documentation: Sample</a> + * @see #sample(long, TimeUnit) + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.COMPUTATION) + @NonNull + public final Observable<T> throttleLast(long intervalDuration, @NonNull TimeUnit unit) { + return sample(intervalDuration, unit); + } + + /** + * Returns an {@code Observable} that emits only the last item emitted by the current {@code Observable} during sequential + * time windows of a specified duration, where the duration is governed by a specified {@link Scheduler}. + * <p> + * This differs from {@link #throttleFirst} in that this ticks along at a scheduled interval whereas + * {@code throttleFirst} does not tick, it just tracks passage of time. + * <p> + * <img width="640" height="305" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/throttleLast.s.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>You specify which {@code Scheduler} this operator will use.</dd> + * </dl> + * + * @param intervalDuration + * duration of windows within which the last item emitted by the current {@code Observable} will be + * emitted + * @param unit + * the unit of time of {@code intervalDuration} + * @param scheduler + * the {@code Scheduler} to use internally to manage the timers that handle timeout for each + * event + * @param onDropped + * called with the current entry when it has been replaced by a new one + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code unit} or {@code scheduler} is {@code null} or {@code onDropped} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/sample.html">ReactiveX operators documentation: Sample</a> + * @see #sample(long, TimeUnit, Scheduler) + * @since 3.1.6 - Experimental + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.CUSTOM) + @NonNull + @Experimental + public final Observable<T> throttleLast(long intervalDuration, @NonNull TimeUnit unit, @NonNull Scheduler scheduler, @NonNull Consumer<? super T> onDropped) { + return sample(intervalDuration, unit, scheduler, false, onDropped); + } + + /** + * Returns an {@code Observable} that emits only the last item emitted by the current {@code Observable} during sequential + * time windows of a specified duration, where the duration is governed by a specified {@link Scheduler}. + * <p> + * This differs from {@link #throttleFirst} in that this ticks along at a scheduled interval whereas + * {@code throttleFirst} does not tick, it just tracks passage of time. + * <p> + * <img width="640" height="305" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/throttleLast.s.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>You specify which {@code Scheduler} this operator will use.</dd> + * </dl> + * + * @param intervalDuration + * duration of windows within which the last item emitted by the current {@code Observable} will be + * emitted + * @param unit + * the unit of time of {@code intervalDuration} + * @param scheduler + * the {@code Scheduler} to use internally to manage the timers that handle timeout for each + * event + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code unit} or {@code scheduler} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/sample.html">ReactiveX operators documentation: Sample</a> + * @see #sample(long, TimeUnit, Scheduler) + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.CUSTOM) + @NonNull + public final Observable<T> throttleLast(long intervalDuration, @NonNull TimeUnit unit, @NonNull Scheduler scheduler) { + return sample(intervalDuration, unit, scheduler); + } + + /** + * Throttles items from the current {@code Observable} by first emitting the next + * item from upstream, then periodically emitting the latest item (if any) when + * the specified timeout elapses between them. + * <p> + * <img width="640" height="326" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/throttleLatest.png" alt=""> + * <p> + * Unlike the option with {@link #throttleLatest(long, TimeUnit, boolean)}, the very last item being held back + * (if any) is not emitted when the upstream completes. + * <p> + * If no items were emitted from the upstream during this timeout phase, the next + * upstream item is emitted immediately and the timeout window starts from then. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code throttleLatest} operates by default on the {@code computation} {@link Scheduler}.</dd> + * </dl> + * <p>History: 2.1.14 - experimental + * @param timeout the time to wait after an item emission towards the downstream + * before trying to emit the latest item from upstream again + * @param unit the time unit + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code unit} is {@code null} + * @see #throttleLatest(long, TimeUnit, boolean) + * @see #throttleLatest(long, TimeUnit, Scheduler) + * @since 2.2 + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.COMPUTATION) + @NonNull + public final Observable<T> throttleLatest(long timeout, @NonNull TimeUnit unit) { + return throttleLatest(timeout, unit, Schedulers.computation(), false); + } + + /** + * Throttles items from the current {@code Observable} by first emitting the next + * item from upstream, then periodically emitting the latest item (if any) when + * the specified timeout elapses between them. + * <p> + * <img width="640" height="326" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/throttleLatest.e.png" alt=""> + * <p> + * If no items were emitted from the upstream during this timeout phase, the next + * upstream item is emitted immediately and the timeout window starts from then. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code throttleLatest} operates by default on the {@code computation} {@link Scheduler}.</dd> + * </dl> + * <p>History: 2.1.14 - experimental + * @param timeout the time to wait after an item emission towards the downstream + * before trying to emit the latest item from upstream again + * @param unit the time unit + * @param emitLast If {@code true}, the very last item from the upstream will be emitted + * immediately when the upstream completes, regardless if there is + * a timeout window active or not. If {@code false}, the very last + * upstream item is ignored and the flow terminates. + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code unit} is {@code null} + * @see #throttleLatest(long, TimeUnit, Scheduler, boolean) + * @since 2.2 + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.COMPUTATION) + @NonNull + public final Observable<T> throttleLatest(long timeout, @NonNull TimeUnit unit, boolean emitLast) { + return throttleLatest(timeout, unit, Schedulers.computation(), emitLast); + } + + /** + * Throttles items from the current {@code Observable} by first emitting the next + * item from upstream, then periodically emitting the latest item (if any) when + * the specified timeout elapses between them. + * <p> + * <img width="640" height="326" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/throttleLatest.s.png" alt=""> + * <p> + * Unlike the option with {@link #throttleLatest(long, TimeUnit, Scheduler, boolean)}, the very last item being held back + * (if any) is not emitted when the upstream completes. + * <p> + * If no items were emitted from the upstream during this timeout phase, the next + * upstream item is emitted immediately and the timeout window starts from then. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>You specify which {@link Scheduler} this operator will use.</dd> + * </dl> + * <p>History: 2.1.14 - experimental + * @param timeout the time to wait after an item emission towards the downstream + * before trying to emit the latest item from upstream again + * @param unit the time unit + * @param scheduler the {@code Scheduler} where the timed wait and latest item + * emission will be performed + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code unit} or {@code scheduler} is {@code null} + * @see #throttleLatest(long, TimeUnit, Scheduler, boolean) + * @since 2.2 + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.CUSTOM) + @NonNull + public final Observable<T> throttleLatest(long timeout, @NonNull TimeUnit unit, @NonNull Scheduler scheduler) { + return throttleLatest(timeout, unit, scheduler, false); + } + + /** + * Throttles items from the current {@code Observable} by first emitting the next + * item from upstream, then periodically emitting the latest item (if any) when + * the specified timeout elapses between them. + * <p> + * <img width="640" height="326" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/throttleLatest.se.png" alt=""> + * <p> + * If no items were emitted from the upstream during this timeout phase, the next + * upstream item is emitted immediately and the timeout window starts from then. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>You specify which {@link Scheduler} this operator will use.</dd> + * </dl> + * <p>History: 2.1.14 - experimental + * @param timeout the time to wait after an item emission towards the downstream + * before trying to emit the latest item from upstream again + * @param unit the time unit + * @param scheduler the {@code Scheduler} where the timed wait and latest item + * emission will be performed + * @param emitLast If {@code true}, the very last item from the upstream will be emitted + * immediately when the upstream completes, regardless if there is + * a timeout window active or not. If {@code false}, the very last + * upstream item is ignored and the flow terminates. + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code unit} or {@code scheduler} is {@code null} + * @since 2.2 + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.CUSTOM) + @NonNull + public final Observable<T> throttleLatest(long timeout, @NonNull TimeUnit unit, @NonNull Scheduler scheduler, boolean emitLast) { + Objects.requireNonNull(unit, "unit is null"); + Objects.requireNonNull(scheduler, "scheduler is null"); + return RxJavaPlugins.onAssembly(new ObservableThrottleLatest<>(this, timeout, unit, scheduler, emitLast, null)); + } + + /** + * Throttles items from the current {@code Observable} by first emitting the next + * item from upstream, then periodically emitting the latest item (if any) when + * the specified timeout elapses between them, invoking the consumer for any dropped item. + * <p> + * <img width="640" height="326" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/throttleLatest.se.png" alt=""> + * <p> + * If no items were emitted from the upstream during this timeout phase, the next + * upstream item is emitted immediately and the timeout window starts from then. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>You specify which {@link Scheduler} this operator will use.</dd> + * <dt><b>Error handling:</b></dt> + * <dd> + * If the upstream signals an {@code onError} or {@code onDropped} callback crashes, + * the error is delivered immediately to the downstream. If both happen, a {@link CompositeException} + * is created, containing both the upstream and the callback error. + * If the {@code onDropped} callback crashes when the sequence gets disposed, the exception is forwarded + * to the global error handler via {@link RxJavaPlugins#onError(Throwable)}. + * </dd> + * </dl> + * @param timeout the time to wait after an item emission towards the downstream + * before trying to emit the latest item from upstream again + * @param unit the time unit + * @param scheduler the {@code Scheduler} where the timed wait and latest item + * emission will be performed + * @param emitLast If {@code true}, the very last item from the upstream will be emitted + * immediately when the upstream completes, regardless if there is + * a timeout window active or not. If {@code false}, the very last + * upstream item is ignored and the flow terminates. + * @param onDropped called when an item is replaced by a newer item that doesn't get delivered + * to the downstream, including the very last item if {@code emitLast} is {@code false} + * and the current undelivered item when the sequence gets disposed. + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code unit}, {@code scheduler} or {@code onDropped} is {@code null} + * @since 3.1.6 - Experimental + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.CUSTOM) + @NonNull + @Experimental + public final Observable<T> throttleLatest(long timeout, @NonNull TimeUnit unit, @NonNull Scheduler scheduler, boolean emitLast, @NonNull Consumer<? super T> onDropped) { + Objects.requireNonNull(unit, "unit is null"); + Objects.requireNonNull(scheduler, "scheduler is null"); + Objects.requireNonNull(onDropped, "onDropped is null"); + return RxJavaPlugins.onAssembly(new ObservableThrottleLatest<>(this, timeout, unit, scheduler, emitLast, onDropped)); + } + + /** + * Returns an {@code Observable} that mirrors the current {@code Observable}, except that it drops items emitted by the + * current {@code Observable} that are followed by newer items before a timeout value expires. The timer resets on + * each emission (alias to {@link #debounce(long, TimeUnit, Scheduler)}). + * <p> + * <em>Note:</em> If items keep being emitted by the current {@code Observable} faster than the timeout then no items + * will be emitted by the resulting {@code Observable}. + * <p> + * <img width="640" height="305" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/throttleWithTimeout.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code throttleWithTimeout} operates by default on the {@code computation} {@link Scheduler}.</dd> + * </dl> + * + * @param timeout + * the length of the window of time that must pass after the emission of an item from the current + * {@code Observable}, in which the current {@code Observable} emits no items, in order for the item to be emitted by the + * resulting {@code Observable} + * @param unit + * the unit of time for the specified {@code timeout} + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code unit} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/debounce.html">ReactiveX operators documentation: Debounce</a> + * @see #debounce(long, TimeUnit) + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.COMPUTATION) + @NonNull + public final Observable<T> throttleWithTimeout(long timeout, @NonNull TimeUnit unit) { + return debounce(timeout, unit); + } + + /** + * Returns an {@code Observable} that mirrors the current {@code Observable}, except that it drops items emitted by the + * current {@code Observable} that are followed by newer items before a timeout value expires on a specified + * {@link Scheduler}. The timer resets on each emission (Alias to {@link #debounce(long, TimeUnit, Scheduler)}). + * <p> + * <em>Note:</em> If items keep being emitted by the current {@code Observable} faster than the timeout then no items + * will be emitted by the resulting {@code Observable}. + * <p> + * <img width="640" height="305" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/throttleWithTimeout.s.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>You specify which {@code Scheduler} this operator will use.</dd> + * </dl> + * + * @param timeout + * the length of the window of time that must pass after the emission of an item from the current + * {@code Observable}, in which the current {@code Observable} emits no items, in order for the item to be emitted by the + * resulting {@code Observable} + * @param unit + * the unit of time for the specified {@code timeout} + * @param scheduler + * the {@code Scheduler} to use internally to manage the timers that handle the timeout for each + * item + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code unit} or {@code scheduler} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/debounce.html">ReactiveX operators documentation: Debounce</a> + * @see #debounce(long, TimeUnit, Scheduler) + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.CUSTOM) + @NonNull + public final Observable<T> throttleWithTimeout(long timeout, @NonNull TimeUnit unit, @NonNull Scheduler scheduler) { + return debounce(timeout, unit, scheduler); + } + + /** + * Returns an {@code Observable} that mirrors the current {@code Observable}, except that it drops items emitted by the + * current {@code Observable} that are followed by newer items before a timeout value expires on a specified + * {@link Scheduler}. The timer resets on each emission (Alias to {@link #debounce(long, TimeUnit, Scheduler)}). + * <p> + * <em>Note:</em> If items keep being emitted by the current {@code Observable} faster than the timeout then no items + * will be emitted by the resulting {@code Observable}. + * <p> + * <img width="640" height="305" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/throttleWithTimeout.s.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>You specify which {@code Scheduler} this operator will use.</dd> + * </dl> + * + * @param timeout + * the length of the window of time that must pass after the emission of an item from the current + * {@code Observable}, in which the current {@code Observable} emits no items, in order for the item to be emitted by the + * resulting {@code Observable} + * @param unit + * the unit of time for the specified {@code timeout} + * @param scheduler + * the {@code Scheduler} to use internally to manage the timers that handle the timeout for each + * item + * @param onDropped + * called with the current entry when it has been replaced by a new one + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code unit} or {@code scheduler} is {@code null} or {@code onDropped} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/debounce.html">ReactiveX operators documentation: Debounce</a> + * @see #debounce(long, TimeUnit, Scheduler, Consumer) + * @since 3.1.6 - Experimental + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.CUSTOM) + @NonNull + @Experimental + public final Observable<T> throttleWithTimeout(long timeout, @NonNull TimeUnit unit, @NonNull Scheduler scheduler, @NonNull Consumer<? super T> onDropped) { + return debounce(timeout, unit, scheduler, onDropped); + } + + /** + * Returns an {@code Observable} that emits records of the time interval between consecutive items emitted by the + * current {@code Observable}. + * <p> + * <img width="640" height="310" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/timeInterval.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code timeInterval} does not operate on any particular scheduler but uses the current time + * from the {@code computation} {@link Scheduler}.</dd> + * </dl> + * + * @return the new {@code Observable} instance + * @see <a href="http://reactivex.io/documentation/operators/timeinterval.html">ReactiveX operators documentation: TimeInterval</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Observable<Timed<T>> timeInterval() { + return timeInterval(TimeUnit.MILLISECONDS, Schedulers.computation()); + } + + /** + * Returns an {@code Observable} that emits records of the time interval between consecutive items emitted by the + * current {@code Observable}, where this interval is computed on a specified {@link Scheduler}. + * <p> + * <img width="640" height="315" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/timeInterval.s.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>The operator does not operate on any particular scheduler but uses the current time + * from the specified {@code Scheduler}.</dd> + * </dl> + * + * @param scheduler + * the {@code Scheduler} used to compute time intervals + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code scheduler} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/timeinterval.html">ReactiveX operators documentation: TimeInterval</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) // Supplied scheduler is only used for creating timestamps. + @NonNull + public final Observable<Timed<T>> timeInterval(@NonNull Scheduler scheduler) { + return timeInterval(TimeUnit.MILLISECONDS, scheduler); + } + + /** + * Returns an {@code Observable} that emits records of the time interval between consecutive items emitted by the + * current {@code Observable}. + * <p> + * <img width="640" height="310" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/timeInterval.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code timeInterval} does not operate on any particular scheduler but uses the current time + * from the {@code computation} {@link Scheduler}.</dd> + * </dl> + * + * @param unit the time unit for the current time + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code unit} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/timeinterval.html">ReactiveX operators documentation: TimeInterval</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Observable<Timed<T>> timeInterval(@NonNull TimeUnit unit) { + return timeInterval(unit, Schedulers.computation()); + } + + /** + * Returns an {@code Observable} that emits records of the time interval between consecutive items emitted by the + * current {@code Observable}, where this interval is computed on a specified {@link Scheduler}. + * <p> + * <img width="640" height="315" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/timeInterval.s.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>The operator does not operate on any particular scheduler but uses the current time + * from the specified {@code Scheduler}.</dd> + * </dl> + * + * @param unit the time unit for the current time + * @param scheduler + * the {@code Scheduler} used to compute time intervals + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code unit} or {@code scheduler} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/timeinterval.html">ReactiveX operators documentation: TimeInterval</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) // Supplied scheduler is only used for creating timestamps. + @NonNull + public final Observable<Timed<T>> timeInterval(@NonNull TimeUnit unit, @NonNull Scheduler scheduler) { + Objects.requireNonNull(unit, "unit is null"); + Objects.requireNonNull(scheduler, "scheduler is null"); + return RxJavaPlugins.onAssembly(new ObservableTimeInterval<>(this, unit, scheduler)); + } + + /** + * Returns an {@code Observable} that mirrors the current {@code Observable}, but notifies observers of a + * {@link TimeoutException} if an item emitted by the current {@code Observable} doesn't arrive within a window of + * time after the emission of the previous item, where that period of time is measured by an {@link ObservableSource} that + * is a function of the previous item. + * <p> + * <img width="640" height="400" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/timeout3.v3.png" alt=""> + * <p> + * Note: The arrival of the first source item is never timed out. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>This version of {@code timeout} operates by default on the {@code immediate} {@link Scheduler}.</dd> + * </dl> + * + * @param <V> + * the timeout value type (ignored) + * @param itemTimeoutIndicator + * a function that returns an {@code ObservableSource} for each item emitted by the current + * {@code Observable} and that determines the timeout window for the subsequent item + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code itemTimeoutIndicator} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/timeout.html">ReactiveX operators documentation: Timeout</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull V> Observable<T> timeout(@NonNull Function<? super T, ? extends ObservableSource<V>> itemTimeoutIndicator) { + return timeout0(null, itemTimeoutIndicator, null); + } + + /** + * Returns an {@code Observable} that mirrors the current {@code Observable}, but that switches to a fallback {@link ObservableSource} if + * an item emitted by the current {@code Observable} doesn't arrive within a window of time after the emission of the + * previous item, where that period of time is measured by an {@code ObservableSource} that is a function of the previous + * item. + * <p> + * <img width="640" height="400" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/timeout4.v3.png" alt=""> + * <p> + * Note: The arrival of the first source item is never timed out. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>This version of {@code timeout} operates by default on the {@code immediate} {@link Scheduler}.</dd> + * </dl> + * + * @param <V> + * the timeout value type (ignored) + * @param itemTimeoutIndicator + * a function that returns an {@code ObservableSource}, for each item emitted by the current {@code Observable}, that + * determines the timeout window for the subsequent item + * @param fallback + * the fallback {@code ObservableSource} to switch to if the current {@code Observable} times out + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code itemTimeoutIndicator} or {@code fallback} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/timeout.html">ReactiveX operators documentation: Timeout</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull V> Observable<T> timeout(@NonNull Function<? super T, ? extends ObservableSource<V>> itemTimeoutIndicator, + @NonNull ObservableSource<? extends T> fallback) { + Objects.requireNonNull(fallback, "fallback is null"); + return timeout0(null, itemTimeoutIndicator, fallback); + } + + /** + * Returns an {@code Observable} that mirrors the current {@code Observable} but applies a timeout policy for each emitted + * item. If the next item isn't emitted within the specified timeout duration starting from its predecessor, + * the resulting {@code Observable} terminates and notifies observers of a {@link TimeoutException}. + * <p> + * <img width="640" height="305" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/timeout.1.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>This version of {@code timeout} operates by default on the {@code computation} {@link Scheduler}.</dd> + * </dl> + * + * @param timeout + * maximum duration between emitted items before a timeout occurs + * @param unit + * the unit of time that applies to the {@code timeout} argument. + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code unit} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/timeout.html">ReactiveX operators documentation: Timeout</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.COMPUTATION) + @NonNull + public final Observable<T> timeout(long timeout, @NonNull TimeUnit unit) { + return timeout0(timeout, unit, null, Schedulers.computation()); + } + + /** + * Returns an {@code Observable} that mirrors the current {@code Observable} but applies a timeout policy for each emitted + * item. If the next item isn't emitted within the specified timeout duration starting from its predecessor, + * the current {@code Observable} is disposed and the resulting {@code Observable} begins instead + * to mirror a fallback {@link ObservableSource}. + * <p> + * <img width="640" height="305" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/timeout.2.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>This version of {@code timeout} operates by default on the {@code computation} {@link Scheduler}.</dd> + * </dl> + * + * @param timeout + * maximum duration between items before a timeout occurs + * @param unit + * the unit of time that applies to the {@code timeout} argument + * @param fallback + * the fallback {@code ObservableSource} to use in case of a timeout + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code unit} or {@code fallback} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/timeout.html">ReactiveX operators documentation: Timeout</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.COMPUTATION) + @NonNull + public final Observable<T> timeout(long timeout, @NonNull TimeUnit unit, @NonNull ObservableSource<? extends T> fallback) { + Objects.requireNonNull(fallback, "fallback is null"); + return timeout0(timeout, unit, fallback, Schedulers.computation()); + } + + /** + * Returns an {@code Observable} that mirrors the current {@code Observable} but applies a timeout policy for each emitted + * item using a specified {@link Scheduler}. If the next item isn't emitted within the specified timeout duration + * starting from its predecessor, the current {@code Observable} is disposed and returned {@code Observable} + * begins instead to mirror a fallback {@link ObservableSource}. + * <p> + * <img width="640" height="305" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/timeout.2s.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>You specify which {@code Scheduler} this operator will use.</dd> + * </dl> + * + * @param timeout + * maximum duration between items before a timeout occurs + * @param unit + * the unit of time that applies to the {@code timeout} argument + * @param scheduler + * the {@code Scheduler} to run the timeout timers on + * @param fallback + * the {@code ObservableSource} to use as the fallback in case of a timeout + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code unit}, {@code scheduler} or {@code fallback} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/timeout.html">ReactiveX operators documentation: Timeout</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.CUSTOM) + @NonNull + public final Observable<T> timeout(long timeout, @NonNull TimeUnit unit, @NonNull Scheduler scheduler, @NonNull ObservableSource<? extends T> fallback) { + Objects.requireNonNull(fallback, "fallback is null"); + return timeout0(timeout, unit, fallback, scheduler); + } + + /** + * Returns an {@code Observable} that mirrors the current {@code Observable} but applies a timeout policy for each emitted + * item, where this policy is governed on a specified {@link Scheduler}. If the next item isn't emitted within the + * specified timeout duration starting from its predecessor, the resulting {@code Observable} terminates and + * notifies observers of a {@link TimeoutException}. + * <p> + * <img width="640" height="305" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/timeout.1s.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>You specify which {@code Scheduler} this operator will use.</dd> + * </dl> + * + * @param timeout + * maximum duration between items before a timeout occurs + * @param unit + * the unit of time that applies to the {@code timeout} argument + * @param scheduler + * the {@code Scheduler} to run the timeout timers on + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code unit} or {@code scheduler} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/timeout.html">ReactiveX operators documentation: Timeout</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.CUSTOM) + @NonNull + public final Observable<T> timeout(long timeout, @NonNull TimeUnit unit, @NonNull Scheduler scheduler) { + return timeout0(timeout, unit, null, scheduler); + } + + /** + * Returns an {@code Observable} that mirrors the current {@code Observable}, but notifies observers of a + * {@link TimeoutException} if either the first item emitted by the current {@code Observable} or any subsequent item + * doesn't arrive within time windows defined by indicator {@link ObservableSource}s. + * <p> + * <img width="640" height="400" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/timeout5.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>This version of {@code timeout} operates by default on the {@code immediate} {@link Scheduler}.</dd> + * </dl> + * + * @param <U> + * the first timeout value type (ignored) + * @param <V> + * the subsequent timeout value type (ignored) + * @param firstTimeoutIndicator + * a function that returns an {@code ObservableSource} that determines the timeout window for the first source + * item + * @param itemTimeoutIndicator + * a function that returns an {@code ObservableSource} for each item emitted by the current {@code Observable} and that + * determines the timeout window in which the subsequent source item must arrive in order to + * continue the sequence + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code firstTimeoutIndicator} or {@code itemTimeoutIndicator} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/timeout.html">ReactiveX operators documentation: Timeout</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull U, @NonNull V> Observable<T> timeout(@NonNull ObservableSource<U> firstTimeoutIndicator, + @NonNull Function<? super T, ? extends ObservableSource<V>> itemTimeoutIndicator) { + Objects.requireNonNull(firstTimeoutIndicator, "firstTimeoutIndicator is null"); + return timeout0(firstTimeoutIndicator, itemTimeoutIndicator, null); + } + + /** + * Returns an {@code Observable} that mirrors the current {@code Observable}, but switches to a fallback {@link ObservableSource} if either + * the first item emitted by the current {@code Observable} or any subsequent item doesn't arrive within time windows + * defined by indicator {@code ObservableSource}s. + * <p> + * <img width="640" height="400" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/timeout6.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>This version of {@code timeout} operates by default on the {@code immediate} {@link Scheduler}.</dd> + * </dl> + * + * @param <U> + * the first timeout value type (ignored) + * @param <V> + * the subsequent timeout value type (ignored) + * @param firstTimeoutIndicator + * a function that returns an {@code ObservableSource} which determines the timeout window for the first source + * item + * @param itemTimeoutIndicator + * a function that returns an {@code ObservableSource} for each item emitted by the current {@code Observable} and that + * determines the timeout window in which the subsequent source item must arrive in order to + * continue the sequence + * @param fallback + * the fallback {@code ObservableSource} to switch to if the current {@code Observable} times out + * @return the new {@code Observable} instance + * @throws NullPointerException + * if {@code firstTimeoutIndicator}, {@code itemTimeoutIndicator} or {@code fallback} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/timeout.html">ReactiveX operators documentation: Timeout</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull U, @NonNull V> Observable<T> timeout( + @NonNull ObservableSource<U> firstTimeoutIndicator, + @NonNull Function<? super T, ? extends ObservableSource<V>> itemTimeoutIndicator, + @NonNull ObservableSource<? extends T> fallback) { + Objects.requireNonNull(firstTimeoutIndicator, "firstTimeoutIndicator is null"); + Objects.requireNonNull(fallback, "fallback is null"); + return timeout0(firstTimeoutIndicator, itemTimeoutIndicator, fallback); + } + + @NonNull + private Observable<T> timeout0(long timeout, @NonNull TimeUnit unit, + @Nullable ObservableSource<? extends T> fallback, + @NonNull Scheduler scheduler) { + Objects.requireNonNull(unit, "unit is null"); + Objects.requireNonNull(scheduler, "scheduler is null"); + return RxJavaPlugins.onAssembly(new ObservableTimeoutTimed<>(this, timeout, unit, scheduler, fallback)); + } + + @NonNull + private <U, V> Observable<T> timeout0( + @NonNull ObservableSource<U> firstTimeoutIndicator, + @NonNull Function<? super T, ? extends ObservableSource<V>> itemTimeoutIndicator, + @Nullable ObservableSource<? extends T> fallback) { + Objects.requireNonNull(itemTimeoutIndicator, "itemTimeoutIndicator is null"); + return RxJavaPlugins.onAssembly(new ObservableTimeout<>(this, firstTimeoutIndicator, itemTimeoutIndicator, fallback)); + } + + /** + * Returns an {@code Observable} that emits each item emitted by the current {@code Observable}, wrapped in a + * {@link Timed} object. + * <p> + * <img width="640" height="310" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/timestamp.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code timestamp} does not operate on any particular scheduler but uses the current time + * from the {@code computation} {@link Scheduler}.</dd> + * </dl> + * + * @return the new {@code Observable} instance + * @see <a href="http://reactivex.io/documentation/operators/timestamp.html">ReactiveX operators documentation: Timestamp</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Observable<Timed<T>> timestamp() { + return timestamp(TimeUnit.MILLISECONDS, Schedulers.computation()); + } + + /** + * Returns an {@code Observable} that emits each item emitted by the current {@code Observable}, wrapped in a + * {@link Timed} object whose timestamps are provided by a specified {@link Scheduler}. + * <p> + * <img width="640" height="310" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/timestamp.s.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>This operator does not operate on any particular scheduler but uses the current time + * from the specified {@code Scheduler}.</dd> + * </dl> + * + * @param scheduler + * the {@code Scheduler} to use as a time source + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code scheduler} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/timestamp.html">ReactiveX operators documentation: Timestamp</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) // Supplied scheduler is only used for creating timestamps. + @NonNull + public final Observable<Timed<T>> timestamp(@NonNull Scheduler scheduler) { + return timestamp(TimeUnit.MILLISECONDS, scheduler); + } + + /** + * Returns an {@code Observable} that emits each item emitted by the current {@code Observable}, wrapped in a + * {@link Timed} object. + * <p> + * <img width="640" height="310" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/timestamp.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code timestamp} does not operate on any particular scheduler but uses the current time + * from the {@code computation} {@link Scheduler}.</dd> + * </dl> + * + * @param unit the time unit for the current time + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code unit} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/timestamp.html">ReactiveX operators documentation: Timestamp</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Observable<Timed<T>> timestamp(@NonNull TimeUnit unit) { + return timestamp(unit, Schedulers.computation()); + } + + /** + * Returns an {@code Observable} that emits each item emitted by the current {@code Observable}, wrapped in a + * {@link Timed} object whose timestamps are provided by a specified {@link Scheduler}. + * <p> + * <img width="640" height="310" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/timestamp.s.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>This operator does not operate on any particular scheduler but uses the current time + * from the specified {@code Scheduler}.</dd> + * </dl> + * + * @param unit the time unit for the current time + * @param scheduler + * the {@code Scheduler} to use as a time source + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code unit} or {@code scheduler} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/timestamp.html">ReactiveX operators documentation: Timestamp</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) // Supplied scheduler is only used for creating timestamps. + @NonNull + public final Observable<Timed<T>> timestamp(@NonNull TimeUnit unit, @NonNull Scheduler scheduler) { + Objects.requireNonNull(unit, "unit is null"); + Objects.requireNonNull(scheduler, "scheduler is null"); + return map(Functions.timestampWith(unit, scheduler)); + } + + /** + * Calls the specified converter function during assembly time and returns its resulting value. + * <p> + * This allows fluent conversion to any other type. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code to} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * <p>History: 2.1.7 - experimental + * @param <R> the resulting object type + * @param converter the function that receives the current {@code Observable} instance and returns a value + * @return the converted value + * @throws NullPointerException if {@code converter} is {@code null} + * @since 2.2 + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull R> R to(@NonNull ObservableConverter<T, ? extends R> converter) { + return Objects.requireNonNull(converter, "converter is null").apply(this); + } + + /** + * Returns a {@link Single} that emits a single item, a {@link List} composed of all the items emitted by the + * current and finite {@code Observable}. + * <p> + * <img width="640" height="305" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/toList.2.v3.png" alt=""> + * <p> + * Normally, an {@link ObservableSource} that returns multiple items will do so by invoking its {@link Observer}'s + * {@link Observer#onNext onNext} method for each such item. You can change this behavior by having the + * operator to compose a list of all of these items and then to invoke the {@link SingleObserver}'s {@code onSuccess} + * method once, passing it the entire list, by calling the {@code Observable}'s {@code toList} method prior to + * calling its {@link #subscribe} method. + * <p> + * Note that this operator requires the upstream to signal {@code onComplete} for the accumulated list to + * be emitted. Sources that are infinite and never complete will never emit anything through this + * operator and an infinite source may lead to a fatal {@link OutOfMemoryError}. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code toList} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @return the new {@code Single} instance + * @see <a href="http://reactivex.io/documentation/operators/to.html">ReactiveX operators documentation: To</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Single<@NonNull List<T>> toList() { + return toList(16); + } + + /** + * Returns a {@link Single} that emits a single item, a {@link List} composed of all the items emitted by the + * current and finite {@code Observable}. + * <p> + * <img width="640" height="305" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/toList.2.v3.png" alt=""> + * <p> + * Normally, an {@link ObservableSource} that returns multiple items will do so by invoking its {@link Observer}'s + * {@link Observer#onNext onNext} method for each such item. You can change this behavior by having the + * operator to compose a list of all of these items and then to invoke the {@link SingleObserver}'s {@code onSuccess} + * method once, passing it the entire list, by calling the {@code Observable}'s {@code toList} method prior to + * calling its {@link #subscribe} method. + * <p> + * Note that this operator requires the upstream to signal {@code onComplete} for the accumulated list to + * be emitted. Sources that are infinite and never complete will never emit anything through this + * operator and an infinite source may lead to a fatal {@link OutOfMemoryError}. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code toList} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param capacityHint + * the number of elements expected from the current {@code Observable} + * @return the new {@code Single} instance + * @throws IllegalArgumentException if {@code capacityHint} is non-positive + * @see <a href="http://reactivex.io/documentation/operators/to.html">ReactiveX operators documentation: To</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Single<@NonNull List<T>> toList(int capacityHint) { + ObjectHelper.verifyPositive(capacityHint, "capacityHint"); + return RxJavaPlugins.onAssembly(new ObservableToListSingle<>(this, capacityHint)); + } + + /** + * Returns a {@link Single} that emits a single item, a {@link Collection} (subclass) composed of all the items emitted by the + * finite upstream {@code Observable}. + * <p> + * <img width="640" height="365" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/toList.o.c.png" alt=""> + * <p> + * Normally, an {@link ObservableSource} that returns multiple items will do so by invoking its {@link Observer}'s + * {@link Observer#onNext onNext} method for each such item. You can change this behavior by having the + * operator to compose a collection of all of these items and then to invoke the {@link SingleObserver}'s {@code onSuccess} + * method once, passing it the entire collection, by calling the {@code Observable}'s {@code toList} method prior to + * calling its {@link #subscribe} method. + * <p> + * Note that this operator requires the upstream to signal {@code onComplete} for the accumulated collection to + * be emitted. Sources that are infinite and never complete will never emit anything through this + * operator and an infinite source may lead to a fatal {@link OutOfMemoryError}. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code toList} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <U> the subclass of a collection of Ts + * @param collectionSupplier + * the {@link Supplier} returning the collection (for each individual {@code Observer}) to be filled in + * @return the new {@code Single} instance + * @throws NullPointerException if {@code collectionSupplier} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/to.html">ReactiveX operators documentation: To</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull U extends Collection<? super T>> Single<U> toList(@NonNull Supplier<U> collectionSupplier) { + Objects.requireNonNull(collectionSupplier, "collectionSupplier is null"); + return RxJavaPlugins.onAssembly(new ObservableToListSingle<>(this, collectionSupplier)); + } + + /** + * Returns a {@link Single} that emits a single {@link HashMap} containing all items emitted by the + * current and finite {@code Observable}, mapped by the keys returned by a specified + * {@code keySelector} function. + * <p> + * <img width="640" height="305" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/toMap.2.v3.png" alt=""> + * <p> + * If more than one source item maps to the same key, the {@code HashMap} will contain the latest of those items. + * <p> + * Note that this operator requires the upstream to signal {@code onComplete} for the accumulated {@code HashMap} to + * be emitted. Sources that are infinite and never complete will never emit anything through this + * operator and an infinite source may lead to a fatal {@link OutOfMemoryError}. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code toMap} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <K> the key type of the Map + * @param keySelector + * the function that extracts the key from a source item to be used in the {@code HashMap} + * @return the new {@code Single} instance + * @throws NullPointerException if {@code keySelector} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/to.html">ReactiveX operators documentation: To</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull K> Single<@NonNull Map<K, T>> toMap(@NonNull Function<? super T, ? extends K> keySelector) { + Objects.requireNonNull(keySelector, "keySelector is null"); + return collect(HashMapSupplier.asSupplier(), Functions.toMapKeySelector(keySelector)); + } + + /** + * Returns a {@link Single} that emits a single {@link HashMap} containing values corresponding to items emitted by the + * current and finite {@code Observable}, mapped by the keys and values returned by the given selector functions. + * <p> + * <img width="640" height="305" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/toMap.2.v3.png" alt=""> + * <p> + * If more than one source item maps to the same key, the {@code HashMap} will contain a single entry that + * corresponds to the latest of those items. + * <p> + * Note that this operator requires the upstream to signal {@code onComplete} for the accumulated {@code HashMap} to + * be emitted. Sources that are infinite and never complete will never emit anything through this + * operator and an infinite source may lead to a fatal {@link OutOfMemoryError}. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code toMap} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <K> the key type of the {@code HashMap} + * @param <V> the value type of the {@code HashMap} + * @param keySelector + * the function that extracts the key from a source item to be used in the {@code HashMap} + * @param valueSelector + * the function that extracts the value from a source item to be used in the {@code HashMap} + * @return the new {@code Single} instance + * @throws NullPointerException if {@code keySelector} or {@code valueSelector} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/to.html">ReactiveX operators documentation: To</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull K, @NonNull V> Single<Map<K, V>> toMap( + @NonNull Function<? super T, ? extends K> keySelector, + @NonNull Function<? super T, ? extends V> valueSelector) { + Objects.requireNonNull(keySelector, "keySelector is null"); + Objects.requireNonNull(valueSelector, "valueSelector is null"); + return collect(HashMapSupplier.asSupplier(), Functions.toMapKeyValueSelector(keySelector, valueSelector)); + } + + /** + * Returns a {@link Single} that emits a single {@link Map} (subclass), returned by a specified {@code mapFactory} function, that + * contains keys and values extracted from the items, via selector functions, emitted by the current and finite {@code Observable}. + * <p> + * <img width="640" height="305" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/toMap.2.v3.png" alt=""> + * <p> + * Note that this operator requires the upstream to signal {@code onComplete} for the accumulated {@code Map} to + * be emitted. Sources that are infinite and never complete will never emit anything through this + * operator and an infinite source may lead to a fatal {@link OutOfMemoryError}. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code toMap} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <K> the key type of the {@code Map} + * @param <V> the value type of the {@code Map} + * @param keySelector + * the function that extracts the key from a source item to be used in the {@code Map} + * @param valueSelector + * the function that extracts the value from the source items to be used as value in the {@code Map} + * @param mapSupplier + * the function that returns a {@code Map} instance to be used + * @return the new {@code Single} instance + * @throws NullPointerException if {@code keySelector}, {@code valueSelector} or {@code mapSupplier} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/to.html">ReactiveX operators documentation: To</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull K, @NonNull V> Single<Map<K, V>> toMap( + @NonNull Function<? super T, ? extends K> keySelector, + @NonNull Function<? super T, ? extends V> valueSelector, + @NonNull Supplier<? extends Map<K, V>> mapSupplier) { + Objects.requireNonNull(keySelector, "keySelector is null"); + Objects.requireNonNull(valueSelector, "valueSelector is null"); + Objects.requireNonNull(mapSupplier, "mapSupplier is null"); + return collect(mapSupplier, Functions.toMapKeyValueSelector(keySelector, valueSelector)); + } + + /** + * Returns a {@link Single} that emits a single {@link HashMap} that contains an {@link ArrayList} of items emitted by the + * current and finite {@code Observable} keyed by a specified {@code keySelector} function. + * <p> + * <img width="640" height="305" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/toMultiMap.2.v3.png" alt=""> + * <p> + * Note that this operator requires the upstream to signal {@code onComplete} for the accumulated {@code HashMap} to + * be emitted. Sources that are infinite and never complete will never emit anything through this + * operator and an infinite source may lead to a fatal {@link OutOfMemoryError}. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code toMultimap} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <K> the key type of the {@code HashMap} + * @param keySelector + * the function that extracts the key from the source items to be used as key in the {@code HashMap} + * @return the new {@code Single} instance + * @throws NullPointerException if {@code keySelector} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/to.html">ReactiveX operators documentation: To</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull K> Single<@NonNull Map<K, Collection<T>>> toMultimap(@NonNull Function<? super T, ? extends K> keySelector) { + Function<? super T, ? extends T> valueSelector = Functions.identity(); + Supplier<Map<K, Collection<T>>> mapSupplier = HashMapSupplier.asSupplier(); + Function<K, List<T>> collectionFactory = ArrayListSupplier.asFunction(); + return toMultimap(keySelector, valueSelector, mapSupplier, collectionFactory); + } + + /** + * Returns a {@link Single} that emits a single {@link HashMap} that contains an {@link ArrayList} of values extracted by a + * specified {@code valueSelector} function from items emitted by the current and finite {@code Observable}, + * keyed by a specified {@code keySelector} function. + * <p> + * <img width="640" height="305" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/toMultiMap.2.v3.png" alt=""> + * <p> + * Note that this operator requires the upstream to signal {@code onComplete} for the accumulated {@code HashMap} to + * be emitted. Sources that are infinite and never complete will never emit anything through this + * operator and an infinite source may lead to a fatal {@link OutOfMemoryError}. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code toMultimap} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <K> the key type of the {@code HashMap} + * @param <V> the value type of the {@code HashMap} + * @param keySelector + * the function that extracts a key from the source items to be used as key in the {@code HashMap} + * @param valueSelector + * the function that extracts a value from the source items to be used as value in the {@code HashMap} + * @return the new {@code Single} instance + * @throws NullPointerException if {@code keySelector} or {@code valueSelector} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/to.html">ReactiveX operators documentation: To</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull K, @NonNull V> Single<@NonNull Map<K, Collection<V>>> toMultimap(@NonNull Function<? super T, ? extends K> keySelector, Function<? super T, ? extends V> valueSelector) { + Supplier<Map<K, Collection<V>>> mapSupplier = HashMapSupplier.asSupplier(); + Function<K, List<V>> collectionFactory = ArrayListSupplier.asFunction(); + return toMultimap(keySelector, valueSelector, mapSupplier, collectionFactory); + } + + /** + * Returns a {@link Single} that emits a single {@code Map} (subclass), returned by a specified {@code mapFactory} function, that + * contains a custom {@link Collection} of values, extracted by a specified {@code valueSelector} function from + * items emitted by the current and finite {@code Observable}, and keyed by the {@code keySelector} function. + * <p> + * <img width="640" height="305" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/toMultiMap.2.v3.png" alt=""> + * <p> + * Note that this operator requires the upstream to signal {@code onComplete} for the accumulated {@code Map} to + * be emitted. Sources that are infinite and never complete will never emit anything through this + * operator and an infinite source may lead to a fatal {@link OutOfMemoryError}. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code toMultimap} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <K> the key type of the {@code Map} + * @param <V> the value type of the {@code Map} + * @param keySelector + * the function that extracts a key from the source items to be used as the key in the {@code Map} + * @param valueSelector + * the function that extracts a value from the source items to be used as the value in the {@code Map} + * @param mapSupplier + * the function that returns a {@code Map} instance to be used + * @param collectionFactory + * the function that returns a {@code Collection} instance for a particular key to be used in the {@code Map} + * @return the new {@code Single} instance + * @throws NullPointerException if {@code keySelector}, {@code valueSelector}, {@code mapSupplier} or {@code collectionFactory} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/to.html">ReactiveX operators documentation: To</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull K, @NonNull V> Single<@NonNull Map<K, Collection<V>>> toMultimap( + @NonNull Function<? super T, ? extends K> keySelector, + @NonNull Function<? super T, ? extends V> valueSelector, + @NonNull Supplier<? extends Map<K, Collection<V>>> mapSupplier, + @NonNull Function<? super K, ? extends Collection<? super V>> collectionFactory) { + Objects.requireNonNull(keySelector, "keySelector is null"); + Objects.requireNonNull(valueSelector, "valueSelector is null"); + Objects.requireNonNull(mapSupplier, "mapSupplier is null"); + Objects.requireNonNull(collectionFactory, "collectionFactory is null"); + return collect(mapSupplier, Functions.toMultimapKeyValueSelector(keySelector, valueSelector, collectionFactory)); + } + + /** + * Returns a {@link Single} that emits a single {@link Map} (subclass), returned by a specified {@code mapFactory} function, that + * contains an {@link ArrayList} of values, extracted by a specified {@code valueSelector} function from items + * emitted by the current and finite {@code Observable} and keyed by the {@code keySelector} function. + * <p> + * <img width="640" height="305" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/toMultiMap.2.v3.png" alt=""> + * <p> + * Note that this operator requires the upstream to signal {@code onComplete} for the accumulated {@code Map} to + * be emitted. Sources that are infinite and never complete will never emit anything through this + * operator and an infinite source may lead to a fatal {@link OutOfMemoryError}. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code toMultimap} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <K> the key type of the {@code Map} + * @param <V> the value type of the {@code Map} + * @param keySelector + * the function that extracts a key from the source items to be used as the key in the {@code Map} + * @param valueSelector + * the function that extracts a value from the source items to be used as the value in the {@code Map} + * @param mapSupplier + * the function that returns a {@code Map} instance to be used + * @return the new {@code Single} instance + * @throws NullPointerException if {@code keySelector}, {@code valueSelector} or {@code mapSupplier} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/to.html">ReactiveX operators documentation: To</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull K, @NonNull V> Single<@NonNull Map<K, Collection<V>>> toMultimap( + @NonNull Function<? super T, ? extends K> keySelector, + @NonNull Function<? super T, ? extends V> valueSelector, + @NonNull Supplier<Map<K, Collection<V>>> mapSupplier + ) { + return toMultimap(keySelector, valueSelector, mapSupplier, ArrayListSupplier.asFunction()); + } + + /** + * Converts the current {@code Observable} into a {@link Flowable} by applying the specified backpressure strategy. + * <p> + * Marble diagrams for the various backpressure strategies are as follows: + * <ul> + * <li>{@link BackpressureStrategy#BUFFER} + * <p> + * <img width="640" height="274" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/toFlowable.o.buffer.png" alt=""> + * </li> + * <li>{@link BackpressureStrategy#DROP} + * <p> + * <img width="640" height="389" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/toFlowable.o.drop.png" alt=""> + * </li> + * <li>{@link BackpressureStrategy#LATEST} + * <p> + * <img width="640" height="297" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/toFlowable.o.latest.png" alt=""> + * </li> + * <li>{@link BackpressureStrategy#ERROR} + * <p> + * <img width="640" height="380" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/toFlowable.o.error.png" alt=""> + * </li> + * <li>{@link BackpressureStrategy#MISSING} + * <p> + * <img width="640" height="412" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/toFlowable.o.missing.png" alt=""> + * </li> + * </ul> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator applies the chosen backpressure strategy of {@link BackpressureStrategy} enum.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code toFlowable} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param strategy the backpressure strategy to apply + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code strategy} is {@code null} + */ + @BackpressureSupport(BackpressureKind.SPECIAL) + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Flowable<T> toFlowable(@NonNull BackpressureStrategy strategy) { + Objects.requireNonNull(strategy, "strategy is null"); + Flowable<T> f = new FlowableFromObservable<>(this); + switch (strategy) { + case DROP: + return f.onBackpressureDrop(); + case LATEST: + return f.onBackpressureLatest(); + case MISSING: + return f; + case ERROR: + return RxJavaPlugins.onAssembly(new FlowableOnBackpressureError<>(f)); + default: + return f.onBackpressureBuffer(); + } + } + + /** + * Returns a {@link Single} that emits a {@link List} that contains the items emitted by the current and finite {@code Observable}, in a + * sorted order. Each item emitted by the current {@code Observable} must implement {@link Comparable} with respect to all + * other items in the sequence. + * + * <p> + * If any item emitted by the current {@code Observable} does not implement {@code Comparable} with respect to + * all other items emitted by the current {@code Observable}, no items will be emitted and the + * sequence is terminated with a {@link ClassCastException}. + * <p> + * <img width="640" height="310" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/toSortedList.2.v3.png" alt=""> + * <p> + * Note that this operator requires the upstream to signal {@code onComplete} for the accumulated {@code List} to + * be emitted. Sources that are infinite and never complete will never emit anything through this + * operator and an infinite source may lead to a fatal {@link OutOfMemoryError}. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code toSortedList} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @return the new {@code Single} instance + * @see <a href="http://reactivex.io/documentation/operators/to.html">ReactiveX operators documentation: To</a> + * @see #toSortedList(int) + * @see #toSortedList(Comparator) + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Single<@NonNull List<T>> toSortedList() { + return toSortedList(Functions.naturalComparator()); + } + + /** + * Returns a {@link Single} that emits a {@link List} that contains the items emitted by the current and finite {@code Observable}, in a + * sorted order based on a specified comparison function. + * <p> + * <img width="640" height="310" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/toSortedList.f.2.v3.png" alt=""> + * <p> + * Note that this operator requires the upstream to signal {@code onComplete} for the accumulated {@code List} to + * be emitted. Sources that are infinite and never complete will never emit anything through this + * operator and an infinite source may lead to a fatal {@link OutOfMemoryError}. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code toSortedList} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param comparator + * a function that compares two items emitted by the current {@code Observable} and returns an {@code int} + * that indicates their sort order + * @return the new {@code Single} instance + * @throws NullPointerException if {@code comparator} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/to.html">ReactiveX operators documentation: To</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Single<@NonNull List<T>> toSortedList(@NonNull Comparator<? super T> comparator) { + Objects.requireNonNull(comparator, "comparator is null"); + return toList().map(Functions.listSorter(comparator)); + } + + /** + * Returns a {@link Single} that emits a {@link List} that contains the items emitted by the current and finite {@code Observable}, in a + * sorted order based on a specified comparison function. + * <p> + * <img width="640" height="310" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/toSortedList.f.2.v3.png" alt=""> + * <p> + * Note that this operator requires the upstream to signal {@code onComplete} for the accumulated {@code List} to + * be emitted. Sources that are infinite and never complete will never emit anything through this + * operator and an infinite source may lead to a fatal {@link OutOfMemoryError}. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code toSortedList} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param comparator + * a function that compares two items emitted by the current {@code Observable} and returns an {@code int} + * that indicates their sort order + * @param capacityHint + * the initial capacity of the {@code List} used to accumulate items before sorting + * @return the new {@code Single} instance + * @throws NullPointerException if {@code comparator} is {@code null} + * @throws IllegalArgumentException if {@code capacityHint} is non-positive + * @see <a href="http://reactivex.io/documentation/operators/to.html">ReactiveX operators documentation: To</a> + * @since 2.0 + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Single<@NonNull List<T>> toSortedList(@NonNull Comparator<? super T> comparator, int capacityHint) { + Objects.requireNonNull(comparator, "comparator is null"); + return toList(capacityHint).map(Functions.listSorter(comparator)); + } + + /** + * Returns a {@link Single} that emits a {@link List} that contains the items emitted by the current and finite {@code Observable}, in a + * sorted order. Each item emitted by the current {@code Observable} must implement {@link Comparable} with respect to all + * other items in the sequence. + * <p> + * If any item emitted by the current {@code Observable} does not implement {@code Comparable} with respect to + * all other items emitted by the current {@code Observable}, no items will be emitted and the + * sequence is terminated with a {@link ClassCastException}. + * <p> + * <img width="640" height="310" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/toSortedList.2.v3.png" alt=""> + * <p> + * Note that this operator requires the upstream to signal {@code onComplete} for the accumulated {@code List} to + * be emitted. Sources that are infinite and never complete will never emit anything through this + * operator and an infinite source may lead to a fatal {@link OutOfMemoryError}. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code toSortedList} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param capacityHint + * the initial capacity of the {@code List} used to accumulate items before sorting + * @return the new {@code Single} instance + * @throws IllegalArgumentException if {@code capacityHint} is non-positive + * @see <a href="http://reactivex.io/documentation/operators/to.html">ReactiveX operators documentation: To</a> + * @since 2.0 + * @see #toSortedList(Comparator, int) + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Single<@NonNull List<T>> toSortedList(int capacityHint) { + return toSortedList(Functions.naturalComparator(), capacityHint); + } + + /** + * Return an {@code Observable} that schedules the downstream {@link Observer}s' {@code dispose} calls + * aimed at the current {@code Observable} on the given {@link Scheduler}. + * <p> + * <img width="640" height="453" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/unsubscribeOn.o.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>You specify which {@code Scheduler} this operator will use.</dd> + * </dl> + * + * @param scheduler + * the {@code Scheduler} to perform the call to {@code dispose()} of the upstream {@link Disposable} + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code scheduler} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/subscribeon.html">ReactiveX operators documentation: SubscribeOn</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.CUSTOM) + @NonNull + public final Observable<T> unsubscribeOn(@NonNull Scheduler scheduler) { + Objects.requireNonNull(scheduler, "scheduler is null"); + return RxJavaPlugins.onAssembly(new ObservableUnsubscribeOn<>(this, scheduler)); + } + + /** + * Returns an {@code Observable} that emits windows of items it collects from the current {@code Observable}. The resulting + * {@code Observable} emits connected, non-overlapping windows, each containing {@code count} items. When the current + * {@code Observable} completes or encounters an error, the resulting {@code Observable} emits the current window and + * propagates the notification from the current {@code Observable}. + * <p> + * <img width="640" height="400" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/window3.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>This version of {@code window} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param count + * the maximum size of each window before it should be emitted + * @return the new {@code Observable} instance + * @throws IllegalArgumentException if {@code count} is non-positive + * @see <a href="http://reactivex.io/documentation/operators/window.html">ReactiveX operators documentation: Window</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Observable<Observable<T>> window(long count) { + return window(count, count, bufferSize()); + } + + /** + * Returns an {@code Observable} that emits windows of items it collects from the current {@code Observable}. The resulting + * {@code Observable} emits windows every {@code skip} items, each containing no more than {@code count} items. When + * the current {@code Observable} completes or encounters an error, the resulting {@code Observable} emits the current window + * and propagates the notification from the current {@code Observable}. + * <p> + * <img width="640" height="365" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/window4.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>This version of {@code window} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param count + * the maximum size of each window before it should be emitted + * @param skip + * how many items need to be skipped before starting a new window. Note that if {@code skip} and + * {@code count} are equal this is the same operation as {@link #window(long)}. + * @return the new {@code Observable} instance + * @throws IllegalArgumentException if {@code count} or {@code skip} is non-positive + * @see <a href="http://reactivex.io/documentation/operators/window.html">ReactiveX operators documentation: Window</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Observable<Observable<T>> window(long count, long skip) { + return window(count, skip, bufferSize()); + } + + /** + * Returns an {@code Observable} that emits windows of items it collects from the current {@code Observable}. The resulting + * {@code Observable} emits windows every {@code skip} items, each containing no more than {@code count} items. When + * the current {@code Observable} completes or encounters an error, the resulting {@code Observable} emits the current window + * and propagates the notification from the current {@code Observable}. + * <p> + * <img width="640" height="365" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/window4.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>This version of {@code window} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param count + * the maximum size of each window before it should be emitted + * @param skip + * how many items need to be skipped before starting a new window. Note that if {@code skip} and + * {@code count} are equal this is the same operation as {@link #window(long)}. + * @param bufferSize + * the capacity hint for the buffer in the inner windows + * @return the new {@code Observable} instance + * @throws IllegalArgumentException if {@code count}, {@code skip} or {@code bufferSize} is non-positive + * @see <a href="http://reactivex.io/documentation/operators/window.html">ReactiveX operators documentation: Window</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Observable<Observable<T>> window(long count, long skip, int bufferSize) { + ObjectHelper.verifyPositive(count, "count"); + ObjectHelper.verifyPositive(skip, "skip"); + ObjectHelper.verifyPositive(bufferSize, "bufferSize"); + return RxJavaPlugins.onAssembly(new ObservableWindow<>(this, count, skip, bufferSize)); + } + + /** + * Returns an {@code Observable} that emits windows of items it collects from the current {@code Observable}. The resulting + * {@code Observable} starts a new window periodically, as determined by the {@code timeskip} argument. It emits + * each window after a fixed timespan, specified by the {@code timespan} argument. When the current + * {@code Observable} completes or encounters an error, the resulting {@code Observable} emits the + * current window and propagates the notification from the current {@code Observable}. + * <p> + * <img width="640" height="335" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/window7.v3.png" alt=""> + * <p> + * Note that ignoring windows or subscribing later (i.e., on another thread) will result in + * so-called window abandonment where a window may not contain any elements. In this case, subsequent + * elements will be dropped until the condition for the next window boundary is satisfied. The behavior is + * a trade-off for ensuring upstream cancellation can happen under some race conditions. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>This version of {@code window} operates by default on the {@code computation} {@link Scheduler}.</dd> + * </dl> + * + * @param timespan + * the period of time each window collects items before it should be emitted + * @param timeskip + * the period of time after which a new window will be created + * @param unit + * the unit of time that applies to the {@code timespan} and {@code timeskip} arguments + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code unit} is {@code null} + * @throws IllegalArgumentException if {@code timespan} or {@code timeskip} is non-positive + * @see <a href="http://reactivex.io/documentation/operators/window.html">ReactiveX operators documentation: Window</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.COMPUTATION) + @NonNull + public final Observable<Observable<T>> window(long timespan, long timeskip, @NonNull TimeUnit unit) { + return window(timespan, timeskip, unit, Schedulers.computation(), bufferSize()); + } + + /** + * Returns an {@code Observable} that emits windows of items it collects from the current {@code Observable}. The resulting + * {@code Observable} starts a new window periodically, as determined by the {@code timeskip} argument. It emits + * each window after a fixed timespan, specified by the {@code timespan} argument. When the current + * {@code Observable} completes or encounters an error, the resulting {@code Observable} emits the + * current window and propagates the notification from the current {@code Observable}. + * <p> + * <img width="640" height="335" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/window7.s.v3.png" alt=""> + * <p> + * Note that ignoring windows or subscribing later (i.e., on another thread) will result in + * so-called window abandonment where a window may not contain any elements. In this case, subsequent + * elements will be dropped until the condition for the next window boundary is satisfied. The behavior is + * a trade-off for ensuring upstream cancellation can happen under some race conditions. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>You specify which {@link Scheduler} this operator will use.</dd> + * </dl> + * + * @param timespan + * the period of time each window collects items before it should be emitted + * @param timeskip + * the period of time after which a new window will be created + * @param unit + * the unit of time that applies to the {@code timespan} and {@code timeskip} arguments + * @param scheduler + * the {@code Scheduler} to use when determining the end and start of a window + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code unit} or {@code scheduler} is {@code null} + * @throws IllegalArgumentException if {@code timespan} or {@code timeskip} is non-positive + * @see <a href="http://reactivex.io/documentation/operators/window.html">ReactiveX operators documentation: Window</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.CUSTOM) + @NonNull + public final Observable<Observable<T>> window(long timespan, long timeskip, @NonNull TimeUnit unit, @NonNull Scheduler scheduler) { + return window(timespan, timeskip, unit, scheduler, bufferSize()); + } + + /** + * Returns an {@code Observable} that emits windows of items it collects from the current {@code Observable}. The resulting + * {@code Observable} starts a new window periodically, as determined by the {@code timeskip} argument. It emits + * each window after a fixed timespan, specified by the {@code timespan} argument. When the current + * {@code Observable} completes or encounters an error, the resulting {@code Observable} emits the + * current window and propagates the notification from the current {@code Observable}. + * <p> + * <img width="640" height="335" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/window7.s.v3.png" alt=""> + * <p> + * Note that ignoring windows or subscribing later (i.e., on another thread) will result in + * so-called window abandonment where a window may not contain any elements. In this case, subsequent + * elements will be dropped until the condition for the next window boundary is satisfied. The behavior is + * a trade-off for ensuring upstream cancellation can happen under some race conditions. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>You specify which {@link Scheduler} this operator will use.</dd> + * </dl> + * + * @param timespan + * the period of time each window collects items before it should be emitted + * @param timeskip + * the period of time after which a new window will be created + * @param unit + * the unit of time that applies to the {@code timespan} and {@code timeskip} arguments + * @param scheduler + * the {@code Scheduler} to use when determining the end and start of a window + * @param bufferSize + * the capacity hint for the buffer in the inner windows + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code unit} or {@code scheduler} is {@code null} + * @throws IllegalArgumentException if {@code timespan}, {@code timeskip} or {@code bufferSize} is non-positive + * @see <a href="http://reactivex.io/documentation/operators/window.html">ReactiveX operators documentation: Window</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.CUSTOM) + @NonNull + public final Observable<Observable<T>> window(long timespan, long timeskip, @NonNull TimeUnit unit, @NonNull Scheduler scheduler, int bufferSize) { + ObjectHelper.verifyPositive(timespan, "timespan"); + ObjectHelper.verifyPositive(timeskip, "timeskip"); + ObjectHelper.verifyPositive(bufferSize, "bufferSize"); + Objects.requireNonNull(scheduler, "scheduler is null"); + Objects.requireNonNull(unit, "unit is null"); + return RxJavaPlugins.onAssembly(new ObservableWindowTimed<>(this, timespan, timeskip, unit, scheduler, Long.MAX_VALUE, bufferSize, false)); + } + + /** + * Returns an {@code Observable} that emits windows of items it collects from the current {@code Observable}. The resulting + * {@code Observable} emits connected, non-overlapping windows, each of a fixed duration specified by the + * {@code timespan} argument. When the current {@code Observable} completes or encounters an error, the resulting + * {@code Observable} emits the current window and propagates the notification from the current {@code Observable}. + * <p> + * <img width="640" height="375" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/window5.v3.png" alt=""> + * <p> + * Note that ignoring windows or subscribing later (i.e., on another thread) will result in + * so-called window abandonment where a window may not contain any elements. In this case, subsequent + * elements will be dropped until the condition for the next window boundary is satisfied. The behavior is + * a trade-off for ensuring upstream cancellation can happen under some race conditions. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>This version of {@code window} operates by default on the {@code computation} {@link Scheduler}.</dd> + * </dl> + * + * @param timespan + * the period of time each window collects items before it should be emitted and replaced with a + * new window + * @param unit + * the unit of time that applies to the {@code timespan} argument + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code unit} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/window.html">ReactiveX operators documentation: Window</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.COMPUTATION) + @NonNull + public final Observable<Observable<T>> window(long timespan, @NonNull TimeUnit unit) { + return window(timespan, unit, Schedulers.computation(), Long.MAX_VALUE, false); + } + + /** + * Returns an {@code Observable} that emits windows of items it collects from the current {@code Observable}. The resulting + * {@code Observable} emits connected, non-overlapping windows, each of a fixed duration as specified by the + * {@code timespan} argument or a maximum size as specified by the {@code count} argument (whichever is + * reached first). When the current {@code Observable} completes or encounters an error, the resulting {@code Observable} + * emits the current window and propagates the notification from the current {@code Observable}. + * <p> + * <img width="640" height="370" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/window6.v3.png" alt=""> + * <p> + * Note that ignoring windows or subscribing later (i.e., on another thread) will result in + * so-called window abandonment where a window may not contain any elements. In this case, subsequent + * elements will be dropped until the condition for the next window boundary is satisfied. The behavior is + * a trade-off for ensuring upstream cancellation can happen under some race conditions. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>This version of {@code window} operates by default on the {@code computation} {@link Scheduler}.</dd> + * </dl> + * + * @param timespan + * the period of time each window collects items before it should be emitted and replaced with a + * new window + * @param unit + * the unit of time that applies to the {@code timespan} argument + * @param count + * the maximum size of each window before it should be emitted + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code unit} is {@code null} + * @throws IllegalArgumentException if {@code count} is non-positive + * @see <a href="http://reactivex.io/documentation/operators/window.html">ReactiveX operators documentation: Window</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.COMPUTATION) + @NonNull + public final Observable<Observable<T>> window(long timespan, @NonNull TimeUnit unit, + long count) { + return window(timespan, unit, Schedulers.computation(), count, false); + } + + /** + * Returns an {@code Observable} that emits windows of items it collects from the current {@code Observable}. The resulting + * {@code Observable} emits connected, non-overlapping windows, each of a fixed duration as specified by the + * {@code timespan} argument or a maximum size as specified by the {@code count} argument (whichever is + * reached first). When the current {@code Observable} completes or encounters an error, the resulting {@code Observable} + * emits the current window and propagates the notification from the current {@code Observable}. + * <p> + * <img width="640" height="370" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/window6.v3.png" alt=""> + * <p> + * Note that ignoring windows or subscribing later (i.e., on another thread) will result in + * so-called window abandonment where a window may not contain any elements. In this case, subsequent + * elements will be dropped until the condition for the next window boundary is satisfied. The behavior is + * a trade-off for ensuring upstream cancellation can happen under some race conditions. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>This version of {@code window} operates by default on the {@code computation} {@link Scheduler}.</dd> + * </dl> + * + * @param timespan + * the period of time each window collects items before it should be emitted and replaced with a + * new window + * @param unit + * the unit of time that applies to the {@code timespan} argument + * @param count + * the maximum size of each window before it should be emitted + * @param restart + * if {@code true}, when a window reaches the capacity limit, the timer is restarted as well + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code unit} is {@code null} + * @throws IllegalArgumentException if {@code count} is non-positive + * @see <a href="http://reactivex.io/documentation/operators/window.html">ReactiveX operators documentation: Window</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.COMPUTATION) + @NonNull + public final Observable<Observable<T>> window(long timespan, @NonNull TimeUnit unit, + long count, boolean restart) { + return window(timespan, unit, Schedulers.computation(), count, restart); + } + + /** + * Returns an {@code Observable} that emits windows of items it collects from the current {@code Observable}. The resulting + * {@code Observable} emits connected, non-overlapping windows, each of a fixed duration as specified by the + * {@code timespan} argument. When the current {@code Observable} completes or encounters an error, the resulting + * {@code Observable} emits the current window and propagates the notification from the current {@code Observable}. + * <p> + * <img width="640" height="375" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/window5.s.v3.png" alt=""> + * <p> + * Note that ignoring windows or subscribing later (i.e., on another thread) will result in + * so-called window abandonment where a window may not contain any elements. In this case, subsequent + * elements will be dropped until the condition for the next window boundary is satisfied. The behavior is + * a trade-off for ensuring upstream cancellation can happen under some race conditions. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>You specify which {@link Scheduler} this operator will use.</dd> + * </dl> + * + * @param timespan + * the period of time each window collects items before it should be emitted and replaced with a + * new window + * @param unit + * the unit of time which applies to the {@code timespan} argument + * @param scheduler + * the {@code Scheduler} to use when determining the end and start of a window + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code unit} or {@code scheduler} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/window.html">ReactiveX operators documentation: Window</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.CUSTOM) + @NonNull + public final Observable<Observable<T>> window(long timespan, @NonNull TimeUnit unit, + @NonNull Scheduler scheduler) { + return window(timespan, unit, scheduler, Long.MAX_VALUE, false); + } + + /** + * Returns an {@code Observable} that emits windows of items it collects from the current {@code Observable}. The resulting + * {@code Observable} emits connected, non-overlapping windows, each of a fixed duration specified by the + * {@code timespan} argument or a maximum size specified by the {@code count} argument (whichever is reached + * first). When the current {@code Observable} completes or encounters an error, the resulting {@code Observable} emits the + * current window and propagates the notification from the current {@code Observable}. + * <p> + * <img width="640" height="370" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/window6.s.v3.png" alt=""> + * <p> + * Note that ignoring windows or subscribing later (i.e., on another thread) will result in + * so-called window abandonment where a window may not contain any elements. In this case, subsequent + * elements will be dropped until the condition for the next window boundary is satisfied. The behavior is + * a trade-off for ensuring upstream cancellation can happen under some race conditions. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>You specify which {@link Scheduler} this operator will use.</dd> + * </dl> + * + * @param timespan + * the period of time each window collects items before it should be emitted and replaced with a + * new window + * @param unit + * the unit of time which applies to the {@code timespan} argument + * @param count + * the maximum size of each window before it should be emitted + * @param scheduler + * the {@code Scheduler} to use when determining the end and start of a window + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code unit} or {@code scheduler} is {@code null} + * @throws IllegalArgumentException if {@code count} is non-positive + * @see <a href="http://reactivex.io/documentation/operators/window.html">ReactiveX operators documentation: Window</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.CUSTOM) + @NonNull + public final Observable<Observable<T>> window(long timespan, @NonNull TimeUnit unit, + @NonNull Scheduler scheduler, long count) { + return window(timespan, unit, scheduler, count, false); + } + + /** + * Returns an {@code Observable} that emits windows of items it collects from the current {@code Observable}. The resulting + * {@code Observable} emits connected, non-overlapping windows, each of a fixed duration specified by the + * {@code timespan} argument or a maximum size specified by the {@code count} argument (whichever is reached + * first). When the current {@code Observable} completes or encounters an error, the resulting {@code Observable} emits the + * current window and propagates the notification from the current {@code Observable}. + * <p> + * <img width="640" height="370" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/window6.s.v3.png" alt=""> + * <p> + * Note that ignoring windows or subscribing later (i.e., on another thread) will result in + * so-called window abandonment where a window may not contain any elements. In this case, subsequent + * elements will be dropped until the condition for the next window boundary is satisfied. The behavior is + * a trade-off for ensuring upstream cancellation can happen under some race conditions. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>You specify which {@link Scheduler} this operator will use.</dd> + * </dl> + * + * @param timespan + * the period of time each window collects items before it should be emitted and replaced with a + * new window + * @param unit + * the unit of time which applies to the {@code timespan} argument + * @param count + * the maximum size of each window before it should be emitted + * @param scheduler + * the {@code Scheduler} to use when determining the end and start of a window + * @param restart + * if {@code true}, when a window reaches the capacity limit, the timer is restarted as well + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code unit} or {@code scheduler} is {@code null} + * @throws IllegalArgumentException if {@code count} is non-positive + * @see <a href="http://reactivex.io/documentation/operators/window.html">ReactiveX operators documentation: Window</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.CUSTOM) + @NonNull + public final Observable<Observable<T>> window(long timespan, @NonNull TimeUnit unit, + @NonNull Scheduler scheduler, long count, boolean restart) { + return window(timespan, unit, scheduler, count, restart, bufferSize()); + } + + /** + * Returns an {@code Observable} that emits windows of items it collects from the current {@code Observable}. The resulting + * {@code Observable} emits connected, non-overlapping windows, each of a fixed duration specified by the + * {@code timespan} argument or a maximum size specified by the {@code count} argument (whichever is reached + * first). When the current {@code Observable} completes or encounters an error, the resulting {@code Observable} emits the + * current window and propagates the notification from the current {@code Observable}. + * <p> + * <img width="640" height="370" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/window6.s.v3.png" alt=""> + * <p> + * Note that ignoring windows or subscribing later (i.e., on another thread) will result in + * so-called window abandonment where a window may not contain any elements. In this case, subsequent + * elements will be dropped until the condition for the next window boundary is satisfied. The behavior is + * a trade-off for ensuring upstream cancellation can happen under some race conditions. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>You specify which {@link Scheduler} this operator will use.</dd> + * </dl> + * + * @param timespan + * the period of time each window collects items before it should be emitted and replaced with a + * new window + * @param unit + * the unit of time which applies to the {@code timespan} argument + * @param count + * the maximum size of each window before it should be emitted + * @param scheduler + * the {@code Scheduler} to use when determining the end and start of a window + * @param restart + * if {@code true}, when a window reaches the capacity limit, the timer is restarted as well + * @param bufferSize + * the capacity hint for the buffer in the inner windows + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code unit} or {@code scheduler} is {@code null} + * @throws IllegalArgumentException if {@code count} or {@code bufferSize} is non-positive + * @see <a href="http://reactivex.io/documentation/operators/window.html">ReactiveX operators documentation: Window</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.CUSTOM) + @NonNull + public final Observable<Observable<T>> window( + long timespan, @NonNull TimeUnit unit, @NonNull Scheduler scheduler, + long count, boolean restart, int bufferSize) { + ObjectHelper.verifyPositive(bufferSize, "bufferSize"); + Objects.requireNonNull(scheduler, "scheduler is null"); + Objects.requireNonNull(unit, "unit is null"); + ObjectHelper.verifyPositive(count, "count"); + return RxJavaPlugins.onAssembly(new ObservableWindowTimed<>(this, timespan, timespan, unit, scheduler, count, bufferSize, restart)); + } + + /** + * Returns an {@code Observable} that emits non-overlapping windows of items it collects from the current {@code Observable} + * where the boundary of each window is determined by the items emitted from a specified boundary-governing + * {@link ObservableSource}. + * <p> + * <img width="640" height="475" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/window8.v3.png" alt=""> + * <p> + * Note that ignoring windows or subscribing later (i.e., on another thread) will result in + * so-called window abandonment where a window may not contain any elements. In this case, subsequent + * elements will be dropped until the condition for the next window boundary is satisfied. The behavior is + * a trade-off for ensuring upstream cancellation can happen under some race conditions. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>This version of {@code window} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <B> + * the window element type (ignored) + * @param boundaryIndicator + * an {@code ObservableSource} whose emitted items close and open windows + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code boundaryIndicator} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/window.html">ReactiveX operators documentation: Window</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull B> Observable<Observable<T>> window(@NonNull ObservableSource<B> boundaryIndicator) { + return window(boundaryIndicator, bufferSize()); + } + + /** + * Returns an {@code Observable} that emits non-overlapping windows of items it collects from the current {@code Observable} + * where the boundary of each window is determined by the items emitted from a specified boundary-governing + * {@link ObservableSource}. + * <p> + * <img width="640" height="475" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/window8.v3.png" alt=""> + * <p> + * Note that ignoring windows or subscribing later (i.e., on another thread) will result in + * so-called window abandonment where a window may not contain any elements. In this case, subsequent + * elements will be dropped until the condition for the next window boundary is satisfied. The behavior is + * a trade-off for ensuring upstream cancellation can happen under some race conditions. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>This version of {@code window} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <B> + * the window element type (ignored) + * @param boundaryIndicator + * an {@code ObservableSource} whose emitted items close and open windows + * @param bufferSize + * the capacity hint for the buffer in the inner windows + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code boundaryIndicator} is {@code null} + * @throws IllegalArgumentException if {@code bufferSize} is non-positive + * @see <a href="http://reactivex.io/documentation/operators/window.html">ReactiveX operators documentation: Window</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull B> Observable<Observable<T>> window(@NonNull ObservableSource<B> boundaryIndicator, int bufferSize) { + Objects.requireNonNull(boundaryIndicator, "boundaryIndicator is null"); + ObjectHelper.verifyPositive(bufferSize, "bufferSize"); + return RxJavaPlugins.onAssembly(new ObservableWindowBoundary<>(this, boundaryIndicator, bufferSize)); + } + + /** + * Returns an {@code Observable} that emits windows of items it collects from the current {@code Observable}. The resulting + * {@code Observable} emits windows that contain those items emitted by the current {@code Observable} between the time when + * the {@code openingIndicator} {@link ObservableSource} emits an item and when the {@code ObservableSource} returned by + * {@code closingIndicator} emits an item. + * <p> + * <img width="640" height="550" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/window2.v3.png" alt=""> + * <p> + * Note that ignoring windows or subscribing later (i.e., on another thread) will result in + * so-called window abandonment where a window may not contain any elements. In this case, subsequent + * elements will be dropped until the condition for the next window boundary is satisfied. The behavior is + * a trade-off for ensuring upstream cancellation can happen under some race conditions. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>This version of {@code window} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <U> the element type of the window-opening {@code ObservableSource} + * @param <V> the element type of the window-closing {@code ObservableSource}s + * @param openingIndicator + * an {@code ObservableSource} that, when it emits an item, causes another window to be created + * @param closingIndicator + * a {@link Function} that produces an {@code ObservableSource} for every window created. When this indicator {@code ObservableSource} + * emits an item, the associated window is completed + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code openingIndicator} or {@code closingIndicator} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/window.html">ReactiveX operators documentation: Window</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull U, @NonNull V> Observable<Observable<T>> window( + @NonNull ObservableSource<U> openingIndicator, + @NonNull Function<? super U, ? extends ObservableSource<V>> closingIndicator) { + return window(openingIndicator, closingIndicator, bufferSize()); + } + + /** + * Returns an {@code Observable} that emits windows of items it collects from the current {@code Observable}. The resulting + * {@code Observable} emits windows that contain those items emitted by the current {@code Observable} between the time when + * the {@code openingIndicator} {@link ObservableSource} emits an item and when the {@code ObservableSource} returned by + * {@code closingIndicator} emits an item. + * <p> + * <img width="640" height="550" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/window2.v3.png" alt=""> + * <p> + * Note that ignoring windows or subscribing later (i.e., on another thread) will result in + * so-called window abandonment where a window may not contain any elements. In this case, subsequent + * elements will be dropped until the condition for the next window boundary is satisfied. The behavior is + * a trade-off for ensuring upstream cancellation can happen under some race conditions. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>This version of {@code window} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <U> the element type of the window-opening {@code ObservableSource} + * @param <V> the element type of the window-closing {@code ObservableSource}s + * @param openingIndicator + * an {@code ObservableSource} that, when it emits an item, causes another window to be created + * @param closingIndicator + * a {@link Function} that produces an {@code ObservableSource} for every window created. When this indicator {@code ObservableSource} + * emits an item, the associated window is completed + * @param bufferSize + * the capacity hint for the buffer in the inner windows + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code openingIndicator} or {@code closingIndicator} is {@code null} + * @throws IllegalArgumentException if {@code bufferSize} is non-positive + * @see <a href="http://reactivex.io/documentation/operators/window.html">ReactiveX operators documentation: Window</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull U, @NonNull V> Observable<Observable<T>> window( + @NonNull ObservableSource<U> openingIndicator, + @NonNull Function<? super U, ? extends ObservableSource<V>> closingIndicator, int bufferSize) { + Objects.requireNonNull(openingIndicator, "openingIndicator is null"); + Objects.requireNonNull(closingIndicator, "closingIndicator is null"); + ObjectHelper.verifyPositive(bufferSize, "bufferSize"); + return RxJavaPlugins.onAssembly(new ObservableWindowBoundarySelector<>(this, openingIndicator, closingIndicator, bufferSize)); + } + + /** + * Merges the specified {@link ObservableSource} into the current {@code Observable} sequence by using the {@code resultSelector} + * function only when the current {@code Observable} emits an item. + * + * <p>Note that this operator doesn't emit anything until the other source has produced at + * least one value. The resulting emission only happens when the current {@code Observable} emits (and + * not when the other source emits, unlike combineLatest). + * If the other source doesn't produce any value and just completes, the sequence is completed immediately. + * If the upstream completes before the other source has produced at least one value, the sequence completes + * without emission. + * <p> + * <img width="640" height="380" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/withLatestFrom.v3.png" alt=""> + * + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>This operator, by default, doesn't run any particular {@link Scheduler}.</dd> + * </dl> + * + * @param <U> the element type of the other {@code ObservableSource} + * @param <R> the result type of the combination + * @param other + * the other {@code ObservableSource} + * @param combiner + * the function to call when the current {@code Observable} emits an item and the other {@code ObservableSource} has already + * emitted an item, to generate the item to be emitted by the resulting {@code Observable} + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code other} or {@code combiner} is {@code null} + * @since 2.0 + * @see <a href="http://reactivex.io/documentation/operators/combinelatest.html">ReactiveX operators documentation: CombineLatest</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull U, @NonNull R> Observable<R> withLatestFrom(@NonNull ObservableSource<? extends U> other, @NonNull BiFunction<? super T, ? super U, ? extends R> combiner) { + Objects.requireNonNull(other, "other is null"); + Objects.requireNonNull(combiner, "combiner is null"); + + return RxJavaPlugins.onAssembly(new ObservableWithLatestFrom<T, U, R>(this, combiner, other)); + } + + /** + * Combines the value emission from the current {@code Observable} with the latest emissions from the + * other {@link ObservableSource}s via a function to produce the output item. + * + * <p>Note that this operator doesn't emit anything until all other sources have produced at + * least one value. The resulting emission only happens when the current {@code Observable} emits (and + * not when any of the other sources emit, unlike {@code combineLatest}). + * If a source doesn't produce any value and just completes, the sequence is completed immediately. + * If the upstream completes before all other sources have produced at least one value, the sequence completes + * without emission. + * <p> + * <img width="640" height="380" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/withLatestFrom.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>This operator does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T1> the first other source's value type + * @param <T2> the second other source's value type + * @param <R> the result value type + * @param source1 the first other {@code ObservableSource} + * @param source2 the second other {@code ObservableSource} + * @param combiner the function called with an array of values from each participating {@code ObservableSource} + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code source1}, {@code source2} or {@code combiner} is {@code null} + * @since 2.0 + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull T1, @NonNull T2, @NonNull R> Observable<R> withLatestFrom( + @NonNull ObservableSource<T1> source1, @NonNull ObservableSource<T2> source2, + @NonNull Function3<? super T, ? super T1, ? super T2, R> combiner) { + Objects.requireNonNull(source1, "source1 is null"); + Objects.requireNonNull(source2, "source2 is null"); + Objects.requireNonNull(combiner, "combiner is null"); + Function<Object[], R> f = Functions.toFunction(combiner); + return withLatestFrom(new ObservableSource[] { source1, source2 }, f); + } + + /** + * Combines the value emission from the current {@code Observable} with the latest emissions from the + * other {@link ObservableSource}s via a function to produce the output item. + * + * <p>Note that this operator doesn't emit anything until all other sources have produced at + * least one value. The resulting emission only happens when the current {@code Observable} emits (and + * not when any of the other sources emit, unlike combineLatest). + * If a source doesn't produce any value and just completes, the sequence is completed immediately. + * If the upstream completes before all other sources have produced at least one value, the sequence completes + * without emission. + * <p> + * <img width="640" height="380" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/withLatestFrom.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>This operator does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T1> the first other source's value type + * @param <T2> the second other source's value type + * @param <T3> the third other source's value type + * @param <R> the result value type + * @param source1 the first other {@code ObservableSource} + * @param source2 the second other {@code ObservableSource} + * @param source3 the third other {@code ObservableSource} + * @param combiner the function called with an array of values from each participating {@code ObservableSource} + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code source1}, {@code source2}, {@code source3} or {@code combiner} is {@code null} + * @since 2.0 + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull T1, @NonNull T2, @NonNull T3, @NonNull R> Observable<R> withLatestFrom( + @NonNull ObservableSource<T1> source1, @NonNull ObservableSource<T2> source2, + @NonNull ObservableSource<T3> source3, + @NonNull Function4<? super T, ? super T1, ? super T2, ? super T3, R> combiner) { + Objects.requireNonNull(source1, "source1 is null"); + Objects.requireNonNull(source2, "source2 is null"); + Objects.requireNonNull(source3, "source3 is null"); + Objects.requireNonNull(combiner, "combiner is null"); + Function<Object[], R> f = Functions.toFunction(combiner); + return withLatestFrom(new ObservableSource[] { source1, source2, source3 }, f); + } + + /** + * Combines the value emission from the current {@code Observable} with the latest emissions from the + * other {@link ObservableSource}s via a function to produce the output item. + * + * <p>Note that this operator doesn't emit anything until all other sources have produced at + * least one value. The resulting emission only happens when the current {@code Observable} emits (and + * not when any of the other sources emit, unlike combineLatest). + * If a source doesn't produce any value and just completes, the sequence is completed immediately. + * If the upstream completes before all other sources have produced at least one value, the sequence completes + * without emission. + * <p> + * <img width="640" height="380" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/withLatestFrom.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>This operator does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T1> the first other source's value type + * @param <T2> the second other source's value type + * @param <T3> the third other source's value type + * @param <T4> the fourth other source's value type + * @param <R> the result value type + * @param source1 the first other {@code ObservableSource} + * @param source2 the second other {@code ObservableSource} + * @param source3 the third other {@code ObservableSource} + * @param source4 the fourth other {@code ObservableSource} + * @param combiner the function called with an array of values from each participating {@code ObservableSource} + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code source1}, {@code source2}, {@code source3}, + * {@code source4} or {@code combiner} is {@code null} + * @since 2.0 + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull T1, @NonNull T2, @NonNull T3, @NonNull T4, @NonNull R> Observable<R> withLatestFrom( + @NonNull ObservableSource<T1> source1, @NonNull ObservableSource<T2> source2, + @NonNull ObservableSource<T3> source3, @NonNull ObservableSource<T4> source4, + @NonNull Function5<? super T, ? super T1, ? super T2, ? super T3, ? super T4, R> combiner) { + Objects.requireNonNull(source1, "source1 is null"); + Objects.requireNonNull(source2, "source2 is null"); + Objects.requireNonNull(source3, "source3 is null"); + Objects.requireNonNull(source4, "source4 is null"); + Objects.requireNonNull(combiner, "combiner is null"); + Function<Object[], R> f = Functions.toFunction(combiner); + return withLatestFrom(new ObservableSource[] { source1, source2, source3, source4 }, f); + } + + /** + * Combines the value emission from the current {@code Observable} with the latest emissions from the + * other {@link ObservableSource}s via a function to produce the output item. + * + * <p>Note that this operator doesn't emit anything until all other sources have produced at + * least one value. The resulting emission only happens when the current {@code Observable} emits (and + * not when any of the other sources emit, unlike combineLatest). + * If a source doesn't produce any value and just completes, the sequence is completed immediately. + * If the upstream completes before all other sources have produced at least one value, the sequence completes + * without emission. + * <p> + * <img width="640" height="380" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/withLatestFrom.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>This operator does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <R> the result value type + * @param others the array of other sources + * @param combiner the function called with an array of values from each participating {@code ObservableSource} + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code others} or {@code combiner} is {@code null} + * @since 2.0 + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull R> Observable<R> withLatestFrom(@NonNull ObservableSource<?>[] others, @NonNull Function<? super Object[], R> combiner) { + Objects.requireNonNull(others, "others is null"); + Objects.requireNonNull(combiner, "combiner is null"); + return RxJavaPlugins.onAssembly(new ObservableWithLatestFromMany<>(this, others, combiner)); + } + + /** + * Combines the value emission from the current {@code Observable} with the latest emissions from the + * other {@link ObservableSource}s via a function to produce the output item. + * + * <p>Note that this operator doesn't emit anything until all other sources have produced at + * least one value. The resulting emission only happens when the current {@code Observable} emits (and + * not when any of the other sources emit, unlike {@code combineLatest}). + * If a source doesn't produce any value and just completes, the sequence is completed immediately. + * If the upstream completes before all other sources have produced at least one value, the sequence completes + * without emission. + * <p> + * <img width="640" height="380" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/withLatestFrom.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>This operator does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <R> the result value type + * @param others the iterable of other sources + * @param combiner the function called with an array of values from each participating {@code ObservableSource} + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code others} or {@code combiner} is {@code null} + * @since 2.0 + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull R> Observable<R> withLatestFrom(@NonNull Iterable<@NonNull ? extends ObservableSource<?>> others, @NonNull Function<? super Object[], R> combiner) { + Objects.requireNonNull(others, "others is null"); + Objects.requireNonNull(combiner, "combiner is null"); + return RxJavaPlugins.onAssembly(new ObservableWithLatestFromMany<>(this, others, combiner)); + } + + /** + * Returns an {@code Observable} that emits items that are the result of applying a specified function to pairs of + * values, one each from the current {@code Observable} and a specified {@link Iterable} sequence. + * <p> + * <img width="640" height="380" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/zip.i.v3.png" alt=""> + * <p> + * Note that the {@code other} {@code Iterable} is evaluated as items are observed from the current {@code Observable}; it is + * not pre-consumed. This allows you to zip infinite streams on either side. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code zipWith} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <U> + * the type of items in the {@code other} {@code Iterable} + * @param <R> + * the type of items emitted by the resulting {@code Observable} + * @param other + * the {@code Iterable} sequence + * @param zipper + * a function that combines the pairs of items from the current {@code Observable} and the {@code Iterable} to generate + * the items to be emitted by the resulting {@code Observable} + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code other} or {@code zipper} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/zip.html">ReactiveX operators documentation: Zip</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull U, @NonNull R> Observable<R> zipWith(@NonNull Iterable<U> other, @NonNull BiFunction<? super T, ? super U, ? extends R> zipper) { + Objects.requireNonNull(other, "other is null"); + Objects.requireNonNull(zipper, "zipper is null"); + return RxJavaPlugins.onAssembly(new ObservableZipIterable<>(this, other, zipper)); + } + + /** + * Returns an {@code Observable} that emits items that are the result of applying a specified function to pairs of + * values, one each from the current {@code Observable} and another specified {@link ObservableSource}. + * <p> + * <img width="640" height="380" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/zip.v3.png" alt=""> + * <p> + * The operator subscribes to its sources in order they are specified and completes eagerly if + * one of the sources is shorter than the rest while disposing the other sources. Therefore, it + * is possible those other sources will never be able to run to completion (and thus not calling + * {@code doOnComplete()}). This can also happen if the sources are exactly the same length; if + * source A completes and B has been consumed and is about to complete, the operator detects A won't + * be sending further values and it will dispose B immediately. For example: + * <pre><code>range(1, 5).doOnComplete(action1).zipWith(range(6, 5).doOnComplete(action2), (a, b) -> a + b)</code></pre> + * {@code action1} will be called but {@code action2} won't. + * <br>To work around this termination property, + * use {@link #doOnDispose(Action)} as well or use {@code using()} to do cleanup in case of completion + * or a dispose() call. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code zipWith} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <U> + * the type of items emitted by the {@code other} {@code ObservableSource} + * @param <R> + * the type of items emitted by the resulting {@code Observable} + * @param other + * the other {@code ObservableSource} + * @param zipper + * a function that combines the pairs of items from the current {@code Observable} and the other {@code ObservableSource} to generate the items to + * be emitted by the resulting {@code Observable} + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code other} or {@code zipper} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/zip.html">ReactiveX operators documentation: Zip</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull U, @NonNull R> Observable<R> zipWith(@NonNull ObservableSource<? extends U> other, + @NonNull BiFunction<? super T, ? super U, ? extends R> zipper) { + Objects.requireNonNull(other, "other is null"); + return zip(this, other, zipper); + } + + /** + * Returns an {@code Observable} that emits items that are the result of applying a specified function to pairs of + * values, one each from the current {@code Observable} and another specified {@link ObservableSource}. + * <p> + * <img width="640" height="380" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/zip.v3.png" alt=""> + * <p> + * The operator subscribes to its sources in order they are specified and completes eagerly if + * one of the sources is shorter than the rest while disposing the other sources. Therefore, it + * is possible those other sources will never be able to run to completion (and thus not calling + * {@code doOnComplete()}). This can also happen if the sources are exactly the same length; if + * source A completes and B has been consumed and is about to complete, the operator detects A won't + * be sending further values and it will dispose B immediately. For example: + * <pre><code>range(1, 5).doOnComplete(action1).zipWith(range(6, 5).doOnComplete(action2), (a, b) -> a + b)</code></pre> + * {@code action1} will be called but {@code action2} won't. + * <br>To work around this termination property, + * use {@link #doOnDispose(Action)} as well or use {@code using()} to do cleanup in case of completion + * or a dispose() call. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code zipWith} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <U> + * the type of items emitted by the {@code other} {@code ObservableSource} + * @param <R> + * the type of items emitted by the resulting {@code Observable} + * @param other + * the other {@code ObservableSource} + * @param zipper + * a function that combines the pairs of items from the current {@code Observable} and the other {@code ObservableSource} to generate the items to + * be emitted by the resulting {@code Observable} + * @param delayError + * if {@code true}, errors from the current {@code Observable} or the other {@code ObservableSource} is delayed until both terminate + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code other} or {@code zipper} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/zip.html">ReactiveX operators documentation: Zip</a> + * @since 2.0 + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull U, @NonNull R> Observable<R> zipWith(@NonNull ObservableSource<? extends U> other, + @NonNull BiFunction<? super T, ? super U, ? extends R> zipper, boolean delayError) { + return zip(this, other, zipper, delayError); + } + + /** + * Returns an {@code Observable} that emits items that are the result of applying a specified function to pairs of + * values, one each from the current {@code Observable} and another specified {@link ObservableSource}. + * <p> + * <img width="640" height="380" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/zip.v3.png" alt=""> + * <p> + * The operator subscribes to its sources in order they are specified and completes eagerly if + * one of the sources is shorter than the rest while disposing the other sources. Therefore, it + * is possible those other sources will never be able to run to completion (and thus not calling + * {@code doOnComplete()}). This can also happen if the sources are exactly the same length; if + * source A completes and B has been consumed and is about to complete, the operator detects A won't + * be sending further values and it will dispose B immediately. For example: + * <pre><code>range(1, 5).doOnComplete(action1).zipWith(range(6, 5).doOnComplete(action2), (a, b) -> a + b)</code></pre> + * {@code action1} will be called but {@code action2} won't. + * <br>To work around this termination property, + * use {@link #doOnDispose(Action)} as well or use {@code using()} to do cleanup in case of completion + * or a dispose() call. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code zipWith} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <U> + * the type of items emitted by the {@code other} {@code ObservableSource} + * @param <R> + * the type of items emitted by the resulting {@code Observable} + * @param other + * the other {@code ObservableSource} + * @param zipper + * a function that combines the pairs of items from the current {@code Observable} and the other {@code ObservableSource} to generate the items to + * be emitted by the resulting {@code Observable} + * @param bufferSize + * the capacity hint for the buffer in the inner windows + * @param delayError + * if {@code true}, errors from the current {@code Observable} or the other {@code ObservableSource} is delayed until both terminate + * @return the new {@code Observable} instance + * @see <a href="http://reactivex.io/documentation/operators/zip.html">ReactiveX operators documentation: Zip</a> + * @throws NullPointerException if {@code other} or {@code zipper} is {@code null} + * @throws IllegalArgumentException if {@code bufferSize} is non-positive + * @since 2.0 + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull U, @NonNull R> Observable<R> zipWith(@NonNull ObservableSource<? extends U> other, + @NonNull BiFunction<? super T, ? super U, ? extends R> zipper, boolean delayError, int bufferSize) { + return zip(this, other, zipper, delayError, bufferSize); + } + + // ------------------------------------------------------------------------- + // Fluent test support, super handy and reduces test preparation boilerplate + // ------------------------------------------------------------------------- + /** + * Creates a {@link TestObserver} and subscribes it to the current {@code Observable}. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code test} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @return the new {@code TestObserver} instance + * @since 2.0 + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final TestObserver<T> test() { // NoPMD + TestObserver<T> to = new TestObserver<>(); + subscribe(to); + return to; + } + + /** + * Creates a {@link TestObserver}, optionally disposes it and then subscribes + * it to the current {@code Observable}. + * + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code test} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param dispose indicates if the {@code TestObserver} should be disposed before + * it is subscribed to the current {@code Observable} + * @return the new {@code TestObserver} instance + * @since 2.0 + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final TestObserver<T> test(boolean dispose) { // NoPMD + TestObserver<T> to = new TestObserver<>(); + if (dispose) { + to.dispose(); + } + subscribe(to); + return to; + } + + // ------------------------------------------------------------------------- + // JDK 8 Support + // ------------------------------------------------------------------------- + + /** + * Converts the existing value of the provided optional into a {@link #just(Object)} + * or an empty optional into an {@link #empty()} {@code Observable} instance. + * <p> + * <img width="640" height="335" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/fromOptional.o.png" alt=""> + * <p> + * Note that the operator takes an already instantiated optional reference and does not + * by any means create this original optional. If the optional is to be created per + * consumer upon subscription, use {@link #defer(Supplier)} around {@code fromOptional}: + * <pre><code> + * Observable.defer(() -> Observable.fromOptional(createOptional())); + * </code></pre> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code fromOptional} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <T> the element type of the optional value + * @param optional the optional value to convert into an {@code Observable} + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code optional} is {@code null} + * @since 3.0.0 + * @see #just(Object) + * @see #empty() + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public static <@NonNull T> Observable<@NonNull T> fromOptional(@NonNull Optional<T> optional) { + Objects.requireNonNull(optional, "optional is null"); + return optional.map(Observable::just).orElseGet(Observable::empty); + } + + /** + * Signals the completion value or error of the given (hot) {@link CompletionStage}-based asynchronous calculation. + * <p> + * <img width="640" height="262" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/fromCompletionStage.o.png" alt=""> + * <p> + * Note that the operator takes an already instantiated, running or terminated {@code CompletionStage}. + * If the {@code CompletionStage} is to be created per consumer upon subscription, use {@link #defer(Supplier)} + * around {@code fromCompletionStage}: + * <pre><code> + * Observable.defer(() -> Observable.fromCompletionStage(createCompletionStage())); + * </code></pre> + * <p> + * If the {@code CompletionStage} completes with {@code null}, a {@link NullPointerException} is signaled. + * <p> + * Canceling the flow can't cancel the execution of the {@code CompletionStage} because {@code CompletionStage} + * itself doesn't support cancellation. Instead, the operator detaches from the {@code CompletionStage}. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code fromCompletionStage} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <T> the element type of the {@code CompletionStage} + * @param stage the {@code CompletionStage} to convert to {@code Observable} and signal its terminal value or error + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code stage} is {@code null} + * @since 3.0.0 + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public static <@NonNull T> Observable<@NonNull T> fromCompletionStage(@NonNull CompletionStage<T> stage) { + Objects.requireNonNull(stage, "stage is null"); + return RxJavaPlugins.onAssembly(new ObservableFromCompletionStage<>(stage)); + } + + /** + * Converts a {@link Stream} into a finite {@code Observable} and emits its items in the sequence. + * <p> + * <img width="640" height="407" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/fromStream.o.png" alt=""> + * <p> + * The operator closes the {@code Stream} upon cancellation and when it terminates. The exceptions raised when + * closing a {@code Stream} are routed to the global error handler ({@link RxJavaPlugins#onError(Throwable)}. + * If a {@code Stream} should not be closed, turn it into an {@link Iterable} and use {@link #fromIterable(Iterable)}: + * <pre><code> + * Stream<T> stream = ... + * Observable.fromIterable(stream::iterator); + * </code></pre> + * <p> + * Note that {@code Stream}s can be consumed only once; any subsequent attempt to consume a {@code Stream} + * will result in an {@link IllegalStateException}. + * <p> + * Primitive streams are not supported and items have to be boxed manually (e.g., via {@link IntStream#boxed()}): + * <pre><code> + * IntStream intStream = IntStream.rangeClosed(1, 10); + * Observable.fromStream(intStream.boxed()); + * </code></pre> + * <p> + * {@code Stream} does not support concurrent usage so creating and/or consuming the same instance multiple times + * from multiple threads can lead to undefined behavior. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code fromStream} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <T> the element type of the source {@code Stream} + * @param stream the {@code Stream} of values to emit + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code stream} is {@code null} + * @since 3.0.0 + * @see #fromIterable(Iterable) + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public static <@NonNull T> Observable<@NonNull T> fromStream(@NonNull Stream<T> stream) { + Objects.requireNonNull(stream, "stream is null"); + return RxJavaPlugins.onAssembly(new ObservableFromStream<>(stream)); + } + + /** + * Maps each upstream value into an {@link Optional} and emits the contained item if not empty. + * <p> + * <img width="640" height="306" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/mapOptional.o.png" alt=""> + * + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code mapOptional} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <R> the non-{@code null} output type + * @param mapper the function that receives the upstream item and should return a <em>non-empty</em> {@code Optional} + * to emit as the output or an <em>empty</em> {@code Optional} to skip to the next upstream value + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code mapper} is {@code null} + * @since 3.0.0 + * @see #map(Function) + * @see #filter(Predicate) + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull R> Observable<R> mapOptional(@NonNull Function<? super T, @NonNull Optional<? extends R>> mapper) { + Objects.requireNonNull(mapper, "mapper is null"); + return RxJavaPlugins.onAssembly(new ObservableMapOptional<>(this, mapper)); + } + + /** + * Collects the finite upstream's values into a container via a {@link Stream} {@link Collector} callback set and emits + * it as the success result as a {@link Single}. + * <p> + * <img width="640" height="358" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/collector.o.png" alt=""> + * + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code collect} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <R> the non-{@code null} result type + * @param <A> the intermediate container type used for the accumulation + * @param collector the interface defining the container supplier, accumulator and finisher functions; + * see {@link Collectors} for some standard implementations + * @return the new {@code Single} instance + * @throws NullPointerException if {@code collector} is {@code null} + * @since 3.0.0 + * @see Collectors + * @see #collect(Supplier, BiConsumer) + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull R, @Nullable A> Single<R> collect(@NonNull Collector<? super T, A, R> collector) { + Objects.requireNonNull(collector, "collector is null"); + return RxJavaPlugins.onAssembly(new ObservableCollectWithCollectorSingle<>(this, collector)); + } + + /** + * Signals the first upstream item (or the default item if the upstream is empty) via + * a {@link CompletionStage}. + * <p> + * <img width="640" height="313" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/firstStage.o.png" alt=""> + * <p> + * The upstream can be canceled by converting the resulting {@code CompletionStage} into + * {@link CompletableFuture} via {@link CompletionStage#toCompletableFuture()} and + * calling {@link CompletableFuture#cancel(boolean)} on it. + * The upstream will be also cancelled if the resulting {@code CompletionStage} is converted to and + * completed manually by {@link CompletableFuture#complete(Object)} or {@link CompletableFuture#completeExceptionally(Throwable)}. + * <p> + * {@code CompletionStage}s don't have a notion of emptiness and allow {@code null}s, therefore, one can either use + * a {@code defaultItem} of {@code null} or turn the flow into a sequence of {@link Optional}s and default to {@link Optional#empty()}: + * <pre><code> + * CompletionStage<Optional<T>> stage = source.map(Optional::of).firstStage(Optional.empty()); + * </code></pre> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code firstStage} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param defaultItem the item to signal if the upstream is empty + * @return the new {@code CompletionStage} instance + * @throws NullPointerException if {@code defaultItem} is {@code null} + * @since 3.0.0 + * @see #firstOrErrorStage() + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final CompletionStage<T> firstStage(@Nullable T defaultItem) { + return subscribeWith(new ObservableFirstStageObserver<>(true, defaultItem)); + } + + /** + * Signals the only expected upstream item (or the default item if the upstream is empty) + * or signals {@link IllegalArgumentException} if the upstream has more than one item + * via a {@link CompletionStage}. + * <p> + * <img width="640" height="227" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/singleStage.o.png" alt=""> + * <p> + * The upstream can be canceled by converting the resulting {@code CompletionStage} into + * {@link CompletableFuture} via {@link CompletionStage#toCompletableFuture()} and + * calling {@link CompletableFuture#cancel(boolean)} on it. + * The upstream will be also cancelled if the resulting {@code CompletionStage} is converted to and + * completed manually by {@link CompletableFuture#complete(Object)} or {@link CompletableFuture#completeExceptionally(Throwable)}. + * <p> + * {@code CompletionStage}s don't have a notion of emptiness and allow {@code null}s, therefore, one can either use + * a {@code defaultItem} of {@code null} or turn the flow into a sequence of {@link Optional}s and default to {@link Optional#empty()}: + * <pre><code> + * CompletionStage<Optional<T>> stage = source.map(Optional::of).singleStage(Optional.empty()); + * </code></pre> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code singleStage} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param defaultItem the item to signal if the upstream is empty + * @return the new {@code CompletionStage} instance + * @throws NullPointerException if {@code defaultItem} is {@code null} + * @since 3.0.0 + * @see #singleOrErrorStage() + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final CompletionStage<T> singleStage(@Nullable T defaultItem) { + return subscribeWith(new ObservableSingleStageObserver<>(true, defaultItem)); + } + + /** + * Signals the last upstream item (or the default item if the upstream is empty) via + * a {@link CompletionStage}. + * <p> + * <img width="640" height="313" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/lastStage.o.png" alt=""> + * <p> + * The upstream can be canceled by converting the resulting {@code CompletionStage} into + * {@link CompletableFuture} via {@link CompletionStage#toCompletableFuture()} and + * calling {@link CompletableFuture#cancel(boolean)} on it. + * The upstream will be also cancelled if the resulting {@code CompletionStage} is converted to and + * completed manually by {@link CompletableFuture#complete(Object)} or {@link CompletableFuture#completeExceptionally(Throwable)}. + * <p> + * {@code CompletionStage}s don't have a notion of emptiness and allow {@code null}s, therefore, one can either use + * a {@code defaultItem} of {@code null} or turn the flow into a sequence of {@link Optional}s and default to {@link Optional#empty()}: + * <pre><code> + * CompletionStage<Optional<T>> stage = source.map(Optional::of).lastStage(Optional.empty()); + * </code></pre> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code lastStage} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param defaultItem the item to signal if the upstream is empty + * @return the new {@code CompletionStage} instance + * @throws NullPointerException if {@code defaultItem} is {@code null} + * @since 3.0.0 + * @see #lastOrErrorStage() + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final CompletionStage<T> lastStage(@Nullable T defaultItem) { + return subscribeWith(new ObservableLastStageObserver<>(true, defaultItem)); + } + + /** + * Signals the first upstream item or a {@link NoSuchElementException} if the upstream is empty via + * a {@link CompletionStage}. + * <p> + * <img width="640" height="341" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/firstOrErrorStage.o.png" alt=""> + * <p> + * The upstream can be canceled by converting the resulting {@code CompletionStage} into + * {@link CompletableFuture} via {@link CompletionStage#toCompletableFuture()} and + * calling {@link CompletableFuture#cancel(boolean)} on it. + * The upstream will be also cancelled if the resulting {@code CompletionStage} is converted to and + * completed manually by {@link CompletableFuture#complete(Object)} or {@link CompletableFuture#completeExceptionally(Throwable)}. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code firstOrErrorStage} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @return the new {@code CompletionStage} instance + * @since 3.0.0 + * @see #firstStage(Object) + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final CompletionStage<T> firstOrErrorStage() { + return subscribeWith(new ObservableFirstStageObserver<>(false, null)); + } + + /** + * Signals the only expected upstream item, a {@link NoSuchElementException} if the upstream is empty + * or signals {@link IllegalArgumentException} if the upstream has more than one item + * via a {@link CompletionStage}. + * <p> + * <img width="640" height="227" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/singleOrErrorStage.o.png" alt=""> + * <p> + * The upstream can be canceled by converting the resulting {@code CompletionStage} into + * {@link CompletableFuture} via {@link CompletionStage#toCompletableFuture()} and + * calling {@link CompletableFuture#cancel(boolean)} on it. + * The upstream will be also cancelled if the resulting {@code CompletionStage} is converted to and + * completed manually by {@link CompletableFuture#complete(Object)} or {@link CompletableFuture#completeExceptionally(Throwable)}. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code singleOrErrorStage} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @return the new {@code CompletionStage} instance + * @since 3.0.0 + * @see #singleStage(Object) + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final CompletionStage<T> singleOrErrorStage() { + return subscribeWith(new ObservableSingleStageObserver<>(false, null)); + } + + /** + * Signals the last upstream item or a {@link NoSuchElementException} if the upstream is empty via + * a {@link CompletionStage}. + * <p> + * <img width="640" height="343" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/lastOrErrorStage.o.png" alt=""> + * <p> + * The upstream can be canceled by converting the resulting {@code CompletionStage} into + * {@link CompletableFuture} via {@link CompletionStage#toCompletableFuture()} and + * calling {@link CompletableFuture#cancel(boolean)} on it. + * The upstream will be also cancelled if the resulting {@code CompletionStage} is converted to and + * completed manually by {@link CompletableFuture#complete(Object)} or {@link CompletableFuture#completeExceptionally(Throwable)}. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code lastOrErrorStage} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @return the new {@code CompletionStage} instance + * @since 3.0.0 + * @see #lastStage(Object) + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final CompletionStage<T> lastOrErrorStage() { + return subscribeWith(new ObservableLastStageObserver<>(false, null)); + } + + /** + * Creates a sequential {@link Stream} to consume or process the current {@code Observable} in a blocking manner via + * the Java {@code Stream} API. + * <p> + * <img width="640" height="399" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/blockingStream.o.png" alt=""> + * <p> + * Cancellation of the upstream is done via {@link Stream#close()}, therefore, it is strongly recommended the + * consumption is performed within a try-with-resources construct: + * <pre><code> + * Observable<Integer> source = Observable.range(1, 10) + * .subscribeOn(Schedulers.computation()); + * + * try (Stream<Integer> stream = source.blockingStream()) { + * stream.limit(3).forEach(System.out::println); + * } + * </code></pre> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code blockingStream} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @return the new {@code Stream} instance + * @since 3.0.0 + * @see #blockingStream(int) + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Stream<T> blockingStream() { + return blockingStream(bufferSize()); + } + + /** + * Creates a sequential {@link Stream} to consume or process the current {@code Observable} in a blocking manner via + * the Java {@code Stream} API. + * <p> + * <img width="640" height="399" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/blockingStream.oi.png" alt=""> + * <p> + * Cancellation of the upstream is done via {@link Stream#close()}, therefore, it is strongly recommended the + * consumption is performed within a try-with-resources construct: + * <pre><code> + * Observable<Integer> source = Observable.range(1, 10) + * .subscribeOn(Schedulers.computation()); + * + * try (Stream<Integer> stream = source.blockingStream(4)) { + * stream.limit(3).forEach(System.out::println); + * } + * </code></pre> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code blockingStream} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param capacityHint the expected number of items to be buffered + * @return the new {@code Stream} instance + * @throws IllegalArgumentException if {@code capacityHint} is non-positive + * @since 3.0.0 + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Stream<T> blockingStream(int capacityHint) { + Iterator<T> iterator = blockingIterable(capacityHint).iterator(); + return StreamSupport.stream(Spliterators.spliteratorUnknownSize(iterator, 0), false) + .onClose(((Disposable) iterator)::dispose); + } + + /** + * Maps each upstream item into a {@link Stream} and emits the {@code Stream}'s items to the downstream in a sequential fashion. + * <p> + * <img width="640" height="299" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/concatMapStream.o.png" alt=""> + * <p> + * Due to the blocking and sequential nature of Java {@code Stream}s, the streams are mapped and consumed in a sequential fashion + * without interleaving (unlike a more general {@link #flatMap(Function)}). Therefore, {@code flatMapStream} and + * {@code concatMapStream} are identical operators and are provided as aliases. + * <p> + * The operator closes the {@code Stream} upon cancellation and when it terminates. The exceptions raised when + * closing a {@code Stream} are routed to the global error handler ({@link RxJavaPlugins#onError(Throwable)}. + * If a {@code Stream} should not be closed, turn it into an {@link Iterable} and use {@link #concatMapIterable(Function)}: + * <pre><code> + * source.concatMapIterable(v -> createStream(v)::iterator); + * </code></pre> + * <p> + * Note that {@code Stream}s can be consumed only once; any subsequent attempt to consume a {@code Stream} + * will result in an {@link IllegalStateException}. + * <p> + * Primitive streams are not supported and items have to be boxed manually (e.g., via {@link IntStream#boxed()}): + * <pre><code> + * source.concatMapStream(v -> IntStream.rangeClosed(v + 1, v + 10).boxed()); + * </code></pre> + * <p> + * {@code Stream} does not support concurrent usage so creating and/or consuming the same instance multiple times + * from multiple threads can lead to undefined behavior. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code concatMapStream} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <R> the element type of the {@code Stream}s and the result + * @param mapper the function that receives an upstream item and should return a {@code Stream} whose elements + * will be emitted to the downstream + * @return the new {@code Observable} instance + * @since 3.0.0 + * @throws NullPointerException if {@code mapper} is {@code null} + * @see #concatMap(Function) + * @see #concatMapIterable(Function) + * @see #flatMapStream(Function) + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull R> Observable<R> concatMapStream(@NonNull Function<? super T, @NonNull ? extends Stream<? extends R>> mapper) { + return flatMapStream(mapper); + } + + /** + * Maps each upstream item into a {@link Stream} and emits the {@code Stream}'s items to the downstream in a sequential fashion. + * <p> + * <img width="640" height="299" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/flatMapStream.o.png" alt=""> + * <p> + * Due to the blocking and sequential nature of Java {@code Stream}s, the streams are mapped and consumed in a sequential fashion + * without interleaving (unlike a more general {@link #flatMap(Function)}). Therefore, {@code flatMapStream} and + * {@code concatMapStream} are identical operators and are provided as aliases. + * <p> + * The operator closes the {@code Stream} upon cancellation and when it terminates. The exceptions raised when + * closing a {@code Stream} are routed to the global error handler ({@link RxJavaPlugins#onError(Throwable)}. + * If a {@code Stream} should not be closed, turn it into an {@link Iterable} and use {@link #flatMapIterable(Function)}: + * <pre><code> + * source.flatMapIterable(v -> createStream(v)::iterator); + * </code></pre> + * <p> + * Note that {@code Stream}s can be consumed only once; any subsequent attempt to consume a {@code Stream} + * will result in an {@link IllegalStateException}. + * <p> + * Primitive streams are not supported and items have to be boxed manually (e.g., via {@link IntStream#boxed()}): + * <pre><code> + * source.flatMapStream(v -> IntStream.rangeClosed(v + 1, v + 10).boxed()); + * </code></pre> + * <p> + * {@code Stream} does not support concurrent usage so creating and/or consuming the same instance multiple times + * from multiple threads can lead to undefined behavior. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code flatMapStream} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <R> the element type of the {@code Stream}s and the result + * @param mapper the function that receives an upstream item and should return a {@code Stream} whose elements + * will be emitted to the downstream + * @return the new {@code Observable} instance + * @since 3.0.0 + * @throws NullPointerException if {@code mapper} is {@code null} + * @see #flatMap(Function) + * @see #flatMapIterable(Function) + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull R> Observable<R> flatMapStream(@NonNull Function<? super T, @NonNull ? extends Stream<? extends R>> mapper) { + Objects.requireNonNull(mapper, "mapper is null"); + return RxJavaPlugins.onAssembly(new ObservableFlatMapStream<>(this, mapper)); + } +} diff --git a/src/main/java/io/reactivex/rxjava3/core/ObservableConverter.java b/src/main/java/io/reactivex/rxjava3/core/ObservableConverter.java new file mode 100644 index 0000000000..24cb2c9fad --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/core/ObservableConverter.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.core; + +import io.reactivex.rxjava3.annotations.NonNull; + +/** + * Convenience interface and callback used by the {@link Observable#to} operator to turn an {@link Observable} into another + * value fluently. + * <p>History: 2.1.7 - experimental + * @param <T> the upstream type + * @param <R> the output type + * @since 2.2 + */ +@FunctionalInterface +public interface ObservableConverter<@NonNull T, @NonNull R> { + /** + * Applies a function to the upstream {@link Observable} and returns a converted value of type {@code R}. + * + * @param upstream the upstream {@code Observable} instance + * @return the converted value + */ + R apply(@NonNull Observable<T> upstream); +} diff --git a/src/main/java/io/reactivex/rxjava3/core/ObservableEmitter.java b/src/main/java/io/reactivex/rxjava3/core/ObservableEmitter.java new file mode 100644 index 0000000000..0658c4ba81 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/core/ObservableEmitter.java @@ -0,0 +1,98 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.core; + +import io.reactivex.rxjava3.annotations.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.functions.*; + +/** + * Abstraction over an RxJava {@link Observer} that allows associating + * a resource with it. + * <p> + * The {@link #onNext(Object)}, {@link #onError(Throwable)}, {@link #tryOnError(Throwable)} + * and {@link #onComplete()} methods should be called in a sequential manner, just like the + * {@link Observer}'s methods should be. + * Use the {@code ObservableEmitter} the {@link #serialize()} method returns instead of the original + * {@code ObservableEmitter} instance provided by the generator routine if you want to ensure this. + * The other methods are thread-safe. + * <p> + * The emitter allows the registration of a single resource, in the form of a {@link Disposable} + * or {@link Cancellable} via {@link #setDisposable(Disposable)} or {@link #setCancellable(Cancellable)} + * respectively. The emitter implementations will dispose/cancel this instance when the + * downstream cancels the flow or after the event generator logic calls {@link #onError(Throwable)}, + * {@link #onComplete()} or when {@link #tryOnError(Throwable)} succeeds. + * <p> + * Only one {@code Disposable} or {@code Cancellable} object can be associated with the emitter at + * a time. Calling either {@code set} method will dispose/cancel any previous object. If there + * is a need for handling multiple resources, one can create a {@link io.reactivex.rxjava3.disposables.CompositeDisposable} + * and associate that with the emitter instead. + * <p> + * The {@link Cancellable} is logically equivalent to {@code Disposable} but allows using cleanup logic that can + * throw a checked exception (such as many {@code close()} methods on Java IO components). Since + * the release of resources happens after the terminal events have been delivered or the sequence gets + * cancelled, exceptions throw within {@code Cancellable} are routed to the global error handler via + * {@link io.reactivex.rxjava3.plugins.RxJavaPlugins#onError(Throwable)}. + * + * @param <T> the value type to emit + */ +public interface ObservableEmitter<@NonNull T> extends Emitter<T> { + + /** + * Sets a {@link Disposable} on this emitter; any previous {@code Disposable} + * or {@link Cancellable} will be disposed/cancelled. + * <p>This method is thread-safe. + * @param d the {@code Disposable}, {@code null} is allowed + */ + void setDisposable(@Nullable Disposable d); + + /** + * Sets a {@link Cancellable} on this emitter; any previous {@link Disposable} + * or {@code Cancellable} will be disposed/cancelled. + * <p>This method is thread-safe. + * @param c the {@code Cancellable} resource, {@code null} is allowed + */ + void setCancellable(@Nullable Cancellable c); + + /** + * Returns true if the downstream disposed the sequence or the + * emitter was terminated via {@link #onError(Throwable)}, {@link #onComplete} or a + * successful {@link #tryOnError(Throwable)}. + * <p>This method is thread-safe. + * @return true if the downstream disposed the sequence or the emitter was terminated + */ + boolean isDisposed(); + + /** + * Ensures that calls to {@code onNext}, {@code onError} and {@code onComplete} are properly serialized. + * @return the serialized {@link ObservableEmitter} + */ + @NonNull + ObservableEmitter<T> serialize(); + + /** + * Attempts to emit the specified {@link Throwable} error if the downstream + * hasn't cancelled the sequence or is otherwise terminated, returning false + * if the emission is not allowed to happen due to lifecycle restrictions. + * <p> + * Unlike {@link #onError(Throwable)}, the {@link io.reactivex.rxjava3.plugins.RxJavaPlugins#onError(Throwable) RxjavaPlugins.onError} + * is not called if the error could not be delivered. + * <p>History: 2.1.1 - experimental + * @param t the {@code Throwable} error to signal if possible + * @return true if successful, false if the downstream is not able to accept further + * events + * @since 2.2 + */ + boolean tryOnError(@NonNull Throwable t); +} diff --git a/src/main/java/io/reactivex/rxjava3/core/ObservableOnSubscribe.java b/src/main/java/io/reactivex/rxjava3/core/ObservableOnSubscribe.java new file mode 100644 index 0000000000..056441620a --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/core/ObservableOnSubscribe.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.core; + +import io.reactivex.rxjava3.annotations.NonNull; + +/** + * A functional interface that has a {@code subscribe()} method that receives + * an {@link ObservableEmitter} instance that allows pushing + * events in a cancellation-safe manner. + * + * @param <T> the value type pushed + */ +@FunctionalInterface +public interface ObservableOnSubscribe<@NonNull T> { + + /** + * Called for each {@link Observer} that subscribes. + * @param emitter the safe emitter instance, never {@code null} + * @throws Throwable on error + */ + void subscribe(@NonNull ObservableEmitter<T> emitter) throws Throwable; +} + diff --git a/src/main/java/io/reactivex/rxjava3/core/ObservableOperator.java b/src/main/java/io/reactivex/rxjava3/core/ObservableOperator.java new file mode 100644 index 0000000000..fd697fa626 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/core/ObservableOperator.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.core; + +import io.reactivex.rxjava3.annotations.NonNull; + +/** + * Interface to map/wrap a downstream {@link Observer} to an upstream {@code Observer}. + * + * @param <Downstream> the value type of the downstream + * @param <Upstream> the value type of the upstream + */ +@FunctionalInterface +public interface ObservableOperator<@NonNull Downstream, @NonNull Upstream> { + /** + * Applies a function to the child {@link Observer} and returns a new parent {@code Observer}. + * @param observer the child {@code Observer} instance + * @return the parent {@code Observer} instance + * @throws Throwable on failure + */ + @NonNull + Observer<? super Upstream> apply(@NonNull Observer<? super Downstream> observer) throws Throwable; +} diff --git a/src/main/java/io/reactivex/rxjava3/core/ObservableSource.java b/src/main/java/io/reactivex/rxjava3/core/ObservableSource.java new file mode 100644 index 0000000000..c00bfc2170 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/core/ObservableSource.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.core; + +import io.reactivex.rxjava3.annotations.NonNull; + +/** + * Represents a basic, non-backpressured {@link Observable} source base interface, + * consumable via an {@link Observer}. + * + * @param <T> the element type + * @since 2.0 + */ +@FunctionalInterface +public interface ObservableSource<@NonNull T> { + + /** + * Subscribes the given {@link Observer} to this {@link ObservableSource} instance. + * @param observer the {@code Observer}, not {@code null} + * @throws NullPointerException if {@code observer} is {@code null} + */ + void subscribe(@NonNull Observer<? super T> observer); +} diff --git a/src/main/java/io/reactivex/rxjava3/core/ObservableTransformer.java b/src/main/java/io/reactivex/rxjava3/core/ObservableTransformer.java new file mode 100644 index 0000000000..57b32bdc71 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/core/ObservableTransformer.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.core; + +import io.reactivex.rxjava3.annotations.NonNull; + +/** + * Interface to compose {@link Observable}s. + * + * @param <Upstream> the upstream value type + * @param <Downstream> the downstream value type + */ +@FunctionalInterface +public interface ObservableTransformer<@NonNull Upstream, @NonNull Downstream> { + /** + * Applies a function to the upstream {@link Observable} and returns an {@link ObservableSource} with + * optionally different element type. + * @param upstream the upstream {@code Observable} instance + * @return the transformed {@code ObservableSource} instance + */ + @NonNull + ObservableSource<Downstream> apply(@NonNull Observable<Upstream> upstream); +} diff --git a/src/main/java/io/reactivex/rxjava3/core/Observer.java b/src/main/java/io/reactivex/rxjava3/core/Observer.java new file mode 100644 index 0000000000..6b911f51e5 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/core/Observer.java @@ -0,0 +1,119 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.core; + +import io.reactivex.rxjava3.annotations.NonNull; +import io.reactivex.rxjava3.disposables.Disposable; + +/** + * Provides a mechanism for receiving push-based notifications. + * <p> + * When an {@code Observer} is subscribed to an {@link ObservableSource} through the {@link ObservableSource#subscribe(Observer)} method, + * the {@code ObservableSource} calls {@link #onSubscribe(Disposable)} with a {@link Disposable} that allows + * disposing the sequence at any time, then the + * {@code ObservableSource} may call the Observer's {@link #onNext} method any number of times + * to provide notifications. A well-behaved + * {@code ObservableSource} will call an {@code Observer}'s {@link #onComplete} method exactly once or the {@code Observer}'s + * {@link #onError} method exactly once. + * <p> + * Calling the {@code Observer}'s method must happen in a serialized fashion, that is, they must not + * be invoked concurrently by multiple threads in an overlapping fashion and the invocation pattern must + * adhere to the following protocol: + * <pre><code> onSubscribe onNext* (onError | onComplete)?</code></pre> + * <p> + * Subscribing an {@code Observer} to multiple {@code ObservableSource}s is not recommended. If such reuse + * happens, it is the duty of the {@code Observer} implementation to be ready to receive multiple calls to + * its methods and ensure proper concurrent behavior of its business logic. + * <p> + * Calling {@link #onSubscribe(Disposable)}, {@link #onNext(Object)} or {@link #onError(Throwable)} with a + * {@code null} argument is forbidden. + * <p> + * The implementations of the {@code onXXX} methods should avoid throwing runtime exceptions other than the following cases + * (see <a href="https://github.com/reactive-streams/reactive-streams-jvm#2.13">Rule 2.13</a> of the Reactive Streams specification): + * <ul> + * <li>If the argument is {@code null}, the methods can throw a {@code NullPointerException}. + * Note though that RxJava prevents {@code null}s to enter into the flow and thus there is generally no + * need to check for nulls in flows assembled from standard sources and intermediate operators. + * </li> + * <li>If there is a fatal error (such as {@code VirtualMachineError}).</li> + * </ul> + * <p> + * Violating Rule 2.13 results in undefined flow behavior. Generally, the following can happen: + * <ul> + * <li>An upstream operator turns it into an {@link #onError} call.</li> + * <li>If the flow is synchronous, the {@link ObservableSource#subscribe(Observer)} throws instead of returning normally.</li> + * <li>If the flow is asynchronous, the exception propagates up to the component ({@link Scheduler} or {@link java.util.concurrent.Executor}) + * providing the asynchronous boundary the code is running and either routes the exception to the global + * {@link io.reactivex.rxjava3.plugins.RxJavaPlugins#onError(Throwable)} handler or the current thread's + * {@link java.lang.Thread.UncaughtExceptionHandler#uncaughtException(Thread, Throwable)} handler.</li> + * </ul> + * From the {@code Observable}'s perspective, an {@code Observer} is the end consumer thus it is the {@code Observer}'s + * responsibility to handle the error case and signal it "further down". This means unreliable code in the {@code onXXX} + * methods should be wrapped into `try-catch`es, specifically in {@link #onError(Throwable)} or {@link #onComplete()}, and handled there + * (for example, by logging it or presenting the user with an error dialog). However, if the error would be thrown from + * {@link #onNext(Object)}, <a href="https://github.com/reactive-streams/reactive-streams-jvm#2.13">Rule 2.13</a> mandates + * the implementation calls {@link Disposable#dispose()} and signals the exception in a way that is adequate to the target context, + * for example, by calling {@link #onError(Throwable)} on the same {@code Observer} instance. + * <p> + * If, for some reason, the {@code Observer} won't follow Rule 2.13, the {@link Observable#safeSubscribe(Observer)} can wrap it + * with the necessary safeguards and route exceptions thrown from {@code onNext} into {@code onError} and route exceptions thrown + * from {@code onError} and {@code onComplete} into the global error handler via {@link io.reactivex.rxjava3.plugins.RxJavaPlugins#onError(Throwable)}. + * @see <a href="http://reactivex.io/documentation/observable.html">ReactiveX documentation: Observable</a> + * @param <T> + * the type of item the Observer expects to observe + */ +public interface Observer<@NonNull T> { + + /** + * Provides the {@link Observer} with the means of cancelling (disposing) the + * connection (channel) with the {@link Observable} in both + * synchronous (from within {@link #onNext(Object)}) and asynchronous manner. + * @param d the {@link Disposable} instance whose {@link Disposable#dispose()} can + * be called anytime to cancel the connection + * @since 2.0 + */ + void onSubscribe(@NonNull Disposable d); + + /** + * Provides the {@link Observer} with a new item to observe. + * <p> + * The {@link Observable} may call this method 0 or more times. + * <p> + * The {@code Observable} will not call this method again after it calls either {@link #onComplete} or + * {@link #onError}. + * + * @param t + * the item emitted by the Observable + */ + void onNext(@NonNull T t); + + /** + * Notifies the {@link Observer} that the {@link Observable} has experienced an error condition. + * <p> + * If the {@code Observable} calls this method, it will not thereafter call {@link #onNext} or + * {@link #onComplete}. + * + * @param e + * the exception encountered by the Observable + */ + void onError(@NonNull Throwable e); + + /** + * Notifies the {@link Observer} that the {@link Observable} has finished sending push-based notifications. + * <p> + * The {@code Observable} will not call this method if it calls {@link #onError}. + */ + void onComplete(); + +} diff --git a/src/main/java/io/reactivex/rxjava3/core/Scheduler.java b/src/main/java/io/reactivex/rxjava3/core/Scheduler.java new file mode 100644 index 0000000000..3aa001127a --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/core/Scheduler.java @@ -0,0 +1,675 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.core; + +import java.util.Objects; +import java.util.concurrent.TimeUnit; + +import io.reactivex.rxjava3.annotations.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.disposables.*; +import io.reactivex.rxjava3.internal.schedulers.*; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.schedulers.SchedulerRunnableIntrospection; + +/** + * A {@code Scheduler} is an object that specifies an API for scheduling + * units of work provided in the form of {@link Runnable}s to be + * executed without delay (effectively as soon as possible), after a specified time delay or periodically + * and represents an abstraction over an asynchronous boundary that ensures + * these units of work get executed by some underlying task-execution scheme + * (such as custom Threads, event loop, {@link java.util.concurrent.Executor Executor} or Actor system) + * with some uniform properties and guarantees regardless of the particular underlying + * scheme. + * <p> + * You can get various standard, RxJava-specific instances of this class via + * the static methods of the {@link io.reactivex.rxjava3.schedulers.Schedulers} utility class. + * <p> + * The so-called {@link Worker}s of a {@code Scheduler} can be created via the {@link #createWorker()} method which allow the scheduling + * of multiple {@link Runnable} tasks in an isolated manner. {@code Runnable} tasks scheduled on a {@code Worker} are guaranteed to be + * executed sequentially and in a non-overlapping fashion. Non-delayed {@code Runnable} tasks are guaranteed to execute in a + * First-In-First-Out order but their execution may be interleaved with delayed tasks. + * In addition, outstanding or running tasks can be cancelled together via + * {@link Worker#dispose()} without affecting any other {@code Worker} instances of the same {@code Scheduler}. + * <p> + * Implementations of the {@link #scheduleDirect} and {@link Worker#schedule} methods are encouraged to call the {@link io.reactivex.rxjava3.plugins.RxJavaPlugins#onSchedule(Runnable)} + * method to allow a scheduler hook to manipulate (wrap or replace) the original {@code Runnable} task before it is submitted to the + * underlying task-execution scheme. + * <p> + * The default implementations of the {@code scheduleDirect} methods provided by this abstract class + * delegate to the respective {@code schedule} methods in the {@link Worker} instance created via {@link #createWorker()} + * for each individual {@link Runnable} task submitted. Implementors of this class are encouraged to provide + * a more efficient direct scheduling implementation to avoid the time and memory overhead of creating such {@code Worker}s + * for every task. + * This delegation is done via special wrapper instances around the original {@code Runnable} before calling the respective + * {@code Worker.schedule} method. Note that this can lead to multiple {@code RxJavaPlugins.onSchedule} calls and potentially + * multiple hooks applied. Therefore, the default implementations of {@code scheduleDirect} (and the {@link Worker#schedulePeriodically(Runnable, long, long, TimeUnit)}) + * wrap the incoming {@code Runnable} into a class that implements the {@link io.reactivex.rxjava3.schedulers.SchedulerRunnableIntrospection} + * interface which can grant access to the original or hooked {@code Runnable}, thus, a repeated {@code RxJavaPlugins.onSchedule} + * can detect the earlier hook and not apply a new one over again. + * <p> + * The default implementation of {@link #now(TimeUnit)} and {@link Worker#now(TimeUnit)} methods to return current {@link System#currentTimeMillis()} + * value in the desired time unit, unless {@code rx3.scheduler.use-nanotime} (boolean) is set. When the property is set to + * {@code true}, the method uses {@link System#nanoTime()} as its basis instead. Custom {@code Scheduler} implementations can override this + * to provide specialized time accounting (such as virtual time to be advanced programmatically). + * Note that operators requiring a {@code Scheduler} may rely on either of the {@code now()} calls provided by + * {@code Scheduler} or {@code Worker} respectively, therefore, it is recommended they represent a logically + * consistent source of the current time. + * <p> + * The default implementation of the {@link Worker#schedulePeriodically(Runnable, long, long, TimeUnit)} method uses + * the {@link Worker#schedule(Runnable, long, TimeUnit)} for scheduling the {@code Runnable} task periodically. + * The algorithm calculates the next absolute time when the task should run again and schedules this execution + * based on the relative time between it and {@link Worker#now(TimeUnit)}. However, drifts or changes in the + * system clock could affect this calculation either by scheduling subsequent runs too frequently or too far apart. + * Therefore, the default implementation uses the {@link #clockDriftTolerance()} value (set via + * {@code rx3.scheduler.drift-tolerance} and {@code rx3.scheduler.drift-tolerance-unit}) to detect a + * drift in {@link Worker#now(TimeUnit)} and re-adjust the absolute/relative time calculation accordingly. + * <p> + * The default implementations of {@link #start()} and {@link #shutdown()} do nothing and should be overridden if the + * underlying task-execution scheme supports stopping and restarting itself. + * <p> + * If the {@code Scheduler} is shut down or a {@code Worker} is disposed, the {@code schedule} methods + * should return the {@link Disposable#disposed()} singleton instance indicating the shut down/disposed + * state to the caller. Since the shutdown or dispose can happen from any thread, the {@code schedule} implementations + * should make best effort to cancel tasks immediately after those tasks have been submitted to the + * underlying task-execution scheme if the shutdown/dispose was detected after this submission. + * <p> + * All methods on the {@code Scheduler} and {@code Worker} classes should be thread safe. + */ +public abstract class Scheduler { + /** + * Value representing whether to use {@link System#nanoTime()}, or default as clock for {@link #now(TimeUnit)} + * and {@link Scheduler.Worker#now(TimeUnit)}. + * <p> + * Associated system parameter: + * <ul> + * <li>{@code rx3.scheduler.use-nanotime}, boolean, default {@code false} + * </ul> + */ + static boolean IS_DRIFT_USE_NANOTIME = Boolean.getBoolean("rx3.scheduler.use-nanotime"); + + /** + * Returns the current clock time depending on state of {@link Scheduler#IS_DRIFT_USE_NANOTIME} in given {@code unit} + * <p> + * By default {@link System#currentTimeMillis()} will be used as the clock. When the property is set + * {@link System#nanoTime()} will be used. + * <p> + * @param unit the time unit + * @return the 'current time' in given unit + * @throws NullPointerException if {@code unit} is {@code null} + */ + static long computeNow(TimeUnit unit) { + if (!IS_DRIFT_USE_NANOTIME) { + return unit.convert(System.currentTimeMillis(), TimeUnit.MILLISECONDS); + } + return unit.convert(System.nanoTime(), TimeUnit.NANOSECONDS); + } + + /** + * The tolerance for a clock drift in nanoseconds where the periodic scheduler will rebase. + * <p> + * Associated system parameters: + * <ul> + * <li>{@code rx3.scheduler.drift-tolerance}, long, default {@code 15}</li> + * <li>{@code rx3.scheduler.drift-tolerance-unit}, string, default {@code minutes}, + * supports {@code seconds} and {@code milliseconds}. + * </ul> + */ + static final long CLOCK_DRIFT_TOLERANCE_NANOSECONDS = + computeClockDrift( + Long.getLong("rx3.scheduler.drift-tolerance", 15), + System.getProperty("rx3.scheduler.drift-tolerance-unit", "minutes") + ); + + /** + * Returns the clock drift tolerance in nanoseconds based on the input selection. + * @param time the time value + * @param timeUnit the time unit string + * @return the time amount in nanoseconds + */ + static long computeClockDrift(long time, String timeUnit) { + if ("seconds".equalsIgnoreCase(timeUnit)) { + return TimeUnit.SECONDS.toNanos(time); + } else if ("milliseconds".equalsIgnoreCase(timeUnit)) { + return TimeUnit.MILLISECONDS.toNanos(time); + } + return TimeUnit.MINUTES.toNanos(time); + } + + /** + * Returns the clock drift tolerance in nanoseconds. + * <p>Related system properties: + * <ul> + * <li>{@code rx3.scheduler.drift-tolerance}, long, default {@code 15}</li> + * <li>{@code rx3.scheduler.drift-tolerance-unit}, string, default {@code minutes}, + * supports {@code seconds} and {@code milliseconds}. + * </ul> + * @return the tolerance in nanoseconds + * @since 2.0 + */ + public static long clockDriftTolerance() { + return CLOCK_DRIFT_TOLERANCE_NANOSECONDS; + } + + /** + * Retrieves or creates a new {@link Scheduler.Worker} that represents sequential execution of actions. + * <p> + * When work is completed, the {@code Worker} instance should be released + * by calling {@link Scheduler.Worker#dispose()} to avoid potential resource leaks in the + * underlying task-execution scheme. + * <p> + * Work on a {@link Scheduler.Worker} is guaranteed to be sequential and non-overlapping. + * + * @return a Worker representing a serial queue of actions to be executed + */ + @NonNull + public abstract Worker createWorker(); + + /** + * Returns the 'current time' of the Scheduler in the specified time unit. + * @param unit the time unit + * @return the 'current time' + * @throws NullPointerException if {@code unit} is {@code null} + * @since 2.0 + */ + public long now(@NonNull TimeUnit unit) { + return computeNow(unit); + } + + /** + * Allows the Scheduler instance to start threads + * and accept tasks on them. + * <p> + * Implementations should make sure the call is idempotent, thread-safe and + * should not throw any {@code RuntimeException} if it doesn't support this + * functionality. + * + * @since 2.0 + */ + public void start() { + + } + + /** + * Instructs the Scheduler instance to stop threads, + * stop accepting tasks on any outstanding {@link Worker} instances + * and clean up any associated resources with this Scheduler. + * <p> + * Implementations should make sure the call is idempotent, thread-safe and + * should not throw any {@code RuntimeException} if it doesn't support this + * functionality. + * @since 2.0 + */ + public void shutdown() { + + } + + /** + * Schedules the given task on this Scheduler without any time delay. + * + * <p> + * This method is safe to be called from multiple threads but there are no + * ordering or non-overlapping guarantees between tasks. + * + * @param run the task to execute + * + * @return the Disposable instance that let's one cancel this particular task. + * @throws NullPointerException if {@code run} is {@code null} + * @since 2.0 + */ + @NonNull + public Disposable scheduleDirect(@NonNull Runnable run) { + return scheduleDirect(run, 0L, TimeUnit.NANOSECONDS); + } + + /** + * Schedules the execution of the given task with the given time delay. + * + * <p> + * This method is safe to be called from multiple threads but there are no + * ordering guarantees between tasks. + * + * @param run the task to schedule + * @param delay the delay amount, non-positive values indicate non-delayed scheduling + * @param unit the unit of measure of the delay amount + * @return the Disposable that let's one cancel this particular delayed task. + * @throws NullPointerException if {@code run} or {@code unit} is {@code null} + * @since 2.0 + */ + @NonNull + public Disposable scheduleDirect(@NonNull Runnable run, long delay, @NonNull TimeUnit unit) { + final Worker w = createWorker(); + + final Runnable decoratedRun = RxJavaPlugins.onSchedule(run); + + DisposeTask task = new DisposeTask(decoratedRun, w); + + w.schedule(task, delay, unit); + + return task; + } + + /** + * Schedules a periodic execution of the given task with the given initial time delay and repeat period. + * + * <p> + * This method is safe to be called from multiple threads but there are no + * ordering guarantees between tasks. + * + * <p> + * The periodic execution is at a fixed rate, that is, the first execution will be after the + * {@code initialDelay}, the second after {@code initialDelay + period}, the third after + * {@code initialDelay + 2 * period}, and so on. + * + * @param run the task to schedule + * @param initialDelay the initial delay amount, non-positive values indicate non-delayed scheduling + * @param period the period at which the task should be re-executed + * @param unit the unit of measure of the delay amount + * @return the Disposable that let's one cancel this particular delayed task. + * @throws NullPointerException if {@code run} or {@code unit} is {@code null} + * @since 2.0 + */ + @NonNull + public Disposable schedulePeriodicallyDirect(@NonNull Runnable run, long initialDelay, long period, @NonNull TimeUnit unit) { + final Worker w = createWorker(); + + final Runnable decoratedRun = RxJavaPlugins.onSchedule(run); + + PeriodicDirectTask periodicTask = new PeriodicDirectTask(decoratedRun, w); + + Disposable d = w.schedulePeriodically(periodicTask, initialDelay, period, unit); + if (d == EmptyDisposable.INSTANCE) { + return d; + } + + return periodicTask; + } + + /** + * Allows the use of operators for controlling the timing around when + * actions scheduled on workers are actually done. This makes it possible to + * layer additional behavior on this {@link Scheduler}. The only parameter + * is a function that flattens an {@link Flowable} of {@link Flowable} + * of {@link Completable}s into just one {@link Completable}. There must be + * a chain of operators connecting the returned value to the source + * {@link Flowable} otherwise any work scheduled on the returned + * {@link Scheduler} will not be executed. + * <p> + * When {@link Scheduler#createWorker()} is invoked a {@link Flowable} of + * {@link Completable}s is onNext'd to the combinator to be flattened. If + * the inner {@link Flowable} is not immediately subscribed to an calls to + * {@link Worker#schedule} are buffered. Once the {@link Flowable} is + * subscribed to actions are then onNext'd as {@link Completable}s. + * <p> + * Finally the actions scheduled on the parent {@link Scheduler} when the + * inner most {@link Completable}s are subscribed to. + * <p> + * When the {@link Worker} is unsubscribed the {@link Completable} emits an + * onComplete and triggers any behavior in the flattening operator. The + * {@link Flowable} and all {@link Completable}s give to the flattening + * function never onError. + * <p> + * Limit the amount concurrency two at a time without creating a new fix + * size thread pool: + * + * <pre> + * Scheduler limitScheduler = Schedulers.computation().when(workers -> { + * // use merge max concurrent to limit the number of concurrent + * // callbacks two at a time + * return Completable.merge(Flowable.merge(workers), 2); + * }); + * </pre> + * <p> + * This is a slightly different way to limit the concurrency but it has some + * interesting benefits and drawbacks to the method above. It works by + * limited the number of concurrent {@link Worker}s rather than individual + * actions. Generally each {@link Flowable} uses its own {@link Worker}. + * This means that this will essentially limit the number of concurrent + * subscribes. The danger comes from using operators like + * {@link Flowable#zip(org.reactivestreams.Publisher, org.reactivestreams.Publisher, io.reactivex.rxjava3.functions.BiFunction)} where + * subscribing to the first {@link Flowable} could deadlock the + * subscription to the second. + * + * <pre> + * Scheduler limitScheduler = Schedulers.computation().when(workers -> { + * // use merge max concurrent to limit the number of concurrent + * // Flowables two at a time + * return Completable.merge(Flowable.merge(workers, 2)); + * }); + * </pre> + * + * Slowing down the rate to no more than 1 a second. This suffers from + * the same problem as the one above I could find an {@link Flowable} + * operator that limits the rate without dropping the values (aka leaky + * bucket algorithm). + * + * <pre> + * Scheduler slowScheduler = Schedulers.computation().when(workers -> { + * // use concatenate to make each worker happen one at a time. + * return Completable.concat(workers.map(actions -> { + * // delay the starting of the next worker by 1 second. + * return Completable.merge(actions.delaySubscription(1, TimeUnit.SECONDS)); + * })); + * }); + * </pre> + * + * <p>History: 2.0.1 - experimental + * @param <S> a Scheduler and a Subscription + * @param combine the function that takes a two-level nested Flowable sequence of a Completable and returns + * the Completable that will be subscribed to and should trigger the execution of the scheduled Actions. + * @return the Scheduler with the customized execution behavior + * @throws NullPointerException if {@code combine} is {@code null} + * @since 2.1 + */ + @SuppressWarnings("unchecked") + @NonNull + public <S extends Scheduler & Disposable> S when(@NonNull Function<Flowable<Flowable<Completable>>, Completable> combine) { + Objects.requireNonNull(combine, "combine is null"); + return (S) new SchedulerWhen(combine, this); + } + + /** + * Represents an isolated, sequential worker of a parent Scheduler for executing {@code Runnable} tasks on + * an underlying task-execution scheme (such as custom Threads, event loop, {@link java.util.concurrent.Executor Executor} or Actor system). + * <p> + * Disposing the {@link Worker} should cancel all outstanding work and allows resource cleanup. + * <p> + * The default implementations of {@link #schedule(Runnable)} and {@link #schedulePeriodically(Runnable, long, long, TimeUnit)} + * delegate to the abstract {@link #schedule(Runnable, long, TimeUnit)} method. Its implementation is encouraged to + * track the individual {@code Runnable} tasks while they are waiting to be executed (with or without delay) so that + * {@link #dispose()} can prevent their execution or potentially interrupt them if they are currently running. + * <p> + * The default implementation of the {@link #now(TimeUnit)} method returns current {@link System#currentTimeMillis()} + * value in the desired time unit, unless {@code rx3.scheduler.use-nanotime} (boolean) is set. When the property is set to + * {@code true}, the method uses {@link System#nanoTime()} as its basis instead. Custom {@code Worker} implementations can override this + * to provide specialized time accounting (such as virtual time to be advanced programmatically). + * Note that operators requiring a scheduler may rely on either of the {@code now()} calls provided by + * {@code Scheduler} or {@code Worker} respectively, therefore, it is recommended they represent a logically + * consistent source of the current time. + * <p> + * The default implementation of the {@link #schedulePeriodically(Runnable, long, long, TimeUnit)} method uses + * the {@link #schedule(Runnable, long, TimeUnit)} for scheduling the {@code Runnable} task periodically. + * The algorithm calculates the next absolute time when the task should run again and schedules this execution + * based on the relative time between it and {@link #now(TimeUnit)}. However, drifts or changes in the + * system clock would affect this calculation either by scheduling subsequent runs too frequently or too far apart. + * Therefore, the default implementation uses the {@link #clockDriftTolerance()} value (set via + * {@code rx3.scheduler.drift-tolerance} and {@code rx3.scheduler.drift-tolerance-unit}) to detect a drift in {@link #now(TimeUnit)} and + * re-adjust the absolute/relative time calculation accordingly. + * <p> + * If the {@code Worker} is disposed, the {@code schedule} methods + * should return the {@link Disposable#disposed()} singleton instance indicating the disposed + * state to the caller. Since the {@link #dispose()} call can happen on any thread, the {@code schedule} implementations + * should make best effort to cancel tasks immediately after those tasks have been submitted to the + * underlying task-execution scheme if the dispose was detected after this submission. + * <p> + * All methods on the {@code Worker} class should be thread safe. + */ + public abstract static class Worker implements Disposable { + /** + * Schedules a Runnable for execution without any time delay. + * + * <p>The default implementation delegates to {@link #schedule(Runnable, long, TimeUnit)}. + * + * @param run + * Runnable to schedule + * @return a Disposable to be able to unsubscribe the action (cancel it if not executed) + * @throws NullPointerException if {@code run} is {@code null} + */ + @NonNull + public Disposable schedule(@NonNull Runnable run) { + return schedule(run, 0L, TimeUnit.NANOSECONDS); + } + + /** + * Schedules an Runnable for execution at some point in the future specified by a time delay + * relative to the current time. + * <p> + * Note to implementors: non-positive {@code delayTime} should be regarded as non-delayed schedule, i.e., + * as if the {@link #schedule(Runnable)} was called. + * + * @param run + * the Runnable to schedule + * @param delay + * time to "wait" before executing the action; non-positive values indicate an non-delayed + * schedule + * @param unit + * the time unit of {@code delayTime} + * @return a Disposable to be able to unsubscribe the action (cancel it if not executed) + * @throws NullPointerException if {@code run} or {@code unit} is {@code null} + */ + @NonNull + public abstract Disposable schedule(@NonNull Runnable run, long delay, @NonNull TimeUnit unit); + + /** + * Schedules a periodic execution of the given task with the given initial time delay and repeat period. + * <p> + * The default implementation schedules and reschedules the {@code Runnable} task via the + * {@link #schedule(Runnable, long, TimeUnit)} + * method over and over and at a fixed rate, that is, the first execution will be after the + * {@code initialDelay}, the second after {@code initialDelay + period}, the third after + * {@code initialDelay + 2 * period}, and so on. + * <p> + * Note to implementors: non-positive {@code initialTime} and {@code period} should be regarded as + * non-delayed scheduling of the first and any subsequent executions. + * In addition, a more specific {@code Worker} implementation should override this method + * if it can perform the periodic task execution with less overhead (such as by avoiding the + * creation of the wrapper and tracker objects upon each periodic invocation of the + * common {@link #schedule(Runnable, long, TimeUnit)} method). + * + * @param run + * the Runnable to execute periodically + * @param initialDelay + * time to wait before executing the action for the first time; non-positive values indicate + * an non-delayed schedule + * @param period + * the time interval to wait each time in between executing the action; non-positive values + * indicate no delay between repeated schedules + * @param unit + * the time unit of {@code period} + * @return a Disposable to be able to unsubscribe the action (cancel it if not executed) + * @throws NullPointerException if {@code run} or {@code unit} is {@code null} + */ + @NonNull + public Disposable schedulePeriodically(@NonNull Runnable run, final long initialDelay, final long period, @NonNull final TimeUnit unit) { + final SequentialDisposable first = new SequentialDisposable(); + + final SequentialDisposable sd = new SequentialDisposable(first); + + final Runnable decoratedRun = RxJavaPlugins.onSchedule(run); + + final long periodInNanoseconds = unit.toNanos(period); + final long firstNowNanoseconds = now(TimeUnit.NANOSECONDS); + final long firstStartInNanoseconds = firstNowNanoseconds + unit.toNanos(initialDelay); + + Disposable d = schedule(new PeriodicTask(firstStartInNanoseconds, decoratedRun, firstNowNanoseconds, sd, + periodInNanoseconds), initialDelay, unit); + + if (d == EmptyDisposable.INSTANCE) { + return d; + } + first.replace(d); + + return sd; + } + + /** + * Returns the 'current time' of the Worker in the specified time unit. + * @param unit the time unit + * @return the 'current time' + * @throws NullPointerException if {@code unit} is {@code null} + * @since 2.0 + */ + public long now(@NonNull TimeUnit unit) { + return computeNow(unit); + } + + /** + * Holds state and logic to calculate when the next delayed invocation + * of this task has to happen (accounting for clock drifts). + */ + final class PeriodicTask implements Runnable, SchedulerRunnableIntrospection { + @NonNull + final Runnable decoratedRun; + @NonNull + final SequentialDisposable sd; + final long periodInNanoseconds; + long count; + long lastNowNanoseconds; + long startInNanoseconds; + + PeriodicTask(long firstStartInNanoseconds, @NonNull Runnable decoratedRun, + long firstNowNanoseconds, @NonNull SequentialDisposable sd, long periodInNanoseconds) { + this.decoratedRun = decoratedRun; + this.sd = sd; + this.periodInNanoseconds = periodInNanoseconds; + lastNowNanoseconds = firstNowNanoseconds; + startInNanoseconds = firstStartInNanoseconds; + } + + @Override + public void run() { + decoratedRun.run(); + + if (!sd.isDisposed()) { + + long nextTick; + + long nowNanoseconds = now(TimeUnit.NANOSECONDS); + // If the clock moved in a direction quite a bit, rebase the repetition period + if (nowNanoseconds + CLOCK_DRIFT_TOLERANCE_NANOSECONDS < lastNowNanoseconds + || nowNanoseconds >= lastNowNanoseconds + periodInNanoseconds + CLOCK_DRIFT_TOLERANCE_NANOSECONDS) { + nextTick = nowNanoseconds + periodInNanoseconds; + /* + * Shift the start point back by the drift as if the whole thing + * started count periods ago. + */ + startInNanoseconds = nextTick - (periodInNanoseconds * (++count)); + } else { + nextTick = startInNanoseconds + (++count * periodInNanoseconds); + } + lastNowNanoseconds = nowNanoseconds; + + long delay = nextTick - nowNanoseconds; + sd.replace(schedule(this, delay, TimeUnit.NANOSECONDS)); + } + } + + @Override + public Runnable getWrappedRunnable() { + return this.decoratedRun; + } + } + } + + static final class PeriodicDirectTask + implements Disposable, Runnable, SchedulerRunnableIntrospection { + + @NonNull + final Runnable run; + + @NonNull + final Worker worker; + + volatile boolean disposed; + + PeriodicDirectTask(@NonNull Runnable run, @NonNull Worker worker) { + this.run = run; + this.worker = worker; + } + + @Override + public void run() { + if (!disposed) { + try { + run.run(); + } catch (Throwable ex) { + // Exceptions.throwIfFatal(ex); nowhere to go + dispose(); + RxJavaPlugins.onError(ex); + throw ex; + } + } + } + + @Override + public void dispose() { + disposed = true; + worker.dispose(); + } + + @Override + public boolean isDisposed() { + return disposed; + } + + @Override + public Runnable getWrappedRunnable() { + return run; + } + } + + static final class DisposeTask implements Disposable, Runnable, SchedulerRunnableIntrospection { + + @NonNull + final Runnable decoratedRun; + + @NonNull + final Worker w; + + @Nullable + Thread runner; + + DisposeTask(@NonNull Runnable decoratedRun, @NonNull Worker w) { + this.decoratedRun = decoratedRun; + this.w = w; + } + + @Override + public void run() { + runner = Thread.currentThread(); + try { + try { + decoratedRun.run(); + } catch (Throwable ex) { + // Exceptions.throwIfFatal(e); nowhere to go + RxJavaPlugins.onError(ex); + throw ex; + } + } finally { + dispose(); + runner = null; + } + } + + @Override + public void dispose() { + if (runner == Thread.currentThread() && w instanceof NewThreadWorker) { + ((NewThreadWorker)w).shutdown(); + } else { + w.dispose(); + } + } + + @Override + public boolean isDisposed() { + return w.isDisposed(); + } + + @Override + public Runnable getWrappedRunnable() { + return this.decoratedRun; + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/core/Single.java b/src/main/java/io/reactivex/rxjava3/core/Single.java new file mode 100644 index 0000000000..6cf5a3f789 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/core/Single.java @@ -0,0 +1,5764 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.core; + +import java.util.*; +import java.util.concurrent.*; +import java.util.stream.*; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.annotations.*; +import io.reactivex.rxjava3.disposables.*; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.functions.*; +import io.reactivex.rxjava3.internal.fuseable.*; +import io.reactivex.rxjava3.internal.jdk8.*; +import io.reactivex.rxjava3.internal.observers.*; +import io.reactivex.rxjava3.internal.operators.completable.*; +import io.reactivex.rxjava3.internal.operators.flowable.*; +import io.reactivex.rxjava3.internal.operators.maybe.*; +import io.reactivex.rxjava3.internal.operators.mixed.*; +import io.reactivex.rxjava3.internal.operators.observable.ObservableSingleSingle; +import io.reactivex.rxjava3.internal.operators.single.*; +import io.reactivex.rxjava3.internal.util.ErrorMode; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.schedulers.*; + +/** + * The {@code Single} class implements the Reactive Pattern for a single value response. + * <p> + * {@code Single} behaves similarly to {@link Observable} except that it can only emit either a single successful + * value or an error (there is no {@code onComplete} notification as there is for an {@code Observable}). + * <p> + * The {@code Single} class implements the {@link SingleSource} base interface and the default consumer + * type it interacts with is the {@link SingleObserver} via the {@link #subscribe(SingleObserver)} method. + * <p> + * The {@code Single} operates with the following sequential protocol: + * <pre> + * <code>onSubscribe (onSuccess | onError)?</code> + * </pre> + * <p> + * Note that {@code onSuccess} and {@code onError} are mutually exclusive events; unlike {@code Observable}, + * {@code onSuccess} is never followed by {@code onError}. + * <p> + * Like {@code Observable}, a running {@code Single} can be stopped through the {@link Disposable} instance + * provided to consumers through {@link SingleObserver#onSubscribe}. + * <p> + * Like an {@code Observable}, a {@code Single} is lazy, can be either "hot" or "cold", synchronous or + * asynchronous. {@code Single} instances returned by the methods of this class are <em>cold</em> + * and there is a standard <em>hot</em> implementation in the form of a subject: + * {@link io.reactivex.rxjava3.subjects.SingleSubject SingleSubject}. + * <p> + * The documentation for this class makes use of marble diagrams. The following legend explains these diagrams: + * <p> + * <img width="640" height="301" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.legend.v3.png" alt=""> + * <p> + * See {@link Flowable} or {@code Observable} for the + * implementation of the Reactive Pattern for a stream or vector of values. + * <p> + * For more information see the <a href="http://reactivex.io/documentation/single.html">ReactiveX + * documentation</a>. + * <p> + * Example: + * <pre><code> + * Disposable d = Single.just("Hello World") + * .delay(10, TimeUnit.SECONDS, Schedulers.io()) + * .subscribeWith(new DisposableSingleObserver<String>() { + * @Override + * public void onStart() { + * System.out.println("Started"); + * } + * + * @Override + * public void onSuccess(String value) { + * System.out.println("Success: " + value); + * } + * + * @Override + * public void onError(Throwable error) { + * error.printStackTrace(); + * } + * }); + * + * Thread.sleep(5000); + * + * d.dispose(); + * </code></pre> + * <p> + * Note that by design, subscriptions via {@link #subscribe(SingleObserver)} can't be disposed + * from the outside (hence the + * {@code void} return of the {@link #subscribe(SingleObserver)} method) and it is the + * responsibility of the implementor of the {@code SingleObserver} to allow this to happen. + * RxJava supports such usage with the standard + * {@link io.reactivex.rxjava3.observers.DisposableSingleObserver DisposableSingleObserver} instance. + * For convenience, the {@link #subscribeWith(SingleObserver)} method is provided as well to + * allow working with a {@code SingleObserver} (or subclass) instance to be applied with in + * a fluent manner (such as in the example above). + * @param <T> + * the type of the item emitted by the {@code Single} + * @since 2.0 + * @see io.reactivex.rxjava3.observers.DisposableSingleObserver + */ +public abstract class Single<@NonNull T> implements SingleSource<T> { + + /** + * Runs multiple {@link SingleSource}s and signals the events of the first one that signals (disposing + * the rest). + * <p> + * <img width="640" height="516" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.amb.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code amb} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <T> the value type + * @param sources the {@link Iterable} sequence of sources. A subscription to each source will + * occur in the same order as in this {@code Iterable}. + * @return the new {@code Single} instance + * @throws NullPointerException if {@code sources} is {@code null} + * @since 2.0 + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T> Single<T> amb(@NonNull Iterable<@NonNull ? extends SingleSource<? extends T>> sources) { + Objects.requireNonNull(sources, "sources is null"); + return RxJavaPlugins.onAssembly(new SingleAmb<>(null, sources)); + } + + /** + * Runs multiple {@link SingleSource}s and signals the events of the first one that signals (disposing + * the rest). + * <p> + * <img width="640" height="516" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.ambArray.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code ambArray} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <T> the value type + * @param sources the array of sources. A subscription to each source will + * occur in the same order as in this array. + * @return the new {@code Single} instance + * @throws NullPointerException if {@code sources} is {@code null} + * @since 2.0 + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @SafeVarargs + @NonNull + public static <@NonNull T> Single<T> ambArray(@NonNull SingleSource<? extends T>... sources) { + Objects.requireNonNull(sources, "sources is null"); + if (sources.length == 0) { + return error(SingleInternalHelper.emptyThrower()); + } + if (sources.length == 1) { + @SuppressWarnings("unchecked") + SingleSource<T> source = (SingleSource<T>)sources[0]; + return wrap(source); + } + return RxJavaPlugins.onAssembly(new SingleAmb<>(sources, null)); + } + + /** + * Concatenate the single values, in a non-overlapping fashion, of the {@link SingleSource}s provided by + * an {@link Iterable} sequence. + * <p> + * <img width="640" height="319" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.concat.i.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The returned {@link Flowable} honors the backpressure of the downstream consumer.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code concat} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <T> the value type + * @param sources the {@code Iterable} sequence of {@code SingleSource} instances + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code sources} is {@code null} + * @since 2.0 + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + @BackpressureSupport(BackpressureKind.FULL) + public static <@NonNull T> Flowable<T> concat(@NonNull Iterable<@NonNull ? extends SingleSource<? extends T>> sources) { + return Flowable.fromIterable(sources).concatMapSingleDelayError(Functions.identity(), false); + } + + /** + * Concatenate the single values, in a non-overlapping fashion, of the {@link SingleSource}s provided by + * an {@link ObservableSource} sequence. + * <p> + * <img width="640" height="319" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.concat.o.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code concat} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <T> the value type + * @param sources the {@code ObservableSource} of {@code SingleSource} instances + * @return the new {@link Observable} instance + * @throws NullPointerException if {@code sources} is {@code null} + * @since 2.0 + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T> Observable<T> concat(@NonNull ObservableSource<? extends SingleSource<? extends T>> sources) { + Objects.requireNonNull(sources, "sources is null"); + return RxJavaPlugins.onAssembly(new ObservableConcatMapSingle<>(sources, Functions.identity(), ErrorMode.IMMEDIATE, 2)); + } + + /** + * Concatenate the single values, in a non-overlapping fashion, of the {@link SingleSource}s provided by + * a {@link Publisher} sequence. + * <p> + * <img width="640" height="309" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.concat.p.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The returned {@link Flowable} honors the backpressure of the downstream consumer + * and the sources {@code Publisher} is expected to honor it as well.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code concat} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <T> the value type + * @param sources the {@code Publisher} of {@code SingleSource} instances + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code sources} is {@code null} + * @since 2.0 + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T> Flowable<T> concat(@NonNull Publisher<@NonNull ? extends SingleSource<? extends T>> sources) { + return concat(sources, 2); + } + + /** + * Concatenate the single values, in a non-overlapping fashion, of the {@link SingleSource}s provided by + * a {@link Publisher} sequence and prefetched by the specified amount. + * <p> + * <img width="640" height="310" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.concat.pn.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The returned {@link Flowable} honors the backpressure of the downstream consumer + * and the sources {@code Publisher} is expected to honor it as well.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code concat} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <T> the value type + * @param sources the {@code Publisher} of {@code SingleSource} instances + * @param prefetch the number of {@code SingleSource}s to prefetch from the {@code Publisher} + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code sources} is {@code null} + * @throws IllegalArgumentException if {@code prefetch} is non-positive + * @since 2.0 + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T> Flowable<T> concat(@NonNull Publisher<@NonNull ? extends SingleSource<? extends T>> sources, int prefetch) { + Objects.requireNonNull(sources, "sources is null"); + ObjectHelper.verifyPositive(prefetch, "prefetch"); + return RxJavaPlugins.onAssembly(new FlowableConcatMapSinglePublisher<>(sources, Functions.identity(), ErrorMode.IMMEDIATE, prefetch)); + } + + /** + * Returns a {@link Flowable} that emits the items emitted by two {@link SingleSource}s, one after the other. + * <p> + * <img width="640" height="366" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.concat.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The returned {@code Flowable} honors the backpressure of the downstream consumer.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code concat} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T> the common value type + * @param source1 + * a {@code SingleSource} to be concatenated + * @param source2 + * a {@code SingleSource} to be concatenated + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code source1} or {@code source2} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/concat.html">ReactiveX operators documentation: Concat</a> + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T> Flowable<T> concat( + @NonNull SingleSource<? extends T> source1, @NonNull SingleSource<? extends T> source2 + ) { + Objects.requireNonNull(source1, "source1 is null"); + Objects.requireNonNull(source2, "source2 is null"); + return Flowable.fromArray(source1, source2).concatMapSingleDelayError(Functions.identity(), false); + } + + /** + * Returns a {@link Flowable} that emits the items emitted by three {@link SingleSource}s, one after the other. + * <p> + * <img width="640" height="366" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.concat.o3.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The returned {@code Flowable} honors the backpressure of the downstream consumer.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code concat} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T> the common value type + * @param source1 + * a {@code SingleSource} to be concatenated + * @param source2 + * a {@code SingleSource} to be concatenated + * @param source3 + * a {@code SingleSource} to be concatenated + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code source1}, {@code source2} or {@code source3} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/concat.html">ReactiveX operators documentation: Concat</a> + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T> Flowable<T> concat( + @NonNull SingleSource<? extends T> source1, @NonNull SingleSource<? extends T> source2, + @NonNull SingleSource<? extends T> source3 + ) { + Objects.requireNonNull(source1, "source1 is null"); + Objects.requireNonNull(source2, "source2 is null"); + Objects.requireNonNull(source3, "source3 is null"); + return Flowable.fromArray(source1, source2, source3).concatMapSingleDelayError(Functions.identity(), false); + } + + /** + * Returns a {@link Flowable} that emits the items emitted by four {@link SingleSource}s, one after the other. + * <p> + * <img width="640" height="362" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.concat.o4.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The returned {@code Flowable} honors the backpressure of the downstream consumer.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code concat} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T> the common value type + * @param source1 + * a {@code SingleSource} to be concatenated + * @param source2 + * a {@code SingleSource} to be concatenated + * @param source3 + * a {@code SingleSource} to be concatenated + * @param source4 + * a {@code SingleSource} to be concatenated + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code source1}, {@code source2}, {@code source3} or {@code source4} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/concat.html">ReactiveX operators documentation: Concat</a> + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T> Flowable<T> concat( + @NonNull SingleSource<? extends T> source1, @NonNull SingleSource<? extends T> source2, + @NonNull SingleSource<? extends T> source3, @NonNull SingleSource<? extends T> source4 + ) { + Objects.requireNonNull(source1, "source1 is null"); + Objects.requireNonNull(source2, "source2 is null"); + Objects.requireNonNull(source3, "source3 is null"); + Objects.requireNonNull(source4, "source4 is null"); + return Flowable.fromArray(source1, source2, source3, source4).concatMapSingleDelayError(Functions.identity(), false); + } + + /** + * Concatenate the single values, in a non-overlapping fashion, of the {@link SingleSource}s provided in + * an array. + * <p> + * <img width="640" height="319" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.concatArray.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The returned {@link Flowable} honors the backpressure of the downstream consumer.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code concatArray} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <T> the value type + * @param sources the array of {@code SingleSource} instances + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code sources} is {@code null} + * @since 2.0 + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + @SafeVarargs + public static <@NonNull T> Flowable<T> concatArray(@NonNull SingleSource<? extends T>... sources) { + return Flowable.fromArray(sources).concatMapSingleDelayError(Functions.identity(), false); + } + + /** + * Concatenate the single values, in a non-overlapping fashion, of the {@link SingleSource}s provided in + * an array. + * <p> + * <img width="640" height="408" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.concatArrayDelayError.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The returned {@link Flowable} honors the backpressure of the downstream consumer.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code concatArrayDelayError} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <T> the value type + * @param sources the array of {@code SingleSource} instances + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code sources} is {@code null} + * @since 3.0.0 + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + @SafeVarargs + public static <@NonNull T> Flowable<T> concatArrayDelayError(@NonNull SingleSource<? extends T>... sources) { + return Flowable.fromArray(sources).concatMapSingleDelayError(Functions.identity(), true); + } + + /** + * Concatenates a sequence of {@link SingleSource} eagerly into a single stream of values. + * <p> + * <img width="640" height="257" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.concatArrayEager.png" alt=""> + * <p> + * Eager concatenation means that once a subscriber subscribes, this operator subscribes to all of the + * source {@code SingleSource}s. The operator buffers the value emitted by these {@code SingleSource}s and then drains them + * in order, each one after the previous one succeeds. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>This method does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <T> the value type + * @param sources a sequence of {@code SingleSource}s that need to be eagerly concatenated + * @return the new {@link Flowable} instance with the specified concatenation behavior + * @throws NullPointerException if {@code sources} is {@code null} + */ + @BackpressureSupport(BackpressureKind.FULL) + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + @SafeVarargs + public static <@NonNull T> Flowable<T> concatArrayEager(@NonNull SingleSource<? extends T>... sources) { + return Flowable.fromArray(sources).concatMapEager(SingleInternalHelper.toFlowable()); + } + + /** + * Concatenates a sequence of {@link SingleSource} eagerly into a single stream of values. + * <p> + * <img width="640" height="426" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.concatArrayEagerDelayError.png" alt=""> + * <p> + * Eager concatenation means that once a subscriber subscribes, this operator subscribes to all of the + * source {@code SingleSource}s. The operator buffers the value emitted by these {@code SingleSource}s and then drains them + * in order, each one after the previous one succeeds. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>This method does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <T> the value type + * @param sources a sequence of {@code SingleSource}s that need to be eagerly concatenated + * @return the new {@link Flowable} instance with the specified concatenation behavior + * @throws NullPointerException if {@code sources} is {@code null} + * @since 3.0.0 + */ + @BackpressureSupport(BackpressureKind.FULL) + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + @SafeVarargs + public static <@NonNull T> Flowable<T> concatArrayEagerDelayError(@NonNull SingleSource<? extends T>... sources) { + return Flowable.fromArray(sources).concatMapEagerDelayError(SingleInternalHelper.toFlowable(), true); + } + + /** + * Concatenates the {@link Iterable} sequence of {@link SingleSource}s into a single sequence by subscribing to each {@code SingleSource}, + * one after the other, one at a time and delays any errors till the all inner {@code SingleSource}s terminate + * as a {@link Flowable} sequence. + * <p> + * <img width="640" height="451" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.concatDelayError.i.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code concatDelayError} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T> the common element base type + * @param sources the {@code Iterable} sequence of {@code SingleSource}s + * @return the new {@code Flowable} with the concatenating behavior + * @throws NullPointerException if {@code sources} is {@code null} + * @since 3.0.0 + */ + @BackpressureSupport(BackpressureKind.FULL) + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T> Flowable<T> concatDelayError(@NonNull Iterable<@NonNull ? extends SingleSource<? extends T>> sources) { + return Flowable.fromIterable(sources).concatMapSingleDelayError(Functions.identity()); + } + + /** + * Concatenates the {@link Publisher} sequence of {@link SingleSource}s into a single sequence by subscribing to each inner {@code SingleSource}, + * one after the other, one at a time and delays any errors till the all inner and the outer {@code Publisher} terminate + * as a {@link Flowable} sequence. + * <p> + * <img width="640" height="345" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.concatDelayError.p.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>{@code concatDelayError} fully supports backpressure.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code concatDelayError} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T> the common element base type + * @param sources the {@code Publisher} sequence of {@code SingleSource}s + * @return the new {@code Flowable} with the concatenating behavior + * @throws NullPointerException if {@code sources} is {@code null} + * @since 3.0.0 + */ + @BackpressureSupport(BackpressureKind.FULL) + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public static <@NonNull T> Flowable<T> concatDelayError(@NonNull Publisher<@NonNull ? extends SingleSource<? extends T>> sources) { + return Flowable.fromPublisher(sources).concatMapSingleDelayError(Functions.identity()); + } + + /** + * Concatenates the {@link Publisher} sequence of {@link SingleSource}s into a single sequence by subscribing to each inner {@code SingleSource}, + * one after the other, one at a time and delays any errors till the all inner and the outer {@code Publisher} terminate + * as a {@link Flowable} sequence. + * <p> + * <img width="640" height="299" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.concatDelayError.pn.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>{@code concatDelayError} fully supports backpressure.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code concatDelayError} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T> the common element base type + * @param sources the {@code Publisher} sequence of {@code SingleSource}s + * @param prefetch The number of upstream items to prefetch so that fresh items are + * ready to be mapped when a previous {@code SingleSource} terminates. + * The operator replenishes after half of the prefetch amount has been consumed + * and turned into {@code SingleSource}s. + * @return the new {@code Flowable} with the concatenating behavior + * @throws NullPointerException if {@code sources} is {@code null} + * @throws IllegalArgumentException if {@code prefetch} is non-positive + * @since 3.0.0 + */ + @BackpressureSupport(BackpressureKind.FULL) + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public static <@NonNull T> Flowable<T> concatDelayError(@NonNull Publisher<@NonNull ? extends SingleSource<? extends T>> sources, int prefetch) { + return Flowable.fromPublisher(sources).concatMapSingleDelayError(Functions.identity(), true, prefetch); + } + + /** + * Concatenates an {@link Iterable} sequence of {@link SingleSource}s eagerly into a single stream of values. + * <p> + * <img width="640" height="319" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.concatEager.i.v3.png" alt=""> + * <p> + * Eager concatenation means that once a subscriber subscribes, this operator subscribes to all of the + * source {@code SingleSource}s. The operator buffers the values emitted by these {@code SingleSource}s and then drains them + * in order, each one after the previous one succeeds. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>Backpressure is honored towards the downstream.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>This method does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <T> the value type + * @param sources an {@code Iterable} sequence of {@code SingleSource} that need to be eagerly concatenated + * @return the new {@link Flowable} instance with the specified concatenation behavior + * @throws NullPointerException if {@code sources} is {@code null} + */ + @BackpressureSupport(BackpressureKind.FULL) + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T> Flowable<T> concatEager(@NonNull Iterable<@NonNull ? extends SingleSource<? extends T>> sources) { + return Flowable.fromIterable(sources).concatMapEagerDelayError(SingleInternalHelper.toFlowable(), false); + } + + /** + * Concatenates an {@link Iterable} sequence of {@link SingleSource}s eagerly into a single stream of values and + * runs a limited number of the inner sources at once. + * <p> + * <img width="640" height="439" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.concatEager.in.png" alt=""> + * <p> + * Eager concatenation means that once a subscriber subscribes, this operator subscribes to all of the + * source {@code SingleSource}s. The operator buffers the values emitted by these {@code SingleSource}s and then drains them + * in order, each one after the previous one succeeds. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>Backpressure is honored towards the downstream.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>This method does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <T> the value type + * @param sources an {@code Iterable} sequence of {@code SingleSource} that need to be eagerly concatenated + * @param maxConcurrency the maximum number of concurrently running inner {@code SingleSource}s; {@link Integer#MAX_VALUE} + * is interpreted as all inner {@code SingleSource}s can be active at the same time + * @return the new {@link Flowable} instance with the specified concatenation behavior + * @throws NullPointerException if {@code sources} is {@code null} + * @throws IllegalArgumentException if {@code maxConcurrency} is non-positive + * @since 3.0.0 + */ + @BackpressureSupport(BackpressureKind.FULL) + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T> Flowable<T> concatEager(@NonNull Iterable<@NonNull ? extends SingleSource<? extends T>> sources, int maxConcurrency) { + return Flowable.fromIterable(sources).concatMapEagerDelayError(SingleInternalHelper.toFlowable(), false, maxConcurrency, 1); + } + + /** + * Concatenates a {@link Publisher} sequence of {@link SingleSource}s eagerly into a single stream of values. + * <p> + * <img width="640" height="307" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.concatEager.p.v3.png" alt=""> + * <p> + * Eager concatenation means that once a subscriber subscribes, this operator subscribes to all of the + * emitted source {@code SingleSource}s as they are observed. The operator buffers the values emitted by these + * {@code SingleSource}s and then drains them in order, each one after the previous one succeeds. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>Backpressure is honored towards the downstream and the outer {@code Publisher} is + * expected to support backpressure. Violating this assumption, the operator will + * signal {@link io.reactivex.rxjava3.exceptions.MissingBackpressureException}.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>This method does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <T> the value type + * @param sources a sequence of {@code SingleSource}s that need to be eagerly concatenated + * @return the new {@link Flowable} instance with the specified concatenation behavior + * @throws NullPointerException if {@code sources} is {@code null} + */ + @BackpressureSupport(BackpressureKind.FULL) + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T> Flowable<T> concatEager(@NonNull Publisher<@NonNull ? extends SingleSource<? extends T>> sources) { + return Flowable.fromPublisher(sources).concatMapEager(SingleInternalHelper.toFlowable()); + } + + /** + * Concatenates a {@link Publisher} sequence of {@link SingleSource}s eagerly into a single stream of values and + * runs a limited number of those inner {@code SingleSource}s at once. + * <p> + * <img width="640" height="425" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.concatEager.pn.png" alt=""> + * <p> + * Eager concatenation means that once a subscriber subscribes, this operator subscribes to all of the + * emitted source {@code SingleSource}s as they are observed. The operator buffers the values emitted by these + * {@code SingleSource}s and then drains them in order, each one after the previous one succeeds. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>Backpressure is honored towards the downstream and the outer {@code Publisher} is + * expected to support backpressure. Violating this assumption, the operator will + * signal {@link io.reactivex.rxjava3.exceptions.MissingBackpressureException}.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>This method does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <T> the value type + * @param sources a sequence of {@code SingleSource}s that need to be eagerly concatenated + * @param maxConcurrency the maximum number of concurrently running inner {@code SingleSource}s; {@link Integer#MAX_VALUE} + * is interpreted as all inner {@code SingleSource}s can be active at the same time + * @return the new {@link Flowable} instance with the specified concatenation behavior + * @throws NullPointerException if {@code sources} is {@code null} + * @throws IllegalArgumentException if {@code maxConcurrency} is non-positive + * @since 3.0.0 + */ + @BackpressureSupport(BackpressureKind.FULL) + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T> Flowable<T> concatEager(@NonNull Publisher<@NonNull ? extends SingleSource<? extends T>> sources, int maxConcurrency) { + return Flowable.fromPublisher(sources).concatMapEager(SingleInternalHelper.toFlowable(), maxConcurrency, 1); + } + + /** + * Concatenates an {@link Iterable} sequence of {@link SingleSource}s eagerly into a single stream of values, + * delaying errors until all the inner sources terminate. + * <p> + * <img width="640" height="431" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.concatEagerDelayError.i.png" alt=""> + * <p> + * Eager concatenation means that once a subscriber subscribes, this operator subscribes to all of the + * source {@code SingleSource}s. The operator buffers the values emitted by these {@code SingleSource}s and then drains them + * in order, each one after the previous one succeeds. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>Backpressure is honored towards the downstream.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>This method does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <T> the value type + * @param sources an {@code Iterable} sequence of {@code SingleSource} that need to be eagerly concatenated + * @return the new {@link Flowable} instance with the specified concatenation behavior + * @throws NullPointerException if {@code sources} is {@code null} + * @since 3.0.0 + */ + @BackpressureSupport(BackpressureKind.FULL) + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T> Flowable<T> concatEagerDelayError(@NonNull Iterable<@NonNull ? extends SingleSource<? extends T>> sources) { + return Flowable.fromIterable(sources).concatMapEagerDelayError(SingleInternalHelper.toFlowable(), true); + } + + /** + * Concatenates an {@link Iterable} sequence of {@link SingleSource}s eagerly into a single stream of values, + * delaying errors until all the inner sources terminate. + * <p> + * <img width="640" height="378" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.concatEagerDelayError.in.png" alt=""> + * <p> + * Eager concatenation means that once a subscriber subscribes, this operator subscribes to all of the + * source {@code SingleSource}s. The operator buffers the values emitted by these {@code SingleSource}s and then drains them + * in order, each one after the previous one succeeds. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>Backpressure is honored towards the downstream.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>This method does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <T> the value type + * @param sources an {@code Iterable} sequence of {@code SingleSource} that need to be eagerly concatenated + * @param maxConcurrency the maximum number of concurrently running inner {@code SingleSource}s; {@link Integer#MAX_VALUE} + * is interpreted as all inner {@code SingleSource}s can be active at the same time + * @return the new {@link Flowable} instance with the specified concatenation behavior + * @throws NullPointerException if {@code sources} is {@code null} + * @throws IllegalArgumentException if {@code maxConcurrency} is non-positive + * @since 3.0.0 + */ + @BackpressureSupport(BackpressureKind.FULL) + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T> Flowable<T> concatEagerDelayError(@NonNull Iterable<@NonNull ? extends SingleSource<? extends T>> sources, int maxConcurrency) { + return Flowable.fromIterable(sources).concatMapEagerDelayError(SingleInternalHelper.toFlowable(), true, maxConcurrency, 1); + } + + /** + * Concatenates a {@link Publisher} sequence of {@link SingleSource}s eagerly into a single stream of values, + * delaying errors until all the inner and the outer sequence terminate. + * <p> + * <img width="640" height="444" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.concatEagerDelayError.p.png" alt=""> + * <p> + * Eager concatenation means that once a subscriber subscribes, this operator subscribes to all of the + * emitted source {@code SingleSource}s as they are observed. The operator buffers the values emitted by these + * {@code SingleSource}s and then drains them in order, each one after the previous one succeeds. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>Backpressure is honored towards the downstream and the outer {@code Publisher} is + * expected to support backpressure. Violating this assumption, the operator will + * signal {@link io.reactivex.rxjava3.exceptions.MissingBackpressureException}.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>This method does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <T> the value type + * @param sources a sequence of {@code SingleSource}s that need to be eagerly concatenated + * @return the new {@link Flowable} instance with the specified concatenation behavior + * @throws NullPointerException if {@code sources} is {@code null} + * @since 3.0.0 + */ + @BackpressureSupport(BackpressureKind.FULL) + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T> Flowable<T> concatEagerDelayError(@NonNull Publisher<@NonNull ? extends SingleSource<? extends T>> sources) { + return Flowable.fromPublisher(sources).concatMapEagerDelayError(SingleInternalHelper.toFlowable(), true); + } + + /** + * Concatenates a {@link Publisher} sequence of {@link SingleSource}s eagerly into a single stream of values, + * running at most the specified number of those inner {@code SingleSource}s at once and + * delaying errors until all the inner and the outer sequence terminate. + * <p> + * <img width="640" height="421" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.concatEagerDelayError.pn.png" alt=""> + * <p> + * Eager concatenation means that once a subscriber subscribes, this operator subscribes to all of the + * emitted source {@code SingleSource}s as they are observed. The operator buffers the values emitted by these + * {@code SingleSource}s and then drains them in order, each one after the previous one succeeds. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>Backpressure is honored towards the downstream and the outer {@code Publisher} is + * expected to support backpressure. Violating this assumption, the operator will + * signal {@link io.reactivex.rxjava3.exceptions.MissingBackpressureException}.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>This method does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <T> the value type + * @param sources a sequence of {@code SingleSource}s that need to be eagerly concatenated + * @param maxConcurrency the number of inner {@code SingleSource}s to run at once + * @return the new {@link Flowable} instance with the specified concatenation behavior + * @throws NullPointerException if {@code sources} is {@code null} + * @throws IllegalArgumentException if {@code maxConcurrency} is non-positive + * @since 3.0.0 + */ + @BackpressureSupport(BackpressureKind.FULL) + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T> Flowable<T> concatEagerDelayError(@NonNull Publisher<@NonNull ? extends SingleSource<? extends T>> sources, int maxConcurrency) { + return Flowable.fromPublisher(sources).concatMapEagerDelayError(SingleInternalHelper.toFlowable(), true, maxConcurrency, 1); + } + + /** + * Provides an API (via a cold {@code Single}) that bridges the reactive world with the callback-style world. + * <p> + * <img width="640" height="454" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.create.v3.png" alt=""> + * <p> + * Example: + * <pre><code> + * Single.<Event>create(emitter -> { + * Callback listener = new Callback() { + * @Override + * public void onEvent(Event e) { + * emitter.onSuccess(e); + * } + * + * @Override + * public void onFailure(Exception e) { + * emitter.onError(e); + * } + * }; + * + * AutoCloseable c = api.someMethod(listener); + * + * emitter.setCancellable(c::close); + * + * }); + * </code></pre> + * <p> + * Whenever a {@link SingleObserver} subscribes to the returned {@code Single}, the provided + * {@link SingleOnSubscribe} callback is invoked with a fresh instance of a {@link SingleEmitter} + * that will interact only with that specific {@code SingleObserver}. If this {@code SingleObserver} + * disposes the flow (making {@link SingleEmitter#isDisposed} return {@code true}), + * other observers subscribed to the same returned {@code Single} are not affected. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code create} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <T> the value type + * @param source the emitter that is called when a {@code SingleObserver} subscribes to the returned {@code Single} + * @return the new {@code Single} instance + * @throws NullPointerException if {@code source} is {@code null} + * @see SingleOnSubscribe + * @see Cancellable + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T> Single<T> create(@NonNull SingleOnSubscribe<T> source) { + Objects.requireNonNull(source, "source is null"); + return RxJavaPlugins.onAssembly(new SingleCreate<>(source)); + } + + /** + * Calls a {@link Supplier} for each individual {@link SingleObserver} to return the actual {@link SingleSource} to + * be subscribed to. + * <p> + * <img width="640" height="515" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.defer.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code defer} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <T> the value type + * @param supplier the {@code Supplier} that is called for each individual {@code SingleObserver} and + * returns a {@code SingleSource} instance to subscribe to + * @throws NullPointerException if {@code supplier} is {@code null} + * @return the new {@code Single} instance + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T> Single<T> defer(@NonNull Supplier<? extends @NonNull SingleSource<? extends T>> supplier) { + Objects.requireNonNull(supplier, "supplier is null"); + return RxJavaPlugins.onAssembly(new SingleDefer<>(supplier)); + } + + /** + * Signals a {@link Throwable} returned by the callback function for each individual {@link SingleObserver}. + * <p> + * <img width="640" height="283" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.error.c.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code error} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <T> the value type + * @param supplier the {@link Supplier} that is called for each individual {@code SingleObserver} and + * returns a {@code Throwable} instance to be emitted. + * @throws NullPointerException if {@code supplier} is {@code null} + * @return the new {@code Single} instance + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T> Single<T> error(@NonNull Supplier<? extends @NonNull Throwable> supplier) { + Objects.requireNonNull(supplier, "supplier is null"); + return RxJavaPlugins.onAssembly(new SingleError<>(supplier)); + } + + /** + * Returns a {@code Single} that invokes a subscriber's {@link SingleObserver#onError onError} method when the + * subscriber subscribes to it. + * <p> + * <img width="640" height="283" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.error.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code error} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param throwable + * the particular {@link Throwable} to pass to {@link SingleObserver#onError onError} + * @param <T> + * the type of the item (ostensibly) emitted by the {@code Single} + * @return the new {@code Single} that invokes the subscriber's {@link SingleObserver#onError onError} method when + * the subscriber subscribes to it + * @throws NullPointerException if {@code throwable} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/empty-never-throw.html">ReactiveX operators documentation: Throw</a> + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T> Single<T> error(@NonNull Throwable throwable) { + Objects.requireNonNull(throwable, "throwable is null"); + return error(Functions.justSupplier(throwable)); + } + + /** + * Returns a {@code Single} that invokes the given {@link Callable} for each incoming {@link SingleObserver} + * and emits its value or exception to them. + * <p> + * Allows you to defer execution of passed function until {@code SingleObserver} subscribes to the {@code Single}. + * It makes passed function "lazy". + * Result of the function invocation will be emitted by the {@link Single}. + * <p> + * <img width="640" height="467" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.fromCallable.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code fromCallable} does not operate by default on a particular {@link Scheduler}.</dd> + * <dt><b>Error handling:</b></dt> + * <dd> If the {@code Callable} throws an exception, the respective {@link Throwable} is + * delivered to the downstream via {@link SingleObserver#onError(Throwable)}, + * except when the downstream has disposed this {@code Single} source. + * In this latter case, the {@code Throwable} is delivered to the global error handler via + * {@link RxJavaPlugins#onError(Throwable)} as an {@link io.reactivex.rxjava3.exceptions.UndeliverableException UndeliverableException}. + * </dd> + * </dl> + * + * @param callable + * function which execution should be deferred, it will be invoked when {@code SingleObserver} will subscribe to the {@link Single}. + * @param <T> + * the type of the item emitted by the {@code Single}. + * @return the new {@code Single} whose {@code SingleObserver}s' subscriptions trigger an invocation of the given function. + * @throws NullPointerException if {@code callable} is {@code null} + * @see #defer(Supplier) + * @see #fromSupplier(Supplier) + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T> Single<T> fromCallable(@NonNull Callable<? extends T> callable) { + Objects.requireNonNull(callable, "callable is null"); + return RxJavaPlugins.onAssembly(new SingleFromCallable<>(callable)); + } + + /** + * Converts a {@link Future} into a {@code Single} and awaits its outcome in a blocking fashion. + * <p> + * <img width="640" height="315" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.from.Future.v3.png" alt=""> + * <p> + * The operator calls {@link Future#get()}, which is a blocking method, on the subscription thread. + * It is recommended applying {@link #subscribeOn(Scheduler)} to move this blocking wait to a + * background thread, and if the {@link Scheduler} supports it, interrupt the wait when the flow + * is disposed. + * <p> + * A non-{@code null} value is then emitted via {@code onSuccess} or any exception is emitted via + * {@code onError}. If the {@code Future} completes with {@code null}, a {@link NullPointerException} + * is signaled. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code fromFuture} does not operate by default on a particular {@code Scheduler}.</dd> + * </dl> + * + * @param future + * the source {@code Future} + * @param <T> + * the type of object that the {@code Future} returns, and also the type of item to be emitted by + * the resulting {@code Single} + * @return the new {@code Single} that emits the item from the source {@code Future} + * @throws NullPointerException if {@code future} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/from.html">ReactiveX operators documentation: From</a> + * @see #fromFuture(Future, long, TimeUnit) + * @see #fromCompletionStage(CompletionStage) + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public static <@NonNull T> Single<T> fromFuture(@NonNull Future<? extends T> future) { + return toSingle(Flowable.fromFuture(future)); + } + + /** + * Converts a {@link Future} into a {@code Single} and awaits its outcome, or timeout, in a blocking fashion. + * <p> + * <img width="640" height="315" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.from.Future.v3.png" alt=""> + * <p> + * The operator calls {@link Future#get(long, TimeUnit)}, which is a blocking method, on the subscription thread. + * It is recommended applying {@link #subscribeOn(Scheduler)} to move this blocking wait to a + * background thread, and if the {@link Scheduler} supports it, interrupt the wait when the flow + * is disposed. + * <p> + * A non-{@code null} value is then emitted via {@code onSuccess} or any exception is emitted via + * {@code onError}. If the {@code Future} completes with {@code null}, a {@link NullPointerException} + * is signaled. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code fromFuture} does not operate by default on a particular {@code Scheduler}.</dd> + * </dl> + * + * @param future + * the source {@code Future} + * @param timeout + * the maximum time to wait before calling {@code get} + * @param unit + * the {@link TimeUnit} of the {@code timeout} argument + * @param <T> + * the type of object that the {@code Future} returns, and also the type of item to be emitted by + * the resulting {@code Single} + * @return the new {@code Single} that emits the item from the source {@code Future} + * @throws NullPointerException if {@code future} or {@code unit} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/from.html">ReactiveX operators documentation: From</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public static <@NonNull T> Single<T> fromFuture(@NonNull Future<? extends T> future, long timeout, @NonNull TimeUnit unit) { + return toSingle(Flowable.fromFuture(future, timeout, unit)); + } + + /** + * Returns a {@code Single} instance that when subscribed to, subscribes to the {@link MaybeSource} instance and + * emits {@code onSuccess} as a single item, turns an {@code onComplete} into {@link NoSuchElementException} error signal or + * forwards the {@code onError} signal. + * <p> + * <img width="640" height="241" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.fromMaybe.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code fromMaybe} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <T> the value type of the {@code MaybeSource} element + * @param maybe the {@code MaybeSource} instance to subscribe to, not {@code null} + * @return the new {@code Single} instance + * @throws NullPointerException if {@code maybe} is {@code null} + * @since 3.0.0 + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T> Single<T> fromMaybe(@NonNull MaybeSource<T> maybe) { + Objects.requireNonNull(maybe, "maybe is null"); + return RxJavaPlugins.onAssembly(new MaybeToSingle<>(maybe, null)); + } + + /** + * Returns a {@code Single} instance that when subscribed to, subscribes to the {@link MaybeSource} instance and + * emits {@code onSuccess} as a single item, emits the {@code defaultItem} for an {@code onComplete} signal or + * forwards the {@code onError} signal. + * <p> + * <img width="640" height="353" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.fromMaybe.v.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code fromMaybe} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <T> the value type of the {@code MaybeSource} element + * @param maybe the {@code MaybeSource} instance to subscribe to, not {@code null} + * @param defaultItem the item to signal if the current {@code MaybeSource} is empty + * @return the new {@code Single} instance + * @throws NullPointerException if {@code maybe} or {@code defaultItem} is {@code null} + * @since 3.0.0 + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T> Single<T> fromMaybe(@NonNull MaybeSource<T> maybe, @NonNull T defaultItem) { + Objects.requireNonNull(maybe, "maybe is null"); + Objects.requireNonNull(defaultItem, "defaultItem is null"); + return RxJavaPlugins.onAssembly(new MaybeToSingle<>(maybe, defaultItem)); + } + + /** + * Wraps a specific {@link Publisher} into a {@code Single} and signals its single element or error. + * <p> + * <img width="640" height="322" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.fromPublisher.v3.png" alt=""> + * <p> + * If the source {@code Publisher} is empty, a {@link NoSuchElementException} is signaled. If + * the source has more than one element, an {@link IndexOutOfBoundsException} is signaled. + * <p> + * The {@code Publisher} must follow the + * <a href="https://github.com/reactive-streams/reactive-streams-jvm#reactive-streams">Reactive Streams specification</a>. + * Violating the specification may result in undefined behavior. + * <p> + * If possible, use {@link #create(SingleOnSubscribe)} to create a + * source-like {@code Single} instead. + * <p> + * Note that even though {@code Publisher} appears to be a functional interface, it + * is not recommended to implement it through a lambda as the specification requires + * state management that is not achievable with a stateless lambda. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The {@code publisher} is consumed in an unbounded fashion but will be cancelled + * if it produced more than one item.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code fromPublisher} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <T> the value type + * @param publisher the source {@code Publisher} instance, not {@code null} + * @return the new {@code Single} instance + * @throws NullPointerException if {@code publisher} is {@code null} + * @see #create(SingleOnSubscribe) + */ + @BackpressureSupport(BackpressureKind.UNBOUNDED_IN) + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T> Single<T> fromPublisher(@NonNull Publisher<? extends T> publisher) { + Objects.requireNonNull(publisher, "publisher is null"); + return RxJavaPlugins.onAssembly(new SingleFromPublisher<>(publisher)); + } + + /** + * Wraps a specific {@link ObservableSource} into a {@code Single} and signals its single element or error. + * <p> + * <img width="640" height="343" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.fromObservable.v3.png" alt=""> + * <p> + * If the {@code ObservableSource} is empty, a {@link NoSuchElementException} is signaled. + * If the source has more than one element, an {@link IndexOutOfBoundsException} is signaled. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code fromObservable} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param observable the source sequence to wrap, not {@code null} + * @param <T> + * the type of the item emitted by the {@code Single}. + * @return the new {@code Single} instance + * @throws NullPointerException if {@code observable} is {@code null} + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T> Single<T> fromObservable(@NonNull ObservableSource<? extends T> observable) { + Objects.requireNonNull(observable, "observable is null"); + return RxJavaPlugins.onAssembly(new ObservableSingleSingle<>(observable, null)); + } + + /** + * Returns a {@code Single} that invokes passed supplier and emits its result + * for each individual {@link SingleObserver} that subscribes. + * <p> + * Allows you to defer execution of passed function until a {@code SingleObserver} subscribes to the {@link Single}. + * It makes passed function "lazy". + * Result of the function invocation will be emitted by the {@link Single}. + * <p> + * <img width="640" height="467" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.fromSupplier.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code fromSupplier} does not operate by default on a particular {@link Scheduler}.</dd> + * <dt><b>Error handling:</b></dt> + * <dd> If the {@link Supplier} throws an exception, the respective {@link Throwable} is + * delivered to the downstream via {@link SingleObserver#onError(Throwable)}, + * except when the downstream has disposed this {@code Single} source. + * In this latter case, the {@code Throwable} is delivered to the global error handler via + * {@link RxJavaPlugins#onError(Throwable)} as an {@link io.reactivex.rxjava3.exceptions.UndeliverableException UndeliverableException}. + * </dd> + * </dl> + * + * @param supplier + * function which execution should be deferred, it will be invoked when {@code SingleObserver} subscribes to the {@code Single}. + * @param <T> + * the type of the item emitted by the {@code Single}. + * @return the new {@code Single} whose {@code SingleObserver}s' subscriptions trigger an invocation of the given function. + * @throws NullPointerException if {@code supplier} is {@code null} + * @see #defer(Supplier) + * @see #fromCallable(Callable) + * @since 3.0.0 + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T> Single<T> fromSupplier(@NonNull Supplier<? extends T> supplier) { + Objects.requireNonNull(supplier, "supplier is null"); + return RxJavaPlugins.onAssembly(new SingleFromSupplier<>(supplier)); + } + + /** + * Returns a {@code Single} that emits a specified item. + * <p> + * <img width="640" height="310" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.just.v3.png" alt=""> + * <p> + * To convert any object into a {@code Single} that emits that object, pass that object into the + * {@code just} method. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code just} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param item + * the item to emit + * @param <T> + * the type of that item + * @return the new {@code Single} that emits {@code item} + * @throws NullPointerException if {@code item} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/just.html">ReactiveX operators documentation: Just</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public static <@NonNull T> Single<T> just(T item) { + Objects.requireNonNull(item, "item is null"); + return RxJavaPlugins.onAssembly(new SingleJust<>(item)); + } + + /** + * Merges an {@link Iterable} sequence of {@link SingleSource} instances into a single {@link Flowable} sequence, + * running all {@code SingleSource}s at once. + * <p> + * <img width="640" height="319" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.merge.i.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The returned {@code Flowable} honors the backpressure of the downstream consumer.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code merge} does not operate by default on a particular {@link Scheduler}.</dd> + * <dt><b>Error handling:</b></dt> + * <dd>If any of the source {@code SingleSource}s signal a {@link Throwable} via {@code onError}, the resulting + * {@code Flowable} terminates with that {@code Throwable} and all other source {@code SingleSource}s are disposed. + * If more than one {@code SingleSource} signals an error, the resulting {@code Flowable} may terminate with the + * first one's error or, depending on the concurrency of the sources, may terminate with a + * {@link CompositeException} containing two or more of the various error signals. + * {@code Throwable}s that didn't make into the composite will be sent (individually) to the global error handler via + * {@link RxJavaPlugins#onError(Throwable)} method as {@link UndeliverableException} errors. Similarly, {@code Throwable}s + * signaled by source(s) after the returned {@code Flowable} has been cancelled or terminated with a + * (composite) error will be sent to the same global error handler. + * Use {@link #mergeDelayError(Iterable)} to merge sources and terminate only when all source {@code SingleSource}s + * have completed or failed with an error. + * </dd> + * </dl> + * @param <T> the common and resulting value type + * @param sources the {@code Iterable} sequence of {@code SingleSource} sources + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code sources} is {@code null} + * @since 2.0 + * @see #mergeDelayError(Iterable) + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T> Flowable<T> merge(@NonNull Iterable<@NonNull ? extends SingleSource<? extends T>> sources) { + return Flowable.fromIterable(sources).flatMapSingle(Functions.identity()); + } + + /** + * Merges a sequence of {@link SingleSource} instances emitted by a {@link Publisher} into a single {@link Flowable} sequence, + * running all {@code SingleSource}s at once. + * <p> + * <img width="640" height="307" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.merge.p.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The returned {@code Flowable} honors the backpressure of the downstream consumer.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code merge} does not operate by default on a particular {@link Scheduler}.</dd> + * <dt><b>Error handling:</b></dt> + * <dd>If any of the source {@code SingleSource}s signal a {@link Throwable} via {@code onError}, the resulting + * {@code Flowable} terminates with that {@code Throwable} and all other source {@code SingleSource}s are disposed. + * If more than one {@code SingleSource} signals an error, the resulting {@code Flowable} may terminate with the + * first one's error or, depending on the concurrency of the sources, may terminate with a + * {@link CompositeException} containing two or more of the various error signals. + * {@code Throwable}s that didn't make into the composite will be sent (individually) to the global error handler via + * {@link RxJavaPlugins#onError(Throwable)} method as {@link UndeliverableException} errors. Similarly, {@code Throwable}s + * signaled by source(s) after the returned {@code Flowable} has been cancelled or terminated with a + * (composite) error will be sent to the same global error handler. + * Use {@link #mergeDelayError(Publisher)} to merge sources and terminate only when all source {@code SingleSource}s + * have completed or failed with an error. + * </dd> + * </dl> + * @param <T> the common and resulting value type + * @param sources the {@code Publisher} emitting a sequence of {@code SingleSource}s + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code sources} is {@code null} + * @see #mergeDelayError(Publisher) + * @since 2.0 + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T> Flowable<T> merge(@NonNull Publisher<@NonNull ? extends SingleSource<? extends T>> sources) { + Objects.requireNonNull(sources, "sources is null"); + return RxJavaPlugins.onAssembly(new FlowableFlatMapSinglePublisher<>(sources, Functions.identity(), false, Integer.MAX_VALUE)); + } + + /** + * Flattens a {@link SingleSource} that emits a {@code SingleSingle} into a single {@code Single} that emits the item + * emitted by the nested {@code SingleSource}, without any transformation. + * <p> + * <img width="640" height="412" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.merge.oo.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code merge} does not operate by default on a particular {@link Scheduler}.</dd> + * <dd>The resulting {@code Single} emits the outer source's or the inner {@code SingleSource}'s {@link Throwable} as is. + * Unlike the other {@code merge()} operators, this operator won't and can't produce a {@link CompositeException} because there is + * only one possibility for the outer or the inner {@code SingleSource} to emit an {@code onError} signal. + * Therefore, there is no need for a {@code mergeDelayError(SingleSource<SingleSource<T>>)} operator. + * </dd> + * </dl> + * + * @param <T> the value type of the sources and the output + * @param source + * a {@code Single} that emits a {@code Single} + * @return the new {@code Single} that emits the item that is the result of flattening the {@code Single} emitted + * by {@code source} + * @throws NullPointerException if {@code source} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/merge.html">ReactiveX operators documentation: Merge</a> + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T> Single<T> merge(@NonNull SingleSource<? extends SingleSource<? extends T>> source) { + Objects.requireNonNull(source, "source is null"); + return RxJavaPlugins.onAssembly(new SingleFlatMap<SingleSource<? extends T>, T>(source, Functions.identity())); + } + + /** + * Flattens two {@link SingleSource}s into one {@link Flowable} sequence, without any transformation. + * <p> + * <img width="640" height="414" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.merge.v3.png" alt=""> + * <p> + * You can combine items emitted by multiple {@code SingleSource}s so that they appear as a single {@code Flowable}, by + * using the {@code merge} method. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The returned {@code Flowable} honors the backpressure of the downstream consumer.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code merge} does not operate by default on a particular {@link Scheduler}.</dd> + * <dt><b>Error handling:</b></dt> + * <dd>If any of the source {@code SingleSource}s signal a {@link Throwable} via {@code onError}, the resulting + * {@code Flowable} terminates with that {@code Throwable} and all other source {@code SingleSource}s are disposed. + * If more than one {@code SingleSource} signals an error, the resulting {@code Flowable} may terminate with the + * first one's error or, depending on the concurrency of the sources, may terminate with a + * {@link CompositeException} containing two or more of the various error signals. + * {@code Throwable}s that didn't make into the composite will be sent (individually) to the global error handler via + * {@link RxJavaPlugins#onError(Throwable)} method as {@link UndeliverableException} errors. Similarly, {@code Throwable}s + * signaled by source(s) after the returned {@code Flowable} has been cancelled or terminated with a + * (composite) error will be sent to the same global error handler. + * Use {@link #mergeDelayError(SingleSource, SingleSource)} to merge sources and terminate only when all source {@code SingleSource}s + * have completed or failed with an error. + * </dd> + * </dl> + * + * @param <T> the common value type + * @param source1 + * a {@code SingleSource} to be merged + * @param source2 + * a {@code SingleSource} to be merged + * @return the new {@code Flowable} that emits all of the items emitted by the source {@code SingleSource}s + * @throws NullPointerException if {@code source1} or {@code source2} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/merge.html">ReactiveX operators documentation: Merge</a> + * @see #mergeDelayError(SingleSource, SingleSource) + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T> Flowable<T> merge( + @NonNull SingleSource<? extends T> source1, @NonNull SingleSource<? extends T> source2 + ) { + Objects.requireNonNull(source1, "source1 is null"); + Objects.requireNonNull(source2, "source2 is null"); + return Flowable.fromArray(source1, source2).flatMapSingle(Functions.identity(), false, Integer.MAX_VALUE); + } + + /** + * Flattens three {@link SingleSource}s into one {@link Flowable} sequence, without any transformation. + * <p> + * <img width="640" height="366" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.merge.o3.v3.png" alt=""> + * <p> + * You can combine items emitted by multiple {@code SingleSource}s so that they appear as a single {@code Flowable}, by + * the {@code merge} method. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The returned {@code Flowable} honors the backpressure of the downstream consumer.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code merge} does not operate by default on a particular {@link Scheduler}.</dd> + * <dt><b>Error handling:</b></dt> + * <dd>If any of the source {@code SingleSource}s signal a {@link Throwable} via {@code onError}, the resulting + * {@code Flowable} terminates with that {@code Throwable} and all other source {@code SingleSource}s are disposed. + * If more than one {@code SingleSource} signals an error, the resulting {@code Flowable} may terminate with the + * first one's error or, depending on the concurrency of the sources, may terminate with a + * {@link CompositeException} containing two or more of the various error signals. + * {@code Throwable}s that didn't make into the composite will be sent (individually) to the global error handler via + * {@link RxJavaPlugins#onError(Throwable)} method as {@link UndeliverableException} errors. Similarly, {@code Throwable}s + * signaled by source(s) after the returned {@code Flowable} has been cancelled or terminated with a + * (composite) error will be sent to the same global error handler. + * Use {@link #mergeDelayError(SingleSource, SingleSource, SingleSource)} to merge sources and terminate only when all source {@code SingleSource}s + * have completed or failed with an error. + * </dd> + * </dl> + * + * @param <T> the common value type + * @param source1 + * a {@code SingleSource} to be merged + * @param source2 + * a {@code SingleSource} to be merged + * @param source3 + * a {@code SingleSource} to be merged + * @return the new {@code Flowable} that emits all of the items emitted by the source {@code SingleSource}s + * @throws NullPointerException if {@code source1}, {@code source2} or {@code source3} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/merge.html">ReactiveX operators documentation: Merge</a> + * @see #mergeDelayError(SingleSource, SingleSource, SingleSource) + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T> Flowable<T> merge( + @NonNull SingleSource<? extends T> source1, @NonNull SingleSource<? extends T> source2, + @NonNull SingleSource<? extends T> source3 + ) { + Objects.requireNonNull(source1, "source1 is null"); + Objects.requireNonNull(source2, "source2 is null"); + Objects.requireNonNull(source3, "source3 is null"); + return Flowable.fromArray(source1, source2, source3).flatMapSingle(Functions.identity(), false, Integer.MAX_VALUE); + } + + /** + * Flattens four {@link SingleSource}s into one {@link Flowable} sequence, without any transformation. + * <p> + * <img width="640" height="362" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.merge.o4.v3.png" alt=""> + * <p> + * You can combine items emitted by multiple {@code SingleSource}s so that they appear as a single {@code Flowable}, by + * the {@code merge} method. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The returned {@code Flowable} honors the backpressure of the downstream consumer.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code merge} does not operate by default on a particular {@link Scheduler}.</dd> + * <dt><b>Error handling:</b></dt> + * <dd>If any of the source {@code SingleSource}s signal a {@link Throwable} via {@code onError}, the resulting + * {@code Flowable} terminates with that {@code Throwable} and all other source {@code SingleSource}s are disposed. + * If more than one {@code SingleSource} signals an error, the resulting {@code Flowable} may terminate with the + * first one's error or, depending on the concurrency of the sources, may terminate with a + * {@link CompositeException} containing two or more of the various error signals. + * {@code Throwable}s that didn't make into the composite will be sent (individually) to the global error handler via + * {@link RxJavaPlugins#onError(Throwable)} method as {@link UndeliverableException} errors. Similarly, {@code Throwable}s + * signaled by source(s) after the returned {@code Flowable} has been cancelled or terminated with a + * (composite) error will be sent to the same global error handler. + * Use {@link #mergeDelayError(SingleSource, SingleSource, SingleSource, SingleSource)} to merge sources and terminate only when all source {@code SingleSource}s + * have completed or failed with an error. + * </dd> + * </dl> + * + * @param <T> the common value type + * @param source1 + * a {@code SingleSource} to be merged + * @param source2 + * a {@code SingleSource} to be merged + * @param source3 + * a {@code SingleSource} to be merged + * @param source4 + * a {@code SingleSource} to be merged + * @return the new {@code Flowable} that emits all of the items emitted by the source {@code SingleSource}s + * @throws NullPointerException if {@code source1}, {@code source2}, {@code source3} or {@code source4} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/merge.html">ReactiveX operators documentation: Merge</a> + * @see #mergeDelayError(SingleSource, SingleSource, SingleSource, SingleSource) + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T> Flowable<T> merge( + @NonNull SingleSource<? extends T> source1, @NonNull SingleSource<? extends T> source2, + @NonNull SingleSource<? extends T> source3, @NonNull SingleSource<? extends T> source4 + ) { + Objects.requireNonNull(source1, "source1 is null"); + Objects.requireNonNull(source2, "source2 is null"); + Objects.requireNonNull(source3, "source3 is null"); + Objects.requireNonNull(source4, "source4 is null"); + return Flowable.fromArray(source1, source2, source3, source4).flatMapSingle(Functions.identity(), false, Integer.MAX_VALUE); + } + + /** + * Merges an array of {@link SingleSource} instances into a single {@link Flowable} sequence, + * running all {@code SingleSource}s at once. + * <p> + * <img width="640" height="272" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.mergeArray.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code mergeArray} does not operate by default on a particular {@link Scheduler}.</dd> + * <dt><b>Error handling:</b></dt> + * <dd>If any of the source {@code SingleSource}s signal a {@link Throwable} via {@code onError}, the resulting + * {@code Flowable} terminates with that {@code Throwable} and all other source {@code SingleSource}s are disposed. + * If more than one {@code SingleSource} signals an error, the resulting {@code Flowable} may terminate with the + * first one's error or, depending on the concurrency of the sources, may terminate with a + * {@link CompositeException} containing two or more of the various error signals. + * {@code Throwable}s that didn't make into the composite will be sent (individually) to the global error handler via + * {@link RxJavaPlugins#onError(Throwable)} method as {@link UndeliverableException} errors. Similarly, {@code Throwable}s + * signaled by source(s) after the returned {@code Flowable} has been cancelled or terminated with a + * (composite) error will be sent to the same global error handler. + * Use {@link #mergeArrayDelayError(SingleSource...)} to merge sources and terminate only when all source {@code SingleSource}s + * have completed or failed with an error. + * </dd> + * </dl> + * @param <T> the common and resulting value type + * @param sources the array sequence of {@code SingleSource} sources + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code sources} is {@code null} + * @see #mergeArrayDelayError(SingleSource...) + */ + @BackpressureSupport(BackpressureKind.FULL) + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + @SafeVarargs + public static <@NonNull T> Flowable<T> mergeArray(SingleSource<? extends T>... sources) { + return Flowable.fromArray(sources).flatMapSingle(Functions.identity(), false, Math.max(1, sources.length)); + } + + /** + * Flattens an array of {@link SingleSource}s into one {@link Flowable}, in a way that allows a subscriber to receive all + * successfully emitted items from each of the source {@code SingleSource}s without being interrupted by an error + * notification from one of them. + * <p> + * <img width="640" height="422" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.mergeArrayDelayError.png" alt=""> + * <p> + * This behaves like {@link #merge(Publisher)} except that if any of the merged {@code SingleSource}s notify of an + * error via {@link Subscriber#onError onError}, {@code mergeArrayDelayError} will refrain from propagating that + * error notification until all of the merged {@code SingleSource}s have finished emitting items. + * <p> + * Even if multiple merged {@code SingleSource}s send {@code onError} notifications, {@code mergeArrayDelayError} will only + * invoke the {@code onError} method of its subscribers once. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code mergeArrayDelayError} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T> the common element base type + * @param sources + * the array of {@code SingleSource}s + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code sources} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/merge.html">ReactiveX operators documentation: Merge</a> + */ + @BackpressureSupport(BackpressureKind.FULL) + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @SafeVarargs + @NonNull + public static <@NonNull T> Flowable<T> mergeArrayDelayError(@NonNull SingleSource<? extends T>... sources) { + return Flowable.fromArray(sources).flatMapSingle(Functions.identity(), true, Math.max(1, sources.length)); + } + + /** + * Merges an {@link Iterable} sequence of {@link SingleSource} instances into one {@link Flowable} sequence, + * running all {@code SingleSource}s at once and delaying any error(s) until all sources succeed or fail. + * <p> + * <img width="640" height="469" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.mergeDelayError.i.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The returned {@code Flowable} honors the backpressure of the downstream consumer.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code mergeDelayError} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * <p>History: 2.1.9 - experimental + * @param <T> the common and resulting value type + * @param sources the {@code Iterable} sequence of {@code SingleSource}s + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code sources} is {@code null} + * @see #merge(Iterable) + * @since 2.2 + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T> Flowable<T> mergeDelayError(@NonNull Iterable<@NonNull ? extends SingleSource<? extends T>> sources) { + return Flowable.fromIterable(sources).flatMapSingle(Functions.identity(), true, Integer.MAX_VALUE); + } + + /** + * Merges a sequence of {@link SingleSource} instances emitted by a {@link Publisher} into a {@link Flowable} sequence, + * running all {@code SingleSource}s at once and delaying any error(s) until all sources succeed or fail. + * <p> + * <img width="640" height="356" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.mergeDelayError.p.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The returned {@code Flowable} honors the backpressure of the downstream consumer.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code mergeDelayError} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * <p>History: 2.1.9 - experimental + * @param <T> the common and resulting value type + * @param sources the {@code Flowable} sequence of {@code SingleSource}s + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code sources} is {@code null} + * @since 2.2 + * @see #merge(Publisher) + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T> Flowable<T> mergeDelayError(@NonNull Publisher<@NonNull ? extends SingleSource<? extends T>> sources) { + Objects.requireNonNull(sources, "sources is null"); + return RxJavaPlugins.onAssembly(new FlowableFlatMapSinglePublisher<>(sources, Functions.identity(), true, Integer.MAX_VALUE)); + } + + /** + * Flattens two {@link SingleSource}s into one {@link Flowable}, without any transformation, delaying + * any error(s) until all sources succeed or fail. + * <p> + * <img width="640" height="554" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.mergeDelayError.2.png" alt=""> + * <p> + * You can combine items emitted by multiple {@code SingleSource}s so that they appear as one {@code Flowable}, by + * using the {@code mergeDelayError} method. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The returned {@code Flowable} honors the backpressure of the downstream consumer.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code mergeDelayError} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * <p>History: 2.1.9 - experimental + * @param <T> the common value type + * @param source1 + * a {@code SingleSource} to be merged + * @param source2 + * a {@code SingleSource} to be merged + * @return the new {@code Flowable} that emits all of the items emitted by the source {@code SingleSource}s + * @throws NullPointerException if {@code source1} or {@code source2} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/merge.html">ReactiveX operators documentation: Merge</a> + * @see #merge(SingleSource, SingleSource) + * @since 2.2 + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T> Flowable<T> mergeDelayError( + @NonNull SingleSource<? extends T> source1, @NonNull SingleSource<? extends T> source2 + ) { + Objects.requireNonNull(source1, "source1 is null"); + Objects.requireNonNull(source2, "source2 is null"); + return Flowable.fromArray(source1, source2).flatMapSingle(Functions.identity(), true, Integer.MAX_VALUE); + } + + /** + * Flattens two {@link SingleSource}s into one {@link Flowable}, without any transformation, delaying + * any error(s) until all sources succeed or fail. + * <p> + * <img width="640" height="496" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.mergeDelayError.3.png" alt=""> + * <p> + * You can combine items emitted by multiple {@code SingleSource}s so that they appear as one {@code Flowable}, by + * the {@code mergeDelayError} method. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The returned {@code Flowable} honors the backpressure of the downstream consumer.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code mergeDelayError} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * <p>History: 2.1.9 - experimental + * @param <T> the common value type + * @param source1 + * a {@code SingleSource} to be merged + * @param source2 + * a {@code SingleSource} to be merged + * @param source3 + * a {@code SingleSource} to be merged + * @return the new {@code Flowable} that emits all of the items emitted by the source {@code SingleSource}s + * @throws NullPointerException if {@code source1}, {@code source2} or {@code source3} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/merge.html">ReactiveX operators documentation: Merge</a> + * @see #merge(SingleSource, SingleSource, SingleSource) + * @since 2.2 + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T> Flowable<T> mergeDelayError( + @NonNull SingleSource<? extends T> source1, @NonNull SingleSource<? extends T> source2, + @NonNull SingleSource<? extends T> source3 + ) { + Objects.requireNonNull(source1, "source1 is null"); + Objects.requireNonNull(source2, "source2 is null"); + Objects.requireNonNull(source3, "source3 is null"); + return Flowable.fromArray(source1, source2, source3).flatMapSingle(Functions.identity(), true, Integer.MAX_VALUE); + } + + /** + * Flattens two {@link SingleSource}s into one {@link Flowable}, without any transformation, delaying + * any error(s) until all sources succeed or fail. + * <p> + * <img width="640" height="509" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.mergeDelayError.4.png" alt=""> + * <p> + * You can combine items emitted by multiple {@code SingleSource}s so that they appear as one {@code Flowable}, by + * the {@code mergeDelayError} method. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The returned {@code Flowable} honors the backpressure of the downstream consumer.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code mergeDelayError} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * <p>History: 2.1.9 - experimental + * @param <T> the common value type + * @param source1 + * a {@code SingleSource} to be merged + * @param source2 + * a {@code SingleSource} to be merged + * @param source3 + * a {@code SingleSource} to be merged + * @param source4 + * a {@code SingleSource} to be merged + * @return the new {@code Flowable} that emits all of the items emitted by the source {@code SingleSource}s + * @throws NullPointerException if {@code source1}, {@code source2}, {@code source3} or {@code source4} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/merge.html">ReactiveX operators documentation: Merge</a> + * @see #merge(SingleSource, SingleSource, SingleSource, SingleSource) + * @since 2.2 + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T> Flowable<T> mergeDelayError( + @NonNull SingleSource<? extends T> source1, @NonNull SingleSource<? extends T> source2, + @NonNull SingleSource<? extends T> source3, @NonNull SingleSource<? extends T> source4 + ) { + Objects.requireNonNull(source1, "source1 is null"); + Objects.requireNonNull(source2, "source2 is null"); + Objects.requireNonNull(source3, "source3 is null"); + Objects.requireNonNull(source4, "source4 is null"); + return Flowable.fromArray(source1, source2, source3, source4).flatMapSingle(Functions.identity(), true, Integer.MAX_VALUE); + } + + /** + * Returns a singleton instance of a never-signaling {@code Single} (only calls {@code onSubscribe}). + * <p> + * <img width="640" height="244" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.never.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code never} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <T> the target value type + * @return the singleton never instance + * @since 2.0 + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @SuppressWarnings("unchecked") + @NonNull + public static <@NonNull T> Single<T> never() { + return RxJavaPlugins.onAssembly((Single<T>) SingleNever.INSTANCE); + } + + /** + * Signals success with 0L value after the given delay when a {@link SingleObserver} subscribes. + * <p> + * <img width="640" height="292" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.timer.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code timer} operates by default on the {@code computation} {@link Scheduler}.</dd> + * </dl> + * @param delay the delay amount + * @param unit the time unit of the delay + * @return the new {@code Single} instance + * @throws NullPointerException if {@code unit} is {@code null} + * @since 2.0 + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.COMPUTATION) + @NonNull + public static Single<Long> timer(long delay, @NonNull TimeUnit unit) { + return timer(delay, unit, Schedulers.computation()); + } + + /** + * Signals success with 0L value on the specified {@link Scheduler} after the given + * delay when a {@link SingleObserver} subscribes. + * <p> + * <img width="640" height="292" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.timer.s.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>you specify the {@code Scheduler} to signal on.</dd> + * </dl> + * @param delay the delay amount + * @param unit the time unit of the delay + * @param scheduler the {@code Scheduler} where the single 0L will be emitted + * @return the new {@code Single} instance + * @throws NullPointerException + * if {@code unit} is {@code null}, or + * if {@code scheduler} is {@code null} + * @since 2.0 + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.CUSTOM) + public static Single<Long> timer(long delay, @NonNull TimeUnit unit, @NonNull Scheduler scheduler) { + Objects.requireNonNull(unit, "unit is null"); + Objects.requireNonNull(scheduler, "scheduler is null"); + return RxJavaPlugins.onAssembly(new SingleTimer(delay, unit, scheduler)); + } + + /** + * Compares two {@link SingleSource}s and emits {@code true} if they emit the same value (compared via {@link Object#equals(Object)}). + * <p> + * <img width="640" height="465" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.equals.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code sequenceEqual} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <T> the common value type + * @param source1 the first {@code SingleSource} instance + * @param source2 the second {@code SingleSource} instance + * @return the new {@code Single} instance + * @throws NullPointerException if {@code source1} or {@code source2} is {@code null} + * @since 2.0 + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T> Single<Boolean> sequenceEqual(@NonNull SingleSource<? extends T> source1, @NonNull SingleSource<? extends T> source2) { // NOPMD + Objects.requireNonNull(source1, "source1 is null"); + Objects.requireNonNull(source2, "source2 is null"); + return RxJavaPlugins.onAssembly(new SingleEquals<>(source1, source2)); + } + + /** + * Switches between {@link SingleSource}s emitted by the source {@link Publisher} whenever + * a new {@code SingleSource} is emitted, disposing the previously running {@code SingleSource}, + * exposing the success items as a {@link Flowable} sequence. + * <p> + * <img width="640" height="521" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.switchOnNext.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The {@code sources} {@code Publisher} is consumed in an unbounded manner (requesting {@link Long#MAX_VALUE}). + * The returned {@code Flowable} respects the backpressure from the downstream.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code switchOnNext} does not operate by default on a particular {@link Scheduler}.</dd> + * <dt><b>Error handling:</b></dt> + * <dd>The returned sequence fails with the first error signaled by the {@code sources} {@code Publisher} + * or the currently running {@code SingleSource}, disposing the rest. Late errors are + * forwarded to the global error handler via {@link RxJavaPlugins#onError(Throwable)}.</dd> + * </dl> + * @param <T> the element type of the {@code SingleSource}s + * @param sources the {@code Publisher} sequence of inner {@code SingleSource}s to switch between + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code sources} is {@code null} + * @since 3.0.0 + * @see #switchOnNextDelayError(Publisher) + * @see <a href="http://reactivex.io/documentation/operators/switch.html">ReactiveX operators documentation: Switch</a> + */ + @BackpressureSupport(BackpressureKind.FULL) + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T> Flowable<T> switchOnNext(@NonNull Publisher<@NonNull ? extends SingleSource<? extends T>> sources) { + Objects.requireNonNull(sources, "sources is null"); + return RxJavaPlugins.onAssembly(new FlowableSwitchMapSinglePublisher<>(sources, Functions.identity(), false)); + } + + /** + * Switches between {@link SingleSource}s emitted by the source {@link Publisher} whenever + * a new {@code SingleSource} is emitted, disposing the previously running {@code SingleSource}, + * exposing the success items as a {@link Flowable} sequence and delaying all errors from + * all of them until all terminate. + * <p> + * <img width="640" height="423" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Maybe.switchOnNextDelayError.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The {@code sources} {@code Publisher} is consumed in an unbounded manner (requesting {@link Long#MAX_VALUE}). + * The returned {@code Flowable} respects the backpressure from the downstream.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code switchOnNextDelayError} does not operate by default on a particular {@link Scheduler}.</dd> + * <dt><b>Error handling:</b></dt> + * <dd>The returned {@code Flowable} collects all errors emitted by either the {@code sources} + * {@code Publisher} or any inner {@code SingleSource} and emits them as a {@link CompositeException} + * when all sources terminate. If only one source ever failed, its error is emitted as-is at the end.</dd> + * </dl> + * @param <T> the element type of the {@code SingleSource}s + * @param sources the {@code Publisher} sequence of inner {@code SingleSource}s to switch between + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code sources} is {@code null} + * @since 3.0.0 + * @see #switchOnNext(Publisher) + * @see <a href="http://reactivex.io/documentation/operators/switch.html">ReactiveX operators documentation: Switch</a> + */ + @BackpressureSupport(BackpressureKind.FULL) + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T> Flowable<T> switchOnNextDelayError(@NonNull Publisher<@NonNull ? extends SingleSource<? extends T>> sources) { + Objects.requireNonNull(sources, "sources is null"); + return RxJavaPlugins.onAssembly(new FlowableSwitchMapSinglePublisher<>(sources, Functions.identity(), true)); + } + + /** + * <strong>Advanced use only:</strong> creates a {@code Single} instance without + * any safeguards by using a callback that is called with a {@link SingleObserver}. + * <p> + * <img width="640" height="261" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.unsafeCreate.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code unsafeCreate} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <T> the value type + * @param onSubscribe the function that is called with the subscribing {@code SingleObserver} + * @return the new {@code Single} instance + * @throws NullPointerException if {@code onSubscribe} is {@code null} + * @throws IllegalArgumentException if {@code source} is a subclass of {@code Single}; such + * instances don't need conversion and is possibly a port remnant from 1.x or one should use {@link #hide()} + * instead. + * @since 2.0 + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T> Single<T> unsafeCreate(@NonNull SingleSource<T> onSubscribe) { + Objects.requireNonNull(onSubscribe, "onSubscribe is null"); + if (onSubscribe instanceof Single) { + throw new IllegalArgumentException("unsafeCreate(Single) should be upgraded"); + } + return RxJavaPlugins.onAssembly(new SingleFromUnsafeSource<>(onSubscribe)); + } + + /** + * Allows using and disposing a resource while running a {@link SingleSource} instance generated from + * that resource (similar to a try-with-resources). + * <p> + * <img width="640" height="380" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.using.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code using} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <T> the value type of the {@code SingleSource} generated + * @param <U> the resource type + * @param resourceSupplier the {@link Supplier} called for each {@link SingleObserver} to generate a resource object + * @param sourceSupplier the function called with the returned resource + * object from {@code resourceSupplier} and should return a {@code SingleSource} instance + * to be run by the operator + * @param resourceCleanup the consumer of the generated resource that is called exactly once for + * that particular resource when the generated {@code SingleSource} terminates + * (successfully or with an error) or gets disposed. + * @return the new {@code Single} instance + * @throws NullPointerException if {@code resourceSupplier}, {@code sourceSupplier} and {@code resourceCleanup} is {@code null} + * @since 2.0 + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public static <@NonNull T, @NonNull U> Single<T> using(@NonNull Supplier<U> resourceSupplier, + @NonNull Function<? super U, ? extends SingleSource<? extends T>> sourceSupplier, + @NonNull Consumer<? super U> resourceCleanup) { + return using(resourceSupplier, sourceSupplier, resourceCleanup, true); + } + + /** + * Allows using and disposing a resource while running a {@link SingleSource} instance generated from + * that resource (similar to a try-with-resources). + * <p> + * <img width="640" height="325" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.using.b.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code using} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <T> the value type of the {@code SingleSource} generated + * @param <U> the resource type + * @param resourceSupplier the {@link Supplier} called for each {@link SingleObserver} to generate a resource object + * @param sourceSupplier the function called with the returned resource + * object from {@code resourceSupplier} and should return a {@code SingleSource} instance + * to be run by the operator + * @param resourceCleanup the consumer of the generated resource that is called exactly once for + * that particular resource when the generated {@code SingleSource} terminates + * (successfully or with an error) or gets disposed. + * @param eager + * If {@code true} then resource disposal will happen either on a {@code dispose()} call before the upstream is disposed + * or just before the emission of a terminal event ({@code onSuccess} or {@code onError}). + * If {@code false} the resource disposal will happen either on a {@code dispose()} call after the upstream is disposed + * or just after the emission of a terminal event ({@code onSuccess} or {@code onError}). + * @return the new {@code Single} instance + * @throws NullPointerException if {@code resourceSupplier}, {@code sourceSupplier} or {@code resourceCleanup} is {@code null} + * @since 2.0 + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T, @NonNull U> Single<T> using( + @NonNull Supplier<U> resourceSupplier, + @NonNull Function<? super U, ? extends SingleSource<? extends T>> sourceSupplier, + @NonNull Consumer<? super U> resourceCleanup, + boolean eager) { + Objects.requireNonNull(resourceSupplier, "resourceSupplier is null"); + Objects.requireNonNull(sourceSupplier, "sourceSupplier is null"); + Objects.requireNonNull(resourceCleanup, "resourceCleanup is null"); + + return RxJavaPlugins.onAssembly(new SingleUsing<>(resourceSupplier, sourceSupplier, resourceCleanup, eager)); + } + + /** + * Wraps a {@link SingleSource} instance into a new {@code Single} instance if not already a {@code Single} + * instance. + * <p> + * <img width="640" height="350" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.wrap.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code wrap} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <T> the value type + * @param source the source to wrap + * @return the new {@code Single} instance + * @throws NullPointerException if {@code source} is {@code null} + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T> Single<T> wrap(@NonNull SingleSource<T> source) { + Objects.requireNonNull(source, "source is null"); + if (source instanceof Single) { + return RxJavaPlugins.onAssembly((Single<T>)source); + } + return RxJavaPlugins.onAssembly(new SingleFromUnsafeSource<>(source)); + } + + /** + * Waits until all {@link SingleSource} sources provided by the {@link Iterable} sequence signal a success + * value and calls a zipper function with an array of these values to return a result + * to be emitted to the downstream. + * <p> + * If the {@code Iterable} of {@code SingleSource}s is empty a {@link NoSuchElementException} error is signaled after subscription. + * <p> + * Note on method signature: since Java doesn't allow creating a generic array with {@code new T[]}, the + * implementation of this operator has to create an {@code Object[]} instead. Unfortunately, a + * {@code Function<Integer[], R>} passed to the method would trigger a {@link ClassCastException}. + * + * <p> + * <img width="640" height="340" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.zip.i.png" alt=""> + * <p> + * If any of the {@code SingleSources} signal an error, all other {@code SingleSource}s get disposed and the + * error emitted to downstream immediately. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code zip} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <T> the common value type + * @param <R> the result value type + * @param sources the {@code Iterable} sequence of {@code SingleSource} instances. An empty sequence will result in an + * {@code onError} signal of {@code NoSuchElementException}. + * @param zipper the function that receives an array with values from each {@code SingleSource} + * and should return a value to be emitted to downstream + * @return the new {@code Single} instance + * @throws NullPointerException if {@code zipper} or {@code sources} is {@code null} + * @since 2.0 + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T, @NonNull R> Single<R> zip(@NonNull Iterable<@NonNull ? extends SingleSource<? extends T>> sources, + @NonNull Function<? super Object[], ? extends R> zipper) { + Objects.requireNonNull(zipper, "zipper is null"); + Objects.requireNonNull(sources, "sources is null"); + return RxJavaPlugins.onAssembly(new SingleZipIterable<>(sources, zipper)); + } + + /** + * Returns a {@code Single} that emits the results of a specified combiner function applied to two items emitted by + * two other {@link SingleSource}s. + * <p> + * <img width="640" height="434" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.zip.n.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code zip} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T1> the first source {@code SingleSource}'s value type + * @param <T2> the second source {@code SingleSource}'s value type + * @param <R> the result value type + * @param source1 + * the first source {@code SingleSource} + * @param source2 + * a second source {@code SingleSource} + * @param zipper + * a function that, when applied to the item emitted by each of the source {@code SingleSource}s, results in an + * item that will be emitted by the resulting {@code Single} + * @return the new {@code Single} that emits the zipped results + * @throws NullPointerException if {@code source1}, {@code source2} or {@code zipper} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/zip.html">ReactiveX operators documentation: Zip</a> + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T1, @NonNull T2, @NonNull R> Single<R> zip( + @NonNull SingleSource<? extends T1> source1, @NonNull SingleSource<? extends T2> source2, + @NonNull BiFunction<? super T1, ? super T2, ? extends R> zipper + ) { + Objects.requireNonNull(source1, "source1 is null"); + Objects.requireNonNull(source2, "source2 is null"); + Objects.requireNonNull(zipper, "zipper is null"); + return zipArray(Functions.toFunction(zipper), source1, source2); + } + + /** + * Returns a {@code Single} that emits the results of a specified combiner function applied to three items emitted + * by three other {@link SingleSource}s. + * <p> + * <img width="640" height="434" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.zip.n.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code zip} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T1> the first source {@code SingleSource}'s value type + * @param <T2> the second source {@code SingleSource}'s value type + * @param <T3> the third source {@code SingleSource}'s value type + * @param <R> the result value type + * @param source1 + * the first source {@code SingleSource} + * @param source2 + * a second source {@code SingleSource} + * @param source3 + * a third source {@code SingleSource} + * @param zipper + * a function that, when applied to the item emitted by each of the source {@code SingleSource}s, results in an + * item that will be emitted by the resulting {@code Single} + * @return the new {@code Single} that emits the zipped results + * @throws NullPointerException if {@code source1}, {@code source2}, {@code source3} or {@code zipper} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/zip.html">ReactiveX operators documentation: Zip</a> + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T1, @NonNull T2, @NonNull T3, @NonNull R> Single<R> zip( + @NonNull SingleSource<? extends T1> source1, @NonNull SingleSource<? extends T2> source2, + @NonNull SingleSource<? extends T3> source3, + @NonNull Function3<? super T1, ? super T2, ? super T3, ? extends R> zipper + ) { + Objects.requireNonNull(source1, "source1 is null"); + Objects.requireNonNull(source2, "source2 is null"); + Objects.requireNonNull(source3, "source3 is null"); + Objects.requireNonNull(zipper, "zipper is null"); + return zipArray(Functions.toFunction(zipper), source1, source2, source3); + } + + /** + * Returns a {@code Single} that emits the results of a specified combiner function applied to four items + * emitted by four other {@link SingleSource}s. + * <p> + * <img width="640" height="434" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.zip.n.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code zip} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T1> the first source {@code SingleSource}'s value type + * @param <T2> the second source {@code SingleSource}'s value type + * @param <T3> the third source {@code SingleSource}'s value type + * @param <T4> the fourth source {@code SingleSource}'s value type + * @param <R> the result value type + * @param source1 + * the first source {@code SingleSource} + * @param source2 + * a second source {@code SingleSource} + * @param source3 + * a third source {@code SingleSource} + * @param source4 + * a fourth source {@code SingleSource} + * @param zipper + * a function that, when applied to the item emitted by each of the source {@code SingleSource}s, results in an + * item that will be emitted by the resulting {@code Single} + * @return the new {@code Single} that emits the zipped results + * @throws NullPointerException if {@code source1}, {@code source2}, {@code source3}, {@code source4} or {@code zipper} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/zip.html">ReactiveX operators documentation: Zip</a> + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T1, @NonNull T2, @NonNull T3, @NonNull T4, @NonNull R> Single<R> zip( + @NonNull SingleSource<? extends T1> source1, @NonNull SingleSource<? extends T2> source2, + @NonNull SingleSource<? extends T3> source3, @NonNull SingleSource<? extends T4> source4, + @NonNull Function4<? super T1, ? super T2, ? super T3, ? super T4, ? extends R> zipper + ) { + Objects.requireNonNull(source1, "source1 is null"); + Objects.requireNonNull(source2, "source2 is null"); + Objects.requireNonNull(source3, "source3 is null"); + Objects.requireNonNull(source4, "source4 is null"); + Objects.requireNonNull(zipper, "zipper is null"); + return zipArray(Functions.toFunction(zipper), source1, source2, source3, source4); + } + + /** + * Returns a {@code Single} that emits the results of a specified combiner function applied to five items + * emitted by five other {@link SingleSource}s. + * <p> + * <img width="640" height="434" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.zip.n.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code zip} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T1> the first source {@code SingleSource}'s value type + * @param <T2> the second source {@code SingleSource}'s value type + * @param <T3> the third source {@code SingleSource}'s value type + * @param <T4> the fourth source {@code SingleSource}'s value type + * @param <T5> the fifth source {@code SingleSource}'s value type + * @param <R> the result value type + * @param source1 + * the first source {@code SingleSource} + * @param source2 + * a second source {@code SingleSource} + * @param source3 + * a third source {@code SingleSource} + * @param source4 + * a fourth source {@code SingleSource} + * @param source5 + * a fifth source {@code SingleSource} + * @param zipper + * a function that, when applied to the item emitted by each of the source {@code SingleSource}s, results in an + * item that will be emitted by the resulting {@code Single} + * @return the new {@code Single} that emits the zipped results + * @throws NullPointerException if {@code source1}, {@code source2}, {@code source3}, {@code source4} + * {@code source5} or {@code zipper} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/zip.html">ReactiveX operators documentation: Zip</a> + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T1, @NonNull T2, @NonNull T3, @NonNull T4, @NonNull T5, @NonNull R> Single<R> zip( + @NonNull SingleSource<? extends T1> source1, @NonNull SingleSource<? extends T2> source2, + @NonNull SingleSource<? extends T3> source3, @NonNull SingleSource<? extends T4> source4, + @NonNull SingleSource<? extends T5> source5, + @NonNull Function5<? super T1, ? super T2, ? super T3, ? super T4, ? super T5, ? extends R> zipper + ) { + Objects.requireNonNull(source1, "source1 is null"); + Objects.requireNonNull(source2, "source2 is null"); + Objects.requireNonNull(source3, "source3 is null"); + Objects.requireNonNull(source4, "source4 is null"); + Objects.requireNonNull(source5, "source5 is null"); + Objects.requireNonNull(zipper, "zipper is null"); + return zipArray(Functions.toFunction(zipper), source1, source2, source3, source4, source5); + } + + /** + * Returns a {@code Single} that emits the results of a specified combiner function applied to six items + * emitted by six other {@link SingleSource}s. + * <p> + * <img width="640" height="434" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.zip.n.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code zip} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T1> the first source {@code SingleSource}'s value type + * @param <T2> the second source {@code SingleSource}'s value type + * @param <T3> the third source {@code SingleSource}'s value type + * @param <T4> the fourth source {@code SingleSource}'s value type + * @param <T5> the fifth source {@code SingleSource}'s value type + * @param <T6> the sixth source {@code SingleSource}'s value type + * @param <R> the result value type + * @param source1 + * the first source {@code SingleSource} + * @param source2 + * a second source {@code SingleSource} + * @param source3 + * a third source {@code SingleSource} + * @param source4 + * a fourth source {@code SingleSource} + * @param source5 + * a fifth source {@code SingleSource} + * @param source6 + * a sixth source {@code SingleSource} + * @param zipper + * a function that, when applied to the item emitted by each of the source {@code SingleSource}s, results in an + * item that will be emitted by the resulting {@code Single} + * @return the new {@code Single} that emits the zipped results + * @throws NullPointerException if {@code source1}, {@code source2}, {@code source3}, {@code source4} + * {@code source5}, {@code source6} or {@code zipper} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/zip.html">ReactiveX operators documentation: Zip</a> + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T1, @NonNull T2, @NonNull T3, @NonNull T4, @NonNull T5, @NonNull T6, @NonNull R> Single<R> zip( + @NonNull SingleSource<? extends T1> source1, @NonNull SingleSource<? extends T2> source2, + @NonNull SingleSource<? extends T3> source3, @NonNull SingleSource<? extends T4> source4, + @NonNull SingleSource<? extends T5> source5, @NonNull SingleSource<? extends T6> source6, + @NonNull Function6<? super T1, ? super T2, ? super T3, ? super T4, ? super T5, ? super T6, ? extends R> zipper + ) { + Objects.requireNonNull(source1, "source1 is null"); + Objects.requireNonNull(source2, "source2 is null"); + Objects.requireNonNull(source3, "source3 is null"); + Objects.requireNonNull(source4, "source4 is null"); + Objects.requireNonNull(source5, "source5 is null"); + Objects.requireNonNull(source6, "source6 is null"); + Objects.requireNonNull(zipper, "zipper is null"); + return zipArray(Functions.toFunction(zipper), source1, source2, source3, source4, source5, source6); + } + + /** + * Returns a {@code Single} that emits the results of a specified combiner function applied to seven items + * emitted by seven other {@link SingleSource}s. + * <p> + * <img width="640" height="434" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.zip.n.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code zip} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T1> the first source {@code SingleSource}'s value type + * @param <T2> the second source {@code SingleSource}'s value type + * @param <T3> the third source {@code SingleSource}'s value type + * @param <T4> the fourth source {@code SingleSource}'s value type + * @param <T5> the fifth source {@code SingleSource}'s value type + * @param <T6> the sixth source {@code SingleSource}'s value type + * @param <T7> the seventh source {@code SingleSource}'s value type + * @param <R> the result value type + * @param source1 + * the first source {@code SingleSource} + * @param source2 + * a second source {@code SingleSource} + * @param source3 + * a third source {@code SingleSource} + * @param source4 + * a fourth source {@code SingleSource} + * @param source5 + * a fifth source {@code SingleSource} + * @param source6 + * a sixth source {@code SingleSource} + * @param source7 + * a seventh source {@code SingleSource} + * @param zipper + * a function that, when applied to the item emitted by each of the source {@code SingleSource}s, results in an + * item that will be emitted by the resulting {@code Single} + * @return the new {@code Single} that emits the zipped results + * @throws NullPointerException if {@code source1}, {@code source2}, {@code source3}, {@code source4} + * {@code source5}, {@code source6}, {@code source7} or {@code zipper} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/zip.html">ReactiveX operators documentation: Zip</a> + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T1, @NonNull T2, @NonNull T3, @NonNull T4, @NonNull T5, @NonNull T6, @NonNull T7, @NonNull R> Single<R> zip( + @NonNull SingleSource<? extends T1> source1, @NonNull SingleSource<? extends T2> source2, + @NonNull SingleSource<? extends T3> source3, @NonNull SingleSource<? extends T4> source4, + @NonNull SingleSource<? extends T5> source5, @NonNull SingleSource<? extends T6> source6, + @NonNull SingleSource<? extends T7> source7, + @NonNull Function7<? super T1, ? super T2, ? super T3, ? super T4, ? super T5, ? super T6, ? super T7, ? extends R> zipper + ) { + Objects.requireNonNull(source1, "source1 is null"); + Objects.requireNonNull(source2, "source2 is null"); + Objects.requireNonNull(source3, "source3 is null"); + Objects.requireNonNull(source4, "source4 is null"); + Objects.requireNonNull(source5, "source5 is null"); + Objects.requireNonNull(source6, "source6 is null"); + Objects.requireNonNull(source7, "source7 is null"); + Objects.requireNonNull(zipper, "zipper is null"); + return zipArray(Functions.toFunction(zipper), source1, source2, source3, source4, source5, source6, source7); + } + + /** + * Returns a {@code Single} that emits the results of a specified combiner function applied to eight items + * emitted by eight other {@link SingleSource}s. + * <p> + * <img width="640" height="434" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.zip.n.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code zip} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T1> the first source {@code SingleSource}'s value type + * @param <T2> the second source {@code SingleSource}'s value type + * @param <T3> the third source {@code SingleSource}'s value type + * @param <T4> the fourth source {@code SingleSource}'s value type + * @param <T5> the fifth source {@code SingleSource}'s value type + * @param <T6> the sixth source {@code SingleSource}'s value type + * @param <T7> the seventh source {@code SingleSource}'s value type + * @param <T8> the eighth source {@code SingleSource}'s value type + * @param <R> the result value type + * @param source1 + * the first source {@code SingleSource} + * @param source2 + * a second source {@code SingleSource} + * @param source3 + * a third source {@code SingleSource} + * @param source4 + * a fourth source {@code SingleSource} + * @param source5 + * a fifth source {@code SingleSource} + * @param source6 + * a sixth source {@code SingleSource} + * @param source7 + * a seventh source {@code SingleSource} + * @param source8 + * an eighth source {@code SingleSource} + * @param zipper + * a function that, when applied to the item emitted by each of the source {@code SingleSource}s, results in an + * item that will be emitted by the resulting {@code Single} + * @return the new {@code Single} that emits the zipped results + * @throws NullPointerException if {@code source1}, {@code source2}, {@code source3}, {@code source4} + * {@code source5}, {@code source6}, {@code source7}, {@code source8} or {@code zipper} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/zip.html">ReactiveX operators documentation: Zip</a> + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T1, @NonNull T2, @NonNull T3, @NonNull T4, @NonNull T5, @NonNull T6, @NonNull T7, @NonNull T8, @NonNull R> Single<R> zip( + @NonNull SingleSource<? extends T1> source1, @NonNull SingleSource<? extends T2> source2, + @NonNull SingleSource<? extends T3> source3, @NonNull SingleSource<? extends T4> source4, + @NonNull SingleSource<? extends T5> source5, @NonNull SingleSource<? extends T6> source6, + @NonNull SingleSource<? extends T7> source7, @NonNull SingleSource<? extends T8> source8, + @NonNull Function8<? super T1, ? super T2, ? super T3, ? super T4, ? super T5, ? super T6, ? super T7, ? super T8, ? extends R> zipper + ) { + Objects.requireNonNull(source1, "source1 is null"); + Objects.requireNonNull(source2, "source2 is null"); + Objects.requireNonNull(source3, "source3 is null"); + Objects.requireNonNull(source4, "source4 is null"); + Objects.requireNonNull(source5, "source5 is null"); + Objects.requireNonNull(source6, "source6 is null"); + Objects.requireNonNull(source7, "source7 is null"); + Objects.requireNonNull(source8, "source8 is null"); + Objects.requireNonNull(zipper, "zipper is null"); + return zipArray(Functions.toFunction(zipper), source1, source2, source3, source4, source5, source6, source7, source8); + } + + /** + * Returns a {@code Single} that emits the results of a specified combiner function applied to nine items + * emitted by nine other {@link SingleSource}s. + * <p> + * <img width="640" height="434" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.zip.n.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code zip} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T1> the first source {@code SingleSource}'s value type + * @param <T2> the second source {@code SingleSource}'s value type + * @param <T3> the third source {@code SingleSource}'s value type + * @param <T4> the fourth source {@code SingleSource}'s value type + * @param <T5> the fifth source {@code SingleSource}'s value type + * @param <T6> the sixth source {@code SingleSource}'s value type + * @param <T7> the seventh source {@code SingleSource}'s value type + * @param <T8> the eighth source {@code SingleSource}'s value type + * @param <T9> the ninth source {@code SingleSource}'s value type + * @param <R> the result value type + * @param source1 + * the first source {@code SingleSource} + * @param source2 + * a second source {@code SingleSource} + * @param source3 + * a third source {@code SingleSource} + * @param source4 + * a fourth source {@code SingleSource} + * @param source5 + * a fifth source {@code SingleSource} + * @param source6 + * a sixth source {@code SingleSource} + * @param source7 + * a seventh source {@code SingleSource} + * @param source8 + * an eighth source {@code SingleSource} + * @param source9 + * a ninth source {@code SingleSource} + * @param zipper + * a function that, when applied to the item emitted by each of the source {@code SingleSource}s, results in an + * item that will be emitted by the resulting {@code Single} + * @return the new {@code Single} that emits the zipped results + * @throws NullPointerException if {@code source1}, {@code source2}, {@code source3}, {@code source4} + * {@code source5}, {@code source6}, {@code source7}, {@code source8}, + * {@code source9} or {@code zipper} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/zip.html">ReactiveX operators documentation: Zip</a> + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T1, @NonNull T2, @NonNull T3, @NonNull T4, @NonNull T5, @NonNull T6, @NonNull T7, @NonNull T8, @NonNull T9, @NonNull R> Single<R> zip( + @NonNull SingleSource<? extends T1> source1, @NonNull SingleSource<? extends T2> source2, + @NonNull SingleSource<? extends T3> source3, @NonNull SingleSource<? extends T4> source4, + @NonNull SingleSource<? extends T5> source5, @NonNull SingleSource<? extends T6> source6, + @NonNull SingleSource<? extends T7> source7, @NonNull SingleSource<? extends T8> source8, + @NonNull SingleSource<? extends T9> source9, + @NonNull Function9<? super T1, ? super T2, ? super T3, ? super T4, ? super T5, ? super T6, ? super T7, ? super T8, ? super T9, ? extends R> zipper + ) { + Objects.requireNonNull(source1, "source1 is null"); + Objects.requireNonNull(source2, "source2 is null"); + Objects.requireNonNull(source3, "source3 is null"); + Objects.requireNonNull(source4, "source4 is null"); + Objects.requireNonNull(source5, "source5 is null"); + Objects.requireNonNull(source6, "source6 is null"); + Objects.requireNonNull(source7, "source7 is null"); + Objects.requireNonNull(source8, "source8 is null"); + Objects.requireNonNull(source9, "source9 is null"); + Objects.requireNonNull(zipper, "zipper is null"); + return zipArray(Functions.toFunction(zipper), source1, source2, source3, source4, source5, source6, source7, source8, source9); + } + + /** + * Waits until all {@link SingleSource} sources provided via an array signal a success + * value and calls a zipper function with an array of these values to return a result + * to be emitted to downstream. + * <p> + * <img width="640" height="340" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.zipArray.png" alt=""> + * <p> + * If the array of {@code SingleSource}s is empty a {@link NoSuchElementException} error is signaled immediately. + * <p> + * Note on method signature: since Java doesn't allow creating a generic array with {@code new T[]}, the + * implementation of this operator has to create an {@code Object[]} instead. Unfortunately, a + * {@code Function<Integer[], R>} passed to the method would trigger a {@link ClassCastException}. + * <p> + * If any of the {@code SingleSource}s signal an error, all other {@code SingleSource}s get disposed and the + * error emitted to downstream immediately. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code zipArray} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <T> the common value type + * @param <R> the result value type + * @param sources the array of {@code SingleSource} instances. An empty sequence will result in an + * {@code onError} signal of {@code NoSuchElementException}. + * @param zipper the function that receives an array with values from each {@code SingleSource} + * and should return a value to be emitted to downstream + * @return the new {@code Single} instance + * @throws NullPointerException if {@code zipper} or {@code sources} is {@code null} + * @since 2.0 + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + @SafeVarargs + public static <@NonNull T, @NonNull R> Single<R> zipArray(@NonNull Function<? super Object[], ? extends R> zipper, @NonNull SingleSource<? extends T>... sources) { + Objects.requireNonNull(zipper, "zipper is null"); + Objects.requireNonNull(sources, "sources is null"); + if (sources.length == 0) { + return error(new NoSuchElementException()); + } + return RxJavaPlugins.onAssembly(new SingleZipArray<>(sources, zipper)); + } + + /** + * Signals the event of this or the other {@link SingleSource} whichever signals first. + * <p> + * <img width="640" height="463" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.ambWith.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code ambWith} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param other the other {@code SingleSource} to race for the first emission of success or error + * @return the new {@code Single} instance. A subscription to this provided source will occur after subscribing + * to the current source. + * @throws NullPointerException if {@code other} is {@code null} + * @since 2.0 + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public final Single<T> ambWith(@NonNull SingleSource<? extends T> other) { + Objects.requireNonNull(other, "other is null"); + return ambArray(this, other); + } + + /** + * Hides the identity of the current {@code Single}, including the {@link Disposable} that is sent + * to the downstream via {@code onSubscribe()}. + * <p> + * <img width="640" height="458" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.hide.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code hide} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @return the new {@code Single} instance + * @since 2.0 + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Single<T> hide() { + return RxJavaPlugins.onAssembly(new SingleHide<>(this)); + } + + /** + * Transform a {@code Single} by applying a particular {@link SingleTransformer} function to it. + * <p> + * <img width="640" height="612" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.compose.v3.png" alt=""> + * <p> + * This method operates on the {@code Single} itself whereas {@link #lift} operates on {@link SingleObserver}s. + * <p> + * If the operator you are creating is designed to act on the individual item emitted by a {@code Single}, use + * {@link #lift}. If your operator is designed to transform the current {@code Single} as a whole (for instance, by + * applying a particular set of existing RxJava operators to it) use {@code compose}. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code compose} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <R> the value type of the single returned by the transformer function + * @param transformer the transformer function, not {@code null} + * @return the new {@code Single} instance + * @throws NullPointerException if {@code transformer} is {@code null} + * @see <a href="https://github.com/ReactiveX/RxJava/wiki/Implementing-Your-Own-Operators">RxJava wiki: Implementing Your Own Operators</a> + */ + @SuppressWarnings("unchecked") + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull R> Single<R> compose(@NonNull SingleTransformer<? super T, ? extends R> transformer) { + return wrap(((SingleTransformer<T, R>) Objects.requireNonNull(transformer, "transformer is null")).apply(this)); + } + + /** + * Stores the success value or exception from the current {@code Single} and replays it to late {@link SingleObserver}s. + * <p> + * <img width="640" height="363" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.cache.png" alt=""> + * <p> + * The returned {@code Single} subscribes to the current {@code Single} when the first {@code SingleObserver} subscribes. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code cache} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @return the new {@code Single} instance + * @since 2.0 + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Single<T> cache() { + return RxJavaPlugins.onAssembly(new SingleCache<>(this)); + } + + /** + * Casts the success value of the current {@code Single} into the target type or signals a + * {@link ClassCastException} if not compatible. + * <p> + * <img width="640" height="393" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.cast.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code cast} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <U> the target type + * @param clazz the type token to use for casting the success result from the current {@code Single} + * @return the new {@code Single} instance + * @throws NullPointerException if {@code clazz} is {@code null} + * @since 2.0 + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public final <@NonNull U> Single<U> cast(@NonNull Class<? extends U> clazz) { + Objects.requireNonNull(clazz, "clazz is null"); + return map(Functions.castFunction(clazz)); + } + + /** + * Returns a {@code Single} that is based on applying a specified function to the item emitted by the current {@code Single}, + * where that function returns a {@link SingleSource}. + * <p> + * <img width="640" height="313" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.concatMap.png" alt=""> + * <p> + * The operator is an alias for {@link #flatMap(Function)} + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code concatMap} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <R> the result value type + * @param mapper + * a function that, when applied to the item emitted by the current {@code Single}, returns a {@code SingleSource} + * @return the new {@code Single} returned from {@code mapper} when applied to the item emitted by the current {@code Single} + * @throws NullPointerException if {@code mapper} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/flatmap.html">ReactiveX operators documentation: FlatMap</a> + * @since 3.0.0 + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public final <@NonNull R> Single<R> concatMap(@NonNull Function<? super T, ? extends SingleSource<? extends R>> mapper) { + Objects.requireNonNull(mapper, "mapper is null"); + return RxJavaPlugins.onAssembly(new SingleFlatMap<>(this, mapper)); + } + + /** + * Returns a {@link Completable} that completes based on applying a specified function to the item emitted by the + * current {@code Single}, where that function returns a {@link CompletableSource}. + * <p> + * <img width="640" height="298" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.concatMapCompletable.png" alt=""> + * <p> + * The operator is an alias for {@link #flatMapCompletable(Function)}. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code concatMapCompletable} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param mapper + * a function that, when applied to the item emitted by the current {@code Single}, returns a + * {@code CompletableSource} + * @return the new {@code Completable} instance + * @throws NullPointerException if {@code mapper} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/flatmap.html">ReactiveX operators documentation: FlatMap</a> + * @since 3.0.0 + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public final Completable concatMapCompletable(@NonNull Function<? super T, ? extends CompletableSource> mapper) { + return flatMapCompletable(mapper); + } + + /** + * Returns a {@link Maybe} that is based on applying a specified function to the item emitted by the current {@code Single}, + * where that function returns a {@link MaybeSource}. + * <p> + * <img width="640" height="254" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.concatMapMaybe.png" alt=""> + * <p> + * The operator is an alias for {@link #flatMapMaybe(Function)}. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code concatMapMaybe} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <R> the result value type + * @param mapper + * a function that, when applied to the item emitted by the current {@code Single}, returns a {@code MaybeSource} + * @return the new {@code Maybe} returned from {@code mapper} when applied to the item emitted by the current {@code Single} + * @throws NullPointerException if {@code mapper} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/flatmap.html">ReactiveX operators documentation: FlatMap</a> + * @since 3.0.0 + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public final <@NonNull R> Maybe<R> concatMapMaybe(@NonNull Function<? super T, ? extends MaybeSource<? extends R>> mapper) { + return flatMapMaybe(mapper); + } + + /** + * Returns a {@link Flowable} that emits the item emitted by the current {@code Single}, then the item emitted by the + * specified {@link SingleSource}. + * <p> + * <img width="640" height="335" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.concatWith.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The returned {@code Flowable} honors the backpressure of the downstream consumer.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code concatWith} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param other + * a {@code SingleSource} to be concatenated after the current + * @return the new {@code Flowable} that emits the item emitted by the current {@code Single}, followed by the item emitted by + * {@code other} + * @throws NullPointerException if {@code other} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/concat.html">ReactiveX operators documentation: Concat</a> + */ + @BackpressureSupport(BackpressureKind.FULL) + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Flowable<T> concatWith(@NonNull SingleSource<? extends T> other) { + return concat(this, other); + } + + /** + * Delays the emission of the success signal from the current {@code Single} by the specified amount. + * An error signal will not be delayed. + * <p> + * <img width="640" height="457" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.delay.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code delay} operates by default on the {@code computation} {@link Scheduler}.</dd> + * </dl> + * + * @param time the amount of time the success signal should be delayed for + * @param unit the time unit + * @return the new {@code Single} instance + * @since 2.0 + * @throws NullPointerException if {@code unit} is {@code null} + * @see #delay(long, TimeUnit, boolean) + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.COMPUTATION) + @NonNull + public final Single<T> delay(long time, @NonNull TimeUnit unit) { + return delay(time, unit, Schedulers.computation(), false); + } + + /** + * Delays the emission of the success or error signal from the current {@code Single} by the specified amount. + * <p> + * <img width="640" height="457" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.delay.e.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code delay} operates by default on the {@code computation} {@link Scheduler}.</dd> + * </dl> + * <p>History: 2.1.5 - experimental + * @param time the amount of time the success or error signal should be delayed for + * @param unit the time unit + * @param delayError if {@code true}, both success and error signals are delayed. if {@code false}, only success signals are delayed. + * @return the new {@code Single} instance + * @throws NullPointerException if {@code unit} is {@code null} + * @since 2.2 + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.COMPUTATION) + @NonNull + public final Single<T> delay(long time, @NonNull TimeUnit unit, boolean delayError) { + return delay(time, unit, Schedulers.computation(), delayError); + } + + /** + * Delays the emission of the success signal from the current {@code Single} by the specified amount. + * An error signal will not be delayed. + * <p> + * <img width="640" height="457" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.delay.s.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>you specify the {@link Scheduler} where the non-blocking wait and emission happens</dd> + * </dl> + * + * @param time the amount of time the success signal should be delayed for + * @param unit the time unit + * @param scheduler the target scheduler to use for the non-blocking wait and emission + * @return the new {@code Single} instance + * @throws NullPointerException + * if {@code unit} is {@code null}, or + * if {@code scheduler} is {@code null} + * @since 2.0 + * @see #delay(long, TimeUnit, Scheduler, boolean) + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.CUSTOM) + @NonNull + public final Single<T> delay(long time, @NonNull TimeUnit unit, @NonNull Scheduler scheduler) { + return delay(time, unit, scheduler, false); + } + + /** + * Delays the emission of the success or error signal from the current {@code Single} by the specified amount. + * <p> + * <img width="640" height="457" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.delay.se.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>you specify the {@link Scheduler} where the non-blocking wait and emission happens</dd> + * </dl> + * <p>History: 2.1.5 - experimental + * @param time the amount of time the success or error signal should be delayed for + * @param unit the time unit + * @param scheduler the target scheduler to use for the non-blocking wait and emission + * @param delayError if {@code true}, both success and error signals are delayed. if {@code false}, only success signals are delayed. + * @return the new {@code Single} instance + * @throws NullPointerException + * if {@code unit} is {@code null}, or + * if {@code scheduler} is {@code null} + * @since 2.2 + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.CUSTOM) + public final Single<T> delay(long time, @NonNull TimeUnit unit, @NonNull Scheduler scheduler, boolean delayError) { + Objects.requireNonNull(unit, "unit is null"); + Objects.requireNonNull(scheduler, "scheduler is null"); + return RxJavaPlugins.onAssembly(new SingleDelay<>(this, time, unit, scheduler, delayError)); + } + + /** + * Delays the actual subscription to the current {@code Single} until the given other {@link CompletableSource} + * completes. + * <p> + * <img width="640" height="309" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.delaySubscription.c.png" alt=""> + * <p>If the delaying source signals an error, that error is re-emitted and no subscription + * to the current {@code Single} happens. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code delaySubscription} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param subscriptionIndicator the {@code CompletableSource} that has to complete before the subscription to the + * current {@code Single} happens + * @return the new {@code Single} instance + * @throws NullPointerException if {@code subscriptionIndicator} is {@code null} + * @since 2.0 + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public final Single<T> delaySubscription(@NonNull CompletableSource subscriptionIndicator) { + Objects.requireNonNull(subscriptionIndicator, "subscriptionIndicator is null"); + return RxJavaPlugins.onAssembly(new SingleDelayWithCompletable<>(this, subscriptionIndicator)); + } + + /** + * Delays the actual subscription to the current {@code Single} until the given other {@link SingleSource} + * signals success. + * <p> + * <img width="640" height="309" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.delaySubscription.s.png" alt=""> + * <p>If the delaying source signals an error, that error is re-emitted and no subscription + * to the current {@code Single} happens. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code delaySubscription} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <U> the element type of the other source + * @param subscriptionIndicator the {@code SingleSource} that has to complete before the subscription to the + * current {@code Single} happens + * @return the new {@code Single} instance + * @throws NullPointerException if {@code subscriptionIndicator} is {@code null} + * @since 2.0 + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public final <@NonNull U> Single<T> delaySubscription(@NonNull SingleSource<U> subscriptionIndicator) { + Objects.requireNonNull(subscriptionIndicator, "subscriptionIndicator is null"); + return RxJavaPlugins.onAssembly(new SingleDelayWithSingle<>(this, subscriptionIndicator)); + } + + /** + * Delays the actual subscription to the current {@code Single} until the given other {@link ObservableSource} + * signals its first value or completes. + * <p> + * <img width="640" height="214" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.delaySubscription.o.png" alt=""> + * <p>If the delaying source signals an error, that error is re-emitted and no subscription + * to the current {@code Single} happens. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code delaySubscription} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <U> the element type of the other source + * @param subscriptionIndicator the {@code ObservableSource} that has to signal a value or complete before the + * subscription to the current {@code Single} happens + * @return the new {@code Single} instance + * @throws NullPointerException if {@code subscriptionIndicator} is {@code null} + * @since 2.0 + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public final <@NonNull U> Single<T> delaySubscription(@NonNull ObservableSource<U> subscriptionIndicator) { + Objects.requireNonNull(subscriptionIndicator, "subscriptionIndicator is null"); + return RxJavaPlugins.onAssembly(new SingleDelayWithObservable<>(this, subscriptionIndicator)); + } + + /** + * Delays the actual subscription to the current {@code Single} until the given other {@link Publisher} + * signals its first value or completes. + * <p> + * <img width="640" height="214" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.delaySubscription.p.png" alt=""> + * <p>If the delaying source signals an error, that error is re-emitted and no subscription + * to the current {@code Single} happens. + * <p>The other source is consumed in an unbounded manner (requesting {@link Long#MAX_VALUE} from it). + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The {@code other} publisher is consumed in an unbounded fashion but will be + * cancelled after the first item it produced.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code delaySubscription} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <U> the element type of the other source + * @param subscriptionIndicator the {@code Publisher} that has to signal a value or complete before the + * subscription to the current {@code Single} happens + * @return the new {@code Single} instance + * @throws NullPointerException if {@code subscriptionIndicator} is {@code null} + * @since 2.0 + */ + @BackpressureSupport(BackpressureKind.FULL) + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public final <@NonNull U> Single<T> delaySubscription(@NonNull Publisher<U> subscriptionIndicator) { + Objects.requireNonNull(subscriptionIndicator, "subscriptionIndicator is null"); + return RxJavaPlugins.onAssembly(new SingleDelayWithPublisher<>(this, subscriptionIndicator)); + } + + /** + * Delays the actual subscription to the current {@code Single} until the given time delay elapsed. + * <p> + * <img width="640" height="472" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.delaySubscription.t.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code delaySubscription} does by default subscribe to the current {@code Single} + * on the {@code computation} {@link Scheduler} after the delay.</dd> + * </dl> + * @param time the time amount to wait with the subscription + * @param unit the time unit of the waiting + * @return the new {@code Single} instance + * @throws NullPointerException if {@code unit} is {@code null} + * @since 2.0 + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.COMPUTATION) + @NonNull + public final Single<T> delaySubscription(long time, @NonNull TimeUnit unit) { + return delaySubscription(time, unit, Schedulers.computation()); + } + + /** + * Delays the actual subscription to the current {@code Single} until the given time delay elapsed. + * <p> + * <img width="640" height="420" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.delaySubscription.ts.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code delaySubscription} does by default subscribe to the current {@code Single} + * on the {@link Scheduler} you provided, after the delay.</dd> + * </dl> + * @param time the time amount to wait with the subscription + * @param unit the time unit of the waiting + * @param scheduler the {@code Scheduler} to wait on and subscribe on to the current {@code Single} + * @return the new {@code Single} instance + * @throws NullPointerException if {@code unit} or {@code scheduler} is {@code null} + * @since 2.0 + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.CUSTOM) + @NonNull + public final Single<T> delaySubscription(long time, @NonNull TimeUnit unit, @NonNull Scheduler scheduler) { + return delaySubscription(Observable.timer(time, unit, scheduler)); + } + + /** + * Maps the {@link Notification} success value of the current {@code Single} back into normal + * {@code onSuccess}, {@code onError} or {@code onComplete} signals as a + * {@link Maybe} source. + * <p> + * <img width="640" height="341" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.dematerialize.png" alt=""> + * <p> + * The intended use of the {@code selector} function is to perform a + * type-safe identity mapping (see example) on a source that is already of type + * {@code Notification<T>}. The Java language doesn't allow + * limiting instance methods to a certain generic argument shape, therefore, + * a function is used to ensure the conversion remains type safe. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code dematerialize} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * <p> + * Example: + * <pre><code> + * Single.just(Notification.createOnNext(1)) + * .dematerialize(notification -> notification) + * .test() + * .assertResult(1); + * </code></pre> + * <p>History: 2.2.4 - experimental + * @param <R> the result type + * @param selector the function called with the success item and should + * return a {@code Notification} instance. + * @return the new {@code Maybe} instance + * @throws NullPointerException if {@code selector} is {@code null} + * @since 3.0.0 + * @see #materialize() + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public final <@NonNull R> Maybe<R> dematerialize(@NonNull Function<? super T, @NonNull Notification<R>> selector) { + Objects.requireNonNull(selector, "selector is null"); + return RxJavaPlugins.onAssembly(new SingleDematerialize<>(this, selector)); + } + + /** + * Calls the specified consumer with the success item after this item has been emitted to the downstream. + * <p> + * <img width="640" height="461" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.doAfterSuccess.v3.png" alt=""> + * <p> + * Note that the {@code doAfterSuccess} action is shared between subscriptions and as such + * should be thread-safe. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code doAfterSuccess} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * <p>History: 2.0.1 - experimental + * @param onAfterSuccess the {@link Consumer} that will be called after emitting an item from upstream to the downstream + * @return the new {@code Single} instance + * @throws NullPointerException if {@code onAfterSuccess} is {@code null} + * @since 2.1 + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public final Single<T> doAfterSuccess(@NonNull Consumer<? super T> onAfterSuccess) { + Objects.requireNonNull(onAfterSuccess, "onAfterSuccess is null"); + return RxJavaPlugins.onAssembly(new SingleDoAfterSuccess<>(this, onAfterSuccess)); + } + + /** + * Registers an {@link Action} to be called after this {@code Single} invokes either {@code onSuccess} or {@code onError}. + * <p> + * <img width="640" height="461" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.doAfterTerminate.v3.png" alt=""> + * <p> + * Note that the {@code doAfterTerminate} action is shared between subscriptions and as such + * should be thread-safe.</p> + * + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code doAfterTerminate} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * <p>History: 2.0.6 - experimental + * @param onAfterTerminate + * an {@code Action} to be invoked when the current {@code Single} finishes + * @return the new {@code Single} that emits the same items as the current {@code Single}, then invokes the + * {@code Action} + * @throws NullPointerException if {@code onAfterTerminate} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/do.html">ReactiveX operators documentation: Do</a> + * @since 2.1 + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public final Single<T> doAfterTerminate(@NonNull Action onAfterTerminate) { + Objects.requireNonNull(onAfterTerminate, "onAfterTerminate is null"); + return RxJavaPlugins.onAssembly(new SingleDoAfterTerminate<>(this, onAfterTerminate)); + } + + /** + * Calls the specified action after this {@code Single} signals {@code onSuccess} or {@code onError} or gets disposed by + * the downstream. + * <p>In case of a race between a terminal event and a dispose call, the provided {@code onFinally} action + * is executed once per subscription. + * <p>Note that the {@code onFinally} action is shared between subscriptions and as such + * should be thread-safe. + * <p> + * <img width="640" height="291" src="https://raw.githubusercontent.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.doFinally.v3.png" alt=""> + * </p> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code doFinally} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * <p>History: 2.0.1 - experimental + * @param onFinally the action called when this {@code Single} terminates or gets disposed + * @return the new {@code Single} instance + * @throws NullPointerException if {@code onFinally} is {@code null} + * @since 2.1 + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public final Single<T> doFinally(@NonNull Action onFinally) { + Objects.requireNonNull(onFinally, "onFinally is null"); + return RxJavaPlugins.onAssembly(new SingleDoFinally<>(this, onFinally)); + } + + /** + * Calls the appropriate {@code onXXX} method (shared between all {@link SingleObserver}s) for the lifecycle events of + * the sequence (subscription, disposal). + * <p> + * <img width="640" height="232" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.doOnLifecycle.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code doOnLifecycle} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param onSubscribe + * a {@link Consumer} called with the {@link Disposable} sent via {@link SingleObserver#onSubscribe(Disposable)} + * @param onDispose + * called when the downstream disposes the {@code Disposable} via {@code dispose()} + * @return the new {@code Single} instance + * @throws NullPointerException if {@code onSubscribe} or {@code onDispose} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/do.html">ReactiveX operators documentation: Do</a> + * @since 3.0.0 + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Single<T> doOnLifecycle(@NonNull Consumer<? super Disposable> onSubscribe, @NonNull Action onDispose) { + Objects.requireNonNull(onSubscribe, "onSubscribe is null"); + Objects.requireNonNull(onDispose, "onDispose is null"); + return RxJavaPlugins.onAssembly(new SingleDoOnLifecycle<>(this, onSubscribe, onDispose)); + } + + /** + * Calls the shared consumer with the {@link Disposable} sent through the {@code onSubscribe} for each + * {@link SingleObserver} that subscribes to the current {@code Single}. + * <p> + * <img width="640" height="347" src="https://raw.githubusercontent.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.doOnSubscribe.v3.png" alt=""> + * </p> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code doOnSubscribe} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param onSubscribe the consumer called with the {@code Disposable} sent via {@code onSubscribe} + * @return the new {@code Single} instance + * @throws NullPointerException if {@code onSubscribe} is {@code null} + * @since 2.0 + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public final Single<T> doOnSubscribe(@NonNull Consumer<? super Disposable> onSubscribe) { + Objects.requireNonNull(onSubscribe, "onSubscribe is null"); + return RxJavaPlugins.onAssembly(new SingleDoOnSubscribe<>(this, onSubscribe)); + } + + /** + * Returns a {@code Single} instance that calls the given {@code onTerminate} callback + * just before this {@code Single} completes normally or with an exception. + * <p> + * <img width="640" height="305" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/doOnTerminate.v3.png" alt=""> + * <p> + * This differs from {@code doAfterTerminate} in that this happens <em>before</em> the {@code onSuccess} or + * {@code onError} notification. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code doOnTerminate} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * <p>History: 2.2.7 - experimental + * @param onTerminate the action to invoke when the consumer calls {@code onSuccess} or {@code onError} + * @return the new {@code Single} instance + * @throws NullPointerException if {@code onTerminate} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/do.html">ReactiveX operators documentation: Do</a> + * @see #doOnTerminate(Action) + * @since 3.0.0 + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public final Single<T> doOnTerminate(@NonNull Action onTerminate) { + Objects.requireNonNull(onTerminate, "onTerminate is null"); + return RxJavaPlugins.onAssembly(new SingleDoOnTerminate<>(this, onTerminate)); + } + + /** + * Calls the shared consumer with the success value sent via {@code onSuccess} for each + * {@link SingleObserver} that subscribes to the current {@code Single}. + * <p> + * <img width="640" height="347" src="https://raw.githubusercontent.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.doOnSuccess.2.v3.png" alt=""> + * </p> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code doOnSuccess} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param onSuccess the consumer called with the success value of {@code onSuccess} + * @return the new {@code Single} instance + * @throws NullPointerException if {@code onSuccess} is {@code null} + * @since 2.0 + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public final Single<T> doOnSuccess(@NonNull Consumer<? super T> onSuccess) { + Objects.requireNonNull(onSuccess, "onSuccess is null"); + return RxJavaPlugins.onAssembly(new SingleDoOnSuccess<>(this, onSuccess)); + } + + /** + * Calls the shared consumer with the error sent via {@code onError} or the value + * via {@code onSuccess} for each {@link SingleObserver} that subscribes to the current {@code Single}. + * <p> + * <img width="640" height="264" src="https://raw.githubusercontent.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.doOnEvent.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code doOnEvent} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param onEvent the consumer called with the success value of onEvent + * @return the new {@code Single} instance + * @throws NullPointerException if {@code onEvent} is {@code null} + * @since 2.0 + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public final Single<T> doOnEvent(@NonNull BiConsumer<@Nullable ? super T, @Nullable ? super Throwable> onEvent) { + Objects.requireNonNull(onEvent, "onEvent is null"); + return RxJavaPlugins.onAssembly(new SingleDoOnEvent<>(this, onEvent)); + } + + /** + * Calls the shared consumer with the error sent via {@code onError} for each + * {@link SingleObserver} that subscribes to the current {@code Single}. + * <p> + * <img width="640" height="349" src="https://raw.githubusercontent.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.doOnError.2.v3.png" alt=""> + * </p> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code doOnError} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param onError the consumer called with the success value of {@code onError} + * @return the new {@code Single} instance + * @throws NullPointerException if {@code onError} is {@code null} + * @since 2.0 + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public final Single<T> doOnError(@NonNull Consumer<? super Throwable> onError) { + Objects.requireNonNull(onError, "onError is null"); + return RxJavaPlugins.onAssembly(new SingleDoOnError<>(this, onError)); + } + + /** + * Calls the shared {@link Action} if a {@link SingleObserver} subscribed to the current {@code Single} + * disposes the common {@link Disposable} it received via {@code onSubscribe}. + * <p> + * <img width="640" height="332" src="https://raw.githubusercontent.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.doOnDispose.v3.png" alt=""> + * </p> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code doOnDispose} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param onDispose the action called when the subscription is disposed + * @return the new {@code Single} instance + * @throws NullPointerException if {@code onDispose} is {@code null} + * @since 2.0 + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public final Single<T> doOnDispose(@NonNull Action onDispose) { + Objects.requireNonNull(onDispose, "onDispose is null"); + return RxJavaPlugins.onAssembly(new SingleDoOnDispose<>(this, onDispose)); + } + + /** + * Filters the success item of the {@code Single} via a predicate function and emitting it if the predicate + * returns {@code true}, completing otherwise. + * <p> + * <img width="640" height="457" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.filter.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code filter} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param predicate + * a function that evaluates the item emitted by the current {@code Single}, returning {@code true} + * if it passes the filter + * @return the new {@link Maybe} that emit the item emitted by the current {@code Single} that the filter + * evaluates as {@code true} + * @throws NullPointerException if {@code predicate} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/filter.html">ReactiveX operators documentation: Filter</a> + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public final Maybe<T> filter(@NonNull Predicate<? super T> predicate) { + Objects.requireNonNull(predicate, "predicate is null"); + return RxJavaPlugins.onAssembly(new MaybeFilterSingle<>(this, predicate)); + } + + /** + * Returns a {@code Single} that is based on applying a specified function to the item emitted by the current {@code Single}, + * where that function returns a {@link SingleSource}. + * <p> + * <img width="640" height="300" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.flatMap.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code flatMap} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <R> the result value type + * @param mapper + * a function that, when applied to the item emitted by the current {@code Single}, returns a {@code SingleSource} + * @return the new {@code Single} returned from {@code mapper} when applied to the item emitted by the current {@code Single} + * @throws NullPointerException if {@code mapper} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/flatmap.html">ReactiveX operators documentation: FlatMap</a> + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public final <@NonNull R> Single<R> flatMap(@NonNull Function<? super T, ? extends SingleSource<? extends R>> mapper) { + Objects.requireNonNull(mapper, "mapper is null"); + return RxJavaPlugins.onAssembly(new SingleFlatMap<>(this, mapper)); + } + + /** + * Returns a {@code Single} that emits the results of a specified function to the pair of values emitted by the + * current {@code Single} and a specified mapped {@link SingleSource}. + * <p> + * <img width="640" height="268" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.flatMap.combiner.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code flatMap} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <U> + * the type of items emitted by the {@code SingleSource} returned by the {@code mapper} function + * @param <R> + * the type of items emitted by the resulting {@code Single} + * @param mapper + * a function that returns a {@code SingleSource} for the item emitted by the current {@code Single} + * @param combiner + * a function that combines one item emitted by each of the source and collection {@code SingleSource} and + * returns an item to be emitted by the resulting {@code SingleSource} + * @return the new {@code Single} instance + * @throws NullPointerException if {@code mapper} or {@code combiner} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/flatmap.html">ReactiveX operators documentation: FlatMap</a> + * @since 3.0.0 + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public final <@NonNull U, @NonNull R> Single<R> flatMap(@NonNull Function<? super T, ? extends SingleSource<? extends U>> mapper, + @NonNull BiFunction<? super T, ? super U, ? extends R> combiner) { + Objects.requireNonNull(mapper, "mapper is null"); + Objects.requireNonNull(combiner, "combiner is null"); + return RxJavaPlugins.onAssembly(new SingleFlatMapBiSelector<>(this, mapper, combiner)); + } + + /** + * Maps the {@code onSuccess} or {@code onError} signals of the current {@code Single} into a {@link SingleSource} and emits that + * {@code SingleSource}'s signals. + * <p> + * <img width="640" height="449" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.flatMap.notification.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code flatMap} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <R> + * the result type + * @param onSuccessMapper + * a function that returns a {@code SingleSource} to merge for the {@code onSuccess} item emitted by this {@code Single} + * @param onErrorMapper + * a function that returns a {@code SingleSource} to merge for an {@code onError} notification from this {@code Single} + * @return the new {@code Single} instance + * @throws NullPointerException if {@code onSuccessMapper} or {@code onErrorMapper} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/flatmap.html">ReactiveX operators documentation: FlatMap</a> + * @since 3.0.0 + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public final <@NonNull R> Single<R> flatMap( + @NonNull Function<? super T, ? extends SingleSource<? extends R>> onSuccessMapper, + @NonNull Function<? super Throwable, ? extends SingleSource<? extends R>> onErrorMapper) { + Objects.requireNonNull(onSuccessMapper, "onSuccessMapper is null"); + Objects.requireNonNull(onErrorMapper, "onErrorMapper is null"); + return RxJavaPlugins.onAssembly(new SingleFlatMapNotification<>(this, onSuccessMapper, onErrorMapper)); + } + + /** + * Returns a {@link Maybe} that is based on applying a specified function to the item emitted by the current {@code Single}, + * where that function returns a {@link MaybeSource}. + * <p> + * <img width="640" height="191" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.flatMapMaybe.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code flatMapMaybe} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <R> the result value type + * @param mapper + * a function that, when applied to the item emitted by the current {@code Single}, returns a {@code MaybeSource} + * @return the new {@code Maybe} returned from {@code mapper} when applied to the item emitted by the current {@code Single} + * @throws NullPointerException if {@code mapper} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/flatmap.html">ReactiveX operators documentation: FlatMap</a> + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public final <@NonNull R> Maybe<R> flatMapMaybe(@NonNull Function<? super T, ? extends MaybeSource<? extends R>> mapper) { + Objects.requireNonNull(mapper, "mapper is null"); + return RxJavaPlugins.onAssembly(new SingleFlatMapMaybe<>(this, mapper)); + } + + /** + * Returns a {@link Flowable} that emits items based on applying a specified function to the item emitted by the + * current {@code Single}, where that function returns a {@link Publisher}. + * <p> + * <img width="640" height="305" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.flatMapPublisher.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The returned {@code Flowable} honors the backpressure of the downstream consumer + * and the {@code Publisher} returned by the mapper function is expected to honor it as well.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code flatMapPublisher} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <R> the result value type + * @param mapper + * a function that, when applied to the item emitted by the current {@code Single}, returns a + * {@code Publisher} + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code mapper} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/flatmap.html">ReactiveX operators documentation: FlatMap</a> + */ + @BackpressureSupport(BackpressureKind.FULL) + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public final <@NonNull R> Flowable<R> flatMapPublisher(@NonNull Function<? super T, @NonNull ? extends Publisher<? extends R>> mapper) { + Objects.requireNonNull(mapper, "mapper is null"); + return RxJavaPlugins.onAssembly(new SingleFlatMapPublisher<>(this, mapper)); + } + + /** + * Maps the success value of the current {@code Single} into an {@link Iterable} and emits its items as a + * {@link Flowable} sequence. + * <p> + * <img width="640" height="373" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/flattenAsFlowable.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code flattenAsFlowable} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <U> + * the type of item emitted by the resulting {@code Iterable} + * @param mapper + * a function that returns an {@code Iterable} sequence of values for when given an item emitted by the + * current {@code Single} + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code mapper} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/flatmap.html">ReactiveX operators documentation: FlatMap</a> + * @see #flattenStreamAsFlowable(Function) + */ + @BackpressureSupport(BackpressureKind.FULL) + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public final <@NonNull U> Flowable<U> flattenAsFlowable(@NonNull Function<? super T, @NonNull ? extends Iterable<? extends U>> mapper) { + Objects.requireNonNull(mapper, "mapper is null"); + return RxJavaPlugins.onAssembly(new SingleFlatMapIterableFlowable<>(this, mapper)); + } + + /** + * Maps the success value of the current {@code Single} into an {@link Iterable} and emits its items as an + * {@link Observable} sequence. + * <p> + * <img width="640" height="373" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/flattenAsObservable.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code flattenAsObservable} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <U> + * the type of item emitted by the resulting {@code Iterable} + * @param mapper + * a function that returns an {@code Iterable} sequence of values for when given an item emitted by the + * current {@code Single} + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code mapper} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/flatmap.html">ReactiveX operators documentation: FlatMap</a> + * @see #flattenStreamAsObservable(Function) + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public final <@NonNull U> Observable<U> flattenAsObservable(@NonNull Function<@NonNull ? super T, @NonNull ? extends Iterable<? extends U>> mapper) { + Objects.requireNonNull(mapper, "mapper is null"); + return RxJavaPlugins.onAssembly(new SingleFlatMapIterableObservable<>(this, mapper)); + } + + /** + * Returns an {@link Observable} that is based on applying a specified function to the item emitted by the current {@code Single}, + * where that function returns an {@link ObservableSource}. + * <p> + * <img width="640" height="305" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.flatMapObservable.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code flatMapObservable} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <R> the result value type + * @param mapper + * a function that, when applied to the item emitted by the current {@code Single}, returns an {@code ObservableSource} + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code mapper} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/flatmap.html">ReactiveX operators documentation: FlatMap</a> + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public final <@NonNull R> Observable<R> flatMapObservable(@NonNull Function<? super T, ? extends ObservableSource<? extends R>> mapper) { + Objects.requireNonNull(mapper, "mapper is null"); + return RxJavaPlugins.onAssembly(new SingleFlatMapObservable<>(this, mapper)); + } + + /** + * Returns a {@link Completable} that completes based on applying a specified function to the item emitted by the + * current {@code Single}, where that function returns a {@link CompletableSource}. + * <p> + * <img width="640" height="267" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.flatMapCompletable.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code flatMapCompletable} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param mapper + * a function that, when applied to the item emitted by the current {@code Single}, returns a + * {@code CompletableSource} + * @return the new {@code Completable} instance + * @throws NullPointerException if {@code mapper} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/flatmap.html">ReactiveX operators documentation: FlatMap</a> + * @since 2.0 + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public final Completable flatMapCompletable(@NonNull Function<? super T, ? extends CompletableSource> mapper) { + Objects.requireNonNull(mapper, "mapper is null"); + return RxJavaPlugins.onAssembly(new SingleFlatMapCompletable<>(this, mapper)); + } + + /** + * Waits in a blocking fashion until the current {@code Single} signals a success value (which is returned) or + * an exception (which is propagated). + * <p> + * <img width="640" height="429" src="https://raw.githubusercontent.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.blockingGet.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code blockingGet} does not operate by default on a particular {@link Scheduler}.</dd> + * <dt><b>Error handling:</b></dt> + * <dd>If the source signals an error, the operator wraps a checked {@link Exception} + * into {@link RuntimeException} and throws that. Otherwise, {@code RuntimeException}s and + * {@link Error}s are rethrown as they are.</dd> + * </dl> + * @return the success value + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final T blockingGet() { + BlockingMultiObserver<T> observer = new BlockingMultiObserver<>(); + subscribe(observer); + return observer.blockingGet(); + } + + /** + * Subscribes to the current {@code Single} and <em>blocks the current thread</em> until it terminates. + * <p> + * <img width="640" height="329" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.blockingSubscribe.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code blockingSubscribe} does not operate by default on a particular {@link Scheduler}.</dd> + * <dt><b>Error handling:</b></dt> + * <dd>If the current {@code Single} signals an error, + * the {@link Throwable} is routed to the global error handler via {@link RxJavaPlugins#onError(Throwable)}. + * If the current thread is interrupted, an {@link InterruptedException} is routed to the same global error handler. + * </dd> + * </dl> + * @since 3.0.0 + * @see #blockingSubscribe(Consumer) + * @see #blockingSubscribe(Consumer, Consumer) + */ + @SchedulerSupport(SchedulerSupport.NONE) + public final void blockingSubscribe() { + blockingSubscribe(Functions.emptyConsumer(), Functions.ERROR_CONSUMER); + } + + /** + * Subscribes to the current {@code Single} and calls given {@code onSuccess} callback on the <em>current thread</em> + * when it completes normally. + * <p> + * <img width="640" height="351" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.blockingSubscribe.c.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code blockingSubscribe} does not operate by default on a particular {@link Scheduler}.</dd> + * <dt><b>Error handling:</b></dt> + * <dd>If either the current {@code Single} signals an error or {@code onSuccess} throws, + * the respective {@link Throwable} is routed to the global error handler via {@link RxJavaPlugins#onError(Throwable)}. + * If the current thread is interrupted, an {@link InterruptedException} is routed to the same global error handler. + * </dd> + * </dl> + * @param onSuccess the {@link Consumer} to call if the current {@code Single} succeeds + * @throws NullPointerException if {@code onSuccess} is {@code null} + * @since 3.0.0 + * @see #blockingSubscribe(Consumer, Consumer) + */ + @SchedulerSupport(SchedulerSupport.NONE) + public final void blockingSubscribe(@NonNull Consumer<? super T> onSuccess) { + blockingSubscribe(onSuccess, Functions.ERROR_CONSUMER); + } + + /** + * Subscribes to the current {@code Single} and calls the appropriate callback on the <em>current thread</em> + * when it terminates. + * <p> + * <img width="640" height="348" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.blockingSubscribe.cc.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code blockingSubscribe} does not operate by default on a particular {@link Scheduler}.</dd> + * <dt><b>Error handling:</b></dt> + * <dd>If either {@code onSuccess} or {@code onError} throw, the {@link Throwable} is routed to the + * global error handler via {@link RxJavaPlugins#onError(Throwable)}. + * If the current thread is interrupted, the {@code onError} consumer is called with an {@link InterruptedException}. + * </dd> + * </dl> + * @param onSuccess the {@link Consumer} to call if the current {@code Single} succeeds + * @param onError the {@code Consumer} to call if the current {@code Single} signals an error + * @throws NullPointerException if {@code onSuccess} or {@code onError} is {@code null} + * @since 3.0.0 + */ + @SchedulerSupport(SchedulerSupport.NONE) + public final void blockingSubscribe(@NonNull Consumer<? super T> onSuccess, @NonNull Consumer<? super Throwable> onError) { + Objects.requireNonNull(onSuccess, "onSuccess is null"); + Objects.requireNonNull(onError, "onError is null"); + BlockingMultiObserver<T> observer = new BlockingMultiObserver<>(); + subscribe(observer); + observer.blockingConsume(onSuccess, onError, Functions.EMPTY_ACTION); + } + + /** + * Subscribes to the current {@code Single} and calls the appropriate {@link SingleObserver} method on the <em>current thread</em>. + * <p> + * <img width="640" height="479" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.blockingSubscribe.o.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code blockingSubscribe} does not operate by default on a particular {@link Scheduler}.</dd> + * <dt><b>Error handling:</b></dt> + * <dd>An {@code onError} signal is delivered to the {@link SingleObserver#onError(Throwable)} method. + * If any of the {@code SingleObserver}'s methods throw, the {@link RuntimeException} is propagated to the caller of this method. + * If the current thread is interrupted, an {@link InterruptedException} is delivered to {@code observer.onError}. + * </dd> + * </dl> + * @param observer the {@code SingleObserver} to call methods on the current thread + * @throws NullPointerException if {@code observer} is {@code null} + * @since 3.0.0 + */ + @SchedulerSupport(SchedulerSupport.NONE) + public final void blockingSubscribe(@NonNull SingleObserver<? super T> observer) { + Objects.requireNonNull(observer, "observer is null"); + BlockingDisposableMultiObserver<T> blockingObserver = new BlockingDisposableMultiObserver<>(); + observer.onSubscribe(blockingObserver); + subscribe(blockingObserver); + blockingObserver.blockingConsume(observer); + } + + /** + * <strong>This method requires advanced knowledge about building operators, please consider + * other standard composition methods first;</strong> + * Returns a {@code Single} which, when subscribed to, invokes the {@link SingleOperator#apply(SingleObserver) apply(SingleObserver)} method + * of the provided {@link SingleOperator} for each individual downstream {@link Single} and allows the + * insertion of a custom operator by accessing the downstream's {@link SingleObserver} during this subscription phase + * and providing a new {@code SingleObserver}, containing the custom operator's intended business logic, that will be + * used in the subscription process going further upstream. + * <p> + * <img width="640" height="304" src="https://raw.githubusercontent.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.lift.png" alt=""> + * <p> + * Generally, such a new {@code SingleObserver} will wrap the downstream's {@code SingleObserver} and forwards the + * {@code onSuccess} and {@code onError} events from the upstream directly or according to the + * emission pattern the custom operator's business logic requires. In addition, such operator can intercept the + * flow control calls of {@code dispose} and {@code isDisposed} that would have traveled upstream and perform + * additional actions depending on the same business logic requirements. + * <p> + * Example: + * <pre><code> + * // Step 1: Create the consumer type that will be returned by the SingleOperator.apply(): + * + * public final class CustomSingleObserver<T> implements SingleObserver<T>, Disposable { + * + * // The downstream's SingleObserver that will receive the onXXX events + * final SingleObserver<? super String> downstream; + * + * // The connection to the upstream source that will call this class' onXXX methods + * Disposable upstream; + * + * // The constructor takes the downstream subscriber and usually any other parameters + * public CustomSingleObserver(SingleObserver<? super String> downstream) { + * this.downstream = downstream; + * } + * + * // In the subscription phase, the upstream sends a Disposable to this class + * // and subsequently this class has to send a Disposable to the downstream. + * // Note that relaying the upstream's Disposable directly is not allowed in RxJava + * @Override + * public void onSubscribe(Disposable d) { + * if (upstream != null) { + * d.dispose(); + * } else { + * upstream = d; + * downstream.onSubscribe(this); + * } + * } + * + * // The upstream calls this with the next item and the implementation's + * // responsibility is to emit an item to the downstream based on the intended + * // business logic, or if it can't do so for the particular item, + * // request more from the upstream + * @Override + * public void onSuccess(T item) { + * String str = item.toString(); + * if (str.length() < 2) { + * downstream.onSuccess(str); + * } else { + * // Single is usually expected to produce one of the onXXX events + * downstream.onError(new NoSuchElementException()); + * } + * } + * + * // Some operators may handle the upstream's error while others + * // could just forward it to the downstream. + * @Override + * public void onError(Throwable throwable) { + * downstream.onError(throwable); + * } + * + * // Some operators may use their own resources which should be cleaned up if + * // the downstream disposes the flow before it completed. Operators without + * // resources can simply forward the dispose to the upstream. + * // In some cases, a disposed flag may be set by this method so that other parts + * // of this class may detect the dispose and stop sending events + * // to the downstream. + * @Override + * public void dispose() { + * upstream.dispose(); + * } + * + * // Some operators may simply forward the call to the upstream while others + * // can return the disposed flag set in dispose(). + * @Override + * public boolean isDisposed() { + * return upstream.isDisposed(); + * } + * } + * + * // Step 2: Create a class that implements the SingleOperator interface and + * // returns the custom consumer type from above in its apply() method. + * // Such class may define additional parameters to be submitted to + * // the custom consumer type. + * + * final class CustomSingleOperator<T> implements SingleOperator<String> { + * @Override + * public SingleObserver<? super String> apply(SingleObserver<? super T> upstream) { + * return new CustomSingleObserver<T>(upstream); + * } + * } + * + * // Step 3: Apply the custom operator via lift() in a flow by creating an instance of it + * // or reusing an existing one. + * + * Single.just(5) + * .lift(new CustomSingleOperator<Integer>()) + * .test() + * .assertResult("5"); + * + * Single.just(15) + * .lift(new CustomSingleOperator<Integer>()) + * .test() + * .assertFailure(NoSuchElementException.class); + * </code></pre> + * <p> + * Creating custom operators can be complicated and it is recommended one consults the + * <a href="https://github.com/ReactiveX/RxJava/wiki/Writing-operators-for-2.0">RxJava wiki: Writing operators</a> page about + * the tools, requirements, rules, considerations and pitfalls of implementing them. + * <p> + * Note that implementing custom operators via this {@code lift()} method adds slightly more overhead by requiring + * an additional allocation and indirection per assembled flows. Instead, extending the abstract {@code Single} + * class and creating a {@link SingleTransformer} with it is recommended. + * <p> + * Note also that it is not possible to stop the subscription phase in {@code lift()} as the {@code apply()} method + * requires a non-{@code null} {@code SingleObserver} instance to be returned, which is then unconditionally subscribed to + * the current {@code Single}. For example, if the operator decided there is no reason to subscribe to the + * upstream source because of some optimization possibility or a failure to prepare the operator, it still has to + * return a {@code SingleObserver} that should immediately dispose the upstream's {@link Disposable} in its + * {@code onSubscribe} method. Again, using a {@code SingleTransformer} and extending the {@code Single} is + * a better option as {@link #subscribeActual} can decide to not subscribe to its upstream after all. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code lift} does not operate by default on a particular {@link Scheduler}, however, the + * {@code SingleOperator} may use a {@code Scheduler} to support its own asynchronous behavior.</dd> + * </dl> + * + * @param <R> the output value type + * @param lift the {@code SingleOperator} that receives the downstream's {@code SingleObserver} and should return + * a {@code SingleObserver} with custom behavior to be used as the consumer for the current + * {@code Single}. + * @return the new {@code Single} instance + * @throws NullPointerException if {@code lift} is {@code null} + * @see <a href="https://github.com/ReactiveX/RxJava/wiki/Writing-operators-for-2.0">RxJava wiki: Writing operators</a> + * @see #compose(SingleTransformer) + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public final <@NonNull R> Single<R> lift(@NonNull SingleOperator<? extends R, ? super T> lift) { + Objects.requireNonNull(lift, "lift is null"); + return RxJavaPlugins.onAssembly(new SingleLift<>(this, lift)); + } + + /** + * Returns a {@code Single} that applies a specified function to the item emitted by the current {@code Single} and + * emits the result of this function application. + * <p> + * <img width="640" height="305" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.map.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code map} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <R> the result value type + * @param mapper + * a function to apply to the item emitted by the {@code Single} + * @return the new {@code Single} that emits the item from the current {@code Single}, transformed by the specified function + * @throws NullPointerException if {@code mapper} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/map.html">ReactiveX operators documentation: Map</a> + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public final <@NonNull R> Single<R> map(@NonNull Function<? super T, ? extends R> mapper) { + Objects.requireNonNull(mapper, "mapper is null"); + return RxJavaPlugins.onAssembly(new SingleMap<>(this, mapper)); + } + + /** + * Maps the signal types of this {@code Single} into a {@link Notification} of the same kind + * and emits it as a single success value to downstream. + * <p> + * <img width="640" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/materialize.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code materialize} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * <p>History: 2.2.4 - experimental + * @return the new {@code Single} instance + * @since 3.0.0 + * @see #dematerialize(Function) + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Single<Notification<T>> materialize() { + return RxJavaPlugins.onAssembly(new SingleMaterialize<>(this)); + } + + /** + * Signals {@code true} if the current {@code Single} signals a success value that is {@link Object#equals(Object)} with the value + * provided. + * <p> + * <img width="640" height="401" src="https://raw.githubusercontent.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.contains.png" alt=""> + * <p> + * <img width="640" height="401" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.contains.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code contains} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param item the value to compare against the success value of this {@code Single} + * @return the new {@code Single} instance + * @throws NullPointerException if {@code item} is {@code null} + * @since 2.0 + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Single<Boolean> contains(@NonNull Object item) { + return contains(item, ObjectHelper.equalsPredicate()); + } + + /** + * Signals {@code true} if the current {@code Single} signals a success value that is equal with + * the value provided by calling a {@link BiPredicate}. + * <p> + * <img width="640" height="401" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.contains.f.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code contains} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param item the value to compare against the success value of this {@code Single} + * @param comparer the function that receives the success value of this {@code Single}, the value provided + * and should return {@code true} if they are considered equal + * @return the new {@code Single} instance + * @throws NullPointerException if {@code item} or {@code comparer} is {@code null} + * @since 2.0 + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public final Single<Boolean> contains(@NonNull Object item, @NonNull BiPredicate<Object, Object> comparer) { + Objects.requireNonNull(item, "item is null"); + Objects.requireNonNull(comparer, "comparer is null"); + return RxJavaPlugins.onAssembly(new SingleContains<>(this, item, comparer)); + } + + /** + * Flattens this {@code Single} and another {@link SingleSource} into one {@link Flowable}, without any transformation. + * <p> + * <img width="640" height="416" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.mergeWith.v3.png" alt=""> + * <p> + * You can combine items emitted by multiple {@code SingleSource}s so that they appear as one {@code Flowable}, by using + * the {@code mergeWith} method. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The returned {@code Flowable} honors the backpressure of the downstream consumer.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code mergeWith} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param other + * a {@code SingleSource} to be merged + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code other} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/merge.html">ReactiveX operators documentation: Merge</a> + */ + @BackpressureSupport(BackpressureKind.FULL) + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Flowable<T> mergeWith(@NonNull SingleSource<? extends T> other) { + return merge(this, other); + } + /** + * Filters the items emitted by the current {@code Single}, only emitting its success value if that + * is an instance of the supplied {@link Class}. + * <p> + * <img width="640" height="399" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.ofType.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code ofType} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <U> the output type + * @param clazz + * the class type to filter the items emitted by the current {@code Single} + * @return the new {@link Maybe} instance + * @throws NullPointerException if {@code clazz} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/filter.html">ReactiveX operators documentation: Filter</a> + * @since 3.0.0 + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public final <@NonNull U> Maybe<U> ofType(@NonNull Class<U> clazz) { + Objects.requireNonNull(clazz, "clazz is null"); + return filter(Functions.isInstanceOf(clazz)).cast(clazz); + } + + /** + * Signals the success item or the terminal signals of the current {@code Single} on the specified {@link Scheduler}, + * asynchronously. + * <p> + * <img width="640" height="305" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.observeOn.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>you specify which {@code Scheduler} this operator will use.</dd> + * </dl> + * + * @param scheduler + * the {@code Scheduler} to notify subscribers on + * @return the new {@code Single} instance + * @throws NullPointerException if {@code scheduler} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/observeon.html">ReactiveX operators documentation: ObserveOn</a> + * @see <a href="http://www.grahamlea.com/2014/07/rxjava-threading-examples/">RxJava Threading Examples</a> + * @see #subscribeOn + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.CUSTOM) + public final Single<T> observeOn(@NonNull Scheduler scheduler) { + Objects.requireNonNull(scheduler, "scheduler is null"); + return RxJavaPlugins.onAssembly(new SingleObserveOn<>(this, scheduler)); + } + + /** + * Ends the flow with a success item returned by a function for the {@link Throwable} error signaled by the current + * {@code Single} instead of signaling the error via {@code onError}. + * <p> + * <img width="640" height="461" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.onErrorReturn.v3.png" alt=""> + * <p> + * By default, when a {@code Single} encounters an error that prevents it from emitting the expected item to its + * subscriber, the {@code Single} invokes its subscriber's {@link SingleObserver#onError} method, and then quits + * without invoking any more of its observer's methods. The {@code onErrorReturn} method changes this + * behavior. If you pass a function ({@code resumeFunction}) to a {@code Single}'s {@code onErrorReturn} method, if + * the original {@code Single} encounters an error, instead of invoking its observer's + * {@link SingleObserver#onError} method, it will instead emit the return value of {@code resumeFunction}. + * <p> + * You can use this to prevent errors from propagating or to supply fallback data should errors be + * encountered. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code onErrorReturn} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param itemSupplier + * a function that returns an item that the new {@code Single} will emit if the current {@code Single} encounters + * an error + * @return the new {@code Single} instance + * @throws NullPointerException if {@code itemSupplier} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/catch.html">ReactiveX operators documentation: Catch</a> + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public final Single<T> onErrorReturn(@NonNull Function<Throwable, ? extends T> itemSupplier) { + Objects.requireNonNull(itemSupplier, "itemSupplier is null"); + return RxJavaPlugins.onAssembly(new SingleOnErrorReturn<>(this, itemSupplier, null)); + } + + /** + * Signals the specified value as success in case the current {@code Single} signals an error. + * <p> + * <img width="640" height="461" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.onErrorReturnItem.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code onErrorReturnItem} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param item the value to signal if the current {@code Single} fails + * @return the new {@code Single} instance + * @throws NullPointerException if {@code item} is {@code null} + * @since 2.0 + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public final Single<T> onErrorReturnItem(@NonNull T item) { + Objects.requireNonNull(item, "item is null"); + return RxJavaPlugins.onAssembly(new SingleOnErrorReturn<>(this, null, item)); + } + + /** + * Resumes the flow with the given {@link SingleSource} when the current {@code Single} fails instead of + * signaling the error via {@code onError}. + * <p> + * <img width="640" height="461" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.onErrorResumeWith.png" alt=""> + * <p> + * By default, when a {@code Single} encounters an error that prevents it from emitting the expected item to + * its {@link SingleObserver}, the {@code Single} invokes its {@code SingleObserver}'s {@code onError} method, and then quits + * without invoking any more of its {@code SingleObserver}'s methods. The {@code onErrorResumeWith} method changes this + * behavior. If you pass another {@code Single} ({@code resumeSingleInCaseOfError}) to a {@code Single}'s + * {@code onErrorResumeWith} method, if the original {@code Single} encounters an error, instead of invoking its + * {@code SingleObserver}'s {@code onError} method, it will instead relinquish control to {@code resumeSingleInCaseOfError} which + * will invoke the {@code SingleObserver}'s {@link SingleObserver#onSuccess onSuccess} method if it is able to do so. In such a case, + * because no {@code Single} necessarily invokes {@code onError}, the {@code SingleObserver} may never know that an error + * happened. + * <p> + * You can use this to prevent errors from propagating or to supply fallback data should errors be + * encountered. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code onErrorResumeWith} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param fallback a {@code Single} that will take control if source {@code Single} encounters an error. + * @return the new {@code Single} instance + * @throws NullPointerException if {@code fallback} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/catch.html">ReactiveX operators documentation: Catch</a> + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public final Single<T> onErrorResumeWith(@NonNull SingleSource<? extends T> fallback) { + Objects.requireNonNull(fallback, "fallback is null"); + return onErrorResumeNext(Functions.justFunction(fallback)); + } + + /** + * Returns a {@link Maybe} instance that if the current {@code Single} emits an error, it will emit an {@code onComplete} + * and swallow the throwable. + * <p> + * <img width="640" height="554" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.onErrorComplete.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code onErrorComplete} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @return the new {@code Maybe} instance + * @since 3.0.0 + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Maybe<T> onErrorComplete() { + return onErrorComplete(Functions.alwaysTrue()); + } + + /** + * Returns a {@link Maybe} instance that if this {@code Single} emits an error and the predicate returns + * {@code true}, it will emit an {@code onComplete} and swallow the throwable. + * <p> + * <img width="640" height="270" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.onErrorComplete.f.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code onErrorComplete} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param predicate the predicate to call when an {@link Throwable} is emitted which should return {@code true} + * if the {@code Throwable} should be swallowed and replaced with an {@code onComplete}. + * @return the new {@code Maybe} instance + * @throws NullPointerException if {@code predicate} is {@code null} + * @since 3.0.0 + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public final Maybe<T> onErrorComplete(@NonNull Predicate<? super Throwable> predicate) { + Objects.requireNonNull(predicate, "predicate is null"); + + return RxJavaPlugins.onAssembly(new SingleOnErrorComplete<>(this, predicate)); + } + + /** + * Resumes the flow with a {@link SingleSource} returned for the failure {@link Throwable} of the current {@code Single} by a + * function instead of signaling the error via {@code onError}. + * <p> + * <img width="640" height="461" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.onErrorResumeNext.f.v3.png" alt=""> + * <p> + * By default, when a {@code Single} encounters an error that prevents it from emitting the expected item to + * its {@link SingleObserver}, the {@code Single} invokes its {@code SingleObserver}'s {@code onError} method, and then quits + * without invoking any more of its {@code SingleObserver}'s methods. The {@code onErrorResumeNext} method changes this + * behavior. If you pass a function that will return another {@code Single} ({@code resumeFunctionInCaseOfError}) to a {@code Single}'s + * {@code onErrorResumeNext} method, if the original {@code Single} encounters an error, instead of invoking its + * {@code SingleObserver}'s {@code onError} method, it will instead relinquish control to {@code resumeSingleInCaseOfError} which + * will invoke the {@code SingleObserver}'s {@link SingleObserver#onSuccess onSuccess} method if it is able to do so. In such a case, + * because no {@code Single} necessarily invokes {@code onError}, the {@code SingleObserver} may never know that an error + * happened. + * <p> + * You can use this to prevent errors from propagating or to supply fallback data should errors be + * encountered. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code onErrorResumeNext} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param fallbackSupplier a function that returns a {@code SingleSource} that will take control if source {@code Single} encounters an error. + * @return the new {@code Single} instance + * @throws NullPointerException if {@code fallbackSupplier} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/catch.html">ReactiveX operators documentation: Catch</a> + * @since .20 + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public final Single<T> onErrorResumeNext( + @NonNull Function<? super Throwable, ? extends SingleSource<? extends T>> fallbackSupplier) { + Objects.requireNonNull(fallbackSupplier, "fallbackSupplier is null"); + return RxJavaPlugins.onAssembly(new SingleResumeNext<>(this, fallbackSupplier)); + } + + /** + * Nulls out references to the upstream producer and downstream {@link SingleObserver} if + * the sequence is terminated or downstream calls {@code dispose()}. + * <p> + * <img width="640" height="346" src="https://raw.githubusercontent.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.onTerminateDetach.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code onTerminateDetach} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * <p>History: 2.1.5 - experimental + * @return the new {@code Single} which {@code null}s out references to the upstream producer and downstream {@code SingleObserver} if + * the sequence is terminated or downstream calls {@code dispose()} + * @since 2.2 + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Single<T> onTerminateDetach() { + return RxJavaPlugins.onAssembly(new SingleDetach<>(this)); + } + + /** + * Repeatedly re-subscribes to the current {@code Single} and emits each success value as a {@link Flowable} sequence. + * <p> + * <img width="640" height="463" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.repeat.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The returned {@code Flowable} honors the backpressure of the downstream consumer.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code repeat} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @return the new {@code Flowable} instance + * @since 2.0 + */ + @BackpressureSupport(BackpressureKind.FULL) + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Flowable<T> repeat() { + return toFlowable().repeat(); + } + + /** + * Re-subscribes to the current {@code Single} at most the given number of times and emits each success value as a {@link Flowable} sequence. + * <p> + * <img width="640" height="463" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.repeat.n.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The returned {@code Flowable} honors the backpressure of the downstream consumer.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code repeat} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param times the number of times to re-subscribe to the current {@code Single} + * @return the new {@code Flowable} instance + * @throws IllegalArgumentException if {@code times} is negative + * @since 2.0 + */ + @BackpressureSupport(BackpressureKind.FULL) + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Flowable<T> repeat(long times) { + return toFlowable().repeat(times); + } + + /** + * Re-subscribes to the current {@code Single} if + * the {@link Publisher} returned by the handler function signals a value in response to a + * value signaled through the {@link Flowable} the handler receives. + * <p> + * <img width="640" height="1480" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.repeatWhen.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The returned {@code Flowable} honors the backpressure of the downstream consumer. + * The {@code Publisher} returned by the handler function is expected to honor backpressure as well.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code repeatWhen} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param handler the function that is called with a {@code Flowable} that signals a value when the {@code Single} + * signaled a success value and returns a {@code Publisher} that has to signal a value to + * trigger a resubscription to the current {@code Single}, otherwise the terminal signal of + * the {@code Publisher} will be the terminal signal of the sequence as well. + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code handler} is {@code null} + * @since 2.0 + */ + @BackpressureSupport(BackpressureKind.FULL) + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Flowable<T> repeatWhen(@NonNull Function<? super Flowable<Object>, @NonNull ? extends Publisher<@NonNull ?>> handler) { + return toFlowable().repeatWhen(handler); + } + + /** + * Re-subscribes to the current {@code Single} until the given {@link BooleanSupplier} returns {@code true} + * and emits the success items as a {@link Flowable} sequence. + * <p> + * <img width="640" height="463" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.repeatUntil.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The returned {@code Flowable} honors the backpressure of the downstream consumer.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code repeatUntil} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param stop the {@code BooleanSupplier} called after the current {@code Single} succeeds and if returns {@code false}, + * the {@code Single} is re-subscribed; otherwise the sequence completes. + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code stop} is {@code null} + * @since 2.0 + */ + @BackpressureSupport(BackpressureKind.FULL) + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Flowable<T> repeatUntil(@NonNull BooleanSupplier stop) { + return toFlowable().repeatUntil(stop); + } + + /** + * Repeatedly re-subscribes to the current {@code Single} indefinitely if it fails with an {@code onError}. + * <p> + * <img width="640" height="399" src="https://raw.githubusercontent.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.retry.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code retry} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @return the new {@code Single} instance + * @since 2.0 + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Single<T> retry() { + return toSingle(toFlowable().retry()); + } + + /** + * Repeatedly re-subscribe at most the specified times to the current {@code Single} + * if it fails with an {@code onError}. + * <p> + * <img width="640" height="329" src="https://raw.githubusercontent.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.retry.n.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code retry} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param times the number of times to resubscribe if the current {@code Single} fails + * @return the new {@code Single} instance + * @throws IllegalArgumentException if {@code times} is negative + * @since 2.0 + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Single<T> retry(long times) { + return toSingle(toFlowable().retry(times)); + } + + /** + * Re-subscribe to the current {@code Single} if the given predicate returns {@code true} when the {@code Single} fails + * with an {@code onError}. + * <p> + * <img width="640" height="230" src="https://raw.githubusercontent.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.retry.f2.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code retry} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param predicate the predicate called with the resubscription count and the failure {@link Throwable} + * and should return {@code true} if a resubscription should happen + * @return the new {@code Single} instance + * @throws NullPointerException if {@code predicate} is {@code null} + * @since 2.0 + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Single<T> retry(@NonNull BiPredicate<? super Integer, ? super Throwable> predicate) { + return toSingle(toFlowable().retry(predicate)); + } + + /** + * Repeatedly re-subscribe at most times or until the predicate returns {@code false}, whichever happens first + * if it fails with an {@code onError}. + * <p> + * <img width="640" height="259" src="https://raw.githubusercontent.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.retry.nf.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code retry} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * <p>History: 2.1.8 - experimental + * @param times the number of times to resubscribe if the current {@code Single} fails + * @param predicate the predicate called with the failure {@link Throwable} + * and should return {@code true} if a resubscription should happen + * @return the new {@code Single} instance + * @throws NullPointerException if {@code predicate} is {@code null} + * @throws IllegalArgumentException if {@code times} is negative + * @since 2.2 + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Single<T> retry(long times, @NonNull Predicate<? super Throwable> predicate) { + return toSingle(toFlowable().retry(times, predicate)); + } + + /** + * Re-subscribe to the current {@code Single} if the given predicate returns {@code true} when the {@code Single} fails + * with an {@code onError}. + * <p> + * <img width="640" height="240" src="https://raw.githubusercontent.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.retry.f.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code retry} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param predicate the predicate called with the failure {@link Throwable} + * and should return {@code true} if a resubscription should happen + * @return the new {@code Single} instance + * @throws NullPointerException if {@code predicate} is {@code null} + * @since 2.0 + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Single<T> retry(@NonNull Predicate<? super Throwable> predicate) { + return toSingle(toFlowable().retry(predicate)); + } + + /** + * Retries until the given stop function returns {@code true}. + * <p> + * <img width="640" height="364" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.retryUntil.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code retryUntil} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param stop the function that should return {@code true} to stop retrying + * @return the new {@code Single} instance + * @throws NullPointerException if {@code stop} is {@code null} + * @since 3.0.0 + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public final Single<T> retryUntil(@NonNull BooleanSupplier stop) { + Objects.requireNonNull(stop, "stop is null"); + return retry(Long.MAX_VALUE, Functions.predicateReverseFor(stop)); + } + + /** + * Re-subscribes to the current {@code Single} if and when the {@link Publisher} returned by the handler + * function signals a value. + * <p> + * <img width="640" height="405" src="https://raw.githubusercontent.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.retryWhen.png" alt=""> + * <p> + * If the {@code Publisher} signals an {@code onComplete}, the resulting {@code Single} will signal a {@link NoSuchElementException}. + * <p> + * Note that the inner {@code Publisher} returned by the handler function should signal + * either {@code onNext}, {@code onError} or {@code onComplete} in response to the received + * {@link Throwable} to indicate the operator should retry or terminate. If the upstream to + * the operator is asynchronous, signaling {@code onNext} followed by {@code onComplete} immediately may + * result in the sequence to be completed immediately. Similarly, if this inner + * {@code Publisher} signals {@code onError} or {@code onComplete} while the upstream is + * active, the sequence is terminated with the same signal immediately. + * <p> + * The following example demonstrates how to retry an asynchronous source with a delay: + * <pre><code> + * Single.timer(1, TimeUnit.SECONDS) + * .doOnSubscribe(s -> System.out.println("subscribing")) + * .map(v -> { throw new RuntimeException(); }) + * .retryWhen(errors -> { + * AtomicInteger counter = new AtomicInteger(); + * return errors + * .takeWhile(e -> counter.getAndIncrement() != 3) + * .flatMap(e -> { + * System.out.println("delay retry by " + counter.get() + " second(s)"); + * return Flowable.timer(counter.get(), TimeUnit.SECONDS); + * }); + * }) + * .blockingGet(); + * </code></pre> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code retryWhen} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param handler the function that receives a {@link Flowable} of the error the {@code Single} emits and should + * return a {@code Publisher} that should signal a normal value (in response to the + * throwable the {@code Flowable} emits) to trigger a resubscription or signal an error to + * be the output of the resulting {@code Single} + * @return the new {@code Single} instance + * @throws NullPointerException if {@code handler} is {@code null} + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Single<T> retryWhen(@NonNull Function<? super Flowable<Throwable>, @NonNull ? extends Publisher<@NonNull ?>> handler) { + return toSingle(toFlowable().retryWhen(handler)); + } + + /** + * Wraps the given {@link SingleObserver}, catches any {@link RuntimeException}s thrown by its + * {@link SingleObserver#onSubscribe(Disposable)}, {@link SingleObserver#onSuccess(Object)} or + * {@link SingleObserver#onError(Throwable)} methods* and routes those to the global error handler + * via {@link RxJavaPlugins#onError(Throwable)}. + * <p> + * By default, the {@code Single} protocol forbids the {@code onXXX} methods to throw, but some + * {@code SingleObserver} implementation may do it anyway, causing undefined behavior in the + * upstream. This method and the underlying safe wrapper ensures such misbehaving consumers don't + * disrupt the protocol. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code safeSubscribe} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param observer the potentially misbehaving {@code SingleObserver} + * @throws NullPointerException if {@code observer} is {@code null} + * @see #subscribe(Consumer,Consumer) + * @since 3.0.0 + */ + @SchedulerSupport(SchedulerSupport.NONE) + public final void safeSubscribe(@NonNull SingleObserver<? super T> observer) { + Objects.requireNonNull(observer, "observer is null"); + subscribe(new SafeSingleObserver<>(observer)); + } + + /** + * Returns a {@link Flowable} which first runs the other {@link CompletableSource} + * then the current {@code Single} if the other completed normally. + * <p> + * <img width="640" height="360" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.startWith.c.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The returned {@code Flowable} honors the backpressure of the downstream consumer.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code startWith} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param other the other {@code CompletableSource} to run first + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code other} is {@code null} + * @since 3.0.0 + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + @BackpressureSupport(BackpressureKind.FULL) + public final Flowable<T> startWith(@NonNull CompletableSource other) { + Objects.requireNonNull(other, "other is null"); + return Flowable.concat(Completable.wrap(other).<T>toFlowable(), toFlowable()); + } + + /** + * Returns a {@link Flowable} which first runs the other {@link SingleSource} + * then the current {@code Single} if the other succeeded normally. + * <p> + * <img width="640" height="341" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.startWith.s.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The returned {@code Flowable} honors the backpressure of the downstream consumer.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code startWith} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param other the other {@code SingleSource} to run first + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code other} is {@code null} + * @since 3.0.0 + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + @BackpressureSupport(BackpressureKind.FULL) + public final Flowable<T> startWith(@NonNull SingleSource<T> other) { + Objects.requireNonNull(other, "other is null"); + return Flowable.concat(Single.wrap(other).toFlowable(), toFlowable()); + } + + /** + * Returns a {@link Flowable} which first runs the other {@link MaybeSource} + * then the current {@code Single} if the other succeeded or completed normally. + * <p> + * <img width="640" height="232" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.startWith.m.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The returned {@code Flowable} honors the backpressure of the downstream consumer.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code startWith} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param other the other {@code MaybeSource} to run first + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code other} is {@code null} + * @since 3.0.0 + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + @BackpressureSupport(BackpressureKind.FULL) + public final Flowable<T> startWith(@NonNull MaybeSource<T> other) { + Objects.requireNonNull(other, "other is null"); + return Flowable.concat(Maybe.wrap(other).toFlowable(), toFlowable()); + } + + /** + * Returns an {@link Observable} which first delivers the events + * of the other {@link ObservableSource} then runs the current {@code Single}. + * <p> + * <img width="640" height="175" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.startWith.o.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code startWith} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param other the other {@code ObservableSource} to run first + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code other} is {@code null} + * @since 3.0.0 + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public final Observable<T> startWith(@NonNull ObservableSource<T> other) { + Objects.requireNonNull(other, "other is null"); + return Observable.wrap(other).concatWith(this.toObservable()); + } + + /** + * Returns a {@link Flowable} which first delivers the events + * of the other {@link Publisher} then runs the current {@code Single}. + * <p> + * <img width="640" height="175" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.startWith.p.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The returned {@code Flowable} honors the backpressure of the downstream consumer + * and expects the other {@code Publisher} to honor it as well.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code startWith} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param other the other {@code Publisher} to run first + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code other} is {@code null} + * @since 3.0.0 + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + public final Flowable<T> startWith(@NonNull Publisher<T> other) { + Objects.requireNonNull(other, "other is null"); + return toFlowable().startWith(other); + } + + /** + * Subscribes to a {@code Single} but ignore its emission or notification. + * <p> + * <img width="640" height="340" src="https://raw.githubusercontent.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.subscribe.png" alt=""> + * <p> + * If the {@code Single} emits an error, it is wrapped into an + * {@link io.reactivex.rxjava3.exceptions.OnErrorNotImplementedException OnErrorNotImplementedException} + * and routed to the {@link RxJavaPlugins#onError(Throwable)} handler. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code subscribe} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @return the new {@link Disposable} instance that can be used for disposing the subscription at any time + * @see <a href="http://reactivex.io/documentation/operators/subscribe.html">ReactiveX operators documentation: Subscribe</a> + * @see #subscribe(Consumer, Consumer, DisposableContainer) + */ + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Disposable subscribe() { + return subscribe(Functions.emptyConsumer(), Functions.ON_ERROR_MISSING); + } + + /** + * Subscribes to a {@code Single} and provides a composite callback to handle the item it emits + * or any error notification it issues. + * <p> + * <img width="640" height="340" src="https://raw.githubusercontent.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.subscribe.c2.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code subscribe} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param onCallback + * the callback that receives either the success value or the failure {@link Throwable} + * (whichever is not {@code null}) + * @return the new {@link Disposable} instance that can be used for disposing the subscription at any time + * @throws NullPointerException + * if {@code onCallback} is {@code null} + * @see #subscribe(Consumer, Consumer, DisposableContainer) + * @see <a href="http://reactivex.io/documentation/operators/subscribe.html">ReactiveX operators documentation: Subscribe</a> + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public final Disposable subscribe(@NonNull BiConsumer<@Nullable ? super T, @Nullable ? super Throwable> onCallback) { + Objects.requireNonNull(onCallback, "onCallback is null"); + + BiConsumerSingleObserver<T> observer = new BiConsumerSingleObserver<>(onCallback); + subscribe(observer); + return observer; + } + + /** + * Subscribes to a {@code Single} and provides a callback to handle the item it emits. + * <p> + * <img width="640" height="341" src="https://raw.githubusercontent.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.subscribe.c.png" alt=""> + * <p> + * If the {@code Single} emits an error, it is wrapped into an + * {@link io.reactivex.rxjava3.exceptions.OnErrorNotImplementedException OnErrorNotImplementedException} + * and routed to the {@link RxJavaPlugins#onError(Throwable)} handler. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code subscribe} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param onSuccess + * the {@code Consumer<T>} you have designed to accept the emission from the {@code Single} + * @return the new {@link Disposable} instance that can be used for disposing the subscription at any time + * @throws NullPointerException + * if {@code onSuccess} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/subscribe.html">ReactiveX operators documentation: Subscribe</a> + * @see #subscribe(Consumer, Consumer, DisposableContainer) + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Disposable subscribe(@NonNull Consumer<? super T> onSuccess) { + return subscribe(onSuccess, Functions.ON_ERROR_MISSING); + } + + /** + * Subscribes to a {@code Single} and provides callbacks to handle the item it emits or any error notification it + * issues. + * <p> + * <img width="640" height="340" src="https://raw.githubusercontent.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.subscribe.cc.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code subscribe} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param onSuccess + * the {@code Consumer<T>} you have designed to accept the emission from the {@code Single} + * @param onError + * the {@code Consumer<Throwable>} you have designed to accept any error notification from the + * {@code Single} + * @return the new {@link Disposable} instance that can be used for disposing the subscription at any time + * @throws NullPointerException + * if {@code onSuccess} or {@code onError} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/subscribe.html">ReactiveX operators documentation: Subscribe</a> + * @see #subscribe(Consumer, Consumer, DisposableContainer) + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public final Disposable subscribe(@NonNull Consumer<? super T> onSuccess, @NonNull Consumer<? super Throwable> onError) { + Objects.requireNonNull(onSuccess, "onSuccess is null"); + Objects.requireNonNull(onError, "onError is null"); + + ConsumerSingleObserver<T> observer = new ConsumerSingleObserver<>(onSuccess, onError); + subscribe(observer); + return observer; + } + + /** + * Wraps the given onXXX callbacks into a {@link Disposable} {@link SingleObserver}, + * adds it to the given {@link DisposableContainer} and ensures, that if the upstream + * terminates or this particular {@code Disposable} is disposed, the {@code SingleObserver} is removed + * from the given container. + * <p> + * The {@code SingleObserver} will be removed after the callback for the terminal event has been invoked. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code subscribe} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param onSuccess the callback for upstream items + * @param onError the callback for an upstream error if any + * @param container the {@code DisposableContainer} (such as {@link CompositeDisposable}) to add and remove the + * created {@code Disposable} {@code SingleObserver} + * @return the {@code Disposable} that allows disposing the particular subscription. + * @throws NullPointerException + * if {@code onSuccess}, {@code onError} + * or {@code container} is {@code null} + * @since 3.1.0 + */ + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Disposable subscribe( + @NonNull Consumer<? super T> onSuccess, + @NonNull Consumer<? super Throwable> onError, + @NonNull DisposableContainer container) { + Objects.requireNonNull(onSuccess, "onSuccess is null"); + Objects.requireNonNull(onError, "onError is null"); + Objects.requireNonNull(container, "container is null"); + + DisposableAutoReleaseMultiObserver<T> observer = new DisposableAutoReleaseMultiObserver<>( + container, onSuccess, onError, Functions.EMPTY_ACTION); + container.add(observer); + subscribe(observer); + return observer; + } + + @SchedulerSupport(SchedulerSupport.NONE) + @Override + public final void subscribe(@NonNull SingleObserver<? super T> observer) { + Objects.requireNonNull(observer, "observer is null"); + + observer = RxJavaPlugins.onSubscribe(this, observer); + + Objects.requireNonNull(observer, "The RxJavaPlugins.onSubscribe hook returned a null SingleObserver. Please check the handler provided to RxJavaPlugins.setOnSingleSubscribe for invalid null returns. Further reading: https://github.com/ReactiveX/RxJava/wiki/Plugins"); + + try { + subscribeActual(observer); + } catch (NullPointerException ex) { + throw ex; + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + NullPointerException npe = new NullPointerException("subscribeActual failed"); + npe.initCause(ex); + throw npe; + } + } + + /** + * Implement this method in subclasses to handle the incoming {@link SingleObserver}s. + * <p>There is no need to call any of the plugin hooks on the current {@code Single} instance or + * the {@code SingleObserver}; all hooks and basic safeguards have been + * applied by {@link #subscribe(SingleObserver)} before this method gets called. + * @param observer the {@code SingleObserver} to handle, not {@code null} + */ + protected abstract void subscribeActual(@NonNull SingleObserver<? super T> observer); + + /** + * Subscribes a given {@link SingleObserver} (subclass) to this {@code Single} and returns the given + * {@code SingleObserver} as is. + * <p> + * <img width="640" height="338" src="https://raw.githubusercontent.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.subscribeWith.png" alt=""> + * <p>Usage example: + * <pre><code> + * Single<Integer> source = Single.just(1); + * CompositeDisposable composite = new CompositeDisposable(); + * + * DisposableSingleObserver<Integer> ds = new DisposableSingleObserver<>() { + * // ... + * }; + * + * composite.add(source.subscribeWith(ds)); + * </code></pre> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code subscribeWith} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <E> the type of the {@code SingleObserver} to use and return + * @param observer the {@code SingleObserver} (subclass) to use and return, not {@code null} + * @return the input {@code observer} + * @throws NullPointerException if {@code observer} is {@code null} + * @since 2.0 + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull E extends SingleObserver<? super T>> E subscribeWith(E observer) { + subscribe(observer); + return observer; + } + + /** + * Asynchronously subscribes {@link SingleObserver}s to this {@code Single} on the specified {@link Scheduler}. + * <p> + * <img width="640" height="305" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.subscribeOn.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>You specify which {@code Scheduler} this operator will use.</dd> + * </dl> + * + * @param scheduler + * the {@code Scheduler} to perform subscription actions on + * @return the new {@code Single} instance + * @throws NullPointerException if {@code scheduler} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/subscribeon.html">ReactiveX operators documentation: SubscribeOn</a> + * @see <a href="http://www.grahamlea.com/2014/07/rxjava-threading-examples/">RxJava Threading Examples</a> + * @see #observeOn + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.CUSTOM) + public final Single<T> subscribeOn(@NonNull Scheduler scheduler) { + Objects.requireNonNull(scheduler, "scheduler is null"); + return RxJavaPlugins.onAssembly(new SingleSubscribeOn<>(this, scheduler)); + } + + /** + * Measures the time (in milliseconds) between the subscription and success item emission + * of the current {@code Single} and signals it as a tuple ({@link Timed}) + * success value. + * <p> + * <img width="640" height="466" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.timeInterval.png" alt=""> + * <p> + * If the current {@code Single} fails, the resulting {@code Single} will + * pass along the signal to the downstream. To measure the time to error, + * use {@link #materialize()} and apply {@link #timeInterval()}. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code timeInterval} uses the {@code computation} {@link Scheduler} + * for determining the current time upon subscription and upon receiving the + * success item from the current {@code Single}.</dd> + * </dl> + * @return the new {@code Single} instance + * @since 3.0.0 + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.COMPUTATION) + public final Single<Timed<T>> timeInterval() { + return timeInterval(TimeUnit.MILLISECONDS, Schedulers.computation()); + } + + /** + * Measures the time (in milliseconds) between the subscription and success item emission + * of the current {@code Single} and signals it as a tuple ({@link Timed}) + * success value. + * <p> + * <img width="640" height="463" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.timeInterval.s.png" alt=""> + * <p> + * If the current {@code Single} fails, the resulting {@code Single} will + * pass along the signal to the downstream. To measure the time to error, + * use {@link #materialize()} and apply {@link #timeInterval(Scheduler)}. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code timeInterval} uses the provided {@link Scheduler} + * for determining the current time upon subscription and upon receiving the + * success item from the current {@code Single}.</dd> + * </dl> + * @param scheduler the {@code Scheduler} used for providing the current time + * @return the new {@code Single} instance + * @throws NullPointerException if {@code scheduler} is {@code null} + * @since 3.0.0 + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.CUSTOM) + public final Single<Timed<T>> timeInterval(@NonNull Scheduler scheduler) { + return timeInterval(TimeUnit.MILLISECONDS, scheduler); + } + + /** + * Measures the time between the subscription and success item emission + * of the current {@code Single} and signals it as a tuple ({@link Timed}) + * success value. + * <p> + * <img width="640" height="466" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.timeInterval.png" alt=""> + * <p> + * If the current {@code Single} fails, the resulting {@code Single} will + * pass along the signals to the downstream. To measure the time to error, + * use {@link #materialize()} and apply {@link #timeInterval(TimeUnit, Scheduler)}. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code timeInterval} uses the {@code computation} {@link Scheduler} + * for determining the current time upon subscription and upon receiving the + * success item from the current {@code Single}.</dd> + * </dl> + * @param unit the time unit for measurement + * @return the new {@code Single} instance + * @throws NullPointerException if {@code unit} is {@code null} + * @since 3.0.0 + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.COMPUTATION) + public final Single<Timed<T>> timeInterval(@NonNull TimeUnit unit) { + return timeInterval(unit, Schedulers.computation()); + } + + /** + * Measures the time between the subscription and success item emission + * of the current {@code Single} and signals it as a tuple ({@link Timed}) + * success value. + * <p> + * <img width="640" height="463" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.timeInterval.s.png" alt=""> + * <p> + * If the current {@code Single} is empty or fails, the resulting {@code Single} will + * pass along the signals to the downstream. To measure the time to termination, + * use {@link #materialize()} and apply {@link #timeInterval(TimeUnit, Scheduler)}. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code timeInterval} uses the provided {@link Scheduler} + * for determining the current time upon subscription and upon receiving the + * success item from the current {@code Single}.</dd> + * </dl> + * @param unit the time unit for measurement + * @param scheduler the {@code Scheduler} used for providing the current time + * @return the new {@code Single} instance + * @throws NullPointerException if {@code unit} or {@code scheduler} is {@code null} + * @since 3.0.0 + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.CUSTOM) + public final Single<Timed<T>> timeInterval(@NonNull TimeUnit unit, @NonNull Scheduler scheduler) { + Objects.requireNonNull(unit, "unit is null"); + Objects.requireNonNull(scheduler, "scheduler is null"); + return RxJavaPlugins.onAssembly(new SingleTimeInterval<>(this, unit, scheduler, true)); + } + + /** + * Combines the success value from the current {@code Single} with the current time (in milliseconds) of + * its reception, using the {@code computation} {@link Scheduler} as time source, + * then signals them as a {@link Timed} instance. + * <p> + * <img width="640" height="465" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.timestamp.png" alt=""> + * <p> + * If the current {@code Single} is empty or fails, the resulting {@code Single} will + * pass along the signals to the downstream. To get the timestamp of the error, + * use {@link #materialize()} and apply {@link #timestamp()}. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code timestamp} uses the {@code computation} {@code Scheduler} + * for determining the current time upon receiving the + * success item from the current {@code Single}.</dd> + * </dl> + * @return the new {@code Single} instance + * @since 3.0.0 + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.COMPUTATION) + public final Single<Timed<T>> timestamp() { + return timestamp(TimeUnit.MILLISECONDS, Schedulers.computation()); + } + + /** + * Combines the success value from the current {@code Single} with the current time (in milliseconds) of + * its reception, using the given {@link Scheduler} as time source, + * then signals them as a {@link Timed} instance. + * <p> + * <img width="640" height="465" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.timestamp.s.png" alt=""> + * <p> + * If the current {@code Single} is empty or fails, the resulting {@code Single} will + * pass along the signals to the downstream. To get the timestamp of the error, + * use {@link #materialize()} and apply {@link #timestamp(Scheduler)}. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code timestamp} uses the provided {@code Scheduler} + * for determining the current time upon receiving the + * success item from the current {@code Single}.</dd> + * </dl> + * @param scheduler the {@code Scheduler} used for providing the current time + * @return the new {@code Single} instance + * @throws NullPointerException if {@code scheduler} is {@code null} + * @since 3.0.0 + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.CUSTOM) + public final Single<Timed<T>> timestamp(@NonNull Scheduler scheduler) { + return timestamp(TimeUnit.MILLISECONDS, scheduler); + } + + /** + * Combines the success value from the current {@code Single} with the current time of + * its reception, using the {@code computation} {@link Scheduler} as time source, + * then signals it as a {@link Timed} instance. + * <p> + * <img width="640" height="465" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.timestamp.png" alt=""> + * <p> + * If the current {@code Single} is empty or fails, the resulting {@code Single} will + * pass along the signals to the downstream. To get the timestamp of the error, + * use {@link #materialize()} and apply {@link #timestamp(TimeUnit)}. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code timestamp} uses the {@code computation} {@code Scheduler}, + * for determining the current time upon receiving the + * success item from the current {@code Single}.</dd> + * </dl> + * @param unit the time unit for measurement + * @return the new {@code Single} instance + * @throws NullPointerException if {@code unit} is {@code null} + * @since 3.0.0 + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.COMPUTATION) + public final Single<Timed<T>> timestamp(@NonNull TimeUnit unit) { + return timestamp(unit, Schedulers.computation()); + } + + /** + * Combines the success value from the current {@code Single} with the current time of + * its reception, using the given {@link Scheduler} as time source, + * then signals it as a {@link Timed} instance. + * <p> + * <img width="640" height="465" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.timestamp.s.png" alt=""> + * <p> + * If the current {@code Single} is empty or fails, the resulting {@code Single} will + * pass along the signals to the downstream. To get the timestamp of the error, + * use {@link #materialize()} and apply {@link #timestamp(TimeUnit, Scheduler)}. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code timestamp} uses the provided {@code Scheduler}, + * which is used for determining the current time upon receiving the + * success item from the current {@code Single}.</dd> + * </dl> + * @param unit the time unit for measurement + * @param scheduler the {@code Scheduler} used for providing the current time + * @return the new {@code Single} instance + * @throws NullPointerException if {@code unit} or {@code scheduler} is {@code null} + * @since 3.0.0 + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.CUSTOM) + public final Single<Timed<T>> timestamp(@NonNull TimeUnit unit, @NonNull Scheduler scheduler) { + Objects.requireNonNull(unit, "unit is null"); + Objects.requireNonNull(scheduler, "scheduler is null"); + return RxJavaPlugins.onAssembly(new SingleTimeInterval<>(this, unit, scheduler, false)); + } + + /** + * Returns a {@code Single} that emits the item emitted by the current {@code Single} until a {@link CompletableSource} terminates. Upon + * termination of {@code other}, this will emit a {@link CancellationException} rather than go to + * {@link SingleObserver#onSuccess(Object)}. + * <p> + * <img width="640" height="333" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.takeUntil.c.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code takeUntil} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param other + * the {@code CompletableSource} whose termination will cause {@code takeUntil} to emit the item from the current + * {@code Single} + * @return the new {@code Single} that emits the item emitted by the current {@code Single} until such time as {@code other} terminates. + * @throws NullPointerException if {@code other} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/takeuntil.html">ReactiveX operators documentation: TakeUntil</a> + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public final Single<T> takeUntil(@NonNull CompletableSource other) { + Objects.requireNonNull(other, "other is null"); + return takeUntil(new CompletableToFlowable<T>(other)); + } + + /** + * Returns a {@code Single} that emits the item emitted by the current {@code Single} until a {@link Publisher} emits an item or completes. Upon + * emission of an item from {@code other}, this will emit a {@link CancellationException} rather than go to + * {@link SingleObserver#onSuccess(Object)}. + * <p> + * <img width="640" height="215" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.takeUntil.p.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The {@code other} publisher is consumed in an unbounded fashion but will be + * cancelled after the first item it produced.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code takeUntil} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param other + * the {@code Publisher} whose first emitted item or completion will cause {@code takeUntil} to emit {@code CancellationException} + * if the current {@code Single} hasn't completed till then + * @param <E> + * the type of items emitted by {@code other} + * @return the new {@code Single} that emits the item emitted by the current {@code Single} until such time as {@code other} emits + * its first item + * @throws NullPointerException if {@code other} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/takeuntil.html">ReactiveX operators documentation: TakeUntil</a> + */ + @BackpressureSupport(BackpressureKind.FULL) + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public final <@NonNull E> Single<T> takeUntil(@NonNull Publisher<E> other) { + Objects.requireNonNull(other, "other is null"); + return RxJavaPlugins.onAssembly(new SingleTakeUntil<>(this, other)); + } + + /** + * Returns a {@code Single} that emits the item emitted by the current {@code Single} until a second {@code Single} emits an item. Upon + * emission of an item from {@code other}, this will emit a {@link CancellationException} rather than go to + * {@link SingleObserver#onSuccess(Object)}. + * <p> + * <img width="640" height="314" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.takeUntil.s.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code takeUntil} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param other + * the {@code Single} whose emitted item will cause {@code takeUntil} to emit {@code CancellationException} + * if the current {@code Single} hasn't completed till then + * @param <E> + * the type of item emitted by {@code other} + * @return the new {@code Single} that emits the item emitted by the current {@code Single} until such time as {@code other} emits its item + * @throws NullPointerException if {@code other} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/takeuntil.html">ReactiveX operators documentation: TakeUntil</a> + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public final <@NonNull E> Single<T> takeUntil(@NonNull SingleSource<? extends E> other) { + Objects.requireNonNull(other, "other is null"); + return takeUntil(new SingleToFlowable<E>(other)); + } + + /** + * Signals a {@link TimeoutException} if the current {@code Single} doesn't signal a success value within the + * specified timeout window. + * <p> + * <img width="640" height="334" src="https://raw.githubusercontent.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.timeout.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code timeout} signals the {@code TimeoutException} on the {@code computation} {@link Scheduler}.</dd> + * </dl> + * @param timeout the timeout amount + * @param unit the time unit + * @return the new {@code Single} instance + * @throws NullPointerException if {@code unit} is {@code null} + * @since 2.0 + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.COMPUTATION) + @NonNull + public final Single<T> timeout(long timeout, @NonNull TimeUnit unit) { + return timeout0(timeout, unit, Schedulers.computation(), null); + } + + /** + * Signals a {@link TimeoutException} if the current {@code Single} doesn't signal a success value within the + * specified timeout window. + * <p> + * <img width="640" height="334" src="https://raw.githubusercontent.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.timeout.s.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code timeout} signals the {@code TimeoutException} on the {@link Scheduler} you specify.</dd> + * </dl> + * @param timeout the timeout amount + * @param unit the time unit + * @param scheduler the target {@code Scheduler} where the timeout is awaited and the {@code TimeoutException} + * signaled + * @return the new {@code Single} instance + * @throws NullPointerException if {@code unit} or {@code scheduler} is {@code null} + * @since 2.0 + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.CUSTOM) + @NonNull + public final Single<T> timeout(long timeout, @NonNull TimeUnit unit, @NonNull Scheduler scheduler) { + return timeout0(timeout, unit, scheduler, null); + } + + /** + * Runs the current {@code Single} and if it doesn't signal within the specified timeout window, it is + * disposed and the other {@link SingleSource} subscribed to. + * <p> + * <img width="640" height="283" src="https://raw.githubusercontent.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.timeout.sb.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code timeout} subscribes to the other {@code SingleSource} on the {@link Scheduler} you specify.</dd> + * </dl> + * @param timeout the timeout amount + * @param unit the time unit + * @param scheduler the {@code Scheduler} where the timeout is awaited and the subscription to other happens + * @param fallback the other {@code SingleSource} that gets subscribed to if the current {@code Single} times out + * @return the new {@code Single} instance + * @throws NullPointerException if {@code unit}, {@code scheduler} or {@code fallback} is {@code null} + * @since 2.0 + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.CUSTOM) + public final Single<T> timeout(long timeout, @NonNull TimeUnit unit, @NonNull Scheduler scheduler, @NonNull SingleSource<? extends T> fallback) { + Objects.requireNonNull(fallback, "fallback is null"); + return timeout0(timeout, unit, scheduler, fallback); + } + + /** + * Runs the current {@code Single} and if it doesn't signal within the specified timeout window, it is + * disposed and the other {@link SingleSource} subscribed to. + * <p> + * <img width="640" height="282" src="https://raw.githubusercontent.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.timeout.b.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code timeout} subscribes to the other {@code SingleSource} on + * the {@code computation} {@link Scheduler}.</dd> + * </dl> + * @param timeout the timeout amount + * @param unit the time unit + * @param fallback the other {@code SingleSource} that gets subscribed to if the current {@code Single} times out + * @return the new {@code Single} instance + * @throws NullPointerException + * if {@code fallback} or {@code unit} is {@code null} + * @since 2.0 + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.COMPUTATION) + public final Single<T> timeout(long timeout, @NonNull TimeUnit unit, @NonNull SingleSource<? extends T> fallback) { + Objects.requireNonNull(fallback, "fallback is null"); + return timeout0(timeout, unit, Schedulers.computation(), fallback); + } + + private Single<T> timeout0(final long timeout, final TimeUnit unit, final Scheduler scheduler, final SingleSource<? extends T> fallback) { + Objects.requireNonNull(unit, "unit is null"); + Objects.requireNonNull(scheduler, "scheduler is null"); + return RxJavaPlugins.onAssembly(new SingleTimeout<>(this, timeout, unit, scheduler, fallback)); + } + + /** + * Calls the specified converter function during assembly time and returns its resulting value. + * <p> + * <img width="640" height="553" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.to.v3.png" alt=""> + * <p> + * This allows fluent conversion to any other type. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code to} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * <p>History: 2.1.7 - experimental + * @param <R> the resulting object type + * @param converter the function that receives the current {@code Single} instance and returns a value + * @return the converted value + * @throws NullPointerException if {@code converter} is {@code null} + * @since 2.2 + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + public final <R> R to(@NonNull SingleConverter<T, ? extends R> converter) { + return Objects.requireNonNull(converter, "converter is null").apply(this); + } + + /** + * Returns a {@link Completable} that ignores the success value of this {@code Single} + * and signals {@code onComplete} instead. + * <p> + * <img width="640" height="436" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.ignoreElement.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code ignoreElement} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @return the new {@code Completable} instance + * @since 2.1.13 + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Completable ignoreElement() { + return RxJavaPlugins.onAssembly(new CompletableFromSingle<>(this)); + } + + /** + * Converts this {@code Single} into a {@link Flowable}. + * <p> + * <img width="640" height="462" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.toFlowable.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The returned {@code Flowable} honors the backpressure of the downstream consumer.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code toFlowable} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @return the new {@code Flowable} instance + */ + @BackpressureSupport(BackpressureKind.FULL) + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @SuppressWarnings("unchecked") + @NonNull + public final Flowable<T> toFlowable() { + if (this instanceof FuseToFlowable) { + return ((FuseToFlowable<T>)this).fuseToFlowable(); + } + return RxJavaPlugins.onAssembly(new SingleToFlowable<>(this)); + } + + /** + * Returns a {@link Future} representing the single value emitted by this {@code Single}. + * <p> + * <img width="640" height="467" src="https://github.com/ReactiveX/RxJava/wiki/images/rx-operators/Single.toFuture.v3.png" alt=""> + * <p> + * Cancelling the {@code Future} will cancel the subscription to the current {@code Single}. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code toFuture} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @return the new {@code Future} instance + * @see <a href="http://reactivex.io/documentation/operators/to.html">ReactiveX documentation: To</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Future<T> toFuture() { + return subscribeWith(new FutureMultiObserver<>()); + } + + /** + * Converts this {@code Single} into a {@link Maybe}. + * <p> + * <img width="640" height="463" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.toMaybe.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code toMaybe} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @return the new {@code Maybe} instance + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @SuppressWarnings("unchecked") + @NonNull + public final Maybe<T> toMaybe() { + if (this instanceof FuseToMaybe) { + return ((FuseToMaybe<T>)this).fuseToMaybe(); + } + return RxJavaPlugins.onAssembly(new MaybeFromSingle<>(this)); + } + /** + * Converts this {@code Single} into an {@link Observable}. + * <p> + * <img width="640" height="305" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.toObservable.v3.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code toObservable} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @return the new {@code Observable} instance + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @SuppressWarnings("unchecked") + @NonNull + public final Observable<T> toObservable() { + if (this instanceof FuseToObservable) { + return ((FuseToObservable<T>)this).fuseToObservable(); + } + return RxJavaPlugins.onAssembly(new SingleToObservable<>(this)); + } + + /** + * Returns a {@code Single} which makes sure when a {@link SingleObserver} disposes the {@link Disposable}, + * that call is propagated up on the specified {@link Scheduler}. + * <p> + * <img width="640" height="693" src="https://raw.githubusercontent.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.unsubscribeOn.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code unsubscribeOn} calls {@code dispose()} of the upstream on the {@code Scheduler} you specify.</dd> + * </dl> + * <p>History: 2.0.9 - experimental + * @param scheduler the target scheduler where to execute the disposal + * @return the new {@code Single} instance + * @throws NullPointerException if {@code scheduler} is {@code null} + * @since 2.2 + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.CUSTOM) + public final Single<T> unsubscribeOn(@NonNull Scheduler scheduler) { + Objects.requireNonNull(scheduler, "scheduler is null"); + return RxJavaPlugins.onAssembly(new SingleUnsubscribeOn<>(this, scheduler)); + } + + /** + * Returns a {@code Single} that emits the result of applying a specified function to the pair of items emitted by + * the current {@code Single} and another specified {@link SingleSource}. + * <p> + * <img width="640" height="422" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.zipWith.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code zipWith} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <U> + * the type of items emitted by the {@code other} {@code Single} + * @param <R> + * the type of items emitted by the resulting {@code Single} + * @param other + * the other {@code SingleSource} + * @param zipper + * a function that combines the pairs of items from the two {@code SingleSource}s to generate the items to + * be emitted by the resulting {@code Single} + * @return the new {@code Single} that pairs up values from the current {@code Single} and the {@code other} {@code SingleSource} + * and emits the results of {@code zipFunction} applied to these pairs + * @throws NullPointerException if {@code other} or {@code zipper} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/zip.html">ReactiveX operators documentation: Zip</a> + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull U, @NonNull R> Single<R> zipWith(@NonNull SingleSource<U> other, @NonNull BiFunction<? super T, ? super U, ? extends R> zipper) { + return zip(this, other, zipper); + } + + // ------------------------------------------------------------------------- + // Fluent test support, super handy and reduces test preparation boilerplate + // ------------------------------------------------------------------------- + /** + * Creates a {@link TestObserver} and subscribes it to this {@code Single}. + * <p> + * <img width="640" height="442" src="https://raw.githubusercontent.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.test.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code test} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @return the new {@code TestObserver} instance + * @since 2.0 + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final TestObserver<T> test() { + TestObserver<T> to = new TestObserver<>(); + subscribe(to); + return to; + } + + /** + * Creates a {@link TestObserver} optionally in cancelled state, then subscribes it to this {@code Single}. + * <p> + * <img width="640" height="482" src="https://raw.githubusercontent.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.test.b.png" alt=""> + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code test} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param dispose if {@code true}, the {@code TestObserver} will be cancelled before subscribing to this + * {@code Single}. + * @return the new {@code TestObserver} instance + * @since 2.0 + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final TestObserver<T> test(boolean dispose) { + TestObserver<T> to = new TestObserver<>(); + + if (dispose) { + to.dispose(); + } + + subscribe(to); + return to; + } + + @NonNull + private static <T> Single<T> toSingle(@NonNull Flowable<T> source) { + return RxJavaPlugins.onAssembly(new FlowableSingleSingle<>(source, null)); + } + + // ------------------------------------------------------------------------- + // JDK 8 Support + // ------------------------------------------------------------------------- + + /** + * Signals the completion value or error of the given (hot) {@link CompletionStage}-based asynchronous calculation. + * <p> + * <img width="640" height="262" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/fromCompletionStage.s.png" alt=""> + * <p> + * Note that the operator takes an already instantiated, running or terminated {@code CompletionStage}. + * If the {@code CompletionStage} is to be created per consumer upon subscription, use {@link #defer(Supplier)} + * around {@code fromCompletionStage}: + * <pre><code> + * Single.defer(() -> Single.fromCompletionStage(createCompletionStage())); + * </code></pre> + * <p> + * If the {@code CompletionStage} completes with {@code null}, the resulting {@code Single} is terminated with + * a {@link NullPointerException}. + * <p> + * Canceling the flow can't cancel the execution of the {@code CompletionStage} because {@code CompletionStage} + * itself doesn't support cancellation. Instead, the operator detaches from the {@code CompletionStage}. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code fromCompletionStage} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <T> the element type of the {@code CompletionStage} + * @param stage the {@code CompletionStage} to convert to {@code Single} and signal its success value or error + * @return the new {@code Single} instance + * @throws NullPointerException if {@code stage} is {@code null} + * @since 3.0.0 + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public static <@NonNull T> Single<@NonNull T> fromCompletionStage(@NonNull CompletionStage<T> stage) { + Objects.requireNonNull(stage, "stage is null"); + return RxJavaPlugins.onAssembly(new SingleFromCompletionStage<>(stage)); + } + + /** + * Maps the upstream success value into an {@link Optional} and emits the contained item if not empty as a {@link Maybe}. + * <p> + * <img width="640" height="323" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/mapOptional.s.png" alt=""> + * + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code mapOptional} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <R> the non-{@code null} output type + * @param mapper the function that receives the upstream success item and should return a <em>non-empty</em> {@code Optional} + * to emit as the success output or an <em>empty</em> {@code Optional} to complete the {@code Maybe} + * @return the new {@code Maybe} instance + * @throws NullPointerException if {@code mapper} is {@code null} + * @since 3.0.0 + * @see #map(Function) + * @see #filter(Predicate) + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull R> Maybe<R> mapOptional(@NonNull Function<? super T, @NonNull Optional<? extends R>> mapper) { + Objects.requireNonNull(mapper, "mapper is null"); + return RxJavaPlugins.onAssembly(new SingleMapOptional<>(this, mapper)); + } + + /** + * Signals the upstream success item (or error) via a {@link CompletionStage}. + * <p> + * <img width="640" height="321" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/toCompletionStage.s.png" alt=""> + * <p> + * The upstream can be canceled by converting the resulting {@code CompletionStage} into + * {@link CompletableFuture} via {@link CompletionStage#toCompletableFuture()} and + * calling {@link CompletableFuture#cancel(boolean)} on it. + * The upstream will be also cancelled if the resulting {@code CompletionStage} is converted to and + * completed manually by {@link CompletableFuture#complete(Object)} or {@link CompletableFuture#completeExceptionally(Throwable)}. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code toCompletionStage} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @return the new {@code CompletionStage} instance + * @since 3.0.0 + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final CompletionStage<T> toCompletionStage() { + return subscribeWith(new CompletionStageConsumer<>(false, null)); + } + + /** + * Maps the upstream succecss value into a Java {@link Stream} and emits its + * items to the downstream consumer as a {@link Flowable}. + * <img width="640" height="247" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/flattenStreamAsFlowable.s.png" alt=""> + * <p> + * The operator closes the {@code Stream} upon cancellation and when it terminates. The exceptions raised when + * closing a {@code Stream} are routed to the global error handler ({@link RxJavaPlugins#onError(Throwable)}. + * If a {@code Stream} should not be closed, turn it into an {@link Iterable} and use {@link #flattenAsFlowable(Function)}: + * <pre><code> + * source.flattenAsFlowable(item -> createStream(item)::iterator); + * </code></pre> + * <p> + * Primitive streams are not supported and items have to be boxed manually (e.g., via {@link IntStream#boxed()}): + * <pre><code> + * source.flattenStreamAsFlowable(item -> IntStream.rangeClosed(1, 10).boxed()); + * </code></pre> + * <p> + * {@code Stream} does not support concurrent usage so creating and/or consuming the same instance multiple times + * from multiple threads can lead to undefined behavior. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from downstream and iterates the given {@code Stream} + * on demand (i.e., when requested).</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code flattenStreamAsFlowable} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <R> the element type of the {@code Stream} and the output {@code Flowable} + * @param mapper the function that receives the upstream success item and should + * return a {@code Stream} of values to emit. + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code mapper} is {@code null} + * @since 3.0.0 + * @see #flattenAsFlowable(Function) + * @see #flattenStreamAsObservable(Function) + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @BackpressureSupport(BackpressureKind.FULL) + @NonNull + public final <@NonNull R> Flowable<R> flattenStreamAsFlowable(@NonNull Function<? super T, @NonNull ? extends Stream<? extends R>> mapper) { + Objects.requireNonNull(mapper, "mapper is null"); + return RxJavaPlugins.onAssembly(new SingleFlattenStreamAsFlowable<>(this, mapper)); + } + + /** + * Maps the upstream succecss value into a Java {@link Stream} and emits its + * items to the downstream consumer as an {@link Observable}. + * <p> + * <img width="640" height="241" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/flattenStreamAsObservable.s.png" alt=""> + * <p> + * The operator closes the {@code Stream} upon cancellation and when it terminates. The exceptions raised when + * closing a {@code Stream} are routed to the global error handler ({@link RxJavaPlugins#onError(Throwable)}. + * If a {@code Stream} should not be closed, turn it into an {@link Iterable} and use {@link #flattenAsObservable(Function)}: + * <pre><code> + * source.flattenAsObservable(item -> createStream(item)::iterator); + * </code></pre> + * <p> + * Primitive streams are not supported and items have to be boxed manually (e.g., via {@link IntStream#boxed()}): + * <pre><code> + * source.flattenStreamAsObservable(item -> IntStream.rangeClosed(1, 10).boxed()); + * </code></pre> + * <p> + * {@code Stream} does not support concurrent usage so creating and/or consuming the same instance multiple times + * from multiple threads can lead to undefined behavior. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code flattenStreamAsObservable} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <R> the element type of the {@code Stream} and the output {@code Observable} + * @param mapper the function that receives the upstream success item and should + * return a {@code Stream} of values to emit. + * @return the new {@code Observable} instance + * @throws NullPointerException if {@code mapper} is {@code null} + * @since 3.0.0 + * @see #flattenAsObservable(Function) + * @see #flattenStreamAsFlowable(Function) + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull R> Observable<R> flattenStreamAsObservable(@NonNull Function<? super T, @NonNull ? extends Stream<? extends R>> mapper) { + Objects.requireNonNull(mapper, "mapper is null"); + return RxJavaPlugins.onAssembly(new SingleFlattenStreamAsObservable<>(this, mapper)); + } +} diff --git a/src/main/java/io/reactivex/rxjava3/core/SingleConverter.java b/src/main/java/io/reactivex/rxjava3/core/SingleConverter.java new file mode 100644 index 0000000000..960a17d9d5 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/core/SingleConverter.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.core; + +import io.reactivex.rxjava3.annotations.NonNull; + +/** + * Convenience interface and callback used by the {@link Single#to} operator to turn a {@link Single} into another + * value fluently. + * <p>History: 2.1.7 - experimental + * @param <T> the upstream type + * @param <R> the output type + * @since 2.2 + */ +@FunctionalInterface +public interface SingleConverter<@NonNull T, @NonNull R> { + /** + * Applies a function to the upstream {@link Single} and returns a converted value of type {@code R}. + * + * @param upstream the upstream {@code Single} instance + * @return the converted value + */ + R apply(@NonNull Single<T> upstream); +} diff --git a/src/main/java/io/reactivex/rxjava3/core/SingleEmitter.java b/src/main/java/io/reactivex/rxjava3/core/SingleEmitter.java new file mode 100644 index 0000000000..4b18a81644 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/core/SingleEmitter.java @@ -0,0 +1,103 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.core; + +import io.reactivex.rxjava3.annotations.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.functions.*; + +/** + * Abstraction over an RxJava {@link SingleObserver} that allows associating + * a resource with it. + * <p> + * All methods are safe to call from multiple threads, but note that there is no guarantee + * whose terminal event will win and get delivered to the downstream. + * <p> + * Calling {@link #onSuccess(Object)} multiple times has no effect. + * Calling {@link #onError(Throwable)} multiple times or after {@code onSuccess} will route the + * exception into the global error handler via {@link io.reactivex.rxjava3.plugins.RxJavaPlugins#onError(Throwable)}. + * <p> + * The emitter allows the registration of a single resource, in the form of a {@link Disposable} + * or {@link Cancellable} via {@link #setDisposable(Disposable)} or {@link #setCancellable(Cancellable)} + * respectively. The emitter implementations will dispose/cancel this instance when the + * downstream cancels the flow or after the event generator logic calls {@link #onSuccess(Object)}, + * {@link #onError(Throwable)}, or when {@link #tryOnError(Throwable)} succeeds. + * <p> + * Only one {@code Disposable} or {@code Cancellable} object can be associated with the emitter at + * a time. Calling either {@code set} method will dispose/cancel any previous object. If there + * is a need for handling multiple resources, one can create a {@link io.reactivex.rxjava3.disposables.CompositeDisposable} + * and associate that with the emitter instead. + * <p> + * The {@link Cancellable} is logically equivalent to {@code Disposable} but allows using cleanup logic that can + * throw a checked exception (such as many {@code close()} methods on Java IO components). Since + * the release of resources happens after the terminal events have been delivered or the sequence gets + * cancelled, exceptions throw within {@code Cancellable} are routed to the global error handler via + * {@link io.reactivex.rxjava3.plugins.RxJavaPlugins#onError(Throwable)}. + * + * @param <T> the value type to emit + */ +public interface SingleEmitter<@NonNull T> { + + /** + * Signal a success value. + * @param t the value, not null + */ + void onSuccess(@NonNull T t); + + /** + * Signal an exception. + * @param t the exception, not {@code null} + */ + void onError(@NonNull Throwable t); + + /** + * Sets a {@link Disposable} on this emitter; any previous {@code Disposable} + * or {@link Cancellable} will be disposed/cancelled. + * <p>This method is thread-safe. + * @param d the {@code Disposable}, {@code null} is allowed + */ + void setDisposable(@Nullable Disposable d); + + /** + * Sets a Cancellable on this emitter; any previous {@link Disposable} + * or {@link Cancellable} will be disposed/cancelled. + * <p>This method is thread-safe. + * @param c the {@code Cancellable} resource, {@code null} is allowed + */ + void setCancellable(@Nullable Cancellable c); + + /** + * Returns true if the downstream disposed the sequence or the + * emitter was terminated via {@link #onSuccess(Object)}, {@link #onError(Throwable)}, + * or a successful {@link #tryOnError(Throwable)}. + * <p>This method is thread-safe. + * @return true if the downstream disposed the sequence or the emitter was terminated + */ + boolean isDisposed(); + + /** + * Attempts to emit the specified {@link Throwable} error if the downstream + * hasn't cancelled the sequence or is otherwise terminated, returning false + * if the emission is not allowed to happen due to lifecycle restrictions. + * <p> + * Unlike {@link #onError(Throwable)}, the {@link io.reactivex.rxjava3.plugins.RxJavaPlugins#onError(Throwable) RxjavaPlugins.onError} + * is not called if the error could not be delivered. + * <p>History: 2.1.1 - experimental + * @param t the throwable error to signal if possible + * @return true if successful, false if the downstream is not able to accept further + * events + * @since 2.2 + */ + boolean tryOnError(@NonNull Throwable t); +} diff --git a/src/main/java/io/reactivex/rxjava3/core/SingleObserver.java b/src/main/java/io/reactivex/rxjava3/core/SingleObserver.java new file mode 100644 index 0000000000..a110d4bcff --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/core/SingleObserver.java @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.core; + +import io.reactivex.rxjava3.annotations.NonNull; +import io.reactivex.rxjava3.disposables.Disposable; + +/** + * Provides a mechanism for receiving push-based notification of a single value or an error. + * <p> + * When a {@code SingleObserver} is subscribed to a {@link SingleSource} through the {@link SingleSource#subscribe(SingleObserver)} method, + * the {@code SingleSource} calls {@link #onSubscribe(Disposable)} with a {@link Disposable} that allows + * disposing the sequence at any time. A well-behaved + * {@code SingleSource} will call a {@code SingleObserver}'s {@link #onSuccess(Object)} method exactly once or the {@code SingleObserver}'s + * {@link #onError} method exactly once as they are considered mutually exclusive <strong>terminal signals</strong>. + * <p> + * Calling the {@code SingleObserver}'s method must happen in a serialized fashion, that is, they must not + * be invoked concurrently by multiple threads in an overlapping fashion and the invocation pattern must + * adhere to the following protocol: + * <pre><code> onSubscribe (onSuccess | onError)?</code></pre> + * <p> + * Subscribing a {@code SingleObserver} to multiple {@code SingleSource}s is not recommended. If such reuse + * happens, it is the duty of the {@code SingleObserver} implementation to be ready to receive multiple calls to + * its methods and ensure proper concurrent behavior of its business logic. + * <p> + * Calling {@link #onSubscribe(Disposable)}, {@link #onSuccess(Object)} or {@link #onError(Throwable)} with a + * {@code null} argument is forbidden. + * <p> + * The implementations of the {@code onXXX} methods should avoid throwing runtime exceptions other than the following cases: + * <ul> + * <li>If the argument is {@code null}, the methods can throw a {@code NullPointerException}. + * Note though that RxJava prevents {@code null}s to enter into the flow and thus there is generally no + * need to check for nulls in flows assembled from standard sources and intermediate operators. + * </li> + * <li>If there is a fatal error (such as {@code VirtualMachineError}).</li> + * </ul> + * @see <a href="http://reactivex.io/documentation/observable.html">ReactiveX documentation: Observable</a> + * @param <T> + * the type of item the SingleObserver expects to observe + * @since 2.0 + */ +public interface SingleObserver<@NonNull T> { + + /** + * Provides the {@link SingleObserver} with the means of cancelling (disposing) the + * connection (channel) with the Single in both + * synchronous (from within {@code onSubscribe(Disposable)} itself) and asynchronous manner. + * @param d the Disposable instance whose {@link Disposable#dispose()} can + * be called anytime to cancel the connection + * @since 2.0 + */ + void onSubscribe(@NonNull Disposable d); + + /** + * Notifies the {@link SingleObserver} with a single item and that the {@link Single} has finished sending + * push-based notifications. + * <p> + * The {@code Single} will not call this method if it calls {@link #onError}. + * + * @param t + * the item emitted by the {@code Single} + */ + void onSuccess(@NonNull T t); + + /** + * Notifies the {@link SingleObserver} that the {@link Single} has experienced an error condition. + * <p> + * If the {@code Single} calls this method, it will not thereafter call {@link #onSuccess}. + * + * @param e + * the exception encountered by the {@code Single} + */ + void onError(@NonNull Throwable e); +} diff --git a/src/main/java/io/reactivex/rxjava3/core/SingleOnSubscribe.java b/src/main/java/io/reactivex/rxjava3/core/SingleOnSubscribe.java new file mode 100644 index 0000000000..e8b9e89f8b --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/core/SingleOnSubscribe.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.core; + +import io.reactivex.rxjava3.annotations.NonNull; + +/** + * A functional interface that has a {@code subscribe()} method that receives + * a {@link SingleEmitter} instance that allows pushing + * an event in a cancellation-safe manner. + * + * @param <T> the value type pushed + */ +@FunctionalInterface +public interface SingleOnSubscribe<@NonNull T> { + + /** + * Called for each {@link SingleObserver} that subscribes. + * @param emitter the safe emitter instance, never {@code null} + * @throws Throwable on error + */ + void subscribe(@NonNull SingleEmitter<T> emitter) throws Throwable; +} + diff --git a/src/main/java/io/reactivex/rxjava3/core/SingleOperator.java b/src/main/java/io/reactivex/rxjava3/core/SingleOperator.java new file mode 100644 index 0000000000..3b9c4a53c5 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/core/SingleOperator.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.core; + +import io.reactivex.rxjava3.annotations.NonNull; + +/** + * Interface to map/wrap a downstream {@link SingleObserver} to an upstream {@code SingleObserver}. + * + * @param <Downstream> the value type of the downstream + * @param <Upstream> the value type of the upstream + */ +@FunctionalInterface +public interface SingleOperator<@NonNull Downstream, @NonNull Upstream> { + /** + * Applies a function to the child {@link SingleObserver} and returns a new parent {@code SingleObserver}. + * @param observer the child {@code SingleObserver} instance + * @return the parent {@code SingleObserver} instance + * @throws Throwable on failure + */ + @NonNull + SingleObserver<? super Upstream> apply(@NonNull SingleObserver<? super Downstream> observer) throws Throwable; +} diff --git a/src/main/java/io/reactivex/rxjava3/core/SingleSource.java b/src/main/java/io/reactivex/rxjava3/core/SingleSource.java new file mode 100644 index 0000000000..16147fd55b --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/core/SingleSource.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.core; + +import io.reactivex.rxjava3.annotations.NonNull; + +/** + * Represents a basic {@link Single} source base interface, + * consumable via an {@link SingleObserver}. + * <p> + * This class also serves the base type for custom operators wrapped into + * Single via {@link Single#create(SingleOnSubscribe)}. + * + * @param <T> the element type + * @since 2.0 + */ +@FunctionalInterface +public interface SingleSource<@NonNull T> { + + /** + * Subscribes the given {@link SingleObserver} to this {@link SingleSource} instance. + * @param observer the {@code SingleObserver}, not {@code null} + * @throws NullPointerException if {@code observer} is {@code null} + */ + void subscribe(@NonNull SingleObserver<? super T> observer); +} diff --git a/src/main/java/io/reactivex/rxjava3/core/SingleTransformer.java b/src/main/java/io/reactivex/rxjava3/core/SingleTransformer.java new file mode 100644 index 0000000000..3ece315f97 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/core/SingleTransformer.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.core; + +import io.reactivex.rxjava3.annotations.NonNull; + +/** + * Interface to compose {@link Single}s. + * + * @param <Upstream> the upstream value type + * @param <Downstream> the downstream value type + */ +@FunctionalInterface +public interface SingleTransformer<@NonNull Upstream, @NonNull Downstream> { + /** + * Applies a function to the upstream {@link Single} and returns a {@link SingleSource} with + * optionally different element type. + * @param upstream the upstream {@code Single} instance + * @return the transformed {@code SingleSource} instance + */ + @NonNull + SingleSource<Downstream> apply(@NonNull Single<Upstream> upstream); +} diff --git a/src/main/java/io/reactivex/rxjava3/core/package-info.java b/src/main/java/io/reactivex/rxjava3/core/package-info.java new file mode 100644 index 0000000000..ee76f76a64 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/core/package-info.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +/** + * Base reactive classes: {@link io.reactivex.rxjava3.core.Flowable}, {@link io.reactivex.rxjava3.core.Observable}, + * {@link io.reactivex.rxjava3.core.Single}, {@link io.reactivex.rxjava3.core.Maybe} and + * {@link io.reactivex.rxjava3.core.Completable}; base reactive consumers; + * other common base interfaces. + * + * <p>A library that enables subscribing to and composing asynchronous events and + * callbacks.</p> + * <p>The Flowable/Subscriber, Observable/Observer, Single/SingleObserver and + * Completable/CompletableObserver interfaces and associated operators (in + * the {@code io.reactivex.internal.operators} package) are inspired by the + * Reactive Rx library in Microsoft .NET but designed and implemented on + * the more advanced Reactive-Streams ( http://www.reactivestreams.org ) principles.</p> + * <p> + * More information can be found at <a + * href="http://msdn.microsoft.com/en-us/data/gg577609">http://msdn.microsoft.com/en-us/data/gg577609</a>. + * </p> + * + * + * <p>Compared with the Microsoft implementation: + * <ul> + * <li>Observable == IObservable (base type)</li> + * <li>Observer == IObserver (event consumer)</li> + * <li>Disposable == IDisposable (resource/cancellation management)</li> + * <li>Observable == Observable (factory methods)</li> + * <li>Flowable == IAsyncEnumerable (backpressure)</li> + * <li>Subscriber == IAsyncEnumerator</li> + * </ul> + * The Single and Completable reactive base types have no equivalent in Rx.NET as of 3.x. + * + * <p>Services which intend on exposing data asynchronously and wish + * to allow reactive processing and composition can implement the + * {@link io.reactivex.rxjava3.core.Flowable}, {@link io.reactivex.rxjava3.core.Observable}, {@link io.reactivex.rxjava3.core.Single}, + * {@link io.reactivex.rxjava3.core.Maybe} or {@link io.reactivex.rxjava3.core.Completable} class which then allow + * consumers to subscribe to them and receive events.</p> + * <p>Usage examples can be found on the {@link io.reactivex.rxjava3.core.Flowable}/{@link io.reactivex.rxjava3.core.Observable} and {@link org.reactivestreams.Subscriber} classes.</p> + */ +package io.reactivex.rxjava3.core; + diff --git a/src/main/java/io/reactivex/rxjava3/disposables/ActionDisposable.java b/src/main/java/io/reactivex/rxjava3/disposables/ActionDisposable.java new file mode 100644 index 0000000000..4caad11d79 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/disposables/ActionDisposable.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.disposables; + +import io.reactivex.rxjava3.annotations.NonNull; +import io.reactivex.rxjava3.functions.Action; +import io.reactivex.rxjava3.internal.util.ExceptionHelper; + +/** + * A Disposable container that manages an {@link Action} instance. + */ +final class ActionDisposable extends ReferenceDisposable<Action> { + + private static final long serialVersionUID = -8219729196779211169L; + + ActionDisposable(Action value) { + super(value); + } + + @Override + protected void onDisposed(@NonNull Action value) { + try { + value.run(); + } catch (Throwable ex) { + throw ExceptionHelper.wrapOrThrow(ex); + } + } + + @Override + public String toString() { + return "ActionDisposable(disposed=" + isDisposed() + ", " + get() + ")"; + } +} diff --git a/src/main/java/io/reactivex/rxjava3/disposables/AutoCloseableDisposable.java b/src/main/java/io/reactivex/rxjava3/disposables/AutoCloseableDisposable.java new file mode 100644 index 0000000000..34cdaed0eb --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/disposables/AutoCloseableDisposable.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.disposables; + +import io.reactivex.rxjava3.annotations.NonNull; +import io.reactivex.rxjava3.internal.util.ExceptionHelper; + +/** + * A disposable container that manages an {@link AutoCloseable} instance. + * @since 3.0.0 + */ +final class AutoCloseableDisposable extends ReferenceDisposable<AutoCloseable> { + + private static final long serialVersionUID = -6646144244598696847L; + + AutoCloseableDisposable(AutoCloseable value) { + super(value); + } + + @Override + protected void onDisposed(@NonNull AutoCloseable value) { + try { + value.close(); + } catch (Throwable ex) { + throw ExceptionHelper.wrapOrThrow(ex); + } + } + + @Override + public String toString() { + return "AutoCloseableDisposable(disposed=" + isDisposed() + ", " + get() + ")"; + } + +} diff --git a/src/main/java/io/reactivex/rxjava3/disposables/CompositeDisposable.java b/src/main/java/io/reactivex/rxjava3/disposables/CompositeDisposable.java new file mode 100644 index 0000000000..bcf28ec148 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/disposables/CompositeDisposable.java @@ -0,0 +1,258 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.disposables; + +import java.util.*; + +import io.reactivex.rxjava3.annotations.*; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.internal.util.*; + +/** + * A disposable container that can hold onto multiple other {@link Disposable}s and + * offers <em>O(1)</em> time complexity for {@link #add(Disposable)}, {@link #remove(Disposable)} and {@link #delete(Disposable)} + * operations. + */ +public final class CompositeDisposable implements Disposable, DisposableContainer { + + OpenHashSet<Disposable> resources; + + volatile boolean disposed; + + /** + * Creates an empty {@code CompositeDisposable}. + */ + public CompositeDisposable() { + } + + /** + * Creates a {@code CompositeDisposable} with the given array of initial {@link Disposable} elements. + * @param disposables the array of {@code Disposable}s to start with + * @throws NullPointerException if {@code disposables} or any of its array items is {@code null} + */ + public CompositeDisposable(@NonNull Disposable... disposables) { + Objects.requireNonNull(disposables, "disposables is null"); + this.resources = new OpenHashSet<>(disposables.length + 1); + for (Disposable d : disposables) { + Objects.requireNonNull(d, "A Disposable in the disposables array is null"); + this.resources.add(d); + } + } + + /** + * Creates a {@code CompositeDisposable} with the given {@link Iterable} sequence of initial {@link Disposable} elements. + * @param disposables the {@code Iterable} sequence of {@code Disposable} to start with + * @throws NullPointerException if {@code disposables} or any of its items is {@code null} + */ + public CompositeDisposable(@NonNull Iterable<? extends Disposable> disposables) { + Objects.requireNonNull(disposables, "disposables is null"); + this.resources = new OpenHashSet<>(); + for (Disposable d : disposables) { + Objects.requireNonNull(d, "A Disposable item in the disposables sequence is null"); + this.resources.add(d); + } + } + + @Override + public void dispose() { + if (disposed) { + return; + } + OpenHashSet<Disposable> set; + synchronized (this) { + if (disposed) { + return; + } + disposed = true; + set = resources; + resources = null; + } + + dispose(set); + } + + @Override + public boolean isDisposed() { + return disposed; + } + + /** + * Adds a {@link Disposable} to this container or disposes it if the + * container has been disposed. + * @param disposable the {@code Disposable} to add, not {@code null} + * @return {@code true} if successful, {@code false} if this container has been disposed + * @throws NullPointerException if {@code disposable} is {@code null} + */ + @Override + public boolean add(@NonNull Disposable disposable) { + Objects.requireNonNull(disposable, "disposable is null"); + if (!disposed) { + synchronized (this) { + if (!disposed) { + OpenHashSet<Disposable> set = resources; + if (set == null) { + set = new OpenHashSet<>(); + resources = set; + } + set.add(disposable); + return true; + } + } + } + disposable.dispose(); + return false; + } + + /** + * Atomically adds the given array of {@link Disposable}s to the container or + * disposes them all if the container has been disposed. + * @param disposables the array of {@code Disposable}s + * @return {@code true} if the operation was successful, {@code false} if the container has been disposed + * @throws NullPointerException if {@code disposables} or any of its array items is {@code null} + */ + public boolean addAll(@NonNull Disposable... disposables) { + Objects.requireNonNull(disposables, "disposables is null"); + if (!disposed) { + synchronized (this) { + if (!disposed) { + OpenHashSet<Disposable> set = resources; + if (set == null) { + set = new OpenHashSet<>(disposables.length + 1); + resources = set; + } + for (Disposable d : disposables) { + Objects.requireNonNull(d, "A Disposable in the disposables array is null"); + set.add(d); + } + return true; + } + } + } + for (Disposable d : disposables) { + d.dispose(); + } + return false; + } + + /** + * Removes and disposes the given {@link Disposable} if it is part of this + * container. + * @param disposable the disposable to remove and dispose, not {@code null} + * @return {@code true} if the operation was successful + * @throws NullPointerException if {@code disposable} is {@code null} + */ + @Override + public boolean remove(@NonNull Disposable disposable) { + if (delete(disposable)) { + disposable.dispose(); + return true; + } + return false; + } + + /** + * Removes (but does not dispose) the given {@link Disposable} if it is part of this + * container. + * @param disposable the disposable to remove, not {@code null} + * @return {@code true} if the operation was successful + * @throws NullPointerException if {@code disposable} is {@code null} + */ + @Override + public boolean delete(@NonNull Disposable disposable) { + Objects.requireNonNull(disposable, "disposable is null"); + if (disposed) { + return false; + } + synchronized (this) { + if (disposed) { + return false; + } + + OpenHashSet<Disposable> set = resources; + if (set == null || !set.remove(disposable)) { + return false; + } + } + return true; + } + + /** + * Atomically clears the container, then disposes all the previously contained {@link Disposable}s. + */ + public void clear() { + if (disposed) { + return; + } + OpenHashSet<Disposable> set; + synchronized (this) { + if (disposed) { + return; + } + + set = resources; + resources = null; + } + + dispose(set); + } + + /** + * Returns the number of currently held {@link Disposable}s. + * @return the number of currently held {@code Disposable}s + */ + public int size() { + if (disposed) { + return 0; + } + synchronized (this) { + if (disposed) { + return 0; + } + OpenHashSet<Disposable> set = resources; + return set != null ? set.size() : 0; + } + } + + /** + * Dispose the contents of the {@link OpenHashSet} by suppressing non-fatal + * {@link Throwable}s till the end. + * @param set the {@code OpenHashSet} to dispose elements of + */ + void dispose(@Nullable OpenHashSet<Disposable> set) { + if (set == null) { + return; + } + List<Throwable> errors = null; + Object[] array = set.keys(); + for (Object o : array) { + if (o instanceof Disposable) { + try { + ((Disposable) o).dispose(); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + if (errors == null) { + errors = new ArrayList<>(); + } + errors.add(ex); + } + } + } + if (errors != null) { + if (errors.size() == 1) { + throw ExceptionHelper.wrapOrThrow(errors.get(0)); + } + throw new CompositeException(errors); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/disposables/Disposable.java b/src/main/java/io/reactivex/rxjava3/disposables/Disposable.java new file mode 100644 index 0000000000..845d603171 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/disposables/Disposable.java @@ -0,0 +1,161 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.disposables; + +import io.reactivex.rxjava3.annotations.NonNull; +import io.reactivex.rxjava3.functions.Action; +import io.reactivex.rxjava3.internal.disposables.EmptyDisposable; +import io.reactivex.rxjava3.internal.functions.Functions; +import org.reactivestreams.Subscription; + +import java.util.Objects; +import java.util.concurrent.Future; + +/** + * Represents a disposable resource. + */ +public interface Disposable { + /** + * Dispose the resource, the operation should be idempotent. + */ + void dispose(); + + /** + * Returns true if this resource has been disposed. + * @return true if this resource has been disposed + */ + boolean isDisposed(); + + /** + * Construct a {@code Disposable} by wrapping a {@link Runnable} that is + * executed exactly once when the {@code Disposable} is disposed. + * @param run the {@code Runnable} to wrap + * @return the new {@code Disposable} instance + * @throws NullPointerException if {@code run} is {@code null} + * @since 3.0.0 + */ + @NonNull + static Disposable fromRunnable(@NonNull Runnable run) { + Objects.requireNonNull(run, "run is null"); + return new RunnableDisposable(run); + } + + /** + * Construct a {@code Disposable} by wrapping a {@link Action} that is + * executed exactly once when the {@code Disposable} is disposed. + * @param action the {@code Action} to wrap + * @return the new {@code Disposable} instance + * @throws NullPointerException if {@code action} is {@code null} + * @since 3.0.0 + */ + @NonNull + static Disposable fromAction(@NonNull Action action) { + Objects.requireNonNull(action, "action is null"); + return new ActionDisposable(action); + } + + /** + * Construct a {@code Disposable} by wrapping a {@link Future} that is + * cancelled exactly once when the {@code Disposable} is disposed. + * <p> + * The {@code Future} is cancelled with {@code mayInterruptIfRunning == true}. + * @param future the {@code Future} to wrap + * @return the new {@code Disposable} instance + * @throws NullPointerException if {@code future} is {@code null} + * @see #fromFuture(Future, boolean) + * @since 3.0.0 + */ + @NonNull + static Disposable fromFuture(@NonNull Future<?> future) { + Objects.requireNonNull(future, "future is null"); + return fromFuture(future, true); + } + + /** + * Construct a {@code Disposable} by wrapping a {@link Future} that is + * cancelled exactly once when the {@code Disposable} is disposed. + * @param future the {@code Future} to wrap + * @param allowInterrupt if true, the future cancel happens via {@code Future.cancel(true)} + * @return the new {@code Disposable} instance + * @throws NullPointerException if {@code future} is {@code null} + * @since 3.0.0 + */ + @NonNull + static Disposable fromFuture(@NonNull Future<?> future, boolean allowInterrupt) { + Objects.requireNonNull(future, "future is null"); + return new FutureDisposable(future, allowInterrupt); + } + + /** + * Construct a {@code Disposable} by wrapping a {@link Subscription} that is + * cancelled exactly once when the {@code Disposable} is disposed. + * @param subscription the {@code Runnable} to wrap + * @return the new {@code Disposable} instance + * @throws NullPointerException if {@code subscription} is {@code null} + * @since 3.0.0 + */ + @NonNull + static Disposable fromSubscription(@NonNull Subscription subscription) { + Objects.requireNonNull(subscription, "subscription is null"); + return new SubscriptionDisposable(subscription); + } + + /** + * Construct a {@code Disposable} by wrapping an {@link AutoCloseable} that is + * closed exactly once when the {@code Disposable} is disposed. + * @param autoCloseable the {@code AutoCloseable} to wrap + * @return the new {@code Disposable} instance + * @throws NullPointerException if {@code autoCloseable} is {@code null} + * @since 3.0.0 + */ + @NonNull + static Disposable fromAutoCloseable(@NonNull AutoCloseable autoCloseable) { + Objects.requireNonNull(autoCloseable, "autoCloseable is null"); + return new AutoCloseableDisposable(autoCloseable); + } + + /** + * Construct an {@link AutoCloseable} by wrapping a {@code Disposable} that is + * disposed when the returned {@code AutoCloseable} is closed. + * @param disposable the {@code Disposable} instance + * @return the new {@code AutoCloseable} instance + * @throws NullPointerException if {@code disposable} is {@code null} + * @since 3.0.0 + */ + @NonNull + static AutoCloseable toAutoCloseable(@NonNull Disposable disposable) { + Objects.requireNonNull(disposable, "disposable is null"); + return disposable::dispose; + } + + /** + * Returns a new, non-disposed {@code Disposable} instance. + * @return a new, non-disposed {@code Disposable} instance + * @since 3.0.0 + */ + @NonNull + static Disposable empty() { + return fromRunnable(Functions.EMPTY_RUNNABLE); + } + + /** + * Returns a shared, disposed {@code Disposable} instance. + * @return a shared, disposed {@code Disposable} instance + * @since 3.0.0 + */ + @NonNull + static Disposable disposed() { + return EmptyDisposable.INSTANCE; + } +} diff --git a/src/main/java/io/reactivex/rxjava3/disposables/DisposableContainer.java b/src/main/java/io/reactivex/rxjava3/disposables/DisposableContainer.java new file mode 100644 index 0000000000..49311e4082 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/disposables/DisposableContainer.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.disposables; + +/** + * Common interface to add and remove disposables from a container. + * @since 2.0 + */ +public interface DisposableContainer { + + /** + * Adds a disposable to this container or disposes it if the + * container has been disposed. + * @param d the disposable to add, not null + * @return true if successful, false if this container has been disposed + */ + boolean add(Disposable d); + + /** + * Removes and disposes the given disposable if it is part of this + * container. + * @param d the disposable to remove and dispose, not null + * @return true if the operation was successful + */ + boolean remove(Disposable d); + + /** + * Removes but does not dispose the given disposable if it is part of this + * container. + * @param d the disposable to remove, not null + * @return true if the operation was successful + */ + boolean delete(Disposable d); +} diff --git a/src/main/java/io/reactivex/rxjava3/disposables/FutureDisposable.java b/src/main/java/io/reactivex/rxjava3/disposables/FutureDisposable.java new file mode 100644 index 0000000000..d8f70ff655 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/disposables/FutureDisposable.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.disposables; + +import java.util.concurrent.Future; +import java.util.concurrent.atomic.AtomicReference; + +/** + * A Disposable container that cancels a {@link Future} instance. + */ +final class FutureDisposable extends AtomicReference<Future<?>> implements Disposable { + + private static final long serialVersionUID = 6545242830671168775L; + + private final boolean allowInterrupt; + + FutureDisposable(Future<?> run, boolean allowInterrupt) { + super(run); + this.allowInterrupt = allowInterrupt; + } + + @Override + public boolean isDisposed() { + Future<?> f = get(); + return f == null || f.isDone(); + } + + @Override + public void dispose() { + Future<?> f = getAndSet(null); + if (f != null) { + f.cancel(allowInterrupt); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/disposables/ReferenceDisposable.java b/src/main/java/io/reactivex/rxjava3/disposables/ReferenceDisposable.java new file mode 100644 index 0000000000..55bb24a268 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/disposables/ReferenceDisposable.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.disposables; + +import java.util.Objects; +import java.util.concurrent.atomic.AtomicReference; + +import io.reactivex.rxjava3.annotations.NonNull; + +/** + * Base class for Disposable containers that manage some other type that + * has to be run when the container is disposed. + * + * @param <T> the type contained + */ +abstract class ReferenceDisposable<T> extends AtomicReference<T> implements Disposable { + + private static final long serialVersionUID = 6537757548749041217L; + + ReferenceDisposable(T value) { + super(Objects.requireNonNull(value, "value is null")); + } + + protected abstract void onDisposed(@NonNull T value); + + @Override + public final void dispose() { + T value = get(); + if (value != null) { + value = getAndSet(null); + if (value != null) { + onDisposed(value); + } + } + } + + @Override + public final boolean isDisposed() { + return get() == null; + } +} diff --git a/src/main/java/io/reactivex/rxjava3/disposables/RunnableDisposable.java b/src/main/java/io/reactivex/rxjava3/disposables/RunnableDisposable.java new file mode 100644 index 0000000000..390a7912b4 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/disposables/RunnableDisposable.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.disposables; + +import io.reactivex.rxjava3.annotations.NonNull; + +/** + * A disposable container that manages a {@link Runnable} instance. + */ +final class RunnableDisposable extends ReferenceDisposable<Runnable> { + + private static final long serialVersionUID = -8219729196779211169L; + + RunnableDisposable(Runnable value) { + super(value); + } + + @Override + protected void onDisposed(@NonNull Runnable value) { + value.run(); + } + + @Override + public String toString() { + return "RunnableDisposable(disposed=" + isDisposed() + ", " + get() + ")"; + } +} diff --git a/src/main/java/io/reactivex/rxjava3/disposables/SerialDisposable.java b/src/main/java/io/reactivex/rxjava3/disposables/SerialDisposable.java new file mode 100644 index 0000000000..7b1f6463a8 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/disposables/SerialDisposable.java @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.disposables; + +import java.util.concurrent.atomic.AtomicReference; + +import io.reactivex.rxjava3.annotations.Nullable; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; + +/** + * A Disposable container that allows atomically updating/replacing the contained + * Disposable with another Disposable, disposing the old one when updating plus + * handling the disposition when the container itself is disposed. + */ +public final class SerialDisposable implements Disposable { + final AtomicReference<Disposable> resource; + + /** + * Constructs an empty SerialDisposable. + */ + public SerialDisposable() { + this.resource = new AtomicReference<>(); + } + + /** + * Constructs a SerialDisposable with the given initial Disposable instance. + * @param initialDisposable the initial Disposable instance to use, null allowed + */ + public SerialDisposable(@Nullable Disposable initialDisposable) { + this.resource = new AtomicReference<>(initialDisposable); + } + + /** + * Atomically: set the next disposable on this container and dispose the previous + * one (if any) or dispose next if the container has been disposed. + * @param next the Disposable to set, may be null + * @return true if the operation succeeded, false if the container has been disposed + * @see #replace(Disposable) + */ + public boolean set(@Nullable Disposable next) { + return DisposableHelper.set(resource, next); + } + + /** + * Atomically: set the next disposable on this container but don't dispose the previous + * one (if any) or dispose next if the container has been disposed. + * @param next the Disposable to set, may be null + * @return true if the operation succeeded, false if the container has been disposed + * @see #set(Disposable) + */ + public boolean replace(@Nullable Disposable next) { + return DisposableHelper.replace(resource, next); + } + + /** + * Returns the currently contained Disposable or null if this container is empty. + * @return the current Disposable, may be null + */ + @Nullable + public Disposable get() { + Disposable d = resource.get(); + if (d == DisposableHelper.DISPOSED) { + return Disposable.disposed(); + } + return d; + } + + @Override + public void dispose() { + DisposableHelper.dispose(resource); + } + + @Override + public boolean isDisposed() { + return DisposableHelper.isDisposed(resource.get()); + } +} diff --git a/src/main/java/io/reactivex/rxjava3/disposables/SubscriptionDisposable.java b/src/main/java/io/reactivex/rxjava3/disposables/SubscriptionDisposable.java new file mode 100644 index 0000000000..48c5c7ab3d --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/disposables/SubscriptionDisposable.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.disposables; + +import org.reactivestreams.Subscription; + +import io.reactivex.rxjava3.annotations.NonNull; + +/** + * A Disposable container that handles a {@link Subscription}. + */ +final class SubscriptionDisposable extends ReferenceDisposable<Subscription> { + + private static final long serialVersionUID = -707001650852963139L; + + SubscriptionDisposable(Subscription value) { + super(value); + } + + @Override + protected void onDisposed(@NonNull Subscription value) { + value.cancel(); + } +} diff --git a/src/main/java/io/reactivex/rxjava3/disposables/package-info.java b/src/main/java/io/reactivex/rxjava3/disposables/package-info.java new file mode 100644 index 0000000000..00ff5dd6ff --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/disposables/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +/** + * Default implementations for {@link io.reactivex.rxjava3.disposables.Disposable Disposable}-based resource management + * ({@code Disposable} container types) and utility classes to construct + * {@link io.reactivex.rxjava3.disposables.Disposable Disposables} from callbacks and other types. + */ +package io.reactivex.rxjava3.disposables; diff --git a/src/main/java/io/reactivex/rxjava3/exceptions/CompositeException.java b/src/main/java/io/reactivex/rxjava3/exceptions/CompositeException.java new file mode 100644 index 0000000000..5d2928b28f --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/exceptions/CompositeException.java @@ -0,0 +1,298 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.exceptions; + +import java.io.*; +import java.util.*; + +import io.reactivex.rxjava3.annotations.NonNull; + +/** + * Represents an exception that is a composite of one or more other exceptions. A {@code CompositeException} + * does not modify the structure of any exception it wraps, but at print-time it iterates through the list of + * Throwables contained in the composite in order to print them all. + * + * Its invariant is to contain an immutable, ordered (by insertion order), unique list of non-composite + * exceptions. You can retrieve individual exceptions in this list with {@link #getExceptions()}. + * + * The {@link #printStackTrace()} implementation handles the StackTrace in a customized way instead of using + * {@code getCause()} so that it can avoid circular references. + * + * If you invoke {@link #getCause()}, it will lazily create the causal chain but will stop if it finds any + * Throwable in the chain that it has already seen. + */ +public final class CompositeException extends RuntimeException { + + private static final long serialVersionUID = 3026362227162912146L; + + private final List<Throwable> exceptions; + private final String message; + private Throwable cause; + + /** + * Constructs a CompositeException with the given array of Throwables as the + * list of suppressed exceptions. + * @param exceptions the Throwables to have as initially suppressed exceptions + * + * @throws IllegalArgumentException if <code>exceptions</code> is empty. + */ + public CompositeException(@NonNull Throwable... exceptions) { + this(exceptions == null ? + Collections.singletonList(new NullPointerException("exceptions was null")) : Arrays.asList(exceptions)); + } + + /** + * Constructs a CompositeException with the given array of Throwables as the + * list of suppressed exceptions. + * @param errors the Throwables to have as initially suppressed exceptions + * + * @throws IllegalArgumentException if <code>errors</code> is empty. + */ + public CompositeException(@NonNull Iterable<? extends Throwable> errors) { + Set<Throwable> deDupedExceptions = new LinkedHashSet<>(); + if (errors != null) { + for (Throwable ex : errors) { + if (ex instanceof CompositeException) { + deDupedExceptions.addAll(((CompositeException) ex).getExceptions()); + } else + if (ex != null) { + deDupedExceptions.add(ex); + } else { + deDupedExceptions.add(new NullPointerException("Throwable was null!")); + } + } + } else { + deDupedExceptions.add(new NullPointerException("errors was null")); + } + if (deDupedExceptions.isEmpty()) { + throw new IllegalArgumentException("errors is empty"); + } + List<Throwable> localExceptions = new ArrayList<>(deDupedExceptions); + this.exceptions = Collections.unmodifiableList(localExceptions); + this.message = exceptions.size() + " exceptions occurred. "; + } + + /** + * Retrieves the list of exceptions that make up the {@code CompositeException}. + * + * @return the exceptions that make up the {@code CompositeException}, as a {@link List} of {@link Throwable}s + */ + @NonNull + public List<Throwable> getExceptions() { + return exceptions; + } + + @Override + @NonNull + public String getMessage() { + return message; + } + + @Override + @NonNull + public synchronized Throwable getCause() { // NOPMD + if (cause == null) { + String separator = System.getProperty("line.separator"); + if (exceptions.size() > 1) { + Map<Throwable, Boolean> seenCauses = new IdentityHashMap<>(); + + StringBuilder aggregateMessage = new StringBuilder(); + aggregateMessage.append("Multiple exceptions (").append(exceptions.size()).append(")").append(separator); + + for (Throwable inner : exceptions) { + int depth = 0; + while (inner != null) { + for (int i = 0; i < depth; i++) { + aggregateMessage.append(" "); + } + aggregateMessage.append("|-- "); + aggregateMessage.append(inner.getClass().getCanonicalName()).append(": "); + String innerMessage = inner.getMessage(); + if (innerMessage != null && innerMessage.contains(separator)) { + aggregateMessage.append(separator); + for (String line : innerMessage.split(separator)) { + for (int i = 0; i < depth + 2; i++) { + aggregateMessage.append(" "); + } + aggregateMessage.append(line).append(separator); + } + } else { + aggregateMessage.append(innerMessage); + aggregateMessage.append(separator); + } + + for (int i = 0; i < depth + 2; i++) { + aggregateMessage.append(" "); + } + StackTraceElement[] st = inner.getStackTrace(); + if (st.length > 0) { + aggregateMessage.append("at ").append(st[0]).append(separator); + } + + if (!seenCauses.containsKey(inner)) { + seenCauses.put(inner, true); + + inner = inner.getCause(); + depth++; + } else { + inner = inner.getCause(); + if (inner != null) { + for (int i = 0; i < depth + 2; i++) { + aggregateMessage.append(" "); + } + aggregateMessage.append("|-- "); + aggregateMessage.append("(cause not expanded again) "); + aggregateMessage.append(inner.getClass().getCanonicalName()).append(": "); + aggregateMessage.append(inner.getMessage()); + aggregateMessage.append(separator); + } + break; + } + } + } + + cause = new ExceptionOverview(aggregateMessage.toString().trim()); + } else { + cause = exceptions.get(0); + } + } + return cause; + } + + /** + * All of the following {@code printStackTrace} functionality is derived from JDK {@link Throwable} + * {@code printStackTrace}. In particular, the {@code PrintStreamOrWriter} abstraction is copied wholesale. + * + * Changes from the official JDK implementation:<ul> + * <li>no infinite loop detection</li> + * <li>smaller critical section holding {@link PrintStream} lock</li> + * <li>explicit knowledge about the exceptions {@link List} that this loops through</li> + * </ul> + */ + @Override + public void printStackTrace() { + printStackTrace(System.err); + } + + @Override + public void printStackTrace(PrintStream s) { + printStackTrace(new WrappedPrintStream(s)); + } + + @Override + public void printStackTrace(PrintWriter s) { + printStackTrace(new WrappedPrintWriter(s)); + } + + /** + * Special handling for printing out a {@code CompositeException}. + * Loops through all inner exceptions and prints them out. + * + * @param output + * stream to print to + */ + private void printStackTrace(PrintStreamOrWriter output) { + output.append(this).append("\n"); + for (StackTraceElement myStackElement : getStackTrace()) { + output.append("\tat ").append(myStackElement).append("\n"); + } + int i = 1; + for (Throwable ex : exceptions) { + output.append(" ComposedException ").append(i).append(" :\n"); + appendStackTrace(output, ex, "\t"); + i++; + } + output.append("\n"); + } + + private void appendStackTrace(PrintStreamOrWriter output, Throwable ex, String prefix) { + output.append(prefix).append(ex).append('\n'); + for (StackTraceElement stackElement : ex.getStackTrace()) { + output.append("\t\tat ").append(stackElement).append('\n'); + } + if (ex.getCause() != null) { + output.append("\tCaused by: "); + appendStackTrace(output, ex.getCause(), ""); + } + } + + abstract static class PrintStreamOrWriter { + /** + * Prints the object's string representation via the underlying PrintStream or PrintWriter. + * @param o the object to print + * @return this + */ + abstract PrintStreamOrWriter append(Object o); + } + + /** + * Same abstraction and implementation as in JDK to allow PrintStream and PrintWriter to share implementation. + */ + static final class WrappedPrintStream extends PrintStreamOrWriter { + private final PrintStream printStream; + + WrappedPrintStream(PrintStream printStream) { + this.printStream = printStream; + } + + @Override + WrappedPrintStream append(Object o) { + printStream.print(o); + return this; + } + } + + /** + * Same abstraction and implementation as in JDK to allow PrintStream and PrintWriter to share implementation. + */ + static final class WrappedPrintWriter extends PrintStreamOrWriter { + private final PrintWriter printWriter; + + WrappedPrintWriter(PrintWriter printWriter) { + this.printWriter = printWriter; + } + + @Override + WrappedPrintWriter append(Object o) { + printWriter.print(o); + return this; + } + } + + /** + * Contains a formatted message with a simplified representation of the exception graph + * contained within the CompositeException. + */ + static final class ExceptionOverview extends RuntimeException { + + private static final long serialVersionUID = 3875212506787802066L; + + ExceptionOverview(String message) { + super(message); + } + + @Override + public synchronized Throwable fillInStackTrace() { + return this; + } + } + + /** + * Returns the number of suppressed exceptions. + * @return the number of suppressed exceptions + */ + public int size() { + return exceptions.size(); + } +} diff --git a/src/main/java/io/reactivex/rxjava3/exceptions/Exceptions.java b/src/main/java/io/reactivex/rxjava3/exceptions/Exceptions.java new file mode 100644 index 0000000000..155a2801d2 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/exceptions/Exceptions.java @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.exceptions; + +import io.reactivex.rxjava3.annotations.NonNull; +import io.reactivex.rxjava3.internal.util.ExceptionHelper; + +/** + * Utility class to help propagate checked exceptions and rethrow exceptions + * designated as fatal. + */ +public final class Exceptions { + + /** Utility class. */ + private Exceptions() { + throw new IllegalStateException("No instances!"); + } + /** + * Convenience method to throw a {@code RuntimeException} and {@code Error} directly + * or wrap any other exception type into a {@code RuntimeException}. + * @param t the exception to throw directly or wrapped + * @return because {@code propagate} itself throws an exception or error, this is a sort of phantom return + * value; {@code propagate} does not actually return anything + */ + @NonNull + public static RuntimeException propagate(@NonNull Throwable t) { + /* + * The return type of RuntimeException is a trick for code to be like this: + * + * throw Exceptions.propagate(e); + * + * Even though nothing will return and throw via that 'throw', it allows the code to look like it + * so it's easy to read and understand that it will always result in a throw. + */ + throw ExceptionHelper.wrapOrThrow(t); + } + + /** + * Throws a particular {@code Throwable} only if it belongs to a set of "fatal" error varieties. These + * varieties are as follows: + * <ul> + * <li>{@code VirtualMachineError}</li> + * <li>{@code ThreadDeath}</li> + * <li>{@code LinkageError}</li> + * </ul> + * This can be useful if you are writing an operator that calls user-supplied code, and you want to + * notify subscribers of errors encountered in that code by calling their {@code onError} methods, but only + * if the errors are not so catastrophic that such a call would be futile, in which case you simply want to + * rethrow the error. + * + * @param t + * the {@code Throwable} to test and perhaps throw + * @see <a href="https://github.com/ReactiveX/RxJava/issues/748#issuecomment-32471495">RxJava: StackOverflowError is swallowed (Issue #748)</a> + */ + public static void throwIfFatal(@NonNull Throwable t) { + // values here derived from https://github.com/ReactiveX/RxJava/issues/748#issuecomment-32471495 + if (t instanceof VirtualMachineError) { + throw (VirtualMachineError) t; + } else if (t instanceof ThreadDeath) { + throw (ThreadDeath) t; + } else if (t instanceof LinkageError) { + throw (LinkageError) t; + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/exceptions/MissingBackpressureException.java b/src/main/java/io/reactivex/rxjava3/exceptions/MissingBackpressureException.java new file mode 100644 index 0000000000..f0a173ba59 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/exceptions/MissingBackpressureException.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.exceptions; + +/** + * Indicates that an operator attempted to emit a value but the downstream wasn't ready for it. + */ +public final class MissingBackpressureException extends RuntimeException { + + private static final long serialVersionUID = 8517344746016032542L; + + /** + * The default error message. + * <p> + * This can happen if the downstream doesn't call {@link org.reactivestreams.Subscription#request(long)} + * in time or at all. + * @since 3.1.6 + */ + public static final String DEFAULT_MESSAGE = "Could not emit value due to lack of requests"; + + /** + * Constructs a MissingBackpressureException without message or cause. + */ + public MissingBackpressureException() { + // no message + } + + /** + * Constructs a MissingBackpressureException with the given message but no cause. + * @param message the error message + */ + public MissingBackpressureException(String message) { + super(message); + } + + /** + * Constructs a new {@code MissingBackpressureException} with the + * default message {@value #DEFAULT_MESSAGE}. + * @return the new {@code MissingBackpressureException} instance. + * @since 3.1.6 + */ + public static MissingBackpressureException createDefault() { + return new MissingBackpressureException(DEFAULT_MESSAGE); + } +} diff --git a/src/main/java/io/reactivex/rxjava3/exceptions/OnErrorNotImplementedException.java b/src/main/java/io/reactivex/rxjava3/exceptions/OnErrorNotImplementedException.java new file mode 100644 index 0000000000..072b889a57 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/exceptions/OnErrorNotImplementedException.java @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.exceptions; + +import io.reactivex.rxjava3.annotations.NonNull; + +/** + * Represents an exception used to signal to the {@code RxJavaPlugins.onError()} that a + * callback-based subscribe() method on a base reactive type didn't specify + * an onError handler. + * <p>History: 2.0.6 - experimental; 2.1 - beta + * @since 2.2 + */ +public final class OnErrorNotImplementedException extends RuntimeException { + + private static final long serialVersionUID = -6298857009889503852L; + + /** + * Customizes the {@code Throwable} with a custom message and wraps it before it + * is signalled to the {@code RxJavaPlugins.onError()} handler as {@code OnErrorNotImplementedException}. + * + * @param message + * the message to assign to the {@code Throwable} to signal + * @param e + * the {@code Throwable} to signal; if null, a NullPointerException is constructed + */ + public OnErrorNotImplementedException(String message, @NonNull Throwable e) { + super(message, e != null ? e : new NullPointerException()); + } + + /** + * Wraps the {@code Throwable} before it + * is signalled to the {@code RxJavaPlugins.onError()} + * handler as {@code OnErrorNotImplementedException}. + * + * @param e + * the {@code Throwable} to signal; if null, a NullPointerException is constructed + */ + public OnErrorNotImplementedException(@NonNull Throwable e) { + this("The exception was not handled due to missing onError handler in the subscribe() method call. Further reading: https://github.com/ReactiveX/RxJava/wiki/Error-Handling | " + e, e); + } +} diff --git a/src/main/java/io/reactivex/rxjava3/exceptions/ProtocolViolationException.java b/src/main/java/io/reactivex/rxjava3/exceptions/ProtocolViolationException.java new file mode 100644 index 0000000000..66fab7f911 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/exceptions/ProtocolViolationException.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.exceptions; + +/** + * Explicitly named exception to indicate a Reactive-Streams + * protocol violation. + * <p>History: 2.0.6 - experimental; 2.1 - beta + * @since 2.2 + */ +public final class ProtocolViolationException extends IllegalStateException { + + private static final long serialVersionUID = 1644750035281290266L; + + /** + * Creates an instance with the given message. + * @param message the message + */ + public ProtocolViolationException(String message) { + super(message); + } +} diff --git a/src/main/java/io/reactivex/rxjava3/exceptions/QueueOverflowException.java b/src/main/java/io/reactivex/rxjava3/exceptions/QueueOverflowException.java new file mode 100644 index 0000000000..bdd8a25e6f --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/exceptions/QueueOverflowException.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.exceptions; + +/** + * Indicates an overflow happened because the upstream disregarded backpressure completely or + * {@link org.reactivestreams.Subscriber#onNext(Object)} was called concurrently from multiple threads + * without synchronization. Rarely, it is an indication of bugs inside an operator. + * @since 3.1.6 + */ +public final class QueueOverflowException extends RuntimeException { + + private static final long serialVersionUID = 8517344746016032542L; + + /** + * The message for queue overflows. + * <p> + * This can happen if the upstream disregards backpressure completely or calls + * {@link org.reactivestreams.Subscriber#onNext(Object)} concurrently from multiple threads + * without synchronization. Rarely, it is an indication of bugs inside an operator. + */ + private static final String DEFAULT_MESSAGE = "Queue overflow due to illegal concurrent onNext calls or a bug in an operator"; + + /** + * Constructs a QueueOverflowException with the default message. + */ + public QueueOverflowException() { + this(DEFAULT_MESSAGE); + } + + /** + * Constructs a QueueOverflowException with the given message but no cause. + * @param message the error message + */ + public QueueOverflowException(String message) { + super(message); + } +} diff --git a/src/main/java/io/reactivex/rxjava3/exceptions/UndeliverableException.java b/src/main/java/io/reactivex/rxjava3/exceptions/UndeliverableException.java new file mode 100644 index 0000000000..11a4c07f8a --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/exceptions/UndeliverableException.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.exceptions; + +/** + * Wrapper for Throwable errors that are sent to {@link io.reactivex.rxjava3.plugins.RxJavaPlugins#onError(Throwable) RxJavaPlugins.onError}. + * <p>History: 2.0.6 - experimental; 2.1 - beta + * @since 2.2 + */ +public final class UndeliverableException extends IllegalStateException { + + private static final long serialVersionUID = 1644750035281290266L; + + /** + * Construct an instance by wrapping the given, non-null + * cause Throwable. + * @param cause the cause, not null + */ + public UndeliverableException(Throwable cause) { + super("The exception could not be delivered to the consumer because it has already canceled/disposed the flow or the exception has nowhere to go to begin with. Further reading: https://github.com/ReactiveX/RxJava/wiki/What's-different-in-2.0#error-handling | " + cause, cause); + } +} diff --git a/src/main/java/io/reactivex/rxjava3/exceptions/package-info.java b/src/main/java/io/reactivex/rxjava3/exceptions/package-info.java new file mode 100644 index 0000000000..91de82962f --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/exceptions/package-info.java @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +/** + * Exception handling utilities ({@link io.reactivex.rxjava3.exceptions.Exceptions Exceptions}), + * composite exception container ({@link io.reactivex.rxjava3.exceptions.CompositeException CompositeException}) and + * various lifecycle-related ({@link io.reactivex.rxjava3.exceptions.MissingBackpressureException UndeliverableException}) + * and behavior-violation exception types ({@link io.reactivex.rxjava3.exceptions.OnErrorNotImplementedException OnErrorNotImplementedException}, + * {@link io.reactivex.rxjava3.exceptions.MissingBackpressureException MissingBackpressureException}). + */ +package io.reactivex.rxjava3.exceptions; diff --git a/src/main/java/io/reactivex/rxjava3/flowables/ConnectableFlowable.java b/src/main/java/io/reactivex/rxjava3/flowables/ConnectableFlowable.java new file mode 100644 index 0000000000..133d0186c8 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/flowables/ConnectableFlowable.java @@ -0,0 +1,386 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.flowables; + +import java.util.Objects; +import java.util.concurrent.TimeUnit; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.annotations.*; +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.functions.Consumer; +import io.reactivex.rxjava3.internal.functions.*; +import io.reactivex.rxjava3.internal.operators.flowable.*; +import io.reactivex.rxjava3.internal.util.ConnectConsumer; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.schedulers.Schedulers; + +/** + * A {@code ConnectableFlowable} resembles an ordinary {@link Flowable}, except that it does not begin + * emitting items when it is subscribed to, but only when its {@link #connect} method is called. In this way you + * can wait for all intended {@link Subscriber}s to {@link Flowable#subscribe} to the {@code Flowable} + * before the {@code Flowable} begins emitting items. + * <p> + * <img width="640" height="510" src="https://github.com/ReactiveX/RxJava/wiki/images/rx-operators/publishConnect.v3.png" alt=""> + * <p> + * When the upstream terminates, the {@code ConnectableFlowable} remains in this terminated state and, + * depending on the actual underlying implementation, relays cached events to late {@code Subscriber}s. + * In order to reuse and restart this {@code ConnectableFlowable}, the {@link #reset()} method has to be called. + * When called, this {@code ConnectableFlowable} will appear as fresh, unconnected source to new {@code Subscriber}s. + * Disposing the connection will reset the {@code ConnectableFlowable} to its fresh state and there is no need to call + * {@code reset()} in this case. + * <p> + * Note that although {@link #connect()} and {@link #reset()} are safe to call from multiple threads, it is recommended + * a dedicated thread or business logic manages the connection or resetting of a {@code ConnectableFlowable} so that + * there is no unwanted signal loss due to early {@code connect()} or {@code reset()} calls while {@code Subscriber}s are + * still being subscribed to to this {@code ConnectableFlowable} to receive signals from the get go. + * <p> + * @see <a href="https://github.com/ReactiveX/RxJava/wiki/Connectable-Observable-Operators">RxJava Wiki: Connectable Observable Operators</a> + * @param <T> + * the type of items emitted by the {@code ConnectableFlowable} + * @since 2.0.0 + */ +public abstract class ConnectableFlowable<T> extends Flowable<T> { + + /** + * Instructs the {@code ConnectableFlowable} to begin emitting the items from its underlying + * {@link Flowable} to its {@link Subscriber}s. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>The behavior is determined by the implementor of this abstract class.</dd> + * </dl> + * + * @param connection + * the action that receives the connection subscription before the subscription to source happens + * allowing the caller to synchronously disconnect a synchronous source + * @throws NullPointerException if {@code connection} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/connect.html">ReactiveX documentation: Connect</a> + */ + @SchedulerSupport(SchedulerSupport.NONE) + public abstract void connect(@NonNull Consumer<? super Disposable> connection); + + /** + * Resets this {@code ConnectableFlowable} into its fresh state if it has terminated. + * <p> + * Calling this method on a fresh or active {@code ConnectableFlowable} has no effect. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>The behavior is determined by the implementor of this abstract class.</dd> + * </dl> + * @since 3.0.0 + */ + @SchedulerSupport(SchedulerSupport.NONE) + public abstract void reset(); + + /** + * Instructs the {@code ConnectableFlowable} to begin emitting the items from its underlying + * {@link Flowable} to its {@link Subscriber}s. + * <p> + * To disconnect from a synchronous source, use the {@link #connect(io.reactivex.rxjava3.functions.Consumer)} method. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>The behavior is determined by the implementor of this abstract class.</dd> + * </dl> + * + * @return the subscription representing the connection + * @see <a href="http://reactivex.io/documentation/operators/connect.html">ReactiveX documentation: Connect</a> + */ + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public final Disposable connect() { + ConnectConsumer cc = new ConnectConsumer(); + connect(cc); + return cc.disposable; + } + + /** + * Returns a {@link Flowable} that stays connected to this {@code ConnectableFlowable} as long as there + * is at least one subscription to this {@code ConnectableFlowable}. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator itself doesn't interfere with backpressure which is determined by the upstream + * {@code ConnectableFlowable}'s backpressure behavior.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>This {@code refCount} overload does not operate on any particular {@link Scheduler}.</dd> + * </dl> + * @return the new {@code Flowable} instance + * @see <a href="http://reactivex.io/documentation/operators/refcount.html">ReactiveX documentation: RefCount</a> + * @see #refCount(int) + * @see #refCount(long, TimeUnit) + * @see #refCount(int, long, TimeUnit) + */ + @NonNull + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @BackpressureSupport(BackpressureKind.PASS_THROUGH) + public Flowable<T> refCount() { + return RxJavaPlugins.onAssembly(new FlowableRefCount<>(this)); + } + + /** + * Connects to the upstream {@code ConnectableFlowable} if the number of subscribed + * subscriber reaches the specified count and disconnect if all subscribers have unsubscribed. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator itself doesn't interfere with backpressure which is determined by the upstream + * {@code ConnectableFlowable}'s backpressure behavior.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>This {@code refCount} overload does not operate on any particular {@link Scheduler}.</dd> + * </dl> + * <p>History: 2.1.14 - experimental + * @param subscriberCount the number of subscribers required to connect to the upstream + * @return the new {@link Flowable} instance + * @throws IllegalArgumentException if {@code subscriberCount} is non-positive + * @since 2.2 + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @BackpressureSupport(BackpressureKind.PASS_THROUGH) + @NonNull + public final Flowable<T> refCount(int subscriberCount) { + return refCount(subscriberCount, 0, TimeUnit.NANOSECONDS, Schedulers.trampoline()); + } + + /** + * Connects to the upstream {@code ConnectableFlowable} if the number of subscribed + * subscriber reaches 1 and disconnect after the specified + * timeout if all subscribers have unsubscribed. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator itself doesn't interfere with backpressure which is determined by the upstream + * {@code ConnectableFlowable}'s backpressure behavior.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>This {@code refCount} overload operates on the {@code computation} {@link Scheduler}.</dd> + * </dl> + * <p>History: 2.1.14 - experimental + * @param timeout the time to wait before disconnecting after all subscribers unsubscribed + * @param unit the time unit of the timeout + * @return the new {@link Flowable} instance + * @throws NullPointerException if {@code unit} is {@code null} + * @see #refCount(long, TimeUnit, Scheduler) + * @since 2.2 + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.COMPUTATION) + @BackpressureSupport(BackpressureKind.PASS_THROUGH) + @NonNull + public final Flowable<T> refCount(long timeout, @NonNull TimeUnit unit) { + return refCount(1, timeout, unit, Schedulers.computation()); + } + + /** + * Connects to the upstream {@code ConnectableFlowable} if the number of subscribed + * subscriber reaches 1 and disconnect after the specified + * timeout if all subscribers have unsubscribed. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator itself doesn't interfere with backpressure which is determined by the upstream + * {@code ConnectableFlowable}'s backpressure behavior.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>This {@code refCount} overload operates on the specified {@link Scheduler}.</dd> + * </dl> + * <p>History: 2.1.14 - experimental + * @param timeout the time to wait before disconnecting after all subscribers unsubscribed + * @param unit the time unit of the timeout + * @param scheduler the target scheduler to wait on before disconnecting + * @return the new {@link Flowable} instance + * @throws NullPointerException if {@code unit} or {@code scheduler} is {@code null} + * @since 2.2 + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.CUSTOM) + @BackpressureSupport(BackpressureKind.PASS_THROUGH) + @NonNull + public final Flowable<T> refCount(long timeout, @NonNull TimeUnit unit, @NonNull Scheduler scheduler) { + return refCount(1, timeout, unit, scheduler); + } + + /** + * Connects to the upstream {@code ConnectableFlowable} if the number of subscribed + * subscriber reaches the specified count and disconnect after the specified + * timeout if all subscribers have unsubscribed. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator itself doesn't interfere with backpressure which is determined by the upstream + * {@code ConnectableFlowable}'s backpressure behavior.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>This {@code refCount} overload operates on the {@code computation} {@link Scheduler}.</dd> + * </dl> + * <p>History: 2.1.14 - experimental + * @param subscriberCount the number of subscribers required to connect to the upstream + * @param timeout the time to wait before disconnecting after all subscribers unsubscribed + * @param unit the time unit of the timeout + * @return the new {@link Flowable} instance + * @throws NullPointerException if {@code unit} is {@code null} + * @throws IllegalArgumentException if {@code subscriberCount} is non-positive + * @see #refCount(int, long, TimeUnit, Scheduler) + * @since 2.2 + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.COMPUTATION) + @BackpressureSupport(BackpressureKind.PASS_THROUGH) + @NonNull + public final Flowable<T> refCount(int subscriberCount, long timeout, @NonNull TimeUnit unit) { + return refCount(subscriberCount, timeout, unit, Schedulers.computation()); + } + + /** + * Connects to the upstream {@code ConnectableFlowable} if the number of subscribed + * subscriber reaches the specified count and disconnect after the specified + * timeout if all subscribers have unsubscribed. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator itself doesn't interfere with backpressure which is determined by the upstream + * {@code ConnectableFlowable}'s backpressure behavior.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>This {@code refCount} overload operates on the specified {@link Scheduler}.</dd> + * </dl> + * <p>History: 2.1.14 - experimental + * @param subscriberCount the number of subscribers required to connect to the upstream + * @param timeout the time to wait before disconnecting after all subscribers unsubscribed + * @param unit the time unit of the timeout + * @param scheduler the target scheduler to wait on before disconnecting + * @return the new {@link Flowable} instance + * @throws NullPointerException if {@code unit} or {@code scheduler} is {@code null} + * @throws IllegalArgumentException if {@code subscriberCount} is non-positive + * @since 2.2 + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.CUSTOM) + @BackpressureSupport(BackpressureKind.PASS_THROUGH) + @NonNull + public final Flowable<T> refCount(int subscriberCount, long timeout, @NonNull TimeUnit unit, @NonNull Scheduler scheduler) { + ObjectHelper.verifyPositive(subscriberCount, "subscriberCount"); + Objects.requireNonNull(unit, "unit is null"); + Objects.requireNonNull(scheduler, "scheduler is null"); + return RxJavaPlugins.onAssembly(new FlowableRefCount<>(this, subscriberCount, timeout, unit, scheduler)); + } + + /** + * Returns a {@link Flowable} that automatically connects (at most once) to this {@code ConnectableFlowable} + * when the first {@link Subscriber} subscribes. + * <p> + * <img width="640" height="392" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/autoConnect.f.png" alt=""> + * <p> + * The connection happens after the first subscription and happens at most once + * during the lifetime of the returned {@code Flowable}. If this {@code ConnectableFlowable} + * terminates, the connection is never renewed, no matter how {@code Subscriber}s come + * and go. Use {@link #refCount()} to renew a connection or dispose an active + * connection when all {@code Subscriber}s have cancelled their {@link Subscription}s. + * <p> + * This overload does not allow disconnecting the connection established via + * {@link #connect(Consumer)}. Use the {@link #autoConnect(int, Consumer)} overload + * to gain access to the {@link Disposable} representing the only connection. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator itself doesn't interfere with backpressure which is determined by + * the upstream {@code ConnectableFlowable}'s behavior.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code autoConnect} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @return a new {@code Flowable} instance that automatically connects to this {@code ConnectableFlowable} + * when the first {@code Subscriber} subscribes + * @see #refCount() + * @see #autoConnect(int, Consumer) + */ + @NonNull + @CheckReturnValue + @BackpressureSupport(BackpressureKind.PASS_THROUGH) + @SchedulerSupport(SchedulerSupport.NONE) + public Flowable<T> autoConnect() { + return autoConnect(1); + } + /** + * Returns a {@link Flowable} that automatically connects (at most once) to this {@code ConnectableFlowable} + * when the specified number of {@link Subscriber}s subscribe to it. + * <p> + * <img width="640" height="392" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/autoConnect.f.png" alt=""> + * <p> + * The connection happens after the given number of subscriptions and happens at most once + * during the lifetime of the returned {@code Flowable}. If this {@code ConnectableFlowable} + * terminates, the connection is never renewed, no matter how {@code Subscriber}s come + * and go. Use {@link #refCount()} to renew a connection or dispose an active + * connection when all {@code Subscriber}s have cancelled their {@link Subscription}s. + * <p> + * This overload does not allow disconnecting the connection established via + * {@link #connect(Consumer)}. Use the {@link #autoConnect(int, Consumer)} overload + * to gain access to the {@link Disposable} representing the only connection. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator itself doesn't interfere with backpressure which is determined by + * the upstream {@code ConnectableFlowable}'s behavior.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code autoConnect} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param numberOfSubscribers the number of subscribers to await before calling connect + * on the {@code ConnectableFlowable}. A non-positive value indicates + * an immediate connection. + * @return a new {@code Flowable} instance that automatically connects to this {@code ConnectableFlowable} + * when the specified number of {@code Subscriber}s subscribe to it + */ + @NonNull + @CheckReturnValue + @BackpressureSupport(BackpressureKind.PASS_THROUGH) + @SchedulerSupport(SchedulerSupport.NONE) + public Flowable<T> autoConnect(int numberOfSubscribers) { + return autoConnect(numberOfSubscribers, Functions.emptyConsumer()); + } + + /** + * Returns a {@link Flowable} that automatically connects (at most once) to this {@code ConnectableFlowable} + * when the specified number of {@link Subscriber}s subscribe to it and calls the + * specified callback with the {@link Disposable} associated with the established connection. + * <p> + * <img width="640" height="392" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/autoConnect.f.png" alt=""> + * <p> + * The connection happens after the given number of subscriptions and happens at most once + * during the lifetime of the returned {@code Flowable}. If this {@code ConnectableFlowable} + * terminates, the connection is never renewed, no matter how {@code Subscriber}s come + * and go. Use {@link #refCount()} to renew a connection or dispose an active + * connection when all {@code Subscriber}s have cancelled their {@link Subscription}s. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator itself doesn't interfere with backpressure which is determined by + * the upstream {@code ConnectableFlowable}'s behavior.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code autoConnect} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param numberOfSubscribers the number of subscribers to await before calling connect + * on the {@code ConnectableFlowable}. A non-positive value indicates + * an immediate connection. + * @param connection the callback {@link Consumer} that will receive the {@code Disposable} representing the + * established connection + * @return a new {@code Flowable} instance that automatically connects to this {@code ConnectableFlowable} + * when the specified number of {@code Subscriber}s subscribe to it and calls the + * specified callback with the {@code Disposable} associated with the established connection + * @throws NullPointerException if {@code connection} is {@code null} + */ + @NonNull + @CheckReturnValue + @BackpressureSupport(BackpressureKind.PASS_THROUGH) + @SchedulerSupport(SchedulerSupport.NONE) + public Flowable<T> autoConnect(int numberOfSubscribers, @NonNull Consumer<? super Disposable> connection) { + Objects.requireNonNull(connection, "connection is null"); + if (numberOfSubscribers <= 0) { + this.connect(connection); + return RxJavaPlugins.onAssembly(this); + } + return RxJavaPlugins.onAssembly(new FlowableAutoConnect<>(this, numberOfSubscribers, connection)); + } +} diff --git a/src/main/java/io/reactivex/rxjava3/flowables/GroupedFlowable.java b/src/main/java/io/reactivex/rxjava3/flowables/GroupedFlowable.java new file mode 100644 index 0000000000..609fc7808f --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/flowables/GroupedFlowable.java @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.flowables; + +import io.reactivex.rxjava3.annotations.Nullable; +import io.reactivex.rxjava3.core.Flowable; + +/** + * A {@link Flowable} that has been grouped by key, the value of which can be obtained with {@link #getKey()}. + * <p> + * <em>Note:</em> A {@link GroupedFlowable} will cache the items it is to emit until such time as it + * is subscribed to. For this reason, in order to avoid memory leaks, you should not simply ignore those + * {@code GroupedFlowable}s that do not concern you. Instead, you can signal to them that they + * may discard their buffers by applying an operator like {@link Flowable#take take}{@code (0)} to them. + * + * @param <K> + * the type of the key + * @param <T> + * the type of the items emitted by the {@code GroupedFlowable} + * @see Flowable#groupBy(io.reactivex.rxjava3.functions.Function) + * @see <a href="http://reactivex.io/documentation/operators/groupby.html">ReactiveX documentation: GroupBy</a> + */ +public abstract class GroupedFlowable<K, T> extends Flowable<T> { + + final K key; + + /** + * Constructs a GroupedFlowable with the given key. + * @param key the key + */ + protected GroupedFlowable(@Nullable K key) { + this.key = key; + } + + /** + * Returns the key that identifies the group of items emitted by this {@code GroupedFlowable}. + * + * @return the key that the items emitted by this {@code GroupedFlowable} were grouped by + */ + @Nullable + public K getKey() { + return key; + } +} diff --git a/src/main/java/io/reactivex/rxjava3/flowables/package-info.java b/src/main/java/io/reactivex/rxjava3/flowables/package-info.java new file mode 100644 index 0000000000..06475a34ea --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/flowables/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +/** + * Classes supporting the Flowable base reactive class: + * {@link io.reactivex.rxjava3.flowables.ConnectableFlowable} and + * {@link io.reactivex.rxjava3.flowables.GroupedFlowable}. + */ +package io.reactivex.rxjava3.flowables; diff --git a/src/main/java/io/reactivex/rxjava3/functions/Action.java b/src/main/java/io/reactivex/rxjava3/functions/Action.java new file mode 100644 index 0000000000..509e5fcb64 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/functions/Action.java @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.functions; + +/** + * A functional interface similar to Runnable but allows throwing a checked exception. + */ +@FunctionalInterface +public interface Action { + /** + * Runs the action and optionally throws a checked exception. + * @throws Throwable if the implementation wishes to throw any type of exception + */ + void run() throws Throwable; +} diff --git a/src/main/java/io/reactivex/rxjava3/functions/BiConsumer.java b/src/main/java/io/reactivex/rxjava3/functions/BiConsumer.java new file mode 100644 index 0000000000..09fec68cbf --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/functions/BiConsumer.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.functions; + +import io.reactivex.rxjava3.annotations.NonNull; + +/** + * A functional interface (callback) that accepts two values (of possibly different types). + * @param <T1> the first value type + * @param <T2> the second value type + */ +@FunctionalInterface +public interface BiConsumer<@NonNull T1, @NonNull T2> { + + /** + * Performs an operation on the given values. + * @param t1 the first value + * @param t2 the second value + * @throws Throwable if the implementation wishes to throw any type of exception + */ + void accept(T1 t1, T2 t2) throws Throwable; +} diff --git a/src/main/java/io/reactivex/rxjava3/functions/BiFunction.java b/src/main/java/io/reactivex/rxjava3/functions/BiFunction.java new file mode 100644 index 0000000000..27aa9ee016 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/functions/BiFunction.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.functions; + +import io.reactivex.rxjava3.annotations.NonNull; + +/** + * A functional interface (callback) that computes a value based on multiple input values. + * @param <T1> the first value type + * @param <T2> the second value type + * @param <R> the result type + */ +@FunctionalInterface +public interface BiFunction<@NonNull T1, @NonNull T2, @NonNull R> { + + /** + * Calculate a value based on the input values. + * @param t1 the first value + * @param t2 the second value + * @return the result value + * @throws Throwable if the implementation wishes to throw any type of exception + */ + R apply(T1 t1, T2 t2) throws Throwable; +} diff --git a/src/main/java/io/reactivex/rxjava3/functions/BiPredicate.java b/src/main/java/io/reactivex/rxjava3/functions/BiPredicate.java new file mode 100644 index 0000000000..e45397aab7 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/functions/BiPredicate.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.functions; + +import io.reactivex.rxjava3.annotations.NonNull; + +/** + * A functional interface (callback) that returns true or false for the given input values. + * @param <T1> the first value + * @param <T2> the second value + */ +@FunctionalInterface +public interface BiPredicate<@NonNull T1, @NonNull T2> { + + /** + * Test the given input values and return a boolean. + * @param t1 the first value + * @param t2 the second value + * @return the boolean result + * @throws Throwable if the implementation wishes to throw any type of exception + */ + boolean test(@NonNull T1 t1, @NonNull T2 t2) throws Throwable; +} diff --git a/src/main/java/io/reactivex/rxjava3/functions/BooleanSupplier.java b/src/main/java/io/reactivex/rxjava3/functions/BooleanSupplier.java new file mode 100644 index 0000000000..4e3b447b6e --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/functions/BooleanSupplier.java @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.functions; + +/** + * A functional interface (callback) that returns a boolean value. + */ +@FunctionalInterface +public interface BooleanSupplier { + /** + * Returns a boolean value. + * @return a boolean value + * @throws Throwable if the implementation wishes to throw any type of exception + */ + boolean getAsBoolean() throws Throwable; // NOPMD +} diff --git a/src/main/java/io/reactivex/rxjava3/functions/Cancellable.java b/src/main/java/io/reactivex/rxjava3/functions/Cancellable.java new file mode 100644 index 0000000000..574c832268 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/functions/Cancellable.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.functions; + +/** + * A functional interface that has a single cancel method + * that can throw. + */ +@FunctionalInterface +public interface Cancellable { + + /** + * Cancel the action or free a resource. + * @throws Throwable if the implementation wishes to throw any type of exception + */ + void cancel() throws Throwable; +} diff --git a/src/main/java/io/reactivex/rxjava3/functions/Consumer.java b/src/main/java/io/reactivex/rxjava3/functions/Consumer.java new file mode 100644 index 0000000000..3ac1806438 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/functions/Consumer.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.functions; + +import io.reactivex.rxjava3.annotations.NonNull; + +/** + * A functional interface (callback) that accepts a single value. + * @param <T> the value type + */ +@FunctionalInterface +public interface Consumer<@NonNull T> { + /** + * Consume the given value. + * @param t the value + * @throws Throwable if the implementation wishes to throw any type of exception + */ + void accept(T t) throws Throwable; +} diff --git a/src/main/java/io/reactivex/rxjava3/functions/Function.java b/src/main/java/io/reactivex/rxjava3/functions/Function.java new file mode 100644 index 0000000000..40e11ca3ba --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/functions/Function.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.functions; + +import io.reactivex.rxjava3.annotations.NonNull; + +/** + * A functional interface that takes a value and returns another value, possibly with a + * different type and allows throwing a checked exception. + * + * @param <T> the input value type + * @param <R> the output value type + */ +@FunctionalInterface +public interface Function<@NonNull T, @NonNull R> { + /** + * Apply some calculation to the input value and return some other value. + * @param t the input value + * @return the output value + * @throws Throwable if the implementation wishes to throw any type of exception + */ + R apply(T t) throws Throwable; +} diff --git a/src/main/java/io/reactivex/rxjava3/functions/Function3.java b/src/main/java/io/reactivex/rxjava3/functions/Function3.java new file mode 100644 index 0000000000..377cceaeaf --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/functions/Function3.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.functions; + +import io.reactivex.rxjava3.annotations.NonNull; + +/** + * A functional interface (callback) that computes a value based on multiple input values. + * @param <T1> the first value type + * @param <T2> the second value type + * @param <T3> the third value type + * @param <R> the result type + */ +@FunctionalInterface +public interface Function3<@NonNull T1, @NonNull T2, @NonNull T3, @NonNull R> { + /** + * Calculate a value based on the input values. + * @param t1 the first value + * @param t2 the second value + * @param t3 the third value + * @return the result value + * @throws Throwable if the implementation wishes to throw any type of exception + */ + R apply(T1 t1, T2 t2, T3 t3) throws Throwable; +} diff --git a/src/main/java/io/reactivex/rxjava3/functions/Function4.java b/src/main/java/io/reactivex/rxjava3/functions/Function4.java new file mode 100644 index 0000000000..36db385819 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/functions/Function4.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.functions; + +import io.reactivex.rxjava3.annotations.NonNull; + +/** + * A functional interface (callback) that computes a value based on multiple input values. + * @param <T1> the first value type + * @param <T2> the second value type + * @param <T3> the third value type + * @param <T4> the fourth value type + * @param <R> the result type + */ +@FunctionalInterface +public interface Function4<@NonNull T1, @NonNull T2, @NonNull T3, @NonNull T4, @NonNull R> { + /** + * Calculate a value based on the input values. + * @param t1 the first value + * @param t2 the second value + * @param t3 the third value + * @param t4 the fourth value + * @return the result value + * @throws Throwable if the implementation wishes to throw any type of exception + */ + R apply(T1 t1, T2 t2, T3 t3, T4 t4) throws Throwable; +} diff --git a/src/main/java/io/reactivex/rxjava3/functions/Function5.java b/src/main/java/io/reactivex/rxjava3/functions/Function5.java new file mode 100644 index 0000000000..d3fc1504c4 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/functions/Function5.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.functions; + +import io.reactivex.rxjava3.annotations.NonNull; + +/** + * A functional interface (callback) that computes a value based on multiple input values. + * @param <T1> the first value type + * @param <T2> the second value type + * @param <T3> the third value type + * @param <T4> the fourth value type + * @param <T5> the fifth value type + * @param <R> the result type + */ +@FunctionalInterface +public interface Function5<@NonNull T1, @NonNull T2, @NonNull T3, @NonNull T4, @NonNull T5, @NonNull R> { + /** + * Calculate a value based on the input values. + * @param t1 the first value + * @param t2 the second value + * @param t3 the third value + * @param t4 the fourth value + * @param t5 the fifth value + * @return the result value + * @throws Throwable if the implementation wishes to throw any type of exception + */ + R apply(T1 t1, T2 t2, T3 t3, T4 t4, T5 t5) throws Throwable; +} diff --git a/src/main/java/io/reactivex/rxjava3/functions/Function6.java b/src/main/java/io/reactivex/rxjava3/functions/Function6.java new file mode 100644 index 0000000000..2969a39f82 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/functions/Function6.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.functions; + +import io.reactivex.rxjava3.annotations.NonNull; + +/** + * A functional interface (callback) that computes a value based on multiple input values. + * @param <T1> the first value type + * @param <T2> the second value type + * @param <T3> the third value type + * @param <T4> the fourth value type + * @param <T5> the fifth value type + * @param <T6> the sixth value type + * @param <R> the result type + */ +@FunctionalInterface +public interface Function6<@NonNull T1, @NonNull T2, @NonNull T3, @NonNull T4, @NonNull T5, @NonNull T6, @NonNull R> { + /** + * Calculate a value based on the input values. + * @param t1 the first value + * @param t2 the second value + * @param t3 the third value + * @param t4 the fourth value + * @param t5 the fifth value + * @param t6 the sixth value + * @return the result value + * @throws Throwable if the implementation wishes to throw any type of exception + */ + R apply(T1 t1, T2 t2, T3 t3, T4 t4, T5 t5, T6 t6) throws Throwable; +} diff --git a/src/main/java/io/reactivex/rxjava3/functions/Function7.java b/src/main/java/io/reactivex/rxjava3/functions/Function7.java new file mode 100644 index 0000000000..091b203296 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/functions/Function7.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.functions; + +import io.reactivex.rxjava3.annotations.NonNull; + +/** + * A functional interface (callback) that computes a value based on multiple input values. + * @param <T1> the first value type + * @param <T2> the second value type + * @param <T3> the third value type + * @param <T4> the fourth value type + * @param <T5> the fifth value type + * @param <T6> the sixth value type + * @param <T7> the seventh value type + * @param <R> the result type + */ +@FunctionalInterface +public interface Function7<@NonNull T1, @NonNull T2, @NonNull T3, @NonNull T4, @NonNull T5, @NonNull T6, @NonNull T7, @NonNull R> { + /** + * Calculate a value based on the input values. + * @param t1 the first value + * @param t2 the second value + * @param t3 the third value + * @param t4 the fourth value + * @param t5 the fifth value + * @param t6 the sixth value + * @param t7 the seventh value + * @return the result value + * @throws Throwable if the implementation wishes to throw any type of exception + */ + R apply(T1 t1, T2 t2, T3 t3, T4 t4, T5 t5, T6 t6, T7 t7) throws Throwable; +} diff --git a/src/main/java/io/reactivex/rxjava3/functions/Function8.java b/src/main/java/io/reactivex/rxjava3/functions/Function8.java new file mode 100644 index 0000000000..c8e21acf9d --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/functions/Function8.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.functions; + +import io.reactivex.rxjava3.annotations.NonNull; + +/** + * A functional interface (callback) that computes a value based on multiple input values. + * @param <T1> the first value type + * @param <T2> the second value type + * @param <T3> the third value type + * @param <T4> the fourth value type + * @param <T5> the fifth value type + * @param <T6> the sixth value type + * @param <T7> the seventh value type + * @param <T8> the eighth value type + * @param <R> the result type + */ +@FunctionalInterface +public interface Function8<@NonNull T1, @NonNull T2, @NonNull T3, @NonNull T4, @NonNull T5, @NonNull T6, @NonNull T7, @NonNull T8, @NonNull R> { + /** + * Calculate a value based on the input values. + * @param t1 the first value + * @param t2 the second value + * @param t3 the third value + * @param t4 the fourth value + * @param t5 the fifth value + * @param t6 the sixth value + * @param t7 the seventh value + * @param t8 the eighth value + * @return the result value + * @throws Throwable if the implementation wishes to throw any type of exception + */ + R apply(T1 t1, T2 t2, T3 t3, T4 t4, T5 t5, T6 t6, T7 t7, T8 t8) throws Throwable; +} diff --git a/src/main/java/io/reactivex/rxjava3/functions/Function9.java b/src/main/java/io/reactivex/rxjava3/functions/Function9.java new file mode 100644 index 0000000000..0b182eb5f3 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/functions/Function9.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.functions; + +import io.reactivex.rxjava3.annotations.NonNull; + +/** + * A functional interface (callback) that computes a value based on multiple input values. + * @param <T1> the first value type + * @param <T2> the second value type + * @param <T3> the third value type + * @param <T4> the fourth value type + * @param <T5> the fifth value type + * @param <T6> the sixth value type + * @param <T7> the seventh value type + * @param <T8> the eighth value type + * @param <T9> the ninth value type + * @param <R> the result type + */ +@FunctionalInterface +public interface Function9<@NonNull T1, @NonNull T2, @NonNull T3, @NonNull T4, @NonNull T5, @NonNull T6, @NonNull T7, @NonNull T8, @NonNull T9, @NonNull R> { + /** + * Calculate a value based on the input values. + * @param t1 the first value + * @param t2 the second value + * @param t3 the third value + * @param t4 the fourth value + * @param t5 the fifth value + * @param t6 the sixth value + * @param t7 the seventh value + * @param t8 the eighth value + * @param t9 the ninth value + * @return the result value + * @throws Throwable if the implementation wishes to throw any type of exception + */ + R apply(T1 t1, T2 t2, T3 t3, T4 t4, T5 t5, T6 t6, T7 t7, T8 t8, T9 t9) throws Throwable; +} diff --git a/src/main/java/io/reactivex/rxjava3/functions/IntFunction.java b/src/main/java/io/reactivex/rxjava3/functions/IntFunction.java new file mode 100644 index 0000000000..3a405aac26 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/functions/IntFunction.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.functions; + +import io.reactivex.rxjava3.annotations.NonNull; + +/** + * A functional interface (callback) that takes a primitive value and return value of type T. + * @param <T> the returned value type + */ +@FunctionalInterface +public interface IntFunction<@NonNull T> { + /** + * Calculates a value based on a primitive integer input. + * @param i the input value + * @return the result Object + * @throws Throwable if the implementation wishes to throw any type of exception + */ + T apply(int i) throws Throwable; +} diff --git a/src/main/java/io/reactivex/rxjava3/functions/LongConsumer.java b/src/main/java/io/reactivex/rxjava3/functions/LongConsumer.java new file mode 100644 index 0000000000..5b12752c1d --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/functions/LongConsumer.java @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.functions; + +/** + * A functional interface (callback) that consumes a primitive long value. + */ +@FunctionalInterface +public interface LongConsumer { + /** + * Consume a primitive long input. + * @param t the primitive long value + * @throws Throwable if the implementation wishes to throw any type of exception + */ + void accept(long t) throws Throwable; +} diff --git a/src/main/java/io/reactivex/rxjava3/functions/Predicate.java b/src/main/java/io/reactivex/rxjava3/functions/Predicate.java new file mode 100644 index 0000000000..1bce2d6bff --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/functions/Predicate.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.functions; + +import io.reactivex.rxjava3.annotations.NonNull; + +/** + * A functional interface (callback) that returns true or false for the given input value. + * @param <T> the first value + */ +@FunctionalInterface +public interface Predicate<@NonNull T> { + /** + * Test the given input value and return a boolean. + * @param t the value + * @return the boolean result + * @throws Throwable if the implementation wishes to throw any type of exception + */ + boolean test(T t) throws Throwable; +} diff --git a/src/main/java/io/reactivex/rxjava3/functions/Supplier.java b/src/main/java/io/reactivex/rxjava3/functions/Supplier.java new file mode 100644 index 0000000000..ebf7fdda6d --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/functions/Supplier.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.functions; + +/** + * A functional interface (callback) that provides a single value or + * throws an exception. + * <p> + * This interface was added to allow throwing any subclass of {@link Throwable}s, + * which is not directly possible with the Java standard {@link java.util.concurrent.Callable} interface. + * @param <T> the value type returned + * @since 3.0.0 + */ +@FunctionalInterface +public interface Supplier<T> { + + /** + * Produces a value or throws an exception. + * @return the value produced + * @throws Throwable if the implementation wishes to throw any type of exception + */ + T get() throws Throwable; +} diff --git a/src/main/java/io/reactivex/rxjava3/functions/package-info.java b/src/main/java/io/reactivex/rxjava3/functions/package-info.java new file mode 100644 index 0000000000..ab9dad0426 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/functions/package-info.java @@ -0,0 +1,18 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +/** + * Functional interfaces of functions and actions of arity 0 to 9 and related + * utility classes. + */ +package io.reactivex.rxjava3.functions; diff --git a/src/main/java/io/reactivex/rxjava3/internal/disposables/ArrayCompositeDisposable.java b/src/main/java/io/reactivex/rxjava3/internal/disposables/ArrayCompositeDisposable.java new file mode 100644 index 0000000000..c9a6bc9bc4 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/disposables/ArrayCompositeDisposable.java @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.disposables; + +import java.util.concurrent.atomic.AtomicReferenceArray; + +import io.reactivex.rxjava3.disposables.Disposable; + +/** + * A composite disposable with a fixed number of slots. + * + * <p>Note that since the implementation leaks the methods of AtomicReferenceArray, one must be + * careful to only call setResource, replaceResource and dispose on it. All other methods may lead to undefined behavior + * and should be used by internal means only. + */ +public final class ArrayCompositeDisposable extends AtomicReferenceArray<Disposable> implements Disposable { + + private static final long serialVersionUID = 2746389416410565408L; + + public ArrayCompositeDisposable(int capacity) { + super(capacity); + } + + /** + * Sets the resource at the specified index and disposes the old resource. + * @param index the index of the resource to set + * @param resource the new resource + * @return true if the resource has ben set, false if the composite has been disposed + */ + public boolean setResource(int index, Disposable resource) { + for (;;) { + Disposable o = get(index); + if (o == DisposableHelper.DISPOSED) { + resource.dispose(); + return false; + } + if (compareAndSet(index, o, resource)) { + if (o != null) { + o.dispose(); + } + return true; + } + } + } + + /** + * Replaces the resource at the specified index and returns the old resource. + * @param index the index of the resource to replace + * @param resource the new resource + * @return the old resource, can be null + */ + public Disposable replaceResource(int index, Disposable resource) { + for (;;) { + Disposable o = get(index); + if (o == DisposableHelper.DISPOSED) { + resource.dispose(); + return null; + } + if (compareAndSet(index, o, resource)) { + return o; + } + } + } + + @Override + public void dispose() { + if (get(0) != DisposableHelper.DISPOSED) { + int s = length(); + for (int i = 0; i < s; i++) { + Disposable o = get(i); + if (o != DisposableHelper.DISPOSED) { + o = getAndSet(i, DisposableHelper.DISPOSED); + if (o != DisposableHelper.DISPOSED && o != null) { + o.dispose(); + } + } + } + } + } + + @Override + public boolean isDisposed() { + return get(0) == DisposableHelper.DISPOSED; + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/disposables/CancellableDisposable.java b/src/main/java/io/reactivex/rxjava3/internal/disposables/CancellableDisposable.java new file mode 100644 index 0000000000..863f52e91b --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/disposables/CancellableDisposable.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.disposables; + +import java.util.concurrent.atomic.AtomicReference; + +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.Cancellable; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +/** + * A disposable container that wraps a Cancellable instance. + * <p> + * Watch out for the AtomicReference API leak! + */ +public final class CancellableDisposable extends AtomicReference<Cancellable> +implements Disposable { + + private static final long serialVersionUID = 5718521705281392066L; + + public CancellableDisposable(Cancellable cancellable) { + super(cancellable); + } + + @Override + public boolean isDisposed() { + return get() == null; + } + + @Override + public void dispose() { + if (get() != null) { + Cancellable c = getAndSet(null); + if (c != null) { + try { + c.cancel(); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + RxJavaPlugins.onError(ex); + } + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/disposables/DisposableHelper.java b/src/main/java/io/reactivex/rxjava3/internal/disposables/DisposableHelper.java new file mode 100644 index 0000000000..79dbd855ac --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/disposables/DisposableHelper.java @@ -0,0 +1,185 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.disposables; + +import java.util.Objects; +import java.util.concurrent.atomic.AtomicReference; + +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.ProtocolViolationException; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +/** + * Utility methods for working with Disposables atomically. + */ +public enum DisposableHelper implements Disposable { + /** + * The singleton instance representing a terminal, disposed state, don't leak it. + */ + DISPOSED + ; + + /** + * Checks if the given Disposable is the common {@link #DISPOSED} enum value. + * @param d the disposable to check + * @return true if d is {@link #DISPOSED} + */ + public static boolean isDisposed(Disposable d) { + return d == DISPOSED; + } + + /** + * Atomically sets the field and disposes the old contents. + * @param field the target field + * @param d the new Disposable to set + * @return true if successful, false if the field contains the {@link #DISPOSED} instance. + */ + public static boolean set(AtomicReference<Disposable> field, Disposable d) { + for (;;) { + Disposable current = field.get(); + if (current == DISPOSED) { + if (d != null) { + d.dispose(); + } + return false; + } + if (field.compareAndSet(current, d)) { + if (current != null) { + current.dispose(); + } + return true; + } + } + } + + /** + * Atomically sets the field to the given non-null Disposable and returns true + * or returns false if the field is non-null. + * If the target field contains the common DISPOSED instance, the supplied disposable + * is disposed. If the field contains other non-null Disposable, an IllegalStateException + * is signalled to the RxJavaPlugins.onError hook. + * + * @param field the target field + * @param d the disposable to set, not null + * @return true if the operation succeeded, false + */ + public static boolean setOnce(AtomicReference<Disposable> field, Disposable d) { + Objects.requireNonNull(d, "d is null"); + if (!field.compareAndSet(null, d)) { + d.dispose(); + if (field.get() != DISPOSED) { + reportDisposableSet(); + } + return false; + } + return true; + } + + /** + * Atomically replaces the Disposable in the field with the given new Disposable + * but does not dispose the old one. + * @param field the target field to change + * @param d the new disposable, null allowed + * @return true if the operation succeeded, false if the target field contained + * the common DISPOSED instance and the given disposable (if not null) is disposed. + */ + public static boolean replace(AtomicReference<Disposable> field, Disposable d) { + for (;;) { + Disposable current = field.get(); + if (current == DISPOSED) { + if (d != null) { + d.dispose(); + } + return false; + } + if (field.compareAndSet(current, d)) { + return true; + } + } + } + + /** + * Atomically disposes the Disposable in the field if not already disposed. + * @param field the target field + * @return true if the current thread managed to dispose the Disposable + */ + public static boolean dispose(AtomicReference<Disposable> field) { + Disposable current = field.get(); + Disposable d = DISPOSED; + if (current != d) { + current = field.getAndSet(d); + if (current != d) { + if (current != null) { + current.dispose(); + } + return true; + } + } + return false; + } + + /** + * Verifies that current is null, next is not null, otherwise signals errors + * to the RxJavaPlugins and returns false. + * @param current the current Disposable, expected to be null + * @param next the next Disposable, expected to be non-null + * @return true if the validation succeeded + */ + public static boolean validate(Disposable current, Disposable next) { + if (next == null) { + RxJavaPlugins.onError(new NullPointerException("next is null")); + return false; + } + if (current != null) { + next.dispose(); + reportDisposableSet(); + return false; + } + return true; + } + + /** + * Reports that the disposable is already set to the RxJavaPlugins error handler. + */ + public static void reportDisposableSet() { + RxJavaPlugins.onError(new ProtocolViolationException("Disposable already set!")); + } + + /** + * Atomically tries to set the given Disposable on the field if it is null or disposes it if + * the field contains {@link #DISPOSED}. + * @param field the target field + * @param d the disposable to set + * @return true if successful, false otherwise + */ + public static boolean trySet(AtomicReference<Disposable> field, Disposable d) { + if (!field.compareAndSet(null, d)) { + if (field.get() == DISPOSED) { + d.dispose(); + } + return false; + } + return true; + } + + @Override + public void dispose() { + // deliberately no-op + } + + @Override + public boolean isDisposed() { + return true; + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/disposables/EmptyDisposable.java b/src/main/java/io/reactivex/rxjava3/internal/disposables/EmptyDisposable.java new file mode 100644 index 0000000000..fdb30932ed --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/disposables/EmptyDisposable.java @@ -0,0 +1,117 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.disposables; + +import io.reactivex.rxjava3.annotations.Nullable; +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.operators.QueueDisposable; + +/** + * Represents a stateless empty Disposable that reports being always + * empty and disposed. + * <p>It is also async-fuseable but empty all the time. + * <p>Since EmptyDisposable implements QueueDisposable and is empty, + * don't use it in tests and then signal onNext with it; + * use Disposables.empty() instead. + */ +public enum EmptyDisposable implements QueueDisposable<Object> { + /** + * Since EmptyDisposable implements QueueDisposable and is empty, + * don't use it in tests and then signal onNext with it; + * use Disposables.empty() instead. + */ + INSTANCE, + /** + * An empty disposable that returns false for isDisposed. + */ + NEVER + ; + + @Override + public void dispose() { + // no-op + } + + @Override + public boolean isDisposed() { + return this == INSTANCE; + } + + public static void complete(Observer<?> observer) { + observer.onSubscribe(INSTANCE); + observer.onComplete(); + } + + public static void complete(MaybeObserver<?> observer) { + observer.onSubscribe(INSTANCE); + observer.onComplete(); + } + + public static void error(Throwable e, Observer<?> observer) { + observer.onSubscribe(INSTANCE); + observer.onError(e); + } + + public static void complete(CompletableObserver observer) { + observer.onSubscribe(INSTANCE); + observer.onComplete(); + } + + public static void error(Throwable e, CompletableObserver observer) { + observer.onSubscribe(INSTANCE); + observer.onError(e); + } + + public static void error(Throwable e, SingleObserver<?> observer) { + observer.onSubscribe(INSTANCE); + observer.onError(e); + } + + public static void error(Throwable e, MaybeObserver<?> observer) { + observer.onSubscribe(INSTANCE); + observer.onError(e); + } + + @Override + public boolean offer(Object value) { + throw new UnsupportedOperationException("Should not be called!"); + } + + @Override + public boolean offer(Object v1, Object v2) { + throw new UnsupportedOperationException("Should not be called!"); + } + + @Nullable + @Override + public Object poll() { + return null; // always empty + } + + @Override + public boolean isEmpty() { + return true; // always empty + } + + @Override + public void clear() { + // nothing to do + } + + @Override + public int requestFusion(int mode) { + return mode & ASYNC; + } + +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/disposables/ListCompositeDisposable.java b/src/main/java/io/reactivex/rxjava3/internal/disposables/ListCompositeDisposable.java new file mode 100644 index 0000000000..4c4b1f8a73 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/disposables/ListCompositeDisposable.java @@ -0,0 +1,187 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.disposables; + +import java.util.*; + +import io.reactivex.rxjava3.disposables.*; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.internal.util.ExceptionHelper; + +/** + * A disposable container that can hold onto multiple other disposables. + */ +public final class ListCompositeDisposable implements Disposable, DisposableContainer { + + List<Disposable> resources; + + volatile boolean disposed; + + public ListCompositeDisposable() { + } + + public ListCompositeDisposable(Disposable... resources) { + Objects.requireNonNull(resources, "resources is null"); + this.resources = new LinkedList<>(); + for (Disposable d : resources) { + Objects.requireNonNull(d, "Disposable item is null"); + this.resources.add(d); + } + } + + public ListCompositeDisposable(Iterable<? extends Disposable> resources) { + Objects.requireNonNull(resources, "resources is null"); + this.resources = new LinkedList<>(); + for (Disposable d : resources) { + Objects.requireNonNull(d, "Disposable item is null"); + this.resources.add(d); + } + } + + @Override + public void dispose() { + if (disposed) { + return; + } + List<Disposable> set; + synchronized (this) { + if (disposed) { + return; + } + disposed = true; + set = resources; + resources = null; + } + + dispose(set); + } + + @Override + public boolean isDisposed() { + return disposed; + } + + @Override + public boolean add(Disposable d) { + Objects.requireNonNull(d, "d is null"); + if (!disposed) { + synchronized (this) { + if (!disposed) { + List<Disposable> set = resources; + if (set == null) { + set = new LinkedList<>(); + resources = set; + } + set.add(d); + return true; + } + } + } + d.dispose(); + return false; + } + + public boolean addAll(Disposable... ds) { + Objects.requireNonNull(ds, "ds is null"); + if (!disposed) { + synchronized (this) { + if (!disposed) { + List<Disposable> set = resources; + if (set == null) { + set = new LinkedList<>(); + resources = set; + } + for (Disposable d : ds) { + Objects.requireNonNull(d, "d is null"); + set.add(d); + } + return true; + } + } + } + for (Disposable d : ds) { + d.dispose(); + } + return false; + } + + @Override + public boolean remove(Disposable d) { + if (delete(d)) { + d.dispose(); + return true; + } + return false; + } + + @Override + public boolean delete(Disposable d) { + Objects.requireNonNull(d, "Disposable item is null"); + if (disposed) { + return false; + } + synchronized (this) { + if (disposed) { + return false; + } + + List<Disposable> set = resources; + if (set == null || !set.remove(d)) { + return false; + } + } + return true; + } + + public void clear() { + if (disposed) { + return; + } + List<Disposable> set; + synchronized (this) { + if (disposed) { + return; + } + + set = resources; + resources = null; + } + + dispose(set); + } + + void dispose(List<Disposable> set) { + if (set == null) { + return; + } + List<Throwable> errors = null; + for (Disposable o : set) { + try { + o.dispose(); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + if (errors == null) { + errors = new ArrayList<>(); + } + errors.add(ex); + } + } + if (errors != null) { + if (errors.size() == 1) { + throw ExceptionHelper.wrapOrThrow(errors.get(0)); + } + throw new CompositeException(errors); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/disposables/SequentialDisposable.java b/src/main/java/io/reactivex/rxjava3/internal/disposables/SequentialDisposable.java new file mode 100644 index 0000000000..f6aef6bdd3 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/disposables/SequentialDisposable.java @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.disposables; + +import java.util.concurrent.atomic.AtomicReference; + +import io.reactivex.rxjava3.disposables.Disposable; + +/** + * A Disposable container that allows updating/replacing a Disposable + * atomically and with respect of disposing the container itself. + * <p> + * The class extends AtomicReference directly so watch out for the API leak! + * @since 2.0 + */ +public final class SequentialDisposable +extends AtomicReference<Disposable> +implements Disposable { + + private static final long serialVersionUID = -754898800686245608L; + + /** + * Constructs an empty SequentialDisposable. + */ + public SequentialDisposable() { + // nothing to do + } + + /** + * Construct a SequentialDisposable with the initial Disposable provided. + * @param initial the initial disposable, null allowed + */ + public SequentialDisposable(Disposable initial) { + lazySet(initial); + } + + /** + * Atomically: set the next disposable on this container and dispose the previous + * one (if any) or dispose next if the container has been disposed. + * @param next the Disposable to set, may be null + * @return true if the operation succeeded, false if the container has been disposed + * @see #replace(Disposable) + */ + public boolean update(Disposable next) { + return DisposableHelper.set(this, next); + } + + /** + * Atomically: set the next disposable on this container but don't dispose the previous + * one (if any) or dispose next if the container has been disposed. + * @param next the Disposable to set, may be null + * @return true if the operation succeeded, false if the container has been disposed + * @see #update(Disposable) + */ + public boolean replace(Disposable next) { + return DisposableHelper.replace(this, next); + } + + @Override + public void dispose() { + DisposableHelper.dispose(this); + } + + @Override + public boolean isDisposed() { + return DisposableHelper.isDisposed(get()); + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/functions/Functions.java b/src/main/java/io/reactivex/rxjava3/internal/functions/Functions.java new file mode 100644 index 0000000000..e0d3dead3c --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/functions/Functions.java @@ -0,0 +1,773 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.functions; + +import java.util.*; +import java.util.concurrent.*; + +import org.reactivestreams.Subscription; + +import io.reactivex.rxjava3.annotations.NonNull; +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.OnErrorNotImplementedException; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.schedulers.Timed; + +/** + * Utility methods to convert the BiFunction, Function3..Function9 instances to Function of Object array. + */ +public final class Functions { + + /** Utility class. */ + private Functions() { + throw new IllegalStateException("No instances!"); + } + + @NonNull + public static <T1, T2, R> Function<Object[], R> toFunction(@NonNull BiFunction<? super T1, ? super T2, ? extends R> f) { + return new Array2Func<>(f); + } + + @NonNull + public static <T1, T2, T3, R> Function<Object[], R> toFunction(@NonNull Function3<T1, T2, T3, R> f) { + return new Array3Func<>(f); + } + + @NonNull + public static <T1, T2, T3, T4, R> Function<Object[], R> toFunction(@NonNull Function4<T1, T2, T3, T4, R> f) { + return new Array4Func<>(f); + } + + @NonNull + public static <T1, T2, T3, T4, T5, R> Function<Object[], R> toFunction(@NonNull Function5<T1, T2, T3, T4, T5, R> f) { + return new Array5Func<>(f); + } + + @NonNull + public static <T1, T2, T3, T4, T5, T6, R> Function<Object[], R> toFunction( + @NonNull Function6<T1, T2, T3, T4, T5, T6, R> f) { + return new Array6Func<>(f); + } + + @NonNull + public static <T1, T2, T3, T4, T5, T6, T7, R> Function<Object[], R> toFunction( + @NonNull Function7<T1, T2, T3, T4, T5, T6, T7, R> f) { + return new Array7Func<>(f); + } + + @NonNull + public static <T1, T2, T3, T4, T5, T6, T7, T8, R> Function<Object[], R> toFunction( + @NonNull Function8<T1, T2, T3, T4, T5, T6, T7, T8, R> f) { + return new Array8Func<>(f); + } + + @NonNull + public static <T1, T2, T3, T4, T5, T6, T7, T8, T9, R> Function<Object[], R> toFunction( + @NonNull Function9<T1, T2, T3, T4, T5, T6, T7, T8, T9, R> f) { + return new Array9Func<>(f); + } + + /** A singleton identity function. */ + static final Function<Object, Object> IDENTITY = new Identity(); + + /** + * Returns an identity function that simply returns its argument. + * @param <T> the input and output value type + * @return the identity function + */ + @SuppressWarnings("unchecked") + @NonNull + public static <T> Function<T, T> identity() { + return (Function<T, T>)IDENTITY; + } + + public static final Runnable EMPTY_RUNNABLE = new EmptyRunnable(); + + public static final Action EMPTY_ACTION = new EmptyAction(); + + static final Consumer<Object> EMPTY_CONSUMER = new EmptyConsumer(); + + /** + * Returns an empty consumer that does nothing. + * @param <T> the consumed value type, the value is ignored + * @return an empty consumer that does nothing. + */ + @SuppressWarnings("unchecked") + public static <T> Consumer<T> emptyConsumer() { + return (Consumer<T>)EMPTY_CONSUMER; + } + + public static final Consumer<Throwable> ERROR_CONSUMER = new ErrorConsumer(); + + /** + * Wraps the consumed Throwable into an OnErrorNotImplementedException and + * signals it to the plugin error handler. + */ + public static final Consumer<Throwable> ON_ERROR_MISSING = new OnErrorMissingConsumer(); + + public static final LongConsumer EMPTY_LONG_CONSUMER = new EmptyLongConsumer(); + + static final Predicate<Object> ALWAYS_TRUE = new TruePredicate(); + + static final Predicate<Object> ALWAYS_FALSE = new FalsePredicate(); + + static final Supplier<Object> NULL_SUPPLIER = new NullProvider(); + + @SuppressWarnings("unchecked") + @NonNull + public static <T> Predicate<T> alwaysTrue() { + return (Predicate<T>)ALWAYS_TRUE; + } + + @SuppressWarnings("unchecked") + @NonNull + public static <T> Predicate<T> alwaysFalse() { + return (Predicate<T>)ALWAYS_FALSE; + } + + @SuppressWarnings("unchecked") + @NonNull + public static <T> Supplier<T> nullSupplier() { + return (Supplier<T>)NULL_SUPPLIER; + } + + static final class FutureAction implements Action { + final Future<?> future; + + FutureAction(Future<?> future) { + this.future = future; + } + + @Override + public void run() throws Exception { + future.get(); + } + } + + /** + * Wraps the blocking get call of the Future into an Action. + * @param future the future to call get() on, not null + * @return the new Action instance + */ + @NonNull + public static Action futureAction(@NonNull Future<?> future) { + return new FutureAction(future); + } + + static final class JustValue<T, U> implements Callable<U>, Supplier<U>, Function<T, U> { + final U value; + + JustValue(U value) { + this.value = value; + } + + @Override + public U call() { + return value; + } + + @Override + public U apply(T t) { + return value; + } + + @Override + public U get() { + return value; + } + } + + /** + * Returns a Callable that returns the given value. + * @param <T> the value type + * @param value the value to return + * @return the new Callable instance + */ + @NonNull + public static <T> Callable<T> justCallable(@NonNull T value) { + return new JustValue<>(value); + } + + /** + * Returns a Supplier that returns the given value. + * @param <T> the value type + * @param value the value to return + * @return the new Callable instance + */ + @NonNull + public static <T> Supplier<T> justSupplier(@NonNull T value) { + return new JustValue<>(value); + } + + /** + * Returns a Function that ignores its parameter and returns the given value. + * @param <T> the function's input type + * @param <U> the value and return type of the function + * @param value the value to return + * @return the new Function instance + */ + @NonNull + public static <T, U> Function<T, U> justFunction(@NonNull U value) { + return new JustValue<>(value); + } + + static final class CastToClass<T, U> implements Function<T, U> { + final Class<U> clazz; + + CastToClass(Class<U> clazz) { + this.clazz = clazz; + } + + @Override + public U apply(T t) { + return clazz.cast(t); + } + } + + /** + * Returns a function that cast the incoming values via a Class object. + * @param <T> the input value type + * @param <U> the output and target type + * @param target the target class + * @return the new Function instance + */ + @NonNull + public static <T, U> Function<T, U> castFunction(@NonNull Class<U> target) { + return new CastToClass<>(target); + } + + static final class ArrayListCapacityCallable<T> implements Supplier<List<T>> { + final int capacity; + + ArrayListCapacityCallable(int capacity) { + this.capacity = capacity; + } + + @Override + public List<T> get() { + return new ArrayList<>(capacity); + } + } + + public static <T> Supplier<List<T>> createArrayList(int capacity) { + return new ArrayListCapacityCallable<>(capacity); + } + + static final class EqualsPredicate<T> implements Predicate<T> { + final T value; + + EqualsPredicate(T value) { + this.value = value; + } + + @Override + public boolean test(T t) { + return Objects.equals(t, value); + } + } + + public static <T> Predicate<T> equalsWith(T value) { + return new EqualsPredicate<>(value); + } + + enum HashSetSupplier implements Supplier<Set<Object>> { + INSTANCE; + @Override + public Set<Object> get() { + return new HashSet<>(); + } + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + public static <T> Supplier<Set<T>> createHashSet() { + return (Supplier)HashSetSupplier.INSTANCE; + } + + static final class NotificationOnNext<T> implements Consumer<T> { + final Consumer<? super Notification<T>> onNotification; + + NotificationOnNext(Consumer<? super Notification<T>> onNotification) { + this.onNotification = onNotification; + } + + @Override + public void accept(T v) throws Throwable { + onNotification.accept(Notification.createOnNext(v)); + } + } + + static final class NotificationOnError<T> implements Consumer<Throwable> { + final Consumer<? super Notification<T>> onNotification; + + NotificationOnError(Consumer<? super Notification<T>> onNotification) { + this.onNotification = onNotification; + } + + @Override + public void accept(Throwable v) throws Throwable { + onNotification.accept(Notification.createOnError(v)); + } + } + + static final class NotificationOnComplete<T> implements Action { + final Consumer<? super Notification<T>> onNotification; + + NotificationOnComplete(Consumer<? super Notification<T>> onNotification) { + this.onNotification = onNotification; + } + + @Override + public void run() throws Throwable { + onNotification.accept(Notification.createOnComplete()); + } + } + + public static <T> Consumer<T> notificationOnNext(Consumer<? super Notification<T>> onNotification) { + return new NotificationOnNext<>(onNotification); + } + + public static <T> Consumer<Throwable> notificationOnError(Consumer<? super Notification<T>> onNotification) { + return new NotificationOnError<>(onNotification); + } + + public static <T> Action notificationOnComplete(Consumer<? super Notification<T>> onNotification) { + return new NotificationOnComplete<>(onNotification); + } + + static final class ActionConsumer<T> implements Consumer<T> { + final Action action; + + ActionConsumer(Action action) { + this.action = action; + } + + @Override + public void accept(T t) throws Throwable { + action.run(); + } + } + + public static <T> Consumer<T> actionConsumer(Action action) { + return new ActionConsumer<>(action); + } + + static final class ClassFilter<T, U> implements Predicate<T> { + final Class<U> clazz; + + ClassFilter(Class<U> clazz) { + this.clazz = clazz; + } + + @Override + public boolean test(T t) { + return clazz.isInstance(t); + } + } + + public static <T, U> Predicate<T> isInstanceOf(Class<U> clazz) { + return new ClassFilter<>(clazz); + } + + static final class BooleanSupplierPredicateReverse<T> implements Predicate<T> { + final BooleanSupplier supplier; + + BooleanSupplierPredicateReverse(BooleanSupplier supplier) { + this.supplier = supplier; + } + + @Override + public boolean test(T t) throws Throwable { + return !supplier.getAsBoolean(); + } + } + + public static <T> Predicate<T> predicateReverseFor(BooleanSupplier supplier) { + return new BooleanSupplierPredicateReverse<>(supplier); + } + + static final class TimestampFunction<T> implements Function<T, Timed<T>> { + final TimeUnit unit; + + final Scheduler scheduler; + + TimestampFunction(TimeUnit unit, Scheduler scheduler) { + this.unit = unit; + this.scheduler = scheduler; + } + + @Override + public Timed<T> apply(T t) { + return new Timed<>(t, scheduler.now(unit), unit); + } + } + + public static <T> Function<T, Timed<T>> timestampWith(TimeUnit unit, Scheduler scheduler) { + return new TimestampFunction<>(unit, scheduler); + } + + static final class ToMapKeySelector<K, T> implements BiConsumer<Map<K, T>, T> { + private final Function<? super T, ? extends K> keySelector; + + ToMapKeySelector(Function<? super T, ? extends K> keySelector) { + this.keySelector = keySelector; + } + + @Override + public void accept(Map<K, T> m, T t) throws Throwable { + K key = keySelector.apply(t); + m.put(key, t); + } + } + + public static <T, K> BiConsumer<Map<K, T>, T> toMapKeySelector(final Function<? super T, ? extends K> keySelector) { + return new ToMapKeySelector<>(keySelector); + } + + static final class ToMapKeyValueSelector<K, V, T> implements BiConsumer<Map<K, V>, T> { + private final Function<? super T, ? extends V> valueSelector; + private final Function<? super T, ? extends K> keySelector; + + ToMapKeyValueSelector(Function<? super T, ? extends V> valueSelector, + Function<? super T, ? extends K> keySelector) { + this.valueSelector = valueSelector; + this.keySelector = keySelector; + } + + @Override + public void accept(Map<K, V> m, T t) throws Throwable { + K key = keySelector.apply(t); + V value = valueSelector.apply(t); + m.put(key, value); + } + } + + public static <T, K, V> BiConsumer<Map<K, V>, T> toMapKeyValueSelector(final Function<? super T, ? extends K> keySelector, final Function<? super T, ? extends V> valueSelector) { + return new ToMapKeyValueSelector<>(valueSelector, keySelector); + } + + static final class ToMultimapKeyValueSelector<K, V, T> implements BiConsumer<Map<K, Collection<V>>, T> { + private final Function<? super K, ? extends Collection<? super V>> collectionFactory; + private final Function<? super T, ? extends V> valueSelector; + private final Function<? super T, ? extends K> keySelector; + + ToMultimapKeyValueSelector(Function<? super K, ? extends Collection<? super V>> collectionFactory, + Function<? super T, ? extends V> valueSelector, Function<? super T, ? extends K> keySelector) { + this.collectionFactory = collectionFactory; + this.valueSelector = valueSelector; + this.keySelector = keySelector; + } + + @SuppressWarnings("unchecked") + @Override + public void accept(Map<K, Collection<V>> m, T t) throws Throwable { + K key = keySelector.apply(t); + + Collection<V> coll = m.get(key); + if (coll == null) { + coll = (Collection<V>)collectionFactory.apply(key); + m.put(key, coll); + } + + V value = valueSelector.apply(t); + + coll.add(value); + } + } + + public static <T, K, V> BiConsumer<Map<K, Collection<V>>, T> toMultimapKeyValueSelector( + final Function<? super T, ? extends K> keySelector, final Function<? super T, ? extends V> valueSelector, + final Function<? super K, ? extends Collection<? super V>> collectionFactory) { + return new ToMultimapKeyValueSelector<>(collectionFactory, valueSelector, keySelector); + } + + enum NaturalComparator implements Comparator<Object> { + INSTANCE; + + @SuppressWarnings("unchecked") + @Override + public int compare(Object o1, Object o2) { + return ((Comparable<Object>)o1).compareTo(o2); + } + } + + @SuppressWarnings("unchecked") + public static <T> Comparator<T> naturalComparator() { + return (Comparator<T>)NaturalComparator.INSTANCE; + } + + static final class ListSorter<T> implements Function<List<T>, List<T>> { + final Comparator<? super T> comparator; + + ListSorter(Comparator<? super T> comparator) { + this.comparator = comparator; + } + + @Override + public List<T> apply(List<T> v) { + Collections.sort(v, comparator); + return v; + } + } + + public static <T> Function<List<T>, List<T>> listSorter(final Comparator<? super T> comparator) { + return new ListSorter<>(comparator); + } + + public static final Consumer<Subscription> REQUEST_MAX = new MaxRequestSubscription(); + + static final class Array2Func<T1, T2, R> implements Function<Object[], R> { + final BiFunction<? super T1, ? super T2, ? extends R> f; + + Array2Func(BiFunction<? super T1, ? super T2, ? extends R> f) { + this.f = f; + } + + @SuppressWarnings("unchecked") + @Override + public R apply(Object[] a) throws Throwable { + if (a.length != 2) { + throw new IllegalArgumentException("Array of size 2 expected but got " + a.length); + } + return f.apply((T1)a[0], (T2)a[1]); + } + } + + static final class Array3Func<T1, T2, T3, R> implements Function<Object[], R> { + final Function3<T1, T2, T3, R> f; + + Array3Func(Function3<T1, T2, T3, R> f) { + this.f = f; + } + + @SuppressWarnings("unchecked") + @Override + public R apply(Object[] a) throws Throwable { + if (a.length != 3) { + throw new IllegalArgumentException("Array of size 3 expected but got " + a.length); + } + return f.apply((T1)a[0], (T2)a[1], (T3)a[2]); + } + } + + static final class Array4Func<T1, T2, T3, T4, R> implements Function<Object[], R> { + final Function4<T1, T2, T3, T4, R> f; + + Array4Func(Function4<T1, T2, T3, T4, R> f) { + this.f = f; + } + + @SuppressWarnings("unchecked") + @Override + public R apply(Object[] a) throws Throwable { + if (a.length != 4) { + throw new IllegalArgumentException("Array of size 4 expected but got " + a.length); + } + return f.apply((T1)a[0], (T2)a[1], (T3)a[2], (T4)a[3]); + } + } + + static final class Array5Func<T1, T2, T3, T4, T5, R> implements Function<Object[], R> { + private final Function5<T1, T2, T3, T4, T5, R> f; + + Array5Func(Function5<T1, T2, T3, T4, T5, R> f) { + this.f = f; + } + + @SuppressWarnings("unchecked") + @Override + public R apply(Object[] a) throws Throwable { + if (a.length != 5) { + throw new IllegalArgumentException("Array of size 5 expected but got " + a.length); + } + return f.apply((T1)a[0], (T2)a[1], (T3)a[2], (T4)a[3], (T5)a[4]); + } + } + + static final class Array6Func<T1, T2, T3, T4, T5, T6, R> implements Function<Object[], R> { + final Function6<T1, T2, T3, T4, T5, T6, R> f; + + Array6Func(Function6<T1, T2, T3, T4, T5, T6, R> f) { + this.f = f; + } + + @SuppressWarnings("unchecked") + @Override + public R apply(Object[] a) throws Throwable { + if (a.length != 6) { + throw new IllegalArgumentException("Array of size 6 expected but got " + a.length); + } + return f.apply((T1)a[0], (T2)a[1], (T3)a[2], (T4)a[3], (T5)a[4], (T6)a[5]); + } + } + + static final class Array7Func<T1, T2, T3, T4, T5, T6, T7, R> implements Function<Object[], R> { + final Function7<T1, T2, T3, T4, T5, T6, T7, R> f; + + Array7Func(Function7<T1, T2, T3, T4, T5, T6, T7, R> f) { + this.f = f; + } + + @SuppressWarnings("unchecked") + @Override + public R apply(Object[] a) throws Throwable { + if (a.length != 7) { + throw new IllegalArgumentException("Array of size 7 expected but got " + a.length); + } + return f.apply((T1)a[0], (T2)a[1], (T3)a[2], (T4)a[3], (T5)a[4], (T6)a[5], (T7)a[6]); + } + } + + static final class Array8Func<T1, T2, T3, T4, T5, T6, T7, T8, R> implements Function<Object[], R> { + final Function8<T1, T2, T3, T4, T5, T6, T7, T8, R> f; + + Array8Func(Function8<T1, T2, T3, T4, T5, T6, T7, T8, R> f) { + this.f = f; + } + + @SuppressWarnings("unchecked") + @Override + public R apply(Object[] a) throws Throwable { + if (a.length != 8) { + throw new IllegalArgumentException("Array of size 8 expected but got " + a.length); + } + return f.apply((T1)a[0], (T2)a[1], (T3)a[2], (T4)a[3], (T5)a[4], (T6)a[5], (T7)a[6], (T8)a[7]); + } + } + + static final class Array9Func<T1, T2, T3, T4, T5, T6, T7, T8, T9, R> implements Function<Object[], R> { + final Function9<T1, T2, T3, T4, T5, T6, T7, T8, T9, R> f; + + Array9Func(Function9<T1, T2, T3, T4, T5, T6, T7, T8, T9, R> f) { + this.f = f; + } + + @SuppressWarnings("unchecked") + @Override + public R apply(Object[] a) throws Throwable { + if (a.length != 9) { + throw new IllegalArgumentException("Array of size 9 expected but got " + a.length); + } + return f.apply((T1)a[0], (T2)a[1], (T3)a[2], (T4)a[3], (T5)a[4], (T6)a[5], (T7)a[6], (T8)a[7], (T9)a[8]); + } + } + + static final class Identity implements Function<Object, Object> { + @Override + public Object apply(Object v) { + return v; + } + + @Override + public String toString() { + return "IdentityFunction"; + } + } + + static final class EmptyRunnable implements Runnable { + @Override + public void run() { } + + @Override + public String toString() { + return "EmptyRunnable"; + } + } + + static final class EmptyAction implements Action { + @Override + public void run() { } + + @Override + public String toString() { + return "EmptyAction"; + } + } + + static final class EmptyConsumer implements Consumer<Object> { + @Override + public void accept(Object v) { } + + @Override + public String toString() { + return "EmptyConsumer"; + } + } + + static final class ErrorConsumer implements Consumer<Throwable> { + @Override + public void accept(Throwable error) { + RxJavaPlugins.onError(error); + } + } + + static final class OnErrorMissingConsumer implements Consumer<Throwable> { + @Override + public void accept(Throwable error) { + RxJavaPlugins.onError(new OnErrorNotImplementedException(error)); + } + } + + static final class EmptyLongConsumer implements LongConsumer { + @Override + public void accept(long v) { } + } + + static final class TruePredicate implements Predicate<Object> { + @Override + public boolean test(Object o) { + return true; + } + } + + static final class FalsePredicate implements Predicate<Object> { + @Override + public boolean test(Object o) { + return false; + } + } + + static final class NullProvider implements Supplier<Object> { + @Override + public Object get() { + return null; + } + } + + static final class MaxRequestSubscription implements Consumer<Subscription> { + @Override + public void accept(Subscription t) { + t.request(Long.MAX_VALUE); + } + } + + @SuppressWarnings("unchecked") + public static <T> Consumer<T> boundedConsumer(int bufferSize) { + return (Consumer<T>) new BoundedConsumer(bufferSize); + } + + public static class BoundedConsumer implements Consumer<Subscription> { + + final int bufferSize; + + BoundedConsumer(int bufferSize) { + this.bufferSize = bufferSize; + } + + @Override + public void accept(Subscription s) { + s.request(bufferSize); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/functions/ObjectHelper.java b/src/main/java/io/reactivex/rxjava3/internal/functions/ObjectHelper.java new file mode 100644 index 0000000000..5bbdf22ad3 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/functions/ObjectHelper.java @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.functions; + +import io.reactivex.rxjava3.functions.BiPredicate; +import java.util.Objects; + +/** + * Utility methods containing the backport of Java 7's Objects utility class. + * <p>Named as such to avoid clash with java.util.Objects. + */ +public final class ObjectHelper { + + /** Utility class. */ + private ObjectHelper() { + throw new IllegalStateException("No instances!"); + } + + static final BiPredicate<Object, Object> EQUALS = new BiObjectPredicate(); + + /** + * Returns a BiPredicate that compares its parameters via Objects.equals(). + * @param <T> the value type + * @return the bi-predicate instance + */ + @SuppressWarnings("unchecked") + public static <T> BiPredicate<T, T> equalsPredicate() { + return (BiPredicate<T, T>)EQUALS; + } + + /** + * Validate that the given value is positive or report an IllegalArgumentException with + * the parameter name. + * @param value the value to validate + * @param paramName the parameter name of the value + * @return value + * @throws IllegalArgumentException if bufferSize <= 0 + */ + public static int verifyPositive(int value, String paramName) { + if (value <= 0) { + throw new IllegalArgumentException(paramName + " > 0 required but it was " + value); + } + return value; + } + + /** + * Validate that the given value is positive or report an IllegalArgumentException with + * the parameter name. + * @param value the value to validate + * @param paramName the parameter name of the value + * @return value + * @throws IllegalArgumentException if bufferSize <= 0 + */ + public static long verifyPositive(long value, String paramName) { + if (value <= 0L) { + throw new IllegalArgumentException(paramName + " > 0 required but it was " + value); + } + return value; + } + + static final class BiObjectPredicate implements BiPredicate<Object, Object> { + @Override + public boolean test(Object o1, Object o2) { + return Objects.equals(o1, o2); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/fuseable/AbstractEmptyQueueFuseable.java b/src/main/java/io/reactivex/rxjava3/internal/fuseable/AbstractEmptyQueueFuseable.java new file mode 100644 index 0000000000..13732bd426 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/fuseable/AbstractEmptyQueueFuseable.java @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.fuseable; + +import io.reactivex.rxjava3.annotations.NonNull; +import io.reactivex.rxjava3.operators.QueueDisposable; +import io.reactivex.rxjava3.operators.QueueFuseable; +import io.reactivex.rxjava3.operators.QueueSubscription; + +/** + * Represents an empty, async-only {@link QueueFuseable} instance. + * + * @param <T> the output value type + * @since 3.0.0 + */ +public abstract class AbstractEmptyQueueFuseable<T> +implements QueueSubscription<T>, QueueDisposable<T> { + + @Override + public final int requestFusion(int mode) { + return mode & ASYNC; + } + + @Override + public final boolean offer(@NonNull T value) { + throw new UnsupportedOperationException("Should not be called!"); + } + + @Override + public final boolean offer(@NonNull T v1, @NonNull T v2) { + throw new UnsupportedOperationException("Should not be called!"); + } + + @Override + public final T poll() throws Throwable { + return null; // always empty + } + + @Override + public final boolean isEmpty() { + return true; // always empty + } + + @Override + public final void clear() { + // always empty + } + + @Override + public final void request(long n) { + // no items to request + } + + @Override + public void cancel() { + // default No-op + } + + @Override + public void dispose() { + // default No-op + } + + @Override + public boolean isDisposed() { + return false; + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/fuseable/CancellableQueueFuseable.java b/src/main/java/io/reactivex/rxjava3/internal/fuseable/CancellableQueueFuseable.java new file mode 100644 index 0000000000..71f993e226 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/fuseable/CancellableQueueFuseable.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.fuseable; + +import io.reactivex.rxjava3.operators.QueueFuseable; + +/** + * Represents an empty, async-only {@link QueueFuseable} instance that tracks and exposes a + * canceled/disposed state. + * + * @param <T> the output value type + * @since 3.0.0 + */ +public final class CancellableQueueFuseable<T> +extends AbstractEmptyQueueFuseable<T> { + + volatile boolean disposed; + + @Override + public void cancel() { + disposed = true; + } + + @Override + public void dispose() { + disposed = true; + } + + @Override + public boolean isDisposed() { + return disposed; + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/fuseable/FuseToFlowable.java b/src/main/java/io/reactivex/rxjava3/internal/fuseable/FuseToFlowable.java new file mode 100644 index 0000000000..fe70b40a01 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/fuseable/FuseToFlowable.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.fuseable; + +import io.reactivex.rxjava3.annotations.NonNull; +import io.reactivex.rxjava3.core.Flowable; + +/** + * Interface indicating a operator implementation can be macro-fused back to Flowable in case + * the operator goes from Flowable to some other reactive type and then the sequence calls + * for toFlowable again: + * <pre> + * {@code + * Single<Integer> single = Flowable.range(1, 10).reduce((a, b) -> a + b); + * Flowable<Integer> flowable = single.toFlowable(); + * } + * </pre> + * + * The {@code Single.toFlowable()} will check for this interface and call the {@link #fuseToFlowable()} + * to return a Flowable which could be the Flowable-specific implementation of reduce(BiFunction). + * <p> + * This causes a slight overhead in assembly time (1 instanceof check, 1 operator allocation and 1 dropped + * operator) but does not incur the conversion overhead at runtime. + * + * @param <T> the value type + */ +public interface FuseToFlowable<@NonNull T> { + + /** + * Returns a (direct) Flowable for the operator. + * <p>The implementation should handle the necessary RxJavaPlugins wrapping. + * @return the Flowable instance + */ + @NonNull + Flowable<T> fuseToFlowable(); +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/fuseable/FuseToMaybe.java b/src/main/java/io/reactivex/rxjava3/internal/fuseable/FuseToMaybe.java new file mode 100644 index 0000000000..13ccd6be75 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/fuseable/FuseToMaybe.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.fuseable; + +import io.reactivex.rxjava3.annotations.NonNull; +import io.reactivex.rxjava3.core.Maybe; + +/** + * Interface indicating an operator implementation can be macro-fused back to Maybe in case + * the operator goes from Maybe to some other reactive type and then the sequence calls + * for toMaybe again: + * <pre> + * {@code + * Single<Integer> single = Maybe.just(1).isEmpty(); + * Maybe<Integer> maybe = single.toMaybe(); + * } + * </pre> + * + * The {@code Single.toMaybe()} will check for this interface and call the {@link #fuseToMaybe()} + * to return a Maybe which could be the Maybe-specific implementation of isEmpty(). + * <p> + * This causes a slight overhead in assembly time (1 instanceof check, 1 operator allocation and 1 dropped + * operator) but does not incur the conversion overhead at runtime. + * + * @param <T> the value type + */ +public interface FuseToMaybe<@NonNull T> { + + /** + * Returns a (direct) Maybe for the operator. + * <p>The implementation should handle the necessary RxJavaPlugins wrapping. + * @return the Maybe instance + */ + @NonNull + Maybe<T> fuseToMaybe(); +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/fuseable/FuseToObservable.java b/src/main/java/io/reactivex/rxjava3/internal/fuseable/FuseToObservable.java new file mode 100644 index 0000000000..d7ae761e37 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/fuseable/FuseToObservable.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.fuseable; + +import io.reactivex.rxjava3.annotations.NonNull; +import io.reactivex.rxjava3.core.Observable; + +/** + * Interface indicating a operator implementation can be macro-fused back to Observable in case + * the operator goes from Observable to some other reactive type and then the sequence calls + * for toObservable again: + * <pre> + * {@code + * Single<Integer> single = Observable.range(1, 10).reduce((a, b) -> a + b); + * Observable<Integer> observable = single.toObservable(); + * } + * </pre> + * + * The {@code Single.toObservable()} will check for this interface and call the {@link #fuseToObservable()} + * to return an Observable which could be the Observable-specific implementation of reduce(BiFunction). + * <p> + * This causes a slight overhead in assembly time (1 instanceof check, 1 operator allocation and 1 dropped + * operator) but does not incur the conversion overhead at runtime. + * + * @param <T> the value type + */ +public interface FuseToObservable<@NonNull T> { + + /** + * Returns a (direct) Observable for the operator. + * <p>The implementation should handle the necessary RxJavaPlugins wrapping. + * @return the Observable instance + */ + @NonNull + Observable<T> fuseToObservable(); +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/fuseable/HasUpstreamCompletableSource.java b/src/main/java/io/reactivex/rxjava3/internal/fuseable/HasUpstreamCompletableSource.java new file mode 100644 index 0000000000..fddb14ef0b --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/fuseable/HasUpstreamCompletableSource.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.fuseable; + +import io.reactivex.rxjava3.annotations.NonNull; +import io.reactivex.rxjava3.core.CompletableSource; + +/** + * Interface indicating the implementor has an upstream CompletableSource-like source available + * via {@link #source()} method. + */ +public interface HasUpstreamCompletableSource { + /** + * Returns the upstream source of this Completable. + * <p>Allows discovering the chain of observables. + * @return the source CompletableSource + */ + @NonNull + CompletableSource source(); +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/fuseable/HasUpstreamMaybeSource.java b/src/main/java/io/reactivex/rxjava3/internal/fuseable/HasUpstreamMaybeSource.java new file mode 100644 index 0000000000..121e2937e4 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/fuseable/HasUpstreamMaybeSource.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.fuseable; + +import io.reactivex.rxjava3.annotations.NonNull; +import io.reactivex.rxjava3.core.MaybeSource; + +/** + * Interface indicating the implementor has an upstream MaybeSource-like source available + * via {@link #source()} method. + * + * @param <T> the value type + */ +public interface HasUpstreamMaybeSource<@NonNull T> { + /** + * Returns the upstream source of this Maybe. + * <p>Allows discovering the chain of observables. + * @return the source MaybeSource + */ + @NonNull + MaybeSource<T> source(); +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/fuseable/HasUpstreamObservableSource.java b/src/main/java/io/reactivex/rxjava3/internal/fuseable/HasUpstreamObservableSource.java new file mode 100644 index 0000000000..1a1e878b65 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/fuseable/HasUpstreamObservableSource.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.fuseable; + +import io.reactivex.rxjava3.annotations.NonNull; +import io.reactivex.rxjava3.core.ObservableSource; + +/** + * Interface indicating the implementor has an upstream ObservableSource-like source available + * via {@link #source()} method. + * + * @param <T> the value type + */ +public interface HasUpstreamObservableSource<@NonNull T> { + /** + * Returns the upstream source of this Observable. + * <p>Allows discovering the chain of observables. + * @return the source ObservableSource + */ + @NonNull + ObservableSource<T> source(); +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/fuseable/HasUpstreamPublisher.java b/src/main/java/io/reactivex/rxjava3/internal/fuseable/HasUpstreamPublisher.java new file mode 100644 index 0000000000..5a46f38555 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/fuseable/HasUpstreamPublisher.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.fuseable; + +import org.reactivestreams.Publisher; + +import io.reactivex.rxjava3.annotations.NonNull; + +/** + * Interface indicating the implementor has an upstream Publisher-like source available + * via {@link #source()} method. + * + * @param <T> the value type + */ +public interface HasUpstreamPublisher<@NonNull T> { + /** + * Returns the source Publisher. + * <p> + * This method is intended to discover the assembly + * graph of sequences. + * @return the source Publisher + */ + @NonNull + Publisher<T> source(); +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/fuseable/HasUpstreamSingleSource.java b/src/main/java/io/reactivex/rxjava3/internal/fuseable/HasUpstreamSingleSource.java new file mode 100644 index 0000000000..ef833ae2ff --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/fuseable/HasUpstreamSingleSource.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.fuseable; + +import io.reactivex.rxjava3.annotations.NonNull; +import io.reactivex.rxjava3.core.SingleSource; + +/** + * Interface indicating the implementor has an upstream SingleSource-like source available + * via {@link #source()} method. + * + * @param <T> the value type + */ +public interface HasUpstreamSingleSource<@NonNull T> { + /** + * Returns the upstream source of this Single. + * <p>Allows discovering the chain of observables. + * @return the source SingleSource + */ + @NonNull + SingleSource<T> source(); +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/fuseable/package-info.java b/src/main/java/io/reactivex/rxjava3/internal/fuseable/package-info.java new file mode 100644 index 0000000000..339eabbfd2 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/fuseable/package-info.java @@ -0,0 +1,17 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +/** + * Base interfaces and types for supporting operator-fusion. + */ +package io.reactivex.rxjava3.internal.fuseable; diff --git a/src/main/java/io/reactivex/rxjava3/internal/jdk8/CompletableFromCompletionStage.java b/src/main/java/io/reactivex/rxjava3/internal/jdk8/CompletableFromCompletionStage.java new file mode 100644 index 0000000000..639516d005 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/jdk8/CompletableFromCompletionStage.java @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.jdk8; + +import java.util.concurrent.CompletionStage; +import java.util.function.BiConsumer; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.jdk8.FlowableFromCompletionStage.BiConsumerAtomicReference; + +/** + * Wrap a CompletionStage and signal its outcome. + * @param <T> the element type of the CompletionsStage + * @since 3.0.0 + */ +public final class CompletableFromCompletionStage<T> extends Completable { + + final CompletionStage<T> stage; + + public CompletableFromCompletionStage(CompletionStage<T> stage) { + this.stage = stage; + } + + @Override + protected void subscribeActual(CompletableObserver observer) { + // We need an indirection because one can't detach from a whenComplete + // and cancellation should not hold onto the stage. + BiConsumerAtomicReference<Object> whenReference = new BiConsumerAtomicReference<>(); + CompletionStageHandler<Object> handler = new CompletionStageHandler<>(observer, whenReference); + whenReference.lazySet(handler); + + observer.onSubscribe(handler); + stage.whenComplete(whenReference); + } + + static final class CompletionStageHandler<T> + implements Disposable, BiConsumer<T, Throwable> { + + final CompletableObserver downstream; + + final BiConsumerAtomicReference<T> whenReference; + + CompletionStageHandler(CompletableObserver downstream, BiConsumerAtomicReference<T> whenReference) { + this.downstream = downstream; + this.whenReference = whenReference; + } + + @Override + public void accept(T item, Throwable error) { + if (error != null) { + downstream.onError(error); + } else { + downstream.onComplete(); + } + } + + @Override + public void dispose() { + whenReference.set(null); + } + + @Override + public boolean isDisposed() { + return whenReference.get() == null; + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/jdk8/CompletionStageConsumer.java b/src/main/java/io/reactivex/rxjava3/internal/jdk8/CompletionStageConsumer.java new file mode 100644 index 0000000000..2410290734 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/jdk8/CompletionStageConsumer.java @@ -0,0 +1,100 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.jdk8; + +import java.util.NoSuchElementException; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicReference; + +import io.reactivex.rxjava3.annotations.NonNull; +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +/** + * Class that extends CompletableFuture and converts multiple types of reactive consumers + * and their signals into completion signals. + * @param <T> the element type + * @since 3.0.0 + */ +public final class CompletionStageConsumer<T> extends CompletableFuture<T> +implements MaybeObserver<T>, SingleObserver<T>, CompletableObserver { + + final AtomicReference<Disposable> upstream; + + final boolean hasDefault; + + final T defaultItem; + + public CompletionStageConsumer(boolean hasDefault, T defaultItem) { + this.hasDefault = hasDefault; + this.defaultItem = defaultItem; + this.upstream = new AtomicReference<>(); + } + + @Override + public void onSubscribe(@NonNull Disposable d) { + DisposableHelper.setOnce(upstream, d); + } + + @Override + public void onSuccess(@NonNull T t) { + clear(); + complete(t); + } + + @Override + public void onError(Throwable t) { + clear(); + if (!completeExceptionally(t)) { + RxJavaPlugins.onError(t); + } + } + + @Override + public void onComplete() { + if (hasDefault) { + complete(defaultItem); + } else { + completeExceptionally(new NoSuchElementException("The source was empty")); + } + } + + void cancelUpstream() { + DisposableHelper.dispose(upstream); + } + + void clear() { + upstream.lazySet(DisposableHelper.DISPOSED); + } + + @Override + public boolean cancel(boolean mayInterruptIfRunning) { + cancelUpstream(); + return super.cancel(mayInterruptIfRunning); + } + + @Override + public boolean complete(T value) { + cancelUpstream(); + return super.complete(value); + } + + @Override + public boolean completeExceptionally(Throwable ex) { + cancelUpstream(); + return super.completeExceptionally(ex); + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/jdk8/FlowableCollectWithCollector.java b/src/main/java/io/reactivex/rxjava3/internal/jdk8/FlowableCollectWithCollector.java new file mode 100644 index 0000000000..63ca949295 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/jdk8/FlowableCollectWithCollector.java @@ -0,0 +1,154 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.jdk8; + +import java.util.Objects; +import java.util.function.*; +import java.util.stream.Collector; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.annotations.NonNull; +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.internal.subscriptions.*; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +/** + * Collect items into a container defined by a Stream {@link Collector} callback set. + * + * @param <T> the upstream value type + * @param <A> the intermediate accumulator type + * @param <R> the result type + * @since 3.0.0 + */ +public final class FlowableCollectWithCollector<T, A, R> extends Flowable<R> { + + final Flowable<T> source; + + final Collector<? super T, A, R> collector; + + public FlowableCollectWithCollector(Flowable<T> source, Collector<? super T, A, R> collector) { + this.source = source; + this.collector = collector; + } + + @Override + protected void subscribeActual(@NonNull Subscriber<? super R> s) { + A container; + BiConsumer<A, ? super T> accumulator; + Function<A, R> finisher; + + try { + container = collector.supplier().get(); + accumulator = collector.accumulator(); + finisher = collector.finisher(); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + EmptySubscription.error(ex, s); + return; + } + + source.subscribe(new CollectorSubscriber<>(s, container, accumulator, finisher)); + } + + static final class CollectorSubscriber<T, A, R> + extends DeferredScalarSubscription<R> + implements FlowableSubscriber<T> { + + private static final long serialVersionUID = -229544830565448758L; + + final BiConsumer<A, T> accumulator; + + final Function<A, R> finisher; + + Subscription upstream; + + boolean done; + + A container; + + CollectorSubscriber(Subscriber<? super R> downstream, A container, BiConsumer<A, T> accumulator, Function<A, R> finisher) { + super(downstream); + this.container = container; + this.accumulator = accumulator; + this.finisher = finisher; + } + + @Override + public void onSubscribe(@NonNull Subscription s) { + if (SubscriptionHelper.validate(this.upstream, s)) { + this.upstream = s; + + downstream.onSubscribe(this); + + s.request(Long.MAX_VALUE); + } + } + + @Override + public void onNext(T t) { + if (done) { + return; + } + try { + accumulator.accept(container, t); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + upstream.cancel(); + onError(ex); + } + } + + @Override + public void onError(Throwable t) { + if (done) { + RxJavaPlugins.onError(t); + } else { + done = true; + upstream = SubscriptionHelper.CANCELLED; + this.container = null; + downstream.onError(t); + } + } + + @Override + public void onComplete() { + if (done) { + return; + } + + done = true; + upstream = SubscriptionHelper.CANCELLED; + A container = this.container; + this.container = null; + R result; + try { + result = Objects.requireNonNull(finisher.apply(container), "The finisher returned a null value"); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + downstream.onError(ex); + return; + } + + complete(result); + } + + @Override + public void cancel() { + super.cancel(); + upstream.cancel(); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/jdk8/FlowableCollectWithCollectorSingle.java b/src/main/java/io/reactivex/rxjava3/internal/jdk8/FlowableCollectWithCollectorSingle.java new file mode 100644 index 0000000000..aba390e1da --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/jdk8/FlowableCollectWithCollectorSingle.java @@ -0,0 +1,165 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.jdk8; + +import java.util.Objects; +import java.util.function.*; +import java.util.stream.Collector; + +import org.reactivestreams.Subscription; + +import io.reactivex.rxjava3.annotations.NonNull; +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.internal.disposables.EmptyDisposable; +import io.reactivex.rxjava3.internal.fuseable.FuseToFlowable; +import io.reactivex.rxjava3.internal.subscriptions.SubscriptionHelper; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +/** + * Collect items into a container defined by a Stream {@link Collector} callback set. + * + * @param <T> the upstream value type + * @param <A> the intermediate accumulator type + * @param <R> the result type + * @since 3.0.0 + */ +public final class FlowableCollectWithCollectorSingle<T, A, R> extends Single<R> implements FuseToFlowable<R> { + + final Flowable<T> source; + + final Collector<? super T, A, R> collector; + + public FlowableCollectWithCollectorSingle(Flowable<T> source, Collector<? super T, A, R> collector) { + this.source = source; + this.collector = collector; + } + + @Override + public Flowable<R> fuseToFlowable() { + return new FlowableCollectWithCollector<>(source, collector); + } + + @Override + protected void subscribeActual(@NonNull SingleObserver<? super R> observer) { + A container; + BiConsumer<A, ? super T> accumulator; + Function<A, R> finisher; + + try { + container = collector.supplier().get(); + accumulator = collector.accumulator(); + finisher = collector.finisher(); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + EmptyDisposable.error(ex, observer); + return; + } + + source.subscribe(new CollectorSingleObserver<>(observer, container, accumulator, finisher)); + } + + static final class CollectorSingleObserver<T, A, R> implements FlowableSubscriber<T>, Disposable { + + final SingleObserver<? super R> downstream; + + final BiConsumer<A, T> accumulator; + + final Function<A, R> finisher; + + Subscription upstream; + + boolean done; + + A container; + + CollectorSingleObserver(SingleObserver<? super R> downstream, A container, BiConsumer<A, T> accumulator, Function<A, R> finisher) { + this.downstream = downstream; + this.container = container; + this.accumulator = accumulator; + this.finisher = finisher; + } + + @Override + public void onSubscribe(@NonNull Subscription s) { + if (SubscriptionHelper.validate(this.upstream, s)) { + this.upstream = s; + + downstream.onSubscribe(this); + + s.request(Long.MAX_VALUE); + } + } + + @Override + public void onNext(T t) { + if (done) { + return; + } + try { + accumulator.accept(container, t); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + upstream.cancel(); + onError(ex); + } + } + + @Override + public void onError(Throwable t) { + if (done) { + RxJavaPlugins.onError(t); + } else { + done = true; + upstream = SubscriptionHelper.CANCELLED; + this.container = null; + downstream.onError(t); + } + } + + @Override + public void onComplete() { + if (done) { + return; + } + + done = true; + upstream = SubscriptionHelper.CANCELLED; + A container = this.container; + this.container = null; + R result; + try { + result = Objects.requireNonNull(finisher.apply(container), "The finisher returned a null value"); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + downstream.onError(ex); + return; + } + + downstream.onSuccess(result); + } + + @Override + public void dispose() { + upstream.cancel(); + upstream = SubscriptionHelper.CANCELLED; + } + + @Override + public boolean isDisposed() { + return upstream == SubscriptionHelper.CANCELLED; + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/jdk8/FlowableFirstStageSubscriber.java b/src/main/java/io/reactivex/rxjava3/internal/jdk8/FlowableFirstStageSubscriber.java new file mode 100644 index 0000000000..19bfcbff6d --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/jdk8/FlowableFirstStageSubscriber.java @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.jdk8; + +import java.util.NoSuchElementException; + +import org.reactivestreams.Subscription; + +/** + * Signals the first element of the source via the underlying CompletableFuture, + * signals the a default item if the upstream is empty or signals {@link NoSuchElementException}. + * + * @param <T> the element type + * @since 3.0.0 + */ +public final class FlowableFirstStageSubscriber<T> extends FlowableStageSubscriber<T> { + + final boolean hasDefault; + + final T defaultItem; + + public FlowableFirstStageSubscriber(boolean hasDefault, T defaultItem) { + this.hasDefault = hasDefault; + this.defaultItem = defaultItem; + } + + @Override + public void onNext(T t) { + complete(t); + } + + @Override + public void onComplete() { + if (!isDone()) { + clear(); + if (hasDefault) { + complete(defaultItem); + } else { + completeExceptionally(new NoSuchElementException()); + } + } + } + + @Override + protected void afterSubscribe(Subscription s) { + s.request(1); + } + +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/jdk8/FlowableFlatMapStream.java b/src/main/java/io/reactivex/rxjava3/internal/jdk8/FlowableFlatMapStream.java new file mode 100644 index 0000000000..48ea8b6e5f --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/jdk8/FlowableFlatMapStream.java @@ -0,0 +1,353 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.jdk8; + +import java.util.*; +import java.util.concurrent.atomic.*; +import java.util.stream.Stream; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.annotations.NonNull; +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.subscriptions.*; +import io.reactivex.rxjava3.internal.util.*; +import io.reactivex.rxjava3.operators.QueueFuseable; +import io.reactivex.rxjava3.operators.QueueSubscription; +import io.reactivex.rxjava3.operators.SimpleQueue; +import io.reactivex.rxjava3.operators.SpscArrayQueue; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +/** + * Maps the upstream values onto {@link Stream}s and emits their items in order to the downstream. + * + * @param <T> the upstream element type + * @param <R> the inner {@code Stream} and result element type + * @since 3.0.0 + */ +public final class FlowableFlatMapStream<T, R> extends Flowable<R> { + + final Flowable<T> source; + + final Function<? super T, ? extends Stream<? extends R>> mapper; + + final int prefetch; + + public FlowableFlatMapStream(Flowable<T> source, Function<? super T, ? extends Stream<? extends R>> mapper, int prefetch) { + this.source = source; + this.mapper = mapper; + this.prefetch = prefetch; + } + + @Override + protected void subscribeActual(Subscriber<? super R> s) { + if (source instanceof Supplier) { + Stream<? extends R> stream = null; + try { + @SuppressWarnings("unchecked") + T t = ((Supplier<T>)source).get(); + if (t != null) { + stream = Objects.requireNonNull(mapper.apply(t), "The mapper returned a null Stream"); + } + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + EmptySubscription.error(ex, s); + return; + } + + if (stream != null) { + FlowableFromStream.subscribeStream(s, stream); + } else { + EmptySubscription.complete(s); + } + } else { + source.subscribe(subscribe(s, mapper, prefetch)); + } + } + + /** + * Create a {@link Subscriber} with the given parameters. + * @param <T> the upstream value type + * @param <R> the {@link Stream} and output value type + * @param downstream the downstream {@code Subscriber} to wrap + * @param mapper the mapper function + * @param prefetch the number of items to prefetch + * @return the new {@code Subscriber} + */ + public static <T, R> Subscriber<T> subscribe(Subscriber<? super R> downstream, Function<? super T, ? extends Stream<? extends R>> mapper, int prefetch) { + return new FlatMapStreamSubscriber<>(downstream, mapper, prefetch); + } + + static final class FlatMapStreamSubscriber<T, R> extends AtomicInteger + implements FlowableSubscriber<T>, Subscription { + + private static final long serialVersionUID = -5127032662980523968L; + + final Subscriber<? super R> downstream; + + final Function<? super T, ? extends Stream<? extends R>> mapper; + + final int prefetch; + + final AtomicLong requested; + + SimpleQueue<T> queue; + + Subscription upstream; + + Iterator<? extends R> currentIterator; + + AutoCloseable currentCloseable; + + volatile boolean cancelled; + + volatile boolean upstreamDone; + final AtomicThrowable error; + + long emitted; + + int consumed; + + int sourceMode; + + FlatMapStreamSubscriber(Subscriber<? super R> downstream, Function<? super T, ? extends Stream<? extends R>> mapper, int prefetch) { + this.downstream = downstream; + this.mapper = mapper; + this.prefetch = prefetch; + this.requested = new AtomicLong(); + this.error = new AtomicThrowable(); + } + + @Override + public void onSubscribe(@NonNull Subscription s) { + if (SubscriptionHelper.validate(this.upstream, s)) { + this.upstream = s; + + if (s instanceof QueueSubscription) { + + @SuppressWarnings("unchecked") + QueueSubscription<T> qs = (QueueSubscription<T>)s; + + int m = qs.requestFusion(QueueFuseable.ANY | QueueFuseable.BOUNDARY); + if (m == QueueFuseable.SYNC) { + sourceMode = m; + queue = qs; + upstreamDone = true; + + downstream.onSubscribe(this); + return; + } + else if (m == QueueFuseable.ASYNC) { + sourceMode = m; + queue = qs; + + downstream.onSubscribe(this); + + s.request(prefetch); + return; + } + } + + queue = new SpscArrayQueue<>(prefetch); + + downstream.onSubscribe(this); + + s.request(prefetch); + } + } + + @Override + public void onNext(T t) { + if (sourceMode != QueueFuseable.ASYNC) { + if (!queue.offer(t)) { + upstream.cancel(); + onError(new QueueOverflowException()); + return; + } + } + drain(); + } + + @Override + public void onError(Throwable t) { + if (error.compareAndSet(null, t)) { + upstreamDone = true; + drain(); + } else { + RxJavaPlugins.onError(t); + } + } + + @Override + public void onComplete() { + upstreamDone = true; + drain(); + } + + @Override + public void request(long n) { + if (SubscriptionHelper.validate(n)) { + BackpressureHelper.add(requested, n); + drain(); + } + } + + @Override + public void cancel() { + cancelled = true; + upstream.cancel(); + drain(); + } + + void drain() { + if (getAndIncrement() != 0) { + return; + } + + int missed = 1; + + final Subscriber<? super R> downstream = this.downstream; + final SimpleQueue<T> queue = this.queue; + final AtomicThrowable error = this.error; + Iterator<? extends R> iterator = this.currentIterator; + long requested = this.requested.get(); + long emitted = this.emitted; + final int limit = prefetch - (prefetch >> 2); + boolean canRequest = sourceMode != QueueFuseable.SYNC; + + for (;;) { + if (cancelled) { + queue.clear(); + clearCurrentSuppressCloseError(); + } else { + boolean isDone = upstreamDone; + if (error.get() != null) { + downstream.onError(error.get()); + cancelled = true; + continue; + } + + if (iterator == null) { + T t; + + try { + t = queue.poll(); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + trySignalError(downstream, ex); + continue; + } + + boolean isEmpty = t == null; + + if (isDone && isEmpty) { + downstream.onComplete(); + cancelled = true; + } + else if (!isEmpty) { + if (canRequest && ++consumed == limit) { + consumed = 0; + upstream.request(limit); + } + + Stream<? extends R> stream; + try { + stream = Objects.requireNonNull(mapper.apply(t), "The mapper returned a null Stream"); + iterator = stream.iterator(); + + if (iterator.hasNext()) { + currentIterator = iterator; + currentCloseable = stream; + } else { + iterator = null; + } + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + trySignalError(downstream, ex); + } + continue; + } + } + if (iterator != null && emitted != requested) { + R item; + + try { + item = Objects.requireNonNull(iterator.next(), "The Stream.Iterator returned a null value"); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + trySignalError(downstream, ex); + continue; + } + + if (!cancelled) { + downstream.onNext(item); + emitted++; + + if (!cancelled) { + try { + if (!iterator.hasNext()) { + iterator = null; + clearCurrentRethrowCloseError(); + } + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + trySignalError(downstream, ex); + } + } + } + + continue; + } + } + + this.emitted = emitted; + missed = addAndGet(-missed); + if (missed == 0) { + break; + } + requested = this.requested.get(); + } + } + + void clearCurrentRethrowCloseError() throws Throwable { + currentIterator = null; + AutoCloseable ac = currentCloseable; + currentCloseable = null; + if (ac != null) { + ac.close(); + } + } + + void clearCurrentSuppressCloseError() { + try { + clearCurrentRethrowCloseError(); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + RxJavaPlugins.onError(ex); + } + } + + void trySignalError(Subscriber<?> downstream, Throwable ex) { + if (error.compareAndSet(null, ex)) { + upstream.cancel(); + cancelled = true; + downstream.onError(ex); + } else { + RxJavaPlugins.onError(ex); + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/jdk8/FlowableFromCompletionStage.java b/src/main/java/io/reactivex/rxjava3/internal/jdk8/FlowableFromCompletionStage.java new file mode 100644 index 0000000000..1ee0f7786e --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/jdk8/FlowableFromCompletionStage.java @@ -0,0 +1,95 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.jdk8; + +import java.util.concurrent.CompletionStage; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.BiConsumer; + +import org.reactivestreams.Subscriber; + +import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.internal.subscriptions.DeferredScalarSubscription; + +/** + * Wrap a CompletionStage and signal its outcome. + * @param <T> the element type + * @since 3.0.0 + */ +public final class FlowableFromCompletionStage<T> extends Flowable<T> { + + final CompletionStage<T> stage; + + public FlowableFromCompletionStage(CompletionStage<T> stage) { + this.stage = stage; + } + + @Override + protected void subscribeActual(Subscriber<? super T> s) { + // We need an indirection because one can't detach from a whenComplete + // and cancellation should not hold onto the stage. + BiConsumerAtomicReference<T> whenReference = new BiConsumerAtomicReference<>(); + CompletionStageHandler<T> handler = new CompletionStageHandler<>(s, whenReference); + whenReference.lazySet(handler); + + s.onSubscribe(handler); + stage.whenComplete(whenReference); + } + + static final class CompletionStageHandler<T> + extends DeferredScalarSubscription<T> + implements BiConsumer<T, Throwable> { + + private static final long serialVersionUID = 4665335664328839859L; + + final BiConsumerAtomicReference<T> whenReference; + + CompletionStageHandler(Subscriber<? super T> downstream, BiConsumerAtomicReference<T> whenReference) { + super(downstream); + this.whenReference = whenReference; + } + + @Override + public void accept(T item, Throwable error) { + if (error != null) { + downstream.onError(error); + } + else if (item != null) { + complete(item); + } else { + downstream.onError(new NullPointerException("The CompletionStage terminated with null.")); + } + } + + @Override + public void cancel() { + super.cancel(); + whenReference.set(null); + } + } + + static final class BiConsumerAtomicReference<T> extends AtomicReference<BiConsumer<T, Throwable>> + implements BiConsumer<T, Throwable> { + + private static final long serialVersionUID = 45838553147237545L; + + @Override + public void accept(T t, Throwable u) { + BiConsumer<T, Throwable> biConsumer = get(); + if (biConsumer != null) { + biConsumer.accept(t, u); + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/jdk8/FlowableFromStream.java b/src/main/java/io/reactivex/rxjava3/internal/jdk8/FlowableFromStream.java new file mode 100644 index 0000000000..4fd3b4eb00 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/jdk8/FlowableFromStream.java @@ -0,0 +1,319 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.jdk8; + +import java.util.Iterator; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicLong; +import java.util.stream.Stream; + +import org.reactivestreams.Subscriber; + +import io.reactivex.rxjava3.annotations.*; +import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.internal.subscriptions.*; +import io.reactivex.rxjava3.internal.util.BackpressureHelper; +import io.reactivex.rxjava3.operators.ConditionalSubscriber; +import io.reactivex.rxjava3.operators.QueueSubscription; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +/** + * Wraps a {@link Stream} and emits its values as a Flowable sequence. + * @param <T> the element type of the Stream + * @since 3.0.0 + */ +public final class FlowableFromStream<T> extends Flowable<T> { + + final Stream<T> stream; + + public FlowableFromStream(Stream<T> stream) { + this.stream = stream; + } + + @Override + protected void subscribeActual(Subscriber<? super T> s) { + subscribeStream(s, stream); + } + + /** + * Subscribes to the Stream by picking the normal or conditional stream Subscription implementation. + * @param <T> the element type of the flow + * @param s the subscriber to drive + * @param stream the sequence to consume + */ + public static <T> void subscribeStream(Subscriber<? super T> s, Stream<T> stream) { + Iterator<T> iterator; + try { + iterator = stream.iterator(); + + if (!iterator.hasNext()) { + EmptySubscription.complete(s); + closeSafely(stream); + return; + } + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + EmptySubscription.error(ex, s); + closeSafely(stream); + return; + } + + if (s instanceof ConditionalSubscriber) { + s.onSubscribe(new StreamConditionalSubscription<>((ConditionalSubscriber<? super T>)s, iterator, stream)); + } else { + s.onSubscribe(new StreamSubscription<>(s, iterator, stream)); + } + } + + static void closeSafely(AutoCloseable c) { + try { + c.close(); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + RxJavaPlugins.onError(ex); + } + } + + abstract static class AbstractStreamSubscription<T> extends AtomicLong implements QueueSubscription<T> { + + private static final long serialVersionUID = -9082954702547571853L; + + Iterator<T> iterator; + + AutoCloseable closeable; + + volatile boolean cancelled; + + boolean once; + + AbstractStreamSubscription(Iterator<T> iterator, AutoCloseable closeable) { + this.iterator = iterator; + this.closeable = closeable; + } + + @Override + public void request(long n) { + if (SubscriptionHelper.validate(n)) { + if (BackpressureHelper.add(this, n) == 0L) { + run(n); + } + } + } + + abstract void run(long n); + + @Override + public void cancel() { + cancelled = true; + request(1L); + } + + @Override + public int requestFusion(int mode) { + if ((mode & SYNC) != 0) { + lazySet(Long.MAX_VALUE); + return SYNC; + } + return NONE; + } + + @Override + public boolean offer(@NonNull T value) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean offer(@NonNull T v1, @NonNull T v2) { + throw new UnsupportedOperationException(); + } + + @Nullable + @Override + public T poll() { + if (iterator == null) { + return null; + } + if (!once) { + once = true; + } else { + if (!iterator.hasNext()) { + clear(); + return null; + } + } + return Objects.requireNonNull(iterator.next(), "The Stream's Iterator.next() returned a null value"); + } + + @Override + public boolean isEmpty() { + Iterator<T> it = iterator; + if (it != null) { + if (!once || it.hasNext()) { + return false; + } + clear(); + } + return true; + } + + @Override + public void clear() { + iterator = null; + AutoCloseable c = closeable; + closeable = null; + if (c != null) { + closeSafely(c); + } + } + } + + static final class StreamSubscription<T> extends AbstractStreamSubscription<T> { + + private static final long serialVersionUID = -9082954702547571853L; + + final Subscriber<? super T> downstream; + + StreamSubscription(Subscriber<? super T> downstream, Iterator<T> iterator, AutoCloseable closeable) { + super(iterator, closeable); + this.downstream = downstream; + } + + @Override + public void run(long n) { + long emitted = 0L; + Iterator<T> iterator = this.iterator; + Subscriber<? super T> downstream = this.downstream; + + for (;;) { + + if (cancelled) { + clear(); + break; + } else { + T next; + try { + next = Objects.requireNonNull(iterator.next(), "The Stream's Iterator returned a null value"); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + downstream.onError(ex); + cancelled = true; + continue; + } + + downstream.onNext(next); + + if (cancelled) { + continue; + } + + try { + if (!iterator.hasNext()) { + downstream.onComplete(); + cancelled = true; + continue; + } + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + downstream.onError(ex); + cancelled = true; + continue; + } + + if (++emitted != n) { + continue; + } + } + + n = get(); + if (emitted == n) { + if (compareAndSet(n, 0L)) { + break; + } + n = get(); + } + } + } + } + + static final class StreamConditionalSubscription<T> extends AbstractStreamSubscription<T> { + + private static final long serialVersionUID = -9082954702547571853L; + + final ConditionalSubscriber<? super T> downstream; + + StreamConditionalSubscription(ConditionalSubscriber<? super T> downstream, Iterator<T> iterator, AutoCloseable closeable) { + super(iterator, closeable); + this.downstream = downstream; + } + + @Override + public void run(long n) { + long emitted = 0L; + Iterator<T> iterator = this.iterator; + ConditionalSubscriber<? super T> downstream = this.downstream; + + for (;;) { + + if (cancelled) { + clear(); + break; + } else { + T next; + try { + next = Objects.requireNonNull(iterator.next(), "The Stream's Iterator returned a null value"); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + downstream.onError(ex); + cancelled = true; + continue; + } + + if (downstream.tryOnNext(next)) { + emitted++; + } + + if (cancelled) { + continue; + } + + try { + if (!iterator.hasNext()) { + downstream.onComplete(); + cancelled = true; + continue; + } + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + downstream.onError(ex); + cancelled = true; + continue; + } + + if (emitted != n) { + continue; + } + } + + n = get(); + if (emitted == n) { + if (compareAndSet(n, 0L)) { + break; + } + n = get(); + } + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/jdk8/FlowableLastStageSubscriber.java b/src/main/java/io/reactivex/rxjava3/internal/jdk8/FlowableLastStageSubscriber.java new file mode 100644 index 0000000000..0bdf3e9304 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/jdk8/FlowableLastStageSubscriber.java @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.jdk8; + +import java.util.NoSuchElementException; + +import org.reactivestreams.Subscription; + +/** + * Signals the last element of the source via the underlying CompletableFuture, + * signals the a default item if the upstream is empty or signals {@link NoSuchElementException}. + * + * @param <T> the element type + * @since 3.0.0 + */ +public final class FlowableLastStageSubscriber<T> extends FlowableStageSubscriber<T> { + + final boolean hasDefault; + + final T defaultItem; + + public FlowableLastStageSubscriber(boolean hasDefault, T defaultItem) { + this.hasDefault = hasDefault; + this.defaultItem = defaultItem; + } + + @Override + public void onNext(T t) { + value = t; + } + + @Override + public void onComplete() { + if (!isDone()) { + T v = value; + clear(); + if (v != null) { + complete(v); + } else if (hasDefault) { + complete(defaultItem); + } else { + completeExceptionally(new NoSuchElementException()); + } + } + } + + @Override + protected void afterSubscribe(Subscription s) { + s.request(Long.MAX_VALUE); + } + +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/jdk8/FlowableMapOptional.java b/src/main/java/io/reactivex/rxjava3/internal/jdk8/FlowableMapOptional.java new file mode 100644 index 0000000000..cc4ff8bdf5 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/jdk8/FlowableMapOptional.java @@ -0,0 +1,180 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.jdk8; + +import java.util.*; + +import org.reactivestreams.Subscriber; + +import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.subscribers.*; +import io.reactivex.rxjava3.operators.ConditionalSubscriber; + +/** + * Map the upstream values into an Optional and emit its value if any. + * @param <T> the upstream element type + * @param <R> the output element type + * @since 3.0.0 + */ +public final class FlowableMapOptional<T, R> extends Flowable<R> { + + final Flowable<T> source; + + final Function<? super T, Optional<? extends R>> mapper; + + public FlowableMapOptional(Flowable<T> source, Function<? super T, Optional<? extends R>> mapper) { + this.source = source; + this.mapper = mapper; + } + + @Override + protected void subscribeActual(Subscriber<? super R> s) { + if (s instanceof ConditionalSubscriber) { + source.subscribe(new MapOptionalConditionalSubscriber<>((ConditionalSubscriber<? super R>)s, mapper)); + } else { + source.subscribe(new MapOptionalSubscriber<>(s, mapper)); + } + } + + static final class MapOptionalSubscriber<T, R> extends BasicFuseableSubscriber<T, R> + implements ConditionalSubscriber<T> { + + final Function<? super T, Optional<? extends R>> mapper; + + MapOptionalSubscriber(Subscriber<? super R> downstream, Function<? super T, Optional<? extends R>> mapper) { + super(downstream); + this.mapper = mapper; + } + + @Override + public void onNext(T t) { + if (!tryOnNext(t)) { + upstream.request(1); + } + } + + @Override + public boolean tryOnNext(T t) { + if (done) { + return true; + } + + if (sourceMode != NONE) { + downstream.onNext(null); + return true; + } + + Optional<? extends R> result; + try { + result = Objects.requireNonNull(mapper.apply(t), "The mapper returned a null Optional"); + } catch (Throwable ex) { + fail(ex); + return true; + } + + if (result.isPresent()) { + downstream.onNext(result.get()); + return true; + } + return false; + } + + @Override + public int requestFusion(int mode) { + return transitiveBoundaryFusion(mode); + } + + @Override + public R poll() throws Throwable { + for (;;) { + T item = qs.poll(); + if (item == null) { + return null; + } + Optional<? extends R> result = Objects.requireNonNull(mapper.apply(item), "The mapper returned a null Optional"); + if (result.isPresent()) { + return result.get(); + } + if (sourceMode == ASYNC) { + qs.request(1); + } + } + } + } + + static final class MapOptionalConditionalSubscriber<T, R> extends BasicFuseableConditionalSubscriber<T, R> { + + final Function<? super T, Optional<? extends R>> mapper; + + MapOptionalConditionalSubscriber(ConditionalSubscriber<? super R> downstream, Function<? super T, Optional<? extends R>> mapper) { + super(downstream); + this.mapper = mapper; + } + + @Override + public void onNext(T t) { + if (!tryOnNext(t)) { + upstream.request(1); + } + } + + @Override + public boolean tryOnNext(T t) { + if (done) { + return true; + } + + if (sourceMode != NONE) { + downstream.onNext(null); + return true; + } + + Optional<? extends R> result; + try { + result = Objects.requireNonNull(mapper.apply(t), "The mapper returned a null Optional"); + } catch (Throwable ex) { + fail(ex); + return true; + } + + if (result.isPresent()) { + return downstream.tryOnNext(result.get()); + } + return false; + } + + @Override + public int requestFusion(int mode) { + return transitiveBoundaryFusion(mode); + } + + @Override + public R poll() throws Throwable { + for (;;) { + T item = qs.poll(); + if (item == null) { + return null; + } + Optional<? extends R> result = Objects.requireNonNull(mapper.apply(item), "The mapper returned a null Optional"); + if (result.isPresent()) { + return result.get(); + } + if (sourceMode == ASYNC) { + qs.request(1); + } + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/jdk8/FlowableSingleStageSubscriber.java b/src/main/java/io/reactivex/rxjava3/internal/jdk8/FlowableSingleStageSubscriber.java new file mode 100644 index 0000000000..8206a9a8ac --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/jdk8/FlowableSingleStageSubscriber.java @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.jdk8; + +import java.util.NoSuchElementException; + +import org.reactivestreams.Subscription; + +/** + * Signals the only element of the source via the underlying CompletableFuture, + * signals the a default item if the upstream is empty or signals {@link IllegalArgumentException} + * if the upstream has more than one item. + * + * @param <T> the element type + * @since 3.0.0 + */ +public final class FlowableSingleStageSubscriber<T> extends FlowableStageSubscriber<T> { + + final boolean hasDefault; + + final T defaultItem; + + public FlowableSingleStageSubscriber(boolean hasDefault, T defaultItem) { + this.hasDefault = hasDefault; + this.defaultItem = defaultItem; + } + + @Override + public void onNext(T t) { + if (value != null) { + value = null; + completeExceptionally(new IllegalArgumentException("Sequence contains more than one element!")); + } else { + value = t; + } + } + + @Override + public void onComplete() { + if (!isDone()) { + T v = value; + clear(); + if (v != null) { + complete(v); + } else if (hasDefault) { + complete(defaultItem); + } else { + completeExceptionally(new NoSuchElementException()); + } + } + } + + @Override + protected void afterSubscribe(Subscription s) { + s.request(2); + } + +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/jdk8/FlowableStageSubscriber.java b/src/main/java/io/reactivex/rxjava3/internal/jdk8/FlowableStageSubscriber.java new file mode 100644 index 0000000000..66e4fddde7 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/jdk8/FlowableStageSubscriber.java @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.jdk8; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicReference; + +import org.reactivestreams.Subscription; + +import io.reactivex.rxjava3.annotations.NonNull; +import io.reactivex.rxjava3.core.FlowableSubscriber; +import io.reactivex.rxjava3.internal.subscriptions.SubscriptionHelper; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +/** + * Base class that extends CompletableFuture and provides basic infrastructure + * to notify watchers upon upstream signals. + * @param <T> the element type + * @since 3.0.0 + */ +abstract class FlowableStageSubscriber<T> extends CompletableFuture<T> implements FlowableSubscriber<T> { + + final AtomicReference<Subscription> upstream = new AtomicReference<>(); + + T value; + + @Override + public final void onSubscribe(@NonNull Subscription s) { + if (SubscriptionHelper.setOnce(upstream, s)) { + afterSubscribe(s); + } + } + + protected abstract void afterSubscribe(Subscription s); + + @Override + public final void onError(Throwable t) { + clear(); + if (!completeExceptionally(t)) { + RxJavaPlugins.onError(t); + } + } + + protected final void cancelUpstream() { + SubscriptionHelper.cancel(upstream); + } + + protected final void clear() { + value = null; + upstream.lazySet(SubscriptionHelper.CANCELLED); + } + + @Override + public final boolean cancel(boolean mayInterruptIfRunning) { + cancelUpstream(); + return super.cancel(mayInterruptIfRunning); + } + + @Override + public final boolean complete(T value) { + cancelUpstream(); + return super.complete(value); + } + + @Override + public final boolean completeExceptionally(Throwable ex) { + cancelUpstream(); + return super.completeExceptionally(ex); + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/jdk8/MaybeFlattenStreamAsFlowable.java b/src/main/java/io/reactivex/rxjava3/internal/jdk8/MaybeFlattenStreamAsFlowable.java new file mode 100644 index 0000000000..653642426e --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/jdk8/MaybeFlattenStreamAsFlowable.java @@ -0,0 +1,285 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.jdk8; + +import java.util.*; +import java.util.concurrent.atomic.AtomicLong; +import java.util.stream.Stream; + +import org.reactivestreams.Subscriber; + +import io.reactivex.rxjava3.annotations.*; +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; +import io.reactivex.rxjava3.internal.subscriptions.*; +import io.reactivex.rxjava3.internal.util.BackpressureHelper; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +/** + * Map the success value into a Java {@link Stream} and emits its values. + * + * @param <T> the source value type + * @param <R> the output value type + * @since 3.0.0 + */ +public final class MaybeFlattenStreamAsFlowable<T, R> extends Flowable<R> { + + final Maybe<T> source; + + final Function<? super T, ? extends Stream<? extends R>> mapper; + + public MaybeFlattenStreamAsFlowable(Maybe<T> source, Function<? super T, ? extends Stream<? extends R>> mapper) { + this.source = source; + this.mapper = mapper; + } + + @Override + protected void subscribeActual(@NonNull Subscriber<? super R> s) { + source.subscribe(new FlattenStreamMultiObserver<>(s, mapper)); + } + + static final class FlattenStreamMultiObserver<T, R> + extends BasicIntQueueSubscription<R> + implements MaybeObserver<T>, SingleObserver<T> { + + private static final long serialVersionUID = 7363336003027148283L; + + final Subscriber<? super R> downstream; + + final Function<? super T, ? extends Stream<? extends R>> mapper; + + final AtomicLong requested; + + Disposable upstream; + + volatile Iterator<? extends R> iterator; + + AutoCloseable close; + + boolean once; + + volatile boolean cancelled; + + boolean outputFused; + + long emitted; + + FlattenStreamMultiObserver(Subscriber<? super R> downstream, Function<? super T, ? extends Stream<? extends R>> mapper) { + this.downstream = downstream; + this.mapper = mapper; + this.requested = new AtomicLong(); + } + + @Override + public void onSubscribe(@NonNull Disposable d) { + if (DisposableHelper.validate(this.upstream, d)) { + this.upstream = d; + + downstream.onSubscribe(this); + } + } + + @Override + public void onSuccess(@NonNull T t) { + try { + Stream<? extends R> stream = Objects.requireNonNull(mapper.apply(t), "The mapper returned a null Stream"); + Iterator<? extends R> iterator = stream.iterator(); + AutoCloseable c = stream; + + if (!iterator.hasNext()) { + downstream.onComplete(); + close(c); + return; + } + this.iterator = iterator; + this.close = stream; + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + downstream.onError(ex); + return; + } + drain(); + } + + @Override + public void onError(@NonNull Throwable e) { + downstream.onError(e); + } + + @Override + public void onComplete() { + downstream.onComplete(); + } + + @Override + public void request(long n) { + if (SubscriptionHelper.validate(n)) { + BackpressureHelper.add(requested, n); + drain(); + } + } + + @Override + public void cancel() { + cancelled = true; + upstream.dispose(); + if (!outputFused) { + drain(); + } + } + + @Override + public int requestFusion(int mode) { + if ((mode & ASYNC) != 0) { + outputFused = true; + return ASYNC; + } + return NONE; + } + + @Override + public @Nullable R poll() throws Throwable { + Iterator<? extends R> it = iterator; + if (it != null) { + if (once) { + if (!it.hasNext()) { + clear(); + return null; + } + } else { + once = true; + } + return it.next(); + } + return null; + } + + @Override + public boolean isEmpty() { + Iterator<? extends R> it = iterator; + if (it != null) { + if (!once) { + return false; + } + if (it.hasNext()) { + return false; + } + clear(); + } + return true; + } + + @Override + public void clear() { + iterator = null; + AutoCloseable close = this.close; + this.close = null; + close(close); + } + + void close(AutoCloseable c) { + try { + if (c != null) { + c.close(); + } + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + RxJavaPlugins.onError(ex); + } + } + + void drain() { + if (getAndIncrement() != 0) { + return; + } + + int missed = 1; + Subscriber<? super R> downstream = this.downstream; + long emitted = this.emitted; + long requested = this.requested.get(); + Iterator<? extends R> it = iterator; + + for (;;) { + + if (cancelled) { + clear(); + } else { + if (outputFused) { + if (it != null) { + downstream.onNext(null); + downstream.onComplete(); + } + } else { + if (it != null && emitted != requested) { + R item; + try { + item = it.next(); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + downstream.onError(ex); + cancelled = true; + continue; + } + + if (cancelled) { + continue; + } + + downstream.onNext(item); + emitted++; + + if (cancelled) { + continue; + } + + boolean has; + try { + has = it.hasNext(); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + downstream.onError(ex); + cancelled = true; + continue; + } + + if (cancelled) { + continue; + } + + if (!has) { + downstream.onComplete(); + cancelled = true; + } + continue; + } + } + } + + this.emitted = emitted; + missed = addAndGet(-missed); + if (missed == 0) { + return; + } + + requested = this.requested.get(); + if (it == null) { + it = iterator; + } + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/jdk8/MaybeFlattenStreamAsObservable.java b/src/main/java/io/reactivex/rxjava3/internal/jdk8/MaybeFlattenStreamAsObservable.java new file mode 100644 index 0000000000..404fcb8106 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/jdk8/MaybeFlattenStreamAsObservable.java @@ -0,0 +1,262 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.jdk8; + +import java.util.*; +import java.util.stream.Stream; + +import io.reactivex.rxjava3.annotations.*; +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.core.Observer; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; +import io.reactivex.rxjava3.internal.observers.BasicIntQueueDisposable; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +/** + * Map the success value into a Java {@link Stream} and emits its values. + * + * @param <T> the source value type + * @param <R> the output value type + * @since 3.0.0 + */ +public final class MaybeFlattenStreamAsObservable<T, R> extends Observable<R> { + + final Maybe<T> source; + + final Function<? super T, ? extends Stream<? extends R>> mapper; + + public MaybeFlattenStreamAsObservable(Maybe<T> source, Function<? super T, ? extends Stream<? extends R>> mapper) { + this.source = source; + this.mapper = mapper; + } + + @Override + protected void subscribeActual(@NonNull Observer<? super R> s) { + source.subscribe(new FlattenStreamMultiObserver<>(s, mapper)); + } + + static final class FlattenStreamMultiObserver<T, R> + extends BasicIntQueueDisposable<R> + implements MaybeObserver<T>, SingleObserver<T> { + + private static final long serialVersionUID = 7363336003027148283L; + + final Observer<? super R> downstream; + + final Function<? super T, ? extends Stream<? extends R>> mapper; + + Disposable upstream; + + volatile Iterator<? extends R> iterator; + + AutoCloseable close; + + boolean once; + + volatile boolean disposed; + + boolean outputFused; + + FlattenStreamMultiObserver(Observer<? super R> downstream, Function<? super T, ? extends Stream<? extends R>> mapper) { + this.downstream = downstream; + this.mapper = mapper; + } + + @Override + public void onSubscribe(@NonNull Disposable d) { + if (DisposableHelper.validate(this.upstream, d)) { + this.upstream = d; + + downstream.onSubscribe(this); + } + } + + @Override + public void onSuccess(@NonNull T t) { + try { + Stream<? extends R> stream = Objects.requireNonNull(mapper.apply(t), "The mapper returned a null Stream"); + Iterator<? extends R> iterator = stream.iterator(); + AutoCloseable c = stream; + + if (!iterator.hasNext()) { + downstream.onComplete(); + close(c); + return; + } + this.iterator = iterator; + this.close = stream; + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + downstream.onError(ex); + return; + } + drain(); + } + + @Override + public void onError(@NonNull Throwable e) { + downstream.onError(e); + } + + @Override + public void onComplete() { + downstream.onComplete(); + } + + @Override + public void dispose() { + disposed = true; + upstream.dispose(); + if (!outputFused) { + drain(); + } + } + + @Override + public boolean isDisposed() { + return disposed; + } + + @Override + public int requestFusion(int mode) { + if ((mode & ASYNC) != 0) { + outputFused = true; + return ASYNC; + } + return NONE; + } + + @Override + public @Nullable R poll() throws Throwable { + Iterator<? extends R> it = iterator; + if (it != null) { + if (once) { + if (!it.hasNext()) { + clear(); + return null; + } + } else { + once = true; + } + return it.next(); + } + return null; + } + + @Override + public boolean isEmpty() { + Iterator<? extends R> it = iterator; + if (it != null) { + if (!once) { + return false; + } + if (it.hasNext()) { + return false; + } + clear(); + } + return true; + } + + @Override + public void clear() { + iterator = null; + AutoCloseable close = this.close; + this.close = null; + close(close); + } + + void close(AutoCloseable c) { + try { + if (c != null) { + c.close(); + } + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + RxJavaPlugins.onError(ex); + } + } + + void drain() { + if (getAndIncrement() != 0) { + return; + } + + int missed = 1; + Observer<? super R> downstream = this.downstream; + Iterator<? extends R> it = iterator; + + for (;;) { + + if (disposed) { + clear(); + } else { + if (outputFused) { + downstream.onNext(null); + downstream.onComplete(); + } else { + R item; + try { + item = it.next(); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + downstream.onError(ex); + disposed = true; + continue; + } + + if (disposed) { + continue; + } + + downstream.onNext(item); + + if (disposed) { + continue; + } + + boolean has; + try { + has = it.hasNext(); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + downstream.onError(ex); + disposed = true; + continue; + } + + if (disposed) { + continue; + } + + if (!has) { + downstream.onComplete(); + disposed = true; + } + continue; + } + } + + missed = addAndGet(-missed); + if (missed == 0) { + return; + } + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/jdk8/MaybeFromCompletionStage.java b/src/main/java/io/reactivex/rxjava3/internal/jdk8/MaybeFromCompletionStage.java new file mode 100644 index 0000000000..45af605e96 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/jdk8/MaybeFromCompletionStage.java @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.jdk8; + +import java.util.concurrent.CompletionStage; +import java.util.function.BiConsumer; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.jdk8.FlowableFromCompletionStage.BiConsumerAtomicReference; + +/** + * Wrap a CompletionStage and signal its outcome. + * @param <T> the element type + * @since 3.0.0 + */ +public final class MaybeFromCompletionStage<T> extends Maybe<T> { + + final CompletionStage<T> stage; + + public MaybeFromCompletionStage(CompletionStage<T> stage) { + this.stage = stage; + } + + @Override + protected void subscribeActual(MaybeObserver<? super T> observer) { + // We need an indirection because one can't detach from a whenComplete + // and cancellation should not hold onto the stage. + BiConsumerAtomicReference<T> whenReference = new BiConsumerAtomicReference<>(); + CompletionStageHandler<T> handler = new CompletionStageHandler<>(observer, whenReference); + whenReference.lazySet(handler); + + observer.onSubscribe(handler); + stage.whenComplete(whenReference); + } + + static final class CompletionStageHandler<T> + implements Disposable, BiConsumer<T, Throwable> { + + final MaybeObserver<? super T> downstream; + + final BiConsumerAtomicReference<T> whenReference; + + CompletionStageHandler(MaybeObserver<? super T> downstream, BiConsumerAtomicReference<T> whenReference) { + this.downstream = downstream; + this.whenReference = whenReference; + } + + @Override + public void accept(T item, Throwable error) { + if (error != null) { + downstream.onError(error); + } + else if (item != null) { + downstream.onSuccess(item); + } else { + downstream.onComplete(); + } + } + + @Override + public void dispose() { + whenReference.set(null); + } + + @Override + public boolean isDisposed() { + return whenReference.get() == null; + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/jdk8/MaybeMapOptional.java b/src/main/java/io/reactivex/rxjava3/internal/jdk8/MaybeMapOptional.java new file mode 100644 index 0000000000..760456f2ee --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/jdk8/MaybeMapOptional.java @@ -0,0 +1,110 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.jdk8; + +import java.util.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; + +/** + * Maps the success value to an {@link Optional} and emits its non-empty value or completes. + * + * @param <T> the upstream success value type + * @param <R> the result value type + * @since 3.0.0 + */ +public final class MaybeMapOptional<T, R> extends Maybe<R> { + + final Maybe<T> source; + + final Function<? super T, Optional<? extends R>> mapper; + + public MaybeMapOptional(Maybe<T> source, Function<? super T, Optional<? extends R>> mapper) { + this.source = source; + this.mapper = mapper; + } + + @Override + protected void subscribeActual(MaybeObserver<? super R> observer) { + source.subscribe(new MapOptionalMaybeObserver<>(observer, mapper)); + } + + static final class MapOptionalMaybeObserver<T, R> implements MaybeObserver<T>, Disposable { + + final MaybeObserver<? super R> downstream; + + final Function<? super T, Optional<? extends R>> mapper; + + Disposable upstream; + + MapOptionalMaybeObserver(MaybeObserver<? super R> downstream, Function<? super T, Optional<? extends R>> mapper) { + this.downstream = downstream; + this.mapper = mapper; + } + + @Override + public void dispose() { + Disposable d = this.upstream; + this.upstream = DisposableHelper.DISPOSED; + d.dispose(); + } + + @Override + public boolean isDisposed() { + return upstream.isDisposed(); + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.validate(this.upstream, d)) { + this.upstream = d; + + downstream.onSubscribe(this); + } + } + + @Override + public void onSuccess(T value) { + Optional<? extends R> v; + + try { + v = Objects.requireNonNull(mapper.apply(value), "The mapper returned a null item"); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + downstream.onError(ex); + return; + } + + if (v.isPresent()) { + downstream.onSuccess(v.get()); + } else { + downstream.onComplete(); + } + } + + @Override + public void onError(Throwable e) { + downstream.onError(e); + } + + @Override + public void onComplete() { + downstream.onComplete(); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/jdk8/ObservableCollectWithCollector.java b/src/main/java/io/reactivex/rxjava3/internal/jdk8/ObservableCollectWithCollector.java new file mode 100644 index 0000000000..980317750e --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/jdk8/ObservableCollectWithCollector.java @@ -0,0 +1,152 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.jdk8; + +import java.util.Objects; +import java.util.function.*; +import java.util.stream.Collector; + +import io.reactivex.rxjava3.annotations.NonNull; +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.internal.disposables.*; +import io.reactivex.rxjava3.internal.observers.DeferredScalarDisposable; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +/** + * Collect items into a container defined by a Stream {@link Collector} callback set. + * + * @param <T> the upstream value type + * @param <A> the intermediate accumulator type + * @param <R> the result type + * @since 3.0.0 + */ +public final class ObservableCollectWithCollector<T, A, R> extends Observable<R> { + + final Observable<T> source; + + final Collector<? super T, A, R> collector; + + public ObservableCollectWithCollector(Observable<T> source, Collector<? super T, A, R> collector) { + this.source = source; + this.collector = collector; + } + + @Override + protected void subscribeActual(@NonNull Observer<? super R> observer) { + A container; + BiConsumer<A, ? super T> accumulator; + Function<A, R> finisher; + + try { + container = collector.supplier().get(); + accumulator = collector.accumulator(); + finisher = collector.finisher(); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + EmptyDisposable.error(ex, observer); + return; + } + + source.subscribe(new CollectorObserver<>(observer, container, accumulator, finisher)); + } + + static final class CollectorObserver<T, A, R> + extends DeferredScalarDisposable<R> + implements Observer<T> { + + private static final long serialVersionUID = -229544830565448758L; + + final BiConsumer<A, T> accumulator; + + final Function<A, R> finisher; + + Disposable upstream; + + boolean done; + + A container; + + CollectorObserver(Observer<? super R> downstream, A container, BiConsumer<A, T> accumulator, Function<A, R> finisher) { + super(downstream); + this.container = container; + this.accumulator = accumulator; + this.finisher = finisher; + } + + @Override + public void onSubscribe(@NonNull Disposable d) { + if (DisposableHelper.validate(this.upstream, d)) { + this.upstream = d; + + downstream.onSubscribe(this); + } + } + + @Override + public void onNext(T t) { + if (done) { + return; + } + try { + accumulator.accept(container, t); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + upstream.dispose(); + onError(ex); + } + } + + @Override + public void onError(Throwable t) { + if (done) { + RxJavaPlugins.onError(t); + } else { + done = true; + upstream = DisposableHelper.DISPOSED; + this.container = null; + downstream.onError(t); + } + } + + @Override + public void onComplete() { + if (done) { + return; + } + + done = true; + upstream = DisposableHelper.DISPOSED; + A container = this.container; + this.container = null; + R result; + try { + result = Objects.requireNonNull(finisher.apply(container), "The finisher returned a null value"); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + downstream.onError(ex); + return; + } + + complete(result); + } + + @Override + public void dispose() { + super.dispose(); + upstream.dispose(); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/jdk8/ObservableCollectWithCollectorSingle.java b/src/main/java/io/reactivex/rxjava3/internal/jdk8/ObservableCollectWithCollectorSingle.java new file mode 100644 index 0000000000..fa1ccd9c8f --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/jdk8/ObservableCollectWithCollectorSingle.java @@ -0,0 +1,160 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.jdk8; + +import java.util.Objects; +import java.util.function.*; +import java.util.stream.Collector; + +import io.reactivex.rxjava3.annotations.NonNull; +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.internal.disposables.*; +import io.reactivex.rxjava3.internal.fuseable.FuseToObservable; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +/** + * Collect items into a container defined by a Stream {@link Collector} callback set. + * + * @param <T> the upstream value type + * @param <A> the intermediate accumulator type + * @param <R> the result type + * @since 3.0.0 + */ +public final class ObservableCollectWithCollectorSingle<T, A, R> extends Single<R> implements FuseToObservable<R> { + + final Observable<T> source; + + final Collector<? super T, A, R> collector; + + public ObservableCollectWithCollectorSingle(Observable<T> source, Collector<? super T, A, R> collector) { + this.source = source; + this.collector = collector; + } + + @Override + public Observable<R> fuseToObservable() { + return new ObservableCollectWithCollector<>(source, collector); + } + + @Override + protected void subscribeActual(@NonNull SingleObserver<? super R> observer) { + A container; + BiConsumer<A, ? super T> accumulator; + Function<A, R> finisher; + + try { + container = collector.supplier().get(); + accumulator = collector.accumulator(); + finisher = collector.finisher(); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + EmptyDisposable.error(ex, observer); + return; + } + + source.subscribe(new CollectorSingleObserver<>(observer, container, accumulator, finisher)); + } + + static final class CollectorSingleObserver<T, A, R> implements Observer<T>, Disposable { + + final SingleObserver<? super R> downstream; + + final BiConsumer<A, T> accumulator; + + final Function<A, R> finisher; + + Disposable upstream; + + boolean done; + + A container; + + CollectorSingleObserver(SingleObserver<? super R> downstream, A container, BiConsumer<A, T> accumulator, Function<A, R> finisher) { + this.downstream = downstream; + this.container = container; + this.accumulator = accumulator; + this.finisher = finisher; + } + + @Override + public void onSubscribe(@NonNull Disposable d) { + if (DisposableHelper.validate(this.upstream, d)) { + this.upstream = d; + + downstream.onSubscribe(this); + } + } + + @Override + public void onNext(T t) { + if (done) { + return; + } + try { + accumulator.accept(container, t); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + upstream.dispose(); + onError(ex); + } + } + + @Override + public void onError(Throwable t) { + if (done) { + RxJavaPlugins.onError(t); + } else { + done = true; + upstream = DisposableHelper.DISPOSED; + this.container = null; + downstream.onError(t); + } + } + + @Override + public void onComplete() { + if (done) { + return; + } + + done = true; + upstream = DisposableHelper.DISPOSED; + A container = this.container; + this.container = null; + R result; + try { + result = Objects.requireNonNull(finisher.apply(container), "The finisher returned a null value"); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + downstream.onError(ex); + return; + } + + downstream.onSuccess(result); + } + + @Override + public void dispose() { + upstream.dispose(); + upstream = DisposableHelper.DISPOSED; + } + + @Override + public boolean isDisposed() { + return upstream == DisposableHelper.DISPOSED; + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/jdk8/ObservableFirstStageObserver.java b/src/main/java/io/reactivex/rxjava3/internal/jdk8/ObservableFirstStageObserver.java new file mode 100644 index 0000000000..6ac5ae51f1 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/jdk8/ObservableFirstStageObserver.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.jdk8; + +import java.util.NoSuchElementException; + +/** + * Signals the first element of the source via the underlying CompletableFuture, + * signals the a default item if the upstream is empty or signals {@link NoSuchElementException}. + * + * @param <T> the element type + * @since 3.0.0 + */ +public final class ObservableFirstStageObserver<T> extends ObservableStageObserver<T> { + + final boolean hasDefault; + + final T defaultItem; + + public ObservableFirstStageObserver(boolean hasDefault, T defaultItem) { + this.hasDefault = hasDefault; + this.defaultItem = defaultItem; + } + + @Override + public void onNext(T t) { + complete(t); + } + + @Override + public void onComplete() { + if (!isDone()) { + clear(); + if (hasDefault) { + complete(defaultItem); + } else { + completeExceptionally(new NoSuchElementException()); + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/jdk8/ObservableFlatMapStream.java b/src/main/java/io/reactivex/rxjava3/internal/jdk8/ObservableFlatMapStream.java new file mode 100644 index 0000000000..72bbea2a3f --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/jdk8/ObservableFlatMapStream.java @@ -0,0 +1,163 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.jdk8; + +import java.util.*; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Stream; + +import io.reactivex.rxjava3.annotations.NonNull; +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.core.Observer; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.disposables.*; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +/** + * Maps the upstream values onto {@link Stream}s and emits their items in order to the downstream. + * + * @param <T> the upstream element type + * @param <R> the inner {@code Stream} and result element type + * @since 3.0.0 + */ +public final class ObservableFlatMapStream<T, R> extends Observable<R> { + + final Observable<T> source; + + final Function<? super T, ? extends Stream<? extends R>> mapper; + + public ObservableFlatMapStream(Observable<T> source, Function<? super T, ? extends Stream<? extends R>> mapper) { + this.source = source; + this.mapper = mapper; + } + + @Override + protected void subscribeActual(Observer<? super R> observer) { + if (source instanceof Supplier) { + Stream<? extends R> stream = null; + try { + @SuppressWarnings("unchecked") + T t = ((Supplier<T>)source).get(); + if (t != null) { + stream = Objects.requireNonNull(mapper.apply(t), "The mapper returned a null Stream"); + } + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + EmptyDisposable.error(ex, observer); + return; + } + + if (stream != null) { + ObservableFromStream.subscribeStream(observer, stream); + } else { + EmptyDisposable.complete(observer); + } + } else { + source.subscribe(new FlatMapStreamObserver<>(observer, mapper)); + } + } + + static final class FlatMapStreamObserver<T, R> extends AtomicInteger + implements Observer<T>, Disposable { + + private static final long serialVersionUID = -5127032662980523968L; + + final Observer<? super R> downstream; + + final Function<? super T, ? extends Stream<? extends R>> mapper; + + Disposable upstream; + + volatile boolean disposed; + + boolean done; + + FlatMapStreamObserver(Observer<? super R> downstream, Function<? super T, ? extends Stream<? extends R>> mapper) { + this.downstream = downstream; + this.mapper = mapper; + } + + @Override + public void onSubscribe(@NonNull Disposable d) { + if (DisposableHelper.validate(this.upstream, d)) { + this.upstream = d; + + downstream.onSubscribe(this); + } + } + + @Override + public void onNext(@NonNull T t) { + if (done) { + return; + } + try { + try (Stream<? extends R> stream = Objects.requireNonNull(mapper.apply(t), "The mapper returned a null Stream")) { + Iterator<? extends R> it = stream.iterator(); + while (it.hasNext()) { + if (disposed) { + done = true; + break; + } + R value = Objects.requireNonNull(it.next(), "The Stream's Iterator.next returned a null value"); + if (disposed) { + done = true; + break; + } + downstream.onNext(value); + if (disposed) { + done = true; + break; + } + } + } + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + upstream.dispose(); + onError(ex); + } + } + + @Override + public void onError(@NonNull Throwable e) { + if (done) { + RxJavaPlugins.onError(e); + } else { + done = true; + downstream.onError(e); + } + } + + @Override + public void onComplete() { + if (!done) { + done = true; + downstream.onComplete(); + } + } + + @Override + public void dispose() { + disposed = true; + upstream.dispose(); + } + + @Override + public boolean isDisposed() { + return disposed; + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/jdk8/ObservableFromCompletionStage.java b/src/main/java/io/reactivex/rxjava3/internal/jdk8/ObservableFromCompletionStage.java new file mode 100644 index 0000000000..11a5a6307c --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/jdk8/ObservableFromCompletionStage.java @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.jdk8; + +import java.util.concurrent.CompletionStage; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.BiConsumer; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.internal.observers.DeferredScalarDisposable; + +/** + * Wrap a CompletionStage and signal its outcome. + * @param <T> the element type + * @since 3.0.0 + */ +public final class ObservableFromCompletionStage<T> extends Observable<T> { + + final CompletionStage<T> stage; + + public ObservableFromCompletionStage(CompletionStage<T> stage) { + this.stage = stage; + } + + @Override + protected void subscribeActual(Observer<? super T> observer) { + // We need an indirection because one can't detach from a whenComplete + // and cancellation should not hold onto the stage. + BiConsumerAtomicReference<T> whenReference = new BiConsumerAtomicReference<>(); + CompletionStageHandler<T> handler = new CompletionStageHandler<>(observer, whenReference); + whenReference.lazySet(handler); + + observer.onSubscribe(handler); + stage.whenComplete(whenReference); + } + + static final class CompletionStageHandler<T> + extends DeferredScalarDisposable<T> + implements BiConsumer<T, Throwable> { + + private static final long serialVersionUID = 4665335664328839859L; + + final BiConsumerAtomicReference<T> whenReference; + + CompletionStageHandler(Observer<? super T> downstream, BiConsumerAtomicReference<T> whenReference) { + super(downstream); + this.whenReference = whenReference; + } + + @Override + public void accept(T item, Throwable error) { + if (error != null) { + downstream.onError(error); + } + else if (item != null) { + complete(item); + } else { + downstream.onError(new NullPointerException("The CompletionStage terminated with null.")); + } + } + + @Override + public void dispose() { + super.dispose(); + whenReference.set(null); + } + } + + static final class BiConsumerAtomicReference<T> extends AtomicReference<BiConsumer<T, Throwable>> + implements BiConsumer<T, Throwable> { + + private static final long serialVersionUID = 45838553147237545L; + + @Override + public void accept(T t, Throwable u) { + BiConsumer<T, Throwable> biConsumer = get(); + if (biConsumer != null) { + biConsumer.accept(t, u); + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/jdk8/ObservableFromStream.java b/src/main/java/io/reactivex/rxjava3/internal/jdk8/ObservableFromStream.java new file mode 100644 index 0000000000..169eeb0bcc --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/jdk8/ObservableFromStream.java @@ -0,0 +1,220 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.jdk8; + +import java.util.*; +import java.util.stream.Stream; + +import io.reactivex.rxjava3.annotations.*; +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.core.Observer; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.internal.disposables.EmptyDisposable; +import io.reactivex.rxjava3.operators.QueueDisposable; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +/** + * Wraps a {@link Stream} and emits its values as an {@link Observable} sequence. + * @param <T> the element type of the Stream + * @since 3.0.0 + */ +public final class ObservableFromStream<T> extends Observable<T> { + + final Stream<T> stream; + + public ObservableFromStream(Stream<T> stream) { + this.stream = stream; + } + + @Override + protected void subscribeActual(Observer<? super T> observer) { + subscribeStream(observer, stream); + } + + /** + * Subscribes to the Stream. + * @param <T> the element type of the flow + * @param observer the observer to drive + * @param stream the sequence to consume + */ + public static <T> void subscribeStream(Observer<? super T> observer, Stream<T> stream) { + Iterator<T> iterator; + try { + iterator = stream.iterator(); + + if (!iterator.hasNext()) { + EmptyDisposable.complete(observer); + closeSafely(stream); + return; + } + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + EmptyDisposable.error(ex, observer); + closeSafely(stream); + return; + } + + StreamDisposable<T> disposable = new StreamDisposable<>(observer, iterator, stream); + observer.onSubscribe(disposable); + disposable.run(); + } + + static void closeSafely(AutoCloseable c) { + try { + c.close(); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + RxJavaPlugins.onError(ex); + } + } + + static final class StreamDisposable<T> implements QueueDisposable<T> { + + final Observer<? super T> downstream; + + Iterator<T> iterator; + + AutoCloseable closeable; + + volatile boolean disposed; + + boolean once; + + boolean outputFused; + + StreamDisposable(Observer<? super T> downstream, Iterator<T> iterator, AutoCloseable closeable) { + this.downstream = downstream; + this.iterator = iterator; + this.closeable = closeable; + } + + @Override + public void dispose() { + disposed = true; + run(); + } + + @Override + public boolean isDisposed() { + return disposed; + } + + @Override + public int requestFusion(int mode) { + if ((mode & SYNC) != 0) { + outputFused = true; + return SYNC; + } + return NONE; + } + + @Override + public boolean offer(@NonNull T value) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean offer(@NonNull T v1, @NonNull T v2) { + throw new UnsupportedOperationException(); + } + + @Nullable + @Override + public T poll() { + if (iterator == null) { + return null; + } + if (!once) { + once = true; + } else { + if (!iterator.hasNext()) { + clear(); + return null; + } + } + return Objects.requireNonNull(iterator.next(), "The Stream's Iterator.next() returned a null value"); + } + + @Override + public boolean isEmpty() { + Iterator<T> it = iterator; + if (it != null) { + if (!once || it.hasNext()) { + return false; + } + clear(); + } + return true; + } + + @Override + public void clear() { + iterator = null; + AutoCloseable c = closeable; + closeable = null; + if (c != null) { + closeSafely(c); + } + } + + public void run() { + if (outputFused) { + return; + } + Iterator<T> iterator = this.iterator; + Observer<? super T> downstream = this.downstream; + + for (;;) { + if (disposed) { + clear(); + break; + } + + T next; + try { + next = Objects.requireNonNull(iterator.next(), "The Stream's Iterator.next returned a null value"); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + downstream.onError(ex); + disposed = true; + continue; + } + + if (disposed) { + continue; + } + + downstream.onNext(next); + + if (disposed) { + continue; + } + + try { + if (iterator.hasNext()) { + continue; + } + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + downstream.onError(ex); + disposed = true; + continue; + } + + downstream.onComplete(); + disposed = true; + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/jdk8/ObservableLastStageObserver.java b/src/main/java/io/reactivex/rxjava3/internal/jdk8/ObservableLastStageObserver.java new file mode 100644 index 0000000000..b35ab3dba9 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/jdk8/ObservableLastStageObserver.java @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.jdk8; + +import java.util.NoSuchElementException; + +/** + * Signals the last element of the source via the underlying CompletableFuture, + * signals the a default item if the upstream is empty or signals {@link NoSuchElementException}. + * + * @param <T> the element type + * @since 3.0.0 + */ +public final class ObservableLastStageObserver<T> extends ObservableStageObserver<T> { + + final boolean hasDefault; + + final T defaultItem; + + public ObservableLastStageObserver(boolean hasDefault, T defaultItem) { + this.hasDefault = hasDefault; + this.defaultItem = defaultItem; + } + + @Override + public void onNext(T t) { + value = t; + } + + @Override + public void onComplete() { + if (!isDone()) { + T v = value; + clear(); + if (v != null) { + complete(v); + } else if (hasDefault) { + complete(defaultItem); + } else { + completeExceptionally(new NoSuchElementException()); + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/jdk8/ObservableMapOptional.java b/src/main/java/io/reactivex/rxjava3/internal/jdk8/ObservableMapOptional.java new file mode 100644 index 0000000000..a51ef1af28 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/jdk8/ObservableMapOptional.java @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.jdk8; + +import java.util.*; + +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.core.Observer; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.observers.BasicFuseableObserver; + +/** + * Map the upstream values into an Optional and emit its value if any. + * @param <T> the upstream element type + * @param <R> the output element type + * @since 3.0.0 + */ +public final class ObservableMapOptional<T, R> extends Observable<R> { + + final Observable<T> source; + + final Function<? super T, Optional<? extends R>> mapper; + + public ObservableMapOptional(Observable<T> source, Function<? super T, Optional<? extends R>> mapper) { + this.source = source; + this.mapper = mapper; + } + + @Override + protected void subscribeActual(Observer<? super R> observer) { + source.subscribe(new MapOptionalObserver<>(observer, mapper)); + } + + static final class MapOptionalObserver<T, R> extends BasicFuseableObserver<T, R> { + + final Function<? super T, Optional<? extends R>> mapper; + + MapOptionalObserver(Observer<? super R> downstream, Function<? super T, Optional<? extends R>> mapper) { + super(downstream); + this.mapper = mapper; + } + + @Override + public void onNext(T t) { + if (done) { + return; + } + + if (sourceMode != NONE) { + downstream.onNext(null); + return; + } + + Optional<? extends R> result; + try { + result = Objects.requireNonNull(mapper.apply(t), "The mapper returned a null Optional"); + } catch (Throwable ex) { + fail(ex); + return; + } + + if (result.isPresent()) { + downstream.onNext(result.get()); + } + } + + @Override + public int requestFusion(int mode) { + return transitiveBoundaryFusion(mode); + } + + @Override + public R poll() throws Throwable { + for (;;) { + T item = qd.poll(); + if (item == null) { + return null; + } + Optional<? extends R> result = Objects.requireNonNull(mapper.apply(item), "The mapper returned a null Optional"); + if (result.isPresent()) { + return result.get(); + } + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/jdk8/ObservableSingleStageObserver.java b/src/main/java/io/reactivex/rxjava3/internal/jdk8/ObservableSingleStageObserver.java new file mode 100644 index 0000000000..c506fc36d4 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/jdk8/ObservableSingleStageObserver.java @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.jdk8; + +import java.util.NoSuchElementException; + +/** + * Signals the only element of the source via the underlying CompletableFuture, + * signals the a default item if the upstream is empty or signals {@link IllegalArgumentException} + * if the upstream has more than one item. + * + * @param <T> the element type + * @since 3.0.0 + */ +public final class ObservableSingleStageObserver<T> extends ObservableStageObserver<T> { + + final boolean hasDefault; + + final T defaultItem; + + public ObservableSingleStageObserver(boolean hasDefault, T defaultItem) { + this.hasDefault = hasDefault; + this.defaultItem = defaultItem; + } + + @Override + public void onNext(T t) { + if (value != null) { + value = null; + completeExceptionally(new IllegalArgumentException("Sequence contains more than one element!")); + } else { + value = t; + } + } + + @Override + public void onComplete() { + if (!isDone()) { + T v = value; + clear(); + if (v != null) { + complete(v); + } else if (hasDefault) { + complete(defaultItem); + } else { + completeExceptionally(new NoSuchElementException()); + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/jdk8/ObservableStageObserver.java b/src/main/java/io/reactivex/rxjava3/internal/jdk8/ObservableStageObserver.java new file mode 100644 index 0000000000..4025890f17 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/jdk8/ObservableStageObserver.java @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.jdk8; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicReference; + +import io.reactivex.rxjava3.annotations.NonNull; +import io.reactivex.rxjava3.core.Observer; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +/** + * Base class that extends CompletableFuture and provides basic infrastructure + * to notify watchers upon upstream signals. + * @param <T> the element type + * @since 3.0.0 + */ +abstract class ObservableStageObserver<T> extends CompletableFuture<T> implements Observer<T> { + + final AtomicReference<Disposable> upstream = new AtomicReference<>(); + + T value; + + @Override + public final void onSubscribe(@NonNull Disposable d) { + DisposableHelper.setOnce(upstream, d); + } + + @Override + public final void onError(Throwable t) { + clear(); + if (!completeExceptionally(t)) { + RxJavaPlugins.onError(t); + } + } + + protected final void disposeUpstream() { + DisposableHelper.dispose(upstream); + } + + protected final void clear() { + value = null; + upstream.lazySet(DisposableHelper.DISPOSED); + } + + @Override + public final boolean cancel(boolean mayInterruptIfRunning) { + disposeUpstream(); + return super.cancel(mayInterruptIfRunning); + } + + @Override + public final boolean complete(T value) { + disposeUpstream(); + return super.complete(value); + } + + @Override + public final boolean completeExceptionally(Throwable ex) { + disposeUpstream(); + return super.completeExceptionally(ex); + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/jdk8/ParallelCollector.java b/src/main/java/io/reactivex/rxjava3/internal/jdk8/ParallelCollector.java new file mode 100644 index 0000000000..7fefa11e80 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/jdk8/ParallelCollector.java @@ -0,0 +1,269 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.jdk8; + +import java.util.Objects; +import java.util.concurrent.atomic.*; +import java.util.function.*; +import java.util.stream.Collector; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.internal.subscriptions.*; +import io.reactivex.rxjava3.internal.util.AtomicThrowable; +import io.reactivex.rxjava3.parallel.ParallelFlowable; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +/** + * Reduces all 'rails' into a single via a Java 8 {@link Collector} callback set. + * + * @param <T> the value type + * @param <A> the accumulator type + * @param <R> the result type + * @since 3.0.0 + */ +public final class ParallelCollector<T, A, R> extends Flowable<R> { + + final ParallelFlowable<? extends T> source; + + final Collector<T, A, R> collector; + + public ParallelCollector(ParallelFlowable<? extends T> source, Collector<T, A, R> collector) { + this.source = source; + this.collector = collector; + } + + @Override + protected void subscribeActual(Subscriber<? super R> s) { + ParallelCollectorSubscriber<T, A, R> parent; + try { + parent = new ParallelCollectorSubscriber<>(s, source.parallelism(), collector); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + EmptySubscription.error(ex, s); + return; + } + s.onSubscribe(parent); + + source.subscribe(parent.subscribers); + } + + static final class ParallelCollectorSubscriber<T, A, R> extends DeferredScalarSubscription<R> { + + private static final long serialVersionUID = -5370107872170712765L; + + final ParallelCollectorInnerSubscriber<T, A, R>[] subscribers; + + final AtomicReference<SlotPair<A>> current = new AtomicReference<>(); + + final AtomicInteger remaining = new AtomicInteger(); + + final AtomicThrowable error = new AtomicThrowable(); + + final Function<A, R> finisher; + + ParallelCollectorSubscriber(Subscriber<? super R> subscriber, int n, Collector<T, A, R> collector) { + super(subscriber); + this.finisher = collector.finisher(); + @SuppressWarnings("unchecked") + ParallelCollectorInnerSubscriber<T, A, R>[] a = new ParallelCollectorInnerSubscriber[n]; + for (int i = 0; i < n; i++) { + a[i] = new ParallelCollectorInnerSubscriber<>(this, collector.supplier().get(), collector.accumulator(), collector.combiner()); + } + this.subscribers = a; + remaining.lazySet(n); + } + + SlotPair<A> addValue(A value) { + for (;;) { + SlotPair<A> curr = current.get(); + + if (curr == null) { + curr = new SlotPair<>(); + if (!current.compareAndSet(null, curr)) { + continue; + } + } + + int c = curr.tryAcquireSlot(); + if (c < 0) { + current.compareAndSet(curr, null); + continue; + } + if (c == 0) { + curr.first = value; + } else { + curr.second = value; + } + + if (curr.releaseSlot()) { + current.compareAndSet(curr, null); + return curr; + } + return null; + } + } + + @Override + public void cancel() { + for (ParallelCollectorInnerSubscriber<T, A, R> inner : subscribers) { + inner.cancel(); + } + } + + void innerError(Throwable ex) { + if (error.compareAndSet(null, ex)) { + cancel(); + downstream.onError(ex); + } else { + if (ex != error.get()) { + RxJavaPlugins.onError(ex); + } + } + } + + void innerComplete(A value, BinaryOperator<A> combiner) { + for (;;) { + SlotPair<A> sp = addValue(value); + + if (sp != null) { + + try { + value = combiner.apply(sp.first, sp.second); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + innerError(ex); + return; + } + + } else { + break; + } + } + + if (remaining.decrementAndGet() == 0) { + SlotPair<A> sp = current.get(); + current.lazySet(null); + + R result; + try { + result = Objects.requireNonNull(finisher.apply(sp.first), "The finisher returned a null value"); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + innerError(ex); + return; + } + + complete(result); + } + } + } + + static final class ParallelCollectorInnerSubscriber<T, A, R> + extends AtomicReference<Subscription> + implements FlowableSubscriber<T> { + + private static final long serialVersionUID = -7954444275102466525L; + + final ParallelCollectorSubscriber<T, A, R> parent; + + final BiConsumer<A, T> accumulator; + + final BinaryOperator<A> combiner; + + A container; + + boolean done; + + ParallelCollectorInnerSubscriber(ParallelCollectorSubscriber<T, A, R> parent, A container, BiConsumer<A, T> accumulator, BinaryOperator<A> combiner) { + this.parent = parent; + this.accumulator = accumulator; + this.combiner = combiner; + this.container = container; + } + + @Override + public void onSubscribe(Subscription s) { + SubscriptionHelper.setOnce(this, s, Long.MAX_VALUE); + } + + @Override + public void onNext(T t) { + if (!done) { + try { + accumulator.accept(container, t); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + get().cancel(); + onError(ex); + } + } + } + + @Override + public void onError(Throwable t) { + if (done) { + RxJavaPlugins.onError(t); + return; + } + container = null; + done = true; + parent.innerError(t); + } + + @Override + public void onComplete() { + if (!done) { + A v = container; + container = null; + done = true; + parent.innerComplete(v, combiner); + } + } + + void cancel() { + SubscriptionHelper.cancel(this); + } + } + + static final class SlotPair<T> extends AtomicInteger { + + private static final long serialVersionUID = 473971317683868662L; + + T first; + + T second; + + final AtomicInteger releaseIndex = new AtomicInteger(); + + int tryAcquireSlot() { + for (;;) { + int acquired = get(); + if (acquired >= 2) { + return -1; + } + + if (compareAndSet(acquired, acquired + 1)) { + return acquired; + } + } + } + + boolean releaseSlot() { + return releaseIndex.incrementAndGet() == 2; + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/jdk8/ParallelFlatMapStream.java b/src/main/java/io/reactivex/rxjava3/internal/jdk8/ParallelFlatMapStream.java new file mode 100644 index 0000000000..beae3fee36 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/jdk8/ParallelFlatMapStream.java @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.jdk8; + +import java.util.stream.Stream; + +import org.reactivestreams.Subscriber; + +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.parallel.ParallelFlowable; + +/** + * Flattens the generated {@link Stream}s on each rail. + * + * @param <T> the input value type + * @param <R> the output value type + * @since 3.0.0 + */ +public final class ParallelFlatMapStream<T, R> extends ParallelFlowable<R> { + + final ParallelFlowable<T> source; + + final Function<? super T, ? extends Stream<? extends R>> mapper; + + final int prefetch; + + public ParallelFlatMapStream( + ParallelFlowable<T> source, + Function<? super T, ? extends Stream<? extends R>> mapper, + int prefetch) { + this.source = source; + this.mapper = mapper; + this.prefetch = prefetch; + } + + @Override + public int parallelism() { + return source.parallelism(); + } + + @Override + public void subscribe(Subscriber<? super R>[] subscribers) { + if (!validate(subscribers)) { + return; + } + + int n = subscribers.length; + + @SuppressWarnings("unchecked") + final Subscriber<T>[] parents = new Subscriber[n]; + + for (int i = 0; i < n; i++) { + parents[i] = FlowableFlatMapStream.subscribe(subscribers[i], mapper, prefetch); + } + + source.subscribe(parents); + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/jdk8/ParallelMapOptional.java b/src/main/java/io/reactivex/rxjava3/internal/jdk8/ParallelMapOptional.java new file mode 100644 index 0000000000..a31be9a75e --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/jdk8/ParallelMapOptional.java @@ -0,0 +1,236 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.jdk8; + +import java.util.*; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.subscriptions.SubscriptionHelper; +import io.reactivex.rxjava3.operators.ConditionalSubscriber; +import io.reactivex.rxjava3.parallel.ParallelFlowable; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +/** + * Maps each 'rail' of the source ParallelFlowable with a mapper function. + * + * @param <T> the input value type + * @param <R> the output value type + * @since 3.0.0 + */ +public final class ParallelMapOptional<T, R> extends ParallelFlowable<R> { + + final ParallelFlowable<T> source; + + final Function<? super T, Optional<? extends R>> mapper; + + public ParallelMapOptional(ParallelFlowable<T> source, Function<? super T, Optional<? extends R>> mapper) { + this.source = source; + this.mapper = mapper; + } + + @Override + public void subscribe(Subscriber<? super R>[] subscribers) { + if (!validate(subscribers)) { + return; + } + + int n = subscribers.length; + @SuppressWarnings("unchecked") + Subscriber<? super T>[] parents = new Subscriber[n]; + + for (int i = 0; i < n; i++) { + Subscriber<? super R> a = subscribers[i]; + if (a instanceof ConditionalSubscriber) { + parents[i] = new ParallelMapConditionalSubscriber<>((ConditionalSubscriber<? super R>)a, mapper); + } else { + parents[i] = new ParallelMapSubscriber<>(a, mapper); + } + } + + source.subscribe(parents); + } + + @Override + public int parallelism() { + return source.parallelism(); + } + + static final class ParallelMapSubscriber<T, R> implements ConditionalSubscriber<T>, Subscription { + + final Subscriber<? super R> downstream; + + final Function<? super T, Optional<? extends R>> mapper; + + Subscription upstream; + + boolean done; + + ParallelMapSubscriber(Subscriber<? super R> actual, Function<? super T, Optional<? extends R>> mapper) { + this.downstream = actual; + this.mapper = mapper; + } + + @Override + public void request(long n) { + upstream.request(n); + } + + @Override + public void cancel() { + upstream.cancel(); + } + + @Override + public void onSubscribe(Subscription s) { + if (SubscriptionHelper.validate(this.upstream, s)) { + this.upstream = s; + + downstream.onSubscribe(this); + } + } + + @Override + public void onNext(T t) { + if (!tryOnNext(t)) { + upstream.request(1); + } + } + + @Override + public boolean tryOnNext(T t) { + if (done) { + return true; + } + Optional<? extends R> v; + + try { + v = Objects.requireNonNull(mapper.apply(t), "The mapper returned a null Optional"); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + cancel(); + onError(ex); + return true; + } + + if (v.isPresent()) { + downstream.onNext(v.get()); + return true; + } + return false; + } + + @Override + public void onError(Throwable t) { + if (done) { + RxJavaPlugins.onError(t); + return; + } + done = true; + downstream.onError(t); + } + + @Override + public void onComplete() { + if (done) { + return; + } + done = true; + downstream.onComplete(); + } + + } + static final class ParallelMapConditionalSubscriber<T, R> implements ConditionalSubscriber<T>, Subscription { + + final ConditionalSubscriber<? super R> downstream; + + final Function<? super T, Optional<? extends R>> mapper; + + Subscription upstream; + + boolean done; + + ParallelMapConditionalSubscriber(ConditionalSubscriber<? super R> actual, Function<? super T, Optional<? extends R>> mapper) { + this.downstream = actual; + this.mapper = mapper; + } + + @Override + public void request(long n) { + upstream.request(n); + } + + @Override + public void cancel() { + upstream.cancel(); + } + + @Override + public void onSubscribe(Subscription s) { + if (SubscriptionHelper.validate(this.upstream, s)) { + this.upstream = s; + + downstream.onSubscribe(this); + } + } + + @Override + public void onNext(T t) { + if (!tryOnNext(t)) { + upstream.request(1); + } + } + + @Override + public boolean tryOnNext(T t) { + if (done) { + return false; + } + Optional<? extends R> v; + + try { + v = Objects.requireNonNull(mapper.apply(t), "The mapper returned a null value"); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + cancel(); + onError(ex); + return false; + } + + return v.isPresent() && downstream.tryOnNext(v.get()); + } + + @Override + public void onError(Throwable t) { + if (done) { + RxJavaPlugins.onError(t); + return; + } + done = true; + downstream.onError(t); + } + + @Override + public void onComplete() { + if (done) { + return; + } + done = true; + downstream.onComplete(); + } + + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/jdk8/ParallelMapTryOptional.java b/src/main/java/io/reactivex/rxjava3/internal/jdk8/ParallelMapTryOptional.java new file mode 100644 index 0000000000..d463c7bbab --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/jdk8/ParallelMapTryOptional.java @@ -0,0 +1,306 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.jdk8; + +import java.util.*; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.subscriptions.SubscriptionHelper; +import io.reactivex.rxjava3.operators.ConditionalSubscriber; +import io.reactivex.rxjava3.parallel.*; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +/** + * Maps each 'rail' of the source ParallelFlowable with a mapper function + * and handle any failure based on a handler function. + * @param <T> the input value type + * @param <R> the output value type + * @since 3.0.0 + */ +public final class ParallelMapTryOptional<T, R> extends ParallelFlowable<R> { + + final ParallelFlowable<T> source; + + final Function<? super T, Optional<? extends R>> mapper; + + final BiFunction<? super Long, ? super Throwable, ParallelFailureHandling> errorHandler; + + public ParallelMapTryOptional( + ParallelFlowable<T> source, + Function<? super T, Optional<? extends R>> mapper, + BiFunction<? super Long, ? super Throwable, ParallelFailureHandling> errorHandler) { + this.source = source; + this.mapper = mapper; + this.errorHandler = errorHandler; + } + + @Override + public void subscribe(Subscriber<? super R>[] subscribers) { + if (!validate(subscribers)) { + return; + } + + int n = subscribers.length; + @SuppressWarnings("unchecked") + Subscriber<? super T>[] parents = new Subscriber[n]; + + for (int i = 0; i < n; i++) { + Subscriber<? super R> a = subscribers[i]; + if (a instanceof ConditionalSubscriber) { + parents[i] = new ParallelMapTryConditionalSubscriber<>((ConditionalSubscriber<? super R>)a, mapper, errorHandler); + } else { + parents[i] = new ParallelMapTrySubscriber<>(a, mapper, errorHandler); + } + } + + source.subscribe(parents); + } + + @Override + public int parallelism() { + return source.parallelism(); + } + + static final class ParallelMapTrySubscriber<T, R> implements ConditionalSubscriber<T>, Subscription { + + final Subscriber<? super R> downstream; + + final Function<? super T, Optional<? extends R>> mapper; + + final BiFunction<? super Long, ? super Throwable, ParallelFailureHandling> errorHandler; + + Subscription upstream; + + boolean done; + + ParallelMapTrySubscriber(Subscriber<? super R> actual, + Function<? super T, Optional<? extends R>> mapper, + BiFunction<? super Long, ? super Throwable, ParallelFailureHandling> errorHandler) { + this.downstream = actual; + this.mapper = mapper; + this.errorHandler = errorHandler; + } + + @Override + public void request(long n) { + upstream.request(n); + } + + @Override + public void cancel() { + upstream.cancel(); + } + + @Override + public void onSubscribe(Subscription s) { + if (SubscriptionHelper.validate(this.upstream, s)) { + this.upstream = s; + + downstream.onSubscribe(this); + } + } + + @Override + public void onNext(T t) { + if (!tryOnNext(t) && !done) { + upstream.request(1); + } + } + + @Override + public boolean tryOnNext(T t) { + if (done) { + return false; + } + long retries = 0; + + for (;;) { + Optional<? extends R> v; + + try { + v = Objects.requireNonNull(mapper.apply(t), "The mapper returned a null Optional"); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + + ParallelFailureHandling h; + + try { + h = Objects.requireNonNull(errorHandler.apply(++retries, ex), "The errorHandler returned a null ParallelFailureHandling"); + } catch (Throwable exc) { + Exceptions.throwIfFatal(exc); + cancel(); + onError(new CompositeException(ex, exc)); + return false; + } + + switch (h) { + case RETRY: + continue; + case SKIP: + return false; + case STOP: + cancel(); + onComplete(); + return false; + default: + cancel(); + onError(ex); + return false; + } + } + + if (v.isPresent()) { + downstream.onNext(v.get()); + return true; + } + return false; + } + } + + @Override + public void onError(Throwable t) { + if (done) { + RxJavaPlugins.onError(t); + return; + } + done = true; + downstream.onError(t); + } + + @Override + public void onComplete() { + if (done) { + return; + } + done = true; + downstream.onComplete(); + } + + } + static final class ParallelMapTryConditionalSubscriber<T, R> implements ConditionalSubscriber<T>, Subscription { + + final ConditionalSubscriber<? super R> downstream; + + final Function<? super T, Optional<? extends R>> mapper; + + final BiFunction<? super Long, ? super Throwable, ParallelFailureHandling> errorHandler; + + Subscription upstream; + + boolean done; + + ParallelMapTryConditionalSubscriber(ConditionalSubscriber<? super R> actual, + Function<? super T, Optional<? extends R>> mapper, + BiFunction<? super Long, ? super Throwable, ParallelFailureHandling> errorHandler) { + this.downstream = actual; + this.mapper = mapper; + this.errorHandler = errorHandler; + } + + @Override + public void request(long n) { + upstream.request(n); + } + + @Override + public void cancel() { + upstream.cancel(); + } + + @Override + public void onSubscribe(Subscription s) { + if (SubscriptionHelper.validate(this.upstream, s)) { + this.upstream = s; + + downstream.onSubscribe(this); + } + } + + @Override + public void onNext(T t) { + if (!tryOnNext(t) && !done) { + upstream.request(1); + } + } + + @Override + public boolean tryOnNext(T t) { + if (done) { + return false; + } + long retries = 0; + + for (;;) { + Optional<? extends R> v; + + try { + v = Objects.requireNonNull(mapper.apply(t), "The mapper returned a null Optional"); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + + ParallelFailureHandling h; + + try { + h = Objects.requireNonNull(errorHandler.apply(++retries, ex), "The errorHandler returned a null ParallelFailureHandling"); + } catch (Throwable exc) { + Exceptions.throwIfFatal(exc); + cancel(); + onError(new CompositeException(ex, exc)); + return false; + } + + switch (h) { + case RETRY: + continue; + case SKIP: + return false; + case STOP: + cancel(); + onComplete(); + return false; + default: + cancel(); + onError(ex); + return false; + } + } + + return v.isPresent() && downstream.tryOnNext(v.get()); + } + } + + @Override + public void onError(Throwable t) { + if (done) { + RxJavaPlugins.onError(t); + return; + } + done = true; + downstream.onError(t); + } + + @Override + public void onComplete() { + if (done) { + return; + } + done = true; + downstream.onComplete(); + } + + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/jdk8/SingleFlattenStreamAsFlowable.java b/src/main/java/io/reactivex/rxjava3/internal/jdk8/SingleFlattenStreamAsFlowable.java new file mode 100644 index 0000000000..8ed45d40b9 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/jdk8/SingleFlattenStreamAsFlowable.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.jdk8; + +import java.util.stream.Stream; + +import org.reactivestreams.Subscriber; + +import io.reactivex.rxjava3.annotations.NonNull; +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.jdk8.MaybeFlattenStreamAsFlowable.FlattenStreamMultiObserver; + +/** + * Map the success value into a Java {@link Stream} and emits its values. + * + * @param <T> the source value type + * @param <R> the output value type + * @since 3.0.0 + */ +public final class SingleFlattenStreamAsFlowable<T, R> extends Flowable<R> { + + final Single<T> source; + + final Function<? super T, ? extends Stream<? extends R>> mapper; + + public SingleFlattenStreamAsFlowable(Single<T> source, Function<? super T, ? extends Stream<? extends R>> mapper) { + this.source = source; + this.mapper = mapper; + } + + @Override + protected void subscribeActual(@NonNull Subscriber<? super R> s) { + source.subscribe(new FlattenStreamMultiObserver<>(s, mapper)); + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/jdk8/SingleFlattenStreamAsObservable.java b/src/main/java/io/reactivex/rxjava3/internal/jdk8/SingleFlattenStreamAsObservable.java new file mode 100644 index 0000000000..4733474f13 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/jdk8/SingleFlattenStreamAsObservable.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.jdk8; + +import java.util.stream.Stream; + +import io.reactivex.rxjava3.annotations.NonNull; +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.jdk8.MaybeFlattenStreamAsObservable.FlattenStreamMultiObserver; + +/** + * Map the success value into a Java {@link Stream} and emits its values. + * + * @param <T> the source value type + * @param <R> the output value type + * @since 3.0.0 + */ +public final class SingleFlattenStreamAsObservable<T, R> extends Observable<R> { + + final Single<T> source; + + final Function<? super T, ? extends Stream<? extends R>> mapper; + + public SingleFlattenStreamAsObservable(Single<T> source, Function<? super T, ? extends Stream<? extends R>> mapper) { + this.source = source; + this.mapper = mapper; + } + + @Override + protected void subscribeActual(@NonNull Observer<? super R> s) { + source.subscribe(new FlattenStreamMultiObserver<>(s, mapper)); + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/jdk8/SingleFromCompletionStage.java b/src/main/java/io/reactivex/rxjava3/internal/jdk8/SingleFromCompletionStage.java new file mode 100644 index 0000000000..126a096a26 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/jdk8/SingleFromCompletionStage.java @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.jdk8; + +import java.util.concurrent.CompletionStage; +import java.util.function.BiConsumer; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.jdk8.FlowableFromCompletionStage.BiConsumerAtomicReference; + +/** + * Wrap a CompletionStage and signal its outcome. + * @param <T> the element type + * @since 3.0.0 + */ +public final class SingleFromCompletionStage<T> extends Single<T> { + + final CompletionStage<T> stage; + + public SingleFromCompletionStage(CompletionStage<T> stage) { + this.stage = stage; + } + + @Override + protected void subscribeActual(SingleObserver<? super T> observer) { + // We need an indirection because one can't detach from a whenComplete + // and cancellation should not hold onto the stage. + BiConsumerAtomicReference<T> whenReference = new BiConsumerAtomicReference<>(); + CompletionStageHandler<T> handler = new CompletionStageHandler<>(observer, whenReference); + whenReference.lazySet(handler); + + observer.onSubscribe(handler); + stage.whenComplete(whenReference); + } + + static final class CompletionStageHandler<T> + implements Disposable, BiConsumer<T, Throwable> { + + final SingleObserver<? super T> downstream; + + final BiConsumerAtomicReference<T> whenReference; + + CompletionStageHandler(SingleObserver<? super T> downstream, BiConsumerAtomicReference<T> whenReference) { + this.downstream = downstream; + this.whenReference = whenReference; + } + + @Override + public void accept(T item, Throwable error) { + if (error != null) { + downstream.onError(error); + } + else if (item != null) { + downstream.onSuccess(item); + } else { + downstream.onError(new NullPointerException("The CompletionStage terminated with null.")); + } + } + + @Override + public void dispose() { + whenReference.set(null); + } + + @Override + public boolean isDisposed() { + return whenReference.get() == null; + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/jdk8/SingleMapOptional.java b/src/main/java/io/reactivex/rxjava3/internal/jdk8/SingleMapOptional.java new file mode 100644 index 0000000000..ad54068d34 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/jdk8/SingleMapOptional.java @@ -0,0 +1,105 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.jdk8; + +import java.util.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; + +/** + * Maps the success value to an {@link Optional} and emits its non-empty value or completes. + * + * @param <T> the upstream success value type + * @param <R> the result value type + * @since 3.0.0 + */ +public final class SingleMapOptional<T, R> extends Maybe<R> { + + final Single<T> source; + + final Function<? super T, Optional<? extends R>> mapper; + + public SingleMapOptional(Single<T> source, Function<? super T, Optional<? extends R>> mapper) { + this.source = source; + this.mapper = mapper; + } + + @Override + protected void subscribeActual(MaybeObserver<? super R> observer) { + source.subscribe(new MapOptionalSingleObserver<>(observer, mapper)); + } + + static final class MapOptionalSingleObserver<T, R> implements SingleObserver<T>, Disposable { + + final MaybeObserver<? super R> downstream; + + final Function<? super T, Optional<? extends R>> mapper; + + Disposable upstream; + + MapOptionalSingleObserver(MaybeObserver<? super R> downstream, Function<? super T, Optional<? extends R>> mapper) { + this.downstream = downstream; + this.mapper = mapper; + } + + @Override + public void dispose() { + Disposable d = this.upstream; + this.upstream = DisposableHelper.DISPOSED; + d.dispose(); + } + + @Override + public boolean isDisposed() { + return upstream.isDisposed(); + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.validate(this.upstream, d)) { + this.upstream = d; + + downstream.onSubscribe(this); + } + } + + @Override + public void onSuccess(T value) { + Optional<? extends R> v; + + try { + v = Objects.requireNonNull(mapper.apply(value), "The mapper returned a null item"); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + downstream.onError(ex); + return; + } + + if (v.isPresent()) { + downstream.onSuccess(v.get()); + } else { + downstream.onComplete(); + } + } + + @Override + public void onError(Throwable e) { + downstream.onError(e); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/observers/AbstractDisposableAutoRelease.java b/src/main/java/io/reactivex/rxjava3/internal/observers/AbstractDisposableAutoRelease.java new file mode 100644 index 0000000000..e5d89a4502 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/observers/AbstractDisposableAutoRelease.java @@ -0,0 +1,125 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +/* + * Copyright 2016-2019 David Karnok + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.reactivex.rxjava3.internal.observers; + +import java.util.concurrent.atomic.AtomicReference; + +import io.reactivex.rxjava3.disposables.*; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.observers.LambdaConsumerIntrospection; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +/** + * Wraps lambda callbacks and when the upstream terminates or the observer gets disposed, + * removes itself from a {@link io.reactivex.rxjava3.disposables.CompositeDisposable}. + * <p>History: 0.18.0 @ RxJavaExtensions + * @since 3.1.0 + */ +abstract class AbstractDisposableAutoRelease +extends AtomicReference<Disposable> +implements Disposable, LambdaConsumerIntrospection { + + private static final long serialVersionUID = 8924480688481408726L; + + final AtomicReference<DisposableContainer> composite; + + final Consumer<? super Throwable> onError; + + final Action onComplete; + + AbstractDisposableAutoRelease( + DisposableContainer composite, + Consumer<? super Throwable> onError, + Action onComplete + ) { + this.onError = onError; + this.onComplete = onComplete; + this.composite = new AtomicReference<>(composite); + } + + public final void onError(Throwable t) { + if (get() != DisposableHelper.DISPOSED) { + lazySet(DisposableHelper.DISPOSED); + try { + onError.accept(t); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + RxJavaPlugins.onError(new CompositeException(t, e)); + } + } else { + RxJavaPlugins.onError(t); + } + removeSelf(); + } + + public final void onComplete() { + if (get() != DisposableHelper.DISPOSED) { + lazySet(DisposableHelper.DISPOSED); + try { + onComplete.run(); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + RxJavaPlugins.onError(e); + } + } + removeSelf(); + } + + @Override + public final void dispose() { + DisposableHelper.dispose(this); + removeSelf(); + } + + final void removeSelf() { + DisposableContainer c = composite.getAndSet(null); + if (c != null) { + c.delete(this); + } + } + + @Override + public final boolean isDisposed() { + return DisposableHelper.isDisposed(get()); + } + + public final void onSubscribe(Disposable d) { + DisposableHelper.setOnce(this, d); + } + + @Override + public final boolean hasCustomOnError() { + return onError != Functions.ON_ERROR_MISSING; + } + +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/observers/BasicFuseableObserver.java b/src/main/java/io/reactivex/rxjava3/internal/observers/BasicFuseableObserver.java new file mode 100644 index 0000000000..6bd12f3940 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/observers/BasicFuseableObserver.java @@ -0,0 +1,183 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.observers; + +import io.reactivex.rxjava3.core.Observer; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; +import io.reactivex.rxjava3.operators.QueueDisposable; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +/** + * Base class for a fuseable intermediate observer. + * @param <T> the upstream value type + * @param <R> the downstream value type + */ +public abstract class BasicFuseableObserver<T, R> implements Observer<T>, QueueDisposable<R> { + + /** The downstream subscriber. */ + protected final Observer<? super R> downstream; + + /** The upstream subscription. */ + protected Disposable upstream; + + /** The upstream's QueueDisposable if not null. */ + protected QueueDisposable<T> qd; + + /** Flag indicating no further onXXX event should be accepted. */ + protected boolean done; + + /** Holds the established fusion mode of the upstream. */ + protected int sourceMode; + + /** + * Construct a BasicFuseableObserver by wrapping the given subscriber. + * @param downstream the subscriber, not null (not verified) + */ + public BasicFuseableObserver(Observer<? super R> downstream) { + this.downstream = downstream; + } + + // final: fixed protocol steps to support fuseable and non-fuseable upstream + @SuppressWarnings("unchecked") + @Override + public final void onSubscribe(Disposable d) { + if (DisposableHelper.validate(this.upstream, d)) { + + this.upstream = d; + if (d instanceof QueueDisposable) { + this.qd = (QueueDisposable<T>)d; + } + + if (beforeDownstream()) { + + downstream.onSubscribe(this); + + afterDownstream(); + } + + } + } + + /** + * Override this to perform actions before the call {@code actual.onSubscribe(this)} happens. + * @return true if onSubscribe should continue with the call + */ + protected boolean beforeDownstream() { + return true; + } + + /** + * Override this to perform actions after the call to {@code actual.onSubscribe(this)} happened. + */ + protected void afterDownstream() { + // default no-op + } + + // ----------------------------------- + // Convenience and state-aware methods + // ----------------------------------- + + @Override + public void onError(Throwable t) { + if (done) { + RxJavaPlugins.onError(t); + return; + } + done = true; + downstream.onError(t); + } + + /** + * Rethrows the throwable if it is a fatal exception or calls {@link #onError(Throwable)}. + * @param t the throwable to rethrow or signal to the actual subscriber + */ + protected final void fail(Throwable t) { + Exceptions.throwIfFatal(t); + upstream.dispose(); + onError(t); + } + + @Override + public void onComplete() { + if (done) { + return; + } + done = true; + downstream.onComplete(); + } + + /** + * Calls the upstream's QueueDisposable.requestFusion with the mode and + * saves the established mode in {@link #sourceMode} if that mode doesn't + * have the {@link QueueDisposable#BOUNDARY} flag set. + * <p> + * If the upstream doesn't support fusion ({@link #qd} is null), the method + * returns {@link QueueDisposable#NONE}. + * @param mode the fusion mode requested + * @return the established fusion mode + */ + protected final int transitiveBoundaryFusion(int mode) { + QueueDisposable<T> qd = this.qd; + if (qd != null) { + if ((mode & BOUNDARY) == 0) { + int m = qd.requestFusion(mode); + if (m != NONE) { + sourceMode = m; + } + return m; + } + } + return NONE; + } + + // -------------------------------------------------------------- + // Default implementation of the RS and QS protocol (can be overridden) + // -------------------------------------------------------------- + + @Override + public void dispose() { + upstream.dispose(); + } + + @Override + public boolean isDisposed() { + return upstream.isDisposed(); + } + + @Override + public boolean isEmpty() { + return qd.isEmpty(); + } + + @Override + public void clear() { + qd.clear(); + } + + // ----------------------------------------------------------- + // The rest of the Queue interface methods shouldn't be called + // ----------------------------------------------------------- + + @Override + public final boolean offer(R e) { + throw new UnsupportedOperationException("Should not be called!"); + } + + @Override + public final boolean offer(R v1, R v2) { + throw new UnsupportedOperationException("Should not be called!"); + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/observers/BasicIntQueueDisposable.java b/src/main/java/io/reactivex/rxjava3/internal/observers/BasicIntQueueDisposable.java new file mode 100644 index 0000000000..726db3ae96 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/observers/BasicIntQueueDisposable.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.observers; + +import java.util.concurrent.atomic.AtomicInteger; + +import io.reactivex.rxjava3.operators.QueueDisposable; + +/** + * An abstract QueueDisposable implementation, extending an AtomicInteger, + * that defaults all unnecessary Queue methods to throw UnsupportedOperationException. + * @param <T> the output value type + */ +public abstract class BasicIntQueueDisposable<T> +extends AtomicInteger +implements QueueDisposable<T> { + + private static final long serialVersionUID = -1001730202384742097L; + + @Override + public final boolean offer(T e) { + throw new UnsupportedOperationException("Should not be called"); + } + + @Override + public final boolean offer(T v1, T v2) { + throw new UnsupportedOperationException("Should not be called"); + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/observers/BasicQueueDisposable.java b/src/main/java/io/reactivex/rxjava3/internal/observers/BasicQueueDisposable.java new file mode 100644 index 0000000000..0daaf520a5 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/observers/BasicQueueDisposable.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.observers; + +import io.reactivex.rxjava3.operators.QueueDisposable; + +/** + * An abstract QueueDisposable implementation that defaults all + * unnecessary Queue methods to throw UnsupportedOperationException. + * @param <T> the output value type + */ +public abstract class BasicQueueDisposable<T> implements QueueDisposable<T> { + + @Override + public final boolean offer(T e) { + throw new UnsupportedOperationException("Should not be called"); + } + + @Override + public final boolean offer(T v1, T v2) { + throw new UnsupportedOperationException("Should not be called"); + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/observers/BiConsumerSingleObserver.java b/src/main/java/io/reactivex/rxjava3/internal/observers/BiConsumerSingleObserver.java new file mode 100644 index 0000000000..1e9696cb15 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/observers/BiConsumerSingleObserver.java @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.observers; + +import java.util.concurrent.atomic.AtomicReference; + +import io.reactivex.rxjava3.core.SingleObserver; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.BiConsumer; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +public final class BiConsumerSingleObserver<T> +extends AtomicReference<Disposable> +implements SingleObserver<T>, Disposable { + + private static final long serialVersionUID = 4943102778943297569L; + final BiConsumer<? super T, ? super Throwable> onCallback; + + public BiConsumerSingleObserver(BiConsumer<? super T, ? super Throwable> onCallback) { + this.onCallback = onCallback; + } + + @Override + public void onError(Throwable e) { + try { + lazySet(DisposableHelper.DISPOSED); + onCallback.accept(null, e); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + RxJavaPlugins.onError(new CompositeException(e, ex)); + } + } + + @Override + public void onSubscribe(Disposable d) { + DisposableHelper.setOnce(this, d); + } + + @Override + public void onSuccess(T value) { + try { + lazySet(DisposableHelper.DISPOSED); + onCallback.accept(value, null); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + RxJavaPlugins.onError(ex); + } + } + + @Override + public void dispose() { + DisposableHelper.dispose(this); + } + + @Override + public boolean isDisposed() { + return get() == DisposableHelper.DISPOSED; + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/observers/BlockingBaseObserver.java b/src/main/java/io/reactivex/rxjava3/internal/observers/BlockingBaseObserver.java new file mode 100644 index 0000000000..93ce06eedf --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/observers/BlockingBaseObserver.java @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.observers; + +import java.util.concurrent.CountDownLatch; + +import io.reactivex.rxjava3.core.Observer; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.util.*; + +public abstract class BlockingBaseObserver<T> extends CountDownLatch +implements Observer<T>, Disposable { + + T value; + Throwable error; + + Disposable upstream; + + volatile boolean cancelled; + + public BlockingBaseObserver() { + super(1); + } + + @Override + public final void onSubscribe(Disposable d) { + this.upstream = d; + if (cancelled) { + d.dispose(); + } + } + + @Override + public final void onComplete() { + countDown(); + } + + @Override + public final void dispose() { + cancelled = true; + Disposable d = this.upstream; + if (d != null) { + d.dispose(); + } + } + + @Override + public final boolean isDisposed() { + return cancelled; + } + + /** + * Block until the first value arrives and return it, otherwise + * return null for an empty source and rethrow any exception. + * @return the first value or null if the source is empty + */ + public final T blockingGet() { + if (getCount() != 0) { + try { + BlockingHelper.verifyNonBlocking(); + await(); + } catch (InterruptedException ex) { + dispose(); + throw ExceptionHelper.wrapOrThrow(ex); + } + } + + Throwable e = error; + if (e != null) { + throw ExceptionHelper.wrapOrThrow(e); + } + return value; + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/observers/BlockingDisposableMultiObserver.java b/src/main/java/io/reactivex/rxjava3/internal/observers/BlockingDisposableMultiObserver.java new file mode 100644 index 0000000000..a2334c21ba --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/observers/BlockingDisposableMultiObserver.java @@ -0,0 +1,154 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.observers; + +import java.util.concurrent.CountDownLatch; + +import io.reactivex.rxjava3.annotations.NonNull; +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.disposables.*; +import io.reactivex.rxjava3.internal.util.BlockingHelper; + +/** + * Blocks until the upstream terminates and dispatches the outcome to + * the actual observer. + * + * @param <T> the element type of the source + * @since 3.0.0 + */ +public final class BlockingDisposableMultiObserver<T> +extends CountDownLatch +implements MaybeObserver<T>, SingleObserver<T>, CompletableObserver, Disposable { + + T value; + Throwable error; + + final SequentialDisposable upstream; + + public BlockingDisposableMultiObserver() { + super(1); + upstream = new SequentialDisposable(); + } + + @Override + public void dispose() { + upstream.dispose(); + countDown(); + } + + @Override + public boolean isDisposed() { + return upstream.isDisposed(); + } + + @Override + public void onSubscribe(@NonNull Disposable d) { + DisposableHelper.setOnce(upstream, d); + } + + @Override + public void onSuccess(@NonNull T t) { + this.value = t; + upstream.lazySet(Disposable.disposed()); + countDown(); + } + + @Override + public void onError(@NonNull Throwable e) { + this.error = e; + upstream.lazySet(Disposable.disposed()); + countDown(); + } + + @Override + public void onComplete() { + upstream.lazySet(Disposable.disposed()); + countDown(); + } + + public void blockingConsume(CompletableObserver observer) { + if (getCount() != 0) { + try { + BlockingHelper.verifyNonBlocking(); + await(); + } catch (InterruptedException ex) { + dispose(); + observer.onError(ex); + return; + } + } + if (isDisposed()) { + return; + } + + Throwable ex = error; + if (ex != null) { + observer.onError(ex); + } else { + observer.onComplete(); + } + } + + public void blockingConsume(SingleObserver<? super T> observer) { + if (getCount() != 0) { + try { + BlockingHelper.verifyNonBlocking(); + await(); + } catch (InterruptedException ex) { + dispose(); + observer.onError(ex); + return; + } + } + if (isDisposed()) { + return; + } + + Throwable ex = error; + if (ex != null) { + observer.onError(ex); + } else { + observer.onSuccess(value); + } + } + + public void blockingConsume(MaybeObserver<? super T> observer) { + if (getCount() != 0) { + try { + BlockingHelper.verifyNonBlocking(); + await(); + } catch (InterruptedException ex) { + dispose(); + observer.onError(ex); + return; + } + } + if (isDisposed()) { + return; + } + + Throwable ex = error; + if (ex != null) { + observer.onError(ex); + } else { + T v = value; + if (v == null) { + observer.onComplete(); + } else { + observer.onSuccess(v); + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/observers/BlockingFirstObserver.java b/src/main/java/io/reactivex/rxjava3/internal/observers/BlockingFirstObserver.java new file mode 100644 index 0000000000..570000a55a --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/observers/BlockingFirstObserver.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.observers; + +/** + * Blocks until the upstream signals its first value or completes. + * + * @param <T> the value type + */ +public final class BlockingFirstObserver<T> extends BlockingBaseObserver<T> { + + @Override + public void onNext(T t) { + if (value == null) { + value = t; + upstream.dispose(); + countDown(); + } + } + + @Override + public void onError(Throwable t) { + if (value == null) { + error = t; + } + countDown(); + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/observers/BlockingLastObserver.java b/src/main/java/io/reactivex/rxjava3/internal/observers/BlockingLastObserver.java new file mode 100644 index 0000000000..c8e979cc31 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/observers/BlockingLastObserver.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.observers; + +/** + * Blocks until the upstream signals its last value or completes. + * + * @param <T> the value type + */ +public final class BlockingLastObserver<T> extends BlockingBaseObserver<T> { + + @Override + public void onNext(T t) { + value = t; + } + + @Override + public void onError(Throwable t) { + value = null; + error = t; + countDown(); + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/observers/BlockingMultiObserver.java b/src/main/java/io/reactivex/rxjava3/internal/observers/BlockingMultiObserver.java new file mode 100644 index 0000000000..458e6db5a1 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/observers/BlockingMultiObserver.java @@ -0,0 +1,184 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.observers; + +import java.util.concurrent.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.util.*; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +/** + * A combined Observer that awaits the success or error signal via a CountDownLatch. + * @param <T> the value type + */ +public final class BlockingMultiObserver<T> +extends CountDownLatch +implements SingleObserver<T>, CompletableObserver, MaybeObserver<T> { + + T value; + Throwable error; + + Disposable upstream; + + volatile boolean cancelled; + + public BlockingMultiObserver() { + super(1); + } + + void dispose() { + cancelled = true; + Disposable d = this.upstream; + if (d != null) { + d.dispose(); + } + } + + @Override + public void onSubscribe(Disposable d) { + this.upstream = d; + if (cancelled) { + d.dispose(); + } + } + + @Override + public void onSuccess(T value) { + this.value = value; + countDown(); + } + + @Override + public void onError(Throwable e) { + error = e; + countDown(); + } + + @Override + public void onComplete() { + countDown(); + } + + /** + * Block until the latch is counted down then rethrow any exception received (wrapped if checked) + * or return the received value (null if none). + * @return the value received or null if no value received + */ + public T blockingGet() { + if (getCount() != 0) { + try { + BlockingHelper.verifyNonBlocking(); + await(); + } catch (InterruptedException ex) { + dispose(); + throw ExceptionHelper.wrapOrThrow(ex); + } + } + Throwable ex = error; + if (ex != null) { + throw ExceptionHelper.wrapOrThrow(ex); + } + return value; + } + + /** + * Block until the latch is counted down then rethrow any exception received (wrapped if checked) + * or return the received value (the defaultValue if none). + * @param defaultValue the default value to return if no value was received + * @return the value received or defaultValue if no value received + */ + public T blockingGet(T defaultValue) { + if (getCount() != 0) { + try { + BlockingHelper.verifyNonBlocking(); + await(); + } catch (InterruptedException ex) { + dispose(); + throw ExceptionHelper.wrapOrThrow(ex); + } + } + Throwable ex = error; + if (ex != null) { + throw ExceptionHelper.wrapOrThrow(ex); + } + T v = value; + return v != null ? v : defaultValue; + } + + /** + * Block until the observer terminates and return true; return false if + * the wait times out. + * @param timeout the timeout value + * @param unit the time unit + * @return true if the observer terminated in time, false otherwise + */ + public boolean blockingAwait(long timeout, TimeUnit unit) { + if (getCount() != 0) { + try { + BlockingHelper.verifyNonBlocking(); + if (!await(timeout, unit)) { + dispose(); + return false; + } + } catch (InterruptedException ex) { + dispose(); + throw ExceptionHelper.wrapOrThrow(ex); + } + } + Throwable ex = error; + if (ex != null) { + throw ExceptionHelper.wrapOrThrow(ex); + } + return true; + } + + /** + * Blocks until the source completes and calls the appropriate callback. + * @param onSuccess for a succeeding source + * @param onError for a failing source + * @param onComplete for an empty source + */ + public void blockingConsume(Consumer<? super T> onSuccess, Consumer<? super Throwable> onError, Action onComplete) { + try { + if (getCount() != 0) { + try { + BlockingHelper.verifyNonBlocking(); + await(); + } catch (InterruptedException ex) { + dispose(); + onError.accept(ex); + return; + } + } + Throwable ex = error; + if (ex != null) { + onError.accept(ex); + return; + } + T v = value; + if (v != null) { + onSuccess.accept(v); + } else { + onComplete.run(); + } + } catch (Throwable t) { + Exceptions.throwIfFatal(t); + RxJavaPlugins.onError(t); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/observers/BlockingObserver.java b/src/main/java/io/reactivex/rxjava3/internal/observers/BlockingObserver.java new file mode 100644 index 0000000000..ff84f2a4c6 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/observers/BlockingObserver.java @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.observers; + +import java.util.Queue; +import java.util.concurrent.atomic.AtomicReference; + +import io.reactivex.rxjava3.core.Observer; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; +import io.reactivex.rxjava3.internal.util.NotificationLite; + +public final class BlockingObserver<T> extends AtomicReference<Disposable> implements Observer<T>, Disposable { + + private static final long serialVersionUID = -4875965440900746268L; + + public static final Object TERMINATED = new Object(); + + final Queue<Object> queue; + + public BlockingObserver(Queue<Object> queue) { + this.queue = queue; + } + + @Override + public void onSubscribe(Disposable d) { + DisposableHelper.setOnce(this, d); + } + + @Override + public void onNext(T t) { + queue.offer(NotificationLite.next(t)); + } + + @Override + public void onError(Throwable t) { + queue.offer(NotificationLite.error(t)); + } + + @Override + public void onComplete() { + queue.offer(NotificationLite.complete()); + } + + @Override + public void dispose() { + if (DisposableHelper.dispose(this)) { + queue.offer(TERMINATED); + } + } + + @Override + public boolean isDisposed() { + return get() == DisposableHelper.DISPOSED; + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/observers/CallbackCompletableObserver.java b/src/main/java/io/reactivex/rxjava3/internal/observers/CallbackCompletableObserver.java new file mode 100644 index 0000000000..be2da69160 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/observers/CallbackCompletableObserver.java @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.observers; + +import java.util.concurrent.atomic.AtomicReference; + +import io.reactivex.rxjava3.core.CompletableObserver; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.observers.LambdaConsumerIntrospection; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +public final class CallbackCompletableObserver +extends AtomicReference<Disposable> + implements CompletableObserver, Disposable, LambdaConsumerIntrospection { + + private static final long serialVersionUID = -4361286194466301354L; + + final Consumer<? super Throwable> onError; + final Action onComplete; + + public CallbackCompletableObserver(Consumer<? super Throwable> onError, Action onComplete) { + this.onError = onError; + this.onComplete = onComplete; + } + + @Override + public void onComplete() { + try { + onComplete.run(); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + RxJavaPlugins.onError(ex); + } + lazySet(DisposableHelper.DISPOSED); + } + + @Override + public void onError(Throwable e) { + try { + onError.accept(e); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + RxJavaPlugins.onError(ex); + } + lazySet(DisposableHelper.DISPOSED); + } + + @Override + public void onSubscribe(Disposable d) { + DisposableHelper.setOnce(this, d); + } + + @Override + public void dispose() { + DisposableHelper.dispose(this); + } + + @Override + public boolean isDisposed() { + return get() == DisposableHelper.DISPOSED; + } + + @Override + public boolean hasCustomOnError() { + return onError != Functions.ON_ERROR_MISSING; + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/observers/ConsumerSingleObserver.java b/src/main/java/io/reactivex/rxjava3/internal/observers/ConsumerSingleObserver.java new file mode 100644 index 0000000000..eff6e4a0c1 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/observers/ConsumerSingleObserver.java @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.observers; + +import java.util.concurrent.atomic.AtomicReference; + +import io.reactivex.rxjava3.core.SingleObserver; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.Consumer; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.observers.LambdaConsumerIntrospection; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +public final class ConsumerSingleObserver<T> +extends AtomicReference<Disposable> +implements SingleObserver<T>, Disposable, LambdaConsumerIntrospection { + + private static final long serialVersionUID = -7012088219455310787L; + + final Consumer<? super T> onSuccess; + + final Consumer<? super Throwable> onError; + + public ConsumerSingleObserver(Consumer<? super T> onSuccess, Consumer<? super Throwable> onError) { + this.onSuccess = onSuccess; + this.onError = onError; + } + + @Override + public void onError(Throwable e) { + lazySet(DisposableHelper.DISPOSED); + try { + onError.accept(e); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + RxJavaPlugins.onError(new CompositeException(e, ex)); + } + } + + @Override + public void onSubscribe(Disposable d) { + DisposableHelper.setOnce(this, d); + } + + @Override + public void onSuccess(T value) { + lazySet(DisposableHelper.DISPOSED); + try { + onSuccess.accept(value); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + RxJavaPlugins.onError(ex); + } + } + + @Override + public void dispose() { + DisposableHelper.dispose(this); + } + + @Override + public boolean isDisposed() { + return get() == DisposableHelper.DISPOSED; + } + + @Override + public boolean hasCustomOnError() { + return onError != Functions.ON_ERROR_MISSING; + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/observers/DeferredScalarDisposable.java b/src/main/java/io/reactivex/rxjava3/internal/observers/DeferredScalarDisposable.java new file mode 100644 index 0000000000..084ad68571 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/observers/DeferredScalarDisposable.java @@ -0,0 +1,157 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.observers; + +import io.reactivex.rxjava3.annotations.Nullable; +import io.reactivex.rxjava3.core.Observer; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +/** + * Represents a fuseable container for a single value. + * + * @param <T> the value type received and emitted + */ +public class DeferredScalarDisposable<T> extends BasicIntQueueDisposable<T> { + + private static final long serialVersionUID = -5502432239815349361L; + + /** The target of the events. */ + protected final Observer<? super T> downstream; + + /** The value stored temporarily when in fusion mode. */ + protected T value; + + /** Indicates there was a call to complete(T). */ + static final int TERMINATED = 2; + + /** Indicates the Disposable has been disposed. */ + static final int DISPOSED = 4; + + /** Indicates this Disposable is in fusion mode and is currently empty. */ + static final int FUSED_EMPTY = 8; + /** Indicates this Disposable is in fusion mode and has a value. */ + static final int FUSED_READY = 16; + /** Indicates this Disposable is in fusion mode and its value has been consumed. */ + static final int FUSED_CONSUMED = 32; + + /** + * Constructs a DeferredScalarDisposable by wrapping the Observer. + * @param downstream the Observer to wrap, not null (not verified) + */ + public DeferredScalarDisposable(Observer<? super T> downstream) { + this.downstream = downstream; + } + + @Override + public final int requestFusion(int mode) { + if ((mode & ASYNC) != 0) { + lazySet(FUSED_EMPTY); + return ASYNC; + } + return NONE; + } + + /** + * Complete the target with a single value or indicate there is a value available in + * fusion mode. + * @param value the value to signal, not null (not verified) + */ + public final void complete(T value) { + int state = get(); + if ((state & (FUSED_READY | FUSED_CONSUMED | TERMINATED | DISPOSED)) != 0) { + return; + } + Observer<? super T> a = downstream; + if (state == FUSED_EMPTY) { + this.value = value; + lazySet(FUSED_READY); + a.onNext(null); + } else { + lazySet(TERMINATED); + a.onNext(value); + } + if (get() != DISPOSED) { + a.onComplete(); + } + } + + /** + * Complete the target with an error signal. + * @param t the Throwable to signal, not null (not verified) + */ + public final void error(Throwable t) { + int state = get(); + if ((state & (FUSED_READY | FUSED_CONSUMED | TERMINATED | DISPOSED)) != 0) { + RxJavaPlugins.onError(t); + return; + } + lazySet(TERMINATED); + downstream.onError(t); + } + + /** + * Complete the target without any value. + */ + public final void complete() { + int state = get(); + if ((state & (FUSED_READY | FUSED_CONSUMED | TERMINATED | DISPOSED)) != 0) { + return; + } + lazySet(TERMINATED); + downstream.onComplete(); + } + + @Nullable + @Override + public final T poll() { + if (get() == FUSED_READY) { + T v = value; + value = null; + lazySet(FUSED_CONSUMED); + return v; + } + return null; + } + + @Override + public final boolean isEmpty() { + return get() != FUSED_READY; + } + + @Override + public final void clear() { + lazySet(FUSED_CONSUMED); + value = null; + } + + @Override + public void dispose() { + set(DISPOSED); + value = null; + } + + /** + * Try disposing this Disposable and return true if the current thread succeeded. + * @return true if the current thread succeeded + */ + public final boolean tryDispose() { + return getAndSet(DISPOSED) != DISPOSED; + } + + @Override + public final boolean isDisposed() { + return get() == DISPOSED; + } + +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/observers/DeferredScalarObserver.java b/src/main/java/io/reactivex/rxjava3/internal/observers/DeferredScalarObserver.java new file mode 100644 index 0000000000..4453e0ffa8 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/observers/DeferredScalarObserver.java @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.observers; + +import io.reactivex.rxjava3.core.Observer; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; + +/** + * A fuseable Observer that can generate 0 or 1 resulting value. + * @param <T> the input value type + * @param <R> the output value type + */ +public abstract class DeferredScalarObserver<T, R> +extends DeferredScalarDisposable<R> +implements Observer<T> { + + private static final long serialVersionUID = -266195175408988651L; + + /** The upstream disposable. */ + protected Disposable upstream; + + /** + * Creates a DeferredScalarObserver instance and wraps a downstream Observer. + * @param downstream the downstream subscriber, not null (not verified) + */ + public DeferredScalarObserver(Observer<? super R> downstream) { + super(downstream); + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.validate(this.upstream, d)) { + this.upstream = d; + + downstream.onSubscribe(this); + } + } + + @Override + public void onError(Throwable t) { + value = null; + error(t); + } + + @Override + public void onComplete() { + R v = value; + if (v != null) { + value = null; + complete(v); + } else { + complete(); + } + } + + @Override + public void dispose() { + super.dispose(); + upstream.dispose(); + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/observers/DisposableAutoReleaseMultiObserver.java b/src/main/java/io/reactivex/rxjava3/internal/observers/DisposableAutoReleaseMultiObserver.java new file mode 100644 index 0000000000..4779ff5723 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/observers/DisposableAutoReleaseMultiObserver.java @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +/* + * Copyright 2016-2019 David Karnok + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.reactivex.rxjava3.internal.observers; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.DisposableContainer; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +/** + * Wraps lambda callbacks and when the upstream terminates or this (Single | Maybe | Completable) + * observer gets disposed, removes itself from a {@link io.reactivex.rxjava3.disposables.CompositeDisposable}. + * <p>History: 0.18.0 @ RxJavaExtensions + * @param <T> the element type consumed + * @since 3.1.0 + */ +public final class DisposableAutoReleaseMultiObserver<T> +extends AbstractDisposableAutoRelease +implements SingleObserver<T>, MaybeObserver<T>, CompletableObserver { + + private static final long serialVersionUID = 8924480688481408726L; + + final Consumer<? super T> onSuccess; + + public DisposableAutoReleaseMultiObserver( + DisposableContainer composite, + Consumer<? super T> onSuccess, + Consumer<? super Throwable> onError, + Action onComplete + ) { + super(composite, onError, onComplete); + this.onSuccess = onSuccess; + } + + @Override + public void onSuccess(T t) { + if (get() != DisposableHelper.DISPOSED) { + lazySet(DisposableHelper.DISPOSED); + try { + onSuccess.accept(t); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + RxJavaPlugins.onError(e); + } + } + removeSelf(); + } + +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/observers/DisposableAutoReleaseObserver.java b/src/main/java/io/reactivex/rxjava3/internal/observers/DisposableAutoReleaseObserver.java new file mode 100644 index 0000000000..89435c96ed --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/observers/DisposableAutoReleaseObserver.java @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +/* + * Copyright 2016-2019 David Karnok + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.reactivex.rxjava3.internal.observers; + +import io.reactivex.rxjava3.core.Observer; +import io.reactivex.rxjava3.disposables.DisposableContainer; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; + +/** + * Wraps lambda callbacks and when the upstream terminates or this observer gets disposed, + * removes itself from a {@link io.reactivex.rxjava3.disposables.CompositeDisposable}. + * <p>History: 0.18.0 @ RxJavaExtensions + * @param <T> the element type consumed + * @since 3.1.0 + */ +public final class DisposableAutoReleaseObserver<T> +extends AbstractDisposableAutoRelease +implements Observer<T> { + + private static final long serialVersionUID = 8924480688481408726L; + + final Consumer<? super T> onNext; + + public DisposableAutoReleaseObserver( + DisposableContainer composite, + Consumer<? super T> onNext, + Consumer<? super Throwable> onError, + Action onComplete + ) { + super(composite, onError, onComplete); + this.onNext = onNext; + } + + @Override + public void onNext(T t) { + if (get() != DisposableHelper.DISPOSED) { + try { + onNext.accept(t); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + get().dispose(); + onError(e); + } + } + } + +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/observers/DisposableLambdaObserver.java b/src/main/java/io/reactivex/rxjava3/internal/observers/DisposableLambdaObserver.java new file mode 100644 index 0000000000..529d651c67 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/observers/DisposableLambdaObserver.java @@ -0,0 +1,98 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.observers; + +import io.reactivex.rxjava3.core.Observer; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.disposables.*; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +public final class DisposableLambdaObserver<T> implements Observer<T>, Disposable { + final Observer<? super T> downstream; + final Consumer<? super Disposable> onSubscribe; + final Action onDispose; + + Disposable upstream; + + public DisposableLambdaObserver(Observer<? super T> actual, + Consumer<? super Disposable> onSubscribe, + Action onDispose) { + this.downstream = actual; + this.onSubscribe = onSubscribe; + this.onDispose = onDispose; + } + + @Override + public void onSubscribe(Disposable d) { + // this way, multiple calls to onSubscribe can show up in tests that use doOnSubscribe to validate behavior + try { + onSubscribe.accept(d); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + d.dispose(); + this.upstream = DisposableHelper.DISPOSED; + EmptyDisposable.error(e, downstream); + return; + } + if (DisposableHelper.validate(this.upstream, d)) { + this.upstream = d; + downstream.onSubscribe(this); + } + } + + @Override + public void onNext(T t) { + downstream.onNext(t); + } + + @Override + public void onError(Throwable t) { + if (upstream != DisposableHelper.DISPOSED) { + upstream = DisposableHelper.DISPOSED; + downstream.onError(t); + } else { + RxJavaPlugins.onError(t); + } + } + + @Override + public void onComplete() { + if (upstream != DisposableHelper.DISPOSED) { + upstream = DisposableHelper.DISPOSED; + downstream.onComplete(); + } + } + + @Override + public void dispose() { + Disposable d = upstream; + if (d != DisposableHelper.DISPOSED) { + upstream = DisposableHelper.DISPOSED; + try { + onDispose.run(); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + RxJavaPlugins.onError(e); + } + d.dispose(); + } + } + + @Override + public boolean isDisposed() { + return upstream.isDisposed(); + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/observers/EmptyCompletableObserver.java b/src/main/java/io/reactivex/rxjava3/internal/observers/EmptyCompletableObserver.java new file mode 100644 index 0000000000..e92893b9bd --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/observers/EmptyCompletableObserver.java @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.observers; + +import java.util.concurrent.atomic.AtomicReference; + +import io.reactivex.rxjava3.core.CompletableObserver; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.OnErrorNotImplementedException; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; +import io.reactivex.rxjava3.observers.LambdaConsumerIntrospection; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +public final class EmptyCompletableObserver +extends AtomicReference<Disposable> +implements CompletableObserver, Disposable, LambdaConsumerIntrospection { + + private static final long serialVersionUID = -7545121636549663526L; + + @Override + public void dispose() { + DisposableHelper.dispose(this); + } + + @Override + public boolean isDisposed() { + return get() == DisposableHelper.DISPOSED; + } + + @Override + public void onComplete() { + // no-op + lazySet(DisposableHelper.DISPOSED); + } + + @Override + public void onError(Throwable e) { + lazySet(DisposableHelper.DISPOSED); + RxJavaPlugins.onError(new OnErrorNotImplementedException(e)); + } + + @Override + public void onSubscribe(Disposable d) { + DisposableHelper.setOnce(this, d); + } + + @Override + public boolean hasCustomOnError() { + return false; + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/observers/ForEachWhileObserver.java b/src/main/java/io/reactivex/rxjava3/internal/observers/ForEachWhileObserver.java new file mode 100644 index 0000000000..bed185523f --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/observers/ForEachWhileObserver.java @@ -0,0 +1,111 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.observers; + +import java.util.concurrent.atomic.AtomicReference; + +import io.reactivex.rxjava3.core.Observer; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +public final class ForEachWhileObserver<T> +extends AtomicReference<Disposable> +implements Observer<T>, Disposable { + + private static final long serialVersionUID = -4403180040475402120L; + + final Predicate<? super T> onNext; + + final Consumer<? super Throwable> onError; + + final Action onComplete; + + boolean done; + + public ForEachWhileObserver(Predicate<? super T> onNext, + Consumer<? super Throwable> onError, Action onComplete) { + this.onNext = onNext; + this.onError = onError; + this.onComplete = onComplete; + } + + @Override + public void onSubscribe(Disposable d) { + DisposableHelper.setOnce(this, d); + } + + @Override + public void onNext(T t) { + if (done) { + return; + } + + boolean b; + try { + b = onNext.test(t); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + dispose(); + onError(ex); + return; + } + + if (!b) { + dispose(); + onComplete(); + } + } + + @Override + public void onError(Throwable t) { + if (done) { + RxJavaPlugins.onError(t); + return; + } + done = true; + try { + onError.accept(t); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + RxJavaPlugins.onError(new CompositeException(t, ex)); + } + } + + @Override + public void onComplete() { + if (done) { + return; + } + done = true; + try { + onComplete.run(); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + RxJavaPlugins.onError(ex); + } + } + + @Override + public void dispose() { + DisposableHelper.dispose(this); + } + + @Override + public boolean isDisposed() { + return DisposableHelper.isDisposed(this.get()); + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/observers/FutureMultiObserver.java b/src/main/java/io/reactivex/rxjava3/internal/observers/FutureMultiObserver.java new file mode 100644 index 0000000000..f109aaa5a3 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/observers/FutureMultiObserver.java @@ -0,0 +1,163 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.observers; + +import static io.reactivex.rxjava3.internal.util.ExceptionHelper.timeoutMessage; + +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicReference; + +import io.reactivex.rxjava3.annotations.NonNull; +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; +import io.reactivex.rxjava3.internal.util.BlockingHelper; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +/** + * An Observer + Future that expects exactly one upstream value and provides it + * via the (blocking) Future API. + * + * @param <T> the value type + */ +public final class FutureMultiObserver<T> extends CountDownLatch +implements MaybeObserver<T>, SingleObserver<T>, CompletableObserver, Future<T>, Disposable { + + T value; + Throwable error; + + final AtomicReference<Disposable> upstream; + + public FutureMultiObserver() { + super(1); + this.upstream = new AtomicReference<>(); + } + + @Override + public boolean cancel(boolean mayInterruptIfRunning) { + for (;;) { + Disposable a = upstream.get(); + if (a == this || a == DisposableHelper.DISPOSED) { + return false; + } + + if (upstream.compareAndSet(a, DisposableHelper.DISPOSED)) { + if (a != null) { + a.dispose(); + } + countDown(); + return true; + } + } + } + + @Override + public boolean isCancelled() { + return DisposableHelper.isDisposed(upstream.get()); + } + + @Override + public boolean isDone() { + return getCount() == 0; + } + + @Override + public T get() throws InterruptedException, ExecutionException { + if (getCount() != 0) { + BlockingHelper.verifyNonBlocking(); + await(); + } + + if (isCancelled()) { + throw new CancellationException(); + } + Throwable ex = error; + if (ex != null) { + throw new ExecutionException(ex); + } + return value; + } + + @Override + public T get(long timeout, @NonNull TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException { + if (getCount() != 0) { + BlockingHelper.verifyNonBlocking(); + if (!await(timeout, unit)) { + throw new TimeoutException(timeoutMessage(timeout, unit)); + } + } + + if (isCancelled()) { + throw new CancellationException(); + } + + Throwable ex = error; + if (ex != null) { + throw new ExecutionException(ex); + } + return value; + } + + @Override + public void onSubscribe(Disposable d) { + DisposableHelper.setOnce(this.upstream, d); + } + + @Override + public void onSuccess(T t) { + Disposable a = upstream.get(); + if (a == DisposableHelper.DISPOSED) { + return; + } + value = t; + upstream.compareAndSet(a, this); + countDown(); + } + + @Override + public void onError(Throwable t) { + for (;;) { + Disposable a = upstream.get(); + if (a == DisposableHelper.DISPOSED) { + RxJavaPlugins.onError(t); + return; + } + error = t; + if (upstream.compareAndSet(a, this)) { + countDown(); + return; + } + } + } + + @Override + public void onComplete() { + Disposable a = upstream.get(); + if (a == DisposableHelper.DISPOSED) { + return; + } + upstream.compareAndSet(a, this); + countDown(); + } + + @Override + public void dispose() { + // ignoring as `this` means a finished Disposable only + } + + @Override + public boolean isDisposed() { + return isDone(); + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/observers/FutureObserver.java b/src/main/java/io/reactivex/rxjava3/internal/observers/FutureObserver.java new file mode 100644 index 0000000000..13fd2dc9bf --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/observers/FutureObserver.java @@ -0,0 +1,166 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.observers; + +import static io.reactivex.rxjava3.internal.util.ExceptionHelper.timeoutMessage; + +import java.util.NoSuchElementException; +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicReference; + +import io.reactivex.rxjava3.annotations.NonNull; +import io.reactivex.rxjava3.core.Observer; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; +import io.reactivex.rxjava3.internal.util.BlockingHelper; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +/** + * An Observer + Future that expects exactly one upstream value and provides it + * via the (blocking) Future API. + * + * @param <T> the value type + */ +public final class FutureObserver<T> extends CountDownLatch +implements Observer<T>, Future<T>, Disposable { + + T value; + Throwable error; + + final AtomicReference<Disposable> upstream; + + public FutureObserver() { + super(1); + this.upstream = new AtomicReference<>(); + } + + @Override + public boolean cancel(boolean mayInterruptIfRunning) { + for (;;) { + Disposable a = upstream.get(); + if (a == this || a == DisposableHelper.DISPOSED) { + return false; + } + + if (upstream.compareAndSet(a, DisposableHelper.DISPOSED)) { + if (a != null) { + a.dispose(); + } + countDown(); + return true; + } + } + } + + @Override + public boolean isCancelled() { + return DisposableHelper.isDisposed(upstream.get()); + } + + @Override + public boolean isDone() { + return getCount() == 0; + } + + @Override + public T get() throws InterruptedException, ExecutionException { + if (getCount() != 0) { + BlockingHelper.verifyNonBlocking(); + await(); + } + + if (isCancelled()) { + throw new CancellationException(); + } + Throwable ex = error; + if (ex != null) { + throw new ExecutionException(ex); + } + return value; + } + + @Override + public T get(long timeout, @NonNull TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException { + if (getCount() != 0) { + BlockingHelper.verifyNonBlocking(); + if (!await(timeout, unit)) { + throw new TimeoutException(timeoutMessage(timeout, unit)); + } + } + + if (isCancelled()) { + throw new CancellationException(); + } + + Throwable ex = error; + if (ex != null) { + throw new ExecutionException(ex); + } + return value; + } + + @Override + public void onSubscribe(Disposable d) { + DisposableHelper.setOnce(this.upstream, d); + } + + @Override + public void onNext(T t) { + if (value != null) { + upstream.get().dispose(); + onError(new IndexOutOfBoundsException("More than one element received")); + return; + } + value = t; + } + + @Override + public void onError(Throwable t) { + if (error == null) { + Disposable a = upstream.get(); + if (a != this && a != DisposableHelper.DISPOSED + && upstream.compareAndSet(a, this)) { + error = t; + countDown(); + return; + } + } + RxJavaPlugins.onError(t); + } + + @Override + public void onComplete() { + if (value == null) { + onError(new NoSuchElementException("The source is empty")); + return; + } + Disposable a = upstream.get(); + if (a == this || a == DisposableHelper.DISPOSED) { + return; + } + if (upstream.compareAndSet(a, this)) { + countDown(); + } + } + + @Override + public void dispose() { + // ignoring as `this` means a finished Disposable only + } + + @Override + public boolean isDisposed() { + return isDone(); + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/observers/InnerQueuedObserver.java b/src/main/java/io/reactivex/rxjava3/internal/observers/InnerQueuedObserver.java new file mode 100644 index 0000000000..9d6417c0e8 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/observers/InnerQueuedObserver.java @@ -0,0 +1,119 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.observers; + +import java.util.concurrent.atomic.AtomicReference; + +import io.reactivex.rxjava3.core.Observer; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; +import io.reactivex.rxjava3.internal.util.QueueDrainHelper; +import io.reactivex.rxjava3.operators.QueueDisposable; +import io.reactivex.rxjava3.operators.QueueSubscription; +import io.reactivex.rxjava3.operators.SimpleQueue; + +/** + * Subscriber that can fuse with the upstream and calls a support interface + * whenever an event is available. + * + * @param <T> the value type + */ +public final class InnerQueuedObserver<T> +extends AtomicReference<Disposable> +implements Observer<T>, Disposable { + + private static final long serialVersionUID = -5417183359794346637L; + + final InnerQueuedObserverSupport<T> parent; + + final int prefetch; + + SimpleQueue<T> queue; + + volatile boolean done; + + int fusionMode; + + public InnerQueuedObserver(InnerQueuedObserverSupport<T> parent, int prefetch) { + this.parent = parent; + this.prefetch = prefetch; + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.setOnce(this, d)) { + if (d instanceof QueueDisposable) { + @SuppressWarnings("unchecked") + QueueDisposable<T> qd = (QueueDisposable<T>) d; + + int m = qd.requestFusion(QueueDisposable.ANY); + if (m == QueueSubscription.SYNC) { + fusionMode = m; + queue = qd; + done = true; + parent.innerComplete(this); + return; + } + if (m == QueueDisposable.ASYNC) { + fusionMode = m; + queue = qd; + return; + } + } + + queue = QueueDrainHelper.createQueue(-prefetch); + } + } + + @Override + public void onNext(T t) { + if (fusionMode == QueueDisposable.NONE) { + parent.innerNext(this, t); + } else { + parent.drain(); + } + } + + @Override + public void onError(Throwable t) { + parent.innerError(this, t); + } + + @Override + public void onComplete() { + parent.innerComplete(this); + } + + @Override + public void dispose() { + DisposableHelper.dispose(this); + } + + @Override + public boolean isDisposed() { + return DisposableHelper.isDisposed(get()); + } + + public boolean isDone() { + return done; + } + + public void setDone() { + this.done = true; + } + + public SimpleQueue<T> queue() { + return queue; + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/observers/InnerQueuedObserverSupport.java b/src/main/java/io/reactivex/rxjava3/internal/observers/InnerQueuedObserverSupport.java new file mode 100644 index 0000000000..01bebaf007 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/observers/InnerQueuedObserverSupport.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.observers; + +/** + * Interface to allow the InnerQueuedSubscriber to call back a parent + * with signals. + * + * @param <T> the value type + */ +public interface InnerQueuedObserverSupport<T> { + + void innerNext(InnerQueuedObserver<T> inner, T value); + + void innerError(InnerQueuedObserver<T> inner, Throwable e); + + void innerComplete(InnerQueuedObserver<T> inner); + + void drain(); +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/observers/LambdaObserver.java b/src/main/java/io/reactivex/rxjava3/internal/observers/LambdaObserver.java new file mode 100644 index 0000000000..6bd5dece94 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/observers/LambdaObserver.java @@ -0,0 +1,114 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.observers; + +import java.util.concurrent.atomic.AtomicReference; + +import io.reactivex.rxjava3.core.Observer; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.observers.LambdaConsumerIntrospection; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +public final class LambdaObserver<T> extends AtomicReference<Disposable> + implements Observer<T>, Disposable, LambdaConsumerIntrospection { + + private static final long serialVersionUID = -7251123623727029452L; + final Consumer<? super T> onNext; + final Consumer<? super Throwable> onError; + final Action onComplete; + final Consumer<? super Disposable> onSubscribe; + + public LambdaObserver(Consumer<? super T> onNext, Consumer<? super Throwable> onError, + Action onComplete, + Consumer<? super Disposable> onSubscribe) { + super(); + this.onNext = onNext; + this.onError = onError; + this.onComplete = onComplete; + this.onSubscribe = onSubscribe; + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.setOnce(this, d)) { + try { + onSubscribe.accept(this); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + d.dispose(); + onError(ex); + } + } + } + + @Override + public void onNext(T t) { + if (!isDisposed()) { + try { + onNext.accept(t); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + get().dispose(); + onError(e); + } + } + } + + @Override + public void onError(Throwable t) { + if (!isDisposed()) { + lazySet(DisposableHelper.DISPOSED); + try { + onError.accept(t); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + RxJavaPlugins.onError(new CompositeException(t, e)); + } + } else { + RxJavaPlugins.onError(t); + } + } + + @Override + public void onComplete() { + if (!isDisposed()) { + lazySet(DisposableHelper.DISPOSED); + try { + onComplete.run(); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + RxJavaPlugins.onError(e); + } + } + } + + @Override + public void dispose() { + DisposableHelper.dispose(this); + } + + @Override + public boolean isDisposed() { + return get() == DisposableHelper.DISPOSED; + } + + @Override + public boolean hasCustomOnError() { + return onError != Functions.ON_ERROR_MISSING; + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/observers/QueueDrainObserver.java b/src/main/java/io/reactivex/rxjava3/internal/observers/QueueDrainObserver.java new file mode 100644 index 0000000000..aa25f4286c --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/observers/QueueDrainObserver.java @@ -0,0 +1,142 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.observers; + +import java.util.concurrent.atomic.AtomicInteger; + +import io.reactivex.rxjava3.core.Observer; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.util.*; +import io.reactivex.rxjava3.operators.SimplePlainQueue; + +/** + * Abstract base class for subscribers that hold another subscriber, a queue + * and requires queue-drain behavior. + * + * @param <T> the source type to which this subscriber will be subscribed + * @param <U> the value type in the queue + * @param <V> the value type the child subscriber accepts + */ +public abstract class QueueDrainObserver<T, U, V> extends QueueDrainSubscriberPad2 implements Observer<T>, ObservableQueueDrain<U, V> { + protected final Observer<? super V> downstream; + protected final SimplePlainQueue<U> queue; + + protected volatile boolean cancelled; + + protected volatile boolean done; + protected Throwable error; + + public QueueDrainObserver(Observer<? super V> actual, SimplePlainQueue<U> queue) { + this.downstream = actual; + this.queue = queue; + } + + @Override + public final boolean cancelled() { + return cancelled; + } + + @Override + public final boolean done() { + return done; + } + + @Override + public final boolean enter() { + return wip.getAndIncrement() == 0; + } + + protected final void fastPathEmit(U value, boolean delayError, Disposable dispose) { + final Observer<? super V> observer = downstream; + final SimplePlainQueue<U> q = queue; + + if (wip.get() == 0 && wip.compareAndSet(0, 1)) { + accept(observer, value); + if (leave(-1) == 0) { + return; + } + } else { + q.offer(value); + if (!enter()) { + return; + } + } + QueueDrainHelper.drainLoop(q, observer, delayError, dispose, this); + } + + /** + * Makes sure the fast-path emits in order. + * @param value the value to emit or queue up + * @param delayError if true, errors are delayed until the source has terminated + * @param disposable the resource to dispose if the drain terminates + */ + protected final void fastPathOrderedEmit(U value, boolean delayError, Disposable disposable) { + final Observer<? super V> observer = downstream; + final SimplePlainQueue<U> q = queue; + + if (wip.get() == 0 && wip.compareAndSet(0, 1)) { + if (q.isEmpty()) { + accept(observer, value); + if (leave(-1) == 0) { + return; + } + } else { + q.offer(value); + } + } else { + q.offer(value); + if (!enter()) { + return; + } + } + QueueDrainHelper.drainLoop(q, observer, delayError, disposable, this); + } + + @Override + public final Throwable error() { + return error; + } + + @Override + public final int leave(int m) { + return wip.addAndGet(m); + } + + @Override + public void accept(Observer<? super V> a, U v) { + // ignored by default + } +} + +// ------------------------------------------------------------------- +// Padding superclasses +//------------------------------------------------------------------- + +/** Pads the header away from other fields. */ +class QueueDrainSubscriberPad0 { + volatile long p1, p2, p3, p4, p5, p6, p7; + volatile long p8, p9, p10, p11, p12, p13, p14, p15; +} + +/** The wip counter. */ +class QueueDrainSubscriberWip extends QueueDrainSubscriberPad0 { + final AtomicInteger wip = new AtomicInteger(); +} + +/** Pads away the wip from the other fields. */ +class QueueDrainSubscriberPad2 extends QueueDrainSubscriberWip { + volatile long p1a, p2a, p3a, p4a, p5a, p6a, p7a; + volatile long p8a, p9a, p10a, p11a, p12a, p13a, p14a, p15a; +} + diff --git a/src/main/java/io/reactivex/rxjava3/internal/observers/ResumeSingleObserver.java b/src/main/java/io/reactivex/rxjava3/internal/observers/ResumeSingleObserver.java new file mode 100644 index 0000000000..acaa71257c --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/observers/ResumeSingleObserver.java @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.observers; + +import java.util.concurrent.atomic.AtomicReference; + +import io.reactivex.rxjava3.core.SingleObserver; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; + +/** + * A SingleObserver implementation used for subscribing to the actual SingleSource + * and replace the current Disposable in a parent AtomicReference. + * + * @param <T> the value type + */ +public final class ResumeSingleObserver<T> implements SingleObserver<T> { + + final AtomicReference<Disposable> parent; + + final SingleObserver<? super T> downstream; + + public ResumeSingleObserver(AtomicReference<Disposable> parent, SingleObserver<? super T> downstream) { + this.parent = parent; + this.downstream = downstream; + } + + @Override + public void onSubscribe(Disposable d) { + DisposableHelper.replace(parent, d); + } + + @Override + public void onSuccess(T value) { + downstream.onSuccess(value); + } + + @Override + public void onError(Throwable e) { + downstream.onError(e); + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/observers/SafeCompletableObserver.java b/src/main/java/io/reactivex/rxjava3/internal/observers/SafeCompletableObserver.java new file mode 100644 index 0000000000..18e16383af --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/observers/SafeCompletableObserver.java @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.observers; + +import io.reactivex.rxjava3.annotations.NonNull; +import io.reactivex.rxjava3.core.CompletableObserver; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +/** + * Wraps another {@link CompletableObserver} and catches exceptions thrown by its + * {@code onSubscribe}, {@code onError} or + * {@code onComplete} methods despite the protocol forbids it. + * <p> + * Such exceptions are routed to the {@link RxJavaPlugins#onError(Throwable)} handler. + * + * @since 3.0.0 + */ +public final class SafeCompletableObserver implements CompletableObserver { + + final CompletableObserver downstream; + + boolean onSubscribeFailed; + + public SafeCompletableObserver(CompletableObserver downstream) { + this.downstream = downstream; + } + + @Override + public void onSubscribe(@NonNull Disposable d) { + try { + downstream.onSubscribe(d); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + onSubscribeFailed = true; + d.dispose(); + RxJavaPlugins.onError(ex); + } + } + + @Override + public void onError(@NonNull Throwable e) { + if (onSubscribeFailed) { + RxJavaPlugins.onError(e); + } else { + try { + downstream.onError(e); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + RxJavaPlugins.onError(new CompositeException(e, ex)); + } + } + } + + @Override + public void onComplete() { + if (!onSubscribeFailed) { + try { + downstream.onComplete(); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + RxJavaPlugins.onError(ex); + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/observers/SafeMaybeObserver.java b/src/main/java/io/reactivex/rxjava3/internal/observers/SafeMaybeObserver.java new file mode 100644 index 0000000000..320ac53427 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/observers/SafeMaybeObserver.java @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.observers; + +import io.reactivex.rxjava3.annotations.NonNull; +import io.reactivex.rxjava3.core.MaybeObserver; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +/** + * Wraps another {@link MaybeObserver} and catches exceptions thrown by its + * {@code onSubscribe}, {@code onSuccess}, {@code onError} or + * {@code onComplete} methods despite the protocol forbids it. + * <p> + * Such exceptions are routed to the {@link RxJavaPlugins#onError(Throwable)} handler. + * + * @param <T> the element type of the sequence + * @since 3.0.0 + */ +public final class SafeMaybeObserver<T> implements MaybeObserver<T> { + + final MaybeObserver<? super T> downstream; + + boolean onSubscribeFailed; + + public SafeMaybeObserver(MaybeObserver<? super T> downstream) { + this.downstream = downstream; + } + + @Override + public void onSubscribe(@NonNull Disposable d) { + try { + downstream.onSubscribe(d); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + onSubscribeFailed = true; + d.dispose(); + RxJavaPlugins.onError(ex); + } + } + + @Override + public void onSuccess(@NonNull T t) { + if (!onSubscribeFailed) { + try { + downstream.onSuccess(t); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + RxJavaPlugins.onError(ex); + } + } + } + + @Override + public void onError(@NonNull Throwable e) { + if (onSubscribeFailed) { + RxJavaPlugins.onError(e); + } else { + try { + downstream.onError(e); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + RxJavaPlugins.onError(new CompositeException(e, ex)); + } + } + } + + @Override + public void onComplete() { + if (!onSubscribeFailed) { + try { + downstream.onComplete(); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + RxJavaPlugins.onError(ex); + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/observers/SafeSingleObserver.java b/src/main/java/io/reactivex/rxjava3/internal/observers/SafeSingleObserver.java new file mode 100644 index 0000000000..3e73d1e639 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/observers/SafeSingleObserver.java @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.observers; + +import io.reactivex.rxjava3.annotations.NonNull; +import io.reactivex.rxjava3.core.SingleObserver; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +/** + * Wraps another {@link SingleObserver} and catches exceptions thrown by its + * {@code onSubscribe}, {@code onSuccess} or {@code onError} methods despite + * the protocol forbids it. + * <p> + * Such exceptions are routed to the {@link RxJavaPlugins#onError(Throwable)} handler. + * + * @param <T> the element type of the sequence + * @since 3.0.0 + */ +public final class SafeSingleObserver<T> implements SingleObserver<T> { + + final SingleObserver<? super T> downstream; + + boolean onSubscribeFailed; + + public SafeSingleObserver(SingleObserver<? super T> downstream) { + this.downstream = downstream; + } + + @Override + public void onSubscribe(@NonNull Disposable d) { + try { + downstream.onSubscribe(d); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + onSubscribeFailed = true; + d.dispose(); + RxJavaPlugins.onError(ex); + } + } + + @Override + public void onSuccess(@NonNull T t) { + if (!onSubscribeFailed) { + try { + downstream.onSuccess(t); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + RxJavaPlugins.onError(ex); + } + } + } + + @Override + public void onError(@NonNull Throwable e) { + if (onSubscribeFailed) { + RxJavaPlugins.onError(e); + } else { + try { + downstream.onError(e); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + RxJavaPlugins.onError(new CompositeException(e, ex)); + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/completable/CompletableAmb.java b/src/main/java/io/reactivex/rxjava3/internal/operators/completable/CompletableAmb.java new file mode 100644 index 0000000000..60fa3ce4c6 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/completable/CompletableAmb.java @@ -0,0 +1,133 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.completable; + +import java.util.concurrent.atomic.AtomicBoolean; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.*; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.internal.disposables.EmptyDisposable; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +public final class CompletableAmb extends Completable { + private final CompletableSource[] sources; + private final Iterable<? extends CompletableSource> sourcesIterable; + + public CompletableAmb(CompletableSource[] sources, Iterable<? extends CompletableSource> sourcesIterable) { + this.sources = sources; + this.sourcesIterable = sourcesIterable; + } + + @Override + public void subscribeActual(final CompletableObserver observer) { + CompletableSource[] sources = this.sources; + int count = 0; + if (sources == null) { + sources = new CompletableSource[8]; + try { + for (CompletableSource element : sourcesIterable) { + if (element == null) { + EmptyDisposable.error(new NullPointerException("One of the sources is null"), observer); + return; + } + if (count == sources.length) { + CompletableSource[] b = new CompletableSource[count + (count >> 2)]; + System.arraycopy(sources, 0, b, 0, count); + sources = b; + } + sources[count++] = element; + } + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + EmptyDisposable.error(e, observer); + return; + } + } else { + count = sources.length; + } + + final CompositeDisposable set = new CompositeDisposable(); + observer.onSubscribe(set); + + final AtomicBoolean once = new AtomicBoolean(); + + for (int i = 0; i < count; i++) { + CompletableSource c = sources[i]; + if (set.isDisposed()) { + return; + } + if (c == null) { + NullPointerException npe = new NullPointerException("One of the sources is null"); + if (once.compareAndSet(false, true)) { + set.dispose(); + observer.onError(npe); + } else { + RxJavaPlugins.onError(npe); + } + return; + } + + // no need to have separate subscribers because inner is stateless + c.subscribe(new Amb(once, set, observer)); + } + + if (count == 0) { + observer.onComplete(); + } + } + + static final class Amb implements CompletableObserver { + + final AtomicBoolean once; + + final CompositeDisposable set; + + final CompletableObserver downstream; + + Disposable upstream; + + Amb(AtomicBoolean once, CompositeDisposable set, CompletableObserver observer) { + this.once = once; + this.set = set; + this.downstream = observer; + } + + @Override + public void onComplete() { + if (once.compareAndSet(false, true)) { + set.delete(upstream); + set.dispose(); + downstream.onComplete(); + } + } + + @Override + public void onError(Throwable e) { + if (once.compareAndSet(false, true)) { + set.delete(upstream); + set.dispose(); + downstream.onError(e); + } else { + RxJavaPlugins.onError(e); + } + } + + @Override + public void onSubscribe(Disposable d) { + upstream = d; + set.add(d); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/completable/CompletableAndThenCompletable.java b/src/main/java/io/reactivex/rxjava3/internal/operators/completable/CompletableAndThenCompletable.java new file mode 100644 index 0000000000..13e582e764 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/completable/CompletableAndThenCompletable.java @@ -0,0 +1,107 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.completable; + +import java.util.concurrent.atomic.AtomicReference; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; + +public final class CompletableAndThenCompletable extends Completable { + + final CompletableSource source; + + final CompletableSource next; + + public CompletableAndThenCompletable(CompletableSource source, CompletableSource next) { + this.source = source; + this.next = next; + } + + @Override + protected void subscribeActual(CompletableObserver observer) { + source.subscribe(new SourceObserver(observer, next)); + } + + static final class SourceObserver + extends AtomicReference<Disposable> + implements CompletableObserver, Disposable { + + private static final long serialVersionUID = -4101678820158072998L; + + final CompletableObserver actualObserver; + + final CompletableSource next; + + SourceObserver(CompletableObserver actualObserver, CompletableSource next) { + this.actualObserver = actualObserver; + this.next = next; + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.setOnce(this, d)) { + actualObserver.onSubscribe(this); + } + } + + @Override + public void onError(Throwable e) { + actualObserver.onError(e); + } + + @Override + public void onComplete() { + next.subscribe(new NextObserver(this, actualObserver)); + } + + @Override + public void dispose() { + DisposableHelper.dispose(this); + } + + @Override + public boolean isDisposed() { + return DisposableHelper.isDisposed(get()); + } + } + + static final class NextObserver implements CompletableObserver { + + final AtomicReference<Disposable> parent; + + final CompletableObserver downstream; + + NextObserver(AtomicReference<Disposable> parent, CompletableObserver downstream) { + this.parent = parent; + this.downstream = downstream; + } + + @Override + public void onSubscribe(Disposable d) { + DisposableHelper.replace(parent, d); + } + + @Override + public void onComplete() { + downstream.onComplete(); + } + + @Override + public void onError(Throwable e) { + downstream.onError(e); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/completable/CompletableCache.java b/src/main/java/io/reactivex/rxjava3/internal/operators/completable/CompletableCache.java new file mode 100644 index 0000000000..c5fd3279d6 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/completable/CompletableCache.java @@ -0,0 +1,170 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.completable; + +import java.util.concurrent.atomic.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; + +/** + * Consume the upstream source exactly once and cache its terminal event. + * <p>History: 2.0.4 - experimental + * @since 2.1 + */ +public final class CompletableCache extends Completable implements CompletableObserver { + + static final InnerCompletableCache[] EMPTY = new InnerCompletableCache[0]; + + static final InnerCompletableCache[] TERMINATED = new InnerCompletableCache[0]; + + final CompletableSource source; + + final AtomicReference<InnerCompletableCache[]> observers; + + final AtomicBoolean once; + + Throwable error; + + public CompletableCache(CompletableSource source) { + this.source = source; + this.observers = new AtomicReference<>(EMPTY); + this.once = new AtomicBoolean(); + } + + @Override + protected void subscribeActual(CompletableObserver observer) { + InnerCompletableCache inner = new InnerCompletableCache(observer); + observer.onSubscribe(inner); + + if (add(inner)) { + if (inner.isDisposed()) { + remove(inner); + } + + if (once.compareAndSet(false, true)) { + source.subscribe(this); + } + } else { + Throwable ex = error; + if (ex != null) { + observer.onError(ex); + } else { + observer.onComplete(); + } + } + } + + @Override + public void onSubscribe(Disposable d) { + // not used + } + + @Override + public void onError(Throwable e) { + error = e; + for (InnerCompletableCache inner : observers.getAndSet(TERMINATED)) { + if (!inner.get()) { + inner.downstream.onError(e); + } + } + } + + @Override + public void onComplete() { + for (InnerCompletableCache inner : observers.getAndSet(TERMINATED)) { + if (!inner.get()) { + inner.downstream.onComplete(); + } + } + } + + boolean add(InnerCompletableCache inner) { + for (;;) { + InnerCompletableCache[] a = observers.get(); + if (a == TERMINATED) { + return false; + } + int n = a.length; + InnerCompletableCache[] b = new InnerCompletableCache[n + 1]; + System.arraycopy(a, 0, b, 0, n); + b[n] = inner; + if (observers.compareAndSet(a, b)) { + return true; + } + } + } + + void remove(InnerCompletableCache inner) { + for (;;) { + InnerCompletableCache[] a = observers.get(); + int n = a.length; + if (n == 0) { + return; + } + + int j = -1; + + for (int i = 0; i < n; i++) { + if (a[i] == inner) { + j = i; + break; + } + } + + if (j < 0) { + return; + } + + InnerCompletableCache[] b; + + if (n == 1) { + b = EMPTY; + } else { + b = new InnerCompletableCache[n - 1]; + System.arraycopy(a, 0, b, 0, j); + System.arraycopy(a, j + 1, b, j, n - j - 1); + } + + if (observers.compareAndSet(a, b)) { + break; + } + } + } + + final class InnerCompletableCache + extends AtomicBoolean + implements Disposable { + + private static final long serialVersionUID = 8943152917179642732L; + + final CompletableObserver downstream; + + InnerCompletableCache(CompletableObserver downstream) { + this.downstream = downstream; + } + + @Override + public boolean isDisposed() { + return get(); + } + + @Override + public void dispose() { + if (compareAndSet(false, true)) { + remove(this); + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/completable/CompletableConcat.java b/src/main/java/io/reactivex/rxjava3/internal/operators/completable/CompletableConcat.java new file mode 100644 index 0000000000..00a53f3ad7 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/completable/CompletableConcat.java @@ -0,0 +1,253 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.completable; + +import java.util.concurrent.atomic.*; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; +import io.reactivex.rxjava3.internal.subscriptions.SubscriptionHelper; +import io.reactivex.rxjava3.operators.*; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +public final class CompletableConcat extends Completable { + final Publisher<? extends CompletableSource> sources; + final int prefetch; + + public CompletableConcat(Publisher<? extends CompletableSource> sources, int prefetch) { + this.sources = sources; + this.prefetch = prefetch; + } + + @Override + public void subscribeActual(CompletableObserver observer) { + sources.subscribe(new CompletableConcatSubscriber(observer, prefetch)); + } + + static final class CompletableConcatSubscriber + extends AtomicInteger + implements FlowableSubscriber<CompletableSource>, Disposable { + private static final long serialVersionUID = 9032184911934499404L; + + final CompletableObserver downstream; + + final int prefetch; + + final int limit; + + final ConcatInnerObserver inner; + + final AtomicBoolean once; + + int sourceFused; + + int consumed; + + SimpleQueue<CompletableSource> queue; + + Subscription upstream; + + volatile boolean done; + + volatile boolean active; + + CompletableConcatSubscriber(CompletableObserver actual, int prefetch) { + this.downstream = actual; + this.prefetch = prefetch; + this.inner = new ConcatInnerObserver(this); + this.once = new AtomicBoolean(); + this.limit = prefetch - (prefetch >> 2); + } + + @Override + public void onSubscribe(Subscription s) { + if (SubscriptionHelper.validate(this.upstream, s)) { + this.upstream = s; + + long r = prefetch == Integer.MAX_VALUE ? Long.MAX_VALUE : prefetch; + + if (s instanceof QueueSubscription) { + @SuppressWarnings("unchecked") + QueueSubscription<CompletableSource> qs = (QueueSubscription<CompletableSource>) s; + + int m = qs.requestFusion(QueueSubscription.ANY); + + if (m == QueueSubscription.SYNC) { + sourceFused = m; + queue = qs; + done = true; + downstream.onSubscribe(this); + drain(); + return; + } + if (m == QueueSubscription.ASYNC) { + sourceFused = m; + queue = qs; + downstream.onSubscribe(this); + s.request(r); + return; + } + } + + if (prefetch == Integer.MAX_VALUE) { + queue = new SpscLinkedArrayQueue<>(Flowable.bufferSize()); + } else { + queue = new SpscArrayQueue<>(prefetch); + } + + downstream.onSubscribe(this); + + s.request(r); + } + } + + @Override + public void onNext(CompletableSource t) { + if (sourceFused == QueueSubscription.NONE) { + if (!queue.offer(t)) { + onError(new QueueOverflowException()); + return; + } + } + drain(); + } + + @Override + public void onError(Throwable t) { + if (once.compareAndSet(false, true)) { + DisposableHelper.dispose(inner); + downstream.onError(t); + } else { + RxJavaPlugins.onError(t); + } + } + + @Override + public void onComplete() { + done = true; + drain(); + } + + @Override + public void dispose() { + upstream.cancel(); + DisposableHelper.dispose(inner); + } + + @Override + public boolean isDisposed() { + return DisposableHelper.isDisposed(inner.get()); + } + + void drain() { + if (getAndIncrement() != 0) { + return; + } + + for (;;) { + if (isDisposed()) { + return; + } + + if (!active) { + + boolean d = done; + + CompletableSource cs; + + try { + cs = queue.poll(); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + innerError(ex); + return; + } + + boolean empty = cs == null; + + if (d && empty) { + // errors never set done or call drain. + downstream.onComplete(); + return; + } + + if (!empty) { + active = true; + cs.subscribe(inner); + request(); + } + } + + if (decrementAndGet() == 0) { + break; + } + } + } + + void request() { + if (sourceFused != QueueSubscription.SYNC) { + int p = consumed + 1; + if (p == limit) { + consumed = 0; + upstream.request(p); + } else { + consumed = p; + } + } + } + + void innerError(Throwable e) { + if (once.compareAndSet(false, true)) { + upstream.cancel(); + downstream.onError(e); + } else { + RxJavaPlugins.onError(e); + } + } + + void innerComplete() { + active = false; + drain(); + } + + static final class ConcatInnerObserver extends AtomicReference<Disposable> implements CompletableObserver { + private static final long serialVersionUID = -5454794857847146511L; + + final CompletableConcatSubscriber parent; + + ConcatInnerObserver(CompletableConcatSubscriber parent) { + this.parent = parent; + } + + @Override + public void onSubscribe(Disposable d) { + DisposableHelper.replace(this, d); + } + + @Override + public void onError(Throwable e) { + parent.innerError(e); + } + + @Override + public void onComplete() { + parent.innerComplete(); + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/completable/CompletableConcatArray.java b/src/main/java/io/reactivex/rxjava3/internal/operators/completable/CompletableConcatArray.java new file mode 100644 index 0000000000..e87968e774 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/completable/CompletableConcatArray.java @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.completable; + +import java.util.concurrent.atomic.AtomicInteger; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.disposables.SequentialDisposable; + +public final class CompletableConcatArray extends Completable { + final CompletableSource[] sources; + + public CompletableConcatArray(CompletableSource[] sources) { + this.sources = sources; + } + + @Override + public void subscribeActual(CompletableObserver observer) { + ConcatInnerObserver inner = new ConcatInnerObserver(observer, sources); + observer.onSubscribe(inner.sd); + inner.next(); + } + + static final class ConcatInnerObserver extends AtomicInteger implements CompletableObserver { + + private static final long serialVersionUID = -7965400327305809232L; + + final CompletableObserver downstream; + final CompletableSource[] sources; + + int index; + + final SequentialDisposable sd; + + ConcatInnerObserver(CompletableObserver actual, CompletableSource[] sources) { + this.downstream = actual; + this.sources = sources; + this.sd = new SequentialDisposable(); + } + + @Override + public void onSubscribe(Disposable d) { + sd.replace(d); + } + + @Override + public void onError(Throwable e) { + downstream.onError(e); + } + + @Override + public void onComplete() { + next(); + } + + void next() { + if (sd.isDisposed()) { + return; + } + + if (getAndIncrement() != 0) { + return; + } + + CompletableSource[] a = sources; + do { + if (sd.isDisposed()) { + return; + } + + int idx = index++; + if (idx == a.length) { + downstream.onComplete(); + return; + } + + a[idx].subscribe(this); + } while (decrementAndGet() != 0); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/completable/CompletableConcatIterable.java b/src/main/java/io/reactivex/rxjava3/internal/operators/completable/CompletableConcatIterable.java new file mode 100644 index 0000000000..73510018ff --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/completable/CompletableConcatIterable.java @@ -0,0 +1,123 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.completable; + +import java.util.Iterator; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicInteger; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.internal.disposables.*; + +public final class CompletableConcatIterable extends Completable { + final Iterable<? extends CompletableSource> sources; + + public CompletableConcatIterable(Iterable<? extends CompletableSource> sources) { + this.sources = sources; + } + + @Override + public void subscribeActual(CompletableObserver observer) { + + Iterator<? extends CompletableSource> it; + + try { + it = Objects.requireNonNull(sources.iterator(), "The iterator returned is null"); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + EmptyDisposable.error(e, observer); + return; + } + + ConcatInnerObserver inner = new ConcatInnerObserver(observer, it); + observer.onSubscribe(inner.sd); + inner.next(); + } + + static final class ConcatInnerObserver extends AtomicInteger implements CompletableObserver { + + private static final long serialVersionUID = -7965400327305809232L; + + final CompletableObserver downstream; + final Iterator<? extends CompletableSource> sources; + + final SequentialDisposable sd; + + ConcatInnerObserver(CompletableObserver actual, Iterator<? extends CompletableSource> sources) { + this.downstream = actual; + this.sources = sources; + this.sd = new SequentialDisposable(); + } + + @Override + public void onSubscribe(Disposable d) { + sd.replace(d); + } + + @Override + public void onError(Throwable e) { + downstream.onError(e); + } + + @Override + public void onComplete() { + next(); + } + + void next() { + if (sd.isDisposed()) { + return; + } + + if (getAndIncrement() != 0) { + return; + } + + Iterator<? extends CompletableSource> a = sources; + do { + if (sd.isDisposed()) { + return; + } + + boolean b; + try { + b = a.hasNext(); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + downstream.onError(ex); + return; + } + + if (!b) { + downstream.onComplete(); + return; + } + + CompletableSource c; + + try { + c = Objects.requireNonNull(a.next(), "The CompletableSource returned is null"); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + downstream.onError(ex); + return; + } + + c.subscribe(this); + } while (decrementAndGet() != 0); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/completable/CompletableCreate.java b/src/main/java/io/reactivex/rxjava3/internal/operators/completable/CompletableCreate.java new file mode 100644 index 0000000000..b721cbb351 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/completable/CompletableCreate.java @@ -0,0 +1,128 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.completable; + +import java.util.concurrent.atomic.AtomicReference; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.Cancellable; +import io.reactivex.rxjava3.internal.disposables.*; +import io.reactivex.rxjava3.internal.util.ExceptionHelper; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +public final class CompletableCreate extends Completable { + + final CompletableOnSubscribe source; + + public CompletableCreate(CompletableOnSubscribe source) { + this.source = source; + } + + @Override + protected void subscribeActual(CompletableObserver observer) { + Emitter parent = new Emitter(observer); + observer.onSubscribe(parent); + + try { + source.subscribe(parent); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + parent.onError(ex); + } + } + + static final class Emitter + extends AtomicReference<Disposable> + implements CompletableEmitter, Disposable { + + private static final long serialVersionUID = -2467358622224974244L; + + final CompletableObserver downstream; + + Emitter(CompletableObserver downstream) { + this.downstream = downstream; + } + + @Override + public void onComplete() { + if (get() != DisposableHelper.DISPOSED) { + Disposable d = getAndSet(DisposableHelper.DISPOSED); + if (d != DisposableHelper.DISPOSED) { + try { + downstream.onComplete(); + } finally { + if (d != null) { + d.dispose(); + } + } + } + } + } + + @Override + public void onError(Throwable t) { + if (!tryOnError(t)) { + RxJavaPlugins.onError(t); + } + } + + @Override + public boolean tryOnError(Throwable t) { + if (t == null) { + t = ExceptionHelper.createNullPointerException("onError called with a null Throwable."); + } + if (get() != DisposableHelper.DISPOSED) { + Disposable d = getAndSet(DisposableHelper.DISPOSED); + if (d != DisposableHelper.DISPOSED) { + try { + downstream.onError(t); + } finally { + if (d != null) { + d.dispose(); + } + } + return true; + } + } + return false; + } + + @Override + public void setDisposable(Disposable d) { + DisposableHelper.set(this, d); + } + + @Override + public void setCancellable(Cancellable c) { + setDisposable(new CancellableDisposable(c)); + } + + @Override + public void dispose() { + DisposableHelper.dispose(this); + } + + @Override + public boolean isDisposed() { + return DisposableHelper.isDisposed(get()); + } + + @Override + public String toString() { + return String.format("%s{%s}", getClass().getSimpleName(), super.toString()); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/completable/CompletableDefer.java b/src/main/java/io/reactivex/rxjava3/internal/operators/completable/CompletableDefer.java new file mode 100644 index 0000000000..2a7763bb52 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/completable/CompletableDefer.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.completable; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.Supplier; +import io.reactivex.rxjava3.internal.disposables.EmptyDisposable; + +import java.util.Objects; + +public final class CompletableDefer extends Completable { + + final Supplier<? extends CompletableSource> completableSupplier; + + public CompletableDefer(Supplier<? extends CompletableSource> completableSupplier) { + this.completableSupplier = completableSupplier; + } + + @Override + protected void subscribeActual(CompletableObserver observer) { + CompletableSource c; + + try { + c = Objects.requireNonNull(completableSupplier.get(), "The completableSupplier returned a null CompletableSource"); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + EmptyDisposable.error(e, observer); + return; + } + + c.subscribe(observer); + } + +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/completable/CompletableDelay.java b/src/main/java/io/reactivex/rxjava3/internal/operators/completable/CompletableDelay.java new file mode 100644 index 0000000000..c21134569b --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/completable/CompletableDelay.java @@ -0,0 +1,112 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.completable; + +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; + +public final class CompletableDelay extends Completable { + + final CompletableSource source; + + final long delay; + + final TimeUnit unit; + + final Scheduler scheduler; + + final boolean delayError; + + public CompletableDelay(CompletableSource source, long delay, TimeUnit unit, Scheduler scheduler, boolean delayError) { + this.source = source; + this.delay = delay; + this.unit = unit; + this.scheduler = scheduler; + this.delayError = delayError; + } + + @Override + protected void subscribeActual(final CompletableObserver observer) { + source.subscribe(new Delay(observer, delay, unit, scheduler, delayError)); + } + + static final class Delay extends AtomicReference<Disposable> + implements CompletableObserver, Runnable, Disposable { + + private static final long serialVersionUID = 465972761105851022L; + + final CompletableObserver downstream; + + final long delay; + + final TimeUnit unit; + + final Scheduler scheduler; + + final boolean delayError; + + Throwable error; + + Delay(CompletableObserver downstream, long delay, TimeUnit unit, Scheduler scheduler, boolean delayError) { + this.downstream = downstream; + this.delay = delay; + this.unit = unit; + this.scheduler = scheduler; + this.delayError = delayError; + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.setOnce(this, d)) { + downstream.onSubscribe(this); + } + } + + @Override + public void onComplete() { + DisposableHelper.replace(this, scheduler.scheduleDirect(this, delay, unit)); + } + + @Override + public void onError(final Throwable e) { + error = e; + DisposableHelper.replace(this, scheduler.scheduleDirect(this, delayError ? delay : 0, unit)); + } + + @Override + public void dispose() { + DisposableHelper.dispose(this); + } + + @Override + public boolean isDisposed() { + return DisposableHelper.isDisposed(get()); + } + + @Override + public void run() { + Throwable e = error; + error = null; + if (e != null) { + downstream.onError(e); + } else { + downstream.onComplete(); + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/completable/CompletableDetach.java b/src/main/java/io/reactivex/rxjava3/internal/operators/completable/CompletableDetach.java new file mode 100644 index 0000000000..d01d297495 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/completable/CompletableDetach.java @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.completable; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; + +/** + * Breaks the references between the upstream and downstream when the Completable terminates. + * <p>History: 2.1.5 - experimental + * @since 2.2 + */ +public final class CompletableDetach extends Completable { + + final CompletableSource source; + + public CompletableDetach(CompletableSource source) { + this.source = source; + } + + @Override + protected void subscribeActual(CompletableObserver observer) { + source.subscribe(new DetachCompletableObserver(observer)); + } + + static final class DetachCompletableObserver implements CompletableObserver, Disposable { + + CompletableObserver downstream; + + Disposable upstream; + + DetachCompletableObserver(CompletableObserver downstream) { + this.downstream = downstream; + } + + @Override + public void dispose() { + downstream = null; + upstream.dispose(); + upstream = DisposableHelper.DISPOSED; + } + + @Override + public boolean isDisposed() { + return upstream.isDisposed(); + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.validate(this.upstream, d)) { + this.upstream = d; + + downstream.onSubscribe(this); + } + } + + @Override + public void onError(Throwable e) { + upstream = DisposableHelper.DISPOSED; + CompletableObserver a = downstream; + if (a != null) { + downstream = null; + a.onError(e); + } + } + + @Override + public void onComplete() { + upstream = DisposableHelper.DISPOSED; + CompletableObserver a = downstream; + if (a != null) { + downstream = null; + a.onComplete(); + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/completable/CompletableDisposeOn.java b/src/main/java/io/reactivex/rxjava3/internal/operators/completable/CompletableDisposeOn.java new file mode 100644 index 0000000000..bd213b5fc4 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/completable/CompletableDisposeOn.java @@ -0,0 +1,95 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.completable; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +public final class CompletableDisposeOn extends Completable { + + final CompletableSource source; + + final Scheduler scheduler; + + public CompletableDisposeOn(CompletableSource source, Scheduler scheduler) { + this.source = source; + this.scheduler = scheduler; + } + + @Override + protected void subscribeActual(final CompletableObserver observer) { + source.subscribe(new DisposeOnObserver(observer, scheduler)); + } + + static final class DisposeOnObserver implements CompletableObserver, Disposable, Runnable { + final CompletableObserver downstream; + + final Scheduler scheduler; + + Disposable upstream; + + volatile boolean disposed; + + DisposeOnObserver(CompletableObserver observer, Scheduler scheduler) { + this.downstream = observer; + this.scheduler = scheduler; + } + + @Override + public void onComplete() { + if (disposed) { + return; + } + downstream.onComplete(); + } + + @Override + public void onError(Throwable e) { + if (disposed) { + RxJavaPlugins.onError(e); + return; + } + downstream.onError(e); + } + + @Override + public void onSubscribe(final Disposable d) { + if (DisposableHelper.validate(this.upstream, d)) { + this.upstream = d; + + downstream.onSubscribe(this); + } + } + + @Override + public void dispose() { + disposed = true; + scheduler.scheduleDirect(this); + } + + @Override + public boolean isDisposed() { + return disposed; + } + + @Override + public void run() { + upstream.dispose(); + upstream = DisposableHelper.DISPOSED; + } + } + +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/completable/CompletableDoFinally.java b/src/main/java/io/reactivex/rxjava3/internal/operators/completable/CompletableDoFinally.java new file mode 100644 index 0000000000..02fa08cdb0 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/completable/CompletableDoFinally.java @@ -0,0 +1,104 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.completable; + +import java.util.concurrent.atomic.AtomicInteger; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.Action; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +/** + * Execute an action after an onError, onComplete or a dispose event. + * <p>History: 2.0.1 - experimental + * @since 2.1 + */ +public final class CompletableDoFinally extends Completable { + + final CompletableSource source; + + final Action onFinally; + + public CompletableDoFinally(CompletableSource source, Action onFinally) { + this.source = source; + this.onFinally = onFinally; + } + + @Override + protected void subscribeActual(CompletableObserver observer) { + source.subscribe(new DoFinallyObserver(observer, onFinally)); + } + + static final class DoFinallyObserver extends AtomicInteger implements CompletableObserver, Disposable { + + private static final long serialVersionUID = 4109457741734051389L; + + final CompletableObserver downstream; + + final Action onFinally; + + Disposable upstream; + + DoFinallyObserver(CompletableObserver actual, Action onFinally) { + this.downstream = actual; + this.onFinally = onFinally; + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.validate(this.upstream, d)) { + this.upstream = d; + + downstream.onSubscribe(this); + } + } + + @Override + public void onError(Throwable t) { + downstream.onError(t); + runFinally(); + } + + @Override + public void onComplete() { + downstream.onComplete(); + runFinally(); + } + + @Override + public void dispose() { + upstream.dispose(); + runFinally(); + } + + @Override + public boolean isDisposed() { + return upstream.isDisposed(); + } + + void runFinally() { + if (compareAndSet(0, 1)) { + try { + onFinally.run(); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + RxJavaPlugins.onError(ex); + } + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/completable/CompletableDoOnEvent.java b/src/main/java/io/reactivex/rxjava3/internal/operators/completable/CompletableDoOnEvent.java new file mode 100644 index 0000000000..dfa24b000f --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/completable/CompletableDoOnEvent.java @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.completable; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.Consumer; + +public final class CompletableDoOnEvent extends Completable { + final CompletableSource source; + final Consumer<? super Throwable> onEvent; + + public CompletableDoOnEvent(final CompletableSource source, final Consumer<? super Throwable> onEvent) { + this.source = source; + this.onEvent = onEvent; + } + + @Override + protected void subscribeActual(final CompletableObserver observer) { + source.subscribe(new DoOnEvent(observer)); + } + + final class DoOnEvent implements CompletableObserver { + private final CompletableObserver observer; + + DoOnEvent(CompletableObserver observer) { + this.observer = observer; + } + + @Override + public void onComplete() { + try { + onEvent.accept(null); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + observer.onError(e); + return; + } + + observer.onComplete(); + } + + @Override + public void onError(Throwable e) { + try { + onEvent.accept(e); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + e = new CompositeException(e, ex); + } + + observer.onError(e); + } + + @Override + public void onSubscribe(final Disposable d) { + observer.onSubscribe(d); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/completable/CompletableEmpty.java b/src/main/java/io/reactivex/rxjava3/internal/operators/completable/CompletableEmpty.java new file mode 100644 index 0000000000..7c18a4ddc9 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/completable/CompletableEmpty.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.completable; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.internal.disposables.EmptyDisposable; + +public final class CompletableEmpty extends Completable { + public static final Completable INSTANCE = new CompletableEmpty(); + + private CompletableEmpty() { + } + + @Override + public void subscribeActual(CompletableObserver observer) { + EmptyDisposable.complete(observer); + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/completable/CompletableError.java b/src/main/java/io/reactivex/rxjava3/internal/operators/completable/CompletableError.java new file mode 100644 index 0000000000..8d2cb2fcc2 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/completable/CompletableError.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.completable; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.internal.disposables.EmptyDisposable; + +public final class CompletableError extends Completable { + + final Throwable error; + + public CompletableError(Throwable error) { + this.error = error; + } + + @Override + protected void subscribeActual(CompletableObserver observer) { + EmptyDisposable.error(error, observer); + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/completable/CompletableErrorSupplier.java b/src/main/java/io/reactivex/rxjava3/internal/operators/completable/CompletableErrorSupplier.java new file mode 100644 index 0000000000..80c95806b8 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/completable/CompletableErrorSupplier.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.completable; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.Supplier; +import io.reactivex.rxjava3.internal.disposables.EmptyDisposable; + +import java.util.Objects; + +public final class CompletableErrorSupplier extends Completable { + + final Supplier<? extends Throwable> errorSupplier; + + public CompletableErrorSupplier(Supplier<? extends Throwable> errorSupplier) { + this.errorSupplier = errorSupplier; + } + + @Override + protected void subscribeActual(CompletableObserver observer) { + Throwable error; + + try { + error = Objects.requireNonNull(errorSupplier.get(), "The error returned is null"); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + error = e; + } + + EmptyDisposable.error(error, observer); + } + +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/completable/CompletableFromAction.java b/src/main/java/io/reactivex/rxjava3/internal/operators/completable/CompletableFromAction.java new file mode 100644 index 0000000000..53cfc2615b --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/completable/CompletableFromAction.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.completable; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.*; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.Action; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +public final class CompletableFromAction extends Completable { + + final Action run; + + public CompletableFromAction(Action run) { + this.run = run; + } + + @Override + protected void subscribeActual(CompletableObserver observer) { + Disposable d = Disposable.empty(); + observer.onSubscribe(d); + if (!d.isDisposed()) { + try { + run.run(); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + if (!d.isDisposed()) { + observer.onError(e); + } else { + RxJavaPlugins.onError(e); + } + return; + } + if (!d.isDisposed()) { + observer.onComplete(); + } + } + } + +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/completable/CompletableFromCallable.java b/src/main/java/io/reactivex/rxjava3/internal/operators/completable/CompletableFromCallable.java new file mode 100644 index 0000000000..90f9266934 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/completable/CompletableFromCallable.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.completable; + +import java.util.concurrent.Callable; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.*; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +public final class CompletableFromCallable extends Completable { + + final Callable<?> callable; + + public CompletableFromCallable(Callable<?> callable) { + this.callable = callable; + } + + @Override + protected void subscribeActual(CompletableObserver observer) { + Disposable d = Disposable.empty(); + observer.onSubscribe(d); + try { + callable.call(); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + if (!d.isDisposed()) { + observer.onError(e); + } else { + RxJavaPlugins.onError(e); + } + return; + } + if (!d.isDisposed()) { + observer.onComplete(); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/completable/CompletableFromObservable.java b/src/main/java/io/reactivex/rxjava3/internal/operators/completable/CompletableFromObservable.java new file mode 100644 index 0000000000..25dffbbebf --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/completable/CompletableFromObservable.java @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.completable; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; + +public final class CompletableFromObservable<T> extends Completable { + + final ObservableSource<T> observable; + + public CompletableFromObservable(ObservableSource<T> observable) { + this.observable = observable; + } + + @Override + protected void subscribeActual(final CompletableObserver observer) { + observable.subscribe(new CompletableFromObservableObserver<>(observer)); + } + + static final class CompletableFromObservableObserver<T> implements Observer<T> { + final CompletableObserver co; + + CompletableFromObservableObserver(CompletableObserver co) { + this.co = co; + } + + @Override + public void onSubscribe(Disposable d) { + co.onSubscribe(d); + } + + @Override + public void onNext(T value) { + // Deliberately ignored. + } + + @Override + public void onError(Throwable e) { + co.onError(e); + } + + @Override + public void onComplete() { + co.onComplete(); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/completable/CompletableFromPublisher.java b/src/main/java/io/reactivex/rxjava3/internal/operators/completable/CompletableFromPublisher.java new file mode 100644 index 0000000000..4205d1141c --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/completable/CompletableFromPublisher.java @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.completable; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.subscriptions.SubscriptionHelper; + +public final class CompletableFromPublisher<T> extends Completable { + + final Publisher<T> flowable; + + public CompletableFromPublisher(Publisher<T> flowable) { + this.flowable = flowable; + } + + @Override + protected void subscribeActual(final CompletableObserver downstream) { + flowable.subscribe(new FromPublisherSubscriber<>(downstream)); + } + + static final class FromPublisherSubscriber<T> implements FlowableSubscriber<T>, Disposable { + + final CompletableObserver downstream; + + Subscription upstream; + + FromPublisherSubscriber(CompletableObserver downstream) { + this.downstream = downstream; + } + + @Override + public void onSubscribe(Subscription s) { + if (SubscriptionHelper.validate(this.upstream, s)) { + this.upstream = s; + + downstream.onSubscribe(this); + + s.request(Long.MAX_VALUE); + } + } + + @Override + public void onNext(T t) { + // ignored + } + + @Override + public void onError(Throwable t) { + downstream.onError(t); + } + + @Override + public void onComplete() { + downstream.onComplete(); + } + + @Override + public void dispose() { + upstream.cancel(); + upstream = SubscriptionHelper.CANCELLED; + } + + @Override + public boolean isDisposed() { + return upstream == SubscriptionHelper.CANCELLED; + } + } + +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/completable/CompletableFromRunnable.java b/src/main/java/io/reactivex/rxjava3/internal/operators/completable/CompletableFromRunnable.java new file mode 100644 index 0000000000..79f2efb470 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/completable/CompletableFromRunnable.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.completable; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.*; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +public final class CompletableFromRunnable extends Completable { + + final Runnable runnable; + + public CompletableFromRunnable(Runnable runnable) { + this.runnable = runnable; + } + + @Override + protected void subscribeActual(CompletableObserver observer) { + Disposable d = Disposable.empty(); + observer.onSubscribe(d); + if (!d.isDisposed()) { + try { + runnable.run(); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + if (!d.isDisposed()) { + observer.onError(e); + } else { + RxJavaPlugins.onError(e); + } + return; + } + if (!d.isDisposed()) { + observer.onComplete(); + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/completable/CompletableFromSingle.java b/src/main/java/io/reactivex/rxjava3/internal/operators/completable/CompletableFromSingle.java new file mode 100644 index 0000000000..d55fc30339 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/completable/CompletableFromSingle.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.completable; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; + +public final class CompletableFromSingle<T> extends Completable { + + final SingleSource<T> single; + + public CompletableFromSingle(SingleSource<T> single) { + this.single = single; + } + + @Override + protected void subscribeActual(final CompletableObserver observer) { + single.subscribe(new CompletableFromSingleObserver<>(observer)); + } + + static final class CompletableFromSingleObserver<T> implements SingleObserver<T> { + final CompletableObserver co; + + CompletableFromSingleObserver(CompletableObserver co) { + this.co = co; + } + + @Override + public void onError(Throwable e) { + co.onError(e); + } + + @Override + public void onSubscribe(Disposable d) { + co.onSubscribe(d); + } + + @Override + public void onSuccess(T value) { + co.onComplete(); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/completable/CompletableFromSupplier.java b/src/main/java/io/reactivex/rxjava3/internal/operators/completable/CompletableFromSupplier.java new file mode 100644 index 0000000000..8b45e6b5f0 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/completable/CompletableFromSupplier.java @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.completable; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.*; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.Supplier; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +/** + * Call a Supplier for each incoming CompletableObserver and signal completion or the thrown exception. + * @since 3.0.0 + */ +public final class CompletableFromSupplier extends Completable { + + final Supplier<?> supplier; + + public CompletableFromSupplier(Supplier<?> supplier) { + this.supplier = supplier; + } + + @Override + protected void subscribeActual(CompletableObserver observer) { + Disposable d = Disposable.empty(); + observer.onSubscribe(d); + try { + supplier.get(); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + if (!d.isDisposed()) { + observer.onError(e); + } else { + RxJavaPlugins.onError(e); + } + return; + } + if (!d.isDisposed()) { + observer.onComplete(); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/completable/CompletableFromUnsafeSource.java b/src/main/java/io/reactivex/rxjava3/internal/operators/completable/CompletableFromUnsafeSource.java new file mode 100644 index 0000000000..dfc0356bf7 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/completable/CompletableFromUnsafeSource.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.completable; + +import io.reactivex.rxjava3.core.*; + +public final class CompletableFromUnsafeSource extends Completable { + + final CompletableSource source; + + public CompletableFromUnsafeSource(CompletableSource source) { + this.source = source; + } + + @Override + protected void subscribeActual(CompletableObserver observer) { + source.subscribe(observer); + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/completable/CompletableHide.java b/src/main/java/io/reactivex/rxjava3/internal/operators/completable/CompletableHide.java new file mode 100644 index 0000000000..536855a7b7 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/completable/CompletableHide.java @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.completable; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; + +/** + * Hides the identity of the upstream Completable and its Disposable sent through onSubscribe. + */ +public final class CompletableHide extends Completable { + + final CompletableSource source; + + public CompletableHide(CompletableSource source) { + this.source = source; + } + + @Override + protected void subscribeActual(CompletableObserver observer) { + source.subscribe(new HideCompletableObserver(observer)); + } + + static final class HideCompletableObserver implements CompletableObserver, Disposable { + + final CompletableObserver downstream; + + Disposable upstream; + + HideCompletableObserver(CompletableObserver downstream) { + this.downstream = downstream; + } + + @Override + public void dispose() { + upstream.dispose(); + upstream = DisposableHelper.DISPOSED; + } + + @Override + public boolean isDisposed() { + return upstream.isDisposed(); + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.validate(this.upstream, d)) { + this.upstream = d; + + downstream.onSubscribe(this); + } + } + + @Override + public void onError(Throwable e) { + downstream.onError(e); + } + + @Override + public void onComplete() { + downstream.onComplete(); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/completable/CompletableLift.java b/src/main/java/io/reactivex/rxjava3/internal/operators/completable/CompletableLift.java new file mode 100644 index 0000000000..6749689bcc --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/completable/CompletableLift.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.completable; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +public final class CompletableLift extends Completable { + + final CompletableSource source; + + final CompletableOperator onLift; + + public CompletableLift(CompletableSource source, CompletableOperator onLift) { + this.source = source; + this.onLift = onLift; + } + + @Override + protected void subscribeActual(CompletableObserver observer) { + try { + // TODO plugin wrapping + + CompletableObserver sw = onLift.apply(observer); + + source.subscribe(sw); + } catch (NullPointerException ex) { // NOPMD + throw ex; + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + RxJavaPlugins.onError(ex); + } + } + +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/completable/CompletableMaterialize.java b/src/main/java/io/reactivex/rxjava3/internal/operators/completable/CompletableMaterialize.java new file mode 100644 index 0000000000..198ba94b1a --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/completable/CompletableMaterialize.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.completable; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.internal.operators.mixed.MaterializeSingleObserver; + +/** + * Turn the signal types of a Completable source into a single Notification of + * equal kind. + * <p>History: 2.2.4 - experimental + * + * @param <T> the element type of the source + * @since 3.0.0 + */ +public final class CompletableMaterialize<T> extends Single<Notification<T>> { + + final Completable source; + + public CompletableMaterialize(Completable source) { + this.source = source; + } + + @Override + protected void subscribeActual(SingleObserver<? super Notification<T>> observer) { + source.subscribe(new MaterializeSingleObserver<>(observer)); + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/completable/CompletableMerge.java b/src/main/java/io/reactivex/rxjava3/internal/operators/completable/CompletableMerge.java new file mode 100644 index 0000000000..2528fe2f3b --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/completable/CompletableMerge.java @@ -0,0 +1,194 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.completable; + +import java.util.concurrent.atomic.*; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.*; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; +import io.reactivex.rxjava3.internal.subscriptions.SubscriptionHelper; +import io.reactivex.rxjava3.internal.util.AtomicThrowable; + +public final class CompletableMerge extends Completable { + final Publisher<? extends CompletableSource> source; + final int maxConcurrency; + final boolean delayErrors; + + public CompletableMerge(Publisher<? extends CompletableSource> source, int maxConcurrency, boolean delayErrors) { + this.source = source; + this.maxConcurrency = maxConcurrency; + this.delayErrors = delayErrors; + } + + @Override + public void subscribeActual(CompletableObserver observer) { + CompletableMergeSubscriber parent = new CompletableMergeSubscriber(observer, maxConcurrency, delayErrors); + source.subscribe(parent); + } + + static final class CompletableMergeSubscriber + extends AtomicInteger + implements FlowableSubscriber<CompletableSource>, Disposable { + + private static final long serialVersionUID = -2108443387387077490L; + + final CompletableObserver downstream; + final int maxConcurrency; + final boolean delayErrors; + + final AtomicThrowable errors; + + final CompositeDisposable set; + + Subscription upstream; + + CompletableMergeSubscriber(CompletableObserver actual, int maxConcurrency, boolean delayErrors) { + this.downstream = actual; + this.maxConcurrency = maxConcurrency; + this.delayErrors = delayErrors; + this.set = new CompositeDisposable(); + this.errors = new AtomicThrowable(); + lazySet(1); + } + + @Override + public void dispose() { + upstream.cancel(); + set.dispose(); + errors.tryTerminateAndReport(); + } + + @Override + public boolean isDisposed() { + return set.isDisposed(); + } + + @Override + public void onSubscribe(Subscription s) { + if (SubscriptionHelper.validate(this.upstream, s)) { + this.upstream = s; + downstream.onSubscribe(this); + if (maxConcurrency == Integer.MAX_VALUE) { + s.request(Long.MAX_VALUE); + } else { + s.request(maxConcurrency); + } + } + } + + @Override + public void onNext(CompletableSource t) { + getAndIncrement(); + + MergeInnerObserver inner = new MergeInnerObserver(); + set.add(inner); + t.subscribe(inner); + } + + @Override + public void onError(Throwable t) { + if (!delayErrors) { + set.dispose(); + + if (errors.tryAddThrowableOrReport(t)) { + if (getAndSet(0) > 0) { + errors.tryTerminateConsumer(downstream); + } + } + } else { + if (errors.tryAddThrowableOrReport(t)) { + if (decrementAndGet() == 0) { + errors.tryTerminateConsumer(downstream); + } + } + } + } + + @Override + public void onComplete() { + if (decrementAndGet() == 0) { + errors.tryTerminateConsumer(downstream); + } + } + + void innerError(MergeInnerObserver inner, Throwable t) { + set.delete(inner); + if (!delayErrors) { + upstream.cancel(); + set.dispose(); + + if (errors.tryAddThrowableOrReport(t)) { + if (getAndSet(0) > 0) { + errors.tryTerminateConsumer(downstream); + } + } + } else { + if (errors.tryAddThrowableOrReport(t)) { + if (decrementAndGet() == 0) { + errors.tryTerminateConsumer(downstream); + } else { + if (maxConcurrency != Integer.MAX_VALUE) { + upstream.request(1); + } + } + } + } + } + + void innerComplete(MergeInnerObserver inner) { + set.delete(inner); + if (decrementAndGet() == 0) { + errors.tryTerminateConsumer(downstream); + } else { + if (maxConcurrency != Integer.MAX_VALUE) { + upstream.request(1); + } + } + } + + final class MergeInnerObserver + extends AtomicReference<Disposable> + implements CompletableObserver, Disposable { + private static final long serialVersionUID = 251330541679988317L; + + @Override + public void onSubscribe(Disposable d) { + DisposableHelper.setOnce(this, d); + } + + @Override + public void onError(Throwable e) { + innerError(this, e); + } + + @Override + public void onComplete() { + innerComplete(this); + } + + @Override + public boolean isDisposed() { + return DisposableHelper.isDisposed(get()); + } + + @Override + public void dispose() { + DisposableHelper.dispose(this); + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/completable/CompletableMergeArray.java b/src/main/java/io/reactivex/rxjava3/internal/operators/completable/CompletableMergeArray.java new file mode 100644 index 0000000000..56095221cc --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/completable/CompletableMergeArray.java @@ -0,0 +1,105 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.completable; + +import java.util.concurrent.atomic.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.*; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +public final class CompletableMergeArray extends Completable { + final CompletableSource[] sources; + + public CompletableMergeArray(CompletableSource[] sources) { + this.sources = sources; + } + + @Override + public void subscribeActual(final CompletableObserver observer) { + final CompositeDisposable set = new CompositeDisposable(); + final AtomicBoolean once = new AtomicBoolean(); + + InnerCompletableObserver shared = new InnerCompletableObserver(observer, once, set, sources.length + 1); + observer.onSubscribe(shared); + + for (CompletableSource c : sources) { + if (set.isDisposed()) { + return; + } + + if (c == null) { + set.dispose(); + NullPointerException npe = new NullPointerException("A completable source is null"); + shared.onError(npe); + return; + } + + c.subscribe(shared); + } + + shared.onComplete(); + } + + static final class InnerCompletableObserver extends AtomicInteger implements CompletableObserver, Disposable { + private static final long serialVersionUID = -8360547806504310570L; + + final CompletableObserver downstream; + + final AtomicBoolean once; + + final CompositeDisposable set; + + InnerCompletableObserver(CompletableObserver actual, AtomicBoolean once, CompositeDisposable set, int n) { + this.downstream = actual; + this.once = once; + this.set = set; + this.lazySet(n); + } + + @Override + public void onSubscribe(Disposable d) { + set.add(d); + } + + @Override + public void onError(Throwable e) { + set.dispose(); + if (once.compareAndSet(false, true)) { + downstream.onError(e); + } else { + RxJavaPlugins.onError(e); + } + } + + @Override + public void onComplete() { + if (decrementAndGet() == 0) { + // errors don't decrement this + downstream.onComplete(); + } + } + + @Override + public void dispose() { + set.dispose(); + once.set(true); + } + + @Override + public boolean isDisposed() { + return set.isDisposed(); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/completable/CompletableMergeArrayDelayError.java b/src/main/java/io/reactivex/rxjava3/internal/operators/completable/CompletableMergeArrayDelayError.java new file mode 100644 index 0000000000..85b4c61225 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/completable/CompletableMergeArrayDelayError.java @@ -0,0 +1,115 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.completable; + +import java.util.concurrent.atomic.AtomicInteger; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.*; +import io.reactivex.rxjava3.internal.util.AtomicThrowable; + +public final class CompletableMergeArrayDelayError extends Completable { + + final CompletableSource[] sources; + + public CompletableMergeArrayDelayError(CompletableSource[] sources) { + this.sources = sources; + } + + @Override + public void subscribeActual(final CompletableObserver observer) { + final CompositeDisposable set = new CompositeDisposable(); + final AtomicInteger wip = new AtomicInteger(sources.length + 1); + + final AtomicThrowable errors = new AtomicThrowable(); + set.add(new TryTerminateAndReportDisposable(errors)); + + observer.onSubscribe(set); + + for (CompletableSource c : sources) { + if (set.isDisposed()) { + return; + } + + if (c == null) { + Throwable ex = new NullPointerException("A completable source is null"); + errors.tryAddThrowableOrReport(ex); + wip.decrementAndGet(); + continue; + } + + c.subscribe(new MergeInnerCompletableObserver(observer, set, errors, wip)); + } + + if (wip.decrementAndGet() == 0) { + errors.tryTerminateConsumer(observer); + } + } + + static final class TryTerminateAndReportDisposable implements Disposable { + final AtomicThrowable errors; + TryTerminateAndReportDisposable(AtomicThrowable errors) { + this.errors = errors; + } + + @Override + public void dispose() { + errors.tryTerminateAndReport(); + } + + @Override + public boolean isDisposed() { + return errors.isTerminated(); + } + } + + static final class MergeInnerCompletableObserver + implements CompletableObserver { + final CompletableObserver downstream; + final CompositeDisposable set; + final AtomicThrowable errors; + final AtomicInteger wip; + + MergeInnerCompletableObserver(CompletableObserver observer, CompositeDisposable set, AtomicThrowable error, + AtomicInteger wip) { + this.downstream = observer; + this.set = set; + this.errors = error; + this.wip = wip; + } + + @Override + public void onSubscribe(Disposable d) { + set.add(d); + } + + @Override + public void onError(Throwable e) { + if (errors.tryAddThrowableOrReport(e)) { + tryTerminate(); + } + } + + @Override + public void onComplete() { + tryTerminate(); + } + + void tryTerminate() { + if (wip.decrementAndGet() == 0) { + errors.tryTerminateConsumer(downstream); + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/completable/CompletableMergeDelayErrorIterable.java b/src/main/java/io/reactivex/rxjava3/internal/operators/completable/CompletableMergeDelayErrorIterable.java new file mode 100644 index 0000000000..cb62974753 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/completable/CompletableMergeDelayErrorIterable.java @@ -0,0 +1,100 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.completable; + +import java.util.Iterator; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicInteger; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.CompositeDisposable; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.internal.operators.completable.CompletableMergeArrayDelayError.*; +import io.reactivex.rxjava3.internal.util.AtomicThrowable; + +public final class CompletableMergeDelayErrorIterable extends Completable { + + final Iterable<? extends CompletableSource> sources; + + public CompletableMergeDelayErrorIterable(Iterable<? extends CompletableSource> sources) { + this.sources = sources; + } + + @Override + public void subscribeActual(final CompletableObserver observer) { + final CompositeDisposable set = new CompositeDisposable(); + + observer.onSubscribe(set); + + Iterator<? extends CompletableSource> iterator; + + try { + iterator = Objects.requireNonNull(sources.iterator(), "The source iterator returned is null"); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + observer.onError(e); + return; + } + + final AtomicInteger wip = new AtomicInteger(1); + + final AtomicThrowable errors = new AtomicThrowable(); + set.add(new TryTerminateAndReportDisposable(errors)); + + for (;;) { + if (set.isDisposed()) { + return; + } + + boolean b; + try { + b = iterator.hasNext(); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + errors.tryAddThrowableOrReport(e); + break; + } + + if (!b) { + break; + } + + if (set.isDisposed()) { + return; + } + + CompletableSource c; + + try { + c = Objects.requireNonNull(iterator.next(), "The iterator returned a null CompletableSource"); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + errors.tryAddThrowableOrReport(e); + break; + } + + if (set.isDisposed()) { + return; + } + + wip.getAndIncrement(); + + c.subscribe(new MergeInnerCompletableObserver(observer, set, errors, wip)); + } + + if (wip.decrementAndGet() == 0) { + errors.tryTerminateConsumer(observer); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/completable/CompletableMergeIterable.java b/src/main/java/io/reactivex/rxjava3/internal/operators/completable/CompletableMergeIterable.java new file mode 100644 index 0000000000..3f2dce90e1 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/completable/CompletableMergeIterable.java @@ -0,0 +1,147 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.completable; + +import java.util.Iterator; +import java.util.Objects; +import java.util.concurrent.atomic.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.*; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +public final class CompletableMergeIterable extends Completable { + final Iterable<? extends CompletableSource> sources; + + public CompletableMergeIterable(Iterable<? extends CompletableSource> sources) { + this.sources = sources; + } + + @Override + public void subscribeActual(final CompletableObserver observer) { + final CompositeDisposable set = new CompositeDisposable(); + final AtomicInteger wip = new AtomicInteger(1); + + MergeCompletableObserver shared = new MergeCompletableObserver(observer, set, wip); + + observer.onSubscribe(shared); + + Iterator<? extends CompletableSource> iterator; + + try { + iterator = Objects.requireNonNull(sources.iterator(), "The source iterator returned is null"); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + observer.onError(e); + return; + } + + for (;;) { + if (set.isDisposed()) { + return; + } + + boolean b; + try { + b = iterator.hasNext(); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + set.dispose(); + shared.onError(e); + return; + } + + if (!b) { + break; + } + + if (set.isDisposed()) { + return; + } + + CompletableSource c; + + try { + c = Objects.requireNonNull(iterator.next(), "The iterator returned a null CompletableSource"); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + set.dispose(); + shared.onError(e); + return; + } + + if (set.isDisposed()) { + return; + } + + wip.getAndIncrement(); + + c.subscribe(shared); + } + + shared.onComplete(); + } + + static final class MergeCompletableObserver extends AtomicBoolean implements CompletableObserver, Disposable { + + private static final long serialVersionUID = -7730517613164279224L; + + final CompositeDisposable set; + + final CompletableObserver downstream; + + final AtomicInteger wip; + + MergeCompletableObserver(CompletableObserver actual, CompositeDisposable set, AtomicInteger wip) { + this.downstream = actual; + this.set = set; + this.wip = wip; + } + + @Override + public void onSubscribe(Disposable d) { + set.add(d); + } + + @Override + public void onError(Throwable e) { + set.dispose(); + if (compareAndSet(false, true)) { + downstream.onError(e); + } else { + RxJavaPlugins.onError(e); + } + } + + @Override + public void onComplete() { + if (wip.decrementAndGet() == 0) { + // errors don't decrement wip + downstream.onComplete(); + } + } + + @Override + public void dispose() { + set.dispose(); + set(true); + } + + @Override + public boolean isDisposed() { + return set.isDisposed(); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/completable/CompletableNever.java b/src/main/java/io/reactivex/rxjava3/internal/operators/completable/CompletableNever.java new file mode 100644 index 0000000000..17bc2cc140 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/completable/CompletableNever.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.completable; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.internal.disposables.EmptyDisposable; + +public final class CompletableNever extends Completable { + public static final Completable INSTANCE = new CompletableNever(); + + private CompletableNever() { + } + + @Override + protected void subscribeActual(CompletableObserver observer) { + observer.onSubscribe(EmptyDisposable.NEVER); + } + +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/completable/CompletableObserveOn.java b/src/main/java/io/reactivex/rxjava3/internal/operators/completable/CompletableObserveOn.java new file mode 100644 index 0000000000..b48ade189f --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/completable/CompletableObserveOn.java @@ -0,0 +1,94 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.completable; + +import java.util.concurrent.atomic.AtomicReference; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; + +public final class CompletableObserveOn extends Completable { + + final CompletableSource source; + + final Scheduler scheduler; + public CompletableObserveOn(CompletableSource source, Scheduler scheduler) { + this.source = source; + this.scheduler = scheduler; + } + + @Override + protected void subscribeActual(final CompletableObserver observer) { + source.subscribe(new ObserveOnCompletableObserver(observer, scheduler)); + } + + static final class ObserveOnCompletableObserver + extends AtomicReference<Disposable> + implements CompletableObserver, Disposable, Runnable { + + private static final long serialVersionUID = 8571289934935992137L; + + final CompletableObserver downstream; + + final Scheduler scheduler; + + Throwable error; + + ObserveOnCompletableObserver(CompletableObserver actual, Scheduler scheduler) { + this.downstream = actual; + this.scheduler = scheduler; + } + + @Override + public void dispose() { + DisposableHelper.dispose(this); + } + + @Override + public boolean isDisposed() { + return DisposableHelper.isDisposed(get()); + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.setOnce(this, d)) { + downstream.onSubscribe(this); + } + } + + @Override + public void onError(Throwable e) { + this.error = e; + DisposableHelper.replace(this, scheduler.scheduleDirect(this)); + } + + @Override + public void onComplete() { + DisposableHelper.replace(this, scheduler.scheduleDirect(this)); + } + + @Override + public void run() { + Throwable ex = error; + if (ex != null) { + error = null; + downstream.onError(ex); + } else { + downstream.onComplete(); + } + } + } + +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/completable/CompletableOnErrorComplete.java b/src/main/java/io/reactivex/rxjava3/internal/operators/completable/CompletableOnErrorComplete.java new file mode 100644 index 0000000000..c4252e7f3b --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/completable/CompletableOnErrorComplete.java @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.completable; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.Predicate; + +public final class CompletableOnErrorComplete extends Completable { + + final CompletableSource source; + + final Predicate<? super Throwable> predicate; + + public CompletableOnErrorComplete(CompletableSource source, Predicate<? super Throwable> predicate) { + this.source = source; + this.predicate = predicate; + } + + @Override + protected void subscribeActual(final CompletableObserver observer) { + + source.subscribe(new OnError(observer, predicate)); + } + + static final class OnError implements CompletableObserver { + + private final CompletableObserver downstream; + private final Predicate<? super Throwable> predicate; + + OnError(CompletableObserver observer, + Predicate<? super Throwable> predicate) { + this.downstream = observer; + this.predicate = predicate; + } + + @Override + public void onComplete() { + downstream.onComplete(); + } + + @Override + public void onError(Throwable e) { + boolean b; + + try { + b = predicate.test(e); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + downstream.onError(new CompositeException(e, ex)); + return; + } + + if (b) { + downstream.onComplete(); + } else { + downstream.onError(e); + } + } + + @Override + public void onSubscribe(Disposable d) { + downstream.onSubscribe(d); + } + + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/completable/CompletableOnErrorReturn.java b/src/main/java/io/reactivex/rxjava3/internal/operators/completable/CompletableOnErrorReturn.java new file mode 100644 index 0000000000..9cd91262a5 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/completable/CompletableOnErrorReturn.java @@ -0,0 +1,99 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.completable; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; + +import java.util.Objects; + +/** + * Returns a value generated via a function if the main source signals an onError. + * @param <T> the value type + * @since 3.0.0 + */ +public final class CompletableOnErrorReturn<T> extends Maybe<T> { + + final CompletableSource source; + + final Function<? super Throwable, ? extends T> valueSupplier; + + public CompletableOnErrorReturn(CompletableSource source, + Function<? super Throwable, ? extends T> valueSupplier) { + this.source = source; + this.valueSupplier = valueSupplier; + } + + @Override + protected void subscribeActual(MaybeObserver<? super T> observer) { + source.subscribe(new OnErrorReturnMaybeObserver<>(observer, valueSupplier)); + } + + static final class OnErrorReturnMaybeObserver<T> implements CompletableObserver, Disposable { + + final MaybeObserver<? super T> downstream; + + final Function<? super Throwable, ? extends T> itemSupplier; + + Disposable upstream; + + OnErrorReturnMaybeObserver(MaybeObserver<? super T> actual, + Function<? super Throwable, ? extends T> itemSupplier) { + this.downstream = actual; + this.itemSupplier = itemSupplier; + } + + @Override + public void dispose() { + upstream.dispose(); + } + + @Override + public boolean isDisposed() { + return upstream.isDisposed(); + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.validate(this.upstream, d)) { + this.upstream = d; + + downstream.onSubscribe(this); + } + } + + @Override + public void onError(Throwable e) { + T v; + + try { + v = Objects.requireNonNull(itemSupplier.apply(e), "The itemSupplier returned a null value"); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + downstream.onError(new CompositeException(e, ex)); + return; + } + + downstream.onSuccess(v); + } + + @Override + public void onComplete() { + downstream.onComplete(); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/completable/CompletablePeek.java b/src/main/java/io/reactivex/rxjava3/internal/operators/completable/CompletablePeek.java new file mode 100644 index 0000000000..aeba5381b5 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/completable/CompletablePeek.java @@ -0,0 +1,145 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.completable; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.disposables.*; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +public final class CompletablePeek extends Completable { + + final CompletableSource source; + final Consumer<? super Disposable> onSubscribe; + final Consumer<? super Throwable> onError; + final Action onComplete; + final Action onTerminate; + final Action onAfterTerminate; + final Action onDispose; + + public CompletablePeek(CompletableSource source, Consumer<? super Disposable> onSubscribe, + Consumer<? super Throwable> onError, + Action onComplete, + Action onTerminate, + Action onAfterTerminate, + Action onDispose) { + this.source = source; + this.onSubscribe = onSubscribe; + this.onError = onError; + this.onComplete = onComplete; + this.onTerminate = onTerminate; + this.onAfterTerminate = onAfterTerminate; + this.onDispose = onDispose; + } + + @Override + protected void subscribeActual(final CompletableObserver observer) { + + source.subscribe(new CompletableObserverImplementation(observer)); + } + + final class CompletableObserverImplementation implements CompletableObserver, Disposable { + + final CompletableObserver downstream; + + Disposable upstream; + + CompletableObserverImplementation(CompletableObserver downstream) { + this.downstream = downstream; + } + + @Override + public void onSubscribe(final Disposable d) { + try { + onSubscribe.accept(d); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + d.dispose(); + this.upstream = DisposableHelper.DISPOSED; + EmptyDisposable.error(ex, downstream); + return; + } + if (DisposableHelper.validate(this.upstream, d)) { + this.upstream = d; + downstream.onSubscribe(this); + } + } + + @Override + public void onError(Throwable e) { + if (upstream == DisposableHelper.DISPOSED) { + RxJavaPlugins.onError(e); + return; + } + try { + onError.accept(e); + onTerminate.run(); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + e = new CompositeException(e, ex); + } + + downstream.onError(e); + + doAfter(); + } + + @Override + public void onComplete() { + if (upstream == DisposableHelper.DISPOSED) { + return; + } + + try { + onComplete.run(); + onTerminate.run(); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + downstream.onError(e); + return; + } + + downstream.onComplete(); + + doAfter(); + } + + void doAfter() { + try { + onAfterTerminate.run(); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + RxJavaPlugins.onError(ex); + } + } + + @Override + public void dispose() { + try { + onDispose.run(); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + RxJavaPlugins.onError(e); + } + upstream.dispose(); + } + + @Override + public boolean isDisposed() { + return upstream.isDisposed(); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/completable/CompletableResumeNext.java b/src/main/java/io/reactivex/rxjava3/internal/operators/completable/CompletableResumeNext.java new file mode 100644 index 0000000000..785501511a --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/completable/CompletableResumeNext.java @@ -0,0 +1,102 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.completable; + +import java.util.Objects; +import java.util.concurrent.atomic.AtomicReference; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; + +public final class CompletableResumeNext extends Completable { + + final CompletableSource source; + + final Function<? super Throwable, ? extends CompletableSource> errorMapper; + + public CompletableResumeNext(CompletableSource source, + Function<? super Throwable, ? extends CompletableSource> errorMapper) { + this.source = source; + this.errorMapper = errorMapper; + } + + @Override + protected void subscribeActual(final CompletableObserver observer) { + ResumeNextObserver parent = new ResumeNextObserver(observer, errorMapper); + observer.onSubscribe(parent); + source.subscribe(parent); + } + + static final class ResumeNextObserver + extends AtomicReference<Disposable> + implements CompletableObserver, Disposable { + + private static final long serialVersionUID = 5018523762564524046L; + + final CompletableObserver downstream; + + final Function<? super Throwable, ? extends CompletableSource> errorMapper; + + boolean once; + + ResumeNextObserver(CompletableObserver observer, Function<? super Throwable, ? extends CompletableSource> errorMapper) { + this.downstream = observer; + this.errorMapper = errorMapper; + } + + @Override + public void onSubscribe(Disposable d) { + DisposableHelper.replace(this, d); + } + + @Override + public void onComplete() { + downstream.onComplete(); + } + + @Override + public void onError(Throwable e) { + if (once) { + downstream.onError(e); + return; + } + once = true; + + CompletableSource c; + + try { + c = Objects.requireNonNull(errorMapper.apply(e), "The errorMapper returned a null CompletableSource"); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + downstream.onError(new CompositeException(e, ex)); + return; + } + + c.subscribe(this); + } + + @Override + public boolean isDisposed() { + return DisposableHelper.isDisposed(get()); + } + + @Override + public void dispose() { + DisposableHelper.dispose(this); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/completable/CompletableSubscribeOn.java b/src/main/java/io/reactivex/rxjava3/internal/operators/completable/CompletableSubscribeOn.java new file mode 100644 index 0000000000..748602f978 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/completable/CompletableSubscribeOn.java @@ -0,0 +1,94 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.completable; + +import java.util.concurrent.atomic.AtomicReference; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.disposables.*; + +public final class CompletableSubscribeOn extends Completable { + final CompletableSource source; + + final Scheduler scheduler; + + public CompletableSubscribeOn(CompletableSource source, Scheduler scheduler) { + this.source = source; + this.scheduler = scheduler; + } + + @Override + protected void subscribeActual(final CompletableObserver observer) { + + final SubscribeOnObserver parent = new SubscribeOnObserver(observer, source); + observer.onSubscribe(parent); + + Disposable f = scheduler.scheduleDirect(parent); + + parent.task.replace(f); + + } + + static final class SubscribeOnObserver + extends AtomicReference<Disposable> + implements CompletableObserver, Disposable, Runnable { + + private static final long serialVersionUID = 7000911171163930287L; + + final CompletableObserver downstream; + + final SequentialDisposable task; + + final CompletableSource source; + + SubscribeOnObserver(CompletableObserver actual, CompletableSource source) { + this.downstream = actual; + this.source = source; + this.task = new SequentialDisposable(); + } + + @Override + public void run() { + source.subscribe(this); + } + + @Override + public void onSubscribe(Disposable d) { + DisposableHelper.setOnce(this, d); + } + + @Override + public void onError(Throwable e) { + downstream.onError(e); + } + + @Override + public void onComplete() { + downstream.onComplete(); + } + + @Override + public void dispose() { + DisposableHelper.dispose(this); + task.dispose(); + } + + @Override + public boolean isDisposed() { + return DisposableHelper.isDisposed(get()); + } + } + +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/completable/CompletableTakeUntilCompletable.java b/src/main/java/io/reactivex/rxjava3/internal/operators/completable/CompletableTakeUntilCompletable.java new file mode 100644 index 0000000000..364484ab10 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/completable/CompletableTakeUntilCompletable.java @@ -0,0 +1,144 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.completable; + +import java.util.concurrent.atomic.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +/** + * Terminates the sequence if either the main or the other Completable terminate. + * <p>History: 2.1.17 - experimental + * @since 2.2 + */ +public final class CompletableTakeUntilCompletable extends Completable { + + final Completable source; + + final CompletableSource other; + + public CompletableTakeUntilCompletable(Completable source, + CompletableSource other) { + this.source = source; + this.other = other; + } + + @Override + protected void subscribeActual(CompletableObserver observer) { + TakeUntilMainObserver parent = new TakeUntilMainObserver(observer); + observer.onSubscribe(parent); + + other.subscribe(parent.other); + source.subscribe(parent); + } + + static final class TakeUntilMainObserver extends AtomicReference<Disposable> + implements CompletableObserver, Disposable { + + private static final long serialVersionUID = 3533011714830024923L; + + final CompletableObserver downstream; + + final OtherObserver other; + + final AtomicBoolean once; + + TakeUntilMainObserver(CompletableObserver downstream) { + this.downstream = downstream; + this.other = new OtherObserver(this); + this.once = new AtomicBoolean(); + } + + @Override + public void dispose() { + if (once.compareAndSet(false, true)) { + DisposableHelper.dispose(this); + DisposableHelper.dispose(other); + } + } + + @Override + public boolean isDisposed() { + return once.get(); + } + + @Override + public void onSubscribe(Disposable d) { + DisposableHelper.setOnce(this, d); + } + + @Override + public void onComplete() { + if (once.compareAndSet(false, true)) { + DisposableHelper.dispose(other); + downstream.onComplete(); + } + } + + @Override + public void onError(Throwable e) { + if (once.compareAndSet(false, true)) { + DisposableHelper.dispose(other); + downstream.onError(e); + } else { + RxJavaPlugins.onError(e); + } + } + + void innerComplete() { + if (once.compareAndSet(false, true)) { + DisposableHelper.dispose(this); + downstream.onComplete(); + } + } + + void innerError(Throwable e) { + if (once.compareAndSet(false, true)) { + DisposableHelper.dispose(this); + downstream.onError(e); + } else { + RxJavaPlugins.onError(e); + } + } + + static final class OtherObserver extends AtomicReference<Disposable> + implements CompletableObserver { + + private static final long serialVersionUID = 5176264485428790318L; + final TakeUntilMainObserver parent; + + OtherObserver(TakeUntilMainObserver parent) { + this.parent = parent; + } + + @Override + public void onSubscribe(Disposable d) { + DisposableHelper.setOnce(this, d); + } + + @Override + public void onComplete() { + parent.innerComplete(); + } + + @Override + public void onError(Throwable e) { + parent.innerError(e); + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/completable/CompletableTimeout.java b/src/main/java/io/reactivex/rxjava3/internal/operators/completable/CompletableTimeout.java new file mode 100644 index 0000000000..80f40dfd72 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/completable/CompletableTimeout.java @@ -0,0 +1,137 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.completable; + +import static io.reactivex.rxjava3.internal.util.ExceptionHelper.timeoutMessage; + +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicBoolean; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.*; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +public final class CompletableTimeout extends Completable { + + final CompletableSource source; + final long timeout; + final TimeUnit unit; + final Scheduler scheduler; + final CompletableSource other; + + public CompletableTimeout(CompletableSource source, long timeout, + TimeUnit unit, Scheduler scheduler, CompletableSource other) { + this.source = source; + this.timeout = timeout; + this.unit = unit; + this.scheduler = scheduler; + this.other = other; + } + + @Override + public void subscribeActual(final CompletableObserver observer) { + final CompositeDisposable set = new CompositeDisposable(); + observer.onSubscribe(set); + + final AtomicBoolean once = new AtomicBoolean(); + + Disposable timer = scheduler.scheduleDirect(new DisposeTask(once, set, observer), timeout, unit); + + set.add(timer); + + source.subscribe(new TimeOutObserver(set, once, observer)); + } + + static final class TimeOutObserver implements CompletableObserver { + + private final CompositeDisposable set; + private final AtomicBoolean once; + private final CompletableObserver downstream; + + TimeOutObserver(CompositeDisposable set, AtomicBoolean once, CompletableObserver observer) { + this.set = set; + this.once = once; + this.downstream = observer; + } + + @Override + public void onSubscribe(Disposable d) { + set.add(d); + } + + @Override + public void onError(Throwable e) { + if (once.compareAndSet(false, true)) { + set.dispose(); + downstream.onError(e); + } else { + RxJavaPlugins.onError(e); + } + } + + @Override + public void onComplete() { + if (once.compareAndSet(false, true)) { + set.dispose(); + downstream.onComplete(); + } + } + + } + + final class DisposeTask implements Runnable { + private final AtomicBoolean once; + final CompositeDisposable set; + final CompletableObserver downstream; + + DisposeTask(AtomicBoolean once, CompositeDisposable set, CompletableObserver observer) { + this.once = once; + this.set = set; + this.downstream = observer; + } + + @Override + public void run() { + if (once.compareAndSet(false, true)) { + set.clear(); + if (other == null) { + downstream.onError(new TimeoutException(timeoutMessage(timeout, unit))); + } else { + other.subscribe(new DisposeObserver()); + } + } + } + + final class DisposeObserver implements CompletableObserver { + + @Override + public void onSubscribe(Disposable d) { + set.add(d); + } + + @Override + public void onError(Throwable e) { + set.dispose(); + downstream.onError(e); + } + + @Override + public void onComplete() { + set.dispose(); + downstream.onComplete(); + } + + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/completable/CompletableTimer.java b/src/main/java/io/reactivex/rxjava3/internal/operators/completable/CompletableTimer.java new file mode 100644 index 0000000000..b50fd5db2e --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/completable/CompletableTimer.java @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.completable; + +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; + +/** + * Signals an {@code onComplete} event after the specified delay. + */ +public final class CompletableTimer extends Completable { + + final long delay; + final TimeUnit unit; + final Scheduler scheduler; + + public CompletableTimer(long delay, TimeUnit unit, Scheduler scheduler) { + this.delay = delay; + this.unit = unit; + this.scheduler = scheduler; + } + + @Override + protected void subscribeActual(final CompletableObserver observer) { + TimerDisposable parent = new TimerDisposable(observer); + observer.onSubscribe(parent); + parent.setFuture(scheduler.scheduleDirect(parent, delay, unit)); + } + + static final class TimerDisposable extends AtomicReference<Disposable> implements Disposable, Runnable { + + private static final long serialVersionUID = 3167244060586201109L; + final CompletableObserver downstream; + + TimerDisposable(final CompletableObserver downstream) { + this.downstream = downstream; + } + + @Override + public void run() { + downstream.onComplete(); + } + + @Override + public void dispose() { + DisposableHelper.dispose(this); + } + + @Override + public boolean isDisposed() { + return DisposableHelper.isDisposed(get()); + } + + void setFuture(Disposable d) { + DisposableHelper.replace(this, d); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/completable/CompletableToFlowable.java b/src/main/java/io/reactivex/rxjava3/internal/operators/completable/CompletableToFlowable.java new file mode 100644 index 0000000000..346267ce13 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/completable/CompletableToFlowable.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.completable; + +import org.reactivestreams.Subscriber; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.internal.operators.flowable.FlowableFromCompletable; + +public final class CompletableToFlowable<T> extends Flowable<T> { + + final CompletableSource source; + + public CompletableToFlowable(CompletableSource source) { + this.source = source; + } + + @Override + protected void subscribeActual(Subscriber<? super T> s) { + source.subscribe(new FlowableFromCompletable.FromCompletableObserver<>(s)); + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/completable/CompletableToObservable.java b/src/main/java/io/reactivex/rxjava3/internal/operators/completable/CompletableToObservable.java new file mode 100644 index 0000000000..ccf1a9e504 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/completable/CompletableToObservable.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.completable; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.internal.operators.observable.ObservableFromCompletable; + +/** + * Wraps a Completable and exposes it as an Observable. + * + * @param <T> the value type + */ +public final class CompletableToObservable<T> extends Observable<T> { + + final CompletableSource source; + + public CompletableToObservable(CompletableSource source) { + this.source = source; + } + + @Override + protected void subscribeActual(Observer<? super T> observer) { + source.subscribe(new ObservableFromCompletable.FromCompletableObserver<>(observer)); + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/completable/CompletableToSingle.java b/src/main/java/io/reactivex/rxjava3/internal/operators/completable/CompletableToSingle.java new file mode 100644 index 0000000000..58e47247df --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/completable/CompletableToSingle.java @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.completable; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.Supplier; + +public final class CompletableToSingle<T> extends Single<T> { + final CompletableSource source; + + final Supplier<? extends T> completionValueSupplier; + + final T completionValue; + + public CompletableToSingle(CompletableSource source, + Supplier<? extends T> completionValueSupplier, T completionValue) { + this.source = source; + this.completionValue = completionValue; + this.completionValueSupplier = completionValueSupplier; + } + + @Override + protected void subscribeActual(final SingleObserver<? super T> observer) { + source.subscribe(new ToSingle(observer)); + } + + final class ToSingle implements CompletableObserver { + + private final SingleObserver<? super T> observer; + + ToSingle(SingleObserver<? super T> observer) { + this.observer = observer; + } + + @Override + public void onComplete() { + T v; + + if (completionValueSupplier != null) { + try { + v = completionValueSupplier.get(); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + observer.onError(e); + return; + } + } else { + v = completionValue; + } + + if (v == null) { + observer.onError(new NullPointerException("The value supplied is null")); + } else { + observer.onSuccess(v); + } + } + + @Override + public void onError(Throwable e) { + observer.onError(e); + } + + @Override + public void onSubscribe(Disposable d) { + observer.onSubscribe(d); + } + + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/completable/CompletableUsing.java b/src/main/java/io/reactivex/rxjava3/internal/operators/completable/CompletableUsing.java new file mode 100644 index 0000000000..648e6529e2 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/completable/CompletableUsing.java @@ -0,0 +1,199 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.completable; + +import java.util.Objects; +import java.util.concurrent.atomic.AtomicReference; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.disposables.*; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +public final class CompletableUsing<R> extends Completable { + + final Supplier<R> resourceSupplier; + final Function<? super R, ? extends CompletableSource> completableFunction; + final Consumer<? super R> disposer; + final boolean eager; + + public CompletableUsing(Supplier<R> resourceSupplier, + Function<? super R, ? extends CompletableSource> completableFunction, Consumer<? super R> disposer, + boolean eager) { + this.resourceSupplier = resourceSupplier; + this.completableFunction = completableFunction; + this.disposer = disposer; + this.eager = eager; + } + + @Override + protected void subscribeActual(CompletableObserver observer) { + R resource; + + try { + resource = resourceSupplier.get(); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + EmptyDisposable.error(ex, observer); + return; + } + + CompletableSource source; + + try { + source = Objects.requireNonNull(completableFunction.apply(resource), "The completableFunction returned a null CompletableSource"); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + if (eager) { + try { + disposer.accept(resource); + } catch (Throwable exc) { + Exceptions.throwIfFatal(exc); + EmptyDisposable.error(new CompositeException(ex, exc), observer); + return; + } + } + + EmptyDisposable.error(ex, observer); + + if (!eager) { + try { + disposer.accept(resource); + } catch (Throwable exc) { + Exceptions.throwIfFatal(exc); + RxJavaPlugins.onError(exc); + } + } + return; + } + + source.subscribe(new UsingObserver<>(observer, resource, disposer, eager)); + } + + static final class UsingObserver<R> + extends AtomicReference<Object> + implements CompletableObserver, Disposable { + + private static final long serialVersionUID = -674404550052917487L; + + final CompletableObserver downstream; + + final Consumer<? super R> disposer; + + final boolean eager; + + Disposable upstream; + + UsingObserver(CompletableObserver actual, R resource, Consumer<? super R> disposer, boolean eager) { + super(resource); + this.downstream = actual; + this.disposer = disposer; + this.eager = eager; + } + + @Override + public void dispose() { + if (eager) { + disposeResource(); + upstream.dispose(); + upstream = DisposableHelper.DISPOSED; + } else { + upstream.dispose(); + upstream = DisposableHelper.DISPOSED; + disposeResource(); + } + } + + @SuppressWarnings("unchecked") + void disposeResource() { + Object resource = getAndSet(this); + if (resource != this) { + try { + disposer.accept((R)resource); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + RxJavaPlugins.onError(ex); + } + } + } + + @Override + public boolean isDisposed() { + return upstream.isDisposed(); + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.validate(this.upstream, d)) { + this.upstream = d; + + downstream.onSubscribe(this); + } + } + + @SuppressWarnings("unchecked") + @Override + public void onError(Throwable e) { + upstream = DisposableHelper.DISPOSED; + if (eager) { + Object resource = getAndSet(this); + if (resource != this) { + try { + disposer.accept((R)resource); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + e = new CompositeException(e, ex); + } + } else { + return; + } + } + + downstream.onError(e); + + if (!eager) { + disposeResource(); + } + } + + @SuppressWarnings("unchecked") + @Override + public void onComplete() { + upstream = DisposableHelper.DISPOSED; + if (eager) { + Object resource = getAndSet(this); + if (resource != this) { + try { + disposer.accept((R)resource); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + downstream.onError(ex); + return; + } + } else { + return; + } + } + + downstream.onComplete(); + + if (!eager) { + disposeResource(); + } + } + } + +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/AbstractBackpressureThrottlingSubscriber.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/AbstractBackpressureThrottlingSubscriber.java new file mode 100644 index 0000000000..62c7d07a88 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/AbstractBackpressureThrottlingSubscriber.java @@ -0,0 +1,165 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import io.reactivex.rxjava3.core.FlowableSubscriber; +import io.reactivex.rxjava3.internal.subscriptions.SubscriptionHelper; +import io.reactivex.rxjava3.internal.util.BackpressureHelper; +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; + +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; + +/** + * Abstract base class for operators that throttle excessive updates from upstream in case if + * downstream {@link Subscriber} is not ready to receive updates. + * + * @param <T> the upstream value type + * @param <R> the downstream value type + */ +abstract class AbstractBackpressureThrottlingSubscriber<T, R> extends AtomicInteger implements FlowableSubscriber<T>, Subscription { + + private static final long serialVersionUID = -5050301752721603566L; + + final Subscriber<? super R> downstream; + + Subscription upstream; + + volatile boolean done; + Throwable error; + + volatile boolean cancelled; + + final AtomicLong requested = new AtomicLong(); + + final AtomicReference<R> current = new AtomicReference<>(); + + AbstractBackpressureThrottlingSubscriber(Subscriber<? super R> downstream) { + this.downstream = downstream; + } + + @Override + public void onSubscribe(Subscription s) { + if (SubscriptionHelper.validate(this.upstream, s)) { + this.upstream = s; + downstream.onSubscribe(this); + s.request(Long.MAX_VALUE); + } + } + + @Override + public abstract void onNext(T t); + + @Override + public void onError(Throwable t) { + error = t; + done = true; + drain(); + } + + @Override + public void onComplete() { + done = true; + drain(); + } + + @Override + public void request(long n) { + if (SubscriptionHelper.validate(n)) { + BackpressureHelper.add(requested, n); + drain(); + } + } + + @Override + public void cancel() { + if (!cancelled) { + cancelled = true; + upstream.cancel(); + + if (getAndIncrement() == 0) { + current.lazySet(null); + } + } + } + + void drain() { + if (getAndIncrement() != 0) { + return; + } + final Subscriber<? super R> a = downstream; + int missed = 1; + final AtomicLong r = requested; + final AtomicReference<R> q = current; + + for (;;) { + long e = 0L; + + while (e != r.get()) { + boolean d = done; + R v = q.getAndSet(null); + boolean empty = v == null; + + if (checkTerminated(d, empty, a, q)) { + return; + } + + if (empty) { + break; + } + + a.onNext(v); + + e++; + } + + if (e == r.get() && checkTerminated(done, q.get() == null, a, q)) { + return; + } + + if (e != 0L) { + BackpressureHelper.produced(r, e); + } + + missed = addAndGet(-missed); + if (missed == 0) { + break; + } + } + } + + boolean checkTerminated(boolean d, boolean empty, Subscriber<?> a, AtomicReference<R> q) { + if (cancelled) { + q.lazySet(null); + return true; + } + + if (d) { + Throwable e = error; + if (e != null) { + q.lazySet(null); + a.onError(e); + return true; + } else + if (empty) { + a.onComplete(); + return true; + } + } + + return false; + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/AbstractFlowableWithUpstream.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/AbstractFlowableWithUpstream.java new file mode 100644 index 0000000000..4a9484f743 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/AbstractFlowableWithUpstream.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import org.reactivestreams.Publisher; + +import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.internal.fuseable.HasUpstreamPublisher; + +import java.util.Objects; + +/** + * Abstract base class for operators that take an upstream + * source {@link Publisher}. + * + * @param <T> the upstream value type + * @param <R> the output value type + */ +abstract class AbstractFlowableWithUpstream<T, R> extends Flowable<R> implements HasUpstreamPublisher<T> { + + /** + * The upstream source Publisher. + */ + protected final Flowable<T> source; + + /** + * Constructs a FlowableSource wrapping the given non-null (verified) + * source Publisher. + * @param source the source (upstream) Publisher instance, not null (verified) + */ + AbstractFlowableWithUpstream(Flowable<T> source) { + this.source = Objects.requireNonNull(source, "source is null"); + } + + @Override + public final Publisher<T> source() { + return source; + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/BlockingFlowableIterable.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/BlockingFlowableIterable.java new file mode 100644 index 0000000000..11239d04a7 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/BlockingFlowableIterable.java @@ -0,0 +1,196 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import java.util.*; +import java.util.concurrent.atomic.AtomicReference; +import java.util.concurrent.locks.*; + +import org.reactivestreams.Subscription; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.internal.subscriptions.SubscriptionHelper; +import io.reactivex.rxjava3.internal.util.*; +import io.reactivex.rxjava3.operators.SpscArrayQueue; + +public final class BlockingFlowableIterable<T> implements Iterable<T> { + final Flowable<T> source; + + final int bufferSize; + + public BlockingFlowableIterable(Flowable<T> source, int bufferSize) { + this.source = source; + this.bufferSize = bufferSize; + } + + @Override + public Iterator<T> iterator() { + BlockingFlowableIterator<T> it = new BlockingFlowableIterator<>(bufferSize); + source.subscribe(it); + return it; + } + + static final class BlockingFlowableIterator<T> + extends AtomicReference<Subscription> + implements FlowableSubscriber<T>, Iterator<T>, Runnable, Disposable { + + private static final long serialVersionUID = 6695226475494099826L; + + final SpscArrayQueue<T> queue; + + final long batchSize; + + final long limit; + + final Lock lock; + + final Condition condition; + + long produced; + + volatile boolean done; + volatile Throwable error; + + BlockingFlowableIterator(int batchSize) { + this.queue = new SpscArrayQueue<>(batchSize); + this.batchSize = batchSize; + this.limit = batchSize - (batchSize >> 2); + this.lock = new ReentrantLock(); + this.condition = lock.newCondition(); + } + + @Override + public boolean hasNext() { + for (;;) { + if (isDisposed()) { + Throwable e = error; + if (e != null) { + throw ExceptionHelper.wrapOrThrow(e); + } + return false; + } + boolean d = done; + boolean empty = queue.isEmpty(); + if (d) { + Throwable e = error; + if (e != null) { + throw ExceptionHelper.wrapOrThrow(e); + } else + if (empty) { + return false; + } + } + if (empty) { + BlockingHelper.verifyNonBlocking(); + lock.lock(); + try { + while (!done && queue.isEmpty() && !isDisposed()) { + condition.await(); + } + } catch (InterruptedException ex) { + run(); + throw ExceptionHelper.wrapOrThrow(ex); + } finally { + lock.unlock(); + } + } else { + return true; + } + } + } + + @Override + public T next() { + if (hasNext()) { + T v = queue.poll(); + + long p = produced + 1; + if (p == limit) { + produced = 0; + get().request(p); + } else { + produced = p; + } + + return v; + } + throw new NoSuchElementException(); + } + + @Override + public void onSubscribe(Subscription s) { + SubscriptionHelper.setOnce(this, s, batchSize); + } + + @Override + public void onNext(T t) { + if (!queue.offer(t)) { + // Error must be set first before calling cancel to avoid race + // with hasNext(), which checks for cancel first before checking + // for error. + error = new QueueOverflowException(); + SubscriptionHelper.cancel(this); + onComplete(); + } else { + signalConsumer(); + } + } + + @Override + public void onError(Throwable t) { + error = t; + done = true; + signalConsumer(); + } + + @Override + public void onComplete() { + done = true; + signalConsumer(); + } + + void signalConsumer() { + lock.lock(); + try { + condition.signalAll(); + } finally { + lock.unlock(); + } + } + + @Override + public void run() { + SubscriptionHelper.cancel(this); + signalConsumer(); + } + + @Override // otherwise default method which isn't available in Java 7 + public void remove() { + throw new UnsupportedOperationException("remove"); + } + + @Override + public void dispose() { + SubscriptionHelper.cancel(this); + signalConsumer(); // Just in case it is currently blocking in hasNext. + } + + @Override + public boolean isDisposed() { + return get() == SubscriptionHelper.CANCELLED; + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/BlockingFlowableLatest.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/BlockingFlowableLatest.java new file mode 100644 index 0000000000..7628776ae0 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/BlockingFlowableLatest.java @@ -0,0 +1,118 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import java.util.*; +import java.util.concurrent.Semaphore; +import java.util.concurrent.atomic.AtomicReference; + +import org.reactivestreams.Publisher; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.internal.util.*; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.subscribers.DisposableSubscriber; + +/** + * Wait for and iterate over the latest values of the source observable. If the source works faster than the + * iterator, values may be skipped, but not the {@code onError} or {@code onComplete} events. + * @param <T> the value type emitted + */ +public final class BlockingFlowableLatest<T> implements Iterable<T> { + + final Publisher<? extends T> source; + + public BlockingFlowableLatest(Publisher<? extends T> source) { + this.source = source; + } + + @Override + public Iterator<T> iterator() { + LatestSubscriberIterator<T> lio = new LatestSubscriberIterator<>(); + Flowable.<T>fromPublisher(source).materialize().subscribe(lio); + return lio; + } + + /** Subscriber of source, iterator for output. */ + static final class LatestSubscriberIterator<T> extends DisposableSubscriber<Notification<T>> implements Iterator<T> { + final Semaphore notify = new Semaphore(0); + // observer's notification + final AtomicReference<Notification<T>> value = new AtomicReference<>(); + + // iterator's notification + Notification<T> iteratorNotification; + + @Override + public void onNext(Notification<T> args) { + boolean wasNotAvailable = value.getAndSet(args) == null; + if (wasNotAvailable) { + notify.release(); + } + } + + @Override + public void onError(Throwable e) { + RxJavaPlugins.onError(e); + } + + @Override + public void onComplete() { + // not expected + } + + @Override + public boolean hasNext() { + if (iteratorNotification != null && iteratorNotification.isOnError()) { + throw ExceptionHelper.wrapOrThrow(iteratorNotification.getError()); + } + if (iteratorNotification == null || iteratorNotification.isOnNext()) { + if (iteratorNotification == null) { + try { + BlockingHelper.verifyNonBlocking(); + notify.acquire(); + } catch (InterruptedException ex) { + dispose(); + iteratorNotification = Notification.createOnError(ex); + throw ExceptionHelper.wrapOrThrow(ex); + } + + Notification<T> n = value.getAndSet(null); + iteratorNotification = n; + if (n.isOnError()) { + throw ExceptionHelper.wrapOrThrow(n.getError()); + } + } + } + return iteratorNotification.isOnNext(); + } + + @Override + public T next() { + if (hasNext()) { + if (iteratorNotification.isOnNext()) { + T v = iteratorNotification.getValue(); + iteratorNotification = null; + return v; + } + } + throw new NoSuchElementException(); + } + + @Override + public void remove() { + throw new UnsupportedOperationException("Read-only iterator."); + } + + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/BlockingFlowableMostRecent.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/BlockingFlowableMostRecent.java new file mode 100644 index 0000000000..7c41634c55 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/BlockingFlowableMostRecent.java @@ -0,0 +1,119 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import java.util.*; + +import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.internal.util.*; +import io.reactivex.rxjava3.subscribers.DefaultSubscriber; + +/** + * Returns an Iterable that always returns the item most recently emitted by an Observable, or a + * seed value if no item has yet been emitted. + * <p> + * <img width="640" height="490" src="https://github.com/ReactiveX/RxJava/wiki/images/rx-operators/B.mostRecent.v3.png" alt=""> + * + * @param <T> the value type + */ +public final class BlockingFlowableMostRecent<T> implements Iterable<T> { + + final Flowable<T> source; + + final T initialValue; + + public BlockingFlowableMostRecent(Flowable<T> source, T initialValue) { + this.source = source; + this.initialValue = initialValue; + } + + @Override + public Iterator<T> iterator() { + MostRecentSubscriber<T> mostRecentSubscriber = new MostRecentSubscriber<>(initialValue); + + source.subscribe(mostRecentSubscriber); + + return mostRecentSubscriber.getIterable(); + } + + static final class MostRecentSubscriber<T> extends DefaultSubscriber<T> { + volatile Object value; + + MostRecentSubscriber(T value) { + this.value = NotificationLite.next(value); + } + + @Override + public void onComplete() { + value = NotificationLite.complete(); + } + + @Override + public void onError(Throwable e) { + value = NotificationLite.error(e); + } + + @Override + public void onNext(T args) { + value = NotificationLite.next(args); + } + + /** + * The {@link Iterator} return is not thread safe. In other words don't call {@link Iterator#hasNext()} in one + * thread expect {@link Iterator#next()} called from a different thread to work. + * @return the Iterator + */ + public Iterator getIterable() { + return new Iterator(); + } + + final class Iterator implements java.util.Iterator<T> { + /** + * buffer to make sure that the state of the iterator doesn't change between calling hasNext() and next(). + */ + private Object buf; + + @Override + public boolean hasNext() { + buf = value; + return !NotificationLite.isComplete(buf); + } + + @Override + public T next() { + try { + // if hasNext wasn't called before calling next. + if (buf == null) { + buf = value; + } + if (NotificationLite.isComplete(buf)) { + throw new NoSuchElementException(); + } + if (NotificationLite.isError(buf)) { + throw ExceptionHelper.wrapOrThrow(NotificationLite.getError(buf)); + } + return NotificationLite.getValue(buf); + } + finally { + buf = null; + } + } + + @Override + public void remove() { + throw new UnsupportedOperationException("Read only iterator"); + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/BlockingFlowableNext.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/BlockingFlowableNext.java new file mode 100644 index 0000000000..9704d2d210 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/BlockingFlowableNext.java @@ -0,0 +1,172 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import java.util.*; +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicInteger; + +import org.reactivestreams.Publisher; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.internal.util.*; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.subscribers.DisposableSubscriber; + +/** + * Returns an Iterable that blocks until the Observable emits another item, then returns that item. + * <p> + * <img width="640" height="490" src="https://github.com/ReactiveX/RxJava/wiki/images/rx-operators/B.next.v3.png" alt=""> + * + * @param <T> the value type + */ +public final class BlockingFlowableNext<T> implements Iterable<T> { + + final Publisher<? extends T> source; + + public BlockingFlowableNext(Publisher<? extends T> source) { + this.source = source; + } + + @Override + public Iterator<T> iterator() { + NextSubscriber<T> nextSubscriber = new NextSubscriber<>(); + return new NextIterator<>(source, nextSubscriber); + } + + // test needs to access the observer.waiting flag + static final class NextIterator<T> implements Iterator<T> { + + private final NextSubscriber<T> subscriber; + private final Publisher<? extends T> items; + private T next; + private boolean hasNext = true; + private boolean isNextConsumed = true; + private Throwable error; + private boolean started; + + NextIterator(Publisher<? extends T> items, NextSubscriber<T> subscriber) { + this.items = items; + this.subscriber = subscriber; + } + + @Override + public boolean hasNext() { + if (error != null) { + // If any error has already been thrown, throw it again. + throw ExceptionHelper.wrapOrThrow(error); + } + // Since an iterator should not be used in different thread, + // so we do not need any synchronization. + if (!hasNext) { + // the iterator has reached the end. + return false; + } + // next has not been used yet. + return !isNextConsumed || moveToNext(); + } + + private boolean moveToNext() { + try { + if (!started) { + started = true; + // if not started, start now + subscriber.setWaiting(); + Flowable.<T>fromPublisher(items) + .materialize().subscribe(subscriber); + } + + Notification<T> nextNotification = subscriber.takeNext(); + if (nextNotification.isOnNext()) { + isNextConsumed = false; + next = nextNotification.getValue(); + return true; + } + // If an observable is completed or fails, + // hasNext() always return false. + hasNext = false; + if (nextNotification.isOnComplete()) { + return false; + } + error = nextNotification.getError(); + throw ExceptionHelper.wrapOrThrow(error); + } catch (InterruptedException e) { + subscriber.dispose(); + error = e; + throw ExceptionHelper.wrapOrThrow(e); + } + } + + @Override + public T next() { + if (error != null) { + // If any error has already been thrown, throw it again. + throw ExceptionHelper.wrapOrThrow(error); + } + if (hasNext()) { + isNextConsumed = true; + return next; + } + else { + throw new NoSuchElementException("No more elements"); + } + } + + @Override + public void remove() { + throw new UnsupportedOperationException("Read only iterator"); + } + } + + static final class NextSubscriber<T> extends DisposableSubscriber<Notification<T>> { + private final BlockingQueue<Notification<T>> buf = new ArrayBlockingQueue<>(1); + final AtomicInteger waiting = new AtomicInteger(); + + @Override + public void onComplete() { + // ignore + } + + @Override + public void onError(Throwable e) { + RxJavaPlugins.onError(e); + } + + @Override + public void onNext(Notification<T> args) { + + if (waiting.getAndSet(0) == 1 || !args.isOnNext()) { + Notification<T> toOffer = args; + while (!buf.offer(toOffer)) { + Notification<T> concurrentItem = buf.poll(); + + // in case if we won race condition with onComplete/onError method + if (concurrentItem != null && !concurrentItem.isOnNext()) { + toOffer = concurrentItem; + } + } + } + + } + + public Notification<T> takeNext() throws InterruptedException { + setWaiting(); + BlockingHelper.verifyNonBlocking(); + return buf.take(); + } + void setWaiting() { + waiting.set(1); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableAll.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableAll.java new file mode 100644 index 0000000000..7f8271733d --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableAll.java @@ -0,0 +1,108 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.Predicate; +import io.reactivex.rxjava3.internal.subscriptions.*; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +public final class FlowableAll<T> extends AbstractFlowableWithUpstream<T, Boolean> { + + final Predicate<? super T> predicate; + + public FlowableAll(Flowable<T> source, Predicate<? super T> predicate) { + super(source); + this.predicate = predicate; + } + + @Override + protected void subscribeActual(Subscriber<? super Boolean> s) { + source.subscribe(new AllSubscriber<>(s, predicate)); + } + + static final class AllSubscriber<T> extends DeferredScalarSubscription<Boolean> implements FlowableSubscriber<T> { + + private static final long serialVersionUID = -3521127104134758517L; + final Predicate<? super T> predicate; + + Subscription upstream; + + boolean done; + + AllSubscriber(Subscriber<? super Boolean> actual, Predicate<? super T> predicate) { + super(actual); + this.predicate = predicate; + } + + @Override + public void onSubscribe(Subscription s) { + if (SubscriptionHelper.validate(this.upstream, s)) { + this.upstream = s; + downstream.onSubscribe(this); + s.request(Long.MAX_VALUE); + } + } + + @Override + public void onNext(T t) { + if (done) { + return; + } + boolean b; + try { + b = predicate.test(t); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + upstream.cancel(); + onError(e); + return; + } + if (!b) { + done = true; + upstream.cancel(); + complete(false); + } + } + + @Override + public void onError(Throwable t) { + if (done) { + RxJavaPlugins.onError(t); + return; + } + done = true; + downstream.onError(t); + } + + @Override + public void onComplete() { + if (done) { + return; + } + done = true; + + complete(true); + } + + @Override + public void cancel() { + super.cancel(); + upstream.cancel(); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableAllSingle.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableAllSingle.java new file mode 100644 index 0000000000..03d269e738 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableAllSingle.java @@ -0,0 +1,127 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import org.reactivestreams.Subscription; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.Predicate; +import io.reactivex.rxjava3.internal.fuseable.FuseToFlowable; +import io.reactivex.rxjava3.internal.subscriptions.SubscriptionHelper; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +public final class FlowableAllSingle<T> extends Single<Boolean> implements FuseToFlowable<Boolean> { + + final Flowable<T> source; + + final Predicate<? super T> predicate; + + public FlowableAllSingle(Flowable<T> source, Predicate<? super T> predicate) { + this.source = source; + this.predicate = predicate; + } + + @Override + protected void subscribeActual(SingleObserver<? super Boolean> observer) { + source.subscribe(new AllSubscriber<>(observer, predicate)); + } + + @Override + public Flowable<Boolean> fuseToFlowable() { + return RxJavaPlugins.onAssembly(new FlowableAll<>(source, predicate)); + } + + static final class AllSubscriber<T> implements FlowableSubscriber<T>, Disposable { + + final SingleObserver<? super Boolean> downstream; + + final Predicate<? super T> predicate; + + Subscription upstream; + + boolean done; + + AllSubscriber(SingleObserver<? super Boolean> actual, Predicate<? super T> predicate) { + this.downstream = actual; + this.predicate = predicate; + } + + @Override + public void onSubscribe(Subscription s) { + if (SubscriptionHelper.validate(this.upstream, s)) { + this.upstream = s; + downstream.onSubscribe(this); + s.request(Long.MAX_VALUE); + } + } + + @Override + public void onNext(T t) { + if (done) { + return; + } + boolean b; + try { + b = predicate.test(t); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + upstream.cancel(); + upstream = SubscriptionHelper.CANCELLED; + onError(e); + return; + } + if (!b) { + done = true; + upstream.cancel(); + upstream = SubscriptionHelper.CANCELLED; + downstream.onSuccess(false); + } + } + + @Override + public void onError(Throwable t) { + if (done) { + RxJavaPlugins.onError(t); + return; + } + done = true; + upstream = SubscriptionHelper.CANCELLED; + downstream.onError(t); + } + + @Override + public void onComplete() { + if (done) { + return; + } + done = true; + upstream = SubscriptionHelper.CANCELLED; + + downstream.onSuccess(true); + } + + @Override + public void dispose() { + upstream.cancel(); + upstream = SubscriptionHelper.CANCELLED; + } + + @Override + public boolean isDisposed() { + return upstream == SubscriptionHelper.CANCELLED; + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableAmb.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableAmb.java new file mode 100644 index 0000000000..5646fdb993 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableAmb.java @@ -0,0 +1,226 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import java.util.concurrent.atomic.*; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.internal.subscriptions.*; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +public final class FlowableAmb<T> extends Flowable<T> { + final Publisher<? extends T>[] sources; + final Iterable<? extends Publisher<? extends T>> sourcesIterable; + + public FlowableAmb(Publisher<? extends T>[] sources, Iterable<? extends Publisher<? extends T>> sourcesIterable) { + this.sources = sources; + this.sourcesIterable = sourcesIterable; + } + + @Override + @SuppressWarnings("unchecked") + public void subscribeActual(Subscriber<? super T> s) { + Publisher<? extends T>[] sources = this.sources; + int count = 0; + if (sources == null) { + sources = new Publisher[8]; + try { + for (Publisher<? extends T> p : sourcesIterable) { + if (p == null) { + EmptySubscription.error(new NullPointerException("One of the sources is null"), s); + return; + } + if (count == sources.length) { + Publisher<? extends T>[] b = new Publisher[count + (count >> 2)]; + System.arraycopy(sources, 0, b, 0, count); + sources = b; + } + sources[count++] = p; + } + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + EmptySubscription.error(e, s); + return; + } + } else { + count = sources.length; + } + + if (count == 0) { + EmptySubscription.complete(s); + return; + } else + if (count == 1) { + sources[0].subscribe(s); + return; + } + + AmbCoordinator<T> ac = new AmbCoordinator<>(s, count); + ac.subscribe(sources); + } + + static final class AmbCoordinator<T> implements Subscription { + final Subscriber<? super T> downstream; + final AmbInnerSubscriber<T>[] subscribers; + + final AtomicInteger winner = new AtomicInteger(); + + @SuppressWarnings("unchecked") + AmbCoordinator(Subscriber<? super T> actual, int count) { + this.downstream = actual; + this.subscribers = new AmbInnerSubscriber[count]; + } + + public void subscribe(Publisher<? extends T>[] sources) { + AmbInnerSubscriber<T>[] as = subscribers; + int len = as.length; + for (int i = 0; i < len; i++) { + as[i] = new AmbInnerSubscriber<>(this, i + 1, downstream); + } + winner.lazySet(0); // release the contents of 'as' + downstream.onSubscribe(this); + + for (int i = 0; i < len; i++) { + if (winner.get() != 0) { + return; + } + + sources[i].subscribe(as[i]); + } + } + + @Override + public void request(long n) { + if (SubscriptionHelper.validate(n)) { + int w = winner.get(); + if (w > 0) { + subscribers[w - 1].request(n); + } else + if (w == 0) { + for (AmbInnerSubscriber<T> a : subscribers) { + a.request(n); + } + } + } + } + + public boolean win(int index) { + int w = winner.get(); + if (w == 0) { + if (winner.compareAndSet(0, index)) { + AmbInnerSubscriber<T>[] a = subscribers; + int n = a.length; + for (int i = 0; i < n; i++) { + if (i + 1 != index) { + a[i].cancel(); + } + } + return true; + } + } + return false; + } + + @Override + public void cancel() { + if (winner.get() != -1) { + winner.lazySet(-1); + + for (AmbInnerSubscriber<T> a : subscribers) { + a.cancel(); + } + } + } + } + + static final class AmbInnerSubscriber<T> extends AtomicReference<Subscription> implements FlowableSubscriber<T>, Subscription { + + private static final long serialVersionUID = -1185974347409665484L; + final AmbCoordinator<T> parent; + final int index; + final Subscriber<? super T> downstream; + + boolean won; + + final AtomicLong missedRequested = new AtomicLong(); + + AmbInnerSubscriber(AmbCoordinator<T> parent, int index, Subscriber<? super T> downstream) { + this.parent = parent; + this.index = index; + this.downstream = downstream; + } + + @Override + public void onSubscribe(Subscription s) { + SubscriptionHelper.deferredSetOnce(this, missedRequested, s); + } + + @Override + public void request(long n) { + SubscriptionHelper.deferredRequest(this, missedRequested, n); + } + + @Override + public void onNext(T t) { + if (won) { + downstream.onNext(t); + } else { + if (parent.win(index)) { + won = true; + downstream.onNext(t); + } else { + get().cancel(); + } + } + } + + @Override + public void onError(Throwable t) { + if (won) { + downstream.onError(t); + } else { + if (parent.win(index)) { + won = true; + downstream.onError(t); + } else { + get().cancel(); + RxJavaPlugins.onError(t); + } + } + } + + @Override + public void onComplete() { + if (won) { + downstream.onComplete(); + } else { + if (parent.win(index)) { + won = true; + downstream.onComplete(); + } else { + get().cancel(); + } + } + } + + @Override + public void cancel() { + SubscriptionHelper.cancel(this); + } + + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableAny.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableAny.java new file mode 100644 index 0000000000..b09dce4c03 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableAny.java @@ -0,0 +1,106 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.Predicate; +import io.reactivex.rxjava3.internal.subscriptions.*; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +public final class FlowableAny<T> extends AbstractFlowableWithUpstream<T, Boolean> { + final Predicate<? super T> predicate; + public FlowableAny(Flowable<T> source, Predicate<? super T> predicate) { + super(source); + this.predicate = predicate; + } + + @Override + protected void subscribeActual(Subscriber<? super Boolean> s) { + source.subscribe(new AnySubscriber<>(s, predicate)); + } + + static final class AnySubscriber<T> extends DeferredScalarSubscription<Boolean> implements FlowableSubscriber<T> { + + private static final long serialVersionUID = -2311252482644620661L; + + final Predicate<? super T> predicate; + + Subscription upstream; + + boolean done; + + AnySubscriber(Subscriber<? super Boolean> actual, Predicate<? super T> predicate) { + super(actual); + this.predicate = predicate; + } + + @Override + public void onSubscribe(Subscription s) { + if (SubscriptionHelper.validate(this.upstream, s)) { + this.upstream = s; + downstream.onSubscribe(this); + s.request(Long.MAX_VALUE); + } + } + + @Override + public void onNext(T t) { + if (done) { + return; + } + boolean b; + try { + b = predicate.test(t); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + upstream.cancel(); + onError(e); + return; + } + if (b) { + done = true; + upstream.cancel(); + complete(true); + } + } + + @Override + public void onError(Throwable t) { + if (done) { + RxJavaPlugins.onError(t); + return; + } + + done = true; + downstream.onError(t); + } + + @Override + public void onComplete() { + if (!done) { + done = true; + complete(false); + } + } + + @Override + public void cancel() { + super.cancel(); + upstream.cancel(); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableAnySingle.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableAnySingle.java new file mode 100644 index 0000000000..1b210b82d4 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableAnySingle.java @@ -0,0 +1,125 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import org.reactivestreams.Subscription; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.Predicate; +import io.reactivex.rxjava3.internal.fuseable.FuseToFlowable; +import io.reactivex.rxjava3.internal.subscriptions.SubscriptionHelper; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +public final class FlowableAnySingle<T> extends Single<Boolean> implements FuseToFlowable<Boolean> { + final Flowable<T> source; + + final Predicate<? super T> predicate; + + public FlowableAnySingle(Flowable<T> source, Predicate<? super T> predicate) { + this.source = source; + this.predicate = predicate; + } + + @Override + protected void subscribeActual(SingleObserver<? super Boolean> observer) { + source.subscribe(new AnySubscriber<>(observer, predicate)); + } + + @Override + public Flowable<Boolean> fuseToFlowable() { + return RxJavaPlugins.onAssembly(new FlowableAny<>(source, predicate)); + } + + static final class AnySubscriber<T> implements FlowableSubscriber<T>, Disposable { + + final SingleObserver<? super Boolean> downstream; + + final Predicate<? super T> predicate; + + Subscription upstream; + + boolean done; + + AnySubscriber(SingleObserver<? super Boolean> actual, Predicate<? super T> predicate) { + this.downstream = actual; + this.predicate = predicate; + } + + @Override + public void onSubscribe(Subscription s) { + if (SubscriptionHelper.validate(this.upstream, s)) { + this.upstream = s; + downstream.onSubscribe(this); + s.request(Long.MAX_VALUE); + } + } + + @Override + public void onNext(T t) { + if (done) { + return; + } + boolean b; + try { + b = predicate.test(t); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + upstream.cancel(); + upstream = SubscriptionHelper.CANCELLED; + onError(e); + return; + } + if (b) { + done = true; + upstream.cancel(); + upstream = SubscriptionHelper.CANCELLED; + downstream.onSuccess(true); + } + } + + @Override + public void onError(Throwable t) { + if (done) { + RxJavaPlugins.onError(t); + return; + } + + done = true; + upstream = SubscriptionHelper.CANCELLED; + downstream.onError(t); + } + + @Override + public void onComplete() { + if (!done) { + done = true; + upstream = SubscriptionHelper.CANCELLED; + downstream.onSuccess(false); + } + } + + @Override + public void dispose() { + upstream.cancel(); + upstream = SubscriptionHelper.CANCELLED; + } + + @Override + public boolean isDisposed() { + return upstream == SubscriptionHelper.CANCELLED; + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableAutoConnect.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableAutoConnect.java new file mode 100644 index 0000000000..8d4b428c2b --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableAutoConnect.java @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import java.util.concurrent.atomic.AtomicInteger; + +import org.reactivestreams.Subscriber; + +import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.flowables.ConnectableFlowable; +import io.reactivex.rxjava3.functions.Consumer; + +/** + * Wraps a {@link ConnectableFlowable} and calls its {@code connect()} method once + * the specified number of {@link Subscriber}s have subscribed. + * + * @param <T> the value type of the chain + */ +public final class FlowableAutoConnect<T> extends Flowable<T> { + final ConnectableFlowable<? extends T> source; + final int numberOfSubscribers; + final Consumer<? super Disposable> connection; + final AtomicInteger clients; + + public FlowableAutoConnect(ConnectableFlowable<? extends T> source, + int numberOfSubscribers, + Consumer<? super Disposable> connection) { + this.source = source; + this.numberOfSubscribers = numberOfSubscribers; + this.connection = connection; + this.clients = new AtomicInteger(); + } + + @Override + public void subscribeActual(Subscriber<? super T> child) { + source.subscribe(child); + if (clients.incrementAndGet() == numberOfSubscribers) { + source.connect(connection); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableBlockingSubscribe.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableBlockingSubscribe.java new file mode 100644 index 0000000000..22cede1410 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableBlockingSubscribe.java @@ -0,0 +1,131 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import java.util.Objects; +import java.util.concurrent.*; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.functions.*; +import io.reactivex.rxjava3.internal.subscribers.*; +import io.reactivex.rxjava3.internal.util.*; + +/** + * Utility methods to consume a Publisher in a blocking manner with callbacks or Subscriber. + */ +public final class FlowableBlockingSubscribe { + + /** Utility class. */ + private FlowableBlockingSubscribe() { + throw new IllegalStateException("No instances!"); + } + + /** + * Subscribes to the source and calls the Subscriber methods on the current thread. + * <p> + * @param source the source publisher + * The cancellation and backpressure is composed through. + * @param subscriber the subscriber to forward events and calls to in the current thread + * @param <T> the value type + */ + public static <T> void subscribe(Publisher<? extends T> source, Subscriber<? super T> subscriber) { + final BlockingQueue<Object> queue = new LinkedBlockingQueue<>(); + + BlockingSubscriber<T> bs = new BlockingSubscriber<>(queue); + + source.subscribe(bs); + + try { + for (;;) { + if (bs.isCancelled()) { + break; + } + Object v = queue.poll(); + if (v == null) { + if (bs.isCancelled()) { + break; + } + BlockingHelper.verifyNonBlocking(); + v = queue.take(); + } + if (bs.isCancelled()) { + break; + } + if (v == BlockingSubscriber.TERMINATED + || NotificationLite.acceptFull(v, subscriber)) { + break; + } + } + } catch (InterruptedException e) { + bs.cancel(); + subscriber.onError(e); + } + } + + /** + * Runs the source observable to a terminal event, ignoring any values and rethrowing any exception. + * @param source the source to await + * @param <T> the value type + */ + public static <T> void subscribe(Publisher<? extends T> source) { + BlockingIgnoringReceiver callback = new BlockingIgnoringReceiver(); + LambdaSubscriber<T> ls = new LambdaSubscriber<>(Functions.emptyConsumer(), + callback, callback, Functions.REQUEST_MAX); + + source.subscribe(ls); + + BlockingHelper.awaitForComplete(callback, ls); + Throwable e = callback.error; + if (e != null) { + throw ExceptionHelper.wrapOrThrow(e); + } + } + + /** + * Subscribes to the source and calls the given actions on the current thread. + * @param o the source publisher + * @param onNext the callback action for each source value + * @param onError the callback action for an error event + * @param onComplete the callback action for the completion event. + * @param <T> the value type + */ + public static <T> void subscribe(Publisher<? extends T> o, final Consumer<? super T> onNext, + final Consumer<? super Throwable> onError, final Action onComplete) { + Objects.requireNonNull(onNext, "onNext is null"); + Objects.requireNonNull(onError, "onError is null"); + Objects.requireNonNull(onComplete, "onComplete is null"); + subscribe(o, new LambdaSubscriber<T>(onNext, onError, onComplete, Functions.REQUEST_MAX)); + } + + /** + * Subscribes to the source and calls the given actions on the current thread. + * @param o the source publisher + * @param onNext the callback action for each source value + * @param onError the callback action for an error event + * @param onComplete the callback action for the completion event. + * @param bufferSize the number of elements to prefetch from the source Publisher + * @param <T> the value type + */ + public static <T> void subscribe(Publisher<? extends T> o, final Consumer<? super T> onNext, + final Consumer<? super Throwable> onError, final Action onComplete, int bufferSize) { + Objects.requireNonNull(onNext, "onNext is null"); + Objects.requireNonNull(onError, "onError is null"); + Objects.requireNonNull(onComplete, "onComplete is null"); + ObjectHelper.verifyPositive(bufferSize, "number > 0 required"); + subscribe(o, new BoundedSubscriber<T>(onNext, onError, onComplete, Functions.boundedConsumer(bufferSize), + bufferSize)); + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableBuffer.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableBuffer.java new file mode 100644 index 0000000000..a1cb792f08 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableBuffer.java @@ -0,0 +1,443 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import java.util.*; +import java.util.concurrent.atomic.*; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.subscriptions.SubscriptionHelper; +import io.reactivex.rxjava3.internal.util.*; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +public final class FlowableBuffer<T, C extends Collection<? super T>> extends AbstractFlowableWithUpstream<T, C> { + final int size; + + final int skip; + + final Supplier<C> bufferSupplier; + + public FlowableBuffer(Flowable<T> source, int size, int skip, Supplier<C> bufferSupplier) { + super(source); + this.size = size; + this.skip = skip; + this.bufferSupplier = bufferSupplier; + } + + @Override + public void subscribeActual(Subscriber<? super C> s) { + if (size == skip) { + source.subscribe(new PublisherBufferExactSubscriber<>(s, size, bufferSupplier)); + } else if (skip > size) { + source.subscribe(new PublisherBufferSkipSubscriber<>(s, size, skip, bufferSupplier)); + } else { + source.subscribe(new PublisherBufferOverlappingSubscriber<>(s, size, skip, bufferSupplier)); + } + } + + static final class PublisherBufferExactSubscriber<T, C extends Collection<? super T>> + implements FlowableSubscriber<T>, Subscription { + + final Subscriber<? super C> downstream; + + final Supplier<C> bufferSupplier; + + final int size; + + C buffer; + + Subscription upstream; + + boolean done; + + int index; + + PublisherBufferExactSubscriber(Subscriber<? super C> actual, int size, Supplier<C> bufferSupplier) { + this.downstream = actual; + this.size = size; + this.bufferSupplier = bufferSupplier; + } + + @Override + public void request(long n) { + if (SubscriptionHelper.validate(n)) { + upstream.request(BackpressureHelper.multiplyCap(n, size)); + } + } + + @Override + public void cancel() { + upstream.cancel(); + } + + @Override + public void onSubscribe(Subscription s) { + if (SubscriptionHelper.validate(this.upstream, s)) { + this.upstream = s; + + downstream.onSubscribe(this); + } + } + + @Override + public void onNext(T t) { + if (done) { + return; + } + + C b = buffer; + if (b == null) { + + try { + b = Objects.requireNonNull(bufferSupplier.get(), "The bufferSupplier returned a null buffer"); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + cancel(); + onError(e); + return; + } + + buffer = b; + } + + b.add(t); + + int i = index + 1; + if (i == size) { + index = 0; + buffer = null; + downstream.onNext(b); + } else { + index = i; + } + } + + @Override + public void onError(Throwable t) { + if (done) { + RxJavaPlugins.onError(t); + return; + } + buffer = null; + done = true; + downstream.onError(t); + } + + @Override + public void onComplete() { + if (done) { + return; + } + done = true; + + C b = buffer; + buffer = null; + + if (b != null) { + downstream.onNext(b); + } + downstream.onComplete(); + } + } + + static final class PublisherBufferSkipSubscriber<T, C extends Collection<? super T>> + extends AtomicInteger + implements FlowableSubscriber<T>, Subscription { + + private static final long serialVersionUID = -5616169793639412593L; + + final Subscriber<? super C> downstream; + + final Supplier<C> bufferSupplier; + + final int size; + + final int skip; + + C buffer; + + Subscription upstream; + + boolean done; + + int index; + + PublisherBufferSkipSubscriber(Subscriber<? super C> actual, int size, int skip, + Supplier<C> bufferSupplier) { + this.downstream = actual; + this.size = size; + this.skip = skip; + this.bufferSupplier = bufferSupplier; + } + + @Override + public void request(long n) { + if (SubscriptionHelper.validate(n)) { + if (get() == 0 && compareAndSet(0, 1)) { + // n full buffers + long u = BackpressureHelper.multiplyCap(n, size); + // + (n - 1) gaps + long v = BackpressureHelper.multiplyCap(skip - size, n - 1); + + upstream.request(BackpressureHelper.addCap(u, v)); + } else { + // n full buffer + gap + upstream.request(BackpressureHelper.multiplyCap(skip, n)); + } + } + } + + @Override + public void cancel() { + upstream.cancel(); + } + + @Override + public void onSubscribe(Subscription s) { + if (SubscriptionHelper.validate(this.upstream, s)) { + this.upstream = s; + + downstream.onSubscribe(this); + } + } + + @Override + public void onNext(T t) { + if (done) { + return; + } + + C b = buffer; + + int i = index; + + if (i++ == 0) { + try { + b = Objects.requireNonNull(bufferSupplier.get(), "The bufferSupplier returned a null buffer"); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + cancel(); + + onError(e); + return; + } + + buffer = b; + } + + if (b != null) { + b.add(t); + if (b.size() == size) { + buffer = null; + downstream.onNext(b); + } + } + + if (i == skip) { + i = 0; + } + index = i; + } + + @Override + public void onError(Throwable t) { + if (done) { + RxJavaPlugins.onError(t); + return; + } + + done = true; + buffer = null; + + downstream.onError(t); + } + + @Override + public void onComplete() { + if (done) { + return; + } + + done = true; + C b = buffer; + buffer = null; + + if (b != null) { + downstream.onNext(b); + } + + downstream.onComplete(); + } + } + + static final class PublisherBufferOverlappingSubscriber<T, C extends Collection<? super T>> + extends AtomicLong + implements FlowableSubscriber<T>, Subscription, BooleanSupplier { + + private static final long serialVersionUID = -7370244972039324525L; + + final Subscriber<? super C> downstream; + + final Supplier<C> bufferSupplier; + + final int size; + + final int skip; + + final ArrayDeque<C> buffers; + + final AtomicBoolean once; + + Subscription upstream; + + boolean done; + + int index; + + volatile boolean cancelled; + + long produced; + + PublisherBufferOverlappingSubscriber(Subscriber<? super C> actual, int size, int skip, + Supplier<C> bufferSupplier) { + this.downstream = actual; + this.size = size; + this.skip = skip; + this.bufferSupplier = bufferSupplier; + this.once = new AtomicBoolean(); + this.buffers = new ArrayDeque<>(); + } + + @Override + public boolean getAsBoolean() { + return cancelled; + } + + @Override + public void request(long n) { + if (SubscriptionHelper.validate(n)) { + if (QueueDrainHelper.postCompleteRequest(n, downstream, buffers, this, this)) { + return; + } + + if (!once.get() && once.compareAndSet(false, true)) { + // (n - 1) skips + long u = BackpressureHelper.multiplyCap(skip, n - 1); + + // + 1 full buffer + long r = BackpressureHelper.addCap(size, u); + upstream.request(r); + } else { + // n skips + long r = BackpressureHelper.multiplyCap(skip, n); + upstream.request(r); + } + } + } + + @Override + public void cancel() { + cancelled = true; + upstream.cancel(); + } + + @Override + public void onSubscribe(Subscription s) { + if (SubscriptionHelper.validate(this.upstream, s)) { + this.upstream = s; + + downstream.onSubscribe(this); + } + } + + @Override + public void onNext(T t) { + if (done) { + return; + } + + ArrayDeque<C> bs = buffers; + + int i = index; + + if (i++ == 0) { + C b; + + try { + b = Objects.requireNonNull(bufferSupplier.get(), "The bufferSupplier returned a null buffer"); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + cancel(); + onError(e); + return; + } + + bs.offer(b); + } + + C b = bs.peek(); + + if (b.size() + 1 == size) { + bs.poll(); + + b.add(t); + + produced++; + + downstream.onNext(b); + } + + for (C b0 : bs) { + b0.add(t); + } + + if (i == skip) { + i = 0; + } + index = i; + } + + @Override + public void onError(Throwable t) { + if (done) { + RxJavaPlugins.onError(t); + return; + } + + done = true; + buffers.clear(); + + downstream.onError(t); + } + + @Override + public void onComplete() { + if (done) { + return; + } + + done = true; + + long p = produced; + if (p != 0L) { + BackpressureHelper.produced(this, p); + } + QueueDrainHelper.postComplete(downstream, buffers, this, this); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableBufferBoundary.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableBufferBoundary.java new file mode 100644 index 0000000000..b009b1bc25 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableBufferBoundary.java @@ -0,0 +1,414 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import java.util.*; +import java.util.concurrent.atomic.*; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.*; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.subscriptions.SubscriptionHelper; +import io.reactivex.rxjava3.internal.util.*; +import io.reactivex.rxjava3.operators.SpscLinkedArrayQueue; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +public final class FlowableBufferBoundary<T, U extends Collection<? super T>, Open, Close> +extends AbstractFlowableWithUpstream<T, U> { + final Supplier<U> bufferSupplier; + final Publisher<? extends Open> bufferOpen; + final Function<? super Open, ? extends Publisher<? extends Close>> bufferClose; + + public FlowableBufferBoundary(Flowable<T> source, Publisher<? extends Open> bufferOpen, + Function<? super Open, ? extends Publisher<? extends Close>> bufferClose, Supplier<U> bufferSupplier) { + super(source); + this.bufferOpen = bufferOpen; + this.bufferClose = bufferClose; + this.bufferSupplier = bufferSupplier; + } + + @Override + protected void subscribeActual(Subscriber<? super U> s) { + BufferBoundarySubscriber<T, U, Open, Close> parent = + new BufferBoundarySubscriber<>( + s, bufferOpen, bufferClose, bufferSupplier + ); + s.onSubscribe(parent); + source.subscribe(parent); + } + + static final class BufferBoundarySubscriber<T, C extends Collection<? super T>, Open, Close> + extends AtomicInteger implements FlowableSubscriber<T>, Subscription { + + private static final long serialVersionUID = -8466418554264089604L; + + final Subscriber<? super C> downstream; + + final Supplier<C> bufferSupplier; + + final Publisher<? extends Open> bufferOpen; + + final Function<? super Open, ? extends Publisher<? extends Close>> bufferClose; + + final CompositeDisposable subscribers; + + final AtomicLong requested; + + final AtomicReference<Subscription> upstream; + + final AtomicThrowable errors; + + volatile boolean done; + + final SpscLinkedArrayQueue<C> queue; + + volatile boolean cancelled; + + long index; + + Map<Long, C> buffers; + + long emitted; + + BufferBoundarySubscriber(Subscriber<? super C> actual, + Publisher<? extends Open> bufferOpen, + Function<? super Open, ? extends Publisher<? extends Close>> bufferClose, + Supplier<C> bufferSupplier + ) { + this.downstream = actual; + this.bufferSupplier = bufferSupplier; + this.bufferOpen = bufferOpen; + this.bufferClose = bufferClose; + this.queue = new SpscLinkedArrayQueue<>(bufferSize()); + this.subscribers = new CompositeDisposable(); + this.requested = new AtomicLong(); + this.upstream = new AtomicReference<>(); + this.buffers = new LinkedHashMap<>(); + this.errors = new AtomicThrowable(); + } + + @Override + public void onSubscribe(Subscription s) { + if (SubscriptionHelper.setOnce(this.upstream, s)) { + + BufferOpenSubscriber<Open> open = new BufferOpenSubscriber<>(this); + subscribers.add(open); + + bufferOpen.subscribe(open); + + s.request(Long.MAX_VALUE); + } + } + + @Override + public void onNext(T t) { + synchronized (this) { + Map<Long, C> bufs = buffers; + if (bufs == null) { + return; + } + for (C b : bufs.values()) { + b.add(t); + } + } + } + + @Override + public void onError(Throwable t) { + if (errors.tryAddThrowableOrReport(t)) { + subscribers.dispose(); + synchronized (this) { + buffers = null; + } + done = true; + drain(); + } + } + + @Override + public void onComplete() { + subscribers.dispose(); + synchronized (this) { + Map<Long, C> bufs = buffers; + if (bufs == null) { + return; + } + for (C b : bufs.values()) { + queue.offer(b); + } + buffers = null; + } + done = true; + drain(); + } + + @Override + public void request(long n) { + BackpressureHelper.add(requested, n); + drain(); + } + + @Override + public void cancel() { + if (SubscriptionHelper.cancel(upstream)) { + cancelled = true; + subscribers.dispose(); + synchronized (this) { + buffers = null; + } + if (getAndIncrement() != 0) { + queue.clear(); + } + } + } + + void open(Open token) { + Publisher<? extends Close> p; + C buf; + try { + buf = Objects.requireNonNull(bufferSupplier.get(), "The bufferSupplier returned a null Collection"); + p = Objects.requireNonNull(bufferClose.apply(token), "The bufferClose returned a null Publisher"); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + SubscriptionHelper.cancel(upstream); + onError(ex); + return; + } + + long idx = index; + index = idx + 1; + synchronized (this) { + Map<Long, C> bufs = buffers; + if (bufs == null) { + return; + } + bufs.put(idx, buf); + } + + BufferCloseSubscriber<T, C> bc = new BufferCloseSubscriber<>(this, idx); + subscribers.add(bc); + p.subscribe(bc); + } + + void openComplete(BufferOpenSubscriber<Open> os) { + subscribers.delete(os); + if (subscribers.size() == 0) { + SubscriptionHelper.cancel(upstream); + done = true; + drain(); + } + } + + void close(BufferCloseSubscriber<T, C> closer, long idx) { + subscribers.delete(closer); + boolean makeDone = false; + if (subscribers.size() == 0) { + makeDone = true; + SubscriptionHelper.cancel(upstream); + } + synchronized (this) { + Map<Long, C> bufs = buffers; + if (bufs == null) { + return; + } + queue.offer(buffers.remove(idx)); + } + if (makeDone) { + done = true; + } + drain(); + } + + void boundaryError(Disposable subscriber, Throwable ex) { + SubscriptionHelper.cancel(upstream); + subscribers.delete(subscriber); + onError(ex); + } + + void drain() { + if (getAndIncrement() != 0) { + return; + } + + int missed = 1; + long e = emitted; + Subscriber<? super C> a = downstream; + SpscLinkedArrayQueue<C> q = queue; + + for (;;) { + long r = requested.get(); + + while (e != r) { + if (cancelled) { + q.clear(); + return; + } + + boolean d = done; + if (d && errors.get() != null) { + q.clear(); + errors.tryTerminateConsumer(a); + return; + } + + C v = q.poll(); + boolean empty = v == null; + + if (d && empty) { + a.onComplete(); + return; + } + + if (empty) { + break; + } + + a.onNext(v); + e++; + } + + if (e == r) { + if (cancelled) { + q.clear(); + return; + } + + if (done) { + if (errors.get() != null) { + q.clear(); + errors.tryTerminateConsumer(a); + return; + } else if (q.isEmpty()) { + a.onComplete(); + return; + } + } + } + + emitted = e; + missed = addAndGet(-missed); + if (missed == 0) { + break; + } + } + } + + static final class BufferOpenSubscriber<Open> + extends AtomicReference<Subscription> + implements FlowableSubscriber<Open>, Disposable { + + private static final long serialVersionUID = -8498650778633225126L; + + final BufferBoundarySubscriber<?, ?, Open, ?> parent; + + BufferOpenSubscriber(BufferBoundarySubscriber<?, ?, Open, ?> parent) { + this.parent = parent; + } + + @Override + public void onSubscribe(Subscription s) { + SubscriptionHelper.setOnce(this, s, Long.MAX_VALUE); + } + + @Override + public void onNext(Open t) { + parent.open(t); + } + + @Override + public void onError(Throwable t) { + lazySet(SubscriptionHelper.CANCELLED); + parent.boundaryError(this, t); + } + + @Override + public void onComplete() { + lazySet(SubscriptionHelper.CANCELLED); + parent.openComplete(this); + } + + @Override + public void dispose() { + SubscriptionHelper.cancel(this); + } + + @Override + public boolean isDisposed() { + return get() == SubscriptionHelper.CANCELLED; + } + } + } + + static final class BufferCloseSubscriber<T, C extends Collection<? super T>> + extends AtomicReference<Subscription> + implements FlowableSubscriber<Object>, Disposable { + + private static final long serialVersionUID = -8498650778633225126L; + + final BufferBoundarySubscriber<T, C, ?, ?> parent; + + final long index; + + BufferCloseSubscriber(BufferBoundarySubscriber<T, C, ?, ?> parent, long index) { + this.parent = parent; + this.index = index; + } + + @Override + public void onSubscribe(Subscription s) { + SubscriptionHelper.setOnce(this, s, Long.MAX_VALUE); + } + + @Override + public void onNext(Object t) { + Subscription s = get(); + if (s != SubscriptionHelper.CANCELLED) { + lazySet(SubscriptionHelper.CANCELLED); + s.cancel(); + parent.close(this, index); + } + } + + @Override + public void onError(Throwable t) { + if (get() != SubscriptionHelper.CANCELLED) { + lazySet(SubscriptionHelper.CANCELLED); + parent.boundaryError(this, t); + } else { + RxJavaPlugins.onError(t); + } + } + + @Override + public void onComplete() { + if (get() != SubscriptionHelper.CANCELLED) { + lazySet(SubscriptionHelper.CANCELLED); + parent.close(this, index); + } + } + + @Override + public void dispose() { + SubscriptionHelper.cancel(this); + } + + @Override + public boolean isDisposed() { + return get() == SubscriptionHelper.CANCELLED; + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableBufferExactBoundary.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableBufferExactBoundary.java new file mode 100644 index 0000000000..3e2abefdd9 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableBufferExactBoundary.java @@ -0,0 +1,216 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import java.util.Collection; +import java.util.Objects; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.Supplier; +import io.reactivex.rxjava3.internal.queue.MpscLinkedQueue; +import io.reactivex.rxjava3.internal.subscribers.QueueDrainSubscriber; +import io.reactivex.rxjava3.internal.subscriptions.*; +import io.reactivex.rxjava3.internal.util.QueueDrainHelper; +import io.reactivex.rxjava3.subscribers.*; + +public final class FlowableBufferExactBoundary<T, U extends Collection<? super T>, B> +extends AbstractFlowableWithUpstream<T, U> { + final Publisher<B> boundary; + final Supplier<U> bufferSupplier; + + public FlowableBufferExactBoundary(Flowable<T> source, Publisher<B> boundary, Supplier<U> bufferSupplier) { + super(source); + this.boundary = boundary; + this.bufferSupplier = bufferSupplier; + } + + @Override + protected void subscribeActual(Subscriber<? super U> s) { + source.subscribe(new BufferExactBoundarySubscriber<>(new SerializedSubscriber<>(s), bufferSupplier, boundary)); + } + + static final class BufferExactBoundarySubscriber<T, U extends Collection<? super T>, B> + extends QueueDrainSubscriber<T, U, U> implements Subscription, Disposable { + + final Supplier<U> bufferSupplier; + final Publisher<B> boundary; + + Subscription upstream; + + Disposable other; + + U buffer; + + BufferExactBoundarySubscriber(Subscriber<? super U> actual, Supplier<U> bufferSupplier, + Publisher<B> boundary) { + super(actual, new MpscLinkedQueue<>()); + this.bufferSupplier = bufferSupplier; + this.boundary = boundary; + } + + @Override + public void onSubscribe(Subscription s) { + if (!SubscriptionHelper.validate(this.upstream, s)) { + return; + } + this.upstream = s; + + U b; + + try { + b = Objects.requireNonNull(bufferSupplier.get(), "The buffer supplied is null"); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + cancelled = true; + s.cancel(); + EmptySubscription.error(e, downstream); + return; + } + + buffer = b; + + BufferBoundarySubscriber<T, U, B> bs = new BufferBoundarySubscriber<>(this); + other = bs; + + downstream.onSubscribe(this); + + if (!cancelled) { + s.request(Long.MAX_VALUE); + + boundary.subscribe(bs); + } + } + + @Override + public void onNext(T t) { + synchronized (this) { + U b = buffer; + if (b == null) { + return; + } + b.add(t); + } + } + + @Override + public void onError(Throwable t) { + cancel(); + downstream.onError(t); + } + + @Override + public void onComplete() { + U b; + synchronized (this) { + b = buffer; + if (b == null) { + return; + } + buffer = null; + } + queue.offer(b); + done = true; + if (enter()) { + QueueDrainHelper.drainMaxLoop(queue, downstream, false, this, this); + } + } + + @Override + public void request(long n) { + requested(n); + } + + @Override + public void cancel() { + if (!cancelled) { + cancelled = true; + other.dispose(); + upstream.cancel(); + + if (enter()) { + queue.clear(); + } + } + } + + void next() { + + U next; + + try { + next = Objects.requireNonNull(bufferSupplier.get(), "The buffer supplied is null"); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + cancel(); + downstream.onError(e); + return; + } + + U b; + synchronized (this) { + b = buffer; + if (b == null) { + return; + } + buffer = next; + } + + fastPathEmitMax(b, false, this); + } + + @Override + public void dispose() { + cancel(); + } + + @Override + public boolean isDisposed() { + return cancelled; + } + + @Override + public boolean accept(Subscriber<? super U> a, U v) { + downstream.onNext(v); + return true; + } + + } + + static final class BufferBoundarySubscriber<T, U extends Collection<? super T>, B> extends DisposableSubscriber<B> { + final BufferExactBoundarySubscriber<T, U, B> parent; + + BufferBoundarySubscriber(BufferExactBoundarySubscriber<T, U, B> parent) { + this.parent = parent; + } + + @Override + public void onNext(B t) { + parent.next(); + } + + @Override + public void onError(Throwable t) { + parent.onError(t); + } + + @Override + public void onComplete() { + parent.onComplete(); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableBufferTimed.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableBufferTimed.java new file mode 100644 index 0000000000..89749acaef --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableBufferTimed.java @@ -0,0 +1,573 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import java.util.*; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.core.Scheduler.Worker; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.Supplier; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; +import io.reactivex.rxjava3.internal.queue.MpscLinkedQueue; +import io.reactivex.rxjava3.internal.subscribers.QueueDrainSubscriber; +import io.reactivex.rxjava3.internal.subscriptions.*; +import io.reactivex.rxjava3.internal.util.QueueDrainHelper; +import io.reactivex.rxjava3.subscribers.SerializedSubscriber; + +public final class FlowableBufferTimed<T, U extends Collection<? super T>> extends AbstractFlowableWithUpstream<T, U> { + + final long timespan; + final long timeskip; + final TimeUnit unit; + final Scheduler scheduler; + final Supplier<U> bufferSupplier; + final int maxSize; + final boolean restartTimerOnMaxSize; + + public FlowableBufferTimed(Flowable<T> source, long timespan, long timeskip, TimeUnit unit, Scheduler scheduler, Supplier<U> bufferSupplier, int maxSize, + boolean restartTimerOnMaxSize) { + super(source); + this.timespan = timespan; + this.timeskip = timeskip; + this.unit = unit; + this.scheduler = scheduler; + this.bufferSupplier = bufferSupplier; + this.maxSize = maxSize; + this.restartTimerOnMaxSize = restartTimerOnMaxSize; + } + + @Override + protected void subscribeActual(Subscriber<? super U> s) { + if (timespan == timeskip && maxSize == Integer.MAX_VALUE) { + source.subscribe(new BufferExactUnboundedSubscriber<>( + new SerializedSubscriber<>(s), + bufferSupplier, timespan, unit, scheduler)); + return; + } + Scheduler.Worker w = scheduler.createWorker(); + + if (timespan == timeskip) { + source.subscribe(new BufferExactBoundedSubscriber<>( + new SerializedSubscriber<>(s), + bufferSupplier, + timespan, unit, maxSize, restartTimerOnMaxSize, w + )); + return; + } + // Can't use maxSize because what to do if a buffer is full but its + // timespan hasn't been elapsed? + source.subscribe(new BufferSkipBoundedSubscriber<>( + new SerializedSubscriber<>(s), + bufferSupplier, timespan, timeskip, unit, w)); + } + + static final class BufferExactUnboundedSubscriber<T, U extends Collection<? super T>> + extends QueueDrainSubscriber<T, U, U> implements Subscription, Runnable, Disposable { + final Supplier<U> bufferSupplier; + final long timespan; + final TimeUnit unit; + final Scheduler scheduler; + + Subscription upstream; + + U buffer; + + final AtomicReference<Disposable> timer = new AtomicReference<>(); + + BufferExactUnboundedSubscriber( + Subscriber<? super U> actual, Supplier<U> bufferSupplier, + long timespan, TimeUnit unit, Scheduler scheduler) { + super(actual, new MpscLinkedQueue<>()); + this.bufferSupplier = bufferSupplier; + this.timespan = timespan; + this.unit = unit; + this.scheduler = scheduler; + } + + @Override + public void onSubscribe(Subscription s) { + if (SubscriptionHelper.validate(this.upstream, s)) { + this.upstream = s; + + U b; + + try { + b = Objects.requireNonNull(bufferSupplier.get(), "The supplied buffer is null"); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + cancel(); + EmptySubscription.error(e, downstream); + return; + } + + buffer = b; + + downstream.onSubscribe(this); + + if (!cancelled) { + s.request(Long.MAX_VALUE); + + Disposable d = scheduler.schedulePeriodicallyDirect(this, timespan, timespan, unit); + if (!timer.compareAndSet(null, d)) { + d.dispose(); + } + } + } + } + + @Override + public void onNext(T t) { + synchronized (this) { + U b = buffer; + if (b != null) { + b.add(t); + } + } + } + + @Override + public void onError(Throwable t) { + DisposableHelper.dispose(timer); + synchronized (this) { + buffer = null; + } + downstream.onError(t); + } + + @Override + public void onComplete() { + DisposableHelper.dispose(timer); + U b; + synchronized (this) { + b = buffer; + if (b == null) { + return; + } + buffer = null; + } + queue.offer(b); + done = true; + if (enter()) { + QueueDrainHelper.drainMaxLoop(queue, downstream, false, null, this); + } + } + + @Override + public void request(long n) { + requested(n); + } + + @Override + public void cancel() { + cancelled = true; + upstream.cancel(); + DisposableHelper.dispose(timer); + } + + @Override + public void run() { + U next; + + try { + next = Objects.requireNonNull(bufferSupplier.get(), "The supplied buffer is null"); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + cancel(); + downstream.onError(e); + return; + } + + U current; + + synchronized (this) { + current = buffer; + if (current == null) { + return; + } + buffer = next; + } + + fastPathEmitMax(current, false, this); + } + + @Override + public boolean accept(Subscriber<? super U> a, U v) { + downstream.onNext(v); + return true; + } + + @Override + public void dispose() { + cancel(); + } + + @Override + public boolean isDisposed() { + return timer.get() == DisposableHelper.DISPOSED; + } + } + + static final class BufferSkipBoundedSubscriber<T, U extends Collection<? super T>> + extends QueueDrainSubscriber<T, U, U> implements Subscription, Runnable { + final Supplier<U> bufferSupplier; + final long timespan; + final long timeskip; + final TimeUnit unit; + final Worker w; + final List<U> buffers; + + Subscription upstream; + + BufferSkipBoundedSubscriber(Subscriber<? super U> actual, + Supplier<U> bufferSupplier, long timespan, + long timeskip, TimeUnit unit, Worker w) { + super(actual, new MpscLinkedQueue<>()); + this.bufferSupplier = bufferSupplier; + this.timespan = timespan; + this.timeskip = timeskip; + this.unit = unit; + this.w = w; + this.buffers = new LinkedList<>(); + } + + @Override + public void onSubscribe(Subscription s) { + if (!SubscriptionHelper.validate(this.upstream, s)) { + return; + } + this.upstream = s; + + final U b; // NOPMD + + try { + b = Objects.requireNonNull(bufferSupplier.get(), "The supplied buffer is null"); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + w.dispose(); + s.cancel(); + EmptySubscription.error(e, downstream); + return; + } + + buffers.add(b); + + downstream.onSubscribe(this); + + s.request(Long.MAX_VALUE); + + w.schedulePeriodically(this, timeskip, timeskip, unit); + + w.schedule(new RemoveFromBuffer(b), timespan, unit); + } + + @Override + public void onNext(T t) { + synchronized (this) { + for (U b : buffers) { + b.add(t); + } + } + } + + @Override + public void onError(Throwable t) { + done = true; + w.dispose(); + clear(); + downstream.onError(t); + } + + @Override + public void onComplete() { + List<U> bs; + synchronized (this) { + bs = new ArrayList<>(buffers); + buffers.clear(); + } + + for (U b : bs) { + queue.offer(b); + } + done = true; + if (enter()) { + QueueDrainHelper.drainMaxLoop(queue, downstream, false, w, this); + } + } + + @Override + public void request(long n) { + requested(n); + } + + @Override + public void cancel() { + cancelled = true; + upstream.cancel(); + w.dispose(); + clear(); + } + + void clear() { + synchronized (this) { + buffers.clear(); + } + } + + @Override + public void run() { + if (cancelled) { + return; + } + final U b; // NOPMD + + try { + b = Objects.requireNonNull(bufferSupplier.get(), "The supplied buffer is null"); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + cancel(); + downstream.onError(e); + return; + } + + synchronized (this) { + if (cancelled) { + return; + } + buffers.add(b); + } + + w.schedule(new RemoveFromBuffer(b), timespan, unit); + } + + @Override + public boolean accept(Subscriber<? super U> a, U v) { + a.onNext(v); + return true; + } + + final class RemoveFromBuffer implements Runnable { + private final U buffer; + + RemoveFromBuffer(U buffer) { + this.buffer = buffer; + } + + @Override + public void run() { + synchronized (BufferSkipBoundedSubscriber.this) { + buffers.remove(buffer); + } + + fastPathOrderedEmitMax(buffer, false, w); + } + } + } + + static final class BufferExactBoundedSubscriber<T, U extends Collection<? super T>> + extends QueueDrainSubscriber<T, U, U> implements Subscription, Runnable, Disposable { + final Supplier<U> bufferSupplier; + final long timespan; + final TimeUnit unit; + final int maxSize; + final boolean restartTimerOnMaxSize; + final Worker w; + + U buffer; + + Disposable timer; + + Subscription upstream; + + long producerIndex; + + long consumerIndex; + + BufferExactBoundedSubscriber( + Subscriber<? super U> actual, + Supplier<U> bufferSupplier, + long timespan, TimeUnit unit, int maxSize, + boolean restartOnMaxSize, Worker w) { + super(actual, new MpscLinkedQueue<>()); + this.bufferSupplier = bufferSupplier; + this.timespan = timespan; + this.unit = unit; + this.maxSize = maxSize; + this.restartTimerOnMaxSize = restartOnMaxSize; + this.w = w; + } + + @Override + public void onSubscribe(Subscription s) { + if (!SubscriptionHelper.validate(this.upstream, s)) { + return; + } + this.upstream = s; + + U b; + + try { + b = Objects.requireNonNull(bufferSupplier.get(), "The supplied buffer is null"); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + w.dispose(); + s.cancel(); + EmptySubscription.error(e, downstream); + return; + } + + buffer = b; + + downstream.onSubscribe(this); + + timer = w.schedulePeriodically(this, timespan, timespan, unit); + + s.request(Long.MAX_VALUE); + } + + @Override + public void onNext(T t) { + U b; + synchronized (this) { + b = buffer; + if (b == null) { + return; + } + + b.add(t); + + if (b.size() < maxSize) { + return; + } + + buffer = null; + producerIndex++; + } + + if (restartTimerOnMaxSize) { + timer.dispose(); + } + + fastPathOrderedEmitMax(b, false, this); + + try { + b = Objects.requireNonNull(bufferSupplier.get(), "The supplied buffer is null"); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + cancel(); + downstream.onError(e); + return; + } + + synchronized (this) { + buffer = b; + consumerIndex++; + } + if (restartTimerOnMaxSize) { + timer = w.schedulePeriodically(this, timespan, timespan, unit); + } + } + + @Override + public void onError(Throwable t) { + synchronized (this) { + buffer = null; + } + downstream.onError(t); + w.dispose(); + } + + @Override + public void onComplete() { + U b; + synchronized (this) { + b = buffer; + buffer = null; + } + + if (b != null) { + queue.offer(b); + done = true; + if (enter()) { + QueueDrainHelper.drainMaxLoop(queue, downstream, false, this, this); + } + w.dispose(); + } + } + + @Override + public boolean accept(Subscriber<? super U> a, U v) { + a.onNext(v); + return true; + } + + @Override + public void request(long n) { + requested(n); + } + + @Override + public void cancel() { + if (!cancelled) { + cancelled = true; + dispose(); + } + } + + @Override + public void dispose() { + synchronized (this) { + buffer = null; + } + upstream.cancel(); + w.dispose(); + } + + @Override + public boolean isDisposed() { + return w.isDisposed(); + } + + @Override + public void run() { + U next; + + try { + next = Objects.requireNonNull(bufferSupplier.get(), "The supplied buffer is null"); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + cancel(); + downstream.onError(e); + return; + } + + U current; + + synchronized (this) { + current = buffer; + if (current == null || producerIndex != consumerIndex) { + return; + } + buffer = next; + } + + fastPathOrderedEmitMax(current, false, this); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableCache.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableCache.java new file mode 100644 index 0000000000..ee52769e69 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableCache.java @@ -0,0 +1,417 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import java.util.concurrent.atomic.*; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.internal.subscriptions.SubscriptionHelper; +import io.reactivex.rxjava3.internal.util.BackpressureHelper; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +/** + * An observable which auto-connects to another observable, caches the elements + * from that observable but allows terminating the connection and completing the cache. + * + * @param <T> the source element type + */ +public final class FlowableCache<T> extends AbstractFlowableWithUpstream<T, T> +implements FlowableSubscriber<T> { + + /** + * The subscription to the source should happen at most once. + */ + final AtomicBoolean once; + + /** + * The number of items per cached nodes. + */ + final int capacityHint; + + /** + * The current known array of subscriber state to notify. + */ + final AtomicReference<CacheSubscription<T>[]> subscribers; + + /** + * A shared instance of an empty array of subscribers to avoid creating + * a new empty array when all subscribers cancel. + */ + @SuppressWarnings("rawtypes") + static final CacheSubscription[] EMPTY = new CacheSubscription[0]; + /** + * A shared instance indicating the source has no more events and there + * is no need to remember subscribers anymore. + */ + @SuppressWarnings("rawtypes") + static final CacheSubscription[] TERMINATED = new CacheSubscription[0]; + + /** + * The total number of elements in the list available for reads. + */ + volatile long size; + + /** + * The starting point of the cached items. + */ + final Node<T> head; + + /** + * The current tail of the linked structure holding the items. + */ + Node<T> tail; + + /** + * How many items have been put into the tail node so far. + */ + int tailOffset; + + /** + * If {@link #subscribers} is {@link #TERMINATED}, this holds the terminal error if not null. + */ + Throwable error; + + /** + * True if the source has terminated. + */ + volatile boolean done; + + /** + * Constructs an empty, non-connected cache. + * @param source the source to subscribe to for the first incoming subscriber + * @param capacityHint the number of items expected (reduce allocation frequency) + */ + @SuppressWarnings("unchecked") + public FlowableCache(Flowable<T> source, int capacityHint) { + super(source); + this.capacityHint = capacityHint; + this.once = new AtomicBoolean(); + Node<T> n = new Node<>(capacityHint); + this.head = n; + this.tail = n; + this.subscribers = new AtomicReference<>(EMPTY); + } + + @Override + protected void subscribeActual(Subscriber<? super T> t) { + CacheSubscription<T> consumer = new CacheSubscription<>(t, this); + t.onSubscribe(consumer); + add(consumer); + + if (!once.get() && once.compareAndSet(false, true)) { + source.subscribe(this); + } else { + replay(consumer); + } + } + + /** + * Check if this cached observable is connected to its source. + * @return true if already connected + */ + /* public */boolean isConnected() { + return once.get(); + } + + /** + * Returns true if there are observers subscribed to this observable. + * @return true if the cache has Subscribers + */ + /* public */ boolean hasSubscribers() { + return subscribers.get().length != 0; + } + + /** + * Returns the number of events currently cached. + * @return the number of currently cached event count + */ + /* public */ long cachedEventCount() { + return size; + } + + /** + * Atomically adds the consumer to the {@link #subscribers} copy-on-write array + * if the source has not yet terminated. + * @param consumer the consumer to add + */ + void add(CacheSubscription<T> consumer) { + for (;;) { + CacheSubscription<T>[] current = subscribers.get(); + if (current == TERMINATED) { + return; + } + int n = current.length; + + @SuppressWarnings("unchecked") + CacheSubscription<T>[] next = new CacheSubscription[n + 1]; + System.arraycopy(current, 0, next, 0, n); + next[n] = consumer; + + if (subscribers.compareAndSet(current, next)) { + return; + } + } + } + + /** + * Atomically removes the consumer from the {@link #subscribers} copy-on-write array. + * @param consumer the consumer to remove + */ + @SuppressWarnings("unchecked") + void remove(CacheSubscription<T> consumer) { + for (;;) { + CacheSubscription<T>[] current = subscribers.get(); + int n = current.length; + if (n == 0) { + return; + } + + int j = -1; + for (int i = 0; i < n; i++) { + if (current[i] == consumer) { + j = i; + break; + } + } + + if (j < 0) { + return; + } + CacheSubscription<T>[] next; + + if (n == 1) { + next = EMPTY; + } else { + next = new CacheSubscription[n - 1]; + System.arraycopy(current, 0, next, 0, j); + System.arraycopy(current, j + 1, next, j, n - j - 1); + } + + if (subscribers.compareAndSet(current, next)) { + return; + } + } + } + + /** + * Replays the contents of this cache to the given consumer based on its + * current state and number of items requested by it. + * @param consumer the consumer to continue replaying items to + */ + void replay(CacheSubscription<T> consumer) { + // make sure there is only one replay going on at a time + if (consumer.getAndIncrement() != 0) { + return; + } + + // see if there were more replay request in the meantime + int missed = 1; + // read out state into locals upfront to avoid being re-read due to volatile reads + long index = consumer.index; + int offset = consumer.offset; + Node<T> node = consumer.node; + AtomicLong requested = consumer.requested; + Subscriber<? super T> downstream = consumer.downstream; + int capacity = capacityHint; + + for (;;) { + // first see if the source has terminated, read order matters! + boolean sourceDone = done; + // and if the number of items is the same as this consumer has received + boolean empty = size == index; + + // if the source is done and we have all items so far, terminate the consumer + if (sourceDone && empty) { + // release the node object to avoid leaks through retained consumers + consumer.node = null; + // if error is not null then the source failed + Throwable ex = error; + if (ex != null) { + downstream.onError(ex); + } else { + downstream.onComplete(); + } + return; + } + + // there are still items not sent to the consumer + if (!empty) { + // see how many items the consumer has requested in total so far + long consumerRequested = requested.get(); + // MIN_VALUE indicates a cancelled consumer, we stop replaying + if (consumerRequested == Long.MIN_VALUE) { + // release the node object to avoid leaks through retained consumers + consumer.node = null; + return; + } + // if the consumer has requested more and there is more, we will emit an item + if (consumerRequested != index) { + + // if the offset in the current node has reached the node capacity + if (offset == capacity) { + // switch to the subsequent node + node = node.next; + // reset the in-node offset + offset = 0; + } + + // emit the cached item + downstream.onNext(node.values[offset]); + + // move the node offset forward + offset++; + // move the total consumed item count forward + index++; + + // retry for the next item/terminal event if any + continue; + } + } + + // commit the changed references back + consumer.index = index; + consumer.offset = offset; + consumer.node = node; + // release the changes and see if there were more replay request in the meantime + missed = consumer.addAndGet(-missed); + if (missed == 0) { + break; + } + } + } + + @Override + public void onSubscribe(Subscription s) { + s.request(Long.MAX_VALUE); + } + + @Override + public void onNext(T t) { + int tailOffset = this.tailOffset; + // if the current tail node is full, create a fresh node + if (tailOffset == capacityHint) { + Node<T> n = new Node<>(tailOffset); + n.values[0] = t; + this.tailOffset = 1; + tail.next = n; + tail = n; + } else { + tail.values[tailOffset] = t; + this.tailOffset = tailOffset + 1; + } + size++; + for (CacheSubscription<T> consumer : subscribers.get()) { + replay(consumer); + } + } + + @SuppressWarnings("unchecked") + @Override + public void onError(Throwable t) { + if (done) { + RxJavaPlugins.onError(t); + return; + } + error = t; + done = true; + for (CacheSubscription<T> consumer : subscribers.getAndSet(TERMINATED)) { + replay(consumer); + } + } + + @SuppressWarnings("unchecked") + @Override + public void onComplete() { + done = true; + for (CacheSubscription<T> consumer : subscribers.getAndSet(TERMINATED)) { + replay(consumer); + } + } + + /** + * Hosts the downstream consumer and its current requested and replay states. + * {@code this} holds the work-in-progress counter for the serialized replay. + * @param <T> the value type + */ + static final class CacheSubscription<T> extends AtomicInteger + implements Subscription { + + private static final long serialVersionUID = 6770240836423125754L; + + final Subscriber<? super T> downstream; + + final FlowableCache<T> parent; + + final AtomicLong requested; + + Node<T> node; + + int offset; + + long index; + + /** + * Constructs a new instance with the actual downstream consumer and + * the parent cache object. + * @param downstream the actual consumer + * @param parent the parent that holds onto the cached items + */ + CacheSubscription(Subscriber<? super T> downstream, FlowableCache<T> parent) { + this.downstream = downstream; + this.parent = parent; + this.node = parent.head; + this.requested = new AtomicLong(); + } + + @Override + public void request(long n) { + if (SubscriptionHelper.validate(n)) { + BackpressureHelper.addCancel(requested, n); + parent.replay(this); + } + } + + @Override + public void cancel() { + if (requested.getAndSet(Long.MIN_VALUE) != Long.MIN_VALUE) { + parent.remove(this); + } + } + } + + /** + * Represents a segment of the cached item list as + * part of a linked-node-list structure. + * @param <T> the element type + */ + static final class Node<T> { + + /** + * The array of values held by this node. + */ + final T[] values; + + /** + * The next node if not null. + */ + volatile Node<T> next; + + @SuppressWarnings("unchecked") + Node(int capacityHint) { + this.values = (T[])new Object[capacityHint]; + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableCollect.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableCollect.java new file mode 100644 index 0000000000..8f45d62404 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableCollect.java @@ -0,0 +1,117 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.subscriptions.*; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +import java.util.Objects; + +public final class FlowableCollect<T, U> extends AbstractFlowableWithUpstream<T, U> { + + final Supplier<? extends U> initialSupplier; + final BiConsumer<? super U, ? super T> collector; + + public FlowableCollect(Flowable<T> source, Supplier<? extends U> initialSupplier, BiConsumer<? super U, ? super T> collector) { + super(source); + this.initialSupplier = initialSupplier; + this.collector = collector; + } + + @Override + protected void subscribeActual(Subscriber<? super U> s) { + U u; + try { + u = Objects.requireNonNull(initialSupplier.get(), "The initial value supplied is null"); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + EmptySubscription.error(e, s); + return; + } + + source.subscribe(new CollectSubscriber<>(s, u, collector)); + } + + static final class CollectSubscriber<T, U> extends DeferredScalarSubscription<U> implements FlowableSubscriber<T> { + + private static final long serialVersionUID = -3589550218733891694L; + + final BiConsumer<? super U, ? super T> collector; + + final U u; + + Subscription upstream; + + boolean done; + + CollectSubscriber(Subscriber<? super U> actual, U u, BiConsumer<? super U, ? super T> collector) { + super(actual); + this.collector = collector; + this.u = u; + } + + @Override + public void onSubscribe(Subscription s) { + if (SubscriptionHelper.validate(this.upstream, s)) { + this.upstream = s; + downstream.onSubscribe(this); + s.request(Long.MAX_VALUE); + } + } + + @Override + public void onNext(T t) { + if (done) { + return; + } + try { + collector.accept(u, t); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + upstream.cancel(); + onError(e); + } + } + + @Override + public void onError(Throwable t) { + if (done) { + RxJavaPlugins.onError(t); + return; + } + done = true; + downstream.onError(t); + } + + @Override + public void onComplete() { + if (done) { + return; + } + done = true; + complete(u); + } + + @Override + public void cancel() { + super.cancel(); + upstream.cancel(); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableCollectSingle.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableCollectSingle.java new file mode 100644 index 0000000000..714bc92eea --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableCollectSingle.java @@ -0,0 +1,134 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import org.reactivestreams.Subscription; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.disposables.EmptyDisposable; +import io.reactivex.rxjava3.internal.fuseable.FuseToFlowable; +import io.reactivex.rxjava3.internal.subscriptions.SubscriptionHelper; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +import java.util.Objects; + +public final class FlowableCollectSingle<T, U> extends Single<U> implements FuseToFlowable<U> { + + final Flowable<T> source; + + final Supplier<? extends U> initialSupplier; + final BiConsumer<? super U, ? super T> collector; + + public FlowableCollectSingle(Flowable<T> source, Supplier<? extends U> initialSupplier, BiConsumer<? super U, ? super T> collector) { + this.source = source; + this.initialSupplier = initialSupplier; + this.collector = collector; + } + + @Override + protected void subscribeActual(SingleObserver<? super U> observer) { + U u; + try { + u = Objects.requireNonNull(initialSupplier.get(), "The initialSupplier returned a null value"); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + EmptyDisposable.error(e, observer); + return; + } + + source.subscribe(new CollectSubscriber<>(observer, u, collector)); + } + + @Override + public Flowable<U> fuseToFlowable() { + return RxJavaPlugins.onAssembly(new FlowableCollect<>(source, initialSupplier, collector)); + } + + static final class CollectSubscriber<T, U> implements FlowableSubscriber<T>, Disposable { + + final SingleObserver<? super U> downstream; + + final BiConsumer<? super U, ? super T> collector; + + final U u; + + Subscription upstream; + + boolean done; + + CollectSubscriber(SingleObserver<? super U> actual, U u, BiConsumer<? super U, ? super T> collector) { + this.downstream = actual; + this.collector = collector; + this.u = u; + } + + @Override + public void onSubscribe(Subscription s) { + if (SubscriptionHelper.validate(this.upstream, s)) { + this.upstream = s; + downstream.onSubscribe(this); + s.request(Long.MAX_VALUE); + } + } + + @Override + public void onNext(T t) { + if (done) { + return; + } + try { + collector.accept(u, t); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + upstream.cancel(); + onError(e); + } + } + + @Override + public void onError(Throwable t) { + if (done) { + RxJavaPlugins.onError(t); + return; + } + done = true; + upstream = SubscriptionHelper.CANCELLED; + downstream.onError(t); + } + + @Override + public void onComplete() { + if (done) { + return; + } + done = true; + upstream = SubscriptionHelper.CANCELLED; + downstream.onSuccess(u); + } + + @Override + public void dispose() { + upstream.cancel(); + upstream = SubscriptionHelper.CANCELLED; + } + + @Override + public boolean isDisposed() { + return upstream == SubscriptionHelper.CANCELLED; + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableCombineLatest.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableCombineLatest.java new file mode 100644 index 0000000000..ac947b9541 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableCombineLatest.java @@ -0,0 +1,523 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import java.util.Objects; +import java.util.concurrent.atomic.*; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.annotations.*; +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.operators.flowable.FlowableMap.MapSubscriber; +import io.reactivex.rxjava3.internal.subscriptions.*; +import io.reactivex.rxjava3.internal.util.*; +import io.reactivex.rxjava3.operators.SpscLinkedArrayQueue; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +/** + * Combines the latest values from multiple sources through a function. + * + * @param <T> the value type of the sources + * @param <R> the result type + */ +public final class FlowableCombineLatest<T, R> +extends Flowable<R> { + + @Nullable + final Publisher<? extends T>[] array; + + @Nullable + final Iterable<? extends Publisher<? extends T>> iterable; + + final Function<? super Object[], ? extends R> combiner; + + final int bufferSize; + + final boolean delayErrors; + + public FlowableCombineLatest(@NonNull Publisher<? extends T>[] array, + @NonNull Function<? super Object[], ? extends R> combiner, + int bufferSize, boolean delayErrors) { + this.array = array; + this.iterable = null; + this.combiner = combiner; + this.bufferSize = bufferSize; + this.delayErrors = delayErrors; + } + + public FlowableCombineLatest(@NonNull Iterable<? extends Publisher<? extends T>> iterable, + @NonNull Function<? super Object[], ? extends R> combiner, + int bufferSize, boolean delayErrors) { + this.array = null; + this.iterable = iterable; + this.combiner = combiner; + this.bufferSize = bufferSize; + this.delayErrors = delayErrors; + } + + @SuppressWarnings("unchecked") + @Override + public void subscribeActual(Subscriber<? super R> s) { + Publisher<? extends T>[] sources = array; + int count; + if (sources == null) { + count = 0; + sources = new Publisher[8]; + + try { + for (Publisher<? extends T> p : iterable) { + if (count == sources.length) { + Publisher<? extends T>[] b = new Publisher[count + (count >> 2)]; + System.arraycopy(sources, 0, b, 0, count); + sources = b; + } + sources[count++] = Objects.requireNonNull(p, "The Iterator returned a null Publisher"); + } + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + EmptySubscription.error(ex, s); + return; + } + + } else { + count = sources.length; + } + + if (count == 0) { + EmptySubscription.complete(s); + return; + } + if (count == 1) { + sources[0].subscribe(new MapSubscriber<>(s, new SingletonArrayFunc())); + return; + } + + CombineLatestCoordinator<T, R> coordinator = + new CombineLatestCoordinator<>(s, combiner, count, bufferSize, delayErrors); + + s.onSubscribe(coordinator); + + coordinator.subscribe(sources, count); + } + + static final class CombineLatestCoordinator<T, R> + extends BasicIntQueueSubscription<R> { + + private static final long serialVersionUID = -5082275438355852221L; + + final Subscriber<? super R> downstream; + + final Function<? super Object[], ? extends R> combiner; + + final CombineLatestInnerSubscriber<T>[] subscribers; + + final SpscLinkedArrayQueue<Object> queue; + + final Object[] latest; + + final boolean delayErrors; + + boolean outputFused; + + int nonEmptySources; + + int completedSources; + + volatile boolean cancelled; + + final AtomicLong requested; + + volatile boolean done; + + final AtomicThrowable error; + + CombineLatestCoordinator(Subscriber<? super R> actual, + Function<? super Object[], ? extends R> combiner, int n, + int bufferSize, boolean delayErrors) { + this.downstream = actual; + this.combiner = combiner; + @SuppressWarnings("unchecked") + CombineLatestInnerSubscriber<T>[] a = new CombineLatestInnerSubscriber[n]; + for (int i = 0; i < n; i++) { + a[i] = new CombineLatestInnerSubscriber<>(this, i, bufferSize); + } + this.subscribers = a; + this.latest = new Object[n]; + this.queue = new SpscLinkedArrayQueue<>(bufferSize); + this.requested = new AtomicLong(); + this.error = new AtomicThrowable(); + this.delayErrors = delayErrors; + } + + @Override + public void request(long n) { + if (SubscriptionHelper.validate(n)) { + BackpressureHelper.add(requested, n); + drain(); + } + } + + @Override + public void cancel() { + cancelled = true; + cancelAll(); + drain(); + } + + void subscribe(Publisher<? extends T>[] sources, int n) { + CombineLatestInnerSubscriber<T>[] a = subscribers; + + for (int i = 0; i < n; i++) { + if (done || cancelled) { + return; + } + sources[i].subscribe(a[i]); + } + } + + void innerValue(int index, T value) { + + boolean replenishInsteadOfDrain; + + synchronized (this) { + Object[] os = latest; + + int localNonEmptySources = nonEmptySources; + + if (os[index] == null) { + localNonEmptySources++; + nonEmptySources = localNonEmptySources; + } + + os[index] = value; + + if (os.length == localNonEmptySources) { + + queue.offer(subscribers[index], os.clone()); + + replenishInsteadOfDrain = false; + } else { + replenishInsteadOfDrain = true; + } + } + + if (replenishInsteadOfDrain) { + subscribers[index].requestOne(); + } else { + drain(); + } + } + + void innerComplete(int index) { + synchronized (this) { + Object[] os = latest; + + if (os[index] != null) { + int localCompletedSources = completedSources + 1; + + if (localCompletedSources == os.length) { + done = true; + } else { + completedSources = localCompletedSources; + return; + } + } else { + done = true; + } + } + drain(); + } + + void innerError(int index, Throwable e) { + + if (ExceptionHelper.addThrowable(error, e)) { + if (!delayErrors) { + cancelAll(); + done = true; + drain(); + } else { + innerComplete(index); + } + } else { + RxJavaPlugins.onError(e); + } + } + + void drainOutput() { + final Subscriber<? super R> a = downstream; + final SpscLinkedArrayQueue<Object> q = queue; + + int missed = 1; + + for (;;) { + + if (cancelled) { + q.clear(); + return; + } + + Throwable ex = error.get(); + if (ex != null) { + q.clear(); + + a.onError(ex); + return; + } + + boolean d = done; + + boolean empty = q.isEmpty(); + + if (!empty) { + a.onNext(null); + } + + if (d && empty) { + a.onComplete(); + return; + } + + missed = addAndGet(-missed); + if (missed == 0) { + break; + } + } + } + + @SuppressWarnings("unchecked") + void drainAsync() { + final Subscriber<? super R> a = downstream; + final SpscLinkedArrayQueue<Object> q = queue; + + int missed = 1; + + for (;;) { + + long r = requested.get(); + long e = 0L; + + while (e != r) { + boolean d = done; + + Object v = q.poll(); + + boolean empty = v == null; + + if (checkTerminated(d, empty, a, q)) { + return; + } + + if (empty) { + break; + } + + T[] va = (T[])q.poll(); + + R w; + + try { + w = Objects.requireNonNull(combiner.apply(va), "The combiner returned a null value"); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + + cancelAll(); + ExceptionHelper.addThrowable(error, ex); + ex = ExceptionHelper.terminate(error); + + a.onError(ex); + return; + } + + a.onNext(w); + + ((CombineLatestInnerSubscriber<T>)v).requestOne(); + + e++; + } + + if (e == r) { + if (checkTerminated(done, q.isEmpty(), a, q)) { + return; + } + } + + if (e != 0L && r != Long.MAX_VALUE) { + requested.addAndGet(-e); + } + + missed = addAndGet(-missed); + if (missed == 0) { + break; + } + } + } + + void drain() { + if (getAndIncrement() != 0) { + return; + } + + if (outputFused) { + drainOutput(); + } else { + drainAsync(); + } + } + + boolean checkTerminated(boolean d, boolean empty, Subscriber<?> a, SpscLinkedArrayQueue<?> q) { + if (cancelled) { + cancelAll(); + q.clear(); + error.tryTerminateAndReport(); + return true; + } + + if (d) { + if (delayErrors) { + if (empty) { + cancelAll(); + error.tryTerminateConsumer(a); + return true; + } + } else { + Throwable e = ExceptionHelper.terminate(error); + + if (e != null && e != ExceptionHelper.TERMINATED) { + cancelAll(); + q.clear(); + a.onError(e); + return true; + } else + if (empty) { + cancelAll(); + + a.onComplete(); + return true; + } + } + } + return false; + } + + void cancelAll() { + for (CombineLatestInnerSubscriber<T> inner : subscribers) { + inner.cancel(); + } + } + + @Override + public int requestFusion(int requestedMode) { + if ((requestedMode & BOUNDARY) != 0) { + return NONE; + } + int m = requestedMode & ASYNC; + outputFused = m != 0; + return m; + } + + @Nullable + @SuppressWarnings("unchecked") + @Override + public R poll() throws Throwable { + Object e = queue.poll(); + if (e == null) { + return null; + } + T[] a = (T[])queue.poll(); + R r = Objects.requireNonNull(combiner.apply(a), "The combiner returned a null value"); + ((CombineLatestInnerSubscriber<T>)e).requestOne(); + return r; + } + + @Override + public void clear() { + queue.clear(); + } + + @Override + public boolean isEmpty() { + return queue.isEmpty(); + } + } + + static final class CombineLatestInnerSubscriber<T> + extends AtomicReference<Subscription> + implements FlowableSubscriber<T> { + + private static final long serialVersionUID = -8730235182291002949L; + + final CombineLatestCoordinator<T, ?> parent; + + final int index; + + final int prefetch; + + final int limit; + + int produced; + + CombineLatestInnerSubscriber(CombineLatestCoordinator<T, ?> parent, int index, int prefetch) { + this.parent = parent; + this.index = index; + this.prefetch = prefetch; + this.limit = prefetch - (prefetch >> 2); + } + + @Override + public void onSubscribe(Subscription s) { + SubscriptionHelper.setOnce(this, s, prefetch); + } + + @Override + public void onNext(T t) { + parent.innerValue(index, t); + } + + @Override + public void onError(Throwable t) { + parent.innerError(index, t); + } + + @Override + public void onComplete() { + parent.innerComplete(index); + } + + public void cancel() { + SubscriptionHelper.cancel(this); + } + + public void requestOne() { + + int p = produced + 1; + if (p == limit) { + produced = 0; + get().request(p); + } else { + produced = p; + } + + } + } + + final class SingletonArrayFunc implements Function<T, R> { + @Override + public R apply(T t) throws Throwable { + return combiner.apply(new Object[] { t }); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableConcatArray.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableConcatArray.java new file mode 100644 index 0000000000..34d4248514 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableConcatArray.java @@ -0,0 +1,154 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import java.util.*; +import java.util.concurrent.atomic.AtomicInteger; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.CompositeException; +import io.reactivex.rxjava3.internal.subscriptions.SubscriptionArbiter; + +public final class FlowableConcatArray<T> extends Flowable<T> { + + final Publisher<? extends T>[] sources; + + final boolean delayError; + + public FlowableConcatArray(Publisher<? extends T>[] sources, boolean delayError) { + this.sources = sources; + this.delayError = delayError; + } + + @Override + protected void subscribeActual(Subscriber<? super T> s) { + ConcatArraySubscriber<T> parent = new ConcatArraySubscriber<>(sources, delayError, s); + s.onSubscribe(parent); + + parent.onComplete(); + } + + static final class ConcatArraySubscriber<T> extends SubscriptionArbiter implements FlowableSubscriber<T> { + + private static final long serialVersionUID = -8158322871608889516L; + + final Subscriber<? super T> downstream; + + final Publisher<? extends T>[] sources; + + final boolean delayError; + + final AtomicInteger wip; + + int index; + + List<Throwable> errors; + + long produced; + + ConcatArraySubscriber(Publisher<? extends T>[] sources, boolean delayError, Subscriber<? super T> downstream) { + super(false); + this.downstream = downstream; + this.sources = sources; + this.delayError = delayError; + this.wip = new AtomicInteger(); + } + + @Override + public void onSubscribe(Subscription s) { + setSubscription(s); + } + + @Override + public void onNext(T t) { + produced++; + downstream.onNext(t); + } + + @Override + public void onError(Throwable t) { + if (delayError) { + List<Throwable> list = errors; + if (list == null) { + list = new ArrayList<>(sources.length - index + 1); + errors = list; + } + list.add(t); + onComplete(); + } else { + downstream.onError(t); + } + } + + @Override + public void onComplete() { + if (wip.getAndIncrement() == 0) { + Publisher<? extends T>[] sources = this.sources; + int n = sources.length; + int i = index; + for (;;) { + + if (i == n) { + List<Throwable> list = errors; + if (list != null) { + if (list.size() == 1) { + downstream.onError(list.get(0)); + } else { + downstream.onError(new CompositeException(list)); + } + } else { + downstream.onComplete(); + } + return; + } + + Publisher<? extends T> p = sources[i]; + + if (p == null) { + Throwable ex = new NullPointerException("A Publisher entry is null"); + if (delayError) { + List<Throwable> list = errors; + if (list == null) { + list = new ArrayList<>(n - i + 1); + errors = list; + } + list.add(ex); + i++; + continue; + } else { + downstream.onError(ex); + return; + } + } else { + long r = produced; + if (r != 0L) { + produced = 0L; + produced(r); + } + p.subscribe(this); + } + + index = ++i; + + if (wip.decrementAndGet() == 0) { + break; + } + } + } + } + } + +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableConcatMap.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableConcatMap.java new file mode 100644 index 0000000000..5d1b6e4c33 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableConcatMap.java @@ -0,0 +1,590 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import java.util.Objects; +import java.util.concurrent.atomic.*; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.subscriptions.*; +import io.reactivex.rxjava3.internal.util.*; +import io.reactivex.rxjava3.operators.QueueSubscription; +import io.reactivex.rxjava3.operators.SimpleQueue; +import io.reactivex.rxjava3.operators.SpscArrayQueue; + +public final class FlowableConcatMap<T, R> extends AbstractFlowableWithUpstream<T, R> { + + final Function<? super T, ? extends Publisher<? extends R>> mapper; + + final int prefetch; + + final ErrorMode errorMode; + + public FlowableConcatMap(Flowable<T> source, + Function<? super T, ? extends Publisher<? extends R>> mapper, + int prefetch, ErrorMode errorMode) { + super(source); + this.mapper = mapper; + this.prefetch = prefetch; + this.errorMode = errorMode; + } + + public static <T, R> Subscriber<T> subscribe(Subscriber<? super R> s, Function<? super T, ? extends Publisher<? extends R>> mapper, + int prefetch, ErrorMode errorMode) { + switch (errorMode) { + case BOUNDARY: + return new ConcatMapDelayed<>(s, mapper, prefetch, false); + case END: + return new ConcatMapDelayed<>(s, mapper, prefetch, true); + default: + return new ConcatMapImmediate<>(s, mapper, prefetch); + } + } + + @Override + protected void subscribeActual(Subscriber<? super R> s) { + + if (FlowableScalarXMap.tryScalarXMapSubscribe(source, s, mapper)) { + return; + } + + source.subscribe(subscribe(s, mapper, prefetch, errorMode)); + } + + abstract static class BaseConcatMapSubscriber<T, R> + extends AtomicInteger + implements FlowableSubscriber<T>, ConcatMapSupport<R>, Subscription { + + private static final long serialVersionUID = -3511336836796789179L; + + final ConcatMapInner<R> inner; + + final Function<? super T, ? extends Publisher<? extends R>> mapper; + + final int prefetch; + + final int limit; + + Subscription upstream; + + int consumed; + + SimpleQueue<T> queue; + + volatile boolean done; + + volatile boolean cancelled; + + final AtomicThrowable errors; + + volatile boolean active; + + int sourceMode; + + BaseConcatMapSubscriber( + Function<? super T, ? extends Publisher<? extends R>> mapper, + int prefetch) { + this.mapper = mapper; + this.prefetch = prefetch; + this.limit = prefetch - (prefetch >> 2); + this.inner = new ConcatMapInner<>(this); + this.errors = new AtomicThrowable(); + } + + @Override + public final void onSubscribe(Subscription s) { + if (SubscriptionHelper.validate(this.upstream, s)) { + this.upstream = s; + + if (s instanceof QueueSubscription) { + @SuppressWarnings("unchecked") QueueSubscription<T> f = (QueueSubscription<T>)s; + int m = f.requestFusion(QueueSubscription.ANY | QueueSubscription.BOUNDARY); + if (m == QueueSubscription.SYNC) { + sourceMode = m; + queue = f; + done = true; + + subscribeActual(); + + drain(); + return; + } + if (m == QueueSubscription.ASYNC) { + sourceMode = m; + queue = f; + + subscribeActual(); + + s.request(prefetch); + return; + } + } + + queue = new SpscArrayQueue<>(prefetch); + + subscribeActual(); + + s.request(prefetch); + } + } + + abstract void drain(); + + abstract void subscribeActual(); + + @Override + public final void onNext(T t) { + if (sourceMode != QueueSubscription.ASYNC) { + if (!queue.offer(t)) { + upstream.cancel(); + onError(new QueueOverflowException()); + return; + } + } + drain(); + } + + @Override + public final void onComplete() { + done = true; + drain(); + } + + @Override + public final void innerComplete() { + active = false; + drain(); + } + + } + + static final class ConcatMapImmediate<T, R> + extends BaseConcatMapSubscriber<T, R> { + + private static final long serialVersionUID = 7898995095634264146L; + + final Subscriber<? super R> downstream; + + final AtomicInteger wip; + + ConcatMapImmediate(Subscriber<? super R> actual, + Function<? super T, ? extends Publisher<? extends R>> mapper, + int prefetch) { + super(mapper, prefetch); + this.downstream = actual; + this.wip = new AtomicInteger(); + } + + @Override + void subscribeActual() { + downstream.onSubscribe(this); + } + + @Override + public void onError(Throwable t) { + inner.cancel(); + HalfSerializer.onError(downstream, t, this, errors); + } + + @Override + public void innerNext(R value) { + HalfSerializer.onNext(downstream, value, this, errors); + } + + @Override + public void innerError(Throwable e) { + upstream.cancel(); + HalfSerializer.onError(downstream, e, this, errors); + } + + @Override + public void request(long n) { + inner.request(n); + } + + @Override + public void cancel() { + if (!cancelled) { + cancelled = true; + + inner.cancel(); + upstream.cancel(); + + errors.tryTerminateAndReport(); + } + } + + @Override + void drain() { + if (wip.getAndIncrement() == 0) { + for (;;) { + if (cancelled) { + return; + } + + if (!active) { + boolean d = done; + + T v; + + try { + v = queue.poll(); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + upstream.cancel(); + errors.tryAddThrowableOrReport(e); + errors.tryTerminateConsumer(downstream); + return; + } + + boolean empty = v == null; + + if (d && empty) { + downstream.onComplete(); + return; + } + + if (!empty) { + Publisher<? extends R> p; + + try { + p = Objects.requireNonNull(mapper.apply(v), "The mapper returned a null Publisher"); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + + upstream.cancel(); + errors.tryAddThrowableOrReport(e); + errors.tryTerminateConsumer(downstream); + return; + } + + if (sourceMode != QueueSubscription.SYNC) { + int c = consumed + 1; + if (c == limit) { + consumed = 0; + upstream.request(c); + } else { + consumed = c; + } + } + + if (p instanceof Supplier) { + @SuppressWarnings("unchecked") + Supplier<R> supplier = (Supplier<R>) p; + + R vr; + + try { + vr = supplier.get(); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + upstream.cancel(); + errors.tryAddThrowableOrReport(e); + errors.tryTerminateConsumer(downstream); + return; + } + + if (vr == null) { + continue; + } + + if (inner.isUnbounded()) { + if (!HalfSerializer.onNext(downstream, vr, this, errors)) { + return; + } + continue; + } else { + active = true; + inner.setSubscription(new SimpleScalarSubscription<>(vr, inner)); + } + + } else { + active = true; + p.subscribe(inner); + } + } + } + if (wip.decrementAndGet() == 0) { + break; + } + } + } + } + } + + static final class SimpleScalarSubscription<T> + extends AtomicBoolean + implements Subscription { + private static final long serialVersionUID = -7606889335172043256L; + + final Subscriber<? super T> downstream; + final T value; + + SimpleScalarSubscription(T value, Subscriber<? super T> downstream) { + this.value = value; + this.downstream = downstream; + } + + @Override + public void request(long n) { + if (n > 0L && compareAndSet(false, true)) { + Subscriber<? super T> a = downstream; + a.onNext(value); + a.onComplete(); + } + } + + @Override + public void cancel() { + + } + } + + static final class ConcatMapDelayed<T, R> + extends BaseConcatMapSubscriber<T, R> { + + private static final long serialVersionUID = -2945777694260521066L; + + final Subscriber<? super R> downstream; + + final boolean veryEnd; + + ConcatMapDelayed(Subscriber<? super R> actual, + Function<? super T, ? extends Publisher<? extends R>> mapper, + int prefetch, boolean veryEnd) { + super(mapper, prefetch); + this.downstream = actual; + this.veryEnd = veryEnd; + } + + @Override + void subscribeActual() { + downstream.onSubscribe(this); + } + + @Override + public void onError(Throwable t) { + if (errors.tryAddThrowableOrReport(t)) { + done = true; + drain(); + } + } + + @Override + public void innerNext(R value) { + downstream.onNext(value); + } + + @Override + public void innerError(Throwable e) { + if (errors.tryAddThrowableOrReport(e)) { + if (!veryEnd) { + upstream.cancel(); + done = true; + } + active = false; + drain(); + } + } + + @Override + public void request(long n) { + inner.request(n); + } + + @Override + public void cancel() { + if (!cancelled) { + cancelled = true; + + inner.cancel(); + upstream.cancel(); + + errors.tryTerminateAndReport(); + } + } + + @Override + void drain() { + if (getAndIncrement() == 0) { + + for (;;) { + if (cancelled) { + return; + } + + if (!active) { + + boolean d = done; + + if (d && !veryEnd) { + Throwable ex = errors.get(); + if (ex != null) { + errors.tryTerminateConsumer(downstream); + return; + } + } + + T v; + + try { + v = queue.poll(); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + upstream.cancel(); + errors.tryAddThrowableOrReport(e); + errors.tryTerminateConsumer(downstream); + return; + } + + boolean empty = v == null; + + if (d && empty) { + errors.tryTerminateConsumer(downstream); + return; + } + + if (!empty) { + Publisher<? extends R> p; + + try { + p = Objects.requireNonNull(mapper.apply(v), "The mapper returned a null Publisher"); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + + upstream.cancel(); + errors.tryAddThrowableOrReport(e); + errors.tryTerminateConsumer(downstream); + return; + } + + if (sourceMode != QueueSubscription.SYNC) { + int c = consumed + 1; + if (c == limit) { + consumed = 0; + upstream.request(c); + } else { + consumed = c; + } + } + + if (p instanceof Supplier) { + @SuppressWarnings("unchecked") + Supplier<R> supplier = (Supplier<R>) p; + + R vr; + + try { + vr = supplier.get(); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + errors.tryAddThrowableOrReport(e); + if (!veryEnd) { + upstream.cancel(); + errors.tryTerminateConsumer(downstream); + return; + } + vr = null; + } + + if (vr == null) { + continue; + } + + if (inner.isUnbounded()) { + downstream.onNext(vr); + continue; + } else { + active = true; + inner.setSubscription(new SimpleScalarSubscription<>(vr, inner)); + } + } else { + active = true; + p.subscribe(inner); + } + } + } + if (decrementAndGet() == 0) { + break; + } + } + } + } + } + + interface ConcatMapSupport<T> { + + void innerNext(T value); + + void innerComplete(); + + void innerError(Throwable e); + } + + static final class ConcatMapInner<R> + extends SubscriptionArbiter + implements FlowableSubscriber<R> { + + private static final long serialVersionUID = 897683679971470653L; + + final ConcatMapSupport<R> parent; + + long produced; + + ConcatMapInner(ConcatMapSupport<R> parent) { + super(false); + this.parent = parent; + } + + @Override + public void onSubscribe(Subscription s) { + setSubscription(s); + } + + @Override + public void onNext(R t) { + produced++; + + parent.innerNext(t); + } + + @Override + public void onError(Throwable t) { + long p = produced; + + if (p != 0L) { + produced = 0L; + produced(p); + } + + parent.innerError(t); + } + + @Override + public void onComplete() { + long p = produced; + + if (p != 0L) { + produced = 0L; + produced(p); + } + + parent.innerComplete(); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableConcatMapEager.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableConcatMapEager.java new file mode 100644 index 0000000000..dc423d89fc --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableConcatMapEager.java @@ -0,0 +1,371 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import java.util.Objects; +import java.util.concurrent.atomic.*; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.subscribers.*; +import io.reactivex.rxjava3.internal.subscriptions.SubscriptionHelper; +import io.reactivex.rxjava3.internal.util.*; +import io.reactivex.rxjava3.operators.SimpleQueue; +import io.reactivex.rxjava3.operators.SpscLinkedArrayQueue; + +public final class FlowableConcatMapEager<T, R> extends AbstractFlowableWithUpstream<T, R> { + + final Function<? super T, ? extends Publisher<? extends R>> mapper; + + final int maxConcurrency; + + final int prefetch; + + final ErrorMode errorMode; + + public FlowableConcatMapEager(Flowable<T> source, + Function<? super T, ? extends Publisher<? extends R>> mapper, + int maxConcurrency, + int prefetch, + ErrorMode errorMode) { + super(source); + this.mapper = mapper; + this.maxConcurrency = maxConcurrency; + this.prefetch = prefetch; + this.errorMode = errorMode; + } + + @Override + protected void subscribeActual(Subscriber<? super R> s) { + source.subscribe(new ConcatMapEagerDelayErrorSubscriber<>( + s, mapper, maxConcurrency, prefetch, errorMode)); + } + + static final class ConcatMapEagerDelayErrorSubscriber<T, R> + extends AtomicInteger + implements FlowableSubscriber<T>, Subscription, InnerQueuedSubscriberSupport<R> { + + private static final long serialVersionUID = -4255299542215038287L; + + final Subscriber<? super R> downstream; + + final Function<? super T, ? extends Publisher<? extends R>> mapper; + + final int maxConcurrency; + + final int prefetch; + + final ErrorMode errorMode; + + final AtomicThrowable errors; + + final AtomicLong requested; + + final SpscLinkedArrayQueue<InnerQueuedSubscriber<R>> subscribers; + + Subscription upstream; + + volatile boolean cancelled; + + volatile boolean done; + + volatile InnerQueuedSubscriber<R> current; + + ConcatMapEagerDelayErrorSubscriber(Subscriber<? super R> actual, + Function<? super T, ? extends Publisher<? extends R>> mapper, int maxConcurrency, int prefetch, + ErrorMode errorMode) { + this.downstream = actual; + this.mapper = mapper; + this.maxConcurrency = maxConcurrency; + this.prefetch = prefetch; + this.errorMode = errorMode; + this.subscribers = new SpscLinkedArrayQueue<>(Math.min(prefetch, maxConcurrency)); + this.errors = new AtomicThrowable(); + this.requested = new AtomicLong(); + } + + @Override + public void onSubscribe(Subscription s) { + if (SubscriptionHelper.validate(this.upstream, s)) { + this.upstream = s; + + downstream.onSubscribe(this); + + s.request(maxConcurrency == Integer.MAX_VALUE ? Long.MAX_VALUE : maxConcurrency); + } + + } + + @Override + public void onNext(T t) { + Publisher<? extends R> p; + + try { + p = Objects.requireNonNull(mapper.apply(t), "The mapper returned a null Publisher"); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + upstream.cancel(); + onError(ex); + return; + } + + InnerQueuedSubscriber<R> inner = new InnerQueuedSubscriber<>(this, prefetch); + + if (cancelled) { + return; + } + + subscribers.offer(inner); + + p.subscribe(inner); + + if (cancelled) { + inner.cancel(); + drainAndCancel(); + } + } + + @Override + public void onError(Throwable t) { + if (errors.tryAddThrowableOrReport(t)) { + done = true; + drain(); + } + } + + @Override + public void onComplete() { + done = true; + drain(); + } + + @Override + public void cancel() { + if (cancelled) { + return; + } + cancelled = true; + upstream.cancel(); + errors.tryTerminateAndReport(); + + drainAndCancel(); + } + + void drainAndCancel() { + if (getAndIncrement() == 0) { + do { + cancelAll(); + } while (decrementAndGet() != 0); + } + } + + void cancelAll() { + InnerQueuedSubscriber<R> inner = current; + current = null; + + if (inner != null) { + inner.cancel(); + } + + while ((inner = subscribers.poll()) != null) { + inner.cancel(); + } + } + + @Override + public void request(long n) { + if (SubscriptionHelper.validate(n)) { + BackpressureHelper.add(requested, n); + drain(); + } + } + + @Override + public void innerNext(InnerQueuedSubscriber<R> inner, R value) { + if (inner.queue().offer(value)) { + drain(); + } else { + inner.cancel(); + innerError(inner, MissingBackpressureException.createDefault()); + } + } + + @Override + public void innerError(InnerQueuedSubscriber<R> inner, Throwable e) { + if (errors.tryAddThrowableOrReport(e)) { + inner.setDone(); + if (errorMode != ErrorMode.END) { + upstream.cancel(); + } + drain(); + } + } + + @Override + public void innerComplete(InnerQueuedSubscriber<R> inner) { + inner.setDone(); + drain(); + } + + @Override + public void drain() { + if (getAndIncrement() != 0) { + return; + } + + int missed = 1; + InnerQueuedSubscriber<R> inner = current; + Subscriber<? super R> a = downstream; + ErrorMode em = errorMode; + + for (;;) { + long r = requested.get(); + long e = 0L; + + if (inner == null) { + + if (em != ErrorMode.END) { + Throwable ex = errors.get(); + if (ex != null) { + cancelAll(); + + errors.tryTerminateConsumer(downstream); + return; + } + } + + boolean outerDone = done; + + inner = subscribers.poll(); + + if (outerDone && inner == null) { + errors.tryTerminateConsumer(downstream); + return; + } + + if (inner != null) { + current = inner; + } + } + + boolean continueNextSource = false; + + if (inner != null) { + SimpleQueue<R> q = inner.queue(); + if (q != null) { + while (e != r) { + if (cancelled) { + cancelAll(); + return; + } + + if (em == ErrorMode.IMMEDIATE) { + Throwable ex = errors.get(); + if (ex != null) { + current = null; + inner.cancel(); + cancelAll(); + + errors.tryTerminateConsumer(downstream); + return; + } + } + + boolean d = inner.isDone(); + + R v; + + try { + v = q.poll(); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + current = null; + inner.cancel(); + cancelAll(); + a.onError(ex); + return; + } + + boolean empty = v == null; + + if (d && empty) { + inner = null; + current = null; + upstream.request(1); + continueNextSource = true; + break; + } + + if (empty) { + break; + } + + a.onNext(v); + + e++; + + inner.request(1L); + } + + if (e == r) { + if (cancelled) { + cancelAll(); + return; + } + + if (em == ErrorMode.IMMEDIATE) { + Throwable ex = errors.get(); + if (ex != null) { + current = null; + inner.cancel(); + cancelAll(); + + errors.tryTerminateConsumer(downstream); + return; + } + } + + boolean d = inner.isDone(); + + boolean empty = q.isEmpty(); + + if (d && empty) { + inner = null; + current = null; + upstream.request(1); + continueNextSource = true; + } + } + } + } + + if (e != 0L && r != Long.MAX_VALUE) { + requested.addAndGet(-e); + } + + if (continueNextSource) { + continue; + } + + missed = addAndGet(-missed); + if (missed == 0) { + break; + } + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableConcatMapEagerPublisher.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableConcatMapEagerPublisher.java new file mode 100644 index 0000000000..218f90c2a6 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableConcatMapEagerPublisher.java @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.operators.flowable.FlowableConcatMapEager.ConcatMapEagerDelayErrorSubscriber; +import io.reactivex.rxjava3.internal.util.ErrorMode; + +/** + * ConcatMapEager which works with an arbitrary Publisher source. + * <p>History: 2.0.7 - experimental + * @param <T> the input value type + * @param <R> the output type + * @since 2.1 + */ +public final class FlowableConcatMapEagerPublisher<T, R> extends Flowable<R> { + + final Publisher<T> source; + + final Function<? super T, ? extends Publisher<? extends R>> mapper; + + final int maxConcurrency; + + final int prefetch; + + final ErrorMode errorMode; + + public FlowableConcatMapEagerPublisher(Publisher<T> source, + Function<? super T, ? extends Publisher<? extends R>> mapper, + int maxConcurrency, + int prefetch, + ErrorMode errorMode) { + this.source = source; + this.mapper = mapper; + this.maxConcurrency = maxConcurrency; + this.prefetch = prefetch; + this.errorMode = errorMode; + } + + @Override + protected void subscribeActual(Subscriber<? super R> s) { + source.subscribe(new ConcatMapEagerDelayErrorSubscriber<>( + s, mapper, maxConcurrency, prefetch, errorMode)); + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableConcatMapScheduler.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableConcatMapScheduler.java new file mode 100644 index 0000000000..707d10a19e --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableConcatMapScheduler.java @@ -0,0 +1,547 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import java.util.Objects; +import java.util.concurrent.atomic.AtomicInteger; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.operators.flowable.FlowableConcatMap.*; +import io.reactivex.rxjava3.internal.subscriptions.SubscriptionHelper; +import io.reactivex.rxjava3.internal.util.*; +import io.reactivex.rxjava3.operators.QueueSubscription; +import io.reactivex.rxjava3.operators.SimpleQueue; +import io.reactivex.rxjava3.operators.SpscArrayQueue; + +public final class FlowableConcatMapScheduler<T, R> extends AbstractFlowableWithUpstream<T, R> { + + final Function<? super T, ? extends Publisher<? extends R>> mapper; + + final int prefetch; + + final ErrorMode errorMode; + + final Scheduler scheduler; + + public FlowableConcatMapScheduler(Flowable<T> source, + Function<? super T, ? extends Publisher<? extends R>> mapper, + int prefetch, ErrorMode errorMode, Scheduler scheduler) { + super(source); + this.mapper = mapper; + this.prefetch = prefetch; + this.errorMode = errorMode; + this.scheduler = scheduler; + } + + @Override + protected void subscribeActual(Subscriber<? super R> s) { + switch (errorMode) { + case BOUNDARY: + source.subscribe(new ConcatMapDelayed<>(s, mapper, prefetch, false, scheduler.createWorker())); + break; + case END: + source.subscribe(new ConcatMapDelayed<>(s, mapper, prefetch, true, scheduler.createWorker())); + break; + default: + source.subscribe(new ConcatMapImmediate<>(s, mapper, prefetch, scheduler.createWorker())); + } + } + + abstract static class BaseConcatMapSubscriber<T, R> + extends AtomicInteger + implements FlowableSubscriber<T>, ConcatMapSupport<R>, Subscription, Runnable { + + private static final long serialVersionUID = -3511336836796789179L; + + final ConcatMapInner<R> inner; + + final Function<? super T, ? extends Publisher<? extends R>> mapper; + + final int prefetch; + + final int limit; + + final Scheduler.Worker worker; + + Subscription upstream; + + int consumed; + + SimpleQueue<T> queue; + + volatile boolean done; + + volatile boolean cancelled; + + final AtomicThrowable errors; + + volatile boolean active; + + int sourceMode; + + BaseConcatMapSubscriber( + Function<? super T, ? extends Publisher<? extends R>> mapper, + int prefetch, Scheduler.Worker worker) { + this.mapper = mapper; + this.prefetch = prefetch; + this.limit = prefetch - (prefetch >> 2); + this.inner = new ConcatMapInner<>(this); + this.errors = new AtomicThrowable(); + this.worker = worker; + } + + @Override + public final void onSubscribe(Subscription s) { + if (SubscriptionHelper.validate(this.upstream, s)) { + this.upstream = s; + + if (s instanceof QueueSubscription) { + @SuppressWarnings("unchecked") QueueSubscription<T> f = (QueueSubscription<T>)s; + int m = f.requestFusion(QueueSubscription.ANY | QueueSubscription.BOUNDARY); + if (m == QueueSubscription.SYNC) { + sourceMode = m; + queue = f; + done = true; + + subscribeActual(); + + schedule(); + return; + } + if (m == QueueSubscription.ASYNC) { + sourceMode = m; + queue = f; + + subscribeActual(); + + s.request(prefetch); + return; + } + } + + queue = new SpscArrayQueue<>(prefetch); + + subscribeActual(); + + s.request(prefetch); + } + } + + abstract void schedule(); + + abstract void subscribeActual(); + + @Override + public final void onNext(T t) { + if (sourceMode != QueueSubscription.ASYNC) { + if (!queue.offer(t)) { + upstream.cancel(); + onError(new QueueOverflowException()); + return; + } + } + schedule(); + } + + @Override + public final void onComplete() { + done = true; + schedule(); + } + + @Override + public final void innerComplete() { + active = false; + schedule(); + } + + } + + static final class ConcatMapImmediate<T, R> + extends BaseConcatMapSubscriber<T, R> { + + private static final long serialVersionUID = 7898995095634264146L; + + final Subscriber<? super R> downstream; + + final AtomicInteger wip; + + ConcatMapImmediate(Subscriber<? super R> actual, + Function<? super T, ? extends Publisher<? extends R>> mapper, + int prefetch, Scheduler.Worker worker) { + super(mapper, prefetch, worker); + this.downstream = actual; + this.wip = new AtomicInteger(); + } + + @Override + void subscribeActual() { + downstream.onSubscribe(this); + } + + @Override + public void onError(Throwable t) { + if (errors.tryAddThrowableOrReport(t)) { + inner.cancel(); + + if (getAndIncrement() == 0) { + errors.tryTerminateConsumer(downstream); + worker.dispose(); + } + } + } + + boolean tryEnter() { + return get() == 0 && compareAndSet(0, 1); + } + + @Override + public void innerNext(R value) { + if (tryEnter()) { + downstream.onNext(value); + if (compareAndSet(1, 0)) { + return; + } + errors.tryTerminateConsumer(downstream); + worker.dispose(); + } + } + + @Override + public void innerError(Throwable e) { + if (errors.tryAddThrowableOrReport(e)) { + upstream.cancel(); + + if (getAndIncrement() == 0) { + errors.tryTerminateConsumer(downstream); + worker.dispose(); + } + } + } + + @Override + public void request(long n) { + inner.request(n); + } + + @Override + public void cancel() { + if (!cancelled) { + cancelled = true; + + inner.cancel(); + upstream.cancel(); + worker.dispose(); + errors.tryTerminateAndReport(); + } + } + + @Override + void schedule() { + if (wip.getAndIncrement() == 0) { + worker.schedule(this); + } + } + + @Override + public void run() { + for (;;) { + if (cancelled) { + return; + } + + if (!active) { + boolean d = done; + + T v; + + try { + v = queue.poll(); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + upstream.cancel(); + errors.tryAddThrowableOrReport(e); + errors.tryTerminateConsumer(downstream); + worker.dispose(); + return; + } + + boolean empty = v == null; + + if (d && empty) { + downstream.onComplete(); + worker.dispose(); + return; + } + + if (!empty) { + Publisher<? extends R> p; + + try { + p = Objects.requireNonNull(mapper.apply(v), "The mapper returned a null Publisher"); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + + upstream.cancel(); + errors.tryAddThrowableOrReport(e); + errors.tryTerminateConsumer(downstream); + worker.dispose(); + return; + } + + if (sourceMode != QueueSubscription.SYNC) { + int c = consumed + 1; + if (c == limit) { + consumed = 0; + upstream.request(c); + } else { + consumed = c; + } + } + + if (p instanceof Supplier) { + @SuppressWarnings("unchecked") + Supplier<R> supplier = (Supplier<R>) p; + + R vr; + + try { + vr = supplier.get(); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + upstream.cancel(); + errors.tryAddThrowableOrReport(e); + errors.tryTerminateConsumer(downstream); + worker.dispose(); + return; + } + + if (vr == null || cancelled) { + continue; + } + + if (inner.isUnbounded()) { + if (tryEnter()) { + downstream.onNext(vr); + if (!compareAndSet(1, 0)) { + errors.tryTerminateConsumer(downstream); + worker.dispose(); + return; + } + } + continue; + } else { + active = true; + inner.setSubscription(new SimpleScalarSubscription<>(vr, inner)); + } + + } else { + active = true; + p.subscribe(inner); + } + } + } + if (wip.decrementAndGet() == 0) { + break; + } + } + } + } + + static final class ConcatMapDelayed<T, R> + extends BaseConcatMapSubscriber<T, R> { + + private static final long serialVersionUID = -2945777694260521066L; + + final Subscriber<? super R> downstream; + + final boolean veryEnd; + + ConcatMapDelayed(Subscriber<? super R> actual, + Function<? super T, ? extends Publisher<? extends R>> mapper, + int prefetch, boolean veryEnd, Scheduler.Worker worker) { + super(mapper, prefetch, worker); + this.downstream = actual; + this.veryEnd = veryEnd; + } + + @Override + void subscribeActual() { + downstream.onSubscribe(this); + } + + @Override + public void onError(Throwable t) { + if (errors.tryAddThrowableOrReport(t)) { + done = true; + schedule(); + } + } + + @Override + public void innerNext(R value) { + downstream.onNext(value); + } + + @Override + public void innerError(Throwable e) { + if (errors.tryAddThrowableOrReport(e)) { + if (!veryEnd) { + upstream.cancel(); + done = true; + } + active = false; + schedule(); + } + } + + @Override + public void request(long n) { + inner.request(n); + } + + @Override + public void cancel() { + if (!cancelled) { + cancelled = true; + + inner.cancel(); + upstream.cancel(); + worker.dispose(); + errors.tryTerminateAndReport(); + } + } + + @Override + void schedule() { + if (getAndIncrement() == 0) { + worker.schedule(this); + } + } + + @Override + public void run() { + + for (;;) { + if (cancelled) { + return; + } + + if (!active) { + + boolean d = done; + + if (d && !veryEnd) { + Throwable ex = errors.get(); + if (ex != null) { + errors.tryTerminateConsumer(downstream); + worker.dispose(); + return; + } + } + + T v; + + try { + v = queue.poll(); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + upstream.cancel(); + errors.tryAddThrowableOrReport(e); + errors.tryTerminateConsumer(downstream); + worker.dispose(); + return; + } + + boolean empty = v == null; + + if (d && empty) { + errors.tryTerminateConsumer(downstream); + worker.dispose(); + return; + } + + if (!empty) { + Publisher<? extends R> p; + + try { + p = Objects.requireNonNull(mapper.apply(v), "The mapper returned a null Publisher"); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + + upstream.cancel(); + errors.tryAddThrowableOrReport(e); + errors.tryTerminateConsumer(downstream); + worker.dispose(); + return; + } + + if (sourceMode != QueueSubscription.SYNC) { + int c = consumed + 1; + if (c == limit) { + consumed = 0; + upstream.request(c); + } else { + consumed = c; + } + } + + if (p instanceof Supplier) { + @SuppressWarnings("unchecked") + Supplier<R> supplier = (Supplier<R>) p; + + R vr; + + try { + vr = supplier.get(); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + errors.tryAddThrowableOrReport(e); + if (!veryEnd) { + upstream.cancel(); + errors.tryTerminateConsumer(downstream); + worker.dispose(); + return; + } + vr = null; + } + + if (vr == null || cancelled) { + continue; + } + + if (inner.isUnbounded()) { + downstream.onNext(vr); + continue; + } else { + active = true; + inner.setSubscription(new SimpleScalarSubscription<>(vr, inner)); + } + } else { + active = true; + p.subscribe(inner); + } + } + } + if (decrementAndGet() == 0) { + break; + } + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableConcatWithCompletable.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableConcatWithCompletable.java new file mode 100644 index 0000000000..81ac13776a --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableConcatWithCompletable.java @@ -0,0 +1,112 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import java.util.concurrent.atomic.AtomicReference; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; +import io.reactivex.rxjava3.internal.subscriptions.SubscriptionHelper; + +/** + * Subscribe to a main Flowable first, then when it completes normally, subscribe to a Completable + * and terminate when it terminates. + * <p>History: 2.1.10 - experimental + * @param <T> the element type of the main source and output type + * @since 2.2 + */ +public final class FlowableConcatWithCompletable<T> extends AbstractFlowableWithUpstream<T, T> { + + final CompletableSource other; + + public FlowableConcatWithCompletable(Flowable<T> source, CompletableSource other) { + super(source); + this.other = other; + } + + @Override + protected void subscribeActual(Subscriber<? super T> s) { + source.subscribe(new ConcatWithSubscriber<>(s, other)); + } + + static final class ConcatWithSubscriber<T> + extends AtomicReference<Disposable> + implements FlowableSubscriber<T>, CompletableObserver, Subscription { + + private static final long serialVersionUID = -7346385463600070225L; + + final Subscriber<? super T> downstream; + + Subscription upstream; + + CompletableSource other; + + boolean inCompletable; + + ConcatWithSubscriber(Subscriber<? super T> actual, CompletableSource other) { + this.downstream = actual; + this.other = other; + } + + @Override + public void onSubscribe(Subscription s) { + if (SubscriptionHelper.validate(upstream, s)) { + this.upstream = s; + downstream.onSubscribe(this); + } + } + + @Override + public void onSubscribe(Disposable d) { + DisposableHelper.setOnce(this, d); + } + + @Override + public void onNext(T t) { + downstream.onNext(t); + } + + @Override + public void onError(Throwable t) { + downstream.onError(t); + } + + @Override + public void onComplete() { + if (inCompletable) { + downstream.onComplete(); + } else { + inCompletable = true; + upstream = SubscriptionHelper.CANCELLED; + CompletableSource cs = other; + other = null; + cs.subscribe(this); + } + } + + @Override + public void request(long n) { + upstream.request(n); + } + + @Override + public void cancel() { + upstream.cancel(); + DisposableHelper.dispose(this); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableConcatWithMaybe.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableConcatWithMaybe.java new file mode 100644 index 0000000000..e82076533a --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableConcatWithMaybe.java @@ -0,0 +1,105 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import java.util.concurrent.atomic.AtomicReference; + +import org.reactivestreams.Subscriber; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; +import io.reactivex.rxjava3.internal.subscribers.SinglePostCompleteSubscriber; +import io.reactivex.rxjava3.internal.subscriptions.SubscriptionHelper; + +/** + * Subscribe to a main Flowable first, then when it completes normally, subscribe to a Maybe, + * signal its success value followed by a completion or signal its error or completion signal as is. + * <p>History: 2.1.10 - experimental + * @param <T> the element type of the main source and output type + * @since 2.2 + */ +public final class FlowableConcatWithMaybe<T> extends AbstractFlowableWithUpstream<T, T> { + + final MaybeSource<? extends T> other; + + public FlowableConcatWithMaybe(Flowable<T> source, MaybeSource<? extends T> other) { + super(source); + this.other = other; + } + + @Override + protected void subscribeActual(Subscriber<? super T> s) { + source.subscribe(new ConcatWithSubscriber<>(s, other)); + } + + static final class ConcatWithSubscriber<T> + extends SinglePostCompleteSubscriber<T, T> + implements MaybeObserver<T> { + + private static final long serialVersionUID = -7346385463600070225L; + + final AtomicReference<Disposable> otherDisposable; + + MaybeSource<? extends T> other; + + boolean inMaybe; + + ConcatWithSubscriber(Subscriber<? super T> actual, MaybeSource<? extends T> other) { + super(actual); + this.other = other; + this.otherDisposable = new AtomicReference<>(); + } + + @Override + public void onSubscribe(Disposable d) { + DisposableHelper.setOnce(otherDisposable, d); + } + + @Override + public void onNext(T t) { + produced++; + downstream.onNext(t); + } + + @Override + public void onError(Throwable t) { + downstream.onError(t); + } + + @Override + public void onSuccess(T t) { + complete(t); + } + + @Override + public void onComplete() { + if (inMaybe) { + downstream.onComplete(); + } else { + inMaybe = true; + upstream = SubscriptionHelper.CANCELLED; + MaybeSource<? extends T> ms = other; + other = null; + ms.subscribe(this); + } + } + + @Override + public void cancel() { + super.cancel(); + DisposableHelper.dispose(otherDisposable); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableConcatWithSingle.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableConcatWithSingle.java new file mode 100644 index 0000000000..bfe900ab2e --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableConcatWithSingle.java @@ -0,0 +1,98 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import java.util.concurrent.atomic.AtomicReference; + +import org.reactivestreams.Subscriber; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; +import io.reactivex.rxjava3.internal.subscribers.SinglePostCompleteSubscriber; +import io.reactivex.rxjava3.internal.subscriptions.SubscriptionHelper; + +/** + * Subscribe to a main Flowable first, then when it completes normally, subscribe to a Single, + * signal its success value followed by a completion or signal its error as is. + * <p>History: 2.1.10 - experimental + * @param <T> the element type of the main source and output type + * @since 2.2 + */ +public final class FlowableConcatWithSingle<T> extends AbstractFlowableWithUpstream<T, T> { + + final SingleSource<? extends T> other; + + public FlowableConcatWithSingle(Flowable<T> source, SingleSource<? extends T> other) { + super(source); + this.other = other; + } + + @Override + protected void subscribeActual(Subscriber<? super T> s) { + source.subscribe(new ConcatWithSubscriber<>(s, other)); + } + + static final class ConcatWithSubscriber<T> + extends SinglePostCompleteSubscriber<T, T> + implements SingleObserver<T> { + + private static final long serialVersionUID = -7346385463600070225L; + + final AtomicReference<Disposable> otherDisposable; + + SingleSource<? extends T> other; + + ConcatWithSubscriber(Subscriber<? super T> actual, SingleSource<? extends T> other) { + super(actual); + this.other = other; + this.otherDisposable = new AtomicReference<>(); + } + + @Override + public void onSubscribe(Disposable d) { + DisposableHelper.setOnce(otherDisposable, d); + } + + @Override + public void onNext(T t) { + produced++; + downstream.onNext(t); + } + + @Override + public void onError(Throwable t) { + downstream.onError(t); + } + + @Override + public void onSuccess(T t) { + complete(t); + } + + @Override + public void onComplete() { + upstream = SubscriptionHelper.CANCELLED; + SingleSource<? extends T> ss = other; + other = null; + ss.subscribe(this); + } + + @Override + public void cancel() { + super.cancel(); + DisposableHelper.dispose(otherDisposable); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableCount.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableCount.java new file mode 100644 index 0000000000..2c32dd4d76 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableCount.java @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.internal.subscriptions.*; + +public final class FlowableCount<T> extends AbstractFlowableWithUpstream<T, Long> { + + public FlowableCount(Flowable<T> source) { + super(source); + } + + @Override + protected void subscribeActual(Subscriber<? super Long> s) { + source.subscribe(new CountSubscriber(s)); + } + + static final class CountSubscriber extends DeferredScalarSubscription<Long> + implements FlowableSubscriber<Object> { + + private static final long serialVersionUID = 4973004223787171406L; + + Subscription upstream; + + long count; + + CountSubscriber(Subscriber<? super Long> downstream) { + super(downstream); + } + + @Override + public void onSubscribe(Subscription s) { + if (SubscriptionHelper.validate(this.upstream, s)) { + this.upstream = s; + downstream.onSubscribe(this); + s.request(Long.MAX_VALUE); + } + } + + @Override + public void onNext(Object t) { + count++; + } + + @Override + public void onError(Throwable t) { + downstream.onError(t); + } + + @Override + public void onComplete() { + complete(count); + } + + @Override + public void cancel() { + super.cancel(); + upstream.cancel(); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableCountSingle.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableCountSingle.java new file mode 100644 index 0000000000..750f722dbc --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableCountSingle.java @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import org.reactivestreams.Subscription; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.fuseable.FuseToFlowable; +import io.reactivex.rxjava3.internal.subscriptions.SubscriptionHelper; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +public final class FlowableCountSingle<T> extends Single<Long> implements FuseToFlowable<Long> { + + final Flowable<T> source; + + public FlowableCountSingle(Flowable<T> source) { + this.source = source; + } + + @Override + protected void subscribeActual(SingleObserver<? super Long> observer) { + source.subscribe(new CountSubscriber(observer)); + } + + @Override + public Flowable<Long> fuseToFlowable() { + return RxJavaPlugins.onAssembly(new FlowableCount<>(source)); + } + + static final class CountSubscriber implements FlowableSubscriber<Object>, Disposable { + + final SingleObserver<? super Long> downstream; + + Subscription upstream; + + long count; + + CountSubscriber(SingleObserver<? super Long> downstream) { + this.downstream = downstream; + } + + @Override + public void onSubscribe(Subscription s) { + if (SubscriptionHelper.validate(this.upstream, s)) { + this.upstream = s; + downstream.onSubscribe(this); + s.request(Long.MAX_VALUE); + } + } + + @Override + public void onNext(Object t) { + count++; + } + + @Override + public void onError(Throwable t) { + upstream = SubscriptionHelper.CANCELLED; + downstream.onError(t); + } + + @Override + public void onComplete() { + upstream = SubscriptionHelper.CANCELLED; + downstream.onSuccess(count); + } + + @Override + public void dispose() { + upstream.cancel(); + upstream = SubscriptionHelper.CANCELLED; + } + + @Override + public boolean isDisposed() { + return upstream == SubscriptionHelper.CANCELLED; + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableCreate.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableCreate.java new file mode 100644 index 0000000000..1e87c4b7e6 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableCreate.java @@ -0,0 +1,725 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import java.util.concurrent.atomic.*; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.Cancellable; +import io.reactivex.rxjava3.internal.disposables.*; +import io.reactivex.rxjava3.internal.subscriptions.SubscriptionHelper; +import io.reactivex.rxjava3.internal.util.*; +import io.reactivex.rxjava3.operators.SimplePlainQueue; +import io.reactivex.rxjava3.operators.SpscLinkedArrayQueue; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +public final class FlowableCreate<T> extends Flowable<T> { + + final FlowableOnSubscribe<T> source; + + final BackpressureStrategy backpressure; + + public FlowableCreate(FlowableOnSubscribe<T> source, BackpressureStrategy backpressure) { + this.source = source; + this.backpressure = backpressure; + } + + @Override + public void subscribeActual(Subscriber<? super T> t) { + BaseEmitter<T> emitter; + + switch (backpressure) { + case MISSING: { + emitter = new MissingEmitter<>(t); + break; + } + case ERROR: { + emitter = new ErrorAsyncEmitter<>(t); + break; + } + case DROP: { + emitter = new DropAsyncEmitter<>(t); + break; + } + case LATEST: { + emitter = new LatestAsyncEmitter<>(t); + break; + } + default: { + emitter = new BufferAsyncEmitter<>(t, bufferSize()); + break; + } + } + + t.onSubscribe(emitter); + try { + source.subscribe(emitter); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + emitter.onError(ex); + } + } + + /** + * Serializes calls to onNext, onError and onComplete. + * + * @param <T> the value type + */ + static final class SerializedEmitter<T> + extends AtomicInteger + implements FlowableEmitter<T> { + + private static final long serialVersionUID = 4883307006032401862L; + + final BaseEmitter<T> emitter; + + final AtomicThrowable errors; + + final SimplePlainQueue<T> queue; + + volatile boolean done; + + SerializedEmitter(BaseEmitter<T> emitter) { + this.emitter = emitter; + this.errors = new AtomicThrowable(); + this.queue = new SpscLinkedArrayQueue<>(16); + } + + @Override + public void onNext(T t) { + if (emitter.isCancelled() || done) { + return; + } + if (t == null) { + onError(ExceptionHelper.createNullPointerException("onNext called with a null value.")); + return; + } + if (get() == 0 && compareAndSet(0, 1)) { + emitter.onNext(t); + if (decrementAndGet() == 0) { + return; + } + } else { + SimplePlainQueue<T> q = queue; + synchronized (q) { + q.offer(t); + } + if (getAndIncrement() != 0) { + return; + } + } + drainLoop(); + } + + @Override + public void onError(Throwable t) { + if (!tryOnError(t)) { + RxJavaPlugins.onError(t); + } + } + + @Override + public boolean tryOnError(Throwable t) { + if (emitter.isCancelled() || done) { + return false; + } + if (t == null) { + t = ExceptionHelper.createNullPointerException("onError called with a null Throwable."); + } + if (errors.tryAddThrowable(t)) { + done = true; + drain(); + return true; + } + return false; + } + + @Override + public void onComplete() { + if (emitter.isCancelled() || done) { + return; + } + done = true; + drain(); + } + + void drain() { + if (getAndIncrement() == 0) { + drainLoop(); + } + } + + void drainLoop() { + BaseEmitter<T> e = emitter; + SimplePlainQueue<T> q = queue; + AtomicThrowable errors = this.errors; + int missed = 1; + for (;;) { + + for (;;) { + if (e.isCancelled()) { + q.clear(); + return; + } + + if (errors.get() != null) { + q.clear(); + errors.tryTerminateConsumer(e); + return; + } + + boolean d = done; + + T v = q.poll(); + + boolean empty = v == null; + + if (d && empty) { + e.onComplete(); + return; + } + + if (empty) { + break; + } + + e.onNext(v); + } + + missed = addAndGet(-missed); + if (missed == 0) { + break; + } + } + } + + @Override + public void setDisposable(Disposable d) { + emitter.setDisposable(d); + } + + @Override + public void setCancellable(Cancellable c) { + emitter.setCancellable(c); + } + + @Override + public long requested() { + return emitter.requested(); + } + + @Override + public boolean isCancelled() { + return emitter.isCancelled(); + } + + @Override + public FlowableEmitter<T> serialize() { + return this; + } + + @Override + public String toString() { + return emitter.toString(); + } + } + + abstract static class BaseEmitter<T> + extends AtomicLong + implements FlowableEmitter<T>, Subscription { + private static final long serialVersionUID = 7326289992464377023L; + + final Subscriber<? super T> downstream; + + final SequentialDisposable serial; + + BaseEmitter(Subscriber<? super T> downstream) { + this.downstream = downstream; + this.serial = new SequentialDisposable(); + } + + @Override + public void onComplete() { + completeDownstream(); + } + + protected void completeDownstream() { + if (isCancelled()) { + return; + } + try { + downstream.onComplete(); + } finally { + serial.dispose(); + } + } + + @Override + public final void onError(Throwable e) { + if (e == null) { + e = ExceptionHelper.createNullPointerException("onError called with a null Throwable."); + } + if (!signalError(e)) { + RxJavaPlugins.onError(e); + } + } + + @Override + public final boolean tryOnError(Throwable e) { + if (e == null) { + e = ExceptionHelper.createNullPointerException("tryOnError called with a null Throwable."); + } + return signalError(e); + } + + public boolean signalError(Throwable e) { + return errorDownstream(e); + } + + protected boolean errorDownstream(Throwable e) { + if (isCancelled()) { + return false; + } + try { + downstream.onError(e); + } finally { + serial.dispose(); + } + return true; + } + + @Override + public final void cancel() { + serial.dispose(); + onUnsubscribed(); + } + + void onUnsubscribed() { + // default is no-op + } + + @Override + public final boolean isCancelled() { + return serial.isDisposed(); + } + + @Override + public final void request(long n) { + if (SubscriptionHelper.validate(n)) { + BackpressureHelper.add(this, n); + onRequested(); + } + } + + void onRequested() { + // default is no-op + } + + @Override + public final void setDisposable(Disposable d) { + serial.update(d); + } + + @Override + public final void setCancellable(Cancellable c) { + setDisposable(new CancellableDisposable(c)); + } + + @Override + public final long requested() { + return get(); + } + + @Override + public final FlowableEmitter<T> serialize() { + return new SerializedEmitter<>(this); + } + + @Override + public String toString() { + return String.format("%s{%s}", getClass().getSimpleName(), super.toString()); + } + } + + static final class MissingEmitter<T> extends BaseEmitter<T> { + + private static final long serialVersionUID = 3776720187248809713L; + + MissingEmitter(Subscriber<? super T> downstream) { + super(downstream); + } + + @Override + public void onNext(T t) { + if (isCancelled()) { + return; + } + + if (t != null) { + downstream.onNext(t); + } else { + onError(ExceptionHelper.createNullPointerException("onNext called with a null value.")); + return; + } + + for (;;) { + long r = get(); + if (r == 0L || compareAndSet(r, r - 1)) { + return; + } + } + } + + } + + abstract static class NoOverflowBaseAsyncEmitter<T> extends BaseEmitter<T> { + + private static final long serialVersionUID = 4127754106204442833L; + + NoOverflowBaseAsyncEmitter(Subscriber<? super T> downstream) { + super(downstream); + } + + @Override + public final void onNext(T t) { + if (isCancelled()) { + return; + } + + if (t == null) { + onError(ExceptionHelper.createNullPointerException("onNext called with a null value.")); + return; + } + + if (get() != 0) { + downstream.onNext(t); + BackpressureHelper.produced(this, 1); + } else { + onOverflow(); + } + } + + abstract void onOverflow(); + } + + static final class DropAsyncEmitter<T> extends NoOverflowBaseAsyncEmitter<T> { + + private static final long serialVersionUID = 8360058422307496563L; + + DropAsyncEmitter(Subscriber<? super T> downstream) { + super(downstream); + } + + @Override + void onOverflow() { + // nothing to do + } + + } + + static final class ErrorAsyncEmitter<T> extends NoOverflowBaseAsyncEmitter<T> { + + private static final long serialVersionUID = 338953216916120960L; + + ErrorAsyncEmitter(Subscriber<? super T> downstream) { + super(downstream); + } + + @Override + void onOverflow() { + onError(new MissingBackpressureException("create: " + MissingBackpressureException.DEFAULT_MESSAGE)); + } + + } + + static final class BufferAsyncEmitter<T> extends BaseEmitter<T> { + + private static final long serialVersionUID = 2427151001689639875L; + + final SpscLinkedArrayQueue<T> queue; + + Throwable error; + volatile boolean done; + + final AtomicInteger wip; + + BufferAsyncEmitter(Subscriber<? super T> actual, int capacityHint) { + super(actual); + this.queue = new SpscLinkedArrayQueue<>(capacityHint); + this.wip = new AtomicInteger(); + } + + @Override + public void onNext(T t) { + if (done || isCancelled()) { + return; + } + + if (t == null) { + onError(ExceptionHelper.createNullPointerException("onNext called with a null value.")); + return; + } + queue.offer(t); + drain(); + } + + @Override + public boolean signalError(Throwable e) { + if (done || isCancelled()) { + return false; + } + + error = e; + done = true; + drain(); + return true; + } + + @Override + public void onComplete() { + done = true; + drain(); + } + + @Override + void onRequested() { + drain(); + } + + @Override + void onUnsubscribed() { + if (wip.getAndIncrement() == 0) { + queue.clear(); + } + } + + void drain() { + if (wip.getAndIncrement() != 0) { + return; + } + + int missed = 1; + final Subscriber<? super T> a = downstream; + final SpscLinkedArrayQueue<T> q = queue; + + for (;;) { + long r = get(); + long e = 0L; + + while (e != r) { + if (isCancelled()) { + q.clear(); + return; + } + + boolean d = done; + + T o = q.poll(); + + boolean empty = o == null; + + if (d && empty) { + Throwable ex = error; + if (ex != null) { + errorDownstream(ex); + } else { + completeDownstream(); + } + return; + } + + if (empty) { + break; + } + + a.onNext(o); + + e++; + } + + if (e == r) { + if (isCancelled()) { + q.clear(); + return; + } + + boolean d = done; + + boolean empty = q.isEmpty(); + + if (d && empty) { + Throwable ex = error; + if (ex != null) { + errorDownstream(ex); + } else { + completeDownstream(); + } + return; + } + } + + if (e != 0) { + BackpressureHelper.produced(this, e); + } + + missed = wip.addAndGet(-missed); + if (missed == 0) { + break; + } + } + } + } + + static final class LatestAsyncEmitter<T> extends BaseEmitter<T> { + + private static final long serialVersionUID = 4023437720691792495L; + + final AtomicReference<T> queue; + + Throwable error; + volatile boolean done; + + final AtomicInteger wip; + + LatestAsyncEmitter(Subscriber<? super T> downstream) { + super(downstream); + this.queue = new AtomicReference<>(); + this.wip = new AtomicInteger(); + } + + @Override + public void onNext(T t) { + if (done || isCancelled()) { + return; + } + + if (t == null) { + onError(ExceptionHelper.createNullPointerException("onNext called with a null value.")); + return; + } + queue.set(t); + drain(); + } + + @Override + public boolean signalError(Throwable e) { + if (done || isCancelled()) { + return false; + } + error = e; + done = true; + drain(); + return true; + } + + @Override + public void onComplete() { + done = true; + drain(); + } + + @Override + void onRequested() { + drain(); + } + + @Override + void onUnsubscribed() { + if (wip.getAndIncrement() == 0) { + queue.lazySet(null); + } + } + + void drain() { + if (wip.getAndIncrement() != 0) { + return; + } + + int missed = 1; + final Subscriber<? super T> a = downstream; + final AtomicReference<T> q = queue; + + for (;;) { + long r = get(); + long e = 0L; + + while (e != r) { + if (isCancelled()) { + q.lazySet(null); + return; + } + + boolean d = done; + + T o = q.getAndSet(null); + + boolean empty = o == null; + + if (d && empty) { + Throwable ex = error; + if (ex != null) { + errorDownstream(ex); + } else { + completeDownstream(); + } + return; + } + + if (empty) { + break; + } + + a.onNext(o); + + e++; + } + + if (e == r) { + if (isCancelled()) { + q.lazySet(null); + return; + } + + boolean d = done; + + boolean empty = q.get() == null; + + if (d && empty) { + Throwable ex = error; + if (ex != null) { + errorDownstream(ex); + } else { + completeDownstream(); + } + return; + } + } + + if (e != 0) { + BackpressureHelper.produced(this, e); + } + + missed = wip.addAndGet(-missed); + if (missed == 0) { + break; + } + } + } + } + +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableDebounce.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableDebounce.java new file mode 100644 index 0000000000..51838a1864 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableDebounce.java @@ -0,0 +1,207 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import java.util.Objects; +import java.util.concurrent.atomic.*; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; +import io.reactivex.rxjava3.internal.subscriptions.SubscriptionHelper; +import io.reactivex.rxjava3.internal.util.BackpressureHelper; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.subscribers.*; + +public final class FlowableDebounce<T, U> extends AbstractFlowableWithUpstream<T, T> { + final Function<? super T, ? extends Publisher<U>> debounceSelector; + + public FlowableDebounce(Flowable<T> source, Function<? super T, ? extends Publisher<U>> debounceSelector) { + super(source); + this.debounceSelector = debounceSelector; + } + + @Override + protected void subscribeActual(Subscriber<? super T> s) { + source.subscribe(new DebounceSubscriber<>(new SerializedSubscriber<>(s), debounceSelector)); + } + + static final class DebounceSubscriber<T, U> extends AtomicLong + implements FlowableSubscriber<T>, Subscription { + + private static final long serialVersionUID = 6725975399620862591L; + final Subscriber<? super T> downstream; + final Function<? super T, ? extends Publisher<U>> debounceSelector; + + Subscription upstream; + + final AtomicReference<Disposable> debouncer = new AtomicReference<>(); + + volatile long index; + + boolean done; + + DebounceSubscriber(Subscriber<? super T> actual, + Function<? super T, ? extends Publisher<U>> debounceSelector) { + this.downstream = actual; + this.debounceSelector = debounceSelector; + } + + @Override + public void onSubscribe(Subscription s) { + if (SubscriptionHelper.validate(this.upstream, s)) { + this.upstream = s; + downstream.onSubscribe(this); + s.request(Long.MAX_VALUE); + } + } + + @Override + public void onNext(T t) { + if (done) { + return; + } + + long idx = index + 1; + index = idx; + + Disposable d = debouncer.get(); + if (d != null) { + d.dispose(); + } + + Publisher<U> p; + + try { + p = Objects.requireNonNull(debounceSelector.apply(t), "The publisher supplied is null"); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + cancel(); + downstream.onError(e); + return; + } + + DebounceInnerSubscriber<T, U> dis = new DebounceInnerSubscriber<>(this, idx, t); + + if (debouncer.compareAndSet(d, dis)) { + p.subscribe(dis); + } + } + + @Override + public void onError(Throwable t) { + DisposableHelper.dispose(debouncer); + downstream.onError(t); + } + + @Override + public void onComplete() { + if (done) { + return; + } + done = true; + Disposable d = debouncer.get(); + if (!DisposableHelper.isDisposed(d)) { + @SuppressWarnings("unchecked") + DebounceInnerSubscriber<T, U> dis = (DebounceInnerSubscriber<T, U>)d; + if (dis != null) { + dis.emit(); + } + DisposableHelper.dispose(debouncer); + downstream.onComplete(); + } + } + + @Override + public void request(long n) { + if (SubscriptionHelper.validate(n)) { + BackpressureHelper.add(this, n); + } + } + + @Override + public void cancel() { + upstream.cancel(); + DisposableHelper.dispose(debouncer); + } + + void emit(long idx, T value) { + if (idx == index) { + long r = get(); + if (r != 0L) { + downstream.onNext(value); + BackpressureHelper.produced(this, 1); + } else { + cancel(); + downstream.onError(MissingBackpressureException.createDefault()); + } + } + } + + static final class DebounceInnerSubscriber<T, U> extends DisposableSubscriber<U> { + final DebounceSubscriber<T, U> parent; + final long index; + final T value; + + boolean done; + + final AtomicBoolean once = new AtomicBoolean(); + + DebounceInnerSubscriber(DebounceSubscriber<T, U> parent, long index, T value) { + this.parent = parent; + this.index = index; + this.value = value; + } + + @Override + public void onNext(U t) { + if (done) { + return; + } + done = true; + cancel(); + emit(); + } + + void emit() { + if (once.compareAndSet(false, true)) { + parent.emit(index, value); + } + } + + @Override + public void onError(Throwable t) { + if (done) { + RxJavaPlugins.onError(t); + return; + } + done = true; + parent.onError(t); + } + + @Override + public void onComplete() { + if (done) { + return; + } + done = true; + emit(); + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableDebounceTimed.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableDebounceTimed.java new file mode 100644 index 0000000000..b9e5dff7f3 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableDebounceTimed.java @@ -0,0 +1,223 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.*; + +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.Consumer; +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.core.Scheduler.Worker; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.MissingBackpressureException; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; +import io.reactivex.rxjava3.internal.subscriptions.SubscriptionHelper; +import io.reactivex.rxjava3.internal.util.BackpressureHelper; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.subscribers.SerializedSubscriber; + +public final class FlowableDebounceTimed<T> extends AbstractFlowableWithUpstream<T, T> { + final long timeout; + final TimeUnit unit; + final Scheduler scheduler; + final Consumer<? super T> onDropped; + + public FlowableDebounceTimed(Flowable<T> source, long timeout, TimeUnit unit, Scheduler scheduler, Consumer<? super T> onDropped) { + super(source); + this.timeout = timeout; + this.unit = unit; + this.scheduler = scheduler; + this.onDropped = onDropped; + } + + @Override + protected void subscribeActual(Subscriber<? super T> s) { + source.subscribe(new DebounceTimedSubscriber<>( + new SerializedSubscriber<>(s), timeout, unit, scheduler.createWorker(), onDropped)); + } + + static final class DebounceTimedSubscriber<T> extends AtomicLong + implements FlowableSubscriber<T>, Subscription { + + private static final long serialVersionUID = -9102637559663639004L; + final Subscriber<? super T> downstream; + final long timeout; + final TimeUnit unit; + final Scheduler.Worker worker; + final Consumer<? super T> onDropped; + + Subscription upstream; + + DebounceEmitter<T> timer; + + volatile long index; + + boolean done; + + DebounceTimedSubscriber(Subscriber<? super T> actual, long timeout, TimeUnit unit, Worker worker, Consumer<? super T> onDropped) { + this.downstream = actual; + this.timeout = timeout; + this.unit = unit; + this.worker = worker; + this.onDropped = onDropped; + } + + @Override + public void onSubscribe(Subscription s) { + if (SubscriptionHelper.validate(this.upstream, s)) { + this.upstream = s; + downstream.onSubscribe(this); + s.request(Long.MAX_VALUE); + } + } + + @Override + public void onNext(T t) { + if (done) { + return; + } + long idx = index + 1; + index = idx; + + DebounceEmitter<T> currentEmitter = timer; + if (currentEmitter != null) { + currentEmitter.dispose(); + } + + if (onDropped != null && currentEmitter != null) { + try { + onDropped.accept(currentEmitter.value); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + upstream.cancel(); + done = true; + downstream.onError(ex); + worker.dispose(); + } + } + + DebounceEmitter<T> newEmitter = new DebounceEmitter<>(t, idx, this); + timer = newEmitter; + newEmitter.setResource(worker.schedule(newEmitter, timeout, unit)); + } + + @Override + public void onError(Throwable t) { + if (done) { + RxJavaPlugins.onError(t); + return; + } + done = true; + Disposable d = timer; + if (d != null) { + d.dispose(); + } + downstream.onError(t); + worker.dispose(); + } + + @Override + public void onComplete() { + if (done) { + return; + } + done = true; + + DebounceEmitter<T> d = timer; + if (d != null) { + d.dispose(); + } + + if (d != null) { + d.emit(); + } + + downstream.onComplete(); + worker.dispose(); + } + + @Override + public void request(long n) { + if (SubscriptionHelper.validate(n)) { + BackpressureHelper.add(this, n); + } + } + + @Override + public void cancel() { + upstream.cancel(); + worker.dispose(); + } + + void emit(long idx, T t, DebounceEmitter<T> emitter) { + if (idx == index) { + long r = get(); + if (r != 0L) { + downstream.onNext(t); + BackpressureHelper.produced(this, 1); + + emitter.dispose(); + } else { + cancel(); + downstream.onError(MissingBackpressureException.createDefault()); + } + } + } + } + + static final class DebounceEmitter<T> extends AtomicReference<Disposable> implements Runnable, Disposable { + + private static final long serialVersionUID = 6812032969491025141L; + + final T value; + final long idx; + final DebounceTimedSubscriber<T> parent; + + final AtomicBoolean once = new AtomicBoolean(); + + DebounceEmitter(T value, long idx, DebounceTimedSubscriber<T> parent) { + this.value = value; + this.idx = idx; + this.parent = parent; + } + + @Override + public void run() { + emit(); + } + + void emit() { + if (once.compareAndSet(false, true)) { + parent.emit(idx, value, this); + } + } + + @Override + public void dispose() { + DisposableHelper.dispose(this); + } + + @Override + public boolean isDisposed() { + return get() == DisposableHelper.DISPOSED; + } + + public void setResource(Disposable d) { + DisposableHelper.replace(this, d); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableDefer.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableDefer.java new file mode 100644 index 0000000000..b3ea3f1ecc --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableDefer.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.Supplier; +import io.reactivex.rxjava3.internal.subscriptions.EmptySubscription; + +import java.util.Objects; + +public final class FlowableDefer<T> extends Flowable<T> { + final Supplier<? extends Publisher<? extends T>> supplier; + public FlowableDefer(Supplier<? extends Publisher<? extends T>> supplier) { + this.supplier = supplier; + } + + @Override + public void subscribeActual(Subscriber<? super T> s) { + Publisher<? extends T> pub; + try { + pub = Objects.requireNonNull(supplier.get(), "The publisher supplied is null"); + } catch (Throwable t) { + Exceptions.throwIfFatal(t); + EmptySubscription.error(t, s); + return; + } + + pub.subscribe(s); + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableDelay.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableDelay.java new file mode 100644 index 0000000000..469d0dd48b --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableDelay.java @@ -0,0 +1,146 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import java.util.concurrent.TimeUnit; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.core.Scheduler.Worker; +import io.reactivex.rxjava3.internal.subscriptions.SubscriptionHelper; +import io.reactivex.rxjava3.subscribers.SerializedSubscriber; + +public final class FlowableDelay<T> extends AbstractFlowableWithUpstream<T, T> { + final long delay; + final TimeUnit unit; + final Scheduler scheduler; + final boolean delayError; + + public FlowableDelay(Flowable<T> source, long delay, TimeUnit unit, Scheduler scheduler, boolean delayError) { + super(source); + this.delay = delay; + this.unit = unit; + this.scheduler = scheduler; + this.delayError = delayError; + } + + @Override + protected void subscribeActual(Subscriber<? super T> t) { + Subscriber<? super T> downstream; + if (delayError) { + downstream = t; + } else { + downstream = new SerializedSubscriber<>(t); + } + + Scheduler.Worker w = scheduler.createWorker(); + + source.subscribe(new DelaySubscriber<>(downstream, delay, unit, w, delayError)); + } + + static final class DelaySubscriber<T> implements FlowableSubscriber<T>, Subscription { + final Subscriber<? super T> downstream; + final long delay; + final TimeUnit unit; + final Scheduler.Worker w; + final boolean delayError; + + Subscription upstream; + + DelaySubscriber(Subscriber<? super T> actual, long delay, TimeUnit unit, Worker w, boolean delayError) { + super(); + this.downstream = actual; + this.delay = delay; + this.unit = unit; + this.w = w; + this.delayError = delayError; + } + + @Override + public void onSubscribe(Subscription s) { + if (SubscriptionHelper.validate(this.upstream, s)) { + this.upstream = s; + downstream.onSubscribe(this); + } + } + + @Override + public void onNext(final T t) { + w.schedule(new OnNext(t), delay, unit); + } + + @Override + public void onError(final Throwable t) { + w.schedule(new OnError(t), delayError ? delay : 0, unit); + } + + @Override + public void onComplete() { + w.schedule(new OnComplete(), delay, unit); + } + + @Override + public void request(long n) { + upstream.request(n); + } + + @Override + public void cancel() { + upstream.cancel(); + w.dispose(); + } + + final class OnNext implements Runnable { + private final T t; + + OnNext(T t) { + this.t = t; + } + + @Override + public void run() { + downstream.onNext(t); + } + } + + final class OnError implements Runnable { + private final Throwable t; + + OnError(Throwable t) { + this.t = t; + } + + @Override + public void run() { + try { + downstream.onError(t); + } finally { + w.dispose(); + } + } + } + + final class OnComplete implements Runnable { + @Override + public void run() { + try { + downstream.onComplete(); + } finally { + w.dispose(); + } + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableDelaySubscriptionOther.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableDelaySubscriptionOther.java new file mode 100644 index 0000000000..2f12cc6e4e --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableDelaySubscriptionOther.java @@ -0,0 +1,142 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import java.util.concurrent.atomic.*; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.internal.subscriptions.SubscriptionHelper; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +/** + * Delays the subscription to the main source until the other + * observable fires an event or completes. + * @param <T> the main type + * @param <U> the other value type, ignored + */ +public final class FlowableDelaySubscriptionOther<T, U> extends Flowable<T> { + final Publisher<? extends T> main; + final Publisher<U> other; + + public FlowableDelaySubscriptionOther(Publisher<? extends T> main, Publisher<U> other) { + this.main = main; + this.other = other; + } + + @Override + public void subscribeActual(final Subscriber<? super T> child) { + MainSubscriber<T> parent = new MainSubscriber<>(child, main); + child.onSubscribe(parent); + other.subscribe(parent.other); + } + + static final class MainSubscriber<T> extends AtomicLong implements FlowableSubscriber<T>, Subscription { + + private static final long serialVersionUID = 2259811067697317255L; + + final Subscriber<? super T> downstream; + + final Publisher<? extends T> main; + + final OtherSubscriber other; + + final AtomicReference<Subscription> upstream; + + MainSubscriber(Subscriber<? super T> downstream, Publisher<? extends T> main) { + this.downstream = downstream; + this.main = main; + this.other = new OtherSubscriber(); + this.upstream = new AtomicReference<>(); + } + + void next() { + main.subscribe(this); + } + + @Override + public void onNext(T t) { + downstream.onNext(t); + } + + @Override + public void onError(Throwable t) { + downstream.onError(t); + } + + @Override + public void onComplete() { + downstream.onComplete(); + } + + @Override + public void request(long n) { + if (SubscriptionHelper.validate(n)) { + SubscriptionHelper.deferredRequest(upstream, this, n); + } + } + + @Override + public void cancel() { + SubscriptionHelper.cancel(other); + SubscriptionHelper.cancel(upstream); + } + + @Override + public void onSubscribe(Subscription s) { + SubscriptionHelper.deferredSetOnce(upstream, this, s); + } + + final class OtherSubscriber extends AtomicReference<Subscription> implements FlowableSubscriber<Object> { + + private static final long serialVersionUID = -3892798459447644106L; + + @Override + public void onSubscribe(Subscription s) { + if (SubscriptionHelper.setOnce(this, s)) { + s.request(Long.MAX_VALUE); + } + } + + @Override + public void onNext(Object t) { + Subscription s = get(); + if (s != SubscriptionHelper.CANCELLED) { + lazySet(SubscriptionHelper.CANCELLED); + s.cancel(); + next(); + } + } + + @Override + public void onError(Throwable t) { + Subscription s = get(); + if (s != SubscriptionHelper.CANCELLED) { + downstream.onError(t); + } else { + RxJavaPlugins.onError(t); + } + } + + @Override + public void onComplete() { + Subscription s = get(); + if (s != SubscriptionHelper.CANCELLED) { + next(); + } + } + } +} +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableDematerialize.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableDematerialize.java new file mode 100644 index 0000000000..10806f5776 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableDematerialize.java @@ -0,0 +1,127 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.subscriptions.SubscriptionHelper; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +import java.util.Objects; + +public final class FlowableDematerialize<T, R> extends AbstractFlowableWithUpstream<T, R> { + + final Function<? super T, ? extends Notification<R>> selector; + + public FlowableDematerialize(Flowable<T> source, Function<? super T, ? extends Notification<R>> selector) { + super(source); + this.selector = selector; + } + + @Override + protected void subscribeActual(Subscriber<? super R> subscriber) { + source.subscribe(new DematerializeSubscriber<>(subscriber, selector)); + } + + static final class DematerializeSubscriber<T, R> implements FlowableSubscriber<T>, Subscription { + + final Subscriber<? super R> downstream; + + final Function<? super T, ? extends Notification<R>> selector; + + boolean done; + + Subscription upstream; + + DematerializeSubscriber(Subscriber<? super R> downstream, Function<? super T, ? extends Notification<R>> selector) { + this.downstream = downstream; + this.selector = selector; + } + + @Override + public void onSubscribe(Subscription s) { + if (SubscriptionHelper.validate(this.upstream, s)) { + this.upstream = s; + downstream.onSubscribe(this); + } + } + + @Override + public void onNext(T item) { + if (done) { + if (item instanceof Notification) { + Notification<?> notification = (Notification<?>)item; + if (notification.isOnError()) { + RxJavaPlugins.onError(notification.getError()); + } + } + return; + } + + Notification<R> notification; + + try { + notification = Objects.requireNonNull(selector.apply(item), "The selector returned a null Notification"); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + upstream.cancel(); + onError(ex); + return; + } + if (notification.isOnError()) { + upstream.cancel(); + onError(notification.getError()); + } else if (notification.isOnComplete()) { + upstream.cancel(); + onComplete(); + } else { + downstream.onNext(notification.getValue()); + } + } + + @Override + public void onError(Throwable t) { + if (done) { + RxJavaPlugins.onError(t); + return; + } + done = true; + + downstream.onError(t); + } + + @Override + public void onComplete() { + if (done) { + return; + } + done = true; + + downstream.onComplete(); + } + + @Override + public void request(long n) { + upstream.request(n); + } + + @Override + public void cancel() { + upstream.cancel(); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableDetach.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableDetach.java new file mode 100644 index 0000000000..8d755b8ad3 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableDetach.java @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.internal.subscriptions.SubscriptionHelper; +import io.reactivex.rxjava3.internal.util.EmptyComponent; + +public final class FlowableDetach<T> extends AbstractFlowableWithUpstream<T, T> { + + public FlowableDetach(Flowable<T> source) { + super(source); + } + + @Override + protected void subscribeActual(Subscriber<? super T> s) { + source.subscribe(new DetachSubscriber<>(s)); + } + + static final class DetachSubscriber<T> implements FlowableSubscriber<T>, Subscription { + + Subscriber<? super T> downstream; + + Subscription upstream; + + DetachSubscriber(Subscriber<? super T> downstream) { + this.downstream = downstream; + } + + @Override + public void request(long n) { + upstream.request(n); + } + + @Override + public void cancel() { + Subscription s = this.upstream; + this.upstream = EmptyComponent.INSTANCE; + this.downstream = EmptyComponent.asSubscriber(); + s.cancel(); + } + + @Override + public void onSubscribe(Subscription s) { + if (SubscriptionHelper.validate(this.upstream, s)) { + this.upstream = s; + + downstream.onSubscribe(this); + } + } + + @Override + public void onNext(T t) { + downstream.onNext(t); + } + + @Override + public void onError(Throwable t) { + Subscriber<? super T> a = downstream; + this.upstream = EmptyComponent.INSTANCE; + this.downstream = EmptyComponent.asSubscriber(); + a.onError(t); + } + + @Override + public void onComplete() { + Subscriber<? super T> a = downstream; + this.upstream = EmptyComponent.INSTANCE; + this.downstream = EmptyComponent.asSubscriber(); + a.onComplete(); + } + } +} + diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableDistinct.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableDistinct.java new file mode 100644 index 0000000000..7c3c777c26 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableDistinct.java @@ -0,0 +1,144 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import java.util.Collection; +import java.util.Objects; + +import org.reactivestreams.Subscriber; + +import io.reactivex.rxjava3.annotations.Nullable; +import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.subscribers.BasicFuseableSubscriber; +import io.reactivex.rxjava3.internal.subscriptions.EmptySubscription; +import io.reactivex.rxjava3.internal.util.ExceptionHelper; +import io.reactivex.rxjava3.operators.QueueFuseable; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +public final class FlowableDistinct<T, K> extends AbstractFlowableWithUpstream<T, T> { + + final Function<? super T, K> keySelector; + + final Supplier<? extends Collection<? super K>> collectionSupplier; + + public FlowableDistinct(Flowable<T> source, Function<? super T, K> keySelector, Supplier<? extends Collection<? super K>> collectionSupplier) { + super(source); + this.keySelector = keySelector; + this.collectionSupplier = collectionSupplier; + } + + @Override + protected void subscribeActual(Subscriber<? super T> subscriber) { + Collection<? super K> collection; + + try { + collection = ExceptionHelper.nullCheck(collectionSupplier.get(), "The collectionSupplier returned a null Collection."); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + EmptySubscription.error(ex, subscriber); + return; + } + + source.subscribe(new DistinctSubscriber<>(subscriber, keySelector, collection)); + } + + static final class DistinctSubscriber<T, K> extends BasicFuseableSubscriber<T, T> { + + final Collection<? super K> collection; + + final Function<? super T, K> keySelector; + + DistinctSubscriber(Subscriber<? super T> actual, Function<? super T, K> keySelector, Collection<? super K> collection) { + super(actual); + this.keySelector = keySelector; + this.collection = collection; + } + + @Override + public void onNext(T value) { + if (done) { + return; + } + if (sourceMode == NONE) { + K key; + boolean b; + + try { + key = Objects.requireNonNull(keySelector.apply(value), "The keySelector returned a null key"); + b = collection.add(key); + } catch (Throwable ex) { + fail(ex); + return; + } + + if (b) { + downstream.onNext(value); + } else { + upstream.request(1); + } + } else { + downstream.onNext(null); + } + } + + @Override + public void onError(Throwable e) { + if (done) { + RxJavaPlugins.onError(e); + } else { + done = true; + collection.clear(); + downstream.onError(e); + } + } + + @Override + public void onComplete() { + if (!done) { + done = true; + collection.clear(); + downstream.onComplete(); + } + } + + @Override + public int requestFusion(int mode) { + return transitiveBoundaryFusion(mode); + } + + @Nullable + @Override + public T poll() throws Throwable { + for (;;) { + T v = qs.poll(); + + if (v == null || collection.add(Objects.requireNonNull(keySelector.apply(v), "The keySelector returned a null key"))) { + return v; + } else { + if (sourceMode == QueueFuseable.ASYNC) { + upstream.request(1); + } + } + } + } + + @Override + public void clear() { + collection.clear(); + super.clear(); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableDistinctUntilChanged.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableDistinctUntilChanged.java new file mode 100644 index 0000000000..2c597556ec --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableDistinctUntilChanged.java @@ -0,0 +1,227 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import org.reactivestreams.Subscriber; + +import io.reactivex.rxjava3.annotations.Nullable; +import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.subscribers.*; +import io.reactivex.rxjava3.operators.ConditionalSubscriber; + +public final class FlowableDistinctUntilChanged<T, K> extends AbstractFlowableWithUpstream<T, T> { + + final Function<? super T, K> keySelector; + + final BiPredicate<? super K, ? super K> comparer; + + public FlowableDistinctUntilChanged(Flowable<T> source, Function<? super T, K> keySelector, BiPredicate<? super K, ? super K> comparer) { + super(source); + this.keySelector = keySelector; + this.comparer = comparer; + } + + @Override + protected void subscribeActual(Subscriber<? super T> s) { + if (s instanceof ConditionalSubscriber) { + ConditionalSubscriber<? super T> cs = (ConditionalSubscriber<? super T>) s; + source.subscribe(new DistinctUntilChangedConditionalSubscriber<>(cs, keySelector, comparer)); + } else { + source.subscribe(new DistinctUntilChangedSubscriber<>(s, keySelector, comparer)); + } + } + + static final class DistinctUntilChangedSubscriber<T, K> extends BasicFuseableSubscriber<T, T> + implements ConditionalSubscriber<T> { + + final Function<? super T, K> keySelector; + + final BiPredicate<? super K, ? super K> comparer; + + K last; + + boolean hasValue; + + DistinctUntilChangedSubscriber(Subscriber<? super T> actual, + Function<? super T, K> keySelector, + BiPredicate<? super K, ? super K> comparer) { + super(actual); + this.keySelector = keySelector; + this.comparer = comparer; + } + + @Override + public void onNext(T t) { + if (!tryOnNext(t)) { + upstream.request(1); + } + } + + @Override + public boolean tryOnNext(T t) { + if (done) { + return false; + } + if (sourceMode != NONE) { + downstream.onNext(t); + return true; + } + + K key; + + try { + key = keySelector.apply(t); + if (hasValue) { + boolean equal = comparer.test(last, key); + last = key; + if (equal) { + return false; + } + } else { + hasValue = true; + last = key; + } + } catch (Throwable ex) { + fail(ex); + return true; + } + + downstream.onNext(t); + return true; + } + + @Override + public int requestFusion(int mode) { + return transitiveBoundaryFusion(mode); + } + + @Nullable + @Override + public T poll() throws Throwable { + for (;;) { + T v = qs.poll(); + if (v == null) { + return null; + } + K key = keySelector.apply(v); + if (!hasValue) { + hasValue = true; + last = key; + return v; + } + + if (!comparer.test(last, key)) { + last = key; + return v; + } + last = key; + if (sourceMode != SYNC) { + upstream.request(1); + } + } + } + + } + + static final class DistinctUntilChangedConditionalSubscriber<T, K> extends BasicFuseableConditionalSubscriber<T, T> { + + final Function<? super T, K> keySelector; + + final BiPredicate<? super K, ? super K> comparer; + + K last; + + boolean hasValue; + + DistinctUntilChangedConditionalSubscriber(ConditionalSubscriber<? super T> actual, + Function<? super T, K> keySelector, + BiPredicate<? super K, ? super K> comparer) { + super(actual); + this.keySelector = keySelector; + this.comparer = comparer; + } + + @Override + public void onNext(T t) { + if (!tryOnNext(t)) { + upstream.request(1); + } + } + + @Override + public boolean tryOnNext(T t) { + if (done) { + return false; + } + if (sourceMode != NONE) { + return downstream.tryOnNext(t); + } + + K key; + + try { + key = keySelector.apply(t); + if (hasValue) { + boolean equal = comparer.test(last, key); + last = key; + if (equal) { + return false; + } + } else { + hasValue = true; + last = key; + } + } catch (Throwable ex) { + fail(ex); + return true; + } + + downstream.onNext(t); + return true; + } + + @Override + public int requestFusion(int mode) { + return transitiveBoundaryFusion(mode); + } + + @Nullable + @Override + public T poll() throws Throwable { + for (;;) { + T v = qs.poll(); + if (v == null) { + return null; + } + K key = keySelector.apply(v); + if (!hasValue) { + hasValue = true; + last = key; + return v; + } + + if (!comparer.test(last, key)) { + last = key; + return v; + } + last = key; + if (sourceMode != SYNC) { + upstream.request(1); + } + } + } + + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableDoAfterNext.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableDoAfterNext.java new file mode 100644 index 0000000000..763e93a54a --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableDoAfterNext.java @@ -0,0 +1,137 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import org.reactivestreams.Subscriber; + +import io.reactivex.rxjava3.annotations.Nullable; +import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.functions.Consumer; +import io.reactivex.rxjava3.internal.subscribers.*; +import io.reactivex.rxjava3.operators.ConditionalSubscriber; + +/** + * Calls a consumer after pushing the current item to the downstream. + * <p>History: 2.0.1 - experimental + * @param <T> the value type + * @since 2.1 + */ +public final class FlowableDoAfterNext<T> extends AbstractFlowableWithUpstream<T, T> { + + final Consumer<? super T> onAfterNext; + + public FlowableDoAfterNext(Flowable<T> source, Consumer<? super T> onAfterNext) { + super(source); + this.onAfterNext = onAfterNext; + } + + @Override + protected void subscribeActual(Subscriber<? super T> s) { + if (s instanceof ConditionalSubscriber) { + source.subscribe(new DoAfterConditionalSubscriber<>((ConditionalSubscriber<? super T>) s, onAfterNext)); + } else { + source.subscribe(new DoAfterSubscriber<>(s, onAfterNext)); + } + } + + static final class DoAfterSubscriber<T> extends BasicFuseableSubscriber<T, T> { + + final Consumer<? super T> onAfterNext; + + DoAfterSubscriber(Subscriber<? super T> actual, Consumer<? super T> onAfterNext) { + super(actual); + this.onAfterNext = onAfterNext; + } + + @Override + public void onNext(T t) { + if (done) { + return; + } + downstream.onNext(t); + + if (sourceMode == NONE) { + try { + onAfterNext.accept(t); + } catch (Throwable ex) { + fail(ex); + } + } + } + + @Override + public int requestFusion(int mode) { + return transitiveBoundaryFusion(mode); + } + + @Nullable + @Override + public T poll() throws Throwable { + T v = qs.poll(); + if (v != null) { + onAfterNext.accept(v); + } + return v; + } + } + + static final class DoAfterConditionalSubscriber<T> extends BasicFuseableConditionalSubscriber<T, T> { + + final Consumer<? super T> onAfterNext; + + DoAfterConditionalSubscriber(ConditionalSubscriber<? super T> actual, Consumer<? super T> onAfterNext) { + super(actual); + this.onAfterNext = onAfterNext; + } + + @Override + public void onNext(T t) { + downstream.onNext(t); + + if (sourceMode == NONE) { + try { + onAfterNext.accept(t); + } catch (Throwable ex) { + fail(ex); + } + } + } + + @Override + public boolean tryOnNext(T t) { + boolean b = downstream.tryOnNext(t); + try { + onAfterNext.accept(t); + } catch (Throwable ex) { + fail(ex); + } + return b; + } + + @Override + public int requestFusion(int mode) { + return transitiveBoundaryFusion(mode); + } + + @Nullable + @Override + public T poll() throws Throwable { + T v = qs.poll(); + if (v != null) { + onAfterNext.accept(v); + } + return v; + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableDoFinally.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableDoFinally.java new file mode 100644 index 0000000000..71da853227 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableDoFinally.java @@ -0,0 +1,265 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.annotations.Nullable; +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.Action; +import io.reactivex.rxjava3.internal.subscriptions.*; +import io.reactivex.rxjava3.operators.ConditionalSubscriber; +import io.reactivex.rxjava3.operators.QueueSubscription; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +/** + * Execute an action after an onError, onComplete or a cancel event. + * <p>History: 2.0.1 - experimental + * @param <T> the value type + * @since 2.1 + */ +public final class FlowableDoFinally<T> extends AbstractFlowableWithUpstream<T, T> { + + final Action onFinally; + + public FlowableDoFinally(Flowable<T> source, Action onFinally) { + super(source); + this.onFinally = onFinally; + } + + @Override + protected void subscribeActual(Subscriber<? super T> s) { + if (s instanceof ConditionalSubscriber) { + source.subscribe(new DoFinallyConditionalSubscriber<>((ConditionalSubscriber<? super T>) s, onFinally)); + } else { + source.subscribe(new DoFinallySubscriber<>(s, onFinally)); + } + } + + static final class DoFinallySubscriber<T> extends BasicIntQueueSubscription<T> implements FlowableSubscriber<T> { + + private static final long serialVersionUID = 4109457741734051389L; + + final Subscriber<? super T> downstream; + + final Action onFinally; + + Subscription upstream; + + QueueSubscription<T> qs; + + boolean syncFused; + + DoFinallySubscriber(Subscriber<? super T> actual, Action onFinally) { + this.downstream = actual; + this.onFinally = onFinally; + } + + @SuppressWarnings("unchecked") + @Override + public void onSubscribe(Subscription s) { + if (SubscriptionHelper.validate(this.upstream, s)) { + this.upstream = s; + if (s instanceof QueueSubscription) { + this.qs = (QueueSubscription<T>)s; + } + + downstream.onSubscribe(this); + } + } + + @Override + public void onNext(T t) { + downstream.onNext(t); + } + + @Override + public void onError(Throwable t) { + downstream.onError(t); + runFinally(); + } + + @Override + public void onComplete() { + downstream.onComplete(); + runFinally(); + } + + @Override + public void cancel() { + upstream.cancel(); + runFinally(); + } + + @Override + public void request(long n) { + upstream.request(n); + } + + @Override + public int requestFusion(int mode) { + QueueSubscription<T> qs = this.qs; + if (qs != null && (mode & BOUNDARY) == 0) { + int m = qs.requestFusion(mode); + if (m != NONE) { + syncFused = m == SYNC; + } + return m; + } + return NONE; + } + + @Override + public void clear() { + qs.clear(); + } + + @Override + public boolean isEmpty() { + return qs.isEmpty(); + } + + @Nullable + @Override + public T poll() throws Throwable { + T v = qs.poll(); + if (v == null && syncFused) { + runFinally(); + } + return v; + } + + void runFinally() { + if (compareAndSet(0, 1)) { + try { + onFinally.run(); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + RxJavaPlugins.onError(ex); + } + } + } + } + + static final class DoFinallyConditionalSubscriber<T> extends BasicIntQueueSubscription<T> implements ConditionalSubscriber<T> { + + private static final long serialVersionUID = 4109457741734051389L; + + final ConditionalSubscriber<? super T> downstream; + + final Action onFinally; + + Subscription upstream; + + QueueSubscription<T> qs; + + boolean syncFused; + + DoFinallyConditionalSubscriber(ConditionalSubscriber<? super T> actual, Action onFinally) { + this.downstream = actual; + this.onFinally = onFinally; + } + + @SuppressWarnings("unchecked") + @Override + public void onSubscribe(Subscription s) { + if (SubscriptionHelper.validate(this.upstream, s)) { + this.upstream = s; + if (s instanceof QueueSubscription) { + this.qs = (QueueSubscription<T>)s; + } + + downstream.onSubscribe(this); + } + } + + @Override + public void onNext(T t) { + downstream.onNext(t); + } + + @Override + public boolean tryOnNext(T t) { + return downstream.tryOnNext(t); + } + + @Override + public void onError(Throwable t) { + downstream.onError(t); + runFinally(); + } + + @Override + public void onComplete() { + downstream.onComplete(); + runFinally(); + } + + @Override + public void cancel() { + upstream.cancel(); + runFinally(); + } + + @Override + public void request(long n) { + upstream.request(n); + } + + @Override + public int requestFusion(int mode) { + QueueSubscription<T> qs = this.qs; + if (qs != null && (mode & BOUNDARY) == 0) { + int m = qs.requestFusion(mode); + if (m != NONE) { + syncFused = m == SYNC; + } + return m; + } + return NONE; + } + + @Override + public void clear() { + qs.clear(); + } + + @Override + public boolean isEmpty() { + return qs.isEmpty(); + } + + @Nullable + @Override + public T poll() throws Throwable { + T v = qs.poll(); + if (v == null && syncFused) { + runFinally(); + } + return v; + } + + void runFinally() { + if (compareAndSet(0, 1)) { + try { + onFinally.run(); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + RxJavaPlugins.onError(ex); + } + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableDoOnEach.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableDoOnEach.java new file mode 100644 index 0000000000..3284a2e507 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableDoOnEach.java @@ -0,0 +1,352 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import org.reactivestreams.Subscriber; + +import io.reactivex.rxjava3.annotations.Nullable; +import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.subscribers.*; +import io.reactivex.rxjava3.internal.util.ExceptionHelper; +import io.reactivex.rxjava3.operators.ConditionalSubscriber; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +public final class FlowableDoOnEach<T> extends AbstractFlowableWithUpstream<T, T> { + final Consumer<? super T> onNext; + final Consumer<? super Throwable> onError; + final Action onComplete; + final Action onAfterTerminate; + + public FlowableDoOnEach(Flowable<T> source, Consumer<? super T> onNext, + Consumer<? super Throwable> onError, + Action onComplete, + Action onAfterTerminate) { + super(source); + this.onNext = onNext; + this.onError = onError; + this.onComplete = onComplete; + this.onAfterTerminate = onAfterTerminate; + } + + @Override + protected void subscribeActual(Subscriber<? super T> s) { + if (s instanceof ConditionalSubscriber) { + source.subscribe(new DoOnEachConditionalSubscriber<>( + (ConditionalSubscriber<? super T>) s, onNext, onError, onComplete, onAfterTerminate)); + } else { + source.subscribe(new DoOnEachSubscriber<>( + s, onNext, onError, onComplete, onAfterTerminate)); + } + } + + static final class DoOnEachSubscriber<T> extends BasicFuseableSubscriber<T, T> { + final Consumer<? super T> onNext; + final Consumer<? super Throwable> onError; + final Action onComplete; + final Action onAfterTerminate; + + DoOnEachSubscriber( + Subscriber<? super T> actual, + Consumer<? super T> onNext, + Consumer<? super Throwable> onError, + Action onComplete, + Action onAfterTerminate) { + super(actual); + this.onNext = onNext; + this.onError = onError; + this.onComplete = onComplete; + this.onAfterTerminate = onAfterTerminate; + } + + @Override + public void onNext(T t) { + if (done) { + return; + } + + if (sourceMode != NONE) { + downstream.onNext(null); + return; + } + + try { + onNext.accept(t); + } catch (Throwable e) { + fail(e); + return; + } + + downstream.onNext(t); + } + + @Override + public void onError(Throwable t) { + if (done) { + RxJavaPlugins.onError(t); + return; + } + done = true; + boolean relay = true; + try { + onError.accept(t); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + downstream.onError(new CompositeException(t, e)); + relay = false; + } + if (relay) { + downstream.onError(t); + } + + try { + onAfterTerminate.run(); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + RxJavaPlugins.onError(e); + } + } + + @Override + public void onComplete() { + if (done) { + return; + } + try { + onComplete.run(); + } catch (Throwable e) { + fail(e); + return; + } + + done = true; + downstream.onComplete(); + + try { + onAfterTerminate.run(); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + RxJavaPlugins.onError(e); + } + } + + @Override + public int requestFusion(int mode) { + return transitiveBoundaryFusion(mode); + } + + @Nullable + @Override + public T poll() throws Throwable { + T v; + + try { + v = qs.poll(); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + try { + onError.accept(ex); + } catch (Throwable exc) { + Exceptions.throwIfFatal(exc); + throw new CompositeException(ex, exc); + } + throw ExceptionHelper.<Exception>throwIfThrowable(ex); + } + + if (v != null) { + try { + try { + onNext.accept(v); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + try { + onError.accept(ex); + } catch (Throwable exc) { + Exceptions.throwIfFatal(exc); + throw new CompositeException(ex, exc); + } + throw ExceptionHelper.<Exception>throwIfThrowable(ex); + } + } finally { + onAfterTerminate.run(); + } + } else { + if (sourceMode == SYNC) { + onComplete.run(); + + onAfterTerminate.run(); + } + } + return v; + } + } + + static final class DoOnEachConditionalSubscriber<T> extends BasicFuseableConditionalSubscriber<T, T> { + final Consumer<? super T> onNext; + final Consumer<? super Throwable> onError; + final Action onComplete; + final Action onAfterTerminate; + + DoOnEachConditionalSubscriber( + ConditionalSubscriber<? super T> actual, + Consumer<? super T> onNext, + Consumer<? super Throwable> onError, + Action onComplete, + Action onAfterTerminate) { + super(actual); + this.onNext = onNext; + this.onError = onError; + this.onComplete = onComplete; + this.onAfterTerminate = onAfterTerminate; + } + + @Override + public void onNext(T t) { + if (done) { + return; + } + + if (sourceMode != NONE) { + downstream.onNext(null); + return; + } + + try { + onNext.accept(t); + } catch (Throwable e) { + fail(e); + return; + } + + downstream.onNext(t); + } + + @Override + public boolean tryOnNext(T t) { + if (done) { + return false; + } + + try { + onNext.accept(t); + } catch (Throwable e) { + fail(e); + return false; + } + + return downstream.tryOnNext(t); + } + + @Override + public void onError(Throwable t) { + if (done) { + RxJavaPlugins.onError(t); + return; + } + done = true; + boolean relay = true; + try { + onError.accept(t); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + downstream.onError(new CompositeException(t, e)); + relay = false; + } + if (relay) { + downstream.onError(t); + } + + try { + onAfterTerminate.run(); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + RxJavaPlugins.onError(e); + } + } + + @Override + public void onComplete() { + if (done) { + return; + } + try { + onComplete.run(); + } catch (Throwable e) { + fail(e); + return; + } + + done = true; + downstream.onComplete(); + + try { + onAfterTerminate.run(); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + RxJavaPlugins.onError(e); + } + } + + @Override + public int requestFusion(int mode) { + return transitiveBoundaryFusion(mode); + } + + @Nullable + @Override + public T poll() throws Throwable { + T v; + + try { + v = qs.poll(); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + try { + onError.accept(ex); + } catch (Throwable exc) { + Exceptions.throwIfFatal(exc); + throw new CompositeException(ex, exc); + } + throw ExceptionHelper.<Exception>throwIfThrowable(ex); + } + + if (v != null) { + try { + try { + onNext.accept(v); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + try { + onError.accept(ex); + } catch (Throwable exc) { + Exceptions.throwIfFatal(exc); + throw new CompositeException(ex, exc); + } + throw ExceptionHelper.<Exception>throwIfThrowable(ex); + } + } finally { + onAfterTerminate.run(); + } + } else { + if (sourceMode == SYNC) { + onComplete.run(); + + onAfterTerminate.run(); + } + } + return v; + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableDoOnLifecycle.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableDoOnLifecycle.java new file mode 100644 index 0000000000..8b56c38c70 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableDoOnLifecycle.java @@ -0,0 +1,125 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.subscriptions.*; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +public final class FlowableDoOnLifecycle<T> extends AbstractFlowableWithUpstream<T, T> { + private final Consumer<? super Subscription> onSubscribe; + private final LongConsumer onRequest; + private final Action onCancel; + + public FlowableDoOnLifecycle(Flowable<T> source, Consumer<? super Subscription> onSubscribe, + LongConsumer onRequest, Action onCancel) { + super(source); + this.onSubscribe = onSubscribe; + this.onRequest = onRequest; + this.onCancel = onCancel; + } + + @Override + protected void subscribeActual(Subscriber<? super T> s) { + source.subscribe(new SubscriptionLambdaSubscriber<>(s, onSubscribe, onRequest, onCancel)); + } + + static final class SubscriptionLambdaSubscriber<T> implements FlowableSubscriber<T>, Subscription { + final Subscriber<? super T> downstream; + final Consumer<? super Subscription> onSubscribe; + final LongConsumer onRequest; + final Action onCancel; + + Subscription upstream; + + SubscriptionLambdaSubscriber(Subscriber<? super T> actual, + Consumer<? super Subscription> onSubscribe, + LongConsumer onRequest, + Action onCancel) { + this.downstream = actual; + this.onSubscribe = onSubscribe; + this.onCancel = onCancel; + this.onRequest = onRequest; + } + + @Override + public void onSubscribe(Subscription s) { + // this way, multiple calls to onSubscribe can show up in tests that use doOnSubscribe to validate behavior + try { + onSubscribe.accept(s); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + s.cancel(); + this.upstream = SubscriptionHelper.CANCELLED; + EmptySubscription.error(e, downstream); + return; + } + if (SubscriptionHelper.validate(this.upstream, s)) { + this.upstream = s; + downstream.onSubscribe(this); + } + } + + @Override + public void onNext(T t) { + downstream.onNext(t); + } + + @Override + public void onError(Throwable t) { + if (upstream != SubscriptionHelper.CANCELLED) { + downstream.onError(t); + } else { + RxJavaPlugins.onError(t); + } + } + + @Override + public void onComplete() { + if (upstream != SubscriptionHelper.CANCELLED) { + downstream.onComplete(); + } + } + + @Override + public void request(long n) { + try { + onRequest.accept(n); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + RxJavaPlugins.onError(e); + } + upstream.request(n); + } + + @Override + public void cancel() { + Subscription s = upstream; + if (s != SubscriptionHelper.CANCELLED) { + upstream = SubscriptionHelper.CANCELLED; + try { + onCancel.run(); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + RxJavaPlugins.onError(e); + } + s.cancel(); + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableElementAt.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableElementAt.java new file mode 100644 index 0000000000..eecd3d1c17 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableElementAt.java @@ -0,0 +1,119 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import java.util.NoSuchElementException; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.internal.subscriptions.*; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +public final class FlowableElementAt<T> extends AbstractFlowableWithUpstream<T, T> { + final long index; + final T defaultValue; + final boolean errorOnFewer; + + public FlowableElementAt(Flowable<T> source, long index, T defaultValue, boolean errorOnFewer) { + super(source); + this.index = index; + this.defaultValue = defaultValue; + this.errorOnFewer = errorOnFewer; + } + + @Override + protected void subscribeActual(Subscriber<? super T> s) { + source.subscribe(new ElementAtSubscriber<>(s, index, defaultValue, errorOnFewer)); + } + + static final class ElementAtSubscriber<T> extends DeferredScalarSubscription<T> implements FlowableSubscriber<T> { + + private static final long serialVersionUID = 4066607327284737757L; + + final long index; + final T defaultValue; + final boolean errorOnFewer; + + Subscription upstream; + + long count; + + boolean done; + + ElementAtSubscriber(Subscriber<? super T> actual, long index, T defaultValue, boolean errorOnFewer) { + super(actual); + this.index = index; + this.defaultValue = defaultValue; + this.errorOnFewer = errorOnFewer; + } + + @Override + public void onSubscribe(Subscription s) { + if (SubscriptionHelper.validate(this.upstream, s)) { + this.upstream = s; + downstream.onSubscribe(this); + s.request(Long.MAX_VALUE); + } + } + + @Override + public void onNext(T t) { + if (done) { + return; + } + long c = count; + if (c == index) { + done = true; + upstream.cancel(); + complete(t); + return; + } + count = c + 1; + } + + @Override + public void onError(Throwable t) { + if (done) { + RxJavaPlugins.onError(t); + return; + } + done = true; + downstream.onError(t); + } + + @Override + public void onComplete() { + if (!done) { + done = true; + T v = defaultValue; + if (v == null) { + if (errorOnFewer) { + downstream.onError(new NoSuchElementException()); + } else { + downstream.onComplete(); + } + } else { + complete(v); + } + } + } + + @Override + public void cancel() { + super.cancel(); + upstream.cancel(); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableElementAtMaybe.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableElementAtMaybe.java new file mode 100644 index 0000000000..f62439c577 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableElementAtMaybe.java @@ -0,0 +1,118 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import org.reactivestreams.Subscription; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.fuseable.FuseToFlowable; +import io.reactivex.rxjava3.internal.subscriptions.SubscriptionHelper; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +public final class FlowableElementAtMaybe<T> extends Maybe<T> implements FuseToFlowable<T> { + final Flowable<T> source; + + final long index; + + public FlowableElementAtMaybe(Flowable<T> source, long index) { + this.source = source; + this.index = index; + } + + @Override + protected void subscribeActual(MaybeObserver<? super T> observer) { + source.subscribe(new ElementAtSubscriber<>(observer, index)); + } + + @Override + public Flowable<T> fuseToFlowable() { + return RxJavaPlugins.onAssembly(new FlowableElementAt<>(source, index, null, false)); + } + + static final class ElementAtSubscriber<T> implements FlowableSubscriber<T>, Disposable { + + final MaybeObserver<? super T> downstream; + + final long index; + + Subscription upstream; + + long count; + + boolean done; + + ElementAtSubscriber(MaybeObserver<? super T> actual, long index) { + this.downstream = actual; + this.index = index; + } + + @Override + public void onSubscribe(Subscription s) { + if (SubscriptionHelper.validate(this.upstream, s)) { + this.upstream = s; + downstream.onSubscribe(this); + s.request(index + 1); + } + } + + @Override + public void onNext(T t) { + if (done) { + return; + } + long c = count; + if (c == index) { + done = true; + upstream.cancel(); + upstream = SubscriptionHelper.CANCELLED; + downstream.onSuccess(t); + return; + } + count = c + 1; + } + + @Override + public void onError(Throwable t) { + if (done) { + RxJavaPlugins.onError(t); + return; + } + done = true; + upstream = SubscriptionHelper.CANCELLED; + downstream.onError(t); + } + + @Override + public void onComplete() { + upstream = SubscriptionHelper.CANCELLED; + if (!done) { + done = true; + downstream.onComplete(); + } + } + + @Override + public void dispose() { + upstream.cancel(); + upstream = SubscriptionHelper.CANCELLED; + } + + @Override + public boolean isDisposed() { + return upstream == SubscriptionHelper.CANCELLED; + } + + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableElementAtMaybePublisher.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableElementAtMaybePublisher.java new file mode 100644 index 0000000000..b6b16a3e40 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableElementAtMaybePublisher.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import org.reactivestreams.Publisher; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.internal.operators.flowable.FlowableElementAtMaybe.ElementAtSubscriber; + +/** + * Emits the indexth element from a Publisher as a Maybe. + * + * @param <T> the element type of the source + * @since 3.0.0 + */ +public final class FlowableElementAtMaybePublisher<T> extends Maybe<T> { + + final Publisher<T> source; + + final long index; + + public FlowableElementAtMaybePublisher(Publisher<T> source, long index) { + this.source = source; + this.index = index; + } + + @Override + protected void subscribeActual(MaybeObserver<? super T> observer) { + source.subscribe(new ElementAtSubscriber<>(observer, index)); + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableElementAtSingle.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableElementAtSingle.java new file mode 100644 index 0000000000..95b32bde6d --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableElementAtSingle.java @@ -0,0 +1,131 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import java.util.NoSuchElementException; + +import org.reactivestreams.Subscription; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.fuseable.FuseToFlowable; +import io.reactivex.rxjava3.internal.subscriptions.SubscriptionHelper; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +public final class FlowableElementAtSingle<T> extends Single<T> implements FuseToFlowable<T> { + final Flowable<T> source; + + final long index; + + final T defaultValue; + + public FlowableElementAtSingle(Flowable<T> source, long index, T defaultValue) { + this.source = source; + this.index = index; + this.defaultValue = defaultValue; + } + + @Override + protected void subscribeActual(SingleObserver<? super T> observer) { + source.subscribe(new ElementAtSubscriber<>(observer, index, defaultValue)); + } + + @Override + public Flowable<T> fuseToFlowable() { + return RxJavaPlugins.onAssembly(new FlowableElementAt<>(source, index, defaultValue, true)); + } + + static final class ElementAtSubscriber<T> implements FlowableSubscriber<T>, Disposable { + + final SingleObserver<? super T> downstream; + + final long index; + final T defaultValue; + + Subscription upstream; + + long count; + + boolean done; + + ElementAtSubscriber(SingleObserver<? super T> actual, long index, T defaultValue) { + this.downstream = actual; + this.index = index; + this.defaultValue = defaultValue; + } + + @Override + public void onSubscribe(Subscription s) { + if (SubscriptionHelper.validate(this.upstream, s)) { + this.upstream = s; + downstream.onSubscribe(this); + s.request(index + 1); + } + } + + @Override + public void onNext(T t) { + if (done) { + return; + } + long c = count; + if (c == index) { + done = true; + upstream.cancel(); + upstream = SubscriptionHelper.CANCELLED; + downstream.onSuccess(t); + return; + } + count = c + 1; + } + + @Override + public void onError(Throwable t) { + if (done) { + RxJavaPlugins.onError(t); + return; + } + done = true; + upstream = SubscriptionHelper.CANCELLED; + downstream.onError(t); + } + + @Override + public void onComplete() { + upstream = SubscriptionHelper.CANCELLED; + if (!done) { + done = true; + + T v = defaultValue; + + if (v != null) { + downstream.onSuccess(v); + } else { + downstream.onError(new NoSuchElementException()); + } + } + } + + @Override + public void dispose() { + upstream.cancel(); + upstream = SubscriptionHelper.CANCELLED; + } + + @Override + public boolean isDisposed() { + return upstream == SubscriptionHelper.CANCELLED; + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableEmpty.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableEmpty.java new file mode 100644 index 0000000000..3606ac57d6 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableEmpty.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import org.reactivestreams.Subscriber; + +import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.internal.subscriptions.EmptySubscription; +import io.reactivex.rxjava3.operators.ScalarSupplier; + +/** + * A source Flowable that signals an onSubscribe() + onComplete() only. + */ +public final class FlowableEmpty extends Flowable<Object> implements ScalarSupplier<Object> { + + public static final Flowable<Object> INSTANCE = new FlowableEmpty(); + + private FlowableEmpty() { + } + + @Override + public void subscribeActual(Subscriber<? super Object> s) { + EmptySubscription.complete(s); + } + + @Override + public Object get() { + return null; // null scalar is interpreted as being empty + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableError.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableError.java new file mode 100644 index 0000000000..190eb53c6b --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableError.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import org.reactivestreams.Subscriber; + +import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.Supplier; +import io.reactivex.rxjava3.internal.subscriptions.EmptySubscription; +import io.reactivex.rxjava3.internal.util.ExceptionHelper; + +public final class FlowableError<T> extends Flowable<T> { + final Supplier<? extends Throwable> errorSupplier; + public FlowableError(Supplier<? extends Throwable> errorSupplier) { + this.errorSupplier = errorSupplier; + } + + @Override + public void subscribeActual(Subscriber<? super T> s) { + Throwable error; + try { + error = ExceptionHelper.nullCheck(errorSupplier.get(), "Callable returned a null Throwable."); + } catch (Throwable t) { + Exceptions.throwIfFatal(t); + error = t; + } + EmptySubscription.error(error, s); + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableFilter.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableFilter.java new file mode 100644 index 0000000000..74a4413e36 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableFilter.java @@ -0,0 +1,170 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import org.reactivestreams.Subscriber; + +import io.reactivex.rxjava3.annotations.Nullable; +import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.functions.Predicate; +import io.reactivex.rxjava3.internal.subscribers.*; +import io.reactivex.rxjava3.operators.ConditionalSubscriber; +import io.reactivex.rxjava3.operators.QueueSubscription; + +public final class FlowableFilter<T> extends AbstractFlowableWithUpstream<T, T> { + final Predicate<? super T> predicate; + public FlowableFilter(Flowable<T> source, Predicate<? super T> predicate) { + super(source); + this.predicate = predicate; + } + + @Override + protected void subscribeActual(Subscriber<? super T> s) { + if (s instanceof ConditionalSubscriber) { + source.subscribe(new FilterConditionalSubscriber<>( + (ConditionalSubscriber<? super T>) s, predicate)); + } else { + source.subscribe(new FilterSubscriber<>(s, predicate)); + } + } + + static final class FilterSubscriber<T> extends BasicFuseableSubscriber<T, T> + implements ConditionalSubscriber<T> { + final Predicate<? super T> filter; + + FilterSubscriber(Subscriber<? super T> actual, Predicate<? super T> filter) { + super(actual); + this.filter = filter; + } + + @Override + public void onNext(T t) { + if (!tryOnNext(t)) { + upstream.request(1); + } + } + + @Override + public boolean tryOnNext(T t) { + if (done) { + return false; + } + if (sourceMode != NONE) { + downstream.onNext(null); + return true; + } + boolean b; + try { + b = filter.test(t); + } catch (Throwable e) { + fail(e); + return true; + } + if (b) { + downstream.onNext(t); + } + return b; + } + + @Override + public int requestFusion(int mode) { + return transitiveBoundaryFusion(mode); + } + + @Nullable + @Override + public T poll() throws Throwable { + QueueSubscription<T> qs = this.qs; + Predicate<? super T> f = filter; + + for (;;) { + T t = qs.poll(); + if (t == null) { + return null; + } + + if (f.test(t)) { + return t; + } + + if (sourceMode == ASYNC) { + qs.request(1); + } + } + } + } + + static final class FilterConditionalSubscriber<T> extends BasicFuseableConditionalSubscriber<T, T> { + final Predicate<? super T> filter; + + FilterConditionalSubscriber(ConditionalSubscriber<? super T> actual, Predicate<? super T> filter) { + super(actual); + this.filter = filter; + } + + @Override + public void onNext(T t) { + if (!tryOnNext(t)) { + upstream.request(1); + } + } + + @Override + public boolean tryOnNext(T t) { + if (done) { + return false; + } + + if (sourceMode != NONE) { + return downstream.tryOnNext(null); + } + + boolean b; + try { + b = filter.test(t); + } catch (Throwable e) { + fail(e); + return true; + } + return b && downstream.tryOnNext(t); + } + + @Override + public int requestFusion(int mode) { + return transitiveBoundaryFusion(mode); + } + + @Nullable + @Override + public T poll() throws Throwable { + QueueSubscription<T> qs = this.qs; + Predicate<? super T> f = filter; + + for (;;) { + T t = qs.poll(); + if (t == null) { + return null; + } + + if (f.test(t)) { + return t; + } + + if (sourceMode == ASYNC) { + qs.request(1); + } + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableFlatMap.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableFlatMap.java new file mode 100644 index 0000000000..c250d3b165 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableFlatMap.java @@ -0,0 +1,667 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import java.util.Objects; +import java.util.concurrent.atomic.*; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.subscriptions.SubscriptionHelper; +import io.reactivex.rxjava3.internal.util.*; +import io.reactivex.rxjava3.operators.*; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +public final class FlowableFlatMap<T, U> extends AbstractFlowableWithUpstream<T, U> { + final Function<? super T, ? extends Publisher<? extends U>> mapper; + final boolean delayErrors; + final int maxConcurrency; + final int bufferSize; + + public FlowableFlatMap(Flowable<T> source, + Function<? super T, ? extends Publisher<? extends U>> mapper, + boolean delayErrors, int maxConcurrency, int bufferSize) { + super(source); + this.mapper = mapper; + this.delayErrors = delayErrors; + this.maxConcurrency = maxConcurrency; + this.bufferSize = bufferSize; + } + + @Override + protected void subscribeActual(Subscriber<? super U> s) { + if (FlowableScalarXMap.tryScalarXMapSubscribe(source, s, mapper)) { + return; + } + source.subscribe(subscribe(s, mapper, delayErrors, maxConcurrency, bufferSize)); + } + + public static <T, U> FlowableSubscriber<T> subscribe(Subscriber<? super U> s, + Function<? super T, ? extends Publisher<? extends U>> mapper, + boolean delayErrors, int maxConcurrency, int bufferSize) { + return new MergeSubscriber<>(s, mapper, delayErrors, maxConcurrency, bufferSize); + } + + static final class MergeSubscriber<T, U> extends AtomicInteger implements FlowableSubscriber<T>, Subscription { + + private static final long serialVersionUID = -2117620485640801370L; + + final Subscriber<? super U> downstream; + final Function<? super T, ? extends Publisher<? extends U>> mapper; + final boolean delayErrors; + final int maxConcurrency; + final int bufferSize; + + volatile SimplePlainQueue<U> queue; + + volatile boolean done; + + final AtomicThrowable errors = new AtomicThrowable(); + + volatile boolean cancelled; + + final AtomicReference<InnerSubscriber<?, ?>[]> subscribers = new AtomicReference<>(); + + static final InnerSubscriber<?, ?>[] EMPTY = new InnerSubscriber<?, ?>[0]; + + static final InnerSubscriber<?, ?>[] CANCELLED = new InnerSubscriber<?, ?>[0]; + + final AtomicLong requested = new AtomicLong(); + + Subscription upstream; + + long uniqueId; + long lastId; + int lastIndex; + + int scalarEmitted; + final int scalarLimit; + + MergeSubscriber(Subscriber<? super U> actual, Function<? super T, ? extends Publisher<? extends U>> mapper, + boolean delayErrors, int maxConcurrency, int bufferSize) { + this.downstream = actual; + this.mapper = mapper; + this.delayErrors = delayErrors; + this.maxConcurrency = maxConcurrency; + this.bufferSize = bufferSize; + this.scalarLimit = Math.max(1, maxConcurrency >> 1); + subscribers.lazySet(EMPTY); + } + + @Override + public void onSubscribe(Subscription s) { + if (SubscriptionHelper.validate(this.upstream, s)) { + this.upstream = s; + downstream.onSubscribe(this); + if (!cancelled) { + if (maxConcurrency == Integer.MAX_VALUE) { + s.request(Long.MAX_VALUE); + } else { + s.request(maxConcurrency); + } + } + } + } + + @SuppressWarnings("unchecked") + @Override + public void onNext(T t) { + // safeguard against misbehaving sources + if (done) { + return; + } + Publisher<? extends U> p; + try { + p = Objects.requireNonNull(mapper.apply(t), "The mapper returned a null Publisher"); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + upstream.cancel(); + onError(e); + return; + } + if (p instanceof Supplier) { + U u; + + try { + u = ((Supplier<U>)p).get(); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + errors.tryAddThrowableOrReport(ex); + drain(); + return; + } + + if (u != null) { + tryEmitScalar(u); + } else { + if (maxConcurrency != Integer.MAX_VALUE + && !cancelled && ++scalarEmitted == scalarLimit) { + scalarEmitted = 0; + upstream.request(scalarLimit); + } + } + } else { + InnerSubscriber<T, U> inner = new InnerSubscriber<>(this, bufferSize, uniqueId++); + if (addInner(inner)) { + p.subscribe(inner); + } + } + } + + boolean addInner(InnerSubscriber<T, U> inner) { + for (;;) { + InnerSubscriber<?, ?>[] a = subscribers.get(); + if (a == CANCELLED) { + inner.dispose(); + return false; + } + int n = a.length; + InnerSubscriber<?, ?>[] b = new InnerSubscriber[n + 1]; + System.arraycopy(a, 0, b, 0, n); + b[n] = inner; + if (subscribers.compareAndSet(a, b)) { + return true; + } + } + } + + void removeInner(InnerSubscriber<T, U> inner) { + for (;;) { + InnerSubscriber<?, ?>[] a = subscribers.get(); + int n = a.length; + if (n == 0) { + return; + } + int j = -1; + for (int i = 0; i < n; i++) { + if (a[i] == inner) { + j = i; + break; + } + } + if (j < 0) { + return; + } + InnerSubscriber<?, ?>[] b; + if (n == 1) { + b = EMPTY; + } else { + b = new InnerSubscriber<?, ?>[n - 1]; + System.arraycopy(a, 0, b, 0, j); + System.arraycopy(a, j + 1, b, j, n - j - 1); + } + if (subscribers.compareAndSet(a, b)) { + return; + } + } + } + + SimpleQueue<U> getMainQueue() { + SimplePlainQueue<U> q = queue; + if (q == null) { + if (maxConcurrency == Integer.MAX_VALUE) { + q = new SpscLinkedArrayQueue<>(bufferSize); + } else { + q = new SpscArrayQueue<>(maxConcurrency); + } + queue = q; + } + return q; + } + + void tryEmitScalar(U value) { + if (get() == 0 && compareAndSet(0, 1)) { + long r = requested.get(); + SimpleQueue<U> q = queue; + if (r != 0L && (q == null || q.isEmpty())) { + downstream.onNext(value); + if (r != Long.MAX_VALUE) { + requested.decrementAndGet(); + } + if (maxConcurrency != Integer.MAX_VALUE + && !cancelled && ++scalarEmitted == scalarLimit) { + scalarEmitted = 0; + upstream.request(scalarLimit); + } + } else { + if (q == null) { + q = getMainQueue(); + } + if (!q.offer(value)) { + onError(new QueueOverflowException()); + } + } + if (decrementAndGet() == 0) { + return; + } + } else { + SimpleQueue<U> q = getMainQueue(); + if (!q.offer(value)) { + onError(new QueueOverflowException()); + return; + } + if (getAndIncrement() != 0) { + return; + } + } + drainLoop(); + } + + void tryEmit(U value, InnerSubscriber<T, U> inner) { + if (get() == 0 && compareAndSet(0, 1)) { + long r = requested.get(); + SimpleQueue<U> q = inner.queue; + if (r != 0L && (q == null || q.isEmpty())) { + downstream.onNext(value); + if (r != Long.MAX_VALUE) { + requested.decrementAndGet(); + } + inner.requestMore(1); + } else { + if (q == null) { + q = new SpscArrayQueue<>(bufferSize); + inner.queue = q; + } + if (!q.offer(value)) { + onError(new QueueOverflowException()); + } + } + if (decrementAndGet() == 0) { + return; + } + } else { + SimpleQueue<U> q = inner.queue; + if (q == null) { + q = new SpscArrayQueue<>(bufferSize); + inner.queue = q; + } + if (!q.offer(value)) { + onError(new QueueOverflowException()); + return; + } + if (getAndIncrement() != 0) { + return; + } + } + drainLoop(); + } + + @Override + public void onError(Throwable t) { + // safeguard against misbehaving sources + if (done) { + RxJavaPlugins.onError(t); + return; + } + if (errors.tryAddThrowableOrReport(t)) { + done = true; + if (!delayErrors) { + for (InnerSubscriber<?, ?> a : subscribers.getAndSet(CANCELLED)) { + a.dispose(); + } + } + drain(); + } + } + + @Override + public void onComplete() { + // safeguard against misbehaving sources + if (done) { + return; + } + done = true; + drain(); + } + + @Override + public void request(long n) { + if (SubscriptionHelper.validate(n)) { + BackpressureHelper.add(requested, n); + drain(); + } + } + + @Override + public void cancel() { + if (!cancelled) { + cancelled = true; + upstream.cancel(); + disposeAll(); + if (getAndIncrement() == 0) { + SimpleQueue<U> q = queue; + if (q != null) { + q.clear(); + } + } + } + } + + void drain() { + if (getAndIncrement() == 0) { + drainLoop(); + } + } + + void drainLoop() { + final Subscriber<? super U> child = this.downstream; + int missed = 1; + for (;;) { + if (checkTerminate()) { + return; + } + SimplePlainQueue<U> svq = queue; + + long r = requested.get(); + boolean unbounded = r == Long.MAX_VALUE; + + long replenishMain = 0; + + if (svq != null) { + long scalarEmission = 0; + U o = null; + while (r != 0L) { + o = svq.poll(); + + if (checkTerminate()) { + return; + } + if (o == null) { + break; + } + + child.onNext(o); + + replenishMain++; + scalarEmission++; + r--; + } + if (scalarEmission != 0L) { + if (unbounded) { + r = Long.MAX_VALUE; + } else { + r = requested.addAndGet(-scalarEmission); + } + } + } + + boolean d = done; + svq = queue; + InnerSubscriber<?, ?>[] inner = subscribers.get(); + int n = inner.length; + + if (d && (svq == null || svq.isEmpty()) && n == 0) { + errors.tryTerminateConsumer(downstream); + return; + } + + boolean innerCompleted = false; + if (n != 0) { + long startId = lastId; + int index = lastIndex; + + if (n <= index || inner[index].id != startId) { + if (n <= index) { + index = 0; + } + int j = index; + for (int i = 0; i < n; i++) { + if (inner[j].id == startId) { + break; + } + j++; + if (j == n) { + j = 0; + } + } + index = j; + lastIndex = j; + lastId = inner[j].id; + } + + int j = index; + sourceLoop: + for (int i = 0; i < n; i++) { + if (checkTerminate()) { + return; + } + + @SuppressWarnings("unchecked") + InnerSubscriber<T, U> is = (InnerSubscriber<T, U>)inner[j]; + + U o = null; + for (;;) { + SimpleQueue<U> q = is.queue; + if (q == null) { + break; + } + long produced = 0; + while (r != 0L) { + if (checkTerminate()) { + return; + } + + try { + o = q.poll(); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + is.dispose(); + errors.tryAddThrowableOrReport(ex); + if (!delayErrors) { + upstream.cancel(); + } + if (checkTerminate()) { + return; + } + removeInner(is); + innerCompleted = true; + i++; + continue sourceLoop; + } + if (o == null) { + break; + } + + child.onNext(o); + + r--; + produced++; + } + if (produced != 0L) { + if (!unbounded) { + r = requested.addAndGet(-produced); + } else { + r = Long.MAX_VALUE; + } + is.requestMore(produced); + } + if (r == 0 || o == null) { + break; + } + } + boolean innerDone = is.done; + SimpleQueue<U> innerQueue = is.queue; + if (innerDone && (innerQueue == null || innerQueue.isEmpty())) { + removeInner(is); + if (checkTerminate()) { + return; + } + replenishMain++; + innerCompleted = true; + } + if (r == 0L) { + break; + } + + j++; + if (j == n) { + j = 0; + } + } + lastIndex = j; + lastId = inner[j].id; + } + + if (replenishMain != 0L && !cancelled) { + upstream.request(replenishMain); + } + if (innerCompleted) { + continue; + } + missed = addAndGet(-missed); + if (missed == 0) { + break; + } + } + } + + boolean checkTerminate() { + if (cancelled) { + clearScalarQueue(); + return true; + } + if (!delayErrors && errors.get() != null) { + clearScalarQueue(); + errors.tryTerminateConsumer(downstream); + return true; + } + return false; + } + + void clearScalarQueue() { + SimpleQueue<U> q = queue; + if (q != null) { + q.clear(); + } + } + + void disposeAll() { + InnerSubscriber<?, ?>[] a = subscribers.getAndSet(CANCELLED); + if (a != CANCELLED) { + for (InnerSubscriber<?, ?> inner : a) { + inner.dispose(); + } + errors.tryTerminateAndReport(); + } + } + + void innerError(InnerSubscriber<T, U> inner, Throwable t) { + if (errors.tryAddThrowableOrReport(t)) { + inner.done = true; + if (!delayErrors) { + upstream.cancel(); + for (InnerSubscriber<?, ?> a : subscribers.getAndSet(CANCELLED)) { + a.dispose(); + } + } + drain(); + } + } + } + + static final class InnerSubscriber<T, U> extends AtomicReference<Subscription> + implements FlowableSubscriber<U>, Disposable { + + private static final long serialVersionUID = -4606175640614850599L; + final long id; + final MergeSubscriber<T, U> parent; + final int limit; + final int bufferSize; + + volatile boolean done; + volatile SimpleQueue<U> queue; + long produced; + int fusionMode; + + InnerSubscriber(MergeSubscriber<T, U> parent, int bufferSize, long id) { + this.id = id; + this.parent = parent; + this.bufferSize = bufferSize; + this.limit = bufferSize >> 2; + } + + @Override + public void onSubscribe(Subscription s) { + if (SubscriptionHelper.setOnce(this, s)) { + + if (s instanceof QueueSubscription) { + @SuppressWarnings("unchecked") + QueueSubscription<U> qs = (QueueSubscription<U>) s; + int m = qs.requestFusion(QueueSubscription.ANY | QueueSubscription.BOUNDARY); + if (m == QueueSubscription.SYNC) { + fusionMode = m; + queue = qs; + done = true; + parent.drain(); + return; + } + if (m == QueueSubscription.ASYNC) { + fusionMode = m; + queue = qs; + } + + } + + s.request(bufferSize); + } + } + + @Override + public void onNext(U t) { + if (fusionMode != QueueSubscription.ASYNC) { + parent.tryEmit(t, this); + } else { + parent.drain(); + } + } + + @Override + public void onError(Throwable t) { + lazySet(SubscriptionHelper.CANCELLED); + parent.innerError(this, t); + } + + @Override + public void onComplete() { + done = true; + parent.drain(); + } + + void requestMore(long n) { + if (fusionMode != QueueSubscription.SYNC) { + long p = produced + n; + if (p >= limit) { + produced = 0; + get().request(p); + } else { + produced = p; + } + } + } + + @Override + public void dispose() { + SubscriptionHelper.cancel(this); + } + + @Override + public boolean isDisposed() { + return get() == SubscriptionHelper.CANCELLED; + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableFlatMapCompletable.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableFlatMapCompletable.java new file mode 100644 index 0000000000..b69a6e2895 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableFlatMapCompletable.java @@ -0,0 +1,230 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import java.util.Objects; +import java.util.concurrent.atomic.AtomicReference; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.annotations.Nullable; +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.*; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; +import io.reactivex.rxjava3.internal.subscriptions.*; +import io.reactivex.rxjava3.internal.util.AtomicThrowable; + +/** + * Maps a sequence of values into CompletableSources and awaits their termination. + * @param <T> the value type + */ +public final class FlowableFlatMapCompletable<T> extends AbstractFlowableWithUpstream<T, T> { + + final Function<? super T, ? extends CompletableSource> mapper; + + final int maxConcurrency; + + final boolean delayErrors; + + public FlowableFlatMapCompletable(Flowable<T> source, + Function<? super T, ? extends CompletableSource> mapper, boolean delayErrors, + int maxConcurrency) { + super(source); + this.mapper = mapper; + this.delayErrors = delayErrors; + this.maxConcurrency = maxConcurrency; + } + + @Override + protected void subscribeActual(Subscriber<? super T> subscriber) { + source.subscribe(new FlatMapCompletableMainSubscriber<>(subscriber, mapper, delayErrors, maxConcurrency)); + } + + static final class FlatMapCompletableMainSubscriber<T> extends BasicIntQueueSubscription<T> + implements FlowableSubscriber<T> { + private static final long serialVersionUID = 8443155186132538303L; + + final Subscriber<? super T> downstream; + + final AtomicThrowable errors; + + final Function<? super T, ? extends CompletableSource> mapper; + + final boolean delayErrors; + + final CompositeDisposable set; + + final int maxConcurrency; + + Subscription upstream; + + volatile boolean cancelled; + + FlatMapCompletableMainSubscriber(Subscriber<? super T> subscriber, + Function<? super T, ? extends CompletableSource> mapper, boolean delayErrors, + int maxConcurrency) { + this.downstream = subscriber; + this.mapper = mapper; + this.delayErrors = delayErrors; + this.errors = new AtomicThrowable(); + this.set = new CompositeDisposable(); + this.maxConcurrency = maxConcurrency; + this.lazySet(1); + } + + @Override + public void onSubscribe(Subscription s) { + if (SubscriptionHelper.validate(this.upstream, s)) { + this.upstream = s; + + downstream.onSubscribe(this); + + int m = maxConcurrency; + if (m == Integer.MAX_VALUE) { + s.request(Long.MAX_VALUE); + } else { + s.request(m); + } + } + } + + @Override + public void onNext(T value) { + CompletableSource cs; + + try { + cs = Objects.requireNonNull(mapper.apply(value), "The mapper returned a null CompletableSource"); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + upstream.cancel(); + onError(ex); + return; + } + + getAndIncrement(); + + InnerConsumer inner = new InnerConsumer(); + + if (!cancelled && set.add(inner)) { + cs.subscribe(inner); + } + } + + @Override + public void onError(Throwable e) { + if (errors.tryAddThrowableOrReport(e)) { + if (delayErrors) { + if (decrementAndGet() == 0) { + errors.tryTerminateConsumer(downstream); + } else { + if (maxConcurrency != Integer.MAX_VALUE) { + upstream.request(1); + } + } + } else { + cancelled = true; + upstream.cancel(); + set.dispose(); + errors.tryTerminateConsumer(downstream); + } + } + } + + @Override + public void onComplete() { + if (decrementAndGet() == 0) { + errors.tryTerminateConsumer(downstream); + } else { + if (maxConcurrency != Integer.MAX_VALUE) { + upstream.request(1); + } + } + } + + @Override + public void cancel() { + cancelled = true; + upstream.cancel(); + set.dispose(); + errors.tryTerminateAndReport(); + } + + @Override + public void request(long n) { + // ignored, no values emitted + } + + @Nullable + @Override + public T poll() { + return null; // always empty + } + + @Override + public boolean isEmpty() { + return true; // always empty + } + + @Override + public void clear() { + // nothing to clear + } + + @Override + public int requestFusion(int mode) { + return mode & ASYNC; + } + + void innerComplete(InnerConsumer inner) { + set.delete(inner); + onComplete(); + } + + void innerError(InnerConsumer inner, Throwable e) { + set.delete(inner); + onError(e); + } + + final class InnerConsumer extends AtomicReference<Disposable> implements CompletableObserver, Disposable { + private static final long serialVersionUID = 8606673141535671828L; + + @Override + public void onSubscribe(Disposable d) { + DisposableHelper.setOnce(this, d); + } + + @Override + public void onComplete() { + innerComplete(this); + } + + @Override + public void onError(Throwable e) { + innerError(this, e); + } + + @Override + public void dispose() { + DisposableHelper.dispose(this); + } + + @Override + public boolean isDisposed() { + return DisposableHelper.isDisposed(get()); + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableFlatMapCompletableCompletable.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableFlatMapCompletableCompletable.java new file mode 100644 index 0000000000..2d604e5157 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableFlatMapCompletableCompletable.java @@ -0,0 +1,217 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import java.util.Objects; +import java.util.concurrent.atomic.*; + +import org.reactivestreams.Subscription; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.*; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; +import io.reactivex.rxjava3.internal.fuseable.FuseToFlowable; +import io.reactivex.rxjava3.internal.subscriptions.SubscriptionHelper; +import io.reactivex.rxjava3.internal.util.AtomicThrowable; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +/** + * Maps a sequence of values into CompletableSources and awaits their termination. + * @param <T> the value type + */ +public final class FlowableFlatMapCompletableCompletable<T> extends Completable implements FuseToFlowable<T> { + + final Flowable<T> source; + + final Function<? super T, ? extends CompletableSource> mapper; + + final int maxConcurrency; + + final boolean delayErrors; + + public FlowableFlatMapCompletableCompletable(Flowable<T> source, + Function<? super T, ? extends CompletableSource> mapper, boolean delayErrors, + int maxConcurrency) { + this.source = source; + this.mapper = mapper; + this.delayErrors = delayErrors; + this.maxConcurrency = maxConcurrency; + } + + @Override + protected void subscribeActual(CompletableObserver observer) { + source.subscribe(new FlatMapCompletableMainSubscriber<>(observer, mapper, delayErrors, maxConcurrency)); + } + + @Override + public Flowable<T> fuseToFlowable() { + return RxJavaPlugins.onAssembly(new FlowableFlatMapCompletable<>(source, mapper, delayErrors, maxConcurrency)); + } + + static final class FlatMapCompletableMainSubscriber<T> extends AtomicInteger + implements FlowableSubscriber<T>, Disposable { + private static final long serialVersionUID = 8443155186132538303L; + + final CompletableObserver downstream; + + final AtomicThrowable errors; + + final Function<? super T, ? extends CompletableSource> mapper; + + final boolean delayErrors; + + final CompositeDisposable set; + + final int maxConcurrency; + + Subscription upstream; + + volatile boolean disposed; + + FlatMapCompletableMainSubscriber(CompletableObserver observer, + Function<? super T, ? extends CompletableSource> mapper, boolean delayErrors, + int maxConcurrency) { + this.downstream = observer; + this.mapper = mapper; + this.delayErrors = delayErrors; + this.errors = new AtomicThrowable(); + this.set = new CompositeDisposable(); + this.maxConcurrency = maxConcurrency; + this.lazySet(1); + } + + @Override + public void onSubscribe(Subscription s) { + if (SubscriptionHelper.validate(this.upstream, s)) { + this.upstream = s; + + downstream.onSubscribe(this); + + int m = maxConcurrency; + if (m == Integer.MAX_VALUE) { + s.request(Long.MAX_VALUE); + } else { + s.request(m); + } + } + } + + @Override + public void onNext(T value) { + CompletableSource cs; + + try { + cs = Objects.requireNonNull(mapper.apply(value), "The mapper returned a null CompletableSource"); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + upstream.cancel(); + onError(ex); + return; + } + + getAndIncrement(); + + InnerObserver inner = new InnerObserver(); + + if (!disposed && set.add(inner)) { + cs.subscribe(inner); + } + } + + @Override + public void onError(Throwable e) { + if (errors.tryAddThrowableOrReport(e)) { + if (delayErrors) { + if (decrementAndGet() == 0) { + errors.tryTerminateConsumer(downstream); + } else { + if (maxConcurrency != Integer.MAX_VALUE) { + upstream.request(1); + } + } + } else { + disposed = true; + upstream.cancel(); + set.dispose(); + errors.tryTerminateConsumer(downstream); + } + } + } + + @Override + public void onComplete() { + if (decrementAndGet() == 0) { + errors.tryTerminateConsumer(downstream); + } else { + if (maxConcurrency != Integer.MAX_VALUE) { + upstream.request(1); + } + } + } + + @Override + public void dispose() { + disposed = true; + upstream.cancel(); + set.dispose(); + errors.tryTerminateAndReport(); + } + + @Override + public boolean isDisposed() { + return set.isDisposed(); + } + + void innerComplete(InnerObserver inner) { + set.delete(inner); + onComplete(); + } + + void innerError(InnerObserver inner, Throwable e) { + set.delete(inner); + onError(e); + } + + final class InnerObserver extends AtomicReference<Disposable> implements CompletableObserver, Disposable { + private static final long serialVersionUID = 8606673141535671828L; + + @Override + public void onSubscribe(Disposable d) { + DisposableHelper.setOnce(this, d); + } + + @Override + public void onComplete() { + innerComplete(this); + } + + @Override + public void onError(Throwable e) { + innerError(this, e); + } + + @Override + public void dispose() { + DisposableHelper.dispose(this); + } + + @Override + public boolean isDisposed() { + return DisposableHelper.isDisposed(get()); + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableFlatMapMaybe.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableFlatMapMaybe.java new file mode 100644 index 0000000000..2936fa9066 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableFlatMapMaybe.java @@ -0,0 +1,398 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import java.util.Objects; +import java.util.concurrent.atomic.*; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.*; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; +import io.reactivex.rxjava3.internal.subscriptions.SubscriptionHelper; +import io.reactivex.rxjava3.internal.util.*; +import io.reactivex.rxjava3.operators.SpscLinkedArrayQueue; + +/** + * Maps upstream values into MaybeSources and merges their signals into one sequence. + * @param <T> the source value type + * @param <R> the result value type + */ +public final class FlowableFlatMapMaybe<T, R> extends AbstractFlowableWithUpstream<T, R> { + + final Function<? super T, ? extends MaybeSource<? extends R>> mapper; + + final boolean delayErrors; + + final int maxConcurrency; + + public FlowableFlatMapMaybe(Flowable<T> source, Function<? super T, ? extends MaybeSource<? extends R>> mapper, + boolean delayError, int maxConcurrency) { + super(source); + this.mapper = mapper; + this.delayErrors = delayError; + this.maxConcurrency = maxConcurrency; + } + + @Override + protected void subscribeActual(Subscriber<? super R> s) { + source.subscribe(new FlatMapMaybeSubscriber<>(s, mapper, delayErrors, maxConcurrency)); + } + + static final class FlatMapMaybeSubscriber<T, R> + extends AtomicInteger + implements FlowableSubscriber<T>, Subscription { + + private static final long serialVersionUID = 8600231336733376951L; + + final Subscriber<? super R> downstream; + + final boolean delayErrors; + + final int maxConcurrency; + + final AtomicLong requested; + + final CompositeDisposable set; + + final AtomicInteger active; + + final AtomicThrowable errors; + + final Function<? super T, ? extends MaybeSource<? extends R>> mapper; + + final AtomicReference<SpscLinkedArrayQueue<R>> queue; + + Subscription upstream; + + volatile boolean cancelled; + + FlatMapMaybeSubscriber(Subscriber<? super R> actual, + Function<? super T, ? extends MaybeSource<? extends R>> mapper, boolean delayErrors, int maxConcurrency) { + this.downstream = actual; + this.mapper = mapper; + this.delayErrors = delayErrors; + this.maxConcurrency = maxConcurrency; + this.requested = new AtomicLong(); + this.set = new CompositeDisposable(); + this.errors = new AtomicThrowable(); + this.active = new AtomicInteger(1); + this.queue = new AtomicReference<>(); + } + + @Override + public void onSubscribe(Subscription s) { + if (SubscriptionHelper.validate(this.upstream, s)) { + this.upstream = s; + + downstream.onSubscribe(this); + + int m = maxConcurrency; + if (m == Integer.MAX_VALUE) { + s.request(Long.MAX_VALUE); + } else { + s.request(maxConcurrency); + } + } + } + + @Override + public void onNext(T t) { + MaybeSource<? extends R> ms; + + try { + ms = Objects.requireNonNull(mapper.apply(t), "The mapper returned a null MaybeSource"); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + upstream.cancel(); + onError(ex); + return; + } + + active.getAndIncrement(); + + InnerObserver inner = new InnerObserver(); + + if (!cancelled && set.add(inner)) { + ms.subscribe(inner); + } + } + + @Override + public void onError(Throwable t) { + active.decrementAndGet(); + if (errors.tryAddThrowableOrReport(t)) { + if (!delayErrors) { + set.dispose(); + } + drain(); + } + } + + @Override + public void onComplete() { + active.decrementAndGet(); + drain(); + } + + @Override + public void cancel() { + cancelled = true; + upstream.cancel(); + set.dispose(); + errors.tryTerminateAndReport(); + } + + @Override + public void request(long n) { + if (SubscriptionHelper.validate(n)) { + BackpressureHelper.add(requested, n); + drain(); + } + } + + void innerSuccess(InnerObserver inner, R value) { + set.delete(inner); + if (get() == 0 && compareAndSet(0, 1)) { + boolean d = active.decrementAndGet() == 0; + if (requested.get() != 0) { + downstream.onNext(value); + + SpscLinkedArrayQueue<R> q = queue.get(); + + if (checkTerminate(d, q)) { + errors.tryTerminateConsumer(downstream); + return; + } + BackpressureHelper.produced(requested, 1); + if (maxConcurrency != Integer.MAX_VALUE) { + upstream.request(1); + } + } else { + SpscLinkedArrayQueue<R> q = getOrCreateQueue(); + synchronized (q) { + q.offer(value); + } + } + if (decrementAndGet() == 0) { + return; + } + } else { + SpscLinkedArrayQueue<R> q = getOrCreateQueue(); + synchronized (q) { + q.offer(value); + } + active.decrementAndGet(); + if (getAndIncrement() != 0) { + return; + } + } + drainLoop(); + } + + SpscLinkedArrayQueue<R> getOrCreateQueue() { + SpscLinkedArrayQueue<R> current = queue.get(); + if (current != null) { + return current; + } + current = new SpscLinkedArrayQueue<>(Flowable.bufferSize()); + if (queue.compareAndSet(null, current)) { + return current; + } + return queue.get(); + } + + void innerError(InnerObserver inner, Throwable e) { + set.delete(inner); + if (errors.tryAddThrowableOrReport(e)) { + if (!delayErrors) { + upstream.cancel(); + set.dispose(); + } else { + if (maxConcurrency != Integer.MAX_VALUE) { + upstream.request(1); + } + } + active.decrementAndGet(); + drain(); + } + } + + void innerComplete(InnerObserver inner) { + set.delete(inner); + + if (get() == 0 && compareAndSet(0, 1)) { + boolean d = active.decrementAndGet() == 0; + SpscLinkedArrayQueue<R> q = queue.get(); + + if (checkTerminate(d, q)) { + errors.tryTerminateConsumer(downstream); + return; + } + + if (maxConcurrency != Integer.MAX_VALUE) { + upstream.request(1); + } + if (decrementAndGet() == 0) { + return; + } + drainLoop(); + } else { + active.decrementAndGet(); + if (maxConcurrency != Integer.MAX_VALUE) { + upstream.request(1); + } + drain(); + } + } + + static boolean checkTerminate(boolean d, SpscLinkedArrayQueue<?> q) { + return d && (q == null || q.isEmpty()); + } + + void drain() { + if (getAndIncrement() == 0) { + drainLoop(); + } + } + + void clear() { + SpscLinkedArrayQueue<R> q = queue.get(); + if (q != null) { + q.clear(); + } + } + + void drainLoop() { + int missed = 1; + Subscriber<? super R> a = downstream; + AtomicInteger n = active; + AtomicReference<SpscLinkedArrayQueue<R>> qr = queue; + + for (;;) { + long r = requested.get(); + long e = 0L; + + while (e != r) { + if (cancelled) { + clear(); + return; + } + + if (!delayErrors) { + Throwable ex = errors.get(); + if (ex != null) { + clear(); + errors.tryTerminateConsumer(a); + return; + } + } + + boolean d = n.get() == 0; + SpscLinkedArrayQueue<R> q = qr.get(); + R v = q != null ? q.poll() : null; + boolean empty = v == null; + + if (d && empty) { + errors.tryTerminateConsumer(a); + return; + } + + if (empty) { + break; + } + + a.onNext(v); + + e++; + } + + if (e == r) { + if (cancelled) { + clear(); + return; + } + + if (!delayErrors) { + Throwable ex = errors.get(); + if (ex != null) { + clear(); + errors.tryTerminateConsumer(a); + return; + } + } + + boolean d = n.get() == 0; + SpscLinkedArrayQueue<R> q = qr.get(); + boolean empty = q == null || q.isEmpty(); + + if (d && empty) { + errors.tryTerminateConsumer(a); + return; + } + } + + if (e != 0L) { + BackpressureHelper.produced(requested, e); + if (maxConcurrency != Integer.MAX_VALUE) { + upstream.request(e); + } + } + + missed = addAndGet(-missed); + if (missed == 0) { + break; + } + } + } + + final class InnerObserver extends AtomicReference<Disposable> + implements MaybeObserver<R>, Disposable { + private static final long serialVersionUID = -502562646270949838L; + + @Override + public void onSubscribe(Disposable d) { + DisposableHelper.setOnce(this, d); + } + + @Override + public void onSuccess(R value) { + innerSuccess(this, value); + } + + @Override + public void onError(Throwable e) { + innerError(this, e); + } + + @Override + public void onComplete() { + innerComplete(this); + } + + @Override + public boolean isDisposed() { + return DisposableHelper.isDisposed(get()); + } + + @Override + public void dispose() { + DisposableHelper.dispose(this); + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableFlatMapMaybePublisher.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableFlatMapMaybePublisher.java new file mode 100644 index 0000000000..acc527215c --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableFlatMapMaybePublisher.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.operators.flowable.FlowableFlatMapMaybe.FlatMapMaybeSubscriber; + +/** + * Maps upstream values into MaybeSources and merges their signals into one sequence. + * @param <T> the source value type + * @param <R> the result value type + */ +public final class FlowableFlatMapMaybePublisher<T, R> extends Flowable<R> { + + final Publisher<T> source; + + final Function<? super T, ? extends MaybeSource<? extends R>> mapper; + + final boolean delayErrors; + + final int maxConcurrency; + + public FlowableFlatMapMaybePublisher(Publisher<T> source, Function<? super T, ? extends MaybeSource<? extends R>> mapper, + boolean delayError, int maxConcurrency) { + this.source = source; + this.mapper = mapper; + this.delayErrors = delayError; + this.maxConcurrency = maxConcurrency; + } + + @Override + protected void subscribeActual(Subscriber<? super R> s) { + source.subscribe(new FlatMapMaybeSubscriber<>(s, mapper, delayErrors, maxConcurrency)); + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableFlatMapSingle.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableFlatMapSingle.java new file mode 100644 index 0000000000..f5277d603a --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableFlatMapSingle.java @@ -0,0 +1,361 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import java.util.Objects; +import java.util.concurrent.atomic.*; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.*; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; +import io.reactivex.rxjava3.internal.subscriptions.SubscriptionHelper; +import io.reactivex.rxjava3.internal.util.*; +import io.reactivex.rxjava3.operators.SpscLinkedArrayQueue; + +/** + * Maps upstream values into SingleSources and merges their signals into one sequence. + * @param <T> the source value type + * @param <R> the result value type + */ +public final class FlowableFlatMapSingle<T, R> extends AbstractFlowableWithUpstream<T, R> { + + final Function<? super T, ? extends SingleSource<? extends R>> mapper; + + final boolean delayErrors; + + final int maxConcurrency; + + public FlowableFlatMapSingle(Flowable<T> source, Function<? super T, ? extends SingleSource<? extends R>> mapper, + boolean delayError, int maxConcurrency) { + super(source); + this.mapper = mapper; + this.delayErrors = delayError; + this.maxConcurrency = maxConcurrency; + } + + @Override + protected void subscribeActual(Subscriber<? super R> s) { + source.subscribe(new FlatMapSingleSubscriber<>(s, mapper, delayErrors, maxConcurrency)); + } + + static final class FlatMapSingleSubscriber<T, R> + extends AtomicInteger + implements FlowableSubscriber<T>, Subscription { + + private static final long serialVersionUID = 8600231336733376951L; + + final Subscriber<? super R> downstream; + + final boolean delayErrors; + + final int maxConcurrency; + + final AtomicLong requested; + + final CompositeDisposable set; + + final AtomicInteger active; + + final AtomicThrowable errors; + + final Function<? super T, ? extends SingleSource<? extends R>> mapper; + + final AtomicReference<SpscLinkedArrayQueue<R>> queue; + + Subscription upstream; + + volatile boolean cancelled; + + FlatMapSingleSubscriber(Subscriber<? super R> actual, + Function<? super T, ? extends SingleSource<? extends R>> mapper, boolean delayErrors, int maxConcurrency) { + this.downstream = actual; + this.mapper = mapper; + this.delayErrors = delayErrors; + this.maxConcurrency = maxConcurrency; + this.requested = new AtomicLong(); + this.set = new CompositeDisposable(); + this.errors = new AtomicThrowable(); + this.active = new AtomicInteger(1); + this.queue = new AtomicReference<>(); + } + + @Override + public void onSubscribe(Subscription s) { + if (SubscriptionHelper.validate(this.upstream, s)) { + this.upstream = s; + + downstream.onSubscribe(this); + + int m = maxConcurrency; + if (m == Integer.MAX_VALUE) { + s.request(Long.MAX_VALUE); + } else { + s.request(maxConcurrency); + } + } + } + + @Override + public void onNext(T t) { + SingleSource<? extends R> ms; + + try { + ms = Objects.requireNonNull(mapper.apply(t), "The mapper returned a null SingleSource"); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + upstream.cancel(); + onError(ex); + return; + } + + active.getAndIncrement(); + + InnerObserver inner = new InnerObserver(); + + if (!cancelled && set.add(inner)) { + ms.subscribe(inner); + } + } + + @Override + public void onError(Throwable t) { + active.decrementAndGet(); + if (errors.tryAddThrowableOrReport(t)) { + if (!delayErrors) { + set.dispose(); + } + drain(); + } + } + + @Override + public void onComplete() { + active.decrementAndGet(); + drain(); + } + + @Override + public void cancel() { + cancelled = true; + upstream.cancel(); + set.dispose(); + errors.tryTerminateAndReport(); + } + + @Override + public void request(long n) { + if (SubscriptionHelper.validate(n)) { + BackpressureHelper.add(requested, n); + drain(); + } + } + + void innerSuccess(InnerObserver inner, R value) { + set.delete(inner); + if (get() == 0 && compareAndSet(0, 1)) { + boolean d = active.decrementAndGet() == 0; + if (requested.get() != 0) { + downstream.onNext(value); + + SpscLinkedArrayQueue<R> q = queue.get(); + + if (d && (q == null || q.isEmpty())) { + errors.tryTerminateConsumer(downstream); + return; + } + BackpressureHelper.produced(requested, 1); + if (maxConcurrency != Integer.MAX_VALUE) { + upstream.request(1); + } + } else { + SpscLinkedArrayQueue<R> q = getOrCreateQueue(); + synchronized (q) { + q.offer(value); + } + } + if (decrementAndGet() == 0) { + return; + } + } else { + SpscLinkedArrayQueue<R> q = getOrCreateQueue(); + synchronized (q) { + q.offer(value); + } + active.decrementAndGet(); + if (getAndIncrement() != 0) { + return; + } + } + drainLoop(); + } + + SpscLinkedArrayQueue<R> getOrCreateQueue() { + SpscLinkedArrayQueue<R> current = queue.get(); + if (current != null) { + return current; + } + current = new SpscLinkedArrayQueue<>(Flowable.bufferSize()); + if (queue.compareAndSet(null, current)) { + return current; + } + return queue.get(); + } + + void innerError(InnerObserver inner, Throwable e) { + set.delete(inner); + if (errors.tryAddThrowableOrReport(e)) { + if (!delayErrors) { + upstream.cancel(); + set.dispose(); + } else { + if (maxConcurrency != Integer.MAX_VALUE) { + upstream.request(1); + } + } + active.decrementAndGet(); + drain(); + } + } + + void drain() { + if (getAndIncrement() == 0) { + drainLoop(); + } + } + + void clear() { + SpscLinkedArrayQueue<R> q = queue.get(); + if (q != null) { + q.clear(); + } + } + + void drainLoop() { + int missed = 1; + Subscriber<? super R> a = downstream; + AtomicInteger n = active; + AtomicReference<SpscLinkedArrayQueue<R>> qr = queue; + + for (;;) { + long r = requested.get(); + long e = 0L; + + while (e != r) { + if (cancelled) { + clear(); + return; + } + + if (!delayErrors) { + Throwable ex = errors.get(); + if (ex != null) { + clear(); + errors.tryTerminateConsumer(downstream); + return; + } + } + + boolean d = n.get() == 0; + SpscLinkedArrayQueue<R> q = qr.get(); + R v = q != null ? q.poll() : null; + boolean empty = v == null; + + if (d && empty) { + errors.tryTerminateConsumer(a); + return; + } + + if (empty) { + break; + } + + a.onNext(v); + + e++; + } + + if (e == r) { + if (cancelled) { + clear(); + return; + } + + if (!delayErrors) { + Throwable ex = errors.get(); + if (ex != null) { + clear(); + errors.tryTerminateConsumer(a); + return; + } + } + + boolean d = n.get() == 0; + SpscLinkedArrayQueue<R> q = qr.get(); + boolean empty = q == null || q.isEmpty(); + + if (d && empty) { + errors.tryTerminateConsumer(a); + return; + } + } + + if (e != 0L) { + BackpressureHelper.produced(requested, e); + if (maxConcurrency != Integer.MAX_VALUE) { + upstream.request(e); + } + } + + missed = addAndGet(-missed); + if (missed == 0) { + break; + } + } + } + + final class InnerObserver extends AtomicReference<Disposable> + implements SingleObserver<R>, Disposable { + private static final long serialVersionUID = -502562646270949838L; + + @Override + public void onSubscribe(Disposable d) { + DisposableHelper.setOnce(this, d); + } + + @Override + public void onSuccess(R value) { + innerSuccess(this, value); + } + + @Override + public void onError(Throwable e) { + innerError(this, e); + } + + @Override + public boolean isDisposed() { + return DisposableHelper.isDisposed(get()); + } + + @Override + public void dispose() { + DisposableHelper.dispose(this); + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableFlatMapSinglePublisher.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableFlatMapSinglePublisher.java new file mode 100644 index 0000000000..570ef96f78 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableFlatMapSinglePublisher.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.operators.flowable.FlowableFlatMapSingle.FlatMapSingleSubscriber; + +/** + * Maps upstream values into SingleSources and merges their signals into one sequence. + * @param <T> the source value type + * @param <R> the result value type + */ +public final class FlowableFlatMapSinglePublisher<T, R> extends Flowable<R> { + + final Publisher<T> source; + + final Function<? super T, ? extends SingleSource<? extends R>> mapper; + + final boolean delayErrors; + + final int maxConcurrency; + + public FlowableFlatMapSinglePublisher(Publisher<T> source, Function<? super T, ? extends SingleSource<? extends R>> mapper, + boolean delayError, int maxConcurrency) { + this.source = source; + this.mapper = mapper; + this.delayErrors = delayError; + this.maxConcurrency = maxConcurrency; + } + + @Override + protected void subscribeActual(Subscriber<? super R> s) { + source.subscribe(new FlatMapSingleSubscriber<>(s, mapper, delayErrors, maxConcurrency)); + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableFlattenIterable.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableFlattenIterable.java new file mode 100644 index 0000000000..0b9164e0cd --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableFlattenIterable.java @@ -0,0 +1,466 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import java.util.*; +import java.util.concurrent.atomic.*; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.annotations.Nullable; +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.subscriptions.*; +import io.reactivex.rxjava3.internal.util.*; +import io.reactivex.rxjava3.operators.QueueSubscription; +import io.reactivex.rxjava3.operators.SimpleQueue; +import io.reactivex.rxjava3.operators.SpscArrayQueue; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +public final class FlowableFlattenIterable<T, R> extends AbstractFlowableWithUpstream<T, R> { + + final Function<? super T, ? extends Iterable<? extends R>> mapper; + + final int prefetch; + + public FlowableFlattenIterable(Flowable<T> source, + Function<? super T, ? extends Iterable<? extends R>> mapper, int prefetch) { + super(source); + this.mapper = mapper; + this.prefetch = prefetch; + } + + @SuppressWarnings("unchecked") + @Override + public void subscribeActual(Subscriber<? super R> s) { + if (source instanceof Supplier) { + T v; + + try { + v = ((Supplier<T>)source).get(); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + EmptySubscription.error(ex, s); + return; + } + + if (v == null) { + EmptySubscription.complete(s); + return; + } + + Iterator<? extends R> it; + + try { + Iterable<? extends R> iterable = mapper.apply(v); + + it = iterable.iterator(); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + EmptySubscription.error(ex, s); + return; + } + + FlowableFromIterable.subscribe(s, it); + + return; + } + source.subscribe(new FlattenIterableSubscriber<>(s, mapper, prefetch)); + } + + /** + * Create a {@link Subscriber} with the given parameters. + * @param <T> the upstream value type + * @param <R> the {@link Iterable} and output value type + * @param downstream the downstream {@code Subscriber} to wrap + * @param mapper the mapper function + * @param prefetch the number of items to prefetch + * @return the new {@code Subscriber} + */ + public static <T, R> Subscriber<T> subscribe(Subscriber<? super R> downstream, Function<? super T, ? extends Iterable<? extends R>> mapper, int prefetch) { + return new FlattenIterableSubscriber<>(downstream, mapper, prefetch); + } + + static final class FlattenIterableSubscriber<T, R> + extends BasicIntQueueSubscription<R> + implements FlowableSubscriber<T> { + + private static final long serialVersionUID = -3096000382929934955L; + + final Subscriber<? super R> downstream; + + final Function<? super T, ? extends Iterable<? extends R>> mapper; + + final int prefetch; + + final int limit; + + final AtomicLong requested; + + Subscription upstream; + + SimpleQueue<T> queue; + + volatile boolean done; + + volatile boolean cancelled; + + final AtomicReference<Throwable> error; + + Iterator<? extends R> current; + + int consumed; + + int fusionMode; + + FlattenIterableSubscriber(Subscriber<? super R> actual, + Function<? super T, ? extends Iterable<? extends R>> mapper, int prefetch) { + this.downstream = actual; + this.mapper = mapper; + this.prefetch = prefetch; + this.limit = prefetch - (prefetch >> 2); + this.error = new AtomicReference<>(); + this.requested = new AtomicLong(); + } + + @Override + public void onSubscribe(Subscription s) { + if (SubscriptionHelper.validate(this.upstream, s)) { + this.upstream = s; + + if (s instanceof QueueSubscription) { + @SuppressWarnings("unchecked") + QueueSubscription<T> qs = (QueueSubscription<T>) s; + + int m = qs.requestFusion(ANY); + + if (m == SYNC) { + fusionMode = m; + this.queue = qs; + done = true; + + downstream.onSubscribe(this); + + return; + } + if (m == ASYNC) { + fusionMode = m; + this.queue = qs; + + downstream.onSubscribe(this); + + s.request(prefetch); + return; + } + } + + queue = new SpscArrayQueue<>(prefetch); + + downstream.onSubscribe(this); + + s.request(prefetch); + } + } + + @Override + public void onNext(T t) { + if (done) { + return; + } + if (fusionMode == NONE && !queue.offer(t)) { + onError(new QueueOverflowException()); + return; + } + drain(); + } + + @Override + public void onError(Throwable t) { + if (!done && ExceptionHelper.addThrowable(error, t)) { + done = true; + drain(); + } else { + RxJavaPlugins.onError(t); + } + } + + @Override + public void onComplete() { + if (done) { + return; + } + done = true; + drain(); + } + + @Override + public void request(long n) { + if (SubscriptionHelper.validate(n)) { + BackpressureHelper.add(requested, n); + drain(); + } + } + + @Override + public void cancel() { + if (!cancelled) { + cancelled = true; + + upstream.cancel(); + + if (getAndIncrement() == 0) { + queue.clear(); + } + } + } + + void drain() { + if (getAndIncrement() != 0) { + return; + } + + final Subscriber<? super R> a = downstream; + final SimpleQueue<T> q = queue; + final boolean replenish = fusionMode != SYNC; + + int missed = 1; + + Iterator<? extends R> it = current; + + for (;;) { + + if (it == null) { + + boolean d = done; + + T t; + + try { + t = q.poll(); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + upstream.cancel(); + ExceptionHelper.addThrowable(error, ex); + ex = ExceptionHelper.terminate(error); + + current = null; + q.clear(); + + a.onError(ex); + return; + } + + boolean empty = t == null; + + if (checkTerminated(d, empty, a, q)) { + return; + } + + if (t != null) { + Iterable<? extends R> iterable; + + boolean b; + + try { + iterable = mapper.apply(t); + + it = iterable.iterator(); + + b = it.hasNext(); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + upstream.cancel(); + ExceptionHelper.addThrowable(error, ex); + ex = ExceptionHelper.terminate(error); + a.onError(ex); + return; + } + + if (!b) { + it = null; + consumedOne(replenish); + continue; + } + + current = it; + } + } + + if (it != null) { + long r = requested.get(); + long e = 0L; + + while (e != r) { + if (checkTerminated(done, false, a, q)) { + return; + } + + R v; + + try { + v = Objects.requireNonNull(it.next(), "The iterator returned a null value"); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + current = null; + upstream.cancel(); + ExceptionHelper.addThrowable(error, ex); + ex = ExceptionHelper.terminate(error); + a.onError(ex); + return; + } + + a.onNext(v); + + if (checkTerminated(done, false, a, q)) { + return; + } + + e++; + + boolean b; + + try { + b = it.hasNext(); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + current = null; + upstream.cancel(); + ExceptionHelper.addThrowable(error, ex); + ex = ExceptionHelper.terminate(error); + a.onError(ex); + return; + } + + if (!b) { + consumedOne(replenish); + it = null; + current = null; + break; + } + } + + if (e == r) { + boolean d = done; + boolean empty = q.isEmpty() && it == null; + + if (checkTerminated(d, empty, a, q)) { + return; + } + } + + if (e != 0L) { + if (r != Long.MAX_VALUE) { + requested.addAndGet(-e); + } + } + + if (it == null) { + continue; + } + } + + missed = addAndGet(-missed); + if (missed == 0) { + break; + } + } + } + + void consumedOne(boolean enabled) { + if (enabled) { + int c = consumed + 1; + if (c == limit) { + consumed = 0; + upstream.request(c); + } else { + consumed = c; + } + } + } + + boolean checkTerminated(boolean d, boolean empty, Subscriber<?> a, SimpleQueue<?> q) { + if (cancelled) { + current = null; + q.clear(); + return true; + } + if (d) { + Throwable ex = error.get(); + if (ex != null) { + ex = ExceptionHelper.terminate(error); + + current = null; + q.clear(); + + a.onError(ex); + return true; + } else if (empty) { + a.onComplete(); + return true; + } + } + return false; + } + + @Override + public void clear() { + current = null; + queue.clear(); + } + + @Override + public boolean isEmpty() { + return current == null && queue.isEmpty(); + } + + @Nullable + @Override + public R poll() throws Throwable { + Iterator<? extends R> it = current; + for (;;) { + if (it == null) { + T v = queue.poll(); + if (v == null) { + return null; + } + + it = mapper.apply(v).iterator(); + + if (!it.hasNext()) { + it = null; + continue; + } + current = it; + } + + R r = Objects.requireNonNull(it.next(), "The iterator returned a null value"); + + if (!it.hasNext()) { + current = null; + } + + return r; + } + } + + @Override + public int requestFusion(int requestedMode) { + if ((requestedMode & SYNC) != 0 && fusionMode == SYNC) { + return SYNC; + } + return NONE; + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableFromAction.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableFromAction.java new file mode 100644 index 0000000000..693f40ddb4 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableFromAction.java @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import org.reactivestreams.Subscriber; + +import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.fuseable.CancellableQueueFuseable; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +/** + * Executes an {@link Action} and signals its exception or completes normally. + * + * @param <T> the value type + * @since 3.0.0 + */ +public final class FlowableFromAction<T> extends Flowable<T> implements Supplier<T> { + + final Action action; + + public FlowableFromAction(Action action) { + this.action = action; + } + + @Override + protected void subscribeActual(Subscriber<? super T> subscriber) { + CancellableQueueFuseable<T> qs = new CancellableQueueFuseable<>(); + subscriber.onSubscribe(qs); + + if (!qs.isDisposed()) { + + try { + action.run(); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + if (!qs.isDisposed()) { + subscriber.onError(ex); + } else { + RxJavaPlugins.onError(ex); + } + return; + } + + if (!qs.isDisposed()) { + subscriber.onComplete(); + } + } + } + + @Override + public T get() throws Throwable { + action.run(); + return null; // considered as onComplete() + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableFromArray.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableFromArray.java new file mode 100644 index 0000000000..1b13bcff3b --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableFromArray.java @@ -0,0 +1,273 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import org.reactivestreams.Subscriber; + +import io.reactivex.rxjava3.annotations.Nullable; +import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.internal.subscriptions.*; +import io.reactivex.rxjava3.internal.util.BackpressureHelper; +import io.reactivex.rxjava3.operators.ConditionalSubscriber; + +import java.util.Objects; + +public final class FlowableFromArray<T> extends Flowable<T> { + final T[] array; + + public FlowableFromArray(T[] array) { + this.array = array; + } + + @Override + public void subscribeActual(Subscriber<? super T> s) { + if (s instanceof ConditionalSubscriber) { + s.onSubscribe(new ArrayConditionalSubscription<>( + (ConditionalSubscriber<? super T>)s, array)); + } else { + s.onSubscribe(new ArraySubscription<>(s, array)); + } + } + + abstract static class BaseArraySubscription<T> extends BasicQueueSubscription<T> { + private static final long serialVersionUID = -2252972430506210021L; + + final T[] array; + + int index; + + volatile boolean cancelled; + + BaseArraySubscription(T[] array) { + this.array = array; + } + + @Override + public final int requestFusion(int mode) { + return mode & SYNC; + } + + @Nullable + @Override + public final T poll() { + int i = index; + T[] arr = array; + if (i == arr.length) { + return null; + } + + index = i + 1; + return Objects.requireNonNull(arr[i], "array element is null"); + } + + @Override + public final boolean isEmpty() { + return index == array.length; + } + + @Override + public final void clear() { + index = array.length; + } + + @Override + public final void request(long n) { + if (SubscriptionHelper.validate(n)) { + if (BackpressureHelper.add(this, n) == 0L) { + if (n == Long.MAX_VALUE) { + fastPath(); + } else { + slowPath(n); + } + } + } + } + + @Override + public final void cancel() { + cancelled = true; + } + + abstract void fastPath(); + + abstract void slowPath(long r); + } + + static final class ArraySubscription<T> extends BaseArraySubscription<T> { + + private static final long serialVersionUID = 2587302975077663557L; + + final Subscriber<? super T> downstream; + + ArraySubscription(Subscriber<? super T> actual, T[] array) { + super(array); + this.downstream = actual; + } + + @Override + void fastPath() { + T[] arr = array; + int f = arr.length; + Subscriber<? super T> a = downstream; + + for (int i = index; i != f; i++) { + if (cancelled) { + return; + } + T t = arr[i]; + if (t == null) { + a.onError(new NullPointerException("The element at index " + i + " is null")); + return; + } else { + a.onNext(t); + } + } + if (cancelled) { + return; + } + a.onComplete(); + } + + @Override + void slowPath(long r) { + long e = 0; + T[] arr = array; + int f = arr.length; + int i = index; + Subscriber<? super T> a = downstream; + + for (;;) { + + while (e != r && i != f) { + if (cancelled) { + return; + } + + T t = arr[i]; + + if (t == null) { + a.onError(new NullPointerException("The element at index " + i + " is null")); + return; + } else { + a.onNext(t); + } + + e++; + i++; + } + + if (i == f) { + if (!cancelled) { + a.onComplete(); + } + return; + } + + r = get(); + if (e == r) { + index = i; + r = addAndGet(-e); + if (r == 0L) { + return; + } + e = 0L; + } + } + } + } + + static final class ArrayConditionalSubscription<T> extends BaseArraySubscription<T> { + + private static final long serialVersionUID = 2587302975077663557L; + + final ConditionalSubscriber<? super T> downstream; + + ArrayConditionalSubscription(ConditionalSubscriber<? super T> actual, T[] array) { + super(array); + this.downstream = actual; + } + + @Override + void fastPath() { + T[] arr = array; + int f = arr.length; + ConditionalSubscriber<? super T> a = downstream; + + for (int i = index; i != f; i++) { + if (cancelled) { + return; + } + T t = arr[i]; + if (t == null) { + a.onError(new NullPointerException("The element at index " + i + " is null")); + return; + } else { + a.tryOnNext(t); + } + } + if (cancelled) { + return; + } + a.onComplete(); + } + + @Override + void slowPath(long r) { + long e = 0; + T[] arr = array; + int f = arr.length; + int i = index; + ConditionalSubscriber<? super T> a = downstream; + + for (;;) { + + while (e != r && i != f) { + if (cancelled) { + return; + } + + T t = arr[i]; + + if (t == null) { + a.onError(new NullPointerException("The element at index " + i + " is null")); + return; + } else { + if (a.tryOnNext(t)) { + e++; + } + + i++; + } + } + + if (i == f) { + if (!cancelled) { + a.onComplete(); + } + return; + } + + r = get(); + if (e == r) { + index = i; + r = addAndGet(-e); + if (r == 0L) { + return; + } + e = 0L; + } + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableFromCallable.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableFromCallable.java new file mode 100644 index 0000000000..e2a331d2f0 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableFromCallable.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import java.util.Objects; +import java.util.concurrent.Callable; + +import org.reactivestreams.Subscriber; + +import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.Supplier; +import io.reactivex.rxjava3.internal.subscriptions.DeferredScalarSubscription; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +public final class FlowableFromCallable<T> extends Flowable<T> implements Supplier<T> { + final Callable<? extends T> callable; + public FlowableFromCallable(Callable<? extends T> callable) { + this.callable = callable; + } + + @Override + public void subscribeActual(Subscriber<? super T> s) { + DeferredScalarSubscription<T> deferred = new DeferredScalarSubscription<>(s); + s.onSubscribe(deferred); + + T t; + try { + t = Objects.requireNonNull(callable.call(), "The callable returned a null value"); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + if (deferred.isCancelled()) { + RxJavaPlugins.onError(ex); + } else { + s.onError(ex); + } + return; + } + + deferred.complete(t); + } + + @Override + public T get() throws Throwable { + return Objects.requireNonNull(callable.call(), "The callable returned a null value"); + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableFromCompletable.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableFromCompletable.java new file mode 100644 index 0000000000..c4b15b947d --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableFromCompletable.java @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import org.reactivestreams.Subscriber; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; +import io.reactivex.rxjava3.internal.fuseable.*; + +/** + * Wrap a Completable into a Flowable. + * + * @param <T> the value type + * @since 3.0.0 + */ +public final class FlowableFromCompletable<T> extends Flowable<T> implements HasUpstreamCompletableSource { + + final CompletableSource source; + + public FlowableFromCompletable(CompletableSource source) { + this.source = source; + } + + @Override + public CompletableSource source() { + return source; + } + + @Override + protected void subscribeActual(Subscriber<? super T> observer) { + source.subscribe(new FromCompletableObserver<T>(observer)); + } + + public static final class FromCompletableObserver<T> + extends AbstractEmptyQueueFuseable<T> + implements CompletableObserver { + + final Subscriber<? super T> downstream; + + Disposable upstream; + + public FromCompletableObserver(Subscriber<? super T> downstream) { + this.downstream = downstream; + } + + @Override + public void cancel() { + upstream.dispose(); + upstream = DisposableHelper.DISPOSED; + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.validate(this.upstream, d)) { + this.upstream = d; + + downstream.onSubscribe(this); + } + } + + @Override + public void onComplete() { + upstream = DisposableHelper.DISPOSED; + downstream.onComplete(); + } + + @Override + public void onError(Throwable e) { + upstream = DisposableHelper.DISPOSED; + downstream.onError(e); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableFromFuture.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableFromFuture.java new file mode 100644 index 0000000000..0fef368287 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableFromFuture.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import java.util.concurrent.*; + +import org.reactivestreams.Subscriber; + +import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.internal.subscriptions.DeferredScalarSubscription; +import io.reactivex.rxjava3.internal.util.ExceptionHelper; + +public final class FlowableFromFuture<T> extends Flowable<T> { + final Future<? extends T> future; + final long timeout; + final TimeUnit unit; + + public FlowableFromFuture(Future<? extends T> future, long timeout, TimeUnit unit) { + this.future = future; + this.timeout = timeout; + this.unit = unit; + } + + @Override + public void subscribeActual(Subscriber<? super T> s) { + DeferredScalarSubscription<T> deferred = new DeferredScalarSubscription<>(s); + s.onSubscribe(deferred); + + T v; + try { + v = unit != null ? future.get(timeout, unit) : future.get(); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + if (!deferred.isCancelled()) { + s.onError(ex); + } + return; + } + if (v == null) { + s.onError(ExceptionHelper.createNullPointerException("The future returned a null value.")); + } else { + deferred.complete(v); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableFromIterable.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableFromIterable.java new file mode 100644 index 0000000000..314f0f2f54 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableFromIterable.java @@ -0,0 +1,421 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import java.util.Iterator; +import java.util.Objects; + +import org.reactivestreams.Subscriber; + +import io.reactivex.rxjava3.annotations.Nullable; +import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.internal.subscriptions.*; +import io.reactivex.rxjava3.internal.util.BackpressureHelper; +import io.reactivex.rxjava3.operators.ConditionalSubscriber; + +public final class FlowableFromIterable<T> extends Flowable<T> { + + final Iterable<? extends T> source; + + public FlowableFromIterable(Iterable<? extends T> source) { + this.source = source; + } + + @Override + public void subscribeActual(Subscriber<? super T> s) { + Iterator<? extends T> it; + try { + it = source.iterator(); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + EmptySubscription.error(e, s); + return; + } + + subscribe(s, it); + } + + public static <T> void subscribe(Subscriber<? super T> s, Iterator<? extends T> it) { + boolean hasNext; + try { + hasNext = it.hasNext(); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + EmptySubscription.error(e, s); + return; + } + + if (!hasNext) { + EmptySubscription.complete(s); + return; + } + + if (s instanceof ConditionalSubscriber) { + s.onSubscribe(new IteratorConditionalSubscription<T>( + (ConditionalSubscriber<? super T>)s, it)); + } else { + s.onSubscribe(new IteratorSubscription<T>(s, it)); + } + } + + abstract static class BaseRangeSubscription<T> extends BasicQueueSubscription<T> { + private static final long serialVersionUID = -2252972430506210021L; + + Iterator<? extends T> iterator; + + volatile boolean cancelled; + + boolean once; + + BaseRangeSubscription(Iterator<? extends T> it) { + this.iterator = it; + } + + @Override + public final int requestFusion(int mode) { + return mode & SYNC; + } + + @Nullable + @Override + public final T poll() { + if (iterator == null) { + return null; + } + if (!once) { + once = true; + } else { + if (!iterator.hasNext()) { + return null; + } + } + return Objects.requireNonNull(iterator.next(), "Iterator.next() returned a null value"); + } + + @Override + public final boolean isEmpty() { + Iterator<? extends T> it = this.iterator; + if (it != null) { + if (!once || it.hasNext()) { + return false; + } + clear(); + } + return true; + } + + @Override + public final void clear() { + iterator = null; + } + + @Override + public final void request(long n) { + if (SubscriptionHelper.validate(n)) { + if (BackpressureHelper.add(this, n) == 0L) { + if (n == Long.MAX_VALUE) { + fastPath(); + } else { + slowPath(n); + } + } + } + } + + @Override + public final void cancel() { + cancelled = true; + } + + abstract void fastPath(); + + abstract void slowPath(long r); + } + + static final class IteratorSubscription<T> extends BaseRangeSubscription<T> { + + private static final long serialVersionUID = -6022804456014692607L; + + final Subscriber<? super T> downstream; + + IteratorSubscription(Subscriber<? super T> actual, Iterator<? extends T> it) { + super(it); + this.downstream = actual; + } + + @Override + void fastPath() { + Iterator<? extends T> it = this.iterator; + Subscriber<? super T> a = downstream; + for (;;) { + if (cancelled) { + return; + } + + T t; + + try { + t = it.next(); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + a.onError(ex); + return; + } + + if (cancelled) { + return; + } + + if (t == null) { + a.onError(new NullPointerException("Iterator.next() returned a null value")); + return; + } else { + a.onNext(t); + } + + if (cancelled) { + return; + } + + boolean b; + + try { + b = it.hasNext(); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + a.onError(ex); + return; + } + + if (!b) { + if (!cancelled) { + a.onComplete(); + } + return; + } + } + } + + @Override + void slowPath(long r) { + long e = 0L; + Iterator<? extends T> it = this.iterator; + Subscriber<? super T> a = downstream; + + for (;;) { + + while (e != r) { + + if (cancelled) { + return; + } + + T t; + + try { + t = it.next(); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + a.onError(ex); + return; + } + + if (cancelled) { + return; + } + + if (t == null) { + a.onError(new NullPointerException("Iterator.next() returned a null value")); + return; + } else { + a.onNext(t); + } + + if (cancelled) { + return; + } + + boolean b; + + try { + b = it.hasNext(); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + a.onError(ex); + return; + } + + if (!b) { + if (!cancelled) { + a.onComplete(); + } + return; + } + + e++; + } + + r = get(); + if (e == r) { + r = addAndGet(-e); + if (r == 0L) { + return; + } + e = 0L; + } + } + } + + } + + static final class IteratorConditionalSubscription<T> extends BaseRangeSubscription<T> { + + private static final long serialVersionUID = -6022804456014692607L; + + final ConditionalSubscriber<? super T> downstream; + + IteratorConditionalSubscription(ConditionalSubscriber<? super T> actual, Iterator<? extends T> it) { + super(it); + this.downstream = actual; + } + + @Override + void fastPath() { + Iterator<? extends T> it = this.iterator; + ConditionalSubscriber<? super T> a = downstream; + for (;;) { + if (cancelled) { + return; + } + + T t; + + try { + t = it.next(); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + a.onError(ex); + return; + } + + if (cancelled) { + return; + } + + if (t == null) { + a.onError(new NullPointerException("Iterator.next() returned a null value")); + return; + } else { + a.tryOnNext(t); + } + + if (cancelled) { + return; + } + + boolean b; + + try { + b = it.hasNext(); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + a.onError(ex); + return; + } + + if (!b) { + if (!cancelled) { + a.onComplete(); + } + return; + } + } + } + + @Override + void slowPath(long r) { + long e = 0L; + Iterator<? extends T> it = this.iterator; + ConditionalSubscriber<? super T> a = downstream; + + for (;;) { + + while (e != r) { + + if (cancelled) { + return; + } + + T t; + + try { + t = it.next(); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + a.onError(ex); + return; + } + + if (cancelled) { + return; + } + + boolean b; + if (t == null) { + a.onError(new NullPointerException("Iterator.next() returned a null value")); + return; + } else { + b = a.tryOnNext(t); + } + + if (cancelled) { + return; + } + + boolean hasNext; + + try { + hasNext = it.hasNext(); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + a.onError(ex); + return; + } + + if (!hasNext) { + if (!cancelled) { + a.onComplete(); + } + return; + } + + if (b) { + e++; + } + } + + r = get(); + if (e == r) { + r = addAndGet(-e); + if (r == 0L) { + return; + } + e = 0L; + } + } + } + + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableFromObservable.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableFromObservable.java new file mode 100644 index 0000000000..664dbf4acf --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableFromObservable.java @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; + +public final class FlowableFromObservable<T> extends Flowable<T> { + + private final ObservableSource<T> upstream; + + public FlowableFromObservable(ObservableSource<T> upstream) { + this.upstream = upstream; + } + + @Override + protected void subscribeActual(Subscriber<? super T> s) { + upstream.subscribe(new SubscriberObserver<>(s)); + } + + static final class SubscriberObserver<T> implements Observer<T>, Subscription { + + final Subscriber<? super T> downstream; + + Disposable upstream; + + SubscriberObserver(Subscriber<? super T> s) { + this.downstream = s; + } + + @Override + public void onComplete() { + downstream.onComplete(); + } + + @Override + public void onError(Throwable e) { + downstream.onError(e); + } + + @Override + public void onNext(T value) { + downstream.onNext(value); + } + + @Override + public void onSubscribe(Disposable d) { + this.upstream = d; + downstream.onSubscribe(this); + } + + @Override public void cancel() { + upstream.dispose(); + } + + @Override + public void request(long n) { + // no backpressure so nothing we can do about this + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableFromPublisher.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableFromPublisher.java new file mode 100644 index 0000000000..a021846388 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableFromPublisher.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.Flowable; + +public final class FlowableFromPublisher<T> extends Flowable<T> { + final Publisher<? extends T> publisher; + + public FlowableFromPublisher(Publisher<? extends T> publisher) { + this.publisher = publisher; + } + + @Override + protected void subscribeActual(Subscriber<? super T> s) { + publisher.subscribe(s); + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableFromRunnable.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableFromRunnable.java new file mode 100644 index 0000000000..0d503d4668 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableFromRunnable.java @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import org.reactivestreams.Subscriber; + +import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.Supplier; +import io.reactivex.rxjava3.internal.fuseable.CancellableQueueFuseable; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +/** + * Executes an {@link Runnable} and signals its exception or completes normally. + * + * @param <T> the value type + * @since 3.0.0 + */ +public final class FlowableFromRunnable<T> extends Flowable<T> implements Supplier<T> { + + final Runnable run; + + public FlowableFromRunnable(Runnable run) { + this.run = run; + } + + @Override + protected void subscribeActual(Subscriber<? super T> subscriber) { + CancellableQueueFuseable<T> qs = new CancellableQueueFuseable<>(); + subscriber.onSubscribe(qs); + + if (!qs.isDisposed()) { + + try { + run.run(); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + if (!qs.isDisposed()) { + subscriber.onError(ex); + } else { + RxJavaPlugins.onError(ex); + } + return; + } + + if (!qs.isDisposed()) { + subscriber.onComplete(); + } + } + } + + @Override + public T get() throws Throwable { + run.run(); + return null; // considered as onComplete() + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableFromSupplier.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableFromSupplier.java new file mode 100644 index 0000000000..ca2a331396 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableFromSupplier.java @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import org.reactivestreams.Subscriber; + +import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.Supplier; +import io.reactivex.rxjava3.internal.subscriptions.DeferredScalarSubscription; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +import java.util.Objects; + +/** + * Call a Supplier for each incoming Subscriber and signal the returned value or the thrown exception. + * @param <T> the value type and element type returned by the supplier and the flow + * @since 3.0.0 + */ +public final class FlowableFromSupplier<T> extends Flowable<T> implements Supplier<T> { + + final Supplier<? extends T> supplier; + + public FlowableFromSupplier(Supplier<? extends T> supplier) { + this.supplier = supplier; + } + + @Override + public void subscribeActual(Subscriber<? super T> s) { + DeferredScalarSubscription<T> deferred = new DeferredScalarSubscription<>(s); + s.onSubscribe(deferred); + + T t; + try { + t = Objects.requireNonNull(supplier.get(), "The supplier returned a null value"); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + if (deferred.isCancelled()) { + RxJavaPlugins.onError(ex); + } else { + s.onError(ex); + } + return; + } + + deferred.complete(t); + } + + @Override + public T get() throws Throwable { + return Objects.requireNonNull(supplier.get(), "The supplier returned a null value"); + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableGenerate.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableGenerate.java new file mode 100644 index 0000000000..3639d64eeb --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableGenerate.java @@ -0,0 +1,200 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import java.util.concurrent.atomic.AtomicLong; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.subscriptions.*; +import io.reactivex.rxjava3.internal.util.*; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +public final class FlowableGenerate<T, S> extends Flowable<T> { + final Supplier<S> stateSupplier; + final BiFunction<S, Emitter<T>, S> generator; + final Consumer<? super S> disposeState; + + public FlowableGenerate(Supplier<S> stateSupplier, BiFunction<S, Emitter<T>, S> generator, + Consumer<? super S> disposeState) { + this.stateSupplier = stateSupplier; + this.generator = generator; + this.disposeState = disposeState; + } + + @Override + public void subscribeActual(Subscriber<? super T> s) { + S state; + + try { + state = stateSupplier.get(); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + EmptySubscription.error(e, s); + return; + } + + s.onSubscribe(new GeneratorSubscription<>(s, generator, disposeState, state)); + } + + static final class GeneratorSubscription<T, S> + extends AtomicLong + implements Emitter<T>, Subscription { + + private static final long serialVersionUID = 7565982551505011832L; + + final Subscriber<? super T> downstream; + final BiFunction<S, ? super Emitter<T>, S> generator; + final Consumer<? super S> disposeState; + + S state; + + volatile boolean cancelled; + + boolean terminate; + + boolean hasNext; + + GeneratorSubscription(Subscriber<? super T> actual, + BiFunction<S, ? super Emitter<T>, S> generator, + Consumer<? super S> disposeState, S initialState) { + this.downstream = actual; + this.generator = generator; + this.disposeState = disposeState; + this.state = initialState; + } + + @Override + public void request(long n) { + if (!SubscriptionHelper.validate(n)) { + return; + } + if (BackpressureHelper.add(this, n) != 0L) { + return; + } + + long e = 0L; + + S s = state; + + final BiFunction<S, ? super Emitter<T>, S> f = generator; + + for (;;) { + while (e != n) { + + if (cancelled) { + state = null; + dispose(s); + return; + } + + hasNext = false; + + try { + s = f.apply(s, this); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + cancelled = true; + state = null; + onError(ex); + dispose(s); + return; + } + + if (terminate) { + cancelled = true; + state = null; + dispose(s); + return; + } + + e++; + } + + n = get(); + if (e == n) { + state = s; + n = addAndGet(-e); + if (n == 0L) { + break; + } + e = 0L; + } + } + } + + private void dispose(S s) { + try { + disposeState.accept(s); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + RxJavaPlugins.onError(ex); + } + } + + @Override + public void cancel() { + if (!cancelled) { + cancelled = true; + + // if there are no running requests, just dispose the state + if (BackpressureHelper.add(this, 1) == 0) { + S s = state; + state = null; + dispose(s); + } + } + } + + @Override + public void onNext(T t) { + if (!terminate) { + if (hasNext) { + onError(new IllegalStateException("onNext already called in this generate turn")); + } else { + if (t == null) { + onError(ExceptionHelper.createNullPointerException("onNext called with a null value.")); + } else { + hasNext = true; + downstream.onNext(t); + } + } + } + } + + @Override + public void onError(Throwable t) { + if (terminate) { + RxJavaPlugins.onError(t); + } else { + if (t == null) { + t = ExceptionHelper.createNullPointerException("onError called with a null Throwable."); + } + terminate = true; + downstream.onError(t); + } + } + + @Override + public void onComplete() { + if (!terminate) { + terminate = true; + downstream.onComplete(); + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableGroupBy.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableGroupBy.java new file mode 100644 index 0000000000..50e641ffc1 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableGroupBy.java @@ -0,0 +1,705 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import java.util.*; +import java.util.concurrent.*; +import java.util.concurrent.atomic.*; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.annotations.Nullable; +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.flowables.GroupedFlowable; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.subscriptions.*; +import io.reactivex.rxjava3.internal.util.*; +import io.reactivex.rxjava3.operators.SpscLinkedArrayQueue; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +public final class FlowableGroupBy<T, K, V> extends AbstractFlowableWithUpstream<T, GroupedFlowable<K, V>> { + final Function<? super T, ? extends K> keySelector; + final Function<? super T, ? extends V> valueSelector; + final int bufferSize; + final boolean delayError; + final Function<? super Consumer<Object>, ? extends Map<K, Object>> mapFactory; + + public FlowableGroupBy(Flowable<T> source, Function<? super T, ? extends K> keySelector, Function<? super T, ? extends V> valueSelector, + int bufferSize, boolean delayError, Function<? super Consumer<Object>, ? extends Map<K, Object>> mapFactory) { + super(source); + this.keySelector = keySelector; + this.valueSelector = valueSelector; + this.bufferSize = bufferSize; + this.delayError = delayError; + this.mapFactory = mapFactory; + } + + @Override + @SuppressWarnings({ "unchecked", "rawtypes" }) + protected void subscribeActual(Subscriber<? super GroupedFlowable<K, V>> s) { + + final Map<Object, GroupedUnicast<K, V>> groups; + final Queue<GroupedUnicast<K, V>> evictedGroups; + + try { + if (mapFactory == null) { + evictedGroups = null; + groups = new ConcurrentHashMap<>(); + } else { + evictedGroups = new ConcurrentLinkedQueue<>(); + Consumer<Object> evictionAction = (Consumer) new EvictionAction<>(evictedGroups); + groups = (Map) mapFactory.apply(evictionAction); + } + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + s.onSubscribe(EmptyComponent.INSTANCE); + s.onError(e); + return; + } + GroupBySubscriber<T, K, V> subscriber = + new GroupBySubscriber<>(s, keySelector, valueSelector, bufferSize, delayError, groups, evictedGroups); + source.subscribe(subscriber); + } + + public static final class GroupBySubscriber<T, K, V> + extends AtomicLong + implements FlowableSubscriber<T>, Subscription { + + private static final long serialVersionUID = -3688291656102519502L; + + final Subscriber<? super GroupedFlowable<K, V>> downstream; + final Function<? super T, ? extends K> keySelector; + final Function<? super T, ? extends V> valueSelector; + final int bufferSize; + final int limit; + final boolean delayError; + final Map<Object, GroupedUnicast<K, V>> groups; + final Queue<GroupedUnicast<K, V>> evictedGroups; + + static final Object NULL_KEY = new Object(); + + Subscription upstream; + + final AtomicBoolean cancelled = new AtomicBoolean(); + + long emittedGroups; + + final AtomicInteger groupCount = new AtomicInteger(1); + + final AtomicLong groupConsumed = new AtomicLong(); + + boolean done; + + public GroupBySubscriber(Subscriber<? super GroupedFlowable<K, V>> actual, Function<? super T, ? extends K> keySelector, + Function<? super T, ? extends V> valueSelector, int bufferSize, boolean delayError, + Map<Object, GroupedUnicast<K, V>> groups, Queue<GroupedUnicast<K, V>> evictedGroups) { + this.downstream = actual; + this.keySelector = keySelector; + this.valueSelector = valueSelector; + this.bufferSize = bufferSize; + this.limit = bufferSize - (bufferSize >> 2); + this.delayError = delayError; + this.groups = groups; + this.evictedGroups = evictedGroups; + } + + @Override + public void onSubscribe(Subscription s) { + if (SubscriptionHelper.validate(this.upstream, s)) { + this.upstream = s; + downstream.onSubscribe(this); + s.request(bufferSize); + } + } + + @Override + public void onNext(T t) { + if (done) { + return; + } + + K key; + try { + key = keySelector.apply(t); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + upstream.cancel(); + onError(ex); + return; + } + + boolean newGroup = false; + Object mapKey = key != null ? key : NULL_KEY; + GroupedUnicast<K, V> group = groups.get(mapKey); + if (group == null) { + // if the main has been cancelled, stop creating groups + // and skip this value + if (cancelled.get()) { + return; + } + + group = GroupedUnicast.createWith(key, bufferSize, this, delayError); + groups.put(mapKey, group); + + groupCount.getAndIncrement(); + + newGroup = true; + } + + V v; + try { + v = ExceptionHelper.nullCheck(valueSelector.apply(t), "The valueSelector returned a null value."); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + upstream.cancel(); + if (newGroup) { + if (emittedGroups != get()) { + downstream.onNext(group); + } else { + MissingBackpressureException mbe = groupHangWarning(emittedGroups); + mbe.initCause(ex); + onError(mbe); + return; + } + } + onError(ex); + return; + } + + group.onNext(v); + + completeEvictions(); + + if (newGroup) { + if (emittedGroups != get()) { + emittedGroups++; + downstream.onNext(group); + if (group.state.tryAbandon()) { + cancel(key); + group.onComplete(); + + requestGroup(1); + } + } else { + upstream.cancel(); + onError(groupHangWarning(emittedGroups)); + } + } + } + + static MissingBackpressureException groupHangWarning(long n) { + return new MissingBackpressureException("Unable to emit a new group (#" + n + ") due to lack of requests. Please make sure the downstream can always accept a new group as well as each group is consumed in order for the whole operator to be able to proceed."); + } + + @Override + public void onError(Throwable t) { + if (done) { + RxJavaPlugins.onError(t); + return; + } + done = true; + for (GroupedUnicast<K, V> g : groups.values()) { + g.onError(t); + } + groups.clear(); + completeEvictions(); + downstream.onError(t); + } + + @Override + public void onComplete() { + if (!done) { + for (GroupedUnicast<K, V> g : groups.values()) { + g.onComplete(); + } + + groups.clear(); + completeEvictions(); + + done = true; + downstream.onComplete(); + } + } + + @Override + public void request(long n) { + if (SubscriptionHelper.validate(n)) { + BackpressureHelper.add(this, n); + } + } + + @Override + public void cancel() { + // cancelling the main source means we don't want any more groups + // but running groups still require new values + if (cancelled.compareAndSet(false, true)) { + completeEvictions(); + if (groupCount.decrementAndGet() == 0) { + upstream.cancel(); + } + } + } + + private void completeEvictions() { + if (evictedGroups != null) { + int count = 0; + GroupedUnicast<K, V> evictedGroup; + while ((evictedGroup = evictedGroups.poll()) != null) { + if (evictedGroup.state.tryComplete()) { + count++; + } + } + if (count != 0) { + groupCount.addAndGet(-count); + } + } + } + + public void cancel(K key) { + Object mapKey = key != null ? key : NULL_KEY; + if (groups.remove(mapKey) != null) { + if (groupCount.decrementAndGet() == 0) { + upstream.cancel(); + } + } + } + + void requestGroup(long n) { + // lots of atomics, save local + AtomicLong groupConsumed = this.groupConsumed; + int limit = this.limit; + // Concurrent groups can request at once, a CAS loop is needed + for (;;) { + long currentConsumed = groupConsumed.get(); + long newConsumed = BackpressureHelper.addCap(currentConsumed, n); + // Accumulate the consumed amounts and atomically update the total + if (groupConsumed.compareAndSet(currentConsumed, newConsumed)) { + // if successful, let's see if the prefetch limit has been surpassed + for (;;) { + if (newConsumed < limit) { + // no further actions to be taken + return; + } + + // Yes, remove one limit from total consumed + long newConsumedAfterLimit = newConsumed - limit; + // Only one thread should subtract + if (groupConsumed.compareAndSet(newConsumed, newConsumedAfterLimit)) { + // Then request up to limit + upstream.request(limit); + } + // We don't quit but loop to see if we are still above the prefetch limit + newConsumed = groupConsumed.get(); + } + } + } + } + } + + static final class EvictionAction<K, V> implements Consumer<GroupedUnicast<K, V>> { + + final Queue<GroupedUnicast<K, V>> evictedGroups; + + EvictionAction(Queue<GroupedUnicast<K, V>> evictedGroups) { + this.evictedGroups = evictedGroups; + } + + @Override + public void accept(GroupedUnicast<K, V> value) { + evictedGroups.offer(value); + } + } + + static final class GroupedUnicast<K, T> extends GroupedFlowable<K, T> { + + final State<T, K> state; + + public static <T, K> GroupedUnicast<K, T> createWith(K key, int bufferSize, GroupBySubscriber<?, K, T> parent, boolean delayError) { + State<T, K> state = new State<>(bufferSize, parent, key, delayError); + return new GroupedUnicast<>(key, state); + } + + protected GroupedUnicast(K key, State<T, K> state) { + super(key); + this.state = state; + } + + @Override + protected void subscribeActual(Subscriber<? super T> s) { + state.subscribe(s); + } + + public void onNext(T t) { + state.onNext(t); + } + + public void onError(Throwable e) { + state.onError(e); + } + + public void onComplete() { + state.onComplete(); + } + } + + static final class State<T, K> extends BasicIntQueueSubscription<T> implements Publisher<T> { + + private static final long serialVersionUID = -3852313036005250360L; + + final K key; + final SpscLinkedArrayQueue<T> queue; + final GroupBySubscriber<?, K, T> parent; + final boolean delayError; + + final AtomicLong requested = new AtomicLong(); + + volatile boolean done; + Throwable error; + + final AtomicBoolean cancelled = new AtomicBoolean(); + + final AtomicReference<Subscriber<? super T>> actual = new AtomicReference<>(); + + boolean outputFused; + int produced; + + final AtomicInteger once = new AtomicInteger(); + + static final int FRESH = 0; + static final int HAS_SUBSCRIBER = 1; + static final int ABANDONED = 2; + static final int ABANDONED_HAS_SUBSCRIBER = ABANDONED | HAS_SUBSCRIBER; + + final AtomicBoolean evictOnce = new AtomicBoolean(); + + State(int bufferSize, GroupBySubscriber<?, K, T> parent, K key, boolean delayError) { + this.queue = new SpscLinkedArrayQueue<>(bufferSize); + this.parent = parent; + this.key = key; + this.delayError = delayError; + } + + @Override + public void request(long n) { + if (SubscriptionHelper.validate(n)) { + BackpressureHelper.add(requested, n); + drain(); + } + } + + @Override + public void cancel() { + if (cancelled.compareAndSet(false, true)) { + cancelParent(); + drain(); + } + } + + @Override + public void subscribe(Subscriber<? super T> subscriber) { + for (;;) { + int s = once.get(); + if ((s & HAS_SUBSCRIBER) != 0) { + break; + } + int u = s | HAS_SUBSCRIBER; + if (once.compareAndSet(s, u)) { + subscriber.onSubscribe(this); + actual.lazySet(subscriber); + if (cancelled.get()) { + actual.lazySet(null); + } else { + drain(); + } + return; + } + } + EmptySubscription.error(new IllegalStateException("Only one Subscriber allowed!"), subscriber); + } + + public void onNext(T t) { + queue.offer(t); + drain(); + } + + public void onError(Throwable e) { + error = e; + done = true; + drain(); + } + + public void onComplete() { + done = true; + drain(); + } + + boolean tryComplete() { + boolean canEvict = evictOnce.compareAndSet(false, true); + done = true; + drain(); + return canEvict; + } + + void cancelParent() { + if ((once.get() & ABANDONED) == 0) { + if (evictOnce.compareAndSet(false, true)) { + parent.cancel(key); + } + } + } + + boolean tryAbandon() { + return once.get() == FRESH && once.compareAndSet(FRESH, ABANDONED); + } + + void drain() { + if (getAndIncrement() != 0) { + return; + } + if (outputFused) { + drainFused(); + } else { + drainNormal(); + } + } + + void drainFused() { + int missed = 1; + + final SpscLinkedArrayQueue<T> q = this.queue; + Subscriber<? super T> a = this.actual.get(); + + for (;;) { + if (a != null) { + if (cancelled.get()) { + return; + } + + boolean d = done; + + if (d && !delayError) { + Throwable ex = error; + if (ex != null) { + q.clear(); + a.onError(ex); + return; + } + } + + a.onNext(null); + + if (d) { + Throwable ex = error; + if (ex != null) { + a.onError(ex); + } else { + a.onComplete(); + } + return; + } + } + + missed = addAndGet(-missed); + if (missed == 0) { + return; + } + + if (a == null) { + a = this.actual.get(); + } + } + } + + void drainNormal() { + int missed = 1; + + final SpscLinkedArrayQueue<T> q = queue; + final boolean delayError = this.delayError; + Subscriber<? super T> a = actual.get(); + final AtomicBoolean cancelled = this.cancelled; + + outer: + for (;;) { + if (cancelled.get()) { + cleanupQueue(0, false); + } else { + if (a != null) { + long r = requested.get(); + long e = 0; + + while (e != r) { + boolean d = done; + T v = q.poll(); + boolean empty = v == null; + + if (checkTerminated(d, empty, a, delayError, e, !empty)) { + continue outer; + } + + if (empty) { + break; + } + + a.onNext(v); + + e++; + } + + if (e == r && checkTerminated(done, q.isEmpty(), a, delayError, e, false)) { + continue outer; + } + + if (e != 0L) { + BackpressureHelper.produced(requested, e); + // replenish based on this batch run + requestParent(e); + } + } + } + + missed = addAndGet(-missed); + if (missed == 0) { + break; + } + if (a == null) { + a = actual.get(); + } + } + } + + void requestParent(long e) { + if ((once.get() & ABANDONED) == 0) { + parent.requestGroup(e); + } + } + + void cleanupQueue(long emitted, boolean polled) { + // if this group is canceled, all accumulated emissions and + // remaining items in the queue should be requested + // so that other groups can proceed + while (queue.poll() != null) { + emitted++; + } + + replenishParent(emitted, polled); + } + + void replenishParent(long emitted, boolean polled) { + if (polled) { + emitted++; + } + if (emitted != 0L) { + requestParent(emitted); + } + } + + boolean checkTerminated(boolean d, boolean empty, Subscriber<? super T> a, + boolean delayError, long emitted, boolean polled) { + if (cancelled.get()) { + cleanupQueue(emitted, polled); + return true; + } + + if (d) { + if (delayError) { + if (empty) { + cancelled.lazySet(true); + Throwable e = error; + if (e != null) { + a.onError(e); + } else { + a.onComplete(); + // completion doesn't mean the parent has completed + // because of evicted groups + replenishParent(emitted, polled); + } + return true; + } + } else { + Throwable e = error; + if (e != null) { + queue.clear(); + cancelled.lazySet(true); + a.onError(e); + return true; + } else + if (empty) { + cancelled.lazySet(true); + a.onComplete(); + + // completion doesn't mean the parent has completed + // because of evicted groups + replenishParent(emitted, polled); + return true; + } + } + } + + return false; + } + + @Override + public int requestFusion(int mode) { + // FIXME fusion mode causes hangs + /* + if ((mode & ASYNC) != 0) { + outputFused = true; + return ASYNC; + } + */ + return NONE; + } + + void tryReplenish() { + int p = produced; + if (p != 0) { + produced = 0; + requestParent(p); + } + } + + @Nullable + @Override + public T poll() { + T v = queue.poll(); + if (v != null) { + produced++; + return v; + } + tryReplenish(); + return null; + } + + @Override + public boolean isEmpty() { + if (queue.isEmpty()) { + tryReplenish(); + return true; + } + tryReplenish(); + return false; + } + + @Override + public void clear() { + SpscLinkedArrayQueue<T> q = queue; + // queue.clear() would drop submitted items and not replenish, possibly hanging other groups + while (q.poll() != null) { + produced++; + } + tryReplenish(); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableGroupJoin.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableGroupJoin.java new file mode 100644 index 0000000000..c9619c48b7 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableGroupJoin.java @@ -0,0 +1,488 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import java.util.*; +import java.util.concurrent.atomic.*; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.*; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.subscriptions.SubscriptionHelper; +import io.reactivex.rxjava3.internal.util.*; +import io.reactivex.rxjava3.operators.SimpleQueue; +import io.reactivex.rxjava3.operators.SpscLinkedArrayQueue; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.processors.UnicastProcessor; + +public final class FlowableGroupJoin<TLeft, TRight, TLeftEnd, TRightEnd, R> extends AbstractFlowableWithUpstream<TLeft, R> { + + final Publisher<? extends TRight> other; + + final Function<? super TLeft, ? extends Publisher<TLeftEnd>> leftEnd; + + final Function<? super TRight, ? extends Publisher<TRightEnd>> rightEnd; + + final BiFunction<? super TLeft, ? super Flowable<TRight>, ? extends R> resultSelector; + + public FlowableGroupJoin( + Flowable<TLeft> source, + Publisher<? extends TRight> other, + Function<? super TLeft, ? extends Publisher<TLeftEnd>> leftEnd, + Function<? super TRight, ? extends Publisher<TRightEnd>> rightEnd, + BiFunction<? super TLeft, ? super Flowable<TRight>, ? extends R> resultSelector) { + super(source); + this.other = other; + this.leftEnd = leftEnd; + this.rightEnd = rightEnd; + this.resultSelector = resultSelector; + } + + @Override + protected void subscribeActual(Subscriber<? super R> s) { + + GroupJoinSubscription<TLeft, TRight, TLeftEnd, TRightEnd, R> parent = + new GroupJoinSubscription<>(s, leftEnd, rightEnd, resultSelector); + + s.onSubscribe(parent); + + LeftRightSubscriber left = new LeftRightSubscriber(parent, true); + parent.disposables.add(left); + LeftRightSubscriber right = new LeftRightSubscriber(parent, false); + parent.disposables.add(right); + + source.subscribe(left); + other.subscribe(right); + } + + interface JoinSupport { + + void innerError(Throwable ex); + + void innerComplete(LeftRightSubscriber sender); + + void innerValue(boolean isLeft, Object o); + + void innerClose(boolean isLeft, LeftRightEndSubscriber index); + + void innerCloseError(Throwable ex); + } + + static final class GroupJoinSubscription<TLeft, TRight, TLeftEnd, TRightEnd, R> + extends AtomicInteger implements Subscription, JoinSupport { + + private static final long serialVersionUID = -6071216598687999801L; + + final Subscriber<? super R> downstream; + + final AtomicLong requested; + + final SpscLinkedArrayQueue<Object> queue; + + final CompositeDisposable disposables; + + final Map<Integer, UnicastProcessor<TRight>> lefts; + + final Map<Integer, TRight> rights; + + final AtomicReference<Throwable> error; + + final Function<? super TLeft, ? extends Publisher<TLeftEnd>> leftEnd; + + final Function<? super TRight, ? extends Publisher<TRightEnd>> rightEnd; + + final BiFunction<? super TLeft, ? super Flowable<TRight>, ? extends R> resultSelector; + + final AtomicInteger active; + + int leftIndex; + + int rightIndex; + + volatile boolean cancelled; + + static final Integer LEFT_VALUE = 1; + + static final Integer RIGHT_VALUE = 2; + + static final Integer LEFT_CLOSE = 3; + + static final Integer RIGHT_CLOSE = 4; + + GroupJoinSubscription(Subscriber<? super R> actual, Function<? super TLeft, ? extends Publisher<TLeftEnd>> leftEnd, + Function<? super TRight, ? extends Publisher<TRightEnd>> rightEnd, + BiFunction<? super TLeft, ? super Flowable<TRight>, ? extends R> resultSelector) { + this.downstream = actual; + this.requested = new AtomicLong(); + this.disposables = new CompositeDisposable(); + this.queue = new SpscLinkedArrayQueue<>(bufferSize()); + this.lefts = new LinkedHashMap<>(); + this.rights = new LinkedHashMap<>(); + this.error = new AtomicReference<>(); + this.leftEnd = leftEnd; + this.rightEnd = rightEnd; + this.resultSelector = resultSelector; + this.active = new AtomicInteger(2); + } + + @Override + public void request(long n) { + if (SubscriptionHelper.validate(n)) { + BackpressureHelper.add(requested, n); + } + } + + @Override + public void cancel() { + if (cancelled) { + return; + } + cancelled = true; + cancelAll(); + if (getAndIncrement() == 0) { + queue.clear(); + } + } + + void cancelAll() { + disposables.dispose(); + } + + void errorAll(Subscriber<?> a) { + Throwable ex = ExceptionHelper.terminate(error); + + for (UnicastProcessor<TRight> up : lefts.values()) { + up.onError(ex); + } + + lefts.clear(); + rights.clear(); + + a.onError(ex); + } + + void fail(Throwable exc, Subscriber<?> a, SimpleQueue<?> q) { + Exceptions.throwIfFatal(exc); + ExceptionHelper.addThrowable(error, exc); + q.clear(); + cancelAll(); + errorAll(a); + } + + void drain() { + if (getAndIncrement() != 0) { + return; + } + + int missed = 1; + SpscLinkedArrayQueue<Object> q = queue; + Subscriber<? super R> a = downstream; + + for (;;) { + for (;;) { + if (cancelled) { + q.clear(); + return; + } + + Throwable ex = error.get(); + if (ex != null) { + q.clear(); + cancelAll(); + errorAll(a); + return; + } + + boolean d = active.get() == 0; + + Integer mode = (Integer)q.poll(); + + boolean empty = mode == null; + + if (d && empty) { + for (UnicastProcessor<?> up : lefts.values()) { + up.onComplete(); + } + + lefts.clear(); + rights.clear(); + disposables.dispose(); + + a.onComplete(); + return; + } + + if (empty) { + break; + } + + Object val = q.poll(); + + if (mode == LEFT_VALUE) { + @SuppressWarnings("unchecked") + TLeft left = (TLeft)val; + + UnicastProcessor<TRight> up = UnicastProcessor.create(); + int idx = leftIndex++; + lefts.put(idx, up); + + Publisher<TLeftEnd> p; + + try { + p = Objects.requireNonNull(leftEnd.apply(left), "The leftEnd returned a null Publisher"); + } catch (Throwable exc) { + fail(exc, a, q); + return; + } + + LeftRightEndSubscriber end = new LeftRightEndSubscriber(this, true, idx); + disposables.add(end); + + p.subscribe(end); + + ex = error.get(); + if (ex != null) { + q.clear(); + cancelAll(); + errorAll(a); + return; + } + + R w; + + try { + w = Objects.requireNonNull(resultSelector.apply(left, up), "The resultSelector returned a null value"); + } catch (Throwable exc) { + fail(exc, a, q); + return; + } + + // TODO since only left emission calls the actual, it is possible to link downstream backpressure with left's source and not error out + if (requested.get() != 0L) { + a.onNext(w); + BackpressureHelper.produced(requested, 1); + } else { + fail(MissingBackpressureException.createDefault(), a, q); + return; + } + + for (TRight right : rights.values()) { + up.onNext(right); + } + } + else if (mode == RIGHT_VALUE) { + @SuppressWarnings("unchecked") + TRight right = (TRight)val; + + int idx = rightIndex++; + + rights.put(idx, right); + + Publisher<TRightEnd> p; + + try { + p = Objects.requireNonNull(rightEnd.apply(right), "The rightEnd returned a null Publisher"); + } catch (Throwable exc) { + fail(exc, a, q); + return; + } + + LeftRightEndSubscriber end = new LeftRightEndSubscriber(this, false, idx); + disposables.add(end); + + p.subscribe(end); + + ex = error.get(); + if (ex != null) { + q.clear(); + cancelAll(); + errorAll(a); + return; + } + + for (UnicastProcessor<TRight> up : lefts.values()) { + up.onNext(right); + } + } + else if (mode == LEFT_CLOSE) { + LeftRightEndSubscriber end = (LeftRightEndSubscriber)val; + + UnicastProcessor<TRight> up = lefts.remove(end.index); + disposables.remove(end); + if (up != null) { + up.onComplete(); + } + } + else { + LeftRightEndSubscriber end = (LeftRightEndSubscriber)val; + + rights.remove(end.index); + disposables.remove(end); + } + } + + missed = addAndGet(-missed); + if (missed == 0) { + break; + } + } + } + + @Override + public void innerError(Throwable ex) { + if (ExceptionHelper.addThrowable(error, ex)) { + active.decrementAndGet(); + drain(); + } else { + RxJavaPlugins.onError(ex); + } + } + + @Override + public void innerComplete(LeftRightSubscriber sender) { + disposables.delete(sender); + active.decrementAndGet(); + drain(); + } + + @Override + public void innerValue(boolean isLeft, Object o) { + synchronized (this) { + queue.offer(isLeft ? LEFT_VALUE : RIGHT_VALUE, o); + } + drain(); + } + + @Override + public void innerClose(boolean isLeft, LeftRightEndSubscriber index) { + synchronized (this) { + queue.offer(isLeft ? LEFT_CLOSE : RIGHT_CLOSE, index); + } + drain(); + } + + @Override + public void innerCloseError(Throwable ex) { + if (ExceptionHelper.addThrowable(error, ex)) { + drain(); + } else { + RxJavaPlugins.onError(ex); + } + } + } + + static final class LeftRightSubscriber + extends AtomicReference<Subscription> + implements FlowableSubscriber<Object>, Disposable { + + private static final long serialVersionUID = 1883890389173668373L; + + final JoinSupport parent; + + final boolean isLeft; + + LeftRightSubscriber(JoinSupport parent, boolean isLeft) { + this.parent = parent; + this.isLeft = isLeft; + } + + @Override + public void dispose() { + SubscriptionHelper.cancel(this); + } + + @Override + public boolean isDisposed() { + return get() == SubscriptionHelper.CANCELLED; + } + + @Override + public void onSubscribe(Subscription s) { + SubscriptionHelper.setOnce(this, s, Long.MAX_VALUE); + } + + @Override + public void onNext(Object t) { + parent.innerValue(isLeft, t); + } + + @Override + public void onError(Throwable t) { + parent.innerError(t); + } + + @Override + public void onComplete() { + parent.innerComplete(this); + } + + } + + static final class LeftRightEndSubscriber + extends AtomicReference<Subscription> + implements FlowableSubscriber<Object>, Disposable { + + private static final long serialVersionUID = 1883890389173668373L; + + final JoinSupport parent; + + final boolean isLeft; + + final int index; + + LeftRightEndSubscriber(JoinSupport parent, + boolean isLeft, int index) { + this.parent = parent; + this.isLeft = isLeft; + this.index = index; + } + + @Override + public void dispose() { + SubscriptionHelper.cancel(this); + } + + @Override + public boolean isDisposed() { + return get() == SubscriptionHelper.CANCELLED; + } + + @Override + public void onSubscribe(Subscription s) { + SubscriptionHelper.setOnce(this, s, Long.MAX_VALUE); + } + + @Override + public void onNext(Object t) { + if (SubscriptionHelper.cancel(this)) { + parent.innerClose(isLeft, this); + } + } + + @Override + public void onError(Throwable t) { + parent.innerCloseError(t); + } + + @Override + public void onComplete() { + parent.innerClose(isLeft, this); + } + + } + +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableHide.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableHide.java new file mode 100644 index 0000000000..bc9b47011b --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableHide.java @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.internal.subscriptions.SubscriptionHelper; + +/** + * Hides the identity of the wrapped Flowable and its Subscription. + * @param <T> the value type + * + * @since 2.0 + */ +public final class FlowableHide<T> extends AbstractFlowableWithUpstream<T, T> { + + public FlowableHide(Flowable<T> source) { + super(source); + } + + @Override + protected void subscribeActual(Subscriber<? super T> s) { + source.subscribe(new HideSubscriber<>(s)); + } + + static final class HideSubscriber<T> implements FlowableSubscriber<T>, Subscription { + + final Subscriber<? super T> downstream; + + Subscription upstream; + + HideSubscriber(Subscriber<? super T> downstream) { + this.downstream = downstream; + } + + @Override + public void request(long n) { + upstream.request(n); + } + + @Override + public void cancel() { + upstream.cancel(); + } + + @Override + public void onSubscribe(Subscription s) { + if (SubscriptionHelper.validate(this.upstream, s)) { + this.upstream = s; + downstream.onSubscribe(this); + } + } + + @Override + public void onNext(T t) { + downstream.onNext(t); + } + + @Override + public void onError(Throwable t) { + downstream.onError(t); + } + + @Override + public void onComplete() { + downstream.onComplete(); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableIgnoreElements.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableIgnoreElements.java new file mode 100644 index 0000000000..20f546feee --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableIgnoreElements.java @@ -0,0 +1,108 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.annotations.Nullable; +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.internal.subscriptions.SubscriptionHelper; +import io.reactivex.rxjava3.operators.QueueSubscription; + +public final class FlowableIgnoreElements<T> extends AbstractFlowableWithUpstream<T, T> { + + public FlowableIgnoreElements(Flowable<T> source) { + super(source); + } + + @Override + protected void subscribeActual(final Subscriber<? super T> t) { + source.subscribe(new IgnoreElementsSubscriber<>(t)); + } + + static final class IgnoreElementsSubscriber<T> implements FlowableSubscriber<T>, QueueSubscription<T> { + final Subscriber<? super T> downstream; + + Subscription upstream; + + IgnoreElementsSubscriber(Subscriber<? super T> downstream) { + this.downstream = downstream; + } + + @Override + public void onSubscribe(Subscription s) { + if (SubscriptionHelper.validate(this.upstream, s)) { + this.upstream = s; + downstream.onSubscribe(this); + s.request(Long.MAX_VALUE); + } + } + + @Override + public void onNext(T t) { + // deliberately ignored + } + + @Override + public void onError(Throwable t) { + downstream.onError(t); + } + + @Override + public void onComplete() { + downstream.onComplete(); + } + + @Override + public boolean offer(T e) { + throw new UnsupportedOperationException("Should not be called!"); + } + + @Override + public boolean offer(T v1, T v2) { + throw new UnsupportedOperationException("Should not be called!"); + } + + @Nullable + @Override + public T poll() { + return null; // empty, always + } + + @Override + public boolean isEmpty() { + return true; + } + + @Override + public void clear() { + // always empty + } + + @Override + public void request(long n) { + // never emits a value + } + + @Override + public void cancel() { + upstream.cancel(); + } + + @Override + public int requestFusion(int mode) { + return mode & ASYNC; + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableIgnoreElementsCompletable.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableIgnoreElementsCompletable.java new file mode 100644 index 0000000000..19d8546d2d --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableIgnoreElementsCompletable.java @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import org.reactivestreams.Subscription; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.fuseable.FuseToFlowable; +import io.reactivex.rxjava3.internal.subscriptions.SubscriptionHelper; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +public final class FlowableIgnoreElementsCompletable<T> extends Completable implements FuseToFlowable<T> { + + final Flowable<T> source; + + public FlowableIgnoreElementsCompletable(Flowable<T> source) { + this.source = source; + } + + @Override + protected void subscribeActual(final CompletableObserver t) { + source.subscribe(new IgnoreElementsSubscriber<>(t)); + } + + @Override + public Flowable<T> fuseToFlowable() { + return RxJavaPlugins.onAssembly(new FlowableIgnoreElements<>(source)); + } + + static final class IgnoreElementsSubscriber<T> implements FlowableSubscriber<T>, Disposable { + final CompletableObserver downstream; + + Subscription upstream; + + IgnoreElementsSubscriber(CompletableObserver downstream) { + this.downstream = downstream; + } + + @Override + public void onSubscribe(Subscription s) { + if (SubscriptionHelper.validate(this.upstream, s)) { + this.upstream = s; + downstream.onSubscribe(this); + s.request(Long.MAX_VALUE); + } + } + + @Override + public void onNext(T t) { + // deliberately ignored + } + + @Override + public void onError(Throwable t) { + upstream = SubscriptionHelper.CANCELLED; + downstream.onError(t); + } + + @Override + public void onComplete() { + upstream = SubscriptionHelper.CANCELLED; + downstream.onComplete(); + } + + @Override + public void dispose() { + upstream.cancel(); + upstream = SubscriptionHelper.CANCELLED; + } + + @Override + public boolean isDisposed() { + return upstream == SubscriptionHelper.CANCELLED; + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableInternalHelper.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableInternalHelper.java new file mode 100644 index 0000000000..781d475872 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableInternalHelper.java @@ -0,0 +1,300 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import java.util.Objects; +import java.util.concurrent.TimeUnit; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.flowables.ConnectableFlowable; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.functions.*; + +/** + * Helper utility class to support Flowable with inner classes. + */ +public final class FlowableInternalHelper { + + /** Utility class. */ + private FlowableInternalHelper() { + throw new IllegalStateException("No instances!"); + } + + static final class SimpleGenerator<T, S> implements BiFunction<S, Emitter<T>, S> { + final Consumer<Emitter<T>> consumer; + + SimpleGenerator(Consumer<Emitter<T>> consumer) { + this.consumer = consumer; + } + + @Override + public S apply(S t1, Emitter<T> t2) throws Throwable { + consumer.accept(t2); + return t1; + } + } + + public static <T, S> BiFunction<S, Emitter<T>, S> simpleGenerator(Consumer<Emitter<T>> consumer) { + return new SimpleGenerator<>(consumer); + } + + static final class SimpleBiGenerator<T, S> implements BiFunction<S, Emitter<T>, S> { + final BiConsumer<S, Emitter<T>> consumer; + + SimpleBiGenerator(BiConsumer<S, Emitter<T>> consumer) { + this.consumer = consumer; + } + + @Override + public S apply(S t1, Emitter<T> t2) throws Throwable { + consumer.accept(t1, t2); + return t1; + } + } + + public static <T, S> BiFunction<S, Emitter<T>, S> simpleBiGenerator(BiConsumer<S, Emitter<T>> consumer) { + return new SimpleBiGenerator<>(consumer); + } + + static final class ItemDelayFunction<T, U> implements Function<T, Publisher<T>> { + final Function<? super T, ? extends Publisher<U>> itemDelay; + + ItemDelayFunction(Function<? super T, ? extends Publisher<U>> itemDelay) { + this.itemDelay = itemDelay; + } + + @Override + public Publisher<T> apply(final T v) throws Throwable { + Publisher<U> p = Objects.requireNonNull(itemDelay.apply(v), "The itemDelay returned a null Publisher"); + return new FlowableTakePublisher<>(p, 1).map(Functions.justFunction(v)).defaultIfEmpty(v); + } + } + + public static <T, U> Function<T, Publisher<T>> itemDelay(final Function<? super T, ? extends Publisher<U>> itemDelay) { + return new ItemDelayFunction<>(itemDelay); + } + + static final class SubscriberOnNext<T> implements Consumer<T> { + final Subscriber<T> subscriber; + + SubscriberOnNext(Subscriber<T> subscriber) { + this.subscriber = subscriber; + } + + @Override + public void accept(T v) { + subscriber.onNext(v); + } + } + + static final class SubscriberOnError<T> implements Consumer<Throwable> { + final Subscriber<T> subscriber; + + SubscriberOnError(Subscriber<T> subscriber) { + this.subscriber = subscriber; + } + + @Override + public void accept(Throwable v) { + subscriber.onError(v); + } + } + + static final class SubscriberOnComplete<T> implements Action { + final Subscriber<T> subscriber; + + SubscriberOnComplete(Subscriber<T> subscriber) { + this.subscriber = subscriber; + } + + @Override + public void run() { + subscriber.onComplete(); + } + } + + public static <T> Consumer<T> subscriberOnNext(Subscriber<T> subscriber) { + return new SubscriberOnNext<>(subscriber); + } + + public static <T> Consumer<Throwable> subscriberOnError(Subscriber<T> subscriber) { + return new SubscriberOnError<>(subscriber); + } + + public static <T> Action subscriberOnComplete(Subscriber<T> subscriber) { + return new SubscriberOnComplete<>(subscriber); + } + + static final class FlatMapWithCombinerInner<U, R, T> implements Function<U, R> { + private final BiFunction<? super T, ? super U, ? extends R> combiner; + private final T t; + + FlatMapWithCombinerInner(BiFunction<? super T, ? super U, ? extends R> combiner, T t) { + this.combiner = combiner; + this.t = t; + } + + @Override + public R apply(U w) throws Throwable { + return combiner.apply(t, w); + } + } + + static final class FlatMapWithCombinerOuter<T, R, U> implements Function<T, Publisher<R>> { + private final BiFunction<? super T, ? super U, ? extends R> combiner; + private final Function<? super T, ? extends Publisher<? extends U>> mapper; + + FlatMapWithCombinerOuter(BiFunction<? super T, ? super U, ? extends R> combiner, + Function<? super T, ? extends Publisher<? extends U>> mapper) { + this.combiner = combiner; + this.mapper = mapper; + } + + @Override + public Publisher<R> apply(final T t) throws Throwable { + @SuppressWarnings("unchecked") + Publisher<U> u = (Publisher<U>)Objects.requireNonNull(mapper.apply(t), "The mapper returned a null Publisher"); + return new FlowableMapPublisher<>(u, new FlatMapWithCombinerInner<U, R, T>(combiner, t)); + } + } + + public static <T, U, R> Function<T, Publisher<R>> flatMapWithCombiner( + final Function<? super T, ? extends Publisher<? extends U>> mapper, + final BiFunction<? super T, ? super U, ? extends R> combiner) { + return new FlatMapWithCombinerOuter<>(combiner, mapper); + } + + static final class FlatMapIntoIterable<T, U> implements Function<T, Publisher<U>> { + private final Function<? super T, ? extends Iterable<? extends U>> mapper; + + FlatMapIntoIterable(Function<? super T, ? extends Iterable<? extends U>> mapper) { + this.mapper = mapper; + } + + @Override + public Publisher<U> apply(T t) throws Throwable { + return new FlowableFromIterable<>(Objects.requireNonNull(mapper.apply(t), "The mapper returned a null Iterable")); + } + } + + public static <T, U> Function<T, Publisher<U>> flatMapIntoIterable(final Function<? super T, ? extends Iterable<? extends U>> mapper) { + return new FlatMapIntoIterable<>(mapper); + } + + public static <T> Supplier<ConnectableFlowable<T>> replaySupplier(final Flowable<T> parent) { + return new ReplaySupplier<>(parent); + } + + public static <T> Supplier<ConnectableFlowable<T>> replaySupplier(final Flowable<T> parent, final int bufferSize, boolean eagerTruncate) { + return new BufferedReplaySupplier<>(parent, bufferSize, eagerTruncate); + } + + public static <T> Supplier<ConnectableFlowable<T>> replaySupplier(final Flowable<T> parent, final int bufferSize, final long time, final TimeUnit unit, final Scheduler scheduler, boolean eagerTruncate) { + return new BufferedTimedReplay<>(parent, bufferSize, time, unit, scheduler, eagerTruncate); + } + + public static <T> Supplier<ConnectableFlowable<T>> replaySupplier(final Flowable<T> parent, final long time, final TimeUnit unit, final Scheduler scheduler, boolean eagerTruncate) { + return new TimedReplay<>(parent, time, unit, scheduler, eagerTruncate); + } + + public enum RequestMax implements Consumer<Subscription> { + INSTANCE; + @Override + public void accept(Subscription t) { + t.request(Long.MAX_VALUE); + } + } + + static final class ReplaySupplier<T> implements Supplier<ConnectableFlowable<T>> { + + final Flowable<T> parent; + + ReplaySupplier(Flowable<T> parent) { + this.parent = parent; + } + + @Override + public ConnectableFlowable<T> get() { + return parent.replay(); + } + } + + static final class BufferedReplaySupplier<T> implements Supplier<ConnectableFlowable<T>> { + + final Flowable<T> parent; + + final int bufferSize; + + final boolean eagerTruncate; + + BufferedReplaySupplier(Flowable<T> parent, int bufferSize, boolean eagerTruncate) { + this.parent = parent; + this.bufferSize = bufferSize; + this.eagerTruncate = eagerTruncate; + } + + @Override + public ConnectableFlowable<T> get() { + return parent.replay(bufferSize, eagerTruncate); + } + } + + static final class BufferedTimedReplay<T> implements Supplier<ConnectableFlowable<T>> { + final Flowable<T> parent; + final int bufferSize; + final long time; + final TimeUnit unit; + final Scheduler scheduler; + + final boolean eagerTruncate; + + BufferedTimedReplay(Flowable<T> parent, int bufferSize, long time, TimeUnit unit, Scheduler scheduler, boolean eagerTruncate) { + this.parent = parent; + this.bufferSize = bufferSize; + this.time = time; + this.unit = unit; + this.scheduler = scheduler; + this.eagerTruncate = eagerTruncate; + } + + @Override + public ConnectableFlowable<T> get() { + return parent.replay(bufferSize, time, unit, scheduler, eagerTruncate); + } + } + + static final class TimedReplay<T> implements Supplier<ConnectableFlowable<T>> { + private final Flowable<T> parent; + private final long time; + private final TimeUnit unit; + private final Scheduler scheduler; + + final boolean eagerTruncate; + + TimedReplay(Flowable<T> parent, long time, TimeUnit unit, Scheduler scheduler, boolean eagerTruncate) { + this.parent = parent; + this.time = time; + this.unit = unit; + this.scheduler = scheduler; + this.eagerTruncate = eagerTruncate; + } + + @Override + public ConnectableFlowable<T> get() { + return parent.replay(time, unit, scheduler, eagerTruncate); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableInterval.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableInterval.java new file mode 100644 index 0000000000..98fbe6dee1 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableInterval.java @@ -0,0 +1,106 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.*; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.core.Scheduler.Worker; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.MissingBackpressureException; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; +import io.reactivex.rxjava3.internal.schedulers.TrampolineScheduler; +import io.reactivex.rxjava3.internal.subscriptions.SubscriptionHelper; +import io.reactivex.rxjava3.internal.util.BackpressureHelper; + +public final class FlowableInterval extends Flowable<Long> { + final Scheduler scheduler; + final long initialDelay; + final long period; + final TimeUnit unit; + + public FlowableInterval(long initialDelay, long period, TimeUnit unit, Scheduler scheduler) { + this.initialDelay = initialDelay; + this.period = period; + this.unit = unit; + this.scheduler = scheduler; + } + + @Override + public void subscribeActual(Subscriber<? super Long> s) { + IntervalSubscriber is = new IntervalSubscriber(s); + s.onSubscribe(is); + + Scheduler sch = scheduler; + + if (sch instanceof TrampolineScheduler) { + Worker worker = sch.createWorker(); + is.setResource(worker); + worker.schedulePeriodically(is, initialDelay, period, unit); + } else { + Disposable d = sch.schedulePeriodicallyDirect(is, initialDelay, period, unit); + is.setResource(d); + } + } + + static final class IntervalSubscriber extends AtomicLong + implements Subscription, Runnable { + + private static final long serialVersionUID = -2809475196591179431L; + + final Subscriber<? super Long> downstream; + + long count; + + final AtomicReference<Disposable> resource = new AtomicReference<>(); + + IntervalSubscriber(Subscriber<? super Long> downstream) { + this.downstream = downstream; + } + + @Override + public void request(long n) { + if (SubscriptionHelper.validate(n)) { + BackpressureHelper.add(this, n); + } + } + + @Override + public void cancel() { + DisposableHelper.dispose(resource); + } + + @Override + public void run() { + if (resource.get() != DisposableHelper.DISPOSED) { + long r = get(); + + if (r != 0L) { + downstream.onNext(count++); + BackpressureHelper.produced(this, 1); + } else { + downstream.onError(new MissingBackpressureException("Could not emit value " + count + " due to lack of requests")); + DisposableHelper.dispose(resource); + } + } + } + + public void setResource(Disposable d) { + DisposableHelper.setOnce(resource, d); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableIntervalRange.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableIntervalRange.java new file mode 100644 index 0000000000..cfa605882d --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableIntervalRange.java @@ -0,0 +1,127 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.*; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.core.Scheduler.Worker; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.MissingBackpressureException; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; +import io.reactivex.rxjava3.internal.schedulers.TrampolineScheduler; +import io.reactivex.rxjava3.internal.subscriptions.SubscriptionHelper; +import io.reactivex.rxjava3.internal.util.BackpressureHelper; + +public final class FlowableIntervalRange extends Flowable<Long> { + final Scheduler scheduler; + final long start; + final long end; + final long initialDelay; + final long period; + final TimeUnit unit; + + public FlowableIntervalRange(long start, long end, long initialDelay, long period, TimeUnit unit, Scheduler scheduler) { + this.initialDelay = initialDelay; + this.period = period; + this.unit = unit; + this.scheduler = scheduler; + this.start = start; + this.end = end; + } + + @Override + public void subscribeActual(Subscriber<? super Long> s) { + IntervalRangeSubscriber is = new IntervalRangeSubscriber(s, start, end); + s.onSubscribe(is); + + Scheduler sch = scheduler; + + if (sch instanceof TrampolineScheduler) { + Worker worker = sch.createWorker(); + is.setResource(worker); + worker.schedulePeriodically(is, initialDelay, period, unit); + } else { + Disposable d = sch.schedulePeriodicallyDirect(is, initialDelay, period, unit); + is.setResource(d); + } + } + + static final class IntervalRangeSubscriber extends AtomicLong + implements Subscription, Runnable { + + private static final long serialVersionUID = -2809475196591179431L; + + final Subscriber<? super Long> downstream; + final long end; + + long count; + + final AtomicReference<Disposable> resource = new AtomicReference<>(); + + IntervalRangeSubscriber(Subscriber<? super Long> actual, long start, long end) { + this.downstream = actual; + this.count = start; + this.end = end; + } + + @Override + public void request(long n) { + if (SubscriptionHelper.validate(n)) { + BackpressureHelper.add(this, n); + } + } + + @Override + public void cancel() { + DisposableHelper.dispose(resource); + } + + @Override + public void run() { + if (resource.get() != DisposableHelper.DISPOSED) { + long r = get(); + + if (r != 0L) { + long c = count; + downstream.onNext(c); + + if (c == end) { + if (resource.get() != DisposableHelper.DISPOSED) { + downstream.onComplete(); + } + DisposableHelper.dispose(resource); + return; + } + + count = c + 1; + + if (r != Long.MAX_VALUE) { + decrementAndGet(); + } + } else { + downstream.onError(new MissingBackpressureException("Could not emit value " + count + " due to lack of requests")); + DisposableHelper.dispose(resource); + } + } + } + + public void setResource(Disposable d) { + DisposableHelper.setOnce(resource, d); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableJoin.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableJoin.java new file mode 100644 index 0000000000..9cda3d457d --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableJoin.java @@ -0,0 +1,399 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import java.util.*; +import java.util.concurrent.atomic.*; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.disposables.CompositeDisposable; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.operators.flowable.FlowableGroupJoin.*; +import io.reactivex.rxjava3.internal.subscriptions.SubscriptionHelper; +import io.reactivex.rxjava3.internal.util.*; +import io.reactivex.rxjava3.operators.SimpleQueue; +import io.reactivex.rxjava3.operators.SpscLinkedArrayQueue; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +public final class FlowableJoin<TLeft, TRight, TLeftEnd, TRightEnd, R> extends AbstractFlowableWithUpstream<TLeft, R> { + + final Publisher<? extends TRight> other; + + final Function<? super TLeft, ? extends Publisher<TLeftEnd>> leftEnd; + + final Function<? super TRight, ? extends Publisher<TRightEnd>> rightEnd; + + final BiFunction<? super TLeft, ? super TRight, ? extends R> resultSelector; + + public FlowableJoin( + Flowable<TLeft> source, + Publisher<? extends TRight> other, + Function<? super TLeft, ? extends Publisher<TLeftEnd>> leftEnd, + Function<? super TRight, ? extends Publisher<TRightEnd>> rightEnd, + BiFunction<? super TLeft, ? super TRight, ? extends R> resultSelector) { + super(source); + this.other = other; + this.leftEnd = leftEnd; + this.rightEnd = rightEnd; + this.resultSelector = resultSelector; + } + + @Override + protected void subscribeActual(Subscriber<? super R> s) { + + JoinSubscription<TLeft, TRight, TLeftEnd, TRightEnd, R> parent = + new JoinSubscription<>(s, leftEnd, rightEnd, resultSelector); + + s.onSubscribe(parent); + + LeftRightSubscriber left = new LeftRightSubscriber(parent, true); + parent.disposables.add(left); + LeftRightSubscriber right = new LeftRightSubscriber(parent, false); + parent.disposables.add(right); + + source.subscribe(left); + other.subscribe(right); + } + + static final class JoinSubscription<TLeft, TRight, TLeftEnd, TRightEnd, R> + extends AtomicInteger implements Subscription, JoinSupport { + + private static final long serialVersionUID = -6071216598687999801L; + + final Subscriber<? super R> downstream; + + final AtomicLong requested; + + final SpscLinkedArrayQueue<Object> queue; + + final CompositeDisposable disposables; + + final Map<Integer, TLeft> lefts; + + final Map<Integer, TRight> rights; + + final AtomicReference<Throwable> error; + + final Function<? super TLeft, ? extends Publisher<TLeftEnd>> leftEnd; + + final Function<? super TRight, ? extends Publisher<TRightEnd>> rightEnd; + + final BiFunction<? super TLeft, ? super TRight, ? extends R> resultSelector; + + final AtomicInteger active; + + int leftIndex; + + int rightIndex; + + volatile boolean cancelled; + + static final Integer LEFT_VALUE = 1; + + static final Integer RIGHT_VALUE = 2; + + static final Integer LEFT_CLOSE = 3; + + static final Integer RIGHT_CLOSE = 4; + + JoinSubscription(Subscriber<? super R> actual, Function<? super TLeft, ? extends Publisher<TLeftEnd>> leftEnd, + Function<? super TRight, ? extends Publisher<TRightEnd>> rightEnd, + BiFunction<? super TLeft, ? super TRight, ? extends R> resultSelector) { + this.downstream = actual; + this.requested = new AtomicLong(); + this.disposables = new CompositeDisposable(); + this.queue = new SpscLinkedArrayQueue<>(bufferSize()); + this.lefts = new LinkedHashMap<>(); + this.rights = new LinkedHashMap<>(); + this.error = new AtomicReference<>(); + this.leftEnd = leftEnd; + this.rightEnd = rightEnd; + this.resultSelector = resultSelector; + this.active = new AtomicInteger(2); + } + + @Override + public void request(long n) { + if (SubscriptionHelper.validate(n)) { + BackpressureHelper.add(requested, n); + } + } + + @Override + public void cancel() { + if (cancelled) { + return; + } + cancelled = true; + cancelAll(); + if (getAndIncrement() == 0) { + queue.clear(); + } + } + + void cancelAll() { + disposables.dispose(); + } + + void errorAll(Subscriber<?> a) { + Throwable ex = ExceptionHelper.terminate(error); + + lefts.clear(); + rights.clear(); + + a.onError(ex); + } + + void fail(Throwable exc, Subscriber<?> a, SimpleQueue<?> q) { + Exceptions.throwIfFatal(exc); + ExceptionHelper.addThrowable(error, exc); + q.clear(); + cancelAll(); + errorAll(a); + } + + void drain() { + if (getAndIncrement() != 0) { + return; + } + + int missed = 1; + SpscLinkedArrayQueue<Object> q = queue; + Subscriber<? super R> a = downstream; + + for (;;) { + for (;;) { + if (cancelled) { + q.clear(); + return; + } + + Throwable ex = error.get(); + if (ex != null) { + q.clear(); + cancelAll(); + errorAll(a); + return; + } + + boolean d = active.get() == 0; + + Integer mode = (Integer)q.poll(); + + boolean empty = mode == null; + + if (d && empty) { + + lefts.clear(); + rights.clear(); + disposables.dispose(); + + a.onComplete(); + return; + } + + if (empty) { + break; + } + + Object val = q.poll(); + + if (mode == LEFT_VALUE) { + @SuppressWarnings("unchecked") + TLeft left = (TLeft)val; + + int idx = leftIndex++; + lefts.put(idx, left); + + Publisher<TLeftEnd> p; + + try { + p = Objects.requireNonNull(leftEnd.apply(left), "The leftEnd returned a null Publisher"); + } catch (Throwable exc) { + fail(exc, a, q); + return; + } + + LeftRightEndSubscriber end = new LeftRightEndSubscriber(this, true, idx); + disposables.add(end); + + p.subscribe(end); + + ex = error.get(); + if (ex != null) { + q.clear(); + cancelAll(); + errorAll(a); + return; + } + + long r = requested.get(); + long e = 0L; + + for (TRight right : rights.values()) { + + R w; + + try { + w = Objects.requireNonNull(resultSelector.apply(left, right), "The resultSelector returned a null value"); + } catch (Throwable exc) { + fail(exc, a, q); + return; + } + + if (e != r) { + a.onNext(w); + + e++; + } else { + ExceptionHelper.addThrowable(error, MissingBackpressureException.createDefault()); + q.clear(); + cancelAll(); + errorAll(a); + return; + } + } + + if (e != 0L) { + BackpressureHelper.produced(requested, e); + } + } + else if (mode == RIGHT_VALUE) { + @SuppressWarnings("unchecked") + TRight right = (TRight)val; + + int idx = rightIndex++; + + rights.put(idx, right); + + Publisher<TRightEnd> p; + + try { + p = Objects.requireNonNull(rightEnd.apply(right), "The rightEnd returned a null Publisher"); + } catch (Throwable exc) { + fail(exc, a, q); + return; + } + + LeftRightEndSubscriber end = new LeftRightEndSubscriber(this, false, idx); + disposables.add(end); + + p.subscribe(end); + + ex = error.get(); + if (ex != null) { + q.clear(); + cancelAll(); + errorAll(a); + return; + } + + long r = requested.get(); + long e = 0L; + + for (TLeft left : lefts.values()) { + + R w; + + try { + w = Objects.requireNonNull(resultSelector.apply(left, right), "The resultSelector returned a null value"); + } catch (Throwable exc) { + fail(exc, a, q); + return; + } + + if (e != r) { + a.onNext(w); + + e++; + } else { + ExceptionHelper.addThrowable(error, MissingBackpressureException.createDefault()); + q.clear(); + cancelAll(); + errorAll(a); + return; + } + } + + if (e != 0L) { + BackpressureHelper.produced(requested, e); + } + } + else if (mode == LEFT_CLOSE) { + LeftRightEndSubscriber end = (LeftRightEndSubscriber)val; + + lefts.remove(end.index); + disposables.remove(end); + } + else { + LeftRightEndSubscriber end = (LeftRightEndSubscriber)val; + + rights.remove(end.index); + disposables.remove(end); + } + } + + missed = addAndGet(-missed); + if (missed == 0) { + break; + } + } + } + + @Override + public void innerError(Throwable ex) { + if (ExceptionHelper.addThrowable(error, ex)) { + active.decrementAndGet(); + drain(); + } else { + RxJavaPlugins.onError(ex); + } + } + + @Override + public void innerComplete(LeftRightSubscriber sender) { + disposables.delete(sender); + active.decrementAndGet(); + drain(); + } + + @Override + public void innerValue(boolean isLeft, Object o) { + synchronized (this) { + queue.offer(isLeft ? LEFT_VALUE : RIGHT_VALUE, o); + } + drain(); + } + + @Override + public void innerClose(boolean isLeft, LeftRightEndSubscriber index) { + synchronized (this) { + queue.offer(isLeft ? LEFT_CLOSE : RIGHT_CLOSE, index); + } + drain(); + } + + @Override + public void innerCloseError(Throwable ex) { + if (ExceptionHelper.addThrowable(error, ex)) { + drain(); + } else { + RxJavaPlugins.onError(ex); + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableJust.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableJust.java new file mode 100644 index 0000000000..2926d49509 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableJust.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import org.reactivestreams.Subscriber; + +import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.internal.subscriptions.ScalarSubscription; +import io.reactivex.rxjava3.operators.ScalarSupplier; + +/** + * Represents a constant scalar value. + * @param <T> the value type + */ +public final class FlowableJust<T> extends Flowable<T> implements ScalarSupplier<T> { + private final T value; + public FlowableJust(final T value) { + this.value = value; + } + + @Override + protected void subscribeActual(Subscriber<? super T> s) { + s.onSubscribe(new ScalarSubscription<>(s, value)); + } + + @Override + public T get() { + return value; + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableLastMaybe.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableLastMaybe.java new file mode 100644 index 0000000000..056dfe9201 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableLastMaybe.java @@ -0,0 +1,100 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.subscriptions.SubscriptionHelper; + +/** + * Consumes the source Publisher and emits its last item or completes. + * + * @param <T> the value type + */ +public final class FlowableLastMaybe<T> extends Maybe<T> { + + final Publisher<T> source; + + public FlowableLastMaybe(Publisher<T> source) { + this.source = source; + } + + // TODO fuse back to Flowable + + @Override + protected void subscribeActual(MaybeObserver<? super T> observer) { + source.subscribe(new LastSubscriber<>(observer)); + } + + static final class LastSubscriber<T> implements FlowableSubscriber<T>, Disposable { + + final MaybeObserver<? super T> downstream; + + Subscription upstream; + + T item; + + LastSubscriber(MaybeObserver<? super T> downstream) { + this.downstream = downstream; + } + + @Override + public void dispose() { + upstream.cancel(); + upstream = SubscriptionHelper.CANCELLED; + } + + @Override + public boolean isDisposed() { + return upstream == SubscriptionHelper.CANCELLED; + } + + @Override + public void onSubscribe(Subscription s) { + if (SubscriptionHelper.validate(this.upstream, s)) { + this.upstream = s; + + downstream.onSubscribe(this); + + s.request(Long.MAX_VALUE); + } + } + + @Override + public void onNext(T t) { + item = t; + } + + @Override + public void onError(Throwable t) { + upstream = SubscriptionHelper.CANCELLED; + item = null; + downstream.onError(t); + } + + @Override + public void onComplete() { + upstream = SubscriptionHelper.CANCELLED; + T v = item; + if (v != null) { + item = null; + downstream.onSuccess(v); + } else { + downstream.onComplete(); + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableLastSingle.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableLastSingle.java new file mode 100644 index 0000000000..ef4fb1a015 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableLastSingle.java @@ -0,0 +1,115 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import java.util.NoSuchElementException; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.subscriptions.SubscriptionHelper; + +/** + * Consumes the source Publisher and emits its last item or the defaultItem + * if empty. + * + * @param <T> the value type + */ +public final class FlowableLastSingle<T> extends Single<T> { + + final Publisher<T> source; + + final T defaultItem; + + public FlowableLastSingle(Publisher<T> source, T defaultItem) { + this.source = source; + this.defaultItem = defaultItem; + } + + // TODO fuse back to Flowable + + @Override + protected void subscribeActual(SingleObserver<? super T> observer) { + source.subscribe(new LastSubscriber<>(observer, defaultItem)); + } + + static final class LastSubscriber<T> implements FlowableSubscriber<T>, Disposable { + + final SingleObserver<? super T> downstream; + + final T defaultItem; + + Subscription upstream; + + T item; + + LastSubscriber(SingleObserver<? super T> actual, T defaultItem) { + this.downstream = actual; + this.defaultItem = defaultItem; + } + + @Override + public void dispose() { + upstream.cancel(); + upstream = SubscriptionHelper.CANCELLED; + } + + @Override + public boolean isDisposed() { + return upstream == SubscriptionHelper.CANCELLED; + } + + @Override + public void onSubscribe(Subscription s) { + if (SubscriptionHelper.validate(this.upstream, s)) { + this.upstream = s; + + downstream.onSubscribe(this); + + s.request(Long.MAX_VALUE); + } + } + + @Override + public void onNext(T t) { + item = t; + } + + @Override + public void onError(Throwable t) { + upstream = SubscriptionHelper.CANCELLED; + item = null; + downstream.onError(t); + } + + @Override + public void onComplete() { + upstream = SubscriptionHelper.CANCELLED; + T v = item; + if (v != null) { + item = null; + downstream.onSuccess(v); + } else { + v = defaultItem; + + if (v != null) { + downstream.onSuccess(v); + } else { + downstream.onError(new NoSuchElementException()); + } + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableLift.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableLift.java new file mode 100644 index 0000000000..99e4075bc4 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableLift.java @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import org.reactivestreams.Subscriber; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +/** + * Allows lifting operators into a chain of Publishers. + * + * <p>By having a concrete Publisher as lift, operator fusing can now identify + * both the source and the operation inside it via casting, unlike the lambda version of this. + * + * @param <T> the upstream value type + * @param <R> the downstream parameter type + */ +public final class FlowableLift<R, T> extends AbstractFlowableWithUpstream<T, R> { + /** The actual operator. */ + final FlowableOperator<? extends R, ? super T> operator; + + public FlowableLift(Flowable<T> source, FlowableOperator<? extends R, ? super T> operator) { + super(source); + this.operator = operator; + } + + @Override + public void subscribeActual(Subscriber<? super R> s) { + try { + Subscriber<? super T> st = operator.apply(s); + + if (st == null) { + throw new NullPointerException("Operator " + operator + " returned a null Subscriber"); + } + + source.subscribe(st); + } catch (NullPointerException e) { // NOPMD + throw e; + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + // can't call onError because no way to know if a Subscription has been set or not + // can't call onSubscribe because the call might have set a Subscription already + RxJavaPlugins.onError(e); + + NullPointerException npe = new NullPointerException("Actually not, but can't throw other exceptions due to RS"); + npe.initCause(e); + throw npe; + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableMap.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableMap.java new file mode 100644 index 0000000000..90ad6c2883 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableMap.java @@ -0,0 +1,150 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import org.reactivestreams.Subscriber; + +import io.reactivex.rxjava3.annotations.Nullable; +import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.subscribers.*; +import io.reactivex.rxjava3.operators.ConditionalSubscriber; + +import java.util.Objects; + +public final class FlowableMap<T, U> extends AbstractFlowableWithUpstream<T, U> { + final Function<? super T, ? extends U> mapper; + public FlowableMap(Flowable<T> source, Function<? super T, ? extends U> mapper) { + super(source); + this.mapper = mapper; + } + + @Override + protected void subscribeActual(Subscriber<? super U> s) { + if (s instanceof ConditionalSubscriber) { + source.subscribe(new MapConditionalSubscriber<T, U>((ConditionalSubscriber<? super U>)s, mapper)); + } else { + source.subscribe(new MapSubscriber<T, U>(s, mapper)); + } + } + + static final class MapSubscriber<T, U> extends BasicFuseableSubscriber<T, U> { + final Function<? super T, ? extends U> mapper; + + MapSubscriber(Subscriber<? super U> actual, Function<? super T, ? extends U> mapper) { + super(actual); + this.mapper = mapper; + } + + @Override + public void onNext(T t) { + if (done) { + return; + } + + if (sourceMode != NONE) { + downstream.onNext(null); + return; + } + + U v; + + try { + v = Objects.requireNonNull(mapper.apply(t), "The mapper function returned a null value."); + } catch (Throwable ex) { + fail(ex); + return; + } + downstream.onNext(v); + } + + @Override + public int requestFusion(int mode) { + return transitiveBoundaryFusion(mode); + } + + @Nullable + @Override + public U poll() throws Throwable { + T t = qs.poll(); + return t != null ? Objects.requireNonNull(mapper.apply(t), "The mapper function returned a null value.") : null; + } + } + + static final class MapConditionalSubscriber<T, U> extends BasicFuseableConditionalSubscriber<T, U> { + final Function<? super T, ? extends U> mapper; + + MapConditionalSubscriber(ConditionalSubscriber<? super U> actual, Function<? super T, ? extends U> function) { + super(actual); + this.mapper = function; + } + + @Override + public void onNext(T t) { + if (done) { + return; + } + + if (sourceMode != NONE) { + downstream.onNext(null); + return; + } + + U v; + + try { + v = Objects.requireNonNull(mapper.apply(t), "The mapper function returned a null value."); + } catch (Throwable ex) { + fail(ex); + return; + } + downstream.onNext(v); + } + + @Override + public boolean tryOnNext(T t) { + if (done) { + return true; + } + + if (sourceMode != NONE) { + downstream.tryOnNext(null); + return true; + } + + U v; + + try { + v = Objects.requireNonNull(mapper.apply(t), "The mapper function returned a null value."); + } catch (Throwable ex) { + fail(ex); + return true; + } + return downstream.tryOnNext(v); + } + + @Override + public int requestFusion(int mode) { + return transitiveBoundaryFusion(mode); + } + + @Nullable + @Override + public U poll() throws Throwable { + T t = qs.poll(); + return t != null ? Objects.requireNonNull(mapper.apply(t), "The mapper function returned a null value.") : null; + } + } + +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableMapNotification.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableMapNotification.java new file mode 100644 index 0000000000..be2d78062e --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableMapNotification.java @@ -0,0 +1,111 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import org.reactivestreams.Subscriber; + +import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.subscribers.SinglePostCompleteSubscriber; + +import java.util.Objects; + +public final class FlowableMapNotification<T, R> extends AbstractFlowableWithUpstream<T, R> { + + final Function<? super T, ? extends R> onNextMapper; + final Function<? super Throwable, ? extends R> onErrorMapper; + final Supplier<? extends R> onCompleteSupplier; + + public FlowableMapNotification( + Flowable<T> source, + Function<? super T, ? extends R> onNextMapper, + Function<? super Throwable, ? extends R> onErrorMapper, + Supplier<? extends R> onCompleteSupplier) { + super(source); + this.onNextMapper = onNextMapper; + this.onErrorMapper = onErrorMapper; + this.onCompleteSupplier = onCompleteSupplier; + } + + @Override + protected void subscribeActual(Subscriber<? super R> s) { + source.subscribe(new MapNotificationSubscriber<>(s, onNextMapper, onErrorMapper, onCompleteSupplier)); + } + + static final class MapNotificationSubscriber<T, R> + extends SinglePostCompleteSubscriber<T, R> { + + private static final long serialVersionUID = 2757120512858778108L; + final Function<? super T, ? extends R> onNextMapper; + final Function<? super Throwable, ? extends R> onErrorMapper; + final Supplier<? extends R> onCompleteSupplier; + + MapNotificationSubscriber(Subscriber<? super R> actual, + Function<? super T, ? extends R> onNextMapper, + Function<? super Throwable, ? extends R> onErrorMapper, + Supplier<? extends R> onCompleteSupplier) { + super(actual); + this.onNextMapper = onNextMapper; + this.onErrorMapper = onErrorMapper; + this.onCompleteSupplier = onCompleteSupplier; + } + + @Override + public void onNext(T t) { + R p; + + try { + p = Objects.requireNonNull(onNextMapper.apply(t), "The onNext publisher returned is null"); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + downstream.onError(e); + return; + } + + produced++; + downstream.onNext(p); + } + + @Override + public void onError(Throwable t) { + R p; + + try { + p = Objects.requireNonNull(onErrorMapper.apply(t), "The onError publisher returned is null"); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + downstream.onError(new CompositeException(t, e)); + return; + } + + complete(p); + } + + @Override + public void onComplete() { + R p; + + try { + p = Objects.requireNonNull(onCompleteSupplier.get(), "The onComplete publisher returned is null"); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + downstream.onError(e); + return; + } + + complete(p); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableMapPublisher.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableMapPublisher.java new file mode 100644 index 0000000000..cc8ccb8fe8 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableMapPublisher.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.operators.flowable.FlowableMap.MapSubscriber; + +/** + * Map working with an arbitrary Publisher source. + * <p>History: 2.0.7 - experimental + * @param <T> the input value type + * @param <U> the output value type + * @since 2.1 + */ +public final class FlowableMapPublisher<T, U> extends Flowable<U> { + + final Publisher<T> source; + + final Function<? super T, ? extends U> mapper; + public FlowableMapPublisher(Publisher<T> source, Function<? super T, ? extends U> mapper) { + this.source = source; + this.mapper = mapper; + } + + @Override + protected void subscribeActual(Subscriber<? super U> s) { + source.subscribe(new MapSubscriber<T, U>(s, mapper)); + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableMaterialize.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableMaterialize.java new file mode 100644 index 0000000000..7a806b8d5d --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableMaterialize.java @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import org.reactivestreams.Subscriber; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.internal.subscribers.SinglePostCompleteSubscriber; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +public final class FlowableMaterialize<T> extends AbstractFlowableWithUpstream<T, Notification<T>> { + + public FlowableMaterialize(Flowable<T> source) { + super(source); + } + + @Override + protected void subscribeActual(Subscriber<? super Notification<T>> s) { + source.subscribe(new MaterializeSubscriber<>(s)); + } + + static final class MaterializeSubscriber<T> extends SinglePostCompleteSubscriber<T, Notification<T>> { + + private static final long serialVersionUID = -3740826063558713822L; + + MaterializeSubscriber(Subscriber<? super Notification<T>> downstream) { + super(downstream); + } + + @Override + public void onNext(T t) { + produced++; + downstream.onNext(Notification.createOnNext(t)); + } + + @Override + public void onError(Throwable t) { + complete(Notification.createOnError(t)); + } + + @Override + public void onComplete() { + complete(Notification.createOnComplete()); + } + + @Override + protected void onDrop(Notification<T> n) { + if (n.isOnError()) { + RxJavaPlugins.onError(n.getError()); + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableMergeWithCompletable.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableMergeWithCompletable.java new file mode 100644 index 0000000000..ccd48a78fa --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableMergeWithCompletable.java @@ -0,0 +1,152 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import java.util.concurrent.atomic.*; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; +import io.reactivex.rxjava3.internal.subscriptions.SubscriptionHelper; +import io.reactivex.rxjava3.internal.util.*; + +/** + * Merges a Flowable and a Completable by emitting the items of the Flowable and waiting until + * both the Flowable and Completable complete normally. + * <p>History: 2.1.10 - experimental + * @param <T> the element type of the Flowable + * @since 2.2 + */ +public final class FlowableMergeWithCompletable<T> extends AbstractFlowableWithUpstream<T, T> { + + final CompletableSource other; + + public FlowableMergeWithCompletable(Flowable<T> source, CompletableSource other) { + super(source); + this.other = other; + } + + @Override + protected void subscribeActual(Subscriber<? super T> subscriber) { + MergeWithSubscriber<T> parent = new MergeWithSubscriber<>(subscriber); + subscriber.onSubscribe(parent); + source.subscribe(parent); + other.subscribe(parent.otherObserver); + } + + static final class MergeWithSubscriber<T> extends AtomicInteger + implements FlowableSubscriber<T>, Subscription { + + private static final long serialVersionUID = -4592979584110982903L; + + final Subscriber<? super T> downstream; + + final AtomicReference<Subscription> mainSubscription; + + final OtherObserver otherObserver; + + final AtomicThrowable errors; + + final AtomicLong requested; + + volatile boolean mainDone; + + volatile boolean otherDone; + + MergeWithSubscriber(Subscriber<? super T> downstream) { + this.downstream = downstream; + this.mainSubscription = new AtomicReference<>(); + this.otherObserver = new OtherObserver(this); + this.errors = new AtomicThrowable(); + this.requested = new AtomicLong(); + } + + @Override + public void onSubscribe(Subscription s) { + SubscriptionHelper.deferredSetOnce(mainSubscription, requested, s); + } + + @Override + public void onNext(T t) { + HalfSerializer.onNext(downstream, t, this, errors); + } + + @Override + public void onError(Throwable ex) { + DisposableHelper.dispose(otherObserver); + HalfSerializer.onError(downstream, ex, this, errors); + } + + @Override + public void onComplete() { + mainDone = true; + if (otherDone) { + HalfSerializer.onComplete(downstream, this, errors); + } + } + + @Override + public void request(long n) { + SubscriptionHelper.deferredRequest(mainSubscription, requested, n); + } + + @Override + public void cancel() { + SubscriptionHelper.cancel(mainSubscription); + DisposableHelper.dispose(otherObserver); + errors.tryTerminateAndReport(); + } + + void otherError(Throwable ex) { + SubscriptionHelper.cancel(mainSubscription); + HalfSerializer.onError(downstream, ex, this, errors); + } + + void otherComplete() { + otherDone = true; + if (mainDone) { + HalfSerializer.onComplete(downstream, this, errors); + } + } + + static final class OtherObserver extends AtomicReference<Disposable> + implements CompletableObserver { + + private static final long serialVersionUID = -2935427570954647017L; + + final MergeWithSubscriber<?> parent; + + OtherObserver(MergeWithSubscriber<?> parent) { + this.parent = parent; + } + + @Override + public void onSubscribe(Disposable d) { + DisposableHelper.setOnce(this, d); + } + + @Override + public void onError(Throwable e) { + parent.otherError(e); + } + + @Override + public void onComplete() { + parent.otherComplete(); + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableMergeWithMaybe.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableMergeWithMaybe.java new file mode 100644 index 0000000000..e7581bcaed --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableMergeWithMaybe.java @@ -0,0 +1,353 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import java.util.concurrent.atomic.*; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; +import io.reactivex.rxjava3.internal.subscriptions.SubscriptionHelper; +import io.reactivex.rxjava3.internal.util.*; +import io.reactivex.rxjava3.operators.SimplePlainQueue; +import io.reactivex.rxjava3.operators.SpscArrayQueue; + +/** + * Merges an Observable and a Maybe by emitting the items of the Observable and the success + * value of the Maybe and waiting until both the Observable and Maybe terminate normally. + * <p>History: 2.1.10 - experimental + * @param <T> the element type of the Observable + * @since 2.2 + */ +public final class FlowableMergeWithMaybe<T> extends AbstractFlowableWithUpstream<T, T> { + + final MaybeSource<? extends T> other; + + public FlowableMergeWithMaybe(Flowable<T> source, MaybeSource<? extends T> other) { + super(source); + this.other = other; + } + + @Override + protected void subscribeActual(Subscriber<? super T> subscriber) { + MergeWithObserver<T> parent = new MergeWithObserver<>(subscriber); + subscriber.onSubscribe(parent); + source.subscribe(parent); + other.subscribe(parent.otherObserver); + } + + static final class MergeWithObserver<T> extends AtomicInteger + implements FlowableSubscriber<T>, Subscription { + + private static final long serialVersionUID = -4592979584110982903L; + + final Subscriber<? super T> downstream; + + final AtomicReference<Subscription> mainSubscription; + + final OtherObserver<T> otherObserver; + + final AtomicThrowable errors; + + final AtomicLong requested; + + final int prefetch; + + final int limit; + + volatile SimplePlainQueue<T> queue; + + T singleItem; + + volatile boolean cancelled; + + volatile boolean mainDone; + + volatile int otherState; + + long emitted; + + int consumed; + + static final int OTHER_STATE_HAS_VALUE = 1; + + static final int OTHER_STATE_CONSUMED_OR_EMPTY = 2; + + MergeWithObserver(Subscriber<? super T> downstream) { + this.downstream = downstream; + this.mainSubscription = new AtomicReference<>(); + this.otherObserver = new OtherObserver<>(this); + this.errors = new AtomicThrowable(); + this.requested = new AtomicLong(); + this.prefetch = bufferSize(); + this.limit = prefetch - (prefetch >> 2); + } + + @Override + public void onSubscribe(Subscription s) { + SubscriptionHelper.setOnce(mainSubscription, s, prefetch); + } + + @Override + public void onNext(T t) { + if (compareAndSet(0, 1)) { + long e = emitted; + if (requested.get() != e) { + SimplePlainQueue<T> q = queue; + if (q == null || q.isEmpty()) { + + emitted = e + 1; + downstream.onNext(t); + + int c = consumed + 1; + if (c == limit) { + consumed = 0; + mainSubscription.get().request(c); + } else { + consumed = c; + } + } else { + q.offer(t); + } + } else { + SimplePlainQueue<T> q = getOrCreateQueue(); + q.offer(t); + } + if (decrementAndGet() == 0) { + return; + } + } else { + SimplePlainQueue<T> q = getOrCreateQueue(); + q.offer(t); + if (getAndIncrement() != 0) { + return; + } + } + drainLoop(); + } + + @Override + public void onError(Throwable ex) { + if (errors.tryAddThrowableOrReport(ex)) { + DisposableHelper.dispose(otherObserver); + drain(); + } + } + + @Override + public void onComplete() { + mainDone = true; + drain(); + } + + @Override + public void request(long n) { + BackpressureHelper.add(requested, n); + drain(); + } + + @Override + public void cancel() { + cancelled = true; + SubscriptionHelper.cancel(mainSubscription); + DisposableHelper.dispose(otherObserver); + errors.tryTerminateAndReport(); + if (getAndIncrement() == 0) { + queue = null; + singleItem = null; + } + } + + void otherSuccess(T value) { + if (compareAndSet(0, 1)) { + long e = emitted; + if (requested.get() != e) { + + emitted = e + 1; + downstream.onNext(value); + otherState = OTHER_STATE_CONSUMED_OR_EMPTY; + } else { + singleItem = value; + otherState = OTHER_STATE_HAS_VALUE; + if (decrementAndGet() == 0) { + return; + } + } + } else { + singleItem = value; + otherState = OTHER_STATE_HAS_VALUE; + if (getAndIncrement() != 0) { + return; + } + } + drainLoop(); + } + + void otherError(Throwable ex) { + if (errors.tryAddThrowableOrReport(ex)) { + SubscriptionHelper.cancel(mainSubscription); + drain(); + } + } + + void otherComplete() { + otherState = OTHER_STATE_CONSUMED_OR_EMPTY; + drain(); + } + + SimplePlainQueue<T> getOrCreateQueue() { + SimplePlainQueue<T> q = queue; + if (q == null) { + q = new SpscArrayQueue<>(bufferSize()); + queue = q; + } + return q; + } + + void drain() { + if (getAndIncrement() == 0) { + drainLoop(); + } + } + + void drainLoop() { + Subscriber<? super T> actual = this.downstream; + int missed = 1; + long e = emitted; + int c = consumed; + int lim = limit; + for (;;) { + + long r = requested.get(); + + while (e != r) { + if (cancelled) { + singleItem = null; + queue = null; + return; + } + + if (errors.get() != null) { + singleItem = null; + queue = null; + errors.tryTerminateConsumer(downstream); + return; + } + + int os = otherState; + if (os == OTHER_STATE_HAS_VALUE) { + T v = singleItem; + singleItem = null; + otherState = OTHER_STATE_CONSUMED_OR_EMPTY; + os = OTHER_STATE_CONSUMED_OR_EMPTY; + actual.onNext(v); + + e++; + continue; + } + + boolean d = mainDone; + SimplePlainQueue<T> q = queue; + T v = q != null ? q.poll() : null; + boolean empty = v == null; + + if (d && empty && os == OTHER_STATE_CONSUMED_OR_EMPTY) { + queue = null; + actual.onComplete(); + return; + } + + if (empty) { + break; + } + + actual.onNext(v); + + e++; + + if (++c == lim) { + c = 0; + mainSubscription.get().request(lim); + } + } + + if (e == r) { + if (cancelled) { + singleItem = null; + queue = null; + return; + } + + if (errors.get() != null) { + singleItem = null; + queue = null; + errors.tryTerminateConsumer(downstream); + return; + } + + boolean d = mainDone; + SimplePlainQueue<T> q = queue; + boolean empty = q == null || q.isEmpty(); + + if (d && empty && otherState == 2) { + queue = null; + actual.onComplete(); + return; + } + } + + emitted = e; + consumed = c; + missed = addAndGet(-missed); + if (missed == 0) { + break; + } + } + } + + static final class OtherObserver<T> extends AtomicReference<Disposable> + implements MaybeObserver<T> { + + private static final long serialVersionUID = -2935427570954647017L; + + final MergeWithObserver<T> parent; + + OtherObserver(MergeWithObserver<T> parent) { + this.parent = parent; + } + + @Override + public void onSubscribe(Disposable d) { + DisposableHelper.setOnce(this, d); + } + + @Override + public void onSuccess(T t) { + parent.otherSuccess(t); + } + + @Override + public void onError(Throwable e) { + parent.otherError(e); + } + + @Override + public void onComplete() { + parent.otherComplete(); + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableMergeWithSingle.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableMergeWithSingle.java new file mode 100644 index 0000000000..b0f3e8b3ed --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableMergeWithSingle.java @@ -0,0 +1,343 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import java.util.concurrent.atomic.*; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; +import io.reactivex.rxjava3.internal.subscriptions.SubscriptionHelper; +import io.reactivex.rxjava3.internal.util.*; +import io.reactivex.rxjava3.operators.SimplePlainQueue; +import io.reactivex.rxjava3.operators.SpscArrayQueue; + +/** + * Merges an Observable and a Maybe by emitting the items of the Observable and the success + * value of the Maybe and waiting until both the Observable and Maybe terminate normally. + * <p>History: 2.1.10 - experimental + * @param <T> the element type of the Observable + * @since 2.2 + */ +public final class FlowableMergeWithSingle<T> extends AbstractFlowableWithUpstream<T, T> { + + final SingleSource<? extends T> other; + + public FlowableMergeWithSingle(Flowable<T> source, SingleSource<? extends T> other) { + super(source); + this.other = other; + } + + @Override + protected void subscribeActual(Subscriber<? super T> subscriber) { + MergeWithObserver<T> parent = new MergeWithObserver<>(subscriber); + subscriber.onSubscribe(parent); + source.subscribe(parent); + other.subscribe(parent.otherObserver); + } + + static final class MergeWithObserver<T> extends AtomicInteger + implements FlowableSubscriber<T>, Subscription { + + private static final long serialVersionUID = -4592979584110982903L; + + final Subscriber<? super T> downstream; + + final AtomicReference<Subscription> mainSubscription; + + final OtherObserver<T> otherObserver; + + final AtomicThrowable errors; + + final AtomicLong requested; + + final int prefetch; + + final int limit; + + volatile SimplePlainQueue<T> queue; + + T singleItem; + + volatile boolean cancelled; + + volatile boolean mainDone; + + volatile int otherState; + + long emitted; + + int consumed; + + static final int OTHER_STATE_HAS_VALUE = 1; + + static final int OTHER_STATE_CONSUMED_OR_EMPTY = 2; + + MergeWithObserver(Subscriber<? super T> downstream) { + this.downstream = downstream; + this.mainSubscription = new AtomicReference<>(); + this.otherObserver = new OtherObserver<>(this); + this.errors = new AtomicThrowable(); + this.requested = new AtomicLong(); + this.prefetch = bufferSize(); + this.limit = prefetch - (prefetch >> 2); + } + + @Override + public void onSubscribe(Subscription s) { + SubscriptionHelper.setOnce(mainSubscription, s, prefetch); + } + + @Override + public void onNext(T t) { + if (compareAndSet(0, 1)) { + long e = emitted; + if (requested.get() != e) { + SimplePlainQueue<T> q = queue; + if (q == null || q.isEmpty()) { + + emitted = e + 1; + downstream.onNext(t); + + int c = consumed + 1; + if (c == limit) { + consumed = 0; + mainSubscription.get().request(c); + } else { + consumed = c; + } + } else { + q.offer(t); + } + } else { + SimplePlainQueue<T> q = getOrCreateQueue(); + q.offer(t); + } + if (decrementAndGet() == 0) { + return; + } + } else { + SimplePlainQueue<T> q = getOrCreateQueue(); + q.offer(t); + if (getAndIncrement() != 0) { + return; + } + } + drainLoop(); + } + + @Override + public void onError(Throwable ex) { + if (errors.tryAddThrowableOrReport(ex)) { + DisposableHelper.dispose(otherObserver); + drain(); + } + } + + @Override + public void onComplete() { + mainDone = true; + drain(); + } + + @Override + public void request(long n) { + BackpressureHelper.add(requested, n); + drain(); + } + + @Override + public void cancel() { + cancelled = true; + SubscriptionHelper.cancel(mainSubscription); + DisposableHelper.dispose(otherObserver); + errors.tryTerminateAndReport(); + if (getAndIncrement() == 0) { + queue = null; + singleItem = null; + } + } + + void otherSuccess(T value) { + if (compareAndSet(0, 1)) { + long e = emitted; + if (requested.get() != e) { + + emitted = e + 1; + downstream.onNext(value); + otherState = OTHER_STATE_CONSUMED_OR_EMPTY; + } else { + singleItem = value; + otherState = OTHER_STATE_HAS_VALUE; + if (decrementAndGet() == 0) { + return; + } + } + } else { + singleItem = value; + otherState = OTHER_STATE_HAS_VALUE; + if (getAndIncrement() != 0) { + return; + } + } + drainLoop(); + } + + void otherError(Throwable ex) { + if (errors.tryAddThrowableOrReport(ex)) { + SubscriptionHelper.cancel(mainSubscription); + drain(); + } + } + + SimplePlainQueue<T> getOrCreateQueue() { + SimplePlainQueue<T> q = queue; + if (q == null) { + q = new SpscArrayQueue<>(bufferSize()); + queue = q; + } + return q; + } + + void drain() { + if (getAndIncrement() == 0) { + drainLoop(); + } + } + + void drainLoop() { + Subscriber<? super T> actual = this.downstream; + int missed = 1; + long e = emitted; + int c = consumed; + int lim = limit; + for (;;) { + + long r = requested.get(); + + while (e != r) { + if (cancelled) { + singleItem = null; + queue = null; + return; + } + + if (errors.get() != null) { + singleItem = null; + queue = null; + errors.tryTerminateConsumer(downstream); + return; + } + + int os = otherState; + if (os == OTHER_STATE_HAS_VALUE) { + T v = singleItem; + singleItem = null; + otherState = OTHER_STATE_CONSUMED_OR_EMPTY; + os = OTHER_STATE_CONSUMED_OR_EMPTY; + actual.onNext(v); + + e++; + continue; + } + + boolean d = mainDone; + SimplePlainQueue<T> q = queue; + T v = q != null ? q.poll() : null; + boolean empty = v == null; + + if (d && empty && os == OTHER_STATE_CONSUMED_OR_EMPTY) { + queue = null; + actual.onComplete(); + return; + } + + if (empty) { + break; + } + + actual.onNext(v); + + e++; + + if (++c == lim) { + c = 0; + mainSubscription.get().request(lim); + } + } + + if (e == r) { + if (cancelled) { + singleItem = null; + queue = null; + return; + } + + if (errors.get() != null) { + singleItem = null; + queue = null; + errors.tryTerminateConsumer(downstream); + return; + } + + boolean d = mainDone; + SimplePlainQueue<T> q = queue; + boolean empty = q == null || q.isEmpty(); + + if (d && empty && otherState == 2) { + queue = null; + actual.onComplete(); + return; + } + } + + emitted = e; + consumed = c; + missed = addAndGet(-missed); + if (missed == 0) { + break; + } + } + } + + static final class OtherObserver<T> extends AtomicReference<Disposable> + implements SingleObserver<T> { + + private static final long serialVersionUID = -2935427570954647017L; + + final MergeWithObserver<T> parent; + + OtherObserver(MergeWithObserver<T> parent) { + this.parent = parent; + } + + @Override + public void onSubscribe(Disposable d) { + DisposableHelper.setOnce(this, d); + } + + @Override + public void onSuccess(T t) { + parent.otherSuccess(t); + } + + @Override + public void onError(Throwable e) { + parent.otherError(e); + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableNever.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableNever.java new file mode 100644 index 0000000000..83aeb1533d --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableNever.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import org.reactivestreams.Subscriber; + +import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.internal.subscriptions.EmptySubscription; + +public final class FlowableNever extends Flowable<Object> { + public static final Flowable<Object> INSTANCE = new FlowableNever(); + + private FlowableNever() { + } + + @Override + public void subscribeActual(Subscriber<? super Object> s) { + s.onSubscribe(EmptySubscription.INSTANCE); + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableObserveOn.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableObserveOn.java new file mode 100644 index 0000000000..576debbf2b --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableObserveOn.java @@ -0,0 +1,715 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import java.util.concurrent.atomic.AtomicLong; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.annotations.Nullable; +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.core.Scheduler.Worker; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.internal.subscriptions.*; +import io.reactivex.rxjava3.internal.util.BackpressureHelper; +import io.reactivex.rxjava3.operators.ConditionalSubscriber; +import io.reactivex.rxjava3.operators.QueueSubscription; +import io.reactivex.rxjava3.operators.SimpleQueue; +import io.reactivex.rxjava3.operators.SpscArrayQueue; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +public final class FlowableObserveOn<T> extends AbstractFlowableWithUpstream<T, T> { +final Scheduler scheduler; + + final boolean delayError; + + final int prefetch; + + public FlowableObserveOn( + Flowable<T> source, + Scheduler scheduler, + boolean delayError, + int prefetch) { + super(source); + this.scheduler = scheduler; + this.delayError = delayError; + this.prefetch = prefetch; + } + + @Override + public void subscribeActual(Subscriber<? super T> s) { + Worker worker = scheduler.createWorker(); + + if (s instanceof ConditionalSubscriber) { + source.subscribe(new ObserveOnConditionalSubscriber<>( + (ConditionalSubscriber<? super T>) s, worker, delayError, prefetch)); + } else { + source.subscribe(new ObserveOnSubscriber<>(s, worker, delayError, prefetch)); + } + } + + abstract static class BaseObserveOnSubscriber<T> + extends BasicIntQueueSubscription<T> + implements FlowableSubscriber<T>, Runnable { + private static final long serialVersionUID = -8241002408341274697L; + + final Worker worker; + + final boolean delayError; + + final int prefetch; + + final int limit; + + final AtomicLong requested; + + Subscription upstream; + + SimpleQueue<T> queue; + + volatile boolean cancelled; + + volatile boolean done; + + Throwable error; + + int sourceMode; + + long produced; + + boolean outputFused; + + BaseObserveOnSubscriber( + Worker worker, + boolean delayError, + int prefetch) { + this.worker = worker; + this.delayError = delayError; + this.prefetch = prefetch; + this.requested = new AtomicLong(); + this.limit = prefetch - (prefetch >> 2); + } + + @Override + public final void onNext(T t) { + if (done) { + return; + } + if (sourceMode == ASYNC) { + trySchedule(); + return; + } + if (!queue.offer(t)) { + upstream.cancel(); + + error = new QueueOverflowException(); + done = true; + } + trySchedule(); + } + + @Override + public final void onError(Throwable t) { + if (done) { + RxJavaPlugins.onError(t); + return; + } + error = t; + done = true; + trySchedule(); + } + + @Override + public final void onComplete() { + if (!done) { + done = true; + trySchedule(); + } + } + + @Override + public final void request(long n) { + if (SubscriptionHelper.validate(n)) { + BackpressureHelper.add(requested, n); + trySchedule(); + } + } + + @Override + public final void cancel() { + if (cancelled) { + return; + } + + cancelled = true; + upstream.cancel(); + worker.dispose(); + + if (!outputFused && getAndIncrement() == 0) { + queue.clear(); + } + } + + final void trySchedule() { + if (getAndIncrement() != 0) { + return; + } + worker.schedule(this); + } + + @Override + public final void run() { + if (outputFused) { + runBackfused(); + } else if (sourceMode == SYNC) { + runSync(); + } else { + runAsync(); + } + } + + abstract void runBackfused(); + + abstract void runSync(); + + abstract void runAsync(); + + final boolean checkTerminated(boolean d, boolean empty, Subscriber<?> a) { + if (cancelled) { + clear(); + return true; + } + if (d) { + if (delayError) { + if (empty) { + cancelled = true; + Throwable e = error; + if (e != null) { + a.onError(e); + } else { + a.onComplete(); + } + worker.dispose(); + return true; + } + } else { + Throwable e = error; + if (e != null) { + cancelled = true; + clear(); + a.onError(e); + worker.dispose(); + return true; + } else + if (empty) { + cancelled = true; + a.onComplete(); + worker.dispose(); + return true; + } + } + } + + return false; + } + + @Override + public final int requestFusion(int requestedMode) { + if ((requestedMode & ASYNC) != 0) { + outputFused = true; + return ASYNC; + } + return NONE; + } + + @Override + public final void clear() { + queue.clear(); + } + + @Override + public final boolean isEmpty() { + return queue.isEmpty(); + } + } + + static final class ObserveOnSubscriber<T> extends BaseObserveOnSubscriber<T> { + + private static final long serialVersionUID = -4547113800637756442L; + + final Subscriber<? super T> downstream; + + ObserveOnSubscriber( + Subscriber<? super T> actual, + Worker worker, + boolean delayError, + int prefetch) { + super(worker, delayError, prefetch); + this.downstream = actual; + } + + @Override + public void onSubscribe(Subscription s) { + if (SubscriptionHelper.validate(this.upstream, s)) { + this.upstream = s; + + if (s instanceof QueueSubscription) { + @SuppressWarnings("unchecked") + QueueSubscription<T> f = (QueueSubscription<T>) s; + + int m = f.requestFusion(ANY | BOUNDARY); + + if (m == SYNC) { + sourceMode = SYNC; + queue = f; + done = true; + + downstream.onSubscribe(this); + return; + } else + if (m == ASYNC) { + sourceMode = ASYNC; + queue = f; + + downstream.onSubscribe(this); + + s.request(prefetch); + + return; + } + } + + queue = new SpscArrayQueue<>(prefetch); + + downstream.onSubscribe(this); + + s.request(prefetch); + } + } + + @Override + void runSync() { + int missed = 1; + + final Subscriber<? super T> a = downstream; + final SimpleQueue<T> q = queue; + + long e = produced; + + for (;;) { + + long r = requested.get(); + + while (e != r) { + T v; + + try { + v = q.poll(); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + cancelled = true; + upstream.cancel(); + a.onError(ex); + worker.dispose(); + return; + } + + if (cancelled) { + return; + } + if (v == null) { + cancelled = true; + a.onComplete(); + worker.dispose(); + return; + } + + a.onNext(v); + + e++; + } + + if (cancelled) { + return; + } + + if (q.isEmpty()) { + cancelled = true; + a.onComplete(); + worker.dispose(); + return; + } + + produced = e; + missed = addAndGet(-missed); + if (missed == 0) { + break; + } + } + } + + @Override + void runAsync() { + int missed = 1; + + final Subscriber<? super T> a = downstream; + final SimpleQueue<T> q = queue; + + long e = produced; + + for (;;) { + + long r = requested.get(); + + while (e != r) { + boolean d = done; + T v; + + try { + v = q.poll(); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + + cancelled = true; + upstream.cancel(); + q.clear(); + + a.onError(ex); + worker.dispose(); + return; + } + + boolean empty = v == null; + + if (checkTerminated(d, empty, a)) { + return; + } + + if (empty) { + break; + } + + a.onNext(v); + + e++; + if (e == limit) { + if (r != Long.MAX_VALUE) { + r = requested.addAndGet(-e); + } + upstream.request(e); + e = 0L; + } + } + + if (e == r && checkTerminated(done, q.isEmpty(), a)) { + return; + } + + int w = get(); + if (missed == w) { + produced = e; + missed = addAndGet(-missed); + if (missed == 0) { + break; + } + } else { + missed = w; + } + } + } + + @Override + void runBackfused() { + int missed = 1; + + for (;;) { + + if (cancelled) { + return; + } + + boolean d = done; + + downstream.onNext(null); + + if (d) { + cancelled = true; + Throwable e = error; + if (e != null) { + downstream.onError(e); + } else { + downstream.onComplete(); + } + worker.dispose(); + return; + } + + missed = addAndGet(-missed); + if (missed == 0) { + break; + } + } + } + + @Nullable + @Override + public T poll() throws Throwable { + T v = queue.poll(); + if (v != null && sourceMode != SYNC) { + long p = produced + 1; + if (p == limit) { + produced = 0; + upstream.request(p); + } else { + produced = p; + } + } + return v; + } + + } + + static final class ObserveOnConditionalSubscriber<T> + extends BaseObserveOnSubscriber<T> { + + private static final long serialVersionUID = 644624475404284533L; + + final ConditionalSubscriber<? super T> downstream; + + long consumed; + + ObserveOnConditionalSubscriber( + ConditionalSubscriber<? super T> actual, + Worker worker, + boolean delayError, + int prefetch) { + super(worker, delayError, prefetch); + this.downstream = actual; + } + + @Override + public void onSubscribe(Subscription s) { + if (SubscriptionHelper.validate(this.upstream, s)) { + this.upstream = s; + + if (s instanceof QueueSubscription) { + @SuppressWarnings("unchecked") + QueueSubscription<T> f = (QueueSubscription<T>) s; + + int m = f.requestFusion(ANY | BOUNDARY); + + if (m == SYNC) { + sourceMode = SYNC; + queue = f; + done = true; + + downstream.onSubscribe(this); + return; + } else + if (m == ASYNC) { + sourceMode = ASYNC; + queue = f; + + downstream.onSubscribe(this); + + s.request(prefetch); + + return; + } + } + + queue = new SpscArrayQueue<>(prefetch); + + downstream.onSubscribe(this); + + s.request(prefetch); + } + } + + @Override + void runSync() { + int missed = 1; + + final ConditionalSubscriber<? super T> a = downstream; + final SimpleQueue<T> q = queue; + + long e = produced; + + for (;;) { + + long r = requested.get(); + + while (e != r) { + T v; + try { + v = q.poll(); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + cancelled = true; + upstream.cancel(); + a.onError(ex); + worker.dispose(); + return; + } + + if (cancelled) { + return; + } + if (v == null) { + cancelled = true; + a.onComplete(); + worker.dispose(); + return; + } + + if (a.tryOnNext(v)) { + e++; + } + } + + if (cancelled) { + return; + } + + if (q.isEmpty()) { + cancelled = true; + a.onComplete(); + worker.dispose(); + return; + } + + produced = e; + missed = addAndGet(-missed); + if (missed == 0) { + break; + } + } + } + + @Override + void runAsync() { + int missed = 1; + + final ConditionalSubscriber<? super T> a = downstream; + final SimpleQueue<T> q = queue; + + long emitted = produced; + long polled = consumed; + + for (;;) { + + long r = requested.get(); + + while (emitted != r) { + boolean d = done; + T v; + try { + v = q.poll(); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + + cancelled = true; + upstream.cancel(); + q.clear(); + + a.onError(ex); + worker.dispose(); + return; + } + boolean empty = v == null; + + if (checkTerminated(d, empty, a)) { + return; + } + + if (empty) { + break; + } + + if (a.tryOnNext(v)) { + emitted++; + } + + polled++; + + if (polled == limit) { + upstream.request(polled); + polled = 0L; + } + } + + if (emitted == r && checkTerminated(done, q.isEmpty(), a)) { + return; + } + + produced = emitted; + consumed = polled; + missed = addAndGet(-missed); + if (missed == 0) { + break; + } + } + + } + + @Override + void runBackfused() { + int missed = 1; + + for (;;) { + + if (cancelled) { + return; + } + + boolean d = done; + + downstream.onNext(null); + + if (d) { + cancelled = true; + Throwable e = error; + if (e != null) { + downstream.onError(e); + } else { + downstream.onComplete(); + } + worker.dispose(); + return; + } + + missed = addAndGet(-missed); + if (missed == 0) { + break; + } + } + } + + @Nullable + @Override + public T poll() throws Throwable { + T v = queue.poll(); + if (v != null && sourceMode != SYNC) { + long p = consumed + 1; + if (p == limit) { + consumed = 0; + upstream.request(p); + } else { + consumed = p; + } + } + return v; + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableOnBackpressureBuffer.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableOnBackpressureBuffer.java new file mode 100644 index 0000000000..db58b68a10 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableOnBackpressureBuffer.java @@ -0,0 +1,276 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import java.util.concurrent.atomic.AtomicLong; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.annotations.Nullable; +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.subscriptions.*; +import io.reactivex.rxjava3.internal.util.BackpressureHelper; +import io.reactivex.rxjava3.operators.*; + +public final class FlowableOnBackpressureBuffer<T> extends AbstractFlowableWithUpstream<T, T> { + final int bufferSize; + final boolean unbounded; + final boolean delayError; + final Action onOverflow; + final Consumer<? super T> onDropped; + + public FlowableOnBackpressureBuffer(Flowable<T> source, int bufferSize, boolean unbounded, + boolean delayError, Action onOverflow, Consumer<? super T> onDropped) { + super(source); + this.bufferSize = bufferSize; + this.unbounded = unbounded; + this.delayError = delayError; + this.onOverflow = onOverflow; + this.onDropped = onDropped; + } + + @Override + protected void subscribeActual(Subscriber<? super T> s) { + source.subscribe(new BackpressureBufferSubscriber<>(s, bufferSize, unbounded, delayError, onOverflow, onDropped)); + } + + static final class BackpressureBufferSubscriber<T> extends BasicIntQueueSubscription<T> implements FlowableSubscriber<T> { + + private static final long serialVersionUID = -2514538129242366402L; + + final Subscriber<? super T> downstream; + final SimplePlainQueue<T> queue; + final boolean delayError; + final Action onOverflow; + final Consumer<? super T> onDropped; + + Subscription upstream; + + volatile boolean cancelled; + + volatile boolean done; + Throwable error; + + final AtomicLong requested = new AtomicLong(); + + boolean outputFused; + + BackpressureBufferSubscriber(Subscriber<? super T> actual, int bufferSize, + boolean unbounded, boolean delayError, Action onOverflow, Consumer<? super T> onDropped) { + this.downstream = actual; + this.onOverflow = onOverflow; + this.delayError = delayError; + this.onDropped = onDropped; + + SimplePlainQueue<T> q; + + if (unbounded) { + q = new SpscLinkedArrayQueue<>(bufferSize); + } else { + q = new SpscArrayQueue<>(bufferSize); + } + + this.queue = q; + } + + @Override + public void onSubscribe(Subscription s) { + if (SubscriptionHelper.validate(this.upstream, s)) { + this.upstream = s; + downstream.onSubscribe(this); + s.request(Long.MAX_VALUE); + } + } + + @Override + public void onNext(T t) { + if (!queue.offer(t)) { + upstream.cancel(); + MissingBackpressureException ex = new MissingBackpressureException("Buffer is full"); + try { + onOverflow.run(); + onDropped.accept(t); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + ex.initCause(e); + } + onError(ex); + return; + } + if (outputFused) { + downstream.onNext(null); + } else { + drain(); + } + } + + @Override + public void onError(Throwable t) { + error = t; + done = true; + if (outputFused) { + downstream.onError(t); + } else { + drain(); + } + } + + @Override + public void onComplete() { + done = true; + if (outputFused) { + downstream.onComplete(); + } else { + drain(); + } + } + + @Override + public void request(long n) { + if (!outputFused) { + if (SubscriptionHelper.validate(n)) { + BackpressureHelper.add(requested, n); + drain(); + } + } + } + + @Override + public void cancel() { + if (!cancelled) { + cancelled = true; + upstream.cancel(); + + if (!outputFused && getAndIncrement() == 0) { + queue.clear(); + } + } + } + + void drain() { + if (getAndIncrement() == 0) { + int missed = 1; + final SimplePlainQueue<T> q = queue; + final Subscriber<? super T> a = downstream; + for (;;) { + + if (checkTerminated(done, q.isEmpty(), a)) { + return; + } + + long r = requested.get(); + + long e = 0L; + + while (e != r) { + boolean d = done; + T v = q.poll(); + boolean empty = v == null; + + if (checkTerminated(d, empty, a)) { + return; + } + + if (empty) { + break; + } + + a.onNext(v); + + e++; + } + + if (e == r) { + boolean d = done; + boolean empty = q.isEmpty(); + + if (checkTerminated(d, empty, a)) { + return; + } + } + + if (e != 0L) { + if (r != Long.MAX_VALUE) { + requested.addAndGet(-e); + } + } + + missed = addAndGet(-missed); + if (missed == 0) { + break; + } + } + } + } + + boolean checkTerminated(boolean d, boolean empty, Subscriber<? super T> a) { + if (cancelled) { + queue.clear(); + return true; + } + if (d) { + if (delayError) { + if (empty) { + Throwable e = error; + if (e != null) { + a.onError(e); + } else { + a.onComplete(); + } + return true; + } + } else { + Throwable e = error; + if (e != null) { + queue.clear(); + a.onError(e); + return true; + } else + if (empty) { + a.onComplete(); + return true; + } + } + } + return false; + } + + @Override + public int requestFusion(int mode) { + if ((mode & ASYNC) != 0) { + outputFused = true; + return ASYNC; + } + return NONE; + } + + @Nullable + @Override + public T poll() { + return queue.poll(); + } + + @Override + public void clear() { + queue.clear(); + } + + @Override + public boolean isEmpty() { + return queue.isEmpty(); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableOnBackpressureBufferStrategy.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableOnBackpressureBufferStrategy.java new file mode 100644 index 0000000000..7963fb7d40 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableOnBackpressureBufferStrategy.java @@ -0,0 +1,302 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import java.util.*; +import java.util.concurrent.atomic.*; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.subscriptions.SubscriptionHelper; +import io.reactivex.rxjava3.internal.util.BackpressureHelper; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +/** + * Handle backpressure with a bounded buffer and custom strategy. + * + * @param <T> the input and output value type + */ +public final class FlowableOnBackpressureBufferStrategy<T> extends AbstractFlowableWithUpstream<T, T> { + + final long bufferSize; + + final Action onOverflow; + + final BackpressureOverflowStrategy strategy; + + final Consumer<? super T> onDropped; + + public FlowableOnBackpressureBufferStrategy(Flowable<T> source, + long bufferSize, Action onOverflow, BackpressureOverflowStrategy strategy, + Consumer<? super T> onDropped) { + super(source); + this.bufferSize = bufferSize; + this.onOverflow = onOverflow; + this.strategy = strategy; + this.onDropped = onDropped; + } + + @Override + protected void subscribeActual(Subscriber<? super T> s) { + source.subscribe(new OnBackpressureBufferStrategySubscriber<>(s, onOverflow, strategy, bufferSize, onDropped)); + } + + static final class OnBackpressureBufferStrategySubscriber<T> + extends AtomicInteger + implements FlowableSubscriber<T>, Subscription { + + private static final long serialVersionUID = 3240706908776709697L; + + final Subscriber<? super T> downstream; + + final Action onOverflow; + + final Consumer<? super T> onDropped; + + final BackpressureOverflowStrategy strategy; + + final long bufferSize; + + final AtomicLong requested; + + final Deque<T> deque; + + Subscription upstream; + + volatile boolean cancelled; + + volatile boolean done; + Throwable error; + + OnBackpressureBufferStrategySubscriber(Subscriber<? super T> actual, Action onOverflow, + BackpressureOverflowStrategy strategy, long bufferSize, + Consumer<? super T> onDropped) { + this.downstream = actual; + this.onOverflow = onOverflow; + this.strategy = strategy; + this.bufferSize = bufferSize; + this.requested = new AtomicLong(); + this.deque = new ArrayDeque<>(); + this.onDropped = onDropped; + } + + @Override + public void onSubscribe(Subscription s) { + if (SubscriptionHelper.validate(this.upstream, s)) { + this.upstream = s; + + downstream.onSubscribe(this); + + s.request(Long.MAX_VALUE); + } + } + + @Override + public void onNext(T t) { + if (done) { + return; + } + boolean callOnOverflow = false; + boolean callError = false; + boolean callDrain = false; + Deque<T> dq = deque; + T toDrop = null; + synchronized (dq) { + if (dq.size() == bufferSize) { + switch (strategy) { + case DROP_LATEST: + toDrop = dq.pollLast(); + dq.offer(t); + callOnOverflow = true; + break; + case DROP_OLDEST: + toDrop = dq.poll(); + dq.offer(t); + callOnOverflow = true; + break; + default: + // signal error + toDrop = t; + callError = true; + break; + } + } else { + dq.offer(t); + callDrain = true; + } + } + + if (callOnOverflow && onOverflow != null) { + try { + onOverflow.run(); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + upstream.cancel(); + onError(ex); + } + } + + if (onDropped != null && toDrop != null) { + try { + onDropped.accept(toDrop); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + upstream.cancel(); + onError(ex); + } + } + + if (callError) { + upstream.cancel(); + onError(MissingBackpressureException.createDefault()); + } + + if (callDrain) { + drain(); + } + } + + @Override + public void onError(Throwable t) { + if (done) { + RxJavaPlugins.onError(t); + return; + } + error = t; + done = true; + drain(); + } + + @Override + public void onComplete() { + done = true; + drain(); + } + + @Override + public void request(long n) { + if (SubscriptionHelper.validate(n)) { + BackpressureHelper.add(requested, n); + drain(); + } + } + + @Override + public void cancel() { + cancelled = true; + upstream.cancel(); + + if (getAndIncrement() == 0) { + clear(deque); + } + } + + void clear(Deque<T> dq) { + synchronized (dq) { + dq.clear(); + } + } + + void drain() { + if (getAndIncrement() != 0) { + return; + } + + int missed = 1; + Deque<T> dq = deque; + Subscriber<? super T> a = downstream; + for (;;) { + long r = requested.get(); + long e = 0L; + while (e != r) { + if (cancelled) { + clear(dq); + return; + } + + boolean d = done; + + T v; + + synchronized (dq) { + v = dq.poll(); + } + + boolean empty = v == null; + + if (d) { + Throwable ex = error; + if (ex != null) { + clear(dq); + a.onError(ex); + return; + } + if (empty) { + a.onComplete(); + return; + } + } + + if (empty) { + break; + } + + a.onNext(v); + + e++; + } + + if (e == r) { + if (cancelled) { + clear(dq); + return; + } + + boolean d = done; + + boolean empty; + + synchronized (dq) { + empty = dq.isEmpty(); + } + + if (d) { + Throwable ex = error; + if (ex != null) { + clear(dq); + a.onError(ex); + return; + } + if (empty) { + a.onComplete(); + return; + } + } + } + + if (e != 0L) { + BackpressureHelper.produced(requested, e); + } + + missed = addAndGet(-missed); + if (missed == 0) { + break; + } + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableOnBackpressureDrop.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableOnBackpressureDrop.java new file mode 100644 index 0000000000..799c5dbca1 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableOnBackpressureDrop.java @@ -0,0 +1,128 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import java.util.concurrent.atomic.AtomicLong; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.Consumer; +import io.reactivex.rxjava3.internal.subscriptions.SubscriptionHelper; +import io.reactivex.rxjava3.internal.util.BackpressureHelper; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +public final class FlowableOnBackpressureDrop<T> extends AbstractFlowableWithUpstream<T, T> implements Consumer<T> { + + final Consumer<? super T> onDrop; + + public FlowableOnBackpressureDrop(Flowable<T> source) { + super(source); + this.onDrop = this; + } + + public FlowableOnBackpressureDrop(Flowable<T> source, Consumer<? super T> onDrop) { + super(source); + this.onDrop = onDrop; + } + + @Override + public void accept(T t) { + // deliberately ignoring + } + + @Override + protected void subscribeActual(Subscriber<? super T> s) { + this.source.subscribe(new BackpressureDropSubscriber<>(s, onDrop)); + } + + static final class BackpressureDropSubscriber<T> + extends AtomicLong implements FlowableSubscriber<T>, Subscription { + + private static final long serialVersionUID = -6246093802440953054L; + + final Subscriber<? super T> downstream; + final Consumer<? super T> onDrop; + + Subscription upstream; + + boolean done; + + BackpressureDropSubscriber(Subscriber<? super T> actual, Consumer<? super T> onDrop) { + this.downstream = actual; + this.onDrop = onDrop; + } + + @Override + public void onSubscribe(Subscription s) { + if (SubscriptionHelper.validate(this.upstream, s)) { + this.upstream = s; + downstream.onSubscribe(this); + s.request(Long.MAX_VALUE); + } + } + + @Override + public void onNext(T t) { + if (done) { + return; + } + long r = get(); + if (r != 0L) { + downstream.onNext(t); + BackpressureHelper.produced(this, 1); + } else { + try { + onDrop.accept(t); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + cancel(); + onError(e); + } + } + } + + @Override + public void onError(Throwable t) { + if (done) { + RxJavaPlugins.onError(t); + return; + } + done = true; + downstream.onError(t); + } + + @Override + public void onComplete() { + if (done) { + return; + } + done = true; + downstream.onComplete(); + } + + @Override + public void request(long n) { + if (SubscriptionHelper.validate(n)) { + BackpressureHelper.add(this, n); + } + } + + @Override + public void cancel() { + upstream.cancel(); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableOnBackpressureError.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableOnBackpressureError.java new file mode 100644 index 0000000000..acaf165069 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableOnBackpressureError.java @@ -0,0 +1,104 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import java.util.concurrent.atomic.AtomicLong; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.MissingBackpressureException; +import io.reactivex.rxjava3.internal.subscriptions.SubscriptionHelper; +import io.reactivex.rxjava3.internal.util.BackpressureHelper; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +public final class FlowableOnBackpressureError<T> extends AbstractFlowableWithUpstream<T, T> { + + public FlowableOnBackpressureError(Flowable<T> source) { + super(source); + } + + @Override + protected void subscribeActual(Subscriber<? super T> s) { + this.source.subscribe(new BackpressureErrorSubscriber<>(s)); + } + + static final class BackpressureErrorSubscriber<T> + extends AtomicLong implements FlowableSubscriber<T>, Subscription { + private static final long serialVersionUID = -3176480756392482682L; + + final Subscriber<? super T> downstream; + Subscription upstream; + boolean done; + + BackpressureErrorSubscriber(Subscriber<? super T> downstream) { + this.downstream = downstream; + } + + @Override + public void onSubscribe(Subscription s) { + if (SubscriptionHelper.validate(this.upstream, s)) { + this.upstream = s; + downstream.onSubscribe(this); + s.request(Long.MAX_VALUE); + } + } + + @Override + public void onNext(T t) { + if (done) { + return; + } + long r = get(); + if (r != 0L) { + downstream.onNext(t); + BackpressureHelper.produced(this, 1); + } else { + upstream.cancel(); + onError(MissingBackpressureException.createDefault()); + } + } + + @Override + public void onError(Throwable t) { + if (done) { + RxJavaPlugins.onError(t); + return; + } + done = true; + downstream.onError(t); + } + + @Override + public void onComplete() { + if (done) { + return; + } + done = true; + downstream.onComplete(); + } + + @Override + public void request(long n) { + if (SubscriptionHelper.validate(n)) { + BackpressureHelper.add(this, n); + } + } + + @Override + public void cancel() { + upstream.cancel(); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableOnBackpressureLatest.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableOnBackpressureLatest.java new file mode 100644 index 0000000000..155e284e93 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableOnBackpressureLatest.java @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.Consumer; +import org.reactivestreams.Subscriber; + +public final class FlowableOnBackpressureLatest<T> extends AbstractFlowableWithUpstream<T, T> { + + final Consumer<? super T> onDropped; + + public FlowableOnBackpressureLatest(Flowable<T> source, Consumer<? super T> onDropped) { + super(source); + this.onDropped = onDropped; + } + + @Override + protected void subscribeActual(Subscriber<? super T> s) { + source.subscribe(new BackpressureLatestSubscriber<>(s, onDropped)); + } + + static final class BackpressureLatestSubscriber<T> extends AbstractBackpressureThrottlingSubscriber<T, T> { + + private static final long serialVersionUID = 163080509307634843L; + + final Consumer<? super T> onDropped; + + BackpressureLatestSubscriber(Subscriber<? super T> downstream, + Consumer<? super T> onDropped) { + super(downstream); + this.onDropped = onDropped; + } + + @Override + public void onNext(T t) { + T oldValue = current.getAndSet(t); + if (onDropped != null && oldValue != null) { + try { + onDropped.accept(oldValue); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + upstream.cancel(); + downstream.onError(ex); + } + } + drain(); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableOnBackpressureReduce.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableOnBackpressureReduce.java new file mode 100644 index 0000000000..9a6abab89c --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableOnBackpressureReduce.java @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import io.reactivex.rxjava3.annotations.NonNull; +import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.BiFunction; +import org.reactivestreams.Subscriber; + +import java.util.Objects; + +public final class FlowableOnBackpressureReduce<T> extends AbstractFlowableWithUpstream<T, T> { + + final BiFunction<T, T, T> reducer; + + public FlowableOnBackpressureReduce(@NonNull Flowable<T> source, @NonNull BiFunction<T, T, T> reducer) { + super(source); + this.reducer = reducer; + } + + @Override + protected void subscribeActual(@NonNull Subscriber<? super T> s) { + source.subscribe(new BackpressureReduceSubscriber<>(s, reducer)); + } + + static final class BackpressureReduceSubscriber<T> extends AbstractBackpressureThrottlingSubscriber<T, T> { + + private static final long serialVersionUID = 821363947659780367L; + + final BiFunction<T, T, T> reducer; + + BackpressureReduceSubscriber(@NonNull Subscriber<? super T> downstream, @NonNull BiFunction<T, T, T> reducer) { + super(downstream); + this.reducer = reducer; + } + + @Override + public void onNext(T t) { + T v = current.get(); + if (v != null) { + v = current.getAndSet(null); + } + if (v == null) { + current.lazySet(t); + } else { + try { + current.lazySet(Objects.requireNonNull(reducer.apply(v, t), "The reducer returned a null value")); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + upstream.cancel(); + onError(ex); + return; + } + } + drain(); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableOnBackpressureReduceWith.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableOnBackpressureReduceWith.java new file mode 100644 index 0000000000..faed723240 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableOnBackpressureReduceWith.java @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import io.reactivex.rxjava3.annotations.NonNull; +import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.BiFunction; +import io.reactivex.rxjava3.functions.Supplier; +import org.reactivestreams.Subscriber; + +import java.util.Objects; + +public final class FlowableOnBackpressureReduceWith<T, R> extends AbstractFlowableWithUpstream<T, R> { + + final BiFunction<R, ? super T, R> reducer; + final Supplier<R> supplier; + + public FlowableOnBackpressureReduceWith(@NonNull Flowable<T> source, + @NonNull Supplier<R> supplier, + @NonNull BiFunction<R, ? super T, R> reducer) { + super(source); + this.reducer = reducer; + this.supplier = supplier; + } + + @Override + protected void subscribeActual(@NonNull Subscriber<? super R> s) { + source.subscribe(new BackpressureReduceWithSubscriber<>(s, supplier, reducer)); + } + + static final class BackpressureReduceWithSubscriber<T, R> extends AbstractBackpressureThrottlingSubscriber<T, R> { + + private static final long serialVersionUID = 8255923705960622424L; + + final BiFunction<R, ? super T, R> reducer; + final Supplier<R> supplier; + + BackpressureReduceWithSubscriber(@NonNull Subscriber<? super R> downstream, + @NonNull Supplier<R> supplier, + @NonNull BiFunction<R, ? super T, R> reducer) { + super(downstream); + this.reducer = reducer; + this.supplier = supplier; + } + + @Override + public void onNext(T t) { + R v = current.get(); + if (v != null) { + v = current.getAndSet(null); + } + try { + if (v == null) { + current.lazySet(Objects.requireNonNull( + reducer.apply(Objects.requireNonNull(supplier.get(), "The supplier returned a null value"), t), + "The reducer returned a null value" + )); + } else { + current.lazySet(Objects.requireNonNull(reducer.apply(v, t), "The reducer returned a null value")); + } + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + upstream.cancel(); + onError(ex); + return; + } + drain(); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableOnErrorComplete.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableOnErrorComplete.java new file mode 100644 index 0000000000..a0999f25c4 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableOnErrorComplete.java @@ -0,0 +1,107 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.Predicate; +import io.reactivex.rxjava3.internal.subscriptions.SubscriptionHelper; + +/** + * Emits an onComplete if the source emits an onError and the predicate returns true for + * that Throwable. + * + * @param <T> the value type + * @since 3.0.0 + */ +public final class FlowableOnErrorComplete<T> extends AbstractFlowableWithUpstream<T, T> { + + final Predicate<? super Throwable> predicate; + + public FlowableOnErrorComplete(Flowable<T> source, + Predicate<? super Throwable> predicate) { + super(source); + this.predicate = predicate; + } + + @Override + protected void subscribeActual(Subscriber<? super T> observer) { + source.subscribe(new OnErrorCompleteSubscriber<>(observer, predicate)); + } + + public static final class OnErrorCompleteSubscriber<T> + implements FlowableSubscriber<T>, Subscription { + + final Subscriber<? super T> downstream; + + final Predicate<? super Throwable> predicate; + + Subscription upstream; + + public OnErrorCompleteSubscriber(Subscriber<? super T> actual, Predicate<? super Throwable> predicate) { + this.downstream = actual; + this.predicate = predicate; + } + + @Override + public void onSubscribe(Subscription s) { + if (SubscriptionHelper.validate(this.upstream, s)) { + this.upstream = s; + + downstream.onSubscribe(this); + } + } + + @Override + public void onNext(T value) { + downstream.onNext(value); + } + + @Override + public void onError(Throwable e) { + boolean b; + + try { + b = predicate.test(e); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + downstream.onError(new CompositeException(e, ex)); + return; + } + + if (b) { + downstream.onComplete(); + } else { + downstream.onError(e); + } + } + + @Override + public void onComplete() { + downstream.onComplete(); + } + + @Override + public void cancel() { + upstream.cancel(); + } + + @Override + public void request(long n) { + upstream.request(n); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableOnErrorNext.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableOnErrorNext.java new file mode 100644 index 0000000000..cb33b81a2f --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableOnErrorNext.java @@ -0,0 +1,119 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.subscriptions.SubscriptionArbiter; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +import java.util.Objects; + +public final class FlowableOnErrorNext<T> extends AbstractFlowableWithUpstream<T, T> { + final Function<? super Throwable, ? extends Publisher<? extends T>> nextSupplier; + + public FlowableOnErrorNext(Flowable<T> source, + Function<? super Throwable, ? extends Publisher<? extends T>> nextSupplier) { + super(source); + this.nextSupplier = nextSupplier; + } + + @Override + protected void subscribeActual(Subscriber<? super T> s) { + OnErrorNextSubscriber<T> parent = new OnErrorNextSubscriber<>(s, nextSupplier); + s.onSubscribe(parent); + source.subscribe(parent); + } + + static final class OnErrorNextSubscriber<T> + extends SubscriptionArbiter + implements FlowableSubscriber<T> { + private static final long serialVersionUID = 4063763155303814625L; + + final Subscriber<? super T> downstream; + + final Function<? super Throwable, ? extends Publisher<? extends T>> nextSupplier; + + boolean once; + + boolean done; + + long produced; + + OnErrorNextSubscriber(Subscriber<? super T> actual, Function<? super Throwable, ? extends Publisher<? extends T>> nextSupplier) { + super(false); + this.downstream = actual; + this.nextSupplier = nextSupplier; + } + + @Override + public void onSubscribe(Subscription s) { + setSubscription(s); + } + + @Override + public void onNext(T t) { + if (done) { + return; + } + if (!once) { + produced++; + } + downstream.onNext(t); + } + + @Override + public void onError(Throwable t) { + if (once) { + if (done) { + RxJavaPlugins.onError(t); + return; + } + downstream.onError(t); + return; + } + once = true; + + Publisher<? extends T> p; + + try { + p = Objects.requireNonNull(nextSupplier.apply(t), "The nextSupplier returned a null Publisher"); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + downstream.onError(new CompositeException(t, e)); + return; + } + + long mainProduced = produced; + if (mainProduced != 0L) { + produced(mainProduced); + } + + p.subscribe(this); + } + + @Override + public void onComplete() { + if (done) { + return; + } + done = true; + once = true; + downstream.onComplete(); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableOnErrorReturn.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableOnErrorReturn.java new file mode 100644 index 0000000000..85a375fac9 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableOnErrorReturn.java @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import org.reactivestreams.Subscriber; + +import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.subscribers.SinglePostCompleteSubscriber; + +import java.util.Objects; + +public final class FlowableOnErrorReturn<T> extends AbstractFlowableWithUpstream<T, T> { + final Function<? super Throwable, ? extends T> valueSupplier; + public FlowableOnErrorReturn(Flowable<T> source, Function<? super Throwable, ? extends T> valueSupplier) { + super(source); + this.valueSupplier = valueSupplier; + } + + @Override + protected void subscribeActual(Subscriber<? super T> s) { + source.subscribe(new OnErrorReturnSubscriber<>(s, valueSupplier)); + } + + static final class OnErrorReturnSubscriber<T> + extends SinglePostCompleteSubscriber<T, T> { + + private static final long serialVersionUID = -3740826063558713822L; + final Function<? super Throwable, ? extends T> valueSupplier; + + OnErrorReturnSubscriber(Subscriber<? super T> actual, Function<? super Throwable, ? extends T> valueSupplier) { + super(actual); + this.valueSupplier = valueSupplier; + } + + @Override + public void onNext(T t) { + produced++; + downstream.onNext(t); + } + + @Override + public void onError(Throwable t) { + T v; + try { + v = Objects.requireNonNull(valueSupplier.apply(t), "The valueSupplier returned a null value"); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + downstream.onError(new CompositeException(t, ex)); + return; + } + complete(v); + } + + @Override + public void onComplete() { + downstream.onComplete(); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowablePublish.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowablePublish.java new file mode 100644 index 0000000000..b05bc8b748 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowablePublish.java @@ -0,0 +1,481 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import java.util.concurrent.atomic.*; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.FlowableSubscriber; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.flowables.ConnectableFlowable; +import io.reactivex.rxjava3.functions.Consumer; +import io.reactivex.rxjava3.internal.fuseable.*; +import io.reactivex.rxjava3.internal.subscriptions.SubscriptionHelper; +import io.reactivex.rxjava3.internal.util.*; +import io.reactivex.rxjava3.operators.QueueSubscription; +import io.reactivex.rxjava3.operators.SimpleQueue; +import io.reactivex.rxjava3.operators.SpscArrayQueue; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +/** + * Shares a single underlying connection to the upstream Publisher + * and multicasts events to all subscribed subscribers until the upstream + * completes or the connection is disposed. + * <p> + * The difference to FlowablePublish is that when the upstream terminates, + * late subscribers will receive that terminal event until the connection is + * disposed and the ConnectableFlowable is reset to its fresh state. + * + * @param <T> the element type + * @since 2.2.10 + */ +public final class FlowablePublish<T> extends ConnectableFlowable<T> +implements HasUpstreamPublisher<T> { + + final Publisher<T> source; + + final int bufferSize; + + final AtomicReference<PublishConnection<T>> current; + + public FlowablePublish(Publisher<T> source, int bufferSize) { + this.source = source; + this.bufferSize = bufferSize; + this.current = new AtomicReference<>(); + } + + @Override + public Publisher<T> source() { + return source; + } + + @Override + public void connect(Consumer<? super Disposable> connection) { + PublishConnection<T> conn; + boolean doConnect = false; + + for (;;) { + conn = current.get(); + + if (conn == null || conn.isDisposed()) { + PublishConnection<T> fresh = new PublishConnection<>(current, bufferSize); + if (!current.compareAndSet(conn, fresh)) { + continue; + } + conn = fresh; + } + + doConnect = !conn.connect.get() && conn.connect.compareAndSet(false, true); + break; + } + + try { + connection.accept(conn); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + throw ExceptionHelper.wrapOrThrow(ex); + } + + if (doConnect) { + source.subscribe(conn); + } + } + + @Override + protected void subscribeActual(Subscriber<? super T> s) { + PublishConnection<T> conn; + + for (;;) { + conn = current.get(); + + // don't create a fresh connection if the current is disposed + if (conn == null) { + PublishConnection<T> fresh = new PublishConnection<>(current, bufferSize); + if (!current.compareAndSet(conn, fresh)) { + continue; + } + conn = fresh; + } + + break; + } + + InnerSubscription<T> inner = new InnerSubscription<>(s, conn); + s.onSubscribe(inner); + + if (conn.add(inner)) { + if (inner.isCancelled()) { + conn.remove(inner); + } else { + conn.drain(); + } + return; + } + + Throwable ex = conn.error; + if (ex != null) { + inner.downstream.onError(ex); + } else { + inner.downstream.onComplete(); + } + } + + @Override + public void reset() { + PublishConnection<T> conn = current.get(); + if (conn != null && conn.isDisposed()) { + current.compareAndSet(conn, null); + } + } + + static final class PublishConnection<T> + extends AtomicInteger + implements FlowableSubscriber<T>, Disposable { + + private static final long serialVersionUID = -1672047311619175801L; + + final AtomicReference<PublishConnection<T>> current; + + final AtomicReference<Subscription> upstream; + + final AtomicBoolean connect; + + final AtomicReference<InnerSubscription<T>[]> subscribers; + + final int bufferSize; + + volatile SimpleQueue<T> queue; + + int sourceMode; + + volatile boolean done; + Throwable error; + + int consumed; + + @SuppressWarnings("rawtypes") + static final InnerSubscription[] EMPTY = new InnerSubscription[0]; + @SuppressWarnings("rawtypes") + static final InnerSubscription[] TERMINATED = new InnerSubscription[0]; + + @SuppressWarnings("unchecked") + PublishConnection(AtomicReference<PublishConnection<T>> current, int bufferSize) { + this.current = current; + this.upstream = new AtomicReference<>(); + this.connect = new AtomicBoolean(); + this.bufferSize = bufferSize; + this.subscribers = new AtomicReference<>(EMPTY); + } + + @SuppressWarnings("unchecked") + @Override + public void dispose() { + subscribers.getAndSet(TERMINATED); + current.compareAndSet(this, null); + SubscriptionHelper.cancel(upstream); + } + + @Override + public boolean isDisposed() { + return subscribers.get() == TERMINATED; + } + + @Override + public void onSubscribe(Subscription s) { + if (SubscriptionHelper.setOnce(this.upstream, s)) { + if (s instanceof QueueSubscription) { + @SuppressWarnings("unchecked") + QueueSubscription<T> qs = (QueueSubscription<T>) s; + + int m = qs.requestFusion(QueueSubscription.ANY | QueueSubscription.BOUNDARY); + if (m == QueueSubscription.SYNC) { + sourceMode = m; + queue = qs; + done = true; + drain(); + return; + } + if (m == QueueSubscription.ASYNC) { + sourceMode = m; + queue = qs; + s.request(bufferSize); + return; + } + } + + queue = new SpscArrayQueue<>(bufferSize); + + s.request(bufferSize); + } + } + + @Override + public void onNext(T t) { + // we expect upstream to honor backpressure requests + if (sourceMode == QueueSubscription.NONE && !queue.offer(t)) { + onError(new QueueOverflowException()); + return; + } + // since many things can happen concurrently, we have a common dispatch + // loop to act on the current state serially + drain(); + } + + @Override + public void onError(Throwable t) { + if (done) { + RxJavaPlugins.onError(t); + } else { + error = t; + done = true; + drain(); + } + } + + @Override + public void onComplete() { + done = true; + drain(); + } + + void drain() { + if (getAndIncrement() != 0) { + return; + } + + int missed = 1; + SimpleQueue<T> queue = this.queue; + int consumed = this.consumed; + int limit = this.bufferSize - (this.bufferSize >> 2); + boolean async = this.sourceMode != QueueSubscription.SYNC; + + outer: + for (;;) { + if (queue != null) { + long minDemand = Long.MAX_VALUE; + boolean hasDemand = false; + + InnerSubscription<T>[] consumers = subscribers.get(); + + for (InnerSubscription<T> inner : consumers) { + long request = inner.get(); + if (request != Long.MIN_VALUE) { + hasDemand = true; + minDemand = Math.min(request - inner.emitted, minDemand); + } + } + + if (!hasDemand) { + minDemand = 0L; + } + + while (minDemand != 0L) { + boolean d = done; + T v; + + try { + v = queue.poll(); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + upstream.get().cancel(); + queue.clear(); + done = true; + signalError(ex); + return; + } + + boolean empty = v == null; + + if (checkTerminated(d, empty)) { + return; + } + + if (empty) { + break; + } + + for (InnerSubscription<T> inner : consumers) { + if (!inner.isCancelled()) { + inner.downstream.onNext(v); + inner.emitted++; + } + } + + if (async && ++consumed == limit) { + consumed = 0; + upstream.get().request(limit); + } + minDemand--; + + if (consumers != subscribers.get()) { + continue outer; + } + } + + if (checkTerminated(done, queue.isEmpty())) { + return; + } + } + + this.consumed = consumed; + missed = addAndGet(-missed); + if (missed == 0) { + break; + } + if (queue == null) { + queue = this.queue; + } + } + } + + @SuppressWarnings("unchecked") + boolean checkTerminated(boolean isDone, boolean isEmpty) { + if (isDone && isEmpty) { + Throwable ex = error; + + if (ex != null) { + signalError(ex); + } else { + for (InnerSubscription<T> inner : subscribers.getAndSet(TERMINATED)) { + if (!inner.isCancelled()) { + inner.downstream.onComplete(); + } + } + } + return true; + } + return false; + } + + @SuppressWarnings("unchecked") + void signalError(Throwable ex) { + for (InnerSubscription<T> inner : subscribers.getAndSet(TERMINATED)) { + if (!inner.isCancelled()) { + inner.downstream.onError(ex); + } + } + } + + boolean add(InnerSubscription<T> inner) { + // the state can change so we do a CAS loop to achieve atomicity + for (;;) { + // get the current producer array + InnerSubscription<T>[] c = subscribers.get(); + // if this subscriber-to-source reached a terminal state by receiving + // an onError or onComplete, just refuse to add the new producer + if (c == TERMINATED) { + return false; + } + // we perform a copy-on-write logic + int len = c.length; + @SuppressWarnings("unchecked") + InnerSubscription<T>[] u = new InnerSubscription[len + 1]; + System.arraycopy(c, 0, u, 0, len); + u[len] = inner; + // try setting the subscribers array + if (subscribers.compareAndSet(c, u)) { + return true; + } + // if failed, some other operation succeeded (another add, remove or termination) + // so retry + } + } + + @SuppressWarnings("unchecked") + void remove(InnerSubscription<T> inner) { + // the state can change so we do a CAS loop to achieve atomicity + for (;;) { + // let's read the current subscribers array + InnerSubscription<T>[] c = subscribers.get(); + int len = c.length; + // if it is either empty or terminated, there is nothing to remove so we quit + if (len == 0) { + break; + } + // let's find the supplied producer in the array + // although this is O(n), we don't expect too many child subscribers in general + int j = -1; + for (int i = 0; i < len; i++) { + if (c[i] == inner) { + j = i; + break; + } + } + // we didn't find it so just quit + if (j < 0) { + return; + } + // we do copy-on-write logic here + InnerSubscription<T>[] u; + // we don't create a new empty array if producer was the single inhabitant + // but rather reuse an empty array + if (len == 1) { + u = EMPTY; + } else { + // otherwise, create a new array one less in size + u = new InnerSubscription[len - 1]; + // copy elements being before the given producer + System.arraycopy(c, 0, u, 0, j); + // copy elements being after the given producer + System.arraycopy(c, j + 1, u, j, len - j - 1); + } + // try setting this new array as + if (subscribers.compareAndSet(c, u)) { + break; + } + // if we failed, it means something else happened + // (a concurrent add/remove or termination), we need to retry + } + } + } + + static final class InnerSubscription<T> extends AtomicLong + implements Subscription { + + private static final long serialVersionUID = 2845000326761540265L; + + final Subscriber<? super T> downstream; + + final PublishConnection<T> parent; + + long emitted; + + InnerSubscription(Subscriber<? super T> downstream, PublishConnection<T> parent) { + this.downstream = downstream; + this.parent = parent; + } + + @Override + public void request(long n) { + if (SubscriptionHelper.validate(n)) { + BackpressureHelper.addCancel(this, n); + parent.drain(); + } + } + + @Override + public void cancel() { + if (getAndSet(Long.MIN_VALUE) != Long.MIN_VALUE) { + parent.remove(this); + parent.drain(); + } + } + + public boolean isCancelled() { + return get() == Long.MIN_VALUE; + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowablePublishMulticast.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowablePublishMulticast.java new file mode 100644 index 0000000000..08d0e40058 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowablePublishMulticast.java @@ -0,0 +1,517 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import java.util.Objects; +import java.util.concurrent.atomic.*; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.subscriptions.*; +import io.reactivex.rxjava3.internal.util.*; +import io.reactivex.rxjava3.operators.QueueSubscription; +import io.reactivex.rxjava3.operators.SimpleQueue; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +/** + * Multicasts a Flowable over a selector function. + * + * @param <T> the input value type + * @param <R> the output value type + */ +public final class FlowablePublishMulticast<T, R> extends AbstractFlowableWithUpstream<T, R> { + + final Function<? super Flowable<T>, ? extends Publisher<? extends R>> selector; + + final int prefetch; + + final boolean delayError; + + public FlowablePublishMulticast(Flowable<T> source, + Function<? super Flowable<T>, ? extends Publisher<? extends R>> selector, int prefetch, + boolean delayError) { + super(source); + this.selector = selector; + this.prefetch = prefetch; + this.delayError = delayError; + } + + @Override + protected void subscribeActual(Subscriber<? super R> s) { + MulticastProcessor<T> mp = new MulticastProcessor<>(prefetch, delayError); + + Publisher<? extends R> other; + + try { + other = Objects.requireNonNull(selector.apply(mp), "selector returned a null Publisher"); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + EmptySubscription.error(ex, s); + return; + } + + OutputCanceller<R> out = new OutputCanceller<>(s, mp); + + other.subscribe(out); + + source.subscribe(mp); + } + + static final class OutputCanceller<R> implements FlowableSubscriber<R>, Subscription { + final Subscriber<? super R> downstream; + + final MulticastProcessor<?> processor; + + Subscription upstream; + + OutputCanceller(Subscriber<? super R> actual, MulticastProcessor<?> processor) { + this.downstream = actual; + this.processor = processor; + } + + @Override + public void onSubscribe(Subscription s) { + if (SubscriptionHelper.validate(this.upstream, s)) { + this.upstream = s; + + downstream.onSubscribe(this); + } + } + + @Override + public void onNext(R t) { + downstream.onNext(t); + } + + @Override + public void onError(Throwable t) { + downstream.onError(t); + processor.dispose(); + } + + @Override + public void onComplete() { + downstream.onComplete(); + processor.dispose(); + } + + @Override + public void request(long n) { + upstream.request(n); + } + + @Override + public void cancel() { + upstream.cancel(); + processor.dispose(); + } + } + + static final class MulticastProcessor<T> extends Flowable<T> implements FlowableSubscriber<T> { + + @SuppressWarnings("rawtypes") + static final MulticastSubscription[] EMPTY = new MulticastSubscription[0]; + + @SuppressWarnings("rawtypes") + static final MulticastSubscription[] TERMINATED = new MulticastSubscription[0]; + + final AtomicInteger wip; + + final AtomicReference<MulticastSubscription<T>[]> subscribers; + + final int prefetch; + + final int limit; + + final boolean delayError; + + final AtomicReference<Subscription> upstream; + + volatile SimpleQueue<T> queue; + + int sourceMode; + + volatile boolean done; + Throwable error; + + int consumed; + + @SuppressWarnings("unchecked") + MulticastProcessor(int prefetch, boolean delayError) { + this.prefetch = prefetch; + this.limit = prefetch - (prefetch >> 2); // request after 75% consumption + this.delayError = delayError; + this.wip = new AtomicInteger(); + this.upstream = new AtomicReference<>(); + this.subscribers = new AtomicReference<>(EMPTY); + } + + @Override + public void onSubscribe(Subscription s) { + if (SubscriptionHelper.setOnce(this.upstream, s)) { + if (s instanceof QueueSubscription) { + @SuppressWarnings("unchecked") + QueueSubscription<T> qs = (QueueSubscription<T>) s; + + int m = qs.requestFusion(QueueSubscription.ANY); + if (m == QueueSubscription.SYNC) { + sourceMode = m; + queue = qs; + done = true; + drain(); + return; + } + if (m == QueueSubscription.ASYNC) { + sourceMode = m; + queue = qs; + QueueDrainHelper.request(s, prefetch); + return; + } + } + + queue = QueueDrainHelper.createQueue(prefetch); + + QueueDrainHelper.request(s, prefetch); + } + } + + void dispose() { + if (!done) { + SubscriptionHelper.cancel(upstream); + if (wip.getAndIncrement() == 0) { + SimpleQueue<T> q = queue; + if (q != null) { + q.clear(); + } + } + } + } + + boolean isDisposed() { + return upstream.get() == SubscriptionHelper.CANCELLED; + } + + @Override + public void onNext(T t) { + if (done) { + return; + } + if (sourceMode == QueueSubscription.NONE && !queue.offer(t)) { + upstream.get().cancel(); + onError(MissingBackpressureException.createDefault()); + return; + } + drain(); + } + + @Override + public void onError(Throwable t) { + if (done) { + RxJavaPlugins.onError(t); + return; + } + error = t; + done = true; + drain(); + } + + @Override + public void onComplete() { + if (!done) { + done = true; + drain(); + } + } + + boolean add(MulticastSubscription<T> s) { + for (;;) { + MulticastSubscription<T>[] current = subscribers.get(); + if (current == TERMINATED) { + return false; + } + int n = current.length; + @SuppressWarnings("unchecked") + MulticastSubscription<T>[] next = new MulticastSubscription[n + 1]; + System.arraycopy(current, 0, next, 0, n); + next[n] = s; + if (subscribers.compareAndSet(current, next)) { + return true; + } + } + } + + @SuppressWarnings("unchecked") + void remove(MulticastSubscription<T> s) { + for (;;) { + MulticastSubscription<T>[] current = subscribers.get(); + int n = current.length; + if (n == 0) { + return; + } + int j = -1; + + for (int i = 0; i < n; i++) { + if (current[i] == s) { + j = i; + break; + } + } + + if (j < 0) { + return; + } + MulticastSubscription<T>[] next; + if (n == 1) { + next = EMPTY; + } else { + next = new MulticastSubscription[n - 1]; + System.arraycopy(current, 0, next, 0, j); + System.arraycopy(current, j + 1, next, j, n - j - 1); + } + if (subscribers.compareAndSet(current, next)) { + return; + } + } + } + + @Override + protected void subscribeActual(Subscriber<? super T> s) { + MulticastSubscription<T> ms = new MulticastSubscription<>(s, this); + s.onSubscribe(ms); + if (add(ms)) { + if (ms.isCancelled()) { + remove(ms); + return; + } + drain(); + } else { + Throwable ex = error; + if (ex != null) { + s.onError(ex); + } else { + s.onComplete(); + } + } + } + + void drain() { + if (wip.getAndIncrement() != 0) { + return; + } + + int missed = 1; + + SimpleQueue<T> q = queue; + + int upstreamConsumed = consumed; + int localLimit = limit; + boolean canRequest = sourceMode != QueueSubscription.SYNC; + AtomicReference<MulticastSubscription<T>[]> subs = subscribers; + + MulticastSubscription<T>[] array = subs.get(); + + outer: + for (;;) { + + int n = array.length; + + if (q != null && n != 0) { + long r = Long.MAX_VALUE; + + for (MulticastSubscription<T> ms : array) { + long u = ms.get() - ms.emitted; + if (u != Long.MIN_VALUE) { + if (r > u) { + r = u; + } + } else { + n--; + } + } + + if (n == 0) { + r = 0; + } + + while (r != 0) { + if (isDisposed()) { + q.clear(); + return; + } + + boolean d = done; + + if (d && !delayError) { + Throwable ex = error; + if (ex != null) { + errorAll(ex); + return; + } + } + + T v; + + try { + v = q.poll(); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + SubscriptionHelper.cancel(upstream); + errorAll(ex); + return; + } + + boolean empty = v == null; + + if (d && empty) { + Throwable ex = error; + if (ex != null) { + errorAll(ex); + } else { + completeAll(); + } + return; + } + + if (empty) { + break; + } + + boolean subscribersChange = false; + + for (MulticastSubscription<T> ms : array) { + long msr = ms.get(); + if (msr != Long.MIN_VALUE) { + if (msr != Long.MAX_VALUE) { + ms.emitted++; + } + ms.downstream.onNext(v); + } else { + subscribersChange = true; + } + } + + r--; + + if (canRequest && ++upstreamConsumed == localLimit) { + upstreamConsumed = 0; + upstream.get().request(localLimit); + } + + MulticastSubscription<T>[] freshArray = subs.get(); + if (subscribersChange || freshArray != array) { + array = freshArray; + continue outer; + } + } + + if (r == 0) { + if (isDisposed()) { + q.clear(); + return; + } + + boolean d = done; + + if (d && !delayError) { + Throwable ex = error; + if (ex != null) { + errorAll(ex); + return; + } + } + + if (d && q.isEmpty()) { + Throwable ex = error; + if (ex != null) { + errorAll(ex); + } else { + completeAll(); + } + return; + } + } + } + + consumed = upstreamConsumed; + missed = wip.addAndGet(-missed); + if (missed == 0) { + break; + } + if (q == null) { + q = queue; + } + array = subs.get(); + } + } + + @SuppressWarnings("unchecked") + void errorAll(Throwable ex) { + for (MulticastSubscription<T> ms : subscribers.getAndSet(TERMINATED)) { + if (ms.get() != Long.MIN_VALUE) { + ms.downstream.onError(ex); + } + } + } + + @SuppressWarnings("unchecked") + void completeAll() { + for (MulticastSubscription<T> ms : subscribers.getAndSet(TERMINATED)) { + if (ms.get() != Long.MIN_VALUE) { + ms.downstream.onComplete(); + } + } + } + } + + static final class MulticastSubscription<T> + extends AtomicLong + implements Subscription { + + private static final long serialVersionUID = 8664815189257569791L; + + final Subscriber<? super T> downstream; + + final MulticastProcessor<T> parent; + + long emitted; + + MulticastSubscription(Subscriber<? super T> actual, MulticastProcessor<T> parent) { + this.downstream = actual; + this.parent = parent; + } + + @Override + public void request(long n) { + if (SubscriptionHelper.validate(n)) { + BackpressureHelper.addCancel(this, n); + parent.drain(); + } + } + + @Override + public void cancel() { + if (getAndSet(Long.MIN_VALUE) != Long.MIN_VALUE) { + parent.remove(this); + parent.drain(); // unblock the others + } + } + + public boolean isCancelled() { + return get() == Long.MIN_VALUE; + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableRange.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableRange.java new file mode 100644 index 0000000000..e2e3b9a6fb --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableRange.java @@ -0,0 +1,244 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import org.reactivestreams.Subscriber; + +import io.reactivex.rxjava3.annotations.Nullable; +import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.internal.subscriptions.*; +import io.reactivex.rxjava3.internal.util.BackpressureHelper; +import io.reactivex.rxjava3.operators.ConditionalSubscriber; + +/** + * Emits a range of integer values. + */ +public final class FlowableRange extends Flowable<Integer> { + final int start; + final int end; + public FlowableRange(int start, int count) { + this.start = start; + this.end = start + count; + } + + @Override + public void subscribeActual(Subscriber<? super Integer> s) { + if (s instanceof ConditionalSubscriber) { + s.onSubscribe(new RangeConditionalSubscription( + (ConditionalSubscriber<? super Integer>)s, start, end)); + } else { + s.onSubscribe(new RangeSubscription(s, start, end)); + } + } + + abstract static class BaseRangeSubscription extends BasicQueueSubscription<Integer> { + private static final long serialVersionUID = -2252972430506210021L; + + final int end; + + int index; + + volatile boolean cancelled; + + BaseRangeSubscription(int index, int end) { + this.index = index; + this.end = end; + } + + @Override + public final int requestFusion(int mode) { + return mode & SYNC; + } + + @Nullable + @Override + public final Integer poll() { + int i = index; + if (i == end) { + return null; + } + index = i + 1; + return i; + } + + @Override + public final boolean isEmpty() { + return index == end; + } + + @Override + public final void clear() { + index = end; + } + + @Override + public final void request(long n) { + if (SubscriptionHelper.validate(n)) { + if (BackpressureHelper.add(this, n) == 0L) { + if (n == Long.MAX_VALUE) { + fastPath(); + } else { + slowPath(n); + } + } + } + } + + @Override + public final void cancel() { + cancelled = true; + } + + abstract void fastPath(); + + abstract void slowPath(long r); + } + + static final class RangeSubscription extends BaseRangeSubscription { + + private static final long serialVersionUID = 2587302975077663557L; + + final Subscriber<? super Integer> downstream; + + RangeSubscription(Subscriber<? super Integer> actual, int index, int end) { + super(index, end); + this.downstream = actual; + } + + @Override + void fastPath() { + int f = end; + Subscriber<? super Integer> a = downstream; + + for (int i = index; i != f; i++) { + if (cancelled) { + return; + } + a.onNext(i); + } + if (cancelled) { + return; + } + a.onComplete(); + } + + @Override + void slowPath(long r) { + long e = 0; + int f = end; + int i = index; + Subscriber<? super Integer> a = downstream; + + for (;;) { + + while (e != r && i != f) { + if (cancelled) { + return; + } + + a.onNext(i); + + e++; + i++; + } + + if (i == f) { + if (!cancelled) { + a.onComplete(); + } + return; + } + + r = get(); + if (e == r) { + index = i; + r = addAndGet(-e); + if (r == 0L) { + return; + } + e = 0L; + } + } + } + } + + static final class RangeConditionalSubscription extends BaseRangeSubscription { + + private static final long serialVersionUID = 2587302975077663557L; + + final ConditionalSubscriber<? super Integer> downstream; + + RangeConditionalSubscription(ConditionalSubscriber<? super Integer> actual, int index, int end) { + super(index, end); + this.downstream = actual; + } + + @Override + void fastPath() { + int f = end; + ConditionalSubscriber<? super Integer> a = downstream; + + for (int i = index; i != f; i++) { + if (cancelled) { + return; + } + a.tryOnNext(i); + } + if (cancelled) { + return; + } + a.onComplete(); + } + + @Override + void slowPath(long r) { + long e = 0; + int f = end; + int i = index; + ConditionalSubscriber<? super Integer> a = downstream; + + for (;;) { + + while (e != r && i != f) { + if (cancelled) { + return; + } + + if (a.tryOnNext(i)) { + e++; + } + + i++; + } + + if (i == f) { + if (!cancelled) { + a.onComplete(); + } + return; + } + + r = get(); + if (e == r) { + index = i; + r = addAndGet(-e); + if (r == 0) { + return; + } + e = 0; + } + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableRangeLong.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableRangeLong.java new file mode 100644 index 0000000000..3be56da6ef --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableRangeLong.java @@ -0,0 +1,246 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import org.reactivestreams.Subscriber; + +import io.reactivex.rxjava3.annotations.Nullable; +import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.internal.subscriptions.*; +import io.reactivex.rxjava3.internal.util.BackpressureHelper; +import io.reactivex.rxjava3.operators.ConditionalSubscriber; + +/** + * Emits a range of long values. + */ +public final class FlowableRangeLong extends Flowable<Long> { + final long start; + final long end; + + public FlowableRangeLong(long start, long count) { + this.start = start; + this.end = start + count; + } + + @Override + public void subscribeActual(Subscriber<? super Long> s) { + if (s instanceof ConditionalSubscriber) { + s.onSubscribe(new RangeConditionalSubscription( + (ConditionalSubscriber<? super Long>)s, start, end)); + } else { + s.onSubscribe(new RangeSubscription(s, start, end)); + } + } + + abstract static class BaseRangeSubscription extends BasicQueueSubscription<Long> { + + private static final long serialVersionUID = -2252972430506210021L; + + final long end; + + long index; + + volatile boolean cancelled; + + BaseRangeSubscription(long index, long end) { + this.index = index; + this.end = end; + } + + @Override + public final int requestFusion(int mode) { + return mode & SYNC; + } + + @Nullable + @Override + public final Long poll() { + long i = index; + if (i == end) { + return null; + } + index = i + 1; + return i; + } + + @Override + public final boolean isEmpty() { + return index == end; + } + + @Override + public final void clear() { + index = end; + } + + @Override + public final void request(long n) { + if (SubscriptionHelper.validate(n)) { + if (BackpressureHelper.add(this, n) == 0L) { + if (n == Long.MAX_VALUE) { + fastPath(); + } else { + slowPath(n); + } + } + } + } + + @Override + public final void cancel() { + cancelled = true; + } + + abstract void fastPath(); + + abstract void slowPath(long r); + } + + static final class RangeSubscription extends BaseRangeSubscription { + + private static final long serialVersionUID = 2587302975077663557L; + + final Subscriber<? super Long> downstream; + + RangeSubscription(Subscriber<? super Long> actual, long index, long end) { + super(index, end); + this.downstream = actual; + } + + @Override + void fastPath() { + long f = end; + Subscriber<? super Long> a = downstream; + + for (long i = index; i != f; i++) { + if (cancelled) { + return; + } + a.onNext(i); + } + if (cancelled) { + return; + } + a.onComplete(); + } + + @Override + void slowPath(long r) { + long e = 0; + long f = end; + long i = index; + Subscriber<? super Long> a = downstream; + + for (;;) { + + while (e != r && i != f) { + if (cancelled) { + return; + } + + a.onNext(i); + + e++; + i++; + } + + if (i == f) { + if (!cancelled) { + a.onComplete(); + } + return; + } + + r = get(); + if (e == r) { + index = i; + r = addAndGet(-e); + if (r == 0L) { + return; + } + e = 0L; + } + } + } + } + + static final class RangeConditionalSubscription extends BaseRangeSubscription { + + private static final long serialVersionUID = 2587302975077663557L; + + final ConditionalSubscriber<? super Long> downstream; + + RangeConditionalSubscription(ConditionalSubscriber<? super Long> actual, long index, long end) { + super(index, end); + this.downstream = actual; + } + + @Override + void fastPath() { + long f = end; + ConditionalSubscriber<? super Long> a = downstream; + + for (long i = index; i != f; i++) { + if (cancelled) { + return; + } + a.tryOnNext(i); + } + if (cancelled) { + return; + } + a.onComplete(); + } + + @Override + void slowPath(long r) { + long e = 0; + long f = end; + long i = index; + ConditionalSubscriber<? super Long> a = downstream; + + for (;;) { + + while (e != r && i != f) { + if (cancelled) { + return; + } + + if (a.tryOnNext(i)) { + e++; + } + + i++; + } + + if (i == f) { + if (!cancelled) { + a.onComplete(); + } + return; + } + + r = get(); + if (e == r) { + index = i; + r = addAndGet(-e); + if (r == 0) { + return; + } + e = 0; + } + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableReduce.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableReduce.java new file mode 100644 index 0000000000..99bc861519 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableReduce.java @@ -0,0 +1,123 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.BiFunction; +import io.reactivex.rxjava3.internal.subscriptions.*; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +import java.util.Objects; + +/** + * Reduces a sequence via a function into a single value or signals NoSuchElementException for + * an empty source. + * + * @param <T> the value type + */ +public final class FlowableReduce<T> extends AbstractFlowableWithUpstream<T, T> { + + final BiFunction<T, T, T> reducer; + + public FlowableReduce(Flowable<T> source, BiFunction<T, T, T> reducer) { + super(source); + this.reducer = reducer; + } + + @Override + protected void subscribeActual(Subscriber<? super T> s) { + source.subscribe(new ReduceSubscriber<>(s, reducer)); + } + + static final class ReduceSubscriber<T> extends DeferredScalarSubscription<T> implements FlowableSubscriber<T> { + + private static final long serialVersionUID = -4663883003264602070L; + + final BiFunction<T, T, T> reducer; + + Subscription upstream; + + ReduceSubscriber(Subscriber<? super T> actual, BiFunction<T, T, T> reducer) { + super(actual); + this.reducer = reducer; + } + + @Override + public void onSubscribe(Subscription s) { + if (SubscriptionHelper.validate(this.upstream, s)) { + this.upstream = s; + + downstream.onSubscribe(this); + + s.request(Long.MAX_VALUE); + } + } + + @Override + public void onNext(T t) { + if (upstream == SubscriptionHelper.CANCELLED) { + return; + } + + T v = value; + if (v == null) { + value = t; + } else { + try { + value = Objects.requireNonNull(reducer.apply(v, t), "The reducer returned a null value"); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + upstream.cancel(); + onError(ex); + } + } + } + + @Override + public void onError(Throwable t) { + if (upstream == SubscriptionHelper.CANCELLED) { + RxJavaPlugins.onError(t); + return; + } + upstream = SubscriptionHelper.CANCELLED; + downstream.onError(t); + } + + @Override + public void onComplete() { + if (upstream == SubscriptionHelper.CANCELLED) { + return; + } + upstream = SubscriptionHelper.CANCELLED; + + T v = value; + if (v != null) { + complete(v); + } else { + downstream.onComplete(); + } + } + + @Override + public void cancel() { + super.cancel(); + upstream.cancel(); + upstream = SubscriptionHelper.CANCELLED; + } + + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableReduceMaybe.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableReduceMaybe.java new file mode 100644 index 0000000000..6541fee6bb --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableReduceMaybe.java @@ -0,0 +1,143 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.BiFunction; +import io.reactivex.rxjava3.internal.fuseable.*; +import io.reactivex.rxjava3.internal.subscriptions.SubscriptionHelper; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +import java.util.Objects; + +/** + * Reduce a Flowable into a single value exposed as Single or signal NoSuchElementException. + * + * @param <T> the value type + */ +public final class FlowableReduceMaybe<T> +extends Maybe<T> +implements HasUpstreamPublisher<T>, FuseToFlowable<T> { + + final Flowable<T> source; + + final BiFunction<T, T, T> reducer; + + public FlowableReduceMaybe(Flowable<T> source, BiFunction<T, T, T> reducer) { + this.source = source; + this.reducer = reducer; + } + + @Override + public Publisher<T> source() { + return source; + } + + @Override + public Flowable<T> fuseToFlowable() { + return RxJavaPlugins.onAssembly(new FlowableReduce<>(source, reducer)); + } + + @Override + protected void subscribeActual(MaybeObserver<? super T> observer) { + source.subscribe(new ReduceSubscriber<>(observer, reducer)); + } + + static final class ReduceSubscriber<T> implements FlowableSubscriber<T>, Disposable { + final MaybeObserver<? super T> downstream; + + final BiFunction<T, T, T> reducer; + + T value; + + Subscription upstream; + + boolean done; + + ReduceSubscriber(MaybeObserver<? super T> actual, BiFunction<T, T, T> reducer) { + this.downstream = actual; + this.reducer = reducer; + } + + @Override + public void dispose() { + upstream.cancel(); + done = true; + } + + @Override + public boolean isDisposed() { + return done; + } + + @Override + public void onSubscribe(Subscription s) { + if (SubscriptionHelper.validate(this.upstream, s)) { + this.upstream = s; + + downstream.onSubscribe(this); + + s.request(Long.MAX_VALUE); + } + } + + @Override + public void onNext(T t) { + if (done) { + return; + } + T v = value; + if (v == null) { + value = t; + } else { + try { + value = Objects.requireNonNull(reducer.apply(v, t), "The reducer returned a null value"); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + upstream.cancel(); + onError(ex); + } + } + } + + @Override + public void onError(Throwable t) { + if (done) { + RxJavaPlugins.onError(t); + return; + } + done = true; + downstream.onError(t); + } + + @Override + public void onComplete() { + if (done) { + return; + } + done = true; + T v = value; + if (v != null) { +// value = null; + downstream.onSuccess(v); + } else { + downstream.onComplete(); + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableReduceSeedSingle.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableReduceSeedSingle.java new file mode 100644 index 0000000000..a5a8cc40cb --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableReduceSeedSingle.java @@ -0,0 +1,126 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.BiFunction; +import io.reactivex.rxjava3.internal.subscriptions.SubscriptionHelper; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +import java.util.Objects; + +/** + * Reduce a sequence of values, starting from a seed value and by using + * an accumulator function and return the last accumulated value. + * + * @param <T> the source value type + * @param <R> the accumulated result type + */ +public final class FlowableReduceSeedSingle<T, R> extends Single<R> { + + final Publisher<T> source; + + final R seed; + + final BiFunction<R, ? super T, R> reducer; + + public FlowableReduceSeedSingle(Publisher<T> source, R seed, BiFunction<R, ? super T, R> reducer) { + this.source = source; + this.seed = seed; + this.reducer = reducer; + } + + @Override + protected void subscribeActual(SingleObserver<? super R> observer) { + source.subscribe(new ReduceSeedObserver<>(observer, reducer, seed)); + } + + static final class ReduceSeedObserver<T, R> implements FlowableSubscriber<T>, Disposable { + + final SingleObserver<? super R> downstream; + + final BiFunction<R, ? super T, R> reducer; + + R value; + + Subscription upstream; + + ReduceSeedObserver(SingleObserver<? super R> actual, BiFunction<R, ? super T, R> reducer, R value) { + this.downstream = actual; + this.value = value; + this.reducer = reducer; + } + + @Override + public void onSubscribe(Subscription s) { + if (SubscriptionHelper.validate(this.upstream, s)) { + this.upstream = s; + + downstream.onSubscribe(this); + + s.request(Long.MAX_VALUE); + } + } + + @Override + public void onNext(T value) { + R v = this.value; + if (v != null) { + try { + this.value = Objects.requireNonNull(reducer.apply(v, value), "The reducer returned a null value"); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + upstream.cancel(); + onError(ex); + } + } + } + + @Override + public void onError(Throwable e) { + if (value != null) { + value = null; + upstream = SubscriptionHelper.CANCELLED; + downstream.onError(e); + } else { + RxJavaPlugins.onError(e); + } + } + + @Override + public void onComplete() { + R v = value; + if (v != null) { + value = null; + upstream = SubscriptionHelper.CANCELLED; + downstream.onSuccess(v); + } + } + + @Override + public void dispose() { + upstream.cancel(); + upstream = SubscriptionHelper.CANCELLED; + } + + @Override + public boolean isDisposed() { + return upstream == SubscriptionHelper.CANCELLED; + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableReduceWithSingle.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableReduceWithSingle.java new file mode 100644 index 0000000000..9d27a19668 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableReduceWithSingle.java @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import org.reactivestreams.Publisher; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.disposables.EmptyDisposable; +import io.reactivex.rxjava3.internal.operators.flowable.FlowableReduceSeedSingle.ReduceSeedObserver; + +import java.util.Objects; + +/** + * Reduce a sequence of values, starting from a generated seed value and by using + * an accumulator function and return the last accumulated value. + * + * @param <T> the source value type + * @param <R> the accumulated result type + */ +public final class FlowableReduceWithSingle<T, R> extends Single<R> { + + final Publisher<T> source; + + final Supplier<R> seedSupplier; + + final BiFunction<R, ? super T, R> reducer; + + public FlowableReduceWithSingle(Publisher<T> source, Supplier<R> seedSupplier, BiFunction<R, ? super T, R> reducer) { + this.source = source; + this.seedSupplier = seedSupplier; + this.reducer = reducer; + } + + @Override + protected void subscribeActual(SingleObserver<? super R> observer) { + R seed; + + try { + seed = Objects.requireNonNull(seedSupplier.get(), "The seedSupplier returned a null value"); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + EmptyDisposable.error(ex, observer); + return; + } + source.subscribe(new ReduceSeedObserver<>(observer, reducer, seed)); + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableRefCount.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableRefCount.java new file mode 100644 index 0000000000..0c9f17b0bd --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableRefCount.java @@ -0,0 +1,245 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.*; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.flowables.ConnectableFlowable; +import io.reactivex.rxjava3.functions.Consumer; +import io.reactivex.rxjava3.internal.disposables.*; +import io.reactivex.rxjava3.internal.subscriptions.SubscriptionHelper; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +/** + * Returns an observable sequence that stays connected to the source as long as + * there is at least one subscription to the observable sequence. + * + * @param <T> + * the value type + */ +public final class FlowableRefCount<T> extends Flowable<T> { + + final ConnectableFlowable<T> source; + + final int n; + + final long timeout; + + final TimeUnit unit; + + final Scheduler scheduler; + + RefConnection connection; + + public FlowableRefCount(ConnectableFlowable<T> source) { + this(source, 1, 0L, TimeUnit.NANOSECONDS, null); + } + + public FlowableRefCount(ConnectableFlowable<T> source, int n, long timeout, TimeUnit unit, + Scheduler scheduler) { + this.source = source; + this.n = n; + this.timeout = timeout; + this.unit = unit; + this.scheduler = scheduler; + } + + @Override + protected void subscribeActual(Subscriber<? super T> s) { + + RefConnection conn; + + boolean connect = false; + synchronized (this) { + conn = connection; + if (conn == null) { + conn = new RefConnection(this); + connection = conn; + } + + long c = conn.subscriberCount; + if (c == 0L && conn.timer != null) { + conn.timer.dispose(); + } + conn.subscriberCount = c + 1; + if (!conn.connected && c + 1 == n) { + connect = true; + conn.connected = true; + } + } + + source.subscribe(new RefCountSubscriber<>(s, this, conn)); + + if (connect) { + source.connect(conn); + } + } + + void cancel(RefConnection rc) { + SequentialDisposable sd; + synchronized (this) { + if (connection == null || connection != rc) { + return; + } + long c = rc.subscriberCount - 1; + rc.subscriberCount = c; + if (c != 0L || !rc.connected) { + return; + } + if (timeout == 0L) { + timeout(rc); + return; + } + sd = new SequentialDisposable(); + rc.timer = sd; + } + + sd.replace(scheduler.scheduleDirect(rc, timeout, unit)); + } + + void terminated(RefConnection rc) { + synchronized (this) { + if (connection == rc) { + if (rc.timer != null) { + rc.timer.dispose(); + rc.timer = null; + } + if (--rc.subscriberCount == 0) { + connection = null; + source.reset(); + } + } + } + } + + void timeout(RefConnection rc) { + synchronized (this) { + if (rc.subscriberCount == 0 && rc == connection) { + connection = null; + Disposable connectionObject = rc.get(); + DisposableHelper.dispose(rc); + if (connectionObject == null) { + rc.disconnectedEarly = true; + } else { + source.reset(); + } + } + } + } + + static final class RefConnection extends AtomicReference<Disposable> + implements Runnable, Consumer<Disposable> { + + private static final long serialVersionUID = -4552101107598366241L; + + final FlowableRefCount<?> parent; + + Disposable timer; + + long subscriberCount; + + boolean connected; + + boolean disconnectedEarly; + + RefConnection(FlowableRefCount<?> parent) { + this.parent = parent; + } + + @Override + public void run() { + parent.timeout(this); + } + + @Override + public void accept(Disposable t) { + DisposableHelper.replace(this, t); + synchronized (parent) { + if (disconnectedEarly) { + parent.source.reset(); + } + } + } + } + + static final class RefCountSubscriber<T> + extends AtomicBoolean implements FlowableSubscriber<T>, Subscription { + + private static final long serialVersionUID = -7419642935409022375L; + + final Subscriber<? super T> downstream; + + final FlowableRefCount<T> parent; + + final RefConnection connection; + + Subscription upstream; + + RefCountSubscriber(Subscriber<? super T> actual, FlowableRefCount<T> parent, RefConnection connection) { + this.downstream = actual; + this.parent = parent; + this.connection = connection; + } + + @Override + public void onNext(T t) { + downstream.onNext(t); + } + + @Override + public void onError(Throwable t) { + if (compareAndSet(false, true)) { + parent.terminated(connection); + downstream.onError(t); + } else { + RxJavaPlugins.onError(t); + } + } + + @Override + public void onComplete() { + if (compareAndSet(false, true)) { + parent.terminated(connection); + downstream.onComplete(); + } + } + + @Override + public void request(long n) { + upstream.request(n); + } + + @Override + public void cancel() { + upstream.cancel(); + if (compareAndSet(false, true)) { + parent.cancel(connection); + } + } + + @Override + public void onSubscribe(Subscription s) { + if (SubscriptionHelper.validate(upstream, s)) { + this.upstream = s; + + downstream.onSubscribe(this); + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableRepeat.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableRepeat.java new file mode 100644 index 0000000000..ffc90b8f4b --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableRepeat.java @@ -0,0 +1,111 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import java.util.concurrent.atomic.AtomicInteger; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.internal.subscriptions.SubscriptionArbiter; + +public final class FlowableRepeat<T> extends AbstractFlowableWithUpstream<T, T> { + final long count; + public FlowableRepeat(Flowable<T> source, long count) { + super(source); + this.count = count; + } + + @Override + public void subscribeActual(Subscriber<? super T> s) { + SubscriptionArbiter sa = new SubscriptionArbiter(false); + s.onSubscribe(sa); + + RepeatSubscriber<T> rs = new RepeatSubscriber<>(s, count != Long.MAX_VALUE ? count - 1 : Long.MAX_VALUE, sa, source); + rs.subscribeNext(); + } + + static final class RepeatSubscriber<T> extends AtomicInteger implements FlowableSubscriber<T> { + + private static final long serialVersionUID = -7098360935104053232L; + + final Subscriber<? super T> downstream; + final SubscriptionArbiter sa; + final Publisher<? extends T> source; + long remaining; + + long produced; + + RepeatSubscriber(Subscriber<? super T> actual, long count, SubscriptionArbiter sa, Publisher<? extends T> source) { + this.downstream = actual; + this.sa = sa; + this.source = source; + this.remaining = count; + } + + @Override + public void onSubscribe(Subscription s) { + sa.setSubscription(s); + } + + @Override + public void onNext(T t) { + produced++; + downstream.onNext(t); + } + + @Override + public void onError(Throwable t) { + downstream.onError(t); + } + + @Override + public void onComplete() { + long r = remaining; + if (r != Long.MAX_VALUE) { + remaining = r - 1; + } + if (r != 0L) { + subscribeNext(); + } else { + downstream.onComplete(); + } + } + + /** + * Subscribes to the source again via trampolining. + */ + void subscribeNext() { + if (getAndIncrement() == 0) { + int missed = 1; + for (;;) { + if (sa.isCancelled()) { + return; + } + long p = produced; + if (p != 0L) { + produced = 0L; + sa.produced(p); + } + source.subscribe(this); + + missed = addAndGet(-missed); + if (missed == 0) { + break; + } + } + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableRepeatUntil.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableRepeatUntil.java new file mode 100644 index 0000000000..1d527fa59c --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableRepeatUntil.java @@ -0,0 +1,119 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import java.util.concurrent.atomic.AtomicInteger; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.BooleanSupplier; +import io.reactivex.rxjava3.internal.subscriptions.SubscriptionArbiter; + +public final class FlowableRepeatUntil<T> extends AbstractFlowableWithUpstream<T, T> { + final BooleanSupplier until; + public FlowableRepeatUntil(Flowable<T> source, BooleanSupplier until) { + super(source); + this.until = until; + } + + @Override + public void subscribeActual(Subscriber<? super T> s) { + SubscriptionArbiter sa = new SubscriptionArbiter(false); + s.onSubscribe(sa); + + RepeatSubscriber<T> rs = new RepeatSubscriber<>(s, until, sa, source); + rs.subscribeNext(); + } + + static final class RepeatSubscriber<T> extends AtomicInteger implements FlowableSubscriber<T> { + + private static final long serialVersionUID = -7098360935104053232L; + + final Subscriber<? super T> downstream; + final SubscriptionArbiter sa; + final Publisher<? extends T> source; + final BooleanSupplier stop; + + long produced; + + RepeatSubscriber(Subscriber<? super T> actual, BooleanSupplier until, SubscriptionArbiter sa, Publisher<? extends T> source) { + this.downstream = actual; + this.sa = sa; + this.source = source; + this.stop = until; + } + + @Override + public void onSubscribe(Subscription s) { + sa.setSubscription(s); + } + + @Override + public void onNext(T t) { + produced++; + downstream.onNext(t); + } + + @Override + public void onError(Throwable t) { + downstream.onError(t); + } + + @Override + public void onComplete() { + boolean b; + try { + b = stop.getAsBoolean(); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + downstream.onError(e); + return; + } + if (b) { + downstream.onComplete(); + } else { + subscribeNext(); + } + } + + /** + * Subscribes to the source again via trampolining. + */ + void subscribeNext() { + if (getAndIncrement() == 0) { + int missed = 1; + for (;;) { + if (sa.isCancelled()) { + return; + } + + long p = produced; + if (p != 0L) { + produced = 0L; + sa.produced(p); + } + + source.subscribe(this); + + missed = addAndGet(-missed); + if (missed == 0) { + break; + } + } + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableRepeatWhen.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableRepeatWhen.java new file mode 100644 index 0000000000..a2bfa250e5 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableRepeatWhen.java @@ -0,0 +1,201 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import java.util.Objects; +import java.util.concurrent.atomic.*; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.subscriptions.*; +import io.reactivex.rxjava3.processors.*; +import io.reactivex.rxjava3.subscribers.SerializedSubscriber; + +public final class FlowableRepeatWhen<T> extends AbstractFlowableWithUpstream<T, T> { + final Function<? super Flowable<Object>, ? extends Publisher<?>> handler; + + public FlowableRepeatWhen(Flowable<T> source, + Function<? super Flowable<Object>, ? extends Publisher<?>> handler) { + super(source); + this.handler = handler; + } + + @Override + public void subscribeActual(Subscriber<? super T> s) { + + SerializedSubscriber<T> z = new SerializedSubscriber<>(s); + + FlowableProcessor<Object> processor = UnicastProcessor.create(8).toSerialized(); + + Publisher<?> when; + + try { + when = Objects.requireNonNull(handler.apply(processor), "handler returned a null Publisher"); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + EmptySubscription.error(ex, s); + return; + } + + WhenReceiver<T, Object> receiver = new WhenReceiver<>(source); + + RepeatWhenSubscriber<T> subscriber = new RepeatWhenSubscriber<>(z, processor, receiver); + + receiver.subscriber = subscriber; + + s.onSubscribe(subscriber); + + when.subscribe(receiver); + + receiver.onNext(0); + } + + static final class WhenReceiver<T, U> + extends AtomicInteger + implements FlowableSubscriber<Object>, Subscription { + + private static final long serialVersionUID = 2827772011130406689L; + + final Publisher<T> source; + + final AtomicReference<Subscription> upstream; + + final AtomicLong requested; + + WhenSourceSubscriber<T, U> subscriber; + + WhenReceiver(Publisher<T> source) { + this.source = source; + this.upstream = new AtomicReference<>(); + this.requested = new AtomicLong(); + } + + @Override + public void onSubscribe(Subscription s) { + SubscriptionHelper.deferredSetOnce(upstream, requested, s); + } + + @Override + public void onNext(Object t) { + if (getAndIncrement() == 0) { + for (;;) { + if (upstream.get() == SubscriptionHelper.CANCELLED) { + return; + } + + source.subscribe(subscriber); + + if (decrementAndGet() == 0) { + break; + } + } + } + } + + @Override + public void onError(Throwable t) { + subscriber.cancel(); + subscriber.downstream.onError(t); + } + + @Override + public void onComplete() { + subscriber.cancel(); + subscriber.downstream.onComplete(); + } + + @Override + public void request(long n) { + SubscriptionHelper.deferredRequest(upstream, requested, n); + } + + @Override + public void cancel() { + SubscriptionHelper.cancel(upstream); + } + } + + abstract static class WhenSourceSubscriber<T, U> extends SubscriptionArbiter implements FlowableSubscriber<T> { + + private static final long serialVersionUID = -5604623027276966720L; + + protected final Subscriber<? super T> downstream; + + protected final FlowableProcessor<U> processor; + + protected final Subscription receiver; + + private long produced; + + WhenSourceSubscriber(Subscriber<? super T> actual, FlowableProcessor<U> processor, + Subscription receiver) { + super(false); + this.downstream = actual; + this.processor = processor; + this.receiver = receiver; + } + + @Override + public final void onSubscribe(Subscription s) { + setSubscription(s); + } + + @Override + public final void onNext(T t) { + produced++; + downstream.onNext(t); + } + + protected final void again(U signal) { + setSubscription(EmptySubscription.INSTANCE); + long p = produced; + if (p != 0L) { + produced = 0L; + produced(p); + } + receiver.request(1); + processor.onNext(signal); + } + + @Override + public final void cancel() { + super.cancel(); + receiver.cancel(); + } + } + + static final class RepeatWhenSubscriber<T> extends WhenSourceSubscriber<T, Object> { + + private static final long serialVersionUID = -2680129890138081029L; + + RepeatWhenSubscriber(Subscriber<? super T> actual, FlowableProcessor<Object> processor, + Subscription receiver) { + super(actual, processor, receiver); + } + + @Override + public void onError(Throwable t) { + receiver.cancel(); + downstream.onError(t); + } + + @Override + public void onComplete() { + again(0); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableReplay.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableReplay.java new file mode 100644 index 0000000000..11489490c7 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableReplay.java @@ -0,0 +1,1249 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import java.util.*; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.*; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.flowables.ConnectableFlowable; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.fuseable.HasUpstreamPublisher; +import io.reactivex.rxjava3.internal.subscribers.SubscriberResourceWrapper; +import io.reactivex.rxjava3.internal.subscriptions.*; +import io.reactivex.rxjava3.internal.util.*; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.schedulers.Timed; + +public final class FlowableReplay<T> extends ConnectableFlowable<T> implements HasUpstreamPublisher<T> { + /** The source observable. */ + final Flowable<T> source; + /** Holds the current subscriber that is, will be or just was subscribed to the source observable. */ + final AtomicReference<ReplaySubscriber<T>> current; + /** A factory that creates the appropriate buffer for the ReplaySubscriber. */ + final Supplier<? extends ReplayBuffer<T>> bufferFactory; + + final Publisher<T> onSubscribe; + + @SuppressWarnings("rawtypes") + static final Supplier DEFAULT_UNBOUNDED_FACTORY = new DefaultUnboundedFactory(); + + /** + * Given a connectable observable factory, it multicasts over the generated + * ConnectableObservable via a selector function. + * @param <U> the connectable observable type + * @param <R> the result type + * @param connectableFactory the factory that returns a ConnectableFlowable for each individual subscriber + * @param selector the function that receives a Flowable and should return another Flowable that will be subscribed to + * @return the new Observable instance + */ + public static <U, R> Flowable<R> multicastSelector( + final Supplier<? extends ConnectableFlowable<U>> connectableFactory, + final Function<? super Flowable<U>, ? extends Publisher<R>> selector) { + return new MulticastFlowable<>(connectableFactory, selector); + } + + /** + * Creates a replaying ConnectableObservable with an unbounded buffer. + * @param <T> the value type + * @param source the source Publisher to use + * @return the new ConnectableObservable instance + */ + @SuppressWarnings("unchecked") + public static <T> ConnectableFlowable<T> createFrom(Flowable<? extends T> source) { + return create(source, DEFAULT_UNBOUNDED_FACTORY); + } + + /** + * Creates a replaying ConnectableObservable with a size bound buffer. + * @param <T> the value type + * @param source the source Flowable to use + * @param bufferSize the maximum number of elements to hold + * @param eagerTruncate if true, the head reference is refreshed to avoid unwanted item retention + * @return the new ConnectableObservable instance + */ + public static <T> ConnectableFlowable<T> create(Flowable<T> source, + final int bufferSize, boolean eagerTruncate) { + if (bufferSize == Integer.MAX_VALUE) { + return createFrom(source); + } + return create(source, new ReplayBufferSupplier<>(bufferSize, eagerTruncate)); + } + + /** + * Creates a replaying ConnectableObservable with a time bound buffer. + * @param <T> the value type + * @param source the source Flowable to use + * @param maxAge the maximum age of entries + * @param unit the unit of measure of the age amount + * @param scheduler the target scheduler providing the current time + * @param eagerTruncate if true, the head reference is refreshed to avoid unwanted item retention + * @return the new ConnectableObservable instance + */ + public static <T> ConnectableFlowable<T> create(Flowable<T> source, + long maxAge, TimeUnit unit, Scheduler scheduler, boolean eagerTruncate) { + return create(source, maxAge, unit, scheduler, Integer.MAX_VALUE, eagerTruncate); + } + + /** + * Creates a replaying ConnectableObservable with a size and time bound buffer. + * @param <T> the value type + * @param source the source Flowable to use + * @param maxAge the maximum age of entries + * @param unit the unit of measure of the age amount + * @param scheduler the target scheduler providing the current time + * @param bufferSize the maximum number of elements to hold + * @param eagerTruncate if true, the head reference is refreshed to avoid unwanted item retention + * @return the new ConnectableFlowable instance + */ + public static <T> ConnectableFlowable<T> create(Flowable<T> source, + final long maxAge, final TimeUnit unit, final Scheduler scheduler, final int bufferSize, boolean eagerTruncate) { + return create(source, new ScheduledReplayBufferSupplier<>(bufferSize, maxAge, unit, scheduler, eagerTruncate)); + } + + /** + * Creates a OperatorReplay instance to replay values of the given source {@code Flowable}. + * @param <T> the value type + * @param source the source {@code Flowable} to use + * @param bufferFactory the factory to instantiate the appropriate buffer when the {@code Flowable} becomes active + * @return the {@code ConnectableFlowable} instance + */ + static <T> ConnectableFlowable<T> create(Flowable<T> source, + final Supplier<? extends ReplayBuffer<T>> bufferFactory) { + // the current connection to source needs to be shared between the operator and its onSubscribe call + final AtomicReference<ReplaySubscriber<T>> curr = new AtomicReference<>(); + Publisher<T> onSubscribe = new ReplayPublisher<>(curr, bufferFactory); + return RxJavaPlugins.onAssembly(new FlowableReplay<>(onSubscribe, source, curr, bufferFactory)); + } + + private FlowableReplay(Publisher<T> onSubscribe, Flowable<T> source, + final AtomicReference<ReplaySubscriber<T>> current, + final Supplier<? extends ReplayBuffer<T>> bufferFactory) { + this.onSubscribe = onSubscribe; + this.source = source; + this.current = current; + this.bufferFactory = bufferFactory; + } + + @Override + public Publisher<T> source() { + return source; + } + + @Override + protected void subscribeActual(Subscriber<? super T> s) { + onSubscribe.subscribe(s); + } + + @Override + public void reset() { + ReplaySubscriber<T> conn = current.get(); + if (conn != null && conn.isDisposed()) { + current.compareAndSet(conn, null); + } + } + + @Override + public void connect(Consumer<? super Disposable> connection) { + boolean doConnect; + ReplaySubscriber<T> ps; + // we loop because concurrent connect/disconnect and termination may change the state + for (;;) { + // retrieve the current subscriber-to-source instance + ps = current.get(); + // if there is none yet or the current was disposed + if (ps == null || ps.isDisposed()) { + + ReplayBuffer<T> buf; + + try { + buf = bufferFactory.get(); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + throw ExceptionHelper.wrapOrThrow(ex); + } + + // create a new subscriber-to-source + ReplaySubscriber<T> u = new ReplaySubscriber<>(buf, current); + // try setting it as the current subscriber-to-source + if (!current.compareAndSet(ps, u)) { + // did not work, perhaps a new subscriber arrived + // and created a new subscriber-to-source as well, retry + continue; + } + ps = u; + } + // if connect() was called concurrently, only one of them should actually + // connect to the source + doConnect = !ps.shouldConnect.get() && ps.shouldConnect.compareAndSet(false, true); + break; // NOPMD + } + /* + * Notify the callback that we have a (new) connection which it can dispose + * but since ps is unique to a connection, multiple calls to connect() will return the + * same Subscription and even if there was a connect-disconnect-connect pair, the older + * references won't disconnect the newer connection. + * Synchronous source consumers have the opportunity to disconnect via dispose on the + * Disposable as unsafeSubscribe may never return in its own. + * + * Note however, that asynchronously disconnecting a running source might leave + * child-subscribers without any terminal event; ReplaySubject does not have this + * issue because the cancellation was always triggered by the child-subscribers + * themselves. + */ + try { + connection.accept(ps); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + if (doConnect) { + ps.shouldConnect.compareAndSet(true, false); + } + Exceptions.throwIfFatal(ex); + throw ExceptionHelper.wrapOrThrow(ex); + } + if (doConnect) { + source.subscribe(ps); + } + } + + @SuppressWarnings("rawtypes") + static final class ReplaySubscriber<T> + extends AtomicReference<Subscription> + implements FlowableSubscriber<T>, Disposable { + private static final long serialVersionUID = 7224554242710036740L; + /** Holds notifications from upstream. */ + final ReplayBuffer<T> buffer; + /** Indicates this Subscriber received a terminal event. */ + boolean done; + + /** Indicates an empty array of inner subscriptions. */ + static final InnerSubscription[] EMPTY = new InnerSubscription[0]; + /** Indicates a terminated ReplaySubscriber. */ + static final InnerSubscription[] TERMINATED = new InnerSubscription[0]; + + /** Tracks the subscribed InnerSubscriptions. */ + final AtomicReference<InnerSubscription<T>[]> subscribers; + /** + * Atomically changed from false to true by connect to make sure the + * connection is only performed by one thread. + */ + final AtomicBoolean shouldConnect; + + final AtomicInteger management; + + /** Tracks the amount already requested from the upstream. */ + long requestedFromUpstream; + + /** The current connection. */ + final AtomicReference<ReplaySubscriber<T>> current; + + @SuppressWarnings("unchecked") + ReplaySubscriber(ReplayBuffer<T> buffer, AtomicReference<ReplaySubscriber<T>> current) { + this.buffer = buffer; + this.current = current; + this.management = new AtomicInteger(); + this.subscribers = new AtomicReference<>(EMPTY); + this.shouldConnect = new AtomicBoolean(); + } + + @Override + public boolean isDisposed() { + return subscribers.get() == TERMINATED; + } + + @SuppressWarnings("unchecked") + @Override + public void dispose() { + subscribers.set(TERMINATED); + current.compareAndSet(ReplaySubscriber.this, null); + // we don't care if it fails because it means the current has + // been replaced in the meantime + SubscriptionHelper.cancel(this); + } + + /** + * Atomically try adding a new InnerSubscription to this Subscriber or return false if this + * Subscriber was terminated. + * @param producer the producer to add + * @return true if succeeded, false otherwise + */ + @SuppressWarnings("unchecked") + boolean add(InnerSubscription<T> producer) { + // the state can change so we do a CAS loop to achieve atomicity + for (;;) { + // get the current producer array + InnerSubscription<T>[] c = subscribers.get(); + // if this subscriber-to-source reached a terminal state by receiving + // an onError or onComplete, just refuse to add the new producer + if (c == TERMINATED) { + return false; + } + // we perform a copy-on-write logic + int len = c.length; + InnerSubscription<T>[] u = new InnerSubscription[len + 1]; + System.arraycopy(c, 0, u, 0, len); + u[len] = producer; + // try setting the subscribers array + if (subscribers.compareAndSet(c, u)) { + return true; + } + // if failed, some other operation succeeded (another add, remove or termination) + // so retry + } + } + + /** + * Atomically removes the given InnerSubscription from the subscribers array. + * @param p the InnerSubscription to remove + */ + @SuppressWarnings("unchecked") + void remove(InnerSubscription<T> p) { + // the state can change so we do a CAS loop to achieve atomicity + for (;;) { + // let's read the current subscribers array + InnerSubscription<T>[] c = subscribers.get(); + int len = c.length; + // if it is either empty or terminated, there is nothing to remove so we quit + if (len == 0) { + return; + } + // let's find the supplied producer in the array + // although this is O(n), we don't expect too many child subscribers in general + int j = -1; + for (int i = 0; i < len; i++) { + if (c[i].equals(p)) { + j = i; + break; + } + } + // we didn't find it so just quit + if (j < 0) { + return; + } + // we do copy-on-write logic here + InnerSubscription<T>[] u; + // we don't create a new empty array if producer was the single inhabitant + // but rather reuse an empty array + if (len == 1) { + u = EMPTY; + } else { + // otherwise, create a new array one less in size + u = new InnerSubscription[len - 1]; + // copy elements being before the given producer + System.arraycopy(c, 0, u, 0, j); + // copy elements being after the given producer + System.arraycopy(c, j + 1, u, j, len - j - 1); + } + // try setting this new array as + if (subscribers.compareAndSet(c, u)) { + return; + } + // if we failed, it means something else happened + // (a concurrent add/remove or termination), we need to retry + } + } + + @Override + public void onSubscribe(Subscription p) { + if (SubscriptionHelper.setOnce(this, p)) { + manageRequests(); + for (InnerSubscription<T> rp : subscribers.get()) { + buffer.replay(rp); + } + } + } + + @Override + public void onNext(T t) { + if (!done) { + buffer.next(t); + for (InnerSubscription<T> rp : subscribers.get()) { + buffer.replay(rp); + } + } + } + + @SuppressWarnings("unchecked") + @Override + public void onError(Throwable e) { + // The observer front is accessed serially as required by spec so + // no need to CAS in the terminal value + if (!done) { + done = true; + buffer.error(e); + for (InnerSubscription<T> rp : subscribers.getAndSet(TERMINATED)) { + buffer.replay(rp); + } + } else { + RxJavaPlugins.onError(e); + } + } + + @SuppressWarnings("unchecked") + @Override + public void onComplete() { + // The observer front is accessed serially as required by spec so + // no need to CAS in the terminal value + if (!done) { + done = true; + buffer.complete(); + for (InnerSubscription<T> rp : subscribers.getAndSet(TERMINATED)) { + buffer.replay(rp); + } + } + } + + /** + * Coordinates the request amounts of various child Subscribers. + */ + void manageRequests() { + AtomicInteger m = management; + if (m.getAndIncrement() != 0) { + return; + } + int missed = 1; + for (;;) { + // if the upstream has completed, no more requesting is possible + if (isDisposed()) { + return; + } + Subscription p = get(); + + // only request when there is an upstream Subscription available + if (p != null) { + // how many items were requested so far + long alreadyRequested = requestedFromUpstream; + long downstreamMaxRequest = alreadyRequested; + + // find out the maximum total requested of the current subscribers + for (InnerSubscription<T> rp : subscribers.get()) { + downstreamMaxRequest = Math.max(downstreamMaxRequest, rp.totalRequested.get()); + } + + // how much more to request from the upstream + long diff = downstreamMaxRequest - alreadyRequested; + if (diff != 0L) { + // save the new maximum requested + requestedFromUpstream = downstreamMaxRequest; + p.request(diff); + } + } + + missed = m.addAndGet(-missed); + if (missed == 0) { + break; + } + } + } + } + /** + * A Subscription that manages the request and cancellation state of a + * child subscriber in thread-safe manner. + * @param <T> the value type + */ + static final class InnerSubscription<T> extends AtomicLong implements Subscription, Disposable { + + private static final long serialVersionUID = -4453897557930727610L; + /** + * The parent subscriber-to-source used to allow removing the child in case of + * child cancellation. + */ + final ReplaySubscriber<T> parent; + /** The actual child subscriber. */ + final Subscriber<? super T> child; + /** + * Holds an object that represents the current location in the buffer. + * Guarded by the emitter loop. + */ + Object index; + /** + * Keeps the sum of all requested amounts. + */ + final AtomicLong totalRequested; + /** Indicates an emission state. Guarded by this. */ + boolean emitting; + /** Indicates a missed update. Guarded by this. */ + boolean missed; + /** + * Indicates this child has been cancelled: the state is swapped in atomically and + * will prevent the dispatch() to emit (too many) values to a terminated child subscriber. + */ + static final long CANCELLED = Long.MIN_VALUE; + + InnerSubscription(ReplaySubscriber<T> parent, Subscriber<? super T> child) { + this.parent = parent; + this.child = child; + this.totalRequested = new AtomicLong(); + } + + @Override + public void request(long n) { + // ignore negative requests + if (SubscriptionHelper.validate(n)) { + // add to the current requested and cap it at MAX_VALUE + // except when there was a concurrent cancellation + if (BackpressureHelper.addCancel(this, n) != CANCELLED) { + // increment the total request counter + BackpressureHelper.add(totalRequested, n); + // if successful, notify the parent dispatcher this child can receive more + // elements + parent.manageRequests(); + // try replaying any cached content + parent.buffer.replay(this); + } + } + } + + /** + * Indicate that values have been emitted to this child subscriber by the dispatch() method. + * @param n the number of items emitted + * @return the updated request value (may indicate how much can be produced or a terminal state) + */ + public long produced(long n) { + return BackpressureHelper.producedCancel(this, n); + } + + @Override + public boolean isDisposed() { + return get() == CANCELLED; + } + + @Override + public void cancel() { + dispose(); + } + + @Override + public void dispose() { + if (getAndSet(CANCELLED) != CANCELLED) { + // remove this from the parent + parent.remove(this); + // After removal, we might have unblocked the other child subscribers: + // let's assume this child had 0 requested before the cancellation while + // the others had non-zero. By removing this 'blocking' child, the others + // are now free to receive events + parent.manageRequests(); + // make sure the last known node is not retained + index = null; + } + } + /** + * Convenience method to auto-cast the index object. + * @param <U> type to cast index object + * @return the current index object + */ + @SuppressWarnings("unchecked") + <U> U index() { + return (U)index; + } + } + /** + * The interface for interacting with various buffering logic. + * + * @param <T> the value type + */ + interface ReplayBuffer<T> { + /** + * Adds a regular value to the buffer. + * @param value the next value to store + */ + void next(T value); + /** + * Adds a terminal exception to the buffer. + * @param e the Throwable instance + */ + void error(Throwable e); + /** + * Adds a completion event to the buffer. + */ + void complete(); + /** + * Tries to replay the buffered values to the + * subscriber inside the output if there + * is new value and requests available at the + * same time. + * @param output the receiver of the events + */ + void replay(InnerSubscription<T> output); + } + + /** + * Holds an unbounded list of events. + * + * @param <T> the value type + */ + static final class UnboundedReplayBuffer<T> extends ArrayList<Object> implements ReplayBuffer<T> { + + private static final long serialVersionUID = 7063189396499112664L; + /** The total number of events in the buffer. */ + volatile int size; + + UnboundedReplayBuffer(int capacityHint) { + super(capacityHint); + } + + @Override + public void next(T value) { + add(NotificationLite.next(value)); + size++; + } + + @Override + public void error(Throwable e) { + add(NotificationLite.error(e)); + size++; + } + + @Override + public void complete() { + add(NotificationLite.complete()); + size++; + } + + @Override + public void replay(InnerSubscription<T> output) { + synchronized (output) { + if (output.emitting) { + output.missed = true; + return; + } + output.emitting = true; + } + final Subscriber<? super T> child = output.child; + + for (;;) { + if (output.isDisposed()) { + return; + } + int sourceIndex = size; + + Integer destinationIndexObject = output.index(); + int destinationIndex = destinationIndexObject != null ? destinationIndexObject : 0; + + long r = output.get(); + long r0 = r; // NOPMD + long e = 0L; + + while (r != 0L && destinationIndex < sourceIndex) { + Object o = get(destinationIndex); + try { + if (NotificationLite.accept(o, child)) { + return; + } + } catch (Throwable err) { + Exceptions.throwIfFatal(err); + output.dispose(); + if (!NotificationLite.isError(o) && !NotificationLite.isComplete(o)) { + child.onError(err); + } else { + RxJavaPlugins.onError(err); + } + return; + } + if (output.isDisposed()) { + return; + } + destinationIndex++; + r--; + e++; + } + if (e != 0L) { + output.index = destinationIndex; + if (r0 != Long.MAX_VALUE) { + output.produced(e); + } + } + + synchronized (output) { + if (!output.missed) { + output.emitting = false; + return; + } + output.missed = false; + } + } + } + } + + /** + * Represents a node in a bounded replay buffer's linked list. + */ + static final class Node extends AtomicReference<Node> { + + private static final long serialVersionUID = 245354315435971818L; + final Object value; + final long index; + + Node(Object value, long index) { + this.value = value; + this.index = index; + } + } + + /** + * Base class for bounded buffering with options to specify an + * enter and leave transforms and custom truncation behavior. + * + * @param <T> the value type + */ + abstract static class BoundedReplayBuffer<T> extends AtomicReference<Node> implements ReplayBuffer<T> { + + private static final long serialVersionUID = 2346567790059478686L; + + final boolean eagerTruncate; + + Node tail; + int size; + + long index; + + BoundedReplayBuffer(boolean eagerTruncate) { + this.eagerTruncate = eagerTruncate; + Node n = new Node(null, 0); + tail = n; + set(n); + } + + /** + * Add a new node to the linked list. + * @param n the Node instance to add + */ + final void addLast(Node n) { + tail.set(n); + tail = n; + size++; + } + /** + * Remove the first node from the linked list. + */ + final void removeFirst() { + Node head = get(); + Node next = head.get(); + if (next == null) { + throw new IllegalStateException("Empty list!"); + } + size--; + // can't just move the head because it would retain the very first value + // can't null out the head's value because of late replayers would see null + setFirst(next); + } + /* test */ final void removeSome(int n) { + Node head = get(); + while (n > 0) { + head = head.get(); + n--; + size--; + } + + setFirst(head); + // correct the tail if all items have been removed + head = get(); + if (head.get() == null) { + tail = head; + } + } + /** + * Arranges the given node is the new head from now on. + * @param n the Node instance to set as first + */ + final void setFirst(Node n) { + if (eagerTruncate) { + Node m = new Node(null, n.index); + m.lazySet(n.get()); + n = m; + } + set(n); + } + + @Override + public final void next(T value) { + Object o = enterTransform(NotificationLite.next(value), false); + Node n = new Node(o, ++index); + addLast(n); + truncate(); + } + + @Override + public final void error(Throwable e) { + Object o = enterTransform(NotificationLite.error(e), true); + Node n = new Node(o, ++index); + addLast(n); + truncateFinal(); + } + + @Override + public final void complete() { + Object o = enterTransform(NotificationLite.complete(), true); + Node n = new Node(o, ++index); + addLast(n); + truncateFinal(); + } + + final void trimHead() { + Node head = get(); + if (head.value != null) { + Node n = new Node(null, 0L); + n.lazySet(head.get()); + set(n); + } + } + + @Override + public final void replay(InnerSubscription<T> output) { + synchronized (output) { + if (output.emitting) { + output.missed = true; + return; + } + output.emitting = true; + } + for (;;) { + long r = output.get(); + boolean unbounded = r == Long.MAX_VALUE; // NOPMD + long e = 0L; + + Node node = output.index(); + if (node == null) { + node = getHead(); + output.index = node; + + BackpressureHelper.add(output.totalRequested, node.index); + } + + while (r != 0) { + if (output.isDisposed()) { + output.index = null; + return; + } + + Node v = node.get(); + if (v != null) { + Object o = leaveTransform(v.value); + try { + if (NotificationLite.accept(o, output.child)) { + output.index = null; + return; + } + } catch (Throwable err) { + Exceptions.throwIfFatal(err); + output.index = null; + output.dispose(); + if (!NotificationLite.isError(o) && !NotificationLite.isComplete(o)) { + output.child.onError(err); + } else { + RxJavaPlugins.onError(err); + } + return; + } + e++; + r--; + node = v; + } else { + break; + } + } + + if (r == 0 && output.isDisposed()) { + output.index = null; + return; + } + + if (e != 0L) { + output.index = node; + if (!unbounded) { + output.produced(e); + } + } + + synchronized (output) { + if (!output.missed) { + output.emitting = false; + return; + } + output.missed = false; + } + } + + } + + /** + * Override this to wrap the NotificationLite object into a + * container to be used later by truncate. + * @param value the value to transform into the internal representation + * @param terminal is this a terminal value? + * @return the transformed value + */ + Object enterTransform(Object value, boolean terminal) { + return value; + } + /** + * Override this to unwrap the transformed value into a + * NotificationLite object. + * @param value the input value to transform to the external representation + * @return the transformed value + */ + Object leaveTransform(Object value) { + return value; + } + /** + * Override this method to truncate a non-terminated buffer + * based on its current properties. + */ + abstract void truncate(); + /** + * Override this method to truncate a terminated buffer + * based on its properties (i.e., truncate but the very last node). + */ + void truncateFinal() { + trimHead(); + } + /* test */ final void collect(Collection<? super T> output) { + Node n = getHead(); + for (;;) { + Node next = n.get(); + if (next != null) { + Object o = next.value; + Object v = leaveTransform(o); + if (NotificationLite.isComplete(v) || NotificationLite.isError(v)) { + break; + } + output.add(NotificationLite.<T>getValue(v)); + n = next; + } else { + break; + } + } + } + /* test */ boolean hasError() { + return tail.value != null && NotificationLite.isError(leaveTransform(tail.value)); + } + /* test */ boolean hasCompleted() { + return tail.value != null && NotificationLite.isComplete(leaveTransform(tail.value)); + } + + Node getHead() { + return get(); + } + } + + /** + * A bounded replay buffer implementation with size limit only. + * + * @param <T> the value type + */ + static final class SizeBoundReplayBuffer<T> extends BoundedReplayBuffer<T> { + + private static final long serialVersionUID = -5898283885385201806L; + + final int limit; + SizeBoundReplayBuffer(int limit, boolean eagerTruncate) { + super(eagerTruncate); + this.limit = limit; + } + + @Override + void truncate() { + // overflow can be at most one element + if (size > limit) { + removeFirst(); + } + } + + // no need for final truncation because values are truncated one by one + } + + /** + * Size and time bound replay buffer. + * + * @param <T> the buffered value type + */ + static final class SizeAndTimeBoundReplayBuffer<T> extends BoundedReplayBuffer<T> { + + private static final long serialVersionUID = 3457957419649567404L; + final Scheduler scheduler; + final long maxAge; + final TimeUnit unit; + final int limit; + SizeAndTimeBoundReplayBuffer(int limit, long maxAge, TimeUnit unit, Scheduler scheduler, boolean eagerTruncate) { + super(eagerTruncate); + this.scheduler = scheduler; + this.limit = limit; + this.maxAge = maxAge; + this.unit = unit; + } + + @Override + Object enterTransform(Object value, boolean terminal) { + return new Timed<>(value, terminal ? Long.MAX_VALUE : scheduler.now(unit), unit); + } + + @Override + Object leaveTransform(Object value) { + return ((Timed<?>)value).value(); + } + + @Override + void truncate() { + long timeLimit = scheduler.now(unit) - maxAge; + + Node prev = get(); + Node next = prev.get(); + + int e = 0; + for (;;) { + if (size > 1) { // never truncate the very last item just added + if (size > limit) { + e++; + size--; + prev = next; + next = next.get(); + } else { + Timed<?> v = (Timed<?>)next.value; + if (v.time() <= timeLimit) { + e++; + size--; + prev = next; + next = next.get(); + } else { + break; + } + } + } else { + break; + } + } + if (e != 0) { + setFirst(prev); + } + } + + @Override + void truncateFinal() { + long timeLimit = scheduler.now(unit) - maxAge; + + Node prev = get(); + Node next = prev.get(); + + int e = 0; + for (;;) { + if (size > 1) { + Timed<?> v = (Timed<?>)next.value; + if (v.time() <= timeLimit) { + e++; + size--; + prev = next; + next = next.get(); + } else { + break; + } + } else { + break; + } + } + if (e != 0) { + setFirst(prev); + } + } + + @Override + Node getHead() { + long timeLimit = scheduler.now(unit) - maxAge; + Node prev = get(); + Node next = prev.get(); + for (;;) { + if (next == null) { + break; + } + Timed<?> v = (Timed<?>)next.value; + if (NotificationLite.isComplete(v.value()) || NotificationLite.isError(v.value())) { + break; + } + if (v.time() <= timeLimit) { + prev = next; + next = next.get(); + } else { + break; + } + } + return prev; + } + } + + static final class MulticastFlowable<R, U> extends Flowable<R> { + private final Supplier<? extends ConnectableFlowable<U>> connectableFactory; + private final Function<? super Flowable<U>, ? extends Publisher<R>> selector; + + MulticastFlowable(Supplier<? extends ConnectableFlowable<U>> connectableFactory, Function<? super Flowable<U>, ? extends Publisher<R>> selector) { + this.connectableFactory = connectableFactory; + this.selector = selector; + } + + @Override + protected void subscribeActual(Subscriber<? super R> child) { + ConnectableFlowable<U> cf; + try { + cf = ExceptionHelper.nullCheck(connectableFactory.get(), "The connectableFactory returned a null ConnectableFlowable."); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + EmptySubscription.error(e, child); + return; + } + + Publisher<R> observable; + try { + observable = ExceptionHelper.nullCheck(selector.apply(cf), "The selector returned a null Publisher."); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + EmptySubscription.error(e, child); + return; + } + + final SubscriberResourceWrapper<R> srw = new SubscriberResourceWrapper<>(child); + + observable.subscribe(srw); + + cf.connect(new DisposableConsumer(srw)); + } + + final class DisposableConsumer implements Consumer<Disposable> { + private final SubscriberResourceWrapper<R> srw; + + DisposableConsumer(SubscriberResourceWrapper<R> srw) { + this.srw = srw; + } + + @Override + public void accept(Disposable r) { + srw.setResource(r); + } + } + } + + static final class ReplayBufferSupplier<T> implements Supplier<ReplayBuffer<T>> { + + final int bufferSize; + + final boolean eagerTruncate; + + ReplayBufferSupplier(int bufferSize, boolean eagerTruncate) { + this.bufferSize = bufferSize; + this.eagerTruncate = eagerTruncate; + } + + @Override + public ReplayBuffer<T> get() { + return new SizeBoundReplayBuffer<>(bufferSize, eagerTruncate); + } + } + + static final class ScheduledReplayBufferSupplier<T> implements Supplier<ReplayBuffer<T>> { + private final int bufferSize; + private final long maxAge; + private final TimeUnit unit; + private final Scheduler scheduler; + + final boolean eagerTruncate; + + ScheduledReplayBufferSupplier(int bufferSize, long maxAge, TimeUnit unit, Scheduler scheduler, boolean eagerTruncate) { + this.bufferSize = bufferSize; + this.maxAge = maxAge; + this.unit = unit; + this.scheduler = scheduler; + this.eagerTruncate = eagerTruncate; + } + + @Override + public ReplayBuffer<T> get() { + return new SizeAndTimeBoundReplayBuffer<>(bufferSize, maxAge, unit, scheduler, eagerTruncate); + } + } + + static final class ReplayPublisher<T> implements Publisher<T> { + private final AtomicReference<ReplaySubscriber<T>> curr; + private final Supplier<? extends ReplayBuffer<T>> bufferFactory; + + ReplayPublisher(AtomicReference<ReplaySubscriber<T>> curr, Supplier<? extends ReplayBuffer<T>> bufferFactory) { + this.curr = curr; + this.bufferFactory = bufferFactory; + } + + @Override + public void subscribe(Subscriber<? super T> child) { + // concurrent connection/disconnection may change the state, + // we loop to be atomic while the child subscribes + for (;;) { + // get the current subscriber-to-source + ReplaySubscriber<T> r = curr.get(); + // if there isn't one + if (r == null) { + ReplayBuffer<T> buf; + + try { + buf = bufferFactory.get(); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + EmptySubscription.error(ex, child); + return; + } + // create a new subscriber to source + ReplaySubscriber<T> u = new ReplaySubscriber<>(buf, curr); + // let's try setting it as the current subscriber-to-source + if (!curr.compareAndSet(null, u)) { + // didn't work, maybe someone else did it or the current subscriber + // to source has just finished + continue; + } + // we won, let's use it going onwards + r = u; + } + + // create the backpressure-managing producer for this child + InnerSubscription<T> inner = new InnerSubscription<>(r, child); + // the producer has been registered with the current subscriber-to-source so + // at least it will receive the next terminal event + // setting the producer will trigger the first request to be considered by + // the subscriber-to-source. + child.onSubscribe(inner); + // we try to add it to the array of subscribers + // if it fails, no worries because we will still have its buffer + // so it is going to replay it for us + r.add(inner); + + if (inner.isDisposed()) { + r.remove(inner); + return; + } + + r.manageRequests(); + + // trigger the capturing of the current node and total requested + r.buffer.replay(inner); + + break; // NOPMD + } + } + } + + static final class DefaultUnboundedFactory implements Supplier<Object> { + @Override + public Object get() { + return new UnboundedReplayBuffer<>(16); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableRetryBiPredicate.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableRetryBiPredicate.java new file mode 100644 index 0000000000..8c8a0e960e --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableRetryBiPredicate.java @@ -0,0 +1,123 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import java.util.concurrent.atomic.AtomicInteger; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.BiPredicate; +import io.reactivex.rxjava3.internal.subscriptions.SubscriptionArbiter; + +public final class FlowableRetryBiPredicate<T> extends AbstractFlowableWithUpstream<T, T> { + final BiPredicate<? super Integer, ? super Throwable> predicate; + public FlowableRetryBiPredicate( + Flowable<T> source, + BiPredicate<? super Integer, ? super Throwable> predicate) { + super(source); + this.predicate = predicate; + } + + @Override + public void subscribeActual(Subscriber<? super T> s) { + SubscriptionArbiter sa = new SubscriptionArbiter(false); + s.onSubscribe(sa); + + RetryBiSubscriber<T> rs = new RetryBiSubscriber<>(s, predicate, sa, source); + rs.subscribeNext(); + } + + static final class RetryBiSubscriber<T> extends AtomicInteger implements FlowableSubscriber<T> { + + private static final long serialVersionUID = -7098360935104053232L; + + final Subscriber<? super T> downstream; + final SubscriptionArbiter sa; + final Publisher<? extends T> source; + final BiPredicate<? super Integer, ? super Throwable> predicate; + int retries; + + long produced; + + RetryBiSubscriber(Subscriber<? super T> actual, + BiPredicate<? super Integer, ? super Throwable> predicate, SubscriptionArbiter sa, Publisher<? extends T> source) { + this.downstream = actual; + this.sa = sa; + this.source = source; + this.predicate = predicate; + } + + @Override + public void onSubscribe(Subscription s) { + sa.setSubscription(s); + } + + @Override + public void onNext(T t) { + produced++; + downstream.onNext(t); + } + + @Override + public void onError(Throwable t) { + boolean b; + try { + b = predicate.test(++retries, t); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + downstream.onError(new CompositeException(t, e)); + return; + } + if (!b) { + downstream.onError(t); + return; + } + subscribeNext(); + } + + @Override + public void onComplete() { + downstream.onComplete(); + } + + /** + * Subscribes to the source again via trampolining. + */ + void subscribeNext() { + if (getAndIncrement() == 0) { + int missed = 1; + for (;;) { + if (sa.isCancelled()) { + return; + } + + long p = produced; + if (p != 0L) { + produced = 0L; + sa.produced(p); + } + + source.subscribe(this); + + missed = addAndGet(-missed); + if (missed == 0) { + break; + } + } + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableRetryPredicate.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableRetryPredicate.java new file mode 100644 index 0000000000..a954a09300 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableRetryPredicate.java @@ -0,0 +1,134 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import java.util.concurrent.atomic.AtomicInteger; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.Predicate; +import io.reactivex.rxjava3.internal.subscriptions.SubscriptionArbiter; + +public final class FlowableRetryPredicate<T> extends AbstractFlowableWithUpstream<T, T> { + final Predicate<? super Throwable> predicate; + final long count; + public FlowableRetryPredicate(Flowable<T> source, + long count, + Predicate<? super Throwable> predicate) { + super(source); + this.predicate = predicate; + this.count = count; + } + + @Override + public void subscribeActual(Subscriber<? super T> s) { + SubscriptionArbiter sa = new SubscriptionArbiter(false); + s.onSubscribe(sa); + + RetrySubscriber<T> rs = new RetrySubscriber<>(s, count, predicate, sa, source); + rs.subscribeNext(); + } + + static final class RetrySubscriber<T> extends AtomicInteger implements FlowableSubscriber<T> { + + private static final long serialVersionUID = -7098360935104053232L; + + final Subscriber<? super T> downstream; + final SubscriptionArbiter sa; + final Publisher<? extends T> source; + final Predicate<? super Throwable> predicate; + long remaining; + + long produced; + + RetrySubscriber(Subscriber<? super T> actual, long count, + Predicate<? super Throwable> predicate, SubscriptionArbiter sa, Publisher<? extends T> source) { + this.downstream = actual; + this.sa = sa; + this.source = source; + this.predicate = predicate; + this.remaining = count; + } + + @Override + public void onSubscribe(Subscription s) { + sa.setSubscription(s); + } + + @Override + public void onNext(T t) { + produced++; + downstream.onNext(t); + } + + @Override + public void onError(Throwable t) { + long r = remaining; + if (r != Long.MAX_VALUE) { + remaining = r - 1; + } + if (r == 0) { + downstream.onError(t); + } else { + boolean b; + try { + b = predicate.test(t); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + downstream.onError(new CompositeException(t, e)); + return; + } + if (!b) { + downstream.onError(t); + return; + } + subscribeNext(); + } + } + + @Override + public void onComplete() { + downstream.onComplete(); + } + + /** + * Subscribes to the source again via trampolining. + */ + void subscribeNext() { + if (getAndIncrement() == 0) { + int missed = 1; + for (;;) { + if (sa.isCancelled()) { + return; + } + + long p = produced; + if (p != 0L) { + produced = 0L; + sa.produced(p); + } + + source.subscribe(this); + + missed = addAndGet(-missed); + if (missed == 0) { + break; + } + } + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableRetryWhen.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableRetryWhen.java new file mode 100644 index 0000000000..9babc65472 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableRetryWhen.java @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.operators.flowable.FlowableRepeatWhen.*; +import io.reactivex.rxjava3.internal.subscriptions.EmptySubscription; +import io.reactivex.rxjava3.processors.*; +import io.reactivex.rxjava3.subscribers.SerializedSubscriber; + +import java.util.Objects; + +public final class FlowableRetryWhen<T> extends AbstractFlowableWithUpstream<T, T> { + final Function<? super Flowable<Throwable>, ? extends Publisher<?>> handler; + + public FlowableRetryWhen(Flowable<T> source, + Function<? super Flowable<Throwable>, ? extends Publisher<?>> handler) { + super(source); + this.handler = handler; + } + + @Override + public void subscribeActual(Subscriber<? super T> s) { + SerializedSubscriber<T> z = new SerializedSubscriber<>(s); + + FlowableProcessor<Throwable> processor = UnicastProcessor.<Throwable>create(8).toSerialized(); + + Publisher<?> when; + + try { + when = Objects.requireNonNull(handler.apply(processor), "handler returned a null Publisher"); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + EmptySubscription.error(ex, s); + return; + } + + WhenReceiver<T, Throwable> receiver = new WhenReceiver<>(source); + + RetryWhenSubscriber<T> subscriber = new RetryWhenSubscriber<>(z, processor, receiver); + + receiver.subscriber = subscriber; + + s.onSubscribe(subscriber); + + when.subscribe(receiver); + + receiver.onNext(0); + } + + static final class RetryWhenSubscriber<T> extends WhenSourceSubscriber<T, Throwable> { + + private static final long serialVersionUID = -2680129890138081029L; + + RetryWhenSubscriber(Subscriber<? super T> actual, FlowableProcessor<Throwable> processor, + Subscription receiver) { + super(actual, processor, receiver); + } + + @Override + public void onError(Throwable t) { + again(t); + } + + @Override + public void onComplete() { + receiver.cancel(); + downstream.onComplete(); + } + } + +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableSamplePublisher.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableSamplePublisher.java new file mode 100644 index 0000000000..19474d2cc0 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableSamplePublisher.java @@ -0,0 +1,225 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import java.util.concurrent.atomic.*; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.MissingBackpressureException; +import io.reactivex.rxjava3.internal.subscriptions.SubscriptionHelper; +import io.reactivex.rxjava3.internal.util.BackpressureHelper; +import io.reactivex.rxjava3.subscribers.SerializedSubscriber; + +public final class FlowableSamplePublisher<T> extends Flowable<T> { + final Publisher<T> source; + final Publisher<?> other; + + final boolean emitLast; + + public FlowableSamplePublisher(Publisher<T> source, Publisher<?> other, boolean emitLast) { + this.source = source; + this.other = other; + this.emitLast = emitLast; + } + + @Override + protected void subscribeActual(Subscriber<? super T> s) { + SerializedSubscriber<T> serial = new SerializedSubscriber<>(s); + if (emitLast) { + source.subscribe(new SampleMainEmitLast<>(serial, other)); + } else { + source.subscribe(new SampleMainNoLast<>(serial, other)); + } + } + + abstract static class SamplePublisherSubscriber<T> extends AtomicReference<T> implements FlowableSubscriber<T>, Subscription { + + private static final long serialVersionUID = -3517602651313910099L; + + final Subscriber<? super T> downstream; + final Publisher<?> sampler; + + final AtomicLong requested = new AtomicLong(); + + final AtomicReference<Subscription> other = new AtomicReference<>(); + + Subscription upstream; + + SamplePublisherSubscriber(Subscriber<? super T> actual, Publisher<?> other) { + this.downstream = actual; + this.sampler = other; + } + + @Override + public void onSubscribe(Subscription s) { + if (SubscriptionHelper.validate(this.upstream, s)) { + this.upstream = s; + downstream.onSubscribe(this); + if (other.get() == null) { + sampler.subscribe(new SamplerSubscriber<>(this)); + s.request(Long.MAX_VALUE); + } + } + + } + + @Override + public void onNext(T t) { + lazySet(t); + } + + @Override + public void onError(Throwable t) { + SubscriptionHelper.cancel(other); + downstream.onError(t); + } + + @Override + public void onComplete() { + SubscriptionHelper.cancel(other); + completion(); + } + + void setOther(Subscription o) { + SubscriptionHelper.setOnce(other, o, Long.MAX_VALUE); + } + + @Override + public void request(long n) { + if (SubscriptionHelper.validate(n)) { + BackpressureHelper.add(requested, n); + } + } + + @Override + public void cancel() { + SubscriptionHelper.cancel(other); + upstream.cancel(); + } + + public void error(Throwable e) { + upstream.cancel(); + downstream.onError(e); + } + + public void complete() { + upstream.cancel(); + completion(); + } + + void emit() { + T value = getAndSet(null); + if (value != null) { + long r = requested.get(); + if (r != 0L) { + downstream.onNext(value); + BackpressureHelper.produced(requested, 1); + } else { + cancel(); + downstream.onError(MissingBackpressureException.createDefault()); + } + } + } + + abstract void completion(); + + abstract void run(); + } + + static final class SamplerSubscriber<T> implements FlowableSubscriber<Object> { + final SamplePublisherSubscriber<T> parent; + SamplerSubscriber(SamplePublisherSubscriber<T> parent) { + this.parent = parent; + + } + + @Override + public void onSubscribe(Subscription s) { + parent.setOther(s); + } + + @Override + public void onNext(Object t) { + parent.run(); + } + + @Override + public void onError(Throwable t) { + parent.error(t); + } + + @Override + public void onComplete() { + parent.complete(); + } + } + + static final class SampleMainNoLast<T> extends SamplePublisherSubscriber<T> { + + private static final long serialVersionUID = -3029755663834015785L; + + SampleMainNoLast(Subscriber<? super T> actual, Publisher<?> other) { + super(actual, other); + } + + @Override + void completion() { + downstream.onComplete(); + } + + @Override + void run() { + emit(); + } + } + + static final class SampleMainEmitLast<T> extends SamplePublisherSubscriber<T> { + + private static final long serialVersionUID = -3029755663834015785L; + + final AtomicInteger wip; + + volatile boolean done; + + SampleMainEmitLast(Subscriber<? super T> actual, Publisher<?> other) { + super(actual, other); + this.wip = new AtomicInteger(); + } + + @Override + void completion() { + done = true; + if (wip.getAndIncrement() == 0) { + emit(); + downstream.onComplete(); + } + } + + @Override + void run() { + if (wip.getAndIncrement() == 0) { + do { + boolean d = done; + emit(); + if (d) { + downstream.onComplete(); + return; + } + } while (wip.decrementAndGet() != 0); + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableSampleTimed.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableSampleTimed.java new file mode 100644 index 0000000000..40551f4a8e --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableSampleTimed.java @@ -0,0 +1,199 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.*; + +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.Consumer; +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.MissingBackpressureException; +import io.reactivex.rxjava3.internal.disposables.*; +import io.reactivex.rxjava3.internal.subscriptions.SubscriptionHelper; +import io.reactivex.rxjava3.internal.util.BackpressureHelper; +import io.reactivex.rxjava3.subscribers.SerializedSubscriber; + +public final class FlowableSampleTimed<T> extends AbstractFlowableWithUpstream<T, T> { + final long period; + final TimeUnit unit; + final Scheduler scheduler; + final boolean emitLast; + final Consumer<? super T> onDropped; + + public FlowableSampleTimed(Flowable<T> source, long period, TimeUnit unit, Scheduler scheduler, boolean emitLast, Consumer<? super T> onDropped) { + super(source); + this.period = period; + this.unit = unit; + this.scheduler = scheduler; + this.emitLast = emitLast; + this.onDropped = onDropped; + } + + @Override + protected void subscribeActual(Subscriber<? super T> s) { + SerializedSubscriber<T> serial = new SerializedSubscriber<>(s); + if (emitLast) { + source.subscribe(new SampleTimedEmitLast<>(serial, period, unit, scheduler, onDropped)); + } else { + source.subscribe(new SampleTimedNoLast<>(serial, period, unit, scheduler, onDropped)); + } + } + + abstract static class SampleTimedSubscriber<T> extends AtomicReference<T> implements FlowableSubscriber<T>, Subscription, Runnable { + + private static final long serialVersionUID = -3517602651313910099L; + + final Subscriber<? super T> downstream; + final long period; + final TimeUnit unit; + final Scheduler scheduler; + final Consumer<? super T> onDropped; + + final AtomicLong requested = new AtomicLong(); + + final SequentialDisposable timer = new SequentialDisposable(); + + Subscription upstream; + + SampleTimedSubscriber(Subscriber<? super T> actual, long period, TimeUnit unit, Scheduler scheduler, Consumer<? super T> onDropped) { + this.downstream = actual; + this.period = period; + this.unit = unit; + this.scheduler = scheduler; + this.onDropped = onDropped; + } + + @Override + public void onSubscribe(Subscription s) { + if (SubscriptionHelper.validate(this.upstream, s)) { + this.upstream = s; + downstream.onSubscribe(this); + timer.replace(scheduler.schedulePeriodicallyDirect(this, period, period, unit)); + s.request(Long.MAX_VALUE); + } + } + + @Override + public void onNext(T t) { + T oldValue = getAndSet(t); + if (oldValue != null && onDropped != null) { + try { + onDropped.accept(oldValue); + } catch (Throwable throwable) { + Exceptions.throwIfFatal(throwable); + cancelTimer(); + upstream.cancel(); + downstream.onError(throwable); + } + } + } + + @Override + public void onError(Throwable t) { + cancelTimer(); + downstream.onError(t); + } + + @Override + public void onComplete() { + cancelTimer(); + complete(); + } + + void cancelTimer() { + DisposableHelper.dispose(timer); + } + + @Override + public void request(long n) { + if (SubscriptionHelper.validate(n)) { + BackpressureHelper.add(requested, n); + } + } + + @Override + public void cancel() { + cancelTimer(); + upstream.cancel(); + } + + void emit() { + T value = getAndSet(null); + if (value != null) { + long r = requested.get(); + if (r != 0L) { + downstream.onNext(value); + BackpressureHelper.produced(requested, 1); + } else { + cancel(); + downstream.onError(MissingBackpressureException.createDefault()); + } + } + } + + abstract void complete(); + } + + static final class SampleTimedNoLast<T> extends SampleTimedSubscriber<T> { + + private static final long serialVersionUID = -7139995637533111443L; + + SampleTimedNoLast(Subscriber<? super T> actual, long period, TimeUnit unit, Scheduler scheduler, Consumer<? super T> onDropped) { + super(actual, period, unit, scheduler, onDropped); + } + + @Override + void complete() { + downstream.onComplete(); + } + + @Override + public void run() { + emit(); + } + } + + static final class SampleTimedEmitLast<T> extends SampleTimedSubscriber<T> { + + private static final long serialVersionUID = -7139995637533111443L; + + final AtomicInteger wip; + + SampleTimedEmitLast(Subscriber<? super T> actual, long period, TimeUnit unit, Scheduler scheduler, Consumer<? super T> onDropped) { + super(actual, period, unit, scheduler, onDropped); + this.wip = new AtomicInteger(1); + } + + @Override + void complete() { + emit(); + if (wip.decrementAndGet() == 0) { + downstream.onComplete(); + } + } + + @Override + public void run() { + if (wip.incrementAndGet() == 2) { + emit(); + if (wip.decrementAndGet() == 0) { + downstream.onComplete(); + } + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableScalarXMap.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableScalarXMap.java new file mode 100644 index 0000000000..cb6471b878 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableScalarXMap.java @@ -0,0 +1,164 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.subscriptions.*; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +import java.util.Objects; + +/** + * Utility classes to work with scalar-sourced XMap operators (where X == { flat, concat, switch }). + */ +public final class FlowableScalarXMap { + + /** Utility class. */ + private FlowableScalarXMap() { + throw new IllegalStateException("No instances!"); + } + + /** + * Tries to subscribe to a possibly Supplier source's mapped Publisher. + * @param <T> the input value type + * @param <R> the output value type + * @param source the source Publisher + * @param subscriber the subscriber + * @param mapper the function mapping a scalar value into a Publisher + * @return true if successful, false if the caller should continue with the regular path. + */ + @SuppressWarnings("unchecked") + public static <T, R> boolean tryScalarXMapSubscribe(Publisher<T> source, + Subscriber<? super R> subscriber, + Function<? super T, ? extends Publisher<? extends R>> mapper) { + if (source instanceof Supplier) { + T t; + + try { + t = ((Supplier<T>)source).get(); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + EmptySubscription.error(ex, subscriber); + return true; + } + + if (t == null) { + EmptySubscription.complete(subscriber); + return true; + } + + Publisher<? extends R> r; + + try { + r = Objects.requireNonNull(mapper.apply(t), "The mapper returned a null Publisher"); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + EmptySubscription.error(ex, subscriber); + return true; + } + + if (r instanceof Supplier) { + R u; + + try { + u = ((Supplier<R>)r).get(); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + EmptySubscription.error(ex, subscriber); + return true; + } + + if (u == null) { + EmptySubscription.complete(subscriber); + return true; + } + subscriber.onSubscribe(new ScalarSubscription<>(subscriber, u)); + } else { + r.subscribe(subscriber); + } + + return true; + } + return false; + } + + /** + * Maps a scalar value into a Publisher and emits its values. + * + * @param <T> the scalar value type + * @param <U> the output value type + * @param value the scalar value to map + * @param mapper the function that gets the scalar value and should return + * a Publisher that gets streamed + * @return the new Flowable instance + */ + public static <T, U> Flowable<U> scalarXMap(final T value, final Function<? super T, ? extends Publisher<? extends U>> mapper) { + return RxJavaPlugins.onAssembly(new ScalarXMapFlowable<>(value, mapper)); + } + + /** + * Maps a scalar value to a Publisher and subscribes to it. + * + * @param <T> the scalar value type + * @param <R> the mapped Publisher's element type. + */ + static final class ScalarXMapFlowable<T, R> extends Flowable<R> { + + final T value; + + final Function<? super T, ? extends Publisher<? extends R>> mapper; + + ScalarXMapFlowable(T value, + Function<? super T, ? extends Publisher<? extends R>> mapper) { + this.value = value; + this.mapper = mapper; + } + + @SuppressWarnings("unchecked") + @Override + public void subscribeActual(Subscriber<? super R> s) { + Publisher<? extends R> other; + try { + other = Objects.requireNonNull(mapper.apply(value), "The mapper returned a null Publisher"); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + EmptySubscription.error(e, s); + return; + } + if (other instanceof Supplier) { + R u; + + try { + u = ((Supplier<R>)other).get(); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + EmptySubscription.error(ex, s); + return; + } + + if (u == null) { + EmptySubscription.complete(s); + return; + } + s.onSubscribe(new ScalarSubscription<>(s, u)); + } else { + other.subscribe(s); + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableScan.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableScan.java new file mode 100644 index 0000000000..84c348adfb --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableScan.java @@ -0,0 +1,117 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.BiFunction; +import io.reactivex.rxjava3.internal.subscriptions.SubscriptionHelper; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +import java.util.Objects; + +public final class FlowableScan<T> extends AbstractFlowableWithUpstream<T, T> { + final BiFunction<T, T, T> accumulator; + public FlowableScan(Flowable<T> source, BiFunction<T, T, T> accumulator) { + super(source); + this.accumulator = accumulator; + } + + @Override + protected void subscribeActual(Subscriber<? super T> s) { + source.subscribe(new ScanSubscriber<>(s, accumulator)); + } + + static final class ScanSubscriber<T> implements FlowableSubscriber<T>, Subscription { + final Subscriber<? super T> downstream; + final BiFunction<T, T, T> accumulator; + + Subscription upstream; + + T value; + + boolean done; + + ScanSubscriber(Subscriber<? super T> actual, BiFunction<T, T, T> accumulator) { + this.downstream = actual; + this.accumulator = accumulator; + } + + @Override + public void onSubscribe(Subscription s) { + if (SubscriptionHelper.validate(this.upstream, s)) { + this.upstream = s; + downstream.onSubscribe(this); + } + } + + @Override + public void onNext(T t) { + if (done) { + return; + } + final Subscriber<? super T> a = downstream; + T v = value; + if (v == null) { + value = t; + a.onNext(t); + } else { + T u; + + try { + u = Objects.requireNonNull(accumulator.apply(v, t), "The value returned by the accumulator is null"); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + upstream.cancel(); + onError(e); + return; + } + + value = u; + a.onNext(u); + } + } + + @Override + public void onError(Throwable t) { + if (done) { + RxJavaPlugins.onError(t); + return; + } + done = true; + downstream.onError(t); + } + + @Override + public void onComplete() { + if (done) { + return; + } + done = true; + downstream.onComplete(); + } + + @Override + public void request(long n) { + upstream.request(n); + } + + @Override + public void cancel() { + upstream.cancel(); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableScanSeed.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableScanSeed.java new file mode 100644 index 0000000000..2ffbc6c8e2 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableScanSeed.java @@ -0,0 +1,243 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import java.util.Objects; +import java.util.concurrent.atomic.*; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.subscriptions.*; +import io.reactivex.rxjava3.internal.util.BackpressureHelper; +import io.reactivex.rxjava3.operators.SimplePlainQueue; +import io.reactivex.rxjava3.operators.SpscArrayQueue; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +public final class FlowableScanSeed<T, R> extends AbstractFlowableWithUpstream<T, R> { + final BiFunction<R, ? super T, R> accumulator; + final Supplier<R> seedSupplier; + + public FlowableScanSeed(Flowable<T> source, Supplier<R> seedSupplier, BiFunction<R, ? super T, R> accumulator) { + super(source); + this.accumulator = accumulator; + this.seedSupplier = seedSupplier; + } + + @Override + protected void subscribeActual(Subscriber<? super R> s) { + R r; + + try { + r = Objects.requireNonNull(seedSupplier.get(), "The seed supplied is null"); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + EmptySubscription.error(e, s); + return; + } + + source.subscribe(new ScanSeedSubscriber<>(s, accumulator, r, bufferSize())); + } + + static final class ScanSeedSubscriber<T, R> + extends AtomicInteger + implements FlowableSubscriber<T>, Subscription { + private static final long serialVersionUID = -1776795561228106469L; + + final Subscriber<? super R> downstream; + + final BiFunction<R, ? super T, R> accumulator; + + final SimplePlainQueue<R> queue; + + final AtomicLong requested; + + final int prefetch; + + final int limit; + + volatile boolean cancelled; + + volatile boolean done; + Throwable error; + + Subscription upstream; + + R value; + + int consumed; + + ScanSeedSubscriber(Subscriber<? super R> actual, BiFunction<R, ? super T, R> accumulator, R value, int prefetch) { + this.downstream = actual; + this.accumulator = accumulator; + this.value = value; + this.prefetch = prefetch; + this.limit = prefetch - (prefetch >> 2); + this.queue = new SpscArrayQueue<>(prefetch); + this.queue.offer(value); + this.requested = new AtomicLong(); + } + + @Override + public void onSubscribe(Subscription s) { + if (SubscriptionHelper.validate(this.upstream, s)) { + this.upstream = s; + + downstream.onSubscribe(this); + + s.request(prefetch - 1); + } + } + + @Override + public void onNext(T t) { + if (done) { + return; + } + + R v = value; + try { + v = Objects.requireNonNull(accumulator.apply(v, t), "The accumulator returned a null value"); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + upstream.cancel(); + onError(ex); + return; + } + + value = v; + queue.offer(v); + drain(); + } + + @Override + public void onError(Throwable t) { + if (done) { + RxJavaPlugins.onError(t); + return; + } + error = t; + done = true; + drain(); + } + + @Override + public void onComplete() { + if (done) { + return; + } + done = true; + drain(); + } + + @Override + public void cancel() { + cancelled = true; + upstream.cancel(); + if (getAndIncrement() == 0) { + queue.clear(); + } + } + + @Override + public void request(long n) { + if (SubscriptionHelper.validate(n)) { + BackpressureHelper.add(requested, n); + drain(); + } + } + + void drain() { + if (getAndIncrement() != 0) { + return; + } + + int missed = 1; + Subscriber<? super R> a = downstream; + SimplePlainQueue<R> q = queue; + int lim = limit; + int c = consumed; + + for (;;) { + + long r = requested.get(); + long e = 0L; + + while (e != r) { + if (cancelled) { + q.clear(); + return; + } + boolean d = done; + + if (d) { + Throwable ex = error; + if (ex != null) { + q.clear(); + a.onError(ex); + return; + } + } + + R v = q.poll(); + boolean empty = v == null; + + if (d && empty) { + a.onComplete(); + return; + } + + if (empty) { + break; + } + + a.onNext(v); + + e++; + if (++c == lim) { + c = 0; + upstream.request(lim); + } + } + + if (e == r) { + if (done) { + Throwable ex = error; + if (ex != null) { + q.clear(); + a.onError(ex); + return; + } + if (q.isEmpty()) { + a.onComplete(); + return; + } + } + } + + if (e != 0L) { + BackpressureHelper.produced(requested, e); + } + + consumed = c; + missed = addAndGet(-missed); + if (missed == 0) { + break; + } + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableSequenceEqual.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableSequenceEqual.java new file mode 100644 index 0000000000..8dadbe0161 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableSequenceEqual.java @@ -0,0 +1,344 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import java.util.concurrent.atomic.*; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.BiPredicate; +import io.reactivex.rxjava3.internal.subscriptions.*; +import io.reactivex.rxjava3.internal.util.AtomicThrowable; +import io.reactivex.rxjava3.operators.QueueSubscription; +import io.reactivex.rxjava3.operators.SimpleQueue; +import io.reactivex.rxjava3.operators.SpscArrayQueue; + +public final class FlowableSequenceEqual<T> extends Flowable<Boolean> { + final Publisher<? extends T> first; + final Publisher<? extends T> second; + final BiPredicate<? super T, ? super T> comparer; + final int prefetch; + + public FlowableSequenceEqual(Publisher<? extends T> first, Publisher<? extends T> second, + BiPredicate<? super T, ? super T> comparer, int prefetch) { + this.first = first; + this.second = second; + this.comparer = comparer; + this.prefetch = prefetch; + } + + @Override + public void subscribeActual(Subscriber<? super Boolean> s) { + EqualCoordinator<T> parent = new EqualCoordinator<>(s, prefetch, comparer); + s.onSubscribe(parent); + parent.subscribe(first, second); + } + + /** + * Provides callbacks for the EqualSubscribers. + */ + interface EqualCoordinatorHelper { + + void drain(); + + void innerError(Throwable ex); + } + + static final class EqualCoordinator<T> extends DeferredScalarSubscription<Boolean> + implements EqualCoordinatorHelper { + + private static final long serialVersionUID = -6178010334400373240L; + + final BiPredicate<? super T, ? super T> comparer; + + final EqualSubscriber<T> first; + + final EqualSubscriber<T> second; + + final AtomicThrowable errors; + + final AtomicInteger wip; + + T v1; + + T v2; + + EqualCoordinator(Subscriber<? super Boolean> actual, int prefetch, BiPredicate<? super T, ? super T> comparer) { + super(actual); + this.comparer = comparer; + this.wip = new AtomicInteger(); + this.first = new EqualSubscriber<>(this, prefetch); + this.second = new EqualSubscriber<>(this, prefetch); + this.errors = new AtomicThrowable(); + } + + void subscribe(Publisher<? extends T> source1, Publisher<? extends T> source2) { + source1.subscribe(first); + source2.subscribe(second); + } + + @Override + public void cancel() { + super.cancel(); + first.cancel(); + second.cancel(); + errors.tryTerminateAndReport(); + if (wip.getAndIncrement() == 0) { + first.clear(); + second.clear(); + } + } + + void cancelAndClear() { + first.cancel(); + first.clear(); + second.cancel(); + second.clear(); + } + + @Override + public void drain() { + if (wip.getAndIncrement() != 0) { + return; + } + + int missed = 1; + + for (;;) { + SimpleQueue<T> q1 = first.queue; + SimpleQueue<T> q2 = second.queue; + + if (q1 != null && q2 != null) { + for (;;) { + if (isCancelled()) { + first.clear(); + second.clear(); + return; + } + + Throwable ex = errors.get(); + if (ex != null) { + cancelAndClear(); + + errors.tryTerminateConsumer(downstream); + return; + } + + boolean d1 = first.done; + + T a = v1; + if (a == null) { + try { + a = q1.poll(); + } catch (Throwable exc) { + Exceptions.throwIfFatal(exc); + cancelAndClear(); + errors.tryAddThrowableOrReport(exc); + errors.tryTerminateConsumer(downstream); + return; + } + v1 = a; + } + boolean e1 = a == null; + + boolean d2 = second.done; + T b = v2; + if (b == null) { + try { + b = q2.poll(); + } catch (Throwable exc) { + Exceptions.throwIfFatal(exc); + cancelAndClear(); + errors.tryAddThrowableOrReport(exc); + errors.tryTerminateConsumer(downstream); + return; + } + v2 = b; + } + + boolean e2 = b == null; + + if (d1 && d2 && e1 && e2) { + complete(true); + return; + } + if ((d1 && d2) && (e1 != e2)) { + cancelAndClear(); + complete(false); + return; + } + + if (e1 || e2) { + break; + } + + boolean c; + + try { + c = comparer.test(a, b); + } catch (Throwable exc) { + Exceptions.throwIfFatal(exc); + cancelAndClear(); + errors.tryAddThrowableOrReport(exc); + errors.tryTerminateConsumer(downstream); + return; + } + + if (!c) { + cancelAndClear(); + complete(false); + return; + } + + v1 = null; + v2 = null; + + first.request(); + second.request(); + } + + } else { + if (isCancelled()) { + first.clear(); + second.clear(); + return; + } + + Throwable ex = errors.get(); + if (ex != null) { + cancelAndClear(); + + errors.tryTerminateConsumer(downstream); + return; + } + } + + missed = wip.addAndGet(-missed); + if (missed == 0) { + break; + } + } + } + + @Override + public void innerError(Throwable t) { + if (errors.tryAddThrowableOrReport(t)) { + drain(); + } + } + } + + static final class EqualSubscriber<T> + extends AtomicReference<Subscription> + implements FlowableSubscriber<T> { + + private static final long serialVersionUID = 4804128302091633067L; + + final EqualCoordinatorHelper parent; + + final int prefetch; + + final int limit; + + long produced; + + volatile SimpleQueue<T> queue; + + volatile boolean done; + + int sourceMode; + + EqualSubscriber(EqualCoordinatorHelper parent, int prefetch) { + this.parent = parent; + this.limit = prefetch - (prefetch >> 2); + this.prefetch = prefetch; + } + + @Override + public void onSubscribe(Subscription s) { + if (SubscriptionHelper.setOnce(this, s)) { + if (s instanceof QueueSubscription) { + @SuppressWarnings("unchecked") + QueueSubscription<T> qs = (QueueSubscription<T>) s; + + int m = qs.requestFusion(QueueSubscription.ANY); + if (m == QueueSubscription.SYNC) { + sourceMode = m; + queue = qs; + done = true; + parent.drain(); + return; + } + if (m == QueueSubscription.ASYNC) { + sourceMode = m; + queue = qs; + s.request(prefetch); + return; + } + } + + queue = new SpscArrayQueue<>(prefetch); + + s.request(prefetch); + } + } + + @Override + public void onNext(T t) { + if (sourceMode == QueueSubscription.NONE) { + if (!queue.offer(t)) { + onError(MissingBackpressureException.createDefault()); + return; + } + } + parent.drain(); + } + + @Override + public void onError(Throwable t) { + parent.innerError(t); + } + + @Override + public void onComplete() { + done = true; + parent.drain(); + } + + public void request() { + if (sourceMode != QueueSubscription.SYNC) { + long p = produced + 1; + if (p >= limit) { + produced = 0; + get().request(p); + } else { + produced = p; + } + } + } + + public void cancel() { + SubscriptionHelper.cancel(this); + } + + void clear() { + SimpleQueue<T> sq = queue; + if (sq != null) { + sq.clear(); + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableSequenceEqualSingle.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableSequenceEqualSingle.java new file mode 100644 index 0000000000..5ede5990f9 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableSequenceEqualSingle.java @@ -0,0 +1,244 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import java.util.concurrent.atomic.AtomicInteger; + +import org.reactivestreams.Publisher; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.BiPredicate; +import io.reactivex.rxjava3.internal.fuseable.*; +import io.reactivex.rxjava3.internal.operators.flowable.FlowableSequenceEqual.*; +import io.reactivex.rxjava3.internal.subscriptions.SubscriptionHelper; +import io.reactivex.rxjava3.internal.util.AtomicThrowable; +import io.reactivex.rxjava3.operators.SimpleQueue; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +public final class FlowableSequenceEqualSingle<T> extends Single<Boolean> implements FuseToFlowable<Boolean> { + final Publisher<? extends T> first; + final Publisher<? extends T> second; + final BiPredicate<? super T, ? super T> comparer; + final int prefetch; + + public FlowableSequenceEqualSingle(Publisher<? extends T> first, Publisher<? extends T> second, + BiPredicate<? super T, ? super T> comparer, int prefetch) { + this.first = first; + this.second = second; + this.comparer = comparer; + this.prefetch = prefetch; + } + + @Override + public void subscribeActual(SingleObserver<? super Boolean> observer) { + EqualCoordinator<T> parent = new EqualCoordinator<>(observer, prefetch, comparer); + observer.onSubscribe(parent); + parent.subscribe(first, second); + } + + @Override + public Flowable<Boolean> fuseToFlowable() { + return RxJavaPlugins.onAssembly(new FlowableSequenceEqual<>(first, second, comparer, prefetch)); + } + + static final class EqualCoordinator<T> + extends AtomicInteger + implements Disposable, EqualCoordinatorHelper { + + private static final long serialVersionUID = -6178010334400373240L; + + final SingleObserver<? super Boolean> downstream; + + final BiPredicate<? super T, ? super T> comparer; + + final EqualSubscriber<T> first; + + final EqualSubscriber<T> second; + + final AtomicThrowable errors; + + T v1; + + T v2; + + EqualCoordinator(SingleObserver<? super Boolean> actual, int prefetch, BiPredicate<? super T, ? super T> comparer) { + this.downstream = actual; + this.comparer = comparer; + this.first = new EqualSubscriber<>(this, prefetch); + this.second = new EqualSubscriber<>(this, prefetch); + this.errors = new AtomicThrowable(); + } + + void subscribe(Publisher<? extends T> source1, Publisher<? extends T> source2) { + source1.subscribe(first); + source2.subscribe(second); + } + + @Override + public void dispose() { + first.cancel(); + second.cancel(); + errors.tryTerminateAndReport(); + if (getAndIncrement() == 0) { + first.clear(); + second.clear(); + } + } + + @Override + public boolean isDisposed() { + return first.get() == SubscriptionHelper.CANCELLED; + } + + void cancelAndClear() { + first.cancel(); + first.clear(); + second.cancel(); + second.clear(); + } + + @Override + public void drain() { + if (getAndIncrement() != 0) { + return; + } + + int missed = 1; + + for (;;) { + SimpleQueue<T> q1 = first.queue; + SimpleQueue<T> q2 = second.queue; + + if (q1 != null && q2 != null) { + for (;;) { + if (isDisposed()) { + first.clear(); + second.clear(); + return; + } + + Throwable ex = errors.get(); + if (ex != null) { + cancelAndClear(); + + errors.tryTerminateConsumer(downstream); + return; + } + + boolean d1 = first.done; + + T a = v1; + if (a == null) { + try { + a = q1.poll(); + } catch (Throwable exc) { + Exceptions.throwIfFatal(exc); + cancelAndClear(); + errors.tryAddThrowableOrReport(exc); + errors.tryTerminateConsumer(downstream); + return; + } + v1 = a; + } + boolean e1 = a == null; + + boolean d2 = second.done; + T b = v2; + if (b == null) { + try { + b = q2.poll(); + } catch (Throwable exc) { + Exceptions.throwIfFatal(exc); + cancelAndClear(); + errors.tryAddThrowableOrReport(exc); + errors.tryTerminateConsumer(downstream); + return; + } + v2 = b; + } + + boolean e2 = b == null; + + if (d1 && d2 && e1 && e2) { + downstream.onSuccess(true); + return; + } + if ((d1 && d2) && (e1 != e2)) { + cancelAndClear(); + downstream.onSuccess(false); + return; + } + + if (e1 || e2) { + break; + } + + boolean c; + + try { + c = comparer.test(a, b); + } catch (Throwable exc) { + Exceptions.throwIfFatal(exc); + cancelAndClear(); + errors.tryAddThrowableOrReport(exc); + errors.tryTerminateConsumer(downstream); + return; + } + + if (!c) { + cancelAndClear(); + downstream.onSuccess(false); + return; + } + + v1 = null; + v2 = null; + + first.request(); + second.request(); + } + + } else { + if (isDisposed()) { + first.clear(); + second.clear(); + return; + } + + Throwable ex = errors.get(); + if (ex != null) { + cancelAndClear(); + + errors.tryTerminateConsumer(downstream); + return; + } + } + + missed = addAndGet(-missed); + if (missed == 0) { + break; + } + } + } + + @Override + public void innerError(Throwable t) { + if (errors.tryAddThrowableOrReport(t)) { + drain(); + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableSerialized.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableSerialized.java new file mode 100644 index 0000000000..4c43ff9c6f --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableSerialized.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import org.reactivestreams.Subscriber; + +import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.subscribers.SerializedSubscriber; + +public final class FlowableSerialized<T> extends AbstractFlowableWithUpstream<T, T> { + public FlowableSerialized(Flowable<T> source) { + super(source); + } + + @Override + protected void subscribeActual(Subscriber<? super T> s) { + source.subscribe(new SerializedSubscriber<>(s)); + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableSingle.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableSingle.java new file mode 100644 index 0000000000..02077e5444 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableSingle.java @@ -0,0 +1,121 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import java.util.NoSuchElementException; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.internal.subscriptions.*; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +public final class FlowableSingle<T> extends AbstractFlowableWithUpstream<T, T> { + + final T defaultValue; + + final boolean failOnEmpty; + + public FlowableSingle(Flowable<T> source, T defaultValue, boolean failOnEmpty) { + super(source); + this.defaultValue = defaultValue; + this.failOnEmpty = failOnEmpty; + } + + @Override + protected void subscribeActual(Subscriber<? super T> s) { + source.subscribe(new SingleElementSubscriber<>(s, defaultValue, failOnEmpty)); + } + + static final class SingleElementSubscriber<T> extends DeferredScalarSubscription<T> + implements FlowableSubscriber<T> { + + private static final long serialVersionUID = -5526049321428043809L; + + final T defaultValue; + + final boolean failOnEmpty; + + Subscription upstream; + + boolean done; + + SingleElementSubscriber(Subscriber<? super T> actual, T defaultValue, boolean failOnEmpty) { + super(actual); + this.defaultValue = defaultValue; + this.failOnEmpty = failOnEmpty; + } + + @Override + public void onSubscribe(Subscription s) { + if (SubscriptionHelper.validate(this.upstream, s)) { + this.upstream = s; + downstream.onSubscribe(this); + s.request(Long.MAX_VALUE); + } + } + + @Override + public void onNext(T t) { + if (done) { + return; + } + if (value != null) { + done = true; + upstream.cancel(); + downstream.onError(new IllegalArgumentException("Sequence contains more than one element!")); + return; + } + value = t; + } + + @Override + public void onError(Throwable t) { + if (done) { + RxJavaPlugins.onError(t); + return; + } + done = true; + downstream.onError(t); + } + + @Override + public void onComplete() { + if (done) { + return; + } + done = true; + T v = value; + value = null; + if (v == null) { + v = defaultValue; + } + if (v == null) { + if (failOnEmpty) { + downstream.onError(new NoSuchElementException()); + } else { + downstream.onComplete(); + } + } else { + complete(v); + } + } + + @Override + public void cancel() { + super.cancel(); + upstream.cancel(); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableSingleMaybe.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableSingleMaybe.java new file mode 100644 index 0000000000..51beeff27c --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableSingleMaybe.java @@ -0,0 +1,119 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import org.reactivestreams.Subscription; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.fuseable.FuseToFlowable; +import io.reactivex.rxjava3.internal.subscriptions.SubscriptionHelper; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +public final class FlowableSingleMaybe<T> extends Maybe<T> implements FuseToFlowable<T> { + + final Flowable<T> source; + + public FlowableSingleMaybe(Flowable<T> source) { + this.source = source; + } + + @Override + protected void subscribeActual(MaybeObserver<? super T> observer) { + source.subscribe(new SingleElementSubscriber<>(observer)); + } + + @Override + public Flowable<T> fuseToFlowable() { + return RxJavaPlugins.onAssembly(new FlowableSingle<>(source, null, false)); + } + + static final class SingleElementSubscriber<T> + implements FlowableSubscriber<T>, Disposable { + + final MaybeObserver<? super T> downstream; + + Subscription upstream; + + boolean done; + + T value; + + SingleElementSubscriber(MaybeObserver<? super T> downstream) { + this.downstream = downstream; + } + + @Override + public void onSubscribe(Subscription s) { + if (SubscriptionHelper.validate(this.upstream, s)) { + this.upstream = s; + downstream.onSubscribe(this); + s.request(Long.MAX_VALUE); + } + } + + @Override + public void onNext(T t) { + if (done) { + return; + } + if (value != null) { + done = true; + upstream.cancel(); + upstream = SubscriptionHelper.CANCELLED; + downstream.onError(new IllegalArgumentException("Sequence contains more than one element!")); + return; + } + value = t; + } + + @Override + public void onError(Throwable t) { + if (done) { + RxJavaPlugins.onError(t); + return; + } + done = true; + upstream = SubscriptionHelper.CANCELLED; + downstream.onError(t); + } + + @Override + public void onComplete() { + if (done) { + return; + } + done = true; + upstream = SubscriptionHelper.CANCELLED; + T v = value; + value = null; + if (v == null) { + downstream.onComplete(); + } else { + downstream.onSuccess(v); + } + } + + @Override + public void dispose() { + upstream.cancel(); + upstream = SubscriptionHelper.CANCELLED; + } + + @Override + public boolean isDisposed() { + return upstream == SubscriptionHelper.CANCELLED; + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableSingleSingle.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableSingleSingle.java new file mode 100644 index 0000000000..8a4f7fe339 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableSingleSingle.java @@ -0,0 +1,131 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import java.util.NoSuchElementException; + +import org.reactivestreams.Subscription; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.fuseable.FuseToFlowable; +import io.reactivex.rxjava3.internal.subscriptions.SubscriptionHelper; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +public final class FlowableSingleSingle<T> extends Single<T> implements FuseToFlowable<T> { + + final Flowable<T> source; + + final T defaultValue; + + public FlowableSingleSingle(Flowable<T> source, T defaultValue) { + this.source = source; + this.defaultValue = defaultValue; + } + + @Override + protected void subscribeActual(SingleObserver<? super T> observer) { + source.subscribe(new SingleElementSubscriber<>(observer, defaultValue)); + } + + @Override + public Flowable<T> fuseToFlowable() { + return RxJavaPlugins.onAssembly(new FlowableSingle<>(source, defaultValue, true)); + } + + static final class SingleElementSubscriber<T> + implements FlowableSubscriber<T>, Disposable { + + final SingleObserver<? super T> downstream; + + final T defaultValue; + + Subscription upstream; + + boolean done; + + T value; + + SingleElementSubscriber(SingleObserver<? super T> actual, T defaultValue) { + this.downstream = actual; + this.defaultValue = defaultValue; + } + + @Override + public void onSubscribe(Subscription s) { + if (SubscriptionHelper.validate(this.upstream, s)) { + this.upstream = s; + downstream.onSubscribe(this); + s.request(Long.MAX_VALUE); + } + } + + @Override + public void onNext(T t) { + if (done) { + return; + } + if (value != null) { + done = true; + upstream.cancel(); + upstream = SubscriptionHelper.CANCELLED; + downstream.onError(new IllegalArgumentException("Sequence contains more than one element!")); + return; + } + value = t; + } + + @Override + public void onError(Throwable t) { + if (done) { + RxJavaPlugins.onError(t); + return; + } + done = true; + upstream = SubscriptionHelper.CANCELLED; + downstream.onError(t); + } + + @Override + public void onComplete() { + if (done) { + return; + } + done = true; + upstream = SubscriptionHelper.CANCELLED; + T v = value; + value = null; + if (v == null) { + v = defaultValue; + } + + if (v != null) { + downstream.onSuccess(v); + } else { + downstream.onError(new NoSuchElementException()); + } + } + + @Override + public void dispose() { + upstream.cancel(); + upstream = SubscriptionHelper.CANCELLED; + } + + @Override + public boolean isDisposed() { + return upstream == SubscriptionHelper.CANCELLED; + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableSkip.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableSkip.java new file mode 100644 index 0000000000..d9c2f49d1e --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableSkip.java @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.internal.subscriptions.SubscriptionHelper; + +public final class FlowableSkip<T> extends AbstractFlowableWithUpstream<T, T> { + final long n; + public FlowableSkip(Flowable<T> source, long n) { + super(source); + this.n = n; + } + + @Override + protected void subscribeActual(Subscriber<? super T> s) { + source.subscribe(new SkipSubscriber<>(s, n)); + } + + static final class SkipSubscriber<T> implements FlowableSubscriber<T>, Subscription { + final Subscriber<? super T> downstream; + long remaining; + + Subscription upstream; + + SkipSubscriber(Subscriber<? super T> actual, long n) { + this.downstream = actual; + this.remaining = n; + } + + @Override + public void onSubscribe(Subscription s) { + if (SubscriptionHelper.validate(this.upstream, s)) { + long n = remaining; + this.upstream = s; + downstream.onSubscribe(this); + s.request(n); + } + } + + @Override + public void onNext(T t) { + if (remaining != 0L) { + remaining--; + } else { + downstream.onNext(t); + } + } + + @Override + public void onError(Throwable t) { + downstream.onError(t); + } + + @Override + public void onComplete() { + downstream.onComplete(); + } + + @Override + public void request(long n) { + upstream.request(n); + } + + @Override + public void cancel() { + upstream.cancel(); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableSkipLast.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableSkipLast.java new file mode 100644 index 0000000000..9f19ee86ba --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableSkipLast.java @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import java.util.ArrayDeque; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.internal.subscriptions.SubscriptionHelper; + +public final class FlowableSkipLast<T> extends AbstractFlowableWithUpstream<T, T> { + final int skip; + + public FlowableSkipLast(Flowable<T> source, int skip) { + super(source); + this.skip = skip; + } + + @Override + protected void subscribeActual(Subscriber<? super T> s) { + source.subscribe(new SkipLastSubscriber<>(s, skip)); + } + + static final class SkipLastSubscriber<T> extends ArrayDeque<T> implements FlowableSubscriber<T>, Subscription { + + private static final long serialVersionUID = -3807491841935125653L; + final Subscriber<? super T> downstream; + final int skip; + + Subscription upstream; + + SkipLastSubscriber(Subscriber<? super T> actual, int skip) { + super(skip); + this.downstream = actual; + this.skip = skip; + } + + @Override + public void onSubscribe(Subscription s) { + if (SubscriptionHelper.validate(this.upstream, s)) { + this.upstream = s; + downstream.onSubscribe(this); + } + } + + @Override + public void onNext(T t) { + if (skip == size()) { + downstream.onNext(poll()); + } else { + upstream.request(1); + } + offer(t); + } + + @Override + public void onError(Throwable t) { + downstream.onError(t); + } + + @Override + public void onComplete() { + downstream.onComplete(); + } + + @Override + public void request(long n) { + upstream.request(n); + } + + @Override + public void cancel() { + upstream.cancel(); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableSkipLastTimed.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableSkipLastTimed.java new file mode 100644 index 0000000000..66f7d240f5 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableSkipLastTimed.java @@ -0,0 +1,218 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.*; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.internal.subscriptions.SubscriptionHelper; +import io.reactivex.rxjava3.internal.util.BackpressureHelper; +import io.reactivex.rxjava3.operators.SpscLinkedArrayQueue; + +public final class FlowableSkipLastTimed<T> extends AbstractFlowableWithUpstream<T, T> { + final long time; + final TimeUnit unit; + final Scheduler scheduler; + final int bufferSize; + final boolean delayError; + + public FlowableSkipLastTimed(Flowable<T> source, long time, TimeUnit unit, Scheduler scheduler, int bufferSize, boolean delayError) { + super(source); + this.time = time; + this.unit = unit; + this.scheduler = scheduler; + this.bufferSize = bufferSize; + this.delayError = delayError; + } + + @Override + protected void subscribeActual(Subscriber<? super T> s) { + source.subscribe(new SkipLastTimedSubscriber<>(s, time, unit, scheduler, bufferSize, delayError)); + } + + static final class SkipLastTimedSubscriber<T> extends AtomicInteger implements FlowableSubscriber<T>, Subscription { + + private static final long serialVersionUID = -5677354903406201275L; + final Subscriber<? super T> downstream; + final long time; + final TimeUnit unit; + final Scheduler scheduler; + final SpscLinkedArrayQueue<Object> queue; + final boolean delayError; + + Subscription upstream; + + final AtomicLong requested = new AtomicLong(); + + volatile boolean cancelled; + + volatile boolean done; + Throwable error; + + SkipLastTimedSubscriber(Subscriber<? super T> actual, long time, TimeUnit unit, Scheduler scheduler, int bufferSize, boolean delayError) { + this.downstream = actual; + this.time = time; + this.unit = unit; + this.scheduler = scheduler; + this.queue = new SpscLinkedArrayQueue<>(bufferSize); + this.delayError = delayError; + } + + @Override + public void onSubscribe(Subscription s) { + if (SubscriptionHelper.validate(this.upstream, s)) { + this.upstream = s; + downstream.onSubscribe(this); + s.request(Long.MAX_VALUE); + } + } + + @Override + public void onNext(T t) { + long now = scheduler.now(unit); + + queue.offer(now, t); + + drain(); + } + + @Override + public void onError(Throwable t) { + error = t; + done = true; + drain(); + } + + @Override + public void onComplete() { + done = true; + drain(); + } + + @Override + public void request(long n) { + if (SubscriptionHelper.validate(n)) { + BackpressureHelper.add(requested, n); + drain(); + } + } + + @Override + public void cancel() { + if (!cancelled) { + cancelled = true; + upstream.cancel(); + + if (getAndIncrement() == 0) { + queue.clear(); + } + } + } + + void drain() { + if (getAndIncrement() != 0) { + return; + } + + int missed = 1; + + final Subscriber<? super T> a = downstream; + final SpscLinkedArrayQueue<Object> q = queue; + final boolean delayError = this.delayError; + final TimeUnit unit = this.unit; + final Scheduler scheduler = this.scheduler; + final long time = this.time; + + for (;;) { + + long r = requested.get(); + long e = 0L; + + while (e != r) { + boolean d = done; + + Long ts = (Long)q.peek(); + + boolean empty = ts == null; + + long now = scheduler.now(unit); + + if (!empty && ts > now - time) { + empty = true; + } + + if (checkTerminated(d, empty, a, delayError)) { + return; + } + + if (empty) { + break; + } + + q.poll(); + @SuppressWarnings("unchecked") + T v = (T)q.poll(); + + a.onNext(v); + + e++; + } + + if (e != 0L) { + BackpressureHelper.produced(requested, e); + } + + missed = addAndGet(-missed); + if (missed == 0) { + break; + } + } + } + + boolean checkTerminated(boolean d, boolean empty, Subscriber<? super T> a, boolean delayError) { + if (cancelled) { + queue.clear(); + return true; + } + if (d) { + if (delayError) { + if (empty) { + Throwable e = error; + if (e != null) { + a.onError(e); + } else { + a.onComplete(); + } + return true; + } + } else { + Throwable e = error; + if (e != null) { + queue.clear(); + a.onError(e); + return true; + } else + if (empty) { + a.onComplete(); + return true; + } + } + } + return false; + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableSkipUntil.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableSkipUntil.java new file mode 100644 index 0000000000..a7c51f9941 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableSkipUntil.java @@ -0,0 +1,138 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import java.util.concurrent.atomic.*; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.internal.subscriptions.SubscriptionHelper; +import io.reactivex.rxjava3.internal.util.*; +import io.reactivex.rxjava3.operators.ConditionalSubscriber; + +public final class FlowableSkipUntil<T, U> extends AbstractFlowableWithUpstream<T, T> { + final Publisher<U> other; + public FlowableSkipUntil(Flowable<T> source, Publisher<U> other) { + super(source); + this.other = other; + } + + @Override + protected void subscribeActual(Subscriber<? super T> child) { + SkipUntilMainSubscriber<T> parent = new SkipUntilMainSubscriber<>(child); + child.onSubscribe(parent); + + other.subscribe(parent.other); + + source.subscribe(parent); + } + + static final class SkipUntilMainSubscriber<T> extends AtomicInteger + implements ConditionalSubscriber<T>, Subscription { + private static final long serialVersionUID = -6270983465606289181L; + + final Subscriber<? super T> downstream; + + final AtomicReference<Subscription> upstream; + + final AtomicLong requested; + + final OtherSubscriber other; + + final AtomicThrowable error; + + volatile boolean gate; + + SkipUntilMainSubscriber(Subscriber<? super T> downstream) { + this.downstream = downstream; + this.upstream = new AtomicReference<>(); + this.requested = new AtomicLong(); + this.other = new OtherSubscriber(); + this.error = new AtomicThrowable(); + } + + @Override + public void onSubscribe(Subscription s) { + SubscriptionHelper.deferredSetOnce(this.upstream, requested, s); + } + + @Override + public void onNext(T t) { + if (!tryOnNext(t)) { + upstream.get().request(1); + } + } + + @Override + public boolean tryOnNext(T t) { + if (gate) { + HalfSerializer.onNext(downstream, t, this, error); + return true; + } + return false; + } + + @Override + public void onError(Throwable t) { + SubscriptionHelper.cancel(other); + HalfSerializer.onError(downstream, t, SkipUntilMainSubscriber.this, error); + } + + @Override + public void onComplete() { + SubscriptionHelper.cancel(other); + HalfSerializer.onComplete(downstream, this, error); + } + + @Override + public void request(long n) { + SubscriptionHelper.deferredRequest(upstream, requested, n); + } + + @Override + public void cancel() { + SubscriptionHelper.cancel(upstream); + SubscriptionHelper.cancel(other); + } + + final class OtherSubscriber extends AtomicReference<Subscription> + implements FlowableSubscriber<Object> { + + private static final long serialVersionUID = -5592042965931999169L; + + @Override + public void onSubscribe(Subscription s) { + SubscriptionHelper.setOnce(this, s, Long.MAX_VALUE); + } + + @Override + public void onNext(Object t) { + gate = true; + get().cancel(); + } + + @Override + public void onError(Throwable t) { + SubscriptionHelper.cancel(upstream); + HalfSerializer.onError(downstream, t, SkipUntilMainSubscriber.this, error); + } + + @Override + public void onComplete() { + gate = true; + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableSkipWhile.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableSkipWhile.java new file mode 100644 index 0000000000..be88184f38 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableSkipWhile.java @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.Predicate; +import io.reactivex.rxjava3.internal.subscriptions.SubscriptionHelper; + +public final class FlowableSkipWhile<T> extends AbstractFlowableWithUpstream<T, T> { + final Predicate<? super T> predicate; + public FlowableSkipWhile(Flowable<T> source, Predicate<? super T> predicate) { + super(source); + this.predicate = predicate; + } + + @Override + protected void subscribeActual(Subscriber<? super T> s) { + source.subscribe(new SkipWhileSubscriber<>(s, predicate)); + } + + static final class SkipWhileSubscriber<T> implements FlowableSubscriber<T>, Subscription { + final Subscriber<? super T> downstream; + final Predicate<? super T> predicate; + Subscription upstream; + boolean notSkipping; + SkipWhileSubscriber(Subscriber<? super T> actual, Predicate<? super T> predicate) { + this.downstream = actual; + this.predicate = predicate; + } + + @Override + public void onSubscribe(Subscription s) { + if (SubscriptionHelper.validate(this.upstream, s)) { + this.upstream = s; + downstream.onSubscribe(this); + } + } + + @Override + public void onNext(T t) { + if (notSkipping) { + downstream.onNext(t); + } else { + boolean b; + try { + b = predicate.test(t); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + upstream.cancel(); + downstream.onError(e); + return; + } + if (b) { + upstream.request(1); + } else { + notSkipping = true; + downstream.onNext(t); + } + } + } + + @Override + public void onError(Throwable t) { + downstream.onError(t); + } + + @Override + public void onComplete() { + downstream.onComplete(); + } + + @Override + public void request(long n) { + upstream.request(n); + } + + @Override + public void cancel() { + upstream.cancel(); + } + + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableSubscribeOn.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableSubscribeOn.java new file mode 100644 index 0000000000..7d4a51f0f2 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableSubscribeOn.java @@ -0,0 +1,160 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import java.util.concurrent.atomic.*; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.internal.subscriptions.SubscriptionHelper; +import io.reactivex.rxjava3.internal.util.BackpressureHelper; + +/** + * Subscribes to the source Flowable on the specified Scheduler and makes + * sure downstream requests are scheduled there as well. + * + * @param <T> the value type emitted + */ +public final class FlowableSubscribeOn<T> extends AbstractFlowableWithUpstream<T , T> { + + final Scheduler scheduler; + + final boolean nonScheduledRequests; + + public FlowableSubscribeOn(Flowable<T> source, Scheduler scheduler, boolean nonScheduledRequests) { + super(source); + this.scheduler = scheduler; + this.nonScheduledRequests = nonScheduledRequests; + } + + @Override + public void subscribeActual(final Subscriber<? super T> s) { + Scheduler.Worker w = scheduler.createWorker(); + final SubscribeOnSubscriber<T> sos = new SubscribeOnSubscriber<>(s, w, source, nonScheduledRequests); + s.onSubscribe(sos); + + w.schedule(sos); + } + + static final class SubscribeOnSubscriber<T> extends AtomicReference<Thread> + implements FlowableSubscriber<T>, Subscription, Runnable { + + private static final long serialVersionUID = 8094547886072529208L; + + final Subscriber<? super T> downstream; + + final Scheduler.Worker worker; + + final AtomicReference<Subscription> upstream; + + final AtomicLong requested; + + final boolean nonScheduledRequests; + + Publisher<T> source; + + SubscribeOnSubscriber(Subscriber<? super T> actual, Scheduler.Worker worker, Publisher<T> source, boolean requestOn) { + this.downstream = actual; + this.worker = worker; + this.source = source; + this.upstream = new AtomicReference<>(); + this.requested = new AtomicLong(); + this.nonScheduledRequests = !requestOn; + } + + @Override + public void run() { + lazySet(Thread.currentThread()); + Publisher<T> src = source; + source = null; + src.subscribe(this); + } + + @Override + public void onSubscribe(Subscription s) { + if (SubscriptionHelper.setOnce(this.upstream, s)) { + long r = requested.getAndSet(0L); + if (r != 0L) { + requestUpstream(r, s); + } + } + } + + @Override + public void onNext(T t) { + downstream.onNext(t); + } + + @Override + public void onError(Throwable t) { + downstream.onError(t); + worker.dispose(); + } + + @Override + public void onComplete() { + downstream.onComplete(); + worker.dispose(); + } + + @Override + public void request(final long n) { + if (SubscriptionHelper.validate(n)) { + Subscription s = this.upstream.get(); + if (s != null) { + requestUpstream(n, s); + } else { + BackpressureHelper.add(requested, n); + s = this.upstream.get(); + if (s != null) { + long r = requested.getAndSet(0L); + if (r != 0L) { + requestUpstream(r, s); + } + } + } + } + } + + void requestUpstream(final long n, final Subscription s) { + if (nonScheduledRequests || Thread.currentThread() == get()) { + s.request(n); + } else { + worker.schedule(new Request(s, n)); + } + } + + @Override + public void cancel() { + SubscriptionHelper.cancel(upstream); + worker.dispose(); + } + + static final class Request implements Runnable { + final Subscription upstream; + final long n; + + Request(Subscription s, long n) { + this.upstream = s; + this.n = n; + } + + @Override + public void run() { + upstream.request(n); + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableSwitchIfEmpty.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableSwitchIfEmpty.java new file mode 100644 index 0000000000..1d45216f14 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableSwitchIfEmpty.java @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.internal.subscriptions.SubscriptionArbiter; + +public final class FlowableSwitchIfEmpty<T> extends AbstractFlowableWithUpstream<T, T> { + final Publisher<? extends T> other; + public FlowableSwitchIfEmpty(Flowable<T> source, Publisher<? extends T> other) { + super(source); + this.other = other; + } + + @Override + protected void subscribeActual(Subscriber<? super T> s) { + SwitchIfEmptySubscriber<T> parent = new SwitchIfEmptySubscriber<>(s, other); + s.onSubscribe(parent.arbiter); + source.subscribe(parent); + } + + static final class SwitchIfEmptySubscriber<T> implements FlowableSubscriber<T> { + final Subscriber<? super T> downstream; + final Publisher<? extends T> other; + final SubscriptionArbiter arbiter; + + boolean empty; + + SwitchIfEmptySubscriber(Subscriber<? super T> actual, Publisher<? extends T> other) { + this.downstream = actual; + this.other = other; + this.empty = true; + this.arbiter = new SubscriptionArbiter(false); + } + + @Override + public void onSubscribe(Subscription s) { + arbiter.setSubscription(s); + } + + @Override + public void onNext(T t) { + if (empty) { + empty = false; + } + downstream.onNext(t); + } + + @Override + public void onError(Throwable t) { + downstream.onError(t); + } + + @Override + public void onComplete() { + if (empty) { + empty = false; + other.subscribe(this); + } else { + downstream.onComplete(); + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableSwitchMap.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableSwitchMap.java new file mode 100644 index 0000000000..4d4521c655 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableSwitchMap.java @@ -0,0 +1,425 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import java.util.Objects; +import java.util.concurrent.atomic.*; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.subscriptions.SubscriptionHelper; +import io.reactivex.rxjava3.internal.util.*; +import io.reactivex.rxjava3.operators.QueueSubscription; +import io.reactivex.rxjava3.operators.SimpleQueue; +import io.reactivex.rxjava3.operators.SpscArrayQueue; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +public final class FlowableSwitchMap<T, R> extends AbstractFlowableWithUpstream<T, R> { + final Function<? super T, ? extends Publisher<? extends R>> mapper; + final int bufferSize; + final boolean delayErrors; + + public FlowableSwitchMap(Flowable<T> source, + Function<? super T, ? extends Publisher<? extends R>> mapper, int bufferSize, + boolean delayErrors) { + super(source); + this.mapper = mapper; + this.bufferSize = bufferSize; + this.delayErrors = delayErrors; + } + + @Override + protected void subscribeActual(Subscriber<? super R> s) { + if (FlowableScalarXMap.tryScalarXMapSubscribe(source, s, mapper)) { + return; + } + source.subscribe(new SwitchMapSubscriber<>(s, mapper, bufferSize, delayErrors)); + } + + static final class SwitchMapSubscriber<T, R> extends AtomicInteger implements FlowableSubscriber<T>, Subscription { + + private static final long serialVersionUID = -3491074160481096299L; + final Subscriber<? super R> downstream; + final Function<? super T, ? extends Publisher<? extends R>> mapper; + final int bufferSize; + final boolean delayErrors; + + volatile boolean done; + final AtomicThrowable errors; + + volatile boolean cancelled; + + Subscription upstream; + + final AtomicReference<SwitchMapInnerSubscriber<T, R>> active = new AtomicReference<>(); + + final AtomicLong requested = new AtomicLong(); + + static final SwitchMapInnerSubscriber<Object, Object> CANCELLED; + static { + CANCELLED = new SwitchMapInnerSubscriber<>(null, -1L, 1); + CANCELLED.cancel(); + } + + volatile long unique; + + SwitchMapSubscriber(Subscriber<? super R> actual, + Function<? super T, ? extends Publisher<? extends R>> mapper, int bufferSize, + boolean delayErrors) { + this.downstream = actual; + this.mapper = mapper; + this.bufferSize = bufferSize; + this.delayErrors = delayErrors; + this.errors = new AtomicThrowable(); + } + + @Override + public void onSubscribe(Subscription s) { + if (SubscriptionHelper.validate(this.upstream, s)) { + this.upstream = s; + downstream.onSubscribe(this); + } + } + + @Override + public void onNext(T t) { + if (done) { + return; + } + + long c = unique + 1; + unique = c; + + SwitchMapInnerSubscriber<T, R> inner = active.get(); + if (inner != null) { + inner.cancel(); + } + + Publisher<? extends R> p; + try { + p = Objects.requireNonNull(mapper.apply(t), "The publisher returned is null"); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + upstream.cancel(); + onError(e); + return; + } + + SwitchMapInnerSubscriber<T, R> nextInner = new SwitchMapInnerSubscriber<>(this, c, bufferSize); + + for (;;) { + inner = active.get(); + if (inner == CANCELLED) { + break; + } + if (active.compareAndSet(inner, nextInner)) { + p.subscribe(nextInner); + break; + } + } + } + + @Override + public void onError(Throwable t) { + if (!done && errors.tryAddThrowable(t)) { + if (!delayErrors) { + disposeInner(); + } + done = true; + drain(); + } else { + RxJavaPlugins.onError(t); + } + } + + @Override + public void onComplete() { + if (done) { + return; + } + done = true; + drain(); + } + + @Override + public void request(long n) { + if (SubscriptionHelper.validate(n)) { + BackpressureHelper.add(requested, n); + if (unique == 0L) { + upstream.request(Long.MAX_VALUE); + } else { + drain(); + } + } + } + + @Override + public void cancel() { + if (!cancelled) { + cancelled = true; + upstream.cancel(); + + disposeInner(); + + errors.tryTerminateAndReport(); + } + } + + @SuppressWarnings("unchecked") + void disposeInner() { + SwitchMapInnerSubscriber<T, R> a = active.getAndSet((SwitchMapInnerSubscriber<T, R>)CANCELLED); + if (a != CANCELLED && a != null) { + a.cancel(); + } + } + + void drain() { + if (getAndIncrement() != 0) { + return; + } + + final Subscriber<? super R> a = downstream; + + int missing = 1; + + for (;;) { + + if (cancelled) { + return; + } + + if (done) { + if (delayErrors) { + if (active.get() == null) { + errors.tryTerminateConsumer(a); + return; + } + } else { + Throwable err = errors.get(); + if (err != null) { + disposeInner(); + errors.tryTerminateConsumer(a); + return; + } else + if (active.get() == null) { + a.onComplete(); + return; + } + } + } + + SwitchMapInnerSubscriber<T, R> inner = active.get(); + SimpleQueue<R> q = inner != null ? inner.queue : null; + if (q != null) { + long r = requested.get(); + long e = 0L; + boolean retry = false; + + while (e != r) { + if (cancelled) { + return; + } + + boolean d = inner.done; + R v; + + try { + v = q.poll(); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + inner.cancel(); + errors.tryAddThrowableOrReport(ex); + d = true; + v = null; + } + boolean empty = v == null; + + if (inner != active.get()) { + retry = true; + break; + } + + if (d) { + if (!delayErrors) { + Throwable err = errors.get(); + if (err != null) { + errors.tryTerminateConsumer(a); + return; + } else + if (empty) { + active.compareAndSet(inner, null); + retry = true; + break; + } + } else { + if (empty) { + active.compareAndSet(inner, null); + retry = true; + break; + } + } + } + + if (empty) { + break; + } + + a.onNext(v); + + e++; + } + + if (e == r) { + if (inner.done) { + if (!delayErrors) { + Throwable err = errors.get(); + if (err != null) { + disposeInner(); + errors.tryTerminateConsumer(a); + return; + } else + if (q.isEmpty()) { + active.compareAndSet(inner, null); + continue; + } + } else { + if (q.isEmpty()) { + active.compareAndSet(inner, null); + continue; + } + } + } + } + + if (e != 0L) { + if (!cancelled) { + if (r != Long.MAX_VALUE) { + requested.addAndGet(-e); + } + inner.request(e); + } + } + + if (retry) { + continue; + } + } + + missing = addAndGet(-missing); + if (missing == 0) { + break; + } + } + } + } + + static final class SwitchMapInnerSubscriber<T, R> + extends AtomicReference<Subscription> implements FlowableSubscriber<R> { + + private static final long serialVersionUID = 3837284832786408377L; + final SwitchMapSubscriber<T, R> parent; + final long index; + final int bufferSize; + + volatile SimpleQueue<R> queue; + + volatile boolean done; + + int fusionMode; + + SwitchMapInnerSubscriber(SwitchMapSubscriber<T, R> parent, long index, int bufferSize) { + this.parent = parent; + this.index = index; + this.bufferSize = bufferSize; + } + + @Override + public void onSubscribe(Subscription s) { + if (SubscriptionHelper.setOnce(this, s)) { + if (s instanceof QueueSubscription) { + @SuppressWarnings("unchecked") + QueueSubscription<R> qs = (QueueSubscription<R>) s; + + int m = qs.requestFusion(QueueSubscription.ANY | QueueSubscription.BOUNDARY); + if (m == QueueSubscription.SYNC) { + fusionMode = m; + queue = qs; + done = true; + parent.drain(); + return; + } + if (m == QueueSubscription.ASYNC) { + fusionMode = m; + queue = qs; + s.request(bufferSize); + return; + } + } + + queue = new SpscArrayQueue<>(bufferSize); + + s.request(bufferSize); + } + } + + @Override + public void onNext(R t) { + SwitchMapSubscriber<T, R> p = parent; + if (index == p.unique) { + if (fusionMode == QueueSubscription.NONE && !queue.offer(t)) { + onError(new QueueOverflowException()); + return; + } + p.drain(); + } + } + + @Override + public void onError(Throwable t) { + SwitchMapSubscriber<T, R> p = parent; + if (index == p.unique && p.errors.tryAddThrowable(t)) { + if (!p.delayErrors) { + p.upstream.cancel(); + p.done = true; + } + done = true; + p.drain(); + } else { + RxJavaPlugins.onError(t); + } + } + + @Override + public void onComplete() { + SwitchMapSubscriber<T, R> p = parent; + if (index == p.unique) { + done = true; + p.drain(); + } + } + + public void cancel() { + SubscriptionHelper.cancel(this); + } + + public void request(long n) { + if (fusionMode != QueueSubscription.SYNC) { + get().request(n); + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableTake.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableTake.java new file mode 100644 index 0000000000..ea5ca1c4e1 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableTake.java @@ -0,0 +1,124 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import java.util.concurrent.atomic.AtomicLong; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.internal.subscriptions.*; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +public final class FlowableTake<T> extends AbstractFlowableWithUpstream<T, T> { + + final long n; + + public FlowableTake(Flowable<T> source, long n) { + super(source); + this.n = n; + } + + @Override + protected void subscribeActual(Subscriber<? super T> s) { + source.subscribe(new TakeSubscriber<>(s, n)); + } + + static final class TakeSubscriber<T> + extends AtomicLong + implements FlowableSubscriber<T>, Subscription { + + private static final long serialVersionUID = 2288246011222124525L; + + final Subscriber<? super T> downstream; + + long remaining; + + Subscription upstream; + + TakeSubscriber(Subscriber<? super T> actual, long remaining) { + this.downstream = actual; + this.remaining = remaining; + lazySet(remaining); + } + + @Override + public void onSubscribe(Subscription s) { + if (SubscriptionHelper.validate(this.upstream, s)) { + if (remaining == 0L) { + s.cancel(); + EmptySubscription.complete(downstream); + } else { + this.upstream = s; + downstream.onSubscribe(this); + } + } + } + + @Override + public void onNext(T t) { + long r = remaining; + if (r > 0L) { + remaining = --r; + downstream.onNext(t); + if (r == 0L) { + upstream.cancel(); + downstream.onComplete(); + } + } + } + + @Override + public void onError(Throwable t) { + if (remaining > 0L) { + remaining = 0L; + downstream.onError(t); + } else { + RxJavaPlugins.onError(t); + } + } + + @Override + public void onComplete() { + if (remaining > 0L) { + remaining = 0L; + downstream.onComplete(); + } + } + + @Override + public void request(long n) { + if (SubscriptionHelper.validate(n)) { + for (;;) { + long r = get(); + if (r == 0L) { + break; + } + long toRequest = Math.min(r, n); + long u = r - toRequest; + if (compareAndSet(r, u)) { + upstream.request(toRequest); + break; + } + } + } + } + + @Override + public void cancel() { + upstream.cancel(); + } + + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableTakeLast.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableTakeLast.java new file mode 100644 index 0000000000..ad26f45124 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableTakeLast.java @@ -0,0 +1,134 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import java.util.ArrayDeque; +import java.util.concurrent.atomic.*; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.internal.subscriptions.SubscriptionHelper; +import io.reactivex.rxjava3.internal.util.BackpressureHelper; + +public final class FlowableTakeLast<T> extends AbstractFlowableWithUpstream<T, T> { + final int count; + + public FlowableTakeLast(Flowable<T> source, int count) { + super(source); + this.count = count; + } + + @Override + protected void subscribeActual(Subscriber<? super T> s) { + source.subscribe(new TakeLastSubscriber<>(s, count)); + } + + static final class TakeLastSubscriber<T> extends ArrayDeque<T> implements FlowableSubscriber<T>, Subscription { + + private static final long serialVersionUID = 7240042530241604978L; + final Subscriber<? super T> downstream; + final int count; + + Subscription upstream; + volatile boolean done; + volatile boolean cancelled; + + final AtomicLong requested = new AtomicLong(); + + final AtomicInteger wip = new AtomicInteger(); + + TakeLastSubscriber(Subscriber<? super T> actual, int count) { + this.downstream = actual; + this.count = count; + } + + @Override + public void onSubscribe(Subscription s) { + if (SubscriptionHelper.validate(this.upstream, s)) { + this.upstream = s; + downstream.onSubscribe(this); + s.request(Long.MAX_VALUE); + } + } + + @Override + public void onNext(T t) { + if (count == size()) { + poll(); + } + offer(t); + } + + @Override + public void onError(Throwable t) { + downstream.onError(t); + } + + @Override + public void onComplete() { + done = true; + drain(); + } + + @Override + public void request(long n) { + if (SubscriptionHelper.validate(n)) { + BackpressureHelper.add(requested, n); + drain(); + } + } + + @Override + public void cancel() { + cancelled = true; + upstream.cancel(); + } + + void drain() { + if (wip.getAndIncrement() == 0) { + Subscriber<? super T> a = downstream; + long r = requested.get(); + do { + if (cancelled) { + return; + } + if (done) { + long e = 0L; + + while (e != r) { + if (cancelled) { + return; + } + T v = poll(); + if (v == null) { + a.onComplete(); + return; + } + a.onNext(v); + e++; + } + if (isEmpty()) { + a.onComplete(); + return; + } + if (e != 0L) { + r = BackpressureHelper.produced(requested, e); + } + } + } while (wip.decrementAndGet() != 0); + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableTakeLastOne.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableTakeLastOne.java new file mode 100644 index 0000000000..43d3bf4446 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableTakeLastOne.java @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.internal.subscriptions.*; + +public final class FlowableTakeLastOne<T> extends AbstractFlowableWithUpstream<T, T> { + + public FlowableTakeLastOne(Flowable<T> source) { + super(source); + } + + @Override + protected void subscribeActual(Subscriber<? super T> s) { + source.subscribe(new TakeLastOneSubscriber<>(s)); + } + + static final class TakeLastOneSubscriber<T> extends DeferredScalarSubscription<T> + implements FlowableSubscriber<T> { + + private static final long serialVersionUID = -5467847744262967226L; + + Subscription upstream; + + TakeLastOneSubscriber(Subscriber<? super T> downstream) { + super(downstream); + } + + @Override + public void onSubscribe(Subscription s) { + if (SubscriptionHelper.validate(this.upstream, s)) { + this.upstream = s; + downstream.onSubscribe(this); + s.request(Long.MAX_VALUE); + } + } + + @Override + public void onNext(T t) { + value = t; + } + + @Override + public void onError(Throwable t) { + value = null; + downstream.onError(t); + } + + @Override + public void onComplete() { + T v = value; + if (v != null) { + complete(v); + } else { + downstream.onComplete(); + } + } + + @Override + public void cancel() { + super.cancel(); + upstream.cancel(); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableTakeLastTimed.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableTakeLastTimed.java new file mode 100644 index 0000000000..b42b5717ce --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableTakeLastTimed.java @@ -0,0 +1,240 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.*; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.internal.subscriptions.SubscriptionHelper; +import io.reactivex.rxjava3.internal.util.BackpressureHelper; +import io.reactivex.rxjava3.operators.SpscLinkedArrayQueue; + +public final class FlowableTakeLastTimed<T> extends AbstractFlowableWithUpstream<T, T> { + final long count; + final long time; + final TimeUnit unit; + final Scheduler scheduler; + final int bufferSize; + final boolean delayError; + + public FlowableTakeLastTimed(Flowable<T> source, + long count, long time, TimeUnit unit, Scheduler scheduler, + int bufferSize, boolean delayError) { + super(source); + this.count = count; + this.time = time; + this.unit = unit; + this.scheduler = scheduler; + this.bufferSize = bufferSize; + this.delayError = delayError; + } + + @Override + protected void subscribeActual(Subscriber<? super T> s) { + source.subscribe(new TakeLastTimedSubscriber<>(s, count, time, unit, scheduler, bufferSize, delayError)); + } + + static final class TakeLastTimedSubscriber<T> extends AtomicInteger implements FlowableSubscriber<T>, Subscription { + + private static final long serialVersionUID = -5677354903406201275L; + final Subscriber<? super T> downstream; + final long count; + final long time; + final TimeUnit unit; + final Scheduler scheduler; + final SpscLinkedArrayQueue<Object> queue; + final boolean delayError; + + Subscription upstream; + + final AtomicLong requested = new AtomicLong(); + + volatile boolean cancelled; + + volatile boolean done; + Throwable error; + + TakeLastTimedSubscriber(Subscriber<? super T> actual, long count, long time, TimeUnit unit, Scheduler scheduler, int bufferSize, boolean delayError) { + this.downstream = actual; + this.count = count; + this.time = time; + this.unit = unit; + this.scheduler = scheduler; + this.queue = new SpscLinkedArrayQueue<>(bufferSize); + this.delayError = delayError; + } + + @Override + public void onSubscribe(Subscription s) { + if (SubscriptionHelper.validate(this.upstream, s)) { + this.upstream = s; + downstream.onSubscribe(this); + s.request(Long.MAX_VALUE); + } + } + + @Override + public void onNext(T t) { + final SpscLinkedArrayQueue<Object> q = queue; + + long now = scheduler.now(unit); + + q.offer(now, t); + + trim(now, q); + } + + @Override + public void onError(Throwable t) { + if (delayError) { + trim(scheduler.now(unit), queue); + } + error = t; + done = true; + drain(); + } + + @Override + public void onComplete() { + trim(scheduler.now(unit), queue); + done = true; + drain(); + } + + void trim(long now, SpscLinkedArrayQueue<Object> q) { + long time = this.time; + long c = count; + boolean unbounded = c == Long.MAX_VALUE; + + while (!q.isEmpty()) { + long ts = (Long)q.peek(); + if (ts < now - time || (!unbounded && (q.size() >> 1) > c)) { + q.poll(); + q.poll(); + } else { + break; + } + } + } + + @Override + public void request(long n) { + if (SubscriptionHelper.validate(n)) { + BackpressureHelper.add(requested, n); + drain(); + } + } + + @Override + public void cancel() { + if (!cancelled) { + cancelled = true; + upstream.cancel(); + + if (getAndIncrement() == 0) { + queue.clear(); + } + } + } + + void drain() { + if (getAndIncrement() != 0) { + return; + } + + int missed = 1; + + final Subscriber<? super T> a = downstream; + final SpscLinkedArrayQueue<Object> q = queue; + final boolean delayError = this.delayError; + + for (;;) { + + if (done) { + boolean empty = q.isEmpty(); + + if (checkTerminated(empty, a, delayError)) { + return; + } + + long r = requested.get(); + long e = 0L; + + for (;;) { + Object ts = q.peek(); // the timestamp long + empty = ts == null; + + if (checkTerminated(empty, a, delayError)) { + return; + } + + if (r == e) { + break; + } + + q.poll(); + @SuppressWarnings("unchecked") + T o = (T)q.poll(); + + a.onNext(o); + + e++; + } + + if (e != 0L) { + BackpressureHelper.produced(requested, e); + } + } + + missed = addAndGet(-missed); + if (missed == 0) { + break; + } + } + } + + boolean checkTerminated(boolean empty, Subscriber<? super T> a, boolean delayError) { + if (cancelled) { + queue.clear(); + return true; + } + if (delayError) { + if (empty) { + Throwable e = error; + if (e != null) { + a.onError(e); + } else { + a.onComplete(); + } + return true; + } + } else { + Throwable e = error; + if (e != null) { + queue.clear(); + a.onError(e); + return true; + } else + if (empty) { + a.onComplete(); + return true; + } + } + return false; + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableTakePublisher.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableTakePublisher.java new file mode 100644 index 0000000000..b105b34ca2 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableTakePublisher.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.internal.operators.flowable.FlowableTake.TakeSubscriber; + +/** + * Take with a generic Publisher source. + * <p>History: 2.0.7 - experimental + * @param <T> the value type + * @since 2.1 + */ +public final class FlowableTakePublisher<T> extends Flowable<T> { + + final Publisher<T> source; + final long limit; + public FlowableTakePublisher(Publisher<T> source, long limit) { + this.source = source; + this.limit = limit; + } + + @Override + protected void subscribeActual(Subscriber<? super T> s) { + source.subscribe(new TakeSubscriber<>(s, limit)); + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableTakeUntil.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableTakeUntil.java new file mode 100644 index 0000000000..6ea62f6975 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableTakeUntil.java @@ -0,0 +1,125 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import java.util.concurrent.atomic.*; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.internal.subscriptions.SubscriptionHelper; +import io.reactivex.rxjava3.internal.util.*; + +public final class FlowableTakeUntil<T, U> extends AbstractFlowableWithUpstream<T, T> { + final Publisher<? extends U> other; + public FlowableTakeUntil(Flowable<T> source, Publisher<? extends U> other) { + super(source); + this.other = other; + } + + @Override + protected void subscribeActual(Subscriber<? super T> child) { + TakeUntilMainSubscriber<T> parent = new TakeUntilMainSubscriber<>(child); + child.onSubscribe(parent); + + other.subscribe(parent.other); + + source.subscribe(parent); + } + + static final class TakeUntilMainSubscriber<T> extends AtomicInteger implements FlowableSubscriber<T>, Subscription { + + private static final long serialVersionUID = -4945480365982832967L; + + final Subscriber<? super T> downstream; + + final AtomicLong requested; + + final AtomicReference<Subscription> upstream; + + final AtomicThrowable error; + + final OtherSubscriber other; + + TakeUntilMainSubscriber(Subscriber<? super T> downstream) { + this.downstream = downstream; + this.requested = new AtomicLong(); + this.upstream = new AtomicReference<>(); + this.other = new OtherSubscriber(); + this.error = new AtomicThrowable(); + } + + @Override + public void onSubscribe(Subscription s) { + SubscriptionHelper.deferredSetOnce(this.upstream, requested, s); + } + + @Override + public void onNext(T t) { + HalfSerializer.onNext(downstream, t, this, error); + } + + @Override + public void onError(Throwable t) { + SubscriptionHelper.cancel(other); + HalfSerializer.onError(downstream, t, this, error); + } + + @Override + public void onComplete() { + SubscriptionHelper.cancel(other); + HalfSerializer.onComplete(downstream, this, error); + } + + @Override + public void request(long n) { + SubscriptionHelper.deferredRequest(upstream, requested, n); + } + + @Override + public void cancel() { + SubscriptionHelper.cancel(upstream); + SubscriptionHelper.cancel(other); + } + + final class OtherSubscriber extends AtomicReference<Subscription> implements FlowableSubscriber<Object> { + + private static final long serialVersionUID = -3592821756711087922L; + + @Override + public void onSubscribe(Subscription s) { + SubscriptionHelper.setOnce(this, s, Long.MAX_VALUE); + } + + @Override + public void onNext(Object t) { + SubscriptionHelper.cancel(this); + onComplete(); + } + + @Override + public void onError(Throwable t) { + SubscriptionHelper.cancel(upstream); + HalfSerializer.onError(downstream, t, TakeUntilMainSubscriber.this, error); + } + + @Override + public void onComplete() { + SubscriptionHelper.cancel(upstream); + HalfSerializer.onComplete(downstream, TakeUntilMainSubscriber.this, error); + } + + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableTakeUntilPredicate.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableTakeUntilPredicate.java new file mode 100644 index 0000000000..a57bb14c0a --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableTakeUntilPredicate.java @@ -0,0 +1,104 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.Predicate; +import io.reactivex.rxjava3.internal.subscriptions.SubscriptionHelper; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +public final class FlowableTakeUntilPredicate<T> extends AbstractFlowableWithUpstream<T, T> { + final Predicate<? super T> predicate; + public FlowableTakeUntilPredicate(Flowable<T> source, Predicate<? super T> predicate) { + super(source); + this.predicate = predicate; + } + + @Override + protected void subscribeActual(Subscriber<? super T> s) { + source.subscribe(new InnerSubscriber<>(s, predicate)); + } + + static final class InnerSubscriber<T> implements FlowableSubscriber<T>, Subscription { + final Subscriber<? super T> downstream; + final Predicate<? super T> predicate; + Subscription upstream; + boolean done; + InnerSubscriber(Subscriber<? super T> actual, Predicate<? super T> predicate) { + this.downstream = actual; + this.predicate = predicate; + } + + @Override + public void onSubscribe(Subscription s) { + if (SubscriptionHelper.validate(this.upstream, s)) { + this.upstream = s; + downstream.onSubscribe(this); + } + } + + @Override + public void onNext(T t) { + if (!done) { + downstream.onNext(t); + boolean b; + try { + b = predicate.test(t); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + upstream.cancel(); + onError(e); + return; + } + if (b) { + done = true; + upstream.cancel(); + downstream.onComplete(); + } + } + } + + @Override + public void onError(Throwable t) { + if (!done) { + done = true; + downstream.onError(t); + } else { + RxJavaPlugins.onError(t); + } + } + + @Override + public void onComplete() { + if (!done) { + done = true; + downstream.onComplete(); + } + } + + @Override + public void request(long n) { + upstream.request(n); + } + + @Override + public void cancel() { + upstream.cancel(); + } + + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableTakeWhile.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableTakeWhile.java new file mode 100644 index 0000000000..ea8d4f13eb --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableTakeWhile.java @@ -0,0 +1,111 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.Predicate; +import io.reactivex.rxjava3.internal.subscriptions.SubscriptionHelper; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +public final class FlowableTakeWhile<T> extends AbstractFlowableWithUpstream<T, T> { + final Predicate<? super T> predicate; + public FlowableTakeWhile(Flowable<T> source, Predicate<? super T> predicate) { + super(source); + this.predicate = predicate; + } + + @Override + protected void subscribeActual(Subscriber<? super T> s) { + source.subscribe(new TakeWhileSubscriber<>(s, predicate)); + } + + static final class TakeWhileSubscriber<T> implements FlowableSubscriber<T>, Subscription { + final Subscriber<? super T> downstream; + final Predicate<? super T> predicate; + + Subscription upstream; + + boolean done; + + TakeWhileSubscriber(Subscriber<? super T> actual, Predicate<? super T> predicate) { + this.downstream = actual; + this.predicate = predicate; + } + + @Override + public void onSubscribe(Subscription s) { + if (SubscriptionHelper.validate(this.upstream, s)) { + this.upstream = s; + downstream.onSubscribe(this); + } + } + + @Override + public void onNext(T t) { + if (done) { + return; + } + boolean b; + try { + b = predicate.test(t); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + upstream.cancel(); + onError(e); + return; + } + + if (!b) { + done = true; + upstream.cancel(); + downstream.onComplete(); + return; + } + + downstream.onNext(t); + } + + @Override + public void onError(Throwable t) { + if (done) { + RxJavaPlugins.onError(t); + return; + } + done = true; + downstream.onError(t); + } + + @Override + public void onComplete() { + if (done) { + return; + } + done = true; + downstream.onComplete(); + } + + @Override + public void request(long n) { + upstream.request(n); + } + + @Override + public void cancel() { + upstream.cancel(); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableThrottleFirstTimed.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableThrottleFirstTimed.java new file mode 100644 index 0000000000..44499ec255 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableThrottleFirstTimed.java @@ -0,0 +1,173 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; + +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.Consumer; +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.core.Scheduler.Worker; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.MissingBackpressureException; +import io.reactivex.rxjava3.internal.disposables.SequentialDisposable; +import io.reactivex.rxjava3.internal.subscriptions.SubscriptionHelper; +import io.reactivex.rxjava3.internal.util.BackpressureHelper; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.subscribers.SerializedSubscriber; + +public final class FlowableThrottleFirstTimed<T> extends AbstractFlowableWithUpstream<T, T> { + final long timeout; + final TimeUnit unit; + final Scheduler scheduler; + final Consumer<? super T> onDropped; + + public FlowableThrottleFirstTimed(Flowable<T> source, + long timeout, + TimeUnit unit, + Scheduler scheduler, + Consumer<? super T> onDropped) { + super(source); + this.timeout = timeout; + this.unit = unit; + this.scheduler = scheduler; + this.onDropped = onDropped; + } + + @Override + protected void subscribeActual(Subscriber<? super T> s) { + source.subscribe(new DebounceTimedSubscriber<>( + new SerializedSubscriber<>(s), + timeout, unit, scheduler.createWorker(), + onDropped)); + } + + static final class DebounceTimedSubscriber<T> + extends AtomicLong + implements FlowableSubscriber<T>, Subscription, Runnable { + private static final long serialVersionUID = -9102637559663639004L; + + final Subscriber<? super T> downstream; + final long timeout; + final TimeUnit unit; + final Scheduler.Worker worker; + final Consumer<? super T> onDropped; + Subscription upstream; + final SequentialDisposable timer = new SequentialDisposable(); + volatile boolean gate; + boolean done; + + DebounceTimedSubscriber(Subscriber<? super T> actual, + long timeout, + TimeUnit unit, + Worker worker, + Consumer<? super T> onDropped) { + this.downstream = actual; + this.timeout = timeout; + this.unit = unit; + this.worker = worker; + this.onDropped = onDropped; + } + + @Override + public void onSubscribe(Subscription s) { + if (SubscriptionHelper.validate(this.upstream, s)) { + this.upstream = s; + downstream.onSubscribe(this); + s.request(Long.MAX_VALUE); + } + } + + @Override + public void onNext(T t) { + if (done) { + return; + } + + if (!gate) { + gate = true; + long r = get(); + if (r != 0L) { + downstream.onNext(t); + BackpressureHelper.produced(this, 1); + } else { + upstream.cancel(); + done = true; + downstream.onError(MissingBackpressureException.createDefault()); + worker.dispose(); + return; + } + + Disposable d = timer.get(); + if (d != null) { + d.dispose(); + } + + timer.replace(worker.schedule(this, timeout, unit)); + } else if (onDropped != null) { + try { + onDropped.accept(t); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + upstream.cancel(); + done = true; + downstream.onError(ex); + worker.dispose(); + } + } + } + + @Override + public void run() { + gate = false; + } + + @Override + public void onError(Throwable t) { + if (done) { + RxJavaPlugins.onError(t); + return; + } + done = true; + downstream.onError(t); + worker.dispose(); + } + + @Override + public void onComplete() { + if (done) { + return; + } + done = true; + downstream.onComplete(); + worker.dispose(); + } + + @Override + public void request(long n) { + if (SubscriptionHelper.validate(n)) { + BackpressureHelper.add(this, n); + } + } + + @Override + public void cancel() { + upstream.cancel(); + worker.dispose(); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableThrottleLatest.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableThrottleLatest.java new file mode 100644 index 0000000000..e28e5f09df --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableThrottleLatest.java @@ -0,0 +1,321 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.*; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.Consumer; +import io.reactivex.rxjava3.internal.subscriptions.SubscriptionHelper; +import io.reactivex.rxjava3.internal.util.BackpressureHelper; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +/** + * Emits the next or latest item when the given time elapses. + * <p> + * The operator emits the next item, then starts a timer. When the timer fires, + * it tries to emit the latest item from upstream. If there was no upstream item, + * in the meantime, the next upstream item is emitted immediately and the + * timed process repeats. + * <p>History: 2.1.14 - experimental + * @param <T> the upstream and downstream value type + * @since 2.2 + */ +public final class FlowableThrottleLatest<T> extends AbstractFlowableWithUpstream<T, T> { + + final long timeout; + + final TimeUnit unit; + + final Scheduler scheduler; + + final boolean emitLast; + + final Consumer<? super T> onDropped; + + public FlowableThrottleLatest(Flowable<T> source, + long timeout, TimeUnit unit, + Scheduler scheduler, + boolean emitLast, + Consumer<? super T> onDropped) { + super(source); + this.timeout = timeout; + this.unit = unit; + this.scheduler = scheduler; + this.emitLast = emitLast; + this.onDropped = onDropped; + } + + @Override + protected void subscribeActual(Subscriber<? super T> s) { + source.subscribe(new ThrottleLatestSubscriber<>(s, timeout, unit, scheduler.createWorker(), emitLast, onDropped)); + } + + static final class ThrottleLatestSubscriber<T> + extends AtomicInteger + implements FlowableSubscriber<T>, Subscription, Runnable { + + private static final long serialVersionUID = -8296689127439125014L; + + final Subscriber<? super T> downstream; + + final long timeout; + + final TimeUnit unit; + + final Scheduler.Worker worker; + + final boolean emitLast; + + final AtomicReference<T> latest; + + final AtomicLong requested; + + final Consumer<? super T> onDropped; + + Subscription upstream; + + volatile boolean done; + Throwable error; + + volatile boolean cancelled; + + volatile boolean timerFired; + + long emitted; + + boolean timerRunning; + + ThrottleLatestSubscriber(Subscriber<? super T> downstream, + long timeout, TimeUnit unit, + Scheduler.Worker worker, + boolean emitLast, + Consumer<? super T> onDropped) { + this.downstream = downstream; + this.timeout = timeout; + this.unit = unit; + this.worker = worker; + this.emitLast = emitLast; + this.latest = new AtomicReference<>(); + this.requested = new AtomicLong(); + this.onDropped = onDropped; + } + + @Override + public void onSubscribe(Subscription s) { + if (SubscriptionHelper.validate(upstream, s)) { + upstream = s; + downstream.onSubscribe(this); + s.request(Long.MAX_VALUE); + } + } + + @Override + public void onNext(T t) { + T old = latest.getAndSet(t); + if (onDropped != null && old != null) { + try { + onDropped.accept(old); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + upstream.cancel(); + error = ex; + done = true; + } + } + drain(); + } + + @Override + public void onError(Throwable t) { + error = t; + done = true; + drain(); + } + + @Override + public void onComplete() { + done = true; + drain(); + } + + @Override + public void request(long n) { + if (SubscriptionHelper.validate(n)) { + BackpressureHelper.add(requested, n); + } + } + + @Override + public void cancel() { + cancelled = true; + upstream.cancel(); + worker.dispose(); + if (getAndIncrement() == 0) { + clear(); + } + } + + void clear() { + if (onDropped != null) { + T v = latest.getAndSet(null); + if (v != null) { + try { + onDropped.accept(v); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + RxJavaPlugins.onError(ex); + } + } + } else { + latest.lazySet(null); + } + } + + @Override + public void run() { + timerFired = true; + drain(); + } + + void drain() { + if (getAndIncrement() != 0) { + return; + } + + int missed = 1; + + AtomicReference<T> latest = this.latest; + AtomicLong requested = this.requested; + Subscriber<? super T> downstream = this.downstream; + + for (;;) { + + for (;;) { + if (cancelled) { + clear(); + return; + } + + boolean d = done; + Throwable error = this.error; + + if (d && error != null) { + if (onDropped != null) { + T v = latest.getAndSet(null); + if (v != null) { + try { + onDropped.accept(v); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + error = new CompositeException(error, ex); + } + } + } else { + latest.lazySet(null); + } + downstream.onError(error); + worker.dispose(); + return; + } + + T v = latest.get(); + boolean empty = v == null; + + if (d) { + if (!empty) { + v = latest.getAndSet(null); + if (emitLast) { + long e = emitted; + if (e != requested.get()) { + emitted = e + 1; + downstream.onNext(v); + downstream.onComplete(); + } else { + tryDropAndSignalMBE(v); + } + } else { + if (onDropped != null) { + try { + onDropped.accept(v); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + downstream.onError(ex); + worker.dispose(); + return; + } + } + downstream.onComplete(); + } + } else { + downstream.onComplete(); + } + worker.dispose(); + return; + } + + if (empty) { + if (timerFired) { + timerRunning = false; + timerFired = false; + } + break; + } + + if (!timerRunning || timerFired) { + v = latest.getAndSet(null); + long e = emitted; + if (e != requested.get()) { + downstream.onNext(v); + emitted = e + 1; + } else { + upstream.cancel(); + tryDropAndSignalMBE(v); + worker.dispose(); + return; + } + + timerFired = false; + timerRunning = true; + worker.schedule(this, timeout, unit); + } else { + break; + } + } + + missed = addAndGet(-missed); + if (missed == 0) { + break; + } + } + } + + void tryDropAndSignalMBE(T valueToDrop) { + Throwable errorToSignal = MissingBackpressureException.createDefault(); + if (onDropped != null) { + try { + onDropped.accept(valueToDrop); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + errorToSignal = new CompositeException(errorToSignal, ex); + } + } + downstream.onError(errorToSignal); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableTimeInterval.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableTimeInterval.java new file mode 100644 index 0000000000..1601814c98 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableTimeInterval.java @@ -0,0 +1,92 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import java.util.concurrent.TimeUnit; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.internal.subscriptions.SubscriptionHelper; +import io.reactivex.rxjava3.schedulers.Timed; + +public final class FlowableTimeInterval<T> extends AbstractFlowableWithUpstream<T, Timed<T>> { + final Scheduler scheduler; + final TimeUnit unit; + + public FlowableTimeInterval(Flowable<T> source, TimeUnit unit, Scheduler scheduler) { + super(source); + this.scheduler = scheduler; + this.unit = unit; + } + + @Override + protected void subscribeActual(Subscriber<? super Timed<T>> s) { + source.subscribe(new TimeIntervalSubscriber<>(s, unit, scheduler)); + } + + static final class TimeIntervalSubscriber<T> implements FlowableSubscriber<T>, Subscription { + final Subscriber<? super Timed<T>> downstream; + final TimeUnit unit; + final Scheduler scheduler; + + Subscription upstream; + + long lastTime; + + TimeIntervalSubscriber(Subscriber<? super Timed<T>> actual, TimeUnit unit, Scheduler scheduler) { + this.downstream = actual; + this.scheduler = scheduler; + this.unit = unit; + } + + @Override + public void onSubscribe(Subscription s) { + if (SubscriptionHelper.validate(this.upstream, s)) { + lastTime = scheduler.now(unit); + this.upstream = s; + downstream.onSubscribe(this); + } + } + + @Override + public void onNext(T t) { + long now = scheduler.now(unit); + long last = lastTime; + lastTime = now; + long delta = now - last; + downstream.onNext(new Timed<>(t, delta, unit)); + } + + @Override + public void onError(Throwable t) { + downstream.onError(t); + } + + @Override + public void onComplete() { + downstream.onComplete(); + } + + @Override + public void request(long n) { + upstream.request(n); + } + + @Override + public void cancel() { + upstream.cancel(); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableTimeout.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableTimeout.java new file mode 100644 index 0000000000..506132f595 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableTimeout.java @@ -0,0 +1,389 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import java.util.Objects; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.*; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.disposables.SequentialDisposable; +import io.reactivex.rxjava3.internal.operators.flowable.FlowableTimeoutTimed.TimeoutSupport; +import io.reactivex.rxjava3.internal.subscriptions.*; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +public final class FlowableTimeout<T, U, V> extends AbstractFlowableWithUpstream<T, T> { + final Publisher<U> firstTimeoutIndicator; + final Function<? super T, ? extends Publisher<V>> itemTimeoutIndicator; + final Publisher<? extends T> other; + + public FlowableTimeout( + Flowable<T> source, + Publisher<U> firstTimeoutIndicator, + Function<? super T, ? extends Publisher<V>> itemTimeoutIndicator, + Publisher<? extends T> other) { + super(source); + this.firstTimeoutIndicator = firstTimeoutIndicator; + this.itemTimeoutIndicator = itemTimeoutIndicator; + this.other = other; + } + + @Override + protected void subscribeActual(Subscriber<? super T> s) { + if (other == null) { + TimeoutSubscriber<T> parent = new TimeoutSubscriber<>(s, itemTimeoutIndicator); + s.onSubscribe(parent); + parent.startFirstTimeout(firstTimeoutIndicator); + source.subscribe(parent); + } else { + TimeoutFallbackSubscriber<T> parent = new TimeoutFallbackSubscriber<>(s, itemTimeoutIndicator, other); + s.onSubscribe(parent); + parent.startFirstTimeout(firstTimeoutIndicator); + source.subscribe(parent); + } + } + + interface TimeoutSelectorSupport extends TimeoutSupport { + void onTimeoutError(long idx, Throwable ex); + } + + static final class TimeoutSubscriber<T> extends AtomicLong + implements FlowableSubscriber<T>, Subscription, TimeoutSelectorSupport { + + private static final long serialVersionUID = 3764492702657003550L; + + final Subscriber<? super T> downstream; + + final Function<? super T, ? extends Publisher<?>> itemTimeoutIndicator; + + final SequentialDisposable task; + + final AtomicReference<Subscription> upstream; + + final AtomicLong requested; + + TimeoutSubscriber(Subscriber<? super T> actual, Function<? super T, ? extends Publisher<?>> itemTimeoutIndicator) { + this.downstream = actual; + this.itemTimeoutIndicator = itemTimeoutIndicator; + this.task = new SequentialDisposable(); + this.upstream = new AtomicReference<>(); + this.requested = new AtomicLong(); + } + + @Override + public void onSubscribe(Subscription s) { + SubscriptionHelper.deferredSetOnce(upstream, requested, s); + } + + @Override + public void onNext(T t) { + long idx = get(); + if (idx == Long.MAX_VALUE || !compareAndSet(idx, idx + 1)) { + return; + } + + Disposable d = task.get(); + if (d != null) { + d.dispose(); + } + + downstream.onNext(t); + + Publisher<?> itemTimeoutPublisher; + + try { + itemTimeoutPublisher = Objects.requireNonNull( + itemTimeoutIndicator.apply(t), + "The itemTimeoutIndicator returned a null Publisher."); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + upstream.get().cancel(); + getAndSet(Long.MAX_VALUE); + downstream.onError(ex); + return; + } + + TimeoutConsumer consumer = new TimeoutConsumer(idx + 1, this); + if (task.replace(consumer)) { + itemTimeoutPublisher.subscribe(consumer); + } + } + + void startFirstTimeout(Publisher<?> firstTimeoutIndicator) { + if (firstTimeoutIndicator != null) { + TimeoutConsumer consumer = new TimeoutConsumer(0L, this); + if (task.replace(consumer)) { + firstTimeoutIndicator.subscribe(consumer); + } + } + } + + @Override + public void onError(Throwable t) { + if (getAndSet(Long.MAX_VALUE) != Long.MAX_VALUE) { + task.dispose(); + + downstream.onError(t); + } else { + RxJavaPlugins.onError(t); + } + } + + @Override + public void onComplete() { + if (getAndSet(Long.MAX_VALUE) != Long.MAX_VALUE) { + task.dispose(); + + downstream.onComplete(); + } + } + + @Override + public void onTimeout(long idx) { + if (compareAndSet(idx, Long.MAX_VALUE)) { + SubscriptionHelper.cancel(upstream); + + downstream.onError(new TimeoutException()); + } + } + + @Override + public void onTimeoutError(long idx, Throwable ex) { + if (compareAndSet(idx, Long.MAX_VALUE)) { + SubscriptionHelper.cancel(upstream); + + downstream.onError(ex); + } else { + RxJavaPlugins.onError(ex); + } + } + + @Override + public void request(long n) { + SubscriptionHelper.deferredRequest(upstream, requested, n); + } + + @Override + public void cancel() { + SubscriptionHelper.cancel(upstream); + task.dispose(); + } + } + + static final class TimeoutFallbackSubscriber<T> extends SubscriptionArbiter + implements FlowableSubscriber<T>, TimeoutSelectorSupport { + + private static final long serialVersionUID = 3764492702657003550L; + + final Subscriber<? super T> downstream; + + final Function<? super T, ? extends Publisher<?>> itemTimeoutIndicator; + + final SequentialDisposable task; + + final AtomicReference<Subscription> upstream; + + final AtomicLong index; + + Publisher<? extends T> fallback; + + long consumed; + + TimeoutFallbackSubscriber(Subscriber<? super T> actual, + Function<? super T, ? extends Publisher<?>> itemTimeoutIndicator, + Publisher<? extends T> fallback) { + super(true); + this.downstream = actual; + this.itemTimeoutIndicator = itemTimeoutIndicator; + this.task = new SequentialDisposable(); + this.upstream = new AtomicReference<>(); + this.fallback = fallback; + this.index = new AtomicLong(); + } + + @Override + public void onSubscribe(Subscription s) { + if (SubscriptionHelper.setOnce(this.upstream, s)) { + setSubscription(s); + } + } + + @Override + public void onNext(T t) { + long idx = index.get(); + if (idx == Long.MAX_VALUE || !index.compareAndSet(idx, idx + 1)) { + return; + } + + Disposable d = task.get(); + if (d != null) { + d.dispose(); + } + + consumed++; + + downstream.onNext(t); + + Publisher<?> itemTimeoutPublisher; + + try { + itemTimeoutPublisher = Objects.requireNonNull( + itemTimeoutIndicator.apply(t), + "The itemTimeoutIndicator returned a null Publisher."); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + upstream.get().cancel(); + index.getAndSet(Long.MAX_VALUE); + downstream.onError(ex); + return; + } + + TimeoutConsumer consumer = new TimeoutConsumer(idx + 1, this); + if (task.replace(consumer)) { + itemTimeoutPublisher.subscribe(consumer); + } + } + + void startFirstTimeout(Publisher<?> firstTimeoutIndicator) { + if (firstTimeoutIndicator != null) { + TimeoutConsumer consumer = new TimeoutConsumer(0L, this); + if (task.replace(consumer)) { + firstTimeoutIndicator.subscribe(consumer); + } + } + } + + @Override + public void onError(Throwable t) { + if (index.getAndSet(Long.MAX_VALUE) != Long.MAX_VALUE) { + task.dispose(); + + downstream.onError(t); + + task.dispose(); + } else { + RxJavaPlugins.onError(t); + } + } + + @Override + public void onComplete() { + if (index.getAndSet(Long.MAX_VALUE) != Long.MAX_VALUE) { + task.dispose(); + + downstream.onComplete(); + + task.dispose(); + } + } + + @Override + public void onTimeout(long idx) { + if (index.compareAndSet(idx, Long.MAX_VALUE)) { + SubscriptionHelper.cancel(upstream); + + Publisher<? extends T> f = fallback; + fallback = null; + + long c = consumed; + if (c != 0L) { + produced(c); + } + + f.subscribe(new FlowableTimeoutTimed.FallbackSubscriber<T>(downstream, this)); + } + } + + @Override + public void onTimeoutError(long idx, Throwable ex) { + if (index.compareAndSet(idx, Long.MAX_VALUE)) { + SubscriptionHelper.cancel(upstream); + + downstream.onError(ex); + } else { + RxJavaPlugins.onError(ex); + } + } + + @Override + public void cancel() { + super.cancel(); + task.dispose(); + } + } + + static final class TimeoutConsumer extends AtomicReference<Subscription> + implements FlowableSubscriber<Object>, Disposable { + + private static final long serialVersionUID = 8708641127342403073L; + + final TimeoutSelectorSupport parent; + + final long idx; + + TimeoutConsumer(long idx, TimeoutSelectorSupport parent) { + this.idx = idx; + this.parent = parent; + } + + @Override + public void onSubscribe(Subscription s) { + SubscriptionHelper.setOnce(this, s, Long.MAX_VALUE); + } + + @Override + public void onNext(Object t) { + Subscription upstream = get(); + if (upstream != SubscriptionHelper.CANCELLED) { + upstream.cancel(); + lazySet(SubscriptionHelper.CANCELLED); + parent.onTimeout(idx); + } + } + + @Override + public void onError(Throwable t) { + if (get() != SubscriptionHelper.CANCELLED) { + lazySet(SubscriptionHelper.CANCELLED); + parent.onTimeoutError(idx, t); + } else { + RxJavaPlugins.onError(t); + } + } + + @Override + public void onComplete() { + if (get() != SubscriptionHelper.CANCELLED) { + lazySet(SubscriptionHelper.CANCELLED); + parent.onTimeout(idx); + } + } + + @Override + public void dispose() { + SubscriptionHelper.cancel(this); + } + + @Override + public boolean isDisposed() { + return this.get() == SubscriptionHelper.CANCELLED; + } + } + +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableTimeoutTimed.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableTimeoutTimed.java new file mode 100644 index 0000000000..67d612011d --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableTimeoutTimed.java @@ -0,0 +1,324 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import static io.reactivex.rxjava3.internal.util.ExceptionHelper.timeoutMessage; + +import java.util.concurrent.*; +import java.util.concurrent.atomic.*; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.internal.disposables.SequentialDisposable; +import io.reactivex.rxjava3.internal.subscriptions.*; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +public final class FlowableTimeoutTimed<T> extends AbstractFlowableWithUpstream<T, T> { + final long timeout; + final TimeUnit unit; + final Scheduler scheduler; + final Publisher<? extends T> other; + + public FlowableTimeoutTimed(Flowable<T> source, + long timeout, TimeUnit unit, Scheduler scheduler, Publisher<? extends T> other) { + super(source); + this.timeout = timeout; + this.unit = unit; + this.scheduler = scheduler; + this.other = other; + } + + @Override + protected void subscribeActual(Subscriber<? super T> s) { + if (other == null) { + TimeoutSubscriber<T> parent = new TimeoutSubscriber<>(s, timeout, unit, scheduler.createWorker()); + s.onSubscribe(parent); + parent.startTimeout(0L); + source.subscribe(parent); + } else { + TimeoutFallbackSubscriber<T> parent = new TimeoutFallbackSubscriber<>(s, timeout, unit, scheduler.createWorker(), other); + s.onSubscribe(parent); + parent.startTimeout(0L); + source.subscribe(parent); + } + } + + static final class TimeoutSubscriber<T> extends AtomicLong + implements FlowableSubscriber<T>, Subscription, TimeoutSupport { + + private static final long serialVersionUID = 3764492702657003550L; + + final Subscriber<? super T> downstream; + + final long timeout; + + final TimeUnit unit; + + final Scheduler.Worker worker; + + final SequentialDisposable task; + + final AtomicReference<Subscription> upstream; + + final AtomicLong requested; + + TimeoutSubscriber(Subscriber<? super T> actual, long timeout, TimeUnit unit, Scheduler.Worker worker) { + this.downstream = actual; + this.timeout = timeout; + this.unit = unit; + this.worker = worker; + this.task = new SequentialDisposable(); + this.upstream = new AtomicReference<>(); + this.requested = new AtomicLong(); + } + + @Override + public void onSubscribe(Subscription s) { + SubscriptionHelper.deferredSetOnce(upstream, requested, s); + } + + @Override + public void onNext(T t) { + long idx = get(); + if (idx == Long.MAX_VALUE || !compareAndSet(idx, idx + 1)) { + return; + } + + task.get().dispose(); + + downstream.onNext(t); + + startTimeout(idx + 1); + } + + void startTimeout(long nextIndex) { + task.replace(worker.schedule(new TimeoutTask(nextIndex, this), timeout, unit)); + } + + @Override + public void onError(Throwable t) { + if (getAndSet(Long.MAX_VALUE) != Long.MAX_VALUE) { + task.dispose(); + + downstream.onError(t); + + worker.dispose(); + } else { + RxJavaPlugins.onError(t); + } + } + + @Override + public void onComplete() { + if (getAndSet(Long.MAX_VALUE) != Long.MAX_VALUE) { + task.dispose(); + + downstream.onComplete(); + + worker.dispose(); + } + } + + @Override + public void onTimeout(long idx) { + if (compareAndSet(idx, Long.MAX_VALUE)) { + SubscriptionHelper.cancel(upstream); + + downstream.onError(new TimeoutException(timeoutMessage(timeout, unit))); + + worker.dispose(); + } + } + + @Override + public void request(long n) { + SubscriptionHelper.deferredRequest(upstream, requested, n); + } + + @Override + public void cancel() { + SubscriptionHelper.cancel(upstream); + worker.dispose(); + } + } + + static final class TimeoutTask implements Runnable { + + final TimeoutSupport parent; + + final long idx; + + TimeoutTask(long idx, TimeoutSupport parent) { + this.idx = idx; + this.parent = parent; + } + + @Override + public void run() { + parent.onTimeout(idx); + } + } + + static final class TimeoutFallbackSubscriber<T> extends SubscriptionArbiter + implements FlowableSubscriber<T>, TimeoutSupport { + + private static final long serialVersionUID = 3764492702657003550L; + + final Subscriber<? super T> downstream; + + final long timeout; + + final TimeUnit unit; + + final Scheduler.Worker worker; + + final SequentialDisposable task; + + final AtomicReference<Subscription> upstream; + + final AtomicLong index; + + long consumed; + + Publisher<? extends T> fallback; + + TimeoutFallbackSubscriber(Subscriber<? super T> actual, long timeout, TimeUnit unit, + Scheduler.Worker worker, Publisher<? extends T> fallback) { + super(true); + this.downstream = actual; + this.timeout = timeout; + this.unit = unit; + this.worker = worker; + this.fallback = fallback; + this.task = new SequentialDisposable(); + this.upstream = new AtomicReference<>(); + this.index = new AtomicLong(); + } + + @Override + public void onSubscribe(Subscription s) { + if (SubscriptionHelper.setOnce(upstream, s)) { + setSubscription(s); + } + } + + @Override + public void onNext(T t) { + long idx = index.get(); + if (idx == Long.MAX_VALUE || !index.compareAndSet(idx, idx + 1)) { + return; + } + + task.get().dispose(); + + consumed++; + + downstream.onNext(t); + + startTimeout(idx + 1); + } + + void startTimeout(long nextIndex) { + task.replace(worker.schedule(new TimeoutTask(nextIndex, this), timeout, unit)); + } + + @Override + public void onError(Throwable t) { + if (index.getAndSet(Long.MAX_VALUE) != Long.MAX_VALUE) { + task.dispose(); + + downstream.onError(t); + + worker.dispose(); + } else { + RxJavaPlugins.onError(t); + } + } + + @Override + public void onComplete() { + if (index.getAndSet(Long.MAX_VALUE) != Long.MAX_VALUE) { + task.dispose(); + + downstream.onComplete(); + + worker.dispose(); + } + } + + @Override + public void onTimeout(long idx) { + if (index.compareAndSet(idx, Long.MAX_VALUE)) { + SubscriptionHelper.cancel(upstream); + + long c = consumed; + if (c != 0L) { + produced(c); + } + + Publisher<? extends T> f = fallback; + fallback = null; + + f.subscribe(new FallbackSubscriber<T>(downstream, this)); + + worker.dispose(); + } + } + + @Override + public void cancel() { + super.cancel(); + worker.dispose(); + } + } + + static final class FallbackSubscriber<T> implements FlowableSubscriber<T> { + + final Subscriber<? super T> downstream; + + final SubscriptionArbiter arbiter; + + FallbackSubscriber(Subscriber<? super T> actual, SubscriptionArbiter arbiter) { + this.downstream = actual; + this.arbiter = arbiter; + } + + @Override + public void onSubscribe(Subscription s) { + arbiter.setSubscription(s); + } + + @Override + public void onNext(T t) { + downstream.onNext(t); + } + + @Override + public void onError(Throwable t) { + downstream.onError(t); + } + + @Override + public void onComplete() { + downstream.onComplete(); + } + } + + interface TimeoutSupport { + + void onTimeout(long idx); + + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableTimer.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableTimer.java new file mode 100644 index 0000000000..db45fe2e98 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableTimer.java @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.MissingBackpressureException; +import io.reactivex.rxjava3.internal.disposables.*; +import io.reactivex.rxjava3.internal.subscriptions.SubscriptionHelper; + +public final class FlowableTimer extends Flowable<Long> { + final Scheduler scheduler; + final long delay; + final TimeUnit unit; + public FlowableTimer(long delay, TimeUnit unit, Scheduler scheduler) { + this.delay = delay; + this.unit = unit; + this.scheduler = scheduler; + } + + @Override + public void subscribeActual(Subscriber<? super Long> s) { + TimerSubscriber ios = new TimerSubscriber(s); + s.onSubscribe(ios); + + Disposable d = scheduler.scheduleDirect(ios, delay, unit); + + ios.setResource(d); + } + + static final class TimerSubscriber extends AtomicReference<Disposable> + implements Subscription, Runnable { + + private static final long serialVersionUID = -2809475196591179431L; + + final Subscriber<? super Long> downstream; + + volatile boolean requested; + + TimerSubscriber(Subscriber<? super Long> downstream) { + this.downstream = downstream; + } + + @Override + public void request(long n) { + if (SubscriptionHelper.validate(n)) { + requested = true; + } + } + + @Override + public void cancel() { + DisposableHelper.dispose(this); + } + + @Override + public void run() { + if (get() != DisposableHelper.DISPOSED) { + if (requested) { + downstream.onNext(0L); + lazySet(EmptyDisposable.INSTANCE); + downstream.onComplete(); + } else { + lazySet(EmptyDisposable.INSTANCE); + downstream.onError(MissingBackpressureException.createDefault()); + } + } + } + + public void setResource(Disposable d) { + DisposableHelper.trySet(this, d); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableToList.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableToList.java new file mode 100644 index 0000000000..3f03d93112 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableToList.java @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import java.util.Collection; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.Supplier; +import io.reactivex.rxjava3.internal.subscriptions.*; +import io.reactivex.rxjava3.internal.util.ExceptionHelper; + +public final class FlowableToList<T, U extends Collection<? super T>> extends AbstractFlowableWithUpstream<T, U> { + final Supplier<U> collectionSupplier; + + public FlowableToList(Flowable<T> source, Supplier<U> collectionSupplier) { + super(source); + this.collectionSupplier = collectionSupplier; + } + + @Override + protected void subscribeActual(Subscriber<? super U> s) { + U coll; + try { + coll = ExceptionHelper.nullCheck(collectionSupplier.get(), "The collectionSupplier returned a null Collection."); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + EmptySubscription.error(e, s); + return; + } + source.subscribe(new ToListSubscriber<>(s, coll)); + } + + static final class ToListSubscriber<T, U extends Collection<? super T>> + extends DeferredScalarSubscription<U> + implements FlowableSubscriber<T> { + + private static final long serialVersionUID = -8134157938864266736L; + Subscription upstream; + + ToListSubscriber(Subscriber<? super U> actual, U collection) { + super(actual); + this.value = collection; + } + + @Override + public void onSubscribe(Subscription s) { + if (SubscriptionHelper.validate(this.upstream, s)) { + this.upstream = s; + downstream.onSubscribe(this); + s.request(Long.MAX_VALUE); + } + } + + @Override + public void onNext(T t) { + U v = value; + if (v != null) { + v.add(t); + } + } + + @Override + public void onError(Throwable t) { + value = null; + downstream.onError(t); + } + + @Override + public void onComplete() { + complete(value); + } + + @Override + public void cancel() { + super.cancel(); + upstream.cancel(); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableToListSingle.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableToListSingle.java new file mode 100644 index 0000000000..5e65b48e08 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableToListSingle.java @@ -0,0 +1,116 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import java.util.Collection; + +import org.reactivestreams.Subscription; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.Supplier; +import io.reactivex.rxjava3.internal.disposables.EmptyDisposable; +import io.reactivex.rxjava3.internal.fuseable.FuseToFlowable; +import io.reactivex.rxjava3.internal.subscriptions.SubscriptionHelper; +import io.reactivex.rxjava3.internal.util.*; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +public final class FlowableToListSingle<T, U extends Collection<? super T>> extends Single<U> implements FuseToFlowable<U> { + + final Flowable<T> source; + + final Supplier<U> collectionSupplier; + + @SuppressWarnings("unchecked") + public FlowableToListSingle(Flowable<T> source) { + this(source, (Supplier<U>)ArrayListSupplier.asSupplier()); + } + + public FlowableToListSingle(Flowable<T> source, Supplier<U> collectionSupplier) { + this.source = source; + this.collectionSupplier = collectionSupplier; + } + + @Override + protected void subscribeActual(SingleObserver<? super U> observer) { + U coll; + try { + coll = ExceptionHelper.nullCheck(collectionSupplier.get(), "The collectionSupplier returned a null Collection."); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + EmptyDisposable.error(e, observer); + return; + } + source.subscribe(new ToListSubscriber<>(observer, coll)); + } + + @Override + public Flowable<U> fuseToFlowable() { + return RxJavaPlugins.onAssembly(new FlowableToList<>(source, collectionSupplier)); + } + + static final class ToListSubscriber<T, U extends Collection<? super T>> + implements FlowableSubscriber<T>, Disposable { + + final SingleObserver<? super U> downstream; + + Subscription upstream; + + U value; + + ToListSubscriber(SingleObserver<? super U> actual, U collection) { + this.downstream = actual; + this.value = collection; + } + + @Override + public void onSubscribe(Subscription s) { + if (SubscriptionHelper.validate(this.upstream, s)) { + this.upstream = s; + downstream.onSubscribe(this); + s.request(Long.MAX_VALUE); + } + } + + @Override + public void onNext(T t) { + value.add(t); + } + + @Override + public void onError(Throwable t) { + value = null; + upstream = SubscriptionHelper.CANCELLED; + downstream.onError(t); + } + + @Override + public void onComplete() { + upstream = SubscriptionHelper.CANCELLED; + downstream.onSuccess(value); + } + + @Override + public void dispose() { + upstream.cancel(); + upstream = SubscriptionHelper.CANCELLED; + } + + @Override + public boolean isDisposed() { + return upstream == SubscriptionHelper.CANCELLED; + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableUnsubscribeOn.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableUnsubscribeOn.java new file mode 100644 index 0000000000..dd8c4ce4cf --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableUnsubscribeOn.java @@ -0,0 +1,100 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import java.util.concurrent.atomic.AtomicBoolean; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.internal.subscriptions.SubscriptionHelper; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +public final class FlowableUnsubscribeOn<T> extends AbstractFlowableWithUpstream<T, T> { + final Scheduler scheduler; + public FlowableUnsubscribeOn(Flowable<T> source, Scheduler scheduler) { + super(source); + this.scheduler = scheduler; + } + + @Override + protected void subscribeActual(Subscriber<? super T> s) { + source.subscribe(new UnsubscribeSubscriber<>(s, scheduler)); + } + + static final class UnsubscribeSubscriber<T> extends AtomicBoolean implements FlowableSubscriber<T>, Subscription { + + private static final long serialVersionUID = 1015244841293359600L; + + final Subscriber<? super T> downstream; + final Scheduler scheduler; + + Subscription upstream; + + UnsubscribeSubscriber(Subscriber<? super T> actual, Scheduler scheduler) { + this.downstream = actual; + this.scheduler = scheduler; + } + + @Override + public void onSubscribe(Subscription s) { + if (SubscriptionHelper.validate(this.upstream, s)) { + this.upstream = s; + downstream.onSubscribe(this); + } + } + + @Override + public void onNext(T t) { + if (!get()) { + downstream.onNext(t); + } + } + + @Override + public void onError(Throwable t) { + if (get()) { + RxJavaPlugins.onError(t); + return; + } + downstream.onError(t); + } + + @Override + public void onComplete() { + if (!get()) { + downstream.onComplete(); + } + } + + @Override + public void request(long n) { + upstream.request(n); + } + + @Override + public void cancel() { + if (compareAndSet(false, true)) { + scheduler.scheduleDirect(new Cancellation()); + } + } + + final class Cancellation implements Runnable { + @Override + public void run() { + upstream.cancel(); + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableUsing.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableUsing.java new file mode 100644 index 0000000000..8f34a6e414 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableUsing.java @@ -0,0 +1,181 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import java.util.Objects; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.subscriptions.*; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +public final class FlowableUsing<T, D> extends Flowable<T> { + final Supplier<? extends D> resourceSupplier; + final Function<? super D, ? extends Publisher<? extends T>> sourceSupplier; + final Consumer<? super D> disposer; + final boolean eager; + + public FlowableUsing(Supplier<? extends D> resourceSupplier, + Function<? super D, ? extends Publisher<? extends T>> sourceSupplier, + Consumer<? super D> disposer, + boolean eager) { + this.resourceSupplier = resourceSupplier; + this.sourceSupplier = sourceSupplier; + this.disposer = disposer; + this.eager = eager; + } + + @Override + public void subscribeActual(Subscriber<? super T> s) { + D resource; + + try { + resource = resourceSupplier.get(); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + EmptySubscription.error(e, s); + return; + } + + Publisher<? extends T> source; + try { + source = Objects.requireNonNull(sourceSupplier.apply(resource), "The sourceSupplier returned a null Publisher"); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + try { + disposer.accept(resource); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + EmptySubscription.error(new CompositeException(e, ex), s); + return; + } + EmptySubscription.error(e, s); + return; + } + + UsingSubscriber<T, D> us = new UsingSubscriber<>(s, resource, disposer, eager); + + source.subscribe(us); + } + + static final class UsingSubscriber<T, D> extends AtomicBoolean implements FlowableSubscriber<T>, Subscription { + + private static final long serialVersionUID = 5904473792286235046L; + + final Subscriber<? super T> downstream; + final D resource; + final Consumer<? super D> disposer; + final boolean eager; + + Subscription upstream; + + UsingSubscriber(Subscriber<? super T> actual, D resource, Consumer<? super D> disposer, boolean eager) { + this.downstream = actual; + this.resource = resource; + this.disposer = disposer; + this.eager = eager; + } + + @Override + public void onSubscribe(Subscription s) { + if (SubscriptionHelper.validate(this.upstream, s)) { + this.upstream = s; + downstream.onSubscribe(this); + } + } + + @Override + public void onNext(T t) { + downstream.onNext(t); + } + + @Override + public void onError(Throwable t) { + if (eager) { + Throwable innerError = null; + if (compareAndSet(false, true)) { + try { + disposer.accept(resource); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + innerError = e; + } + } + + if (innerError != null) { + downstream.onError(new CompositeException(t, innerError)); + } else { + downstream.onError(t); + } + } else { + downstream.onError(t); + disposeResource(); + } + } + + @Override + public void onComplete() { + if (eager) { + if (compareAndSet(false, true)) { + try { + disposer.accept(resource); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + downstream.onError(e); + return; + } + } + + downstream.onComplete(); + } else { + downstream.onComplete(); + disposeResource(); + } + } + + @Override + public void request(long n) { + upstream.request(n); + } + + @Override + public void cancel() { + if (eager) { + disposeResource(); + upstream.cancel(); + upstream = SubscriptionHelper.CANCELLED; + } else { + upstream.cancel(); + upstream = SubscriptionHelper.CANCELLED; + disposeResource(); + } + } + + void disposeResource() { + if (compareAndSet(false, true)) { + try { + disposer.accept(resource); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + // can't call actual.onError unless it is serialized, which is expensive + RxJavaPlugins.onError(e); + } + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableWindow.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableWindow.java new file mode 100644 index 0000000000..85ab5a5e0b --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableWindow.java @@ -0,0 +1,549 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import java.util.ArrayDeque; +import java.util.concurrent.atomic.*; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.internal.subscriptions.SubscriptionHelper; +import io.reactivex.rxjava3.internal.util.BackpressureHelper; +import io.reactivex.rxjava3.operators.SpscLinkedArrayQueue; +import io.reactivex.rxjava3.processors.UnicastProcessor; + +public final class FlowableWindow<T> extends AbstractFlowableWithUpstream<T, Flowable<T>> { + final long size; + + final long skip; + + final int bufferSize; + + public FlowableWindow(Flowable<T> source, long size, long skip, int bufferSize) { + super(source); + this.size = size; + this.skip = skip; + this.bufferSize = bufferSize; + } + + @Override + public void subscribeActual(Subscriber<? super Flowable<T>> s) { + if (skip == size) { + source.subscribe(new WindowExactSubscriber<>(s, size, bufferSize)); + } else + if (skip > size) { + source.subscribe(new WindowSkipSubscriber<>(s, size, skip, bufferSize)); + } else { + source.subscribe(new WindowOverlapSubscriber<>(s, size, skip, bufferSize)); + } + } + + static final class WindowExactSubscriber<T> + extends AtomicInteger + implements FlowableSubscriber<T>, Subscription, Runnable { + + private static final long serialVersionUID = -2365647875069161133L; + + final Subscriber<? super Flowable<T>> downstream; + + final long size; + + final AtomicBoolean once; + + final int bufferSize; + + long index; + + Subscription upstream; + + UnicastProcessor<T> window; + + WindowExactSubscriber(Subscriber<? super Flowable<T>> actual, long size, int bufferSize) { + super(1); + this.downstream = actual; + this.size = size; + this.once = new AtomicBoolean(); + this.bufferSize = bufferSize; + } + + @Override + public void onSubscribe(Subscription s) { + if (SubscriptionHelper.validate(this.upstream, s)) { + this.upstream = s; + downstream.onSubscribe(this); + } + } + + @Override + public void onNext(T t) { + long i = index; + + UnicastProcessor<T> w = window; + FlowableWindowSubscribeIntercept<T> intercept = null; + if (i == 0) { + getAndIncrement(); + + w = UnicastProcessor.create(bufferSize, this); + window = w; + + intercept = new FlowableWindowSubscribeIntercept<>(w); + downstream.onNext(intercept); + } + + i++; + + w.onNext(t); + + if (i == size) { + index = 0; + window = null; + w.onComplete(); + } else { + index = i; + } + + if (intercept != null && intercept.tryAbandon()) { + intercept.window.onComplete(); + } + } + + @Override + public void onError(Throwable t) { + Processor<T, T> w = window; + if (w != null) { + window = null; + w.onError(t); + } + + downstream.onError(t); + } + + @Override + public void onComplete() { + Processor<T, T> w = window; + if (w != null) { + window = null; + w.onComplete(); + } + + downstream.onComplete(); + } + + @Override + public void request(long n) { + if (SubscriptionHelper.validate(n)) { + long u = BackpressureHelper.multiplyCap(size, n); + upstream.request(u); + } + } + + @Override + public void cancel() { + if (once.compareAndSet(false, true)) { + run(); + } + } + + @Override + public void run() { + if (decrementAndGet() == 0) { + upstream.cancel(); + } + } + } + + static final class WindowSkipSubscriber<T> + extends AtomicInteger + implements FlowableSubscriber<T>, Subscription, Runnable { + + private static final long serialVersionUID = -8792836352386833856L; + + final Subscriber<? super Flowable<T>> downstream; + + final long size; + + final long skip; + + final AtomicBoolean once; + + final AtomicBoolean firstRequest; + + final int bufferSize; + + long index; + + Subscription upstream; + + UnicastProcessor<T> window; + + WindowSkipSubscriber(Subscriber<? super Flowable<T>> actual, long size, long skip, int bufferSize) { + super(1); + this.downstream = actual; + this.size = size; + this.skip = skip; + this.once = new AtomicBoolean(); + this.firstRequest = new AtomicBoolean(); + this.bufferSize = bufferSize; + } + + @Override + public void onSubscribe(Subscription s) { + if (SubscriptionHelper.validate(this.upstream, s)) { + this.upstream = s; + downstream.onSubscribe(this); + } + } + + @Override + public void onNext(T t) { + long i = index; + + FlowableWindowSubscribeIntercept<T> intercept = null; + UnicastProcessor<T> w = window; + if (i == 0) { + getAndIncrement(); + + w = UnicastProcessor.create(bufferSize, this); + window = w; + + intercept = new FlowableWindowSubscribeIntercept<>(w); + downstream.onNext(intercept); + } + + i++; + + if (w != null) { + w.onNext(t); + } + + if (i == size) { + window = null; + w.onComplete(); + } + + if (i == skip) { + index = 0; + } else { + index = i; + } + + if (intercept != null && intercept.tryAbandon()) { + intercept.window.onComplete(); + } + } + + @Override + public void onError(Throwable t) { + Processor<T, T> w = window; + if (w != null) { + window = null; + w.onError(t); + } + + downstream.onError(t); + } + + @Override + public void onComplete() { + Processor<T, T> w = window; + if (w != null) { + window = null; + w.onComplete(); + } + + downstream.onComplete(); + } + + @Override + public void request(long n) { + if (SubscriptionHelper.validate(n)) { + if (!firstRequest.get() && firstRequest.compareAndSet(false, true)) { + long u = BackpressureHelper.multiplyCap(size, n); + long v = BackpressureHelper.multiplyCap(skip - size, n - 1); + long w = BackpressureHelper.addCap(u, v); + upstream.request(w); + } else { + long u = BackpressureHelper.multiplyCap(skip, n); + upstream.request(u); + } + } + } + + @Override + public void cancel() { + if (once.compareAndSet(false, true)) { + run(); + } + } + + @Override + public void run() { + if (decrementAndGet() == 0) { + upstream.cancel(); + } + } + } + + static final class WindowOverlapSubscriber<T> + extends AtomicInteger + implements FlowableSubscriber<T>, Subscription, Runnable { + + private static final long serialVersionUID = 2428527070996323976L; + + final Subscriber<? super Flowable<T>> downstream; + + final SpscLinkedArrayQueue<UnicastProcessor<T>> queue; + + final long size; + + final long skip; + + final ArrayDeque<UnicastProcessor<T>> windows; + + final AtomicBoolean once; + + final AtomicBoolean firstRequest; + + final AtomicLong requested; + + final AtomicInteger wip; + + final int bufferSize; + + long index; + + long produced; + + Subscription upstream; + + volatile boolean done; + Throwable error; + + volatile boolean cancelled; + + WindowOverlapSubscriber(Subscriber<? super Flowable<T>> actual, long size, long skip, int bufferSize) { + super(1); + this.downstream = actual; + this.size = size; + this.skip = skip; + this.queue = new SpscLinkedArrayQueue<>(bufferSize); + this.windows = new ArrayDeque<>(); + this.once = new AtomicBoolean(); + this.firstRequest = new AtomicBoolean(); + this.requested = new AtomicLong(); + this.wip = new AtomicInteger(); + this.bufferSize = bufferSize; + } + + @Override + public void onSubscribe(Subscription s) { + if (SubscriptionHelper.validate(this.upstream, s)) { + this.upstream = s; + downstream.onSubscribe(this); + } + } + + @Override + public void onNext(T t) { + long i = index; + + UnicastProcessor<T> newWindow = null; + if (i == 0) { + if (!cancelled) { + getAndIncrement(); + + newWindow = UnicastProcessor.create(bufferSize, this); + + windows.offer(newWindow); + } + } + + i++; + + for (Processor<T, T> w : windows) { + w.onNext(t); + } + + if (newWindow != null) { + queue.offer(newWindow); + drain(); + } + + long p = produced + 1; + if (p == size) { + produced = p - skip; + + Processor<T, T> w = windows.poll(); + if (w != null) { + w.onComplete(); + } + } else { + produced = p; + } + + if (i == skip) { + index = 0; + } else { + index = i; + } + } + + @Override + public void onError(Throwable t) { + for (Processor<T, T> w : windows) { + w.onError(t); + } + windows.clear(); + + error = t; + done = true; + drain(); + } + + @Override + public void onComplete() { + for (Processor<T, T> w : windows) { + w.onComplete(); + } + windows.clear(); + + done = true; + drain(); + } + + void drain() { + if (wip.getAndIncrement() != 0) { + return; + } + + final Subscriber<? super Flowable<T>> a = downstream; + final SpscLinkedArrayQueue<UnicastProcessor<T>> q = queue; + int missed = 1; + + outer: + for (;;) { + + if (cancelled) { + UnicastProcessor<T> up = null; + while ((up = q.poll()) != null) { + up.onComplete(); + } + } else { + long r = requested.get(); + long e = 0; + + while (e != r) { + boolean d = done; + + UnicastProcessor<T> t = q.poll(); + + boolean empty = t == null; + + if (cancelled) { + continue outer; + } + + if (checkTerminated(d, empty, a, q)) { + return; + } + + if (empty) { + break; + } + + FlowableWindowSubscribeIntercept<T> intercept = new FlowableWindowSubscribeIntercept<>(t); + a.onNext(intercept); + + if (intercept.tryAbandon()) { + t.onComplete(); + } + e++; + } + + if (e == r) { + if (cancelled) { + continue; + } + + if (checkTerminated(done, q.isEmpty(), a, q)) { + return; + } + } + + if (e != 0L && r != Long.MAX_VALUE) { + requested.addAndGet(-e); + } + } + + missed = wip.addAndGet(-missed); + if (missed == 0) { + break; + } + } + } + + boolean checkTerminated(boolean d, boolean empty, Subscriber<?> a, SpscLinkedArrayQueue<?> q) { + if (d) { + Throwable e = error; + + if (e != null) { + q.clear(); + a.onError(e); + return true; + } else + if (empty) { + a.onComplete(); + return true; + } + } + + return false; + } + + @Override + public void request(long n) { + if (SubscriptionHelper.validate(n)) { + BackpressureHelper.add(requested, n); + + if (!firstRequest.get() && firstRequest.compareAndSet(false, true)) { + long u = BackpressureHelper.multiplyCap(skip, n - 1); + long v = BackpressureHelper.addCap(size, u); + upstream.request(v); + } else { + long u = BackpressureHelper.multiplyCap(skip, n); + upstream.request(u); + } + + drain(); + } + } + + @Override + public void cancel() { + cancelled = true; + if (once.compareAndSet(false, true)) { + run(); + } + drain(); + } + + @Override + public void run() { + if (decrementAndGet() == 0) { + upstream.cancel(); + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableWindowBoundary.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableWindowBoundary.java new file mode 100644 index 0000000000..2aad69dcb5 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableWindowBoundary.java @@ -0,0 +1,303 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import java.util.concurrent.atomic.*; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.MissingBackpressureException; +import io.reactivex.rxjava3.internal.queue.MpscLinkedQueue; +import io.reactivex.rxjava3.internal.subscriptions.SubscriptionHelper; +import io.reactivex.rxjava3.internal.util.*; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.processors.UnicastProcessor; +import io.reactivex.rxjava3.subscribers.DisposableSubscriber; + +public final class FlowableWindowBoundary<T, B> extends AbstractFlowableWithUpstream<T, Flowable<T>> { + final Publisher<B> other; + final int capacityHint; + + public FlowableWindowBoundary(Flowable<T> source, Publisher<B> other, int capacityHint) { + super(source); + this.other = other; + this.capacityHint = capacityHint; + } + + @Override + protected void subscribeActual(Subscriber<? super Flowable<T>> subscriber) { + WindowBoundaryMainSubscriber<T, B> parent = new WindowBoundaryMainSubscriber<>(subscriber, capacityHint); + + subscriber.onSubscribe(parent); + + parent.innerNext(); + + other.subscribe(parent.boundarySubscriber); + + source.subscribe(parent); + } + + static final class WindowBoundaryMainSubscriber<T, B> + extends AtomicInteger + implements FlowableSubscriber<T>, Subscription, Runnable { + + private static final long serialVersionUID = 2233020065421370272L; + + final Subscriber<? super Flowable<T>> downstream; + + final int capacityHint; + + final WindowBoundaryInnerSubscriber<T, B> boundarySubscriber; + + final AtomicReference<Subscription> upstream; + + final AtomicInteger windows; + + final MpscLinkedQueue<Object> queue; + + final AtomicThrowable errors; + + final AtomicBoolean stopWindows; + + final AtomicLong requested; + + static final Object NEXT_WINDOW = new Object(); + + volatile boolean done; + + UnicastProcessor<T> window; + + long emitted; + + WindowBoundaryMainSubscriber(Subscriber<? super Flowable<T>> downstream, int capacityHint) { + this.downstream = downstream; + this.capacityHint = capacityHint; + this.boundarySubscriber = new WindowBoundaryInnerSubscriber<>(this); + this.upstream = new AtomicReference<>(); + this.windows = new AtomicInteger(1); + this.queue = new MpscLinkedQueue<>(); + this.errors = new AtomicThrowable(); + this.stopWindows = new AtomicBoolean(); + this.requested = new AtomicLong(); + } + + @Override + public void onSubscribe(Subscription s) { + SubscriptionHelper.setOnce(upstream, s, Long.MAX_VALUE); + } + + @Override + public void onNext(T t) { + queue.offer(t); + drain(); + } + + @Override + public void onError(Throwable e) { + boundarySubscriber.dispose(); + if (errors.tryAddThrowableOrReport(e)) { + done = true; + drain(); + } + } + + @Override + public void onComplete() { + boundarySubscriber.dispose(); + done = true; + drain(); + } + + @Override + public void cancel() { + if (stopWindows.compareAndSet(false, true)) { + boundarySubscriber.dispose(); + if (windows.decrementAndGet() == 0) { + SubscriptionHelper.cancel(upstream); + } + } + } + + @Override + public void request(long n) { + BackpressureHelper.add(requested, n); + } + + @Override + public void run() { + if (windows.decrementAndGet() == 0) { + SubscriptionHelper.cancel(upstream); + } + } + + void innerNext() { + queue.offer(NEXT_WINDOW); + drain(); + } + + void innerError(Throwable e) { + SubscriptionHelper.cancel(upstream); + if (errors.tryAddThrowableOrReport(e)) { + done = true; + drain(); + } + } + + void innerComplete() { + SubscriptionHelper.cancel(upstream); + done = true; + drain(); + } + + @SuppressWarnings("unchecked") + void drain() { + if (getAndIncrement() != 0) { + return; + } + + int missed = 1; + Subscriber<? super Flowable<T>> downstream = this.downstream; + MpscLinkedQueue<Object> queue = this.queue; + AtomicThrowable errors = this.errors; + long emitted = this.emitted; + + for (;;) { + + for (;;) { + if (windows.get() == 0) { + queue.clear(); + window = null; + return; + } + + UnicastProcessor<T> w = window; + + boolean d = done; + + if (d && errors.get() != null) { + queue.clear(); + Throwable ex = errors.terminate(); + if (w != null) { + window = null; + w.onError(ex); + } + downstream.onError(ex); + return; + } + + Object v = queue.poll(); + + boolean empty = v == null; + + if (d && empty) { + Throwable ex = errors.terminate(); + if (ex == null) { + if (w != null) { + window = null; + w.onComplete(); + } + downstream.onComplete(); + } else { + if (w != null) { + window = null; + w.onError(ex); + } + downstream.onError(ex); + } + return; + } + + if (empty) { + break; + } + + if (v != NEXT_WINDOW) { + w.onNext((T)v); + continue; + } + + if (w != null) { + window = null; + w.onComplete(); + } + + if (!stopWindows.get()) { + w = UnicastProcessor.create(capacityHint, this); + window = w; + windows.getAndIncrement(); + + if (emitted != requested.get()) { + emitted++; + FlowableWindowSubscribeIntercept<T> intercept = new FlowableWindowSubscribeIntercept<>(w); + downstream.onNext(intercept); + if (intercept.tryAbandon()) { + w.onComplete(); + } + } else { + SubscriptionHelper.cancel(upstream); + boundarySubscriber.dispose(); + errors.tryAddThrowableOrReport(MissingBackpressureException.createDefault()); + done = true; + } + } + } + + this.emitted = emitted; + missed = addAndGet(-missed); + if (missed == 0) { + break; + } + } + } + } + + static final class WindowBoundaryInnerSubscriber<T, B> extends DisposableSubscriber<B> { + + final WindowBoundaryMainSubscriber<T, B> parent; + + boolean done; + + WindowBoundaryInnerSubscriber(WindowBoundaryMainSubscriber<T, B> parent) { + this.parent = parent; + } + + @Override + public void onNext(B t) { + if (done) { + return; + } + parent.innerNext(); + } + + @Override + public void onError(Throwable t) { + if (done) { + RxJavaPlugins.onError(t); + return; + } + done = true; + parent.innerError(t); + } + + @Override + public void onComplete() { + if (done) { + return; + } + done = true; + parent.innerComplete(); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableWindowBoundarySelector.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableWindowBoundarySelector.java new file mode 100644 index 0000000000..d6bffae2e1 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableWindowBoundarySelector.java @@ -0,0 +1,445 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import java.util.*; +import java.util.concurrent.atomic.*; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.*; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.queue.MpscLinkedQueue; +import io.reactivex.rxjava3.internal.subscriptions.SubscriptionHelper; +import io.reactivex.rxjava3.internal.util.*; +import io.reactivex.rxjava3.operators.SimplePlainQueue; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.processors.UnicastProcessor; + +public final class FlowableWindowBoundarySelector<T, B, V> extends AbstractFlowableWithUpstream<T, Flowable<T>> { + final Publisher<B> open; + final Function<? super B, ? extends Publisher<V>> closingIndicator; + final int bufferSize; + + public FlowableWindowBoundarySelector( + Flowable<T> source, + Publisher<B> open, Function<? super B, ? extends Publisher<V>> closingIndicator, + int bufferSize) { + super(source); + this.open = open; + this.closingIndicator = closingIndicator; + this.bufferSize = bufferSize; + } + + @Override + protected void subscribeActual(Subscriber<? super Flowable<T>> s) { + source.subscribe(new WindowBoundaryMainSubscriber<>( + s, open, closingIndicator, bufferSize)); + } + + static final class WindowBoundaryMainSubscriber<T, B, V> + extends AtomicInteger + implements FlowableSubscriber<T>, Subscription, Runnable { + private static final long serialVersionUID = 8646217640096099753L; + + final Subscriber<? super Flowable<T>> downstream; + final Publisher<B> open; + final Function<? super B, ? extends Publisher<V>> closingIndicator; + final int bufferSize; + final CompositeDisposable resources; + + final WindowStartSubscriber<B> startSubscriber; + + final List<UnicastProcessor<T>> windows; + + final SimplePlainQueue<Object> queue; + + final AtomicLong windowCount; + + final AtomicBoolean downstreamCancelled; + + final AtomicLong requested; + long emitted; + + volatile boolean upstreamCanceled; + + volatile boolean upstreamDone; + volatile boolean openDone; + final AtomicThrowable error; + + Subscription upstream; + + WindowBoundaryMainSubscriber(Subscriber<? super Flowable<T>> actual, + Publisher<B> open, Function<? super B, ? extends Publisher<V>> closingIndicator, int bufferSize) { + this.downstream = actual; + this.queue = new MpscLinkedQueue<>(); + this.open = open; + this.closingIndicator = closingIndicator; + this.bufferSize = bufferSize; + this.resources = new CompositeDisposable(); + this.windows = new ArrayList<>(); + this.windowCount = new AtomicLong(1L); + this.downstreamCancelled = new AtomicBoolean(); + this.error = new AtomicThrowable(); + this.startSubscriber = new WindowStartSubscriber<>(this); + this.requested = new AtomicLong(); + } + + @Override + public void onSubscribe(Subscription s) { + if (SubscriptionHelper.validate(this.upstream, s)) { + this.upstream = s; + + downstream.onSubscribe(this); + + open.subscribe(startSubscriber); + + s.request(Long.MAX_VALUE); + } + } + + @Override + public void onNext(T t) { + queue.offer(t); + drain(); + } + + @Override + public void onError(Throwable t) { + startSubscriber.cancel(); + resources.dispose(); + if (error.tryAddThrowableOrReport(t)) { + upstreamDone = true; + drain(); + } + } + + @Override + public void onComplete() { + startSubscriber.cancel(); + resources.dispose(); + upstreamDone = true; + drain(); + } + + @Override + public void request(long n) { + if (SubscriptionHelper.validate(n)) { + BackpressureHelper.add(requested, n); + } + } + + @Override + public void cancel() { + if (downstreamCancelled.compareAndSet(false, true)) { + if (windowCount.decrementAndGet() == 0) { + upstream.cancel(); + startSubscriber.cancel(); + resources.dispose(); + error.tryTerminateAndReport(); + upstreamCanceled = true; + drain(); + } else { + startSubscriber.cancel(); + } + } + } + + @Override + public void run() { + if (windowCount.decrementAndGet() == 0) { + upstream.cancel(); + startSubscriber.cancel(); + resources.dispose(); + error.tryTerminateAndReport(); + upstreamCanceled = true; + drain(); + } + } + + void open(B startValue) { + queue.offer(new WindowStartItem<>(startValue)); + drain(); + } + + void openError(Throwable t) { + upstream.cancel(); + resources.dispose(); + if (error.tryAddThrowableOrReport(t)) { + upstreamDone = true; + drain(); + } + } + + void openComplete() { + openDone = true; + drain(); + } + + void close(WindowEndSubscriberIntercept<T, V> what) { + queue.offer(what); + drain(); + } + + void closeError(Throwable t) { + upstream.cancel(); + startSubscriber.cancel(); + resources.dispose(); + if (error.tryAddThrowableOrReport(t)) { + upstreamDone = true; + drain(); + } + } + + void drain() { + if (getAndIncrement() != 0) { + return; + } + + int missed = 1; + final Subscriber<? super Flowable<T>> downstream = this.downstream; + final SimplePlainQueue<Object> queue = this.queue; + final List<UnicastProcessor<T>> windows = this.windows; + + for (;;) { + if (upstreamCanceled) { + queue.clear(); + windows.clear(); + } else { + boolean isDone = upstreamDone; + Object o = queue.poll(); + boolean isEmpty = o == null; + + if (isDone) { + if (isEmpty || error.get() != null) { + terminateDownstream(downstream); + upstreamCanceled = true; + continue; + } + } + + if (!isEmpty) { + if (o instanceof WindowStartItem) { + if (!downstreamCancelled.get()) { + long emitted = this.emitted; + if (requested.get() != emitted) { + this.emitted = ++emitted; + + @SuppressWarnings("unchecked") + B startItem = ((WindowStartItem<B>)o).item; + + Publisher<V> endSource; + try { + endSource = Objects.requireNonNull(closingIndicator.apply(startItem), "The closingIndicator returned a null Publisher"); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + upstream.cancel(); + startSubscriber.cancel(); + resources.dispose(); + Exceptions.throwIfFatal(ex); + error.tryAddThrowableOrReport(ex); + upstreamDone = true; + continue; + } + + windowCount.getAndIncrement(); + UnicastProcessor<T> newWindow = UnicastProcessor.create(bufferSize, this); + WindowEndSubscriberIntercept<T, V> endSubscriber = new WindowEndSubscriberIntercept<>(this, newWindow); + + downstream.onNext(endSubscriber); + + if (endSubscriber.tryAbandon()) { + newWindow.onComplete(); + } else { + windows.add(newWindow); + resources.add(endSubscriber); + endSource.subscribe(endSubscriber); + } + } else { + upstream.cancel(); + startSubscriber.cancel(); + resources.dispose(); + error.tryAddThrowableOrReport(FlowableWindowTimed.missingBackpressureMessage(emitted)); + upstreamDone = true; + } + } + } + else if (o instanceof WindowEndSubscriberIntercept) { + @SuppressWarnings("unchecked") + UnicastProcessor<T> w = ((WindowEndSubscriberIntercept<T, V>)o).window; + + windows.remove(w); + resources.delete((Disposable)o); + w.onComplete(); + } else { + @SuppressWarnings("unchecked") + T item = (T)o; + + for (UnicastProcessor<T> w : windows) { + w.onNext(item); + } + } + + continue; + } + else if (openDone && windows.size() == 0) { + upstream.cancel(); + startSubscriber.cancel(); + resources.dispose(); + terminateDownstream(downstream); + upstreamCanceled = true; + continue; + } + } + + missed = addAndGet(-missed); + if (missed == 0) { + break; + } + } + } + + void terminateDownstream(Subscriber<?> downstream) { + Throwable ex = error.terminate(); + if (ex == null) { + for (UnicastProcessor<T> w : windows) { + w.onComplete(); + } + downstream.onComplete(); + } else if (ex != ExceptionHelper.TERMINATED) { + for (UnicastProcessor<T> w : windows) { + w.onError(ex); + } + downstream.onError(ex); + } + } + + static final class WindowStartItem<B> { + + final B item; + + WindowStartItem(B item) { + this.item = item; + } + } + + static final class WindowStartSubscriber<B> extends AtomicReference<Subscription> + implements FlowableSubscriber<B> { + + private static final long serialVersionUID = -3326496781427702834L; + + final WindowBoundaryMainSubscriber<?, B, ?> parent; + + WindowStartSubscriber(WindowBoundaryMainSubscriber<?, B, ?> parent) { + this.parent = parent; + } + + @Override + public void onSubscribe(Subscription s) { + if (SubscriptionHelper.setOnce(this, s)) { + s.request(Long.MAX_VALUE); + } + } + + @Override + public void onNext(B t) { + parent.open(t); + } + + @Override + public void onError(Throwable t) { + parent.openError(t); + } + + @Override + public void onComplete() { + parent.openComplete(); + } + + void cancel() { + SubscriptionHelper.cancel(this); + } + } + + static final class WindowEndSubscriberIntercept<T, V> extends Flowable<T> + implements FlowableSubscriber<V>, Disposable { + + final WindowBoundaryMainSubscriber<T, ?, V> parent; + + final UnicastProcessor<T> window; + + final AtomicReference<Subscription> upstream; + + final AtomicBoolean once; + + WindowEndSubscriberIntercept(WindowBoundaryMainSubscriber<T, ?, V> parent, UnicastProcessor<T> window) { + this.parent = parent; + this.window = window; + this.upstream = new AtomicReference<>(); + this.once = new AtomicBoolean(); + } + + @Override + public void onSubscribe(Subscription s) { + if (SubscriptionHelper.setOnce(upstream, s)) { + s.request(Long.MAX_VALUE); + } + } + + @Override + public void onNext(V t) { + if (SubscriptionHelper.cancel(upstream)) { + parent.close(this); + } + } + + @Override + public void onError(Throwable t) { + if (isDisposed()) { + RxJavaPlugins.onError(t); + } else { + parent.closeError(t); + } + } + + @Override + public void onComplete() { + parent.close(this); + } + + @Override + public void dispose() { + SubscriptionHelper.cancel(upstream); + } + + @Override + public boolean isDisposed() { + return upstream.get() == SubscriptionHelper.CANCELLED; + } + + @Override + protected void subscribeActual(Subscriber<? super T> s) { + window.subscribe(s); + once.set(true); + } + + boolean tryAbandon() { + return !once.get() && once.compareAndSet(false, true); + } + } + } + +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableWindowSubscribeIntercept.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableWindowSubscribeIntercept.java new file mode 100644 index 0000000000..8ef2204c97 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableWindowSubscribeIntercept.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import java.util.concurrent.atomic.AtomicBoolean; + +import org.reactivestreams.Subscriber; + +import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.processors.FlowableProcessor; + +/** + * Wrapper for a FlowableProcessor that detects an incoming subscriber. + * @param <T> the element type of the flow. + * @since 3.0.0 + */ +final class FlowableWindowSubscribeIntercept<T> extends Flowable<T> { + + final FlowableProcessor<T> window; + + final AtomicBoolean once; + + FlowableWindowSubscribeIntercept(FlowableProcessor<T> source) { + this.window = source; + this.once = new AtomicBoolean(); + } + + @Override + protected void subscribeActual(Subscriber<? super T> s) { + window.subscribe(s); + once.set(true); + } + + boolean tryAbandon() { + return !once.get() && once.compareAndSet(false, true); + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableWindowTimed.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableWindowTimed.java new file mode 100644 index 0000000000..39e7de59ad --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableWindowTimed.java @@ -0,0 +1,724 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import java.util.*; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.*; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.core.Scheduler.Worker; +import io.reactivex.rxjava3.exceptions.MissingBackpressureException; +import io.reactivex.rxjava3.internal.disposables.SequentialDisposable; +import io.reactivex.rxjava3.internal.queue.MpscLinkedQueue; +import io.reactivex.rxjava3.internal.subscriptions.SubscriptionHelper; +import io.reactivex.rxjava3.internal.util.BackpressureHelper; +import io.reactivex.rxjava3.operators.SimplePlainQueue; +import io.reactivex.rxjava3.processors.UnicastProcessor; + +public final class FlowableWindowTimed<T> extends AbstractFlowableWithUpstream<T, Flowable<T>> { + final long timespan; + final long timeskip; + final TimeUnit unit; + final Scheduler scheduler; + final long maxSize; + final int bufferSize; + final boolean restartTimerOnMaxSize; + + public FlowableWindowTimed(Flowable<T> source, + long timespan, long timeskip, TimeUnit unit, Scheduler scheduler, long maxSize, + int bufferSize, boolean restartTimerOnMaxSize) { + super(source); + this.timespan = timespan; + this.timeskip = timeskip; + this.unit = unit; + this.scheduler = scheduler; + this.maxSize = maxSize; + this.bufferSize = bufferSize; + this.restartTimerOnMaxSize = restartTimerOnMaxSize; + } + + @Override + protected void subscribeActual(Subscriber<? super Flowable<T>> downstream) { + if (timespan == timeskip) { + if (maxSize == Long.MAX_VALUE) { + source.subscribe(new WindowExactUnboundedSubscriber<>( + downstream, + timespan, unit, scheduler, bufferSize)); + return; + } + source.subscribe(new WindowExactBoundedSubscriber<>( + downstream, + timespan, unit, scheduler, + bufferSize, maxSize, restartTimerOnMaxSize)); + return; + } + source.subscribe(new WindowSkipSubscriber<>(downstream, + timespan, timeskip, unit, scheduler.createWorker(), bufferSize)); + } + + abstract static class AbstractWindowSubscriber<T> + extends AtomicInteger + implements FlowableSubscriber<T>, Subscription { + private static final long serialVersionUID = 5724293814035355511L; + + final Subscriber<? super Flowable<T>> downstream; + + final SimplePlainQueue<Object> queue; + + final long timespan; + final TimeUnit unit; + final int bufferSize; + + final AtomicLong requested; + long emitted; + + volatile boolean done; + Throwable error; + + Subscription upstream; + + final AtomicBoolean downstreamCancelled; + + volatile boolean upstreamCancelled; + + final AtomicInteger windowCount; + + AbstractWindowSubscriber(Subscriber<? super Flowable<T>> downstream, long timespan, TimeUnit unit, int bufferSize) { + this.downstream = downstream; + this.queue = new MpscLinkedQueue<>(); + this.timespan = timespan; + this.unit = unit; + this.bufferSize = bufferSize; + this.requested = new AtomicLong(); + this.downstreamCancelled = new AtomicBoolean(); + this.windowCount = new AtomicInteger(1); + } + + @Override + public final void onSubscribe(Subscription s) { + if (SubscriptionHelper.validate(this.upstream, s)) { + this.upstream = s; + + downstream.onSubscribe(this); + + createFirstWindow(); + } + } + + abstract void createFirstWindow(); + + @Override + public final void onNext(T t) { + queue.offer(t); + drain(); + } + + @Override + public final void onError(Throwable t) { + error = t; + done = true; + drain(); + } + + @Override + public final void onComplete() { + done = true; + drain(); + } + + @Override + public final void request(long n) { + if (SubscriptionHelper.validate(n)) { + BackpressureHelper.add(requested, n); + } + } + + @Override + public final void cancel() { + if (downstreamCancelled.compareAndSet(false, true)) { + windowDone(); + } + } + + final void windowDone() { + if (windowCount.decrementAndGet() == 0) { + cleanupResources(); + upstream.cancel(); + upstreamCancelled = true; + drain(); + } + } + + abstract void cleanupResources(); + + abstract void drain(); + } + + static final class WindowExactUnboundedSubscriber<T> + extends AbstractWindowSubscriber<T> + implements Runnable { + + private static final long serialVersionUID = 1155822639622580836L; + + final Scheduler scheduler; + + UnicastProcessor<T> window; + + final SequentialDisposable timer; + + static final Object NEXT_WINDOW = new Object(); + + final Runnable windowRunnable; + + WindowExactUnboundedSubscriber(Subscriber<? super Flowable<T>> actual, long timespan, TimeUnit unit, + Scheduler scheduler, int bufferSize) { + super(actual, timespan, unit, bufferSize); + this.scheduler = scheduler; + this.timer = new SequentialDisposable(); + this.windowRunnable = new WindowRunnable(); + } + + @Override + void createFirstWindow() { + if (!downstreamCancelled.get()) { + if (requested.get() != 0L) { + windowCount.getAndIncrement(); + window = UnicastProcessor.create(bufferSize, windowRunnable); + + emitted = 1; + + FlowableWindowSubscribeIntercept<T> intercept = new FlowableWindowSubscribeIntercept<>(window); + downstream.onNext(intercept); + + timer.replace(scheduler.schedulePeriodicallyDirect(this, timespan, timespan, unit)); + + if (intercept.tryAbandon()) { + window.onComplete(); + } + + upstream.request(Long.MAX_VALUE); + } else { + upstream.cancel(); + downstream.onError(missingBackpressureMessage(emitted)); + + cleanupResources(); + upstreamCancelled = true; + } + } + } + + @Override + public void run() { + queue.offer(NEXT_WINDOW); + drain(); + } + + @Override + void drain() { + if (getAndIncrement() != 0) { + return; + } + + final SimplePlainQueue<Object> queue = this.queue; + final Subscriber<? super Flowable<T>> downstream = this.downstream; + UnicastProcessor<T> window = this.window; + + int missed = 1; + for (;;) { + + if (upstreamCancelled) { + queue.clear(); + window = null; + this.window = null; + } else { + boolean isDone = done; + Object o = queue.poll(); + boolean isEmpty = o == null; + + if (isDone && isEmpty) { + Throwable ex = error; + if (ex != null) { + if (window != null) { + window.onError(ex); + } + downstream.onError(ex); + } else { + if (window != null) { + window.onComplete(); + } + downstream.onComplete(); + } + cleanupResources(); + upstreamCancelled = true; + continue; + } + else if (!isEmpty) { + + if (o == NEXT_WINDOW) { + if (window != null) { + window.onComplete(); + window = null; + this.window = null; + } + if (downstreamCancelled.get()) { + timer.dispose(); + } else { + if (requested.get() == emitted) { + upstream.cancel(); + cleanupResources(); + upstreamCancelled = true; + + downstream.onError(missingBackpressureMessage(emitted)); + } else { + emitted++; + + windowCount.getAndIncrement(); + window = UnicastProcessor.create(bufferSize, windowRunnable); + this.window = window; + + FlowableWindowSubscribeIntercept<T> intercept = new FlowableWindowSubscribeIntercept<>(window); + downstream.onNext(intercept); + + if (intercept.tryAbandon()) { + window.onComplete(); + } + } + } + } else if (window != null) { + @SuppressWarnings("unchecked") + T item = (T)o; + window.onNext(item); + } + + continue; + } + } + + missed = addAndGet(-missed); + if (missed == 0) { + break; + } + } + } + + @Override + void cleanupResources() { + timer.dispose(); + } + + final class WindowRunnable implements Runnable { + @Override + public void run() { + windowDone(); + } + } + } + + static final class WindowExactBoundedSubscriber<T> + extends AbstractWindowSubscriber<T> + implements Runnable { + private static final long serialVersionUID = -6130475889925953722L; + + final Scheduler scheduler; + final boolean restartTimerOnMaxSize; + final long maxSize; + final Scheduler.Worker worker; + + long count; + + UnicastProcessor<T> window; + + final SequentialDisposable timer; + + WindowExactBoundedSubscriber( + Subscriber<? super Flowable<T>> actual, + long timespan, TimeUnit unit, Scheduler scheduler, + int bufferSize, long maxSize, boolean restartTimerOnMaxSize) { + super(actual, timespan, unit, bufferSize); + this.scheduler = scheduler; + this.maxSize = maxSize; + this.restartTimerOnMaxSize = restartTimerOnMaxSize; + if (restartTimerOnMaxSize) { + worker = scheduler.createWorker(); + } else { + worker = null; + } + this.timer = new SequentialDisposable(); + } + + @Override + void createFirstWindow() { + if (!downstreamCancelled.get()) { + if (requested.get() != 0L) { + emitted = 1; + + windowCount.getAndIncrement(); + window = UnicastProcessor.create(bufferSize, this); + + FlowableWindowSubscribeIntercept<T> intercept = new FlowableWindowSubscribeIntercept<>(window); + downstream.onNext(intercept); + + Runnable boundaryTask = new WindowBoundaryRunnable(this, 1L); + if (restartTimerOnMaxSize) { + timer.replace(worker.schedulePeriodically(boundaryTask, timespan, timespan, unit)); + } else { + timer.replace(scheduler.schedulePeriodicallyDirect(boundaryTask, timespan, timespan, unit)); + } + + if (intercept.tryAbandon()) { + window.onComplete(); + } + + upstream.request(Long.MAX_VALUE); + } else { + upstream.cancel(); + downstream.onError(missingBackpressureMessage(emitted)); + + cleanupResources(); + upstreamCancelled = true; + } + } + } + + @Override + public void run() { + windowDone(); + } + + @Override + void cleanupResources() { + timer.dispose(); + Worker w = worker; + if (w != null) { + w.dispose(); + } + } + + void boundary(WindowBoundaryRunnable sender) { + queue.offer(sender); + drain(); + } + + @Override + void drain() { + if (getAndIncrement() != 0) { + return; + } + + int missed = 1; + final SimplePlainQueue<Object> queue = this.queue; + final Subscriber<? super Flowable<T>> downstream = this.downstream; + UnicastProcessor<T> window = this.window; + + for (;;) { + + if (upstreamCancelled) { + queue.clear(); + window = null; + this.window = null; + } else { + + boolean isDone = done; + Object o = queue.poll(); + boolean isEmpty = o == null; + + if (isDone && isEmpty) { + Throwable ex = error; + if (ex != null) { + if (window != null) { + window.onError(ex); + } + downstream.onError(ex); + } else { + if (window != null) { + window.onComplete(); + } + downstream.onComplete(); + } + cleanupResources(); + upstreamCancelled = true; + continue; + } else if (!isEmpty) { + if (o instanceof WindowBoundaryRunnable) { + WindowBoundaryRunnable boundary = (WindowBoundaryRunnable) o; + if (boundary.index == emitted || !restartTimerOnMaxSize) { + this.count = 0; + window = createNewWindow(window); + } + } else if (window != null) { + @SuppressWarnings("unchecked") + T item = (T)o; + window.onNext(item); + + long count = this.count + 1; + if (count == maxSize) { + this.count = 0; + window = createNewWindow(window); + } else { + this.count = count; + } + } + + continue; + } + } + + missed = addAndGet(-missed); + if (missed == 0) { + break; + } + } + } + + UnicastProcessor<T> createNewWindow(UnicastProcessor<T> window) { + if (window != null) { + window.onComplete(); + window = null; + } + + if (downstreamCancelled.get()) { + cleanupResources(); + } else { + long emitted = this.emitted; + if (requested.get() == emitted) { + upstream.cancel(); + cleanupResources(); + upstreamCancelled = true; + + downstream.onError(missingBackpressureMessage(emitted)); + } else { + this.emitted = ++emitted; + + windowCount.getAndIncrement(); + window = UnicastProcessor.create(bufferSize, this); + this.window = window; + + FlowableWindowSubscribeIntercept<T> intercept = new FlowableWindowSubscribeIntercept<>(window); + downstream.onNext(intercept); + + if (restartTimerOnMaxSize) { + timer.update(worker.schedulePeriodically(new WindowBoundaryRunnable(this, emitted), timespan, timespan, unit)); + } + + if (intercept.tryAbandon()) { + window.onComplete(); + } + } + } + + return window; + } + + static final class WindowBoundaryRunnable implements Runnable { + + final WindowExactBoundedSubscriber<?> parent; + + final long index; + + WindowBoundaryRunnable(WindowExactBoundedSubscriber<?> parent, long index) { + this.parent = parent; + this.index = index; + } + + @Override + public void run() { + parent.boundary(this); + } + } + } + + static final class WindowSkipSubscriber<T> + extends AbstractWindowSubscriber<T> + implements Runnable { + private static final long serialVersionUID = -7852870764194095894L; + + final long timeskip; + final Scheduler.Worker worker; + + final List<UnicastProcessor<T>> windows; + + WindowSkipSubscriber(Subscriber<? super Flowable<T>> actual, + long timespan, long timeskip, TimeUnit unit, + Worker worker, int bufferSize) { + super(actual, timespan, unit, bufferSize); + this.timeskip = timeskip; + this.worker = worker; + this.windows = new LinkedList<>(); + } + + @Override + void createFirstWindow() { + if (!downstreamCancelled.get()) { + if (requested.get() != 0L) { + emitted = 1; + + windowCount.getAndIncrement(); + UnicastProcessor<T> window = UnicastProcessor.create(bufferSize, this); + windows.add(window); + + FlowableWindowSubscribeIntercept<T> intercept = new FlowableWindowSubscribeIntercept<>(window); + downstream.onNext(intercept); + + worker.schedule(new WindowBoundaryRunnable(this, false), timespan, unit); + worker.schedulePeriodically(new WindowBoundaryRunnable(this, true), timeskip, timeskip, unit); + + if (intercept.tryAbandon()) { + window.onComplete(); + windows.remove(window); + } + + upstream.request(Long.MAX_VALUE); + } else { + upstream.cancel(); + downstream.onError(missingBackpressureMessage(emitted)); + + cleanupResources(); + upstreamCancelled = true; + } + } + } + + @Override + void cleanupResources() { + worker.dispose(); + } + + @Override + void drain() { + if (getAndIncrement() != 0) { + return; + } + + int missed = 1; + final SimplePlainQueue<Object> queue = this.queue; + final Subscriber<? super Flowable<T>> downstream = this.downstream; + final List<UnicastProcessor<T>> windows = this.windows; + + for (;;) { + if (upstreamCancelled) { + queue.clear(); + windows.clear(); + } else { + boolean isDone = done; + Object o = queue.poll(); + boolean isEmpty = o == null; + + if (isDone && isEmpty) { + Throwable ex = error; + if (ex != null) { + for (UnicastProcessor<T> window : windows) { + window.onError(ex); + } + downstream.onError(ex); + } else { + for (UnicastProcessor<T> window : windows) { + window.onComplete(); + } + downstream.onComplete(); + } + cleanupResources(); + upstreamCancelled = true; + continue; + } else if (!isEmpty) { + if (o == WINDOW_OPEN) { + if (!downstreamCancelled.get()) { + long emitted = this.emitted; + if (requested.get() != emitted) { + this.emitted = ++emitted; + + windowCount.getAndIncrement(); + UnicastProcessor<T> window = UnicastProcessor.create(bufferSize, this); + windows.add(window); + + FlowableWindowSubscribeIntercept<T> intercept = new FlowableWindowSubscribeIntercept<>(window); + downstream.onNext(intercept); + + worker.schedule(new WindowBoundaryRunnable(this, false), timespan, unit); + + if (intercept.tryAbandon()) { + window.onComplete(); + } + } else { + upstream.cancel(); + Throwable ex = missingBackpressureMessage(emitted); + for (UnicastProcessor<T> window : windows) { + window.onError(ex); + } + downstream.onError(ex); + + cleanupResources(); + upstreamCancelled = true; + continue; + } + } + } else if (o == WINDOW_CLOSE) { + if (!windows.isEmpty()) { + windows.remove(0).onComplete(); + } + } else { + @SuppressWarnings("unchecked") + T item = (T)o; + for (UnicastProcessor<T> window : windows) { + window.onNext(item); + } + } + continue; + } + } + missed = addAndGet(-missed); + if (missed == 0) { + break; + } + } + } + + @Override + public void run() { + windowDone(); + } + + void boundary(boolean isOpen) { + queue.offer(isOpen ? WINDOW_OPEN : WINDOW_CLOSE); + drain(); + } + + static final Object WINDOW_OPEN = new Object(); + static final Object WINDOW_CLOSE = new Object(); + + static final class WindowBoundaryRunnable implements Runnable { + + final WindowSkipSubscriber<?> parent; + + final boolean isOpen; + + WindowBoundaryRunnable(WindowSkipSubscriber<?> parent, boolean isOpen) { + this.parent = parent; + this.isOpen = isOpen; + } + + @Override + public void run() { + parent.boundary(isOpen); + } + } + } + + static MissingBackpressureException missingBackpressureMessage(long index) { + return new MissingBackpressureException("Unable to emit the next window (#" + index + ") due to lack of requests. Please make sure the downstream is ready to consume windows."); + } + +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableWithLatestFrom.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableWithLatestFrom.java new file mode 100644 index 0000000000..cad9f3c0ca --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableWithLatestFrom.java @@ -0,0 +1,163 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import java.util.Objects; +import java.util.concurrent.atomic.*; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.BiFunction; +import io.reactivex.rxjava3.internal.subscriptions.SubscriptionHelper; +import io.reactivex.rxjava3.operators.ConditionalSubscriber; +import io.reactivex.rxjava3.subscribers.SerializedSubscriber; + +public final class FlowableWithLatestFrom<T, U, R> extends AbstractFlowableWithUpstream<T, R> { + final BiFunction<? super T, ? super U, ? extends R> combiner; + final Publisher<? extends U> other; + public FlowableWithLatestFrom(Flowable<T> source, BiFunction<? super T, ? super U, ? extends R> combiner, Publisher<? extends U> other) { + super(source); + this.combiner = combiner; + this.other = other; + } + + @Override + protected void subscribeActual(Subscriber<? super R> s) { + final SerializedSubscriber<R> serial = new SerializedSubscriber<>(s); + final WithLatestFromSubscriber<T, U, R> wlf = new WithLatestFromSubscriber<>(serial, combiner); + + serial.onSubscribe(wlf); + + other.subscribe(new FlowableWithLatestSubscriber(wlf)); + + source.subscribe(wlf); + } + + static final class WithLatestFromSubscriber<T, U, R> extends AtomicReference<U> + implements ConditionalSubscriber<T>, Subscription { + + private static final long serialVersionUID = -312246233408980075L; + + final Subscriber<? super R> downstream; + + final BiFunction<? super T, ? super U, ? extends R> combiner; + + final AtomicReference<Subscription> upstream = new AtomicReference<>(); + + final AtomicLong requested = new AtomicLong(); + + final AtomicReference<Subscription> other = new AtomicReference<>(); + + WithLatestFromSubscriber(Subscriber<? super R> actual, BiFunction<? super T, ? super U, ? extends R> combiner) { + this.downstream = actual; + this.combiner = combiner; + } + + @Override + public void onSubscribe(Subscription s) { + SubscriptionHelper.deferredSetOnce(this.upstream, requested, s); + } + + @Override + public void onNext(T t) { + if (!tryOnNext(t)) { + upstream.get().request(1); + } + } + + @Override + public boolean tryOnNext(T t) { + U u = get(); + if (u != null) { + R r; + try { + r = Objects.requireNonNull(combiner.apply(t, u), "The combiner returned a null value"); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + cancel(); + downstream.onError(e); + return false; + } + downstream.onNext(r); + return true; + } else { + return false; + } + } + + @Override + public void onError(Throwable t) { + SubscriptionHelper.cancel(other); + downstream.onError(t); + } + + @Override + public void onComplete() { + SubscriptionHelper.cancel(other); + downstream.onComplete(); + } + + @Override + public void request(long n) { + SubscriptionHelper.deferredRequest(upstream, requested, n); + } + + @Override + public void cancel() { + SubscriptionHelper.cancel(upstream); + SubscriptionHelper.cancel(other); + } + + public boolean setOther(Subscription o) { + return SubscriptionHelper.setOnce(other, o); + } + + public void otherError(Throwable e) { + SubscriptionHelper.cancel(upstream); + downstream.onError(e); + } + } + + final class FlowableWithLatestSubscriber implements FlowableSubscriber<U> { + private final WithLatestFromSubscriber<T, U, R> wlf; + + FlowableWithLatestSubscriber(WithLatestFromSubscriber<T, U, R> wlf) { + this.wlf = wlf; + } + + @Override + public void onSubscribe(Subscription s) { + if (wlf.setOther(s)) { + s.request(Long.MAX_VALUE); + } + } + + @Override + public void onNext(U t) { + wlf.lazySet(t); + } + + @Override + public void onError(Throwable t) { + wlf.otherError(t); + } + + @Override + public void onComplete() { + // nothing to do, the wlf will complete on its own pace + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableWithLatestFromMany.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableWithLatestFromMany.java new file mode 100644 index 0000000000..77d0527729 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableWithLatestFromMany.java @@ -0,0 +1,304 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import java.util.Arrays; +import java.util.Objects; +import java.util.concurrent.atomic.*; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.annotations.*; +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.subscriptions.*; +import io.reactivex.rxjava3.internal.util.*; +import io.reactivex.rxjava3.operators.ConditionalSubscriber; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +/** + * Combines a main sequence of values with the latest from multiple other sequences via + * a selector function. + * + * @param <T> the main sequence's type + * @param <R> the output type + */ +public final class FlowableWithLatestFromMany<T, R> extends AbstractFlowableWithUpstream<T, R> { + @Nullable + final Publisher<?>[] otherArray; + + @Nullable + final Iterable<? extends Publisher<?>> otherIterable; + + final Function<? super Object[], R> combiner; + + public FlowableWithLatestFromMany(@NonNull Flowable<T> source, @NonNull Publisher<?>[] otherArray, Function<? super Object[], R> combiner) { + super(source); + this.otherArray = otherArray; + this.otherIterable = null; + this.combiner = combiner; + } + + public FlowableWithLatestFromMany(@NonNull Flowable<T> source, @NonNull Iterable<? extends Publisher<?>> otherIterable, @NonNull Function<? super Object[], R> combiner) { + super(source); + this.otherArray = null; + this.otherIterable = otherIterable; + this.combiner = combiner; + } + + @Override + protected void subscribeActual(Subscriber<? super R> s) { + Publisher<?>[] others = otherArray; + int n = 0; + if (others == null) { + others = new Publisher[8]; + + try { + for (Publisher<?> p : otherIterable) { + if (n == others.length) { + others = Arrays.copyOf(others, n + (n >> 1)); + } + others[n++] = p; + } + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + EmptySubscription.error(ex, s); + return; + } + + } else { + n = others.length; + } + + if (n == 0) { + new FlowableMap<>(source, new SingletonArrayFunc()).subscribeActual(s); + return; + } + + WithLatestFromSubscriber<T, R> parent = new WithLatestFromSubscriber<>(s, combiner, n); + s.onSubscribe(parent); + parent.subscribe(others, n); + + source.subscribe(parent); + } + + static final class WithLatestFromSubscriber<T, R> + extends AtomicInteger + implements ConditionalSubscriber<T>, Subscription { + + private static final long serialVersionUID = 1577321883966341961L; + + final Subscriber<? super R> downstream; + + final Function<? super Object[], R> combiner; + + final WithLatestInnerSubscriber[] subscribers; + + final AtomicReferenceArray<Object> values; + + final AtomicReference<Subscription> upstream; + + final AtomicLong requested; + + final AtomicThrowable error; + + volatile boolean done; + + WithLatestFromSubscriber(Subscriber<? super R> actual, Function<? super Object[], R> combiner, int n) { + this.downstream = actual; + this.combiner = combiner; + WithLatestInnerSubscriber[] s = new WithLatestInnerSubscriber[n]; + for (int i = 0; i < n; i++) { + s[i] = new WithLatestInnerSubscriber(this, i); + } + this.subscribers = s; + this.values = new AtomicReferenceArray<>(n); + this.upstream = new AtomicReference<>(); + this.requested = new AtomicLong(); + this.error = new AtomicThrowable(); + } + + void subscribe(Publisher<?>[] others, int n) { + WithLatestInnerSubscriber[] subscribers = this.subscribers; + AtomicReference<Subscription> upstream = this.upstream; + for (int i = 0; i < n; i++) { + if (upstream.get() == SubscriptionHelper.CANCELLED) { + return; + } + others[i].subscribe(subscribers[i]); + } + } + + @Override + public void onSubscribe(Subscription s) { + SubscriptionHelper.deferredSetOnce(this.upstream, requested, s); + } + + @Override + public void onNext(T t) { + if (!tryOnNext(t) && !done) { + upstream.get().request(1); + } + } + + @Override + public boolean tryOnNext(T t) { + if (done) { + return false; + } + AtomicReferenceArray<Object> ara = values; + int n = ara.length(); + Object[] objects = new Object[n + 1]; + objects[0] = t; + + for (int i = 0; i < n; i++) { + Object o = ara.get(i); + if (o == null) { + // somebody hasn't signalled yet, skip this T + return false; + } + objects[i + 1] = o; + } + + R v; + + try { + v = Objects.requireNonNull(combiner.apply(objects), "The combiner returned a null value"); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + cancel(); + onError(ex); + return false; + } + + HalfSerializer.onNext(downstream, v, this, error); + return true; + } + + @Override + public void onError(Throwable t) { + if (done) { + RxJavaPlugins.onError(t); + return; + } + done = true; + cancelAllBut(-1); + HalfSerializer.onError(downstream, t, this, error); + } + + @Override + public void onComplete() { + if (!done) { + done = true; + cancelAllBut(-1); + HalfSerializer.onComplete(downstream, this, error); + } + } + + @Override + public void request(long n) { + SubscriptionHelper.deferredRequest(upstream, requested, n); + } + + @Override + public void cancel() { + SubscriptionHelper.cancel(upstream); + for (WithLatestInnerSubscriber s : subscribers) { + s.dispose(); + } + } + + void innerNext(int index, Object o) { + values.set(index, o); + } + + void innerError(int index, Throwable t) { + done = true; + SubscriptionHelper.cancel(upstream); + cancelAllBut(index); + HalfSerializer.onError(downstream, t, this, error); + } + + void innerComplete(int index, boolean nonEmpty) { + if (!nonEmpty) { + done = true; + SubscriptionHelper.cancel(upstream); + cancelAllBut(index); + HalfSerializer.onComplete(downstream, this, error); + } + } + + void cancelAllBut(int index) { + WithLatestInnerSubscriber[] subscribers = this.subscribers; + for (int i = 0; i < subscribers.length; i++) { + if (i != index) { + subscribers[i].dispose(); + } + } + } + } + + static final class WithLatestInnerSubscriber + extends AtomicReference<Subscription> + implements FlowableSubscriber<Object> { + + private static final long serialVersionUID = 3256684027868224024L; + + final WithLatestFromSubscriber<?, ?> parent; + + final int index; + + boolean hasValue; + + WithLatestInnerSubscriber(WithLatestFromSubscriber<?, ?> parent, int index) { + this.parent = parent; + this.index = index; + } + + @Override + public void onSubscribe(Subscription s) { + SubscriptionHelper.setOnce(this, s, Long.MAX_VALUE); + } + + @Override + public void onNext(Object t) { + if (!hasValue) { + hasValue = true; + } + parent.innerNext(index, t); + } + + @Override + public void onError(Throwable t) { + parent.innerError(index, t); + } + + @Override + public void onComplete() { + parent.innerComplete(index, hasValue); + } + + void dispose() { + SubscriptionHelper.cancel(this); + } + } + + final class SingletonArrayFunc implements Function<T, R> { + @Override + public R apply(T t) throws Throwable { + return Objects.requireNonNull(combiner.apply(new Object[] { t }), "The combiner returned a null value"); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableZip.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableZip.java new file mode 100644 index 0000000000..de680b19af --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableZip.java @@ -0,0 +1,403 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import java.util.Arrays; +import java.util.Objects; +import java.util.concurrent.atomic.*; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.subscriptions.*; +import io.reactivex.rxjava3.internal.util.*; +import io.reactivex.rxjava3.operators.QueueSubscription; +import io.reactivex.rxjava3.operators.SimpleQueue; +import io.reactivex.rxjava3.operators.SpscArrayQueue; + +public final class FlowableZip<T, R> extends Flowable<R> { + + final Publisher<? extends T>[] sources; + final Iterable<? extends Publisher<? extends T>> sourcesIterable; + final Function<? super Object[], ? extends R> zipper; + final int bufferSize; + final boolean delayError; + + public FlowableZip(Publisher<? extends T>[] sources, + Iterable<? extends Publisher<? extends T>> sourcesIterable, + Function<? super Object[], ? extends R> zipper, + int bufferSize, + boolean delayError) { + this.sources = sources; + this.sourcesIterable = sourcesIterable; + this.zipper = zipper; + this.bufferSize = bufferSize; + this.delayError = delayError; + } + + @Override + @SuppressWarnings("unchecked") + public void subscribeActual(Subscriber<? super R> s) { + Publisher<? extends T>[] sources = this.sources; + int count = 0; + if (sources == null) { + sources = new Publisher[8]; + for (Publisher<? extends T> p : sourcesIterable) { + if (count == sources.length) { + Publisher<? extends T>[] b = new Publisher[count + (count >> 2)]; + System.arraycopy(sources, 0, b, 0, count); + sources = b; + } + sources[count++] = p; + } + } else { + count = sources.length; + } + + if (count == 0) { + EmptySubscription.complete(s); + return; + } + + ZipCoordinator<T, R> coordinator = new ZipCoordinator<>(s, zipper, count, bufferSize, delayError); + + s.onSubscribe(coordinator); + + coordinator.subscribe(sources, count); + } + + static final class ZipCoordinator<T, R> + extends AtomicInteger + implements Subscription { + + private static final long serialVersionUID = -2434867452883857743L; + + final Subscriber<? super R> downstream; + + final ZipSubscriber<T, R>[] subscribers; + + final Function<? super Object[], ? extends R> zipper; + + final AtomicLong requested; + + final AtomicThrowable errors; + + final boolean delayErrors; + + volatile boolean cancelled; + + final Object[] current; + + ZipCoordinator(Subscriber<? super R> actual, + Function<? super Object[], ? extends R> zipper, int n, int prefetch, boolean delayErrors) { + this.downstream = actual; + this.zipper = zipper; + this.delayErrors = delayErrors; + @SuppressWarnings("unchecked") + ZipSubscriber<T, R>[] a = new ZipSubscriber[n]; + for (int i = 0; i < n; i++) { + a[i] = new ZipSubscriber<>(this, prefetch); + } + this.current = new Object[n]; + this.subscribers = a; + this.requested = new AtomicLong(); + this.errors = new AtomicThrowable(); + } + + void subscribe(Publisher<? extends T>[] sources, int n) { + ZipSubscriber<T, R>[] a = subscribers; + for (int i = 0; i < n; i++) { + if (cancelled || (!delayErrors && errors.get() != null)) { + return; + } + sources[i].subscribe(a[i]); + } + } + + @Override + public void request(long n) { + if (SubscriptionHelper.validate(n)) { + BackpressureHelper.add(requested, n); + drain(); + } + } + + @Override + public void cancel() { + if (!cancelled) { + cancelled = true; + + cancelAll(); + } + } + + void error(ZipSubscriber<T, R> inner, Throwable e) { + if (errors.tryAddThrowableOrReport(e)) { + inner.done = true; + drain(); + } + } + + void cancelAll() { + for (ZipSubscriber<T, R> s : subscribers) { + s.cancel(); + } + } + + void drain() { + + if (getAndIncrement() != 0) { + return; + } + + final Subscriber<? super R> a = downstream; + final ZipSubscriber<T, R>[] qs = subscribers; + final int n = qs.length; + Object[] values = current; + + int missed = 1; + + for (;;) { + + long r = requested.get(); + long e = 0L; + + while (r != e) { + + if (cancelled) { + return; + } + + if (!delayErrors && errors.get() != null) { + cancelAll(); + errors.tryTerminateConsumer(a); + return; + } + + boolean empty = false; + + for (int j = 0; j < n; j++) { + ZipSubscriber<T, R> inner = qs[j]; + if (values[j] == null) { + boolean d = inner.done; + SimpleQueue<T> q = inner.queue; + T v = null; + try { + + v = q != null ? q.poll() : null; + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + + errors.tryAddThrowableOrReport(ex); + if (!delayErrors) { + cancelAll(); + errors.tryTerminateConsumer(a); + return; + } + d = true; + } + + boolean sourceEmpty = v == null; + if (d && sourceEmpty) { + cancelAll(); + errors.tryTerminateConsumer(a); + return; + } + if (!sourceEmpty) { + values[j] = v; + } else { + empty = true; + } + } + } + + if (empty) { + break; + } + + R v; + + try { + v = Objects.requireNonNull(zipper.apply(values.clone()), "The zipper returned a null value"); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + cancelAll(); + errors.tryAddThrowableOrReport(ex); + errors.tryTerminateConsumer(a); + return; + } + + a.onNext(v); + + e++; + + Arrays.fill(values, null); + } + + if (r == e) { + if (cancelled) { + return; + } + + if (!delayErrors && errors.get() != null) { + cancelAll(); + errors.tryTerminateConsumer(a); + return; + } + + for (int j = 0; j < n; j++) { + ZipSubscriber<T, R> inner = qs[j]; + if (values[j] == null) { + boolean d = inner.done; + SimpleQueue<T> q = inner.queue; + T v = null; + try { + v = q != null ? q.poll() : null; + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + errors.tryAddThrowableOrReport(ex); + if (!delayErrors) { + cancelAll(); + errors.tryTerminateConsumer(a); + return; + } + d = true; + } + boolean empty = v == null; + if (d && empty) { + cancelAll(); + errors.tryTerminateConsumer(a); + return; + } + if (!empty) { + values[j] = v; + } + } + } + + } + + if (e != 0L) { + + for (ZipSubscriber<T, R> inner : qs) { + inner.request(e); + } + + if (r != Long.MAX_VALUE) { + requested.addAndGet(-e); + } + } + + missed = addAndGet(-missed); + if (missed == 0) { + break; + } + } + } + } + + static final class ZipSubscriber<T, R> extends AtomicReference<Subscription> implements FlowableSubscriber<T>, Subscription { + + private static final long serialVersionUID = -4627193790118206028L; + + final ZipCoordinator<T, R> parent; + + final int prefetch; + + final int limit; + + SimpleQueue<T> queue; + + long produced; + + volatile boolean done; + + int sourceMode; + + ZipSubscriber(ZipCoordinator<T, R> parent, int prefetch) { + this.parent = parent; + this.prefetch = prefetch; + this.limit = prefetch - (prefetch >> 2); + } + + @SuppressWarnings("unchecked") + @Override + public void onSubscribe(Subscription s) { + if (SubscriptionHelper.setOnce(this, s)) { + if (s instanceof QueueSubscription) { + QueueSubscription<T> f = (QueueSubscription<T>) s; + + int m = f.requestFusion(QueueSubscription.ANY | QueueSubscription.BOUNDARY); + + if (m == QueueSubscription.SYNC) { + sourceMode = m; + queue = f; + done = true; + parent.drain(); + return; + } + if (m == QueueSubscription.ASYNC) { + sourceMode = m; + queue = f; + s.request(prefetch); + return; + } + } + + queue = new SpscArrayQueue<>(prefetch); + + s.request(prefetch); + } + } + + @Override + public void onNext(T t) { + if (sourceMode != QueueSubscription.ASYNC) { + queue.offer(t); + } + parent.drain(); + } + + @Override + public void onError(Throwable t) { + parent.error(this, t); + } + + @Override + public void onComplete() { + done = true; + parent.drain(); + } + + @Override + public void cancel() { + SubscriptionHelper.cancel(this); + } + + @Override + public void request(long n) { + if (sourceMode != QueueSubscription.SYNC) { + long p = produced + n; + if (p >= limit) { + produced = 0L; + get().request(p); + } else { + produced = p; + } + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableZipIterable.java b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableZipIterable.java new file mode 100644 index 0000000000..25d4df529d --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableZipIterable.java @@ -0,0 +1,171 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import java.util.Iterator; +import java.util.Objects; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.BiFunction; +import io.reactivex.rxjava3.internal.subscriptions.*; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +public final class FlowableZipIterable<T, U, V> extends AbstractFlowableWithUpstream<T, V> { + final Iterable<U> other; + final BiFunction<? super T, ? super U, ? extends V> zipper; + + public FlowableZipIterable( + Flowable<T> source, + Iterable<U> other, BiFunction<? super T, ? super U, ? extends V> zipper) { + super(source); + this.other = other; + this.zipper = zipper; + } + + @Override + public void subscribeActual(Subscriber<? super V> t) { + Iterator<U> it; + + try { + it = Objects.requireNonNull(other.iterator(), "The iterator returned by other is null"); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + EmptySubscription.error(e, t); + return; + } + + boolean b; + + try { + b = it.hasNext(); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + EmptySubscription.error(e, t); + return; + } + + if (!b) { + EmptySubscription.complete(t); + return; + } + + source.subscribe(new ZipIterableSubscriber<T, U, V>(t, it, zipper)); + } + + static final class ZipIterableSubscriber<T, U, V> implements FlowableSubscriber<T>, Subscription { + final Subscriber<? super V> downstream; + final Iterator<U> iterator; + final BiFunction<? super T, ? super U, ? extends V> zipper; + + Subscription upstream; + + boolean done; + + ZipIterableSubscriber(Subscriber<? super V> actual, Iterator<U> iterator, + BiFunction<? super T, ? super U, ? extends V> zipper) { + this.downstream = actual; + this.iterator = iterator; + this.zipper = zipper; + } + + @Override + public void onSubscribe(Subscription s) { + if (SubscriptionHelper.validate(this.upstream, s)) { + this.upstream = s; + downstream.onSubscribe(this); + } + } + + @Override + public void onNext(T t) { + if (done) { + return; + } + + U u; + + try { + u = Objects.requireNonNull(iterator.next(), "The iterator returned a null value"); + } catch (Throwable e) { + fail(e); + return; + } + + V v; + try { + v = Objects.requireNonNull(zipper.apply(t, u), "The zipper function returned a null value"); + } catch (Throwable e) { + fail(e); + return; + } + + downstream.onNext(v); + + boolean b; + + try { + b = iterator.hasNext(); + } catch (Throwable e) { + fail(e); + return; + } + + if (!b) { + done = true; + upstream.cancel(); + downstream.onComplete(); + } + } + + void fail(Throwable e) { + Exceptions.throwIfFatal(e); + done = true; + upstream.cancel(); + downstream.onError(e); + } + + @Override + public void onError(Throwable t) { + if (done) { + RxJavaPlugins.onError(t); + return; + } + done = true; + downstream.onError(t); + } + + @Override + public void onComplete() { + if (done) { + return; + } + done = true; + downstream.onComplete(); + } + + @Override + public void request(long n) { + upstream.request(n); + } + + @Override + public void cancel() { + upstream.cancel(); + } + + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/AbstractMaybeWithUpstream.java b/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/AbstractMaybeWithUpstream.java new file mode 100644 index 0000000000..348bb48227 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/AbstractMaybeWithUpstream.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.maybe; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.internal.fuseable.HasUpstreamMaybeSource; + +/** + * Abstract base class for intermediate Maybe operators that take an upstream MaybeSource. + * + * @param <T> the source value type + * @param <R> the output value type + */ +abstract class AbstractMaybeWithUpstream<T, R> extends Maybe<R> implements HasUpstreamMaybeSource<T> { + + protected final MaybeSource<T> source; + + AbstractMaybeWithUpstream(MaybeSource<T> source) { + this.source = source; + } + + @Override + public final MaybeSource<T> source() { + return source; + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeAmb.java b/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeAmb.java new file mode 100644 index 0000000000..802075b2ab --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeAmb.java @@ -0,0 +1,152 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.maybe; + +import java.util.concurrent.atomic.AtomicBoolean; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.*; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.internal.disposables.EmptyDisposable; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +/** + * Signals the event of the first MaybeSource that signals. + * + * @param <T> the value type emitted + */ +public final class MaybeAmb<T> extends Maybe<T> { + private final MaybeSource<? extends T>[] sources; + private final Iterable<? extends MaybeSource<? extends T>> sourcesIterable; + + public MaybeAmb(MaybeSource<? extends T>[] sources, Iterable<? extends MaybeSource<? extends T>> sourcesIterable) { + this.sources = sources; + this.sourcesIterable = sourcesIterable; + } + + @Override + @SuppressWarnings("unchecked") + protected void subscribeActual(MaybeObserver<? super T> observer) { + MaybeSource<? extends T>[] sources = this.sources; + int count = 0; + if (sources == null) { + sources = new MaybeSource[8]; + try { + for (MaybeSource<? extends T> element : sourcesIterable) { + if (element == null) { + EmptyDisposable.error(new NullPointerException("One of the sources is null"), observer); + return; + } + if (count == sources.length) { + MaybeSource<? extends T>[] b = new MaybeSource[count + (count >> 2)]; + System.arraycopy(sources, 0, b, 0, count); + sources = b; + } + sources[count++] = element; + } + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + EmptyDisposable.error(e, observer); + return; + } + } else { + count = sources.length; + } + + CompositeDisposable set = new CompositeDisposable(); + observer.onSubscribe(set); + + AtomicBoolean winner = new AtomicBoolean(); + + for (int i = 0; i < count; i++) { + MaybeSource<? extends T> s = sources[i]; + if (set.isDisposed()) { + return; + } + + if (s == null) { + set.dispose(); + NullPointerException ex = new NullPointerException("One of the MaybeSources is null"); + if (winner.compareAndSet(false, true)) { + observer.onError(ex); + } else { + RxJavaPlugins.onError(ex); + } + return; + } + + s.subscribe(new AmbMaybeObserver<T>(observer, set, winner)); + } + + if (count == 0) { + observer.onComplete(); + } + } + + static final class AmbMaybeObserver<T> + implements MaybeObserver<T> { + + final MaybeObserver<? super T> downstream; + + final AtomicBoolean winner; + + final CompositeDisposable set; + + Disposable upstream; + + AmbMaybeObserver(MaybeObserver<? super T> downstream, CompositeDisposable set, AtomicBoolean winner) { + this.downstream = downstream; + this.set = set; + this.winner = winner; + } + + @Override + public void onSubscribe(Disposable d) { + upstream = d; + set.add(d); + } + + @Override + public void onSuccess(T value) { + if (winner.compareAndSet(false, true)) { + set.delete(upstream); + set.dispose(); + + downstream.onSuccess(value); + } + } + + @Override + public void onError(Throwable e) { + if (winner.compareAndSet(false, true)) { + set.delete(upstream); + set.dispose(); + + downstream.onError(e); + } else { + RxJavaPlugins.onError(e); + } + } + + @Override + public void onComplete() { + if (winner.compareAndSet(false, true)) { + set.delete(upstream); + set.dispose(); + + downstream.onComplete(); + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeCache.java b/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeCache.java new file mode 100644 index 0000000000..f9080a1282 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeCache.java @@ -0,0 +1,198 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.maybe; + +import java.util.concurrent.atomic.AtomicReference; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; + +/** + * Consumes the source once and replays its signal to any current or future MaybeObservers. + * + * @param <T> the value type + */ +public final class MaybeCache<T> extends Maybe<T> implements MaybeObserver<T> { + + @SuppressWarnings("rawtypes") + static final CacheDisposable[] EMPTY = new CacheDisposable[0]; + + @SuppressWarnings("rawtypes") + static final CacheDisposable[] TERMINATED = new CacheDisposable[0]; + + final AtomicReference<MaybeSource<T>> source; + + final AtomicReference<CacheDisposable<T>[]> observers; + + T value; + + Throwable error; + + @SuppressWarnings("unchecked") + public MaybeCache(MaybeSource<T> source) { + this.source = new AtomicReference<>(source); + this.observers = new AtomicReference<>(EMPTY); + } + + @Override + protected void subscribeActual(MaybeObserver<? super T> observer) { + CacheDisposable<T> parent = new CacheDisposable<>(observer, this); + observer.onSubscribe(parent); + + if (add(parent)) { + if (parent.isDisposed()) { + remove(parent); + return; + } + } else { + if (!parent.isDisposed()) { + Throwable ex = error; + if (ex != null) { + observer.onError(ex); + } else { + T v = value; + if (v != null) { + observer.onSuccess(v); + } else { + observer.onComplete(); + } + } + } + return; + } + + MaybeSource<T> src = source.getAndSet(null); + if (src != null) { + src.subscribe(this); + } + } + + @Override + public void onSubscribe(Disposable d) { + // deliberately ignored + } + + @SuppressWarnings("unchecked") + @Override + public void onSuccess(T value) { + this.value = value; + for (CacheDisposable<T> inner : observers.getAndSet(TERMINATED)) { + if (!inner.isDisposed()) { + inner.downstream.onSuccess(value); + } + } + } + + @SuppressWarnings("unchecked") + @Override + public void onError(Throwable e) { + this.error = e; + for (CacheDisposable<T> inner : observers.getAndSet(TERMINATED)) { + if (!inner.isDisposed()) { + inner.downstream.onError(e); + } + } + } + + @SuppressWarnings("unchecked") + @Override + public void onComplete() { + for (CacheDisposable<T> inner : observers.getAndSet(TERMINATED)) { + if (!inner.isDisposed()) { + inner.downstream.onComplete(); + } + } + } + + boolean add(CacheDisposable<T> inner) { + for (;;) { + CacheDisposable<T>[] a = observers.get(); + if (a == TERMINATED) { + return false; + } + int n = a.length; + + @SuppressWarnings("unchecked") + CacheDisposable<T>[] b = new CacheDisposable[n + 1]; + System.arraycopy(a, 0, b, 0, n); + b[n] = inner; + if (observers.compareAndSet(a, b)) { + return true; + } + } + } + + @SuppressWarnings("unchecked") + void remove(CacheDisposable<T> inner) { + for (;;) { + CacheDisposable<T>[] a = observers.get(); + int n = a.length; + if (n == 0) { + return; + } + + int j = -1; + + for (int i = 0; i < n; i++) { + if (a[i] == inner) { + j = i; + break; + } + } + + if (j < 0) { + return; + } + + CacheDisposable<T>[] b; + if (n == 1) { + b = EMPTY; + } else { + b = new CacheDisposable[n - 1]; + System.arraycopy(a, 0, b, 0, j); + System.arraycopy(a, j + 1, b, j, n - j - 1); + } + if (observers.compareAndSet(a, b)) { + return; + } + } + } + + static final class CacheDisposable<T> + extends AtomicReference<MaybeCache<T>> + implements Disposable { + + private static final long serialVersionUID = -5791853038359966195L; + + final MaybeObserver<? super T> downstream; + + CacheDisposable(MaybeObserver<? super T> actual, MaybeCache<T> parent) { + super(parent); + this.downstream = actual; + } + + @Override + public void dispose() { + MaybeCache<T> mc = getAndSet(null); + if (mc != null) { + mc.remove(this); + } + } + + @Override + public boolean isDisposed() { + return get() == null; + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeCallbackObserver.java b/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeCallbackObserver.java new file mode 100644 index 0000000000..c94c7d4929 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeCallbackObserver.java @@ -0,0 +1,104 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.maybe; + +import java.util.concurrent.atomic.AtomicReference; + +import io.reactivex.rxjava3.core.MaybeObserver; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.observers.LambdaConsumerIntrospection; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +/** + * MaybeObserver that delegates the onSuccess, onError and onComplete method calls to callbacks. + * + * @param <T> the value type + */ +public final class MaybeCallbackObserver<T> +extends AtomicReference<Disposable> +implements MaybeObserver<T>, Disposable, LambdaConsumerIntrospection { + + private static final long serialVersionUID = -6076952298809384986L; + + final Consumer<? super T> onSuccess; + + final Consumer<? super Throwable> onError; + + final Action onComplete; + + public MaybeCallbackObserver(Consumer<? super T> onSuccess, Consumer<? super Throwable> onError, + Action onComplete) { + super(); + this.onSuccess = onSuccess; + this.onError = onError; + this.onComplete = onComplete; + } + + @Override + public void dispose() { + DisposableHelper.dispose(this); + } + + @Override + public boolean isDisposed() { + return DisposableHelper.isDisposed(get()); + } + + @Override + public void onSubscribe(Disposable d) { + DisposableHelper.setOnce(this, d); + } + + @Override + public void onSuccess(T value) { + lazySet(DisposableHelper.DISPOSED); + try { + onSuccess.accept(value); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + RxJavaPlugins.onError(ex); + } + } + + @Override + public void onError(Throwable e) { + lazySet(DisposableHelper.DISPOSED); + try { + onError.accept(e); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + RxJavaPlugins.onError(new CompositeException(e, ex)); + } + } + + @Override + public void onComplete() { + lazySet(DisposableHelper.DISPOSED); + try { + onComplete.run(); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + RxJavaPlugins.onError(ex); + } + } + + @Override + public boolean hasCustomOnError() { + return onError != Functions.ON_ERROR_MISSING; + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeConcatArray.java b/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeConcatArray.java new file mode 100644 index 0000000000..889e229b64 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeConcatArray.java @@ -0,0 +1,163 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.maybe; + +import java.util.concurrent.atomic.*; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.disposables.SequentialDisposable; +import io.reactivex.rxjava3.internal.subscriptions.SubscriptionHelper; +import io.reactivex.rxjava3.internal.util.*; + +/** + * Concatenate values of each MaybeSource provided in an array. + * + * @param <T> the value type + */ +public final class MaybeConcatArray<T> extends Flowable<T> { + + final MaybeSource<? extends T>[] sources; + + public MaybeConcatArray(MaybeSource<? extends T>[] sources) { + this.sources = sources; + } + + @Override + protected void subscribeActual(Subscriber<? super T> s) { + ConcatMaybeObserver<T> parent = new ConcatMaybeObserver<>(s, sources); + s.onSubscribe(parent); + parent.drain(); + } + + static final class ConcatMaybeObserver<T> + extends AtomicInteger + implements MaybeObserver<T>, Subscription { + + private static final long serialVersionUID = 3520831347801429610L; + + final Subscriber<? super T> downstream; + + final AtomicLong requested; + + final AtomicReference<Object> current; + + final SequentialDisposable disposables; + + final MaybeSource<? extends T>[] sources; + + int index; + + long produced; + + ConcatMaybeObserver(Subscriber<? super T> actual, MaybeSource<? extends T>[] sources) { + this.downstream = actual; + this.sources = sources; + this.requested = new AtomicLong(); + this.disposables = new SequentialDisposable(); + this.current = new AtomicReference<>(NotificationLite.COMPLETE); // as if a previous completed + } + + @Override + public void request(long n) { + if (SubscriptionHelper.validate(n)) { + BackpressureHelper.add(requested, n); + drain(); + } + } + + @Override + public void cancel() { + disposables.dispose(); + } + + @Override + public void onSubscribe(Disposable d) { + disposables.replace(d); + } + + @Override + public void onSuccess(T value) { + current.lazySet(value); + drain(); + } + + @Override + public void onError(Throwable e) { + downstream.onError(e); + } + + @Override + public void onComplete() { + current.lazySet(NotificationLite.COMPLETE); + drain(); + } + + @SuppressWarnings("unchecked") + void drain() { + if (getAndIncrement() != 0) { + return; + } + + AtomicReference<Object> c = current; + Subscriber<? super T> a = downstream; + Disposable cancelled = disposables; + + for (;;) { + if (cancelled.isDisposed()) { + c.lazySet(null); + return; + } + + Object o = c.get(); + + if (o != null) { + boolean goNextSource; + if (o != NotificationLite.COMPLETE) { + long p = produced; + if (p != requested.get()) { + produced = p + 1; + c.lazySet(null); + goNextSource = true; + + a.onNext((T)o); + } else { + goNextSource = false; + } + } else { + goNextSource = true; + c.lazySet(null); + } + + if (goNextSource && !cancelled.isDisposed()) { + int i = index; + if (i == sources.length) { + a.onComplete(); + return; + } + index = i + 1; + + sources[i].subscribe(this); + } + } + + if (decrementAndGet() == 0) { + break; + } + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeConcatArrayDelayError.java b/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeConcatArrayDelayError.java new file mode 100644 index 0000000000..3b4b87399a --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeConcatArrayDelayError.java @@ -0,0 +1,171 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.maybe; + +import java.util.concurrent.atomic.*; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.disposables.SequentialDisposable; +import io.reactivex.rxjava3.internal.subscriptions.SubscriptionHelper; +import io.reactivex.rxjava3.internal.util.*; + +/** + * Concatenate values of each MaybeSource provided in an array and delays + * any errors till the very end. + * + * @param <T> the value type + */ +public final class MaybeConcatArrayDelayError<T> extends Flowable<T> { + + final MaybeSource<? extends T>[] sources; + + public MaybeConcatArrayDelayError(MaybeSource<? extends T>[] sources) { + this.sources = sources; + } + + @Override + protected void subscribeActual(Subscriber<? super T> s) { + ConcatMaybeObserver<T> parent = new ConcatMaybeObserver<>(s, sources); + s.onSubscribe(parent); + parent.drain(); + } + + static final class ConcatMaybeObserver<T> + extends AtomicInteger + implements MaybeObserver<T>, Subscription { + + private static final long serialVersionUID = 3520831347801429610L; + + final Subscriber<? super T> downstream; + + final AtomicLong requested; + + final AtomicReference<Object> current; + + final SequentialDisposable disposables; + + final MaybeSource<? extends T>[] sources; + + final AtomicThrowable errors; + + int index; + + long produced; + + ConcatMaybeObserver(Subscriber<? super T> actual, MaybeSource<? extends T>[] sources) { + this.downstream = actual; + this.sources = sources; + this.requested = new AtomicLong(); + this.disposables = new SequentialDisposable(); + this.current = new AtomicReference<>(NotificationLite.COMPLETE); // as if a previous completed + this.errors = new AtomicThrowable(); + } + + @Override + public void request(long n) { + if (SubscriptionHelper.validate(n)) { + BackpressureHelper.add(requested, n); + drain(); + } + } + + @Override + public void cancel() { + disposables.dispose(); + errors.tryTerminateAndReport(); + } + + @Override + public void onSubscribe(Disposable d) { + disposables.replace(d); + } + + @Override + public void onSuccess(T value) { + current.lazySet(value); + drain(); + } + + @Override + public void onError(Throwable e) { + current.lazySet(NotificationLite.COMPLETE); + if (errors.tryAddThrowableOrReport(e)) { + drain(); + } + } + + @Override + public void onComplete() { + current.lazySet(NotificationLite.COMPLETE); + drain(); + } + + @SuppressWarnings("unchecked") + void drain() { + if (getAndIncrement() != 0) { + return; + } + + AtomicReference<Object> c = current; + Subscriber<? super T> a = downstream; + Disposable cancelled = disposables; + + for (;;) { + if (cancelled.isDisposed()) { + c.lazySet(null); + return; + } + + Object o = c.get(); + + if (o != null) { + boolean goNextSource; + if (o != NotificationLite.COMPLETE) { + long p = produced; + if (p != requested.get()) { + produced = p + 1; + c.lazySet(null); + goNextSource = true; + + a.onNext((T)o); + } else { + goNextSource = false; + } + } else { + goNextSource = true; + c.lazySet(null); + } + + if (goNextSource && !cancelled.isDisposed()) { + int i = index; + if (i == sources.length) { + errors.tryTerminateConsumer(downstream); + return; + } + index = i + 1; + + sources[i].subscribe(this); + } + } + + if (decrementAndGet() == 0) { + break; + } + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeConcatIterable.java b/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeConcatIterable.java new file mode 100644 index 0000000000..b777f4c751 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeConcatIterable.java @@ -0,0 +1,192 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.maybe; + +import java.util.Iterator; +import java.util.Objects; +import java.util.concurrent.atomic.*; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.internal.disposables.SequentialDisposable; +import io.reactivex.rxjava3.internal.subscriptions.*; +import io.reactivex.rxjava3.internal.util.*; + +/** + * Concatenate values of each MaybeSource provided by an Iterable. + * + * @param <T> the value type + */ +public final class MaybeConcatIterable<T> extends Flowable<T> { + + final Iterable<? extends MaybeSource<? extends T>> sources; + + public MaybeConcatIterable(Iterable<? extends MaybeSource<? extends T>> sources) { + this.sources = sources; + } + + @Override + protected void subscribeActual(Subscriber<? super T> s) { + + Iterator<? extends MaybeSource<? extends T>> it; + + try { + it = Objects.requireNonNull(sources.iterator(), "The sources Iterable returned a null Iterator"); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + EmptySubscription.error(ex, s); + return; + } + + ConcatMaybeObserver<T> parent = new ConcatMaybeObserver<>(s, it); + s.onSubscribe(parent); + parent.drain(); + } + + static final class ConcatMaybeObserver<T> + extends AtomicInteger + implements MaybeObserver<T>, Subscription { + + private static final long serialVersionUID = 3520831347801429610L; + + final Subscriber<? super T> downstream; + + final AtomicLong requested; + + final AtomicReference<Object> current; + + final SequentialDisposable disposables; + + final Iterator<? extends MaybeSource<? extends T>> sources; + + long produced; + + ConcatMaybeObserver(Subscriber<? super T> actual, Iterator<? extends MaybeSource<? extends T>> sources) { + this.downstream = actual; + this.sources = sources; + this.requested = new AtomicLong(); + this.disposables = new SequentialDisposable(); + this.current = new AtomicReference<>(NotificationLite.COMPLETE); // as if a previous completed + } + + @Override + public void request(long n) { + if (SubscriptionHelper.validate(n)) { + BackpressureHelper.add(requested, n); + drain(); + } + } + + @Override + public void cancel() { + disposables.dispose(); + } + + @Override + public void onSubscribe(Disposable d) { + disposables.replace(d); + } + + @Override + public void onSuccess(T value) { + current.lazySet(value); + drain(); + } + + @Override + public void onError(Throwable e) { + downstream.onError(e); + } + + @Override + public void onComplete() { + current.lazySet(NotificationLite.COMPLETE); + drain(); + } + + @SuppressWarnings("unchecked") + void drain() { + if (getAndIncrement() != 0) { + return; + } + + AtomicReference<Object> c = current; + Subscriber<? super T> a = downstream; + Disposable cancelled = disposables; + + for (;;) { + if (cancelled.isDisposed()) { + c.lazySet(null); + return; + } + + Object o = c.get(); + + if (o != null) { + boolean goNextSource; + if (o != NotificationLite.COMPLETE) { + long p = produced; + if (p != requested.get()) { + produced = p + 1; + c.lazySet(null); + goNextSource = true; + + a.onNext((T)o); + } else { + goNextSource = false; + } + } else { + goNextSource = true; + c.lazySet(null); + } + + if (goNextSource && !cancelled.isDisposed()) { + boolean b; + + try { + b = sources.hasNext(); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + a.onError(ex); + return; + } + + if (b) { + MaybeSource<? extends T> source; + + try { + source = Objects.requireNonNull(sources.next(), "The source Iterator returned a null MaybeSource"); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + a.onError(ex); + return; + } + + source.subscribe(this); + } else { + a.onComplete(); + } + } + } + + if (decrementAndGet() == 0) { + break; + } + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeContains.java b/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeContains.java new file mode 100644 index 0000000000..c1154067f5 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeContains.java @@ -0,0 +1,99 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.maybe; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; +import io.reactivex.rxjava3.internal.fuseable.HasUpstreamMaybeSource; +import java.util.Objects; + +/** + * Signals true if the source signals a value that is object-equals with the provided + * value, false otherwise or for empty sources. + * + * @param <T> the value type + */ +public final class MaybeContains<T> extends Single<Boolean> implements HasUpstreamMaybeSource<T> { + + final MaybeSource<T> source; + + final Object value; + + public MaybeContains(MaybeSource<T> source, Object value) { + this.source = source; + this.value = value; + } + + @Override + public MaybeSource<T> source() { + return source; + } + + @Override + protected void subscribeActual(SingleObserver<? super Boolean> observer) { + source.subscribe(new ContainsMaybeObserver(observer, value)); + } + + static final class ContainsMaybeObserver implements MaybeObserver<Object>, Disposable { + + final SingleObserver<? super Boolean> downstream; + + final Object value; + + Disposable upstream; + + ContainsMaybeObserver(SingleObserver<? super Boolean> actual, Object value) { + this.downstream = actual; + this.value = value; + } + + @Override + public void dispose() { + upstream.dispose(); + upstream = DisposableHelper.DISPOSED; + } + + @Override + public boolean isDisposed() { + return upstream.isDisposed(); + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.validate(this.upstream, d)) { + this.upstream = d; + downstream.onSubscribe(this); + } + } + + @Override + public void onSuccess(Object value) { + upstream = DisposableHelper.DISPOSED; + downstream.onSuccess(Objects.equals(value, this.value)); + } + + @Override + public void onError(Throwable e) { + upstream = DisposableHelper.DISPOSED; + downstream.onError(e); + } + + @Override + public void onComplete() { + upstream = DisposableHelper.DISPOSED; + downstream.onSuccess(false); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeCount.java b/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeCount.java new file mode 100644 index 0000000000..cd2bececf1 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeCount.java @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.maybe; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; +import io.reactivex.rxjava3.internal.fuseable.HasUpstreamMaybeSource; + +/** + * Signals 1L if the source signalled an item or 0L if the source is empty. + * + * @param <T> the source value type + */ +public final class MaybeCount<T> extends Single<Long> implements HasUpstreamMaybeSource<T> { + + final MaybeSource<T> source; + + public MaybeCount(MaybeSource<T> source) { + this.source = source; + } + + @Override + public MaybeSource<T> source() { + return source; + } + + @Override + protected void subscribeActual(SingleObserver<? super Long> observer) { + source.subscribe(new CountMaybeObserver(observer)); + } + + static final class CountMaybeObserver implements MaybeObserver<Object>, Disposable { + final SingleObserver<? super Long> downstream; + + Disposable upstream; + + CountMaybeObserver(SingleObserver<? super Long> downstream) { + this.downstream = downstream; + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.validate(this.upstream, d)) { + this.upstream = d; + + downstream.onSubscribe(this); + } + } + + @Override + public void onSuccess(Object value) { + upstream = DisposableHelper.DISPOSED; + downstream.onSuccess(1L); + } + + @Override + public void onError(Throwable e) { + upstream = DisposableHelper.DISPOSED; + downstream.onError(e); + } + + @Override + public void onComplete() { + upstream = DisposableHelper.DISPOSED; + downstream.onSuccess(0L); + } + + @Override + public boolean isDisposed() { + return upstream.isDisposed(); + } + + @Override + public void dispose() { + upstream.dispose(); + upstream = DisposableHelper.DISPOSED; + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeCreate.java b/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeCreate.java new file mode 100644 index 0000000000..1a6bc60d31 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeCreate.java @@ -0,0 +1,154 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.maybe; + +import java.util.concurrent.atomic.AtomicReference; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.Cancellable; +import io.reactivex.rxjava3.internal.disposables.*; +import io.reactivex.rxjava3.internal.util.ExceptionHelper; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +/** + * Provides an API over MaybeObserver that serializes calls to onXXX and manages cancellation + * in a safe manner. + * + * @param <T> the value type emitted + */ +public final class MaybeCreate<T> extends Maybe<T> { + + final MaybeOnSubscribe<T> source; + + public MaybeCreate(MaybeOnSubscribe<T> source) { + this.source = source; + } + + @Override + protected void subscribeActual(MaybeObserver<? super T> observer) { + Emitter<T> parent = new Emitter<>(observer); + observer.onSubscribe(parent); + + try { + source.subscribe(parent); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + parent.onError(ex); + } + } + + static final class Emitter<T> + extends AtomicReference<Disposable> + implements MaybeEmitter<T>, Disposable { + + final MaybeObserver<? super T> downstream; + + Emitter(MaybeObserver<? super T> downstream) { + this.downstream = downstream; + } + + private static final long serialVersionUID = -2467358622224974244L; + + @Override + public void onSuccess(T value) { + if (get() != DisposableHelper.DISPOSED) { + Disposable d = getAndSet(DisposableHelper.DISPOSED); + if (d != DisposableHelper.DISPOSED) { + try { + if (value == null) { + downstream.onError(ExceptionHelper.createNullPointerException("onSuccess called with a null value.")); + } else { + downstream.onSuccess(value); + } + } finally { + if (d != null) { + d.dispose(); + } + } + } + } + } + + @Override + public void onError(Throwable t) { + if (!tryOnError(t)) { + RxJavaPlugins.onError(t); + } + } + + @Override + public boolean tryOnError(Throwable t) { + if (t == null) { + t = ExceptionHelper.createNullPointerException("onError called with a null Throwable."); + } + if (get() != DisposableHelper.DISPOSED) { + Disposable d = getAndSet(DisposableHelper.DISPOSED); + if (d != DisposableHelper.DISPOSED) { + try { + downstream.onError(t); + } finally { + if (d != null) { + d.dispose(); + } + } + return true; + } + } + return false; + } + + @Override + public void onComplete() { + if (get() != DisposableHelper.DISPOSED) { + Disposable d = getAndSet(DisposableHelper.DISPOSED); + if (d != DisposableHelper.DISPOSED) { + try { + downstream.onComplete(); + } finally { + if (d != null) { + d.dispose(); + } + } + } + } + } + + @Override + public void setDisposable(Disposable d) { + DisposableHelper.set(this, d); + } + + @Override + public void setCancellable(Cancellable c) { + setDisposable(new CancellableDisposable(c)); + } + + @Override + public void dispose() { + DisposableHelper.dispose(this); + } + + @Override + public boolean isDisposed() { + return DisposableHelper.isDisposed(get()); + } + + @Override + public String toString() { + return String.format("%s{%s}", getClass().getSimpleName(), super.toString()); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeDefer.java b/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeDefer.java new file mode 100644 index 0000000000..a1ffacef74 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeDefer.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.maybe; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.Supplier; +import io.reactivex.rxjava3.internal.disposables.EmptyDisposable; + +import java.util.Objects; + +/** + * Defers the creation of the actual Maybe the incoming MaybeObserver is subscribed to. + * + * @param <T> the value type + */ +public final class MaybeDefer<T> extends Maybe<T> { + + final Supplier<? extends MaybeSource<? extends T>> maybeSupplier; + + public MaybeDefer(Supplier<? extends MaybeSource<? extends T>> maybeSupplier) { + this.maybeSupplier = maybeSupplier; + } + + @Override + protected void subscribeActual(MaybeObserver<? super T> observer) { + MaybeSource<? extends T> source; + + try { + source = Objects.requireNonNull(maybeSupplier.get(), "The maybeSupplier returned a null MaybeSource"); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + EmptyDisposable.error(ex, observer); + return; + } + + source.subscribe(observer); + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeDelay.java b/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeDelay.java new file mode 100644 index 0000000000..5f13010781 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeDelay.java @@ -0,0 +1,132 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.maybe; + +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; + +/** + * Delays all signal types by the given amount and re-emits them on the given scheduler. + * + * @param <T> the value type + */ +public final class MaybeDelay<T> extends AbstractMaybeWithUpstream<T, T> { + + final long delay; + + final TimeUnit unit; + + final Scheduler scheduler; + + final boolean delayError; + + public MaybeDelay(MaybeSource<T> source, long delay, TimeUnit unit, Scheduler scheduler, boolean delayError) { + super(source); + this.delay = delay; + this.unit = unit; + this.scheduler = scheduler; + this.delayError = delayError; + } + + @Override + protected void subscribeActual(MaybeObserver<? super T> observer) { + source.subscribe(new DelayMaybeObserver<>(observer, delay, unit, scheduler, delayError)); + } + + static final class DelayMaybeObserver<T> + extends AtomicReference<Disposable> + implements MaybeObserver<T>, Disposable, Runnable { + + private static final long serialVersionUID = 5566860102500855068L; + + final MaybeObserver<? super T> downstream; + + final long delay; + + final TimeUnit unit; + + final Scheduler scheduler; + + final boolean delayError; + + T value; + + Throwable error; + + DelayMaybeObserver(MaybeObserver<? super T> actual, long delay, TimeUnit unit, Scheduler scheduler, boolean delayError) { + this.downstream = actual; + this.delay = delay; + this.unit = unit; + this.scheduler = scheduler; + this.delayError = delayError; + } + + @Override + public void run() { + Throwable ex = error; + if (ex != null) { + downstream.onError(ex); + } else { + T v = value; + if (v != null) { + downstream.onSuccess(v); + } else { + downstream.onComplete(); + } + } + } + + @Override + public void dispose() { + DisposableHelper.dispose(this); + } + + @Override + public boolean isDisposed() { + return DisposableHelper.isDisposed(get()); + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.setOnce(this, d)) { + downstream.onSubscribe(this); + } + } + + @Override + public void onSuccess(T value) { + this.value = value; + schedule(delay); + } + + @Override + public void onError(Throwable e) { + this.error = e; + schedule(delayError ? delay : 0); + } + + @Override + public void onComplete() { + schedule(delay); + } + + void schedule(long delay) { + DisposableHelper.replace(this, scheduler.scheduleDirect(this, delay, unit)); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeDelayOtherPublisher.java b/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeDelayOtherPublisher.java new file mode 100644 index 0000000000..eebddc3c87 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeDelayOtherPublisher.java @@ -0,0 +1,161 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.maybe; + +import java.util.concurrent.atomic.AtomicReference; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.CompositeException; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; +import io.reactivex.rxjava3.internal.subscriptions.SubscriptionHelper; + +/** + * Delay the emission of the main signal until the other signals an item or completes. + * + * @param <T> the main value type + * @param <U> the other value type + */ +public final class MaybeDelayOtherPublisher<T, U> extends AbstractMaybeWithUpstream<T, T> { + + final Publisher<U> other; + + public MaybeDelayOtherPublisher(MaybeSource<T> source, Publisher<U> other) { + super(source); + this.other = other; + } + + @Override + protected void subscribeActual(MaybeObserver<? super T> observer) { + source.subscribe(new DelayMaybeObserver<>(observer, other)); + } + + static final class DelayMaybeObserver<T, U> + implements MaybeObserver<T>, Disposable { + final OtherSubscriber<T> other; + + final Publisher<U> otherSource; + + Disposable upstream; + + DelayMaybeObserver(MaybeObserver<? super T> actual, Publisher<U> otherSource) { + this.other = new OtherSubscriber<>(actual); + this.otherSource = otherSource; + } + + @Override + public void dispose() { + upstream.dispose(); + upstream = DisposableHelper.DISPOSED; + SubscriptionHelper.cancel(other); + } + + @Override + public boolean isDisposed() { + return other.get() == SubscriptionHelper.CANCELLED; + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.validate(this.upstream, d)) { + this.upstream = d; + + other.downstream.onSubscribe(this); + } + } + + @Override + public void onSuccess(T value) { + upstream = DisposableHelper.DISPOSED; + other.value = value; + subscribeNext(); + } + + @Override + public void onError(Throwable e) { + upstream = DisposableHelper.DISPOSED; + other.error = e; + subscribeNext(); + } + + @Override + public void onComplete() { + upstream = DisposableHelper.DISPOSED; + subscribeNext(); + } + + void subscribeNext() { + otherSource.subscribe(other); + } + } + + static final class OtherSubscriber<T> extends + AtomicReference<Subscription> + implements FlowableSubscriber<Object> { + + private static final long serialVersionUID = -1215060610805418006L; + + final MaybeObserver<? super T> downstream; + + T value; + + Throwable error; + + OtherSubscriber(MaybeObserver<? super T> downstream) { + this.downstream = downstream; + } + + @Override + public void onSubscribe(Subscription s) { + SubscriptionHelper.setOnce(this, s, Long.MAX_VALUE); + } + + @Override + public void onNext(Object t) { + Subscription s = get(); + if (s != SubscriptionHelper.CANCELLED) { + lazySet(SubscriptionHelper.CANCELLED); + s.cancel(); + onComplete(); + } + } + + @Override + public void onError(Throwable t) { + Throwable e = error; + if (e == null) { + downstream.onError(t); + } else { + downstream.onError(new CompositeException(e, t)); + } + } + + @Override + public void onComplete() { + Throwable e = error; + if (e != null) { + downstream.onError(e); + } else { + T v = value; + if (v != null) { + downstream.onSuccess(v); + } else { + downstream.onComplete(); + } + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeDelaySubscriptionOtherPublisher.java b/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeDelaySubscriptionOtherPublisher.java new file mode 100644 index 0000000000..7cd54da714 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeDelaySubscriptionOtherPublisher.java @@ -0,0 +1,150 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.maybe; + +import java.util.concurrent.atomic.AtomicReference; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; +import io.reactivex.rxjava3.internal.subscriptions.SubscriptionHelper; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +/** + * Delay the subscription to the main Maybe until the other signals an item or completes. + * + * @param <T> the main value type + * @param <U> the other value type + */ +public final class MaybeDelaySubscriptionOtherPublisher<T, U> extends AbstractMaybeWithUpstream<T, T> { + + final Publisher<U> other; + + public MaybeDelaySubscriptionOtherPublisher(MaybeSource<T> source, Publisher<U> other) { + super(source); + this.other = other; + } + + @Override + protected void subscribeActual(MaybeObserver<? super T> observer) { + other.subscribe(new OtherSubscriber<>(observer, source)); + } + + static final class OtherSubscriber<T> implements FlowableSubscriber<Object>, Disposable { + final DelayMaybeObserver<T> main; + + MaybeSource<T> source; + + Subscription upstream; + + OtherSubscriber(MaybeObserver<? super T> actual, MaybeSource<T> source) { + this.main = new DelayMaybeObserver<>(actual); + this.source = source; + } + + @Override + public void onSubscribe(Subscription s) { + if (SubscriptionHelper.validate(this.upstream, s)) { + this.upstream = s; + + main.downstream.onSubscribe(this); + + s.request(Long.MAX_VALUE); + } + } + + @Override + public void onNext(Object t) { + if (upstream != SubscriptionHelper.CANCELLED) { + upstream.cancel(); + upstream = SubscriptionHelper.CANCELLED; + + subscribeNext(); + } + } + + @Override + public void onError(Throwable t) { + if (upstream != SubscriptionHelper.CANCELLED) { + upstream = SubscriptionHelper.CANCELLED; + + main.downstream.onError(t); + } else { + RxJavaPlugins.onError(t); + } + } + + @Override + public void onComplete() { + if (upstream != SubscriptionHelper.CANCELLED) { + upstream = SubscriptionHelper.CANCELLED; + + subscribeNext(); + } + } + + void subscribeNext() { + MaybeSource<T> src = source; + source = null; + + src.subscribe(main); + } + + @Override + public boolean isDisposed() { + return DisposableHelper.isDisposed(main.get()); + } + + @Override + public void dispose() { + upstream.cancel(); + upstream = SubscriptionHelper.CANCELLED; + DisposableHelper.dispose(main); + } + } + + static final class DelayMaybeObserver<T> extends AtomicReference<Disposable> + implements MaybeObserver<T> { + + private static final long serialVersionUID = 706635022205076709L; + + final MaybeObserver<? super T> downstream; + + DelayMaybeObserver(MaybeObserver<? super T> downstream) { + this.downstream = downstream; + } + + @Override + public void onSubscribe(Disposable d) { + DisposableHelper.setOnce(this, d); + } + + @Override + public void onSuccess(T value) { + downstream.onSuccess(value); + } + + @Override + public void onError(Throwable e) { + downstream.onError(e); + } + + @Override + public void onComplete() { + downstream.onComplete(); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeDelayWithCompletable.java b/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeDelayWithCompletable.java new file mode 100644 index 0000000000..ecbd84a274 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeDelayWithCompletable.java @@ -0,0 +1,112 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.maybe; + +import java.util.concurrent.atomic.AtomicReference; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; + +public final class MaybeDelayWithCompletable<T> extends Maybe<T> { + + final MaybeSource<T> source; + + final CompletableSource other; + + public MaybeDelayWithCompletable(MaybeSource<T> source, CompletableSource other) { + this.source = source; + this.other = other; + } + + @Override + protected void subscribeActual(MaybeObserver<? super T> observer) { + other.subscribe(new OtherObserver<>(observer, source)); + } + + static final class OtherObserver<T> + extends AtomicReference<Disposable> + implements CompletableObserver, Disposable { + private static final long serialVersionUID = 703409937383992161L; + + final MaybeObserver<? super T> downstream; + + final MaybeSource<T> source; + + OtherObserver(MaybeObserver<? super T> actual, MaybeSource<T> source) { + this.downstream = actual; + this.source = source; + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.setOnce(this, d)) { + + downstream.onSubscribe(this); + } + } + + @Override + public void onError(Throwable e) { + downstream.onError(e); + } + + @Override + public void onComplete() { + source.subscribe(new DelayWithMainObserver<>(this, downstream)); + } + + @Override + public void dispose() { + DisposableHelper.dispose(this); + } + + @Override + public boolean isDisposed() { + return DisposableHelper.isDisposed(get()); + } + } + + static final class DelayWithMainObserver<T> implements MaybeObserver<T> { + + final AtomicReference<Disposable> parent; + + final MaybeObserver<? super T> downstream; + + DelayWithMainObserver(AtomicReference<Disposable> parent, MaybeObserver<? super T> downstream) { + this.parent = parent; + this.downstream = downstream; + } + + @Override + public void onSubscribe(Disposable d) { + DisposableHelper.replace(parent, d); + } + + @Override + public void onSuccess(T value) { + downstream.onSuccess(value); + } + + @Override + public void onError(Throwable e) { + downstream.onError(e); + } + + @Override + public void onComplete() { + downstream.onComplete(); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeDematerialize.java b/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeDematerialize.java new file mode 100644 index 0000000000..80b76d3171 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeDematerialize.java @@ -0,0 +1,108 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.maybe; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; + +import java.util.Objects; + +/** + * Maps the success value of the source to a Notification, then + * maps it back to the corresponding signal type. + * <p>History: 2.2.4 - experimental + * @param <T> the element type of the source + * @param <R> the element type of the Notification and result + * @since 3.0.0 + */ +public final class MaybeDematerialize<T, R> extends AbstractMaybeWithUpstream<T, R> { + + final Function<? super T, Notification<R>> selector; + + public MaybeDematerialize(Maybe<T> source, Function<? super T, Notification<R>> selector) { + super(source); + this.selector = selector; + } + + @Override + protected void subscribeActual(MaybeObserver<? super R> observer) { + source.subscribe(new DematerializeObserver<>(observer, selector)); + } + + static final class DematerializeObserver<T, R> implements MaybeObserver<T>, Disposable { + + final MaybeObserver<? super R> downstream; + + final Function<? super T, Notification<R>> selector; + + Disposable upstream; + + DematerializeObserver(MaybeObserver<? super R> downstream, + Function<? super T, Notification<R>> selector) { + this.downstream = downstream; + this.selector = selector; + } + + @Override + public void dispose() { + upstream.dispose(); + } + + @Override + public boolean isDisposed() { + return upstream.isDisposed(); + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.validate(upstream, d)) { + upstream = d; + downstream.onSubscribe(this); + } + } + + @Override + public void onSuccess(T t) { + Notification<R> notification; + + try { + notification = Objects.requireNonNull(selector.apply(t), "The selector returned a null Notification"); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + downstream.onError(ex); + return; + } + if (notification.isOnNext()) { + downstream.onSuccess(notification.getValue()); + } else if (notification.isOnComplete()) { + downstream.onComplete(); + } else { + downstream.onError(notification.getError()); + } + } + + @Override + public void onError(Throwable e) { + downstream.onError(e); + } + + @Override + public void onComplete() { + downstream.onComplete(); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeDetach.java b/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeDetach.java new file mode 100644 index 0000000000..7dad3b0cf2 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeDetach.java @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.maybe; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; + +/** + * Breaks the references between the upstream and downstream when the Maybe terminates. + * + * @param <T> the value type + */ +public final class MaybeDetach<T> extends AbstractMaybeWithUpstream<T, T> { + + public MaybeDetach(MaybeSource<T> source) { + super(source); + } + + @Override + protected void subscribeActual(MaybeObserver<? super T> observer) { + source.subscribe(new DetachMaybeObserver<>(observer)); + } + + static final class DetachMaybeObserver<T> implements MaybeObserver<T>, Disposable { + + MaybeObserver<? super T> downstream; + + Disposable upstream; + + DetachMaybeObserver(MaybeObserver<? super T> downstream) { + this.downstream = downstream; + } + + @Override + public void dispose() { + downstream = null; + upstream.dispose(); + upstream = DisposableHelper.DISPOSED; + } + + @Override + public boolean isDisposed() { + return upstream.isDisposed(); + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.validate(this.upstream, d)) { + this.upstream = d; + + downstream.onSubscribe(this); + } + } + + @Override + public void onSuccess(T value) { + upstream = DisposableHelper.DISPOSED; + MaybeObserver<? super T> a = downstream; + if (a != null) { + downstream = null; + a.onSuccess(value); + } + } + + @Override + public void onError(Throwable e) { + upstream = DisposableHelper.DISPOSED; + MaybeObserver<? super T> a = downstream; + if (a != null) { + downstream = null; + a.onError(e); + } + } + + @Override + public void onComplete() { + upstream = DisposableHelper.DISPOSED; + MaybeObserver<? super T> a = downstream; + if (a != null) { + downstream = null; + a.onComplete(); + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeDoAfterSuccess.java b/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeDoAfterSuccess.java new file mode 100644 index 0000000000..c8f4799789 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeDoAfterSuccess.java @@ -0,0 +1,98 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.maybe; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.Consumer; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +/** + * Calls a consumer after pushing the current item to the downstream. + * <p>History: 2.0.1 - experimental + * @param <T> the value type + * @since 2.1 + */ +public final class MaybeDoAfterSuccess<T> extends AbstractMaybeWithUpstream<T, T> { + + final Consumer<? super T> onAfterSuccess; + + public MaybeDoAfterSuccess(MaybeSource<T> source, Consumer<? super T> onAfterSuccess) { + super(source); + this.onAfterSuccess = onAfterSuccess; + } + + @Override + protected void subscribeActual(MaybeObserver<? super T> observer) { + source.subscribe(new DoAfterObserver<>(observer, onAfterSuccess)); + } + + static final class DoAfterObserver<T> implements MaybeObserver<T>, Disposable { + + final MaybeObserver<? super T> downstream; + + final Consumer<? super T> onAfterSuccess; + + Disposable upstream; + + DoAfterObserver(MaybeObserver<? super T> actual, Consumer<? super T> onAfterSuccess) { + this.downstream = actual; + this.onAfterSuccess = onAfterSuccess; + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.validate(this.upstream, d)) { + this.upstream = d; + + downstream.onSubscribe(this); + } + } + + @Override + public void onSuccess(T t) { + downstream.onSuccess(t); + + try { + onAfterSuccess.accept(t); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + // remember, onSuccess is a terminal event and we can't call onError + RxJavaPlugins.onError(ex); + } + } + + @Override + public void onError(Throwable e) { + downstream.onError(e); + } + + @Override + public void onComplete() { + downstream.onComplete(); + } + + @Override + public void dispose() { + upstream.dispose(); + } + + @Override + public boolean isDisposed() { + return upstream.isDisposed(); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeDoFinally.java b/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeDoFinally.java new file mode 100644 index 0000000000..0d8dfe8de2 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeDoFinally.java @@ -0,0 +1,109 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.maybe; + +import java.util.concurrent.atomic.AtomicInteger; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.Action; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +/** + * Execute an action after an onSuccess, onError, onComplete or a dispose event. + * <p>History: 2.0.1 - experimental + * @param <T> the value type + * @since 2.1 + */ +public final class MaybeDoFinally<T> extends AbstractMaybeWithUpstream<T, T> { + + final Action onFinally; + + public MaybeDoFinally(MaybeSource<T> source, Action onFinally) { + super(source); + this.onFinally = onFinally; + } + + @Override + protected void subscribeActual(MaybeObserver<? super T> observer) { + source.subscribe(new DoFinallyObserver<>(observer, onFinally)); + } + + static final class DoFinallyObserver<T> extends AtomicInteger implements MaybeObserver<T>, Disposable { + + private static final long serialVersionUID = 4109457741734051389L; + + final MaybeObserver<? super T> downstream; + + final Action onFinally; + + Disposable upstream; + + DoFinallyObserver(MaybeObserver<? super T> actual, Action onFinally) { + this.downstream = actual; + this.onFinally = onFinally; + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.validate(this.upstream, d)) { + this.upstream = d; + + downstream.onSubscribe(this); + } + } + + @Override + public void onSuccess(T t) { + downstream.onSuccess(t); + runFinally(); + } + + @Override + public void onError(Throwable t) { + downstream.onError(t); + runFinally(); + } + + @Override + public void onComplete() { + downstream.onComplete(); + runFinally(); + } + + @Override + public void dispose() { + upstream.dispose(); + runFinally(); + } + + @Override + public boolean isDisposed() { + return upstream.isDisposed(); + } + + void runFinally() { + if (compareAndSet(0, 1)) { + try { + onFinally.run(); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + RxJavaPlugins.onError(ex); + } + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeDoOnEvent.java b/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeDoOnEvent.java new file mode 100644 index 0000000000..a2771dcb8c --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeDoOnEvent.java @@ -0,0 +1,118 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.maybe; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.BiConsumer; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; + +/** + * Calls a BiConsumer with the success, error values of the upstream Maybe or with two nulls if + * the Maybe completed. + * + * @param <T> the value type + */ +public final class MaybeDoOnEvent<T> extends AbstractMaybeWithUpstream<T, T> { + + final BiConsumer<? super T, ? super Throwable> onEvent; + + public MaybeDoOnEvent(MaybeSource<T> source, BiConsumer<? super T, ? super Throwable> onEvent) { + super(source); + this.onEvent = onEvent; + } + + @Override + protected void subscribeActual(MaybeObserver<? super T> observer) { + source.subscribe(new DoOnEventMaybeObserver<>(observer, onEvent)); + } + + static final class DoOnEventMaybeObserver<T> implements MaybeObserver<T>, Disposable { + final MaybeObserver<? super T> downstream; + + final BiConsumer<? super T, ? super Throwable> onEvent; + + Disposable upstream; + + DoOnEventMaybeObserver(MaybeObserver<? super T> actual, BiConsumer<? super T, ? super Throwable> onEvent) { + this.downstream = actual; + this.onEvent = onEvent; + } + + @Override + public void dispose() { + upstream.dispose(); + upstream = DisposableHelper.DISPOSED; + } + + @Override + public boolean isDisposed() { + return upstream.isDisposed(); + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.validate(this.upstream, d)) { + this.upstream = d; + + downstream.onSubscribe(this); + } + } + + @Override + public void onSuccess(T value) { + upstream = DisposableHelper.DISPOSED; + + try { + onEvent.accept(value, null); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + downstream.onError(ex); + return; + } + + downstream.onSuccess(value); + } + + @Override + public void onError(Throwable e) { + upstream = DisposableHelper.DISPOSED; + + try { + onEvent.accept(null, e); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + e = new CompositeException(e, ex); + } + + downstream.onError(e); + } + + @Override + public void onComplete() { + upstream = DisposableHelper.DISPOSED; + + try { + onEvent.accept(null, null); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + downstream.onError(ex); + return; + } + + downstream.onComplete(); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeDoOnLifecycle.java b/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeDoOnLifecycle.java new file mode 100644 index 0000000000..0399329362 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeDoOnLifecycle.java @@ -0,0 +1,126 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.maybe; + +import io.reactivex.rxjava3.annotations.NonNull; +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.disposables.*; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +/** + * Invokes callbacks upon {@code onSubscribe} from upstream and + * {@code dispose} from downstream. + * + * @param <T> the element type of the flow + * @since 3.0.0 + */ +public final class MaybeDoOnLifecycle<T> extends AbstractMaybeWithUpstream<T, T> { + + final Consumer<? super Disposable> onSubscribe; + + final Action onDispose; + + public MaybeDoOnLifecycle(Maybe<T> upstream, Consumer<? super Disposable> onSubscribe, + Action onDispose) { + super(upstream); + this.onSubscribe = onSubscribe; + this.onDispose = onDispose; + } + + @Override + protected void subscribeActual(MaybeObserver<? super T> observer) { + source.subscribe(new MaybeLifecycleObserver<>(observer, onSubscribe, onDispose)); + } + + static final class MaybeLifecycleObserver<T> implements MaybeObserver<T>, Disposable { + + final MaybeObserver<? super T> downstream; + + final Consumer<? super Disposable> onSubscribe; + + final Action onDispose; + + Disposable upstream; + + MaybeLifecycleObserver(MaybeObserver<? super T> downstream, Consumer<? super Disposable> onSubscribe, Action onDispose) { + this.downstream = downstream; + this.onSubscribe = onSubscribe; + this.onDispose = onDispose; + } + + @Override + public void onSubscribe(@NonNull Disposable d) { + // this way, multiple calls to onSubscribe can show up in tests that use doOnSubscribe to validate behavior + try { + onSubscribe.accept(d); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + d.dispose(); + this.upstream = DisposableHelper.DISPOSED; + EmptyDisposable.error(e, downstream); + return; + } + if (DisposableHelper.validate(this.upstream, d)) { + this.upstream = d; + downstream.onSubscribe(this); + } + } + + @Override + public void onSuccess(@NonNull T t) { + if (upstream != DisposableHelper.DISPOSED) { + upstream = DisposableHelper.DISPOSED; + downstream.onSuccess(t); + } + } + + @Override + public void onError(@NonNull Throwable e) { + if (upstream != DisposableHelper.DISPOSED) { + upstream = DisposableHelper.DISPOSED; + downstream.onError(e); + } else { + RxJavaPlugins.onError(e); + } + } + + @Override + public void onComplete() { + if (upstream != DisposableHelper.DISPOSED) { + upstream = DisposableHelper.DISPOSED; + downstream.onComplete(); + } + } + + @Override + public void dispose() { + try { + onDispose.run(); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + RxJavaPlugins.onError(e); + } + upstream.dispose(); + upstream = DisposableHelper.DISPOSED; + } + + @Override + public boolean isDisposed() { + return upstream.isDisposed(); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeDoOnTerminate.java b/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeDoOnTerminate.java new file mode 100644 index 0000000000..6c3494ac96 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeDoOnTerminate.java @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.maybe; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.Action; + +public final class MaybeDoOnTerminate<T> extends Maybe<T> { + + final MaybeSource<T> source; + + final Action onTerminate; + + public MaybeDoOnTerminate(MaybeSource<T> source, Action onTerminate) { + this.source = source; + this.onTerminate = onTerminate; + } + + @Override + protected void subscribeActual(MaybeObserver<? super T> observer) { + source.subscribe(new DoOnTerminate(observer)); + } + + final class DoOnTerminate implements MaybeObserver<T> { + final MaybeObserver<? super T> downstream; + + DoOnTerminate(MaybeObserver<? super T> observer) { + this.downstream = observer; + } + + @Override + public void onSubscribe(Disposable d) { + downstream.onSubscribe(d); + } + + @Override + public void onSuccess(T value) { + try { + onTerminate.run(); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + downstream.onError(ex); + return; + } + + downstream.onSuccess(value); + } + + @Override + public void onError(Throwable e) { + try { + onTerminate.run(); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + e = new CompositeException(e, ex); + } + + downstream.onError(e); + } + + @Override + public void onComplete() { + try { + onTerminate.run(); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + downstream.onError(ex); + return; + } + + downstream.onComplete(); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeEmpty.java b/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeEmpty.java new file mode 100644 index 0000000000..a803b84fa9 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeEmpty.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.maybe; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.internal.disposables.EmptyDisposable; +import io.reactivex.rxjava3.operators.ScalarSupplier; + +/** + * Signals an onComplete. + */ +public final class MaybeEmpty extends Maybe<Object> implements ScalarSupplier<Object> { + + public static final MaybeEmpty INSTANCE = new MaybeEmpty(); + + @Override + protected void subscribeActual(MaybeObserver<? super Object> observer) { + EmptyDisposable.complete(observer); + } + + @Override + public Object get() { + return null; // nulls of ScalarCallable are considered empty sources + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeEqualSingle.java b/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeEqualSingle.java new file mode 100644 index 0000000000..bf940b8223 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeEqualSingle.java @@ -0,0 +1,166 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.maybe; + +import java.util.concurrent.atomic.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.BiPredicate; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +/** + * Compares two MaybeSources to see if they are both empty or emit the same value compared + * via a BiPredicate. + * + * @param <T> the common base type of the sources + */ +public final class MaybeEqualSingle<T> extends Single<Boolean> { + final MaybeSource<? extends T> source1; + + final MaybeSource<? extends T> source2; + + final BiPredicate<? super T, ? super T> isEqual; + + public MaybeEqualSingle(MaybeSource<? extends T> source1, MaybeSource<? extends T> source2, + BiPredicate<? super T, ? super T> isEqual) { + this.source1 = source1; + this.source2 = source2; + this.isEqual = isEqual; + } + + @Override + protected void subscribeActual(SingleObserver<? super Boolean> observer) { + EqualCoordinator<T> parent = new EqualCoordinator<>(observer, isEqual); + observer.onSubscribe(parent); + parent.subscribe(source1, source2); + } + + @SuppressWarnings("serial") + static final class EqualCoordinator<T> + extends AtomicInteger + implements Disposable { + final SingleObserver<? super Boolean> downstream; + + final EqualObserver<T> observer1; + + final EqualObserver<T> observer2; + + final BiPredicate<? super T, ? super T> isEqual; + + EqualCoordinator(SingleObserver<? super Boolean> actual, BiPredicate<? super T, ? super T> isEqual) { + super(2); + this.downstream = actual; + this.isEqual = isEqual; + this.observer1 = new EqualObserver<>(this); + this.observer2 = new EqualObserver<>(this); + } + + void subscribe(MaybeSource<? extends T> source1, MaybeSource<? extends T> source2) { + source1.subscribe(observer1); + source2.subscribe(observer2); + } + + @Override + public void dispose() { + observer1.dispose(); + observer2.dispose(); + } + + @Override + public boolean isDisposed() { + return DisposableHelper.isDisposed(observer1.get()); + } + + @SuppressWarnings("unchecked") + void done() { + if (decrementAndGet() == 0) { + Object o1 = observer1.value; + Object o2 = observer2.value; + + if (o1 != null && o2 != null) { + boolean b; + + try { + b = isEqual.test((T)o1, (T)o2); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + downstream.onError(ex); + return; + } + + downstream.onSuccess(b); + } else { + downstream.onSuccess(o1 == null && o2 == null); + } + } + } + + void error(EqualObserver<T> sender, Throwable ex) { + if (getAndSet(0) > 0) { + if (sender == observer1) { + observer2.dispose(); + } else { + observer1.dispose(); + } + downstream.onError(ex); + } else { + RxJavaPlugins.onError(ex); + } + } + } + + static final class EqualObserver<T> + extends AtomicReference<Disposable> + implements MaybeObserver<T> { + + private static final long serialVersionUID = -3031974433025990931L; + + final EqualCoordinator<T> parent; + + Object value; + + EqualObserver(EqualCoordinator<T> parent) { + this.parent = parent; + } + + public void dispose() { + DisposableHelper.dispose(this); + } + + @Override + public void onSubscribe(Disposable d) { + DisposableHelper.setOnce(this, d); + } + + @Override + public void onSuccess(T value) { + this.value = value; + parent.done(); + } + + @Override + public void onError(Throwable e) { + parent.error(this, e); + } + + @Override + public void onComplete() { + parent.done(); + } + + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeError.java b/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeError.java new file mode 100644 index 0000000000..ed9c391f02 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeError.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.maybe; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; + +/** + * Signals a constant Throwable. + * + * @param <T> the value type + */ +public final class MaybeError<T> extends Maybe<T> { + + final Throwable error; + + public MaybeError(Throwable error) { + this.error = error; + } + + @Override + protected void subscribeActual(MaybeObserver<? super T> observer) { + observer.onSubscribe(Disposable.disposed()); + observer.onError(error); + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeErrorCallable.java b/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeErrorCallable.java new file mode 100644 index 0000000000..360b729933 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeErrorCallable.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.maybe; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.Supplier; +import io.reactivex.rxjava3.internal.util.ExceptionHelper; + +/** + * Signals a Throwable returned by a Supplier. + * + * @param <T> the value type + */ +public final class MaybeErrorCallable<T> extends Maybe<T> { + + final Supplier<? extends Throwable> errorSupplier; + + public MaybeErrorCallable(Supplier<? extends Throwable> errorSupplier) { + this.errorSupplier = errorSupplier; + } + + @Override + protected void subscribeActual(MaybeObserver<? super T> observer) { + observer.onSubscribe(Disposable.disposed()); + Throwable ex; + + try { + ex = ExceptionHelper.nullCheck(errorSupplier.get(), "Supplier returned a null Throwable."); + } catch (Throwable ex1) { + Exceptions.throwIfFatal(ex1); + ex = ex1; + } + + observer.onError(ex); + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeFilter.java b/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeFilter.java new file mode 100644 index 0000000000..833edb75f3 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeFilter.java @@ -0,0 +1,105 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.maybe; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.Predicate; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; + +/** + * Filters the upstream via a predicate, returning the success item or completing if + * the predicate returns false. + * + * @param <T> the upstream value type + */ +public final class MaybeFilter<T> extends AbstractMaybeWithUpstream<T, T> { + + final Predicate<? super T> predicate; + + public MaybeFilter(MaybeSource<T> source, Predicate<? super T> predicate) { + super(source); + this.predicate = predicate; + } + + @Override + protected void subscribeActual(MaybeObserver<? super T> observer) { + source.subscribe(new FilterMaybeObserver<>(observer, predicate)); + } + + static final class FilterMaybeObserver<T> implements MaybeObserver<T>, Disposable { + + final MaybeObserver<? super T> downstream; + + final Predicate<? super T> predicate; + + Disposable upstream; + + FilterMaybeObserver(MaybeObserver<? super T> actual, Predicate<? super T> predicate) { + this.downstream = actual; + this.predicate = predicate; + } + + @Override + public void dispose() { + Disposable d = this.upstream; + this.upstream = DisposableHelper.DISPOSED; + d.dispose(); + } + + @Override + public boolean isDisposed() { + return upstream.isDisposed(); + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.validate(this.upstream, d)) { + this.upstream = d; + + downstream.onSubscribe(this); + } + } + + @Override + public void onSuccess(T value) { + boolean b; + + try { + b = predicate.test(value); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + downstream.onError(ex); + return; + } + + if (b) { + downstream.onSuccess(value); + } else { + downstream.onComplete(); + } + } + + @Override + public void onError(Throwable e) { + downstream.onError(e); + } + + @Override + public void onComplete() { + downstream.onComplete(); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeFilterSingle.java b/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeFilterSingle.java new file mode 100644 index 0000000000..aa3cae5e5c --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeFilterSingle.java @@ -0,0 +1,102 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.maybe; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.Predicate; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; + +/** + * Filters the upstream SingleSource via a predicate, returning the success item or completing if + * the predicate returns false. + * + * @param <T> the upstream value type + */ +public final class MaybeFilterSingle<T> extends Maybe<T> { + final SingleSource<T> source; + + final Predicate<? super T> predicate; + + public MaybeFilterSingle(SingleSource<T> source, Predicate<? super T> predicate) { + this.source = source; + this.predicate = predicate; + } + + @Override + protected void subscribeActual(MaybeObserver<? super T> observer) { + source.subscribe(new FilterMaybeObserver<>(observer, predicate)); + } + + static final class FilterMaybeObserver<T> implements SingleObserver<T>, Disposable { + + final MaybeObserver<? super T> downstream; + + final Predicate<? super T> predicate; + + Disposable upstream; + + FilterMaybeObserver(MaybeObserver<? super T> actual, Predicate<? super T> predicate) { + this.downstream = actual; + this.predicate = predicate; + } + + @Override + public void dispose() { + Disposable d = this.upstream; + this.upstream = DisposableHelper.DISPOSED; + d.dispose(); + } + + @Override + public boolean isDisposed() { + return upstream.isDisposed(); + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.validate(this.upstream, d)) { + this.upstream = d; + + downstream.onSubscribe(this); + } + } + + @Override + public void onSuccess(T value) { + boolean b; + + try { + b = predicate.test(value); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + downstream.onError(ex); + return; + } + + if (b) { + downstream.onSuccess(value); + } else { + downstream.onComplete(); + } + } + + @Override + public void onError(Throwable e) { + downstream.onError(e); + } + } + +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeFlatMapBiSelector.java b/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeFlatMapBiSelector.java new file mode 100644 index 0000000000..c57caee8f7 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeFlatMapBiSelector.java @@ -0,0 +1,163 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.maybe; + +import java.util.Objects; +import java.util.concurrent.atomic.AtomicReference; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; + +/** + * Maps a source item to another MaybeSource then calls a BiFunction with the + * original item and the secondary item to generate the final result. + * + * @param <T> the main value type + * @param <U> the second value type + * @param <R> the result value type + */ +public final class MaybeFlatMapBiSelector<T, U, R> extends AbstractMaybeWithUpstream<T, R> { + + final Function<? super T, ? extends MaybeSource<? extends U>> mapper; + + final BiFunction<? super T, ? super U, ? extends R> resultSelector; + + public MaybeFlatMapBiSelector(MaybeSource<T> source, + Function<? super T, ? extends MaybeSource<? extends U>> mapper, + BiFunction<? super T, ? super U, ? extends R> resultSelector) { + super(source); + this.mapper = mapper; + this.resultSelector = resultSelector; + } + + @Override + protected void subscribeActual(MaybeObserver<? super R> observer) { + source.subscribe(new FlatMapBiMainObserver<T, U, R>(observer, mapper, resultSelector)); + } + + static final class FlatMapBiMainObserver<T, U, R> + implements MaybeObserver<T>, Disposable { + + final Function<? super T, ? extends MaybeSource<? extends U>> mapper; + + final InnerObserver<T, U, R> inner; + + FlatMapBiMainObserver(MaybeObserver<? super R> actual, + Function<? super T, ? extends MaybeSource<? extends U>> mapper, + BiFunction<? super T, ? super U, ? extends R> resultSelector) { + this.inner = new InnerObserver<>(actual, resultSelector); + this.mapper = mapper; + } + + @Override + public void dispose() { + DisposableHelper.dispose(inner); + } + + @Override + public boolean isDisposed() { + return DisposableHelper.isDisposed(inner.get()); + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.setOnce(inner, d)) { + inner.downstream.onSubscribe(this); + } + } + + @Override + public void onSuccess(T value) { + MaybeSource<? extends U> next; + + try { + next = Objects.requireNonNull(mapper.apply(value), "The mapper returned a null MaybeSource"); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + inner.downstream.onError(ex); + return; + } + + if (DisposableHelper.replace(inner, null)) { + inner.value = value; + next.subscribe(inner); + } + } + + @Override + public void onError(Throwable e) { + inner.downstream.onError(e); + } + + @Override + public void onComplete() { + inner.downstream.onComplete(); + } + + static final class InnerObserver<T, U, R> + extends AtomicReference<Disposable> + implements MaybeObserver<U> { + + private static final long serialVersionUID = -2897979525538174559L; + + final MaybeObserver<? super R> downstream; + + final BiFunction<? super T, ? super U, ? extends R> resultSelector; + + T value; + + InnerObserver(MaybeObserver<? super R> actual, + BiFunction<? super T, ? super U, ? extends R> resultSelector) { + this.downstream = actual; + this.resultSelector = resultSelector; + } + + @Override + public void onSubscribe(Disposable d) { + DisposableHelper.setOnce(this, d); + } + + @Override + public void onSuccess(U value) { + T t = this.value; + this.value = null; + + R r; + + try { + r = Objects.requireNonNull(resultSelector.apply(t, value), "The resultSelector returned a null value"); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + downstream.onError(ex); + return; + } + + downstream.onSuccess(r); + } + + @Override + public void onError(Throwable e) { + downstream.onError(e); + } + + @Override + public void onComplete() { + downstream.onComplete(); + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeFlatMapCompletable.java b/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeFlatMapCompletable.java new file mode 100644 index 0000000000..1611fddfff --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeFlatMapCompletable.java @@ -0,0 +1,105 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.maybe; + +import java.util.Objects; +import java.util.concurrent.atomic.AtomicReference; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; + +/** + * Maps the success value of the source MaybeSource into a Completable. + * @param <T> the value type of the source MaybeSource + */ +public final class MaybeFlatMapCompletable<T> extends Completable { + + final MaybeSource<T> source; + + final Function<? super T, ? extends CompletableSource> mapper; + + public MaybeFlatMapCompletable(MaybeSource<T> source, Function<? super T, ? extends CompletableSource> mapper) { + this.source = source; + this.mapper = mapper; + } + + @Override + protected void subscribeActual(CompletableObserver observer) { + FlatMapCompletableObserver<T> parent = new FlatMapCompletableObserver<>(observer, mapper); + observer.onSubscribe(parent); + source.subscribe(parent); + } + + static final class FlatMapCompletableObserver<T> + extends AtomicReference<Disposable> + implements MaybeObserver<T>, CompletableObserver, Disposable { + + private static final long serialVersionUID = -2177128922851101253L; + + final CompletableObserver downstream; + + final Function<? super T, ? extends CompletableSource> mapper; + + FlatMapCompletableObserver(CompletableObserver actual, + Function<? super T, ? extends CompletableSource> mapper) { + this.downstream = actual; + this.mapper = mapper; + } + + @Override + public void dispose() { + DisposableHelper.dispose(this); + } + + @Override + public boolean isDisposed() { + return DisposableHelper.isDisposed(get()); + } + + @Override + public void onSubscribe(Disposable d) { + DisposableHelper.replace(this, d); + } + + @Override + public void onSuccess(T value) { + CompletableSource cs; + + try { + cs = Objects.requireNonNull(mapper.apply(value), "The mapper returned a null CompletableSource"); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + onError(ex); + return; + } + + if (!isDisposed()) { + cs.subscribe(this); + } + } + + @Override + public void onError(Throwable e) { + downstream.onError(e); + } + + @Override + public void onComplete() { + downstream.onComplete(); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeFlatMapIterableFlowable.java b/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeFlatMapIterableFlowable.java new file mode 100644 index 0000000000..322a73177f --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeFlatMapIterableFlowable.java @@ -0,0 +1,296 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.maybe; + +import java.util.Iterator; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicLong; + +import org.reactivestreams.Subscriber; + +import io.reactivex.rxjava3.annotations.Nullable; +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; +import io.reactivex.rxjava3.internal.subscriptions.*; +import io.reactivex.rxjava3.internal.util.BackpressureHelper; + +/** + * Maps a success value into an Iterable and streams it back as a Flowable. + * + * @param <T> the source value type + * @param <R> the element type of the Iterable + */ +public final class MaybeFlatMapIterableFlowable<T, R> extends Flowable<R> { + + final MaybeSource<T> source; + + final Function<? super T, ? extends Iterable<? extends R>> mapper; + + public MaybeFlatMapIterableFlowable(MaybeSource<T> source, + Function<? super T, ? extends Iterable<? extends R>> mapper) { + this.source = source; + this.mapper = mapper; + } + + @Override + protected void subscribeActual(Subscriber<? super R> s) { + source.subscribe(new FlatMapIterableObserver<>(s, mapper)); + } + + static final class FlatMapIterableObserver<T, R> + extends BasicIntQueueSubscription<R> + implements MaybeObserver<T> { + + private static final long serialVersionUID = -8938804753851907758L; + + final Subscriber<? super R> downstream; + + final Function<? super T, ? extends Iterable<? extends R>> mapper; + + final AtomicLong requested; + + Disposable upstream; + + volatile Iterator<? extends R> it; + + volatile boolean cancelled; + + boolean outputFused; + + FlatMapIterableObserver(Subscriber<? super R> actual, + Function<? super T, ? extends Iterable<? extends R>> mapper) { + this.downstream = actual; + this.mapper = mapper; + this.requested = new AtomicLong(); + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.validate(this.upstream, d)) { + this.upstream = d; + + downstream.onSubscribe(this); + } + } + + @Override + public void onSuccess(T value) { + Iterator<? extends R> iterator; + boolean has; + try { + iterator = mapper.apply(value).iterator(); + + has = iterator.hasNext(); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + downstream.onError(ex); + return; + } + + if (!has) { + downstream.onComplete(); + return; + } + + this.it = iterator; + drain(); + } + + @Override + public void onError(Throwable e) { + upstream = DisposableHelper.DISPOSED; + downstream.onError(e); + } + + @Override + public void onComplete() { + downstream.onComplete(); + } + + @Override + public void request(long n) { + if (SubscriptionHelper.validate(n)) { + BackpressureHelper.add(requested, n); + drain(); + } + } + + @Override + public void cancel() { + cancelled = true; + upstream.dispose(); + upstream = DisposableHelper.DISPOSED; + } + + void fastPath(Subscriber<? super R> a, Iterator<? extends R> iterator) { + for (;;) { + if (cancelled) { + return; + } + + R v; + + try { + v = iterator.next(); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + a.onError(ex); + return; + } + + a.onNext(v); + + if (cancelled) { + return; + } + + boolean b; + + try { + b = iterator.hasNext(); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + a.onError(ex); + return; + } + + if (!b) { + a.onComplete(); + return; + } + } + } + + void drain() { + if (getAndIncrement() != 0) { + return; + } + + Subscriber<? super R> a = downstream; + Iterator<? extends R> iterator = this.it; + + if (outputFused && iterator != null) { + a.onNext(null); + a.onComplete(); + return; + } + + int missed = 1; + + for (;;) { + + if (iterator != null) { + long r = requested.get(); + + if (r == Long.MAX_VALUE) { + fastPath(a, iterator); + return; + } + + long e = 0L; + + while (e != r) { + if (cancelled) { + return; + } + + R v; + + try { + v = Objects.requireNonNull(iterator.next(), "The iterator returned a null value"); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + a.onError(ex); + return; + } + + a.onNext(v); + + if (cancelled) { + return; + } + + e++; + + boolean b; + + try { + b = iterator.hasNext(); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + a.onError(ex); + return; + } + + if (!b) { + a.onComplete(); + return; + } + } + + if (e != 0L) { + BackpressureHelper.produced(requested, e); + } + } + + missed = addAndGet(-missed); + if (missed == 0) { + break; + } + + if (iterator == null) { + iterator = it; + } + } + } + + @Override + public int requestFusion(int mode) { + if ((mode & ASYNC) != 0) { + outputFused = true; + return ASYNC; + } + return NONE; + } + + @Override + public void clear() { + it = null; + } + + @Override + public boolean isEmpty() { + return it == null; + } + + @Nullable + @Override + public R poll() { + Iterator<? extends R> iterator = it; + + if (iterator != null) { + R v = Objects.requireNonNull(iterator.next(), "The iterator returned a null value"); + if (!iterator.hasNext()) { + it = null; + } + return v; + } + return null; + } + + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeFlatMapIterableObservable.java b/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeFlatMapIterableObservable.java new file mode 100644 index 0000000000..2018274b1a --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeFlatMapIterableObservable.java @@ -0,0 +1,206 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.maybe; + +import java.util.Iterator; +import java.util.Objects; + +import io.reactivex.rxjava3.annotations.Nullable; +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; +import io.reactivex.rxjava3.internal.observers.BasicQueueDisposable; + +/** + * Maps a success value into an Iterable and streams it back as a Flowable. + * + * @param <T> the source value type + * @param <R> the element type of the Iterable + */ +public final class MaybeFlatMapIterableObservable<T, R> extends Observable<R> { + + final MaybeSource<T> source; + + final Function<? super T, ? extends Iterable<? extends R>> mapper; + + public MaybeFlatMapIterableObservable(MaybeSource<T> source, + Function<? super T, ? extends Iterable<? extends R>> mapper) { + this.source = source; + this.mapper = mapper; + } + + @Override + protected void subscribeActual(Observer<? super R> observer) { + source.subscribe(new FlatMapIterableObserver<>(observer, mapper)); + } + + static final class FlatMapIterableObserver<T, R> + extends BasicQueueDisposable<R> + implements MaybeObserver<T> { + + final Observer<? super R> downstream; + + final Function<? super T, ? extends Iterable<? extends R>> mapper; + + Disposable upstream; + + volatile Iterator<? extends R> it; + + volatile boolean cancelled; + + boolean outputFused; + + FlatMapIterableObserver(Observer<? super R> actual, + Function<? super T, ? extends Iterable<? extends R>> mapper) { + this.downstream = actual; + this.mapper = mapper; + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.validate(this.upstream, d)) { + this.upstream = d; + + downstream.onSubscribe(this); + } + } + + @Override + public void onSuccess(T value) { + Observer<? super R> a = downstream; + + Iterator<? extends R> iterator; + boolean has; + try { + iterator = mapper.apply(value).iterator(); + + has = iterator.hasNext(); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + a.onError(ex); + return; + } + + if (!has) { + a.onComplete(); + return; + } + + this.it = iterator; + + if (outputFused) { + a.onNext(null); + a.onComplete(); + return; + } + + for (;;) { + if (cancelled) { + return; + } + + R v; + + try { + v = iterator.next(); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + a.onError(ex); + return; + } + + a.onNext(v); + + if (cancelled) { + return; + } + + boolean b; + + try { + b = iterator.hasNext(); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + a.onError(ex); + return; + } + + if (!b) { + a.onComplete(); + return; + } + } + } + + @Override + public void onError(Throwable e) { + upstream = DisposableHelper.DISPOSED; + downstream.onError(e); + } + + @Override + public void onComplete() { + downstream.onComplete(); + } + + @Override + public void dispose() { + cancelled = true; + upstream.dispose(); + upstream = DisposableHelper.DISPOSED; + } + + @Override + public boolean isDisposed() { + return cancelled; + } + + @Override + public int requestFusion(int mode) { + if ((mode & ASYNC) != 0) { + outputFused = true; + return ASYNC; + } + return NONE; + } + + @Override + public void clear() { + it = null; + } + + @Override + public boolean isEmpty() { + return it == null; + } + + @Nullable + @Override + public R poll() { + Iterator<? extends R> iterator = it; + + if (iterator != null) { + R v = Objects.requireNonNull(iterator.next(), "The iterator returned a null value"); + if (!iterator.hasNext()) { + it = null; + } + return v; + } + return null; + } + + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeFlatMapNotification.java b/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeFlatMapNotification.java new file mode 100644 index 0000000000..d91bd46da6 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeFlatMapNotification.java @@ -0,0 +1,174 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.maybe; + +import java.util.Objects; +import java.util.concurrent.atomic.AtomicReference; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; + +/** + * Maps a value into a MaybeSource and relays its signal. + * + * @param <T> the source value type + * @param <R> the result value type + */ +public final class MaybeFlatMapNotification<T, R> extends AbstractMaybeWithUpstream<T, R> { + + final Function<? super T, ? extends MaybeSource<? extends R>> onSuccessMapper; + + final Function<? super Throwable, ? extends MaybeSource<? extends R>> onErrorMapper; + + final Supplier<? extends MaybeSource<? extends R>> onCompleteSupplier; + + public MaybeFlatMapNotification(MaybeSource<T> source, + Function<? super T, ? extends MaybeSource<? extends R>> onSuccessMapper, + Function<? super Throwable, ? extends MaybeSource<? extends R>> onErrorMapper, + Supplier<? extends MaybeSource<? extends R>> onCompleteSupplier) { + super(source); + this.onSuccessMapper = onSuccessMapper; + this.onErrorMapper = onErrorMapper; + this.onCompleteSupplier = onCompleteSupplier; + } + + @Override + protected void subscribeActual(MaybeObserver<? super R> observer) { + source.subscribe(new FlatMapMaybeObserver<>(observer, onSuccessMapper, onErrorMapper, onCompleteSupplier)); + } + + static final class FlatMapMaybeObserver<T, R> + extends AtomicReference<Disposable> + implements MaybeObserver<T>, Disposable { + + private static final long serialVersionUID = 4375739915521278546L; + + final MaybeObserver<? super R> downstream; + + final Function<? super T, ? extends MaybeSource<? extends R>> onSuccessMapper; + + final Function<? super Throwable, ? extends MaybeSource<? extends R>> onErrorMapper; + + final Supplier<? extends MaybeSource<? extends R>> onCompleteSupplier; + + Disposable upstream; + + FlatMapMaybeObserver(MaybeObserver<? super R> actual, + Function<? super T, ? extends MaybeSource<? extends R>> onSuccessMapper, + Function<? super Throwable, ? extends MaybeSource<? extends R>> onErrorMapper, + Supplier<? extends MaybeSource<? extends R>> onCompleteSupplier) { + this.downstream = actual; + this.onSuccessMapper = onSuccessMapper; + this.onErrorMapper = onErrorMapper; + this.onCompleteSupplier = onCompleteSupplier; + } + + @Override + public void dispose() { + DisposableHelper.dispose(this); + upstream.dispose(); + } + + @Override + public boolean isDisposed() { + return DisposableHelper.isDisposed(get()); + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.validate(this.upstream, d)) { + this.upstream = d; + + downstream.onSubscribe(this); + } + } + + @Override + public void onSuccess(T value) { + MaybeSource<? extends R> source; + + try { + source = Objects.requireNonNull(onSuccessMapper.apply(value), "The onSuccessMapper returned a null MaybeSource"); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + downstream.onError(ex); + return; + } + + if (!isDisposed()) { + source.subscribe(new InnerObserver()); + } + } + + @Override + public void onError(Throwable e) { + MaybeSource<? extends R> source; + + try { + source = Objects.requireNonNull(onErrorMapper.apply(e), "The onErrorMapper returned a null MaybeSource"); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + downstream.onError(new CompositeException(e, ex)); + return; + } + + if (!isDisposed()) { + source.subscribe(new InnerObserver()); + } + } + + @Override + public void onComplete() { + MaybeSource<? extends R> source; + + try { + source = Objects.requireNonNull(onCompleteSupplier.get(), "The onCompleteSupplier returned a null MaybeSource"); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + downstream.onError(ex); + return; + } + + if (!isDisposed()) { + source.subscribe(new InnerObserver()); + } + } + + final class InnerObserver implements MaybeObserver<R> { + + @Override + public void onSubscribe(Disposable d) { + DisposableHelper.setOnce(FlatMapMaybeObserver.this, d); + } + + @Override + public void onSuccess(R value) { + downstream.onSuccess(value); + } + + @Override + public void onError(Throwable e) { + downstream.onError(e); + } + + @Override + public void onComplete() { + downstream.onComplete(); + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeFlatMapSingle.java b/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeFlatMapSingle.java new file mode 100644 index 0000000000..e88dd6fc3d --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeFlatMapSingle.java @@ -0,0 +1,134 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.maybe; + +import java.util.Objects; +import java.util.concurrent.atomic.AtomicReference; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; + +/** + * Maps the success value of the source MaybeSource into a Single. + * <p>History: 2.0.2 - experimental + * @param <T> the input value type + * @param <R> the result value type + * @since 2.1 + */ +public final class MaybeFlatMapSingle<T, R> extends Maybe<R> { + + final MaybeSource<T> source; + + final Function<? super T, ? extends SingleSource<? extends R>> mapper; + + public MaybeFlatMapSingle(MaybeSource<T> source, Function<? super T, ? extends SingleSource<? extends R>> mapper) { + this.source = source; + this.mapper = mapper; + } + + @Override + protected void subscribeActual(MaybeObserver<? super R> downstream) { + source.subscribe(new FlatMapMaybeObserver<>(downstream, mapper)); + } + + static final class FlatMapMaybeObserver<T, R> + extends AtomicReference<Disposable> + implements MaybeObserver<T>, Disposable { + + private static final long serialVersionUID = 4827726964688405508L; + + final MaybeObserver<? super R> downstream; + + final Function<? super T, ? extends SingleSource<? extends R>> mapper; + + FlatMapMaybeObserver(MaybeObserver<? super R> actual, Function<? super T, ? extends SingleSource<? extends R>> mapper) { + this.downstream = actual; + this.mapper = mapper; + } + + @Override + public void dispose() { + DisposableHelper.dispose(this); + } + + @Override + public boolean isDisposed() { + return DisposableHelper.isDisposed(get()); + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.setOnce(this, d)) { + downstream.onSubscribe(this); + } + } + + @Override + public void onSuccess(T value) { + SingleSource<? extends R> ss; + + try { + ss = Objects.requireNonNull(mapper.apply(value), "The mapper returned a null SingleSource"); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + onError(ex); + return; + } + + if (!isDisposed()) { + ss.subscribe(new FlatMapSingleObserver<R>(this, downstream)); + } + } + + @Override + public void onError(Throwable e) { + downstream.onError(e); + } + + @Override + public void onComplete() { + downstream.onComplete(); + } + } + + static final class FlatMapSingleObserver<R> implements SingleObserver<R> { + + final AtomicReference<Disposable> parent; + + final MaybeObserver<? super R> downstream; + + FlatMapSingleObserver(AtomicReference<Disposable> parent, MaybeObserver<? super R> downstream) { + this.parent = parent; + this.downstream = downstream; + } + + @Override + public void onSubscribe(final Disposable d) { + DisposableHelper.replace(parent, d); + } + + @Override + public void onSuccess(final R value) { + downstream.onSuccess(value); + } + + @Override + public void onError(final Throwable e) { + downstream.onError(e); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeFlatten.java b/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeFlatten.java new file mode 100644 index 0000000000..485cf69ea1 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeFlatten.java @@ -0,0 +1,133 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.maybe; + +import java.util.Objects; +import java.util.concurrent.atomic.AtomicReference; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; + +/** + * Maps a value into a MaybeSource and relays its signal. + * + * @param <T> the source value type + * @param <R> the result value type + */ +public final class MaybeFlatten<T, R> extends AbstractMaybeWithUpstream<T, R> { + + final Function<? super T, ? extends MaybeSource<? extends R>> mapper; + + public MaybeFlatten(MaybeSource<T> source, Function<? super T, ? extends MaybeSource<? extends R>> mapper) { + super(source); + this.mapper = mapper; + } + + @Override + protected void subscribeActual(MaybeObserver<? super R> observer) { + source.subscribe(new FlatMapMaybeObserver<>(observer, mapper)); + } + + static final class FlatMapMaybeObserver<T, R> + extends AtomicReference<Disposable> + implements MaybeObserver<T>, Disposable { + + private static final long serialVersionUID = 4375739915521278546L; + + final MaybeObserver<? super R> downstream; + + final Function<? super T, ? extends MaybeSource<? extends R>> mapper; + + Disposable upstream; + + FlatMapMaybeObserver(MaybeObserver<? super R> actual, + Function<? super T, ? extends MaybeSource<? extends R>> mapper) { + this.downstream = actual; + this.mapper = mapper; + } + + @Override + public void dispose() { + DisposableHelper.dispose(this); + upstream.dispose(); + } + + @Override + public boolean isDisposed() { + return DisposableHelper.isDisposed(get()); + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.validate(this.upstream, d)) { + this.upstream = d; + + downstream.onSubscribe(this); + } + } + + @Override + public void onSuccess(T value) { + MaybeSource<? extends R> source; + + try { + source = Objects.requireNonNull(mapper.apply(value), "The mapper returned a null MaybeSource"); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + downstream.onError(ex); + return; + } + + if (!isDisposed()) { + source.subscribe(new InnerObserver()); + } + } + + @Override + public void onError(Throwable e) { + downstream.onError(e); + } + + @Override + public void onComplete() { + downstream.onComplete(); + } + + final class InnerObserver implements MaybeObserver<R> { + + @Override + public void onSubscribe(Disposable d) { + DisposableHelper.setOnce(FlatMapMaybeObserver.this, d); + } + + @Override + public void onSuccess(R value) { + downstream.onSuccess(value); + } + + @Override + public void onError(Throwable e) { + downstream.onError(e); + } + + @Override + public void onComplete() { + downstream.onComplete(); + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeFromAction.java b/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeFromAction.java new file mode 100644 index 0000000000..3d9cf428cf --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeFromAction.java @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.maybe; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.*; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +/** + * Executes an Action and signals its exception or completes normally. + * + * @param <T> the value type + */ +public final class MaybeFromAction<T> extends Maybe<T> implements Supplier<T> { + + final Action action; + + public MaybeFromAction(Action action) { + this.action = action; + } + + @Override + protected void subscribeActual(MaybeObserver<? super T> observer) { + Disposable d = Disposable.empty(); + observer.onSubscribe(d); + + if (!d.isDisposed()) { + + try { + action.run(); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + if (!d.isDisposed()) { + observer.onError(ex); + } else { + RxJavaPlugins.onError(ex); + } + return; + } + + if (!d.isDisposed()) { + observer.onComplete(); + } + } + } + + @Override + public T get() throws Throwable { + action.run(); + return null; // considered as onComplete() + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeFromCallable.java b/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeFromCallable.java new file mode 100644 index 0000000000..592b02fad2 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeFromCallable.java @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.maybe; + +import java.util.concurrent.Callable; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.*; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.Supplier; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +/** + * Executes a callable and signals its value as success or signals an exception. + * + * @param <T> the value type + */ +public final class MaybeFromCallable<T> extends Maybe<T> implements Supplier<T> { + + final Callable<? extends T> callable; + + public MaybeFromCallable(Callable<? extends T> callable) { + this.callable = callable; + } + + @Override + protected void subscribeActual(MaybeObserver<? super T> observer) { + Disposable d = Disposable.empty(); + observer.onSubscribe(d); + + if (!d.isDisposed()) { + + T v; + + try { + v = callable.call(); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + if (!d.isDisposed()) { + observer.onError(ex); + } else { + RxJavaPlugins.onError(ex); + } + return; + } + + if (!d.isDisposed()) { + if (v == null) { + observer.onComplete(); + } else { + observer.onSuccess(v); + } + } + } + } + + @Override + public T get() throws Exception { + return callable.call(); + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeFromCompletable.java b/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeFromCompletable.java new file mode 100644 index 0000000000..f6ad406e35 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeFromCompletable.java @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.maybe; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; +import io.reactivex.rxjava3.internal.fuseable.HasUpstreamCompletableSource; + +/** + * Wrap a Completable into a Maybe. + * + * @param <T> the value type + */ +public final class MaybeFromCompletable<T> extends Maybe<T> implements HasUpstreamCompletableSource { + + final CompletableSource source; + + public MaybeFromCompletable(CompletableSource source) { + this.source = source; + } + + @Override + public CompletableSource source() { + return source; + } + + @Override + protected void subscribeActual(MaybeObserver<? super T> observer) { + source.subscribe(new FromCompletableObserver<T>(observer)); + } + + static final class FromCompletableObserver<T> implements CompletableObserver, Disposable { + final MaybeObserver<? super T> downstream; + + Disposable upstream; + + FromCompletableObserver(MaybeObserver<? super T> downstream) { + this.downstream = downstream; + } + + @Override + public void dispose() { + upstream.dispose(); + upstream = DisposableHelper.DISPOSED; + } + + @Override + public boolean isDisposed() { + return upstream.isDisposed(); + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.validate(this.upstream, d)) { + this.upstream = d; + + downstream.onSubscribe(this); + } + } + + @Override + public void onComplete() { + upstream = DisposableHelper.DISPOSED; + downstream.onComplete(); + } + + @Override + public void onError(Throwable e) { + upstream = DisposableHelper.DISPOSED; + downstream.onError(e); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeFromFuture.java b/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeFromFuture.java new file mode 100644 index 0000000000..e506a32265 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeFromFuture.java @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.maybe; + +import java.util.concurrent.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.*; +import io.reactivex.rxjava3.exceptions.Exceptions; + +/** + * Waits until the source Future completes or the wait times out; treats a {@code null} + * result as indication to signal {@code onComplete} instead of {@code onSuccess}. + * + * @param <T> the value type + */ +public final class MaybeFromFuture<T> extends Maybe<T> { + + final Future<? extends T> future; + + final long timeout; + + final TimeUnit unit; + + public MaybeFromFuture(Future<? extends T> future, long timeout, TimeUnit unit) { + this.future = future; + this.timeout = timeout; + this.unit = unit; + } + + @Override + protected void subscribeActual(MaybeObserver<? super T> observer) { + Disposable d = Disposable.empty(); + observer.onSubscribe(d); + if (!d.isDisposed()) { + T v; + try { + if (timeout <= 0L) { + v = future.get(); + } else { + v = future.get(timeout, unit); + } + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + if (ex instanceof ExecutionException) { + ex = ex.getCause(); + } + Exceptions.throwIfFatal(ex); + if (!d.isDisposed()) { + observer.onError(ex); + } + return; + } + if (!d.isDisposed()) { + if (v == null) { + observer.onComplete(); + } else { + observer.onSuccess(v); + } + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeFromRunnable.java b/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeFromRunnable.java new file mode 100644 index 0000000000..a102e4b945 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeFromRunnable.java @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.maybe; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.*; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.Supplier; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +/** + * Executes an Runnable and signals its exception or completes normally. + * + * @param <T> the value type + */ +public final class MaybeFromRunnable<T> extends Maybe<T> implements Supplier<T> { + + final Runnable runnable; + + public MaybeFromRunnable(Runnable runnable) { + this.runnable = runnable; + } + + @Override + protected void subscribeActual(MaybeObserver<? super T> observer) { + Disposable d = Disposable.empty(); + observer.onSubscribe(d); + + if (!d.isDisposed()) { + + try { + runnable.run(); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + if (!d.isDisposed()) { + observer.onError(ex); + } else { + RxJavaPlugins.onError(ex); + } + return; + } + + if (!d.isDisposed()) { + observer.onComplete(); + } + } + } + + @Override + public T get() { + runnable.run(); + return null; + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeFromSingle.java b/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeFromSingle.java new file mode 100644 index 0000000000..8fe691b767 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeFromSingle.java @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.maybe; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; +import io.reactivex.rxjava3.internal.fuseable.HasUpstreamSingleSource; + +/** + * Wrap a Single into a Maybe. + * + * @param <T> the value type + */ +public final class MaybeFromSingle<T> extends Maybe<T> implements HasUpstreamSingleSource<T> { + + final SingleSource<T> source; + + public MaybeFromSingle(SingleSource<T> source) { + this.source = source; + } + + @Override + public SingleSource<T> source() { + return source; + } + + @Override + protected void subscribeActual(MaybeObserver<? super T> observer) { + source.subscribe(new FromSingleObserver<>(observer)); + } + + static final class FromSingleObserver<T> implements SingleObserver<T>, Disposable { + final MaybeObserver<? super T> downstream; + + Disposable upstream; + + FromSingleObserver(MaybeObserver<? super T> downstream) { + this.downstream = downstream; + } + + @Override + public void dispose() { + upstream.dispose(); + upstream = DisposableHelper.DISPOSED; + } + + @Override + public boolean isDisposed() { + return upstream.isDisposed(); + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.validate(this.upstream, d)) { + this.upstream = d; + + downstream.onSubscribe(this); + } + } + + @Override + public void onSuccess(T value) { + upstream = DisposableHelper.DISPOSED; + downstream.onSuccess(value); + } + + @Override + public void onError(Throwable e) { + upstream = DisposableHelper.DISPOSED; + downstream.onError(e); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeFromSupplier.java b/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeFromSupplier.java new file mode 100644 index 0000000000..6eaf31ee64 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeFromSupplier.java @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.maybe; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.*; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.Supplier; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +/** + * Executes a supplier and signals its value as success or signals an exception. + * + * @param <T> the value type + * @since 3.0.0 + */ +public final class MaybeFromSupplier<T> extends Maybe<T> implements Supplier<T> { + + final Supplier<? extends T> supplier; + + public MaybeFromSupplier(Supplier<? extends T> supplier) { + this.supplier = supplier; + } + + @Override + protected void subscribeActual(MaybeObserver<? super T> observer) { + Disposable d = Disposable.empty(); + observer.onSubscribe(d); + + if (!d.isDisposed()) { + + T v; + + try { + v = supplier.get(); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + if (!d.isDisposed()) { + observer.onError(ex); + } else { + RxJavaPlugins.onError(ex); + } + return; + } + + if (!d.isDisposed()) { + if (v == null) { + observer.onComplete(); + } else { + observer.onSuccess(v); + } + } + } + } + + @Override + public T get() throws Throwable { + return supplier.get(); + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeHide.java b/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeHide.java new file mode 100644 index 0000000000..2f87b5fddd --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeHide.java @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.maybe; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; + +/** + * Hides the identity of the upstream Maybe and its Disposable sent through onSubscribe. + * + * @param <T> the value type + */ +public final class MaybeHide<T> extends AbstractMaybeWithUpstream<T, T> { + + public MaybeHide(MaybeSource<T> source) { + super(source); + } + + @Override + protected void subscribeActual(MaybeObserver<? super T> observer) { + source.subscribe(new HideMaybeObserver<>(observer)); + } + + static final class HideMaybeObserver<T> implements MaybeObserver<T>, Disposable { + + final MaybeObserver<? super T> downstream; + + Disposable upstream; + + HideMaybeObserver(MaybeObserver<? super T> downstream) { + this.downstream = downstream; + } + + @Override + public void dispose() { + upstream.dispose(); + upstream = DisposableHelper.DISPOSED; + } + + @Override + public boolean isDisposed() { + return upstream.isDisposed(); + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.validate(this.upstream, d)) { + this.upstream = d; + + downstream.onSubscribe(this); + } + } + + @Override + public void onSuccess(T value) { + downstream.onSuccess(value); + } + + @Override + public void onError(Throwable e) { + downstream.onError(e); + } + + @Override + public void onComplete() { + downstream.onComplete(); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeIgnoreElement.java b/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeIgnoreElement.java new file mode 100644 index 0000000000..dcdbd2cd73 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeIgnoreElement.java @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.maybe; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; + +/** + * Turns an onSuccess into an onComplete, onError and onComplete is relayed as is. + * + * @param <T> the value type + */ +public final class MaybeIgnoreElement<T> extends AbstractMaybeWithUpstream<T, T> { + + public MaybeIgnoreElement(MaybeSource<T> source) { + super(source); + } + + @Override + protected void subscribeActual(MaybeObserver<? super T> observer) { + source.subscribe(new IgnoreMaybeObserver<>(observer)); + } + + static final class IgnoreMaybeObserver<T> implements MaybeObserver<T>, Disposable { + + final MaybeObserver<? super T> downstream; + + Disposable upstream; + + IgnoreMaybeObserver(MaybeObserver<? super T> downstream) { + this.downstream = downstream; + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.validate(this.upstream, d)) { + this.upstream = d; + + downstream.onSubscribe(this); + } + } + + @Override + public void onSuccess(T value) { + upstream = DisposableHelper.DISPOSED; + downstream.onComplete(); + } + + @Override + public void onError(Throwable e) { + upstream = DisposableHelper.DISPOSED; + downstream.onError(e); + } + + @Override + public void onComplete() { + upstream = DisposableHelper.DISPOSED; + downstream.onComplete(); + } + + @Override + public boolean isDisposed() { + return upstream.isDisposed(); + } + + @Override + public void dispose() { + upstream.dispose(); + upstream = DisposableHelper.DISPOSED; + } + + } + +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeIgnoreElementCompletable.java b/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeIgnoreElementCompletable.java new file mode 100644 index 0000000000..42f40c92f1 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeIgnoreElementCompletable.java @@ -0,0 +1,95 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.maybe; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; +import io.reactivex.rxjava3.internal.fuseable.FuseToMaybe; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +/** + * Turns an onSuccess into an onComplete, onError and onComplete is relayed as is. + * + * @param <T> the value type + */ +public final class MaybeIgnoreElementCompletable<T> extends Completable implements FuseToMaybe<T> { + + final MaybeSource<T> source; + + public MaybeIgnoreElementCompletable(MaybeSource<T> source) { + this.source = source; + } + + @Override + protected void subscribeActual(CompletableObserver observer) { + source.subscribe(new IgnoreMaybeObserver<>(observer)); + } + + @Override + public Maybe<T> fuseToMaybe() { + return RxJavaPlugins.onAssembly(new MaybeIgnoreElement<>(source)); + } + + static final class IgnoreMaybeObserver<T> implements MaybeObserver<T>, Disposable { + + final CompletableObserver downstream; + + Disposable upstream; + + IgnoreMaybeObserver(CompletableObserver downstream) { + this.downstream = downstream; + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.validate(this.upstream, d)) { + this.upstream = d; + + downstream.onSubscribe(this); + } + } + + @Override + public void onSuccess(T value) { + upstream = DisposableHelper.DISPOSED; + downstream.onComplete(); + } + + @Override + public void onError(Throwable e) { + upstream = DisposableHelper.DISPOSED; + downstream.onError(e); + } + + @Override + public void onComplete() { + upstream = DisposableHelper.DISPOSED; + downstream.onComplete(); + } + + @Override + public boolean isDisposed() { + return upstream.isDisposed(); + } + + @Override + public void dispose() { + upstream.dispose(); + upstream = DisposableHelper.DISPOSED; + } + + } + +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeIsEmpty.java b/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeIsEmpty.java new file mode 100644 index 0000000000..a9642b92fe --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeIsEmpty.java @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.maybe; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; + +/** + * Signals true if the source Maybe signals onComplete, signals false if the source Maybe + * signals onSuccess. + * + * @param <T> the value type + */ +public final class MaybeIsEmpty<T> extends AbstractMaybeWithUpstream<T, Boolean> { + + public MaybeIsEmpty(MaybeSource<T> source) { + super(source); + } + + @Override + protected void subscribeActual(MaybeObserver<? super Boolean> observer) { + source.subscribe(new IsEmptyMaybeObserver<>(observer)); + } + + static final class IsEmptyMaybeObserver<T> + implements MaybeObserver<T>, Disposable { + + final MaybeObserver<? super Boolean> downstream; + + Disposable upstream; + + IsEmptyMaybeObserver(MaybeObserver<? super Boolean> downstream) { + this.downstream = downstream; + } + + @Override + public void dispose() { + upstream.dispose(); + } + + @Override + public boolean isDisposed() { + return upstream.isDisposed(); + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.validate(this.upstream, d)) { + this.upstream = d; + + downstream.onSubscribe(this); + } + } + + @Override + public void onSuccess(T value) { + downstream.onSuccess(false); + } + + @Override + public void onError(Throwable e) { + downstream.onError(e); + } + + @Override + public void onComplete() { + downstream.onSuccess(true); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeIsEmptySingle.java b/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeIsEmptySingle.java new file mode 100644 index 0000000000..b4795a2985 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeIsEmptySingle.java @@ -0,0 +1,101 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.maybe; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; +import io.reactivex.rxjava3.internal.fuseable.*; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +/** + * Signals true if the source Maybe signals onComplete, signals false if the source Maybe + * signals onSuccess. + * + * @param <T> the value type + */ +public final class MaybeIsEmptySingle<T> extends Single<Boolean> +implements HasUpstreamMaybeSource<T>, FuseToMaybe<Boolean> { + + final MaybeSource<T> source; + + public MaybeIsEmptySingle(MaybeSource<T> source) { + this.source = source; + } + + @Override + public MaybeSource<T> source() { + return source; + } + + @Override + public Maybe<Boolean> fuseToMaybe() { + return RxJavaPlugins.onAssembly(new MaybeIsEmpty<>(source)); + } + + @Override + protected void subscribeActual(SingleObserver<? super Boolean> observer) { + source.subscribe(new IsEmptyMaybeObserver<>(observer)); + } + + static final class IsEmptyMaybeObserver<T> + implements MaybeObserver<T>, Disposable { + + final SingleObserver<? super Boolean> downstream; + + Disposable upstream; + + IsEmptyMaybeObserver(SingleObserver<? super Boolean> downstream) { + this.downstream = downstream; + } + + @Override + public void dispose() { + upstream.dispose(); + upstream = DisposableHelper.DISPOSED; + } + + @Override + public boolean isDisposed() { + return upstream.isDisposed(); + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.validate(this.upstream, d)) { + this.upstream = d; + + downstream.onSubscribe(this); + } + } + + @Override + public void onSuccess(T value) { + upstream = DisposableHelper.DISPOSED; + downstream.onSuccess(false); + } + + @Override + public void onError(Throwable e) { + upstream = DisposableHelper.DISPOSED; + downstream.onError(e); + } + + @Override + public void onComplete() { + upstream = DisposableHelper.DISPOSED; + downstream.onSuccess(true); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeJust.java b/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeJust.java new file mode 100644 index 0000000000..7e6fc6dfa6 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeJust.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.maybe; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.operators.ScalarSupplier; + +/** + * Signals a constant value. + * + * @param <T> the value type + */ +public final class MaybeJust<T> extends Maybe<T> implements ScalarSupplier<T> { + + final T value; + + public MaybeJust(T value) { + this.value = value; + } + + @Override + protected void subscribeActual(MaybeObserver<? super T> observer) { + observer.onSubscribe(Disposable.disposed()); + observer.onSuccess(value); + } + + @Override + public T get() { + return value; + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeLift.java b/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeLift.java new file mode 100644 index 0000000000..a5a6d76877 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeLift.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.maybe; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.internal.disposables.EmptyDisposable; + +import java.util.Objects; + +/** + * Calls a MaybeOperator for the incoming MaybeObserver. + * + * @param <T> the upstream value type + * @param <R> the downstream value type + */ +public final class MaybeLift<T, R> extends AbstractMaybeWithUpstream<T, R> { + + final MaybeOperator<? extends R, ? super T> operator; + + public MaybeLift(MaybeSource<T> source, MaybeOperator<? extends R, ? super T> operator) { + super(source); + this.operator = operator; + } + + @Override + protected void subscribeActual(MaybeObserver<? super R> observer) { + MaybeObserver<? super T> lifted; + + try { + lifted = Objects.requireNonNull(operator.apply(observer), "The operator returned a null MaybeObserver"); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + EmptyDisposable.error(ex, observer); + return; + } + + source.subscribe(lifted); + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeMap.java b/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeMap.java new file mode 100644 index 0000000000..2d637fdd43 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeMap.java @@ -0,0 +1,103 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.maybe; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; + +import java.util.Objects; + +/** + * Maps the upstream success value into some other value. + * + * @param <T> the upstream value type + * @param <R> the downstream value type + */ +public final class MaybeMap<T, R> extends AbstractMaybeWithUpstream<T, R> { + + final Function<? super T, ? extends R> mapper; + + public MaybeMap(MaybeSource<T> source, Function<? super T, ? extends R> mapper) { + super(source); + this.mapper = mapper; + } + + @Override + protected void subscribeActual(MaybeObserver<? super R> observer) { + source.subscribe(new MapMaybeObserver<T, R>(observer, mapper)); + } + + static final class MapMaybeObserver<T, R> implements MaybeObserver<T>, Disposable { + + final MaybeObserver<? super R> downstream; + + final Function<? super T, ? extends R> mapper; + + Disposable upstream; + + MapMaybeObserver(MaybeObserver<? super R> actual, Function<? super T, ? extends R> mapper) { + this.downstream = actual; + this.mapper = mapper; + } + + @Override + public void dispose() { + Disposable d = this.upstream; + this.upstream = DisposableHelper.DISPOSED; + d.dispose(); + } + + @Override + public boolean isDisposed() { + return upstream.isDisposed(); + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.validate(this.upstream, d)) { + this.upstream = d; + + downstream.onSubscribe(this); + } + } + + @Override + public void onSuccess(T value) { + R v; + + try { + v = Objects.requireNonNull(mapper.apply(value), "The mapper returned a null item"); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + downstream.onError(ex); + return; + } + + downstream.onSuccess(v); + } + + @Override + public void onError(Throwable e) { + downstream.onError(e); + } + + @Override + public void onComplete() { + downstream.onComplete(); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeMaterialize.java b/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeMaterialize.java new file mode 100644 index 0000000000..022d92bbe8 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeMaterialize.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.maybe; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.internal.operators.mixed.MaterializeSingleObserver; + +/** + * Turn the signal types of a Maybe source into a single Notification of + * equal kind. + * <p>History: 2.2.4 - experimental + * + * @param <T> the element type of the source + * @since 3.0.0 + */ +public final class MaybeMaterialize<T> extends Single<Notification<T>> { + + final Maybe<T> source; + + public MaybeMaterialize(Maybe<T> source) { + this.source = source; + } + + @Override + protected void subscribeActual(SingleObserver<? super Notification<T>> observer) { + source.subscribe(new MaterializeSingleObserver<>(observer)); + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeMergeArray.java b/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeMergeArray.java new file mode 100644 index 0000000000..82f6d62ddf --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeMergeArray.java @@ -0,0 +1,450 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.maybe; + +import java.util.Objects; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.atomic.*; + +import org.reactivestreams.Subscriber; + +import io.reactivex.rxjava3.annotations.Nullable; +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.*; +import io.reactivex.rxjava3.internal.subscriptions.*; +import io.reactivex.rxjava3.internal.util.*; +import io.reactivex.rxjava3.operators.SimpleQueue; + +/** + * Run all MaybeSources of an array at once and signal their values as they become available. + * + * @param <T> the value type + */ +public final class MaybeMergeArray<T> extends Flowable<T> { + + final MaybeSource<? extends T>[] sources; + + public MaybeMergeArray(MaybeSource<? extends T>[] sources) { + this.sources = sources; + } + + @Override + protected void subscribeActual(Subscriber<? super T> s) { + MaybeSource<? extends T>[] maybes = sources; + int n = maybes.length; + + SimpleQueueWithConsumerIndex<Object> queue; + + if (n <= bufferSize()) { + queue = new MpscFillOnceSimpleQueue<>(n); + } else { + queue = new ClqSimpleQueue<>(); + } + MergeMaybeObserver<T> parent = new MergeMaybeObserver<>(s, n, queue); + + s.onSubscribe(parent); + + AtomicThrowable e = parent.errors; + + for (MaybeSource<? extends T> source : maybes) { + if (parent.isCancelled() || e.get() != null) { + return; + } + + source.subscribe(parent); + } + } + + static final class MergeMaybeObserver<T> + extends BasicIntQueueSubscription<T> implements MaybeObserver<T> { + + private static final long serialVersionUID = -660395290758764731L; + + final Subscriber<? super T> downstream; + + final CompositeDisposable set; + + final AtomicLong requested; + + final SimpleQueueWithConsumerIndex<Object> queue; + + final AtomicThrowable errors; + + final int sourceCount; + + volatile boolean cancelled; + + boolean outputFused; + + long consumed; + + MergeMaybeObserver(Subscriber<? super T> actual, int sourceCount, SimpleQueueWithConsumerIndex<Object> queue) { + this.downstream = actual; + this.sourceCount = sourceCount; + this.set = new CompositeDisposable(); + this.requested = new AtomicLong(); + this.errors = new AtomicThrowable(); + this.queue = queue; + } + + @Override + public int requestFusion(int mode) { + if ((mode & ASYNC) != 0) { + outputFused = true; + return ASYNC; + } + return NONE; + } + + @Nullable + @SuppressWarnings("unchecked") + @Override + public T poll() { + for (;;) { + Object o = queue.poll(); + if (o != NotificationLite.COMPLETE) { + return (T)o; + } + } + } + + @Override + public boolean isEmpty() { + return queue.isEmpty(); + } + + @Override + public void clear() { + queue.clear(); + } + + @Override + public void request(long n) { + if (SubscriptionHelper.validate(n)) { + BackpressureHelper.add(requested, n); + drain(); + } + } + + @Override + public void cancel() { + if (!cancelled) { + cancelled = true; + set.dispose(); + if (getAndIncrement() == 0) { + queue.clear(); + } + } + } + + @Override + public void onSubscribe(Disposable d) { + set.add(d); + } + + @Override + public void onSuccess(T value) { + queue.offer(value); + drain(); + } + + @Override + public void onError(Throwable e) { + if (errors.tryAddThrowableOrReport(e)) { + set.dispose(); + queue.offer(NotificationLite.COMPLETE); + drain(); + } + } + + @Override + public void onComplete() { + queue.offer(NotificationLite.COMPLETE); + drain(); + } + + boolean isCancelled() { + return cancelled; + } + + @SuppressWarnings("unchecked") + void drainNormal() { + int missed = 1; + Subscriber<? super T> a = downstream; + SimpleQueueWithConsumerIndex<Object> q = queue; + long e = consumed; + + for (;;) { + + long r = requested.get(); + + while (e != r) { + if (cancelled) { + q.clear(); + return; + } + + Throwable ex = errors.get(); + if (ex != null) { + q.clear(); + errors.tryTerminateConsumer(downstream); + return; + } + + if (q.consumerIndex() == sourceCount) { + a.onComplete(); + return; + } + + Object v = q.poll(); + + if (v == null) { + break; + } + + if (v != NotificationLite.COMPLETE) { + a.onNext((T)v); + + e++; + } + } + + if (e == r) { + Throwable ex = errors.get(); + if (ex != null) { + q.clear(); + errors.tryTerminateConsumer(downstream); + return; + } + + while (q.peek() == NotificationLite.COMPLETE) { + q.drop(); + } + + if (q.consumerIndex() == sourceCount) { + a.onComplete(); + return; + } + } + + consumed = e; + missed = addAndGet(-missed); + if (missed == 0) { + break; + } + } + + } + + void drainFused() { + int missed = 1; + Subscriber<? super T> a = downstream; + SimpleQueueWithConsumerIndex<Object> q = queue; + + for (;;) { + if (cancelled) { + q.clear(); + return; + } + Throwable ex = errors.get(); + if (ex != null) { + q.clear(); + a.onError(ex); + return; + } + + boolean d = q.producerIndex() == sourceCount; + + if (!q.isEmpty()) { + a.onNext(null); + } + + if (d) { + a.onComplete(); + return; + } + + missed = addAndGet(-missed); + if (missed == 0) { + break; + } + } + + } + + void drain() { + if (getAndIncrement() != 0) { + return; + } + + if (outputFused) { + drainFused(); + } else { + drainNormal(); + } + } + } + + interface SimpleQueueWithConsumerIndex<T> extends SimpleQueue<T> { + + @Nullable + @Override + T poll(); + + T peek(); + + void drop(); + + int consumerIndex(); + + int producerIndex(); + } + + static final class MpscFillOnceSimpleQueue<T> + extends AtomicReferenceArray<T> + implements SimpleQueueWithConsumerIndex<T> { + + private static final long serialVersionUID = -7969063454040569579L; + final AtomicInteger producerIndex; + + int consumerIndex; + + MpscFillOnceSimpleQueue(int length) { + super(length); + this.producerIndex = new AtomicInteger(); + } + + @Override + public boolean offer(T value) { + Objects.requireNonNull(value, "value is null"); + int idx = producerIndex.getAndIncrement(); + if (idx < length()) { + lazySet(idx, value); + return true; + } + return false; + } + + @Override + public boolean offer(T v1, T v2) { + throw new UnsupportedOperationException(); + } + + @Nullable + @Override + public T poll() { + int ci = consumerIndex; + if (ci == length()) { + return null; + } + AtomicInteger pi = producerIndex; + for (;;) { + T v = get(ci); + if (v != null) { + consumerIndex = ci + 1; + lazySet(ci, null); + return v; + } + if (pi.get() == ci) { + return null; + } + } + } + + @Override + public T peek() { + int ci = consumerIndex; + if (ci == length()) { + return null; + } + return get(ci); + } + + @Override + public void drop() { + int ci = consumerIndex; + lazySet(ci, null); + consumerIndex = ci + 1; + } + + @Override + public boolean isEmpty() { + return consumerIndex == producerIndex(); + } + + @Override + public void clear() { + while (poll() != null && !isEmpty()) { } + } + + @Override + public int consumerIndex() { + return consumerIndex; + } + + @Override + public int producerIndex() { + return producerIndex.get(); + } + } + + static final class ClqSimpleQueue<T> extends ConcurrentLinkedQueue<T> implements SimpleQueueWithConsumerIndex<T> { + + private static final long serialVersionUID = -4025173261791142821L; + + int consumerIndex; + + final AtomicInteger producerIndex; + + ClqSimpleQueue() { + this.producerIndex = new AtomicInteger(); + } + + @Override + public boolean offer(T v1, T v2) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean offer(T e) { + producerIndex.getAndIncrement(); + return super.offer(e); + } + + @Nullable + @Override + public T poll() { + T v = super.poll(); + if (v != null) { + consumerIndex++; + } + return v; + } + + @Override + public int consumerIndex() { + return consumerIndex; + } + + @Override + public int producerIndex() { + return producerIndex.get(); + } + + @Override + public void drop() { + poll(); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeNever.java b/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeNever.java new file mode 100644 index 0000000000..a9c35c9b67 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeNever.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.maybe; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.internal.disposables.EmptyDisposable; + +/** + * Doesn't signal any event other than onSubscribe. + */ +public final class MaybeNever extends Maybe<Object> { + + public static final MaybeNever INSTANCE = new MaybeNever(); + + @Override + protected void subscribeActual(MaybeObserver<? super Object> observer) { + observer.onSubscribe(EmptyDisposable.NEVER); + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeObserveOn.java b/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeObserveOn.java new file mode 100644 index 0000000000..5afb4beaf3 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeObserveOn.java @@ -0,0 +1,110 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.maybe; + +import java.util.concurrent.atomic.AtomicReference; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; + +/** + * Signals the onSuccess, onError or onComplete events on a the specific scheduler. + * + * @param <T> the value type delivered + */ +public final class MaybeObserveOn<T> extends AbstractMaybeWithUpstream<T, T> { + + final Scheduler scheduler; + + public MaybeObserveOn(MaybeSource<T> source, Scheduler scheduler) { + super(source); + this.scheduler = scheduler; + } + + @Override + protected void subscribeActual(MaybeObserver<? super T> observer) { + source.subscribe(new ObserveOnMaybeObserver<>(observer, scheduler)); + } + + static final class ObserveOnMaybeObserver<T> + extends AtomicReference<Disposable> + implements MaybeObserver<T>, Disposable, Runnable { + + private static final long serialVersionUID = 8571289934935992137L; + + final MaybeObserver<? super T> downstream; + + final Scheduler scheduler; + + T value; + Throwable error; + + ObserveOnMaybeObserver(MaybeObserver<? super T> actual, Scheduler scheduler) { + this.downstream = actual; + this.scheduler = scheduler; + } + + @Override + public void dispose() { + DisposableHelper.dispose(this); + } + + @Override + public boolean isDisposed() { + return DisposableHelper.isDisposed(get()); + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.setOnce(this, d)) { + downstream.onSubscribe(this); + } + } + + @Override + public void onSuccess(T value) { + this.value = value; + DisposableHelper.replace(this, scheduler.scheduleDirect(this)); + } + + @Override + public void onError(Throwable e) { + this.error = e; + DisposableHelper.replace(this, scheduler.scheduleDirect(this)); + } + + @Override + public void onComplete() { + DisposableHelper.replace(this, scheduler.scheduleDirect(this)); + } + + @Override + public void run() { + Throwable ex = error; + if (ex != null) { + error = null; + downstream.onError(ex); + } else { + T v = value; + if (v != null) { + value = null; + downstream.onSuccess(v); + } else { + downstream.onComplete(); + } + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeOnErrorComplete.java b/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeOnErrorComplete.java new file mode 100644 index 0000000000..46c15bcb2a --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeOnErrorComplete.java @@ -0,0 +1,105 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.maybe; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.Predicate; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; + +/** + * Emits an onComplete if the source emits an onError and the predicate returns true for + * that Throwable. + * + * @param <T> the value type + */ +public final class MaybeOnErrorComplete<T> extends AbstractMaybeWithUpstream<T, T> { + + final Predicate<? super Throwable> predicate; + + public MaybeOnErrorComplete(MaybeSource<T> source, + Predicate<? super Throwable> predicate) { + super(source); + this.predicate = predicate; + } + + @Override + protected void subscribeActual(MaybeObserver<? super T> observer) { + source.subscribe(new OnErrorCompleteMultiObserver<>(observer, predicate)); + } + + public static final class OnErrorCompleteMultiObserver<T> + implements MaybeObserver<T>, SingleObserver<T>, Disposable { + + final MaybeObserver<? super T> downstream; + + final Predicate<? super Throwable> predicate; + + Disposable upstream; + + public OnErrorCompleteMultiObserver(MaybeObserver<? super T> actual, Predicate<? super Throwable> predicate) { + this.downstream = actual; + this.predicate = predicate; + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.validate(this.upstream, d)) { + this.upstream = d; + + downstream.onSubscribe(this); + } + } + + @Override + public void onSuccess(T value) { + downstream.onSuccess(value); + } + + @Override + public void onError(Throwable e) { + boolean b; + + try { + b = predicate.test(e); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + downstream.onError(new CompositeException(e, ex)); + return; + } + + if (b) { + downstream.onComplete(); + } else { + downstream.onError(e); + } + } + + @Override + public void onComplete() { + downstream.onComplete(); + } + + @Override + public void dispose() { + upstream.dispose(); + } + + @Override + public boolean isDisposed() { + return upstream.isDisposed(); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeOnErrorNext.java b/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeOnErrorNext.java new file mode 100644 index 0000000000..a3e975570d --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeOnErrorNext.java @@ -0,0 +1,136 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.maybe; + +import java.util.Objects; +import java.util.concurrent.atomic.AtomicReference; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; + +/** + * Subscribes to the MaybeSource returned by a function if the main source signals an onError. + * + * @param <T> the value type + */ +public final class MaybeOnErrorNext<T> extends AbstractMaybeWithUpstream<T, T> { + + final Function<? super Throwable, ? extends MaybeSource<? extends T>> resumeFunction; + + public MaybeOnErrorNext(MaybeSource<T> source, + Function<? super Throwable, ? extends MaybeSource<? extends T>> resumeFunction) { + super(source); + this.resumeFunction = resumeFunction; + } + + @Override + protected void subscribeActual(MaybeObserver<? super T> observer) { + source.subscribe(new OnErrorNextMaybeObserver<>(observer, resumeFunction)); + } + + static final class OnErrorNextMaybeObserver<T> + extends AtomicReference<Disposable> + implements MaybeObserver<T>, Disposable { + + private static final long serialVersionUID = 2026620218879969836L; + + final MaybeObserver<? super T> downstream; + + final Function<? super Throwable, ? extends MaybeSource<? extends T>> resumeFunction; + + OnErrorNextMaybeObserver(MaybeObserver<? super T> actual, + Function<? super Throwable, ? extends MaybeSource<? extends T>> resumeFunction) { + this.downstream = actual; + this.resumeFunction = resumeFunction; + } + + @Override + public void dispose() { + DisposableHelper.dispose(this); + } + + @Override + public boolean isDisposed() { + return DisposableHelper.isDisposed(get()); + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.setOnce(this, d)) { + downstream.onSubscribe(this); + } + } + + @Override + public void onSuccess(T value) { + downstream.onSuccess(value); + } + + @Override + public void onError(Throwable e) { + MaybeSource<? extends T> m; + + try { + m = Objects.requireNonNull(resumeFunction.apply(e), "The resumeFunction returned a null MaybeSource"); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + downstream.onError(new CompositeException(e, ex)); + return; + } + + DisposableHelper.replace(this, null); + + m.subscribe(new NextMaybeObserver<T>(downstream, this)); + } + + @Override + public void onComplete() { + downstream.onComplete(); + } + + static final class NextMaybeObserver<T> implements MaybeObserver<T> { + final MaybeObserver<? super T> downstream; + + final AtomicReference<Disposable> upstream; + + NextMaybeObserver(MaybeObserver<? super T> actual, AtomicReference<Disposable> d) { + this.downstream = actual; + this.upstream = d; + } + + @Override + public void onSubscribe(Disposable d) { + DisposableHelper.setOnce(this.upstream, d); + } + + @Override + public void onSuccess(T value) { + downstream.onSuccess(value); + } + + @Override + public void onError(Throwable e) { + downstream.onError(e); + } + + @Override + public void onComplete() { + downstream.onComplete(); + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeOnErrorReturn.java b/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeOnErrorReturn.java new file mode 100644 index 0000000000..6d14b2c8cd --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeOnErrorReturn.java @@ -0,0 +1,101 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.maybe; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; + +import java.util.Objects; + +/** + * Returns a value generated via a function if the main source signals an onError. + * @param <T> the value type + */ +public final class MaybeOnErrorReturn<T> extends AbstractMaybeWithUpstream<T, T> { + + final Function<? super Throwable, ? extends T> itemSupplier; + + public MaybeOnErrorReturn(MaybeSource<T> source, + Function<? super Throwable, ? extends T> itemSupplier) { + super(source); + this.itemSupplier = itemSupplier; + } + + @Override + protected void subscribeActual(MaybeObserver<? super T> observer) { + source.subscribe(new OnErrorReturnMaybeObserver<>(observer, itemSupplier)); + } + + static final class OnErrorReturnMaybeObserver<T> implements MaybeObserver<T>, Disposable { + + final MaybeObserver<? super T> downstream; + + final Function<? super Throwable, ? extends T> itemSupplier; + + Disposable upstream; + + OnErrorReturnMaybeObserver(MaybeObserver<? super T> actual, + Function<? super Throwable, ? extends T> valueSupplier) { + this.downstream = actual; + this.itemSupplier = valueSupplier; + } + + @Override + public void dispose() { + upstream.dispose(); + } + + @Override + public boolean isDisposed() { + return upstream.isDisposed(); + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.validate(this.upstream, d)) { + this.upstream = d; + + downstream.onSubscribe(this); + } + } + + @Override + public void onSuccess(T value) { + downstream.onSuccess(value); + } + + @Override + public void onError(Throwable e) { + T v; + + try { + v = Objects.requireNonNull(itemSupplier.apply(e), "The itemSupplier returned a null value"); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + downstream.onError(new CompositeException(e, ex)); + return; + } + + downstream.onSuccess(v); + } + + @Override + public void onComplete() { + downstream.onComplete(); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybePeek.java b/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybePeek.java new file mode 100644 index 0000000000..58c69839a1 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybePeek.java @@ -0,0 +1,181 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.maybe; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.disposables.*; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +/** + * Peeks into the lifecycle of a Maybe and MaybeObserver. + * + * @param <T> the value type + */ +public final class MaybePeek<T> extends AbstractMaybeWithUpstream<T, T> { + + final Consumer<? super Disposable> onSubscribeCall; + + final Consumer<? super T> onSuccessCall; + + final Consumer<? super Throwable> onErrorCall; + + final Action onCompleteCall; + + final Action onAfterTerminate; + + final Action onDisposeCall; + + public MaybePeek(MaybeSource<T> source, Consumer<? super Disposable> onSubscribeCall, + Consumer<? super T> onSuccessCall, Consumer<? super Throwable> onErrorCall, Action onCompleteCall, + Action onAfterTerminate, Action onDispose) { + super(source); + this.onSubscribeCall = onSubscribeCall; + this.onSuccessCall = onSuccessCall; + this.onErrorCall = onErrorCall; + this.onCompleteCall = onCompleteCall; + this.onAfterTerminate = onAfterTerminate; + this.onDisposeCall = onDispose; + } + + @Override + protected void subscribeActual(MaybeObserver<? super T> observer) { + source.subscribe(new MaybePeekObserver<>(observer, this)); + } + + static final class MaybePeekObserver<T> implements MaybeObserver<T>, Disposable { + final MaybeObserver<? super T> downstream; + + final MaybePeek<T> parent; + + Disposable upstream; + + MaybePeekObserver(MaybeObserver<? super T> actual, MaybePeek<T> parent) { + this.downstream = actual; + this.parent = parent; + } + + @Override + public void dispose() { + try { + parent.onDisposeCall.run(); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + RxJavaPlugins.onError(ex); + } + + upstream.dispose(); + upstream = DisposableHelper.DISPOSED; + } + + @Override + public boolean isDisposed() { + return upstream.isDisposed(); + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.validate(this.upstream, d)) { + try { + parent.onSubscribeCall.accept(d); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + d.dispose(); + this.upstream = DisposableHelper.DISPOSED; + EmptyDisposable.error(ex, downstream); + return; + } + + this.upstream = d; + + downstream.onSubscribe(this); + } + } + + @Override + public void onSuccess(T value) { + if (this.upstream == DisposableHelper.DISPOSED) { + return; + } + try { + parent.onSuccessCall.accept(value); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + onErrorInner(ex); + return; + } + this.upstream = DisposableHelper.DISPOSED; + + downstream.onSuccess(value); + + onAfterTerminate(); + } + + @Override + public void onError(Throwable e) { + if (this.upstream == DisposableHelper.DISPOSED) { + RxJavaPlugins.onError(e); + return; + } + + onErrorInner(e); + } + + void onErrorInner(Throwable e) { + try { + parent.onErrorCall.accept(e); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + e = new CompositeException(e, ex); + } + + this.upstream = DisposableHelper.DISPOSED; + + downstream.onError(e); + + onAfterTerminate(); + } + + @Override + public void onComplete() { + if (this.upstream == DisposableHelper.DISPOSED) { + return; + } + + try { + parent.onCompleteCall.run(); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + onErrorInner(ex); + return; + } + this.upstream = DisposableHelper.DISPOSED; + + downstream.onComplete(); + + onAfterTerminate(); + } + + void onAfterTerminate() { + try { + parent.onAfterTerminate.run(); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + RxJavaPlugins.onError(ex); + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeSubscribeOn.java b/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeSubscribeOn.java new file mode 100644 index 0000000000..7ec1a6ad71 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeSubscribeOn.java @@ -0,0 +1,104 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.maybe; + +import java.util.concurrent.atomic.AtomicReference; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.disposables.*; +/** + * Subscribes to the upstream MaybeSource on the specified scheduler. + * + * @param <T> the value type delivered + */ +public final class MaybeSubscribeOn<T> extends AbstractMaybeWithUpstream<T, T> { + + final Scheduler scheduler; + + public MaybeSubscribeOn(MaybeSource<T> source, Scheduler scheduler) { + super(source); + this.scheduler = scheduler; + } + + @Override + protected void subscribeActual(MaybeObserver<? super T> observer) { + SubscribeOnMaybeObserver<T> parent = new SubscribeOnMaybeObserver<>(observer); + observer.onSubscribe(parent); + + parent.task.replace(scheduler.scheduleDirect(new SubscribeTask<>(parent, source))); + } + + static final class SubscribeTask<T> implements Runnable { + final MaybeObserver<? super T> observer; + final MaybeSource<T> source; + + SubscribeTask(MaybeObserver<? super T> observer, MaybeSource<T> source) { + this.observer = observer; + this.source = source; + } + + @Override + public void run() { + source.subscribe(observer); + } + } + + static final class SubscribeOnMaybeObserver<T> + extends AtomicReference<Disposable> + implements MaybeObserver<T>, Disposable { + + final SequentialDisposable task; + + private static final long serialVersionUID = 8571289934935992137L; + + final MaybeObserver<? super T> downstream; + + SubscribeOnMaybeObserver(MaybeObserver<? super T> downstream) { + this.downstream = downstream; + this.task = new SequentialDisposable(); + } + + @Override + public void dispose() { + DisposableHelper.dispose(this); + task.dispose(); + } + + @Override + public boolean isDisposed() { + return DisposableHelper.isDisposed(get()); + } + + @Override + public void onSubscribe(Disposable d) { + DisposableHelper.setOnce(this, d); + } + + @Override + public void onSuccess(T value) { + downstream.onSuccess(value); + } + + @Override + public void onError(Throwable e) { + downstream.onError(e); + } + + @Override + public void onComplete() { + downstream.onComplete(); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeSwitchIfEmpty.java b/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeSwitchIfEmpty.java new file mode 100644 index 0000000000..c432b3c674 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeSwitchIfEmpty.java @@ -0,0 +1,125 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.maybe; + +import java.util.concurrent.atomic.AtomicReference; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; + +/** + * Subscribes to the other source if the main source is empty. + * + * @param <T> the value type + */ +public final class MaybeSwitchIfEmpty<T> extends AbstractMaybeWithUpstream<T, T> { + + final MaybeSource<? extends T> other; + + public MaybeSwitchIfEmpty(MaybeSource<T> source, MaybeSource<? extends T> other) { + super(source); + this.other = other; + } + + @Override + protected void subscribeActual(MaybeObserver<? super T> observer) { + source.subscribe(new SwitchIfEmptyMaybeObserver<>(observer, other)); + } + + static final class SwitchIfEmptyMaybeObserver<T> + extends AtomicReference<Disposable> + implements MaybeObserver<T>, Disposable { + + private static final long serialVersionUID = -2223459372976438024L; + + final MaybeObserver<? super T> downstream; + + final MaybeSource<? extends T> other; + + SwitchIfEmptyMaybeObserver(MaybeObserver<? super T> actual, MaybeSource<? extends T> other) { + this.downstream = actual; + this.other = other; + } + + @Override + public void dispose() { + DisposableHelper.dispose(this); + } + + @Override + public boolean isDisposed() { + return DisposableHelper.isDisposed(get()); + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.setOnce(this, d)) { + downstream.onSubscribe(this); + } + } + + @Override + public void onSuccess(T value) { + downstream.onSuccess(value); + } + + @Override + public void onError(Throwable e) { + downstream.onError(e); + } + + @Override + public void onComplete() { + Disposable d = get(); + if (d != DisposableHelper.DISPOSED) { + if (compareAndSet(d, null)) { + other.subscribe(new OtherMaybeObserver<T>(downstream, this)); + } + } + } + + static final class OtherMaybeObserver<T> implements MaybeObserver<T> { + + final MaybeObserver<? super T> downstream; + + final AtomicReference<Disposable> parent; + OtherMaybeObserver(MaybeObserver<? super T> actual, AtomicReference<Disposable> parent) { + this.downstream = actual; + this.parent = parent; + } + + @Override + public void onSubscribe(Disposable d) { + DisposableHelper.setOnce(parent, d); + } + + @Override + public void onSuccess(T value) { + downstream.onSuccess(value); + } + + @Override + public void onError(Throwable e) { + downstream.onError(e); + } + + @Override + public void onComplete() { + downstream.onComplete(); + } + } + + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeSwitchIfEmptySingle.java b/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeSwitchIfEmptySingle.java new file mode 100644 index 0000000000..29e77a3e56 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeSwitchIfEmptySingle.java @@ -0,0 +1,127 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.maybe; + +import java.util.concurrent.atomic.AtomicReference; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; +import io.reactivex.rxjava3.internal.fuseable.HasUpstreamMaybeSource; + +/** + * Subscribes to the other source if the main source is empty. + * + * @param <T> the value type + */ +public final class MaybeSwitchIfEmptySingle<T> extends Single<T> implements HasUpstreamMaybeSource<T> { + + final MaybeSource<T> source; + final SingleSource<? extends T> other; + + public MaybeSwitchIfEmptySingle(MaybeSource<T> source, SingleSource<? extends T> other) { + this.source = source; + this.other = other; + } + + @Override + public MaybeSource<T> source() { + return source; + } + + @Override + protected void subscribeActual(SingleObserver<? super T> observer) { + source.subscribe(new SwitchIfEmptyMaybeObserver<>(observer, other)); + } + + static final class SwitchIfEmptyMaybeObserver<T> + extends AtomicReference<Disposable> + implements MaybeObserver<T>, Disposable { + + private static final long serialVersionUID = 4603919676453758899L; + + final SingleObserver<? super T> downstream; + + final SingleSource<? extends T> other; + + SwitchIfEmptyMaybeObserver(SingleObserver<? super T> actual, SingleSource<? extends T> other) { + this.downstream = actual; + this.other = other; + } + + @Override + public void dispose() { + DisposableHelper.dispose(this); + } + + @Override + public boolean isDisposed() { + return DisposableHelper.isDisposed(get()); + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.setOnce(this, d)) { + downstream.onSubscribe(this); + } + } + + @Override + public void onSuccess(T value) { + downstream.onSuccess(value); + } + + @Override + public void onError(Throwable e) { + downstream.onError(e); + } + + @Override + public void onComplete() { + Disposable d = get(); + if (d != DisposableHelper.DISPOSED) { + if (compareAndSet(d, null)) { + other.subscribe(new OtherSingleObserver<T>(downstream, this)); + } + } + } + + static final class OtherSingleObserver<T> implements SingleObserver<T> { + + final SingleObserver<? super T> downstream; + + final AtomicReference<Disposable> parent; + OtherSingleObserver(SingleObserver<? super T> actual, AtomicReference<Disposable> parent) { + this.downstream = actual; + this.parent = parent; + } + + @Override + public void onSubscribe(Disposable d) { + DisposableHelper.setOnce(parent, d); + } + + @Override + public void onSuccess(T value) { + downstream.onSuccess(value); + } + + @Override + public void onError(Throwable e) { + downstream.onError(e); + } + } + + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeTakeUntilMaybe.java b/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeTakeUntilMaybe.java new file mode 100644 index 0000000000..e99011e509 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeTakeUntilMaybe.java @@ -0,0 +1,152 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.maybe; + +import java.util.concurrent.atomic.AtomicReference; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +/** + * Relays the main source's event unless the other Maybe signals an item first or just completes + * at which point the resulting Maybe is completed. + * + * @param <T> the value type + * @param <U> the other's value type + */ +public final class MaybeTakeUntilMaybe<T, U> extends AbstractMaybeWithUpstream<T, T> { + + final MaybeSource<U> other; + + public MaybeTakeUntilMaybe(MaybeSource<T> source, MaybeSource<U> other) { + super(source); + this.other = other; + } + + @Override + protected void subscribeActual(MaybeObserver<? super T> observer) { + TakeUntilMainMaybeObserver<T, U> parent = new TakeUntilMainMaybeObserver<>(observer); + observer.onSubscribe(parent); + + other.subscribe(parent.other); + + source.subscribe(parent); + } + + static final class TakeUntilMainMaybeObserver<T, U> + extends AtomicReference<Disposable> implements MaybeObserver<T>, Disposable { + + private static final long serialVersionUID = -2187421758664251153L; + + final MaybeObserver<? super T> downstream; + + final TakeUntilOtherMaybeObserver<U> other; + + TakeUntilMainMaybeObserver(MaybeObserver<? super T> downstream) { + this.downstream = downstream; + this.other = new TakeUntilOtherMaybeObserver<>(this); + } + + @Override + public void dispose() { + DisposableHelper.dispose(this); + DisposableHelper.dispose(other); + } + + @Override + public boolean isDisposed() { + return DisposableHelper.isDisposed(get()); + } + + @Override + public void onSubscribe(Disposable d) { + DisposableHelper.setOnce(this, d); + } + + @Override + public void onSuccess(T value) { + DisposableHelper.dispose(other); + if (getAndSet(DisposableHelper.DISPOSED) != DisposableHelper.DISPOSED) { + downstream.onSuccess(value); + } + } + + @Override + public void onError(Throwable e) { + DisposableHelper.dispose(other); + if (getAndSet(DisposableHelper.DISPOSED) != DisposableHelper.DISPOSED) { + downstream.onError(e); + } else { + RxJavaPlugins.onError(e); + } + } + + @Override + public void onComplete() { + DisposableHelper.dispose(other); + if (getAndSet(DisposableHelper.DISPOSED) != DisposableHelper.DISPOSED) { + downstream.onComplete(); + } + } + + void otherError(Throwable e) { + if (DisposableHelper.dispose(this)) { + downstream.onError(e); + } else { + RxJavaPlugins.onError(e); + } + } + + void otherComplete() { + if (DisposableHelper.dispose(this)) { + downstream.onComplete(); + } + } + + static final class TakeUntilOtherMaybeObserver<U> + extends AtomicReference<Disposable> implements MaybeObserver<U> { + + private static final long serialVersionUID = -1266041316834525931L; + + final TakeUntilMainMaybeObserver<?, U> parent; + + TakeUntilOtherMaybeObserver(TakeUntilMainMaybeObserver<?, U> parent) { + this.parent = parent; + } + + @Override + public void onSubscribe(Disposable d) { + DisposableHelper.setOnce(this, d); + } + + @Override + public void onSuccess(Object value) { + parent.otherComplete(); + } + + @Override + public void onError(Throwable e) { + parent.otherError(e); + } + + @Override + public void onComplete() { + parent.otherComplete(); + } + } + } + +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeTakeUntilPublisher.java b/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeTakeUntilPublisher.java new file mode 100644 index 0000000000..8e57a5ee84 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeTakeUntilPublisher.java @@ -0,0 +1,156 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.maybe; + +import java.util.concurrent.atomic.AtomicReference; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; +import io.reactivex.rxjava3.internal.subscriptions.SubscriptionHelper; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +/** + * Relays the main source's event unless the other Publisher signals an item first or just completes + * at which point the resulting Maybe is completed. + * + * @param <T> the value type + * @param <U> the other's value type + */ +public final class MaybeTakeUntilPublisher<T, U> extends AbstractMaybeWithUpstream<T, T> { + + final Publisher<U> other; + + public MaybeTakeUntilPublisher(MaybeSource<T> source, Publisher<U> other) { + super(source); + this.other = other; + } + + @Override + protected void subscribeActual(MaybeObserver<? super T> observer) { + TakeUntilMainMaybeObserver<T, U> parent = new TakeUntilMainMaybeObserver<>(observer); + observer.onSubscribe(parent); + + other.subscribe(parent.other); + + source.subscribe(parent); + } + + static final class TakeUntilMainMaybeObserver<T, U> + extends AtomicReference<Disposable> implements MaybeObserver<T>, Disposable { + + private static final long serialVersionUID = -2187421758664251153L; + + final MaybeObserver<? super T> downstream; + + final TakeUntilOtherMaybeObserver<U> other; + + TakeUntilMainMaybeObserver(MaybeObserver<? super T> downstream) { + this.downstream = downstream; + this.other = new TakeUntilOtherMaybeObserver<>(this); + } + + @Override + public void dispose() { + DisposableHelper.dispose(this); + SubscriptionHelper.cancel(other); + } + + @Override + public boolean isDisposed() { + return DisposableHelper.isDisposed(get()); + } + + @Override + public void onSubscribe(Disposable d) { + DisposableHelper.setOnce(this, d); + } + + @Override + public void onSuccess(T value) { + SubscriptionHelper.cancel(other); + if (getAndSet(DisposableHelper.DISPOSED) != DisposableHelper.DISPOSED) { + downstream.onSuccess(value); + } + } + + @Override + public void onError(Throwable e) { + SubscriptionHelper.cancel(other); + if (getAndSet(DisposableHelper.DISPOSED) != DisposableHelper.DISPOSED) { + downstream.onError(e); + } else { + RxJavaPlugins.onError(e); + } + } + + @Override + public void onComplete() { + SubscriptionHelper.cancel(other); + if (getAndSet(DisposableHelper.DISPOSED) != DisposableHelper.DISPOSED) { + downstream.onComplete(); + } + } + + void otherError(Throwable e) { + if (DisposableHelper.dispose(this)) { + downstream.onError(e); + } else { + RxJavaPlugins.onError(e); + } + } + + void otherComplete() { + if (DisposableHelper.dispose(this)) { + downstream.onComplete(); + } + } + + static final class TakeUntilOtherMaybeObserver<U> + extends AtomicReference<Subscription> implements FlowableSubscriber<U> { + + private static final long serialVersionUID = -1266041316834525931L; + + final TakeUntilMainMaybeObserver<?, U> parent; + + TakeUntilOtherMaybeObserver(TakeUntilMainMaybeObserver<?, U> parent) { + this.parent = parent; + } + + @Override + public void onSubscribe(Subscription s) { + SubscriptionHelper.setOnce(this, s, Long.MAX_VALUE); + } + + @Override + public void onNext(Object value) { + SubscriptionHelper.cancel(this); + parent.otherComplete(); + } + + @Override + public void onError(Throwable e) { + parent.otherError(e); + } + + @Override + public void onComplete() { + parent.otherComplete(); + } + } + } + +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeTimeInterval.java b/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeTimeInterval.java new file mode 100644 index 0000000000..9580976d89 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeTimeInterval.java @@ -0,0 +1,105 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.maybe; + +import java.util.concurrent.TimeUnit; + +import io.reactivex.rxjava3.annotations.NonNull; +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; +import io.reactivex.rxjava3.schedulers.Timed; + +/** + * Measures the time between subscription and the success item emission + * from the upstream and emits this as a {@link Timed} success value. + * @param <T> the element type of the sequence + * @since 3.0.0 + */ +public final class MaybeTimeInterval<T> extends Maybe<Timed<T>> { + + final MaybeSource<T> source; + + final TimeUnit unit; + + final Scheduler scheduler; + + final boolean start; + + public MaybeTimeInterval(MaybeSource<T> source, TimeUnit unit, Scheduler scheduler, boolean start) { + this.source = source; + this.unit = unit; + this.scheduler = scheduler; + this.start = start; + } + + @Override + protected void subscribeActual(@NonNull MaybeObserver<? super @NonNull Timed<T>> observer) { + source.subscribe(new TimeIntervalMaybeObserver<>(observer, unit, scheduler, start)); + } + + static final class TimeIntervalMaybeObserver<T> implements MaybeObserver<T>, Disposable { + + final MaybeObserver<? super Timed<T>> downstream; + + final TimeUnit unit; + + final Scheduler scheduler; + + final long startTime; + + Disposable upstream; + + TimeIntervalMaybeObserver(MaybeObserver<? super Timed<T>> downstream, TimeUnit unit, Scheduler scheduler, boolean start) { + this.downstream = downstream; + this.unit = unit; + this.scheduler = scheduler; + this.startTime = start ? scheduler.now(unit) : 0L; + } + + @Override + public void onSubscribe(@NonNull Disposable d) { + if (DisposableHelper.validate(this.upstream, d)) { + this.upstream = d; + + downstream.onSubscribe(this); + } + } + + @Override + public void onSuccess(@NonNull T t) { + downstream.onSuccess(new Timed<>(t, scheduler.now(unit) - startTime, unit)); + } + + @Override + public void onError(@NonNull Throwable e) { + downstream.onError(e); + } + + @Override + public void onComplete() { + downstream.onComplete(); + } + + @Override + public void dispose() { + upstream.dispose(); + } + + @Override + public boolean isDisposed() { + return upstream.isDisposed(); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeTimeoutMaybe.java b/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeTimeoutMaybe.java new file mode 100644 index 0000000000..2a2581c5f3 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeTimeoutMaybe.java @@ -0,0 +1,203 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.maybe; + +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicReference; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +/** + * Switches to the fallback Maybe if the other MaybeSource signals a success or completes, or + * signals TimeoutException if fallback is null. + * + * @param <T> the main value type + * @param <U> the other value type + */ +public final class MaybeTimeoutMaybe<T, U> extends AbstractMaybeWithUpstream<T, T> { + + final MaybeSource<U> other; + + final MaybeSource<? extends T> fallback; + + public MaybeTimeoutMaybe(MaybeSource<T> source, MaybeSource<U> other, MaybeSource<? extends T> fallback) { + super(source); + this.other = other; + this.fallback = fallback; + } + + @Override + protected void subscribeActual(MaybeObserver<? super T> observer) { + TimeoutMainMaybeObserver<T, U> parent = new TimeoutMainMaybeObserver<>(observer, fallback); + observer.onSubscribe(parent); + + other.subscribe(parent.other); + + source.subscribe(parent); + } + + static final class TimeoutMainMaybeObserver<T, U> + extends AtomicReference<Disposable> + implements MaybeObserver<T>, Disposable { + + private static final long serialVersionUID = -5955289211445418871L; + + final MaybeObserver<? super T> downstream; + + final TimeoutOtherMaybeObserver<T, U> other; + + final MaybeSource<? extends T> fallback; + + final TimeoutFallbackMaybeObserver<T> otherObserver; + + TimeoutMainMaybeObserver(MaybeObserver<? super T> actual, MaybeSource<? extends T> fallback) { + this.downstream = actual; + this.other = new TimeoutOtherMaybeObserver<>(this); + this.fallback = fallback; + this.otherObserver = fallback != null ? new TimeoutFallbackMaybeObserver<>(actual) : null; + } + + @Override + public void dispose() { + DisposableHelper.dispose(this); + DisposableHelper.dispose(other); + TimeoutFallbackMaybeObserver<T> oo = otherObserver; + if (oo != null) { + DisposableHelper.dispose(oo); + } + } + + @Override + public boolean isDisposed() { + return DisposableHelper.isDisposed(get()); + } + + @Override + public void onSubscribe(Disposable d) { + DisposableHelper.setOnce(this, d); + } + + @Override + public void onSuccess(T value) { + DisposableHelper.dispose(other); + if (getAndSet(DisposableHelper.DISPOSED) != DisposableHelper.DISPOSED) { + downstream.onSuccess(value); + } + } + + @Override + public void onError(Throwable e) { + DisposableHelper.dispose(other); + if (getAndSet(DisposableHelper.DISPOSED) != DisposableHelper.DISPOSED) { + downstream.onError(e); + } else { + RxJavaPlugins.onError(e); + } + } + + @Override + public void onComplete() { + DisposableHelper.dispose(other); + if (getAndSet(DisposableHelper.DISPOSED) != DisposableHelper.DISPOSED) { + downstream.onComplete(); + } + } + + public void otherError(Throwable e) { + if (DisposableHelper.dispose(this)) { + downstream.onError(e); + } else { + RxJavaPlugins.onError(e); + } + } + + public void otherComplete() { + if (DisposableHelper.dispose(this)) { + if (fallback == null) { + downstream.onError(new TimeoutException()); + } else { + fallback.subscribe(otherObserver); + } + } + } + } + + static final class TimeoutOtherMaybeObserver<T, U> + extends AtomicReference<Disposable> + implements MaybeObserver<Object> { + + private static final long serialVersionUID = 8663801314800248617L; + + final TimeoutMainMaybeObserver<T, U> parent; + + TimeoutOtherMaybeObserver(TimeoutMainMaybeObserver<T, U> parent) { + this.parent = parent; + } + + @Override + public void onSubscribe(Disposable d) { + DisposableHelper.setOnce(this, d); + } + + @Override + public void onSuccess(Object value) { + parent.otherComplete(); + } + + @Override + public void onError(Throwable e) { + parent.otherError(e); + } + + @Override + public void onComplete() { + parent.otherComplete(); + } + } + static final class TimeoutFallbackMaybeObserver<T> + extends AtomicReference<Disposable> + implements MaybeObserver<T> { + + private static final long serialVersionUID = 8663801314800248617L; + + final MaybeObserver<? super T> downstream; + + TimeoutFallbackMaybeObserver(MaybeObserver<? super T> downstream) { + this.downstream = downstream; + } + + @Override + public void onSubscribe(Disposable d) { + DisposableHelper.setOnce(this, d); + } + + @Override + public void onSuccess(T value) { + downstream.onSuccess(value); + } + + @Override + public void onError(Throwable e) { + downstream.onError(e); + } + + @Override + public void onComplete() { + downstream.onComplete(); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeTimeoutPublisher.java b/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeTimeoutPublisher.java new file mode 100644 index 0000000000..e89a8d0bc9 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeTimeoutPublisher.java @@ -0,0 +1,208 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.maybe; + +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicReference; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; +import io.reactivex.rxjava3.internal.subscriptions.SubscriptionHelper; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +/** + * Switches to the fallback Maybe if the other Publisher signals a success or completes, or + * signals TimeoutException if fallback is null. + * + * @param <T> the main value type + * @param <U> the other value type + */ +public final class MaybeTimeoutPublisher<T, U> extends AbstractMaybeWithUpstream<T, T> { + + final Publisher<U> other; + + final MaybeSource<? extends T> fallback; + + public MaybeTimeoutPublisher(MaybeSource<T> source, Publisher<U> other, MaybeSource<? extends T> fallback) { + super(source); + this.other = other; + this.fallback = fallback; + } + + @Override + protected void subscribeActual(MaybeObserver<? super T> observer) { + TimeoutMainMaybeObserver<T, U> parent = new TimeoutMainMaybeObserver<>(observer, fallback); + observer.onSubscribe(parent); + + other.subscribe(parent.other); + + source.subscribe(parent); + } + + static final class TimeoutMainMaybeObserver<T, U> + extends AtomicReference<Disposable> + implements MaybeObserver<T>, Disposable { + + private static final long serialVersionUID = -5955289211445418871L; + + final MaybeObserver<? super T> downstream; + + final TimeoutOtherMaybeObserver<T, U> other; + + final MaybeSource<? extends T> fallback; + + final TimeoutFallbackMaybeObserver<T> otherObserver; + + TimeoutMainMaybeObserver(MaybeObserver<? super T> actual, MaybeSource<? extends T> fallback) { + this.downstream = actual; + this.other = new TimeoutOtherMaybeObserver<>(this); + this.fallback = fallback; + this.otherObserver = fallback != null ? new TimeoutFallbackMaybeObserver<>(actual) : null; + } + + @Override + public void dispose() { + DisposableHelper.dispose(this); + SubscriptionHelper.cancel(other); + TimeoutFallbackMaybeObserver<T> oo = otherObserver; + if (oo != null) { + DisposableHelper.dispose(oo); + } + } + + @Override + public boolean isDisposed() { + return DisposableHelper.isDisposed(get()); + } + + @Override + public void onSubscribe(Disposable d) { + DisposableHelper.setOnce(this, d); + } + + @Override + public void onSuccess(T value) { + SubscriptionHelper.cancel(other); + if (getAndSet(DisposableHelper.DISPOSED) != DisposableHelper.DISPOSED) { + downstream.onSuccess(value); + } + } + + @Override + public void onError(Throwable e) { + SubscriptionHelper.cancel(other); + if (getAndSet(DisposableHelper.DISPOSED) != DisposableHelper.DISPOSED) { + downstream.onError(e); + } else { + RxJavaPlugins.onError(e); + } + } + + @Override + public void onComplete() { + SubscriptionHelper.cancel(other); + if (getAndSet(DisposableHelper.DISPOSED) != DisposableHelper.DISPOSED) { + downstream.onComplete(); + } + } + + public void otherError(Throwable e) { + if (DisposableHelper.dispose(this)) { + downstream.onError(e); + } else { + RxJavaPlugins.onError(e); + } + } + + public void otherComplete() { + if (DisposableHelper.dispose(this)) { + if (fallback == null) { + downstream.onError(new TimeoutException()); + } else { + fallback.subscribe(otherObserver); + } + } + } + } + + static final class TimeoutOtherMaybeObserver<T, U> + extends AtomicReference<Subscription> + implements FlowableSubscriber<Object> { + + private static final long serialVersionUID = 8663801314800248617L; + + final TimeoutMainMaybeObserver<T, U> parent; + + TimeoutOtherMaybeObserver(TimeoutMainMaybeObserver<T, U> parent) { + this.parent = parent; + } + + @Override + public void onSubscribe(Subscription s) { + SubscriptionHelper.setOnce(this, s, Long.MAX_VALUE); + } + + @Override + public void onNext(Object value) { + get().cancel(); + parent.otherComplete(); + } + + @Override + public void onError(Throwable e) { + parent.otherError(e); + } + + @Override + public void onComplete() { + parent.otherComplete(); + } + } + + static final class TimeoutFallbackMaybeObserver<T> + extends AtomicReference<Disposable> + implements MaybeObserver<T> { + + private static final long serialVersionUID = 8663801314800248617L; + + final MaybeObserver<? super T> downstream; + + TimeoutFallbackMaybeObserver(MaybeObserver<? super T> downstream) { + this.downstream = downstream; + } + + @Override + public void onSubscribe(Disposable d) { + DisposableHelper.setOnce(this, d); + } + + @Override + public void onSuccess(T value) { + downstream.onSuccess(value); + } + + @Override + public void onError(Throwable e) { + downstream.onError(e); + } + + @Override + public void onComplete() { + downstream.onComplete(); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeTimer.java b/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeTimer.java new file mode 100644 index 0000000000..2b9033c66b --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeTimer.java @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.maybe; + +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; + +/** + * Signals a {@code 0L} after the specified delay. + */ +public final class MaybeTimer extends Maybe<Long> { + + final long delay; + + final TimeUnit unit; + + final Scheduler scheduler; + + public MaybeTimer(long delay, TimeUnit unit, Scheduler scheduler) { + this.delay = delay; + this.unit = unit; + this.scheduler = scheduler; + } + + @Override + protected void subscribeActual(final MaybeObserver<? super Long> observer) { + TimerDisposable parent = new TimerDisposable(observer); + observer.onSubscribe(parent); + parent.setFuture(scheduler.scheduleDirect(parent, delay, unit)); + } + + static final class TimerDisposable extends AtomicReference<Disposable> implements Disposable, Runnable { + + private static final long serialVersionUID = 2875964065294031672L; + final MaybeObserver<? super Long> downstream; + + TimerDisposable(final MaybeObserver<? super Long> downstream) { + this.downstream = downstream; + } + + @Override + public void run() { + downstream.onSuccess(0L); + } + + @Override + public void dispose() { + DisposableHelper.dispose(this); + } + + @Override + public boolean isDisposed() { + return DisposableHelper.isDisposed(get()); + } + + void setFuture(Disposable d) { + DisposableHelper.replace(this, d); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeToFlowable.java b/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeToFlowable.java new file mode 100644 index 0000000000..5ce9f66103 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeToFlowable.java @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.maybe; + +import org.reactivestreams.Subscriber; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; +import io.reactivex.rxjava3.internal.fuseable.HasUpstreamMaybeSource; +import io.reactivex.rxjava3.internal.subscriptions.DeferredScalarSubscription; + +/** + * Wraps a MaybeSource and exposes it as a Flowable, relaying signals in a backpressure-aware manner + * and composes cancellation through. + * + * @param <T> the value type + */ +public final class MaybeToFlowable<T> extends Flowable<T> implements HasUpstreamMaybeSource<T> { + + final MaybeSource<T> source; + + public MaybeToFlowable(MaybeSource<T> source) { + this.source = source; + } + + @Override + public MaybeSource<T> source() { + return source; + } + + @Override + protected void subscribeActual(Subscriber<? super T> s) { + source.subscribe(new MaybeToFlowableSubscriber<>(s)); + } + + static final class MaybeToFlowableSubscriber<T> extends DeferredScalarSubscription<T> + implements MaybeObserver<T> { + + private static final long serialVersionUID = 7603343402964826922L; + + Disposable upstream; + + MaybeToFlowableSubscriber(Subscriber<? super T> downstream) { + super(downstream); + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.validate(this.upstream, d)) { + this.upstream = d; + + downstream.onSubscribe(this); + } + } + + @Override + public void onSuccess(T value) { + complete(value); + } + + @Override + public void onError(Throwable e) { + downstream.onError(e); + } + + @Override + public void onComplete() { + downstream.onComplete(); + } + + @Override + public void cancel() { + super.cancel(); + upstream.dispose(); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeToObservable.java b/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeToObservable.java new file mode 100644 index 0000000000..65ef0ced1b --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeToObservable.java @@ -0,0 +1,99 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.maybe; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; +import io.reactivex.rxjava3.internal.fuseable.HasUpstreamMaybeSource; +import io.reactivex.rxjava3.internal.observers.DeferredScalarDisposable; + +/** + * Wraps a MaybeSource and exposes it as an Observable, relaying signals in a backpressure-aware manner + * and composes cancellation through. + * + * @param <T> the value type + */ +public final class MaybeToObservable<T> extends Observable<T> implements HasUpstreamMaybeSource<T> { + + final MaybeSource<T> source; + + public MaybeToObservable(MaybeSource<T> source) { + this.source = source; + } + + @Override + public MaybeSource<T> source() { + return source; + } + + @Override + protected void subscribeActual(Observer<? super T> observer) { + source.subscribe(create(observer)); + } + + /** + * Creates a {@link MaybeObserver} wrapper around a {@link Observer}. + * <p>History: 2.1.11 - experimental + * @param <T> the value type + * @param downstream the downstream {@code Observer} to talk to + * @return the new MaybeObserver instance + * @since 2.2 + */ + public static <T> MaybeObserver<T> create(Observer<? super T> downstream) { + return new MaybeToObservableObserver<>(downstream); + } + + static final class MaybeToObservableObserver<T> extends DeferredScalarDisposable<T> + implements MaybeObserver<T> { + + private static final long serialVersionUID = 7603343402964826922L; + + Disposable upstream; + + MaybeToObservableObserver(Observer<? super T> downstream) { + super(downstream); + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.validate(this.upstream, d)) { + this.upstream = d; + + downstream.onSubscribe(this); + } + } + + @Override + public void onSuccess(T value) { + complete(value); + } + + @Override + public void onError(Throwable e) { + error(e); + } + + @Override + public void onComplete() { + complete(); + } + + @Override + public void dispose() { + super.dispose(); + upstream.dispose(); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeToPublisher.java b/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeToPublisher.java new file mode 100644 index 0000000000..8f1f10d28d --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeToPublisher.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.maybe; + +import org.reactivestreams.Publisher; + +import io.reactivex.rxjava3.core.MaybeSource; +import io.reactivex.rxjava3.functions.Function; + +/** + * Helper function to merge/concat values of each MaybeSource provided by a Publisher. + */ +public enum MaybeToPublisher implements Function<MaybeSource<Object>, Publisher<Object>> { + INSTANCE; + + @SuppressWarnings({ "rawtypes", "unchecked" }) + public static <T> Function<MaybeSource<T>, Publisher<T>> instance() { + return (Function)INSTANCE; + } + + @Override + public Publisher<Object> apply(MaybeSource<Object> t) { + return new MaybeToFlowable<>(t); + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeToSingle.java b/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeToSingle.java new file mode 100644 index 0000000000..48ab0134e3 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeToSingle.java @@ -0,0 +1,102 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.maybe; + +import java.util.NoSuchElementException; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; +import io.reactivex.rxjava3.internal.fuseable.HasUpstreamMaybeSource; + +/** + * Wraps a MaybeSource and exposes its onSuccess and onError signals and signals + * NoSuchElementException for onComplete if {@code defaultValue} is null. + * + * @param <T> the value type + */ +public final class MaybeToSingle<T> extends Single<T> implements HasUpstreamMaybeSource<T> { + + final MaybeSource<T> source; + final T defaultValue; + + public MaybeToSingle(MaybeSource<T> source, T defaultValue) { + this.source = source; + this.defaultValue = defaultValue; + } + + @Override + public MaybeSource<T> source() { + return source; + } + + @Override + protected void subscribeActual(SingleObserver<? super T> observer) { + source.subscribe(new ToSingleMaybeSubscriber<>(observer, defaultValue)); + } + + static final class ToSingleMaybeSubscriber<T> implements MaybeObserver<T>, Disposable { + final SingleObserver<? super T> downstream; + final T defaultValue; + + Disposable upstream; + + ToSingleMaybeSubscriber(SingleObserver<? super T> actual, T defaultValue) { + this.downstream = actual; + this.defaultValue = defaultValue; + } + + @Override + public void dispose() { + upstream.dispose(); + upstream = DisposableHelper.DISPOSED; + } + + @Override + public boolean isDisposed() { + return upstream.isDisposed(); + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.validate(this.upstream, d)) { + this.upstream = d; + + downstream.onSubscribe(this); + } + } + + @Override + public void onSuccess(T value) { + upstream = DisposableHelper.DISPOSED; + downstream.onSuccess(value); + } + + @Override + public void onError(Throwable e) { + upstream = DisposableHelper.DISPOSED; + downstream.onError(e); + } + + @Override + public void onComplete() { + upstream = DisposableHelper.DISPOSED; + if (defaultValue != null) { + downstream.onSuccess(defaultValue); + } else { + downstream.onError(new NoSuchElementException("The MaybeSource is empty")); + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeUnsafeCreate.java b/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeUnsafeCreate.java new file mode 100644 index 0000000000..462a2525b1 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeUnsafeCreate.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.maybe; + +import io.reactivex.rxjava3.core.*; + +/** + * Wraps a MaybeSource without safeguard and calls its subscribe() method for each MaybeObserver. + * + * @param <T> the value type + */ +public final class MaybeUnsafeCreate<T> extends AbstractMaybeWithUpstream<T, T> { + + public MaybeUnsafeCreate(MaybeSource<T> source) { + super(source); + } + + @Override + protected void subscribeActual(MaybeObserver<? super T> observer) { + source.subscribe(observer); + } + +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeUnsubscribeOn.java b/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeUnsubscribeOn.java new file mode 100644 index 0000000000..b1d8cb6f65 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeUnsubscribeOn.java @@ -0,0 +1,98 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.maybe; + +import java.util.concurrent.atomic.AtomicReference; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; + +/** + * Makes sure a dispose() call from downstream happens on the specified scheduler. + * + * @param <T> the value type + */ +public final class MaybeUnsubscribeOn<T> extends AbstractMaybeWithUpstream<T, T> { + + final Scheduler scheduler; + + public MaybeUnsubscribeOn(MaybeSource<T> source, Scheduler scheduler) { + super(source); + this.scheduler = scheduler; + } + + @Override + protected void subscribeActual(MaybeObserver<? super T> observer) { + source.subscribe(new UnsubscribeOnMaybeObserver<>(observer, scheduler)); + } + + static final class UnsubscribeOnMaybeObserver<T> extends AtomicReference<Disposable> + implements MaybeObserver<T>, Disposable, Runnable { + + private static final long serialVersionUID = 3256698449646456986L; + + final MaybeObserver<? super T> downstream; + + final Scheduler scheduler; + + Disposable ds; + + UnsubscribeOnMaybeObserver(MaybeObserver<? super T> actual, Scheduler scheduler) { + this.downstream = actual; + this.scheduler = scheduler; + } + + @Override + public void dispose() { + Disposable d = getAndSet(DisposableHelper.DISPOSED); + if (d != DisposableHelper.DISPOSED) { + this.ds = d; + scheduler.scheduleDirect(this); + } + } + + @Override + public void run() { + ds.dispose(); + } + + @Override + public boolean isDisposed() { + return DisposableHelper.isDisposed(get()); + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.setOnce(this, d)) { + downstream.onSubscribe(this); + } + } + + @Override + public void onSuccess(T value) { + downstream.onSuccess(value); + } + + @Override + public void onError(Throwable e) { + downstream.onError(e); + } + + @Override + public void onComplete() { + downstream.onComplete(); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeUsing.java b/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeUsing.java new file mode 100644 index 0000000000..74e4250e5f --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeUsing.java @@ -0,0 +1,235 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.maybe; + +import java.util.Objects; +import java.util.concurrent.atomic.AtomicReference; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.disposables.*; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +/** + * Creates a resource and a dependent Maybe for each incoming Observer and optionally + * disposes the resource eagerly (before the terminal event is send out). + * + * @param <T> the value type + * @param <D> the resource type + */ +public final class MaybeUsing<T, D> extends Maybe<T> { + + final Supplier<? extends D> resourceSupplier; + + final Function<? super D, ? extends MaybeSource<? extends T>> sourceSupplier; + + final Consumer<? super D> resourceDisposer; + + final boolean eager; + + public MaybeUsing(Supplier<? extends D> resourceSupplier, + Function<? super D, ? extends MaybeSource<? extends T>> sourceSupplier, + Consumer<? super D> resourceDisposer, + boolean eager) { + this.resourceSupplier = resourceSupplier; + this.sourceSupplier = sourceSupplier; + this.resourceDisposer = resourceDisposer; + this.eager = eager; + } + + @Override + protected void subscribeActual(MaybeObserver<? super T> observer) { + D resource; + + try { + resource = resourceSupplier.get(); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + EmptyDisposable.error(ex, observer); + return; + } + + MaybeSource<? extends T> source; + + try { + source = Objects.requireNonNull(sourceSupplier.apply(resource), "The sourceSupplier returned a null MaybeSource"); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + if (eager) { + try { + resourceDisposer.accept(resource); + } catch (Throwable exc) { + Exceptions.throwIfFatal(exc); + EmptyDisposable.error(new CompositeException(ex, exc), observer); + return; + } + } + + EmptyDisposable.error(ex, observer); + + if (!eager) { + try { + resourceDisposer.accept(resource); + } catch (Throwable exc) { + Exceptions.throwIfFatal(exc); + RxJavaPlugins.onError(exc); + } + } + return; + } + + source.subscribe(new UsingObserver<T, D>(observer, resource, resourceDisposer, eager)); + } + + static final class UsingObserver<T, D> + extends AtomicReference<Object> + implements MaybeObserver<T>, Disposable { + + private static final long serialVersionUID = -674404550052917487L; + + final MaybeObserver<? super T> downstream; + + final Consumer<? super D> disposer; + + final boolean eager; + + Disposable upstream; + + UsingObserver(MaybeObserver<? super T> actual, D resource, Consumer<? super D> disposer, boolean eager) { + super(resource); + this.downstream = actual; + this.disposer = disposer; + this.eager = eager; + } + + @Override + public void dispose() { + if (eager) { + disposeResource(); + upstream.dispose(); + upstream = DisposableHelper.DISPOSED; + } else { + upstream.dispose(); + upstream = DisposableHelper.DISPOSED; + disposeResource(); + } + } + + @SuppressWarnings("unchecked") + void disposeResource() { + Object resource = getAndSet(this); + if (resource != this) { + try { + disposer.accept((D)resource); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + RxJavaPlugins.onError(ex); + } + } + } + + @Override + public boolean isDisposed() { + return upstream.isDisposed(); + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.validate(this.upstream, d)) { + this.upstream = d; + + downstream.onSubscribe(this); + } + } + + @SuppressWarnings("unchecked") + @Override + public void onSuccess(T value) { + upstream = DisposableHelper.DISPOSED; + if (eager) { + Object resource = getAndSet(this); + if (resource != this) { + try { + disposer.accept((D)resource); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + downstream.onError(ex); + return; + } + } else { + return; + } + } + + downstream.onSuccess(value); + + if (!eager) { + disposeResource(); + } + } + + @SuppressWarnings("unchecked") + @Override + public void onError(Throwable e) { + upstream = DisposableHelper.DISPOSED; + if (eager) { + Object resource = getAndSet(this); + if (resource != this) { + try { + disposer.accept((D)resource); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + e = new CompositeException(e, ex); + } + } else { + return; + } + } + + downstream.onError(e); + + if (!eager) { + disposeResource(); + } + } + + @SuppressWarnings("unchecked") + @Override + public void onComplete() { + upstream = DisposableHelper.DISPOSED; + if (eager) { + Object resource = getAndSet(this); + if (resource != this) { + try { + disposer.accept((D)resource); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + downstream.onError(ex); + return; + } + } else { + return; + } + } + + downstream.onComplete(); + + if (!eager) { + disposeResource(); + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeZipArray.java b/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeZipArray.java new file mode 100644 index 0000000000..ac7514fb68 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeZipArray.java @@ -0,0 +1,205 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.maybe; + +import java.util.Objects; +import java.util.concurrent.atomic.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +public final class MaybeZipArray<T, R> extends Maybe<R> { + + final MaybeSource<? extends T>[] sources; + + final Function<? super Object[], ? extends R> zipper; + + public MaybeZipArray(MaybeSource<? extends T>[] sources, Function<? super Object[], ? extends R> zipper) { + this.sources = sources; + this.zipper = zipper; + } + + @Override + protected void subscribeActual(MaybeObserver<? super R> observer) { + MaybeSource<? extends T>[] sources = this.sources; + int n = sources.length; + + if (n == 1) { + sources[0].subscribe(new MaybeMap.MapMaybeObserver<>(observer, new SingletonArrayFunc())); + return; + } + + ZipCoordinator<T, R> parent = new ZipCoordinator<>(observer, n, zipper); + + observer.onSubscribe(parent); + + for (int i = 0; i < n; i++) { + if (parent.isDisposed()) { + return; + } + + MaybeSource<? extends T> source = sources[i]; + + if (source == null) { + parent.innerError(new NullPointerException("One of the sources is null"), i); + return; + } + source.subscribe(parent.observers[i]); + } + } + + static final class ZipCoordinator<T, R> extends AtomicInteger implements Disposable { + + private static final long serialVersionUID = -5556924161382950569L; + + final MaybeObserver<? super R> downstream; + + final Function<? super Object[], ? extends R> zipper; + + final ZipMaybeObserver<T>[] observers; + + Object[] values; + + @SuppressWarnings("unchecked") + ZipCoordinator(MaybeObserver<? super R> observer, int n, Function<? super Object[], ? extends R> zipper) { + super(n); + this.downstream = observer; + this.zipper = zipper; + ZipMaybeObserver<T>[] o = new ZipMaybeObserver[n]; + for (int i = 0; i < n; i++) { + o[i] = new ZipMaybeObserver<>(this, i); + } + this.observers = o; + this.values = new Object[n]; + } + + @Override + public boolean isDisposed() { + return get() <= 0; + } + + @Override + public void dispose() { + if (getAndSet(0) > 0) { + for (ZipMaybeObserver<?> d : observers) { + d.dispose(); + } + + values = null; + } + } + + void innerSuccess(T value, int index) { + Object[] values = this.values; + if (values != null) { + values[index] = value; + } + if (decrementAndGet() == 0) { + R v; + + try { + v = Objects.requireNonNull(zipper.apply(values), "The zipper returned a null value"); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + this.values = null; + downstream.onError(ex); + return; + } + + this.values = null; + downstream.onSuccess(v); + } + } + + void disposeExcept(int index) { + ZipMaybeObserver<T>[] observers = this.observers; + int n = observers.length; + for (int i = 0; i < index; i++) { + observers[i].dispose(); + } + for (int i = index + 1; i < n; i++) { + observers[i].dispose(); + } + } + + void innerError(Throwable ex, int index) { + if (getAndSet(0) > 0) { + disposeExcept(index); + values = null; + downstream.onError(ex); + } else { + RxJavaPlugins.onError(ex); + } + } + + void innerComplete(int index) { + if (getAndSet(0) > 0) { + disposeExcept(index); + values = null; + downstream.onComplete(); + } + } + } + + static final class ZipMaybeObserver<T> + extends AtomicReference<Disposable> + implements MaybeObserver<T> { + + private static final long serialVersionUID = 3323743579927613702L; + + final ZipCoordinator<T, ?> parent; + + final int index; + + ZipMaybeObserver(ZipCoordinator<T, ?> parent, int index) { + this.parent = parent; + this.index = index; + } + + public void dispose() { + DisposableHelper.dispose(this); + } + + @Override + public void onSubscribe(Disposable d) { + DisposableHelper.setOnce(this, d); + } + + @Override + public void onSuccess(T value) { + parent.innerSuccess(value, index); + } + + @Override + public void onError(Throwable e) { + parent.innerError(e, index); + } + + @Override + public void onComplete() { + parent.innerComplete(index); + } + } + + final class SingletonArrayFunc implements Function<T, R> { + @Override + public R apply(T t) throws Throwable { + return Objects.requireNonNull(zipper.apply(new Object[] { t }), "The zipper returned a null value"); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeZipIterable.java b/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeZipIterable.java new file mode 100644 index 0000000000..b247ca4ded --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeZipIterable.java @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.maybe; + +import java.util.Arrays; +import java.util.Objects; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.disposables.EmptyDisposable; +import io.reactivex.rxjava3.internal.operators.maybe.MaybeZipArray.ZipCoordinator; + +public final class MaybeZipIterable<T, R> extends Maybe<R> { + + final Iterable<? extends MaybeSource<? extends T>> sources; + + final Function<? super Object[], ? extends R> zipper; + + public MaybeZipIterable(Iterable<? extends MaybeSource<? extends T>> sources, Function<? super Object[], ? extends R> zipper) { + this.sources = sources; + this.zipper = zipper; + } + + @Override + protected void subscribeActual(MaybeObserver<? super R> observer) { + @SuppressWarnings("unchecked") + MaybeSource<? extends T>[] a = new MaybeSource[8]; + int n = 0; + + try { + for (MaybeSource<? extends T> source : sources) { + if (source == null) { + EmptyDisposable.error(new NullPointerException("One of the sources is null"), observer); + return; + } + if (n == a.length) { + a = Arrays.copyOf(a, n + (n >> 2)); + } + a[n++] = source; + } + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + EmptyDisposable.error(ex, observer); + return; + } + + if (n == 0) { + EmptyDisposable.complete(observer); + return; + } + + if (n == 1) { + a[0].subscribe(new MaybeMap.MapMaybeObserver<>(observer, new SingletonArrayFunc())); + return; + } + + ZipCoordinator<T, R> parent = new ZipCoordinator<>(observer, n, zipper); + + observer.onSubscribe(parent); + + for (int i = 0; i < n; i++) { + if (parent.isDisposed()) { + return; + } + + a[i].subscribe(parent.observers[i]); + } + } + + final class SingletonArrayFunc implements Function<T, R> { + @Override + public R apply(T t) throws Throwable { + return Objects.requireNonNull(zipper.apply(new Object[] { t }), "The zipper returned a null value"); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/mixed/CompletableAndThenObservable.java b/src/main/java/io/reactivex/rxjava3/internal/operators/mixed/CompletableAndThenObservable.java new file mode 100644 index 0000000000..9f46c097f6 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/mixed/CompletableAndThenObservable.java @@ -0,0 +1,100 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.mixed; + +import java.util.concurrent.atomic.AtomicReference; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; + +/** + * After Completable completes, it relays the signals + * of the ObservableSource to the downstream observer. + * + * @param <R> the result type of the ObservableSource and this operator + * @since 2.1.15 + */ +public final class CompletableAndThenObservable<R> extends Observable<R> { + + final CompletableSource source; + + final ObservableSource<? extends R> other; + + public CompletableAndThenObservable(CompletableSource source, + ObservableSource<? extends R> other) { + this.source = source; + this.other = other; + } + + @Override + protected void subscribeActual(Observer<? super R> observer) { + AndThenObservableObserver<R> parent = new AndThenObservableObserver<>(observer, other); + observer.onSubscribe(parent); + source.subscribe(parent); + } + + static final class AndThenObservableObserver<R> + extends AtomicReference<Disposable> + implements Observer<R>, CompletableObserver, Disposable { + + private static final long serialVersionUID = -8948264376121066672L; + + final Observer<? super R> downstream; + + ObservableSource<? extends R> other; + + AndThenObservableObserver(Observer<? super R> downstream, ObservableSource<? extends R> other) { + this.other = other; + this.downstream = downstream; + } + + @Override + public void onNext(R t) { + downstream.onNext(t); + } + + @Override + public void onError(Throwable t) { + downstream.onError(t); + } + + @Override + public void onComplete() { + ObservableSource<? extends R> o = other; + if (o == null) { + downstream.onComplete(); + } else { + other = null; + o.subscribe(this); + } + } + + @Override + public void dispose() { + DisposableHelper.dispose(this); + } + + @Override + public boolean isDisposed() { + return DisposableHelper.isDisposed(get()); + } + + @Override + public void onSubscribe(Disposable d) { + DisposableHelper.replace(this, d); + } + + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/mixed/CompletableAndThenPublisher.java b/src/main/java/io/reactivex/rxjava3/internal/operators/mixed/CompletableAndThenPublisher.java new file mode 100644 index 0000000000..7004181bcc --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/mixed/CompletableAndThenPublisher.java @@ -0,0 +1,114 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.mixed; + +import java.util.concurrent.atomic.*; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; +import io.reactivex.rxjava3.internal.subscriptions.SubscriptionHelper; + +/** + * After Completable completes, it relays the signals + * of the Publisher to the downstream subscriber. + * + * @param <R> the result type of the Publisher and this operator + * @since 2.1.15 + */ +public final class CompletableAndThenPublisher<R> extends Flowable<R> { + + final CompletableSource source; + + final Publisher<? extends R> other; + + public CompletableAndThenPublisher(CompletableSource source, + Publisher<? extends R> other) { + this.source = source; + this.other = other; + } + + @Override + protected void subscribeActual(Subscriber<? super R> s) { + source.subscribe(new AndThenPublisherSubscriber<R>(s, other)); + } + + static final class AndThenPublisherSubscriber<R> + extends AtomicReference<Subscription> + implements FlowableSubscriber<R>, CompletableObserver, Subscription { + + private static final long serialVersionUID = -8948264376121066672L; + + final Subscriber<? super R> downstream; + + Publisher<? extends R> other; + + Disposable upstream; + + final AtomicLong requested; + + AndThenPublisherSubscriber(Subscriber<? super R> downstream, Publisher<? extends R> other) { + this.downstream = downstream; + this.other = other; + this.requested = new AtomicLong(); + } + + @Override + public void onNext(R t) { + downstream.onNext(t); + } + + @Override + public void onError(Throwable t) { + downstream.onError(t); + } + + @Override + public void onComplete() { + Publisher<? extends R> p = other; + if (p == null) { + downstream.onComplete(); + } else { + other = null; + p.subscribe(this); + } + } + + @Override + public void request(long n) { + SubscriptionHelper.deferredRequest(this, requested, n); + } + + @Override + public void cancel() { + upstream.dispose(); + SubscriptionHelper.cancel(this); + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.validate(upstream, d)) { + this.upstream = d; + downstream.onSubscribe(this); + } + } + + @Override + public void onSubscribe(Subscription s) { + SubscriptionHelper.deferredSetOnce(this, requested, s); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/mixed/ConcatMapXMainObserver.java b/src/main/java/io/reactivex/rxjava3/internal/operators/mixed/ConcatMapXMainObserver.java new file mode 100644 index 0000000000..d2d29b1213 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/mixed/ConcatMapXMainObserver.java @@ -0,0 +1,152 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.mixed; + +import java.util.concurrent.atomic.AtomicInteger; + +import io.reactivex.rxjava3.core.Observer; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; +import io.reactivex.rxjava3.internal.util.*; +import io.reactivex.rxjava3.operators.QueueDisposable; +import io.reactivex.rxjava3.operators.QueueFuseable; +import io.reactivex.rxjava3.operators.SimpleQueue; +import io.reactivex.rxjava3.operators.SpscLinkedArrayQueue; + +/** + * Base class for implementing concatMapX main observers. + * + * @param <T> the upstream value type + * @since 3.0.10 + */ +public abstract class ConcatMapXMainObserver<T> extends AtomicInteger +implements Observer<T>, Disposable { + + private static final long serialVersionUID = -3214213361171757852L; + + final AtomicThrowable errors; + + final int prefetch; + + final ErrorMode errorMode; + + SimpleQueue<T> queue; + + Disposable upstream; + + volatile boolean done; + + volatile boolean disposed; + + public ConcatMapXMainObserver(int prefetch, ErrorMode errorMode) { + this.errorMode = errorMode; + this.errors = new AtomicThrowable(); + this.prefetch = prefetch; + } + + @Override + public final void onSubscribe(Disposable d) { + if (DisposableHelper.validate(upstream, d)) { + upstream = d; + if (d instanceof QueueDisposable) { + @SuppressWarnings("unchecked") + QueueDisposable<T> qd = (QueueDisposable<T>)d; + int mode = qd.requestFusion(QueueFuseable.ANY | QueueFuseable.BOUNDARY); + if (mode == QueueFuseable.SYNC) { + queue = qd; + done = true; + + onSubscribeDownstream(); + + drain(); + return; + } + else if (mode == QueueFuseable.ASYNC) { + queue = qd; + + onSubscribeDownstream(); + + return; + } + } + + queue = new SpscLinkedArrayQueue<>(prefetch); + onSubscribeDownstream(); + } + } + + @Override + public final void onNext(T t) { + // In async fusion mode, t is a drain indicator + if (t != null) { + queue.offer(t); + } + drain(); + } + + @Override + public final void onError(Throwable t) { + if (errors.tryAddThrowableOrReport(t)) { + if (errorMode == ErrorMode.IMMEDIATE) { + disposeInner(); + } + done = true; + drain(); + } + } + + @Override + public final void onComplete() { + done = true; + drain(); + } + + @Override + public final void dispose() { + disposed = true; + upstream.dispose(); + disposeInner(); + errors.tryTerminateAndReport(); + if (getAndIncrement() == 0) { + queue.clear(); + clearValue(); + } + } + + @Override + public final boolean isDisposed() { + return disposed; + } + + /** + * Override this to clear values when the downstream disposes. + */ + void clearValue() { + } + + /** + * Typically, this should be {@code downstream.onSubscribe(this)}. + */ + abstract void onSubscribeDownstream(); + + /** + * Typically, this should be {@code inner.dispose()}. + */ + abstract void disposeInner(); + + /** + * Implement the serialized inner subscribing and value emission here. + */ + abstract void drain(); +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/mixed/ConcatMapXMainSubscriber.java b/src/main/java/io/reactivex/rxjava3/internal/operators/mixed/ConcatMapXMainSubscriber.java new file mode 100644 index 0000000000..03e4c568d6 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/mixed/ConcatMapXMainSubscriber.java @@ -0,0 +1,157 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.mixed; + +import java.util.concurrent.atomic.AtomicInteger; + +import org.reactivestreams.Subscription; + +import io.reactivex.rxjava3.core.FlowableSubscriber; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.internal.subscriptions.SubscriptionHelper; +import io.reactivex.rxjava3.internal.util.*; +import io.reactivex.rxjava3.operators.QueueFuseable; +import io.reactivex.rxjava3.operators.QueueSubscription; +import io.reactivex.rxjava3.operators.SimpleQueue; +import io.reactivex.rxjava3.operators.SpscArrayQueue; + +/** + * Base class for implementing concatMapX main subscribers. + * + * @param <T> the upstream value type + * @since 3.0.10 + */ +public abstract class ConcatMapXMainSubscriber<T> extends AtomicInteger +implements FlowableSubscriber<T> { + + private static final long serialVersionUID = -3214213361171757852L; + + final AtomicThrowable errors; + + final int prefetch; + + final ErrorMode errorMode; + + SimpleQueue<T> queue; + + Subscription upstream; + + volatile boolean done; + + volatile boolean cancelled; + + boolean syncFused; + + public ConcatMapXMainSubscriber(int prefetch, ErrorMode errorMode) { + this.errorMode = errorMode; + this.errors = new AtomicThrowable(); + this.prefetch = prefetch; + } + + @Override + public final void onSubscribe(Subscription s) { + if (SubscriptionHelper.validate(upstream, s)) { + upstream = s; + if (s instanceof QueueSubscription) { + @SuppressWarnings("unchecked") + QueueSubscription<T> qs = (QueueSubscription<T>)s; + int mode = qs.requestFusion(QueueFuseable.ANY | QueueFuseable.BOUNDARY); + if (mode == QueueFuseable.SYNC) { + queue = qs; + syncFused = true; + done = true; + + onSubscribeDownstream(); + + drain(); + return; + } + else if (mode == QueueFuseable.ASYNC) { + queue = qs; + + onSubscribeDownstream(); + + upstream.request(prefetch); + return; + } + } + + queue = new SpscArrayQueue<>(prefetch); + onSubscribeDownstream(); + upstream.request(prefetch); + } + } + + @Override + public final void onNext(T t) { + // In async fusion mode, t is a drain indicator + if (t != null) { + if (!queue.offer(t)) { + upstream.cancel(); + onError(new QueueOverflowException()); + return; + } + } + drain(); + } + + @Override + public final void onError(Throwable t) { + if (errors.tryAddThrowableOrReport(t)) { + if (errorMode == ErrorMode.IMMEDIATE) { + disposeInner(); + } + done = true; + drain(); + } + } + + @Override + public final void onComplete() { + done = true; + drain(); + } + + final void stop() { + cancelled = true; + upstream.cancel(); + disposeInner(); + errors.tryTerminateAndReport(); + if (getAndIncrement() == 0) { + queue.clear(); + clearValue(); + } + } + + /** + * Override this to clear values when the downstream disposes. + */ + void clearValue() { + } + + /** + * Typically, this should be {@code downstream.onSubscribe(this);}. + */ + abstract void onSubscribeDownstream(); + + /** + * Typically, this should be {@code inner.dispose()}. + */ + abstract void disposeInner(); + + /** + * Implement the serialized inner subscribing and value emission here. + */ + abstract void drain(); +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/mixed/FlowableConcatMapCompletable.java b/src/main/java/io/reactivex/rxjava3/internal/operators/mixed/FlowableConcatMapCompletable.java new file mode 100644 index 0000000000..ab93e4c27f --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/mixed/FlowableConcatMapCompletable.java @@ -0,0 +1,234 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.mixed; + +import java.util.Objects; +import java.util.concurrent.atomic.AtomicReference; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; +import io.reactivex.rxjava3.internal.util.*; +import io.reactivex.rxjava3.operators.SimpleQueue; + +/** + * Maps the upstream items into {@link CompletableSource}s and subscribes to them one after the + * other completes or terminates (in error-delaying mode). + * <p>History: 2.1.11 - experimental + * @param <T> the upstream value type + * @since 2.2 + */ +public final class FlowableConcatMapCompletable<T> extends Completable { + + final Flowable<T> source; + + final Function<? super T, ? extends CompletableSource> mapper; + + final ErrorMode errorMode; + + final int prefetch; + + public FlowableConcatMapCompletable(Flowable<T> source, + Function<? super T, ? extends CompletableSource> mapper, + ErrorMode errorMode, + int prefetch) { + this.source = source; + this.mapper = mapper; + this.errorMode = errorMode; + this.prefetch = prefetch; + } + + @Override + protected void subscribeActual(CompletableObserver observer) { + source.subscribe(new ConcatMapCompletableObserver<>(observer, mapper, errorMode, prefetch)); + } + + static final class ConcatMapCompletableObserver<T> + extends ConcatMapXMainSubscriber<T> + implements Disposable { + + private static final long serialVersionUID = 3610901111000061034L; + + final CompletableObserver downstream; + + final Function<? super T, ? extends CompletableSource> mapper; + + final ConcatMapInnerObserver inner; + + volatile boolean active; + + int consumed; + + ConcatMapCompletableObserver(CompletableObserver downstream, + Function<? super T, ? extends CompletableSource> mapper, + ErrorMode errorMode, int prefetch) { + super(prefetch, errorMode); + this.downstream = downstream; + this.mapper = mapper; + this.inner = new ConcatMapInnerObserver(this); + } + + @Override + void onSubscribeDownstream() { + downstream.onSubscribe(this); + } + + @Override + void disposeInner() { + inner.dispose(); + } + + @Override + public void dispose() { + stop(); + } + + @Override + public boolean isDisposed() { + return cancelled; + } + + void innerError(Throwable ex) { + if (errors.tryAddThrowableOrReport(ex)) { + if (errorMode == ErrorMode.IMMEDIATE) { + upstream.cancel(); + errors.tryTerminateConsumer(downstream); + if (getAndIncrement() == 0) { + queue.clear(); + } + } else { + active = false; + drain(); + } + } + } + + void innerComplete() { + active = false; + drain(); + } + + @Override + void drain() { + if (getAndIncrement() != 0) { + return; + } + + ErrorMode errorMode = this.errorMode; + SimpleQueue<T> queue = this.queue; + AtomicThrowable errors = this.errors; + boolean syncFused = this.syncFused; + + do { + if (cancelled) { + queue.clear(); + return; + } + + if (errors.get() != null) { + if (errorMode == ErrorMode.IMMEDIATE + || (errorMode == ErrorMode.BOUNDARY && !active)) { + queue.clear(); + errors.tryTerminateConsumer(downstream); + return; + } + } + + if (!active) { + + boolean d = done; + T v; + try { + v = queue.poll(); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + upstream.cancel(); + errors.tryAddThrowableOrReport(ex); + errors.tryTerminateConsumer(downstream); + return; + } + boolean empty = v == null; + + if (d && empty) { + errors.tryTerminateConsumer(downstream); + return; + } + + if (!empty) { + + int limit = prefetch - (prefetch >> 1); + + if (!syncFused) { + int c = consumed + 1; + if (c == limit) { + consumed = 0; + upstream.request(limit); + } else { + consumed = c; + } + } + + CompletableSource cs; + + try { + cs = Objects.requireNonNull(mapper.apply(v), "The mapper returned a null CompletableSource"); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + queue.clear(); + upstream.cancel(); + errors.tryAddThrowableOrReport(ex); + errors.tryTerminateConsumer(downstream); + return; + } + active = true; + cs.subscribe(inner); + } + } + } while (decrementAndGet() != 0); + } + + static final class ConcatMapInnerObserver extends AtomicReference<Disposable> + implements CompletableObserver { + + private static final long serialVersionUID = 5638352172918776687L; + + final ConcatMapCompletableObserver<?> parent; + + ConcatMapInnerObserver(ConcatMapCompletableObserver<?> parent) { + this.parent = parent; + } + + @Override + public void onSubscribe(Disposable d) { + DisposableHelper.replace(this, d); + } + + @Override + public void onError(Throwable e) { + parent.innerError(e); + } + + @Override + public void onComplete() { + parent.innerComplete(); + } + + void dispose() { + DisposableHelper.dispose(this); + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/mixed/FlowableConcatMapMaybe.java b/src/main/java/io/reactivex/rxjava3/internal/operators/mixed/FlowableConcatMapMaybe.java new file mode 100644 index 0000000000..944ac69311 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/mixed/FlowableConcatMapMaybe.java @@ -0,0 +1,294 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.mixed; + +import java.util.Objects; +import java.util.concurrent.atomic.*; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; +import io.reactivex.rxjava3.internal.util.*; +import io.reactivex.rxjava3.operators.SimpleQueue; + +/** + * Maps each upstream item into a {@link MaybeSource}, subscribes to them one after the other terminates + * and relays their success values, optionally delaying any errors till the main and inner sources + * terminate. + * <p>History: 2.1.11 - experimental + * @param <T> the upstream element type + * @param <R> the output element type + * @since 2.2 + */ +public final class FlowableConcatMapMaybe<T, R> extends Flowable<R> { + + final Flowable<T> source; + + final Function<? super T, ? extends MaybeSource<? extends R>> mapper; + + final ErrorMode errorMode; + + final int prefetch; + + public FlowableConcatMapMaybe(Flowable<T> source, + Function<? super T, ? extends MaybeSource<? extends R>> mapper, + ErrorMode errorMode, int prefetch) { + this.source = source; + this.mapper = mapper; + this.errorMode = errorMode; + this.prefetch = prefetch; + } + + @Override + protected void subscribeActual(Subscriber<? super R> s) { + source.subscribe(new ConcatMapMaybeSubscriber<>(s, mapper, prefetch, errorMode)); + } + + static final class ConcatMapMaybeSubscriber<T, R> + extends ConcatMapXMainSubscriber<T> implements Subscription { + + private static final long serialVersionUID = -9140123220065488293L; + + final Subscriber<? super R> downstream; + + final Function<? super T, ? extends MaybeSource<? extends R>> mapper; + + final AtomicLong requested; + + final ConcatMapMaybeObserver<R> inner; + + long emitted; + + int consumed; + + R item; + + volatile int state; + + /** No inner MaybeSource is running. */ + static final int STATE_INACTIVE = 0; + /** An inner MaybeSource is running but there are no results yet. */ + static final int STATE_ACTIVE = 1; + /** The inner MaybeSource succeeded with a value in {@link #item}. */ + static final int STATE_RESULT_VALUE = 2; + + ConcatMapMaybeSubscriber(Subscriber<? super R> downstream, + Function<? super T, ? extends MaybeSource<? extends R>> mapper, + int prefetch, ErrorMode errorMode) { + super(prefetch, errorMode); + this.downstream = downstream; + this.mapper = mapper; + this.requested = new AtomicLong(); + this.inner = new ConcatMapMaybeObserver<>(this); + } + + @Override + void onSubscribeDownstream() { + downstream.onSubscribe(this); + } + + @Override + public void request(long n) { + BackpressureHelper.add(requested, n); + drain(); + } + + @Override + public void cancel() { + stop(); + } + + void innerSuccess(R item) { + this.item = item; + this.state = STATE_RESULT_VALUE; + drain(); + } + + void innerComplete() { + this.state = STATE_INACTIVE; + drain(); + } + + void innerError(Throwable ex) { + if (errors.tryAddThrowableOrReport(ex)) { + if (errorMode != ErrorMode.END) { + upstream.cancel(); + } + this.state = STATE_INACTIVE; + drain(); + } + } + + @Override + void clearValue() { + item = null; + } + + @Override + void disposeInner() { + inner.dispose(); + } + + @Override + void drain() { + if (getAndIncrement() != 0) { + return; + } + + int missed = 1; + Subscriber<? super R> downstream = this.downstream; + ErrorMode errorMode = this.errorMode; + SimpleQueue<T> queue = this.queue; + AtomicThrowable errors = this.errors; + AtomicLong requested = this.requested; + int limit = prefetch - (prefetch >> 1); + boolean syncFused = this.syncFused; + + for (;;) { + + for (;;) { + if (cancelled) { + queue.clear(); + item = null; + break; + } + + int s = state; + + if (errors.get() != null) { + if (errorMode == ErrorMode.IMMEDIATE + || (errorMode == ErrorMode.BOUNDARY && s == STATE_INACTIVE)) { + queue.clear(); + item = null; + errors.tryTerminateConsumer(downstream); + return; + } + } + + if (s == STATE_INACTIVE) { + boolean d = done; + T v; + try { + v = queue.poll(); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + upstream.cancel(); + errors.tryAddThrowableOrReport(ex); + errors.tryTerminateConsumer(downstream); + return; + } + boolean empty = v == null; + + if (d && empty) { + errors.tryTerminateConsumer(downstream); + return; + } + + if (empty) { + break; + } + + if (!syncFused) { + int c = consumed + 1; + if (c == limit) { + consumed = 0; + upstream.request(limit); + } else { + consumed = c; + } + } + + MaybeSource<? extends R> ms; + + try { + ms = Objects.requireNonNull(mapper.apply(v), "The mapper returned a null MaybeSource"); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + upstream.cancel(); + queue.clear(); + errors.tryAddThrowableOrReport(ex); + errors.tryTerminateConsumer(downstream); + return; + } + + state = STATE_ACTIVE; + ms.subscribe(inner); + break; + } else if (s == STATE_RESULT_VALUE) { + long e = emitted; + if (e != requested.get()) { + R w = item; + item = null; + + downstream.onNext(w); + + emitted = e + 1; + state = STATE_INACTIVE; + } else { + break; + } + } else { + break; + } + } + + missed = addAndGet(-missed); + if (missed == 0) { + break; + } + } + } + + static final class ConcatMapMaybeObserver<R> + extends AtomicReference<Disposable> + implements MaybeObserver<R> { + + private static final long serialVersionUID = -3051469169682093892L; + + final ConcatMapMaybeSubscriber<?, R> parent; + + ConcatMapMaybeObserver(ConcatMapMaybeSubscriber<?, R> parent) { + this.parent = parent; + } + + @Override + public void onSubscribe(Disposable d) { + DisposableHelper.replace(this, d); + } + + @Override + public void onSuccess(R t) { + parent.innerSuccess(t); + } + + @Override + public void onError(Throwable e) { + parent.innerError(e); + } + + @Override + public void onComplete() { + parent.innerComplete(); + } + + void dispose() { + DisposableHelper.dispose(this); + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/mixed/FlowableConcatMapMaybePublisher.java b/src/main/java/io/reactivex/rxjava3/internal/operators/mixed/FlowableConcatMapMaybePublisher.java new file mode 100644 index 0000000000..42c1f732c2 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/mixed/FlowableConcatMapMaybePublisher.java @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.mixed; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.operators.mixed.FlowableConcatMapMaybe.ConcatMapMaybeSubscriber; +import io.reactivex.rxjava3.internal.util.ErrorMode; + +/** + * Maps each upstream item into a {@link MaybeSource}, subscribes to them one after the other terminates + * and relays their success values, optionally delaying any errors till the main and inner sources + * terminate. + * <p>History: 2.1.11 - experimental + * @param <T> the upstream element type + * @param <R> the output element type + * @since 2.2 + */ +public final class FlowableConcatMapMaybePublisher<T, R> extends Flowable<R> { + + final Publisher<T> source; + + final Function<? super T, ? extends MaybeSource<? extends R>> mapper; + + final ErrorMode errorMode; + + final int prefetch; + + public FlowableConcatMapMaybePublisher(Publisher<T> source, + Function<? super T, ? extends MaybeSource<? extends R>> mapper, + ErrorMode errorMode, int prefetch) { + this.source = source; + this.mapper = mapper; + this.errorMode = errorMode; + this.prefetch = prefetch; + } + + @Override + protected void subscribeActual(Subscriber<? super R> s) { + source.subscribe(new ConcatMapMaybeSubscriber<>(s, mapper, prefetch, errorMode)); + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/mixed/FlowableConcatMapSingle.java b/src/main/java/io/reactivex/rxjava3/internal/operators/mixed/FlowableConcatMapSingle.java new file mode 100644 index 0000000000..5c18b6b258 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/mixed/FlowableConcatMapSingle.java @@ -0,0 +1,284 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.mixed; + +import java.util.Objects; +import java.util.concurrent.atomic.*; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; +import io.reactivex.rxjava3.internal.util.*; +import io.reactivex.rxjava3.operators.SimpleQueue; + +/** + * Maps each upstream item into a {@link SingleSource}, subscribes to them one after the other terminates + * and relays their success values, optionally delaying any errors till the main and inner sources + * terminate. + * <p>History: 2.1.11 - experimental + * @param <T> the upstream element type + * @param <R> the output element type + * @since 2.2 + */ +public final class FlowableConcatMapSingle<T, R> extends Flowable<R> { + + final Flowable<T> source; + + final Function<? super T, ? extends SingleSource<? extends R>> mapper; + + final ErrorMode errorMode; + + final int prefetch; + + public FlowableConcatMapSingle(Flowable<T> source, + Function<? super T, ? extends SingleSource<? extends R>> mapper, + ErrorMode errorMode, int prefetch) { + this.source = source; + this.mapper = mapper; + this.errorMode = errorMode; + this.prefetch = prefetch; + } + + @Override + protected void subscribeActual(Subscriber<? super R> s) { + source.subscribe(new ConcatMapSingleSubscriber<>(s, mapper, prefetch, errorMode)); + } + + static final class ConcatMapSingleSubscriber<T, R> + extends ConcatMapXMainSubscriber<T> implements Subscription { + + private static final long serialVersionUID = -9140123220065488293L; + + final Subscriber<? super R> downstream; + + final Function<? super T, ? extends SingleSource<? extends R>> mapper; + + final AtomicLong requested; + + final ConcatMapSingleObserver<R> inner; + + long emitted; + + int consumed; + + R item; + + volatile int state; + + /** No inner SingleSource is running. */ + static final int STATE_INACTIVE = 0; + /** An inner SingleSource is running but there are no results yet. */ + static final int STATE_ACTIVE = 1; + /** The inner SingleSource succeeded with a value in {@link #item}. */ + static final int STATE_RESULT_VALUE = 2; + + ConcatMapSingleSubscriber(Subscriber<? super R> downstream, + Function<? super T, ? extends SingleSource<? extends R>> mapper, + int prefetch, ErrorMode errorMode) { + super(prefetch, errorMode); + this.downstream = downstream; + this.mapper = mapper; + this.requested = new AtomicLong(); + this.inner = new ConcatMapSingleObserver<>(this); + } + + @Override + void onSubscribeDownstream() { + downstream.onSubscribe(this); + } + + @Override + public void request(long n) { + BackpressureHelper.add(requested, n); + drain(); + } + + @Override + public void cancel() { + stop(); + } + + @Override + void clearValue() { + item = null; + } + + @Override + void disposeInner() { + inner.dispose(); + } + + void innerSuccess(R item) { + this.item = item; + this.state = STATE_RESULT_VALUE; + drain(); + } + + void innerError(Throwable ex) { + if (errors.tryAddThrowableOrReport(ex)) { + if (errorMode != ErrorMode.END) { + upstream.cancel(); + } + this.state = STATE_INACTIVE; + drain(); + } + } + + @Override + void drain() { + if (getAndIncrement() != 0) { + return; + } + + int missed = 1; + Subscriber<? super R> downstream = this.downstream; + ErrorMode errorMode = this.errorMode; + SimpleQueue<T> queue = this.queue; + AtomicThrowable errors = this.errors; + AtomicLong requested = this.requested; + int limit = prefetch - (prefetch >> 1); + boolean syncFused = this.syncFused; + + for (;;) { + + for (;;) { + if (cancelled) { + queue.clear(); + item = null; + break; + } + + int s = state; + + if (errors.get() != null) { + if (errorMode == ErrorMode.IMMEDIATE + || (errorMode == ErrorMode.BOUNDARY && s == STATE_INACTIVE)) { + queue.clear(); + item = null; + errors.tryTerminateConsumer(downstream); + return; + } + } + + if (s == STATE_INACTIVE) { + boolean d = done; + T v; + try { + v = queue.poll(); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + upstream.cancel(); + errors.tryAddThrowableOrReport(ex); + errors.tryTerminateConsumer(downstream); + return; + } + boolean empty = v == null; + + if (d && empty) { + errors.tryTerminateConsumer(downstream); + return; + } + + if (empty) { + break; + } + + if (!syncFused) { + int c = consumed + 1; + if (c == limit) { + consumed = 0; + upstream.request(limit); + } else { + consumed = c; + } + } + + SingleSource<? extends R> ss; + + try { + ss = Objects.requireNonNull(mapper.apply(v), "The mapper returned a null SingleSource"); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + upstream.cancel(); + queue.clear(); + errors.tryAddThrowableOrReport(ex); + errors.tryTerminateConsumer(downstream); + return; + } + + state = STATE_ACTIVE; + ss.subscribe(inner); + break; + } else if (s == STATE_RESULT_VALUE) { + long e = emitted; + if (e != requested.get()) { + R w = item; + item = null; + + downstream.onNext(w); + + emitted = e + 1; + state = STATE_INACTIVE; + } else { + break; + } + } else { + break; + } + } + + missed = addAndGet(-missed); + if (missed == 0) { + break; + } + } + } + + static final class ConcatMapSingleObserver<R> + extends AtomicReference<Disposable> + implements SingleObserver<R> { + + private static final long serialVersionUID = -3051469169682093892L; + + final ConcatMapSingleSubscriber<?, R> parent; + + ConcatMapSingleObserver(ConcatMapSingleSubscriber<?, R> parent) { + this.parent = parent; + } + + @Override + public void onSubscribe(Disposable d) { + DisposableHelper.replace(this, d); + } + + @Override + public void onSuccess(R t) { + parent.innerSuccess(t); + } + + @Override + public void onError(Throwable e) { + parent.innerError(e); + } + + void dispose() { + DisposableHelper.dispose(this); + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/mixed/FlowableConcatMapSinglePublisher.java b/src/main/java/io/reactivex/rxjava3/internal/operators/mixed/FlowableConcatMapSinglePublisher.java new file mode 100644 index 0000000000..0e1986180a --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/mixed/FlowableConcatMapSinglePublisher.java @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.mixed; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.operators.mixed.FlowableConcatMapSingle.ConcatMapSingleSubscriber; +import io.reactivex.rxjava3.internal.util.ErrorMode; + +/** + * Maps each upstream item into a {@link SingleSource}, subscribes to them one after the other terminates + * and relays their success values, optionally delaying any errors till the main and inner sources + * terminate. + * <p>History: 2.1.11 - experimental + * @param <T> the upstream element type + * @param <R> the output element type + * @since 2.2 + */ +public final class FlowableConcatMapSinglePublisher<T, R> extends Flowable<R> { + + final Publisher<T> source; + + final Function<? super T, ? extends SingleSource<? extends R>> mapper; + + final ErrorMode errorMode; + + final int prefetch; + + public FlowableConcatMapSinglePublisher(Publisher<T> source, + Function<? super T, ? extends SingleSource<? extends R>> mapper, + ErrorMode errorMode, int prefetch) { + this.source = source; + this.mapper = mapper; + this.errorMode = errorMode; + this.prefetch = prefetch; + } + + @Override + protected void subscribeActual(Subscriber<? super R> s) { + source.subscribe(new ConcatMapSingleSubscriber<>(s, mapper, prefetch, errorMode)); + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/mixed/FlowableSwitchMapCompletable.java b/src/main/java/io/reactivex/rxjava3/internal/operators/mixed/FlowableSwitchMapCompletable.java new file mode 100644 index 0000000000..9ca61ab15a --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/mixed/FlowableSwitchMapCompletable.java @@ -0,0 +1,220 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.mixed; + +import java.util.Objects; +import java.util.concurrent.atomic.AtomicReference; + +import org.reactivestreams.Subscription; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; +import io.reactivex.rxjava3.internal.subscriptions.SubscriptionHelper; +import io.reactivex.rxjava3.internal.util.AtomicThrowable; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +/** + * Maps the upstream values into {@link CompletableSource}s, subscribes to the newer one while + * disposing the subscription to the previous {@code CompletableSource}, thus keeping at most one + * active {@code CompletableSource} running. + * <p>History: 2.1.11 - experimental + * @param <T> the upstream value type + * @since 2.2 + */ +public final class FlowableSwitchMapCompletable<T> extends Completable { + + final Flowable<T> source; + + final Function<? super T, ? extends CompletableSource> mapper; + + final boolean delayErrors; + + public FlowableSwitchMapCompletable(Flowable<T> source, + Function<? super T, ? extends CompletableSource> mapper, boolean delayErrors) { + this.source = source; + this.mapper = mapper; + this.delayErrors = delayErrors; + } + + @Override + protected void subscribeActual(CompletableObserver observer) { + source.subscribe(new SwitchMapCompletableObserver<>(observer, mapper, delayErrors)); + } + + static final class SwitchMapCompletableObserver<T> implements FlowableSubscriber<T>, Disposable { + + final CompletableObserver downstream; + + final Function<? super T, ? extends CompletableSource> mapper; + + final boolean delayErrors; + + final AtomicThrowable errors; + + final AtomicReference<SwitchMapInnerObserver> inner; + + static final SwitchMapInnerObserver INNER_DISPOSED = new SwitchMapInnerObserver(null); + + volatile boolean done; + + Subscription upstream; + + SwitchMapCompletableObserver(CompletableObserver downstream, + Function<? super T, ? extends CompletableSource> mapper, boolean delayErrors) { + this.downstream = downstream; + this.mapper = mapper; + this.delayErrors = delayErrors; + this.errors = new AtomicThrowable(); + this.inner = new AtomicReference<>(); + } + + @Override + public void onSubscribe(Subscription s) { + if (SubscriptionHelper.validate(upstream, s)) { + this.upstream = s; + downstream.onSubscribe(this); + s.request(Long.MAX_VALUE); + } + } + + @Override + public void onNext(T t) { + CompletableSource c; + + try { + c = Objects.requireNonNull(mapper.apply(t), "The mapper returned a null CompletableSource"); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + upstream.cancel(); + onError(ex); + return; + } + + SwitchMapInnerObserver o = new SwitchMapInnerObserver(this); + + for (;;) { + SwitchMapInnerObserver current = inner.get(); + if (current == INNER_DISPOSED) { + break; + } + if (inner.compareAndSet(current, o)) { + if (current != null) { + current.dispose(); + } + c.subscribe(o); + break; + } + } + } + + @Override + public void onError(Throwable t) { + if (errors.tryAddThrowableOrReport(t)) { + if (delayErrors) { + onComplete(); + } else { + disposeInner(); + errors.tryTerminateConsumer(downstream); + } + } + } + + @Override + public void onComplete() { + done = true; + if (inner.get() == null) { + errors.tryTerminateConsumer(downstream); + } + } + + void disposeInner() { + SwitchMapInnerObserver o = inner.getAndSet(INNER_DISPOSED); + if (o != null && o != INNER_DISPOSED) { + o.dispose(); + } + } + + @Override + public void dispose() { + upstream.cancel(); + disposeInner(); + errors.tryTerminateAndReport(); + } + + @Override + public boolean isDisposed() { + return inner.get() == INNER_DISPOSED; + } + + void innerError(SwitchMapInnerObserver sender, Throwable error) { + if (inner.compareAndSet(sender, null)) { + if (errors.tryAddThrowableOrReport(error)) { + if (delayErrors) { + if (done) { + errors.tryTerminateConsumer(downstream); + } + } else { + upstream.cancel(); + disposeInner(); + errors.tryTerminateConsumer(downstream); + } + } + } else { + RxJavaPlugins.onError(error); + } + } + + void innerComplete(SwitchMapInnerObserver sender) { + if (inner.compareAndSet(sender, null)) { + if (done) { + errors.tryTerminateConsumer(downstream); + } + } + } + + static final class SwitchMapInnerObserver extends AtomicReference<Disposable> + implements CompletableObserver { + + private static final long serialVersionUID = -8003404460084760287L; + + final SwitchMapCompletableObserver<?> parent; + + SwitchMapInnerObserver(SwitchMapCompletableObserver<?> parent) { + this.parent = parent; + } + + @Override + public void onSubscribe(Disposable d) { + DisposableHelper.setOnce(this, d); + } + + @Override + public void onError(Throwable e) { + parent.innerError(this, e); + } + + @Override + public void onComplete() { + parent.innerComplete(this); + } + + void dispose() { + DisposableHelper.dispose(this); + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/mixed/FlowableSwitchMapCompletablePublisher.java b/src/main/java/io/reactivex/rxjava3/internal/operators/mixed/FlowableSwitchMapCompletablePublisher.java new file mode 100644 index 0000000000..9eae52029e --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/mixed/FlowableSwitchMapCompletablePublisher.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.mixed; + +import org.reactivestreams.Publisher; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.functions.Function; + +/** + * Switch between subsequent {@link CompletableSource}s emitted by a {@link Publisher}. + * Reuses {@link FlowableSwitchMapCompletable} internals. + * @param <T> the upstream value type + * @since 3.0.0 + */ +public final class FlowableSwitchMapCompletablePublisher<T> extends Completable { + + final Publisher<T> source; + + final Function<? super T, ? extends CompletableSource> mapper; + + final boolean delayErrors; + + public FlowableSwitchMapCompletablePublisher(Publisher<T> source, + Function<? super T, ? extends CompletableSource> mapper, boolean delayErrors) { + this.source = source; + this.mapper = mapper; + this.delayErrors = delayErrors; + } + + @Override + protected void subscribeActual(CompletableObserver observer) { + source.subscribe(new FlowableSwitchMapCompletable.SwitchMapCompletableObserver<>(observer, mapper, delayErrors)); + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/mixed/FlowableSwitchMapMaybe.java b/src/main/java/io/reactivex/rxjava3/internal/operators/mixed/FlowableSwitchMapMaybe.java new file mode 100644 index 0000000000..3eab4e0d62 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/mixed/FlowableSwitchMapMaybe.java @@ -0,0 +1,294 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.mixed; + +import java.util.Objects; +import java.util.concurrent.atomic.*; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; +import io.reactivex.rxjava3.internal.subscriptions.SubscriptionHelper; +import io.reactivex.rxjava3.internal.util.*; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +/** + * Maps the upstream items into {@link MaybeSource}s and switches (subscribes) to the newer ones + * while disposing the older ones and emits the latest success value if available, optionally delaying + * errors from the main source or the inner sources. + * <p>History: 2.1.11 - experimental + * @param <T> the upstream value type + * @param <R> the downstream value type + * @since 2.2 + */ +public final class FlowableSwitchMapMaybe<T, R> extends Flowable<R> { + + final Flowable<T> source; + + final Function<? super T, ? extends MaybeSource<? extends R>> mapper; + + final boolean delayErrors; + + public FlowableSwitchMapMaybe(Flowable<T> source, + Function<? super T, ? extends MaybeSource<? extends R>> mapper, + boolean delayErrors) { + this.source = source; + this.mapper = mapper; + this.delayErrors = delayErrors; + } + + @Override + protected void subscribeActual(Subscriber<? super R> s) { + source.subscribe(new SwitchMapMaybeSubscriber<>(s, mapper, delayErrors)); + } + + static final class SwitchMapMaybeSubscriber<T, R> extends AtomicInteger + implements FlowableSubscriber<T>, Subscription { + + private static final long serialVersionUID = -5402190102429853762L; + + final Subscriber<? super R> downstream; + + final Function<? super T, ? extends MaybeSource<? extends R>> mapper; + + final boolean delayErrors; + + final AtomicThrowable errors; + + final AtomicLong requested; + + final AtomicReference<SwitchMapMaybeObserver<R>> inner; + + static final SwitchMapMaybeObserver<Object> INNER_DISPOSED = + new SwitchMapMaybeObserver<>(null); + + Subscription upstream; + + volatile boolean done; + + volatile boolean cancelled; + + long emitted; + + SwitchMapMaybeSubscriber(Subscriber<? super R> downstream, + Function<? super T, ? extends MaybeSource<? extends R>> mapper, + boolean delayErrors) { + this.downstream = downstream; + this.mapper = mapper; + this.delayErrors = delayErrors; + this.errors = new AtomicThrowable(); + this.requested = new AtomicLong(); + this.inner = new AtomicReference<>(); + } + + @Override + public void onSubscribe(Subscription s) { + if (SubscriptionHelper.validate(upstream, s)) { + upstream = s; + downstream.onSubscribe(this); + s.request(Long.MAX_VALUE); + } + } + + @Override + @SuppressWarnings({ "unchecked", "rawtypes" }) + public void onNext(T t) { + SwitchMapMaybeObserver<R> current = inner.get(); + if (current != null) { + current.dispose(); + } + + MaybeSource<? extends R> ms; + + try { + ms = Objects.requireNonNull(mapper.apply(t), "The mapper returned a null MaybeSource"); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + upstream.cancel(); + inner.getAndSet((SwitchMapMaybeObserver)INNER_DISPOSED); + onError(ex); + return; + } + + SwitchMapMaybeObserver<R> observer = new SwitchMapMaybeObserver<>(this); + + for (;;) { + current = inner.get(); + if (current == INNER_DISPOSED) { + break; + } + if (inner.compareAndSet(current, observer)) { + ms.subscribe(observer); + break; + } + } + } + + @Override + public void onError(Throwable t) { + if (errors.tryAddThrowableOrReport(t)) { + if (!delayErrors) { + disposeInner(); + } + done = true; + drain(); + } + } + + @Override + public void onComplete() { + done = true; + drain(); + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + void disposeInner() { + SwitchMapMaybeObserver<R> current = inner.getAndSet((SwitchMapMaybeObserver)INNER_DISPOSED); + if (current != null && current != INNER_DISPOSED) { + current.dispose(); + } + } + + @Override + public void request(long n) { + BackpressureHelper.add(requested, n); + drain(); + } + + @Override + public void cancel() { + cancelled = true; + upstream.cancel(); + disposeInner(); + errors.tryTerminateAndReport(); + } + + void innerError(SwitchMapMaybeObserver<R> sender, Throwable ex) { + if (inner.compareAndSet(sender, null)) { + if (errors.tryAddThrowableOrReport(ex)) { + if (!delayErrors) { + upstream.cancel(); + disposeInner(); + } + drain(); + } + } else { + RxJavaPlugins.onError(ex); + } + } + + void innerComplete(SwitchMapMaybeObserver<R> sender) { + if (inner.compareAndSet(sender, null)) { + drain(); + } + } + + void drain() { + if (getAndIncrement() != 0) { + return; + } + + int missed = 1; + Subscriber<? super R> downstream = this.downstream; + AtomicThrowable errors = this.errors; + AtomicReference<SwitchMapMaybeObserver<R>> inner = this.inner; + AtomicLong requested = this.requested; + long emitted = this.emitted; + + for (;;) { + + for (;;) { + if (cancelled) { + return; + } + + if (errors.get() != null) { + if (!delayErrors) { + errors.tryTerminateConsumer(downstream); + return; + } + } + + boolean d = done; + SwitchMapMaybeObserver<R> current = inner.get(); + boolean empty = current == null; + + if (d && empty) { + errors.tryTerminateConsumer(downstream); + return; + } + + if (empty || current.item == null || emitted == requested.get()) { + break; + } + + inner.compareAndSet(current, null); + + downstream.onNext(current.item); + + emitted++; + } + + this.emitted = emitted; + missed = addAndGet(-missed); + if (missed == 0) { + break; + } + } + } + + static final class SwitchMapMaybeObserver<R> + extends AtomicReference<Disposable> implements MaybeObserver<R> { + + private static final long serialVersionUID = 8042919737683345351L; + + final SwitchMapMaybeSubscriber<?, R> parent; + + volatile R item; + + SwitchMapMaybeObserver(SwitchMapMaybeSubscriber<?, R> parent) { + this.parent = parent; + } + + @Override + public void onSubscribe(Disposable d) { + DisposableHelper.setOnce(this, d); + } + + @Override + public void onSuccess(R t) { + item = t; + parent.drain(); + } + + @Override + public void onError(Throwable e) { + parent.innerError(this, e); + } + + @Override + public void onComplete() { + parent.innerComplete(this); + } + + void dispose() { + DisposableHelper.dispose(this); + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/mixed/FlowableSwitchMapMaybePublisher.java b/src/main/java/io/reactivex/rxjava3/internal/operators/mixed/FlowableSwitchMapMaybePublisher.java new file mode 100644 index 0000000000..319247c295 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/mixed/FlowableSwitchMapMaybePublisher.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.mixed; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.functions.Function; + +/** + * Switch between subsequent {@link MaybeSource}s emitted by a {@link Publisher}. + * Reuses {@link FlowableSwitchMapMaybe} internals. + * @param <T> the upstream value type + * @param <R> the downstream value type + * @since 3.0.0 + */ +public final class FlowableSwitchMapMaybePublisher<T, R> extends Flowable<R> { + + final Publisher<T> source; + + final Function<? super T, ? extends MaybeSource<? extends R>> mapper; + + final boolean delayErrors; + + public FlowableSwitchMapMaybePublisher(Publisher<T> source, + Function<? super T, ? extends MaybeSource<? extends R>> mapper, + boolean delayErrors) { + this.source = source; + this.mapper = mapper; + this.delayErrors = delayErrors; + } + + @Override + protected void subscribeActual(Subscriber<? super R> s) { + source.subscribe(new FlowableSwitchMapMaybe.SwitchMapMaybeSubscriber<>(s, mapper, delayErrors)); + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/mixed/FlowableSwitchMapSingle.java b/src/main/java/io/reactivex/rxjava3/internal/operators/mixed/FlowableSwitchMapSingle.java new file mode 100644 index 0000000000..59c9cb7f8f --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/mixed/FlowableSwitchMapSingle.java @@ -0,0 +1,283 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.mixed; + +import java.util.Objects; +import java.util.concurrent.atomic.*; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; +import io.reactivex.rxjava3.internal.subscriptions.SubscriptionHelper; +import io.reactivex.rxjava3.internal.util.*; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +/** + * Maps the upstream items into {@link SingleSource}s and switches (subscribes) to the newer ones + * while disposing the older ones and emits the latest success value, optionally delaying + * errors from the main source or the inner sources. + * <p>History: 2.1.11 - experimental + * @param <T> the upstream value type + * @param <R> the downstream value type + * @since 2.2 + */ +public final class FlowableSwitchMapSingle<T, R> extends Flowable<R> { + + final Flowable<T> source; + + final Function<? super T, ? extends SingleSource<? extends R>> mapper; + + final boolean delayErrors; + + public FlowableSwitchMapSingle(Flowable<T> source, + Function<? super T, ? extends SingleSource<? extends R>> mapper, + boolean delayErrors) { + this.source = source; + this.mapper = mapper; + this.delayErrors = delayErrors; + } + + @Override + protected void subscribeActual(Subscriber<? super R> s) { + source.subscribe(new SwitchMapSingleSubscriber<>(s, mapper, delayErrors)); + } + + static final class SwitchMapSingleSubscriber<T, R> extends AtomicInteger + implements FlowableSubscriber<T>, Subscription { + + private static final long serialVersionUID = -5402190102429853762L; + + final Subscriber<? super R> downstream; + + final Function<? super T, ? extends SingleSource<? extends R>> mapper; + + final boolean delayErrors; + + final AtomicThrowable errors; + + final AtomicLong requested; + + final AtomicReference<SwitchMapSingleObserver<R>> inner; + + static final SwitchMapSingleObserver<Object> INNER_DISPOSED = + new SwitchMapSingleObserver<>(null); + + Subscription upstream; + + volatile boolean done; + + volatile boolean cancelled; + + long emitted; + + SwitchMapSingleSubscriber(Subscriber<? super R> downstream, + Function<? super T, ? extends SingleSource<? extends R>> mapper, + boolean delayErrors) { + this.downstream = downstream; + this.mapper = mapper; + this.delayErrors = delayErrors; + this.errors = new AtomicThrowable(); + this.requested = new AtomicLong(); + this.inner = new AtomicReference<>(); + } + + @Override + public void onSubscribe(Subscription s) { + if (SubscriptionHelper.validate(upstream, s)) { + upstream = s; + downstream.onSubscribe(this); + s.request(Long.MAX_VALUE); + } + } + + @Override + @SuppressWarnings({ "unchecked", "rawtypes" }) + public void onNext(T t) { + SwitchMapSingleObserver<R> current = inner.get(); + if (current != null) { + current.dispose(); + } + + SingleSource<? extends R> ss; + + try { + ss = Objects.requireNonNull(mapper.apply(t), "The mapper returned a null SingleSource"); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + upstream.cancel(); + inner.getAndSet((SwitchMapSingleObserver)INNER_DISPOSED); + onError(ex); + return; + } + + SwitchMapSingleObserver<R> observer = new SwitchMapSingleObserver<>(this); + + for (;;) { + current = inner.get(); + if (current == INNER_DISPOSED) { + break; + } + if (inner.compareAndSet(current, observer)) { + ss.subscribe(observer); + break; + } + } + } + + @Override + public void onError(Throwable t) { + if (errors.tryAddThrowableOrReport(t)) { + if (!delayErrors) { + disposeInner(); + } + done = true; + drain(); + } + } + + @Override + public void onComplete() { + done = true; + drain(); + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + void disposeInner() { + SwitchMapSingleObserver<R> current = inner.getAndSet((SwitchMapSingleObserver)INNER_DISPOSED); + if (current != null && current != INNER_DISPOSED) { + current.dispose(); + } + } + + @Override + public void request(long n) { + BackpressureHelper.add(requested, n); + drain(); + } + + @Override + public void cancel() { + cancelled = true; + upstream.cancel(); + disposeInner(); + errors.tryTerminateAndReport(); + } + + void innerError(SwitchMapSingleObserver<R> sender, Throwable ex) { + if (inner.compareAndSet(sender, null)) { + if (errors.tryAddThrowableOrReport(ex)) { + if (!delayErrors) { + upstream.cancel(); + disposeInner(); + } + drain(); + } + } else { + RxJavaPlugins.onError(ex); + } + } + + void drain() { + if (getAndIncrement() != 0) { + return; + } + + int missed = 1; + Subscriber<? super R> downstream = this.downstream; + AtomicThrowable errors = this.errors; + AtomicReference<SwitchMapSingleObserver<R>> inner = this.inner; + AtomicLong requested = this.requested; + long emitted = this.emitted; + + for (;;) { + + for (;;) { + if (cancelled) { + return; + } + + if (errors.get() != null) { + if (!delayErrors) { + errors.tryTerminateConsumer(downstream); + return; + } + } + + boolean d = done; + SwitchMapSingleObserver<R> current = inner.get(); + boolean empty = current == null; + + if (d && empty) { + errors.tryTerminateConsumer(downstream); + return; + } + + if (empty || current.item == null || emitted == requested.get()) { + break; + } + + inner.compareAndSet(current, null); + + downstream.onNext(current.item); + + emitted++; + } + + this.emitted = emitted; + missed = addAndGet(-missed); + if (missed == 0) { + break; + } + } + } + + static final class SwitchMapSingleObserver<R> + extends AtomicReference<Disposable> implements SingleObserver<R> { + + private static final long serialVersionUID = 8042919737683345351L; + + final SwitchMapSingleSubscriber<?, R> parent; + + volatile R item; + + SwitchMapSingleObserver(SwitchMapSingleSubscriber<?, R> parent) { + this.parent = parent; + } + + @Override + public void onSubscribe(Disposable d) { + DisposableHelper.setOnce(this, d); + } + + @Override + public void onSuccess(R t) { + item = t; + parent.drain(); + } + + @Override + public void onError(Throwable e) { + parent.innerError(this, e); + } + + void dispose() { + DisposableHelper.dispose(this); + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/mixed/FlowableSwitchMapSinglePublisher.java b/src/main/java/io/reactivex/rxjava3/internal/operators/mixed/FlowableSwitchMapSinglePublisher.java new file mode 100644 index 0000000000..31ace1bf2d --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/mixed/FlowableSwitchMapSinglePublisher.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.mixed; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.functions.Function; + +/** + * Switch between subsequent {@link SingleSource}s emitted by a {@link Publisher}. + * Reuses {@link FlowableSwitchMapSingle} internals. + * @param <T> the upstream value type + * @param <R> the downstream value type + * @since 3.0.0 + */ +public final class FlowableSwitchMapSinglePublisher<T, R> extends Flowable<R> { + + final Publisher<T> source; + + final Function<? super T, ? extends SingleSource<? extends R>> mapper; + + final boolean delayErrors; + + public FlowableSwitchMapSinglePublisher(Publisher<T> source, + Function<? super T, ? extends SingleSource<? extends R>> mapper, + boolean delayErrors) { + this.source = source; + this.mapper = mapper; + this.delayErrors = delayErrors; + } + + @Override + protected void subscribeActual(Subscriber<? super R> s) { + source.subscribe(new FlowableSwitchMapSingle.SwitchMapSingleSubscriber<>(s, mapper, delayErrors)); + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/mixed/MaterializeSingleObserver.java b/src/main/java/io/reactivex/rxjava3/internal/operators/mixed/MaterializeSingleObserver.java new file mode 100644 index 0000000000..3f1c540d7d --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/mixed/MaterializeSingleObserver.java @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.mixed; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; + +/** + * A consumer that implements the consumer types of Maybe, Single and Completable + * and turns their signals into Notifications for a SingleObserver. + * <p>History: 2.2.4 - experimental + * @param <T> the element type of the source + * @since 3.0.0 + */ +public final class MaterializeSingleObserver<T> +implements SingleObserver<T>, MaybeObserver<T>, CompletableObserver, Disposable { + + final SingleObserver<? super Notification<T>> downstream; + + Disposable upstream; + + public MaterializeSingleObserver(SingleObserver<? super Notification<T>> downstream) { + this.downstream = downstream; + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.validate(upstream, d)) { + this.upstream = d; + downstream.onSubscribe(this); + } + } + + @Override + public void onComplete() { + downstream.onSuccess(Notification.createOnComplete()); + } + + @Override + public void onSuccess(T t) { + downstream.onSuccess(Notification.createOnNext(t)); + } + + @Override + public void onError(Throwable e) { + downstream.onSuccess(Notification.createOnError(e)); + } + + @Override + public boolean isDisposed() { + return upstream.isDisposed(); + } + + @Override + public void dispose() { + upstream.dispose(); + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/mixed/MaybeFlatMapObservable.java b/src/main/java/io/reactivex/rxjava3/internal/operators/mixed/MaybeFlatMapObservable.java new file mode 100644 index 0000000000..09e634f944 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/mixed/MaybeFlatMapObservable.java @@ -0,0 +1,115 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.mixed; + +import java.util.Objects; +import java.util.concurrent.atomic.AtomicReference; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; + +/** + * Maps the success value of a Maybe onto an ObservableSource and + * relays its signals to the downstream observer. + * + * @param <T> the success value type of the Maybe source + * @param <R> the result type of the ObservableSource and this operator + * @since 2.1.15 + */ +public final class MaybeFlatMapObservable<T, R> extends Observable<R> { + + final MaybeSource<T> source; + + final Function<? super T, ? extends ObservableSource<? extends R>> mapper; + + public MaybeFlatMapObservable(MaybeSource<T> source, + Function<? super T, ? extends ObservableSource<? extends R>> mapper) { + this.source = source; + this.mapper = mapper; + } + + @Override + protected void subscribeActual(Observer<? super R> observer) { + FlatMapObserver<T, R> parent = new FlatMapObserver<>(observer, mapper); + observer.onSubscribe(parent); + source.subscribe(parent); + } + + static final class FlatMapObserver<T, R> + extends AtomicReference<Disposable> + implements Observer<R>, MaybeObserver<T>, Disposable { + + private static final long serialVersionUID = -8948264376121066672L; + + final Observer<? super R> downstream; + + final Function<? super T, ? extends ObservableSource<? extends R>> mapper; + + FlatMapObserver(Observer<? super R> downstream, Function<? super T, ? extends ObservableSource<? extends R>> mapper) { + this.downstream = downstream; + this.mapper = mapper; + } + + @Override + public void onNext(R t) { + downstream.onNext(t); + } + + @Override + public void onError(Throwable t) { + downstream.onError(t); + } + + @Override + public void onComplete() { + downstream.onComplete(); + } + + @Override + public void dispose() { + DisposableHelper.dispose(this); + } + + @Override + public boolean isDisposed() { + return DisposableHelper.isDisposed(get()); + } + + @Override + public void onSubscribe(Disposable d) { + DisposableHelper.replace(this, d); + } + + @Override + public void onSuccess(T t) { + ObservableSource<? extends R> o; + + try { + o = Objects.requireNonNull(mapper.apply(t), "The mapper returned a null Publisher"); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + downstream.onError(ex); + return; + } + + if (!isDisposed()) { + o.subscribe(this); + } + } + + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/mixed/MaybeFlatMapPublisher.java b/src/main/java/io/reactivex/rxjava3/internal/operators/mixed/MaybeFlatMapPublisher.java new file mode 100644 index 0000000000..b6d2afeb2d --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/mixed/MaybeFlatMapPublisher.java @@ -0,0 +1,129 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.mixed; + +import java.util.Objects; +import java.util.concurrent.atomic.*; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; +import io.reactivex.rxjava3.internal.subscriptions.SubscriptionHelper; + +/** + * Maps the success value of a Maybe onto a Publisher and + * relays its signals to the downstream subscriber. + * + * @param <T> the success value type of the Maybe source + * @param <R> the result type of the Publisher and this operator + * @since 2.1.15 + */ +public final class MaybeFlatMapPublisher<T, R> extends Flowable<R> { + + final MaybeSource<T> source; + + final Function<? super T, ? extends Publisher<? extends R>> mapper; + + public MaybeFlatMapPublisher(MaybeSource<T> source, + Function<? super T, ? extends Publisher<? extends R>> mapper) { + this.source = source; + this.mapper = mapper; + } + + @Override + protected void subscribeActual(Subscriber<? super R> s) { + source.subscribe(new FlatMapPublisherSubscriber<>(s, mapper)); + } + + static final class FlatMapPublisherSubscriber<T, R> + extends AtomicReference<Subscription> + implements FlowableSubscriber<R>, MaybeObserver<T>, Subscription { + + private static final long serialVersionUID = -8948264376121066672L; + + final Subscriber<? super R> downstream; + + final Function<? super T, ? extends Publisher<? extends R>> mapper; + + Disposable upstream; + + final AtomicLong requested; + + FlatMapPublisherSubscriber(Subscriber<? super R> downstream, Function<? super T, ? extends Publisher<? extends R>> mapper) { + this.downstream = downstream; + this.mapper = mapper; + this.requested = new AtomicLong(); + } + + @Override + public void onNext(R t) { + downstream.onNext(t); + } + + @Override + public void onError(Throwable t) { + downstream.onError(t); + } + + @Override + public void onComplete() { + downstream.onComplete(); + } + + @Override + public void request(long n) { + SubscriptionHelper.deferredRequest(this, requested, n); + } + + @Override + public void cancel() { + upstream.dispose(); + SubscriptionHelper.cancel(this); + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.validate(upstream, d)) { + this.upstream = d; + downstream.onSubscribe(this); + } + } + + @Override + public void onSuccess(T t) { + Publisher<? extends R> p; + + try { + p = Objects.requireNonNull(mapper.apply(t), "The mapper returned a null Publisher"); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + downstream.onError(ex); + return; + } + + if (get() != SubscriptionHelper.CANCELLED) { + p.subscribe(this); + } + } + + @Override + public void onSubscribe(Subscription s) { + SubscriptionHelper.deferredSetOnce(this, requested, s); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/mixed/ObservableConcatMapCompletable.java b/src/main/java/io/reactivex/rxjava3/internal/operators/mixed/ObservableConcatMapCompletable.java new file mode 100644 index 0000000000..f9d91b170a --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/mixed/ObservableConcatMapCompletable.java @@ -0,0 +1,200 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.mixed; + +import java.util.Objects; +import java.util.concurrent.atomic.AtomicReference; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; +import io.reactivex.rxjava3.internal.util.*; +import io.reactivex.rxjava3.operators.SimpleQueue; + +/** + * Maps the upstream items into {@link CompletableSource}s and subscribes to them one after the + * other completes or terminates (in error-delaying mode). + * <p>History: 2.1.11 - experimental + * @param <T> the upstream value type + * @since 2.2 + */ +public final class ObservableConcatMapCompletable<T> extends Completable { + + final Observable<T> source; + + final Function<? super T, ? extends CompletableSource> mapper; + + final ErrorMode errorMode; + + final int prefetch; + + public ObservableConcatMapCompletable(Observable<T> source, + Function<? super T, ? extends CompletableSource> mapper, + ErrorMode errorMode, + int prefetch) { + this.source = source; + this.mapper = mapper; + this.errorMode = errorMode; + this.prefetch = prefetch; + } + + @Override + protected void subscribeActual(CompletableObserver observer) { + if (!ScalarXMapZHelper.tryAsCompletable(source, mapper, observer)) { + source.subscribe(new ConcatMapCompletableObserver<>(observer, mapper, errorMode, prefetch)); + } + } + + static final class ConcatMapCompletableObserver<T> + extends ConcatMapXMainObserver<T> { + + private static final long serialVersionUID = 3610901111000061034L; + + final CompletableObserver downstream; + + final Function<? super T, ? extends CompletableSource> mapper; + + final ConcatMapInnerObserver inner; + + volatile boolean active; + + ConcatMapCompletableObserver(CompletableObserver downstream, + Function<? super T, ? extends CompletableSource> mapper, + ErrorMode errorMode, int prefetch) { + super(prefetch, errorMode); + this.downstream = downstream; + this.mapper = mapper; + this.inner = new ConcatMapInnerObserver(this); + } + + @Override + void onSubscribeDownstream() { + downstream.onSubscribe(this); + } + + @Override + void disposeInner() { + inner.dispose(); + } + + void innerError(Throwable ex) { + if (errors.tryAddThrowableOrReport(ex)) { + if (errorMode != ErrorMode.END) { + upstream.dispose(); + } + active = false; + drain(); + } + } + + void innerComplete() { + active = false; + drain(); + } + + @Override + void drain() { + if (getAndIncrement() != 0) { + return; + } + + AtomicThrowable errors = this.errors; + ErrorMode errorMode = this.errorMode; + SimpleQueue<T> queue = this.queue; + + do { + if (disposed) { + queue.clear(); + return; + } + + if (errors.get() != null) { + if (errorMode == ErrorMode.IMMEDIATE + || (errorMode == ErrorMode.BOUNDARY && !active)) { + disposed = true; + queue.clear(); + errors.tryTerminateConsumer(downstream); + return; + } + } + + if (!active) { + + boolean d = done; + boolean empty = true; + CompletableSource cs = null; + try { + T v = queue.poll(); + if (v != null) { + cs = Objects.requireNonNull(mapper.apply(v), "The mapper returned a null CompletableSource"); + empty = false; + } + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + disposed = true; + queue.clear(); + upstream.dispose(); + errors.tryAddThrowableOrReport(ex); + errors.tryTerminateConsumer(downstream); + return; + } + + if (d && empty) { + disposed = true; + errors.tryTerminateConsumer(downstream); + return; + } + + if (!empty) { + active = true; + cs.subscribe(inner); + } + } + } while (decrementAndGet() != 0); + } + + static final class ConcatMapInnerObserver extends AtomicReference<Disposable> + implements CompletableObserver { + + private static final long serialVersionUID = 5638352172918776687L; + + final ConcatMapCompletableObserver<?> parent; + + ConcatMapInnerObserver(ConcatMapCompletableObserver<?> parent) { + this.parent = parent; + } + + @Override + public void onSubscribe(Disposable d) { + DisposableHelper.replace(this, d); + } + + @Override + public void onError(Throwable e) { + parent.innerError(e); + } + + @Override + public void onComplete() { + parent.innerComplete(); + } + + void dispose() { + DisposableHelper.dispose(this); + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/mixed/ObservableConcatMapMaybe.java b/src/main/java/io/reactivex/rxjava3/internal/operators/mixed/ObservableConcatMapMaybe.java new file mode 100644 index 0000000000..7e85104dd7 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/mixed/ObservableConcatMapMaybe.java @@ -0,0 +1,258 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.mixed; + +import java.util.Objects; +import java.util.concurrent.atomic.AtomicReference; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; +import io.reactivex.rxjava3.internal.util.*; +import io.reactivex.rxjava3.operators.SimpleQueue; + +/** + * Maps each upstream item into a {@link MaybeSource}, subscribes to them one after the other terminates + * and relays their success values, optionally delaying any errors till the main and inner sources + * terminate. + * <p>History: 2.1.11 - experimental + * @param <T> the upstream element type + * @param <R> the output element type + * @since 2.2 + */ +public final class ObservableConcatMapMaybe<T, R> extends Observable<R> { + + final Observable<T> source; + + final Function<? super T, ? extends MaybeSource<? extends R>> mapper; + + final ErrorMode errorMode; + + final int prefetch; + + public ObservableConcatMapMaybe(Observable<T> source, + Function<? super T, ? extends MaybeSource<? extends R>> mapper, + ErrorMode errorMode, int prefetch) { + this.source = source; + this.mapper = mapper; + this.errorMode = errorMode; + this.prefetch = prefetch; + } + + @Override + protected void subscribeActual(Observer<? super R> observer) { + if (!ScalarXMapZHelper.tryAsMaybe(source, mapper, observer)) { + source.subscribe(new ConcatMapMaybeMainObserver<>(observer, mapper, prefetch, errorMode)); + } + } + + static final class ConcatMapMaybeMainObserver<T, R> + extends ConcatMapXMainObserver<T> { + + private static final long serialVersionUID = -9140123220065488293L; + + final Observer<? super R> downstream; + + final Function<? super T, ? extends MaybeSource<? extends R>> mapper; + + final ConcatMapMaybeObserver<R> inner; + + R item; + + volatile int state; + + /** No inner MaybeSource is running. */ + static final int STATE_INACTIVE = 0; + /** An inner MaybeSource is running but there are no results yet. */ + static final int STATE_ACTIVE = 1; + /** The inner MaybeSource succeeded with a value in {@link #item}. */ + static final int STATE_RESULT_VALUE = 2; + + ConcatMapMaybeMainObserver(Observer<? super R> downstream, + Function<? super T, ? extends MaybeSource<? extends R>> mapper, + int prefetch, ErrorMode errorMode) { + super(prefetch, errorMode); + this.downstream = downstream; + this.mapper = mapper; + this.inner = new ConcatMapMaybeObserver<>(this); + } + + @Override + void onSubscribeDownstream() { + downstream.onSubscribe(this); + } + + @Override + void clearValue() { + item = null; + } + + void innerSuccess(R item) { + this.item = item; + this.state = STATE_RESULT_VALUE; + drain(); + } + + void innerComplete() { + this.state = STATE_INACTIVE; + drain(); + } + + void innerError(Throwable ex) { + if (errors.tryAddThrowableOrReport(ex)) { + if (errorMode != ErrorMode.END) { + upstream.dispose(); + } + this.state = STATE_INACTIVE; + drain(); + } + } + + @Override + void disposeInner() { + inner.dispose(); + } + + @Override + void drain() { + if (getAndIncrement() != 0) { + return; + } + + int missed = 1; + Observer<? super R> downstream = this.downstream; + ErrorMode errorMode = this.errorMode; + SimpleQueue<T> queue = this.queue; + AtomicThrowable errors = this.errors; + + for (;;) { + + for (;;) { + if (disposed) { + queue.clear(); + item = null; + break; + } + + int s = state; + + if (errors.get() != null) { + if (errorMode == ErrorMode.IMMEDIATE + || (errorMode == ErrorMode.BOUNDARY && s == STATE_INACTIVE)) { + queue.clear(); + item = null; + errors.tryTerminateConsumer(downstream); + return; + } + } + + if (s == STATE_INACTIVE) { + boolean d = done; + T v; + + try { + v = queue.poll(); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + disposed = true; + upstream.dispose(); + errors.tryAddThrowableOrReport(ex); + errors.tryTerminateConsumer(downstream); + return; + } + boolean empty = v == null; + + if (d && empty) { + errors.tryTerminateConsumer(downstream); + return; + } + + if (empty) { + break; + } + + MaybeSource<? extends R> ms; + + try { + ms = Objects.requireNonNull(mapper.apply(v), "The mapper returned a null MaybeSource"); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + upstream.dispose(); + queue.clear(); + errors.tryAddThrowableOrReport(ex); + errors.tryTerminateConsumer(downstream); + return; + } + + state = STATE_ACTIVE; + ms.subscribe(inner); + break; + } else if (s == STATE_RESULT_VALUE) { + R w = item; + item = null; + downstream.onNext(w); + + state = STATE_INACTIVE; + } else { + break; + } + } + + missed = addAndGet(-missed); + if (missed == 0) { + break; + } + } + } + + static final class ConcatMapMaybeObserver<R> + extends AtomicReference<Disposable> + implements MaybeObserver<R> { + + private static final long serialVersionUID = -3051469169682093892L; + + final ConcatMapMaybeMainObserver<?, R> parent; + + ConcatMapMaybeObserver(ConcatMapMaybeMainObserver<?, R> parent) { + this.parent = parent; + } + + @Override + public void onSubscribe(Disposable d) { + DisposableHelper.replace(this, d); + } + + @Override + public void onSuccess(R t) { + parent.innerSuccess(t); + } + + @Override + public void onError(Throwable e) { + parent.innerError(e); + } + + @Override + public void onComplete() { + parent.innerComplete(); + } + + void dispose() { + DisposableHelper.dispose(this); + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/mixed/ObservableConcatMapSingle.java b/src/main/java/io/reactivex/rxjava3/internal/operators/mixed/ObservableConcatMapSingle.java new file mode 100644 index 0000000000..b57940a5e7 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/mixed/ObservableConcatMapSingle.java @@ -0,0 +1,248 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.mixed; + +import java.util.Objects; +import java.util.concurrent.atomic.AtomicReference; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; +import io.reactivex.rxjava3.internal.util.*; +import io.reactivex.rxjava3.operators.SimpleQueue; + +/** + * Maps each upstream item into a {@link SingleSource}, subscribes to them one after the other terminates + * and relays their success values, optionally delaying any errors till the main and inner sources + * terminate. + * <p>History: 2.1.11 - experimental + * @param <T> the upstream element type + * @param <R> the output element type + * @since 2.2 + */ +public final class ObservableConcatMapSingle<T, R> extends Observable<R> { + + final ObservableSource<T> source; + + final Function<? super T, ? extends SingleSource<? extends R>> mapper; + + final ErrorMode errorMode; + + final int prefetch; + + public ObservableConcatMapSingle(ObservableSource<T> source, + Function<? super T, ? extends SingleSource<? extends R>> mapper, + ErrorMode errorMode, int prefetch) { + this.source = source; + this.mapper = mapper; + this.errorMode = errorMode; + this.prefetch = prefetch; + } + + @Override + protected void subscribeActual(Observer<? super R> observer) { + if (!ScalarXMapZHelper.tryAsSingle(source, mapper, observer)) { + source.subscribe(new ConcatMapSingleMainObserver<>(observer, mapper, prefetch, errorMode)); + } + } + + static final class ConcatMapSingleMainObserver<T, R> + extends ConcatMapXMainObserver<T> { + + private static final long serialVersionUID = -9140123220065488293L; + + final Observer<? super R> downstream; + + final Function<? super T, ? extends SingleSource<? extends R>> mapper; + + final ConcatMapSingleObserver<R> inner; + + R item; + + volatile int state; + + /** No inner SingleSource is running. */ + static final int STATE_INACTIVE = 0; + /** An inner SingleSource is running but there are no results yet. */ + static final int STATE_ACTIVE = 1; + /** The inner SingleSource succeeded with a value in {@link #item}. */ + static final int STATE_RESULT_VALUE = 2; + + ConcatMapSingleMainObserver(Observer<? super R> downstream, + Function<? super T, ? extends SingleSource<? extends R>> mapper, + int prefetch, ErrorMode errorMode) { + super(prefetch, errorMode); + this.downstream = downstream; + this.mapper = mapper; + this.inner = new ConcatMapSingleObserver<>(this); + } + + void innerSuccess(R item) { + this.item = item; + this.state = STATE_RESULT_VALUE; + drain(); + } + + void innerError(Throwable ex) { + if (errors.tryAddThrowableOrReport(ex)) { + if (errorMode != ErrorMode.END) { + upstream.dispose(); + } + this.state = STATE_INACTIVE; + drain(); + } + } + + @Override + void disposeInner() { + inner.dispose(); + } + + @Override + void onSubscribeDownstream() { + downstream.onSubscribe(this); + } + + @Override + void clearValue() { + item = null; + } + + @Override + void drain() { + if (getAndIncrement() != 0) { + return; + } + + int missed = 1; + Observer<? super R> downstream = this.downstream; + ErrorMode errorMode = this.errorMode; + SimpleQueue<T> queue = this.queue; + AtomicThrowable errors = this.errors; + + for (;;) { + + for (;;) { + if (disposed) { + queue.clear(); + item = null; + break; + } + + int s = state; + + if (errors.get() != null) { + if (errorMode == ErrorMode.IMMEDIATE + || (errorMode == ErrorMode.BOUNDARY && s == STATE_INACTIVE)) { + queue.clear(); + item = null; + errors.tryTerminateConsumer(downstream); + return; + } + } + + if (s == STATE_INACTIVE) { + boolean d = done; + T v; + + try { + v = queue.poll(); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + disposed = true; + upstream.dispose(); + errors.tryAddThrowableOrReport(ex); + errors.tryTerminateConsumer(downstream); + return; + } + boolean empty = v == null; + + if (d && empty) { + errors.tryTerminateConsumer(downstream); + return; + } + + if (empty) { + break; + } + + SingleSource<? extends R> ss; + + try { + ss = Objects.requireNonNull(mapper.apply(v), "The mapper returned a null SingleSource"); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + upstream.dispose(); + queue.clear(); + errors.tryAddThrowableOrReport(ex); + errors.tryTerminateConsumer(downstream); + return; + } + + state = STATE_ACTIVE; + ss.subscribe(inner); + break; + } else if (s == STATE_RESULT_VALUE) { + R w = item; + item = null; + downstream.onNext(w); + + state = STATE_INACTIVE; + } else { + break; + } + } + + missed = addAndGet(-missed); + if (missed == 0) { + break; + } + } + } + + static final class ConcatMapSingleObserver<R> + extends AtomicReference<Disposable> + implements SingleObserver<R> { + + private static final long serialVersionUID = -3051469169682093892L; + + final ConcatMapSingleMainObserver<?, R> parent; + + ConcatMapSingleObserver(ConcatMapSingleMainObserver<?, R> parent) { + this.parent = parent; + } + + @Override + public void onSubscribe(Disposable d) { + DisposableHelper.replace(this, d); + } + + @Override + public void onSuccess(R t) { + parent.innerSuccess(t); + } + + @Override + public void onError(Throwable e) { + parent.innerError(e); + } + + void dispose() { + DisposableHelper.dispose(this); + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/mixed/ObservableSwitchMapCompletable.java b/src/main/java/io/reactivex/rxjava3/internal/operators/mixed/ObservableSwitchMapCompletable.java new file mode 100644 index 0000000000..b751afdbb0 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/mixed/ObservableSwitchMapCompletable.java @@ -0,0 +1,218 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.mixed; + +import java.util.Objects; +import java.util.concurrent.atomic.AtomicReference; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; +import io.reactivex.rxjava3.internal.util.AtomicThrowable; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +/** + * Maps the upstream values into {@link CompletableSource}s, subscribes to the newer one while + * disposing the subscription to the previous {@code CompletableSource}, thus keeping at most one + * active {@code CompletableSource} running. + * <p>History: 2.1.11 - experimental + * @param <T> the upstream value type + * @since 2.2 + */ +public final class ObservableSwitchMapCompletable<T> extends Completable { + + final Observable<T> source; + + final Function<? super T, ? extends CompletableSource> mapper; + + final boolean delayErrors; + + public ObservableSwitchMapCompletable(Observable<T> source, + Function<? super T, ? extends CompletableSource> mapper, boolean delayErrors) { + this.source = source; + this.mapper = mapper; + this.delayErrors = delayErrors; + } + + @Override + protected void subscribeActual(CompletableObserver observer) { + if (!ScalarXMapZHelper.tryAsCompletable(source, mapper, observer)) { + source.subscribe(new SwitchMapCompletableObserver<>(observer, mapper, delayErrors)); + } + } + + static final class SwitchMapCompletableObserver<T> implements Observer<T>, Disposable { + + final CompletableObserver downstream; + + final Function<? super T, ? extends CompletableSource> mapper; + + final boolean delayErrors; + + final AtomicThrowable errors; + + final AtomicReference<SwitchMapInnerObserver> inner; + + static final SwitchMapInnerObserver INNER_DISPOSED = new SwitchMapInnerObserver(null); + + volatile boolean done; + + Disposable upstream; + + SwitchMapCompletableObserver(CompletableObserver downstream, + Function<? super T, ? extends CompletableSource> mapper, boolean delayErrors) { + this.downstream = downstream; + this.mapper = mapper; + this.delayErrors = delayErrors; + this.errors = new AtomicThrowable(); + this.inner = new AtomicReference<>(); + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.validate(upstream, d)) { + this.upstream = d; + downstream.onSubscribe(this); + } + } + + @Override + public void onNext(T t) { + CompletableSource c; + + try { + c = Objects.requireNonNull(mapper.apply(t), "The mapper returned a null CompletableSource"); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + upstream.dispose(); + onError(ex); + return; + } + + SwitchMapInnerObserver o = new SwitchMapInnerObserver(this); + + for (;;) { + SwitchMapInnerObserver current = inner.get(); + if (current == INNER_DISPOSED) { + break; + } + if (inner.compareAndSet(current, o)) { + if (current != null) { + current.dispose(); + } + c.subscribe(o); + break; + } + } + } + + @Override + public void onError(Throwable t) { + if (errors.tryAddThrowableOrReport(t)) { + if (delayErrors) { + onComplete(); + } else { + disposeInner(); + errors.tryTerminateConsumer(downstream); + } + } + } + + @Override + public void onComplete() { + done = true; + if (inner.get() == null) { + errors.tryTerminateConsumer(downstream); + } + } + + void disposeInner() { + SwitchMapInnerObserver o = inner.getAndSet(INNER_DISPOSED); + if (o != null && o != INNER_DISPOSED) { + o.dispose(); + } + } + + @Override + public void dispose() { + upstream.dispose(); + disposeInner(); + errors.tryTerminateAndReport(); + } + + @Override + public boolean isDisposed() { + return inner.get() == INNER_DISPOSED; + } + + void innerError(SwitchMapInnerObserver sender, Throwable error) { + if (inner.compareAndSet(sender, null)) { + if (errors.tryAddThrowableOrReport(error)) { + if (delayErrors) { + if (done) { + errors.tryTerminateConsumer(downstream); + } + } else { + upstream.dispose(); + disposeInner(); + errors.tryTerminateConsumer(downstream); + } + } + } else { + RxJavaPlugins.onError(error); + } + } + + void innerComplete(SwitchMapInnerObserver sender) { + if (inner.compareAndSet(sender, null)) { + if (done) { + errors.tryTerminateConsumer(downstream); + } + } + } + + static final class SwitchMapInnerObserver extends AtomicReference<Disposable> + implements CompletableObserver { + + private static final long serialVersionUID = -8003404460084760287L; + + final SwitchMapCompletableObserver<?> parent; + + SwitchMapInnerObserver(SwitchMapCompletableObserver<?> parent) { + this.parent = parent; + } + + @Override + public void onSubscribe(Disposable d) { + DisposableHelper.setOnce(this, d); + } + + @Override + public void onError(Throwable e) { + parent.innerError(this, e); + } + + @Override + public void onComplete() { + parent.innerComplete(this); + } + + void dispose() { + DisposableHelper.dispose(this); + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/mixed/ObservableSwitchMapMaybe.java b/src/main/java/io/reactivex/rxjava3/internal/operators/mixed/ObservableSwitchMapMaybe.java new file mode 100644 index 0000000000..fe394df1e8 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/mixed/ObservableSwitchMapMaybe.java @@ -0,0 +1,281 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.mixed; + +import java.util.Objects; +import java.util.concurrent.atomic.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; +import io.reactivex.rxjava3.internal.util.AtomicThrowable; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +/** + * Maps the upstream items into {@link MaybeSource}s and switches (subscribes) to the newer ones + * while disposing the older ones and emits the latest success value if available, optionally delaying + * errors from the main source or the inner sources. + * <p>History: 2.1.11 - experimental + * @param <T> the upstream value type + * @param <R> the downstream value type + * @since 2.2 + */ +public final class ObservableSwitchMapMaybe<T, R> extends Observable<R> { + + final Observable<T> source; + + final Function<? super T, ? extends MaybeSource<? extends R>> mapper; + + final boolean delayErrors; + + public ObservableSwitchMapMaybe(Observable<T> source, + Function<? super T, ? extends MaybeSource<? extends R>> mapper, + boolean delayErrors) { + this.source = source; + this.mapper = mapper; + this.delayErrors = delayErrors; + } + + @Override + protected void subscribeActual(Observer<? super R> observer) { + if (!ScalarXMapZHelper.tryAsMaybe(source, mapper, observer)) { + source.subscribe(new SwitchMapMaybeMainObserver<>(observer, mapper, delayErrors)); + } + } + + static final class SwitchMapMaybeMainObserver<T, R> extends AtomicInteger + implements Observer<T>, Disposable { + + private static final long serialVersionUID = -5402190102429853762L; + + final Observer<? super R> downstream; + + final Function<? super T, ? extends MaybeSource<? extends R>> mapper; + + final boolean delayErrors; + + final AtomicThrowable errors; + + final AtomicReference<SwitchMapMaybeObserver<R>> inner; + + static final SwitchMapMaybeObserver<Object> INNER_DISPOSED = + new SwitchMapMaybeObserver<>(null); + + Disposable upstream; + + volatile boolean done; + + volatile boolean cancelled; + + SwitchMapMaybeMainObserver(Observer<? super R> downstream, + Function<? super T, ? extends MaybeSource<? extends R>> mapper, + boolean delayErrors) { + this.downstream = downstream; + this.mapper = mapper; + this.delayErrors = delayErrors; + this.errors = new AtomicThrowable(); + this.inner = new AtomicReference<>(); + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.validate(upstream, d)) { + upstream = d; + downstream.onSubscribe(this); + } + } + + @Override + @SuppressWarnings({ "unchecked", "rawtypes" }) + public void onNext(T t) { + SwitchMapMaybeObserver<R> current = inner.get(); + if (current != null) { + current.dispose(); + } + + MaybeSource<? extends R> ms; + + try { + ms = Objects.requireNonNull(mapper.apply(t), "The mapper returned a null MaybeSource"); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + upstream.dispose(); + inner.getAndSet((SwitchMapMaybeObserver)INNER_DISPOSED); + onError(ex); + return; + } + + SwitchMapMaybeObserver<R> observer = new SwitchMapMaybeObserver<>(this); + + for (;;) { + current = inner.get(); + if (current == INNER_DISPOSED) { + break; + } + if (inner.compareAndSet(current, observer)) { + ms.subscribe(observer); + break; + } + } + } + + @Override + public void onError(Throwable t) { + if (errors.tryAddThrowableOrReport(t)) { + if (!delayErrors) { + disposeInner(); + } + done = true; + drain(); + } + } + + @Override + public void onComplete() { + done = true; + drain(); + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + void disposeInner() { + SwitchMapMaybeObserver<R> current = inner.getAndSet((SwitchMapMaybeObserver)INNER_DISPOSED); + if (current != null && current != INNER_DISPOSED) { + current.dispose(); + } + } + + @Override + public void dispose() { + cancelled = true; + upstream.dispose(); + disposeInner(); + errors.tryTerminateAndReport(); + } + + @Override + public boolean isDisposed() { + return cancelled; + } + + void innerError(SwitchMapMaybeObserver<R> sender, Throwable ex) { + if (inner.compareAndSet(sender, null)) { + if (errors.tryAddThrowableOrReport(ex)) { + if (!delayErrors) { + upstream.dispose(); + disposeInner(); + } + drain(); + } + } else { + RxJavaPlugins.onError(ex); + } + } + + void innerComplete(SwitchMapMaybeObserver<R> sender) { + if (inner.compareAndSet(sender, null)) { + drain(); + } + } + + void drain() { + if (getAndIncrement() != 0) { + return; + } + + int missed = 1; + Observer<? super R> downstream = this.downstream; + AtomicThrowable errors = this.errors; + AtomicReference<SwitchMapMaybeObserver<R>> inner = this.inner; + + for (;;) { + + for (;;) { + if (cancelled) { + return; + } + + if (errors.get() != null) { + if (!delayErrors) { + errors.tryTerminateConsumer(downstream); + return; + } + } + + boolean d = done; + SwitchMapMaybeObserver<R> current = inner.get(); + boolean empty = current == null; + + if (d && empty) { + errors.tryTerminateConsumer(downstream); + return; + } + + if (empty || current.item == null) { + break; + } + + inner.compareAndSet(current, null); + + downstream.onNext(current.item); + } + + missed = addAndGet(-missed); + if (missed == 0) { + break; + } + } + } + + static final class SwitchMapMaybeObserver<R> + extends AtomicReference<Disposable> implements MaybeObserver<R> { + + private static final long serialVersionUID = 8042919737683345351L; + + final SwitchMapMaybeMainObserver<?, R> parent; + + volatile R item; + + SwitchMapMaybeObserver(SwitchMapMaybeMainObserver<?, R> parent) { + this.parent = parent; + } + + @Override + public void onSubscribe(Disposable d) { + DisposableHelper.setOnce(this, d); + } + + @Override + public void onSuccess(R t) { + item = t; + parent.drain(); + } + + @Override + public void onError(Throwable e) { + parent.innerError(this, e); + } + + @Override + public void onComplete() { + parent.innerComplete(this); + } + + void dispose() { + DisposableHelper.dispose(this); + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/mixed/ObservableSwitchMapSingle.java b/src/main/java/io/reactivex/rxjava3/internal/operators/mixed/ObservableSwitchMapSingle.java new file mode 100644 index 0000000000..ca0e2953e0 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/mixed/ObservableSwitchMapSingle.java @@ -0,0 +1,270 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.mixed; + +import java.util.Objects; +import java.util.concurrent.atomic.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; +import io.reactivex.rxjava3.internal.util.AtomicThrowable; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +/** + * Maps the upstream items into {@link SingleSource}s and switches (subscribes) to the newer ones + * while disposing the older ones and emits the latest success value if available, optionally delaying + * errors from the main source or the inner sources. + * <p>History: 2.1.11 - experimental + * @param <T> the upstream value type + * @param <R> the downstream value type + * @since 2.2 + */ +public final class ObservableSwitchMapSingle<T, R> extends Observable<R> { + + final Observable<T> source; + + final Function<? super T, ? extends SingleSource<? extends R>> mapper; + + final boolean delayErrors; + + public ObservableSwitchMapSingle(Observable<T> source, + Function<? super T, ? extends SingleSource<? extends R>> mapper, + boolean delayErrors) { + this.source = source; + this.mapper = mapper; + this.delayErrors = delayErrors; + } + + @Override + protected void subscribeActual(Observer<? super R> observer) { + if (!ScalarXMapZHelper.tryAsSingle(source, mapper, observer)) { + source.subscribe(new SwitchMapSingleMainObserver<>(observer, mapper, delayErrors)); + } + } + + static final class SwitchMapSingleMainObserver<T, R> extends AtomicInteger + implements Observer<T>, Disposable { + + private static final long serialVersionUID = -5402190102429853762L; + + final Observer<? super R> downstream; + + final Function<? super T, ? extends SingleSource<? extends R>> mapper; + + final boolean delayErrors; + + final AtomicThrowable errors; + + final AtomicReference<SwitchMapSingleObserver<R>> inner; + + static final SwitchMapSingleObserver<Object> INNER_DISPOSED = + new SwitchMapSingleObserver<>(null); + + Disposable upstream; + + volatile boolean done; + + volatile boolean cancelled; + + SwitchMapSingleMainObserver(Observer<? super R> downstream, + Function<? super T, ? extends SingleSource<? extends R>> mapper, + boolean delayErrors) { + this.downstream = downstream; + this.mapper = mapper; + this.delayErrors = delayErrors; + this.errors = new AtomicThrowable(); + this.inner = new AtomicReference<>(); + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.validate(upstream, d)) { + upstream = d; + downstream.onSubscribe(this); + } + } + + @Override + @SuppressWarnings({ "unchecked", "rawtypes" }) + public void onNext(T t) { + SwitchMapSingleObserver<R> current = inner.get(); + if (current != null) { + current.dispose(); + } + + SingleSource<? extends R> ss; + + try { + ss = Objects.requireNonNull(mapper.apply(t), "The mapper returned a null SingleSource"); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + upstream.dispose(); + inner.getAndSet((SwitchMapSingleObserver)INNER_DISPOSED); + onError(ex); + return; + } + + SwitchMapSingleObserver<R> observer = new SwitchMapSingleObserver<>(this); + + for (;;) { + current = inner.get(); + if (current == INNER_DISPOSED) { + break; + } + if (inner.compareAndSet(current, observer)) { + ss.subscribe(observer); + break; + } + } + } + + @Override + public void onError(Throwable t) { + if (errors.tryAddThrowableOrReport(t)) { + if (!delayErrors) { + disposeInner(); + } + done = true; + drain(); + } + } + + @Override + public void onComplete() { + done = true; + drain(); + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + void disposeInner() { + SwitchMapSingleObserver<R> current = inner.getAndSet((SwitchMapSingleObserver)INNER_DISPOSED); + if (current != null && current != INNER_DISPOSED) { + current.dispose(); + } + } + + @Override + public void dispose() { + cancelled = true; + upstream.dispose(); + disposeInner(); + errors.tryTerminateAndReport(); + } + + @Override + public boolean isDisposed() { + return cancelled; + } + + void innerError(SwitchMapSingleObserver<R> sender, Throwable ex) { + if (inner.compareAndSet(sender, null)) { + if (errors.tryAddThrowableOrReport(ex)) { + if (!delayErrors) { + upstream.dispose(); + disposeInner(); + } + drain(); + } + } else { + RxJavaPlugins.onError(ex); + } + } + + void drain() { + if (getAndIncrement() != 0) { + return; + } + + int missed = 1; + Observer<? super R> downstream = this.downstream; + AtomicThrowable errors = this.errors; + AtomicReference<SwitchMapSingleObserver<R>> inner = this.inner; + + for (;;) { + + for (;;) { + if (cancelled) { + return; + } + + if (errors.get() != null) { + if (!delayErrors) { + errors.tryTerminateConsumer(downstream); + return; + } + } + + boolean d = done; + SwitchMapSingleObserver<R> current = inner.get(); + boolean empty = current == null; + + if (d && empty) { + errors.tryTerminateConsumer(downstream); + return; + } + + if (empty || current.item == null) { + break; + } + + inner.compareAndSet(current, null); + + downstream.onNext(current.item); + } + + missed = addAndGet(-missed); + if (missed == 0) { + break; + } + } + } + + static final class SwitchMapSingleObserver<R> + extends AtomicReference<Disposable> implements SingleObserver<R> { + + private static final long serialVersionUID = 8042919737683345351L; + + final SwitchMapSingleMainObserver<?, R> parent; + + volatile R item; + + SwitchMapSingleObserver(SwitchMapSingleMainObserver<?, R> parent) { + this.parent = parent; + } + + @Override + public void onSubscribe(Disposable d) { + DisposableHelper.setOnce(this, d); + } + + @Override + public void onSuccess(R t) { + item = t; + parent.drain(); + } + + @Override + public void onError(Throwable e) { + parent.innerError(this, e); + } + + void dispose() { + DisposableHelper.dispose(this); + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/mixed/ScalarXMapZHelper.java b/src/main/java/io/reactivex/rxjava3/internal/operators/mixed/ScalarXMapZHelper.java new file mode 100644 index 0000000000..2ed6301e42 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/mixed/ScalarXMapZHelper.java @@ -0,0 +1,156 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.mixed; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.disposables.EmptyDisposable; +import io.reactivex.rxjava3.internal.operators.maybe.MaybeToObservable; +import io.reactivex.rxjava3.internal.operators.single.SingleToObservable; + +import java.util.Objects; + +/** + * Utility class to extract a value from a scalar source reactive type, + * map it to a 0-1 type then subscribe the output type's consumer to it, + * saving on the overhead of the regular subscription channel. + * <p>History: 2.1.11 - experimental + * @since 2.2 + */ +final class ScalarXMapZHelper { + + private ScalarXMapZHelper() { + throw new IllegalStateException("No instances!"); + } + + /** + * Try subscribing to a {@link CompletableSource} mapped from + * a scalar source (which implements {@link Supplier}). + * @param <T> the upstream value type + * @param source the source reactive type ({@code Flowable} or {@code Observable}) + * possibly implementing {@link Supplier}. + * @param mapper the function that turns the scalar upstream value into a + * {@link CompletableSource} + * @param observer the consumer to subscribe to the mapped {@link CompletableSource} + * @return true if a subscription did happen and the regular path should be skipped + */ + static <T> boolean tryAsCompletable(Object source, + Function<? super T, ? extends CompletableSource> mapper, + CompletableObserver observer) { + if (source instanceof Supplier) { + @SuppressWarnings("unchecked") + Supplier<T> supplier = (Supplier<T>) source; + CompletableSource cs = null; + try { + T item = supplier.get(); + if (item != null) { + cs = Objects.requireNonNull(mapper.apply(item), "The mapper returned a null CompletableSource"); + } + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + EmptyDisposable.error(ex, observer); + return true; + } + + if (cs == null) { + EmptyDisposable.complete(observer); + } else { + cs.subscribe(observer); + } + return true; + } + return false; + } + + /** + * Try subscribing to a {@link MaybeSource} mapped from + * a scalar source (which implements {@link Supplier}). + * @param <T> the upstream value type + * @param <R> the downstream value type + * @param source the source reactive type ({@code Flowable} or {@code Observable}) + * possibly implementing {@link Supplier}. + * @param mapper the function that turns the scalar upstream value into a + * {@link MaybeSource} + * @param observer the consumer to subscribe to the mapped {@link MaybeSource} + * @return true if a subscription did happen and the regular path should be skipped + */ + static <T, R> boolean tryAsMaybe(Object source, + Function<? super T, ? extends MaybeSource<? extends R>> mapper, + Observer<? super R> observer) { + if (source instanceof Supplier) { + @SuppressWarnings("unchecked") + Supplier<T> supplier = (Supplier<T>) source; + MaybeSource<? extends R> cs = null; + try { + T item = supplier.get(); + if (item != null) { + cs = Objects.requireNonNull(mapper.apply(item), "The mapper returned a null MaybeSource"); + } + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + EmptyDisposable.error(ex, observer); + return true; + } + + if (cs == null) { + EmptyDisposable.complete(observer); + } else { + cs.subscribe(MaybeToObservable.create(observer)); + } + return true; + } + return false; + } + + /** + * Try subscribing to a {@link SingleSource} mapped from + * a scalar source (which implements {@link Supplier}). + * @param <T> the upstream value type + * @param <R> the downstream value type + * @param source the source reactive type ({@code Flowable} or {@code Observable}) + * possibly implementing {@link Supplier}. + * @param mapper the function that turns the scalar upstream value into a + * {@link SingleSource} + * @param observer the consumer to subscribe to the mapped {@link SingleSource} + * @return true if a subscription did happen and the regular path should be skipped + */ + static <T, R> boolean tryAsSingle(Object source, + Function<? super T, ? extends SingleSource<? extends R>> mapper, + Observer<? super R> observer) { + if (source instanceof Supplier) { + @SuppressWarnings("unchecked") + Supplier<T> supplier = (Supplier<T>) source; + SingleSource<? extends R> cs = null; + try { + T item = supplier.get(); + if (item != null) { + cs = Objects.requireNonNull(mapper.apply(item), "The mapper returned a null SingleSource"); + } + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + EmptyDisposable.error(ex, observer); + return true; + } + + if (cs == null) { + EmptyDisposable.complete(observer); + } else { + cs.subscribe(SingleToObservable.create(observer)); + } + return true; + } + return false; + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/mixed/SingleFlatMapObservable.java b/src/main/java/io/reactivex/rxjava3/internal/operators/mixed/SingleFlatMapObservable.java new file mode 100644 index 0000000000..5db7515890 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/mixed/SingleFlatMapObservable.java @@ -0,0 +1,115 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.mixed; + +import java.util.Objects; +import java.util.concurrent.atomic.AtomicReference; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; + +/** + * Maps the success value of a Single onto an ObservableSource and + * relays its signals to the downstream observer. + * + * @param <T> the success value type of the Single source + * @param <R> the result type of the ObservableSource and this operator + * @since 2.1.15 + */ +public final class SingleFlatMapObservable<T, R> extends Observable<R> { + + final SingleSource<T> source; + + final Function<? super T, ? extends ObservableSource<? extends R>> mapper; + + public SingleFlatMapObservable(SingleSource<T> source, + Function<? super T, ? extends ObservableSource<? extends R>> mapper) { + this.source = source; + this.mapper = mapper; + } + + @Override + protected void subscribeActual(Observer<? super R> observer) { + FlatMapObserver<T, R> parent = new FlatMapObserver<>(observer, mapper); + observer.onSubscribe(parent); + source.subscribe(parent); + } + + static final class FlatMapObserver<T, R> + extends AtomicReference<Disposable> + implements Observer<R>, SingleObserver<T>, Disposable { + + private static final long serialVersionUID = -8948264376121066672L; + + final Observer<? super R> downstream; + + final Function<? super T, ? extends ObservableSource<? extends R>> mapper; + + FlatMapObserver(Observer<? super R> downstream, Function<? super T, ? extends ObservableSource<? extends R>> mapper) { + this.downstream = downstream; + this.mapper = mapper; + } + + @Override + public void onNext(R t) { + downstream.onNext(t); + } + + @Override + public void onError(Throwable t) { + downstream.onError(t); + } + + @Override + public void onComplete() { + downstream.onComplete(); + } + + @Override + public void dispose() { + DisposableHelper.dispose(this); + } + + @Override + public boolean isDisposed() { + return DisposableHelper.isDisposed(get()); + } + + @Override + public void onSubscribe(Disposable d) { + DisposableHelper.replace(this, d); + } + + @Override + public void onSuccess(T t) { + ObservableSource<? extends R> o; + + try { + o = Objects.requireNonNull(mapper.apply(t), "The mapper returned a null Publisher"); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + downstream.onError(ex); + return; + } + + if (!isDisposed()) { + o.subscribe(this); + } + } + + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/observable/AbstractObservableWithUpstream.java b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/AbstractObservableWithUpstream.java new file mode 100644 index 0000000000..dcad29a5d8 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/AbstractObservableWithUpstream.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.internal.fuseable.HasUpstreamObservableSource; + +/** + * Base class for operators with a source consumable. + * + * @param <T> the input source type + * @param <U> the output type + */ +abstract class AbstractObservableWithUpstream<T, U> extends Observable<U> implements HasUpstreamObservableSource<T> { + + /** The source consumable Observable. */ + protected final ObservableSource<T> source; + + /** + * Constructs the ObservableSource with the given consumable. + * @param source the consumable Observable + */ + AbstractObservableWithUpstream(ObservableSource<T> source) { + this.source = source; + } + + @Override + public final ObservableSource<T> source() { + return source; + } + +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/observable/BlockingObservableIterable.java b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/BlockingObservableIterable.java new file mode 100644 index 0000000000..561571febf --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/BlockingObservableIterable.java @@ -0,0 +1,164 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import java.util.*; +import java.util.concurrent.atomic.AtomicReference; +import java.util.concurrent.locks.*; + +import io.reactivex.rxjava3.core.ObservableSource; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; +import io.reactivex.rxjava3.internal.util.*; +import io.reactivex.rxjava3.operators.SpscLinkedArrayQueue; + +public final class BlockingObservableIterable<T> implements Iterable<T> { + final ObservableSource<? extends T> source; + + final int bufferSize; + + public BlockingObservableIterable(ObservableSource<? extends T> source, int bufferSize) { + this.source = source; + this.bufferSize = bufferSize; + } + + @Override + public Iterator<T> iterator() { + BlockingObservableIterator<T> it = new BlockingObservableIterator<>(bufferSize); + source.subscribe(it); + return it; + } + + static final class BlockingObservableIterator<T> + extends AtomicReference<Disposable> + implements io.reactivex.rxjava3.core.Observer<T>, Iterator<T>, Disposable { + + private static final long serialVersionUID = 6695226475494099826L; + + final SpscLinkedArrayQueue<T> queue; + + final Lock lock; + + final Condition condition; + + volatile boolean done; + volatile Throwable error; + + BlockingObservableIterator(int batchSize) { + this.queue = new SpscLinkedArrayQueue<>(batchSize); + this.lock = new ReentrantLock(); + this.condition = lock.newCondition(); + } + + @Override + public boolean hasNext() { + for (;;) { + if (isDisposed()) { + Throwable e = error; + if (e != null) { + throw ExceptionHelper.wrapOrThrow(e); + } + return false; + } + boolean d = done; + boolean empty = queue.isEmpty(); + if (d) { + Throwable e = error; + if (e != null) { + throw ExceptionHelper.wrapOrThrow(e); + } else + if (empty) { + return false; + } + } + if (empty) { + try { + BlockingHelper.verifyNonBlocking(); + lock.lock(); + try { + while (!done && queue.isEmpty() && !isDisposed()) { + condition.await(); + } + } finally { + lock.unlock(); + } + } catch (InterruptedException ex) { + DisposableHelper.dispose(this); + signalConsumer(); + throw ExceptionHelper.wrapOrThrow(ex); + } + } else { + return true; + } + } + } + + @Override + public T next() { + if (hasNext()) { + return queue.poll(); + } + throw new NoSuchElementException(); + } + + @Override + public void onSubscribe(Disposable d) { + DisposableHelper.setOnce(this, d); + } + + @Override + public void onNext(T t) { + queue.offer(t); + signalConsumer(); + } + + @Override + public void onError(Throwable t) { + error = t; + done = true; + signalConsumer(); + } + + @Override + public void onComplete() { + done = true; + signalConsumer(); + } + + void signalConsumer() { + lock.lock(); + try { + condition.signalAll(); + } finally { + lock.unlock(); + } + } + + @Override // otherwise default method which isn't available in Java 7 + public void remove() { + throw new UnsupportedOperationException("remove"); + } + + @Override + public void dispose() { + DisposableHelper.dispose(this); + signalConsumer(); // Just in case it is currently blocking in hasNext. + } + + @Override + public boolean isDisposed() { + return DisposableHelper.isDisposed(get()); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/observable/BlockingObservableLatest.java b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/BlockingObservableLatest.java new file mode 100644 index 0000000000..4aca95b210 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/BlockingObservableLatest.java @@ -0,0 +1,114 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import java.util.*; +import java.util.concurrent.Semaphore; +import java.util.concurrent.atomic.AtomicReference; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.internal.util.*; +import io.reactivex.rxjava3.observers.DisposableObserver; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +/** + * Wait for and iterate over the latest values of the source observable. If the source works faster than the + * iterator, values may be skipped, but not the {@code onError} or {@code onComplete} events. + * @param <T> the value type + */ +public final class BlockingObservableLatest<T> implements Iterable<T> { + + final ObservableSource<T> source; + + public BlockingObservableLatest(ObservableSource<T> source) { + this.source = source; + } + + @Override + public Iterator<T> iterator() { + BlockingObservableLatestIterator<T> lio = new BlockingObservableLatestIterator<>(); + + Observable<Notification<T>> materialized = Observable.wrap(source).materialize(); + + materialized.subscribe(lio); + return lio; + } + + static final class BlockingObservableLatestIterator<T> extends DisposableObserver<Notification<T>> implements Iterator<T> { + // iterator's notification + Notification<T> iteratorNotification; + + final Semaphore notify = new Semaphore(0); + // observer's notification + final AtomicReference<Notification<T>> value = new AtomicReference<>(); + + @Override + public void onNext(Notification<T> args) { + boolean wasNotAvailable = value.getAndSet(args) == null; + if (wasNotAvailable) { + notify.release(); + } + } + + @Override + public void onError(Throwable e) { + RxJavaPlugins.onError(e); + } + + @Override + public void onComplete() { + // not expected + } + + @Override + public boolean hasNext() { + if (iteratorNotification != null && iteratorNotification.isOnError()) { + throw ExceptionHelper.wrapOrThrow(iteratorNotification.getError()); + } + if (iteratorNotification == null) { + try { + BlockingHelper.verifyNonBlocking(); + notify.acquire(); + } catch (InterruptedException ex) { + dispose(); + iteratorNotification = Notification.createOnError(ex); + throw ExceptionHelper.wrapOrThrow(ex); + } + + Notification<T> n = value.getAndSet(null); + iteratorNotification = n; + if (n.isOnError()) { + throw ExceptionHelper.wrapOrThrow(n.getError()); + } + } + return iteratorNotification.isOnNext(); + } + + @Override + public T next() { + if (hasNext()) { + T v = iteratorNotification.getValue(); + iteratorNotification = null; + return v; + } + throw new NoSuchElementException(); + } + + @Override + public void remove() { + throw new UnsupportedOperationException("Read-only iterator."); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/observable/BlockingObservableMostRecent.java b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/BlockingObservableMostRecent.java new file mode 100644 index 0000000000..0b2ea5bd34 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/BlockingObservableMostRecent.java @@ -0,0 +1,119 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import java.util.*; + +import io.reactivex.rxjava3.core.ObservableSource; +import io.reactivex.rxjava3.internal.util.*; +import io.reactivex.rxjava3.observers.DefaultObserver; + +/** + * Returns an Iterable that always returns the item most recently emitted by an Observable, or a + * seed value if no item has yet been emitted. + * <p> + * <img width="640" height="490" src="https://github.com/ReactiveX/RxJava/wiki/images/rx-operators/B.mostRecent.v3.png" alt=""> + * + * @param <T> the value type + */ +public final class BlockingObservableMostRecent<T> implements Iterable<T> { + + final ObservableSource<T> source; + + final T initialValue; + + public BlockingObservableMostRecent(ObservableSource<T> source, T initialValue) { + this.source = source; + this.initialValue = initialValue; + } + + @Override + public Iterator<T> iterator() { + MostRecentObserver<T> mostRecentObserver = new MostRecentObserver<>(initialValue); + + source.subscribe(mostRecentObserver); + + return mostRecentObserver.getIterable(); + } + + static final class MostRecentObserver<T> extends DefaultObserver<T> { + volatile Object value; + + MostRecentObserver(T value) { + this.value = NotificationLite.next(value); + } + + @Override + public void onComplete() { + value = NotificationLite.complete(); + } + + @Override + public void onError(Throwable e) { + value = NotificationLite.error(e); + } + + @Override + public void onNext(T args) { + value = NotificationLite.next(args); + } + + /** + * The {@link MostRecentIterator} return is not thread safe. In other words don't call {@link MostRecentIterator#hasNext()} in one + * thread expect {@link MostRecentIterator#next()} called from a different thread to work. + * @return the Iterator + */ + public MostRecentIterator getIterable() { + return new MostRecentIterator(); + } + + final class MostRecentIterator implements java.util.Iterator<T> { + /** + * buffer to make sure that the state of the iterator doesn't change between calling hasNext() and next(). + */ + private Object buf; + + @Override + public boolean hasNext() { + buf = value; + return !NotificationLite.isComplete(buf); + } + + @Override + public T next() { + try { + // if hasNext wasn't called before calling next. + if (buf == null) { + buf = value; + } + if (NotificationLite.isComplete(buf)) { + throw new NoSuchElementException(); + } + if (NotificationLite.isError(buf)) { + throw ExceptionHelper.wrapOrThrow(NotificationLite.getError(buf)); + } + return NotificationLite.getValue(buf); + } + finally { + buf = null; + } + } + + @Override + public void remove() { + throw new UnsupportedOperationException("Read only iterator"); + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/observable/BlockingObservableNext.java b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/BlockingObservableNext.java new file mode 100644 index 0000000000..2bbfe2b036 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/BlockingObservableNext.java @@ -0,0 +1,172 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import java.util.*; +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicInteger; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.internal.util.*; +import io.reactivex.rxjava3.observers.DisposableObserver; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +/** + * Returns an Iterable that blocks until the Observable emits another item, then returns that item. + * <p> + * <img width="640" height="490" src="https://github.com/ReactiveX/RxJava/wiki/images/rx-operators/B.next.v3.png" alt=""> + * + * @param <T> the value type + */ +public final class BlockingObservableNext<T> implements Iterable<T> { + + final ObservableSource<T> source; + + public BlockingObservableNext(ObservableSource<T> source) { + this.source = source; + } + + @Override + public Iterator<T> iterator() { + NextObserver<T> nextObserver = new NextObserver<>(); + return new NextIterator<>(source, nextObserver); + } + + // test needs to access the observer.waiting flag + static final class NextIterator<T> implements Iterator<T> { + + private final NextObserver<T> observer; + private final ObservableSource<T> items; + private T next; + private boolean hasNext = true; + private boolean isNextConsumed = true; + private Throwable error; + private boolean started; + + NextIterator(ObservableSource<T> items, NextObserver<T> observer) { + this.items = items; + this.observer = observer; + } + + @Override + public boolean hasNext() { + if (error != null) { + // If any error has already been thrown, throw it again. + throw ExceptionHelper.wrapOrThrow(error); + } + // Since an iterator should not be used in different thread, + // so we do not need any synchronization. + if (!hasNext) { + // the iterator has reached the end. + return false; + } + // next has not been used yet. + return !isNextConsumed || moveToNext(); + } + + private boolean moveToNext() { + if (!started) { + started = true; + // if not started, start now + observer.setWaiting(); + new ObservableMaterialize<>(items).subscribe(observer); + } + + Notification<T> nextNotification; + + try { + nextNotification = observer.takeNext(); + } catch (InterruptedException e) { + observer.dispose(); + error = e; + throw ExceptionHelper.wrapOrThrow(e); + } + + if (nextNotification.isOnNext()) { + isNextConsumed = false; + next = nextNotification.getValue(); + return true; + } + // If an observable is completed or fails, + // hasNext() always return false. + hasNext = false; + if (nextNotification.isOnComplete()) { + return false; + } + error = nextNotification.getError(); + throw ExceptionHelper.wrapOrThrow(error); + } + + @Override + public T next() { + if (error != null) { + // If any error has already been thrown, throw it again. + throw ExceptionHelper.wrapOrThrow(error); + } + if (hasNext()) { + isNextConsumed = true; + return next; + } + else { + throw new NoSuchElementException("No more elements"); + } + } + + @Override + public void remove() { + throw new UnsupportedOperationException("Read only iterator"); + } + } + + static final class NextObserver<T> extends DisposableObserver<Notification<T>> { + private final BlockingQueue<Notification<T>> buf = new ArrayBlockingQueue<>(1); + final AtomicInteger waiting = new AtomicInteger(); + + @Override + public void onComplete() { + // ignore + } + + @Override + public void onError(Throwable e) { + RxJavaPlugins.onError(e); + } + + @Override + public void onNext(Notification<T> args) { + + if (waiting.getAndSet(0) == 1 || !args.isOnNext()) { + Notification<T> toOffer = args; + while (!buf.offer(toOffer)) { + Notification<T> concurrentItem = buf.poll(); + + // in case if we won race condition with onComplete/onError method + if (concurrentItem != null && !concurrentItem.isOnNext()) { + toOffer = concurrentItem; + } + } + } + + } + + public Notification<T> takeNext() throws InterruptedException { + setWaiting(); + BlockingHelper.verifyNonBlocking(); + return buf.take(); + } + void setWaiting() { + waiting.set(1); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableAll.java b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableAll.java new file mode 100644 index 0000000000..0f365414c6 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableAll.java @@ -0,0 +1,108 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.Predicate; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +public final class ObservableAll<T> extends AbstractObservableWithUpstream<T, Boolean> { + final Predicate<? super T> predicate; + public ObservableAll(ObservableSource<T> source, Predicate<? super T> predicate) { + super(source); + this.predicate = predicate; + } + + @Override + protected void subscribeActual(Observer<? super Boolean> t) { + source.subscribe(new AllObserver<>(t, predicate)); + } + + static final class AllObserver<T> implements Observer<T>, Disposable { + final Observer<? super Boolean> downstream; + final Predicate<? super T> predicate; + + Disposable upstream; + + boolean done; + + AllObserver(Observer<? super Boolean> actual, Predicate<? super T> predicate) { + this.downstream = actual; + this.predicate = predicate; + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.validate(this.upstream, d)) { + this.upstream = d; + downstream.onSubscribe(this); + } + } + + @Override + public void onNext(T t) { + if (done) { + return; + } + boolean b; + try { + b = predicate.test(t); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + upstream.dispose(); + onError(e); + return; + } + if (!b) { + done = true; + upstream.dispose(); + downstream.onNext(false); + downstream.onComplete(); + } + } + + @Override + public void onError(Throwable t) { + if (done) { + RxJavaPlugins.onError(t); + return; + } + done = true; + downstream.onError(t); + } + + @Override + public void onComplete() { + if (done) { + return; + } + done = true; + downstream.onNext(true); + downstream.onComplete(); + } + + @Override + public void dispose() { + upstream.dispose(); + } + + @Override + public boolean isDisposed() { + return upstream.isDisposed(); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableAllSingle.java b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableAllSingle.java new file mode 100644 index 0000000000..89e4e38a16 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableAllSingle.java @@ -0,0 +1,114 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.Predicate; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; +import io.reactivex.rxjava3.internal.fuseable.FuseToObservable; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +public final class ObservableAllSingle<T> extends Single<Boolean> implements FuseToObservable<Boolean> { + final ObservableSource<T> source; + + final Predicate<? super T> predicate; + public ObservableAllSingle(ObservableSource<T> source, Predicate<? super T> predicate) { + this.source = source; + this.predicate = predicate; + } + + @Override + protected void subscribeActual(SingleObserver<? super Boolean> t) { + source.subscribe(new AllObserver<>(t, predicate)); + } + + @Override + public Observable<Boolean> fuseToObservable() { + return RxJavaPlugins.onAssembly(new ObservableAll<>(source, predicate)); + } + + static final class AllObserver<T> implements Observer<T>, Disposable { + final SingleObserver<? super Boolean> downstream; + final Predicate<? super T> predicate; + + Disposable upstream; + + boolean done; + + AllObserver(SingleObserver<? super Boolean> actual, Predicate<? super T> predicate) { + this.downstream = actual; + this.predicate = predicate; + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.validate(this.upstream, d)) { + this.upstream = d; + downstream.onSubscribe(this); + } + } + + @Override + public void onNext(T t) { + if (done) { + return; + } + boolean b; + try { + b = predicate.test(t); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + upstream.dispose(); + onError(e); + return; + } + if (!b) { + done = true; + upstream.dispose(); + downstream.onSuccess(false); + } + } + + @Override + public void onError(Throwable t) { + if (done) { + RxJavaPlugins.onError(t); + return; + } + done = true; + downstream.onError(t); + } + + @Override + public void onComplete() { + if (done) { + return; + } + done = true; + downstream.onSuccess(true); + } + + @Override + public void dispose() { + upstream.dispose(); + } + + @Override + public boolean isDisposed() { + return upstream.isDisposed(); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableAmb.java b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableAmb.java new file mode 100644 index 0000000000..f8841956c6 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableAmb.java @@ -0,0 +1,203 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import java.util.concurrent.atomic.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.internal.disposables.*; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +public final class ObservableAmb<T> extends Observable<T> { + final ObservableSource<? extends T>[] sources; + final Iterable<? extends ObservableSource<? extends T>> sourcesIterable; + + public ObservableAmb(ObservableSource<? extends T>[] sources, Iterable<? extends ObservableSource<? extends T>> sourcesIterable) { + this.sources = sources; + this.sourcesIterable = sourcesIterable; + } + + @Override + @SuppressWarnings("unchecked") + public void subscribeActual(Observer<? super T> observer) { + ObservableSource<? extends T>[] sources = this.sources; + int count = 0; + if (sources == null) { + sources = new ObservableSource[8]; + try { + for (ObservableSource<? extends T> p : sourcesIterable) { + if (p == null) { + EmptyDisposable.error(new NullPointerException("One of the sources is null"), observer); + return; + } + if (count == sources.length) { + ObservableSource<? extends T>[] b = new ObservableSource[count + (count >> 2)]; + System.arraycopy(sources, 0, b, 0, count); + sources = b; + } + sources[count++] = p; + } + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + EmptyDisposable.error(e, observer); + return; + } + } else { + count = sources.length; + } + + if (count == 0) { + EmptyDisposable.complete(observer); + return; + } else + if (count == 1) { + sources[0].subscribe(observer); + return; + } + + AmbCoordinator<T> ac = new AmbCoordinator<>(observer, count); + ac.subscribe(sources); + } + + static final class AmbCoordinator<T> implements Disposable { + final Observer<? super T> downstream; + final AmbInnerObserver<T>[] observers; + + final AtomicInteger winner = new AtomicInteger(); + + @SuppressWarnings("unchecked") + AmbCoordinator(Observer<? super T> actual, int count) { + this.downstream = actual; + this.observers = new AmbInnerObserver[count]; + } + + public void subscribe(ObservableSource<? extends T>[] sources) { + AmbInnerObserver<T>[] as = observers; + int len = as.length; + for (int i = 0; i < len; i++) { + as[i] = new AmbInnerObserver<>(this, i + 1, downstream); + } + winner.lazySet(0); // release the contents of 'as' + downstream.onSubscribe(this); + + for (int i = 0; i < len; i++) { + if (winner.get() != 0) { + return; + } + + sources[i].subscribe(as[i]); + } + } + + public boolean win(int index) { + int w = winner.get(); + if (w == 0) { + if (winner.compareAndSet(0, index)) { + AmbInnerObserver<T>[] a = observers; + int n = a.length; + for (int i = 0; i < n; i++) { + if (i + 1 != index) { + a[i].dispose(); + } + } + return true; + } + } + return false; + } + + @Override + public void dispose() { + if (winner.get() != -1) { + winner.lazySet(-1); + + for (AmbInnerObserver<T> a : observers) { + a.dispose(); + } + } + } + + @Override + public boolean isDisposed() { + return winner.get() == -1; + } + } + + static final class AmbInnerObserver<T> extends AtomicReference<Disposable> implements Observer<T> { + + private static final long serialVersionUID = -1185974347409665484L; + final AmbCoordinator<T> parent; + final int index; + final Observer<? super T> downstream; + + boolean won; + + AmbInnerObserver(AmbCoordinator<T> parent, int index, Observer<? super T> downstream) { + this.parent = parent; + this.index = index; + this.downstream = downstream; + } + + @Override + public void onSubscribe(Disposable d) { + DisposableHelper.setOnce(this, d); + } + + @Override + public void onNext(T t) { + if (won) { + downstream.onNext(t); + } else { + if (parent.win(index)) { + won = true; + downstream.onNext(t); + } else { + get().dispose(); + } + } + } + + @Override + public void onError(Throwable t) { + if (won) { + downstream.onError(t); + } else { + if (parent.win(index)) { + won = true; + downstream.onError(t); + } else { + RxJavaPlugins.onError(t); + } + } + } + + @Override + public void onComplete() { + if (won) { + downstream.onComplete(); + } else { + if (parent.win(index)) { + won = true; + downstream.onComplete(); + } + } + } + + public void dispose() { + DisposableHelper.dispose(this); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableAny.java b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableAny.java new file mode 100644 index 0000000000..fad20eb6e2 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableAny.java @@ -0,0 +1,109 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.Predicate; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +public final class ObservableAny<T> extends AbstractObservableWithUpstream<T, Boolean> { + final Predicate<? super T> predicate; + public ObservableAny(ObservableSource<T> source, Predicate<? super T> predicate) { + super(source); + this.predicate = predicate; + } + + @Override + protected void subscribeActual(Observer<? super Boolean> t) { + source.subscribe(new AnyObserver<>(t, predicate)); + } + + static final class AnyObserver<T> implements Observer<T>, Disposable { + + final Observer<? super Boolean> downstream; + final Predicate<? super T> predicate; + + Disposable upstream; + + boolean done; + + AnyObserver(Observer<? super Boolean> actual, Predicate<? super T> predicate) { + this.downstream = actual; + this.predicate = predicate; + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.validate(this.upstream, d)) { + this.upstream = d; + downstream.onSubscribe(this); + } + } + + @Override + public void onNext(T t) { + if (done) { + return; + } + boolean b; + try { + b = predicate.test(t); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + upstream.dispose(); + onError(e); + return; + } + if (b) { + done = true; + upstream.dispose(); + downstream.onNext(true); + downstream.onComplete(); + } + } + + @Override + public void onError(Throwable t) { + if (done) { + RxJavaPlugins.onError(t); + return; + } + + done = true; + downstream.onError(t); + } + + @Override + public void onComplete() { + if (!done) { + done = true; + downstream.onNext(false); + downstream.onComplete(); + } + } + + @Override + public void dispose() { + upstream.dispose(); + } + + @Override + public boolean isDisposed() { + return upstream.isDisposed(); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableAnySingle.java b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableAnySingle.java new file mode 100644 index 0000000000..ff7875fb54 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableAnySingle.java @@ -0,0 +1,116 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.Predicate; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; +import io.reactivex.rxjava3.internal.fuseable.FuseToObservable; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +public final class ObservableAnySingle<T> extends Single<Boolean> implements FuseToObservable<Boolean> { + final ObservableSource<T> source; + + final Predicate<? super T> predicate; + + public ObservableAnySingle(ObservableSource<T> source, Predicate<? super T> predicate) { + this.source = source; + this.predicate = predicate; + } + + @Override + protected void subscribeActual(SingleObserver<? super Boolean> t) { + source.subscribe(new AnyObserver<>(t, predicate)); + } + + @Override + public Observable<Boolean> fuseToObservable() { + return RxJavaPlugins.onAssembly(new ObservableAny<>(source, predicate)); + } + + static final class AnyObserver<T> implements Observer<T>, Disposable { + + final SingleObserver<? super Boolean> downstream; + final Predicate<? super T> predicate; + + Disposable upstream; + + boolean done; + + AnyObserver(SingleObserver<? super Boolean> actual, Predicate<? super T> predicate) { + this.downstream = actual; + this.predicate = predicate; + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.validate(this.upstream, d)) { + this.upstream = d; + downstream.onSubscribe(this); + } + } + + @Override + public void onNext(T t) { + if (done) { + return; + } + boolean b; + try { + b = predicate.test(t); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + upstream.dispose(); + onError(e); + return; + } + if (b) { + done = true; + upstream.dispose(); + downstream.onSuccess(true); + } + } + + @Override + public void onError(Throwable t) { + if (done) { + RxJavaPlugins.onError(t); + return; + } + + done = true; + downstream.onError(t); + } + + @Override + public void onComplete() { + if (!done) { + done = true; + downstream.onSuccess(false); + } + } + + @Override + public void dispose() { + upstream.dispose(); + } + + @Override + public boolean isDisposed() { + return upstream.isDisposed(); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableAutoConnect.java b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableAutoConnect.java new file mode 100644 index 0000000000..d00a2fc291 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableAutoConnect.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import java.util.concurrent.atomic.AtomicInteger; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.functions.Consumer; +import io.reactivex.rxjava3.observables.ConnectableObservable; + +/** + * Wraps a ConnectableObservable and calls its connect() method once + * the specified number of Observers have subscribed. + * + * @param <T> the value type of the chain + */ +public final class ObservableAutoConnect<T> extends Observable<T> { + final ConnectableObservable<? extends T> source; + final int numberOfObservers; + final Consumer<? super Disposable> connection; + final AtomicInteger clients; + + public ObservableAutoConnect(ConnectableObservable<? extends T> source, + int numberOfObservers, + Consumer<? super Disposable> connection) { + this.source = source; + this.numberOfObservers = numberOfObservers; + this.connection = connection; + this.clients = new AtomicInteger(); + } + + @Override + public void subscribeActual(Observer<? super T> child) { + source.subscribe(child); + if (clients.incrementAndGet() == numberOfObservers) { + source.connect(connection); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableBlockingSubscribe.java b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableBlockingSubscribe.java new file mode 100644 index 0000000000..e173785760 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableBlockingSubscribe.java @@ -0,0 +1,107 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import java.util.Objects; +import java.util.concurrent.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.functions.*; +import io.reactivex.rxjava3.internal.observers.*; +import io.reactivex.rxjava3.internal.util.*; + +/** + * Utility methods to consume an Observable in a blocking manner with callbacks or Observer. + */ +public final class ObservableBlockingSubscribe { + + /** Utility class. */ + private ObservableBlockingSubscribe() { + throw new IllegalStateException("No instances!"); + } + + /** + * Subscribes to the source and calls the Observer methods on the current thread. + * <p> + * @param o the source ObservableSource + * The call to dispose() is composed through. + * @param observer the subscriber to forward events and calls to in the current thread + * @param <T> the value type + * @throws NullPointerException if {@code observer} is {@code null} + */ + public static <T> void subscribe(ObservableSource<? extends T> o, Observer<? super T> observer) { + final BlockingQueue<Object> queue = new LinkedBlockingQueue<>(); + + BlockingObserver<T> bs = new BlockingObserver<>(queue); + observer.onSubscribe(bs); + + o.subscribe(bs); + for (;;) { + if (bs.isDisposed()) { + break; + } + Object v = queue.poll(); + if (v == null) { + try { + v = queue.take(); + } catch (InterruptedException ex) { + bs.dispose(); + observer.onError(ex); + return; + } + } + if (bs.isDisposed() + || v == BlockingObserver.TERMINATED + || NotificationLite.acceptFull(v, observer)) { + break; + } + } + } + + /** + * Runs the source observable to a terminal event, ignoring any values and rethrowing any exception. + * @param o the source ObservableSource + * @param <T> the value type + */ + public static <T> void subscribe(ObservableSource<? extends T> o) { + BlockingIgnoringReceiver callback = new BlockingIgnoringReceiver(); + LambdaObserver<T> ls = new LambdaObserver<>(Functions.emptyConsumer(), + callback, callback, Functions.emptyConsumer()); + + o.subscribe(ls); + + BlockingHelper.awaitForComplete(callback, ls); + Throwable e = callback.error; + if (e != null) { + throw ExceptionHelper.wrapOrThrow(e); + } + } + + /** + * Subscribes to the source and calls the given actions on the current thread. + * @param o the source ObservableSource + * @param onNext the callback action for each source value + * @param onError the callback action for an error event + * @param onComplete the callback action for the completion event. + * @param <T> the value type + */ + public static <T> void subscribe(ObservableSource<? extends T> o, final Consumer<? super T> onNext, + final Consumer<? super Throwable> onError, final Action onComplete) { + Objects.requireNonNull(onNext, "onNext is null"); + Objects.requireNonNull(onError, "onError is null"); + Objects.requireNonNull(onComplete, "onComplete is null"); + subscribe(o, new LambdaObserver<T>(onNext, onError, onComplete, Functions.emptyConsumer())); + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableBuffer.java b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableBuffer.java new file mode 100644 index 0000000000..6f94a97cc6 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableBuffer.java @@ -0,0 +1,225 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import java.util.*; +import java.util.concurrent.atomic.AtomicBoolean; + +import io.reactivex.rxjava3.core.ObservableSource; +import io.reactivex.rxjava3.core.Observer; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.Supplier; +import io.reactivex.rxjava3.internal.disposables.*; +import io.reactivex.rxjava3.internal.util.ExceptionHelper; + +public final class ObservableBuffer<T, U extends Collection<? super T>> extends AbstractObservableWithUpstream<T, U> { + final int count; + final int skip; + final Supplier<U> bufferSupplier; + + public ObservableBuffer(ObservableSource<T> source, int count, int skip, Supplier<U> bufferSupplier) { + super(source); + this.count = count; + this.skip = skip; + this.bufferSupplier = bufferSupplier; + } + + @Override + protected void subscribeActual(Observer<? super U> t) { + if (skip == count) { + BufferExactObserver<T, U> bes = new BufferExactObserver<>(t, count, bufferSupplier); + if (bes.createBuffer()) { + source.subscribe(bes); + } + } else { + source.subscribe(new BufferSkipObserver<>(t, count, skip, bufferSupplier)); + } + } + + static final class BufferExactObserver<T, U extends Collection<? super T>> implements Observer<T>, Disposable { + final Observer<? super U> downstream; + final int count; + final Supplier<U> bufferSupplier; + U buffer; + + int size; + + Disposable upstream; + + BufferExactObserver(Observer<? super U> actual, int count, Supplier<U> bufferSupplier) { + this.downstream = actual; + this.count = count; + this.bufferSupplier = bufferSupplier; + } + + boolean createBuffer() { + U b; + try { + b = Objects.requireNonNull(bufferSupplier.get(), "Empty buffer supplied"); + } catch (Throwable t) { + Exceptions.throwIfFatal(t); + buffer = null; + if (upstream == null) { + EmptyDisposable.error(t, downstream); + } else { + upstream.dispose(); + downstream.onError(t); + } + return false; + } + + buffer = b; + + return true; + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.validate(this.upstream, d)) { + this.upstream = d; + downstream.onSubscribe(this); + } + } + + @Override + public void dispose() { + upstream.dispose(); + } + + @Override + public boolean isDisposed() { + return upstream.isDisposed(); + } + + @Override + public void onNext(T t) { + U b = buffer; + if (b != null) { + b.add(t); + + if (++size >= count) { + downstream.onNext(b); + + size = 0; + createBuffer(); + } + } + } + + @Override + public void onError(Throwable t) { + buffer = null; + downstream.onError(t); + } + + @Override + public void onComplete() { + U b = buffer; + if (b != null) { + buffer = null; + if (!b.isEmpty()) { + downstream.onNext(b); + } + downstream.onComplete(); + } + } + } + + static final class BufferSkipObserver<T, U extends Collection<? super T>> + extends AtomicBoolean implements Observer<T>, Disposable { + + private static final long serialVersionUID = -8223395059921494546L; + final Observer<? super U> downstream; + final int count; + final int skip; + final Supplier<U> bufferSupplier; + + Disposable upstream; + + final ArrayDeque<U> buffers; + + long index; + + BufferSkipObserver(Observer<? super U> actual, int count, int skip, Supplier<U> bufferSupplier) { + this.downstream = actual; + this.count = count; + this.skip = skip; + this.bufferSupplier = bufferSupplier; + this.buffers = new ArrayDeque<>(); + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.validate(this.upstream, d)) { + this.upstream = d; + downstream.onSubscribe(this); + } + } + + @Override + public void dispose() { + upstream.dispose(); + } + + @Override + public boolean isDisposed() { + return upstream.isDisposed(); + } + + @Override + public void onNext(T t) { + if (index++ % skip == 0) { + U b; + + try { + b = ExceptionHelper.nullCheck(bufferSupplier.get(), "The bufferSupplier returned a null Collection."); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + buffers.clear(); + upstream.dispose(); + downstream.onError(e); + return; + } + + buffers.offer(b); + } + + Iterator<U> it = buffers.iterator(); + while (it.hasNext()) { + U b = it.next(); + b.add(t); + if (count <= b.size()) { + it.remove(); + + downstream.onNext(b); + } + } + } + + @Override + public void onError(Throwable t) { + buffers.clear(); + downstream.onError(t); + } + + @Override + public void onComplete() { + while (!buffers.isEmpty()) { + downstream.onNext(buffers.poll()); + } + downstream.onComplete(); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableBufferBoundary.java b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableBufferBoundary.java new file mode 100644 index 0000000000..4b0fafb188 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableBufferBoundary.java @@ -0,0 +1,382 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import java.util.*; +import java.util.concurrent.atomic.*; + +import io.reactivex.rxjava3.core.ObservableSource; +import io.reactivex.rxjava3.core.Observer; +import io.reactivex.rxjava3.disposables.*; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; +import io.reactivex.rxjava3.internal.util.AtomicThrowable; +import io.reactivex.rxjava3.operators.SpscLinkedArrayQueue; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +public final class ObservableBufferBoundary<T, U extends Collection<? super T>, Open, Close> +extends AbstractObservableWithUpstream<T, U> { + final Supplier<U> bufferSupplier; + final ObservableSource<? extends Open> bufferOpen; + final Function<? super Open, ? extends ObservableSource<? extends Close>> bufferClose; + + public ObservableBufferBoundary(ObservableSource<T> source, ObservableSource<? extends Open> bufferOpen, + Function<? super Open, ? extends ObservableSource<? extends Close>> bufferClose, Supplier<U> bufferSupplier) { + super(source); + this.bufferOpen = bufferOpen; + this.bufferClose = bufferClose; + this.bufferSupplier = bufferSupplier; + } + + @Override + protected void subscribeActual(Observer<? super U> t) { + BufferBoundaryObserver<T, U, Open, Close> parent = + new BufferBoundaryObserver<>( + t, bufferOpen, bufferClose, bufferSupplier + ); + t.onSubscribe(parent); + source.subscribe(parent); + } + + static final class BufferBoundaryObserver<T, C extends Collection<? super T>, Open, Close> + extends AtomicInteger implements Observer<T>, Disposable { + + private static final long serialVersionUID = -8466418554264089604L; + + final Observer<? super C> downstream; + + final Supplier<C> bufferSupplier; + + final ObservableSource<? extends Open> bufferOpen; + + final Function<? super Open, ? extends ObservableSource<? extends Close>> bufferClose; + + final CompositeDisposable observers; + + final AtomicReference<Disposable> upstream; + + final AtomicThrowable errors; + + volatile boolean done; + + final SpscLinkedArrayQueue<C> queue; + + volatile boolean cancelled; + + long index; + + Map<Long, C> buffers; + + BufferBoundaryObserver(Observer<? super C> actual, + ObservableSource<? extends Open> bufferOpen, + Function<? super Open, ? extends ObservableSource<? extends Close>> bufferClose, + Supplier<C> bufferSupplier + ) { + this.downstream = actual; + this.bufferSupplier = bufferSupplier; + this.bufferOpen = bufferOpen; + this.bufferClose = bufferClose; + this.queue = new SpscLinkedArrayQueue<>(bufferSize()); + this.observers = new CompositeDisposable(); + this.upstream = new AtomicReference<>(); + this.buffers = new LinkedHashMap<>(); + this.errors = new AtomicThrowable(); + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.setOnce(this.upstream, d)) { + + BufferOpenObserver<Open> open = new BufferOpenObserver<>(this); + observers.add(open); + + bufferOpen.subscribe(open); + } + } + + @Override + public void onNext(T t) { + synchronized (this) { + Map<Long, C> bufs = buffers; + if (bufs == null) { + return; + } + for (C b : bufs.values()) { + b.add(t); + } + } + } + + @Override + public void onError(Throwable t) { + if (errors.tryAddThrowableOrReport(t)) { + observers.dispose(); + synchronized (this) { + buffers = null; + } + done = true; + drain(); + } + } + + @Override + public void onComplete() { + observers.dispose(); + synchronized (this) { + Map<Long, C> bufs = buffers; + if (bufs == null) { + return; + } + for (C b : bufs.values()) { + queue.offer(b); + } + buffers = null; + } + done = true; + drain(); + } + + @Override + public void dispose() { + if (DisposableHelper.dispose(upstream)) { + cancelled = true; + observers.dispose(); + synchronized (this) { + buffers = null; + } + if (getAndIncrement() != 0) { + queue.clear(); + } + } + } + + @Override + public boolean isDisposed() { + return DisposableHelper.isDisposed(upstream.get()); + } + + void open(Open token) { + ObservableSource<? extends Close> p; + C buf; + try { + buf = Objects.requireNonNull(bufferSupplier.get(), "The bufferSupplier returned a null Collection"); + p = Objects.requireNonNull(bufferClose.apply(token), "The bufferClose returned a null ObservableSource"); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + DisposableHelper.dispose(upstream); + onError(ex); + return; + } + + long idx = index; + index = idx + 1; + synchronized (this) { + Map<Long, C> bufs = buffers; + if (bufs == null) { + return; + } + bufs.put(idx, buf); + } + + BufferCloseObserver<T, C> bc = new BufferCloseObserver<>(this, idx); + observers.add(bc); + p.subscribe(bc); + } + + void openComplete(BufferOpenObserver<Open> os) { + observers.delete(os); + if (observers.size() == 0) { + DisposableHelper.dispose(upstream); + done = true; + drain(); + } + } + + void close(BufferCloseObserver<T, C> closer, long idx) { + observers.delete(closer); + boolean makeDone = false; + if (observers.size() == 0) { + makeDone = true; + DisposableHelper.dispose(upstream); + } + synchronized (this) { + Map<Long, C> bufs = buffers; + if (bufs == null) { + return; + } + queue.offer(buffers.remove(idx)); + } + if (makeDone) { + done = true; + } + drain(); + } + + void boundaryError(Disposable observer, Throwable ex) { + DisposableHelper.dispose(upstream); + observers.delete(observer); + onError(ex); + } + + void drain() { + if (getAndIncrement() != 0) { + return; + } + + int missed = 1; + Observer<? super C> a = downstream; + SpscLinkedArrayQueue<C> q = queue; + + for (;;) { + for (;;) { + if (cancelled) { + q.clear(); + return; + } + + boolean d = done; + if (d && errors.get() != null) { + q.clear(); + errors.tryTerminateConsumer(a); + return; + } + + C v = q.poll(); + boolean empty = v == null; + + if (d && empty) { + a.onComplete(); + return; + } + + if (empty) { + break; + } + + a.onNext(v); + } + + missed = addAndGet(-missed); + if (missed == 0) { + break; + } + } + } + + static final class BufferOpenObserver<Open> + extends AtomicReference<Disposable> + implements Observer<Open>, Disposable { + + private static final long serialVersionUID = -8498650778633225126L; + + final BufferBoundaryObserver<?, ?, Open, ?> parent; + + BufferOpenObserver(BufferBoundaryObserver<?, ?, Open, ?> parent) { + this.parent = parent; + } + + @Override + public void onSubscribe(Disposable d) { + DisposableHelper.setOnce(this, d); + } + + @Override + public void onNext(Open t) { + parent.open(t); + } + + @Override + public void onError(Throwable t) { + lazySet(DisposableHelper.DISPOSED); + parent.boundaryError(this, t); + } + + @Override + public void onComplete() { + lazySet(DisposableHelper.DISPOSED); + parent.openComplete(this); + } + + @Override + public void dispose() { + DisposableHelper.dispose(this); + } + + @Override + public boolean isDisposed() { + return get() == DisposableHelper.DISPOSED; + } + } + } + + static final class BufferCloseObserver<T, C extends Collection<? super T>> + extends AtomicReference<Disposable> + implements Observer<Object>, Disposable { + + private static final long serialVersionUID = -8498650778633225126L; + + final BufferBoundaryObserver<T, C, ?, ?> parent; + + final long index; + + BufferCloseObserver(BufferBoundaryObserver<T, C, ?, ?> parent, long index) { + this.parent = parent; + this.index = index; + } + + @Override + public void onSubscribe(Disposable d) { + DisposableHelper.setOnce(this, d); + } + + @Override + public void onNext(Object t) { + Disposable upstream = get(); + if (upstream != DisposableHelper.DISPOSED) { + lazySet(DisposableHelper.DISPOSED); + upstream.dispose(); + parent.close(this, index); + } + } + + @Override + public void onError(Throwable t) { + if (get() != DisposableHelper.DISPOSED) { + lazySet(DisposableHelper.DISPOSED); + parent.boundaryError(this, t); + } else { + RxJavaPlugins.onError(t); + } + } + + @Override + public void onComplete() { + if (get() != DisposableHelper.DISPOSED) { + lazySet(DisposableHelper.DISPOSED); + parent.close(this, index); + } + } + + @Override + public void dispose() { + DisposableHelper.dispose(this); + } + + @Override + public boolean isDisposed() { + return get() == DisposableHelper.DISPOSED; + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableBufferExactBoundary.java b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableBufferExactBoundary.java new file mode 100644 index 0000000000..bb65058908 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableBufferExactBoundary.java @@ -0,0 +1,201 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import java.util.Collection; +import java.util.Objects; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.Supplier; +import io.reactivex.rxjava3.internal.disposables.*; +import io.reactivex.rxjava3.internal.observers.QueueDrainObserver; +import io.reactivex.rxjava3.internal.queue.MpscLinkedQueue; +import io.reactivex.rxjava3.internal.util.QueueDrainHelper; +import io.reactivex.rxjava3.observers.*; + +public final class ObservableBufferExactBoundary<T, U extends Collection<? super T>, B> +extends AbstractObservableWithUpstream<T, U> { + final ObservableSource<B> boundary; + final Supplier<U> bufferSupplier; + + public ObservableBufferExactBoundary(ObservableSource<T> source, ObservableSource<B> boundary, Supplier<U> bufferSupplier) { + super(source); + this.boundary = boundary; + this.bufferSupplier = bufferSupplier; + } + + @Override + protected void subscribeActual(Observer<? super U> t) { + source.subscribe(new BufferExactBoundaryObserver<>(new SerializedObserver<>(t), bufferSupplier, boundary)); + } + + static final class BufferExactBoundaryObserver<T, U extends Collection<? super T>, B> + extends QueueDrainObserver<T, U, U> implements Disposable { + + final Supplier<U> bufferSupplier; + final ObservableSource<B> boundary; + + Disposable upstream; + + Disposable other; + + U buffer; + + BufferExactBoundaryObserver(Observer<? super U> actual, Supplier<U> bufferSupplier, + ObservableSource<B> boundary) { + super(actual, new MpscLinkedQueue<>()); + this.bufferSupplier = bufferSupplier; + this.boundary = boundary; + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.validate(this.upstream, d)) { + this.upstream = d; + + U b; + + try { + b = Objects.requireNonNull(bufferSupplier.get(), "The buffer supplied is null"); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + cancelled = true; + d.dispose(); + EmptyDisposable.error(e, downstream); + return; + } + + buffer = b; + + BufferBoundaryObserver<T, U, B> bs = new BufferBoundaryObserver<>(this); + other = bs; + + downstream.onSubscribe(this); + + if (!cancelled) { + boundary.subscribe(bs); + } + } + } + + @Override + public void onNext(T t) { + synchronized (this) { + U b = buffer; + if (b == null) { + return; + } + b.add(t); + } + } + + @Override + public void onError(Throwable t) { + dispose(); + downstream.onError(t); + } + + @Override + public void onComplete() { + U b; + synchronized (this) { + b = buffer; + if (b == null) { + return; + } + buffer = null; + } + queue.offer(b); + done = true; + if (enter()) { + QueueDrainHelper.drainLoop(queue, downstream, false, this, this); + } + } + + @Override + public void dispose() { + if (!cancelled) { + cancelled = true; + other.dispose(); + upstream.dispose(); + + if (enter()) { + queue.clear(); + } + } + } + + @Override + public boolean isDisposed() { + return cancelled; + } + + void next() { + + U next; + + try { + next = Objects.requireNonNull(bufferSupplier.get(), "The buffer supplied is null"); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + dispose(); + downstream.onError(e); + return; + } + + U b; + synchronized (this) { + b = buffer; + if (b == null) { + return; + } + buffer = next; + } + + fastPathEmit(b, false, this); + } + + @Override + public void accept(Observer<? super U> a, U v) { + downstream.onNext(v); + } + + } + + static final class BufferBoundaryObserver<T, U extends Collection<? super T>, B> + extends DisposableObserver<B> { + final BufferExactBoundaryObserver<T, U, B> parent; + + BufferBoundaryObserver(BufferExactBoundaryObserver<T, U, B> parent) { + this.parent = parent; + } + + @Override + public void onNext(B t) { + parent.next(); + } + + @Override + public void onError(Throwable t) { + parent.onError(t); + } + + @Override + public void onComplete() { + parent.onComplete(); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableBufferTimed.java b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableBufferTimed.java new file mode 100644 index 0000000000..7628a8f685 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableBufferTimed.java @@ -0,0 +1,562 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import java.util.*; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.core.Observer; +import io.reactivex.rxjava3.core.Scheduler.Worker; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.Supplier; +import io.reactivex.rxjava3.internal.disposables.*; +import io.reactivex.rxjava3.internal.observers.QueueDrainObserver; +import io.reactivex.rxjava3.internal.queue.MpscLinkedQueue; +import io.reactivex.rxjava3.internal.util.QueueDrainHelper; +import io.reactivex.rxjava3.observers.SerializedObserver; + +public final class ObservableBufferTimed<T, U extends Collection<? super T>> +extends AbstractObservableWithUpstream<T, U> { + + final long timespan; + final long timeskip; + final TimeUnit unit; + final Scheduler scheduler; + final Supplier<U> bufferSupplier; + final int maxSize; + final boolean restartTimerOnMaxSize; + + public ObservableBufferTimed(ObservableSource<T> source, long timespan, long timeskip, TimeUnit unit, Scheduler scheduler, Supplier<U> bufferSupplier, int maxSize, + boolean restartTimerOnMaxSize) { + super(source); + this.timespan = timespan; + this.timeskip = timeskip; + this.unit = unit; + this.scheduler = scheduler; + this.bufferSupplier = bufferSupplier; + this.maxSize = maxSize; + this.restartTimerOnMaxSize = restartTimerOnMaxSize; + } + + @Override + protected void subscribeActual(Observer<? super U> t) { + if (timespan == timeskip && maxSize == Integer.MAX_VALUE) { + source.subscribe(new BufferExactUnboundedObserver<>( + new SerializedObserver<>(t), + bufferSupplier, timespan, unit, scheduler)); + return; + } + Scheduler.Worker w = scheduler.createWorker(); + + if (timespan == timeskip) { + source.subscribe(new BufferExactBoundedObserver<>( + new SerializedObserver<>(t), + bufferSupplier, + timespan, unit, maxSize, restartTimerOnMaxSize, w + )); + return; + } + // Can't use maxSize because what to do if a buffer is full but its + // timespan hasn't been elapsed? + source.subscribe(new BufferSkipBoundedObserver<>( + new SerializedObserver<>(t), + bufferSupplier, timespan, timeskip, unit, w)); + + } + + static final class BufferExactUnboundedObserver<T, U extends Collection<? super T>> + extends QueueDrainObserver<T, U, U> implements Runnable, Disposable { + final Supplier<U> bufferSupplier; + final long timespan; + final TimeUnit unit; + final Scheduler scheduler; + + Disposable upstream; + + U buffer; + + final AtomicReference<Disposable> timer = new AtomicReference<>(); + + BufferExactUnboundedObserver( + Observer<? super U> actual, Supplier<U> bufferSupplier, + long timespan, TimeUnit unit, Scheduler scheduler) { + super(actual, new MpscLinkedQueue<>()); + this.bufferSupplier = bufferSupplier; + this.timespan = timespan; + this.unit = unit; + this.scheduler = scheduler; + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.validate(this.upstream, d)) { + this.upstream = d; + + U b; + + try { + b = Objects.requireNonNull(bufferSupplier.get(), "The buffer supplied is null"); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + dispose(); + EmptyDisposable.error(e, downstream); + return; + } + + buffer = b; + + downstream.onSubscribe(this); + + if (!DisposableHelper.isDisposed(timer.get())) { + Disposable task = scheduler.schedulePeriodicallyDirect(this, timespan, timespan, unit); + DisposableHelper.set(timer, task); + } + } + } + + @Override + public void onNext(T t) { + synchronized (this) { + U b = buffer; + if (b == null) { + return; + } + b.add(t); + } + } + + @Override + public void onError(Throwable t) { + synchronized (this) { + buffer = null; + } + downstream.onError(t); + DisposableHelper.dispose(timer); + } + + @Override + public void onComplete() { + U b; + synchronized (this) { + b = buffer; + buffer = null; + } + if (b != null) { + queue.offer(b); + done = true; + if (enter()) { + QueueDrainHelper.drainLoop(queue, downstream, false, null, this); + } + } + DisposableHelper.dispose(timer); + } + + @Override + public void dispose() { + DisposableHelper.dispose(timer); + upstream.dispose(); + } + + @Override + public boolean isDisposed() { + return timer.get() == DisposableHelper.DISPOSED; + } + + @Override + public void run() { + U next; + + try { + next = Objects.requireNonNull(bufferSupplier.get(), "The bufferSupplier returned a null buffer"); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + downstream.onError(e); + dispose(); + return; + } + + U current; + + synchronized (this) { + current = buffer; + if (current != null) { + buffer = next; + } + } + + if (current == null) { + DisposableHelper.dispose(timer); + return; + } + + fastPathEmit(current, false, this); + } + + @Override + public void accept(Observer<? super U> a, U v) { + downstream.onNext(v); + } + } + + static final class BufferSkipBoundedObserver<T, U extends Collection<? super T>> + extends QueueDrainObserver<T, U, U> implements Runnable, Disposable { + final Supplier<U> bufferSupplier; + final long timespan; + final long timeskip; + final TimeUnit unit; + final Worker w; + final List<U> buffers; + + Disposable upstream; + + BufferSkipBoundedObserver(Observer<? super U> actual, + Supplier<U> bufferSupplier, long timespan, + long timeskip, TimeUnit unit, Worker w) { + super(actual, new MpscLinkedQueue<>()); + this.bufferSupplier = bufferSupplier; + this.timespan = timespan; + this.timeskip = timeskip; + this.unit = unit; + this.w = w; + this.buffers = new LinkedList<>(); + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.validate(this.upstream, d)) { + this.upstream = d; + + final U b; // NOPMD + + try { + b = Objects.requireNonNull(bufferSupplier.get(), "The buffer supplied is null"); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + d.dispose(); + EmptyDisposable.error(e, downstream); + w.dispose(); + return; + } + + buffers.add(b); + + downstream.onSubscribe(this); + + w.schedulePeriodically(this, timeskip, timeskip, unit); + + w.schedule(new RemoveFromBufferEmit(b), timespan, unit); + } + } + + @Override + public void onNext(T t) { + synchronized (this) { + for (U b : buffers) { + b.add(t); + } + } + } + + @Override + public void onError(Throwable t) { + done = true; + clear(); + downstream.onError(t); + w.dispose(); + } + + @Override + public void onComplete() { + List<U> bs; + synchronized (this) { + bs = new ArrayList<>(buffers); + buffers.clear(); + } + + for (U b : bs) { + queue.offer(b); + } + done = true; + if (enter()) { + QueueDrainHelper.drainLoop(queue, downstream, false, w, this); + } + } + + @Override + public void dispose() { + if (!cancelled) { + cancelled = true; + clear(); + upstream.dispose(); + w.dispose(); + } + } + + @Override + public boolean isDisposed() { + return cancelled; + } + + void clear() { + synchronized (this) { + buffers.clear(); + } + } + + @Override + public void run() { + if (cancelled) { + return; + } + final U b; // NOPMD + + try { + b = Objects.requireNonNull(bufferSupplier.get(), "The bufferSupplier returned a null buffer"); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + downstream.onError(e); + dispose(); + return; + } + + synchronized (this) { + if (cancelled) { + return; + } + buffers.add(b); + } + + w.schedule(new RemoveFromBuffer(b), timespan, unit); + } + + @Override + public void accept(Observer<? super U> a, U v) { + a.onNext(v); + } + + final class RemoveFromBuffer implements Runnable { + private final U b; + + RemoveFromBuffer(U b) { + this.b = b; + } + + @Override + public void run() { + synchronized (BufferSkipBoundedObserver.this) { + buffers.remove(b); + } + + fastPathOrderedEmit(b, false, w); + } + } + + final class RemoveFromBufferEmit implements Runnable { + private final U buffer; + + RemoveFromBufferEmit(U buffer) { + this.buffer = buffer; + } + + @Override + public void run() { + synchronized (BufferSkipBoundedObserver.this) { + buffers.remove(buffer); + } + + fastPathOrderedEmit(buffer, false, w); + } + } + } + + static final class BufferExactBoundedObserver<T, U extends Collection<? super T>> + extends QueueDrainObserver<T, U, U> implements Runnable, Disposable { + final Supplier<U> bufferSupplier; + final long timespan; + final TimeUnit unit; + final int maxSize; + final boolean restartTimerOnMaxSize; + final Worker w; + + U buffer; + + Disposable timer; + + Disposable upstream; + + long producerIndex; + + long consumerIndex; + + BufferExactBoundedObserver( + Observer<? super U> actual, + Supplier<U> bufferSupplier, + long timespan, TimeUnit unit, int maxSize, + boolean restartOnMaxSize, Worker w) { + super(actual, new MpscLinkedQueue<>()); + this.bufferSupplier = bufferSupplier; + this.timespan = timespan; + this.unit = unit; + this.maxSize = maxSize; + this.restartTimerOnMaxSize = restartOnMaxSize; + this.w = w; + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.validate(this.upstream, d)) { + this.upstream = d; + + U b; + + try { + b = Objects.requireNonNull(bufferSupplier.get(), "The buffer supplied is null"); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + d.dispose(); + EmptyDisposable.error(e, downstream); + w.dispose(); + return; + } + + buffer = b; + + downstream.onSubscribe(this); + + timer = w.schedulePeriodically(this, timespan, timespan, unit); + } + } + + @Override + public void onNext(T t) { + U b; + synchronized (this) { + b = buffer; + if (b == null) { + return; + } + + b.add(t); + + if (b.size() < maxSize) { + return; + } + buffer = null; + producerIndex++; + } + + if (restartTimerOnMaxSize) { + timer.dispose(); + } + + fastPathOrderedEmit(b, false, this); + + try { + b = Objects.requireNonNull(bufferSupplier.get(), "The buffer supplied is null"); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + downstream.onError(e); + dispose(); + return; + } + + synchronized (this) { + buffer = b; + consumerIndex++; + } + if (restartTimerOnMaxSize) { + timer = w.schedulePeriodically(this, timespan, timespan, unit); + } + } + + @Override + public void onError(Throwable t) { + synchronized (this) { + buffer = null; + } + downstream.onError(t); + w.dispose(); + } + + @Override + public void onComplete() { + w.dispose(); + + U b; + synchronized (this) { + b = buffer; + buffer = null; + } + + if (b != null) { + queue.offer(b); + done = true; + if (enter()) { + QueueDrainHelper.drainLoop(queue, downstream, false, this, this); + } + } + } + + @Override + public void accept(Observer<? super U> a, U v) { + a.onNext(v); + } + + @Override + public void dispose() { + if (!cancelled) { + cancelled = true; + upstream.dispose(); + w.dispose(); + synchronized (this) { + buffer = null; + } + } + } + + @Override + public boolean isDisposed() { + return cancelled; + } + + @Override + public void run() { + U next; + + try { + next = Objects.requireNonNull(bufferSupplier.get(), "The bufferSupplier returned a null buffer"); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + dispose(); + downstream.onError(e); + return; + } + + U current; + + synchronized (this) { + current = buffer; + if (current == null || producerIndex != consumerIndex) { + return; + } + buffer = next; + } + + fastPathOrderedEmit(current, false, this); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableCache.java b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableCache.java new file mode 100644 index 0000000000..daa9edd533 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableCache.java @@ -0,0 +1,399 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import java.util.concurrent.atomic.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; + +/** + * An observable which auto-connects to another observable, caches the elements + * from that observable but allows terminating the connection and completing the cache. + * + * @param <T> the source element type + */ +public final class ObservableCache<T> extends AbstractObservableWithUpstream<T, T> +implements Observer<T> { + + /** + * The subscription to the source should happen at most once. + */ + final AtomicBoolean once; + + /** + * The number of items per cached nodes. + */ + final int capacityHint; + + /** + * The current known array of observer state to notify. + */ + final AtomicReference<CacheDisposable<T>[]> observers; + + /** + * A shared instance of an empty array of observers to avoid creating + * a new empty array when all observers dispose. + */ + @SuppressWarnings("rawtypes") + static final CacheDisposable[] EMPTY = new CacheDisposable[0]; + /** + * A shared instance indicating the source has no more events and there + * is no need to remember observers anymore. + */ + @SuppressWarnings("rawtypes") + static final CacheDisposable[] TERMINATED = new CacheDisposable[0]; + + /** + * The total number of elements in the list available for reads. + */ + volatile long size; + + /** + * The starting point of the cached items. + */ + final Node<T> head; + + /** + * The current tail of the linked structure holding the items. + */ + Node<T> tail; + + /** + * How many items have been put into the tail node so far. + */ + int tailOffset; + + /** + * If {@link #observers} is {@link #TERMINATED}, this holds the terminal error if not null. + */ + Throwable error; + + /** + * True if the source has terminated. + */ + volatile boolean done; + + /** + * Constructs an empty, non-connected cache. + * @param source the source to subscribe to for the first incoming observer + * @param capacityHint the number of items expected (reduce allocation frequency) + */ + @SuppressWarnings("unchecked") + public ObservableCache(Observable<T> source, int capacityHint) { + super(source); + this.capacityHint = capacityHint; + this.once = new AtomicBoolean(); + Node<T> n = new Node<>(capacityHint); + this.head = n; + this.tail = n; + this.observers = new AtomicReference<>(EMPTY); + } + + @Override + protected void subscribeActual(Observer<? super T> t) { + CacheDisposable<T> consumer = new CacheDisposable<>(t, this); + t.onSubscribe(consumer); + add(consumer); + + if (!once.get() && once.compareAndSet(false, true)) { + source.subscribe(this); + } else { + replay(consumer); + } + } + + /** + * Check if this cached observable is connected to its source. + * @return true if already connected + */ + /* public */boolean isConnected() { + return once.get(); + } + + /** + * Returns true if there are observers subscribed to this observable. + * @return true if the cache has observers + */ + /* public */ boolean hasObservers() { + return observers.get().length != 0; + } + + /** + * Returns the number of events currently cached. + * @return the number of currently cached event count + */ + /* public */ long cachedEventCount() { + return size; + } + + /** + * Atomically adds the consumer to the {@link #observers} copy-on-write array + * if the source has not yet terminated. + * @param consumer the consumer to add + */ + void add(CacheDisposable<T> consumer) { + for (;;) { + CacheDisposable<T>[] current = observers.get(); + if (current == TERMINATED) { + return; + } + int n = current.length; + + @SuppressWarnings("unchecked") + CacheDisposable<T>[] next = new CacheDisposable[n + 1]; + System.arraycopy(current, 0, next, 0, n); + next[n] = consumer; + + if (observers.compareAndSet(current, next)) { + return; + } + } + } + + /** + * Atomically removes the consumer from the {@link #observers} copy-on-write array. + * @param consumer the consumer to remove + */ + @SuppressWarnings("unchecked") + void remove(CacheDisposable<T> consumer) { + for (;;) { + CacheDisposable<T>[] current = observers.get(); + int n = current.length; + if (n == 0) { + return; + } + + int j = -1; + for (int i = 0; i < n; i++) { + if (current[i] == consumer) { + j = i; + break; + } + } + + if (j < 0) { + return; + } + CacheDisposable<T>[] next; + + if (n == 1) { + next = EMPTY; + } else { + next = new CacheDisposable[n - 1]; + System.arraycopy(current, 0, next, 0, j); + System.arraycopy(current, j + 1, next, j, n - j - 1); + } + + if (observers.compareAndSet(current, next)) { + return; + } + } + } + + /** + * Replays the contents of this cache to the given consumer based on its + * current state and number of items requested by it. + * @param consumer the consumer to continue replaying items to + */ + void replay(CacheDisposable<T> consumer) { + // make sure there is only one replay going on at a time + if (consumer.getAndIncrement() != 0) { + return; + } + + // see if there were more replay request in the meantime + int missed = 1; + // read out state into locals upfront to avoid being re-read due to volatile reads + long index = consumer.index; + int offset = consumer.offset; + Node<T> node = consumer.node; + Observer<? super T> downstream = consumer.downstream; + int capacity = capacityHint; + + for (;;) { + // if the consumer got disposed, clear the node and quit + if (consumer.disposed) { + consumer.node = null; + return; + } + + // first see if the source has terminated, read order matters! + boolean sourceDone = done; + // and if the number of items is the same as this consumer has received + boolean empty = size == index; + + // if the source is done and we have all items so far, terminate the consumer + if (sourceDone && empty) { + // release the node object to avoid leaks through retained consumers + consumer.node = null; + // if error is not null then the source failed + Throwable ex = error; + if (ex != null) { + downstream.onError(ex); + } else { + downstream.onComplete(); + } + return; + } + + // there are still items not sent to the consumer + if (!empty) { + // if the offset in the current node has reached the node capacity + if (offset == capacity) { + // switch to the subsequent node + node = node.next; + // reset the in-node offset + offset = 0; + } + + // emit the cached item + downstream.onNext(node.values[offset]); + + // move the node offset forward + offset++; + // move the total consumed item count forward + index++; + + // retry for the next item/terminal event if any + continue; + } + + // commit the changed references back + consumer.index = index; + consumer.offset = offset; + consumer.node = node; + // release the changes and see if there were more replay request in the meantime + missed = consumer.addAndGet(-missed); + if (missed == 0) { + break; + } + } + } + + @Override + public void onSubscribe(Disposable d) { + // we can't do much with the upstream disposable + } + + @Override + public void onNext(T t) { + int tailOffset = this.tailOffset; + // if the current tail node is full, create a fresh node + if (tailOffset == capacityHint) { + Node<T> n = new Node<>(tailOffset); + n.values[0] = t; + this.tailOffset = 1; + tail.next = n; + tail = n; + } else { + tail.values[tailOffset] = t; + this.tailOffset = tailOffset + 1; + } + size++; + for (CacheDisposable<T> consumer : observers.get()) { + replay(consumer); + } + } + + @SuppressWarnings("unchecked") + @Override + public void onError(Throwable t) { + error = t; + done = true; + for (CacheDisposable<T> consumer : observers.getAndSet(TERMINATED)) { + replay(consumer); + } + } + + @SuppressWarnings("unchecked") + @Override + public void onComplete() { + done = true; + for (CacheDisposable<T> consumer : observers.getAndSet(TERMINATED)) { + replay(consumer); + } + } + + /** + * Hosts the downstream consumer and its current requested and replay states. + * {@code this} holds the work-in-progress counter for the serialized replay. + * @param <T> the value type + */ + static final class CacheDisposable<T> extends AtomicInteger + implements Disposable { + + private static final long serialVersionUID = 6770240836423125754L; + + final Observer<? super T> downstream; + + final ObservableCache<T> parent; + + Node<T> node; + + int offset; + + long index; + + volatile boolean disposed; + + /** + * Constructs a new instance with the actual downstream consumer and + * the parent cache object. + * @param downstream the actual consumer + * @param parent the parent that holds onto the cached items + */ + CacheDisposable(Observer<? super T> downstream, ObservableCache<T> parent) { + this.downstream = downstream; + this.parent = parent; + this.node = parent.head; + } + + @Override + public void dispose() { + if (!disposed) { + disposed = true; + parent.remove(this); + } + } + + @Override + public boolean isDisposed() { + return disposed; + } + } + + /** + * Represents a segment of the cached item list as + * part of a linked-node-list structure. + * @param <T> the element type + */ + static final class Node<T> { + + /** + * The array of values held by this node. + */ + final T[] values; + + /** + * The next node if not null. + */ + volatile Node<T> next; + + @SuppressWarnings("unchecked") + Node(int capacityHint) { + this.values = (T[])new Object[capacityHint]; + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableCollect.java b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableCollect.java new file mode 100644 index 0000000000..6b700addd7 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableCollect.java @@ -0,0 +1,118 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.disposables.*; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +import java.util.Objects; + +public final class ObservableCollect<T, U> extends AbstractObservableWithUpstream<T, U> { + final Supplier<? extends U> initialSupplier; + final BiConsumer<? super U, ? super T> collector; + + public ObservableCollect(ObservableSource<T> source, + Supplier<? extends U> initialSupplier, BiConsumer<? super U, ? super T> collector) { + super(source); + this.initialSupplier = initialSupplier; + this.collector = collector; + } + + @Override + protected void subscribeActual(Observer<? super U> t) { + U u; + try { + u = Objects.requireNonNull(initialSupplier.get(), "The initialSupplier returned a null value"); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + EmptyDisposable.error(e, t); + return; + } + + source.subscribe(new CollectObserver<>(t, u, collector)); + + } + + static final class CollectObserver<T, U> implements Observer<T>, Disposable { + final Observer<? super U> downstream; + final BiConsumer<? super U, ? super T> collector; + final U u; + + Disposable upstream; + + boolean done; + + CollectObserver(Observer<? super U> actual, U u, BiConsumer<? super U, ? super T> collector) { + this.downstream = actual; + this.collector = collector; + this.u = u; + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.validate(this.upstream, d)) { + this.upstream = d; + downstream.onSubscribe(this); + } + } + + @Override + public void dispose() { + upstream.dispose(); + } + + @Override + public boolean isDisposed() { + return upstream.isDisposed(); + } + + @Override + public void onNext(T t) { + if (done) { + return; + } + try { + collector.accept(u, t); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + upstream.dispose(); + onError(e); + } + } + + @Override + public void onError(Throwable t) { + if (done) { + RxJavaPlugins.onError(t); + return; + } + done = true; + downstream.onError(t); + } + + @Override + public void onComplete() { + if (done) { + return; + } + done = true; + downstream.onNext(u); + downstream.onComplete(); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableCollectSingle.java b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableCollectSingle.java new file mode 100644 index 0000000000..88137432ac --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableCollectSingle.java @@ -0,0 +1,125 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.disposables.*; +import io.reactivex.rxjava3.internal.fuseable.FuseToObservable; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +import java.util.Objects; + +public final class ObservableCollectSingle<T, U> extends Single<U> implements FuseToObservable<U> { + + final ObservableSource<T> source; + + final Supplier<? extends U> initialSupplier; + final BiConsumer<? super U, ? super T> collector; + + public ObservableCollectSingle(ObservableSource<T> source, + Supplier<? extends U> initialSupplier, BiConsumer<? super U, ? super T> collector) { + this.source = source; + this.initialSupplier = initialSupplier; + this.collector = collector; + } + + @Override + protected void subscribeActual(SingleObserver<? super U> t) { + U u; + try { + u = Objects.requireNonNull(initialSupplier.get(), "The initialSupplier returned a null value"); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + EmptyDisposable.error(e, t); + return; + } + + source.subscribe(new CollectObserver<>(t, u, collector)); + } + + @Override + public Observable<U> fuseToObservable() { + return RxJavaPlugins.onAssembly(new ObservableCollect<>(source, initialSupplier, collector)); + } + + static final class CollectObserver<T, U> implements Observer<T>, Disposable { + final SingleObserver<? super U> downstream; + final BiConsumer<? super U, ? super T> collector; + final U u; + + Disposable upstream; + + boolean done; + + CollectObserver(SingleObserver<? super U> actual, U u, BiConsumer<? super U, ? super T> collector) { + this.downstream = actual; + this.collector = collector; + this.u = u; + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.validate(this.upstream, d)) { + this.upstream = d; + downstream.onSubscribe(this); + } + } + + @Override + public void dispose() { + upstream.dispose(); + } + + @Override + public boolean isDisposed() { + return upstream.isDisposed(); + } + + @Override + public void onNext(T t) { + if (done) { + return; + } + try { + collector.accept(u, t); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + upstream.dispose(); + onError(e); + } + } + + @Override + public void onError(Throwable t) { + if (done) { + RxJavaPlugins.onError(t); + return; + } + done = true; + downstream.onError(t); + } + + @Override + public void onComplete() { + if (done) { + return; + } + done = true; + downstream.onSuccess(u); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableCombineLatest.java b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableCombineLatest.java new file mode 100644 index 0000000000..fded0d0ef5 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableCombineLatest.java @@ -0,0 +1,318 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import java.util.Objects; +import java.util.concurrent.atomic.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.disposables.*; +import io.reactivex.rxjava3.internal.util.AtomicThrowable; +import io.reactivex.rxjava3.operators.SpscLinkedArrayQueue; + +public final class ObservableCombineLatest<T, R> extends Observable<R> { + final ObservableSource<? extends T>[] sources; + final Iterable<? extends ObservableSource<? extends T>> sourcesIterable; + final Function<? super Object[], ? extends R> combiner; + final int bufferSize; + final boolean delayError; + + public ObservableCombineLatest(ObservableSource<? extends T>[] sources, + Iterable<? extends ObservableSource<? extends T>> sourcesIterable, + Function<? super Object[], ? extends R> combiner, int bufferSize, + boolean delayError) { + this.sources = sources; + this.sourcesIterable = sourcesIterable; + this.combiner = combiner; + this.bufferSize = bufferSize; + this.delayError = delayError; + } + + @Override + @SuppressWarnings("unchecked") + public void subscribeActual(Observer<? super R> observer) { + ObservableSource<? extends T>[] sources = this.sources; + int count = 0; + if (sources == null) { + sources = new ObservableSource[8]; + try { + for (ObservableSource<? extends T> p : sourcesIterable) { + if (count == sources.length) { + ObservableSource<? extends T>[] b = new ObservableSource[count + (count >> 2)]; + System.arraycopy(sources, 0, b, 0, count); + sources = b; + } + sources[count++] = Objects.requireNonNull(p, "The Iterator returned a null ObservableSource"); + } + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + EmptyDisposable.error(ex, observer); + return; + } + } else { + count = sources.length; + } + + if (count == 0) { + EmptyDisposable.complete(observer); + return; + } + + LatestCoordinator<T, R> lc = new LatestCoordinator<>(observer, combiner, count, bufferSize, delayError); + lc.subscribe(sources); + } + + static final class LatestCoordinator<T, R> extends AtomicInteger implements Disposable { + + private static final long serialVersionUID = 8567835998786448817L; + final Observer<? super R> downstream; + final Function<? super Object[], ? extends R> combiner; + final CombinerObserver<T, R>[] observers; + Object[] latest; + final SpscLinkedArrayQueue<Object[]> queue; + final boolean delayError; + + volatile boolean cancelled; + + volatile boolean done; + + final AtomicThrowable errors = new AtomicThrowable(); + + int active; + int complete; + + @SuppressWarnings("unchecked") + LatestCoordinator(Observer<? super R> actual, + Function<? super Object[], ? extends R> combiner, + int count, int bufferSize, boolean delayError) { + this.downstream = actual; + this.combiner = combiner; + this.delayError = delayError; + this.latest = new Object[count]; + CombinerObserver<T, R>[] as = new CombinerObserver[count]; + for (int i = 0; i < count; i++) { + as[i] = new CombinerObserver<>(this, i); + } + this.observers = as; + this.queue = new SpscLinkedArrayQueue<>(bufferSize); + } + + public void subscribe(ObservableSource<? extends T>[] sources) { + Observer<T>[] as = observers; + int len = as.length; + downstream.onSubscribe(this); + for (int i = 0; i < len; i++) { + if (done || cancelled) { + return; + } + sources[i].subscribe(as[i]); + } + } + + @Override + public void dispose() { + if (!cancelled) { + cancelled = true; + cancelSources(); + drain(); + } + } + + @Override + public boolean isDisposed() { + return cancelled; + } + + void cancelSources() { + for (CombinerObserver<T, R> observer : observers) { + observer.dispose(); + } + } + + void clear(SpscLinkedArrayQueue<?> q) { + synchronized (this) { + latest = null; + } + q.clear(); + } + + void drain() { + if (getAndIncrement() != 0) { + return; + } + + final SpscLinkedArrayQueue<Object[]> q = queue; + final Observer<? super R> a = downstream; + final boolean delayError = this.delayError; + + int missed = 1; + for (;;) { + + for (;;) { + if (cancelled) { + clear(q); + errors.tryTerminateAndReport(); + return; + } + + if (!delayError && errors.get() != null) { + cancelSources(); + clear(q); + errors.tryTerminateConsumer(a); + return; + } + + boolean d = done; + Object[] s = q.poll(); + boolean empty = s == null; + + if (d && empty) { + clear(q); + errors.tryTerminateConsumer(a); + return; + } + + if (empty) { + break; + } + + R v; + + try { + v = Objects.requireNonNull(combiner.apply(s), "The combiner returned a null value"); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + errors.tryAddThrowableOrReport(ex); + cancelSources(); + clear(q); + errors.tryTerminateConsumer(a); + return; + } + + a.onNext(v); + } + + missed = addAndGet(-missed); + if (missed == 0) { + break; + } + } + } + + void innerNext(int index, T item) { + boolean shouldDrain = false; + synchronized (this) { + Object[] latest = this.latest; + if (latest == null) { + return; + } + Object o = latest[index]; + int a = active; + if (o == null) { + active = ++a; + } + latest[index] = item; + if (a == latest.length) { + queue.offer(latest.clone()); + shouldDrain = true; + } + } + if (shouldDrain) { + drain(); + } + } + + void innerError(int index, Throwable ex) { + if (errors.tryAddThrowableOrReport(ex)) { + boolean cancelOthers = true; + if (delayError) { + synchronized (this) { + Object[] latest = this.latest; + if (latest == null) { + return; + } + cancelOthers = latest[index] == null; + if (cancelOthers || ++complete == latest.length) { + done = true; + } + } + } + if (cancelOthers) { + cancelSources(); + } + drain(); + } + } + + void innerComplete(int index) { + boolean cancelOthers = false; + synchronized (this) { + Object[] latest = this.latest; + if (latest == null) { + return; + } + + cancelOthers = latest[index] == null; + if (cancelOthers || ++complete == latest.length) { + done = true; + } + } + if (cancelOthers) { + cancelSources(); + } + drain(); + } + + } + + static final class CombinerObserver<T, R> extends AtomicReference<Disposable> implements Observer<T> { + private static final long serialVersionUID = -4823716997131257941L; + + final LatestCoordinator<T, R> parent; + + final int index; + + CombinerObserver(LatestCoordinator<T, R> parent, int index) { + this.parent = parent; + this.index = index; + } + + @Override + public void onSubscribe(Disposable d) { + DisposableHelper.setOnce(this, d); + } + + @Override + public void onNext(T t) { + parent.innerNext(index, t); + } + + @Override + public void onError(Throwable t) { + parent.innerError(index, t); + } + + @Override + public void onComplete() { + parent.innerComplete(index); + } + + public void dispose() { + DisposableHelper.dispose(this); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableConcatMap.java b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableConcatMap.java new file mode 100644 index 0000000000..b8f34df3d7 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableConcatMap.java @@ -0,0 +1,527 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import java.util.Objects; +import java.util.concurrent.atomic.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; +import io.reactivex.rxjava3.internal.util.*; +import io.reactivex.rxjava3.observers.SerializedObserver; +import io.reactivex.rxjava3.operators.QueueDisposable; +import io.reactivex.rxjava3.operators.SimpleQueue; +import io.reactivex.rxjava3.operators.SpscLinkedArrayQueue; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +public final class ObservableConcatMap<T, U> extends AbstractObservableWithUpstream<T, U> { + final Function<? super T, ? extends ObservableSource<? extends U>> mapper; + final int bufferSize; + + final ErrorMode delayErrors; + + public ObservableConcatMap(ObservableSource<T> source, Function<? super T, ? extends ObservableSource<? extends U>> mapper, + int bufferSize, ErrorMode delayErrors) { + super(source); + this.mapper = mapper; + this.delayErrors = delayErrors; + this.bufferSize = Math.max(8, bufferSize); + } + + @Override + public void subscribeActual(Observer<? super U> observer) { + + if (ObservableScalarXMap.tryScalarXMapSubscribe(source, observer, mapper)) { + return; + } + + if (delayErrors == ErrorMode.IMMEDIATE) { + SerializedObserver<U> serial = new SerializedObserver<>(observer); + source.subscribe(new SourceObserver<>(serial, mapper, bufferSize)); + } else { + source.subscribe(new ConcatMapDelayErrorObserver<>(observer, mapper, bufferSize, delayErrors == ErrorMode.END)); + } + } + + static final class SourceObserver<T, U> extends AtomicInteger implements Observer<T>, Disposable { + + private static final long serialVersionUID = 8828587559905699186L; + final Observer<? super U> downstream; + final Function<? super T, ? extends ObservableSource<? extends U>> mapper; + final InnerObserver<U> inner; + final int bufferSize; + + SimpleQueue<T> queue; + + Disposable upstream; + + volatile boolean active; + + volatile boolean disposed; + + volatile boolean done; + + int fusionMode; + + SourceObserver(Observer<? super U> actual, + Function<? super T, ? extends ObservableSource<? extends U>> mapper, int bufferSize) { + this.downstream = actual; + this.mapper = mapper; + this.bufferSize = bufferSize; + this.inner = new InnerObserver<>(actual, this); + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.validate(this.upstream, d)) { + this.upstream = d; + if (d instanceof QueueDisposable) { + @SuppressWarnings("unchecked") + QueueDisposable<T> qd = (QueueDisposable<T>) d; + + int m = qd.requestFusion(QueueDisposable.ANY); + if (m == QueueDisposable.SYNC) { + fusionMode = m; + queue = qd; + done = true; + + downstream.onSubscribe(this); + + drain(); + return; + } + + if (m == QueueDisposable.ASYNC) { + fusionMode = m; + queue = qd; + + downstream.onSubscribe(this); + + return; + } + } + + queue = new SpscLinkedArrayQueue<>(bufferSize); + + downstream.onSubscribe(this); + } + } + + @Override + public void onNext(T t) { + if (done) { + return; + } + if (fusionMode == QueueDisposable.NONE) { + queue.offer(t); + } + drain(); + } + + @Override + public void onError(Throwable t) { + if (done) { + RxJavaPlugins.onError(t); + return; + } + done = true; + dispose(); + downstream.onError(t); + } + + @Override + public void onComplete() { + if (done) { + return; + } + done = true; + drain(); + } + + void innerComplete() { + active = false; + drain(); + } + + @Override + public boolean isDisposed() { + return disposed; + } + + @Override + public void dispose() { + disposed = true; + inner.dispose(); + upstream.dispose(); + if (getAndIncrement() == 0) { + queue.clear(); + } + } + + void drain() { + if (getAndIncrement() != 0) { + return; + } + + for (;;) { + if (disposed) { + queue.clear(); + return; + } + if (!active) { + + boolean d = done; + + T t; + + try { + t = queue.poll(); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + dispose(); + queue.clear(); + downstream.onError(ex); + return; + } + + boolean empty = t == null; + + if (d && empty) { + disposed = true; + downstream.onComplete(); + return; + } + + if (!empty) { + ObservableSource<? extends U> o; + + try { + o = Objects.requireNonNull(mapper.apply(t), "The mapper returned a null ObservableSource"); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + dispose(); + queue.clear(); + downstream.onError(ex); + return; + } + + active = true; + o.subscribe(inner); + } + } + + if (decrementAndGet() == 0) { + break; + } + } + } + + static final class InnerObserver<U> extends AtomicReference<Disposable> implements Observer<U> { + + private static final long serialVersionUID = -7449079488798789337L; + + final Observer<? super U> downstream; + final SourceObserver<?, ?> parent; + + InnerObserver(Observer<? super U> actual, SourceObserver<?, ?> parent) { + this.downstream = actual; + this.parent = parent; + } + + @Override + public void onSubscribe(Disposable d) { + DisposableHelper.replace(this, d); + } + + @Override + public void onNext(U t) { + downstream.onNext(t); + } + + @Override + public void onError(Throwable t) { + parent.dispose(); + downstream.onError(t); + } + + @Override + public void onComplete() { + parent.innerComplete(); + } + + void dispose() { + DisposableHelper.dispose(this); + } + } + } + + static final class ConcatMapDelayErrorObserver<T, R> + extends AtomicInteger + implements Observer<T>, Disposable { + + private static final long serialVersionUID = -6951100001833242599L; + + final Observer<? super R> downstream; + + final Function<? super T, ? extends ObservableSource<? extends R>> mapper; + + final int bufferSize; + + final AtomicThrowable errors; + + final DelayErrorInnerObserver<R> observer; + + final boolean tillTheEnd; + + SimpleQueue<T> queue; + + Disposable upstream; + + volatile boolean active; + + volatile boolean done; + + volatile boolean cancelled; + + int sourceMode; + + ConcatMapDelayErrorObserver(Observer<? super R> actual, + Function<? super T, ? extends ObservableSource<? extends R>> mapper, int bufferSize, + boolean tillTheEnd) { + this.downstream = actual; + this.mapper = mapper; + this.bufferSize = bufferSize; + this.tillTheEnd = tillTheEnd; + this.errors = new AtomicThrowable(); + this.observer = new DelayErrorInnerObserver<>(actual, this); + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.validate(this.upstream, d)) { + this.upstream = d; + + if (d instanceof QueueDisposable) { + @SuppressWarnings("unchecked") + QueueDisposable<T> qd = (QueueDisposable<T>) d; + + int m = qd.requestFusion(QueueDisposable.ANY); + if (m == QueueDisposable.SYNC) { + sourceMode = m; + queue = qd; + done = true; + + downstream.onSubscribe(this); + + drain(); + return; + } + if (m == QueueDisposable.ASYNC) { + sourceMode = m; + queue = qd; + + downstream.onSubscribe(this); + + return; + } + } + + queue = new SpscLinkedArrayQueue<>(bufferSize); + + downstream.onSubscribe(this); + } + } + + @Override + public void onNext(T value) { + if (sourceMode == QueueDisposable.NONE) { + queue.offer(value); + } + drain(); + } + + @Override + public void onError(Throwable e) { + if (errors.tryAddThrowableOrReport(e)) { + done = true; + drain(); + } + } + + @Override + public void onComplete() { + done = true; + drain(); + } + + @Override + public boolean isDisposed() { + return cancelled; + } + + @Override + public void dispose() { + cancelled = true; + upstream.dispose(); + observer.dispose(); + errors.tryTerminateAndReport(); + } + + @SuppressWarnings("unchecked") + void drain() { + if (getAndIncrement() != 0) { + return; + } + + Observer<? super R> downstream = this.downstream; + SimpleQueue<T> queue = this.queue; + AtomicThrowable errors = this.errors; + + for (;;) { + + if (!active) { + + if (cancelled) { + queue.clear(); + return; + } + + if (!tillTheEnd) { + Throwable ex = errors.get(); + if (ex != null) { + queue.clear(); + cancelled = true; + errors.tryTerminateConsumer(downstream); + return; + } + } + + boolean d = done; + + T v; + + try { + v = queue.poll(); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + cancelled = true; + this.upstream.dispose(); + errors.tryAddThrowableOrReport(ex); + errors.tryTerminateConsumer(downstream); + return; + } + + boolean empty = v == null; + + if (d && empty) { + cancelled = true; + errors.tryTerminateConsumer(downstream); + return; + } + + if (!empty) { + + ObservableSource<? extends R> o; + + try { + o = Objects.requireNonNull(mapper.apply(v), "The mapper returned a null ObservableSource"); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + cancelled = true; + this.upstream.dispose(); + queue.clear(); + errors.tryAddThrowableOrReport(ex); + errors.tryTerminateConsumer(downstream); + return; + } + + if (o instanceof Supplier) { + R w; + + try { + w = ((Supplier<R>)o).get(); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + errors.tryAddThrowableOrReport(ex); + continue; + } + + if (w != null && !cancelled) { + downstream.onNext(w); + } + continue; + } else { + active = true; + o.subscribe(observer); + } + } + } + + if (decrementAndGet() == 0) { + break; + } + } + } + + static final class DelayErrorInnerObserver<R> extends AtomicReference<Disposable> implements Observer<R> { + + private static final long serialVersionUID = 2620149119579502636L; + + final Observer<? super R> downstream; + + final ConcatMapDelayErrorObserver<?, R> parent; + + DelayErrorInnerObserver(Observer<? super R> actual, ConcatMapDelayErrorObserver<?, R> parent) { + this.downstream = actual; + this.parent = parent; + } + + @Override + public void onSubscribe(Disposable d) { + DisposableHelper.replace(this, d); + } + + @Override + public void onNext(R value) { + downstream.onNext(value); + } + + @Override + public void onError(Throwable e) { + ConcatMapDelayErrorObserver<?, R> p = parent; + if (p.errors.tryAddThrowableOrReport(e)) { + if (!p.tillTheEnd) { + p.upstream.dispose(); + } + p.active = false; + p.drain(); + } + } + + @Override + public void onComplete() { + ConcatMapDelayErrorObserver<?, R> p = parent; + p.active = false; + p.drain(); + } + + void dispose() { + DisposableHelper.dispose(this); + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableConcatMapEager.java b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableConcatMapEager.java new file mode 100644 index 0000000000..5a4b1c3a47 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableConcatMapEager.java @@ -0,0 +1,412 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import java.util.ArrayDeque; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicInteger; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; +import io.reactivex.rxjava3.internal.observers.*; +import io.reactivex.rxjava3.internal.util.*; +import io.reactivex.rxjava3.operators.QueueDisposable; +import io.reactivex.rxjava3.operators.SimpleQueue; +import io.reactivex.rxjava3.operators.SpscLinkedArrayQueue; + +public final class ObservableConcatMapEager<T, R> extends AbstractObservableWithUpstream<T, R> { + + final Function<? super T, ? extends ObservableSource<? extends R>> mapper; + + final ErrorMode errorMode; + + final int maxConcurrency; + + final int prefetch; + + public ObservableConcatMapEager(ObservableSource<T> source, + Function<? super T, ? extends ObservableSource<? extends R>> mapper, + ErrorMode errorMode, + int maxConcurrency, int prefetch) { + super(source); + this.mapper = mapper; + this.errorMode = errorMode; + this.maxConcurrency = maxConcurrency; + this.prefetch = prefetch; + } + + @Override + protected void subscribeActual(Observer<? super R> observer) { + source.subscribe(new ConcatMapEagerMainObserver<>(observer, mapper, maxConcurrency, prefetch, errorMode)); + } + + static final class ConcatMapEagerMainObserver<T, R> + extends AtomicInteger + implements Observer<T>, Disposable, InnerQueuedObserverSupport<R> { + + private static final long serialVersionUID = 8080567949447303262L; + + final Observer<? super R> downstream; + + final Function<? super T, ? extends ObservableSource<? extends R>> mapper; + + final int maxConcurrency; + + final int prefetch; + + final ErrorMode errorMode; + + final AtomicThrowable errors; + + final ArrayDeque<InnerQueuedObserver<R>> observers; + + SimpleQueue<T> queue; + + Disposable upstream; + + volatile boolean done; + + int sourceMode; + + volatile boolean cancelled; + + InnerQueuedObserver<R> current; + + int activeCount; + + ConcatMapEagerMainObserver(Observer<? super R> actual, + Function<? super T, ? extends ObservableSource<? extends R>> mapper, + int maxConcurrency, int prefetch, ErrorMode errorMode) { + this.downstream = actual; + this.mapper = mapper; + this.maxConcurrency = maxConcurrency; + this.prefetch = prefetch; + this.errorMode = errorMode; + this.errors = new AtomicThrowable(); + this.observers = new ArrayDeque<>(); + } + + @SuppressWarnings("unchecked") + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.validate(this.upstream, d)) { + this.upstream = d; + + if (d instanceof QueueDisposable) { + QueueDisposable<T> qd = (QueueDisposable<T>) d; + + int m = qd.requestFusion(QueueDisposable.ANY); + if (m == QueueDisposable.SYNC) { + sourceMode = m; + queue = qd; + done = true; + + downstream.onSubscribe(this); + + drain(); + return; + } + if (m == QueueDisposable.ASYNC) { + sourceMode = m; + queue = qd; + + downstream.onSubscribe(this); + + return; + } + } + + queue = new SpscLinkedArrayQueue<>(prefetch); + + downstream.onSubscribe(this); + } + } + + @Override + public void onNext(T value) { + if (sourceMode == QueueDisposable.NONE) { + queue.offer(value); + } + drain(); + } + + @Override + public void onError(Throwable e) { + if (errors.tryAddThrowableOrReport(e)) { + done = true; + drain(); + } + } + + @Override + public void onComplete() { + done = true; + drain(); + } + + @Override + public void dispose() { + if (cancelled) { + return; + } + cancelled = true; + upstream.dispose(); + errors.tryTerminateAndReport(); + + drainAndDispose(); + } + + void drainAndDispose() { + if (getAndIncrement() == 0) { + do { + queue.clear(); + disposeAll(); + } while (decrementAndGet() != 0); + } + } + + @Override + public boolean isDisposed() { + return cancelled; + } + + void disposeAll() { + InnerQueuedObserver<R> inner = current; + + if (inner != null) { + inner.dispose(); + } + + for (;;) { + + inner = observers.poll(); + + if (inner == null) { + return; + } + + inner.dispose(); + } + } + + @Override + public void innerNext(InnerQueuedObserver<R> inner, R value) { + inner.queue().offer(value); + drain(); + } + + @Override + public void innerError(InnerQueuedObserver<R> inner, Throwable e) { + if (errors.tryAddThrowableOrReport(e)) { + if (errorMode == ErrorMode.IMMEDIATE) { + upstream.dispose(); + } + inner.setDone(); + drain(); + } + } + + @Override + public void innerComplete(InnerQueuedObserver<R> inner) { + inner.setDone(); + drain(); + } + + @Override + public void drain() { + if (getAndIncrement() != 0) { + return; + } + + int missed = 1; + + SimpleQueue<T> q = queue; + ArrayDeque<InnerQueuedObserver<R>> observers = this.observers; + Observer<? super R> a = this.downstream; + ErrorMode errorMode = this.errorMode; + + outer: + for (;;) { + + int ac = activeCount; + + while (ac != maxConcurrency) { + if (cancelled) { + q.clear(); + disposeAll(); + return; + } + + if (errorMode == ErrorMode.IMMEDIATE) { + Throwable ex = errors.get(); + if (ex != null) { + q.clear(); + disposeAll(); + + errors.tryTerminateConsumer(downstream); + return; + } + } + + T v; + ObservableSource<? extends R> source; + + try { + v = q.poll(); + + if (v == null) { + break; + } + + source = Objects.requireNonNull(mapper.apply(v), "The mapper returned a null ObservableSource"); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + upstream.dispose(); + q.clear(); + disposeAll(); + errors.tryAddThrowableOrReport(ex); + errors.tryTerminateConsumer(downstream); + return; + } + + InnerQueuedObserver<R> inner = new InnerQueuedObserver<>(this, prefetch); + + observers.offer(inner); + + source.subscribe(inner); + + ac++; + } + + activeCount = ac; + + if (cancelled) { + q.clear(); + disposeAll(); + return; + } + + if (errorMode == ErrorMode.IMMEDIATE) { + Throwable ex = errors.get(); + if (ex != null) { + q.clear(); + disposeAll(); + + errors.tryTerminateConsumer(downstream); + return; + } + } + + InnerQueuedObserver<R> active = current; + + if (active == null) { + if (errorMode == ErrorMode.BOUNDARY) { + Throwable ex = errors.get(); + if (ex != null) { + q.clear(); + disposeAll(); + + errors.tryTerminateConsumer(a); + return; + } + } + boolean d = done; + + active = observers.poll(); + + boolean empty = active == null; + + if (d && empty) { + Throwable ex = errors.get(); + if (ex != null) { + q.clear(); + disposeAll(); + + errors.tryTerminateConsumer(a); + } else { + a.onComplete(); + } + return; + } + + if (!empty) { + current = active; + } + + } + + if (active != null) { + SimpleQueue<R> aq = active.queue(); + + for (;;) { + if (cancelled) { + q.clear(); + disposeAll(); + return; + } + + boolean d = active.isDone(); + + if (errorMode == ErrorMode.IMMEDIATE) { + Throwable ex = errors.get(); + if (ex != null) { + q.clear(); + disposeAll(); + + errors.tryTerminateConsumer(a); + return; + } + } + + R w; + + try { + w = aq.poll(); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + errors.tryAddThrowableOrReport(ex); + + current = null; + activeCount--; + continue outer; + } + + boolean empty = w == null; + + if (d && empty) { + current = null; + activeCount--; + continue outer; + } + + if (empty) { + break; + } + + a.onNext(w); + } + } + + missed = addAndGet(-missed); + if (missed == 0) { + break; + } + } + } + } + +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableConcatMapScheduler.java b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableConcatMapScheduler.java new file mode 100644 index 0000000000..1ea59c83c3 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableConcatMapScheduler.java @@ -0,0 +1,551 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import java.util.Objects; +import java.util.concurrent.atomic.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; +import io.reactivex.rxjava3.internal.util.*; +import io.reactivex.rxjava3.observers.SerializedObserver; +import io.reactivex.rxjava3.operators.QueueDisposable; +import io.reactivex.rxjava3.operators.SimpleQueue; +import io.reactivex.rxjava3.operators.SpscLinkedArrayQueue; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +public final class ObservableConcatMapScheduler<T, U> extends AbstractObservableWithUpstream<T, U> { + + final Function<? super T, ? extends ObservableSource<? extends U>> mapper; + + final int bufferSize; + + final ErrorMode delayErrors; + + final Scheduler scheduler; + + public ObservableConcatMapScheduler(ObservableSource<T> source, Function<? super T, ? extends ObservableSource<? extends U>> mapper, + int bufferSize, ErrorMode delayErrors, Scheduler scheduler) { + super(source); + this.mapper = mapper; + this.delayErrors = delayErrors; + this.bufferSize = Math.max(8, bufferSize); + this.scheduler = scheduler; + } + + @Override + public void subscribeActual(Observer<? super U> observer) { + if (delayErrors == ErrorMode.IMMEDIATE) { + SerializedObserver<U> serial = new SerializedObserver<>(observer); + source.subscribe(new ConcatMapObserver<>(serial, mapper, bufferSize, scheduler.createWorker())); + } else { + source.subscribe(new ConcatMapDelayErrorObserver<>(observer, mapper, bufferSize, delayErrors == ErrorMode.END, scheduler.createWorker())); + } + } + + static final class ConcatMapObserver<T, U> extends AtomicInteger implements Observer<T>, Disposable, Runnable { + + private static final long serialVersionUID = 8828587559905699186L; + final Observer<? super U> downstream; + final Function<? super T, ? extends ObservableSource<? extends U>> mapper; + final InnerObserver<U> inner; + final int bufferSize; + final Scheduler.Worker worker; + + SimpleQueue<T> queue; + + Disposable upstream; + + volatile boolean active; + + volatile boolean disposed; + + volatile boolean done; + + int fusionMode; + + ConcatMapObserver(Observer<? super U> actual, + Function<? super T, ? extends ObservableSource<? extends U>> mapper, int bufferSize, Scheduler.Worker worker) { + this.downstream = actual; + this.mapper = mapper; + this.bufferSize = bufferSize; + this.inner = new InnerObserver<>(actual, this); + this.worker = worker; + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.validate(this.upstream, d)) { + this.upstream = d; + if (d instanceof QueueDisposable) { + @SuppressWarnings("unchecked") + QueueDisposable<T> qd = (QueueDisposable<T>) d; + + int m = qd.requestFusion(QueueDisposable.ANY); + if (m == QueueDisposable.SYNC) { + fusionMode = m; + queue = qd; + done = true; + + downstream.onSubscribe(this); + + drain(); + return; + } + + if (m == QueueDisposable.ASYNC) { + fusionMode = m; + queue = qd; + + downstream.onSubscribe(this); + + return; + } + } + + queue = new SpscLinkedArrayQueue<>(bufferSize); + + downstream.onSubscribe(this); + } + } + + @Override + public void onNext(T t) { + if (done) { + return; + } + if (fusionMode == QueueDisposable.NONE) { + queue.offer(t); + } + drain(); + } + + @Override + public void onError(Throwable t) { + if (done) { + RxJavaPlugins.onError(t); + return; + } + done = true; + dispose(); + downstream.onError(t); + } + + @Override + public void onComplete() { + if (done) { + return; + } + done = true; + drain(); + } + + void innerComplete() { + active = false; + drain(); + } + + @Override + public boolean isDisposed() { + return disposed; + } + + @Override + public void dispose() { + disposed = true; + inner.dispose(); + upstream.dispose(); + worker.dispose(); + + if (getAndIncrement() == 0) { + queue.clear(); + } + } + + void drain() { + if (getAndIncrement() != 0) { + return; + } + worker.schedule(this); + } + + @Override + public void run() { + for (;;) { + if (disposed) { + queue.clear(); + return; + } + if (!active) { + + boolean d = done; + + T t; + + try { + t = queue.poll(); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + dispose(); + queue.clear(); + downstream.onError(ex); + worker.dispose(); + return; + } + + boolean empty = t == null; + + if (d && empty) { + disposed = true; + downstream.onComplete(); + worker.dispose(); + return; + } + + if (!empty) { + ObservableSource<? extends U> o; + + try { + o = Objects.requireNonNull(mapper.apply(t), "The mapper returned a null ObservableSource"); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + dispose(); + queue.clear(); + downstream.onError(ex); + worker.dispose(); + return; + } + + active = true; + o.subscribe(inner); + } + } + + if (decrementAndGet() == 0) { + break; + } + } + } + + static final class InnerObserver<U> extends AtomicReference<Disposable> implements Observer<U> { + + private static final long serialVersionUID = -7449079488798789337L; + + final Observer<? super U> downstream; + final ConcatMapObserver<?, ?> parent; + + InnerObserver(Observer<? super U> actual, ConcatMapObserver<?, ?> parent) { + this.downstream = actual; + this.parent = parent; + } + + @Override + public void onSubscribe(Disposable d) { + DisposableHelper.replace(this, d); + } + + @Override + public void onNext(U t) { + downstream.onNext(t); + } + + @Override + public void onError(Throwable t) { + parent.dispose(); + downstream.onError(t); + } + + @Override + public void onComplete() { + parent.innerComplete(); + } + + void dispose() { + DisposableHelper.dispose(this); + } + } + } + + static final class ConcatMapDelayErrorObserver<T, R> + extends AtomicInteger + implements Observer<T>, Disposable, Runnable { + + private static final long serialVersionUID = -6951100001833242599L; + + final Observer<? super R> downstream; + + final Function<? super T, ? extends ObservableSource<? extends R>> mapper; + + final int bufferSize; + + final AtomicThrowable errors; + + final DelayErrorInnerObserver<R> observer; + + final boolean tillTheEnd; + + final Scheduler.Worker worker; + + SimpleQueue<T> queue; + + Disposable upstream; + + volatile boolean active; + + volatile boolean done; + + volatile boolean cancelled; + + int sourceMode; + + ConcatMapDelayErrorObserver(Observer<? super R> actual, + Function<? super T, ? extends ObservableSource<? extends R>> mapper, int bufferSize, + boolean tillTheEnd, Scheduler.Worker worker) { + this.downstream = actual; + this.mapper = mapper; + this.bufferSize = bufferSize; + this.tillTheEnd = tillTheEnd; + this.errors = new AtomicThrowable(); + this.observer = new DelayErrorInnerObserver<>(actual, this); + this.worker = worker; + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.validate(this.upstream, d)) { + this.upstream = d; + + if (d instanceof QueueDisposable) { + @SuppressWarnings("unchecked") + QueueDisposable<T> qd = (QueueDisposable<T>) d; + + int m = qd.requestFusion(QueueDisposable.ANY); + if (m == QueueDisposable.SYNC) { + sourceMode = m; + queue = qd; + done = true; + + downstream.onSubscribe(this); + + drain(); + return; + } + if (m == QueueDisposable.ASYNC) { + sourceMode = m; + queue = qd; + + downstream.onSubscribe(this); + + return; + } + } + + queue = new SpscLinkedArrayQueue<>(bufferSize); + + downstream.onSubscribe(this); + } + } + + @Override + public void onNext(T value) { + if (sourceMode == QueueDisposable.NONE) { + queue.offer(value); + } + drain(); + } + + @Override + public void onError(Throwable e) { + if (errors.tryAddThrowableOrReport(e)) { + done = true; + drain(); + } + } + + @Override + public void onComplete() { + done = true; + drain(); + } + + @Override + public boolean isDisposed() { + return cancelled; + } + + @Override + public void dispose() { + cancelled = true; + upstream.dispose(); + observer.dispose(); + worker.dispose(); + errors.tryTerminateAndReport(); + } + + void drain() { + if (getAndIncrement() != 0) { + return; + } + + worker.schedule(this); + } + + @SuppressWarnings("unchecked") + @Override + public void run() { + Observer<? super R> downstream = this.downstream; + SimpleQueue<T> queue = this.queue; + AtomicThrowable errors = this.errors; + + for (;;) { + + if (!active) { + + if (cancelled) { + queue.clear(); + return; + } + + if (!tillTheEnd) { + Throwable ex = errors.get(); + if (ex != null) { + queue.clear(); + cancelled = true; + errors.tryTerminateConsumer(downstream); + worker.dispose(); + return; + } + } + + boolean d = done; + + T v; + + try { + v = queue.poll(); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + cancelled = true; + this.upstream.dispose(); + errors.tryAddThrowableOrReport(ex); + errors.tryTerminateConsumer(downstream); + worker.dispose(); + return; + } + + boolean empty = v == null; + + if (d && empty) { + cancelled = true; + errors.tryTerminateConsumer(downstream); + worker.dispose(); + return; + } + + if (!empty) { + + ObservableSource<? extends R> o; + + try { + o = Objects.requireNonNull(mapper.apply(v), "The mapper returned a null ObservableSource"); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + cancelled = true; + this.upstream.dispose(); + queue.clear(); + errors.tryAddThrowableOrReport(ex); + errors.tryTerminateConsumer(downstream); + worker.dispose(); + return; + } + + if (o instanceof Supplier) { + R w; + + try { + w = ((Supplier<R>)o).get(); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + errors.tryAddThrowableOrReport(ex); + continue; + } + + if (w != null && !cancelled) { + downstream.onNext(w); + } + continue; + } else { + active = true; + o.subscribe(observer); + } + } + } + + if (decrementAndGet() == 0) { + break; + } + } + } + + static final class DelayErrorInnerObserver<R> extends AtomicReference<Disposable> implements Observer<R> { + + private static final long serialVersionUID = 2620149119579502636L; + + final Observer<? super R> downstream; + + final ConcatMapDelayErrorObserver<?, R> parent; + + DelayErrorInnerObserver(Observer<? super R> actual, ConcatMapDelayErrorObserver<?, R> parent) { + this.downstream = actual; + this.parent = parent; + } + + @Override + public void onSubscribe(Disposable d) { + DisposableHelper.replace(this, d); + } + + @Override + public void onNext(R value) { + downstream.onNext(value); + } + + @Override + public void onError(Throwable e) { + ConcatMapDelayErrorObserver<?, R> p = parent; + if (p.errors.tryAddThrowableOrReport(e)) { + if (!p.tillTheEnd) { + p.upstream.dispose(); + } + p.active = false; + p.drain(); + } + } + + @Override + public void onComplete() { + ConcatMapDelayErrorObserver<?, R> p = parent; + p.active = false; + p.drain(); + } + + void dispose() { + DisposableHelper.dispose(this); + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableConcatWithCompletable.java b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableConcatWithCompletable.java new file mode 100644 index 0000000000..8d97074a8c --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableConcatWithCompletable.java @@ -0,0 +1,100 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import java.util.concurrent.atomic.AtomicReference; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; + +/** + * Subscribe to a main Observable first, then when it completes normally, subscribe to a Single, + * signal its success value followed by a completion or signal its error as is. + * <p>History: 2.1.10 - experimental + * @param <T> the element type of the main source and output type + * @since 2.2 + */ +public final class ObservableConcatWithCompletable<T> extends AbstractObservableWithUpstream<T, T> { + + final CompletableSource other; + + public ObservableConcatWithCompletable(Observable<T> source, CompletableSource other) { + super(source); + this.other = other; + } + + @Override + protected void subscribeActual(Observer<? super T> observer) { + source.subscribe(new ConcatWithObserver<>(observer, other)); + } + + static final class ConcatWithObserver<T> + extends AtomicReference<Disposable> + implements Observer<T>, CompletableObserver, Disposable { + + private static final long serialVersionUID = -1953724749712440952L; + + final Observer<? super T> downstream; + + CompletableSource other; + + boolean inCompletable; + + ConcatWithObserver(Observer<? super T> actual, CompletableSource other) { + this.downstream = actual; + this.other = other; + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.setOnce(this, d) && !inCompletable) { + downstream.onSubscribe(this); + } + } + + @Override + public void onNext(T t) { + downstream.onNext(t); + } + + @Override + public void onError(Throwable e) { + downstream.onError(e); + } + + @Override + public void onComplete() { + if (inCompletable) { + downstream.onComplete(); + } else { + inCompletable = true; + DisposableHelper.replace(this, null); + CompletableSource cs = other; + other = null; + cs.subscribe(this); + } + } + + @Override + public void dispose() { + DisposableHelper.dispose(this); + } + + @Override + public boolean isDisposed() { + return DisposableHelper.isDisposed(get()); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableConcatWithMaybe.java b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableConcatWithMaybe.java new file mode 100644 index 0000000000..e198a0412a --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableConcatWithMaybe.java @@ -0,0 +1,106 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import java.util.concurrent.atomic.AtomicReference; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; + +/** + * Subscribe to a main Observable first, then when it completes normally, subscribe to a Maybe, + * signal its success value followed by a completion or signal its error or completion signal as is. + * <p>History: 2.1.10 - experimental + * @param <T> the element type of the main source and output type + * @since 2.2 + */ +public final class ObservableConcatWithMaybe<T> extends AbstractObservableWithUpstream<T, T> { + + final MaybeSource<? extends T> other; + + public ObservableConcatWithMaybe(Observable<T> source, MaybeSource<? extends T> other) { + super(source); + this.other = other; + } + + @Override + protected void subscribeActual(Observer<? super T> observer) { + source.subscribe(new ConcatWithObserver<>(observer, other)); + } + + static final class ConcatWithObserver<T> + extends AtomicReference<Disposable> + implements Observer<T>, MaybeObserver<T>, Disposable { + + private static final long serialVersionUID = -1953724749712440952L; + + final Observer<? super T> downstream; + + MaybeSource<? extends T> other; + + boolean inMaybe; + + ConcatWithObserver(Observer<? super T> actual, MaybeSource<? extends T> other) { + this.downstream = actual; + this.other = other; + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.setOnce(this, d) && !inMaybe) { + downstream.onSubscribe(this); + } + } + + @Override + public void onNext(T t) { + downstream.onNext(t); + } + + @Override + public void onSuccess(T t) { + downstream.onNext(t); + downstream.onComplete(); + } + + @Override + public void onError(Throwable e) { + downstream.onError(e); + } + + @Override + public void onComplete() { + if (inMaybe) { + downstream.onComplete(); + } else { + inMaybe = true; + DisposableHelper.replace(this, null); + MaybeSource<? extends T> ms = other; + other = null; + ms.subscribe(this); + } + } + + @Override + public void dispose() { + DisposableHelper.dispose(this); + } + + @Override + public boolean isDisposed() { + return DisposableHelper.isDisposed(get()); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableConcatWithSingle.java b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableConcatWithSingle.java new file mode 100644 index 0000000000..3d69ae83ac --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableConcatWithSingle.java @@ -0,0 +1,102 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import java.util.concurrent.atomic.AtomicReference; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; + +/** + * Subscribe to a main Observable first, then when it completes normally, subscribe to a Single, + * signal its success value followed by a completion or signal its error as is. + * <p>History: 2.1.10 - experimental + * @param <T> the element type of the main source and output type + * @since 2.2 + */ +public final class ObservableConcatWithSingle<T> extends AbstractObservableWithUpstream<T, T> { + + final SingleSource<? extends T> other; + + public ObservableConcatWithSingle(Observable<T> source, SingleSource<? extends T> other) { + super(source); + this.other = other; + } + + @Override + protected void subscribeActual(Observer<? super T> observer) { + source.subscribe(new ConcatWithObserver<>(observer, other)); + } + + static final class ConcatWithObserver<T> + extends AtomicReference<Disposable> + implements Observer<T>, SingleObserver<T>, Disposable { + + private static final long serialVersionUID = -1953724749712440952L; + + final Observer<? super T> downstream; + + SingleSource<? extends T> other; + + boolean inSingle; + + ConcatWithObserver(Observer<? super T> actual, SingleSource<? extends T> other) { + this.downstream = actual; + this.other = other; + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.setOnce(this, d) && !inSingle) { + downstream.onSubscribe(this); + } + } + + @Override + public void onNext(T t) { + downstream.onNext(t); + } + + @Override + public void onSuccess(T t) { + downstream.onNext(t); + downstream.onComplete(); + } + + @Override + public void onError(Throwable e) { + downstream.onError(e); + } + + @Override + public void onComplete() { + inSingle = true; + DisposableHelper.replace(this, null); + SingleSource<? extends T> ss = other; + other = null; + ss.subscribe(this); + } + + @Override + public void dispose() { + DisposableHelper.dispose(this); + } + + @Override + public boolean isDisposed() { + return DisposableHelper.isDisposed(get()); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableCount.java b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableCount.java new file mode 100644 index 0000000000..366c4bb3db --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableCount.java @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; + +public final class ObservableCount<T> extends AbstractObservableWithUpstream<T, Long> { + public ObservableCount(ObservableSource<T> source) { + super(source); + } + + @Override + public void subscribeActual(Observer<? super Long> t) { + source.subscribe(new CountObserver(t)); + } + + static final class CountObserver implements Observer<Object>, Disposable { + final Observer<? super Long> downstream; + + Disposable upstream; + + long count; + + CountObserver(Observer<? super Long> downstream) { + this.downstream = downstream; + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.validate(this.upstream, d)) { + this.upstream = d; + downstream.onSubscribe(this); + } + } + + @Override + public void dispose() { + upstream.dispose(); + } + + @Override + public boolean isDisposed() { + return upstream.isDisposed(); + } + + @Override + public void onNext(Object t) { + count++; + } + + @Override + public void onError(Throwable t) { + downstream.onError(t); + } + + @Override + public void onComplete() { + downstream.onNext(count); + downstream.onComplete(); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableCountSingle.java b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableCountSingle.java new file mode 100644 index 0000000000..be73420542 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableCountSingle.java @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; +import io.reactivex.rxjava3.internal.fuseable.FuseToObservable; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +public final class ObservableCountSingle<T> extends Single<Long> implements FuseToObservable<Long> { + final ObservableSource<T> source; + public ObservableCountSingle(ObservableSource<T> source) { + this.source = source; + } + + @Override + public void subscribeActual(SingleObserver<? super Long> t) { + source.subscribe(new CountObserver(t)); + } + + @Override + public Observable<Long> fuseToObservable() { + return RxJavaPlugins.onAssembly(new ObservableCount<>(source)); + } + + static final class CountObserver implements Observer<Object>, Disposable { + final SingleObserver<? super Long> downstream; + + Disposable upstream; + + long count; + + CountObserver(SingleObserver<? super Long> downstream) { + this.downstream = downstream; + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.validate(this.upstream, d)) { + this.upstream = d; + downstream.onSubscribe(this); + } + } + + @Override + public void dispose() { + upstream.dispose(); + upstream = DisposableHelper.DISPOSED; + } + + @Override + public boolean isDisposed() { + return upstream.isDisposed(); + } + + @Override + public void onNext(Object t) { + count++; + } + + @Override + public void onError(Throwable t) { + upstream = DisposableHelper.DISPOSED; + downstream.onError(t); + } + + @Override + public void onComplete() { + upstream = DisposableHelper.DISPOSED; + downstream.onSuccess(count); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableCreate.java b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableCreate.java new file mode 100644 index 0000000000..1e2e910f0f --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableCreate.java @@ -0,0 +1,294 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import java.util.concurrent.atomic.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.Cancellable; +import io.reactivex.rxjava3.internal.disposables.*; +import io.reactivex.rxjava3.internal.util.*; +import io.reactivex.rxjava3.operators.SimpleQueue; +import io.reactivex.rxjava3.operators.SpscLinkedArrayQueue; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +public final class ObservableCreate<T> extends Observable<T> { + final ObservableOnSubscribe<T> source; + + public ObservableCreate(ObservableOnSubscribe<T> source) { + this.source = source; + } + + @Override + protected void subscribeActual(Observer<? super T> observer) { + CreateEmitter<T> parent = new CreateEmitter<>(observer); + observer.onSubscribe(parent); + + try { + source.subscribe(parent); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + parent.onError(ex); + } + } + + static final class CreateEmitter<T> + extends AtomicReference<Disposable> + implements ObservableEmitter<T>, Disposable { + + private static final long serialVersionUID = -3434801548987643227L; + + final Observer<? super T> observer; + + CreateEmitter(Observer<? super T> observer) { + this.observer = observer; + } + + @Override + public void onNext(T t) { + if (t == null) { + onError(ExceptionHelper.createNullPointerException("onNext called with a null value.")); + return; + } + if (!isDisposed()) { + observer.onNext(t); + } + } + + @Override + public void onError(Throwable t) { + if (!tryOnError(t)) { + RxJavaPlugins.onError(t); + } + } + + @Override + public boolean tryOnError(Throwable t) { + if (t == null) { + t = ExceptionHelper.createNullPointerException("onError called with a null Throwable."); + } + if (!isDisposed()) { + try { + observer.onError(t); + } finally { + dispose(); + } + return true; + } + return false; + } + + @Override + public void onComplete() { + if (!isDisposed()) { + try { + observer.onComplete(); + } finally { + dispose(); + } + } + } + + @Override + public void setDisposable(Disposable d) { + DisposableHelper.set(this, d); + } + + @Override + public void setCancellable(Cancellable c) { + setDisposable(new CancellableDisposable(c)); + } + + @Override + public ObservableEmitter<T> serialize() { + return new SerializedEmitter<>(this); + } + + @Override + public void dispose() { + DisposableHelper.dispose(this); + } + + @Override + public boolean isDisposed() { + return DisposableHelper.isDisposed(get()); + } + + @Override + public String toString() { + return String.format("%s{%s}", getClass().getSimpleName(), super.toString()); + } + } + + /** + * Serializes calls to onNext, onError and onComplete. + * + * @param <T> the value type + */ + static final class SerializedEmitter<T> + extends AtomicInteger + implements ObservableEmitter<T> { + + private static final long serialVersionUID = 4883307006032401862L; + + final ObservableEmitter<T> emitter; + + final AtomicThrowable errors; + + final SpscLinkedArrayQueue<T> queue; + + volatile boolean done; + + SerializedEmitter(ObservableEmitter<T> emitter) { + this.emitter = emitter; + this.errors = new AtomicThrowable(); + this.queue = new SpscLinkedArrayQueue<>(16); + } + + @Override + public void onNext(T t) { + if (done || emitter.isDisposed()) { + return; + } + if (t == null) { + onError(ExceptionHelper.createNullPointerException("onNext called with a null value.")); + return; + } + if (get() == 0 && compareAndSet(0, 1)) { + emitter.onNext(t); + if (decrementAndGet() == 0) { + return; + } + } else { + SimpleQueue<T> q = queue; + synchronized (q) { + q.offer(t); + } + if (getAndIncrement() != 0) { + return; + } + } + drainLoop(); + } + + @Override + public void onError(Throwable t) { + if (!tryOnError(t)) { + RxJavaPlugins.onError(t); + } + } + + @Override + public boolean tryOnError(Throwable t) { + if (done || emitter.isDisposed()) { + return false; + } + if (t == null) { + t = ExceptionHelper.createNullPointerException("onError called with a null Throwable."); + } + if (errors.tryAddThrowable(t)) { + done = true; + drain(); + return true; + } + return false; + } + + @Override + public void onComplete() { + if (done || emitter.isDisposed()) { + return; + } + done = true; + drain(); + } + + void drain() { + if (getAndIncrement() == 0) { + drainLoop(); + } + } + + void drainLoop() { + ObservableEmitter<T> e = emitter; + SpscLinkedArrayQueue<T> q = queue; + AtomicThrowable errors = this.errors; + int missed = 1; + for (;;) { + + for (;;) { + if (e.isDisposed()) { + q.clear(); + return; + } + + if (errors.get() != null) { + q.clear(); + errors.tryTerminateConsumer(e); + return; + } + + boolean d = done; + T v = q.poll(); + + boolean empty = v == null; + + if (d && empty) { + e.onComplete(); + return; + } + + if (empty) { + break; + } + + e.onNext(v); + } + + missed = addAndGet(-missed); + if (missed == 0) { + break; + } + } + } + + @Override + public void setDisposable(Disposable d) { + emitter.setDisposable(d); + } + + @Override + public void setCancellable(Cancellable c) { + emitter.setCancellable(c); + } + + @Override + public boolean isDisposed() { + return emitter.isDisposed(); + } + + @Override + public ObservableEmitter<T> serialize() { + return this; + } + + @Override + public String toString() { + return emitter.toString(); + } + } + +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableDebounce.java b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableDebounce.java new file mode 100644 index 0000000000..862227e305 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableDebounce.java @@ -0,0 +1,191 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import java.util.Objects; +import java.util.concurrent.atomic.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; +import io.reactivex.rxjava3.observers.*; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +public final class ObservableDebounce<T, U> extends AbstractObservableWithUpstream<T, T> { + final Function<? super T, ? extends ObservableSource<U>> debounceSelector; + + public ObservableDebounce(ObservableSource<T> source, Function<? super T, ? extends ObservableSource<U>> debounceSelector) { + super(source); + this.debounceSelector = debounceSelector; + } + + @Override + public void subscribeActual(Observer<? super T> t) { + source.subscribe(new DebounceObserver<>(new SerializedObserver<>(t), debounceSelector)); + } + + static final class DebounceObserver<T, U> + implements Observer<T>, Disposable { + final Observer<? super T> downstream; + final Function<? super T, ? extends ObservableSource<U>> debounceSelector; + + Disposable upstream; + + final AtomicReference<Disposable> debouncer = new AtomicReference<>(); + + volatile long index; + + boolean done; + + DebounceObserver(Observer<? super T> actual, + Function<? super T, ? extends ObservableSource<U>> debounceSelector) { + this.downstream = actual; + this.debounceSelector = debounceSelector; + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.validate(this.upstream, d)) { + this.upstream = d; + downstream.onSubscribe(this); + } + } + + @Override + public void onNext(T t) { + if (done) { + return; + } + + long idx = index + 1; + index = idx; + + Disposable d = debouncer.get(); + if (d != null) { + d.dispose(); + } + + ObservableSource<U> p; + + try { + p = Objects.requireNonNull(debounceSelector.apply(t), "The ObservableSource supplied is null"); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + dispose(); + downstream.onError(e); + return; + } + + DebounceInnerObserver<T, U> dis = new DebounceInnerObserver<>(this, idx, t); + + if (debouncer.compareAndSet(d, dis)) { + p.subscribe(dis); + } + } + + @Override + public void onError(Throwable t) { + DisposableHelper.dispose(debouncer); + downstream.onError(t); + } + + @Override + public void onComplete() { + if (done) { + return; + } + done = true; + Disposable d = debouncer.get(); + if (d != DisposableHelper.DISPOSED) { + @SuppressWarnings("unchecked") + DebounceInnerObserver<T, U> dis = (DebounceInnerObserver<T, U>)d; + if (dis != null) { + dis.emit(); + } + DisposableHelper.dispose(debouncer); + downstream.onComplete(); + } + } + + @Override + public void dispose() { + upstream.dispose(); + DisposableHelper.dispose(debouncer); + } + + @Override + public boolean isDisposed() { + return upstream.isDisposed(); + } + + void emit(long idx, T value) { + if (idx == index) { + downstream.onNext(value); + } + } + + static final class DebounceInnerObserver<T, U> extends DisposableObserver<U> { + final DebounceObserver<T, U> parent; + final long index; + final T value; + + boolean done; + + final AtomicBoolean once = new AtomicBoolean(); + + DebounceInnerObserver(DebounceObserver<T, U> parent, long index, T value) { + this.parent = parent; + this.index = index; + this.value = value; + } + + @Override + public void onNext(U t) { + if (done) { + return; + } + done = true; + dispose(); + emit(); + } + + void emit() { + if (once.compareAndSet(false, true)) { + parent.emit(index, value); + } + } + + @Override + public void onError(Throwable t) { + if (done) { + RxJavaPlugins.onError(t); + return; + } + done = true; + parent.onError(t); + } + + @Override + public void onComplete() { + if (done) { + return; + } + done = true; + emit(); + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableDebounceTimed.java b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableDebounceTimed.java new file mode 100644 index 0000000000..f2db191229 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableDebounceTimed.java @@ -0,0 +1,199 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.core.Scheduler.Worker; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.Consumer; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; +import io.reactivex.rxjava3.observers.SerializedObserver; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +public final class ObservableDebounceTimed<T> extends AbstractObservableWithUpstream<T, T> { + final long timeout; + final TimeUnit unit; + final Scheduler scheduler; + final Consumer<? super T> onDropped; + + public ObservableDebounceTimed(ObservableSource<T> source, long timeout, TimeUnit unit, Scheduler scheduler, Consumer<? super T> onDropped) { + super(source); + this.timeout = timeout; + this.unit = unit; + this.scheduler = scheduler; + this.onDropped = onDropped; + } + + @Override + public void subscribeActual(Observer<? super T> t) { + source.subscribe(new DebounceTimedObserver<>( + new SerializedObserver<>(t), timeout, unit, scheduler.createWorker(), onDropped)); + } + + static final class DebounceTimedObserver<T> + implements Observer<T>, Disposable { + final Observer<? super T> downstream; + final long timeout; + final TimeUnit unit; + final Scheduler.Worker worker; + final Consumer<? super T> onDropped; + + Disposable upstream; + + DebounceEmitter<T> timer; + + volatile long index; + + boolean done; + + DebounceTimedObserver(Observer<? super T> actual, long timeout, TimeUnit unit, Worker worker, Consumer<? super T> onDropped) { + this.downstream = actual; + this.timeout = timeout; + this.unit = unit; + this.worker = worker; + this.onDropped = onDropped; + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.validate(this.upstream, d)) { + this.upstream = d; + downstream.onSubscribe(this); + } + } + + @Override + public void onNext(T t) { + if (done) { + return; + } + long idx = index + 1; + index = idx; + + DebounceEmitter<T> currentEmitter = timer; + if (currentEmitter != null) { + currentEmitter.dispose(); + } + + if (onDropped != null && currentEmitter != null) { + try { + onDropped.accept(timer.value); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + upstream.dispose(); + downstream.onError(ex); + done = true; + } + } + + DebounceEmitter<T> newEmitter = new DebounceEmitter<>(t, idx, this); + timer = newEmitter; + newEmitter.setResource(worker.schedule(newEmitter, timeout, unit)); + } + + @Override + public void onError(Throwable t) { + if (done) { + RxJavaPlugins.onError(t); + return; + } + Disposable d = timer; + if (d != null) { + d.dispose(); + } + done = true; + downstream.onError(t); + worker.dispose(); + } + + @Override + public void onComplete() { + if (done) { + return; + } + done = true; + + DebounceEmitter<T> d = timer; + if (d != null) { + d.dispose(); + } + + if (d != null) { + d.run(); + } + downstream.onComplete(); + worker.dispose(); + } + + @Override + public void dispose() { + upstream.dispose(); + worker.dispose(); + } + + @Override + public boolean isDisposed() { + return worker.isDisposed(); + } + + void emit(long idx, T t, DebounceEmitter<T> emitter) { + if (idx == index) { + downstream.onNext(t); + emitter.dispose(); + } + } + } + + static final class DebounceEmitter<T> extends AtomicReference<Disposable> implements Runnable, Disposable { + + private static final long serialVersionUID = 6812032969491025141L; + + final T value; + final long idx; + final DebounceTimedObserver<T> parent; + + final AtomicBoolean once = new AtomicBoolean(); + + DebounceEmitter(T value, long idx, DebounceTimedObserver<T> parent) { + this.value = value; + this.idx = idx; + this.parent = parent; + } + + @Override + public void run() { + if (once.compareAndSet(false, true)) { + parent.emit(idx, value, this); + } + } + + @Override + public void dispose() { + DisposableHelper.dispose(this); + } + + @Override + public boolean isDisposed() { + return get() == DisposableHelper.DISPOSED; + } + + public void setResource(Disposable d) { + DisposableHelper.replace(this, d); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableDefer.java b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableDefer.java new file mode 100644 index 0000000000..0b3388c84c --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableDefer.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.Supplier; +import io.reactivex.rxjava3.internal.disposables.EmptyDisposable; + +import java.util.Objects; + +public final class ObservableDefer<T> extends Observable<T> { + final Supplier<? extends ObservableSource<? extends T>> supplier; + public ObservableDefer(Supplier<? extends ObservableSource<? extends T>> supplier) { + this.supplier = supplier; + } + + @Override + public void subscribeActual(Observer<? super T> observer) { + ObservableSource<? extends T> pub; + try { + pub = Objects.requireNonNull(supplier.get(), "The supplier returned a null ObservableSource"); + } catch (Throwable t) { + Exceptions.throwIfFatal(t); + EmptyDisposable.error(t, observer); + return; + } + + pub.subscribe(observer); + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableDelay.java b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableDelay.java new file mode 100644 index 0000000000..1801cce1f2 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableDelay.java @@ -0,0 +1,146 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import java.util.concurrent.TimeUnit; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.core.Scheduler.Worker; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; +import io.reactivex.rxjava3.observers.SerializedObserver; + +public final class ObservableDelay<T> extends AbstractObservableWithUpstream<T, T> { + final long delay; + final TimeUnit unit; + final Scheduler scheduler; + final boolean delayError; + + public ObservableDelay(ObservableSource<T> source, long delay, TimeUnit unit, Scheduler scheduler, boolean delayError) { + super(source); + this.delay = delay; + this.unit = unit; + this.scheduler = scheduler; + this.delayError = delayError; + } + + @Override + @SuppressWarnings("unchecked") + public void subscribeActual(Observer<? super T> t) { + Observer<T> observer; + if (delayError) { + observer = (Observer<T>)t; + } else { + observer = new SerializedObserver<>(t); + } + + Scheduler.Worker w = scheduler.createWorker(); + + source.subscribe(new DelayObserver<>(observer, delay, unit, w, delayError)); + } + + static final class DelayObserver<T> implements Observer<T>, Disposable { + final Observer<? super T> downstream; + final long delay; + final TimeUnit unit; + final Scheduler.Worker w; + final boolean delayError; + + Disposable upstream; + + DelayObserver(Observer<? super T> actual, long delay, TimeUnit unit, Worker w, boolean delayError) { + super(); + this.downstream = actual; + this.delay = delay; + this.unit = unit; + this.w = w; + this.delayError = delayError; + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.validate(this.upstream, d)) { + this.upstream = d; + downstream.onSubscribe(this); + } + } + + @Override + public void onNext(final T t) { + w.schedule(new OnNext(t), delay, unit); + } + + @Override + public void onError(final Throwable t) { + w.schedule(new OnError(t), delayError ? delay : 0, unit); + } + + @Override + public void onComplete() { + w.schedule(new OnComplete(), delay, unit); + } + + @Override + public void dispose() { + upstream.dispose(); + w.dispose(); + } + + @Override + public boolean isDisposed() { + return w.isDisposed(); + } + + final class OnNext implements Runnable { + private final T t; + + OnNext(T t) { + this.t = t; + } + + @Override + public void run() { + downstream.onNext(t); + } + } + + final class OnError implements Runnable { + private final Throwable throwable; + + OnError(Throwable throwable) { + this.throwable = throwable; + } + + @Override + public void run() { + try { + downstream.onError(throwable); + } finally { + w.dispose(); + } + } + } + + final class OnComplete implements Runnable { + @Override + public void run() { + try { + downstream.onComplete(); + } finally { + w.dispose(); + } + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableDelaySubscriptionOther.java b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableDelaySubscriptionOther.java new file mode 100644 index 0000000000..978e4d2f32 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableDelaySubscriptionOther.java @@ -0,0 +1,108 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.disposables.SequentialDisposable; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +/** + * Delays the subscription to the main source until the other + * observable fires an event or completes. + * @param <T> the main type + * @param <U> the other value type, ignored + */ +public final class ObservableDelaySubscriptionOther<T, U> extends Observable<T> { + final ObservableSource<? extends T> main; + final ObservableSource<U> other; + + public ObservableDelaySubscriptionOther(ObservableSource<? extends T> main, ObservableSource<U> other) { + this.main = main; + this.other = other; + } + + @Override + public void subscribeActual(final Observer<? super T> child) { + final SequentialDisposable serial = new SequentialDisposable(); + child.onSubscribe(serial); + + Observer<U> otherObserver = new DelayObserver(serial, child); + + other.subscribe(otherObserver); + } + + final class DelayObserver implements Observer<U> { + final SequentialDisposable serial; + final Observer<? super T> child; + boolean done; + + DelayObserver(SequentialDisposable serial, Observer<? super T> child) { + this.serial = serial; + this.child = child; + } + + @Override + public void onSubscribe(Disposable d) { + serial.update(d); + } + + @Override + public void onNext(U t) { + onComplete(); + } + + @Override + public void onError(Throwable e) { + if (done) { + RxJavaPlugins.onError(e); + return; + } + done = true; + child.onError(e); + } + + @Override + public void onComplete() { + if (done) { + return; + } + done = true; + + main.subscribe(new OnComplete()); + } + + final class OnComplete implements Observer<T> { + @Override + public void onSubscribe(Disposable d) { + serial.update(d); + } + + @Override + public void onNext(T value) { + child.onNext(value); + } + + @Override + public void onError(Throwable e) { + child.onError(e); + } + + @Override + public void onComplete() { + child.onComplete(); + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableDematerialize.java b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableDematerialize.java new file mode 100644 index 0000000000..2016451986 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableDematerialize.java @@ -0,0 +1,127 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +import java.util.Objects; + +public final class ObservableDematerialize<T, R> extends AbstractObservableWithUpstream<T, R> { + + final Function<? super T, ? extends Notification<R>> selector; + + public ObservableDematerialize(ObservableSource<T> source, Function<? super T, ? extends Notification<R>> selector) { + super(source); + this.selector = selector; + } + + @Override + public void subscribeActual(Observer<? super R> observer) { + source.subscribe(new DematerializeObserver<>(observer, selector)); + } + + static final class DematerializeObserver<T, R> implements Observer<T>, Disposable { + final Observer<? super R> downstream; + + final Function<? super T, ? extends Notification<R>> selector; + + boolean done; + + Disposable upstream; + + DematerializeObserver(Observer<? super R> downstream, Function<? super T, ? extends Notification<R>> selector) { + this.downstream = downstream; + this.selector = selector; + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.validate(this.upstream, d)) { + this.upstream = d; + + downstream.onSubscribe(this); + } + } + + @Override + public void dispose() { + upstream.dispose(); + } + + @Override + public boolean isDisposed() { + return upstream.isDisposed(); + } + + @Override + public void onNext(T item) { + if (done) { + if (item instanceof Notification) { + Notification<?> notification = (Notification<?>)item; + if (notification.isOnError()) { + RxJavaPlugins.onError(notification.getError()); + } + } + return; + } + + Notification<R> notification; + + try { + notification = Objects.requireNonNull(selector.apply(item), "The selector returned a null Notification"); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + upstream.dispose(); + onError(ex); + return; + } + if (notification.isOnError()) { + upstream.dispose(); + onError(notification.getError()); + } + else if (notification.isOnComplete()) { + upstream.dispose(); + onComplete(); + } else { + downstream.onNext(notification.getValue()); + } + } + + @Override + public void onError(Throwable t) { + if (done) { + RxJavaPlugins.onError(t); + return; + } + done = true; + + downstream.onError(t); + } + + @Override + public void onComplete() { + if (done) { + return; + } + done = true; + + downstream.onComplete(); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableDetach.java b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableDetach.java new file mode 100644 index 0000000000..b9c6f2bade --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableDetach.java @@ -0,0 +1,92 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; +import io.reactivex.rxjava3.internal.util.EmptyComponent; + +/** + * Breaks the links between the upstream and the downstream (the Disposable and + * the Observer references) when the sequence terminates or gets disposed. + * + * @param <T> the value type + */ +public final class ObservableDetach<T> extends AbstractObservableWithUpstream<T, T> { + + public ObservableDetach(ObservableSource<T> source) { + super(source); + } + + @Override + protected void subscribeActual(Observer<? super T> observer) { + source.subscribe(new DetachObserver<>(observer)); + } + + static final class DetachObserver<T> implements Observer<T>, Disposable { + + Observer<? super T> downstream; + + Disposable upstream; + + DetachObserver(Observer<? super T> downstream) { + this.downstream = downstream; + } + + @Override + public void dispose() { + Disposable d = this.upstream; + this.upstream = EmptyComponent.INSTANCE; + this.downstream = EmptyComponent.asObserver(); + d.dispose(); + } + + @Override + public boolean isDisposed() { + return upstream.isDisposed(); + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.validate(this.upstream, d)) { + this.upstream = d; + + downstream.onSubscribe(this); + } + } + + @Override + public void onNext(T t) { + downstream.onNext(t); + } + + @Override + public void onError(Throwable t) { + Observer<? super T> a = downstream; + this.upstream = EmptyComponent.INSTANCE; + this.downstream = EmptyComponent.asObserver(); + a.onError(t); + } + + @Override + public void onComplete() { + Observer<? super T> a = downstream; + this.upstream = EmptyComponent.INSTANCE; + this.downstream = EmptyComponent.asObserver(); + a.onComplete(); + } + } +} + diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableDistinct.java b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableDistinct.java new file mode 100644 index 0000000000..ff377833a5 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableDistinct.java @@ -0,0 +1,135 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import java.util.Collection; +import java.util.Objects; + +import io.reactivex.rxjava3.annotations.Nullable; +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.disposables.EmptyDisposable; +import io.reactivex.rxjava3.internal.observers.BasicFuseableObserver; +import io.reactivex.rxjava3.internal.util.ExceptionHelper; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +public final class ObservableDistinct<T, K> extends AbstractObservableWithUpstream<T, T> { + + final Function<? super T, K> keySelector; + + final Supplier<? extends Collection<? super K>> collectionSupplier; + + public ObservableDistinct(ObservableSource<T> source, Function<? super T, K> keySelector, Supplier<? extends Collection<? super K>> collectionSupplier) { + super(source); + this.keySelector = keySelector; + this.collectionSupplier = collectionSupplier; + } + + @Override + protected void subscribeActual(Observer<? super T> observer) { + Collection<? super K> collection; + + try { + collection = ExceptionHelper.nullCheck(collectionSupplier.get(), "The collectionSupplier returned a null Collection."); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + EmptyDisposable.error(ex, observer); + return; + } + + source.subscribe(new DistinctObserver<>(observer, keySelector, collection)); + } + + static final class DistinctObserver<T, K> extends BasicFuseableObserver<T, T> { + + final Collection<? super K> collection; + + final Function<? super T, K> keySelector; + + DistinctObserver(Observer<? super T> actual, Function<? super T, K> keySelector, Collection<? super K> collection) { + super(actual); + this.keySelector = keySelector; + this.collection = collection; + } + + @Override + public void onNext(T value) { + if (done) { + return; + } + if (sourceMode == NONE) { + K key; + boolean b; + + try { + key = Objects.requireNonNull(keySelector.apply(value), "The keySelector returned a null key"); + b = collection.add(key); + } catch (Throwable ex) { + fail(ex); + return; + } + + if (b) { + downstream.onNext(value); + } + } else { + downstream.onNext(null); + } + } + + @Override + public void onError(Throwable e) { + if (done) { + RxJavaPlugins.onError(e); + } else { + done = true; + collection.clear(); + downstream.onError(e); + } + } + + @Override + public void onComplete() { + if (!done) { + done = true; + collection.clear(); + downstream.onComplete(); + } + } + + @Override + public int requestFusion(int mode) { + return transitiveBoundaryFusion(mode); + } + + @Nullable + @Override + public T poll() throws Throwable { + for (;;) { + T v = qd.poll(); + + if (v == null || collection.add(Objects.requireNonNull(keySelector.apply(v), "The keySelector returned a null key"))) { + return v; + } + } + } + + @Override + public void clear() { + collection.clear(); + super.clear(); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableDistinctUntilChanged.java b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableDistinctUntilChanged.java new file mode 100644 index 0000000000..7604db7c8f --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableDistinctUntilChanged.java @@ -0,0 +1,117 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import io.reactivex.rxjava3.annotations.Nullable; +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.observers.BasicFuseableObserver; + +public final class ObservableDistinctUntilChanged<T, K> extends AbstractObservableWithUpstream<T, T> { + + final Function<? super T, K> keySelector; + + final BiPredicate<? super K, ? super K> comparer; + + public ObservableDistinctUntilChanged(ObservableSource<T> source, Function<? super T, K> keySelector, BiPredicate<? super K, ? super K> comparer) { + super(source); + this.keySelector = keySelector; + this.comparer = comparer; + } + + @Override + protected void subscribeActual(Observer<? super T> observer) { + source.subscribe(new DistinctUntilChangedObserver<>(observer, keySelector, comparer)); + } + + static final class DistinctUntilChangedObserver<T, K> extends BasicFuseableObserver<T, T> { + + final Function<? super T, K> keySelector; + + final BiPredicate<? super K, ? super K> comparer; + + K last; + + boolean hasValue; + + DistinctUntilChangedObserver(Observer<? super T> actual, + Function<? super T, K> keySelector, + BiPredicate<? super K, ? super K> comparer) { + super(actual); + this.keySelector = keySelector; + this.comparer = comparer; + } + + @Override + public void onNext(T t) { + if (done) { + return; + } + if (sourceMode != NONE) { + downstream.onNext(t); + return; + } + + K key; + + try { + key = keySelector.apply(t); + if (hasValue) { + boolean equal = comparer.test(last, key); + last = key; + if (equal) { + return; + } + } else { + hasValue = true; + last = key; + } + } catch (Throwable ex) { + fail(ex); + return; + } + + downstream.onNext(t); + } + + @Override + public int requestFusion(int mode) { + return transitiveBoundaryFusion(mode); + } + + @Nullable + @Override + public T poll() throws Throwable { + for (;;) { + T v = qd.poll(); + if (v == null) { + return null; + } + K key = keySelector.apply(v); + if (!hasValue) { + hasValue = true; + last = key; + return v; + } + + if (!comparer.test(last, key)) { + last = key; + return v; + } + last = key; + } + } + + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableDoAfterNext.java b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableDoAfterNext.java new file mode 100644 index 0000000000..6382e6b822 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableDoAfterNext.java @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import io.reactivex.rxjava3.annotations.Nullable; +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.functions.Consumer; +import io.reactivex.rxjava3.internal.observers.BasicFuseableObserver; + +/** + * Calls a consumer after pushing the current item to the downstream. + * <p>History: 2.0.1 - experimental + * @param <T> the value type + * @since 2.1 + */ +public final class ObservableDoAfterNext<T> extends AbstractObservableWithUpstream<T, T> { + + final Consumer<? super T> onAfterNext; + + public ObservableDoAfterNext(ObservableSource<T> source, Consumer<? super T> onAfterNext) { + super(source); + this.onAfterNext = onAfterNext; + } + + @Override + protected void subscribeActual(Observer<? super T> observer) { + source.subscribe(new DoAfterObserver<>(observer, onAfterNext)); + } + + static final class DoAfterObserver<T> extends BasicFuseableObserver<T, T> { + + final Consumer<? super T> onAfterNext; + + DoAfterObserver(Observer<? super T> actual, Consumer<? super T> onAfterNext) { + super(actual); + this.onAfterNext = onAfterNext; + } + + @Override + public void onNext(T t) { + downstream.onNext(t); + + if (sourceMode == NONE) { + try { + onAfterNext.accept(t); + } catch (Throwable ex) { + fail(ex); + } + } + } + + @Override + public int requestFusion(int mode) { + return transitiveBoundaryFusion(mode); + } + + @Nullable + @Override + public T poll() throws Throwable { + T v = qd.poll(); + if (v != null) { + onAfterNext.accept(v); + } + return v; + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableDoFinally.java b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableDoFinally.java new file mode 100644 index 0000000000..e6d9031f72 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableDoFinally.java @@ -0,0 +1,150 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import io.reactivex.rxjava3.annotations.Nullable; +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.Action; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; +import io.reactivex.rxjava3.internal.observers.BasicIntQueueDisposable; +import io.reactivex.rxjava3.operators.QueueDisposable; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +/** + * Execute an action after an onError, onComplete or a dispose event. + * <p>History: 2.0.1 - experimental + * @param <T> the value type + * @since 2.1 + */ +public final class ObservableDoFinally<T> extends AbstractObservableWithUpstream<T, T> { + + final Action onFinally; + + public ObservableDoFinally(ObservableSource<T> source, Action onFinally) { + super(source); + this.onFinally = onFinally; + } + + @Override + protected void subscribeActual(Observer<? super T> observer) { + source.subscribe(new DoFinallyObserver<>(observer, onFinally)); + } + + static final class DoFinallyObserver<T> extends BasicIntQueueDisposable<T> implements Observer<T> { + + private static final long serialVersionUID = 4109457741734051389L; + + final Observer<? super T> downstream; + + final Action onFinally; + + Disposable upstream; + + QueueDisposable<T> qd; + + boolean syncFused; + + DoFinallyObserver(Observer<? super T> actual, Action onFinally) { + this.downstream = actual; + this.onFinally = onFinally; + } + + @SuppressWarnings("unchecked") + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.validate(this.upstream, d)) { + this.upstream = d; + if (d instanceof QueueDisposable) { + this.qd = (QueueDisposable<T>)d; + } + + downstream.onSubscribe(this); + } + } + + @Override + public void onNext(T t) { + downstream.onNext(t); + } + + @Override + public void onError(Throwable t) { + downstream.onError(t); + runFinally(); + } + + @Override + public void onComplete() { + downstream.onComplete(); + runFinally(); + } + + @Override + public void dispose() { + upstream.dispose(); + runFinally(); + } + + @Override + public boolean isDisposed() { + return upstream.isDisposed(); + } + + @Override + public int requestFusion(int mode) { + QueueDisposable<T> qd = this.qd; + if (qd != null && (mode & BOUNDARY) == 0) { + int m = qd.requestFusion(mode); + if (m != NONE) { + syncFused = m == SYNC; + } + return m; + } + return NONE; + } + + @Override + public void clear() { + qd.clear(); + } + + @Override + public boolean isEmpty() { + return qd.isEmpty(); + } + + @Nullable + @Override + public T poll() throws Throwable { + T v = qd.poll(); + if (v == null && syncFused) { + runFinally(); + } + return v; + } + + void runFinally() { + if (compareAndSet(0, 1)) { + try { + onFinally.run(); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + RxJavaPlugins.onError(ex); + } + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableDoOnEach.java b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableDoOnEach.java new file mode 100644 index 0000000000..f4869d19a8 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableDoOnEach.java @@ -0,0 +1,151 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +public final class ObservableDoOnEach<T> extends AbstractObservableWithUpstream<T, T> { + final Consumer<? super T> onNext; + final Consumer<? super Throwable> onError; + final Action onComplete; + final Action onAfterTerminate; + + public ObservableDoOnEach(ObservableSource<T> source, Consumer<? super T> onNext, + Consumer<? super Throwable> onError, + Action onComplete, + Action onAfterTerminate) { + super(source); + this.onNext = onNext; + this.onError = onError; + this.onComplete = onComplete; + this.onAfterTerminate = onAfterTerminate; + } + + @Override + public void subscribeActual(Observer<? super T> t) { + source.subscribe(new DoOnEachObserver<>(t, onNext, onError, onComplete, onAfterTerminate)); + } + + static final class DoOnEachObserver<T> implements Observer<T>, Disposable { + final Observer<? super T> downstream; + final Consumer<? super T> onNext; + final Consumer<? super Throwable> onError; + final Action onComplete; + final Action onAfterTerminate; + + Disposable upstream; + + boolean done; + + DoOnEachObserver( + Observer<? super T> actual, + Consumer<? super T> onNext, + Consumer<? super Throwable> onError, + Action onComplete, + Action onAfterTerminate) { + this.downstream = actual; + this.onNext = onNext; + this.onError = onError; + this.onComplete = onComplete; + this.onAfterTerminate = onAfterTerminate; + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.validate(this.upstream, d)) { + this.upstream = d; + downstream.onSubscribe(this); + } + } + + @Override + public void dispose() { + upstream.dispose(); + } + + @Override + public boolean isDisposed() { + return upstream.isDisposed(); + } + + @Override + public void onNext(T t) { + if (done) { + return; + } + try { + onNext.accept(t); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + upstream.dispose(); + onError(e); + return; + } + + downstream.onNext(t); + } + + @Override + public void onError(Throwable t) { + if (done) { + RxJavaPlugins.onError(t); + return; + } + done = true; + try { + onError.accept(t); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + t = new CompositeException(t, e); + } + downstream.onError(t); + + try { + onAfterTerminate.run(); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + RxJavaPlugins.onError(e); + } + } + + @Override + public void onComplete() { + if (done) { + return; + } + try { + onComplete.run(); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + onError(e); + return; + } + + done = true; + downstream.onComplete(); + + try { + onAfterTerminate.run(); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + RxJavaPlugins.onError(e); + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableDoOnLifecycle.java b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableDoOnLifecycle.java new file mode 100644 index 0000000000..f185231246 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableDoOnLifecycle.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.observers.DisposableLambdaObserver; + +public final class ObservableDoOnLifecycle<T> extends AbstractObservableWithUpstream<T, T> { + private final Consumer<? super Disposable> onSubscribe; + private final Action onDispose; + + public ObservableDoOnLifecycle(Observable<T> upstream, Consumer<? super Disposable> onSubscribe, + Action onDispose) { + super(upstream); + this.onSubscribe = onSubscribe; + this.onDispose = onDispose; + } + + @Override + protected void subscribeActual(Observer<? super T> observer) { + source.subscribe(new DisposableLambdaObserver<>(observer, onSubscribe, onDispose)); + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableElementAt.java b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableElementAt.java new file mode 100644 index 0000000000..8d8a535bcf --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableElementAt.java @@ -0,0 +1,119 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import java.util.NoSuchElementException; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +public final class ObservableElementAt<T> extends AbstractObservableWithUpstream<T, T> { + final long index; + final T defaultValue; + final boolean errorOnFewer; + + public ObservableElementAt(ObservableSource<T> source, long index, T defaultValue, boolean errorOnFewer) { + super(source); + this.index = index; + this.defaultValue = defaultValue; + this.errorOnFewer = errorOnFewer; + } + + @Override + public void subscribeActual(Observer<? super T> t) { + source.subscribe(new ElementAtObserver<>(t, index, defaultValue, errorOnFewer)); + } + + static final class ElementAtObserver<T> implements Observer<T>, Disposable { + final Observer<? super T> downstream; + final long index; + final T defaultValue; + final boolean errorOnFewer; + + Disposable upstream; + + long count; + + boolean done; + + ElementAtObserver(Observer<? super T> actual, long index, T defaultValue, boolean errorOnFewer) { + this.downstream = actual; + this.index = index; + this.defaultValue = defaultValue; + this.errorOnFewer = errorOnFewer; + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.validate(this.upstream, d)) { + this.upstream = d; + downstream.onSubscribe(this); + } + } + + @Override + public void dispose() { + upstream.dispose(); + } + + @Override + public boolean isDisposed() { + return upstream.isDisposed(); + } + + @Override + public void onNext(T t) { + if (done) { + return; + } + long c = count; + if (c == index) { + done = true; + upstream.dispose(); + downstream.onNext(t); + downstream.onComplete(); + return; + } + count = c + 1; + } + + @Override + public void onError(Throwable t) { + if (done) { + RxJavaPlugins.onError(t); + return; + } + done = true; + downstream.onError(t); + } + + @Override + public void onComplete() { + if (!done) { + done = true; + T v = defaultValue; + if (v == null && errorOnFewer) { + downstream.onError(new NoSuchElementException()); + } else { + if (v != null) { + downstream.onNext(v); + } + downstream.onComplete(); + } + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableElementAtMaybe.java b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableElementAtMaybe.java new file mode 100644 index 0000000000..14bb368a23 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableElementAtMaybe.java @@ -0,0 +1,106 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; +import io.reactivex.rxjava3.internal.fuseable.FuseToObservable; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +public final class ObservableElementAtMaybe<T> extends Maybe<T> implements FuseToObservable<T> { + final ObservableSource<T> source; + final long index; + public ObservableElementAtMaybe(ObservableSource<T> source, long index) { + this.source = source; + this.index = index; + } + + @Override + public void subscribeActual(MaybeObserver<? super T> t) { + source.subscribe(new ElementAtObserver<>(t, index)); + } + + @Override + public Observable<T> fuseToObservable() { + return RxJavaPlugins.onAssembly(new ObservableElementAt<>(source, index, null, false)); + } + + static final class ElementAtObserver<T> implements Observer<T>, Disposable { + final MaybeObserver<? super T> downstream; + final long index; + + Disposable upstream; + + long count; + + boolean done; + + ElementAtObserver(MaybeObserver<? super T> actual, long index) { + this.downstream = actual; + this.index = index; + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.validate(this.upstream, d)) { + this.upstream = d; + downstream.onSubscribe(this); + } + } + + @Override + public void dispose() { + upstream.dispose(); + } + + @Override + public boolean isDisposed() { + return upstream.isDisposed(); + } + + @Override + public void onNext(T t) { + if (done) { + return; + } + long c = count; + if (c == index) { + done = true; + upstream.dispose(); + downstream.onSuccess(t); + return; + } + count = c + 1; + } + + @Override + public void onError(Throwable t) { + if (done) { + RxJavaPlugins.onError(t); + return; + } + done = true; + downstream.onError(t); + } + + @Override + public void onComplete() { + if (!done) { + done = true; + downstream.onComplete(); + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableElementAtSingle.java b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableElementAtSingle.java new file mode 100644 index 0000000000..c779f9eec2 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableElementAtSingle.java @@ -0,0 +1,120 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import java.util.NoSuchElementException; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; +import io.reactivex.rxjava3.internal.fuseable.FuseToObservable; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +public final class ObservableElementAtSingle<T> extends Single<T> implements FuseToObservable<T> { + final ObservableSource<T> source; + final long index; + final T defaultValue; + + public ObservableElementAtSingle(ObservableSource<T> source, long index, T defaultValue) { + this.source = source; + this.index = index; + this.defaultValue = defaultValue; + } + + @Override + public void subscribeActual(SingleObserver<? super T> t) { + source.subscribe(new ElementAtObserver<>(t, index, defaultValue)); + } + + @Override + public Observable<T> fuseToObservable() { + return RxJavaPlugins.onAssembly(new ObservableElementAt<>(source, index, defaultValue, true)); + } + + static final class ElementAtObserver<T> implements Observer<T>, Disposable { + final SingleObserver<? super T> downstream; + final long index; + final T defaultValue; + + Disposable upstream; + + long count; + + boolean done; + + ElementAtObserver(SingleObserver<? super T> actual, long index, T defaultValue) { + this.downstream = actual; + this.index = index; + this.defaultValue = defaultValue; + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.validate(this.upstream, d)) { + this.upstream = d; + downstream.onSubscribe(this); + } + } + + @Override + public void dispose() { + upstream.dispose(); + } + + @Override + public boolean isDisposed() { + return upstream.isDisposed(); + } + + @Override + public void onNext(T t) { + if (done) { + return; + } + long c = count; + if (c == index) { + done = true; + upstream.dispose(); + downstream.onSuccess(t); + return; + } + count = c + 1; + } + + @Override + public void onError(Throwable t) { + if (done) { + RxJavaPlugins.onError(t); + return; + } + done = true; + downstream.onError(t); + } + + @Override + public void onComplete() { + if (!done) { + done = true; + + T v = defaultValue; + + if (v != null) { + downstream.onSuccess(v); + } else { + downstream.onError(new NoSuchElementException()); + } + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableEmpty.java b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableEmpty.java new file mode 100644 index 0000000000..7ff9e8b955 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableEmpty.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.internal.disposables.EmptyDisposable; +import io.reactivex.rxjava3.operators.ScalarSupplier; + +public final class ObservableEmpty extends Observable<Object> implements ScalarSupplier<Object> { + public static final Observable<Object> INSTANCE = new ObservableEmpty(); + + private ObservableEmpty() { + } + + @Override + protected void subscribeActual(Observer<? super Object> o) { + EmptyDisposable.complete(o); + } + + @Override + public Object get() { + return null; // null scalar is interpreted as being empty + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableError.java b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableError.java new file mode 100644 index 0000000000..f25af956e0 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableError.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.Supplier; +import io.reactivex.rxjava3.internal.disposables.EmptyDisposable; +import io.reactivex.rxjava3.internal.util.ExceptionHelper; + +public final class ObservableError<T> extends Observable<T> { + final Supplier<? extends Throwable> errorSupplier; + public ObservableError(Supplier<? extends Throwable> errorSupplier) { + this.errorSupplier = errorSupplier; + } + + @Override + public void subscribeActual(Observer<? super T> observer) { + Throwable error; + try { + error = ExceptionHelper.nullCheck(errorSupplier.get(), "Supplier returned a null Throwable."); + } catch (Throwable t) { + Exceptions.throwIfFatal(t); + error = t; + } + EmptyDisposable.error(error, observer); + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableFilter.java b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableFilter.java new file mode 100644 index 0000000000..ed7068f6a0 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableFilter.java @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import io.reactivex.rxjava3.annotations.Nullable; +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.functions.Predicate; +import io.reactivex.rxjava3.internal.observers.BasicFuseableObserver; + +public final class ObservableFilter<T> extends AbstractObservableWithUpstream<T, T> { + final Predicate<? super T> predicate; + public ObservableFilter(ObservableSource<T> source, Predicate<? super T> predicate) { + super(source); + this.predicate = predicate; + } + + @Override + public void subscribeActual(Observer<? super T> observer) { + source.subscribe(new FilterObserver<>(observer, predicate)); + } + + static final class FilterObserver<T> extends BasicFuseableObserver<T, T> { + final Predicate<? super T> filter; + + FilterObserver(Observer<? super T> actual, Predicate<? super T> filter) { + super(actual); + this.filter = filter; + } + + @Override + public void onNext(T t) { + if (sourceMode == NONE) { + boolean b; + try { + b = filter.test(t); + } catch (Throwable e) { + fail(e); + return; + } + if (b) { + downstream.onNext(t); + } + } else { + downstream.onNext(null); + } + } + + @Override + public int requestFusion(int mode) { + return transitiveBoundaryFusion(mode); + } + + @Nullable + @Override + public T poll() throws Throwable { + for (;;) { + T v = qd.poll(); + if (v == null || filter.test(v)) { + return v; + } + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableFlatMap.java b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableFlatMap.java new file mode 100644 index 0000000000..d19b505999 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableFlatMap.java @@ -0,0 +1,549 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import java.util.*; +import java.util.concurrent.atomic.*; + +import io.reactivex.rxjava3.core.ObservableSource; +import io.reactivex.rxjava3.core.Observer; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; +import io.reactivex.rxjava3.internal.util.AtomicThrowable; +import io.reactivex.rxjava3.operators.*; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +public final class ObservableFlatMap<T, U> extends AbstractObservableWithUpstream<T, U> { + final Function<? super T, ? extends ObservableSource<? extends U>> mapper; + final boolean delayErrors; + final int maxConcurrency; + final int bufferSize; + + public ObservableFlatMap(ObservableSource<T> source, + Function<? super T, ? extends ObservableSource<? extends U>> mapper, + boolean delayErrors, int maxConcurrency, int bufferSize) { + super(source); + this.mapper = mapper; + this.delayErrors = delayErrors; + this.maxConcurrency = maxConcurrency; + this.bufferSize = bufferSize; + } + + @Override + public void subscribeActual(Observer<? super U> t) { + + if (ObservableScalarXMap.tryScalarXMapSubscribe(source, t, mapper)) { + return; + } + + source.subscribe(new MergeObserver<>(t, mapper, delayErrors, maxConcurrency, bufferSize)); + } + + static final class MergeObserver<T, U> extends AtomicInteger implements Disposable, Observer<T> { + + private static final long serialVersionUID = -2117620485640801370L; + + final Observer<? super U> downstream; + final Function<? super T, ? extends ObservableSource<? extends U>> mapper; + final boolean delayErrors; + final int maxConcurrency; + final int bufferSize; + + volatile SimplePlainQueue<U> queue; + + volatile boolean done; + + final AtomicThrowable errors = new AtomicThrowable(); + + volatile boolean disposed; + + final AtomicReference<InnerObserver<?, ?>[]> observers; + + static final InnerObserver<?, ?>[] EMPTY = new InnerObserver<?, ?>[0]; + + static final InnerObserver<?, ?>[] CANCELLED = new InnerObserver<?, ?>[0]; + + Disposable upstream; + + long uniqueId; + int lastIndex; + + Queue<ObservableSource<? extends U>> sources; + + int wip; + + MergeObserver(Observer<? super U> actual, Function<? super T, ? extends ObservableSource<? extends U>> mapper, + boolean delayErrors, int maxConcurrency, int bufferSize) { + this.downstream = actual; + this.mapper = mapper; + this.delayErrors = delayErrors; + this.maxConcurrency = maxConcurrency; + this.bufferSize = bufferSize; + if (maxConcurrency != Integer.MAX_VALUE) { + sources = new ArrayDeque<>(maxConcurrency); + } + this.observers = new AtomicReference<>(EMPTY); + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.validate(this.upstream, d)) { + this.upstream = d; + downstream.onSubscribe(this); + } + } + + @Override + public void onNext(T t) { + // safeguard against misbehaving sources + if (done) { + return; + } + ObservableSource<? extends U> p; + try { + p = Objects.requireNonNull(mapper.apply(t), "The mapper returned a null ObservableSource"); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + upstream.dispose(); + onError(e); + return; + } + + if (maxConcurrency != Integer.MAX_VALUE) { + synchronized (this) { + if (wip == maxConcurrency) { + sources.offer(p); + return; + } + wip++; + } + } + + subscribeInner(p); + } + + @SuppressWarnings("unchecked") + void subscribeInner(ObservableSource<? extends U> p) { + for (;;) { + if (p instanceof Supplier) { + if (tryEmitScalar(((Supplier<? extends U>)p)) && maxConcurrency != Integer.MAX_VALUE) { + boolean empty = false; + synchronized (this) { + p = sources.poll(); + if (p == null) { + wip--; + empty = true; + } + } + if (empty) { + drain(); + break; + } + } else { + break; + } + } else { + InnerObserver<T, U> inner = new InnerObserver<>(this, uniqueId++); + if (addInner(inner)) { + p.subscribe(inner); + } + break; + } + } + } + + boolean addInner(InnerObserver<T, U> inner) { + for (;;) { + InnerObserver<?, ?>[] a = observers.get(); + if (a == CANCELLED) { + inner.dispose(); + return false; + } + int n = a.length; + InnerObserver<?, ?>[] b = new InnerObserver[n + 1]; + System.arraycopy(a, 0, b, 0, n); + b[n] = inner; + if (observers.compareAndSet(a, b)) { + return true; + } + } + } + + void removeInner(InnerObserver<T, U> inner) { + for (;;) { + InnerObserver<?, ?>[] a = observers.get(); + int n = a.length; + int j = -1; + for (int i = 0; i < n; i++) { + if (a[i] == inner) { + j = i; + break; + } + } + if (j < 0) { + return; + } + InnerObserver<?, ?>[] b; + if (n == 1) { + b = EMPTY; + } else { + b = new InnerObserver<?, ?>[n - 1]; + System.arraycopy(a, 0, b, 0, j); + System.arraycopy(a, j + 1, b, j, n - j - 1); + } + if (observers.compareAndSet(a, b)) { + return; + } + } + } + + boolean tryEmitScalar(Supplier<? extends U> value) { + U u; + try { + u = value.get(); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + errors.tryAddThrowableOrReport(ex); + drain(); + return true; + } + + if (u == null) { + return true; + } + + if (get() == 0 && compareAndSet(0, 1)) { + downstream.onNext(u); + if (decrementAndGet() == 0) { + return true; + } + } else { + SimplePlainQueue<U> q = queue; + if (q == null) { + if (maxConcurrency == Integer.MAX_VALUE) { + q = new SpscLinkedArrayQueue<>(bufferSize); + } else { + q = new SpscArrayQueue<>(maxConcurrency); + } + queue = q; + } + + q.offer(u); + if (getAndIncrement() != 0) { + return false; + } + } + drainLoop(); + return true; + } + + void tryEmit(U value, InnerObserver<T, U> inner) { + if (get() == 0 && compareAndSet(0, 1)) { + downstream.onNext(value); + if (decrementAndGet() == 0) { + return; + } + } else { + SimpleQueue<U> q = inner.queue; + if (q == null) { + q = new SpscLinkedArrayQueue<>(bufferSize); + inner.queue = q; + } + q.offer(value); + if (getAndIncrement() != 0) { + return; + } + } + drainLoop(); + } + + @Override + public void onError(Throwable t) { + if (done) { + RxJavaPlugins.onError(t); + return; + } + if (errors.tryAddThrowableOrReport(t)) { + done = true; + drain(); + } + } + + @Override + public void onComplete() { + if (done) { + return; + } + done = true; + drain(); + } + + @Override + public void dispose() { + disposed = true; + if (disposeAll()) { + errors.tryTerminateAndReport(); + } + } + + @Override + public boolean isDisposed() { + return disposed; + } + + void drain() { + if (getAndIncrement() == 0) { + drainLoop(); + } + } + + void drainLoop() { + final Observer<? super U> child = this.downstream; + int missed = 1; + for (;;) { + if (checkTerminate()) { + return; + } + int innerCompleted = 0; + SimplePlainQueue<U> svq = queue; + + if (svq != null) { + for (;;) { + if (checkTerminate()) { + return; + } + + U o = svq.poll(); + + if (o == null) { + break; + } + + child.onNext(o); + innerCompleted++; + } + } + + if (innerCompleted != 0) { + if (maxConcurrency != Integer.MAX_VALUE) { + subscribeMore(innerCompleted); + innerCompleted = 0; + } + continue; + } + + boolean d = done; + svq = queue; + InnerObserver<?, ?>[] inner = observers.get(); + int n = inner.length; + + int nSources = 0; + if (maxConcurrency != Integer.MAX_VALUE) { + synchronized (this) { + nSources = sources.size(); + } + } + + if (d && (svq == null || svq.isEmpty()) && n == 0 && nSources == 0) { + errors.tryTerminateConsumer(downstream); + return; + } + + if (n != 0) { + int j = Math.min(n - 1, lastIndex); + + sourceLoop: + for (int i = 0; i < n; i++) { + if (checkTerminate()) { + return; + } + + @SuppressWarnings("unchecked") + InnerObserver<T, U> is = (InnerObserver<T, U>)inner[j]; + SimpleQueue<U> q = is.queue; + if (q != null) { + for (;;) { + U o; + try { + o = q.poll(); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + is.dispose(); + errors.tryAddThrowableOrReport(ex); + if (checkTerminate()) { + return; + } + removeInner(is); + innerCompleted++; + j++; + if (j == n) { + j = 0; + } + continue sourceLoop; + } + if (o == null) { + break; + } + + child.onNext(o); + + if (checkTerminate()) { + return; + } + } + } + + boolean innerDone = is.done; + SimpleQueue<U> innerQueue = is.queue; + if (innerDone && (innerQueue == null || innerQueue.isEmpty())) { + removeInner(is); + innerCompleted++; + } + + j++; + if (j == n) { + j = 0; + } + } + lastIndex = j; + } + + if (innerCompleted != 0) { + if (maxConcurrency != Integer.MAX_VALUE) { + subscribeMore(innerCompleted); + innerCompleted = 0; + } + continue; + } + + missed = addAndGet(-missed); + if (missed == 0) { + break; + } + } + } + + void subscribeMore(int innerCompleted) { + while (innerCompleted-- != 0) { + ObservableSource<? extends U> p; + synchronized (this) { + p = sources.poll(); + if (p == null) { + wip--; + continue; + } + } + subscribeInner(p); + } + } + + boolean checkTerminate() { + if (disposed) { + return true; + } + Throwable e = errors.get(); + if (!delayErrors && (e != null)) { + disposeAll(); + errors.tryTerminateConsumer(downstream); + return true; + } + return false; + } + + boolean disposeAll() { + upstream.dispose(); + InnerObserver<?, ?>[] a = observers.getAndSet(CANCELLED); + if (a != CANCELLED) { + for (InnerObserver<?, ?> inner : a) { + inner.dispose(); + } + return true; + } + return false; + } + } + + static final class InnerObserver<T, U> extends AtomicReference<Disposable> + implements Observer<U> { + + private static final long serialVersionUID = -4606175640614850599L; + final long id; + final MergeObserver<T, U> parent; + + volatile boolean done; + volatile SimpleQueue<U> queue; + + int fusionMode; + + InnerObserver(MergeObserver<T, U> parent, long id) { + this.id = id; + this.parent = parent; + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.setOnce(this, d)) { + if (d instanceof QueueDisposable) { + @SuppressWarnings("unchecked") + QueueDisposable<U> qd = (QueueDisposable<U>) d; + + int m = qd.requestFusion(QueueDisposable.ANY | QueueDisposable.BOUNDARY); + if (m == QueueDisposable.SYNC) { + fusionMode = m; + queue = qd; + done = true; + parent.drain(); + return; + } + if (m == QueueDisposable.ASYNC) { + fusionMode = m; + queue = qd; + } + } + } + } + + @Override + public void onNext(U t) { + if (fusionMode == QueueDisposable.NONE) { + parent.tryEmit(t, this); + } else { + parent.drain(); + } + } + + @Override + public void onError(Throwable t) { + if (parent.errors.tryAddThrowableOrReport(t)) { + if (!parent.delayErrors) { + parent.disposeAll(); + } + done = true; + parent.drain(); + } + } + + @Override + public void onComplete() { + done = true; + parent.drain(); + } + + public void dispose() { + DisposableHelper.dispose(this); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableFlatMapCompletable.java b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableFlatMapCompletable.java new file mode 100644 index 0000000000..ae7c6f98ac --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableFlatMapCompletable.java @@ -0,0 +1,204 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import java.util.Objects; +import java.util.concurrent.atomic.AtomicReference; + +import io.reactivex.rxjava3.annotations.Nullable; +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.*; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; +import io.reactivex.rxjava3.internal.observers.BasicIntQueueDisposable; +import io.reactivex.rxjava3.internal.util.AtomicThrowable; + +/** + * Maps a sequence of values into CompletableSources and awaits their termination. + * @param <T> the value type + */ +public final class ObservableFlatMapCompletable<T> extends AbstractObservableWithUpstream<T, T> { + + final Function<? super T, ? extends CompletableSource> mapper; + + final boolean delayErrors; + + public ObservableFlatMapCompletable(ObservableSource<T> source, + Function<? super T, ? extends CompletableSource> mapper, boolean delayErrors) { + super(source); + this.mapper = mapper; + this.delayErrors = delayErrors; + } + + @Override + protected void subscribeActual(Observer<? super T> observer) { + source.subscribe(new FlatMapCompletableMainObserver<>(observer, mapper, delayErrors)); + } + + static final class FlatMapCompletableMainObserver<T> extends BasicIntQueueDisposable<T> + implements Observer<T> { + private static final long serialVersionUID = 8443155186132538303L; + + final Observer<? super T> downstream; + + final AtomicThrowable errors; + + final Function<? super T, ? extends CompletableSource> mapper; + + final boolean delayErrors; + + final CompositeDisposable set; + + Disposable upstream; + + volatile boolean disposed; + + FlatMapCompletableMainObserver(Observer<? super T> observer, Function<? super T, ? extends CompletableSource> mapper, boolean delayErrors) { + this.downstream = observer; + this.mapper = mapper; + this.delayErrors = delayErrors; + this.errors = new AtomicThrowable(); + this.set = new CompositeDisposable(); + this.lazySet(1); + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.validate(this.upstream, d)) { + this.upstream = d; + + downstream.onSubscribe(this); + } + } + + @Override + public void onNext(T value) { + CompletableSource cs; + + try { + cs = Objects.requireNonNull(mapper.apply(value), "The mapper returned a null CompletableSource"); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + upstream.dispose(); + onError(ex); + return; + } + + getAndIncrement(); + + InnerObserver inner = new InnerObserver(); + + if (!disposed && set.add(inner)) { + cs.subscribe(inner); + } + } + + @Override + public void onError(Throwable e) { + if (errors.tryAddThrowableOrReport(e)) { + if (delayErrors) { + if (decrementAndGet() == 0) { + errors.tryTerminateConsumer(downstream); + } + } else { + disposed = true; + upstream.dispose(); + set.dispose(); + errors.tryTerminateConsumer(downstream); + } + } + } + + @Override + public void onComplete() { + if (decrementAndGet() == 0) { + errors.tryTerminateConsumer(downstream); + } + } + + @Override + public void dispose() { + disposed = true; + upstream.dispose(); + set.dispose(); + errors.tryTerminateAndReport(); + } + + @Override + public boolean isDisposed() { + return upstream.isDisposed(); + } + + @Nullable + @Override + public T poll() { + return null; // always empty + } + + @Override + public boolean isEmpty() { + return true; // always empty + } + + @Override + public void clear() { + // nothing to clear + } + + @Override + public int requestFusion(int mode) { + return mode & ASYNC; + } + + void innerComplete(InnerObserver inner) { + set.delete(inner); + onComplete(); + } + + void innerError(InnerObserver inner, Throwable e) { + set.delete(inner); + onError(e); + } + + final class InnerObserver extends AtomicReference<Disposable> implements CompletableObserver, Disposable { + private static final long serialVersionUID = 8606673141535671828L; + + @Override + public void onSubscribe(Disposable d) { + DisposableHelper.setOnce(this, d); + } + + @Override + public void onComplete() { + innerComplete(this); + } + + @Override + public void onError(Throwable e) { + innerError(this, e); + } + + @Override + public void dispose() { + DisposableHelper.dispose(this); + } + + @Override + public boolean isDisposed() { + return DisposableHelper.isDisposed(get()); + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableFlatMapCompletableCompletable.java b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableFlatMapCompletableCompletable.java new file mode 100644 index 0000000000..62ea03a54d --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableFlatMapCompletableCompletable.java @@ -0,0 +1,189 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import java.util.Objects; +import java.util.concurrent.atomic.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.*; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; +import io.reactivex.rxjava3.internal.fuseable.FuseToObservable; +import io.reactivex.rxjava3.internal.util.AtomicThrowable; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +/** + * Maps a sequence of values into CompletableSources and awaits their termination. + * @param <T> the value type + */ +public final class ObservableFlatMapCompletableCompletable<T> extends Completable implements FuseToObservable<T> { + + final ObservableSource<T> source; + + final Function<? super T, ? extends CompletableSource> mapper; + + final boolean delayErrors; + + public ObservableFlatMapCompletableCompletable(ObservableSource<T> source, + Function<? super T, ? extends CompletableSource> mapper, boolean delayErrors) { + this.source = source; + this.mapper = mapper; + this.delayErrors = delayErrors; + } + + @Override + protected void subscribeActual(CompletableObserver observer) { + source.subscribe(new FlatMapCompletableMainObserver<>(observer, mapper, delayErrors)); + } + + @Override + public Observable<T> fuseToObservable() { + return RxJavaPlugins.onAssembly(new ObservableFlatMapCompletable<>(source, mapper, delayErrors)); + } + + static final class FlatMapCompletableMainObserver<T> extends AtomicInteger implements Disposable, Observer<T> { + private static final long serialVersionUID = 8443155186132538303L; + + final CompletableObserver downstream; + + final AtomicThrowable errors; + + final Function<? super T, ? extends CompletableSource> mapper; + + final boolean delayErrors; + + final CompositeDisposable set; + + Disposable upstream; + + volatile boolean disposed; + + FlatMapCompletableMainObserver(CompletableObserver observer, Function<? super T, ? extends CompletableSource> mapper, boolean delayErrors) { + this.downstream = observer; + this.mapper = mapper; + this.delayErrors = delayErrors; + this.errors = new AtomicThrowable(); + this.set = new CompositeDisposable(); + this.lazySet(1); + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.validate(this.upstream, d)) { + this.upstream = d; + + downstream.onSubscribe(this); + } + } + + @Override + public void onNext(T value) { + CompletableSource cs; + + try { + cs = Objects.requireNonNull(mapper.apply(value), "The mapper returned a null CompletableSource"); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + upstream.dispose(); + onError(ex); + return; + } + + getAndIncrement(); + + InnerObserver inner = new InnerObserver(); + + if (!disposed && set.add(inner)) { + cs.subscribe(inner); + } + } + + @Override + public void onError(Throwable e) { + if (errors.tryAddThrowableOrReport(e)) { + if (delayErrors) { + if (decrementAndGet() == 0) { + errors.tryTerminateConsumer(downstream); + } + } else { + disposed = true; + upstream.dispose(); + set.dispose(); + errors.tryTerminateConsumer(downstream); + } + } + } + + @Override + public void onComplete() { + if (decrementAndGet() == 0) { + errors.tryTerminateConsumer(downstream); + } + } + + @Override + public void dispose() { + disposed = true; + upstream.dispose(); + set.dispose(); + errors.tryTerminateAndReport(); + } + + @Override + public boolean isDisposed() { + return upstream.isDisposed(); + } + + void innerComplete(InnerObserver inner) { + set.delete(inner); + onComplete(); + } + + void innerError(InnerObserver inner, Throwable e) { + set.delete(inner); + onError(e); + } + + final class InnerObserver extends AtomicReference<Disposable> implements CompletableObserver, Disposable { + private static final long serialVersionUID = 8606673141535671828L; + + @Override + public void onSubscribe(Disposable d) { + DisposableHelper.setOnce(this, d); + } + + @Override + public void onComplete() { + innerComplete(this); + } + + @Override + public void onError(Throwable e) { + innerError(this, e); + } + + @Override + public void dispose() { + DisposableHelper.dispose(this); + } + + @Override + public boolean isDisposed() { + return DisposableHelper.isDisposed(get()); + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableFlatMapMaybe.java b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableFlatMapMaybe.java new file mode 100644 index 0000000000..b8d223d4ac --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableFlatMapMaybe.java @@ -0,0 +1,313 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import java.util.Objects; +import java.util.concurrent.atomic.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.*; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; +import io.reactivex.rxjava3.internal.util.AtomicThrowable; +import io.reactivex.rxjava3.operators.SpscLinkedArrayQueue; + +/** + * Maps upstream values into MaybeSources and merges their signals into one sequence. + * @param <T> the source value type + * @param <R> the result value type + */ +public final class ObservableFlatMapMaybe<T, R> extends AbstractObservableWithUpstream<T, R> { + + final Function<? super T, ? extends MaybeSource<? extends R>> mapper; + + final boolean delayErrors; + + public ObservableFlatMapMaybe(ObservableSource<T> source, Function<? super T, ? extends MaybeSource<? extends R>> mapper, + boolean delayError) { + super(source); + this.mapper = mapper; + this.delayErrors = delayError; + } + + @Override + protected void subscribeActual(Observer<? super R> observer) { + source.subscribe(new FlatMapMaybeObserver<>(observer, mapper, delayErrors)); + } + + static final class FlatMapMaybeObserver<T, R> + extends AtomicInteger + implements Observer<T>, Disposable { + + private static final long serialVersionUID = 8600231336733376951L; + + final Observer<? super R> downstream; + + final boolean delayErrors; + + final CompositeDisposable set; + + final AtomicInteger active; + + final AtomicThrowable errors; + + final Function<? super T, ? extends MaybeSource<? extends R>> mapper; + + final AtomicReference<SpscLinkedArrayQueue<R>> queue; + + Disposable upstream; + + volatile boolean cancelled; + + FlatMapMaybeObserver(Observer<? super R> actual, + Function<? super T, ? extends MaybeSource<? extends R>> mapper, boolean delayErrors) { + this.downstream = actual; + this.mapper = mapper; + this.delayErrors = delayErrors; + this.set = new CompositeDisposable(); + this.errors = new AtomicThrowable(); + this.active = new AtomicInteger(1); + this.queue = new AtomicReference<>(); + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.validate(this.upstream, d)) { + this.upstream = d; + + downstream.onSubscribe(this); + } + } + + @Override + public void onNext(T t) { + MaybeSource<? extends R> ms; + + try { + ms = Objects.requireNonNull(mapper.apply(t), "The mapper returned a null MaybeSource"); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + upstream.dispose(); + onError(ex); + return; + } + + active.getAndIncrement(); + + InnerObserver inner = new InnerObserver(); + + if (!cancelled && set.add(inner)) { + ms.subscribe(inner); + } + } + + @Override + public void onError(Throwable t) { + active.decrementAndGet(); + if (errors.tryAddThrowableOrReport(t)) { + if (!delayErrors) { + set.dispose(); + } + drain(); + } + } + + @Override + public void onComplete() { + active.decrementAndGet(); + drain(); + } + + @Override + public void dispose() { + cancelled = true; + upstream.dispose(); + set.dispose(); + errors.tryTerminateAndReport(); + } + + @Override + public boolean isDisposed() { + return cancelled; + } + + void innerSuccess(InnerObserver inner, R value) { + set.delete(inner); + if (get() == 0 && compareAndSet(0, 1)) { + downstream.onNext(value); + + boolean d = active.decrementAndGet() == 0; + SpscLinkedArrayQueue<R> q = queue.get(); + + if (d && (q == null || q.isEmpty())) { + errors.tryTerminateConsumer(downstream); + return; + } + if (decrementAndGet() == 0) { + return; + } + } else { + SpscLinkedArrayQueue<R> q = getOrCreateQueue(); + synchronized (q) { + q.offer(value); + } + active.decrementAndGet(); + if (getAndIncrement() != 0) { + return; + } + } + drainLoop(); + } + + SpscLinkedArrayQueue<R> getOrCreateQueue() { + SpscLinkedArrayQueue<R> current = queue.get(); + if (current != null) { + return current; + } + current = new SpscLinkedArrayQueue<>(Observable.bufferSize()); + if (queue.compareAndSet(null, current)) { + return current; + } + return queue.get(); + } + + void innerError(InnerObserver inner, Throwable e) { + set.delete(inner); + if (errors.tryAddThrowableOrReport(e)) { + if (!delayErrors) { + upstream.dispose(); + set.dispose(); + } + active.decrementAndGet(); + drain(); + } + } + + void innerComplete(InnerObserver inner) { + set.delete(inner); + + if (get() == 0 && compareAndSet(0, 1)) { + boolean d = active.decrementAndGet() == 0; + SpscLinkedArrayQueue<R> q = queue.get(); + + if (d && (q == null || q.isEmpty())) { + errors.tryTerminateConsumer(downstream); + return; + } + if (decrementAndGet() == 0) { + return; + } + drainLoop(); + } else { + active.decrementAndGet(); + drain(); + } + } + + void drain() { + if (getAndIncrement() == 0) { + drainLoop(); + } + } + + void clear() { + SpscLinkedArrayQueue<R> q = queue.get(); + if (q != null) { + q.clear(); + } + } + + void drainLoop() { + int missed = 1; + Observer<? super R> a = downstream; + AtomicInteger n = active; + AtomicReference<SpscLinkedArrayQueue<R>> qr = queue; + + for (;;) { + for (;;) { + if (cancelled) { + clear(); + return; + } + + if (!delayErrors) { + Throwable ex = errors.get(); + if (ex != null) { + clear(); + errors.tryTerminateConsumer(a); + return; + } + } + + boolean d = n.get() == 0; + SpscLinkedArrayQueue<R> q = qr.get(); + R v = q != null ? q.poll() : null; + boolean empty = v == null; + + if (d && empty) { + errors.tryTerminateConsumer(a); + return; + } + + if (empty) { + break; + } + + a.onNext(v); + } + + missed = addAndGet(-missed); + if (missed == 0) { + break; + } + } + } + + final class InnerObserver extends AtomicReference<Disposable> + implements MaybeObserver<R>, Disposable { + private static final long serialVersionUID = -502562646270949838L; + + @Override + public void onSubscribe(Disposable d) { + DisposableHelper.setOnce(this, d); + } + + @Override + public void onSuccess(R value) { + innerSuccess(this, value); + } + + @Override + public void onError(Throwable e) { + innerError(this, e); + } + + @Override + public void onComplete() { + innerComplete(this); + } + + @Override + public boolean isDisposed() { + return DisposableHelper.isDisposed(get()); + } + + @Override + public void dispose() { + DisposableHelper.dispose(this); + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableFlatMapSingle.java b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableFlatMapSingle.java new file mode 100644 index 0000000000..985e4fa2d9 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableFlatMapSingle.java @@ -0,0 +1,287 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import java.util.Objects; +import java.util.concurrent.atomic.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.*; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; +import io.reactivex.rxjava3.internal.util.AtomicThrowable; +import io.reactivex.rxjava3.operators.SpscLinkedArrayQueue; + +/** + * Maps upstream values into SingleSources and merges their signals into one sequence. + * @param <T> the source value type + * @param <R> the result value type + */ +public final class ObservableFlatMapSingle<T, R> extends AbstractObservableWithUpstream<T, R> { + + final Function<? super T, ? extends SingleSource<? extends R>> mapper; + + final boolean delayErrors; + + public ObservableFlatMapSingle(ObservableSource<T> source, Function<? super T, ? extends SingleSource<? extends R>> mapper, + boolean delayError) { + super(source); + this.mapper = mapper; + this.delayErrors = delayError; + } + + @Override + protected void subscribeActual(Observer<? super R> observer) { + source.subscribe(new FlatMapSingleObserver<>(observer, mapper, delayErrors)); + } + + static final class FlatMapSingleObserver<T, R> + extends AtomicInteger + implements Observer<T>, Disposable { + + private static final long serialVersionUID = 8600231336733376951L; + + final Observer<? super R> downstream; + + final boolean delayErrors; + + final CompositeDisposable set; + + final AtomicInteger active; + + final AtomicThrowable errors; + + final Function<? super T, ? extends SingleSource<? extends R>> mapper; + + final AtomicReference<SpscLinkedArrayQueue<R>> queue; + + Disposable upstream; + + volatile boolean cancelled; + + FlatMapSingleObserver(Observer<? super R> actual, + Function<? super T, ? extends SingleSource<? extends R>> mapper, boolean delayErrors) { + this.downstream = actual; + this.mapper = mapper; + this.delayErrors = delayErrors; + this.set = new CompositeDisposable(); + this.errors = new AtomicThrowable(); + this.active = new AtomicInteger(1); + this.queue = new AtomicReference<>(); + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.validate(this.upstream, d)) { + this.upstream = d; + + downstream.onSubscribe(this); + } + } + + @Override + public void onNext(T t) { + SingleSource<? extends R> ms; + + try { + ms = Objects.requireNonNull(mapper.apply(t), "The mapper returned a null SingleSource"); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + upstream.dispose(); + onError(ex); + return; + } + + active.getAndIncrement(); + + InnerObserver inner = new InnerObserver(); + + if (!cancelled && set.add(inner)) { + ms.subscribe(inner); + } + } + + @Override + public void onError(Throwable t) { + active.decrementAndGet(); + if (errors.tryAddThrowableOrReport(t)) { + if (!delayErrors) { + set.dispose(); + } + drain(); + } + } + + @Override + public void onComplete() { + active.decrementAndGet(); + drain(); + } + + @Override + public void dispose() { + cancelled = true; + upstream.dispose(); + set.dispose(); + errors.tryTerminateAndReport(); + } + + @Override + public boolean isDisposed() { + return cancelled; + } + + void innerSuccess(InnerObserver inner, R value) { + set.delete(inner); + if (get() == 0 && compareAndSet(0, 1)) { + downstream.onNext(value); + + boolean d = active.decrementAndGet() == 0; + SpscLinkedArrayQueue<R> q = queue.get(); + + if (d && (q == null || q.isEmpty())) { + errors.tryTerminateConsumer(downstream); + return; + } + if (decrementAndGet() == 0) { + return; + } + } else { + SpscLinkedArrayQueue<R> q = getOrCreateQueue(); + synchronized (q) { + q.offer(value); + } + active.decrementAndGet(); + if (getAndIncrement() != 0) { + return; + } + } + drainLoop(); + } + + SpscLinkedArrayQueue<R> getOrCreateQueue() { + SpscLinkedArrayQueue<R> current = queue.get(); + if (current != null) { + return current; + } + current = new SpscLinkedArrayQueue<>(Observable.bufferSize()); + if (queue.compareAndSet(null, current)) { + return current; + } + return queue.get(); + } + + void innerError(InnerObserver inner, Throwable e) { + set.delete(inner); + if (errors.tryAddThrowableOrReport(e)) { + if (!delayErrors) { + upstream.dispose(); + set.dispose(); + } + active.decrementAndGet(); + drain(); + } + } + + void drain() { + if (getAndIncrement() == 0) { + drainLoop(); + } + } + + void clear() { + SpscLinkedArrayQueue<R> q = queue.get(); + if (q != null) { + q.clear(); + } + } + + void drainLoop() { + int missed = 1; + Observer<? super R> a = downstream; + AtomicInteger n = active; + AtomicReference<SpscLinkedArrayQueue<R>> qr = queue; + + for (;;) { + for (;;) { + if (cancelled) { + clear(); + return; + } + + if (!delayErrors) { + Throwable ex = errors.get(); + if (ex != null) { + clear(); + errors.tryTerminateConsumer(a); + return; + } + } + + boolean d = n.get() == 0; + SpscLinkedArrayQueue<R> q = qr.get(); + R v = q != null ? q.poll() : null; + boolean empty = v == null; + + if (d && empty) { + errors.tryTerminateConsumer(downstream); + return; + } + + if (empty) { + break; + } + + a.onNext(v); + } + + missed = addAndGet(-missed); + if (missed == 0) { + break; + } + } + } + + final class InnerObserver extends AtomicReference<Disposable> + implements SingleObserver<R>, Disposable { + private static final long serialVersionUID = -502562646270949838L; + + @Override + public void onSubscribe(Disposable d) { + DisposableHelper.setOnce(this, d); + } + + @Override + public void onSuccess(R value) { + innerSuccess(this, value); + } + + @Override + public void onError(Throwable e) { + innerError(this, e); + } + + @Override + public boolean isDisposed() { + return DisposableHelper.isDisposed(get()); + } + + @Override + public void dispose() { + DisposableHelper.dispose(this); + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableFlattenIterable.java b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableFlattenIterable.java new file mode 100644 index 0000000000..47dbde9a1e --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableFlattenIterable.java @@ -0,0 +1,148 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import java.util.Iterator; +import java.util.Objects; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +/** + * Maps a sequence into an Iterable and emits its values. + * + * @param <T> the input value type to map to Iterable + * @param <R> the element type of the Iterable and the output + */ +public final class ObservableFlattenIterable<T, R> extends AbstractObservableWithUpstream<T, R> { + + final Function<? super T, ? extends Iterable<? extends R>> mapper; + + public ObservableFlattenIterable(ObservableSource<T> source, + Function<? super T, ? extends Iterable<? extends R>> mapper) { + super(source); + this.mapper = mapper; + } + + @Override + protected void subscribeActual(Observer<? super R> observer) { + source.subscribe(new FlattenIterableObserver<>(observer, mapper)); + } + + static final class FlattenIterableObserver<T, R> implements Observer<T>, Disposable { + final Observer<? super R> downstream; + + final Function<? super T, ? extends Iterable<? extends R>> mapper; + + Disposable upstream; + + FlattenIterableObserver(Observer<? super R> actual, Function<? super T, ? extends Iterable<? extends R>> mapper) { + this.downstream = actual; + this.mapper = mapper; + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.validate(this.upstream, d)) { + this.upstream = d; + + downstream.onSubscribe(this); + } + } + + @Override + public void onNext(T value) { + if (upstream == DisposableHelper.DISPOSED) { + return; + } + + Iterator<? extends R> it; + + try { + it = mapper.apply(value).iterator(); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + upstream.dispose(); + onError(ex); + return; + } + + Observer<? super R> a = downstream; + + for (;;) { + boolean b; + + try { + b = it.hasNext(); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + upstream.dispose(); + onError(ex); + return; + } + + if (b) { + R v; + + try { + v = Objects.requireNonNull(it.next(), "The iterator returned a null value"); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + upstream.dispose(); + onError(ex); + return; + } + + a.onNext(v); + } else { + break; + } + } + } + + @Override + public void onError(Throwable e) { + if (upstream == DisposableHelper.DISPOSED) { + RxJavaPlugins.onError(e); + return; + } + upstream = DisposableHelper.DISPOSED; + downstream.onError(e); + } + + @Override + public void onComplete() { + if (upstream == DisposableHelper.DISPOSED) { + return; + } + upstream = DisposableHelper.DISPOSED; + downstream.onComplete(); + } + + @Override + public boolean isDisposed() { + return upstream.isDisposed(); + } + + @Override + public void dispose() { + upstream.dispose(); + upstream = DisposableHelper.DISPOSED; + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableFromAction.java b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableFromAction.java new file mode 100644 index 0000000000..cd0970f0d0 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableFromAction.java @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.fuseable.CancellableQueueFuseable; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +/** + * Executes an {@link Action} and signals its exception or completes normally. + * + * @param <T> the value type + * @since 3.0.0 + */ +public final class ObservableFromAction<T> extends Observable<T> implements Supplier<T> { + + final Action action; + + public ObservableFromAction(Action action) { + this.action = action; + } + + @Override + protected void subscribeActual(Observer<? super T> observer) { + CancellableQueueFuseable<T> qs = new CancellableQueueFuseable<>(); + observer.onSubscribe(qs); + + if (!qs.isDisposed()) { + + try { + action.run(); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + if (!qs.isDisposed()) { + observer.onError(ex); + } else { + RxJavaPlugins.onError(ex); + } + return; + } + + if (!qs.isDisposed()) { + observer.onComplete(); + } + } + } + + @Override + public T get() throws Throwable { + action.run(); + return null; // considered as onComplete() + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableFromArray.java b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableFromArray.java new file mode 100644 index 0000000000..f84c118346 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableFromArray.java @@ -0,0 +1,116 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import io.reactivex.rxjava3.annotations.Nullable; +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.internal.observers.BasicQueueDisposable; + +import java.util.Objects; + +public final class ObservableFromArray<T> extends Observable<T> { + final T[] array; + public ObservableFromArray(T[] array) { + this.array = array; + } + + @Override + public void subscribeActual(Observer<? super T> observer) { + FromArrayDisposable<T> d = new FromArrayDisposable<>(observer, array); + + observer.onSubscribe(d); + + if (d.fusionMode) { + return; + } + + d.run(); + } + + static final class FromArrayDisposable<T> extends BasicQueueDisposable<T> { + + final Observer<? super T> downstream; + + final T[] array; + + int index; + + boolean fusionMode; + + volatile boolean disposed; + + FromArrayDisposable(Observer<? super T> actual, T[] array) { + this.downstream = actual; + this.array = array; + } + + @Override + public int requestFusion(int mode) { + if ((mode & SYNC) != 0) { + fusionMode = true; + return SYNC; + } + return NONE; + } + + @Nullable + @Override + public T poll() { + int i = index; + T[] a = array; + if (i != a.length) { + index = i + 1; + return Objects.requireNonNull(a[i], "The array element is null"); + } + return null; + } + + @Override + public boolean isEmpty() { + return index == array.length; + } + + @Override + public void clear() { + index = array.length; + } + + @Override + public void dispose() { + disposed = true; + } + + @Override + public boolean isDisposed() { + return disposed; + } + + void run() { + T[] a = array; + int n = a.length; + + for (int i = 0; i < n && !isDisposed(); i++) { + T value = a[i]; + if (value == null) { + downstream.onError(new NullPointerException("The element at index " + i + " is null")); + return; + } + downstream.onNext(value); + } + if (!isDisposed()) { + downstream.onComplete(); + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableFromCallable.java b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableFromCallable.java new file mode 100644 index 0000000000..ab4b7aa7b8 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableFromCallable.java @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import java.util.concurrent.Callable; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.Supplier; +import io.reactivex.rxjava3.internal.observers.DeferredScalarDisposable; +import io.reactivex.rxjava3.internal.util.ExceptionHelper; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +/** + * Calls a Callable and emits its resulting single value or signals its exception. + * @param <T> the value type + */ +public final class ObservableFromCallable<T> extends Observable<T> implements Supplier<T> { + + final Callable<? extends T> callable; + + public ObservableFromCallable(Callable<? extends T> callable) { + this.callable = callable; + } + + @Override + public void subscribeActual(Observer<? super T> observer) { + DeferredScalarDisposable<T> d = new DeferredScalarDisposable<>(observer); + observer.onSubscribe(d); + if (d.isDisposed()) { + return; + } + T value; + try { + value = ExceptionHelper.nullCheck(callable.call(), "Callable returned a null value."); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + if (!d.isDisposed()) { + observer.onError(e); + } else { + RxJavaPlugins.onError(e); + } + return; + } + d.complete(value); + } + + @Override + public T get() throws Throwable { + return ExceptionHelper.nullCheck(callable.call(), "The Callable returned a null value."); + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableFromCompletable.java b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableFromCompletable.java new file mode 100644 index 0000000000..83bf51f7e0 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableFromCompletable.java @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; +import io.reactivex.rxjava3.internal.fuseable.*; + +/** + * Wrap a Completable into an Observable. + * + * @param <T> the value type + * @since 3.0.0 + */ +public final class ObservableFromCompletable<T> extends Observable<T> implements HasUpstreamCompletableSource { + + final CompletableSource source; + + public ObservableFromCompletable(CompletableSource source) { + this.source = source; + } + + @Override + public CompletableSource source() { + return source; + } + + @Override + protected void subscribeActual(Observer<? super T> observer) { + source.subscribe(new FromCompletableObserver<T>(observer)); + } + + public static final class FromCompletableObserver<T> + extends AbstractEmptyQueueFuseable<T> + implements CompletableObserver { + + final Observer<? super T> downstream; + + Disposable upstream; + + public FromCompletableObserver(Observer<? super T> downstream) { + this.downstream = downstream; + } + + @Override + public void dispose() { + upstream.dispose(); + upstream = DisposableHelper.DISPOSED; + } + + @Override + public boolean isDisposed() { + return upstream.isDisposed(); + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.validate(this.upstream, d)) { + this.upstream = d; + + downstream.onSubscribe(this); + } + } + + @Override + public void onComplete() { + upstream = DisposableHelper.DISPOSED; + downstream.onComplete(); + } + + @Override + public void onError(Throwable e) { + upstream = DisposableHelper.DISPOSED; + downstream.onError(e); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableFromFuture.java b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableFromFuture.java new file mode 100644 index 0000000000..d6f21ad133 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableFromFuture.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import java.util.concurrent.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.internal.observers.DeferredScalarDisposable; +import io.reactivex.rxjava3.internal.util.ExceptionHelper; + +public final class ObservableFromFuture<T> extends Observable<T> { + final Future<? extends T> future; + final long timeout; + final TimeUnit unit; + + public ObservableFromFuture(Future<? extends T> future, long timeout, TimeUnit unit) { + this.future = future; + this.timeout = timeout; + this.unit = unit; + } + + @Override + public void subscribeActual(Observer<? super T> observer) { + DeferredScalarDisposable<T> d = new DeferredScalarDisposable<>(observer); + observer.onSubscribe(d); + if (!d.isDisposed()) { + T v; + try { + v = ExceptionHelper.nullCheck(unit != null ? future.get(timeout, unit) : future.get(), "Future returned a null value."); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + if (!d.isDisposed()) { + observer.onError(ex); + } + return; + } + d.complete(v); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableFromIterable.java b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableFromIterable.java new file mode 100644 index 0000000000..d5f402d193 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableFromIterable.java @@ -0,0 +1,164 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import java.util.Iterator; +import java.util.Objects; + +import io.reactivex.rxjava3.annotations.Nullable; +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.internal.disposables.EmptyDisposable; +import io.reactivex.rxjava3.internal.observers.BasicQueueDisposable; + +public final class ObservableFromIterable<T> extends Observable<T> { + final Iterable<? extends T> source; + public ObservableFromIterable(Iterable<? extends T> source) { + this.source = source; + } + + @Override + public void subscribeActual(Observer<? super T> observer) { + Iterator<? extends T> it; + try { + it = source.iterator(); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + EmptyDisposable.error(e, observer); + return; + } + boolean hasNext; + try { + hasNext = it.hasNext(); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + EmptyDisposable.error(e, observer); + return; + } + if (!hasNext) { + EmptyDisposable.complete(observer); + return; + } + + FromIterableDisposable<T> d = new FromIterableDisposable<>(observer, it); + observer.onSubscribe(d); + + if (!d.fusionMode) { + d.run(); + } + } + + static final class FromIterableDisposable<T> extends BasicQueueDisposable<T> { + + final Observer<? super T> downstream; + + final Iterator<? extends T> it; + + volatile boolean disposed; + + boolean fusionMode; + + boolean done; + + boolean checkNext; + + FromIterableDisposable(Observer<? super T> actual, Iterator<? extends T> it) { + this.downstream = actual; + this.it = it; + } + + void run() { + boolean hasNext; + + do { + if (isDisposed()) { + return; + } + T v; + + try { + v = Objects.requireNonNull(it.next(), "The iterator returned a null value"); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + downstream.onError(e); + return; + } + + downstream.onNext(v); + + if (isDisposed()) { + return; + } + try { + hasNext = it.hasNext(); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + downstream.onError(e); + return; + } + } while (hasNext); + + if (!isDisposed()) { + downstream.onComplete(); + } + } + + @Override + public int requestFusion(int mode) { + if ((mode & SYNC) != 0) { + fusionMode = true; + return SYNC; + } + return NONE; + } + + @Nullable + @Override + public T poll() { + if (done) { + return null; + } + if (checkNext) { + if (!it.hasNext()) { + done = true; + return null; + } + } else { + checkNext = true; + } + + return Objects.requireNonNull(it.next(), "The iterator returned a null value"); + } + + @Override + public boolean isEmpty() { + return done; + } + + @Override + public void clear() { + done = true; + } + + @Override + public void dispose() { + disposed = true; + } + + @Override + public boolean isDisposed() { + return disposed; + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableFromPublisher.java b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableFromPublisher.java new file mode 100644 index 0000000000..ade1eeb870 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableFromPublisher.java @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.subscriptions.SubscriptionHelper; + +public final class ObservableFromPublisher<T> extends Observable<T> { + + final Publisher<? extends T> source; + + public ObservableFromPublisher(Publisher<? extends T> publisher) { + this.source = publisher; + } + + @Override + protected void subscribeActual(final Observer<? super T> o) { + source.subscribe(new PublisherSubscriber<T>(o)); + } + + static final class PublisherSubscriber<T> + implements FlowableSubscriber<T>, Disposable { + + final Observer<? super T> downstream; + Subscription upstream; + + PublisherSubscriber(Observer<? super T> o) { + this.downstream = o; + } + + @Override + public void onComplete() { + downstream.onComplete(); + } + + @Override + public void onError(Throwable t) { + downstream.onError(t); + } + + @Override + public void onNext(T t) { + downstream.onNext(t); + } + + @Override + public void onSubscribe(Subscription s) { + if (SubscriptionHelper.validate(this.upstream, s)) { + this.upstream = s; + downstream.onSubscribe(this); + s.request(Long.MAX_VALUE); + } + } + + @Override + public void dispose() { + upstream.cancel(); + upstream = SubscriptionHelper.CANCELLED; + } + + @Override + public boolean isDisposed() { + return upstream == SubscriptionHelper.CANCELLED; + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableFromRunnable.java b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableFromRunnable.java new file mode 100644 index 0000000000..eeda8e4832 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableFromRunnable.java @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.Supplier; +import io.reactivex.rxjava3.internal.fuseable.CancellableQueueFuseable; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +/** + * Executes an {@link Runnable} and signals its exception or completes normally. + * + * @param <T> the value type + * @since 3.0.0 + */ +public final class ObservableFromRunnable<T> extends Observable<T> implements Supplier<T> { + + final Runnable run; + + public ObservableFromRunnable(Runnable run) { + this.run = run; + } + + @Override + protected void subscribeActual(Observer<? super T> observer) { + CancellableQueueFuseable<T> qs = new CancellableQueueFuseable<>(); + observer.onSubscribe(qs); + + if (!qs.isDisposed()) { + + try { + run.run(); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + if (!qs.isDisposed()) { + observer.onError(ex); + } else { + RxJavaPlugins.onError(ex); + } + return; + } + + if (!qs.isDisposed()) { + observer.onComplete(); + } + } + } + + @Override + public T get() throws Throwable { + run.run(); + return null; // considered as onComplete() + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableFromSupplier.java b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableFromSupplier.java new file mode 100644 index 0000000000..7d3cd41480 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableFromSupplier.java @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.Supplier; +import io.reactivex.rxjava3.internal.observers.DeferredScalarDisposable; +import io.reactivex.rxjava3.internal.util.ExceptionHelper; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +/** + * Calls a Supplier and emits its resulting single value or signals its exception. + * @param <T> the value type + * @since 3.0.0 + */ +public final class ObservableFromSupplier<T> extends Observable<T> implements Supplier<T> { + + final Supplier<? extends T> supplier; + + public ObservableFromSupplier(Supplier<? extends T> supplier) { + this.supplier = supplier; + } + + @Override + public void subscribeActual(Observer<? super T> observer) { + DeferredScalarDisposable<T> d = new DeferredScalarDisposable<>(observer); + observer.onSubscribe(d); + if (d.isDisposed()) { + return; + } + T value; + try { + value = ExceptionHelper.nullCheck(supplier.get(), "Supplier returned a null value."); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + if (!d.isDisposed()) { + observer.onError(e); + } else { + RxJavaPlugins.onError(e); + } + return; + } + d.complete(value); + } + + @Override + public T get() throws Throwable { + return ExceptionHelper.nullCheck(supplier.get(), "The supplier returned a null value."); + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableFromUnsafeSource.java b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableFromUnsafeSource.java new file mode 100644 index 0000000000..1f7c7bcccd --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableFromUnsafeSource.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import io.reactivex.rxjava3.core.*; + +public final class ObservableFromUnsafeSource<T> extends Observable<T> { + final ObservableSource<T> source; + + public ObservableFromUnsafeSource(ObservableSource<T> source) { + this.source = source; + } + + @Override + protected void subscribeActual(Observer<? super T> observer) { + source.subscribe(observer); + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableGenerate.java b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableGenerate.java new file mode 100644 index 0000000000..fee8dfe26e --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableGenerate.java @@ -0,0 +1,175 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.disposables.EmptyDisposable; +import io.reactivex.rxjava3.internal.util.ExceptionHelper; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +public final class ObservableGenerate<T, S> extends Observable<T> { + final Supplier<S> stateSupplier; + final BiFunction<S, Emitter<T>, S> generator; + final Consumer<? super S> disposeState; + + public ObservableGenerate(Supplier<S> stateSupplier, BiFunction<S, Emitter<T>, S> generator, + Consumer<? super S> disposeState) { + this.stateSupplier = stateSupplier; + this.generator = generator; + this.disposeState = disposeState; + } + + @Override + public void subscribeActual(Observer<? super T> observer) { + S state; + + try { + state = stateSupplier.get(); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + EmptyDisposable.error(e, observer); + return; + } + + GeneratorDisposable<T, S> gd = new GeneratorDisposable<>(observer, generator, disposeState, state); + observer.onSubscribe(gd); + gd.run(); + } + + static final class GeneratorDisposable<T, S> + implements Emitter<T>, Disposable { + + final Observer<? super T> downstream; + final BiFunction<S, ? super Emitter<T>, S> generator; + final Consumer<? super S> disposeState; + + S state; + + volatile boolean cancelled; + + boolean terminate; + + boolean hasNext; + + GeneratorDisposable(Observer<? super T> actual, + BiFunction<S, ? super Emitter<T>, S> generator, + Consumer<? super S> disposeState, S initialState) { + this.downstream = actual; + this.generator = generator; + this.disposeState = disposeState; + this.state = initialState; + } + + public void run() { + S s = state; + + if (cancelled) { + state = null; + dispose(s); + return; + } + + final BiFunction<S, ? super Emitter<T>, S> f = generator; + + for (;;) { + + if (cancelled) { + state = null; + dispose(s); + return; + } + + hasNext = false; + + try { + s = f.apply(s, this); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + state = null; + cancelled = true; + onError(ex); + dispose(s); + return; + } + + if (terminate) { + cancelled = true; + state = null; + dispose(s); + return; + } + } + + } + + private void dispose(S s) { + try { + disposeState.accept(s); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + RxJavaPlugins.onError(ex); + } + } + + @Override + public void dispose() { + cancelled = true; + } + + @Override + public boolean isDisposed() { + return cancelled; + } + + @Override + public void onNext(T t) { + if (!terminate) { + if (hasNext) { + onError(new IllegalStateException("onNext already called in this generate turn")); + } else { + if (t == null) { + onError(ExceptionHelper.createNullPointerException("onNext called with a null value.")); + } else { + hasNext = true; + downstream.onNext(t); + } + } + } + } + + @Override + public void onError(Throwable t) { + if (terminate) { + RxJavaPlugins.onError(t); + } else { + if (t == null) { + t = ExceptionHelper.createNullPointerException("onError called with a null Throwable."); + } + terminate = true; + downstream.onError(t); + } + } + + @Override + public void onComplete() { + if (!terminate) { + terminate = true; + downstream.onComplete(); + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableGroupBy.java b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableGroupBy.java new file mode 100644 index 0000000000..e1af50378e --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableGroupBy.java @@ -0,0 +1,391 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.*; + +import io.reactivex.rxjava3.core.ObservableSource; +import io.reactivex.rxjava3.core.Observer; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.disposables.*; +import io.reactivex.rxjava3.observables.GroupedObservable; +import io.reactivex.rxjava3.operators.SpscLinkedArrayQueue; + +public final class ObservableGroupBy<T, K, V> extends AbstractObservableWithUpstream<T, GroupedObservable<K, V>> { + final Function<? super T, ? extends K> keySelector; + final Function<? super T, ? extends V> valueSelector; + final int bufferSize; + final boolean delayError; + + public ObservableGroupBy(ObservableSource<T> source, + Function<? super T, ? extends K> keySelector, Function<? super T, ? extends V> valueSelector, + int bufferSize, boolean delayError) { + super(source); + this.keySelector = keySelector; + this.valueSelector = valueSelector; + this.bufferSize = bufferSize; + this.delayError = delayError; + } + + @Override + public void subscribeActual(Observer<? super GroupedObservable<K, V>> t) { + source.subscribe(new GroupByObserver<>(t, keySelector, valueSelector, bufferSize, delayError)); + } + + public static final class GroupByObserver<T, K, V> extends AtomicInteger implements Observer<T>, Disposable { + + private static final long serialVersionUID = -3688291656102519502L; + + final Observer<? super GroupedObservable<K, V>> downstream; + final Function<? super T, ? extends K> keySelector; + final Function<? super T, ? extends V> valueSelector; + final int bufferSize; + final boolean delayError; + final Map<Object, GroupedUnicast<K, V>> groups; + + static final Object NULL_KEY = new Object(); + + Disposable upstream; + + final AtomicBoolean cancelled = new AtomicBoolean(); + + public GroupByObserver(Observer<? super GroupedObservable<K, V>> actual, Function<? super T, ? extends K> keySelector, Function<? super T, ? extends V> valueSelector, int bufferSize, boolean delayError) { + this.downstream = actual; + this.keySelector = keySelector; + this.valueSelector = valueSelector; + this.bufferSize = bufferSize; + this.delayError = delayError; + this.groups = new ConcurrentHashMap<>(); + this.lazySet(1); + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.validate(this.upstream, d)) { + this.upstream = d; + downstream.onSubscribe(this); + } + } + + @Override + public void onNext(T t) { + K key; + try { + key = keySelector.apply(t); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + upstream.dispose(); + onError(e); + return; + } + + Object mapKey = key != null ? key : NULL_KEY; + GroupedUnicast<K, V> group = groups.get(mapKey); + boolean newGroup = false; + if (group == null) { + // if the main has been cancelled, stop creating groups + // and skip this value + if (cancelled.get()) { + return; + } + + group = GroupedUnicast.createWith(key, bufferSize, this, delayError); + groups.put(mapKey, group); + + getAndIncrement(); + + newGroup = true; + } + + V v; + try { + v = Objects.requireNonNull(valueSelector.apply(t), "The value supplied is null"); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + upstream.dispose(); + if (newGroup) { + downstream.onNext(group); + } + onError(e); + return; + } + + group.onNext(v); + + if (newGroup) { + downstream.onNext(group); + + if (group.state.tryAbandon()) { + cancel(key); + group.onComplete(); + } + } + } + + @Override + public void onError(Throwable t) { + List<GroupedUnicast<K, V>> list = new ArrayList<>(groups.values()); + groups.clear(); + + for (GroupedUnicast<K, V> e : list) { + e.onError(t); + } + + downstream.onError(t); + } + + @Override + public void onComplete() { + List<GroupedUnicast<K, V>> list = new ArrayList<>(groups.values()); + groups.clear(); + + for (GroupedUnicast<K, V> e : list) { + e.onComplete(); + } + + downstream.onComplete(); + } + + @Override + public void dispose() { + // canceling the main source means we don't want any more groups + // but running groups still require new values + if (cancelled.compareAndSet(false, true)) { + if (decrementAndGet() == 0) { + upstream.dispose(); + } + } + } + + @Override + public boolean isDisposed() { + return cancelled.get(); + } + + public void cancel(K key) { + Object mapKey = key != null ? key : NULL_KEY; + groups.remove(mapKey); + if (decrementAndGet() == 0) { + upstream.dispose(); + } + } + } + + static final class GroupedUnicast<K, T> extends GroupedObservable<K, T> { + + final State<T, K> state; + + public static <T, K> GroupedUnicast<K, T> createWith(K key, int bufferSize, GroupByObserver<?, K, T> parent, boolean delayError) { + State<T, K> state = new State<>(bufferSize, parent, key, delayError); + return new GroupedUnicast<>(key, state); + } + + protected GroupedUnicast(K key, State<T, K> state) { + super(key); + this.state = state; + } + + @Override + protected void subscribeActual(Observer<? super T> observer) { + state.subscribe(observer); + } + + public void onNext(T t) { + state.onNext(t); + } + + public void onError(Throwable e) { + state.onError(e); + } + + public void onComplete() { + state.onComplete(); + } + } + + static final class State<T, K> extends AtomicInteger implements Disposable, ObservableSource<T> { + + private static final long serialVersionUID = -3852313036005250360L; + + final K key; + final SpscLinkedArrayQueue<T> queue; + final GroupByObserver<?, K, T> parent; + final boolean delayError; + + volatile boolean done; + Throwable error; + + final AtomicBoolean cancelled = new AtomicBoolean(); + + final AtomicReference<Observer<? super T>> actual = new AtomicReference<>(); + + final AtomicInteger once = new AtomicInteger(); + + static final int FRESH = 0; + static final int HAS_SUBSCRIBER = 1; + static final int ABANDONED = 2; + static final int ABANDONED_HAS_SUBSCRIBER = ABANDONED | HAS_SUBSCRIBER; + + State(int bufferSize, GroupByObserver<?, K, T> parent, K key, boolean delayError) { + this.queue = new SpscLinkedArrayQueue<>(bufferSize); + this.parent = parent; + this.key = key; + this.delayError = delayError; + } + + @Override + public void dispose() { + if (cancelled.compareAndSet(false, true)) { + if (getAndIncrement() == 0) { + actual.lazySet(null); + cancelParent(); + } + } + } + + @Override + public boolean isDisposed() { + return cancelled.get(); + } + + @Override + public void subscribe(Observer<? super T> observer) { + for (;;) { + int s = once.get(); + if ((s & HAS_SUBSCRIBER) != 0) { + break; + } + int u = s | HAS_SUBSCRIBER; + if (once.compareAndSet(s, u)) { + observer.onSubscribe(this); + actual.lazySet(observer); + if (cancelled.get()) { + actual.lazySet(null); + } else { + drain(); + } + return; + } + } + EmptyDisposable.error(new IllegalStateException("Only one Observer allowed!"), observer); + } + + public void onNext(T t) { + queue.offer(t); + drain(); + } + + public void onError(Throwable e) { + error = e; + done = true; + drain(); + } + + public void onComplete() { + done = true; + drain(); + } + + void drain() { + if (getAndIncrement() != 0) { + return; + } + int missed = 1; + + final SpscLinkedArrayQueue<T> q = queue; + final boolean delayError = this.delayError; + Observer<? super T> a = actual.get(); + for (;;) { + if (a != null) { + for (;;) { + boolean d = done; + T v = q.poll(); + boolean empty = v == null; + + if (checkTerminated(d, empty, a, delayError)) { + return; + } + + if (empty) { + break; + } + + a.onNext(v); + } + } + + missed = addAndGet(-missed); + if (missed == 0) { + break; + } + if (a == null) { + a = actual.get(); + } + } + } + + void cancelParent() { + if ((once.get() & ABANDONED) == 0) { + parent.cancel(key); + } + } + + boolean tryAbandon() { + return once.get() == FRESH && once.compareAndSet(FRESH, ABANDONED); + } + + boolean checkTerminated(boolean d, boolean empty, Observer<? super T> a, boolean delayError) { + if (cancelled.get()) { + queue.clear(); + actual.lazySet(null); + cancelParent(); + return true; + } + + if (d) { + if (delayError) { + if (empty) { + Throwable e = error; + actual.lazySet(null); + if (e != null) { + a.onError(e); + } else { + a.onComplete(); + } + return true; + } + } else { + Throwable e = error; + if (e != null) { + queue.clear(); + actual.lazySet(null); + a.onError(e); + return true; + } else + if (empty) { + actual.lazySet(null); + a.onComplete(); + return true; + } + } + } + + return false; + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableGroupJoin.java b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableGroupJoin.java new file mode 100644 index 0000000000..4df7fc7bd8 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableGroupJoin.java @@ -0,0 +1,477 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import java.util.*; +import java.util.concurrent.atomic.*; + +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.core.ObservableSource; +import io.reactivex.rxjava3.core.Observer; +import io.reactivex.rxjava3.disposables.*; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; +import io.reactivex.rxjava3.internal.util.ExceptionHelper; +import io.reactivex.rxjava3.operators.SpscLinkedArrayQueue; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.subjects.UnicastSubject; + +public final class ObservableGroupJoin<TLeft, TRight, TLeftEnd, TRightEnd, R> extends AbstractObservableWithUpstream<TLeft, R> { + + final ObservableSource<? extends TRight> other; + + final Function<? super TLeft, ? extends ObservableSource<TLeftEnd>> leftEnd; + + final Function<? super TRight, ? extends ObservableSource<TRightEnd>> rightEnd; + + final BiFunction<? super TLeft, ? super Observable<TRight>, ? extends R> resultSelector; + + public ObservableGroupJoin( + ObservableSource<TLeft> source, + ObservableSource<? extends TRight> other, + Function<? super TLeft, ? extends ObservableSource<TLeftEnd>> leftEnd, + Function<? super TRight, ? extends ObservableSource<TRightEnd>> rightEnd, + BiFunction<? super TLeft, ? super Observable<TRight>, ? extends R> resultSelector) { + super(source); + this.other = other; + this.leftEnd = leftEnd; + this.rightEnd = rightEnd; + this.resultSelector = resultSelector; + } + + @Override + protected void subscribeActual(Observer<? super R> observer) { + + GroupJoinDisposable<TLeft, TRight, TLeftEnd, TRightEnd, R> parent = + new GroupJoinDisposable<>(observer, leftEnd, rightEnd, resultSelector); + + observer.onSubscribe(parent); + + LeftRightObserver left = new LeftRightObserver(parent, true); + parent.disposables.add(left); + LeftRightObserver right = new LeftRightObserver(parent, false); + parent.disposables.add(right); + + source.subscribe(left); + other.subscribe(right); + } + + interface JoinSupport { + + void innerError(Throwable ex); + + void innerComplete(LeftRightObserver sender); + + void innerValue(boolean isLeft, Object o); + + void innerClose(boolean isLeft, LeftRightEndObserver index); + + void innerCloseError(Throwable ex); + } + + static final class GroupJoinDisposable<TLeft, TRight, TLeftEnd, TRightEnd, R> + extends AtomicInteger implements Disposable, JoinSupport { + + private static final long serialVersionUID = -6071216598687999801L; + + final Observer<? super R> downstream; + + final SpscLinkedArrayQueue<Object> queue; + + final CompositeDisposable disposables; + + final Map<Integer, UnicastSubject<TRight>> lefts; + + final Map<Integer, TRight> rights; + + final AtomicReference<Throwable> error; + + final Function<? super TLeft, ? extends ObservableSource<TLeftEnd>> leftEnd; + + final Function<? super TRight, ? extends ObservableSource<TRightEnd>> rightEnd; + + final BiFunction<? super TLeft, ? super Observable<TRight>, ? extends R> resultSelector; + + final AtomicInteger active; + + int leftIndex; + + int rightIndex; + + volatile boolean cancelled; + + static final Integer LEFT_VALUE = 1; + + static final Integer RIGHT_VALUE = 2; + + static final Integer LEFT_CLOSE = 3; + + static final Integer RIGHT_CLOSE = 4; + + GroupJoinDisposable( + Observer<? super R> actual, + Function<? super TLeft, ? extends ObservableSource<TLeftEnd>> leftEnd, + Function<? super TRight, ? extends ObservableSource<TRightEnd>> rightEnd, + BiFunction<? super TLeft, ? super Observable<TRight>, ? extends R> resultSelector) { + this.downstream = actual; + this.disposables = new CompositeDisposable(); + this.queue = new SpscLinkedArrayQueue<>(bufferSize()); + this.lefts = new LinkedHashMap<>(); + this.rights = new LinkedHashMap<>(); + this.error = new AtomicReference<>(); + this.leftEnd = leftEnd; + this.rightEnd = rightEnd; + this.resultSelector = resultSelector; + this.active = new AtomicInteger(2); + } + + @Override + public void dispose() { + if (cancelled) { + return; + } + cancelled = true; + cancelAll(); + if (getAndIncrement() == 0) { + queue.clear(); + } + } + + @Override + public boolean isDisposed() { + return cancelled; + } + + void cancelAll() { + disposables.dispose(); + } + + void errorAll(Observer<?> a) { + Throwable ex = ExceptionHelper.terminate(error); + + for (UnicastSubject<TRight> up : lefts.values()) { + up.onError(ex); + } + + lefts.clear(); + rights.clear(); + + a.onError(ex); + } + + void fail(Throwable exc, Observer<?> a, SpscLinkedArrayQueue<?> q) { + Exceptions.throwIfFatal(exc); + ExceptionHelper.addThrowable(error, exc); + q.clear(); + cancelAll(); + errorAll(a); + } + + void drain() { + if (getAndIncrement() != 0) { + return; + } + + int missed = 1; + SpscLinkedArrayQueue<Object> q = queue; + Observer<? super R> a = downstream; + + for (;;) { + for (;;) { + if (cancelled) { + q.clear(); + return; + } + + Throwable ex = error.get(); + if (ex != null) { + q.clear(); + cancelAll(); + errorAll(a); + return; + } + + boolean d = active.get() == 0; + + Integer mode = (Integer)q.poll(); + + boolean empty = mode == null; + + if (d && empty) { + for (UnicastSubject<?> up : lefts.values()) { + up.onComplete(); + } + + lefts.clear(); + rights.clear(); + disposables.dispose(); + + a.onComplete(); + return; + } + + if (empty) { + break; + } + + Object val = q.poll(); + + if (mode == LEFT_VALUE) { + @SuppressWarnings("unchecked") + TLeft left = (TLeft)val; + + UnicastSubject<TRight> up = UnicastSubject.create(); + int idx = leftIndex++; + lefts.put(idx, up); + + ObservableSource<TLeftEnd> p; + + try { + p = Objects.requireNonNull(leftEnd.apply(left), "The leftEnd returned a null ObservableSource"); + } catch (Throwable exc) { + fail(exc, a, q); + return; + } + + LeftRightEndObserver end = new LeftRightEndObserver(this, true, idx); + disposables.add(end); + + p.subscribe(end); + + ex = error.get(); + if (ex != null) { + q.clear(); + cancelAll(); + errorAll(a); + return; + } + + R w; + + try { + w = Objects.requireNonNull(resultSelector.apply(left, up), "The resultSelector returned a null value"); + } catch (Throwable exc) { + fail(exc, a, q); + return; + } + + a.onNext(w); + + for (TRight right : rights.values()) { + up.onNext(right); + } + } + else if (mode == RIGHT_VALUE) { + @SuppressWarnings("unchecked") + TRight right = (TRight)val; + + int idx = rightIndex++; + + rights.put(idx, right); + + ObservableSource<TRightEnd> p; + + try { + p = Objects.requireNonNull(rightEnd.apply(right), "The rightEnd returned a null ObservableSource"); + } catch (Throwable exc) { + fail(exc, a, q); + return; + } + + LeftRightEndObserver end = new LeftRightEndObserver(this, false, idx); + disposables.add(end); + + p.subscribe(end); + + ex = error.get(); + if (ex != null) { + q.clear(); + cancelAll(); + errorAll(a); + return; + } + + for (UnicastSubject<TRight> up : lefts.values()) { + up.onNext(right); + } + } + else if (mode == LEFT_CLOSE) { + LeftRightEndObserver end = (LeftRightEndObserver)val; + + UnicastSubject<TRight> up = lefts.remove(end.index); + disposables.remove(end); + if (up != null) { + up.onComplete(); + } + } + else { + LeftRightEndObserver end = (LeftRightEndObserver)val; + + rights.remove(end.index); + disposables.remove(end); + } + } + + missed = addAndGet(-missed); + if (missed == 0) { + break; + } + } + } + + @Override + public void innerError(Throwable ex) { + if (ExceptionHelper.addThrowable(error, ex)) { + active.decrementAndGet(); + drain(); + } else { + RxJavaPlugins.onError(ex); + } + } + + @Override + public void innerComplete(LeftRightObserver sender) { + disposables.delete(sender); + active.decrementAndGet(); + drain(); + } + + @Override + public void innerValue(boolean isLeft, Object o) { + synchronized (this) { + queue.offer(isLeft ? LEFT_VALUE : RIGHT_VALUE, o); + } + drain(); + } + + @Override + public void innerClose(boolean isLeft, LeftRightEndObserver index) { + synchronized (this) { + queue.offer(isLeft ? LEFT_CLOSE : RIGHT_CLOSE, index); + } + drain(); + } + + @Override + public void innerCloseError(Throwable ex) { + if (ExceptionHelper.addThrowable(error, ex)) { + drain(); + } else { + RxJavaPlugins.onError(ex); + } + } + } + + static final class LeftRightObserver + extends AtomicReference<Disposable> + implements Observer<Object>, Disposable { + + private static final long serialVersionUID = 1883890389173668373L; + + final JoinSupport parent; + + final boolean isLeft; + + LeftRightObserver(JoinSupport parent, boolean isLeft) { + this.parent = parent; + this.isLeft = isLeft; + } + + @Override + public void dispose() { + DisposableHelper.dispose(this); + } + + @Override + public boolean isDisposed() { + return DisposableHelper.isDisposed(get()); + } + + @Override + public void onSubscribe(Disposable d) { + DisposableHelper.setOnce(this, d); + } + + @Override + public void onNext(Object t) { + parent.innerValue(isLeft, t); + } + + @Override + public void onError(Throwable t) { + parent.innerError(t); + } + + @Override + public void onComplete() { + parent.innerComplete(this); + } + + } + + static final class LeftRightEndObserver + extends AtomicReference<Disposable> + implements Observer<Object>, Disposable { + + private static final long serialVersionUID = 1883890389173668373L; + + final JoinSupport parent; + + final boolean isLeft; + + final int index; + + LeftRightEndObserver(JoinSupport parent, + boolean isLeft, int index) { + this.parent = parent; + this.isLeft = isLeft; + this.index = index; + } + + @Override + public void dispose() { + DisposableHelper.dispose(this); + } + + @Override + public boolean isDisposed() { + return DisposableHelper.isDisposed(get()); + } + + @Override + public void onSubscribe(Disposable d) { + DisposableHelper.setOnce(this, d); + } + + @Override + public void onNext(Object t) { + if (DisposableHelper.dispose(this)) { + parent.innerClose(isLeft, this); + } + } + + @Override + public void onError(Throwable t) { + parent.innerCloseError(t); + } + + @Override + public void onComplete() { + parent.innerClose(isLeft, this); + } + + } + +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableHide.java b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableHide.java new file mode 100644 index 0000000000..8d6c7a7b60 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableHide.java @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; + +/** + * Hides the identity of the wrapped ObservableSource and its Disposable. + * @param <T> the value type + * + * @since 2.0 + */ +public final class ObservableHide<T> extends AbstractObservableWithUpstream<T, T> { + + public ObservableHide(ObservableSource<T> source) { + super(source); + } + + @Override + protected void subscribeActual(Observer<? super T> o) { + source.subscribe(new HideDisposable<>(o)); + } + + static final class HideDisposable<T> implements Observer<T>, Disposable { + + final Observer<? super T> downstream; + + Disposable upstream; + + HideDisposable(Observer<? super T> downstream) { + this.downstream = downstream; + } + + @Override + public void dispose() { + upstream.dispose(); + } + + @Override + public boolean isDisposed() { + return upstream.isDisposed(); + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.validate(this.upstream, d)) { + this.upstream = d; + downstream.onSubscribe(this); + } + } + + @Override + public void onNext(T t) { + downstream.onNext(t); + } + + @Override + public void onError(Throwable t) { + downstream.onError(t); + } + + @Override + public void onComplete() { + downstream.onComplete(); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableIgnoreElements.java b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableIgnoreElements.java new file mode 100644 index 0000000000..6afab8124d --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableIgnoreElements.java @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; + +public final class ObservableIgnoreElements<T> extends AbstractObservableWithUpstream<T, T> { + + public ObservableIgnoreElements(ObservableSource<T> source) { + super(source); + } + + @Override + public void subscribeActual(final Observer<? super T> t) { + source.subscribe(new IgnoreObservable<>(t)); + } + + static final class IgnoreObservable<T> implements Observer<T>, Disposable { + final Observer<? super T> downstream; + + Disposable upstream; + + IgnoreObservable(Observer<? super T> t) { + this.downstream = t; + } + + @Override + public void onSubscribe(Disposable d) { + this.upstream = d; + downstream.onSubscribe(this); + } + + @Override + public void onNext(T v) { + // deliberately ignored + } + + @Override + public void onError(Throwable e) { + downstream.onError(e); + } + + @Override + public void onComplete() { + downstream.onComplete(); + } + + @Override + public void dispose() { + upstream.dispose(); + } + + @Override + public boolean isDisposed() { + return upstream.isDisposed(); + } + } + +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableIgnoreElementsCompletable.java b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableIgnoreElementsCompletable.java new file mode 100644 index 0000000000..10e4880b55 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableIgnoreElementsCompletable.java @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.fuseable.FuseToObservable; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +public final class ObservableIgnoreElementsCompletable<T> extends Completable implements FuseToObservable<T> { + + final ObservableSource<T> source; + + public ObservableIgnoreElementsCompletable(ObservableSource<T> source) { + this.source = source; + } + + @Override + public void subscribeActual(final CompletableObserver t) { + source.subscribe(new IgnoreObservable<>(t)); + } + + @Override + public Observable<T> fuseToObservable() { + return RxJavaPlugins.onAssembly(new ObservableIgnoreElements<>(source)); + } + + static final class IgnoreObservable<T> implements Observer<T>, Disposable { + final CompletableObserver downstream; + + Disposable upstream; + + IgnoreObservable(CompletableObserver t) { + this.downstream = t; + } + + @Override + public void onSubscribe(Disposable d) { + this.upstream = d; + downstream.onSubscribe(this); + } + + @Override + public void onNext(T v) { + // deliberately ignored + } + + @Override + public void onError(Throwable e) { + downstream.onError(e); + } + + @Override + public void onComplete() { + downstream.onComplete(); + } + + @Override + public void dispose() { + upstream.dispose(); + } + + @Override + public boolean isDisposed() { + return upstream.isDisposed(); + } + } + +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableInternalHelper.java b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableInternalHelper.java new file mode 100644 index 0000000000..89822c3e9b --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableInternalHelper.java @@ -0,0 +1,294 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import java.util.Objects; +import java.util.concurrent.TimeUnit; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.functions.*; +import io.reactivex.rxjava3.observables.ConnectableObservable; + +/** + * Helper utility class to support Observable with inner classes. + */ +public final class ObservableInternalHelper { + + private ObservableInternalHelper() { + throw new IllegalStateException("No instances!"); + } + + static final class SimpleGenerator<T, S> implements BiFunction<S, Emitter<T>, S> { + final Consumer<Emitter<T>> consumer; + + SimpleGenerator(Consumer<Emitter<T>> consumer) { + this.consumer = consumer; + } + + @Override + public S apply(S t1, Emitter<T> t2) throws Throwable { + consumer.accept(t2); + return t1; + } + } + + public static <T, S> BiFunction<S, Emitter<T>, S> simpleGenerator(Consumer<Emitter<T>> consumer) { + return new SimpleGenerator<>(consumer); + } + + static final class SimpleBiGenerator<T, S> implements BiFunction<S, Emitter<T>, S> { + final BiConsumer<S, Emitter<T>> consumer; + + SimpleBiGenerator(BiConsumer<S, Emitter<T>> consumer) { + this.consumer = consumer; + } + + @Override + public S apply(S t1, Emitter<T> t2) throws Throwable { + consumer.accept(t1, t2); + return t1; + } + } + + public static <T, S> BiFunction<S, Emitter<T>, S> simpleBiGenerator(BiConsumer<S, Emitter<T>> consumer) { + return new SimpleBiGenerator<>(consumer); + } + + static final class ItemDelayFunction<T, U> implements Function<T, ObservableSource<T>> { + final Function<? super T, ? extends ObservableSource<U>> itemDelay; + + ItemDelayFunction(Function<? super T, ? extends ObservableSource<U>> itemDelay) { + this.itemDelay = itemDelay; + } + + @Override + public ObservableSource<T> apply(final T v) throws Throwable { + ObservableSource<U> o = Objects.requireNonNull(itemDelay.apply(v), "The itemDelay returned a null ObservableSource"); + return new ObservableTake<>(o, 1).map(Functions.justFunction(v)).defaultIfEmpty(v); + } + } + + public static <T, U> Function<T, ObservableSource<T>> itemDelay(final Function<? super T, ? extends ObservableSource<U>> itemDelay) { + return new ItemDelayFunction<>(itemDelay); + } + + static final class ObserverOnNext<T> implements Consumer<T> { + final Observer<T> observer; + + ObserverOnNext(Observer<T> observer) { + this.observer = observer; + } + + @Override + public void accept(T v) { + observer.onNext(v); + } + } + + static final class ObserverOnError<T> implements Consumer<Throwable> { + final Observer<T> observer; + + ObserverOnError(Observer<T> observer) { + this.observer = observer; + } + + @Override + public void accept(Throwable v) { + observer.onError(v); + } + } + + static final class ObserverOnComplete<T> implements Action { + final Observer<T> observer; + + ObserverOnComplete(Observer<T> observer) { + this.observer = observer; + } + + @Override + public void run() { + observer.onComplete(); + } + } + + public static <T> Consumer<T> observerOnNext(Observer<T> observer) { + return new ObserverOnNext<>(observer); + } + + public static <T> Consumer<Throwable> observerOnError(Observer<T> observer) { + return new ObserverOnError<>(observer); + } + + public static <T> Action observerOnComplete(Observer<T> observer) { + return new ObserverOnComplete<>(observer); + } + + static final class FlatMapWithCombinerInner<U, R, T> implements Function<U, R> { + private final BiFunction<? super T, ? super U, ? extends R> combiner; + private final T t; + + FlatMapWithCombinerInner(BiFunction<? super T, ? super U, ? extends R> combiner, T t) { + this.combiner = combiner; + this.t = t; + } + + @Override + public R apply(U w) throws Throwable { + return combiner.apply(t, w); + } + } + + static final class FlatMapWithCombinerOuter<T, R, U> implements Function<T, ObservableSource<R>> { + private final BiFunction<? super T, ? super U, ? extends R> combiner; + private final Function<? super T, ? extends ObservableSource<? extends U>> mapper; + + FlatMapWithCombinerOuter(BiFunction<? super T, ? super U, ? extends R> combiner, + Function<? super T, ? extends ObservableSource<? extends U>> mapper) { + this.combiner = combiner; + this.mapper = mapper; + } + + @Override + public ObservableSource<R> apply(final T t) throws Throwable { + @SuppressWarnings("unchecked") + ObservableSource<U> u = (ObservableSource<U>)Objects.requireNonNull(mapper.apply(t), "The mapper returned a null ObservableSource"); + return new ObservableMap<>(u, new FlatMapWithCombinerInner<U, R, T>(combiner, t)); + } + } + + public static <T, U, R> Function<T, ObservableSource<R>> flatMapWithCombiner( + final Function<? super T, ? extends ObservableSource<? extends U>> mapper, + final BiFunction<? super T, ? super U, ? extends R> combiner) { + return new FlatMapWithCombinerOuter<>(combiner, mapper); + } + + static final class FlatMapIntoIterable<T, U> implements Function<T, ObservableSource<U>> { + private final Function<? super T, ? extends Iterable<? extends U>> mapper; + + FlatMapIntoIterable(Function<? super T, ? extends Iterable<? extends U>> mapper) { + this.mapper = mapper; + } + + @Override + public ObservableSource<U> apply(T t) throws Throwable { + return new ObservableFromIterable<>(Objects.requireNonNull(mapper.apply(t), "The mapper returned a null Iterable")); + } + } + + public static <T, U> Function<T, ObservableSource<U>> flatMapIntoIterable(final Function<? super T, ? extends Iterable<? extends U>> mapper) { + return new FlatMapIntoIterable<>(mapper); + } + + enum MapToInt implements Function<Object, Object> { + INSTANCE; + @Override + public Object apply(Object t) { + return 0; + } + } + + public static <T> Supplier<ConnectableObservable<T>> replaySupplier(final Observable<T> parent) { + return new ReplaySupplier<>(parent); + } + + public static <T> Supplier<ConnectableObservable<T>> replaySupplier(final Observable<T> parent, final int bufferSize, boolean eagerTruncate) { + return new BufferedReplaySupplier<>(parent, bufferSize, eagerTruncate); + } + + public static <T> Supplier<ConnectableObservable<T>> replaySupplier(final Observable<T> parent, final int bufferSize, final long time, final TimeUnit unit, final Scheduler scheduler, boolean eagerTruncate) { + return new BufferedTimedReplaySupplier<>(parent, bufferSize, time, unit, scheduler, eagerTruncate); + } + + public static <T> Supplier<ConnectableObservable<T>> replaySupplier(final Observable<T> parent, final long time, final TimeUnit unit, final Scheduler scheduler, boolean eagerTruncate) { + return new TimedReplayCallable<>(parent, time, unit, scheduler, eagerTruncate); + } + + static final class ReplaySupplier<T> implements Supplier<ConnectableObservable<T>> { + private final Observable<T> parent; + + ReplaySupplier(Observable<T> parent) { + this.parent = parent; + } + + @Override + public ConnectableObservable<T> get() { + return parent.replay(); + } + } + + static final class BufferedReplaySupplier<T> implements Supplier<ConnectableObservable<T>> { + final Observable<T> parent; + final int bufferSize; + + final boolean eagerTruncate; + + BufferedReplaySupplier(Observable<T> parent, int bufferSize, boolean eagerTruncate) { + this.parent = parent; + this.bufferSize = bufferSize; + this.eagerTruncate = eagerTruncate; + } + + @Override + public ConnectableObservable<T> get() { + return parent.replay(bufferSize, eagerTruncate); + } + } + + static final class BufferedTimedReplaySupplier<T> implements Supplier<ConnectableObservable<T>> { + final Observable<T> parent; + final int bufferSize; + final long time; + final TimeUnit unit; + final Scheduler scheduler; + + final boolean eagerTruncate; + + BufferedTimedReplaySupplier(Observable<T> parent, int bufferSize, long time, TimeUnit unit, Scheduler scheduler, boolean eagerTruncate) { + this.parent = parent; + this.bufferSize = bufferSize; + this.time = time; + this.unit = unit; + this.scheduler = scheduler; + this.eagerTruncate = eagerTruncate; + } + + @Override + public ConnectableObservable<T> get() { + return parent.replay(bufferSize, time, unit, scheduler, eagerTruncate); + } + } + + static final class TimedReplayCallable<T> implements Supplier<ConnectableObservable<T>> { + final Observable<T> parent; + final long time; + final TimeUnit unit; + final Scheduler scheduler; + + final boolean eagerTruncate; + + TimedReplayCallable(Observable<T> parent, long time, TimeUnit unit, Scheduler scheduler, boolean eagerTruncate) { + this.parent = parent; + this.time = time; + this.unit = unit; + this.scheduler = scheduler; + this.eagerTruncate = eagerTruncate; + } + + @Override + public ConnectableObservable<T> get() { + return parent.replay(time, unit, scheduler, eagerTruncate); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableInterval.java b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableInterval.java new file mode 100644 index 0000000000..c61645bdd9 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableInterval.java @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.core.Scheduler.Worker; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; +import io.reactivex.rxjava3.internal.schedulers.TrampolineScheduler; + +public final class ObservableInterval extends Observable<Long> { + final Scheduler scheduler; + final long initialDelay; + final long period; + final TimeUnit unit; + + public ObservableInterval(long initialDelay, long period, TimeUnit unit, Scheduler scheduler) { + this.initialDelay = initialDelay; + this.period = period; + this.unit = unit; + this.scheduler = scheduler; + } + + @Override + public void subscribeActual(Observer<? super Long> observer) { + IntervalObserver is = new IntervalObserver(observer); + observer.onSubscribe(is); + + Scheduler sch = scheduler; + + if (sch instanceof TrampolineScheduler) { + Worker worker = sch.createWorker(); + is.setResource(worker); + worker.schedulePeriodically(is, initialDelay, period, unit); + } else { + Disposable d = sch.schedulePeriodicallyDirect(is, initialDelay, period, unit); + is.setResource(d); + } + } + + static final class IntervalObserver + extends AtomicReference<Disposable> + implements Disposable, Runnable { + + private static final long serialVersionUID = 346773832286157679L; + + final Observer<? super Long> downstream; + + long count; + + IntervalObserver(Observer<? super Long> downstream) { + this.downstream = downstream; + } + + @Override + public void dispose() { + DisposableHelper.dispose(this); + } + + @Override + public boolean isDisposed() { + return get() == DisposableHelper.DISPOSED; + } + + @Override + public void run() { + if (get() != DisposableHelper.DISPOSED) { + downstream.onNext(count++); + } + } + + public void setResource(Disposable d) { + DisposableHelper.setOnce(this, d); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableIntervalRange.java b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableIntervalRange.java new file mode 100644 index 0000000000..68d303b6ab --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableIntervalRange.java @@ -0,0 +1,109 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.core.Scheduler.Worker; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; +import io.reactivex.rxjava3.internal.schedulers.TrampolineScheduler; + +public final class ObservableIntervalRange extends Observable<Long> { + final Scheduler scheduler; + final long start; + final long end; + final long initialDelay; + final long period; + final TimeUnit unit; + + public ObservableIntervalRange(long start, long end, long initialDelay, long period, TimeUnit unit, Scheduler scheduler) { + this.initialDelay = initialDelay; + this.period = period; + this.unit = unit; + this.scheduler = scheduler; + this.start = start; + this.end = end; + } + + @Override + public void subscribeActual(Observer<? super Long> observer) { + IntervalRangeObserver is = new IntervalRangeObserver(observer, start, end); + observer.onSubscribe(is); + + Scheduler sch = scheduler; + + if (sch instanceof TrampolineScheduler) { + Worker worker = sch.createWorker(); + is.setResource(worker); + worker.schedulePeriodically(is, initialDelay, period, unit); + } else { + Disposable d = sch.schedulePeriodicallyDirect(is, initialDelay, period, unit); + is.setResource(d); + } + } + + static final class IntervalRangeObserver + extends AtomicReference<Disposable> + implements Disposable, Runnable { + + private static final long serialVersionUID = 1891866368734007884L; + + final Observer<? super Long> downstream; + final long end; + + long count; + + IntervalRangeObserver(Observer<? super Long> actual, long start, long end) { + this.downstream = actual; + this.count = start; + this.end = end; + } + + @Override + public void dispose() { + DisposableHelper.dispose(this); + } + + @Override + public boolean isDisposed() { + return get() == DisposableHelper.DISPOSED; + } + + @Override + public void run() { + if (!isDisposed()) { + long c = count; + downstream.onNext(c); + + if (c == end) { + if (!isDisposed()) { + downstream.onComplete(); + } + DisposableHelper.dispose(this); + return; + } + + count = c + 1; + + } + } + + public void setResource(Disposable d) { + DisposableHelper.setOnce(this, d); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableJoin.java b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableJoin.java new file mode 100644 index 0000000000..4a0f95695e --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableJoin.java @@ -0,0 +1,357 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import java.util.*; +import java.util.concurrent.atomic.*; + +import io.reactivex.rxjava3.core.ObservableSource; +import io.reactivex.rxjava3.core.Observer; +import io.reactivex.rxjava3.disposables.*; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.operators.observable.ObservableGroupJoin.*; +import io.reactivex.rxjava3.internal.util.ExceptionHelper; +import io.reactivex.rxjava3.operators.SpscLinkedArrayQueue; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +public final class ObservableJoin<TLeft, TRight, TLeftEnd, TRightEnd, R> extends AbstractObservableWithUpstream<TLeft, R> { + + final ObservableSource<? extends TRight> other; + + final Function<? super TLeft, ? extends ObservableSource<TLeftEnd>> leftEnd; + + final Function<? super TRight, ? extends ObservableSource<TRightEnd>> rightEnd; + + final BiFunction<? super TLeft, ? super TRight, ? extends R> resultSelector; + + public ObservableJoin( + ObservableSource<TLeft> source, + ObservableSource<? extends TRight> other, + Function<? super TLeft, ? extends ObservableSource<TLeftEnd>> leftEnd, + Function<? super TRight, ? extends ObservableSource<TRightEnd>> rightEnd, + BiFunction<? super TLeft, ? super TRight, ? extends R> resultSelector) { + super(source); + this.other = other; + this.leftEnd = leftEnd; + this.rightEnd = rightEnd; + this.resultSelector = resultSelector; + } + + @Override + protected void subscribeActual(Observer<? super R> observer) { + + JoinDisposable<TLeft, TRight, TLeftEnd, TRightEnd, R> parent = + new JoinDisposable<>( + observer, leftEnd, rightEnd, resultSelector); + + observer.onSubscribe(parent); + + LeftRightObserver left = new LeftRightObserver(parent, true); + parent.disposables.add(left); + LeftRightObserver right = new LeftRightObserver(parent, false); + parent.disposables.add(right); + + source.subscribe(left); + other.subscribe(right); + } + + static final class JoinDisposable<TLeft, TRight, TLeftEnd, TRightEnd, R> + extends AtomicInteger implements Disposable, JoinSupport { + + private static final long serialVersionUID = -6071216598687999801L; + + final Observer<? super R> downstream; + + final SpscLinkedArrayQueue<Object> queue; + + final CompositeDisposable disposables; + + final Map<Integer, TLeft> lefts; + + final Map<Integer, TRight> rights; + + final AtomicReference<Throwable> error; + + final Function<? super TLeft, ? extends ObservableSource<TLeftEnd>> leftEnd; + + final Function<? super TRight, ? extends ObservableSource<TRightEnd>> rightEnd; + + final BiFunction<? super TLeft, ? super TRight, ? extends R> resultSelector; + + final AtomicInteger active; + + int leftIndex; + + int rightIndex; + + volatile boolean cancelled; + + static final Integer LEFT_VALUE = 1; + + static final Integer RIGHT_VALUE = 2; + + static final Integer LEFT_CLOSE = 3; + + static final Integer RIGHT_CLOSE = 4; + + JoinDisposable(Observer<? super R> actual, + Function<? super TLeft, ? extends ObservableSource<TLeftEnd>> leftEnd, + Function<? super TRight, ? extends ObservableSource<TRightEnd>> rightEnd, + BiFunction<? super TLeft, ? super TRight, ? extends R> resultSelector) { + this.downstream = actual; + this.disposables = new CompositeDisposable(); + this.queue = new SpscLinkedArrayQueue<>(bufferSize()); + this.lefts = new LinkedHashMap<>(); + this.rights = new LinkedHashMap<>(); + this.error = new AtomicReference<>(); + this.leftEnd = leftEnd; + this.rightEnd = rightEnd; + this.resultSelector = resultSelector; + this.active = new AtomicInteger(2); + } + + @Override + public void dispose() { + if (!cancelled) { + cancelled = true; + cancelAll(); + if (getAndIncrement() == 0) { + queue.clear(); + } + } + } + + @Override + public boolean isDisposed() { + return cancelled; + } + + void cancelAll() { + disposables.dispose(); + } + + void errorAll(Observer<?> a) { + Throwable ex = ExceptionHelper.terminate(error); + + lefts.clear(); + rights.clear(); + + a.onError(ex); + } + + void fail(Throwable exc, Observer<?> a, SpscLinkedArrayQueue<?> q) { + Exceptions.throwIfFatal(exc); + ExceptionHelper.addThrowable(error, exc); + q.clear(); + cancelAll(); + errorAll(a); + } + + void drain() { + if (getAndIncrement() != 0) { + return; + } + + int missed = 1; + SpscLinkedArrayQueue<Object> q = queue; + Observer<? super R> a = downstream; + + for (;;) { + for (;;) { + if (cancelled) { + q.clear(); + return; + } + + Throwable ex = error.get(); + if (ex != null) { + q.clear(); + cancelAll(); + errorAll(a); + return; + } + + boolean d = active.get() == 0; + + Integer mode = (Integer)q.poll(); + + boolean empty = mode == null; + + if (d && empty) { + + lefts.clear(); + rights.clear(); + disposables.dispose(); + + a.onComplete(); + return; + } + + if (empty) { + break; + } + + Object val = q.poll(); + + if (mode == LEFT_VALUE) { + @SuppressWarnings("unchecked") + TLeft left = (TLeft)val; + + int idx = leftIndex++; + lefts.put(idx, left); + + ObservableSource<TLeftEnd> p; + + try { + p = Objects.requireNonNull(leftEnd.apply(left), "The leftEnd returned a null ObservableSource"); + } catch (Throwable exc) { + fail(exc, a, q); + return; + } + + LeftRightEndObserver end = new LeftRightEndObserver(this, true, idx); + disposables.add(end); + + p.subscribe(end); + + ex = error.get(); + if (ex != null) { + q.clear(); + cancelAll(); + errorAll(a); + return; + } + + for (TRight right : rights.values()) { + + R w; + + try { + w = Objects.requireNonNull(resultSelector.apply(left, right), "The resultSelector returned a null value"); + } catch (Throwable exc) { + fail(exc, a, q); + return; + } + + a.onNext(w); + } + } + else if (mode == RIGHT_VALUE) { + @SuppressWarnings("unchecked") + TRight right = (TRight)val; + + int idx = rightIndex++; + + rights.put(idx, right); + + ObservableSource<TRightEnd> p; + + try { + p = Objects.requireNonNull(rightEnd.apply(right), "The rightEnd returned a null ObservableSource"); + } catch (Throwable exc) { + fail(exc, a, q); + return; + } + + LeftRightEndObserver end = new LeftRightEndObserver(this, false, idx); + disposables.add(end); + + p.subscribe(end); + + ex = error.get(); + if (ex != null) { + q.clear(); + cancelAll(); + errorAll(a); + return; + } + + for (TLeft left : lefts.values()) { + + R w; + + try { + w = Objects.requireNonNull(resultSelector.apply(left, right), "The resultSelector returned a null value"); + } catch (Throwable exc) { + fail(exc, a, q); + return; + } + + a.onNext(w); + } + } + else if (mode == LEFT_CLOSE) { + LeftRightEndObserver end = (LeftRightEndObserver)val; + + lefts.remove(end.index); + disposables.remove(end); + } else { + LeftRightEndObserver end = (LeftRightEndObserver)val; + + rights.remove(end.index); + disposables.remove(end); + } + } + + missed = addAndGet(-missed); + if (missed == 0) { + break; + } + } + } + + @Override + public void innerError(Throwable ex) { + if (ExceptionHelper.addThrowable(error, ex)) { + active.decrementAndGet(); + drain(); + } else { + RxJavaPlugins.onError(ex); + } + } + + @Override + public void innerComplete(LeftRightObserver sender) { + disposables.delete(sender); + active.decrementAndGet(); + drain(); + } + + @Override + public void innerValue(boolean isLeft, Object o) { + synchronized (this) { + queue.offer(isLeft ? LEFT_VALUE : RIGHT_VALUE, o); + } + drain(); + } + + @Override + public void innerClose(boolean isLeft, LeftRightEndObserver index) { + synchronized (this) { + queue.offer(isLeft ? LEFT_CLOSE : RIGHT_CLOSE, index); + } + drain(); + } + + @Override + public void innerCloseError(Throwable ex) { + if (ExceptionHelper.addThrowable(error, ex)) { + drain(); + } else { + RxJavaPlugins.onError(ex); + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableJust.java b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableJust.java new file mode 100644 index 0000000000..af01eb09bb --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableJust.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.internal.operators.observable.ObservableScalarXMap.ScalarDisposable; +import io.reactivex.rxjava3.operators.ScalarSupplier; + +/** + * Represents a constant scalar value. + * @param <T> the value type + */ +public final class ObservableJust<T> extends Observable<T> implements ScalarSupplier<T> { + + private final T value; + public ObservableJust(final T value) { + this.value = value; + } + + @Override + protected void subscribeActual(Observer<? super T> observer) { + ScalarDisposable<T> sd = new ScalarDisposable<>(observer, value); + observer.onSubscribe(sd); + sd.run(); + } + + @Override + public T get() { + return value; + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableLastMaybe.java b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableLastMaybe.java new file mode 100644 index 0000000000..b20119d430 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableLastMaybe.java @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; + +/** + * Consumes the source ObservableSource and emits its last item, the defaultItem + * if empty or a NoSuchElementException if even the defaultItem is null. + * + * @param <T> the value type + */ +public final class ObservableLastMaybe<T> extends Maybe<T> { + + final ObservableSource<T> source; + + public ObservableLastMaybe(ObservableSource<T> source) { + this.source = source; + } + + // TODO fuse back to Observable + + @Override + protected void subscribeActual(MaybeObserver<? super T> observer) { + source.subscribe(new LastObserver<>(observer)); + } + + static final class LastObserver<T> implements Observer<T>, Disposable { + + final MaybeObserver<? super T> downstream; + + Disposable upstream; + + T item; + + LastObserver(MaybeObserver<? super T> downstream) { + this.downstream = downstream; + } + + @Override + public void dispose() { + upstream.dispose(); + upstream = DisposableHelper.DISPOSED; + } + + @Override + public boolean isDisposed() { + return upstream == DisposableHelper.DISPOSED; + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.validate(this.upstream, d)) { + this.upstream = d; + + downstream.onSubscribe(this); + } + } + + @Override + public void onNext(T t) { + item = t; + } + + @Override + public void onError(Throwable t) { + upstream = DisposableHelper.DISPOSED; + item = null; + downstream.onError(t); + } + + @Override + public void onComplete() { + upstream = DisposableHelper.DISPOSED; + T v = item; + if (v != null) { + item = null; + downstream.onSuccess(v); + } else { + downstream.onComplete(); + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableLastSingle.java b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableLastSingle.java new file mode 100644 index 0000000000..049e9341c1 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableLastSingle.java @@ -0,0 +1,110 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import java.util.NoSuchElementException; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; + +/** + * Consumes the source ObservableSource and emits its last item, the defaultItem + * if empty or a NoSuchElementException if even the defaultItem is null. + * + * @param <T> the value type + */ +public final class ObservableLastSingle<T> extends Single<T> { + + final ObservableSource<T> source; + + final T defaultItem; + + public ObservableLastSingle(ObservableSource<T> source, T defaultItem) { + this.source = source; + this.defaultItem = defaultItem; + } + + // TODO fuse back to Observable + + @Override + protected void subscribeActual(SingleObserver<? super T> observer) { + source.subscribe(new LastObserver<>(observer, defaultItem)); + } + + static final class LastObserver<T> implements Observer<T>, Disposable { + + final SingleObserver<? super T> downstream; + + final T defaultItem; + + Disposable upstream; + + T item; + + LastObserver(SingleObserver<? super T> actual, T defaultItem) { + this.downstream = actual; + this.defaultItem = defaultItem; + } + + @Override + public void dispose() { + upstream.dispose(); + upstream = DisposableHelper.DISPOSED; + } + + @Override + public boolean isDisposed() { + return upstream == DisposableHelper.DISPOSED; + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.validate(this.upstream, d)) { + this.upstream = d; + + downstream.onSubscribe(this); + } + } + + @Override + public void onNext(T t) { + item = t; + } + + @Override + public void onError(Throwable t) { + upstream = DisposableHelper.DISPOSED; + item = null; + downstream.onError(t); + } + + @Override + public void onComplete() { + upstream = DisposableHelper.DISPOSED; + T v = item; + if (v != null) { + item = null; + downstream.onSuccess(v); + } else { + v = defaultItem; + if (v != null) { + downstream.onSuccess(v); + } else { + downstream.onError(new NoSuchElementException()); + } + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableLift.java b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableLift.java new file mode 100644 index 0000000000..67dd7a0649 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableLift.java @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +import java.util.Objects; + +/** + * Allows lifting operators into a chain of Observables. + * + * <p>By having a concrete ObservableSource as lift, operator fusing can now identify + * both the source and the operation inside it via casting, unlike the lambda version of this. + * + * @param <T> the upstream value type + * @param <R> the downstream parameter type + */ +public final class ObservableLift<R, T> extends AbstractObservableWithUpstream<T, R> { + /** The actual operator. */ + final ObservableOperator<? extends R, ? super T> operator; + + public ObservableLift(ObservableSource<T> source, ObservableOperator<? extends R, ? super T> operator) { + super(source); + this.operator = operator; + } + + @Override + public void subscribeActual(Observer<? super R> observer) { + Observer<? super T> liftedObserver; + try { + liftedObserver = Objects.requireNonNull(operator.apply(observer), "Operator " + operator + " returned a null Observer"); + } catch (NullPointerException e) { // NOPMD + throw e; + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + // can't call onError because no way to know if a Disposable has been set or not + // can't call onSubscribe because the call might have set a Disposable already + RxJavaPlugins.onError(e); + + NullPointerException npe = new NullPointerException("Actually not, but can't throw other exceptions due to RS"); + npe.initCause(e); + throw npe; + } + + source.subscribe(liftedObserver); + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableMap.java b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableMap.java new file mode 100644 index 0000000000..8f4502ba19 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableMap.java @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import io.reactivex.rxjava3.annotations.Nullable; +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.observers.BasicFuseableObserver; + +import java.util.Objects; + +public final class ObservableMap<T, U> extends AbstractObservableWithUpstream<T, U> { + final Function<? super T, ? extends U> function; + + public ObservableMap(ObservableSource<T> source, Function<? super T, ? extends U> function) { + super(source); + this.function = function; + } + + @Override + public void subscribeActual(Observer<? super U> t) { + source.subscribe(new MapObserver<T, U>(t, function)); + } + + static final class MapObserver<T, U> extends BasicFuseableObserver<T, U> { + final Function<? super T, ? extends U> mapper; + + MapObserver(Observer<? super U> actual, Function<? super T, ? extends U> mapper) { + super(actual); + this.mapper = mapper; + } + + @Override + public void onNext(T t) { + if (done) { + return; + } + + if (sourceMode != NONE) { + downstream.onNext(null); + return; + } + + U v; + + try { + v = Objects.requireNonNull(mapper.apply(t), "The mapper function returned a null value."); + } catch (Throwable ex) { + fail(ex); + return; + } + downstream.onNext(v); + } + + @Override + public int requestFusion(int mode) { + return transitiveBoundaryFusion(mode); + } + + @Nullable + @Override + public U poll() throws Throwable { + T t = qd.poll(); + return t != null ? Objects.requireNonNull(mapper.apply(t), "The mapper function returned a null value.") : null; + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableMapNotification.java b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableMapNotification.java new file mode 100644 index 0000000000..51883e7519 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableMapNotification.java @@ -0,0 +1,130 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; + +import java.util.Objects; + +public final class ObservableMapNotification<T, R> extends AbstractObservableWithUpstream<T, ObservableSource<? extends R>> { + + final Function<? super T, ? extends ObservableSource<? extends R>> onNextMapper; + final Function<? super Throwable, ? extends ObservableSource<? extends R>> onErrorMapper; + final Supplier<? extends ObservableSource<? extends R>> onCompleteSupplier; + + public ObservableMapNotification( + ObservableSource<T> source, + Function<? super T, ? extends ObservableSource<? extends R>> onNextMapper, + Function<? super Throwable, ? extends ObservableSource<? extends R>> onErrorMapper, + Supplier<? extends ObservableSource<? extends R>> onCompleteSupplier) { + super(source); + this.onNextMapper = onNextMapper; + this.onErrorMapper = onErrorMapper; + this.onCompleteSupplier = onCompleteSupplier; + } + + @Override + public void subscribeActual(Observer<? super ObservableSource<? extends R>> t) { + source.subscribe(new MapNotificationObserver<>(t, onNextMapper, onErrorMapper, onCompleteSupplier)); + } + + static final class MapNotificationObserver<T, R> + implements Observer<T>, Disposable { + final Observer<? super ObservableSource<? extends R>> downstream; + final Function<? super T, ? extends ObservableSource<? extends R>> onNextMapper; + final Function<? super Throwable, ? extends ObservableSource<? extends R>> onErrorMapper; + final Supplier<? extends ObservableSource<? extends R>> onCompleteSupplier; + + Disposable upstream; + + MapNotificationObserver(Observer<? super ObservableSource<? extends R>> actual, + Function<? super T, ? extends ObservableSource<? extends R>> onNextMapper, + Function<? super Throwable, ? extends ObservableSource<? extends R>> onErrorMapper, + Supplier<? extends ObservableSource<? extends R>> onCompleteSupplier) { + this.downstream = actual; + this.onNextMapper = onNextMapper; + this.onErrorMapper = onErrorMapper; + this.onCompleteSupplier = onCompleteSupplier; + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.validate(this.upstream, d)) { + this.upstream = d; + downstream.onSubscribe(this); + } + } + + @Override + public void dispose() { + upstream.dispose(); + } + + @Override + public boolean isDisposed() { + return upstream.isDisposed(); + } + + @Override + public void onNext(T t) { + ObservableSource<? extends R> p; + + try { + p = Objects.requireNonNull(onNextMapper.apply(t), "The onNext ObservableSource returned is null"); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + downstream.onError(e); + return; + } + + downstream.onNext(p); + } + + @Override + public void onError(Throwable t) { + ObservableSource<? extends R> p; + + try { + p = Objects.requireNonNull(onErrorMapper.apply(t), "The onError ObservableSource returned is null"); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + downstream.onError(new CompositeException(t, e)); + return; + } + + downstream.onNext(p); + downstream.onComplete(); + } + + @Override + public void onComplete() { + ObservableSource<? extends R> p; + + try { + p = Objects.requireNonNull(onCompleteSupplier.get(), "The onComplete ObservableSource returned is null"); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + downstream.onError(e); + return; + } + + downstream.onNext(p); + downstream.onComplete(); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableMaterialize.java b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableMaterialize.java new file mode 100644 index 0000000000..866ced7dd9 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableMaterialize.java @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; + +public final class ObservableMaterialize<T> extends AbstractObservableWithUpstream<T, Notification<T>> { + + public ObservableMaterialize(ObservableSource<T> source) { + super(source); + } + + @Override + public void subscribeActual(Observer<? super Notification<T>> t) { + source.subscribe(new MaterializeObserver<>(t)); + } + + static final class MaterializeObserver<T> implements Observer<T>, Disposable { + final Observer<? super Notification<T>> downstream; + + Disposable upstream; + + MaterializeObserver(Observer<? super Notification<T>> downstream) { + this.downstream = downstream; + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.validate(this.upstream, d)) { + this.upstream = d; + downstream.onSubscribe(this); + } + } + + @Override + public void dispose() { + upstream.dispose(); + } + + @Override + public boolean isDisposed() { + return upstream.isDisposed(); + } + + @Override + public void onNext(T t) { + downstream.onNext(Notification.createOnNext(t)); + } + + @Override + public void onError(Throwable t) { + Notification<T> v = Notification.createOnError(t); + downstream.onNext(v); + downstream.onComplete(); + } + + @Override + public void onComplete() { + Notification<T> v = Notification.createOnComplete(); + + downstream.onNext(v); + downstream.onComplete(); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableMergeWithCompletable.java b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableMergeWithCompletable.java new file mode 100644 index 0000000000..e1a79180d8 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableMergeWithCompletable.java @@ -0,0 +1,146 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import java.util.concurrent.atomic.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; +import io.reactivex.rxjava3.internal.util.*; + +/** + * Merges an Observable and a Completable by emitting the items of the Observable and waiting until + * both the Observable and Completable complete normally. + * <p>History: 2.1.10 - experimental + * @param <T> the element type of the Observable + * @since 2.2 + */ +public final class ObservableMergeWithCompletable<T> extends AbstractObservableWithUpstream<T, T> { + + final CompletableSource other; + + public ObservableMergeWithCompletable(Observable<T> source, CompletableSource other) { + super(source); + this.other = other; + } + + @Override + protected void subscribeActual(Observer<? super T> observer) { + MergeWithObserver<T> parent = new MergeWithObserver<>(observer); + observer.onSubscribe(parent); + source.subscribe(parent); + other.subscribe(parent.otherObserver); + } + + static final class MergeWithObserver<T> extends AtomicInteger + implements Observer<T>, Disposable { + + private static final long serialVersionUID = -4592979584110982903L; + + final Observer<? super T> downstream; + + final AtomicReference<Disposable> mainDisposable; + + final OtherObserver otherObserver; + + final AtomicThrowable errors; + + volatile boolean mainDone; + + volatile boolean otherDone; + + MergeWithObserver(Observer<? super T> downstream) { + this.downstream = downstream; + this.mainDisposable = new AtomicReference<>(); + this.otherObserver = new OtherObserver(this); + this.errors = new AtomicThrowable(); + } + + @Override + public void onSubscribe(Disposable d) { + DisposableHelper.setOnce(mainDisposable, d); + } + + @Override + public void onNext(T t) { + HalfSerializer.onNext(downstream, t, this, errors); + } + + @Override + public void onError(Throwable ex) { + DisposableHelper.dispose(otherObserver); + HalfSerializer.onError(downstream, ex, this, errors); + } + + @Override + public void onComplete() { + mainDone = true; + if (otherDone) { + HalfSerializer.onComplete(downstream, this, errors); + } + } + + @Override + public boolean isDisposed() { + return DisposableHelper.isDisposed(mainDisposable.get()); + } + + @Override + public void dispose() { + DisposableHelper.dispose(mainDisposable); + DisposableHelper.dispose(otherObserver); + errors.tryTerminateAndReport(); + } + + void otherError(Throwable ex) { + DisposableHelper.dispose(mainDisposable); + HalfSerializer.onError(downstream, ex, this, errors); + } + + void otherComplete() { + otherDone = true; + if (mainDone) { + HalfSerializer.onComplete(downstream, this, errors); + } + } + + static final class OtherObserver extends AtomicReference<Disposable> + implements CompletableObserver { + + private static final long serialVersionUID = -2935427570954647017L; + + final MergeWithObserver<?> parent; + + OtherObserver(MergeWithObserver<?> parent) { + this.parent = parent; + } + + @Override + public void onSubscribe(Disposable d) { + DisposableHelper.setOnce(this, d); + } + + @Override + public void onError(Throwable e) { + parent.otherError(e); + } + + @Override + public void onComplete() { + parent.otherComplete(); + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableMergeWithMaybe.java b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableMergeWithMaybe.java new file mode 100644 index 0000000000..4e940a74f5 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableMergeWithMaybe.java @@ -0,0 +1,262 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import java.util.concurrent.atomic.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; +import io.reactivex.rxjava3.internal.util.AtomicThrowable; +import io.reactivex.rxjava3.operators.SimplePlainQueue; +import io.reactivex.rxjava3.operators.SpscLinkedArrayQueue; + +/** + * Merges an Observable and a Maybe by emitting the items of the Observable and the success + * value of the Maybe and waiting until both the Observable and Maybe terminate normally. + * <p>History: 2.1.10 - experimental + * @param <T> the element type of the Observable + * @since 2.2 + */ +public final class ObservableMergeWithMaybe<T> extends AbstractObservableWithUpstream<T, T> { + + final MaybeSource<? extends T> other; + + public ObservableMergeWithMaybe(Observable<T> source, MaybeSource<? extends T> other) { + super(source); + this.other = other; + } + + @Override + protected void subscribeActual(Observer<? super T> observer) { + MergeWithObserver<T> parent = new MergeWithObserver<>(observer); + observer.onSubscribe(parent); + source.subscribe(parent); + other.subscribe(parent.otherObserver); + } + + static final class MergeWithObserver<T> extends AtomicInteger + implements Observer<T>, Disposable { + + private static final long serialVersionUID = -4592979584110982903L; + + final Observer<? super T> downstream; + + final AtomicReference<Disposable> mainDisposable; + + final OtherObserver<T> otherObserver; + + final AtomicThrowable errors; + + volatile SimplePlainQueue<T> queue; + + T singleItem; + + volatile boolean disposed; + + volatile boolean mainDone; + + volatile int otherState; + + static final int OTHER_STATE_HAS_VALUE = 1; + + static final int OTHER_STATE_CONSUMED_OR_EMPTY = 2; + + MergeWithObserver(Observer<? super T> downstream) { + this.downstream = downstream; + this.mainDisposable = new AtomicReference<>(); + this.otherObserver = new OtherObserver<>(this); + this.errors = new AtomicThrowable(); + } + + @Override + public void onSubscribe(Disposable d) { + DisposableHelper.setOnce(mainDisposable, d); + } + + @Override + public void onNext(T t) { + if (compareAndSet(0, 1)) { + downstream.onNext(t); + if (decrementAndGet() == 0) { + return; + } + } else { + SimplePlainQueue<T> q = getOrCreateQueue(); + q.offer(t); + if (getAndIncrement() != 0) { + return; + } + } + drainLoop(); + } + + @Override + public void onError(Throwable ex) { + if (errors.tryAddThrowableOrReport(ex)) { + DisposableHelper.dispose(otherObserver); + drain(); + } + } + + @Override + public void onComplete() { + mainDone = true; + drain(); + } + + @Override + public boolean isDisposed() { + return DisposableHelper.isDisposed(mainDisposable.get()); + } + + @Override + public void dispose() { + disposed = true; + DisposableHelper.dispose(mainDisposable); + DisposableHelper.dispose(otherObserver); + errors.tryTerminateAndReport(); + if (getAndIncrement() == 0) { + queue = null; + singleItem = null; + } + } + + void otherSuccess(T value) { + if (compareAndSet(0, 1)) { + downstream.onNext(value); + otherState = OTHER_STATE_CONSUMED_OR_EMPTY; + } else { + singleItem = value; + otherState = OTHER_STATE_HAS_VALUE; + if (getAndIncrement() != 0) { + return; + } + } + drainLoop(); + } + + void otherError(Throwable ex) { + if (errors.tryAddThrowableOrReport(ex)) { + DisposableHelper.dispose(mainDisposable); + drain(); + } + } + + void otherComplete() { + otherState = OTHER_STATE_CONSUMED_OR_EMPTY; + drain(); + } + + SimplePlainQueue<T> getOrCreateQueue() { + SimplePlainQueue<T> q = queue; + if (q == null) { + q = new SpscLinkedArrayQueue<>(bufferSize()); + queue = q; + } + return q; + } + + void drain() { + if (getAndIncrement() == 0) { + drainLoop(); + } + } + + void drainLoop() { + Observer<? super T> actual = this.downstream; + int missed = 1; + for (;;) { + + for (;;) { + if (disposed) { + singleItem = null; + queue = null; + return; + } + + if (errors.get() != null) { + singleItem = null; + queue = null; + errors.tryTerminateConsumer(actual); + return; + } + + int os = otherState; + if (os == OTHER_STATE_HAS_VALUE) { + T v = singleItem; + singleItem = null; + otherState = OTHER_STATE_CONSUMED_OR_EMPTY; + os = OTHER_STATE_CONSUMED_OR_EMPTY; + actual.onNext(v); + } + + boolean d = mainDone; + SimplePlainQueue<T> q = queue; + T v = q != null ? q.poll() : null; + boolean empty = v == null; + + if (d && empty && os == OTHER_STATE_CONSUMED_OR_EMPTY) { + queue = null; + actual.onComplete(); + return; + } + + if (empty) { + break; + } + + actual.onNext(v); + } + + missed = addAndGet(-missed); + if (missed == 0) { + break; + } + } + } + + static final class OtherObserver<T> extends AtomicReference<Disposable> + implements MaybeObserver<T> { + + private static final long serialVersionUID = -2935427570954647017L; + + final MergeWithObserver<T> parent; + + OtherObserver(MergeWithObserver<T> parent) { + this.parent = parent; + } + + @Override + public void onSubscribe(Disposable d) { + DisposableHelper.setOnce(this, d); + } + + @Override + public void onSuccess(T t) { + parent.otherSuccess(t); + } + + @Override + public void onError(Throwable e) { + parent.otherError(e); + } + + @Override + public void onComplete() { + parent.otherComplete(); + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableMergeWithSingle.java b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableMergeWithSingle.java new file mode 100644 index 0000000000..014df98cc2 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableMergeWithSingle.java @@ -0,0 +1,253 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import java.util.concurrent.atomic.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; +import io.reactivex.rxjava3.internal.util.AtomicThrowable; +import io.reactivex.rxjava3.operators.SimplePlainQueue; +import io.reactivex.rxjava3.operators.SpscLinkedArrayQueue; + +/** + * Merges an Observable and a Single by emitting the items of the Observable and the success + * value of the Single and waiting until both the Observable and Single terminate normally. + * <p>History: 2.1.10 - experimental + * @param <T> the element type of the Observable + * @since 2.2 + */ +public final class ObservableMergeWithSingle<T> extends AbstractObservableWithUpstream<T, T> { + + final SingleSource<? extends T> other; + + public ObservableMergeWithSingle(Observable<T> source, SingleSource<? extends T> other) { + super(source); + this.other = other; + } + + @Override + protected void subscribeActual(Observer<? super T> observer) { + MergeWithObserver<T> parent = new MergeWithObserver<>(observer); + observer.onSubscribe(parent); + source.subscribe(parent); + other.subscribe(parent.otherObserver); + } + + static final class MergeWithObserver<T> extends AtomicInteger + implements Observer<T>, Disposable { + + private static final long serialVersionUID = -4592979584110982903L; + + final Observer<? super T> downstream; + + final AtomicReference<Disposable> mainDisposable; + + final OtherObserver<T> otherObserver; + + final AtomicThrowable errors; + + volatile SimplePlainQueue<T> queue; + + T singleItem; + + volatile boolean disposed; + + volatile boolean mainDone; + + volatile int otherState; + + static final int OTHER_STATE_HAS_VALUE = 1; + + static final int OTHER_STATE_CONSUMED_OR_EMPTY = 2; + + MergeWithObserver(Observer<? super T> downstream) { + this.downstream = downstream; + this.mainDisposable = new AtomicReference<>(); + this.otherObserver = new OtherObserver<>(this); + this.errors = new AtomicThrowable(); + } + + @Override + public void onSubscribe(Disposable d) { + DisposableHelper.setOnce(mainDisposable, d); + } + + @Override + public void onNext(T t) { + if (compareAndSet(0, 1)) { + downstream.onNext(t); + if (decrementAndGet() == 0) { + return; + } + } else { + SimplePlainQueue<T> q = getOrCreateQueue(); + q.offer(t); + if (getAndIncrement() != 0) { + return; + } + } + drainLoop(); + } + + @Override + public void onError(Throwable ex) { + if (errors.tryAddThrowableOrReport(ex)) { + DisposableHelper.dispose(otherObserver); + drain(); + } + } + + @Override + public void onComplete() { + mainDone = true; + drain(); + } + + @Override + public boolean isDisposed() { + return DisposableHelper.isDisposed(mainDisposable.get()); + } + + @Override + public void dispose() { + disposed = true; + DisposableHelper.dispose(mainDisposable); + DisposableHelper.dispose(otherObserver); + errors.tryTerminateAndReport(); + if (getAndIncrement() == 0) { + queue = null; + singleItem = null; + } + } + + void otherSuccess(T value) { + if (compareAndSet(0, 1)) { + downstream.onNext(value); + otherState = OTHER_STATE_CONSUMED_OR_EMPTY; + } else { + singleItem = value; + otherState = OTHER_STATE_HAS_VALUE; + if (getAndIncrement() != 0) { + return; + } + } + drainLoop(); + } + + void otherError(Throwable ex) { + if (errors.tryAddThrowableOrReport(ex)) { + DisposableHelper.dispose(mainDisposable); + drain(); + } + } + + SimplePlainQueue<T> getOrCreateQueue() { + SimplePlainQueue<T> q = queue; + if (q == null) { + q = new SpscLinkedArrayQueue<>(bufferSize()); + queue = q; + } + return q; + } + + void drain() { + if (getAndIncrement() == 0) { + drainLoop(); + } + } + + void drainLoop() { + Observer<? super T> actual = this.downstream; + int missed = 1; + for (;;) { + + for (;;) { + if (disposed) { + singleItem = null; + queue = null; + return; + } + + if (errors.get() != null) { + singleItem = null; + queue = null; + errors.tryTerminateConsumer(actual); + return; + } + + int os = otherState; + if (os == OTHER_STATE_HAS_VALUE) { + T v = singleItem; + singleItem = null; + otherState = OTHER_STATE_CONSUMED_OR_EMPTY; + os = OTHER_STATE_CONSUMED_OR_EMPTY; + actual.onNext(v); + } + + boolean d = mainDone; + SimplePlainQueue<T> q = queue; + T v = q != null ? q.poll() : null; + boolean empty = v == null; + + if (d && empty && os == OTHER_STATE_CONSUMED_OR_EMPTY) { + queue = null; + actual.onComplete(); + return; + } + + if (empty) { + break; + } + + actual.onNext(v); + } + + missed = addAndGet(-missed); + if (missed == 0) { + break; + } + } + } + + static final class OtherObserver<T> extends AtomicReference<Disposable> + implements SingleObserver<T> { + + private static final long serialVersionUID = -2935427570954647017L; + + final MergeWithObserver<T> parent; + + OtherObserver(MergeWithObserver<T> parent) { + this.parent = parent; + } + + @Override + public void onSubscribe(Disposable d) { + DisposableHelper.setOnce(this, d); + } + + @Override + public void onSuccess(T t) { + parent.otherSuccess(t); + } + + @Override + public void onError(Throwable e) { + parent.otherError(e); + } + + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableNever.java b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableNever.java new file mode 100644 index 0000000000..1d237491cc --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableNever.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.internal.disposables.EmptyDisposable; + +public final class ObservableNever extends Observable<Object> { + public static final Observable<Object> INSTANCE = new ObservableNever(); + + private ObservableNever() { + } + + @Override + protected void subscribeActual(Observer<? super Object> o) { + o.onSubscribe(EmptyDisposable.NEVER); + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableObserveOn.java b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableObserveOn.java new file mode 100644 index 0000000000..05a0064f41 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableObserveOn.java @@ -0,0 +1,322 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import io.reactivex.rxjava3.annotations.Nullable; +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; +import io.reactivex.rxjava3.internal.observers.BasicIntQueueDisposable; +import io.reactivex.rxjava3.internal.schedulers.TrampolineScheduler; +import io.reactivex.rxjava3.operators.QueueDisposable; +import io.reactivex.rxjava3.operators.SimpleQueue; +import io.reactivex.rxjava3.operators.SpscLinkedArrayQueue; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +public final class ObservableObserveOn<T> extends AbstractObservableWithUpstream<T, T> { + final Scheduler scheduler; + final boolean delayError; + final int bufferSize; + public ObservableObserveOn(ObservableSource<T> source, Scheduler scheduler, boolean delayError, int bufferSize) { + super(source); + this.scheduler = scheduler; + this.delayError = delayError; + this.bufferSize = bufferSize; + } + + @Override + protected void subscribeActual(Observer<? super T> observer) { + if (scheduler instanceof TrampolineScheduler) { + source.subscribe(observer); + } else { + Scheduler.Worker w = scheduler.createWorker(); + + source.subscribe(new ObserveOnObserver<>(observer, w, delayError, bufferSize)); + } + } + + static final class ObserveOnObserver<T> extends BasicIntQueueDisposable<T> + implements Observer<T>, Runnable { + + private static final long serialVersionUID = 6576896619930983584L; + final Observer<? super T> downstream; + final Scheduler.Worker worker; + final boolean delayError; + final int bufferSize; + + SimpleQueue<T> queue; + + Disposable upstream; + + Throwable error; + volatile boolean done; + + volatile boolean disposed; + + int sourceMode; + + boolean outputFused; + + ObserveOnObserver(Observer<? super T> actual, Scheduler.Worker worker, boolean delayError, int bufferSize) { + this.downstream = actual; + this.worker = worker; + this.delayError = delayError; + this.bufferSize = bufferSize; + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.validate(this.upstream, d)) { + this.upstream = d; + if (d instanceof QueueDisposable) { + @SuppressWarnings("unchecked") + QueueDisposable<T> qd = (QueueDisposable<T>) d; + + int m = qd.requestFusion(QueueDisposable.ANY | QueueDisposable.BOUNDARY); + + if (m == QueueDisposable.SYNC) { + sourceMode = m; + queue = qd; + done = true; + downstream.onSubscribe(this); + schedule(); + return; + } + if (m == QueueDisposable.ASYNC) { + sourceMode = m; + queue = qd; + downstream.onSubscribe(this); + return; + } + } + + queue = new SpscLinkedArrayQueue<>(bufferSize); + + downstream.onSubscribe(this); + } + } + + @Override + public void onNext(T t) { + if (done) { + return; + } + + if (sourceMode != QueueDisposable.ASYNC) { + queue.offer(t); + } + schedule(); + } + + @Override + public void onError(Throwable t) { + if (done) { + RxJavaPlugins.onError(t); + return; + } + error = t; + done = true; + schedule(); + } + + @Override + public void onComplete() { + if (done) { + return; + } + done = true; + schedule(); + } + + @Override + public void dispose() { + if (!disposed) { + disposed = true; + upstream.dispose(); + worker.dispose(); + if (!outputFused && getAndIncrement() == 0) { + queue.clear(); + } + } + } + + @Override + public boolean isDisposed() { + return disposed; + } + + void schedule() { + if (getAndIncrement() == 0) { + worker.schedule(this); + } + } + + void drainNormal() { + int missed = 1; + + final SimpleQueue<T> q = queue; + final Observer<? super T> a = downstream; + + for (;;) { + if (checkTerminated(done, q.isEmpty(), a)) { + return; + } + + for (;;) { + boolean d = done; + T v; + + try { + v = q.poll(); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + disposed = true; + upstream.dispose(); + q.clear(); + a.onError(ex); + worker.dispose(); + return; + } + boolean empty = v == null; + + if (checkTerminated(d, empty, a)) { + return; + } + + if (empty) { + break; + } + + a.onNext(v); + } + + missed = addAndGet(-missed); + if (missed == 0) { + break; + } + } + } + + void drainFused() { + int missed = 1; + + for (;;) { + if (disposed) { + return; + } + + boolean d = done; + Throwable ex = error; + + if (!delayError && d && ex != null) { + disposed = true; + downstream.onError(error); + worker.dispose(); + return; + } + + downstream.onNext(null); + + if (d) { + disposed = true; + ex = error; + if (ex != null) { + downstream.onError(ex); + } else { + downstream.onComplete(); + } + worker.dispose(); + return; + } + + missed = addAndGet(-missed); + if (missed == 0) { + break; + } + } + } + + @Override + public void run() { + if (outputFused) { + drainFused(); + } else { + drainNormal(); + } + } + + boolean checkTerminated(boolean d, boolean empty, Observer<? super T> a) { + if (disposed) { + queue.clear(); + return true; + } + if (d) { + Throwable e = error; + if (delayError) { + if (empty) { + disposed = true; + if (e != null) { + a.onError(e); + } else { + a.onComplete(); + } + worker.dispose(); + return true; + } + } else { + if (e != null) { + disposed = true; + queue.clear(); + a.onError(e); + worker.dispose(); + return true; + } else + if (empty) { + disposed = true; + a.onComplete(); + worker.dispose(); + return true; + } + } + } + return false; + } + + @Override + public int requestFusion(int mode) { + if ((mode & ASYNC) != 0) { + outputFused = true; + return ASYNC; + } + return NONE; + } + + @Nullable + @Override + public T poll() throws Throwable { + return queue.poll(); + } + + @Override + public void clear() { + queue.clear(); + } + + @Override + public boolean isEmpty() { + return queue.isEmpty(); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableOnErrorComplete.java b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableOnErrorComplete.java new file mode 100644 index 0000000000..70266c14d3 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableOnErrorComplete.java @@ -0,0 +1,106 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.Predicate; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; + +/** + * Emits an onComplete if the source emits an onError and the predicate returns true for + * that Throwable. + * + * @param <T> the value type + * @since 3.0.0 + */ +public final class ObservableOnErrorComplete<T> extends AbstractObservableWithUpstream<T, T> { + + final Predicate<? super Throwable> predicate; + + public ObservableOnErrorComplete(ObservableSource<T> source, + Predicate<? super Throwable> predicate) { + super(source); + this.predicate = predicate; + } + + @Override + protected void subscribeActual(Observer<? super T> observer) { + source.subscribe(new OnErrorCompleteObserver<>(observer, predicate)); + } + + public static final class OnErrorCompleteObserver<T> + implements Observer<T>, Disposable { + + final Observer<? super T> downstream; + + final Predicate<? super Throwable> predicate; + + Disposable upstream; + + public OnErrorCompleteObserver(Observer<? super T> actual, Predicate<? super Throwable> predicate) { + this.downstream = actual; + this.predicate = predicate; + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.validate(this.upstream, d)) { + this.upstream = d; + + downstream.onSubscribe(this); + } + } + + @Override + public void onNext(T value) { + downstream.onNext(value); + } + + @Override + public void onError(Throwable e) { + boolean b; + + try { + b = predicate.test(e); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + downstream.onError(new CompositeException(e, ex)); + return; + } + + if (b) { + downstream.onComplete(); + } else { + downstream.onError(e); + } + } + + @Override + public void onComplete() { + downstream.onComplete(); + } + + @Override + public void dispose() { + upstream.dispose(); + } + + @Override + public boolean isDisposed() { + return upstream.isDisposed(); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableOnErrorNext.java b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableOnErrorNext.java new file mode 100644 index 0000000000..9bcc3b8dd6 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableOnErrorNext.java @@ -0,0 +1,109 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.disposables.SequentialDisposable; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +public final class ObservableOnErrorNext<T> extends AbstractObservableWithUpstream<T, T> { + final Function<? super Throwable, ? extends ObservableSource<? extends T>> nextSupplier; + + public ObservableOnErrorNext(ObservableSource<T> source, + Function<? super Throwable, ? extends ObservableSource<? extends T>> nextSupplier) { + super(source); + this.nextSupplier = nextSupplier; + } + + @Override + public void subscribeActual(Observer<? super T> t) { + OnErrorNextObserver<T> parent = new OnErrorNextObserver<>(t, nextSupplier); + t.onSubscribe(parent.arbiter); + source.subscribe(parent); + } + + static final class OnErrorNextObserver<T> implements Observer<T> { + final Observer<? super T> downstream; + final Function<? super Throwable, ? extends ObservableSource<? extends T>> nextSupplier; + final SequentialDisposable arbiter; + + boolean once; + + boolean done; + + OnErrorNextObserver(Observer<? super T> actual, Function<? super Throwable, ? extends ObservableSource<? extends T>> nextSupplier) { + this.downstream = actual; + this.nextSupplier = nextSupplier; + this.arbiter = new SequentialDisposable(); + } + + @Override + public void onSubscribe(Disposable d) { + arbiter.replace(d); + } + + @Override + public void onNext(T t) { + if (done) { + return; + } + downstream.onNext(t); + } + + @Override + public void onError(Throwable t) { + if (once) { + if (done) { + RxJavaPlugins.onError(t); + return; + } + downstream.onError(t); + return; + } + once = true; + + ObservableSource<? extends T> p; + + try { + p = nextSupplier.apply(t); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + downstream.onError(new CompositeException(t, e)); + return; + } + + if (p == null) { + NullPointerException npe = new NullPointerException("Observable is null"); + npe.initCause(t); + downstream.onError(npe); + return; + } + + p.subscribe(this); + } + + @Override + public void onComplete() { + if (done) { + return; + } + done = true; + once = true; + downstream.onComplete(); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableOnErrorReturn.java b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableOnErrorReturn.java new file mode 100644 index 0000000000..24c9af4700 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableOnErrorReturn.java @@ -0,0 +1,95 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; + +public final class ObservableOnErrorReturn<T> extends AbstractObservableWithUpstream<T, T> { + final Function<? super Throwable, ? extends T> valueSupplier; + public ObservableOnErrorReturn(ObservableSource<T> source, Function<? super Throwable, ? extends T> valueSupplier) { + super(source); + this.valueSupplier = valueSupplier; + } + + @Override + public void subscribeActual(Observer<? super T> t) { + source.subscribe(new OnErrorReturnObserver<>(t, valueSupplier)); + } + + static final class OnErrorReturnObserver<T> implements Observer<T>, Disposable { + final Observer<? super T> downstream; + final Function<? super Throwable, ? extends T> valueSupplier; + + Disposable upstream; + + OnErrorReturnObserver(Observer<? super T> actual, Function<? super Throwable, ? extends T> valueSupplier) { + this.downstream = actual; + this.valueSupplier = valueSupplier; + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.validate(this.upstream, d)) { + this.upstream = d; + downstream.onSubscribe(this); + } + } + + @Override + public void dispose() { + upstream.dispose(); + } + + @Override + public boolean isDisposed() { + return upstream.isDisposed(); + } + + @Override + public void onNext(T t) { + downstream.onNext(t); + } + + @Override + public void onError(Throwable t) { + T v; + try { + v = valueSupplier.apply(t); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + downstream.onError(new CompositeException(t, e)); + return; + } + + if (v == null) { + NullPointerException e = new NullPointerException("The supplied value is null"); + e.initCause(t); + downstream.onError(e); + return; + } + + downstream.onNext(v); + downstream.onComplete(); + } + + @Override + public void onComplete() { + downstream.onComplete(); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservablePublish.java b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservablePublish.java new file mode 100644 index 0000000000..f2d4c8f0f4 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservablePublish.java @@ -0,0 +1,290 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import java.util.concurrent.atomic.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.Consumer; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; +import io.reactivex.rxjava3.internal.fuseable.HasUpstreamObservableSource; +import io.reactivex.rxjava3.internal.util.ExceptionHelper; +import io.reactivex.rxjava3.observables.ConnectableObservable; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +/** + * Shares a single underlying connection to the upstream ObservableSource + * and multicasts events to all subscribed observers until the upstream + * completes or the connection is disposed. + * <p> + * The difference to ObservablePublish is that when the upstream terminates, + * late observers will receive that terminal event until the connection is + * disposed and the ConnectableObservable is reset to its fresh state. + * + * @param <T> the element type + * @since 2.2.10 + */ +public final class ObservablePublish<T> extends ConnectableObservable<T> +implements HasUpstreamObservableSource<T> { + + final ObservableSource<T> source; + + final AtomicReference<PublishConnection<T>> current; + + public ObservablePublish(ObservableSource<T> source) { + this.source = source; + this.current = new AtomicReference<>(); + } + + @Override + public void connect(Consumer<? super Disposable> connection) { + boolean doConnect = false; + PublishConnection<T> conn; + + for (;;) { + conn = current.get(); + + if (conn == null || conn.isDisposed()) { + PublishConnection<T> fresh = new PublishConnection<>(current); + if (!current.compareAndSet(conn, fresh)) { + continue; + } + conn = fresh; + } + + doConnect = !conn.connect.get() && conn.connect.compareAndSet(false, true); + break; + } + + try { + connection.accept(conn); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + throw ExceptionHelper.wrapOrThrow(ex); + } + + if (doConnect) { + source.subscribe(conn); + } + } + + @Override + protected void subscribeActual(Observer<? super T> observer) { + PublishConnection<T> conn; + + for (;;) { + conn = current.get(); + // we don't create a fresh connection if the current is terminated + if (conn == null) { + PublishConnection<T> fresh = new PublishConnection<>(current); + if (!current.compareAndSet(conn, fresh)) { + continue; + } + conn = fresh; + } + break; + } + + InnerDisposable<T> inner = new InnerDisposable<>(observer, conn); + observer.onSubscribe(inner); + if (conn.add(inner)) { + if (inner.isDisposed()) { + conn.remove(inner); + } + return; + } + // Late observers will be simply terminated + Throwable error = conn.error; + if (error != null) { + observer.onError(error); + } else { + observer.onComplete(); + } + } + + @Override + public void reset() { + PublishConnection<T> conn = current.get(); + if (conn != null && conn.isDisposed()) { + current.compareAndSet(conn, null); + } + } + + @Override + public ObservableSource<T> source() { + return source; + } + + static final class PublishConnection<T> + extends AtomicReference<InnerDisposable<T>[]> + implements Observer<T>, Disposable { + + private static final long serialVersionUID = -3251430252873581268L; + + final AtomicBoolean connect; + + final AtomicReference<PublishConnection<T>> current; + + final AtomicReference<Disposable> upstream; + + @SuppressWarnings("rawtypes") + static final InnerDisposable[] EMPTY = new InnerDisposable[0]; + + @SuppressWarnings("rawtypes") + static final InnerDisposable[] TERMINATED = new InnerDisposable[0]; + + Throwable error; + + @SuppressWarnings("unchecked") + PublishConnection(AtomicReference<PublishConnection<T>> current) { + this.connect = new AtomicBoolean(); + this.current = current; + this.upstream = new AtomicReference<>(); + lazySet(EMPTY); + } + + @SuppressWarnings("unchecked") + @Override + public void dispose() { + getAndSet(TERMINATED); + current.compareAndSet(this, null); + DisposableHelper.dispose(upstream); + } + + @Override + public boolean isDisposed() { + return get() == TERMINATED; + } + + @Override + public void onSubscribe(Disposable d) { + DisposableHelper.setOnce(upstream, d); + } + + @Override + public void onNext(T t) { + for (InnerDisposable<T> inner : get()) { + inner.downstream.onNext(t); + } + } + + @Override + @SuppressWarnings("unchecked") + public void onError(Throwable e) { + if (upstream.get() != DisposableHelper.DISPOSED) { + error = e; + upstream.lazySet(DisposableHelper.DISPOSED); + for (InnerDisposable<T> inner : getAndSet(TERMINATED)) { + inner.downstream.onError(e); + } + } else { + RxJavaPlugins.onError(e); + } + } + + @Override + @SuppressWarnings("unchecked") + public void onComplete() { + upstream.lazySet(DisposableHelper.DISPOSED); + for (InnerDisposable<T> inner : getAndSet(TERMINATED)) { + inner.downstream.onComplete(); + } + } + + public boolean add(InnerDisposable<T> inner) { + for (;;) { + InnerDisposable<T>[] a = get(); + if (a == TERMINATED) { + return false; + } + int n = a.length; + @SuppressWarnings("unchecked") + InnerDisposable<T>[] b = new InnerDisposable[n + 1]; + System.arraycopy(a, 0, b, 0, n); + b[n] = inner; + if (compareAndSet(a, b)) { + return true; + } + } + } + + @SuppressWarnings("unchecked") + public void remove(InnerDisposable<T> inner) { + for (;;) { + InnerDisposable<T>[] a = get(); + int n = a.length; + if (n == 0) { + return; + } + + int j = -1; + for (int i = 0; i < n; i++) { + if (a[i] == inner) { + j = i; + break; + } + } + + if (j < 0) { + return; + } + InnerDisposable<T>[] b = EMPTY; + if (n != 1) { + b = new InnerDisposable[n - 1]; + System.arraycopy(a, 0, b, 0, j); + System.arraycopy(a, j + 1, b, j, n - j - 1); + } + if (compareAndSet(a, b)) { + return; + } + } + } + } + + /** + * Intercepts the dispose signal from the downstream and + * removes itself from the connection's observers array + * at most once. + * @param <T> the element type + */ + static final class InnerDisposable<T> + extends AtomicReference<PublishConnection<T>> + implements Disposable { + + private static final long serialVersionUID = 7463222674719692880L; + + final Observer<? super T> downstream; + + InnerDisposable(Observer<? super T> downstream, PublishConnection<T> parent) { + this.downstream = downstream; + lazySet(parent); + } + + @Override + public void dispose() { + PublishConnection<T> p = getAndSet(null); + if (p != null) { + p.remove(this); + } + } + + @Override + public boolean isDisposed() { + return get() == null; + } + } +} + diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservablePublishSelector.java b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservablePublishSelector.java new file mode 100644 index 0000000000..d025a5487d --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservablePublishSelector.java @@ -0,0 +1,143 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import java.util.Objects; +import java.util.concurrent.atomic.AtomicReference; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.disposables.*; +import io.reactivex.rxjava3.subjects.PublishSubject; + +/** + * Shares a source Observable for the duration of a selector function. + * @param <T> the input value type + * @param <R> the output value type + */ +public final class ObservablePublishSelector<T, R> extends AbstractObservableWithUpstream<T, R> { + + final Function<? super Observable<T>, ? extends ObservableSource<R>> selector; + + public ObservablePublishSelector(final ObservableSource<T> source, + final Function<? super Observable<T>, ? extends ObservableSource<R>> selector) { + super(source); + this.selector = selector; + } + + @Override + protected void subscribeActual(Observer<? super R> observer) { + PublishSubject<T> subject = PublishSubject.create(); + + ObservableSource<? extends R> target; + + try { + target = Objects.requireNonNull(selector.apply(subject), "The selector returned a null ObservableSource"); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + EmptyDisposable.error(ex, observer); + return; + } + + TargetObserver<R> o = new TargetObserver<>(observer); + + target.subscribe(o); + + source.subscribe(new SourceObserver<>(subject, o)); + } + + static final class SourceObserver<T> implements Observer<T> { + + final PublishSubject<T> subject; + + final AtomicReference<Disposable> target; + + SourceObserver(PublishSubject<T> subject, AtomicReference<Disposable> target) { + this.subject = subject; + this.target = target; + } + + @Override + public void onSubscribe(Disposable d) { + DisposableHelper.setOnce(target, d); + } + + @Override + public void onNext(T value) { + subject.onNext(value); + } + + @Override + public void onError(Throwable e) { + subject.onError(e); + } + + @Override + public void onComplete() { + subject.onComplete(); + } + } + + static final class TargetObserver<R> + extends AtomicReference<Disposable> implements Observer<R>, Disposable { + private static final long serialVersionUID = 854110278590336484L; + + final Observer<? super R> downstream; + + Disposable upstream; + + TargetObserver(Observer<? super R> downstream) { + this.downstream = downstream; + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.validate(this.upstream, d)) { + this.upstream = d; + + downstream.onSubscribe(this); + } + } + + @Override + public void onNext(R value) { + downstream.onNext(value); + } + + @Override + public void onError(Throwable e) { + DisposableHelper.dispose(this); + downstream.onError(e); + } + + @Override + public void onComplete() { + DisposableHelper.dispose(this); + downstream.onComplete(); + } + + @Override + public void dispose() { + upstream.dispose(); + DisposableHelper.dispose(this); + } + + @Override + public boolean isDisposed() { + return upstream.isDisposed(); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableRange.java b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableRange.java new file mode 100644 index 0000000000..53855a46c2 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableRange.java @@ -0,0 +1,115 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import io.reactivex.rxjava3.annotations.Nullable; +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.internal.observers.BasicIntQueueDisposable; + +/** + * Emits a range of integer values from start to end. + */ +public final class ObservableRange extends Observable<Integer> { + private final int start; + private final long end; + + public ObservableRange(int start, int count) { + this.start = start; + this.end = (long)start + count; + } + + @Override + protected void subscribeActual(Observer<? super Integer> o) { + RangeDisposable parent = new RangeDisposable(o, start, end); + o.onSubscribe(parent); + parent.run(); + } + + static final class RangeDisposable + extends BasicIntQueueDisposable<Integer> { + + private static final long serialVersionUID = 396518478098735504L; + + final Observer<? super Integer> downstream; + + final long end; + + long index; + + boolean fused; + + RangeDisposable(Observer<? super Integer> actual, long start, long end) { + this.downstream = actual; + this.index = start; + this.end = end; + } + + void run() { + if (fused) { + return; + } + Observer<? super Integer> actual = this.downstream; + long e = end; + for (long i = index; i != e && get() == 0; i++) { + actual.onNext((int)i); + } + if (get() == 0) { + lazySet(1); + actual.onComplete(); + } + } + + @Nullable + @Override + public Integer poll() { + long i = index; + if (i != end) { + index = i + 1; + return (int)i; + } + lazySet(1); + return null; + } + + @Override + public boolean isEmpty() { + return index == end; + } + + @Override + public void clear() { + index = end; + lazySet(1); + } + + @Override + public void dispose() { + set(1); + } + + @Override + public boolean isDisposed() { + return get() != 0; + } + + @Override + public int requestFusion(int mode) { + if ((mode & SYNC) != 0) { + fused = true; + return SYNC; + } + return NONE; + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableRangeLong.java b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableRangeLong.java new file mode 100644 index 0000000000..39a36e72f2 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableRangeLong.java @@ -0,0 +1,112 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import io.reactivex.rxjava3.annotations.Nullable; +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.internal.observers.BasicIntQueueDisposable; + +public final class ObservableRangeLong extends Observable<Long> { + private final long start; + private final long count; + + public ObservableRangeLong(long start, long count) { + this.start = start; + this.count = count; + } + + @Override + protected void subscribeActual(Observer<? super Long> o) { + RangeDisposable parent = new RangeDisposable(o, start, start + count); + o.onSubscribe(parent); + parent.run(); + } + + static final class RangeDisposable + extends BasicIntQueueDisposable<Long> { + + private static final long serialVersionUID = 396518478098735504L; + + final Observer<? super Long> downstream; + + final long end; + + long index; + + boolean fused; + + RangeDisposable(Observer<? super Long> actual, long start, long end) { + this.downstream = actual; + this.index = start; + this.end = end; + } + + void run() { + if (fused) { + return; + } + Observer<? super Long> actual = this.downstream; + long e = end; + for (long i = index; i != e && get() == 0; i++) { + actual.onNext(i); + } + if (get() == 0) { + lazySet(1); + actual.onComplete(); + } + } + + @Nullable + @Override + public Long poll() { + long i = index; + if (i != end) { + index = i + 1; + return i; + } + lazySet(1); + return null; + } + + @Override + public boolean isEmpty() { + return index == end; + } + + @Override + public void clear() { + index = end; + lazySet(1); + } + + @Override + public void dispose() { + set(1); + } + + @Override + public boolean isDisposed() { + return get() != 0; + } + + @Override + public int requestFusion(int mode) { + if ((mode & SYNC) != 0) { + fused = true; + return SYNC; + } + return NONE; + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableReduceMaybe.java b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableReduceMaybe.java new file mode 100644 index 0000000000..3f66614ef0 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableReduceMaybe.java @@ -0,0 +1,128 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.BiFunction; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +import java.util.Objects; + +/** + * Reduce a sequence of values into a single value via an aggregator function and emit the final value or complete + * if the source is empty. + * + * @param <T> the source and result value type + */ +public final class ObservableReduceMaybe<T> extends Maybe<T> { + + final ObservableSource<T> source; + + final BiFunction<T, T, T> reducer; + + public ObservableReduceMaybe(ObservableSource<T> source, BiFunction<T, T, T> reducer) { + this.source = source; + this.reducer = reducer; + } + + @Override + protected void subscribeActual(MaybeObserver<? super T> observer) { + source.subscribe(new ReduceObserver<>(observer, reducer)); + } + + static final class ReduceObserver<T> implements Observer<T>, Disposable { + + final MaybeObserver<? super T> downstream; + + final BiFunction<T, T, T> reducer; + + boolean done; + + T value; + + Disposable upstream; + + ReduceObserver(MaybeObserver<? super T> observer, BiFunction<T, T, T> reducer) { + this.downstream = observer; + this.reducer = reducer; + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.validate(this.upstream, d)) { + this.upstream = d; + + downstream.onSubscribe(this); + } + } + + @Override + public void onNext(T value) { + if (!done) { + T v = this.value; + + if (v == null) { + this.value = value; + } else { + try { + this.value = Objects.requireNonNull(reducer.apply(v, value), "The reducer returned a null value"); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + upstream.dispose(); + onError(ex); + } + } + } + } + + @Override + public void onError(Throwable e) { + if (done) { + RxJavaPlugins.onError(e); + return; + } + done = true; + value = null; + downstream.onError(e); + } + + @Override + public void onComplete() { + if (done) { + return; + } + done = true; + T v = value; + value = null; + if (v != null) { + downstream.onSuccess(v); + } else { + downstream.onComplete(); + } + } + + @Override + public void dispose() { + upstream.dispose(); + } + + @Override + public boolean isDisposed() { + return upstream.isDisposed(); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableReduceSeedSingle.java b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableReduceSeedSingle.java new file mode 100644 index 0000000000..f2382f6f9d --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableReduceSeedSingle.java @@ -0,0 +1,120 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.BiFunction; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +import java.util.Objects; + +/** + * Reduce a sequence of values, starting from a seed value and by using + * an accumulator function and return the last accumulated value. + * + * @param <T> the source value type + * @param <R> the accumulated result type + */ +public final class ObservableReduceSeedSingle<T, R> extends Single<R> { + + final ObservableSource<T> source; + + final R seed; + + final BiFunction<R, ? super T, R> reducer; + + public ObservableReduceSeedSingle(ObservableSource<T> source, R seed, BiFunction<R, ? super T, R> reducer) { + this.source = source; + this.seed = seed; + this.reducer = reducer; + } + + @Override + protected void subscribeActual(SingleObserver<? super R> observer) { + source.subscribe(new ReduceSeedObserver<>(observer, reducer, seed)); + } + + static final class ReduceSeedObserver<T, R> implements Observer<T>, Disposable { + + final SingleObserver<? super R> downstream; + + final BiFunction<R, ? super T, R> reducer; + + R value; + + Disposable upstream; + + ReduceSeedObserver(SingleObserver<? super R> actual, BiFunction<R, ? super T, R> reducer, R value) { + this.downstream = actual; + this.value = value; + this.reducer = reducer; + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.validate(this.upstream, d)) { + this.upstream = d; + + downstream.onSubscribe(this); + } + } + + @Override + public void onNext(T value) { + R v = this.value; + if (v != null) { + try { + this.value = Objects.requireNonNull(reducer.apply(v, value), "The reducer returned a null value"); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + upstream.dispose(); + onError(ex); + } + } + } + + @Override + public void onError(Throwable e) { + R v = value; + if (v != null) { + value = null; + downstream.onError(e); + } else { + RxJavaPlugins.onError(e); + } + } + + @Override + public void onComplete() { + R v = value; + if (v != null) { + value = null; + downstream.onSuccess(v); + } + } + + @Override + public void dispose() { + upstream.dispose(); + } + + @Override + public boolean isDisposed() { + return upstream.isDisposed(); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableReduceWithSingle.java b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableReduceWithSingle.java new file mode 100644 index 0000000000..22dbaa6a1d --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableReduceWithSingle.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.disposables.EmptyDisposable; +import io.reactivex.rxjava3.internal.operators.observable.ObservableReduceSeedSingle.ReduceSeedObserver; + +import java.util.Objects; + +/** + * Reduce a sequence of values, starting from a generated seed value and by using + * an accumulator function and return the last accumulated value. + * + * @param <T> the source value type + * @param <R> the accumulated result type + */ +public final class ObservableReduceWithSingle<T, R> extends Single<R> { + + final ObservableSource<T> source; + + final Supplier<R> seedSupplier; + + final BiFunction<R, ? super T, R> reducer; + + public ObservableReduceWithSingle(ObservableSource<T> source, Supplier<R> seedSupplier, BiFunction<R, ? super T, R> reducer) { + this.source = source; + this.seedSupplier = seedSupplier; + this.reducer = reducer; + } + + @Override + protected void subscribeActual(SingleObserver<? super R> observer) { + R seed; + + try { + seed = Objects.requireNonNull(seedSupplier.get(), "The seedSupplier returned a null value"); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + EmptyDisposable.error(ex, observer); + return; + } + source.subscribe(new ReduceSeedObserver<>(observer, reducer, seed)); + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableRefCount.java b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableRefCount.java new file mode 100644 index 0000000000..1a5d9f55cc --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableRefCount.java @@ -0,0 +1,243 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.functions.Consumer; +import io.reactivex.rxjava3.internal.disposables.*; +import io.reactivex.rxjava3.observables.ConnectableObservable; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +/** + * Returns an observable sequence that stays connected to the source as long as + * there is at least one subscription to the observable sequence. + * + * @param <T> + * the value type + */ +public final class ObservableRefCount<T> extends Observable<T> { + + final ConnectableObservable<T> source; + + final int n; + + final long timeout; + + final TimeUnit unit; + + final Scheduler scheduler; + + RefConnection connection; + + public ObservableRefCount(ConnectableObservable<T> source) { + this(source, 1, 0L, TimeUnit.NANOSECONDS, null); + } + + public ObservableRefCount(ConnectableObservable<T> source, int n, long timeout, TimeUnit unit, + Scheduler scheduler) { + this.source = source; + this.n = n; + this.timeout = timeout; + this.unit = unit; + this.scheduler = scheduler; + } + + @Override + protected void subscribeActual(Observer<? super T> observer) { + + RefConnection conn; + + boolean connect = false; + synchronized (this) { + conn = connection; + if (conn == null) { + conn = new RefConnection(this); + connection = conn; + } + + long c = conn.subscriberCount; + if (c == 0L && conn.timer != null) { + conn.timer.dispose(); + } + conn.subscriberCount = c + 1; + if (!conn.connected && c + 1 == n) { + connect = true; + conn.connected = true; + } + } + + source.subscribe(new RefCountObserver<>(observer, this, conn)); + + if (connect) { + source.connect(conn); + } + } + + void cancel(RefConnection rc) { + SequentialDisposable sd; + synchronized (this) { + if (connection == null || connection != rc) { + return; + } + long c = rc.subscriberCount - 1; + rc.subscriberCount = c; + if (c != 0L || !rc.connected) { + return; + } + if (timeout == 0L) { + timeout(rc); + return; + } + sd = new SequentialDisposable(); + rc.timer = sd; + } + + sd.replace(scheduler.scheduleDirect(rc, timeout, unit)); + } + + void terminated(RefConnection rc) { + synchronized (this) { + if (connection == rc) { + if (rc.timer != null) { + rc.timer.dispose(); + rc.timer = null; + } + if (--rc.subscriberCount == 0) { + connection = null; + source.reset(); + } + } + } + } + + void timeout(RefConnection rc) { + synchronized (this) { + if (rc.subscriberCount == 0 && rc == connection) { + connection = null; + Disposable connectionObject = rc.get(); + DisposableHelper.dispose(rc); + + if (connectionObject == null) { + rc.disconnectedEarly = true; + } else { + source.reset(); + } + } + } + } + + static final class RefConnection extends AtomicReference<Disposable> + implements Runnable, Consumer<Disposable> { + + private static final long serialVersionUID = -4552101107598366241L; + + final ObservableRefCount<?> parent; + + Disposable timer; + + long subscriberCount; + + boolean connected; + + boolean disconnectedEarly; + + RefConnection(ObservableRefCount<?> parent) { + this.parent = parent; + } + + @Override + public void run() { + parent.timeout(this); + } + + @Override + public void accept(Disposable t) { + DisposableHelper.replace(this, t); + synchronized (parent) { + if (disconnectedEarly) { + parent.source.reset(); + } + } + } + } + + static final class RefCountObserver<T> + extends AtomicBoolean implements Observer<T>, Disposable { + + private static final long serialVersionUID = -7419642935409022375L; + + final Observer<? super T> downstream; + + final ObservableRefCount<T> parent; + + final RefConnection connection; + + Disposable upstream; + + RefCountObserver(Observer<? super T> downstream, ObservableRefCount<T> parent, RefConnection connection) { + this.downstream = downstream; + this.parent = parent; + this.connection = connection; + } + + @Override + public void onNext(T t) { + downstream.onNext(t); + } + + @Override + public void onError(Throwable t) { + if (compareAndSet(false, true)) { + parent.terminated(connection); + downstream.onError(t); + } else { + RxJavaPlugins.onError(t); + } + } + + @Override + public void onComplete() { + if (compareAndSet(false, true)) { + parent.terminated(connection); + downstream.onComplete(); + } + } + + @Override + public void dispose() { + upstream.dispose(); + if (compareAndSet(false, true)) { + parent.cancel(connection); + } + } + + @Override + public boolean isDisposed() { + return upstream.isDisposed(); + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.validate(upstream, d)) { + this.upstream = d; + + downstream.onSubscribe(this); + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableRepeat.java b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableRepeat.java new file mode 100644 index 0000000000..b14549f65f --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableRepeat.java @@ -0,0 +1,101 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import java.util.concurrent.atomic.AtomicInteger; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.disposables.SequentialDisposable; + +public final class ObservableRepeat<T> extends AbstractObservableWithUpstream<T, T> { + final long count; + public ObservableRepeat(Observable<T> source, long count) { + super(source); + this.count = count; + } + + @Override + public void subscribeActual(Observer<? super T> observer) { + SequentialDisposable sd = new SequentialDisposable(); + observer.onSubscribe(sd); + + RepeatObserver<T> rs = new RepeatObserver<>(observer, count != Long.MAX_VALUE ? count - 1 : Long.MAX_VALUE, sd, source); + rs.subscribeNext(); + } + + static final class RepeatObserver<T> extends AtomicInteger implements Observer<T> { + + private static final long serialVersionUID = -7098360935104053232L; + + final Observer<? super T> downstream; + final SequentialDisposable sd; + final ObservableSource<? extends T> source; + long remaining; + RepeatObserver(Observer<? super T> actual, long count, SequentialDisposable sd, ObservableSource<? extends T> source) { + this.downstream = actual; + this.sd = sd; + this.source = source; + this.remaining = count; + } + + @Override + public void onSubscribe(Disposable d) { + sd.replace(d); + } + + @Override + public void onNext(T t) { + downstream.onNext(t); + } + + @Override + public void onError(Throwable t) { + downstream.onError(t); + } + + @Override + public void onComplete() { + long r = remaining; + if (r != Long.MAX_VALUE) { + remaining = r - 1; + } + if (r != 0L) { + subscribeNext(); + } else { + downstream.onComplete(); + } + } + + /** + * Subscribes to the source again via trampolining. + */ + void subscribeNext() { + if (getAndIncrement() == 0) { + int missed = 1; + for (;;) { + if (sd.isDisposed()) { + return; + } + source.subscribe(this); + + missed = addAndGet(-missed); + if (missed == 0) { + break; + } + } + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableRepeatUntil.java b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableRepeatUntil.java new file mode 100644 index 0000000000..d89fd17b83 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableRepeatUntil.java @@ -0,0 +1,104 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import java.util.concurrent.atomic.AtomicInteger; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.BooleanSupplier; +import io.reactivex.rxjava3.internal.disposables.SequentialDisposable; + +public final class ObservableRepeatUntil<T> extends AbstractObservableWithUpstream<T, T> { + final BooleanSupplier until; + public ObservableRepeatUntil(Observable<T> source, BooleanSupplier until) { + super(source); + this.until = until; + } + + @Override + public void subscribeActual(Observer<? super T> observer) { + SequentialDisposable sd = new SequentialDisposable(); + observer.onSubscribe(sd); + + RepeatUntilObserver<T> rs = new RepeatUntilObserver<>(observer, until, sd, source); + rs.subscribeNext(); + } + + static final class RepeatUntilObserver<T> extends AtomicInteger implements Observer<T> { + + private static final long serialVersionUID = -7098360935104053232L; + + final Observer<? super T> downstream; + final SequentialDisposable upstream; + final ObservableSource<? extends T> source; + final BooleanSupplier stop; + RepeatUntilObserver(Observer<? super T> actual, BooleanSupplier until, SequentialDisposable sd, ObservableSource<? extends T> source) { + this.downstream = actual; + this.upstream = sd; + this.source = source; + this.stop = until; + } + + @Override + public void onSubscribe(Disposable d) { + upstream.replace(d); + } + + @Override + public void onNext(T t) { + downstream.onNext(t); + } + + @Override + public void onError(Throwable t) { + downstream.onError(t); + } + + @Override + public void onComplete() { + boolean b; + try { + b = stop.getAsBoolean(); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + downstream.onError(e); + return; + } + if (b) { + downstream.onComplete(); + } else { + subscribeNext(); + } + } + + /** + * Subscribes to the source again via trampolining. + */ + void subscribeNext() { + if (getAndIncrement() == 0) { + int missed = 1; + for (;;) { + source.subscribe(this); + + missed = addAndGet(-missed); + if (missed == 0) { + break; + } + } + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableRepeatWhen.java b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableRepeatWhen.java new file mode 100644 index 0000000000..87d30330ba --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableRepeatWhen.java @@ -0,0 +1,182 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import java.util.Objects; +import java.util.concurrent.atomic.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.disposables.*; +import io.reactivex.rxjava3.internal.util.*; +import io.reactivex.rxjava3.subjects.*; + +/** + * Repeatedly subscribe to a source if a handler ObservableSource signals an item. + * + * @param <T> the value type + */ +public final class ObservableRepeatWhen<T> extends AbstractObservableWithUpstream<T, T> { + + final Function<? super Observable<Object>, ? extends ObservableSource<?>> handler; + + public ObservableRepeatWhen(ObservableSource<T> source, Function<? super Observable<Object>, ? extends ObservableSource<?>> handler) { + super(source); + this.handler = handler; + } + + @Override + protected void subscribeActual(Observer<? super T> observer) { + Subject<Object> signaller = PublishSubject.create().toSerialized(); + + ObservableSource<?> other; + + try { + other = Objects.requireNonNull(handler.apply(signaller), "The handler returned a null ObservableSource"); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + EmptyDisposable.error(ex, observer); + return; + } + + RepeatWhenObserver<T> parent = new RepeatWhenObserver<>(observer, signaller, source); + observer.onSubscribe(parent); + + other.subscribe(parent.inner); + + parent.subscribeNext(); + } + + static final class RepeatWhenObserver<T> extends AtomicInteger implements Observer<T>, Disposable { + + private static final long serialVersionUID = 802743776666017014L; + + final Observer<? super T> downstream; + + final AtomicInteger wip; + + final AtomicThrowable error; + + final Subject<Object> signaller; + + final InnerRepeatObserver inner; + + final AtomicReference<Disposable> upstream; + + final ObservableSource<T> source; + + volatile boolean active; + + RepeatWhenObserver(Observer<? super T> actual, Subject<Object> signaller, ObservableSource<T> source) { + this.downstream = actual; + this.signaller = signaller; + this.source = source; + this.wip = new AtomicInteger(); + this.error = new AtomicThrowable(); + this.inner = new InnerRepeatObserver(); + this.upstream = new AtomicReference<>(); + } + + @Override + public void onSubscribe(Disposable d) { + DisposableHelper.setOnce(this.upstream, d); + } + + @Override + public void onNext(T t) { + HalfSerializer.onNext(downstream, t, this, error); + } + + @Override + public void onError(Throwable e) { + DisposableHelper.dispose(inner); + HalfSerializer.onError(downstream, e, this, error); + } + + @Override + public void onComplete() { + DisposableHelper.replace(upstream, null); + active = false; + signaller.onNext(0); + } + + @Override + public boolean isDisposed() { + return DisposableHelper.isDisposed(upstream.get()); + } + + @Override + public void dispose() { + DisposableHelper.dispose(upstream); + DisposableHelper.dispose(inner); + } + + void innerNext() { + subscribeNext(); + } + + void innerError(Throwable ex) { + DisposableHelper.dispose(upstream); + HalfSerializer.onError(downstream, ex, this, error); + } + + void innerComplete() { + DisposableHelper.dispose(upstream); + HalfSerializer.onComplete(downstream, this, error); + } + + void subscribeNext() { + if (wip.getAndIncrement() == 0) { + + do { + if (isDisposed()) { + return; + } + + if (!active) { + active = true; + source.subscribe(this); + } + } while (wip.decrementAndGet() != 0); + } + } + + final class InnerRepeatObserver extends AtomicReference<Disposable> implements Observer<Object> { + + private static final long serialVersionUID = 3254781284376480842L; + + @Override + public void onSubscribe(Disposable d) { + DisposableHelper.setOnce(this, d); + } + + @Override + public void onNext(Object t) { + innerNext(); + } + + @Override + public void onError(Throwable e) { + innerError(e); + } + + @Override + public void onComplete() { + innerComplete(); + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableReplay.java b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableReplay.java new file mode 100644 index 0000000000..0e8c122d62 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableReplay.java @@ -0,0 +1,1076 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import java.util.*; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.core.Observer; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.disposables.*; +import io.reactivex.rxjava3.internal.fuseable.HasUpstreamObservableSource; +import io.reactivex.rxjava3.internal.util.*; +import io.reactivex.rxjava3.observables.ConnectableObservable; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.schedulers.Timed; + +public final class ObservableReplay<T> extends ConnectableObservable<T> implements HasUpstreamObservableSource<T> { + /** The source observable. */ + final ObservableSource<T> source; + /** Holds the current subscriber that is, will be or just was subscribed to the source observable. */ + final AtomicReference<ReplayObserver<T>> current; + /** A factory that creates the appropriate buffer for the ReplayObserver. */ + final BufferSupplier<T> bufferFactory; + + final ObservableSource<T> onSubscribe; + + interface BufferSupplier<T> { + ReplayBuffer<T> call(); + } + + @SuppressWarnings("rawtypes") + static final BufferSupplier DEFAULT_UNBOUNDED_FACTORY = new UnBoundedFactory(); + + /** + * Given a connectable observable factory, it multicasts over the generated + * ConnectableObservable via a selector function. + * @param <U> the value type of the ConnectableObservable + * @param <R> the result value type + * @param connectableFactory the factory that returns a ConnectableObservable for each individual subscriber + * @param selector the function that receives an Observable and should return another Observable that will be subscribed to + * @return the new Observable instance + */ + public static <U, R> Observable<R> multicastSelector( + final Supplier<? extends ConnectableObservable<U>> connectableFactory, + final Function<? super Observable<U>, ? extends ObservableSource<R>> selector) { + return RxJavaPlugins.onAssembly(new MulticastReplay<>(connectableFactory, selector)); + } + + /** + * Creates a replaying ConnectableObservable with an unbounded buffer. + * @param <T> the value type + * @param source the source observable + * @return the new ConnectableObservable instance + */ + @SuppressWarnings("unchecked") + public static <T> ConnectableObservable<T> createFrom(ObservableSource<? extends T> source) { + return create(source, DEFAULT_UNBOUNDED_FACTORY); + } + + /** + * Creates a replaying ConnectableObservable with a size bound buffer. + * @param <T> the value type + * @param source the source ObservableSource to use + * @param bufferSize the maximum number of elements to hold + * @param eagerTruncate if true, the head reference is refreshed to avoid unwanted item retention + * @return the new ConnectableObservable instance + */ + public static <T> ConnectableObservable<T> create(ObservableSource<T> source, + final int bufferSize, boolean eagerTruncate) { + if (bufferSize == Integer.MAX_VALUE) { + return createFrom(source); + } + return create(source, new ReplayBufferSupplier<>(bufferSize, eagerTruncate)); + } + + /** + * Creates a replaying ConnectableObservable with a time bound buffer. + * @param <T> the value type + * @param source the source ObservableSource to use + * @param maxAge the maximum age of entries + * @param unit the unit of measure of the age amount + * @param scheduler the target scheduler providing the current time + * @param eagerTruncate if true, the head reference is refreshed to avoid unwanted item retention + * @return the new ConnectableObservable instance + */ + public static <T> ConnectableObservable<T> create(ObservableSource<T> source, + long maxAge, TimeUnit unit, Scheduler scheduler, boolean eagerTruncate) { + return create(source, maxAge, unit, scheduler, Integer.MAX_VALUE, eagerTruncate); + } + + /** + * Creates a replaying ConnectableObservable with a size and time bound buffer. + * @param <T> the value type + * @param source the source ObservableSource to use + * @param maxAge the maximum age of entries + * @param unit the unit of measure of the age amount + * @param scheduler the target scheduler providing the current time + * @param bufferSize the maximum number of elements to hold + * @param eagerTruncate if true, the head reference is refreshed to avoid unwanted item retention + * @return the new ConnectableObservable instance + */ + public static <T> ConnectableObservable<T> create(ObservableSource<T> source, + final long maxAge, final TimeUnit unit, final Scheduler scheduler, final int bufferSize, boolean eagerTruncate) { + return create(source, new ScheduledReplaySupplier<>(bufferSize, maxAge, unit, scheduler, eagerTruncate)); + } + + /** + * Creates a OperatorReplay instance to replay values of the given source observable. + * @param <T> the value type + * @param source the source observable + * @param bufferFactory the factory to instantiate the appropriate buffer when the observable becomes active + * @return the connectable observable + */ + static <T> ConnectableObservable<T> create(ObservableSource<T> source, + final BufferSupplier<T> bufferFactory) { + // the current connection to source needs to be shared between the operator and its onSubscribe call + final AtomicReference<ReplayObserver<T>> curr = new AtomicReference<>(); + ObservableSource<T> onSubscribe = new ReplaySource<>(curr, bufferFactory); + return RxJavaPlugins.onAssembly(new ObservableReplay<>(onSubscribe, source, curr, bufferFactory)); + } + + private ObservableReplay(ObservableSource<T> onSubscribe, ObservableSource<T> source, + final AtomicReference<ReplayObserver<T>> current, + final BufferSupplier<T> bufferFactory) { + this.onSubscribe = onSubscribe; + this.source = source; + this.current = current; + this.bufferFactory = bufferFactory; + } + + @Override + public ObservableSource<T> source() { + return source; + } + + @Override + public void reset() { + ReplayObserver<T> conn = current.get(); + if (conn != null && conn.isDisposed()) { + current.compareAndSet(conn, null); + } + } + + @Override + protected void subscribeActual(Observer<? super T> observer) { + onSubscribe.subscribe(observer); + } + + @Override + public void connect(Consumer<? super Disposable> connection) { + boolean doConnect; + ReplayObserver<T> ps; + // we loop because concurrent connect/disconnect and termination may change the state + for (;;) { + // retrieve the current subscriber-to-source instance + ps = current.get(); + // if there is none yet or the current has been disposed + if (ps == null || ps.isDisposed()) { + // create a new subscriber-to-source + ReplayBuffer<T> buf = bufferFactory.call(); + + ReplayObserver<T> u = new ReplayObserver<>(buf, current); + // try setting it as the current subscriber-to-source + if (!current.compareAndSet(ps, u)) { + // did not work, perhaps a new subscriber arrived + // and created a new subscriber-to-source as well, retry + continue; + } + ps = u; + } + // if connect() was called concurrently, only one of them should actually + // connect to the source + doConnect = !ps.shouldConnect.get() && ps.shouldConnect.compareAndSet(false, true); + break; // NOPMD + } + /* + * Notify the callback that we have a (new) connection which it can dispose + * but since ps is unique to a connection, multiple calls to connect() will return the + * same Disposable and even if there was a connect-disconnect-connect pair, the older + * references won't disconnect the newer connection. + * Synchronous source consumers have the opportunity to disconnect via dispose() on the + * Disposable as subscribe() may never return in its own. + * + * Note however, that asynchronously disconnecting a running source might leave + * child observers without any terminal event; ReplaySubject does not have this + * issue because the dispose() call was always triggered by the child observers + * themselves. + */ + + try { + connection.accept(ps); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + if (doConnect) { + ps.shouldConnect.compareAndSet(true, false); + } + Exceptions.throwIfFatal(ex); + throw ExceptionHelper.wrapOrThrow(ex); + } + if (doConnect) { + source.subscribe(ps); + } + } + + @SuppressWarnings("rawtypes") + static final class ReplayObserver<T> + extends AtomicReference<Disposable> + implements Observer<T>, Disposable { + private static final long serialVersionUID = -533785617179540163L; + /** Holds notifications from upstream. */ + final ReplayBuffer<T> buffer; + /** Indicates this Observer received a terminal event. */ + boolean done; + + /** Indicates an empty array of inner observers. */ + static final InnerDisposable[] EMPTY = new InnerDisposable[0]; + /** Indicates a terminated ReplayObserver. */ + static final InnerDisposable[] TERMINATED = new InnerDisposable[0]; + + /** Tracks the subscribed observers. */ + final AtomicReference<InnerDisposable[]> observers; + /** + * Atomically changed from false to true by connect to make sure the + * connection is only performed by one thread. + */ + final AtomicBoolean shouldConnect; + + /** The current connection. */ + final AtomicReference<ReplayObserver<T>> current; + + ReplayObserver(ReplayBuffer<T> buffer, AtomicReference<ReplayObserver<T>> current) { + this.buffer = buffer; + this.current = current; + + this.observers = new AtomicReference<>(EMPTY); + this.shouldConnect = new AtomicBoolean(); + } + + @Override + public boolean isDisposed() { + return observers.get() == TERMINATED; + } + + @Override + public void dispose() { + observers.set(TERMINATED); + current.compareAndSet(ReplayObserver.this, null); + // we don't care if it fails because it means the current has + // been replaced in the meantime + DisposableHelper.dispose(this); + } + + /** + * Atomically try adding a new InnerDisposable to this Observer or return false if this + * Observer was terminated. + * @param producer the producer to add + * @return true if succeeded, false otherwise + */ + boolean add(InnerDisposable<T> producer) { + // the state can change so we do a CAS loop to achieve atomicity + for (;;) { + // get the current producer array + InnerDisposable[] c = observers.get(); + // if this subscriber-to-source reached a terminal state by receiving + // an onError or onComplete, just refuse to add the new producer + if (c == TERMINATED) { + return false; + } + // we perform a copy-on-write logic + int len = c.length; + InnerDisposable[] u = new InnerDisposable[len + 1]; + System.arraycopy(c, 0, u, 0, len); + u[len] = producer; + // try setting the observers array + if (observers.compareAndSet(c, u)) { + return true; + } + // if failed, some other operation succeeded (another add, remove or termination) + // so retry + } + } + + /** + * Atomically removes the given InnerDisposable from the observers array. + * @param producer the producer to remove + */ + void remove(InnerDisposable<T> producer) { + // the state can change so we do a CAS loop to achieve atomicity + for (;;) { + // let's read the current observers array + InnerDisposable[] c = observers.get(); + + int len = c.length; + // if it is either empty or terminated, there is nothing to remove so we quit + if (len == 0) { + return; + } + // let's find the supplied producer in the array + // although this is O(n), we don't expect too many child observers in general + int j = -1; + for (int i = 0; i < len; i++) { + if (c[i].equals(producer)) { + j = i; + break; + } + } + // we didn't find it so just quit + if (j < 0) { + return; + } + // we do copy-on-write logic here + InnerDisposable[] u; + // we don't create a new empty array if producer was the single inhabitant + // but rather reuse an empty array + if (len == 1) { + u = EMPTY; + } else { + // otherwise, create a new array one less in size + u = new InnerDisposable[len - 1]; + // copy elements being before the given producer + System.arraycopy(c, 0, u, 0, j); + // copy elements being after the given producer + System.arraycopy(c, j + 1, u, j, len - j - 1); + } + // try setting this new array as + if (observers.compareAndSet(c, u)) { + return; + } + // if we failed, it means something else happened + // (a concurrent add/remove or termination), we need to retry + } + } + + @Override + public void onSubscribe(Disposable p) { + if (DisposableHelper.setOnce(this, p)) { + replay(); + } + } + + @Override + public void onNext(T t) { + if (!done) { + buffer.next(t); + replay(); + } + } + + @Override + public void onError(Throwable e) { + // The observer front is accessed serially as required by spec so + // no need to CAS in the terminal value + if (!done) { + done = true; + buffer.error(e); + replayFinal(); + } else { + RxJavaPlugins.onError(e); + } + } + + @Override + public void onComplete() { + // The observer front is accessed serially as required by spec so + // no need to CAS in the terminal value + if (!done) { + done = true; + buffer.complete(); + replayFinal(); + } + } + + /** + * Tries to replay the buffer contents to all known observers. + */ + void replay() { + @SuppressWarnings("unchecked") + InnerDisposable<T>[] a = observers.get(); + for (InnerDisposable<T> rp : a) { + buffer.replay(rp); + } + } + + /** + * Tries to replay the buffer contents to all known observers. + */ + void replayFinal() { + @SuppressWarnings("unchecked") + InnerDisposable<T>[] a = observers.getAndSet(TERMINATED); + for (InnerDisposable<T> rp : a) { + buffer.replay(rp); + } + } + } + /** + * A Disposable that manages the disposed state of a + * child Observer in thread-safe manner. + * @param <T> the value type + */ + static final class InnerDisposable<T> + extends AtomicInteger + implements Disposable { + private static final long serialVersionUID = 2728361546769921047L; + /** + * The parent subscriber-to-source used to allow removing the child in case of + * child dispose() call. + */ + final ReplayObserver<T> parent; + /** The actual child subscriber. */ + final Observer<? super T> child; + /** + * Holds an object that represents the current location in the buffer. + * Guarded by the emitter loop. + */ + Object index; + + volatile boolean cancelled; + + InnerDisposable(ReplayObserver<T> parent, Observer<? super T> child) { + this.parent = parent; + this.child = child; + } + + @Override + public boolean isDisposed() { + return cancelled; + } + + @Override + public void dispose() { + if (!cancelled) { + cancelled = true; + // remove this from the parent + parent.remove(this); + // make sure the last known node is not retained + index = null; + } + } + /** + * Convenience method to auto-cast the index object. + * @param <U> type index to be casted to + * @return the index Object or null + */ + @SuppressWarnings("unchecked") + <U> U index() { + return (U)index; + } + } + /** + * The interface for interacting with various buffering logic. + * + * @param <T> the value type + */ + interface ReplayBuffer<T> { + /** + * Adds a regular value to the buffer. + * @param value the value to be stored in the buffer + */ + void next(T value); + /** + * Adds a terminal exception to the buffer. + * @param e the error to be stored in the buffer + */ + void error(Throwable e); + /** + * Adds a completion event to the buffer. + */ + void complete(); + /** + * Tries to replay the buffered values to the + * subscriber inside the output if there + * is new value and requests available at the + * same time. + * @param output the receiver of the buffered events + */ + void replay(InnerDisposable<T> output); + } + + /** + * Holds an unbounded list of events. + * + * @param <T> the value type + */ + static final class UnboundedReplayBuffer<T> extends ArrayList<Object> implements ReplayBuffer<T> { + + private static final long serialVersionUID = 7063189396499112664L; + /** The total number of events in the buffer. */ + volatile int size; + + UnboundedReplayBuffer(int capacityHint) { + super(capacityHint); + } + + @Override + public void next(T value) { + add(NotificationLite.next(value)); + size++; + } + + @Override + public void error(Throwable e) { + add(NotificationLite.error(e)); + size++; + } + + @Override + public void complete() { + add(NotificationLite.complete()); + size++; + } + + @Override + public void replay(InnerDisposable<T> output) { + if (output.getAndIncrement() != 0) { + return; + } + + final Observer<? super T> child = output.child; + + int missed = 1; + + for (;;) { + if (output.isDisposed()) { + return; + } + int sourceIndex = size; + + Integer destinationIndexObject = output.index(); + int destinationIndex = destinationIndexObject != null ? destinationIndexObject : 0; + + while (destinationIndex < sourceIndex) { + Object o = get(destinationIndex); + if (NotificationLite.accept(o, child)) { + return; + } + if (output.isDisposed()) { + return; + } + destinationIndex++; + } + + output.index = destinationIndex; + missed = output.addAndGet(-missed); + if (missed == 0) { + break; + } + } + } + } + + /** + * Represents a node in a bounded replay buffer's linked list. + */ + static final class Node extends AtomicReference<Node> { + + private static final long serialVersionUID = 245354315435971818L; + final Object value; + Node(Object value) { + this.value = value; + } + } + + /** + * Base class for bounded buffering with options to specify an + * enter and leave transforms and custom truncation behavior. + * + * @param <T> the value type + */ + abstract static class BoundedReplayBuffer<T> extends AtomicReference<Node> implements ReplayBuffer<T> { + + private static final long serialVersionUID = 2346567790059478686L; + + Node tail; + int size; + + final boolean eagerTruncate; + + BoundedReplayBuffer(boolean eagerTruncate) { + this.eagerTruncate = eagerTruncate; + Node n = new Node(null); + tail = n; + set(n); + } + + /** + * Add a new node to the linked list. + * @param n the Node instance to add as last + */ + final void addLast(Node n) { + tail.set(n); + tail = n; + size++; + } + /** + * Remove the first node from the linked list. + */ + final void removeFirst() { + Node head = get(); + Node next = head.get(); + size--; + // can't just move the head because it would retain the very first value + // can't null out the head's value because of late replayers would see null + setFirst(next); + } + + final void trimHead() { + Node head = get(); + if (head.value != null) { + Node n = new Node(null); + n.lazySet(head.get()); + set(n); + } + } + + /* test */ final void removeSome(int n) { + Node head = get(); + while (n > 0) { + head = head.get(); + n--; + size--; + } + + setFirst(head); + // correct the tail if all items have been removed + head = get(); + if (head.get() == null) { + tail = head; + } + } + /** + * Arranges the given node is the new head from now on. + * @param n the Node instance to set as first + */ + final void setFirst(Node n) { + if (eagerTruncate) { + Node m = new Node(null); + m.lazySet(n.get()); + n = m; + } + set(n); + } + + @Override + public final void next(T value) { + Object o = enterTransform(NotificationLite.next(value)); + Node n = new Node(o); + addLast(n); + truncate(); + } + + @Override + public final void error(Throwable e) { + Object o = enterTransform(NotificationLite.error(e)); + Node n = new Node(o); + addLast(n); + truncateFinal(); + } + + @Override + public final void complete() { + Object o = enterTransform(NotificationLite.complete()); + Node n = new Node(o); + addLast(n); + truncateFinal(); + } + + @Override + public final void replay(InnerDisposable<T> output) { + if (output.getAndIncrement() != 0) { + return; + } + + int missed = 1; + + for (;;) { + Node node = output.index(); + if (node == null) { + node = getHead(); + output.index = node; + } + + for (;;) { + if (output.isDisposed()) { + output.index = null; + return; + } + + Node v = node.get(); + if (v != null) { + Object o = leaveTransform(v.value); + if (NotificationLite.accept(o, output.child)) { + output.index = null; + return; + } + node = v; + } else { + break; + } + } + + output.index = node; + + missed = output.addAndGet(-missed); + if (missed == 0) { + break; + } + } + + } + + /** + * Override this to wrap the NotificationLite object into a + * container to be used later by truncate. + * @param value the value to transform into the internal representation + * @return the transformed value + */ + Object enterTransform(Object value) { + return value; + } + /** + * Override this to unwrap the transformed value into a + * NotificationLite object. + * @param value the value in the internal representation to transform + * @return the transformed value + */ + Object leaveTransform(Object value) { + return value; + } + /** + * Override this method to truncate a non-terminated buffer + * based on its current properties. + */ + abstract void truncate(); + + /** + * Override this method to truncate a terminated buffer + * based on its properties (i.e., truncate but the very last node). + */ + void truncateFinal() { + trimHead(); + } + /* test */ final void collect(Collection<? super T> output) { + Node n = getHead(); + for (;;) { + Node next = n.get(); + if (next != null) { + Object o = next.value; + Object v = leaveTransform(o); + if (NotificationLite.isComplete(v) || NotificationLite.isError(v)) { + break; + } + output.add(NotificationLite.<T>getValue(v)); + n = next; + } else { + break; + } + } + } + /* test */ boolean hasError() { + return tail.value != null && NotificationLite.isError(leaveTransform(tail.value)); + } + /* test */ boolean hasCompleted() { + return tail.value != null && NotificationLite.isComplete(leaveTransform(tail.value)); + } + + Node getHead() { + return get(); + } + } + + /** + * A bounded replay buffer implementation with size limit only. + * + * @param <T> the value type + */ + static final class SizeBoundReplayBuffer<T> extends BoundedReplayBuffer<T> { + + private static final long serialVersionUID = -5898283885385201806L; + + final int limit; + + SizeBoundReplayBuffer(int limit, boolean eagerTruncate) { + super(eagerTruncate); + this.limit = limit; + } + + @Override + void truncate() { + // overflow can be at most one element + if (size > limit) { + removeFirst(); + } + } + + // no need for final truncation because values are truncated one by one + } + + /** + * Size and time bound replay buffer. + * + * @param <T> the buffered value type + */ + static final class SizeAndTimeBoundReplayBuffer<T> extends BoundedReplayBuffer<T> { + + private static final long serialVersionUID = 3457957419649567404L; + final Scheduler scheduler; + final long maxAge; + final TimeUnit unit; + final int limit; + SizeAndTimeBoundReplayBuffer(int limit, long maxAge, TimeUnit unit, Scheduler scheduler, boolean eagerTruncate) { + super(eagerTruncate); + this.scheduler = scheduler; + this.limit = limit; + this.maxAge = maxAge; + this.unit = unit; + } + + @Override + Object enterTransform(Object value) { + return new Timed<>(value, scheduler.now(unit), unit); + } + + @Override + Object leaveTransform(Object value) { + return ((Timed<?>)value).value(); + } + + @Override + void truncate() { + long timeLimit = scheduler.now(unit) - maxAge; + + Node prev = get(); + Node next = prev.get(); + + int e = 0; + for (;;) { + if (size > 1) { // never truncate the very last item just added + if (size > limit) { + e++; + size--; + prev = next; + next = next.get(); + } else { + Timed<?> v = (Timed<?>)next.value; + if (v.time() <= timeLimit) { + e++; + size--; + prev = next; + next = next.get(); + } else { + break; + } + } + } else { + break; + } + } + if (e != 0) { + setFirst(prev); + } + } + + @Override + void truncateFinal() { + long timeLimit = scheduler.now(unit) - maxAge; + + Node prev = get(); + Node next = prev.get(); + + int e = 0; + for (;;) { + if (size > 1) { + Timed<?> v = (Timed<?>)next.value; + if (v.time() <= timeLimit) { + e++; + size--; + prev = next; + next = next.get(); + } else { + break; + } + } else { + break; + } + } + if (e != 0) { + setFirst(prev); + } + } + + @Override + Node getHead() { + long timeLimit = scheduler.now(unit) - maxAge; + Node prev = get(); + Node next = prev.get(); + for (;;) { + if (next == null) { + break; + } + Timed<?> v = (Timed<?>)next.value; + if (NotificationLite.isComplete(v.value()) || NotificationLite.isError(v.value())) { + break; + } + if (v.time() <= timeLimit) { + prev = next; + next = next.get(); + } else { + break; + } + } + return prev; + } + } + + static final class UnBoundedFactory implements BufferSupplier<Object> { + @Override + public ReplayBuffer<Object> call() { + return new UnboundedReplayBuffer<>(16); + } + } + + static final class DisposeConsumer<R> implements Consumer<Disposable> { + private final ObserverResourceWrapper<R> srw; + + DisposeConsumer(ObserverResourceWrapper<R> srw) { + this.srw = srw; + } + + @Override + public void accept(Disposable r) { + srw.setResource(r); + } + } + + static final class ReplayBufferSupplier<T> implements BufferSupplier<T> { + + final int bufferSize; + + final boolean eagerTruncate; + + ReplayBufferSupplier(int bufferSize, boolean eagerTruncate) { + this.bufferSize = bufferSize; + this.eagerTruncate = eagerTruncate; + } + + @Override + public ReplayBuffer<T> call() { + return new SizeBoundReplayBuffer<>(bufferSize, eagerTruncate); + } + } + + static final class ScheduledReplaySupplier<T> implements BufferSupplier<T> { + private final int bufferSize; + private final long maxAge; + private final TimeUnit unit; + private final Scheduler scheduler; + + final boolean eagerTruncate; + + ScheduledReplaySupplier(int bufferSize, long maxAge, TimeUnit unit, Scheduler scheduler, boolean eagerTruncate) { + this.bufferSize = bufferSize; + this.maxAge = maxAge; + this.unit = unit; + this.scheduler = scheduler; + this.eagerTruncate = eagerTruncate; + } + + @Override + public ReplayBuffer<T> call() { + return new SizeAndTimeBoundReplayBuffer<>(bufferSize, maxAge, unit, scheduler, eagerTruncate); + } + } + + static final class ReplaySource<T> implements ObservableSource<T> { + private final AtomicReference<ReplayObserver<T>> curr; + private final BufferSupplier<T> bufferFactory; + + ReplaySource(AtomicReference<ReplayObserver<T>> curr, BufferSupplier<T> bufferFactory) { + this.curr = curr; + this.bufferFactory = bufferFactory; + } + + @Override + public void subscribe(Observer<? super T> child) { + // concurrent connection/disconnection may change the state, + // we loop to be atomic while the child subscribes + for (;;) { + // get the current subscriber-to-source + ReplayObserver<T> r = curr.get(); + // if there isn't one + if (r == null) { + // create a new subscriber to source + ReplayBuffer<T> buf = bufferFactory.call(); + + ReplayObserver<T> u = new ReplayObserver<>(buf, curr); + // let's try setting it as the current subscriber-to-source + if (!curr.compareAndSet(null, u)) { + // didn't work, maybe someone else did it or the current subscriber + // to source has just finished + continue; + } + // we won, let's use it going onwards + r = u; + } + + // create the backpressure-managing producer for this child + InnerDisposable<T> inner = new InnerDisposable<>(r, child); + // the producer has been registered with the current subscriber-to-source so + // at least it will receive the next terminal event + // setting the producer will trigger the first request to be considered by + // the subscriber-to-source. + child.onSubscribe(inner); + // we try to add it to the array of observers + // if it fails, no worries because we will still have its buffer + // so it is going to replay it for us + r.add(inner); + + if (inner.isDisposed()) { + r.remove(inner); + return; + } + + // replay the contents of the buffer + r.buffer.replay(inner); + + break; // NOPMD + } + } + } + + static final class MulticastReplay<R, U> extends Observable<R> { + private final Supplier<? extends ConnectableObservable<U>> connectableFactory; + private final Function<? super Observable<U>, ? extends ObservableSource<R>> selector; + + MulticastReplay(Supplier<? extends ConnectableObservable<U>> connectableFactory, Function<? super Observable<U>, ? extends ObservableSource<R>> selector) { + this.connectableFactory = connectableFactory; + this.selector = selector; + } + + @Override + protected void subscribeActual(Observer<? super R> child) { + ConnectableObservable<U> co; + ObservableSource<R> observable; + try { + co = Objects.requireNonNull(connectableFactory.get(), "The connectableFactory returned a null ConnectableObservable"); + observable = Objects.requireNonNull(selector.apply(co), "The selector returned a null ObservableSource"); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + EmptyDisposable.error(e, child); + return; + } + + final ObserverResourceWrapper<R> srw = new ObserverResourceWrapper<>(child); + + observable.subscribe(srw); + + co.connect(new DisposeConsumer<>(srw)); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableRetryBiPredicate.java b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableRetryBiPredicate.java new file mode 100644 index 0000000000..1340a5ad9f --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableRetryBiPredicate.java @@ -0,0 +1,111 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import java.util.concurrent.atomic.AtomicInteger; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.BiPredicate; +import io.reactivex.rxjava3.internal.disposables.SequentialDisposable; + +public final class ObservableRetryBiPredicate<T> extends AbstractObservableWithUpstream<T, T> { + final BiPredicate<? super Integer, ? super Throwable> predicate; + public ObservableRetryBiPredicate( + Observable<T> source, + BiPredicate<? super Integer, ? super Throwable> predicate) { + super(source); + this.predicate = predicate; + } + + @Override + public void subscribeActual(Observer<? super T> observer) { + SequentialDisposable sa = new SequentialDisposable(); + observer.onSubscribe(sa); + + RetryBiObserver<T> rs = new RetryBiObserver<>(observer, predicate, sa, source); + rs.subscribeNext(); + } + + static final class RetryBiObserver<T> extends AtomicInteger implements Observer<T> { + + private static final long serialVersionUID = -7098360935104053232L; + + final Observer<? super T> downstream; + final SequentialDisposable upstream; + final ObservableSource<? extends T> source; + final BiPredicate<? super Integer, ? super Throwable> predicate; + int retries; + RetryBiObserver(Observer<? super T> actual, + BiPredicate<? super Integer, ? super Throwable> predicate, SequentialDisposable sa, ObservableSource<? extends T> source) { + this.downstream = actual; + this.upstream = sa; + this.source = source; + this.predicate = predicate; + } + + @Override + public void onSubscribe(Disposable d) { + upstream.replace(d); + } + + @Override + public void onNext(T t) { + downstream.onNext(t); + } + + @Override + public void onError(Throwable t) { + boolean b; + try { + b = predicate.test(++retries, t); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + downstream.onError(new CompositeException(t, e)); + return; + } + if (!b) { + downstream.onError(t); + return; + } + subscribeNext(); + } + + @Override + public void onComplete() { + downstream.onComplete(); + } + + /** + * Subscribes to the source again via trampolining. + */ + void subscribeNext() { + if (getAndIncrement() == 0) { + int missed = 1; + for (;;) { + if (upstream.isDisposed()) { + return; + } + source.subscribe(this); + + missed = addAndGet(-missed); + if (missed == 0) { + break; + } + } + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableRetryPredicate.java b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableRetryPredicate.java new file mode 100644 index 0000000000..5c40dc6d97 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableRetryPredicate.java @@ -0,0 +1,122 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import java.util.concurrent.atomic.AtomicInteger; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.Predicate; +import io.reactivex.rxjava3.internal.disposables.SequentialDisposable; + +public final class ObservableRetryPredicate<T> extends AbstractObservableWithUpstream<T, T> { + final Predicate<? super Throwable> predicate; + final long count; + public ObservableRetryPredicate(Observable<T> source, + long count, + Predicate<? super Throwable> predicate) { + super(source); + this.predicate = predicate; + this.count = count; + } + + @Override + public void subscribeActual(Observer<? super T> observer) { + SequentialDisposable sa = new SequentialDisposable(); + observer.onSubscribe(sa); + + RepeatObserver<T> rs = new RepeatObserver<>(observer, count, predicate, sa, source); + rs.subscribeNext(); + } + + static final class RepeatObserver<T> extends AtomicInteger implements Observer<T> { + + private static final long serialVersionUID = -7098360935104053232L; + + final Observer<? super T> downstream; + final SequentialDisposable upstream; + final ObservableSource<? extends T> source; + final Predicate<? super Throwable> predicate; + long remaining; + RepeatObserver(Observer<? super T> actual, long count, + Predicate<? super Throwable> predicate, SequentialDisposable sa, ObservableSource<? extends T> source) { + this.downstream = actual; + this.upstream = sa; + this.source = source; + this.predicate = predicate; + this.remaining = count; + } + + @Override + public void onSubscribe(Disposable d) { + upstream.replace(d); + } + + @Override + public void onNext(T t) { + downstream.onNext(t); + } + + @Override + public void onError(Throwable t) { + long r = remaining; + if (r != Long.MAX_VALUE) { + remaining = r - 1; + } + if (r == 0) { + downstream.onError(t); + } else { + boolean b; + try { + b = predicate.test(t); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + downstream.onError(new CompositeException(t, e)); + return; + } + if (!b) { + downstream.onError(t); + return; + } + subscribeNext(); + } + } + + @Override + public void onComplete() { + downstream.onComplete(); + } + + /** + * Subscribes to the source again via trampolining. + */ + void subscribeNext() { + if (getAndIncrement() == 0) { + int missed = 1; + for (;;) { + if (upstream.isDisposed()) { + return; + } + source.subscribe(this); + + missed = addAndGet(-missed); + if (missed == 0) { + break; + } + } + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableRetryWhen.java b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableRetryWhen.java new file mode 100644 index 0000000000..bae50456e4 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableRetryWhen.java @@ -0,0 +1,182 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import java.util.Objects; +import java.util.concurrent.atomic.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.disposables.*; +import io.reactivex.rxjava3.internal.util.*; +import io.reactivex.rxjava3.subjects.*; + +/** + * Repeatedly subscribe to a source if a handler ObservableSource signals an item. + * + * @param <T> the value type + */ +public final class ObservableRetryWhen<T> extends AbstractObservableWithUpstream<T, T> { + + final Function<? super Observable<Throwable>, ? extends ObservableSource<?>> handler; + + public ObservableRetryWhen(ObservableSource<T> source, Function<? super Observable<Throwable>, ? extends ObservableSource<?>> handler) { + super(source); + this.handler = handler; + } + + @Override + protected void subscribeActual(Observer<? super T> observer) { + Subject<Throwable> signaller = PublishSubject.<Throwable>create().toSerialized(); + + ObservableSource<?> other; + + try { + other = Objects.requireNonNull(handler.apply(signaller), "The handler returned a null ObservableSource"); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + EmptyDisposable.error(ex, observer); + return; + } + + RepeatWhenObserver<T> parent = new RepeatWhenObserver<>(observer, signaller, source); + observer.onSubscribe(parent); + + other.subscribe(parent.inner); + + parent.subscribeNext(); + } + + static final class RepeatWhenObserver<T> extends AtomicInteger implements Observer<T>, Disposable { + + private static final long serialVersionUID = 802743776666017014L; + + final Observer<? super T> downstream; + + final AtomicInteger wip; + + final AtomicThrowable error; + + final Subject<Throwable> signaller; + + final InnerRepeatObserver inner; + + final AtomicReference<Disposable> upstream; + + final ObservableSource<T> source; + + volatile boolean active; + + RepeatWhenObserver(Observer<? super T> actual, Subject<Throwable> signaller, ObservableSource<T> source) { + this.downstream = actual; + this.signaller = signaller; + this.source = source; + this.wip = new AtomicInteger(); + this.error = new AtomicThrowable(); + this.inner = new InnerRepeatObserver(); + this.upstream = new AtomicReference<>(); + } + + @Override + public void onSubscribe(Disposable d) { + DisposableHelper.replace(this.upstream, d); + } + + @Override + public void onNext(T t) { + HalfSerializer.onNext(downstream, t, this, error); + } + + @Override + public void onError(Throwable e) { + DisposableHelper.replace(upstream, null); + active = false; + signaller.onNext(e); + } + + @Override + public void onComplete() { + DisposableHelper.dispose(inner); + HalfSerializer.onComplete(downstream, this, error); + } + + @Override + public boolean isDisposed() { + return DisposableHelper.isDisposed(upstream.get()); + } + + @Override + public void dispose() { + DisposableHelper.dispose(upstream); + DisposableHelper.dispose(inner); + } + + void innerNext() { + subscribeNext(); + } + + void innerError(Throwable ex) { + DisposableHelper.dispose(upstream); + HalfSerializer.onError(downstream, ex, this, error); + } + + void innerComplete() { + DisposableHelper.dispose(upstream); + HalfSerializer.onComplete(downstream, this, error); + } + + void subscribeNext() { + if (wip.getAndIncrement() == 0) { + + do { + if (isDisposed()) { + return; + } + + if (!active) { + active = true; + source.subscribe(this); + } + } while (wip.decrementAndGet() != 0); + } + } + + final class InnerRepeatObserver extends AtomicReference<Disposable> implements Observer<Object> { + + private static final long serialVersionUID = 3254781284376480842L; + + @Override + public void onSubscribe(Disposable d) { + DisposableHelper.setOnce(this, d); + } + + @Override + public void onNext(Object t) { + innerNext(); + } + + @Override + public void onError(Throwable e) { + innerError(e); + } + + @Override + public void onComplete() { + innerComplete(); + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableSampleTimed.java b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableSampleTimed.java new file mode 100644 index 0000000000..f264b8e76d --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableSampleTimed.java @@ -0,0 +1,190 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.Consumer; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; +import io.reactivex.rxjava3.observers.SerializedObserver; + +public final class ObservableSampleTimed<T> extends AbstractObservableWithUpstream<T, T> { + final long period; + final TimeUnit unit; + final Scheduler scheduler; + final Consumer<? super T> onDropped; + final boolean emitLast; + + public ObservableSampleTimed(ObservableSource<T> source, + long period, + TimeUnit unit, + Scheduler scheduler, + boolean emitLast, + Consumer<? super T> onDropped) { + super(source); + this.period = period; + this.unit = unit; + this.scheduler = scheduler; + this.emitLast = emitLast; + this.onDropped = onDropped; + } + + @Override + public void subscribeActual(Observer<? super T> t) { + SerializedObserver<T> serial = new SerializedObserver<>(t); + if (emitLast) { + source.subscribe(new SampleTimedEmitLast<>(serial, period, unit, scheduler, onDropped)); + } else { + source.subscribe(new SampleTimedNoLast<>(serial, period, unit, scheduler, onDropped)); + } + } + + abstract static class SampleTimedObserver<T> extends AtomicReference<T> implements Observer<T>, Disposable, Runnable { + + private static final long serialVersionUID = -3517602651313910099L; + + final Observer<? super T> downstream; + final long period; + final TimeUnit unit; + final Scheduler scheduler; + final Consumer<? super T> onDropped; + + final AtomicReference<Disposable> timer = new AtomicReference<>(); + + Disposable upstream; + + SampleTimedObserver(Observer<? super T> actual, long period, TimeUnit unit, Scheduler scheduler, Consumer<? super T> onDropped) { + this.downstream = actual; + this.period = period; + this.unit = unit; + this.scheduler = scheduler; + this.onDropped = onDropped; + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.validate(this.upstream, d)) { + this.upstream = d; + downstream.onSubscribe(this); + + Disposable task = scheduler.schedulePeriodicallyDirect(this, period, period, unit); + DisposableHelper.replace(timer, task); + } + } + + @Override + public void onNext(T t) { + T oldValue = getAndSet(t); + if (oldValue != null && onDropped != null) { + try { + onDropped.accept(oldValue); + } catch (Throwable throwable) { + Exceptions.throwIfFatal(throwable); + cancelTimer(); + upstream.dispose(); + downstream.onError(throwable); + } + } + } + + @Override + public void onError(Throwable t) { + cancelTimer(); + downstream.onError(t); + } + + @Override + public void onComplete() { + cancelTimer(); + complete(); + } + + void cancelTimer() { + DisposableHelper.dispose(timer); + } + + @Override + public void dispose() { + cancelTimer(); + upstream.dispose(); + } + + @Override + public boolean isDisposed() { + return upstream.isDisposed(); + } + + void emit() { + T value = getAndSet(null); + if (value != null) { + downstream.onNext(value); + } + } + + abstract void complete(); + } + + static final class SampleTimedNoLast<T> extends SampleTimedObserver<T> { + + private static final long serialVersionUID = -7139995637533111443L; + + SampleTimedNoLast(Observer<? super T> actual, long period, TimeUnit unit, Scheduler scheduler, Consumer<? super T> onDropped) { + super(actual, period, unit, scheduler, onDropped); + } + + @Override + void complete() { + downstream.onComplete(); + } + + @Override + public void run() { + emit(); + } + } + + static final class SampleTimedEmitLast<T> extends SampleTimedObserver<T> { + + private static final long serialVersionUID = -7139995637533111443L; + + final AtomicInteger wip; + + SampleTimedEmitLast(Observer<? super T> actual, long period, TimeUnit unit, Scheduler scheduler, Consumer<? super T> onDropped) { + super(actual, period, unit, scheduler, onDropped); + this.wip = new AtomicInteger(1); + } + + @Override + void complete() { + emit(); + if (wip.decrementAndGet() == 0) { + downstream.onComplete(); + } + } + + @Override + public void run() { + if (wip.incrementAndGet() == 2) { + emit(); + if (wip.decrementAndGet() == 0) { + downstream.onComplete(); + } + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableSampleWithObservable.java b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableSampleWithObservable.java new file mode 100644 index 0000000000..53b9aec31f --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableSampleWithObservable.java @@ -0,0 +1,210 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import java.util.concurrent.atomic.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; +import io.reactivex.rxjava3.observers.SerializedObserver; + +public final class ObservableSampleWithObservable<T> extends AbstractObservableWithUpstream<T, T> { + + final ObservableSource<?> other; + + final boolean emitLast; + + public ObservableSampleWithObservable(ObservableSource<T> source, ObservableSource<?> other, boolean emitLast) { + super(source); + this.other = other; + this.emitLast = emitLast; + } + + @Override + public void subscribeActual(Observer<? super T> t) { + SerializedObserver<T> serial = new SerializedObserver<>(t); + if (emitLast) { + source.subscribe(new SampleMainEmitLast<>(serial, other)); + } else { + source.subscribe(new SampleMainNoLast<>(serial, other)); + } + } + + abstract static class SampleMainObserver<T> extends AtomicReference<T> + implements Observer<T>, Disposable { + + private static final long serialVersionUID = -3517602651313910099L; + + final Observer<? super T> downstream; + final ObservableSource<?> sampler; + + final AtomicReference<Disposable> other = new AtomicReference<>(); + + Disposable upstream; + + SampleMainObserver(Observer<? super T> actual, ObservableSource<?> other) { + this.downstream = actual; + this.sampler = other; + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.validate(this.upstream, d)) { + this.upstream = d; + downstream.onSubscribe(this); + if (other.get() == null) { + sampler.subscribe(new SamplerObserver<>(this)); + } + } + } + + @Override + public void onNext(T t) { + lazySet(t); + } + + @Override + public void onError(Throwable t) { + DisposableHelper.dispose(other); + downstream.onError(t); + } + + @Override + public void onComplete() { + DisposableHelper.dispose(other); + completion(); + } + + boolean setOther(Disposable o) { + return DisposableHelper.setOnce(other, o); + } + + @Override + public void dispose() { + DisposableHelper.dispose(other); + upstream.dispose(); + } + + @Override + public boolean isDisposed() { + return other.get() == DisposableHelper.DISPOSED; + } + + public void error(Throwable e) { + upstream.dispose(); + downstream.onError(e); + } + + public void complete() { + upstream.dispose(); + completion(); + } + + void emit() { + T value = getAndSet(null); + if (value != null) { + downstream.onNext(value); + } + } + + abstract void completion(); + + abstract void run(); + } + + static final class SamplerObserver<T> implements Observer<Object> { + final SampleMainObserver<T> parent; + SamplerObserver(SampleMainObserver<T> parent) { + this.parent = parent; + + } + + @Override + public void onSubscribe(Disposable d) { + parent.setOther(d); + } + + @Override + public void onNext(Object t) { + parent.run(); + } + + @Override + public void onError(Throwable t) { + parent.error(t); + } + + @Override + public void onComplete() { + parent.complete(); + } + } + + static final class SampleMainNoLast<T> extends SampleMainObserver<T> { + + private static final long serialVersionUID = -3029755663834015785L; + + SampleMainNoLast(Observer<? super T> actual, ObservableSource<?> other) { + super(actual, other); + } + + @Override + void completion() { + downstream.onComplete(); + } + + @Override + void run() { + emit(); + } + } + + static final class SampleMainEmitLast<T> extends SampleMainObserver<T> { + + private static final long serialVersionUID = -3029755663834015785L; + + final AtomicInteger wip; + + volatile boolean done; + + SampleMainEmitLast(Observer<? super T> actual, ObservableSource<?> other) { + super(actual, other); + this.wip = new AtomicInteger(); + } + + @Override + void completion() { + done = true; + if (wip.getAndIncrement() == 0) { + emit(); + downstream.onComplete(); + } + } + + @Override + void run() { + if (wip.getAndIncrement() == 0) { + do { + boolean d = done; + emit(); + if (d) { + downstream.onComplete(); + return; + } + } while (wip.decrementAndGet() != 0); + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableScalarXMap.java b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableScalarXMap.java new file mode 100644 index 0000000000..7ce5bb82f0 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableScalarXMap.java @@ -0,0 +1,256 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import java.util.Objects; +import java.util.concurrent.atomic.AtomicInteger; + +import io.reactivex.rxjava3.annotations.Nullable; +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.disposables.EmptyDisposable; +import io.reactivex.rxjava3.operators.QueueDisposable; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +/** + * Utility classes to work with scalar-sourced XMap operators (where X == { flat, concat, switch }). + */ +public final class ObservableScalarXMap { + + /** Utility class. */ + private ObservableScalarXMap() { + throw new IllegalStateException("No instances!"); + } + + /** + * Tries to subscribe to a possibly Supplier source's mapped ObservableSource. + * @param <T> the input value type + * @param <R> the output value type + * @param source the source ObservableSource + * @param observer the subscriber + * @param mapper the function mapping a scalar value into an ObservableSource + * @return true if successful, false if the caller should continue with the regular path. + */ + @SuppressWarnings("unchecked") + public static <T, R> boolean tryScalarXMapSubscribe(ObservableSource<T> source, + Observer<? super R> observer, + Function<? super T, ? extends ObservableSource<? extends R>> mapper) { + if (source instanceof Supplier) { + T t; + + try { + t = ((Supplier<T>)source).get(); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + EmptyDisposable.error(ex, observer); + return true; + } + + if (t == null) { + EmptyDisposable.complete(observer); + return true; + } + + ObservableSource<? extends R> r; + + try { + r = Objects.requireNonNull(mapper.apply(t), "The mapper returned a null ObservableSource"); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + EmptyDisposable.error(ex, observer); + return true; + } + + if (r instanceof Supplier) { + R u; + + try { + u = ((Supplier<R>)r).get(); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + EmptyDisposable.error(ex, observer); + return true; + } + + if (u == null) { + EmptyDisposable.complete(observer); + return true; + } + ScalarDisposable<R> sd = new ScalarDisposable<>(observer, u); + observer.onSubscribe(sd); + sd.run(); + } else { + r.subscribe(observer); + } + + return true; + } + return false; + } + + /** + * Maps a scalar value into an Observable and emits its values. + * + * @param <T> the scalar value type + * @param <U> the output value type + * @param value the scalar value to map + * @param mapper the function that gets the scalar value and should return + * an ObservableSource that gets streamed + * @return the new Observable instance + */ + public static <T, U> Observable<U> scalarXMap(T value, + Function<? super T, ? extends ObservableSource<? extends U>> mapper) { + return RxJavaPlugins.onAssembly(new ScalarXMapObservable<>(value, mapper)); + } + + /** + * Maps a scalar value to an ObservableSource and subscribes to it. + * + * @param <T> the scalar value type + * @param <R> the mapped ObservableSource's element type. + */ + static final class ScalarXMapObservable<T, R> extends Observable<R> { + + final T value; + + final Function<? super T, ? extends ObservableSource<? extends R>> mapper; + + ScalarXMapObservable(T value, + Function<? super T, ? extends ObservableSource<? extends R>> mapper) { + this.value = value; + this.mapper = mapper; + } + + @SuppressWarnings("unchecked") + @Override + public void subscribeActual(Observer<? super R> observer) { + ObservableSource<? extends R> other; + try { + other = Objects.requireNonNull(mapper.apply(value), "The mapper returned a null ObservableSource"); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + EmptyDisposable.error(e, observer); + return; + } + if (other instanceof Supplier) { + R u; + + try { + u = ((Supplier<R>)other).get(); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + EmptyDisposable.error(ex, observer); + return; + } + + if (u == null) { + EmptyDisposable.complete(observer); + return; + } + ScalarDisposable<R> sd = new ScalarDisposable<>(observer, u); + observer.onSubscribe(sd); + sd.run(); + } else { + other.subscribe(observer); + } + } + } + + /** + * Represents a Disposable that signals one onNext followed by an onComplete. + * + * @param <T> the value type + */ + public static final class ScalarDisposable<T> + extends AtomicInteger + implements QueueDisposable<T>, Runnable { + + private static final long serialVersionUID = 3880992722410194083L; + + final Observer<? super T> observer; + + final T value; + + static final int START = 0; + static final int FUSED = 1; + static final int ON_NEXT = 2; + static final int ON_COMPLETE = 3; + + public ScalarDisposable(Observer<? super T> observer, T value) { + this.observer = observer; + this.value = value; + } + + @Override + public boolean offer(T value) { + throw new UnsupportedOperationException("Should not be called!"); + } + + @Override + public boolean offer(T v1, T v2) { + throw new UnsupportedOperationException("Should not be called!"); + } + + @Nullable + @Override + public T poll() { + if (get() == FUSED) { + lazySet(ON_COMPLETE); + return value; + } + return null; + } + + @Override + public boolean isEmpty() { + return get() != FUSED; + } + + @Override + public void clear() { + lazySet(ON_COMPLETE); + } + + @Override + public void dispose() { + set(ON_COMPLETE); + } + + @Override + public boolean isDisposed() { + return get() == ON_COMPLETE; + } + + @Override + public int requestFusion(int mode) { + if ((mode & SYNC) != 0) { + lazySet(FUSED); + return SYNC; + } + return NONE; + } + + @Override + public void run() { + if (get() == START && compareAndSet(START, ON_NEXT)) { + observer.onNext(value); + if (get() == ON_NEXT) { + lazySet(ON_COMPLETE); + observer.onComplete(); + } + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableScan.java b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableScan.java new file mode 100644 index 0000000000..59a74661cc --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableScan.java @@ -0,0 +1,116 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.BiFunction; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +import java.util.Objects; + +public final class ObservableScan<T> extends AbstractObservableWithUpstream<T, T> { + final BiFunction<T, T, T> accumulator; + public ObservableScan(ObservableSource<T> source, BiFunction<T, T, T> accumulator) { + super(source); + this.accumulator = accumulator; + } + + @Override + public void subscribeActual(Observer<? super T> t) { + source.subscribe(new ScanObserver<>(t, accumulator)); + } + + static final class ScanObserver<T> implements Observer<T>, Disposable { + final Observer<? super T> downstream; + final BiFunction<T, T, T> accumulator; + + Disposable upstream; + + T value; + + boolean done; + + ScanObserver(Observer<? super T> actual, BiFunction<T, T, T> accumulator) { + this.downstream = actual; + this.accumulator = accumulator; + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.validate(this.upstream, d)) { + this.upstream = d; + downstream.onSubscribe(this); + } + } + + @Override + public void dispose() { + upstream.dispose(); + } + + @Override + public boolean isDisposed() { + return upstream.isDisposed(); + } + + @Override + public void onNext(T t) { + if (done) { + return; + } + final Observer<? super T> a = downstream; + T v = value; + if (v == null) { + value = t; + a.onNext(t); + } else { + T u; + + try { + u = Objects.requireNonNull(accumulator.apply(v, t), "The value returned by the accumulator is null"); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + upstream.dispose(); + onError(e); + return; + } + + value = u; + a.onNext(u); + } + } + + @Override + public void onError(Throwable t) { + if (done) { + RxJavaPlugins.onError(t); + return; + } + done = true; + downstream.onError(t); + } + + @Override + public void onComplete() { + if (done) { + return; + } + done = true; + downstream.onComplete(); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableScanSeed.java b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableScanSeed.java new file mode 100644 index 0000000000..f0e197db7e --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableScanSeed.java @@ -0,0 +1,130 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.disposables.*; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +import java.util.Objects; + +public final class ObservableScanSeed<T, R> extends AbstractObservableWithUpstream<T, R> { + final BiFunction<R, ? super T, R> accumulator; + final Supplier<R> seedSupplier; + + public ObservableScanSeed(ObservableSource<T> source, Supplier<R> seedSupplier, BiFunction<R, ? super T, R> accumulator) { + super(source); + this.accumulator = accumulator; + this.seedSupplier = seedSupplier; + } + + @Override + public void subscribeActual(Observer<? super R> t) { + R r; + + try { + r = Objects.requireNonNull(seedSupplier.get(), "The seed supplied is null"); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + EmptyDisposable.error(e, t); + return; + } + + source.subscribe(new ScanSeedObserver<>(t, accumulator, r)); + } + + static final class ScanSeedObserver<T, R> implements Observer<T>, Disposable { + final Observer<? super R> downstream; + final BiFunction<R, ? super T, R> accumulator; + + R value; + + Disposable upstream; + + boolean done; + + ScanSeedObserver(Observer<? super R> actual, BiFunction<R, ? super T, R> accumulator, R value) { + this.downstream = actual; + this.accumulator = accumulator; + this.value = value; + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.validate(this.upstream, d)) { + this.upstream = d; + + downstream.onSubscribe(this); + + downstream.onNext(value); + } + } + + @Override + public void dispose() { + upstream.dispose(); + } + + @Override + public boolean isDisposed() { + return upstream.isDisposed(); + } + + @Override + public void onNext(T t) { + if (done) { + return; + } + + R v = value; + + R u; + + try { + u = Objects.requireNonNull(accumulator.apply(v, t), "The accumulator returned a null value"); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + upstream.dispose(); + onError(e); + return; + } + + value = u; + + downstream.onNext(u); + } + + @Override + public void onError(Throwable t) { + if (done) { + RxJavaPlugins.onError(t); + return; + } + done = true; + downstream.onError(t); + } + + @Override + public void onComplete() { + if (done) { + return; + } + done = true; + downstream.onComplete(); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableSequenceEqual.java b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableSequenceEqual.java new file mode 100644 index 0000000000..e79f992243 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableSequenceEqual.java @@ -0,0 +1,256 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import java.util.concurrent.atomic.AtomicInteger; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.BiPredicate; +import io.reactivex.rxjava3.internal.disposables.ArrayCompositeDisposable; +import io.reactivex.rxjava3.operators.SpscLinkedArrayQueue; + +public final class ObservableSequenceEqual<T> extends Observable<Boolean> { + final ObservableSource<? extends T> first; + final ObservableSource<? extends T> second; + final BiPredicate<? super T, ? super T> comparer; + final int bufferSize; + + public ObservableSequenceEqual(ObservableSource<? extends T> first, ObservableSource<? extends T> second, + BiPredicate<? super T, ? super T> comparer, int bufferSize) { + this.first = first; + this.second = second; + this.comparer = comparer; + this.bufferSize = bufferSize; + } + + @Override + public void subscribeActual(Observer<? super Boolean> observer) { + EqualCoordinator<T> ec = new EqualCoordinator<>(observer, bufferSize, first, second, comparer); + observer.onSubscribe(ec); + ec.subscribe(); + } + + static final class EqualCoordinator<T> extends AtomicInteger implements Disposable { + + private static final long serialVersionUID = -6178010334400373240L; + final Observer<? super Boolean> downstream; + final BiPredicate<? super T, ? super T> comparer; + final ArrayCompositeDisposable resources; + final ObservableSource<? extends T> first; + final ObservableSource<? extends T> second; + final EqualObserver<T>[] observers; + + volatile boolean cancelled; + + T v1; + + T v2; + + EqualCoordinator(Observer<? super Boolean> actual, int bufferSize, + ObservableSource<? extends T> first, ObservableSource<? extends T> second, + BiPredicate<? super T, ? super T> comparer) { + this.downstream = actual; + this.first = first; + this.second = second; + this.comparer = comparer; + @SuppressWarnings("unchecked") + EqualObserver<T>[] as = new EqualObserver[2]; + this.observers = as; + as[0] = new EqualObserver<>(this, 0, bufferSize); + as[1] = new EqualObserver<>(this, 1, bufferSize); + this.resources = new ArrayCompositeDisposable(2); + } + + boolean setDisposable(Disposable d, int index) { + return resources.setResource(index, d); + } + + void subscribe() { + EqualObserver<T>[] as = observers; + first.subscribe(as[0]); + second.subscribe(as[1]); + } + + @Override + public void dispose() { + if (!cancelled) { + cancelled = true; + resources.dispose(); + + if (getAndIncrement() == 0) { + EqualObserver<T>[] as = observers; + as[0].queue.clear(); + as[1].queue.clear(); + } + } + } + + @Override + public boolean isDisposed() { + return cancelled; + } + + void cancel(SpscLinkedArrayQueue<T> q1, SpscLinkedArrayQueue<T> q2) { + cancelled = true; + q1.clear(); + q2.clear(); + } + + void drain() { + if (getAndIncrement() != 0) { + return; + } + + int missed = 1; + EqualObserver<T>[] as = observers; + + final EqualObserver<T> observer1 = as[0]; + final SpscLinkedArrayQueue<T> q1 = observer1.queue; + final EqualObserver<T> observer2 = as[1]; + final SpscLinkedArrayQueue<T> q2 = observer2.queue; + + for (;;) { + + for (;;) { + if (cancelled) { + q1.clear(); + q2.clear(); + return; + } + + boolean d1 = observer1.done; + + if (d1) { + Throwable e = observer1.error; + if (e != null) { + cancel(q1, q2); + + downstream.onError(e); + return; + } + } + + boolean d2 = observer2.done; + if (d2) { + Throwable e = observer2.error; + if (e != null) { + cancel(q1, q2); + + downstream.onError(e); + return; + } + } + + if (v1 == null) { + v1 = q1.poll(); + } + boolean e1 = v1 == null; + + if (v2 == null) { + v2 = q2.poll(); + } + boolean e2 = v2 == null; + + if (d1 && d2 && e1 && e2) { + downstream.onNext(true); + downstream.onComplete(); + return; + } + if ((d1 && d2) && (e1 != e2)) { + cancel(q1, q2); + + downstream.onNext(false); + downstream.onComplete(); + return; + } + + if (!e1 && !e2) { + boolean c; + + try { + c = comparer.test(v1, v2); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + cancel(q1, q2); + + downstream.onError(ex); + return; + } + + if (!c) { + cancel(q1, q2); + + downstream.onNext(false); + downstream.onComplete(); + return; + } + + v1 = null; + v2 = null; + } + + if (e1 || e2) { + break; + } + } + + missed = addAndGet(-missed); + if (missed == 0) { + break; + } + } + } + } + + static final class EqualObserver<T> implements Observer<T> { + final EqualCoordinator<T> parent; + final SpscLinkedArrayQueue<T> queue; + final int index; + + volatile boolean done; + Throwable error; + + EqualObserver(EqualCoordinator<T> parent, int index, int bufferSize) { + this.parent = parent; + this.index = index; + this.queue = new SpscLinkedArrayQueue<>(bufferSize); + } + + @Override + public void onSubscribe(Disposable d) { + parent.setDisposable(d, index); + } + + @Override + public void onNext(T t) { + queue.offer(t); + parent.drain(); + } + + @Override + public void onError(Throwable t) { + error = t; + done = true; + parent.drain(); + } + + @Override + public void onComplete() { + done = true; + parent.drain(); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableSequenceEqualSingle.java b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableSequenceEqualSingle.java new file mode 100644 index 0000000000..9b51815ce0 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableSequenceEqualSingle.java @@ -0,0 +1,260 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import java.util.concurrent.atomic.AtomicInteger; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.BiPredicate; +import io.reactivex.rxjava3.internal.disposables.ArrayCompositeDisposable; +import io.reactivex.rxjava3.internal.fuseable.FuseToObservable; +import io.reactivex.rxjava3.operators.SpscLinkedArrayQueue; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +public final class ObservableSequenceEqualSingle<T> extends Single<Boolean> implements FuseToObservable<Boolean> { + final ObservableSource<? extends T> first; + final ObservableSource<? extends T> second; + final BiPredicate<? super T, ? super T> comparer; + final int bufferSize; + + public ObservableSequenceEqualSingle(ObservableSource<? extends T> first, ObservableSource<? extends T> second, + BiPredicate<? super T, ? super T> comparer, int bufferSize) { + this.first = first; + this.second = second; + this.comparer = comparer; + this.bufferSize = bufferSize; + } + + @Override + public void subscribeActual(SingleObserver<? super Boolean> observer) { + EqualCoordinator<T> ec = new EqualCoordinator<>(observer, bufferSize, first, second, comparer); + observer.onSubscribe(ec); + ec.subscribe(); + } + + @Override + public Observable<Boolean> fuseToObservable() { + return RxJavaPlugins.onAssembly(new ObservableSequenceEqual<>(first, second, comparer, bufferSize)); + } + + static final class EqualCoordinator<T> extends AtomicInteger implements Disposable { + + private static final long serialVersionUID = -6178010334400373240L; + final SingleObserver<? super Boolean> downstream; + final BiPredicate<? super T, ? super T> comparer; + final ArrayCompositeDisposable resources; + final ObservableSource<? extends T> first; + final ObservableSource<? extends T> second; + final EqualObserver<T>[] observers; + + volatile boolean cancelled; + + T v1; + + T v2; + + EqualCoordinator(SingleObserver<? super Boolean> actual, int bufferSize, + ObservableSource<? extends T> first, ObservableSource<? extends T> second, + BiPredicate<? super T, ? super T> comparer) { + this.downstream = actual; + this.first = first; + this.second = second; + this.comparer = comparer; + @SuppressWarnings("unchecked") + EqualObserver<T>[] as = new EqualObserver[2]; + this.observers = as; + as[0] = new EqualObserver<>(this, 0, bufferSize); + as[1] = new EqualObserver<>(this, 1, bufferSize); + this.resources = new ArrayCompositeDisposable(2); + } + + boolean setDisposable(Disposable d, int index) { + return resources.setResource(index, d); + } + + void subscribe() { + EqualObserver<T>[] as = observers; + first.subscribe(as[0]); + second.subscribe(as[1]); + } + + @Override + public void dispose() { + if (!cancelled) { + cancelled = true; + resources.dispose(); + + if (getAndIncrement() == 0) { + EqualObserver<T>[] as = observers; + as[0].queue.clear(); + as[1].queue.clear(); + } + } + } + + @Override + public boolean isDisposed() { + return cancelled; + } + + void cancel(SpscLinkedArrayQueue<T> q1, SpscLinkedArrayQueue<T> q2) { + cancelled = true; + q1.clear(); + q2.clear(); + } + + void drain() { + if (getAndIncrement() != 0) { + return; + } + + int missed = 1; + EqualObserver<T>[] as = observers; + + final EqualObserver<T> observer1 = as[0]; + final SpscLinkedArrayQueue<T> q1 = observer1.queue; + final EqualObserver<T> observer2 = as[1]; + final SpscLinkedArrayQueue<T> q2 = observer2.queue; + + for (;;) { + + for (;;) { + if (cancelled) { + q1.clear(); + q2.clear(); + return; + } + + boolean d1 = observer1.done; + + if (d1) { + Throwable e = observer1.error; + if (e != null) { + cancel(q1, q2); + + downstream.onError(e); + return; + } + } + + boolean d2 = observer2.done; + if (d2) { + Throwable e = observer2.error; + if (e != null) { + cancel(q1, q2); + + downstream.onError(e); + return; + } + } + + if (v1 == null) { + v1 = q1.poll(); + } + boolean e1 = v1 == null; + + if (v2 == null) { + v2 = q2.poll(); + } + boolean e2 = v2 == null; + + if (d1 && d2 && e1 && e2) { + downstream.onSuccess(true); + return; + } + if ((d1 && d2) && (e1 != e2)) { + cancel(q1, q2); + + downstream.onSuccess(false); + return; + } + + if (!e1 && !e2) { + boolean c; + + try { + c = comparer.test(v1, v2); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + cancel(q1, q2); + + downstream.onError(ex); + return; + } + + if (!c) { + cancel(q1, q2); + + downstream.onSuccess(false); + return; + } + + v1 = null; + v2 = null; + } + + if (e1 || e2) { + break; + } + } + + missed = addAndGet(-missed); + if (missed == 0) { + break; + } + } + } + } + + static final class EqualObserver<T> implements Observer<T> { + final EqualCoordinator<T> parent; + final SpscLinkedArrayQueue<T> queue; + final int index; + + volatile boolean done; + Throwable error; + + EqualObserver(EqualCoordinator<T> parent, int index, int bufferSize) { + this.parent = parent; + this.index = index; + this.queue = new SpscLinkedArrayQueue<>(bufferSize); + } + + @Override + public void onSubscribe(Disposable d) { + parent.setDisposable(d, index); + } + + @Override + public void onNext(T t) { + queue.offer(t); + parent.drain(); + } + + @Override + public void onError(Throwable t) { + error = t; + done = true; + parent.drain(); + } + + @Override + public void onComplete() { + done = true; + parent.drain(); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableSerialized.java b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableSerialized.java new file mode 100644 index 0000000000..c57d0d9a24 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableSerialized.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.observers.SerializedObserver; + +public final class ObservableSerialized<T> extends AbstractObservableWithUpstream<T, T> { + public ObservableSerialized(Observable<T> upstream) { + super(upstream); + } + + @Override + protected void subscribeActual(Observer<? super T> observer) { + source.subscribe(new SerializedObserver<>(observer)); + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableSingleMaybe.java b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableSingleMaybe.java new file mode 100644 index 0000000000..b4068e834d --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableSingleMaybe.java @@ -0,0 +1,104 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +public final class ObservableSingleMaybe<T> extends Maybe<T> { + + final ObservableSource<T> source; + + public ObservableSingleMaybe(ObservableSource<T> source) { + this.source = source; + } + + @Override + public void subscribeActual(MaybeObserver<? super T> t) { + source.subscribe(new SingleElementObserver<>(t)); + } + + static final class SingleElementObserver<T> implements Observer<T>, Disposable { + final MaybeObserver<? super T> downstream; + + Disposable upstream; + + T value; + + boolean done; + + SingleElementObserver(MaybeObserver<? super T> downstream) { + this.downstream = downstream; + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.validate(this.upstream, d)) { + this.upstream = d; + downstream.onSubscribe(this); + } + } + + @Override + public void dispose() { + upstream.dispose(); + } + + @Override + public boolean isDisposed() { + return upstream.isDisposed(); + } + + @Override + public void onNext(T t) { + if (done) { + return; + } + if (value != null) { + done = true; + upstream.dispose(); + downstream.onError(new IllegalArgumentException("Sequence contains more than one element!")); + return; + } + value = t; + } + + @Override + public void onError(Throwable t) { + if (done) { + RxJavaPlugins.onError(t); + return; + } + done = true; + downstream.onError(t); + } + + @Override + public void onComplete() { + if (done) { + return; + } + done = true; + T v = value; + value = null; + if (v == null) { + downstream.onComplete(); + } else { + downstream.onSuccess(v); + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableSingleSingle.java b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableSingleSingle.java new file mode 100644 index 0000000000..dd8032a400 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableSingleSingle.java @@ -0,0 +1,116 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import java.util.NoSuchElementException; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +public final class ObservableSingleSingle<T> extends Single<T> { + + final ObservableSource<? extends T> source; + + final T defaultValue; + + public ObservableSingleSingle(ObservableSource<? extends T> source, T defaultValue) { + this.source = source; + this.defaultValue = defaultValue; + } + + @Override + public void subscribeActual(SingleObserver<? super T> t) { + source.subscribe(new SingleElementObserver<>(t, defaultValue)); + } + + static final class SingleElementObserver<T> implements Observer<T>, Disposable { + final SingleObserver<? super T> downstream; + + final T defaultValue; + + Disposable upstream; + + T value; + + boolean done; + + SingleElementObserver(SingleObserver<? super T> actual, T defaultValue) { + this.downstream = actual; + this.defaultValue = defaultValue; + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.validate(this.upstream, d)) { + this.upstream = d; + downstream.onSubscribe(this); + } + } + + @Override + public void dispose() { + upstream.dispose(); + } + + @Override + public boolean isDisposed() { + return upstream.isDisposed(); + } + + @Override + public void onNext(T t) { + if (done) { + return; + } + if (value != null) { + done = true; + upstream.dispose(); + downstream.onError(new IllegalArgumentException("Sequence contains more than one element!")); + return; + } + value = t; + } + + @Override + public void onError(Throwable t) { + if (done) { + RxJavaPlugins.onError(t); + return; + } + done = true; + downstream.onError(t); + } + + @Override + public void onComplete() { + if (done) { + return; + } + done = true; + T v = value; + value = null; + if (v == null) { + v = defaultValue; + } + + if (v != null) { + downstream.onSuccess(v); + } else { + downstream.onError(new NoSuchElementException()); + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableSkip.java b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableSkip.java new file mode 100644 index 0000000000..0fe5787c1b --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableSkip.java @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; + +public final class ObservableSkip<T> extends AbstractObservableWithUpstream<T, T> { + final long n; + public ObservableSkip(ObservableSource<T> source, long n) { + super(source); + this.n = n; + } + + @Override + public void subscribeActual(Observer<? super T> observer) { + source.subscribe(new SkipObserver<>(observer, n)); + } + + static final class SkipObserver<T> implements Observer<T>, Disposable { + final Observer<? super T> downstream; + long remaining; + + Disposable upstream; + + SkipObserver(Observer<? super T> actual, long n) { + this.downstream = actual; + this.remaining = n; + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.validate(this.upstream, d)) { + this.upstream = d; + downstream.onSubscribe(this); + } + } + + @Override + public void onNext(T t) { + if (remaining != 0L) { + remaining--; + } else { + downstream.onNext(t); + } + } + + @Override + public void onError(Throwable t) { + downstream.onError(t); + } + + @Override + public void onComplete() { + downstream.onComplete(); + } + + @Override + public void dispose() { + upstream.dispose(); + } + + @Override + public boolean isDisposed() { + return upstream.isDisposed(); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableSkipLast.java b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableSkipLast.java new file mode 100644 index 0000000000..68fabb3ac7 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableSkipLast.java @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import java.util.ArrayDeque; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; + +public final class ObservableSkipLast<T> extends AbstractObservableWithUpstream<T, T> { + final int skip; + + public ObservableSkipLast(ObservableSource<T> source, int skip) { + super(source); + this.skip = skip; + } + + @Override + public void subscribeActual(Observer<? super T> observer) { + source.subscribe(new SkipLastObserver<>(observer, skip)); + } + + static final class SkipLastObserver<T> extends ArrayDeque<T> implements Observer<T>, Disposable { + + private static final long serialVersionUID = -3807491841935125653L; + final Observer<? super T> downstream; + final int skip; + + Disposable upstream; + + SkipLastObserver(Observer<? super T> actual, int skip) { + super(skip); + this.downstream = actual; + this.skip = skip; + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.validate(this.upstream, d)) { + this.upstream = d; + downstream.onSubscribe(this); + } + } + + @Override + public void dispose() { + upstream.dispose(); + } + + @Override + public boolean isDisposed() { + return upstream.isDisposed(); + } + + @Override + public void onNext(T t) { + if (skip == size()) { + downstream.onNext(poll()); + } + offer(t); + } + + @Override + public void onError(Throwable t) { + downstream.onError(t); + } + + @Override + public void onComplete() { + downstream.onComplete(); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableSkipLastTimed.java b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableSkipLastTimed.java new file mode 100644 index 0000000000..7cc6f34f24 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableSkipLastTimed.java @@ -0,0 +1,198 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; +import io.reactivex.rxjava3.operators.SpscLinkedArrayQueue; + +public final class ObservableSkipLastTimed<T> extends AbstractObservableWithUpstream<T, T> { + final long time; + final TimeUnit unit; + final Scheduler scheduler; + final int bufferSize; + final boolean delayError; + + public ObservableSkipLastTimed(ObservableSource<T> source, + long time, TimeUnit unit, Scheduler scheduler, int bufferSize, boolean delayError) { + super(source); + this.time = time; + this.unit = unit; + this.scheduler = scheduler; + this.bufferSize = bufferSize; + this.delayError = delayError; + } + + @Override + public void subscribeActual(Observer<? super T> t) { + source.subscribe(new SkipLastTimedObserver<>(t, time, unit, scheduler, bufferSize, delayError)); + } + + static final class SkipLastTimedObserver<T> extends AtomicInteger implements Observer<T>, Disposable { + + private static final long serialVersionUID = -5677354903406201275L; + final Observer<? super T> downstream; + final long time; + final TimeUnit unit; + final Scheduler scheduler; + final SpscLinkedArrayQueue<Object> queue; + final boolean delayError; + + Disposable upstream; + + volatile boolean cancelled; + + volatile boolean done; + Throwable error; + + SkipLastTimedObserver(Observer<? super T> actual, long time, TimeUnit unit, Scheduler scheduler, int bufferSize, boolean delayError) { + this.downstream = actual; + this.time = time; + this.unit = unit; + this.scheduler = scheduler; + this.queue = new SpscLinkedArrayQueue<>(bufferSize); + this.delayError = delayError; + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.validate(this.upstream, d)) { + this.upstream = d; + downstream.onSubscribe(this); + } + } + + @Override + public void onNext(T t) { + final SpscLinkedArrayQueue<Object> q = queue; + + long now = scheduler.now(unit); + + q.offer(now, t); + + drain(); + } + + @Override + public void onError(Throwable t) { + error = t; + done = true; + drain(); + } + + @Override + public void onComplete() { + done = true; + drain(); + } + + @Override + public void dispose() { + if (!cancelled) { + cancelled = true; + upstream.dispose(); + + if (getAndIncrement() == 0) { + queue.clear(); + } + } + } + + @Override + public boolean isDisposed() { + return cancelled; + } + + void drain() { + if (getAndIncrement() != 0) { + return; + } + + int missed = 1; + + final Observer<? super T> a = downstream; + final SpscLinkedArrayQueue<Object> q = queue; + final boolean delayError = this.delayError; + final TimeUnit unit = this.unit; + final Scheduler scheduler = this.scheduler; + final long time = this.time; + + for (;;) { + + for (;;) { + if (cancelled) { + queue.clear(); + return; + } + + boolean d = done; + + Long ts = (Long)q.peek(); + + boolean empty = ts == null; + + long now = scheduler.now(unit); + + if (!empty && ts > now - time) { + empty = true; + } + + if (d) { + if (delayError) { + if (empty) { + Throwable e = error; + if (e != null) { + a.onError(e); + } else { + a.onComplete(); + } + return; + } + } else { + Throwable e = error; + if (e != null) { + queue.clear(); + a.onError(e); + return; + } else + if (empty) { + a.onComplete(); + return; + } + } + } + + if (empty) { + break; + } + + q.poll(); + @SuppressWarnings("unchecked") + T v = (T)q.poll(); + + a.onNext(v); + } + + missed = addAndGet(-missed); + if (missed == 0) { + break; + } + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableSkipUntil.java b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableSkipUntil.java new file mode 100644 index 0000000000..87535fabf2 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableSkipUntil.java @@ -0,0 +1,128 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.disposables.*; +import io.reactivex.rxjava3.observers.SerializedObserver; + +public final class ObservableSkipUntil<T, U> extends AbstractObservableWithUpstream<T, T> { + final ObservableSource<U> other; + public ObservableSkipUntil(ObservableSource<T> source, ObservableSource<U> other) { + super(source); + this.other = other; + } + + @Override + public void subscribeActual(Observer<? super T> child) { + + final SerializedObserver<T> serial = new SerializedObserver<>(child); + + final ArrayCompositeDisposable frc = new ArrayCompositeDisposable(2); + + serial.onSubscribe(frc); + + final SkipUntilObserver<T> sus = new SkipUntilObserver<>(serial, frc); + + other.subscribe(new SkipUntil(frc, sus, serial)); + + source.subscribe(sus); + } + + static final class SkipUntilObserver<T> implements Observer<T> { + + final Observer<? super T> downstream; + final ArrayCompositeDisposable frc; + + Disposable upstream; + + volatile boolean notSkipping; + boolean notSkippingLocal; + + SkipUntilObserver(Observer<? super T> actual, ArrayCompositeDisposable frc) { + this.downstream = actual; + this.frc = frc; + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.validate(this.upstream, d)) { + this.upstream = d; + frc.setResource(0, d); + } + } + + @Override + public void onNext(T t) { + if (notSkippingLocal) { + downstream.onNext(t); + } else + if (notSkipping) { + notSkippingLocal = true; + downstream.onNext(t); + } + } + + @Override + public void onError(Throwable t) { + frc.dispose(); + downstream.onError(t); + } + + @Override + public void onComplete() { + frc.dispose(); + downstream.onComplete(); + } + } + + final class SkipUntil implements Observer<U> { + final ArrayCompositeDisposable frc; + final SkipUntilObserver<T> sus; + final SerializedObserver<T> serial; + Disposable upstream; + + SkipUntil(ArrayCompositeDisposable frc, SkipUntilObserver<T> sus, SerializedObserver<T> serial) { + this.frc = frc; + this.sus = sus; + this.serial = serial; + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.validate(this.upstream, d)) { + this.upstream = d; + frc.setResource(1, d); + } + } + + @Override + public void onNext(U t) { + upstream.dispose(); + sus.notSkipping = true; + } + + @Override + public void onError(Throwable t) { + frc.dispose(); + serial.onError(t); + } + + @Override + public void onComplete() { + sus.notSkipping = true; + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableSkipWhile.java b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableSkipWhile.java new file mode 100644 index 0000000000..1614b0b472 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableSkipWhile.java @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.Predicate; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; + +public final class ObservableSkipWhile<T> extends AbstractObservableWithUpstream<T, T> { + final Predicate<? super T> predicate; + public ObservableSkipWhile(ObservableSource<T> source, Predicate<? super T> predicate) { + super(source); + this.predicate = predicate; + } + + @Override + public void subscribeActual(Observer<? super T> observer) { + source.subscribe(new SkipWhileObserver<>(observer, predicate)); + } + + static final class SkipWhileObserver<T> implements Observer<T>, Disposable { + final Observer<? super T> downstream; + final Predicate<? super T> predicate; + Disposable upstream; + boolean notSkipping; + SkipWhileObserver(Observer<? super T> actual, Predicate<? super T> predicate) { + this.downstream = actual; + this.predicate = predicate; + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.validate(this.upstream, d)) { + this.upstream = d; + downstream.onSubscribe(this); + } + } + + @Override + public void dispose() { + upstream.dispose(); + } + + @Override + public boolean isDisposed() { + return upstream.isDisposed(); + } + + @Override + public void onNext(T t) { + if (notSkipping) { + downstream.onNext(t); + } else { + boolean b; + try { + b = predicate.test(t); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + upstream.dispose(); + downstream.onError(e); + return; + } + if (!b) { + notSkipping = true; + downstream.onNext(t); + } + } + } + + @Override + public void onError(Throwable t) { + downstream.onError(t); + } + + @Override + public void onComplete() { + downstream.onComplete(); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableSubscribeOn.java b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableSubscribeOn.java new file mode 100644 index 0000000000..5eaeaa5035 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableSubscribeOn.java @@ -0,0 +1,99 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import java.util.concurrent.atomic.AtomicReference; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; + +public final class ObservableSubscribeOn<T> extends AbstractObservableWithUpstream<T, T> { + final Scheduler scheduler; + + public ObservableSubscribeOn(ObservableSource<T> source, Scheduler scheduler) { + super(source); + this.scheduler = scheduler; + } + + @Override + public void subscribeActual(final Observer<? super T> observer) { + final SubscribeOnObserver<T> parent = new SubscribeOnObserver<>(observer); + + observer.onSubscribe(parent); + + parent.setDisposable(scheduler.scheduleDirect(new SubscribeTask(parent))); + } + + static final class SubscribeOnObserver<T> extends AtomicReference<Disposable> implements Observer<T>, Disposable { + + private static final long serialVersionUID = 8094547886072529208L; + final Observer<? super T> downstream; + + final AtomicReference<Disposable> upstream; + + SubscribeOnObserver(Observer<? super T> downstream) { + this.downstream = downstream; + this.upstream = new AtomicReference<>(); + } + + @Override + public void onSubscribe(Disposable d) { + DisposableHelper.setOnce(this.upstream, d); + } + + @Override + public void onNext(T t) { + downstream.onNext(t); + } + + @Override + public void onError(Throwable t) { + downstream.onError(t); + } + + @Override + public void onComplete() { + downstream.onComplete(); + } + + @Override + public void dispose() { + DisposableHelper.dispose(upstream); + DisposableHelper.dispose(this); + } + + @Override + public boolean isDisposed() { + return DisposableHelper.isDisposed(get()); + } + + void setDisposable(Disposable d) { + DisposableHelper.setOnce(this, d); + } + } + + final class SubscribeTask implements Runnable { + private final SubscribeOnObserver<T> parent; + + SubscribeTask(SubscribeOnObserver<T> parent) { + this.parent = parent; + } + + @Override + public void run() { + source.subscribe(parent); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableSwitchIfEmpty.java b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableSwitchIfEmpty.java new file mode 100644 index 0000000000..6aa1d84ecf --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableSwitchIfEmpty.java @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.disposables.SequentialDisposable; + +public final class ObservableSwitchIfEmpty<T> extends AbstractObservableWithUpstream<T, T> { + final ObservableSource<? extends T> other; + public ObservableSwitchIfEmpty(ObservableSource<T> source, ObservableSource<? extends T> other) { + super(source); + this.other = other; + } + + @Override + public void subscribeActual(Observer<? super T> t) { + SwitchIfEmptyObserver<T> parent = new SwitchIfEmptyObserver<>(t, other); + t.onSubscribe(parent.arbiter); + source.subscribe(parent); + } + + static final class SwitchIfEmptyObserver<T> implements Observer<T> { + final Observer<? super T> downstream; + final ObservableSource<? extends T> other; + final SequentialDisposable arbiter; + + boolean empty; + + SwitchIfEmptyObserver(Observer<? super T> actual, ObservableSource<? extends T> other) { + this.downstream = actual; + this.other = other; + this.empty = true; + this.arbiter = new SequentialDisposable(); + } + + @Override + public void onSubscribe(Disposable d) { + arbiter.update(d); + } + + @Override + public void onNext(T t) { + if (empty) { + empty = false; + } + downstream.onNext(t); + } + + @Override + public void onError(Throwable t) { + downstream.onError(t); + } + + @Override + public void onComplete() { + if (empty) { + empty = false; + other.subscribe(this); + } else { + downstream.onComplete(); + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableSwitchMap.java b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableSwitchMap.java new file mode 100644 index 0000000000..3e0558aacf --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableSwitchMap.java @@ -0,0 +1,378 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import java.util.Objects; +import java.util.concurrent.atomic.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; +import io.reactivex.rxjava3.internal.util.AtomicThrowable; +import io.reactivex.rxjava3.operators.QueueDisposable; +import io.reactivex.rxjava3.operators.SimpleQueue; +import io.reactivex.rxjava3.operators.SpscLinkedArrayQueue; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +public final class ObservableSwitchMap<T, R> extends AbstractObservableWithUpstream<T, R> { + final Function<? super T, ? extends ObservableSource<? extends R>> mapper; + final int bufferSize; + + final boolean delayErrors; + + public ObservableSwitchMap(ObservableSource<T> source, + Function<? super T, ? extends ObservableSource<? extends R>> mapper, int bufferSize, + boolean delayErrors) { + super(source); + this.mapper = mapper; + this.bufferSize = bufferSize; + this.delayErrors = delayErrors; + } + + @Override + public void subscribeActual(Observer<? super R> t) { + + if (ObservableScalarXMap.tryScalarXMapSubscribe(source, t, mapper)) { + return; + } + + source.subscribe(new SwitchMapObserver<>(t, mapper, bufferSize, delayErrors)); + } + + static final class SwitchMapObserver<T, R> extends AtomicInteger implements Observer<T>, Disposable { + + private static final long serialVersionUID = -3491074160481096299L; + final Observer<? super R> downstream; + final Function<? super T, ? extends ObservableSource<? extends R>> mapper; + final int bufferSize; + + final boolean delayErrors; + + final AtomicThrowable errors; + + volatile boolean done; + + volatile boolean cancelled; + + Disposable upstream; + + final AtomicReference<SwitchMapInnerObserver<T, R>> active = new AtomicReference<>(); + + static final SwitchMapInnerObserver<Object, Object> CANCELLED; + static { + CANCELLED = new SwitchMapInnerObserver<>(null, -1L, 1); + CANCELLED.cancel(); + } + + volatile long unique; + + SwitchMapObserver(Observer<? super R> actual, + Function<? super T, ? extends ObservableSource<? extends R>> mapper, int bufferSize, + boolean delayErrors) { + this.downstream = actual; + this.mapper = mapper; + this.bufferSize = bufferSize; + this.delayErrors = delayErrors; + this.errors = new AtomicThrowable(); + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.validate(this.upstream, d)) { + this.upstream = d; + downstream.onSubscribe(this); + } + } + + @Override + public void onNext(T t) { + long c = unique + 1; + unique = c; + + SwitchMapInnerObserver<T, R> inner = active.get(); + if (inner != null) { + inner.cancel(); + } + + ObservableSource<? extends R> p; + try { + p = Objects.requireNonNull(mapper.apply(t), "The ObservableSource returned is null"); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + upstream.dispose(); + onError(e); + return; + } + + SwitchMapInnerObserver<T, R> nextInner = new SwitchMapInnerObserver<>(this, c, bufferSize); + + for (;;) { + inner = active.get(); + if (inner == CANCELLED) { + break; + } + if (active.compareAndSet(inner, nextInner)) { + p.subscribe(nextInner); + break; + } + } + } + + @Override + public void onError(Throwable t) { + if (!done && errors.tryAddThrowable(t)) { + if (!delayErrors) { + disposeInner(); + } + done = true; + drain(); + } else { + RxJavaPlugins.onError(t); + } + } + + @Override + public void onComplete() { + if (!done) { + done = true; + drain(); + } + } + + @Override + public void dispose() { + if (!cancelled) { + cancelled = true; + upstream.dispose(); + disposeInner(); + + errors.tryTerminateAndReport(); + } + } + + @Override + public boolean isDisposed() { + return cancelled; + } + + @SuppressWarnings("unchecked") + void disposeInner() { + SwitchMapInnerObserver<T, R> a = active.getAndSet((SwitchMapInnerObserver<T, R>)CANCELLED); + if (a != null) { + a.cancel(); + } + } + + void drain() { + if (getAndIncrement() != 0) { + return; + } + + final Observer<? super R> a = downstream; + final AtomicReference<SwitchMapInnerObserver<T, R>> active = this.active; + final boolean delayErrors = this.delayErrors; + + int missing = 1; + + for (;;) { + + if (cancelled) { + return; + } + + if (done) { + boolean empty = active.get() == null; + if (delayErrors) { + if (empty) { + Throwable ex = errors.get(); + if (ex != null) { + a.onError(ex); + } else { + a.onComplete(); + } + return; + } + } else { + Throwable ex = errors.get(); + if (ex != null) { + errors.tryTerminateConsumer(a); + return; + } + if (empty) { + a.onComplete(); + return; + } + } + } + + SwitchMapInnerObserver<T, R> inner = active.get(); + + if (inner != null) { + SimpleQueue<R> q = inner.queue; + + if (q != null) { + + boolean retry = false; + + for (;;) { + if (cancelled) { + return; + } + if (inner != active.get()) { + retry = true; + break; + } + + if (!delayErrors) { + Throwable ex = errors.get(); + if (ex != null) { + errors.tryTerminateConsumer(a); + return; + } + } + + boolean d = inner.done; + R v; + + try { + v = q.poll(); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + errors.tryAddThrowableOrReport(ex); + active.compareAndSet(inner, null); + if (!delayErrors) { + disposeInner(); + upstream.dispose(); + done = true; + } else { + inner.cancel(); + } + v = null; + retry = true; + } + boolean empty = v == null; + + if (d && empty) { + active.compareAndSet(inner, null); + retry = true; + break; + } + + if (empty) { + break; + } + + a.onNext(v); + } + + if (retry) { + continue; + } + } + } + + missing = addAndGet(-missing); + if (missing == 0) { + break; + } + } + } + + void innerError(SwitchMapInnerObserver<T, R> inner, Throwable ex) { + if (inner.index == unique && errors.tryAddThrowable(ex)) { + if (!delayErrors) { + upstream.dispose(); + done = true; + } + inner.done = true; + drain(); + } else { + RxJavaPlugins.onError(ex); + } + } + } + + static final class SwitchMapInnerObserver<T, R> extends AtomicReference<Disposable> implements Observer<R> { + + private static final long serialVersionUID = 3837284832786408377L; + final SwitchMapObserver<T, R> parent; + final long index; + + final int bufferSize; + + volatile SimpleQueue<R> queue; + + volatile boolean done; + + SwitchMapInnerObserver(SwitchMapObserver<T, R> parent, long index, int bufferSize) { + this.parent = parent; + this.index = index; + this.bufferSize = bufferSize; + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.setOnce(this, d)) { + if (d instanceof QueueDisposable) { + @SuppressWarnings("unchecked") + QueueDisposable<R> qd = (QueueDisposable<R>) d; + + int m = qd.requestFusion(QueueDisposable.ANY | QueueDisposable.BOUNDARY); + if (m == QueueDisposable.SYNC) { + queue = qd; + done = true; + parent.drain(); + return; + } + if (m == QueueDisposable.ASYNC) { + queue = qd; + return; + } + } + + queue = new SpscLinkedArrayQueue<>(bufferSize); + } + } + + @Override + public void onNext(R t) { + SimpleQueue<R> q = queue; + if (index == parent.unique && q != null) { + if (t != null) { + q.offer(t); + } + parent.drain(); + } + } + + @Override + public void onError(Throwable t) { + parent.innerError(this, t); + } + + @Override + public void onComplete() { + if (index == parent.unique) { + done = true; + parent.drain(); + } + } + + public void cancel() { + DisposableHelper.dispose(this); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableTake.java b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableTake.java new file mode 100644 index 0000000000..8cc73b1b56 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableTake.java @@ -0,0 +1,102 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.disposables.*; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +public final class ObservableTake<T> extends AbstractObservableWithUpstream<T, T> { + final long limit; + public ObservableTake(ObservableSource<T> source, long limit) { + super(source); + this.limit = limit; + } + + @Override + protected void subscribeActual(Observer<? super T> observer) { + source.subscribe(new TakeObserver<>(observer, limit)); + } + + static final class TakeObserver<T> implements Observer<T>, Disposable { + final Observer<? super T> downstream; + + boolean done; + + Disposable upstream; + + long remaining; + TakeObserver(Observer<? super T> actual, long limit) { + this.downstream = actual; + this.remaining = limit; + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.validate(this.upstream, d)) { + upstream = d; + if (remaining == 0) { + done = true; + d.dispose(); + EmptyDisposable.complete(downstream); + } else { + downstream.onSubscribe(this); + } + } + } + + @Override + public void onNext(T t) { + if (!done && remaining-- > 0) { + boolean stop = remaining == 0; + downstream.onNext(t); + if (stop) { + onComplete(); + } + } + } + + @Override + public void onError(Throwable t) { + if (done) { + RxJavaPlugins.onError(t); + return; + } + + done = true; + upstream.dispose(); + downstream.onError(t); + } + + @Override + public void onComplete() { + if (!done) { + done = true; + upstream.dispose(); + downstream.onComplete(); + } + } + + @Override + public void dispose() { + upstream.dispose(); + } + + @Override + public boolean isDisposed() { + return upstream.isDisposed(); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableTakeLast.java b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableTakeLast.java new file mode 100644 index 0000000000..b0b04c141d --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableTakeLast.java @@ -0,0 +1,100 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import java.util.ArrayDeque; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; + +public final class ObservableTakeLast<T> extends AbstractObservableWithUpstream<T, T> { + final int count; + + public ObservableTakeLast(ObservableSource<T> source, int count) { + super(source); + this.count = count; + } + + @Override + public void subscribeActual(Observer<? super T> t) { + source.subscribe(new TakeLastObserver<>(t, count)); + } + + static final class TakeLastObserver<T> extends ArrayDeque<T> implements Observer<T>, Disposable { + + private static final long serialVersionUID = 7240042530241604978L; + final Observer<? super T> downstream; + final int count; + + Disposable upstream; + + volatile boolean cancelled; + + TakeLastObserver(Observer<? super T> actual, int count) { + this.downstream = actual; + this.count = count; + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.validate(this.upstream, d)) { + this.upstream = d; + downstream.onSubscribe(this); + } + } + + @Override + public void onNext(T t) { + if (count == size()) { + poll(); + } + offer(t); + } + + @Override + public void onError(Throwable t) { + downstream.onError(t); + } + + @Override + public void onComplete() { + Observer<? super T> a = downstream; + for (;;) { + if (cancelled) { + return; + } + T v = poll(); + if (v == null) { + a.onComplete(); + return; + } + a.onNext(v); + } + } + + @Override + public void dispose() { + if (!cancelled) { + cancelled = true; + upstream.dispose(); + } + } + + @Override + public boolean isDisposed() { + return cancelled; + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableTakeLastOne.java b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableTakeLastOne.java new file mode 100644 index 0000000000..cb243dd3bd --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableTakeLastOne.java @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; + +public final class ObservableTakeLastOne<T> extends AbstractObservableWithUpstream<T, T> { + + public ObservableTakeLastOne(ObservableSource<T> source) { + super(source); + } + + @Override + public void subscribeActual(Observer<? super T> observer) { + source.subscribe(new TakeLastOneObserver<>(observer)); + } + + static final class TakeLastOneObserver<T> implements Observer<T>, Disposable { + final Observer<? super T> downstream; + + Disposable upstream; + + T value; + + TakeLastOneObserver(Observer<? super T> downstream) { + this.downstream = downstream; + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.validate(this.upstream, d)) { + this.upstream = d; + downstream.onSubscribe(this); + } + } + + @Override + public void onNext(T t) { + value = t; + } + + @Override + public void onError(Throwable t) { + value = null; + downstream.onError(t); + } + + @Override + public void onComplete() { + emit(); + } + + void emit() { + T v = value; + if (v != null) { + value = null; + downstream.onNext(v); + } + downstream.onComplete(); + } + + @Override + public void dispose() { + value = null; + upstream.dispose(); + } + + @Override + public boolean isDisposed() { + return upstream.isDisposed(); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableTakeLastTimed.java b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableTakeLastTimed.java new file mode 100644 index 0000000000..33066f374b --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableTakeLastTimed.java @@ -0,0 +1,183 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; +import io.reactivex.rxjava3.operators.SpscLinkedArrayQueue; + +public final class ObservableTakeLastTimed<T> extends AbstractObservableWithUpstream<T, T> { + final long count; + final long time; + final TimeUnit unit; + final Scheduler scheduler; + final int bufferSize; + final boolean delayError; + + public ObservableTakeLastTimed(ObservableSource<T> source, + long count, long time, TimeUnit unit, Scheduler scheduler, int bufferSize, boolean delayError) { + super(source); + this.count = count; + this.time = time; + this.unit = unit; + this.scheduler = scheduler; + this.bufferSize = bufferSize; + this.delayError = delayError; + } + + @Override + public void subscribeActual(Observer<? super T> t) { + source.subscribe(new TakeLastTimedObserver<>(t, count, time, unit, scheduler, bufferSize, delayError)); + } + + static final class TakeLastTimedObserver<T> + extends AtomicBoolean implements Observer<T>, Disposable { + + private static final long serialVersionUID = -5677354903406201275L; + final Observer<? super T> downstream; + final long count; + final long time; + final TimeUnit unit; + final Scheduler scheduler; + final SpscLinkedArrayQueue<Object> queue; + final boolean delayError; + + Disposable upstream; + + volatile boolean cancelled; + + Throwable error; + + TakeLastTimedObserver(Observer<? super T> actual, long count, long time, TimeUnit unit, Scheduler scheduler, int bufferSize, boolean delayError) { + this.downstream = actual; + this.count = count; + this.time = time; + this.unit = unit; + this.scheduler = scheduler; + this.queue = new SpscLinkedArrayQueue<>(bufferSize); + this.delayError = delayError; + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.validate(this.upstream, d)) { + this.upstream = d; + downstream.onSubscribe(this); + } + } + + @Override + public void onNext(T t) { + final SpscLinkedArrayQueue<Object> q = queue; + + long now = scheduler.now(unit); + long time = this.time; + long c = count; + boolean unbounded = c == Long.MAX_VALUE; + + q.offer(now, t); + + while (!q.isEmpty()) { + long ts = (Long)q.peek(); + if (ts <= now - time || (!unbounded && (q.size() >> 1) > c)) { + q.poll(); + q.poll(); + } else { + break; + } + } + } + + @Override + public void onError(Throwable t) { + error = t; + drain(); + } + + @Override + public void onComplete() { + drain(); + } + + @Override + public void dispose() { + if (!cancelled) { + cancelled = true; + upstream.dispose(); + + if (compareAndSet(false, true)) { + queue.clear(); + } + } + } + + @Override + public boolean isDisposed() { + return cancelled; + } + + void drain() { + if (!compareAndSet(false, true)) { + return; + } + + final Observer<? super T> a = downstream; + final SpscLinkedArrayQueue<Object> q = queue; + final boolean delayError = this.delayError; + final long timestampLimit = scheduler.now(unit) - time; + + for (;;) { + if (cancelled) { + q.clear(); + return; + } + + if (!delayError) { + Throwable ex = error; + if (ex != null) { + q.clear(); + a.onError(ex); + return; + } + } + + Object ts = q.poll(); // the timestamp long + boolean empty = ts == null; + + if (empty) { + Throwable ex = error; + if (ex != null) { + a.onError(ex); + } else { + a.onComplete(); + } + return; + } + + @SuppressWarnings("unchecked") + T o = (T)q.poll(); + + if ((Long)ts < timestampLimit) { + continue; + } + + a.onNext(o); + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableTakeUntil.java b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableTakeUntil.java new file mode 100644 index 0000000000..c32f3a6f65 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableTakeUntil.java @@ -0,0 +1,133 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import java.util.concurrent.atomic.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; +import io.reactivex.rxjava3.internal.util.*; + +public final class ObservableTakeUntil<T, U> extends AbstractObservableWithUpstream<T, T> { + + final ObservableSource<? extends U> other; + + public ObservableTakeUntil(ObservableSource<T> source, ObservableSource<? extends U> other) { + super(source); + this.other = other; + } + + @Override + public void subscribeActual(Observer<? super T> child) { + TakeUntilMainObserver<T, U> parent = new TakeUntilMainObserver<>(child); + child.onSubscribe(parent); + + other.subscribe(parent.otherObserver); + source.subscribe(parent); + } + + static final class TakeUntilMainObserver<T, U> extends AtomicInteger + implements Observer<T>, Disposable { + + private static final long serialVersionUID = 1418547743690811973L; + + final Observer<? super T> downstream; + + final AtomicReference<Disposable> upstream; + + final OtherObserver otherObserver; + + final AtomicThrowable error; + + TakeUntilMainObserver(Observer<? super T> downstream) { + this.downstream = downstream; + this.upstream = new AtomicReference<>(); + this.otherObserver = new OtherObserver(); + this.error = new AtomicThrowable(); + } + + @Override + public void dispose() { + DisposableHelper.dispose(upstream); + DisposableHelper.dispose(otherObserver); + } + + @Override + public boolean isDisposed() { + return DisposableHelper.isDisposed(upstream.get()); + } + + @Override + public void onSubscribe(Disposable d) { + DisposableHelper.setOnce(upstream, d); + } + + @Override + public void onNext(T t) { + HalfSerializer.onNext(downstream, t, this, error); + } + + @Override + public void onError(Throwable e) { + DisposableHelper.dispose(otherObserver); + HalfSerializer.onError(downstream, e, this, error); + } + + @Override + public void onComplete() { + DisposableHelper.dispose(otherObserver); + HalfSerializer.onComplete(downstream, this, error); + } + + void otherError(Throwable e) { + DisposableHelper.dispose(upstream); + HalfSerializer.onError(downstream, e, this, error); + } + + void otherComplete() { + DisposableHelper.dispose(upstream); + HalfSerializer.onComplete(downstream, this, error); + } + + final class OtherObserver extends AtomicReference<Disposable> + implements Observer<U> { + + private static final long serialVersionUID = -8693423678067375039L; + + @Override + public void onSubscribe(Disposable d) { + DisposableHelper.setOnce(this, d); + } + + @Override + public void onNext(U t) { + DisposableHelper.dispose(this); + otherComplete(); + } + + @Override + public void onError(Throwable e) { + otherError(e); + } + + @Override + public void onComplete() { + otherComplete(); + } + + } + } + +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableTakeUntilPredicate.java b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableTakeUntilPredicate.java new file mode 100644 index 0000000000..5183a6026e --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableTakeUntilPredicate.java @@ -0,0 +1,102 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.Predicate; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +public final class ObservableTakeUntilPredicate<T> extends AbstractObservableWithUpstream<T, T> { + final Predicate<? super T> predicate; + public ObservableTakeUntilPredicate(ObservableSource<T> source, Predicate<? super T> predicate) { + super(source); + this.predicate = predicate; + } + + @Override + public void subscribeActual(Observer<? super T> observer) { + source.subscribe(new TakeUntilPredicateObserver<>(observer, predicate)); + } + + static final class TakeUntilPredicateObserver<T> implements Observer<T>, Disposable { + final Observer<? super T> downstream; + final Predicate<? super T> predicate; + Disposable upstream; + boolean done; + TakeUntilPredicateObserver(Observer<? super T> downstream, Predicate<? super T> predicate) { + this.downstream = downstream; + this.predicate = predicate; + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.validate(this.upstream, d)) { + this.upstream = d; + downstream.onSubscribe(this); + } + } + + @Override + public void dispose() { + upstream.dispose(); + } + + @Override + public boolean isDisposed() { + return upstream.isDisposed(); + } + + @Override + public void onNext(T t) { + if (!done) { + downstream.onNext(t); + boolean b; + try { + b = predicate.test(t); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + upstream.dispose(); + onError(e); + return; + } + if (b) { + done = true; + upstream.dispose(); + downstream.onComplete(); + } + } + } + + @Override + public void onError(Throwable t) { + if (!done) { + done = true; + downstream.onError(t); + } else { + RxJavaPlugins.onError(t); + } + } + + @Override + public void onComplete() { + if (!done) { + done = true; + downstream.onComplete(); + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableTakeWhile.java b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableTakeWhile.java new file mode 100644 index 0000000000..1b56e6cf34 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableTakeWhile.java @@ -0,0 +1,110 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.Predicate; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +public final class ObservableTakeWhile<T> extends AbstractObservableWithUpstream<T, T> { + final Predicate<? super T> predicate; + public ObservableTakeWhile(ObservableSource<T> source, Predicate<? super T> predicate) { + super(source); + this.predicate = predicate; + } + + @Override + public void subscribeActual(Observer<? super T> t) { + source.subscribe(new TakeWhileObserver<>(t, predicate)); + } + + static final class TakeWhileObserver<T> implements Observer<T>, Disposable { + final Observer<? super T> downstream; + final Predicate<? super T> predicate; + + Disposable upstream; + + boolean done; + + TakeWhileObserver(Observer<? super T> actual, Predicate<? super T> predicate) { + this.downstream = actual; + this.predicate = predicate; + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.validate(this.upstream, d)) { + this.upstream = d; + downstream.onSubscribe(this); + } + } + + @Override + public void dispose() { + upstream.dispose(); + } + + @Override + public boolean isDisposed() { + return upstream.isDisposed(); + } + + @Override + public void onNext(T t) { + if (done) { + return; + } + boolean b; + try { + b = predicate.test(t); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + upstream.dispose(); + onError(e); + return; + } + + if (!b) { + done = true; + upstream.dispose(); + downstream.onComplete(); + return; + } + + downstream.onNext(t); + } + + @Override + public void onError(Throwable t) { + if (done) { + RxJavaPlugins.onError(t); + return; + } + done = true; + downstream.onError(t); + } + + @Override + public void onComplete() { + if (done) { + return; + } + done = true; + downstream.onComplete(); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableThrottleFirstTimed.java b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableThrottleFirstTimed.java new file mode 100644 index 0000000000..6bf3b9f119 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableThrottleFirstTimed.java @@ -0,0 +1,140 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.core.Scheduler.Worker; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.Consumer; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; +import io.reactivex.rxjava3.observers.SerializedObserver; + +public final class ObservableThrottleFirstTimed<T> extends AbstractObservableWithUpstream<T, T> { + final long timeout; + final TimeUnit unit; + final Scheduler scheduler; + final Consumer<? super T> onDropped; + + public ObservableThrottleFirstTimed( + ObservableSource<T> source, + long timeout, + TimeUnit unit, + Scheduler scheduler, + Consumer<? super T> onDropped) { + super(source); + this.timeout = timeout; + this.unit = unit; + this.scheduler = scheduler; + this.onDropped = onDropped; + } + + @Override + public void subscribeActual(Observer<? super T> t) { + source.subscribe(new DebounceTimedObserver<>( + new SerializedObserver<>(t), + timeout, unit, scheduler.createWorker(), + onDropped)); + } + + static final class DebounceTimedObserver<T> + extends AtomicReference<Disposable> + implements Observer<T>, Disposable, Runnable { + private static final long serialVersionUID = 786994795061867455L; + + final Observer<? super T> downstream; + final long timeout; + final TimeUnit unit; + final Scheduler.Worker worker; + final Consumer<? super T> onDropped; + Disposable upstream; + volatile boolean gate; + + DebounceTimedObserver( + Observer<? super T> actual, + long timeout, + TimeUnit unit, + Worker worker, + Consumer<? super T> onDropped) { + this.downstream = actual; + this.timeout = timeout; + this.unit = unit; + this.worker = worker; + this.onDropped = onDropped; + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.validate(this.upstream, d)) { + this.upstream = d; + downstream.onSubscribe(this); + } + } + + @Override + public void onNext(T t) { + if (!gate) { + gate = true; + + downstream.onNext(t); + + Disposable d = get(); + if (d != null) { + d.dispose(); + } + DisposableHelper.replace(this, worker.schedule(this, timeout, unit)); + } else if (onDropped != null) { + try { + onDropped.accept(t); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + upstream.dispose(); + downstream.onError(ex); + worker.dispose(); + } + } + } + + @Override + public void run() { + gate = false; + } + + @Override + public void onError(Throwable t) { + downstream.onError(t); + worker.dispose(); + } + + @Override + public void onComplete() { + downstream.onComplete(); + worker.dispose(); + } + + @Override + public void dispose() { + upstream.dispose(); + worker.dispose(); + } + + @Override + public boolean isDisposed() { + return worker.isDisposed(); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableThrottleLatest.java b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableThrottleLatest.java new file mode 100644 index 0000000000..caf14d3a5e --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableThrottleLatest.java @@ -0,0 +1,279 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.Consumer; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +/** + * Emits the next or latest item when the given time elapses. + * <p> + * The operator emits the next item, then starts a timer. When the timer fires, + * it tries to emit the latest item from upstream. If there was no upstream item, + * in the meantime, the next upstream item is emitted immediately and the + * timed process repeats. + * <p>History: 2.1.14 - experimental + * @param <T> the upstream and downstream value type + * @since 2.2 + */ +public final class ObservableThrottleLatest<T> extends AbstractObservableWithUpstream<T, T> { + + final long timeout; + + final TimeUnit unit; + + final Scheduler scheduler; + + final boolean emitLast; + + final Consumer<? super T> onDropped; + + public ObservableThrottleLatest(Observable<T> source, + long timeout, TimeUnit unit, + Scheduler scheduler, + boolean emitLast, + Consumer<? super T> onDropped) { + super(source); + this.timeout = timeout; + this.unit = unit; + this.scheduler = scheduler; + this.emitLast = emitLast; + this.onDropped = onDropped; + } + + @Override + protected void subscribeActual(Observer<? super T> observer) { + source.subscribe(new ThrottleLatestObserver<>(observer, timeout, unit, scheduler.createWorker(), emitLast, onDropped)); + } + + static final class ThrottleLatestObserver<T> + extends AtomicInteger + implements Observer<T>, Disposable, Runnable { + + private static final long serialVersionUID = -8296689127439125014L; + + final Observer<? super T> downstream; + + final long timeout; + + final TimeUnit unit; + + final Scheduler.Worker worker; + + final boolean emitLast; + + final AtomicReference<T> latest; + + final Consumer<? super T> onDropped; + + Disposable upstream; + + volatile boolean done; + Throwable error; + + volatile boolean cancelled; + + volatile boolean timerFired; + + boolean timerRunning; + + ThrottleLatestObserver(Observer<? super T> downstream, + long timeout, TimeUnit unit, + Scheduler.Worker worker, + boolean emitLast, + Consumer<? super T> onDropped) { + this.downstream = downstream; + this.timeout = timeout; + this.unit = unit; + this.worker = worker; + this.emitLast = emitLast; + this.latest = new AtomicReference<>(); + this.onDropped = onDropped; + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.validate(upstream, d)) { + upstream = d; + downstream.onSubscribe(this); + } + } + + @Override + public void onNext(T t) { + T old = latest.getAndSet(t); + if (onDropped != null && old != null) { + try { + onDropped.accept(old); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + upstream.dispose(); + error = ex; + done = true; + } + } + drain(); + } + + @Override + public void onError(Throwable t) { + error = t; + done = true; + drain(); + } + + @Override + public void onComplete() { + done = true; + drain(); + } + + @Override + public void dispose() { + cancelled = true; + upstream.dispose(); + worker.dispose(); + if (getAndIncrement() == 0) { + clear(); + } + } + + void clear() { + if (onDropped != null) { + T v = latest.getAndSet(null); + if (v != null) { + try { + onDropped.accept(v); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + RxJavaPlugins.onError(ex); + } + } + } else { + latest.lazySet(null); + } + } + + @Override + public boolean isDisposed() { + return cancelled; + } + + @Override + public void run() { + timerFired = true; + drain(); + } + + void drain() { + if (getAndIncrement() != 0) { + return; + } + + int missed = 1; + + AtomicReference<T> latest = this.latest; + Observer<? super T> downstream = this.downstream; + + for (;;) { + + for (;;) { + if (cancelled) { + clear(); + return; + } + + boolean d = done; + Throwable error = this.error; + + if (d && error != null) { + if (onDropped != null) { + T v = latest.getAndSet(null); + if (v != null) { + try { + onDropped.accept(v); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + error = new CompositeException(error, ex); + } + } + } else { + latest.lazySet(null); + } + downstream.onError(error); + worker.dispose(); + return; + } + + T v = latest.get(); + boolean empty = v == null; + + if (d) { + if (!empty) { + v = latest.getAndSet(null); + if (emitLast) { + downstream.onNext(v); + } else { + if (onDropped != null) { + try { + onDropped.accept(v); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + downstream.onError(ex); + worker.dispose(); + return; + } + } + } + } + downstream.onComplete(); + worker.dispose(); + return; + } + + if (empty) { + if (timerFired) { + timerRunning = false; + timerFired = false; + } + break; + } + + if (!timerRunning || timerFired) { + v = latest.getAndSet(null); + downstream.onNext(v); + + timerFired = false; + timerRunning = true; + worker.schedule(this, timeout, unit); + } else { + break; + } + } + + missed = addAndGet(-missed); + if (missed == 0) { + break; + } + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableTimeInterval.java b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableTimeInterval.java new file mode 100644 index 0000000000..4a4e6a787f --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableTimeInterval.java @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import java.util.concurrent.TimeUnit; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; +import io.reactivex.rxjava3.schedulers.Timed; + +public final class ObservableTimeInterval<T> extends AbstractObservableWithUpstream<T, Timed<T>> { + final Scheduler scheduler; + final TimeUnit unit; + + public ObservableTimeInterval(ObservableSource<T> source, TimeUnit unit, Scheduler scheduler) { + super(source); + this.scheduler = scheduler; + this.unit = unit; + } + + @Override + public void subscribeActual(Observer<? super Timed<T>> t) { + source.subscribe(new TimeIntervalObserver<>(t, unit, scheduler)); + } + + static final class TimeIntervalObserver<T> implements Observer<T>, Disposable { + final Observer<? super Timed<T>> downstream; + final TimeUnit unit; + final Scheduler scheduler; + + long lastTime; + + Disposable upstream; + + TimeIntervalObserver(Observer<? super Timed<T>> actual, TimeUnit unit, Scheduler scheduler) { + this.downstream = actual; + this.scheduler = scheduler; + this.unit = unit; + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.validate(this.upstream, d)) { + this.upstream = d; + lastTime = scheduler.now(unit); + downstream.onSubscribe(this); + } + } + + @Override + public void dispose() { + upstream.dispose(); + } + + @Override + public boolean isDisposed() { + return upstream.isDisposed(); + } + + @Override + public void onNext(T t) { + long now = scheduler.now(unit); + long last = lastTime; + lastTime = now; + long delta = now - last; + downstream.onNext(new Timed<>(t, delta, unit)); + } + + @Override + public void onError(Throwable t) { + downstream.onError(t); + } + + @Override + public void onComplete() { + downstream.onComplete(); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableTimeout.java b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableTimeout.java new file mode 100644 index 0000000000..1922251d54 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableTimeout.java @@ -0,0 +1,378 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import java.util.Objects; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.disposables.*; +import io.reactivex.rxjava3.internal.operators.observable.ObservableTimeoutTimed.TimeoutSupport; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +public final class ObservableTimeout<T, U, V> extends AbstractObservableWithUpstream<T, T> { + final ObservableSource<U> firstTimeoutIndicator; + final Function<? super T, ? extends ObservableSource<V>> itemTimeoutIndicator; + final ObservableSource<? extends T> other; + + public ObservableTimeout( + Observable<T> source, + ObservableSource<U> firstTimeoutIndicator, + Function<? super T, ? extends ObservableSource<V>> itemTimeoutIndicator, + ObservableSource<? extends T> other) { + super(source); + this.firstTimeoutIndicator = firstTimeoutIndicator; + this.itemTimeoutIndicator = itemTimeoutIndicator; + this.other = other; + } + + @Override + protected void subscribeActual(Observer<? super T> observer) { + if (other == null) { + TimeoutObserver<T> parent = new TimeoutObserver<>(observer, itemTimeoutIndicator); + observer.onSubscribe(parent); + parent.startFirstTimeout(firstTimeoutIndicator); + source.subscribe(parent); + } else { + TimeoutFallbackObserver<T> parent = new TimeoutFallbackObserver<>(observer, itemTimeoutIndicator, other); + observer.onSubscribe(parent); + parent.startFirstTimeout(firstTimeoutIndicator); + source.subscribe(parent); + } + } + + interface TimeoutSelectorSupport extends TimeoutSupport { + void onTimeoutError(long idx, Throwable ex); + } + + static final class TimeoutObserver<T> extends AtomicLong + implements Observer<T>, Disposable, TimeoutSelectorSupport { + + private static final long serialVersionUID = 3764492702657003550L; + + final Observer<? super T> downstream; + + final Function<? super T, ? extends ObservableSource<?>> itemTimeoutIndicator; + + final SequentialDisposable task; + + final AtomicReference<Disposable> upstream; + + TimeoutObserver(Observer<? super T> actual, Function<? super T, ? extends ObservableSource<?>> itemTimeoutIndicator) { + this.downstream = actual; + this.itemTimeoutIndicator = itemTimeoutIndicator; + this.task = new SequentialDisposable(); + this.upstream = new AtomicReference<>(); + } + + @Override + public void onSubscribe(Disposable d) { + DisposableHelper.setOnce(upstream, d); + } + + @Override + public void onNext(T t) { + long idx = get(); + if (idx == Long.MAX_VALUE || !compareAndSet(idx, idx + 1)) { + return; + } + + Disposable d = task.get(); + if (d != null) { + d.dispose(); + } + + downstream.onNext(t); + + ObservableSource<?> itemTimeoutObservableSource; + + try { + itemTimeoutObservableSource = Objects.requireNonNull( + itemTimeoutIndicator.apply(t), + "The itemTimeoutIndicator returned a null ObservableSource."); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + upstream.get().dispose(); + getAndSet(Long.MAX_VALUE); + downstream.onError(ex); + return; + } + + TimeoutConsumer consumer = new TimeoutConsumer(idx + 1, this); + if (task.replace(consumer)) { + itemTimeoutObservableSource.subscribe(consumer); + } + } + + void startFirstTimeout(ObservableSource<?> firstTimeoutIndicator) { + if (firstTimeoutIndicator != null) { + TimeoutConsumer consumer = new TimeoutConsumer(0L, this); + if (task.replace(consumer)) { + firstTimeoutIndicator.subscribe(consumer); + } + } + } + + @Override + public void onError(Throwable t) { + if (getAndSet(Long.MAX_VALUE) != Long.MAX_VALUE) { + task.dispose(); + + downstream.onError(t); + } else { + RxJavaPlugins.onError(t); + } + } + + @Override + public void onComplete() { + if (getAndSet(Long.MAX_VALUE) != Long.MAX_VALUE) { + task.dispose(); + + downstream.onComplete(); + } + } + + @Override + public void onTimeout(long idx) { + if (compareAndSet(idx, Long.MAX_VALUE)) { + DisposableHelper.dispose(upstream); + + downstream.onError(new TimeoutException()); + } + } + + @Override + public void onTimeoutError(long idx, Throwable ex) { + if (compareAndSet(idx, Long.MAX_VALUE)) { + DisposableHelper.dispose(upstream); + + downstream.onError(ex); + } else { + RxJavaPlugins.onError(ex); + } + } + + @Override + public void dispose() { + DisposableHelper.dispose(upstream); + task.dispose(); + } + + @Override + public boolean isDisposed() { + return DisposableHelper.isDisposed(upstream.get()); + } + } + + static final class TimeoutFallbackObserver<T> + extends AtomicReference<Disposable> + implements Observer<T>, Disposable, TimeoutSelectorSupport { + + private static final long serialVersionUID = -7508389464265974549L; + + final Observer<? super T> downstream; + + final Function<? super T, ? extends ObservableSource<?>> itemTimeoutIndicator; + + final SequentialDisposable task; + + final AtomicLong index; + + final AtomicReference<Disposable> upstream; + + ObservableSource<? extends T> fallback; + + TimeoutFallbackObserver(Observer<? super T> actual, + Function<? super T, ? extends ObservableSource<?>> itemTimeoutIndicator, + ObservableSource<? extends T> fallback) { + this.downstream = actual; + this.itemTimeoutIndicator = itemTimeoutIndicator; + this.task = new SequentialDisposable(); + this.fallback = fallback; + this.index = new AtomicLong(); + this.upstream = new AtomicReference<>(); + } + + @Override + public void onSubscribe(Disposable d) { + DisposableHelper.setOnce(upstream, d); + } + + @Override + public void onNext(T t) { + long idx = index.get(); + if (idx == Long.MAX_VALUE || !index.compareAndSet(idx, idx + 1)) { + return; + } + + Disposable d = task.get(); + if (d != null) { + d.dispose(); + } + + downstream.onNext(t); + + ObservableSource<?> itemTimeoutObservableSource; + + try { + itemTimeoutObservableSource = Objects.requireNonNull( + itemTimeoutIndicator.apply(t), + "The itemTimeoutIndicator returned a null ObservableSource."); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + upstream.get().dispose(); + index.getAndSet(Long.MAX_VALUE); + downstream.onError(ex); + return; + } + + TimeoutConsumer consumer = new TimeoutConsumer(idx + 1, this); + if (task.replace(consumer)) { + itemTimeoutObservableSource.subscribe(consumer); + } + } + + void startFirstTimeout(ObservableSource<?> firstTimeoutIndicator) { + if (firstTimeoutIndicator != null) { + TimeoutConsumer consumer = new TimeoutConsumer(0L, this); + if (task.replace(consumer)) { + firstTimeoutIndicator.subscribe(consumer); + } + } + } + + @Override + public void onError(Throwable t) { + if (index.getAndSet(Long.MAX_VALUE) != Long.MAX_VALUE) { + task.dispose(); + + downstream.onError(t); + + task.dispose(); + } else { + RxJavaPlugins.onError(t); + } + } + + @Override + public void onComplete() { + if (index.getAndSet(Long.MAX_VALUE) != Long.MAX_VALUE) { + task.dispose(); + + downstream.onComplete(); + + task.dispose(); + } + } + + @Override + public void onTimeout(long idx) { + if (index.compareAndSet(idx, Long.MAX_VALUE)) { + DisposableHelper.dispose(upstream); + + ObservableSource<? extends T> f = fallback; + fallback = null; + + f.subscribe(new ObservableTimeoutTimed.FallbackObserver<T>(downstream, this)); + } + } + + @Override + public void onTimeoutError(long idx, Throwable ex) { + if (index.compareAndSet(idx, Long.MAX_VALUE)) { + DisposableHelper.dispose(this); + + downstream.onError(ex); + } else { + RxJavaPlugins.onError(ex); + } + } + + @Override + public void dispose() { + DisposableHelper.dispose(upstream); + DisposableHelper.dispose(this); + task.dispose(); + } + + @Override + public boolean isDisposed() { + return DisposableHelper.isDisposed(get()); + } + } + + static final class TimeoutConsumer extends AtomicReference<Disposable> + implements Observer<Object>, Disposable { + + private static final long serialVersionUID = 8708641127342403073L; + + final TimeoutSelectorSupport parent; + + final long idx; + + TimeoutConsumer(long idx, TimeoutSelectorSupport parent) { + this.idx = idx; + this.parent = parent; + } + + @Override + public void onSubscribe(Disposable d) { + DisposableHelper.setOnce(this, d); + } + + @Override + public void onNext(Object t) { + Disposable upstream = get(); + if (upstream != DisposableHelper.DISPOSED) { + upstream.dispose(); + lazySet(DisposableHelper.DISPOSED); + parent.onTimeout(idx); + } + } + + @Override + public void onError(Throwable t) { + if (get() != DisposableHelper.DISPOSED) { + lazySet(DisposableHelper.DISPOSED); + parent.onTimeoutError(idx, t); + } else { + RxJavaPlugins.onError(t); + } + } + + @Override + public void onComplete() { + if (get() != DisposableHelper.DISPOSED) { + lazySet(DisposableHelper.DISPOSED); + parent.onTimeout(idx); + } + } + + @Override + public void dispose() { + DisposableHelper.dispose(this); + } + + @Override + public boolean isDisposed() { + return DisposableHelper.isDisposed(this.get()); + } + } + +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableTimeoutTimed.java b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableTimeoutTimed.java new file mode 100644 index 0000000000..d0f5695143 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableTimeoutTimed.java @@ -0,0 +1,313 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import static io.reactivex.rxjava3.internal.util.ExceptionHelper.timeoutMessage; + +import java.util.concurrent.*; +import java.util.concurrent.atomic.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.disposables.*; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +public final class ObservableTimeoutTimed<T> extends AbstractObservableWithUpstream<T, T> { + final long timeout; + final TimeUnit unit; + final Scheduler scheduler; + final ObservableSource<? extends T> other; + + public ObservableTimeoutTimed(Observable<T> source, + long timeout, TimeUnit unit, Scheduler scheduler, ObservableSource<? extends T> other) { + super(source); + this.timeout = timeout; + this.unit = unit; + this.scheduler = scheduler; + this.other = other; + } + + @Override + protected void subscribeActual(Observer<? super T> observer) { + if (other == null) { + TimeoutObserver<T> parent = new TimeoutObserver<>(observer, timeout, unit, scheduler.createWorker()); + observer.onSubscribe(parent); + parent.startTimeout(0L); + source.subscribe(parent); + } else { + TimeoutFallbackObserver<T> parent = new TimeoutFallbackObserver<>(observer, timeout, unit, scheduler.createWorker(), other); + observer.onSubscribe(parent); + parent.startTimeout(0L); + source.subscribe(parent); + } + } + + static final class TimeoutObserver<T> extends AtomicLong + implements Observer<T>, Disposable, TimeoutSupport { + + private static final long serialVersionUID = 3764492702657003550L; + + final Observer<? super T> downstream; + + final long timeout; + + final TimeUnit unit; + + final Scheduler.Worker worker; + + final SequentialDisposable task; + + final AtomicReference<Disposable> upstream; + + TimeoutObserver(Observer<? super T> actual, long timeout, TimeUnit unit, Scheduler.Worker worker) { + this.downstream = actual; + this.timeout = timeout; + this.unit = unit; + this.worker = worker; + this.task = new SequentialDisposable(); + this.upstream = new AtomicReference<>(); + } + + @Override + public void onSubscribe(Disposable d) { + DisposableHelper.setOnce(upstream, d); + } + + @Override + public void onNext(T t) { + long idx = get(); + if (idx == Long.MAX_VALUE || !compareAndSet(idx, idx + 1)) { + return; + } + + task.get().dispose(); + + downstream.onNext(t); + + startTimeout(idx + 1); + } + + void startTimeout(long nextIndex) { + task.replace(worker.schedule(new TimeoutTask(nextIndex, this), timeout, unit)); + } + + @Override + public void onError(Throwable t) { + if (getAndSet(Long.MAX_VALUE) != Long.MAX_VALUE) { + task.dispose(); + + downstream.onError(t); + + worker.dispose(); + } else { + RxJavaPlugins.onError(t); + } + } + + @Override + public void onComplete() { + if (getAndSet(Long.MAX_VALUE) != Long.MAX_VALUE) { + task.dispose(); + + downstream.onComplete(); + + worker.dispose(); + } + } + + @Override + public void onTimeout(long idx) { + if (compareAndSet(idx, Long.MAX_VALUE)) { + DisposableHelper.dispose(upstream); + + downstream.onError(new TimeoutException(timeoutMessage(timeout, unit))); + + worker.dispose(); + } + } + + @Override + public void dispose() { + DisposableHelper.dispose(upstream); + worker.dispose(); + } + + @Override + public boolean isDisposed() { + return DisposableHelper.isDisposed(upstream.get()); + } + } + + static final class TimeoutTask implements Runnable { + + final TimeoutSupport parent; + + final long idx; + + TimeoutTask(long idx, TimeoutSupport parent) { + this.idx = idx; + this.parent = parent; + } + + @Override + public void run() { + parent.onTimeout(idx); + } + } + + static final class TimeoutFallbackObserver<T> extends AtomicReference<Disposable> + implements Observer<T>, Disposable, TimeoutSupport { + + private static final long serialVersionUID = 3764492702657003550L; + + final Observer<? super T> downstream; + + final long timeout; + + final TimeUnit unit; + + final Scheduler.Worker worker; + + final SequentialDisposable task; + + final AtomicLong index; + + final AtomicReference<Disposable> upstream; + + ObservableSource<? extends T> fallback; + + TimeoutFallbackObserver(Observer<? super T> actual, long timeout, TimeUnit unit, + Scheduler.Worker worker, ObservableSource<? extends T> fallback) { + this.downstream = actual; + this.timeout = timeout; + this.unit = unit; + this.worker = worker; + this.fallback = fallback; + this.task = new SequentialDisposable(); + this.index = new AtomicLong(); + this.upstream = new AtomicReference<>(); + } + + @Override + public void onSubscribe(Disposable d) { + DisposableHelper.setOnce(upstream, d); + } + + @Override + public void onNext(T t) { + long idx = index.get(); + if (idx == Long.MAX_VALUE || !index.compareAndSet(idx, idx + 1)) { + return; + } + + task.get().dispose(); + + downstream.onNext(t); + + startTimeout(idx + 1); + } + + void startTimeout(long nextIndex) { + task.replace(worker.schedule(new TimeoutTask(nextIndex, this), timeout, unit)); + } + + @Override + public void onError(Throwable t) { + if (index.getAndSet(Long.MAX_VALUE) != Long.MAX_VALUE) { + task.dispose(); + + downstream.onError(t); + + worker.dispose(); + } else { + RxJavaPlugins.onError(t); + } + } + + @Override + public void onComplete() { + if (index.getAndSet(Long.MAX_VALUE) != Long.MAX_VALUE) { + task.dispose(); + + downstream.onComplete(); + + worker.dispose(); + } + } + + @Override + public void onTimeout(long idx) { + if (index.compareAndSet(idx, Long.MAX_VALUE)) { + DisposableHelper.dispose(upstream); + + ObservableSource<? extends T> f = fallback; + fallback = null; + + f.subscribe(new FallbackObserver<T>(downstream, this)); + + worker.dispose(); + } + } + + @Override + public void dispose() { + DisposableHelper.dispose(upstream); + DisposableHelper.dispose(this); + worker.dispose(); + } + + @Override + public boolean isDisposed() { + return DisposableHelper.isDisposed(get()); + } + } + + static final class FallbackObserver<T> implements Observer<T> { + + final Observer<? super T> downstream; + + final AtomicReference<Disposable> arbiter; + + FallbackObserver(Observer<? super T> actual, AtomicReference<Disposable> arbiter) { + this.downstream = actual; + this.arbiter = arbiter; + } + + @Override + public void onSubscribe(Disposable d) { + DisposableHelper.replace(arbiter, d); + } + + @Override + public void onNext(T t) { + downstream.onNext(t); + } + + @Override + public void onError(Throwable t) { + downstream.onError(t); + } + + @Override + public void onComplete() { + downstream.onComplete(); + } + } + + interface TimeoutSupport { + + void onTimeout(long idx); + + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableTimer.java b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableTimer.java new file mode 100644 index 0000000000..3c9500996e --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableTimer.java @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.disposables.*; + +public final class ObservableTimer extends Observable<Long> { + final Scheduler scheduler; + final long delay; + final TimeUnit unit; + public ObservableTimer(long delay, TimeUnit unit, Scheduler scheduler) { + this.delay = delay; + this.unit = unit; + this.scheduler = scheduler; + } + + @Override + public void subscribeActual(Observer<? super Long> observer) { + TimerObserver ios = new TimerObserver(observer); + observer.onSubscribe(ios); + + Disposable d = scheduler.scheduleDirect(ios, delay, unit); + + ios.setResource(d); + } + + static final class TimerObserver extends AtomicReference<Disposable> + implements Disposable, Runnable { + + private static final long serialVersionUID = -2809475196591179431L; + + final Observer<? super Long> downstream; + + TimerObserver(Observer<? super Long> downstream) { + this.downstream = downstream; + } + + @Override + public void dispose() { + DisposableHelper.dispose(this); + } + + @Override + public boolean isDisposed() { + return get() == DisposableHelper.DISPOSED; + } + + @Override + public void run() { + if (!isDisposed()) { + downstream.onNext(0L); + lazySet(EmptyDisposable.INSTANCE); + downstream.onComplete(); + } + } + + public void setResource(Disposable d) { + DisposableHelper.trySet(this, d); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableToList.java b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableToList.java new file mode 100644 index 0000000000..e75986c77c --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableToList.java @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import java.util.Collection; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.Supplier; +import io.reactivex.rxjava3.internal.disposables.*; +import io.reactivex.rxjava3.internal.util.ExceptionHelper; + +public final class ObservableToList<T, U extends Collection<? super T>> +extends AbstractObservableWithUpstream<T, U> { + + final Supplier<U> collectionSupplier; + + public ObservableToList(ObservableSource<T> source, Supplier<U> collectionSupplier) { + super(source); + this.collectionSupplier = collectionSupplier; + } + + @Override + public void subscribeActual(Observer<? super U> t) { + U coll; + try { + coll = ExceptionHelper.nullCheck(collectionSupplier.get(), "The collectionSupplier returned a null Collection."); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + EmptyDisposable.error(e, t); + return; + } + source.subscribe(new ToListObserver<>(t, coll)); + } + + static final class ToListObserver<T, U extends Collection<? super T>> implements Observer<T>, Disposable { + final Observer<? super U> downstream; + + Disposable upstream; + + U collection; + + ToListObserver(Observer<? super U> actual, U collection) { + this.downstream = actual; + this.collection = collection; + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.validate(this.upstream, d)) { + this.upstream = d; + downstream.onSubscribe(this); + } + } + + @Override + public void dispose() { + upstream.dispose(); + } + + @Override + public boolean isDisposed() { + return upstream.isDisposed(); + } + + @Override + public void onNext(T t) { + collection.add(t); + } + + @Override + public void onError(Throwable t) { + collection = null; + downstream.onError(t); + } + + @Override + public void onComplete() { + U c = collection; + collection = null; + downstream.onNext(c); + downstream.onComplete(); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableToListSingle.java b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableToListSingle.java new file mode 100644 index 0000000000..de4db3ecf1 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableToListSingle.java @@ -0,0 +1,112 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import java.util.Collection; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.Supplier; +import io.reactivex.rxjava3.internal.disposables.*; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.internal.fuseable.FuseToObservable; +import io.reactivex.rxjava3.internal.util.ExceptionHelper; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +public final class ObservableToListSingle<T, U extends Collection<? super T>> +extends Single<U> implements FuseToObservable<U> { + + final ObservableSource<T> source; + + final Supplier<U> collectionSupplier; + + @SuppressWarnings({ "unchecked", "rawtypes" }) + public ObservableToListSingle(ObservableSource<T> source, final int defaultCapacityHint) { + this.source = source; + this.collectionSupplier = (Supplier)Functions.createArrayList(defaultCapacityHint); + } + + public ObservableToListSingle(ObservableSource<T> source, Supplier<U> collectionSupplier) { + this.source = source; + this.collectionSupplier = collectionSupplier; + } + + @Override + public void subscribeActual(SingleObserver<? super U> t) { + U coll; + try { + coll = ExceptionHelper.nullCheck(collectionSupplier.get(), "The collectionSupplier returned a null Collection."); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + EmptyDisposable.error(e, t); + return; + } + source.subscribe(new ToListObserver<>(t, coll)); + } + + @Override + public Observable<U> fuseToObservable() { + return RxJavaPlugins.onAssembly(new ObservableToList<>(source, collectionSupplier)); + } + + static final class ToListObserver<T, U extends Collection<? super T>> implements Observer<T>, Disposable { + final SingleObserver<? super U> downstream; + + U collection; + + Disposable upstream; + + ToListObserver(SingleObserver<? super U> actual, U collection) { + this.downstream = actual; + this.collection = collection; + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.validate(this.upstream, d)) { + this.upstream = d; + downstream.onSubscribe(this); + } + } + + @Override + public void dispose() { + upstream.dispose(); + } + + @Override + public boolean isDisposed() { + return upstream.isDisposed(); + } + + @Override + public void onNext(T t) { + collection.add(t); + } + + @Override + public void onError(Throwable t) { + collection = null; + downstream.onError(t); + } + + @Override + public void onComplete() { + U c = collection; + collection = null; + downstream.onSuccess(c); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableUnsubscribeOn.java b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableUnsubscribeOn.java new file mode 100644 index 0000000000..d06d64f34c --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableUnsubscribeOn.java @@ -0,0 +1,99 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import java.util.concurrent.atomic.AtomicBoolean; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +public final class ObservableUnsubscribeOn<T> extends AbstractObservableWithUpstream<T, T> { + final Scheduler scheduler; + public ObservableUnsubscribeOn(ObservableSource<T> source, Scheduler scheduler) { + super(source); + this.scheduler = scheduler; + } + + @Override + public void subscribeActual(Observer<? super T> t) { + source.subscribe(new UnsubscribeObserver<>(t, scheduler)); + } + + static final class UnsubscribeObserver<T> extends AtomicBoolean implements Observer<T>, Disposable { + + private static final long serialVersionUID = 1015244841293359600L; + + final Observer<? super T> downstream; + final Scheduler scheduler; + + Disposable upstream; + + UnsubscribeObserver(Observer<? super T> actual, Scheduler scheduler) { + this.downstream = actual; + this.scheduler = scheduler; + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.validate(this.upstream, d)) { + this.upstream = d; + downstream.onSubscribe(this); + } + } + + @Override + public void onNext(T t) { + if (!get()) { + downstream.onNext(t); + } + } + + @Override + public void onError(Throwable t) { + if (get()) { + RxJavaPlugins.onError(t); + return; + } + downstream.onError(t); + } + + @Override + public void onComplete() { + if (!get()) { + downstream.onComplete(); + } + } + + @Override + public void dispose() { + if (compareAndSet(false, true)) { + scheduler.scheduleDirect(new DisposeTask()); + } + } + + @Override + public boolean isDisposed() { + return get(); + } + + final class DisposeTask implements Runnable { + @Override + public void run() { + upstream.dispose(); + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableUsing.java b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableUsing.java new file mode 100644 index 0000000000..1ffdef32d6 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableUsing.java @@ -0,0 +1,175 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import java.util.Objects; +import java.util.concurrent.atomic.AtomicBoolean; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.disposables.*; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +public final class ObservableUsing<T, D> extends Observable<T> { + final Supplier<? extends D> resourceSupplier; + final Function<? super D, ? extends ObservableSource<? extends T>> sourceSupplier; + final Consumer<? super D> disposer; + final boolean eager; + + public ObservableUsing(Supplier<? extends D> resourceSupplier, + Function<? super D, ? extends ObservableSource<? extends T>> sourceSupplier, + Consumer<? super D> disposer, + boolean eager) { + this.resourceSupplier = resourceSupplier; + this.sourceSupplier = sourceSupplier; + this.disposer = disposer; + this.eager = eager; + } + + @Override + public void subscribeActual(Observer<? super T> observer) { + D resource; + + try { + resource = resourceSupplier.get(); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + EmptyDisposable.error(e, observer); + return; + } + + ObservableSource<? extends T> source; + try { + source = Objects.requireNonNull(sourceSupplier.apply(resource), "The sourceSupplier returned a null ObservableSource"); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + try { + disposer.accept(resource); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + EmptyDisposable.error(new CompositeException(e, ex), observer); + return; + } + EmptyDisposable.error(e, observer); + return; + } + + UsingObserver<T, D> us = new UsingObserver<>(observer, resource, disposer, eager); + + source.subscribe(us); + } + + static final class UsingObserver<T, D> extends AtomicBoolean implements Observer<T>, Disposable { + + private static final long serialVersionUID = 5904473792286235046L; + + final Observer<? super T> downstream; + final D resource; + final Consumer<? super D> disposer; + final boolean eager; + + Disposable upstream; + + UsingObserver(Observer<? super T> actual, D resource, Consumer<? super D> disposer, boolean eager) { + this.downstream = actual; + this.resource = resource; + this.disposer = disposer; + this.eager = eager; + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.validate(this.upstream, d)) { + this.upstream = d; + downstream.onSubscribe(this); + } + } + + @Override + public void onNext(T t) { + downstream.onNext(t); + } + + @Override + public void onError(Throwable t) { + if (eager) { + if (compareAndSet(false, true)) { + try { + disposer.accept(resource); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + t = new CompositeException(t, e); + } + } + + downstream.onError(t); + } else { + downstream.onError(t); + disposeResource(); + } + } + + @Override + public void onComplete() { + if (eager) { + if (compareAndSet(false, true)) { + try { + disposer.accept(resource); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + downstream.onError(e); + return; + } + } + + downstream.onComplete(); + } else { + downstream.onComplete(); + disposeResource(); + } + } + + @Override + public void dispose() { + if (eager) { + disposeResource(); + upstream.dispose(); + upstream = DisposableHelper.DISPOSED; + } else { + upstream.dispose(); + upstream = DisposableHelper.DISPOSED; + disposeResource(); + } + } + + @Override + public boolean isDisposed() { + return get(); + } + + void disposeResource() { + if (compareAndSet(false, true)) { + try { + disposer.accept(resource); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + // can't call actual.onError unless it is serialized, which is expensive + RxJavaPlugins.onError(e); + } + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableWindow.java b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableWindow.java new file mode 100644 index 0000000000..c742ad6288 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableWindow.java @@ -0,0 +1,265 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import java.util.ArrayDeque; +import java.util.concurrent.atomic.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; +import io.reactivex.rxjava3.subjects.UnicastSubject; + +public final class ObservableWindow<T> extends AbstractObservableWithUpstream<T, Observable<T>> { + final long count; + final long skip; + final int capacityHint; + + public ObservableWindow(ObservableSource<T> source, long count, long skip, int capacityHint) { + super(source); + this.count = count; + this.skip = skip; + this.capacityHint = capacityHint; + } + + @Override + public void subscribeActual(Observer<? super Observable<T>> t) { + if (count == skip) { + source.subscribe(new WindowExactObserver<>(t, count, capacityHint)); + } else { + source.subscribe(new WindowSkipObserver<>(t, count, skip, capacityHint)); + } + } + + static final class WindowExactObserver<T> + extends AtomicInteger + implements Observer<T>, Disposable, Runnable { + + private static final long serialVersionUID = -7481782523886138128L; + final Observer<? super Observable<T>> downstream; + final long count; + final int capacityHint; + + final AtomicBoolean cancelled; + + long size; + + Disposable upstream; + + UnicastSubject<T> window; + + WindowExactObserver(Observer<? super Observable<T>> actual, long count, int capacityHint) { + this.downstream = actual; + this.count = count; + this.capacityHint = capacityHint; + this.cancelled = new AtomicBoolean(); + this.lazySet(1); + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.validate(this.upstream, d)) { + this.upstream = d; + + downstream.onSubscribe(this); + } + } + + @Override + public void onNext(T t) { + UnicastSubject<T> w = window; + ObservableWindowSubscribeIntercept<T> intercept = null; + if (w == null && !cancelled.get()) { + getAndIncrement(); + + w = UnicastSubject.create(capacityHint, this); + window = w; + intercept = new ObservableWindowSubscribeIntercept<>(w); + downstream.onNext(intercept); + } + + if (w != null) { + w.onNext(t); + + if (++size >= count) { + size = 0; + window = null; + w.onComplete(); + } + + if (intercept != null && intercept.tryAbandon()) { + window = null; + w.onComplete(); + w = null; + } + } + } + + @Override + public void onError(Throwable t) { + UnicastSubject<T> w = window; + if (w != null) { + window = null; + w.onError(t); + } + downstream.onError(t); + } + + @Override + public void onComplete() { + UnicastSubject<T> w = window; + if (w != null) { + window = null; + w.onComplete(); + } + downstream.onComplete(); + } + + @Override + public void dispose() { + if (cancelled.compareAndSet(false, true)) { + run(); + } + } + + @Override + public boolean isDisposed() { + return cancelled.get(); + } + + @Override + public void run() { + if (decrementAndGet() == 0) { + upstream.dispose(); + } + } + } + + static final class WindowSkipObserver<T> extends AtomicInteger + implements Observer<T>, Disposable, Runnable { + + private static final long serialVersionUID = 3366976432059579510L; + final Observer<? super Observable<T>> downstream; + final long count; + final long skip; + final int capacityHint; + final ArrayDeque<UnicastSubject<T>> windows; + + final AtomicBoolean cancelled; + + long index; + + /** Counts how many elements were emitted to the very first window in windows. */ + long firstEmission; + + Disposable upstream; + + WindowSkipObserver(Observer<? super Observable<T>> actual, long count, long skip, int capacityHint) { + this.downstream = actual; + this.count = count; + this.skip = skip; + this.capacityHint = capacityHint; + this.windows = new ArrayDeque<>(); + this.cancelled = new AtomicBoolean(); + this.lazySet(1); + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.validate(this.upstream, d)) { + this.upstream = d; + + downstream.onSubscribe(this); + } + } + + @Override + public void onNext(T t) { + final ArrayDeque<UnicastSubject<T>> ws = windows; + + long i = index; + + long s = skip; + + ObservableWindowSubscribeIntercept<T> intercept = null; + + if (i % s == 0 && !cancelled.get()) { + getAndIncrement(); + UnicastSubject<T> w = UnicastSubject.create(capacityHint, this); + intercept = new ObservableWindowSubscribeIntercept<>(w); + ws.offer(w); + downstream.onNext(intercept); + } + + long c = firstEmission + 1; + + for (UnicastSubject<T> w : ws) { + w.onNext(t); + } + + if (c >= count) { + ws.poll().onComplete(); + if (ws.isEmpty() && cancelled.get()) { + return; + } + firstEmission = c - s; + } else { + firstEmission = c; + } + + index = i + 1; + + if (intercept != null && intercept.tryAbandon()) { + intercept.window.onComplete(); + } + } + + @Override + public void onError(Throwable t) { + final ArrayDeque<UnicastSubject<T>> ws = windows; + while (!ws.isEmpty()) { + ws.poll().onError(t); + } + downstream.onError(t); + } + + @Override + public void onComplete() { + final ArrayDeque<UnicastSubject<T>> ws = windows; + while (!ws.isEmpty()) { + ws.poll().onComplete(); + } + downstream.onComplete(); + } + + @Override + public void dispose() { + if (cancelled.compareAndSet(false, true)) { + run(); + } + } + + @Override + public boolean isDisposed() { + return cancelled.get(); + } + + @Override + public void run() { + if (decrementAndGet() == 0) { + upstream.dispose(); + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableWindowBoundary.java b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableWindowBoundary.java new file mode 100644 index 0000000000..a1ecb8e869 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableWindowBoundary.java @@ -0,0 +1,286 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import java.util.concurrent.atomic.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; +import io.reactivex.rxjava3.internal.queue.MpscLinkedQueue; +import io.reactivex.rxjava3.internal.util.AtomicThrowable; +import io.reactivex.rxjava3.observers.DisposableObserver; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.subjects.UnicastSubject; + +public final class ObservableWindowBoundary<T, B> extends AbstractObservableWithUpstream<T, Observable<T>> { + final ObservableSource<B> other; + final int capacityHint; + + public ObservableWindowBoundary(ObservableSource<T> source, ObservableSource<B> other, int capacityHint) { + super(source); + this.other = other; + this.capacityHint = capacityHint; + } + + @Override + public void subscribeActual(Observer<? super Observable<T>> observer) { + WindowBoundaryMainObserver<T, B> parent = new WindowBoundaryMainObserver<>(observer, capacityHint); + + observer.onSubscribe(parent); + other.subscribe(parent.boundaryObserver); + + source.subscribe(parent); + } + + static final class WindowBoundaryMainObserver<T, B> + extends AtomicInteger + implements Observer<T>, Disposable, Runnable { + + private static final long serialVersionUID = 2233020065421370272L; + + final Observer<? super Observable<T>> downstream; + + final int capacityHint; + + final WindowBoundaryInnerObserver<T, B> boundaryObserver; + + final AtomicReference<Disposable> upstream; + + final AtomicInteger windows; + + final MpscLinkedQueue<Object> queue; + + final AtomicThrowable errors; + + final AtomicBoolean stopWindows; + + static final Object NEXT_WINDOW = new Object(); + + volatile boolean done; + + UnicastSubject<T> window; + + WindowBoundaryMainObserver(Observer<? super Observable<T>> downstream, int capacityHint) { + this.downstream = downstream; + this.capacityHint = capacityHint; + this.boundaryObserver = new WindowBoundaryInnerObserver<>(this); + this.upstream = new AtomicReference<>(); + this.windows = new AtomicInteger(1); + this.queue = new MpscLinkedQueue<>(); + this.errors = new AtomicThrowable(); + this.stopWindows = new AtomicBoolean(); + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.setOnce(upstream, d)) { + + innerNext(); + } + } + + @Override + public void onNext(T t) { + queue.offer(t); + drain(); + } + + @Override + public void onError(Throwable e) { + boundaryObserver.dispose(); + if (errors.tryAddThrowableOrReport(e)) { + done = true; + drain(); + } + } + + @Override + public void onComplete() { + boundaryObserver.dispose(); + done = true; + drain(); + } + + @Override + public void dispose() { + if (stopWindows.compareAndSet(false, true)) { + boundaryObserver.dispose(); + if (windows.decrementAndGet() == 0) { + DisposableHelper.dispose(upstream); + } + } + } + + @Override + public boolean isDisposed() { + return stopWindows.get(); + } + + @Override + public void run() { + if (windows.decrementAndGet() == 0) { + DisposableHelper.dispose(upstream); + } + } + + void innerNext() { + queue.offer(NEXT_WINDOW); + drain(); + } + + void innerError(Throwable e) { + DisposableHelper.dispose(upstream); + if (errors.tryAddThrowableOrReport(e)) { + done = true; + drain(); + } + } + + void innerComplete() { + DisposableHelper.dispose(upstream); + done = true; + drain(); + } + + @SuppressWarnings("unchecked") + void drain() { + if (getAndIncrement() != 0) { + return; + } + + int missed = 1; + Observer<? super Observable<T>> downstream = this.downstream; + MpscLinkedQueue<Object> queue = this.queue; + AtomicThrowable errors = this.errors; + + for (;;) { + + for (;;) { + if (windows.get() == 0) { + queue.clear(); + window = null; + return; + } + + UnicastSubject<T> w = window; + + boolean d = done; + + if (d && errors.get() != null) { + queue.clear(); + Throwable ex = errors.terminate(); + if (w != null) { + window = null; + w.onError(ex); + } + downstream.onError(ex); + return; + } + + Object v = queue.poll(); + + boolean empty = v == null; + + if (d && empty) { + Throwable ex = errors.terminate(); + if (ex == null) { + if (w != null) { + window = null; + w.onComplete(); + } + downstream.onComplete(); + } else { + if (w != null) { + window = null; + w.onError(ex); + } + downstream.onError(ex); + } + return; + } + + if (empty) { + break; + } + + if (v != NEXT_WINDOW) { + w.onNext((T)v); + continue; + } + + if (w != null) { + window = null; + w.onComplete(); + } + + if (!stopWindows.get()) { + w = UnicastSubject.create(capacityHint, this); + window = w; + windows.getAndIncrement(); + + ObservableWindowSubscribeIntercept<T> intercept = new ObservableWindowSubscribeIntercept<>(w); + downstream.onNext(intercept); + if (intercept.tryAbandon()) { + w.onComplete(); + } + } + } + + missed = addAndGet(-missed); + if (missed == 0) { + break; + } + } + } + } + + static final class WindowBoundaryInnerObserver<T, B> extends DisposableObserver<B> { + + final WindowBoundaryMainObserver<T, B> parent; + + boolean done; + + WindowBoundaryInnerObserver(WindowBoundaryMainObserver<T, B> parent) { + this.parent = parent; + } + + @Override + public void onNext(B t) { + if (done) { + return; + } + parent.innerNext(); + } + + @Override + public void onError(Throwable t) { + if (done) { + RxJavaPlugins.onError(t); + return; + } + done = true; + parent.innerError(t); + } + + @Override + public void onComplete() { + if (done) { + return; + } + done = true; + parent.innerComplete(); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableWindowBoundarySelector.java b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableWindowBoundarySelector.java new file mode 100644 index 0000000000..cec435839e --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableWindowBoundarySelector.java @@ -0,0 +1,425 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import java.util.*; +import java.util.concurrent.atomic.*; + +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.core.ObservableSource; +import io.reactivex.rxjava3.core.Observer; +import io.reactivex.rxjava3.disposables.*; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; +import io.reactivex.rxjava3.internal.queue.MpscLinkedQueue; +import io.reactivex.rxjava3.internal.util.*; +import io.reactivex.rxjava3.operators.SimplePlainQueue; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.subjects.UnicastSubject; + +public final class ObservableWindowBoundarySelector<T, B, V> extends AbstractObservableWithUpstream<T, Observable<T>> { + final ObservableSource<B> open; + final Function<? super B, ? extends ObservableSource<V>> closingIndicator; + final int bufferSize; + + public ObservableWindowBoundarySelector( + ObservableSource<T> source, + ObservableSource<B> open, Function<? super B, ? extends ObservableSource<V>> closingIndicator, + int bufferSize) { + super(source); + this.open = open; + this.closingIndicator = closingIndicator; + this.bufferSize = bufferSize; + } + + @Override + public void subscribeActual(Observer<? super Observable<T>> t) { + source.subscribe(new WindowBoundaryMainObserver<>( + t, open, closingIndicator, bufferSize)); + } + + static final class WindowBoundaryMainObserver<T, B, V> + extends AtomicInteger + implements Observer<T>, Disposable, Runnable { + private static final long serialVersionUID = 8646217640096099753L; + + final Observer<? super Observable<T>> downstream; + final ObservableSource<B> open; + final Function<? super B, ? extends ObservableSource<V>> closingIndicator; + final int bufferSize; + final CompositeDisposable resources; + + final WindowStartObserver<B> startObserver; + + final List<UnicastSubject<T>> windows; + + final SimplePlainQueue<Object> queue; + + final AtomicLong windowCount; + + final AtomicBoolean downstreamDisposed; + + final AtomicLong requested; + long emitted; + + volatile boolean upstreamCanceled; + + volatile boolean upstreamDone; + volatile boolean openDone; + final AtomicThrowable error; + + Disposable upstream; + + WindowBoundaryMainObserver(Observer<? super Observable<T>> downstream, + ObservableSource<B> open, Function<? super B, ? extends ObservableSource<V>> closingIndicator, int bufferSize) { + this.downstream = downstream; + this.queue = new MpscLinkedQueue<>(); + this.open = open; + this.closingIndicator = closingIndicator; + this.bufferSize = bufferSize; + this.resources = new CompositeDisposable(); + this.windows = new ArrayList<>(); + this.windowCount = new AtomicLong(1L); + this.downstreamDisposed = new AtomicBoolean(); + this.error = new AtomicThrowable(); + this.startObserver = new WindowStartObserver<>(this); + this.requested = new AtomicLong(); + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.validate(this.upstream, d)) { + this.upstream = d; + + downstream.onSubscribe(this); + + open.subscribe(startObserver); + } + } + + @Override + public void onNext(T t) { + queue.offer(t); + drain(); + } + + @Override + public void onError(Throwable t) { + startObserver.dispose(); + resources.dispose(); + if (error.tryAddThrowableOrReport(t)) { + upstreamDone = true; + drain(); + } + } + + @Override + public void onComplete() { + startObserver.dispose(); + resources.dispose(); + upstreamDone = true; + drain(); + } + + @Override + public void dispose() { + if (downstreamDisposed.compareAndSet(false, true)) { + if (windowCount.decrementAndGet() == 0) { + upstream.dispose(); + startObserver.dispose(); + resources.dispose(); + error.tryTerminateAndReport(); + upstreamCanceled = true; + drain(); + } else { + startObserver.dispose(); + } + } + } + + @Override + public boolean isDisposed() { + return downstreamDisposed.get(); + } + + @Override + public void run() { + if (windowCount.decrementAndGet() == 0) { + upstream.dispose(); + startObserver.dispose(); + resources.dispose(); + error.tryTerminateAndReport(); + upstreamCanceled = true; + drain(); + } + } + + void open(B startValue) { + queue.offer(new WindowStartItem<>(startValue)); + drain(); + } + + void openError(Throwable t) { + upstream.dispose(); + resources.dispose(); + if (error.tryAddThrowableOrReport(t)) { + upstreamDone = true; + drain(); + } + } + + void openComplete() { + openDone = true; + drain(); + } + + void close(WindowEndObserverIntercept<T, V> what) { + queue.offer(what); + drain(); + } + + void closeError(Throwable t) { + upstream.dispose(); + startObserver.dispose(); + resources.dispose(); + if (error.tryAddThrowableOrReport(t)) { + upstreamDone = true; + drain(); + } + } + + void drain() { + if (getAndIncrement() != 0) { + return; + } + + int missed = 1; + final Observer<? super Observable<T>> downstream = this.downstream; + final SimplePlainQueue<Object> queue = this.queue; + final List<UnicastSubject<T>> windows = this.windows; + + for (;;) { + if (upstreamCanceled) { + queue.clear(); + windows.clear(); + } else { + boolean isDone = upstreamDone; + Object o = queue.poll(); + boolean isEmpty = o == null; + + if (isDone) { + if (isEmpty || error.get() != null) { + terminateDownstream(downstream); + upstreamCanceled = true; + continue; + } + } + + if (!isEmpty) { + if (o instanceof WindowStartItem) { + if (!downstreamDisposed.get()) { + @SuppressWarnings("unchecked") + B startItem = ((WindowStartItem<B>)o).item; + + ObservableSource<V> endSource; + try { + endSource = Objects.requireNonNull(closingIndicator.apply(startItem), "The closingIndicator returned a null ObservableSource"); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + upstream.dispose(); + startObserver.dispose(); + resources.dispose(); + Exceptions.throwIfFatal(ex); + error.tryAddThrowableOrReport(ex); + upstreamDone = true; + continue; + } + + windowCount.getAndIncrement(); + UnicastSubject<T> newWindow = UnicastSubject.create(bufferSize, this); + WindowEndObserverIntercept<T, V> endObserver = new WindowEndObserverIntercept<>(this, newWindow); + + downstream.onNext(endObserver); + + if (endObserver.tryAbandon()) { + newWindow.onComplete(); + } else { + windows.add(newWindow); + resources.add(endObserver); + endSource.subscribe(endObserver); + } + } + } + else if (o instanceof WindowEndObserverIntercept) { + @SuppressWarnings("unchecked") + UnicastSubject<T> w = ((WindowEndObserverIntercept<T, V>)o).window; + + windows.remove(w); + resources.delete((Disposable)o); + w.onComplete(); + } else { + @SuppressWarnings("unchecked") + T item = (T)o; + + for (UnicastSubject<T> w : windows) { + w.onNext(item); + } + } + + continue; + } + else if (openDone && windows.size() == 0) { + upstream.dispose(); + startObserver.dispose(); + resources.dispose(); + terminateDownstream(downstream); + upstreamCanceled = true; + continue; + } + } + + missed = addAndGet(-missed); + if (missed == 0) { + break; + } + } + } + + void terminateDownstream(Observer<?> downstream) { + Throwable ex = error.terminate(); + if (ex == null) { + for (UnicastSubject<T> w : windows) { + w.onComplete(); + } + downstream.onComplete(); + } else if (ex != ExceptionHelper.TERMINATED) { + for (UnicastSubject<T> w : windows) { + w.onError(ex); + } + downstream.onError(ex); + } + } + + static final class WindowStartItem<B> { + + final B item; + + WindowStartItem(B item) { + this.item = item; + } + } + + static final class WindowStartObserver<B> extends AtomicReference<Disposable> + implements Observer<B> { + + private static final long serialVersionUID = -3326496781427702834L; + + final WindowBoundaryMainObserver<?, B, ?> parent; + + WindowStartObserver(WindowBoundaryMainObserver<?, B, ?> parent) { + this.parent = parent; + } + + @Override + public void onSubscribe(Disposable d) { + DisposableHelper.setOnce(this, d); + } + + @Override + public void onNext(B t) { + parent.open(t); + } + + @Override + public void onError(Throwable t) { + parent.openError(t); + } + + @Override + public void onComplete() { + parent.openComplete(); + } + + void dispose() { + DisposableHelper.dispose(this); + } + } + + static final class WindowEndObserverIntercept<T, V> extends Observable<T> + implements Observer<V>, Disposable { + + final WindowBoundaryMainObserver<T, ?, V> parent; + + final UnicastSubject<T> window; + + final AtomicReference<Disposable> upstream; + + final AtomicBoolean once; + + WindowEndObserverIntercept(WindowBoundaryMainObserver<T, ?, V> parent, UnicastSubject<T> window) { + this.parent = parent; + this.window = window; + this.upstream = new AtomicReference<>(); + this.once = new AtomicBoolean(); + } + + @Override + public void onSubscribe(Disposable d) { + DisposableHelper.setOnce(upstream, d); + } + + @Override + public void onNext(V t) { + if (DisposableHelper.dispose(upstream)) { + parent.close(this); + } + } + + @Override + public void onError(Throwable t) { + if (isDisposed()) { + RxJavaPlugins.onError(t); + } else { + parent.closeError(t); + } + } + + @Override + public void onComplete() { + parent.close(this); + } + + @Override + public void dispose() { + DisposableHelper.dispose(upstream); + } + + @Override + public boolean isDisposed() { + return upstream.get() == DisposableHelper.DISPOSED; + } + + @Override + protected void subscribeActual(Observer<? super T> o) { + window.subscribe(o); + once.set(true); + } + + boolean tryAbandon() { + return !once.get() && once.compareAndSet(false, true); + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableWindowSubscribeIntercept.java b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableWindowSubscribeIntercept.java new file mode 100644 index 0000000000..4cc879662d --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableWindowSubscribeIntercept.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import java.util.concurrent.atomic.AtomicBoolean; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.subjects.Subject; + +/** + * Wrapper for a Subject that detects an incoming subscriber. + * @param <T> the element type of the flow. + * @since 3.0.0 + */ +final class ObservableWindowSubscribeIntercept<T> extends Observable<T> { + + final Subject<T> window; + + final AtomicBoolean once; + + ObservableWindowSubscribeIntercept(Subject<T> source) { + this.window = source; + this.once = new AtomicBoolean(); + } + + @Override + protected void subscribeActual(Observer<? super T> s) { + window.subscribe(s); + once.set(true); + } + + boolean tryAbandon() { + return !once.get() && once.compareAndSet(false, true); + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableWindowTimed.java b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableWindowTimed.java new file mode 100644 index 0000000000..2cc96a61a9 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableWindowTimed.java @@ -0,0 +1,655 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import java.util.*; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.*; + +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.core.Observer; +import io.reactivex.rxjava3.core.Scheduler; +import io.reactivex.rxjava3.core.Scheduler.Worker; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.disposables.*; +import io.reactivex.rxjava3.internal.queue.MpscLinkedQueue; +import io.reactivex.rxjava3.operators.SimplePlainQueue; +import io.reactivex.rxjava3.subjects.UnicastSubject; + +public final class ObservableWindowTimed<T> extends AbstractObservableWithUpstream<T, Observable<T>> { + final long timespan; + final long timeskip; + final TimeUnit unit; + final Scheduler scheduler; + final long maxSize; + final int bufferSize; + final boolean restartTimerOnMaxSize; + + public ObservableWindowTimed(Observable<T> source, + long timespan, long timeskip, TimeUnit unit, Scheduler scheduler, long maxSize, + int bufferSize, boolean restartTimerOnMaxSize) { + super(source); + this.timespan = timespan; + this.timeskip = timeskip; + this.unit = unit; + this.scheduler = scheduler; + this.maxSize = maxSize; + this.bufferSize = bufferSize; + this.restartTimerOnMaxSize = restartTimerOnMaxSize; + } + + @Override + protected void subscribeActual(Observer<? super Observable<T>> downstream) { + if (timespan == timeskip) { + if (maxSize == Long.MAX_VALUE) { + source.subscribe(new WindowExactUnboundedObserver<>( + downstream, + timespan, unit, scheduler, bufferSize)); + return; + } + source.subscribe(new WindowExactBoundedObserver<>( + downstream, + timespan, unit, scheduler, + bufferSize, maxSize, restartTimerOnMaxSize)); + return; + } + source.subscribe(new WindowSkipObserver<>(downstream, + timespan, timeskip, unit, scheduler.createWorker(), bufferSize)); + } + + abstract static class AbstractWindowObserver<T> + extends AtomicInteger + implements Observer<T>, Disposable { + private static final long serialVersionUID = 5724293814035355511L; + + final Observer<? super Observable<T>> downstream; + + final SimplePlainQueue<Object> queue; + + final long timespan; + final TimeUnit unit; + final int bufferSize; + + long emitted; + + volatile boolean done; + Throwable error; + + Disposable upstream; + + final AtomicBoolean downstreamCancelled; + + volatile boolean upstreamCancelled; + + final AtomicInteger windowCount; + + AbstractWindowObserver(Observer<? super Observable<T>> downstream, long timespan, TimeUnit unit, int bufferSize) { + this.downstream = downstream; + this.queue = new MpscLinkedQueue<>(); + this.timespan = timespan; + this.unit = unit; + this.bufferSize = bufferSize; + this.downstreamCancelled = new AtomicBoolean(); + this.windowCount = new AtomicInteger(1); + } + + @Override + public final void onSubscribe(Disposable d) { + if (DisposableHelper.validate(this.upstream, d)) { + this.upstream = d; + + downstream.onSubscribe(this); + + createFirstWindow(); + } + } + + abstract void createFirstWindow(); + + @Override + public final void onNext(T t) { + queue.offer(t); + drain(); + } + + @Override + public final void onError(Throwable t) { + error = t; + done = true; + drain(); + } + + @Override + public final void onComplete() { + done = true; + drain(); + } + + @Override + public final void dispose() { + if (downstreamCancelled.compareAndSet(false, true)) { + windowDone(); + } + } + + @Override + public final boolean isDisposed() { + return downstreamCancelled.get(); + } + + final void windowDone() { + if (windowCount.decrementAndGet() == 0) { + cleanupResources(); + upstream.dispose(); + upstreamCancelled = true; + drain(); + } + } + + abstract void cleanupResources(); + + abstract void drain(); + } + + static final class WindowExactUnboundedObserver<T> + extends AbstractWindowObserver<T> + implements Runnable { + + private static final long serialVersionUID = 1155822639622580836L; + + final Scheduler scheduler; + + UnicastSubject<T> window; + + final SequentialDisposable timer; + + static final Object NEXT_WINDOW = new Object(); + + final Runnable windowRunnable; + + WindowExactUnboundedObserver(Observer<? super Observable<T>> actual, long timespan, TimeUnit unit, + Scheduler scheduler, int bufferSize) { + super(actual, timespan, unit, bufferSize); + this.scheduler = scheduler; + this.timer = new SequentialDisposable(); + this.windowRunnable = new WindowRunnable(); + } + + @Override + void createFirstWindow() { + if (!downstreamCancelled.get()) { + windowCount.getAndIncrement(); + window = UnicastSubject.create(bufferSize, windowRunnable); + + emitted = 1; + + ObservableWindowSubscribeIntercept<T> intercept = new ObservableWindowSubscribeIntercept<>(window); + downstream.onNext(intercept); + + timer.replace(scheduler.schedulePeriodicallyDirect(this, timespan, timespan, unit)); + + if (intercept.tryAbandon()) { + window.onComplete(); + } + } + } + + @Override + public void run() { + queue.offer(NEXT_WINDOW); + drain(); + } + + @Override + void drain() { + if (getAndIncrement() != 0) { + return; + } + + final SimplePlainQueue<Object> queue = this.queue; + final Observer<? super Observable<T>> downstream = this.downstream; + UnicastSubject<T> window = this.window; + + int missed = 1; + for (;;) { + + if (upstreamCancelled) { + queue.clear(); + window = null; + this.window = null; + } else { + boolean isDone = done; + Object o = queue.poll(); + boolean isEmpty = o == null; + + if (isDone && isEmpty) { + Throwable ex = error; + if (ex != null) { + if (window != null) { + window.onError(ex); + } + downstream.onError(ex); + } else { + if (window != null) { + window.onComplete(); + } + downstream.onComplete(); + } + cleanupResources(); + upstreamCancelled = true; + continue; + } + else if (!isEmpty) { + + if (o == NEXT_WINDOW) { + if (window != null) { + window.onComplete(); + window = null; + this.window = null; + } + if (downstreamCancelled.get()) { + timer.dispose(); + } else { + emitted++; + + windowCount.getAndIncrement(); + window = UnicastSubject.create(bufferSize, windowRunnable); + this.window = window; + + ObservableWindowSubscribeIntercept<T> intercept = new ObservableWindowSubscribeIntercept<>(window); + downstream.onNext(intercept); + + if (intercept.tryAbandon()) { + window.onComplete(); + } + } + } else if (window != null) { + @SuppressWarnings("unchecked") + T item = (T)o; + window.onNext(item); + } + + continue; + } + } + + missed = addAndGet(-missed); + if (missed == 0) { + break; + } + } + } + + @Override + void cleanupResources() { + timer.dispose(); + } + + final class WindowRunnable implements Runnable { + @Override + public void run() { + windowDone(); + } + } + } + + static final class WindowExactBoundedObserver<T> + extends AbstractWindowObserver<T> + implements Runnable { + private static final long serialVersionUID = -6130475889925953722L; + + final Scheduler scheduler; + final boolean restartTimerOnMaxSize; + final long maxSize; + final Scheduler.Worker worker; + + long count; + + UnicastSubject<T> window; + + final SequentialDisposable timer; + + WindowExactBoundedObserver( + Observer<? super Observable<T>> actual, + long timespan, TimeUnit unit, Scheduler scheduler, + int bufferSize, long maxSize, boolean restartTimerOnMaxSize) { + super(actual, timespan, unit, bufferSize); + this.scheduler = scheduler; + this.maxSize = maxSize; + this.restartTimerOnMaxSize = restartTimerOnMaxSize; + if (restartTimerOnMaxSize) { + worker = scheduler.createWorker(); + } else { + worker = null; + } + this.timer = new SequentialDisposable(); + } + + @Override + void createFirstWindow() { + if (!downstreamCancelled.get()) { + emitted = 1; + + windowCount.getAndIncrement(); + window = UnicastSubject.create(bufferSize, this); + + ObservableWindowSubscribeIntercept<T> intercept = new ObservableWindowSubscribeIntercept<>(window); + downstream.onNext(intercept); + + Runnable boundaryTask = new WindowBoundaryRunnable(this, 1L); + if (restartTimerOnMaxSize) { + timer.replace(worker.schedulePeriodically(boundaryTask, timespan, timespan, unit)); + } else { + timer.replace(scheduler.schedulePeriodicallyDirect(boundaryTask, timespan, timespan, unit)); + } + + if (intercept.tryAbandon()) { + window.onComplete(); + } + } + } + + @Override + public void run() { + windowDone(); + } + + @Override + void cleanupResources() { + timer.dispose(); + Worker w = worker; + if (w != null) { + w.dispose(); + } + } + + void boundary(WindowBoundaryRunnable sender) { + queue.offer(sender); + drain(); + } + + @Override + void drain() { + if (getAndIncrement() != 0) { + return; + } + + int missed = 1; + final SimplePlainQueue<Object> queue = this.queue; + final Observer<? super Observable<T>> downstream = this.downstream; + UnicastSubject<T> window = this.window; + + for (;;) { + + if (upstreamCancelled) { + queue.clear(); + window = null; + this.window = null; + } else { + + boolean isDone = done; + Object o = queue.poll(); + boolean isEmpty = o == null; + + if (isDone && isEmpty) { + Throwable ex = error; + if (ex != null) { + if (window != null) { + window.onError(ex); + } + downstream.onError(ex); + } else { + if (window != null) { + window.onComplete(); + } + downstream.onComplete(); + } + cleanupResources(); + upstreamCancelled = true; + continue; + } else if (!isEmpty) { + if (o instanceof WindowBoundaryRunnable) { + WindowBoundaryRunnable boundary = (WindowBoundaryRunnable) o; + if (boundary.index == emitted || !restartTimerOnMaxSize) { + this.count = 0; + window = createNewWindow(window); + } + } else if (window != null) { + @SuppressWarnings("unchecked") + T item = (T)o; + window.onNext(item); + + long count = this.count + 1; + if (count == maxSize) { + this.count = 0; + window = createNewWindow(window); + } else { + this.count = count; + } + } + + continue; + } + } + + missed = addAndGet(-missed); + if (missed == 0) { + break; + } + } + } + + UnicastSubject<T> createNewWindow(UnicastSubject<T> window) { + if (window != null) { + window.onComplete(); + window = null; + } + + if (downstreamCancelled.get()) { + cleanupResources(); + } else { + long emitted = this.emitted; + this.emitted = ++emitted; + + windowCount.getAndIncrement(); + window = UnicastSubject.create(bufferSize, this); + this.window = window; + + ObservableWindowSubscribeIntercept<T> intercept = new ObservableWindowSubscribeIntercept<>(window); + downstream.onNext(intercept); + + if (restartTimerOnMaxSize) { + timer.update(worker.schedulePeriodically(new WindowBoundaryRunnable(this, emitted), timespan, timespan, unit)); + } + + if (intercept.tryAbandon()) { + window.onComplete(); + } + } + + return window; + } + + static final class WindowBoundaryRunnable implements Runnable { + + final WindowExactBoundedObserver<?> parent; + + final long index; + + WindowBoundaryRunnable(WindowExactBoundedObserver<?> parent, long index) { + this.parent = parent; + this.index = index; + } + + @Override + public void run() { + parent.boundary(this); + } + } + } + + static final class WindowSkipObserver<T> + extends AbstractWindowObserver<T> + implements Runnable { + private static final long serialVersionUID = -7852870764194095894L; + + final long timeskip; + final Scheduler.Worker worker; + + final List<UnicastSubject<T>> windows; + + WindowSkipObserver(Observer<? super Observable<T>> actual, + long timespan, long timeskip, TimeUnit unit, + Worker worker, int bufferSize) { + super(actual, timespan, unit, bufferSize); + this.timeskip = timeskip; + this.worker = worker; + this.windows = new LinkedList<>(); + } + + @Override + void createFirstWindow() { + if (!downstreamCancelled.get()) { + emitted = 1; + + windowCount.getAndIncrement(); + UnicastSubject<T> window = UnicastSubject.create(bufferSize, this); + windows.add(window); + + ObservableWindowSubscribeIntercept<T> intercept = new ObservableWindowSubscribeIntercept<>(window); + downstream.onNext(intercept); + + worker.schedule(new WindowBoundaryRunnable(this, false), timespan, unit); + worker.schedulePeriodically(new WindowBoundaryRunnable(this, true), timeskip, timeskip, unit); + + if (intercept.tryAbandon()) { + window.onComplete(); + windows.remove(window); + } + } + } + + @Override + void cleanupResources() { + worker.dispose(); + } + + @Override + void drain() { + if (getAndIncrement() != 0) { + return; + } + + int missed = 1; + final SimplePlainQueue<Object> queue = this.queue; + final Observer<? super Observable<T>> downstream = this.downstream; + final List<UnicastSubject<T>> windows = this.windows; + + for (;;) { + if (upstreamCancelled) { + queue.clear(); + windows.clear(); + } else { + boolean isDone = done; + Object o = queue.poll(); + boolean isEmpty = o == null; + + if (isDone && isEmpty) { + Throwable ex = error; + if (ex != null) { + for (UnicastSubject<T> window : windows) { + window.onError(ex); + } + downstream.onError(ex); + } else { + for (UnicastSubject<T> window : windows) { + window.onComplete(); + } + downstream.onComplete(); + } + cleanupResources(); + upstreamCancelled = true; + continue; + } else if (!isEmpty) { + if (o == WINDOW_OPEN) { + if (!downstreamCancelled.get()) { + long emitted = this.emitted; + this.emitted = ++emitted; + + windowCount.getAndIncrement(); + UnicastSubject<T> window = UnicastSubject.create(bufferSize, this); + windows.add(window); + + ObservableWindowSubscribeIntercept<T> intercept = new ObservableWindowSubscribeIntercept<>(window); + downstream.onNext(intercept); + + worker.schedule(new WindowBoundaryRunnable(this, false), timespan, unit); + + if (intercept.tryAbandon()) { + window.onComplete(); + } + } + } else if (o == WINDOW_CLOSE) { + if (!windows.isEmpty()) { + windows.remove(0).onComplete(); + } + } else { + @SuppressWarnings("unchecked") + T item = (T)o; + for (UnicastSubject<T> window : windows) { + window.onNext(item); + } + } + continue; + } + } + missed = addAndGet(-missed); + if (missed == 0) { + break; + } + } + } + + @Override + public void run() { + windowDone(); + } + + void boundary(boolean isOpen) { + queue.offer(isOpen ? WINDOW_OPEN : WINDOW_CLOSE); + drain(); + } + + static final Object WINDOW_OPEN = new Object(); + static final Object WINDOW_CLOSE = new Object(); + + static final class WindowBoundaryRunnable implements Runnable { + + final WindowSkipObserver<?> parent; + + final boolean isOpen; + + WindowBoundaryRunnable(WindowSkipObserver<?> parent, boolean isOpen) { + this.parent = parent; + this.isOpen = isOpen; + } + + @Override + public void run() { + parent.boundary(isOpen); + } + } + } + +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableWithLatestFrom.java b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableWithLatestFrom.java new file mode 100644 index 0000000000..95b21187a7 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableWithLatestFrom.java @@ -0,0 +1,147 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import java.util.Objects; +import java.util.concurrent.atomic.AtomicReference; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.BiFunction; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; +import io.reactivex.rxjava3.observers.SerializedObserver; + +public final class ObservableWithLatestFrom<T, U, R> extends AbstractObservableWithUpstream<T, R> { + final BiFunction<? super T, ? super U, ? extends R> combiner; + final ObservableSource<? extends U> other; + public ObservableWithLatestFrom(ObservableSource<T> source, + BiFunction<? super T, ? super U, ? extends R> combiner, ObservableSource<? extends U> other) { + super(source); + this.combiner = combiner; + this.other = other; + } + + @Override + public void subscribeActual(Observer<? super R> t) { + final SerializedObserver<R> serial = new SerializedObserver<>(t); + final WithLatestFromObserver<T, U, R> wlf = new WithLatestFromObserver<>(serial, combiner); + + serial.onSubscribe(wlf); + + other.subscribe(new WithLatestFromOtherObserver(wlf)); + + source.subscribe(wlf); + } + + static final class WithLatestFromObserver<T, U, R> extends AtomicReference<U> implements Observer<T>, Disposable { + + private static final long serialVersionUID = -312246233408980075L; + + final Observer<? super R> downstream; + + final BiFunction<? super T, ? super U, ? extends R> combiner; + + final AtomicReference<Disposable> upstream = new AtomicReference<>(); + + final AtomicReference<Disposable> other = new AtomicReference<>(); + + WithLatestFromObserver(Observer<? super R> actual, BiFunction<? super T, ? super U, ? extends R> combiner) { + this.downstream = actual; + this.combiner = combiner; + } + + @Override + public void onSubscribe(Disposable d) { + DisposableHelper.setOnce(this.upstream, d); + } + + @Override + public void onNext(T t) { + U u = get(); + if (u != null) { + R r; + try { + r = Objects.requireNonNull(combiner.apply(t, u), "The combiner returned a null value"); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + dispose(); + downstream.onError(e); + return; + } + downstream.onNext(r); + } + } + + @Override + public void onError(Throwable t) { + DisposableHelper.dispose(other); + downstream.onError(t); + } + + @Override + public void onComplete() { + DisposableHelper.dispose(other); + downstream.onComplete(); + } + + @Override + public void dispose() { + DisposableHelper.dispose(upstream); + DisposableHelper.dispose(other); + } + + @Override + public boolean isDisposed() { + return DisposableHelper.isDisposed(upstream.get()); + } + + public boolean setOther(Disposable o) { + return DisposableHelper.setOnce(other, o); + } + + public void otherError(Throwable e) { + DisposableHelper.dispose(upstream); + downstream.onError(e); + } + } + + final class WithLatestFromOtherObserver implements Observer<U> { + private final WithLatestFromObserver<T, U, R> parent; + + WithLatestFromOtherObserver(WithLatestFromObserver<T, U, R> parent) { + this.parent = parent; + } + + @Override + public void onSubscribe(Disposable d) { + parent.setOther(d); + } + + @Override + public void onNext(U t) { + parent.lazySet(t); + } + + @Override + public void onError(Throwable t) { + parent.otherError(t); + } + + @Override + public void onComplete() { + // nothing to do, the wlf will complete on its own pace + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableWithLatestFromMany.java b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableWithLatestFromMany.java new file mode 100644 index 0000000000..f8327b8284 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableWithLatestFromMany.java @@ -0,0 +1,292 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import java.util.Arrays; +import java.util.Objects; +import java.util.concurrent.atomic.*; + +import io.reactivex.rxjava3.annotations.*; +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.disposables.*; +import io.reactivex.rxjava3.internal.util.*; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +/** + * Combines a main sequence of values with the latest from multiple other sequences via + * a selector function. + * + * @param <T> the main sequence's type + * @param <R> the output type + */ +public final class ObservableWithLatestFromMany<T, R> extends AbstractObservableWithUpstream<T, R> { + + @Nullable + final ObservableSource<?>[] otherArray; + + @Nullable + final Iterable<? extends ObservableSource<?>> otherIterable; + + @NonNull + final Function<? super Object[], R> combiner; + + public ObservableWithLatestFromMany(@NonNull ObservableSource<T> source, @NonNull ObservableSource<?>[] otherArray, @NonNull Function<? super Object[], R> combiner) { + super(source); + this.otherArray = otherArray; + this.otherIterable = null; + this.combiner = combiner; + } + + public ObservableWithLatestFromMany(@NonNull ObservableSource<T> source, @NonNull Iterable<? extends ObservableSource<?>> otherIterable, @NonNull Function<? super Object[], R> combiner) { + super(source); + this.otherArray = null; + this.otherIterable = otherIterable; + this.combiner = combiner; + } + + @Override + protected void subscribeActual(Observer<? super R> observer) { + ObservableSource<?>[] others = otherArray; + int n = 0; + if (others == null) { + others = new ObservableSource[8]; + + try { + for (ObservableSource<?> p : otherIterable) { + if (n == others.length) { + others = Arrays.copyOf(others, n + (n >> 1)); + } + others[n++] = p; + } + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + EmptyDisposable.error(ex, observer); + return; + } + + } else { + n = others.length; + } + + if (n == 0) { + new ObservableMap<>(source, new SingletonArrayFunc()).subscribeActual(observer); + return; + } + + WithLatestFromObserver<T, R> parent = new WithLatestFromObserver<>(observer, combiner, n); + observer.onSubscribe(parent); + parent.subscribe(others, n); + + source.subscribe(parent); + } + + static final class WithLatestFromObserver<T, R> + extends AtomicInteger + implements Observer<T>, Disposable { + + private static final long serialVersionUID = 1577321883966341961L; + + final Observer<? super R> downstream; + + final Function<? super Object[], R> combiner; + + final WithLatestInnerObserver[] observers; + + final AtomicReferenceArray<Object> values; + + final AtomicReference<Disposable> upstream; + + final AtomicThrowable error; + + volatile boolean done; + + WithLatestFromObserver(Observer<? super R> actual, Function<? super Object[], R> combiner, int n) { + this.downstream = actual; + this.combiner = combiner; + WithLatestInnerObserver[] s = new WithLatestInnerObserver[n]; + for (int i = 0; i < n; i++) { + s[i] = new WithLatestInnerObserver(this, i); + } + this.observers = s; + this.values = new AtomicReferenceArray<>(n); + this.upstream = new AtomicReference<>(); + this.error = new AtomicThrowable(); + } + + void subscribe(ObservableSource<?>[] others, int n) { + WithLatestInnerObserver[] observers = this.observers; + AtomicReference<Disposable> upstream = this.upstream; + for (int i = 0; i < n; i++) { + if (DisposableHelper.isDisposed(upstream.get()) || done) { + return; + } + others[i].subscribe(observers[i]); + } + } + + @Override + public void onSubscribe(Disposable d) { + DisposableHelper.setOnce(this.upstream, d); + } + + @Override + public void onNext(T t) { + if (done) { + return; + } + AtomicReferenceArray<Object> ara = values; + int n = ara.length(); + Object[] objects = new Object[n + 1]; + objects[0] = t; + + for (int i = 0; i < n; i++) { + Object o = ara.get(i); + if (o == null) { + // no latest, skip this value + return; + } + objects[i + 1] = o; + } + + R v; + + try { + v = Objects.requireNonNull(combiner.apply(objects), "combiner returned a null value"); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + dispose(); + onError(ex); + return; + } + + HalfSerializer.onNext(downstream, v, this, error); + } + + @Override + public void onError(Throwable t) { + if (done) { + RxJavaPlugins.onError(t); + return; + } + done = true; + cancelAllBut(-1); + HalfSerializer.onError(downstream, t, this, error); + } + + @Override + public void onComplete() { + if (!done) { + done = true; + cancelAllBut(-1); + HalfSerializer.onComplete(downstream, this, error); + } + } + + @Override + public boolean isDisposed() { + return DisposableHelper.isDisposed(upstream.get()); + } + + @Override + public void dispose() { + DisposableHelper.dispose(upstream); + for (WithLatestInnerObserver observer : observers) { + observer.dispose(); + } + } + + void innerNext(int index, Object o) { + values.set(index, o); + } + + void innerError(int index, Throwable t) { + done = true; + DisposableHelper.dispose(upstream); + cancelAllBut(index); + HalfSerializer.onError(downstream, t, this, error); + } + + void innerComplete(int index, boolean nonEmpty) { + if (!nonEmpty) { + done = true; + cancelAllBut(index); + HalfSerializer.onComplete(downstream, this, error); + } + } + + void cancelAllBut(int index) { + WithLatestInnerObserver[] observers = this.observers; + for (int i = 0; i < observers.length; i++) { + if (i != index) { + observers[i].dispose(); + } + } + } + } + + static final class WithLatestInnerObserver + extends AtomicReference<Disposable> + implements Observer<Object> { + + private static final long serialVersionUID = 3256684027868224024L; + + final WithLatestFromObserver<?, ?> parent; + + final int index; + + boolean hasValue; + + WithLatestInnerObserver(WithLatestFromObserver<?, ?> parent, int index) { + this.parent = parent; + this.index = index; + } + + @Override + public void onSubscribe(Disposable d) { + DisposableHelper.setOnce(this, d); + } + + @Override + public void onNext(Object t) { + if (!hasValue) { + hasValue = true; + } + parent.innerNext(index, t); + } + + @Override + public void onError(Throwable t) { + parent.innerError(index, t); + } + + @Override + public void onComplete() { + parent.innerComplete(index, hasValue); + } + + public void dispose() { + DisposableHelper.dispose(this); + } + } + + final class SingletonArrayFunc implements Function<T, R> { + @Override + public R apply(T t) throws Throwable { + return Objects.requireNonNull(combiner.apply(new Object[] { t }), "The combiner returned a null value"); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableZip.java b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableZip.java new file mode 100644 index 0000000000..e1626b4865 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableZip.java @@ -0,0 +1,301 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import java.util.Arrays; +import java.util.Objects; +import java.util.concurrent.atomic.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.disposables.*; +import io.reactivex.rxjava3.operators.SpscLinkedArrayQueue; + +public final class ObservableZip<T, R> extends Observable<R> { + + final ObservableSource<? extends T>[] sources; + final Iterable<? extends ObservableSource<? extends T>> sourcesIterable; + final Function<? super Object[], ? extends R> zipper; + final int bufferSize; + final boolean delayError; + + public ObservableZip(ObservableSource<? extends T>[] sources, + Iterable<? extends ObservableSource<? extends T>> sourcesIterable, + Function<? super Object[], ? extends R> zipper, + int bufferSize, + boolean delayError) { + this.sources = sources; + this.sourcesIterable = sourcesIterable; + this.zipper = zipper; + this.bufferSize = bufferSize; + this.delayError = delayError; + } + + @Override + @SuppressWarnings("unchecked") + public void subscribeActual(Observer<? super R> observer) { + ObservableSource<? extends T>[] sources = this.sources; + int count = 0; + if (sources == null) { + sources = new ObservableSource[8]; + for (ObservableSource<? extends T> p : sourcesIterable) { + if (count == sources.length) { + ObservableSource<? extends T>[] b = new ObservableSource[count + (count >> 2)]; + System.arraycopy(sources, 0, b, 0, count); + sources = b; + } + sources[count++] = p; + } + } else { + count = sources.length; + } + + if (count == 0) { + EmptyDisposable.complete(observer); + return; + } + + ZipCoordinator<T, R> zc = new ZipCoordinator<>(observer, zipper, count, delayError); + zc.subscribe(sources, bufferSize); + } + + static final class ZipCoordinator<T, R> extends AtomicInteger implements Disposable { + + private static final long serialVersionUID = 2983708048395377667L; + final Observer<? super R> downstream; + final Function<? super Object[], ? extends R> zipper; + final ZipObserver<T, R>[] observers; + final T[] row; + final boolean delayError; + + volatile boolean cancelled; + + @SuppressWarnings("unchecked") + ZipCoordinator(Observer<? super R> actual, + Function<? super Object[], ? extends R> zipper, + int count, boolean delayError) { + this.downstream = actual; + this.zipper = zipper; + this.observers = new ZipObserver[count]; + this.row = (T[])new Object[count]; + this.delayError = delayError; + } + + public void subscribe(ObservableSource<? extends T>[] sources, int bufferSize) { + ZipObserver<T, R>[] s = observers; + int len = s.length; + for (int i = 0; i < len; i++) { + s[i] = new ZipObserver<>(this, bufferSize); + } + // this makes sure the contents of the observers array is visible + this.lazySet(0); + downstream.onSubscribe(this); + for (int i = 0; i < len; i++) { + if (cancelled) { + return; + } + sources[i].subscribe(s[i]); + } + } + + @Override + public void dispose() { + if (!cancelled) { + cancelled = true; + cancelSources(); + if (getAndIncrement() == 0) { + clear(); + } + } + } + + @Override + public boolean isDisposed() { + return cancelled; + } + + void cancel() { + clear(); + cancelSources(); + } + + void cancelSources() { + for (ZipObserver<?, ?> zs : observers) { + zs.dispose(); + } + } + + void clear() { + for (ZipObserver<?, ?> zs : observers) { + zs.queue.clear(); + } + } + + public void drain() { + if (getAndIncrement() != 0) { + return; + } + + int missing = 1; + + final ZipObserver<T, R>[] zs = observers; + final Observer<? super R> a = downstream; + final T[] os = row; + final boolean delayError = this.delayError; + + for (;;) { + + for (;;) { + int i = 0; + int emptyCount = 0; + for (ZipObserver<T, R> z : zs) { + if (os[i] == null) { + boolean d = z.done; + T v = z.queue.poll(); + boolean empty = v == null; + + if (checkTerminated(d, empty, a, delayError, z)) { + return; + } + if (!empty) { + os[i] = v; + } else { + emptyCount++; + } + } else { + if (z.done && !delayError) { + Throwable ex = z.error; + if (ex != null) { + cancelled = true; + cancel(); + a.onError(ex); + return; + } + } + } + i++; + } + + if (emptyCount != 0) { + break; + } + + R v; + try { + v = Objects.requireNonNull(zipper.apply(os.clone()), "The zipper returned a null value"); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + cancel(); + a.onError(ex); + return; + } + + a.onNext(v); + + Arrays.fill(os, null); + } + + missing = addAndGet(-missing); + if (missing == 0) { + return; + } + } + } + + boolean checkTerminated(boolean d, boolean empty, Observer<? super R> a, boolean delayError, ZipObserver<?, ?> source) { + if (cancelled) { + cancel(); + return true; + } + + if (d) { + if (delayError) { + if (empty) { + Throwable e = source.error; + cancelled = true; + cancel(); + if (e != null) { + a.onError(e); + } else { + a.onComplete(); + } + return true; + } + } else { + Throwable e = source.error; + if (e != null) { + cancelled = true; + cancel(); + a.onError(e); + return true; + } else + if (empty) { + cancelled = true; + cancel(); + a.onComplete(); + return true; + } + } + } + + return false; + } + } + + static final class ZipObserver<T, R> implements Observer<T> { + + final ZipCoordinator<T, R> parent; + final SpscLinkedArrayQueue<T> queue; + + volatile boolean done; + Throwable error; + + final AtomicReference<Disposable> upstream = new AtomicReference<>(); + + ZipObserver(ZipCoordinator<T, R> parent, int bufferSize) { + this.parent = parent; + this.queue = new SpscLinkedArrayQueue<>(bufferSize); + } + + @Override + public void onSubscribe(Disposable d) { + DisposableHelper.setOnce(this.upstream, d); + } + + @Override + public void onNext(T t) { + queue.offer(t); + parent.drain(); + } + + @Override + public void onError(Throwable t) { + error = t; + done = true; + parent.drain(); + } + + @Override + public void onComplete() { + done = true; + parent.drain(); + } + + public void dispose() { + DisposableHelper.dispose(upstream); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableZipIterable.java b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableZipIterable.java new file mode 100644 index 0000000000..7c8f5e6d0e --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObservableZipIterable.java @@ -0,0 +1,173 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import java.util.Iterator; +import java.util.Objects; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.BiFunction; +import io.reactivex.rxjava3.internal.disposables.*; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +public final class ObservableZipIterable<T, U, V> extends Observable<V> { + final Observable<? extends T> source; + final Iterable<U> other; + final BiFunction<? super T, ? super U, ? extends V> zipper; + + public ObservableZipIterable( + Observable<? extends T> source, + Iterable<U> other, BiFunction<? super T, ? super U, ? extends V> zipper) { + this.source = source; + this.other = other; + this.zipper = zipper; + } + + @Override + public void subscribeActual(Observer<? super V> t) { + Iterator<U> it; + + try { + it = Objects.requireNonNull(other.iterator(), "The iterator returned by other is null"); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + EmptyDisposable.error(e, t); + return; + } + + boolean b; + + try { + b = it.hasNext(); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + EmptyDisposable.error(e, t); + return; + } + + if (!b) { + EmptyDisposable.complete(t); + return; + } + + source.subscribe(new ZipIterableObserver<T, U, V>(t, it, zipper)); + } + + static final class ZipIterableObserver<T, U, V> implements Observer<T>, Disposable { + final Observer<? super V> downstream; + final Iterator<U> iterator; + final BiFunction<? super T, ? super U, ? extends V> zipper; + + Disposable upstream; + + boolean done; + + ZipIterableObserver(Observer<? super V> actual, Iterator<U> iterator, + BiFunction<? super T, ? super U, ? extends V> zipper) { + this.downstream = actual; + this.iterator = iterator; + this.zipper = zipper; + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.validate(this.upstream, d)) { + this.upstream = d; + downstream.onSubscribe(this); + } + } + + @Override + public void dispose() { + upstream.dispose(); + } + + @Override + public boolean isDisposed() { + return upstream.isDisposed(); + } + + @Override + public void onNext(T t) { + if (done) { + return; + } + + U u; + + try { + u = Objects.requireNonNull(iterator.next(), "The iterator returned a null value"); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + fail(e); + return; + } + + V v; + try { + v = Objects.requireNonNull(zipper.apply(t, u), "The zipper function returned a null value"); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + fail(e); + return; + } + + downstream.onNext(v); + + boolean b; + + try { + b = iterator.hasNext(); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + fail(e); + return; + } + + if (!b) { + done = true; + upstream.dispose(); + downstream.onComplete(); + } + } + + void fail(Throwable e) { + done = true; + upstream.dispose(); + downstream.onError(e); + } + + @Override + public void onError(Throwable t) { + if (done) { + RxJavaPlugins.onError(t); + return; + } + done = true; + downstream.onError(t); + } + + @Override + public void onComplete() { + if (done) { + return; + } + done = true; + downstream.onComplete(); + } + + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObserverResourceWrapper.java b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObserverResourceWrapper.java new file mode 100644 index 0000000000..a3c797fee6 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/observable/ObserverResourceWrapper.java @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import java.util.concurrent.atomic.AtomicReference; + +import io.reactivex.rxjava3.core.Observer; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; + +public final class ObserverResourceWrapper<T> extends AtomicReference<Disposable> implements Observer<T>, Disposable { + + private static final long serialVersionUID = -8612022020200669122L; + + final Observer<? super T> downstream; + + final AtomicReference<Disposable> upstream = new AtomicReference<>(); + + public ObserverResourceWrapper(Observer<? super T> downstream) { + this.downstream = downstream; + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.setOnce(upstream, d)) { + downstream.onSubscribe(this); + } + } + + @Override + public void onNext(T t) { + downstream.onNext(t); + } + + @Override + public void onError(Throwable t) { + dispose(); + downstream.onError(t); + } + + @Override + public void onComplete() { + dispose(); + downstream.onComplete(); + } + + @Override + public void dispose() { + DisposableHelper.dispose(upstream); + + DisposableHelper.dispose(this); + } + + @Override + public boolean isDisposed() { + return upstream.get() == DisposableHelper.DISPOSED; + } + + public void setResource(Disposable resource) { + DisposableHelper.set(this, resource); + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/parallel/ParallelCollect.java b/src/main/java/io/reactivex/rxjava3/internal/operators/parallel/ParallelCollect.java new file mode 100644 index 0000000000..60e36beed4 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/parallel/ParallelCollect.java @@ -0,0 +1,160 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.parallel; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.subscribers.DeferredScalarSubscriber; +import io.reactivex.rxjava3.internal.subscriptions.*; +import io.reactivex.rxjava3.parallel.ParallelFlowable; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +import java.util.Objects; + +/** + * Reduce the sequence of values in each 'rail' to a single value. + * + * @param <T> the input value type + * @param <C> the collection type + */ +public final class ParallelCollect<T, C> extends ParallelFlowable<C> { + + final ParallelFlowable<? extends T> source; + + final Supplier<? extends C> initialCollection; + + final BiConsumer<? super C, ? super T> collector; + + public ParallelCollect(ParallelFlowable<? extends T> source, + Supplier<? extends C> initialCollection, BiConsumer<? super C, ? super T> collector) { + this.source = source; + this.initialCollection = initialCollection; + this.collector = collector; + } + + @Override + public void subscribe(Subscriber<? super C>[] subscribers) { + subscribers = RxJavaPlugins.onSubscribe(this, subscribers); + + if (!validate(subscribers)) { + return; + } + + int n = subscribers.length; + @SuppressWarnings("unchecked") + Subscriber<T>[] parents = new Subscriber[n]; + + for (int i = 0; i < n; i++) { + + C initialValue; + + try { + initialValue = Objects.requireNonNull(initialCollection.get(), "The initialSupplier returned a null value"); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + reportError(subscribers, ex); + return; + } + + parents[i] = new ParallelCollectSubscriber<>(subscribers[i], initialValue, collector); + } + + source.subscribe(parents); + } + + void reportError(Subscriber<?>[] subscribers, Throwable ex) { + for (Subscriber<?> s : subscribers) { + EmptySubscription.error(ex, s); + } + } + + @Override + public int parallelism() { + return source.parallelism(); + } + + static final class ParallelCollectSubscriber<T, C> extends DeferredScalarSubscriber<T, C> { + + private static final long serialVersionUID = -4767392946044436228L; + + final BiConsumer<? super C, ? super T> collector; + + C collection; + + boolean done; + + ParallelCollectSubscriber(Subscriber<? super C> subscriber, + C initialValue, BiConsumer<? super C, ? super T> collector) { + super(subscriber); + this.collection = initialValue; + this.collector = collector; + } + + @Override + public void onSubscribe(Subscription s) { + if (SubscriptionHelper.validate(this.upstream, s)) { + this.upstream = s; + + downstream.onSubscribe(this); + + s.request(Long.MAX_VALUE); + } + } + + @Override + public void onNext(T t) { + if (done) { + return; + } + + try { + collector.accept(collection, t); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + cancel(); + onError(ex); + } + } + + @Override + public void onError(Throwable t) { + if (done) { + RxJavaPlugins.onError(t); + return; + } + done = true; + collection = null; + downstream.onError(t); + } + + @Override + public void onComplete() { + if (done) { + return; + } + done = true; + C c = collection; + collection = null; + complete(c); + } + + @Override + public void cancel() { + super.cancel(); + upstream.cancel(); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/parallel/ParallelConcatMap.java b/src/main/java/io/reactivex/rxjava3/internal/operators/parallel/ParallelConcatMap.java new file mode 100644 index 0000000000..ebdcf0bdf3 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/parallel/ParallelConcatMap.java @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.parallel; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.operators.flowable.FlowableConcatMap; +import io.reactivex.rxjava3.internal.util.ErrorMode; +import io.reactivex.rxjava3.parallel.ParallelFlowable; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +import java.util.Objects; + +/** + * Concatenates the generated Publishers on each rail. + * + * @param <T> the input value type + * @param <R> the output value type + */ +public final class ParallelConcatMap<T, R> extends ParallelFlowable<R> { + + final ParallelFlowable<T> source; + + final Function<? super T, ? extends Publisher<? extends R>> mapper; + + final int prefetch; + + final ErrorMode errorMode; + + public ParallelConcatMap( + ParallelFlowable<T> source, + Function<? super T, ? extends Publisher<? extends R>> mapper, + int prefetch, ErrorMode errorMode) { + this.source = source; + this.mapper = Objects.requireNonNull(mapper, "mapper"); + this.prefetch = prefetch; + this.errorMode = Objects.requireNonNull(errorMode, "errorMode"); + } + + @Override + public int parallelism() { + return source.parallelism(); + } + + @Override + public void subscribe(Subscriber<? super R>[] subscribers) { + subscribers = RxJavaPlugins.onSubscribe(this, subscribers); + + if (!validate(subscribers)) { + return; + } + + int n = subscribers.length; + + @SuppressWarnings("unchecked") + final Subscriber<T>[] parents = new Subscriber[n]; + + for (int i = 0; i < n; i++) { + parents[i] = FlowableConcatMap.subscribe(subscribers[i], mapper, prefetch, errorMode); + } + + source.subscribe(parents); + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/parallel/ParallelDoOnNextTry.java b/src/main/java/io/reactivex/rxjava3/internal/operators/parallel/ParallelDoOnNextTry.java new file mode 100644 index 0000000000..b8ff388535 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/parallel/ParallelDoOnNextTry.java @@ -0,0 +1,298 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.parallel; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.subscriptions.SubscriptionHelper; +import io.reactivex.rxjava3.operators.ConditionalSubscriber; +import io.reactivex.rxjava3.parallel.*; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +import java.util.Objects; + +/** + * Calls a Consumer for each upstream value passing by + * and handles any failure with a handler function. + * <p>History: 2.0.8 - experimental + * @param <T> the input value type + * @since 2.2 + */ +public final class ParallelDoOnNextTry<T> extends ParallelFlowable<T> { + + final ParallelFlowable<T> source; + + final Consumer<? super T> onNext; + + final BiFunction<? super Long, ? super Throwable, ParallelFailureHandling> errorHandler; + + public ParallelDoOnNextTry(ParallelFlowable<T> source, Consumer<? super T> onNext, + BiFunction<? super Long, ? super Throwable, ParallelFailureHandling> errorHandler) { + this.source = source; + this.onNext = onNext; + this.errorHandler = errorHandler; + } + + @Override + public void subscribe(Subscriber<? super T>[] subscribers) { + subscribers = RxJavaPlugins.onSubscribe(this, subscribers); + + if (!validate(subscribers)) { + return; + } + + int n = subscribers.length; + @SuppressWarnings("unchecked") + Subscriber<? super T>[] parents = new Subscriber[n]; + + for (int i = 0; i < n; i++) { + Subscriber<? super T> a = subscribers[i]; + if (a instanceof ConditionalSubscriber) { + parents[i] = new ParallelDoOnNextConditionalSubscriber<>((ConditionalSubscriber<? super T>)a, onNext, errorHandler); + } else { + parents[i] = new ParallelDoOnNextSubscriber<>(a, onNext, errorHandler); + } + } + + source.subscribe(parents); + } + + @Override + public int parallelism() { + return source.parallelism(); + } + + static final class ParallelDoOnNextSubscriber<T> implements ConditionalSubscriber<T>, Subscription { + + final Subscriber<? super T> downstream; + + final Consumer<? super T> onNext; + + final BiFunction<? super Long, ? super Throwable, ParallelFailureHandling> errorHandler; + + Subscription upstream; + + boolean done; + + ParallelDoOnNextSubscriber(Subscriber<? super T> actual, Consumer<? super T> onNext, + BiFunction<? super Long, ? super Throwable, ParallelFailureHandling> errorHandler) { + this.downstream = actual; + this.onNext = onNext; + this.errorHandler = errorHandler; + } + + @Override + public void request(long n) { + upstream.request(n); + } + + @Override + public void cancel() { + upstream.cancel(); + } + + @Override + public void onSubscribe(Subscription s) { + if (SubscriptionHelper.validate(this.upstream, s)) { + this.upstream = s; + + downstream.onSubscribe(this); + } + } + + @Override + public void onNext(T t) { + if (!tryOnNext(t)) { + upstream.request(1); + } + } + + @Override + public boolean tryOnNext(T t) { + if (done) { + return false; + } + long retries = 0; + + for (;;) { + try { + onNext.accept(t); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + + ParallelFailureHandling h; + + try { + h = Objects.requireNonNull(errorHandler.apply(++retries, ex), "The errorHandler returned a null ParallelFailureHandling"); + } catch (Throwable exc) { + Exceptions.throwIfFatal(exc); + cancel(); + onError(new CompositeException(ex, exc)); + return false; + } + + switch (h) { + case RETRY: + continue; + case SKIP: + return false; + case STOP: + cancel(); + onComplete(); + return false; + default: + cancel(); + onError(ex); + return false; + } + } + + downstream.onNext(t); + return true; + } + } + + @Override + public void onError(Throwable t) { + if (done) { + RxJavaPlugins.onError(t); + return; + } + done = true; + downstream.onError(t); + } + + @Override + public void onComplete() { + if (done) { + return; + } + done = true; + downstream.onComplete(); + } + + } + static final class ParallelDoOnNextConditionalSubscriber<T> implements ConditionalSubscriber<T>, Subscription { + + final ConditionalSubscriber<? super T> downstream; + + final Consumer<? super T> onNext; + + final BiFunction<? super Long, ? super Throwable, ParallelFailureHandling> errorHandler; + + Subscription upstream; + + boolean done; + + ParallelDoOnNextConditionalSubscriber(ConditionalSubscriber<? super T> actual, + Consumer<? super T> onNext, + BiFunction<? super Long, ? super Throwable, ParallelFailureHandling> errorHandler) { + this.downstream = actual; + this.onNext = onNext; + this.errorHandler = errorHandler; + } + + @Override + public void request(long n) { + upstream.request(n); + } + + @Override + public void cancel() { + upstream.cancel(); + } + + @Override + public void onSubscribe(Subscription s) { + if (SubscriptionHelper.validate(this.upstream, s)) { + this.upstream = s; + + downstream.onSubscribe(this); + } + } + + @Override + public void onNext(T t) { + if (!tryOnNext(t) && !done) { + upstream.request(1); + } + } + + @Override + public boolean tryOnNext(T t) { + if (done) { + return false; + } + long retries = 0; + + for (;;) { + try { + onNext.accept(t); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + + ParallelFailureHandling h; + + try { + h = Objects.requireNonNull(errorHandler.apply(++retries, ex), "The errorHandler returned a null ParallelFailureHandling"); + } catch (Throwable exc) { + Exceptions.throwIfFatal(exc); + cancel(); + onError(new CompositeException(ex, exc)); + return false; + } + + switch (h) { + case RETRY: + continue; + case SKIP: + return false; + case STOP: + cancel(); + onComplete(); + return false; + default: + cancel(); + onError(ex); + return false; + } + } + + return downstream.tryOnNext(t); + } + } + + @Override + public void onError(Throwable t) { + if (done) { + RxJavaPlugins.onError(t); + return; + } + done = true; + downstream.onError(t); + } + + @Override + public void onComplete() { + if (done) { + return; + } + done = true; + downstream.onComplete(); + } + + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/parallel/ParallelFilter.java b/src/main/java/io/reactivex/rxjava3/internal/operators/parallel/ParallelFilter.java new file mode 100644 index 0000000000..28987a6fc9 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/parallel/ParallelFilter.java @@ -0,0 +1,214 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.parallel; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.Predicate; +import io.reactivex.rxjava3.internal.subscriptions.SubscriptionHelper; +import io.reactivex.rxjava3.operators.ConditionalSubscriber; +import io.reactivex.rxjava3.parallel.ParallelFlowable; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +/** + * Filters each 'rail' of the source ParallelFlowable with a predicate function. + * + * @param <T> the input value type + */ +public final class ParallelFilter<T> extends ParallelFlowable<T> { + + final ParallelFlowable<T> source; + + final Predicate<? super T> predicate; + + public ParallelFilter(ParallelFlowable<T> source, Predicate<? super T> predicate) { + this.source = source; + this.predicate = predicate; + } + + @Override + public void subscribe(Subscriber<? super T>[] subscribers) { + subscribers = RxJavaPlugins.onSubscribe(this, subscribers); + + if (!validate(subscribers)) { + return; + } + + int n = subscribers.length; + @SuppressWarnings("unchecked") + Subscriber<? super T>[] parents = new Subscriber[n]; + + for (int i = 0; i < n; i++) { + Subscriber<? super T> a = subscribers[i]; + if (a instanceof ConditionalSubscriber) { + parents[i] = new ParallelFilterConditionalSubscriber<>((ConditionalSubscriber<? super T>)a, predicate); + } else { + parents[i] = new ParallelFilterSubscriber<>(a, predicate); + } + } + + source.subscribe(parents); + } + + @Override + public int parallelism() { + return source.parallelism(); + } + + abstract static class BaseFilterSubscriber<T> implements ConditionalSubscriber<T>, Subscription { + final Predicate<? super T> predicate; + + Subscription upstream; + + boolean done; + + BaseFilterSubscriber(Predicate<? super T> predicate) { + this.predicate = predicate; + } + + @Override + public final void request(long n) { + upstream.request(n); + } + + @Override + public final void cancel() { + upstream.cancel(); + } + + @Override + public final void onNext(T t) { + if (!tryOnNext(t) && !done) { + upstream.request(1); + } + } + } + + static final class ParallelFilterSubscriber<T> extends BaseFilterSubscriber<T> { + + final Subscriber<? super T> downstream; + + ParallelFilterSubscriber(Subscriber<? super T> actual, Predicate<? super T> predicate) { + super(predicate); + this.downstream = actual; + } + + @Override + public void onSubscribe(Subscription s) { + if (SubscriptionHelper.validate(this.upstream, s)) { + this.upstream = s; + + downstream.onSubscribe(this); + } + } + + @Override + public boolean tryOnNext(T t) { + if (!done) { + boolean b; + + try { + b = predicate.test(t); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + cancel(); + onError(ex); + return false; + } + + if (b) { + downstream.onNext(t); + return true; + } + } + return false; + } + + @Override + public void onError(Throwable t) { + if (done) { + RxJavaPlugins.onError(t); + return; + } + done = true; + downstream.onError(t); + } + + @Override + public void onComplete() { + if (!done) { + done = true; + downstream.onComplete(); + } + } + } + + static final class ParallelFilterConditionalSubscriber<T> extends BaseFilterSubscriber<T> { + + final ConditionalSubscriber<? super T> downstream; + + ParallelFilterConditionalSubscriber(ConditionalSubscriber<? super T> actual, Predicate<? super T> predicate) { + super(predicate); + this.downstream = actual; + } + + @Override + public void onSubscribe(Subscription s) { + if (SubscriptionHelper.validate(this.upstream, s)) { + this.upstream = s; + + downstream.onSubscribe(this); + } + } + + @Override + public boolean tryOnNext(T t) { + if (!done) { + boolean b; + + try { + b = predicate.test(t); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + cancel(); + onError(ex); + return false; + } + + if (b) { + return downstream.tryOnNext(t); + } + } + return false; + } + + @Override + public void onError(Throwable t) { + if (done) { + RxJavaPlugins.onError(t); + return; + } + done = true; + downstream.onError(t); + } + + @Override + public void onComplete() { + if (!done) { + done = true; + downstream.onComplete(); + } + } + }} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/parallel/ParallelFilterTry.java b/src/main/java/io/reactivex/rxjava3/internal/operators/parallel/ParallelFilterTry.java new file mode 100644 index 0000000000..563937e1a6 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/parallel/ParallelFilterTry.java @@ -0,0 +1,278 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.parallel; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.subscriptions.SubscriptionHelper; +import io.reactivex.rxjava3.operators.ConditionalSubscriber; +import io.reactivex.rxjava3.parallel.*; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +import java.util.Objects; + +/** + * Filters each 'rail' of the source ParallelFlowable with a predicate function. + * + * @param <T> the input value type + */ +public final class ParallelFilterTry<T> extends ParallelFlowable<T> { + + final ParallelFlowable<T> source; + + final Predicate<? super T> predicate; + + final BiFunction<? super Long, ? super Throwable, ParallelFailureHandling> errorHandler; + + public ParallelFilterTry(ParallelFlowable<T> source, Predicate<? super T> predicate, + BiFunction<? super Long, ? super Throwable, ParallelFailureHandling> errorHandler) { + this.source = source; + this.predicate = predicate; + this.errorHandler = errorHandler; + } + + @Override + public void subscribe(Subscriber<? super T>[] subscribers) { + subscribers = RxJavaPlugins.onSubscribe(this, subscribers); + + if (!validate(subscribers)) { + return; + } + + int n = subscribers.length; + @SuppressWarnings("unchecked") + Subscriber<? super T>[] parents = new Subscriber[n]; + + for (int i = 0; i < n; i++) { + Subscriber<? super T> a = subscribers[i]; + if (a instanceof ConditionalSubscriber) { + parents[i] = new ParallelFilterConditionalSubscriber<>((ConditionalSubscriber<? super T>)a, predicate, errorHandler); + } else { + parents[i] = new ParallelFilterSubscriber<>(a, predicate, errorHandler); + } + } + + source.subscribe(parents); + } + + @Override + public int parallelism() { + return source.parallelism(); + } + + abstract static class BaseFilterSubscriber<T> implements ConditionalSubscriber<T>, Subscription { + final Predicate<? super T> predicate; + + final BiFunction<? super Long, ? super Throwable, ParallelFailureHandling> errorHandler; + + Subscription upstream; + + boolean done; + + BaseFilterSubscriber(Predicate<? super T> predicate, BiFunction<? super Long, ? super Throwable, ParallelFailureHandling> errorHandler) { + this.predicate = predicate; + this.errorHandler = errorHandler; + } + + @Override + public final void request(long n) { + upstream.request(n); + } + + @Override + public final void cancel() { + upstream.cancel(); + } + + @Override + public final void onNext(T t) { + if (!tryOnNext(t) && !done) { + upstream.request(1); + } + } + } + + static final class ParallelFilterSubscriber<T> extends BaseFilterSubscriber<T> { + + final Subscriber<? super T> downstream; + + ParallelFilterSubscriber(Subscriber<? super T> actual, Predicate<? super T> predicate, BiFunction<? super Long, ? super Throwable, ParallelFailureHandling> errorHandler) { + super(predicate, errorHandler); + this.downstream = actual; + } + + @Override + public void onSubscribe(Subscription s) { + if (SubscriptionHelper.validate(this.upstream, s)) { + this.upstream = s; + + downstream.onSubscribe(this); + } + } + + @Override + public boolean tryOnNext(T t) { + if (!done) { + long retries = 0L; + + for (;;) { + boolean b; + + try { + b = predicate.test(t); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + + ParallelFailureHandling h; + + try { + h = Objects.requireNonNull(errorHandler.apply(++retries, ex), "The errorHandler returned a null ParallelFailureHandling"); + } catch (Throwable exc) { + Exceptions.throwIfFatal(exc); + cancel(); + onError(new CompositeException(ex, exc)); + return false; + } + + switch (h) { + case RETRY: + continue; + case SKIP: + return false; + case STOP: + cancel(); + onComplete(); + return false; + default: + cancel(); + onError(ex); + return false; + } + } + + if (b) { + downstream.onNext(t); + return true; + } + return false; + } + } + return false; + } + + @Override + public void onError(Throwable t) { + if (done) { + RxJavaPlugins.onError(t); + return; + } + done = true; + downstream.onError(t); + } + + @Override + public void onComplete() { + if (!done) { + done = true; + downstream.onComplete(); + } + } + } + + static final class ParallelFilterConditionalSubscriber<T> extends BaseFilterSubscriber<T> { + + final ConditionalSubscriber<? super T> downstream; + + ParallelFilterConditionalSubscriber(ConditionalSubscriber<? super T> actual, + Predicate<? super T> predicate, + BiFunction<? super Long, ? super Throwable, ParallelFailureHandling> errorHandler) { + super(predicate, errorHandler); + this.downstream = actual; + } + + @Override + public void onSubscribe(Subscription s) { + if (SubscriptionHelper.validate(this.upstream, s)) { + this.upstream = s; + + downstream.onSubscribe(this); + } + } + + @Override + public boolean tryOnNext(T t) { + if (!done) { + long retries = 0L; + + for (;;) { + boolean b; + + try { + b = predicate.test(t); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + + ParallelFailureHandling h; + + try { + h = Objects.requireNonNull(errorHandler.apply(++retries, ex), "The errorHandler returned a null ParallelFailureHandling"); + } catch (Throwable exc) { + Exceptions.throwIfFatal(exc); + cancel(); + onError(new CompositeException(ex, exc)); + return false; + } + + switch (h) { + case RETRY: + continue; + case SKIP: + return false; + case STOP: + cancel(); + onComplete(); + return false; + default: + cancel(); + onError(ex); + return false; + } + } + + return b && downstream.tryOnNext(t); + } + } + return false; + } + + @Override + public void onError(Throwable t) { + if (done) { + RxJavaPlugins.onError(t); + return; + } + done = true; + downstream.onError(t); + } + + @Override + public void onComplete() { + if (!done) { + done = true; + downstream.onComplete(); + } + } + }} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/parallel/ParallelFlatMap.java b/src/main/java/io/reactivex/rxjava3/internal/operators/parallel/ParallelFlatMap.java new file mode 100644 index 0000000000..227521143a --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/parallel/ParallelFlatMap.java @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.parallel; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.operators.flowable.FlowableFlatMap; +import io.reactivex.rxjava3.parallel.ParallelFlowable; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +/** + * Flattens the generated Publishers on each rail. + * + * @param <T> the input value type + * @param <R> the output value type + */ +public final class ParallelFlatMap<T, R> extends ParallelFlowable<R> { + + final ParallelFlowable<T> source; + + final Function<? super T, ? extends Publisher<? extends R>> mapper; + + final boolean delayError; + + final int maxConcurrency; + + final int prefetch; + + public ParallelFlatMap( + ParallelFlowable<T> source, + Function<? super T, ? extends Publisher<? extends R>> mapper, + boolean delayError, + int maxConcurrency, + int prefetch) { + this.source = source; + this.mapper = mapper; + this.delayError = delayError; + this.maxConcurrency = maxConcurrency; + this.prefetch = prefetch; + } + + @Override + public int parallelism() { + return source.parallelism(); + } + + @Override + public void subscribe(Subscriber<? super R>[] subscribers) { + subscribers = RxJavaPlugins.onSubscribe(this, subscribers); + + if (!validate(subscribers)) { + return; + } + + int n = subscribers.length; + + @SuppressWarnings("unchecked") + final Subscriber<T>[] parents = new Subscriber[n]; + + for (int i = 0; i < n; i++) { + parents[i] = FlowableFlatMap.subscribe(subscribers[i], mapper, delayError, maxConcurrency, prefetch); + } + + source.subscribe(parents); + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/parallel/ParallelFlatMapIterable.java b/src/main/java/io/reactivex/rxjava3/internal/operators/parallel/ParallelFlatMapIterable.java new file mode 100644 index 0000000000..9e6c45a9ca --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/parallel/ParallelFlatMapIterable.java @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.parallel; + +import org.reactivestreams.Subscriber; + +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.operators.flowable.FlowableFlattenIterable; +import io.reactivex.rxjava3.parallel.ParallelFlowable; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +/** + * Flattens the generated {@link Iterable}s on each rail. + * + * @param <T> the input value type + * @param <R> the output value type + * @since 3.0.0 + */ +public final class ParallelFlatMapIterable<T, R> extends ParallelFlowable<R> { + + final ParallelFlowable<T> source; + + final Function<? super T, ? extends Iterable<? extends R>> mapper; + + final int prefetch; + + public ParallelFlatMapIterable( + ParallelFlowable<T> source, + Function<? super T, ? extends Iterable<? extends R>> mapper, + int prefetch) { + this.source = source; + this.mapper = mapper; + this.prefetch = prefetch; + } + + @Override + public int parallelism() { + return source.parallelism(); + } + + @Override + public void subscribe(Subscriber<? super R>[] subscribers) { + subscribers = RxJavaPlugins.onSubscribe(this, subscribers); + + if (!validate(subscribers)) { + return; + } + + int n = subscribers.length; + + @SuppressWarnings("unchecked") + final Subscriber<T>[] parents = new Subscriber[n]; + + for (int i = 0; i < n; i++) { + parents[i] = FlowableFlattenIterable.subscribe(subscribers[i], mapper, prefetch); + } + + source.subscribe(parents); + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/parallel/ParallelFromArray.java b/src/main/java/io/reactivex/rxjava3/internal/operators/parallel/ParallelFromArray.java new file mode 100644 index 0000000000..d2512ccd7f --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/parallel/ParallelFromArray.java @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.parallel; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.parallel.ParallelFlowable; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +/** + * Wraps multiple Publishers into a ParallelFlowable which runs them + * in parallel. + * + * @param <T> the value type + */ +public final class ParallelFromArray<T> extends ParallelFlowable<T> { + final Publisher<T>[] sources; + + public ParallelFromArray(Publisher<T>[] sources) { + this.sources = sources; + } + + @Override + public int parallelism() { + return sources.length; + } + + @Override + public void subscribe(Subscriber<? super T>[] subscribers) { + subscribers = RxJavaPlugins.onSubscribe(this, subscribers); + + if (!validate(subscribers)) { + return; + } + + int n = subscribers.length; + + for (int i = 0; i < n; i++) { + sources[i].subscribe(subscribers[i]); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/parallel/ParallelFromPublisher.java b/src/main/java/io/reactivex/rxjava3/internal/operators/parallel/ParallelFromPublisher.java new file mode 100644 index 0000000000..eb57bccea5 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/parallel/ParallelFromPublisher.java @@ -0,0 +1,440 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.parallel; + +import java.util.concurrent.atomic.*; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.FlowableSubscriber; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.internal.subscriptions.SubscriptionHelper; +import io.reactivex.rxjava3.internal.util.BackpressureHelper; +import io.reactivex.rxjava3.operators.QueueSubscription; +import io.reactivex.rxjava3.operators.SimpleQueue; +import io.reactivex.rxjava3.operators.SpscArrayQueue; +import io.reactivex.rxjava3.parallel.ParallelFlowable; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +/** + * Dispatches the values from upstream in a round robin fashion to subscribers which are + * ready to consume elements. A value from upstream is sent to only one of the subscribers. + * + * @param <T> the value type + */ +public final class ParallelFromPublisher<T> extends ParallelFlowable<T> { + final Publisher<? extends T> source; + + final int parallelism; + + final int prefetch; + + public ParallelFromPublisher(Publisher<? extends T> source, int parallelism, int prefetch) { + this.source = source; + this.parallelism = parallelism; + this.prefetch = prefetch; + } + + @Override + public int parallelism() { + return parallelism; + } + + @Override + public void subscribe(Subscriber<? super T>[] subscribers) { + subscribers = RxJavaPlugins.onSubscribe(this, subscribers); + + if (!validate(subscribers)) { + return; + } + + source.subscribe(new ParallelDispatcher<>(subscribers, prefetch)); + } + + static final class ParallelDispatcher<T> + extends AtomicInteger + implements FlowableSubscriber<T> { + + private static final long serialVersionUID = -4470634016609963609L; + + final Subscriber<? super T>[] subscribers; + + final AtomicLongArray requests; + + final long[] emissions; + + final int prefetch; + + final int limit; + + Subscription upstream; + + SimpleQueue<T> queue; + + Throwable error; + + volatile boolean done; + + int index; + + volatile boolean cancelled; + + /** + * Counts how many subscribers were setup to delay triggering the + * drain of upstream until all of them have been setup. + */ + final AtomicInteger subscriberCount = new AtomicInteger(); + + int produced; + + int sourceMode; + + ParallelDispatcher(Subscriber<? super T>[] subscribers, int prefetch) { + this.subscribers = subscribers; + this.prefetch = prefetch; + this.limit = prefetch - (prefetch >> 2); + int m = subscribers.length; + this.requests = new AtomicLongArray(m + m + 1); + this.requests.lazySet(m + m, m); + this.emissions = new long[m]; + } + + @Override + public void onSubscribe(Subscription s) { + if (SubscriptionHelper.validate(this.upstream, s)) { + this.upstream = s; + + if (s instanceof QueueSubscription) { + @SuppressWarnings("unchecked") + QueueSubscription<T> qs = (QueueSubscription<T>) s; + + int m = qs.requestFusion(QueueSubscription.ANY | QueueSubscription.BOUNDARY); + + if (m == QueueSubscription.SYNC) { + sourceMode = m; + queue = qs; + done = true; + setupSubscribers(); + drain(); + return; + } else + if (m == QueueSubscription.ASYNC) { + sourceMode = m; + queue = qs; + + setupSubscribers(); + + s.request(prefetch); + + return; + } + } + + queue = new SpscArrayQueue<>(prefetch); + + setupSubscribers(); + + s.request(prefetch); + } + } + + void setupSubscribers() { + Subscriber<? super T>[] subs = subscribers; + final int m = subs.length; + + for (int i = 0; i < m; i++) { + subscriberCount.lazySet(i + 1); + + subs[i].onSubscribe(new RailSubscription(i, m)); + } + } + + final class RailSubscription implements Subscription { + + final int j; + + final int m; + + RailSubscription(int j, int m) { + this.j = j; + this.m = m; + } + + @Override + public void request(long n) { + if (SubscriptionHelper.validate(n)) { + AtomicLongArray ra = requests; + for (;;) { + long r = ra.get(j); + if (r == Long.MAX_VALUE) { + return; + } + long u = BackpressureHelper.addCap(r, n); + if (ra.compareAndSet(j, r, u)) { + break; + } + } + if (subscriberCount.get() == m) { + drain(); + } + } + } + + @Override + public void cancel() { + if (requests.compareAndSet(m + j, 0L, 1L)) { + ParallelDispatcher.this.cancel(m + m); + } + } + } + + @Override + public void onNext(T t) { + if (sourceMode == QueueSubscription.NONE) { + if (!queue.offer(t)) { + upstream.cancel(); + onError(new QueueOverflowException()); + return; + } + } + drain(); + } + + @Override + public void onError(Throwable t) { + error = t; + done = true; + drain(); + } + + @Override + public void onComplete() { + done = true; + drain(); + } + + void cancel(int m) { + if (requests.decrementAndGet(m) == 0L) { + cancelled = true; + this.upstream.cancel(); + + if (getAndIncrement() == 0) { + queue.clear(); + } + } + } + + void drainAsync() { + int missed = 1; + + SimpleQueue<T> q = queue; + Subscriber<? super T>[] a = this.subscribers; + AtomicLongArray r = this.requests; + long[] e = this.emissions; + int n = e.length; + int idx = index; + int consumed = produced; + + for (;;) { + + int notReady = 0; + + for (;;) { + if (cancelled) { + q.clear(); + return; + } + + boolean d = done; + if (d) { + Throwable ex = error; + if (ex != null) { + q.clear(); + for (Subscriber<? super T> s : a) { + s.onError(ex); + } + return; + } + } + + boolean empty = q.isEmpty(); + + if (d && empty) { + for (Subscriber<? super T> s : a) { + s.onComplete(); + } + return; + } + + if (empty) { + break; + } + + long requestAtIndex = r.get(idx); + long emissionAtIndex = e[idx]; + if (requestAtIndex != emissionAtIndex && r.get(n + idx) == 0) { + + T v; + + try { + v = q.poll(); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + upstream.cancel(); + for (Subscriber<? super T> s : a) { + s.onError(ex); + } + return; + } + + if (v == null) { + break; + } + + a[idx].onNext(v); + + e[idx] = emissionAtIndex + 1; + + int c = ++consumed; + if (c == limit) { + consumed = 0; + upstream.request(c); + } + notReady = 0; + } else { + notReady++; + } + + idx++; + if (idx == n) { + idx = 0; + } + + if (notReady == n) { + break; + } + } + + int w = get(); + if (w == missed) { + index = idx; + produced = consumed; + missed = addAndGet(-missed); + if (missed == 0) { + break; + } + } else { + missed = w; + } + } + } + + void drainSync() { + int missed = 1; + + SimpleQueue<T> q = queue; + Subscriber<? super T>[] a = this.subscribers; + AtomicLongArray r = this.requests; + long[] e = this.emissions; + int n = e.length; + int idx = index; + + for (;;) { + + int notReady = 0; + + for (;;) { + if (cancelled) { + q.clear(); + return; + } + + boolean empty = q.isEmpty(); + + if (empty) { + for (Subscriber<? super T> s : a) { + s.onComplete(); + } + return; + } + + long requestAtIndex = r.get(idx); + long emissionAtIndex = e[idx]; + if (requestAtIndex != emissionAtIndex && r.get(n + idx) == 0) { + + T v; + + try { + v = q.poll(); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + upstream.cancel(); + for (Subscriber<? super T> s : a) { + s.onError(ex); + } + return; + } + + if (v == null) { + for (Subscriber<? super T> s : a) { + s.onComplete(); + } + return; + } + + a[idx].onNext(v); + + e[idx] = emissionAtIndex + 1; + + notReady = 0; + } else { + notReady++; + } + + idx++; + if (idx == n) { + idx = 0; + } + + if (notReady == n) { + break; + } + } + + int w = get(); + if (w == missed) { + index = idx; + missed = addAndGet(-missed); + if (missed == 0) { + break; + } + } else { + missed = w; + } + } + } + + void drain() { + if (getAndIncrement() != 0) { + return; + } + + if (sourceMode == QueueSubscription.SYNC) { + drainSync(); + } else { + drainAsync(); + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/parallel/ParallelJoin.java b/src/main/java/io/reactivex/rxjava3/internal/operators/parallel/ParallelJoin.java new file mode 100644 index 0000000000..2852fda8c7 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/parallel/ParallelJoin.java @@ -0,0 +1,549 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.parallel; + +import java.util.concurrent.atomic.*; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.internal.subscriptions.SubscriptionHelper; +import io.reactivex.rxjava3.internal.util.*; +import io.reactivex.rxjava3.operators.SimplePlainQueue; +import io.reactivex.rxjava3.operators.SimpleQueue; +import io.reactivex.rxjava3.operators.SpscArrayQueue; +import io.reactivex.rxjava3.parallel.ParallelFlowable; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +/** + * Merges the individual 'rails' of the source ParallelFlowable, unordered, + * into a single regular Publisher sequence (exposed as Flowable). + * + * @param <T> the value type + */ +public final class ParallelJoin<T> extends Flowable<T> { + + final ParallelFlowable<? extends T> source; + + final int prefetch; + + final boolean delayErrors; + + public ParallelJoin(ParallelFlowable<? extends T> source, int prefetch, boolean delayErrors) { + this.source = source; + this.prefetch = prefetch; + this.delayErrors = delayErrors; + } + + @Override + protected void subscribeActual(Subscriber<? super T> s) { + JoinSubscriptionBase<T> parent; + if (delayErrors) { + parent = new JoinSubscriptionDelayError<>(s, source.parallelism(), prefetch); + } else { + parent = new JoinSubscription<>(s, source.parallelism(), prefetch); + } + s.onSubscribe(parent); + source.subscribe(parent.subscribers); + } + + abstract static class JoinSubscriptionBase<T> extends AtomicInteger + implements Subscription { + + private static final long serialVersionUID = 3100232009247827843L; + + final Subscriber<? super T> downstream; + + final JoinInnerSubscriber<T>[] subscribers; + + final AtomicThrowable errors = new AtomicThrowable(); + + final AtomicLong requested = new AtomicLong(); + + volatile boolean cancelled; + + final AtomicInteger done = new AtomicInteger(); + + JoinSubscriptionBase(Subscriber<? super T> actual, int n, int prefetch) { + this.downstream = actual; + @SuppressWarnings("unchecked") + JoinInnerSubscriber<T>[] a = new JoinInnerSubscriber[n]; + + for (int i = 0; i < n; i++) { + a[i] = new JoinInnerSubscriber<>(this, prefetch); + } + + this.subscribers = a; + done.lazySet(n); + } + + @Override + public void request(long n) { + if (SubscriptionHelper.validate(n)) { + BackpressureHelper.add(requested, n); + drain(); + } + } + + @Override + public void cancel() { + if (!cancelled) { + cancelled = true; + + cancelAll(); + + if (getAndIncrement() == 0) { + cleanup(); + } + } + } + + void cancelAll() { + for (JoinInnerSubscriber<T> s : subscribers) { + s.cancel(); + } + } + + void cleanup() { + for (JoinInnerSubscriber<T> s : subscribers) { + s.queue = null; + } + } + + abstract void onNext(JoinInnerSubscriber<T> inner, T value); + + abstract void onError(Throwable e); + + abstract void onComplete(); + + abstract void drain(); + } + + static final class JoinSubscription<T> extends JoinSubscriptionBase<T> { + + private static final long serialVersionUID = 6312374661811000451L; + + JoinSubscription(Subscriber<? super T> actual, int n, int prefetch) { + super(actual, n, prefetch); + } + + @Override + public void onNext(JoinInnerSubscriber<T> inner, T value) { + if (get() == 0 && compareAndSet(0, 1)) { + if (requested.get() != 0) { + downstream.onNext(value); + if (requested.get() != Long.MAX_VALUE) { + requested.decrementAndGet(); + } + inner.request(1); + } else { + SimplePlainQueue<T> q = inner.getQueue(); + + if (!q.offer(value)) { + cancelAll(); + Throwable mbe = new QueueOverflowException(); + if (errors.compareAndSet(null, mbe)) { + downstream.onError(mbe); + } else { + RxJavaPlugins.onError(mbe); + } + return; + } + } + if (decrementAndGet() == 0) { + return; + } + } else { + SimplePlainQueue<T> q = inner.getQueue(); + + if (!q.offer(value)) { + cancelAll(); + onError(new QueueOverflowException()); + return; + } + + if (getAndIncrement() != 0) { + return; + } + } + + drainLoop(); + } + + @Override + public void onError(Throwable e) { + if (errors.compareAndSet(null, e)) { + cancelAll(); + drain(); + } else { + if (e != errors.get()) { + RxJavaPlugins.onError(e); + } + } + } + + @Override + public void onComplete() { + done.decrementAndGet(); + drain(); + } + + @Override + void drain() { + if (getAndIncrement() != 0) { + return; + } + + drainLoop(); + } + + void drainLoop() { + int missed = 1; + + JoinInnerSubscriber<T>[] s = this.subscribers; + int n = s.length; + Subscriber<? super T> a = this.downstream; + + for (;;) { + + long r = requested.get(); + long e = 0; + + middle: + while (e != r) { + if (cancelled) { + cleanup(); + return; + } + + Throwable ex = errors.get(); + if (ex != null) { + cleanup(); + a.onError(ex); + return; + } + + boolean d = done.get() == 0; + + boolean empty = true; + + for (int i = 0; i < s.length; i++) { + JoinInnerSubscriber<T> inner = s[i]; + SimplePlainQueue<T> q = inner.queue; + if (q != null) { + T v = q.poll(); + + if (v != null) { + empty = false; + a.onNext(v); + inner.requestOne(); + if (++e == r) { + break middle; + } + } + } + } + + if (d && empty) { + a.onComplete(); + return; + } + + if (empty) { + break; + } + } + + if (e == r) { + if (cancelled) { + cleanup(); + return; + } + + Throwable ex = errors.get(); + if (ex != null) { + cleanup(); + a.onError(ex); + return; + } + + boolean d = done.get() == 0; + + boolean empty = true; + + for (int i = 0; i < n; i++) { + JoinInnerSubscriber<T> inner = s[i]; + + SimpleQueue<T> q = inner.queue; + if (q != null && !q.isEmpty()) { + empty = false; + break; + } + } + + if (d && empty) { + a.onComplete(); + return; + } + } + + if (e != 0) { + BackpressureHelper.produced(requested, e); + } + + missed = addAndGet(-missed); + if (missed == 0) { + break; + } + } + } + } + + static final class JoinSubscriptionDelayError<T> extends JoinSubscriptionBase<T> { + + private static final long serialVersionUID = -5737965195918321883L; + + JoinSubscriptionDelayError(Subscriber<? super T> actual, int n, int prefetch) { + super(actual, n, prefetch); + } + + @Override + void onNext(JoinInnerSubscriber<T> inner, T value) { + if (get() == 0 && compareAndSet(0, 1)) { + if (requested.get() != 0) { + downstream.onNext(value); + if (requested.get() != Long.MAX_VALUE) { + requested.decrementAndGet(); + } + inner.request(1); + } else { + SimplePlainQueue<T> q = inner.getQueue(); + + if (!q.offer(value)) { + inner.cancel(); + errors.tryAddThrowableOrReport(new QueueOverflowException()); + done.decrementAndGet(); + drainLoop(); + return; + } + } + if (decrementAndGet() == 0) { + return; + } + } else { + SimplePlainQueue<T> q = inner.getQueue(); + + if (!q.offer(value)) { + inner.cancel(); + errors.tryAddThrowableOrReport(new QueueOverflowException()); + done.decrementAndGet(); + } + + if (getAndIncrement() != 0) { + return; + } + } + + drainLoop(); + } + + @Override + void onError(Throwable e) { + if (errors.tryAddThrowableOrReport(e)) { + done.decrementAndGet(); + drain(); + } + } + + @Override + void onComplete() { + done.decrementAndGet(); + drain(); + } + + @Override + void drain() { + if (getAndIncrement() != 0) { + return; + } + + drainLoop(); + } + + void drainLoop() { + int missed = 1; + + JoinInnerSubscriber<T>[] s = this.subscribers; + int n = s.length; + Subscriber<? super T> a = this.downstream; + + for (;;) { + + long r = requested.get(); + long e = 0; + + middle: + while (e != r) { + if (cancelled) { + cleanup(); + return; + } + + boolean d = done.get() == 0; + + boolean empty = true; + + for (int i = 0; i < n; i++) { + JoinInnerSubscriber<T> inner = s[i]; + + SimplePlainQueue<T> q = inner.queue; + if (q != null) { + T v = q.poll(); + + if (v != null) { + empty = false; + a.onNext(v); + inner.requestOne(); + if (++e == r) { + break middle; + } + } + } + } + + if (d && empty) { + errors.tryTerminateConsumer(a); + return; + } + + if (empty) { + break; + } + } + + if (e == r) { + if (cancelled) { + cleanup(); + return; + } + + boolean d = done.get() == 0; + + boolean empty = true; + + for (int i = 0; i < n; i++) { + JoinInnerSubscriber<T> inner = s[i]; + + SimpleQueue<T> q = inner.queue; + if (q != null && !q.isEmpty()) { + empty = false; + break; + } + } + + if (d && empty) { + errors.tryTerminateConsumer(a); + return; + } + } + + if (e != 0) { + BackpressureHelper.produced(requested, e); + } + + missed = addAndGet(-missed); + if (missed == 0) { + break; + } + } + } + } + + static final class JoinInnerSubscriber<T> + extends AtomicReference<Subscription> + implements FlowableSubscriber<T> { + + private static final long serialVersionUID = 8410034718427740355L; + + final JoinSubscriptionBase<T> parent; + + final int prefetch; + + final int limit; + + long produced; + + volatile SimplePlainQueue<T> queue; + + JoinInnerSubscriber(JoinSubscriptionBase<T> parent, int prefetch) { + this.parent = parent; + this.prefetch = prefetch ; + this.limit = prefetch - (prefetch >> 2); + } + + @Override + public void onSubscribe(Subscription s) { + SubscriptionHelper.setOnce(this, s, prefetch); + } + + @Override + public void onNext(T t) { + parent.onNext(this, t); + } + + @Override + public void onError(Throwable t) { + parent.onError(t); + } + + @Override + public void onComplete() { + parent.onComplete(); + } + + public void requestOne() { + long p = produced + 1; + if (p == limit) { + produced = 0; + get().request(p); + } else { + produced = p; + } + } + + public void request(long n) { + long p = produced + n; + if (p >= limit) { + produced = 0; + get().request(p); + } else { + produced = p; + } + } + + public boolean cancel() { + return SubscriptionHelper.cancel(this); + } + + SimplePlainQueue<T> getQueue() { + SimplePlainQueue<T> q = queue; + if (q == null) { + q = new SpscArrayQueue<>(prefetch); + this.queue = q; + } + return q; + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/parallel/ParallelMap.java b/src/main/java/io/reactivex/rxjava3/internal/operators/parallel/ParallelMap.java new file mode 100644 index 0000000000..81d70a01d9 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/parallel/ParallelMap.java @@ -0,0 +1,239 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.parallel; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.FlowableSubscriber; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.subscriptions.SubscriptionHelper; +import io.reactivex.rxjava3.operators.ConditionalSubscriber; +import io.reactivex.rxjava3.parallel.ParallelFlowable; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +import java.util.Objects; + +/** + * Maps each 'rail' of the source ParallelFlowable with a mapper function. + * + * @param <T> the input value type + * @param <R> the output value type + */ +public final class ParallelMap<T, R> extends ParallelFlowable<R> { + + final ParallelFlowable<T> source; + + final Function<? super T, ? extends R> mapper; + + public ParallelMap(ParallelFlowable<T> source, Function<? super T, ? extends R> mapper) { + this.source = source; + this.mapper = mapper; + } + + @Override + public void subscribe(Subscriber<? super R>[] subscribers) { + subscribers = RxJavaPlugins.onSubscribe(this, subscribers); + + if (!validate(subscribers)) { + return; + } + + int n = subscribers.length; + @SuppressWarnings("unchecked") + Subscriber<? super T>[] parents = new Subscriber[n]; + + for (int i = 0; i < n; i++) { + Subscriber<? super R> a = subscribers[i]; + if (a instanceof ConditionalSubscriber) { + parents[i] = new ParallelMapConditionalSubscriber<T, R>((ConditionalSubscriber<? super R>)a, mapper); + } else { + parents[i] = new ParallelMapSubscriber<T, R>(a, mapper); + } + } + + source.subscribe(parents); + } + + @Override + public int parallelism() { + return source.parallelism(); + } + + static final class ParallelMapSubscriber<T, R> implements FlowableSubscriber<T>, Subscription { + + final Subscriber<? super R> downstream; + + final Function<? super T, ? extends R> mapper; + + Subscription upstream; + + boolean done; + + ParallelMapSubscriber(Subscriber<? super R> actual, Function<? super T, ? extends R> mapper) { + this.downstream = actual; + this.mapper = mapper; + } + + @Override + public void request(long n) { + upstream.request(n); + } + + @Override + public void cancel() { + upstream.cancel(); + } + + @Override + public void onSubscribe(Subscription s) { + if (SubscriptionHelper.validate(this.upstream, s)) { + this.upstream = s; + + downstream.onSubscribe(this); + } + } + + @Override + public void onNext(T t) { + if (done) { + return; + } + R v; + + try { + v = Objects.requireNonNull(mapper.apply(t), "The mapper returned a null value"); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + cancel(); + onError(ex); + return; + } + + downstream.onNext(v); + } + + @Override + public void onError(Throwable t) { + if (done) { + RxJavaPlugins.onError(t); + return; + } + done = true; + downstream.onError(t); + } + + @Override + public void onComplete() { + if (done) { + return; + } + done = true; + downstream.onComplete(); + } + + } + static final class ParallelMapConditionalSubscriber<T, R> implements ConditionalSubscriber<T>, Subscription { + + final ConditionalSubscriber<? super R> downstream; + + final Function<? super T, ? extends R> mapper; + + Subscription upstream; + + boolean done; + + ParallelMapConditionalSubscriber(ConditionalSubscriber<? super R> actual, Function<? super T, ? extends R> mapper) { + this.downstream = actual; + this.mapper = mapper; + } + + @Override + public void request(long n) { + upstream.request(n); + } + + @Override + public void cancel() { + upstream.cancel(); + } + + @Override + public void onSubscribe(Subscription s) { + if (SubscriptionHelper.validate(this.upstream, s)) { + this.upstream = s; + + downstream.onSubscribe(this); + } + } + + @Override + public void onNext(T t) { + if (done) { + return; + } + R v; + + try { + v = Objects.requireNonNull(mapper.apply(t), "The mapper returned a null value"); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + cancel(); + onError(ex); + return; + } + + downstream.onNext(v); + } + + @Override + public boolean tryOnNext(T t) { + if (done) { + return false; + } + R v; + + try { + v = Objects.requireNonNull(mapper.apply(t), "The mapper returned a null value"); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + cancel(); + onError(ex); + return false; + } + + return downstream.tryOnNext(v); + } + + @Override + public void onError(Throwable t) { + if (done) { + RxJavaPlugins.onError(t); + return; + } + done = true; + downstream.onError(t); + } + + @Override + public void onComplete() { + if (done) { + return; + } + done = true; + downstream.onComplete(); + } + + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/parallel/ParallelMapTry.java b/src/main/java/io/reactivex/rxjava3/internal/operators/parallel/ParallelMapTry.java new file mode 100644 index 0000000000..cac64f3711 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/parallel/ParallelMapTry.java @@ -0,0 +1,303 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.parallel; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.subscriptions.SubscriptionHelper; +import io.reactivex.rxjava3.operators.ConditionalSubscriber; +import io.reactivex.rxjava3.parallel.*; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +import java.util.Objects; + +/** + * Maps each 'rail' of the source ParallelFlowable with a mapper function + * and handle any failure based on a handler function. + * <p>History: 2.0.8 - experimental + * @param <T> the input value type + * @param <R> the output value type + * @since 2.2 + */ +public final class ParallelMapTry<T, R> extends ParallelFlowable<R> { + + final ParallelFlowable<T> source; + + final Function<? super T, ? extends R> mapper; + + final BiFunction<? super Long, ? super Throwable, ParallelFailureHandling> errorHandler; + + public ParallelMapTry(ParallelFlowable<T> source, Function<? super T, ? extends R> mapper, + BiFunction<? super Long, ? super Throwable, ParallelFailureHandling> errorHandler) { + this.source = source; + this.mapper = mapper; + this.errorHandler = errorHandler; + } + + @Override + public void subscribe(Subscriber<? super R>[] subscribers) { + subscribers = RxJavaPlugins.onSubscribe(this, subscribers); + + if (!validate(subscribers)) { + return; + } + + int n = subscribers.length; + @SuppressWarnings("unchecked") + Subscriber<? super T>[] parents = new Subscriber[n]; + + for (int i = 0; i < n; i++) { + Subscriber<? super R> a = subscribers[i]; + if (a instanceof ConditionalSubscriber) { + parents[i] = new ParallelMapTryConditionalSubscriber<T, R>((ConditionalSubscriber<? super R>)a, mapper, errorHandler); + } else { + parents[i] = new ParallelMapTrySubscriber<T, R>(a, mapper, errorHandler); + } + } + + source.subscribe(parents); + } + + @Override + public int parallelism() { + return source.parallelism(); + } + + static final class ParallelMapTrySubscriber<T, R> implements ConditionalSubscriber<T>, Subscription { + + final Subscriber<? super R> downstream; + + final Function<? super T, ? extends R> mapper; + + final BiFunction<? super Long, ? super Throwable, ParallelFailureHandling> errorHandler; + + Subscription upstream; + + boolean done; + + ParallelMapTrySubscriber(Subscriber<? super R> actual, Function<? super T, ? extends R> mapper, + BiFunction<? super Long, ? super Throwable, ParallelFailureHandling> errorHandler) { + this.downstream = actual; + this.mapper = mapper; + this.errorHandler = errorHandler; + } + + @Override + public void request(long n) { + upstream.request(n); + } + + @Override + public void cancel() { + upstream.cancel(); + } + + @Override + public void onSubscribe(Subscription s) { + if (SubscriptionHelper.validate(this.upstream, s)) { + this.upstream = s; + + downstream.onSubscribe(this); + } + } + + @Override + public void onNext(T t) { + if (!tryOnNext(t) && !done) { + upstream.request(1); + } + } + + @Override + public boolean tryOnNext(T t) { + if (done) { + return false; + } + long retries = 0; + + for (;;) { + R v; + + try { + v = Objects.requireNonNull(mapper.apply(t), "The mapper returned a null value"); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + + ParallelFailureHandling h; + + try { + h = Objects.requireNonNull(errorHandler.apply(++retries, ex), "The errorHandler returned a null ParallelFailureHandling"); + } catch (Throwable exc) { + Exceptions.throwIfFatal(exc); + cancel(); + onError(new CompositeException(ex, exc)); + return false; + } + + switch (h) { + case RETRY: + continue; + case SKIP: + return false; + case STOP: + cancel(); + onComplete(); + return false; + default: + cancel(); + onError(ex); + return false; + } + } + + downstream.onNext(v); + return true; + } + } + + @Override + public void onError(Throwable t) { + if (done) { + RxJavaPlugins.onError(t); + return; + } + done = true; + downstream.onError(t); + } + + @Override + public void onComplete() { + if (done) { + return; + } + done = true; + downstream.onComplete(); + } + + } + static final class ParallelMapTryConditionalSubscriber<T, R> implements ConditionalSubscriber<T>, Subscription { + + final ConditionalSubscriber<? super R> downstream; + + final Function<? super T, ? extends R> mapper; + + final BiFunction<? super Long, ? super Throwable, ParallelFailureHandling> errorHandler; + + Subscription upstream; + + boolean done; + + ParallelMapTryConditionalSubscriber(ConditionalSubscriber<? super R> actual, + Function<? super T, ? extends R> mapper, + BiFunction<? super Long, ? super Throwable, ParallelFailureHandling> errorHandler) { + this.downstream = actual; + this.mapper = mapper; + this.errorHandler = errorHandler; + } + + @Override + public void request(long n) { + upstream.request(n); + } + + @Override + public void cancel() { + upstream.cancel(); + } + + @Override + public void onSubscribe(Subscription s) { + if (SubscriptionHelper.validate(this.upstream, s)) { + this.upstream = s; + + downstream.onSubscribe(this); + } + } + + @Override + public void onNext(T t) { + if (!tryOnNext(t) && !done) { + upstream.request(1); + } + } + + @Override + public boolean tryOnNext(T t) { + if (done) { + return false; + } + long retries = 0; + + for (;;) { + R v; + + try { + v = Objects.requireNonNull(mapper.apply(t), "The mapper returned a null value"); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + + ParallelFailureHandling h; + + try { + h = Objects.requireNonNull(errorHandler.apply(++retries, ex), "The errorHandler returned a null ParallelFailureHandling"); + } catch (Throwable exc) { + Exceptions.throwIfFatal(exc); + cancel(); + onError(new CompositeException(ex, exc)); + return false; + } + + switch (h) { + case RETRY: + continue; + case SKIP: + return false; + case STOP: + cancel(); + onComplete(); + return false; + default: + cancel(); + onError(ex); + return false; + } + } + + return downstream.tryOnNext(v); + } + } + + @Override + public void onError(Throwable t) { + if (done) { + RxJavaPlugins.onError(t); + return; + } + done = true; + downstream.onError(t); + } + + @Override + public void onComplete() { + if (done) { + return; + } + done = true; + downstream.onComplete(); + } + + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/parallel/ParallelPeek.java b/src/main/java/io/reactivex/rxjava3/internal/operators/parallel/ParallelPeek.java new file mode 100644 index 0000000000..42f65567ca --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/parallel/ParallelPeek.java @@ -0,0 +1,215 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.parallel; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.FlowableSubscriber; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.subscriptions.*; +import io.reactivex.rxjava3.parallel.ParallelFlowable; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +import java.util.Objects; + +/** + * Execute a Consumer in each 'rail' for the current element passing through. + * + * @param <T> the value type + */ +public final class ParallelPeek<T> extends ParallelFlowable<T> { + + final ParallelFlowable<T> source; + + final Consumer<? super T> onNext; + final Consumer<? super T> onAfterNext; + final Consumer<? super Throwable> onError; + final Action onComplete; + final Action onAfterTerminated; + final Consumer<? super Subscription> onSubscribe; + final LongConsumer onRequest; + final Action onCancel; + + public ParallelPeek(ParallelFlowable<T> source, + Consumer<? super T> onNext, + Consumer<? super T> onAfterNext, + Consumer<? super Throwable> onError, + Action onComplete, + Action onAfterTerminated, + Consumer<? super Subscription> onSubscribe, + LongConsumer onRequest, + Action onCancel + ) { + this.source = source; + + this.onNext = Objects.requireNonNull(onNext, "onNext is null"); + this.onAfterNext = Objects.requireNonNull(onAfterNext, "onAfterNext is null"); + this.onError = Objects.requireNonNull(onError, "onError is null"); + this.onComplete = Objects.requireNonNull(onComplete, "onComplete is null"); + this.onAfterTerminated = Objects.requireNonNull(onAfterTerminated, "onAfterTerminated is null"); + this.onSubscribe = Objects.requireNonNull(onSubscribe, "onSubscribe is null"); + this.onRequest = Objects.requireNonNull(onRequest, "onRequest is null"); + this.onCancel = Objects.requireNonNull(onCancel, "onCancel is null"); + } + + @Override + public void subscribe(Subscriber<? super T>[] subscribers) { + subscribers = RxJavaPlugins.onSubscribe(this, subscribers); + + if (!validate(subscribers)) { + return; + } + + int n = subscribers.length; + @SuppressWarnings("unchecked") + Subscriber<? super T>[] parents = new Subscriber[n]; + + for (int i = 0; i < n; i++) { + parents[i] = new ParallelPeekSubscriber<>(subscribers[i], this); + } + + source.subscribe(parents); + } + + @Override + public int parallelism() { + return source.parallelism(); + } + + static final class ParallelPeekSubscriber<T> implements FlowableSubscriber<T>, Subscription { + + final Subscriber<? super T> downstream; + + final ParallelPeek<T> parent; + + Subscription upstream; + + boolean done; + + ParallelPeekSubscriber(Subscriber<? super T> actual, ParallelPeek<T> parent) { + this.downstream = actual; + this.parent = parent; + } + + @Override + public void request(long n) { + try { + parent.onRequest.accept(n); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + RxJavaPlugins.onError(ex); + } + upstream.request(n); + } + + @Override + public void cancel() { + try { + parent.onCancel.run(); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + RxJavaPlugins.onError(ex); + } + upstream.cancel(); + } + + @Override + public void onSubscribe(Subscription s) { + if (SubscriptionHelper.validate(this.upstream, s)) { + this.upstream = s; + + try { + parent.onSubscribe.accept(s); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + s.cancel(); + downstream.onSubscribe(EmptySubscription.INSTANCE); + onError(ex); + return; + } + + downstream.onSubscribe(this); + } + } + + @Override + public void onNext(T t) { + if (!done) { + try { + parent.onNext.accept(t); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + onError(ex); + return; + } + + downstream.onNext(t); + + try { + parent.onAfterNext.accept(t); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + onError(ex); + } + } + } + + @Override + public void onError(Throwable t) { + if (done) { + RxJavaPlugins.onError(t); + return; + } + done = true; + + try { + parent.onError.accept(t); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + t = new CompositeException(t, ex); + } + downstream.onError(t); + + try { + parent.onAfterTerminated.run(); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + RxJavaPlugins.onError(ex); + } + } + + @Override + public void onComplete() { + if (!done) { + done = true; + try { + parent.onComplete.run(); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + downstream.onError(ex); + return; + } + downstream.onComplete(); + + try { + parent.onAfterTerminated.run(); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + RxJavaPlugins.onError(ex); + } + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/parallel/ParallelReduce.java b/src/main/java/io/reactivex/rxjava3/internal/operators/parallel/ParallelReduce.java new file mode 100644 index 0000000000..b955ca711a --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/parallel/ParallelReduce.java @@ -0,0 +1,161 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.parallel; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.subscribers.DeferredScalarSubscriber; +import io.reactivex.rxjava3.internal.subscriptions.*; +import io.reactivex.rxjava3.parallel.ParallelFlowable; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +import java.util.Objects; + +/** + * Reduce the sequence of values in each 'rail' to a single value. + * + * @param <T> the input value type + * @param <R> the result value type + */ +public final class ParallelReduce<T, R> extends ParallelFlowable<R> { + + final ParallelFlowable<? extends T> source; + + final Supplier<R> initialSupplier; + + final BiFunction<R, ? super T, R> reducer; + + public ParallelReduce(ParallelFlowable<? extends T> source, Supplier<R> initialSupplier, BiFunction<R, ? super T, R> reducer) { + this.source = source; + this.initialSupplier = initialSupplier; + this.reducer = reducer; + } + + @Override + public void subscribe(Subscriber<? super R>[] subscribers) { + subscribers = RxJavaPlugins.onSubscribe(this, subscribers); + + if (!validate(subscribers)) { + return; + } + + int n = subscribers.length; + @SuppressWarnings("unchecked") + Subscriber<T>[] parents = new Subscriber[n]; + + for (int i = 0; i < n; i++) { + + R initialValue; + + try { + initialValue = Objects.requireNonNull(initialSupplier.get(), "The initialSupplier returned a null value"); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + reportError(subscribers, ex); + return; + } + + parents[i] = new ParallelReduceSubscriber<>(subscribers[i], initialValue, reducer); + } + + source.subscribe(parents); + } + + void reportError(Subscriber<?>[] subscribers, Throwable ex) { + for (Subscriber<?> s : subscribers) { + EmptySubscription.error(ex, s); + } + } + + @Override + public int parallelism() { + return source.parallelism(); + } + + static final class ParallelReduceSubscriber<T, R> extends DeferredScalarSubscriber<T, R> { + + private static final long serialVersionUID = 8200530050639449080L; + + final BiFunction<R, ? super T, R> reducer; + + R accumulator; + + boolean done; + + ParallelReduceSubscriber(Subscriber<? super R> subscriber, R initialValue, BiFunction<R, ? super T, R> reducer) { + super(subscriber); + this.accumulator = initialValue; + this.reducer = reducer; + } + + @Override + public void onSubscribe(Subscription s) { + if (SubscriptionHelper.validate(this.upstream, s)) { + this.upstream = s; + + downstream.onSubscribe(this); + + s.request(Long.MAX_VALUE); + } + } + + @Override + public void onNext(T t) { + if (!done) { + R v; + + try { + v = Objects.requireNonNull(reducer.apply(accumulator, t), "The reducer returned a null value"); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + cancel(); + onError(ex); + return; + } + + accumulator = v; + } + } + + @Override + public void onError(Throwable t) { + if (done) { + RxJavaPlugins.onError(t); + return; + } + done = true; + accumulator = null; + downstream.onError(t); + } + + @Override + public void onComplete() { + if (!done) { + done = true; + + R a = accumulator; + accumulator = null; + complete(a); + } + } + + @Override + public void cancel() { + super.cancel(); + upstream.cancel(); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/parallel/ParallelReduceFull.java b/src/main/java/io/reactivex/rxjava3/internal/operators/parallel/ParallelReduceFull.java new file mode 100644 index 0000000000..2e24b9e256 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/parallel/ParallelReduceFull.java @@ -0,0 +1,259 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.parallel; + +import java.util.Objects; +import java.util.concurrent.atomic.*; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.BiFunction; +import io.reactivex.rxjava3.internal.subscriptions.*; +import io.reactivex.rxjava3.internal.util.AtomicThrowable; +import io.reactivex.rxjava3.parallel.ParallelFlowable; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +/** + * Reduces all 'rails' into a single value which then gets reduced into a single + * Publisher sequence. + * + * @param <T> the value type + */ +public final class ParallelReduceFull<T> extends Flowable<T> { + + final ParallelFlowable<? extends T> source; + + final BiFunction<T, T, T> reducer; + + public ParallelReduceFull(ParallelFlowable<? extends T> source, BiFunction<T, T, T> reducer) { + this.source = source; + this.reducer = reducer; + } + + @Override + protected void subscribeActual(Subscriber<? super T> s) { + ParallelReduceFullMainSubscriber<T> parent = new ParallelReduceFullMainSubscriber<>(s, source.parallelism(), reducer); + s.onSubscribe(parent); + + source.subscribe(parent.subscribers); + } + + static final class ParallelReduceFullMainSubscriber<T> extends DeferredScalarSubscription<T> { + + private static final long serialVersionUID = -5370107872170712765L; + + final ParallelReduceFullInnerSubscriber<T>[] subscribers; + + final BiFunction<T, T, T> reducer; + + final AtomicReference<SlotPair<T>> current = new AtomicReference<>(); + + final AtomicInteger remaining = new AtomicInteger(); + + final AtomicThrowable error = new AtomicThrowable(); + + ParallelReduceFullMainSubscriber(Subscriber<? super T> subscriber, int n, BiFunction<T, T, T> reducer) { + super(subscriber); + @SuppressWarnings("unchecked") + ParallelReduceFullInnerSubscriber<T>[] a = new ParallelReduceFullInnerSubscriber[n]; + for (int i = 0; i < n; i++) { + a[i] = new ParallelReduceFullInnerSubscriber<>(this, reducer); + } + this.subscribers = a; + this.reducer = reducer; + remaining.lazySet(n); + } + + SlotPair<T> addValue(T value) { + for (;;) { + SlotPair<T> curr = current.get(); + + if (curr == null) { + curr = new SlotPair<>(); + if (!current.compareAndSet(null, curr)) { + continue; + } + } + + int c = curr.tryAcquireSlot(); + if (c < 0) { + current.compareAndSet(curr, null); + continue; + } + if (c == 0) { + curr.first = value; + } else { + curr.second = value; + } + + if (curr.releaseSlot()) { + current.compareAndSet(curr, null); + return curr; + } + return null; + } + } + + @Override + public void cancel() { + for (ParallelReduceFullInnerSubscriber<T> inner : subscribers) { + inner.cancel(); + } + } + + void innerError(Throwable ex) { + if (error.compareAndSet(null, ex)) { + cancel(); + downstream.onError(ex); + } else { + if (ex != error.get()) { + RxJavaPlugins.onError(ex); + } + } + } + + void innerComplete(T value) { + if (value != null) { + for (;;) { + SlotPair<T> sp = addValue(value); + + if (sp != null) { + + try { + value = Objects.requireNonNull(reducer.apply(sp.first, sp.second), "The reducer returned a null value"); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + innerError(ex); + return; + } + + } else { + break; + } + } + } + + if (remaining.decrementAndGet() == 0) { + SlotPair<T> sp = current.get(); + current.lazySet(null); + + if (sp != null) { + complete(sp.first); + } else { + downstream.onComplete(); + } + } + } + } + + static final class ParallelReduceFullInnerSubscriber<T> + extends AtomicReference<Subscription> + implements FlowableSubscriber<T> { + + private static final long serialVersionUID = -7954444275102466525L; + + final ParallelReduceFullMainSubscriber<T> parent; + + final BiFunction<T, T, T> reducer; + + T value; + + boolean done; + + ParallelReduceFullInnerSubscriber(ParallelReduceFullMainSubscriber<T> parent, BiFunction<T, T, T> reducer) { + this.parent = parent; + this.reducer = reducer; + } + + @Override + public void onSubscribe(Subscription s) { + SubscriptionHelper.setOnce(this, s, Long.MAX_VALUE); + } + + @Override + public void onNext(T t) { + if (!done) { + T v = value; + + if (v == null) { + value = t; + } else { + + try { + v = Objects.requireNonNull(reducer.apply(v, t), "The reducer returned a null value"); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + get().cancel(); + onError(ex); + return; + } + + value = v; + } + } + } + + @Override + public void onError(Throwable t) { + if (done) { + RxJavaPlugins.onError(t); + return; + } + done = true; + parent.innerError(t); + } + + @Override + public void onComplete() { + if (!done) { + done = true; + parent.innerComplete(value); + } + } + + void cancel() { + SubscriptionHelper.cancel(this); + } + } + + static final class SlotPair<T> extends AtomicInteger { + + private static final long serialVersionUID = 473971317683868662L; + + T first; + + T second; + + final AtomicInteger releaseIndex = new AtomicInteger(); + + int tryAcquireSlot() { + for (;;) { + int acquired = get(); + if (acquired >= 2) { + return -1; + } + + if (compareAndSet(acquired, acquired + 1)) { + return acquired; + } + } + } + + boolean releaseSlot() { + return releaseIndex.incrementAndGet() == 2; + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/parallel/ParallelRunOn.java b/src/main/java/io/reactivex/rxjava3/internal/operators/parallel/ParallelRunOn.java new file mode 100644 index 0000000000..22f822db34 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/parallel/ParallelRunOn.java @@ -0,0 +1,447 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.parallel; + +import java.util.concurrent.atomic.*; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.core.Scheduler.Worker; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.internal.schedulers.SchedulerMultiWorkerSupport; +import io.reactivex.rxjava3.internal.schedulers.SchedulerMultiWorkerSupport.WorkerCallback; +import io.reactivex.rxjava3.internal.subscriptions.SubscriptionHelper; +import io.reactivex.rxjava3.internal.util.BackpressureHelper; +import io.reactivex.rxjava3.operators.ConditionalSubscriber; +import io.reactivex.rxjava3.operators.SpscArrayQueue; +import io.reactivex.rxjava3.parallel.ParallelFlowable; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +/** + * Ensures each 'rail' from upstream runs on a Worker from a Scheduler. + * + * @param <T> the value type + */ +public final class ParallelRunOn<T> extends ParallelFlowable<T> { + final ParallelFlowable<? extends T> source; + + final Scheduler scheduler; + + final int prefetch; + + public ParallelRunOn(ParallelFlowable<? extends T> parent, + Scheduler scheduler, int prefetch) { + this.source = parent; + this.scheduler = scheduler; + this.prefetch = prefetch; + } + + @Override + public void subscribe(Subscriber<? super T>[] subscribers) { + subscribers = RxJavaPlugins.onSubscribe(this, subscribers); + + if (!validate(subscribers)) { + return; + } + + int n = subscribers.length; + + @SuppressWarnings("unchecked") + final Subscriber<T>[] parents = new Subscriber[n]; + + if (scheduler instanceof SchedulerMultiWorkerSupport) { + SchedulerMultiWorkerSupport multiworker = (SchedulerMultiWorkerSupport) scheduler; + multiworker.createWorkers(n, new MultiWorkerCallback(subscribers, parents)); + } else { + for (int i = 0; i < n; i++) { + createSubscriber(i, subscribers, parents, scheduler.createWorker()); + } + } + source.subscribe(parents); + } + + void createSubscriber(int i, Subscriber<? super T>[] subscribers, + Subscriber<T>[] parents, Scheduler.Worker worker) { + + Subscriber<? super T> a = subscribers[i]; + + SpscArrayQueue<T> q = new SpscArrayQueue<>(prefetch); + + if (a instanceof ConditionalSubscriber) { + parents[i] = new RunOnConditionalSubscriber<>((ConditionalSubscriber<? super T>)a, prefetch, q, worker); + } else { + parents[i] = new RunOnSubscriber<>(a, prefetch, q, worker); + } + } + + final class MultiWorkerCallback implements WorkerCallback { + + final Subscriber<? super T>[] subscribers; + + final Subscriber<T>[] parents; + + MultiWorkerCallback(Subscriber<? super T>[] subscribers, + Subscriber<T>[] parents) { + this.subscribers = subscribers; + this.parents = parents; + } + + @Override + public void onWorker(int i, Worker w) { + createSubscriber(i, subscribers, parents, w); + } + } + + @Override + public int parallelism() { + return source.parallelism(); + } + + abstract static class BaseRunOnSubscriber<T> extends AtomicInteger + implements FlowableSubscriber<T>, Subscription, Runnable { + + private static final long serialVersionUID = 9222303586456402150L; + + final int prefetch; + + final int limit; + + final SpscArrayQueue<T> queue; + + final Worker worker; + + Subscription upstream; + + volatile boolean done; + + Throwable error; + + final AtomicLong requested = new AtomicLong(); + + volatile boolean cancelled; + + int consumed; + + BaseRunOnSubscriber(int prefetch, SpscArrayQueue<T> queue, Worker worker) { + this.prefetch = prefetch; + this.queue = queue; + this.limit = prefetch - (prefetch >> 2); + this.worker = worker; + } + + @Override + public final void onNext(T t) { + if (done) { + return; + } + if (!queue.offer(t)) { + upstream.cancel(); + onError(new QueueOverflowException()); + return; + } + schedule(); + } + + @Override + public final void onError(Throwable t) { + if (done) { + RxJavaPlugins.onError(t); + return; + } + error = t; + done = true; + schedule(); + } + + @Override + public final void onComplete() { + if (done) { + return; + } + done = true; + schedule(); + } + + @Override + public final void request(long n) { + if (SubscriptionHelper.validate(n)) { + BackpressureHelper.add(requested, n); + schedule(); + } + } + + @Override + public final void cancel() { + if (!cancelled) { + cancelled = true; + upstream.cancel(); + worker.dispose(); + + if (getAndIncrement() == 0) { + queue.clear(); + } + } + } + + final void schedule() { + if (getAndIncrement() == 0) { + worker.schedule(this); + } + } + } + + static final class RunOnSubscriber<T> extends BaseRunOnSubscriber<T> { + + private static final long serialVersionUID = 1075119423897941642L; + + final Subscriber<? super T> downstream; + + RunOnSubscriber(Subscriber<? super T> actual, int prefetch, SpscArrayQueue<T> queue, Worker worker) { + super(prefetch, queue, worker); + this.downstream = actual; + } + + @Override + public void onSubscribe(Subscription s) { + if (SubscriptionHelper.validate(this.upstream, s)) { + this.upstream = s; + + downstream.onSubscribe(this); + + s.request(prefetch); + } + } + + @Override + public void run() { + int missed = 1; + int c = consumed; + SpscArrayQueue<T> q = queue; + Subscriber<? super T> a = downstream; + int lim = limit; + + for (;;) { + + long r = requested.get(); + long e = 0L; + + while (e != r) { + if (cancelled) { + q.clear(); + return; + } + + boolean d = done; + + if (d) { + Throwable ex = error; + if (ex != null) { + q.clear(); + + a.onError(ex); + + worker.dispose(); + return; + } + } + + T v = q.poll(); + + boolean empty = v == null; + + if (d && empty) { + a.onComplete(); + + worker.dispose(); + return; + } + + if (empty) { + break; + } + + a.onNext(v); + + e++; + + int p = ++c; + if (p == lim) { + c = 0; + upstream.request(p); + } + } + + if (e == r) { + if (cancelled) { + q.clear(); + return; + } + + if (done) { + Throwable ex = error; + if (ex != null) { + q.clear(); + + a.onError(ex); + + worker.dispose(); + return; + } + if (q.isEmpty()) { + a.onComplete(); + + worker.dispose(); + return; + } + } + } + + if (e != 0L && r != Long.MAX_VALUE) { + requested.addAndGet(-e); + } + + int w = get(); + if (w == missed) { + consumed = c; + missed = addAndGet(-missed); + if (missed == 0) { + break; + } + } else { + missed = w; + } + } + } + } + + static final class RunOnConditionalSubscriber<T> extends BaseRunOnSubscriber<T> { + + private static final long serialVersionUID = 1075119423897941642L; + + final ConditionalSubscriber<? super T> downstream; + + RunOnConditionalSubscriber(ConditionalSubscriber<? super T> actual, int prefetch, SpscArrayQueue<T> queue, Worker worker) { + super(prefetch, queue, worker); + this.downstream = actual; + } + + @Override + public void onSubscribe(Subscription s) { + if (SubscriptionHelper.validate(this.upstream, s)) { + this.upstream = s; + + downstream.onSubscribe(this); + + s.request(prefetch); + } + } + + @Override + public void run() { + int missed = 1; + int c = consumed; + SpscArrayQueue<T> q = queue; + ConditionalSubscriber<? super T> a = downstream; + int lim = limit; + + for (;;) { + + long r = requested.get(); + long e = 0L; + + while (e != r) { + if (cancelled) { + q.clear(); + return; + } + + boolean d = done; + + if (d) { + Throwable ex = error; + if (ex != null) { + q.clear(); + + a.onError(ex); + + worker.dispose(); + return; + } + } + + T v = q.poll(); + + boolean empty = v == null; + + if (d && empty) { + a.onComplete(); + + worker.dispose(); + return; + } + + if (empty) { + break; + } + + if (a.tryOnNext(v)) { + e++; + } + + int p = ++c; + if (p == lim) { + c = 0; + upstream.request(p); + } + } + + if (e == r) { + if (cancelled) { + q.clear(); + return; + } + + if (done) { + Throwable ex = error; + if (ex != null) { + q.clear(); + + a.onError(ex); + + worker.dispose(); + return; + } + if (q.isEmpty()) { + a.onComplete(); + + worker.dispose(); + return; + } + } + } + + if (e != 0L) { + BackpressureHelper.produced(requested, e); + } + + consumed = c; + missed = addAndGet(-missed); + if (missed == 0) { + break; + } + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/parallel/ParallelSortedJoin.java b/src/main/java/io/reactivex/rxjava3/internal/operators/parallel/ParallelSortedJoin.java new file mode 100644 index 0000000000..fb20bb65b9 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/parallel/ParallelSortedJoin.java @@ -0,0 +1,297 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.parallel; + +import java.util.*; +import java.util.concurrent.atomic.*; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.internal.subscriptions.SubscriptionHelper; +import io.reactivex.rxjava3.internal.util.BackpressureHelper; +import io.reactivex.rxjava3.parallel.ParallelFlowable; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +/** + * Given sorted rail sequences (according to the provided comparator) as List + * emit the smallest item from these parallel Lists to the Subscriber. + * <p> + * It expects the source to emit exactly one list (which could be empty). + * + * @param <T> the value type + */ +public final class ParallelSortedJoin<T> extends Flowable<T> { + + final ParallelFlowable<List<T>> source; + + final Comparator<? super T> comparator; + + public ParallelSortedJoin(ParallelFlowable<List<T>> source, Comparator<? super T> comparator) { + this.source = source; + this.comparator = comparator; + } + + @Override + protected void subscribeActual(Subscriber<? super T> s) { + SortedJoinSubscription<T> parent = new SortedJoinSubscription<>(s, source.parallelism(), comparator); + s.onSubscribe(parent); + + source.subscribe(parent.subscribers); + } + + static final class SortedJoinSubscription<T> + extends AtomicInteger + implements Subscription { + + private static final long serialVersionUID = 3481980673745556697L; + + final Subscriber<? super T> downstream; + + final SortedJoinInnerSubscriber<T>[] subscribers; + + final List<T>[] lists; + + final int[] indexes; + + final Comparator<? super T> comparator; + + final AtomicLong requested = new AtomicLong(); + + volatile boolean cancelled; + + final AtomicInteger remaining = new AtomicInteger(); + + final AtomicReference<Throwable> error = new AtomicReference<>(); + + @SuppressWarnings("unchecked") + SortedJoinSubscription(Subscriber<? super T> actual, int n, Comparator<? super T> comparator) { + this.downstream = actual; + this.comparator = comparator; + + SortedJoinInnerSubscriber<T>[] s = new SortedJoinInnerSubscriber[n]; + + for (int i = 0; i < n; i++) { + s[i] = new SortedJoinInnerSubscriber<>(this, i); + } + this.subscribers = s; + this.lists = new List[n]; + this.indexes = new int[n]; + remaining.lazySet(n); + } + + @Override + public void request(long n) { + if (SubscriptionHelper.validate(n)) { + BackpressureHelper.add(requested, n); + if (remaining.get() == 0) { + drain(); + } + } + } + + @Override + public void cancel() { + if (!cancelled) { + cancelled = true; + cancelAll(); + if (getAndIncrement() == 0) { + Arrays.fill(lists, null); + } + } + } + + void cancelAll() { + for (SortedJoinInnerSubscriber<T> s : subscribers) { + s.cancel(); + } + } + + void innerNext(List<T> value, int index) { + lists[index] = value; + if (remaining.decrementAndGet() == 0) { + drain(); + } + } + + void innerError(Throwable e) { + if (error.compareAndSet(null, e)) { + drain(); + } else { + if (e != error.get()) { + RxJavaPlugins.onError(e); + } + } + } + + void drain() { + if (getAndIncrement() != 0) { + return; + } + + int missed = 1; + Subscriber<? super T> a = downstream; + List<T>[] lists = this.lists; + int[] indexes = this.indexes; + int n = indexes.length; + + for (;;) { + + long r = requested.get(); + long e = 0L; + + while (e != r) { + if (cancelled) { + Arrays.fill(lists, null); + return; + } + + Throwable ex = error.get(); + if (ex != null) { + cancelAll(); + Arrays.fill(lists, null); + a.onError(ex); + return; + } + + T min = null; + int minIndex = -1; + + for (int i = 0; i < n; i++) { + List<T> list = lists[i]; + int index = indexes[i]; + + if (list.size() != index) { + if (min == null) { + min = list.get(index); + minIndex = i; + } else { + T b = list.get(index); + + boolean smaller; + + try { + smaller = comparator.compare(min, b) > 0; + } catch (Throwable exc) { + Exceptions.throwIfFatal(exc); + cancelAll(); + Arrays.fill(lists, null); + if (!error.compareAndSet(null, exc)) { + RxJavaPlugins.onError(exc); + } + a.onError(error.get()); + return; + } + if (smaller) { + min = b; + minIndex = i; + } + } + } + } + + if (min == null) { + Arrays.fill(lists, null); + a.onComplete(); + return; + } + + a.onNext(min); + + indexes[minIndex]++; + + e++; + } + + if (cancelled) { + Arrays.fill(lists, null); + return; + } + + Throwable ex = error.get(); + if (ex != null) { + cancelAll(); + Arrays.fill(lists, null); + a.onError(ex); + return; + } + + boolean empty = true; + + for (int i = 0; i < n; i++) { + if (indexes[i] != lists[i].size()) { + empty = false; + break; + } + } + + if (empty) { + Arrays.fill(lists, null); + a.onComplete(); + return; + } + + if (e != 0) { + BackpressureHelper.produced(requested, e); + } + + missed = addAndGet(-missed); + if (missed == 0) { + break; + } + } + } + } + + static final class SortedJoinInnerSubscriber<T> + extends AtomicReference<Subscription> + implements FlowableSubscriber<List<T>> { + + private static final long serialVersionUID = 6751017204873808094L; + + final SortedJoinSubscription<T> parent; + + final int index; + + SortedJoinInnerSubscriber(SortedJoinSubscription<T> parent, int index) { + this.parent = parent; + this.index = index; + } + + @Override + public void onSubscribe(Subscription s) { + SubscriptionHelper.setOnce(this, s, Long.MAX_VALUE); + } + + @Override + public void onNext(List<T> t) { + parent.innerNext(t, index); + } + + @Override + public void onError(Throwable t) { + parent.innerError(t); + } + + @Override + public void onComplete() { + // ignored + } + + void cancel() { + SubscriptionHelper.cancel(this); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleAmb.java b/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleAmb.java new file mode 100644 index 0000000000..24ff7eabed --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleAmb.java @@ -0,0 +1,131 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.single; + +import java.util.concurrent.atomic.AtomicBoolean; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.*; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.internal.disposables.EmptyDisposable; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +public final class SingleAmb<T> extends Single<T> { + private final SingleSource<? extends T>[] sources; + private final Iterable<? extends SingleSource<? extends T>> sourcesIterable; + + public SingleAmb(SingleSource<? extends T>[] sources, Iterable<? extends SingleSource<? extends T>> sourcesIterable) { + this.sources = sources; + this.sourcesIterable = sourcesIterable; + } + + @Override + @SuppressWarnings("unchecked") + protected void subscribeActual(final SingleObserver<? super T> observer) { + SingleSource<? extends T>[] sources = this.sources; + int count = 0; + if (sources == null) { + sources = new SingleSource[8]; + try { + for (SingleSource<? extends T> element : sourcesIterable) { + if (element == null) { + EmptyDisposable.error(new NullPointerException("One of the sources is null"), observer); + return; + } + if (count == sources.length) { + SingleSource<? extends T>[] b = new SingleSource[count + (count >> 2)]; + System.arraycopy(sources, 0, b, 0, count); + sources = b; + } + sources[count++] = element; + } + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + EmptyDisposable.error(e, observer); + return; + } + } else { + count = sources.length; + } + + final AtomicBoolean winner = new AtomicBoolean(); + final CompositeDisposable set = new CompositeDisposable(); + + observer.onSubscribe(set); + + for (int i = 0; i < count; i++) { + SingleSource<? extends T> s1 = sources[i]; + if (set.isDisposed()) { + return; + } + + if (s1 == null) { + set.dispose(); + Throwable e = new NullPointerException("One of the sources is null"); + if (winner.compareAndSet(false, true)) { + observer.onError(e); + } else { + RxJavaPlugins.onError(e); + } + return; + } + + s1.subscribe(new AmbSingleObserver<T>(observer, set, winner)); + } + } + + static final class AmbSingleObserver<T> implements SingleObserver<T> { + + final CompositeDisposable set; + + final SingleObserver<? super T> downstream; + + final AtomicBoolean winner; + + Disposable upstream; + + AmbSingleObserver(SingleObserver<? super T> observer, CompositeDisposable set, AtomicBoolean winner) { + this.downstream = observer; + this.set = set; + this.winner = winner; + } + + @Override + public void onSubscribe(Disposable d) { + this.upstream = d; + set.add(d); + } + + @Override + public void onSuccess(T value) { + if (winner.compareAndSet(false, true)) { + set.delete(upstream); + set.dispose(); + downstream.onSuccess(value); + } + } + + @Override + public void onError(Throwable e) { + if (winner.compareAndSet(false, true)) { + set.delete(upstream); + set.dispose(); + downstream.onError(e); + } else { + RxJavaPlugins.onError(e); + } + } + } + +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleCache.java b/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleCache.java new file mode 100644 index 0000000000..7f0848e4c1 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleCache.java @@ -0,0 +1,178 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.single; + +import java.util.concurrent.atomic.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; + +public final class SingleCache<T> extends Single<T> implements SingleObserver<T> { + + @SuppressWarnings("rawtypes") + static final CacheDisposable[] EMPTY = new CacheDisposable[0]; + @SuppressWarnings("rawtypes") + static final CacheDisposable[] TERMINATED = new CacheDisposable[0]; + + final SingleSource<? extends T> source; + + final AtomicInteger wip; + + final AtomicReference<CacheDisposable<T>[]> observers; + + T value; + + Throwable error; + + @SuppressWarnings("unchecked") + public SingleCache(SingleSource<? extends T> source) { + this.source = source; + this.wip = new AtomicInteger(); + this.observers = new AtomicReference<>(EMPTY); + } + + @Override + protected void subscribeActual(final SingleObserver<? super T> observer) { + CacheDisposable<T> d = new CacheDisposable<>(observer, this); + observer.onSubscribe(d); + + if (add(d)) { + if (d.isDisposed()) { + remove(d); + } + } else { + Throwable ex = error; + if (ex != null) { + observer.onError(ex); + } else { + observer.onSuccess(value); + } + return; + } + + if (wip.getAndIncrement() == 0) { + source.subscribe(this); + } + } + + boolean add(CacheDisposable<T> observer) { + for (;;) { + CacheDisposable<T>[] a = observers.get(); + if (a == TERMINATED) { + return false; + } + int n = a.length; + @SuppressWarnings("unchecked") + CacheDisposable<T>[] b = new CacheDisposable[n + 1]; + System.arraycopy(a, 0, b, 0, n); + b[n] = observer; + if (observers.compareAndSet(a, b)) { + return true; + } + } + } + + @SuppressWarnings("unchecked") + void remove(CacheDisposable<T> observer) { + for (;;) { + CacheDisposable<T>[] a = observers.get(); + int n = a.length; + if (n == 0) { + return; + } + + int j = -1; + for (int i = 0; i < n; i++) { + if (a[i] == observer) { + j = i; + break; + } + } + + if (j < 0) { + return; + } + + CacheDisposable<T>[] b; + + if (n == 1) { + b = EMPTY; + } else { + b = new CacheDisposable[n - 1]; + System.arraycopy(a, 0, b, 0, j); + System.arraycopy(a, j + 1, b, j, n - j - 1); + } + if (observers.compareAndSet(a, b)) { + return; + } + } + } + + @Override + public void onSubscribe(Disposable d) { + // not supported by this operator + } + + @SuppressWarnings("unchecked") + @Override + public void onSuccess(T value) { + this.value = value; + + for (CacheDisposable<T> d : observers.getAndSet(TERMINATED)) { + if (!d.isDisposed()) { + d.downstream.onSuccess(value); + } + } + } + + @SuppressWarnings("unchecked") + @Override + public void onError(Throwable e) { + this.error = e; + + for (CacheDisposable<T> d : observers.getAndSet(TERMINATED)) { + if (!d.isDisposed()) { + d.downstream.onError(e); + } + } + } + + static final class CacheDisposable<T> + extends AtomicBoolean + implements Disposable { + + private static final long serialVersionUID = 7514387411091976596L; + + final SingleObserver<? super T> downstream; + + final SingleCache<T> parent; + + CacheDisposable(SingleObserver<? super T> actual, SingleCache<T> parent) { + this.downstream = actual; + this.parent = parent; + } + + @Override + public boolean isDisposed() { + return get(); + } + + @Override + public void dispose() { + if (compareAndSet(false, true)) { + parent.remove(this); + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleContains.java b/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleContains.java new file mode 100644 index 0000000000..2497fb2047 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleContains.java @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.single; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.BiPredicate; + +public final class SingleContains<T> extends Single<Boolean> { + + final SingleSource<T> source; + + final Object value; + + final BiPredicate<Object, Object> comparer; + + public SingleContains(SingleSource<T> source, Object value, BiPredicate<Object, Object> comparer) { + this.source = source; + this.value = value; + this.comparer = comparer; + } + + @Override + protected void subscribeActual(final SingleObserver<? super Boolean> observer) { + + source.subscribe(new ContainsSingleObserver(observer)); + } + + final class ContainsSingleObserver implements SingleObserver<T> { + + private final SingleObserver<? super Boolean> downstream; + + ContainsSingleObserver(SingleObserver<? super Boolean> observer) { + this.downstream = observer; + } + + @Override + public void onSubscribe(Disposable d) { + downstream.onSubscribe(d); + } + + @Override + public void onSuccess(T v) { + boolean b; + + try { + b = comparer.test(v, value); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + downstream.onError(ex); + return; + } + downstream.onSuccess(b); + } + + @Override + public void onError(Throwable e) { + downstream.onError(e); + } + + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleCreate.java b/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleCreate.java new file mode 100644 index 0000000000..c0f6c4b1d0 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleCreate.java @@ -0,0 +1,132 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.single; + +import java.util.concurrent.atomic.AtomicReference; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.Cancellable; +import io.reactivex.rxjava3.internal.disposables.*; +import io.reactivex.rxjava3.internal.util.ExceptionHelper; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +public final class SingleCreate<T> extends Single<T> { + + final SingleOnSubscribe<T> source; + + public SingleCreate(SingleOnSubscribe<T> source) { + this.source = source; + } + + @Override + protected void subscribeActual(SingleObserver<? super T> observer) { + Emitter<T> parent = new Emitter<>(observer); + observer.onSubscribe(parent); + + try { + source.subscribe(parent); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + parent.onError(ex); + } + } + + static final class Emitter<T> + extends AtomicReference<Disposable> + implements SingleEmitter<T>, Disposable { + + private static final long serialVersionUID = -2467358622224974244L; + + final SingleObserver<? super T> downstream; + + Emitter(SingleObserver<? super T> downstream) { + this.downstream = downstream; + } + + @Override + public void onSuccess(T value) { + if (get() != DisposableHelper.DISPOSED) { + Disposable d = getAndSet(DisposableHelper.DISPOSED); + if (d != DisposableHelper.DISPOSED) { + try { + if (value == null) { + downstream.onError(ExceptionHelper.createNullPointerException("onSuccess called with a null value.")); + } else { + downstream.onSuccess(value); + } + } finally { + if (d != null) { + d.dispose(); + } + } + } + } + } + + @Override + public void onError(Throwable t) { + if (!tryOnError(t)) { + RxJavaPlugins.onError(t); + } + } + + @Override + public boolean tryOnError(Throwable t) { + if (t == null) { + t = ExceptionHelper.createNullPointerException("onError called with a null Throwable."); + } + if (get() != DisposableHelper.DISPOSED) { + Disposable d = getAndSet(DisposableHelper.DISPOSED); + if (d != DisposableHelper.DISPOSED) { + try { + downstream.onError(t); + } finally { + if (d != null) { + d.dispose(); + } + } + return true; + } + } + return false; + } + + @Override + public void setDisposable(Disposable d) { + DisposableHelper.set(this, d); + } + + @Override + public void setCancellable(Cancellable c) { + setDisposable(new CancellableDisposable(c)); + } + + @Override + public void dispose() { + DisposableHelper.dispose(this); + } + + @Override + public boolean isDisposed() { + return DisposableHelper.isDisposed(get()); + } + + @Override + public String toString() { + return String.format("%s{%s}", getClass().getSimpleName(), super.toString()); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleDefer.java b/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleDefer.java new file mode 100644 index 0000000000..ddf2b2c347 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleDefer.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.single; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.Supplier; +import io.reactivex.rxjava3.internal.disposables.EmptyDisposable; + +import java.util.Objects; + +public final class SingleDefer<T> extends Single<T> { + + final Supplier<? extends SingleSource<? extends T>> singleSupplier; + + public SingleDefer(Supplier<? extends SingleSource<? extends T>> singleSupplier) { + this.singleSupplier = singleSupplier; + } + + @Override + protected void subscribeActual(SingleObserver<? super T> observer) { + SingleSource<? extends T> next; + + try { + next = Objects.requireNonNull(singleSupplier.get(), "The singleSupplier returned a null SingleSource"); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + EmptyDisposable.error(e, observer); + return; + } + + next.subscribe(observer); + } + +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleDelay.java b/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleDelay.java new file mode 100644 index 0000000000..1a92d389fa --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleDelay.java @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.single; + +import java.util.concurrent.TimeUnit; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.disposables.SequentialDisposable; + +public final class SingleDelay<T> extends Single<T> { + + final SingleSource<? extends T> source; + final long time; + final TimeUnit unit; + final Scheduler scheduler; + final boolean delayError; + + public SingleDelay(SingleSource<? extends T> source, long time, TimeUnit unit, Scheduler scheduler, boolean delayError) { + this.source = source; + this.time = time; + this.unit = unit; + this.scheduler = scheduler; + this.delayError = delayError; + } + + @Override + protected void subscribeActual(final SingleObserver<? super T> observer) { + + final SequentialDisposable sd = new SequentialDisposable(); + observer.onSubscribe(sd); + source.subscribe(new Delay(sd, observer)); + } + + final class Delay implements SingleObserver<T> { + private final SequentialDisposable sd; + final SingleObserver<? super T> downstream; + + Delay(SequentialDisposable sd, SingleObserver<? super T> observer) { + this.sd = sd; + this.downstream = observer; + } + + @Override + public void onSubscribe(Disposable d) { + sd.replace(d); + } + + @Override + public void onSuccess(final T value) { + sd.replace(scheduler.scheduleDirect(new OnSuccess(value), time, unit)); + } + + @Override + public void onError(final Throwable e) { + sd.replace(scheduler.scheduleDirect(new OnError(e), delayError ? time : 0, unit)); + } + + final class OnSuccess implements Runnable { + private final T value; + + OnSuccess(T value) { + this.value = value; + } + + @Override + public void run() { + downstream.onSuccess(value); + } + } + + final class OnError implements Runnable { + private final Throwable e; + + OnError(Throwable e) { + this.e = e; + } + + @Override + public void run() { + downstream.onError(e); + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleDelayWithCompletable.java b/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleDelayWithCompletable.java new file mode 100644 index 0000000000..164a502992 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleDelayWithCompletable.java @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.single; + +import java.util.concurrent.atomic.AtomicReference; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; +import io.reactivex.rxjava3.internal.observers.ResumeSingleObserver; + +public final class SingleDelayWithCompletable<T> extends Single<T> { + + final SingleSource<T> source; + + final CompletableSource other; + + public SingleDelayWithCompletable(SingleSource<T> source, CompletableSource other) { + this.source = source; + this.other = other; + } + + @Override + protected void subscribeActual(SingleObserver<? super T> observer) { + other.subscribe(new OtherObserver<>(observer, source)); + } + + static final class OtherObserver<T> + extends AtomicReference<Disposable> + implements CompletableObserver, Disposable { + + private static final long serialVersionUID = -8565274649390031272L; + + final SingleObserver<? super T> downstream; + + final SingleSource<T> source; + + OtherObserver(SingleObserver<? super T> actual, SingleSource<T> source) { + this.downstream = actual; + this.source = source; + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.setOnce(this, d)) { + + downstream.onSubscribe(this); + } + } + + @Override + public void onError(Throwable e) { + downstream.onError(e); + } + + @Override + public void onComplete() { + source.subscribe(new ResumeSingleObserver<>(this, downstream)); + } + + @Override + public void dispose() { + DisposableHelper.dispose(this); + } + + @Override + public boolean isDisposed() { + return DisposableHelper.isDisposed(get()); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleDelayWithObservable.java b/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleDelayWithObservable.java new file mode 100644 index 0000000000..58ed261e71 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleDelayWithObservable.java @@ -0,0 +1,100 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.single; + +import java.util.concurrent.atomic.AtomicReference; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; +import io.reactivex.rxjava3.internal.observers.ResumeSingleObserver; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +public final class SingleDelayWithObservable<T, U> extends Single<T> { + + final SingleSource<T> source; + + final ObservableSource<U> other; + + public SingleDelayWithObservable(SingleSource<T> source, ObservableSource<U> other) { + this.source = source; + this.other = other; + } + + @Override + protected void subscribeActual(SingleObserver<? super T> observer) { + other.subscribe(new OtherSubscriber<>(observer, source)); + } + + static final class OtherSubscriber<T, U> + extends AtomicReference<Disposable> + implements Observer<U>, Disposable { + + private static final long serialVersionUID = -8565274649390031272L; + + final SingleObserver<? super T> downstream; + + final SingleSource<T> source; + + boolean done; + + OtherSubscriber(SingleObserver<? super T> actual, SingleSource<T> source) { + this.downstream = actual; + this.source = source; + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.setOnce(this, d)) { + + downstream.onSubscribe(this); + } + } + + @Override + public void onNext(U value) { + get().dispose(); + onComplete(); + } + + @Override + public void onError(Throwable e) { + if (done) { + RxJavaPlugins.onError(e); + return; + } + done = true; + downstream.onError(e); + } + + @Override + public void onComplete() { + if (done) { + return; + } + done = true; + source.subscribe(new ResumeSingleObserver<>(this, downstream)); + } + + @Override + public void dispose() { + DisposableHelper.dispose(this); + } + + @Override + public boolean isDisposed() { + return DisposableHelper.isDisposed(get()); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleDelayWithPublisher.java b/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleDelayWithPublisher.java new file mode 100644 index 0000000000..12eeb7fd13 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleDelayWithPublisher.java @@ -0,0 +1,109 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.single; + +import java.util.concurrent.atomic.AtomicReference; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; +import io.reactivex.rxjava3.internal.observers.ResumeSingleObserver; +import io.reactivex.rxjava3.internal.subscriptions.SubscriptionHelper; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +public final class SingleDelayWithPublisher<T, U> extends Single<T> { + + final SingleSource<T> source; + + final Publisher<U> other; + + public SingleDelayWithPublisher(SingleSource<T> source, Publisher<U> other) { + this.source = source; + this.other = other; + } + + @Override + protected void subscribeActual(SingleObserver<? super T> observer) { + other.subscribe(new OtherSubscriber<>(observer, source)); + } + + static final class OtherSubscriber<T, U> + extends AtomicReference<Disposable> + implements FlowableSubscriber<U>, Disposable { + + private static final long serialVersionUID = -8565274649390031272L; + + final SingleObserver<? super T> downstream; + + final SingleSource<T> source; + + boolean done; + + Subscription upstream; + + OtherSubscriber(SingleObserver<? super T> actual, SingleSource<T> source) { + this.downstream = actual; + this.source = source; + } + + @Override + public void onSubscribe(Subscription s) { + if (SubscriptionHelper.validate(this.upstream, s)) { + this.upstream = s; + + downstream.onSubscribe(this); + + s.request(Long.MAX_VALUE); + } + } + + @Override + public void onNext(U value) { + upstream.cancel(); + onComplete(); + } + + @Override + public void onError(Throwable e) { + if (done) { + RxJavaPlugins.onError(e); + return; + } + done = true; + downstream.onError(e); + } + + @Override + public void onComplete() { + if (done) { + return; + } + done = true; + source.subscribe(new ResumeSingleObserver<>(this, downstream)); + } + + @Override + public void dispose() { + upstream.cancel(); + DisposableHelper.dispose(this); + } + + @Override + public boolean isDisposed() { + return DisposableHelper.isDisposed(get()); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleDelayWithSingle.java b/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleDelayWithSingle.java new file mode 100644 index 0000000000..1f2ffb21dd --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleDelayWithSingle.java @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.single; + +import java.util.concurrent.atomic.AtomicReference; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; +import io.reactivex.rxjava3.internal.observers.ResumeSingleObserver; + +public final class SingleDelayWithSingle<T, U> extends Single<T> { + + final SingleSource<T> source; + + final SingleSource<U> other; + + public SingleDelayWithSingle(SingleSource<T> source, SingleSource<U> other) { + this.source = source; + this.other = other; + } + + @Override + protected void subscribeActual(SingleObserver<? super T> observer) { + other.subscribe(new OtherObserver<>(observer, source)); + } + + static final class OtherObserver<T, U> + extends AtomicReference<Disposable> + implements SingleObserver<U>, Disposable { + + private static final long serialVersionUID = -8565274649390031272L; + + final SingleObserver<? super T> downstream; + + final SingleSource<T> source; + + OtherObserver(SingleObserver<? super T> actual, SingleSource<T> source) { + this.downstream = actual; + this.source = source; + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.setOnce(this, d)) { + + downstream.onSubscribe(this); + } + } + + @Override + public void onSuccess(U value) { + source.subscribe(new ResumeSingleObserver<>(this, downstream)); + } + + @Override + public void onError(Throwable e) { + downstream.onError(e); + } + + @Override + public void dispose() { + DisposableHelper.dispose(this); + } + + @Override + public boolean isDisposed() { + return DisposableHelper.isDisposed(get()); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleDematerialize.java b/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleDematerialize.java new file mode 100644 index 0000000000..e573560e76 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleDematerialize.java @@ -0,0 +1,105 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.single; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; + +import java.util.Objects; + +/** + * Maps the success value of the source to a Notification, then + * maps it back to the corresponding signal type. + * <p>History: 2.2.4 - experimental + * @param <T> the element type of the source + * @param <R> the element type of the Notification and result + * @since 3.0.0 + */ +public final class SingleDematerialize<T, R> extends Maybe<R> { + + final Single<T> source; + + final Function<? super T, Notification<R>> selector; + + public SingleDematerialize(Single<T> source, Function<? super T, Notification<R>> selector) { + this.source = source; + this.selector = selector; + } + + @Override + protected void subscribeActual(MaybeObserver<? super R> observer) { + source.subscribe(new DematerializeObserver<>(observer, selector)); + } + + static final class DematerializeObserver<T, R> implements SingleObserver<T>, Disposable { + + final MaybeObserver<? super R> downstream; + + final Function<? super T, Notification<R>> selector; + + Disposable upstream; + + DematerializeObserver(MaybeObserver<? super R> downstream, + Function<? super T, Notification<R>> selector) { + this.downstream = downstream; + this.selector = selector; + } + + @Override + public void dispose() { + upstream.dispose(); + } + + @Override + public boolean isDisposed() { + return upstream.isDisposed(); + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.validate(upstream, d)) { + upstream = d; + downstream.onSubscribe(this); + } + } + + @Override + public void onSuccess(T t) { + Notification<R> notification; + + try { + notification = Objects.requireNonNull(selector.apply(t), "The selector returned a null Notification"); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + downstream.onError(ex); + return; + } + if (notification.isOnNext()) { + downstream.onSuccess(notification.getValue()); + } else if (notification.isOnComplete()) { + downstream.onComplete(); + } else { + downstream.onError(notification.getError()); + } + } + + @Override + public void onError(Throwable e) { + downstream.onError(e); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleDetach.java b/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleDetach.java new file mode 100644 index 0000000000..2a9acc8a19 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleDetach.java @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.single; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; + +/** + * Breaks the references between the upstream and downstream when the Maybe terminates. + * <p>History: 2.1.5 - experimental + * @param <T> the value type + * @since 2.2 + */ +public final class SingleDetach<T> extends Single<T> { + + final SingleSource<T> source; + + public SingleDetach(SingleSource<T> source) { + this.source = source; + } + + @Override + protected void subscribeActual(SingleObserver<? super T> observer) { + source.subscribe(new DetachSingleObserver<>(observer)); + } + + static final class DetachSingleObserver<T> implements SingleObserver<T>, Disposable { + + SingleObserver<? super T> downstream; + + Disposable upstream; + + DetachSingleObserver(SingleObserver<? super T> downstream) { + this.downstream = downstream; + } + + @Override + public void dispose() { + downstream = null; + upstream.dispose(); + upstream = DisposableHelper.DISPOSED; + } + + @Override + public boolean isDisposed() { + return upstream.isDisposed(); + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.validate(this.upstream, d)) { + this.upstream = d; + + downstream.onSubscribe(this); + } + } + + @Override + public void onSuccess(T value) { + upstream = DisposableHelper.DISPOSED; + SingleObserver<? super T> a = downstream; + if (a != null) { + downstream = null; + a.onSuccess(value); + } + } + + @Override + public void onError(Throwable e) { + upstream = DisposableHelper.DISPOSED; + SingleObserver<? super T> a = downstream; + if (a != null) { + downstream = null; + a.onError(e); + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleDoAfterSuccess.java b/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleDoAfterSuccess.java new file mode 100644 index 0000000000..51b73f0216 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleDoAfterSuccess.java @@ -0,0 +1,95 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.single; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.Consumer; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +/** + * Calls a consumer after pushing the current item to the downstream. + * <p>History: 2.0.1 - experimental + * @param <T> the value type + * @since 2.1 + */ +public final class SingleDoAfterSuccess<T> extends Single<T> { + + final SingleSource<T> source; + + final Consumer<? super T> onAfterSuccess; + + public SingleDoAfterSuccess(SingleSource<T> source, Consumer<? super T> onAfterSuccess) { + this.source = source; + this.onAfterSuccess = onAfterSuccess; + } + + @Override + protected void subscribeActual(SingleObserver<? super T> observer) { + source.subscribe(new DoAfterObserver<>(observer, onAfterSuccess)); + } + + static final class DoAfterObserver<T> implements SingleObserver<T>, Disposable { + + final SingleObserver<? super T> downstream; + + final Consumer<? super T> onAfterSuccess; + + Disposable upstream; + + DoAfterObserver(SingleObserver<? super T> actual, Consumer<? super T> onAfterSuccess) { + this.downstream = actual; + this.onAfterSuccess = onAfterSuccess; + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.validate(this.upstream, d)) { + this.upstream = d; + + downstream.onSubscribe(this); + } + } + + @Override + public void onSuccess(T t) { + downstream.onSuccess(t); + + try { + onAfterSuccess.accept(t); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + // remember, onSuccess is a terminal event and we can't call onError + RxJavaPlugins.onError(ex); + } + } + + @Override + public void onError(Throwable e) { + downstream.onError(e); + } + + @Override + public void dispose() { + upstream.dispose(); + } + + @Override + public boolean isDisposed() { + return upstream.isDisposed(); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleDoAfterTerminate.java b/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleDoAfterTerminate.java new file mode 100644 index 0000000000..651c9ee60d --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleDoAfterTerminate.java @@ -0,0 +1,100 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.single; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.Action; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +/** + * Calls an action after pushing the current item or an error to the downstream. + * <p>History: 2.0.6 - experimental + * @param <T> the value type + * @since 2.1 + */ +public final class SingleDoAfterTerminate<T> extends Single<T> { + + final SingleSource<T> source; + + final Action onAfterTerminate; + + public SingleDoAfterTerminate(SingleSource<T> source, Action onAfterTerminate) { + this.source = source; + this.onAfterTerminate = onAfterTerminate; + } + + @Override + protected void subscribeActual(SingleObserver<? super T> observer) { + source.subscribe(new DoAfterTerminateObserver<>(observer, onAfterTerminate)); + } + + static final class DoAfterTerminateObserver<T> implements SingleObserver<T>, Disposable { + + final SingleObserver<? super T> downstream; + + final Action onAfterTerminate; + + Disposable upstream; + + DoAfterTerminateObserver(SingleObserver<? super T> actual, Action onAfterTerminate) { + this.downstream = actual; + this.onAfterTerminate = onAfterTerminate; + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.validate(this.upstream, d)) { + this.upstream = d; + + downstream.onSubscribe(this); + } + } + + @Override + public void onSuccess(T t) { + downstream.onSuccess(t); + + onAfterTerminate(); + } + + @Override + public void onError(Throwable e) { + downstream.onError(e); + + onAfterTerminate(); + } + + @Override + public void dispose() { + upstream.dispose(); + } + + @Override + public boolean isDisposed() { + return upstream.isDisposed(); + } + + private void onAfterTerminate() { + try { + onAfterTerminate.run(); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + RxJavaPlugins.onError(ex); + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleDoFinally.java b/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleDoFinally.java new file mode 100644 index 0000000000..a64060cc97 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleDoFinally.java @@ -0,0 +1,105 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.single; + +import java.util.concurrent.atomic.AtomicInteger; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.Action; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +/** + * Execute an action after an onSuccess, onError or a dispose event. + * <p>History: 2.0.1 - experimental + * @param <T> the value type + * @since 2.1 + */ +public final class SingleDoFinally<T> extends Single<T> { + + final SingleSource<T> source; + + final Action onFinally; + + public SingleDoFinally(SingleSource<T> source, Action onFinally) { + this.source = source; + this.onFinally = onFinally; + } + + @Override + protected void subscribeActual(SingleObserver<? super T> observer) { + source.subscribe(new DoFinallyObserver<>(observer, onFinally)); + } + + static final class DoFinallyObserver<T> extends AtomicInteger implements SingleObserver<T>, Disposable { + + private static final long serialVersionUID = 4109457741734051389L; + + final SingleObserver<? super T> downstream; + + final Action onFinally; + + Disposable upstream; + + DoFinallyObserver(SingleObserver<? super T> actual, Action onFinally) { + this.downstream = actual; + this.onFinally = onFinally; + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.validate(this.upstream, d)) { + this.upstream = d; + + downstream.onSubscribe(this); + } + } + + @Override + public void onSuccess(T t) { + downstream.onSuccess(t); + runFinally(); + } + + @Override + public void onError(Throwable t) { + downstream.onError(t); + runFinally(); + } + + @Override + public void dispose() { + upstream.dispose(); + runFinally(); + } + + @Override + public boolean isDisposed() { + return upstream.isDisposed(); + } + + void runFinally() { + if (compareAndSet(0, 1)) { + try { + onFinally.run(); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + RxJavaPlugins.onError(ex); + } + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleDoOnDispose.java b/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleDoOnDispose.java new file mode 100644 index 0000000000..8c9fd1cb1b --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleDoOnDispose.java @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.single; + +import java.util.concurrent.atomic.AtomicReference; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.Action; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +public final class SingleDoOnDispose<T> extends Single<T> { + final SingleSource<T> source; + + final Action onDispose; + + public SingleDoOnDispose(SingleSource<T> source, Action onDispose) { + this.source = source; + this.onDispose = onDispose; + } + + @Override + protected void subscribeActual(final SingleObserver<? super T> observer) { + + source.subscribe(new DoOnDisposeObserver<>(observer, onDispose)); + } + + static final class DoOnDisposeObserver<T> + extends AtomicReference<Action> + implements SingleObserver<T>, Disposable { + private static final long serialVersionUID = -8583764624474935784L; + + final SingleObserver<? super T> downstream; + + Disposable upstream; + + DoOnDisposeObserver(SingleObserver<? super T> actual, Action onDispose) { + this.downstream = actual; + this.lazySet(onDispose); + } + + @Override + public void dispose() { + Action a = getAndSet(null); + if (a != null) { + try { + a.run(); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + RxJavaPlugins.onError(ex); + } + upstream.dispose(); + } + } + + @Override + public boolean isDisposed() { + return upstream.isDisposed(); + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.validate(this.upstream, d)) { + this.upstream = d; + downstream.onSubscribe(this); + } + } + + @Override + public void onSuccess(T value) { + downstream.onSuccess(value); + } + + @Override + public void onError(Throwable e) { + downstream.onError(e); + } + } + +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleDoOnError.java b/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleDoOnError.java new file mode 100644 index 0000000000..dd24ac210c --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleDoOnError.java @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.single; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.Consumer; + +public final class SingleDoOnError<T> extends Single<T> { + + final SingleSource<T> source; + + final Consumer<? super Throwable> onError; + + public SingleDoOnError(SingleSource<T> source, Consumer<? super Throwable> onError) { + this.source = source; + this.onError = onError; + } + + @Override + protected void subscribeActual(final SingleObserver<? super T> observer) { + + source.subscribe(new DoOnError(observer)); + } + + final class DoOnError implements SingleObserver<T> { + private final SingleObserver<? super T> downstream; + + DoOnError(SingleObserver<? super T> observer) { + this.downstream = observer; + } + + @Override + public void onSubscribe(Disposable d) { + downstream.onSubscribe(d); + } + + @Override + public void onSuccess(T value) { + downstream.onSuccess(value); + } + + @Override + public void onError(Throwable e) { + try { + onError.accept(e); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + e = new CompositeException(e, ex); + } + downstream.onError(e); + } + + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleDoOnEvent.java b/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleDoOnEvent.java new file mode 100644 index 0000000000..de53462380 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleDoOnEvent.java @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.single; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.BiConsumer; + +public final class SingleDoOnEvent<T> extends Single<T> { + final SingleSource<T> source; + + final BiConsumer<? super T, ? super Throwable> onEvent; + + public SingleDoOnEvent(SingleSource<T> source, BiConsumer<? super T, ? super Throwable> onEvent) { + this.source = source; + this.onEvent = onEvent; + } + + @Override + protected void subscribeActual(final SingleObserver<? super T> observer) { + + source.subscribe(new DoOnEvent(observer)); + } + + final class DoOnEvent implements SingleObserver<T> { + private final SingleObserver<? super T> downstream; + + DoOnEvent(SingleObserver<? super T> observer) { + this.downstream = observer; + } + + @Override + public void onSubscribe(Disposable d) { + downstream.onSubscribe(d); + } + + @Override + public void onSuccess(T value) { + try { + onEvent.accept(value, null); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + downstream.onError(ex); + return; + } + + downstream.onSuccess(value); + } + + @Override + public void onError(Throwable e) { + try { + onEvent.accept(null, e); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + e = new CompositeException(e, ex); + } + downstream.onError(e); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleDoOnLifecycle.java b/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleDoOnLifecycle.java new file mode 100644 index 0000000000..2d4e15f27c --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleDoOnLifecycle.java @@ -0,0 +1,120 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.single; + +import io.reactivex.rxjava3.annotations.NonNull; +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.disposables.*; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +/** + * Invokes callbacks upon {@code onSubscribe} from upstream and + * {@code dispose} from downstream. + * + * @param <T> the element type of the flow + * @since 3.0.0 + */ +public final class SingleDoOnLifecycle<T> extends Single<T> { + + final Single<T> source; + + final Consumer<? super Disposable> onSubscribe; + + final Action onDispose; + + public SingleDoOnLifecycle(Single<T> upstream, Consumer<? super Disposable> onSubscribe, + Action onDispose) { + this.source = upstream; + this.onSubscribe = onSubscribe; + this.onDispose = onDispose; + } + + @Override + protected void subscribeActual(SingleObserver<? super T> observer) { + source.subscribe(new SingleLifecycleObserver<>(observer, onSubscribe, onDispose)); + } + + static final class SingleLifecycleObserver<T> implements SingleObserver<T>, Disposable { + + final SingleObserver<? super T> downstream; + + final Consumer<? super Disposable> onSubscribe; + + final Action onDispose; + + Disposable upstream; + + SingleLifecycleObserver(SingleObserver<? super T> downstream, Consumer<? super Disposable> onSubscribe, Action onDispose) { + this.downstream = downstream; + this.onSubscribe = onSubscribe; + this.onDispose = onDispose; + } + + @Override + public void onSubscribe(@NonNull Disposable d) { + // this way, multiple calls to onSubscribe can show up in tests that use doOnSubscribe to validate behavior + try { + onSubscribe.accept(d); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + d.dispose(); + this.upstream = DisposableHelper.DISPOSED; + EmptyDisposable.error(e, downstream); + return; + } + if (DisposableHelper.validate(this.upstream, d)) { + this.upstream = d; + downstream.onSubscribe(this); + } + } + + @Override + public void onSuccess(@NonNull T t) { + if (upstream != DisposableHelper.DISPOSED) { + upstream = DisposableHelper.DISPOSED; + downstream.onSuccess(t); + } + } + + @Override + public void onError(@NonNull Throwable e) { + if (upstream != DisposableHelper.DISPOSED) { + upstream = DisposableHelper.DISPOSED; + downstream.onError(e); + } else { + RxJavaPlugins.onError(e); + } + } + + @Override + public void dispose() { + try { + onDispose.run(); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + RxJavaPlugins.onError(e); + } + upstream.dispose(); + upstream = DisposableHelper.DISPOSED; + } + + @Override + public boolean isDisposed() { + return upstream.isDisposed(); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleDoOnSubscribe.java b/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleDoOnSubscribe.java new file mode 100644 index 0000000000..567f95404b --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleDoOnSubscribe.java @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.single; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.Consumer; +import io.reactivex.rxjava3.internal.disposables.EmptyDisposable; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +/** + * Calls a callback when the upstream calls onSubscribe with a disposable. + * + * @param <T> the value type + */ +public final class SingleDoOnSubscribe<T> extends Single<T> { + + final SingleSource<T> source; + + final Consumer<? super Disposable> onSubscribe; + + public SingleDoOnSubscribe(SingleSource<T> source, Consumer<? super Disposable> onSubscribe) { + this.source = source; + this.onSubscribe = onSubscribe; + } + + @Override + protected void subscribeActual(final SingleObserver<? super T> observer) { + source.subscribe(new DoOnSubscribeSingleObserver<>(observer, onSubscribe)); + } + + static final class DoOnSubscribeSingleObserver<T> implements SingleObserver<T> { + + final SingleObserver<? super T> downstream; + + final Consumer<? super Disposable> onSubscribe; + + boolean done; + + DoOnSubscribeSingleObserver(SingleObserver<? super T> actual, Consumer<? super Disposable> onSubscribe) { + this.downstream = actual; + this.onSubscribe = onSubscribe; + } + + @Override + public void onSubscribe(Disposable d) { + try { + onSubscribe.accept(d); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + done = true; + d.dispose(); + EmptyDisposable.error(ex, downstream); + return; + } + + downstream.onSubscribe(d); + } + + @Override + public void onSuccess(T value) { + if (done) { + return; + } + downstream.onSuccess(value); + } + + @Override + public void onError(Throwable e) { + if (done) { + RxJavaPlugins.onError(e); + return; + } + downstream.onError(e); + } + } + +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleDoOnSuccess.java b/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleDoOnSuccess.java new file mode 100644 index 0000000000..855dc7e388 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleDoOnSuccess.java @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.single; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.Consumer; + +public final class SingleDoOnSuccess<T> extends Single<T> { + + final SingleSource<T> source; + + final Consumer<? super T> onSuccess; + + public SingleDoOnSuccess(SingleSource<T> source, Consumer<? super T> onSuccess) { + this.source = source; + this.onSuccess = onSuccess; + } + + @Override + protected void subscribeActual(final SingleObserver<? super T> observer) { + + source.subscribe(new DoOnSuccess(observer)); + } + + final class DoOnSuccess implements SingleObserver<T> { + + final SingleObserver<? super T> downstream; + + DoOnSuccess(SingleObserver<? super T> observer) { + this.downstream = observer; + } + + @Override + public void onSubscribe(Disposable d) { + downstream.onSubscribe(d); + } + + @Override + public void onSuccess(T value) { + try { + onSuccess.accept(value); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + downstream.onError(ex); + return; + } + downstream.onSuccess(value); + } + + @Override + public void onError(Throwable e) { + downstream.onError(e); + } + + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleDoOnTerminate.java b/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleDoOnTerminate.java new file mode 100644 index 0000000000..d6f5dea7fa --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleDoOnTerminate.java @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.single; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.Action; + +public final class SingleDoOnTerminate<T> extends Single<T> { + + final SingleSource<T> source; + + final Action onTerminate; + + public SingleDoOnTerminate(SingleSource<T> source, Action onTerminate) { + this.source = source; + this.onTerminate = onTerminate; + } + + @Override + protected void subscribeActual(final SingleObserver<? super T> observer) { + source.subscribe(new DoOnTerminate(observer)); + } + + final class DoOnTerminate implements SingleObserver<T> { + + final SingleObserver<? super T> downstream; + + DoOnTerminate(SingleObserver<? super T> observer) { + this.downstream = observer; + } + + @Override + public void onSubscribe(Disposable d) { + downstream.onSubscribe(d); + } + + @Override + public void onSuccess(T value) { + try { + onTerminate.run(); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + downstream.onError(ex); + return; + } + + downstream.onSuccess(value); + } + + @Override + public void onError(Throwable e) { + try { + onTerminate.run(); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + e = new CompositeException(e, ex); + } + + downstream.onError(e); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleEquals.java b/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleEquals.java new file mode 100644 index 0000000000..d11f938a2e --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleEquals.java @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.single; + +import java.util.Objects; +import java.util.concurrent.atomic.AtomicInteger; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.*; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +public final class SingleEquals<T> extends Single<Boolean> { + + final SingleSource<? extends T> first; + final SingleSource<? extends T> second; + + public SingleEquals(SingleSource<? extends T> first, SingleSource<? extends T> second) { + this.first = first; + this.second = second; + } + + @Override + protected void subscribeActual(final SingleObserver<? super Boolean> observer) { + + final AtomicInteger count = new AtomicInteger(); + final Object[] values = { null, null }; + + final CompositeDisposable set = new CompositeDisposable(); + observer.onSubscribe(set); + + first.subscribe(new InnerObserver<T>(0, set, values, observer, count)); + second.subscribe(new InnerObserver<T>(1, set, values, observer, count)); + } + + static class InnerObserver<T> implements SingleObserver<T> { + final int index; + final CompositeDisposable set; + final Object[] values; + final SingleObserver<? super Boolean> downstream; + final AtomicInteger count; + + InnerObserver(int index, CompositeDisposable set, Object[] values, SingleObserver<? super Boolean> observer, AtomicInteger count) { + this.index = index; + this.set = set; + this.values = values; + this.downstream = observer; + this.count = count; + } + + @Override + public void onSubscribe(Disposable d) { + set.add(d); + } + + @Override + public void onSuccess(T value) { + values[index] = value; + + if (count.incrementAndGet() == 2) { + downstream.onSuccess(Objects.equals(values[0], values[1])); + } + } + + @Override + public void onError(Throwable e) { + int state = count.getAndSet(-1); + if (state == 0 || state == 1) { + set.dispose(); + downstream.onError(e); + } else { + RxJavaPlugins.onError(e); + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleError.java b/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleError.java new file mode 100644 index 0000000000..f016d2c9a9 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleError.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.single; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.Supplier; +import io.reactivex.rxjava3.internal.disposables.EmptyDisposable; +import io.reactivex.rxjava3.internal.util.ExceptionHelper; + +public final class SingleError<T> extends Single<T> { + + final Supplier<? extends Throwable> errorSupplier; + + public SingleError(Supplier<? extends Throwable> errorSupplier) { + this.errorSupplier = errorSupplier; + } + + @Override + protected void subscribeActual(SingleObserver<? super T> observer) { + Throwable error; + + try { + error = ExceptionHelper.nullCheck(errorSupplier.get(), "Supplier returned a null Throwable."); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + error = e; + } + + EmptyDisposable.error(error, observer); + } + +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleFlatMap.java b/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleFlatMap.java new file mode 100644 index 0000000000..ff38538b09 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleFlatMap.java @@ -0,0 +1,121 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.single; + +import java.util.Objects; +import java.util.concurrent.atomic.AtomicReference; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; + +public final class SingleFlatMap<T, R> extends Single<R> { + final SingleSource<? extends T> source; + + final Function<? super T, ? extends SingleSource<? extends R>> mapper; + + public SingleFlatMap(SingleSource<? extends T> source, Function<? super T, ? extends SingleSource<? extends R>> mapper) { + this.mapper = mapper; + this.source = source; + } + + @Override + protected void subscribeActual(SingleObserver<? super R> downstream) { + source.subscribe(new SingleFlatMapCallback<T, R>(downstream, mapper)); + } + + static final class SingleFlatMapCallback<T, R> + extends AtomicReference<Disposable> + implements SingleObserver<T>, Disposable { + private static final long serialVersionUID = 3258103020495908596L; + + final SingleObserver<? super R> downstream; + + final Function<? super T, ? extends SingleSource<? extends R>> mapper; + + SingleFlatMapCallback(SingleObserver<? super R> actual, + Function<? super T, ? extends SingleSource<? extends R>> mapper) { + this.downstream = actual; + this.mapper = mapper; + } + + @Override + public void dispose() { + DisposableHelper.dispose(this); + } + + @Override + public boolean isDisposed() { + return DisposableHelper.isDisposed(get()); + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.setOnce(this, d)) { + downstream.onSubscribe(this); + } + } + + @Override + public void onSuccess(T value) { + SingleSource<? extends R> o; + + try { + o = Objects.requireNonNull(mapper.apply(value), "The single returned by the mapper is null"); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + downstream.onError(e); + return; + } + + if (!isDisposed()) { + o.subscribe(new FlatMapSingleObserver<R>(this, downstream)); + } + } + + @Override + public void onError(Throwable e) { + downstream.onError(e); + } + + static final class FlatMapSingleObserver<R> implements SingleObserver<R> { + + final AtomicReference<Disposable> parent; + + final SingleObserver<? super R> downstream; + + FlatMapSingleObserver(AtomicReference<Disposable> parent, SingleObserver<? super R> downstream) { + this.parent = parent; + this.downstream = downstream; + } + + @Override + public void onSubscribe(final Disposable d) { + DisposableHelper.replace(parent, d); + } + + @Override + public void onSuccess(final R value) { + downstream.onSuccess(value); + } + + @Override + public void onError(final Throwable e) { + downstream.onError(e); + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleFlatMapBiSelector.java b/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleFlatMapBiSelector.java new file mode 100644 index 0000000000..7f6f58de84 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleFlatMapBiSelector.java @@ -0,0 +1,156 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.single; + +import java.util.Objects; +import java.util.concurrent.atomic.AtomicReference; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; + +/** + * Maps a source item to another SingleSource then calls a BiFunction with the + * original item and the secondary item to generate the final result. + * + * @param <T> the main value type + * @param <U> the second value type + * @param <R> the result value type + * @since 3.0.0 + */ +public final class SingleFlatMapBiSelector<T, U, R> extends Single<R> { + + final SingleSource<T> source; + + final Function<? super T, ? extends SingleSource<? extends U>> mapper; + + final BiFunction<? super T, ? super U, ? extends R> resultSelector; + + public SingleFlatMapBiSelector(SingleSource<T> source, + Function<? super T, ? extends SingleSource<? extends U>> mapper, + BiFunction<? super T, ? super U, ? extends R> resultSelector) { + this.source = source; + this.mapper = mapper; + this.resultSelector = resultSelector; + } + + @Override + protected void subscribeActual(SingleObserver<? super R> observer) { + source.subscribe(new FlatMapBiMainObserver<T, U, R>(observer, mapper, resultSelector)); + } + + static final class FlatMapBiMainObserver<T, U, R> + implements SingleObserver<T>, Disposable { + + final Function<? super T, ? extends SingleSource<? extends U>> mapper; + + final InnerObserver<T, U, R> inner; + + FlatMapBiMainObserver(SingleObserver<? super R> actual, + Function<? super T, ? extends SingleSource<? extends U>> mapper, + BiFunction<? super T, ? super U, ? extends R> resultSelector) { + this.inner = new InnerObserver<>(actual, resultSelector); + this.mapper = mapper; + } + + @Override + public void dispose() { + DisposableHelper.dispose(inner); + } + + @Override + public boolean isDisposed() { + return DisposableHelper.isDisposed(inner.get()); + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.setOnce(inner, d)) { + inner.downstream.onSubscribe(this); + } + } + + @Override + public void onSuccess(T value) { + SingleSource<? extends U> next; + + try { + next = Objects.requireNonNull(mapper.apply(value), "The mapper returned a null MaybeSource"); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + inner.downstream.onError(ex); + return; + } + + if (DisposableHelper.replace(inner, null)) { + inner.value = value; + next.subscribe(inner); + } + } + + @Override + public void onError(Throwable e) { + inner.downstream.onError(e); + } + + static final class InnerObserver<T, U, R> + extends AtomicReference<Disposable> + implements SingleObserver<U> { + + private static final long serialVersionUID = -2897979525538174559L; + + final SingleObserver<? super R> downstream; + + final BiFunction<? super T, ? super U, ? extends R> resultSelector; + + T value; + + InnerObserver(SingleObserver<? super R> actual, + BiFunction<? super T, ? super U, ? extends R> resultSelector) { + this.downstream = actual; + this.resultSelector = resultSelector; + } + + @Override + public void onSubscribe(Disposable d) { + DisposableHelper.setOnce(this, d); + } + + @Override + public void onSuccess(U value) { + T t = this.value; + this.value = null; + + R r; + + try { + r = Objects.requireNonNull(resultSelector.apply(t, value), "The resultSelector returned a null value"); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + downstream.onError(ex); + return; + } + + downstream.onSuccess(r); + } + + @Override + public void onError(Throwable e) { + downstream.onError(e); + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleFlatMapCompletable.java b/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleFlatMapCompletable.java new file mode 100644 index 0000000000..a06241c56c --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleFlatMapCompletable.java @@ -0,0 +1,105 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.single; + +import java.util.Objects; +import java.util.concurrent.atomic.AtomicReference; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; + +/** + * Maps the success value of the source SingleSource into a Completable. + * @param <T> the value type of the source SingleSource + */ +public final class SingleFlatMapCompletable<T> extends Completable { + + final SingleSource<T> source; + + final Function<? super T, ? extends CompletableSource> mapper; + + public SingleFlatMapCompletable(SingleSource<T> source, Function<? super T, ? extends CompletableSource> mapper) { + this.source = source; + this.mapper = mapper; + } + + @Override + protected void subscribeActual(CompletableObserver observer) { + FlatMapCompletableObserver<T> parent = new FlatMapCompletableObserver<>(observer, mapper); + observer.onSubscribe(parent); + source.subscribe(parent); + } + + static final class FlatMapCompletableObserver<T> + extends AtomicReference<Disposable> + implements SingleObserver<T>, CompletableObserver, Disposable { + + private static final long serialVersionUID = -2177128922851101253L; + + final CompletableObserver downstream; + + final Function<? super T, ? extends CompletableSource> mapper; + + FlatMapCompletableObserver(CompletableObserver actual, + Function<? super T, ? extends CompletableSource> mapper) { + this.downstream = actual; + this.mapper = mapper; + } + + @Override + public void dispose() { + DisposableHelper.dispose(this); + } + + @Override + public boolean isDisposed() { + return DisposableHelper.isDisposed(get()); + } + + @Override + public void onSubscribe(Disposable d) { + DisposableHelper.replace(this, d); + } + + @Override + public void onSuccess(T value) { + CompletableSource cs; + + try { + cs = Objects.requireNonNull(mapper.apply(value), "The mapper returned a null CompletableSource"); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + onError(ex); + return; + } + + if (!isDisposed()) { + cs.subscribe(this); + } + } + + @Override + public void onError(Throwable e) { + downstream.onError(e); + } + + @Override + public void onComplete() { + downstream.onComplete(); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleFlatMapIterableFlowable.java b/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleFlatMapIterableFlowable.java new file mode 100644 index 0000000000..6d69726588 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleFlatMapIterableFlowable.java @@ -0,0 +1,290 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.single; + +import java.util.Iterator; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicLong; + +import org.reactivestreams.Subscriber; + +import io.reactivex.rxjava3.annotations.Nullable; +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; +import io.reactivex.rxjava3.internal.subscriptions.*; +import io.reactivex.rxjava3.internal.util.BackpressureHelper; + +/** + * Maps a success value into an Iterable and streams it back as a Flowable. + * + * @param <T> the source value type + * @param <R> the element type of the Iterable + */ +public final class SingleFlatMapIterableFlowable<T, R> extends Flowable<R> { + + final SingleSource<T> source; + + final Function<? super T, ? extends Iterable<? extends R>> mapper; + + public SingleFlatMapIterableFlowable(SingleSource<T> source, + Function<? super T, ? extends Iterable<? extends R>> mapper) { + this.source = source; + this.mapper = mapper; + } + + @Override + protected void subscribeActual(Subscriber<? super R> s) { + source.subscribe(new FlatMapIterableObserver<>(s, mapper)); + } + + static final class FlatMapIterableObserver<T, R> + extends BasicIntQueueSubscription<R> + implements SingleObserver<T> { + + private static final long serialVersionUID = -8938804753851907758L; + + final Subscriber<? super R> downstream; + + final Function<? super T, ? extends Iterable<? extends R>> mapper; + + final AtomicLong requested; + + Disposable upstream; + + volatile Iterator<? extends R> it; + + volatile boolean cancelled; + + boolean outputFused; + + FlatMapIterableObserver(Subscriber<? super R> actual, + Function<? super T, ? extends Iterable<? extends R>> mapper) { + this.downstream = actual; + this.mapper = mapper; + this.requested = new AtomicLong(); + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.validate(this.upstream, d)) { + this.upstream = d; + + downstream.onSubscribe(this); + } + } + + @Override + public void onSuccess(T value) { + Iterator<? extends R> iterator; + boolean has; + try { + iterator = mapper.apply(value).iterator(); + + has = iterator.hasNext(); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + downstream.onError(ex); + return; + } + + if (!has) { + downstream.onComplete(); + return; + } + + this.it = iterator; + drain(); + } + + @Override + public void onError(Throwable e) { + upstream = DisposableHelper.DISPOSED; + downstream.onError(e); + } + + @Override + public void request(long n) { + if (SubscriptionHelper.validate(n)) { + BackpressureHelper.add(requested, n); + drain(); + } + } + + @Override + public void cancel() { + cancelled = true; + upstream.dispose(); + upstream = DisposableHelper.DISPOSED; + } + + void drain() { + if (getAndIncrement() != 0) { + return; + } + + Subscriber<? super R> a = downstream; + Iterator<? extends R> iterator = this.it; + + if (outputFused && iterator != null) { + a.onNext(null); + a.onComplete(); + return; + } + + int missed = 1; + + for (;;) { + + if (iterator != null) { + long r = requested.get(); + long e = 0L; + + if (r == Long.MAX_VALUE) { + fastPath(a, iterator); + return; + } + + while (e != r) { + if (cancelled) { + return; + } + + R v; + + try { + v = Objects.requireNonNull(iterator.next(), "The iterator returned a null value"); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + a.onError(ex); + return; + } + + a.onNext(v); + + if (cancelled) { + return; + } + + e++; + + boolean b; + + try { + b = iterator.hasNext(); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + a.onError(ex); + return; + } + + if (!b) { + a.onComplete(); + return; + } + } + + if (e != 0L) { + BackpressureHelper.produced(requested, e); + } + } + + missed = addAndGet(-missed); + if (missed == 0) { + break; + } + + if (iterator == null) { + iterator = it; + } + } + } + + void fastPath(Subscriber<? super R> a, Iterator<? extends R> iterator) { + for (;;) { + if (cancelled) { + return; + } + + R v; + + try { + v = iterator.next(); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + a.onError(ex); + return; + } + + a.onNext(v); + + if (cancelled) { + return; + } + + boolean b; + + try { + b = iterator.hasNext(); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + a.onError(ex); + return; + } + + if (!b) { + a.onComplete(); + return; + } + } + } + + @Override + public int requestFusion(int mode) { + if ((mode & ASYNC) != 0) { + outputFused = true; + return ASYNC; + } + return NONE; + } + + @Override + public void clear() { + it = null; + } + + @Override + public boolean isEmpty() { + return it == null; + } + + @Nullable + @Override + public R poll() { + Iterator<? extends R> iterator = it; + + if (iterator != null) { + R v = Objects.requireNonNull(iterator.next(), "The iterator returned a null value"); + if (!iterator.hasNext()) { + it = null; + } + return v; + } + return null; + } + + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleFlatMapIterableObservable.java b/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleFlatMapIterableObservable.java new file mode 100644 index 0000000000..c07131da8c --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleFlatMapIterableObservable.java @@ -0,0 +1,200 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.single; + +import java.util.Iterator; +import java.util.Objects; + +import io.reactivex.rxjava3.annotations.Nullable; +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; +import io.reactivex.rxjava3.internal.observers.BasicIntQueueDisposable; + +/** + * Maps a success value into an Iterable and streams it back as an Observable. + * + * @param <T> the source value type + * @param <R> the element type of the Iterable + */ +public final class SingleFlatMapIterableObservable<T, R> extends Observable<R> { + + final SingleSource<T> source; + + final Function<? super T, ? extends Iterable<? extends R>> mapper; + + public SingleFlatMapIterableObservable(SingleSource<T> source, + Function<? super T, ? extends Iterable<? extends R>> mapper) { + this.source = source; + this.mapper = mapper; + } + + @Override + protected void subscribeActual(Observer<? super R> observer) { + source.subscribe(new FlatMapIterableObserver<>(observer, mapper)); + } + + static final class FlatMapIterableObserver<T, R> + extends BasicIntQueueDisposable<R> + implements SingleObserver<T> { + + private static final long serialVersionUID = -8938804753851907758L; + + final Observer<? super R> downstream; + + final Function<? super T, ? extends Iterable<? extends R>> mapper; + + Disposable upstream; + + volatile Iterator<? extends R> it; + + volatile boolean cancelled; + + boolean outputFused; + + FlatMapIterableObserver(Observer<? super R> actual, + Function<? super T, ? extends Iterable<? extends R>> mapper) { + this.downstream = actual; + this.mapper = mapper; + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.validate(this.upstream, d)) { + this.upstream = d; + + downstream.onSubscribe(this); + } + } + + @Override + public void onSuccess(T value) { + Observer<? super R> a = downstream; + Iterator<? extends R> iterator; + boolean has; + try { + iterator = mapper.apply(value).iterator(); + + has = iterator.hasNext(); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + downstream.onError(ex); + return; + } + + if (!has) { + a.onComplete(); + return; + } + + if (outputFused) { + it = iterator; + a.onNext(null); + a.onComplete(); + } else { + for (;;) { + if (cancelled) { + return; + } + + R v; + + try { + v = iterator.next(); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + a.onError(ex); + return; + } + + a.onNext(v); + + if (cancelled) { + return; + } + + boolean b; + + try { + b = iterator.hasNext(); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + a.onError(ex); + return; + } + + if (!b) { + a.onComplete(); + return; + } + } + } + } + + @Override + public void onError(Throwable e) { + upstream = DisposableHelper.DISPOSED; + downstream.onError(e); + } + + @Override + public void dispose() { + cancelled = true; + upstream.dispose(); + upstream = DisposableHelper.DISPOSED; + } + + @Override + public boolean isDisposed() { + return cancelled; + } + + @Override + public int requestFusion(int mode) { + if ((mode & ASYNC) != 0) { + outputFused = true; + return ASYNC; + } + return NONE; + } + + @Override + public void clear() { + it = null; + } + + @Override + public boolean isEmpty() { + return it == null; + } + + @Nullable + @Override + public R poll() { + Iterator<? extends R> iterator = it; + + if (iterator != null) { + R v = Objects.requireNonNull(iterator.next(), "The iterator returned a null value"); + if (!iterator.hasNext()) { + it = null; + } + return v; + } + return null; + } + + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleFlatMapMaybe.java b/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleFlatMapMaybe.java new file mode 100644 index 0000000000..c58e6aeeae --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleFlatMapMaybe.java @@ -0,0 +1,127 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.single; + +import java.util.Objects; +import java.util.concurrent.atomic.AtomicReference; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; + +public final class SingleFlatMapMaybe<T, R> extends Maybe<R> { + + final SingleSource<? extends T> source; + + final Function<? super T, ? extends MaybeSource<? extends R>> mapper; + + public SingleFlatMapMaybe(SingleSource<? extends T> source, Function<? super T, ? extends MaybeSource<? extends R>> mapper) { + this.mapper = mapper; + this.source = source; + } + + @Override + protected void subscribeActual(MaybeObserver<? super R> downstream) { + source.subscribe(new FlatMapSingleObserver<T, R>(downstream, mapper)); + } + + static final class FlatMapSingleObserver<T, R> + extends AtomicReference<Disposable> + implements SingleObserver<T>, Disposable { + + private static final long serialVersionUID = -5843758257109742742L; + + final MaybeObserver<? super R> downstream; + + final Function<? super T, ? extends MaybeSource<? extends R>> mapper; + + FlatMapSingleObserver(MaybeObserver<? super R> actual, Function<? super T, ? extends MaybeSource<? extends R>> mapper) { + this.downstream = actual; + this.mapper = mapper; + } + + @Override + public void dispose() { + DisposableHelper.dispose(this); + } + + @Override + public boolean isDisposed() { + return DisposableHelper.isDisposed(get()); + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.setOnce(this, d)) { + downstream.onSubscribe(this); + } + } + + @Override + public void onSuccess(T value) { + MaybeSource<? extends R> ms; + + try { + ms = Objects.requireNonNull(mapper.apply(value), "The mapper returned a null MaybeSource"); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + onError(ex); + return; + } + + if (!isDisposed()) { + ms.subscribe(new FlatMapMaybeObserver<R>(this, downstream)); + } + } + + @Override + public void onError(Throwable e) { + downstream.onError(e); + } + } + + static final class FlatMapMaybeObserver<R> implements MaybeObserver<R> { + + final AtomicReference<Disposable> parent; + + final MaybeObserver<? super R> downstream; + + FlatMapMaybeObserver(AtomicReference<Disposable> parent, MaybeObserver<? super R> downstream) { + this.parent = parent; + this.downstream = downstream; + } + + @Override + public void onSubscribe(final Disposable d) { + DisposableHelper.replace(parent, d); + } + + @Override + public void onSuccess(final R value) { + downstream.onSuccess(value); + } + + @Override + public void onError(final Throwable e) { + downstream.onError(e); + } + + @Override + public void onComplete() { + downstream.onComplete(); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleFlatMapNotification.java b/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleFlatMapNotification.java new file mode 100644 index 0000000000..bb592a74a9 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleFlatMapNotification.java @@ -0,0 +1,147 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.single; + +import java.util.Objects; +import java.util.concurrent.atomic.AtomicReference; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; + +/** + * Maps a value into a SingleSource and relays its signal. + * + * @param <T> the source value type + * @param <R> the result value type + * @since 3.0.0 + */ +public final class SingleFlatMapNotification<T, R> extends Single<R> { + + final SingleSource<T> source; + + final Function<? super T, ? extends SingleSource<? extends R>> onSuccessMapper; + + final Function<? super Throwable, ? extends SingleSource<? extends R>> onErrorMapper; + + public SingleFlatMapNotification(SingleSource<T> source, + Function<? super T, ? extends SingleSource<? extends R>> onSuccessMapper, + Function<? super Throwable, ? extends SingleSource<? extends R>> onErrorMapper) { + this.source = source; + this.onSuccessMapper = onSuccessMapper; + this.onErrorMapper = onErrorMapper; + } + + @Override + protected void subscribeActual(SingleObserver<? super R> observer) { + source.subscribe(new FlatMapSingleObserver<>(observer, onSuccessMapper, onErrorMapper)); + } + + static final class FlatMapSingleObserver<T, R> + extends AtomicReference<Disposable> + implements SingleObserver<T>, Disposable { + + private static final long serialVersionUID = 4375739915521278546L; + + final SingleObserver<? super R> downstream; + + final Function<? super T, ? extends SingleSource<? extends R>> onSuccessMapper; + + final Function<? super Throwable, ? extends SingleSource<? extends R>> onErrorMapper; + + Disposable upstream; + + FlatMapSingleObserver(SingleObserver<? super R> actual, + Function<? super T, ? extends SingleSource<? extends R>> onSuccessMapper, + Function<? super Throwable, ? extends SingleSource<? extends R>> onErrorMapper) { + this.downstream = actual; + this.onSuccessMapper = onSuccessMapper; + this.onErrorMapper = onErrorMapper; + } + + @Override + public void dispose() { + DisposableHelper.dispose(this); + upstream.dispose(); + } + + @Override + public boolean isDisposed() { + return DisposableHelper.isDisposed(get()); + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.validate(this.upstream, d)) { + this.upstream = d; + + downstream.onSubscribe(this); + } + } + + @Override + public void onSuccess(T value) { + SingleSource<? extends R> source; + + try { + source = Objects.requireNonNull(onSuccessMapper.apply(value), "The onSuccessMapper returned a null SingleSource"); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + downstream.onError(ex); + return; + } + + if (!isDisposed()) { + source.subscribe(new InnerObserver()); + } + } + + @Override + public void onError(Throwable e) { + SingleSource<? extends R> source; + + try { + source = Objects.requireNonNull(onErrorMapper.apply(e), "The onErrorMapper returned a null SingleSource"); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + downstream.onError(new CompositeException(e, ex)); + return; + } + + if (!isDisposed()) { + source.subscribe(new InnerObserver()); + } + } + + final class InnerObserver implements SingleObserver<R> { + + @Override + public void onSubscribe(Disposable d) { + DisposableHelper.setOnce(FlatMapSingleObserver.this, d); + } + + @Override + public void onSuccess(R value) { + downstream.onSuccess(value); + } + + @Override + public void onError(Throwable e) { + downstream.onError(e); + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleFlatMapPublisher.java b/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleFlatMapPublisher.java new file mode 100644 index 0000000000..59a067832f --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleFlatMapPublisher.java @@ -0,0 +1,132 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.single; + +import java.util.Objects; +import java.util.concurrent.atomic.*; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.subscriptions.SubscriptionHelper; + +/** + * A Flowable that emits items based on applying a specified function to the item emitted by the + * source Single, where that function returns a Publisher. + * <p> + * <img width="640" height="305" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/Single.flatMapPublisher.v3.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The returned {@code Flowable} honors the backpressure of the downstream consumer + * and the {@code Publisher} returned by the mapper function is expected to honor it as well.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code flatMapPublisher} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T> the source value type + * @param <R> the result value type + * + * @see <a href="http://reactivex.io/documentation/operators/flatmap.html">ReactiveX operators documentation: FlatMap</a> + * @since 2.1.15 + */ +public final class SingleFlatMapPublisher<T, R> extends Flowable<R> { + + final SingleSource<T> source; + final Function<? super T, ? extends Publisher<? extends R>> mapper; + + public SingleFlatMapPublisher(SingleSource<T> source, + Function<? super T, ? extends Publisher<? extends R>> mapper) { + this.source = source; + this.mapper = mapper; + } + + @Override + protected void subscribeActual(Subscriber<? super R> downstream) { + source.subscribe(new SingleFlatMapPublisherObserver<>(downstream, mapper)); + } + + static final class SingleFlatMapPublisherObserver<S, T> extends AtomicLong + implements SingleObserver<S>, FlowableSubscriber<T>, Subscription { + + private static final long serialVersionUID = 7759721921468635667L; + + final Subscriber<? super T> downstream; + final Function<? super S, ? extends Publisher<? extends T>> mapper; + final AtomicReference<Subscription> parent; + Disposable disposable; + + SingleFlatMapPublisherObserver(Subscriber<? super T> actual, + Function<? super S, ? extends Publisher<? extends T>> mapper) { + this.downstream = actual; + this.mapper = mapper; + this.parent = new AtomicReference<>(); + } + + @Override + public void onSubscribe(Disposable d) { + this.disposable = d; + downstream.onSubscribe(this); + } + + @Override + public void onSuccess(S value) { + Publisher<? extends T> f; + try { + f = Objects.requireNonNull(mapper.apply(value), "the mapper returned a null Publisher"); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + downstream.onError(e); + return; + } + if (parent.get() != SubscriptionHelper.CANCELLED) { + f.subscribe(this); + } + } + + @Override + public void onSubscribe(Subscription s) { + SubscriptionHelper.deferredSetOnce(parent, this, s); + } + + @Override + public void onNext(T t) { + downstream.onNext(t); + } + + @Override + public void onComplete() { + downstream.onComplete(); + } + + @Override + public void onError(Throwable e) { + downstream.onError(e); + } + + @Override + public void request(long n) { + SubscriptionHelper.deferredRequest(parent, this, n); + } + + @Override + public void cancel() { + disposable.dispose(); + SubscriptionHelper.cancel(parent); + } + } + +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleFromCallable.java b/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleFromCallable.java new file mode 100644 index 0000000000..d1a6b73384 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleFromCallable.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.single; + +import java.util.Objects; +import java.util.concurrent.Callable; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.*; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +public final class SingleFromCallable<T> extends Single<T> { + + final Callable<? extends T> callable; + + public SingleFromCallable(Callable<? extends T> callable) { + this.callable = callable; + } + + @Override + protected void subscribeActual(SingleObserver<? super T> observer) { + Disposable d = Disposable.empty(); + observer.onSubscribe(d); + + if (d.isDisposed()) { + return; + } + T value; + + try { + value = Objects.requireNonNull(callable.call(), "The callable returned a null value"); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + if (!d.isDisposed()) { + observer.onError(ex); + } else { + RxJavaPlugins.onError(ex); + } + return; + } + + if (!d.isDisposed()) { + observer.onSuccess(value); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleFromPublisher.java b/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleFromPublisher.java new file mode 100644 index 0000000000..860e47f904 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleFromPublisher.java @@ -0,0 +1,116 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.single; + +import java.util.NoSuchElementException; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.subscriptions.SubscriptionHelper; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +public final class SingleFromPublisher<T> extends Single<T> { + + final Publisher<? extends T> publisher; + + public SingleFromPublisher(Publisher<? extends T> publisher) { + this.publisher = publisher; + } + + @Override + protected void subscribeActual(final SingleObserver<? super T> observer) { + publisher.subscribe(new ToSingleObserver<T>(observer)); + } + + static final class ToSingleObserver<T> implements FlowableSubscriber<T>, Disposable { + final SingleObserver<? super T> downstream; + + Subscription upstream; + + T value; + + boolean done; + + volatile boolean disposed; + + ToSingleObserver(SingleObserver<? super T> downstream) { + this.downstream = downstream; + } + + @Override + public void onSubscribe(Subscription s) { + if (SubscriptionHelper.validate(this.upstream, s)) { + this.upstream = s; + + downstream.onSubscribe(this); + + s.request(Long.MAX_VALUE); + } + } + + @Override + public void onNext(T t) { + if (done) { + return; + } + if (value != null) { + upstream.cancel(); + done = true; + this.value = null; + downstream.onError(new IndexOutOfBoundsException("Too many elements in the Publisher")); + } else { + value = t; + } + } + + @Override + public void onError(Throwable t) { + if (done) { + RxJavaPlugins.onError(t); + return; + } + done = true; + this.value = null; + downstream.onError(t); + } + + @Override + public void onComplete() { + if (done) { + return; + } + done = true; + T v = this.value; + this.value = null; + if (v == null) { + downstream.onError(new NoSuchElementException("The source Publisher is empty")); + } else { + downstream.onSuccess(v); + } + } + + @Override + public boolean isDisposed() { + return disposed; + } + + @Override + public void dispose() { + disposed = true; + upstream.cancel(); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleFromSupplier.java b/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleFromSupplier.java new file mode 100644 index 0000000000..83e2dc4780 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleFromSupplier.java @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.single; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.*; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.Supplier; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +import java.util.Objects; + +/** + * Calls a supplier and emits its value or exception to the incoming SingleObserver. + * @param <T> the value type returned + * @since 3.0.0 + */ +public final class SingleFromSupplier<T> extends Single<T> { + + final Supplier<? extends T> supplier; + + public SingleFromSupplier(Supplier<? extends T> supplier) { + this.supplier = supplier; + } + + @Override + protected void subscribeActual(SingleObserver<? super T> observer) { + Disposable d = Disposable.empty(); + observer.onSubscribe(d); + + if (d.isDisposed()) { + return; + } + T value; + + try { + value = Objects.requireNonNull(supplier.get(), "The supplier returned a null value"); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + if (!d.isDisposed()) { + observer.onError(ex); + } else { + RxJavaPlugins.onError(ex); + } + return; + } + + if (!d.isDisposed()) { + observer.onSuccess(value); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleFromUnsafeSource.java b/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleFromUnsafeSource.java new file mode 100644 index 0000000000..e09e38c833 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleFromUnsafeSource.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.single; + +import io.reactivex.rxjava3.core.*; + +public final class SingleFromUnsafeSource<T> extends Single<T> { + final SingleSource<T> source; + + public SingleFromUnsafeSource(SingleSource<T> source) { + this.source = source; + } + + @Override + protected void subscribeActual(SingleObserver<? super T> observer) { + source.subscribe(observer); + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleHide.java b/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleHide.java new file mode 100644 index 0000000000..01a4e36d6e --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleHide.java @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.single; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; + +public final class SingleHide<T> extends Single<T> { + + final SingleSource<? extends T> source; + + public SingleHide(SingleSource<? extends T> source) { + this.source = source; + } + + @Override + protected void subscribeActual(SingleObserver<? super T> observer) { + source.subscribe(new HideSingleObserver<T>(observer)); + } + + static final class HideSingleObserver<T> implements SingleObserver<T>, Disposable { + + final SingleObserver<? super T> downstream; + + Disposable upstream; + + HideSingleObserver(SingleObserver<? super T> downstream) { + this.downstream = downstream; + } + + @Override + public void dispose() { + upstream.dispose(); + } + + @Override + public boolean isDisposed() { + return upstream.isDisposed(); + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.validate(this.upstream, d)) { + this.upstream = d; + downstream.onSubscribe(this); + } + } + + @Override + public void onSuccess(T value) { + downstream.onSuccess(value); + } + + @Override + public void onError(Throwable e) { + downstream.onError(e); + } + } + +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleInternalHelper.java b/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleInternalHelper.java new file mode 100644 index 0000000000..5df3dae35d --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleInternalHelper.java @@ -0,0 +1,101 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.single; + +import java.util.*; + +import org.reactivestreams.Publisher; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.functions.*; + +/** + * Helper utility class to support Single with inner classes. + */ +public final class SingleInternalHelper { + + /** Utility class. */ + private SingleInternalHelper() { + throw new IllegalStateException("No instances!"); + } + + enum NoSuchElementSupplier implements Supplier<NoSuchElementException> { + INSTANCE; + + @Override + public NoSuchElementException get() { + return new NoSuchElementException(); + } + } + + public static Supplier<NoSuchElementException> emptyThrower() { + return NoSuchElementSupplier.INSTANCE; + } + + @SuppressWarnings("rawtypes") + enum ToFlowable implements Function<SingleSource, Publisher> { + INSTANCE; + @SuppressWarnings("unchecked") + @Override + public Publisher apply(SingleSource v) { + return new SingleToFlowable(v); + } + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + public static <T> Function<SingleSource<? extends T>, Publisher<? extends T>> toFlowable() { + return (Function)ToFlowable.INSTANCE; + } + + static final class ToFlowableIterator<T> implements Iterator<Flowable<T>> { + private final Iterator<? extends SingleSource<? extends T>> sit; + + ToFlowableIterator(Iterator<? extends SingleSource<? extends T>> sit) { + this.sit = sit; + } + + @Override + public boolean hasNext() { + return sit.hasNext(); + } + + @Override + public Flowable<T> next() { + return new SingleToFlowable<>(sit.next()); + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + } + + static final class ToFlowableIterable<T> implements Iterable<Flowable<T>> { + + private final Iterable<? extends SingleSource<? extends T>> sources; + + ToFlowableIterable(Iterable<? extends SingleSource<? extends T>> sources) { + this.sources = sources; + } + + @Override + public Iterator<Flowable<T>> iterator() { + return new ToFlowableIterator<>(sources.iterator()); + } + } + + public static <T> Iterable<? extends Flowable<T>> iterableToFlowable(final Iterable<? extends SingleSource<? extends T>> sources) { + return new ToFlowableIterable<>(sources); + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleJust.java b/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleJust.java new file mode 100644 index 0000000000..ceb94b93a1 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleJust.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.single; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; + +public final class SingleJust<T> extends Single<T> { + + final T value; + + public SingleJust(T value) { + this.value = value; + } + + @Override + protected void subscribeActual(SingleObserver<? super T> observer) { + observer.onSubscribe(Disposable.disposed()); + observer.onSuccess(value); + } + +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleLift.java b/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleLift.java new file mode 100644 index 0000000000..5b127e58cb --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleLift.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.single; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.internal.disposables.EmptyDisposable; + +import java.util.Objects; + +public final class SingleLift<T, R> extends Single<R> { + + final SingleSource<T> source; + + final SingleOperator<? extends R, ? super T> onLift; + + public SingleLift(SingleSource<T> source, SingleOperator<? extends R, ? super T> onLift) { + this.source = source; + this.onLift = onLift; + } + + @Override + protected void subscribeActual(SingleObserver<? super R> observer) { + SingleObserver<? super T> sr; + + try { + sr = Objects.requireNonNull(onLift.apply(observer), "The onLift returned a null SingleObserver"); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + EmptyDisposable.error(ex, observer); + return; + } + + source.subscribe(sr); + } + +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleMap.java b/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleMap.java new file mode 100644 index 0000000000..3fb20516ce --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleMap.java @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.single; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.Function; + +import java.util.Objects; + +public final class SingleMap<T, R> extends Single<R> { + final SingleSource<? extends T> source; + + final Function<? super T, ? extends R> mapper; + + public SingleMap(SingleSource<? extends T> source, Function<? super T, ? extends R> mapper) { + this.source = source; + this.mapper = mapper; + } + + @Override + protected void subscribeActual(final SingleObserver<? super R> t) { + source.subscribe(new MapSingleObserver<T, R>(t, mapper)); + } + + static final class MapSingleObserver<T, R> implements SingleObserver<T> { + + final SingleObserver<? super R> t; + + final Function<? super T, ? extends R> mapper; + + MapSingleObserver(SingleObserver<? super R> t, Function<? super T, ? extends R> mapper) { + this.t = t; + this.mapper = mapper; + } + + @Override + public void onSubscribe(Disposable d) { + t.onSubscribe(d); + } + + @Override + public void onSuccess(T value) { + R v; + try { + v = Objects.requireNonNull(mapper.apply(value), "The mapper function returned a null value."); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + onError(e); + return; + } + + t.onSuccess(v); + } + + @Override + public void onError(Throwable e) { + t.onError(e); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleMaterialize.java b/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleMaterialize.java new file mode 100644 index 0000000000..c09d28ffd1 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleMaterialize.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.single; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.internal.operators.mixed.MaterializeSingleObserver; + +/** + * Turn the signal types of a Single source into a single Notification of + * equal kind. + * <p>History: 2.2.4 - experimental + * + * @param <T> the element type of the source + * @since 3.0.0 + */ +public final class SingleMaterialize<T> extends Single<Notification<T>> { + + final Single<T> source; + + public SingleMaterialize(Single<T> source) { + this.source = source; + } + + @Override + protected void subscribeActual(SingleObserver<? super Notification<T>> observer) { + source.subscribe(new MaterializeSingleObserver<>(observer)); + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleNever.java b/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleNever.java new file mode 100644 index 0000000000..0feefe196d --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleNever.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.single; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.internal.disposables.EmptyDisposable; + +public final class SingleNever extends Single<Object> { + public static final Single<Object> INSTANCE = new SingleNever(); + + private SingleNever() { + } + + @Override + protected void subscribeActual(SingleObserver<? super Object> observer) { + observer.onSubscribe(EmptyDisposable.NEVER); + } + +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleObserveOn.java b/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleObserveOn.java new file mode 100644 index 0000000000..e513e16fa2 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleObserveOn.java @@ -0,0 +1,95 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.single; + +import java.util.concurrent.atomic.AtomicReference; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; + +public final class SingleObserveOn<T> extends Single<T> { + + final SingleSource<T> source; + + final Scheduler scheduler; + + public SingleObserveOn(SingleSource<T> source, Scheduler scheduler) { + this.source = source; + this.scheduler = scheduler; + } + + @Override + protected void subscribeActual(final SingleObserver<? super T> observer) { + source.subscribe(new ObserveOnSingleObserver<>(observer, scheduler)); + } + + static final class ObserveOnSingleObserver<T> extends AtomicReference<Disposable> + implements SingleObserver<T>, Disposable, Runnable { + private static final long serialVersionUID = 3528003840217436037L; + + final SingleObserver<? super T> downstream; + + final Scheduler scheduler; + + T value; + Throwable error; + + ObserveOnSingleObserver(SingleObserver<? super T> actual, Scheduler scheduler) { + this.downstream = actual; + this.scheduler = scheduler; + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.setOnce(this, d)) { + downstream.onSubscribe(this); + } + } + + @Override + public void onSuccess(T value) { + this.value = value; + Disposable d = scheduler.scheduleDirect(this); + DisposableHelper.replace(this, d); + } + + @Override + public void onError(Throwable e) { + this.error = e; + Disposable d = scheduler.scheduleDirect(this); + DisposableHelper.replace(this, d); + } + + @Override + public void run() { + Throwable ex = error; + if (ex != null) { + downstream.onError(ex); + } else { + downstream.onSuccess(value); + } + } + + @Override + public void dispose() { + DisposableHelper.dispose(this); + } + + @Override + public boolean isDisposed() { + return DisposableHelper.isDisposed(get()); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleOnErrorComplete.java b/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleOnErrorComplete.java new file mode 100644 index 0000000000..bd083cddb1 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleOnErrorComplete.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.single; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.functions.Predicate; +import io.reactivex.rxjava3.internal.operators.maybe.MaybeOnErrorComplete; + +/** + * Emits an onComplete if the source emits an onError and the predicate returns true for + * that Throwable. + * + * @param <T> the value type + * @since 3.0.0 + */ +public final class SingleOnErrorComplete<T> extends Maybe<T> { + + final Single<T> source; + + final Predicate<? super Throwable> predicate; + + public SingleOnErrorComplete(Single<T> source, + Predicate<? super Throwable> predicate) { + this.source = source; + this.predicate = predicate; + } + + @Override + protected void subscribeActual(MaybeObserver<? super T> observer) { + source.subscribe(new MaybeOnErrorComplete.OnErrorCompleteMultiObserver<T>(observer, predicate)); + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleOnErrorReturn.java b/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleOnErrorReturn.java new file mode 100644 index 0000000000..d1f8b66fd9 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleOnErrorReturn.java @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.single; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.Function; + +public final class SingleOnErrorReturn<T> extends Single<T> { + final SingleSource<? extends T> source; + + final Function<? super Throwable, ? extends T> valueSupplier; + + final T value; + + public SingleOnErrorReturn(SingleSource<? extends T> source, + Function<? super Throwable, ? extends T> valueSupplier, T value) { + this.source = source; + this.valueSupplier = valueSupplier; + this.value = value; + } + + @Override + protected void subscribeActual(final SingleObserver<? super T> observer) { + + source.subscribe(new OnErrorReturn(observer)); + } + + final class OnErrorReturn implements SingleObserver<T> { + + private final SingleObserver<? super T> observer; + + OnErrorReturn(SingleObserver<? super T> observer) { + this.observer = observer; + } + + @Override + public void onError(Throwable e) { + T v; + + if (valueSupplier != null) { + try { + v = valueSupplier.apply(e); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + observer.onError(new CompositeException(e, ex)); + return; + } + } else { + v = value; + } + + if (v == null) { + NullPointerException npe = new NullPointerException("Value supplied was null"); + npe.initCause(e); + observer.onError(npe); + return; + } + + observer.onSuccess(v); + } + + @Override + public void onSubscribe(Disposable d) { + observer.onSubscribe(d); + } + + @Override + public void onSuccess(T value) { + observer.onSuccess(value); + } + + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleResumeNext.java b/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleResumeNext.java new file mode 100644 index 0000000000..554a2a254b --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleResumeNext.java @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.single; + +import java.util.Objects; +import java.util.concurrent.atomic.AtomicReference; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; +import io.reactivex.rxjava3.internal.observers.ResumeSingleObserver; + +public final class SingleResumeNext<T> extends Single<T> { + final SingleSource<? extends T> source; + + final Function<? super Throwable, ? extends SingleSource<? extends T>> nextFunction; + + public SingleResumeNext(SingleSource<? extends T> source, + Function<? super Throwable, ? extends SingleSource<? extends T>> nextFunction) { + this.source = source; + this.nextFunction = nextFunction; + } + + @Override + protected void subscribeActual(final SingleObserver<? super T> observer) { + source.subscribe(new ResumeMainSingleObserver<>(observer, nextFunction)); + } + + static final class ResumeMainSingleObserver<T> extends AtomicReference<Disposable> + implements SingleObserver<T>, Disposable { + private static final long serialVersionUID = -5314538511045349925L; + + final SingleObserver<? super T> downstream; + + final Function<? super Throwable, ? extends SingleSource<? extends T>> nextFunction; + + ResumeMainSingleObserver(SingleObserver<? super T> actual, + Function<? super Throwable, ? extends SingleSource<? extends T>> nextFunction) { + this.downstream = actual; + this.nextFunction = nextFunction; + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.setOnce(this, d)) { + downstream.onSubscribe(this); + } + } + + @Override + public void onSuccess(T value) { + downstream.onSuccess(value); + } + + @Override + public void onError(Throwable e) { + SingleSource<? extends T> source; + + try { + source = Objects.requireNonNull(nextFunction.apply(e), "The nextFunction returned a null SingleSource."); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + downstream.onError(new CompositeException(e, ex)); + return; + } + + source.subscribe(new ResumeSingleObserver<T>(this, downstream)); + } + + @Override + public void dispose() { + DisposableHelper.dispose(this); + } + + @Override + public boolean isDisposed() { + return DisposableHelper.isDisposed(get()); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleSubscribeOn.java b/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleSubscribeOn.java new file mode 100644 index 0000000000..56cf684e9f --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleSubscribeOn.java @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.single; + +import java.util.concurrent.atomic.AtomicReference; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.disposables.*; + +public final class SingleSubscribeOn<T> extends Single<T> { + final SingleSource<? extends T> source; + + final Scheduler scheduler; + + public SingleSubscribeOn(SingleSource<? extends T> source, Scheduler scheduler) { + this.source = source; + this.scheduler = scheduler; + } + + @Override + protected void subscribeActual(final SingleObserver<? super T> observer) { + final SubscribeOnObserver<T> parent = new SubscribeOnObserver<>(observer, source); + observer.onSubscribe(parent); + + Disposable f = scheduler.scheduleDirect(parent); + + parent.task.replace(f); + + } + + static final class SubscribeOnObserver<T> + extends AtomicReference<Disposable> + implements SingleObserver<T>, Disposable, Runnable { + + private static final long serialVersionUID = 7000911171163930287L; + + final SingleObserver<? super T> downstream; + + final SequentialDisposable task; + + final SingleSource<? extends T> source; + + SubscribeOnObserver(SingleObserver<? super T> actual, SingleSource<? extends T> source) { + this.downstream = actual; + this.source = source; + this.task = new SequentialDisposable(); + } + + @Override + public void onSubscribe(Disposable d) { + DisposableHelper.setOnce(this, d); + } + + @Override + public void onSuccess(T value) { + downstream.onSuccess(value); + } + + @Override + public void onError(Throwable e) { + downstream.onError(e); + } + + @Override + public void dispose() { + DisposableHelper.dispose(this); + task.dispose(); + } + + @Override + public boolean isDisposed() { + return DisposableHelper.isDisposed(get()); + } + + @Override + public void run() { + source.subscribe(this); + } + } + +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleTakeUntil.java b/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleTakeUntil.java new file mode 100644 index 0000000000..fb5d0989f6 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleTakeUntil.java @@ -0,0 +1,167 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.single; + +import java.util.concurrent.CancellationException; +import java.util.concurrent.atomic.AtomicReference; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; +import io.reactivex.rxjava3.internal.subscriptions.SubscriptionHelper; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +/** + * Signals the events of the source Single or signals a CancellationException if the + * other Publisher signalled first. + * @param <T> the main value type + * @param <U> the other value type + */ +public final class SingleTakeUntil<T, U> extends Single<T> { + + final SingleSource<T> source; + + final Publisher<U> other; + + public SingleTakeUntil(SingleSource<T> source, Publisher<U> other) { + this.source = source; + this.other = other; + } + + @Override + protected void subscribeActual(SingleObserver<? super T> observer) { + TakeUntilMainObserver<T> parent = new TakeUntilMainObserver<>(observer); + observer.onSubscribe(parent); + + other.subscribe(parent.other); + + source.subscribe(parent); + } + + static final class TakeUntilMainObserver<T> + extends AtomicReference<Disposable> + implements SingleObserver<T>, Disposable { + + private static final long serialVersionUID = -622603812305745221L; + + final SingleObserver<? super T> downstream; + + final TakeUntilOtherSubscriber other; + + TakeUntilMainObserver(SingleObserver<? super T> downstream) { + this.downstream = downstream; + this.other = new TakeUntilOtherSubscriber(this); + } + + @Override + public void dispose() { + DisposableHelper.dispose(this); + other.dispose(); + } + + @Override + public boolean isDisposed() { + return DisposableHelper.isDisposed(get()); + } + + @Override + public void onSubscribe(Disposable d) { + DisposableHelper.setOnce(this, d); + } + + @Override + public void onSuccess(T value) { + other.dispose(); + + Disposable a = getAndSet(DisposableHelper.DISPOSED); + if (a != DisposableHelper.DISPOSED) { + downstream.onSuccess(value); + } + } + + @Override + public void onError(Throwable e) { + other.dispose(); + + Disposable a = get(); + if (a != DisposableHelper.DISPOSED) { + a = getAndSet(DisposableHelper.DISPOSED); + if (a != DisposableHelper.DISPOSED) { + downstream.onError(e); + return; + } + } + RxJavaPlugins.onError(e); + } + + void otherError(Throwable e) { + Disposable a = get(); + if (a != DisposableHelper.DISPOSED) { + a = getAndSet(DisposableHelper.DISPOSED); + if (a != DisposableHelper.DISPOSED) { + if (a != null) { + a.dispose(); + } + downstream.onError(e); + return; + } + } + RxJavaPlugins.onError(e); + } + } + + static final class TakeUntilOtherSubscriber + extends AtomicReference<Subscription> + implements FlowableSubscriber<Object> { + + private static final long serialVersionUID = 5170026210238877381L; + + final TakeUntilMainObserver<?> parent; + + TakeUntilOtherSubscriber(TakeUntilMainObserver<?> parent) { + this.parent = parent; + } + + @Override + public void onSubscribe(Subscription s) { + SubscriptionHelper.setOnce(this, s, Long.MAX_VALUE); + } + + @Override + public void onNext(Object t) { + if (SubscriptionHelper.cancel(this)) { + parent.otherError(new CancellationException()); + } + } + + @Override + public void onError(Throwable t) { + parent.otherError(t); + } + + @Override + public void onComplete() { + if (get() != SubscriptionHelper.CANCELLED) { + lazySet(SubscriptionHelper.CANCELLED); + parent.otherError(new CancellationException()); + } + } + + public void dispose() { + SubscriptionHelper.cancel(this); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleTimeInterval.java b/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleTimeInterval.java new file mode 100644 index 0000000000..f6d65f7c14 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleTimeInterval.java @@ -0,0 +1,100 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.single; + +import java.util.concurrent.TimeUnit; + +import io.reactivex.rxjava3.annotations.NonNull; +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; +import io.reactivex.rxjava3.schedulers.Timed; + +/** + * Measures the time between subscription and the success item emission + * from the upstream and emits this as a {@link Timed} success value. + * @param <T> the element type of the sequence + * @since 3.0.0 + */ +public final class SingleTimeInterval<T> extends Single<Timed<T>> { + + final SingleSource<T> source; + + final TimeUnit unit; + + final Scheduler scheduler; + + final boolean start; + + public SingleTimeInterval(SingleSource<T> source, TimeUnit unit, Scheduler scheduler, boolean start) { + this.source = source; + this.unit = unit; + this.scheduler = scheduler; + this.start = start; + } + + @Override + protected void subscribeActual(@NonNull SingleObserver<? super @NonNull Timed<T>> observer) { + source.subscribe(new TimeIntervalSingleObserver<>(observer, unit, scheduler, start)); + } + + static final class TimeIntervalSingleObserver<T> implements SingleObserver<T>, Disposable { + + final SingleObserver<? super Timed<T>> downstream; + + final TimeUnit unit; + + final Scheduler scheduler; + + final long startTime; + + Disposable upstream; + + TimeIntervalSingleObserver(SingleObserver<? super Timed<T>> downstream, TimeUnit unit, Scheduler scheduler, boolean start) { + this.downstream = downstream; + this.unit = unit; + this.scheduler = scheduler; + this.startTime = start ? scheduler.now(unit) : 0L; + } + + @Override + public void onSubscribe(@NonNull Disposable d) { + if (DisposableHelper.validate(this.upstream, d)) { + this.upstream = d; + + downstream.onSubscribe(this); + } + } + + @Override + public void onSuccess(@NonNull T t) { + downstream.onSuccess(new Timed<>(t, scheduler.now(unit) - startTime, unit)); + } + + @Override + public void onError(@NonNull Throwable e) { + downstream.onError(e); + } + + @Override + public void dispose() { + upstream.dispose(); + } + + @Override + public boolean isDisposed() { + return upstream.isDisposed(); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleTimeout.java b/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleTimeout.java new file mode 100644 index 0000000000..cc5b923727 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleTimeout.java @@ -0,0 +1,166 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.single; + +import static io.reactivex.rxjava3.internal.util.ExceptionHelper.timeoutMessage; + +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicReference; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +public final class SingleTimeout<T> extends Single<T> { + + final SingleSource<T> source; + + final long timeout; + + final TimeUnit unit; + + final Scheduler scheduler; + + final SingleSource<? extends T> other; + + public SingleTimeout(SingleSource<T> source, long timeout, TimeUnit unit, Scheduler scheduler, + SingleSource<? extends T> other) { + this.source = source; + this.timeout = timeout; + this.unit = unit; + this.scheduler = scheduler; + this.other = other; + } + + @Override + protected void subscribeActual(final SingleObserver<? super T> observer) { + + TimeoutMainObserver<T> parent = new TimeoutMainObserver<>(observer, other, timeout, unit); + observer.onSubscribe(parent); + + DisposableHelper.replace(parent.task, scheduler.scheduleDirect(parent, timeout, unit)); + + source.subscribe(parent); + } + + static final class TimeoutMainObserver<T> extends AtomicReference<Disposable> + implements SingleObserver<T>, Runnable, Disposable { + + private static final long serialVersionUID = 37497744973048446L; + + final SingleObserver<? super T> downstream; + + final AtomicReference<Disposable> task; + + final TimeoutFallbackObserver<T> fallback; + + SingleSource<? extends T> other; + + final long timeout; + + final TimeUnit unit; + + static final class TimeoutFallbackObserver<T> extends AtomicReference<Disposable> + implements SingleObserver<T> { + + private static final long serialVersionUID = 2071387740092105509L; + final SingleObserver<? super T> downstream; + + TimeoutFallbackObserver(SingleObserver<? super T> downstream) { + this.downstream = downstream; + } + + @Override + public void onSubscribe(Disposable d) { + DisposableHelper.setOnce(this, d); + } + + @Override + public void onSuccess(T t) { + downstream.onSuccess(t); + } + + @Override + public void onError(Throwable e) { + downstream.onError(e); + } + } + + TimeoutMainObserver(SingleObserver<? super T> actual, SingleSource<? extends T> other, long timeout, TimeUnit unit) { + this.downstream = actual; + this.other = other; + this.timeout = timeout; + this.unit = unit; + this.task = new AtomicReference<>(); + if (other != null) { + this.fallback = new TimeoutFallbackObserver<>(actual); + } else { + this.fallback = null; + } + } + + @Override + public void run() { + if (DisposableHelper.dispose(this)) { + SingleSource<? extends T> other = this.other; + if (other == null) { + downstream.onError(new TimeoutException(timeoutMessage(timeout, unit))); + } else { + this.other = null; + other.subscribe(fallback); + } + } + } + + @Override + public void onSubscribe(Disposable d) { + DisposableHelper.setOnce(this, d); + } + + @Override + public void onSuccess(T t) { + Disposable d = get(); + if (d != DisposableHelper.DISPOSED && compareAndSet(d, DisposableHelper.DISPOSED)) { + DisposableHelper.dispose(task); + downstream.onSuccess(t); + } + } + + @Override + public void onError(Throwable e) { + Disposable d = get(); + if (d != DisposableHelper.DISPOSED && compareAndSet(d, DisposableHelper.DISPOSED)) { + DisposableHelper.dispose(task); + downstream.onError(e); + } else { + RxJavaPlugins.onError(e); + } + } + + @Override + public void dispose() { + DisposableHelper.dispose(this); + DisposableHelper.dispose(task); + if (fallback != null) { + DisposableHelper.dispose(fallback); + } + } + + @Override + public boolean isDisposed() { + return DisposableHelper.isDisposed(get()); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleTimer.java b/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleTimer.java new file mode 100644 index 0000000000..07748952c8 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleTimer.java @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.single; + +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; + +/** + * Signals a {@code 0L} after the specified delay. + */ +public final class SingleTimer extends Single<Long> { + + final long delay; + final TimeUnit unit; + final Scheduler scheduler; + + public SingleTimer(long delay, TimeUnit unit, Scheduler scheduler) { + this.delay = delay; + this.unit = unit; + this.scheduler = scheduler; + } + + @Override + protected void subscribeActual(final SingleObserver<? super Long> observer) { + TimerDisposable parent = new TimerDisposable(observer); + observer.onSubscribe(parent); + parent.setFuture(scheduler.scheduleDirect(parent, delay, unit)); + } + + static final class TimerDisposable extends AtomicReference<Disposable> implements Disposable, Runnable { + + private static final long serialVersionUID = 8465401857522493082L; + final SingleObserver<? super Long> downstream; + + TimerDisposable(final SingleObserver<? super Long> downstream) { + this.downstream = downstream; + } + + @Override + public void run() { + downstream.onSuccess(0L); + } + + @Override + public void dispose() { + DisposableHelper.dispose(this); + } + + @Override + public boolean isDisposed() { + return DisposableHelper.isDisposed(get()); + } + + void setFuture(Disposable d) { + DisposableHelper.replace(this, d); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleToFlowable.java b/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleToFlowable.java new file mode 100644 index 0000000000..e692dea5b9 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleToFlowable.java @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.single; + +import org.reactivestreams.Subscriber; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; +import io.reactivex.rxjava3.internal.subscriptions.DeferredScalarSubscription; + +/** + * Wraps a Single and exposes it as a Flowable. + * + * @param <T> the value type + */ +public final class SingleToFlowable<T> extends Flowable<T> { + + final SingleSource<? extends T> source; + + public SingleToFlowable(SingleSource<? extends T> source) { + this.source = source; + } + + @Override + public void subscribeActual(final Subscriber<? super T> s) { + source.subscribe(new SingleToFlowableObserver<T>(s)); + } + + static final class SingleToFlowableObserver<T> extends DeferredScalarSubscription<T> + implements SingleObserver<T> { + + private static final long serialVersionUID = 187782011903685568L; + + Disposable upstream; + + SingleToFlowableObserver(Subscriber<? super T> downstream) { + super(downstream); + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.validate(this.upstream, d)) { + this.upstream = d; + + downstream.onSubscribe(this); + } + } + + @Override + public void onSuccess(T value) { + complete(value); + } + + @Override + public void onError(Throwable e) { + downstream.onError(e); + } + + @Override + public void cancel() { + super.cancel(); + upstream.dispose(); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleToObservable.java b/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleToObservable.java new file mode 100644 index 0000000000..4586317740 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleToObservable.java @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.single; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; +import io.reactivex.rxjava3.internal.observers.DeferredScalarDisposable; + +/** + * Wraps a Single and exposes it as an Observable. + * + * @param <T> the value type + */ +public final class SingleToObservable<T> extends Observable<T> { + + final SingleSource<? extends T> source; + + public SingleToObservable(SingleSource<? extends T> source) { + this.source = source; + } + + @Override + public void subscribeActual(final Observer<? super T> observer) { + source.subscribe(create(observer)); + } + + /** + * Creates a {@link SingleObserver} wrapper around a {@link Observer}. + * <p>History: 2.0.1 - experimental + * @param <T> the value type + * @param downstream the downstream {@code Observer} to talk to + * @return the new SingleObserver instance + * @since 2.2 + */ + public static <T> SingleObserver<T> create(Observer<? super T> downstream) { + return new SingleToObservableObserver<>(downstream); + } + + static final class SingleToObservableObserver<T> + extends DeferredScalarDisposable<T> + implements SingleObserver<T> { + + private static final long serialVersionUID = 3786543492451018833L; + Disposable upstream; + + SingleToObservableObserver(Observer<? super T> downstream) { + super(downstream); + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.validate(this.upstream, d)) { + this.upstream = d; + + downstream.onSubscribe(this); + } + } + + @Override + public void onSuccess(T value) { + complete(value); + } + + @Override + public void onError(Throwable e) { + error(e); + } + + @Override + public void dispose() { + super.dispose(); + upstream.dispose(); + } + + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleUnsubscribeOn.java b/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleUnsubscribeOn.java new file mode 100644 index 0000000000..4b18599c84 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleUnsubscribeOn.java @@ -0,0 +1,95 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.single; + +import java.util.concurrent.atomic.AtomicReference; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; + +/** + * Makes sure a dispose() call from downstream happens on the specified scheduler. + * + * @param <T> the value type + */ +public final class SingleUnsubscribeOn<T> extends Single<T> { + + final SingleSource<T> source; + + final Scheduler scheduler; + + public SingleUnsubscribeOn(SingleSource<T> source, Scheduler scheduler) { + this.source = source; + this.scheduler = scheduler; + } + + @Override + protected void subscribeActual(SingleObserver<? super T> observer) { + source.subscribe(new UnsubscribeOnSingleObserver<>(observer, scheduler)); + } + + static final class UnsubscribeOnSingleObserver<T> extends AtomicReference<Disposable> + implements SingleObserver<T>, Disposable, Runnable { + + private static final long serialVersionUID = 3256698449646456986L; + + final SingleObserver<? super T> downstream; + + final Scheduler scheduler; + + Disposable ds; + + UnsubscribeOnSingleObserver(SingleObserver<? super T> actual, Scheduler scheduler) { + this.downstream = actual; + this.scheduler = scheduler; + } + + @Override + public void dispose() { + Disposable d = getAndSet(DisposableHelper.DISPOSED); + if (d != DisposableHelper.DISPOSED) { + this.ds = d; + scheduler.scheduleDirect(this); + } + } + + @Override + public void run() { + ds.dispose(); + } + + @Override + public boolean isDisposed() { + return DisposableHelper.isDisposed(get()); + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.setOnce(this, d)) { + downstream.onSubscribe(this); + } + } + + @Override + public void onSuccess(T value) { + downstream.onSuccess(value); + } + + @Override + public void onError(Throwable e) { + downstream.onError(e); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleUsing.java b/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleUsing.java new file mode 100644 index 0000000000..55ab6c31ff --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleUsing.java @@ -0,0 +1,200 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.single; + +import java.util.Objects; +import java.util.concurrent.atomic.AtomicReference; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.disposables.*; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +public final class SingleUsing<T, U> extends Single<T> { + + final Supplier<U> resourceSupplier; + final Function<? super U, ? extends SingleSource<? extends T>> singleFunction; + final Consumer<? super U> disposer; + final boolean eager; + + public SingleUsing(Supplier<U> resourceSupplier, + Function<? super U, ? extends SingleSource<? extends T>> singleFunction, + Consumer<? super U> disposer, + boolean eager) { + this.resourceSupplier = resourceSupplier; + this.singleFunction = singleFunction; + this.disposer = disposer; + this.eager = eager; + } + + @Override + protected void subscribeActual(final SingleObserver<? super T> observer) { + + final U resource; // NOPMD + + try { + resource = resourceSupplier.get(); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + EmptyDisposable.error(ex, observer); + return; + } + + SingleSource<? extends T> source; + + try { + source = Objects.requireNonNull(singleFunction.apply(resource), "The singleFunction returned a null SingleSource"); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + + if (eager) { + try { + disposer.accept(resource); + } catch (Throwable exc) { + Exceptions.throwIfFatal(exc); + ex = new CompositeException(ex, exc); + } + } + EmptyDisposable.error(ex, observer); + if (!eager) { + try { + disposer.accept(resource); + } catch (Throwable exc) { + Exceptions.throwIfFatal(exc); + RxJavaPlugins.onError(exc); + } + } + return; + } + + source.subscribe(new UsingSingleObserver<T, U>(observer, resource, eager, disposer)); + } + + static final class UsingSingleObserver<T, U> extends + AtomicReference<Object> implements SingleObserver<T>, Disposable { + + private static final long serialVersionUID = -5331524057054083935L; + + final SingleObserver<? super T> downstream; + + final Consumer<? super U> disposer; + + final boolean eager; + + Disposable upstream; + + UsingSingleObserver(SingleObserver<? super T> actual, U resource, boolean eager, + Consumer<? super U> disposer) { + super(resource); + this.downstream = actual; + this.eager = eager; + this.disposer = disposer; + } + + @Override + public void dispose() { + if (eager) { + disposeResource(); + upstream.dispose(); + upstream = DisposableHelper.DISPOSED; + } else { + upstream.dispose(); + upstream = DisposableHelper.DISPOSED; + disposeResource(); + } + } + + @Override + public boolean isDisposed() { + return upstream.isDisposed(); + } + + @Override + public void onSubscribe(Disposable d) { + if (DisposableHelper.validate(this.upstream, d)) { + this.upstream = d; + + downstream.onSubscribe(this); + } + } + + @SuppressWarnings("unchecked") + @Override + public void onSuccess(T value) { + upstream = DisposableHelper.DISPOSED; + + if (eager) { + Object u = getAndSet(this); + if (u != this) { + try { + disposer.accept((U)u); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + downstream.onError(ex); + return; + } + } else { + return; + } + } + + downstream.onSuccess(value); + + if (!eager) { + disposeResource(); + } + } + + @SuppressWarnings("unchecked") + @Override + public void onError(Throwable e) { + upstream = DisposableHelper.DISPOSED; + + if (eager) { + Object u = getAndSet(this); + if (u != this) { + try { + disposer.accept((U)u); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + e = new CompositeException(e, ex); + } + } else { + return; + } + } + + downstream.onError(e); + + if (!eager) { + disposeResource(); + } + } + + @SuppressWarnings("unchecked") + void disposeResource() { + Object u = getAndSet(this); + if (u != this) { + try { + disposer.accept((U)u); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + RxJavaPlugins.onError(ex); + } + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleZipArray.java b/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleZipArray.java new file mode 100644 index 0000000000..b53d2f2184 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleZipArray.java @@ -0,0 +1,193 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.single; + +import java.util.Objects; +import java.util.concurrent.atomic.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +public final class SingleZipArray<T, R> extends Single<R> { + + final SingleSource<? extends T>[] sources; + + final Function<? super Object[], ? extends R> zipper; + + public SingleZipArray(SingleSource<? extends T>[] sources, Function<? super Object[], ? extends R> zipper) { + this.sources = sources; + this.zipper = zipper; + } + + @Override + protected void subscribeActual(SingleObserver<? super R> observer) { + SingleSource<? extends T>[] sources = this.sources; + int n = sources.length; + + if (n == 1) { + sources[0].subscribe(new SingleMap.MapSingleObserver<>(observer, new SingletonArrayFunc())); + return; + } + + ZipCoordinator<T, R> parent = new ZipCoordinator<>(observer, n, zipper); + + observer.onSubscribe(parent); + + for (int i = 0; i < n; i++) { + if (parent.isDisposed()) { + return; + } + + SingleSource<? extends T> source = sources[i]; + + if (source == null) { + parent.innerError(new NullPointerException("One of the sources is null"), i); + return; + } + + source.subscribe(parent.observers[i]); + } + } + + static final class ZipCoordinator<T, R> extends AtomicInteger implements Disposable { + + private static final long serialVersionUID = -5556924161382950569L; + + final SingleObserver<? super R> downstream; + + final Function<? super Object[], ? extends R> zipper; + + final ZipSingleObserver<T>[] observers; + + Object[] values; + + @SuppressWarnings("unchecked") + ZipCoordinator(SingleObserver<? super R> observer, int n, Function<? super Object[], ? extends R> zipper) { + super(n); + this.downstream = observer; + this.zipper = zipper; + ZipSingleObserver<T>[] o = new ZipSingleObserver[n]; + for (int i = 0; i < n; i++) { + o[i] = new ZipSingleObserver<>(this, i); + } + this.observers = o; + this.values = new Object[n]; + } + + @Override + public boolean isDisposed() { + return get() <= 0; + } + + @Override + public void dispose() { + if (getAndSet(0) > 0) { + for (ZipSingleObserver<?> d : observers) { + d.dispose(); + } + + values = null; + } + } + + void innerSuccess(T value, int index) { + Object[] values = this.values; + if (values != null) { + values[index] = value; + } + if (decrementAndGet() == 0) { + R v; + + try { + v = Objects.requireNonNull(zipper.apply(values), "The zipper returned a null value"); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + this.values = null; + downstream.onError(ex); + return; + } + + this.values = null; + downstream.onSuccess(v); + } + } + + void disposeExcept(int index) { + ZipSingleObserver<T>[] observers = this.observers; + int n = observers.length; + for (int i = 0; i < index; i++) { + observers[i].dispose(); + } + for (int i = index + 1; i < n; i++) { + observers[i].dispose(); + } + } + + void innerError(Throwable ex, int index) { + if (getAndSet(0) > 0) { + disposeExcept(index); + values = null; + downstream.onError(ex); + } else { + RxJavaPlugins.onError(ex); + } + } + } + + static final class ZipSingleObserver<T> + extends AtomicReference<Disposable> + implements SingleObserver<T> { + + private static final long serialVersionUID = 3323743579927613702L; + + final ZipCoordinator<T, ?> parent; + + final int index; + + ZipSingleObserver(ZipCoordinator<T, ?> parent, int index) { + this.parent = parent; + this.index = index; + } + + public void dispose() { + DisposableHelper.dispose(this); + } + + @Override + public void onSubscribe(Disposable d) { + DisposableHelper.setOnce(this, d); + } + + @Override + public void onSuccess(T value) { + parent.innerSuccess(value, index); + } + + @Override + public void onError(Throwable e) { + parent.innerError(e, index); + } + } + + final class SingletonArrayFunc implements Function<T, R> { + @Override + public R apply(T t) throws Throwable { + return Objects.requireNonNull(zipper.apply(new Object[] { t }), "The zipper returned a null value"); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleZipIterable.java b/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleZipIterable.java new file mode 100644 index 0000000000..935550ab47 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/operators/single/SingleZipIterable.java @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.single; + +import java.util.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.disposables.EmptyDisposable; +import io.reactivex.rxjava3.internal.operators.single.SingleZipArray.ZipCoordinator; + +public final class SingleZipIterable<T, R> extends Single<R> { + + final Iterable<? extends SingleSource<? extends T>> sources; + + final Function<? super Object[], ? extends R> zipper; + + public SingleZipIterable(Iterable<? extends SingleSource<? extends T>> sources, Function<? super Object[], ? extends R> zipper) { + this.sources = sources; + this.zipper = zipper; + } + + @Override + protected void subscribeActual(SingleObserver<? super R> observer) { + @SuppressWarnings("unchecked") + SingleSource<? extends T>[] a = new SingleSource[8]; + int n = 0; + + try { + for (SingleSource<? extends T> source : sources) { + if (source == null) { + EmptyDisposable.error(new NullPointerException("One of the sources is null"), observer); + return; + } + if (n == a.length) { + a = Arrays.copyOf(a, n + (n >> 2)); + } + a[n++] = source; + } + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + EmptyDisposable.error(ex, observer); + return; + } + + if (n == 0) { + EmptyDisposable.error(new NoSuchElementException(), observer); + return; + } + + if (n == 1) { + a[0].subscribe(new SingleMap.MapSingleObserver<>(observer, new SingletonArrayFunc())); + return; + } + + ZipCoordinator<T, R> parent = new ZipCoordinator<>(observer, n, zipper); + + observer.onSubscribe(parent); + + for (int i = 0; i < n; i++) { + if (parent.isDisposed()) { + return; + } + + a[i].subscribe(parent.observers[i]); + } + } + + final class SingletonArrayFunc implements Function<T, R> { + @Override + public R apply(T t) throws Throwable { + return Objects.requireNonNull(zipper.apply(new Object[] { t }), "The zipper returned a null value"); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/queue/MpscLinkedQueue.java b/src/main/java/io/reactivex/rxjava3/internal/queue/MpscLinkedQueue.java new file mode 100644 index 0000000000..e8d19c633e --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/queue/MpscLinkedQueue.java @@ -0,0 +1,193 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +/* + * The code was inspired by the similarly named JCTools class: + * https://github.com/JCTools/JCTools/blob/master/jctools-core/src/main/java/org/jctools/queues/atomic + */ + +package io.reactivex.rxjava3.internal.queue; + +import java.util.concurrent.atomic.AtomicReference; + +import io.reactivex.rxjava3.annotations.Nullable; +import io.reactivex.rxjava3.operators.SimplePlainQueue; + +/** + * A multi-producer single consumer unbounded queue. + * @param <T> the contained value type + */ +public final class MpscLinkedQueue<T> implements SimplePlainQueue<T> { + private final AtomicReference<LinkedQueueNode<T>> producerNode; + private final AtomicReference<LinkedQueueNode<T>> consumerNode; + + public MpscLinkedQueue() { + producerNode = new AtomicReference<>(); + consumerNode = new AtomicReference<>(); + LinkedQueueNode<T> node = new LinkedQueueNode<>(); + spConsumerNode(node); + xchgProducerNode(node); // this ensures correct construction: StoreLoad + } + + /** + * {@inheritDoc} <br> + * <p> + * IMPLEMENTATION NOTES:<br> + * Offer is allowed from multiple threads.<br> + * Offer allocates a new node and: + * <ol> + * <li>Swaps it atomically with current producer node (only one producer 'wins') + * <li>Sets the new node as the node following from the swapped producer node + * </ol> + * This works because each producer is guaranteed to 'plant' a new node and link the old node. No 2 producers can + * get the same producer node as part of XCHG guarantee. + * + * @see java.util.Queue#offer(java.lang.Object) + */ + @Override + public boolean offer(final T e) { + if (null == e) { + throw new NullPointerException("Null is not a valid element"); + } + final LinkedQueueNode<T> nextNode = new LinkedQueueNode<>(e); + final LinkedQueueNode<T> prevProducerNode = xchgProducerNode(nextNode); + // Should a producer thread get interrupted here the chain WILL be broken until that thread is resumed + // and completes the store in prev.next. + prevProducerNode.soNext(nextNode); // StoreStore + return true; + } + + /** + * {@inheritDoc} <br> + * <p> + * IMPLEMENTATION NOTES:<br> + * Poll is allowed from a SINGLE thread.<br> + * Poll reads the next node from the consumerNode and: + * <ol> + * <li>If it is null, the queue is assumed empty (though it might not be). + * <li>If it is not null set it as the consumer node and return it's now evacuated value. + * </ol> + * This means the consumerNode.value is always null, which is also the starting point for the queue. Because null + * values are not allowed to be offered this is the only node with it's value set to null at any one time. + * + * @see java.util.Queue#poll() + */ + @Nullable + @Override + public T poll() { + LinkedQueueNode<T> currConsumerNode = lpConsumerNode(); // don't load twice, it's alright + LinkedQueueNode<T> nextNode = currConsumerNode.lvNext(); + if (nextNode != null) { + // we have to null out the value because we are going to hang on to the node + final T nextValue = nextNode.getAndNullValue(); + spConsumerNode(nextNode); + // unlink previous consumer to help gc + currConsumerNode.soNext(null); + return nextValue; + } + else if (currConsumerNode != lvProducerNode()) { + // spin, we are no longer wait free + while ((nextNode = currConsumerNode.lvNext()) == null) { } // NOPMD + // got the next node... + + // we have to null out the value because we are going to hang on to the node + final T nextValue = nextNode.getAndNullValue(); + spConsumerNode(nextNode); + // unlink previous consumer to help gc + currConsumerNode.soNext(null); + return nextValue; + } + return null; + } + + @Override + public boolean offer(T v1, T v2) { + offer(v1); + offer(v2); + return true; + } + + @Override + public void clear() { + while (poll() != null && !isEmpty()) { } // NOPMD + } + LinkedQueueNode<T> lvProducerNode() { + return producerNode.get(); + } + LinkedQueueNode<T> xchgProducerNode(LinkedQueueNode<T> node) { + return producerNode.getAndSet(node); + } + LinkedQueueNode<T> lvConsumerNode() { + return consumerNode.get(); + } + + LinkedQueueNode<T> lpConsumerNode() { + return consumerNode.get(); + } + void spConsumerNode(LinkedQueueNode<T> node) { + consumerNode.lazySet(node); + } + + /** + * {@inheritDoc} <br> + * <p> + * IMPLEMENTATION NOTES:<br> + * Queue is empty when producerNode is the same as consumerNode. An alternative implementation would be to observe + * the producerNode.value is null, which also means an empty queue because only the consumerNode.value is allowed to + * be null. + */ + @Override + public boolean isEmpty() { + return lvConsumerNode() == lvProducerNode(); + } + + static final class LinkedQueueNode<E> extends AtomicReference<LinkedQueueNode<E>> { + + private static final long serialVersionUID = 2404266111789071508L; + + private E value; + + LinkedQueueNode() { + } + + LinkedQueueNode(E val) { + spValue(val); + } + /** + * Gets the current value and nulls out the reference to it from this node. + * + * @return value + */ + public E getAndNullValue() { + E temp = lpValue(); + spValue(null); + return temp; + } + + public E lpValue() { + return value; + } + + public void spValue(E newValue) { + value = newValue; + } + + public void soNext(LinkedQueueNode<E> n) { + lazySet(n); + } + + public LinkedQueueNode<E> lvNext() { + return get(); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/schedulers/AbstractDirectTask.java b/src/main/java/io/reactivex/rxjava3/internal/schedulers/AbstractDirectTask.java new file mode 100644 index 0000000000..556fcd23c7 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/schedulers/AbstractDirectTask.java @@ -0,0 +1,114 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.schedulers; + +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicReference; + +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.schedulers.SchedulerRunnableIntrospection; + +/** + * Base functionality for direct tasks that manage a runnable and cancellation/completion. + * @since 2.0.8 + */ +abstract class AbstractDirectTask +extends AtomicReference<Future<?>> +implements Disposable, SchedulerRunnableIntrospection { + + private static final long serialVersionUID = 1811839108042568751L; + + protected final Runnable runnable; + + protected final boolean interruptOnCancel; + + protected Thread runner; + + protected static final FutureTask<Void> FINISHED = new FutureTask<>(Functions.EMPTY_RUNNABLE, null); + + protected static final FutureTask<Void> DISPOSED = new FutureTask<>(Functions.EMPTY_RUNNABLE, null); + + AbstractDirectTask(Runnable runnable, boolean interruptOnCancel) { + this.runnable = runnable; + this.interruptOnCancel = interruptOnCancel; + } + + @Override + public final void dispose() { + Future<?> f = get(); + if (f != FINISHED && f != DISPOSED) { + if (compareAndSet(f, DISPOSED)) { + if (f != null) { + cancelFuture(f); + } + } + } + } + + @Override + public final boolean isDisposed() { + Future<?> f = get(); + return f == FINISHED || f == DISPOSED; + } + + public final void setFuture(Future<?> future) { + for (;;) { + Future<?> f = get(); + if (f == FINISHED) { + break; + } + if (f == DISPOSED) { + cancelFuture(future); + break; + } + if (compareAndSet(f, future)) { + break; + } + } + } + + private void cancelFuture(Future<?> future) { + if (runner == Thread.currentThread()) { + future.cancel(false); + } else { + future.cancel(interruptOnCancel); + } + } + + @Override + public Runnable getWrappedRunnable() { + return runnable; + } + + @Override + public String toString() { + String status; + Future<?> f = get(); + if (f == FINISHED) { + status = "Finished"; + } else if (f == DISPOSED) { + status = "Disposed"; + } else { + Thread r = runner; + if (r != null) { + status = "Running on " + runner; + } else { + status = "Waiting"; + } + } + + return getClass().getSimpleName() + "[" + status + "]"; + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/schedulers/ComputationScheduler.java b/src/main/java/io/reactivex/rxjava3/internal/schedulers/ComputationScheduler.java new file mode 100644 index 0000000000..f6845e660e --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/schedulers/ComputationScheduler.java @@ -0,0 +1,238 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.schedulers; + +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicReference; + +import io.reactivex.rxjava3.annotations.NonNull; +import io.reactivex.rxjava3.core.Scheduler; +import io.reactivex.rxjava3.disposables.*; +import io.reactivex.rxjava3.internal.disposables.*; +import io.reactivex.rxjava3.internal.functions.ObjectHelper; + +/** + * Holds a fixed pool of worker threads and assigns them + * to requested Scheduler.Workers in a round-robin fashion. + */ +public final class ComputationScheduler extends Scheduler implements SchedulerMultiWorkerSupport { + /** This will indicate no pool is active. */ + static final FixedSchedulerPool NONE; + /** Manages a fixed number of workers. */ + private static final String THREAD_NAME_PREFIX = "RxComputationThreadPool"; + static final RxThreadFactory THREAD_FACTORY; + /** + * Key to setting the maximum number of computation scheduler threads. + * Zero or less is interpreted as use available. Capped by available. + */ + static final String KEY_MAX_THREADS = "rx3.computation-threads"; + /** The maximum number of computation scheduler threads. */ + static final int MAX_THREADS; + + static final PoolWorker SHUTDOWN_WORKER; + + final ThreadFactory threadFactory; + final AtomicReference<FixedSchedulerPool> pool; + /** The name of the system property for setting the thread priority for this Scheduler. */ + private static final String KEY_COMPUTATION_PRIORITY = "rx3.computation-priority"; + + static { + MAX_THREADS = cap(Runtime.getRuntime().availableProcessors(), Integer.getInteger(KEY_MAX_THREADS, 0)); + + SHUTDOWN_WORKER = new PoolWorker(new RxThreadFactory("RxComputationShutdown")); + SHUTDOWN_WORKER.dispose(); + + int priority = Math.max(Thread.MIN_PRIORITY, Math.min(Thread.MAX_PRIORITY, + Integer.getInteger(KEY_COMPUTATION_PRIORITY, Thread.NORM_PRIORITY))); + + THREAD_FACTORY = new RxThreadFactory(THREAD_NAME_PREFIX, priority, true); + + NONE = new FixedSchedulerPool(0, THREAD_FACTORY); + NONE.shutdown(); + } + + static int cap(int cpuCount, int paramThreads) { + return paramThreads <= 0 || paramThreads > cpuCount ? cpuCount : paramThreads; + } + + static final class FixedSchedulerPool implements SchedulerMultiWorkerSupport { + final int cores; + + final PoolWorker[] eventLoops; + long n; + + FixedSchedulerPool(int maxThreads, ThreadFactory threadFactory) { + // initialize event loops + this.cores = maxThreads; + this.eventLoops = new PoolWorker[maxThreads]; + for (int i = 0; i < maxThreads; i++) { + this.eventLoops[i] = new PoolWorker(threadFactory); + } + } + + public PoolWorker getEventLoop() { + int c = cores; + if (c == 0) { + return SHUTDOWN_WORKER; + } + // simple round robin, improvements to come + return eventLoops[(int)(n++ % c)]; + } + + public void shutdown() { + for (PoolWorker w : eventLoops) { + w.dispose(); + } + } + + @Override + public void createWorkers(int number, WorkerCallback callback) { + int c = cores; + if (c == 0) { + for (int i = 0; i < number; i++) { + callback.onWorker(i, SHUTDOWN_WORKER); + } + } else { + int index = (int)n % c; + for (int i = 0; i < number; i++) { + callback.onWorker(i, new EventLoopWorker(eventLoops[index])); + if (++index == c) { + index = 0; + } + } + n = index; + } + } + } + + /** + * Create a scheduler with pool size equal to the available processor + * count and using least-recent worker selection policy. + */ + public ComputationScheduler() { + this(THREAD_FACTORY); + } + + /** + * Create a scheduler with pool size equal to the available processor + * count and using least-recent worker selection policy. + * + * @param threadFactory thread factory to use for creating worker threads. Note that this takes precedence over any + * system properties for configuring new thread creation. Cannot be null. + */ + public ComputationScheduler(ThreadFactory threadFactory) { + this.threadFactory = threadFactory; + this.pool = new AtomicReference<>(NONE); + start(); + } + + @NonNull + @Override + public Worker createWorker() { + return new EventLoopWorker(pool.get().getEventLoop()); + } + + @Override + public void createWorkers(int number, WorkerCallback callback) { + ObjectHelper.verifyPositive(number, "number > 0 required"); + pool.get().createWorkers(number, callback); + } + + @NonNull + @Override + public Disposable scheduleDirect(@NonNull Runnable run, long delay, TimeUnit unit) { + PoolWorker w = pool.get().getEventLoop(); + return w.scheduleDirect(run, delay, unit); + } + + @NonNull + @Override + public Disposable schedulePeriodicallyDirect(@NonNull Runnable run, long initialDelay, long period, TimeUnit unit) { + PoolWorker w = pool.get().getEventLoop(); + return w.schedulePeriodicallyDirect(run, initialDelay, period, unit); + } + + @Override + public void start() { + FixedSchedulerPool update = new FixedSchedulerPool(MAX_THREADS, threadFactory); + if (!pool.compareAndSet(NONE, update)) { + update.shutdown(); + } + } + + @Override + public void shutdown() { + FixedSchedulerPool curr = pool.getAndSet(NONE); + if (curr != NONE) { + curr.shutdown(); + } + } + + static final class EventLoopWorker extends Scheduler.Worker { + private final ListCompositeDisposable serial; + private final CompositeDisposable timed; + private final ListCompositeDisposable both; + private final PoolWorker poolWorker; + + volatile boolean disposed; + + EventLoopWorker(PoolWorker poolWorker) { + this.poolWorker = poolWorker; + this.serial = new ListCompositeDisposable(); + this.timed = new CompositeDisposable(); + this.both = new ListCompositeDisposable(); + this.both.add(serial); + this.both.add(timed); + } + + @Override + public void dispose() { + if (!disposed) { + disposed = true; + both.dispose(); + } + } + + @Override + public boolean isDisposed() { + return disposed; + } + + @NonNull + @Override + public Disposable schedule(@NonNull Runnable action) { + if (disposed) { + return EmptyDisposable.INSTANCE; + } + + return poolWorker.scheduleActual(action, 0, TimeUnit.MILLISECONDS, serial); + } + + @NonNull + @Override + public Disposable schedule(@NonNull Runnable action, long delayTime, @NonNull TimeUnit unit) { + if (disposed) { + return EmptyDisposable.INSTANCE; + } + + return poolWorker.scheduleActual(action, delayTime, unit, timed); + } + } + + static final class PoolWorker extends NewThreadWorker { + PoolWorker(ThreadFactory threadFactory) { + super(threadFactory); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/schedulers/DisposeOnCancel.java b/src/main/java/io/reactivex/rxjava3/internal/schedulers/DisposeOnCancel.java new file mode 100644 index 0000000000..72ac8ef525 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/schedulers/DisposeOnCancel.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.schedulers; + +import java.util.concurrent.*; + +import io.reactivex.rxjava3.annotations.NonNull; +import io.reactivex.rxjava3.disposables.Disposable; + +/** + * Implements the Future interface and calls dispose() on cancel() but + * the other methods are not implemented. + */ +final class DisposeOnCancel implements Future<Object> { + + final Disposable upstream; + + DisposeOnCancel(Disposable d) { + this.upstream = d; + } + + @Override + public boolean cancel(boolean mayInterruptIfRunning) { + upstream.dispose(); + return false; + } + + @Override + public boolean isCancelled() { + return false; + } + + @Override + public boolean isDone() { + return false; + } + + @Override + public Object get() { + return null; + } + + @Override + public Object get(long timeout, @NonNull TimeUnit unit) { + return null; + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/schedulers/ExecutorScheduler.java b/src/main/java/io/reactivex/rxjava3/internal/schedulers/ExecutorScheduler.java new file mode 100644 index 0000000000..fa0bcab7f8 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/schedulers/ExecutorScheduler.java @@ -0,0 +1,525 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.schedulers; + +import java.util.concurrent.*; +import java.util.concurrent.atomic.*; + +import io.reactivex.rxjava3.annotations.NonNull; +import io.reactivex.rxjava3.core.Scheduler; +import io.reactivex.rxjava3.disposables.*; +import io.reactivex.rxjava3.internal.disposables.*; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.internal.queue.MpscLinkedQueue; +import io.reactivex.rxjava3.internal.schedulers.ExecutorScheduler.ExecutorWorker.*; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.schedulers.*; + +/** + * Wraps an Executor and provides the Scheduler API over it. + */ +public final class ExecutorScheduler extends Scheduler { + + final boolean interruptibleWorker; + + final boolean fair; + + @NonNull + final Executor executor; + + static final class SingleHolder { + static final Scheduler HELPER = Schedulers.single(); + } + + public ExecutorScheduler(@NonNull Executor executor, boolean interruptibleWorker, boolean fair) { + this.executor = executor; + this.interruptibleWorker = interruptibleWorker; + this.fair = fair; + } + + @NonNull + @Override + public Worker createWorker() { + return new ExecutorWorker(executor, interruptibleWorker, fair); + } + + @NonNull + @Override + public Disposable scheduleDirect(@NonNull Runnable run) { + Runnable decoratedRun = RxJavaPlugins.onSchedule(run); + try { + if (executor instanceof ExecutorService) { + ScheduledDirectTask task = new ScheduledDirectTask(decoratedRun, interruptibleWorker); + Future<?> f = ((ExecutorService)executor).submit(task); + task.setFuture(f); + return task; + } + + if (interruptibleWorker) { + InterruptibleRunnable interruptibleTask = new InterruptibleRunnable(decoratedRun, null); + executor.execute(interruptibleTask); + return interruptibleTask; + } else { + BooleanRunnable br = new BooleanRunnable(decoratedRun); + executor.execute(br); + return br; + } + } catch (RejectedExecutionException ex) { + RxJavaPlugins.onError(ex); + return EmptyDisposable.INSTANCE; + } + } + + @NonNull + @Override + public Disposable scheduleDirect(@NonNull Runnable run, final long delay, final TimeUnit unit) { + final Runnable decoratedRun = RxJavaPlugins.onSchedule(run); + if (executor instanceof ScheduledExecutorService) { + try { + ScheduledDirectTask task = new ScheduledDirectTask(decoratedRun, interruptibleWorker); + Future<?> f = ((ScheduledExecutorService)executor).schedule(task, delay, unit); + task.setFuture(f); + return task; + } catch (RejectedExecutionException ex) { + RxJavaPlugins.onError(ex); + return EmptyDisposable.INSTANCE; + } + } + + final DelayedRunnable dr = new DelayedRunnable(decoratedRun); + + Disposable delayed = SingleHolder.HELPER.scheduleDirect(new DelayedDispose(dr), delay, unit); + + dr.timed.replace(delayed); + + return dr; + } + + @NonNull + @Override + public Disposable schedulePeriodicallyDirect(@NonNull Runnable run, long initialDelay, long period, TimeUnit unit) { + if (executor instanceof ScheduledExecutorService) { + Runnable decoratedRun = RxJavaPlugins.onSchedule(run); + try { + ScheduledDirectPeriodicTask task = new ScheduledDirectPeriodicTask(decoratedRun, interruptibleWorker); + Future<?> f = ((ScheduledExecutorService)executor).scheduleAtFixedRate(task, initialDelay, period, unit); + task.setFuture(f); + return task; + } catch (RejectedExecutionException ex) { + RxJavaPlugins.onError(ex); + return EmptyDisposable.INSTANCE; + } + } + return super.schedulePeriodicallyDirect(run, initialDelay, period, unit); + } + /* public: test support. */ + public static final class ExecutorWorker extends Scheduler.Worker implements Runnable { + + final boolean interruptibleWorker; + + final boolean fair; + + final Executor executor; + + final MpscLinkedQueue<Runnable> queue; + + volatile boolean disposed; + + final AtomicInteger wip = new AtomicInteger(); + + final CompositeDisposable tasks = new CompositeDisposable(); + + public ExecutorWorker(Executor executor, boolean interruptibleWorker, boolean fair) { + this.executor = executor; + this.queue = new MpscLinkedQueue<>(); + this.interruptibleWorker = interruptibleWorker; + this.fair = fair; + } + + @NonNull + @Override + public Disposable schedule(@NonNull Runnable run) { + if (disposed) { + return EmptyDisposable.INSTANCE; + } + + Runnable decoratedRun = RxJavaPlugins.onSchedule(run); + + Runnable task; + Disposable disposable; + + if (interruptibleWorker) { + InterruptibleRunnable interruptibleTask = new InterruptibleRunnable(decoratedRun, tasks); + tasks.add(interruptibleTask); + + task = interruptibleTask; + disposable = interruptibleTask; + } else { + BooleanRunnable runnableTask = new BooleanRunnable(decoratedRun); + + task = runnableTask; + disposable = runnableTask; + } + + queue.offer(task); + + if (wip.getAndIncrement() == 0) { + try { + executor.execute(this); + } catch (RejectedExecutionException ex) { + disposed = true; + queue.clear(); + RxJavaPlugins.onError(ex); + return EmptyDisposable.INSTANCE; + } + } + + return disposable; + } + + @NonNull + @Override + public Disposable schedule(@NonNull Runnable run, long delay, @NonNull TimeUnit unit) { + if (delay <= 0) { + return schedule(run); + } + if (disposed) { + return EmptyDisposable.INSTANCE; + } + + SequentialDisposable first = new SequentialDisposable(); + + final SequentialDisposable mar = new SequentialDisposable(first); + + final Runnable decoratedRun = RxJavaPlugins.onSchedule(run); + + ScheduledRunnable sr = new ScheduledRunnable(new SequentialDispose(mar, decoratedRun), tasks, interruptibleWorker); + tasks.add(sr); + + if (executor instanceof ScheduledExecutorService) { + try { + Future<?> f = ((ScheduledExecutorService)executor).schedule((Callable<Object>)sr, delay, unit); + sr.setFuture(f); + } catch (RejectedExecutionException ex) { + disposed = true; + RxJavaPlugins.onError(ex); + return EmptyDisposable.INSTANCE; + } + } else { + final Disposable d = SingleHolder.HELPER.scheduleDirect(sr, delay, unit); + sr.setFuture(new DisposeOnCancel(d)); + } + + first.replace(sr); + + return mar; + } + + @Override + public void dispose() { + if (!disposed) { + disposed = true; + tasks.dispose(); + if (wip.getAndIncrement() == 0) { + queue.clear(); + } + } + } + + @Override + public boolean isDisposed() { + return disposed; + } + + @Override + public void run() { + if (fair) { + runFair(); + } else { + runEager(); + } + } + + void runFair() { + final MpscLinkedQueue<Runnable> q = queue; + if (disposed) { + q.clear(); + return; + } + + Runnable run = q.poll(); + run.run(); // never null because of offer + increment happens first + + if (disposed) { + q.clear(); + return; + } + + if (wip.decrementAndGet() != 0) { + executor.execute(this); + } + } + + void runEager() { + int missed = 1; + final MpscLinkedQueue<Runnable> q = queue; + for (;;) { + + if (disposed) { + q.clear(); + return; + } + + for (;;) { + Runnable run = q.poll(); + if (run == null) { + break; + } + run.run(); + + if (disposed) { + q.clear(); + return; + } + } + + if (disposed) { + q.clear(); + return; + } + + missed = wip.addAndGet(-missed); + if (missed == 0) { + break; + } + } + } + + static final class BooleanRunnable extends AtomicBoolean implements Runnable, Disposable { + + private static final long serialVersionUID = -2421395018820541164L; + + final Runnable actual; + BooleanRunnable(Runnable actual) { + this.actual = actual; + } + + @Override + public void run() { + if (get()) { + return; + } + try { + actual.run(); + } catch (Throwable ex) { + // Exceptions.throwIfFatal(ex); nowhere to go + RxJavaPlugins.onError(ex); + throw ex; + } finally { + lazySet(true); + } + } + + @Override + public void dispose() { + lazySet(true); + } + + @Override + public boolean isDisposed() { + return get(); + } + } + + final class SequentialDispose implements Runnable { + private final SequentialDisposable mar; + private final Runnable decoratedRun; + + SequentialDispose(SequentialDisposable mar, Runnable decoratedRun) { + this.mar = mar; + this.decoratedRun = decoratedRun; + } + + @Override + public void run() { + mar.replace(schedule(decoratedRun)); + } + } + + /** + * Wrapper for a {@link Runnable} with additional logic for handling interruption on + * a shared thread, similar to how Java Executors do it. + */ + static final class InterruptibleRunnable extends AtomicInteger implements Runnable, Disposable { + + private static final long serialVersionUID = -3603436687413320876L; + + final Runnable run; + + final DisposableContainer tasks; + + volatile Thread thread; + + static final int READY = 0; + + static final int RUNNING = 1; + + static final int FINISHED = 2; + + static final int INTERRUPTING = 3; + + static final int INTERRUPTED = 4; + + InterruptibleRunnable(Runnable run, DisposableContainer tasks) { + this.run = run; + this.tasks = tasks; + } + + @Override + public void run() { + if (get() == READY) { + thread = Thread.currentThread(); + if (compareAndSet(READY, RUNNING)) { + try { + try { + run.run(); + } catch (Throwable ex) { + // Exceptions.throwIfFatal(ex); nowhere to go + RxJavaPlugins.onError(ex); + throw ex; + } + } finally { + thread = null; + if (compareAndSet(RUNNING, FINISHED)) { + cleanup(); + } else { + while (get() == INTERRUPTING) { + Thread.yield(); + } + Thread.interrupted(); + } + } + } else { + thread = null; + } + } + } + + @Override + public void dispose() { + for (;;) { + int state = get(); + if (state >= FINISHED) { + break; + } else if (state == READY) { + if (compareAndSet(READY, INTERRUPTED)) { + cleanup(); + break; + } + } else { + if (compareAndSet(RUNNING, INTERRUPTING)) { + Thread t = thread; + if (t != null) { + t.interrupt(); + thread = null; + } + set(INTERRUPTED); + cleanup(); + break; + } + } + } + } + + void cleanup() { + if (tasks != null) { + tasks.delete(this); + } + } + + @Override + public boolean isDisposed() { + return get() >= FINISHED; + } + } + } + + static final class DelayedRunnable extends AtomicReference<Runnable> + implements Runnable, Disposable, SchedulerRunnableIntrospection { + + private static final long serialVersionUID = -4101336210206799084L; + + final SequentialDisposable timed; + + final SequentialDisposable direct; + + DelayedRunnable(Runnable run) { + super(run); + this.timed = new SequentialDisposable(); + this.direct = new SequentialDisposable(); + } + + @Override + public void run() { + Runnable r = get(); + if (r != null) { + try { + try { + r.run(); + } finally { + lazySet(null); + timed.lazySet(DisposableHelper.DISPOSED); + direct.lazySet(DisposableHelper.DISPOSED); + } + } catch (Throwable ex) { + // Exceptions.throwIfFatal(ex); nowhere to go + RxJavaPlugins.onError(ex); + throw ex; + } + } + } + + @Override + public boolean isDisposed() { + return get() == null; + } + + @Override + public void dispose() { + if (getAndSet(null) != null) { + timed.dispose(); + direct.dispose(); + } + } + + @Override + public Runnable getWrappedRunnable() { + Runnable r = get(); + return r != null ? r : Functions.EMPTY_RUNNABLE; + } + } + + final class DelayedDispose implements Runnable { + private final DelayedRunnable dr; + + DelayedDispose(DelayedRunnable dr) { + this.dr = dr; + } + + @Override + public void run() { + dr.direct.replace(scheduleDirect(dr)); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/schedulers/ImmediateThinScheduler.java b/src/main/java/io/reactivex/rxjava3/internal/schedulers/ImmediateThinScheduler.java new file mode 100644 index 0000000000..e2430e74cb --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/schedulers/ImmediateThinScheduler.java @@ -0,0 +1,105 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.schedulers; + +import java.util.concurrent.TimeUnit; + +import io.reactivex.rxjava3.annotations.NonNull; +import io.reactivex.rxjava3.core.Scheduler; +import io.reactivex.rxjava3.disposables.*; + +/** + * A Scheduler partially implementing the API by allowing only non-delayed, non-periodic + * task execution on the current thread immediately. + * <p> + * Note that this doesn't support recursive scheduling and disposing the returned Disposable + * has no effect (because when the schedule() method returns, the task has been already run). + */ +public final class ImmediateThinScheduler extends Scheduler { + + /** + * The singleton instance of the immediate (thin) scheduler. + */ + public static final Scheduler INSTANCE = new ImmediateThinScheduler(); + + static final Worker WORKER = new ImmediateThinWorker(); + + static final Disposable DISPOSED; + + static { + DISPOSED = Disposable.empty(); + DISPOSED.dispose(); + } + + private ImmediateThinScheduler() { + // singleton class + } + + @NonNull + @Override + public Disposable scheduleDirect(@NonNull Runnable run) { + run.run(); + return DISPOSED; + } + + @NonNull + @Override + public Disposable scheduleDirect(@NonNull Runnable run, long delay, TimeUnit unit) { + throw new UnsupportedOperationException("This scheduler doesn't support delayed execution"); + } + + @NonNull + @Override + public Disposable schedulePeriodicallyDirect(@NonNull Runnable run, long initialDelay, long period, TimeUnit unit) { + throw new UnsupportedOperationException("This scheduler doesn't support periodic execution"); + } + + @NonNull + @Override + public Worker createWorker() { + return WORKER; + } + + static final class ImmediateThinWorker extends Worker { + + @Override + public void dispose() { + // This worker is always stateless and won't track tasks + } + + @Override + public boolean isDisposed() { + return false; // dispose() has no effect + } + + @NonNull + @Override + public Disposable schedule(@NonNull Runnable run) { + run.run(); + return DISPOSED; + } + + @NonNull + @Override + public Disposable schedule(@NonNull Runnable run, long delay, @NonNull TimeUnit unit) { + throw new UnsupportedOperationException("This scheduler doesn't support delayed execution"); + } + + @NonNull + @Override + public Disposable schedulePeriodically(@NonNull Runnable run, long initialDelay, long period, TimeUnit unit) { + throw new UnsupportedOperationException("This scheduler doesn't support periodic execution"); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/schedulers/InstantPeriodicTask.java b/src/main/java/io/reactivex/rxjava3/internal/schedulers/InstantPeriodicTask.java new file mode 100644 index 0000000000..1fd37f5d93 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/schedulers/InstantPeriodicTask.java @@ -0,0 +1,106 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.schedulers; + +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicReference; + +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +/** + * Wrapper for a regular task that gets immediately rescheduled when the task completed. + */ +final class InstantPeriodicTask implements Callable<Void>, Disposable { + + final Runnable task; + + final AtomicReference<Future<?>> rest; + + final AtomicReference<Future<?>> first; + + final ExecutorService executor; + + Thread runner; + + static final FutureTask<Void> CANCELLED = new FutureTask<>(Functions.EMPTY_RUNNABLE, null); + + InstantPeriodicTask(Runnable task, ExecutorService executor) { + super(); + this.task = task; + this.first = new AtomicReference<>(); + this.rest = new AtomicReference<>(); + this.executor = executor; + } + + @Override + public Void call() { + runner = Thread.currentThread(); + try { + task.run(); + runner = null; + setRest(executor.submit(this)); + } catch (Throwable ex) { + // Exceptions.throwIfFatal(ex); nowhere to go + runner = null; + RxJavaPlugins.onError(ex); + throw ex; + } + return null; + } + + @Override + public void dispose() { + Future<?> current = first.getAndSet(CANCELLED); + if (current != null && current != CANCELLED) { + current.cancel(runner != Thread.currentThread()); + } + current = rest.getAndSet(CANCELLED); + if (current != null && current != CANCELLED) { + current.cancel(runner != Thread.currentThread()); + } + } + + @Override + public boolean isDisposed() { + return first.get() == CANCELLED; + } + + void setFirst(Future<?> f) { + for (;;) { + Future<?> current = first.get(); + if (current == CANCELLED) { + f.cancel(runner != Thread.currentThread()); + return; + } + if (first.compareAndSet(current, f)) { + return; + } + } + } + + void setRest(Future<?> f) { + for (;;) { + Future<?> current = rest.get(); + if (current == CANCELLED) { + f.cancel(runner != Thread.currentThread()); + return; + } + if (rest.compareAndSet(current, f)) { + return; + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/schedulers/IoScheduler.java b/src/main/java/io/reactivex/rxjava3/internal/schedulers/IoScheduler.java new file mode 100644 index 0000000000..cfe9520685 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/schedulers/IoScheduler.java @@ -0,0 +1,266 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.schedulers; + +import java.util.concurrent.*; +import java.util.concurrent.atomic.*; + +import io.reactivex.rxjava3.annotations.NonNull; +import io.reactivex.rxjava3.core.Scheduler; +import io.reactivex.rxjava3.disposables.*; +import io.reactivex.rxjava3.internal.disposables.EmptyDisposable; + +/** + * Scheduler that creates and caches a set of thread pools and reuses them if possible. + */ +public final class IoScheduler extends Scheduler { + private static final String WORKER_THREAD_NAME_PREFIX = "RxCachedThreadScheduler"; + static final RxThreadFactory WORKER_THREAD_FACTORY; + + private static final String EVICTOR_THREAD_NAME_PREFIX = "RxCachedWorkerPoolEvictor"; + static final RxThreadFactory EVICTOR_THREAD_FACTORY; + + /** The name of the system property for setting the keep-alive time (in seconds) for this Scheduler workers. */ + private static final String KEY_KEEP_ALIVE_TIME = "rx3.io-keep-alive-time"; + public static final long KEEP_ALIVE_TIME_DEFAULT = 60; + + private static final long KEEP_ALIVE_TIME; + private static final TimeUnit KEEP_ALIVE_UNIT = TimeUnit.SECONDS; + + static final ThreadWorker SHUTDOWN_THREAD_WORKER; + final ThreadFactory threadFactory; + final AtomicReference<CachedWorkerPool> pool; + + /** The name of the system property for setting the thread priority for this Scheduler. */ + private static final String KEY_IO_PRIORITY = "rx3.io-priority"; + + /** The name of the system property for setting the release behaviour for this Scheduler. */ + private static final String KEY_SCHEDULED_RELEASE = "rx3.io-scheduled-release"; + static boolean USE_SCHEDULED_RELEASE; + + static final CachedWorkerPool NONE; + + static { + KEEP_ALIVE_TIME = Long.getLong(KEY_KEEP_ALIVE_TIME, KEEP_ALIVE_TIME_DEFAULT); + + SHUTDOWN_THREAD_WORKER = new ThreadWorker(new RxThreadFactory("RxCachedThreadSchedulerShutdown")); + SHUTDOWN_THREAD_WORKER.dispose(); + + int priority = Math.max(Thread.MIN_PRIORITY, Math.min(Thread.MAX_PRIORITY, + Integer.getInteger(KEY_IO_PRIORITY, Thread.NORM_PRIORITY))); + + WORKER_THREAD_FACTORY = new RxThreadFactory(WORKER_THREAD_NAME_PREFIX, priority); + + EVICTOR_THREAD_FACTORY = new RxThreadFactory(EVICTOR_THREAD_NAME_PREFIX, priority); + + USE_SCHEDULED_RELEASE = Boolean.getBoolean(KEY_SCHEDULED_RELEASE); + + NONE = new CachedWorkerPool(0, null, WORKER_THREAD_FACTORY); + NONE.shutdown(); + } + + static final class CachedWorkerPool implements Runnable { + private final long keepAliveTime; + private final ConcurrentLinkedQueue<ThreadWorker> expiringWorkerQueue; + final CompositeDisposable allWorkers; + private final ScheduledExecutorService evictorService; + private final Future<?> evictorTask; + private final ThreadFactory threadFactory; + + CachedWorkerPool(long keepAliveTime, TimeUnit unit, ThreadFactory threadFactory) { + this.keepAliveTime = unit != null ? unit.toNanos(keepAliveTime) : 0L; + this.expiringWorkerQueue = new ConcurrentLinkedQueue<>(); + this.allWorkers = new CompositeDisposable(); + this.threadFactory = threadFactory; + + ScheduledExecutorService evictor = null; + Future<?> task = null; + if (unit != null) { + evictor = Executors.newScheduledThreadPool(1, EVICTOR_THREAD_FACTORY); + task = evictor.scheduleWithFixedDelay(this, this.keepAliveTime, this.keepAliveTime, TimeUnit.NANOSECONDS); + } + evictorService = evictor; + evictorTask = task; + } + + @Override + public void run() { + evictExpiredWorkers(expiringWorkerQueue, allWorkers); + } + + ThreadWorker get() { + if (allWorkers.isDisposed()) { + return SHUTDOWN_THREAD_WORKER; + } + while (!expiringWorkerQueue.isEmpty()) { + ThreadWorker threadWorker = expiringWorkerQueue.poll(); + if (threadWorker != null) { + return threadWorker; + } + } + + // No cached worker found, so create a new one. + ThreadWorker w = new ThreadWorker(threadFactory); + allWorkers.add(w); + return w; + } + + void release(ThreadWorker threadWorker) { + // Refresh expire time before putting worker back in pool + threadWorker.setExpirationTime(now() + keepAliveTime); + + expiringWorkerQueue.offer(threadWorker); + } + + static void evictExpiredWorkers(ConcurrentLinkedQueue<ThreadWorker> expiringWorkerQueue, CompositeDisposable allWorkers) { + if (!expiringWorkerQueue.isEmpty()) { + long currentTimestamp = now(); + + for (ThreadWorker threadWorker : expiringWorkerQueue) { + if (threadWorker.getExpirationTime() <= currentTimestamp) { + if (expiringWorkerQueue.remove(threadWorker)) { + allWorkers.remove(threadWorker); + } + } else { + // Queue is ordered with the worker that will expire first in the beginning, so when we + // find a non-expired worker we can stop evicting. + break; + } + } + } + } + + static long now() { + return System.nanoTime(); + } + + void shutdown() { + allWorkers.dispose(); + if (evictorTask != null) { + evictorTask.cancel(true); + } + if (evictorService != null) { + evictorService.shutdownNow(); + } + } + } + + public IoScheduler() { + this(WORKER_THREAD_FACTORY); + } + + /** + * Constructs an IoScheduler with the given thread factory and starts the pool of workers. + * @param threadFactory thread factory to use for creating worker threads. Note that this takes precedence over any + * system properties for configuring new thread creation. Cannot be null. + */ + public IoScheduler(ThreadFactory threadFactory) { + this.threadFactory = threadFactory; + this.pool = new AtomicReference<>(NONE); + start(); + } + + @Override + public void start() { + CachedWorkerPool update = new CachedWorkerPool(KEEP_ALIVE_TIME, KEEP_ALIVE_UNIT, threadFactory); + if (!pool.compareAndSet(NONE, update)) { + update.shutdown(); + } + } + + @Override + public void shutdown() { + CachedWorkerPool curr = pool.getAndSet(NONE); + if (curr != NONE) { + curr.shutdown(); + } + } + + @NonNull + @Override + public Worker createWorker() { + return new EventLoopWorker(pool.get()); + } + + public int size() { + return pool.get().allWorkers.size(); + } + + static final class EventLoopWorker extends Scheduler.Worker implements Runnable { + private final CompositeDisposable tasks; + private final CachedWorkerPool pool; + private final ThreadWorker threadWorker; + + final AtomicBoolean once = new AtomicBoolean(); + + EventLoopWorker(CachedWorkerPool pool) { + this.pool = pool; + this.tasks = new CompositeDisposable(); + this.threadWorker = pool.get(); + } + + @Override + public void dispose() { + if (once.compareAndSet(false, true)) { + tasks.dispose(); + + if (USE_SCHEDULED_RELEASE) { + threadWorker.scheduleActual(this, 0, TimeUnit.NANOSECONDS, null); + } else { + // releasing the pool should be the last action + pool.release(threadWorker); + } + } + } + + @Override + public void run() { + pool.release(threadWorker); + } + + @Override + public boolean isDisposed() { + return once.get(); + } + + @NonNull + @Override + public Disposable schedule(@NonNull Runnable action, long delayTime, @NonNull TimeUnit unit) { + if (tasks.isDisposed()) { + // don't schedule, we are unsubscribed + return EmptyDisposable.INSTANCE; + } + + return threadWorker.scheduleActual(action, delayTime, unit, tasks); + } + } + + static final class ThreadWorker extends NewThreadWorker { + + long expirationTime; + + ThreadWorker(ThreadFactory threadFactory) { + super(threadFactory); + this.expirationTime = 0L; + } + + public long getExpirationTime() { + return expirationTime; + } + + public void setExpirationTime(long expirationTime) { + this.expirationTime = expirationTime; + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/schedulers/NewThreadScheduler.java b/src/main/java/io/reactivex/rxjava3/internal/schedulers/NewThreadScheduler.java new file mode 100644 index 0000000000..1c29efddc5 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/schedulers/NewThreadScheduler.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.schedulers; + +import java.util.concurrent.ThreadFactory; + +import io.reactivex.rxjava3.annotations.NonNull; +import io.reactivex.rxjava3.core.Scheduler; + +/** + * Schedules work on a new thread. + */ +public final class NewThreadScheduler extends Scheduler { + + final ThreadFactory threadFactory; + + private static final String THREAD_NAME_PREFIX = "RxNewThreadScheduler"; + private static final RxThreadFactory THREAD_FACTORY; + + /** The name of the system property for setting the thread priority for this Scheduler. */ + private static final String KEY_NEWTHREAD_PRIORITY = "rx3.newthread-priority"; + + static { + int priority = Math.max(Thread.MIN_PRIORITY, Math.min(Thread.MAX_PRIORITY, + Integer.getInteger(KEY_NEWTHREAD_PRIORITY, Thread.NORM_PRIORITY))); + + THREAD_FACTORY = new RxThreadFactory(THREAD_NAME_PREFIX, priority); + } + + public NewThreadScheduler() { + this(THREAD_FACTORY); + } + + public NewThreadScheduler(ThreadFactory threadFactory) { + this.threadFactory = threadFactory; + } + + @NonNull + @Override + public Worker createWorker() { + return new NewThreadWorker(threadFactory); + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/schedulers/NewThreadWorker.java b/src/main/java/io/reactivex/rxjava3/internal/schedulers/NewThreadWorker.java new file mode 100644 index 0000000000..764241e76e --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/schedulers/NewThreadWorker.java @@ -0,0 +1,179 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.schedulers; + +import java.util.concurrent.*; + +import io.reactivex.rxjava3.annotations.*; +import io.reactivex.rxjava3.core.Scheduler; +import io.reactivex.rxjava3.disposables.*; +import io.reactivex.rxjava3.internal.disposables.*; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +/** + * Base class that manages a single-threaded ScheduledExecutorService as a + * worker but doesn't perform task-tracking operations. + * + */ +public class NewThreadWorker extends Scheduler.Worker { + private final ScheduledExecutorService executor; + + volatile boolean disposed; + + public NewThreadWorker(ThreadFactory threadFactory) { + executor = SchedulerPoolFactory.create(threadFactory); + } + + @NonNull + @Override + public Disposable schedule(@NonNull final Runnable run) { + return schedule(run, 0, null); + } + + @NonNull + @Override + public Disposable schedule(@NonNull final Runnable action, long delayTime, @NonNull TimeUnit unit) { + if (disposed) { + return EmptyDisposable.INSTANCE; + } + return scheduleActual(action, delayTime, unit, null); + } + + /** + * Schedules the given runnable on the underlying executor directly and + * returns its future wrapped into a Disposable. + * @param run the Runnable to execute in a delayed fashion + * @param delayTime the delay amount + * @param unit the delay time unit + * @return the ScheduledRunnable instance + */ + public Disposable scheduleDirect(final Runnable run, long delayTime, TimeUnit unit) { + ScheduledDirectTask task = new ScheduledDirectTask(RxJavaPlugins.onSchedule(run), true); + try { + Future<?> f; + if (delayTime <= 0L) { + f = executor.submit(task); + } else { + f = executor.schedule(task, delayTime, unit); + } + task.setFuture(f); + return task; + } catch (RejectedExecutionException ex) { + RxJavaPlugins.onError(ex); + return EmptyDisposable.INSTANCE; + } + } + + /** + * Schedules the given runnable periodically on the underlying executor directly + * and returns its future wrapped into a Disposable. + * @param run the Runnable to execute in a periodic fashion + * @param initialDelay the initial delay amount + * @param period the repeat period amount + * @param unit the time unit for both the initialDelay and period + * @return the ScheduledRunnable instance + */ + public Disposable schedulePeriodicallyDirect(Runnable run, long initialDelay, long period, TimeUnit unit) { + final Runnable decoratedRun = RxJavaPlugins.onSchedule(run); + if (period <= 0L) { + + InstantPeriodicTask periodicWrapper = new InstantPeriodicTask(decoratedRun, executor); + try { + Future<?> f; + if (initialDelay <= 0L) { + f = executor.submit(periodicWrapper); + } else { + f = executor.schedule(periodicWrapper, initialDelay, unit); + } + periodicWrapper.setFirst(f); + } catch (RejectedExecutionException ex) { + RxJavaPlugins.onError(ex); + return EmptyDisposable.INSTANCE; + } + + return periodicWrapper; + } + ScheduledDirectPeriodicTask task = new ScheduledDirectPeriodicTask(decoratedRun, true); + try { + Future<?> f = executor.scheduleAtFixedRate(task, initialDelay, period, unit); + task.setFuture(f); + return task; + } catch (RejectedExecutionException ex) { + RxJavaPlugins.onError(ex); + return EmptyDisposable.INSTANCE; + } + } + + /** + * Wraps and returns the given runnable into a ScheduledRunnable and schedules it + * on the underlying ScheduledExecutorService. + * @param run the runnable instance + * @param delayTime the time to delay the execution + * @param unit the time unit + * @param parent the optional tracker parent to add the created ScheduledRunnable instance to before it gets scheduled + * @return the ScheduledRunnable instance + */ + @NonNull + public ScheduledRunnable scheduleActual(final Runnable run, long delayTime, @NonNull TimeUnit unit, @Nullable DisposableContainer parent) { + Runnable decoratedRun = RxJavaPlugins.onSchedule(run); + + ScheduledRunnable sr = new ScheduledRunnable(decoratedRun, parent); + + if (parent != null) { + if (!parent.add(sr)) { + return sr; + } + } + + Future<?> f; + try { + if (delayTime <= 0) { + f = executor.submit((Callable<Object>)sr); + } else { + f = executor.schedule((Callable<Object>)sr, delayTime, unit); + } + sr.setFuture(f); + } catch (RejectedExecutionException ex) { + if (parent != null) { + parent.remove(sr); + } + RxJavaPlugins.onError(ex); + } + + return sr; + } + + @Override + public void dispose() { + if (!disposed) { + disposed = true; + executor.shutdownNow(); + } + } + + /** + * Shuts down the underlying executor in a non-interrupting fashion. + */ + public void shutdown() { + if (!disposed) { + disposed = true; + executor.shutdown(); + } + } + + @Override + public boolean isDisposed() { + return disposed; + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/schedulers/NonBlockingThread.java b/src/main/java/io/reactivex/rxjava3/internal/schedulers/NonBlockingThread.java new file mode 100644 index 0000000000..ff6dd5c706 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/schedulers/NonBlockingThread.java @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.schedulers; + +/** + * Marker interface to indicate blocking is not recommended while running + * on a Scheduler with a thread type implementing it. + */ +public interface NonBlockingThread { + +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/schedulers/RxThreadFactory.java b/src/main/java/io/reactivex/rxjava3/internal/schedulers/RxThreadFactory.java new file mode 100644 index 0000000000..9b3a7968c3 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/schedulers/RxThreadFactory.java @@ -0,0 +1,92 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.schedulers; + +import io.reactivex.rxjava3.annotations.NonNull; + +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.atomic.AtomicLong; + +/** + * A ThreadFactory that counts how many threads have been created and given a prefix, + * sets the created Thread's name to {@code prefix-count}. + */ +public final class RxThreadFactory extends AtomicLong implements ThreadFactory { + + private static final long serialVersionUID = -7789753024099756196L; + + final String prefix; + + final int priority; + + final boolean nonBlocking; + +// static volatile boolean CREATE_TRACE; + + public RxThreadFactory(String prefix) { + this(prefix, Thread.NORM_PRIORITY, false); + } + + public RxThreadFactory(String prefix, int priority) { + this(prefix, priority, false); + } + + public RxThreadFactory(String prefix, int priority, boolean nonBlocking) { + this.prefix = prefix; + this.priority = priority; + this.nonBlocking = nonBlocking; + } + + @Override + public Thread newThread(@NonNull Runnable r) { + StringBuilder nameBuilder = new StringBuilder(prefix).append('-').append(incrementAndGet()); + +// if (CREATE_TRACE) { +// nameBuilder.append("\r\n"); +// for (StackTraceElement se :Thread.currentThread().getStackTrace()) { +// String s = se.toString(); +// if (s.contains("sun.reflect.")) { +// continue; +// } +// if (s.contains("junit.runners.")) { +// continue; +// } +// if (s.contains("org.gradle.internal.")) { +// continue; +// } +// if (s.contains("java.util.concurrent.ThreadPoolExecutor")) { +// continue; +// } +// nameBuilder.append(s).append("\r\n"); +// } +// } + + String name = nameBuilder.toString(); + Thread t = nonBlocking ? new RxCustomThread(r, name) : new Thread(r, name); + t.setPriority(priority); + t.setDaemon(true); + return t; + } + + @Override + public String toString() { + return "RxThreadFactory[" + prefix + "]"; + } + + static final class RxCustomThread extends Thread implements NonBlockingThread { + RxCustomThread(Runnable run, String name) { + super(run, name); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/schedulers/ScheduledDirectPeriodicTask.java b/src/main/java/io/reactivex/rxjava3/internal/schedulers/ScheduledDirectPeriodicTask.java new file mode 100644 index 0000000000..1862035dde --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/schedulers/ScheduledDirectPeriodicTask.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.schedulers; + +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +/** + * A Callable to be submitted to an ExecutorService that runs a Runnable + * action periodically and manages completion/cancellation. + * @since 2.0.8 + */ +public final class ScheduledDirectPeriodicTask extends AbstractDirectTask implements Runnable { + + private static final long serialVersionUID = 1811839108042568751L; + + public ScheduledDirectPeriodicTask(Runnable runnable, boolean interruptOnCancel) { + super(runnable, interruptOnCancel); + } + + @Override + public void run() { + runner = Thread.currentThread(); + try { + runnable.run(); + runner = null; + } catch (Throwable ex) { + // Exceptions.throwIfFatal(ex); nowhere to go + dispose(); + runner = null; + RxJavaPlugins.onError(ex); + throw ex; + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/schedulers/ScheduledDirectTask.java b/src/main/java/io/reactivex/rxjava3/internal/schedulers/ScheduledDirectTask.java new file mode 100644 index 0000000000..6ca8992971 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/schedulers/ScheduledDirectTask.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.schedulers; + +import java.util.concurrent.Callable; + +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +/** + * A Callable to be submitted to an ExecutorService that runs a Runnable + * action and manages completion/cancellation. + * @since 2.0.8 + */ +public final class ScheduledDirectTask extends AbstractDirectTask implements Callable<Void> { + + private static final long serialVersionUID = 1811839108042568751L; + + public ScheduledDirectTask(Runnable runnable, boolean interruptOnCancel) { + super(runnable, interruptOnCancel); + } + + @Override + public Void call() { + runner = Thread.currentThread(); + try { + try { + runnable.run(); + } finally { + lazySet(FINISHED); + runner = null; + } + } catch (Throwable ex) { + // Exceptions.throwIfFatal(e); nowhere to go + RxJavaPlugins.onError(ex); + throw ex; + } + return null; + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/schedulers/ScheduledRunnable.java b/src/main/java/io/reactivex/rxjava3/internal/schedulers/ScheduledRunnable.java new file mode 100644 index 0000000000..2a1baa641a --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/schedulers/ScheduledRunnable.java @@ -0,0 +1,177 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.schedulers; + +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicReferenceArray; + +import io.reactivex.rxjava3.disposables.*; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +public final class ScheduledRunnable extends AtomicReferenceArray<Object> +implements Runnable, Callable<Object>, Disposable { + + private static final long serialVersionUID = -6120223772001106981L; + final Runnable actual; + final boolean interruptOnCancel; + + /** Indicates that the parent tracking this task has been notified about its completion. */ + static final Object PARENT_DISPOSED = new Object(); + /** Indicates the dispose() was called from within the run/call method. */ + static final Object SYNC_DISPOSED = new Object(); + /** Indicates the dispose() was called from another thread. */ + static final Object ASYNC_DISPOSED = new Object(); + + static final Object DONE = new Object(); + + static final int PARENT_INDEX = 0; + static final int FUTURE_INDEX = 1; + static final int THREAD_INDEX = 2; + + /** + * Creates a ScheduledRunnable by wrapping the given action and setting + * up the optional parent. + * The underlying future will be interrupted if the task is disposed asynchronously. + * @param actual the runnable to wrap, not-null (not verified) + * @param parent the parent tracking container or null if none + */ + public ScheduledRunnable(Runnable actual, DisposableContainer parent) { + this(actual, parent, true); + } + + /** + * Creates a ScheduledRunnable by wrapping the given action and setting + * up the optional parent. + * @param actual the runnable to wrap, not-null (not verified) + * @param parent the parent tracking container or null if none + * @param interruptOnCancel if true, the underlying future will be interrupted when disposing + * this task from a different thread than it is running on. + */ + public ScheduledRunnable(Runnable actual, DisposableContainer parent, boolean interruptOnCancel) { + super(3); + this.actual = actual; + this.interruptOnCancel = interruptOnCancel; + this.lazySet(0, parent); + } + + @Override + public Object call() { + // Being Callable saves an allocation in ThreadPoolExecutor + run(); + return null; + } + + @Override + public void run() { + lazySet(THREAD_INDEX, Thread.currentThread()); + try { + try { + actual.run(); + } catch (Throwable e) { + // Exceptions.throwIfFatal(e); nowhere to go + RxJavaPlugins.onError(e); + throw e; + } + } finally { + Object o = get(PARENT_INDEX); + if (o != PARENT_DISPOSED && compareAndSet(PARENT_INDEX, o, DONE) && o != null) { + ((DisposableContainer)o).delete(this); + } + + for (;;) { + o = get(FUTURE_INDEX); + if (o == SYNC_DISPOSED || o == ASYNC_DISPOSED || compareAndSet(FUTURE_INDEX, o, DONE)) { + break; + } + } + lazySet(THREAD_INDEX, null); + } + } + + public void setFuture(Future<?> f) { + for (;;) { + Object o = get(FUTURE_INDEX); + if (o == DONE) { + return; + } + if (o == SYNC_DISPOSED) { + f.cancel(false); + return; + } + if (o == ASYNC_DISPOSED) { + f.cancel(interruptOnCancel); + return; + } + if (compareAndSet(FUTURE_INDEX, o, f)) { + return; + } + } + } + + @Override + public void dispose() { + for (;;) { + Object o = get(FUTURE_INDEX); + if (o == DONE || o == SYNC_DISPOSED || o == ASYNC_DISPOSED) { + break; + } + boolean async = get(THREAD_INDEX) != Thread.currentThread(); + if (compareAndSet(FUTURE_INDEX, o, async ? ASYNC_DISPOSED : SYNC_DISPOSED)) { + if (o != null) { + ((Future<?>)o).cancel(async && interruptOnCancel); + } + break; + } + } + + for (;;) { + Object o = get(PARENT_INDEX); + if (o == DONE || o == PARENT_DISPOSED || o == null) { + return; + } + if (compareAndSet(PARENT_INDEX, o, PARENT_DISPOSED)) { + ((DisposableContainer)o).delete(this); + return; + } + } + } + + @Override + public boolean isDisposed() { + Object o = get(PARENT_INDEX); + return o == PARENT_DISPOSED || o == DONE; + } + + @Override + public String toString() { + String state; + Object o = get(FUTURE_INDEX); + if (o == DONE) { + state = "Finished"; + } else if (o == SYNC_DISPOSED) { + state = "Disposed(Sync)"; + } else if (o == ASYNC_DISPOSED) { + state = "Disposed(Async)"; + } else { + o = get(THREAD_INDEX); + if (o == null) { + state = "Waiting"; + } else { + state = "Running on " + o; + } + } + + return getClass().getSimpleName() + "[" + state + "]"; + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/schedulers/SchedulerMultiWorkerSupport.java b/src/main/java/io/reactivex/rxjava3/internal/schedulers/SchedulerMultiWorkerSupport.java new file mode 100644 index 0000000000..b1e186adc9 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/schedulers/SchedulerMultiWorkerSupport.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.schedulers; + +import io.reactivex.rxjava3.annotations.NonNull; +import io.reactivex.rxjava3.core.Scheduler; + +/** + * Allows retrieving multiple workers from the implementing + * {@link io.reactivex.rxjava3.core.Scheduler} in a way that when asking for + * at most the parallelism level of the Scheduler, those + * {@link io.reactivex.rxjava3.core.Scheduler.Worker} instances will be running + * with different backing threads. + * <p>History: 2.1.8 - experimental + * @since 2.2 + */ +public interface SchedulerMultiWorkerSupport { + + /** + * Creates the given number of {@link io.reactivex.rxjava3.core.Scheduler.Worker} instances + * that are possibly backed by distinct threads + * and calls the specified {@code Consumer} with them. + * @param number the number of workers to create, positive + * @param callback the callback to send worker instances to + */ + void createWorkers(int number, @NonNull WorkerCallback callback); + + /** + * The callback interface for the {@link SchedulerMultiWorkerSupport#createWorkers(int, WorkerCallback)} + * method. + */ + interface WorkerCallback { + /** + * Called with the Worker index and instance. + * @param index the worker index, zero-based + * @param worker the worker instance + */ + void onWorker(int index, @NonNull Scheduler.Worker worker); + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/schedulers/SchedulerPoolFactory.java b/src/main/java/io/reactivex/rxjava3/internal/schedulers/SchedulerPoolFactory.java new file mode 100644 index 0000000000..44a824a168 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/schedulers/SchedulerPoolFactory.java @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.schedulers; + +import java.util.concurrent.*; + +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.functions.Function; + +/** + * Manages the creating of ScheduledExecutorServices and sets up purging. + */ +public final class SchedulerPoolFactory { + /** Utility class. */ + private SchedulerPoolFactory() { + throw new IllegalStateException("No instances!"); + } + + static final String PURGE_ENABLED_KEY = "rx3.purge-enabled"; + + public static final boolean PURGE_ENABLED; + + static { + SystemPropertyAccessor propertyAccessor = new SystemPropertyAccessor(); + PURGE_ENABLED = getBooleanProperty(true, PURGE_ENABLED_KEY, true, true, propertyAccessor); + } + + static boolean getBooleanProperty(boolean enabled, String key, boolean defaultNotFound, boolean defaultNotEnabled, Function<String, String> propertyAccessor) { + if (enabled) { + try { + String value = propertyAccessor.apply(key); + if (value == null) { + return defaultNotFound; + } + return "true".equals(value); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + return defaultNotFound; + } + } + return defaultNotEnabled; + } + + static final class SystemPropertyAccessor implements Function<String, String> { + @Override + public String apply(String t) { + return System.getProperty(t); + } + } + + /** + * Creates a ScheduledExecutorService with the given factory. + * @param factory the thread factory + * @return the ScheduledExecutorService + */ + public static ScheduledExecutorService create(ThreadFactory factory) { + final ScheduledThreadPoolExecutor exec = new ScheduledThreadPoolExecutor(1, factory); + exec.setRemoveOnCancelPolicy(PURGE_ENABLED); + return exec; + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/schedulers/SchedulerWhen.java b/src/main/java/io/reactivex/rxjava3/internal/schedulers/SchedulerWhen.java new file mode 100644 index 0000000000..814c971f1c --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/schedulers/SchedulerWhen.java @@ -0,0 +1,331 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.schedulers; + +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.*; + +import io.reactivex.rxjava3.annotations.NonNull; +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.*; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.util.ExceptionHelper; +import io.reactivex.rxjava3.processors.*; + +/** + * Allows the use of operators for controlling the timing around when actions + * scheduled on workers are actually done. This makes it possible to layer + * additional behavior on this {@link Scheduler}. The only parameter is a + * function that flattens an {@link Observable} of {@link Observable} of + * {@link Completable}s into just one {@link Completable}. There must be a chain + * of operators connecting the returned value to the source {@link Observable} + * otherwise any work scheduled on the returned {@link Scheduler} will not be + * executed. + * <p> + * When {@link Scheduler#createWorker()} is invoked a {@link Observable} of + * {@link Completable}s is onNext'd to the combinator to be flattened. If the + * inner {@link Observable} is not immediately subscribed to an calls to + * {@link Worker#schedule} are buffered. Once the {@link Observable} is + * subscribed to actions are then onNext'd as {@link Completable}s. + * <p> + * Finally the actions scheduled on the parent {@link Scheduler} when the inner + * most {@link Completable}s are subscribed to. + * <p> + * When the {@link io.reactivex.rxjava3.core.Scheduler.Worker Worker} is unsubscribed the {@link Completable} emits an + * onComplete and triggers any behavior in the flattening operator. The + * {@link Observable} and all {@link Completable}s give to the flattening + * function never onError. + * <p> + * Limit the amount concurrency two at a time without creating a new fix size + * thread pool: + * + * <pre> + * {@code + * Scheduler limitScheduler = Schedulers.computation().when(workers -> { + * // use merge max concurrent to limit the number of concurrent + * // callbacks two at a time + * return Completable.merge(Observable.merge(workers), 2); + * }); + * } + * </pre> + * <p> + * This is a slightly different way to limit the concurrency but it has some + * interesting benefits and drawbacks to the method above. It works by limited + * the number of concurrent {@link io.reactivex.rxjava3.core.Scheduler.Worker Worker}s rather than individual actions. + * Generally each {@link Observable} uses its own {@link io.reactivex.rxjava3.core.Scheduler.Worker Worker}. This means + * that this will essentially limit the number of concurrent subscribes. The + * danger comes from using operators like + * {@link Flowable#zip(org.reactivestreams.Publisher, org.reactivestreams.Publisher, io.reactivex.rxjava3.functions.BiFunction)} where + * subscribing to the first {@link Observable} could deadlock the subscription + * to the second. + * + * <pre> + * {@code + * Scheduler limitScheduler = Schedulers.computation().when(workers -> { + * // use merge max concurrent to limit the number of concurrent + * // Observables two at a time + * return Completable.merge(Observable.merge(workers, 2)); + * }); + * } + * </pre> + * + * Slowing down the rate to no more than 1 a second. This suffers from the + * same problem as the one above I could find an {@link Observable} operator + * that limits the rate without dropping the values (aka leaky bucket + * algorithm). + * + * <pre> + * {@code + * Scheduler slowScheduler = Schedulers.computation().when(workers -> { + * // use concatenate to make each worker happen one at a time. + * return Completable.concat(workers.map(actions -> { + * // delay the starting of the next worker by 1 second. + * return Completable.merge(actions.delaySubscription(1, TimeUnit.SECONDS)); + * })); + * }); + * } + * </pre> + * <p>History 2.0.1 - experimental + * @since 2.1 + */ +public class SchedulerWhen extends Scheduler implements Disposable { + private final Scheduler actualScheduler; + private final FlowableProcessor<Flowable<Completable>> workerProcessor; + private Disposable disposable; + + public SchedulerWhen(Function<Flowable<Flowable<Completable>>, Completable> combine, Scheduler actualScheduler) { + this.actualScheduler = actualScheduler; + // workers are converted into completables and put in this queue. + this.workerProcessor = UnicastProcessor.<Flowable<Completable>>create().toSerialized(); + // send it to a custom combinator to pick the order and rate at which + // workers are processed. + try { + disposable = combine.apply(workerProcessor).subscribe(); + } catch (Throwable e) { + throw ExceptionHelper.wrapOrThrow(e); + } + } + + @Override + public void dispose() { + disposable.dispose(); + } + + @Override + public boolean isDisposed() { + return disposable.isDisposed(); + } + + @NonNull + @Override + public Worker createWorker() { + final Worker actualWorker = actualScheduler.createWorker(); + // a queue for the actions submitted while worker is waiting to get to + // the subscribe to off the workerQueue. + final FlowableProcessor<ScheduledAction> actionProcessor = UnicastProcessor.<ScheduledAction>create().toSerialized(); + // convert the work of scheduling all the actions into a completable + Flowable<Completable> actions = actionProcessor.map(new CreateWorkerFunction(actualWorker)); + + // a worker that queues the action to the actionQueue subject. + Worker worker = new QueueWorker(actionProcessor, actualWorker); + + // enqueue the completable that process actions put in reply subject + workerProcessor.onNext(actions); + + // return the worker that adds actions to the reply subject + return worker; + } + + static final Disposable SUBSCRIBED = new SubscribedDisposable(); + + static final Disposable DISPOSED = Disposable.disposed(); + + @SuppressWarnings("serial") + abstract static class ScheduledAction extends AtomicReference<Disposable> implements Disposable { + ScheduledAction() { + super(SUBSCRIBED); + } + + void call(Worker actualWorker, CompletableObserver actionCompletable) { + Disposable oldState = get(); + // either SUBSCRIBED or UNSUBSCRIBED + if (oldState == DISPOSED) { + // no need to schedule return + return; + } + if (oldState != SUBSCRIBED) { + // has already been scheduled return + // should not be able to get here but handle it anyway by not + // rescheduling. + return; + } + + Disposable newState = callActual(actualWorker, actionCompletable); + + if (!compareAndSet(SUBSCRIBED, newState)) { + // set would only fail if the new current state is some other + // subscription from a concurrent call to this method. + // Unsubscribe from the action just scheduled because it lost + // the race. + newState.dispose(); + } + } + + protected abstract Disposable callActual(Worker actualWorker, CompletableObserver actionCompletable); + + @Override + public boolean isDisposed() { + return get().isDisposed(); + } + + @Override + public void dispose() { + getAndSet(DISPOSED).dispose(); + } + } + + @SuppressWarnings("serial") + static class ImmediateAction extends ScheduledAction { + private final Runnable action; + + ImmediateAction(Runnable action) { + this.action = action; + } + + @Override + protected Disposable callActual(Worker actualWorker, CompletableObserver actionCompletable) { + return actualWorker.schedule(new OnCompletedAction(action, actionCompletable)); + } + } + + @SuppressWarnings("serial") + static class DelayedAction extends ScheduledAction { + private final Runnable action; + private final long delayTime; + private final TimeUnit unit; + + DelayedAction(Runnable action, long delayTime, TimeUnit unit) { + this.action = action; + this.delayTime = delayTime; + this.unit = unit; + } + + @Override + protected Disposable callActual(Worker actualWorker, CompletableObserver actionCompletable) { + return actualWorker.schedule(new OnCompletedAction(action, actionCompletable), delayTime, unit); + } + } + + static class OnCompletedAction implements Runnable { + final CompletableObserver actionCompletable; + final Runnable action; + + OnCompletedAction(Runnable action, CompletableObserver actionCompletable) { + this.action = action; + this.actionCompletable = actionCompletable; + } + + @Override + public void run() { + try { + action.run(); + } finally { + actionCompletable.onComplete(); + } + } + } + + static final class CreateWorkerFunction implements Function<ScheduledAction, Completable> { + final Worker actualWorker; + + CreateWorkerFunction(Worker actualWorker) { + this.actualWorker = actualWorker; + } + + @Override + public Completable apply(final ScheduledAction action) { + return new WorkerCompletable(action); + } + + final class WorkerCompletable extends Completable { + final ScheduledAction action; + + WorkerCompletable(ScheduledAction action) { + this.action = action; + } + + @Override + protected void subscribeActual(CompletableObserver actionCompletable) { + actionCompletable.onSubscribe(action); + action.call(actualWorker, actionCompletable); + } + } + } + + static final class QueueWorker extends Worker { + private final AtomicBoolean unsubscribed; + private final FlowableProcessor<ScheduledAction> actionProcessor; + private final Worker actualWorker; + + QueueWorker(FlowableProcessor<ScheduledAction> actionProcessor, Worker actualWorker) { + this.actionProcessor = actionProcessor; + this.actualWorker = actualWorker; + unsubscribed = new AtomicBoolean(); + } + + @Override + public void dispose() { + // complete the actionQueue when worker is unsubscribed to make + // room for the next worker in the workerQueue. + if (unsubscribed.compareAndSet(false, true)) { + actionProcessor.onComplete(); + actualWorker.dispose(); + } + } + + @Override + public boolean isDisposed() { + return unsubscribed.get(); + } + + @NonNull + @Override + public Disposable schedule(@NonNull final Runnable action, final long delayTime, @NonNull final TimeUnit unit) { + // send a scheduled action to the actionQueue + DelayedAction delayedAction = new DelayedAction(action, delayTime, unit); + actionProcessor.onNext(delayedAction); + return delayedAction; + } + + @NonNull + @Override + public Disposable schedule(@NonNull final Runnable action) { + // send a scheduled action to the actionQueue + ImmediateAction immediateAction = new ImmediateAction(action); + actionProcessor.onNext(immediateAction); + return immediateAction; + } + } + + static final class SubscribedDisposable implements Disposable { + @Override + public void dispose() { + } + + @Override + public boolean isDisposed() { + return false; + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/schedulers/SingleScheduler.java b/src/main/java/io/reactivex/rxjava3/internal/schedulers/SingleScheduler.java new file mode 100644 index 0000000000..98d3b04626 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/schedulers/SingleScheduler.java @@ -0,0 +1,216 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.schedulers; + +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicReference; + +import io.reactivex.rxjava3.annotations.NonNull; +import io.reactivex.rxjava3.core.Scheduler; +import io.reactivex.rxjava3.disposables.*; +import io.reactivex.rxjava3.internal.disposables.EmptyDisposable; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +/** + * A scheduler with a shared, single threaded underlying ScheduledExecutorService. + * @since 2.0 + */ +public final class SingleScheduler extends Scheduler { + + final ThreadFactory threadFactory; + final AtomicReference<ScheduledExecutorService> executor = new AtomicReference<>(); + + /** The name of the system property for setting the thread priority for this Scheduler. */ + private static final String KEY_SINGLE_PRIORITY = "rx3.single-priority"; + + private static final String THREAD_NAME_PREFIX = "RxSingleScheduler"; + + static final RxThreadFactory SINGLE_THREAD_FACTORY; + + static final ScheduledExecutorService SHUTDOWN; + static { + SHUTDOWN = Executors.newScheduledThreadPool(0); + SHUTDOWN.shutdown(); + + int priority = Math.max(Thread.MIN_PRIORITY, Math.min(Thread.MAX_PRIORITY, + Integer.getInteger(KEY_SINGLE_PRIORITY, Thread.NORM_PRIORITY))); + + SINGLE_THREAD_FACTORY = new RxThreadFactory(THREAD_NAME_PREFIX, priority, true); + } + + public SingleScheduler() { + this(SINGLE_THREAD_FACTORY); + } + + /** + * Constructs a SingleScheduler with the given ThreadFactory and prepares the + * single scheduler thread. + * @param threadFactory thread factory to use for creating worker threads. Note that this takes precedence over any + * system properties for configuring new thread creation. Cannot be null. + */ + public SingleScheduler(ThreadFactory threadFactory) { + this.threadFactory = threadFactory; + executor.lazySet(createExecutor(threadFactory)); + } + + static ScheduledExecutorService createExecutor(ThreadFactory threadFactory) { + return SchedulerPoolFactory.create(threadFactory); + } + + @Override + public void start() { + ScheduledExecutorService next = null; + for (;;) { + ScheduledExecutorService current = executor.get(); + if (current != SHUTDOWN) { + if (next != null) { + next.shutdown(); + } + return; + } + if (next == null) { + next = createExecutor(threadFactory); + } + if (executor.compareAndSet(current, next)) { + return; + } + + } + } + + @Override + public void shutdown() { + ScheduledExecutorService current = executor.getAndSet(SHUTDOWN); + if (current != SHUTDOWN) { + current.shutdownNow(); + } + } + + @NonNull + @Override + public Worker createWorker() { + return new ScheduledWorker(executor.get()); + } + + @NonNull + @Override + public Disposable scheduleDirect(@NonNull Runnable run, long delay, TimeUnit unit) { + ScheduledDirectTask task = new ScheduledDirectTask(RxJavaPlugins.onSchedule(run), true); + try { + Future<?> f; + if (delay <= 0L) { + f = executor.get().submit(task); + } else { + f = executor.get().schedule(task, delay, unit); + } + task.setFuture(f); + return task; + } catch (RejectedExecutionException ex) { + RxJavaPlugins.onError(ex); + return EmptyDisposable.INSTANCE; + } + } + + @NonNull + @Override + public Disposable schedulePeriodicallyDirect(@NonNull Runnable run, long initialDelay, long period, TimeUnit unit) { + final Runnable decoratedRun = RxJavaPlugins.onSchedule(run); + if (period <= 0L) { + + ScheduledExecutorService exec = executor.get(); + + InstantPeriodicTask periodicWrapper = new InstantPeriodicTask(decoratedRun, exec); + Future<?> f; + try { + if (initialDelay <= 0L) { + f = exec.submit(periodicWrapper); + } else { + f = exec.schedule(periodicWrapper, initialDelay, unit); + } + periodicWrapper.setFirst(f); + } catch (RejectedExecutionException ex) { + RxJavaPlugins.onError(ex); + return EmptyDisposable.INSTANCE; + } + + return periodicWrapper; + } + ScheduledDirectPeriodicTask task = new ScheduledDirectPeriodicTask(decoratedRun, true); + try { + Future<?> f = executor.get().scheduleAtFixedRate(task, initialDelay, period, unit); + task.setFuture(f); + return task; + } catch (RejectedExecutionException ex) { + RxJavaPlugins.onError(ex); + return EmptyDisposable.INSTANCE; + } + } + + static final class ScheduledWorker extends Scheduler.Worker { + + final ScheduledExecutorService executor; + + final CompositeDisposable tasks; + + volatile boolean disposed; + + ScheduledWorker(ScheduledExecutorService executor) { + this.executor = executor; + this.tasks = new CompositeDisposable(); + } + + @NonNull + @Override + public Disposable schedule(@NonNull Runnable run, long delay, @NonNull TimeUnit unit) { + if (disposed) { + return EmptyDisposable.INSTANCE; + } + + Runnable decoratedRun = RxJavaPlugins.onSchedule(run); + + ScheduledRunnable sr = new ScheduledRunnable(decoratedRun, tasks); + tasks.add(sr); + + try { + Future<?> f; + if (delay <= 0L) { + f = executor.submit((Callable<Object>)sr); + } else { + f = executor.schedule((Callable<Object>)sr, delay, unit); + } + + sr.setFuture(f); + } catch (RejectedExecutionException ex) { + dispose(); + RxJavaPlugins.onError(ex); + return EmptyDisposable.INSTANCE; + } + + return sr; + } + + @Override + public void dispose() { + if (!disposed) { + disposed = true; + tasks.dispose(); + } + } + + @Override + public boolean isDisposed() { + return disposed; + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/schedulers/TrampolineScheduler.java b/src/main/java/io/reactivex/rxjava3/internal/schedulers/TrampolineScheduler.java new file mode 100644 index 0000000000..04496482b0 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/schedulers/TrampolineScheduler.java @@ -0,0 +1,204 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.schedulers; + +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicInteger; + +import io.reactivex.rxjava3.annotations.NonNull; +import io.reactivex.rxjava3.core.Scheduler; +import io.reactivex.rxjava3.disposables.*; +import io.reactivex.rxjava3.internal.disposables.EmptyDisposable; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +/** + * Schedules work on the current thread but does not execute immediately. Work is put in a queue and executed + * after the current unit of work is completed. + */ +public final class TrampolineScheduler extends Scheduler { + private static final TrampolineScheduler INSTANCE = new TrampolineScheduler(); + + public static TrampolineScheduler instance() { + return INSTANCE; + } + + @NonNull + @Override + public Worker createWorker() { + return new TrampolineWorker(); + } + + /* package accessible for unit tests */TrampolineScheduler() { + } + + @NonNull + @Override + public Disposable scheduleDirect(@NonNull Runnable run) { + RxJavaPlugins.onSchedule(run).run(); + return EmptyDisposable.INSTANCE; + } + + @NonNull + @Override + public Disposable scheduleDirect(@NonNull Runnable run, long delay, TimeUnit unit) { + try { + unit.sleep(delay); + RxJavaPlugins.onSchedule(run).run(); + } catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + RxJavaPlugins.onError(ex); + } + return EmptyDisposable.INSTANCE; + } + + static final class TrampolineWorker extends Scheduler.Worker { + final PriorityBlockingQueue<TimedRunnable> queue = new PriorityBlockingQueue<>(); + + private final AtomicInteger wip = new AtomicInteger(); + + final AtomicInteger counter = new AtomicInteger(); + + volatile boolean disposed; + + @NonNull + @Override + public Disposable schedule(@NonNull Runnable action) { + return enqueue(action, now(TimeUnit.MILLISECONDS)); + } + + @NonNull + @Override + public Disposable schedule(@NonNull Runnable action, long delayTime, @NonNull TimeUnit unit) { + long execTime = now(TimeUnit.MILLISECONDS) + unit.toMillis(delayTime); + + return enqueue(new SleepingRunnable(action, this, execTime), execTime); + } + + Disposable enqueue(Runnable action, long execTime) { + if (disposed) { + return EmptyDisposable.INSTANCE; + } + final TimedRunnable timedRunnable = new TimedRunnable(action, execTime, counter.incrementAndGet()); + queue.add(timedRunnable); + + if (wip.getAndIncrement() == 0) { + int missed = 1; + for (;;) { + for (;;) { + if (disposed) { + queue.clear(); + return EmptyDisposable.INSTANCE; + } + final TimedRunnable polled = queue.poll(); + if (polled == null) { + break; + } + if (!polled.disposed) { + polled.run.run(); + } + } + missed = wip.addAndGet(-missed); + if (missed == 0) { + break; + } + } + + return EmptyDisposable.INSTANCE; + } else { + // queue wasn't empty, a parent is already processing so we just add to the end of the queue + return Disposable.fromRunnable(new AppendToQueueTask(timedRunnable)); + } + } + + @Override + public void dispose() { + disposed = true; + } + + @Override + public boolean isDisposed() { + return disposed; + } + + final class AppendToQueueTask implements Runnable { + final TimedRunnable timedRunnable; + + AppendToQueueTask(TimedRunnable timedRunnable) { + this.timedRunnable = timedRunnable; + } + + @Override + public void run() { + timedRunnable.disposed = true; + queue.remove(timedRunnable); + } + } + } + + static final class TimedRunnable implements Comparable<TimedRunnable> { + final Runnable run; + final long execTime; + final int count; // In case if time between enqueueing took less than 1ms + + volatile boolean disposed; + + TimedRunnable(Runnable run, Long execTime, int count) { + this.run = run; + this.execTime = execTime; + this.count = count; + } + + @Override + public int compareTo(TimedRunnable that) { + int result = Long.compare(execTime, that.execTime); + if (result == 0) { + return Integer.compare(count, that.count); + } + return result; + } + } + + static final class SleepingRunnable implements Runnable { + private final Runnable run; + private final TrampolineWorker worker; + private final long execTime; + + SleepingRunnable(Runnable run, TrampolineWorker worker, long execTime) { + this.run = run; + this.worker = worker; + this.execTime = execTime; + } + + @Override + public void run() { + if (!worker.disposed) { + long t = worker.now(TimeUnit.MILLISECONDS); + if (execTime > t) { + long delay = execTime - t; + try { + Thread.sleep(delay); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + RxJavaPlugins.onError(e); + return; + } + } + + if (!worker.disposed) { + run.run(); + } + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/subscribers/BasicFuseableConditionalSubscriber.java b/src/main/java/io/reactivex/rxjava3/internal/subscribers/BasicFuseableConditionalSubscriber.java new file mode 100644 index 0000000000..645a3171f2 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/subscribers/BasicFuseableConditionalSubscriber.java @@ -0,0 +1,184 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.subscribers; + +import org.reactivestreams.Subscription; + +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.internal.subscriptions.SubscriptionHelper; +import io.reactivex.rxjava3.operators.ConditionalSubscriber; +import io.reactivex.rxjava3.operators.QueueSubscription; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +/** + * Base class for a fuseable intermediate subscriber. + * @param <T> the upstream value type + * @param <R> the downstream value type + */ +public abstract class BasicFuseableConditionalSubscriber<T, R> implements ConditionalSubscriber<T>, QueueSubscription<R> { + + /** The downstream subscriber. */ + protected final ConditionalSubscriber<? super R> downstream; + + /** The upstream subscription. */ + protected Subscription upstream; + + /** The upstream's QueueSubscription if not null. */ + protected QueueSubscription<T> qs; + + /** Flag indicating no further onXXX event should be accepted. */ + protected boolean done; + + /** Holds the established fusion mode of the upstream. */ + protected int sourceMode; + + /** + * Construct a BasicFuseableSubscriber by wrapping the given subscriber. + * @param downstream the subscriber, not null (not verified) + */ + public BasicFuseableConditionalSubscriber(ConditionalSubscriber<? super R> downstream) { + this.downstream = downstream; + } + + // final: fixed protocol steps to support fuseable and non-fuseable upstream + @SuppressWarnings("unchecked") + @Override + public final void onSubscribe(Subscription s) { + if (SubscriptionHelper.validate(this.upstream, s)) { + + this.upstream = s; + if (s instanceof QueueSubscription) { + this.qs = (QueueSubscription<T>)s; + } + + if (beforeDownstream()) { + + downstream.onSubscribe(this); + + afterDownstream(); + } + + } + } + + /** + * Override this to perform actions before the call {@code actual.onSubscribe(this)} happens. + * @return true if onSubscribe should continue with the call + */ + protected boolean beforeDownstream() { + return true; + } + + /** + * Override this to perform actions after the call to {@code actual.onSubscribe(this)} happened. + */ + protected void afterDownstream() { + // default no-op + } + + // ----------------------------------- + // Convenience and state-aware methods + // ----------------------------------- + + @Override + public void onError(Throwable t) { + if (done) { + RxJavaPlugins.onError(t); + return; + } + done = true; + downstream.onError(t); + } + + /** + * Rethrows the throwable if it is a fatal exception or calls {@link #onError(Throwable)}. + * @param t the throwable to rethrow or signal to the actual subscriber + */ + protected final void fail(Throwable t) { + Exceptions.throwIfFatal(t); + upstream.cancel(); + onError(t); + } + + @Override + public void onComplete() { + if (done) { + return; + } + done = true; + downstream.onComplete(); + } + + /** + * Calls the upstream's QueueSubscription.requestFusion with the mode and + * saves the established mode in {@link #sourceMode} if that mode doesn't + * have the {@link QueueSubscription#BOUNDARY} flag set. + * <p> + * If the upstream doesn't support fusion ({@link #qs} is null), the method + * returns {@link QueueSubscription#NONE}. + * @param mode the fusion mode requested + * @return the established fusion mode + */ + protected final int transitiveBoundaryFusion(int mode) { + QueueSubscription<T> qs = this.qs; + if (qs != null) { + if ((mode & BOUNDARY) == 0) { + int m = qs.requestFusion(mode); + if (m != NONE) { + sourceMode = m; + } + return m; + } + } + return NONE; + } + + // -------------------------------------------------------------- + // Default implementation of the RS and QS protocol (can be overridden) + // -------------------------------------------------------------- + + @Override + public void request(long n) { + upstream.request(n); + } + + @Override + public void cancel() { + upstream.cancel(); + } + + @Override + public boolean isEmpty() { + return qs.isEmpty(); + } + + @Override + public void clear() { + qs.clear(); + } + + // ----------------------------------------------------------- + // The rest of the Queue interface methods shouldn't be called + // ----------------------------------------------------------- + + @Override + public final boolean offer(R e) { + throw new UnsupportedOperationException("Should not be called!"); + } + + @Override + public final boolean offer(R v1, R v2) { + throw new UnsupportedOperationException("Should not be called!"); + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/subscribers/BasicFuseableSubscriber.java b/src/main/java/io/reactivex/rxjava3/internal/subscribers/BasicFuseableSubscriber.java new file mode 100644 index 0000000000..162a9dbe55 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/subscribers/BasicFuseableSubscriber.java @@ -0,0 +1,184 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.subscribers; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.FlowableSubscriber; +import io.reactivex.rxjava3.exceptions.Exceptions; +import io.reactivex.rxjava3.internal.subscriptions.SubscriptionHelper; +import io.reactivex.rxjava3.operators.QueueSubscription; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +/** + * Base class for a fuseable intermediate subscriber. + * @param <T> the upstream value type + * @param <R> the downstream value type + */ +public abstract class BasicFuseableSubscriber<T, R> implements FlowableSubscriber<T>, QueueSubscription<R> { + + /** The downstream subscriber. */ + protected final Subscriber<? super R> downstream; + + /** The upstream subscription. */ + protected Subscription upstream; + + /** The upstream's QueueSubscription if not null. */ + protected QueueSubscription<T> qs; + + /** Flag indicating no further onXXX event should be accepted. */ + protected boolean done; + + /** Holds the established fusion mode of the upstream. */ + protected int sourceMode; + + /** + * Construct a BasicFuseableSubscriber by wrapping the given subscriber. + * @param downstream the subscriber, not null (not verified) + */ + public BasicFuseableSubscriber(Subscriber<? super R> downstream) { + this.downstream = downstream; + } + + // final: fixed protocol steps to support fuseable and non-fuseable upstream + @SuppressWarnings("unchecked") + @Override + public final void onSubscribe(Subscription s) { + if (SubscriptionHelper.validate(this.upstream, s)) { + + this.upstream = s; + if (s instanceof QueueSubscription) { + this.qs = (QueueSubscription<T>)s; + } + + if (beforeDownstream()) { + + downstream.onSubscribe(this); + + afterDownstream(); + } + + } + } + + /** + * Override this to perform actions before the call {@code actual.onSubscribe(this)} happens. + * @return true if onSubscribe should continue with the call + */ + protected boolean beforeDownstream() { + return true; + } + + /** + * Override this to perform actions after the call to {@code actual.onSubscribe(this)} happened. + */ + protected void afterDownstream() { + // default no-op + } + + // ----------------------------------- + // Convenience and state-aware methods + // ----------------------------------- + + @Override + public void onError(Throwable t) { + if (done) { + RxJavaPlugins.onError(t); + return; + } + done = true; + downstream.onError(t); + } + + /** + * Rethrows the throwable if it is a fatal exception or calls {@link #onError(Throwable)}. + * @param t the throwable to rethrow or signal to the actual subscriber + */ + protected final void fail(Throwable t) { + Exceptions.throwIfFatal(t); + upstream.cancel(); + onError(t); + } + + @Override + public void onComplete() { + if (done) { + return; + } + done = true; + downstream.onComplete(); + } + + /** + * Calls the upstream's QueueSubscription.requestFusion with the mode and + * saves the established mode in {@link #sourceMode} if that mode doesn't + * have the {@link QueueSubscription#BOUNDARY} flag set. + * <p> + * If the upstream doesn't support fusion ({@link #qs} is null), the method + * returns {@link QueueSubscription#NONE}. + * @param mode the fusion mode requested + * @return the established fusion mode + */ + protected final int transitiveBoundaryFusion(int mode) { + QueueSubscription<T> qs = this.qs; + if (qs != null) { + if ((mode & BOUNDARY) == 0) { + int m = qs.requestFusion(mode); + if (m != NONE) { + sourceMode = m; + } + return m; + } + } + return NONE; + } + + // -------------------------------------------------------------- + // Default implementation of the RS and QS protocol (can be overridden) + // -------------------------------------------------------------- + + @Override + public void request(long n) { + upstream.request(n); + } + + @Override + public void cancel() { + upstream.cancel(); + } + + @Override + public boolean isEmpty() { + return qs.isEmpty(); + } + + @Override + public void clear() { + qs.clear(); + } + + // ----------------------------------------------------------- + // The rest of the Queue interface methods shouldn't be called + // ----------------------------------------------------------- + + @Override + public final boolean offer(R e) { + throw new UnsupportedOperationException("Should not be called!"); + } + + @Override + public final boolean offer(R v1, R v2) { + throw new UnsupportedOperationException("Should not be called!"); + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/subscribers/BlockingBaseSubscriber.java b/src/main/java/io/reactivex/rxjava3/internal/subscribers/BlockingBaseSubscriber.java new file mode 100644 index 0000000000..68fa0df3eb --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/subscribers/BlockingBaseSubscriber.java @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.subscribers; + +import java.util.concurrent.CountDownLatch; + +import org.reactivestreams.Subscription; + +import io.reactivex.rxjava3.core.FlowableSubscriber; +import io.reactivex.rxjava3.internal.subscriptions.SubscriptionHelper; +import io.reactivex.rxjava3.internal.util.*; + +public abstract class BlockingBaseSubscriber<T> extends CountDownLatch +implements FlowableSubscriber<T> { + + T value; + Throwable error; + + Subscription upstream; + + volatile boolean cancelled; + + public BlockingBaseSubscriber() { + super(1); + } + + @Override + public final void onSubscribe(Subscription s) { + if (SubscriptionHelper.validate(this.upstream, s)) { + this.upstream = s; + if (!cancelled) { + s.request(Long.MAX_VALUE); + if (cancelled) { + this.upstream = SubscriptionHelper.CANCELLED; + s.cancel(); + } + } + } + } + + @Override + public final void onComplete() { + countDown(); + } + + /** + * Block until the first value arrives and return it, otherwise + * return null for an empty source and rethrow any exception. + * @return the first value or null if the source is empty + */ + public final T blockingGet() { + if (getCount() != 0) { + try { + BlockingHelper.verifyNonBlocking(); + await(); + } catch (InterruptedException ex) { + Subscription s = this.upstream; + this.upstream = SubscriptionHelper.CANCELLED; + if (s != null) { + s.cancel(); + } + throw ExceptionHelper.wrapOrThrow(ex); + } + } + + Throwable e = error; + if (e != null) { + throw ExceptionHelper.wrapOrThrow(e); + } + return value; + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/subscribers/BlockingFirstSubscriber.java b/src/main/java/io/reactivex/rxjava3/internal/subscribers/BlockingFirstSubscriber.java new file mode 100644 index 0000000000..1226eca845 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/subscribers/BlockingFirstSubscriber.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.subscribers; + +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +/** + * Blocks until the upstream signals its first value or completes. + * + * @param <T> the value type + */ +public final class BlockingFirstSubscriber<T> extends BlockingBaseSubscriber<T> { + + @Override + public void onNext(T t) { + if (value == null) { + value = t; + upstream.cancel(); + countDown(); + } + } + + @Override + public void onError(Throwable t) { + if (value == null) { + error = t; + } else { + RxJavaPlugins.onError(t); + } + countDown(); + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/subscribers/BlockingLastSubscriber.java b/src/main/java/io/reactivex/rxjava3/internal/subscribers/BlockingLastSubscriber.java new file mode 100644 index 0000000000..05be9d79fa --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/subscribers/BlockingLastSubscriber.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.subscribers; + +/** + * Blocks until the upstream signals its last value or completes. + * + * @param <T> the value type + */ +public final class BlockingLastSubscriber<T> extends BlockingBaseSubscriber<T> { + + @Override + public void onNext(T t) { + value = t; + } + + @Override + public void onError(Throwable t) { + value = null; + error = t; + countDown(); + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/subscribers/BlockingSubscriber.java b/src/main/java/io/reactivex/rxjava3/internal/subscribers/BlockingSubscriber.java new file mode 100644 index 0000000000..4617833fc4 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/subscribers/BlockingSubscriber.java @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.subscribers; + +import java.util.Queue; +import java.util.concurrent.atomic.AtomicReference; + +import org.reactivestreams.Subscription; + +import io.reactivex.rxjava3.core.FlowableSubscriber; +import io.reactivex.rxjava3.internal.subscriptions.SubscriptionHelper; +import io.reactivex.rxjava3.internal.util.NotificationLite; + +public final class BlockingSubscriber<T> extends AtomicReference<Subscription> implements FlowableSubscriber<T>, Subscription { + + private static final long serialVersionUID = -4875965440900746268L; + + public static final Object TERMINATED = new Object(); + + final Queue<Object> queue; + + public BlockingSubscriber(Queue<Object> queue) { + this.queue = queue; + } + + @Override + public void onSubscribe(Subscription s) { + if (SubscriptionHelper.setOnce(this, s)) { + queue.offer(NotificationLite.subscription(this)); + } + } + + @Override + public void onNext(T t) { + queue.offer(NotificationLite.next(t)); + } + + @Override + public void onError(Throwable t) { + queue.offer(NotificationLite.error(t)); + } + + @Override + public void onComplete() { + queue.offer(NotificationLite.complete()); + } + + @Override + public void request(long n) { + get().request(n); + } + + @Override + public void cancel() { + if (SubscriptionHelper.cancel(this)) { + queue.offer(TERMINATED); + } + } + + public boolean isCancelled() { + return get() == SubscriptionHelper.CANCELLED; + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/subscribers/BoundedSubscriber.java b/src/main/java/io/reactivex/rxjava3/internal/subscribers/BoundedSubscriber.java new file mode 100644 index 0000000000..a55f4ae4fa --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/subscribers/BoundedSubscriber.java @@ -0,0 +1,139 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.subscribers; + +import java.util.concurrent.atomic.AtomicReference; + +import org.reactivestreams.Subscription; + +import io.reactivex.rxjava3.core.FlowableSubscriber; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.internal.subscriptions.SubscriptionHelper; +import io.reactivex.rxjava3.observers.LambdaConsumerIntrospection; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +public final class BoundedSubscriber<T> extends AtomicReference<Subscription> + implements FlowableSubscriber<T>, Subscription, Disposable, LambdaConsumerIntrospection { + + private static final long serialVersionUID = -7251123623727029452L; + final Consumer<? super T> onNext; + final Consumer<? super Throwable> onError; + final Action onComplete; + final Consumer<? super Subscription> onSubscribe; + + final int bufferSize; + int consumed; + final int limit; + + public BoundedSubscriber(Consumer<? super T> onNext, Consumer<? super Throwable> onError, + Action onComplete, Consumer<? super Subscription> onSubscribe, int bufferSize) { + super(); + this.onNext = onNext; + this.onError = onError; + this.onComplete = onComplete; + this.onSubscribe = onSubscribe; + this.bufferSize = bufferSize; + this.limit = bufferSize - (bufferSize >> 2); + } + + @Override + public void onSubscribe(Subscription s) { + if (SubscriptionHelper.setOnce(this, s)) { + try { + onSubscribe.accept(this); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + s.cancel(); + onError(e); + } + } + } + + @Override + public void onNext(T t) { + if (!isDisposed()) { + try { + onNext.accept(t); + + int c = consumed + 1; + if (c == limit) { + consumed = 0; + get().request(limit); + } else { + consumed = c; + } + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + get().cancel(); + onError(e); + } + } + } + + @Override + public void onError(Throwable t) { + if (get() != SubscriptionHelper.CANCELLED) { + lazySet(SubscriptionHelper.CANCELLED); + try { + onError.accept(t); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + RxJavaPlugins.onError(new CompositeException(t, e)); + } + } else { + RxJavaPlugins.onError(t); + } + } + + @Override + public void onComplete() { + if (get() != SubscriptionHelper.CANCELLED) { + lazySet(SubscriptionHelper.CANCELLED); + try { + onComplete.run(); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + RxJavaPlugins.onError(e); + } + } + } + + @Override + public void dispose() { + cancel(); + } + + @Override + public boolean isDisposed() { + return get() == SubscriptionHelper.CANCELLED; + } + + @Override + public void request(long n) { + get().request(n); + } + + @Override + public void cancel() { + SubscriptionHelper.cancel(this); + } + + @Override + public boolean hasCustomOnError() { + return onError != Functions.ON_ERROR_MISSING; + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/subscribers/DeferredScalarSubscriber.java b/src/main/java/io/reactivex/rxjava3/internal/subscribers/DeferredScalarSubscriber.java new file mode 100644 index 0000000000..11123962f2 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/subscribers/DeferredScalarSubscriber.java @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.subscribers; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.FlowableSubscriber; +import io.reactivex.rxjava3.internal.subscriptions.*; + +/** + * A subscriber, extending a DeferredScalarSubscription, + * that is unbounded-in and can generate 0 or 1 resulting value. + * @param <T> the input value type + * @param <R> the output value type + */ +public abstract class DeferredScalarSubscriber<T, R> extends DeferredScalarSubscription<R> +implements FlowableSubscriber<T> { + + private static final long serialVersionUID = 2984505488220891551L; + + /** The upstream subscription. */ + protected Subscription upstream; + + /** Can indicate if there was at least on onNext call. */ + protected boolean hasValue; + + /** + * Creates a DeferredScalarSubscriber instance and wraps a downstream Subscriber. + * @param downstream the downstream subscriber, not null (not verified) + */ + public DeferredScalarSubscriber(Subscriber<? super R> downstream) { + super(downstream); + } + + @Override + public void onSubscribe(Subscription s) { + if (SubscriptionHelper.validate(this.upstream, s)) { + this.upstream = s; + + downstream.onSubscribe(this); + + s.request(Long.MAX_VALUE); + } + } + + @Override + public void onError(Throwable t) { + value = null; + downstream.onError(t); + } + + @Override + public void onComplete() { + if (hasValue) { + complete(value); + } else { + downstream.onComplete(); + } + } + + @Override + public void cancel() { + super.cancel(); + upstream.cancel(); + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/subscribers/DisposableAutoReleaseSubscriber.java b/src/main/java/io/reactivex/rxjava3/internal/subscribers/DisposableAutoReleaseSubscriber.java new file mode 100644 index 0000000000..3b9d2d99b3 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/subscribers/DisposableAutoReleaseSubscriber.java @@ -0,0 +1,151 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +/* + * Copyright 2016-2019 David Karnok + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.reactivex.rxjava3.internal.subscribers; + +import java.util.concurrent.atomic.AtomicReference; + +import org.reactivestreams.Subscription; + +import io.reactivex.rxjava3.core.FlowableSubscriber; +import io.reactivex.rxjava3.disposables.*; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.internal.subscriptions.SubscriptionHelper; +import io.reactivex.rxjava3.observers.LambdaConsumerIntrospection; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +/** + * Wraps lambda callbacks and when the upstream terminates or this subscriber gets disposed, + * removes itself from a {@link io.reactivex.rxjava3.disposables.CompositeDisposable}. + * <p>History: 0.18.0 @ RxJavaExtensions + * @param <T> the element type consumed + * @since 3.1.0 + */ +public final class DisposableAutoReleaseSubscriber<T> +extends AtomicReference<Subscription> +implements FlowableSubscriber<T>, Disposable, LambdaConsumerIntrospection { + + private static final long serialVersionUID = 8924480688481408726L; + + final AtomicReference<DisposableContainer> composite; + + final Consumer<? super T> onNext; + + final Consumer<? super Throwable> onError; + + final Action onComplete; + + public DisposableAutoReleaseSubscriber( + DisposableContainer composite, + Consumer<? super T> onNext, + Consumer<? super Throwable> onError, + Action onComplete + ) { + this.onNext = onNext; + this.onError = onError; + this.onComplete = onComplete; + this.composite = new AtomicReference<>(composite); + } + + @Override + public void onNext(T t) { + if (get() != SubscriptionHelper.CANCELLED) { + try { + onNext.accept(t); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + get().cancel(); + onError(e); + } + } + } + + @Override + public void onError(Throwable t) { + if (get() != SubscriptionHelper.CANCELLED) { + lazySet(SubscriptionHelper.CANCELLED); + try { + onError.accept(t); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + RxJavaPlugins.onError(new CompositeException(t, e)); + } + } else { + RxJavaPlugins.onError(t); + } + removeSelf(); + } + + @Override + public void onComplete() { + if (get() != SubscriptionHelper.CANCELLED) { + lazySet(SubscriptionHelper.CANCELLED); + try { + onComplete.run(); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + RxJavaPlugins.onError(e); + } + } + removeSelf(); + } + + @Override + public void dispose() { + SubscriptionHelper.cancel(this); + removeSelf(); + } + + void removeSelf() { + DisposableContainer c = composite.getAndSet(null); + if (c != null) { + c.delete(this); + } + } + + @Override + public boolean isDisposed() { + return SubscriptionHelper.CANCELLED == get(); + } + + @Override + public void onSubscribe(Subscription s) { + if (SubscriptionHelper.setOnce(this, s)) { + s.request(Long.MAX_VALUE); + } + } + + @Override + public boolean hasCustomOnError() { + return onError != Functions.ON_ERROR_MISSING; + } + +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/subscribers/ForEachWhileSubscriber.java b/src/main/java/io/reactivex/rxjava3/internal/subscribers/ForEachWhileSubscriber.java new file mode 100644 index 0000000000..cb5ff01a13 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/subscribers/ForEachWhileSubscriber.java @@ -0,0 +1,113 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.subscribers; + +import java.util.concurrent.atomic.AtomicReference; + +import org.reactivestreams.Subscription; + +import io.reactivex.rxjava3.core.FlowableSubscriber; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.subscriptions.SubscriptionHelper; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +public final class ForEachWhileSubscriber<T> +extends AtomicReference<Subscription> +implements FlowableSubscriber<T>, Disposable { + + private static final long serialVersionUID = -4403180040475402120L; + + final Predicate<? super T> onNext; + + final Consumer<? super Throwable> onError; + + final Action onComplete; + + boolean done; + + public ForEachWhileSubscriber(Predicate<? super T> onNext, + Consumer<? super Throwable> onError, Action onComplete) { + this.onNext = onNext; + this.onError = onError; + this.onComplete = onComplete; + } + + @Override + public void onSubscribe(Subscription s) { + SubscriptionHelper.setOnce(this, s, Long.MAX_VALUE); + } + + @Override + public void onNext(T t) { + if (done) { + return; + } + + boolean b; + try { + b = onNext.test(t); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + dispose(); + onError(ex); + return; + } + + if (!b) { + dispose(); + onComplete(); + } + } + + @Override + public void onError(Throwable t) { + if (done) { + RxJavaPlugins.onError(t); + return; + } + done = true; + try { + onError.accept(t); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + RxJavaPlugins.onError(new CompositeException(t, ex)); + } + } + + @Override + public void onComplete() { + if (done) { + return; + } + done = true; + try { + onComplete.run(); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + RxJavaPlugins.onError(ex); + } + } + + @Override + public void dispose() { + SubscriptionHelper.cancel(this); + } + + @Override + public boolean isDisposed() { + return this.get() == SubscriptionHelper.CANCELLED; + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/subscribers/FutureSubscriber.java b/src/main/java/io/reactivex/rxjava3/internal/subscribers/FutureSubscriber.java new file mode 100644 index 0000000000..145195e2cc --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/subscribers/FutureSubscriber.java @@ -0,0 +1,167 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.subscribers; + +import static io.reactivex.rxjava3.internal.util.ExceptionHelper.timeoutMessage; + +import java.util.NoSuchElementException; +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicReference; + +import org.reactivestreams.Subscription; + +import io.reactivex.rxjava3.annotations.NonNull; +import io.reactivex.rxjava3.core.FlowableSubscriber; +import io.reactivex.rxjava3.internal.subscriptions.SubscriptionHelper; +import io.reactivex.rxjava3.internal.util.BlockingHelper; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +/** + * A Subscriber + Future that expects exactly one upstream value and provides it + * via the (blocking) Future API. + * + * @param <T> the value type + */ +public final class FutureSubscriber<T> extends CountDownLatch +implements FlowableSubscriber<T>, Future<T>, Subscription { + + T value; + Throwable error; + + final AtomicReference<Subscription> upstream; + + public FutureSubscriber() { + super(1); + this.upstream = new AtomicReference<>(); + } + + @Override + public boolean cancel(boolean mayInterruptIfRunning) { + for (;;) { + Subscription a = upstream.get(); + if (a == this || a == SubscriptionHelper.CANCELLED) { + return false; + } + + if (upstream.compareAndSet(a, SubscriptionHelper.CANCELLED)) { + if (a != null) { + a.cancel(); + } + countDown(); + return true; + } + } + } + + @Override + public boolean isCancelled() { + return upstream.get() == SubscriptionHelper.CANCELLED; + } + + @Override + public boolean isDone() { + return getCount() == 0; + } + + @Override + public T get() throws InterruptedException, ExecutionException { + if (getCount() != 0) { + BlockingHelper.verifyNonBlocking(); + await(); + } + + if (isCancelled()) { + throw new CancellationException(); + } + Throwable ex = error; + if (ex != null) { + throw new ExecutionException(ex); + } + return value; + } + + @Override + public T get(long timeout, @NonNull TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException { + if (getCount() != 0) { + BlockingHelper.verifyNonBlocking(); + if (!await(timeout, unit)) { + throw new TimeoutException(timeoutMessage(timeout, unit)); + } + } + + if (isCancelled()) { + throw new CancellationException(); + } + + Throwable ex = error; + if (ex != null) { + throw new ExecutionException(ex); + } + return value; + } + + @Override + public void onSubscribe(Subscription s) { + SubscriptionHelper.setOnce(this.upstream, s, Long.MAX_VALUE); + } + + @Override + public void onNext(T t) { + if (value != null) { + upstream.get().cancel(); + onError(new IndexOutOfBoundsException("More than one element received")); + return; + } + value = t; + } + + @Override + public void onError(Throwable t) { + if (error == null) { + Subscription a = upstream.get(); + if (a != this && a != SubscriptionHelper.CANCELLED + && upstream.compareAndSet(a, this)) { + error = t; + countDown(); + return; + } + } + RxJavaPlugins.onError(t); + } + + @Override + public void onComplete() { + if (value == null) { + onError(new NoSuchElementException("The source is empty")); + return; + } + Subscription a = upstream.get(); + if (a == this || a == SubscriptionHelper.CANCELLED) { + return; + } + if (upstream.compareAndSet(a, this)) { + countDown(); + } + } + + @Override + public void cancel() { + // ignoring as `this` means a finished Subscription only + } + + @Override + public void request(long n) { + // ignoring as `this` means a finished Subscription only + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/subscribers/InnerQueuedSubscriber.java b/src/main/java/io/reactivex/rxjava3/internal/subscribers/InnerQueuedSubscriber.java new file mode 100644 index 0000000000..f946ec7deb --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/subscribers/InnerQueuedSubscriber.java @@ -0,0 +1,135 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.subscribers; + +import java.util.concurrent.atomic.AtomicReference; + +import org.reactivestreams.Subscription; + +import io.reactivex.rxjava3.core.FlowableSubscriber; +import io.reactivex.rxjava3.internal.subscriptions.SubscriptionHelper; +import io.reactivex.rxjava3.internal.util.QueueDrainHelper; +import io.reactivex.rxjava3.operators.QueueSubscription; +import io.reactivex.rxjava3.operators.SimpleQueue; + +/** + * Subscriber that can fuse with the upstream and calls a support interface + * whenever an event is available. + * + * @param <T> the value type + */ +public final class InnerQueuedSubscriber<T> +extends AtomicReference<Subscription> +implements FlowableSubscriber<T>, Subscription { + + private static final long serialVersionUID = 22876611072430776L; + + final InnerQueuedSubscriberSupport<T> parent; + + final int prefetch; + + final int limit; + + volatile SimpleQueue<T> queue; + + volatile boolean done; + + long produced; + + int fusionMode; + + public InnerQueuedSubscriber(InnerQueuedSubscriberSupport<T> parent, int prefetch) { + this.parent = parent; + this.prefetch = prefetch; + this.limit = prefetch - (prefetch >> 2); + } + + @Override + public void onSubscribe(Subscription s) { + if (SubscriptionHelper.setOnce(this, s)) { + if (s instanceof QueueSubscription) { + @SuppressWarnings("unchecked") + QueueSubscription<T> qs = (QueueSubscription<T>) s; + + int m = qs.requestFusion(QueueSubscription.ANY); + if (m == QueueSubscription.SYNC) { + fusionMode = m; + queue = qs; + done = true; + parent.innerComplete(this); + return; + } + if (m == QueueSubscription.ASYNC) { + fusionMode = m; + queue = qs; + QueueDrainHelper.request(s, prefetch); + return; + } + } + + queue = QueueDrainHelper.createQueue(prefetch); + + QueueDrainHelper.request(s, prefetch); + } + } + + @Override + public void onNext(T t) { + if (fusionMode == QueueSubscription.NONE) { + parent.innerNext(this, t); + } else { + parent.drain(); + } + } + + @Override + public void onError(Throwable t) { + parent.innerError(this, t); + } + + @Override + public void onComplete() { + parent.innerComplete(this); + } + + @Override + public void request(long n) { + if (fusionMode != QueueSubscription.SYNC) { + long p = produced + n; + if (p >= limit) { + produced = 0L; + get().request(p); + } else { + produced = p; + } + } + } + + @Override + public void cancel() { + SubscriptionHelper.cancel(this); + } + + public boolean isDone() { + return done; + } + + public void setDone() { + this.done = true; + } + + public SimpleQueue<T> queue() { + return queue; + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/subscribers/InnerQueuedSubscriberSupport.java b/src/main/java/io/reactivex/rxjava3/internal/subscribers/InnerQueuedSubscriberSupport.java new file mode 100644 index 0000000000..3844c5b981 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/subscribers/InnerQueuedSubscriberSupport.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.subscribers; + +/** + * Interface to allow the InnerQueuedSubscriber to call back a parent + * with signals. + * + * @param <T> the value type + */ +public interface InnerQueuedSubscriberSupport<T> { + + void innerNext(InnerQueuedSubscriber<T> inner, T value); + + void innerError(InnerQueuedSubscriber<T> inner, Throwable e); + + void innerComplete(InnerQueuedSubscriber<T> inner); + + void drain(); +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/subscribers/LambdaSubscriber.java b/src/main/java/io/reactivex/rxjava3/internal/subscribers/LambdaSubscriber.java new file mode 100644 index 0000000000..f137237e97 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/subscribers/LambdaSubscriber.java @@ -0,0 +1,126 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.subscribers; + +import java.util.concurrent.atomic.AtomicReference; + +import org.reactivestreams.Subscription; + +import io.reactivex.rxjava3.core.FlowableSubscriber; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.internal.subscriptions.SubscriptionHelper; +import io.reactivex.rxjava3.observers.LambdaConsumerIntrospection; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +public final class LambdaSubscriber<T> extends AtomicReference<Subscription> + implements FlowableSubscriber<T>, Subscription, Disposable, LambdaConsumerIntrospection { + + private static final long serialVersionUID = -7251123623727029452L; + final Consumer<? super T> onNext; + final Consumer<? super Throwable> onError; + final Action onComplete; + final Consumer<? super Subscription> onSubscribe; + + public LambdaSubscriber(Consumer<? super T> onNext, Consumer<? super Throwable> onError, + Action onComplete, + Consumer<? super Subscription> onSubscribe) { + super(); + this.onNext = onNext; + this.onError = onError; + this.onComplete = onComplete; + this.onSubscribe = onSubscribe; + } + + @Override + public void onSubscribe(Subscription s) { + if (SubscriptionHelper.setOnce(this, s)) { + try { + onSubscribe.accept(this); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + s.cancel(); + onError(ex); + } + } + } + + @Override + public void onNext(T t) { + if (!isDisposed()) { + try { + onNext.accept(t); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + get().cancel(); + onError(e); + } + } + } + + @Override + public void onError(Throwable t) { + if (get() != SubscriptionHelper.CANCELLED) { + lazySet(SubscriptionHelper.CANCELLED); + try { + onError.accept(t); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + RxJavaPlugins.onError(new CompositeException(t, e)); + } + } else { + RxJavaPlugins.onError(t); + } + } + + @Override + public void onComplete() { + if (get() != SubscriptionHelper.CANCELLED) { + lazySet(SubscriptionHelper.CANCELLED); + try { + onComplete.run(); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + RxJavaPlugins.onError(e); + } + } + } + + @Override + public void dispose() { + cancel(); + } + + @Override + public boolean isDisposed() { + return get() == SubscriptionHelper.CANCELLED; + } + + @Override + public void request(long n) { + get().request(n); + } + + @Override + public void cancel() { + SubscriptionHelper.cancel(this); + } + + @Override + public boolean hasCustomOnError() { + return onError != Functions.ON_ERROR_MISSING; + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/subscribers/QueueDrainSubscriber.java b/src/main/java/io/reactivex/rxjava3/internal/subscribers/QueueDrainSubscriber.java new file mode 100644 index 0000000000..8fb7f55295 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/subscribers/QueueDrainSubscriber.java @@ -0,0 +1,196 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.subscribers; + +import java.util.concurrent.atomic.*; + +import org.reactivestreams.Subscriber; + +import io.reactivex.rxjava3.core.FlowableSubscriber; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.MissingBackpressureException; +import io.reactivex.rxjava3.internal.subscriptions.SubscriptionHelper; +import io.reactivex.rxjava3.internal.util.*; +import io.reactivex.rxjava3.operators.SimplePlainQueue; + +/** + * Abstract base class for subscribers that hold another subscriber, a queue + * and requires queue-drain behavior. + * + * @param <T> the source type to which this subscriber will be subscribed + * @param <U> the value type in the queue + * @param <V> the value type the child subscriber accepts + */ +public abstract class QueueDrainSubscriber<T, U, V> extends QueueDrainSubscriberPad4 implements FlowableSubscriber<T>, QueueDrain<U, V> { + + protected final Subscriber<? super V> downstream; + + protected final SimplePlainQueue<U> queue; + + protected volatile boolean cancelled; + + protected volatile boolean done; + protected Throwable error; + + public QueueDrainSubscriber(Subscriber<? super V> actual, SimplePlainQueue<U> queue) { + this.downstream = actual; + this.queue = queue; + } + + @Override + public final boolean cancelled() { + return cancelled; + } + + @Override + public final boolean done() { + return done; + } + + @Override + public final boolean enter() { + return wip.getAndIncrement() == 0; + } + + public final boolean fastEnter() { + return wip.get() == 0 && wip.compareAndSet(0, 1); + } + + protected final void fastPathEmitMax(U value, boolean delayError, Disposable dispose) { + final Subscriber<? super V> s = downstream; + final SimplePlainQueue<U> q = queue; + + if (fastEnter()) { + long r = requested.get(); + if (r != 0L) { + if (accept(s, value)) { + if (r != Long.MAX_VALUE) { + produced(1); + } + } + if (leave(-1) == 0) { + return; + } + } else { + dispose.dispose(); + s.onError(MissingBackpressureException.createDefault()); + return; + } + } else { + q.offer(value); + if (!enter()) { + return; + } + } + QueueDrainHelper.drainMaxLoop(q, s, delayError, dispose, this); + } + + protected final void fastPathOrderedEmitMax(U value, boolean delayError, Disposable dispose) { + final Subscriber<? super V> s = downstream; + final SimplePlainQueue<U> q = queue; + + if (fastEnter()) { + long r = requested.get(); + if (r != 0L) { + if (q.isEmpty()) { + if (accept(s, value)) { + if (r != Long.MAX_VALUE) { + produced(1); + } + } + if (leave(-1) == 0) { + return; + } + } else { + q.offer(value); + } + } else { + cancelled = true; + dispose.dispose(); + s.onError(MissingBackpressureException.createDefault()); + return; + } + } else { + q.offer(value); + if (!enter()) { + return; + } + } + QueueDrainHelper.drainMaxLoop(q, s, delayError, dispose, this); + } + + @Override + public boolean accept(Subscriber<? super V> a, U v) { + return false; + } + + @Override + public final Throwable error() { + return error; + } + + @Override + public final int leave(int m) { + return wip.addAndGet(m); + } + + @Override + public final long requested() { + return requested.get(); + } + + @Override + public final long produced(long n) { + return requested.addAndGet(-n); + } + + public final void requested(long n) { + if (SubscriptionHelper.validate(n)) { + BackpressureHelper.add(requested, n); + } + } + +} + +// ------------------------------------------------------------------- +// Padding superclasses +//------------------------------------------------------------------- + +/** Pads the header away from other fields. */ +class QueueDrainSubscriberPad0 { + volatile long p1, p2, p3, p4, p5, p6, p7; + volatile long p8, p9, p10, p11, p12, p13, p14, p15; +} + +/** The WIP counter. */ +class QueueDrainSubscriberWip extends QueueDrainSubscriberPad0 { + final AtomicInteger wip = new AtomicInteger(); +} + +/** Pads away the wip from the other fields. */ +class QueueDrainSubscriberPad2 extends QueueDrainSubscriberWip { + volatile long p1a, p2a, p3a, p4a, p5a, p6a, p7a; + volatile long p8a, p9a, p10a, p11a, p12a, p13a, p14a, p15a; +} + +/** Contains the requested field. */ +class QueueDrainSubscriberPad3 extends QueueDrainSubscriberPad2 { + final AtomicLong requested = new AtomicLong(); +} + +/** Pads away the requested from the other fields. */ +class QueueDrainSubscriberPad4 extends QueueDrainSubscriberPad3 { + volatile long q1, q2, q3, q4, q5, q6, q7; + volatile long q8, q9, q10, q11, q12, q13, q14, q15; +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/subscribers/SinglePostCompleteSubscriber.java b/src/main/java/io/reactivex/rxjava3/internal/subscribers/SinglePostCompleteSubscriber.java new file mode 100644 index 0000000000..ceec9f6b90 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/subscribers/SinglePostCompleteSubscriber.java @@ -0,0 +1,126 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.subscribers; + +import java.util.concurrent.atomic.AtomicLong; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.FlowableSubscriber; +import io.reactivex.rxjava3.internal.subscriptions.SubscriptionHelper; +import io.reactivex.rxjava3.internal.util.BackpressureHelper; + +/** + * Relays signals from upstream according to downstream requests and allows + * signalling a final value followed by onComplete in a backpressure-aware manner. + * + * @param <T> the input value type + * @param <R> the output value type + */ +public abstract class SinglePostCompleteSubscriber<T, R> extends AtomicLong implements FlowableSubscriber<T>, Subscription { + private static final long serialVersionUID = 7917814472626990048L; + + /** The downstream consumer. */ + protected final Subscriber<? super R> downstream; + + /** The upstream subscription. */ + protected Subscription upstream; + + /** The last value stored in case there is no request for it. */ + protected R value; + + /** Number of values emitted so far. */ + protected long produced; + + /** Masks out the 2^63 bit indicating a completed state. */ + static final long COMPLETE_MASK = Long.MIN_VALUE; + /** Masks out the lower 63 bit holding the current request amount. */ + static final long REQUEST_MASK = Long.MAX_VALUE; + + public SinglePostCompleteSubscriber(Subscriber<? super R> downstream) { + this.downstream = downstream; + } + + @Override + public void onSubscribe(Subscription s) { + if (SubscriptionHelper.validate(this.upstream, s)) { + this.upstream = s; + downstream.onSubscribe(this); + } + } + + /** + * Signals the given value and an onComplete if the downstream is ready to receive the final value. + * @param n the value to emit + */ + protected final void complete(R n) { + long p = produced; + if (p != 0) { + BackpressureHelper.produced(this, p); + } + + for (;;) { + long r = get(); + if ((r & COMPLETE_MASK) != 0) { + onDrop(n); + return; + } + if ((r & REQUEST_MASK) != 0) { + lazySet(COMPLETE_MASK + 1); + downstream.onNext(n); + downstream.onComplete(); + return; + } + value = n; + if (compareAndSet(0, COMPLETE_MASK)) { + return; + } + value = null; + } + } + + /** + * Called in case of multiple calls to complete. + * @param n the value dropped + */ + protected void onDrop(R n) { + // default is no-op + } + + @Override + public final void request(long n) { + if (SubscriptionHelper.validate(n)) { + for (;;) { + long r = get(); + if ((r & COMPLETE_MASK) != 0) { + if (compareAndSet(COMPLETE_MASK, COMPLETE_MASK + 1)) { + downstream.onNext(value); + downstream.onComplete(); + } + break; + } + long u = BackpressureHelper.addCap(r, n); + if (compareAndSet(r, u)) { + upstream.request(n); + break; + } + } + } + } + + @Override + public void cancel() { + upstream.cancel(); + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/subscribers/StrictSubscriber.java b/src/main/java/io/reactivex/rxjava3/internal/subscribers/StrictSubscriber.java new file mode 100644 index 0000000000..265acd60a5 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/subscribers/StrictSubscriber.java @@ -0,0 +1,111 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.subscribers; + +import java.util.concurrent.atomic.*; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.FlowableSubscriber; +import io.reactivex.rxjava3.internal.subscriptions.SubscriptionHelper; +import io.reactivex.rxjava3.internal.util.*; + +/** + * Ensures that the event flow between the upstream and downstream follow + * the Reactive-Streams 1.0 specification by honoring the 3 additional rules + * (which are omitted in standard operators due to performance reasons). + * <ul> + * <li>§1.3: onNext should not be called concurrently until onSubscribe returns</li> + * <li>§2.3: onError or onComplete must not call cancel</li> + * <li>§3.9: negative requests should emit an onError(IllegalArgumentException)</li> + * </ul> + * In addition, if rule §2.12 (onSubscribe must be called at most once) is violated, + * the sequence is cancelled an onError(IllegalStateException) is emitted. + * @param <T> the value type + * @since 2.0.7 + */ +public class StrictSubscriber<T> +extends AtomicInteger +implements FlowableSubscriber<T>, Subscription { + + private static final long serialVersionUID = -4945028590049415624L; + + final Subscriber<? super T> downstream; + + final AtomicThrowable error; + + final AtomicLong requested; + + final AtomicReference<Subscription> upstream; + + final AtomicBoolean once; + + volatile boolean done; + + public StrictSubscriber(Subscriber<? super T> downstream) { + this.downstream = downstream; + this.error = new AtomicThrowable(); + this.requested = new AtomicLong(); + this.upstream = new AtomicReference<>(); + this.once = new AtomicBoolean(); + } + + @Override + public void request(long n) { + if (n <= 0) { + cancel(); + onError(new IllegalArgumentException("§3.9 violated: positive request amount required but it was " + n)); + } else { + SubscriptionHelper.deferredRequest(upstream, requested, n); + } + } + + @Override + public void cancel() { + if (!done) { + SubscriptionHelper.cancel(upstream); + } + } + + @Override + public void onSubscribe(Subscription s) { + if (once.compareAndSet(false, true)) { + + downstream.onSubscribe(this); + + SubscriptionHelper.deferredSetOnce(this.upstream, requested, s); + } else { + s.cancel(); + cancel(); + onError(new IllegalStateException("§2.12 violated: onSubscribe must be called at most once")); + } + } + + @Override + public void onNext(T t) { + HalfSerializer.onNext(downstream, t, this, error); + } + + @Override + public void onError(Throwable t) { + done = true; + HalfSerializer.onError(downstream, t, this, error); + } + + @Override + public void onComplete() { + done = true; + HalfSerializer.onComplete(downstream, this, error); + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/subscribers/SubscriberResourceWrapper.java b/src/main/java/io/reactivex/rxjava3/internal/subscribers/SubscriberResourceWrapper.java new file mode 100644 index 0000000000..df85e95324 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/subscribers/SubscriberResourceWrapper.java @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.subscribers; + +import java.util.concurrent.atomic.AtomicReference; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.FlowableSubscriber; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; +import io.reactivex.rxjava3.internal.subscriptions.SubscriptionHelper; + +public final class SubscriberResourceWrapper<T> extends AtomicReference<Disposable> implements FlowableSubscriber<T>, Disposable, Subscription { + + private static final long serialVersionUID = -8612022020200669122L; + + final Subscriber<? super T> downstream; + + final AtomicReference<Subscription> upstream = new AtomicReference<>(); + + public SubscriberResourceWrapper(Subscriber<? super T> downstream) { + this.downstream = downstream; + } + + @Override + public void onSubscribe(Subscription s) { + if (SubscriptionHelper.setOnce(upstream, s)) { + downstream.onSubscribe(this); + } + } + + @Override + public void onNext(T t) { + downstream.onNext(t); + } + + @Override + public void onError(Throwable t) { + DisposableHelper.dispose(this); + downstream.onError(t); + } + + @Override + public void onComplete() { + DisposableHelper.dispose(this); + downstream.onComplete(); + } + + @Override + public void request(long n) { + if (SubscriptionHelper.validate(n)) { + upstream.get().request(n); + } + } + + @Override + public void dispose() { + SubscriptionHelper.cancel(upstream); + + DisposableHelper.dispose(this); + } + + @Override + public boolean isDisposed() { + return upstream.get() == SubscriptionHelper.CANCELLED; + } + + @Override + public void cancel() { + dispose(); + } + + public void setResource(Disposable resource) { + DisposableHelper.set(this, resource); + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/subscriptions/ArrayCompositeSubscription.java b/src/main/java/io/reactivex/rxjava3/internal/subscriptions/ArrayCompositeSubscription.java new file mode 100644 index 0000000000..ce4b2b5ac8 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/subscriptions/ArrayCompositeSubscription.java @@ -0,0 +1,102 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.subscriptions; + +import java.util.concurrent.atomic.AtomicReferenceArray; + +import org.reactivestreams.Subscription; + +import io.reactivex.rxjava3.disposables.Disposable; + +/** + * A composite disposable with a fixed number of slots. + * + * <p>Note that since the implementation leaks the methods of AtomicReferenceArray, one must be + * careful to only call setResource, replaceResource and dispose on it. All other methods may lead to undefined behavior + * and should be used by internal means only. + */ +public final class ArrayCompositeSubscription extends AtomicReferenceArray<Subscription> implements Disposable { + + private static final long serialVersionUID = 2746389416410565408L; + + public ArrayCompositeSubscription(int capacity) { + super(capacity); + } + + /** + * Sets the resource at the specified index and disposes the old resource. + * @param index the index of the resource to set + * @param resource the new resource + * @return true if the resource has ben set, false if the composite has been disposed + */ + public boolean setResource(int index, Subscription resource) { + for (;;) { + Subscription o = get(index); + if (o == SubscriptionHelper.CANCELLED) { + if (resource != null) { + resource.cancel(); + } + return false; + } + if (compareAndSet(index, o, resource)) { + if (o != null) { + o.cancel(); + } + return true; + } + } + } + + /** + * Replaces the resource at the specified index and returns the old resource. + * @param index the index of the resource to replace + * @param resource the new resource + * @return the old resource, can be null + */ + public Subscription replaceResource(int index, Subscription resource) { + for (;;) { + Subscription o = get(index); + if (o == SubscriptionHelper.CANCELLED) { + if (resource != null) { + resource.cancel(); + } + return null; + } + if (compareAndSet(index, o, resource)) { + return o; + } + } + } + + @Override + public void dispose() { + if (get(0) != SubscriptionHelper.CANCELLED) { + int s = length(); + for (int i = 0; i < s; i++) { + Subscription o = get(i); + if (o != SubscriptionHelper.CANCELLED) { + o = getAndSet(i, SubscriptionHelper.CANCELLED); + if (o != SubscriptionHelper.CANCELLED && o != null) { + o.cancel(); + } + } + } + } + } + + @Override + public boolean isDisposed() { + return get(0) == SubscriptionHelper.CANCELLED; + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/subscriptions/AsyncSubscription.java b/src/main/java/io/reactivex/rxjava3/internal/subscriptions/AsyncSubscription.java new file mode 100644 index 0000000000..d33d05c8d1 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/subscriptions/AsyncSubscription.java @@ -0,0 +1,94 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.subscriptions; + +import java.util.concurrent.atomic.*; + +import org.reactivestreams.Subscription; + +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; + +/** + * A subscription implementation that arbitrates exactly one other Subscription and can + * hold a single disposable resource. + * + * <p>All methods are thread-safe. + */ +public final class AsyncSubscription extends AtomicLong implements Subscription, Disposable { + + private static final long serialVersionUID = 7028635084060361255L; + + final AtomicReference<Subscription> actual; + + final AtomicReference<Disposable> resource; + + public AsyncSubscription() { + resource = new AtomicReference<>(); + actual = new AtomicReference<>(); + } + + public AsyncSubscription(Disposable resource) { + this(); + this.resource.lazySet(resource); + } + + @Override + public void request(long n) { + SubscriptionHelper.deferredRequest(actual, this, n); + } + + @Override + public void cancel() { + dispose(); + } + + @Override + public void dispose() { + SubscriptionHelper.cancel(actual); + DisposableHelper.dispose(resource); + } + + @Override + public boolean isDisposed() { + return actual.get() == SubscriptionHelper.CANCELLED; + } + + /** + * Sets a new resource and disposes the currently held resource. + * @param r the new resource to set + * @return false if this AsyncSubscription has been cancelled/disposed + * @see #replaceResource(Disposable) + */ + public boolean setResource(Disposable r) { + return DisposableHelper.set(resource, r); + } + + /** + * Replaces the currently held resource with the given new one without disposing the old. + * @param r the new resource to set + * @return false if this AsyncSubscription has been cancelled/disposed + */ + public boolean replaceResource(Disposable r) { + return DisposableHelper.replace(resource, r); + } + + /** + * Sets the given subscription if there isn't any subscription held. + * @param s the first and only subscription to set + */ + public void setSubscription(Subscription s) { + SubscriptionHelper.deferredSetOnce(actual, this, s); + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/subscriptions/BasicIntQueueSubscription.java b/src/main/java/io/reactivex/rxjava3/internal/subscriptions/BasicIntQueueSubscription.java new file mode 100644 index 0000000000..46b420354b --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/subscriptions/BasicIntQueueSubscription.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.subscriptions; + +import java.util.concurrent.atomic.AtomicInteger; + +import io.reactivex.rxjava3.annotations.NonNull; +import io.reactivex.rxjava3.operators.QueueSubscription; + +/** + * Base class extending AtomicInteger (wip or request accounting) and QueueSubscription (fusion). + * + * @param <T> the value type + */ +public abstract class BasicIntQueueSubscription<@NonNull T> extends AtomicInteger implements QueueSubscription<T> { + + private static final long serialVersionUID = -6671519529404341862L; + + @Override + public final boolean offer(T e) { + throw new UnsupportedOperationException("Should not be called!"); + } + + @Override + public final boolean offer(T v1, T v2) { + throw new UnsupportedOperationException("Should not be called!"); + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/subscriptions/BasicQueueSubscription.java b/src/main/java/io/reactivex/rxjava3/internal/subscriptions/BasicQueueSubscription.java new file mode 100644 index 0000000000..684bdf4e80 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/subscriptions/BasicQueueSubscription.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.subscriptions; + +import java.util.concurrent.atomic.AtomicLong; + +import io.reactivex.rxjava3.operators.QueueSubscription; + +/** + * Base class extending AtomicLong (wip or request accounting) and QueueSubscription (fusion). + * + * @param <T> the value type + */ +public abstract class BasicQueueSubscription<T> extends AtomicLong implements QueueSubscription<T> { + + private static final long serialVersionUID = -6671519529404341862L; + + @Override + public final boolean offer(T e) { + throw new UnsupportedOperationException("Should not be called!"); + } + + @Override + public final boolean offer(T v1, T v2) { + throw new UnsupportedOperationException("Should not be called!"); + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/subscriptions/BooleanSubscription.java b/src/main/java/io/reactivex/rxjava3/internal/subscriptions/BooleanSubscription.java new file mode 100644 index 0000000000..2d7f01ef05 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/subscriptions/BooleanSubscription.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.subscriptions; + +import java.util.concurrent.atomic.AtomicBoolean; + +import org.reactivestreams.Subscription; + +/** + * Subscription implementation that ignores request but remembers the cancellation + * which can be checked via isCancelled. + */ +public final class BooleanSubscription extends AtomicBoolean implements Subscription { + + private static final long serialVersionUID = -8127758972444290902L; + + @Override + public void request(long n) { + SubscriptionHelper.validate(n); + } + + @Override + public void cancel() { + lazySet(true); + } + + /** + * Returns true if this BooleanSubscription has been cancelled. + * @return true if this BooleanSubscription has been cancelled + */ + public boolean isCancelled() { + return get(); + } + + @Override + public String toString() { + return "BooleanSubscription(cancelled=" + get() + ")"; + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/subscriptions/DeferredScalarSubscription.java b/src/main/java/io/reactivex/rxjava3/internal/subscriptions/DeferredScalarSubscription.java new file mode 100644 index 0000000000..544fac8b58 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/subscriptions/DeferredScalarSubscription.java @@ -0,0 +1,205 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.subscriptions; + +import org.reactivestreams.Subscriber; + +import io.reactivex.rxjava3.annotations.*; + +/** + * A subscription that signals a single value eventually. + * <p> + * Note that the class leaks all methods of {@link java.util.concurrent.atomic.AtomicLong}. + * Use {@link #complete(Object)} to signal the single value. + * <p> + * The this atomic integer stores a bit field:<br> + * bit 0: indicates that there is a value available<br> + * bit 1: indicates that there was a request made<br> + * bit 2: indicates there was a cancellation, exclusively set<br> + * bit 3: indicates in fusion mode but no value yet, exclusively set<br> + * bit 4: indicates in fusion mode and value is available, exclusively set<br> + * bit 5: indicates in fusion mode and value has been consumed, exclusively set<br> + * Where exclusively set means any other bits are 0 when that bit is set. + * @param <T> the value type + */ +public class DeferredScalarSubscription<@NonNull T> extends BasicIntQueueSubscription<T> { + + private static final long serialVersionUID = -2151279923272604993L; + + /** The Subscriber to emit the value to. */ + protected final Subscriber<? super T> downstream; + + /** The value is stored here if there is no request yet or in fusion mode. */ + protected T value; + + /** Indicates this Subscription has no value and not requested yet. */ + static final int NO_REQUEST_NO_VALUE = 0; + /** Indicates this Subscription has a value but not requested yet. */ + static final int NO_REQUEST_HAS_VALUE = 1; + /** Indicates this Subscription has been requested but there is no value yet. */ + static final int HAS_REQUEST_NO_VALUE = 2; + /** Indicates this Subscription has both request and value. */ + static final int HAS_REQUEST_HAS_VALUE = 3; + + /** Indicates the Subscription has been cancelled. */ + static final int CANCELLED = 4; + + /** Indicates this Subscription is in fusion mode and is currently empty. */ + static final int FUSED_EMPTY = 8; + /** Indicates this Subscription is in fusion mode and has a value. */ + static final int FUSED_READY = 16; + /** Indicates this Subscription is in fusion mode and its value has been consumed. */ + static final int FUSED_CONSUMED = 32; + + /** + * Creates a DeferredScalarSubscription by wrapping the given Subscriber. + * @param downstream the Subscriber to wrap, not null (not verified) + */ + public DeferredScalarSubscription(Subscriber<? super T> downstream) { + this.downstream = downstream; + } + + @Override + public final void request(long n) { + if (SubscriptionHelper.validate(n)) { + for (;;) { + int state = get(); + // if the any bits 1-31 are set, we are either in fusion mode (FUSED_*) + // or request has been called (HAS_REQUEST_*) + if ((state & ~NO_REQUEST_HAS_VALUE) != 0) { + return; + } + if (state == NO_REQUEST_HAS_VALUE) { + if (compareAndSet(NO_REQUEST_HAS_VALUE, HAS_REQUEST_HAS_VALUE)) { + T v = value; + if (v != null) { + value = null; + Subscriber<? super T> a = downstream; + a.onNext(v); + if (get() != CANCELLED) { + a.onComplete(); + } + } + } + return; + } + if (compareAndSet(NO_REQUEST_NO_VALUE, HAS_REQUEST_NO_VALUE)) { + return; + } + } + } + } + + /** + * Completes this subscription by indicating the given value should + * be emitted when the first request arrives. + * <p>Make sure this is called exactly once. + * @param v the value to signal, not null (not validated) + */ + public final void complete(T v) { + int state = get(); + for (;;) { + if (state == FUSED_EMPTY) { + value = v; + lazySet(FUSED_READY); + + Subscriber<? super T> a = downstream; + a.onNext(null); + if (get() != CANCELLED) { + a.onComplete(); + } + return; + } + + // if state is >= CANCELLED or bit zero is set (*_HAS_VALUE) case, return + if ((state & ~HAS_REQUEST_NO_VALUE) != 0) { + return; + } + + if (state == HAS_REQUEST_NO_VALUE) { + lazySet(HAS_REQUEST_HAS_VALUE); + Subscriber<? super T> a = downstream; + a.onNext(v); + if (get() != CANCELLED) { + a.onComplete(); + } + return; + } + value = v; + if (compareAndSet(NO_REQUEST_NO_VALUE, NO_REQUEST_HAS_VALUE)) { + return; + } + state = get(); + if (state == CANCELLED) { + value = null; + return; + } + } + } + + @Override + public final int requestFusion(int mode) { + if ((mode & ASYNC) != 0) { + lazySet(FUSED_EMPTY); + return ASYNC; + } + return NONE; + } + + @Nullable + @Override + public final T poll() { + if (get() == FUSED_READY) { + lazySet(FUSED_CONSUMED); + T v = value; + value = null; + return v; + } + return null; + } + + @Override + public final boolean isEmpty() { + return get() != FUSED_READY; + } + + @Override + public final void clear() { + lazySet(FUSED_CONSUMED); + value = null; + } + + @Override + public void cancel() { + set(CANCELLED); + value = null; + } + + /** + * Returns true if this Subscription has been cancelled. + * @return true if this Subscription has been cancelled + */ + public final boolean isCancelled() { + return get() == CANCELLED; + } + + /** + * Atomically sets a cancelled state and returns true if + * the current thread did it successfully. + * @return true if the current thread cancelled + */ + public final boolean tryCancel() { + return getAndSet(CANCELLED) != CANCELLED; + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/subscriptions/EmptySubscription.java b/src/main/java/io/reactivex/rxjava3/internal/subscriptions/EmptySubscription.java new file mode 100644 index 0000000000..6b2c033d5e --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/subscriptions/EmptySubscription.java @@ -0,0 +1,102 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.subscriptions; + +import org.reactivestreams.Subscriber; + +import io.reactivex.rxjava3.annotations.Nullable; +import io.reactivex.rxjava3.operators.QueueSubscription; + +/** + * An empty subscription that does nothing other than validates the request amount. + */ +public enum EmptySubscription implements QueueSubscription<Object> { + /** A singleton, stateless instance. */ + INSTANCE; + + @Override + public void request(long n) { + SubscriptionHelper.validate(n); + } + + @Override + public void cancel() { + // no-op + } + + @Override + public String toString() { + return "EmptySubscription"; + } + + /** + * Sets the empty subscription instance on the subscriber and then + * calls onError with the supplied error. + * + * <p>Make sure this is only called if the subscriber hasn't received a + * subscription already (there is no way of telling this). + * + * @param e the error to deliver to the subscriber + * @param s the target subscriber + */ + public static void error(Throwable e, Subscriber<?> s) { + s.onSubscribe(INSTANCE); + s.onError(e); + } + + /** + * Sets the empty subscription instance on the subscriber and then + * calls onComplete. + * + * <p>Make sure this is only called if the subscriber hasn't received a + * subscription already (there is no way of telling this). + * + * @param s the target subscriber + */ + public static void complete(Subscriber<?> s) { + s.onSubscribe(INSTANCE); + s.onComplete(); + } + + @Nullable + @Override + public Object poll() { + return null; // always empty + } + + @Override + public boolean isEmpty() { + return true; + } + + @Override + public void clear() { + // nothing to do + } + + @Override + public int requestFusion(int mode) { + return mode & ASYNC; // accept async mode: an onComplete or onError will be signalled after anyway + } + + @Override + public boolean offer(Object value) { + throw new UnsupportedOperationException("Should not be called!"); + } + + @Override + public boolean offer(Object v1, Object v2) { + throw new UnsupportedOperationException("Should not be called!"); + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/subscriptions/ScalarSubscription.java b/src/main/java/io/reactivex/rxjava3/internal/subscriptions/ScalarSubscription.java new file mode 100644 index 0000000000..ef1e35e753 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/subscriptions/ScalarSubscription.java @@ -0,0 +1,110 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.subscriptions; + +import java.util.concurrent.atomic.AtomicInteger; + +import org.reactivestreams.Subscriber; + +import io.reactivex.rxjava3.annotations.Nullable; +import io.reactivex.rxjava3.operators.QueueSubscription; + +/** + * A Subscription that holds a constant value and emits it only when requested. + * @param <T> the value type + */ +public final class ScalarSubscription<T> extends AtomicInteger implements QueueSubscription<T> { + + private static final long serialVersionUID = -3830916580126663321L; + /** The single value to emit, set to null. */ + final T value; + /** The actual subscriber. */ + final Subscriber<? super T> subscriber; + + /** No request has been issued yet. */ + static final int NO_REQUEST = 0; + /** Request has been called.*/ + static final int REQUESTED = 1; + /** Cancel has been called. */ + static final int CANCELLED = 2; + + public ScalarSubscription(Subscriber<? super T> subscriber, T value) { + this.subscriber = subscriber; + this.value = value; + } + + @Override + public void request(long n) { + if (!SubscriptionHelper.validate(n)) { + return; + } + if (compareAndSet(NO_REQUEST, REQUESTED)) { + Subscriber<? super T> s = subscriber; + + s.onNext(value); + if (get() != CANCELLED) { + s.onComplete(); + } + } + + } + + @Override + public void cancel() { + lazySet(CANCELLED); + } + + /** + * Returns true if this Subscription was cancelled. + * @return true if this Subscription was cancelled + */ + public boolean isCancelled() { + return get() == CANCELLED; + } + + @Override + public boolean offer(T e) { + throw new UnsupportedOperationException("Should not be called!"); + } + + @Override + public boolean offer(T v1, T v2) { + throw new UnsupportedOperationException("Should not be called!"); + } + + @Nullable + @Override + public T poll() { + if (get() == NO_REQUEST) { + lazySet(REQUESTED); + return value; + } + return null; + } + + @Override + public boolean isEmpty() { + return get() != NO_REQUEST; + } + + @Override + public void clear() { + lazySet(1); + } + + @Override + public int requestFusion(int mode) { + return mode & SYNC; + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/subscriptions/SubscriptionArbiter.java b/src/main/java/io/reactivex/rxjava3/internal/subscriptions/SubscriptionArbiter.java new file mode 100644 index 0000000000..7d964224eb --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/subscriptions/SubscriptionArbiter.java @@ -0,0 +1,273 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.subscriptions; + +import java.util.Objects; +import java.util.concurrent.atomic.*; + +import org.reactivestreams.Subscription; + +import io.reactivex.rxjava3.internal.util.BackpressureHelper; + +/** + * Arbitrates requests and cancellation between Subscriptions. + */ +public class SubscriptionArbiter extends AtomicInteger implements Subscription { + + private static final long serialVersionUID = -2189523197179400958L; + + /** + * The current subscription which may null if no Subscriptions have been set. + */ + Subscription actual; + + /** + * The current outstanding request amount. + */ + long requested; + + final AtomicReference<Subscription> missedSubscription; + + final AtomicLong missedRequested; + + final AtomicLong missedProduced; + + final boolean cancelOnReplace; + + volatile boolean cancelled; + + protected boolean unbounded; + + public SubscriptionArbiter(boolean cancelOnReplace) { + this.cancelOnReplace = cancelOnReplace; + missedSubscription = new AtomicReference<>(); + missedRequested = new AtomicLong(); + missedProduced = new AtomicLong(); + } + + /** + * Atomically sets a new subscription. + * @param s the subscription to set, not null (verified) + */ + public final void setSubscription(Subscription s) { + if (cancelled) { + s.cancel(); + return; + } + + Objects.requireNonNull(s, "s is null"); + + if (get() == 0 && compareAndSet(0, 1)) { + Subscription a = actual; + + if (a != null && cancelOnReplace) { + a.cancel(); + } + + actual = s; + + long r = requested; + + if (decrementAndGet() != 0) { + drainLoop(); + } + + if (r != 0L) { + s.request(r); + } + + return; + } + + Subscription a = missedSubscription.getAndSet(s); + if (a != null && cancelOnReplace) { + a.cancel(); + } + drain(); + } + + @Override + public final void request(long n) { + if (SubscriptionHelper.validate(n)) { + if (unbounded) { + return; + } + if (get() == 0 && compareAndSet(0, 1)) { + long r = requested; + + if (r != Long.MAX_VALUE) { + r = BackpressureHelper.addCap(r, n); + requested = r; + if (r == Long.MAX_VALUE) { + unbounded = true; + } + } + Subscription a = actual; + + if (decrementAndGet() != 0) { + drainLoop(); + } + + if (a != null) { + a.request(n); + } + + return; + } + + BackpressureHelper.add(missedRequested, n); + + drain(); + } + } + + public final void produced(long n) { + if (unbounded) { + return; + } + if (get() == 0 && compareAndSet(0, 1)) { + long r = requested; + + if (r != Long.MAX_VALUE) { + long u = r - n; + if (u < 0L) { + SubscriptionHelper.reportMoreProduced(u); + u = 0; + } + requested = u; + } + + if (decrementAndGet() == 0) { + return; + } + + drainLoop(); + + return; + } + + BackpressureHelper.add(missedProduced, n); + + drain(); + } + + @Override + public void cancel() { + if (!cancelled) { + cancelled = true; + + drain(); + } + } + + final void drain() { + if (getAndIncrement() != 0) { + return; + } + drainLoop(); + } + + final void drainLoop() { + int missed = 1; + + long requestAmount = 0L; + Subscription requestTarget = null; + + for (; ; ) { + + Subscription ms = missedSubscription.get(); + + if (ms != null) { + ms = missedSubscription.getAndSet(null); + } + + long mr = missedRequested.get(); + if (mr != 0L) { + mr = missedRequested.getAndSet(0L); + } + + long mp = missedProduced.get(); + if (mp != 0L) { + mp = missedProduced.getAndSet(0L); + } + + Subscription a = actual; + + if (cancelled) { + if (a != null) { + a.cancel(); + actual = null; + } + if (ms != null) { + ms.cancel(); + } + } else { + long r = requested; + if (r != Long.MAX_VALUE) { + long u = BackpressureHelper.addCap(r, mr); + + if (u != Long.MAX_VALUE) { + long v = u - mp; + if (v < 0L) { + SubscriptionHelper.reportMoreProduced(v); + v = 0; + } + r = v; + } else { + r = u; + } + requested = r; + } + + if (ms != null) { + if (a != null && cancelOnReplace) { + a.cancel(); + } + actual = ms; + if (r != 0L) { + requestAmount = BackpressureHelper.addCap(requestAmount, r); + requestTarget = ms; + } + } else if (a != null && mr != 0L) { + requestAmount = BackpressureHelper.addCap(requestAmount, mr); + requestTarget = a; + } + } + + missed = addAndGet(-missed); + if (missed == 0) { + if (requestAmount != 0L) { + requestTarget.request(requestAmount); + } + return; + } + } + } + + /** + * Returns true if the arbiter runs in unbounded mode. + * @return true if the arbiter runs in unbounded mode + */ + public final boolean isUnbounded() { + return unbounded; + } + + /** + * Returns true if the arbiter has been cancelled. + * @return true if the arbiter has been cancelled + */ + public final boolean isCancelled() { + return cancelled; + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/subscriptions/SubscriptionHelper.java b/src/main/java/io/reactivex/rxjava3/internal/subscriptions/SubscriptionHelper.java new file mode 100644 index 0000000000..922ac9c3c9 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/subscriptions/SubscriptionHelper.java @@ -0,0 +1,254 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.subscriptions; + +import java.util.Objects; +import java.util.concurrent.atomic.*; + +import org.reactivestreams.Subscription; + +import io.reactivex.rxjava3.exceptions.ProtocolViolationException; +import io.reactivex.rxjava3.internal.util.BackpressureHelper; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +/** + * Utility methods to validate Subscriptions in the various onSubscribe calls. + */ +public enum SubscriptionHelper implements Subscription { + /** + * Represents a cancelled Subscription. + * <p>Don't leak this instance! + */ + CANCELLED + ; + + @Override + public void request(long n) { + // deliberately ignored + } + + @Override + public void cancel() { + // deliberately ignored + } + + /** + * Verifies that current is null, next is not null, otherwise signals errors + * to the RxJavaPlugins and returns false. + * @param current the current Subscription, expected to be null + * @param next the next Subscription, expected to be non-null + * @return true if the validation succeeded + */ + public static boolean validate(Subscription current, Subscription next) { + if (next == null) { + RxJavaPlugins.onError(new NullPointerException("next is null")); + return false; + } + if (current != null) { + next.cancel(); + reportSubscriptionSet(); + return false; + } + return true; + } + + /** + * Reports that the subscription is already set to the RxJavaPlugins error handler, + * which is an indication of a onSubscribe management bug. + */ + public static void reportSubscriptionSet() { + RxJavaPlugins.onError(new ProtocolViolationException("Subscription already set!")); + } + + /** + * Validates that the n is positive. + * @param n the request amount + * @return false if n is non-positive. + */ + public static boolean validate(long n) { + if (n <= 0) { + RxJavaPlugins.onError(new IllegalArgumentException("n > 0 required but it was " + n)); + return false; + } + return true; + } + + /** + * Reports to the plugin error handler that there were more values produced than requested, which + * is a sign of internal backpressure handling bug. + * @param n the overproduction amount + */ + public static void reportMoreProduced(long n) { + RxJavaPlugins.onError(new ProtocolViolationException("More produced than requested: " + n)); + } + + /** + * Atomically sets the subscription on the field and cancels the + * previous subscription if any. + * @param field the target field to set the new subscription on + * @param s the new subscription + * @return true if the operation succeeded, false if the target field + * holds the {@link #CANCELLED} instance. + * @see #replace(AtomicReference, Subscription) + */ + public static boolean set(AtomicReference<Subscription> field, Subscription s) { + for (;;) { + Subscription current = field.get(); + if (current == CANCELLED) { + if (s != null) { + s.cancel(); + } + return false; + } + if (field.compareAndSet(current, s)) { + if (current != null) { + current.cancel(); + } + return true; + } + } + } + + /** + * Atomically sets the subscription on the field if it is still null. + * <p>If the field is not null and doesn't contain the {@link #CANCELLED} + * instance, the {@link #reportSubscriptionSet()} is called. + * @param field the target field + * @param s the new subscription to set + * @return true if the operation succeeded, false if the target field was not null. + */ + public static boolean setOnce(AtomicReference<Subscription> field, Subscription s) { + Objects.requireNonNull(s, "s is null"); + if (!field.compareAndSet(null, s)) { + s.cancel(); + if (field.get() != CANCELLED) { + reportSubscriptionSet(); + } + return false; + } + return true; + } + + /** + * Atomically sets the subscription on the field but does not + * cancel the previous subscription. + * @param field the target field to set the new subscription on + * @param s the new subscription + * @return true if the operation succeeded, false if the target field + * holds the {@link #CANCELLED} instance. + * @see #set(AtomicReference, Subscription) + */ + public static boolean replace(AtomicReference<Subscription> field, Subscription s) { + for (;;) { + Subscription current = field.get(); + if (current == CANCELLED) { + if (s != null) { + s.cancel(); + } + return false; + } + if (field.compareAndSet(current, s)) { + return true; + } + } + } + + /** + * Atomically swaps in the common cancelled subscription instance + * and cancels the previous subscription if any. + * @param field the target field to dispose the contents of + * @return true if the swap from the non-cancelled instance to the + * common cancelled instance happened in the caller's thread (allows + * further one-time actions). + */ + public static boolean cancel(AtomicReference<Subscription> field) { + Subscription current = field.get(); + if (current != CANCELLED) { + current = field.getAndSet(CANCELLED); + if (current != CANCELLED) { + if (current != null) { + current.cancel(); + } + return true; + } + } + return false; + } + + /** + * Atomically sets the new Subscription on the field and requests any accumulated amount + * from the requested field. + * @param field the target field for the new Subscription + * @param requested the current requested amount + * @param s the new Subscription, not null (verified) + * @return true if the Subscription was set the first time + */ + public static boolean deferredSetOnce(AtomicReference<Subscription> field, AtomicLong requested, + Subscription s) { + if (SubscriptionHelper.setOnce(field, s)) { + long r = requested.getAndSet(0L); + if (r != 0L) { + s.request(r); + } + return true; + } + return false; + } + + /** + * Atomically requests from the Subscription in the field if not null, otherwise accumulates + * the request amount in the requested field to be requested once the field is set to non-null. + * @param field the target field that may already contain a Subscription + * @param requested the current requested amount + * @param n the request amount, positive (verified) + */ + public static void deferredRequest(AtomicReference<Subscription> field, AtomicLong requested, long n) { + Subscription s = field.get(); + if (s != null) { + s.request(n); + } else { + if (SubscriptionHelper.validate(n)) { + BackpressureHelper.add(requested, n); + + s = field.get(); + if (s != null) { + long r = requested.getAndSet(0L); + if (r != 0L) { + s.request(r); + } + } + } + } + } + + /** + * Atomically sets the subscription on the field if it is still null and issues a positive request + * to the given {@link Subscription}. + * <p> + * If the field is not null and doesn't contain the {@link #CANCELLED} + * instance, the {@link #reportSubscriptionSet()} is called. + * @param field the target field + * @param s the new subscription to set + * @param request the amount to request, positive (not verified) + * @return true if the operation succeeded, false if the target field was not null. + * @since 2.1.11 + */ + public static boolean setOnce(AtomicReference<Subscription> field, Subscription s, long request) { + if (setOnce(field, s)) { + s.request(request); + return true; + } + return false; + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/util/AppendOnlyLinkedArrayList.java b/src/main/java/io/reactivex/rxjava3/internal/util/AppendOnlyLinkedArrayList.java new file mode 100644 index 0000000000..3c17b88009 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/util/AppendOnlyLinkedArrayList.java @@ -0,0 +1,180 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.util; + +import org.reactivestreams.Subscriber; + +import io.reactivex.rxjava3.core.Observer; +import io.reactivex.rxjava3.functions.*; + +/** + * A linked-array-list implementation that only supports appending and consumption. + * + * @param <T> the value type + */ +public class AppendOnlyLinkedArrayList<T> { + final int capacity; + final Object[] head; + Object[] tail; + int offset; + + /** + * Constructs an empty list with a per-link capacity. + * @param capacity the capacity of each link + */ + public AppendOnlyLinkedArrayList(int capacity) { + this.capacity = capacity; + this.head = new Object[capacity + 1]; + this.tail = head; + } + + /** + * Append a non-null value to the list. + * <p>Don't add null to the list! + * @param value the value to append + */ + public void add(T value) { + final int c = capacity; + int o = offset; + if (o == c) { + Object[] next = new Object[c + 1]; + tail[c] = next; + tail = next; + o = 0; + } + tail[o] = value; + offset = o + 1; + } + + /** + * Set a value as the first element of the list. + * @param value the value to set + */ + public void setFirst(T value) { + head[0] = value; + } + + /** + * Predicate interface suppressing the exception. + * + * @param <T> the value type + */ + public interface NonThrowingPredicate<T> extends Predicate<T> { + @Override + boolean test(T t); + } + + /** + * Loops over all elements of the array until a null element is encountered or + * the given predicate returns true. + * @param consumer the consumer of values that returns true if the forEach should terminate + */ + @SuppressWarnings("unchecked") + public void forEachWhile(NonThrowingPredicate<? super T> consumer) { + Object[] a = head; + final int c = capacity; + while (a != null) { + for (int i = 0; i < c; i++) { + Object o = a[i]; + if (o == null) { + break; + } + if (consumer.test((T)o)) { + return; + } + } + a = (Object[])a[c]; + } + } + + /** + * Interprets the contents as NotificationLite objects and calls + * the appropriate Subscriber method. + * + * @param <U> the target type + * @param subscriber the subscriber to emit the events to + * @return true if a terminal event has been reached + */ + public <U> boolean accept(Subscriber<? super U> subscriber) { + Object[] a = head; + final int c = capacity; + while (a != null) { + for (int i = 0; i < c; i++) { + Object o = a[i]; + if (o == null) { + break; + } + + if (NotificationLite.acceptFull(o, subscriber)) { + return true; + } + } + a = (Object[])a[c]; + } + return false; + } + + /** + * Interprets the contents as NotificationLite objects and calls + * the appropriate Observer method. + * + * @param <U> the target type + * @param observer the observer to emit the events to + * @return true if a terminal event has been reached + */ + public <U> boolean accept(Observer<? super U> observer) { + Object[] a = head; + final int c = capacity; + while (a != null) { + for (int i = 0; i < c; i++) { + Object o = a[i]; + if (o == null) { + break; + } + + if (NotificationLite.acceptFull(o, observer)) { + return true; + } + } + a = (Object[])a[c]; + } + return false; + } + + /** + * Loops over all elements of the array until a null element is encountered or + * the given predicate returns true. + * @param <S> the extra state type + * @param state the extra state passed into the consumer + * @param consumer the consumer of values that returns true if the forEach should terminate + * @throws Throwable if the predicate throws + */ + @SuppressWarnings("unchecked") + public <S> void forEachWhile(S state, BiPredicate<? super S, ? super T> consumer) throws Throwable { + Object[] a = head; + final int c = capacity; + for (;;) { + for (int i = 0; i < c; i++) { + Object o = a[i]; + if (o == null) { + return; + } + if (consumer.test(state, (T)o)) { + return; + } + } + a = (Object[])a[c]; + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/util/ArrayListSupplier.java b/src/main/java/io/reactivex/rxjava3/internal/util/ArrayListSupplier.java new file mode 100644 index 0000000000..6379cfcab8 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/util/ArrayListSupplier.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.util; + +import java.util.*; + +import io.reactivex.rxjava3.functions.*; + +public enum ArrayListSupplier implements Supplier<List<Object>>, Function<Object, List<Object>> { + INSTANCE; + + @SuppressWarnings({ "unchecked", "rawtypes" }) + public static <T> Supplier<List<T>> asSupplier() { + return (Supplier)INSTANCE; + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + public static <T, O> Function<O, List<T>> asFunction() { + return (Function)INSTANCE; + } + + @Override + public List<Object> get() { + return new ArrayList<>(); + } + + @Override public List<Object> apply(Object o) { + return new ArrayList<>(); + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/util/AtomicThrowable.java b/src/main/java/io/reactivex/rxjava3/internal/util/AtomicThrowable.java new file mode 100644 index 0000000000..3ef92cb77a --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/util/AtomicThrowable.java @@ -0,0 +1,175 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.util; + +import java.util.concurrent.atomic.AtomicReference; + +import org.reactivestreams.Subscriber; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +/** + * Atomic container for Throwables including combining and having a + * terminal state via ExceptionHelper. + * <p> + * Watch out for the leaked AtomicReference methods! + */ +public final class AtomicThrowable extends AtomicReference<Throwable> { + + private static final long serialVersionUID = 3949248817947090603L; + + /** + * Atomically adds a Throwable to this container (combining with a previous Throwable is necessary). + * @param t the throwable to add + * @return true if successful, false if the container has been terminated + */ + public boolean tryAddThrowable(Throwable t) { + return ExceptionHelper.addThrowable(this, t); + } + + /** + * Atomically adds a Throwable to this container (combining with a previous Throwable is necessary) + * or reports the error the global error handler and no changes are made. + * @param t the throwable to add + * @return true if successful, false if the container has been terminated + */ + public boolean tryAddThrowableOrReport(Throwable t) { + if (tryAddThrowable(t)) { + return true; + } + RxJavaPlugins.onError(t); + return false; + } + + /** + * Atomically terminate the container and return the contents of the last + * non-terminal Throwable of it. + * @return the last Throwable + */ + public Throwable terminate() { + return ExceptionHelper.terminate(this); + } + + public boolean isTerminated() { + return get() == ExceptionHelper.TERMINATED; + } + + /** + * Tries to terminate this atomic throwable (by swapping in the TERMINATED indicator) + * and calls {@link RxJavaPlugins#onError(Throwable)} if there was a non-null, non-indicator + * exception contained within before. + * @since 3.0.0 + */ + public void tryTerminateAndReport() { + Throwable ex = terminate(); + if (ex != null && ex != ExceptionHelper.TERMINATED) { + RxJavaPlugins.onError(ex); + } + } + + /** + * Tries to terminate this atomic throwable (by swapping in the TERMINATED indicator) + * and notifies the consumer if there was no error (onComplete) or there was a + * non-null, non-indicator exception contained before (onError). + * If there was a terminated indicator, the consumer is not signaled. + * @param consumer the consumer to notify + */ + public void tryTerminateConsumer(Subscriber<?> consumer) { + Throwable ex = terminate(); + if (ex == null) { + consumer.onComplete(); + } else if (ex != ExceptionHelper.TERMINATED) { + consumer.onError(ex); + } + } + + /** + * Tries to terminate this atomic throwable (by swapping in the TERMINATED indicator) + * and notifies the consumer if there was no error (onComplete) or there was a + * non-null, non-indicator exception contained before (onError). + * If there was a terminated indicator, the consumer is not signaled. + * @param consumer the consumer to notify + */ + public void tryTerminateConsumer(Observer<?> consumer) { + Throwable ex = terminate(); + if (ex == null) { + consumer.onComplete(); + } else if (ex != ExceptionHelper.TERMINATED) { + consumer.onError(ex); + } + } + + /** + * Tries to terminate this atomic throwable (by swapping in the TERMINATED indicator) + * and notifies the consumer if there was no error (onComplete) or there was a + * non-null, non-indicator exception contained before (onError). + * If there was a terminated indicator, the consumer is not signaled. + * @param consumer the consumer to notify + */ + public void tryTerminateConsumer(MaybeObserver<?> consumer) { + Throwable ex = terminate(); + if (ex == null) { + consumer.onComplete(); + } else if (ex != ExceptionHelper.TERMINATED) { + consumer.onError(ex); + } + } + + /** + * Tries to terminate this atomic throwable (by swapping in the TERMINATED indicator) + * and notifies the consumer if there was no error (onComplete) or there was a + * non-null, non-indicator exception contained before (onError). + * If there was a terminated indicator, the consumer is not signaled. + * @param consumer the consumer to notify + */ + public void tryTerminateConsumer(SingleObserver<?> consumer) { + Throwable ex = terminate(); + if (ex != null && ex != ExceptionHelper.TERMINATED) { + consumer.onError(ex); + } + } + + /** + * Tries to terminate this atomic throwable (by swapping in the TERMINATED indicator) + * and notifies the consumer if there was no error (onComplete) or there was a + * non-null, non-indicator exception contained before (onError). + * If there was a terminated indicator, the consumer is not signaled. + * @param consumer the consumer to notify + */ + public void tryTerminateConsumer(CompletableObserver consumer) { + Throwable ex = terminate(); + if (ex == null) { + consumer.onComplete(); + } else if (ex != ExceptionHelper.TERMINATED) { + consumer.onError(ex); + } + } + + /** + * Tries to terminate this atomic throwable (by swapping in the TERMINATED indicator) + * and notifies the consumer if there was no error (onComplete) or there was a + * non-null, non-indicator exception contained before (onError). + * If there was a terminated indicator, the consumer is not signaled. + * @param consumer the consumer to notify + */ + public void tryTerminateConsumer(Emitter<?> consumer) { + Throwable ex = terminate(); + if (ex == null) { + consumer.onComplete(); + } else if (ex != ExceptionHelper.TERMINATED) { + consumer.onError(ex); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/util/BackpressureHelper.java b/src/main/java/io/reactivex/rxjava3/internal/util/BackpressureHelper.java new file mode 100644 index 0000000000..73b15382c8 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/util/BackpressureHelper.java @@ -0,0 +1,153 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.util; + +import java.util.concurrent.atomic.AtomicLong; + +import io.reactivex.rxjava3.annotations.NonNull; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +/** + * Utility class to help with backpressure-related operations such as request aggregation. + */ +public final class BackpressureHelper { + /** Utility class. */ + private BackpressureHelper() { + throw new IllegalStateException("No instances!"); + } + + /** + * Adds two long values and caps the sum at {@link Long#MAX_VALUE}. + * @param a the first value + * @param b the second value + * @return the sum capped at {@link Long#MAX_VALUE} + */ + public static long addCap(long a, long b) { + long u = a + b; + if (u < 0L) { + return Long.MAX_VALUE; + } + return u; + } + + /** + * Multiplies two long values and caps the product at {@link Long#MAX_VALUE}. + * @param a the first value + * @param b the second value + * @return the product capped at {@link Long#MAX_VALUE} + */ + public static long multiplyCap(long a, long b) { + long u = a * b; + if (((a | b) >>> 31) != 0) { + if (u / a != b) { + return Long.MAX_VALUE; + } + } + return u; + } + + /** + * Atomically adds the positive value n to the requested value in the {@link AtomicLong} and + * caps the result at {@link Long#MAX_VALUE} and returns the previous value. + * @param requested the {@code AtomicLong} holding the current requested value + * @param n the value to add, must be positive (not verified) + * @return the original value before the add + */ + public static long add(@NonNull AtomicLong requested, long n) { + for (;;) { + long r = requested.get(); + if (r == Long.MAX_VALUE) { + return Long.MAX_VALUE; + } + long u = addCap(r, n); + if (requested.compareAndSet(r, u)) { + return r; + } + } + } + + /** + * Atomically adds the positive value n to the requested value in the {@link AtomicLong} and + * caps the result at {@link Long#MAX_VALUE} and returns the previous value and + * considers {@link Long#MIN_VALUE} as a cancel indication (no addition then). + * @param requested the {@code AtomicLong} holding the current requested value + * @param n the value to add, must be positive (not verified) + * @return the original value before the add + */ + public static long addCancel(@NonNull AtomicLong requested, long n) { + for (;;) { + long r = requested.get(); + if (r == Long.MIN_VALUE) { + return Long.MIN_VALUE; + } + if (r == Long.MAX_VALUE) { + return Long.MAX_VALUE; + } + long u = addCap(r, n); + if (requested.compareAndSet(r, u)) { + return r; + } + } + } + + /** + * Atomically subtract the given number (positive, not validated) from the target field unless it contains {@link Long#MAX_VALUE}. + * @param requested the target field holding the current requested amount + * @param n the produced element count, positive (not validated) + * @return the new amount + */ + public static long produced(@NonNull AtomicLong requested, long n) { + for (;;) { + long current = requested.get(); + if (current == Long.MAX_VALUE) { + return Long.MAX_VALUE; + } + long update = current - n; + if (update < 0L) { + RxJavaPlugins.onError(new IllegalStateException("More produced than requested: " + update)); + update = 0L; + } + if (requested.compareAndSet(current, update)) { + return update; + } + } + } + + /** + * Atomically subtract the given number (positive, not validated) from the target field if + * it doesn't contain {@link Long#MIN_VALUE} (indicating some cancelled state) or {@link Long#MAX_VALUE} (unbounded mode). + * @param requested the target field holding the current requested amount + * @param n the produced element count, positive (not validated) + * @return the new amount + */ + public static long producedCancel(@NonNull AtomicLong requested, long n) { + for (;;) { + long current = requested.get(); + if (current == Long.MIN_VALUE) { + return Long.MIN_VALUE; + } + if (current == Long.MAX_VALUE) { + return Long.MAX_VALUE; + } + long update = current - n; + if (update < 0L) { + RxJavaPlugins.onError(new IllegalStateException("More produced than requested: " + update)); + update = 0L; + } + if (requested.compareAndSet(current, update)) { + return update; + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/util/BlockingHelper.java b/src/main/java/io/reactivex/rxjava3/internal/util/BlockingHelper.java new file mode 100644 index 0000000000..84acadee6f --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/util/BlockingHelper.java @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.util; + +import java.util.concurrent.CountDownLatch; + +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.schedulers.NonBlockingThread; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +/** + * Utility methods for helping common blocking operations. + */ +public final class BlockingHelper { + /** Utility class. */ + private BlockingHelper() { + throw new IllegalStateException("No instances!"); + } + + public static void awaitForComplete(CountDownLatch latch, Disposable subscription) { + if (latch.getCount() == 0) { + // Synchronous observable completes before awaiting for it. + // Skip await so InterruptedException will never be thrown. + return; + } + // block until the subscription completes and then return + try { + verifyNonBlocking(); + latch.await(); + } catch (InterruptedException e) { + subscription.dispose(); + // set the interrupted flag again so callers can still get it + // for more information see https://github.com/ReactiveX/RxJava/pull/147#issuecomment-13624780 + Thread.currentThread().interrupt(); + // using Runtime so it is not checked + throw new IllegalStateException("Interrupted while waiting for subscription to complete.", e); + } + } + + /** + * Checks if the {@code failOnNonBlockingScheduler} plugin setting is enabled and the current + * thread is a Scheduler sensitive to blocking operators. + * @throws IllegalStateException if the {@code failOnNonBlockingScheduler} and the current thread is sensitive to blocking + */ + public static void verifyNonBlocking() { + if (RxJavaPlugins.isFailOnNonBlockingScheduler() + && (Thread.currentThread() instanceof NonBlockingThread + || RxJavaPlugins.onBeforeBlocking())) { + throw new IllegalStateException("Attempt to block on a Scheduler " + Thread.currentThread().getName() + " that doesn't support blocking operators as they may lead to deadlock"); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/util/BlockingIgnoringReceiver.java b/src/main/java/io/reactivex/rxjava3/internal/util/BlockingIgnoringReceiver.java new file mode 100644 index 0000000000..8d4c1fb76e --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/util/BlockingIgnoringReceiver.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.util; + +import java.util.concurrent.CountDownLatch; + +import io.reactivex.rxjava3.functions.*; + +/** + * Stores an incoming Throwable (if any) and counts itself down. + */ +public final class BlockingIgnoringReceiver +extends CountDownLatch +implements Consumer<Throwable>, Action { + public Throwable error; + + public BlockingIgnoringReceiver() { + super(1); + } + + @Override + public void accept(Throwable e) { + error = e; + countDown(); + } + + @Override + public void run() { + countDown(); + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/util/ConnectConsumer.java b/src/main/java/io/reactivex/rxjava3/internal/util/ConnectConsumer.java new file mode 100644 index 0000000000..5d5246719d --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/util/ConnectConsumer.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.util; + +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.functions.Consumer; + +/** + * Store the Disposable received from the connection. + */ +public final class ConnectConsumer implements Consumer<Disposable> { + public Disposable disposable; + + @Override + public void accept(Disposable t) { + this.disposable = t; + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/util/EmptyComponent.java b/src/main/java/io/reactivex/rxjava3/internal/util/EmptyComponent.java new file mode 100644 index 0000000000..47390f57cb --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/util/EmptyComponent.java @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.util; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +/** + * Singleton implementing many interfaces as empty. + */ +public enum EmptyComponent implements FlowableSubscriber<Object>, Observer<Object>, MaybeObserver<Object>, +SingleObserver<Object>, CompletableObserver, Subscription, Disposable { + INSTANCE; + + @SuppressWarnings("unchecked") + public static <T> Subscriber<T> asSubscriber() { + return (Subscriber<T>)INSTANCE; + } + + @SuppressWarnings("unchecked") + public static <T> Observer<T> asObserver() { + return (Observer<T>)INSTANCE; + } + + @Override + public void dispose() { + // deliberately no-op + } + + @Override + public boolean isDisposed() { + return true; + } + + @Override + public void request(long n) { + // deliberately no-op + } + + @Override + public void cancel() { + // deliberately no-op + } + + @Override + public void onSubscribe(Disposable d) { + d.dispose(); + } + + @Override + public void onSubscribe(Subscription s) { + s.cancel(); + } + + @Override + public void onNext(Object t) { + // deliberately no-op + } + + @Override + public void onError(Throwable t) { + RxJavaPlugins.onError(t); + } + + @Override + public void onComplete() { + // deliberately no-op + } + + @Override + public void onSuccess(Object value) { + // deliberately no-op + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/util/EndConsumerHelper.java b/src/main/java/io/reactivex/rxjava3/internal/util/EndConsumerHelper.java new file mode 100644 index 0000000000..0e72353800 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/util/EndConsumerHelper.java @@ -0,0 +1,150 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.util; + +import java.util.Objects; +import java.util.concurrent.atomic.AtomicReference; + +import org.reactivestreams.Subscription; + +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.ProtocolViolationException; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; +import io.reactivex.rxjava3.internal.subscriptions.SubscriptionHelper; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +/** + * Utility class to help report multiple subscriptions with the same + * consumer type instead of the internal "Disposable already set!" message + * that is practically reserved for internal operators and indicate bugs in them. + */ +public final class EndConsumerHelper { + + /** + * Utility class. + */ + private EndConsumerHelper() { + throw new IllegalStateException("No instances!"); + } + + /** + * Ensures that the upstream Disposable is null and returns true, otherwise + * disposes the next Disposable and if the upstream is not the shared + * disposed instance, reports a ProtocolViolationException due to + * multiple subscribe attempts. + * @param upstream the upstream current value + * @param next the Disposable to check for nullness and dispose if necessary + * @param observer the class of the consumer to have a personalized + * error message if the upstream already contains a non-cancelled Disposable. + * @return true if successful, false if the upstream was non null + */ + public static boolean validate(Disposable upstream, Disposable next, Class<?> observer) { + Objects.requireNonNull(next, "next is null"); + if (upstream != null) { + next.dispose(); + if (upstream != DisposableHelper.DISPOSED) { + reportDoubleSubscription(observer); + } + return false; + } + return true; + } + + /** + * Atomically updates the target upstream AtomicReference from null to the non-null + * next Disposable, otherwise disposes next and reports a ProtocolViolationException + * if the AtomicReference doesn't contain the shared disposed indicator. + * @param upstream the target AtomicReference to update + * @param next the Disposable to set on it atomically + * @param observer the class of the consumer to have a personalized + * error message if the upstream already contains a non-cancelled Disposable. + * @return true if successful, false if the content of the AtomicReference was non null + */ + public static boolean setOnce(AtomicReference<Disposable> upstream, Disposable next, Class<?> observer) { + Objects.requireNonNull(next, "next is null"); + if (!upstream.compareAndSet(null, next)) { + next.dispose(); + if (upstream.get() != DisposableHelper.DISPOSED) { + reportDoubleSubscription(observer); + } + return false; + } + return true; + } + + /** + * Ensures that the upstream Subscription is null and returns true, otherwise + * cancels the next Subscription and if the upstream is not the shared + * cancelled instance, reports a ProtocolViolationException due to + * multiple subscribe attempts. + * @param upstream the upstream current value + * @param next the Subscription to check for nullness and cancel if necessary + * @param subscriber the class of the consumer to have a personalized + * error message if the upstream already contains a non-cancelled Subscription. + * @return true if successful, false if the upstream was non null + */ + public static boolean validate(Subscription upstream, Subscription next, Class<?> subscriber) { + Objects.requireNonNull(next, "next is null"); + if (upstream != null) { + next.cancel(); + if (upstream != SubscriptionHelper.CANCELLED) { + reportDoubleSubscription(subscriber); + } + return false; + } + return true; + } + + /** + * Atomically updates the target upstream AtomicReference from null to the non-null + * next Subscription, otherwise cancels next and reports a ProtocolViolationException + * if the AtomicReference doesn't contain the shared cancelled indicator. + * @param upstream the target AtomicReference to update + * @param next the Subscription to set on it atomically + * @param subscriber the class of the consumer to have a personalized + * error message if the upstream already contains a non-cancelled Subscription. + * @return true if successful, false if the content of the AtomicReference was non null + */ + public static boolean setOnce(AtomicReference<Subscription> upstream, Subscription next, Class<?> subscriber) { + Objects.requireNonNull(next, "next is null"); + if (!upstream.compareAndSet(null, next)) { + next.cancel(); + if (upstream.get() != SubscriptionHelper.CANCELLED) { + reportDoubleSubscription(subscriber); + } + return false; + } + return true; + } + + /** + * Builds the error message with the consumer class. + * @param consumer the class of the consumer + * @return the error message string + */ + public static String composeMessage(String consumer) { + return "It is not allowed to subscribe with a(n) " + consumer + " multiple times. " + + "Please create a fresh instance of " + consumer + " and subscribe that to the target source instead."; + } + + /** + * Report a ProtocolViolationException with a personalized message referencing + * the simple type name of the consumer class and report it via + * RxJavaPlugins.onError. + * @param consumer the class of the consumer + */ + public static void reportDoubleSubscription(Class<?> consumer) { + RxJavaPlugins.onError(new ProtocolViolationException(composeMessage(consumer.getName()))); + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/util/ErrorMode.java b/src/main/java/io/reactivex/rxjava3/internal/util/ErrorMode.java new file mode 100644 index 0000000000..02389f0b7a --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/util/ErrorMode.java @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.util; + +/** + * Indicates when an error from the main source should be reported. + */ +public enum ErrorMode { + /** Report the error immediately, cancelling the active inner source. */ + IMMEDIATE, + /** Report error after an inner source terminated. */ + BOUNDARY, + /** Report the error after all sources terminated. */ + END +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/util/ExceptionHelper.java b/src/main/java/io/reactivex/rxjava3/internal/util/ExceptionHelper.java new file mode 100644 index 0000000000..1ddbb8aca1 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/util/ExceptionHelper.java @@ -0,0 +1,183 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.util; + +import java.util.*; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +import io.reactivex.rxjava3.exceptions.CompositeException; + +/** + * Terminal atomics for Throwable containers. + */ +public final class ExceptionHelper { + + /** Utility class. */ + private ExceptionHelper() { + throw new IllegalStateException("No instances!"); + } + + /** + * If the provided Throwable is an Error this method + * throws it, otherwise returns a RuntimeException wrapping the error + * if that error is a checked exception. + * @param error the error to wrap or throw + * @return the (wrapped) error + */ + public static RuntimeException wrapOrThrow(Throwable error) { + if (error instanceof Error) { + throw (Error)error; + } + if (error instanceof RuntimeException) { + return (RuntimeException)error; + } + return new RuntimeException(error); + } + + /** + * A singleton instance of a Throwable indicating a terminal state for exceptions, + * don't leak this. + */ + public static final Throwable TERMINATED = new Termination(); + + public static boolean addThrowable(AtomicReference<Throwable> field, Throwable exception) { + for (;;) { + Throwable current = field.get(); + + if (current == TERMINATED) { + return false; + } + + Throwable update; + if (current == null) { + update = exception; + } else { + update = new CompositeException(current, exception); + } + + if (field.compareAndSet(current, update)) { + return true; + } + } + } + + public static Throwable terminate(AtomicReference<Throwable> field) { + Throwable current = field.get(); + if (current != TERMINATED) { + current = field.getAndSet(TERMINATED); + } + return current; + } + + /** + * Returns a flattened list of Throwables from tree-like CompositeException chain. + * @param t the starting throwable + * @return the list of Throwables flattened in a depth-first manner + */ + public static List<Throwable> flatten(Throwable t) { + List<Throwable> list = new ArrayList<>(); + ArrayDeque<Throwable> deque = new ArrayDeque<>(); + deque.offer(t); + + while (!deque.isEmpty()) { + Throwable e = deque.removeFirst(); + if (e instanceof CompositeException) { + CompositeException ce = (CompositeException) e; + List<Throwable> exceptions = ce.getExceptions(); + for (int i = exceptions.size() - 1; i >= 0; i--) { + deque.offerFirst(exceptions.get(i)); + } + } else { + list.add(e); + } + } + + return list; + } + + /** + * Workaround for Java 6 not supporting throwing a final Throwable from a catch block. + * @param <E> the generic exception type + * @param e the Throwable error to return or throw + * @return the Throwable e if it is a subclass of Exception + * @throws E the generic exception thrown + */ + @SuppressWarnings("unchecked") + public static <E extends Throwable> Exception throwIfThrowable(Throwable e) throws E { + if (e instanceof Exception) { + return (Exception)e; + } + throw (E)e; + } + + public static String timeoutMessage(long timeout, TimeUnit unit) { + return "The source did not signal an event for " + + timeout + + " " + + unit.toString().toLowerCase() + + " and has been terminated."; + } + + static final class Termination extends Throwable { + + private static final long serialVersionUID = -4649703670690200604L; + + Termination() { + super("No further exceptions"); + } + + @Override + public Throwable fillInStackTrace() { + return this; + } + } + + /** + * Composes a String with a null warning message. + * @param prefix the prefix to add to the message. + * @return the composed String + * @since 3.0.0 + */ + public static String nullWarning(String prefix) { + return prefix + " Null values are generally not allowed in 3.x operators and sources."; + } + + /** + * Creates a NullPointerException with a composed message via {@link #nullWarning(String)}. + * @param prefix the prefix to add to the message. + * @return the composed String + * @since 3.0.0 + */ + public static NullPointerException createNullPointerException(String prefix) { + return new NullPointerException(nullWarning(prefix)); + } + + /** + * Similar to Objects.requireNonNull but composes the error message via + * {@link #nullWarning(String)}. + * @param <T> the value type + * @param value the value to check + * @param prefix the prefix to the error message + * @return the value + * @throws NullPointerException if value is null + * @since 3.0.0 + */ + public static <T> T nullCheck(T value, String prefix) { + if (value == null) { + throw createNullPointerException(prefix); + } + return value; + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/util/HalfSerializer.java b/src/main/java/io/reactivex/rxjava3/internal/util/HalfSerializer.java new file mode 100644 index 0000000000..e1bf5596e2 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/util/HalfSerializer.java @@ -0,0 +1,136 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.util; + +import java.util.concurrent.atomic.AtomicInteger; + +import org.reactivestreams.Subscriber; + +import io.reactivex.rxjava3.core.Observer; + +/** + * Utility methods to perform half-serialization: a form of serialization + * where onNext is guaranteed to be called from a single thread but + * onError or onComplete may be called from any threads. + */ +public final class HalfSerializer { + /** Utility class. */ + private HalfSerializer() { + throw new IllegalStateException("No instances!"); + } + + /** + * Emits the given value if possible and terminates if there was an onComplete or onError + * while emitting, drops the value otherwise. + * @param <T> the value type + * @param subscriber the target Subscriber to emit to + * @param value the value to emit + * @param wip the serialization work-in-progress counter/indicator + * @param errors the holder of Throwables + * @return true if the operation succeeded, false if there sequence completed + */ + public static <T> boolean onNext(Subscriber<? super T> subscriber, T value, + AtomicInteger wip, AtomicThrowable errors) { + if (wip.get() == 0 && wip.compareAndSet(0, 1)) { + subscriber.onNext(value); + if (wip.decrementAndGet() == 0) { + return true; + } + errors.tryTerminateConsumer(subscriber); + } + return false; + } + + /** + * Emits the given exception if possible or adds it to the given error container to + * be emitted by a concurrent onNext if one is running. + * Undeliverable exceptions are sent to the RxJavaPlugins.onError. + * @param subscriber the target Subscriber to emit to + * @param ex the Throwable to emit + * @param wip the serialization work-in-progress counter/indicator + * @param errors the holder of Throwables + */ + public static void onError(Subscriber<?> subscriber, Throwable ex, + AtomicInteger wip, AtomicThrowable errors) { + if (errors.tryAddThrowableOrReport(ex)) { + if (wip.getAndIncrement() == 0) { + errors.tryTerminateConsumer(subscriber); + } + } + } + + /** + * Emits an onComplete signal or an onError signal with the given error or indicates + * the concurrently running onNext should do that. + * @param subscriber the target Subscriber to emit to + * @param wip the serialization work-in-progress counter/indicator + * @param errors the holder of Throwables + */ + public static void onComplete(Subscriber<?> subscriber, AtomicInteger wip, AtomicThrowable errors) { + if (wip.getAndIncrement() == 0) { + errors.tryTerminateConsumer(subscriber); + } + } + + /** + * Emits the given value if possible and terminates if there was an onComplete or onError + * while emitting, drops the value otherwise. + * @param <T> the value type + * @param observer the target Observer to emit to + * @param value the value to emit + * @param wip the serialization work-in-progress counter/indicator + * @param errors the holder of Throwables + */ + public static <T> void onNext(Observer<? super T> observer, T value, + AtomicInteger wip, AtomicThrowable errors) { + if (wip.get() == 0 && wip.compareAndSet(0, 1)) { + observer.onNext(value); + if (wip.decrementAndGet() != 0) { + errors.tryTerminateConsumer(observer); + } + } + } + + /** + * Emits the given exception if possible or adds it to the given error container to + * be emitted by a concurrent onNext if one is running. + * Undeliverable exceptions are sent to the RxJavaPlugins.onError. + * @param observer the target Subscriber to emit to + * @param ex the Throwable to emit + * @param wip the serialization work-in-progress counter/indicator + * @param errors the holder of Throwables + */ + public static void onError(Observer<?> observer, Throwable ex, + AtomicInteger wip, AtomicThrowable errors) { + if (errors.tryAddThrowableOrReport(ex)) { + if (wip.getAndIncrement() == 0) { + errors.tryTerminateConsumer(observer); + } + } + } + + /** + * Emits an onComplete signal or an onError signal with the given error or indicates + * the concurrently running onNext should do that. + * @param observer the target Subscriber to emit to + * @param wip the serialization work-in-progress counter/indicator + * @param errors the holder of Throwables + */ + public static void onComplete(Observer<?> observer, AtomicInteger wip, AtomicThrowable errors) { + if (wip.getAndIncrement() == 0) { + errors.tryTerminateConsumer(observer); + } + } + +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/util/HashMapSupplier.java b/src/main/java/io/reactivex/rxjava3/internal/util/HashMapSupplier.java new file mode 100644 index 0000000000..15c2884266 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/util/HashMapSupplier.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.util; + +import java.util.*; + +import io.reactivex.rxjava3.functions.Supplier; + +public enum HashMapSupplier implements Supplier<Map<Object, Object>> { + INSTANCE; + + @SuppressWarnings({ "unchecked", "rawtypes" }) + public static <K, V> Supplier<Map<K, V>> asSupplier() { + return (Supplier)INSTANCE; + } + + @Override public Map<Object, Object> get() { + return new HashMap<>(); + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/util/LinkedArrayList.java b/src/main/java/io/reactivex/rxjava3/internal/util/LinkedArrayList.java new file mode 100644 index 0000000000..92ff9d30a6 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/util/LinkedArrayList.java @@ -0,0 +1,112 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.util; + +import java.util.*; + +/** + * A list implementation which combines an ArrayList with a LinkedList to + * avoid copying values when the capacity needs to be increased. + * <p> + * The class is non final to allow embedding it directly and thus saving on object allocation. + */ +public class LinkedArrayList { + /** The capacity of each array segment. */ + final int capacityHint; + /** + * Contains the head of the linked array list if not null. The + * length is always capacityHint + 1 and the last element is an Object[] pointing + * to the next element of the linked array list. + */ + Object[] head; + /** The tail array where new elements will be added. */ + Object[] tail; + /** + * The total size of the list; written after elements have been added (release) and + * and when read, the value indicates how many elements can be safely read (acquire). + */ + volatile int size; + /** The next available slot in the current tail. */ + int indexInTail; + /** + * Constructor with the capacity hint of each array segment. + * @param capacityHint the expected number of elements to hold (can grow beyond that) + */ + public LinkedArrayList(int capacityHint) { + this.capacityHint = capacityHint; + } + /** + * Adds a new element to this list. + * @param o the object to add, nulls are accepted + */ + public void add(Object o) { + // if no value yet, create the first array + if (size == 0) { + head = new Object[capacityHint + 1]; + tail = head; + head[0] = o; + indexInTail = 1; + size = 1; + } else + // if the tail is full, create a new tail and link + if (indexInTail == capacityHint) { + Object[] t = new Object[capacityHint + 1]; + t[0] = o; + tail[capacityHint] = t; + tail = t; + indexInTail = 1; + size++; + } else { + tail[indexInTail] = o; + indexInTail++; + size++; + } + } + /** + * Returns the head buffer segment or null if the list is empty. + * @return the head object array + */ + public Object[] head() { + return head; // NOPMD + } + + /** + * Returns the total size of the list. + * @return the total size of the list + */ + public int size() { + return size; + } + + @Override + public String toString() { + final int cap = capacityHint; + final int s = size; + final List<Object> list = new ArrayList<>(s + 1); + + Object[] h = head(); + int j = 0; + int k = 0; + while (j < s) { + list.add(h[k]); + j++; + if (++k == cap) { + k = 0; + h = (Object[])h[cap]; + } + } + + return list.toString(); + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/util/ListAddBiConsumer.java b/src/main/java/io/reactivex/rxjava3/internal/util/ListAddBiConsumer.java new file mode 100644 index 0000000000..fb643fcff3 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/util/ListAddBiConsumer.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.util; + +import java.util.List; + +import io.reactivex.rxjava3.functions.BiFunction; + +@SuppressWarnings("rawtypes") +public enum ListAddBiConsumer implements BiFunction<List, Object, List> { + INSTANCE; + + @SuppressWarnings("unchecked") + public static <T> BiFunction<List<T>, T, List<T>> instance() { + return (BiFunction)INSTANCE; + } + + @SuppressWarnings("unchecked") + @Override + public List apply(List t1, Object t2) { + t1.add(t2); + return t1; + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/util/MergerBiFunction.java b/src/main/java/io/reactivex/rxjava3/internal/util/MergerBiFunction.java new file mode 100644 index 0000000000..a736309019 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/util/MergerBiFunction.java @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.util; + +import java.util.*; + +import io.reactivex.rxjava3.functions.BiFunction; + +/** + * A BiFunction that merges two Lists into a new list. + * @param <T> the value type + */ +public final class MergerBiFunction<T> implements BiFunction<List<T>, List<T>, List<T>> { + + final Comparator<? super T> comparator; + + public MergerBiFunction(Comparator<? super T> comparator) { + this.comparator = comparator; + } + + @Override + public List<T> apply(List<T> a, List<T> b) { + int n = a.size() + b.size(); + if (n == 0) { + return new ArrayList<>(); + } + List<T> both = new ArrayList<>(n); + + Iterator<T> at = a.iterator(); + Iterator<T> bt = b.iterator(); + + T s1 = at.hasNext() ? at.next() : null; + T s2 = bt.hasNext() ? bt.next() : null; + + while (s1 != null && s2 != null) { + if (comparator.compare(s1, s2) < 0) { // s1 comes before s2 + both.add(s1); + s1 = at.hasNext() ? at.next() : null; + } else { + both.add(s2); + s2 = bt.hasNext() ? bt.next() : null; + } + } + + if (s1 != null) { + both.add(s1); + while (at.hasNext()) { + both.add(at.next()); + } + } else { + both.add(s2); + while (bt.hasNext()) { + both.add(bt.next()); + } + } + + return both; + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/util/NotificationLite.java b/src/main/java/io/reactivex/rxjava3/internal/util/NotificationLite.java new file mode 100644 index 0000000000..c27486d9cf --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/util/NotificationLite.java @@ -0,0 +1,307 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.util; + +import java.io.Serializable; + +import java.util.Objects; +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.Observer; +import io.reactivex.rxjava3.disposables.Disposable; + +/** + * Lightweight notification handling utility class. + */ +public enum NotificationLite { + COMPLETE + ; + + /** + * Wraps a Throwable. + */ + static final class ErrorNotification implements Serializable { + + private static final long serialVersionUID = -8759979445933046293L; + final Throwable e; + ErrorNotification(Throwable e) { + this.e = e; + } + + @Override + public String toString() { + return "NotificationLite.Error[" + e + "]"; + } + + @Override + public int hashCode() { + return e.hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof ErrorNotification) { + ErrorNotification n = (ErrorNotification) obj; + return Objects.equals(e, n.e); + } + return false; + } + } + + /** + * Wraps a Subscription. + */ + static final class SubscriptionNotification implements Serializable { + + private static final long serialVersionUID = -1322257508628817540L; + final Subscription upstream; + SubscriptionNotification(Subscription s) { + this.upstream = s; + } + + @Override + public String toString() { + return "NotificationLite.Subscription[" + upstream + "]"; + } + } + + /** + * Wraps a Disposable. + */ + static final class DisposableNotification implements Serializable { + + private static final long serialVersionUID = -7482590109178395495L; + final Disposable upstream; + + DisposableNotification(Disposable d) { + this.upstream = d; + } + + @Override + public String toString() { + return "NotificationLite.Disposable[" + upstream + "]"; + } + } + + /** + * Converts a value into a notification value. + * @param <T> the actual value type + * @param value the value to convert + * @return the notification representing the value + */ + public static <T> Object next(T value) { + return value; + } + + /** + * Returns a complete notification. + * @return a complete notification + */ + public static Object complete() { + return COMPLETE; + } + + /** + * Converts a Throwable into a notification value. + * @param e the Throwable to convert + * @return the notification representing the Throwable + */ + public static Object error(Throwable e) { + return new ErrorNotification(e); + } + + /** + * Converts a Subscription into a notification value. + * @param s the Subscription to convert + * @return the notification representing the Subscription + */ + public static Object subscription(Subscription s) { + return new SubscriptionNotification(s); + } + + /** + * Converts a Disposable into a notification value. + * @param d the disposable to convert + * @return the notification representing the Disposable + */ + public static Object disposable(Disposable d) { + return new DisposableNotification(d); + } + + /** + * Checks if the given object represents a complete notification. + * @param o the object to check + * @return true if the object represents a complete notification + */ + public static boolean isComplete(Object o) { + return o == COMPLETE; + } + + /** + * Checks if the given object represents a error notification. + * @param o the object to check + * @return true if the object represents a error notification + */ + public static boolean isError(Object o) { + return o instanceof ErrorNotification; + } + + /** + * Checks if the given object represents a subscription notification. + * @param o the object to check + * @return true if the object represents a subscription notification + */ + public static boolean isSubscription(Object o) { + return o instanceof SubscriptionNotification; + } + + public static boolean isDisposable(Object o) { + return o instanceof DisposableNotification; + } + + /** + * Extracts the value from the notification object. + * @param <T> the expected value type when unwrapped + * @param o the notification object + * @return the extracted value + */ + @SuppressWarnings("unchecked") + public static <T> T getValue(Object o) { + return (T)o; + } + + /** + * Extracts the Throwable from the notification object. + * @param o the notification object + * @return the extracted Throwable + */ + public static Throwable getError(Object o) { + return ((ErrorNotification)o).e; + } + + /** + * Extracts the Subscription from the notification object. + * @param o the notification object + * @return the extracted Subscription + */ + public static Subscription getSubscription(Object o) { + return ((SubscriptionNotification)o).upstream; + } + + public static Disposable getDisposable(Object o) { + return ((DisposableNotification)o).upstream; + } + + /** + * Calls the appropriate Subscriber method based on the type of the notification. + * <p>Does not check for a subscription notification, see {@link #acceptFull(Object, Subscriber)}. + * @param <T> the expected value type when unwrapped + * @param o the notification object + * @param s the subscriber to call methods on + * @return true if the notification was a terminal event (i.e., complete or error) + * @see #acceptFull(Object, Subscriber) + */ + @SuppressWarnings("unchecked") + public static <T> boolean accept(Object o, Subscriber<? super T> s) { + if (o == COMPLETE) { + s.onComplete(); + return true; + } else + if (o instanceof ErrorNotification) { + s.onError(((ErrorNotification)o).e); + return true; + } + s.onNext((T)o); + return false; + } + + /** + * Calls the appropriate Observer method based on the type of the notification. + * <p>Does not check for a subscription notification. + * @param <T> the expected value type when unwrapped + * @param o the notification object + * @param observer the Observer to call methods on + * @return true if the notification was a terminal event (i.e., complete or error) + */ + @SuppressWarnings("unchecked") + public static <T> boolean accept(Object o, Observer<? super T> observer) { + if (o == COMPLETE) { + observer.onComplete(); + return true; + } else + if (o instanceof ErrorNotification) { + observer.onError(((ErrorNotification)o).e); + return true; + } + observer.onNext((T)o); + return false; + } + + /** + * Calls the appropriate Subscriber method based on the type of the notification. + * @param <T> the expected value type when unwrapped + * @param o the notification object + * @param s the subscriber to call methods on + * @return true if the notification was a terminal event (i.e., complete or error) + * @see #accept(Object, Subscriber) + */ + @SuppressWarnings("unchecked") + public static <T> boolean acceptFull(Object o, Subscriber<? super T> s) { + if (o == COMPLETE) { + s.onComplete(); + return true; + } else + if (o instanceof ErrorNotification) { + s.onError(((ErrorNotification)o).e); + return true; + } else + if (o instanceof SubscriptionNotification) { + s.onSubscribe(((SubscriptionNotification)o).upstream); + return false; + } + s.onNext((T)o); + return false; + } + + /** + * Calls the appropriate Observer method based on the type of the notification. + * @param <T> the expected value type when unwrapped + * @param o the notification object + * @param observer the subscriber to call methods on + * @return true if the notification was a terminal event (i.e., complete or error) + * @see #accept(Object, Observer) + */ + @SuppressWarnings("unchecked") + public static <T> boolean acceptFull(Object o, Observer<? super T> observer) { + if (o == COMPLETE) { + observer.onComplete(); + return true; + } else + if (o instanceof ErrorNotification) { + observer.onError(((ErrorNotification)o).e); + return true; + } else + if (o instanceof DisposableNotification) { + observer.onSubscribe(((DisposableNotification)o).upstream); + return false; + } + observer.onNext((T)o); + return false; + } + + @Override + public String toString() { + return "NotificationLite.Complete"; + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/util/ObservableQueueDrain.java b/src/main/java/io/reactivex/rxjava3/internal/util/ObservableQueueDrain.java new file mode 100644 index 0000000000..6d69b375d4 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/util/ObservableQueueDrain.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.util; + +import io.reactivex.rxjava3.core.Observer; + +public interface ObservableQueueDrain<T, U> { + + boolean cancelled(); + + boolean done(); + + Throwable error(); + + boolean enter(); + + /** + * Adds m to the wip counter. + * @param m the value to add + * @return the wip value after adding the value + */ + int leave(int m); + + /** + * Accept the value and return true if forwarded. + * @param a the subscriber to deliver values to + * @param v the value to deliver + */ + void accept(Observer<? super U> a, T v); +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/util/OpenHashSet.java b/src/main/java/io/reactivex/rxjava3/internal/util/OpenHashSet.java new file mode 100644 index 0000000000..9967ca831d --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/util/OpenHashSet.java @@ -0,0 +1,174 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +/* + * Inspired by fastutils' OpenHashSet implementation at + * https://github.com/vigna/fastutil/blob/master/drv/OpenHashSet.drv + */ + +package io.reactivex.rxjava3.internal.util; + +/** + * A simple open hash set with add, remove and clear capabilities only. + * <p>Doesn't support nor checks for {@code null}s. + * + * @param <T> the element type + */ +public final class OpenHashSet<T> { + private static final int INT_PHI = 0x9E3779B9; + + final float loadFactor; + int mask; + int size; + int maxSize; + T[] keys; + + public OpenHashSet() { + this(16, 0.75f); + } + + /** + * Creates an OpenHashSet with the initial capacity and load factor of 0.75f. + * @param capacity the initial capacity + */ + public OpenHashSet(int capacity) { + this(capacity, 0.75f); + } + + @SuppressWarnings("unchecked") + public OpenHashSet(int capacity, float loadFactor) { + this.loadFactor = loadFactor; + int c = Pow2.roundToPowerOfTwo(capacity); + this.mask = c - 1; + this.maxSize = (int)(loadFactor * c); + this.keys = (T[])new Object[c]; + } + + public boolean add(T value) { + final T[] a = keys; + final int m = mask; + + int pos = mix(value.hashCode()) & m; + T curr = a[pos]; + if (curr != null) { + if (curr.equals(value)) { + return false; + } + for (;;) { + pos = (pos + 1) & m; + curr = a[pos]; + if (curr == null) { + break; + } + if (curr.equals(value)) { + return false; + } + } + } + a[pos] = value; + if (++size >= maxSize) { + rehash(); + } + return true; + } + public boolean remove(T value) { + T[] a = keys; + int m = mask; + int pos = mix(value.hashCode()) & m; + T curr = a[pos]; + if (curr == null) { + return false; + } + if (curr.equals(value)) { + return removeEntry(pos, a, m); + } + for (;;) { + pos = (pos + 1) & m; + curr = a[pos]; + if (curr == null) { + return false; + } + if (curr.equals(value)) { + return removeEntry(pos, a, m); + } + } + } + + boolean removeEntry(int pos, T[] a, int m) { + size--; + + int last; + int slot; + T curr; + for (;;) { + last = pos; + pos = (pos + 1) & m; + for (;;) { + curr = a[pos]; + if (curr == null) { + a[last] = null; + return true; + } + slot = mix(curr.hashCode()) & m; + + if (last <= pos ? last >= slot || slot > pos : last >= slot && slot > pos) { + break; + } + + pos = (pos + 1) & m; + } + a[last] = curr; + } + } + + @SuppressWarnings("unchecked") + void rehash() { + T[] a = keys; + int i = a.length; + int newCap = i << 1; + int m = newCap - 1; + + T[] b = (T[])new Object[newCap]; + + for (int j = size; j-- != 0; ) { + while (a[--i] == null) { } // NOPMD + int pos = mix(a[i].hashCode()) & m; + if (b[pos] != null) { + for (;;) { + pos = (pos + 1) & m; + if (b[pos] == null) { + break; + } + } + } + b[pos] = a[i]; + } + + this.mask = m; + this.maxSize = (int)(newCap * loadFactor); + this.keys = b; + } + + static int mix(int x) { + final int h = x * INT_PHI; + return h ^ (h >>> 16); + } + + public Object[] keys() { + return keys; // NOPMD + } + + public int size() { + return size; + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/util/Pow2.java b/src/main/java/io/reactivex/rxjava3/internal/util/Pow2.java new file mode 100644 index 0000000000..fd30ee04fb --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/util/Pow2.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +/* + * Original License: https://github.com/JCTools/JCTools/blob/master/LICENSE + * Original location: https://github.com/JCTools/JCTools/blob/master/jctools-core/src/main/java/org/jctools/util/Pow2.java + */ + +package io.reactivex.rxjava3.internal.util; + +public final class Pow2 { + private Pow2() { + throw new IllegalStateException("No instances!"); + } + + /** + * Find the next larger positive power of two value up from the given value. If value is a power of two then + * this value will be returned. + * + * @param value from which next positive power of two will be found. + * @return the next positive power of 2 or this value if it is a power of 2. + */ + public static int roundToPowerOfTwo(final int value) { + return 1 << (32 - Integer.numberOfLeadingZeros(value - 1)); + } + + /** + * Is this value a power of two. + * + * @param value to be tested to see if it is a power of two. + * @return true if the value is a power of 2 otherwise false. + */ + public static boolean isPowerOfTwo(final int value) { + return (value & (value - 1)) == 0; + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/util/QueueDrain.java b/src/main/java/io/reactivex/rxjava3/internal/util/QueueDrain.java new file mode 100644 index 0000000000..481cbe0012 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/util/QueueDrain.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.util; + +import org.reactivestreams.Subscriber; + +public interface QueueDrain<T, U> { + + boolean cancelled(); + + boolean done(); + + Throwable error(); + + boolean enter(); + + long requested(); + + long produced(long n); + + /** + * Adds m to the wip counter. + * @param m the value to add + * @return the current value after adding m + */ + int leave(int m); + + /** + * Accept the value and return true if forwarded. + * @param a the subscriber + * @param v the value + * @return true if the value was delivered + */ + boolean accept(Subscriber<? super U> a, T v); +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/util/QueueDrainHelper.java b/src/main/java/io/reactivex/rxjava3/internal/util/QueueDrainHelper.java new file mode 100644 index 0000000000..fa0c500892 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/util/QueueDrainHelper.java @@ -0,0 +1,430 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.util; + +import java.util.Queue; +import java.util.concurrent.atomic.AtomicLong; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.Observer; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.BooleanSupplier; +import io.reactivex.rxjava3.operators.*; + +/** + * Utility class to help with the queue-drain serialization idiom. + */ +public final class QueueDrainHelper { + /** Utility class. */ + private QueueDrainHelper() { + throw new IllegalStateException("No instances!"); + } + + /** + * Drain the queue but give up with an error if there aren't enough requests. + * @param <T> the queue value type + * @param <U> the emission value type + * @param q the queue + * @param a the subscriber + * @param delayError true if errors should be delayed after all normal items + * @param dispose the disposable to call when termination happens and cleanup is necessary + * @param qd the QueueDrain instance that gives status information to the drain logic + */ + public static <T, U> void drainMaxLoop(SimplePlainQueue<T> q, Subscriber<? super U> a, boolean delayError, + Disposable dispose, QueueDrain<T, U> qd) { + int missed = 1; + + for (;;) { + for (;;) { + boolean d = qd.done(); + + T v = q.poll(); + + boolean empty = v == null; + + if (checkTerminated(d, empty, a, delayError, q, qd)) { + if (dispose != null) { + dispose.dispose(); + } + return; + } + + if (empty) { + break; + } + + long r = qd.requested(); + if (r != 0L) { + if (qd.accept(a, v)) { + if (r != Long.MAX_VALUE) { + qd.produced(1); + } + } + } else { + q.clear(); + if (dispose != null) { + dispose.dispose(); + } + a.onError(MissingBackpressureException.createDefault()); + return; + } + } + + missed = qd.leave(-missed); + if (missed == 0) { + break; + } + } + } + + public static <T, U> boolean checkTerminated(boolean d, boolean empty, + Subscriber<?> s, boolean delayError, SimpleQueue<?> q, QueueDrain<T, U> qd) { + if (qd.cancelled()) { + q.clear(); + return true; + } + + if (d) { + if (delayError) { + if (empty) { + Throwable err = qd.error(); + if (err != null) { + s.onError(err); + } else { + s.onComplete(); + } + return true; + } + } else { + Throwable err = qd.error(); + if (err != null) { + q.clear(); + s.onError(err); + return true; + } else + if (empty) { + s.onComplete(); + return true; + } + } + } + + return false; + } + + public static <T, U> void drainLoop(SimplePlainQueue<T> q, Observer<? super U> a, boolean delayError, Disposable dispose, ObservableQueueDrain<T, U> qd) { + + int missed = 1; + + for (;;) { + if (checkTerminated(qd.done(), q.isEmpty(), a, delayError, q, dispose, qd)) { + return; + } + + for (;;) { + boolean d = qd.done(); + T v = q.poll(); + boolean empty = v == null; + + if (checkTerminated(d, empty, a, delayError, q, dispose, qd)) { + return; + } + + if (empty) { + break; + } + + qd.accept(a, v); + } + + missed = qd.leave(-missed); + if (missed == 0) { + break; + } + } + } + + public static <T, U> boolean checkTerminated(boolean d, boolean empty, + Observer<?> observer, boolean delayError, SimpleQueue<?> q, Disposable disposable, ObservableQueueDrain<T, U> qd) { + if (qd.cancelled()) { + q.clear(); + disposable.dispose(); + return true; + } + + if (d) { + if (delayError) { + if (empty) { + if (disposable != null) { + disposable.dispose(); + } + Throwable err = qd.error(); + if (err != null) { + observer.onError(err); + } else { + observer.onComplete(); + } + return true; + } + } else { + Throwable err = qd.error(); + if (err != null) { + q.clear(); + if (disposable != null) { + disposable.dispose(); + } + observer.onError(err); + return true; + } else + if (empty) { + if (disposable != null) { + disposable.dispose(); + } + observer.onComplete(); + return true; + } + } + } + + return false; + } + + /** + * Creates a queue: spsc-array if capacityHint is positive and + * spsc-linked-array if capacityHint is negative; in both cases, the + * capacity is the absolute value of prefetch. + * @param <T> the value type of the queue + * @param capacityHint the capacity hint, negative value will create an array-based SPSC queue + * @return the queue instance + */ + public static <T> SimpleQueue<T> createQueue(int capacityHint) { + if (capacityHint < 0) { + return new SpscLinkedArrayQueue<>(-capacityHint); + } + return new SpscArrayQueue<>(capacityHint); + } + + /** + * Requests {@link Long#MAX_VALUE} if prefetch is negative or the exact + * amount if prefetch is positive. + * @param s the Subscription to request from + * @param prefetch the prefetch value + */ + public static void request(Subscription s, int prefetch) { + s.request(prefetch < 0 ? Long.MAX_VALUE : prefetch); + } + + static final long COMPLETED_MASK = 0x8000000000000000L; + static final long REQUESTED_MASK = 0x7FFFFFFFFFFFFFFFL; + + /** + * Accumulates requests (not validated) and handles the completed mode draining of the queue based on the requests. + * + * <p> + * Post-completion backpressure handles the case when a source produces values based on + * requests when it is active but more values are available even after its completion. + * In this case, the onComplete() can't just emit the contents of the queue but has to + * coordinate with the requested amounts. This requires two distinct modes: active and + * completed. In active mode, requests flow through and the queue is not accessed but + * in completed mode, requests no-longer reach the upstream but help in draining the queue. + * + * @param <T> the value type emitted + * @param n the request amount, positive (not validated) + * @param actual the target Subscriber to send events to + * @param queue the queue to drain if in the post-complete state + * @param state holds the request amount and the post-completed flag + * @param isCancelled a supplier that returns true if the drain has been cancelled + * @return true if the state indicates a completion state. + */ + public static <T> boolean postCompleteRequest(long n, + Subscriber<? super T> actual, + Queue<T> queue, + AtomicLong state, + BooleanSupplier isCancelled) { + for (; ; ) { + long r = state.get(); + + // extract the current request amount + long r0 = r & REQUESTED_MASK; + + // preserve COMPLETED_MASK and calculate new requested amount + long u = (r & COMPLETED_MASK) | BackpressureHelper.addCap(r0, n); + + if (state.compareAndSet(r, u)) { + // (complete, 0) -> (complete, n) transition then replay + if (r == COMPLETED_MASK) { + + postCompleteDrain(n | COMPLETED_MASK, actual, queue, state, isCancelled); + + return true; + } + // (active, r) -> (active, r + n) transition then continue with requesting from upstream + return false; + } + } + + } + + static boolean isCancelled(BooleanSupplier cancelled) { + try { + return cancelled.getAsBoolean(); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + return true; + } + } + + /** + * Drains the queue based on the outstanding requests in post-completed mode (only!). + * + * @param <T> the value type + * @param n the current request amount + * @param actual the target Subscriber to send events to + * @param queue the queue to drain if in the post-complete state + * @param state holds the request amount and the post-completed flag + * @param isCancelled a supplier that returns true if the drain has been cancelled + * @return true if the queue was completely drained or the drain process was cancelled + */ + static <T> boolean postCompleteDrain(long n, + Subscriber<? super T> actual, + Queue<T> queue, + AtomicLong state, + BooleanSupplier isCancelled) { + +// TODO enable fast-path +// if (n == -1 || n == Long.MAX_VALUE) { +// for (;;) { +// if (isCancelled.getAsBoolean()) { +// break; +// } +// +// T v = queue.poll(); +// +// if (v == null) { +// actual.onComplete(); +// break; +// } +// +// actual.onNext(v); +// } +// +// return true; +// } + + long e = n & COMPLETED_MASK; + + for (; ; ) { + + while (e != n) { + if (isCancelled(isCancelled)) { + return true; + } + + T t = queue.poll(); + + if (t == null) { + actual.onComplete(); + return true; + } + + actual.onNext(t); + e++; + } + + if (isCancelled(isCancelled)) { + return true; + } + + if (queue.isEmpty()) { + actual.onComplete(); + return true; + } + + n = state.get(); + + if (n == e) { + + n = state.addAndGet(-(e & REQUESTED_MASK)); + + if ((n & REQUESTED_MASK) == 0L) { + return false; + } + + e = n & COMPLETED_MASK; + } + } + + } + + /** + * Signals the completion of the main sequence and switches to post-completion replay mode. + * + * <p> + * Don't modify the queue after calling this method! + * + * <p> + * Post-completion backpressure handles the case when a source produces values based on + * requests when it is active but more values are available even after its completion. + * In this case, the onComplete() can't just emit the contents of the queue but has to + * coordinate with the requested amounts. This requires two distinct modes: active and + * completed. In active mode, requests flow through and the queue is not accessed but + * in completed mode, requests no-longer reach the upstream but help in draining the queue. + * <p> + * The algorithm utilizes the most significant bit (bit 63) of a long value (AtomicLong) since + * request amount only goes up to {@link Long#MAX_VALUE} (bits 0-62) and negative values aren't + * allowed. + * + * @param <T> the value type emitted + * @param actual the target Subscriber to send events to + * @param queue the queue to drain if in the post-complete state + * @param state holds the request amount and the post-completed flag + * @param isCancelled a supplier that returns true if the drain has been cancelled + */ + public static <T> void postComplete(Subscriber<? super T> actual, + Queue<T> queue, + AtomicLong state, + BooleanSupplier isCancelled) { + + if (queue.isEmpty()) { + actual.onComplete(); + return; + } + + if (postCompleteDrain(state.get(), actual, queue, state, isCancelled)) { + return; + } + + for (; ; ) { + long r = state.get(); + + if ((r & COMPLETED_MASK) != 0L) { + return; + } + + long u = r | COMPLETED_MASK; + // (active, r) -> (complete, r) transition + if (state.compareAndSet(r, u)) { + // if the requested amount was non-zero, drain the queue + if (r != 0L) { + postCompleteDrain(u, actual, queue, state, isCancelled); + } + + return; + } + } + + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/util/SorterFunction.java b/src/main/java/io/reactivex/rxjava3/internal/util/SorterFunction.java new file mode 100644 index 0000000000..b74f1bccf6 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/util/SorterFunction.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.util; + +import java.util.*; + +import io.reactivex.rxjava3.functions.Function; + +public final class SorterFunction<T> implements Function<List<T>, List<T>> { + + final Comparator<? super T> comparator; + + public SorterFunction(Comparator<? super T> comparator) { + this.comparator = comparator; + } + + @Override + public List<T> apply(List<T> t) { + Collections.sort(t, comparator); + return t; + } +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/util/SuppressAnimalSniffer.java b/src/main/java/io/reactivex/rxjava3/internal/util/SuppressAnimalSniffer.java new file mode 100644 index 0000000000..b55fb0673b --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/util/SuppressAnimalSniffer.java @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.util; + +import java.lang.annotation.*; + +/** + * Suppress errors by the AnimalSniffer plugin. + */ +@Retention(RetentionPolicy.CLASS) +@Documented +@Target({ElementType.METHOD, ElementType.CONSTRUCTOR, ElementType.TYPE}) +public @interface SuppressAnimalSniffer { + +} diff --git a/src/main/java/io/reactivex/rxjava3/internal/util/VolatileSizeArrayList.java b/src/main/java/io/reactivex/rxjava3/internal/util/VolatileSizeArrayList.java new file mode 100644 index 0000000000..798a05a6dc --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/internal/util/VolatileSizeArrayList.java @@ -0,0 +1,189 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.util; + +import io.reactivex.rxjava3.annotations.NonNull; + +import java.util.*; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * Tracks the current underlying array size in a volatile field. + * + * @param <T> the element type + * @since 2.0.7 + */ +public final class VolatileSizeArrayList<T> extends AtomicInteger implements List<T>, RandomAccess { + + private static final long serialVersionUID = 3972397474470203923L; + + final ArrayList<T> list; + + public VolatileSizeArrayList() { + list = new ArrayList<>(); + } + + public VolatileSizeArrayList(int initialCapacity) { + list = new ArrayList<>(initialCapacity); + } + + @Override + public int size() { + return get(); + } + + @Override + public boolean isEmpty() { + return get() == 0; + } + + @Override + public boolean contains(Object o) { + return list.contains(o); + } + + @Override + public Iterator<T> iterator() { + return list.iterator(); + } + + @Override + public Object[] toArray() { + return list.toArray(); + } + + @Override + public <E> E[] toArray(@NonNull E[] a) { + return list.toArray(a); + } + + @Override + public boolean add(T e) { + boolean b = list.add(e); + lazySet(list.size()); + return b; + } + + @Override + public boolean remove(Object o) { + boolean b = list.remove(o); + lazySet(list.size()); + return b; + } + + @Override + public boolean containsAll(@NonNull Collection<?> c) { + return list.containsAll(c); + } + + @Override + public boolean addAll(@NonNull Collection<? extends T> c) { + boolean b = list.addAll(c); + lazySet(list.size()); + return b; + } + + @Override + public boolean addAll(int index, @NonNull Collection<? extends T> c) { + boolean b = list.addAll(index, c); + lazySet(list.size()); + return b; + } + + @Override + public boolean removeAll(@NonNull Collection<?> c) { + boolean b = list.removeAll(c); + lazySet(list.size()); + return b; + } + + @Override + public boolean retainAll(@NonNull Collection<?> c) { + boolean b = list.retainAll(c); + lazySet(list.size()); + return b; + } + + @Override + public void clear() { + list.clear(); + lazySet(0); + } + + @Override + public T get(int index) { + return list.get(index); + } + + @Override + public T set(int index, T element) { + return list.set(index, element); + } + + @Override + public void add(int index, T element) { + list.add(index, element); + lazySet(list.size()); + } + + @Override + public T remove(int index) { + T v = list.remove(index); + lazySet(list.size()); + return v; + } + + @Override + public int indexOf(Object o) { + return list.indexOf(o); + } + + @Override + public int lastIndexOf(Object o) { + return list.lastIndexOf(o); + } + + @Override + public ListIterator<T> listIterator() { + return list.listIterator(); + } + + @Override + public ListIterator<T> listIterator(int index) { + return list.listIterator(index); + } + + @Override + public List<T> subList(int fromIndex, int toIndex) { + return list.subList(fromIndex, toIndex); + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof VolatileSizeArrayList) { + return list.equals(((VolatileSizeArrayList<?>)obj).list); + } + return list.equals(obj); + } + + @Override + public int hashCode() { + return list.hashCode(); + } + + @Override + public String toString() { + return list.toString(); + } +} diff --git a/src/main/java/io/reactivex/rxjava3/observables/ConnectableObservable.java b/src/main/java/io/reactivex/rxjava3/observables/ConnectableObservable.java new file mode 100644 index 0000000000..231d0357fe --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/observables/ConnectableObservable.java @@ -0,0 +1,347 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.observables; + +import java.util.Objects; +import java.util.concurrent.TimeUnit; + +import io.reactivex.rxjava3.annotations.*; +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.functions.Consumer; +import io.reactivex.rxjava3.internal.functions.*; +import io.reactivex.rxjava3.internal.operators.observable.*; +import io.reactivex.rxjava3.internal.util.ConnectConsumer; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.schedulers.Schedulers; + +/** + * A {@code ConnectableObservable} resembles an ordinary {@link Observable}, except that it does not begin + * emitting items when it is subscribed to, but only when its {@link #connect} method is called. In this way you + * can wait for all intended {@link Observer}s to {@link Observable#subscribe} to the {@code Observable} + * before the {@code Observable} begins emitting items. + * <p> + * <img width="640" height="510" src="https://github.com/ReactiveX/RxJava/wiki/images/rx-operators/publishConnect.v3.png" alt=""> + * <p> + * When the upstream terminates, the {@code ConnectableObservable} remains in this terminated state and, + * depending on the actual underlying implementation, relays cached events to late {@code Observer}s. + * In order to reuse and restart this {@code ConnectableObservable}, the {@link #reset()} method has to be called. + * When called, this {@code ConnectableObservable} will appear as fresh, unconnected source to new {@code Observer}s. + * Disposing the connection will reset the {@code ConnectableObservable} to its fresh state and there is no need to call + * {@link #reset()} in this case. + * <p> + * Note that although {@link #connect()} and {@link #reset()} are safe to call from multiple threads, it is recommended + * a dedicated thread or business logic manages the connection or resetting of a {@code ConnectableObservable} so that + * there is no unwanted signal loss due to early {@code connect()} or {@code reset()} calls while {@code Observer}s are + * still being subscribed to to this {@code ConnectableObservable} to receive signals from the get go. + * + * @see <a href="https://github.com/ReactiveX/RxJava/wiki/Connectable-Observable-Operators">RxJava Wiki: Connectable Observable Operators</a> + * @param <T> + * the type of items emitted by the {@code ConnectableObservable} + */ +public abstract class ConnectableObservable<T> extends Observable<T> { + + /** + * Instructs the {@code ConnectableObservable} to begin emitting the items from its underlying + * {@link Observable} to its {@link Observer}s. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>The behavior is determined by the implementor of this abstract class.</dd> + * </dl> + * + * @param connection + * the action that receives the connection subscription before the subscription to source happens + * allowing the caller to synchronously disconnect a synchronous source + * @throws NullPointerException if {@code connection} is {@code null} + * @see <a href="http://reactivex.io/documentation/operators/connect.html">ReactiveX documentation: Connect</a> + */ + @SchedulerSupport(SchedulerSupport.NONE) + public abstract void connect(@NonNull Consumer<? super Disposable> connection); + + /** + * Resets this {@code ConnectableObservable} into its fresh state if it has terminated + * or has been disposed. + * <p> + * Calling this method on a fresh or active {@code ConnectableObservable} has no effect. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>The behavior is determined by the implementor of this abstract class.</dd> + * </dl> + * @since 3.0.0 + */ + @SchedulerSupport(SchedulerSupport.NONE) + public abstract void reset(); + + /** + * Instructs the {@code ConnectableObservable} to begin emitting the items from its underlying + * {@link Observable} to its {@link Observer}s. + * <p> + * To disconnect from a synchronous source, use the {@link #connect(Consumer)} method. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>The behavior is determined by the implementor of this abstract class.</dd> + * </dl> + * + * @return the {@link Disposable} representing the connection + * @see <a href="http://reactivex.io/documentation/operators/connect.html">ReactiveX documentation: Connect</a> + */ + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + public final Disposable connect() { + ConnectConsumer cc = new ConnectConsumer(); + connect(cc); + return cc.disposable; + } + + /** + * Returns an {@link Observable} that stays connected to this {@code ConnectableObservable} as long as there + * is at least one subscription to this {@code ConnectableObservable}. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>This {@code refCount} overload does not operate on any particular {@link Scheduler}.</dd> + * </dl> + * @return a new {@code Observable} instance + * @see <a href="http://reactivex.io/documentation/operators/refcount.html">ReactiveX documentation: RefCount</a> + * @see #refCount(int) + * @see #refCount(long, TimeUnit) + * @see #refCount(int, long, TimeUnit) + */ + @NonNull + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + public Observable<T> refCount() { + return RxJavaPlugins.onAssembly(new ObservableRefCount<>(this)); + } + + /** + * Connects to the upstream {@code ConnectableObservable} if the number of subscribed + * observers reaches the specified count and disconnect if all {@link Observer}s have unsubscribed. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>This {@code refCount} overload does not operate on any particular {@link Scheduler}.</dd> + * </dl> + * <p>History: 2.1.14 - experimental + * @param observerCount the number of {@code Observer}s required to connect to the upstream + * @return the new {@link Observable} instance + * @throws IllegalArgumentException if {@code observerCount} is non-positive + * @since 2.2 + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final Observable<T> refCount(int observerCount) { + return refCount(observerCount, 0, TimeUnit.NANOSECONDS, Schedulers.trampoline()); + } + + /** + * Connects to the upstream {@code ConnectableObservable} if the number of subscribed + * observers reaches 1 and disconnect after the specified + * timeout if all {@link Observer}s have unsubscribed. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>This {@code refCount} overload operates on the {@code computation} {@link Scheduler}.</dd> + * </dl> + * <p>History: 2.1.14 - experimental + * @param timeout the time to wait before disconnecting after all {@code Observer}s unsubscribed + * @param unit the time unit of the timeout + * @return the new {@link Observable} instance + * @throws NullPointerException if {@code unit} is {@code null} + * @see #refCount(long, TimeUnit, Scheduler) + * @since 2.2 + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.COMPUTATION) + @NonNull + public final Observable<T> refCount(long timeout, @NonNull TimeUnit unit) { + return refCount(1, timeout, unit, Schedulers.computation()); + } + + /** + * Connects to the upstream {@code ConnectableObservable} if the number of subscribed + * observers reaches 1 and disconnect after the specified + * timeout if all {@link Observer}s have unsubscribed. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>This {@code refCount} overload operates on the specified {@link Scheduler}.</dd> + * </dl> + * <p>History: 2.1.14 - experimental + * @param timeout the time to wait before disconnecting after all {@code Observer}s unsubscribed + * @param unit the time unit of the timeout + * @param scheduler the target scheduler to wait on before disconnecting + * @return the new {@link Observable} instance + * @throws NullPointerException if {@code unit} or {@code scheduler} is {@code null} + * @since 2.2 + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.CUSTOM) + @NonNull + public final Observable<T> refCount(long timeout, @NonNull TimeUnit unit, @NonNull Scheduler scheduler) { + return refCount(1, timeout, unit, scheduler); + } + + /** + * Connects to the upstream {@code ConnectableObservable} if the number of subscribed + * observers reaches the specified count and disconnect after the specified + * timeout if all {@link Observer}s have unsubscribed. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>This {@code refCount} overload operates on the {@code computation} {@link Scheduler}.</dd> + * </dl> + * <p>History: 2.1.14 - experimental + * @param observerCount the number of {@code Observer}s required to connect to the upstream + * @param timeout the time to wait before disconnecting after all {@code Observer}s unsubscribed + * @param unit the time unit of the timeout + * @return the new {@link Observable} instance + * @throws NullPointerException if {@code unit} or {@code scheduler} is {@code null} + * @throws IllegalArgumentException if {@code observerCount} is non-positive + * @see #refCount(int, long, TimeUnit, Scheduler) + * @since 2.2 + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.COMPUTATION) + @NonNull + public final Observable<T> refCount(int observerCount, long timeout, @NonNull TimeUnit unit) { + return refCount(observerCount, timeout, unit, Schedulers.computation()); + } + + /** + * Connects to the upstream {@code ConnectableObservable} if the number of subscribed + * observers reaches the specified count and disconnect after the specified + * timeout if all {@link Observer}s have unsubscribed. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>This {@code refCount} overload operates on the specified {@link Scheduler}.</dd> + * </dl> + * <p>History: 2.1.14 - experimental + * @param observerCount the number of {@code Observer}s required to connect to the upstream + * @param timeout the time to wait before disconnecting after all {@code Observer}s unsubscribed + * @param unit the time unit of the timeout + * @param scheduler the target scheduler to wait on before disconnecting + * @return the new {@link Observable} instance + * @throws NullPointerException if {@code unit} or {@code scheduler} is {@code null} + * @throws IllegalArgumentException if {@code observerCount} is non-positive + * @since 2.2 + */ + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.CUSTOM) + @NonNull + public final Observable<T> refCount(int observerCount, long timeout, @NonNull TimeUnit unit, @NonNull Scheduler scheduler) { + ObjectHelper.verifyPositive(observerCount, "observerCount"); + Objects.requireNonNull(unit, "unit is null"); + Objects.requireNonNull(scheduler, "scheduler is null"); + return RxJavaPlugins.onAssembly(new ObservableRefCount<>(this, observerCount, timeout, unit, scheduler)); + } + + /** + * Returns an {@link Observable} that automatically connects (at most once) to this {@code ConnectableObservable} + * when the first {@link Observer} subscribes. + * <p> + * <img width="640" height="348" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/autoConnect.o.png" alt=""> + * <p> + * The connection happens after the first subscription and happens at most once + * during the lifetime of the returned {@code Observable}. If this {@code ConnectableObservable} + * terminates, the connection is never renewed, no matter how {@code Observer}s come + * and go. Use {@link #refCount()} to renew a connection or dispose an active + * connection when all {@code Observer}s have disposed their {@link Disposable}s. + * <p> + * This overload does not allow disconnecting the connection established via + * {@link #connect(Consumer)}. Use the {@link #autoConnect(int, Consumer)} overload + * to gain access to the {@code Disposable} representing the only connection. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code autoConnect} overload does not operate on any particular {@link Scheduler}.</dd> + * </dl> + * + * @return a new {@code Observable} instance that automatically connects to this {@code ConnectableObservable} + * when the first {@code Observer} subscribes + */ + @NonNull + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + public Observable<T> autoConnect() { + return autoConnect(1); + } + + /** + * Returns an {@link Observable} that automatically connects (at most once) to this {@code ConnectableObservable} + * when the specified number of {@link Observer}s subscribe to it. + * <p> + * <img width="640" height="348" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/autoConnect.o.png" alt=""> + * <p> + * The connection happens after the given number of subscriptions and happens at most once + * during the lifetime of the returned {@code Observable}. If this {@code ConnectableObservable} + * terminates, the connection is never renewed, no matter how {@code Observer}s come + * and go. Use {@link #refCount()} to renew a connection or dispose an active + * connection when all {@code Observer}s have disposed their {@link Disposable}s. + * <p> + * This overload does not allow disconnecting the connection established via + * {@link #connect(Consumer)}. Use the {@link #autoConnect(int, Consumer)} overload + * to gain access to the {@code Disposable} representing the only connection. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code autoConnect} overload does not operate on any particular {@link Scheduler}.</dd> + * </dl> + * + * @param numberOfObservers the number of subscribers to await before calling connect + * on the {@code ConnectableObservable}. A non-positive value indicates + * an immediate connection. + * @return a new {@code Observable} instance that automatically connects to this {@code ConnectableObservable} + * when the specified number of {@code Observer}s subscribe to it + */ + @NonNull + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + public Observable<T> autoConnect(int numberOfObservers) { + return autoConnect(numberOfObservers, Functions.emptyConsumer()); + } + + /** + * Returns an {@link Observable} that automatically connects (at most once) to this {@code ConnectableObservable} + * when the specified number of {@link Observer}s subscribe to it and calls the + * specified callback with the {@link Disposable} associated with the established connection. + * <p> + * <img width="640" height="348" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/autoConnect.o.png" alt=""> + * <p> + * The connection happens after the given number of subscriptions and happens at most once + * during the lifetime of the returned {@code Observable}. If this {@code ConnectableObservable} + * terminates, the connection is never renewed, no matter how {@code Observer}s come + * and go. Use {@link #refCount()} to renew a connection or dispose an active + * connection when all {@code Observer}s have disposed their {@code Disposable}s. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code autoConnect} overload does not operate on any particular {@link Scheduler}.</dd> + * </dl> + * + * @param numberOfObservers the number of subscribers to await before calling connect + * on the {@code ConnectableObservable}. A non-positive value indicates + * an immediate connection. + * @param connection the callback {@link Consumer} that will receive the {@code Disposable} representing the + * established connection + * @return a new {@code Observable} instance that automatically connects to this {@code ConnectableObservable} + * when the specified number of {@code Observer}s subscribe to it and calls the + * specified callback with the {@code Disposable} associated with the established connection + * @throws NullPointerException if {@code connection} is {@code null} + */ + @NonNull + @CheckReturnValue + @SchedulerSupport(SchedulerSupport.NONE) + public Observable<T> autoConnect(int numberOfObservers, @NonNull Consumer<? super Disposable> connection) { + Objects.requireNonNull(connection, "connection is null"); + if (numberOfObservers <= 0) { + this.connect(connection); + return RxJavaPlugins.onAssembly(this); + } + return RxJavaPlugins.onAssembly(new ObservableAutoConnect<>(this, numberOfObservers, connection)); + } +} diff --git a/src/main/java/io/reactivex/rxjava3/observables/GroupedObservable.java b/src/main/java/io/reactivex/rxjava3/observables/GroupedObservable.java new file mode 100644 index 0000000000..baa35ce48d --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/observables/GroupedObservable.java @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.observables; + +import io.reactivex.rxjava3.annotations.Nullable; +import io.reactivex.rxjava3.core.Observable; + +/** + * An {@link Observable} that has been grouped by key, the value of which can be obtained with {@link #getKey()}. + * <p> + * <em>Note:</em> A {@link GroupedObservable} will cache the items it is to emit until such time as it + * is subscribed to. For this reason, in order to avoid memory leaks, you should not simply ignore those + * {@code GroupedObservable}s that do not concern you. Instead, you can signal to them that they + * may discard their buffers by applying an operator like {@link Observable#take take}{@code (0)} to them. + * + * @param <K> + * the type of the key + * @param <T> + * the type of the items emitted by the {@code GroupedObservable} + * @see Observable#groupBy(io.reactivex.rxjava3.functions.Function) + * @see <a href="http://reactivex.io/documentation/operators/groupby.html">ReactiveX documentation: GroupBy</a> + */ +public abstract class GroupedObservable<K, T> extends Observable<T> { + + final K key; + + /** + * Constructs a GroupedObservable with the given key. + * @param key the key + */ + protected GroupedObservable(@Nullable K key) { + this.key = key; + } + + /** + * Returns the key that identifies the group of items emitted by this {@code GroupedObservable}. + * + * @return the key that the items emitted by this {@code GroupedObservable} were grouped by + */ + @Nullable + public K getKey() { + return key; + } +} diff --git a/src/main/java/io/reactivex/rxjava3/observables/package-info.java b/src/main/java/io/reactivex/rxjava3/observables/package-info.java new file mode 100644 index 0000000000..93c6ad0c40 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/observables/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +/** + * Classes supporting the Observable base reactive class: + * {@link io.reactivex.rxjava3.observables.ConnectableObservable} and + * {@link io.reactivex.rxjava3.observables.GroupedObservable}. + */ +package io.reactivex.rxjava3.observables; diff --git a/src/main/java/io/reactivex/rxjava3/observers/BaseTestConsumer.java b/src/main/java/io/reactivex/rxjava3/observers/BaseTestConsumer.java new file mode 100644 index 0000000000..6b3a72fadb --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/observers/BaseTestConsumer.java @@ -0,0 +1,660 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.observers; + +import java.util.*; +import java.util.concurrent.*; + +import io.reactivex.rxjava3.annotations.*; +import io.reactivex.rxjava3.exceptions.CompositeException; +import io.reactivex.rxjava3.functions.Predicate; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.internal.util.*; + +/** + * Base class with shared infrastructure to support + * {@link io.reactivex.rxjava3.subscribers.TestSubscriber TestSubscriber} and {@link TestObserver}. + * @param <T> the value type consumed + * @param <U> the subclass of this {@code BaseTestConsumer} + */ +public abstract class BaseTestConsumer<T, U extends BaseTestConsumer<T, U>> { + /** The latch that indicates an onError or onComplete has been called. */ + protected final CountDownLatch done; + /** The list of values received. */ + protected final List<T> values; + /** The list of errors received. */ + protected final List<Throwable> errors; + /** The number of completions. */ + protected long completions; + /** The last thread seen by the observer. */ + protected Thread lastThread; + + protected boolean checkSubscriptionOnce; + + /** + * The optional tag associated with this test consumer. + * @since 2.0.7 + */ + protected CharSequence tag; + + /** + * Indicates that one of the {@code awaitX} method has timed out. + * @since 2.0.7 + */ + protected boolean timeout; + + /** + * Constructs a {@code BaseTestConsumer} with {@code CountDownLatch} set to 1. + */ + public BaseTestConsumer() { + this.values = new VolatileSizeArrayList<>(); + this.errors = new VolatileSizeArrayList<>(); + this.done = new CountDownLatch(1); + } + + /** + * Returns a shared list of received {@code onNext} values or the single {@code onSuccess} value. + * <p> + * Note that accessing the items via certain methods of the {@link List} + * interface while the upstream is still actively emitting + * more items may result in a {@code ConcurrentModificationException}. + * <p> + * The {@link List#size()} method will return the number of items + * already received by this {@code TestObserver}/{@code TestSubscriber} in a thread-safe + * manner that can be read via {@link List#get(int)}) method + * (index range of 0 to {@code List.size() - 1}). + * <p> + * A view of the returned List can be created via {@link List#subList(int, int)} + * by using the bounds 0 (inclusive) to {@link List#size()} (exclusive) which, + * when accessed in a read-only fashion, should be also thread-safe and not throw any + * {@code ConcurrentModificationException}. + * @return a list of received onNext values + */ + @NonNull + public final List<T> values() { + return values; + } + + /** + * Fail with the given message and add the sequence of errors as suppressed ones. + * <p>Note this is deliberately the only fail method. Most of the times an assertion + * would fail but it is possible it was due to an exception somewhere. This construct + * will capture those potential errors and report it along with the original failure. + * + * @param message the message to use + * @return AssertionError the prepared AssertionError instance + */ + @NonNull + protected final AssertionError fail(@NonNull String message) { + StringBuilder b = new StringBuilder(64 + message.length()); + b.append(message); + + b.append(" (") + .append("latch = ").append(done.getCount()).append(", ") + .append("values = ").append(values.size()).append(", ") + .append("errors = ").append(errors.size()).append(", ") + .append("completions = ").append(completions) + ; + + if (timeout) { + b.append(", timeout!"); + } + + if (isDisposed()) { + b.append(", disposed!"); + } + + CharSequence tag = this.tag; + if (tag != null) { + b.append(", tag = ") + .append(tag); + } + + b + .append(')') + ; + + AssertionError ae = new AssertionError(b.toString()); + if (!errors.isEmpty()) { + if (errors.size() == 1) { + ae.initCause(errors.get(0)); + } else { + CompositeException ce = new CompositeException(errors); + ae.initCause(ce); + } + } + return ae; + } + + /** + * Awaits until this {@code TestObserver}/{@code TestSubscriber} receives an {@code onError} or {@code onComplete} events. + * @return this + * @throws InterruptedException if the current thread is interrupted while waiting + */ + @SuppressWarnings("unchecked") + @NonNull + public final U await() throws InterruptedException { + if (done.getCount() == 0) { + return (U)this; + } + + done.await(); + return (U)this; + } + + /** + * Awaits the specified amount of time or until this {@code TestObserver}/{@code TestSubscriber} + * receives an {@code onError} or {@code onComplete} events, whichever happens first. + * @param time the waiting time + * @param unit the time unit of the waiting time + * @return true if the {@code TestObserver}/{@code TestSubscriber} terminated, false if timeout happened + * @throws InterruptedException if the current thread is interrupted while waiting + */ + public final boolean await(long time, @NonNull TimeUnit unit) throws InterruptedException { + boolean d = done.getCount() == 0 || (done.await(time, unit)); + timeout = !d; + return d; + } + + // assertion methods + + /** + * Assert that this {@code TestObserver}/{@code TestSubscriber} received exactly one {@code onComplete} event. + * @return this + */ + @SuppressWarnings("unchecked") + @NonNull + public final U assertComplete() { + long c = completions; + if (c == 0) { + throw fail("Not completed"); + } else + if (c > 1) { + throw fail("Multiple completions: " + c); + } + return (U)this; + } + + /** + * Assert that this {@code TestObserver}/{@code TestSubscriber} has not received an {@code onComplete} event. + * @return this + */ + @SuppressWarnings("unchecked") + @NonNull + public final U assertNotComplete() { + long c = completions; + if (c == 1) { + throw fail("Completed!"); + } else + if (c > 1) { + throw fail("Multiple completions: " + c); + } + return (U)this; + } + + /** + * Assert that this {@code TestObserver}/{@code TestSubscriber} has not received an {@code onError} event. + * @return this + */ + @SuppressWarnings("unchecked") + @NonNull + public final U assertNoErrors() { + int s = errors.size(); + if (s != 0) { + throw fail("Error(s) present: " + errors); + } + return (U)this; + } + + /** + * Assert that this {@code TestObserver}/{@code TestSubscriber} received exactly the specified {@code onError} event value. + * + * <p>The comparison is performed via {@link Objects#equals(Object, Object)}; since most exceptions don't + * implement equals(), this assertion may fail. Use the {@link #assertError(Class)} + * overload to test against the class of an error instead of an instance of an error + * or {@link #assertError(Predicate)} to test with different condition. + * @param error the error to check + * @return this + * @see #assertError(Class) + * @see #assertError(Predicate) + */ + @NonNull + public final U assertError(@NonNull Throwable error) { + return assertError(Functions.equalsWith(error), true); + } + + /** + * Asserts that this {@code TestObserver}/{@code TestSubscriber} received exactly one {@code onError} event which is an + * instance of the specified {@code errorClass} {@link Class}. + * @param errorClass the error {@code Class} to expect + * @return this + */ + @SuppressWarnings({ "unchecked", "rawtypes" }) + @NonNull + public final U assertError(@NonNull Class<? extends Throwable> errorClass) { + return (U)assertError((Predicate)Functions.isInstanceOf(errorClass), true); + } + + /** + * Asserts that this {@code TestObserver}/{@code TestSubscriber} received exactly one {@code onError} event for which + * the provided predicate returns {@code true}. + * @param errorPredicate + * the predicate that receives the error {@link Throwable} + * and should return {@code true} for expected errors. + * @return this + */ + @NonNull + public final U assertError(@NonNull Predicate<Throwable> errorPredicate) { + return assertError(errorPredicate, false); + } + + @SuppressWarnings("unchecked") + @NonNull + private U assertError(@NonNull Predicate<Throwable> errorPredicate, boolean exact) { + int s = errors.size(); + if (s == 0) { + throw fail("No errors"); + } + + boolean found = false; + + for (Throwable e : errors) { + try { + if (errorPredicate.test(e)) { + found = true; + break; + } + } catch (Throwable ex) { + throw ExceptionHelper.wrapOrThrow(ex); + } + } + + if (found) { + if (s != 1) { + if (exact) { + throw fail("Error present but other errors as well"); + } + throw fail("One error passed the predicate but other errors are present as well"); + } + } else { + if (exact) { + throw fail("Error not present"); + } + throw fail("No error(s) passed the predicate"); + } + return (U)this; + } + + /** + * Assert that this {@code TestObserver}/{@code TestSubscriber} received exactly one {@code onNext} value which is equal to + * the given value with respect to {@link Objects#equals(Object, Object)}. + * @param value the value to expect + * @return this + */ + @SuppressWarnings("unchecked") + @NonNull + public final U assertValue(@NonNull T value) { + int s = values.size(); + if (s != 1) { + throw fail("\nexpected: " + valueAndClass(value) + "\ngot: " + values); + } + T v = values.get(0); + if (!Objects.equals(value, v)) { + throw fail("\nexpected: " + valueAndClass(value) + "\ngot: " + valueAndClass(v)); + } + return (U)this; + } + + /** + * Asserts that this {@code TestObserver}/{@code TestSubscriber} received exactly one {@code onNext} value for which + * the provided predicate returns {@code true}. + * @param valuePredicate + * the predicate that receives the {@code onNext} value + * and should return {@code true} for the expected value. + * @return this + */ + @SuppressWarnings("unchecked") + @NonNull + public final U assertValue(@NonNull Predicate<T> valuePredicate) { + assertValueAt(0, valuePredicate); + + if (values.size() > 1) { + throw fail("The first value passed the predicate but this consumer received more than one value"); + } + + return (U)this; + } + + /** + * Asserts that this {@code TestObserver}/{@code TestSubscriber} received an {@code onNext} value at the given index + * which is equal to the given value with respect to {@code null}-safe {@link Objects#equals(Object, Object)}. + * <p>History: 2.1.3 - experimental + * @param index the position to assert on + * @param value the value to expect + * @return this + * @since 2.2 + */ + @SuppressWarnings("unchecked") + @NonNull + public final U assertValueAt(int index, @NonNull T value) { + int s = values.size(); + if (s == 0) { + throw fail("No values"); + } + + if (index < 0 || index >= s) { + throw fail("Index " + index + " is out of range [0, " + s + ")"); + } + + T v = values.get(index); + if (!Objects.equals(value, v)) { + throw fail("\nexpected: " + valueAndClass(value) + "\ngot: " + valueAndClass(v) + + "; Value at position " + index + " differ"); + } + return (U)this; + } + + /** + * Asserts that this {@code TestObserver}/{@code TestSubscriber} received an {@code onNext} value at the given index + * for the provided predicate returns {@code true}. + * @param index the position to assert on + * @param valuePredicate + * the predicate that receives the {@code onNext} value + * and should return {@code true} for the expected value. + * @return this + */ + @SuppressWarnings("unchecked") + @NonNull + public final U assertValueAt(int index, @NonNull Predicate<T> valuePredicate) { + int s = values.size(); + if (s == 0) { + throw fail("No values"); + } + + if (index < 0 || index >= s) { + throw fail("Index " + index + " is out of range [0, " + s + ")"); + } + + boolean found = false; + + T v = values.get(index); + try { + if (valuePredicate.test(v)) { + found = true; + } + } catch (Throwable ex) { + throw ExceptionHelper.wrapOrThrow(ex); + } + + if (!found) { + throw fail("Value " + valueAndClass(v) + " at position " + index + " did not pass the predicate"); + } + return (U)this; + } + + /** + * Appends the class name to a non-{@code null} value or returns {@code "null"}. + * @param o the object + * @return the string representation + */ + @NonNull + public static String valueAndClass(@Nullable Object o) { + if (o != null) { + return o + " (class: " + o.getClass().getSimpleName() + ")"; + } + return "null"; + } + + /** + * Assert that this {@code TestObserver}/{@code TestSubscriber} received the specified number {@code onNext} events. + * @param count the expected number of {@code onNext} events + * @return this + */ + @SuppressWarnings("unchecked") + @NonNull + public final U assertValueCount(int count) { + int s = values.size(); + if (s != count) { + throw fail("\nexpected: " + count + "\ngot: " + s + "; Value counts differ"); + } + return (U)this; + } + + /** + * Assert that this {@code TestObserver}/{@code TestSubscriber} has not received any {@code onNext} events. + * @return this + */ + @NonNull + public final U assertNoValues() { + return assertValueCount(0); + } + + /** + * Assert that the {@code TestObserver}/{@code TestSubscriber} received only the specified values in the specified order. + * @param values the values expected + * @return this + */ + @SuppressWarnings("unchecked") + @SafeVarargs + @NonNull + public final U assertValues(@NonNull T... values) { + int s = this.values.size(); + if (s != values.length) { + throw fail("\nexpected: " + values.length + " " + Arrays.toString(values) + + "\ngot: " + s + " " + this.values + "; Value count differs"); + } + for (int i = 0; i < s; i++) { + T v = this.values.get(i); + T u = values[i]; + if (!Objects.equals(u, v)) { + throw fail("\nexpected: " + valueAndClass(u) + "\ngot: " + valueAndClass(v) + + "; Value at position " + i + " differ"); + } + } + return (U)this; + } + + /** + * Assert that the {@code TestObserver}/{@code TestSubscriber} received only the specified values in the specified order without terminating. + * <p>History: 2.1.4 - experimental + * @param values the values expected + * @return this + * @since 2.2 + */ + @SafeVarargs + @NonNull + public final U assertValuesOnly(@NonNull T... values) { + return assertSubscribed() + .assertValues(values) + .assertNoErrors() + .assertNotComplete(); + } + + /** + * Assert that the {@code TestObserver}/{@code TestSubscriber} received only the specified sequence of values in the same order. + * @param sequence the sequence of expected values in order + * @return this + */ + @SuppressWarnings("unchecked") + @NonNull + public final U assertValueSequence(@NonNull Iterable<? extends T> sequence) { + int i = 0; + Iterator<T> actualIterator = values.iterator(); + Iterator<? extends T> expectedIterator = sequence.iterator(); + boolean actualNext; + boolean expectedNext; + for (;;) { + expectedNext = expectedIterator.hasNext(); + actualNext = actualIterator.hasNext(); + + if (!actualNext || !expectedNext) { + break; + } + + T u = expectedIterator.next(); + T v = actualIterator.next(); + + if (!Objects.equals(u, v)) { + throw fail("\nexpected: " + valueAndClass(u) + "\ngot: " + valueAndClass(v) + + "; Value at position " + i + " differ"); + } + i++; + } + + if (actualNext) { + throw fail("More values received than expected (" + i + ")"); + } + if (expectedNext) { + throw fail("Fewer values received than expected (" + i + ")"); + } + return (U)this; + } + + /** + * Assert that the {@code onSubscribe} method was called exactly once. + * @return this + */ + @NonNull + protected abstract U assertSubscribed(); + + /** + * Assert that the upstream signaled the specified values in order and + * completed normally. + * @param values the expected values, asserted in order + * @return this + * @see #assertFailure(Class, Object...) + */ + @SafeVarargs + @NonNull + public final U assertResult(@NonNull T... values) { + return assertSubscribed() + .assertValues(values) + .assertNoErrors() + .assertComplete(); + } + + /** + * Assert that the upstream signaled the specified values in order + * and then failed with a specific class or subclass of {@link Throwable}. + * @param error the expected exception (parent) {@link Class} + * @param values the expected values, asserted in order + * @return this + */ + @SafeVarargs + @NonNull + public final U assertFailure(@NonNull Class<? extends Throwable> error, @NonNull T... values) { + return assertSubscribed() + .assertValues(values) + .assertError(error) + .assertNotComplete(); + } + + /** + * Awaits until the internal latch is counted down. + * <p>If the wait times out or gets interrupted, the {@code TestObserver}/{@code TestSubscriber} is cancelled. + * @param time the waiting time + * @param unit the time unit of the waiting time + * @return this + * @throws RuntimeException wrapping an {@link InterruptedException} if the wait is interrupted + */ + @SuppressWarnings("unchecked") + @NonNull + public final U awaitDone(long time, @NonNull TimeUnit unit) { + try { + if (!done.await(time, unit)) { + timeout = true; + dispose(); + } + } catch (InterruptedException ex) { + dispose(); + throw ExceptionHelper.wrapOrThrow(ex); + } + return (U)this; + } + + /** + * Assert that the {@code TestObserver}/{@code TestSubscriber} has received a + * {@link io.reactivex.rxjava3.disposables.Disposable Disposable}/{@link org.reactivestreams.Subscription Subscription} + * via {@code onSubscribe} but no other events. + * @return this + */ + @NonNull + public final U assertEmpty() { + return assertSubscribed() + .assertNoValues() + .assertNoErrors() + .assertNotComplete(); + } + + /** + * Set the tag displayed along with an assertion failure's + * other state information. + * <p>History: 2.0.7 - experimental + * @param tag the string to display ({@code null} won't print any tag) + * @return this + * @since 2.1 + */ + @SuppressWarnings("unchecked") + @NonNull + public final U withTag(@Nullable CharSequence tag) { + this.tag = tag; + return (U)this; + } + + /** + * Await until the {@code TestObserver}/{@code TestSubscriber} receives the given + * number of items or terminates by sleeping 10 milliseconds at a time + * up to 5000 milliseconds of timeout. + * <p>History: 2.0.7 - experimental + * @param atLeast the number of items expected at least + * @return this + * @since 2.1 + */ + @SuppressWarnings("unchecked") + @NonNull + public final U awaitCount(int atLeast) { + long start = System.currentTimeMillis(); + long timeoutMillis = 5000; + for (;;) { + if (System.currentTimeMillis() - start >= timeoutMillis) { + timeout = true; + break; + } + if (done.getCount() == 0L) { + break; + } + if (values.size() >= atLeast) { + break; + } + + try { + Thread.sleep(10); + } catch (InterruptedException ex) { + throw new RuntimeException(ex); + } + } + return (U)this; + } + + /** + * Returns true if this test consumer was cancelled/disposed. + * @return true if this test consumer was cancelled/disposed. + */ + protected abstract boolean isDisposed(); + + /** + * Cancel/dispose this test consumer. + */ + protected abstract void dispose(); +} diff --git a/src/main/java/io/reactivex/rxjava3/observers/DefaultObserver.java b/src/main/java/io/reactivex/rxjava3/observers/DefaultObserver.java new file mode 100644 index 0000000000..29d1f16d00 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/observers/DefaultObserver.java @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.observers; + +import io.reactivex.rxjava3.annotations.NonNull; +import io.reactivex.rxjava3.core.Observer; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; +import io.reactivex.rxjava3.internal.util.EndConsumerHelper; + +/** + * Abstract base implementation of an {@link io.reactivex.rxjava3.core.Observer Observer} with support for cancelling a + * subscription via {@link #cancel()} (synchronously) and calls {@link #onStart()} + * when the subscription happens. + * + * <p>All pre-implemented final methods are thread-safe. + * + * <p>Use the protected {@link #cancel()} to dispose the sequence from within an + * {@code onNext} implementation. + * + * <p>Like all other consumers, {@code DefaultObserver} can be subscribed only once. + * Any subsequent attempt to subscribe it to a new source will yield an + * {@link IllegalStateException} with message {@code "It is not allowed to subscribe with a(n) <class name> multiple times."}. + * + * <p>Implementation of {@link #onStart()}, {@link #onNext(Object)}, {@link #onError(Throwable)} + * and {@link #onComplete()} are not allowed to throw any unchecked exceptions. + * If for some reason this can't be avoided, use {@link io.reactivex.rxjava3.core.Observable#safeSubscribe(io.reactivex.rxjava3.core.Observer)} + * instead of the standard {@code subscribe()} method. + * + * <p>Example<pre><code> + * Observable.range(1, 5) + * .subscribe(new DefaultObserver<Integer>() { + * @Override public void onStart() { + * System.out.println("Start!"); + * } + * @Override public void onNext(Integer t) { + * if (t == 3) { + * cancel(); + * } + * System.out.println(t); + * } + * @Override public void onError(Throwable t) { + * t.printStackTrace(); + * } + * @Override public void onComplete() { + * System.out.println("Done!"); + * } + * }); + * </code></pre> + * + * @param <T> the value type + */ +public abstract class DefaultObserver<T> implements Observer<T> { + + private Disposable upstream; + + @Override + public final void onSubscribe(@NonNull Disposable d) { + if (EndConsumerHelper.validate(this.upstream, d, getClass())) { + this.upstream = d; + onStart(); + } + } + + /** + * Cancels the upstream's disposable. + */ + protected final void cancel() { + Disposable upstream = this.upstream; + this.upstream = DisposableHelper.DISPOSED; + upstream.dispose(); + } + /** + * Called once the subscription has been set on this observer; override this + * to perform initialization. + */ + protected void onStart() { + } + +} diff --git a/src/main/java/io/reactivex/rxjava3/observers/DisposableCompletableObserver.java b/src/main/java/io/reactivex/rxjava3/observers/DisposableCompletableObserver.java new file mode 100644 index 0000000000..20bf6d59d5 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/observers/DisposableCompletableObserver.java @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.observers; + +import java.util.concurrent.atomic.AtomicReference; + +import io.reactivex.rxjava3.annotations.NonNull; +import io.reactivex.rxjava3.core.CompletableObserver; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; +import io.reactivex.rxjava3.internal.util.EndConsumerHelper; + +/** + * An abstract {@link CompletableObserver} that allows asynchronous cancellation by implementing Disposable. + * + * <p>All pre-implemented final methods are thread-safe. + * + * <p>Like all other consumers, {@code DisposableCompletableObserver} can be subscribed only once. + * Any subsequent attempt to subscribe it to a new source will yield an + * {@link IllegalStateException} with message {@code "It is not allowed to subscribe with a(n) <class name> multiple times."}. + * + * <p>Implementation of {@link #onStart()}, {@link #onError(Throwable)} and + * {@link #onComplete()} are not allowed to throw any unchecked exceptions. + * + * <p>Example<pre><code> + * Disposable d = + * Completable.complete().delay(1, TimeUnit.SECONDS) + * .subscribeWith(new DisposableMaybeObserver<Integer>() { + * @Override public void onStart() { + * System.out.println("Start!"); + * } + * @Override public void onError(Throwable t) { + * t.printStackTrace(); + * } + * @Override public void onComplete() { + * System.out.println("Done!"); + * } + * }); + * // ... + * d.dispose(); + * </code></pre> + */ +public abstract class DisposableCompletableObserver implements CompletableObserver, Disposable { + + final AtomicReference<Disposable> upstream = new AtomicReference<>(); + + @Override + public final void onSubscribe(@NonNull Disposable d) { + if (EndConsumerHelper.setOnce(this.upstream, d, getClass())) { + onStart(); + } + } + + /** + * Called once the single upstream {@link Disposable} is set via {@link #onSubscribe(Disposable)}. + */ + protected void onStart() { + } + + @Override + public final boolean isDisposed() { + return upstream.get() == DisposableHelper.DISPOSED; + } + + @Override + public final void dispose() { + DisposableHelper.dispose(upstream); + } +} diff --git a/src/main/java/io/reactivex/rxjava3/observers/DisposableMaybeObserver.java b/src/main/java/io/reactivex/rxjava3/observers/DisposableMaybeObserver.java new file mode 100644 index 0000000000..6ea1d26e97 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/observers/DisposableMaybeObserver.java @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.observers; + +import java.util.concurrent.atomic.AtomicReference; + +import io.reactivex.rxjava3.annotations.NonNull; +import io.reactivex.rxjava3.core.MaybeObserver; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; +import io.reactivex.rxjava3.internal.util.EndConsumerHelper; + +/** + * An abstract {@link MaybeObserver} that allows asynchronous cancellation by implementing {@link Disposable}. + * + * <p>All pre-implemented final methods are thread-safe. + * + * <p>Note that {@link #onSuccess(Object)}, {@link #onError(Throwable)} and {@link #onComplete()} are + * exclusive to each other, unlike a regular {@link io.reactivex.rxjava3.core.Observer Observer}, and + * {@code onComplete()} is never called after an {@code onSuccess()}. + * + * <p>Like all other consumers, {@code DisposableMaybeObserver} can be subscribed only once. + * Any subsequent attempt to subscribe it to a new source will yield an + * {@link IllegalStateException} with message {@code "It is not allowed to subscribe with a(n) <class name> multiple times."}. + * + * <p>Implementation of {@link #onStart()}, {@link #onSuccess(Object)}, {@link #onError(Throwable)} and + * {@link #onComplete()} are not allowed to throw any unchecked exceptions. + * + * <p>Example<pre><code> + * Disposable d = + * Maybe.just(1).delay(1, TimeUnit.SECONDS) + * .subscribeWith(new DisposableMaybeObserver<Integer>() { + * @Override public void onStart() { + * System.out.println("Start!"); + * } + * @Override public void onSuccess(Integer t) { + * System.out.println(t); + * } + * @Override public void onError(Throwable t) { + * t.printStackTrace(); + * } + * @Override public void onComplete() { + * System.out.println("Done!"); + * } + * }); + * // ... + * d.dispose(); + * </code></pre> + * + * @param <T> the received value type + */ +public abstract class DisposableMaybeObserver<T> implements MaybeObserver<T>, Disposable { + + final AtomicReference<Disposable> upstream = new AtomicReference<>(); + + @Override + public final void onSubscribe(@NonNull Disposable d) { + if (EndConsumerHelper.setOnce(this.upstream, d, getClass())) { + onStart(); + } + } + + /** + * Called once the single upstream {@link Disposable} is set via {@link #onSubscribe(Disposable)}. + */ + protected void onStart() { + } + + @Override + public final boolean isDisposed() { + return upstream.get() == DisposableHelper.DISPOSED; + } + + @Override + public final void dispose() { + DisposableHelper.dispose(upstream); + } +} diff --git a/src/main/java/io/reactivex/rxjava3/observers/DisposableObserver.java b/src/main/java/io/reactivex/rxjava3/observers/DisposableObserver.java new file mode 100644 index 0000000000..d4e6d5f5d8 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/observers/DisposableObserver.java @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.observers; + +import java.util.concurrent.atomic.AtomicReference; + +import io.reactivex.rxjava3.annotations.NonNull; +import io.reactivex.rxjava3.core.Observer; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; +import io.reactivex.rxjava3.internal.util.EndConsumerHelper; + +/** + * An abstract {@link Observer} that allows asynchronous cancellation by implementing {@link Disposable}. + * + * <p>All pre-implemented final methods are thread-safe. + * + * <p>Use the public {@link #dispose()} method to dispose the sequence from within an + * {@code onNext} implementation. + * + * <p>Like all other consumers, {@code DisposableObserver} can be subscribed only once. + * Any subsequent attempt to subscribe it to a new source will yield an + * {@link IllegalStateException} with message {@code "It is not allowed to subscribe with a(n) <class name> multiple times."}. + * + * <p>Implementation of {@link #onStart()}, {@link #onNext(Object)}, {@link #onError(Throwable)} + * and {@link #onComplete()} are not allowed to throw any unchecked exceptions. + * If for some reason this can't be avoided, use {@link io.reactivex.rxjava3.core.Observable#safeSubscribe(io.reactivex.rxjava3.core.Observer)} + * instead of the standard {@code subscribe()} method. + * + * <p>Example<pre><code> + * Disposable d = + * Observable.range(1, 5) + * .subscribeWith(new DisposableObserver<Integer>() { + * @Override public void onStart() { + * System.out.println("Start!"); + * } + * @Override public void onNext(Integer t) { + * if (t == 3) { + * dispose(); + * } + * System.out.println(t); + * } + * @Override public void onError(Throwable t) { + * t.printStackTrace(); + * } + * @Override public void onComplete() { + * System.out.println("Done!"); + * } + * }); + * // ... + * d.dispose(); + * </code></pre> + * + * @param <T> the received value type + */ +public abstract class DisposableObserver<T> implements Observer<T>, Disposable { + + final AtomicReference<Disposable> upstream = new AtomicReference<>(); + + @Override + public final void onSubscribe(@NonNull Disposable d) { + if (EndConsumerHelper.setOnce(this.upstream, d, getClass())) { + onStart(); + } + } + + /** + * Called once the single upstream Disposable is set via onSubscribe. + */ + protected void onStart() { + } + + @Override + public final boolean isDisposed() { + return upstream.get() == DisposableHelper.DISPOSED; + } + + @Override + public final void dispose() { + DisposableHelper.dispose(upstream); + } +} diff --git a/src/main/java/io/reactivex/rxjava3/observers/DisposableSingleObserver.java b/src/main/java/io/reactivex/rxjava3/observers/DisposableSingleObserver.java new file mode 100644 index 0000000000..9126332b21 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/observers/DisposableSingleObserver.java @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.observers; + +import java.util.concurrent.atomic.AtomicReference; + +import io.reactivex.rxjava3.annotations.NonNull; +import io.reactivex.rxjava3.core.SingleObserver; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; +import io.reactivex.rxjava3.internal.util.EndConsumerHelper; + +/** + * An abstract {@link SingleObserver} that allows asynchronous cancellation by implementing {@link Disposable}. + * + * <p>All pre-implemented final methods are thread-safe. + * + * <p>Like all other consumers, {@code DisposableSingleObserver} can be subscribed only once. + * Any subsequent attempt to subscribe it to a new source will yield an + * {@link IllegalStateException} with message {@code "It is not allowed to subscribe with a(n) <class name> multiple times."}. + * + * <p>Implementation of {@link #onStart()}, {@link #onSuccess(Object)} and {@link #onError(Throwable)} + * are not allowed to throw any unchecked exceptions. + * + * <p>Example<pre><code> + * Disposable d = + * Single.just(1).delay(1, TimeUnit.SECONDS) + * .subscribeWith(new DisposableSingleObserver<Integer>() { + * @Override public void onStart() { + * System.out.println("Start!"); + * } + * @Override public void onSuccess(Integer t) { + * System.out.println(t); + * } + * @Override public void onError(Throwable t) { + * t.printStackTrace(); + * } + * }); + * // ... + * d.dispose(); + * </code></pre> + * + * @param <T> the received value type + */ +public abstract class DisposableSingleObserver<T> implements SingleObserver<T>, Disposable { + + final AtomicReference<Disposable> upstream = new AtomicReference<>(); + + @Override + public final void onSubscribe(@NonNull Disposable d) { + if (EndConsumerHelper.setOnce(this.upstream, d, getClass())) { + onStart(); + } + } + + /** + * Called once the single upstream {@link Disposable} is set via {@link #onSubscribe(Disposable)}. + */ + protected void onStart() { + } + + @Override + public final boolean isDisposed() { + return upstream.get() == DisposableHelper.DISPOSED; + } + + @Override + public final void dispose() { + DisposableHelper.dispose(upstream); + } +} diff --git a/src/main/java/io/reactivex/rxjava3/observers/LambdaConsumerIntrospection.java b/src/main/java/io/reactivex/rxjava3/observers/LambdaConsumerIntrospection.java new file mode 100644 index 0000000000..293847979e --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/observers/LambdaConsumerIntrospection.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.observers; + +/** + * An interface that indicates that the implementing type is composed of individual components and exposes information + * about their behavior. + * + * <p><em>NOTE:</em> This is considered a read-only public API and is not intended to be implemented externally. + * <p>History: 2.1.4 - experimental + * @since 2.2 + */ +public interface LambdaConsumerIntrospection { + + /** + * Returns {@code true} or {@code false} if a custom {@code onError} consumer has been provided. + * @return {@code true} if a custom {@code onError} consumer implementation was supplied. Returns {@code false} if the + * implementation is missing an error consumer and thus using a throwing default implementation. + */ + boolean hasCustomOnError(); + +} diff --git a/src/main/java/io/reactivex/rxjava3/observers/ResourceCompletableObserver.java b/src/main/java/io/reactivex/rxjava3/observers/ResourceCompletableObserver.java new file mode 100644 index 0000000000..339692d756 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/observers/ResourceCompletableObserver.java @@ -0,0 +1,132 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.observers; + +import java.util.Objects; +import java.util.concurrent.atomic.AtomicReference; + +import io.reactivex.rxjava3.annotations.NonNull; +import io.reactivex.rxjava3.core.CompletableObserver; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.disposables.*; +import io.reactivex.rxjava3.internal.util.EndConsumerHelper; + +/** + * An abstract {@link CompletableObserver} that allows asynchronous cancellation of its subscription and associated resources. + * + * <p>All pre-implemented final methods are thread-safe. + * + * <p>Override the protected {@link #onStart()} to perform initialization when this + * {@code ResourceCompletableObserver} is subscribed to a source. + * + * <p>Use the public {@link #dispose()} method to dispose the sequence externally and release + * all resources. + * + * <p>To release the associated resources, one has to call {@link #dispose()} + * in {@code onError()} and {@code onComplete()} explicitly. + * + * <p>Use {@link #add(Disposable)} to associate resources (as {@link io.reactivex.rxjava3.disposables.Disposable Disposable}s) + * with this {@code ResourceCompletableObserver} that will be cleaned up when {@link #dispose()} is called. + * Removing previously associated resources is not possible but one can create a + * {@link io.reactivex.rxjava3.disposables.CompositeDisposable CompositeDisposable}, associate it with this + * {@code ResourceCompletableObserver} and then add/remove resources to/from the {@code CompositeDisposable} + * freely. + * + * <p>Like all other consumers, {@code ResourceCompletableObserver} can be subscribed only once. + * Any subsequent attempt to subscribe it to a new source will yield an + * {@link IllegalStateException} with message {@code "It is not allowed to subscribe with a(n) <class name> multiple times."}. + * + * <p>Implementation of {@link #onStart()}, {@link #onError(Throwable)} + * and {@link #onComplete()} are not allowed to throw any unchecked exceptions. + * + * <p>Example<pre><code> + * Disposable d = + * Completable.complete().delay(1, TimeUnit.SECONDS) + * .subscribeWith(new ResourceCompletableObserver() { + * @Override public void onStart() { + * add(Schedulers.single() + * .scheduleDirect(() -> System.out.println("Time!"), + * 2, TimeUnit.SECONDS)); + * } + * @Override public void onError(Throwable t) { + * t.printStackTrace(); + * dispose(); + * } + * @Override public void onComplete() { + * System.out.println("Done!"); + * dispose(); + * } + * }); + * // ... + * d.dispose(); + * </code></pre> + */ +public abstract class ResourceCompletableObserver implements CompletableObserver, Disposable { + /** The active subscription. */ + private final AtomicReference<Disposable> upstream = new AtomicReference<>(); + + /** The resource composite, can never be null. */ + private final ListCompositeDisposable resources = new ListCompositeDisposable(); + + /** + * Adds a resource to this {@code ResourceCompletableObserver}. + * + * @param resource the resource to add + * + * @throws NullPointerException if resource is {@code null} + */ + public final void add(@NonNull Disposable resource) { + Objects.requireNonNull(resource, "resource is null"); + resources.add(resource); + } + + @Override + public final void onSubscribe(@NonNull Disposable d) { + if (EndConsumerHelper.setOnce(this.upstream, d, getClass())) { + onStart(); + } + } + + /** + * Called once the upstream sets a {@link Disposable} on this {@code ResourceCompletableObserver}. + * + * <p>You can perform initialization at this moment. The default + * implementation does nothing. + */ + protected void onStart() { + } + + /** + * Cancels the main disposable (if any) and disposes the resources associated with + * this {@code ResourceCompletableObserver} (if any). + * + * <p>This method can be called before the upstream calls {@link #onSubscribe(Disposable)} at which + * case the main {@link Disposable} will be immediately disposed. + */ + @Override + public final void dispose() { + if (DisposableHelper.dispose(upstream)) { + resources.dispose(); + } + } + + /** + * Returns true if this {@code ResourceCompletableObserver} has been disposed/cancelled. + * @return true if this {@code ResourceCompletableObserver} has been disposed/cancelled + */ + @Override + public final boolean isDisposed() { + return DisposableHelper.isDisposed(upstream.get()); + } +} diff --git a/src/main/java/io/reactivex/rxjava3/observers/ResourceMaybeObserver.java b/src/main/java/io/reactivex/rxjava3/observers/ResourceMaybeObserver.java new file mode 100644 index 0000000000..e9df1c3edc --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/observers/ResourceMaybeObserver.java @@ -0,0 +1,142 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.observers; + +import java.util.Objects; +import java.util.concurrent.atomic.AtomicReference; + +import io.reactivex.rxjava3.annotations.NonNull; +import io.reactivex.rxjava3.core.MaybeObserver; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.disposables.*; +import io.reactivex.rxjava3.internal.util.EndConsumerHelper; + +/** + * An abstract {@link MaybeObserver} that allows asynchronous cancellation of its subscription and associated resources. + * + * <p>All pre-implemented final methods are thread-safe. + * + * <p>Note that {@link #onSuccess(Object)}, {@link #onError(Throwable)} and {@link #onComplete()} are + * exclusive to each other, unlike a regular {@link io.reactivex.rxjava3.core.Observer Observer}, and + * {@code onComplete()} is never called after an {@code onSuccess()}. + * + * <p>Override the protected {@link #onStart()} to perform initialization when this + * {@code ResourceMaybeObserver} is subscribed to a source. + * + * <p>Use the public {@link #dispose()} method to dispose the sequence externally and release + * all resources. + * + * <p>To release the associated resources, one has to call {@link #dispose()} + * in {@code onSuccess()}, {@code onError()} and {@code onComplete()} explicitly. + * + * <p>Use {@link #add(Disposable)} to associate resources (as {@link io.reactivex.rxjava3.disposables.Disposable Disposable}s) + * with this {@code ResourceMaybeObserver} that will be cleaned up when {@link #dispose()} is called. + * Removing previously associated resources is not possible but one can create a + * {@link io.reactivex.rxjava3.disposables.CompositeDisposable CompositeDisposable}, associate it with this + * {@code ResourceMaybeObserver} and then add/remove resources to/from the {@code CompositeDisposable} + * freely. + * + * <p>Like all other consumers, {@code ResourceMaybeObserver} can be subscribed only once. + * Any subsequent attempt to subscribe it to a new source will yield an + * {@link IllegalStateException} with message {@code "It is not allowed to subscribe with a(n) <class name> multiple times."}. + * + * <p>Implementation of {@link #onStart()}, {@link #onSuccess(Object)}, {@link #onError(Throwable)} + * and {@link #onComplete()} are not allowed to throw any unchecked exceptions. + * + * <p>Example<pre><code> + * Disposable d = + * Maybe.just(1).delay(1, TimeUnit.SECONDS) + * .subscribeWith(new ResourceMaybeObserver<Integer>() { + * @Override public void onStart() { + * add(Schedulers.single() + * .scheduleDirect(() -> System.out.println("Time!"), + * 2, TimeUnit.SECONDS)); + * } + * @Override public void onSuccess(Integer t) { + * System.out.println(t); + * dispose(); + * } + * @Override public void onError(Throwable t) { + * t.printStackTrace(); + * dispose(); + * } + * @Override public void onComplete() { + * System.out.println("Done!"); + * dispose(); + * } + * }); + * // ... + * d.dispose(); + * </code></pre> + * + * @param <T> the value type + */ +public abstract class ResourceMaybeObserver<T> implements MaybeObserver<T>, Disposable { + /** The active subscription. */ + private final AtomicReference<Disposable> upstream = new AtomicReference<>(); + + /** The resource composite, can never be null. */ + private final ListCompositeDisposable resources = new ListCompositeDisposable(); + + /** + * Adds a resource to this {@code ResourceMaybeObserver}. + * + * @param resource the resource to add + * + * @throws NullPointerException if resource is {@code null} + */ + public final void add(@NonNull Disposable resource) { + Objects.requireNonNull(resource, "resource is null"); + resources.add(resource); + } + + @Override + public final void onSubscribe(@NonNull Disposable d) { + if (EndConsumerHelper.setOnce(this.upstream, d, getClass())) { + onStart(); + } + } + + /** + * Called once the upstream sets a {@link Disposable} on this {@code ResourceMaybeObserver}. + * + * <p>You can perform initialization at this moment. The default + * implementation does nothing. + */ + protected void onStart() { + } + + /** + * Cancels the main disposable (if any) and disposes the resources associated with + * this {@code ResourceMaybeObserver} (if any). + * + * <p>This method can be called before the upstream calls {@link #onSubscribe(Disposable)} at which + * case the main {@link Disposable} will be immediately disposed. + */ + @Override + public final void dispose() { + if (DisposableHelper.dispose(upstream)) { + resources.dispose(); + } + } + + /** + * Returns true if this {@code ResourceMaybeObserver} has been disposed/cancelled. + * @return true if this {@code ResourceMaybeObserver} has been disposed/cancelled + */ + @Override + public final boolean isDisposed() { + return DisposableHelper.isDisposed(upstream.get()); + } +} diff --git a/src/main/java/io/reactivex/rxjava3/observers/ResourceObserver.java b/src/main/java/io/reactivex/rxjava3/observers/ResourceObserver.java new file mode 100644 index 0000000000..0238dd5a17 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/observers/ResourceObserver.java @@ -0,0 +1,140 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.observers; + +import java.util.Objects; +import java.util.concurrent.atomic.AtomicReference; + +import io.reactivex.rxjava3.annotations.NonNull; +import io.reactivex.rxjava3.core.Observer; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.disposables.*; +import io.reactivex.rxjava3.internal.util.EndConsumerHelper; + +/** + * An abstract {@link Observer} that allows asynchronous cancellation of its subscription and associated resources. + * + * <p>All pre-implemented final methods are thread-safe. + * + * <p>To release the associated resources, one has to call {@link #dispose()} + * in {@code onError()} and {@code onComplete()} explicitly. + * + * <p>Use {@link #add(Disposable)} to associate resources (as {@link io.reactivex.rxjava3.disposables.Disposable Disposable}s) + * with this {@code ResourceObserver} that will be cleaned up when {@link #dispose()} is called. + * Removing previously associated resources is not possible but one can create a + * {@link io.reactivex.rxjava3.disposables.CompositeDisposable CompositeDisposable}, associate it with this + * {@code ResourceObserver} and then add/remove resources to/from the {@code CompositeDisposable} + * freely. + * + * <p>Use the {@link #dispose()} to dispose the sequence from within an + * {@code onNext} implementation. + * + * <p>Like all other consumers, {@code ResourceObserver} can be subscribed only once. + * Any subsequent attempt to subscribe it to a new source will yield an + * {@link IllegalStateException} with message {@code "It is not allowed to subscribe with a(n) <class name> multiple times."}. + * + * <p>Implementation of {@link #onStart()}, {@link #onNext(Object)}, {@link #onError(Throwable)} + * and {@link #onComplete()} are not allowed to throw any unchecked exceptions. + * If for some reason this can't be avoided, use {@link io.reactivex.rxjava3.core.Observable#safeSubscribe(io.reactivex.rxjava3.core.Observer)} + * instead of the standard {@code subscribe()} method. + * + * <p>Example<pre><code> + * Disposable d = + * Observable.range(1, 5) + * .subscribeWith(new ResourceObserver<Integer>() { + * @Override public void onStart() { + * add(Schedulers.single() + * .scheduleDirect(() -> System.out.println("Time!"), + * 2, TimeUnit.SECONDS)); + * request(1); + * } + * @Override public void onNext(Integer t) { + * if (t == 3) { + * dispose(); + * } + * System.out.println(t); + * } + * @Override public void onError(Throwable t) { + * t.printStackTrace(); + * dispose(); + * } + * @Override public void onComplete() { + * System.out.println("Done!"); + * dispose(); + * } + * }); + * // ... + * d.dispose(); + * </code></pre> + * + * @param <T> the value type + */ +public abstract class ResourceObserver<T> implements Observer<T>, Disposable { + /** The active subscription. */ + private final AtomicReference<Disposable> upstream = new AtomicReference<>(); + + /** The resource composite, can never be null. */ + private final ListCompositeDisposable resources = new ListCompositeDisposable(); + + /** + * Adds a resource to this {@code ResourceObserver}. + * + * @param resource the resource to add + * + * @throws NullPointerException if resource is {@code null} + */ + public final void add(@NonNull Disposable resource) { + Objects.requireNonNull(resource, "resource is null"); + resources.add(resource); + } + + @Override + public final void onSubscribe(Disposable d) { + if (EndConsumerHelper.setOnce(this.upstream, d, getClass())) { + onStart(); + } + } + + /** + * Called once the upstream sets a {@link Disposable} on this {@code ResourceObserver}. + * + * <p>You can perform initialization at this moment. The default + * implementation does nothing. + */ + protected void onStart() { + } + + /** + * Cancels the main disposable (if any) and disposes the resources associated with + * this {@code ResourceObserver} (if any). + * + * <p>This method can be called before the upstream calls {@link #onSubscribe(Disposable)} at which + * case the main {@link Disposable} will be immediately disposed. + */ + @Override + public final void dispose() { + if (DisposableHelper.dispose(upstream)) { + resources.dispose(); + } + } + + /** + * Returns true if this {@code ResourceObserver} has been disposed/cancelled. + * @return true if this {@code ResourceObserver} has been disposed/cancelled + */ + @Override + public final boolean isDisposed() { + return DisposableHelper.isDisposed(upstream.get()); + } +} diff --git a/src/main/java/io/reactivex/rxjava3/observers/ResourceSingleObserver.java b/src/main/java/io/reactivex/rxjava3/observers/ResourceSingleObserver.java new file mode 100644 index 0000000000..b1b9049524 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/observers/ResourceSingleObserver.java @@ -0,0 +1,135 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.observers; + +import java.util.Objects; +import java.util.concurrent.atomic.AtomicReference; + +import io.reactivex.rxjava3.annotations.NonNull; +import io.reactivex.rxjava3.core.SingleObserver; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.disposables.*; +import io.reactivex.rxjava3.internal.util.EndConsumerHelper; + +/** + * An abstract {@link SingleObserver} that allows asynchronous cancellation of its subscription + * and the associated resources. + * + * <p>All pre-implemented final methods are thread-safe. + * + * <p>Override the protected {@link #onStart()} to perform initialization when this + * {@code ResourceSingleObserver} is subscribed to a source. + * + * <p>Use the public {@link #dispose()} method to dispose the sequence externally and release + * all resources. + * + * <p>To release the associated resources, one has to call {@link #dispose()} + * in {@code onSuccess()} and {@code onError()} explicitly. + * + * <p>Use {@link #add(Disposable)} to associate resources (as {@link io.reactivex.rxjava3.disposables.Disposable Disposable}s) + * with this {@code ResourceSingleObserver} that will be cleaned up when {@link #dispose()} is called. + * Removing previously associated resources is not possible but one can create a + * {@link io.reactivex.rxjava3.disposables.CompositeDisposable CompositeDisposable}, associate it with this + * {@code ResourceSingleObserver} and then add/remove resources to/from the {@code CompositeDisposable} + * freely. + * + * <p>Like all other consumers, {@code ResourceSingleObserver} can be subscribed only once. + * Any subsequent attempt to subscribe it to a new source will yield an + * {@link IllegalStateException} with message {@code "It is not allowed to subscribe with a(n) <class name> multiple times."}. + * + * <p>Implementation of {@link #onStart()}, {@link #onSuccess(Object)} and {@link #onError(Throwable)} + * are not allowed to throw any unchecked exceptions. + * + * <p>Example<pre><code> + * Disposable d = + * Single.just(1).delay(1, TimeUnit.SECONDS) + * .subscribeWith(new ResourceSingleObserver<Integer>() { + * @Override public void onStart() { + * add(Schedulers.single() + * .scheduleDirect(() -> System.out.println("Time!"), + * 2, TimeUnit.SECONDS)); + * } + * @Override public void onSuccess(Integer t) { + * System.out.println(t); + * dispose(); + * } + * @Override public void onError(Throwable t) { + * t.printStackTrace(); + * dispose(); + * } + * }); + * // ... + * d.dispose(); + * </code></pre> + * + * @param <T> the value type + */ +public abstract class ResourceSingleObserver<T> implements SingleObserver<T>, Disposable { + /** The active subscription. */ + private final AtomicReference<Disposable> upstream = new AtomicReference<>(); + + /** The resource composite, can never be null. */ + private final ListCompositeDisposable resources = new ListCompositeDisposable(); + + /** + * Adds a resource to this {@code ResourceSingleObserver}. + * + * @param resource the resource to add + * + * @throws NullPointerException if resource is {@code null} + */ + public final void add(@NonNull Disposable resource) { + Objects.requireNonNull(resource, "resource is null"); + resources.add(resource); + } + + @Override + public final void onSubscribe(@NonNull Disposable d) { + if (EndConsumerHelper.setOnce(this.upstream, d, getClass())) { + onStart(); + } + } + + /** + * Called once the upstream sets a {@link Disposable} on this {@code ResourceSingleObserver}. + * + * <p>You can perform initialization at this moment. The default + * implementation does nothing. + */ + protected void onStart() { + } + + /** + * Cancels the main disposable (if any) and disposes the resources associated with + * this {@code ResourceSingleObserver} (if any). + * + * <p>This method can be called before the upstream calls {@link #onSubscribe(Disposable)} at which + * case the main {@link Disposable} will be immediately disposed. + */ + @Override + public final void dispose() { + if (DisposableHelper.dispose(upstream)) { + resources.dispose(); + } + } + + /** + * Returns true if this {@code ResourceSingleObserver} has been disposed/cancelled. + * @return true if this {@code ResourceSingleObserver} has been disposed/cancelled + */ + @Override + public final boolean isDisposed() { + return DisposableHelper.isDisposed(upstream.get()); + } +} diff --git a/src/main/java/io/reactivex/rxjava3/observers/SafeObserver.java b/src/main/java/io/reactivex/rxjava3/observers/SafeObserver.java new file mode 100644 index 0000000000..4a72e5b80e --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/observers/SafeObserver.java @@ -0,0 +1,222 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.observers; + +import io.reactivex.rxjava3.annotations.NonNull; +import io.reactivex.rxjava3.core.Observer; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.internal.disposables.*; +import io.reactivex.rxjava3.internal.util.ExceptionHelper; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +/** + * Wraps another {@link Observer} and ensures all {@code onXXX} methods conform the protocol + * (except the requirement for serialized access). + * + * @param <T> the value type + */ +public final class SafeObserver<T> implements Observer<T>, Disposable { + /** The actual Subscriber. */ + final Observer<? super T> downstream; + /** The subscription. */ + Disposable upstream; + /** Indicates a terminal state. */ + boolean done; + + /** + * Constructs a {@code SafeObserver} by wrapping the given actual {@link Observer}. + * @param downstream the actual {@code Observer} to wrap, not {@code null} (not validated) + */ + public SafeObserver(@NonNull Observer<? super T> downstream) { + this.downstream = downstream; + } + + @Override + public void onSubscribe(@NonNull Disposable d) { + if (DisposableHelper.validate(this.upstream, d)) { + this.upstream = d; + try { + downstream.onSubscribe(this); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + done = true; + // can't call onError because the actual's state may be corrupt at this point + try { + d.dispose(); + } catch (Throwable e1) { + Exceptions.throwIfFatal(e1); + RxJavaPlugins.onError(new CompositeException(e, e1)); + return; + } + RxJavaPlugins.onError(e); + } + } + } + + @Override + public void dispose() { + upstream.dispose(); + } + + @Override + public boolean isDisposed() { + return upstream.isDisposed(); + } + + @Override + public void onNext(@NonNull T t) { + if (done) { + return; + } + if (upstream == null) { + onNextNoSubscription(); + return; + } + + if (t == null) { + Throwable ex = ExceptionHelper.createNullPointerException("onNext called with a null value."); + try { + upstream.dispose(); + } catch (Throwable e1) { + Exceptions.throwIfFatal(e1); + onError(new CompositeException(ex, e1)); + return; + } + onError(ex); + return; + } + + try { + downstream.onNext(t); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + try { + upstream.dispose(); + } catch (Throwable e1) { + Exceptions.throwIfFatal(e1); + onError(new CompositeException(e, e1)); + return; + } + onError(e); + } + } + + void onNextNoSubscription() { + done = true; + + Throwable ex = new NullPointerException("Subscription not set!"); + + try { + downstream.onSubscribe(EmptyDisposable.INSTANCE); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + // can't call onError because the actual's state may be corrupt at this point + RxJavaPlugins.onError(new CompositeException(ex, e)); + return; + } + try { + downstream.onError(ex); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + // if onError failed, all that's left is to report the error to plugins + RxJavaPlugins.onError(new CompositeException(ex, e)); + } + } + + @Override + public void onError(@NonNull Throwable t) { + if (done) { + RxJavaPlugins.onError(t); + return; + } + done = true; + + if (upstream == null) { + Throwable npe = new NullPointerException("Subscription not set!"); + + try { + downstream.onSubscribe(EmptyDisposable.INSTANCE); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + // can't call onError because the actual's state may be corrupt at this point + RxJavaPlugins.onError(new CompositeException(t, npe, e)); + return; + } + try { + downstream.onError(new CompositeException(t, npe)); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + // if onError failed, all that's left is to report the error to plugins + RxJavaPlugins.onError(new CompositeException(t, npe, e)); + } + return; + } + + if (t == null) { + t = ExceptionHelper.createNullPointerException("onError called with a null Throwable."); + } + + try { + downstream.onError(t); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + + RxJavaPlugins.onError(new CompositeException(t, ex)); + } + } + + @Override + public void onComplete() { + if (done) { + return; + } + + done = true; + + if (upstream == null) { + onCompleteNoSubscription(); + return; + } + + try { + downstream.onComplete(); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + RxJavaPlugins.onError(e); + } + } + + void onCompleteNoSubscription() { + + Throwable ex = new NullPointerException("Subscription not set!"); + + try { + downstream.onSubscribe(EmptyDisposable.INSTANCE); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + // can't call onError because the actual's state may be corrupt at this point + RxJavaPlugins.onError(new CompositeException(ex, e)); + return; + } + try { + downstream.onError(ex); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + // if onError failed, all that's left is to report the error to plugins + RxJavaPlugins.onError(new CompositeException(ex, e)); + } + } + +} diff --git a/src/main/java/io/reactivex/rxjava3/observers/SerializedObserver.java b/src/main/java/io/reactivex/rxjava3/observers/SerializedObserver.java new file mode 100644 index 0000000000..062a3b6abf --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/observers/SerializedObserver.java @@ -0,0 +1,203 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.observers; + +import io.reactivex.rxjava3.annotations.NonNull; +import io.reactivex.rxjava3.core.Observer; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; +import io.reactivex.rxjava3.internal.util.*; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +/** + * Serializes access to the {@link Observer#onNext(Object)}, {@link Observer#onError(Throwable)} and + * {@link Observer#onComplete()} methods of another {@link Observer}. + * + * <p>Note that {@link #onSubscribe(Disposable)} is not serialized in respect of the other methods so + * make sure the {@code onSubscribe()} is called with a non-null {@link Disposable} + * before any of the other methods are called. + * + * <p>The implementation assumes that the actual {@code Observer}'s methods don't throw. + * + * @param <T> the value type + */ +public final class SerializedObserver<T> implements Observer<T>, Disposable { + final Observer<? super T> downstream; + final boolean delayError; + + static final int QUEUE_LINK_SIZE = 4; + + Disposable upstream; + + boolean emitting; + AppendOnlyLinkedArrayList<Object> queue; + + volatile boolean done; + + /** + * Construct a {@code SerializedObserver} by wrapping the given actual {@link Observer}. + * @param downstream the actual {@code Observer}, not {@code null} (not verified) + */ + public SerializedObserver(@NonNull Observer<? super T> downstream) { + this(downstream, false); + } + + /** + * Construct a SerializedObserver by wrapping the given actual {@link Observer} and + * optionally delaying the errors till all regular values have been emitted + * from the internal buffer. + * @param actual the actual {@code Observer}, not {@code null} (not verified) + * @param delayError if {@code true}, errors are emitted after regular values have been emitted + */ + public SerializedObserver(@NonNull Observer<? super T> actual, boolean delayError) { + this.downstream = actual; + this.delayError = delayError; + } + + @Override + public void onSubscribe(@NonNull Disposable d) { + if (DisposableHelper.validate(this.upstream, d)) { + this.upstream = d; + + downstream.onSubscribe(this); + } + } + + @Override + public void dispose() { + done = true; + upstream.dispose(); + } + + @Override + public boolean isDisposed() { + return upstream.isDisposed(); + } + + @Override + public void onNext(@NonNull T t) { + if (done) { + return; + } + if (t == null) { + upstream.dispose(); + onError(ExceptionHelper.createNullPointerException("onNext called with a null value.")); + return; + } + synchronized (this) { + if (done) { + return; + } + if (emitting) { + AppendOnlyLinkedArrayList<Object> q = queue; + if (q == null) { + q = new AppendOnlyLinkedArrayList<>(QUEUE_LINK_SIZE); + queue = q; + } + q.add(NotificationLite.next(t)); + return; + } + emitting = true; + } + + downstream.onNext(t); + + emitLoop(); + } + + @Override + public void onError(@NonNull Throwable t) { + if (done) { + RxJavaPlugins.onError(t); + return; + } + boolean reportError; + synchronized (this) { + if (done) { + reportError = true; + } else + if (emitting) { + done = true; + AppendOnlyLinkedArrayList<Object> q = queue; + if (q == null) { + q = new AppendOnlyLinkedArrayList<>(QUEUE_LINK_SIZE); + queue = q; + } + Object err = NotificationLite.error(t); + if (delayError) { + q.add(err); + } else { + q.setFirst(err); + } + return; + } else { + done = true; + emitting = true; + reportError = false; + } + } + + if (reportError) { + RxJavaPlugins.onError(t); + return; + } + + downstream.onError(t); + // no need to loop because this onError is the last event + } + + @Override + public void onComplete() { + if (done) { + return; + } + synchronized (this) { + if (done) { + return; + } + if (emitting) { + AppendOnlyLinkedArrayList<Object> q = queue; + if (q == null) { + q = new AppendOnlyLinkedArrayList<>(QUEUE_LINK_SIZE); + queue = q; + } + q.add(NotificationLite.complete()); + return; + } + done = true; + emitting = true; + } + + downstream.onComplete(); + // no need to loop because this onComplete is the last event + } + + void emitLoop() { + for (;;) { + AppendOnlyLinkedArrayList<Object> q; + synchronized (this) { + q = queue; + if (q == null) { + emitting = false; + return; + } + queue = null; + } + + if (q.accept(downstream)) { + return; + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/observers/TestObserver.java b/src/main/java/io/reactivex/rxjava3/observers/TestObserver.java new file mode 100644 index 0000000000..8142747ec1 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/observers/TestObserver.java @@ -0,0 +1,223 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.observers; + +import java.util.concurrent.atomic.AtomicReference; + +import io.reactivex.rxjava3.annotations.NonNull; +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; + +/** + * An {@link Observer}, {@link MaybeObserver}, {@link SingleObserver} and + * {@link CompletableObserver} composite that can record events from + * {@link Observable}s, {@link Maybe}s, {@link Single}s and {@link Completable}s + * and allows making assertions about them. + * + * <p>You can override the {@link #onSubscribe(Disposable)}, {@link #onNext(Object)}, {@link #onError(Throwable)}, + * {@link #onComplete()} and {@link #onSuccess(Object)} methods but not the others (this is by design). + * + * <p>The {@code TestObserver} implements {@link Disposable} for convenience where dispose calls cancel. + * + * @param <T> the value type + * @see io.reactivex.rxjava3.subscribers.TestSubscriber + */ +public class TestObserver<T> +extends BaseTestConsumer<T, TestObserver<T>> +implements Observer<T>, Disposable, MaybeObserver<T>, SingleObserver<T>, CompletableObserver { + /** The actual observer to forward events to. */ + private final Observer<? super T> downstream; + + /** Holds the current subscription if any. */ + private final AtomicReference<Disposable> upstream = new AtomicReference<>(); + + /** + * Constructs a non-forwarding {@code TestObserver}. + * @param <T> the value type received + * @return the new {@code TestObserver} instance + */ + @NonNull + public static <T> TestObserver<T> create() { + return new TestObserver<>(); + } + + /** + * Constructs a forwarding {@code TestObserver}. + * @param <T> the value type received + * @param delegate the actual {@link Observer} to forward events to + * @return the new {@code TestObserver} instance + */ + @NonNull + public static <T> TestObserver<T> create(@NonNull Observer<? super T> delegate) { + return new TestObserver<>(delegate); + } + + /** + * Constructs a non-forwarding TestObserver. + */ + public TestObserver() { + this(EmptyObserver.INSTANCE); + } + + /** + * Constructs a forwarding {@code TestObserver}. + * @param downstream the actual {@link Observer} to forward events to + */ + public TestObserver(@NonNull Observer<? super T> downstream) { + this.downstream = downstream; + } + + @Override + public void onSubscribe(@NonNull Disposable d) { + lastThread = Thread.currentThread(); + + if (d == null) { + errors.add(new NullPointerException("onSubscribe received a null Subscription")); + return; + } + if (!upstream.compareAndSet(null, d)) { + d.dispose(); + if (upstream.get() != DisposableHelper.DISPOSED) { + errors.add(new IllegalStateException("onSubscribe received multiple subscriptions: " + d)); + } + return; + } + + downstream.onSubscribe(d); + } + + @Override + public void onNext(@NonNull T t) { + if (!checkSubscriptionOnce) { + checkSubscriptionOnce = true; + if (upstream.get() == null) { + errors.add(new IllegalStateException("onSubscribe not called in proper order")); + } + } + + lastThread = Thread.currentThread(); + + values.add(t); + + if (t == null) { + errors.add(new NullPointerException("onNext received a null value")); + } + + downstream.onNext(t); + } + + @Override + public void onError(@NonNull Throwable t) { + if (!checkSubscriptionOnce) { + checkSubscriptionOnce = true; + if (upstream.get() == null) { + errors.add(new IllegalStateException("onSubscribe not called in proper order")); + } + } + + try { + lastThread = Thread.currentThread(); + if (t == null) { + errors.add(new NullPointerException("onError received a null Throwable")); + } else { + errors.add(t); + } + + downstream.onError(t); + } finally { + done.countDown(); + } + } + + @Override + public void onComplete() { + if (!checkSubscriptionOnce) { + checkSubscriptionOnce = true; + if (upstream.get() == null) { + errors.add(new IllegalStateException("onSubscribe not called in proper order")); + } + } + + try { + lastThread = Thread.currentThread(); + completions++; + + downstream.onComplete(); + } finally { + done.countDown(); + } + } + + @Override + public final void dispose() { + DisposableHelper.dispose(upstream); + } + + @Override + public final boolean isDisposed() { + return DisposableHelper.isDisposed(upstream.get()); + } + + // state retrieval methods + /** + * Returns true if this {@code TestObserver} received a subscription. + * @return true if this {@code TestObserver} received a subscription + */ + public final boolean hasSubscription() { + return upstream.get() != null; + } + + /** + * Assert that the {@link #onSubscribe(Disposable)} method was called exactly once. + * @return this + */ + @Override + @NonNull + protected final TestObserver<T> assertSubscribed() { + if (upstream.get() == null) { + throw fail("Not subscribed!"); + } + return this; + } + + @Override + public void onSuccess(@NonNull T value) { + onNext(value); + onComplete(); + } + + /** + * An observer that ignores all events and does not report errors. + */ + enum EmptyObserver implements Observer<Object> { + INSTANCE; + + @Override + public void onSubscribe(Disposable d) { + } + + @Override + public void onNext(Object t) { + } + + @Override + public void onError(Throwable t) { + } + + @Override + public void onComplete() { + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/observers/package-info.java b/src/main/java/io/reactivex/rxjava3/observers/package-info.java new file mode 100644 index 0000000000..09f56f3eb0 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/observers/package-info.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +/** + * Default wrappers and implementations for observer-based consumer classes and interfaces, + * including disposable and resource-tracking variants and + * the {@link io.reactivex.rxjava3.observers.TestObserver TestObserver} that allows unit testing + * {@link io.reactivex.rxjava3.core.Observable Observable}-, {@link io.reactivex.rxjava3.core.Single Single}-, + * {@link io.reactivex.rxjava3.core.Maybe Maybe}- and {@link io.reactivex.rxjava3.core.Completable Completable}-based flows. + * <p> + * Available observer variants + * <br> + * <table border="1" style="border-collapse: collapse;"> + * <caption>The available observer types.</caption> + * <tr><td><b>Reactive type</b></td><td><b>Base interface</b></td><td><b>Simple</b></td><td><b>Disposable</b></td><td><b>Resource</b></td></tr> + * <tr> + * <td>{@link io.reactivex.rxjava3.core.Observable Observable}</td> + * <td>{@link io.reactivex.rxjava3.core.Observer Observer}</td> + * <td>{@link io.reactivex.rxjava3.observers.DefaultObserver DefaultObserver}</td> + * <td>{@link io.reactivex.rxjava3.observers.DisposableObserver DisposableObserver}</td> + * <td>{@link io.reactivex.rxjava3.observers.ResourceObserver DisposableObserver}</td> + * </tr> + * <tr> + * <td>{@link io.reactivex.rxjava3.core.Maybe Maybe}</td> + * <td>{@link io.reactivex.rxjava3.core.MaybeObserver MaybeObserver}</td> + * <td>N/A</td> + * <td>{@link io.reactivex.rxjava3.observers.DisposableMaybeObserver DisposableMaybeObserver}</td> + * <td>{@link io.reactivex.rxjava3.observers.ResourceMaybeObserver DisposableMaybeObserver}</td> + * </tr> + * <tr> + * <td>{@link io.reactivex.rxjava3.core.Single Single}</td> + * <td>{@link io.reactivex.rxjava3.core.SingleObserver SingleObserver}</td> + * <td>N/A</td> + * <td>{@link io.reactivex.rxjava3.observers.DisposableSingleObserver DisposableSingleObserver}</td> + * <td>{@link io.reactivex.rxjava3.observers.ResourceSingleObserver DisposableSingleObserver}</td> + * </tr> + * <tr> + * <td>{@link io.reactivex.rxjava3.core.Completable Completable}</td> + * <td>{@link io.reactivex.rxjava3.core.CompletableObserver CompletableObserver}</td> + * <td>N/A</td> + * <td>{@link io.reactivex.rxjava3.observers.DisposableCompletableObserver DisposableCompletableObserver}</td> + * <td>{@link io.reactivex.rxjava3.observers.ResourceCompletableObserver DisposableCompletableObserver}</td> + * </tr> + * </table> + */ +package io.reactivex.rxjava3.observers; diff --git a/src/main/java/io/reactivex/rxjava3/operators/ConditionalSubscriber.java b/src/main/java/io/reactivex/rxjava3/operators/ConditionalSubscriber.java new file mode 100644 index 0000000000..0880d12544 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/operators/ConditionalSubscriber.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.operators; + +import io.reactivex.rxjava3.annotations.NonNull; +import io.reactivex.rxjava3.core.FlowableSubscriber; + +/** + * A {@link FlowableSubscriber} with an additional {@link #tryOnNext(Object)} method that + * tells the caller the specified value has been accepted or not. + * + * <p>This allows certain queue-drain or source-drain operators + * to avoid requesting 1 on behalf of a dropped value. + * + * @param <T> the value type + * @since 3.1.1 + */ +public interface ConditionalSubscriber<@NonNull T> extends FlowableSubscriber<T> { + /** + * Conditionally takes the value. + * @param t the value to deliver + * @return true if the value has been accepted, false if the value has been rejected + * and the next value can be sent immediately + */ + boolean tryOnNext(@NonNull T t); +} diff --git a/src/main/java/io/reactivex/rxjava3/operators/QueueDisposable.java b/src/main/java/io/reactivex/rxjava3/operators/QueueDisposable.java new file mode 100644 index 0000000000..97096064d9 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/operators/QueueDisposable.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.operators; + +import io.reactivex.rxjava3.annotations.NonNull; +import io.reactivex.rxjava3.disposables.Disposable; + +/** + * An interface extending {@link SimpleQueue} and {@link Disposable} and allows negotiating + * the fusion mode between subsequent operators of the {@link io.reactivex.rxjava3.core.Observable Observable} base reactive type. + * <p> + * The negotiation happens in subscription time when the upstream + * calls the {@code onSubscribe} with an instance of this interface. The + * downstream has then the obligation to call {@link #requestFusion(int)} + * with the appropriate mode before calling {@code request()}. + * <p> + * In <b>synchronous fusion</b>, all upstream values are either already available or is generated + * when {@link #poll()} is called synchronously. When the {@link #poll()} returns {@code null}, + * that is the indication if a terminated stream. In this mode, the upstream won't call the onXXX methods. + * <p> + * In <b>asynchronous fusion</b>, upstream values may become available to {@link #poll()} eventually. + * Upstream signals {@code onError()} and {@code onComplete()} as usual, however, + * {@code onNext} will be called with {@code null} instead of the actual value. + * Downstream should treat such onNext as indication that {@link #poll()} can be called. + * <p> + * The general rules for consuming the {@link SimpleQueue} interface: + * <ul> + * <li> {@link #poll()} and {@link #clear()} has to be called sequentially (from within a serializing drain-loop).</li> + * <li>In addition, callers of {@link #poll()} should be prepared to catch exceptions.</li> + * <li>Due to how computation attaches to the {@link #poll()}, {@link #poll()} may return + * {@code null} even if a preceding {@link #isEmpty()} returned false.</li> + * </ul> + * <p> + * Implementations should only allow calling the following methods and the rest of the + * {@link SimpleQueue} interface methods should throw {@link UnsupportedOperationException}: + * <ul> + * <li>{@link #poll()}</li> + * <li>{@link #isEmpty()}</li> + * <li>{@link #clear()}</li> + * </ul> + * @param <T> the value type transmitted through the queue + * @see QueueSubscription + * @since 3.1.1 + */ +public interface QueueDisposable<@NonNull T> extends QueueFuseable<T>, Disposable { +} diff --git a/src/main/java/io/reactivex/rxjava3/operators/QueueFuseable.java b/src/main/java/io/reactivex/rxjava3/operators/QueueFuseable.java new file mode 100644 index 0000000000..d295d12f0d --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/operators/QueueFuseable.java @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.operators; + +import io.reactivex.rxjava3.annotations.NonNull; + +/** + * Represents a {@link SimpleQueue} plus the means and constants for requesting a fusion mode. + * @param <T> the value type returned by the SimpleQueue.poll() + * @since 3.1.1 + */ +public interface QueueFuseable<@NonNull T> extends SimpleQueue<T> { + /** + * Returned by the {@link #requestFusion(int)} if the upstream doesn't support + * the requested mode. + */ + int NONE = 0; + + /** + * Request a synchronous fusion mode and can be returned by {@link #requestFusion(int)} + * for an accepted mode. + * <p> + * In synchronous fusion, all upstream values are either already available or is generated + * when {@link #poll()} is called synchronously. When the {@link #poll()} returns null, + * that is the indication if a terminated stream. + * In this mode, the upstream won't call the onXXX methods and callers of + * {@link #poll()} should be prepared to catch exceptions. Note that {@link #poll()} has + * to be called sequentially (from within a serializing drain-loop). + */ + int SYNC = 1; + + /** + * Request an asynchronous fusion mode and can be returned by {@link #requestFusion(int)} + * for an accepted mode. + * <p> + * In asynchronous fusion, upstream values may become available to {@link #poll()} eventually. + * Upstream signals onError() and onComplete() as usual but onNext may not actually contain + * the upstream value but have {@code null} instead. Downstream should treat such onNext as indication + * that {@link #poll()} can be called. Note that {@link #poll()} has to be called sequentially + * (from within a serializing drain-loop). In addition, callers of {@link #poll()} should be + * prepared to catch exceptions. + */ + int ASYNC = 2; + + /** + * Request any of the {@link #SYNC} or {@link #ASYNC} modes. + */ + int ANY = SYNC | ASYNC; + + /** + * Used in binary or combination with the other constants as an input to {@link #requestFusion(int)} + * indicating that the {@link #poll()} will be called behind an asynchronous boundary and thus + * may change the non-trivial computation locations attached to the {@link #poll()} chain of + * fused operators. + * <p> + * For example, fusing map() and observeOn() may move the computation of the map's function over to + * the thread run after the observeOn(), which is generally unexpected. + */ + int BOUNDARY = 4; + + /** + * Request a fusion mode from the upstream. + * <p> + * This should be called before {@code onSubscribe} returns. + * <p> + * Calling this method multiple times or after {@code onSubscribe} finished is not allowed + * and may result in undefined behavior. + * <p> + * @param mode the requested fusion mode, allowed values are {@link #SYNC}, {@link #ASYNC}, + * {@link #ANY} combined with {@link #BOUNDARY} (e.g., {@code requestFusion(SYNC | BOUNDARY)}). + * @return the established fusion mode: {@link #NONE}, {@link #SYNC}, {@link #ASYNC}. + */ + int requestFusion(int mode); + +} diff --git a/src/main/java/io/reactivex/rxjava3/operators/QueueSubscription.java b/src/main/java/io/reactivex/rxjava3/operators/QueueSubscription.java new file mode 100644 index 0000000000..eae8922992 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/operators/QueueSubscription.java @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.operators; + +import org.reactivestreams.Subscription; + +import io.reactivex.rxjava3.annotations.NonNull; + +/** + * An interface extending {@link SimpleQueue} and {@link Subscription} and allows negotiating + * the fusion mode between subsequent operators of the {@link io.reactivex.rxjava3.core.Flowable Flowable} base reactive type. + * <p> + * The negotiation happens in subscription time when the upstream + * calls the {@code onSubscribe} with an instance of this interface. The + * downstream has then the obligation to call {@link #requestFusion(int)} + * with the appropriate mode before calling {@code request()}. + * <p> + * In <b>synchronous fusion</b>, all upstream values are either already available or is generated + * when {@link #poll()} is called synchronously. When the {@link #poll()} returns null, + * that is the indication if a terminated stream. Downstream should not call {@link #request(long)} + * in this mode. In this mode, the upstream won't call the onXXX methods. + * <p> + * In <b>asynchronous fusion</b>, upstream values may become available to {@link #poll()} eventually. + * Upstream signals {@code onError()} and {@code onComplete()} as usual, however, + * {@code onNext} will be called with {@code null} instead of the actual value. + * Downstream should treat such onNext as indication that {@link #poll()} can be called. + * In this mode, the downstream still has to call {@link #request(long)} + * to indicate it is prepared to receive more values. + * <p> + * The general rules for consuming the {@link SimpleQueue} interface: + * <ul> + * <li> {@link #poll()} and {@link #clear()} has to be called sequentially (from within a serializing drain-loop).</li> + * <li>In addition, callers of {@link #poll()} should be prepared to catch exceptions.</li> + * <li>Due to how computation attaches to the {@link #poll()}, {@link #poll()} may return + * {@code null} even if a preceding {@link #isEmpty()} returned false.</li> + * </ul> + * <p> + * Implementations should only allow calling the following methods and the rest of the + * {@link SimpleQueue} interface methods should throw {@link UnsupportedOperationException}: + * <ul> + * <li>{@link #poll()}</li> + * <li>{@link #isEmpty()}</li> + * <li>{@link #clear()}</li> + * </ul> + * @param <T> the value type transmitted through the queue + * @see QueueDisposable + * @since 3.1.1 + */ +public interface QueueSubscription<@NonNull T> extends QueueFuseable<T>, Subscription { +} diff --git a/src/main/java/io/reactivex/rxjava3/operators/ScalarSupplier.java b/src/main/java/io/reactivex/rxjava3/operators/ScalarSupplier.java new file mode 100644 index 0000000000..9b79c5c102 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/operators/ScalarSupplier.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.operators; + +import io.reactivex.rxjava3.annotations.NonNull; +import io.reactivex.rxjava3.functions.Supplier; + +/** + * A marker interface indicating that a scalar, constant value + * is held by the implementing reactive type which can be + * safely extracted during assembly time can be used for + * optimization. + * <p> + * Implementors of {@link #get()} should not throw any exception. + * <p> + * Design note: the interface extends {@link Supplier} because if a scalar + * is safe to extract during assembly time, it is also safe to extract at + * subscription time or later. This allows optimizations to deal with such + * single-element sources uniformly. + * <p> + * @param <T> the scalar value type held by the implementing reactive type + * @since 3.1.1 + */ +@FunctionalInterface +public interface ScalarSupplier<@NonNull T> extends Supplier<T> { + + // overridden to remove the throws Throwable + @Override + T get(); +} diff --git a/src/main/java/io/reactivex/rxjava3/operators/SimplePlainQueue.java b/src/main/java/io/reactivex/rxjava3/operators/SimplePlainQueue.java new file mode 100644 index 0000000000..7e0cac21f3 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/operators/SimplePlainQueue.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.operators; + +import io.reactivex.rxjava3.annotations.*; + +/** + * Override of the {@link SimpleQueue} interface with no {@code throws Throwable} on {@code poll()}. + * + * @param <T> the value type to offer and poll, not null + * @since 3.1.1 + */ +public interface SimplePlainQueue<@NonNull T> extends SimpleQueue<T> { + + @Nullable + @Override + T poll(); +} diff --git a/src/main/java/io/reactivex/rxjava3/operators/SimpleQueue.java b/src/main/java/io/reactivex/rxjava3/operators/SimpleQueue.java new file mode 100644 index 0000000000..a10b7f9a43 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/operators/SimpleQueue.java @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.operators; + +import io.reactivex.rxjava3.annotations.*; + +/** + * A simplified interface for offering, polling and clearing a queue. + * <p> + * This interface does not define most of the {@link java.util.Collection} + * or {@link java.util.Queue} methods as the intended usage of {@code SimpleQueue} + * does not require support for iteration or introspection. + * + * @param <T> the value type to offer and poll, not null + * @since 3.1.1 + */ +public interface SimpleQueue<@NonNull T> { + + /** + * Atomically enqueue a single value. + * @param value the value to enqueue, not null + * @return true if successful, false if the value was not enqueued + * likely due to reaching the queue capacity) + */ + boolean offer(@NonNull T value); + + /** + * Atomically enqueue two values. + * @param v1 the first value to enqueue, not null + * @param v2 the second value to enqueue, not null + * @return true if successful, false if the value was not enqueued + * likely due to reaching the queue capacity) + */ + boolean offer(@NonNull T v1, @NonNull T v2); + + /** + * Tries to dequeue a value (non-null) or returns null if + * the queue is empty. + * <p> + * If the producer uses {@link #offer(Object, Object)} and + * when polling in pairs, if the first poll() returns a non-null + * item, the second poll() is guaranteed to return a non-null item + * as well. + * @return the item or null to indicate an empty queue + * @throws Throwable if some pre-processing of the dequeued + * item (usually through fused functions) throws. + */ + @Nullable + T poll() throws Throwable; + + /** + * Returns true if the queue is empty. + * <p> + * Note however that due to potential fused functions in {@link #poll()} + * it is possible this method returns false but then poll() returns null + * because the fused function swallowed the available item(s). + * @return true if the queue is empty + */ + boolean isEmpty(); + + /** + * Removes all enqueued items from this queue. + */ + void clear(); +} diff --git a/src/main/java/io/reactivex/rxjava3/operators/SpscArrayQueue.java b/src/main/java/io/reactivex/rxjava3/operators/SpscArrayQueue.java new file mode 100644 index 0000000000..1e4c2d4f11 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/operators/SpscArrayQueue.java @@ -0,0 +1,142 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +/* + * The code was inspired by the similarly named JCTools class: + * https://github.com/JCTools/JCTools/blob/master/jctools-core/src/main/java/org/jctools/queues/atomic + */ + +package io.reactivex.rxjava3.operators; + +import java.util.concurrent.atomic.*; + +import io.reactivex.rxjava3.annotations.Nullable; +import io.reactivex.rxjava3.internal.util.Pow2; + +/** + * A Single-Producer-Single-Consumer queue backed by a pre-allocated buffer. + * <p> + * This implementation is a mashup of the <a href="http://sourceforge.net/projects/mc-fastflow/">Fast Flow</a> + * algorithm with an optimization of the offer method taken from the <a + * href="http://staff.ustc.edu.cn/~bhua/publications/IJPP_draft.pdf">BQueue</a> algorithm (a variation on Fast + * Flow), and adjusted to comply with Queue.offer semantics with regards to capacity.<br> + * For convenience the relevant papers are available in the resources folder:<br> + * <i>2010 - Pisa - SPSC Queues on Shared Cache Multi-Core Systems.pdf<br> + * 2012 - Junchang- BQueue- Efficient and Practical Queuing.pdf <br> + * </i> This implementation is wait free. + * + * @param <E> the element type of the queue + * @since 3.1.1 + */ +public final class SpscArrayQueue<E> extends AtomicReferenceArray<E> implements SimplePlainQueue<E> { + private static final long serialVersionUID = -1296597691183856449L; + private static final Integer MAX_LOOK_AHEAD_STEP = Integer.getInteger("jctools.spsc.max.lookahead.step", 4096); + final int mask; + final AtomicLong producerIndex; + long producerLookAhead; + final AtomicLong consumerIndex; + final int lookAheadStep; + + /** + * Constructs an array-backed queue with the given capacity rounded + * up to the next power of 2 size. + * @param capacity the maximum number of elements the queue would hold, + * rounded up to the next power of 2 + */ + public SpscArrayQueue(int capacity) { + super(Pow2.roundToPowerOfTwo(capacity)); + this.mask = length() - 1; + this.producerIndex = new AtomicLong(); + this.consumerIndex = new AtomicLong(); + lookAheadStep = Math.min(capacity / 4, MAX_LOOK_AHEAD_STEP); + } + + @Override + public boolean offer(E e) { + if (null == e) { + throw new NullPointerException("Null is not a valid element"); + } + // local load of field to avoid repeated loads after volatile reads + final int mask = this.mask; + final long index = producerIndex.get(); + final int offset = calcElementOffset(index, mask); + if (index >= producerLookAhead) { + int step = lookAheadStep; + if (null == lvElement(calcElementOffset(index + step, mask))) { // LoadLoad + producerLookAhead = index + step; + } else if (null != lvElement(offset)) { + return false; + } + } + soElement(offset, e); // StoreStore + soProducerIndex(index + 1); // ordered store -> atomic and ordered for size() + return true; + } + + @Override + public boolean offer(E v1, E v2) { + // FIXME + return offer(v1) && offer(v2); + } + + @Nullable + @Override + public E poll() { + final long index = consumerIndex.get(); + final int offset = calcElementOffset(index); + // local load of field to avoid repeated loads after volatile reads + final E e = lvElement(offset); // LoadLoad + if (null == e) { + return null; + } + soConsumerIndex(index + 1); // ordered store -> atomic and ordered for size() + soElement(offset, null); // StoreStore + return e; + } + + @Override + public boolean isEmpty() { + return producerIndex.get() == consumerIndex.get(); + } + + void soProducerIndex(long newIndex) { + producerIndex.lazySet(newIndex); + } + + void soConsumerIndex(long newIndex) { + consumerIndex.lazySet(newIndex); + } + + @Override + public void clear() { + // we have to test isEmpty because of the weaker poll() guarantee + while (poll() != null || !isEmpty()) { } // NOPMD + } + + int calcElementOffset(long index, int mask) { + return (int)index & mask; + } + + int calcElementOffset(long index) { + return (int)index & mask; + } + + void soElement(int offset, E value) { + lazySet(offset, value); + } + + E lvElement(int offset) { + return get(offset); + } +} + diff --git a/src/main/java/io/reactivex/rxjava3/operators/SpscLinkedArrayQueue.java b/src/main/java/io/reactivex/rxjava3/operators/SpscLinkedArrayQueue.java new file mode 100644 index 0000000000..97fac2253a --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/operators/SpscLinkedArrayQueue.java @@ -0,0 +1,306 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +/* + * The code was inspired by the similarly named JCTools class: + * https://github.com/JCTools/JCTools/blob/master/jctools-core/src/main/java/org/jctools/queues/atomic + */ + +package io.reactivex.rxjava3.operators; + +import java.util.concurrent.atomic.*; + +import io.reactivex.rxjava3.annotations.Nullable; +import io.reactivex.rxjava3.internal.util.Pow2; + +/** + * A single-producer single-consumer array-backed queue which can allocate new arrays in case the consumer is slower + * than the producer. + * @param <T> the contained value type + * @since 3.1.1 + */ +public final class SpscLinkedArrayQueue<T> implements SimplePlainQueue<T> { + static final int MAX_LOOK_AHEAD_STEP = Integer.getInteger("jctools.spsc.max.lookahead.step", 4096); + final AtomicLong producerIndex = new AtomicLong(); + + int producerLookAheadStep; + long producerLookAhead; + + final int producerMask; + + AtomicReferenceArray<Object> producerBuffer; + final int consumerMask; + AtomicReferenceArray<Object> consumerBuffer; + final AtomicLong consumerIndex = new AtomicLong(); + + private static final Object HAS_NEXT = new Object(); + + /** + * Constructs a linked array-based queue instance with the given + * island size rounded up to the next power of 2. + * @param bufferSize the maximum number of elements per island + */ + public SpscLinkedArrayQueue(final int bufferSize) { + int p2capacity = Pow2.roundToPowerOfTwo(Math.max(8, bufferSize)); + int mask = p2capacity - 1; + AtomicReferenceArray<Object> buffer = new AtomicReferenceArray<>(p2capacity + 1); + producerBuffer = buffer; + producerMask = mask; + adjustLookAheadStep(p2capacity); + consumerBuffer = buffer; + consumerMask = mask; + producerLookAhead = mask - 1; // we know it's all empty to start with + soProducerIndex(0L); + } + + /** + * {@inheritDoc} + * <p> + * This implementation is correct for single producer thread use only. + */ + @Override + public boolean offer(final T e) { + if (null == e) { + throw new NullPointerException("Null is not a valid element"); + } + // local load of field to avoid repeated loads after volatile reads + final AtomicReferenceArray<Object> buffer = producerBuffer; + final long index = lpProducerIndex(); + final int mask = producerMask; + final int offset = calcWrappedOffset(index, mask); + if (index < producerLookAhead) { + return writeToQueue(buffer, e, index, offset); + } else { + final int lookAheadStep = producerLookAheadStep; + // go around the buffer or resize if full (unless we hit max capacity) + int lookAheadElementOffset = calcWrappedOffset(index + lookAheadStep, mask); + if (null == lvElement(buffer, lookAheadElementOffset)) { // LoadLoad + producerLookAhead = index + lookAheadStep - 1; // joy, there's plenty of room + return writeToQueue(buffer, e, index, offset); + } else if (null == lvElement(buffer, calcWrappedOffset(index + 1, mask))) { // buffer is not full + return writeToQueue(buffer, e, index, offset); + } else { + resize(buffer, index, offset, e, mask); // add a buffer and link old to new + return true; + } + } + } + + private boolean writeToQueue(final AtomicReferenceArray<Object> buffer, final T e, final long index, final int offset) { + soElement(buffer, offset, e); // StoreStore + soProducerIndex(index + 1); // this ensures atomic write of long on 32bit platforms + return true; + } + + private void resize(final AtomicReferenceArray<Object> oldBuffer, final long currIndex, final int offset, final T e, + final long mask) { + final int capacity = oldBuffer.length(); + final AtomicReferenceArray<Object> newBuffer = new AtomicReferenceArray<>(capacity); + producerBuffer = newBuffer; + producerLookAhead = currIndex + mask - 1; + soElement(newBuffer, offset, e); // StoreStore + soNext(oldBuffer, newBuffer); + soElement(oldBuffer, offset, HAS_NEXT); // new buffer is visible after element is + // inserted + soProducerIndex(currIndex + 1); // this ensures correctness on 32bit platforms + } + + private void soNext(AtomicReferenceArray<Object> curr, AtomicReferenceArray<Object> next) { + soElement(curr, calcDirectOffset(curr.length() - 1), next); + } + + @SuppressWarnings("unchecked") + private AtomicReferenceArray<Object> lvNextBufferAndUnlink(AtomicReferenceArray<Object> curr, int nextIndex) { + int nextOffset = calcDirectOffset(nextIndex); + AtomicReferenceArray<Object> nextBuffer = (AtomicReferenceArray<Object>)lvElement(curr, nextOffset); + soElement(curr, nextOffset, null); // Avoid GC nepotism + return nextBuffer; + } + /** + * {@inheritDoc} + * <p> + * This implementation is correct for single consumer thread use only. + */ + @Nullable + @SuppressWarnings("unchecked") + @Override + public T poll() { + // local load of field to avoid repeated loads after volatile reads + final AtomicReferenceArray<Object> buffer = consumerBuffer; + final long index = lpConsumerIndex(); + final int mask = consumerMask; + final int offset = calcWrappedOffset(index, mask); + final Object e = lvElement(buffer, offset); // LoadLoad + boolean isNextBuffer = e == HAS_NEXT; + if (null != e && !isNextBuffer) { + soElement(buffer, offset, null); // StoreStore + soConsumerIndex(index + 1); // this ensures correctness on 32bit platforms + return (T) e; + } else if (isNextBuffer) { + return newBufferPoll(lvNextBufferAndUnlink(buffer, mask + 1), index, mask); + } + + return null; + } + + @SuppressWarnings("unchecked") + private T newBufferPoll(AtomicReferenceArray<Object> nextBuffer, final long index, final int mask) { + consumerBuffer = nextBuffer; + final int offsetInNew = calcWrappedOffset(index, mask); + final T n = (T) lvElement(nextBuffer, offsetInNew); // LoadLoad + if (null != n) { + soElement(nextBuffer, offsetInNew, null); // StoreStore + soConsumerIndex(index + 1); // this ensures correctness on 32bit platforms + } + return n; + } + + /** + * Returns the next element in this queue without removing it or {@code null} + * if this queue is empty + * @return the next element or {@code null} + */ + @SuppressWarnings("unchecked") + @Nullable + public T peek() { + final AtomicReferenceArray<Object> buffer = consumerBuffer; + final long index = lpConsumerIndex(); + final int mask = consumerMask; + final int offset = calcWrappedOffset(index, mask); + final Object e = lvElement(buffer, offset); // LoadLoad + if (e == HAS_NEXT) { + return newBufferPeek(lvNextBufferAndUnlink(buffer, mask + 1), index, mask); + } + + return (T) e; + } + + @SuppressWarnings("unchecked") + private T newBufferPeek(AtomicReferenceArray<Object> nextBuffer, final long index, final int mask) { + consumerBuffer = nextBuffer; + final int offsetInNew = calcWrappedOffset(index, mask); + return (T) lvElement(nextBuffer, offsetInNew); // LoadLoad + } + + @Override + public void clear() { + while (poll() != null || !isEmpty()) { } // NOPMD + } + + /** + * Returns the number of elements in the queue. + * @return the number of elements in the queue + */ + public int size() { + /* + * It is possible for a thread to be interrupted or reschedule between the read of the producer and + * consumer indices, therefore protection is required to ensure size is within valid range. In the + * event of concurrent polls/offers to this method the size is OVER estimated as we read consumer + * index BEFORE the producer index. + */ + long after = lvConsumerIndex(); + while (true) { + final long before = after; + final long currentProducerIndex = lvProducerIndex(); + after = lvConsumerIndex(); + if (before == after) { + return (int) (currentProducerIndex - after); + } + } + } + + @Override + public boolean isEmpty() { + return lvProducerIndex() == lvConsumerIndex(); + } + + private void adjustLookAheadStep(int capacity) { + producerLookAheadStep = Math.min(capacity / 4, MAX_LOOK_AHEAD_STEP); + } + + private long lvProducerIndex() { + return producerIndex.get(); + } + + private long lvConsumerIndex() { + return consumerIndex.get(); + } + + private long lpProducerIndex() { + return producerIndex.get(); + } + + private long lpConsumerIndex() { + return consumerIndex.get(); + } + + private void soProducerIndex(long v) { + producerIndex.lazySet(v); + } + + private void soConsumerIndex(long v) { + consumerIndex.lazySet(v); + } + + private static int calcWrappedOffset(long index, int mask) { + return calcDirectOffset((int)index & mask); + } + private static int calcDirectOffset(int index) { + return index; + } + private static void soElement(AtomicReferenceArray<Object> buffer, int offset, Object e) { + buffer.lazySet(offset, e); + } + + private static Object lvElement(AtomicReferenceArray<Object> buffer, int offset) { + return buffer.get(offset); + } + + /** + * Offer two elements at the same time. + * <p>Don't use the regular offer() with this at all! + * @param first the first value, not null + * @param second the second value, not null + * @return true if the queue accepted the two new values + */ + @Override + public boolean offer(T first, T second) { + final AtomicReferenceArray<Object> buffer = producerBuffer; + final long p = lvProducerIndex(); + final int m = producerMask; + + int pi = calcWrappedOffset(p + 2, m); + + if (null == lvElement(buffer, pi)) { + pi = calcWrappedOffset(p, m); + soElement(buffer, pi + 1, second); + soElement(buffer, pi, first); + soProducerIndex(p + 2); + } else { + final int capacity = buffer.length(); + final AtomicReferenceArray<Object> newBuffer = new AtomicReferenceArray<>(capacity); + producerBuffer = newBuffer; + + pi = calcWrappedOffset(p, m); + soElement(newBuffer, pi + 1, second); // StoreStore + soElement(newBuffer, pi, first); + soNext(buffer, newBuffer); + + soElement(buffer, pi, HAS_NEXT); // new buffer is visible after element is + + soProducerIndex(p + 2); // this ensures correctness on 32bit platforms + } + + return true; + } +} diff --git a/src/main/java/io/reactivex/rxjava3/operators/package-info.java b/src/main/java/io/reactivex/rxjava3/operators/package-info.java new file mode 100644 index 0000000000..93e1ca05be --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/operators/package-info.java @@ -0,0 +1,18 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +/** + * Classes and interfaces for writing advanced operators within and outside RxJava. + */ + +package io.reactivex.rxjava3.operators; \ No newline at end of file diff --git a/src/main/java/io/reactivex/rxjava3/parallel/ParallelFailureHandling.java b/src/main/java/io/reactivex/rxjava3/parallel/ParallelFailureHandling.java new file mode 100644 index 0000000000..ce0496651e --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/parallel/ParallelFailureHandling.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.parallel; + +import io.reactivex.rxjava3.functions.BiFunction; + +/** + * Enumerations for handling failure within a parallel operator. + * <p>History: 2.0.8 - experimental + * @since 2.2 + */ +public enum ParallelFailureHandling implements BiFunction<Long, Throwable, ParallelFailureHandling> { + /** + * The current rail is stopped and the error is dropped. + */ + STOP, + /** + * The current rail is stopped and the error is signalled. + */ + ERROR, + /** + * The current value and error is ignored and the rail resumes with the next item. + */ + SKIP, + /** + * Retry the current value. + */ + RETRY; + + @Override + public ParallelFailureHandling apply(Long t1, Throwable t2) { + return this; + } +} diff --git a/src/main/java/io/reactivex/rxjava3/parallel/ParallelFlowable.java b/src/main/java/io/reactivex/rxjava3/parallel/ParallelFlowable.java new file mode 100644 index 0000000000..8d5414d27c --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/parallel/ParallelFlowable.java @@ -0,0 +1,1695 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.parallel; + +import java.util.*; +import java.util.stream.*; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.annotations.*; +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.MissingBackpressureException; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.functions.*; +import io.reactivex.rxjava3.internal.jdk8.*; +import io.reactivex.rxjava3.internal.operators.parallel.*; +import io.reactivex.rxjava3.internal.subscriptions.EmptySubscription; +import io.reactivex.rxjava3.internal.util.*; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +/** + * Abstract base class for parallel publishing of events signaled to an array of {@link Subscriber}s. + * <p> + * Use {@link #from(Publisher)} to start processing a regular {@link Publisher} in 'rails'. + * Use {@link #runOn(Scheduler)} to introduce where each 'rail' should run on thread-vise. + * Use {@link #sequential()} to merge the sources back into a single {@link Flowable}. + * + * <p>History: 2.0.5 - experimental; 2.1 - beta + * @param <T> the value type + * @since 2.2 + */ +public abstract class ParallelFlowable<@NonNull T> { + + /** + * Subscribes an array of {@link Subscriber}s to this {@code ParallelFlowable} and triggers + * the execution chain for all 'rails'. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The backpressure behavior/expectation is determined by the supplied {@code Subscriber}.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code subscribe} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param subscribers the subscribers array to run in parallel, the number + * of items must be equal to the parallelism level of this {@code ParallelFlowable} + * @throws NullPointerException if {@code subscribers} is {@code null} + * @see #parallelism() + */ + @BackpressureSupport(BackpressureKind.SPECIAL) + @SchedulerSupport(SchedulerSupport.NONE) + public abstract void subscribe(@NonNull Subscriber<? super T>[] subscribers); + + /** + * Returns the number of expected parallel {@link Subscriber}s. + * @return the number of expected parallel {@code Subscriber}s + */ + @CheckReturnValue + public abstract int parallelism(); + + /** + * Validates the number of subscribers and returns {@code true} if their number + * matches the parallelism level of this {@code ParallelFlowable}. + * + * @param subscribers the array of {@link Subscriber}s + * @return {@code true} if the number of subscribers equals to the parallelism level + * @throws NullPointerException if {@code subscribers} is {@code null} + * @throws IllegalArgumentException if {@code subscribers.length} is different from {@link #parallelism()} + */ + protected final boolean validate(@NonNull Subscriber<@NonNull ?>[] subscribers) { + Objects.requireNonNull(subscribers, "subscribers is null"); + int p = parallelism(); + if (subscribers.length != p) { + Throwable iae = new IllegalArgumentException("parallelism = " + p + ", subscribers = " + subscribers.length); + for (Subscriber<@NonNull ?> s : subscribers) { + EmptySubscription.error(iae, s); + } + return false; + } + return true; + } + + /** + * Take a {@link Publisher} and prepare to consume it on multiple 'rails' (number of CPUs) + * in a round-robin fashion. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors the backpressure of the parallel rails and + * requests {@link Flowable#bufferSize} amount from the upstream, followed + * by 75% of that amount requested after every 75% received.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code from} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <T> the value type + * @param source the source {@code Publisher} + * @return the new {@code ParallelFlowable} instance + * @throws NullPointerException if {@code source} is {@code null} + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + @BackpressureSupport(BackpressureKind.FULL) + public static <@NonNull T> ParallelFlowable<T> from(@NonNull Publisher<? extends T> source) { + return from(source, Runtime.getRuntime().availableProcessors(), Flowable.bufferSize()); + } + + /** + * Take a {@link Publisher} and prepare to consume it on parallelism number of 'rails' in a round-robin fashion. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors the backpressure of the parallel rails and + * requests {@link Flowable#bufferSize} amount from the upstream, followed + * by 75% of that amount requested after every 75% received.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code from} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <T> the value type + * @param source the source {@code Publisher} + * @param parallelism the number of parallel rails + * @return the new {@code ParallelFlowable} instance + * @throws NullPointerException if {@code source} is {@code null} + * @throws IllegalArgumentException if {@code parallelism} is non-positive + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + @BackpressureSupport(BackpressureKind.FULL) + public static <@NonNull T> ParallelFlowable<T> from(@NonNull Publisher<? extends T> source, int parallelism) { + return from(source, parallelism, Flowable.bufferSize()); + } + + /** + * Take a {@link Publisher} and prepare to consume it on parallelism number of 'rails' , + * possibly ordered and round-robin fashion and use custom prefetch amount and queue + * for dealing with the source {@code Publisher}'s values. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors the backpressure of the parallel rails and + * requests the {@code prefetch} amount from the upstream, followed + * by 75% of that amount requested after every 75% received.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code from} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <T> the value type + * @param source the source {@code Publisher} + * @param parallelism the number of parallel rails + * @param prefetch the number of values to prefetch from the source + * the source until there is a rail ready to process it. + * @return the new {@code ParallelFlowable} instance + * @throws NullPointerException if {@code source} is {@code null} + * @throws IllegalArgumentException if {@code parallelism} or {@code prefetch} is non-positive + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + @BackpressureSupport(BackpressureKind.FULL) + public static <@NonNull T> ParallelFlowable<T> from(@NonNull Publisher<? extends T> source, + int parallelism, int prefetch) { + Objects.requireNonNull(source, "source is null"); + ObjectHelper.verifyPositive(parallelism, "parallelism"); + ObjectHelper.verifyPositive(prefetch, "prefetch"); + + return RxJavaPlugins.onAssembly(new ParallelFromPublisher<>(source, parallelism, prefetch)); + } + + /** + * Maps the source values on each 'rail' to another value. + * <p> + * Note that the same {@code mapper} function may be called from multiple threads concurrently. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator is a pass-through for backpressure and the behavior + * is determined by the upstream and downstream rail behaviors.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code map} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <R> the output value type + * @param mapper the mapper function turning Ts into Rs. + * @return the new {@code ParallelFlowable} instance + * @throws NullPointerException if {@code mapper} is {@code null} + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + @BackpressureSupport(BackpressureKind.PASS_THROUGH) + public final <@NonNull R> ParallelFlowable<R> map(@NonNull Function<? super T, ? extends R> mapper) { + Objects.requireNonNull(mapper, "mapper is null"); + return RxJavaPlugins.onAssembly(new ParallelMap<>(this, mapper)); + } + + /** + * Maps the source values on each 'rail' to another value and + * handles errors based on the given {@link ParallelFailureHandling} enumeration value. + * <p> + * Note that the same {@code mapper} function may be called from multiple threads concurrently. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator is a pass-through for backpressure and the behavior + * is determined by the upstream and downstream rail behaviors.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code map} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * <p>History: 2.0.8 - experimental + * @param <R> the output value type + * @param mapper the mapper function turning Ts into Rs. + * @param errorHandler the enumeration that defines how to handle errors thrown + * from the {@code mapper} function + * @return the new {@code ParallelFlowable} instance + * @throws NullPointerException if {@code mapper} or {@code errorHandler} is {@code null} + * @since 2.2 + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + @BackpressureSupport(BackpressureKind.PASS_THROUGH) + public final <@NonNull R> ParallelFlowable<R> map(@NonNull Function<? super T, ? extends R> mapper, @NonNull ParallelFailureHandling errorHandler) { + Objects.requireNonNull(mapper, "mapper is null"); + Objects.requireNonNull(errorHandler, "errorHandler is null"); + return RxJavaPlugins.onAssembly(new ParallelMapTry<>(this, mapper, errorHandler)); + } + + /** + * Maps the source values on each 'rail' to another value and + * handles errors based on the returned value by the handler function. + * <p> + * Note that the same {@code mapper} function may be called from multiple threads concurrently. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator is a pass-through for backpressure and the behavior + * is determined by the upstream and downstream rail behaviors.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code map} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * <p>History: 2.0.8 - experimental + * @param <R> the output value type + * @param mapper the mapper function turning Ts into Rs. + * @param errorHandler the function called with the current repeat count and + * failure {@link Throwable} and should return one of the {@link ParallelFailureHandling} + * enumeration values to indicate how to proceed. + * @return the new {@code ParallelFlowable} instance + * @throws NullPointerException if {@code mapper} or {@code errorHandler} is {@code null} + * @since 2.2 + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + @BackpressureSupport(BackpressureKind.PASS_THROUGH) + public final <@NonNull R> ParallelFlowable<R> map(@NonNull Function<? super T, ? extends R> mapper, @NonNull BiFunction<? super Long, ? super Throwable, ParallelFailureHandling> errorHandler) { + Objects.requireNonNull(mapper, "mapper is null"); + Objects.requireNonNull(errorHandler, "errorHandler is null"); + return RxJavaPlugins.onAssembly(new ParallelMapTry<>(this, mapper, errorHandler)); + } + + /** + * Filters the source values on each 'rail'. + * <p> + * Note that the same predicate may be called from multiple threads concurrently. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator is a pass-through for backpressure and the behavior + * is determined by the upstream and downstream rail behaviors.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code filter} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param predicate the function returning {@code true} to keep a value or {@code false} to drop a value + * @return the new {@code ParallelFlowable} instance + * @throws NullPointerException if {@code predicate} is {@code null} + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + @BackpressureSupport(BackpressureKind.PASS_THROUGH) + public final ParallelFlowable<T> filter(@NonNull Predicate<? super T> predicate) { + Objects.requireNonNull(predicate, "predicate is null"); + return RxJavaPlugins.onAssembly(new ParallelFilter<>(this, predicate)); + } + + /** + * Filters the source values on each 'rail' and + * handles errors based on the given {@link ParallelFailureHandling} enumeration value. + * <p> + * Note that the same predicate may be called from multiple threads concurrently. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator is a pass-through for backpressure and the behavior + * is determined by the upstream and downstream rail behaviors.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code filter} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * <p>History: 2.0.8 - experimental + * @param predicate the function returning {@code true} to keep a value or {@code false} to drop a value + * @param errorHandler the enumeration that defines how to handle errors thrown + * from the {@code predicate} + * @return the new {@code ParallelFlowable} instance + * @throws NullPointerException if {@code predicate} or {@code errorHandler} is {@code null} + * @since 2.2 + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + @BackpressureSupport(BackpressureKind.PASS_THROUGH) + public final ParallelFlowable<T> filter(@NonNull Predicate<? super T> predicate, @NonNull ParallelFailureHandling errorHandler) { + Objects.requireNonNull(predicate, "predicate is null"); + Objects.requireNonNull(errorHandler, "errorHandler is null"); + return RxJavaPlugins.onAssembly(new ParallelFilterTry<>(this, predicate, errorHandler)); + } + + /** + * Filters the source values on each 'rail' and + * handles errors based on the returned value by the handler function. + * <p> + * Note that the same predicate may be called from multiple threads concurrently. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator is a pass-through for backpressure and the behavior + * is determined by the upstream and downstream rail behaviors.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code map} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * <p>History: 2.0.8 - experimental + * @param predicate the function returning {@code true} to keep a value or {@code false} to drop a value + * @param errorHandler the function called with the current repeat count and + * failure {@link Throwable} and should return one of the {@link ParallelFailureHandling} + * enumeration values to indicate how to proceed. + * @return the new {@code ParallelFlowable} instance + * @throws NullPointerException if {@code predicate} or {@code errorHandler} is {@code null} + * @since 2.2 + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + @BackpressureSupport(BackpressureKind.PASS_THROUGH) + public final ParallelFlowable<T> filter(@NonNull Predicate<? super T> predicate, @NonNull BiFunction<? super Long, ? super Throwable, ParallelFailureHandling> errorHandler) { + Objects.requireNonNull(predicate, "predicate is null"); + Objects.requireNonNull(errorHandler, "errorHandler is null"); + return RxJavaPlugins.onAssembly(new ParallelFilterTry<>(this, predicate, errorHandler)); + } + + /** + * Specifies where each 'rail' will observe its incoming values, specified via a {@link Scheduler}, with + * no work-stealing and default prefetch amount. + * <p> + * This operator uses the default prefetch size returned by {@link Flowable#bufferSize()}. + * <p> + * The operator will call {@link Scheduler#createWorker()} as many + * times as this {@code ParallelFlowable}'s parallelism level is. + * <p> + * No assumptions are made about the {@code Scheduler}'s parallelism level, + * if the {@code Scheduler}'s parallelism level is lower than the {@code ParallelFlowable}'s, + * some rails may end up on the same thread/worker. + * <p> + * This operator doesn't require the {@code Scheduler} to be trampolining as it + * does its own built-in trampolining logic. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors the backpressure of the parallel rails and + * requests {@link Flowable#bufferSize} amount from the upstream, followed + * by 75% of that amount requested after every 75% received.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code runOn} drains the upstream rails on the specified {@code Scheduler}'s + * {@link io.reactivex.rxjava3.core.Scheduler.Worker Worker}s.</dd> + * </dl> + * + * @param scheduler the scheduler to use + * @return the new {@code ParallelFlowable} instance + * @throws NullPointerException if {@code scheduler} is {@code null} + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.CUSTOM) + public final ParallelFlowable<T> runOn(@NonNull Scheduler scheduler) { + return runOn(scheduler, Flowable.bufferSize()); + } + + /** + * Specifies where each 'rail' will observe its incoming values, specified via a {@link Scheduler}, with + * possibly work-stealing and a given prefetch amount. + * <p> + * This operator uses the default prefetch size returned by {@link Flowable#bufferSize()}. + * <p> + * The operator will call {@link Scheduler#createWorker()} as many + * times as this {@code ParallelFlowable}'s parallelism level is. + * <p> + * No assumptions are made about the {@code Scheduler}'s parallelism level, + * if the {@code Scheduler}'s parallelism level is lower than the {@code ParallelFlowable}'s, + * some rails may end up on the same thread/worker. + * <p> + * This operator doesn't require the {@code Scheduler} to be trampolining as it + * does its own built-in trampolining logic. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors the backpressure of the parallel rails and + * requests the {@code prefetch} amount from the upstream, followed + * by 75% of that amount requested after every 75% received.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code runOn} drains the upstream rails on the specified {@code Scheduler}'s + * {@link io.reactivex.rxjava3.core.Scheduler.Worker Worker}s.</dd> + * </dl> + * + * @param scheduler the scheduler to use + * that rail's worker has run out of work. + * @param prefetch the number of values to request on each 'rail' from the source + * @return the new {@code ParallelFlowable} instance + * @throws NullPointerException if {@code scheduler} is {@code null} + * @throws IllegalArgumentException if {@code prefetch} is non-positive + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.CUSTOM) + public final ParallelFlowable<T> runOn(@NonNull Scheduler scheduler, int prefetch) { + Objects.requireNonNull(scheduler, "scheduler is null"); + ObjectHelper.verifyPositive(prefetch, "prefetch"); + return RxJavaPlugins.onAssembly(new ParallelRunOn<>(this, scheduler, prefetch)); + } + + /** + * Reduces all values within a 'rail' and across 'rails' with a reducer function into one + * {@link Flowable} sequence. + * <p> + * Note that the same reducer function may be called from multiple threads concurrently. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from the downstream and consumes + * the upstream rails in an unbounded manner (requesting {@link Long#MAX_VALUE}).</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code reduce} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param reducer the function to reduce two values into one. + * @return the new {@code Flowable} instance emitting the reduced value or empty if the current {@code ParallelFlowable} is empty + * @throws NullPointerException if {@code reducer} is {@code null} + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.UNBOUNDED_IN) + @SchedulerSupport(SchedulerSupport.NONE) + public final Flowable<T> reduce(@NonNull BiFunction<T, T, T> reducer) { + Objects.requireNonNull(reducer, "reducer is null"); + return RxJavaPlugins.onAssembly(new ParallelReduceFull<>(this, reducer)); + } + + /** + * Reduces all values within a 'rail' to a single value (with a possibly different type) via + * a reducer function that is initialized on each rail from an {@code initialSupplier} value. + * <p> + * Note that the same mapper function may be called from multiple threads concurrently. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from the downstream rails and consumes + * the upstream rails in an unbounded manner (requesting {@link Long#MAX_VALUE}).</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code reduce} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <R> the reduced output type + * @param initialSupplier the supplier for the initial value + * @param reducer the function to reduce a previous output of reduce (or the initial value supplied) + * with a current source value. + * @return the new {@code ParallelFlowable} instance + * @throws NullPointerException if {@code initialSupplier} or {@code reducer} is {@code null} + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.UNBOUNDED_IN) + @SchedulerSupport(SchedulerSupport.NONE) + public final <@NonNull R> ParallelFlowable<R> reduce(@NonNull Supplier<R> initialSupplier, @NonNull BiFunction<R, ? super T, R> reducer) { + Objects.requireNonNull(initialSupplier, "initialSupplier is null"); + Objects.requireNonNull(reducer, "reducer is null"); + return RxJavaPlugins.onAssembly(new ParallelReduce<>(this, initialSupplier, reducer)); + } + + /** + * Merges the values from each 'rail' in a round-robin or same-order fashion and + * exposes it as a regular {@link Flowable} sequence, running with a default prefetch value + * for the rails. + * <p> + * This operator uses the default prefetch size returned by {@code Flowable.bufferSize()}. + * <img width="640" height="602" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/parallelflowable.sequential.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from the downstream and + * requests {@link Flowable#bufferSize()} amount from each rail, then + * requests from each rail 75% of this amount after 75% received.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code sequential} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @return the new {@code Flowable} instance + * @see ParallelFlowable#sequential(int) + * @see ParallelFlowable#sequentialDelayError() + */ + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + @CheckReturnValue + @NonNull + public final Flowable<T> sequential() { + return sequential(Flowable.bufferSize()); + } + + /** + * Merges the values from each 'rail' in a round-robin or same-order fashion and + * exposes it as a regular {@link Flowable} sequence, running with a give prefetch value + * for the rails. + * <img width="640" height="602" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/parallelflowable.sequential.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from the downstream and + * requests the {@code prefetch} amount from each rail, then + * requests from each rail 75% of this amount after 75% received.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code sequential} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param prefetch the prefetch amount to use for each rail + * @return the new {@code Flowable} instance + * @throws IllegalArgumentException if {@code prefetch} is non-positive + * @see ParallelFlowable#sequential() + * @see ParallelFlowable#sequentialDelayError(int) + */ + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + @CheckReturnValue + @NonNull + public final Flowable<T> sequential(int prefetch) { + ObjectHelper.verifyPositive(prefetch, "prefetch"); + return RxJavaPlugins.onAssembly(new ParallelJoin<>(this, prefetch, false)); + } + + /** + * Merges the values from each 'rail' in a round-robin or same-order fashion and + * exposes it as a regular {@link Flowable} sequence, running with a default prefetch value + * for the rails and delaying errors from all rails till all terminate. + * <p> + * This operator uses the default prefetch size returned by {@code Flowable.bufferSize()}. + * <img width="640" height="602" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/parallelflowable.sequential.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from the downstream and + * requests {@link Flowable#bufferSize()} amount from each rail, then + * requests from each rail 75% of this amount after 75% received.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code sequentialDelayError} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * <p>History: 2.0.7 - experimental + * @return the new {@code Flowable} instance + * @see ParallelFlowable#sequentialDelayError(int) + * @see ParallelFlowable#sequential() + * @since 2.2 + */ + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + @CheckReturnValue + @NonNull + public final Flowable<T> sequentialDelayError() { + return sequentialDelayError(Flowable.bufferSize()); + } + + /** + * Merges the values from each 'rail' in a round-robin or same-order fashion and + * exposes it as a regular {@link Flowable} sequence, running with a give prefetch value + * for the rails and delaying errors from all rails till all terminate. + * <img width="640" height="602" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/parallelflowable.sequential.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from the downstream and + * requests the {@code prefetch} amount from each rail, then + * requests from each rail 75% of this amount after 75% received.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code sequentialDelayError} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * <p>History: 2.0.7 - experimental + * @param prefetch the prefetch amount to use for each rail + * @return the new {@code Flowable} instance + * @throws IllegalArgumentException if {@code prefetch} is non-positive + * @see ParallelFlowable#sequential() + * @see ParallelFlowable#sequentialDelayError() + * @since 2.2 + */ + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + @CheckReturnValue + @NonNull + public final Flowable<T> sequentialDelayError(int prefetch) { + ObjectHelper.verifyPositive(prefetch, "prefetch"); + return RxJavaPlugins.onAssembly(new ParallelJoin<>(this, prefetch, true)); + } + + /** + * Sorts the 'rails' of this {@code ParallelFlowable} and returns a {@link Flowable} that sequentially + * picks the smallest next value from the rails. + * <p> + * This operator requires a finite source {@code ParallelFlowable}. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from the downstream and + * consumes the upstream rails in an unbounded manner (requesting {@link Long#MAX_VALUE}).</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code sorted} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param comparator the comparator to use + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code comparator} is {@code null} + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.UNBOUNDED_IN) + @SchedulerSupport(SchedulerSupport.NONE) + public final Flowable<T> sorted(@NonNull Comparator<? super T> comparator) { + return sorted(comparator, 16); + } + + /** + * Sorts the 'rails' of this {@code ParallelFlowable} and returns a {@link Flowable} that sequentially + * picks the smallest next value from the rails. + * <p> + * This operator requires a finite source {@code ParallelFlowable}. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from the downstream and + * consumes the upstream rails in an unbounded manner (requesting {@link Long#MAX_VALUE}).</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code sorted} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param comparator the comparator to use + * @param capacityHint the expected number of total elements + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code comparator} is {@code null} + * @throws IllegalArgumentException if {@code capacityHint} is non-positive + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.UNBOUNDED_IN) + @SchedulerSupport(SchedulerSupport.NONE) + public final Flowable<T> sorted(@NonNull Comparator<? super T> comparator, int capacityHint) { + Objects.requireNonNull(comparator, "comparator is null"); + ObjectHelper.verifyPositive(capacityHint, "capacityHint"); + int ch = capacityHint / parallelism() + 1; + ParallelFlowable<List<T>> railReduced = reduce(Functions.createArrayList(ch), ListAddBiConsumer.instance()); + ParallelFlowable<List<T>> railSorted = railReduced.map(new SorterFunction<>(comparator)); + + return RxJavaPlugins.onAssembly(new ParallelSortedJoin<>(railSorted, comparator)); + } + + /** + * Sorts the 'rails' according to the comparator and returns a full sorted {@link List} as a {@link Flowable}. + * <p> + * This operator requires a finite source {@code ParallelFlowable}. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from the downstream and + * consumes the upstream rails in an unbounded manner (requesting {@link Long#MAX_VALUE}).</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code toSortedList} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param comparator the comparator to compare elements + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code comparator} is {@code null} + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.UNBOUNDED_IN) + @SchedulerSupport(SchedulerSupport.NONE) + public final Flowable<List<T>> toSortedList(@NonNull Comparator<? super T> comparator) { + return toSortedList(comparator, 16); + } + /** + * Sorts the 'rails' according to the comparator and returns a full sorted {@link List} as a {@link Flowable}. + * <p> + * This operator requires a finite source {@code ParallelFlowable}. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from the downstream and + * consumes the upstream rails in an unbounded manner (requesting {@link Long#MAX_VALUE}).</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code toSortedList} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param comparator the comparator to compare elements + * @param capacityHint the expected number of total elements + * @return the new {@code Flowable} instance + * @throws NullPointerException if {@code comparator} is {@code null} + * @throws IllegalArgumentException if {@code capacityHint} is non-positive + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.UNBOUNDED_IN) + @SchedulerSupport(SchedulerSupport.NONE) + public final Flowable<@NonNull List<T>> toSortedList(@NonNull Comparator<? super T> comparator, int capacityHint) { + Objects.requireNonNull(comparator, "comparator is null"); + ObjectHelper.verifyPositive(capacityHint, "capacityHint"); + + int ch = capacityHint / parallelism() + 1; + ParallelFlowable<List<T>> railReduced = reduce(Functions.createArrayList(ch), ListAddBiConsumer.instance()); + ParallelFlowable<List<T>> railSorted = railReduced.map(new SorterFunction<>(comparator)); + + Flowable<List<T>> merged = railSorted.reduce(new MergerBiFunction<>(comparator)); + + return RxJavaPlugins.onAssembly(merged); + } + + /** + * Call the specified consumer with the current element passing through any 'rail'. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator is a pass-through for backpressure and the behavior + * is determined by the upstream and downstream rail behaviors.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code map} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param onNext the callback + * @return the new {@code ParallelFlowable} instance + * @throws NullPointerException if {@code onNext} is {@code null} + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.PASS_THROUGH) + @SchedulerSupport(SchedulerSupport.NONE) + public final ParallelFlowable<T> doOnNext(@NonNull Consumer<? super T> onNext) { + Objects.requireNonNull(onNext, "onNext is null"); + return RxJavaPlugins.onAssembly(new ParallelPeek<>(this, + onNext, + Functions.emptyConsumer(), + Functions.emptyConsumer(), + Functions.EMPTY_ACTION, + Functions.EMPTY_ACTION, + Functions.emptyConsumer(), + Functions.EMPTY_LONG_CONSUMER, + Functions.EMPTY_ACTION + )); + } + + /** + * Call the specified consumer with the current element passing through any 'rail' and + * handles errors based on the given {@link ParallelFailureHandling} enumeration value. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator is a pass-through for backpressure and the behavior + * is determined by the upstream and downstream rail behaviors.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code map} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * <p>History: 2.0.8 - experimental + * @param onNext the callback + * @param errorHandler the enumeration that defines how to handle errors thrown + * from the {@code onNext} consumer + * @return the new {@code ParallelFlowable} instance + * @throws NullPointerException if {@code onNext} or {@code errorHandler} is {@code null} + * @since 2.2 + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.PASS_THROUGH) + @SchedulerSupport(SchedulerSupport.NONE) + public final ParallelFlowable<T> doOnNext(@NonNull Consumer<? super T> onNext, @NonNull ParallelFailureHandling errorHandler) { + Objects.requireNonNull(onNext, "onNext is null"); + Objects.requireNonNull(errorHandler, "errorHandler is null"); + return RxJavaPlugins.onAssembly(new ParallelDoOnNextTry<>(this, onNext, errorHandler)); + } + + /** + * Call the specified consumer with the current element passing through any 'rail' and + * handles errors based on the returned value by the handler function. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator is a pass-through for backpressure and the behavior + * is determined by the upstream and downstream rail behaviors.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code map} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * <p>History: 2.0.8 - experimental + * @param onNext the callback + * @param errorHandler the function called with the current repeat count and + * failure {@link Throwable} and should return one of the {@link ParallelFailureHandling} + * enumeration values to indicate how to proceed. + * @return the new {@code ParallelFlowable} instance + * @throws NullPointerException if {@code onNext} or {@code errorHandler} is {@code null} + * @since 2.2 + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.PASS_THROUGH) + @SchedulerSupport(SchedulerSupport.NONE) + public final ParallelFlowable<T> doOnNext(@NonNull Consumer<? super T> onNext, @NonNull BiFunction<? super Long, ? super Throwable, ParallelFailureHandling> errorHandler) { + Objects.requireNonNull(onNext, "onNext is null"); + Objects.requireNonNull(errorHandler, "errorHandler is null"); + return RxJavaPlugins.onAssembly(new ParallelDoOnNextTry<>(this, onNext, errorHandler)); + } + + /** + * Call the specified consumer with the current element passing through any 'rail' + * after it has been delivered to downstream within the rail. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator is a pass-through for backpressure and the behavior + * is determined by the upstream and downstream rail behaviors.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code map} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param onAfterNext the callback + * @return the new {@code ParallelFlowable} instance + * @throws NullPointerException if {@code onAfterNext} is {@code null} + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.PASS_THROUGH) + @SchedulerSupport(SchedulerSupport.NONE) + public final ParallelFlowable<T> doAfterNext(@NonNull Consumer<? super T> onAfterNext) { + Objects.requireNonNull(onAfterNext, "onAfterNext is null"); + return RxJavaPlugins.onAssembly(new ParallelPeek<>(this, + Functions.emptyConsumer(), + onAfterNext, + Functions.emptyConsumer(), + Functions.EMPTY_ACTION, + Functions.EMPTY_ACTION, + Functions.emptyConsumer(), + Functions.EMPTY_LONG_CONSUMER, + Functions.EMPTY_ACTION + )); + } + + /** + * Call the specified consumer with the exception passing through any 'rail'. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator is a pass-through for backpressure and the behavior + * is determined by the upstream and downstream rail behaviors.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code map} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param onError the callback + * @return the new {@code ParallelFlowable} instance + * @throws NullPointerException if {@code onError} is {@code null} + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.PASS_THROUGH) + @SchedulerSupport(SchedulerSupport.NONE) + public final ParallelFlowable<T> doOnError(@NonNull Consumer<? super Throwable> onError) { + Objects.requireNonNull(onError, "onError is null"); + return RxJavaPlugins.onAssembly(new ParallelPeek<>(this, + Functions.emptyConsumer(), + Functions.emptyConsumer(), + onError, + Functions.EMPTY_ACTION, + Functions.EMPTY_ACTION, + Functions.emptyConsumer(), + Functions.EMPTY_LONG_CONSUMER, + Functions.EMPTY_ACTION + )); + } + + /** + * Run the specified {@link Action} when a 'rail' completes. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator is a pass-through for backpressure and the behavior + * is determined by the upstream and downstream rail behaviors.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code map} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param onComplete the callback + * @return the new {@code ParallelFlowable} instance + * @throws NullPointerException if {@code onComplete} is {@code null} + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.PASS_THROUGH) + @SchedulerSupport(SchedulerSupport.NONE) + public final ParallelFlowable<T> doOnComplete(@NonNull Action onComplete) { + Objects.requireNonNull(onComplete, "onComplete is null"); + return RxJavaPlugins.onAssembly(new ParallelPeek<>(this, + Functions.emptyConsumer(), + Functions.emptyConsumer(), + Functions.emptyConsumer(), + onComplete, + Functions.EMPTY_ACTION, + Functions.emptyConsumer(), + Functions.EMPTY_LONG_CONSUMER, + Functions.EMPTY_ACTION + )); + } + + /** + * Run the specified {@link Action} when a 'rail' completes or signals an error. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator is a pass-through for backpressure and the behavior + * is determined by the upstream and downstream rail behaviors.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code map} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param onAfterTerminate the callback + * @return the new {@code ParallelFlowable} instance + * @throws NullPointerException if {@code onAfterTerminate} is {@code null} + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.PASS_THROUGH) + @SchedulerSupport(SchedulerSupport.NONE) + public final ParallelFlowable<T> doAfterTerminated(@NonNull Action onAfterTerminate) { + Objects.requireNonNull(onAfterTerminate, "onAfterTerminate is null"); + return RxJavaPlugins.onAssembly(new ParallelPeek<>(this, + Functions.emptyConsumer(), + Functions.emptyConsumer(), + Functions.emptyConsumer(), + Functions.EMPTY_ACTION, + onAfterTerminate, + Functions.emptyConsumer(), + Functions.EMPTY_LONG_CONSUMER, + Functions.EMPTY_ACTION + )); + } + + /** + * Call the specified callback when a 'rail' receives a {@link Subscription} from its upstream. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator is a pass-through for backpressure and the behavior + * is determined by the upstream and downstream rail behaviors.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code map} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param onSubscribe the callback + * @return the new {@code ParallelFlowable} instance + * @throws NullPointerException if {@code onSubscribe} is {@code null} + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.PASS_THROUGH) + @SchedulerSupport(SchedulerSupport.NONE) + public final ParallelFlowable<T> doOnSubscribe(@NonNull Consumer<? super Subscription> onSubscribe) { + Objects.requireNonNull(onSubscribe, "onSubscribe is null"); + return RxJavaPlugins.onAssembly(new ParallelPeek<>(this, + Functions.emptyConsumer(), + Functions.emptyConsumer(), + Functions.emptyConsumer(), + Functions.EMPTY_ACTION, + Functions.EMPTY_ACTION, + onSubscribe, + Functions.EMPTY_LONG_CONSUMER, + Functions.EMPTY_ACTION + )); + } + + /** + * Call the specified consumer with the request amount if any rail receives a request. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator is a pass-through for backpressure and the behavior + * is determined by the upstream and downstream rail behaviors.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code map} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param onRequest the callback + * @return the new {@code ParallelFlowable} instance + * @throws NullPointerException if {@code onRequest} is {@code null} + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.PASS_THROUGH) + @SchedulerSupport(SchedulerSupport.NONE) + public final ParallelFlowable<T> doOnRequest(@NonNull LongConsumer onRequest) { + Objects.requireNonNull(onRequest, "onRequest is null"); + return RxJavaPlugins.onAssembly(new ParallelPeek<>(this, + Functions.emptyConsumer(), + Functions.emptyConsumer(), + Functions.emptyConsumer(), + Functions.EMPTY_ACTION, + Functions.EMPTY_ACTION, + Functions.emptyConsumer(), + onRequest, + Functions.EMPTY_ACTION + )); + } + + /** + * Run the specified {@link Action} when a 'rail' receives a cancellation. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator is a pass-through for backpressure and the behavior + * is determined by the upstream and downstream rail behaviors.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code map} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param onCancel the callback + * @return the new {@code ParallelFlowable} instance + * @throws NullPointerException if {@code onCancel} is {@code null} + */ + @BackpressureSupport(BackpressureKind.PASS_THROUGH) + @SchedulerSupport(SchedulerSupport.NONE) + @CheckReturnValue + @NonNull + public final ParallelFlowable<T> doOnCancel(@NonNull Action onCancel) { + Objects.requireNonNull(onCancel, "onCancel is null"); + return RxJavaPlugins.onAssembly(new ParallelPeek<>(this, + Functions.emptyConsumer(), + Functions.emptyConsumer(), + Functions.emptyConsumer(), + Functions.EMPTY_ACTION, + Functions.EMPTY_ACTION, + Functions.emptyConsumer(), + Functions.EMPTY_LONG_CONSUMER, + onCancel + )); + } + + /** + * Collect the elements in each rail into a collection supplied via a {@code collectionSupplier} + * and collected into with a collector action, emitting the collection at the end. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from the downstream rails and + * consumes the upstream rails in an unbounded manner (requesting {@link Long#MAX_VALUE}).</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code map} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <C> the collection type + * @param collectionSupplier the supplier of the collection in each rail + * @param collector the collector, taking the per-rail collection and the current item + * @return the new {@code ParallelFlowable} instance + * @throws NullPointerException if {@code collectionSupplier} or {@code collector} is {@code null} + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.UNBOUNDED_IN) + @SchedulerSupport(SchedulerSupport.NONE) + public final <@NonNull C> ParallelFlowable<C> collect(@NonNull Supplier<? extends C> collectionSupplier, @NonNull BiConsumer<? super C, ? super T> collector) { + Objects.requireNonNull(collectionSupplier, "collectionSupplier is null"); + Objects.requireNonNull(collector, "collector is null"); + return RxJavaPlugins.onAssembly(new ParallelCollect<>(this, collectionSupplier, collector)); + } + + /** + * Wraps multiple {@link Publisher}s into a {@code ParallelFlowable} which runs them + * in parallel and unordered. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator is a pass-through for backpressure and the behavior + * is determined by the upstream and downstream rail behaviors.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code map} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <T> the value type + * @param publishers the array of publishers + * @return the new {@code ParallelFlowable} instance + * @throws NullPointerException if {@code publishers} is {@code null} + * @throws IllegalArgumentException if {@code publishers} is an empty array + */ + @CheckReturnValue + @NonNull + @SafeVarargs + @BackpressureSupport(BackpressureKind.PASS_THROUGH) + @SchedulerSupport(SchedulerSupport.NONE) + public static <@NonNull T> ParallelFlowable<T> fromArray(@NonNull Publisher<T>... publishers) { + Objects.requireNonNull(publishers, "publishers is null"); + if (publishers.length == 0) { + throw new IllegalArgumentException("Zero publishers not supported"); + } + return RxJavaPlugins.onAssembly(new ParallelFromArray<>(publishers)); + } + + /** + * Calls the specified converter function during assembly time and returns its resulting value. + * <p> + * This allows fluent conversion to any other type. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator is a pass-through for backpressure and the behavior + * is determined by how the converter function composes over the upstream source.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code to} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * <p>History: 2.1.7 - experimental + * @param <R> the resulting object type + * @param converter the function that receives the current {@code ParallelFlowable} instance and returns a value + * @return the converted value + * @throws NullPointerException if {@code converter} is {@code null} + * @since 2.2 + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.PASS_THROUGH) + @SchedulerSupport(SchedulerSupport.NONE) + public final <@NonNull R> R to(@NonNull ParallelFlowableConverter<T, R> converter) { + return Objects.requireNonNull(converter, "converter is null").apply(this); + } + + /** + * Allows composing operators, in assembly time, on top of this {@code ParallelFlowable} + * and returns another {@code ParallelFlowable} with composed features. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator is a pass-through for backpressure and the behavior + * is determined by how the converter function composes over the upstream source.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code compose} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <U> the output value type + * @param composer the composer function from {@code ParallelFlowable} (this) to another {@code ParallelFlowable} + * @return the {@code ParallelFlowable} returned by the function + * @throws NullPointerException if {@code composer} is {@code null} + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.PASS_THROUGH) + @SchedulerSupport(SchedulerSupport.NONE) + public final <@NonNull U> ParallelFlowable<U> compose(@NonNull ParallelTransformer<T, U> composer) { + return RxJavaPlugins.onAssembly(Objects.requireNonNull(composer, "composer is null").apply(this)); + } + + /** + * Generates and flattens {@link Publisher}s on each 'rail'. + * <p> + * The errors are not delayed and uses unbounded concurrency along with default inner prefetch. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from the downstream rails and + * requests {@link Flowable#bufferSize()} amount from each rail upfront + * and keeps requesting as many items per rail as many inner sources on + * that rail completed. The inner sources are requested {@link Flowable#bufferSize()} + * amount upfront, then 75% of this amount requested after 75% received.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code flatMap} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <R> the result type + * @param mapper the function to map each rail's value into a {@code Publisher} + * @return the new {@code ParallelFlowable} instance + * @throws NullPointerException if {@code mapper} is {@code null} + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + public final <@NonNull R> ParallelFlowable<R> flatMap(@NonNull Function<? super T, @NonNull ? extends Publisher<? extends R>> mapper) { + return flatMap(mapper, false, Flowable.bufferSize(), Flowable.bufferSize()); + } + + /** + * Generates and flattens {@link Publisher}s on each 'rail', optionally delaying errors. + * <p> + * It uses unbounded concurrency along with default inner prefetch. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from the downstream rails and + * requests {@link Flowable#bufferSize()} amount from each rail upfront + * and keeps requesting as many items per rail as many inner sources on + * that rail completed. The inner sources are requested {@link Flowable#bufferSize()} + * amount upfront, then 75% of this amount requested after 75% received. + * </dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code flatMap} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <R> the result type + * @param mapper the function to map each rail's value into a {@code Publisher} + * @param delayError should the errors from the main and the inner sources delayed till everybody terminates? + * @return the new {@code ParallelFlowable} instance + * @throws NullPointerException if {@code mapper} is {@code null} + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + public final <@NonNull R> ParallelFlowable<R> flatMap( + @NonNull Function<? super T, @NonNull ? extends Publisher<? extends R>> mapper, boolean delayError) { + return flatMap(mapper, delayError, Flowable.bufferSize(), Flowable.bufferSize()); + } + + /** + * Generates and flattens {@link Publisher}s on each 'rail', optionally delaying errors + * and having a total number of simultaneous subscriptions to the inner {@code Publisher}s. + * <p> + * It uses a default inner prefetch. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from the downstream rails and + * requests {@code maxConcurrency} amount from each rail upfront + * and keeps requesting as many items per rail as many inner sources on + * that rail completed. The inner sources are requested {@link Flowable#bufferSize()} + * amount upfront, then 75% of this amount requested after 75% received. + * </dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code flatMap} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <R> the result type + * @param mapper the function to map each rail's value into a {@code Publisher} + * @param delayError should the errors from the main and the inner sources delayed till everybody terminates? + * @param maxConcurrency the maximum number of simultaneous subscriptions to the generated inner {@code Publisher}s + * @return the new {@code ParallelFlowable} instance + * @throws NullPointerException if {@code mapper} is {@code null} + * @throws IllegalArgumentException if {@code maxConcurrency} is non-positive + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + public final <@NonNull R> ParallelFlowable<R> flatMap( + @NonNull Function<? super T, @NonNull ? extends Publisher<? extends R>> mapper, boolean delayError, int maxConcurrency) { + return flatMap(mapper, delayError, maxConcurrency, Flowable.bufferSize()); + } + + /** + * Generates and flattens {@link Publisher}s on each 'rail', optionally delaying errors, + * having a total number of simultaneous subscriptions to the inner {@code Publisher}s + * and using the given prefetch amount for the inner {@code Publisher}s. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from the downstream rails and + * requests {@code maxConcurrency} amount from each rail upfront + * and keeps requesting as many items per rail as many inner sources on + * that rail completed. The inner sources are requested the {@code prefetch} + * amount upfront, then 75% of this amount requested after 75% received. + * </dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code flatMap} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <R> the result type + * @param mapper the function to map each rail's value into a {@code Publisher} + * @param delayError should the errors from the main and the inner sources delayed till everybody terminates? + * @param maxConcurrency the maximum number of simultaneous subscriptions to the generated inner {@code Publisher}s + * @param prefetch the number of items to prefetch from each inner {@code Publisher} + * @return the new {@code ParallelFlowable} instance + * @throws NullPointerException if {@code mapper} is {@code null} + * @throws IllegalArgumentException if {@code maxConcurrency} or {@code prefetch} is non-positive + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + public final <@NonNull R> ParallelFlowable<R> flatMap( + @NonNull Function<? super T, @NonNull ? extends Publisher<? extends R>> mapper, + boolean delayError, int maxConcurrency, int prefetch) { + Objects.requireNonNull(mapper, "mapper is null"); + ObjectHelper.verifyPositive(maxConcurrency, "maxConcurrency"); + ObjectHelper.verifyPositive(prefetch, "prefetch"); + return RxJavaPlugins.onAssembly(new ParallelFlatMap<>(this, mapper, delayError, maxConcurrency, prefetch)); + } + + /** + * Generates and concatenates {@link Publisher}s on each 'rail', signalling errors immediately + * and generating 2 publishers upfront. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from the downstream rails and + * requests 2 from each rail upfront and keeps requesting 1 when the inner source complete. + * Requests for the inner sources are determined by the downstream rails' + * backpressure behavior.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code concatMap} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <R> the result type + * @param mapper the function to map each rail's value into a {@code Publisher} + * source and the inner {@code Publisher}s (immediate, boundary, end) + * @return the new {@code ParallelFlowable} instance + * @throws NullPointerException if {@code mapper} is {@code null} + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + public final <@NonNull R> ParallelFlowable<R> concatMap( + @NonNull Function<? super T, @NonNull ? extends Publisher<? extends R>> mapper) { + return concatMap(mapper, 2); + } + + /** + * Generates and concatenates {@link Publisher}s on each 'rail', signalling errors immediately + * and using the given prefetch amount for generating {@code Publisher}s upfront. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from the downstream rails and + * requests the {@code prefetch} amount from each rail upfront and keeps + * requesting 75% of this amount after 75% received and the inner sources completed. + * Requests for the inner sources are determined by the downstream rails' + * backpressure behavior.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code concatMap} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <R> the result type + * @param mapper the function to map each rail's value into a {@code Publisher} + * @param prefetch the number of items to prefetch from each inner {@code Publisher} + * source and the inner {@code Publisher}s (immediate, boundary, end) + * @return the new {@code ParallelFlowable} instance + * @throws NullPointerException if {@code mapper} is {@code null} + * @throws IllegalArgumentException if {@code prefetch} is non-positive + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + public final <@NonNull R> ParallelFlowable<R> concatMap( + @NonNull Function<? super T, @NonNull ? extends Publisher<? extends R>> mapper, + int prefetch) { + Objects.requireNonNull(mapper, "mapper is null"); + ObjectHelper.verifyPositive(prefetch, "prefetch"); + return RxJavaPlugins.onAssembly(new ParallelConcatMap<>(this, mapper, prefetch, ErrorMode.IMMEDIATE)); + } + + /** + * Generates and concatenates {@link Publisher}s on each 'rail', optionally delaying errors + * and generating 2 publishers upfront. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from the downstream rails and + * requests 2 from each rail upfront and keeps requesting 1 when the inner source complete. + * Requests for the inner sources are determined by the downstream rails' + * backpressure behavior.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code concatMap} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <R> the result type + * @param mapper the function to map each rail's value into a {@code Publisher} + * @param tillTheEnd if {@code true}, all errors from the upstream and inner {@code Publisher}s are delayed + * till all of them terminate, if {@code false}, the error is emitted when an inner {@code Publisher} terminates. + * source and the inner {@code Publisher}s (immediate, boundary, end) + * @return the new {@code ParallelFlowable} instance + * @throws NullPointerException if {@code mapper} is {@code null} + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + public final <@NonNull R> ParallelFlowable<R> concatMapDelayError( + @NonNull Function<? super T, @NonNull ? extends Publisher<? extends R>> mapper, + boolean tillTheEnd) { + return concatMapDelayError(mapper, 2, tillTheEnd); + } + + /** + * Generates and concatenates {@link Publisher}s on each 'rail', optionally delaying errors + * and using the given prefetch amount for generating {@code Publisher}s upfront. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from the downstream rails and + * requests the {@code prefetch} amount from each rail upfront and keeps + * requesting 75% of this amount after 75% received and the inner sources completed. + * Requests for the inner sources are determined by the downstream rails' + * backpressure behavior.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code concatMap} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <R> the result type + * @param mapper the function to map each rail's value into a {@code Publisher} + * @param prefetch the number of items to prefetch from each inner {@code Publisher} + * @param tillTheEnd if {@code true}, all errors from the upstream and inner {@code Publisher}s are delayed + * till all of them terminate, if {@code false}, the error is emitted when an inner {@code Publisher} terminates. + * @return the new {@code ParallelFlowable} instance + * @throws NullPointerException if {@code mapper} is {@code null} + * @throws IllegalArgumentException if {@code prefetch} is non-positive + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + public final <@NonNull R> ParallelFlowable<R> concatMapDelayError( + @NonNull Function<? super T, @NonNull ? extends Publisher<? extends R>> mapper, + int prefetch, boolean tillTheEnd) { + Objects.requireNonNull(mapper, "mapper is null"); + ObjectHelper.verifyPositive(prefetch, "prefetch"); + return RxJavaPlugins.onAssembly(new ParallelConcatMap<>( + this, mapper, prefetch, tillTheEnd ? ErrorMode.END : ErrorMode.BOUNDARY)); + } + + /** + * Returns a {@code ParallelFlowable} that merges each item emitted by the source on each rail with the values in an + * {@link Iterable} corresponding to that item that is generated by a selector. + * <p> + * <img width="640" height="342" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/flatMapIterable.f.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from each downstream rail. The source {@code ParallelFlowable}s is + * expected to honor backpressure as well. If the source {@code ParallelFlowable} violates the rule, the operator will + * signal a {@link MissingBackpressureException}.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code flatMapIterable} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <U> + * the type of item emitted by the resulting {@code Iterable} + * @param mapper + * a function that returns an {@code Iterable} sequence of values for when given an item emitted by the + * source {@code ParallelFlowable} + * @return the new {@code ParallelFlowable} instance + * @see <a href="http://reactivex.io/documentation/operators/flatmap.html">ReactiveX operators documentation: FlatMap</a> + * @see #flatMapStream(Function) + * @since 3.0.0 + * @throws NullPointerException if {@code mapper} is {@code null} + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull U> ParallelFlowable<U> flatMapIterable(@NonNull Function<? super T, @NonNull ? extends Iterable<? extends U>> mapper) { + return flatMapIterable(mapper, Flowable.bufferSize()); + } + + /** + * Returns a {@code ParallelFlowable} that merges each item emitted by the source {@code ParallelFlowable} with the values in an + * {@link Iterable} corresponding to that item that is generated by a selector. + * <p> + * <img width="640" height="342" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/flatMapIterable.f.png" alt=""> + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from each downstream rail. The source {@code ParallelFlowable}s is + * expected to honor backpressure as well. If the source {@code ParallelFlowable} violates the rule, the operator will + * signal a {@link MissingBackpressureException}.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code flatMapIterable} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <U> + * the type of item emitted by the resulting {@code Iterable} + * @param mapper + * a function that returns an {@code Iterable} sequence of values for when given an item emitted by the + * source {@code ParallelFlowable} + * @param bufferSize + * the number of elements to prefetch from each upstream rail + * @return the new {@code ParallelFlowable} instance + * @see <a href="http://reactivex.io/documentation/operators/flatmap.html">ReactiveX operators documentation: FlatMap</a> + * @see #flatMapStream(Function, int) + * @since 3.0.0 + * @throws NullPointerException if {@code mapper} is {@code null} + * @throws IllegalArgumentException if {@code bufferSize} is non-positive + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + public final <@NonNull U> ParallelFlowable<U> flatMapIterable(@NonNull Function<? super T, @NonNull ? extends Iterable<? extends U>> mapper, int bufferSize) { + Objects.requireNonNull(mapper, "mapper is null"); + ObjectHelper.verifyPositive(bufferSize, "bufferSize"); + return RxJavaPlugins.onAssembly(new ParallelFlatMapIterable<>(this, mapper, bufferSize)); + } + + // ------------------------------------------------------------------------- + // JDK 8 Support + // ------------------------------------------------------------------------- + + /** + * Maps the source values on each 'rail' to an optional and emits its value if any. + * <p> + * Note that the same mapper function may be called from multiple threads concurrently. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator is a pass-through for backpressure and the behavior + * is determined by the upstream and downstream rail behaviors.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code map} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <R> the output value type + * @param mapper the mapper function turning Ts into optional of Rs. + * @return the new {@code ParallelFlowable} instance + * @since 3.0.0 + * @throws NullPointerException if {@code mapper} is {@code null} + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + @BackpressureSupport(BackpressureKind.PASS_THROUGH) + public final <@NonNull R> ParallelFlowable<R> mapOptional(@NonNull Function<? super T, @NonNull Optional<? extends R>> mapper) { + Objects.requireNonNull(mapper, "mapper is null"); + return RxJavaPlugins.onAssembly(new ParallelMapOptional<>(this, mapper)); + } + + /** + * Maps the source values on each 'rail' to an optional and emits its value if any and + * handles errors based on the given {@link ParallelFailureHandling} enumeration value. + * <p> + * Note that the same mapper function may be called from multiple threads concurrently. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator is a pass-through for backpressure and the behavior + * is determined by the upstream and downstream rail behaviors.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code map} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * <p>History: 2.0.8 - experimental + * @param <R> the output value type + * @param mapper the mapper function turning Ts into optional of Rs. + * @param errorHandler the enumeration that defines how to handle errors thrown + * from the mapper function + * @return the new {@code ParallelFlowable} instance + * @since 3.0.0 + * @throws NullPointerException if {@code mapper} or {@code errorHandler} is {@code null} + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + @BackpressureSupport(BackpressureKind.PASS_THROUGH) + public final <@NonNull R> ParallelFlowable<R> mapOptional(@NonNull Function<? super T, @NonNull Optional<? extends R>> mapper, @NonNull ParallelFailureHandling errorHandler) { + Objects.requireNonNull(mapper, "mapper is null"); + Objects.requireNonNull(errorHandler, "errorHandler is null"); + return RxJavaPlugins.onAssembly(new ParallelMapTryOptional<>(this, mapper, errorHandler)); + } + + /** + * Maps the source values on each 'rail' to an optional and emits its value if any and + * handles errors based on the returned value by the handler function. + * <p> + * Note that the same mapper function may be called from multiple threads concurrently. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator is a pass-through for backpressure and the behavior + * is determined by the upstream and downstream rail behaviors.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code map} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * <p>History: 2.0.8 - experimental + * @param <R> the output value type + * @param mapper the mapper function turning Ts into optional of Rs. + * @param errorHandler the function called with the current repeat count and + * failure {@link Throwable} and should return one of the {@link ParallelFailureHandling} + * enumeration values to indicate how to proceed. + * @return the new {@code ParallelFlowable} instance + * @throws NullPointerException if {@code mapper} or {@code errorHandler} is {@code null} + * @since 3.0.0 + */ + @CheckReturnValue + @NonNull + @SchedulerSupport(SchedulerSupport.NONE) + @BackpressureSupport(BackpressureKind.PASS_THROUGH) + public final <@NonNull R> ParallelFlowable<R> mapOptional(@NonNull Function<? super T, @NonNull Optional<? extends R>> mapper, @NonNull BiFunction<? super Long, ? super Throwable, ParallelFailureHandling> errorHandler) { + Objects.requireNonNull(mapper, "mapper is null"); + Objects.requireNonNull(errorHandler, "errorHandler is null"); + return RxJavaPlugins.onAssembly(new ParallelMapTryOptional<>(this, mapper, errorHandler)); + } + + /** + * Maps each upstream item on each rail into a {@link Stream} and emits the {@code Stream}'s items to the downstream in a sequential fashion. + * <p> + * <img width="640" height="328" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/flatMapStream.f.png" alt=""> + * <p> + * Due to the blocking and sequential nature of Java {@code Stream}s, the streams are mapped and consumed in a sequential fashion + * without interleaving (unlike a more general {@link #flatMap(Function)}). Therefore, {@code flatMapStream} and + * {@code concatMapStream} are identical operators and are provided as aliases. + * <p> + * The operator closes the {@code Stream} upon cancellation and when it terminates. The exceptions raised when + * closing a {@code Stream} are routed to the global error handler ({@link RxJavaPlugins#onError(Throwable)}. + * If a {@code Stream} should not be closed, turn it into an {@link Iterable} and use {@link #flatMapIterable(Function)}: + * <pre><code> + * source.flatMapIterable(v -> createStream(v)::iterator); + * </code></pre> + * <p> + * Note that {@code Stream}s can be consumed only once; any subsequent attempt to consume a {@code Stream} + * will result in an {@link IllegalStateException}. + * <p> + * Primitive streams are not supported and items have to be boxed manually (e.g., via {@link IntStream#boxed()}): + * <pre><code> + * source.flatMapStream(v -> IntStream.rangeClosed(v + 1, v + 10).boxed()); + * </code></pre> + * <p> + * {@code Stream} does not support concurrent usage so creating and/or consuming the same instance multiple times + * from multiple threads can lead to undefined behavior. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors the downstream backpressure and consumes the inner stream only on demand. The operator + * prefetches {@link Flowable#bufferSize()} items of the upstream (then 75% of it after the 75% received) + * and caches them until they are ready to be mapped into {@code Stream}s + * after the current {@code Stream} has been consumed.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code flatMapStream} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <R> the element type of the {@code Stream}s and the result + * @param mapper the function that receives an upstream item and should return a {@code Stream} whose elements + * will be emitted to the downstream + * @return the new {@code ParallelFlowable} instance + * @throws NullPointerException if {@code mapper} is {@code null} + * @see #flatMap(Function) + * @see #flatMapIterable(Function) + * @see #flatMapStream(Function, int) + * @since 3.0.0 + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull R> ParallelFlowable<R> flatMapStream(@NonNull Function<? super T, @NonNull ? extends Stream<? extends R>> mapper) { + return flatMapStream(mapper, Flowable.bufferSize()); + } + + /** + * Maps each upstream item of each rail into a {@link Stream} and emits the {@code Stream}'s items to the downstream in a sequential fashion. + * <p> + * <img width="640" height="270" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/flatMapStream.fi.png" alt=""> + * <p> + * Due to the blocking and sequential nature of Java {@code Stream}s, the streams are mapped and consumed in a sequential fashion + * without interleaving (unlike a more general {@link #flatMap(Function)}). Therefore, {@code flatMapStream} and + * {@code concatMapStream} are identical operators and are provided as aliases. + * <p> + * The operator closes the {@code Stream} upon cancellation and when it terminates. The exceptions raised when + * closing a {@code Stream} are routed to the global error handler ({@link RxJavaPlugins#onError(Throwable)}. + * If a {@code Stream} should not be closed, turn it into an {@link Iterable} and use {@link #flatMapIterable(Function, int)}: + * <pre><code> + * source.flatMapIterable(v -> createStream(v)::iterator, 32); + * </code></pre> + * <p> + * Note that {@code Stream}s can be consumed only once; any subsequent attempt to consume a {@code Stream} + * will result in an {@link IllegalStateException}. + * <p> + * Primitive streams are not supported and items have to be boxed manually (e.g., via {@link IntStream#boxed()}): + * <pre><code> + * source.flatMapStream(v -> IntStream.rangeClosed(v + 1, v + 10).boxed(), 32); + * </code></pre> + * <p> + * {@code Stream} does not support concurrent usage so creating and/or consuming the same instance multiple times + * from multiple threads can lead to undefined behavior. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors the downstream backpressure and consumes the inner stream only on demand. The operator + * prefetches the given amount of upstream items and caches them until they are ready to be mapped into {@code Stream}s + * after the current {@code Stream} has been consumed.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code flatMapStream} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * + * @param <R> the element type of the {@code Stream}s and the result + * @param mapper the function that receives an upstream item and should return a {@code Stream} whose elements + * will be emitted to the downstream + * @param prefetch the number of upstream items to request upfront, then 75% of this amount after each 75% upstream items received + * @return the new {@code ParallelFlowable} instance + * @throws NullPointerException if {@code mapper} is {@code null} + * @throws IllegalArgumentException if {@code prefetch} is non-positive + * @see #flatMap(Function, boolean, int) + * @see #flatMapIterable(Function, int) + * @since 3.0.0 + */ + @CheckReturnValue + @BackpressureSupport(BackpressureKind.FULL) + @SchedulerSupport(SchedulerSupport.NONE) + @NonNull + public final <@NonNull R> ParallelFlowable<R> flatMapStream(@NonNull Function<? super T, @NonNull ? extends Stream<? extends R>> mapper, int prefetch) { + Objects.requireNonNull(mapper, "mapper is null"); + ObjectHelper.verifyPositive(prefetch, "prefetch"); + return RxJavaPlugins.onAssembly(new ParallelFlatMapStream<>(this, mapper, prefetch)); + } + + /** + * Reduces all values within a 'rail' and across 'rails' with a callbacks + * of the given {@link Collector} into one {@link Flowable} containing a single value. + * <p> + * Each parallel rail receives its own {@link Collector#accumulator()} and + * {@link Collector#combiner()}. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The operator honors backpressure from the downstream and consumes + * the upstream rails in an unbounded manner (requesting {@link Long#MAX_VALUE}).</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code collect} does not operate by default on a particular {@link Scheduler}.</dd> + * </dl> + * @param <A> the accumulator type + * @param <R> the output value type + * @param collector the {@code Collector} instance + * @return the new {@code Flowable} instance emitting the collected value. + * @throws NullPointerException if {@code collector} is {@code null} + * @since 3.0.0 + */ + @CheckReturnValue + @NonNull + @BackpressureSupport(BackpressureKind.UNBOUNDED_IN) + @SchedulerSupport(SchedulerSupport.NONE) + public final <@NonNull A, @NonNull R> Flowable<R> collect(@NonNull Collector<T, A, R> collector) { + Objects.requireNonNull(collector, "collector is null"); + return RxJavaPlugins.onAssembly(new ParallelCollector<>(this, collector)); + } +} diff --git a/src/main/java/io/reactivex/rxjava3/parallel/ParallelFlowableConverter.java b/src/main/java/io/reactivex/rxjava3/parallel/ParallelFlowableConverter.java new file mode 100644 index 0000000000..47510faf37 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/parallel/ParallelFlowableConverter.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.parallel; + +import io.reactivex.rxjava3.annotations.NonNull; + +/** + * Convenience interface and callback used by the {@link ParallelFlowable#to} operator to turn a ParallelFlowable into + * another value fluently. + * <p>History: 2.1.7 - experimental + * @param <T> the upstream type + * @param <R> the output type + * @since 2.2 + */ +@FunctionalInterface +public interface ParallelFlowableConverter<T, R> { + /** + * Applies a function to the upstream ParallelFlowable and returns a converted value of type {@code R}. + * + * @param upstream the upstream ParallelFlowable instance + * @return the converted value + */ + @NonNull + R apply(@NonNull ParallelFlowable<T> upstream); +} diff --git a/src/main/java/io/reactivex/rxjava3/parallel/ParallelTransformer.java b/src/main/java/io/reactivex/rxjava3/parallel/ParallelTransformer.java new file mode 100644 index 0000000000..f35f71567c --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/parallel/ParallelTransformer.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.parallel; + +import io.reactivex.rxjava3.annotations.NonNull; + +/** + * Interface to compose ParallelFlowable. + * <p>History: 2.0.8 - experimental + * @param <Upstream> the upstream value type + * @param <Downstream> the downstream value type + * @since 2.2 + */ +@FunctionalInterface +public interface ParallelTransformer<@NonNull Upstream, @NonNull Downstream> { + /** + * Applies a function to the upstream ParallelFlowable and returns a ParallelFlowable with + * optionally different element type. + * @param upstream the upstream ParallelFlowable instance + * @return the transformed ParallelFlowable instance + */ + @NonNull + ParallelFlowable<Downstream> apply(@NonNull ParallelFlowable<Upstream> upstream); +} diff --git a/src/main/java/io/reactivex/rxjava3/parallel/package-info.java b/src/main/java/io/reactivex/rxjava3/parallel/package-info.java new file mode 100644 index 0000000000..09605021f0 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/parallel/package-info.java @@ -0,0 +1,18 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +/** + * Contains the base type {@link io.reactivex.rxjava3.parallel.ParallelFlowable}, + * a sub-DSL for working with {@link io.reactivex.rxjava3.core.Flowable} sequences in parallel. + */ +package io.reactivex.rxjava3.parallel; diff --git a/src/main/java/io/reactivex/rxjava3/plugins/RxJavaPlugins.java b/src/main/java/io/reactivex/rxjava3/plugins/RxJavaPlugins.java new file mode 100644 index 0000000000..2949253b31 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/plugins/RxJavaPlugins.java @@ -0,0 +1,1399 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.plugins; + +import java.lang.Thread.UncaughtExceptionHandler; +import java.util.Objects; +import java.util.concurrent.*; + +import org.reactivestreams.Subscriber; + +import io.reactivex.rxjava3.annotations.*; +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.flowables.ConnectableFlowable; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.schedulers.*; +import io.reactivex.rxjava3.internal.util.ExceptionHelper; +import io.reactivex.rxjava3.observables.ConnectableObservable; +import io.reactivex.rxjava3.parallel.ParallelFlowable; +import io.reactivex.rxjava3.schedulers.Schedulers; +/** + * Utility class to inject handlers to certain standard RxJava operations. + */ +public final class RxJavaPlugins { + @Nullable + static volatile Consumer<? super Throwable> errorHandler; + + @Nullable + static volatile Function<? super Runnable, ? extends Runnable> onScheduleHandler; + + @Nullable + static volatile Function<? super Supplier<Scheduler>, ? extends Scheduler> onInitComputationHandler; + + @Nullable + static volatile Function<? super Supplier<Scheduler>, ? extends Scheduler> onInitSingleHandler; + + @Nullable + static volatile Function<? super Supplier<Scheduler>, ? extends Scheduler> onInitIoHandler; + + @Nullable + static volatile Function<? super Supplier<Scheduler>, ? extends Scheduler> onInitNewThreadHandler; + + @Nullable + static volatile Function<? super Scheduler, ? extends Scheduler> onComputationHandler; + + @Nullable + static volatile Function<? super Scheduler, ? extends Scheduler> onSingleHandler; + + @Nullable + static volatile Function<? super Scheduler, ? extends Scheduler> onIoHandler; + + @Nullable + static volatile Function<? super Scheduler, ? extends Scheduler> onNewThreadHandler; + + @SuppressWarnings("rawtypes") + @Nullable + static volatile Function<? super Flowable, ? extends Flowable> onFlowableAssembly; + + @SuppressWarnings("rawtypes") + @Nullable + static volatile Function<? super ConnectableFlowable, ? extends ConnectableFlowable> onConnectableFlowableAssembly; + + @SuppressWarnings("rawtypes") + @Nullable + static volatile Function<? super Observable, ? extends Observable> onObservableAssembly; + + @SuppressWarnings("rawtypes") + @Nullable + static volatile Function<? super ConnectableObservable, ? extends ConnectableObservable> onConnectableObservableAssembly; + + @SuppressWarnings("rawtypes") + @Nullable + static volatile Function<? super Maybe, ? extends Maybe> onMaybeAssembly; + + @SuppressWarnings("rawtypes") + @Nullable + static volatile Function<? super Single, ? extends Single> onSingleAssembly; + + @Nullable + static volatile Function<? super Completable, ? extends Completable> onCompletableAssembly; + + @SuppressWarnings("rawtypes") + @Nullable + static volatile Function<? super ParallelFlowable, ? extends ParallelFlowable> onParallelAssembly; + + @SuppressWarnings("rawtypes") + @Nullable + static volatile BiFunction<? super Flowable, @NonNull ? super Subscriber, @NonNull ? extends Subscriber> onFlowableSubscribe; + + @SuppressWarnings("rawtypes") + @Nullable + static volatile BiFunction<? super Maybe, @NonNull ? super MaybeObserver, @NonNull ? extends MaybeObserver> onMaybeSubscribe; + + @SuppressWarnings("rawtypes") + @Nullable + static volatile BiFunction<? super Observable, @NonNull ? super Observer, @NonNull ? extends Observer> onObservableSubscribe; + + @SuppressWarnings("rawtypes") + @Nullable + static volatile BiFunction<? super Single, @NonNull ? super SingleObserver, @NonNull ? extends SingleObserver> onSingleSubscribe; + + @Nullable + static volatile BiFunction<? super Completable, @NonNull ? super CompletableObserver, @NonNull ? extends CompletableObserver> onCompletableSubscribe; + + @SuppressWarnings("rawtypes") + @Nullable + static volatile BiFunction<? super ParallelFlowable, @NonNull ? super Subscriber[], @NonNull ? extends Subscriber[]> onParallelSubscribe; + + @Nullable + static volatile BooleanSupplier onBeforeBlocking; + + /** Prevents changing the plugins. */ + static volatile boolean lockdown; + + /** + * If true, attempting to run a blockingX operation on a (by default) + * computation or single scheduler will throw an IllegalStateException. + */ + static volatile boolean failNonBlockingScheduler; + + /** + * Prevents changing the plugins from then on. + * <p>This allows container-like environments to prevent clients + * messing with plugins. + */ + public static void lockdown() { + lockdown = true; + } + + /** + * Returns true if the plugins were locked down. + * @return true if the plugins were locked down + */ + public static boolean isLockdown() { + return lockdown; + } + + /** + * Enables or disables the blockingX operators to fail + * with an IllegalStateException on a non-blocking + * scheduler such as computation or single. + * <p>History: 2.0.5 - experimental + * @param enable enable or disable the feature + * @since 2.1 + */ + public static void setFailOnNonBlockingScheduler(boolean enable) { + if (lockdown) { + throw new IllegalStateException("Plugins can't be changed anymore"); + } + failNonBlockingScheduler = enable; + } + + /** + * Returns true if the blockingX operators fail + * with an IllegalStateException on a non-blocking scheduler + * such as computation or single. + * <p>History: 2.0.5 - experimental + * @return true if the blockingX operators fail on a non-blocking scheduler + * @since 2.1 + */ + public static boolean isFailOnNonBlockingScheduler() { + return failNonBlockingScheduler; + } + + /** + * Returns the current hook function. + * @return the hook function, may be null + */ + @Nullable + public static Function<? super Scheduler, ? extends Scheduler> getComputationSchedulerHandler() { + return onComputationHandler; + } + + /** + * Returns the a hook consumer. + * @return the hook consumer, may be null + */ + @Nullable + public static Consumer<? super Throwable> getErrorHandler() { + return errorHandler; + } + + /** + * Returns the current hook function. + * @return the hook function, may be null + */ + @Nullable + public static Function<? super Supplier<Scheduler>, ? extends Scheduler> getInitComputationSchedulerHandler() { + return onInitComputationHandler; + } + + /** + * Returns the current hook function. + * @return the hook function, may be null + */ + @Nullable + public static Function<? super Supplier<Scheduler>, ? extends Scheduler> getInitIoSchedulerHandler() { + return onInitIoHandler; + } + + /** + * Returns the current hook function. + * @return the hook function, may be null + */ + @Nullable + public static Function<? super Supplier<Scheduler>, ? extends Scheduler> getInitNewThreadSchedulerHandler() { + return onInitNewThreadHandler; + } + + /** + * Returns the current hook function. + * @return the hook function, may be null + */ + @Nullable + public static Function<? super Supplier<Scheduler>, ? extends Scheduler> getInitSingleSchedulerHandler() { + return onInitSingleHandler; + } + + /** + * Returns the current hook function. + * @return the hook function, may be null + */ + @Nullable + public static Function<? super Scheduler, ? extends Scheduler> getIoSchedulerHandler() { + return onIoHandler; + } + + /** + * Returns the current hook function. + * @return the hook function, may be null + */ + @Nullable + public static Function<? super Scheduler, ? extends Scheduler> getNewThreadSchedulerHandler() { + return onNewThreadHandler; + } + + /** + * Returns the current hook function. + * @return the hook function, may be null + */ + @Nullable + public static Function<? super Runnable, ? extends Runnable> getScheduleHandler() { + return onScheduleHandler; + } + + /** + * Returns the current hook function. + * @return the hook function, may be null + */ + @Nullable + public static Function<? super Scheduler, ? extends Scheduler> getSingleSchedulerHandler() { + return onSingleHandler; + } + + /** + * Calls the associated hook function. + * @param defaultScheduler a {@link Supplier} which returns the hook's input value + * @return the value returned by the hook, not null + * @throws NullPointerException if the supplier parameter or its result are null + */ + @NonNull + public static Scheduler initComputationScheduler(@NonNull Supplier<Scheduler> defaultScheduler) { + Objects.requireNonNull(defaultScheduler, "Scheduler Supplier can't be null"); + Function<? super Supplier<Scheduler>, ? extends Scheduler> f = onInitComputationHandler; + if (f == null) { + return callRequireNonNull(defaultScheduler); + } + return applyRequireNonNull(f, defaultScheduler); // JIT will skip this + } + + /** + * Calls the associated hook function. + * @param defaultScheduler a {@link Supplier} which returns the hook's input value + * @return the value returned by the hook, not null + * @throws NullPointerException if the supplier parameter or its result are null + */ + @NonNull + public static Scheduler initIoScheduler(@NonNull Supplier<Scheduler> defaultScheduler) { + Objects.requireNonNull(defaultScheduler, "Scheduler Supplier can't be null"); + Function<? super Supplier<Scheduler>, ? extends Scheduler> f = onInitIoHandler; + if (f == null) { + return callRequireNonNull(defaultScheduler); + } + return applyRequireNonNull(f, defaultScheduler); + } + + /** + * Calls the associated hook function. + * @param defaultScheduler a {@link Supplier} which returns the hook's input value + * @return the value returned by the hook, not null + * @throws NullPointerException if the supplier parameter or its result are null + */ + @NonNull + public static Scheduler initNewThreadScheduler(@NonNull Supplier<Scheduler> defaultScheduler) { + Objects.requireNonNull(defaultScheduler, "Scheduler Supplier can't be null"); + Function<? super Supplier<Scheduler>, ? extends Scheduler> f = onInitNewThreadHandler; + if (f == null) { + return callRequireNonNull(defaultScheduler); + } + return applyRequireNonNull(f, defaultScheduler); + } + + /** + * Calls the associated hook function. + * @param defaultScheduler a {@link Supplier} which returns the hook's input value + * @return the value returned by the hook, not null + * @throws NullPointerException if the supplier parameter or its result are null + */ + @NonNull + public static Scheduler initSingleScheduler(@NonNull Supplier<Scheduler> defaultScheduler) { + Objects.requireNonNull(defaultScheduler, "Scheduler Supplier can't be null"); + Function<? super Supplier<Scheduler>, ? extends Scheduler> f = onInitSingleHandler; + if (f == null) { + return callRequireNonNull(defaultScheduler); + } + return applyRequireNonNull(f, defaultScheduler); + } + + /** + * Calls the associated hook function. + * @param defaultScheduler the hook's input value + * @return the value returned by the hook + */ + @NonNull + public static Scheduler onComputationScheduler(@NonNull Scheduler defaultScheduler) { + Function<? super Scheduler, ? extends Scheduler> f = onComputationHandler; + if (f == null) { + return defaultScheduler; + } + return apply(f, defaultScheduler); + } + + /** + * Called when an undeliverable error occurs. + * <p> + * Undeliverable errors are those {@code Observer.onError()} invocations that are not allowed to happen on + * the given consumer type ({@code Observer}, {@code Subscriber}, etc.) due to protocol restrictions + * because the consumer has either disposed/cancelled its {@code Disposable}/{@code Subscription} or + * has already terminated with an {@code onError()} or {@code onComplete()} signal. + * <p> + * By default, this global error handler prints the stacktrace via {@link Throwable#printStackTrace()} + * and calls {@link java.lang.Thread.UncaughtExceptionHandler#uncaughtException(Thread, Throwable)} + * on the current thread. + * <p> + * Note that on some platforms, the platform runtime terminates the current application with an error if such + * uncaught exceptions happen. In this case, it is recommended the application installs a global error + * handler via the {@link #setErrorHandler(Consumer)} plugin method. + * + * @param error the error to report + * @see #getErrorHandler() + * @see #setErrorHandler(Consumer) + * @see <a href="https://github.com/ReactiveX/RxJava/wiki/What's-different-in-2.0#error-handling">Error handling Wiki</a> + */ + public static void onError(@NonNull Throwable error) { + Consumer<? super Throwable> f = errorHandler; + + if (error == null) { + error = ExceptionHelper.createNullPointerException("onError called with a null Throwable."); + } else { + if (!isBug(error)) { + error = new UndeliverableException(error); + } + } + + if (f != null) { + try { + f.accept(error); + return; + } catch (Throwable e) { + // Exceptions.throwIfFatal(e); TODO decide + e.printStackTrace(); // NOPMD + uncaught(e); + } + } + + error.printStackTrace(); // NOPMD + uncaught(error); + } + + /** + * Checks if the given error is one of the already named + * bug cases that should pass through {@link #onError(Throwable)} + * as is. + * @param error the error to check + * @return true if the error should pass through, false if + * it may be wrapped into an UndeliverableException + */ + static boolean isBug(Throwable error) { + // user forgot to add the onError handler in subscribe + if (error instanceof OnErrorNotImplementedException) { + return true; + } + // the sender didn't honor the request amount + if (error instanceof MissingBackpressureException) { + return true; + } + // it's either due to an operator bug or concurrent onNext + if (error instanceof QueueOverflowException) { + return true; + } + // general protocol violations + // it's either due to an operator bug or concurrent onNext + if (error instanceof IllegalStateException) { + return true; + } + // nulls are generally not allowed + // likely an operator bug or missing null-check + if (error instanceof NullPointerException) { + return true; + } + // bad arguments, likely invalid user input + if (error instanceof IllegalArgumentException) { + return true; + } + // Crash while handling an exception + if (error instanceof CompositeException) { + return true; + } + // everything else is probably due to lifecycle limits + return false; + } + + static void uncaught(@NonNull Throwable error) { + Thread currentThread = Thread.currentThread(); + UncaughtExceptionHandler handler = currentThread.getUncaughtExceptionHandler(); + handler.uncaughtException(currentThread, error); + } + + /** + * Calls the associated hook function. + * @param defaultScheduler the hook's input value + * @return the value returned by the hook + */ + @NonNull + public static Scheduler onIoScheduler(@NonNull Scheduler defaultScheduler) { + Function<? super Scheduler, ? extends Scheduler> f = onIoHandler; + if (f == null) { + return defaultScheduler; + } + return apply(f, defaultScheduler); + } + + /** + * Calls the associated hook function. + * @param defaultScheduler the hook's input value + * @return the value returned by the hook + */ + @NonNull + public static Scheduler onNewThreadScheduler(@NonNull Scheduler defaultScheduler) { + Function<? super Scheduler, ? extends Scheduler> f = onNewThreadHandler; + if (f == null) { + return defaultScheduler; + } + return apply(f, defaultScheduler); + } + + /** + * Called when a task is scheduled. + * @param run the runnable instance + * @return the replacement runnable + */ + @NonNull + public static Runnable onSchedule(@NonNull Runnable run) { + Objects.requireNonNull(run, "run is null"); + + Function<? super Runnable, ? extends Runnable> f = onScheduleHandler; + if (f == null) { + return run; + } + return apply(f, run); + } + + /** + * Calls the associated hook function. + * @param defaultScheduler the hook's input value + * @return the value returned by the hook + */ + @NonNull + public static Scheduler onSingleScheduler(@NonNull Scheduler defaultScheduler) { + Function<? super Scheduler, ? extends Scheduler> f = onSingleHandler; + if (f == null) { + return defaultScheduler; + } + return apply(f, defaultScheduler); + } + + /** + * Removes all handlers and resets to default behavior. + */ + public static void reset() { + setErrorHandler(null); + setScheduleHandler(null); + + setComputationSchedulerHandler(null); + setInitComputationSchedulerHandler(null); + + setIoSchedulerHandler(null); + setInitIoSchedulerHandler(null); + + setSingleSchedulerHandler(null); + setInitSingleSchedulerHandler(null); + + setNewThreadSchedulerHandler(null); + setInitNewThreadSchedulerHandler(null); + + setOnFlowableAssembly(null); + setOnFlowableSubscribe(null); + + setOnObservableAssembly(null); + setOnObservableSubscribe(null); + + setOnSingleAssembly(null); + setOnSingleSubscribe(null); + + setOnCompletableAssembly(null); + setOnCompletableSubscribe(null); + + setOnConnectableFlowableAssembly(null); + setOnConnectableObservableAssembly(null); + + setOnMaybeAssembly(null); + setOnMaybeSubscribe(null); + + setOnParallelAssembly(null); + setOnParallelSubscribe(null); + + setFailOnNonBlockingScheduler(false); + setOnBeforeBlocking(null); + } + + /** + * Sets the specific hook function. + * @param handler the hook function to set, null allowed + */ + public static void setComputationSchedulerHandler(@Nullable Function<? super Scheduler, ? extends Scheduler> handler) { + if (lockdown) { + throw new IllegalStateException("Plugins can't be changed anymore"); + } + onComputationHandler = handler; + } + + /** + * Sets the specific hook function. + * @param handler the hook function to set, null allowed + */ + public static void setErrorHandler(@Nullable Consumer<? super Throwable> handler) { + if (lockdown) { + throw new IllegalStateException("Plugins can't be changed anymore"); + } + errorHandler = handler; + } + + /** + * Sets the specific hook function. + * @param handler the hook function to set, null allowed, but the function may not return null + */ + public static void setInitComputationSchedulerHandler(@Nullable Function<? super Supplier<Scheduler>, ? extends Scheduler> handler) { + if (lockdown) { + throw new IllegalStateException("Plugins can't be changed anymore"); + } + onInitComputationHandler = handler; + } + + /** + * Sets the specific hook function. + * @param handler the hook function to set, null allowed, but the function may not return null + */ + public static void setInitIoSchedulerHandler(@Nullable Function<? super Supplier<Scheduler>, ? extends Scheduler> handler) { + if (lockdown) { + throw new IllegalStateException("Plugins can't be changed anymore"); + } + onInitIoHandler = handler; + } + + /** + * Sets the specific hook function. + * @param handler the hook function to set, null allowed, but the function may not return null + */ + public static void setInitNewThreadSchedulerHandler(@Nullable Function<? super Supplier<Scheduler>, ? extends Scheduler> handler) { + if (lockdown) { + throw new IllegalStateException("Plugins can't be changed anymore"); + } + onInitNewThreadHandler = handler; + } + + /** + * Sets the specific hook function. + * @param handler the hook function to set, null allowed, but the function may not return null + */ + public static void setInitSingleSchedulerHandler(@Nullable Function<? super Supplier<Scheduler>, ? extends Scheduler> handler) { + if (lockdown) { + throw new IllegalStateException("Plugins can't be changed anymore"); + } + onInitSingleHandler = handler; + } + + /** + * Sets the specific hook function. + * @param handler the hook function to set, null allowed + */ + public static void setIoSchedulerHandler(@Nullable Function<? super Scheduler, ? extends Scheduler> handler) { + if (lockdown) { + throw new IllegalStateException("Plugins can't be changed anymore"); + } + onIoHandler = handler; + } + + /** + * Sets the specific hook function. + * @param handler the hook function to set, null allowed + */ + public static void setNewThreadSchedulerHandler(@Nullable Function<? super Scheduler, ? extends Scheduler> handler) { + if (lockdown) { + throw new IllegalStateException("Plugins can't be changed anymore"); + } + onNewThreadHandler = handler; + } + + /** + * Sets the specific hook function. + * @param handler the hook function to set, null allowed + */ + public static void setScheduleHandler(@Nullable Function<? super Runnable, ? extends Runnable> handler) { + if (lockdown) { + throw new IllegalStateException("Plugins can't be changed anymore"); + } + onScheduleHandler = handler; + } + + /** + * Sets the specific hook function. + * @param handler the hook function to set, null allowed + */ + public static void setSingleSchedulerHandler(@Nullable Function<? super Scheduler, ? extends Scheduler> handler) { + if (lockdown) { + throw new IllegalStateException("Plugins can't be changed anymore"); + } + onSingleHandler = handler; + } + + /** + * Revokes the lockdown, only for testing purposes. + */ + /* test. */static void unlock() { + lockdown = false; + } + + /** + * Returns the current hook function. + * @return the hook function, may be null + */ + @Nullable + public static Function<? super Completable, ? extends Completable> getOnCompletableAssembly() { + return onCompletableAssembly; + } + + /** + * Returns the current hook function. + * @return the hook function, may be null + */ + @Nullable + public static BiFunction<? super Completable, @NonNull ? super CompletableObserver, @NonNull ? extends CompletableObserver> getOnCompletableSubscribe() { + return onCompletableSubscribe; + } + + /** + * Returns the current hook function. + * @return the hook function, may be null + */ + @SuppressWarnings("rawtypes") + @Nullable + public static Function<? super Flowable, ? extends Flowable> getOnFlowableAssembly() { + return onFlowableAssembly; + } + + /** + * Returns the current hook function. + * @return the hook function, may be null + */ + @SuppressWarnings("rawtypes") + @Nullable + public static Function<? super ConnectableFlowable, ? extends ConnectableFlowable> getOnConnectableFlowableAssembly() { + return onConnectableFlowableAssembly; + } + + /** + * Returns the current hook function. + * @return the hook function, may be null + */ + @Nullable + @SuppressWarnings("rawtypes") + public static BiFunction<? super Flowable, @NonNull ? super Subscriber, @NonNull ? extends Subscriber> getOnFlowableSubscribe() { + return onFlowableSubscribe; + } + + /** + * Returns the current hook function. + * @return the hook function, may be null + */ + @Nullable + @SuppressWarnings("rawtypes") + public static BiFunction<? super Maybe, @NonNull ? super MaybeObserver, @NonNull ? extends MaybeObserver> getOnMaybeSubscribe() { + return onMaybeSubscribe; + } + + /** + * Returns the current hook function. + * @return the hook function, may be null + */ + @Nullable + @SuppressWarnings("rawtypes") + public static Function<? super Maybe, ? extends Maybe> getOnMaybeAssembly() { + return onMaybeAssembly; + } + + /** + * Returns the current hook function. + * @return the hook function, may be null + */ + @Nullable + @SuppressWarnings("rawtypes") + public static Function<? super Single, ? extends Single> getOnSingleAssembly() { + return onSingleAssembly; + } + + /** + * Returns the current hook function. + * @return the hook function, may be null + */ + @Nullable + @SuppressWarnings("rawtypes") + public static BiFunction<? super Single, @NonNull ? super SingleObserver, @NonNull ? extends SingleObserver> getOnSingleSubscribe() { + return onSingleSubscribe; + } + + /** + * Returns the current hook function. + * @return the hook function, may be null + */ + @Nullable + @SuppressWarnings("rawtypes") + public static Function<? super Observable, ? extends Observable> getOnObservableAssembly() { + return onObservableAssembly; + } + + /** + * Returns the current hook function. + * @return the hook function, may be null + */ + @Nullable + @SuppressWarnings("rawtypes") + public static Function<? super ConnectableObservable, ? extends ConnectableObservable> getOnConnectableObservableAssembly() { + return onConnectableObservableAssembly; + } + + /** + * Returns the current hook function. + * @return the hook function, may be null + */ + @Nullable + @SuppressWarnings("rawtypes") + public static BiFunction<? super Observable, @NonNull ? super Observer, @NonNull ? extends Observer> getOnObservableSubscribe() { + return onObservableSubscribe; + } + + /** + * Sets the specific hook function. + * @param onCompletableAssembly the hook function to set, null allowed + */ + public static void setOnCompletableAssembly(@Nullable Function<? super Completable, ? extends Completable> onCompletableAssembly) { + if (lockdown) { + throw new IllegalStateException("Plugins can't be changed anymore"); + } + RxJavaPlugins.onCompletableAssembly = onCompletableAssembly; + } + + /** + * Sets the specific hook function. + * @param onCompletableSubscribe the hook function to set, null allowed + */ + public static void setOnCompletableSubscribe( + @Nullable BiFunction<? super Completable, @NonNull ? super CompletableObserver, @NonNull ? extends CompletableObserver> onCompletableSubscribe) { + if (lockdown) { + throw new IllegalStateException("Plugins can't be changed anymore"); + } + RxJavaPlugins.onCompletableSubscribe = onCompletableSubscribe; + } + + /** + * Sets the specific hook function. + * @param onFlowableAssembly the hook function to set, null allowed + */ + @SuppressWarnings("rawtypes") + public static void setOnFlowableAssembly(@Nullable Function<? super Flowable, ? extends Flowable> onFlowableAssembly) { + if (lockdown) { + throw new IllegalStateException("Plugins can't be changed anymore"); + } + RxJavaPlugins.onFlowableAssembly = onFlowableAssembly; + } + + /** + * Sets the specific hook function. + * @param onMaybeAssembly the hook function to set, null allowed + */ + @SuppressWarnings("rawtypes") + public static void setOnMaybeAssembly(@Nullable Function<? super Maybe, ? extends Maybe> onMaybeAssembly) { + if (lockdown) { + throw new IllegalStateException("Plugins can't be changed anymore"); + } + RxJavaPlugins.onMaybeAssembly = onMaybeAssembly; + } + + /** + * Sets the specific hook function. + * @param onConnectableFlowableAssembly the hook function to set, null allowed + */ + @SuppressWarnings("rawtypes") + public static void setOnConnectableFlowableAssembly(@Nullable Function<? super ConnectableFlowable, ? extends ConnectableFlowable> onConnectableFlowableAssembly) { + if (lockdown) { + throw new IllegalStateException("Plugins can't be changed anymore"); + } + RxJavaPlugins.onConnectableFlowableAssembly = onConnectableFlowableAssembly; + } + + /** + * Sets the specific hook function. + * @param onFlowableSubscribe the hook function to set, null allowed + */ + @SuppressWarnings("rawtypes") + public static void setOnFlowableSubscribe(@Nullable BiFunction<? super Flowable, @NonNull ? super Subscriber, @NonNull ? extends Subscriber> onFlowableSubscribe) { + if (lockdown) { + throw new IllegalStateException("Plugins can't be changed anymore"); + } + RxJavaPlugins.onFlowableSubscribe = onFlowableSubscribe; + } + + /** + * Sets the specific hook function. + * @param onMaybeSubscribe the hook function to set, null allowed + */ + @SuppressWarnings("rawtypes") + public static void setOnMaybeSubscribe(@Nullable BiFunction<? super Maybe, @NonNull MaybeObserver, @NonNull ? extends MaybeObserver> onMaybeSubscribe) { + if (lockdown) { + throw new IllegalStateException("Plugins can't be changed anymore"); + } + RxJavaPlugins.onMaybeSubscribe = onMaybeSubscribe; + } + + /** + * Sets the specific hook function. + * @param onObservableAssembly the hook function to set, null allowed + */ + @SuppressWarnings("rawtypes") + public static void setOnObservableAssembly(@Nullable Function<? super Observable, ? extends Observable> onObservableAssembly) { + if (lockdown) { + throw new IllegalStateException("Plugins can't be changed anymore"); + } + RxJavaPlugins.onObservableAssembly = onObservableAssembly; + } + + /** + * Sets the specific hook function. + * @param onConnectableObservableAssembly the hook function to set, null allowed + */ + @SuppressWarnings("rawtypes") + public static void setOnConnectableObservableAssembly(@Nullable Function<? super ConnectableObservable, ? extends ConnectableObservable> onConnectableObservableAssembly) { + if (lockdown) { + throw new IllegalStateException("Plugins can't be changed anymore"); + } + RxJavaPlugins.onConnectableObservableAssembly = onConnectableObservableAssembly; + } + + /** + * Sets the specific hook function. + * @param onObservableSubscribe the hook function to set, null allowed + */ + @SuppressWarnings("rawtypes") + public static void setOnObservableSubscribe( + @Nullable BiFunction<? super Observable, @NonNull ? super Observer, @NonNull ? extends Observer> onObservableSubscribe) { + if (lockdown) { + throw new IllegalStateException("Plugins can't be changed anymore"); + } + RxJavaPlugins.onObservableSubscribe = onObservableSubscribe; + } + + /** + * Sets the specific hook function. + * @param onSingleAssembly the hook function to set, null allowed + */ + @SuppressWarnings("rawtypes") + public static void setOnSingleAssembly(@Nullable Function<? super Single, ? extends Single> onSingleAssembly) { + if (lockdown) { + throw new IllegalStateException("Plugins can't be changed anymore"); + } + RxJavaPlugins.onSingleAssembly = onSingleAssembly; + } + + /** + * Sets the specific hook function. + * @param onSingleSubscribe the hook function to set, null allowed + */ + @SuppressWarnings("rawtypes") + public static void setOnSingleSubscribe(@Nullable BiFunction<? super Single, @NonNull ? super SingleObserver, @NonNull ? extends SingleObserver> onSingleSubscribe) { + if (lockdown) { + throw new IllegalStateException("Plugins can't be changed anymore"); + } + RxJavaPlugins.onSingleSubscribe = onSingleSubscribe; + } + + /** + * Calls the associated hook function. + * @param <T> the value type + * @param source the hook's input value + * @param subscriber the subscriber + * @return the value returned by the hook + */ + @SuppressWarnings({ "rawtypes", "unchecked" }) + @NonNull + public static <@NonNull T> Subscriber<? super T> onSubscribe(@NonNull Flowable<T> source, @NonNull Subscriber<? super T> subscriber) { + BiFunction<? super Flowable, @NonNull ? super Subscriber, @NonNull ? extends Subscriber> f = onFlowableSubscribe; + if (f != null) { + return apply(f, source, subscriber); + } + return subscriber; + } + + /** + * Calls the associated hook function. + * @param <T> the value type + * @param source the hook's input value + * @param observer the observer + * @return the value returned by the hook + */ + @SuppressWarnings({ "rawtypes", "unchecked" }) + @NonNull + public static <@NonNull T> Observer<? super T> onSubscribe(@NonNull Observable<T> source, @NonNull Observer<? super T> observer) { + BiFunction<? super Observable, @NonNull ? super Observer, @NonNull ? extends Observer> f = onObservableSubscribe; + if (f != null) { + return apply(f, source, observer); + } + return observer; + } + + /** + * Calls the associated hook function. + * @param <T> the value type + * @param source the hook's input value + * @param observer the observer + * @return the value returned by the hook + */ + @SuppressWarnings({ "rawtypes", "unchecked" }) + @NonNull + public static <@NonNull T> SingleObserver<? super T> onSubscribe(@NonNull Single<T> source, @NonNull SingleObserver<? super T> observer) { + BiFunction<? super Single, @NonNull ? super SingleObserver, @NonNull ? extends SingleObserver> f = onSingleSubscribe; + if (f != null) { + return apply(f, source, observer); + } + return observer; + } + + /** + * Calls the associated hook function. + * @param source the hook's input value + * @param observer the observer + * @return the value returned by the hook + */ + @NonNull + public static CompletableObserver onSubscribe(@NonNull Completable source, @NonNull CompletableObserver observer) { + BiFunction<? super Completable, @NonNull ? super CompletableObserver, @NonNull ? extends CompletableObserver> f = onCompletableSubscribe; + if (f != null) { + return apply(f, source, observer); + } + return observer; + } + + /** + * Calls the associated hook function. + * @param <T> the value type + * @param source the hook's input value + * @param observer the subscriber + * @return the value returned by the hook + */ + @SuppressWarnings({ "rawtypes", "unchecked" }) + @NonNull + public static <@NonNull T> MaybeObserver<? super T> onSubscribe(@NonNull Maybe<T> source, @NonNull MaybeObserver<? super T> observer) { + BiFunction<? super Maybe, @NonNull ? super MaybeObserver, @NonNull ? extends MaybeObserver> f = onMaybeSubscribe; + if (f != null) { + return apply(f, source, observer); + } + return observer; + } + + /** + * Calls the associated hook function. + * @param <T> the value type + * @param source the hook's input value + * @param subscribers the array of subscribers + * @return the value returned by the hook + */ + @SuppressWarnings({ "rawtypes" }) + @NonNull + public static <@NonNull T> Subscriber<? super T>[] onSubscribe(@NonNull ParallelFlowable<T> source, @NonNull Subscriber<? super T>[] subscribers) { + BiFunction<? super ParallelFlowable, @NonNull ? super Subscriber[], @NonNull ? extends Subscriber[]> f = onParallelSubscribe; + if (f != null) { + return apply(f, source, subscribers); + } + return subscribers; + } + + /** + * Calls the associated hook function. + * @param <T> the value type + * @param source the hook's input value + * @return the value returned by the hook + */ + @SuppressWarnings({ "rawtypes", "unchecked" }) + @NonNull + public static <@NonNull T> Maybe<T> onAssembly(@NonNull Maybe<T> source) { + Function<? super Maybe, ? extends Maybe> f = onMaybeAssembly; + if (f != null) { + return apply(f, source); + } + return source; + } + + /** + * Calls the associated hook function. + * @param <T> the value type + * @param source the hook's input value + * @return the value returned by the hook + */ + @SuppressWarnings({ "rawtypes", "unchecked" }) + @NonNull + public static <@NonNull T> Flowable<T> onAssembly(@NonNull Flowable<T> source) { + Function<? super Flowable, ? extends Flowable> f = onFlowableAssembly; + if (f != null) { + return apply(f, source); + } + return source; + } + + /** + * Calls the associated hook function. + * @param <T> the value type + * @param source the hook's input value + * @return the value returned by the hook + */ + @SuppressWarnings({ "rawtypes", "unchecked" }) + @NonNull + public static <@NonNull T> ConnectableFlowable<T> onAssembly(@NonNull ConnectableFlowable<T> source) { + Function<? super ConnectableFlowable, ? extends ConnectableFlowable> f = onConnectableFlowableAssembly; + if (f != null) { + return apply(f, source); + } + return source; + } + + /** + * Calls the associated hook function. + * @param <T> the value type + * @param source the hook's input value + * @return the value returned by the hook + */ + @SuppressWarnings({ "rawtypes", "unchecked" }) + @NonNull + public static <@NonNull T> Observable<T> onAssembly(@NonNull Observable<T> source) { + Function<? super Observable, ? extends Observable> f = onObservableAssembly; + if (f != null) { + return apply(f, source); + } + return source; + } + + /** + * Calls the associated hook function. + * @param <T> the value type + * @param source the hook's input value + * @return the value returned by the hook + */ + @SuppressWarnings({ "rawtypes", "unchecked" }) + @NonNull + public static <@NonNull T> ConnectableObservable<T> onAssembly(@NonNull ConnectableObservable<T> source) { + Function<? super ConnectableObservable, ? extends ConnectableObservable> f = onConnectableObservableAssembly; + if (f != null) { + return apply(f, source); + } + return source; + } + + /** + * Calls the associated hook function. + * @param <T> the value type + * @param source the hook's input value + * @return the value returned by the hook + */ + @SuppressWarnings({ "rawtypes", "unchecked" }) + @NonNull + public static <@NonNull T> Single<T> onAssembly(@NonNull Single<T> source) { + Function<? super Single, ? extends Single> f = onSingleAssembly; + if (f != null) { + return apply(f, source); + } + return source; + } + + /** + * Calls the associated hook function. + * @param source the hook's input value + * @return the value returned by the hook + */ + @NonNull + public static Completable onAssembly(@NonNull Completable source) { + Function<? super Completable, ? extends Completable> f = onCompletableAssembly; + if (f != null) { + return apply(f, source); + } + return source; + } + + /** + * Sets the specific hook function. + * <p>History: 2.0.6 - experimental; 2.1 - beta + * @param handler the hook function to set, null allowed + * @since 2.2 + */ + @SuppressWarnings("rawtypes") + public static void setOnParallelAssembly(@Nullable Function<? super ParallelFlowable, ? extends ParallelFlowable> handler) { + if (lockdown) { + throw new IllegalStateException("Plugins can't be changed anymore"); + } + onParallelAssembly = handler; + } + + /** + * Returns the current hook function. + * <p>History: 2.0.6 - experimental; 2.1 - beta + * @return the hook function, may be null + * @since 2.2 + */ + @SuppressWarnings("rawtypes") + @Nullable + public static Function<? super ParallelFlowable, ? extends ParallelFlowable> getOnParallelAssembly() { + return onParallelAssembly; + } + + /** + * Sets the specific hook function. + * <p>History: 3.0.11 - experimental + * @param handler the hook function to set, null allowed + * @since 3.1.0 + */ + @SuppressWarnings("rawtypes") + public static void setOnParallelSubscribe(@Nullable BiFunction<? super ParallelFlowable, @NonNull ? super Subscriber[], @NonNull ? extends Subscriber[]> handler) { + if (lockdown) { + throw new IllegalStateException("Plugins can't be changed anymore"); + } + onParallelSubscribe = handler; + } + + /** + * Returns the current hook function. + * <p>History: 3.0.11 - experimental + * @return the hook function, may be null + * @since 3.1.0 + */ + @SuppressWarnings("rawtypes") + @Nullable + public static BiFunction<? super ParallelFlowable, @NonNull ? super Subscriber[], @NonNull ? extends Subscriber[]> getOnParallelSubscribe() { + return onParallelSubscribe; + } + + /** + * Calls the associated hook function. + * <p>History: 2.0.6 - experimental; 2.1 - beta + * @param <T> the value type of the source + * @param source the hook's input value + * @return the value returned by the hook + * @since 2.2 + */ + @SuppressWarnings({ "rawtypes", "unchecked" }) + @NonNull + public static <@NonNull T> ParallelFlowable<T> onAssembly(@NonNull ParallelFlowable<T> source) { + Function<? super ParallelFlowable, ? extends ParallelFlowable> f = onParallelAssembly; + if (f != null) { + return apply(f, source); + } + return source; + } + + /** + * Called before an operator attempts a blocking operation + * such as awaiting a condition or signal + * and should return true to indicate the operator + * should not block but throw an IllegalArgumentException. + * <p>History: 2.0.5 - experimental + * @return true if the blocking should be prevented + * @see #setFailOnNonBlockingScheduler(boolean) + * @since 2.1 + */ + public static boolean onBeforeBlocking() { + BooleanSupplier f = onBeforeBlocking; + if (f != null) { + try { + return f.getAsBoolean(); + } catch (Throwable ex) { + throw ExceptionHelper.wrapOrThrow(ex); + } + } + return false; + } + + /** + * Set the handler that is called when an operator attempts a blocking + * await; the handler should return true to prevent the blocking + * and to signal an IllegalStateException instead. + * <p>History: 2.0.5 - experimental + * @param handler the handler to set, null resets to the default handler + * that always returns false + * @see #onBeforeBlocking() + * @since 2.1 + */ + public static void setOnBeforeBlocking(@Nullable BooleanSupplier handler) { + if (lockdown) { + throw new IllegalStateException("Plugins can't be changed anymore"); + } + onBeforeBlocking = handler; + } + + /** + * Returns the current blocking handler or null if no custom handler + * is set. + * <p>History: 2.0.5 - experimental + * @return the current blocking handler or null if not specified + * @since 2.1 + */ + @Nullable + public static BooleanSupplier getOnBeforeBlocking() { + return onBeforeBlocking; + } + + /** + * Create an instance of the default {@link Scheduler} used for {@link Schedulers#computation()} + * except using {@code threadFactory} for thread creation. + * <p>History: 2.0.5 - experimental + * @param threadFactory thread factory to use for creating worker threads. Note that this takes precedence over any + * system properties for configuring new thread creation. Cannot be null. + * @return the created Scheduler instance + * @since 2.1 + */ + @NonNull + public static Scheduler createComputationScheduler(@NonNull ThreadFactory threadFactory) { + return new ComputationScheduler(Objects.requireNonNull(threadFactory, "threadFactory is null")); + } + + /** + * Create an instance of the default {@link Scheduler} used for {@link Schedulers#io()} + * except using {@code threadFactory} for thread creation. + * <p>History: 2.0.5 - experimental + * @param threadFactory thread factory to use for creating worker threads. Note that this takes precedence over any + * system properties for configuring new thread creation. Cannot be null. + * @return the created Scheduler instance + * @since 2.1 + */ + @NonNull + public static Scheduler createIoScheduler(@NonNull ThreadFactory threadFactory) { + return new IoScheduler(Objects.requireNonNull(threadFactory, "threadFactory is null")); + } + + /** + * Create an instance of the default {@link Scheduler} used for {@link Schedulers#newThread()} + * except using {@code threadFactory} for thread creation. + * <p>History: 2.0.5 - experimental + * @param threadFactory thread factory to use for creating worker threads. Note that this takes precedence over any + * system properties for configuring new thread creation. Cannot be null. + * @return the created Scheduler instance + * @since 2.1 + */ + @NonNull + public static Scheduler createNewThreadScheduler(@NonNull ThreadFactory threadFactory) { + return new NewThreadScheduler(Objects.requireNonNull(threadFactory, "threadFactory is null")); + } + + /** + * Create an instance of the default {@link Scheduler} used for {@link Schedulers#single()} + * except using {@code threadFactory} for thread creation. + * <p>History: 2.0.5 - experimental + * @param threadFactory thread factory to use for creating worker threads. Note that this takes precedence over any + * system properties for configuring new thread creation. Cannot be null. + * @return the created Scheduler instance + * @since 2.1 + */ + @NonNull + public static Scheduler createSingleScheduler(@NonNull ThreadFactory threadFactory) { + return new SingleScheduler(Objects.requireNonNull(threadFactory, "threadFactory is null")); + } + + /** + * Create an instance of a {@link Scheduler} by wrapping an existing {@link Executor}. + * <p> + * This method allows creating an {@code Executor}-backed {@code Scheduler} before the {@link Schedulers} class + * would initialize the standard {@code Scheduler}s. + * + * @param executor the {@code Executor} to wrap and turn into a {@code Scheduler}. + * @param interruptibleWorker if {@code true}, the tasks submitted to the {@link io.reactivex.rxjava3.core.Scheduler.Worker Scheduler.Worker} will + * be interrupted when the task is disposed. + * @param fair if {@code true}, tasks submitted to the {@code Scheduler} or {@code Worker} will be executed by the underlying {@code Executor} one after the other, still + * in a FIFO and non-overlapping manner, but allows interleaving with other tasks submitted to the underlying {@code Executor}. + * If {@code false}, the underlying FIFO scheme will execute as many tasks as it can before giving up the underlying {@code Executor} thread. + * @return the new {@code Scheduler} wrapping the {@code Executor} + * @since 3.1.0 + */ + @NonNull + public static Scheduler createExecutorScheduler(@NonNull Executor executor, boolean interruptibleWorker, boolean fair) { + return new ExecutorScheduler(executor, interruptibleWorker, fair); + } + + /** + * Wraps the call to the function in try-catch and propagates thrown + * checked exceptions as RuntimeException. + * @param <T> the input type + * @param <R> the output type + * @param f the function to call, not null (not verified) + * @param t the parameter value to the function + * @return the result of the function call + */ + @NonNull + static <@NonNull T, @NonNull R> R apply(@NonNull Function<T, R> f, @NonNull T t) { + try { + return f.apply(t); + } catch (Throwable ex) { + throw ExceptionHelper.wrapOrThrow(ex); + } + } + + /** + * Wraps the call to the function in try-catch and propagates thrown + * checked exceptions as RuntimeException. + * @param <T> the first input type + * @param <U> the second input type + * @param <R> the output type + * @param f the function to call, not null (not verified) + * @param t the first parameter value to the function + * @param u the second parameter value to the function + * @return the result of the function call + */ + @NonNull + static <@NonNull T, @NonNull U, @NonNull R> R apply(@NonNull BiFunction<T, U, R> f, @NonNull T t, @NonNull U u) { + try { + return f.apply(t, u); + } catch (Throwable ex) { + throw ExceptionHelper.wrapOrThrow(ex); + } + } + + /** + * Wraps the call to the Scheduler creation supplier in try-catch and propagates thrown + * checked exceptions as RuntimeException and enforces that result is not null. + * @param s the {@link Supplier} which returns a {@link Scheduler}, not null (not verified). Cannot return null + * @return the result of the supplier call, not null + * @throws NullPointerException if the supplier parameter returns null + */ + @NonNull + static Scheduler callRequireNonNull(@NonNull Supplier<Scheduler> s) { + try { + return Objects.requireNonNull(s.get(), "Scheduler Supplier result can't be null"); + } catch (Throwable ex) { + throw ExceptionHelper.wrapOrThrow(ex); + } + } + + /** + * Wraps the call to the Scheduler creation function in try-catch and propagates thrown + * checked exceptions as RuntimeException and enforces that result is not null. + * @param f the function to call, not null (not verified). Cannot return null + * @param s the parameter value to the function + * @return the result of the function call, not null + * @throws NullPointerException if the function parameter returns null + */ + @NonNull + static Scheduler applyRequireNonNull(@NonNull Function<? super Supplier<Scheduler>, ? extends Scheduler> f, Supplier<Scheduler> s) { + return Objects.requireNonNull(apply(f, s), "Scheduler Supplier result can't be null"); + } + + /** Helper class, no instances. */ + private RxJavaPlugins() { + throw new IllegalStateException("No instances!"); + } +} diff --git a/src/main/java/io/reactivex/rxjava3/plugins/package-info.java b/src/main/java/io/reactivex/rxjava3/plugins/package-info.java new file mode 100644 index 0000000000..49390584df --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/plugins/package-info.java @@ -0,0 +1,18 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +/** + * Contains the central plugin handler {@link io.reactivex.rxjava3.plugins.RxJavaPlugins} + * class to hook into the lifecycle of the base reactive types and schedulers. + */ +package io.reactivex.rxjava3.plugins; diff --git a/src/main/java/io/reactivex/rxjava3/processors/AsyncProcessor.java b/src/main/java/io/reactivex/rxjava3/processors/AsyncProcessor.java new file mode 100644 index 0000000000..b20c8b23ae --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/processors/AsyncProcessor.java @@ -0,0 +1,371 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.processors; + +import java.util.concurrent.atomic.AtomicReference; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.annotations.*; +import io.reactivex.rxjava3.internal.subscriptions.DeferredScalarSubscription; +import io.reactivex.rxjava3.internal.util.ExceptionHelper; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +/** + * Processor that emits the very last value followed by a completion event or the received error + * to {@link Subscriber}s. + * <p> + * <img width="640" height="239" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/AsyncProcessor.png" alt=""> + * <p> + * This processor does not have a public constructor by design; a new empty instance of this + * {@code AsyncProcessor} can be created via the {@link #create()} method. + * <p> + * Since an {@code AsyncProcessor} is a Reactive Streams {@code Processor} type, + * {@code null}s are not allowed (<a href="https://github.com/reactive-streams/reactive-streams-jvm#2.13">Rule 2.13</a>) + * as parameters to {@link #onNext(Object)} and {@link #onError(Throwable)}. Such calls will result in a + * {@link NullPointerException} being thrown and the processor's state is not changed. + * <p> + * {@code AsyncProcessor} is a {@link io.reactivex.rxjava3.core.Flowable} as well as a {@link FlowableProcessor} and supports backpressure from the downstream but + * its {@link Subscriber}-side consumes items in an unbounded manner. + * <p> + * When this {@code AsyncProcessor} is terminated via {@link #onError(Throwable)}, the + * last observed item (if any) is cleared and late {@link Subscriber}s only receive + * the {@code onError} event. + * <p> + * The {@code AsyncProcessor} caches the latest item internally and it emits this item only when {@code onComplete} is called. + * Therefore, it is not recommended to use this {@code Processor} with infinite or never-completing sources. + * <p> + * Even though {@code AsyncProcessor} implements the {@link Subscriber} interface, calling + * {@code onSubscribe} is not required (<a href="https://github.com/reactive-streams/reactive-streams-jvm#2.12">Rule 2.12</a>) + * if the processor is used as a standalone source. However, calling {@code onSubscribe} + * after the {@code AsyncProcessor} reached its terminal state will result in the + * given {@link Subscription} being canceled immediately. + * <p> + * Calling {@link #onNext(Object)}, {@link #onError(Throwable)} and {@link #onComplete()} + * is required to be serialized (called from the same thread or called non-overlappingly from different threads + * through external means of serialization). The {@link #toSerialized()} method available to all {@code FlowableProcessor}s + * provides such serialization and also protects against reentrance (i.e., when a downstream {@code Subscriber} + * consuming this processor also wants to call {@link #onNext(Object)} on this processor recursively). + * The implementation of {@code onXXX} methods are technically thread-safe but non-serialized calls + * to them may lead to undefined state in the currently subscribed {@code Subscriber}s. + * <p> + * This {@code AsyncProcessor} supports the standard state-peeking methods {@link #hasComplete()}, {@link #hasThrowable()}, + * {@link #getThrowable()} and {@link #hasSubscribers()} as well as means to read the very last observed value - + * after this {@code AsyncProcessor} has been completed - in a non-blocking and thread-safe + * manner via {@link #hasValue()} or {@link #getValue()}. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The {@code AsyncProcessor} honors the backpressure of the downstream {@code Subscriber}s and won't emit + * its single value to a particular {@code Subscriber} until that {@code Subscriber} has requested an item. + * When the {@code AsyncProcessor} is subscribed to a {@link io.reactivex.rxjava3.core.Flowable}, the processor consumes this + * {@code Flowable} in an unbounded manner (requesting {@link Long#MAX_VALUE}) as only the very last upstream item is + * retained by it. + * </dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code AsyncProcessor} does not operate by default on a particular {@link io.reactivex.rxjava3.core.Scheduler} and + * the {@code Subscriber}s get notified on the thread where the terminating {@code onError} or {@code onComplete} + * methods were invoked.</dd> + * <dt><b>Error handling:</b></dt> + * <dd>When the {@link #onError(Throwable)} is called, the {@code AsyncProcessor} enters into a terminal state + * and emits the same {@code Throwable} instance to the last set of {@code Subscriber}s. During this emission, + * if one or more {@code Subscriber}s dispose their respective {@code Subscription}s, the + * {@code Throwable} is delivered to the global error handler via + * {@link io.reactivex.rxjava3.plugins.RxJavaPlugins#onError(Throwable)} (multiple times if multiple {@code Subscriber}s + * cancel at once). + * If there were no {@code Subscriber}s subscribed to this {@code AsyncProcessor} when the {@code onError()} + * was called, the global error handler is not invoked. + * </dd> + * </dl> + * <p> + * Example usage: + * <pre><code> + * AsyncProcessor<Object> processor = AsyncProcessor.create(); + * + * TestSubscriber<Object> ts1 = processor.test(); + * + * ts1.assertEmpty(); + * + * processor.onNext(1); + * + * // AsyncProcessor only emits when onComplete was called. + * ts1.assertEmpty(); + * + * processor.onNext(2); + * processor.onComplete(); + * + * // onComplete triggers the emission of the last cached item and the onComplete event. + * ts1.assertResult(2); + * + * TestSubscriber<Object> ts2 = processor.test(); + * + * // late Subscribers receive the last cached item too + * ts2.assertResult(2); + * </code></pre> + * @param <T> the value type + */ +public final class AsyncProcessor<@NonNull T> extends FlowableProcessor<T> { + + @SuppressWarnings("rawtypes") + static final AsyncSubscription[] EMPTY = new AsyncSubscription[0]; + + @SuppressWarnings("rawtypes") + static final AsyncSubscription[] TERMINATED = new AsyncSubscription[0]; + + final AtomicReference<AsyncSubscription<T>[]> subscribers; + + /** Write before updating subscribers, read after reading subscribers as TERMINATED. */ + Throwable error; + + /** Write before updating subscribers, read after reading subscribers as TERMINATED. */ + T value; + + /** + * Creates a new AsyncProcessor. + * @param <T> the value type to be received and emitted + * @return the new AsyncProcessor instance + */ + @CheckReturnValue + @NonNull + public static <T> AsyncProcessor<T> create() { + return new AsyncProcessor<>(); + } + + /** + * Constructs an AsyncProcessor. + * @since 2.0 + */ + @SuppressWarnings("unchecked") + AsyncProcessor() { + this.subscribers = new AtomicReference<>(EMPTY); + } + + @Override + public void onSubscribe(@NonNull Subscription s) { + if (subscribers.get() == TERMINATED) { + s.cancel(); + return; + } + // AsyncProcessor doesn't bother with request coordination. + s.request(Long.MAX_VALUE); + } + + @Override + public void onNext(@NonNull T t) { + ExceptionHelper.nullCheck(t, "onNext called with a null value."); + if (subscribers.get() == TERMINATED) { + return; + } + value = t; + } + + @SuppressWarnings("unchecked") + @Override + public void onError(@NonNull Throwable t) { + ExceptionHelper.nullCheck(t, "onError called with a null Throwable."); + if (subscribers.get() == TERMINATED) { + RxJavaPlugins.onError(t); + return; + } + value = null; + error = t; + for (AsyncSubscription<T> as : subscribers.getAndSet(TERMINATED)) { + as.onError(t); + } + } + + @SuppressWarnings("unchecked") + @Override + public void onComplete() { + if (subscribers.get() == TERMINATED) { + return; + } + T v = value; + AsyncSubscription<T>[] array = subscribers.getAndSet(TERMINATED); + if (v == null) { + for (AsyncSubscription<T> as : array) { + as.onComplete(); + } + } else { + for (AsyncSubscription<T> as : array) { + as.complete(v); + } + } + } + + @Override + @CheckReturnValue + public boolean hasSubscribers() { + return subscribers.get().length != 0; + } + + @Override + @CheckReturnValue + public boolean hasThrowable() { + return subscribers.get() == TERMINATED && error != null; + } + + @Override + @CheckReturnValue + public boolean hasComplete() { + return subscribers.get() == TERMINATED && error == null; + } + + @Override + @Nullable + @CheckReturnValue + public Throwable getThrowable() { + return subscribers.get() == TERMINATED ? error : null; + } + + @Override + protected void subscribeActual(@NonNull Subscriber<? super T> s) { + AsyncSubscription<T> as = new AsyncSubscription<>(s, this); + s.onSubscribe(as); + if (add(as)) { + if (as.isCancelled()) { + remove(as); + } + } else { + Throwable ex = error; + if (ex != null) { + s.onError(ex); + } else { + T v = value; + if (v != null) { + as.complete(v); + } else { + as.onComplete(); + } + } + } + } + + /** + * Tries to add the given subscriber to the subscribers array atomically + * or returns false if the processor has terminated. + * @param ps the subscriber to add + * @return true if successful, false if the processor has terminated + */ + boolean add(AsyncSubscription<T> ps) { + for (;;) { + AsyncSubscription<T>[] a = subscribers.get(); + if (a == TERMINATED) { + return false; + } + + int n = a.length; + @SuppressWarnings("unchecked") + AsyncSubscription<T>[] b = new AsyncSubscription[n + 1]; + System.arraycopy(a, 0, b, 0, n); + b[n] = ps; + + if (subscribers.compareAndSet(a, b)) { + return true; + } + } + } + + /** + * Atomically removes the given subscriber if it is subscribed to this processor. + * @param ps the subscriber's subscription wrapper to remove + */ + @SuppressWarnings("unchecked") + void remove(AsyncSubscription<T> ps) { + for (;;) { + AsyncSubscription<T>[] a = subscribers.get(); + int n = a.length; + if (n == 0) { + return; + } + + int j = -1; + for (int i = 0; i < n; i++) { + if (a[i] == ps) { + j = i; + break; + } + } + + if (j < 0) { + return; + } + + AsyncSubscription<T>[] b; + + if (n == 1) { + b = EMPTY; + } else { + b = new AsyncSubscription[n - 1]; + System.arraycopy(a, 0, b, 0, j); + System.arraycopy(a, j + 1, b, j, n - j - 1); + } + if (subscribers.compareAndSet(a, b)) { + return; + } + } + } + + /** + * Returns true if this processor has any value. + * <p>The method is thread-safe. + * @return true if this processor has any value + */ + @CheckReturnValue + public boolean hasValue() { + return subscribers.get() == TERMINATED && value != null; + } + + /** + * Returns a single value this processor currently has or null if no such value exists. + * <p>The method is thread-safe. + * @return a single value this processor currently has or null if no such value exists + */ + @Nullable + @CheckReturnValue + public T getValue() { + return subscribers.get() == TERMINATED ? value : null; + } + + static final class AsyncSubscription<@NonNull T> extends DeferredScalarSubscription<T> { + private static final long serialVersionUID = 5629876084736248016L; + + final AsyncProcessor<T> parent; + + AsyncSubscription(Subscriber<? super T> actual, AsyncProcessor<T> parent) { + super(actual); + this.parent = parent; + } + + @Override + public void cancel() { + if (super.tryCancel()) { + parent.remove(this); + } + } + + void onComplete() { + if (!isCancelled()) { + downstream.onComplete(); + } + } + + void onError(Throwable t) { + if (isCancelled()) { + RxJavaPlugins.onError(t); + } else { + downstream.onError(t); + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/processors/BehaviorProcessor.java b/src/main/java/io/reactivex/rxjava3/processors/BehaviorProcessor.java new file mode 100644 index 0000000000..2e5117ca56 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/processors/BehaviorProcessor.java @@ -0,0 +1,620 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.processors; + +import java.util.Objects; +import java.util.concurrent.atomic.*; +import java.util.concurrent.locks.*; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.annotations.*; +import io.reactivex.rxjava3.exceptions.MissingBackpressureException; +import io.reactivex.rxjava3.internal.subscriptions.SubscriptionHelper; +import io.reactivex.rxjava3.internal.util.*; +import io.reactivex.rxjava3.internal.util.AppendOnlyLinkedArrayList.NonThrowingPredicate; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +/** + * Processor that emits the most recent item it has observed and all subsequent observed items to each subscribed + * {@link Subscriber}. + * <p> + * <img width="640" height="460" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/S.BehaviorProcessor.png" alt=""> + * <p> + * This processor does not have a public constructor by design; a new empty instance of this + * {@code BehaviorProcessor} can be created via the {@link #create()} method and + * a new non-empty instance can be created via {@link #createDefault(Object)} (named as such to avoid + * overload resolution conflict with {@code Flowable.create} that creates a Flowable, not a {@code BehaviorProcessor}). + * <p> + * In accordance with the Reactive Streams specification (<a href="https://github.com/reactive-streams/reactive-streams-jvm#2.13">Rule 2.13</a>) + * {@code null}s are not allowed as default initial values in {@link #createDefault(Object)} or as parameters to {@link #onNext(Object)} and + * {@link #onError(Throwable)}. + * <p> + * When this {@code BehaviorProcessor} is terminated via {@link #onError(Throwable)} or {@link #onComplete()}, the + * last observed item (if any) is cleared and late {@link org.reactivestreams.Subscriber}s only receive + * the respective terminal event. + * <p> + * The {@code BehaviorProcessor} does not support clearing its cached value (to appear empty again), however, the + * effect can be achieved by using a special item and making sure {@code Subscriber}s subscribe through a + * filter whose predicate filters out this special item: + * <pre><code> + * BehaviorProcessor<Integer> processor = BehaviorProcessor.create(); + * + * final Integer EMPTY = Integer.MIN_VALUE; + * + * Flowable<Integer> flowable = processor.filter(v -> v != EMPTY); + * + * TestSubscriber<Integer> ts1 = flowable.test(); + * + * processor.onNext(1); + * // this will "clear" the cache + * processor.onNext(EMPTY); + * + * TestSubscriber<Integer> ts2 = flowable.test(); + * + * processor.onNext(2); + * processor.onComplete(); + * + * // ts1 received both non-empty items + * ts1.assertResult(1, 2); + * + * // ts2 received only 2 even though the current item was EMPTY + * // when it got subscribed + * ts2.assertResult(2); + * + * // Subscribers coming after the processor was terminated receive + * // no items and only the onComplete event in this case. + * flowable.test().assertResult(); + * </code></pre> + * <p> + * Even though {@code BehaviorProcessor} implements the {@code Subscriber} interface, calling + * {@code onSubscribe} is not required (<a href="https://github.com/reactive-streams/reactive-streams-jvm#2.12">Rule 2.12</a>) + * if the processor is used as a standalone source. However, calling {@code onSubscribe} + * after the {@code BehaviorProcessor} reached its terminal state will result in the + * given {@code Subscription} being cancelled immediately. + * <p> + * Calling {@link #onNext(Object)}, {@link #offer(Object)}, {@link #onError(Throwable)} and {@link #onComplete()} + * is required to be serialized (called from the same thread or called non-overlappingly from different threads + * through external means of serialization). The {@link #toSerialized()} method available to all {@code FlowableProcessor}s + * provides such serialization and also protects against reentrance (i.e., when a downstream {@code Subscriber} + * consuming this processor also wants to call {@link #onNext(Object)} on this processor recursively). + * Note that serializing over {@link #offer(Object)} is not supported through {@code toSerialized()} because it is a method + * available on the {@code PublishProcessor} and {@code BehaviorProcessor} classes only. + * <p> + * This {@code BehaviorProcessor} supports the standard state-peeking methods {@link #hasComplete()}, {@link #hasThrowable()}, + * {@link #getThrowable()} and {@link #hasSubscribers()} as well as means to read the latest observed value + * in a non-blocking and thread-safe manner via {@link #hasValue()} or {@link #getValue()}. + * <p> + * Note that this processor signals {@code MissingBackpressureException} if a particular {@code Subscriber} is not + * ready to receive {@code onNext} events. To avoid this exception being signaled, use {@link #offer(Object)} to only + * try to emit an item when all {@code Subscriber}s have requested item(s). + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The {@code BehaviorProcessor} does not coordinate requests of its downstream {@code Subscriber}s and + * expects each individual {@code Subscriber} is ready to receive {@code onNext} items when {@link #onNext(Object)} + * is called. If a {@code Subscriber} is not ready, a {@code MissingBackpressureException} is signalled to it. + * To avoid overflowing the current {@code Subscriber}s, the conditional {@link #offer(Object)} method is available + * that returns true if any of the {@code Subscriber}s is not ready to receive {@code onNext} events. If + * there are no {@code Subscriber}s to the processor, {@code offer()} always succeeds. + * If the {@code BehaviorProcessor} is (optionally) subscribed to another {@code Publisher}, this upstream + * {@code Publisher} is consumed in an unbounded fashion (requesting {@link Long#MAX_VALUE}).</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code BehaviorProcessor} does not operate by default on a particular {@link io.reactivex.rxjava3.core.Scheduler} and + * the {@code Subscriber}s get notified on the thread the respective {@code onXXX} methods were invoked.</dd> + * <dt><b>Error handling:</b></dt> + * <dd>When the {@link #onError(Throwable)} is called, the {@code BehaviorProcessor} enters into a terminal state + * and emits the same {@code Throwable} instance to the last set of {@code Subscriber}s. During this emission, + * if one or more {@code Subscriber}s cancel their respective {@code Subscription}s, the + * {@code Throwable} is delivered to the global error handler via + * {@link io.reactivex.rxjava3.plugins.RxJavaPlugins#onError(Throwable)} (multiple times if multiple {@code Subscriber}s + * cancel at once). + * If there were no {@code Subscriber}s subscribed to this {@code BehaviorProcessor} when the {@code onError()} + * was called, the global error handler is not invoked. + * </dd> + * </dl> + * <p> + * Example usage: + * <pre> {@code + + // subscriber will receive all events. + BehaviorProcessor<Object> processor = BehaviorProcessor.create("default"); + processor.subscribe(subscriber); + processor.onNext("one"); + processor.onNext("two"); + processor.onNext("three"); + + // subscriber will receive the "one", "two" and "three" events, but not "zero" + BehaviorProcessor<Object> processor = BehaviorProcessor.create("default"); + processor.onNext("zero"); + processor.onNext("one"); + processor.subscribe(subscriber); + processor.onNext("two"); + processor.onNext("three"); + + // subscriber will receive only onComplete + BehaviorProcessor<Object> processor = BehaviorProcessor.create("default"); + processor.onNext("zero"); + processor.onNext("one"); + processor.onComplete(); + processor.subscribe(subscriber); + + // subscriber will receive only onError + BehaviorProcessor<Object> processor = BehaviorProcessor.create("default"); + processor.onNext("zero"); + processor.onNext("one"); + processor.onError(new RuntimeException("error")); + processor.subscribe(subscriber); + } </pre> + * + * @param <T> + * the type of item expected to be observed and emitted by the Processor + */ +public final class BehaviorProcessor<@NonNull T> extends FlowableProcessor<T> { + final AtomicReference<BehaviorSubscription<T>[]> subscribers; + + static final Object[] EMPTY_ARRAY = new Object[0]; + + @SuppressWarnings("rawtypes") + static final BehaviorSubscription[] EMPTY = new BehaviorSubscription[0]; + + @SuppressWarnings("rawtypes") + static final BehaviorSubscription[] TERMINATED = new BehaviorSubscription[0]; + + final ReadWriteLock lock; + final Lock readLock; + final Lock writeLock; + + final AtomicReference<Object> value; + + final AtomicReference<Throwable> terminalEvent; + + long index; + + /** + * Creates a {@link BehaviorProcessor} without a default item. + * + * @param <T> + * the type of item the BehaviorProcessor will emit + * @return the constructed {@link BehaviorProcessor} + */ + @CheckReturnValue + @NonNull + public static <T> BehaviorProcessor<T> create() { + return new BehaviorProcessor<>(); + } + + /** + * Creates a {@link BehaviorProcessor} that emits the last item it observed and all subsequent items to each + * {@link Subscriber} that subscribes to it. + * + * @param <T> + * the type of item the BehaviorProcessor will emit + * @param defaultValue + * the item that will be emitted first to any {@link Subscriber} as long as the + * {@link BehaviorProcessor} has not yet observed any items from its source {@code Observable} + * @return the constructed {@link BehaviorProcessor} + * @throws NullPointerException if {@code defaultValue} is {@code null} + */ + @CheckReturnValue + @NonNull + public static <@NonNull T> BehaviorProcessor<T> createDefault(T defaultValue) { + Objects.requireNonNull(defaultValue, "defaultValue is null"); + return new BehaviorProcessor<>(defaultValue); + } + + /** + * Constructs an empty BehaviorProcessor. + * @since 2.0 + */ + @SuppressWarnings("unchecked") + BehaviorProcessor() { + this.value = new AtomicReference<>(); + this.lock = new ReentrantReadWriteLock(); + this.readLock = lock.readLock(); + this.writeLock = lock.writeLock(); + this.subscribers = new AtomicReference<>(EMPTY); + this.terminalEvent = new AtomicReference<>(); + } + + /** + * Constructs a BehaviorProcessor with the given initial value. + * @param defaultValue the initial value, not null (verified) + * @throws NullPointerException if {@code defaultValue} is {@code null} + * @since 2.0 + */ + BehaviorProcessor(T defaultValue) { + this(); + this.value.lazySet(defaultValue); + } + + @Override + protected void subscribeActual(@NonNull Subscriber<? super T> s) { + BehaviorSubscription<T> bs = new BehaviorSubscription<>(s, this); + s.onSubscribe(bs); + if (add(bs)) { + if (bs.cancelled) { + remove(bs); + } else { + bs.emitFirst(); + } + } else { + Throwable ex = terminalEvent.get(); + if (ex == ExceptionHelper.TERMINATED) { + s.onComplete(); + } else { + s.onError(ex); + } + } + } + + @Override + public void onSubscribe(@NonNull Subscription s) { + if (terminalEvent.get() != null) { + s.cancel(); + return; + } + s.request(Long.MAX_VALUE); + } + + @Override + public void onNext(@NonNull T t) { + ExceptionHelper.nullCheck(t, "onNext called with a null value."); + + if (terminalEvent.get() != null) { + return; + } + Object o = NotificationLite.next(t); + setCurrent(o); + for (BehaviorSubscription<T> bs : subscribers.get()) { + bs.emitNext(o, index); + } + } + + @Override + public void onError(@NonNull Throwable t) { + ExceptionHelper.nullCheck(t, "onError called with a null Throwable."); + if (!terminalEvent.compareAndSet(null, t)) { + RxJavaPlugins.onError(t); + return; + } + Object o = NotificationLite.error(t); + for (BehaviorSubscription<T> bs : terminate(o)) { + bs.emitNext(o, index); + } + } + + @Override + public void onComplete() { + if (!terminalEvent.compareAndSet(null, ExceptionHelper.TERMINATED)) { + return; + } + Object o = NotificationLite.complete(); + for (BehaviorSubscription<T> bs : terminate(o)) { + bs.emitNext(o, index); // relaxed read okay since this is the only mutator thread + } + } + + /** + * Tries to emit the item to all currently subscribed {@link Subscriber}s if all of them + * has requested some value, returns {@code false} otherwise. + * <p> + * This method should be called in a sequential manner just like the {@code onXXX} methods + * of this {@code BehaviorProcessor}. + * <p>History: 2.0.8 - experimental + * @param t the item to emit, not {@code null} + * @return {@code true} if the item was emitted to all {@code Subscriber}s + * @throws NullPointerException if {@code t} is {@code null} + * @since 2.2 + */ + @CheckReturnValue + public boolean offer(@NonNull T t) { + ExceptionHelper.nullCheck(t, "offer called with a null value."); + + BehaviorSubscription<T>[] array = subscribers.get(); + + for (BehaviorSubscription<T> s : array) { + if (s.isFull()) { + return false; + } + } + + Object o = NotificationLite.next(t); + setCurrent(o); + for (BehaviorSubscription<T> bs : array) { + bs.emitNext(o, index); + } + return true; + } + + @Override + @CheckReturnValue + public boolean hasSubscribers() { + return subscribers.get().length != 0; + } + + @CheckReturnValue + /* test support*/ int subscriberCount() { + return subscribers.get().length; + } + + @Override + @Nullable + @CheckReturnValue + public Throwable getThrowable() { + Object o = value.get(); + if (NotificationLite.isError(o)) { + return NotificationLite.getError(o); + } + return null; + } + + /** + * Returns a single value the BehaviorProcessor currently has or null if no such value exists. + * <p>The method is thread-safe. + * @return a single value the BehaviorProcessor currently has or null if no such value exists + */ + @Nullable + @CheckReturnValue + public T getValue() { + Object o = value.get(); + if (NotificationLite.isComplete(o) || NotificationLite.isError(o)) { + return null; + } + return NotificationLite.getValue(o); + } + + @Override + @CheckReturnValue + public boolean hasComplete() { + Object o = value.get(); + return NotificationLite.isComplete(o); + } + + @Override + @CheckReturnValue + public boolean hasThrowable() { + Object o = value.get(); + return NotificationLite.isError(o); + } + + /** + * Returns true if the BehaviorProcessor has any value. + * <p>The method is thread-safe. + * @return true if the BehaviorProcessor has any value + */ + @CheckReturnValue + public boolean hasValue() { + Object o = value.get(); + return o != null && !NotificationLite.isComplete(o) && !NotificationLite.isError(o); + } + + boolean add(BehaviorSubscription<T> rs) { + for (;;) { + BehaviorSubscription<T>[] a = subscribers.get(); + if (a == TERMINATED) { + return false; + } + int len = a.length; + @SuppressWarnings("unchecked") + BehaviorSubscription<T>[] b = new BehaviorSubscription[len + 1]; + System.arraycopy(a, 0, b, 0, len); + b[len] = rs; + if (subscribers.compareAndSet(a, b)) { + return true; + } + } + } + + @SuppressWarnings("unchecked") + void remove(BehaviorSubscription<T> rs) { + for (;;) { + BehaviorSubscription<T>[] a = subscribers.get(); + int len = a.length; + if (len == 0) { + return; + } + int j = -1; + for (int i = 0; i < len; i++) { + if (a[i] == rs) { + j = i; + break; + } + } + + if (j < 0) { + return; + } + BehaviorSubscription<T>[] b; + if (len == 1) { + b = EMPTY; + } else { + b = new BehaviorSubscription[len - 1]; + System.arraycopy(a, 0, b, 0, j); + System.arraycopy(a, j + 1, b, j, len - j - 1); + } + if (subscribers.compareAndSet(a, b)) { + return; + } + } + } + + @SuppressWarnings("unchecked") + BehaviorSubscription<T>[] terminate(Object terminalValue) { + + setCurrent(terminalValue); + + return subscribers.getAndSet(TERMINATED); + } + + void setCurrent(Object o) { + Lock wl = writeLock; + wl.lock(); + index++; + value.lazySet(o); + wl.unlock(); + } + + static final class BehaviorSubscription<@NonNull T> extends AtomicLong implements Subscription, NonThrowingPredicate<Object> { + + private static final long serialVersionUID = 3293175281126227086L; + + final Subscriber<? super T> downstream; + final BehaviorProcessor<T> state; + + boolean next; + boolean emitting; + AppendOnlyLinkedArrayList<Object> queue; + + boolean fastPath; + + volatile boolean cancelled; + + long index; + + BehaviorSubscription(Subscriber<? super T> actual, BehaviorProcessor<T> state) { + this.downstream = actual; + this.state = state; + } + + @Override + public void request(long n) { + if (SubscriptionHelper.validate(n)) { + BackpressureHelper.add(this, n); + } + } + + @Override + public void cancel() { + if (!cancelled) { + cancelled = true; + + state.remove(this); + } + } + + void emitFirst() { + if (cancelled) { + return; + } + Object o; + synchronized (this) { + if (cancelled) { + return; + } + if (next) { + return; + } + + BehaviorProcessor<T> s = state; + + Lock readLock = s.readLock; + readLock.lock(); + index = s.index; + o = s.value.get(); + readLock.unlock(); + + emitting = o != null; + next = true; + } + + if (o != null) { + if (test(o)) { + return; + } + + emitLoop(); + } + } + + void emitNext(Object value, long stateIndex) { + if (cancelled) { + return; + } + if (!fastPath) { + synchronized (this) { + if (cancelled) { + return; + } + if (index == stateIndex) { + return; + } + if (emitting) { + AppendOnlyLinkedArrayList<Object> q = queue; + if (q == null) { + q = new AppendOnlyLinkedArrayList<>(4); + queue = q; + } + q.add(value); + return; + } + next = true; + } + fastPath = true; + } + + test(value); + } + + @Override + public boolean test(Object o) { + if (cancelled) { + return true; + } + + if (NotificationLite.isComplete(o)) { + downstream.onComplete(); + return true; + } else + if (NotificationLite.isError(o)) { + downstream.onError(NotificationLite.getError(o)); + return true; + } + + long r = get(); + if (r != 0L) { + downstream.onNext(NotificationLite.<T>getValue(o)); + if (r != Long.MAX_VALUE) { + decrementAndGet(); + } + return false; + } + cancel(); + downstream.onError(MissingBackpressureException.createDefault()); + return true; + } + + void emitLoop() { + for (;;) { + if (cancelled) { + return; + } + AppendOnlyLinkedArrayList<Object> q; + synchronized (this) { + q = queue; + if (q == null) { + emitting = false; + return; + } + queue = null; + } + + q.forEachWhile(this); + } + } + + public boolean isFull() { + return get() == 0L; + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/processors/FlowableProcessor.java b/src/main/java/io/reactivex/rxjava3/processors/FlowableProcessor.java new file mode 100644 index 0000000000..2bfbb33bd0 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/processors/FlowableProcessor.java @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.processors; + +import org.reactivestreams.Processor; + +import io.reactivex.rxjava3.annotations.*; +import io.reactivex.rxjava3.core.*; + +/** + * Represents a Subscriber and a Flowable (Publisher) at the same time, allowing + * multicasting events from a single source to multiple child Subscribers. + * <p>All methods except the onSubscribe, onNext, onError and onComplete are thread-safe. + * Use {@link #toSerialized()} to make these methods thread-safe as well. + * + * @param <T> the item value type + */ +public abstract class FlowableProcessor<@NonNull T> extends Flowable<T> implements Processor<T, T>, FlowableSubscriber<T> { + + /** + * Returns true if the FlowableProcessor has subscribers. + * <p>The method is thread-safe. + * @return true if the FlowableProcessor has subscribers + */ + @CheckReturnValue + public abstract boolean hasSubscribers(); + + /** + * Returns true if the FlowableProcessor has reached a terminal state through an error event. + * <p>The method is thread-safe. + * @return true if the FlowableProcessor has reached a terminal state through an error event + * @see #getThrowable() + * @see #hasComplete() + */ + @CheckReturnValue + public abstract boolean hasThrowable(); + + /** + * Returns true if the FlowableProcessor has reached a terminal state through a complete event. + * <p>The method is thread-safe. + * @return true if the FlowableProcessor has reached a terminal state through a complete event + * @see #hasThrowable() + */ + @CheckReturnValue + public abstract boolean hasComplete(); + + /** + * Returns the error that caused the FlowableProcessor to terminate or null if the FlowableProcessor + * hasn't terminated yet. + * <p>The method is thread-safe. + * @return the error that caused the FlowableProcessor to terminate or null if the FlowableProcessor + * hasn't terminated yet + */ + @Nullable + @CheckReturnValue + public abstract Throwable getThrowable(); + + /** + * Wraps this FlowableProcessor and serializes the calls to the onSubscribe, onNext, onError and + * onComplete methods, making them thread-safe. + * <p>The method is thread-safe. + * @return the wrapped and serialized FlowableProcessor + */ + @NonNull + @CheckReturnValue + public final FlowableProcessor<T> toSerialized() { + if (this instanceof SerializedProcessor) { + return this; + } + return new SerializedProcessor<>(this); + } +} diff --git a/src/main/java/io/reactivex/rxjava3/processors/MulticastProcessor.java b/src/main/java/io/reactivex/rxjava3/processors/MulticastProcessor.java new file mode 100644 index 0000000000..14a7a55ee2 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/processors/MulticastProcessor.java @@ -0,0 +1,637 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.processors; + +import java.util.concurrent.atomic.*; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.annotations.*; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.internal.functions.ObjectHelper; +import io.reactivex.rxjava3.internal.subscriptions.*; +import io.reactivex.rxjava3.internal.util.*; +import io.reactivex.rxjava3.operators.*; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +/** + * A {@link FlowableProcessor} implementation that coordinates downstream requests through + * a front-buffer and stable-prefetching, optionally canceling the upstream if all + * subscribers have cancelled. + * <p> + * <img width="640" height="360" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/MulticastProcessor.png" alt=""> + * <p> + * This processor does not have a public constructor by design; a new empty instance of this + * {@code MulticastProcessor} can be created via the following {@code create} methods that + * allow configuring it: + * <ul> + * <li>{@link #create()}: create an empty {@code MulticastProcessor} with + * {@link io.reactivex.rxjava3.core.Flowable#bufferSize() Flowable.bufferSize()} prefetch amount + * and no reference counting behavior.</li> + * <li>{@link #create(int)}: create an empty {@code MulticastProcessor} with + * the given prefetch amount and no reference counting behavior.</li> + * <li>{@link #create(boolean)}: create an empty {@code MulticastProcessor} with + * {@link io.reactivex.rxjava3.core.Flowable#bufferSize() Flowable.bufferSize()} prefetch amount + * and an optional reference counting behavior.</li> + * <li>{@link #create(int, boolean)}: create an empty {@code MulticastProcessor} with + * the given prefetch amount and an optional reference counting behavior.</li> + * </ul> + * <p> + * When the reference counting behavior is enabled, the {@code MulticastProcessor} cancels its + * upstream when all {@link Subscriber}s have cancelled. Late {@code Subscriber}s will then be + * immediately completed. + * <p> + * Because {@code MulticastProcessor} implements the {@link Subscriber} interface, calling + * {@code onSubscribe} is mandatory (<a href="https://github.com/reactive-streams/reactive-streams-jvm#2.12">Rule 2.12</a>). + * If {@code MulticastProcessor} should run standalone, i.e., without subscribing the {@code MulticastProcessor} to another {@link Publisher}, + * use {@link #start()} or {@link #startUnbounded()} methods to initialize the internal buffer. + * Failing to do so will lead to a {@link NullPointerException} at runtime. + * <p> + * Use {@link #offer(Object)} to try and offer/emit items but don't fail if the + * internal buffer is full. + * <p> + * A {@code MulticastProcessor} is a {@link Processor} type in the Reactive Streams specification, + * {@code null}s are not allowed (<a href="https://github.com/reactive-streams/reactive-streams-jvm#2.13">Rule 2.13</a>) as + * parameters to {@link #onSubscribe(Subscription)}, {@link #offer(Object)}, {@link #onNext(Object)} and {@link #onError(Throwable)}. + * Such calls will result in a {@link NullPointerException} being thrown and the processor's state is not changed. + * <p> + * Since a {@code MulticastProcessor} is a {@link io.reactivex.rxjava3.core.Flowable}, it supports backpressure. + * The backpressure from the currently subscribed {@link Subscriber}s are coordinated by emitting upstream + * items only if all of those {@code Subscriber}s have requested at least one item. This behavior + * is also called <em>lockstep-mode</em> because even if some {@code Subscriber}s can take any number + * of items, other {@code Subscriber}s requesting less or infrequently will slow down the overall + * throughput of the flow. + * <p> + * Calling {@link #onNext(Object)}, {@link #offer(Object)}, {@link #onError(Throwable)} and {@link #onComplete()} + * is required to be serialized (called from the same thread or called non-overlappingly from different threads + * through external means of serialization). The {@link #toSerialized()} method available to all {@link FlowableProcessor}s + * provides such serialization and also protects against reentrance (i.e., when a downstream {@code Subscriber} + * consuming this processor also wants to call {@link #onNext(Object)} on this processor recursively). + * <p> + * This {@code MulticastProcessor} supports the standard state-peeking methods {@link #hasComplete()}, {@link #hasThrowable()}, + * {@link #getThrowable()} and {@link #hasSubscribers()}. This processor doesn't allow peeking into its buffer. + * <p> + * When this {@code MulticastProcessor} is terminated via {@link #onError(Throwable)} or {@link #onComplete()}, + * all previously signaled but not yet consumed items will be still available to {@code Subscriber}s and the respective + * terminal even is only emitted when all previous items have been successfully delivered to {@code Subscriber}s. + * If there are no {@code Subscriber}s, the remaining items will be buffered indefinitely. + * <p> + * The {@code MulticastProcessor} does not support clearing its cached events (to appear empty again). + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The backpressure from the currently subscribed {@code Subscriber}s are coordinated by emitting upstream + * items only if all of those {@code Subscriber}s have requested at least one item. This behavior + * is also called <em>lockstep-mode</em> because even if some {@code Subscriber}s can take any number + * of items, other {@code Subscriber}s requesting less or infrequently will slow down the overall + * throughput of the flow.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code MulticastProcessor} does not operate by default on a particular {@link io.reactivex.rxjava3.core.Scheduler} and + * the {@code Subscriber}s get notified on an arbitrary thread in a serialized fashion.</dd> + * </dl> + * <p> + * Example: + * <pre><code> + MulticastProcessor<Integer> mp = Flowable.range(1, 10) + .subscribeWith(MulticastProcessor.create()); + + mp.test().assertResult(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + + // -------------------- + + MulticastProcessor<Integer> mp2 = MulticastProcessor.create(4); + mp2.start(); + + assertTrue(mp2.offer(1)); + assertTrue(mp2.offer(2)); + assertTrue(mp2.offer(3)); + assertTrue(mp2.offer(4)); + + assertFalse(mp2.offer(5)); + + mp2.onComplete(); + + mp2.test().assertResult(1, 2, 3, 4); + * </code></pre> + * <p>History: 2.1.14 - experimental + * @param <T> the input and output value type + * @since 2.2 + */ +@BackpressureSupport(BackpressureKind.FULL) +@SchedulerSupport(SchedulerSupport.NONE) +public final class MulticastProcessor<@NonNull T> extends FlowableProcessor<T> { + + final AtomicInteger wip; + + final AtomicReference<Subscription> upstream; + + final AtomicReference<MulticastSubscription<T>[]> subscribers; + + final int bufferSize; + + final int limit; + + final boolean refcount; + + volatile SimpleQueue<T> queue; + + volatile boolean done; + volatile Throwable error; + + int consumed; + + int fusionMode; + + @SuppressWarnings("rawtypes") + static final MulticastSubscription[] EMPTY = new MulticastSubscription[0]; + + @SuppressWarnings("rawtypes") + static final MulticastSubscription[] TERMINATED = new MulticastSubscription[0]; + + /** + * Constructs a fresh instance with the default Flowable.bufferSize() prefetch + * amount and no refCount-behavior. + * @param <T> the input and output value type + * @return the new MulticastProcessor instance + */ + @CheckReturnValue + @NonNull + public static <T> MulticastProcessor<T> create() { + return new MulticastProcessor<>(bufferSize(), false); + } + + /** + * Constructs a fresh instance with the default Flowable.bufferSize() prefetch + * amount and the optional refCount-behavior. + * @param <T> the input and output value type + * @param refCount if true and if all Subscribers have canceled, the upstream + * is cancelled + * @return the new MulticastProcessor instance + */ + @CheckReturnValue + @NonNull + public static <T> MulticastProcessor<T> create(boolean refCount) { + return new MulticastProcessor<>(bufferSize(), refCount); + } + + /** + * Constructs a fresh instance with the given prefetch amount and no refCount behavior. + * @param bufferSize the prefetch amount + * @param <T> the input and output value type + * @return the new MulticastProcessor instance + * @throws IllegalArgumentException if {@code bufferSize} is non-positive + */ + @CheckReturnValue + @NonNull + public static <T> MulticastProcessor<T> create(int bufferSize) { + ObjectHelper.verifyPositive(bufferSize, "bufferSize"); + return new MulticastProcessor<>(bufferSize, false); + } + + /** + * Constructs a fresh instance with the given prefetch amount and the optional + * refCount-behavior. + * @param bufferSize the prefetch amount + * @param refCount if true and if all Subscribers have canceled, the upstream + * is cancelled + * @param <T> the input and output value type + * @return the new MulticastProcessor instance + * @throws IllegalArgumentException if {@code bufferSize} is non-positive + */ + @CheckReturnValue + @NonNull + public static <T> MulticastProcessor<T> create(int bufferSize, boolean refCount) { + ObjectHelper.verifyPositive(bufferSize, "bufferSize"); + return new MulticastProcessor<>(bufferSize, refCount); + } + + /** + * Constructs a fresh instance with the given prefetch amount and the optional + * refCount-behavior. + * @param bufferSize the prefetch amount + * @param refCount if true and if all Subscribers have canceled, the upstream + * is cancelled + */ + @SuppressWarnings("unchecked") + MulticastProcessor(int bufferSize, boolean refCount) { + this.bufferSize = bufferSize; + this.limit = bufferSize - (bufferSize >> 2); + this.wip = new AtomicInteger(); + this.subscribers = new AtomicReference<>(EMPTY); + this.upstream = new AtomicReference<>(); + this.refcount = refCount; + } + + /** + * Initializes this Processor by setting an upstream Subscription that + * ignores request amounts, uses a fixed buffer + * and allows using the onXXX and offer methods + * afterwards. + */ + public void start() { + if (SubscriptionHelper.setOnce(upstream, EmptySubscription.INSTANCE)) { + queue = new SpscArrayQueue<>(bufferSize); + } + } + + /** + * Initializes this Processor by setting an upstream Subscription that + * ignores request amounts, uses an unbounded buffer + * and allows using the onXXX and offer methods + * afterwards. + */ + public void startUnbounded() { + if (SubscriptionHelper.setOnce(upstream, EmptySubscription.INSTANCE)) { + queue = new SpscLinkedArrayQueue<>(bufferSize); + } + } + + @Override + public void onSubscribe(@NonNull Subscription s) { + if (SubscriptionHelper.setOnce(upstream, s)) { + if (s instanceof QueueSubscription) { + @SuppressWarnings("unchecked") + QueueSubscription<T> qs = (QueueSubscription<T>)s; + + int m = qs.requestFusion(QueueSubscription.ANY); + if (m == QueueSubscription.SYNC) { + fusionMode = m; + queue = qs; + done = true; + drain(); + return; + } + if (m == QueueSubscription.ASYNC) { + fusionMode = m; + queue = qs; + + s.request(bufferSize); + return; + } + } + + queue = new SpscArrayQueue<>(bufferSize); + + s.request(bufferSize); + } + } + + @Override + public void onNext(@NonNull T t) { + if (done) { + return; + } + if (fusionMode == QueueSubscription.NONE) { + ExceptionHelper.nullCheck(t, "onNext called with a null value."); + if (!queue.offer(t)) { + SubscriptionHelper.cancel(upstream); + onError(MissingBackpressureException.createDefault()); + return; + } + } + drain(); + } + + /** + * Tries to offer an item into the internal queue and returns false + * if the queue is full. + * @param t the item to offer, not {@code null} + * @return true if successful, false if the queue is full + * @throws NullPointerException if {@code t} is {@code null} + * @throws IllegalStateException if the processor is in fusion mode + */ + @CheckReturnValue + public boolean offer(@NonNull T t) { + ExceptionHelper.nullCheck(t, "offer called with a null value."); + if (done) { + return false; + } + if (fusionMode == QueueSubscription.NONE) { + if (queue.offer(t)) { + drain(); + return true; + } + return false; + } + throw new IllegalStateException("offer() should not be called in fusion mode!"); + } + + @Override + public void onError(@NonNull Throwable t) { + ExceptionHelper.nullCheck(t, "onError called with a null Throwable."); + if (!done) { + error = t; + done = true; + drain(); + return; + } + RxJavaPlugins.onError(t); + } + + @Override + public void onComplete() { + done = true; + drain(); + } + + @Override + @CheckReturnValue + public boolean hasSubscribers() { + return subscribers.get().length != 0; + } + + @Override + @CheckReturnValue + public boolean hasThrowable() { + return done && error != null; + } + + @Override + @CheckReturnValue + public boolean hasComplete() { + return done && error == null; + } + + @Override + @CheckReturnValue + public Throwable getThrowable() { + return done ? error : null; + } + + @Override + protected void subscribeActual(@NonNull Subscriber<? super T> s) { + MulticastSubscription<T> ms = new MulticastSubscription<>(s, this); + s.onSubscribe(ms); + if (add(ms)) { + if (ms.get() == Long.MIN_VALUE) { + remove(ms); + } else { + drain(); + } + } else { + if (done) { + Throwable ex = error; + if (ex != null) { + s.onError(ex); + return; + } + } + s.onComplete(); + } + } + + boolean add(MulticastSubscription<T> inner) { + for (;;) { + MulticastSubscription<T>[] a = subscribers.get(); + if (a == TERMINATED) { + return false; + } + int n = a.length; + @SuppressWarnings("unchecked") + MulticastSubscription<T>[] b = new MulticastSubscription[n + 1]; + System.arraycopy(a, 0, b, 0, n); + b[n] = inner; + if (subscribers.compareAndSet(a, b)) { + return true; + } + } + } + + @SuppressWarnings("unchecked") + void remove(MulticastSubscription<T> inner) { + for (;;) { + MulticastSubscription<T>[] a = subscribers.get(); + int n = a.length; + if (n == 0) { + return; + } + + int j = -1; + for (int i = 0; i < n; i++) { + if (a[i] == inner) { + j = i; + break; + } + } + + if (j < 0) { + break; + } + + if (n == 1) { + if (refcount) { + if (subscribers.compareAndSet(a, TERMINATED)) { + SubscriptionHelper.cancel(upstream); + done = true; + break; + } + } else { + if (subscribers.compareAndSet(a, EMPTY)) { + break; + } + } + } else { + MulticastSubscription<T>[] b = new MulticastSubscription[n - 1]; + System.arraycopy(a, 0, b, 0, j); + System.arraycopy(a, j + 1, b, j, n - j - 1); + if (subscribers.compareAndSet(a, b)) { + break; + } + } + } + } + + @SuppressWarnings("unchecked") + void drain() { + if (wip.getAndIncrement() != 0) { + return; + } + + int missed = 1; + AtomicReference<MulticastSubscription<T>[]> subs = subscribers; + int c = consumed; + int lim = limit; + int fm = fusionMode; + + outer: + for (;;) { + + SimpleQueue<T> q = queue; + + if (q != null) { + MulticastSubscription<T>[] as = subs.get(); + int n = as.length; + + if (n != 0) { + long r = -1L; + + for (MulticastSubscription<T> a : as) { + long ra = a.get(); + if (ra >= 0L) { + if (r == -1L) { + r = ra - a.emitted; + } else { + r = Math.min(r, ra - a.emitted); + } + } + } + + while (r > 0L) { + MulticastSubscription<T>[] bs = subs.get(); + + if (bs == TERMINATED) { + q.clear(); + return; + } + + if (as != bs) { + continue outer; + } + + boolean d = done; + + T v; + + try { + v = q.poll(); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + SubscriptionHelper.cancel(upstream); + d = true; + v = null; + error = ex; + done = true; + } + boolean empty = v == null; + + if (d && empty) { + Throwable ex = error; + if (ex != null) { + for (MulticastSubscription<T> inner : subs.getAndSet(TERMINATED)) { + inner.onError(ex); + } + } else { + for (MulticastSubscription<T> inner : subs.getAndSet(TERMINATED)) { + inner.onComplete(); + } + } + return; + } + + if (empty) { + break; + } + + for (MulticastSubscription<T> inner : as) { + inner.onNext(v); + } + + r--; + + if (fm != QueueSubscription.SYNC) { + if (++c == lim) { + c = 0; + upstream.get().request(lim); + } + } + } + + if (r == 0) { + MulticastSubscription<T>[] bs = subs.get(); + + if (bs == TERMINATED) { + q.clear(); + return; + } + + if (as != bs) { + continue; + } + + if (done && q.isEmpty()) { + Throwable ex = error; + if (ex != null) { + for (MulticastSubscription<T> inner : subs.getAndSet(TERMINATED)) { + inner.onError(ex); + } + } else { + for (MulticastSubscription<T> inner : subs.getAndSet(TERMINATED)) { + inner.onComplete(); + } + } + return; + } + } + } + } + + consumed = c; + missed = wip.addAndGet(-missed); + if (missed == 0) { + break; + } + } + } + + static final class MulticastSubscription<@NonNull T> extends AtomicLong implements Subscription { + + private static final long serialVersionUID = -363282618957264509L; + + final Subscriber<? super T> downstream; + + final MulticastProcessor<T> parent; + + long emitted; + + MulticastSubscription(Subscriber<? super T> actual, MulticastProcessor<T> parent) { + this.downstream = actual; + this.parent = parent; + } + + @Override + public void request(long n) { + if (SubscriptionHelper.validate(n)) { + long r = BackpressureHelper.addCancel(this, n); + if (r != Long.MIN_VALUE && r != Long.MAX_VALUE) { + parent.drain(); + } + } + } + + @Override + public void cancel() { + if (getAndSet(Long.MIN_VALUE) != Long.MIN_VALUE) { + parent.remove(this); + } + } + + void onNext(T t) { + if (get() != Long.MIN_VALUE) { + emitted++; + downstream.onNext(t); + } + } + + void onError(Throwable t) { + if (get() != Long.MIN_VALUE) { + downstream.onError(t); + } + } + + void onComplete() { + if (get() != Long.MIN_VALUE) { + downstream.onComplete(); + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/processors/PublishProcessor.java b/src/main/java/io/reactivex/rxjava3/processors/PublishProcessor.java new file mode 100644 index 0000000000..73507844d2 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/processors/PublishProcessor.java @@ -0,0 +1,405 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.processors; + +import java.util.concurrent.atomic.*; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.annotations.*; +import io.reactivex.rxjava3.exceptions.MissingBackpressureException; +import io.reactivex.rxjava3.internal.subscriptions.SubscriptionHelper; +import io.reactivex.rxjava3.internal.util.*; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +/** + * Processor that multicasts all subsequently observed items to its current {@link Subscriber}s. + * + * <p> + * <img width="640" height="278" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/PublishProcessor.png" alt=""> + * <p> + * This processor does not have a public constructor by design; a new empty instance of this + * {@code PublishProcessor} can be created via the {@link #create()} method. + * <p> + * Since a {@code PublishProcessor} is a Reactive Streams {@code Processor} type, + * {@code null}s are not allowed (<a href="https://github.com/reactive-streams/reactive-streams-jvm#2.13">Rule 2.13</a>) as + * parameters to {@link #onNext(Object)} and {@link #onError(Throwable)}. Such calls will result in a + * {@link NullPointerException} being thrown and the processor's state is not changed. + * <p> + * {@code PublishProcessor} is a {@link io.reactivex.rxjava3.core.Flowable} as well as a {@link FlowableProcessor}, + * however, it does not coordinate backpressure between different subscribers and between an + * upstream source and a subscriber. If an upstream item is received via {@link #onNext(Object)}, if + * a subscriber is not ready to receive an item, that subscriber is terminated via a {@link MissingBackpressureException}. + * To avoid this case, use {@link #offer(Object)} and retry sometime later if it returned false. + * The {@code PublishProcessor}'s {@link Subscriber}-side consumes items in an unbounded manner. + * <p> + * For a multicasting processor type that also coordinates between the downstream {@code Subscriber}s and the upstream + * source as well, consider using {@link MulticastProcessor}. + * <p> + * When this {@code PublishProcessor} is terminated via {@link #onError(Throwable)} or {@link #onComplete()}, + * late {@link Subscriber}s only receive the respective terminal event. + * <p> + * Unlike a {@link BehaviorProcessor}, a {@code PublishProcessor} doesn't retain/cache items, therefore, a new + * {@code Subscriber} won't receive any past items. + * <p> + * Even though {@code PublishProcessor} implements the {@link Subscriber} interface, calling + * {@code onSubscribe} is not required (<a href="https://github.com/reactive-streams/reactive-streams-jvm#2.12">Rule 2.12</a>) + * if the processor is used as a standalone source. However, calling {@code onSubscribe} + * after the {@code PublishProcessor} reached its terminal state will result in the + * given {@link Subscription} being canceled immediately. + * <p> + * Calling {@link #onNext(Object)}, {@link #offer(Object)}, {@link #onError(Throwable)} and {@link #onComplete()} + * is required to be serialized (called from the same thread or called non-overlappingly from different threads + * through external means of serialization). The {@link #toSerialized()} method available to all {@link FlowableProcessor}s + * provides such serialization and also protects against reentrance (i.e., when a downstream {@code Subscriber} + * consuming this processor also wants to call {@link #onNext(Object)} on this processor recursively). + * Note that serializing over {@link #offer(Object)} is not supported through {@code toSerialized()} because it is a method + * available on the {@code PublishProcessor} and {@code BehaviorProcessor} classes only. + * <p> + * This {@code PublishProcessor} supports the standard state-peeking methods {@link #hasComplete()}, {@link #hasThrowable()}, + * {@link #getThrowable()} and {@link #hasSubscribers()}. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>The processor does not coordinate backpressure for its subscribers and implements a weaker {@code onSubscribe} which + * calls requests {@link Long#MAX_VALUE} from the incoming Subscriptions. This makes it possible to subscribe the {@code PublishProcessor} + * to multiple sources (note on serialization though) unlike the standard {@code Subscriber} contract. Child subscribers, however, are not overflown but receive an + * {@link IllegalStateException} in case their requested amount is zero.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code PublishProcessor} does not operate by default on a particular {@link io.reactivex.rxjava3.core.Scheduler} and + * the {@code Subscriber}s get notified on the thread the respective {@code onXXX} methods were invoked.</dd> + * <dt><b>Error handling:</b></dt> + * <dd>When the {@link #onError(Throwable)} is called, the {@code PublishProcessor} enters into a terminal state + * and emits the same {@code Throwable} instance to the last set of {@code Subscriber}s. During this emission, + * if one or more {@code Subscriber}s cancel their respective {@code Subscription}s, the + * {@code Throwable} is delivered to the global error handler via + * {@link io.reactivex.rxjava3.plugins.RxJavaPlugins#onError(Throwable)} (multiple times if multiple {@code Subscriber}s + * cancel at once). + * If there were no {@code Subscriber}s subscribed to this {@code PublishProcessor} when the {@code onError()} + * was called, the global error handler is not invoked. + * </dd> + * </dl> + * + * Example usage: + * <pre> {@code + + PublishProcessor<Object> processor = PublishProcessor.create(); + // subscriber1 will receive all onNext and onComplete events + processor.subscribe(subscriber1); + processor.onNext("one"); + processor.onNext("two"); + // subscriber2 will only receive "three" and onComplete + processor.subscribe(subscriber2); + processor.onNext("three"); + processor.onComplete(); + + } </pre> + * @param <T> the value type multicasted to Subscribers. + * @see MulticastProcessor + */ +public final class PublishProcessor<@NonNull T> extends FlowableProcessor<T> { + /** The terminated indicator for the subscribers array. */ + @SuppressWarnings("rawtypes") + static final PublishSubscription[] TERMINATED = new PublishSubscription[0]; + /** An empty subscribers array to avoid allocating it all the time. */ + @SuppressWarnings("rawtypes") + static final PublishSubscription[] EMPTY = new PublishSubscription[0]; + + /** The array of currently subscribed subscribers. */ + final AtomicReference<PublishSubscription<T>[]> subscribers; + + /** The error, write before terminating and read after checking subscribers. */ + Throwable error; + + /** + * Constructs a PublishProcessor. + * @param <T> the value type + * @return the new PublishProcessor + */ + @CheckReturnValue + @NonNull + public static <T> PublishProcessor<T> create() { + return new PublishProcessor<>(); + } + + /** + * Constructs a PublishProcessor. + * @since 2.0 + */ + @SuppressWarnings("unchecked") + PublishProcessor() { + subscribers = new AtomicReference<>(EMPTY); + } + + @Override + protected void subscribeActual(@NonNull Subscriber<? super T> t) { + PublishSubscription<T> ps = new PublishSubscription<>(t, this); + t.onSubscribe(ps); + if (add(ps)) { + // if cancellation happened while a successful add, the remove() didn't work + // so we need to do it again + if (ps.isCancelled()) { + remove(ps); + } + } else { + Throwable ex = error; + if (ex != null) { + t.onError(ex); + } else { + t.onComplete(); + } + } + } + + /** + * Tries to add the given subscriber to the subscribers array atomically + * or returns false if this processor has terminated. + * @param ps the subscriber to add + * @return true if successful, false if this processor has terminated + */ + boolean add(PublishSubscription<T> ps) { + for (;;) { + PublishSubscription<T>[] a = subscribers.get(); + if (a == TERMINATED) { + return false; + } + + int n = a.length; + @SuppressWarnings("unchecked") + PublishSubscription<T>[] b = new PublishSubscription[n + 1]; + System.arraycopy(a, 0, b, 0, n); + b[n] = ps; + + if (subscribers.compareAndSet(a, b)) { + return true; + } + } + } + + /** + * Atomically removes the given subscriber if it is subscribed to this processor. + * @param ps the subscription wrapping a subscriber to remove + */ + @SuppressWarnings("unchecked") + void remove(PublishSubscription<T> ps) { + for (;;) { + PublishSubscription<T>[] a = subscribers.get(); + if (a == TERMINATED || a == EMPTY) { + return; + } + + int n = a.length; + int j = -1; + for (int i = 0; i < n; i++) { + if (a[i] == ps) { + j = i; + break; + } + } + + if (j < 0) { + return; + } + + PublishSubscription<T>[] b; + + if (n == 1) { + b = EMPTY; + } else { + b = new PublishSubscription[n - 1]; + System.arraycopy(a, 0, b, 0, j); + System.arraycopy(a, j + 1, b, j, n - j - 1); + } + if (subscribers.compareAndSet(a, b)) { + return; + } + } + } + + @Override + public void onSubscribe(@NonNull Subscription s) { + if (subscribers.get() == TERMINATED) { + s.cancel(); + return; + } + // PublishProcessor doesn't bother with request coordination. + s.request(Long.MAX_VALUE); + } + + @Override + public void onNext(@NonNull T t) { + ExceptionHelper.nullCheck(t, "onNext called with a null value."); + for (PublishSubscription<T> s : subscribers.get()) { + s.onNext(t); + } + } + + @SuppressWarnings("unchecked") + @Override + public void onError(@NonNull Throwable t) { + ExceptionHelper.nullCheck(t, "onError called with a null Throwable."); + if (subscribers.get() == TERMINATED) { + RxJavaPlugins.onError(t); + return; + } + error = t; + + for (PublishSubscription<T> s : subscribers.getAndSet(TERMINATED)) { + s.onError(t); + } + } + + @SuppressWarnings("unchecked") + @Override + public void onComplete() { + if (subscribers.get() == TERMINATED) { + return; + } + for (PublishSubscription<T> s : subscribers.getAndSet(TERMINATED)) { + s.onComplete(); + } + } + + /** + * Tries to emit the item to all currently subscribed {@link Subscriber}s if all of them + * has requested some value, returns {@code false} otherwise. + * <p> + * This method should be called in a sequential manner just like the {@code onXXX} methods + * of this {@code PublishProcessor}. + * <p>History: 2.0.8 - experimental + * @param t the item to emit, not {@code null} + * @return {@code true} if the item was emitted to all {@code Subscriber}s + * @throws NullPointerException if {@code t} is {@code null} + * @since 2.2 + */ + @CheckReturnValue + public boolean offer(@NonNull T t) { + ExceptionHelper.nullCheck(t, "offer called with a null value."); + + PublishSubscription<T>[] array = subscribers.get(); + + for (PublishSubscription<T> s : array) { + if (s.isFull()) { + return false; + } + } + + for (PublishSubscription<T> s : array) { + s.onNext(t); + } + return true; + } + + @Override + @CheckReturnValue + public boolean hasSubscribers() { + return subscribers.get().length != 0; + } + + @Override + @Nullable + @CheckReturnValue + public Throwable getThrowable() { + if (subscribers.get() == TERMINATED) { + return error; + } + return null; + } + + @Override + @CheckReturnValue + public boolean hasThrowable() { + return subscribers.get() == TERMINATED && error != null; + } + + @Override + @CheckReturnValue + public boolean hasComplete() { + return subscribers.get() == TERMINATED && error == null; + } + + /** + * Wraps the actual subscriber, tracks its requests and makes cancellation + * to remove itself from the current subscribers array. + * + * @param <T> the value type + */ + static final class PublishSubscription<@NonNull T> extends AtomicLong implements Subscription { + + private static final long serialVersionUID = 3562861878281475070L; + /** The actual subscriber. */ + final Subscriber<? super T> downstream; + /** The parent processor servicing this subscriber. */ + final PublishProcessor<T> parent; + + /** + * Constructs a PublishSubscriber, wraps the actual subscriber and the state. + * @param actual the actual subscriber + * @param parent the parent PublishProcessor + */ + PublishSubscription(Subscriber<? super T> actual, PublishProcessor<T> parent) { + this.downstream = actual; + this.parent = parent; + } + + public void onNext(T t) { + long r = get(); + if (r == Long.MIN_VALUE) { + return; + } + if (r != 0L) { + downstream.onNext(t); + BackpressureHelper.producedCancel(this, 1); + } else { + cancel(); + downstream.onError(MissingBackpressureException.createDefault()); + } + } + + public void onError(Throwable t) { + if (get() != Long.MIN_VALUE) { + downstream.onError(t); + } else { + RxJavaPlugins.onError(t); + } + } + + public void onComplete() { + if (get() != Long.MIN_VALUE) { + downstream.onComplete(); + } + } + + @Override + public void request(long n) { + if (SubscriptionHelper.validate(n)) { + BackpressureHelper.addCancel(this, n); + } + } + + @Override + public void cancel() { + if (getAndSet(Long.MIN_VALUE) != Long.MIN_VALUE) { + parent.remove(this); + } + } + + public boolean isCancelled() { + return get() == Long.MIN_VALUE; + } + + boolean isFull() { + return get() == 0L; + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/processors/ReplayProcessor.java b/src/main/java/io/reactivex/rxjava3/processors/ReplayProcessor.java new file mode 100644 index 0000000000..ae6a7d3960 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/processors/ReplayProcessor.java @@ -0,0 +1,1360 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.processors; + +import java.lang.reflect.Array; +import java.util.*; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.*; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.annotations.*; +import io.reactivex.rxjava3.core.Scheduler; +import io.reactivex.rxjava3.internal.functions.ObjectHelper; +import io.reactivex.rxjava3.internal.subscriptions.SubscriptionHelper; +import io.reactivex.rxjava3.internal.util.*; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +/** + * Replays events to Subscribers. + * <p> + * The {@code ReplayProcessor} supports the following item retention strategies: + * <ul> + * <li>{@link #create()} and {@link #create(int)}: retains and replays all events to current and + * future {@code Subscriber}s. + * <p> + * <img width="640" height="269" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/ReplayProcessor.u.png" alt=""> + * <p> + * <img width="640" height="345" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/ReplayProcessor.ue.png" alt=""> + * </li> + * <li>{@link #createWithSize(int)}: retains at most the given number of items and replays only these + * latest items to new {@code Subscriber}s. + * <p> + * <img width="640" height="332" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/ReplayProcessor.n.png" alt=""> + * </li> + * <li>{@link #createWithTime(long, TimeUnit, Scheduler)}: retains items no older than the specified time + * and replays them to new {@code Subscriber}s (which could mean all items age out). + * <p> + * <img width="640" height="415" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/ReplayProcessor.t.png" alt=""> + * </li> + * <li>{@link #createWithTimeAndSize(long, TimeUnit, Scheduler, int)}: retains no more than the given number of items + * which are also no older than the specified time and replays them to new {@code Subscriber}s (which could mean all items age out). + * <p> + * <img width="640" height="404" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/ReplayProcessor.nt.png" alt=""> + * </li> + * </ul> + * <p> + * The {@code ReplayProcessor} can be created in bounded and unbounded mode. It can be bounded by + * size (maximum number of elements retained at most) and/or time (maximum age of elements replayed). + * <p> + * Since a {@code ReplayProcessor} is a Reactive Streams {@code Processor}, + * {@code null}s are not allowed (<a href="https://github.com/reactive-streams/reactive-streams-jvm#2.13">Rule 2.13</a>) as + * parameters to {@link #onNext(Object)} and {@link #onError(Throwable)}. Such calls will result in a + * {@link NullPointerException} being thrown and the processor's state is not changed. + * <p> + * This {@code ReplayProcessor} respects the individual backpressure behavior of its {@code Subscriber}s but + * does not coordinate their request amounts towards the upstream (because there might not be any) and + * consumes the upstream in an unbounded manner (requesting {@link Long#MAX_VALUE}). + * Note that {@code Subscriber}s receive a continuous sequence of values after they subscribed even + * if an individual item gets delayed due to backpressure. + * Due to concurrency requirements, a size-bounded {@code ReplayProcessor} may hold strong references to more source + * emissions than specified. + * <p> + * When this {@code ReplayProcessor} is terminated via {@link #onError(Throwable)} or {@link #onComplete()}, + * late {@link Subscriber}s will receive the retained/cached items first (if any) followed by the respective + * terminal event. If the {@code ReplayProcessor} has a time-bound, the age of the retained/cached items are still considered + * when replaying and thus it may result in no items being emitted before the terminal event. + * <p> + * Once an {@code Subscriber} has subscribed, it will receive items continuously from that point on. Bounds only affect how + * many past items a new {@code Subscriber} will receive before it catches up with the live event feed. + * <p> + * Even though {@code ReplayProcessor} implements the {@code Subscriber} interface, calling + * {@code onSubscribe} is not required (<a href="https://github.com/reactive-streams/reactive-streams-jvm#2.12">Rule 2.12</a>) + * if the processor is used as a standalone source. However, calling {@code onSubscribe} + * after the {@code ReplayProcessor} reached its terminal state will result in the + * given {@code Subscription} being canceled immediately. + * <p> + * Calling {@link #onNext(Object)}, {@link #onError(Throwable)} and {@link #onComplete()} + * is required to be serialized (called from the same thread or called non-overlappingly from different threads + * through external means of serialization). The {@link #toSerialized()} method available to all {@code FlowableProcessor}s + * provides such serialization and also protects against reentrance (i.e., when a downstream {@code Subscriber} + * consuming this processor also wants to call {@link #onNext(Object)} on this processor recursively). + * <p> + * This {@code ReplayProcessor} supports the standard state-peeking methods {@link #hasComplete()}, {@link #hasThrowable()}, + * {@link #getThrowable()} and {@link #hasSubscribers()} as well as means to read the retained/cached items + * in a non-blocking and thread-safe manner via {@link #hasValue()}, {@link #getValue()}, + * {@link #getValues()} or {@link #getValues(Object[])}. + * <p> + * Note that due to concurrency requirements, a size- and time-bounded {@code ReplayProcessor} may hold strong references to more + * source emissions than specified while it isn't terminated yet. Use the {@link #cleanupBuffer()} to allow + * such inaccessible items to be cleaned up by GC once no consumer references them anymore. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>This {@code ReplayProcessor} respects the individual backpressure behavior of its {@code Subscriber}s but + * does not coordinate their request amounts towards the upstream (because there might not be any) and + * consumes the upstream in an unbounded manner (requesting {@link Long#MAX_VALUE}). + * Note that {@code Subscriber}s receive a continuous sequence of values after they subscribed even + * if an individual item gets delayed due to backpressure.</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code ReplayProcessor} does not operate by default on a particular {@link io.reactivex.rxjava3.core.Scheduler} and + * the {@code Subscriber}s get notified on the thread the respective {@code onXXX} methods were invoked. + * Time-bound {@code ReplayProcessor}s use the given {@code Scheduler} in their {@code create} methods + * as time source to timestamp of items received for the age checks.</dd> + * <dt><b>Error handling:</b></dt> + * <dd>When the {@link #onError(Throwable)} is called, the {@code ReplayProcessor} enters into a terminal state + * and emits the same {@code Throwable} instance to the last set of {@code Subscriber}s. During this emission, + * if one or more {@code Subscriber}s cancel their respective {@code Subscription}s, the + * {@code Throwable} is delivered to the global error handler via + * {@link io.reactivex.rxjava3.plugins.RxJavaPlugins#onError(Throwable)} (multiple times if multiple {@code Subscriber}s + * cancel at once). + * If there were no {@code Subscriber}s subscribed to this {@code ReplayProcessor} when the {@code onError()} + * was called, the global error handler is not invoked. + * </dd> + * </dl> + * <p> + * Example usage: + * <pre> {@code + + ReplayProcessor<Object> processor = new ReplayProcessor<T>(); + processor.onNext("one"); + processor.onNext("two"); + processor.onNext("three"); + processor.onComplete(); + + // both of the following will get the onNext/onComplete calls from above + processor.subscribe(subscriber1); + processor.subscribe(subscriber2); + + } </pre> + * + * @param <T> the value type + */ +public final class ReplayProcessor<@NonNull T> extends FlowableProcessor<T> { + /** An empty array to avoid allocation in getValues(). */ + private static final Object[] EMPTY_ARRAY = new Object[0]; + + final ReplayBuffer<T> buffer; + + boolean done; + + final AtomicReference<ReplaySubscription<T>[]> subscribers; + + @SuppressWarnings("rawtypes") + static final ReplaySubscription[] EMPTY = new ReplaySubscription[0]; + + @SuppressWarnings("rawtypes") + static final ReplaySubscription[] TERMINATED = new ReplaySubscription[0]; + + /** + * Creates an unbounded ReplayProcessor. + * <p> + * The internal buffer is backed by an {@link ArrayList} and starts with an initial capacity of 16. Once the + * number of items reaches this capacity, it will grow as necessary (usually by 50%). However, as the + * number of items grows, this causes frequent array reallocation and copying, and may hurt performance + * and latency. This can be avoided with the {@link #create(int)} overload which takes an initial capacity + * parameter and can be tuned to reduce the array reallocation frequency as needed. + * + * @param <T> + * the type of items observed and emitted by the ReplayProcessor + * @return the created ReplayProcessor + */ + @CheckReturnValue + @NonNull + public static <T> ReplayProcessor<T> create() { + return new ReplayProcessor<>(new UnboundedReplayBuffer<>(16)); + } + + /** + * Creates an unbounded ReplayProcessor with the specified initial buffer capacity. + * <p> + * Use this method to avoid excessive array reallocation while the internal buffer grows to accommodate new + * items. For example, if you know that the buffer will hold 32k items, you can ask the + * {@code ReplayProcessor} to preallocate its internal array with a capacity to hold that many items. Once + * the items start to arrive, the internal array won't need to grow, creating less garbage and no overhead + * due to frequent array-copying. + * + * @param <T> + * the type of items observed and emitted by this type of processor + * @param capacityHint + * the initial buffer capacity + * @return the created processor + * @throws IllegalArgumentException if {@code capacityHint} is non-positive + */ + @CheckReturnValue + @NonNull + public static <T> ReplayProcessor<T> create(int capacityHint) { + ObjectHelper.verifyPositive(capacityHint, "capacityHint"); + return new ReplayProcessor<>(new UnboundedReplayBuffer<>(capacityHint)); + } + + /** + * Creates a size-bounded ReplayProcessor. + * <p> + * In this setting, the {@code ReplayProcessor} holds at most {@code size} items in its internal buffer and + * discards the oldest item. + * <p> + * When {@code Subscriber}s subscribe to a terminated {@code ReplayProcessor}, they are guaranteed to see at most + * {@code size} {@code onNext} events followed by a termination event. + * <p> + * If a {@code Subscriber} subscribes while the {@code ReplayProcessor} is active, it will observe all items in the + * buffer at that point in time and each item observed afterwards, even if the buffer evicts items due to + * the size constraint in the mean time. In other words, once a {@code Subscriber} subscribes, it will receive items + * without gaps in the sequence. + * + * @param <T> + * the type of items observed and emitted by this type of processor + * @param maxSize + * the maximum number of buffered items + * @return the created processor + * @throws IllegalArgumentException if {@code maxSize} is non-positive + */ + @CheckReturnValue + @NonNull + public static <T> ReplayProcessor<T> createWithSize(int maxSize) { + ObjectHelper.verifyPositive(maxSize, "maxSize"); + return new ReplayProcessor<>(new SizeBoundReplayBuffer<>(maxSize)); + } + + /** + * Creates an unbounded ReplayProcessor with the bounded-implementation for testing purposes. + * <p> + * This variant behaves like the regular unbounded {@code ReplayProcessor} created via {@link #create()} but + * uses the structures of the bounded-implementation. This is by no means intended for the replacement of + * the original, array-backed and unbounded {@code ReplayProcessor} due to the additional overhead of the + * linked-list based internal buffer. The sole purpose is to allow testing and reasoning about the behavior + * of the bounded implementations without the interference of the eviction policies. + * + * @param <T> + * the type of items observed and emitted by this type of processor + * @return the created processor + */ + @CheckReturnValue + /* test */ static <T> ReplayProcessor<T> createUnbounded() { + return new ReplayProcessor<>(new SizeBoundReplayBuffer<>(Integer.MAX_VALUE)); + } + + /** + * Creates a time-bounded ReplayProcessor. + * <p> + * In this setting, the {@code ReplayProcessor} internally tags each observed item with a timestamp value + * supplied by the {@link Scheduler} and keeps only those whose age is less than the supplied time value + * converted to milliseconds. For example, an item arrives at T=0 and the max age is set to 5; at T>=5 + * this first item is then evicted by any subsequent item or termination event, leaving the buffer empty. + * <p> + * Once the processor is terminated, {@code Subscriber}s subscribing to it will receive items that remained in the + * buffer after the terminal event, regardless of their age. + * <p> + * If a {@code Subscriber} subscribes while the {@code ReplayProcessor} is active, it will observe only those items + * from within the buffer that have an age less than the specified time, and each item observed thereafter, + * even if the buffer evicts items due to the time constraint in the mean time. In other words, once a + * {@code Subscriber} subscribes, it observes items without gaps in the sequence except for any outdated items at the + * beginning of the sequence. + * <p> + * Note that terminal notifications ({@code onError} and {@code onComplete}) trigger eviction as well. For + * example, with a max age of 5, the first item is observed at T=0, then an {@code onComplete} notification + * arrives at T=10. If a {@code Subscriber} subscribes at T=11, it will find an empty {@code ReplayProcessor} with just + * an {@code onComplete} notification. + * + * @param <T> + * the type of items observed and emitted by this type of processor + * @param maxAge + * the maximum age of the contained items + * @param unit + * the time unit of {@code time} + * @param scheduler + * the {@link Scheduler} that provides the current time + * @return the created processor + * @throws NullPointerException if {@code unit} or {@code scheduler} is {@code null} + * @throws IllegalArgumentException if {@code maxAge} is non-positive + */ + @CheckReturnValue + @NonNull + public static <T> ReplayProcessor<T> createWithTime(long maxAge, @NonNull TimeUnit unit, @NonNull Scheduler scheduler) { + ObjectHelper.verifyPositive(maxAge, "maxAge"); + Objects.requireNonNull(unit, "unit is null"); + Objects.requireNonNull(scheduler, "scheduler is null"); + return new ReplayProcessor<>(new SizeAndTimeBoundReplayBuffer<>(Integer.MAX_VALUE, maxAge, unit, scheduler)); + } + + /** + * Creates a time- and size-bounded ReplayProcessor. + * <p> + * In this setting, the {@code ReplayProcessor} internally tags each received item with a timestamp value + * supplied by the {@link Scheduler} and holds at most {@code size} items in its internal buffer. It evicts + * items from the start of the buffer if their age becomes less-than or equal to the supplied age in + * milliseconds or the buffer reaches its {@code size} limit. + * <p> + * When {@code Subscriber}s subscribe to a terminated {@code ReplayProcessor}, they observe the items that remained in + * the buffer after the terminal notification, regardless of their age, but at most {@code size} items. + * <p> + * If a {@code Subscriber} subscribes while the {@code ReplayProcessor} is active, it will observe only those items + * from within the buffer that have age less than the specified time and each subsequent item, even if the + * buffer evicts items due to the time constraint in the mean time. In other words, once a {@code Subscriber} + * subscribes, it observes items without gaps in the sequence except for the outdated items at the beginning + * of the sequence. + * <p> + * Note that terminal notifications ({@code onError} and {@code onComplete}) trigger eviction as well. For + * example, with a max age of 5, the first item is observed at T=0, then an {@code onComplete} notification + * arrives at T=10. If a {@code Subscriber} subscribes at T=11, it will find an empty {@code ReplayProcessor} with just + * an {@code onComplete} notification. + * + * @param <T> + * the type of items observed and emitted by this type of processor + * @param maxAge + * the maximum age of the contained items + * @param unit + * the time unit of {@code time} + * @param maxSize + * the maximum number of buffered items + * @param scheduler + * the {@link Scheduler} that provides the current time + * @return the created processor + * @throws NullPointerException if {@code unit} or {@code scheduler} is {@code null} + * @throws IllegalArgumentException if {@code maxAge} or {@code maxSize} is non-positive + */ + @CheckReturnValue + @NonNull + public static <T> ReplayProcessor<T> createWithTimeAndSize(long maxAge, @NonNull TimeUnit unit, @NonNull Scheduler scheduler, int maxSize) { + ObjectHelper.verifyPositive(maxSize, "maxSize"); + ObjectHelper.verifyPositive(maxAge, "maxAge"); + Objects.requireNonNull(unit, "unit is null"); + Objects.requireNonNull(scheduler, "scheduler is null"); + return new ReplayProcessor<>(new SizeAndTimeBoundReplayBuffer<>(maxSize, maxAge, unit, scheduler)); + } + + /** + * Constructs a ReplayProcessor with the given custom ReplayBuffer instance. + * @param buffer the ReplayBuffer instance, not null (not verified) + */ + @SuppressWarnings("unchecked") + ReplayProcessor(ReplayBuffer<T> buffer) { + this.buffer = buffer; + this.subscribers = new AtomicReference<>(EMPTY); + } + + @Override + protected void subscribeActual(Subscriber<? super T> s) { + ReplaySubscription<T> rs = new ReplaySubscription<>(s, this); + s.onSubscribe(rs); + + if (add(rs)) { + if (rs.cancelled) { + remove(rs); + return; + } + } + buffer.replay(rs); + } + + @Override + public void onSubscribe(Subscription s) { + if (done) { + s.cancel(); + return; + } + s.request(Long.MAX_VALUE); + } + + @Override + public void onNext(T t) { + ExceptionHelper.nullCheck(t, "onNext called with a null value."); + + if (done) { + return; + } + + ReplayBuffer<T> b = buffer; + b.next(t); + + for (ReplaySubscription<T> rs : subscribers.get()) { + b.replay(rs); + } + } + + @SuppressWarnings("unchecked") + @Override + public void onError(Throwable t) { + ExceptionHelper.nullCheck(t, "onError called with a null Throwable."); + + if (done) { + RxJavaPlugins.onError(t); + return; + } + done = true; + + ReplayBuffer<T> b = buffer; + b.error(t); + + for (ReplaySubscription<T> rs : subscribers.getAndSet(TERMINATED)) { + b.replay(rs); + } + } + + @SuppressWarnings("unchecked") + @Override + public void onComplete() { + if (done) { + return; + } + done = true; + + ReplayBuffer<T> b = buffer; + + b.complete(); + + for (ReplaySubscription<T> rs : subscribers.getAndSet(TERMINATED)) { + b.replay(rs); + } + } + + @Override + @CheckReturnValue + public boolean hasSubscribers() { + return subscribers.get().length != 0; + } + + @CheckReturnValue + /* test */ int subscriberCount() { + return subscribers.get().length; + } + + @Override + @Nullable + @CheckReturnValue + public Throwable getThrowable() { + ReplayBuffer<T> b = buffer; + if (b.isDone()) { + return b.getError(); + } + return null; + } + + /** + * Makes sure the item cached by the head node in a bounded + * ReplayProcessor is released (as it is never part of a replay). + * <p> + * By default, live bounded buffers will remember one item before + * the currently receivable one to ensure subscribers can always + * receive a continuous sequence of items. A terminated ReplayProcessor + * automatically releases this inaccessible item. + * <p> + * The method must be called sequentially, similar to the standard + * {@code onXXX} methods. + * <p>History: 2.1.11 - experimental + * @since 2.2 + */ + public void cleanupBuffer() { + buffer.trimHead(); + } + + /** + * Returns the latest value this processor has or null if no such value exists. + * <p>The method is thread-safe. + * @return the latest value this processor currently has or null if no such value exists + */ + @CheckReturnValue + public T getValue() { + return buffer.getValue(); + } + + /** + * Returns an Object array containing snapshot all values of this processor. + * <p>The method is thread-safe. + * @return the array containing the snapshot of all values of this processor + */ + @CheckReturnValue + public Object[] getValues() { + @SuppressWarnings("unchecked") + T[] a = (T[])EMPTY_ARRAY; + T[] b = getValues(a); + if (b == EMPTY_ARRAY) { + return new Object[0]; + } + return b; + + } + + /** + * Returns a typed array containing a snapshot of all values of this processor. + * <p>The method follows the conventions of Collection.toArray by setting the array element + * after the last value to null (if the capacity permits). + * <p>The method is thread-safe. + * @param array the target array to copy values into if it fits + * @return the given array if the values fit into it or a new array containing all values + */ + @CheckReturnValue + public T[] getValues(T[] array) { + return buffer.getValues(array); + } + + @Override + @CheckReturnValue + public boolean hasComplete() { + ReplayBuffer<T> b = buffer; + return b.isDone() && b.getError() == null; + } + + @Override + @CheckReturnValue + public boolean hasThrowable() { + ReplayBuffer<T> b = buffer; + return b.isDone() && b.getError() != null; + } + + /** + * Returns true if this processor has any value. + * <p>The method is thread-safe. + * @return true if the processor has any value + */ + @CheckReturnValue + public boolean hasValue() { + return buffer.size() != 0; // NOPMD + } + + @CheckReturnValue + /* test*/ int size() { + return buffer.size(); + } + + boolean add(ReplaySubscription<T> rs) { + for (;;) { + ReplaySubscription<T>[] a = subscribers.get(); + if (a == TERMINATED) { + return false; + } + int len = a.length; + @SuppressWarnings("unchecked") + ReplaySubscription<T>[] b = new ReplaySubscription[len + 1]; + System.arraycopy(a, 0, b, 0, len); + b[len] = rs; + if (subscribers.compareAndSet(a, b)) { + return true; + } + } + } + + @SuppressWarnings("unchecked") + void remove(ReplaySubscription<T> rs) { + for (;;) { + ReplaySubscription<T>[] a = subscribers.get(); + if (a == TERMINATED || a == EMPTY) { + return; + } + int len = a.length; + int j = -1; + for (int i = 0; i < len; i++) { + if (a[i] == rs) { + j = i; + break; + } + } + + if (j < 0) { + return; + } + ReplaySubscription<T>[] b; + if (len == 1) { + b = EMPTY; + } else { + b = new ReplaySubscription[len - 1]; + System.arraycopy(a, 0, b, 0, j); + System.arraycopy(a, j + 1, b, j, len - j - 1); + } + if (subscribers.compareAndSet(a, b)) { + return; + } + } + } + + /** + * Abstraction over a buffer that receives events and replays them to + * individual Subscribers. + * + * @param <T> the value type + */ + interface ReplayBuffer<@NonNull T> { + + void next(T value); + + void error(Throwable ex); + + void complete(); + + void replay(ReplaySubscription<T> rs); + + int size(); + + @Nullable + T getValue(); + + T[] getValues(T[] array); + + boolean isDone(); + + Throwable getError(); + + /** + * Make sure an old inaccessible head value is released + * in a bounded buffer. + */ + void trimHead(); + } + + static final class ReplaySubscription<@NonNull T> extends AtomicInteger implements Subscription { + + private static final long serialVersionUID = 466549804534799122L; + final Subscriber<? super T> downstream; + final ReplayProcessor<T> state; + + Object index; + + final AtomicLong requested; + + volatile boolean cancelled; + + long emitted; + + ReplaySubscription(Subscriber<? super T> actual, ReplayProcessor<T> state) { + this.downstream = actual; + this.state = state; + this.requested = new AtomicLong(); + } + + @Override + public void request(long n) { + if (SubscriptionHelper.validate(n)) { + BackpressureHelper.add(requested, n); + state.buffer.replay(this); + } + } + + @Override + public void cancel() { + if (!cancelled) { + cancelled = true; + state.remove(this); + } + } + } + + static final class UnboundedReplayBuffer<T> + implements ReplayBuffer<T> { + + final List<T> buffer; + + Throwable error; + volatile boolean done; + + volatile int size; + + UnboundedReplayBuffer(int capacityHint) { + this.buffer = new ArrayList<>(capacityHint); + } + + @Override + public void next(T value) { + buffer.add(value); + size++; + } + + @Override + public void error(Throwable ex) { + error = ex; + done = true; + } + + @Override + public void complete() { + done = true; + } + + @Override + public void trimHead() { + // not applicable for an unbounded buffer + } + + @Override + @Nullable + public T getValue() { + int s = size; + if (s == 0) { + return null; + } + return buffer.get(s - 1); + } + + @Override + @SuppressWarnings("unchecked") + public T[] getValues(T[] array) { + int s = size; + if (s == 0) { + if (array.length != 0) { + array[0] = null; + } + return array; + } + List<T> b = buffer; + + if (array.length < s) { + array = (T[])Array.newInstance(array.getClass().getComponentType(), s); + } + for (int i = 0; i < s; i++) { + array[i] = b.get(i); + } + if (array.length > s) { + array[s] = null; + } + + return array; + } + + @Override + public void replay(ReplaySubscription<T> rs) { + if (rs.getAndIncrement() != 0) { + return; + } + + int missed = 1; + final List<T> b = buffer; + final Subscriber<? super T> a = rs.downstream; + + Integer indexObject = (Integer)rs.index; + int index; + if (indexObject != null) { + index = indexObject; + } else { + index = 0; + rs.index = 0; + } + long e = rs.emitted; + + for (;;) { + + long r = rs.requested.get(); + + while (e != r) { + if (rs.cancelled) { + rs.index = null; + return; + } + + boolean d = done; + int s = size; + + if (d && index == s) { + rs.index = null; + rs.cancelled = true; + Throwable ex = error; + if (ex == null) { + a.onComplete(); + } else { + a.onError(ex); + } + return; + } + + if (index == s) { + break; + } + + a.onNext(b.get(index)); + + index++; + e++; + } + + if (e == r) { + if (rs.cancelled) { + rs.index = null; + return; + } + + boolean d = done; + int s = size; + + if (d && index == s) { + rs.index = null; + rs.cancelled = true; + Throwable ex = error; + if (ex == null) { + a.onComplete(); + } else { + a.onError(ex); + } + return; + } + } + + rs.index = index; + rs.emitted = e; + missed = rs.addAndGet(-missed); + if (missed == 0) { + break; + } + } + } + + @Override + public int size() { + return size; + } + + @Override + public boolean isDone() { + return done; + } + + @Override + public Throwable getError() { + return error; + } + } + + static final class Node<T> extends AtomicReference<Node<T>> { + + private static final long serialVersionUID = 6404226426336033100L; + + final T value; + + Node(T value) { + this.value = value; + } + } + + static final class TimedNode<T> extends AtomicReference<TimedNode<T>> { + + private static final long serialVersionUID = 6404226426336033100L; + + final T value; + final long time; + + TimedNode(T value, long time) { + this.value = value; + this.time = time; + } + } + + static final class SizeBoundReplayBuffer<@NonNull T> + implements ReplayBuffer<T> { + + final int maxSize; + int size; + + volatile Node<T> head; + + Node<T> tail; + + Throwable error; + volatile boolean done; + + SizeBoundReplayBuffer(int maxSize) { + this.maxSize = maxSize; + Node<T> h = new Node<>(null); + this.tail = h; + this.head = h; + } + + void trim() { + if (size > maxSize) { + size--; + Node<T> h = head; + head = h.get(); + } + } + + @Override + public void next(T value) { + Node<T> n = new Node<>(value); + Node<T> t = tail; + + tail = n; + size++; + t.set(n); // releases both the tail and size + + trim(); + } + + @Override + public void error(Throwable ex) { + error = ex; + trimHead(); + done = true; + } + + @Override + public void complete() { + trimHead(); + done = true; + } + + @Override + public void trimHead() { + if (head.value != null) { + Node<T> n = new Node<>(null); + n.lazySet(head.get()); + head = n; + } + } + + @Override + public boolean isDone() { + return done; + } + + @Override + public Throwable getError() { + return error; + } + + @Override + public T getValue() { + Node<T> h = head; + for (;;) { + Node<T> n = h.get(); + if (n == null) { + return h.value; + } + h = n; + } + } + + @Override + @SuppressWarnings("unchecked") + public T[] getValues(T[] array) { + int s = 0; + Node<T> h = head; + Node<T> h0 = h; + for (;;) { + Node<T> next = h0.get(); + if (next == null) { + break; + } + s++; + h0 = next; + } + if (array.length < s) { + array = (T[])Array.newInstance(array.getClass().getComponentType(), s); + } + + for (int j = 0; j < s; j++) { + h = h.get(); + array[j] = h.value; + } + + if (array.length > s) { + array[s] = null; + } + return array; + } + + @Override + @SuppressWarnings("unchecked") + public void replay(ReplaySubscription<T> rs) { + if (rs.getAndIncrement() != 0) { + return; + } + + int missed = 1; + final Subscriber<? super T> a = rs.downstream; + + Node<T> index = (Node<T>)rs.index; + if (index == null) { + index = head; + } + + long e = rs.emitted; + + for (;;) { + + long r = rs.requested.get(); + + while (e != r) { + if (rs.cancelled) { + rs.index = null; + return; + } + + boolean d = done; + Node<T> next = index.get(); + boolean empty = next == null; + + if (d && empty) { + rs.index = null; + rs.cancelled = true; + Throwable ex = error; + if (ex == null) { + a.onComplete(); + } else { + a.onError(ex); + } + return; + } + + if (empty) { + break; + } + + a.onNext(next.value); + e++; + index = next; + } + + if (e == r) { + if (rs.cancelled) { + rs.index = null; + return; + } + + boolean d = done; + + if (d && index.get() == null) { + rs.index = null; + rs.cancelled = true; + Throwable ex = error; + if (ex == null) { + a.onComplete(); + } else { + a.onError(ex); + } + return; + } + } + + rs.index = index; + rs.emitted = e; + + missed = rs.addAndGet(-missed); + if (missed == 0) { + break; + } + } + } + + @Override + public int size() { + int s = 0; + Node<T> h = head; + while (s != Integer.MAX_VALUE) { + Node<T> next = h.get(); + if (next == null) { + break; + } + s++; + h = next; + } + + return s; + } + } + + static final class SizeAndTimeBoundReplayBuffer<T> + implements ReplayBuffer<T> { + + final int maxSize; + final long maxAge; + final TimeUnit unit; + final Scheduler scheduler; + int size; + + volatile TimedNode<T> head; + + TimedNode<T> tail; + + Throwable error; + volatile boolean done; + + SizeAndTimeBoundReplayBuffer(int maxSize, long maxAge, TimeUnit unit, Scheduler scheduler) { + this.maxSize = maxSize; + this.maxAge = maxAge; + this.unit = unit; + this.scheduler = scheduler; + TimedNode<T> h = new TimedNode<>(null, 0L); + this.tail = h; + this.head = h; + } + + void trim() { + if (size > maxSize) { + size--; + TimedNode<T> h = head; + head = h.get(); + } + long limit = scheduler.now(unit) - maxAge; + + TimedNode<T> h = head; + + for (;;) { + if (size <= 1) { + head = h; + break; + } + TimedNode<T> next = h.get(); + + if (next.time > limit) { + head = h; + break; + } + + h = next; + size--; + } + + } + + void trimFinal() { + long limit = scheduler.now(unit) - maxAge; + + TimedNode<T> h = head; + + for (;;) { + TimedNode<T> next = h.get(); + if (next == null) { + if (h.value != null) { + head = new TimedNode<>(null, 0L); + } else { + head = h; + } + break; + } + + if (next.time > limit) { + if (h.value != null) { + TimedNode<T> n = new TimedNode<>(null, 0L); + n.lazySet(h.get()); + head = n; + } else { + head = h; + } + break; + } + + h = next; + } + } + + @Override + public void trimHead() { + if (head.value != null) { + TimedNode<T> n = new TimedNode<>(null, 0L); + n.lazySet(head.get()); + head = n; + } + } + + @Override + public void next(T value) { + TimedNode<T> n = new TimedNode<>(value, scheduler.now(unit)); + TimedNode<T> t = tail; + + tail = n; + size++; + t.set(n); // releases both the tail and size + + trim(); + } + + @Override + public void error(Throwable ex) { + trimFinal(); + error = ex; + done = true; + } + + @Override + public void complete() { + trimFinal(); + done = true; + } + + @Override + @Nullable + public T getValue() { + TimedNode<T> h = head; + + for (;;) { + TimedNode<T> next = h.get(); + if (next == null) { + break; + } + h = next; + } + + long limit = scheduler.now(unit) - maxAge; + if (h.time < limit) { + return null; + } + + return h.value; + } + + @Override + @SuppressWarnings("unchecked") + public T[] getValues(T[] array) { + TimedNode<T> h = getHead(); + int s = size(h); + + if (s == 0) { + if (array.length != 0) { + array[0] = null; + } + } else { + if (array.length < s) { + array = (T[])Array.newInstance(array.getClass().getComponentType(), s); + } + + int i = 0; + while (i != s) { + TimedNode<T> next = h.get(); + array[i] = next.value; + i++; + h = next; + } + if (array.length > s) { + array[s] = null; + } + } + + return array; + } + + TimedNode<T> getHead() { + TimedNode<T> index = head; + // skip old entries + long limit = scheduler.now(unit) - maxAge; + TimedNode<T> next = index.get(); + while (next != null) { + long ts = next.time; + if (ts > limit) { + break; + } + index = next; + next = index.get(); + } + return index; + } + + @Override + @SuppressWarnings("unchecked") + public void replay(ReplaySubscription<T> rs) { + if (rs.getAndIncrement() != 0) { + return; + } + + int missed = 1; + final Subscriber<? super T> a = rs.downstream; + + TimedNode<T> index = (TimedNode<T>)rs.index; + if (index == null) { + index = getHead(); + } + + long e = rs.emitted; + + for (;;) { + + long r = rs.requested.get(); + + while (e != r) { + if (rs.cancelled) { + rs.index = null; + return; + } + + boolean d = done; + TimedNode<T> next = index.get(); + boolean empty = next == null; + + if (d && empty) { + rs.index = null; + rs.cancelled = true; + Throwable ex = error; + if (ex == null) { + a.onComplete(); + } else { + a.onError(ex); + } + return; + } + + if (empty) { + break; + } + + a.onNext(next.value); + e++; + index = next; + } + + if (e == r) { + if (rs.cancelled) { + rs.index = null; + return; + } + + boolean d = done; + + if (d && index.get() == null) { + rs.index = null; + rs.cancelled = true; + Throwable ex = error; + if (ex == null) { + a.onComplete(); + } else { + a.onError(ex); + } + return; + } + } + + rs.index = index; + rs.emitted = e; + + missed = rs.addAndGet(-missed); + if (missed == 0) { + break; + } + } + } + + @Override + public int size() { + return size(getHead()); + } + + int size(TimedNode<T> h) { + int s = 0; + while (s != Integer.MAX_VALUE) { + TimedNode<T> next = h.get(); + if (next == null) { + break; + } + s++; + h = next; + } + + return s; + } + + @Override + public Throwable getError() { + return error; + } + + @Override + public boolean isDone() { + return done; + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/processors/SerializedProcessor.java b/src/main/java/io/reactivex/rxjava3/processors/SerializedProcessor.java new file mode 100644 index 0000000000..e40d935c51 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/processors/SerializedProcessor.java @@ -0,0 +1,200 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.processors; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.annotations.Nullable; +import io.reactivex.rxjava3.internal.util.*; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +/** + * Serializes calls to the Subscriber methods. + * <p>All other Publisher and Subject methods are thread-safe by design. + * + * @param <T> the item value type + */ +/* public */ final class SerializedProcessor<T> extends FlowableProcessor<T> { + /** The actual subscriber to serialize Subscriber calls to. */ + final FlowableProcessor<T> actual; + /** Indicates an emission is going on, guarded by this. */ + boolean emitting; + /** If not null, it holds the missed NotificationLite events. */ + AppendOnlyLinkedArrayList<Object> queue; + /** Indicates a terminal event has been received and all further events will be dropped. */ + volatile boolean done; + + /** + * Constructor that wraps an actual subject. + * @param actual the subject wrapped + */ + SerializedProcessor(final FlowableProcessor<T> actual) { + this.actual = actual; + } + + @Override + protected void subscribeActual(Subscriber<? super T> s) { + actual.subscribe(s); + } + + @Override + public void onSubscribe(Subscription s) { + boolean cancel; + if (!done) { + synchronized (this) { + if (done) { + cancel = true; + } else { + if (emitting) { + AppendOnlyLinkedArrayList<Object> q = queue; + if (q == null) { + q = new AppendOnlyLinkedArrayList<>(4); + queue = q; + } + q.add(NotificationLite.subscription(s)); + return; + } + emitting = true; + cancel = false; + } + } + } else { + cancel = true; + } + if (cancel) { + s.cancel(); + } else { + actual.onSubscribe(s); + emitLoop(); + } + } + + @Override + public void onNext(T t) { + if (done) { + return; + } + synchronized (this) { + if (done) { + return; + } + if (emitting) { + AppendOnlyLinkedArrayList<Object> q = queue; + if (q == null) { + q = new AppendOnlyLinkedArrayList<>(4); + queue = q; + } + q.add(NotificationLite.next(t)); + return; + } + emitting = true; + } + actual.onNext(t); + emitLoop(); + } + + @Override + public void onError(Throwable t) { + if (done) { + RxJavaPlugins.onError(t); + return; + } + boolean reportError; + synchronized (this) { + if (done) { + reportError = true; + } else { + done = true; + if (emitting) { + AppendOnlyLinkedArrayList<Object> q = queue; + if (q == null) { + q = new AppendOnlyLinkedArrayList<>(4); + queue = q; + } + q.setFirst(NotificationLite.error(t)); + return; + } + reportError = false; + emitting = true; + } + } + if (reportError) { + RxJavaPlugins.onError(t); + return; + } + actual.onError(t); + } + + @Override + public void onComplete() { + if (done) { + return; + } + synchronized (this) { + if (done) { + return; + } + done = true; + if (emitting) { + AppendOnlyLinkedArrayList<Object> q = queue; + if (q == null) { + q = new AppendOnlyLinkedArrayList<>(4); + queue = q; + } + q.add(NotificationLite.complete()); + return; + } + emitting = true; + } + actual.onComplete(); + } + + /** Loops until all notifications in the queue has been processed. */ + void emitLoop() { + for (;;) { + AppendOnlyLinkedArrayList<Object> q; + synchronized (this) { + q = queue; + if (q == null) { + emitting = false; + return; + } + queue = null; + } + + q.accept(actual); + } + } + + @Override + public boolean hasSubscribers() { + return actual.hasSubscribers(); + } + + @Override + public boolean hasThrowable() { + return actual.hasThrowable(); + } + + @Override + @Nullable + public Throwable getThrowable() { + return actual.getThrowable(); + } + + @Override + public boolean hasComplete() { + return actual.hasComplete(); + } +} diff --git a/src/main/java/io/reactivex/rxjava3/processors/UnicastProcessor.java b/src/main/java/io/reactivex/rxjava3/processors/UnicastProcessor.java new file mode 100644 index 0000000000..ad7e7f66b6 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/processors/UnicastProcessor.java @@ -0,0 +1,576 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.processors; + +import java.util.Objects; +import java.util.concurrent.atomic.*; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.annotations.*; +import io.reactivex.rxjava3.internal.functions.*; +import io.reactivex.rxjava3.internal.subscriptions.*; +import io.reactivex.rxjava3.internal.util.*; +import io.reactivex.rxjava3.operators.QueueSubscription; +import io.reactivex.rxjava3.operators.SpscLinkedArrayQueue; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +/** + * A {@link FlowableProcessor} variant that queues up events until a single {@link Subscriber} subscribes to it, replays + * those events to it until the {@code Subscriber} catches up and then switches to relaying events live to + * this single {@code Subscriber} until this {@code UnicastProcessor} terminates or the {@code Subscriber} cancels + * its subscription. + * <p> + * <img width="640" height="370" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/UnicastProcessor.png" alt=""> + * <p> + * This processor does not have a public constructor by design; a new empty instance of this + * {@code UnicastProcessor} can be created via the following {@code create} methods that + * allow specifying the retention policy for items: + * <ul> + * <li>{@link #create()} - creates an empty, unbounded {@code UnicastProcessor} that + * caches all items and the terminal event it receives.</li> + * <li>{@link #create(int)} - creates an empty, unbounded {@code UnicastProcessor} + * with a hint about how many <b>total</b> items one expects to retain.</li> + * <li>{@link #create(boolean)} - creates an empty, unbounded {@code UnicastProcessor} that + * optionally delays an error it receives and replays it after the regular items have been emitted.</li> + * <li>{@link #create(int, Runnable)} - creates an empty, unbounded {@code UnicastProcessor} + * with a hint about how many <b>total</b> items one expects to retain and a callback that will be + * called exactly once when the {@code UnicastProcessor} gets terminated or the single {@code Subscriber} cancels.</li> + * <li>{@link #create(int, Runnable, boolean)} - creates an empty, unbounded {@code UnicastProcessor} + * with a hint about how many <b>total</b> items one expects to retain and a callback that will be + * called exactly once when the {@code UnicastProcessor} gets terminated or the single {@code Subscriber} cancels + * and optionally delays an error it receives and replays it after the regular items have been emitted.</li> + * </ul> + * <p> + * If more than one {@code Subscriber} attempts to subscribe to this Processor, they + * will receive an {@link IllegalStateException} if this {@link UnicastProcessor} hasn't terminated yet, + * or the Subscribers receive the terminal event (error or completion) if this + * Processor has terminated. + * <p> + * The {@code UnicastProcessor} buffers notifications and replays them to the single {@code Subscriber} as requested, + * for which it holds upstream items an unbounded internal buffer until they can be emitted. + * <p> + * Since a {@code UnicastProcessor} is a Reactive Streams {@code Processor}, + * {@code null}s are not allowed (<a href="https://github.com/reactive-streams/reactive-streams-jvm#2.13">Rule 2.13</a>) as + * parameters to {@link #onNext(Object)} and {@link #onError(Throwable)}. Such calls will result in a + * {@link NullPointerException} being thrown and the processor's state is not changed. + * <p> + * Since a {@code UnicastProcessor} is a {@link io.reactivex.rxjava3.core.Flowable} as well as a {@link FlowableProcessor}, it + * honors the downstream backpressure but consumes an upstream source in an unbounded manner (requesting {@link Long#MAX_VALUE}). + * <p> + * When this {@code UnicastProcessor} is terminated via {@link #onError(Throwable)} the current or late single {@code Subscriber} + * may receive the {@code Throwable} before any available items could be emitted. To make sure an {@code onError} event is delivered + * to the {@code Subscriber} after the normal items, create a {@code UnicastProcessor} with the {@link #create(boolean)} or + * {@link #create(int, Runnable, boolean)} factory methods. + * <p> + * Even though {@code UnicastProcessor} implements the {@code Subscriber} interface, calling + * {@code onSubscribe} is not required (<a href="https://github.com/reactive-streams/reactive-streams-jvm#2.12">Rule 2.12</a>) + * if the processor is used as a standalone source. However, calling {@code onSubscribe} + * after the {@code UnicastProcessor} reached its terminal state will result in the + * given {@code Subscription} being canceled immediately. + * <p> + * Calling {@link #onNext(Object)}, {@link #onError(Throwable)} and {@link #onComplete()} + * is required to be serialized (called from the same thread or called non-overlappingly from different threads + * through external means of serialization). The {@link #toSerialized()} method available to all {@link FlowableProcessor}s + * provides such serialization and also protects against reentrance (i.e., when a downstream {@code Subscriber} + * consuming this processor also wants to call {@link #onNext(Object)} on this processor recursively). + * <p> + * This {@code UnicastProcessor} supports the standard state-peeking methods {@link #hasComplete()}, {@link #hasThrowable()}, + * {@link #getThrowable()} and {@link #hasSubscribers()}. + * <dl> + * <dt><b>Backpressure:</b></dt> + * <dd>{@code UnicastProcessor} honors the downstream backpressure but consumes an upstream source + * (if any) in an unbounded manner (requesting {@link Long#MAX_VALUE}).</dd> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code UnicastProcessor} does not operate by default on a particular {@link io.reactivex.rxjava3.core.Scheduler} and + * the single {@code Subscriber} gets notified on the thread the respective {@code onXXX} methods were invoked.</dd> + * <dt><b>Error handling:</b></dt> + * <dd>When the {@link #onError(Throwable)} is called, the {@code UnicastProcessor} enters into a terminal state + * and emits the same {@code Throwable} instance to the current single {@code Subscriber}. During this emission, + * if the single {@code Subscriber}s cancels its respective {@code Subscription}s, the + * {@code Throwable} is delivered to the global error handler via + * {@link io.reactivex.rxjava3.plugins.RxJavaPlugins#onError(Throwable)}. + * If there were no {@code Subscriber}s subscribed to this {@code UnicastProcessor} when the {@code onError()} + * was called, the global error handler is not invoked. + * </dd> + * </dl> + * <p> + * Example usage: + * <pre><code> + * UnicastProcessor<Integer> processor = UnicastProcessor.create(); + * + * TestSubscriber<Integer> ts1 = processor.test(); + * + * // fresh UnicastProcessors are empty + * ts1.assertEmpty(); + * + * TestSubscriber<Integer> ts2 = processor.test(); + * + * // A UnicastProcessor only allows one Subscriber during its lifetime + * ts2.assertFailure(IllegalStateException.class); + * + * processor.onNext(1); + * ts1.assertValue(1); + * + * processor.onNext(2); + * ts1.assertValues(1, 2); + * + * processor.onComplete(); + * ts1.assertResult(1, 2); + * + * // ---------------------------------------------------- + * + * UnicastProcessor<Integer> processor2 = UnicastProcessor.create(); + * + * // a UnicastProcessor caches events until its single Subscriber subscribes + * processor2.onNext(1); + * processor2.onNext(2); + * processor2.onComplete(); + * + * TestSubscriber<Integer> ts3 = processor2.test(); + * + * // the cached events are emitted in order + * ts3.assertResult(1, 2); + * </code></pre> + * + * @param <T> the value type received and emitted by this Processor subclass + * @since 2.0 + */ +public final class UnicastProcessor<@NonNull T> extends FlowableProcessor<T> { + + final SpscLinkedArrayQueue<T> queue; + + final AtomicReference<Runnable> onTerminate; + + final boolean delayError; + + volatile boolean done; + + Throwable error; + + final AtomicReference<Subscriber<? super T>> downstream; + + volatile boolean cancelled; + + final AtomicBoolean once; + + final BasicIntQueueSubscription<T> wip; + + final AtomicLong requested; + + boolean enableOperatorFusion; + + /** + * Creates an UnicastSubject with an internal buffer capacity hint 16. + * @param <T> the value type + * @return an UnicastSubject instance + */ + @CheckReturnValue + @NonNull + public static <T> UnicastProcessor<T> create() { + return new UnicastProcessor<>(bufferSize(), null, true); + } + + /** + * Creates an UnicastProcessor with the given internal buffer capacity hint. + * @param <T> the value type + * @param capacityHint the hint to size the internal unbounded buffer + * @return an UnicastProcessor instance + * @throws IllegalArgumentException if {@code capacityHint} is non-positive + */ + @CheckReturnValue + @NonNull + public static <T> UnicastProcessor<T> create(int capacityHint) { + ObjectHelper.verifyPositive(capacityHint, "capacityHint"); + return new UnicastProcessor<>(capacityHint, null, true); + } + + /** + * Creates an UnicastProcessor with default internal buffer capacity hint and delay error flag. + * <p>History: 2.0.8 - experimental + * @param <T> the value type + * @param delayError deliver pending onNext events before onError + * @return an UnicastProcessor instance + * @since 2.2 + */ + @CheckReturnValue + @NonNull + public static <T> UnicastProcessor<T> create(boolean delayError) { + return new UnicastProcessor<>(bufferSize(), null, delayError); + } + + /** + * Creates an UnicastProcessor with the given internal buffer capacity hint and a callback for + * the case when the single Subscriber cancels its subscription or the + * processor is terminated. + * + * <p>The callback, if not null, is called exactly once and + * non-overlapped with any active replay. + * + * @param <T> the value type + * @param capacityHint the hint to size the internal unbounded buffer + * @param onTerminate the non null callback + * @return an UnicastProcessor instance + * @throws NullPointerException if {@code onTerminate} is {@code null} + * @throws IllegalArgumentException if {@code capacityHint} is non-positive + */ + @CheckReturnValue + @NonNull + public static <T> UnicastProcessor<T> create(int capacityHint, @NonNull Runnable onTerminate) { + return create(capacityHint, onTerminate, true); + } + + /** + * Creates an UnicastProcessor with the given internal buffer capacity hint, delay error flag and a callback for + * the case when the single Subscriber cancels its subscription or + * the processor is terminated. + * + * <p>The callback, if not null, is called exactly once and + * non-overlapped with any active replay. + * <p>History: 2.0.8 - experimental + * @param <T> the value type + * @param capacityHint the hint to size the internal unbounded buffer + * @param onTerminate the non null callback + * @param delayError deliver pending onNext events before onError + * @return an UnicastProcessor instance + * @throws NullPointerException if {@code onTerminate} is {@code null} + * @throws IllegalArgumentException if {@code capacityHint} is non-positive + * @since 2.2 + */ + @CheckReturnValue + @NonNull + public static <T> UnicastProcessor<T> create(int capacityHint, @NonNull Runnable onTerminate, boolean delayError) { + Objects.requireNonNull(onTerminate, "onTerminate"); + ObjectHelper.verifyPositive(capacityHint, "capacityHint"); + return new UnicastProcessor<>(capacityHint, onTerminate, delayError); + } + + /** + * Creates an UnicastProcessor with the given capacity hint and callback + * for when the Processor is terminated normally or its single Subscriber cancels. + * <p>History: 2.0.8 - experimental + * @param capacityHint the capacity hint for the internal, unbounded queue + * @param onTerminate the callback to run when the Processor is terminated or cancelled, null not allowed + * @param delayError deliver pending onNext events before onError + * @since 2.2 + */ + UnicastProcessor(int capacityHint, Runnable onTerminate, boolean delayError) { + this.queue = new SpscLinkedArrayQueue<>(capacityHint); + this.onTerminate = new AtomicReference<>(onTerminate); + this.delayError = delayError; + this.downstream = new AtomicReference<>(); + this.once = new AtomicBoolean(); + this.wip = new UnicastQueueSubscription(); + this.requested = new AtomicLong(); + } + + void doTerminate() { + Runnable r = onTerminate.getAndSet(null); + if (r != null) { + r.run(); + } + } + + void drainRegular(Subscriber<? super T> a) { + int missed = 1; + + final SpscLinkedArrayQueue<T> q = queue; + final boolean failFast = !delayError; + for (;;) { + + long r = requested.get(); + long e = 0L; + + while (r != e) { + boolean d = done; + + T t = q.poll(); + boolean empty = t == null; + + if (checkTerminated(failFast, d, empty, a, q)) { + return; + } + + if (empty) { + break; + } + + a.onNext(t); + + e++; + } + + if (r == e && checkTerminated(failFast, done, q.isEmpty(), a, q)) { + return; + } + + if (e != 0 && r != Long.MAX_VALUE) { + requested.addAndGet(-e); + } + + missed = wip.addAndGet(-missed); + if (missed == 0) { + break; + } + } + } + + void drainFused(Subscriber<? super T> a) { + int missed = 1; + + final SpscLinkedArrayQueue<T> q = queue; + final boolean failFast = !delayError; + for (;;) { + + if (cancelled) { + downstream.lazySet(null); + return; + } + + boolean d = done; + + if (failFast && d && error != null) { + q.clear(); + downstream.lazySet(null); + a.onError(error); + return; + } + a.onNext(null); + + if (d) { + downstream.lazySet(null); + + Throwable ex = error; + if (ex != null) { + a.onError(ex); + } else { + a.onComplete(); + } + return; + } + + missed = wip.addAndGet(-missed); + if (missed == 0) { + break; + } + } + } + + void drain() { + if (wip.getAndIncrement() != 0) { + return; + } + + int missed = 1; + + Subscriber<? super T> a = downstream.get(); + for (;;) { + if (a != null) { + + if (enableOperatorFusion) { + drainFused(a); + } else { + drainRegular(a); + } + return; + } + + missed = wip.addAndGet(-missed); + if (missed == 0) { + break; + } + a = downstream.get(); + } + } + + boolean checkTerminated(boolean failFast, boolean d, boolean empty, Subscriber<? super T> a, SpscLinkedArrayQueue<T> q) { + if (cancelled) { + q.clear(); + downstream.lazySet(null); + return true; + } + + if (d) { + if (failFast && error != null) { + q.clear(); + downstream.lazySet(null); + a.onError(error); + return true; + } + if (empty) { + Throwable e = error; + downstream.lazySet(null); + if (e != null) { + a.onError(e); + } else { + a.onComplete(); + } + return true; + } + } + + return false; + } + + @Override + public void onSubscribe(Subscription s) { + if (done || cancelled) { + s.cancel(); + } else { + s.request(Long.MAX_VALUE); + } + } + + @Override + public void onNext(T t) { + ExceptionHelper.nullCheck(t, "onNext called with a null value."); + + if (done || cancelled) { + return; + } + + queue.offer(t); + drain(); + } + + @Override + public void onError(Throwable t) { + ExceptionHelper.nullCheck(t, "onError called with a null Throwable."); + + if (done || cancelled) { + RxJavaPlugins.onError(t); + return; + } + + error = t; + done = true; + + doTerminate(); + + drain(); + } + + @Override + public void onComplete() { + if (done || cancelled) { + return; + } + + done = true; + + doTerminate(); + + drain(); + } + + @Override + protected void subscribeActual(Subscriber<? super T> s) { + if (!once.get() && once.compareAndSet(false, true)) { + + s.onSubscribe(wip); + downstream.set(s); + if (cancelled) { + downstream.lazySet(null); + } else { + drain(); + } + } else { + EmptySubscription.error(new IllegalStateException("This processor allows only a single Subscriber"), s); + } + } + + final class UnicastQueueSubscription extends BasicIntQueueSubscription<T> { + + private static final long serialVersionUID = -4896760517184205454L; + + @Nullable + @Override + public T poll() { + return queue.poll(); + } + + @Override + public boolean isEmpty() { + return queue.isEmpty(); + } + + @Override + public void clear() { + queue.clear(); + } + + @Override + public int requestFusion(int requestedMode) { + if ((requestedMode & QueueSubscription.ASYNC) != 0) { + enableOperatorFusion = true; + return QueueSubscription.ASYNC; + } + return QueueSubscription.NONE; + } + + @Override + public void request(long n) { + if (SubscriptionHelper.validate(n)) { + BackpressureHelper.add(requested, n); + drain(); + } + } + + @Override + public void cancel() { + if (cancelled) { + return; + } + cancelled = true; + + doTerminate(); + + downstream.lazySet(null); + if (wip.getAndIncrement() == 0) { + downstream.lazySet(null); + if (!enableOperatorFusion) { + queue.clear(); + } + } + } + } + + @Override + @CheckReturnValue + public boolean hasSubscribers() { + return downstream.get() != null; + } + + @Override + @Nullable + @CheckReturnValue + public Throwable getThrowable() { + if (done) { + return error; + } + return null; + } + + @Override + @CheckReturnValue + public boolean hasComplete() { + return done && error == null; + } + + @Override + @CheckReturnValue + public boolean hasThrowable() { + return done && error != null; + } +} diff --git a/src/main/java/io/reactivex/rxjava3/processors/package-info.java b/src/main/java/io/reactivex/rxjava3/processors/package-info.java new file mode 100644 index 0000000000..f4119c9ba8 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/processors/package-info.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +/** + * Classes representing so-called hot backpressure-aware sources, aka <strong>processors</strong>, + * that implement the {@link io.reactivex.rxjava3.processors.FlowableProcessor FlowableProcessor} class, + * the Reactive Streams {@link org.reactivestreams.Processor Processor} interface + * to allow forms of multicasting events to one or more subscribers as well as consuming another + * Reactive Streams {@link org.reactivestreams.Publisher Publisher}. + * <p> + * Available processor implementations: + * <br> + * <ul> + * <li>{@link io.reactivex.rxjava3.processors.AsyncProcessor AsyncProcessor} - replays the very last item</li> + * <li>{@link io.reactivex.rxjava3.processors.BehaviorProcessor BehaviorProcessor} - remembers the latest item</li> + * <li>{@link io.reactivex.rxjava3.processors.MulticastProcessor MulticastProcessor} - coordinates its source with its consumers</li> + * <li>{@link io.reactivex.rxjava3.processors.PublishProcessor PublishProcessor} - dispatches items to current consumers</li> + * <li>{@link io.reactivex.rxjava3.processors.ReplayProcessor ReplayProcessor} - remembers some or all items and replays them to consumers</li> + * <li>{@link io.reactivex.rxjava3.processors.UnicastProcessor UnicastProcessor} - remembers or relays items to a single consumer</li> + * </ul> + * <p> + * The non-backpressured variants of the {@code FlowableProcessor} class are called + * {@link io.reactivex.rxjava3.subjects.Subject}s and reside in the {@code io.reactivex.subjects} package. + * @see io.reactivex.rxjava3.subjects + */ +package io.reactivex.rxjava3.processors; diff --git a/src/main/java/io/reactivex/rxjava3/schedulers/SchedulerRunnableIntrospection.java b/src/main/java/io/reactivex/rxjava3/schedulers/SchedulerRunnableIntrospection.java new file mode 100644 index 0000000000..97f8efcc9d --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/schedulers/SchedulerRunnableIntrospection.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.schedulers; + +import io.reactivex.rxjava3.annotations.NonNull; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +/** + * Interface to indicate the implementor class wraps a {@code Runnable} that can + * be accessed via {@link #getWrappedRunnable()}. + * <p> + * You can check if a {@link Runnable} task submitted to a {@link io.reactivex.rxjava3.core.Scheduler Scheduler} (or its + * {@link io.reactivex.rxjava3.core.Scheduler.Worker Scheduler.Worker}) implements this interface and unwrap the + * original {@code Runnable} instance. This could help to avoid hooking the same underlying {@code Runnable} + * task in a custom {@link RxJavaPlugins#onSchedule(Runnable)} hook set via + * the {@link RxJavaPlugins#setScheduleHandler(Function)} method multiple times due to internal delegation + * of the default {@code Scheduler.scheduleDirect} or {@code Scheduler.Worker.schedule} methods. + * <p>History: 2.1.7 - experimental + * @since 2.2 + */ +public interface SchedulerRunnableIntrospection { + + /** + * Returns the wrapped action. + * + * @return the wrapped action. Cannot be null. + */ + @NonNull + Runnable getWrappedRunnable(); +} diff --git a/src/main/java/io/reactivex/rxjava3/schedulers/Schedulers.java b/src/main/java/io/reactivex/rxjava3/schedulers/Schedulers.java new file mode 100644 index 0000000000..cb25652404 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/schedulers/Schedulers.java @@ -0,0 +1,614 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.schedulers; + +import java.util.concurrent.*; + +import io.reactivex.rxjava3.annotations.NonNull; +import io.reactivex.rxjava3.core.Scheduler; +import io.reactivex.rxjava3.functions.Supplier; +import io.reactivex.rxjava3.internal.schedulers.*; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +/** + * Static factory methods for returning standard {@link Scheduler} instances. + * <p> + * The initial and runtime values of the various scheduler types can be overridden via the + * {@code RxJavaPlugins.setInit(scheduler name)SchedulerHandler()} and + * {@code RxJavaPlugins.set(scheduler name)SchedulerHandler()} respectively. + * Note that overriding any initial {@code Scheduler} via the {@link RxJavaPlugins} + * has to happen before the {@code Schedulers} class is accessed. + * <p> + * <strong>Supported system properties ({@code System.getProperty()}):</strong> + * <ul> + * <li>{@code rx3.io-keep-alive-time} (long): sets the keep-alive time of the {@link #io()} {@code Scheduler} workers, default is {@link IoScheduler#KEEP_ALIVE_TIME_DEFAULT}</li> + * <li>{@code rx3.io-priority} (int): sets the thread priority of the {@link #io()} {@code Scheduler}, default is {@link Thread#NORM_PRIORITY}</li> + * <li>{@code rx3.io-scheduled-release} (boolean): {@code true} sets the worker release mode of the + * {@link #io()} {@code Scheduler} to <em>scheduled</em>, default is {@code false} for <em>eager</em> mode.</li> + * <li>{@code rx3.computation-threads} (int): sets the number of threads in the {@link #computation()} {@code Scheduler}, default is the number of available CPUs</li> + * <li>{@code rx3.computation-priority} (int): sets the thread priority of the {@link #computation()} {@code Scheduler}, default is {@link Thread#NORM_PRIORITY}</li> + * <li>{@code rx3.newthread-priority} (int): sets the thread priority of the {@link #newThread()} {@code Scheduler}, default is {@link Thread#NORM_PRIORITY}</li> + * <li>{@code rx3.single-priority} (int): sets the thread priority of the {@link #single()} {@code Scheduler}, default is {@link Thread#NORM_PRIORITY}</li> + * <li>{@code rx3.purge-enabled} (boolean): enables purging of all {@code Scheduler}'s backing thread pools, default is {@code true}</li> + * <li>{@code rx3.scheduler.use-nanotime} (boolean): {@code true} instructs {@code Scheduler} to use {@link System#nanoTime()} for {@link Scheduler#now(TimeUnit)}, + * instead of default {@link System#currentTimeMillis()} ({@code false})</li> + * </ul> + */ +public final class Schedulers { + @NonNull + static final Scheduler SINGLE; + + @NonNull + static final Scheduler COMPUTATION; + + @NonNull + static final Scheduler IO; + + @NonNull + static final Scheduler TRAMPOLINE; + + @NonNull + static final Scheduler NEW_THREAD; + + static final class SingleHolder { + static final Scheduler DEFAULT = new SingleScheduler(); + } + + static final class ComputationHolder { + static final Scheduler DEFAULT = new ComputationScheduler(); + } + + static final class IoHolder { + static final Scheduler DEFAULT = new IoScheduler(); + } + + static final class NewThreadHolder { + static final Scheduler DEFAULT = new NewThreadScheduler(); + } + + static { + SINGLE = RxJavaPlugins.initSingleScheduler(new SingleTask()); + + COMPUTATION = RxJavaPlugins.initComputationScheduler(new ComputationTask()); + + IO = RxJavaPlugins.initIoScheduler(new IOTask()); + + TRAMPOLINE = TrampolineScheduler.instance(); + + NEW_THREAD = RxJavaPlugins.initNewThreadScheduler(new NewThreadTask()); + } + + /** Utility class. */ + private Schedulers() { + throw new IllegalStateException("No instances!"); + } + + /** + * Returns a default, shared {@link Scheduler} instance intended for computational work. + * <p> + * This can be used for event-loops, processing callbacks and other computational work. + * <p> + * It is not recommended to perform blocking, IO-bound work on this scheduler. Use {@link #io()} instead. + * <p> + * The default instance has a backing pool of single-threaded {@link ScheduledExecutorService} instances equal to + * the number of available processors ({@link java.lang.Runtime#availableProcessors()}) to the Java VM. + * <p> + * Unhandled errors will be delivered to the scheduler Thread's {@link java.lang.Thread.UncaughtExceptionHandler}. + * <p> + * This type of scheduler is less sensitive to leaking {@link io.reactivex.rxjava3.core.Scheduler.Worker} instances, although + * not disposing a worker that has timed/delayed tasks not cancelled by other means may leak resources and/or + * execute those tasks "unexpectedly". + * <p> + * If the {@link RxJavaPlugins#setFailOnNonBlockingScheduler(boolean)} is set to {@code true}, attempting to execute + * operators that block while running on this scheduler will throw an {@link IllegalStateException}. + * <p> + * You can control certain properties of this standard scheduler via system properties that have to be set + * before the {@code Schedulers} class is referenced in your code. + * <p><strong>Supported system properties ({@code System.getProperty()}):</strong> + * <ul> + * <li>{@code rx3.computation-threads} (int): sets the number of threads in the {@code computation()} {@code Scheduler}, default is the number of available CPUs</li> + * <li>{@code rx3.computation-priority} (int): sets the thread priority of the {@code computation()} {@code Scheduler}, default is {@link Thread#NORM_PRIORITY}</li> + * </ul> + * <p> + * The default value of this scheduler can be overridden at initialization time via the + * {@link RxJavaPlugins#setInitComputationSchedulerHandler(io.reactivex.rxjava3.functions.Function)} plugin method. + * Note that due to possible initialization cycles, using any of the other scheduler-returning methods will + * result in a {@link NullPointerException}. + * Once the {@code Schedulers} class has been initialized, you can override the returned {@code Scheduler} instance + * via the {@link RxJavaPlugins#setComputationSchedulerHandler(io.reactivex.rxjava3.functions.Function)} method. + * <p> + * It is possible to create a fresh instance of this scheduler with a custom {@link ThreadFactory}, via the + * {@link RxJavaPlugins#createComputationScheduler(ThreadFactory)} method. Note that such custom + * instances require a manual call to {@link Scheduler#shutdown()} to allow the JVM to exit or the + * (J2EE) container to unload properly. + * <p>Operators on the base reactive classes that use this scheduler are marked with the + * @{@link io.reactivex.rxjava3.annotations.SchedulerSupport SchedulerSupport}({@link io.reactivex.rxjava3.annotations.SchedulerSupport#COMPUTATION COMPUTATION}) + * annotation. + * @return a {@code Scheduler} meant for computation-bound work + */ + @NonNull + public static Scheduler computation() { + return RxJavaPlugins.onComputationScheduler(COMPUTATION); + } + + /** + * Returns a default, shared {@link Scheduler} instance intended for IO-bound work. + * <p> + * This can be used for asynchronously performing blocking IO. + * <p> + * The implementation is backed by a pool of single-threaded {@link ScheduledExecutorService} instances + * that will try to reuse previously started instances used by the worker + * returned by {@link io.reactivex.rxjava3.core.Scheduler#createWorker()} but otherwise will start a new backing + * {@link ScheduledExecutorService} instance. Note that this scheduler may create an unbounded number + * of worker threads that can result in system slowdowns or {@link OutOfMemoryError}. Therefore, for casual uses + * or when implementing an operator, the Worker instances must be disposed via {@link io.reactivex.rxjava3.core.Scheduler.Worker#dispose()}. + * <p> + * It is not recommended to perform computational work on this scheduler. Use {@link #computation()} instead. + * <p> + * Unhandled errors will be delivered to the scheduler Thread's {@link java.lang.Thread.UncaughtExceptionHandler}. + * <p> + * You can control certain properties of this standard scheduler via system properties that have to be set + * before the {@code Schedulers} class is referenced in your code. + * <p><strong>Supported system properties ({@code System.getProperty()}):</strong> + * <ul> + * <li>{@code rx3.io-keep-alive-time} (long): sets the keep-alive time of the {@code io()} {@code Scheduler} workers, default is {@link IoScheduler#KEEP_ALIVE_TIME_DEFAULT}</li> + * <li>{@code rx3.io-priority} (int): sets the thread priority of the {@code io()} {@code Scheduler}, default is {@link Thread#NORM_PRIORITY}</li> + * <li>{@code rx3.io-scheduled-release} (boolean): {@code true} sets the worker release mode of the + * {@code #io()} {@code Scheduler} to <em>scheduled</em>, default is {@code false} for <em>eager</em> mode.</li> + * </ul> + * <p> + * The default value of this scheduler can be overridden at initialization time via the + * {@link RxJavaPlugins#setInitIoSchedulerHandler(io.reactivex.rxjava3.functions.Function)} plugin method. + * Note that due to possible initialization cycles, using any of the other scheduler-returning methods will + * result in a {@link NullPointerException}. + * Once the {@code Schedulers} class has been initialized, you can override the returned {@code Scheduler} instance + * via the {@link RxJavaPlugins#setIoSchedulerHandler(io.reactivex.rxjava3.functions.Function)} method. + * <p> + * It is possible to create a fresh instance of this scheduler with a custom {@link ThreadFactory}, via the + * {@link RxJavaPlugins#createIoScheduler(ThreadFactory)} method. Note that such custom + * instances require a manual call to {@link Scheduler#shutdown()} to allow the JVM to exit or the + * (J2EE) container to unload properly. + * <p>Operators on the base reactive classes that use this scheduler are marked with the + * @{@link io.reactivex.rxjava3.annotations.SchedulerSupport SchedulerSupport}({@link io.reactivex.rxjava3.annotations.SchedulerSupport#IO IO}) + * annotation. + * <p> + * When the {@link io.reactivex.rxjava3.core.Scheduler.Worker Scheduler.Worker} is disposed, + * the underlying worker can be released to the cached worker pool in two modes: + * <ul> + * <li>In <em>eager</em> mode (default), the underlying worker is returned immediately to the cached worker pool + * and can be reused much quicker by operators. The drawback is that if the currently running task doesn't + * respond to interruption in time or at all, this may lead to delays or deadlock with the reuse use of the + * underlying worker. + * </li> + * <li>In <em>scheduled</em> mode (enabled via the system parameter {@code rx3.io-scheduled-release} + * set to {@code true}), the underlying worker is returned to the cached worker pool only after the currently running task + * has finished. This can help prevent premature reuse of the underlying worker and likely won't lead to delays or + * deadlock with such reuses. The drawback is that the delay in release may lead to an excess amount of underlying + * workers being created. + * </li> + * </ul> + * @return a {@code Scheduler} meant for IO-bound work + */ + @NonNull + public static Scheduler io() { + return RxJavaPlugins.onIoScheduler(IO); + } + + /** + * Returns a default, shared {@link Scheduler} instance whose {@link io.reactivex.rxjava3.core.Scheduler.Worker} + * instances queue work and execute them in a FIFO manner on one of the participating threads. + * <p> + * The default implementation's {@link Scheduler#scheduleDirect(Runnable)} methods execute the tasks on the current thread + * without any queueing and the timed overloads use blocking sleep as well. + * <p> + * Note that this scheduler can't be reliably used to return the execution of + * tasks to the "main" thread. Such behavior requires a blocking-queueing scheduler currently not provided + * by RxJava itself but may be found in external libraries. + * <p> + * This scheduler can't be overridden via an {@link RxJavaPlugins} method. + * @return a {@code Scheduler} that queues work on the current thread + */ + @NonNull + public static Scheduler trampoline() { + return TRAMPOLINE; + } + + /** + * Returns a default, shared {@link Scheduler} instance that creates a new {@link Thread} for each unit of work. + * <p> + * The default implementation of this scheduler creates a new, single-threaded {@link ScheduledExecutorService} for + * each invocation of the {@link Scheduler#scheduleDirect(Runnable)} (plus its overloads) and {@link Scheduler#createWorker()} + * methods, thus an unbounded number of worker threads may be created that can + * result in system slowdowns or {@link OutOfMemoryError}. Therefore, for casual uses or when implementing an operator, + * the Worker instances must be disposed via {@link io.reactivex.rxjava3.core.Scheduler.Worker#dispose()}. + * <p> + * Unhandled errors will be delivered to the scheduler Thread's {@link java.lang.Thread.UncaughtExceptionHandler}. + * <p> + * You can control certain properties of this standard scheduler via system properties that have to be set + * before the {@code Schedulers} class is referenced in your code. + * <p><strong>Supported system properties ({@code System.getProperty()}):</strong> + * <ul> + * <li>{@code rx3.newthread-priority} (int): sets the thread priority of the {@code newThread()} {@code Scheduler}, default is {@link Thread#NORM_PRIORITY}</li> + * </ul> + * <p> + * The default value of this scheduler can be overridden at initialization time via the + * {@link RxJavaPlugins#setInitNewThreadSchedulerHandler(io.reactivex.rxjava3.functions.Function)} plugin method. + * Note that due to possible initialization cycles, using any of the other scheduler-returning methods will + * result in a {@link NullPointerException}. + * Once the {@code Schedulers} class has been initialized, you can override the returned {@code Scheduler} instance + * via the {@link RxJavaPlugins#setNewThreadSchedulerHandler(io.reactivex.rxjava3.functions.Function)} method. + * <p> + * It is possible to create a fresh instance of this scheduler with a custom {@link ThreadFactory}, via the + * {@link RxJavaPlugins#createNewThreadScheduler(ThreadFactory)} method. Note that such custom + * instances require a manual call to {@link Scheduler#shutdown()} to allow the JVM to exit or the + * (J2EE) container to unload properly. + * <p>Operators on the base reactive classes that use this scheduler are marked with the + * @{@link io.reactivex.rxjava3.annotations.SchedulerSupport SchedulerSupport}({@link io.reactivex.rxjava3.annotations.SchedulerSupport#NEW_THREAD NEW_TRHEAD}) + * annotation. + * @return a {@code Scheduler} that creates new threads + */ + @NonNull + public static Scheduler newThread() { + return RxJavaPlugins.onNewThreadScheduler(NEW_THREAD); + } + + /** + * Returns a default, shared, single-thread-backed {@link Scheduler} instance for work + * requiring strongly-sequential execution on the same background thread. + * <p> + * Uses: + * <ul> + * <li>event loop</li> + * <li>support {@code Schedulers.from(}{@link Executor}{@code )} and {@code from(}{@link ExecutorService}{@code )} with delayed scheduling</li> + * <li>support benchmarks that pipeline data from some thread to another thread and + * avoid core-bashing of computation's round-robin nature</li> + * </ul> + * <p> + * Unhandled errors will be delivered to the scheduler Thread's {@link java.lang.Thread.UncaughtExceptionHandler}. + * <p> + * This type of scheduler is less sensitive to leaking {@link io.reactivex.rxjava3.core.Scheduler.Worker} instances, although + * not disposing a worker that has timed/delayed tasks not cancelled by other means may leak resources and/or + * execute those tasks "unexpectedly". + * <p> + * If the {@link RxJavaPlugins#setFailOnNonBlockingScheduler(boolean)} is set to {@code true}, attempting to execute + * operators that block while running on this scheduler will throw an {@link IllegalStateException}. + * <p> + * You can control certain properties of this standard scheduler via system properties that have to be set + * before the {@code Schedulers} class is referenced in your code. + * <p><strong>Supported system properties ({@code System.getProperty()}):</strong> + * <ul> + * <li>{@code rx3.single-priority} (int): sets the thread priority of the {@code single()} {@code Scheduler}, default is {@link Thread#NORM_PRIORITY}</li> + * </ul> + * <p> + * The default value of this scheduler can be overridden at initialization time via the + * {@link RxJavaPlugins#setInitSingleSchedulerHandler(io.reactivex.rxjava3.functions.Function)} plugin method. + * Note that due to possible initialization cycles, using any of the other scheduler-returning methods will + * result in a {@link NullPointerException}. + * Once the {@code Schedulers} class has been initialized, you can override the returned {@code Scheduler} instance + * via the {@link RxJavaPlugins#setSingleSchedulerHandler(io.reactivex.rxjava3.functions.Function)} method. + * <p> + * It is possible to create a fresh instance of this scheduler with a custom {@link ThreadFactory}, via the + * {@link RxJavaPlugins#createSingleScheduler(ThreadFactory)} method. Note that such custom + * instances require a manual call to {@link Scheduler#shutdown()} to allow the JVM to exit or the + * (J2EE) container to unload properly. + * <p>Operators on the base reactive classes that use this scheduler are marked with the + * @{@link io.reactivex.rxjava3.annotations.SchedulerSupport SchedulerSupport}({@link io.reactivex.rxjava3.annotations.SchedulerSupport#SINGLE SINGLE}) + * annotation. + * @return a {@code Scheduler} that shares a single backing thread. + * @since 2.0 + */ + @NonNull + public static Scheduler single() { + return RxJavaPlugins.onSingleScheduler(SINGLE); + } + + /** + * Wraps an {@link Executor} into a new {@link Scheduler} instance and delegates {@code schedule()} + * calls to it. + * <p> + * If the provided executor doesn't support any of the more specific standard Java executor + * APIs, tasks scheduled by this scheduler can't be interrupted when they are + * executing but only prevented from running prior to that. In addition, tasks scheduled with + * a time delay or periodically will use the {@link #single()} scheduler for the timed waiting + * before posting the actual task to the given executor. + * <p> + * Tasks submitted to the {@link io.reactivex.rxjava3.core.Scheduler.Worker Scheduler.Worker} of this {@code Scheduler} are also not interruptible. Use the + * {@link #from(Executor, boolean)} overload to enable task interruption via this wrapper. + * <p> + * If the provided executor supports the standard Java {@link ExecutorService} API, + * tasks scheduled by this scheduler can be cancelled/interrupted by calling + * {@link io.reactivex.rxjava3.disposables.Disposable#dispose()}. In addition, tasks scheduled with + * a time delay or periodically will use the {@link #single()} scheduler for the timed waiting + * before posting the actual task to the given executor. + * <p> + * If the provided executor supports the standard Java {@link ScheduledExecutorService} API, + * tasks scheduled by this scheduler can be cancelled/interrupted by calling + * {@link io.reactivex.rxjava3.disposables.Disposable#dispose()}. In addition, tasks scheduled with + * a time delay or periodically will use the provided executor. Note, however, if the provided + * {@code ScheduledExecutorService} instance is not single threaded, tasks scheduled + * with a time delay close to each other may end up executing in different order than + * the original schedule() call was issued. This limitation may be lifted in a future patch. + * <p> + * The implementation of the Worker of this wrapper {@code Scheduler} is eager and will execute as many + * non-delayed tasks as it can, which may result in a longer than expected occupation of a + * thread of the given backing {@code Executor}. In other terms, it does not allow per-{@link Runnable} fairness + * in case the worker runs on a shared underlying thread of the {@code Executor}. + * See {@link #from(Executor, boolean, boolean)} to create a wrapper that uses the underlying {@code Executor} + * more fairly. + * <p> + * Starting, stopping and restarting this scheduler is not supported (no-op) and the provided + * executor's lifecycle must be managed externally: + * <pre><code> + * ExecutorService exec = Executors.newSingleThreadedExecutor(); + * try { + * Scheduler scheduler = Schedulers.from(exec); + * Flowable.just(1) + * .subscribeOn(scheduler) + * .map(v -> v + 1) + * .observeOn(scheduler) + * .blockingSubscribe(System.out::println); + * } finally { + * exec.shutdown(); + * } + * </code></pre> + * <p> + * Note that the provided {@code Executor} should avoid throwing a {@link RejectedExecutionException} + * (for example, by shutting it down prematurely or using a bounded-queue {@code ExecutorService}) + * because such circumstances prevent RxJava from progressing flow-related activities correctly. + * If the {@link Executor#execute(Runnable)} or {@link ExecutorService#submit(Callable)} throws, + * the {@code RejectedExecutionException} is routed to the global error handler via + * {@link RxJavaPlugins#onError(Throwable)}. To avoid shutdown-related problems, it is recommended + * all flows using the returned {@code Scheduler} to be canceled/disposed before the underlying + * {@code Executor} is shut down. To avoid problems due to the {@code Executor} having a bounded-queue, + * it is recommended to rephrase the flow to utilize backpressure as the means to limit outstanding work. + * <p> + * This type of scheduler is less sensitive to leaking {@link io.reactivex.rxjava3.core.Scheduler.Worker Scheduler.Worker} instances, although + * not disposing a worker that has timed/delayed tasks not cancelled by other means may leak resources and/or + * execute those tasks "unexpectedly". + * <p> + * Note that this method returns a new {@code Scheduler} instance, even for the same {@code Executor} instance. + * <p> + * It is possible to wrap an {@code Executor} into a {@code Scheduler} without triggering the initialization of all the + * standard schedulers by using the {@link RxJavaPlugins#createExecutorScheduler(Executor, boolean, boolean)} method + * before the {@code Schedulers} class itself is accessed. + * @param executor + * the executor to wrap + * @return the new {@code Scheduler} wrapping the {@code Executor} + * @see #from(Executor, boolean, boolean) + */ + @NonNull + public static Scheduler from(@NonNull Executor executor) { + return from(executor, false, false); + } + + /** + * Wraps an {@link Executor} into a new {@link Scheduler} instance and delegates {@code schedule()} + * calls to it. + * <p> + * The tasks scheduled by the returned {@code Scheduler} and its {@link io.reactivex.rxjava3.core.Scheduler.Worker Scheduler.Worker} + * can be optionally interrupted. + * <p> + * If the provided executor doesn't support any of the more specific standard Java executor + * APIs, tasks scheduled with a time delay or periodically will use the + * {@link #single()} scheduler for the timed waiting + * before posting the actual task to the given executor. + * <p> + * If the provided executor supports the standard Java {@link ExecutorService} API, + * tasks scheduled by this scheduler can be cancelled/interrupted by calling + * {@link io.reactivex.rxjava3.disposables.Disposable#dispose()}. In addition, tasks scheduled with + * a time delay or periodically will use the {@link #single()} scheduler for the timed waiting + * before posting the actual task to the given executor. + * <p> + * If the provided executor supports the standard Java {@link ScheduledExecutorService} API, + * tasks scheduled by this scheduler can be cancelled/interrupted by calling + * {@link io.reactivex.rxjava3.disposables.Disposable#dispose()}. In addition, tasks scheduled with + * a time delay or periodically will use the provided executor. Note, however, if the provided + * {@code ScheduledExecutorService} instance is not single threaded, tasks scheduled + * with a time delay close to each other may end up executing in different order than + * the original schedule() call was issued. This limitation may be lifted in a future patch. + * <p> + * The implementation of the {@code Worker} of this wrapper {@code Scheduler} is eager and will execute as many + * non-delayed tasks as it can, which may result in a longer than expected occupation of a + * thread of the given backing {@code Executor}. In other terms, it does not allow per-{@link Runnable} fairness + * in case the worker runs on a shared underlying thread of the {@code Executor}. + * See {@link #from(Executor, boolean, boolean)} to create a wrapper that uses the underlying {@code Executor} + * more fairly. + * <p> + * Starting, stopping and restarting this scheduler is not supported (no-op) and the provided + * executor's lifecycle must be managed externally: + * <pre><code> + * ExecutorService exec = Executors.newSingleThreadedExecutor(); + * try { + * Scheduler scheduler = Schedulers.from(exec, true); + * Flowable.just(1) + * .subscribeOn(scheduler) + * .map(v -> v + 1) + * .observeOn(scheduler) + * .blockingSubscribe(System.out::println); + * } finally { + * exec.shutdown(); + * } + * </code></pre> + * <p> + * Note that the provided {@code Executor} should avoid throwing a {@link RejectedExecutionException} + * (for example, by shutting it down prematurely or using a bounded-queue {@code ExecutorService}) + * because such circumstances prevent RxJava from progressing flow-related activities correctly. + * If the {@link Executor#execute(Runnable)} or {@link ExecutorService#submit(Callable)} throws, + * the {@code RejectedExecutionException} is routed to the global error handler via + * {@link RxJavaPlugins#onError(Throwable)}. To avoid shutdown-related problems, it is recommended + * all flows using the returned {@code Scheduler} to be canceled/disposed before the underlying + * {@code Executor} is shut down. To avoid problems due to the {@code Executor} having a bounded-queue, + * it is recommended to rephrase the flow to utilize backpressure as the means to limit outstanding work. + * <p> + * This type of scheduler is less sensitive to leaking {@link io.reactivex.rxjava3.core.Scheduler.Worker Scheduler.Worker} instances, although + * not disposing a worker that has timed/delayed tasks not cancelled by other means may leak resources and/or + * execute those tasks "unexpectedly". + * <p> + * Note that this method returns a new {@code Scheduler} instance, even for the same {@code Executor} instance. + * <p> + * It is possible to wrap an {@code Executor} into a {@code Scheduler} without triggering the initialization of all the + * standard schedulers by using the {@link RxJavaPlugins#createExecutorScheduler(Executor, boolean, boolean)} method + * before the {@code Schedulers} class itself is accessed. + * <p>History: 2.2.6 - experimental + * @param executor + * the executor to wrap + * @param interruptibleWorker if {@code true}, the tasks submitted to the {@link io.reactivex.rxjava3.core.Scheduler.Worker Scheduler.Worker} will + * be interrupted when the task is disposed. + * @return the new {@code Scheduler} wrapping the {@code Executor} + * @since 3.0.0 + * @see #from(Executor, boolean, boolean) + */ + @NonNull + public static Scheduler from(@NonNull Executor executor, boolean interruptibleWorker) { + return from(executor, interruptibleWorker, false); + } + + /** + * Wraps an {@link Executor} into a new {@link Scheduler} instance and delegates {@code schedule()} + * calls to it. + * <p> + * The tasks scheduled by the returned {@code Scheduler} and its {@link io.reactivex.rxjava3.core.Scheduler.Worker Scheduler.Worker} + * can be optionally interrupted. + * <p> + * If the provided executor doesn't support any of the more specific standard Java executor + * APIs, tasks scheduled with a time delay or periodically will use the + * {@link #single()} scheduler for the timed waiting + * before posting the actual task to the given executor. + * <p> + * If the provided executor supports the standard Java {@link ExecutorService} API, + * tasks scheduled by this scheduler can be cancelled/interrupted by calling + * {@link io.reactivex.rxjava3.disposables.Disposable#dispose()}. In addition, tasks scheduled with + * a time delay or periodically will use the {@link #single()} scheduler for the timed waiting + * before posting the actual task to the given executor. + * <p> + * If the provided executor supports the standard Java {@link ScheduledExecutorService} API, + * tasks scheduled by this scheduler can be cancelled/interrupted by calling + * {@link io.reactivex.rxjava3.disposables.Disposable#dispose()}. In addition, tasks scheduled with + * a time delay or periodically will use the provided executor. Note, however, if the provided + * {@code ScheduledExecutorService} instance is not single threaded, tasks scheduled + * with a time delay close to each other may end up executing in different order than + * the original schedule() call was issued. This limitation may be lifted in a future patch. + * <p> + * The implementation of the Worker of this wrapper {@code Scheduler} can operate in both eager (non-fair) and + * fair modes depending on the specified parameter. In <em>eager</em> mode, it will execute as many + * non-delayed tasks as it can, which may result in a longer than expected occupation of a + * thread of the given backing {@code Executor}. In other terms, it does not allow per-{@link Runnable} fairness + * in case the worker runs on a shared underlying thread of the {@code Executor}. In <em>fair</em> mode, + * non-delayed tasks will still be executed in a FIFO and non-overlapping manner, but after each task, + * the execution for the next task is rescheduled with the same underlying {@code Executor}, allowing interleaving + * from both the same {@code Scheduler} or other external usages of the underlying {@code Executor}. + * <p> + * Starting, stopping and restarting this scheduler is not supported (no-op) and the provided + * executor's lifecycle must be managed externally: + * <pre><code> + * ExecutorService exec = Executors.newSingleThreadedExecutor(); + * try { + * Scheduler scheduler = Schedulers.from(exec, true, true); + * Flowable.just(1) + * .subscribeOn(scheduler) + * .map(v -> v + 1) + * .observeOn(scheduler) + * .blockingSubscribe(System.out::println); + * } finally { + * exec.shutdown(); + * } + * </code></pre> + * <p> + * Note that the provided {@code Executor} should avoid throwing a {@link RejectedExecutionException} + * (for example, by shutting it down prematurely or using a bounded-queue {@code ExecutorService}) + * because such circumstances prevent RxJava from progressing flow-related activities correctly. + * If the {@link Executor#execute(Runnable)} or {@link ExecutorService#submit(Callable)} throws, + * the {@code RejectedExecutionException} is routed to the global error handler via + * {@link RxJavaPlugins#onError(Throwable)}. To avoid shutdown-related problems, it is recommended + * all flows using the returned {@code Scheduler} to be canceled/disposed before the underlying + * {@code Executor} is shut down. To avoid problems due to the {@code Executor} having a bounded-queue, + * it is recommended to rephrase the flow to utilize backpressure as the means to limit outstanding work. + * <p> + * This type of scheduler is less sensitive to leaking {@link io.reactivex.rxjava3.core.Scheduler.Worker Scheduler.Worker} instances, although + * not disposing a worker that has timed/delayed tasks not cancelled by other means may leak resources and/or + * execute those tasks "unexpectedly". + * <p> + * Note that this method returns a new {@code Scheduler} instance, even for the same {@code Executor} instance. + * <p> + * It is possible to wrap an {@code Executor} into a {@code Scheduler} without triggering the initialization of all the + * standard schedulers by using the {@link RxJavaPlugins#createExecutorScheduler(Executor, boolean, boolean)} method + * before the {@code Schedulers} class itself is accessed. + * + * @param executor + * the executor to wrap + * @param interruptibleWorker if {@code true}, the tasks submitted to the {@link io.reactivex.rxjava3.core.Scheduler.Worker Scheduler.Worker} will + * be interrupted when the task is disposed. + * @param fair if {@code true}, tasks submitted to the {@code Scheduler} or {@code Worker} will be executed by the underlying {@code Executor} one after the other, still + * in a FIFO and non-overlapping manner, but allows interleaving with other tasks submitted to the underlying {@code Executor}. + * If {@code false}, the underlying FIFO scheme will execute as many tasks as it can before giving up the underlying {@code Executor} thread. + * @return the new {@code Scheduler} wrapping the {@code Executor} + * @since 3.0.0 + */ + @NonNull + public static Scheduler from(@NonNull Executor executor, boolean interruptibleWorker, boolean fair) { + return RxJavaPlugins.createExecutorScheduler(executor, interruptibleWorker, fair); + } + + /** + * Shuts down the standard {@link Scheduler}s. + * <p>The operation is idempotent and thread-safe. + */ + public static void shutdown() { + computation().shutdown(); + io().shutdown(); + newThread().shutdown(); + single().shutdown(); + trampoline().shutdown(); + } + + /** + * Starts the standard {@link Scheduler}s. + * <p>The operation is idempotent and thread-safe. + */ + public static void start() { + computation().start(); + io().start(); + newThread().start(); + single().start(); + trampoline().start(); + } + + static final class IOTask implements Supplier<Scheduler> { + @Override + public Scheduler get() { + return IoHolder.DEFAULT; + } + } + + static final class NewThreadTask implements Supplier<Scheduler> { + @Override + public Scheduler get() { + return NewThreadHolder.DEFAULT; + } + } + + static final class SingleTask implements Supplier<Scheduler> { + @Override + public Scheduler get() { + return SingleHolder.DEFAULT; + } + } + + static final class ComputationTask implements Supplier<Scheduler> { + @Override + public Scheduler get() { + return ComputationHolder.DEFAULT; + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/schedulers/TestScheduler.java b/src/main/java/io/reactivex/rxjava3/schedulers/TestScheduler.java new file mode 100644 index 0000000000..33aca58a48 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/schedulers/TestScheduler.java @@ -0,0 +1,256 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.schedulers; + +import java.util.Queue; +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicReference; + +import io.reactivex.rxjava3.annotations.*; +import io.reactivex.rxjava3.core.Scheduler; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.disposables.EmptyDisposable; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +/** + * A special, non thread-safe scheduler for testing operators that require + * a scheduler without introducing real concurrency and allows manually advancing + * a virtual time. + * <p> + * By default, the tasks submitted via the various {@code schedule} methods are not + * wrapped by the {@link RxJavaPlugins#onSchedule(Runnable)} hook. To enable this behavior, + * create a {@code TestScheduler} via {@link #TestScheduler(boolean)} or {@link #TestScheduler(long, TimeUnit, boolean)}. + */ +public final class TestScheduler extends Scheduler { + /** The ordered queue for the runnable tasks. */ + final Queue<TimedRunnable> queue = new PriorityBlockingQueue<>(11); + /** Use the {@link RxJavaPlugins#onSchedule(Runnable)} hook when scheduling tasks. */ + final boolean useOnScheduleHook; + /** The per-scheduler global order counter. */ + long counter; + // Storing time in nanoseconds internally. + volatile long time; + + /** + * Creates a new TestScheduler with initial virtual time of zero. + */ + public TestScheduler() { + this(false); + } + + /** + * Creates a new TestScheduler with the option to use the + * {@link RxJavaPlugins#onSchedule(Runnable)} hook when scheduling tasks. + * <p>History: 3.0.10 - experimental + * @param useOnScheduleHook if {@code true}, the tasks submitted to this + * TestScheduler is wrapped via the + * {@link RxJavaPlugins#onSchedule(Runnable)} hook + * @since 3.1.0 + */ + public TestScheduler(boolean useOnScheduleHook) { + this.useOnScheduleHook = useOnScheduleHook; + } + + /** + * Creates a new TestScheduler with the specified initial virtual time. + * + * @param delayTime + * the point in time to move the Scheduler's clock to + * @param unit + * the units of time that {@code delayTime} is expressed in + */ + public TestScheduler(long delayTime, TimeUnit unit) { + this(delayTime, unit, false); + } + + /** + * Creates a new TestScheduler with the specified initial virtual time + * and with the option to use the + * {@link RxJavaPlugins#onSchedule(Runnable)} hook when scheduling tasks. + * <p>History: 3.0.10 - experimental + * @param delayTime + * the point in time to move the Scheduler's clock to + * @param unit + * the units of time that {@code delayTime} is expressed in + * @param useOnScheduleHook if {@code true}, the tasks submitted to this + * TestScheduler is wrapped via the + * {@link RxJavaPlugins#onSchedule(Runnable)} hook + * @since 3.1.0 + */ + public TestScheduler(long delayTime, TimeUnit unit, boolean useOnScheduleHook) { + time = unit.toNanos(delayTime); + this.useOnScheduleHook = useOnScheduleHook; + } + + static final class TimedRunnable implements Comparable<TimedRunnable> { + + final long time; + final Runnable run; + final TestWorker scheduler; + final long count; // for differentiating tasks at same time + + TimedRunnable(TestWorker scheduler, long time, Runnable run, long count) { + this.time = time; + this.run = run; + this.scheduler = scheduler; + this.count = count; + } + + @Override + public String toString() { + return String.format("TimedRunnable(time = %d, run = %s)", time, run.toString()); + } + + @Override + public int compareTo(TimedRunnable o) { + if (time == o.time) { + return Long.compare(count, o.count); + } + return Long.compare(time, o.time); + } + } + + @Override + public long now(@NonNull TimeUnit unit) { + return unit.convert(time, TimeUnit.NANOSECONDS); + } + + /** + * Moves the Scheduler's clock forward by a specified amount of time. + * + * @param delayTime + * the amount of time to move the Scheduler's clock forward + * @param unit + * the units of time that {@code delayTime} is expressed in + */ + public void advanceTimeBy(long delayTime, TimeUnit unit) { + advanceTimeTo(time + unit.toNanos(delayTime), TimeUnit.NANOSECONDS); + } + + /** + * Moves the Scheduler's clock to a particular moment in time. + * + * @param delayTime + * the point in time to move the Scheduler's clock to + * @param unit + * the units of time that {@code delayTime} is expressed in + */ + public void advanceTimeTo(long delayTime, TimeUnit unit) { + long targetTime = unit.toNanos(delayTime); + triggerActions(targetTime); + } + + /** + * Triggers any actions that have not yet been triggered and that are scheduled to be triggered at or + * before this Scheduler's present time. + */ + public void triggerActions() { + triggerActions(time); + } + + private void triggerActions(long targetTimeInNanoseconds) { + for (;;) { + TimedRunnable current = queue.peek(); + if (current == null || current.time > targetTimeInNanoseconds) { + break; + } + // if scheduled time is 0 (immediate) use current virtual time + time = current.time == 0 ? time : current.time; + queue.remove(current); + + // Only execute if not unsubscribed + if (!current.scheduler.disposed) { + current.run.run(); + } + } + time = targetTimeInNanoseconds; + } + + @NonNull + @Override + public Worker createWorker() { + return new TestWorker(); + } + + final class TestWorker extends Worker { + + volatile boolean disposed; + + @Override + public void dispose() { + disposed = true; + } + + @Override + public boolean isDisposed() { + return disposed; + } + + @NonNull + @Override + public Disposable schedule(@NonNull Runnable run, long delayTime, @NonNull TimeUnit unit) { + if (disposed) { + return EmptyDisposable.INSTANCE; + } + if (useOnScheduleHook) { + run = RxJavaPlugins.onSchedule(run); + } + final TimedRunnable timedAction = new TimedRunnable(this, time + unit.toNanos(delayTime), run, counter++); + queue.add(timedAction); + + return new QueueRemove(timedAction); + } + + @NonNull + @Override + public Disposable schedule(@NonNull Runnable run) { + if (disposed) { + return EmptyDisposable.INSTANCE; + } + if (useOnScheduleHook) { + run = RxJavaPlugins.onSchedule(run); + } + final TimedRunnable timedAction = new TimedRunnable(this, 0, run, counter++); + queue.add(timedAction); + return new QueueRemove(timedAction); + } + + @Override + public long now(@NonNull TimeUnit unit) { + return TestScheduler.this.now(unit); + } + + final class QueueRemove extends AtomicReference<TimedRunnable> implements Disposable { + + private static final long serialVersionUID = -7874968252110604360L; + + QueueRemove(TimedRunnable timedAction) { + this.lazySet(timedAction); + } + + @Override + public void dispose() { + TimedRunnable tr = getAndSet(null); + if (tr != null) { + queue.remove(tr); + } + } + + @Override + public boolean isDisposed() { + return get() == null; + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/schedulers/Timed.java b/src/main/java/io/reactivex/rxjava3/schedulers/Timed.java new file mode 100644 index 0000000000..3ef1940984 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/schedulers/Timed.java @@ -0,0 +1,102 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.schedulers; + +import java.util.Objects; +import java.util.concurrent.TimeUnit; + +import io.reactivex.rxjava3.annotations.NonNull; + +/** + * Holds onto a value along with time information. + * + * @param <T> the value type + */ +public final class Timed<T> { + final T value; + final long time; + final TimeUnit unit; + + /** + * Constructs a {@code Timed} instance with the given value and time information. + * @param value the value to hold + * @param time the time to hold + * @param unit the time unit, not null + * @throws NullPointerException if {@code value} or {@code unit} is {@code null} + */ + public Timed(@NonNull T value, long time, @NonNull TimeUnit unit) { + this.value = Objects.requireNonNull(value, "value is null"); + this.time = time; + this.unit = Objects.requireNonNull(unit, "unit is null"); + } + + /** + * Returns the contained value. + * @return the contained value + */ + @NonNull + public T value() { + return value; + } + + /** + * Returns the time unit of the contained time. + * @return the time unit of the contained time + */ + @NonNull + public TimeUnit unit() { + return unit; + } + + /** + * Returns the time value. + * @return the time value + */ + public long time() { + return time; + } + + /** + * Returns the contained time value in the time unit specified. + * @param unit the time unit + * @return the converted time + */ + public long time(@NonNull TimeUnit unit) { + return unit.convert(time, this.unit); + } + + @Override + public boolean equals(Object other) { + if (other instanceof Timed) { + Timed<?> o = (Timed<?>) other; + return Objects.equals(value, o.value) + && time == o.time + && Objects.equals(unit, o.unit); + } + return false; + } + + @Override + public int hashCode() { + int h = value.hashCode(); + h = h * 31 + (int)((time >>> 31) ^ time); + h = h * 31 + unit.hashCode(); + return h; + } + + @Override + public String toString() { + return "Timed[time=" + time + ", unit=" + unit + ", value=" + value + "]"; + } +} diff --git a/src/main/java/io/reactivex/rxjava3/schedulers/package-info.java b/src/main/java/io/reactivex/rxjava3/schedulers/package-info.java new file mode 100644 index 0000000000..1f96d0fd2a --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/schedulers/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +/** + * Contains notably the factory class of {@link io.reactivex.rxjava3.schedulers.Schedulers Schedulers} providing methods for + * retrieving the standard scheduler instances, the {@link io.reactivex.rxjava3.schedulers.TestScheduler TestScheduler} for testing flows + * with scheduling in a controlled manner and the class {@link io.reactivex.rxjava3.schedulers.Timed Timed} that can hold + * a value and a timestamp associated with it. + */ +package io.reactivex.rxjava3.schedulers; diff --git a/src/main/java/io/reactivex/rxjava3/subjects/AsyncSubject.java b/src/main/java/io/reactivex/rxjava3/subjects/AsyncSubject.java new file mode 100644 index 0000000000..dd4957fd39 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/subjects/AsyncSubject.java @@ -0,0 +1,358 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.subjects; + +import java.util.concurrent.atomic.AtomicReference; + +import io.reactivex.rxjava3.annotations.*; +import io.reactivex.rxjava3.core.Observer; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.observers.DeferredScalarDisposable; +import io.reactivex.rxjava3.internal.util.ExceptionHelper; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +/** + * A Subject that emits the very last value followed by a completion event or the received error to Observers. + * <p> + * <img width="640" height="239" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/AsyncSubject.png" alt=""> + * <p> + * This subject does not have a public constructor by design; a new empty instance of this + * {@code AsyncSubject} can be created via the {@link #create()} method. + * <p> + * Since a {@code Subject} is conceptionally derived from the {@code Processor} type in the Reactive Streams specification, + * {@code null}s are not allowed (<a href="https://github.com/reactive-streams/reactive-streams-jvm#2.13">Rule 2.13</a>) + * as parameters to {@link #onNext(Object)} and {@link #onError(Throwable)}. Such calls will result in a + * {@link NullPointerException} being thrown and the subject's state is not changed. + * <p> + * Since an {@code AsyncSubject} is an {@link io.reactivex.rxjava3.core.Observable}, it does not support backpressure. + * <p> + * When this {@code AsyncSubject} is terminated via {@link #onError(Throwable)}, the + * last observed item (if any) is cleared and late {@link io.reactivex.rxjava3.core.Observer}s only receive + * the {@code onError} event. + * <p> + * The {@code AsyncSubject} caches the latest item internally and it emits this item only when {@code onComplete} is called. + * Therefore, it is not recommended to use this {@code Subject} with infinite or never-completing sources. + * <p> + * Even though {@code AsyncSubject} implements the {@code Observer} interface, calling + * {@code onSubscribe} is not required (<a href="https://github.com/reactive-streams/reactive-streams-jvm#2.12">Rule 2.12</a>) + * if the subject is used as a standalone source. However, calling {@code onSubscribe} + * after the {@code AsyncSubject} reached its terminal state will result in the + * given {@code Disposable} being disposed immediately. + * <p> + * Calling {@link #onNext(Object)}, {@link #onError(Throwable)} and {@link #onComplete()} + * is required to be serialized (called from the same thread or called non-overlappingly from different threads + * through external means of serialization). The {@link #toSerialized()} method available to all {@code Subject}s + * provides such serialization and also protects against reentrance (i.e., when a downstream {@code Observer} + * consuming this subject also wants to call {@link #onNext(Object)} on this subject recursively). + * The implementation of onXXX methods are technically thread-safe but non-serialized calls + * to them may lead to undefined state in the currently subscribed Observers. + * <p> + * This {@code AsyncSubject} supports the standard state-peeking methods {@link #hasComplete()}, {@link #hasThrowable()}, + * {@link #getThrowable()} and {@link #hasObservers()} as well as means to read the very last observed value - + * after this {@code AsyncSubject} has been completed - in a non-blocking and thread-safe + * manner via {@link #hasValue()} or {@link #getValue()}. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code AsyncSubject} does not operate by default on a particular {@link io.reactivex.rxjava3.core.Scheduler} and + * the {@code Observer}s get notified on the thread where the terminating {@code onError} or {@code onComplete} + * methods were invoked.</dd> + * <dt><b>Error handling:</b></dt> + * <dd>When the {@link #onError(Throwable)} is called, the {@code AsyncSubject} enters into a terminal state + * and emits the same {@code Throwable} instance to the last set of {@code Observer}s. During this emission, + * if one or more {@code Observer}s dispose their respective {@code Disposable}s, the + * {@code Throwable} is delivered to the global error handler via + * {@link io.reactivex.rxjava3.plugins.RxJavaPlugins#onError(Throwable)} (multiple times if multiple {@code Observer}s + * cancel at once). + * If there were no {@code Observer}s subscribed to this {@code AsyncSubject} when the {@code onError()} + * was called, the global error handler is not invoked. + * </dd> + * </dl> + * <p> + * Example usage: + * <pre><code> + * AsyncSubject<Object> subject = AsyncSubject.create(); + * + * TestObserver<Object> to1 = subject.test(); + * + * to1.assertEmpty(); + * + * subject.onNext(1); + * + * // AsyncSubject only emits when onComplete was called. + * to1.assertEmpty(); + * + * subject.onNext(2); + * subject.onComplete(); + * + * // onComplete triggers the emission of the last cached item and the onComplete event. + * to1.assertResult(2); + * + * TestObserver<Object> to2 = subject.test(); + * + * // late Observers receive the last cached item too + * to2.assertResult(2); + * </code></pre> + * @param <T> the value type + */ +public final class AsyncSubject<T> extends Subject<T> { + + @SuppressWarnings("rawtypes") + static final AsyncDisposable[] EMPTY = new AsyncDisposable[0]; + + @SuppressWarnings("rawtypes") + static final AsyncDisposable[] TERMINATED = new AsyncDisposable[0]; + + final AtomicReference<AsyncDisposable<T>[]> subscribers; + + /** Write before updating subscribers, read after reading subscribers as TERMINATED. */ + Throwable error; + + /** Write before updating subscribers, read after reading subscribers as TERMINATED. */ + T value; + + /** + * Creates a new AsyncProcessor. + * @param <T> the value type to be received and emitted + * @return the new AsyncProcessor instance + */ + @CheckReturnValue + @NonNull + public static <T> AsyncSubject<T> create() { + return new AsyncSubject<>(); + } + + /** + * Constructs an AsyncSubject. + * @since 2.0 + */ + @SuppressWarnings("unchecked") + AsyncSubject() { + this.subscribers = new AtomicReference<>(EMPTY); + } + + @Override + public void onSubscribe(Disposable d) { + if (subscribers.get() == TERMINATED) { + d.dispose(); + } + } + + @Override + public void onNext(T t) { + ExceptionHelper.nullCheck(t, "onNext called with a null value."); + if (subscribers.get() == TERMINATED) { + return; + } + value = t; + } + + @SuppressWarnings("unchecked") + @Override + public void onError(Throwable t) { + ExceptionHelper.nullCheck(t, "onError called with a null Throwable."); + if (subscribers.get() == TERMINATED) { + RxJavaPlugins.onError(t); + return; + } + value = null; + error = t; + for (AsyncDisposable<T> as : subscribers.getAndSet(TERMINATED)) { + as.onError(t); + } + } + + @SuppressWarnings("unchecked") + @Override + public void onComplete() { + if (subscribers.get() == TERMINATED) { + return; + } + T v = value; + AsyncDisposable<T>[] array = subscribers.getAndSet(TERMINATED); + if (v == null) { + for (AsyncDisposable<T> as : array) { + as.onComplete(); + } + } else { + for (AsyncDisposable<T> as : array) { + as.complete(v); + } + } + } + + @Override + @CheckReturnValue + public boolean hasObservers() { + return subscribers.get().length != 0; + } + + @Override + @CheckReturnValue + public boolean hasThrowable() { + return subscribers.get() == TERMINATED && error != null; + } + + @Override + @CheckReturnValue + public boolean hasComplete() { + return subscribers.get() == TERMINATED && error == null; + } + + @Override + @CheckReturnValue + public Throwable getThrowable() { + return subscribers.get() == TERMINATED ? error : null; + } + + @Override + protected void subscribeActual(Observer<? super T> observer) { + AsyncDisposable<T> as = new AsyncDisposable<>(observer, this); + observer.onSubscribe(as); + if (add(as)) { + if (as.isDisposed()) { + remove(as); + } + } else { + Throwable ex = error; + if (ex != null) { + observer.onError(ex); + } else { + T v = value; + if (v != null) { + as.complete(v); + } else { + as.onComplete(); + } + } + } + } + + /** + * Tries to add the given subscriber to the subscribers array atomically + * or returns false if the subject has terminated. + * @param ps the subscriber to add + * @return true if successful, false if the subject has terminated + */ + boolean add(AsyncDisposable<T> ps) { + for (;;) { + AsyncDisposable<T>[] a = subscribers.get(); + if (a == TERMINATED) { + return false; + } + + int n = a.length; + @SuppressWarnings("unchecked") + AsyncDisposable<T>[] b = new AsyncDisposable[n + 1]; + System.arraycopy(a, 0, b, 0, n); + b[n] = ps; + + if (subscribers.compareAndSet(a, b)) { + return true; + } + } + } + + /** + * Atomically removes the given subscriber if it is subscribed to the subject. + * @param ps the subject to remove + */ + @SuppressWarnings("unchecked") + void remove(AsyncDisposable<T> ps) { + for (;;) { + AsyncDisposable<T>[] a = subscribers.get(); + int n = a.length; + if (n == 0) { + return; + } + + int j = -1; + for (int i = 0; i < n; i++) { + if (a[i] == ps) { + j = i; + break; + } + } + + if (j < 0) { + return; + } + + AsyncDisposable<T>[] b; + + if (n == 1) { + b = EMPTY; + } else { + b = new AsyncDisposable[n - 1]; + System.arraycopy(a, 0, b, 0, j); + System.arraycopy(a, j + 1, b, j, n - j - 1); + } + if (subscribers.compareAndSet(a, b)) { + return; + } + } + } + + /** + * Returns true if the subject has any value. + * <p>The method is thread-safe. + * @return true if the subject has any value + */ + @CheckReturnValue + public boolean hasValue() { + return subscribers.get() == TERMINATED && value != null; + } + + /** + * Returns a single value the Subject currently has or null if no such value exists. + * <p>The method is thread-safe. + * @return a single value the Subject currently has or null if no such value exists + */ + @Nullable + @CheckReturnValue + public T getValue() { + return subscribers.get() == TERMINATED ? value : null; + } + + static final class AsyncDisposable<T> extends DeferredScalarDisposable<T> { + private static final long serialVersionUID = 5629876084736248016L; + + final AsyncSubject<T> parent; + + AsyncDisposable(Observer<? super T> actual, AsyncSubject<T> parent) { + super(actual); + this.parent = parent; + } + + @Override + public void dispose() { + if (super.tryDispose()) { + parent.remove(this); + } + } + + void onComplete() { + if (!isDisposed()) { + downstream.onComplete(); + } + } + + void onError(Throwable t) { + if (isDisposed()) { + RxJavaPlugins.onError(t); + } else { + downstream.onError(t); + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/subjects/BehaviorSubject.java b/src/main/java/io/reactivex/rxjava3/subjects/BehaviorSubject.java new file mode 100644 index 0000000000..2b19ecdd26 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/subjects/BehaviorSubject.java @@ -0,0 +1,529 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.subjects; + +import java.util.Objects; +import java.util.concurrent.atomic.AtomicReference; +import java.util.concurrent.locks.*; + +import io.reactivex.rxjava3.annotations.*; +import io.reactivex.rxjava3.core.Observer; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.util.*; +import io.reactivex.rxjava3.internal.util.AppendOnlyLinkedArrayList.NonThrowingPredicate; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +/** + * Subject that emits the most recent item it has observed and all subsequent observed items to each subscribed + * {@link Observer}. + * <p> + * <img width="640" height="415" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/S.BehaviorSubject.v3.png" alt=""> + * <p> + * This subject does not have a public constructor by design; a new empty instance of this + * {@code BehaviorSubject} can be created via the {@link #create()} method and + * a new non-empty instance can be created via {@link #createDefault(Object)} (named as such to avoid + * overload resolution conflict with {@code Observable.create} that creates an Observable, not a {@code BehaviorSubject}). + * <p> + * Since a {@code Subject} is conceptionally derived from the {@code Processor} type in the Reactive Streams specification, + * {@code null}s are not allowed (<a href="https://github.com/reactive-streams/reactive-streams-jvm#2.13">Rule 2.13</a>) as + * default initial values in {@link #createDefault(Object)} or as parameters to {@link #onNext(Object)} and + * {@link #onError(Throwable)}. Such calls will result in a + * {@link NullPointerException} being thrown and the subject's state is not changed. + * <p> + * Since a {@code BehaviorSubject} is an {@link io.reactivex.rxjava3.core.Observable}, it does not support backpressure. + * <p> + * When this {@code BehaviorSubject} is terminated via {@link #onError(Throwable)} or {@link #onComplete()}, the + * last observed item (if any) is cleared and late {@link io.reactivex.rxjava3.core.Observer}s only receive + * the respective terminal event. + * <p> + * The {@code BehaviorSubject} does not support clearing its cached value (to appear empty again), however, the + * effect can be achieved by using a special item and making sure {@code Observer}s subscribe through a + * filter whose predicate filters out this special item: + * <pre><code> + * BehaviorSubject<Integer> subject = BehaviorSubject.create(); + * + * final Integer EMPTY = Integer.MIN_VALUE; + * + * Observable<Integer> observable = subject.filter(v -> v != EMPTY); + * + * TestObserver<Integer> to1 = observable.test(); + * + * subject.onNext(1); + * // this will "clear" the cache + * subject.onNext(EMPTY); + * + * TestObserver<Integer> to2 = observable.test(); + * + * subject.onNext(2); + * subject.onComplete(); + * + * // to1 received both non-empty items + * to1.assertResult(1, 2); + * + * // to2 received only 2 even though the current item was EMPTY + * // when it got subscribed + * to2.assertResult(2); + * + * // Observers coming after the subject was terminated receive + * // no items and only the onComplete event in this case. + * observable.test().assertResult(); + * </code></pre> + * <p> + * Even though {@code BehaviorSubject} implements the {@code Observer} interface, calling + * {@code onSubscribe} is not required (<a href="https://github.com/reactive-streams/reactive-streams-jvm#2.12">Rule 2.12</a>) + * if the subject is used as a standalone source. However, calling {@code onSubscribe} + * after the {@code BehaviorSubject} reached its terminal state will result in the + * given {@code Disposable} being disposed immediately. + * <p> + * Calling {@link #onNext(Object)}, {@link #onError(Throwable)} and {@link #onComplete()} + * is required to be serialized (called from the same thread or called non-overlappingly from different threads + * through external means of serialization). The {@link #toSerialized()} method available to all {@code Subject}s + * provides such serialization and also protects against reentrance (i.e., when a downstream {@code Observer} + * consuming this subject also wants to call {@link #onNext(Object)} on this subject recursively). + * <p> + * This {@code BehaviorSubject} supports the standard state-peeking methods {@link #hasComplete()}, {@link #hasThrowable()}, + * {@link #getThrowable()} and {@link #hasObservers()} as well as means to read the latest observed value + * in a non-blocking and thread-safe manner via {@link #hasValue()} or {@link #getValue()}. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code BehaviorSubject} does not operate by default on a particular {@link io.reactivex.rxjava3.core.Scheduler} and + * the {@code Observer}s get notified on the thread the respective {@code onXXX} methods were invoked.</dd> + * <dt><b>Error handling:</b></dt> + * <dd>When the {@link #onError(Throwable)} is called, the {@code BehaviorSubject} enters into a terminal state + * and emits the same {@code Throwable} instance to the last set of {@code Observer}s. During this emission, + * if one or more {@code Observer}s dispose their respective {@code Disposable}s, the + * {@code Throwable} is delivered to the global error handler via + * {@link io.reactivex.rxjava3.plugins.RxJavaPlugins#onError(Throwable)} (multiple times if multiple {@code Observer}s + * cancel at once). + * If there were no {@code Observer}s subscribed to this {@code BehaviorSubject} when the {@code onError()} + * was called, the global error handler is not invoked. + * </dd> + * </dl> + * <p> + * Example usage: + * <pre> {@code + + // observer will receive all 4 events (including "default"). + BehaviorSubject<Object> subject = BehaviorSubject.createDefault("default"); + subject.subscribe(observer); + subject.onNext("one"); + subject.onNext("two"); + subject.onNext("three"); + + // observer will receive the "one", "two" and "three" events, but not "zero" + BehaviorSubject<Object> subject = BehaviorSubject.create(); + subject.onNext("zero"); + subject.onNext("one"); + subject.subscribe(observer); + subject.onNext("two"); + subject.onNext("three"); + + // observer will receive only onComplete + BehaviorSubject<Object> subject = BehaviorSubject.create(); + subject.onNext("zero"); + subject.onNext("one"); + subject.onComplete(); + subject.subscribe(observer); + + // observer will receive only onError + BehaviorSubject<Object> subject = BehaviorSubject.create(); + subject.onNext("zero"); + subject.onNext("one"); + subject.onError(new RuntimeException("error")); + subject.subscribe(observer); + } </pre> + * + * @param <T> + * the type of item expected to be observed by the Subject + */ +public final class BehaviorSubject<T> extends Subject<T> { + + final AtomicReference<Object> value; + + final AtomicReference<BehaviorDisposable<T>[]> observers; + + @SuppressWarnings("rawtypes") + static final BehaviorDisposable[] EMPTY = new BehaviorDisposable[0]; + + @SuppressWarnings("rawtypes") + static final BehaviorDisposable[] TERMINATED = new BehaviorDisposable[0]; + final ReadWriteLock lock; + final Lock readLock; + final Lock writeLock; + + final AtomicReference<Throwable> terminalEvent; + + long index; + + /** + * Creates a {@link BehaviorSubject} without a default item. + * + * @param <T> + * the type of item the Subject will emit + * @return the constructed {@link BehaviorSubject} + */ + @CheckReturnValue + @NonNull + public static <T> BehaviorSubject<T> create() { + return new BehaviorSubject<>(null); + } + + /** + * Creates a {@link BehaviorSubject} that emits the last item it observed and all subsequent items to each + * {@link Observer} that subscribes to it. + * + * @param <T> + * the type of item the Subject will emit + * @param defaultValue + * the item that will be emitted first to any {@link Observer} as long as the + * {@link BehaviorSubject} has not yet observed any items from its source {@code Observable} + * @return the constructed {@link BehaviorSubject} + * @throws NullPointerException if {@code defaultValue} is {@code null} + */ + @CheckReturnValue + @NonNull + public static <@NonNull T> BehaviorSubject<T> createDefault(T defaultValue) { + Objects.requireNonNull(defaultValue, "defaultValue is null"); + return new BehaviorSubject<>(defaultValue); + } + + /** + * Constructs an empty BehaviorSubject. + * @param defaultValue the initial value, not null (verified) + * @since 2.0 + */ + @SuppressWarnings("unchecked") + BehaviorSubject(T defaultValue) { + this.lock = new ReentrantReadWriteLock(); + this.readLock = lock.readLock(); + this.writeLock = lock.writeLock(); + this.observers = new AtomicReference<>(EMPTY); + this.value = new AtomicReference<>(defaultValue); + this.terminalEvent = new AtomicReference<>(); + } + + @Override + protected void subscribeActual(Observer<? super T> observer) { + BehaviorDisposable<T> bs = new BehaviorDisposable<>(observer, this); + observer.onSubscribe(bs); + if (add(bs)) { + if (bs.cancelled) { + remove(bs); + } else { + bs.emitFirst(); + } + } else { + Throwable ex = terminalEvent.get(); + if (ex == ExceptionHelper.TERMINATED) { + observer.onComplete(); + } else { + observer.onError(ex); + } + } + } + + @Override + public void onSubscribe(Disposable d) { + if (terminalEvent.get() != null) { + d.dispose(); + } + } + + @Override + public void onNext(T t) { + ExceptionHelper.nullCheck(t, "onNext called with a null value."); + + if (terminalEvent.get() != null) { + return; + } + Object o = NotificationLite.next(t); + setCurrent(o); + for (BehaviorDisposable<T> bs : observers.get()) { + bs.emitNext(o, index); + } + } + + @Override + public void onError(Throwable t) { + ExceptionHelper.nullCheck(t, "onError called with a null Throwable."); + if (!terminalEvent.compareAndSet(null, t)) { + RxJavaPlugins.onError(t); + return; + } + Object o = NotificationLite.error(t); + for (BehaviorDisposable<T> bs : terminate(o)) { + bs.emitNext(o, index); + } + } + + @Override + public void onComplete() { + if (!terminalEvent.compareAndSet(null, ExceptionHelper.TERMINATED)) { + return; + } + Object o = NotificationLite.complete(); + for (BehaviorDisposable<T> bs : terminate(o)) { + bs.emitNext(o, index); // relaxed read okay since this is the only mutator thread + } + } + + @Override + @CheckReturnValue + public boolean hasObservers() { + return observers.get().length != 0; + } + + @CheckReturnValue + /* test support*/ int subscriberCount() { + return observers.get().length; + } + + @Override + @Nullable + @CheckReturnValue + public Throwable getThrowable() { + Object o = value.get(); + if (NotificationLite.isError(o)) { + return NotificationLite.getError(o); + } + return null; + } + + /** + * Returns a single value the Subject currently has or null if no such value exists. + * <p>The method is thread-safe. + * @return a single value the Subject currently has or null if no such value exists + */ + @Nullable + @CheckReturnValue + public T getValue() { + Object o = value.get(); + if (NotificationLite.isComplete(o) || NotificationLite.isError(o)) { + return null; + } + return NotificationLite.getValue(o); + } + + @Override + @CheckReturnValue + public boolean hasComplete() { + Object o = value.get(); + return NotificationLite.isComplete(o); + } + + @Override + @CheckReturnValue + public boolean hasThrowable() { + Object o = value.get(); + return NotificationLite.isError(o); + } + + /** + * Returns true if the subject has any value. + * <p>The method is thread-safe. + * @return true if the subject has any value + */ + @CheckReturnValue + public boolean hasValue() { + Object o = value.get(); + return o != null && !NotificationLite.isComplete(o) && !NotificationLite.isError(o); + } + + boolean add(BehaviorDisposable<T> rs) { + for (;;) { + BehaviorDisposable<T>[] a = observers.get(); + if (a == TERMINATED) { + return false; + } + int len = a.length; + @SuppressWarnings("unchecked") + BehaviorDisposable<T>[] b = new BehaviorDisposable[len + 1]; + System.arraycopy(a, 0, b, 0, len); + b[len] = rs; + if (observers.compareAndSet(a, b)) { + return true; + } + } + } + + @SuppressWarnings("unchecked") + void remove(BehaviorDisposable<T> rs) { + for (;;) { + BehaviorDisposable<T>[] a = observers.get(); + int len = a.length; + if (len == 0) { + return; + } + int j = -1; + for (int i = 0; i < len; i++) { + if (a[i] == rs) { + j = i; + break; + } + } + + if (j < 0) { + return; + } + BehaviorDisposable<T>[] b; + if (len == 1) { + b = EMPTY; + } else { + b = new BehaviorDisposable[len - 1]; + System.arraycopy(a, 0, b, 0, j); + System.arraycopy(a, j + 1, b, j, len - j - 1); + } + if (observers.compareAndSet(a, b)) { + return; + } + } + } + + @SuppressWarnings("unchecked") + BehaviorDisposable<T>[] terminate(Object terminalValue) { + + setCurrent(terminalValue); + + return observers.getAndSet(TERMINATED); + } + + void setCurrent(Object o) { + writeLock.lock(); + index++; + value.lazySet(o); + writeLock.unlock(); + } + + static final class BehaviorDisposable<T> implements Disposable, NonThrowingPredicate<Object> { + + final Observer<? super T> downstream; + final BehaviorSubject<T> state; + + boolean next; + boolean emitting; + AppendOnlyLinkedArrayList<Object> queue; + + boolean fastPath; + + volatile boolean cancelled; + + long index; + + BehaviorDisposable(Observer<? super T> actual, BehaviorSubject<T> state) { + this.downstream = actual; + this.state = state; + } + + @Override + public void dispose() { + if (!cancelled) { + cancelled = true; + + state.remove(this); + } + } + + @Override + public boolean isDisposed() { + return cancelled; + } + + void emitFirst() { + if (cancelled) { + return; + } + Object o; + synchronized (this) { + if (cancelled) { + return; + } + if (next) { + return; + } + + BehaviorSubject<T> s = state; + Lock lock = s.readLock; + + lock.lock(); + index = s.index; + o = s.value.get(); + lock.unlock(); + + emitting = o != null; + next = true; + } + + if (o != null) { + if (test(o)) { + return; + } + + emitLoop(); + } + } + + void emitNext(Object value, long stateIndex) { + if (cancelled) { + return; + } + if (!fastPath) { + synchronized (this) { + if (cancelled) { + return; + } + if (index == stateIndex) { + return; + } + if (emitting) { + AppendOnlyLinkedArrayList<Object> q = queue; + if (q == null) { + q = new AppendOnlyLinkedArrayList<>(4); + queue = q; + } + q.add(value); + return; + } + next = true; + } + fastPath = true; + } + + test(value); + } + + @Override + public boolean test(Object o) { + return cancelled || NotificationLite.accept(o, downstream); + } + + void emitLoop() { + for (;;) { + if (cancelled) { + return; + } + AppendOnlyLinkedArrayList<Object> q; + synchronized (this) { + q = queue; + if (q == null) { + emitting = false; + return; + } + queue = null; + } + + q.forEachWhile(this); + } + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/subjects/CompletableSubject.java b/src/main/java/io/reactivex/rxjava3/subjects/CompletableSubject.java new file mode 100644 index 0000000000..57be7dbced --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/subjects/CompletableSubject.java @@ -0,0 +1,281 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.subjects; + +import java.util.concurrent.atomic.*; + +import io.reactivex.rxjava3.annotations.*; +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.util.ExceptionHelper; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +/** + * Represents a hot Completable-like source and consumer of events similar to Subjects. + * <p> + * <img width="640" height="243" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/CompletableSubject.png" alt=""> + * <p> + * This subject does not have a public constructor by design; a new non-terminated instance of this + * {@code CompletableSubject} can be created via the {@link #create()} method. + * <p> + * Since the {@code CompletableSubject} is conceptionally derived from the {@code Processor} type in the Reactive Streams specification, + * {@code null}s are not allowed (<a href="https://github.com/reactive-streams/reactive-streams-jvm#2.13">Rule 2.13</a>) + * as parameters to {@link #onError(Throwable)}. + * <p> + * Even though {@code CompletableSubject} implements the {@code CompletableObserver} interface, calling + * {@code onSubscribe} is not required (<a href="https://github.com/reactive-streams/reactive-streams-jvm#2.12">Rule 2.12</a>) + * if the subject is used as a standalone source. However, calling {@code onSubscribe} + * after the {@code CompletableSubject} reached its terminal state will result in the + * given {@code Disposable} being disposed immediately. + * <p> + * All methods are thread safe. Calling {@link #onComplete()} multiple + * times has no effect. Calling {@link #onError(Throwable)} multiple times relays the {@code Throwable} to + * the {@link io.reactivex.rxjava3.plugins.RxJavaPlugins#onError(Throwable)} global error handler. + * <p> + * This {@code CompletableSubject} supports the standard state-peeking methods {@link #hasComplete()}, + * {@link #hasThrowable()}, {@link #getThrowable()} and {@link #hasObservers()}. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code CompletableSubject} does not operate by default on a particular {@link io.reactivex.rxjava3.core.Scheduler} and + * the {@code CompletableObserver}s get notified on the thread where the terminating {@code onError} or {@code onComplete} + * methods were invoked.</dd> + * <dt><b>Error handling:</b></dt> + * <dd>When the {@link #onError(Throwable)} is called, the {@code CompletableSubject} enters into a terminal state + * and emits the same {@code Throwable} instance to the last set of {@code CompletableObserver}s. During this emission, + * if one or more {@code CompletableObserver}s dispose their respective {@code Disposable}s, the + * {@code Throwable} is delivered to the global error handler via + * {@link io.reactivex.rxjava3.plugins.RxJavaPlugins#onError(Throwable)} (multiple times if multiple {@code CompletableObserver}s + * cancel at once). + * If there were no {@code CompletableObserver}s subscribed to this {@code CompletableSubject} when the {@code onError()} + * was called, the global error handler is not invoked. + * </dd> + * </dl> + * <p> + * Example usage: + * <pre><code> + * CompletableSubject subject = CompletableSubject.create(); + * + * TestObserver<Void> to1 = subject.test(); + * + * // a fresh CompletableSubject is empty + * to1.assertEmpty(); + * + * subject.onComplete(); + * + * // a CompletableSubject is always void of items + * to1.assertResult(); + * + * TestObserver<Void> to2 = subject.test() + * + * // late CompletableObservers receive the terminal event + * to2.assertResult(); + * </code></pre> + * <p>History: 2.0.5 - experimental + * @since 2.1 + */ +public final class CompletableSubject extends Completable implements CompletableObserver { + + final AtomicReference<CompletableDisposable[]> observers; + + static final CompletableDisposable[] EMPTY = new CompletableDisposable[0]; + + static final CompletableDisposable[] TERMINATED = new CompletableDisposable[0]; + + final AtomicBoolean once; + Throwable error; + + /** + * Creates a fresh CompletableSubject. + * @return the new CompletableSubject instance + */ + @CheckReturnValue + @NonNull + public static CompletableSubject create() { + return new CompletableSubject(); + } + + CompletableSubject() { + once = new AtomicBoolean(); + observers = new AtomicReference<>(EMPTY); + } + + @Override + public void onSubscribe(Disposable d) { + if (observers.get() == TERMINATED) { + d.dispose(); + } + } + + @Override + public void onError(Throwable e) { + ExceptionHelper.nullCheck(e, "onError called with a null Throwable."); + if (once.compareAndSet(false, true)) { + this.error = e; + for (CompletableDisposable md : observers.getAndSet(TERMINATED)) { + md.downstream.onError(e); + } + } else { + RxJavaPlugins.onError(e); + } + } + + @Override + public void onComplete() { + if (once.compareAndSet(false, true)) { + for (CompletableDisposable md : observers.getAndSet(TERMINATED)) { + md.downstream.onComplete(); + } + } + } + + @Override + protected void subscribeActual(CompletableObserver observer) { + CompletableDisposable md = new CompletableDisposable(observer, this); + observer.onSubscribe(md); + if (add(md)) { + if (md.isDisposed()) { + remove(md); + } + } else { + Throwable ex = error; + if (ex != null) { + observer.onError(ex); + } else { + observer.onComplete(); + } + } + } + + boolean add(CompletableDisposable inner) { + for (;;) { + CompletableDisposable[] a = observers.get(); + if (a == TERMINATED) { + return false; + } + + int n = a.length; + + CompletableDisposable[] b = new CompletableDisposable[n + 1]; + System.arraycopy(a, 0, b, 0, n); + b[n] = inner; + if (observers.compareAndSet(a, b)) { + return true; + } + } + } + + void remove(CompletableDisposable inner) { + for (;;) { + CompletableDisposable[] a = observers.get(); + int n = a.length; + if (n == 0) { + return; + } + + int j = -1; + + for (int i = 0; i < n; i++) { + if (a[i] == inner) { + j = i; + break; + } + } + + if (j < 0) { + return; + } + CompletableDisposable[] b; + if (n == 1) { + b = EMPTY; + } else { + b = new CompletableDisposable[n - 1]; + System.arraycopy(a, 0, b, 0, j); + System.arraycopy(a, j + 1, b, j, n - j - 1); + } + + if (observers.compareAndSet(a, b)) { + return; + } + } + } + + /** + * Returns the terminal error if this CompletableSubject has been terminated with an error, null otherwise. + * @return the terminal error or null if not terminated or not with an error + */ + @Nullable + public Throwable getThrowable() { + if (observers.get() == TERMINATED) { + return error; + } + return null; + } + + /** + * Returns true if this CompletableSubject has been terminated with an error. + * @return true if this CompletableSubject has been terminated with an error + */ + public boolean hasThrowable() { + return observers.get() == TERMINATED && error != null; + } + + /** + * Returns true if this CompletableSubject has been completed. + * @return true if this CompletableSubject has been completed + */ + public boolean hasComplete() { + return observers.get() == TERMINATED && error == null; + } + + /** + * Returns true if this CompletableSubject has observers. + * @return true if this CompletableSubject has observers + */ + public boolean hasObservers() { + return observers.get().length != 0; + } + + /** + * Returns the number of current observers. + * @return the number of current observers + */ + /* test */ int observerCount() { + return observers.get().length; + } + + static final class CompletableDisposable + extends AtomicReference<CompletableSubject> implements Disposable { + private static final long serialVersionUID = -7650903191002190468L; + + final CompletableObserver downstream; + + CompletableDisposable(CompletableObserver actual, CompletableSubject parent) { + this.downstream = actual; + lazySet(parent); + } + + @Override + public void dispose() { + CompletableSubject parent = getAndSet(null); + if (parent != null) { + parent.remove(this); + } + } + + @Override + public boolean isDisposed() { + return get() == null; + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/subjects/MaybeSubject.java b/src/main/java/io/reactivex/rxjava3/subjects/MaybeSubject.java new file mode 100644 index 0000000000..39dbb78d9c --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/subjects/MaybeSubject.java @@ -0,0 +1,351 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.subjects; + +import java.util.concurrent.atomic.*; + +import io.reactivex.rxjava3.annotations.*; +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.util.ExceptionHelper; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +/** + * Represents a hot Maybe-like source and consumer of events similar to Subjects. + * <p> + * <img width="640" height="164" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/MaybeSubject.png" alt=""> + * <p> + * This subject does not have a public constructor by design; a new non-terminated instance of this + * {@code MaybeSubject} can be created via the {@link #create()} method. + * <p> + * Since the {@code MaybeSubject} is conceptionally derived from the {@code Processor} type in the Reactive Streams specification, + * {@code null}s are not allowed (<a href="https://github.com/reactive-streams/reactive-streams-jvm#2.13">Rule 2.13</a>) + * as parameters to {@link #onSuccess(Object)} and {@link #onError(Throwable)}. Such calls will result in a + * {@link NullPointerException} being thrown and the subject's state is not changed. + * <p> + * Since a {@code MaybeSubject} is a {@link io.reactivex.rxjava3.core.Maybe}, calling {@code onSuccess}, {@code onError} + * or {@code onComplete} will move this {@code MaybeSubject} into its terminal state atomically. + * <p> + * All methods are thread safe. Calling {@link #onSuccess(Object)} or {@link #onComplete()} multiple + * times has no effect. Calling {@link #onError(Throwable)} multiple times relays the {@code Throwable} to + * the {@link io.reactivex.rxjava3.plugins.RxJavaPlugins#onError(Throwable)} global error handler. + * <p> + * Even though {@code MaybeSubject} implements the {@code MaybeObserver} interface, calling + * {@code onSubscribe} is not required (<a href="https://github.com/reactive-streams/reactive-streams-jvm#2.12">Rule 2.12</a>) + * if the subject is used as a standalone source. However, calling {@code onSubscribe} + * after the {@code MaybeSubject} reached its terminal state will result in the + * given {@code Disposable} being disposed immediately. + * <p> + * This {@code MaybeSubject} supports the standard state-peeking methods {@link #hasComplete()}, {@link #hasThrowable()}, + * {@link #getThrowable()} and {@link #hasObservers()} as well as means to read any success item in a non-blocking + * and thread-safe manner via {@link #hasValue()} and {@link #getValue()}. + * <p> + * The {@code MaybeSubject} does not support clearing its cached {@code onSuccess} value. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code MaybeSubject} does not operate by default on a particular {@link io.reactivex.rxjava3.core.Scheduler} and + * the {@code MaybeObserver}s get notified on the thread where the terminating {@code onSuccess}, {@code onError} or {@code onComplete} + * methods were invoked.</dd> + * <dt><b>Error handling:</b></dt> + * <dd>When the {@link #onError(Throwable)} is called, the {@code MaybeSubject} enters into a terminal state + * and emits the same {@code Throwable} instance to the last set of {@code MaybeObserver}s. During this emission, + * if one or more {@code MaybeObserver}s dispose their respective {@code Disposable}s, the + * {@code Throwable} is delivered to the global error handler via + * {@link io.reactivex.rxjava3.plugins.RxJavaPlugins#onError(Throwable)} (multiple times if multiple {@code MaybeObserver}s + * cancel at once). + * If there were no {@code MaybeObserver}s subscribed to this {@code MaybeSubject} when the {@code onError()} + * was called, the global error handler is not invoked. + * </dd> + * </dl> + * <p> + * Example usage: + * <pre><code> + * MaybeSubject<Integer> subject1 = MaybeSubject.create(); + * + * TestObserver<Integer> to1 = subject1.test(); + * + * // MaybeSubjects are empty by default + * to1.assertEmpty(); + * + * subject1.onSuccess(1); + * + * // onSuccess is a terminal event with MaybeSubjects + * // TestObserver converts onSuccess into onNext + onComplete + * to1.assertResult(1); + * + * TestObserver<Integer> to2 = subject1.test(); + * + * // late Observers receive the terminal signal (onSuccess) too + * to2.assertResult(1); + * + * // ----------------------------------------------------- + * + * MaybeSubject<Integer> subject2 = MaybeSubject.create(); + * + * TestObserver<Integer> to3 = subject2.test(); + * + * subject2.onComplete(); + * + * // a completed MaybeSubject completes its MaybeObservers + * to3.assertResult(); + * + * TestObserver<Integer> to4 = subject1.test(); + * + * // late Observers receive the terminal signal (onComplete) too + * to4.assertResult(); + * </code></pre> + * <p>History: 2.0.5 - experimental + * @param <T> the value type received and emitted + * @since 2.1 + */ +public final class MaybeSubject<T> extends Maybe<T> implements MaybeObserver<T> { + + final AtomicReference<MaybeDisposable<T>[]> observers; + + @SuppressWarnings("rawtypes") + static final MaybeDisposable[] EMPTY = new MaybeDisposable[0]; + + @SuppressWarnings("rawtypes") + static final MaybeDisposable[] TERMINATED = new MaybeDisposable[0]; + + final AtomicBoolean once; + T value; + Throwable error; + + /** + * Creates a fresh MaybeSubject. + * @param <T> the value type received and emitted + * @return the new MaybeSubject instance + */ + @CheckReturnValue + @NonNull + public static <T> MaybeSubject<T> create() { + return new MaybeSubject<>(); + } + + @SuppressWarnings("unchecked") + MaybeSubject() { + once = new AtomicBoolean(); + observers = new AtomicReference<>(EMPTY); + } + + @Override + public void onSubscribe(Disposable d) { + if (observers.get() == TERMINATED) { + d.dispose(); + } + } + + @SuppressWarnings("unchecked") + @Override + public void onSuccess(T value) { + ExceptionHelper.nullCheck(value, "onSuccess called with a null value."); + if (once.compareAndSet(false, true)) { + this.value = value; + for (MaybeDisposable<T> md : observers.getAndSet(TERMINATED)) { + md.downstream.onSuccess(value); + } + } + } + + @SuppressWarnings("unchecked") + @Override + public void onError(Throwable e) { + ExceptionHelper.nullCheck(e, "onError called with a null Throwable."); + if (once.compareAndSet(false, true)) { + this.error = e; + for (MaybeDisposable<T> md : observers.getAndSet(TERMINATED)) { + md.downstream.onError(e); + } + } else { + RxJavaPlugins.onError(e); + } + } + + @SuppressWarnings("unchecked") + @Override + public void onComplete() { + if (once.compareAndSet(false, true)) { + for (MaybeDisposable<T> md : observers.getAndSet(TERMINATED)) { + md.downstream.onComplete(); + } + } + } + + @Override + protected void subscribeActual(MaybeObserver<? super T> observer) { + MaybeDisposable<T> md = new MaybeDisposable<>(observer, this); + observer.onSubscribe(md); + if (add(md)) { + if (md.isDisposed()) { + remove(md); + } + } else { + Throwable ex = error; + if (ex != null) { + observer.onError(ex); + } else { + T v = value; + if (v == null) { + observer.onComplete(); + } else { + observer.onSuccess(v); + } + } + } + } + + boolean add(MaybeDisposable<T> inner) { + for (;;) { + MaybeDisposable<T>[] a = observers.get(); + if (a == TERMINATED) { + return false; + } + + int n = a.length; + @SuppressWarnings("unchecked") + MaybeDisposable<T>[] b = new MaybeDisposable[n + 1]; + System.arraycopy(a, 0, b, 0, n); + b[n] = inner; + if (observers.compareAndSet(a, b)) { + return true; + } + } + } + + @SuppressWarnings("unchecked") + void remove(MaybeDisposable<T> inner) { + for (;;) { + MaybeDisposable<T>[] a = observers.get(); + int n = a.length; + if (n == 0) { + return; + } + + int j = -1; + + for (int i = 0; i < n; i++) { + if (a[i] == inner) { + j = i; + break; + } + } + + if (j < 0) { + return; + } + MaybeDisposable<T>[] b; + if (n == 1) { + b = EMPTY; + } else { + b = new MaybeDisposable[n - 1]; + System.arraycopy(a, 0, b, 0, j); + System.arraycopy(a, j + 1, b, j, n - j - 1); + } + + if (observers.compareAndSet(a, b)) { + return; + } + } + } + + /** + * Returns the success value if this MaybeSubject was terminated with a success value. + * @return the success value or null + */ + @Nullable + public T getValue() { + if (observers.get() == TERMINATED) { + return value; + } + return null; + } + + /** + * Returns true if this MaybeSubject was terminated with a success value. + * @return true if this MaybeSubject was terminated with a success value + */ + public boolean hasValue() { + return observers.get() == TERMINATED && value != null; + } + + /** + * Returns the terminal error if this MaybeSubject has been terminated with an error, null otherwise. + * @return the terminal error or null if not terminated or not with an error + */ + @Nullable + public Throwable getThrowable() { + if (observers.get() == TERMINATED) { + return error; + } + return null; + } + + /** + * Returns true if this MaybeSubject has been terminated with an error. + * @return true if this MaybeSubject has been terminated with an error + */ + public boolean hasThrowable() { + return observers.get() == TERMINATED && error != null; + } + + /** + * Returns true if this MaybeSubject has been completed. + * @return true if this MaybeSubject has been completed + */ + public boolean hasComplete() { + return observers.get() == TERMINATED && value == null && error == null; + } + + /** + * Returns true if this MaybeSubject has observers. + * @return true if this MaybeSubject has observers + */ + public boolean hasObservers() { + return observers.get().length != 0; + } + + /** + * Returns the number of current observers. + * @return the number of current observers + */ + /* test */ int observerCount() { + return observers.get().length; + } + + static final class MaybeDisposable<T> + extends AtomicReference<MaybeSubject<T>> implements Disposable { + private static final long serialVersionUID = -7650903191002190468L; + + final MaybeObserver<? super T> downstream; + + MaybeDisposable(MaybeObserver<? super T> actual, MaybeSubject<T> parent) { + this.downstream = actual; + lazySet(parent); + } + + @Override + public void dispose() { + MaybeSubject<T> parent = getAndSet(null); + if (parent != null) { + parent.remove(this); + } + } + + @Override + public boolean isDisposed() { + return get() == null; + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/subjects/PublishSubject.java b/src/main/java/io/reactivex/rxjava3/subjects/PublishSubject.java new file mode 100644 index 0000000000..6c2d5ac033 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/subjects/PublishSubject.java @@ -0,0 +1,340 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.subjects; + +import java.util.concurrent.atomic.*; + +import io.reactivex.rxjava3.annotations.*; +import io.reactivex.rxjava3.core.Observer; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.util.ExceptionHelper; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +/** + * A Subject that emits (multicasts) items to currently subscribed {@link Observer}s and terminal events to current + * or late {@code Observer}s. + * <p> + * <img width="640" height="281" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/PublishSubject.png" alt=""> + * <p> + * This subject does not have a public constructor by design; a new empty instance of this + * {@code PublishSubject} can be created via the {@link #create()} method. + * <p> + * Since a {@code Subject} is conceptionally derived from the {@code Processor} type in the Reactive Streams specification, + * {@code null}s are not allowed (<a href="https://github.com/reactive-streams/reactive-streams-jvm#2.13">Rule 2.13</a>) as + * parameters to {@link #onNext(Object)} and {@link #onError(Throwable)}. Such calls will result in a + * {@link NullPointerException} being thrown and the subject's state is not changed. + * <p> + * Since a {@code PublishSubject} is an {@link io.reactivex.rxjava3.core.Observable}, it does not support backpressure. + * <p> + * When this {@code PublishSubject} is terminated via {@link #onError(Throwable)} or {@link #onComplete()}, + * late {@link io.reactivex.rxjava3.core.Observer}s only receive the respective terminal event. + * <p> + * Unlike a {@link BehaviorSubject}, a {@code PublishSubject} doesn't retain/cache items, therefore, a new + * {@code Observer} won't receive any past items. + * <p> + * Even though {@code PublishSubject} implements the {@code Observer} interface, calling + * {@code onSubscribe} is not required (<a href="https://github.com/reactive-streams/reactive-streams-jvm#2.12">Rule 2.12</a>) + * if the subject is used as a standalone source. However, calling {@code onSubscribe} + * after the {@code PublishSubject} reached its terminal state will result in the + * given {@code Disposable} being disposed immediately. + * <p> + * Calling {@link #onNext(Object)}, {@link #onError(Throwable)} and {@link #onComplete()} + * is required to be serialized (called from the same thread or called non-overlappingly from different threads + * through external means of serialization). The {@link #toSerialized()} method available to all {@code Subject}s + * provides such serialization and also protects against reentrance (i.e., when a downstream {@code Observer} + * consuming this subject also wants to call {@link #onNext(Object)} on this subject recursively). + * <p> + * This {@code PublishSubject} supports the standard state-peeking methods {@link #hasComplete()}, {@link #hasThrowable()}, + * {@link #getThrowable()} and {@link #hasObservers()}. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code PublishSubject} does not operate by default on a particular {@link io.reactivex.rxjava3.core.Scheduler} and + * the {@code Observer}s get notified on the thread the respective {@code onXXX} methods were invoked.</dd> + * <dt><b>Error handling:</b></dt> + * <dd>When the {@link #onError(Throwable)} is called, the {@code PublishSubject} enters into a terminal state + * and emits the same {@code Throwable} instance to the last set of {@code Observer}s. During this emission, + * if one or more {@code Observer}s dispose their respective {@code Disposable}s, the + * {@code Throwable} is delivered to the global error handler via + * {@link io.reactivex.rxjava3.plugins.RxJavaPlugins#onError(Throwable)} (multiple times if multiple {@code Observer}s + * cancel at once). + * If there were no {@code Observer}s subscribed to this {@code PublishSubject} when the {@code onError()} + * was called, the global error handler is not invoked. + * </dd> + * </dl> + * <p> + * Example usage: + * <pre> {@code + + PublishSubject<Object> subject = PublishSubject.create(); + // observer1 will receive all onNext and onComplete events + subject.subscribe(observer1); + subject.onNext("one"); + subject.onNext("two"); + // observer2 will only receive "three" and onComplete + subject.subscribe(observer2); + subject.onNext("three"); + subject.onComplete(); + + // late Observers only receive the terminal event + subject.test().assertEmpty(); + } </pre> + * + * @param <T> + * the type of items observed and emitted by the Subject + */ +public final class PublishSubject<T> extends Subject<T> { + /** The terminated indicator for the subscribers array. */ + @SuppressWarnings("rawtypes") + static final PublishDisposable[] TERMINATED = new PublishDisposable[0]; + /** An empty subscribers array to avoid allocating it all the time. */ + @SuppressWarnings("rawtypes") + static final PublishDisposable[] EMPTY = new PublishDisposable[0]; + + /** The array of currently subscribed subscribers. */ + final AtomicReference<PublishDisposable<T>[]> subscribers; + + /** The error, write before terminating and read after checking subscribers. */ + Throwable error; + + /** + * Constructs a PublishSubject. + * @param <T> the value type + * @return the new PublishSubject + */ + @CheckReturnValue + @NonNull + public static <T> PublishSubject<T> create() { + return new PublishSubject<>(); + } + + /** + * Constructs a PublishSubject. + * @since 2.0 + */ + @SuppressWarnings("unchecked") + PublishSubject() { + subscribers = new AtomicReference<>(EMPTY); + } + + @Override + protected void subscribeActual(Observer<? super T> t) { + PublishDisposable<T> ps = new PublishDisposable<>(t, this); + t.onSubscribe(ps); + if (add(ps)) { + // if cancellation happened while a successful add, the remove() didn't work + // so we need to do it again + if (ps.isDisposed()) { + remove(ps); + } + } else { + Throwable ex = error; + if (ex != null) { + t.onError(ex); + } else { + t.onComplete(); + } + } + } + + /** + * Tries to add the given subscriber to the subscribers array atomically + * or returns false if the subject has terminated. + * @param ps the subscriber to add + * @return true if successful, false if the subject has terminated + */ + boolean add(PublishDisposable<T> ps) { + for (;;) { + PublishDisposable<T>[] a = subscribers.get(); + if (a == TERMINATED) { + return false; + } + + int n = a.length; + @SuppressWarnings("unchecked") + PublishDisposable<T>[] b = new PublishDisposable[n + 1]; + System.arraycopy(a, 0, b, 0, n); + b[n] = ps; + + if (subscribers.compareAndSet(a, b)) { + return true; + } + } + } + + /** + * Atomically removes the given subscriber if it is subscribed to the subject. + * @param ps the subject to remove + */ + @SuppressWarnings("unchecked") + void remove(PublishDisposable<T> ps) { + for (;;) { + PublishDisposable<T>[] a = subscribers.get(); + if (a == TERMINATED || a == EMPTY) { + return; + } + + int n = a.length; + int j = -1; + for (int i = 0; i < n; i++) { + if (a[i] == ps) { + j = i; + break; + } + } + + if (j < 0) { + return; + } + + PublishDisposable<T>[] b; + + if (n == 1) { + b = EMPTY; + } else { + b = new PublishDisposable[n - 1]; + System.arraycopy(a, 0, b, 0, j); + System.arraycopy(a, j + 1, b, j, n - j - 1); + } + if (subscribers.compareAndSet(a, b)) { + return; + } + } + } + + @Override + public void onSubscribe(Disposable d) { + if (subscribers.get() == TERMINATED) { + d.dispose(); + } + } + + @Override + public void onNext(T t) { + ExceptionHelper.nullCheck(t, "onNext called with a null value."); + for (PublishDisposable<T> pd : subscribers.get()) { + pd.onNext(t); + } + } + + @SuppressWarnings("unchecked") + @Override + public void onError(Throwable t) { + ExceptionHelper.nullCheck(t, "onError called with a null Throwable."); + if (subscribers.get() == TERMINATED) { + RxJavaPlugins.onError(t); + return; + } + error = t; + + for (PublishDisposable<T> pd : subscribers.getAndSet(TERMINATED)) { + pd.onError(t); + } + } + + @SuppressWarnings("unchecked") + @Override + public void onComplete() { + if (subscribers.get() == TERMINATED) { + return; + } + for (PublishDisposable<T> pd : subscribers.getAndSet(TERMINATED)) { + pd.onComplete(); + } + } + + @Override + @CheckReturnValue + public boolean hasObservers() { + return subscribers.get().length != 0; + } + + @Override + @Nullable + @CheckReturnValue + public Throwable getThrowable() { + if (subscribers.get() == TERMINATED) { + return error; + } + return null; + } + + @Override + @CheckReturnValue + public boolean hasThrowable() { + return subscribers.get() == TERMINATED && error != null; + } + + @Override + @CheckReturnValue + public boolean hasComplete() { + return subscribers.get() == TERMINATED && error == null; + } + + /** + * Wraps the actual subscriber, tracks its requests and makes cancellation + * to remove itself from the current subscribers array. + * + * @param <T> the value type + */ + static final class PublishDisposable<T> extends AtomicBoolean implements Disposable { + + private static final long serialVersionUID = 3562861878281475070L; + /** The actual subscriber. */ + final Observer<? super T> downstream; + /** The subject state. */ + final PublishSubject<T> parent; + + /** + * Constructs a PublishSubscriber, wraps the actual subscriber and the state. + * @param actual the actual subscriber + * @param parent the parent PublishProcessor + */ + PublishDisposable(Observer<? super T> actual, PublishSubject<T> parent) { + this.downstream = actual; + this.parent = parent; + } + + public void onNext(T t) { + if (!get()) { + downstream.onNext(t); + } + } + + public void onError(Throwable t) { + if (get()) { + RxJavaPlugins.onError(t); + } else { + downstream.onError(t); + } + } + + public void onComplete() { + if (!get()) { + downstream.onComplete(); + } + } + + @Override + public void dispose() { + if (compareAndSet(false, true)) { + parent.remove(this); + } + } + + @Override + public boolean isDisposed() { + return get(); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/subjects/ReplaySubject.java b/src/main/java/io/reactivex/rxjava3/subjects/ReplaySubject.java new file mode 100644 index 0000000000..4d5eb3335f --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/subjects/ReplaySubject.java @@ -0,0 +1,1344 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.subjects; + +import java.lang.reflect.Array; +import java.util.*; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.*; + +import io.reactivex.rxjava3.annotations.*; +import io.reactivex.rxjava3.core.Observer; +import io.reactivex.rxjava3.core.Scheduler; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.functions.ObjectHelper; +import io.reactivex.rxjava3.internal.util.*; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +/** + * Replays events (in a configurable bounded or unbounded manner) to current and late {@link Observer}s. + * <p> + * This subject does not have a public constructor by design; a new empty instance of this + * {@code ReplaySubject} can be created via the following {@code create} methods that + * allow specifying the retention policy for items: + * <ul> + * <li>{@link #create()} - creates an empty, unbounded {@code ReplaySubject} that + * caches all items and the terminal event it receives. + * <p> + * <img width="640" height="299" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/ReplaySubject.u.png" alt=""> + * <p> + * <img width="640" height="398" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/ReplaySubject.ue.png" alt=""> + * </li> + * <li>{@link #create(int)} - creates an empty, unbounded {@code ReplaySubject} + * with a hint about how many <b>total</b> items one expects to retain. + * </li> + * <li>{@link #createWithSize(int)} - creates an empty, size-bound {@code ReplaySubject} + * that retains at most the given number of the latest item it receives. + * <p> + * <img width="640" height="420" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/ReplaySubject.n.png" alt=""> + * </li> + * <li>{@link #createWithTime(long, TimeUnit, Scheduler)} - creates an empty, time-bound + * {@code ReplaySubject} that retains items no older than the specified time amount. + * <p> + * <img width="640" height="415" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/ReplaySubject.t.png" alt=""> + * </li> + * <li>{@link #createWithTimeAndSize(long, TimeUnit, Scheduler, int)} - creates an empty, + * time- and size-bound {@code ReplaySubject} that retains at most the given number + * items that are also not older than the specified time amount. + * <p> + * <img width="640" height="404" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/ReplaySubject.nt.png" alt=""> + * </li> + * </ul> + * <p> + * Since a {@code Subject} is conceptionally derived from the {@code Processor} type in the Reactive Streams specification, + * {@code null}s are not allowed (<a href="https://github.com/reactive-streams/reactive-streams-jvm#2.13">Rule 2.13</a>) as + * parameters to {@link #onNext(Object)} and {@link #onError(Throwable)}. Such calls will result in a + * {@link NullPointerException} being thrown and the subject's state is not changed. + * <p> + * Since a {@code ReplaySubject} is an {@link io.reactivex.rxjava3.core.Observable}, it does not support backpressure. + * <p> + * When this {@code ReplaySubject} is terminated via {@link #onError(Throwable)} or {@link #onComplete()}, + * late {@link io.reactivex.rxjava3.core.Observer}s will receive the retained/cached items first (if any) followed by the respective + * terminal event. If the {@code ReplaySubject} has a time-bound, the age of the retained/cached items are still considered + * when replaying and thus it may result in no items being emitted before the terminal event. + * <p> + * Once an {@code Observer} has subscribed, it will receive items continuously from that point on. Bounds only affect how + * many past items a new {@code Observer} will receive before it catches up with the live event feed. + * <p> + * Even though {@code ReplaySubject} implements the {@code Observer} interface, calling + * {@code onSubscribe} is not required (<a href="https://github.com/reactive-streams/reactive-streams-jvm#2.12">Rule 2.12</a>) + * if the subject is used as a standalone source. However, calling {@code onSubscribe} + * after the {@code ReplaySubject} reached its terminal state will result in the + * given {@code Disposable} being disposed immediately. + * <p> + * Calling {@link #onNext(Object)}, {@link #onError(Throwable)} and {@link #onComplete()} + * is required to be serialized (called from the same thread or called non-overlappingly from different threads + * through external means of serialization). The {@link #toSerialized()} method available to all {@code Subject}s + * provides such serialization and also protects against reentrance (i.e., when a downstream {@code Observer} + * consuming this subject also wants to call {@link #onNext(Object)} on this subject recursively). + * <p> + * This {@code ReplaySubject} supports the standard state-peeking methods {@link #hasComplete()}, {@link #hasThrowable()}, + * {@link #getThrowable()} and {@link #hasObservers()} as well as means to read the retained/cached items + * in a non-blocking and thread-safe manner via {@link #hasValue()}, {@link #getValue()}, + * {@link #getValues()} or {@link #getValues(Object[])}. + * <p> + * Note that due to concurrency requirements, a size- and time-bounded {@code ReplaySubject} may hold strong references to more + * source emissions than specified while it isn't terminated yet. Use the {@link #cleanupBuffer()} to allow + * such inaccessible items to be cleaned up by GC once no consumer references it anymore. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code ReplaySubject} does not operate by default on a particular {@link io.reactivex.rxjava3.core.Scheduler} and + * the {@code Observer}s get notified on the thread the respective {@code onXXX} methods were invoked. + * Time-bound {@code ReplaySubject}s use the given {@code Scheduler} in their {@code create} methods + * as time source to timestamp of items received for the age checks.</dd> + * <dt><b>Error handling:</b></dt> + * <dd>When the {@link #onError(Throwable)} is called, the {@code ReplaySubject} enters into a terminal state + * and emits the same {@code Throwable} instance to the last set of {@code Observer}s. During this emission, + * if one or more {@code Observer}s dispose their respective {@code Disposable}s, the + * {@code Throwable} is delivered to the global error handler via + * {@link io.reactivex.rxjava3.plugins.RxJavaPlugins#onError(Throwable)} (multiple times if multiple {@code Observer}s + * cancel at once). + * If there were no {@code Observer}s subscribed to this {@code ReplaySubject} when the {@code onError()} + * was called, the global error handler is not invoked. + * </dd> + * </dl> + * <p> + * Example usage: + * <pre> {@code + + ReplaySubject<Object> subject = ReplaySubject.create(); + subject.onNext("one"); + subject.onNext("two"); + subject.onNext("three"); + subject.onComplete(); + + // both of the following will get the onNext/onComplete calls from above + subject.subscribe(observer1); + subject.subscribe(observer2); + + } </pre> + * + * @param <T> the value type + */ +public final class ReplaySubject<T> extends Subject<T> { + final ReplayBuffer<T> buffer; + + final AtomicReference<ReplayDisposable<T>[]> observers; + + @SuppressWarnings("rawtypes") + static final ReplayDisposable[] EMPTY = new ReplayDisposable[0]; + + @SuppressWarnings("rawtypes") + static final ReplayDisposable[] TERMINATED = new ReplayDisposable[0]; + + boolean done; + + /** + * Creates an unbounded replay subject. + * <p> + * The internal buffer is backed by an {@link ArrayList} and starts with an initial capacity of 16. Once the + * number of items reaches this capacity, it will grow as necessary (usually by 50%). However, as the + * number of items grows, this causes frequent array reallocation and copying, and may hurt performance + * and latency. This can be avoided with the {@link #create(int)} overload which takes an initial capacity + * parameter and can be tuned to reduce the array reallocation frequency as needed. + * + * @param <T> + * the type of items observed and emitted by the Subject + * @return the created subject + */ + @CheckReturnValue + @NonNull + public static <T> ReplaySubject<T> create() { + return new ReplaySubject<>(new UnboundedReplayBuffer<>(16)); + } + + /** + * Creates an unbounded replay subject with the specified initial buffer capacity. + * <p> + * Use this method to avoid excessive array reallocation while the internal buffer grows to accommodate new + * items. For example, if you know that the buffer will hold 32k items, you can ask the + * {@code ReplaySubject} to preallocate its internal array with a capacity to hold that many items. Once + * the items start to arrive, the internal array won't need to grow, creating less garbage and no overhead + * due to frequent array-copying. + * + * @param <T> + * the type of items observed and emitted by the Subject + * @param capacityHint + * the initial buffer capacity + * @return the created subject + * @throws IllegalArgumentException if {@code capacityHint} is non-positive + */ + @CheckReturnValue + @NonNull + public static <T> ReplaySubject<T> create(int capacityHint) { + ObjectHelper.verifyPositive(capacityHint, "capacityHint"); + return new ReplaySubject<>(new UnboundedReplayBuffer<>(capacityHint)); + } + + /** + * Creates a size-bounded replay subject. + * <p> + * In this setting, the {@code ReplaySubject} holds at most {@code size} items in its internal buffer and + * discards the oldest item. + * <p> + * When observers subscribe to a terminated {@code ReplaySubject}, they are guaranteed to see at most + * {@code size} {@code onNext} events followed by a termination event. + * <p> + * If an observer subscribes while the {@code ReplaySubject} is active, it will observe all items in the + * buffer at that point in time and each item observed afterwards, even if the buffer evicts items due to + * the size constraint in the mean time. In other words, once an Observer subscribes, it will receive items + * without gaps in the sequence. + * + * @param <T> + * the type of items observed and emitted by the Subject + * @param maxSize + * the maximum number of buffered items + * @return the created subject + * @throws IllegalArgumentException if {@code maxSize} is non-positive + */ + @CheckReturnValue + @NonNull + public static <T> ReplaySubject<T> createWithSize(int maxSize) { + ObjectHelper.verifyPositive(maxSize, "maxSize"); + return new ReplaySubject<>(new SizeBoundReplayBuffer<>(maxSize)); + } + + /** + * Creates an unbounded replay subject with the bounded-implementation for testing purposes. + * <p> + * This variant behaves like the regular unbounded {@code ReplaySubject} created via {@link #create()} but + * uses the structures of the bounded-implementation. This is by no means intended for the replacement of + * the original, array-backed and unbounded {@code ReplaySubject} due to the additional overhead of the + * linked-list based internal buffer. The sole purpose is to allow testing and reasoning about the behavior + * of the bounded implementations without the interference of the eviction policies. + * + * @param <T> + * the type of items observed and emitted by the Subject + * @return the created subject + */ + /* test */ static <T> ReplaySubject<T> createUnbounded() { + return new ReplaySubject<>(new SizeBoundReplayBuffer<>(Integer.MAX_VALUE)); + } + + /** + * Creates a time-bounded replay subject. + * <p> + * In this setting, the {@code ReplaySubject} internally tags each observed item with a timestamp value + * supplied by the {@link Scheduler} and keeps only those whose age is less than the supplied time value + * converted to milliseconds. For example, an item arrives at T=0 and the max age is set to 5; at T>=5 + * this first item is then evicted by any subsequent item or termination event, leaving the buffer empty. + * <p> + * Once the subject is terminated, observers subscribing to it will receive items that remained in the + * buffer after the terminal event, regardless of their age. + * <p> + * If an observer subscribes while the {@code ReplaySubject} is active, it will observe only those items + * from within the buffer that have an age less than the specified time, and each item observed thereafter, + * even if the buffer evicts items due to the time constraint in the mean time. In other words, once an + * observer subscribes, it observes items without gaps in the sequence except for any outdated items at the + * beginning of the sequence. + * <p> + * Note that terminal notifications ({@code onError} and {@code onComplete}) trigger eviction as well. For + * example, with a max age of 5, the first item is observed at T=0, then an {@code onComplete} notification + * arrives at T=10. If an observer subscribes at T=11, it will find an empty {@code ReplaySubject} with just + * an {@code onComplete} notification. + * + * @param <T> + * the type of items observed and emitted by the Subject + * @param maxAge + * the maximum age of the contained items + * @param unit + * the time unit of {@code time} + * @param scheduler + * the {@link Scheduler} that provides the current time + * @return the created subject + * @throws NullPointerException if {@code unit} or {@code scheduler} is {@code null} + * @throws IllegalArgumentException if {@code maxAge} is non-positive + */ + @CheckReturnValue + @NonNull + public static <T> ReplaySubject<T> createWithTime(long maxAge, @NonNull TimeUnit unit, @NonNull Scheduler scheduler) { + ObjectHelper.verifyPositive(maxAge, "maxAge"); + Objects.requireNonNull(unit, "unit is null"); + Objects.requireNonNull(scheduler, "scheduler is null"); + return new ReplaySubject<>(new SizeAndTimeBoundReplayBuffer<>(Integer.MAX_VALUE, maxAge, unit, scheduler)); + } + + /** + * Creates a time- and size-bounded replay subject. + * <p> + * In this setting, the {@code ReplaySubject} internally tags each received item with a timestamp value + * supplied by the {@link Scheduler} and holds at most {@code size} items in its internal buffer. It evicts + * items from the start of the buffer if their age becomes less-than or equal to the supplied age in + * milliseconds or the buffer reaches its {@code size} limit. + * <p> + * When observers subscribe to a terminated {@code ReplaySubject}, they observe the items that remained in + * the buffer after the terminal notification, regardless of their age, but at most {@code size} items. + * <p> + * If an observer subscribes while the {@code ReplaySubject} is active, it will observe only those items + * from within the buffer that have age less than the specified time and each subsequent item, even if the + * buffer evicts items due to the time constraint in the mean time. In other words, once an observer + * subscribes, it observes items without gaps in the sequence except for the outdated items at the beginning + * of the sequence. + * <p> + * Note that terminal notifications ({@code onError} and {@code onComplete}) trigger eviction as well. For + * example, with a max age of 5, the first item is observed at T=0, then an {@code onComplete} notification + * arrives at T=10. If an observer subscribes at T=11, it will find an empty {@code ReplaySubject} with just + * an {@code onComplete} notification. + * + * @param <T> + * the type of items observed and emitted by the Subject + * @param maxAge + * the maximum age of the contained items + * @param unit + * the time unit of {@code time} + * @param maxSize + * the maximum number of buffered items + * @param scheduler + * the {@link Scheduler} that provides the current time + * @return the created subject + * @throws NullPointerException if {@code unit} or {@code scheduler} is {@code null} + * @throws IllegalArgumentException if {@code maxAge} or {@code maxSize} is non-positive + */ + @CheckReturnValue + @NonNull + public static <T> ReplaySubject<T> createWithTimeAndSize(long maxAge, @NonNull TimeUnit unit, @NonNull Scheduler scheduler, int maxSize) { + ObjectHelper.verifyPositive(maxSize, "maxSize"); + ObjectHelper.verifyPositive(maxAge, "maxAge"); + Objects.requireNonNull(unit, "unit is null"); + Objects.requireNonNull(scheduler, "scheduler is null"); + return new ReplaySubject<>(new SizeAndTimeBoundReplayBuffer<>(maxSize, maxAge, unit, scheduler)); + } + + /** + * Constructs a ReplayProcessor with the given custom ReplayBuffer instance. + * @param buffer the ReplayBuffer instance, not null (not verified) + */ + @SuppressWarnings("unchecked") + ReplaySubject(ReplayBuffer<T> buffer) { + this.buffer = buffer; + this.observers = new AtomicReference<>(EMPTY); + } + + @Override + protected void subscribeActual(Observer<? super T> observer) { + ReplayDisposable<T> rs = new ReplayDisposable<>(observer, this); + observer.onSubscribe(rs); + + if (add(rs)) { + if (rs.cancelled) { + remove(rs); + return; + } + } + buffer.replay(rs); + } + + @Override + public void onSubscribe(Disposable d) { + if (done) { + d.dispose(); + } + } + + @Override + public void onNext(T t) { + ExceptionHelper.nullCheck(t, "onNext called with a null value."); + if (done) { + return; + } + + ReplayBuffer<T> b = buffer; + b.add(t); + + for (ReplayDisposable<T> rs : observers.get()) { + b.replay(rs); + } + } + + @Override + public void onError(Throwable t) { + ExceptionHelper.nullCheck(t, "onError called with a null Throwable."); + if (done) { + RxJavaPlugins.onError(t); + return; + } + done = true; + + Object o = NotificationLite.error(t); + + ReplayBuffer<T> b = buffer; + + b.addFinal(o); + + for (ReplayDisposable<T> rs : terminate(o)) { + b.replay(rs); + } + } + + @Override + public void onComplete() { + if (done) { + return; + } + done = true; + + Object o = NotificationLite.complete(); + + ReplayBuffer<T> b = buffer; + + b.addFinal(o); + + for (ReplayDisposable<T> rs : terminate(o)) { + b.replay(rs); + } + } + + @Override + @CheckReturnValue + public boolean hasObservers() { + return observers.get().length != 0; + } + + @CheckReturnValue + /* test */ int observerCount() { + return observers.get().length; + } + + @Override + @Nullable + @CheckReturnValue + public Throwable getThrowable() { + Object o = buffer.get(); + if (NotificationLite.isError(o)) { + return NotificationLite.getError(o); + } + return null; + } + + /** + * Returns a single value the Subject currently has or null if no such value exists. + * <p>The method is thread-safe. + * @return a single value the Subject currently has or null if no such value exists + */ + @Nullable + @CheckReturnValue + public T getValue() { + return buffer.getValue(); + } + + /** + * Makes sure the item cached by the head node in a bounded + * ReplaySubject is released (as it is never part of a replay). + * <p> + * By default, live bounded buffers will remember one item before + * the currently receivable one to ensure subscribers can always + * receive a continuous sequence of items. A terminated ReplaySubject + * automatically releases this inaccessible item. + * <p> + * The method must be called sequentially, similar to the standard + * {@code onXXX} methods. + * <p>History: 2.1.11 - experimental + * @since 2.2 + */ + public void cleanupBuffer() { + buffer.trimHead(); + } + + /** An empty array to avoid allocation in getValues(). */ + private static final Object[] EMPTY_ARRAY = new Object[0]; + + /** + * Returns an Object array containing snapshot all values of the Subject. + * <p>The method is thread-safe. + * @return the array containing the snapshot of all values of the Subject + */ + @CheckReturnValue + public Object[] getValues() { + @SuppressWarnings("unchecked") + T[] a = (T[])EMPTY_ARRAY; + T[] b = getValues(a); + if (b == EMPTY_ARRAY) { + return new Object[0]; + } + return b; + + } + + /** + * Returns a typed array containing a snapshot of all values of the Subject. + * <p>The method follows the conventions of Collection.toArray by setting the array element + * after the last value to null (if the capacity permits). + * <p>The method is thread-safe. + * @param array the target array to copy values into if it fits + * @return the given array if the values fit into it or a new array containing all values + */ + @CheckReturnValue + public T[] getValues(T[] array) { + return buffer.getValues(array); + } + + @Override + @CheckReturnValue + public boolean hasComplete() { + Object o = buffer.get(); + return NotificationLite.isComplete(o); + } + + @Override + @CheckReturnValue + public boolean hasThrowable() { + Object o = buffer.get(); + return NotificationLite.isError(o); + } + + /** + * Returns true if the subject has any value. + * <p>The method is thread-safe. + * @return true if the subject has any value + */ + @CheckReturnValue + public boolean hasValue() { + return buffer.size() != 0; // NOPMD + } + + @CheckReturnValue + /* test*/ int size() { + return buffer.size(); + } + + boolean add(ReplayDisposable<T> rs) { + for (;;) { + ReplayDisposable<T>[] a = observers.get(); + if (a == TERMINATED) { + return false; + } + int len = a.length; + @SuppressWarnings("unchecked") + ReplayDisposable<T>[] b = new ReplayDisposable[len + 1]; + System.arraycopy(a, 0, b, 0, len); + b[len] = rs; + if (observers.compareAndSet(a, b)) { + return true; + } + } + } + + @SuppressWarnings("unchecked") + void remove(ReplayDisposable<T> rs) { + for (;;) { + ReplayDisposable<T>[] a = observers.get(); + if (a == TERMINATED || a == EMPTY) { + return; + } + int len = a.length; + int j = -1; + for (int i = 0; i < len; i++) { + if (a[i] == rs) { + j = i; + break; + } + } + + if (j < 0) { + return; + } + ReplayDisposable<T>[] b; + if (len == 1) { + b = EMPTY; + } else { + b = new ReplayDisposable[len - 1]; + System.arraycopy(a, 0, b, 0, j); + System.arraycopy(a, j + 1, b, j, len - j - 1); + } + if (observers.compareAndSet(a, b)) { + return; + } + } + } + + @SuppressWarnings("unchecked") + ReplayDisposable<T>[] terminate(Object terminalValue) { + buffer.compareAndSet(null, terminalValue); + return observers.getAndSet(TERMINATED); + } + + /** + * Abstraction over a buffer that receives events and replays them to + * individual Observers. + * + * @param <T> the value type + */ + interface ReplayBuffer<T> { + + void add(T value); + + void addFinal(Object notificationLite); + + void replay(ReplayDisposable<T> rs); + + int size(); + + @Nullable + T getValue(); + + T[] getValues(T[] array); + /** + * Returns the terminal NotificationLite object or null if not yet terminated. + * @return the terminal NotificationLite object or null if not yet terminated + */ + Object get(); + + /** + * Atomically compares and sets the next terminal NotificationLite object if the + * current equals to the expected NotificationLite object. + * @param expected the expected NotificationLite object + * @param next the next NotificationLite object + * @return true if successful + */ + boolean compareAndSet(Object expected, Object next); + + /** + * Make sure an old inaccessible head value is released + * in a bounded buffer. + */ + void trimHead(); + } + + static final class ReplayDisposable<T> extends AtomicInteger implements Disposable { + + private static final long serialVersionUID = 466549804534799122L; + final Observer<? super T> downstream; + final ReplaySubject<T> state; + + Object index; + + volatile boolean cancelled; + + ReplayDisposable(Observer<? super T> actual, ReplaySubject<T> state) { + this.downstream = actual; + this.state = state; + } + + @Override + public void dispose() { + if (!cancelled) { + cancelled = true; + state.remove(this); + } + } + + @Override + public boolean isDisposed() { + return cancelled; + } + } + + static final class UnboundedReplayBuffer<T> + extends AtomicReference<Object> + implements ReplayBuffer<T> { + + private static final long serialVersionUID = -733876083048047795L; + + final List<Object> buffer; + + volatile boolean done; + + volatile int size; + + UnboundedReplayBuffer(int capacityHint) { + this.buffer = new ArrayList<>(capacityHint); + } + + @Override + public void add(T value) { + buffer.add(value); + size++; + } + + @Override + public void addFinal(Object notificationLite) { + buffer.add(notificationLite); + trimHead(); + size++; + done = true; + } + + @Override + public void trimHead() { + // no-op in this type of buffer + } + + @Override + @Nullable + @SuppressWarnings("unchecked") + public T getValue() { + int s = size; + if (s != 0) { + List<Object> b = buffer; + Object o = b.get(s - 1); + if (NotificationLite.isComplete(o) || NotificationLite.isError(o)) { + if (s == 1) { + return null; + } + return (T)b.get(s - 2); + } + return (T)o; + } + return null; + } + + @Override + @SuppressWarnings("unchecked") + public T[] getValues(T[] array) { + int s = size; + if (s == 0) { + if (array.length != 0) { + array[0] = null; + } + return array; + } + List<Object> b = buffer; + Object o = b.get(s - 1); + + if (NotificationLite.isComplete(o) || NotificationLite.isError(o)) { + s--; + if (s == 0) { + if (array.length != 0) { + array[0] = null; + } + return array; + } + } + + if (array.length < s) { + array = (T[])Array.newInstance(array.getClass().getComponentType(), s); + } + for (int i = 0; i < s; i++) { + array[i] = (T)b.get(i); + } + if (array.length > s) { + array[s] = null; + } + + return array; + } + + @Override + @SuppressWarnings("unchecked") + public void replay(ReplayDisposable<T> rs) { + if (rs.getAndIncrement() != 0) { + return; + } + + int missed = 1; + final List<Object> b = buffer; + final Observer<? super T> a = rs.downstream; + + Integer indexObject = (Integer)rs.index; + int index; + if (indexObject != null) { + index = indexObject; + } else { + index = 0; + rs.index = 0; + } + + for (;;) { + + if (rs.cancelled) { + rs.index = null; + return; + } + + int s = size; + + while (s != index) { + + if (rs.cancelled) { + rs.index = null; + return; + } + + Object o = b.get(index); + + if (done) { + if (index + 1 == s) { + s = size; + if (index + 1 == s) { + if (NotificationLite.isComplete(o)) { + a.onComplete(); + } else { + a.onError(NotificationLite.getError(o)); + } + rs.index = null; + rs.cancelled = true; + return; + } + } + } + + a.onNext((T)o); + index++; + } + + if (index != size) { + continue; + } + + rs.index = index; + + missed = rs.addAndGet(-missed); + if (missed == 0) { + break; + } + } + } + + @Override + public int size() { + int s = size; + if (s != 0) { + Object o = buffer.get(s - 1); + if (NotificationLite.isComplete(o) || NotificationLite.isError(o)) { + return s - 1; + } + return s; + } + return 0; + } + } + + static final class Node<T> extends AtomicReference<Node<T>> { + + private static final long serialVersionUID = 6404226426336033100L; + + final T value; + + Node(T value) { + this.value = value; + } + } + + static final class TimedNode<T> extends AtomicReference<TimedNode<T>> { + + private static final long serialVersionUID = 6404226426336033100L; + + final T value; + final long time; + + TimedNode(T value, long time) { + this.value = value; + this.time = time; + } + } + + static final class SizeBoundReplayBuffer<T> + extends AtomicReference<Object> + implements ReplayBuffer<T> { + + private static final long serialVersionUID = 1107649250281456395L; + + final int maxSize; + int size; + + volatile Node<Object> head; + + Node<Object> tail; + + volatile boolean done; + + SizeBoundReplayBuffer(int maxSize) { + this.maxSize = maxSize; + Node<Object> h = new Node<>(null); + this.tail = h; + this.head = h; + } + + void trim() { + if (size > maxSize) { + size--; + Node<Object> h = head; + head = h.get(); + } + } + + @Override + public void add(T value) { + Node<Object> n = new Node<>(value); + Node<Object> t = tail; + + tail = n; + size++; + t.set(n); // releases both the tail and size + + trim(); + } + + @Override + public void addFinal(Object notificationLite) { + Node<Object> n = new Node<>(notificationLite); + Node<Object> t = tail; + + tail = n; + size++; + t.lazySet(n); // releases both the tail and size + + trimHead(); + done = true; + } + + /** + * Replace a non-empty head node with an empty one to + * allow the GC of the inaccessible old value. + */ + @Override + public void trimHead() { + Node<Object> h = head; + if (h.value != null) { + Node<Object> n = new Node<>(null); + n.lazySet(h.get()); + head = n; + } + } + + @Override + @Nullable + @SuppressWarnings("unchecked") + public T getValue() { + Node<Object> prev = null; + Node<Object> h = head; + + for (;;) { + Node<Object> next = h.get(); + if (next == null) { + break; + } + prev = h; + h = next; + } + + Object v = h.value; + if (v == null) { + return null; + } + if (NotificationLite.isComplete(v) || NotificationLite.isError(v)) { + return (T)prev.value; + } + + return (T)v; + } + + @Override + @SuppressWarnings("unchecked") + public T[] getValues(T[] array) { + Node<Object> h = head; + int s = size(); + + if (s == 0) { + if (array.length != 0) { + array[0] = null; + } + } else { + if (array.length < s) { + array = (T[])Array.newInstance(array.getClass().getComponentType(), s); + } + + int i = 0; + while (i != s) { + Node<Object> next = h.get(); + array[i] = (T)next.value; + i++; + h = next; + } + if (array.length > s) { + array[s] = null; + } + } + + return array; + } + + @Override + @SuppressWarnings("unchecked") + public void replay(ReplayDisposable<T> rs) { + if (rs.getAndIncrement() != 0) { + return; + } + + int missed = 1; + final Observer<? super T> a = rs.downstream; + + Node<Object> index = (Node<Object>)rs.index; + if (index == null) { + index = head; + } + + for (;;) { + + for (;;) { + if (rs.cancelled) { + rs.index = null; + return; + } + + Node<Object> n = index.get(); + + if (n == null) { + break; + } + + Object o = n.value; + + if (done) { + if (n.get() == null) { + + if (NotificationLite.isComplete(o)) { + a.onComplete(); + } else { + a.onError(NotificationLite.getError(o)); + } + rs.index = null; + rs.cancelled = true; + return; + } + } + + a.onNext((T)o); + + index = n; + } + + if (index.get() != null) { + continue; + } + + rs.index = index; + + missed = rs.addAndGet(-missed); + if (missed == 0) { + break; + } + } + } + + @Override + public int size() { + int s = 0; + Node<Object> h = head; + while (s != Integer.MAX_VALUE) { + Node<Object> next = h.get(); + if (next == null) { + Object o = h.value; + if (NotificationLite.isComplete(o) || NotificationLite.isError(o)) { + s--; + } + break; + } + s++; + h = next; + } + + return s; + } + } + + static final class SizeAndTimeBoundReplayBuffer<T> + extends AtomicReference<Object> + implements ReplayBuffer<T> { + + private static final long serialVersionUID = -8056260896137901749L; + + final int maxSize; + final long maxAge; + final TimeUnit unit; + final Scheduler scheduler; + int size; + + volatile TimedNode<Object> head; + + TimedNode<Object> tail; + + volatile boolean done; + + SizeAndTimeBoundReplayBuffer(int maxSize, long maxAge, TimeUnit unit, Scheduler scheduler) { + this.maxSize = maxSize; + this.maxAge = maxAge; + this.unit = unit; + this.scheduler = scheduler; + TimedNode<Object> h = new TimedNode<>(null, 0L); + this.tail = h; + this.head = h; + } + + void trim() { + if (size > maxSize) { + size--; + TimedNode<Object> h = head; + head = h.get(); + } + long limit = scheduler.now(unit) - maxAge; + + TimedNode<Object> h = head; + + for (;;) { + if (size <= 1) { + head = h; + break; + } + TimedNode<Object> next = h.get(); + + if (next.time > limit) { + head = h; + break; + } + + h = next; + size--; + } + + } + + void trimFinal() { + long limit = scheduler.now(unit) - maxAge; + + TimedNode<Object> h = head; + + for (;;) { + TimedNode<Object> next = h.get(); + if (next.get() == null) { + if (h.value != null) { + TimedNode<Object> lasth = new TimedNode<>(null, 0L); + lasth.lazySet(h.get()); + head = lasth; + } else { + head = h; + } + break; + } + + if (next.time > limit) { + if (h.value != null) { + TimedNode<Object> lasth = new TimedNode<>(null, 0L); + lasth.lazySet(h.get()); + head = lasth; + } else { + head = h; + } + break; + } + + h = next; + } + } + + @Override + public void add(T value) { + TimedNode<Object> n = new TimedNode<>(value, scheduler.now(unit)); + TimedNode<Object> t = tail; + + tail = n; + size++; + t.set(n); // releases both the tail and size + + trim(); + } + + @Override + public void addFinal(Object notificationLite) { + TimedNode<Object> n = new TimedNode<>(notificationLite, Long.MAX_VALUE); + TimedNode<Object> t = tail; + + tail = n; + size++; + t.lazySet(n); // releases both the tail and size + trimFinal(); + + done = true; + } + + /** + * Replace a non-empty head node with an empty one to + * allow the GC of the inaccessible old value. + */ + @Override + public void trimHead() { + TimedNode<Object> h = head; + if (h.value != null) { + TimedNode<Object> n = new TimedNode<>(null, 0); + n.lazySet(h.get()); + head = n; + } + } + + @Override + @Nullable + @SuppressWarnings("unchecked") + public T getValue() { + TimedNode<Object> prev = null; + TimedNode<Object> h = head; + + for (;;) { + TimedNode<Object> next = h.get(); + if (next == null) { + break; + } + prev = h; + h = next; + } + + long limit = scheduler.now(unit) - maxAge; + if (h.time < limit) { + return null; + } + + Object v = h.value; + if (v == null) { + return null; + } + if (NotificationLite.isComplete(v) || NotificationLite.isError(v)) { + return (T)prev.value; + } + + return (T)v; + } + + TimedNode<Object> getHead() { + TimedNode<Object> index = head; + // skip old entries + long limit = scheduler.now(unit) - maxAge; + TimedNode<Object> next = index.get(); + while (next != null) { + long ts = next.time; + if (ts > limit) { + break; + } + index = next; + next = index.get(); + } + return index; + } + + @Override + @SuppressWarnings("unchecked") + public T[] getValues(T[] array) { + TimedNode<Object> h = getHead(); + int s = size(h); + + if (s == 0) { + if (array.length != 0) { + array[0] = null; + } + } else { + if (array.length < s) { + array = (T[])Array.newInstance(array.getClass().getComponentType(), s); + } + + int i = 0; + while (i != s) { + TimedNode<Object> next = h.get(); + array[i] = (T)next.value; + i++; + h = next; + } + if (array.length > s) { + array[s] = null; + } + } + + return array; + } + + @Override + @SuppressWarnings("unchecked") + public void replay(ReplayDisposable<T> rs) { + if (rs.getAndIncrement() != 0) { + return; + } + + int missed = 1; + final Observer<? super T> a = rs.downstream; + + TimedNode<Object> index = (TimedNode<Object>)rs.index; + if (index == null) { + index = getHead(); + } + + for (;;) { + + for (;;) { + if (rs.cancelled) { + rs.index = null; + return; + } + + TimedNode<Object> n = index.get(); + + if (n == null) { + break; + } + + Object o = n.value; + + if (done) { + if (n.get() == null) { + + if (NotificationLite.isComplete(o)) { + a.onComplete(); + } else { + a.onError(NotificationLite.getError(o)); + } + rs.index = null; + rs.cancelled = true; + return; + } + } + + a.onNext((T)o); + + index = n; + } + + rs.index = index; + + missed = rs.addAndGet(-missed); + if (missed == 0) { + break; + } + } + } + + @Override + public int size() { + return size(getHead()); + } + + int size(TimedNode<Object> h) { + int s = 0; + while (s != Integer.MAX_VALUE) { + TimedNode<Object> next = h.get(); + if (next == null) { + Object o = h.value; + if (NotificationLite.isComplete(o) || NotificationLite.isError(o)) { + s--; + } + break; + } + s++; + h = next; + } + + return s; + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/subjects/SerializedSubject.java b/src/main/java/io/reactivex/rxjava3/subjects/SerializedSubject.java new file mode 100644 index 0000000000..09a45b1da2 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/subjects/SerializedSubject.java @@ -0,0 +1,205 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.subjects; + +import io.reactivex.rxjava3.annotations.Nullable; +import io.reactivex.rxjava3.core.Observer; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.util.*; +import io.reactivex.rxjava3.internal.util.AppendOnlyLinkedArrayList.NonThrowingPredicate; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +/** + * Serializes calls to the Observer methods. + * <p>All other Observable and Subject methods are thread-safe by design. + * + * @param <T> the item value type + */ +/* public */ final class SerializedSubject<T> extends Subject<T> implements NonThrowingPredicate<Object> { + /** The actual subscriber to serialize Subscriber calls to. */ + final Subject<T> actual; + /** Indicates an emission is going on, guarded by this. */ + boolean emitting; + /** If not null, it holds the missed NotificationLite events. */ + AppendOnlyLinkedArrayList<Object> queue; + /** Indicates a terminal event has been received and all further events will be dropped. */ + volatile boolean done; + + /** + * Constructor that wraps an actual subject. + * @param actual the subject wrapped + */ + SerializedSubject(final Subject<T> actual) { + this.actual = actual; + } + + @Override + protected void subscribeActual(Observer<? super T> observer) { + actual.subscribe(observer); + } + + @Override + public void onSubscribe(Disposable d) { + boolean cancel; + if (!done) { + synchronized (this) { + if (done) { + cancel = true; + } else { + if (emitting) { + AppendOnlyLinkedArrayList<Object> q = queue; + if (q == null) { + q = new AppendOnlyLinkedArrayList<>(4); + queue = q; + } + q.add(NotificationLite.disposable(d)); + return; + } + emitting = true; + cancel = false; + } + } + } else { + cancel = true; + } + if (cancel) { + d.dispose(); + } else { + actual.onSubscribe(d); + emitLoop(); + } + } + + @Override + public void onNext(T t) { + if (done) { + return; + } + synchronized (this) { + if (done) { + return; + } + if (emitting) { + AppendOnlyLinkedArrayList<Object> q = queue; + if (q == null) { + q = new AppendOnlyLinkedArrayList<>(4); + queue = q; + } + q.add(NotificationLite.next(t)); + return; + } + emitting = true; + } + actual.onNext(t); + emitLoop(); + } + + @Override + public void onError(Throwable t) { + if (done) { + RxJavaPlugins.onError(t); + return; + } + boolean reportError; + synchronized (this) { + if (done) { + reportError = true; + } else { + done = true; + if (emitting) { + AppendOnlyLinkedArrayList<Object> q = queue; + if (q == null) { + q = new AppendOnlyLinkedArrayList<>(4); + queue = q; + } + q.setFirst(NotificationLite.error(t)); + return; + } + reportError = false; + emitting = true; + } + } + if (reportError) { + RxJavaPlugins.onError(t); + return; + } + actual.onError(t); + } + + @Override + public void onComplete() { + if (done) { + return; + } + synchronized (this) { + if (done) { + return; + } + done = true; + if (emitting) { + AppendOnlyLinkedArrayList<Object> q = queue; + if (q == null) { + q = new AppendOnlyLinkedArrayList<>(4); + queue = q; + } + q.add(NotificationLite.complete()); + return; + } + emitting = true; + } + actual.onComplete(); + } + + /** Loops until all notifications in the queue has been processed. */ + void emitLoop() { + for (;;) { + AppendOnlyLinkedArrayList<Object> q; + synchronized (this) { + q = queue; + if (q == null) { + emitting = false; + return; + } + queue = null; + } + q.forEachWhile(this); + } + } + + @Override + public boolean test(Object o) { + return NotificationLite.acceptFull(o, actual); + } + + @Override + public boolean hasObservers() { + return actual.hasObservers(); + } + + @Override + public boolean hasThrowable() { + return actual.hasThrowable(); + } + + @Override + @Nullable + public Throwable getThrowable() { + return actual.getThrowable(); + } + + @Override + public boolean hasComplete() { + return actual.hasComplete(); + } +} diff --git a/src/main/java/io/reactivex/rxjava3/subjects/SingleSubject.java b/src/main/java/io/reactivex/rxjava3/subjects/SingleSubject.java new file mode 100644 index 0000000000..7559f0329d --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/subjects/SingleSubject.java @@ -0,0 +1,312 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.subjects; + +import java.util.concurrent.atomic.*; + +import io.reactivex.rxjava3.annotations.*; +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.util.ExceptionHelper; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +/** + * Represents a hot Single-like source and consumer of events similar to Subjects. + * <p> + * <img width="640" height="236" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/SingleSubject.png" alt=""> + * <p> + * This subject does not have a public constructor by design; a new non-terminated instance of this + * {@code SingleSubject} can be created via the {@link #create()} method. + * <p> + * Since the {@code SingleSubject} is conceptionally derived from the {@code Processor} type in the Reactive Streams specification, + * {@code null}s are not allowed (<a href="https://github.com/reactive-streams/reactive-streams-jvm#2.13">Rule 2.13</a>) + * as parameters to {@link #onSuccess(Object)} and {@link #onError(Throwable)}. Such calls will result in a + * {@link NullPointerException} being thrown and the subject's state is not changed. + * <p> + * Since a {@code SingleSubject} is a {@link io.reactivex.rxjava3.core.Single}, calling {@code onSuccess} or {@code onError} + * will move this {@code SingleSubject} into its terminal state atomically. + * <p> + * All methods are thread safe. Calling {@link #onSuccess(Object)} multiple + * times has no effect. Calling {@link #onError(Throwable)} multiple times relays the {@code Throwable} to + * the {@link io.reactivex.rxjava3.plugins.RxJavaPlugins#onError(Throwable)} global error handler. + * <p> + * Even though {@code SingleSubject} implements the {@code SingleObserver} interface, calling + * {@code onSubscribe} is not required (<a href="https://github.com/reactive-streams/reactive-streams-jvm#2.12">Rule 2.12</a>) + * if the subject is used as a standalone source. However, calling {@code onSubscribe} + * after the {@code SingleSubject} reached its terminal state will result in the + * given {@code Disposable} being disposed immediately. + * <p> + * This {@code SingleSubject} supports the standard state-peeking methods {@link #hasThrowable()}, + * {@link #getThrowable()} and {@link #hasObservers()} as well as means to read any success item in a non-blocking + * and thread-safe manner via {@link #hasValue()} and {@link #getValue()}. + * <p> + * The {@code SingleSubject} does not support clearing its cached {@code onSuccess} value. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code SingleSubject} does not operate by default on a particular {@link io.reactivex.rxjava3.core.Scheduler} and + * the {@code SingleObserver}s get notified on the thread where the terminating {@code onSuccess} or {@code onError} + * methods were invoked.</dd> + * <dt><b>Error handling:</b></dt> + * <dd>When the {@link #onError(Throwable)} is called, the {@code SingleSubject} enters into a terminal state + * and emits the same {@code Throwable} instance to the last set of {@code SingleObserver}s. During this emission, + * if one or more {@code SingleObserver}s dispose their respective {@code Disposable}s, the + * {@code Throwable} is delivered to the global error handler via + * {@link io.reactivex.rxjava3.plugins.RxJavaPlugins#onError(Throwable)} (multiple times if multiple {@code SingleObserver}s + * cancel at once). + * If there were no {@code SingleObserver}s subscribed to this {@code SingleSubject} when the {@code onError()} + * was called, the global error handler is not invoked. + * </dd> + * </dl> + * <p> + * Example usage: + * <pre><code> + * SingleSubject<Integer> subject1 = SingleSubject.create(); + * + * TestObserver<Integer> to1 = subject1.test(); + * + * // SingleSubjects are empty by default + * to1.assertEmpty(); + * + * subject1.onSuccess(1); + * + * // onSuccess is a terminal event with SingleSubjects + * // TestObserver converts onSuccess into onNext + onComplete + * to1.assertResult(1); + * + * TestObserver<Integer> to2 = subject1.test(); + * + * // late Observers receive the terminal signal (onSuccess) too + * to2.assertResult(1); + * </code></pre> + * <p>History: 2.0.5 - experimental + * @param <T> the value type received and emitted + * @since 2.1 + */ +public final class SingleSubject<T> extends Single<T> implements SingleObserver<T> { + + final AtomicReference<SingleDisposable<T>[]> observers; + + @SuppressWarnings("rawtypes") + static final SingleDisposable[] EMPTY = new SingleDisposable[0]; + + @SuppressWarnings("rawtypes") + static final SingleDisposable[] TERMINATED = new SingleDisposable[0]; + + final AtomicBoolean once; + T value; + Throwable error; + + /** + * Creates a fresh SingleSubject. + * @param <T> the value type received and emitted + * @return the new SingleSubject instance + */ + @CheckReturnValue + @NonNull + public static <T> SingleSubject<T> create() { + return new SingleSubject<>(); + } + + @SuppressWarnings("unchecked") + SingleSubject() { + once = new AtomicBoolean(); + observers = new AtomicReference<>(EMPTY); + } + + @Override + public void onSubscribe(@NonNull Disposable d) { + if (observers.get() == TERMINATED) { + d.dispose(); + } + } + + @SuppressWarnings("unchecked") + @Override + public void onSuccess(@NonNull T value) { + ExceptionHelper.nullCheck(value, "onSuccess called with a null value."); + if (once.compareAndSet(false, true)) { + this.value = value; + for (SingleDisposable<T> md : observers.getAndSet(TERMINATED)) { + md.downstream.onSuccess(value); + } + } + } + + @SuppressWarnings("unchecked") + @Override + public void onError(@NonNull Throwable e) { + ExceptionHelper.nullCheck(e, "onError called with a null Throwable."); + if (once.compareAndSet(false, true)) { + this.error = e; + for (SingleDisposable<T> md : observers.getAndSet(TERMINATED)) { + md.downstream.onError(e); + } + } else { + RxJavaPlugins.onError(e); + } + } + + @Override + protected void subscribeActual(@NonNull SingleObserver<? super T> observer) { + SingleDisposable<T> md = new SingleDisposable<>(observer, this); + observer.onSubscribe(md); + if (add(md)) { + if (md.isDisposed()) { + remove(md); + } + } else { + Throwable ex = error; + if (ex != null) { + observer.onError(ex); + } else { + observer.onSuccess(value); + } + } + } + + boolean add(@NonNull SingleDisposable<T> inner) { + for (;;) { + SingleDisposable<T>[] a = observers.get(); + if (a == TERMINATED) { + return false; + } + + int n = a.length; + @SuppressWarnings("unchecked") + SingleDisposable<T>[] b = new SingleDisposable[n + 1]; + System.arraycopy(a, 0, b, 0, n); + b[n] = inner; + if (observers.compareAndSet(a, b)) { + return true; + } + } + } + + @SuppressWarnings("unchecked") + void remove(@NonNull SingleDisposable<T> inner) { + for (;;) { + SingleDisposable<T>[] a = observers.get(); + int n = a.length; + if (n == 0) { + return; + } + + int j = -1; + + for (int i = 0; i < n; i++) { + if (a[i] == inner) { + j = i; + break; + } + } + + if (j < 0) { + return; + } + SingleDisposable<T>[] b; + if (n == 1) { + b = EMPTY; + } else { + b = new SingleDisposable[n - 1]; + System.arraycopy(a, 0, b, 0, j); + System.arraycopy(a, j + 1, b, j, n - j - 1); + } + + if (observers.compareAndSet(a, b)) { + return; + } + } + } + + /** + * Returns the success value if this SingleSubject was terminated with a success value. + * @return the success value or null + */ + @Nullable + public T getValue() { + if (observers.get() == TERMINATED) { + return value; + } + return null; + } + + /** + * Returns true if this SingleSubject was terminated with a success value. + * @return true if this SingleSubject was terminated with a success value + */ + public boolean hasValue() { + return observers.get() == TERMINATED && value != null; + } + + /** + * Returns the terminal error if this SingleSubject has been terminated with an error, null otherwise. + * @return the terminal error or null if not terminated or not with an error + */ + @Nullable + public Throwable getThrowable() { + if (observers.get() == TERMINATED) { + return error; + } + return null; + } + + /** + * Returns true if this SingleSubject has been terminated with an error. + * @return true if this SingleSubject has been terminated with an error + */ + public boolean hasThrowable() { + return observers.get() == TERMINATED && error != null; + } + + /** + * Returns true if this SingleSubject has observers. + * @return true if this SingleSubject has observers + */ + public boolean hasObservers() { + return observers.get().length != 0; + } + + /** + * Returns the number of current observers. + * @return the number of current observers + */ + /* test */ int observerCount() { + return observers.get().length; + } + + static final class SingleDisposable<T> + extends AtomicReference<SingleSubject<T>> implements Disposable { + private static final long serialVersionUID = -7650903191002190468L; + + final SingleObserver<? super T> downstream; + + SingleDisposable(SingleObserver<? super T> actual, SingleSubject<T> parent) { + this.downstream = actual; + lazySet(parent); + } + + @Override + public void dispose() { + SingleSubject<T> parent = getAndSet(null); + if (parent != null) { + parent.remove(this); + } + } + + @Override + public boolean isDisposed() { + return get() == null; + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/subjects/Subject.java b/src/main/java/io/reactivex/rxjava3/subjects/Subject.java new file mode 100644 index 0000000000..b76a15da33 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/subjects/Subject.java @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.subjects; + +import io.reactivex.rxjava3.annotations.*; +import io.reactivex.rxjava3.core.*; + +/** + * Represents an {@link Observer} and an {@link Observable} at the same time, allowing + * multicasting events from a single source to multiple child {@code Observer}s. + * <p> + * All methods except the {@link #onSubscribe(io.reactivex.rxjava3.disposables.Disposable)}, {@link #onNext(Object)}, + * {@link #onError(Throwable)} and {@link #onComplete()} are thread-safe. + * Use {@link #toSerialized()} to make these methods thread-safe as well. + * + * @param <T> the item value type + */ +public abstract class Subject<T> extends Observable<T> implements Observer<T> { + /** + * Returns true if the subject has any Observers. + * <p>The method is thread-safe. + * @return true if the subject has any Observers + */ + @CheckReturnValue + public abstract boolean hasObservers(); + + /** + * Returns true if the subject has reached a terminal state through an error event. + * <p>The method is thread-safe. + * @return true if the subject has reached a terminal state through an error event + * @see #getThrowable() + * @see #hasComplete() + */ + @CheckReturnValue + public abstract boolean hasThrowable(); + + /** + * Returns true if the subject has reached a terminal state through a complete event. + * <p>The method is thread-safe. + * @return true if the subject has reached a terminal state through a complete event + * @see #hasThrowable() + */ + @CheckReturnValue + public abstract boolean hasComplete(); + + /** + * Returns the error that caused the Subject to terminate or null if the Subject + * hasn't terminated yet. + * <p>The method is thread-safe. + * @return the error that caused the Subject to terminate or null if the Subject + * hasn't terminated yet + */ + @Nullable + @CheckReturnValue + public abstract Throwable getThrowable(); + + /** + * Wraps this Subject and serializes the calls to the onSubscribe, onNext, onError and + * onComplete methods, making them thread-safe. + * <p>The method is thread-safe. + * @return the wrapped and serialized subject + */ + @NonNull + @CheckReturnValue + public final Subject<T> toSerialized() { + if (this instanceof SerializedSubject) { + return this; + } + return new SerializedSubject<>(this); + } +} diff --git a/src/main/java/io/reactivex/rxjava3/subjects/UnicastSubject.java b/src/main/java/io/reactivex/rxjava3/subjects/UnicastSubject.java new file mode 100644 index 0000000000..3092dc73c0 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/subjects/UnicastSubject.java @@ -0,0 +1,560 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.subjects; + +import java.util.Objects; +import java.util.concurrent.atomic.*; + +import io.reactivex.rxjava3.annotations.*; +import io.reactivex.rxjava3.core.Observer; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.disposables.EmptyDisposable; +import io.reactivex.rxjava3.internal.functions.*; +import io.reactivex.rxjava3.internal.observers.BasicIntQueueDisposable; +import io.reactivex.rxjava3.internal.util.ExceptionHelper; +import io.reactivex.rxjava3.operators.SimpleQueue; +import io.reactivex.rxjava3.operators.SpscLinkedArrayQueue; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +/** + * A Subject that queues up events until a single {@link Observer} subscribes to it, replays + * those events to it until the {@code Observer} catches up and then switches to relaying events live to + * this single {@code Observer} until this {@code UnicastSubject} terminates or the {@code Observer} disposes. + * <p> + * <img width="640" height="370" src="https://raw.github.com/wiki/ReactiveX/RxJava/images/rx-operators/UnicastSubject.png" alt=""> + * <p> + * Note that {@code UnicastSubject} holds an unbounded internal buffer. + * <p> + * This subject does not have a public constructor by design; a new empty instance of this + * {@code UnicastSubject} can be created via the following {@code create} methods that + * allow specifying the retention policy for items: + * <ul> + * <li>{@link #create()} - creates an empty, unbounded {@code UnicastSubject} that + * caches all items and the terminal event it receives.</li> + * <li>{@link #create(int)} - creates an empty, unbounded {@code UnicastSubject} + * with a hint about how many <b>total</b> items one expects to retain.</li> + * <li>{@link #create(boolean)} - creates an empty, unbounded {@code UnicastSubject} that + * optionally delays an error it receives and replays it after the regular items have been emitted.</li> + * <li>{@link #create(int, Runnable)} - creates an empty, unbounded {@code UnicastSubject} + * with a hint about how many <b>total</b> items one expects to retain and a callback that will be + * called exactly once when the {@code UnicastSubject} gets terminated or the single {@code Observer} disposes.</li> + * <li>{@link #create(int, Runnable, boolean)} - creates an empty, unbounded {@code UnicastSubject} + * with a hint about how many <b>total</b> items one expects to retain and a callback that will be + * called exactly once when the {@code UnicastSubject} gets terminated or the single {@code Observer} disposes + * and optionally delays an error it receives and replays it after the regular items have been emitted.</li> + * </ul> + * <p> + * If more than one {@code Observer} attempts to subscribe to this {@code UnicastSubject}, they + * will receive an {@code IllegalStateException} indicating the single-use-only nature of this {@code UnicastSubject}, + * even if the {@code UnicastSubject} already terminated with an error. + * <p> + * Since a {@code Subject} is conceptionally derived from the {@code Processor} type in the Reactive Streams specification, + * {@code null}s are not allowed (<a href="https://github.com/reactive-streams/reactive-streams-jvm#2.13">Rule 2.13</a>) as + * parameters to {@link #onNext(Object)} and {@link #onError(Throwable)}. Such calls will result in a + * {@link NullPointerException} being thrown and the subject's state is not changed. + * <p> + * Since a {@code UnicastSubject} is an {@link io.reactivex.rxjava3.core.Observable}, it does not support backpressure. + * <p> + * When this {@code UnicastSubject} is terminated via {@link #onError(Throwable)} the current or late single {@code Observer} + * may receive the {@code Throwable} before any available items could be emitted. To make sure an onError event is delivered + * to the {@code Observer} after the normal items, create a {@code UnicastSubject} with the {@link #create(boolean)} or + * {@link #create(int, Runnable, boolean)} factory methods. + * <p> + * Even though {@code UnicastSubject} implements the {@code Observer} interface, calling + * {@code onSubscribe} is not required (<a href="https://github.com/reactive-streams/reactive-streams-jvm#2.12">Rule 2.12</a>) + * if the subject is used as a standalone source. However, calling {@code onSubscribe} + * after the {@code UnicastSubject} reached its terminal state will result in the + * given {@code Disposable} being disposed immediately. + * <p> + * Calling {@link #onNext(Object)}, {@link #onError(Throwable)} and {@link #onComplete()} + * is required to be serialized (called from the same thread or called non-overlappingly from different threads + * through external means of serialization). The {@link #toSerialized()} method available to all {@code Subject}s + * provides such serialization and also protects against reentrance (i.e., when a downstream {@code Observer} + * consuming this subject also wants to call {@link #onNext(Object)} on this subject recursively). + * <p> + * This {@code UnicastSubject} supports the standard state-peeking methods {@link #hasComplete()}, {@link #hasThrowable()}, + * {@link #getThrowable()} and {@link #hasObservers()}. + * <dl> + * <dt><b>Scheduler:</b></dt> + * <dd>{@code UnicastSubject} does not operate by default on a particular {@link io.reactivex.rxjava3.core.Scheduler} and + * the single {@code Observer} gets notified on the thread the respective {@code onXXX} methods were invoked.</dd> + * <dt><b>Error handling:</b></dt> + * <dd>When the {@link #onError(Throwable)} is called, the {@code UnicastSubject} enters into a terminal state + * and emits the same {@code Throwable} instance to the current single {@code Observer}. During this emission, + * if the single {@code Observer}s disposes its respective {@code Disposable}, the + * {@code Throwable} is delivered to the global error handler via + * {@link io.reactivex.rxjava3.plugins.RxJavaPlugins#onError(Throwable)}. + * If there were no {@code Observer}s subscribed to this {@code UnicastSubject} when the {@code onError()} + * was called, the global error handler is not invoked. + * </dd> + * </dl> + * <p> + * Example usage: + * <pre><code> + * UnicastSubject<Integer> subject = UnicastSubject.create(); + * + * TestObserver<Integer> to1 = subject.test(); + * + * // fresh UnicastSubjects are empty + * to1.assertEmpty(); + * + * TestObserver<Integer> to2 = subject.test(); + * + * // A UnicastSubject only allows one Observer during its lifetime + * to2.assertFailure(IllegalStateException.class); + * + * subject.onNext(1); + * to1.assertValue(1); + * + * subject.onNext(2); + * to1.assertValues(1, 2); + * + * subject.onComplete(); + * to1.assertResult(1, 2); + * + * // ---------------------------------------------------- + * + * UnicastSubject<Integer> subject2 = UnicastSubject.create(); + * + * // a UnicastSubject caches events until its single Observer subscribes + * subject2.onNext(1); + * subject2.onNext(2); + * subject2.onComplete(); + * + * TestObserver<Integer> to3 = subject2.test(); + * + * // the cached events are emitted in order + * to3.assertResult(1, 2); + * </code></pre> + * @param <T> the value type received and emitted by this Subject subclass + * @since 2.0 + */ +public final class UnicastSubject<T> extends Subject<T> { + /** The queue that buffers the source events. */ + final SpscLinkedArrayQueue<T> queue; + + /** The single Observer. */ + final AtomicReference<Observer<? super T>> downstream; + + /** The optional callback when the Subject gets cancelled or terminates. */ + final AtomicReference<Runnable> onTerminate; + + /** deliver onNext events before error event. */ + final boolean delayError; + + /** Indicates the single observer has cancelled. */ + volatile boolean disposed; + + /** Indicates the source has terminated. */ + volatile boolean done; + /** + * The terminal error if not null. + * Must be set before writing to done and read after done == true. + */ + Throwable error; + + /** Set to 1 atomically for the first and only Subscriber. */ + final AtomicBoolean once; + + /** The wip counter and QueueDisposable surface. */ + final BasicIntQueueDisposable<T> wip; + + boolean enableOperatorFusion; + + /** + * Creates an UnicastSubject with an internal buffer capacity hint 16. + * @param <T> the value type + * @return an UnicastSubject instance + */ + @CheckReturnValue + @NonNull + public static <T> UnicastSubject<T> create() { + return new UnicastSubject<>(bufferSize(), null, true); + } + + /** + * Creates an UnicastSubject with the given internal buffer capacity hint. + * @param <T> the value type + * @param capacityHint the hint to size the internal unbounded buffer + * @return an UnicastSubject instance + * @throws IllegalArgumentException if {@code capacityHint} is non-positive + */ + @CheckReturnValue + @NonNull + public static <T> UnicastSubject<T> create(int capacityHint) { + ObjectHelper.verifyPositive(capacityHint, "capacityHint"); + return new UnicastSubject<>(capacityHint, null, true); + } + + /** + * Creates an UnicastSubject with the given internal buffer capacity hint and a callback for + * the case when the single Subscriber cancels its subscription + * or the subject is terminated. + * + * <p>The callback, if not null, is called exactly once and + * non-overlapped with any active replay. + * + * @param <T> the value type + * @param capacityHint the hint to size the internal unbounded buffer + * @param onTerminate the callback to run when the Subject is terminated or cancelled, null not allowed + * @return an UnicastSubject instance + * @throws NullPointerException if {@code onTerminate} is {@code null} + * @throws IllegalArgumentException if {@code capacityHint} is non-positive + */ + @CheckReturnValue + @NonNull + public static <T> UnicastSubject<T> create(int capacityHint, @NonNull Runnable onTerminate) { + ObjectHelper.verifyPositive(capacityHint, "capacityHint"); + Objects.requireNonNull(onTerminate, "onTerminate"); + return new UnicastSubject<>(capacityHint, onTerminate, true); + } + + /** + * Creates an UnicastSubject with the given internal buffer capacity hint, delay error flag and + * a callback for the case when the single Observer disposes its {@link Disposable} + * or the subject is terminated. + * + * <p>The callback, if not null, is called exactly once and + * non-overlapped with any active replay. + * <p>History: 2.0.8 - experimental + * @param <T> the value type + * @param capacityHint the hint to size the internal unbounded buffer + * @param onTerminate the callback to run when the Subject is terminated or cancelled, null not allowed + * @param delayError deliver pending onNext events before onError + * @return an UnicastSubject instance + * @throws NullPointerException if {@code onTerminate} is {@code null} + * @throws IllegalArgumentException if {@code capacityHint} is non-positive + * @since 2.2 + */ + @CheckReturnValue + @NonNull + public static <T> UnicastSubject<T> create(int capacityHint, @NonNull Runnable onTerminate, boolean delayError) { + ObjectHelper.verifyPositive(capacityHint, "capacityHint"); + Objects.requireNonNull(onTerminate, "onTerminate"); + return new UnicastSubject<>(capacityHint, onTerminate, delayError); + } + + /** + * Creates an UnicastSubject with an internal buffer capacity hint 16 and given delay error flag. + * + * <p>The callback, if not null, is called exactly once and + * non-overlapped with any active replay. + * <p>History: 2.0.8 - experimental + * @param <T> the value type + * @param delayError deliver pending onNext events before onError + * @return an UnicastSubject instance + * @since 2.2 + */ + @CheckReturnValue + @NonNull + public static <T> UnicastSubject<T> create(boolean delayError) { + return new UnicastSubject<>(bufferSize(), null, delayError); + } + + /** + * Creates an UnicastSubject with the given capacity hint, delay error flag and callback + * for when the Subject is terminated normally or its single Subscriber cancels. + * <p>History: 2.0.8 - experimental + * @param capacityHint the capacity hint for the internal, unbounded queue + * @param onTerminate the callback to run when the Subject is terminated or cancelled, null not allowed + * @param delayError deliver pending onNext events before onError + * @since 2.2 + */ + UnicastSubject(int capacityHint, Runnable onTerminate, boolean delayError) { + this.queue = new SpscLinkedArrayQueue<>(capacityHint); + this.onTerminate = new AtomicReference<>(onTerminate); + this.delayError = delayError; + this.downstream = new AtomicReference<>(); + this.once = new AtomicBoolean(); + this.wip = new UnicastQueueDisposable(); + } + + @Override + protected void subscribeActual(Observer<? super T> observer) { + if (!once.get() && once.compareAndSet(false, true)) { + observer.onSubscribe(wip); + downstream.lazySet(observer); // full barrier in drain + if (disposed) { + downstream.lazySet(null); + return; + } + drain(); + } else { + EmptyDisposable.error(new IllegalStateException("Only a single observer allowed."), observer); + } + } + + void doTerminate() { + Runnable r = onTerminate.get(); + if (r != null && onTerminate.compareAndSet(r, null)) { + r.run(); + } + } + + @Override + public void onSubscribe(Disposable d) { + if (done || disposed) { + d.dispose(); + } + } + + @Override + public void onNext(T t) { + ExceptionHelper.nullCheck(t, "onNext called with a null value."); + if (done || disposed) { + return; + } + queue.offer(t); + drain(); + } + + @Override + public void onError(Throwable t) { + ExceptionHelper.nullCheck(t, "onError called with a null Throwable."); + if (done || disposed) { + RxJavaPlugins.onError(t); + return; + } + error = t; + done = true; + + doTerminate(); + + drain(); + } + + @Override + public void onComplete() { + if (done || disposed) { + return; + } + done = true; + + doTerminate(); + + drain(); + } + + void drainNormal(Observer<? super T> a) { + int missed = 1; + SimpleQueue<T> q = queue; + boolean failFast = !this.delayError; + boolean canBeError = true; + for (;;) { + for (;;) { + + if (disposed) { + downstream.lazySet(null); + q.clear(); + return; + } + + boolean d = this.done; + T v = queue.poll(); + boolean empty = v == null; + + if (d) { + if (failFast && canBeError) { + if (failedFast(q, a)) { + return; + } else { + canBeError = false; + } + } + + if (empty) { + errorOrComplete(a); + return; + } + } + + if (empty) { + break; + } + + a.onNext(v); + } + + missed = wip.addAndGet(-missed); + if (missed == 0) { + break; + } + } + } + + void drainFused(Observer<? super T> a) { + int missed = 1; + + final SpscLinkedArrayQueue<T> q = queue; + final boolean failFast = !delayError; + + for (;;) { + + if (disposed) { + downstream.lazySet(null); + return; + } + boolean d = done; + + if (failFast && d) { + if (failedFast(q, a)) { + return; + } + } + + a.onNext(null); + + if (d) { + errorOrComplete(a); + return; + } + + missed = wip.addAndGet(-missed); + if (missed == 0) { + break; + } + } + } + + void errorOrComplete(Observer<? super T> a) { + downstream.lazySet(null); + Throwable ex = error; + if (ex != null) { + a.onError(ex); + } else { + a.onComplete(); + } + } + + boolean failedFast(final SimpleQueue<T> q, Observer<? super T> a) { + Throwable ex = error; + if (ex != null) { + downstream.lazySet(null); + q.clear(); + a.onError(ex); + return true; + } else { + return false; + } + } + + void drain() { + if (wip.getAndIncrement() != 0) { + return; + } + + Observer<? super T> a = downstream.get(); + int missed = 1; + + for (;;) { + + if (a != null) { + if (enableOperatorFusion) { + drainFused(a); + } else { + drainNormal(a); + } + return; + } + + missed = wip.addAndGet(-missed); + if (missed == 0) { + break; + } + + a = downstream.get(); + } + } + + @Override + @CheckReturnValue + public boolean hasObservers() { + return downstream.get() != null; + } + + @Override + @Nullable + @CheckReturnValue + public Throwable getThrowable() { + if (done) { + return error; + } + return null; + } + + @Override + @CheckReturnValue + public boolean hasThrowable() { + return done && error != null; + } + + @Override + @CheckReturnValue + public boolean hasComplete() { + return done && error == null; + } + + final class UnicastQueueDisposable extends BasicIntQueueDisposable<T> { + + private static final long serialVersionUID = 7926949470189395511L; + + @Override + public int requestFusion(int mode) { + if ((mode & ASYNC) != 0) { + enableOperatorFusion = true; + return ASYNC; + } + return NONE; + } + + @Nullable + @Override + public T poll() { + return queue.poll(); + } + + @Override + public boolean isEmpty() { + return queue.isEmpty(); + } + + @Override + public void clear() { + queue.clear(); + } + + @Override + public void dispose() { + if (!disposed) { + disposed = true; + + doTerminate(); + + downstream.lazySet(null); + if (wip.getAndIncrement() == 0) { + downstream.lazySet(null); + if (!enableOperatorFusion) { + queue.clear(); + } + } + } + } + + @Override + public boolean isDisposed() { + return disposed; + } + + } +} diff --git a/src/main/java/io/reactivex/rxjava3/subjects/package-info.java b/src/main/java/io/reactivex/rxjava3/subjects/package-info.java new file mode 100644 index 0000000000..a2736d7eee --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/subjects/package-info.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +/** + * Classes representing so-called hot sources, aka <strong>subjects</strong>, that implement a base reactive class and + * the respective consumer type at once to allow forms of multicasting events to multiple + * consumers as well as consuming another base reactive type of their kind. + * <p> + * Available subject classes with their respective base classes and consumer interfaces: + * <br> + * <table border="1" style="border-collapse: collapse;"> + * <caption>The available subject classes with their respective base classes and consumer interfaces.</caption> + * <tr><td><b>Subject type</b></td><td><b>Base class</b></td><td><b>Consumer interface</b></td></tr> + * <tr> + * <td>{@link io.reactivex.rxjava3.subjects.Subject Subject} + * <br>   {@link io.reactivex.rxjava3.subjects.AsyncSubject AsyncSubject} + * <br>   {@link io.reactivex.rxjava3.subjects.BehaviorSubject BehaviorSubject} + * <br>   {@link io.reactivex.rxjava3.subjects.PublishSubject PublishSubject} + * <br>   {@link io.reactivex.rxjava3.subjects.ReplaySubject ReplaySubject} + * <br>   {@link io.reactivex.rxjava3.subjects.UnicastSubject UnicastSubject} + * </td> + * <td>{@link io.reactivex.rxjava3.core.Observable Observable}</td> + * <td>{@link io.reactivex.rxjava3.core.Observer Observer}</td> + * </tr> + * <tr> + * <td>{@link io.reactivex.rxjava3.subjects.SingleSubject SingleSubject}</td> + * <td>{@link io.reactivex.rxjava3.core.Single Single}</td> + * <td>{@link io.reactivex.rxjava3.core.SingleObserver SingleObserver}</td> + * </tr> + * <tr> + * <td>{@link io.reactivex.rxjava3.subjects.MaybeSubject MaybeSubject}</td> + * <td>{@link io.reactivex.rxjava3.core.Maybe Maybe}</td> + * <td>{@link io.reactivex.rxjava3.core.MaybeObserver MaybeObserver}</td> + * </tr> + * <tr> + * <td>{@link io.reactivex.rxjava3.subjects.CompletableSubject CompletableSubject}</td> + * <td>{@link io.reactivex.rxjava3.core.Completable Completable}</td> + * <td>{@link io.reactivex.rxjava3.core.CompletableObserver CompletableObserver}</td> + * </tr> + * </table> + * <p> + * The backpressure-aware variants of the {@code Subject} class are called + * {@link org.reactivestreams.Processor}s and reside in the {@code io.reactivex.processors} package. + * @see io.reactivex.rxjava3.processors + */ +package io.reactivex.rxjava3.subjects; diff --git a/src/main/java/io/reactivex/rxjava3/subscribers/DefaultSubscriber.java b/src/main/java/io/reactivex/rxjava3/subscribers/DefaultSubscriber.java new file mode 100644 index 0000000000..9f8385b586 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/subscribers/DefaultSubscriber.java @@ -0,0 +1,116 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.subscribers; + +import org.reactivestreams.Subscription; + +import io.reactivex.rxjava3.core.FlowableSubscriber; +import io.reactivex.rxjava3.internal.subscriptions.SubscriptionHelper; +import io.reactivex.rxjava3.internal.util.EndConsumerHelper; + +/** + * Abstract base implementation of a {@link org.reactivestreams.Subscriber Subscriber} with + * support for requesting via {@link #request(long)}, cancelling via + * via {@link #cancel()} (both synchronously) and calls {@link #onStart()} + * when the subscription happens. + * + * <p>All pre-implemented final methods are thread-safe. + * + * <p>The default {@link #onStart()} requests {@link Long#MAX_VALUE} by default. Override + * the method to request a custom <em>positive</em> amount. + * + * <p>Note that calling {@link #request(long)} from {@link #onStart()} may trigger + * an immediate, asynchronous emission of data to {@link #onNext(Object)}. Make sure + * all initialization happens before the call to {@code request()} in {@code onStart()}. + * Calling {@link #request(long)} inside {@link #onNext(Object)} can happen at any time + * because by design, {@code onNext} calls from upstream are non-reentrant and non-overlapping. + * + * <p>Use the protected {@link #cancel()} to cancel the sequence from within an + * {@code onNext} implementation. + * + * <p>Like all other consumers, {@code DefaultSubscriber} can be subscribed only once. + * Any subsequent attempt to subscribe it to a new source will yield an + * {@link IllegalStateException} with message {@code "It is not allowed to subscribe with a(n) <class name> multiple times."}. + * + * <p>Implementation of {@link #onStart()}, {@link #onNext(Object)}, {@link #onError(Throwable)} + * and {@link #onComplete()} are not allowed to throw any unchecked exceptions. + * If for some reason this can't be avoided, use {@link io.reactivex.rxjava3.core.Flowable#safeSubscribe(org.reactivestreams.Subscriber)} + * instead of the standard {@code subscribe()} method. + * @param <T> the value type + * + * <p>Example<pre><code> + * Flowable.range(1, 5) + * .subscribe(new DefaultSubscriber<Integer>() { + * @Override public void onStart() { + * System.out.println("Start!"); + * request(1); + * } + * @Override public void onNext(Integer t) { + * if (t == 3) { + * cancel(); + * } + * System.out.println(t); + * request(1); + * } + * @Override public void onError(Throwable t) { + * t.printStackTrace(); + * } + * @Override public void onComplete() { + * System.out.println("Done!"); + * } + * }); + * </code></pre> + */ +public abstract class DefaultSubscriber<T> implements FlowableSubscriber<T> { + + Subscription upstream; + + @Override + public final void onSubscribe(Subscription s) { + if (EndConsumerHelper.validate(this.upstream, s, getClass())) { + this.upstream = s; + onStart(); + } + } + + /** + * Requests from the upstream {@link Subscription}. + * @param n the request amount, positive + */ + protected final void request(long n) { + Subscription s = this.upstream; + if (s != null) { + s.request(n); + } + } + + /** + * Cancels the upstream's {@link Subscription}. + */ + protected final void cancel() { + Subscription s = this.upstream; + this.upstream = SubscriptionHelper.CANCELLED; + s.cancel(); + } + /** + * Called once the subscription has been set on this observer; override this + * to perform initialization or issue an initial request. + * <p> + * The default implementation requests {@link Long#MAX_VALUE}. + */ + protected void onStart() { + request(Long.MAX_VALUE); + } + +} diff --git a/src/main/java/io/reactivex/rxjava3/subscribers/DisposableSubscriber.java b/src/main/java/io/reactivex/rxjava3/subscribers/DisposableSubscriber.java new file mode 100644 index 0000000000..a321283018 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/subscribers/DisposableSubscriber.java @@ -0,0 +1,123 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.subscribers; + +import java.util.concurrent.atomic.AtomicReference; + +import org.reactivestreams.Subscription; + +import io.reactivex.rxjava3.core.FlowableSubscriber; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.subscriptions.SubscriptionHelper; +import io.reactivex.rxjava3.internal.util.EndConsumerHelper; + +/** + * An abstract Subscriber that allows asynchronous, external cancellation by implementing {@link Disposable}. + * + * <p>All pre-implemented final methods are thread-safe. + * + * <p>The default {@link #onStart()} requests {@link Long#MAX_VALUE} by default. Override + * the method to request a custom <em>positive</em> amount. Use the protected {@link #request(long)} + * to request more items and {@link #cancel()} to cancel the sequence from within an + * {@code onNext} implementation. + * + * <p>Note that calling {@link #request(long)} from {@link #onStart()} may trigger + * an immediate, asynchronous emission of data to {@link #onNext(Object)}. Make sure + * all initialization happens before the call to {@code request()} in {@code onStart()}. + * Calling {@link #request(long)} inside {@link #onNext(Object)} can happen at any time + * because by design, {@code onNext} calls from upstream are non-reentrant and non-overlapping. + * + * <p>Like all other consumers, {@code DisposableSubscriber} can be subscribed only once. + * Any subsequent attempt to subscribe it to a new source will yield an + * {@link IllegalStateException} with message {@code "It is not allowed to subscribe with a(n) <class name> multiple times."}. + * + * <p>Implementation of {@link #onStart()}, {@link #onNext(Object)}, {@link #onError(Throwable)} + * and {@link #onComplete()} are not allowed to throw any unchecked exceptions. + * If for some reason this can't be avoided, use {@link io.reactivex.rxjava3.core.Flowable#safeSubscribe(org.reactivestreams.Subscriber)} + * instead of the standard {@code subscribe()} method. + * + * <p>Example<pre><code> + * Disposable d = + * Flowable.range(1, 5) + * .subscribeWith(new DisposableSubscriber<Integer>() { + * @Override public void onStart() { + * request(1); + * } + * @Override public void onNext(Integer t) { + * if (t == 3) { + * cancel(); + * } + * System.out.println(t); + * request(1); + * } + * @Override public void onError(Throwable t) { + * t.printStackTrace(); + * } + * @Override public void onComplete() { + * System.out.println("Done!"); + * } + * }); + * // ... + * d.dispose(); + * </code></pre> + * @param <T> the received value type. + */ +public abstract class DisposableSubscriber<T> implements FlowableSubscriber<T>, Disposable { + final AtomicReference<Subscription> upstream = new AtomicReference<>(); + + @Override + public final void onSubscribe(Subscription s) { + if (EndConsumerHelper.setOnce(this.upstream, s, getClass())) { + onStart(); + } + } + + /** + * Called once the single upstream {@link Subscription} is set via {@link #onSubscribe(Subscription)}. + */ + protected void onStart() { + upstream.get().request(Long.MAX_VALUE); + } + + /** + * Requests the specified amount from the upstream if its {@link Subscription} is set via + * onSubscribe already. + * <p>Note that calling this method before a {@link Subscription} is set via {@link #onSubscribe(Subscription)} + * leads to {@link NullPointerException} and meant to be called from inside {@link #onStart()} or + * {@link #onNext(Object)}. + * @param n the request amount, positive + */ + protected final void request(long n) { + upstream.get().request(n); + } + + /** + * Cancels the Subscription set via {@link #onSubscribe(Subscription)} or makes sure a + * {@link Subscription} set asynchronously (later) is cancelled immediately. + * <p>This method is thread-safe and can be exposed as a public API. + */ + protected final void cancel() { + dispose(); + } + + @Override + public final boolean isDisposed() { + return upstream.get() == SubscriptionHelper.CANCELLED; + } + + @Override + public final void dispose() { + SubscriptionHelper.cancel(upstream); + } +} diff --git a/src/main/java/io/reactivex/rxjava3/subscribers/ResourceSubscriber.java b/src/main/java/io/reactivex/rxjava3/subscribers/ResourceSubscriber.java new file mode 100644 index 0000000000..95dccfd2b1 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/subscribers/ResourceSubscriber.java @@ -0,0 +1,172 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.subscribers; + +import java.util.Objects; +import java.util.concurrent.atomic.*; + +import org.reactivestreams.Subscription; + +import io.reactivex.rxjava3.core.FlowableSubscriber; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.disposables.ListCompositeDisposable; +import io.reactivex.rxjava3.internal.subscriptions.SubscriptionHelper; +import io.reactivex.rxjava3.internal.util.EndConsumerHelper; + +/** + * An abstract Subscriber that allows asynchronous cancellation of its + * subscription and associated resources. + * + * <p>All pre-implemented final methods are thread-safe. + * + * <p>To release the associated resources, one has to call {@link #dispose()} + * in {@code onError()} and {@code onComplete()} explicitly. + * + * <p>Use {@link #add(Disposable)} to associate resources (as {@link io.reactivex.rxjava3.disposables.Disposable Disposable}s) + * with this {@code ResourceSubscriber} that will be cleaned up when {@link #dispose()} is called. + * Removing previously associated resources is not possible but one can create a + * {@link io.reactivex.rxjava3.disposables.CompositeDisposable CompositeDisposable}, associate it with this + * {@code ResourceSubscriber} and then add/remove resources to/from the {@code CompositeDisposable} + * freely. + * + * <p>The default {@link #onStart()} requests {@link Long#MAX_VALUE} by default. Override + * the method to request a custom <em>positive</em> amount. Use the protected {@link #request(long)} + * to request more items and {@link #dispose()} to cancel the sequence from within an + * {@code onNext} implementation. + * + * <p>Note that calling {@link #request(long)} from {@link #onStart()} may trigger + * an immediate, asynchronous emission of data to {@link #onNext(Object)}. Make sure + * all initialization happens before the call to {@code request()} in {@code onStart()}. + * Calling {@link #request(long)} inside {@link #onNext(Object)} can happen at any time + * because by design, {@code onNext} calls from upstream are non-reentrant and non-overlapping. + * + * <p>Like all other consumers, {@code ResourceSubscriber} can be subscribed only once. + * Any subsequent attempt to subscribe it to a new source will yield an + * {@link IllegalStateException} with message {@code "It is not allowed to subscribe with a(n) <class name> multiple times."}. + * + * <p>Implementation of {@link #onStart()}, {@link #onNext(Object)}, {@link #onError(Throwable)} + * and {@link #onComplete()} are not allowed to throw any unchecked exceptions. + * If for some reason this can't be avoided, use {@link io.reactivex.rxjava3.core.Flowable#safeSubscribe(org.reactivestreams.Subscriber)} + * instead of the standard {@code subscribe()} method. + * + * <p>Example<pre><code> + * Disposable d = + * Flowable.range(1, 5) + * .subscribeWith(new ResourceSubscriber<Integer>() { + * @Override public void onStart() { + * add(Schedulers.single() + * .scheduleDirect(() -> System.out.println("Time!"), + * 2, TimeUnit.SECONDS)); + * request(1); + * } + * @Override public void onNext(Integer t) { + * if (t == 3) { + * dispose(); + * } + * System.out.println(t); + * request(1); + * } + * @Override public void onError(Throwable t) { + * t.printStackTrace(); + * dispose(); + * } + * @Override public void onComplete() { + * System.out.println("Done!"); + * dispose(); + * } + * }); + * // ... + * d.dispose(); + * </code></pre> + * + * @param <T> the value type + */ +public abstract class ResourceSubscriber<T> implements FlowableSubscriber<T>, Disposable { + /** The active subscription. */ + private final AtomicReference<Subscription> upstream = new AtomicReference<>(); + + /** The resource composite, can never be null. */ + private final ListCompositeDisposable resources = new ListCompositeDisposable(); + + /** Remembers the request(n) counts until a subscription arrives. */ + private final AtomicLong missedRequested = new AtomicLong(); + + /** + * Adds a resource to this {@code ResourceSubscriber}. + * + * @param resource the resource to add + * + * @throws NullPointerException if {@code resource} is {@code null} + */ + public final void add(Disposable resource) { + Objects.requireNonNull(resource, "resource is null"); + resources.add(resource); + } + + @Override + public final void onSubscribe(Subscription s) { + if (EndConsumerHelper.setOnce(this.upstream, s, getClass())) { + long r = missedRequested.getAndSet(0L); + if (r != 0L) { + s.request(r); + } + onStart(); + } + } + + /** + * Called once the upstream sets a {@link Subscription} on this {@code ResourceSubscriber}. + * + * <p>You can perform initialization at this moment. The default + * implementation requests {@link Long#MAX_VALUE} from upstream. + */ + protected void onStart() { + request(Long.MAX_VALUE); + } + + /** + * Request the specified amount of elements from upstream. + * + * <p>This method can be called before the upstream calls {@link #onSubscribe(Subscription)}. + * When the subscription happens, all missed requests are requested. + * + * @param n the request amount, must be positive + */ + protected final void request(long n) { + SubscriptionHelper.deferredRequest(upstream, missedRequested, n); + } + + /** + * Cancels the subscription (if any) and disposes the resources associated with + * this {@code ResourceSubscriber} (if any). + * + * <p>This method can be called before the upstream calls {@link #onSubscribe(Subscription)} at which + * case the {@link Subscription} will be immediately cancelled. + */ + @Override + public final void dispose() { + if (SubscriptionHelper.cancel(upstream)) { + resources.dispose(); + } + } + + /** + * Returns true if this {@code ResourceSubscriber} has been disposed/cancelled. + * @return true if this {@code ResourceSubscriber} has been disposed/cancelled + */ + @Override + public final boolean isDisposed() { + return upstream.get() == SubscriptionHelper.CANCELLED; + } +} diff --git a/src/main/java/io/reactivex/rxjava3/subscribers/SafeSubscriber.java b/src/main/java/io/reactivex/rxjava3/subscribers/SafeSubscriber.java new file mode 100644 index 0000000000..f66c34b9b9 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/subscribers/SafeSubscriber.java @@ -0,0 +1,237 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.subscribers; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.annotations.NonNull; +import io.reactivex.rxjava3.core.FlowableSubscriber; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.internal.subscriptions.*; +import io.reactivex.rxjava3.internal.util.ExceptionHelper; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +/** + * Wraps another {@link Subscriber} and ensures all {@code onXXX} methods conform the protocol + * (except the requirement for serialized access). + * + * @param <T> the value type + */ +public final class SafeSubscriber<@NonNull T> implements FlowableSubscriber<T>, Subscription { + /** The actual Subscriber. */ + final Subscriber<? super T> downstream; + /** The subscription. */ + Subscription upstream; + /** Indicates a terminal state. */ + boolean done; + + /** + * Constructs a {@code SafeSubscriber} by wrapping the given actual {@link Subscriber}. + * @param downstream the actual {@code Subscriber} to wrap, not {@code null} (not validated) + */ + public SafeSubscriber(@NonNull Subscriber<? super T> downstream) { + this.downstream = downstream; + } + + @Override + public void onSubscribe(@NonNull Subscription s) { + if (SubscriptionHelper.validate(this.upstream, s)) { + this.upstream = s; + try { + downstream.onSubscribe(this); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + done = true; + // can't call onError because the actual's state may be corrupt at this point + try { + s.cancel(); + } catch (Throwable e1) { + Exceptions.throwIfFatal(e1); + RxJavaPlugins.onError(new CompositeException(e, e1)); + return; + } + RxJavaPlugins.onError(e); + } + } + } + + @Override + public void onNext(@NonNull T t) { + if (done) { + return; + } + if (upstream == null) { + onNextNoSubscription(); + return; + } + + if (t == null) { + Throwable ex = ExceptionHelper.createNullPointerException("onNext called with a null Throwable."); + try { + upstream.cancel(); + } catch (Throwable e1) { + Exceptions.throwIfFatal(e1); + onError(new CompositeException(ex, e1)); + return; + } + onError(ex); + return; + } + + try { + downstream.onNext(t); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + try { + upstream.cancel(); + } catch (Throwable e1) { + Exceptions.throwIfFatal(e1); + onError(new CompositeException(e, e1)); + return; + } + onError(e); + } + } + + void onNextNoSubscription() { + done = true; + Throwable ex = new NullPointerException("Subscription not set!"); + + try { + downstream.onSubscribe(EmptySubscription.INSTANCE); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + // can't call onError because the actual's state may be corrupt at this point + RxJavaPlugins.onError(new CompositeException(ex, e)); + return; + } + try { + downstream.onError(ex); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + // if onError failed, all that's left is to report the error to plugins + RxJavaPlugins.onError(new CompositeException(ex, e)); + } + } + + @Override + public void onError(@NonNull Throwable t) { + if (done) { + RxJavaPlugins.onError(t); + return; + } + done = true; + + if (upstream == null) { + Throwable npe = new NullPointerException("Subscription not set!"); + + try { + downstream.onSubscribe(EmptySubscription.INSTANCE); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + // can't call onError because the actual's state may be corrupt at this point + RxJavaPlugins.onError(new CompositeException(t, npe, e)); + return; + } + try { + downstream.onError(new CompositeException(t, npe)); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + // if onError failed, all that's left is to report the error to plugins + RxJavaPlugins.onError(new CompositeException(t, npe, e)); + } + return; + } + + if (t == null) { + t = ExceptionHelper.createNullPointerException("onError called with a null Throwable."); + } + + try { + downstream.onError(t); + } catch (Throwable ex) { + Exceptions.throwIfFatal(ex); + + RxJavaPlugins.onError(new CompositeException(t, ex)); + } + } + + @Override + public void onComplete() { + if (done) { + return; + } + done = true; + + if (upstream == null) { + onCompleteNoSubscription(); + return; + } + + try { + downstream.onComplete(); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + RxJavaPlugins.onError(e); + } + } + + void onCompleteNoSubscription() { + + Throwable ex = new NullPointerException("Subscription not set!"); + + try { + downstream.onSubscribe(EmptySubscription.INSTANCE); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + // can't call onError because the actual's state may be corrupt at this point + RxJavaPlugins.onError(new CompositeException(ex, e)); + return; + } + try { + downstream.onError(ex); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + // if onError failed, all that's left is to report the error to plugins + RxJavaPlugins.onError(new CompositeException(ex, e)); + } + } + + @Override + public void request(long n) { + try { + upstream.request(n); + } catch (Throwable e) { + Exceptions.throwIfFatal(e); + try { + upstream.cancel(); + } catch (Throwable e1) { + Exceptions.throwIfFatal(e1); + RxJavaPlugins.onError(new CompositeException(e, e1)); + return; + } + RxJavaPlugins.onError(e); + } + } + + @Override + public void cancel() { + try { + upstream.cancel(); + } catch (Throwable e1) { + Exceptions.throwIfFatal(e1); + RxJavaPlugins.onError(e1); + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/subscribers/SerializedSubscriber.java b/src/main/java/io/reactivex/rxjava3/subscribers/SerializedSubscriber.java new file mode 100644 index 0000000000..1ea8b4021a --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/subscribers/SerializedSubscriber.java @@ -0,0 +1,202 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.subscribers; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.annotations.NonNull; +import io.reactivex.rxjava3.core.FlowableSubscriber; +import io.reactivex.rxjava3.internal.subscriptions.SubscriptionHelper; +import io.reactivex.rxjava3.internal.util.*; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +/** + * Serializes access to the {@link Subscriber#onNext(Object)}, {@link Subscriber#onError(Throwable)} and + * {@link Subscriber#onComplete()} methods of another {@link Subscriber}. + * + * <p>Note that {@link #onSubscribe(Subscription)} is not serialized in respect of the other methods so + * make sure the {@code onSubscribe} is called with a non-{@code null} {@link Subscription} + * before any of the other methods are called. + * + * <p>The implementation assumes that the actual {@code Subscriber}'s methods don't throw. + * + * @param <T> the value type + */ +public final class SerializedSubscriber<T> implements FlowableSubscriber<T>, Subscription { + final Subscriber<? super T> downstream; + final boolean delayError; + + static final int QUEUE_LINK_SIZE = 4; + + Subscription upstream; + + boolean emitting; + AppendOnlyLinkedArrayList<Object> queue; + + volatile boolean done; + + /** + * Construct a {@code SerializedSubscriber} by wrapping the given actual {@link Subscriber}. + * @param downstream the actual {@code Subscriber}, not null (not verified) + */ + public SerializedSubscriber(Subscriber<? super T> downstream) { + this(downstream, false); + } + + /** + * Construct a {@code SerializedSubscriber} by wrapping the given actual {@link Subscriber} and + * optionally delaying the errors till all regular values have been emitted + * from the internal buffer. + * @param actual the actual {@code Subscriber}, not {@code null} (not verified) + * @param delayError if {@code true}, errors are emitted after regular values have been emitted + */ + public SerializedSubscriber(@NonNull Subscriber<? super T> actual, boolean delayError) { + this.downstream = actual; + this.delayError = delayError; + } + + @Override + public void onSubscribe(@NonNull Subscription s) { + if (SubscriptionHelper.validate(this.upstream, s)) { + this.upstream = s; + downstream.onSubscribe(this); + } + } + + @Override + public void onNext(@NonNull T t) { + if (done) { + return; + } + if (t == null) { + upstream.cancel(); + onError(ExceptionHelper.createNullPointerException("onNext called with a null value.")); + return; + } + synchronized (this) { + if (done) { + return; + } + if (emitting) { + AppendOnlyLinkedArrayList<Object> q = queue; + if (q == null) { + q = new AppendOnlyLinkedArrayList<>(QUEUE_LINK_SIZE); + queue = q; + } + q.add(NotificationLite.next(t)); + return; + } + emitting = true; + } + + downstream.onNext(t); + + emitLoop(); + } + + @Override + public void onError(Throwable t) { + if (done) { + RxJavaPlugins.onError(t); + return; + } + boolean reportError; + synchronized (this) { + if (done) { + reportError = true; + } else + if (emitting) { + done = true; + AppendOnlyLinkedArrayList<Object> q = queue; + if (q == null) { + q = new AppendOnlyLinkedArrayList<>(QUEUE_LINK_SIZE); + queue = q; + } + Object err = NotificationLite.error(t); + if (delayError) { + q.add(err); + } else { + q.setFirst(err); + } + return; + } else { + done = true; + emitting = true; + reportError = false; + } + } + + if (reportError) { + RxJavaPlugins.onError(t); + return; + } + + downstream.onError(t); + // no need to loop because this onError is the last event + } + + @Override + public void onComplete() { + if (done) { + return; + } + synchronized (this) { + if (done) { + return; + } + if (emitting) { + AppendOnlyLinkedArrayList<Object> q = queue; + if (q == null) { + q = new AppendOnlyLinkedArrayList<>(QUEUE_LINK_SIZE); + queue = q; + } + q.add(NotificationLite.complete()); + return; + } + done = true; + emitting = true; + } + + downstream.onComplete(); + // no need to loop because this onComplete is the last event + } + + void emitLoop() { + for (;;) { + AppendOnlyLinkedArrayList<Object> q; + synchronized (this) { + q = queue; + if (q == null) { + emitting = false; + return; + } + queue = null; + } + + if (q.accept(downstream)) { + return; + } + } + } + + @Override + public void request(long n) { + upstream.request(n); + } + + @Override + public void cancel() { + upstream.cancel(); + } +} diff --git a/src/main/java/io/reactivex/rxjava3/subscribers/TestSubscriber.java b/src/main/java/io/reactivex/rxjava3/subscribers/TestSubscriber.java new file mode 100644 index 0000000000..1d96e7cfd3 --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/subscribers/TestSubscriber.java @@ -0,0 +1,308 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.subscribers; + +import java.util.concurrent.atomic.*; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.annotations.NonNull; +import io.reactivex.rxjava3.core.FlowableSubscriber; +import io.reactivex.rxjava3.internal.subscriptions.SubscriptionHelper; +import io.reactivex.rxjava3.observers.BaseTestConsumer; + +/** + * A {@link Subscriber} implementation that records events and allows making assertions about them. + * + * <p>You can override the {@link #onSubscribe(Subscription)}, {@link #onNext(Object)}, {@link #onError(Throwable)} and + * {@link #onComplete()} methods but not the others (this is by design). + * + * <p>When calling the default request method, you are requesting on behalf of the + * wrapped actual {@link Subscriber} if any. + * + * @param <T> the value type + */ +public class TestSubscriber<T> +extends BaseTestConsumer<T, TestSubscriber<T>> +implements FlowableSubscriber<T>, Subscription { + /** The actual subscriber to forward events to. */ + private final Subscriber<? super T> downstream; + + /** Makes sure the incoming Subscriptions get cancelled immediately. */ + private volatile boolean cancelled; + + /** Holds the current subscription if any. */ + private final AtomicReference<Subscription> upstream; + + /** Holds the requested amount until a subscription arrives. */ + private final AtomicLong missedRequested; + + /** + * Creates a {@code TestSubscriber} with {@link Long#MAX_VALUE} initial request amount. + * @param <T> the value type + * @return the new {@code TestSubscriber} instance. + * @see #create(long) + */ + @NonNull + public static <T> TestSubscriber<T> create() { + return new TestSubscriber<>(); + } + + /** + * Creates a {@code TestSubscriber} with the given initial request amount. + * @param <T> the value type + * @param initialRequested the initial requested amount + * @return the new {@code TestSubscriber} instance. + */ + @NonNull + public static <T> TestSubscriber<T> create(long initialRequested) { + return new TestSubscriber<>(initialRequested); + } + + /** + * Constructs a forwarding {@code TestSubscriber}. + * @param <T> the value type received + * @param delegate the actual {@link Subscriber} to forward events to + * @return the new TestObserver instance + */ + public static <T> TestSubscriber<T> create(@NonNull Subscriber<? super T> delegate) { + return new TestSubscriber<>(delegate); + } + + /** + * Constructs a non-forwarding {@code TestSubscriber} with an initial request value of {@link Long#MAX_VALUE}. + */ + public TestSubscriber() { + this(EmptySubscriber.INSTANCE, Long.MAX_VALUE); + } + + /** + * Constructs a non-forwarding {@code TestSubscriber} with the specified initial request value. + * <p>The {@code TestSubscriber} doesn't validate the {@code initialRequest} amount so one can + * test sources with invalid values as well. + * @param initialRequest the initial request amount + */ + public TestSubscriber(long initialRequest) { + this(EmptySubscriber.INSTANCE, initialRequest); + } + + /** + * Constructs a forwarding {@code TestSubscriber} but leaves the requesting to the wrapped {@link Subscriber}. + * @param downstream the actual {@code Subscriber} to forward events to + */ + public TestSubscriber(@NonNull Subscriber<? super T> downstream) { + this(downstream, Long.MAX_VALUE); + } + + /** + * Constructs a forwarding {@code TestSubscriber} with the specified initial request amount + * and an actual {@link Subscriber} to forward events to. + * <p>The {@code TestSubscriber} doesn't validate the initialRequest value so one can + * test sources with invalid values as well. + * @param actual the actual {@code Subscriber} to forward events to + * @param initialRequest the initial request amount + */ + public TestSubscriber(@NonNull Subscriber<? super T> actual, long initialRequest) { + super(); + if (initialRequest < 0) { + throw new IllegalArgumentException("Negative initial request not allowed"); + } + this.downstream = actual; + this.upstream = new AtomicReference<>(); + this.missedRequested = new AtomicLong(initialRequest); + } + + @Override + public void onSubscribe(@NonNull Subscription s) { + lastThread = Thread.currentThread(); + + if (s == null) { + errors.add(new NullPointerException("onSubscribe received a null Subscription")); + return; + } + if (!upstream.compareAndSet(null, s)) { + s.cancel(); + if (upstream.get() != SubscriptionHelper.CANCELLED) { + errors.add(new IllegalStateException("onSubscribe received multiple subscriptions: " + s)); + } + return; + } + + downstream.onSubscribe(s); + + long mr = missedRequested.getAndSet(0L); + if (mr != 0L) { + s.request(mr); + } + + onStart(); + } + + /** + * Called after the onSubscribe is called and handled. + */ + protected void onStart() { + + } + + @Override + public void onNext(@NonNull T t) { + if (!checkSubscriptionOnce) { + checkSubscriptionOnce = true; + if (upstream.get() == null) { + errors.add(new IllegalStateException("onSubscribe not called in proper order")); + } + } + lastThread = Thread.currentThread(); + + values.add(t); + + if (t == null) { + errors.add(new NullPointerException("onNext received a null value")); + } + + downstream.onNext(t); + } + + @Override + public void onError(@NonNull Throwable t) { + if (!checkSubscriptionOnce) { + checkSubscriptionOnce = true; + if (upstream.get() == null) { + errors.add(new IllegalStateException("onSubscribe not called in proper order")); + } + } + try { + lastThread = Thread.currentThread(); + + if (t == null) { + errors.add(new NullPointerException("onError received a null Throwable")); + } else { + errors.add(t); + } + + downstream.onError(t); + } finally { + done.countDown(); + } + } + + @Override + public void onComplete() { + if (!checkSubscriptionOnce) { + checkSubscriptionOnce = true; + if (upstream.get() == null) { + errors.add(new IllegalStateException("onSubscribe not called in proper order")); + } + } + try { + lastThread = Thread.currentThread(); + completions++; + + downstream.onComplete(); + } finally { + done.countDown(); + } + } + + @Override + public final void request(long n) { + SubscriptionHelper.deferredRequest(upstream, missedRequested, n); + } + + @Override + public final void cancel() { + if (!cancelled) { + cancelled = true; + SubscriptionHelper.cancel(upstream); + } + } + + /** + * Returns true if this {@code TestSubscriber} has been cancelled. + * @return true if this {@code TestSubscriber} has been cancelled + */ + public final boolean isCancelled() { + return cancelled; + } + + @Override + protected final void dispose() { + cancel(); + } + + @Override + protected final boolean isDisposed() { + return cancelled; + } + + // state retrieval methods + + /** + * Returns true if this {@code TestSubscriber} received a {@link Subscription} via {@link #onSubscribe(Subscription)}. + * @return true if this {@code TestSubscriber} received a {@link Subscription} via {@link #onSubscribe(Subscription)} + */ + public final boolean hasSubscription() { + return upstream.get() != null; + } + + // assertion methods + + /** + * Assert that the {@link #onSubscribe(Subscription)} method was called exactly once. + * @return this + */ + @Override + protected final TestSubscriber<T> assertSubscribed() { + if (upstream.get() == null) { + throw fail("Not subscribed!"); + } + return this; + } + + /** + * Calls {@link #request(long)} and returns this. + * <p>History: 2.0.1 - experimental + * @param n the request amount + * @return this + * @since 2.1 + */ + public final TestSubscriber<T> requestMore(long n) { + request(n); + return this; + } + + /** + * A subscriber that ignores all events and does not report errors. + */ + enum EmptySubscriber implements FlowableSubscriber<Object> { + INSTANCE; + + @Override + public void onSubscribe(Subscription s) { + } + + @Override + public void onNext(Object t) { + } + + @Override + public void onError(Throwable t) { + } + + @Override + public void onComplete() { + } + } +} diff --git a/src/main/java/io/reactivex/rxjava3/subscribers/package-info.java b/src/main/java/io/reactivex/rxjava3/subscribers/package-info.java new file mode 100644 index 0000000000..592aea185b --- /dev/null +++ b/src/main/java/io/reactivex/rxjava3/subscribers/package-info.java @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +/** + * Default wrappers and implementations for {@link org.reactivestreams.Subscriber Subscriber}-based consumer classes and interfaces, + * including disposable ({@link io.reactivex.rxjava3.subscribers.DisposableSubscriber DisposableSubscriber}) and resource-tracking + * ({@link io.reactivex.rxjava3.subscribers.ResourceSubscriber ResourceSubscriber}) + * variants and the {@link io.reactivex.rxjava3.subscribers.TestSubscriber TestSubscriber} that allows unit testing + * {@link io.reactivex.rxjava3.core.Flowable Flowable}-based flows. + */ +package io.reactivex.rxjava3.subscribers; diff --git a/src/main/module/module-info.java b/src/main/module/module-info.java new file mode 100644 index 0000000000..4bed327f1a --- /dev/null +++ b/src/main/module/module-info.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +module io.reactivex.rxjava3 { + exports io.reactivex.rxjava3.annotations; + exports io.reactivex.rxjava3.core; + exports io.reactivex.rxjava3.disposables; + exports io.reactivex.rxjava3.exceptions; + exports io.reactivex.rxjava3.flowables; + exports io.reactivex.rxjava3.functions; + exports io.reactivex.rxjava3.observables; + exports io.reactivex.rxjava3.observers; + exports io.reactivex.rxjava3.operators; + exports io.reactivex.rxjava3.parallel; + exports io.reactivex.rxjava3.plugins; + exports io.reactivex.rxjava3.processors; + exports io.reactivex.rxjava3.schedulers; + exports io.reactivex.rxjava3.subjects; + exports io.reactivex.rxjava3.subscribers; + + requires transitive org.reactivestreams; +} \ No newline at end of file diff --git a/src/main/resources/META-INF/proguard/rxjava3.pro b/src/main/resources/META-INF/proguard/rxjava3.pro new file mode 100644 index 0000000000..d51378e562 --- /dev/null +++ b/src/main/resources/META-INF/proguard/rxjava3.pro @@ -0,0 +1 @@ +-dontwarn java.util.concurrent.Flow* \ No newline at end of file diff --git a/src/test/java/io/reactivex/rxjava3/completable/CapturingUncaughtExceptionHandler.java b/src/test/java/io/reactivex/rxjava3/completable/CapturingUncaughtExceptionHandler.java new file mode 100644 index 0000000000..8379925bee --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/completable/CapturingUncaughtExceptionHandler.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.completable; + +import java.util.concurrent.CountDownLatch; + +public final class CapturingUncaughtExceptionHandler implements Thread.UncaughtExceptionHandler { + public int count; + public Throwable caught; + public CountDownLatch completed = new CountDownLatch(1); + + @Override + public void uncaughtException(Thread t, Throwable e) { + count++; + caught = e; + completed.countDown(); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/completable/CompletableRetryTest.java b/src/test/java/io/reactivex/rxjava3/completable/CompletableRetryTest.java new file mode 100644 index 0000000000..3d3ad65968 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/completable/CompletableRetryTest.java @@ -0,0 +1,155 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.completable; + +import static org.junit.Assert.assertEquals; + +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.functions.Functions; + +public class CompletableRetryTest extends RxJavaTest { + @Test + public void retryTimesPredicateWithMatchingPredicate() { + final AtomicInteger atomicInteger = new AtomicInteger(3); + final AtomicInteger numberOfSubscribeCalls = new AtomicInteger(0); + + Completable.fromAction(new Action() { + @Override public void run() throws Exception { + numberOfSubscribeCalls.incrementAndGet(); + + if (atomicInteger.decrementAndGet() != 0) { + throw new RuntimeException(); + } + + throw new IllegalArgumentException(); + } + }) + .retry(Integer.MAX_VALUE, new Predicate<Throwable>() { + @Override public boolean test(final Throwable throwable) throws Exception { + return !(throwable instanceof IllegalArgumentException); + } + }) + .test() + .assertFailure(IllegalArgumentException.class); + + assertEquals(3, numberOfSubscribeCalls.get()); + } + + @Test + public void retryTimesPredicateWithMatchingRetryAmount() { + final AtomicInteger atomicInteger = new AtomicInteger(3); + final AtomicInteger numberOfSubscribeCalls = new AtomicInteger(0); + + Completable.fromAction(new Action() { + @Override public void run() throws Exception { + numberOfSubscribeCalls.incrementAndGet(); + + if (atomicInteger.decrementAndGet() != 0) { + throw new RuntimeException(); + } + } + }) + .retry(2, Functions.alwaysTrue()) + .test() + .assertResult(); + + assertEquals(3, numberOfSubscribeCalls.get()); + } + + @Test + public void retryTimesPredicateWithNotMatchingRetryAmount() { + final AtomicInteger atomicInteger = new AtomicInteger(3); + final AtomicInteger numberOfSubscribeCalls = new AtomicInteger(0); + + Completable.fromAction(new Action() { + @Override public void run() throws Exception { + numberOfSubscribeCalls.incrementAndGet(); + + if (atomicInteger.decrementAndGet() != 0) { + throw new RuntimeException(); + } + } + }) + .retry(1, Functions.alwaysTrue()) + .test() + .assertFailure(RuntimeException.class); + + assertEquals(2, numberOfSubscribeCalls.get()); + } + + @Test + public void retryTimesPredicateWithZeroRetries() { + final AtomicInteger atomicInteger = new AtomicInteger(2); + final AtomicInteger numberOfSubscribeCalls = new AtomicInteger(0); + + Completable.fromAction(new Action() { + @Override public void run() throws Exception { + numberOfSubscribeCalls.incrementAndGet(); + + if (atomicInteger.decrementAndGet() != 0) { + throw new RuntimeException(); + } + } + }) + .retry(0, Functions.alwaysTrue()) + .test() + .assertFailure(RuntimeException.class); + + assertEquals(1, numberOfSubscribeCalls.get()); + } + + @Test + public void untilTrueEmpty() { + Completable.complete() + .retryUntil(() -> true) + .test() + .assertResult(); + } + + @Test + public void untilFalseEmpty() { + Completable.complete() + .retryUntil(() -> false) + .test() + .assertResult(); + } + + @Test + public void untilTrueError() { + Completable.error(new TestException()) + .retryUntil(() -> true) + .test() + .assertFailure(TestException.class); + } + + @Test + public void untilFalseError() { + AtomicInteger counter = new AtomicInteger(); + Completable.defer(() -> { + if (counter.getAndIncrement() == 0) { + return Completable.error(new TestException()); + } + return Completable.complete(); + }) + .retryUntil(() -> false) + .test() + .assertResult(); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/completable/CompletableTest.java b/src/test/java/io/reactivex/rxjava3/completable/CompletableTest.java new file mode 100644 index 0000000000..9f64b81d12 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/completable/CompletableTest.java @@ -0,0 +1,4061 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.completable; + +import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +import java.util.*; +import java.util.concurrent.*; +import java.util.concurrent.atomic.*; + +import org.junit.*; +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.disposables.*; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.disposables.*; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.internal.subscriptions.BooleanSubscription; +import io.reactivex.rxjava3.internal.util.ExceptionHelper; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.processors.PublishProcessor; +import io.reactivex.rxjava3.schedulers.*; +import io.reactivex.rxjava3.subjects.PublishSubject; +import io.reactivex.rxjava3.subscribers.TestSubscriber; +import io.reactivex.rxjava3.testsupport.*; + +/** + * Test Completable methods and operators. + */ +public class CompletableTest extends RxJavaTest { + /** + * Iterable that returns an Iterator that throws in its hasNext method. + */ + static final class IterableIteratorNextThrows implements Iterable<Completable> { + @Override + public Iterator<Completable> iterator() { + return new Iterator<Completable>() { + @Override + public boolean hasNext() { + return true; + } + + @Override + public Completable next() { + throw new TestException(); + } + + @Override + public void remove() { + } + }; + } + } + + /** + * Iterable that returns an Iterator that throws in its next method. + */ + static final class IterableIteratorHasNextThrows implements Iterable<Completable> { + @Override + public Iterator<Completable> iterator() { + return new Iterator<Completable>() { + @Override + public boolean hasNext() { + throw new TestException(); + } + + @Override + public Completable next() { + return null; + } + + @Override + public void remove() { + } + }; + } + } + + /** + * A class containing a completable instance and counts the number of subscribers. + */ + static final class NormalCompletable extends AtomicInteger { + + private static final long serialVersionUID = 7192337844700923752L; + + public final Completable completable = Completable.unsafeCreate(new CompletableSource() { + @Override + public void subscribe(CompletableObserver observer) { + getAndIncrement(); + EmptyDisposable.complete(observer); + } + }); + + /** + * Asserts the given number of subscriptions happened. + * @param n the expected number of subscriptions + */ + public void assertSubscriptions(int n) { + Assert.assertEquals(n, get()); + } + } + + /** + * A class containing a completable instance that emits a TestException and counts + * the number of subscribers. + */ + static final class ErrorCompletable extends AtomicInteger { + + private static final long serialVersionUID = 7192337844700923752L; + + public final Completable completable = Completable.unsafeCreate(new CompletableSource() { + @Override + public void subscribe(CompletableObserver observer) { + getAndIncrement(); + EmptyDisposable.error(new TestException(), observer); + } + }); + + /** + * Asserts the given number of subscriptions happened. + * @param n the expected number of subscriptions + */ + public void assertSubscriptions(int n) { + Assert.assertEquals(n, get()); + } + } + + /** A normal Completable object. */ + final NormalCompletable normal = new NormalCompletable(); + + /** An error Completable object. */ + final ErrorCompletable error = new ErrorCompletable(); + + @Test + public void complete() { + Completable c = Completable.complete(); + + c.blockingAwait(); + } + + @Test + public void concatEmpty() { + Completable c = Completable.concatArray(); + + c.blockingAwait(); + } + + @Test + public void concatSingleSource() { + Completable c = Completable.concatArray(normal.completable); + + c.blockingAwait(); + + normal.assertSubscriptions(1); + } + + @Test(expected = TestException.class) + public void concatSingleSourceThrows() { + Completable c = Completable.concatArray(error.completable); + + c.blockingAwait(); + } + + @Test + public void concatMultipleSources() { + Completable c = Completable.concatArray(normal.completable, normal.completable, normal.completable); + + c.blockingAwait(); + + normal.assertSubscriptions(3); + } + + @Test(expected = TestException.class) + public void concatMultipleOneThrows() { + Completable c = Completable.concatArray(normal.completable, error.completable, normal.completable); + + c.blockingAwait(); + } + + @Test(expected = NullPointerException.class) + public void concatMultipleOneIsNull() { + Completable c = Completable.concatArray(normal.completable, null); + + c.blockingAwait(); + } + + @Test + public void concatIterableEmpty() { + Completable c = Completable.concat(Collections.<Completable>emptyList()); + + c.blockingAwait(); + } + + @Test(expected = NullPointerException.class) + public void concatIterableIteratorNull() { + Completable c = Completable.concat(new Iterable<Completable>() { + @Override + public Iterator<Completable> iterator() { + return null; + } + }); + + c.blockingAwait(); + } + + @Test + public void concatIterableSingle() { + Completable c = Completable.concat(Collections.singleton(normal.completable)); + + c.blockingAwait(); + + normal.assertSubscriptions(1); + } + + @Test + public void concatIterableMany() { + Completable c = Completable.concat(Arrays.asList(normal.completable, normal.completable, normal.completable)); + + c.blockingAwait(); + + normal.assertSubscriptions(3); + } + + @Test(expected = TestException.class) + public void concatIterableOneThrows() { + Completable c = Completable.concat(Collections.singleton(error.completable)); + + c.blockingAwait(); + } + + @Test(expected = TestException.class) + public void concatIterableManyOneThrows() { + Completable c = Completable.concat(Arrays.asList(normal.completable, error.completable)); + + c.blockingAwait(); + } + + @Test(expected = TestException.class) + public void concatIterableIterableThrows() { + Completable c = Completable.concat(new Iterable<Completable>() { + @Override + public Iterator<Completable> iterator() { + throw new TestException(); + } + }); + + c.blockingAwait(); + } + + @Test(expected = TestException.class) + public void concatIterableIteratorHasNextThrows() { + Completable c = Completable.concat(new IterableIteratorHasNextThrows()); + + c.blockingAwait(); + } + + @Test(expected = TestException.class) + public void concatIterableIteratorNextThrows() { + Completable c = Completable.concat(new IterableIteratorNextThrows()); + + c.blockingAwait(); + } + + @Test + public void concatObservableEmpty() { + Completable c = Completable.concat(Flowable.<Completable>empty()); + + c.blockingAwait(); + } + + @Test(expected = TestException.class) + public void concatObservableError() { + Completable c = Completable.concat(Flowable.<Completable>error(new Supplier<Throwable>() { + @Override + public Throwable get() { + return new TestException(); + } + })); + + c.blockingAwait(); + } + + @Test + public void concatObservableSingle() { + Completable c = Completable.concat(Flowable.just(normal.completable)); + + c.blockingAwait(); + + normal.assertSubscriptions(1); + } + + @Test(expected = TestException.class) + public void concatObservableSingleThrows() { + Completable c = Completable.concat(Flowable.just(error.completable)); + + c.blockingAwait(); + } + + @Test + public void concatObservableMany() { + Completable c = Completable.concat(Flowable.just(normal.completable).repeat(3)); + + c.blockingAwait(); + + normal.assertSubscriptions(3); + } + + @Test(expected = TestException.class) + public void concatObservableManyOneThrows() { + Completable c = Completable.concat(Flowable.just(normal.completable, error.completable)); + + c.blockingAwait(); + } + + @Test + public void concatObservablePrefetch() { + final List<Long> requested = new ArrayList<>(); + Flowable<Completable> cs = Flowable + .just(normal.completable) + .repeat(10) + .doOnRequest(new LongConsumer() { + @Override + public void accept(long v) { + requested.add(v); + } + }); + + Completable c = Completable.concat(cs, 5); + + c.blockingAwait(); + + Assert.assertEquals(Arrays.asList(5L, 4L, 4L), requested); + } + + @Test(expected = NullPointerException.class) + public void createOnSubscribeThrowsNPE() { + Completable c = Completable.unsafeCreate(new CompletableSource() { + @Override + public void subscribe(CompletableObserver observer) { throw new NullPointerException(); } + }); + + c.blockingAwait(); + } + + @Test + public void createOnSubscribeThrowsRuntimeException() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + Completable c = Completable.unsafeCreate(new CompletableSource() { + @Override + public void subscribe(CompletableObserver observer) { + throw new TestException(); + } + }); + + c.blockingAwait(); + + Assert.fail("Did not throw exception"); + } catch (NullPointerException ex) { + if (!(ex.getCause() instanceof TestException)) { + ex.printStackTrace(); + Assert.fail("Did not wrap the TestException but it returned: " + ex); + } + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void defer() { + Completable c = Completable.defer(new Supplier<Completable>() { + @Override + public Completable get() { + return normal.completable; + } + }); + + normal.assertSubscriptions(0); + + c.blockingAwait(); + + normal.assertSubscriptions(1); + } + + @Test(expected = NullPointerException.class) + public void deferReturnsNull() { + Completable c = Completable.defer(new Supplier<Completable>() { + @Override + public Completable get() { + return null; + } + }); + + c.blockingAwait(); + } + + @Test(expected = TestException.class) + public void deferFunctionThrows() { + Completable c = Completable.defer(new Supplier<Completable>() { + @Override + public Completable get() { throw new TestException(); } + }); + + c.blockingAwait(); + } + + @Test(expected = TestException.class) + public void deferErrorSource() { + Completable c = Completable.defer(new Supplier<Completable>() { + @Override + public Completable get() { + return error.completable; + } + }); + + c.blockingAwait(); + } + + @Test(expected = TestException.class) + public void errorSupplierNormal() { + Completable c = Completable.error(new Supplier<Throwable>() { + @Override + public Throwable get() { + return new TestException(); + } + }); + + c.blockingAwait(); + } + + @Test(expected = NullPointerException.class) + public void errorSupplierReturnsNull() { + Completable c = Completable.error(new Supplier<Throwable>() { + @Override + public Throwable get() { + return null; + } + }); + + c.blockingAwait(); + } + + @Test(expected = TestException.class) + public void errorSupplierThrows() { + Completable c = Completable.error(new Supplier<Throwable>() { + @Override + public Throwable get() { throw new TestException(); } + }); + + c.blockingAwait(); + } + + @Test(expected = TestException.class) + public void errorNormal() { + Completable c = Completable.error(new TestException()); + + c.blockingAwait(); + } + + @Test + public void fromCallableNormal() { + final AtomicInteger calls = new AtomicInteger(); + + Completable c = Completable.fromCallable(new Callable<Object>() { + @Override + public Object call() throws Exception { + return calls.getAndIncrement(); + } + }); + + c.blockingAwait(); + + Assert.assertEquals(1, calls.get()); + } + + @Test(expected = TestException.class) + public void fromCallableThrows() { + Completable c = Completable.fromCallable(new Callable<Object>() { + @Override + public Object call() throws Exception { throw new TestException(); } + }); + + c.blockingAwait(); + } + + @Test + public void fromFlowableEmpty() { + Completable c = Completable.fromPublisher(Flowable.empty()); + + c.blockingAwait(); + } + + @Test + public void fromFlowableSome() { + for (int n = 1; n < 10000; n *= 10) { + Completable c = Completable.fromPublisher(Flowable.range(1, n)); + + c.blockingAwait(); + } + } + + @Test(expected = TestException.class) + public void fromFlowableError() { + Completable c = Completable.fromPublisher(Flowable.error(new Supplier<Throwable>() { + @Override + public Throwable get() { + return new TestException(); + } + })); + + c.blockingAwait(); + } + + @Test + public void fromObservableEmpty() { + Completable c = Completable.fromObservable(Observable.empty()); + + c.blockingAwait(); + } + + @Test + public void fromObservableSome() { + for (int n = 1; n < 10000; n *= 10) { + Completable c = Completable.fromObservable(Observable.range(1, n)); + + c.blockingAwait(); + } + } + + @Test(expected = TestException.class) + public void fromObservableError() { + Completable c = Completable.fromObservable(Observable.error(new Supplier<Throwable>() { + @Override + public Throwable get() { + return new TestException(); + } + })); + + c.blockingAwait(); + } + + @Test + public void fromActionNormal() { + final AtomicInteger calls = new AtomicInteger(); + + Completable c = Completable.fromAction(new Action() { + @Override + public void run() { + calls.getAndIncrement(); + } + }); + + c.blockingAwait(); + + Assert.assertEquals(1, calls.get()); + } + + @Test(expected = TestException.class) + public void fromActionThrows() { + Completable c = Completable.fromAction(new Action() { + @Override + public void run() { throw new TestException(); } + }); + + c.blockingAwait(); + } + + @Test + public void fromSingleNormal() { + Completable c = Completable.fromSingle(Single.just(1)); + + c.blockingAwait(); + } + + @Test(expected = TestException.class) + public void fromSingleThrows() { + Completable c = Completable.fromSingle(Single.error(new Supplier<Throwable>() { + @Override + public Throwable get() { + return new TestException(); + } + })); + + c.blockingAwait(); + } + + @Test + public void mergeEmpty() { + Completable c = Completable.mergeArray(); + + c.blockingAwait(); + } + + @Test + public void mergeSingleSource() { + Completable c = Completable.mergeArray(normal.completable); + + c.blockingAwait(); + + normal.assertSubscriptions(1); + } + + @Test(expected = TestException.class) + public void mergeSingleSourceThrows() { + Completable c = Completable.mergeArray(error.completable); + + c.blockingAwait(); + } + + @Test + public void mergeMultipleSources() { + Completable c = Completable.mergeArray(normal.completable, normal.completable, normal.completable); + + c.blockingAwait(); + + normal.assertSubscriptions(3); + } + + @Test(expected = TestException.class) + public void mergeMultipleOneThrows() { + Completable c = Completable.mergeArray(normal.completable, error.completable, normal.completable); + + c.blockingAwait(); + } + + @Test(expected = NullPointerException.class) + public void mergeMultipleOneIsNull() { + Completable c = Completable.mergeArray(normal.completable, null); + + c.blockingAwait(); + } + + @Test + public void mergeIterableEmpty() { + Completable c = Completable.merge(Collections.<Completable>emptyList()); + + c.blockingAwait(); + } + + @Test(expected = NullPointerException.class) + public void mergeIterableIteratorNull() { + Completable c = Completable.merge(new Iterable<Completable>() { + @Override + public Iterator<Completable> iterator() { + return null; + } + }); + + c.blockingAwait(); + } + + @Test + public void mergeIterableSingle() { + Completable c = Completable.merge(Collections.singleton(normal.completable)); + + c.blockingAwait(); + + normal.assertSubscriptions(1); + } + + @Test + public void mergeIterableMany() { + Completable c = Completable.merge(Arrays.asList(normal.completable, normal.completable, normal.completable)); + + c.blockingAwait(); + + normal.assertSubscriptions(3); + } + + @Test(expected = TestException.class) + public void mergeIterableOneThrows() { + Completable c = Completable.merge(Collections.singleton(error.completable)); + + c.blockingAwait(); + } + + @Test(expected = TestException.class) + public void mergeIterableManyOneThrows() { + Completable c = Completable.merge(Arrays.asList(normal.completable, error.completable)); + + c.blockingAwait(); + } + + @Test(expected = TestException.class) + public void mergeIterableIterableThrows() { + Completable c = Completable.merge(new Iterable<Completable>() { + @Override + public Iterator<Completable> iterator() { + throw new TestException(); + } + }); + + c.blockingAwait(); + } + + @Test(expected = TestException.class) + public void mergeIterableIteratorHasNextThrows() { + Completable c = Completable.merge(new IterableIteratorHasNextThrows()); + + c.blockingAwait(); + } + + @Test(expected = TestException.class) + public void mergeIterableIteratorNextThrows() { + Completable c = Completable.merge(new IterableIteratorNextThrows()); + + c.blockingAwait(); + } + + @Test + public void mergeObservableEmpty() { + Completable c = Completable.merge(Flowable.<Completable>empty()); + + c.blockingAwait(); + } + + @Test(expected = TestException.class) + public void mergeObservableError() { + Completable c = Completable.merge(Flowable.<Completable>error(new Supplier<Throwable>() { + @Override + public Throwable get() { + return new TestException(); + } + })); + + c.blockingAwait(); + } + + @Test + public void mergeObservableSingle() { + Completable c = Completable.merge(Flowable.just(normal.completable)); + + c.blockingAwait(); + + normal.assertSubscriptions(1); + } + + @Test(expected = TestException.class) + public void mergeObservableSingleThrows() { + Completable c = Completable.merge(Flowable.just(error.completable)); + + c.blockingAwait(); + } + + @Test + public void mergeObservableMany() { + Completable c = Completable.merge(Flowable.just(normal.completable).repeat(3)); + + c.blockingAwait(); + + normal.assertSubscriptions(3); + } + + @Test(expected = TestException.class) + public void mergeObservableManyOneThrows() { + Completable c = Completable.merge(Flowable.just(normal.completable, error.completable)); + + c.blockingAwait(); + } + + @Test + public void mergeObservableMaxConcurrent() { + final List<Long> requested = new ArrayList<>(); + Flowable<Completable> cs = Flowable + .just(normal.completable) + .repeat(10) + .doOnRequest(new LongConsumer() { + @Override + public void accept(long v) { + requested.add(v); + } + }); + + Completable c = Completable.merge(cs, 5); + + c.blockingAwait(); + + // FIXME this request pattern looks odd because all 10 completions trigger 1 requests + Assert.assertEquals(Arrays.asList(5L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L), requested); + } + + @Test + public void mergeDelayErrorEmpty() { + Completable c = Completable.mergeArrayDelayError(); + + c.blockingAwait(); + } + + @Test + public void mergeDelayErrorSingleSource() { + Completable c = Completable.mergeArrayDelayError(normal.completable); + + c.blockingAwait(); + + normal.assertSubscriptions(1); + } + + @Test(expected = TestException.class) + public void mergeDelayErrorSingleSourceThrows() { + Completable c = Completable.mergeArrayDelayError(error.completable); + + c.blockingAwait(); + } + + @Test + public void mergeDelayErrorMultipleSources() { + Completable c = Completable.mergeArrayDelayError(normal.completable, normal.completable, normal.completable); + + c.blockingAwait(); + + normal.assertSubscriptions(3); + } + + @Test + public void mergeDelayErrorMultipleOneThrows() { + Completable c = Completable.mergeArrayDelayError(normal.completable, error.completable, normal.completable); + + try { + c.blockingAwait(); + } catch (TestException ex) { + normal.assertSubscriptions(2); + } + } + + @Test(expected = NullPointerException.class) + public void mergeDelayErrorMultipleOneIsNull() { + Completable c = Completable.mergeArrayDelayError(normal.completable, null); + + c.blockingAwait(); + } + + @Test + public void mergeDelayErrorIterableEmpty() { + Completable c = Completable.mergeDelayError(Collections.<Completable>emptyList()); + + c.blockingAwait(); + } + + @Test(expected = NullPointerException.class) + public void mergeDelayErrorIterableIteratorNull() { + Completable c = Completable.mergeDelayError(new Iterable<Completable>() { + @Override + public Iterator<Completable> iterator() { + return null; + } + }); + + c.blockingAwait(); + } + + @Test + public void mergeDelayErrorIterableSingle() { + Completable c = Completable.mergeDelayError(Collections.singleton(normal.completable)); + + c.blockingAwait(); + + normal.assertSubscriptions(1); + } + + @Test + public void mergeDelayErrorIterableMany() { + Completable c = Completable.mergeDelayError(Arrays.asList(normal.completable, normal.completable, normal.completable)); + + c.blockingAwait(); + + normal.assertSubscriptions(3); + } + + @Test(expected = TestException.class) + public void mergeDelayErrorIterableOneThrows() { + Completable c = Completable.mergeDelayError(Collections.singleton(error.completable)); + + c.blockingAwait(); + } + + @Test + public void mergeDelayErrorIterableManyOneThrows() { + Completable c = Completable.mergeDelayError(Arrays.asList(normal.completable, error.completable, normal.completable)); + + try { + c.blockingAwait(); + } catch (TestException ex) { + normal.assertSubscriptions(2); + } + } + + @Test(expected = TestException.class) + public void mergeDelayErrorIterableIterableThrows() { + Completable c = Completable.mergeDelayError(new Iterable<Completable>() { + @Override + public Iterator<Completable> iterator() { + throw new TestException(); + } + }); + + c.blockingAwait(); + } + + @Test(expected = TestException.class) + public void mergeDelayErrorIterableIteratorHasNextThrows() { + Completable c = Completable.mergeDelayError(new IterableIteratorHasNextThrows()); + + c.blockingAwait(); + } + + @Test(expected = TestException.class) + public void mergeDelayErrorIterableIteratorNextThrows() { + Completable c = Completable.mergeDelayError(new IterableIteratorNextThrows()); + + c.blockingAwait(); + } + + @Test + public void mergeDelayErrorObservableEmpty() { + Completable c = Completable.mergeDelayError(Flowable.<Completable>empty()); + + c.blockingAwait(); + } + + @Test(expected = TestException.class) + public void mergeDelayErrorObservableError() { + Completable c = Completable.mergeDelayError(Flowable.<Completable>error(new Supplier<Throwable>() { + @Override + public Throwable get() { + return new TestException(); + } + })); + + c.blockingAwait(); + } + + @Test + public void mergeDelayErrorObservableSingle() { + Completable c = Completable.mergeDelayError(Flowable.just(normal.completable)); + + c.blockingAwait(); + + normal.assertSubscriptions(1); + } + + @Test(expected = TestException.class) + public void mergeDelayErrorObservableSingleThrows() { + Completable c = Completable.mergeDelayError(Flowable.just(error.completable)); + + c.blockingAwait(); + } + + @Test + public void mergeDelayErrorObservableMany() { + Completable c = Completable.mergeDelayError(Flowable.just(normal.completable).repeat(3)); + + c.blockingAwait(); + + normal.assertSubscriptions(3); + } + + @Test(expected = TestException.class) + public void mergeDelayErrorObservableManyOneThrows() { + Completable c = Completable.mergeDelayError(Flowable.just(normal.completable, error.completable)); + + c.blockingAwait(); + } + + @Test + public void mergeDelayErrorObservableMaxConcurrent() { + final List<Long> requested = new ArrayList<>(); + Flowable<Completable> cs = Flowable + .just(normal.completable) + .repeat(10) + .doOnRequest(new LongConsumer() { + @Override + public void accept(long v) { + requested.add(v); + } + }); + + Completable c = Completable.mergeDelayError(cs, 5); + + c.blockingAwait(); + + // FIXME this request pattern looks odd because all 10 completions trigger 1 requests + Assert.assertEquals(Arrays.asList(5L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L), requested); + } + + @Test + public void never() { + final AtomicBoolean onSubscribeCalled = new AtomicBoolean(); + final AtomicInteger calls = new AtomicInteger(); + Completable.never().subscribe(new CompletableObserver() { + @Override + public void onSubscribe(Disposable d) { + onSubscribeCalled.set(true); + } + + @Override + public void onError(Throwable e) { + calls.getAndIncrement(); + } + + @Override + public void onComplete() { + calls.getAndIncrement(); + } + }); + + Assert.assertTrue("onSubscribe not called", onSubscribeCalled.get()); + Assert.assertEquals("There were calls to onXXX methods", 0, calls.get()); + } + + @Test + public void timer() { + Completable c = Completable.timer(500, TimeUnit.MILLISECONDS); + + c.blockingAwait(); + } + + @Test + public void timerNewThread() { + Completable c = Completable.timer(500, TimeUnit.MILLISECONDS, Schedulers.newThread()); + + c.blockingAwait(); + } + + @Test + public void timerTestScheduler() { + TestScheduler scheduler = new TestScheduler(); + + Completable c = Completable.timer(250, TimeUnit.MILLISECONDS, scheduler); + + final AtomicInteger calls = new AtomicInteger(); + + c.subscribe(new CompletableObserver() { + @Override + public void onSubscribe(Disposable d) { + + } + + @Override + public void onComplete() { + calls.getAndIncrement(); + } + + @Override + public void onError(Throwable e) { + RxJavaPlugins.onError(e); + } + }); + + scheduler.advanceTimeBy(100, TimeUnit.MILLISECONDS); + + Assert.assertEquals(0, calls.get()); + + scheduler.advanceTimeBy(200, TimeUnit.MILLISECONDS); + + Assert.assertEquals(1, calls.get()); + } + + @Test + public void timerCancel() throws InterruptedException { + Completable c = Completable.timer(250, TimeUnit.MILLISECONDS); + + final SequentialDisposable sd = new SequentialDisposable(); + final AtomicInteger calls = new AtomicInteger(); + + c.subscribe(new CompletableObserver() { + @Override + public void onSubscribe(Disposable d) { + sd.replace(d); + } + + @Override + public void onError(Throwable e) { + calls.getAndIncrement(); + } + + @Override + public void onComplete() { + calls.getAndIncrement(); + } + }); + + Thread.sleep(100); + + sd.dispose(); + + Thread.sleep(200); + + Assert.assertEquals(0, calls.get()); + } + + @Test + public void usingNormalEager() { + final AtomicInteger dispose = new AtomicInteger(); + + Completable c = Completable.using(new Supplier<Integer>() { + @Override + public Integer get() { + return 1; + } + }, new Function<Object, Completable>() { + @Override + public Completable apply(Object v) { + return normal.completable; + } + }, new Consumer<Integer>() { + @Override + public void accept(Integer d) { + dispose.set(d); + } + }); + + final AtomicBoolean disposedFirst = new AtomicBoolean(); + final AtomicReference<Throwable> error = new AtomicReference<>(); + + c.subscribe(new CompletableObserver() { + @Override + public void onSubscribe(Disposable d) { + + } + + @Override + public void onError(Throwable e) { + error.lazySet(e); + } + + @Override + public void onComplete() { + disposedFirst.set(dispose.get() != 0); + } + }); + + Assert.assertEquals(1, dispose.get()); + Assert.assertTrue("Not disposed first", disposedFirst.get()); + Assert.assertNull(error.get()); + } + + @Test + public void usingNormalLazy() { + final AtomicInteger dispose = new AtomicInteger(); + + Completable c = Completable.using(new Supplier<Integer>() { + @Override + public Integer get() { + return 1; + } + }, new Function<Integer, Completable>() { + @Override + public Completable apply(Integer v) { + return normal.completable; + } + }, new Consumer<Integer>() { + @Override + public void accept(Integer d) { + dispose.set(d); + } + }, false); + + final AtomicBoolean disposedFirst = new AtomicBoolean(); + final AtomicReference<Throwable> error = new AtomicReference<>(); + + c.subscribe(new CompletableObserver() { + @Override + public void onSubscribe(Disposable d) { + + } + + @Override + public void onError(Throwable e) { + error.lazySet(e); + } + + @Override + public void onComplete() { + disposedFirst.set(dispose.get() != 0); + } + }); + + Assert.assertEquals(1, dispose.get()); + Assert.assertFalse("Disposed first", disposedFirst.get()); + Assert.assertNull(error.get()); + } + + @Test + public void usingErrorEager() { + final AtomicInteger dispose = new AtomicInteger(); + + Completable c = Completable.using(new Supplier<Integer>() { + @Override + public Integer get() { + return 1; + } + }, new Function<Integer, Completable>() { + @Override + public Completable apply(Integer v) { + return error.completable; + } + }, new Consumer<Integer>() { + @Override + public void accept(Integer d) { + dispose.set(d); + } + }); + + final AtomicBoolean disposedFirst = new AtomicBoolean(); + final AtomicBoolean complete = new AtomicBoolean(); + + c.subscribe(new CompletableObserver() { + @Override + public void onSubscribe(Disposable d) { + + } + + @Override + public void onError(Throwable e) { + disposedFirst.set(dispose.get() != 0); + } + + @Override + public void onComplete() { + complete.set(true); + } + }); + + Assert.assertEquals(1, dispose.get()); + Assert.assertTrue("Not disposed first", disposedFirst.get()); + Assert.assertFalse(complete.get()); + } + + @Test + public void usingErrorLazy() { + final AtomicInteger dispose = new AtomicInteger(); + + Completable c = Completable.using(new Supplier<Integer>() { + @Override + public Integer get() { + return 1; + } + }, new Function<Integer, Completable>() { + @Override + public Completable apply(Integer v) { + return error.completable; + } + }, new Consumer<Integer>() { + @Override + public void accept(Integer d) { + dispose.set(d); + } + }, false); + + final AtomicBoolean disposedFirst = new AtomicBoolean(); + final AtomicBoolean complete = new AtomicBoolean(); + + c.subscribe(new CompletableObserver() { + @Override + public void onSubscribe(Disposable d) { + + } + + @Override + public void onError(Throwable e) { + disposedFirst.set(dispose.get() != 0); + } + + @Override + public void onComplete() { + complete.set(true); + } + }); + + Assert.assertEquals(1, dispose.get()); + Assert.assertFalse("Disposed first", disposedFirst.get()); + Assert.assertFalse(complete.get()); + } + + @Test(expected = NullPointerException.class) + public void usingMapperReturnsNull() { + Completable c = Completable.using(new Supplier<Object>() { + @Override + public Object get() { + return 1; + } + }, new Function<Object, Completable>() { + @Override + public Completable apply(Object v) { + return null; + } + }, new Consumer<Object>() { + @Override + public void accept(Object v) { } + }); + + c.blockingAwait(); + } + + @Test(expected = TestException.class) + public void usingResourceThrows() { + Completable c = Completable.using(new Supplier<Object>() { + @Override + public Object get() { throw new TestException(); } + }, + new Function<Object, Completable>() { + @Override + public Completable apply(Object v) { + return normal.completable; + } + }, new Consumer<Object>() { + @Override + public void accept(Object v) { } + }); + + c.blockingAwait(); + } + + @Test(expected = TestException.class) + public void usingMapperThrows() { + Completable c = Completable.using(new Supplier<Object>() { + @Override + public Object get() { + return 1; + } + }, + new Function<Object, Completable>() { + @Override + public Completable apply(Object v) { throw new TestException(); } + }, new Consumer<Object>() { + @Override + public void accept(Object v) { } + }); + + c.blockingAwait(); + } + + @Test(expected = TestException.class) + public void usingDisposerThrows() { + Completable c = Completable.using(new Supplier<Object>() { + @Override + public Object get() { + return 1; + } + }, + new Function<Object, Completable>() { + @Override + public Completable apply(Object v) { + return normal.completable; + } + }, new Consumer<Object>() { + @Override + public void accept(Object v) { throw new TestException(); } + }); + + c.blockingAwait(); + } + + @Test + public void composeNormal() { + Completable c = error.completable.compose(new CompletableTransformer() { + @Override + public Completable apply(Completable n) { + return n.onErrorComplete(); + } + }); + + c.blockingAwait(); + } + + @Test + public void concatWithNormal() { + Completable c = normal.completable.concatWith(normal.completable); + + c.blockingAwait(); + + normal.assertSubscriptions(2); + } + + @Test(expected = TestException.class) + public void concatWithError() { + Completable c = normal.completable.concatWith(error.completable); + + c.blockingAwait(); + } + + @Test + public void delayNormal() throws InterruptedException { + Completable c = normal.completable.delay(250, TimeUnit.MILLISECONDS); + + final AtomicBoolean done = new AtomicBoolean(); + final AtomicReference<Throwable> error = new AtomicReference<>(); + + c.subscribe(new CompletableObserver() { + @Override + public void onSubscribe(Disposable d) { + + } + + @Override + public void onError(Throwable e) { + error.set(e); + } + + @Override + public void onComplete() { + done.set(true); + } + }); + + Thread.sleep(100); + + Assert.assertFalse("Already done", done.get()); + + int timeout = 10; + + while (timeout-- > 0 && !done.get()) { + Thread.sleep(100); + } + + Assert.assertTrue("Not done", done.get()); + + Assert.assertNull(error.get()); + } + + @Test + public void delayErrorImmediately() throws InterruptedException { + final TestScheduler scheduler = new TestScheduler(); + final Completable c = error.completable.delay(250, TimeUnit.MILLISECONDS, scheduler); + + final AtomicBoolean done = new AtomicBoolean(); + final AtomicReference<Throwable> error = new AtomicReference<>(); + + c.subscribe(new CompletableObserver() { + @Override + public void onSubscribe(Disposable d) { + + } + + @Override + public void onError(Throwable e) { + error.set(e); + } + + @Override + public void onComplete() { + done.set(true); + } + }); + + scheduler.advanceTimeBy(100, TimeUnit.MILLISECONDS); + + Assert.assertTrue(error.get().toString(), error.get() instanceof TestException); + Assert.assertFalse("Already done", done.get()); + + scheduler.advanceTimeBy(100, TimeUnit.MILLISECONDS); + + Assert.assertFalse("Already done", done.get()); + } + + @Test + public void delayErrorToo() throws InterruptedException { + Completable c = error.completable.delay(250, TimeUnit.MILLISECONDS, Schedulers.computation(), true); + + final AtomicBoolean done = new AtomicBoolean(); + final AtomicReference<Throwable> error = new AtomicReference<>(); + + c.subscribe(new CompletableObserver() { + @Override + public void onSubscribe(Disposable d) { + + } + + @Override + public void onError(Throwable e) { + error.set(e); + } + + @Override + public void onComplete() { + done.set(true); + } + }); + + Thread.sleep(100); + + Assert.assertFalse("Already done", done.get()); + Assert.assertNull(error.get()); + + Thread.sleep(200); + + Assert.assertFalse("Already done", done.get()); + Assert.assertTrue(error.get() instanceof TestException); + } + + @Test + public void doOnCompleteNormal() { + final AtomicInteger calls = new AtomicInteger(); + + Completable c = normal.completable.doOnComplete(new Action() { + @Override + public void run() { + calls.getAndIncrement(); + } + }); + + c.blockingAwait(); + + Assert.assertEquals(1, calls.get()); + } + + @Test + public void doOnCompleteError() { + final AtomicInteger calls = new AtomicInteger(); + + Completable c = error.completable.doOnComplete(new Action() { + @Override + public void run() { + calls.getAndIncrement(); + } + }); + + try { + c.blockingAwait(); + Assert.fail("Failed to throw TestException"); + } catch (TestException ex) { + // expected + } + + Assert.assertEquals(0, calls.get()); + } + + @Test(expected = TestException.class) + public void doOnCompleteThrows() { + Completable c = normal.completable.doOnComplete(new Action() { + @Override + public void run() { throw new TestException(); } + }); + + c.blockingAwait(); + } + + @Test + public void doOnDisposeNormalDoesntCall() { + final AtomicInteger calls = new AtomicInteger(); + + Completable c = normal.completable.doOnDispose(new Action() { + @Override + public void run() { + calls.getAndIncrement(); + } + }); + + c.blockingAwait(); + + Assert.assertEquals(0, calls.get()); + } + + @Test + public void doOnDisposeErrorDoesntCall() { + final AtomicInteger calls = new AtomicInteger(); + + Completable c = error.completable.doOnDispose(new Action() { + @Override + public void run() { + calls.getAndIncrement(); + } + }); + + try { + c.blockingAwait(); + Assert.fail("No exception thrown"); + } catch (TestException ex) { + // expected + } + Assert.assertEquals(0, calls.get()); + } + + @Test + public void doOnDisposeChildCancels() { + final AtomicInteger calls = new AtomicInteger(); + + Completable c = normal.completable.doOnDispose(new Action() { + @Override + public void run() { + calls.getAndIncrement(); + } + }); + + c.subscribe(new CompletableObserver() { + @Override + public void onSubscribe(Disposable d) { + d.dispose(); + } + + @Override + public void onError(Throwable e) { + // ignored + } + + @Override + public void onComplete() { + // ignored + } + }); + + Assert.assertEquals(1, calls.get()); + } + + @Test + public void doOnDisposeThrows() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + Completable c = normal.completable.doOnDispose(new Action() { + @Override + public void run() { throw new TestException(); } + }); + + c.subscribe(new CompletableObserver() { + @Override + public void onSubscribe(Disposable d) { + d.dispose(); + } + + @Override + public void onError(Throwable e) { + // ignored + } + + @Override + public void onComplete() { + // ignored + } + }); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void doOnErrorNoError() { + final AtomicReference<Throwable> error = new AtomicReference<>(); + + Completable c = normal.completable.doOnError(new Consumer<Throwable>() { + @Override + public void accept(Throwable e) { + error.set(e); + } + }); + + c.blockingAwait(); + + Assert.assertNull(error.get()); + } + + @Test + public void doOnErrorHasError() { + final AtomicReference<Throwable> err = new AtomicReference<>(); + + Completable c = error.completable.doOnError(new Consumer<Throwable>() { + @Override + public void accept(Throwable e) { + err.set(e); + } + }); + + try { + c.blockingAwait(); + Assert.fail("Did not throw exception"); + } catch (Throwable e) { + // expected + } + + Assert.assertTrue(err.get() instanceof TestException); + } + + @Test + public void doOnErrorThrows() { + Completable c = error.completable.doOnError(new Consumer<Throwable>() { + @Override + public void accept(Throwable e) { throw new IllegalStateException(); } + }); + + try { + c.blockingAwait(); + } catch (CompositeException ex) { + List<Throwable> a = ex.getExceptions(); + Assert.assertEquals(2, a.size()); + Assert.assertTrue(a.get(0) instanceof TestException); + Assert.assertTrue(a.get(1) instanceof IllegalStateException); + } + } + + @Test + public void doOnSubscribeNormal() { + final AtomicInteger calls = new AtomicInteger(); + + Completable c = normal.completable.doOnSubscribe(new Consumer<Disposable>() { + @Override + public void accept(Disposable d) { + calls.getAndIncrement(); + } + }); + + for (int i = 0; i < 10; i++) { + c.blockingAwait(); + } + + Assert.assertEquals(10, calls.get()); + } + + @Test(expected = TestException.class) + public void doOnSubscribeThrows() { + Completable c = normal.completable.doOnSubscribe(new Consumer<Disposable>() { + @Override + public void accept(Disposable d) { throw new TestException(); } + }); + + c.blockingAwait(); + } + + @Test + public void doOnTerminateNormal() { + final AtomicInteger calls = new AtomicInteger(); + + Completable c = normal.completable.doOnTerminate(new Action() { + @Override + public void run() { + calls.getAndIncrement(); + } + }); + + c.blockingAwait(); + + Assert.assertEquals(1, calls.get()); + } + + @Test + public void doOnTerminateError() { + final AtomicInteger calls = new AtomicInteger(); + + Completable c = error.completable.doOnTerminate(new Action() { + @Override + public void run() { + calls.getAndIncrement(); + } + }); + + try { + c.blockingAwait(); + Assert.fail("Did dot throw exception"); + } catch (TestException ex) { + // expected + } + + Assert.assertEquals(1, calls.get()); + } + + @Test(expected = NullPointerException.class) + public void liftReturnsNull() { + Completable c = normal.completable.lift(new CompletableOperator() { + @Override + public CompletableObserver apply(CompletableObserver v) { + return null; + } + }); + + c.blockingAwait(); + } + + static final class CompletableOperatorSwap implements CompletableOperator { + @Override + public CompletableObserver apply(final CompletableObserver v) { + return new CompletableObserver() { + + @Override + public void onComplete() { + v.onError(new TestException()); + } + + @Override + public void onError(Throwable e) { + v.onComplete(); + } + + @Override + public void onSubscribe(Disposable d) { + v.onSubscribe(d); + } + + }; + } + } + + @Test(expected = TestException.class) + public void liftOnCompleteError() { + Completable c = normal.completable.lift(new CompletableOperatorSwap()); + + c.blockingAwait(); + } + + @Test + public void liftOnErrorComplete() { + Completable c = error.completable.lift(new CompletableOperatorSwap()); + + c.blockingAwait(); + } + + @Test + public void mergeWithNormal() { + Completable c = normal.completable.mergeWith(normal.completable); + + c.blockingAwait(); + + normal.assertSubscriptions(2); + } + + @Test + public void observeOnNormal() throws InterruptedException { + final AtomicReference<String> name = new AtomicReference<>(); + final AtomicReference<Throwable> err = new AtomicReference<>(); + final CountDownLatch cdl = new CountDownLatch(1); + + Completable c = normal.completable.observeOn(Schedulers.computation()); + + c.subscribe(new CompletableObserver() { + @Override + public void onSubscribe(Disposable d) { + + } + + @Override + public void onComplete() { + name.set(Thread.currentThread().getName()); + cdl.countDown(); + } + + @Override + public void onError(Throwable e) { + err.set(e); + cdl.countDown(); + } + }); + + cdl.await(); + + Assert.assertNull(err.get()); + Assert.assertTrue(name.get().startsWith("RxComputation")); + } + + @Test + public void observeOnError() throws InterruptedException { + final AtomicReference<String> name = new AtomicReference<>(); + final AtomicReference<Throwable> err = new AtomicReference<>(); + final CountDownLatch cdl = new CountDownLatch(1); + + Completable c = error.completable.observeOn(Schedulers.computation()); + + c.subscribe(new CompletableObserver() { + @Override + public void onSubscribe(Disposable d) { + + } + + @Override + public void onComplete() { + name.set(Thread.currentThread().getName()); + cdl.countDown(); + } + + @Override + public void onError(Throwable e) { + name.set(Thread.currentThread().getName()); + err.set(e); + cdl.countDown(); + } + }); + + cdl.await(); + + Assert.assertTrue(err.get() instanceof TestException); + Assert.assertTrue(name.get().startsWith("RxComputation")); + } + + @Test + public void onErrorComplete() { + Completable c = error.completable.onErrorComplete(); + + c.blockingAwait(); + } + + @Test(expected = TestException.class) + public void onErrorCompleteFalse() { + Completable c = error.completable.onErrorComplete(new Predicate<Throwable>() { + @Override + public boolean test(Throwable e) { + return e instanceof IllegalStateException; + } + }); + + c.blockingAwait(); + } + + @Test + public void onErrorResumeNextFunctionReturnsNull() { + Completable c = error.completable.onErrorResumeNext(new Function<Throwable, Completable>() { + @Override + public Completable apply(Throwable e) { + return null; + } + }); + + try { + c.blockingAwait(); + Assert.fail("Did not throw an exception"); + } catch (CompositeException ex) { + List<Throwable> errors = ex.getExceptions(); + TestHelper.assertError(errors, 0, TestException.class); + TestHelper.assertError(errors, 1, NullPointerException.class); + assertEquals(2, errors.size()); + } + } + + @Test + public void onErrorResumeNextFunctionThrows() { + Completable c = error.completable.onErrorResumeNext(new Function<Throwable, Completable>() { + @Override + public Completable apply(Throwable e) { throw new TestException(); } + }); + + try { + c.blockingAwait(); + Assert.fail("Did not throw an exception"); + } catch (CompositeException ex) { + List<Throwable> a = ex.getExceptions(); + + Assert.assertEquals(2, a.size()); + Assert.assertTrue(a.get(0) instanceof TestException); + Assert.assertTrue(a.get(1) instanceof TestException); + } + } + + @Test + public void onErrorResumeNextNormal() { + Completable c = error.completable.onErrorResumeNext(new Function<Throwable, Completable>() { + @Override + public Completable apply(Throwable v) { + return normal.completable; + } + }); + + c.blockingAwait(); + } + + @Test(expected = TestException.class) + public void onErrorResumeNextError() { + Completable c = error.completable.onErrorResumeNext(new Function<Throwable, Completable>() { + @Override + public Completable apply(Throwable v) { + return error.completable; + } + }); + + c.blockingAwait(); + } + + @Test + public void repeatNormal() { + final AtomicReference<Throwable> err = new AtomicReference<>(); + final AtomicInteger calls = new AtomicInteger(); + + Completable c = Completable.fromCallable(new Callable<Object>() { + @Override + public Object call() throws Exception { + calls.getAndIncrement(); + Thread.sleep(100); + return null; + } + }).repeat(); + + c.subscribe(new CompletableObserver() { + @Override + public void onSubscribe(final Disposable d) { + Schedulers.single().scheduleDirect(new Runnable() { + @Override + public void run() { + d.dispose(); + } + }, 550, TimeUnit.MILLISECONDS); + } + + @Override + public void onError(Throwable e) { + err.set(e); + } + + @Override + public void onComplete() { + + } + }); + + Assert.assertEquals(6, calls.get()); + Assert.assertNull(err.get()); + } + + @Test(expected = TestException.class) + public void repeatError() { + Completable c = error.completable.repeat(); + + c.blockingAwait(); + } + + @Test + public void repeat5Times() { + final AtomicInteger calls = new AtomicInteger(); + + Completable c = Completable.fromCallable(new Callable<Object>() { + @Override + public Object call() throws Exception { + calls.getAndIncrement(); + return null; + } + }).repeat(5); + + c.blockingAwait(); + + Assert.assertEquals(5, calls.get()); + } + + @Test + public void repeat1Time() { + final AtomicInteger calls = new AtomicInteger(); + + Completable c = Completable.fromCallable(new Callable<Object>() { + @Override + public Object call() throws Exception { + calls.getAndIncrement(); + return null; + } + }).repeat(1); + + c.blockingAwait(); + + Assert.assertEquals(1, calls.get()); + } + + @Test + public void repeat0Time() { + final AtomicInteger calls = new AtomicInteger(); + + Completable c = Completable.fromCallable(new Callable<Object>() { + @Override + public Object call() throws Exception { + calls.getAndIncrement(); + return null; + } + }).repeat(0); + + c.blockingAwait(); + + Assert.assertEquals(0, calls.get()); + } + + @Test + public void repeatUntilNormal() { + final AtomicInteger calls = new AtomicInteger(); + final AtomicInteger times = new AtomicInteger(5); + + Completable c = Completable.fromCallable(new Callable<Object>() { + @Override + public Object call() throws Exception { + calls.getAndIncrement(); + return null; + } + }).repeatUntil(new BooleanSupplier() { + @Override + public boolean getAsBoolean() { + return times.decrementAndGet() == 0; + } + }); + + c.blockingAwait(); + + Assert.assertEquals(5, calls.get()); + } + + @Test + public void retryNormal() { + Completable c = normal.completable.retry(); + + c.blockingAwait(); + + normal.assertSubscriptions(1); + } + + @Test + public void retry5Times() { + final AtomicInteger calls = new AtomicInteger(5); + Completable c = Completable.fromAction(new Action() { + @Override + public void run() { + if (calls.decrementAndGet() != 0) { + throw new TestException(); + } + } + }).retry(); + + c.blockingAwait(); + } + + @Test(expected = TestException.class) + public void retryBiPredicate5Times() { + Completable c = error.completable.retry(new BiPredicate<Integer, Throwable>() { + @Override + public boolean test(Integer n, Throwable e) { + return n < 5; + } + }); + + c.blockingAwait(); + } + + @Test(expected = TestException.class) + public void retryTimes5Error() { + Completable c = error.completable.retry(5); + + c.blockingAwait(); + } + + @Test + public void retryTimes5Normal() { + final AtomicInteger calls = new AtomicInteger(); + + Completable c = Completable.fromAction(new Action() { + @Override + public void run() { + if (calls.incrementAndGet() != 6) { + throw new TestException(); + } + } + }).retry(5); + + c.blockingAwait(); + + assertEquals(6, calls.get()); + } + + @Test(expected = IllegalArgumentException.class) + public void retryNegativeTimes() { + normal.completable.retry(-1); + } + + @Test(expected = TestException.class) + public void retryPredicateError() { + Completable c = error.completable.retry(new Predicate<Throwable>() { + @Override + public boolean test(Throwable e) { + return false; + } + }); + + c.blockingAwait(); + } + + @Test + public void retryPredicate5Times() { + final AtomicInteger calls = new AtomicInteger(5); + + Completable c = Completable.fromAction(new Action() { + @Override + public void run() { + if (calls.decrementAndGet() != 0) { + throw new TestException(); + } + } + }).retry(new Predicate<Throwable>() { + @Override + public boolean test(Throwable e) { + return true; + } + }); + + c.blockingAwait(); + } + + @Test + public void retryWhen5Times() { + final AtomicInteger calls = new AtomicInteger(5); + + Completable c = Completable.fromAction(new Action() { + @Override + public void run() { + if (calls.decrementAndGet() != 0) { + throw new TestException(); + } + } + }).retryWhen(new Function<Flowable<? extends Throwable>, Publisher<Object>>() { + @SuppressWarnings({ "rawtypes", "unchecked" }) + @Override + public Publisher<Object> apply(Flowable<? extends Throwable> f) { + return (Publisher)f; + } + }); + + c.blockingAwait(); + } + + @Test + public void subscribe() throws InterruptedException { + final AtomicBoolean complete = new AtomicBoolean(); + + Completable c = normal.completable + .delay(100, TimeUnit.MILLISECONDS) + .doOnComplete(new Action() { + @Override + public void run() { + complete.set(true); + } + }); + + Disposable d = c.subscribe(); + + assertFalse(d.isDisposed()); + + Thread.sleep(150); + + Assert.assertTrue("Not completed", complete.get()); + + assertTrue(d.isDisposed()); + } + + @Test + public void subscribeDispose() throws InterruptedException { + final AtomicBoolean complete = new AtomicBoolean(); + + Completable c = normal.completable + .delay(200, TimeUnit.MILLISECONDS) + .doOnComplete(new Action() { + @Override + public void run() { + complete.set(true); + } + }); + + Disposable d = c.subscribe(); + + Thread.sleep(100); + + d.dispose(); + + Thread.sleep(150); + + Assert.assertFalse("Completed", complete.get()); + } + + @Test + public void subscribeTwoCallbacksNormal() { + final AtomicReference<Throwable> err = new AtomicReference<>(); + final AtomicBoolean complete = new AtomicBoolean(); + normal.completable.subscribe(new Action() { + @Override + public void run() { + complete.set(true); + } + }, new Consumer<Throwable>() { + @Override + public void accept(Throwable e) { + err.set(e); + } + }); + + Assert.assertNull(err.get()); + Assert.assertTrue("Not completed", complete.get()); + } + + @Test + public void subscribeTwoCallbacksError() { + final AtomicReference<Throwable> err = new AtomicReference<>(); + final AtomicBoolean complete = new AtomicBoolean(); + error.completable.subscribe(new Action() { + @Override + public void run() { + complete.set(true); + } + }, new Consumer<Throwable>() { + @Override + public void accept(Throwable e) { + err.set(e); + } + }); + + Assert.assertTrue(err.get() instanceof TestException); + Assert.assertFalse("Not completed", complete.get()); + } + + @Test + public void subscribeTwoCallbacksCompleteThrows() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + final AtomicReference<Throwable> err = new AtomicReference<>(); + normal.completable.subscribe(new Action() { + @Override + public void run() { throw new TestException(); } + }, new Consumer<Throwable>() { + @Override + public void accept(Throwable e) { + err.set(e); + } + }); + + Assert.assertNull(err.get()); + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void subscribeTwoCallbacksOnErrorThrows() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + error.completable.subscribe(new Action() { + @Override + public void run() { } + }, new Consumer<Throwable>() { + @Override + public void accept(Throwable e) { throw new TestException(); } + }); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void subscribeObserverNormal() { + TestObserver<Object> to = new TestObserver<>(); + + normal.completable.toObservable().subscribe(to); + + to.assertComplete(); + to.assertNoValues(); + to.assertNoErrors(); + } + + @Test + public void subscribeObserverError() { + TestObserver<Object> to = new TestObserver<>(); + + error.completable.toObservable().subscribe(to); + + to.assertNotComplete(); + to.assertNoValues(); + to.assertError(TestException.class); + } + + @Test + public void subscribeActionNormal() { + final AtomicBoolean run = new AtomicBoolean(); + + normal.completable.subscribe(new Action() { + @Override + public void run() { + run.set(true); + } + }); + + Assert.assertTrue("Not completed", run.get()); + } + + @Test + public void subscribeActionError() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + final AtomicBoolean run = new AtomicBoolean(); + + error.completable.subscribe(new Action() { + @Override + public void run() { + run.set(true); + } + }); + + Assert.assertFalse("Completed", run.get()); + + TestHelper.assertError(errors, 0, OnErrorNotImplementedException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void subscribeSubscriberNormal() { + TestSubscriber<Object> ts = new TestSubscriber<>(); + + normal.completable.toFlowable().subscribe(ts); + + ts.assertComplete(); + ts.assertNoValues(); + ts.assertNoErrors(); + } + + @Test + public void subscribeSubscriberError() { + TestSubscriber<Object> ts = new TestSubscriber<>(); + + error.completable.toFlowable().subscribe(ts); + + ts.assertNotComplete(); + ts.assertNoValues(); + ts.assertError(TestException.class); + } + + @Test + public void subscribeOnNormal() { + final AtomicReference<String> name = new AtomicReference<>(); + + Completable c = Completable.unsafeCreate(new CompletableSource() { + @Override + public void subscribe(CompletableObserver observer) { + name.set(Thread.currentThread().getName()); + EmptyDisposable.complete(observer); + } + }).subscribeOn(Schedulers.computation()); + + c.blockingAwait(); + + Assert.assertTrue(name.get().startsWith("RxComputation")); + } + + @Test + public void subscribeOnError() { + final AtomicReference<String> name = new AtomicReference<>(); + + Completable c = Completable.unsafeCreate(new CompletableSource() { + @Override + public void subscribe(CompletableObserver observer) { + name.set(Thread.currentThread().getName()); + EmptyDisposable.error(new TestException(), observer); + } + }).subscribeOn(Schedulers.computation()); + + try { + c.blockingAwait(); + Assert.fail("No exception thrown"); + } catch (TestException ex) { + // expected + } + + Assert.assertTrue(name.get().startsWith("RxComputation")); + } + + @Test + public void timeoutSwitchNormal() { + Completable c = Completable.never().timeout(100, TimeUnit.MILLISECONDS, normal.completable); + + c.blockingAwait(); + + normal.assertSubscriptions(1); + } + + @Test + public void timeoutTimerCancelled() throws InterruptedException { + Completable c = Completable.fromCallable(new Callable<Object>() { + @Override + public Object call() throws Exception { + Thread.sleep(50); + return null; + } + }).timeout(100, TimeUnit.MILLISECONDS, normal.completable); + + c.blockingAwait(); + + Thread.sleep(100); + + normal.assertSubscriptions(0); + } + + @Test + public void toNormal() { + normal.completable + .to(new CompletableConverter<Flowable<Object>>() { + @Override + public Flowable<Object> apply(Completable c) { + return c.toFlowable(); + } + }) + .test() + .assertComplete() + .assertNoValues(); + } + + @Test + public void asNormal() { + normal.completable + .to(new CompletableConverter<Flowable<Object>>() { + @Override + public Flowable<Object> apply(Completable c) { + return c.toFlowable(); + } + }) + .test() + .assertComplete() + .assertNoValues(); + } + + @Test + public void as() { + Completable.complete().to(new CompletableConverter<Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Completable v) { + return v.toFlowable(); + } + }) + .test() + .assertComplete(); + } + + @Test + public void toFlowableNormal() { + normal.completable.toFlowable().blockingForEach(Functions.emptyConsumer()); + } + + @Test(expected = TestException.class) + public void toFlowableError() { + error.completable.toFlowable().blockingForEach(Functions.emptyConsumer()); + } + + @Test + public void toObservableNormal() { + normal.completable.toObservable().blockingForEach(Functions.emptyConsumer()); + } + + @Test(expected = TestException.class) + public void toObservableError() { + error.completable.toObservable().blockingForEach(Functions.emptyConsumer()); + } + + @Test + public void toSingleSupplierNormal() { + Assert.assertEquals(1, normal.completable.toSingle(new Supplier<Object>() { + @Override + public Object get() { + return 1; + } + }).blockingGet()); + } + + @Test(expected = TestException.class) + public void toSingleSupplierError() { + error.completable.toSingle(new Supplier<Object>() { + @Override + public Object get() { + return 1; + } + }).blockingGet(); + } + + @Test(expected = NullPointerException.class) + public void toSingleSupplierReturnsNull() { + normal.completable.toSingle(new Supplier<Object>() { + @Override + public Object get() { + return null; + } + }).blockingGet(); + } + + @Test(expected = TestException.class) + public void toSingleSupplierThrows() { + normal.completable.toSingle(new Supplier<Object>() { + @Override + public Object get() { throw new TestException(); } + }).blockingGet(); + } + + @Test(expected = TestException.class) + public void toSingleDefaultError() { + error.completable.toSingleDefault(1).blockingGet(); + } + + @Test + public void toSingleDefaultNormal() { + Assert.assertEquals((Integer)1, normal.completable.toSingleDefault(1).blockingGet()); + } + + @Test + public void unsubscribeOnNormal() throws InterruptedException { + final AtomicReference<String> name = new AtomicReference<>(); + final CountDownLatch cdl = new CountDownLatch(1); + + normal.completable.delay(1, TimeUnit.SECONDS) + .doOnDispose(new Action() { + @Override + public void run() { + name.set(Thread.currentThread().getName()); + cdl.countDown(); + } + }) + .unsubscribeOn(Schedulers.computation()) + .subscribe(new CompletableObserver() { + @Override + public void onSubscribe(final Disposable d) { + Schedulers.single().scheduleDirect(new Runnable() { + @Override + public void run() { + d.dispose(); + } + }, 100, TimeUnit.MILLISECONDS); + } + + @Override + public void onError(Throwable e) { + + } + + @Override + public void onComplete() { + + } + }); + + cdl.await(); + + Assert.assertTrue(name.get().startsWith("RxComputation")); + } + + @Test + public void ambArrayEmpty() { + Completable c = Completable.ambArray(); + + c.blockingAwait(); + } + + @Test + public void ambArraySingleNormal() { + Completable c = Completable.ambArray(normal.completable); + + c.blockingAwait(); + } + + @Test + public void ambArraySingleError() { + Completable.ambArray(error.completable) + .test() + .assertError(TestException.class); + } + + @Test + public void ambArrayOneFires() { + PublishProcessor<Object> pp1 = PublishProcessor.create(); + PublishProcessor<Object> pp2 = PublishProcessor.create(); + + Completable c1 = Completable.fromPublisher(pp1); + + Completable c2 = Completable.fromPublisher(pp2); + + Completable c = Completable.ambArray(c1, c2); + + final AtomicBoolean complete = new AtomicBoolean(); + + c.subscribe(new Action() { + @Override + public void run() { + complete.set(true); + } + }); + + Assert.assertTrue("First subject no subscribers", pp1.hasSubscribers()); + Assert.assertTrue("Second subject no subscribers", pp2.hasSubscribers()); + + pp1.onComplete(); + + Assert.assertFalse("First subject has subscribers", pp1.hasSubscribers()); + Assert.assertFalse("Second subject has subscribers", pp2.hasSubscribers()); + + Assert.assertTrue("Not completed", complete.get()); + } + + @Test + public void ambArrayOneFiresError() { + PublishProcessor<Object> pp1 = PublishProcessor.create(); + PublishProcessor<Object> pp2 = PublishProcessor.create(); + + Completable c1 = Completable.fromPublisher(pp1); + + Completable c2 = Completable.fromPublisher(pp2); + + Completable c = Completable.ambArray(c1, c2); + + final AtomicReference<Throwable> complete = new AtomicReference<>(); + + c.subscribe(Functions.EMPTY_ACTION, new Consumer<Throwable>() { + @Override + public void accept(Throwable v) { + complete.set(v); + } + }); + + Assert.assertTrue("First subject no subscribers", pp1.hasSubscribers()); + Assert.assertTrue("Second subject no subscribers", pp2.hasSubscribers()); + + pp1.onError(new TestException()); + + Assert.assertFalse("First subject has subscribers", pp1.hasSubscribers()); + Assert.assertFalse("Second subject has subscribers", pp2.hasSubscribers()); + + Assert.assertTrue("Not completed", complete.get() instanceof TestException); + } + + @Test + public void ambArraySecondFires() { + PublishProcessor<Object> pp1 = PublishProcessor.create(); + PublishProcessor<Object> pp2 = PublishProcessor.create(); + + Completable c1 = Completable.fromPublisher(pp1); + + Completable c2 = Completable.fromPublisher(pp2); + + Completable c = Completable.ambArray(c1, c2); + + final AtomicBoolean complete = new AtomicBoolean(); + + c.subscribe(new Action() { + @Override + public void run() { + complete.set(true); + } + }); + + Assert.assertTrue("First subject no subscribers", pp1.hasSubscribers()); + Assert.assertTrue("Second subject no subscribers", pp2.hasSubscribers()); + + pp2.onComplete(); + + Assert.assertFalse("First subject has subscribers", pp1.hasSubscribers()); + Assert.assertFalse("Second subject has subscribers", pp2.hasSubscribers()); + + Assert.assertTrue("Not completed", complete.get()); + } + + @Test + public void ambArraySecondFiresError() { + PublishProcessor<Object> pp1 = PublishProcessor.create(); + PublishProcessor<Object> pp2 = PublishProcessor.create(); + + Completable c1 = Completable.fromPublisher(pp1); + + Completable c2 = Completable.fromPublisher(pp2); + + Completable c = Completable.ambArray(c1, c2); + + final AtomicReference<Throwable> complete = new AtomicReference<>(); + + c.subscribe(Functions.EMPTY_ACTION, new Consumer<Throwable>() { + @Override + public void accept(Throwable v) { + complete.set(v); + } + }); + + Assert.assertTrue("First subject no subscribers", pp1.hasSubscribers()); + Assert.assertTrue("Second subject no subscribers", pp2.hasSubscribers()); + + pp2.onError(new TestException()); + + Assert.assertFalse("First subject has subscribers", pp1.hasSubscribers()); + Assert.assertFalse("Second subject has subscribers", pp2.hasSubscribers()); + + Assert.assertTrue("Not completed", complete.get() instanceof TestException); + } + + @Test + public void ambMultipleOneIsNull() { + Completable.ambArray(null, normal.completable) + .test() + .assertError(NullPointerException.class); + } + + @Test + public void ambIterableEmpty() { + Completable c = Completable.amb(Collections.<Completable>emptyList()); + + c.blockingAwait(); + } + + @Test + public void ambIterableIteratorNull() { + Completable.amb(new Iterable<Completable>() { + @Override + public Iterator<Completable> iterator() { + return null; + } + }).test().assertError(NullPointerException.class); + } + + @Test + public void ambIterableWithNull() { + Completable.amb(Arrays.asList(null, normal.completable)) + .test() + .assertError(NullPointerException.class); + } + + @Test + public void ambIterableSingle() { + Completable c = Completable.amb(Collections.singleton(normal.completable)); + + c.blockingAwait(); + + normal.assertSubscriptions(1); + } + + @Test + public void ambIterableMany() { + Completable c = Completable.amb(Arrays.asList(normal.completable, normal.completable, normal.completable)); + + c.blockingAwait(); + + normal.assertSubscriptions(1); + } + + @Test + public void ambIterableOneThrows() { + Completable.amb(Collections.singleton(error.completable)) + .test() + .assertError(TestException.class); + } + + @Test + public void ambIterableManyOneThrows() { + Completable.amb(Arrays.asList(error.completable, normal.completable)) + .test() + .assertError(TestException.class); + } + + @Test + public void ambIterableIterableThrows() { + Completable.amb(new Iterable<Completable>() { + @Override + public Iterator<Completable> iterator() { + throw new TestException(); + } + }).test().assertError(TestException.class); + } + + @Test + public void ambIterableIteratorHasNextThrows() { + Completable.amb(new IterableIteratorHasNextThrows()) + .test() + .assertError(TestException.class); + } + + @Test + public void ambIterableIteratorNextThrows() { + Completable.amb(new IterableIteratorNextThrows()) + .test() + .assertError(TestException.class); + } + + @Test + public void ambWithArrayOneFires() { + PublishProcessor<Object> pp1 = PublishProcessor.create(); + PublishProcessor<Object> pp2 = PublishProcessor.create(); + + Completable c1 = Completable.fromPublisher(pp1); + + Completable c2 = Completable.fromPublisher(pp2); + + Completable c = c1.ambWith(c2); + + final AtomicBoolean complete = new AtomicBoolean(); + + c.subscribe(new Action() { + @Override + public void run() { + complete.set(true); + } + }); + + Assert.assertTrue("First subject no subscribers", pp1.hasSubscribers()); + Assert.assertTrue("Second subject no subscribers", pp2.hasSubscribers()); + + pp1.onComplete(); + + Assert.assertFalse("First subject has subscribers", pp1.hasSubscribers()); + Assert.assertFalse("Second subject has subscribers", pp2.hasSubscribers()); + + Assert.assertTrue("Not completed", complete.get()); + } + + @Test + public void ambWithArrayOneFiresError() { + PublishProcessor<Object> pp1 = PublishProcessor.create(); + PublishProcessor<Object> pp2 = PublishProcessor.create(); + + Completable c1 = Completable.fromPublisher(pp1); + + Completable c2 = Completable.fromPublisher(pp2); + + Completable c = c1.ambWith(c2); + + final AtomicReference<Throwable> complete = new AtomicReference<>(); + + c.subscribe(Functions.EMPTY_ACTION, new Consumer<Throwable>() { + @Override + public void accept(Throwable v) { + complete.set(v); + } + }); + + Assert.assertTrue("First subject no subscribers", pp1.hasSubscribers()); + Assert.assertTrue("Second subject no subscribers", pp2.hasSubscribers()); + + pp1.onError(new TestException()); + + Assert.assertFalse("First subject has subscribers", pp1.hasSubscribers()); + Assert.assertFalse("Second subject has subscribers", pp2.hasSubscribers()); + + Assert.assertTrue("Not completed", complete.get() instanceof TestException); + } + + @Test + public void ambWithArraySecondFires() { + PublishProcessor<Object> pp1 = PublishProcessor.create(); + PublishProcessor<Object> pp2 = PublishProcessor.create(); + + Completable c1 = Completable.fromPublisher(pp1); + + Completable c2 = Completable.fromPublisher(pp2); + + Completable c = c1.ambWith(c2); + + final AtomicBoolean complete = new AtomicBoolean(); + + c.subscribe(new Action() { + @Override + public void run() { + complete.set(true); + } + }); + + Assert.assertTrue("First subject no subscribers", pp1.hasSubscribers()); + Assert.assertTrue("Second subject no subscribers", pp2.hasSubscribers()); + + pp2.onComplete(); + + Assert.assertFalse("First subject has subscribers", pp1.hasSubscribers()); + Assert.assertFalse("Second subject has subscribers", pp2.hasSubscribers()); + + Assert.assertTrue("Not completed", complete.get()); + } + + @Test + public void ambWithArraySecondFiresError() { + PublishProcessor<Object> pp1 = PublishProcessor.create(); + PublishProcessor<Object> pp2 = PublishProcessor.create(); + + Completable c1 = Completable.fromPublisher(pp1); + + Completable c2 = Completable.fromPublisher(pp2); + + Completable c = c1.ambWith(c2); + + final AtomicReference<Throwable> complete = new AtomicReference<>(); + + c.subscribe(Functions.EMPTY_ACTION, new Consumer<Throwable>() { + @Override + public void accept(Throwable v) { + complete.set(v); + } + }); + + Assert.assertTrue("First subject no subscribers", pp1.hasSubscribers()); + Assert.assertTrue("Second subject no subscribers", pp2.hasSubscribers()); + + pp2.onError(new TestException()); + + Assert.assertFalse("First subject has subscribers", pp1.hasSubscribers()); + Assert.assertFalse("Second subject has subscribers", pp2.hasSubscribers()); + + Assert.assertTrue("Not completed", complete.get() instanceof TestException); + } + + @Test + public void startWithCompletableNormal() { + final AtomicBoolean run = new AtomicBoolean(); + Completable c = normal.completable + .startWith(Completable.fromCallable(new Callable<Object>() { + @Override + public Object call() throws Exception { + run.set(normal.get() == 0); + return null; + } + })); + + c.blockingAwait(); + + Assert.assertTrue("Did not start with other", run.get()); + normal.assertSubscriptions(1); + } + + @Test + public void startWithCompletableError() { + Completable c = normal.completable.startWith(error.completable); + + try { + c.blockingAwait(); + Assert.fail("Did not throw TestException"); + } catch (TestException ex) { + normal.assertSubscriptions(0); + error.assertSubscriptions(1); + } + } + + @Test + public void startWithFlowableNormal() { + final AtomicBoolean run = new AtomicBoolean(); + Flowable<Object> c = normal.completable + .startWith(Flowable.fromCallable(new Callable<Object>() { + @Override + public Object call() throws Exception { + run.set(normal.get() == 0); + return 1; + } + })); + + TestSubscriber<Object> ts = new TestSubscriber<>(); + + c.subscribe(ts); + + Assert.assertTrue("Did not start with other", run.get()); + normal.assertSubscriptions(1); + + ts.assertValue(1); + ts.assertComplete(); + ts.assertNoErrors(); + } + + @Test + public void startWithFlowableError() { + Flowable<Object> c = normal.completable + .startWith(Flowable.error(new TestException())); + + TestSubscriber<Object> ts = new TestSubscriber<>(); + + c.subscribe(ts); + + normal.assertSubscriptions(0); + + ts.assertNoValues(); + ts.assertError(TestException.class); + ts.assertNotComplete(); + } + + @Test + public void startWithObservableNormal() { + final AtomicBoolean run = new AtomicBoolean(); + Observable<Object> o = normal.completable + .startWith(Observable.fromCallable(new Callable<Object>() { + @Override + public Object call() throws Exception { + run.set(normal.get() == 0); + return 1; + } + })); + + TestObserver<Object> to = new TestObserver<>(); + + o.subscribe(to); + + Assert.assertTrue("Did not start with other", run.get()); + normal.assertSubscriptions(1); + + to.assertValue(1); + to.assertComplete(); + to.assertNoErrors(); + } + + @Test + public void startWithObservableError() { + Observable<Object> o = normal.completable + .startWith(Observable.error(new TestException())); + + TestObserver<Object> to = new TestObserver<>(); + + o.subscribe(to); + + normal.assertSubscriptions(0); + + to.assertNoValues(); + to.assertError(TestException.class); + to.assertNotComplete(); + } + + @Test + public void andThen() { + TestSubscriber<String> ts = new TestSubscriber<>(0); + Completable.complete().andThen(Flowable.just("foo")).subscribe(ts); + ts.request(1); + ts.assertValue("foo"); + ts.assertComplete(); + ts.assertNoErrors(); + + TestObserver<String> to = new TestObserver<>(); + Completable.complete().andThen(Observable.just("foo")).subscribe(to); + to.assertValue("foo"); + to.assertComplete(); + to.assertNoErrors(); + } + + private static void expectUncaughtTestException(Action action) { + Thread.UncaughtExceptionHandler originalHandler = Thread.getDefaultUncaughtExceptionHandler(); + CapturingUncaughtExceptionHandler handler = new CapturingUncaughtExceptionHandler(); + Thread.setDefaultUncaughtExceptionHandler(handler); + RxJavaPlugins.setErrorHandler(new Consumer<Throwable>() { + @Override + public void accept(Throwable error) throws Exception { + Thread.currentThread().getUncaughtExceptionHandler().uncaughtException(Thread.currentThread(), error); + } + }); + try { + action.run(); + assertEquals("Should have received exactly 1 exception", 1, handler.count); + Throwable caught = handler.caught; + while (caught != null) { + if (caught instanceof TestException) { break; } + if (caught == caught.getCause()) { break; } + caught = caught.getCause(); + } + assertTrue("A TestException should have been delivered to the handler", + caught instanceof TestException); + } catch (Throwable ex) { + throw ExceptionHelper.wrapOrThrow(ex); + } finally { + Thread.setDefaultUncaughtExceptionHandler(originalHandler); + RxJavaPlugins.setErrorHandler(null); + } + } + + @Test + public void subscribeOneActionThrowFromOnCompleted() { + expectUncaughtTestException(new Action() { + @Override + public void run() { + normal.completable.subscribe(new Action() { + @Override + public void run() { + throw new TestException(); + } + }); + } + }); + } + + @Test + public void subscribeTwoActionsThrowFromOnError() { + expectUncaughtTestException(new Action() { + @Override + public void run() { + error.completable.subscribe( + new Action() { + @Override + public void run() { + } + }, + new Consumer<Throwable>() { + @Override + public void accept(Throwable throwable) { + throw new TestException(); + } + }); + } + }); + } + + @Test + public void propagateExceptionSubscribeOneAction() { + expectUncaughtTestException(new Action() { + @Override + public void run() { + error.completable.toSingleDefault(1).subscribe(new Consumer<Integer>() { + @Override + public void accept(Integer integer) { + } + }); + } + }); + } + + @Test + public void usingFactoryReturnsNullAndDisposerThrows() { + Consumer<Integer> onDispose = new Consumer<Integer>() { + @Override + public void accept(Integer t) { + throw new TestException(); + } + }; + + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(); + + Completable.using(new Supplier<Integer>() { + @Override + public Integer get() { + return 1; + } + }, + new Function<Integer, Completable>() { + @Override + public Completable apply(Integer t) { + return null; + } + }, onDispose).<Integer>toFlowable().subscribe(ts); + + ts.assertNoValues(); + ts.assertNotComplete(); + ts.assertError(CompositeException.class); + + CompositeException ex = (CompositeException)ts.errors().get(0); + + List<Throwable> listEx = ex.getExceptions(); + + assertEquals(2, listEx.size()); + + assertTrue(listEx.get(0).toString(), listEx.get(0) instanceof NullPointerException); + assertTrue(listEx.get(1).toString(), listEx.get(1) instanceof TestException); + } + + @Test + public void subscribeReportsUnsubscribedOnError() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + PublishSubject<String> stringSubject = PublishSubject.create(); + Completable completable = stringSubject.ignoreElements(); + + Disposable completableSubscription = completable.subscribe(); + + stringSubject.onError(new TestException()); + + assertTrue("Not unsubscribed?", completableSubscription.isDisposed()); + + TestHelper.assertError(errors, 0, OnErrorNotImplementedException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void subscribeActionReportsUnsubscribed() { + PublishSubject<String> stringSubject = PublishSubject.create(); + Completable completable = stringSubject.ignoreElements(); + + Disposable completableSubscription = completable.subscribe(new Action() { + @Override + public void run() { + + } + }); + + stringSubject.onComplete(); + + assertTrue("Not unsubscribed?", completableSubscription.isDisposed()); + } + + @Test + public void subscribeActionReportsUnsubscribedAfter() { + PublishSubject<String> stringSubject = PublishSubject.create(); + Completable completable = stringSubject.ignoreElements(); + + final AtomicReference<Disposable> disposableRef = new AtomicReference<>(); + Disposable completableSubscription = completable.subscribe(new Action() { + @Override + public void run() { + if (disposableRef.get().isDisposed()) { + disposableRef.set(null); + } + } + }); + disposableRef.set(completableSubscription); + + stringSubject.onComplete(); + + assertTrue("Not unsubscribed?", completableSubscription.isDisposed()); + assertNotNull("Unsubscribed before the call to onComplete", disposableRef.get()); + } + + @Test + public void subscribeActionReportsUnsubscribedOnError() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + PublishSubject<String> stringSubject = PublishSubject.create(); + Completable completable = stringSubject.ignoreElements(); + + Disposable completableSubscription = completable.subscribe(new Action() { + @Override + public void run() { + } + }); + + stringSubject.onError(new TestException()); + + assertTrue("Not unsubscribed?", completableSubscription.isDisposed()); + + TestHelper.assertError(errors, 0, OnErrorNotImplementedException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void subscribeAction2ReportsUnsubscribed() { + PublishSubject<String> stringSubject = PublishSubject.create(); + Completable completable = stringSubject.ignoreElements(); + + Disposable completableSubscription = completable.subscribe(new Action() { + @Override + public void run() { + + } + }, new Consumer<Throwable>() { + @Override + public void accept(Throwable t) { + + } + }); + + stringSubject.onComplete(); + + assertTrue("Not unsubscribed?", completableSubscription.isDisposed()); + } + + @Test + public void subscribeAction2ReportsUnsubscribedOnError() { + PublishSubject<String> stringSubject = PublishSubject.create(); + Completable completable = stringSubject.ignoreElements(); + + Disposable completableSubscription = completable.subscribe(new Action() { + @Override + public void run() { } + }, new Consumer<Throwable>() { + @Override + public void accept(Throwable e) { } + }); + + stringSubject.onError(new TestException()); + + assertTrue("Not unsubscribed?", completableSubscription.isDisposed()); + } + + @Test + public void andThenSubscribeOn() { + TestSubscriberEx<String> ts = new TestSubscriberEx<>(0); + TestScheduler scheduler = new TestScheduler(); + Completable.complete().andThen(Flowable.just("foo").delay(1, TimeUnit.SECONDS, scheduler)).subscribe(ts); + + ts.request(1); + ts.assertNoValues(); + ts.assertNotTerminated(); + + scheduler.advanceTimeBy(1, TimeUnit.SECONDS); + + ts.assertValue("foo"); + ts.assertComplete(); + ts.assertNoErrors(); + } + + @Test + public void andThenSingleNever() { + TestSubscriberEx<String> ts = new TestSubscriberEx<>(0); + Completable.never().andThen(Single.just("foo")).toFlowable().subscribe(ts); + ts.request(1); + ts.assertNoValues(); + ts.assertNotTerminated(); + } + + @Test + public void andThenSingleError() { + TestSubscriber<String> ts = new TestSubscriber<>(0); + final AtomicBoolean hasRun = new AtomicBoolean(false); + final Exception e = new Exception(); + Completable.error(e) + .andThen(new Single<String>() { + @Override + public void subscribeActual(SingleObserver<? super String> observer) { + hasRun.set(true); + observer.onSuccess("foo"); + } + }) + .toFlowable().subscribe(ts); + ts.assertNoValues(); + ts.assertError(e); + Assert.assertFalse("Should not have subscribed to single when completable errors", hasRun.get()); + } + + @Test + public void andThenSingleSubscribeOn() { + TestSubscriberEx<String> ts = new TestSubscriberEx<>(0); + TestScheduler scheduler = new TestScheduler(); + Completable.complete().andThen(Single.just("foo").delay(1, TimeUnit.SECONDS, scheduler)).toFlowable().subscribe(ts); + + ts.request(1); + ts.assertNoValues(); + ts.assertNotTerminated(); + + scheduler.advanceTimeBy(1, TimeUnit.SECONDS); + + ts.assertValue("foo"); + ts.assertComplete(); + ts.assertNoErrors(); + } + + private Function<Completable, Completable> onCreate; + + private BiFunction<Completable, CompletableObserver, CompletableObserver> onStart; + + @Before + public void setUp() throws Exception { + onCreate = spy(new Function<Completable, Completable>() { + @Override + public Completable apply(Completable t) { + return t; + } + }); + + RxJavaPlugins.setOnCompletableAssembly(onCreate); + + onStart = spy(new BiFunction<Completable, CompletableObserver, CompletableObserver>() { + @Override + public CompletableObserver apply(Completable t1, CompletableObserver t2) { + return t2; + } + }); + + RxJavaPlugins.setOnCompletableSubscribe(onStart); + } + + @After + public void after() { + RxJavaPlugins.reset(); + } + + @Test + public void hookCreate() throws Throwable { + CompletableSource subscriber = mock(CompletableSource.class); + Completable create = Completable.unsafeCreate(subscriber); + + verify(onCreate, times(1)).apply(create); + } + + @Test + public void doOnCompletedNormal() { + final AtomicInteger calls = new AtomicInteger(); + + Completable c = normal.completable.doOnComplete(new Action() { + @Override + public void run() { + calls.getAndIncrement(); + } + }); + + c.blockingAwait(); + + Assert.assertEquals(1, calls.get()); + } + + @Test + public void doOnCompletedError() { + final AtomicInteger calls = new AtomicInteger(); + + Completable c = error.completable.doOnComplete(new Action() { + @Override + public void run() { + calls.getAndIncrement(); + } + }); + + try { + c.blockingAwait(); + Assert.fail("Failed to throw TestException"); + } catch (TestException ex) { + // expected + } + + Assert.assertEquals(0, calls.get()); + } + + @Test(expected = TestException.class) + public void doOnCompletedThrows() { + Completable c = normal.completable.doOnComplete(new Action() { + @Override + public void run() { throw new TestException(); } + }); + + c.blockingAwait(); + } + + @Test + public void doAfterTerminateNormal() { + final AtomicBoolean doneAfter = new AtomicBoolean(); + final AtomicBoolean complete = new AtomicBoolean(); + + Completable c = normal.completable.doAfterTerminate(new Action() { + @Override + public void run() { + doneAfter.set(complete.get()); + } + }); + + c.subscribe(new CompletableObserver() { + @Override + public void onSubscribe(Disposable d) { + + } + + @Override + public void onError(Throwable e) { + + } + + @Override + public void onComplete() { + complete.set(true); + } + }); + + c.blockingAwait(); + + Assert.assertTrue("Not completed", complete.get()); + Assert.assertTrue("Closure called before onComplete", doneAfter.get()); + } + + @Test + public void doAfterTerminateWithError() { + final AtomicBoolean doneAfter = new AtomicBoolean(); + + Completable c = error.completable.doAfterTerminate(new Action() { + @Override + public void run() { + doneAfter.set(true); + } + }); + + try { + c.blockingAwait(5, TimeUnit.SECONDS); + Assert.fail("Did not throw TestException"); + } catch (TestException ex) { + // expected + } + + Assert.assertTrue("Closure not called", doneAfter.get()); + } + + @Test + public void subscribeEmptyOnError() { + expectUncaughtTestException(new Action() { + @Override public void run() { + error.completable.subscribe(); + } + }); + } + + @Test + public void subscribeOneActionOnError() { + expectUncaughtTestException(new Action() { + @Override + public void run() { + error.completable.subscribe(new Action() { + @Override + public void run() { + } + }); + } + }); + } + + @Test + public void propagateExceptionSubscribeEmpty() { + expectUncaughtTestException(new Action() { + @Override + public void run() { + error.completable.toSingleDefault(0).subscribe(); + } + }); + } + + @Test + public void andThenCompletableNormal() { + final AtomicBoolean run = new AtomicBoolean(); + Completable c = normal.completable + .andThen(Completable.fromCallable(new Callable<Object>() { + @Override + public Object call() throws Exception { + run.set(normal.get() == 0); + return null; + } + })); + + c.blockingAwait(); + + Assert.assertFalse("Start with other", run.get()); + normal.assertSubscriptions(1); + } + + @Test + public void andThenCompletableError() { + Completable c = normal.completable.andThen(error.completable); + + try { + c.blockingAwait(); + Assert.fail("Did not throw TestException"); + } catch (TestException ex) { + normal.assertSubscriptions(1); + error.assertSubscriptions(1); + } + } + + @Test + public void andThenFlowableNormal() { + final AtomicBoolean run = new AtomicBoolean(); + Flowable<Object> c = normal.completable + .andThen(Flowable.fromCallable(new Callable<Object>() { + @Override + public Object call() throws Exception { + run.set(normal.get() == 0); + return 1; + } + })); + + TestSubscriber<Object> ts = new TestSubscriber<>(); + + c.subscribe(ts); + + Assert.assertFalse("Start with other", run.get()); + normal.assertSubscriptions(1); + + ts.assertValue(1); + ts.assertComplete(); + ts.assertNoErrors(); + } + + @Test + public void andThenFlowableError() { + Flowable<Object> c = normal.completable + .andThen(Flowable.error(new TestException())); + + TestSubscriber<Object> ts = new TestSubscriber<>(); + + c.subscribe(ts); + + normal.assertSubscriptions(1); + + ts.assertNoValues(); + ts.assertError(TestException.class); + ts.assertNotComplete(); + } + + @Test + public void usingFactoryThrows() throws Throwable { + @SuppressWarnings("unchecked") + Consumer<Integer> onDispose = mock(Consumer.class); + + TestSubscriber<Integer> ts = new TestSubscriber<>(); + + Completable.using(new Supplier<Integer>() { + @Override + public Integer get() { + return 1; + } + }, + new Function<Integer, Completable>() { + @Override + public Completable apply(Integer t) { + throw new TestException(); + } + }, onDispose).<Integer>toFlowable().subscribe(ts); + + verify(onDispose).accept(1); + + ts.assertNoValues(); + ts.assertNotComplete(); + ts.assertError(TestException.class); + } + + @Test + public void usingFactoryAndDisposerThrow() { + Consumer<Integer> onDispose = new Consumer<Integer>() { + @Override + public void accept(Integer t) { + throw new TestException(); + } + }; + + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(); + + Completable.using(new Supplier<Integer>() { + @Override + public Integer get() { + return 1; + } + }, + new Function<Integer, Completable>() { + @Override + public Completable apply(Integer t) { + throw new TestException(); + } + }, onDispose).<Integer>toFlowable().subscribe(ts); + + ts.assertNoValues(); + ts.assertNotComplete(); + ts.assertError(CompositeException.class); + + CompositeException ex = (CompositeException)ts.errors().get(0); + + List<Throwable> listEx = ex.getExceptions(); + + assertEquals(2, listEx.size()); + + assertTrue(listEx.get(0).toString(), listEx.get(0) instanceof TestException); + assertTrue(listEx.get(1).toString(), listEx.get(1) instanceof TestException); + } + + @Test + public void usingFactoryReturnsNull() throws Throwable { + @SuppressWarnings("unchecked") + Consumer<Integer> onDispose = mock(Consumer.class); + + TestSubscriber<Integer> ts = TestSubscriber.create(); + + Completable.using(new Supplier<Integer>() { + @Override + public Integer get() { + return 1; + } + }, + new Function<Integer, Completable>() { + @Override + public Completable apply(Integer t) { + return null; + } + }, onDispose).<Integer>toFlowable().subscribe(ts); + + verify(onDispose).accept(1); + + ts.assertNoValues(); + ts.assertNotComplete(); + ts.assertError(NullPointerException.class); + } + + @Test + public void subscribeReportsUnsubscribed() { + PublishSubject<String> stringSubject = PublishSubject.create(); + Completable completable = stringSubject.ignoreElements(); + + Disposable completableSubscription = completable.subscribe(); + + stringSubject.onComplete(); + + assertTrue("Not unsubscribed?", completableSubscription.isDisposed()); + } + + @Test + public void hookSubscribeStart() throws Throwable { + TestSubscriber<String> ts = new TestSubscriber<>(); + + Completable completable = Completable.unsafeCreate(new CompletableSource() { + @Override public void subscribe(CompletableObserver observer) { + observer.onComplete(); + } + }); + completable.<String>toFlowable().subscribe(ts); + + verify(onStart, times(1)).apply(eq(completable), any(CompletableObserver.class)); + } + + @Test + public void onStartCalledSafe() { + TestSubscriber<Object> ts = new TestSubscriber<Object>() { + @Override + public void onStart() { + onNext(1); + } + }; + + normal.completable.<Object>toFlowable().subscribe(ts); + + ts.assertValue(1); + ts.assertNoErrors(); + ts.assertComplete(); + } + + @Test + public void onErrorCompleteFunctionThrows() { + TestSubscriberEx<String> ts = new TestSubscriberEx<>(); + + error.completable.onErrorComplete(new Predicate<Throwable>() { + @Override + public boolean test(Throwable t) { + throw new TestException("Forced inner failure"); + } + }).<String>toFlowable().subscribe(ts); + + ts.assertNoValues(); + ts.assertNotComplete(); + ts.assertError(CompositeException.class); + + CompositeException composite = (CompositeException)ts.errors().get(0); + + List<Throwable> errors = composite.getExceptions(); + Assert.assertEquals(2, errors.size()); + + Assert.assertTrue(errors.get(0).toString(), errors.get(0) instanceof TestException); + Assert.assertNull(errors.get(0).toString(), errors.get(0).getMessage()); + Assert.assertTrue(errors.get(1).toString(), errors.get(1) instanceof TestException); + Assert.assertEquals(errors.get(1).toString(), "Forced inner failure", errors.get(1).getMessage()); + } + + @Test + public void subscribeAction2ReportsUnsubscribedAfter() { + PublishSubject<String> stringSubject = PublishSubject.create(); + Completable completable = stringSubject.ignoreElements(); + + final AtomicReference<Disposable> disposableRef = new AtomicReference<>(); + Disposable completableSubscription = completable.subscribe(new Action() { + @Override + public void run() { + if (disposableRef.get().isDisposed()) { + disposableRef.set(null); + } + } + }, Functions.emptyConsumer()); + disposableRef.set(completableSubscription); + + stringSubject.onComplete(); + + assertTrue("Not unsubscribed?", completableSubscription.isDisposed()); + assertNotNull("Unsubscribed before the call to onComplete", disposableRef.get()); + } + + @Test + public void subscribeAction2ReportsUnsubscribedOnErrorAfter() { + PublishSubject<String> stringSubject = PublishSubject.create(); + Completable completable = stringSubject.ignoreElements(); + + final AtomicReference<Disposable> disposableRef = new AtomicReference<>(); + Disposable completableSubscription = completable.subscribe(Functions.EMPTY_ACTION, + new Consumer<Throwable>() { + @Override + public void accept(Throwable e) { + if (disposableRef.get().isDisposed()) { + disposableRef.set(null); + } + } + }); + disposableRef.set(completableSubscription); + + stringSubject.onError(new TestException()); + + assertTrue("Not unsubscribed?", completableSubscription.isDisposed()); + assertNotNull("Unsubscribed before the call to onError", disposableRef.get()); + } + + @Test + public void propagateExceptionSubscribeOneActionThrowFromOnSuccess() { + expectUncaughtTestException(new Action() { + @Override + public void run() { + normal.completable.toSingleDefault(1).subscribe(new Consumer<Integer>() { + @Override + public void accept(Integer integer) { + throw new TestException(); + } + }); + } + }); + } + + @Test + public void andThenNever() { + TestSubscriberEx<String> ts = new TestSubscriberEx<>(0); + Completable.never().andThen(Flowable.just("foo")).subscribe(ts); + ts.request(1); + ts.assertNoValues(); + ts.assertNotTerminated(); + } + + @Test + public void andThenError() { + TestSubscriber<String> ts = new TestSubscriber<>(0); + final AtomicBoolean hasRun = new AtomicBoolean(false); + final Exception e = new Exception(); + Completable.unsafeCreate(new CompletableSource() { + @Override + public void subscribe(CompletableObserver co) { + co.onSubscribe(Disposable.empty()); + co.onError(e); + } + }) + .andThen(Flowable.<String>unsafeCreate(new Publisher<String>() { + @Override + public void subscribe(Subscriber<? super String> s) { + hasRun.set(true); + s.onSubscribe(new BooleanSubscription()); + s.onNext("foo"); + s.onComplete(); + } + })) + .subscribe(ts); + ts.assertNoValues(); + ts.assertError(e); + Assert.assertFalse("Should not have subscribed to observable when completable errors", hasRun.get()); + } + + @Test + public void andThenSingle() { + TestSubscriber<String> ts = new TestSubscriber<>(0); + Completable.complete().andThen(Single.just("foo")).toFlowable().subscribe(ts); + ts.request(1); + ts.assertValue("foo"); + ts.assertComplete(); + ts.assertNoErrors(); + } + + @Test + public void fromFutureNormal() { + ExecutorService exec = Executors.newSingleThreadExecutor(); + + try { + Completable c = Completable.fromFuture(exec.submit(new Runnable() { + @Override + public void run() { + // no action + } + })); + + c.blockingAwait(); + } finally { + exec.shutdown(); + } + } + + @Test + public void fromFutureThrows() { + ExecutorService exec = Executors.newSingleThreadExecutor(); + + Completable c = Completable.fromFuture(exec.submit(new Runnable() { + @Override + public void run() { + throw new TestException(); + } + })); + + try { + c.blockingAwait(); + Assert.fail("Failed to throw Exception"); + } catch (RuntimeException ex) { + if (!((ex.getCause() instanceof ExecutionException) && (ex.getCause().getCause() instanceof TestException))) { + ex.printStackTrace(); + Assert.fail("Wrong exception received"); + } + } finally { + exec.shutdown(); + } + } + + @Test + public void fromRunnableNormal() { + final AtomicInteger calls = new AtomicInteger(); + + Completable c = Completable.fromRunnable(new Runnable() { + @Override + public void run() { + calls.getAndIncrement(); + } + }); + + c.blockingAwait(); + + Assert.assertEquals(1, calls.get()); + } + + @Test(expected = TestException.class) + public void fromRunnableThrows() { + Completable c = Completable.fromRunnable(new Runnable() { + @Override + public void run() { throw new TestException(); } + }); + + c.blockingAwait(); + } + + @Test + public void doOnEventComplete() { + final AtomicInteger atomicInteger = new AtomicInteger(0); + + Completable.complete().doOnEvent(new Consumer<Throwable>() { + @Override + public void accept(final Throwable throwable) throws Exception { + if (throwable == null) { + atomicInteger.incrementAndGet(); + } + } + }).subscribe(); + + assertEquals(1, atomicInteger.get()); + } + + @Test + public void doOnEventError() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + final AtomicInteger atomicInteger = new AtomicInteger(0); + + Completable.error(new RuntimeException()).doOnEvent(new Consumer<Throwable>() { + @Override + public void accept(final Throwable throwable) throws Exception { + if (throwable != null) { + atomicInteger.incrementAndGet(); + } + } + }).subscribe(); + + assertEquals(1, atomicInteger.get()); + + TestHelper.assertError(errors, 0, OnErrorNotImplementedException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void subscribeTwoCallbacksDispose() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + Disposable d = pp.ignoreElements().subscribe(Functions.EMPTY_ACTION, Functions.emptyConsumer()); + + assertFalse(d.isDisposed()); + assertTrue(pp.hasSubscribers()); + + d.dispose(); + + assertTrue(d.isDisposed()); + assertFalse(pp.hasSubscribers()); + } + +} diff --git a/src/test/java/io/reactivex/rxjava3/completable/CompletableTimerTest.java b/src/test/java/io/reactivex/rxjava3/completable/CompletableTimerTest.java new file mode 100644 index 0000000000..cad1780eb6 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/completable/CompletableTimerTest.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.completable; + +import static org.junit.Assert.assertEquals; + +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.functions.Action; +import io.reactivex.rxjava3.schedulers.TestScheduler; + +public class CompletableTimerTest extends RxJavaTest { + @Test + public void timer() { + final TestScheduler testScheduler = new TestScheduler(); + + final AtomicLong atomicLong = new AtomicLong(); + Completable.timer(2, TimeUnit.SECONDS, testScheduler).subscribe(new Action() { + @Override + public void run() throws Exception { + atomicLong.incrementAndGet(); + } + }); + + assertEquals(0, atomicLong.get()); + + testScheduler.advanceTimeBy(1, TimeUnit.SECONDS); + + assertEquals(0, atomicLong.get()); + + testScheduler.advanceTimeBy(1, TimeUnit.SECONDS); + + assertEquals(1, atomicLong.get()); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/core/BackpressureEnumTest.java b/src/test/java/io/reactivex/rxjava3/core/BackpressureEnumTest.java new file mode 100644 index 0000000000..e007bfe8ef --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/core/BackpressureEnumTest.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.core; + +import static org.junit.Assert.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.annotations.BackpressureKind; + +public class BackpressureEnumTest extends RxJavaTest { + + @Test + public void backpressureOverflowStrategy() { + assertEquals(3, BackpressureOverflowStrategy.values().length); + + assertNotNull(BackpressureOverflowStrategy.valueOf("ERROR")); + } + + @Test + public void backpressureStrategy() { + assertEquals(5, BackpressureStrategy.values().length); + + assertNotNull(BackpressureStrategy.valueOf("BUFFER")); + } + + @Test + public void backpressureKind() { + assertEquals(6, BackpressureKind.values().length); + + assertNotNull(BackpressureKind.valueOf("FULL")); + } + +} diff --git a/src/test/java/io/reactivex/rxjava3/core/ConverterTest.java b/src/test/java/io/reactivex/rxjava3/core/ConverterTest.java new file mode 100644 index 0000000000..1d0ddd5e40 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/core/ConverterTest.java @@ -0,0 +1,278 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.core; + +import static org.junit.Assert.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.parallel.*; + +public final class ConverterTest extends RxJavaTest { + + @Test + public void flowableConverterThrows() { + try { + Flowable.just(1).to(new FlowableConverter<Integer, Integer>() { + @Override + public Integer apply(Flowable<Integer> v) { + throw new TestException("Forced failure"); + } + }); + fail("Should have thrown!"); + } catch (TestException ex) { + assertEquals("Forced failure", ex.getMessage()); + } + } + + @Test + public void observableConverterThrows() { + try { + Observable.just(1).to(new ObservableConverter<Integer, Integer>() { + @Override + public Integer apply(Observable<Integer> v) { + throw new TestException("Forced failure"); + } + }); + fail("Should have thrown!"); + } catch (TestException ex) { + assertEquals("Forced failure", ex.getMessage()); + } + } + + @Test + public void singleConverterThrows() { + try { + Single.just(1).to(new SingleConverter<Integer, Integer>() { + @Override + public Integer apply(Single<Integer> v) { + throw new TestException("Forced failure"); + } + }); + fail("Should have thrown!"); + } catch (TestException ex) { + assertEquals("Forced failure", ex.getMessage()); + } + } + + @Test + public void maybeConverterThrows() { + try { + Maybe.just(1).to(new MaybeConverter<Integer, Integer>() { + @Override + public Integer apply(Maybe<Integer> v) { + throw new TestException("Forced failure"); + } + }); + fail("Should have thrown!"); + } catch (TestException ex) { + assertEquals("Forced failure", ex.getMessage()); + } + } + + @Test + public void completableConverterThrows() { + try { + Completable.complete().to(new CompletableConverter<Completable>() { + @Override + public Completable apply(Completable v) { + throw new TestException("Forced failure"); + } + }); + fail("Should have thrown!"); + } catch (TestException ex) { + assertEquals("Forced failure", ex.getMessage()); + } + } + + // Test demos for signature generics in compose() methods. Just needs to compile. + + @SuppressWarnings({ "rawtypes", "unchecked" }) + @Test + public void observableGenericsSignatureTest() { + A<String, Integer> a = new A<String, Integer>() { }; + + Observable.just(a).to((ObservableConverter)ConverterTest.testObservableConverterCreator()); + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + @Test + public void singleGenericsSignatureTest() { + A<String, Integer> a = new A<String, Integer>() { }; + + Single.just(a).to((SingleConverter)ConverterTest.<String>testSingleConverterCreator()); + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + @Test + public void maybeGenericsSignatureTest() { + A<String, Integer> a = new A<String, Integer>() { }; + + Maybe.just(a).to((MaybeConverter)ConverterTest.<String>testMaybeConverterCreator()); + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + @Test + public void flowableGenericsSignatureTest() { + A<String, Integer> a = new A<String, Integer>() { }; + + Flowable.just(a).to((FlowableConverter)ConverterTest.<String>testFlowableConverterCreator()); + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + @Test + public void parallelFlowableGenericsSignatureTest() { + A<String, Integer> a = new A<String, Integer>() { }; + + Flowable.just(a).parallel().to((ParallelFlowableConverter)ConverterTest.<String>testParallelFlowableConverterCreator()); + } + + @Test + public void compositeTest() { + CompositeConverter converter = new CompositeConverter(); + + Flowable.just(1) + .to(converter) + .test() + .assertValue(1); + + Observable.just(1) + .to(converter) + .test() + .assertValue(1); + + Maybe.just(1) + .to(converter) + .test() + .assertValue(1); + + Single.just(1) + .to(converter) + .test() + .assertValue(1); + + Completable.complete() + .to(converter) + .test() + .assertComplete(); + + Flowable.just(1) + .parallel() + .to(converter) + .test() + .assertValue(1); + } + + /** + * Two argument type. + * @param <T> the input type + * @param <R> the output type + */ + interface A<T, R> { } + + /** + * One argument type. + * @param <T> the type + */ + interface B<T> { } + + private static <T> ObservableConverter<A<T, ?>, B<T>> testObservableConverterCreator() { + return new ObservableConverter<A<T, ?>, B<T>>() { + @Override + public B<T> apply(Observable<A<T, ?>> a) { + return new B<T>() { + }; + } + }; + } + + private static <T> SingleConverter<A<T, ?>, B<T>> testSingleConverterCreator() { + return new SingleConverter<A<T, ?>, B<T>>() { + @Override + public B<T> apply(Single<A<T, ?>> a) { + return new B<T>() { + }; + } + }; + } + + private static <T> MaybeConverter<A<T, ?>, B<T>> testMaybeConverterCreator() { + return new MaybeConverter<A<T, ?>, B<T>>() { + @Override + public B<T> apply(Maybe<A<T, ?>> a) { + return new B<T>() { + }; + } + }; + } + + private static <T> FlowableConverter<A<T, ?>, B<T>> testFlowableConverterCreator() { + return new FlowableConverter<A<T, ?>, B<T>>() { + @Override + public B<T> apply(Flowable<A<T, ?>> a) { + return new B<T>() { + }; + } + }; + } + + private static <T> ParallelFlowableConverter<A<T, ?>, B<T>> testParallelFlowableConverterCreator() { + return new ParallelFlowableConverter<A<T, ?>, B<T>>() { + @Override + public B<T> apply(ParallelFlowable<A<T, ?>> a) { + return new B<T>() { + }; + } + }; + } + + static class CompositeConverter + implements ObservableConverter<Integer, Flowable<Integer>>, + ParallelFlowableConverter<Integer, Flowable<Integer>>, + FlowableConverter<Integer, Observable<Integer>>, + MaybeConverter<Integer, Flowable<Integer>>, + SingleConverter<Integer, Flowable<Integer>>, + CompletableConverter<Flowable<Integer>> { + @Override + public Flowable<Integer> apply(ParallelFlowable<Integer> upstream) { + return upstream.sequential(); + } + + @Override + public Flowable<Integer> apply(Completable upstream) { + return upstream.toFlowable(); + } + + @Override + public Observable<Integer> apply(Flowable<Integer> upstream) { + return upstream.toObservable(); + } + + @Override + public Flowable<Integer> apply(Maybe<Integer> upstream) { + return upstream.toFlowable(); + } + + @Override + public Flowable<Integer> apply(Observable<Integer> upstream) { + return upstream.toFlowable(BackpressureStrategy.MISSING); + } + + @Override + public Flowable<Integer> apply(Single<Integer> upstream) { + return upstream.toFlowable(); + } + } +} diff --git a/src/test/java/io/reactivex/rxjava3/core/DisposeTaskTest.java b/src/test/java/io/reactivex/rxjava3/core/DisposeTaskTest.java new file mode 100644 index 0000000000..d429b588f7 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/core/DisposeTaskTest.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.core; + +import static org.junit.Assert.fail; +import static org.testng.Assert.assertTrue; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.Scheduler.DisposeTask; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.schedulers.Schedulers; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class DisposeTaskTest extends RxJavaTest { + + @Test + public void runnableThrows() throws Throwable { + TestHelper.withErrorTracking(errors -> { + + Scheduler.Worker worker = Schedulers.single().createWorker(); + + DisposeTask task = new DisposeTask(() -> { + throw new TestException(); + }, worker); + + try { + task.run(); + fail("Should have thrown!"); + } catch (TestException expected) { + // expected + } + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + + assertTrue(worker.isDisposed()); + }); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/core/NotificationTest.java b/src/test/java/io/reactivex/rxjava3/core/NotificationTest.java new file mode 100644 index 0000000000..b26262c15c --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/core/NotificationTest.java @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.core; + +import static org.junit.Assert.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.exceptions.TestException; + +public class NotificationTest extends RxJavaTest { + + @Test + public void valueOfOnErrorIsNull() { + Notification<Integer> notification = Notification.createOnError(new TestException()); + + assertNull(notification.getValue()); + assertTrue(notification.getError().toString(), notification.getError() instanceof TestException); + } + + @Test + public void valueOfOnCompleteIsNull() { + Notification<Integer> notification = Notification.createOnComplete(); + + assertNull(notification.getValue()); + assertNull(notification.getError()); + assertTrue(notification.isOnComplete()); + } + + @Test + public void notEqualsToObject() { + Notification<Integer> n1 = Notification.createOnNext(0); + assertNotEquals(0, n1); + assertNotEquals(n1, 0); + Notification<Integer> n2 = Notification.createOnError(new TestException()); + assertNotEquals(0, n2); + assertNotEquals(n2, 0); + Notification<Integer> n3 = Notification.createOnComplete(); + assertNotEquals(0, n3); + assertNotEquals(n3, 0); + } + + @Test + public void twoEqual() { + Notification<Integer> n1 = Notification.createOnNext(0); + Notification<Integer> n2 = Notification.createOnNext(0); + assertEquals(n1, n2); + assertEquals(n2, n1); + } + + @Test + public void hashCodeIsTheInner() { + Notification<Integer> n1 = Notification.createOnNext(1337); + + assertEquals(Integer.valueOf(1337).hashCode(), n1.hashCode()); + + assertEquals(0, Notification.createOnComplete().hashCode()); + } + + @Test + public void toStringPattern() { + assertEquals("OnNextNotification[1]", Notification.createOnNext(1).toString()); + assertEquals("OnErrorNotification[io.reactivex.rxjava3.exceptions.TestException]", Notification.createOnError(new TestException()).toString()); + assertEquals("OnCompleteNotification", Notification.createOnComplete().toString()); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/core/PeriodicDirectTaskTest.java b/src/test/java/io/reactivex/rxjava3/core/PeriodicDirectTaskTest.java new file mode 100644 index 0000000000..077171acdb --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/core/PeriodicDirectTaskTest.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.core; + +import static org.junit.Assert.fail; +import static org.testng.Assert.assertTrue; + +import java.util.List; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.Scheduler.PeriodicDirectTask; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.schedulers.Schedulers; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class PeriodicDirectTaskTest extends RxJavaTest { + + @Test + public void runnableThrows() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + Scheduler.Worker worker = Schedulers.single().createWorker(); + + PeriodicDirectTask task = new PeriodicDirectTask(() -> { + throw new TestException(); + }, worker); + + try { + task.run(); + fail("Should have thrown!"); + } catch (TestException expected) { + // expected + } + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + + assertTrue(worker.isDisposed()); + + task.run(); + } finally { + RxJavaPlugins.reset(); + } + } +} diff --git a/src/test/java/io/reactivex/rxjava3/core/Retry.java b/src/test/java/io/reactivex/rxjava3/core/Retry.java new file mode 100644 index 0000000000..5d543afb1c --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/core/Retry.java @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.core; + +import org.junit.rules.TestRule; +import org.junit.runner.Description; +import org.junit.runners.model.Statement; + +/** + * Test rule to retry flaky tests. + * <a href="http://stackoverflow.com/a/8301639/61158">From Stackoverflow</a>. + */ +public class Retry implements TestRule { + + final class RetryStatement extends Statement { + private final Statement base; + private final Description description; + + RetryStatement(Statement base, Description description) { + this.base = base; + this.description = description; + } + + @Override + public void evaluate() throws Throwable { + Throwable caughtThrowable = null; + + for (int i = 0; i < retryCount; i++) { + try { + base.evaluate(); + return; + } catch (Throwable t) { + caughtThrowable = t; + System.err.println(description.getDisplayName() + ": run " + (i + 1) + " failed"); + int n = sleep; + if (backoff && i != 0) { + n = n * (2 << i); + } + Thread.sleep(n); + } + } + System.err.println(description.getDisplayName() + ": giving up after " + retryCount + " failures"); + throw caughtThrowable; + } + } + + final int retryCount; + + final int sleep; + + final boolean backoff; + + public Retry(int retryCount, int sleep, boolean backoff) { + this.retryCount = retryCount; + this.sleep = sleep; + this.backoff = backoff; + } + + @Override + public Statement apply(Statement base, Description description) { + return statement(base, description); + } + + private Statement statement(final Statement base, final Description description) { + return new RetryStatement(base, description); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/core/RxJavaTest.java b/src/test/java/io/reactivex/rxjava3/core/RxJavaTest.java new file mode 100644 index 0000000000..1e6d9fd33d --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/core/RxJavaTest.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.core; + +import java.util.concurrent.TimeUnit; + +import org.junit.*; +import org.junit.rules.Timeout; + +import io.reactivex.rxjava3.testsupport.SuppressUndeliverableRule; + +public abstract class RxJavaTest { + @Rule + public Timeout globalTimeout = new Timeout(5, TimeUnit.MINUTES); + @Rule + public final SuppressUndeliverableRule suppressUndeliverableRule = new SuppressUndeliverableRule(); + + /** + * Announce creates a log print preventing Travis CI from killing the build. + */ + @Test + @Ignore + public final void announce() { + } +} diff --git a/src/test/java/io/reactivex/rxjava3/core/SchedulerTest.java b/src/test/java/io/reactivex/rxjava3/core/SchedulerTest.java new file mode 100644 index 0000000000..bbb36f1594 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/core/SchedulerTest.java @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.core; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import org.junit.After; +import org.junit.Test; + +import java.util.concurrent.TimeUnit; + +public class SchedulerTest { + private static final String DRIFT_USE_NANOTIME = "rx3.scheduler.use-nanotime"; + + @After + public void cleanup() { + // reset value to default in order to not influence other tests + Scheduler.IS_DRIFT_USE_NANOTIME = false; + } + + @Test + public void driftUseNanoTimeNotSetByDefault() { + assertFalse(Scheduler.IS_DRIFT_USE_NANOTIME); + assertFalse(Boolean.getBoolean(DRIFT_USE_NANOTIME)); + } + + @Test + public void computeNow_currentTimeMillis() { + TimeUnit unit = TimeUnit.MILLISECONDS; + assertTrue(isInRange(System.currentTimeMillis(), Scheduler.computeNow(unit), unit, 250, TimeUnit.MILLISECONDS)); + } + + @Test + public void computeNow_nanoTime() { + TimeUnit unit = TimeUnit.NANOSECONDS; + Scheduler.IS_DRIFT_USE_NANOTIME = true; + + assertFalse(isInRange(System.currentTimeMillis(), Scheduler.computeNow(unit), unit, 250, TimeUnit.MILLISECONDS)); + assertTrue(isInRange(System.nanoTime(), Scheduler.computeNow(unit), TimeUnit.NANOSECONDS, 250, TimeUnit.MILLISECONDS)); + } + + private boolean isInRange(long start, long stop, TimeUnit source, long maxDiff, TimeUnit diffUnit) { + long diff = Math.abs(stop - start); + return diffUnit.convert(diff, source) <= maxDiff; + } + + @Test + public void clockDriftCalculation() { + assertEquals(100_000_000L, Scheduler.computeClockDrift(100, "milliseconds")); + + assertEquals(2_000_000_000L, Scheduler.computeClockDrift(2, "seconds")); + + assertEquals(180_000_000_000L, Scheduler.computeClockDrift(3, "minutes")); + + assertEquals(240_000_000_000L, Scheduler.computeClockDrift(4, "random")); + + assertEquals(300_000_000_000L, Scheduler.computeClockDrift(5, null)); + } + +} diff --git a/src/test/java/io/reactivex/rxjava3/core/TransformerTest.java b/src/test/java/io/reactivex/rxjava3/core/TransformerTest.java new file mode 100644 index 0000000000..c672196b60 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/core/TransformerTest.java @@ -0,0 +1,166 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.core; + +import static org.junit.Assert.*; + +import org.junit.Test; +import org.reactivestreams.Publisher; + +import io.reactivex.rxjava3.core.ConverterTest.*; +import io.reactivex.rxjava3.exceptions.TestException; + +public class TransformerTest extends RxJavaTest { + + @Test + public void flowableTransformerThrows() { + try { + Flowable.just(1).compose(new FlowableTransformer<Integer, Integer>() { + @Override + public Publisher<Integer> apply(Flowable<Integer> v) { + throw new TestException("Forced failure"); + } + }); + fail("Should have thrown!"); + } catch (TestException ex) { + assertEquals("Forced failure", ex.getMessage()); + } + } + + @Test + public void observableTransformerThrows() { + try { + Observable.just(1).compose(new ObservableTransformer<Integer, Integer>() { + @Override + public Observable<Integer> apply(Observable<Integer> v) { + throw new TestException("Forced failure"); + } + }); + fail("Should have thrown!"); + } catch (TestException ex) { + assertEquals("Forced failure", ex.getMessage()); + } + } + + @Test + public void singleTransformerThrows() { + try { + Single.just(1).compose(new SingleTransformer<Integer, Integer>() { + @Override + public Single<Integer> apply(Single<Integer> v) { + throw new TestException("Forced failure"); + } + }); + fail("Should have thrown!"); + } catch (TestException ex) { + assertEquals("Forced failure", ex.getMessage()); + } + } + + @Test + public void maybeTransformerThrows() { + try { + Maybe.just(1).compose(new MaybeTransformer<Integer, Integer>() { + @Override + public Maybe<Integer> apply(Maybe<Integer> v) { + throw new TestException("Forced failure"); + } + }); + fail("Should have thrown!"); + } catch (TestException ex) { + assertEquals("Forced failure", ex.getMessage()); + } + } + + @Test + public void completableTransformerThrows() { + try { + Completable.complete().compose(new CompletableTransformer() { + @Override + public Completable apply(Completable v) { + throw new TestException("Forced failure"); + } + }); + fail("Should have thrown!"); + } catch (TestException ex) { + assertEquals("Forced failure", ex.getMessage()); + } + } + + // Test demos for signature generics in compose() methods. Just needs to compile. + + @Test + public void observableGenericsSignatureTest() { + A<String, Integer> a = new A<String, Integer>() { }; + + Observable.just(a).compose(TransformerTest.<String>testObservableTransformerCreator()); + } + + @Test + public void singleGenericsSignatureTest() { + A<String, Integer> a = new A<String, Integer>() { }; + + Single.just(a).compose(TransformerTest.<String>testSingleTransformerCreator()); + } + + @Test + public void maybeGenericsSignatureTest() { + A<String, Integer> a = new A<String, Integer>() { }; + + Maybe.just(a).compose(TransformerTest.<String>testMaybeTransformerCreator()); + } + + @Test + public void flowableGenericsSignatureTest() { + A<String, Integer> a = new A<String, Integer>() { }; + + Flowable.just(a).compose(TransformerTest.<String>testFlowableTransformerCreator()); + } + + private static <T> ObservableTransformer<A<T, ?>, B<T>> testObservableTransformerCreator() { + return new ObservableTransformer<A<T, ?>, B<T>>() { + @Override + public ObservableSource<B<T>> apply(Observable<A<T, ?>> a) { + return Observable.empty(); + } + }; + } + + private static <T> SingleTransformer<A<T, ?>, B<T>> testSingleTransformerCreator() { + return new SingleTransformer<A<T, ?>, B<T>>() { + @Override + public SingleSource<B<T>> apply(Single<A<T, ?>> a) { + return Single.never(); + } + }; + } + + private static <T> MaybeTransformer<A<T, ?>, B<T>> testMaybeTransformerCreator() { + return new MaybeTransformer<A<T, ?>, B<T>>() { + @Override + public MaybeSource<B<T>> apply(Maybe<A<T, ?>> a) { + return Maybe.empty(); + } + }; + } + + private static <T> FlowableTransformer<A<T, ?>, B<T>> testFlowableTransformerCreator() { + return new FlowableTransformer<A<T, ?>, B<T>>() { + @Override + public Publisher<B<T>> apply(Flowable<A<T, ?>> a) { + return Flowable.empty(); + } + }; + } +} diff --git a/src/test/java/io/reactivex/rxjava3/core/XFlatMapTest.java b/src/test/java/io/reactivex/rxjava3/core/XFlatMapTest.java new file mode 100644 index 0000000000..16ebfcb9a3 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/core/XFlatMapTest.java @@ -0,0 +1,1070 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.core; + +import static org.junit.Assert.assertTrue; + +import java.util.List; +import java.util.concurrent.CyclicBarrier; + +import org.junit.*; +import org.reactivestreams.Publisher; + +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.schedulers.Schedulers; +import io.reactivex.rxjava3.subscribers.TestSubscriber; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class XFlatMapTest extends RxJavaTest { + + @Rule + public Retry retry = new Retry(5, 1000, true); + + static final int SLEEP_AFTER_CANCEL = 500; + + final CyclicBarrier cb = new CyclicBarrier(2); + + void sleep() throws Exception { + cb.await(); + try { + long before = System.currentTimeMillis(); + Thread.sleep(5000); + throw new IllegalStateException("Was not interrupted in time?! " + (System.currentTimeMillis() - before)); + } catch (InterruptedException ex) { + // ignored here + } + } + + void beforeCancelSleep(TestSubscriber<?> ts) throws Exception { + long before = System.currentTimeMillis(); + Thread.sleep(50); + if (System.currentTimeMillis() - before > 100) { + ts.cancel(); + throw new IllegalStateException("Overslept?" + (System.currentTimeMillis() - before)); + } + } + + void beforeCancelSleep(TestObserver<?> to) throws Exception { + long before = System.currentTimeMillis(); + Thread.sleep(50); + if (System.currentTimeMillis() - before > 100) { + to.dispose(); + throw new IllegalStateException("Overslept?" + (System.currentTimeMillis() - before)); + } + } + + @Test + public void flowableFlowable() throws Exception { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + TestSubscriber<Integer> ts = Flowable.just(1) + .subscribeOn(Schedulers.io()) + .flatMap(new Function<Integer, Publisher<Integer>>() { + @Override + public Publisher<Integer> apply(Integer v) throws Exception { + sleep(); + return Flowable.<Integer>error(new TestException()); + } + }) + .test(); + + cb.await(); + + beforeCancelSleep(ts); + + ts.cancel(); + + Thread.sleep(SLEEP_AFTER_CANCEL); + + ts.assertEmpty(); + + assertTrue(errors.toString(), errors.isEmpty()); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void flowableSingle() throws Exception { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + TestSubscriber<Integer> ts = Flowable.just(1) + .subscribeOn(Schedulers.io()) + .flatMapSingle(new Function<Integer, Single<Integer>>() { + @Override + public Single<Integer> apply(Integer v) throws Exception { + sleep(); + return Single.<Integer>error(new TestException()); + } + }) + .test(); + + cb.await(); + + beforeCancelSleep(ts); + + ts.cancel(); + + Thread.sleep(SLEEP_AFTER_CANCEL); + + ts.assertEmpty(); + + assertTrue(errors.toString(), errors.isEmpty()); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void flowableMaybe() throws Exception { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + TestSubscriber<Integer> ts = Flowable.just(1) + .subscribeOn(Schedulers.io()) + .flatMapMaybe(new Function<Integer, Maybe<Integer>>() { + @Override + public Maybe<Integer> apply(Integer v) throws Exception { + sleep(); + return Maybe.<Integer>error(new TestException()); + } + }) + .test(); + + cb.await(); + + beforeCancelSleep(ts); + + ts.cancel(); + + Thread.sleep(SLEEP_AFTER_CANCEL); + + ts.assertEmpty(); + + assertTrue(errors.toString(), errors.isEmpty()); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void flowableCompletable() throws Exception { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + TestObserver<Void> to = Flowable.just(1) + .subscribeOn(Schedulers.io()) + .flatMapCompletable(new Function<Integer, Completable>() { + @Override + public Completable apply(Integer v) throws Exception { + sleep(); + return Completable.error(new TestException()); + } + }) + .test(); + + cb.await(); + + beforeCancelSleep(to); + + to.dispose(); + + Thread.sleep(SLEEP_AFTER_CANCEL); + + to.assertEmpty(); + + assertTrue(errors.toString(), errors.isEmpty()); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void flowableCompletable2() throws Exception { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + TestSubscriber<Void> ts = Flowable.just(1) + .subscribeOn(Schedulers.io()) + .flatMapCompletable(new Function<Integer, Completable>() { + @Override + public Completable apply(Integer v) throws Exception { + sleep(); + return Completable.error(new TestException()); + } + }) + .<Void>toFlowable() + .test(); + + cb.await(); + + beforeCancelSleep(ts); + + ts.cancel(); + + Thread.sleep(SLEEP_AFTER_CANCEL); + + ts.assertEmpty(); + + assertTrue(errors.toString(), errors.isEmpty()); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void observableObservable() throws Exception { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + TestObserver<Integer> to = Observable.just(1) + .subscribeOn(Schedulers.io()) + .flatMap(new Function<Integer, Observable<Integer>>() { + @Override + public Observable<Integer> apply(Integer v) throws Exception { + sleep(); + return Observable.<Integer>error(new TestException()); + } + }) + .test(); + + cb.await(); + + beforeCancelSleep(to); + + to.dispose(); + + Thread.sleep(SLEEP_AFTER_CANCEL); + + to.assertEmpty(); + + assertTrue(errors.toString(), errors.isEmpty()); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void observerSingle() throws Exception { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + TestObserver<Integer> to = Observable.just(1) + .subscribeOn(Schedulers.io()) + .flatMapSingle(new Function<Integer, Single<Integer>>() { + @Override + public Single<Integer> apply(Integer v) throws Exception { + sleep(); + return Single.<Integer>error(new TestException()); + } + }) + .test(); + + cb.await(); + + beforeCancelSleep(to); + + to.dispose(); + + Thread.sleep(SLEEP_AFTER_CANCEL); + + to.assertEmpty(); + + assertTrue(errors.toString(), errors.isEmpty()); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void observerMaybe() throws Exception { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + TestObserver<Integer> to = Observable.just(1) + .subscribeOn(Schedulers.io()) + .flatMapMaybe(new Function<Integer, Maybe<Integer>>() { + @Override + public Maybe<Integer> apply(Integer v) throws Exception { + sleep(); + return Maybe.<Integer>error(new TestException()); + } + }) + .test(); + + cb.await(); + + beforeCancelSleep(to); + + to.dispose(); + + Thread.sleep(SLEEP_AFTER_CANCEL); + + to.assertEmpty(); + + assertTrue(errors.toString(), errors.isEmpty()); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void observerCompletable() throws Exception { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + TestObserver<Void> to = Observable.just(1) + .subscribeOn(Schedulers.io()) + .flatMapCompletable(new Function<Integer, Completable>() { + @Override + public Completable apply(Integer v) throws Exception { + sleep(); + return Completable.error(new TestException()); + } + }) + .test(); + + cb.await(); + + beforeCancelSleep(to); + + to.dispose(); + + Thread.sleep(SLEEP_AFTER_CANCEL); + + to.assertEmpty(); + + assertTrue(errors.toString(), errors.isEmpty()); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void observerCompletable2() throws Exception { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + TestObserver<Void> to = Observable.just(1) + .subscribeOn(Schedulers.io()) + .flatMapCompletable(new Function<Integer, Completable>() { + @Override + public Completable apply(Integer v) throws Exception { + sleep(); + return Completable.error(new TestException()); + } + }) + .<Void>toObservable() + .test(); + + cb.await(); + + beforeCancelSleep(to); + + to.dispose(); + + Thread.sleep(SLEEP_AFTER_CANCEL); + + to.assertEmpty(); + + assertTrue(errors.toString(), errors.isEmpty()); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void singleSingle() throws Exception { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + TestObserver<Integer> to = Single.just(1) + .subscribeOn(Schedulers.io()) + .flatMap(new Function<Integer, Single<Integer>>() { + @Override + public Single<Integer> apply(Integer v) throws Exception { + sleep(); + return Single.<Integer>error(new TestException()); + } + }) + .test(); + + cb.await(); + + beforeCancelSleep(to); + + to.dispose(); + + Thread.sleep(SLEEP_AFTER_CANCEL); + + to.assertEmpty(); + + assertTrue(errors.toString(), errors.isEmpty()); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void singleMaybe() throws Exception { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + TestObserver<Integer> to = Single.just(1) + .subscribeOn(Schedulers.io()) + .flatMapMaybe(new Function<Integer, Maybe<Integer>>() { + @Override + public Maybe<Integer> apply(Integer v) throws Exception { + sleep(); + return Maybe.<Integer>error(new TestException()); + } + }) + .test(); + + cb.await(); + + beforeCancelSleep(to); + + to.dispose(); + + Thread.sleep(SLEEP_AFTER_CANCEL); + + to.assertEmpty(); + + assertTrue(errors.toString(), errors.isEmpty()); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void singleCompletable() throws Exception { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + TestObserver<Void> to = Single.just(1) + .subscribeOn(Schedulers.io()) + .flatMapCompletable(new Function<Integer, Completable>() { + @Override + public Completable apply(Integer v) throws Exception { + sleep(); + return Completable.error(new TestException()); + } + }) + .test(); + + cb.await(); + + beforeCancelSleep(to); + + to.dispose(); + + Thread.sleep(SLEEP_AFTER_CANCEL); + + to.assertEmpty(); + + assertTrue(errors.toString(), errors.isEmpty()); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void singleCompletable2() throws Exception { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + TestObserver<Integer> to = Single.just(1) + .subscribeOn(Schedulers.io()) + .flatMapCompletable(new Function<Integer, Completable>() { + @Override + public Completable apply(Integer v) throws Exception { + sleep(); + return Completable.error(new TestException()); + } + }) + .toSingleDefault(0) + .test(); + + cb.await(); + + beforeCancelSleep(to); + + to.dispose(); + + Thread.sleep(SLEEP_AFTER_CANCEL); + + to.assertEmpty(); + + assertTrue(errors.toString(), errors.isEmpty()); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void singlePublisher() throws Exception { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + TestSubscriber<Integer> ts = Single.just(1) + .subscribeOn(Schedulers.io()) + .flatMapPublisher(new Function<Integer, Publisher<Integer>>() { + @Override + public Publisher<Integer> apply(Integer v) throws Exception { + sleep(); + return Flowable.<Integer>error(new TestException()); + } + }) + .test(); + + cb.await(); + + beforeCancelSleep(ts); + + ts.cancel(); + + Thread.sleep(SLEEP_AFTER_CANCEL); + + ts.assertEmpty(); + + assertTrue(errors.toString(), errors.isEmpty()); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void singleCombiner() throws Exception { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + TestObserver<Integer> to = Single.just(1) + .subscribeOn(Schedulers.io()) + .flatMap(new Function<Integer, Single<Integer>>() { + @Override + public Single<Integer> apply(Integer v) throws Exception { + sleep(); + return Single.<Integer>error(new TestException()); + } + }, (a, b) -> a + b) + .test(); + + cb.await(); + + beforeCancelSleep(to); + + to.dispose(); + + Thread.sleep(SLEEP_AFTER_CANCEL); + + to.assertEmpty(); + + assertTrue(errors.toString(), errors.isEmpty()); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void singleObservable() throws Exception { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + TestObserver<Integer> to = Single.just(1) + .subscribeOn(Schedulers.io()) + .flatMapObservable(new Function<Integer, Observable<Integer>>() { + @Override + public Observable<Integer> apply(Integer v) throws Exception { + sleep(); + return Observable.<Integer>error(new TestException()); + } + }) + .test(); + + cb.await(); + + beforeCancelSleep(to); + + to.dispose(); + + Thread.sleep(SLEEP_AFTER_CANCEL); + + to.assertEmpty(); + + assertTrue(errors.toString(), errors.isEmpty()); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void singleNotificationSuccess() throws Exception { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + TestObserver<Integer> to = Single.just(1) + .subscribeOn(Schedulers.io()) + .flatMap( + new Function<Integer, Single<Integer>>() { + @Override + public Single<Integer> apply(Integer v) throws Exception { + sleep(); + return Single.<Integer>error(new TestException()); + } + }, + new Function<Throwable, Single<Integer>>() { + @Override + public Single<Integer> apply(Throwable v) throws Exception { + sleep(); + return Single.<Integer>error(new TestException()); + } + } + ) + .test(); + + cb.await(); + + beforeCancelSleep(to); + + to.dispose(); + + Thread.sleep(SLEEP_AFTER_CANCEL); + + to.assertEmpty(); + + assertTrue(errors.toString(), errors.isEmpty()); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void singleNotificationError() throws Exception { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + TestObserver<Integer> to = Single.<Integer>error(new TestException()) + .subscribeOn(Schedulers.io()) + .flatMap( + new Function<Integer, Single<Integer>>() { + @Override + public Single<Integer> apply(Integer v) throws Exception { + sleep(); + return Single.<Integer>error(new TestException()); + } + }, + new Function<Throwable, Single<Integer>>() { + @Override + public Single<Integer> apply(Throwable v) throws Exception { + sleep(); + return Single.<Integer>error(new TestException()); + } + } + ) + .test(); + + cb.await(); + + beforeCancelSleep(to); + + to.dispose(); + + Thread.sleep(SLEEP_AFTER_CANCEL); + + to.assertEmpty(); + + assertTrue(errors.toString(), errors.isEmpty()); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void maybeSingle() throws Exception { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + TestObserver<Integer> to = Maybe.just(1) + .subscribeOn(Schedulers.io()) + .flatMapSingle(new Function<Integer, Single<Integer>>() { + @Override + public Single<Integer> apply(Integer v) throws Exception { + sleep(); + return Single.<Integer>error(new TestException()); + } + }) + .toSingle() + .test(); + + cb.await(); + + beforeCancelSleep(to); + + to.dispose(); + + Thread.sleep(SLEEP_AFTER_CANCEL); + + to.assertEmpty(); + + assertTrue(errors.toString(), errors.isEmpty()); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void maybeSingle2() throws Exception { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + TestObserver<Integer> to = Maybe.just(1) + .subscribeOn(Schedulers.io()) + .flatMapSingle(new Function<Integer, Single<Integer>>() { + @Override + public Single<Integer> apply(Integer v) throws Exception { + sleep(); + return Single.<Integer>error(new TestException()); + } + }) + .test(); + + cb.await(); + + beforeCancelSleep(to); + + to.dispose(); + + Thread.sleep(SLEEP_AFTER_CANCEL); + + to.assertEmpty(); + + assertTrue(errors.toString(), errors.isEmpty()); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void maybeMaybe() throws Exception { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + TestObserver<Integer> to = Maybe.just(1) + .subscribeOn(Schedulers.io()) + .flatMap(new Function<Integer, Maybe<Integer>>() { + @Override + public Maybe<Integer> apply(Integer v) throws Exception { + sleep(); + return Maybe.<Integer>error(new TestException()); + } + }) + .test(); + + cb.await(); + + beforeCancelSleep(to); + + to.dispose(); + + Thread.sleep(SLEEP_AFTER_CANCEL); + + to.assertEmpty(); + + assertTrue(errors.toString(), errors.isEmpty()); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void maybePublisher() throws Exception { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + TestSubscriber<Integer> ts = Maybe.just(1) + .subscribeOn(Schedulers.io()) + .flatMapPublisher(new Function<Integer, Publisher<Integer>>() { + @Override + public Publisher<Integer> apply(Integer v) throws Exception { + sleep(); + return Flowable.<Integer>error(new TestException()); + } + }) + .test(); + + cb.await(); + + beforeCancelSleep(ts); + + ts.cancel(); + + Thread.sleep(SLEEP_AFTER_CANCEL); + + ts.assertEmpty(); + + assertTrue(errors.toString(), errors.isEmpty()); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void maybeObservable() throws Exception { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + TestObserver<Integer> to = Maybe.just(1) + .subscribeOn(Schedulers.io()) + .flatMapObservable(new Function<Integer, Observable<Integer>>() { + @Override + public Observable<Integer> apply(Integer v) throws Exception { + sleep(); + return Observable.<Integer>error(new TestException()); + } + }) + .test(); + + cb.await(); + + beforeCancelSleep(to); + + to.dispose(); + + Thread.sleep(SLEEP_AFTER_CANCEL); + + to.assertEmpty(); + + assertTrue(errors.toString(), errors.isEmpty()); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void maybeNotificationSuccess() throws Exception { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + TestObserver<Integer> to = Maybe.just(1) + .subscribeOn(Schedulers.io()) + .flatMap( + new Function<Integer, Maybe<Integer>>() { + @Override + public Maybe<Integer> apply(Integer v) throws Exception { + sleep(); + return Maybe.<Integer>error(new TestException()); + } + }, + new Function<Throwable, Maybe<Integer>>() { + @Override + public Maybe<Integer> apply(Throwable v) throws Exception { + sleep(); + return Maybe.<Integer>error(new TestException()); + } + }, + new Supplier<Maybe<Integer>>() { + @Override + public Maybe<Integer> get() throws Exception { + sleep(); + return Maybe.<Integer>error(new TestException()); + } + } + ) + .test(); + + cb.await(); + + beforeCancelSleep(to); + + to.dispose(); + + Thread.sleep(SLEEP_AFTER_CANCEL); + + to.assertEmpty(); + + assertTrue(errors.toString(), errors.isEmpty()); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void maybeNotificationError() throws Exception { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + TestObserver<Integer> to = Maybe.<Integer>error(new TestException()) + .subscribeOn(Schedulers.io()) + .flatMap( + new Function<Integer, Maybe<Integer>>() { + @Override + public Maybe<Integer> apply(Integer v) throws Exception { + sleep(); + return Maybe.<Integer>error(new TestException()); + } + }, + new Function<Throwable, Maybe<Integer>>() { + @Override + public Maybe<Integer> apply(Throwable v) throws Exception { + sleep(); + return Maybe.<Integer>error(new TestException()); + } + }, + new Supplier<Maybe<Integer>>() { + @Override + public Maybe<Integer> get() throws Exception { + sleep(); + return Maybe.<Integer>error(new TestException()); + } + } + ) + .test(); + + cb.await(); + + beforeCancelSleep(to); + + to.dispose(); + + Thread.sleep(SLEEP_AFTER_CANCEL); + + to.assertEmpty(); + + assertTrue(errors.toString(), errors.isEmpty()); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void maybeNotificationEmpty() throws Exception { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + TestObserver<Integer> to = Maybe.<Integer>empty() + .subscribeOn(Schedulers.io()) + .flatMap( + new Function<Integer, Maybe<Integer>>() { + @Override + public Maybe<Integer> apply(Integer v) throws Exception { + sleep(); + return Maybe.<Integer>error(new TestException()); + } + }, + new Function<Throwable, Maybe<Integer>>() { + @Override + public Maybe<Integer> apply(Throwable v) throws Exception { + sleep(); + return Maybe.<Integer>error(new TestException()); + } + }, + new Supplier<Maybe<Integer>>() { + @Override + public Maybe<Integer> get() throws Exception { + sleep(); + return Maybe.<Integer>error(new TestException()); + } + } + ) + .test(); + + cb.await(); + + beforeCancelSleep(to); + + to.dispose(); + + Thread.sleep(SLEEP_AFTER_CANCEL); + + to.assertEmpty(); + + assertTrue(errors.toString(), errors.isEmpty()); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void maybeCombiner() throws Exception { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + TestObserver<Integer> to = Maybe.just(1) + .subscribeOn(Schedulers.io()) + .flatMap(new Function<Integer, Maybe<Integer>>() { + @Override + public Maybe<Integer> apply(Integer v) throws Exception { + sleep(); + return Maybe.<Integer>error(new TestException()); + } + }, (a, b) -> a + b) + .test(); + + cb.await(); + + beforeCancelSleep(to); + + to.dispose(); + + Thread.sleep(SLEEP_AFTER_CANCEL); + + to.assertEmpty(); + + assertTrue(errors.toString(), errors.isEmpty()); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void maybeCompletable() throws Exception { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + TestObserver<Void> to = Maybe.just(1) + .subscribeOn(Schedulers.io()) + .flatMapCompletable(new Function<Integer, Completable>() { + @Override + public Completable apply(Integer v) throws Exception { + sleep(); + return Completable.error(new TestException()); + } + }) + .test(); + + cb.await(); + + beforeCancelSleep(to); + + to.dispose(); + + Thread.sleep(SLEEP_AFTER_CANCEL); + + to.assertEmpty(); + + assertTrue(errors.toString(), errors.isEmpty()); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void maybeCompletable2() throws Exception { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + TestObserver<Void> to = Maybe.just(1) + .subscribeOn(Schedulers.io()) + .flatMapCompletable(new Function<Integer, Completable>() { + @Override + public Completable apply(Integer v) throws Exception { + sleep(); + return Completable.error(new TestException()); + } + }) + .<Void>toMaybe() + .test(); + + cb.await(); + + beforeCancelSleep(to); + + to.dispose(); + + Thread.sleep(SLEEP_AFTER_CANCEL); + + to.assertEmpty(); + + assertTrue(errors.toString(), errors.isEmpty()); + } finally { + RxJavaPlugins.reset(); + } + } +} diff --git a/src/test/java/io/reactivex/rxjava3/disposables/CompositeDisposableTest.java b/src/test/java/io/reactivex/rxjava3/disposables/CompositeDisposableTest.java new file mode 100644 index 0000000000..3f76d283d7 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/disposables/CompositeDisposableTest.java @@ -0,0 +1,823 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.disposables; + +import static org.junit.Assert.*; + +import java.io.IOException; +import java.util.*; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.RxJavaTest; +import io.reactivex.rxjava3.exceptions.CompositeException; +import io.reactivex.rxjava3.functions.Action; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class CompositeDisposableTest extends RxJavaTest { + + @Test + public void success() { + final AtomicInteger counter = new AtomicInteger(); + CompositeDisposable cd = new CompositeDisposable(); + cd.add(Disposable.fromRunnable(new Runnable() { + + @Override + public void run() { + counter.incrementAndGet(); + } + + })); + + cd.add(Disposable.fromRunnable(new Runnable() { + + @Override + public void run() { + counter.incrementAndGet(); + } + })); + + cd.dispose(); + + assertEquals(2, counter.get()); + } + + @Test + public void shouldUnsubscribeAll() throws InterruptedException { + final AtomicInteger counter = new AtomicInteger(); + final CompositeDisposable cd = new CompositeDisposable(); + + final int count = 10; + final CountDownLatch start = new CountDownLatch(1); + for (int i = 0; i < count; i++) { + cd.add(Disposable.fromRunnable(new Runnable() { + + @Override + public void run() { + counter.incrementAndGet(); + } + })); + } + + final List<Thread> threads = new ArrayList<>(); + for (int i = 0; i < count; i++) { + final Thread t = new Thread() { + @Override + public void run() { + try { + start.await(); + cd.dispose(); + } catch (final InterruptedException e) { + fail(e.getMessage()); + } + } + }; + t.start(); + threads.add(t); + } + + start.countDown(); + for (final Thread t : threads) { + t.join(); + } + + assertEquals(count, counter.get()); + } + + @Test + public void exception() { + final AtomicInteger counter = new AtomicInteger(); + CompositeDisposable cd = new CompositeDisposable(); + cd.add(Disposable.fromRunnable(new Runnable() { + + @Override + public void run() { + throw new RuntimeException("failed on first one"); + } + + })); + + cd.add(Disposable.fromRunnable(new Runnable() { + + @Override + public void run() { + counter.incrementAndGet(); + } + + })); + + try { + cd.dispose(); + fail("Expecting an exception"); + } catch (RuntimeException e) { + // we expect this + assertEquals(e.getMessage(), "failed on first one"); + } + + // we should still have disposed to the second one + assertEquals(1, counter.get()); + } + + @Test + public void compositeException() { + final AtomicInteger counter = new AtomicInteger(); + CompositeDisposable cd = new CompositeDisposable(); + cd.add(Disposable.fromRunnable(new Runnable() { + + @Override + public void run() { + throw new RuntimeException("failed on first one"); + } + + })); + + cd.add(Disposable.fromRunnable(new Runnable() { + + @Override + public void run() { + throw new RuntimeException("failed on second one too"); + } + })); + + cd.add(Disposable.fromRunnable(new Runnable() { + + @Override + public void run() { + counter.incrementAndGet(); + } + + })); + + try { + cd.dispose(); + fail("Expecting an exception"); + } catch (CompositeException e) { + // we expect this + assertEquals(e.getExceptions().size(), 2); + } + + // we should still have disposed to the second one + assertEquals(1, counter.get()); + } + + @Test + public void removeUnsubscribes() { + Disposable d1 = Disposable.empty(); + Disposable d2 = Disposable.empty(); + + CompositeDisposable cd = new CompositeDisposable(); + cd.add(d1); + cd.add(d2); + + cd.remove(d1); + + assertTrue(d1.isDisposed()); + assertFalse(d2.isDisposed()); + } + + @Test + public void clear() { + Disposable d1 = Disposable.empty(); + Disposable d2 = Disposable.empty(); + + CompositeDisposable cd = new CompositeDisposable(); + cd.add(d1); + cd.add(d2); + + assertFalse(d1.isDisposed()); + assertFalse(d2.isDisposed()); + + cd.clear(); + + assertTrue(d1.isDisposed()); + assertTrue(d2.isDisposed()); + assertFalse(cd.isDisposed()); + + Disposable d3 = Disposable.empty(); + + cd.add(d3); + cd.dispose(); + + assertTrue(d3.isDisposed()); + assertTrue(cd.isDisposed()); + } + + @Test + public void unsubscribeIdempotence() { + final AtomicInteger counter = new AtomicInteger(); + CompositeDisposable cd = new CompositeDisposable(); + cd.add(Disposable.fromRunnable(new Runnable() { + + @Override + public void run() { + counter.incrementAndGet(); + } + + })); + + cd.dispose(); + cd.dispose(); + cd.dispose(); + + // we should have only disposed once + assertEquals(1, counter.get()); + } + + @Test + public void unsubscribeIdempotenceConcurrently() + throws InterruptedException { + final AtomicInteger counter = new AtomicInteger(); + final CompositeDisposable cd = new CompositeDisposable(); + + final int count = 10; + final CountDownLatch start = new CountDownLatch(1); + cd.add(Disposable.fromRunnable(new Runnable() { + + @Override + public void run() { + counter.incrementAndGet(); + } + + })); + + final List<Thread> threads = new ArrayList<>(); + for (int i = 0; i < count; i++) { + final Thread t = new Thread() { + @Override + public void run() { + try { + start.await(); + cd.dispose(); + } catch (final InterruptedException e) { + fail(e.getMessage()); + } + } + }; + t.start(); + threads.add(t); + } + + start.countDown(); + for (final Thread t : threads) { + t.join(); + } + + // we should have only disposed once + assertEquals(1, counter.get()); + } + + @Test + public void tryRemoveIfNotIn() { + CompositeDisposable cd = new CompositeDisposable(); + + CompositeDisposable cd1 = new CompositeDisposable(); + CompositeDisposable cd2 = new CompositeDisposable(); + + cd.add(cd1); + cd.remove(cd1); + cd.add(cd2); + + cd.remove(cd1); // try removing again + } + + @Test(expected = NullPointerException.class) + public void addingNullDisposableIllegal() { + CompositeDisposable cd = new CompositeDisposable(); + cd.add(null); + } + + @Test + public void initializeVarargs() { + Disposable d1 = Disposable.empty(); + Disposable d2 = Disposable.empty(); + + CompositeDisposable cd = new CompositeDisposable(d1, d2); + + assertEquals(2, cd.size()); + + cd.clear(); + + assertEquals(0, cd.size()); + + assertTrue(d1.isDisposed()); + assertTrue(d2.isDisposed()); + + Disposable d3 = Disposable.empty(); + Disposable d4 = Disposable.empty(); + + cd = new CompositeDisposable(d3, d4); + + cd.dispose(); + + assertTrue(d3.isDisposed()); + assertTrue(d4.isDisposed()); + + assertEquals(0, cd.size()); + } + + @Test + public void initializeIterable() { + Disposable d1 = Disposable.empty(); + Disposable d2 = Disposable.empty(); + + CompositeDisposable cd = new CompositeDisposable(Arrays.asList(d1, d2)); + + assertEquals(2, cd.size()); + + cd.clear(); + + assertEquals(0, cd.size()); + + assertTrue(d1.isDisposed()); + assertTrue(d2.isDisposed()); + + Disposable d3 = Disposable.empty(); + Disposable d4 = Disposable.empty(); + + cd = new CompositeDisposable(Arrays.asList(d3, d4)); + + assertEquals(2, cd.size()); + + cd.dispose(); + + assertTrue(d3.isDisposed()); + assertTrue(d4.isDisposed()); + + assertEquals(0, cd.size()); + } + + @Test + public void addAll() { + CompositeDisposable cd = new CompositeDisposable(); + + Disposable d1 = Disposable.empty(); + Disposable d2 = Disposable.empty(); + Disposable d3 = Disposable.empty(); + + cd.addAll(d1, d2); + cd.addAll(d3); + + assertFalse(d1.isDisposed()); + assertFalse(d2.isDisposed()); + assertFalse(d3.isDisposed()); + + cd.clear(); + + assertTrue(d1.isDisposed()); + assertTrue(d2.isDisposed()); + + d1 = Disposable.empty(); + d2 = Disposable.empty(); + + cd = new CompositeDisposable(); + + cd.addAll(d1, d2); + + assertFalse(d1.isDisposed()); + assertFalse(d2.isDisposed()); + + cd.dispose(); + + assertTrue(d1.isDisposed()); + assertTrue(d2.isDisposed()); + + assertEquals(0, cd.size()); + + cd.clear(); + + assertEquals(0, cd.size()); + } + + @Test + public void addAfterDisposed() { + CompositeDisposable cd = new CompositeDisposable(); + cd.dispose(); + + Disposable d1 = Disposable.empty(); + + assertFalse(cd.add(d1)); + + assertTrue(d1.isDisposed()); + + d1 = Disposable.empty(); + Disposable d2 = Disposable.empty(); + + assertFalse(cd.addAll(d1, d2)); + + assertTrue(d1.isDisposed()); + assertTrue(d2.isDisposed()); + + } + + @Test + public void delete() { + + CompositeDisposable cd = new CompositeDisposable(); + + Disposable d1 = Disposable.empty(); + + assertFalse(cd.delete(d1)); + + Disposable d2 = Disposable.empty(); + + cd.add(d2); + + assertFalse(cd.delete(d1)); + + cd.dispose(); + + assertFalse(cd.delete(d1)); + } + + @Test + public void disposeRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final CompositeDisposable cd = new CompositeDisposable(); + + Runnable run = new Runnable() { + @Override + public void run() { + cd.dispose(); + } + }; + + TestHelper.race(run, run); + } + } + + @Test + public void addRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final CompositeDisposable cd = new CompositeDisposable(); + + Runnable run = new Runnable() { + @Override + public void run() { + cd.add(Disposable.empty()); + } + }; + + TestHelper.race(run, run); + } + } + + @Test + public void addAllRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final CompositeDisposable cd = new CompositeDisposable(); + + Runnable run = new Runnable() { + @Override + public void run() { + cd.addAll(Disposable.empty()); + } + }; + + TestHelper.race(run, run); + } + } + + @Test + public void removeRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final CompositeDisposable cd = new CompositeDisposable(); + + final Disposable d1 = Disposable.empty(); + + cd.add(d1); + + Runnable run = new Runnable() { + @Override + public void run() { + cd.remove(d1); + } + }; + + TestHelper.race(run, run); + } + } + + @Test + public void deleteRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final CompositeDisposable cd = new CompositeDisposable(); + + final Disposable d1 = Disposable.empty(); + + cd.add(d1); + + Runnable run = new Runnable() { + @Override + public void run() { + cd.delete(d1); + } + }; + + TestHelper.race(run, run); + } + } + + @Test + public void clearRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final CompositeDisposable cd = new CompositeDisposable(); + + final Disposable d1 = Disposable.empty(); + + cd.add(d1); + + Runnable run = new Runnable() { + @Override + public void run() { + cd.clear(); + } + }; + + TestHelper.race(run, run); + } + } + + @Test + public void addDisposeRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final CompositeDisposable cd = new CompositeDisposable(); + + Runnable run = new Runnable() { + @Override + public void run() { + cd.dispose(); + } + }; + + Runnable run2 = new Runnable() { + @Override + public void run() { + cd.add(Disposable.empty()); + } + }; + + TestHelper.race(run, run2); + } + } + + @Test + public void addAllDisposeRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final CompositeDisposable cd = new CompositeDisposable(); + + Runnable run = new Runnable() { + @Override + public void run() { + cd.dispose(); + } + }; + + Runnable run2 = new Runnable() { + @Override + public void run() { + cd.addAll(Disposable.empty()); + } + }; + + TestHelper.race(run, run2); + } + } + + @Test + public void removeDisposeRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final CompositeDisposable cd = new CompositeDisposable(); + + final Disposable d1 = Disposable.empty(); + + cd.add(d1); + + Runnable run = new Runnable() { + @Override + public void run() { + cd.dispose(); + } + }; + + Runnable run2 = new Runnable() { + @Override + public void run() { + cd.remove(d1); + } + }; + + TestHelper.race(run, run2); + } + } + + @Test + public void deleteDisposeRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final CompositeDisposable cd = new CompositeDisposable(); + + final Disposable d1 = Disposable.empty(); + + cd.add(d1); + + Runnable run = new Runnable() { + @Override + public void run() { + cd.dispose(); + } + }; + + Runnable run2 = new Runnable() { + @Override + public void run() { + cd.delete(d1); + } + }; + + TestHelper.race(run, run2); + } + } + + @Test + public void clearDisposeRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final CompositeDisposable cd = new CompositeDisposable(); + + final Disposable d1 = Disposable.empty(); + + cd.add(d1); + + Runnable run = new Runnable() { + @Override + public void run() { + cd.dispose(); + } + }; + + Runnable run2 = new Runnable() { + @Override + public void run() { + cd.clear(); + } + }; + + TestHelper.race(run, run2); + } + } + + @Test + public void sizeDisposeRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final CompositeDisposable cd = new CompositeDisposable(); + + final Disposable d1 = Disposable.empty(); + + cd.add(d1); + + Runnable run = new Runnable() { + @Override + public void run() { + cd.dispose(); + } + }; + + Runnable run2 = new Runnable() { + @Override + public void run() { + cd.size(); + } + }; + + TestHelper.race(run, run2); + } + } + + @Test + public void disposeThrowsIAE() { + CompositeDisposable cd = new CompositeDisposable(); + + cd.add(Disposable.fromAction(new Action() { + @Override + public void run() throws Exception { + throw new IllegalArgumentException(); + } + })); + + Disposable d1 = Disposable.empty(); + + cd.add(d1); + + try { + cd.dispose(); + fail("Failed to throw"); + } catch (IllegalArgumentException ex) { + // expected + } + + assertTrue(d1.isDisposed()); + } + + @Test + public void disposeThrowsError() { + CompositeDisposable cd = new CompositeDisposable(); + + cd.add(Disposable.fromAction(new Action() { + @Override + public void run() throws Exception { + throw new AssertionError(); + } + })); + + Disposable d1 = Disposable.empty(); + + cd.add(d1); + + try { + cd.dispose(); + fail("Failed to throw"); + } catch (AssertionError ex) { + // expected + } + + assertTrue(d1.isDisposed()); + } + + @Test + public void disposeThrowsCheckedException() { + CompositeDisposable cd = new CompositeDisposable(); + + cd.add(Disposable.fromAction(new Action() { + @Override + public void run() throws Exception { + throw new IOException(); + } + })); + + Disposable d1 = Disposable.empty(); + + cd.add(d1); + + try { + cd.dispose(); + fail("Failed to throw"); + } catch (RuntimeException ex) { + // expected + if (!(ex.getCause() instanceof IOException)) { + fail(ex.toString() + " should have thrown RuntimeException(IOException)"); + } + } + + assertTrue(d1.isDisposed()); + } + + @SuppressWarnings("unchecked") + static <E extends Throwable> void throwSneaky() throws E { + throw (E)new IOException(); + } + + @Test + public void disposeThrowsCheckedExceptionSneaky() { + CompositeDisposable cd = new CompositeDisposable(); + + cd.add(new Disposable() { + @Override + public void dispose() { + CompositeDisposableTest.<RuntimeException>throwSneaky(); + } + + @Override + public boolean isDisposed() { + // TODO Auto-generated method stub + return false; + } + }); + + Disposable d1 = Disposable.empty(); + + cd.add(d1); + + try { + cd.dispose(); + fail("Failed to throw"); + } catch (RuntimeException ex) { + // expected + if (!(ex.getCause() instanceof IOException)) { + fail(ex.toString() + " should have thrown RuntimeException(IOException)"); + } + } + + assertTrue(d1.isDisposed()); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/disposables/DisposableTest.java b/src/test/java/io/reactivex/rxjava3/disposables/DisposableTest.java new file mode 100644 index 0000000000..e541d404da --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/disposables/DisposableTest.java @@ -0,0 +1,254 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.disposables; + +import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.*; + +import java.io.IOException; +import java.util.List; +import java.util.concurrent.atomic.*; + +import org.junit.Test; +import org.reactivestreams.Subscription; + +import io.reactivex.rxjava3.core.RxJavaTest; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.Action; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class DisposableTest extends RxJavaTest { + + @Test + public void unsubscribeOnlyOnce() { + Runnable run = mock(Runnable.class); + + Disposable d = Disposable.fromRunnable(run); + + assertTrue(d.toString(), d.toString().contains("RunnableDisposable(disposed=false, ")); + + d.dispose(); + assertTrue(d.toString(), d.toString().contains("RunnableDisposable(disposed=true, ")); + + d.dispose(); + assertTrue(d.toString(), d.toString().contains("RunnableDisposable(disposed=true, ")); + + verify(run, times(1)).run(); + } + + @Test + public void empty() { + Disposable empty = Disposable.empty(); + assertFalse(empty.isDisposed()); + empty.dispose(); + assertTrue(empty.isDisposed()); + } + + @Test + public void unsubscribed() { + Disposable disposed = Disposable.disposed(); + assertTrue(disposed.isDisposed()); + } + + @Test + public void fromAction() throws Throwable { + Action action = mock(Action.class); + + Disposable d = Disposable.fromAction(action); + + assertTrue(d.toString(), d.toString().contains("ActionDisposable(disposed=false, ")); + + d.dispose(); + assertTrue(d.toString(), d.toString().contains("ActionDisposable(disposed=true, ")); + + d.dispose(); + assertTrue(d.toString(), d.toString().contains("ActionDisposable(disposed=true, ")); + + verify(action, times(1)).run(); + } + + @Test + public void fromActionThrows() { + try { + Disposable.fromAction(new Action() { + @Override + public void run() throws Exception { + throw new IllegalArgumentException(); + } + }).dispose(); + fail("Should have thrown!"); + } catch (IllegalArgumentException ex) { + // expected + } + + try { + Disposable.fromAction(new Action() { + @Override + public void run() throws Exception { + throw new InternalError(); + } + }).dispose(); + fail("Should have thrown!"); + } catch (InternalError ex) { + // expected + } + + try { + Disposable.fromAction(new Action() { + @Override + public void run() throws Exception { + throw new IOException(); + } + }).dispose(); + fail("Should have thrown!"); + } catch (RuntimeException ex) { + if (!(ex.getCause() instanceof IOException)) { + fail(ex.toString() + ": Should have cause of IOException"); + } + // expected + } + + } + + @Test + public void disposeRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final Disposable d = Disposable.empty(); + + Runnable r = new Runnable() { + @Override + public void run() { + d.dispose(); + } + }; + + TestHelper.race(r, r); + } + } + + @Test(expected = NullPointerException.class) + public void fromSubscriptionNull() { + Disposable.fromSubscription(null); + } + + @Test + public void fromSubscription() { + Subscription s = mock(Subscription.class); + + Disposable.fromSubscription(s).dispose(); + + verify(s).cancel(); + verify(s, never()).request(anyInt()); + } + + @Test + public void setOnceTwice() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + + AtomicReference<Disposable> target = new AtomicReference<>(); + Disposable d = Disposable.empty(); + + DisposableHelper.setOnce(target, d); + + Disposable d1 = Disposable.empty(); + + DisposableHelper.setOnce(target, d1); + + assertTrue(d1.isDisposed()); + + TestHelper.assertError(errors, 0, IllegalStateException.class, "Disposable already set!"); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void fromAutoCloseable() { + AtomicInteger counter = new AtomicInteger(); + + AutoCloseable ac = () -> counter.getAndIncrement(); + + Disposable d = Disposable.fromAutoCloseable(ac); + + assertFalse(d.isDisposed()); + assertEquals(0, counter.get()); + assertTrue(d.toString(), d.toString().contains("AutoCloseableDisposable(disposed=false, ")); + + d.dispose(); + + assertTrue(d.isDisposed()); + assertEquals(1, counter.get()); + assertTrue(d.toString(), d.toString().contains("AutoCloseableDisposable(disposed=true, ")); + + d.dispose(); + + assertTrue(d.isDisposed()); + assertEquals(1, counter.get()); + assertTrue(d.toString(), d.toString().contains("AutoCloseableDisposable(disposed=true, ")); + } + + @Test + public void fromAutoCloseableThrows() throws Throwable { + TestHelper.withErrorTracking(errors -> { + AutoCloseable ac = () -> { throw new TestException(); }; + + Disposable d = Disposable.fromAutoCloseable(ac); + + assertFalse(d.isDisposed()); + + assertTrue(errors.isEmpty()); + + try { + d.dispose(); + fail("Should have thrown!"); + } catch (TestException expected) { + // expected + } + + assertTrue(d.isDisposed()); + + d.dispose(); + + assertTrue(d.isDisposed()); + + assertTrue(errors.isEmpty()); + }); + } + + @Test + public void toAutoCloseable() throws Exception { + AtomicInteger counter = new AtomicInteger(); + + Disposable d = Disposable.fromAction(() -> counter.getAndIncrement()); + + AutoCloseable ac = Disposable.toAutoCloseable(d); + + assertFalse(d.isDisposed()); + assertEquals(0, counter.get()); + + ac.close(); + + assertTrue(d.isDisposed()); + assertEquals(1, counter.get()); + + ac.close(); + + assertTrue(d.isDisposed()); + assertEquals(1, counter.get()); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/disposables/FutureDisposableTest.java b/src/test/java/io/reactivex/rxjava3/disposables/FutureDisposableTest.java new file mode 100644 index 0000000000..544e39ad1b --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/disposables/FutureDisposableTest.java @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.disposables; + +import static org.junit.Assert.*; + +import java.util.concurrent.FutureTask; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.RxJavaTest; +import io.reactivex.rxjava3.internal.functions.Functions; + +public class FutureDisposableTest extends RxJavaTest { + + @Test + public void normal() { + FutureTask<Object> ft = new FutureTask<>(Functions.EMPTY_RUNNABLE, null); + Disposable d = Disposable.fromFuture(ft); + assertFalse(d.isDisposed()); + + d.dispose(); + + assertTrue(d.isDisposed()); + + d.dispose(); + + assertTrue(d.isDisposed()); + + assertTrue(ft.isCancelled()); + } + + @Test + public void interruptible() { + FutureTask<Object> ft = new FutureTask<>(Functions.EMPTY_RUNNABLE, null); + Disposable d = Disposable.fromFuture(ft, true); + assertFalse(d.isDisposed()); + + d.dispose(); + + assertTrue(d.isDisposed()); + + d.dispose(); + + assertTrue(d.isDisposed()); + + assertTrue(ft.isCancelled()); + } + + @Test + public void normalDone() { + FutureTask<Object> ft = new FutureTask<>(Functions.EMPTY_RUNNABLE, null); + FutureDisposable d = new FutureDisposable(ft, false); + assertFalse(d.isDisposed()); + + assertFalse(d.isDisposed()); + + ft.run(); + + assertTrue(d.isDisposed()); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/disposables/SequentialDisposableTest.java b/src/test/java/io/reactivex/rxjava3/disposables/SequentialDisposableTest.java new file mode 100644 index 0000000000..3066343301 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/disposables/SequentialDisposableTest.java @@ -0,0 +1,206 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.disposables; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +import java.util.*; +import java.util.concurrent.CountDownLatch; + +import org.junit.*; +import org.junit.runner.RunWith; +import org.mockito.junit.MockitoJUnitRunner; + +import io.reactivex.rxjava3.core.RxJavaTest; +import io.reactivex.rxjava3.internal.disposables.SequentialDisposable; + +@RunWith(MockitoJUnitRunner.class) +public class SequentialDisposableTest extends RxJavaTest { + private SequentialDisposable serialDisposable; + + @Before + public void setUp() { + serialDisposable = new SequentialDisposable(); + } + + @Test + public void unsubscribingWithoutUnderlyingDoesNothing() { + serialDisposable.dispose(); + } + + @Test + public void getDisposableShouldReturnset() { + final Disposable underlying = mock(Disposable.class); + serialDisposable.update(underlying); + assertSame(underlying, serialDisposable.get()); + + final Disposable another = mock(Disposable.class); + serialDisposable.update(another); + assertSame(another, serialDisposable.get()); + } + + @Test + public void notDisposedWhenReplaced() { + final Disposable underlying = mock(Disposable.class); + serialDisposable.update(underlying); + + serialDisposable.replace(Disposable.empty()); + serialDisposable.dispose(); + + verify(underlying, never()).dispose(); + } + + @Test + public void unsubscribingTwiceDoesUnsubscribeOnce() { + Disposable underlying = mock(Disposable.class); + serialDisposable.update(underlying); + + serialDisposable.dispose(); + verify(underlying).dispose(); + + serialDisposable.dispose(); + verifyNoMoreInteractions(underlying); + } + + @Test + public void settingSameDisposableTwiceDoesUnsubscribeIt() { + Disposable underlying = mock(Disposable.class); + serialDisposable.update(underlying); + verifyNoInteractions(underlying); + serialDisposable.update(underlying); + verify(underlying).dispose(); + } + + @Test + public void unsubscribingWithSingleUnderlyingUnsubscribes() { + Disposable underlying = mock(Disposable.class); + serialDisposable.update(underlying); + underlying.dispose(); + verify(underlying).dispose(); + } + + @Test + public void replacingFirstUnderlyingCausesUnsubscription() { + Disposable first = mock(Disposable.class); + serialDisposable.update(first); + Disposable second = mock(Disposable.class); + serialDisposable.update(second); + verify(first).dispose(); + } + + @Test + public void whenUnsubscribingSecondUnderlyingUnsubscribed() { + Disposable first = mock(Disposable.class); + serialDisposable.update(first); + Disposable second = mock(Disposable.class); + serialDisposable.update(second); + serialDisposable.dispose(); + verify(second).dispose(); + } + + @Test + public void settingUnderlyingWhenUnsubscribedCausesImmediateUnsubscription() { + serialDisposable.dispose(); + Disposable underlying = mock(Disposable.class); + serialDisposable.update(underlying); + verify(underlying).dispose(); + } + + @Test + public void settingUnderlyingWhenUnsubscribedCausesImmediateUnsubscriptionConcurrently() + throws InterruptedException { + final Disposable firstSet = mock(Disposable.class); + serialDisposable.update(firstSet); + + final CountDownLatch start = new CountDownLatch(1); + + final int count = 10; + final CountDownLatch end = new CountDownLatch(count); + + final List<Thread> threads = new ArrayList<>(); + for (int i = 0; i < count; i++) { + final Thread t = new Thread() { + @Override + public void run() { + try { + start.await(); + serialDisposable.dispose(); + } catch (InterruptedException e) { + fail(e.getMessage()); + } finally { + end.countDown(); + } + } + }; + t.start(); + threads.add(t); + } + + final Disposable underlying = mock(Disposable.class); + start.countDown(); + serialDisposable.update(underlying); + end.await(); + verify(firstSet).dispose(); + verify(underlying).dispose(); + + for (final Thread t : threads) { + t.join(); + } + } + + @Test + public void concurrentSetDisposableShouldNotInterleave() + throws InterruptedException { + final int count = 10; + final List<Disposable> subscriptions = new ArrayList<>(); + + final CountDownLatch start = new CountDownLatch(1); + final CountDownLatch end = new CountDownLatch(count); + + final List<Thread> threads = new ArrayList<>(); + for (int i = 0; i < count; i++) { + final Disposable subscription = mock(Disposable.class); + subscriptions.add(subscription); + + final Thread t = new Thread() { + @Override + public void run() { + try { + start.await(); + serialDisposable.update(subscription); + } catch (InterruptedException e) { + fail(e.getMessage()); + } finally { + end.countDown(); + } + } + }; + t.start(); + threads.add(t); + } + + start.countDown(); + end.await(); + serialDisposable.dispose(); + + for (final Disposable subscription : subscriptions) { + verify(subscription).dispose(); + } + + for (final Thread t : threads) { + t.join(); + } + } +} diff --git a/src/test/java/io/reactivex/rxjava3/disposables/SerialDisposableTests.java b/src/test/java/io/reactivex/rxjava3/disposables/SerialDisposableTests.java new file mode 100644 index 0000000000..3a2e73582b --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/disposables/SerialDisposableTests.java @@ -0,0 +1,224 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.disposables; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +import java.util.*; +import java.util.concurrent.CountDownLatch; + +import org.junit.*; +import org.junit.runner.RunWith; +import org.mockito.junit.MockitoJUnitRunner; + +import io.reactivex.rxjava3.core.RxJavaTest; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; + +@RunWith(MockitoJUnitRunner.class) +public class SerialDisposableTests extends RxJavaTest { + private SerialDisposable serialDisposable; + + @Before + public void setUp() { + serialDisposable = new SerialDisposable(); + } + + @Test + public void unsubscribingWithoutUnderlyingDoesNothing() { + serialDisposable.dispose(); + } + + @Test + public void getDisposableShouldReturnset() { + final Disposable underlying = mock(Disposable.class); + serialDisposable.set(underlying); + assertSame(underlying, serialDisposable.get()); + + final Disposable another = mock(Disposable.class); + serialDisposable.set(another); + assertSame(another, serialDisposable.get()); + } + + @Test + public void notDisposedWhenReplaced() { + final Disposable underlying = mock(Disposable.class); + serialDisposable.set(underlying); + + serialDisposable.replace(Disposable.empty()); + serialDisposable.dispose(); + + verify(underlying, never()).dispose(); + } + + @Test + public void unsubscribingTwiceDoesUnsubscribeOnce() { + Disposable underlying = mock(Disposable.class); + serialDisposable.set(underlying); + + serialDisposable.dispose(); + verify(underlying).dispose(); + + serialDisposable.dispose(); + verifyNoMoreInteractions(underlying); + } + + @Test + public void settingSameDisposableTwiceDoesUnsubscribeIt() { + Disposable underlying = mock(Disposable.class); + serialDisposable.set(underlying); + verifyNoInteractions(underlying); + serialDisposable.set(underlying); + verify(underlying).dispose(); + } + + @Test + public void unsubscribingWithSingleUnderlyingUnsubscribes() { + Disposable underlying = mock(Disposable.class); + serialDisposable.set(underlying); + underlying.dispose(); + verify(underlying).dispose(); + } + + @Test + public void replacingFirstUnderlyingCausesUnsubscription() { + Disposable first = mock(Disposable.class); + serialDisposable.set(first); + Disposable second = mock(Disposable.class); + serialDisposable.set(second); + verify(first).dispose(); + } + + @Test + public void whenUnsubscribingSecondUnderlyingUnsubscribed() { + Disposable first = mock(Disposable.class); + serialDisposable.set(first); + Disposable second = mock(Disposable.class); + serialDisposable.set(second); + serialDisposable.dispose(); + verify(second).dispose(); + } + + @Test + public void settingUnderlyingWhenUnsubscribedCausesImmediateUnsubscription() { + serialDisposable.dispose(); + Disposable underlying = mock(Disposable.class); + serialDisposable.set(underlying); + verify(underlying).dispose(); + } + + @Test + public void settingUnderlyingWhenUnsubscribedCausesImmediateUnsubscriptionConcurrently() + throws InterruptedException { + final Disposable firstSet = mock(Disposable.class); + serialDisposable.set(firstSet); + + final CountDownLatch start = new CountDownLatch(1); + + final int count = 10; + final CountDownLatch end = new CountDownLatch(count); + + final List<Thread> threads = new ArrayList<>(); + for (int i = 0; i < count; i++) { + final Thread t = new Thread() { + @Override + public void run() { + try { + start.await(); + serialDisposable.dispose(); + } catch (InterruptedException e) { + fail(e.getMessage()); + } finally { + end.countDown(); + } + } + }; + t.start(); + threads.add(t); + } + + final Disposable underlying = mock(Disposable.class); + start.countDown(); + serialDisposable.set(underlying); + end.await(); + verify(firstSet).dispose(); + verify(underlying).dispose(); + + for (final Thread t : threads) { + t.join(); + } + } + + @Test + public void concurrentSetDisposableShouldNotInterleave() + throws InterruptedException { + final int count = 10; + final List<Disposable> subscriptions = new ArrayList<>(); + + final CountDownLatch start = new CountDownLatch(1); + final CountDownLatch end = new CountDownLatch(count); + + final List<Thread> threads = new ArrayList<>(); + for (int i = 0; i < count; i++) { + final Disposable subscription = mock(Disposable.class); + subscriptions.add(subscription); + + final Thread t = new Thread() { + @Override + public void run() { + try { + start.await(); + serialDisposable.set(subscription); + } catch (InterruptedException e) { + fail(e.getMessage()); + } finally { + end.countDown(); + } + } + }; + t.start(); + threads.add(t); + } + + start.countDown(); + end.await(); + serialDisposable.dispose(); + + for (final Disposable subscription : subscriptions) { + verify(subscription).dispose(); + } + + for (final Thread t : threads) { + t.join(); + } + } + + @Test + public void disposeState() { + Disposable empty = Disposable.empty(); + SerialDisposable d = new SerialDisposable(empty); + + assertFalse(d.isDisposed()); + + assertSame(empty, d.get()); + + d.dispose(); + + assertTrue(d.isDisposed()); + + assertNotSame(empty, d.get()); + + assertNotSame(DisposableHelper.DISPOSED, d.get()); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/exceptions/CompositeExceptionTest.java b/src/test/java/io/reactivex/rxjava3/exceptions/CompositeExceptionTest.java new file mode 100644 index 0000000000..cc34f62b89 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/exceptions/CompositeExceptionTest.java @@ -0,0 +1,371 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.exceptions; + +import static org.junit.Assert.*; + +import java.io.*; +import java.util.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.RxJavaTest; + +public class CompositeExceptionTest extends RxJavaTest { + + private final Throwable ex1 = new Throwable("Ex1"); + private final Throwable ex2 = new Throwable("Ex2", ex1); + private final Throwable ex3 = new Throwable("Ex3", ex2); + + private CompositeException getNewCompositeExceptionWithEx123() { + List<Throwable> throwables = new ArrayList<>(); + throwables.add(ex1); + throwables.add(ex2); + throwables.add(ex3); + return new CompositeException(throwables); + } + + @Test + public void multipleWithSameCause() { + Throwable rootCause = new Throwable("RootCause"); + Throwable e1 = new Throwable("1", rootCause); + Throwable e2 = new Throwable("2", rootCause); + Throwable e3 = new Throwable("3", rootCause); + CompositeException ce = new CompositeException(e1, e2, e3); + + System.err.println("----------------------------- print composite stacktrace"); + ce.printStackTrace(); + assertEquals(3, ce.getExceptions().size()); + + assertNoCircularReferences(ce); + assertNotNull(getRootCause(ce)); + System.err.println("----------------------------- print cause stacktrace"); + ce.getCause().printStackTrace(); + } + + @Test + public void emptyErrors() { + try { + new CompositeException(); + fail("CompositeException should fail if errors is empty"); + } catch (IllegalArgumentException e) { + assertEquals("errors is empty", e.getMessage()); + } + try { + new CompositeException(new ArrayList<>()); + fail("CompositeException should fail if errors is empty"); + } catch (IllegalArgumentException e) { + assertEquals("errors is empty", e.getMessage()); + } + } + + @Test + public void compositeExceptionFromParentThenChild() { + CompositeException cex = new CompositeException(ex1, ex2); + + System.err.println("----------------------------- print composite stacktrace"); + cex.printStackTrace(); + assertEquals(2, cex.getExceptions().size()); + + assertNoCircularReferences(cex); + assertNotNull(getRootCause(cex)); + + System.err.println("----------------------------- print cause stacktrace"); + cex.getCause().printStackTrace(); + } + + @Test + public void compositeExceptionFromChildThenParent() { + CompositeException cex = new CompositeException(ex2, ex1); + + System.err.println("----------------------------- print composite stacktrace"); + cex.printStackTrace(); + assertEquals(2, cex.getExceptions().size()); + + assertNoCircularReferences(cex); + assertNotNull(getRootCause(cex)); + + System.err.println("----------------------------- print cause stacktrace"); + cex.getCause().printStackTrace(); + } + + @Test + public void compositeExceptionFromChildAndComposite() { + CompositeException cex = new CompositeException(ex1, getNewCompositeExceptionWithEx123()); + + System.err.println("----------------------------- print composite stacktrace"); + cex.printStackTrace(); + assertEquals(3, cex.getExceptions().size()); + + assertNoCircularReferences(cex); + assertNotNull(getRootCause(cex)); + + System.err.println("----------------------------- print cause stacktrace"); + cex.getCause().printStackTrace(); + } + + @Test + public void compositeExceptionFromCompositeAndChild() { + CompositeException cex = new CompositeException(getNewCompositeExceptionWithEx123(), ex1); + + System.err.println("----------------------------- print composite stacktrace"); + cex.printStackTrace(); + assertEquals(3, cex.getExceptions().size()); + + assertNoCircularReferences(cex); + assertNotNull(getRootCause(cex)); + + System.err.println("----------------------------- print cause stacktrace"); + cex.getCause().printStackTrace(); + } + + @Test + public void compositeExceptionFromTwoDuplicateComposites() { + List<Throwable> exs = new ArrayList<>(); + exs.add(getNewCompositeExceptionWithEx123()); + exs.add(getNewCompositeExceptionWithEx123()); + CompositeException cex = new CompositeException(exs); + + System.err.println("----------------------------- print composite stacktrace"); + cex.printStackTrace(); + assertEquals(3, cex.getExceptions().size()); + + assertNoCircularReferences(cex); + assertNotNull(getRootCause(cex)); + + System.err.println("----------------------------- print cause stacktrace"); + cex.getCause().printStackTrace(); + } + + /* + * This hijacks the Throwable.printStackTrace() output and puts it in a string, where we can look for + * "CIRCULAR REFERENCE" (a String added by Throwable.printEnclosedStackTrace) + */ + private static void assertNoCircularReferences(Throwable ex) { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + PrintStream printStream = new PrintStream(baos); + ex.printStackTrace(printStream); + assertFalse(baos.toString().contains("CIRCULAR REFERENCE")); + } + + private static Throwable getRootCause(Throwable ex) { + Throwable root = ex.getCause(); + if (root == null) { + return null; + } else { + while (true) { + if (root.getCause() == null) { + return root; + } else { + root = root.getCause(); + } + } + } + } + + @Test + public void nullCollection() { + CompositeException composite = new CompositeException((List<Throwable>)null); + composite.getCause(); + composite.printStackTrace(); + } + + @Test + public void nullElement() { + CompositeException composite = new CompositeException(Collections.singletonList((Throwable) null)); + composite.getCause(); + composite.printStackTrace(); + } + + @Test + public void compositeExceptionWithUnsupportedInitCause() { + Throwable t = new Throwable() { + + private static final long serialVersionUID = -3282577447436848385L; + + @Override + public synchronized Throwable initCause(Throwable cause) { + throw new UnsupportedOperationException(); + } + }; + CompositeException cex = new CompositeException(t, ex1); + + System.err.println("----------------------------- print composite stacktrace"); + cex.printStackTrace(); + assertEquals(2, cex.getExceptions().size()); + + assertNoCircularReferences(cex); + assertNotNull(getRootCause(cex)); + + System.err.println("----------------------------- print cause stacktrace"); + cex.getCause().printStackTrace(); + } + + @Test + public void compositeExceptionWithNullInitCause() { + Throwable t = new Throwable("ThrowableWithNullInitCause") { + + private static final long serialVersionUID = -7984762607894527888L; + + @Override + public synchronized Throwable initCause(Throwable cause) { + return null; + } + }; + CompositeException cex = new CompositeException(t, ex1); + + System.err.println("----------------------------- print composite stacktrace"); + cex.printStackTrace(); + assertEquals(2, cex.getExceptions().size()); + + assertNoCircularReferences(cex); + assertNotNull(getRootCause(cex)); + + System.err.println("----------------------------- print cause stacktrace"); + cex.getCause().printStackTrace(); + } + + @Test + public void messageCollection() { + CompositeException compositeException = new CompositeException(ex1, ex3); + assertEquals("2 exceptions occurred. ", compositeException.getMessage()); + } + + @Test + public void messageVarargs() { + CompositeException compositeException = new CompositeException(ex1, ex2, ex3); + assertEquals("3 exceptions occurred. ", compositeException.getMessage()); + } + + @Test + public void constructorWithNull() { + assertTrue(new CompositeException((Throwable[])null).getExceptions().get(0) instanceof NullPointerException); + + assertTrue(new CompositeException((Iterable<Throwable>)null).getExceptions().get(0) instanceof NullPointerException); + + assertTrue(new CompositeException(null, new TestException()).getExceptions().get(0) instanceof NullPointerException); + } + + @Test + public void printStackTrace() { + StringWriter sw = new StringWriter(); + PrintWriter pw = new PrintWriter(sw); + + new CompositeException(new TestException()).printStackTrace(pw); + + assertTrue(sw.toString().contains("TestException")); + } + + @Test + public void badException() { + Throwable e = new BadException(); + assertSame(e, new CompositeException(e).getCause().getCause()); + assertSame(e, new CompositeException(new RuntimeException(e)).getCause().getCause().getCause()); + } + + @Test + public void exceptionOverview() { + CompositeException composite = new CompositeException( + new TestException("ex1"), + new TestException("ex2"), + new TestException("ex3", new TestException("ex4")) + ); + + String overview = composite.getCause().getMessage(); + + assertTrue(overview, overview.contains("Multiple exceptions (3)")); + assertTrue(overview, overview.contains("io.reactivex.rxjava3.exceptions.TestException: ex1")); + assertTrue(overview, overview.contains("io.reactivex.rxjava3.exceptions.TestException: ex2")); + assertTrue(overview, overview.contains("io.reactivex.rxjava3.exceptions.TestException: ex3")); + assertTrue(overview, overview.contains("io.reactivex.rxjava3.exceptions.TestException: ex4")); + assertTrue(overview, overview.contains("at io.reactivex.rxjava3.exceptions.CompositeExceptionTest.exceptionOverview")); + } + + @Test + public void causeWithExceptionWithoutStacktrace() { + CompositeException composite = new CompositeException( + new TestException("ex1"), + new CompositeException.ExceptionOverview("example") + ); + + String overview = composite.getCause().getMessage(); + + assertTrue(overview, overview.contains("Multiple exceptions (2)")); + assertTrue(overview, overview.contains("io.reactivex.rxjava3.exceptions.TestException: ex1")); + assertTrue(overview, overview.contains("io.reactivex.rxjava3.exceptions.CompositeException.ExceptionOverview: example")); + + assertEquals(overview, 2, overview.split("at\\s").length); + } + + @Test + public void reoccurringException() { + TestException ex0 = new TestException("ex0"); + TestException ex1 = new TestException("ex1", ex0); + CompositeException composite = new CompositeException( + ex1, + new TestException("ex2", ex1) + ); + + String overview = composite.getCause().getMessage(); + System.err.println(overview); + + assertTrue(overview, overview.contains("Multiple exceptions (2)")); + assertTrue(overview, overview.contains("io.reactivex.rxjava3.exceptions.TestException: ex0")); + assertTrue(overview, overview.contains("io.reactivex.rxjava3.exceptions.TestException: ex1")); + assertTrue(overview, overview.contains("io.reactivex.rxjava3.exceptions.TestException: ex2")); + assertTrue(overview, overview.contains("(cause not expanded again) io.reactivex.rxjava3.exceptions.TestException: ex0")); + assertEquals(overview, 5, overview.split("at\\s").length); + } + + @Test + public void nestedMultilineMessage() { + TestException ex1 = new TestException("ex1"); + TestException ex2 = new TestException("ex2"); + CompositeException composite1 = new CompositeException( + ex1, + ex2 + ); + TestException ex3 = new TestException("ex3"); + TestException ex4 = new TestException("ex4", composite1); + + CompositeException composite2 = new CompositeException( + ex3, + ex4 + ); + + String overview = composite2.getCause().getMessage(); + System.err.println(overview); + + assertTrue(overview, overview.contains(" Multiple exceptions (2)")); + assertTrue(overview, overview.contains(" |-- io.reactivex.rxjava3.exceptions.TestException: ex1")); + assertTrue(overview, overview.contains(" |-- io.reactivex.rxjava3.exceptions.TestException: ex2")); + } + + @Test + public void singleExceptionIsTheCause() { + TestException ex = new TestException("ex1"); + CompositeException composite = new CompositeException(ex); + + assertSame(composite.getCause(), ex); + } +} + +final class BadException extends Throwable { + private static final long serialVersionUID = 8999507293896399171L; + + @Override + public synchronized Throwable getCause() { + return this; + } +} diff --git a/src/test/java/io/reactivex/rxjava3/exceptions/ExceptionsTest.java b/src/test/java/io/reactivex/rxjava3/exceptions/ExceptionsTest.java new file mode 100644 index 0000000000..b6aa069010 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/exceptions/ExceptionsTest.java @@ -0,0 +1,247 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.exceptions; + +import static org.junit.Assert.*; + +import java.io.IOException; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.functions.Consumer; +import io.reactivex.rxjava3.internal.util.ExceptionHelper; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.subjects.PublishSubject; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class ExceptionsTest extends RxJavaTest { + + @Test + public void constructorShouldBePrivate() { + TestHelper.checkUtilityClass(ExceptionHelper.class); + } + + @Test + public void onErrorNotImplementedIsThrown() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + + Observable.just(1, 2, 3).subscribe(new Consumer<Integer>() { + + @Override + public void accept(Integer t1) { + throw new RuntimeException("hello"); + } + + }); + + TestHelper.assertError(errors, 0, RuntimeException.class); + assertTrue(errors.get(0).toString(), errors.get(0).getMessage().contains("hello")); + RxJavaPlugins.reset(); + } + + @Test + public void stackOverflowWouldOccur() { + final PublishSubject<Integer> a = PublishSubject.create(); + final PublishSubject<Integer> b = PublishSubject.create(); + final int MAX_STACK_DEPTH = 800; + final AtomicInteger depth = new AtomicInteger(); + + a.subscribe(new Observer<Integer>() { + + @Override + public void onSubscribe(Disposable d) { + + } + + @Override + public void onComplete() { + + } + + @Override + public void onError(Throwable e) { + e.printStackTrace(); + } + + @Override + public void onNext(Integer n) { + b.onNext(n + 1); + } + }); + b.subscribe(new Observer<Integer>() { + + @Override + public void onSubscribe(Disposable d) { + // TODO Auto-generated method stub + + } + + @Override + public void onComplete() { + + } + + @Override + public void onError(Throwable e) { + e.printStackTrace(); + } + + @Override + public void onNext(Integer n) { + if (depth.get() < MAX_STACK_DEPTH) { + depth.set(Thread.currentThread().getStackTrace().length); + a.onNext(n + 1); + } + } + }); + a.onNext(1); + assertTrue(depth.get() >= MAX_STACK_DEPTH); + } + + @Test(expected = StackOverflowError.class) + public void stackOverflowErrorIsThrown() { + Observable.just(1).subscribe(new Observer<Integer>() { + + @Override + public void onSubscribe(Disposable d) { + + } + + @Override + public void onComplete() { + + } + + @Override + public void onError(Throwable e) { + e.printStackTrace(); + } + + @Override + public void onNext(Integer t) { + throw new StackOverflowError(); + } + + }); + } + + @Test(expected = ThreadDeath.class) + public void threadDeathIsThrown() { + Observable.just(1).subscribe(new Observer<Integer>() { + + @Override + public void onSubscribe(Disposable d) { + + } + + @Override + public void onComplete() { + + } + + @Override + public void onError(Throwable e) { + e.printStackTrace(); + } + + @Override + public void onNext(Integer t) { + throw new ThreadDeath(); + } + + }); + } + + @Test + public void utilityClass() { + TestHelper.checkUtilityClass(Exceptions.class); + } + + @Test + public void manualThrowIfFatal() { + + try { + Exceptions.throwIfFatal(new ThreadDeath()); + fail("Didn't throw fatal exception"); + } catch (ThreadDeath ex) { + // expected + } + + try { + Exceptions.throwIfFatal(new LinkageError()); + fail("Didn't throw fatal error"); + } catch (LinkageError ex) { + // expected + } + + try { + ExceptionHelper.wrapOrThrow(new LinkageError()); + fail("Didn't propagate Error"); + } catch (LinkageError ex) { + // expected + } + } + + @Test + public void manualPropagate() { + + try { + Exceptions.propagate(new InternalError()); + fail("Didn't throw exception"); + } catch (InternalError ex) { + // expected + } + + try { + throw Exceptions.propagate(new IllegalArgumentException()); + } catch (IllegalArgumentException ex) { + // expected + } + + try { + throw ExceptionHelper.wrapOrThrow(new IOException()); + } catch (RuntimeException ex) { + if (!(ex.getCause() instanceof IOException)) { + fail(ex.toString() + ": should have thrown RuntimeException(IOException)"); + } + } + } + + @Test + public void errorNotImplementedNull1() { + OnErrorNotImplementedException ex = new OnErrorNotImplementedException(null); + + assertTrue("" + ex.getCause(), ex.getCause() instanceof NullPointerException); + } + + @Test + public void errorNotImplementedNull2() { + OnErrorNotImplementedException ex = new OnErrorNotImplementedException("Message", null); + + assertTrue("" + ex.getCause(), ex.getCause() instanceof NullPointerException); + } + + @Test + public void errorNotImplementedWithCause() { + OnErrorNotImplementedException ex = new OnErrorNotImplementedException("Message", new TestException("Forced failure")); + + assertTrue("" + ex.getCause(), ex.getCause() instanceof TestException); + + assertEquals("" + ex.getCause(), "Forced failure", ex.getCause().getMessage()); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/exceptions/OnErrorNotImplementedExceptionTest.java b/src/test/java/io/reactivex/rxjava3/exceptions/OnErrorNotImplementedExceptionTest.java new file mode 100644 index 0000000000..70ba775bc1 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/exceptions/OnErrorNotImplementedExceptionTest.java @@ -0,0 +1,136 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.exceptions; + +import static org.junit.Assert.*; + +import java.util.List; + +import org.junit.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class OnErrorNotImplementedExceptionTest extends RxJavaTest { + + List<Throwable> errors; + + @Before + public void before() { + errors = TestHelper.trackPluginErrors(); + } + + @After + public void after() { + RxJavaPlugins.reset(); + + assertFalse("" + errors, errors.isEmpty()); + TestHelper.assertError(errors, 0, OnErrorNotImplementedException.class); + Throwable c = errors.get(0).getCause(); + assertTrue("" + c, c instanceof TestException); + } + + @Test + public void flowableSubscribe0() { + Flowable.error(new TestException()) + .subscribe(); + } + + @Test + public void flowableSubscribe1() { + Flowable.error(new TestException()) + .subscribe(Functions.emptyConsumer()); + } + + @Test + public void flowableForEachWhile() { + Flowable.error(new TestException()) + .forEachWhile(Functions.alwaysTrue()); + } + + @Test + public void flowableBlockingSubscribe1() { + Flowable.error(new TestException()) + .blockingSubscribe(Functions.emptyConsumer()); + } + + @Test + public void flowableBoundedBlockingSubscribe1() { + Flowable.error(new TestException()) + .blockingSubscribe(Functions.emptyConsumer(), 128); + } + + @Test + public void observableSubscribe0() { + Observable.error(new TestException()) + .subscribe(); + } + + @Test + public void observableSubscribe1() { + Observable.error(new TestException()) + .subscribe(Functions.emptyConsumer()); + } + + @Test + public void observableForEachWhile() { + Observable.error(new TestException()) + .forEachWhile(Functions.alwaysTrue()); + } + + @Test + public void observableBlockingSubscribe1() { + Observable.error(new TestException()) + .blockingSubscribe(Functions.emptyConsumer()); + } + + @Test + public void singleSubscribe0() { + Single.error(new TestException()) + .subscribe(); + } + + @Test + public void singleSubscribe1() { + Single.error(new TestException()) + .subscribe(Functions.emptyConsumer()); + } + + @Test + public void maybeSubscribe0() { + Maybe.error(new TestException()) + .subscribe(); + } + + @Test + public void maybeSubscribe1() { + Maybe.error(new TestException()) + .subscribe(Functions.emptyConsumer()); + } + + @Test + public void completableSubscribe0() { + Completable.error(new TestException()) + .subscribe(); + } + + @Test + public void completableSubscribe1() { + Completable.error(new TestException()) + .subscribe(Functions.EMPTY_ACTION); + } + +} diff --git a/src/test/java/io/reactivex/rxjava3/exceptions/TestException.java b/src/test/java/io/reactivex/rxjava3/exceptions/TestException.java new file mode 100644 index 0000000000..7bcdd318fd --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/exceptions/TestException.java @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.exceptions; + +/** + * Exception for testing if unchecked exceptions propagate as-is without confusing with + * other type of common exceptions. + */ +public final class TestException extends RuntimeException { + + private static final long serialVersionUID = -1438148770465406172L; + + /** + * Constructs a TestException without message or cause. + */ + public TestException() { + super(); + } + + /** + * Counstructs a TestException with message and cause. + * @param message the message + * @param cause the cause + */ + public TestException(String message, Throwable cause) { + super(message, cause); + } + + /** + * Constructs a TestException with a message only. + * @param message the message + */ + public TestException(String message) { + super(message); + } + + /** + * Constructs a TestException with a cause only. + * @param cause the cause + */ + public TestException(Throwable cause) { + super(cause); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/flowable/Burst.java b/src/test/java/io/reactivex/rxjava3/flowable/Burst.java new file mode 100644 index 0000000000..efa0993042 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/flowable/Burst.java @@ -0,0 +1,130 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.flowable; + +import java.util.*; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.atomic.AtomicLong; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.internal.subscriptions.SubscriptionHelper; +import io.reactivex.rxjava3.internal.util.BackpressureHelper; + +/** + * Creates {@link Flowable} of a number of items followed by either an error or + * completion. Cancellation has no effect on preventing emissions until the + * currently outstanding requests have been met. + * @param <T> the value type + */ +public final class Burst<T> extends Flowable<T> { + + final List<T> items; + final Throwable error; + + Burst(Throwable error, List<T> items) { + if (items.isEmpty()) { + throw new IllegalArgumentException("items cannot be empty"); + } + for (T item : items) { + if (item == null) { + throw new IllegalArgumentException("items cannot include null"); + } + } + this.error = error; + this.items = items; + } + + @Override + protected void subscribeActual(final Subscriber<? super T> subscriber) { + subscriber.onSubscribe(new BurstSubscription(subscriber)); + + } + + public static <T> Builder<T> item(T item) { + return items(item); + } + + @SafeVarargs + public static <T> Builder<T> items(T... items) { + return new Builder<>(Arrays.asList(items)); + } + + final class BurstSubscription implements Subscription { + private final Subscriber<? super T> subscriber; + final Queue<T> q = new ConcurrentLinkedQueue<>(items); + final AtomicLong requested = new AtomicLong(); + volatile boolean cancelled; + + BurstSubscription(Subscriber<? super T> subscriber) { + this.subscriber = subscriber; + } + + @Override + public void request(long n) { + if (cancelled) { + // required by reactive-streams-jvm 3.6 + return; + } + if (SubscriptionHelper.validate(n)) { + // just for testing, don't care about perf + // so no attempt made to reduce volatile reads + if (BackpressureHelper.add(requested, n) == 0) { + if (q.isEmpty()) { + return; + } + while (!q.isEmpty() && requested.get() > 0) { + T item = q.poll(); + requested.decrementAndGet(); + subscriber.onNext(item); + } + if (q.isEmpty()) { + if (error != null) { + subscriber.onError(error); + } else { + subscriber.onComplete(); + } + } + } + } + } + + @Override + public void cancel() { + cancelled = true; + } + } + + public static final class Builder<T> { + + private final List<T> items; + private Throwable error; + + Builder(List<T> items) { + this.items = items; + } + + public Flowable<T> error(Throwable e) { + this.error = e; + return create(); + } + + public Flowable<T> create() { + return new Burst<>(error, items); + } + + } + +} diff --git a/src/test/java/io/reactivex/rxjava3/flowable/FlowableBackpressureTests.java b/src/test/java/io/reactivex/rxjava3/flowable/FlowableBackpressureTests.java new file mode 100644 index 0000000000..e6b3a02c68 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/flowable/FlowableBackpressureTests.java @@ -0,0 +1,720 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.flowable; + +import static org.junit.Assert.*; + +import java.util.List; +import java.util.concurrent.*; +import java.util.concurrent.atomic.*; + +import org.junit.*; +import org.junit.rules.TestName; +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.QueueOverflowException; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.subscriptions.SubscriptionHelper; +import io.reactivex.rxjava3.internal.util.BackpressureHelper; +import io.reactivex.rxjava3.schedulers.Schedulers; +import io.reactivex.rxjava3.subscribers.*; + +public class FlowableBackpressureTests extends RxJavaTest { + + static final class FirehoseNoBackpressure extends AtomicBoolean implements Subscription { + + private static final long serialVersionUID = -669931580197884015L; + final Subscriber<? super Integer> downstream; + final AtomicInteger counter; + volatile boolean cancelled; + + private FirehoseNoBackpressure(AtomicInteger counter, Subscriber<? super Integer> s) { + this.counter = counter; + this.downstream = s; + } + + @Override + public void request(long n) { + if (!SubscriptionHelper.validate(n)) { + return; + } + if (compareAndSet(false, true)) { + int i = 0; + final Subscriber<? super Integer> a = downstream; + final AtomicInteger c = counter; + + while (!cancelled) { + a.onNext(i++); + c.incrementAndGet(); + } + System.out.println("unsubscribed after: " + i); + } + } + + @Override + public void cancel() { + cancelled = true; + } + } + + @Rule + public TestName testName = new TestName(); + + @After + public void doAfterTest() { + // FIXME LATER +// TestObstructionDetection.checkObstruction(); + } + + @Test + public void observeOn() { + int num = (int) (Flowable.bufferSize() * 2.1); + AtomicInteger c = new AtomicInteger(); + TestSubscriber<Integer> ts = new TestSubscriber<>(); + incrementingIntegers(c).observeOn(Schedulers.computation()).take(num).subscribe(ts); + ts.awaitDone(5, TimeUnit.SECONDS); + ts.assertNoErrors(); + System.out.println("testObserveOn => Received: " + ts.values().size() + " Emitted: " + c.get()); + assertEquals(num, ts.values().size()); + assertTrue(c.get() < Flowable.bufferSize() * 4); + } + + @Test + public void observeOnWithSlowConsumer() { + int num = (int) (Flowable.bufferSize() * 0.2); + AtomicInteger c = new AtomicInteger(); + TestSubscriber<Integer> ts = new TestSubscriber<>(); + incrementingIntegers(c).observeOn(Schedulers.computation()).map( + new Function<Integer, Integer>() { + @Override + public Integer apply(Integer i) { + try { + Thread.sleep(1); + } catch (InterruptedException e) { + e.printStackTrace(); + } + return i; + } + } + ).take(num).subscribe(ts); + ts.awaitDone(5, TimeUnit.SECONDS); + ts.assertNoErrors(); + System.out.println("testObserveOnWithSlowConsumer => Received: " + ts.values().size() + " Emitted: " + c.get()); + assertEquals(num, ts.values().size()); + assertTrue(c.get() < Flowable.bufferSize() * 2); + } + + @Test + public void mergeSync() { + int num = (int) (Flowable.bufferSize() * 4.1); + AtomicInteger c1 = new AtomicInteger(); + AtomicInteger c2 = new AtomicInteger(); + TestSubscriber<Integer> ts = new TestSubscriber<>(); + Flowable<Integer> merged = Flowable.merge(incrementingIntegers(c1), incrementingIntegers(c2)); + + merged.take(num).subscribe(ts); + ts.awaitDone(5, TimeUnit.SECONDS); + ts.assertNoErrors(); + System.out.println("Expected: " + num + " got: " + ts.values().size()); + System.out.println("testMergeSync => Received: " + ts.values().size() + " Emitted: " + c1.get() + " / " + c2.get()); + assertEquals(num, ts.values().size()); + // either one can starve the other, but neither should be capable of doing more than 5 batches (taking 4.1) + // TODO is it possible to make this deterministic rather than one possibly starving the other? + // benjchristensen => In general I'd say it's not worth trying to make it so, as "fair" algorithms generally take a performance hit + assertTrue(c1.get() < Flowable.bufferSize() * 5); + assertTrue(c2.get() < Flowable.bufferSize() * 5); + } + + @Test + public void mergeAsync() { + int num = (int) (Flowable.bufferSize() * 4.1); + AtomicInteger c1 = new AtomicInteger(); + AtomicInteger c2 = new AtomicInteger(); + TestSubscriber<Integer> ts = new TestSubscriber<>(); + Flowable<Integer> merged = Flowable.merge( + incrementingIntegers(c1).subscribeOn(Schedulers.computation()), + incrementingIntegers(c2).subscribeOn(Schedulers.computation())); + + merged.take(num).subscribe(ts); + ts.awaitDone(5, TimeUnit.SECONDS); + ts.assertNoErrors(); + System.out.println("testMergeAsync => Received: " + ts.values().size() + " Emitted: " + c1.get() + " / " + c2.get()); + assertEquals(num, ts.values().size()); + // either one can starve the other, but neither should be capable of doing more than 5 batches (taking 4.1) + // TODO is it possible to make this deterministic rather than one possibly starving the other? + // benjchristensen => In general I'd say it's not worth trying to make it so, as "fair" algorithms generally take a performance hit + int max = Flowable.bufferSize() * 7; + assertTrue("" + c1.get() + " >= " + max, c1.get() < max); + assertTrue("" + c2.get() + " >= " + max, c2.get() < max); + } + + @Test + public void mergeAsyncThenObserveOnLoop() { + for (int i = 0; i < 500; i++) { + if (i % 10 == 0) { + System.out.println("testMergeAsyncThenObserveOnLoop >> " + i); + } + // Verify there is no MissingBackpressureException + int num = (int) (Flowable.bufferSize() * 4.1); + AtomicInteger c1 = new AtomicInteger(); + AtomicInteger c2 = new AtomicInteger(); + + TestSubscriber<Integer> ts = new TestSubscriber<>(); + Flowable<Integer> merged = Flowable.merge( + incrementingIntegers(c1).subscribeOn(Schedulers.computation()), + incrementingIntegers(c2).subscribeOn(Schedulers.computation())); + + merged + .observeOn(Schedulers.io()) + .take(num) + .subscribe(ts); + + ts.awaitDone(5, TimeUnit.SECONDS); + ts.assertComplete(); + ts.assertNoErrors(); + System.out.println("testMergeAsyncThenObserveOn => Received: " + ts.values().size() + " Emitted: " + c1.get() + " / " + c2.get()); + assertEquals(num, ts.values().size()); + } + } + + @Test + public void mergeAsyncThenObserveOn() { + int num = (int) (Flowable.bufferSize() * 4.1); + AtomicInteger c1 = new AtomicInteger(); + AtomicInteger c2 = new AtomicInteger(); + TestSubscriber<Integer> ts = new TestSubscriber<>(); + Flowable<Integer> merged = Flowable.merge( + incrementingIntegers(c1).subscribeOn(Schedulers.computation()), + incrementingIntegers(c2).subscribeOn(Schedulers.computation())); + + merged.observeOn(Schedulers.newThread()).take(num).subscribe(ts); + ts.awaitDone(5, TimeUnit.SECONDS); + ts.assertNoErrors(); + System.out.println("testMergeAsyncThenObserveOn => Received: " + ts.values().size() + " Emitted: " + c1.get() + " / " + c2.get()); + assertEquals(num, ts.values().size()); + // either one can starve the other, but neither should be capable of doing more than 5 batches (taking 4.1) + // TODO is it possible to make this deterministic rather than one possibly starving the other? + // benjchristensen => In general I'd say it's not worth trying to make it so, as "fair" algorithms generally take a performance hit + // akarnokd => run this in a loop over 10k times and never saw values get as high as 7*SIZE, but since observeOn delays the unsubscription non-deterministically, the test will remain unreliable + assertTrue(c1.get() < Flowable.bufferSize() * 7); + assertTrue(c2.get() < Flowable.bufferSize() * 7); + } + + @Test + public void flatMapSync() { + int num = (int) (Flowable.bufferSize() * 2.1); + AtomicInteger c = new AtomicInteger(); + TestSubscriber<Integer> ts = new TestSubscriber<>(); + + incrementingIntegers(c) + .flatMap(new Function<Integer, Publisher<Integer>>() { + @Override + public Publisher<Integer> apply(Integer i) { + return incrementingIntegers(new AtomicInteger()).take(10); + } + }) + .take(num).subscribe(ts); + + ts.awaitDone(5, TimeUnit.SECONDS); + ts.assertNoErrors(); + System.out.println("testFlatMapSync => Received: " + ts.values().size() + " Emitted: " + c.get()); + assertEquals(num, ts.values().size()); + // expect less than 1 buffer since the flatMap is emitting 10 each time, so it is num/10 that will be taken. + assertTrue(c.get() < Flowable.bufferSize()); + } + + @Test + public void zipSync() { + int num = (int) (Flowable.bufferSize() * 4.1); + AtomicInteger c1 = new AtomicInteger(); + AtomicInteger c2 = new AtomicInteger(); + TestSubscriber<Integer> ts = new TestSubscriber<>(); + + Flowable<Integer> zipped = Flowable.zip( + incrementingIntegers(c1), + incrementingIntegers(c2), + new BiFunction<Integer, Integer, Integer>() { + @Override + public Integer apply(Integer t1, Integer t2) { + return t1 + t2; + } + }); + + zipped.take(num) + .subscribe(ts); + + ts.awaitDone(5, TimeUnit.SECONDS); + ts.assertNoErrors(); + System.out.println("testZipSync => Received: " + ts.values().size() + " Emitted: " + c1.get() + " / " + c2.get()); + assertEquals(num, ts.values().size()); + assertTrue(c1.get() < Flowable.bufferSize() * 7); + assertTrue(c2.get() < Flowable.bufferSize() * 7); + } + + @Test + public void zipAsync() { + int num = (int) (Flowable.bufferSize() * 2.1); + AtomicInteger c1 = new AtomicInteger(); + AtomicInteger c2 = new AtomicInteger(); + TestSubscriber<Integer> ts = new TestSubscriber<>(); + Flowable<Integer> zipped = Flowable.zip( + incrementingIntegers(c1).subscribeOn(Schedulers.computation()), + incrementingIntegers(c2).subscribeOn(Schedulers.computation()), + new BiFunction<Integer, Integer, Integer>() { + @Override + public Integer apply(Integer t1, Integer t2) { + return t1 + t2; + } + }); + + zipped.take(num).subscribe(ts); + ts.awaitDone(5, TimeUnit.SECONDS); + ts.assertNoErrors(); + System.out.println("testZipAsync => Received: " + ts.values().size() + " Emitted: " + c1.get() + " / " + c2.get()); + assertEquals(num, ts.values().size()); + int max = Flowable.bufferSize() * 5; + assertTrue("" + c1.get() + " >= " + max, c1.get() < max); + assertTrue("" + c2.get() + " >= " + max, c2.get() < max); + } + + @Test + public void subscribeOnScheduling() { + // in a loop for repeating the concurrency in this to increase chance of failure + for (int i = 0; i < 100; i++) { + int num = (int) (Flowable.bufferSize() * 2.1); + AtomicInteger c = new AtomicInteger(); + ConcurrentLinkedQueue<Thread> threads = new ConcurrentLinkedQueue<>(); + TestSubscriber<Integer> ts = new TestSubscriber<>(); + // observeOn is there to make it async and need backpressure + incrementingIntegers(c, threads).subscribeOn(Schedulers.computation()).observeOn(Schedulers.computation()).take(num).subscribe(ts); + ts.awaitDone(5, TimeUnit.SECONDS); + ts.assertNoErrors(); + System.out.println("testSubscribeOnScheduling => Received: " + ts.values().size() + " Emitted: " + c.get()); + assertEquals(num, ts.values().size()); + assertTrue(c.get() < Flowable.bufferSize() * 4); + Thread first = null; + for (Thread t : threads) { + System.out.println("testSubscribeOnScheduling => thread: " + t); + if (first == null) { + first = t; + } else { + if (!first.equals(t)) { + fail("Expected to see the same thread"); + } + } + } + System.out.println("testSubscribeOnScheduling => Number of batch requests seen: " + threads.size()); + assertTrue(threads.size() > 1); + System.out.println("-------------------------------------------------------------------------------------------"); + } + } + + @Test + public void takeFilterSkipChainAsync() { + int num = (int) (Flowable.bufferSize() * 2.1); + AtomicInteger c = new AtomicInteger(); + TestSubscriber<Integer> ts = new TestSubscriber<>(); + incrementingIntegers(c).observeOn(Schedulers.computation()) + .skip(10000) + .filter(new Predicate<Integer>() { + @Override + public boolean test(Integer i) { + return i > 11000; + } + }).take(num).subscribe(ts); + + ts.awaitDone(5, TimeUnit.SECONDS); + ts.assertNoErrors(); + + // emit 10000 that are skipped + // emit next 1000 that are filtered out + // take num + // so emitted is at least 10000+1000+num + extra for buffer size/threshold + int expected = 10000 + 1000 + Flowable.bufferSize() * 3 + Flowable.bufferSize() / 2; + + System.out.println("testTakeFilterSkipChain => Received: " + ts.values().size() + " Emitted: " + c.get() + " Expected: " + expected); + assertEquals(num, ts.values().size()); + assertTrue(c.get() < expected); + } + + @Test + public void userSubscriberUsingRequestSync() { + AtomicInteger c = new AtomicInteger(); + final AtomicInteger totalReceived = new AtomicInteger(); + final AtomicInteger batches = new AtomicInteger(); + final AtomicInteger received = new AtomicInteger(); + incrementingIntegers(c).subscribe(new ResourceSubscriber<Integer>() { + + @Override + public void onStart() { + request(100); + } + + @Override + public void onComplete() { + + } + + @Override + public void onError(Throwable e) { + + } + + @Override + public void onNext(Integer t) { + int total = totalReceived.incrementAndGet(); + received.incrementAndGet(); + if (total >= 2000) { + dispose(); + } + if (received.get() == 100) { + batches.incrementAndGet(); + request(100); + received.set(0); + } + } + + }); + + System.out.println("testUserSubscriberUsingRequestSync => Received: " + totalReceived.get() + " Emitted: " + c.get() + " Request Batches: " + batches.get()); + assertEquals(2000, c.get()); + assertEquals(2000, totalReceived.get()); + assertEquals(20, batches.get()); + } + + @Test + public void userSubscriberUsingRequestAsync() throws InterruptedException { + AtomicInteger c = new AtomicInteger(); + final AtomicInteger totalReceived = new AtomicInteger(); + final AtomicInteger received = new AtomicInteger(); + final AtomicInteger batches = new AtomicInteger(); + final CountDownLatch latch = new CountDownLatch(1); + incrementingIntegers(c).subscribeOn(Schedulers.newThread()).subscribe( + new ResourceSubscriber<Integer>() { + + @Override + public void onStart() { + request(100); + } + + @Override + public void onComplete() { + latch.countDown(); + } + + @Override + public void onError(Throwable e) { + latch.countDown(); + } + + @Override + public void onNext(Integer t) { + int total = totalReceived.incrementAndGet(); + received.incrementAndGet(); + boolean done = false; + if (total >= 2000) { + done = true; + dispose(); + } + if (received.get() == 100) { + batches.incrementAndGet(); + received.set(0); + if (!done) { + request(100); + } + } + if (done) { + latch.countDown(); + } + } + + }); + + latch.await(); + System.out.println("testUserSubscriberUsingRequestAsync => Received: " + totalReceived.get() + " Emitted: " + c.get() + " Request Batches: " + batches.get()); + assertEquals(2000, c.get()); + assertEquals(2000, totalReceived.get()); + assertEquals(20, batches.get()); + } + + @Test + public void firehoseFailsAsExpected() { + AtomicInteger c = new AtomicInteger(); + TestSubscriber<Integer> ts = new TestSubscriber<>(); + + firehose(c).observeOn(Schedulers.computation()) + .map(new Function<Integer, Integer>() { + @Override + public Integer apply(Integer v) { + try { + Thread.sleep(10); + } catch (Exception e) { + e.printStackTrace(); + } + return v; + } + }) + .subscribe(ts); + + ts.awaitDone(5, TimeUnit.SECONDS); + System.out.println("testFirehoseFailsAsExpected => Received: " + ts.values().size() + " Emitted: " + c.get()); + + // FIXME it is possible slow is not slow enough or the main gets delayed and thus more than one source value is emitted. + int vc = ts.values().size(); + assertTrue("10 < " + vc, vc <= 10); + + ts.assertError(QueueOverflowException.class); + } + + @Test + public void firehoseFailsAsExpectedLoop() { + for (int i = 0; i < 100; i++) { + firehoseFailsAsExpected(); + } + } + + @Test + public void onBackpressureDrop() { + long t = System.currentTimeMillis(); + for (int i = 0; i < 100; i++) { + // stop the test if we are getting close to the timeout because slow machines + // may not get through 100 iterations + if (System.currentTimeMillis() - t > TimeUnit.SECONDS.toMillis(9)) { + break; + } + int num = (int) (Flowable.bufferSize() * 1.1); // > 1 so that take doesn't prevent buffer overflow + AtomicInteger c = new AtomicInteger(); + TestSubscriber<Integer> ts = new TestSubscriber<>(); + firehose(c).onBackpressureDrop() + .observeOn(Schedulers.computation()) + .map(SLOW_PASS_THRU).take(num).subscribe(ts); + ts.awaitDone(5, TimeUnit.SECONDS); + ts.assertNoErrors(); + + List<Integer> onNextEvents = ts.values(); + assertEquals(num, onNextEvents.size()); + + Integer lastEvent = onNextEvents.get(num - 1); + + System.out.println("testOnBackpressureDrop => Received: " + onNextEvents.size() + " Emitted: " + c.get() + " Last value: " + lastEvent); + // it drop, so we should get some number far higher than what would have sequentially incremented + assertTrue(num - 1 <= lastEvent.intValue()); + } + } + + @Test + public void onBackpressureDropWithAction() { + for (int i = 0; i < 100; i++) { + final AtomicInteger emitCount = new AtomicInteger(); + final AtomicInteger dropCount = new AtomicInteger(); + final AtomicInteger passCount = new AtomicInteger(); + final int num = Flowable.bufferSize() * 3; // > 1 so that take doesn't prevent buffer overflow + TestSubscriber<Integer> ts = new TestSubscriber<>(); + + firehose(emitCount) + .onBackpressureDrop(new Consumer<Integer>() { + @Override + public void accept(Integer v) { + dropCount.incrementAndGet(); + } + }) + .doOnNext(new Consumer<Integer>() { + @Override + public void accept(Integer v) { + passCount.incrementAndGet(); + } + }) + .observeOn(Schedulers.computation()) + .map(SLOW_PASS_THRU) + .take(num).subscribe(ts); + + ts.awaitDone(5, TimeUnit.SECONDS); + ts.assertNoErrors(); + + List<Integer> onNextEvents = ts.values(); + Integer lastEvent = onNextEvents.get(num - 1); + System.out.println(testName.getMethodName() + " => Received: " + onNextEvents.size() + " Passed: " + passCount.get() + " Dropped: " + dropCount.get() + " Emitted: " + emitCount.get() + " Last value: " + lastEvent); + assertEquals(num, onNextEvents.size()); + // in reality, num < passCount + assertTrue(num <= passCount.get()); + // it drop, so we should get some number far higher than what would have sequentially incremented + assertTrue(num - 1 <= lastEvent.intValue()); + assertTrue(0 < dropCount.get()); + assertEquals(emitCount.get(), passCount.get() + dropCount.get()); + } + } + + @Test + public void onBackpressureDropSynchronous() { + for (int i = 0; i < 100; i++) { + int num = (int) (Flowable.bufferSize() * 1.1); // > 1 so that take doesn't prevent buffer overflow + AtomicInteger c = new AtomicInteger(); + TestSubscriber<Integer> ts = new TestSubscriber<>(); + firehose(c).onBackpressureDrop() + .map(SLOW_PASS_THRU).take(num).subscribe(ts); + ts.awaitDone(5, TimeUnit.SECONDS); + ts.assertNoErrors(); + + List<Integer> onNextEvents = ts.values(); + assertEquals(num, onNextEvents.size()); + + Integer lastEvent = onNextEvents.get(num - 1); + + System.out.println("testOnBackpressureDrop => Received: " + onNextEvents.size() + " Emitted: " + c.get() + " Last value: " + lastEvent); + // it drop, so we should get some number far higher than what would have sequentially incremented + assertTrue(num - 1 <= lastEvent.intValue()); + } + } + + @Test + public void onBackpressureDropSynchronousWithAction() { + for (int i = 0; i < 100; i++) { + final AtomicInteger dropCount = new AtomicInteger(); + int num = (int) (Flowable.bufferSize() * 1.1); // > 1 so that take doesn't prevent buffer overflow + AtomicInteger c = new AtomicInteger(); + TestSubscriber<Integer> ts = new TestSubscriber<>(); + firehose(c).onBackpressureDrop(new Consumer<Integer>() { + @Override + public void accept(Integer j) { + dropCount.incrementAndGet(); + } + }) + .map(SLOW_PASS_THRU).take(num).subscribe(ts); + ts.awaitDone(5, TimeUnit.SECONDS); + ts.assertNoErrors(); + + List<Integer> onNextEvents = ts.values(); + assertEquals(num, onNextEvents.size()); + + Integer lastEvent = onNextEvents.get(num - 1); + + System.out.println("testOnBackpressureDrop => Received: " + onNextEvents.size() + " Dropped: " + dropCount.get() + " Emitted: " + c.get() + " Last value: " + lastEvent); + // it drop, so we should get some number far higher than what would have sequentially incremented + assertTrue(num - 1 <= lastEvent.intValue()); + // no drop in synchronous mode + assertEquals(0, dropCount.get()); + assertEquals(c.get(), onNextEvents.size()); + } + } + + @Test + public void onBackpressureBuffer() { + int num = (int) (Flowable.bufferSize() * 1.1); // > 1 so that take doesn't prevent buffer overflow + AtomicInteger c = new AtomicInteger(); + TestSubscriber<Integer> ts = new TestSubscriber<>(); + + firehose(c).takeWhile(new Predicate<Integer>() { + @Override + public boolean test(Integer t1) { + return t1 < 100000; + } + }) + .onBackpressureBuffer() + .observeOn(Schedulers.computation()) + .map(SLOW_PASS_THRU).take(num).subscribe(ts); + + ts.awaitDone(5, TimeUnit.SECONDS); + ts.assertNoErrors(); + System.out.println("testOnBackpressureBuffer => Received: " + ts.values().size() + " Emitted: " + c.get()); + assertEquals(num, ts.values().size()); + // it buffers, so we should get the right value sequentially + assertEquals(num - 1, ts.values().get(num - 1).intValue()); + } + + /** + * A synchronous Flowable that will emit incrementing integers as requested. + * + * @param counter the shared value to be incremented + * @return the incrementing Flowable instance + */ + private static Flowable<Integer> incrementingIntegers(final AtomicInteger counter) { + return incrementingIntegers(counter, null); + } + + private static Flowable<Integer> incrementingIntegers(final AtomicInteger counter, final ConcurrentLinkedQueue<Thread> threadsSeen) { + return Flowable.unsafeCreate(new Publisher<Integer>() { + + @Override + public void subscribe(final Subscriber<? super Integer> s) { + s.onSubscribe(new Subscription() { + int i; + + volatile boolean cancelled; + + final AtomicLong requested = new AtomicLong(); + + @Override + public void request(long n) { + if (!SubscriptionHelper.validate(n)) { + return; + } + if (threadsSeen != null) { + threadsSeen.offer(Thread.currentThread()); + } + long c = BackpressureHelper.add(requested, n); + if (c == 0) { + while (!cancelled) { + counter.incrementAndGet(); + s.onNext(i++); + if (requested.decrementAndGet() == 0) { + // we're done emitting the number requested so return + return; + } + } + } + } + + @Override + public void cancel() { + cancelled = true; + } + }); + } + + }); + } + + /** + * Incrementing int without backpressure. + * + * @param counter the shared value to increment + * @return the Flowable doing the increments + */ + private static Flowable<Integer> firehose(final AtomicInteger counter) { + return Flowable.unsafeCreate(new Publisher<Integer>() { + @Override + public void subscribe(Subscriber<? super Integer> s) { + Subscription s2 = new FirehoseNoBackpressure(counter, s); + s.onSubscribe(s2); + } + }); + } + + static final Function<Integer, Integer> SLOW_PASS_THRU = new Function<Integer, Integer>() { + volatile int sink; + @Override + public Integer apply(Integer t1) { + // be slow ... but faster than Thread.sleep(1) + String t = ""; + int s = sink; + for (int i = 2000; i >= 0; i--) { + t = String.valueOf(i + t.hashCode() + s); + } + sink = t.hashCode(); + return t1; + } + + }; +} diff --git a/src/test/java/io/reactivex/rxjava3/flowable/FlowableCollectTest.java b/src/test/java/io/reactivex/rxjava3/flowable/FlowableCollectTest.java new file mode 100644 index 0000000000..7077995982 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/flowable/FlowableCollectTest.java @@ -0,0 +1,377 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.flowable; + +import static io.reactivex.rxjava3.internal.util.TestingHelper.*; +import static org.junit.Assert.*; + +import java.util.*; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public final class FlowableCollectTest extends RxJavaTest { + + @Test + public void collectToListFlowable() { + Flowable<List<Integer>> f = Flowable.just(1, 2, 3) + .collect(new Supplier<List<Integer>>() { + @Override + public List<Integer> get() { + return new ArrayList<>(); + } + }, new BiConsumer<List<Integer>, Integer>() { + @Override + public void accept(List<Integer> list, Integer v) { + list.add(v); + } + }).toFlowable(); + + List<Integer> list = f.blockingLast(); + + assertEquals(3, list.size()); + assertEquals(1, list.get(0).intValue()); + assertEquals(2, list.get(1).intValue()); + assertEquals(3, list.get(2).intValue()); + + // test multiple subscribe + List<Integer> list2 = f.blockingLast(); + + assertEquals(3, list2.size()); + assertEquals(1, list2.get(0).intValue()); + assertEquals(2, list2.get(1).intValue()); + assertEquals(3, list2.get(2).intValue()); + } + + @Test + public void collectToStringFlowable() { + String value = Flowable.just(1, 2, 3) + .collect( + new Supplier<StringBuilder>() { + @Override + public StringBuilder get() { + return new StringBuilder(); + } + }, + new BiConsumer<StringBuilder, Integer>() { + @Override + public void accept(StringBuilder sb, Integer v) { + if (sb.length() > 0) { + sb.append("-"); + } + sb.append(v); + } + }).toFlowable().blockingLast().toString(); + + assertEquals("1-2-3", value); + } + + @Test + public void factoryFailureResultsInErrorEmissionFlowable() { + final RuntimeException e = new RuntimeException(); + Flowable.just(1).collect(new Supplier<List<Integer>>() { + + @Override + public List<Integer> get() throws Exception { + throw e; + } + }, new BiConsumer<List<Integer>, Integer>() { + + @Override + public void accept(List<Integer> list, Integer t) { + list.add(t); + } + }) + .test() + .assertNoValues() + .assertError(e) + .assertNotComplete(); + } + + @Test + public void collectorFailureDoesNotResultInTwoErrorEmissionsFlowable() { + try { + final List<Throwable> list = new CopyOnWriteArrayList<>(); + RxJavaPlugins.setErrorHandler(addToList(list)); + final RuntimeException e1 = new RuntimeException(); + final RuntimeException e2 = new RuntimeException(); + + Burst.items(1).error(e2) // + .collect(supplierListCreator(), biConsumerThrows(e1)) + .toFlowable() + .test() // + .assertError(e1) // + .assertNotComplete(); + + assertEquals(1, list.size()); + assertEquals(e2, list.get(0).getCause()); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void collectorFailureDoesNotResultInErrorAndCompletedEmissionsFlowable() { + final RuntimeException e = new RuntimeException(); + Burst.item(1).create() // + .collect(supplierListCreator(), biConsumerThrows(e)) // + .toFlowable() + .test() // + .assertError(e) // + .assertNotComplete(); + } + + @Test + public void collectorFailureDoesNotResultInErrorAndOnNextEmissionsFlowable() { + final RuntimeException e = new RuntimeException(); + final AtomicBoolean added = new AtomicBoolean(); + BiConsumer<Object, Integer> throwOnFirstOnly = new BiConsumer<Object, Integer>() { + + boolean once = true; + + @Override + public void accept(Object o, Integer t) { + if (once) { + once = false; + throw e; + } else { + added.set(true); + } + } + }; + Burst.items(1, 2).create() // + .collect(supplierListCreator(), throwOnFirstOnly)// + .toFlowable() + .test() // + .assertError(e) // + .assertNoValues() // + .assertNotComplete(); + assertFalse(added.get()); + } + + @Test + public void collectIntoFlowable() { + Flowable.just(1, 1, 1, 1, 2) + .collectInto(new HashSet<>(), new BiConsumer<HashSet<Integer>, Integer>() { + @Override + public void accept(HashSet<Integer> s, Integer v) throws Exception { + s.add(v); + } + }) + .toFlowable() + .test() + .assertResult(new HashSet<>(Arrays.asList(1, 2))); + } + + @Test + public void collectToList() { + Single<List<Integer>> o = Flowable.just(1, 2, 3) + .collect(new Supplier<List<Integer>>() { + @Override + public List<Integer> get() { + return new ArrayList<>(); + } + }, new BiConsumer<List<Integer>, Integer>() { + @Override + public void accept(List<Integer> list, Integer v) { + list.add(v); + } + }); + + List<Integer> list = o.blockingGet(); + + assertEquals(3, list.size()); + assertEquals(1, list.get(0).intValue()); + assertEquals(2, list.get(1).intValue()); + assertEquals(3, list.get(2).intValue()); + + // test multiple subscribe + List<Integer> list2 = o.blockingGet(); + + assertEquals(3, list2.size()); + assertEquals(1, list2.get(0).intValue()); + assertEquals(2, list2.get(1).intValue()); + assertEquals(3, list2.get(2).intValue()); + } + + @Test + public void collectToString() { + String value = Flowable.just(1, 2, 3) + .collect( + new Supplier<StringBuilder>() { + @Override + public StringBuilder get() { + return new StringBuilder(); + } + }, + new BiConsumer<StringBuilder, Integer>() { + @Override + public void accept(StringBuilder sb, Integer v) { + if (sb.length() > 0) { + sb.append("-"); + } + sb.append(v); + } + }).blockingGet().toString(); + + assertEquals("1-2-3", value); + } + + @Test + public void factoryFailureResultsInErrorEmission() { + final RuntimeException e = new RuntimeException(); + Flowable.just(1).collect(new Supplier<List<Integer>>() { + + @Override + public List<Integer> get() throws Exception { + throw e; + } + }, new BiConsumer<List<Integer>, Integer>() { + + @Override + public void accept(List<Integer> list, Integer t) { + list.add(t); + } + }) + .test() + .assertNoValues() + .assertError(e) + .assertNotComplete(); + } + + @Test + public void collectorFailureDoesNotResultInTwoErrorEmissions() { + try { + final List<Throwable> list = new CopyOnWriteArrayList<>(); + RxJavaPlugins.setErrorHandler(addToList(list)); + final RuntimeException e1 = new RuntimeException(); + final RuntimeException e2 = new RuntimeException(); + + Burst.items(1).error(e2) // + .collect(supplierListCreator(), biConsumerThrows(e1)) // + .test() // + .assertError(e1) // + .assertNotComplete(); + + assertEquals(1, list.size()); + assertEquals(e2, list.get(0).getCause()); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void collectorFailureDoesNotResultInErrorAndCompletedEmissions() { + final RuntimeException e = new RuntimeException(); + Burst.item(1).create() // + .collect(supplierListCreator(), biConsumerThrows(e)) // + .test() // + .assertError(e) // + .assertNotComplete(); + } + + @Test + public void collectorFailureDoesNotResultInErrorAndOnNextEmissions() { + final RuntimeException e = new RuntimeException(); + final AtomicBoolean added = new AtomicBoolean(); + BiConsumer<Object, Integer> throwOnFirstOnly = new BiConsumer<Object, Integer>() { + + boolean once = true; + + @Override + public void accept(Object o, Integer t) { + if (once) { + once = false; + throw e; + } else { + added.set(true); + } + } + }; + Burst.items(1, 2).create() // + .collect(supplierListCreator(), throwOnFirstOnly)// + .test() // + .assertError(e) // + .assertNoValues() // + .assertNotComplete(); + assertFalse(added.get()); + } + + @Test + public void collectInto() { + Flowable.just(1, 1, 1, 1, 2) + .collectInto(new HashSet<>(), new BiConsumer<HashSet<Integer>, Integer>() { + @Override + public void accept(HashSet<Integer> s, Integer v) throws Exception { + s.add(v); + } + }) + .test() + .assertResult(new HashSet<>(Arrays.asList(1, 2))); + } + + @Test + public void dispose() { + TestHelper.checkDisposed(Flowable.just(1, 2) + .collect(Functions.justSupplier(new ArrayList<>()), new BiConsumer<ArrayList<Integer>, Integer>() { + @Override + public void accept(ArrayList<Integer> a, Integer b) throws Exception { + a.add(b); + } + })); + + TestHelper.checkDisposed(Flowable.just(1, 2) + .collect(Functions.justSupplier(new ArrayList<>()), new BiConsumer<ArrayList<Integer>, Integer>() { + @Override + public void accept(ArrayList<Integer> a, Integer b) throws Exception { + a.add(b); + } + }).toFlowable()); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeFlowable(new Function<Flowable<Integer>, Flowable<ArrayList<Integer>>>() { + @Override + public Flowable<ArrayList<Integer>> apply(Flowable<Integer> f) throws Exception { + return f.collect(Functions.justSupplier(new ArrayList<>()), + new BiConsumer<ArrayList<Integer>, Integer>() { + @Override + public void accept(ArrayList<Integer> a, Integer b) throws Exception { + a.add(b); + } + }).toFlowable(); + } + }); + TestHelper.checkDoubleOnSubscribeFlowableToSingle(new Function<Flowable<Integer>, Single<ArrayList<Integer>>>() { + @Override + public Single<ArrayList<Integer>> apply(Flowable<Integer> f) throws Exception { + return f.collect(Functions.justSupplier(new ArrayList<>()), + new BiConsumer<ArrayList<Integer>, Integer>() { + @Override + public void accept(ArrayList<Integer> a, Integer b) throws Exception { + a.add(b); + } + }); + } + }); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/flowable/FlowableCombineLatestTests.java b/src/test/java/io/reactivex/rxjava3/flowable/FlowableCombineLatestTests.java new file mode 100644 index 0000000000..107c5e583f --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/flowable/FlowableCombineLatestTests.java @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.flowable; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.flowable.FlowableCovarianceTest.*; +import io.reactivex.rxjava3.functions.*; + +public class FlowableCombineLatestTests extends RxJavaTest { + /** + * This won't compile if super/extends isn't done correctly on generics. + */ + @Test + public void covarianceOfCombineLatest() { + Flowable<HorrorMovie> horrors = Flowable.just(new HorrorMovie()); + Flowable<CoolRating> ratings = Flowable.just(new CoolRating()); + + Flowable.<Movie, CoolRating, Result> combineLatest(horrors, ratings, combine).blockingForEach(action); + Flowable.<Movie, CoolRating, Result> combineLatest(horrors, ratings, combine).blockingForEach(action); + Flowable.<Media, Rating, ExtendedResult> combineLatest(horrors, ratings, combine).blockingForEach(extendedAction); + Flowable.<Media, Rating, Result> combineLatest(horrors, ratings, combine).blockingForEach(action); + Flowable.<Media, Rating, ExtendedResult> combineLatest(horrors, ratings, combine).blockingForEach(action); + + Flowable.<Movie, CoolRating, Result> combineLatest(horrors, ratings, combine); + } + + BiFunction<Media, Rating, ExtendedResult> combine = new BiFunction<Media, Rating, ExtendedResult>() { + @Override + public ExtendedResult apply(Media m, Rating r) { + return new ExtendedResult(); + } + }; + + Consumer<Result> action = new Consumer<Result>() { + @Override + public void accept(Result t1) { + System.out.println("Result: " + t1); + } + }; + + Consumer<ExtendedResult> extendedAction = new Consumer<ExtendedResult>() { + @Override + public void accept(ExtendedResult t1) { + System.out.println("Result: " + t1); + } + }; +} diff --git a/src/test/java/io/reactivex/rxjava3/flowable/FlowableConcatTests.java b/src/test/java/io/reactivex/rxjava3/flowable/FlowableConcatTests.java new file mode 100644 index 0000000000..5efb4195e6 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/flowable/FlowableConcatTests.java @@ -0,0 +1,167 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.flowable; + +import static org.junit.Assert.assertEquals; + +import java.util.*; + +import org.junit.Test; +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.flowable.FlowableCovarianceTest.*; + +public class FlowableConcatTests extends RxJavaTest { + + @Test + public void concatSimple() { + Flowable<String> f1 = Flowable.just("one", "two"); + Flowable<String> f2 = Flowable.just("three", "four"); + + List<String> values = Flowable.concat(f1, f2).toList().blockingGet(); + + assertEquals("one", values.get(0)); + assertEquals("two", values.get(1)); + assertEquals("three", values.get(2)); + assertEquals("four", values.get(3)); + } + + @Test + public void concatWithFlowableOfFlowable() { + Flowable<String> f1 = Flowable.just("one", "two"); + Flowable<String> f2 = Flowable.just("three", "four"); + Flowable<String> f3 = Flowable.just("five", "six"); + + Flowable<Flowable<String>> os = Flowable.just(f1, f2, f3); + + List<String> values = Flowable.concat(os).toList().blockingGet(); + + assertEquals("one", values.get(0)); + assertEquals("two", values.get(1)); + assertEquals("three", values.get(2)); + assertEquals("four", values.get(3)); + assertEquals("five", values.get(4)); + assertEquals("six", values.get(5)); + } + + @Test + public void concatWithIterableOfFlowable() { + Flowable<String> f1 = Flowable.just("one", "two"); + Flowable<String> f2 = Flowable.just("three", "four"); + Flowable<String> f3 = Flowable.just("five", "six"); + + Iterable<Flowable<String>> is = Arrays.asList(f1, f2, f3); + + List<String> values = Flowable.concat(Flowable.fromIterable(is)).toList().blockingGet(); + + assertEquals("one", values.get(0)); + assertEquals("two", values.get(1)); + assertEquals("three", values.get(2)); + assertEquals("four", values.get(3)); + assertEquals("five", values.get(4)); + assertEquals("six", values.get(5)); + } + + @Test + public void concatCovariance() { + HorrorMovie horrorMovie1 = new HorrorMovie(); + Movie movie = new Movie(); + Media media = new Media(); + HorrorMovie horrorMovie2 = new HorrorMovie(); + + Flowable<Media> f1 = Flowable.<Media> just(horrorMovie1, movie); + Flowable<Media> f2 = Flowable.just(media, horrorMovie2); + + Flowable<Flowable<Media>> os = Flowable.just(f1, f2); + + List<Media> values = Flowable.concat(os).toList().blockingGet(); + + assertEquals(horrorMovie1, values.get(0)); + assertEquals(movie, values.get(1)); + assertEquals(media, values.get(2)); + assertEquals(horrorMovie2, values.get(3)); + assertEquals(4, values.size()); + } + + @Test + public void concatCovariance2() { + HorrorMovie horrorMovie1 = new HorrorMovie(); + Movie movie = new Movie(); + Media media1 = new Media(); + Media media2 = new Media(); + HorrorMovie horrorMovie2 = new HorrorMovie(); + + Flowable<Media> f1 = Flowable.just(horrorMovie1, movie, media1); + Flowable<Media> f2 = Flowable.just(media2, horrorMovie2); + + Flowable<Flowable<Media>> os = Flowable.just(f1, f2); + + List<Media> values = Flowable.concat(os).toList().blockingGet(); + + assertEquals(horrorMovie1, values.get(0)); + assertEquals(movie, values.get(1)); + assertEquals(media1, values.get(2)); + assertEquals(media2, values.get(3)); + assertEquals(horrorMovie2, values.get(4)); + assertEquals(5, values.size()); + } + + @Test + public void concatCovariance3() { + HorrorMovie horrorMovie1 = new HorrorMovie(); + Movie movie = new Movie(); + Media media = new Media(); + HorrorMovie horrorMovie2 = new HorrorMovie(); + + Flowable<Movie> f1 = Flowable.just(horrorMovie1, movie); + Flowable<Media> f2 = Flowable.just(media, horrorMovie2); + + List<Media> values = Flowable.concat(f1, f2).toList().blockingGet(); + + assertEquals(horrorMovie1, values.get(0)); + assertEquals(movie, values.get(1)); + assertEquals(media, values.get(2)); + assertEquals(horrorMovie2, values.get(3)); + assertEquals(4, values.size()); + } + + @Test + public void concatCovariance4() { + final HorrorMovie horrorMovie1 = new HorrorMovie(); + final Movie movie = new Movie(); + Media media = new Media(); + HorrorMovie horrorMovie2 = new HorrorMovie(); + + Flowable<Movie> f1 = Flowable.unsafeCreate(new Publisher<Movie>() { + @Override + public void subscribe(Subscriber<? super Movie> subscriber) { + subscriber.onNext(horrorMovie1); + subscriber.onNext(movie); + // o.onNext(new Media()); // correctly doesn't compile + subscriber.onComplete(); + } + }); + + Flowable<Media> f2 = Flowable.just(media, horrorMovie2); + + List<Media> values = Flowable.concat(f1, f2).toList().blockingGet(); + + assertEquals(horrorMovie1, values.get(0)); + assertEquals(movie, values.get(1)); + assertEquals(media, values.get(2)); + assertEquals(horrorMovie2, values.get(3)); + assertEquals(4, values.size()); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/flowable/FlowableConversionTest.java b/src/test/java/io/reactivex/rxjava3/flowable/FlowableConversionTest.java new file mode 100644 index 0000000000..7079f833f2 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/flowable/FlowableConversionTest.java @@ -0,0 +1,261 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.flowable; + +import java.util.*; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.atomic.*; + +import org.junit.*; +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.operators.flowable.*; +import io.reactivex.rxjava3.internal.util.ExceptionHelper; +import io.reactivex.rxjava3.observers.*; +import io.reactivex.rxjava3.schedulers.Schedulers; +import io.reactivex.rxjava3.subscribers.DefaultSubscriber; + +public class FlowableConversionTest extends RxJavaTest { + + public static class Cylon { } + + public static class Jail { + Object cylon; + + Jail(Object cylon) { + this.cylon = cylon; + } + } + + public static class CylonDetectorObservable<T> { + protected Publisher<T> onSubscribe; + + public static <T> CylonDetectorObservable<T> create(Publisher<T> onSubscribe) { + return new CylonDetectorObservable<>(onSubscribe); + } + + protected CylonDetectorObservable(Publisher<T> onSubscribe) { + this.onSubscribe = onSubscribe; + } + + public void subscribe(Subscriber<T> subscriber) { + onSubscribe.subscribe(subscriber); + } + + public <R> CylonDetectorObservable<R> lift(FlowableOperator<? extends R, ? super T> operator) { + return x(new RobotConversionFunc<>(operator)); + } + + public <O> O x(Function<Publisher<T>, O> operator) { + try { + return operator.apply(onSubscribe); + } catch (Throwable ex) { + throw ExceptionHelper.wrapOrThrow(ex); + } + } + + public <R> CylonDetectorObservable<? extends R> compose(Function<CylonDetectorObservable<? super T>, CylonDetectorObservable<? extends R>> transformer) { + try { + return transformer.apply(this); + } catch (Throwable ex) { + throw ExceptionHelper.wrapOrThrow(ex); + } + } + + public final CylonDetectorObservable<T> beep(Predicate<? super T> predicate) { + return new CylonDetectorObservable<>(new FlowableFilter<>(Flowable.fromPublisher(onSubscribe), predicate)); + } + + public final <R> CylonDetectorObservable<R> boop(Function<? super T, ? extends R> func) { + return new CylonDetectorObservable<>(new FlowableMap<>(Flowable.fromPublisher(onSubscribe), func)); + } + + public CylonDetectorObservable<String> DESTROY() { + return boop(new Function<T, String>() { + @Override + public String apply(T t) { + Object cylon = ((Jail) t).cylon; + throwOutTheAirlock(cylon); + if (t instanceof Jail) { + String name = cylon.toString(); + return "Cylon '" + name + "' has been destroyed"; + } + else { + return "Cylon 'anonymous' has been destroyed"; + } + }}); + } + + private static void throwOutTheAirlock(Object cylon) { + // ... + } + } + + public static class RobotConversionFunc<T, R> implements Function<Publisher<T>, CylonDetectorObservable<R>> { + private FlowableOperator<? extends R, ? super T> operator; + + public RobotConversionFunc(FlowableOperator<? extends R, ? super T> operator) { + this.operator = operator; + } + + @Override + public CylonDetectorObservable<R> apply(final Publisher<T> onSubscribe) { + return CylonDetectorObservable.create(new Publisher<R>() { + @Override + public void subscribe(Subscriber<? super R> subscriber) { + try { + Subscriber<? super T> st = operator.apply(subscriber); + try { + onSubscribe.subscribe(st); + } catch (Throwable e) { + st.onError(e); + } + } catch (Throwable e) { + subscriber.onError(e); + } + + }}); + } + } + + public static class ConvertToCylonDetector<T> implements FlowableConverter<T, CylonDetectorObservable<T>> { + @Override + public CylonDetectorObservable<T> apply(final Flowable<T> onSubscribe) { + return CylonDetectorObservable.create(onSubscribe); + } + } + + public static class ConvertToObservable<T> implements Function<Publisher<T>, Flowable<T>> { + @Override + public Flowable<T> apply(final Publisher<T> onSubscribe) { + return Flowable.fromPublisher(onSubscribe); + } + } + + @Test + public void conversionBetweenObservableClasses() { + final TestObserver<String> to = new TestObserver<>(new DefaultObserver<String>() { + + @Override + public void onComplete() { + System.out.println("Complete"); + } + + @Override + public void onError(Throwable e) { + System.out.println("error: " + e.getMessage()); + e.printStackTrace(); + } + + @Override + public void onNext(String t) { + System.out.println(t); + } + }); + + List<Object> crewOfBattlestarGalactica = Arrays.asList(new Object[] {"William Adama", "Laura Roslin", "Lee Adama", new Cylon()}); + + Flowable.fromIterable(crewOfBattlestarGalactica) + .doOnNext(new Consumer<Object>() { + @Override + public void accept(Object pv) { + System.out.println(pv); + } + }) + .to(new ConvertToCylonDetector<>()) + .beep(new Predicate<Object>() { + @Override + public boolean test(Object t) { + return t instanceof Cylon; + } + }) + .boop(new Function<Object, Object>() { + @Override + public Object apply(Object cylon) { + return new Jail(cylon); + } + }) + .DESTROY() + .x(new ConvertToObservable<>()) + .reduce("Cylon Detector finished. Report:\n", new BiFunction<String, String, String>() { + @Override + public String apply(String a, String n) { + return a + n + "\n"; + } + }) + .subscribe(to); + + to.assertNoErrors(); + to.assertComplete(); + } + + @Test + public void convertToConcurrentQueue() { + final AtomicReference<Throwable> thrown = new AtomicReference<>(null); + final AtomicBoolean isFinished = new AtomicBoolean(false); + ConcurrentLinkedQueue<? extends Integer> queue = Flowable.range(0, 5) + .flatMap(new Function<Integer, Publisher<Integer>>() { + @Override + public Publisher<Integer> apply(final Integer i) { + return Flowable.range(0, 5) + .observeOn(Schedulers.io()) + .map(new Function<Integer, Integer>() { + @Override + public Integer apply(Integer k) { + try { + Thread.sleep(System.currentTimeMillis() % 100); + } catch (InterruptedException e) { + e.printStackTrace(); + } + return i + k; + } + }); + } + }) + .to(new FlowableConverter<Integer, ConcurrentLinkedQueue<Integer>>() { + @Override + public ConcurrentLinkedQueue<Integer> apply(Flowable<Integer> onSubscribe) { + final ConcurrentLinkedQueue<Integer> q = new ConcurrentLinkedQueue<>(); + onSubscribe.subscribe(new DefaultSubscriber<Integer>() { + @Override + public void onComplete() { + isFinished.set(true); + } + + @Override + public void onError(Throwable e) { + thrown.set(e); + } + + @Override + public void onNext(Integer t) { + q.add(t); + }}); + return q; + } + }); + + int x = 0; + while (!isFinished.get()) { + Integer i = queue.poll(); + if (i != null) { + x++; + System.out.println(x + " item: " + i); + } + } + Assert.assertNull(thrown.get()); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/flowable/FlowableCovarianceTest.java b/src/test/java/io/reactivex/rxjava3/flowable/FlowableCovarianceTest.java new file mode 100644 index 0000000000..9d7ca31319 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/flowable/FlowableCovarianceTest.java @@ -0,0 +1,252 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.flowable; + +import static org.junit.Assert.assertEquals; + +import java.util.*; + +import org.junit.Test; +import org.reactivestreams.Publisher; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.flowables.GroupedFlowable; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.testsupport.TestSubscriberEx; + +/** + * Test super/extends of generics. + * + * See https://github.com/Netflix/RxJava/pull/331 + */ +public class FlowableCovarianceTest extends RxJavaTest { + + /** + * This won't compile if super/extends isn't done correctly on generics. + */ + @Test + public void covarianceOfFrom() { + Flowable.<Movie> just(new HorrorMovie()); + Flowable.<Movie> fromIterable(new ArrayList<HorrorMovie>()); + // Observable.<HorrorMovie>from(new Movie()); // may not compile + } + + @Test + public void sortedList() { + Comparator<Media> sortFunction = new Comparator<Media>() { + @Override + public int compare(Media t1, Media t2) { + return 1; + } + }; + + // this one would work without the covariance generics + Flowable<Media> f = Flowable.just(new Movie(), new TVSeason(), new Album()); + f.toSortedList(sortFunction); + + // this one would NOT work without the covariance generics + Flowable<Movie> f2 = Flowable.just(new Movie(), new ActionMovie(), new HorrorMovie()); + f2.toSortedList(sortFunction); + } + + @Test + public void groupByCompose() { + Flowable<Movie> movies = Flowable.just(new HorrorMovie(), new ActionMovie(), new Movie()); + TestSubscriberEx<String> ts = new TestSubscriberEx<>(); + + movies + .groupBy(new Function<Movie, Object>() { + @Override + public Object apply(Movie v) { + return v.getClass(); + } + }) + .doOnNext(new Consumer<GroupedFlowable<Object, Movie>>() { + @Override + public void accept(GroupedFlowable<Object, Movie> g) { + System.out.println(g.getKey()); + } + }) + .flatMap(new Function<GroupedFlowable<Object, Movie>, Publisher<String>>() { + @Override + public Publisher<String> apply(GroupedFlowable<Object, Movie> g) { + return g + .doOnNext(new Consumer<Movie>() { + @Override + public void accept(Movie v) { + System.out.println(v); + } + }) + .compose(new FlowableTransformer<Movie, Movie>() { + @Override + public Publisher<Movie> apply(Flowable<Movie> m) { + return m.concatWith(Flowable.just(new ActionMovie())); + } + } + ) + .map(new Function<Object, String>() { + @Override + public String apply(Object v) { + return v.toString(); + } + }); + } + }) + .subscribe(ts); + ts.assertTerminated(); + ts.assertNoErrors(); + // System.out.println(ts.getOnNextEvents()); + assertEquals(6, ts.values().size()); + } + + @SuppressWarnings("unused") + @Test + public void covarianceOfCompose() { + Flowable<HorrorMovie> movie = Flowable.just(new HorrorMovie()); + Flowable<Movie> movie2 = movie.compose(new FlowableTransformer<HorrorMovie, Movie>() { + @Override + public Publisher<Movie> apply(Flowable<HorrorMovie> t) { + return Flowable.just(new Movie()); + } + }); + } + + @SuppressWarnings("unused") + @Test + public void covarianceOfCompose2() { + Flowable<Movie> movie = Flowable.<Movie> just(new HorrorMovie()); + Flowable<HorrorMovie> movie2 = movie.compose(new FlowableTransformer<Movie, HorrorMovie>() { + @Override + public Publisher<HorrorMovie> apply(Flowable<Movie> t) { + return Flowable.just(new HorrorMovie()); + } + }); + } + + @SuppressWarnings("unused") + @Test + public void covarianceOfCompose3() { + Flowable<Movie> movie = Flowable.<Movie>just(new HorrorMovie()); + Flowable<HorrorMovie> movie2 = movie.compose(new FlowableTransformer<Movie, HorrorMovie>() { + @Override + public Publisher<HorrorMovie> apply(Flowable<Movie> t) { + return Flowable.just(new HorrorMovie()).map(new Function<HorrorMovie, HorrorMovie>() { + @Override + public HorrorMovie apply(HorrorMovie v) { + return v; + } + }); + } + } + ); + } + + @SuppressWarnings("unused") + @Test + public void covarianceOfCompose4() { + Flowable<HorrorMovie> movie = Flowable.just(new HorrorMovie()); + Flowable<HorrorMovie> movie2 = movie.compose(new FlowableTransformer<HorrorMovie, HorrorMovie>() { + @Override + public Publisher<HorrorMovie> apply(Flowable<HorrorMovie> t1) { + return t1.map(new Function<HorrorMovie, HorrorMovie>() { + @Override + public HorrorMovie apply(HorrorMovie v) { + return v; + } + }); + } + }); + } + + @Test + public void composeWithDeltaLogic() { + List<Movie> list1 = Arrays.asList(new Movie(), new HorrorMovie(), new ActionMovie()); + List<Movie> list2 = Arrays.asList(new ActionMovie(), new Movie(), new HorrorMovie(), new ActionMovie()); + Flowable<List<Movie>> movies = Flowable.just(list1, list2); + movies.compose(deltaTransformer); + } + + static Function<List<List<Movie>>, Flowable<Movie>> calculateDelta = new Function<List<List<Movie>>, Flowable<Movie>>() { + @Override + public Flowable<Movie> apply(List<List<Movie>> listOfLists) { + if (listOfLists.size() == 1) { + return Flowable.fromIterable(listOfLists.get(0)); + } else { + // diff the two + List<Movie> newList = listOfLists.get(1); + List<Movie> oldList = new ArrayList<>(listOfLists.get(0)); + + Set<Movie> delta = new LinkedHashSet<>(); + delta.addAll(newList); + // remove all that match in old + delta.removeAll(oldList); + + // filter oldList to those that aren't in the newList + oldList.removeAll(newList); + + // for all left in the oldList we'll create DROP events + for (@SuppressWarnings("unused") Movie old : oldList) { + delta.add(new Movie()); + } + + return Flowable.fromIterable(delta); + } + } + }; + + static FlowableTransformer<List<Movie>, Movie> deltaTransformer = new FlowableTransformer<List<Movie>, Movie>() { + @Override + public Publisher<Movie> apply(Flowable<List<Movie>> movieList) { + return movieList + .startWithItem(new ArrayList<>()) + .buffer(2, 1) + .skip(1) + .flatMap(calculateDelta); + } + }; + + /* + * Most tests are moved into their applicable classes such as [Operator]Tests.java + */ + + static class Media { + } + + static class Movie extends Media { + } + + static class HorrorMovie extends Movie { + } + + static class ActionMovie extends Movie { + } + + static class Album extends Media { + } + + static class TVSeason extends Media { + } + + static class Rating { + } + + static class CoolRating extends Rating { + } + + static class Result { + } + + static class ExtendedResult extends Result { + } +} diff --git a/src/test/java/io/reactivex/rxjava3/flowable/FlowableDoAfterNextTest.java b/src/test/java/io/reactivex/rxjava3/flowable/FlowableDoAfterNextTest.java new file mode 100644 index 0000000000..eb6878edbd --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/flowable/FlowableDoAfterNextTest.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.flowable; + +import static org.junit.Assert.assertEquals; + +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.RxJavaTest; +import io.reactivex.rxjava3.functions.Consumer; + +public class FlowableDoAfterNextTest extends RxJavaTest { + + @Test + public void ifFunctionThrowsThatNoMoreEventsAreProcessed() { + final AtomicInteger count = new AtomicInteger(); + final RuntimeException e = new RuntimeException(); + Burst.items(1, 2).create() + .doAfterNext(new Consumer<Integer>() { + @Override + public void accept(Integer t) throws Exception { + count.incrementAndGet(); + throw e; + }}) + .test() + .assertError(e) + .assertValue(1); + assertEquals(1, count.get()); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/flowable/FlowableDoOnTest.java b/src/test/java/io/reactivex/rxjava3/flowable/FlowableDoOnTest.java new file mode 100644 index 0000000000..f4ff588d53 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/flowable/FlowableDoOnTest.java @@ -0,0 +1,105 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.flowable; + +import static org.junit.Assert.*; + +import java.util.concurrent.atomic.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.*; + +public class FlowableDoOnTest extends RxJavaTest { + + @Test + public void doOnEach() { + final AtomicReference<String> r = new AtomicReference<>(); + String output = Flowable.just("one").doOnNext(new Consumer<String>() { + @Override + public void accept(String v) { + r.set(v); + } + }).blockingSingle(); + + assertEquals("one", output); + assertEquals("one", r.get()); + } + + @Test + public void doOnError() { + final AtomicReference<Throwable> r = new AtomicReference<>(); + Throwable t = null; + try { + Flowable.<String> error(new RuntimeException("an error")) + .doOnError(new Consumer<Throwable>() { + @Override + public void accept(Throwable v) { + r.set(v); + } + }).blockingSingle(); + fail("expected exception, not a return value"); + } catch (Throwable e) { + t = e; + } + + assertNotNull(t); + assertEquals(t, r.get()); + } + + @Test + public void doOnCompleted() { + final AtomicBoolean r = new AtomicBoolean(); + String output = Flowable.just("one").doOnComplete(new Action() { + @Override + public void run() { + r.set(true); + } + }).blockingSingle(); + + assertEquals("one", output); + assertTrue(r.get()); + } + + @Test + public void doOnTerminateError() { + final AtomicBoolean r = new AtomicBoolean(); + Flowable.<String>error(new TestException()).doOnTerminate(new Action() { + @Override + public void run() { + r.set(true); + } + }) + .test() + .assertFailure(TestException.class); + assertTrue(r.get()); + } + + @Test + public void doOnTerminateComplete() { + final AtomicBoolean r = new AtomicBoolean(); + String output = Flowable.just("one").doOnTerminate(new Action() { + @Override + public void run() { + r.set(true); + } + }).blockingSingle(); + + assertEquals("one", output); + assertTrue(r.get()); + + } +} diff --git a/src/test/java/io/reactivex/rxjava3/flowable/FlowableErrorHandlingTests.java b/src/test/java/io/reactivex/rxjava3/flowable/FlowableErrorHandlingTests.java new file mode 100644 index 0000000000..e15c7a1dec --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/flowable/FlowableErrorHandlingTests.java @@ -0,0 +1,103 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.flowable; + +import static org.junit.Assert.assertNotNull; + +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicReference; + +import org.junit.Test; +import org.reactivestreams.Subscriber; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.schedulers.Schedulers; +import io.reactivex.rxjava3.subscribers.DefaultSubscriber; + +public class FlowableErrorHandlingTests extends RxJavaTest { + + /** + * Test that an error from a user provided Observer.onNext + * is handled and emitted to the onError. + * @throws InterruptedException if the test is interrupted + */ + @Test + public void onNextError() throws InterruptedException { + final CountDownLatch latch = new CountDownLatch(1); + final AtomicReference<Throwable> caughtError = new AtomicReference<>(); + Flowable<Long> f = Flowable.interval(50, TimeUnit.MILLISECONDS); + Subscriber<Long> subscriber = new DefaultSubscriber<Long>() { + + @Override + public void onComplete() { + System.out.println("completed"); + latch.countDown(); + } + + @Override + public void onError(Throwable e) { + System.out.println("error: " + e); + caughtError.set(e); + latch.countDown(); + } + + @Override + public void onNext(Long args) { + throw new RuntimeException("forced failure"); + } + }; + f.safeSubscribe(subscriber); + + latch.await(2000, TimeUnit.MILLISECONDS); + assertNotNull(caughtError.get()); + } + + /** + * Test that an error from a user provided Observer.onNext + * is handled and emitted to the onError. + * even when done across thread boundaries with observeOn + * @throws InterruptedException if the test is interrupted + */ + @Test + public void onNextErrorAcrossThread() throws InterruptedException { + final CountDownLatch latch = new CountDownLatch(1); + final AtomicReference<Throwable> caughtError = new AtomicReference<>(); + Flowable<Long> f = Flowable.interval(50, TimeUnit.MILLISECONDS); + Subscriber<Long> subscriber = new DefaultSubscriber<Long>() { + + @Override + public void onComplete() { + System.out.println("completed"); + latch.countDown(); + } + + @Override + public void onError(Throwable e) { + System.out.println("error: " + e); + caughtError.set(e); + latch.countDown(); + } + + @Override + public void onNext(Long args) { + throw new RuntimeException("forced failure"); + } + }; + f.observeOn(Schedulers.newThread()) + .safeSubscribe(subscriber); + + latch.await(2000, TimeUnit.MILLISECONDS); + assertNotNull(caughtError.get()); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/flowable/FlowableEventStream.java b/src/test/java/io/reactivex/rxjava3/flowable/FlowableEventStream.java new file mode 100644 index 0000000000..2428d6cb04 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/flowable/FlowableEventStream.java @@ -0,0 +1,92 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.flowable; + +import java.util.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.functions.Consumer; +import io.reactivex.rxjava3.schedulers.Schedulers; + +/** + * Utility for retrieving a mock eventstream for testing. + */ +public final class FlowableEventStream { + private FlowableEventStream() { + throw new IllegalStateException("No instances!"); + } + public static Flowable<Event> getEventStream(final String type, final int numInstances) { + + return Flowable.<Event>generate(new EventConsumer(type, numInstances)) + .subscribeOn(Schedulers.newThread()); + } + + public static Event randomEvent(String type, int numInstances) { + Map<String, Object> values = new LinkedHashMap<>(); + values.put("count200", randomIntFrom0to(4000)); + values.put("count4xx", randomIntFrom0to(300)); + values.put("count5xx", randomIntFrom0to(500)); + return new Event(type, "instance_" + randomIntFrom0to(numInstances), values); + } + + private static int randomIntFrom0to(int max) { + // XORShift instead of Math.random http://javamex.com/tutorials/random_numbers/xorshift.shtml + long x = System.nanoTime(); + x ^= (x << 21); + x ^= (x >>> 35); + x ^= (x << 4); + return Math.abs((int) x % max); + } + + static final class EventConsumer implements Consumer<Emitter<Event>> { + private final String type; + private final int numInstances; + + EventConsumer(String type, int numInstances) { + this.type = type; + this.numInstances = numInstances; + } + + @Override + public void accept(Emitter<Event> s) { + s.onNext(randomEvent(type, numInstances)); + try { + // slow it down somewhat + Thread.sleep(50); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + s.onError(e); + } + } + } + + public static class Event { + public final String type; + public final String instanceId; + public final Map<String, Object> values; + + /** + * Construct an event with the provided parameters. + * @param type the event type + * @param instanceId the instance identifier + * @param values + * This does NOT deep-copy, so do not mutate this Map after passing it in. + */ + public Event(String type, String instanceId, Map<String, Object> values) { + this.type = type; + this.instanceId = instanceId; + this.values = Collections.unmodifiableMap(values); + } + } +} diff --git a/src/test/java/io/reactivex/rxjava3/flowable/FlowableEventStreamTest.java b/src/test/java/io/reactivex/rxjava3/flowable/FlowableEventStreamTest.java new file mode 100644 index 0000000000..dd38b88afc --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/flowable/FlowableEventStreamTest.java @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.flowable; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.RxJavaTest; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class FlowableEventStreamTest extends RxJavaTest { + @Test + public void constructorShouldBePrivate() { + TestHelper.checkUtilityClass(FlowableEventStream.class); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/flowable/FlowableFuseableTest.java b/src/test/java/io/reactivex/rxjava3/flowable/FlowableFuseableTest.java new file mode 100644 index 0000000000..7bf54e8aba --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/flowable/FlowableFuseableTest.java @@ -0,0 +1,94 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.flowable; + +import java.util.Arrays; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.operators.QueueFuseable; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class FlowableFuseableTest extends RxJavaTest { + + @Test + public void syncRange() { + + Flowable.range(1, 10) + .to(TestHelper.<Integer>testSubscriber(Long.MAX_VALUE, QueueFuseable.ANY, false)) + .assertFusionMode(QueueFuseable.SYNC) + .assertValues(1, 2, 3, 4, 5, 6, 7, 8, 9, 10) + .assertNoErrors() + .assertComplete(); + } + + @Test + public void syncArray() { + + Flowable.fromArray(new Integer[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }) + .to(TestHelper.<Integer>testSubscriber(Long.MAX_VALUE, QueueFuseable.ANY, false)) + .assertFusionMode(QueueFuseable.SYNC) + .assertValues(1, 2, 3, 4, 5, 6, 7, 8, 9, 10) + .assertNoErrors() + .assertComplete(); + } + + @Test + public void syncIterable() { + + Flowable.fromIterable(Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)) + .to(TestHelper.<Integer>testSubscriber(Long.MAX_VALUE, QueueFuseable.ANY, false)) + .assertFusionMode(QueueFuseable.SYNC) + .assertValues(1, 2, 3, 4, 5, 6, 7, 8, 9, 10) + .assertNoErrors() + .assertComplete(); + } + + @Test + public void syncRangeHidden() { + + Flowable.range(1, 10).hide() + .to(TestHelper.<Integer>testSubscriber(Long.MAX_VALUE, QueueFuseable.ANY, false)) + .assertNotFuseable() + .assertFusionMode(QueueFuseable.NONE) + .assertValues(1, 2, 3, 4, 5, 6, 7, 8, 9, 10) + .assertNoErrors() + .assertComplete(); + } + + @Test + public void syncArrayHidden() { + Flowable.fromArray(new Integer[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }) + .hide() + .to(TestHelper.<Integer>testSubscriber(Long.MAX_VALUE, QueueFuseable.ANY, false)) + .assertNotFuseable() + .assertFusionMode(QueueFuseable.NONE) + .assertValues(1, 2, 3, 4, 5, 6, 7, 8, 9, 10) + .assertNoErrors() + .assertComplete(); + } + + @Test + public void syncIterableHidden() { + Flowable.fromIterable(Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)) + .hide() + .to(TestHelper.<Integer>testSubscriber(Long.MAX_VALUE, QueueFuseable.ANY, false)) + .assertNotFuseable() + .assertFusionMode(QueueFuseable.NONE) + .assertValues(1, 2, 3, 4, 5, 6, 7, 8, 9, 10) + .assertNoErrors() + .assertComplete(); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/flowable/FlowableGroupByTests.java b/src/test/java/io/reactivex/rxjava3/flowable/FlowableGroupByTests.java new file mode 100644 index 0000000000..e37934eadd --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/flowable/FlowableGroupByTests.java @@ -0,0 +1,116 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.flowable; + +import org.junit.Test; +import org.reactivestreams.Publisher; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.flowable.FlowableEventStream.Event; +import io.reactivex.rxjava3.flowables.GroupedFlowable; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.subscribers.TestSubscriber; + +public class FlowableGroupByTests extends RxJavaTest { + + @Test + public void takeUnsubscribesOnGroupBy() { + Flowable.merge( + FlowableEventStream.getEventStream("HTTP-ClusterA", 50), + FlowableEventStream.getEventStream("HTTP-ClusterB", 20) + ) + // group by type (2 clusters) + .groupBy(new Function<Event, Object>() { + @Override + public Object apply(Event event) { + return event.type; + } + }) + .take(1) + .blockingForEach(new Consumer<GroupedFlowable<Object, Event>>() { + @Override + public void accept(GroupedFlowable<Object, Event> v) { + System.out.println(v); + v.take(1).subscribe(); // FIXME groups need consumption to a certain degree to cancel upstream + } + }); + + System.out.println("**** finished"); + } + + @Test + public void takeUnsubscribesOnFlatMapOfGroupBy() { + Flowable.merge( + FlowableEventStream.getEventStream("HTTP-ClusterA", 50), + FlowableEventStream.getEventStream("HTTP-ClusterB", 20) + ) + // group by type (2 clusters) + .groupBy(new Function<Event, Object>() { + @Override + public Object apply(Event event) { + return event.type; + } + }) + .flatMap(new Function<GroupedFlowable<Object, Event>, Publisher<Object>>() { + @Override + public Publisher<Object> apply(GroupedFlowable<Object, Event> g) { + return g.map(new Function<Event, Object>() { + @Override + public Object apply(Event event) { + return event.instanceId + " - " + event.values.get("count200"); + } + }); + } + }) + .take(20) + .blockingForEach(new Consumer<Object>() { + @Override + public void accept(Object v) { + System.out.println(v); + } + }); + + System.out.println("**** finished"); + } + + @Test + public void groupsCompleteAsSoonAsMainCompletes() { + TestSubscriber<Integer> ts = TestSubscriber.create(); + + Flowable.range(0, 20) + .groupBy(new Function<Integer, Integer>() { + @Override + public Integer apply(Integer i) { + return i % 5; + } + }) + .concatMap(new Function<GroupedFlowable<Integer, Integer>, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(GroupedFlowable<Integer, Integer> v) { + return v; + } + }, 20) // need to prefetch as many groups as groupBy produces to avoid MBE + .subscribe(ts); + + // Behavior change: this now counts as group abandonment because concatMap + // doesn't subscribe to the 2nd+ emitted groups immediately + ts.assertValues( + 0, 5, 10, 15, // First group is okay + // any other group gets abandoned so we get 16 one-element group + 1, 2, 3, 4, 6, 7, 8, 9, 11, 12, 13, 14, 16, 17, 18, 19 + ); + ts.assertComplete(); + ts.assertNoErrors(); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/flowable/FlowableMergeTests.java b/src/test/java/io/reactivex/rxjava3/flowable/FlowableMergeTests.java new file mode 100644 index 0000000000..d549c3fbb4 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/flowable/FlowableMergeTests.java @@ -0,0 +1,99 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.flowable; + +import static org.junit.Assert.*; + +import java.util.List; + +import org.junit.Test; +import org.reactivestreams.Publisher; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.flowable.FlowableCovarianceTest.*; +import io.reactivex.rxjava3.functions.Supplier; + +public class FlowableMergeTests extends RxJavaTest { + + /** + * This won't compile if super/extends isn't done correctly on generics. + */ + @Test + public void covarianceOfMerge() { + Flowable<HorrorMovie> horrors = Flowable.just(new HorrorMovie()); + Flowable<Flowable<HorrorMovie>> metaHorrors = Flowable.just(horrors); + Flowable.<Media> merge(metaHorrors); + } + + @Test + public void mergeCovariance() { + Flowable<Media> f1 = Flowable.<Media> just(new HorrorMovie(), new Movie()); + Flowable<Media> f2 = Flowable.just(new Media(), new HorrorMovie()); + + Flowable<Flowable<Media>> os = Flowable.just(f1, f2); + + List<Media> values = Flowable.merge(os).toList().blockingGet(); + + assertEquals(4, values.size()); + } + + @Test + public void mergeCovariance2() { + Flowable<Media> f1 = Flowable.just(new HorrorMovie(), new Movie(), new Media()); + Flowable<Media> f2 = Flowable.just(new Media(), new HorrorMovie()); + + Flowable<Flowable<Media>> os = Flowable.just(f1, f2); + + List<Media> values = Flowable.merge(os).toList().blockingGet(); + + assertEquals(5, values.size()); + } + + @Test + public void mergeCovariance3() { + Flowable<Movie> f1 = Flowable.just(new HorrorMovie(), new Movie()); + Flowable<Media> f2 = Flowable.just(new Media(), new HorrorMovie()); + + List<Media> values = Flowable.merge(f1, f2).toList().blockingGet(); + + assertTrue(values.get(0) instanceof HorrorMovie); + assertTrue(values.get(1) instanceof Movie); + assertNotNull(values.get(2)); + assertTrue(values.get(3) instanceof HorrorMovie); + } + + @Test + public void mergeCovariance4() { + + Flowable<Movie> f1 = Flowable.defer(new Supplier<Publisher<Movie>>() { + @Override + public Publisher<Movie> get() { + return Flowable.just( + new HorrorMovie(), + new Movie() + ); + } + }); + + Flowable<Media> f2 = Flowable.just(new Media(), new HorrorMovie()); + + List<Media> values = Flowable.merge(f1, f2).toList().blockingGet(); + + assertTrue(values.get(0) instanceof HorrorMovie); + assertTrue(values.get(1) instanceof Movie); + assertNotNull(values.get(2)); + assertTrue(values.get(3) instanceof HorrorMovie); + } + +} diff --git a/src/test/java/io/reactivex/rxjava3/flowable/FlowableNotificationTest.java b/src/test/java/io/reactivex/rxjava3/flowable/FlowableNotificationTest.java new file mode 100644 index 0000000000..08d1994fcb --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/flowable/FlowableNotificationTest.java @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.flowable; + +import org.junit.*; + +import io.reactivex.rxjava3.core.*; + +public class FlowableNotificationTest extends RxJavaTest { + + @Test(expected = NullPointerException.class) + public void onNextIntegerNotificationDoesNotEqualNullNotification() { + final Notification<Integer> integerNotification = Notification.createOnNext(1); + final Notification<Integer> nullNotification = Notification.createOnNext(null); + Assert.assertNotEquals(integerNotification, nullNotification); + } + + @Test(expected = NullPointerException.class) + public void onNextNullNotificationDoesNotEqualIntegerNotification() { + final Notification<Integer> integerNotification = Notification.createOnNext(1); + final Notification<Integer> nullNotification = Notification.createOnNext(null); + Assert.assertNotEquals(nullNotification, integerNotification); + } + + @Test + public void onNextIntegerNotificationsWhenEqual() { + final Notification<Integer> integerNotification = Notification.createOnNext(1); + final Notification<Integer> integerNotification2 = Notification.createOnNext(1); + Assert.assertEquals(integerNotification, integerNotification2); + } + + @Test + public void onNextIntegerNotificationsWhenNotEqual() { + final Notification<Integer> integerNotification = Notification.createOnNext(1); + final Notification<Integer> integerNotification2 = Notification.createOnNext(2); + Assert.assertNotEquals(integerNotification, integerNotification2); + } + + @Test + public void onErrorIntegerNotificationsWhenEqual() { + final Exception exception = new Exception(); + final Notification<Integer> onErrorNotification = Notification.createOnError(exception); + final Notification<Integer> onErrorNotification2 = Notification.createOnError(exception); + Assert.assertEquals(onErrorNotification, onErrorNotification2); + } + + @Test + public void onErrorIntegerNotificationWhenNotEqual() { + final Notification<Integer> onErrorNotification = Notification.createOnError(new Exception()); + final Notification<Integer> onErrorNotification2 = Notification.createOnError(new Exception()); + Assert.assertNotEquals(onErrorNotification, onErrorNotification2); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/flowable/FlowableNullTests.java b/src/test/java/io/reactivex/rxjava3/flowable/FlowableNullTests.java new file mode 100644 index 0000000000..748a12d3c3 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/flowable/FlowableNullTests.java @@ -0,0 +1,1340 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.flowable; + +import static org.junit.Assert.*; + +import java.lang.reflect.*; +import java.util.*; +import java.util.concurrent.*; + +import org.junit.*; +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.processors.*; +import io.reactivex.rxjava3.schedulers.Schedulers; +import io.reactivex.rxjava3.subscribers.TestSubscriber; +import io.reactivex.rxjava3.testsupport.TestHelper; + +/** + * Verifies the operators handle null values properly by emitting/throwing NullPointerExceptions. + */ +public class FlowableNullTests extends RxJavaTest { + + Flowable<Integer> just1 = Flowable.just(1); + + //*********************************************************** + // Static methods + //*********************************************************** + + @Test(expected = NullPointerException.class) + public void ambVarargsOneIsNull() { + Flowable.ambArray(Flowable.never(), null).blockingLast(); + } + + @Test + public void ambIterableIteratorNull() { + Flowable.amb(new Iterable<Publisher<Object>>() { + @Override + public Iterator<Publisher<Object>> iterator() { + return null; + } + }).test().assertError(NullPointerException.class); + } + + @Test + public void ambIterableOneIsNull() { + Flowable.amb(Arrays.asList(Flowable.never(), null)) + .test() + .assertError(NullPointerException.class); + } + + @Test(expected = NullPointerException.class) + public void combineLatestIterableIteratorNull() { + Flowable.combineLatestDelayError(new Iterable<Publisher<Object>>() { + @Override + public Iterator<Publisher<Object>> iterator() { + return null; + } + }, new Function<Object[], Object>() { + @Override + public Object apply(Object[] v) { + return 1; + } + }).blockingLast(); + } + + @Test(expected = NullPointerException.class) + public void combineLatestIterableOneIsNull() { + Flowable.combineLatestDelayError(Arrays.asList(Flowable.never(), null), new Function<Object[], Object>() { + @Override + public Object apply(Object[] v) { + return 1; + } + }).blockingLast(); + } + + @Test(expected = NullPointerException.class) + public void combineLatestIterableFunctionReturnsNull() { + Flowable.combineLatestDelayError(Arrays.asList(just1), new Function<Object[], Object>() { + @Override + public Object apply(Object[] v) { + return null; + } + }).blockingLast(); + } + + @Test(expected = NullPointerException.class) + public void concatIterableIteratorNull() { + Flowable.concat(new Iterable<Publisher<Object>>() { + @Override + public Iterator<Publisher<Object>> iterator() { + return null; + } + }).blockingLast(); + } + + @Test(expected = NullPointerException.class) + public void concatIterableOneIsNull() { + Flowable.concat(Arrays.asList(just1, null)).blockingLast(); + } + + @Test(expected = NullPointerException.class) + public void concatArrayOneIsNull() { + Flowable.concatArray(just1, null).blockingLast(); + } + + @Test(expected = NullPointerException.class) + public void deferFunctionReturnsNull() { + Flowable.defer(new Supplier<Publisher<Object>>() { + @Override + public Publisher<Object> get() { + return null; + } + }).blockingLast(); + } + + @Test(expected = NullPointerException.class) + public void errorFunctionReturnsNull() { + Flowable.error(new Supplier<Throwable>() { + @Override + public Throwable get() { + return null; + } + }).blockingSubscribe(); + } + + @Test(expected = NullPointerException.class) + public void fromArrayOneIsNull() { + Flowable.fromArray(1, null).blockingLast(); + } + + @Test(expected = NullPointerException.class) + public void fromCallableReturnsNull() { + Flowable.fromCallable(new Callable<Object>() { + @Override + public Object call() throws Exception { + return null; + } + }).blockingLast(); + } + + @Test + public void fromFutureReturnsNull() { + FutureTask<Object> f = new FutureTask<>(Functions.EMPTY_RUNNABLE, null); + f.run(); + + TestSubscriber<Object> ts = new TestSubscriber<>(); + Flowable.fromFuture(f).subscribe(ts); + ts.assertNoValues(); + ts.assertNotComplete(); + ts.assertError(NullPointerException.class); + } + + @Test(expected = NullPointerException.class) + public void fromFutureTimedReturnsNull() { + FutureTask<Object> f = new FutureTask<>(Functions.EMPTY_RUNNABLE, null); + f.run(); + Flowable.fromFuture(f, 1, TimeUnit.SECONDS).blockingLast(); + } + + @Test(expected = NullPointerException.class) + public void fromIterableIteratorNull() { + Flowable.fromIterable(new Iterable<Object>() { + @Override + public Iterator<Object> iterator() { + return null; + } + }).blockingLast(); + } + + @Test(expected = NullPointerException.class) + public void fromIterableValueNull() { + Flowable.fromIterable(Arrays.asList(1, null)).blockingLast(); + } + + @Test(expected = NullPointerException.class) + public void generateConsumerEmitsNull() { + Flowable.generate(new Consumer<Emitter<Object>>() { + @Override + public void accept(Emitter<Object> s) { + s.onNext(null); + } + }).blockingLast(); + } + + @Test(expected = NullPointerException.class) + public void generateStateConsumerInitialStateNull() { + BiConsumer<Integer, Emitter<Integer>> generator = new BiConsumer<Integer, Emitter<Integer>>() { + @Override + public void accept(Integer s, Emitter<Integer> o) { + o.onNext(1); + } + }; + Flowable.generate(null, generator); + } + + @Test(expected = NullPointerException.class) + public void generateStateFunctionInitialStateNull() { + Flowable.generate(null, new BiFunction<Object, Emitter<Object>, Object>() { + @Override + public Object apply(Object s, Emitter<Object> o) { + o.onNext(1); return s; + } + }); + } + + @Test(expected = NullPointerException.class) + public void generateStateConsumerNull() { + Flowable.generate(new Supplier<Integer>() { + @Override + public Integer get() { + return 1; + } + }, (BiConsumer<Integer, Emitter<Object>>)null); + } + + @Test + public void generateConsumerStateNullAllowed() { + BiConsumer<Integer, Emitter<Integer>> generator = new BiConsumer<Integer, Emitter<Integer>>() { + @Override + public void accept(Integer s, Emitter<Integer> o) { + o.onComplete(); + } + }; + Flowable.generate(new Supplier<Integer>() { + @Override + public Integer get() { + return null; + } + }, generator).blockingSubscribe(); + } + + @Test + public void generateFunctionStateNullAllowed() { + Flowable.generate(new Supplier<Object>() { + @Override + public Object get() { + return null; + } + }, new BiFunction<Object, Emitter<Object>, Object>() { + @Override + public Object apply(Object s, Emitter<Object> o) { + o.onComplete(); return s; + } + }).blockingSubscribe(); + } + + @Test + public void justNull() throws Exception { + @SuppressWarnings("rawtypes") + Class<Flowable> clazz = Flowable.class; + for (int argCount = 1; argCount < 10; argCount++) { + for (int argNull = 1; argNull <= argCount; argNull++) { + Class<?>[] params = new Class[argCount]; + Arrays.fill(params, Object.class); + + Object[] values = new Object[argCount]; + Arrays.fill(values, 1); + values[argNull - 1] = null; + + Method m = clazz.getMethod("just", params); + + try { + m.invoke(null, values); + Assert.fail("No exception for argCount " + argCount + " / argNull " + argNull); + } catch (InvocationTargetException ex) { + if (!(ex.getCause() instanceof NullPointerException)) { + Assert.fail("Unexpected exception for argCount " + argCount + " / argNull " + argNull + ": " + ex); + } + } + } + } + } + + @Test(expected = NullPointerException.class) + public void mergeIterableIteratorNull() { + Flowable.merge(new Iterable<Publisher<Object>>() { + @Override + public Iterator<Publisher<Object>> iterator() { + return null; + } + }, 128, 128).blockingLast(); + } + + @Test(expected = NullPointerException.class) + public void mergeIterableOneIsNull() { + Flowable.merge(Arrays.asList(just1, null), 128, 128).blockingLast(); + } + + @Test(expected = NullPointerException.class) + public void mergeArrayOneIsNull() { + Flowable.mergeArray(128, 128, just1, null).blockingLast(); + } + + @Test(expected = NullPointerException.class) + public void mergeDelayErrorIterableIteratorNull() { + Flowable.mergeDelayError(new Iterable<Publisher<Object>>() { + @Override + public Iterator<Publisher<Object>> iterator() { + return null; + } + }, 128, 128).blockingLast(); + } + + @Test(expected = NullPointerException.class) + public void mergeDelayErrorIterableOneIsNull() { + Flowable.mergeDelayError(Arrays.asList(just1, null), 128, 128).blockingLast(); + } + + @Test(expected = NullPointerException.class) + public void mergeDelayErrorArrayOneIsNull() { + Flowable.mergeArrayDelayError(128, 128, just1, null).blockingLast(); + } + + @Test(expected = NullPointerException.class) + public void usingFlowableSupplierReturnsNull() { + Flowable.using(new Supplier<Object>() { + @Override + public Object get() { + return 1; + } + }, new Function<Object, Publisher<Object>>() { + @Override + public Publisher<Object> apply(Object d) { + return null; + } + }, Functions.emptyConsumer()).blockingLast(); + } + + @Test(expected = NullPointerException.class) + public void zipIterableIteratorNull() { + Flowable.zip(new Iterable<Publisher<Object>>() { + @Override + public Iterator<Publisher<Object>> iterator() { + return null; + } + }, new Function<Object[], Object>() { + @Override + public Object apply(Object[] v) { + return 1; + } + }).blockingLast(); + } + + @Test(expected = NullPointerException.class) + public void zipIterableFunctionReturnsNull() { + Flowable.zip(Arrays.asList(just1, just1), new Function<Object[], Object>() { + @Override + public Object apply(Object[] a) { + return null; + } + }).blockingLast(); + } + + @Test(expected = NullPointerException.class) + public void zipIterable2Null() { + Flowable.zip((Iterable<Publisher<Object>>)null, new Function<Object[], Object>() { + @Override + public Object apply(Object[] a) { + return 1; + } + }, true, 128); + } + + @Test(expected = NullPointerException.class) + public void zipIterable2IteratorNull() { + Flowable.zip(new Iterable<Publisher<Object>>() { + @Override + public Iterator<Publisher<Object>> iterator() { + return null; + } + }, new Function<Object[], Object>() { + @Override + public Object apply(Object[] a) { + return 1; + } + }, true, 128).blockingLast(); + } + + @Test(expected = NullPointerException.class) + public void zipIterable2FunctionReturnsNull() { + Flowable.zip(Arrays.asList(just1, just1), new Function<Object[], Object>() { + @Override + public Object apply(Object[] a) { + return null; + } + }, true, 128).blockingLast(); + } + + //************************************************************* + // Instance methods + //************************************************************* + + @Test(expected = NullPointerException.class) + public void bufferSupplierReturnsNull() { + just1.buffer(1, 1, new Supplier<Collection<Integer>>() { + @Override + public Collection<Integer> get() { + return null; + } + }).blockingSubscribe(); + } + + @Test(expected = NullPointerException.class) + public void bufferTimedSupplierReturnsNull() { + just1.buffer(1L, 1L, TimeUnit.SECONDS, Schedulers.single(), new Supplier<Collection<Integer>>() { + @Override + public Collection<Integer> get() { + return null; + } + }).blockingSubscribe(); + } + + @Test(expected = NullPointerException.class) + public void bufferOpenCloseCloseReturnsNull() { + just1.buffer(just1, new Function<Integer, Publisher<Object>>() { + @Override + public Publisher<Object> apply(Integer v) { + return null; + } + }).blockingSubscribe(); + } + + @Test(expected = NullPointerException.class) + public void bufferBoundarySupplierReturnsNull() { + just1.buffer(just1, new Supplier<Collection<Integer>>() { + @Override + public Collection<Integer> get() { + return null; + } + }).blockingSubscribe(); + } + + @Test(expected = NullPointerException.class) + public void collectInitialSupplierReturnsNull() { + just1.collect(new Supplier<Object>() { + @Override + public Object get() { + return null; + } + }, new BiConsumer<Object, Integer>() { + @Override + public void accept(Object a, Integer b) { } + }).blockingGet(); + } + + @Test(expected = NullPointerException.class) + public void concatMapReturnsNull() { + just1.concatMap(new Function<Integer, Publisher<Object>>() { + @Override + public Publisher<Object> apply(Integer v) { + return null; + } + }).blockingSubscribe(); + } + + @Test(expected = NullPointerException.class) + public void concatMapIterableReturnNull() { + just1.concatMapIterable(new Function<Integer, Iterable<Object>>() { + @Override + public Iterable<Object> apply(Integer v) { + return null; + } + }).blockingSubscribe(); + } + + @Test(expected = NullPointerException.class) + public void concatMapIterableIteratorNull() { + just1.concatMapIterable(new Function<Integer, Iterable<Object>>() { + @Override + public Iterable<Object> apply(Integer v) { + return new Iterable<Object>() { + @Override + public Iterator<Object> iterator() { + return null; + } + }; + } + }).blockingSubscribe(); + } + + @Test(expected = NullPointerException.class) + public void debounceFunctionReturnsNull() { + just1.debounce(new Function<Integer, Publisher<Object>>() { + @Override + public Publisher<Object> apply(Integer v) { + return null; + } + }).blockingSubscribe(); + } + + @Test(expected = NullPointerException.class) + public void delayWithFunctionReturnsNull() { + just1.delay(new Function<Integer, Publisher<Object>>() { + @Override + public Publisher<Object> apply(Integer v) { + return null; + } + }).blockingSubscribe(); + } + + @Test(expected = NullPointerException.class) + public void delayBothItemSupplierReturnsNull() { + just1.delay(just1, new Function<Integer, Publisher<Object>>() { + @Override + public Publisher<Object> apply(Integer v) { + return null; + } + }).blockingSubscribe(); + } + + @Test(expected = NullPointerException.class) + public void distinctSupplierReturnsNull() { + just1.distinct(new Function<Integer, Object>() { + @Override + public Object apply(Integer v) { + return v; + } + }, new Supplier<Collection<Object>>() { + @Override + public Collection<Object> get() { + return null; + } + }).blockingSubscribe(); + } + + @Test(expected = NullPointerException.class) + public void distinctFunctionReturnsNull() { + just1.distinct(new Function<Integer, Object>() { + @Override + public Object apply(Integer v) { + return null; + } + }).blockingSubscribe(); + } + + @Test + public void distinctUntilChangedFunctionReturnsNull() { + Flowable.range(1, 2).distinctUntilChanged(new Function<Integer, Object>() { + @Override + public Object apply(Integer v) { + return null; + } + }).test().assertResult(1); + } + + @Test(expected = NullPointerException.class) + public void flatMapFunctionReturnsNull() { + just1.flatMap(new Function<Integer, Publisher<Object>>() { + @Override + public Publisher<Object> apply(Integer v) { + return null; + } + }).blockingSubscribe(); + } + + @Test(expected = NullPointerException.class) + public void flatMapNotificationOnNextReturnsNull() { + just1.flatMap(new Function<Integer, Publisher<Integer>>() { + @Override + public Publisher<Integer> apply(Integer v) { + return null; + } + }, new Function<Throwable, Publisher<Integer>>() { + @Override + public Publisher<Integer> apply(Throwable e) { + return just1; + } + }, new Supplier<Publisher<Integer>>() { + @Override + public Publisher<Integer> get() { + return just1; + } + }).blockingSubscribe(); + } + + @Test(expected = NullPointerException.class) + public void flatMapNotificationOnCompleteReturnsNull() { + just1.flatMap(new Function<Integer, Publisher<Integer>>() { + @Override + public Publisher<Integer> apply(Integer v) { + return just1; + } + }, new Function<Throwable, Publisher<Integer>>() { + @Override + public Publisher<Integer> apply(Throwable e) { + return just1; + } + }, new Supplier<Publisher<Integer>>() { + @Override + public Publisher<Integer> get() { + return null; + } + }).blockingSubscribe(); + } + + @Test(expected = NullPointerException.class) + public void flatMapCombinerMapperReturnsNull() { + just1.flatMap(new Function<Integer, Publisher<Object>>() { + @Override + public Publisher<Object> apply(Integer v) { + return null; + } + }, new BiFunction<Integer, Object, Object>() { + @Override + public Object apply(Integer a, Object b) { + return 1; + } + }).blockingSubscribe(); + } + + @Test(expected = NullPointerException.class) + public void flatMapCombinerCombinerReturnsNull() { + just1.flatMap(new Function<Integer, Publisher<Integer>>() { + @Override + public Publisher<Integer> apply(Integer v) { + return just1; + } + }, new BiFunction<Integer, Integer, Object>() { + @Override + public Object apply(Integer a, Integer b) { + return null; + } + }).blockingSubscribe(); + } + + @Test(expected = NullPointerException.class) + public void flatMapIterableMapperReturnsNull() { + just1.flatMapIterable(new Function<Integer, Iterable<Object>>() { + @Override + public Iterable<Object> apply(Integer v) { + return null; + } + }).blockingSubscribe(); + } + + @Test(expected = NullPointerException.class) + public void flatMapIterableMapperIteratorNull() { + just1.flatMapIterable(new Function<Integer, Iterable<Object>>() { + @Override + public Iterable<Object> apply(Integer v) { + return new Iterable<Object>() { + @Override + public Iterator<Object> iterator() { + return null; + } + }; + } + }).blockingSubscribe(); + } + + @Test(expected = NullPointerException.class) + public void flatMapIterableMapperIterableOneNull() { + just1.flatMapIterable(new Function<Integer, Iterable<Integer>>() { + @Override + public Iterable<Integer> apply(Integer v) { + return Arrays.asList(1, null); + } + }).blockingSubscribe(); + } + + @Test(expected = NullPointerException.class) + public void flatMapIterableCombinerReturnsNull() { + just1.flatMapIterable(new Function<Integer, Iterable<Integer>>() { + @Override + public Iterable<Integer> apply(Integer v) { + return Arrays.asList(1); + } + }, new BiFunction<Integer, Integer, Object>() { + @Override + public Object apply(Integer a, Integer b) { + return null; + } + }).blockingSubscribe(); + } + + public void groupByKeyNull() { + just1.groupBy(new Function<Integer, Object>() { + @Override + public Object apply(Integer v) { + return null; + } + }).blockingSubscribe(); + } + + @Test(expected = NullPointerException.class) + public void groupByValueReturnsNull() { + just1.groupBy(new Function<Integer, Object>() { + @Override + public Object apply(Integer v) { + return v; + } + }, new Function<Integer, Object>() { + @Override + public Object apply(Integer v) { + return null; + } + }).blockingSubscribe(); + } + + @Test(expected = NullPointerException.class) + public void liftReturnsNull() { + just1.lift(new FlowableOperator<Object, Integer>() { + @Override + public Subscriber<? super Integer> apply(Subscriber<? super Object> s) { + return null; + } + }).blockingSubscribe(); + } + + @Test(expected = NullPointerException.class) + public void mapReturnsNull() { + just1.map(new Function<Integer, Object>() { + @Override + public Object apply(Integer v) { + return null; + } + }).blockingSubscribe(); + } + + @Test + public void onErrorResumeNextFunctionReturnsNull() { + try { + Flowable.error(new TestException()).onErrorResumeNext(new Function<Throwable, Publisher<Object>>() { + @Override + public Publisher<Object> apply(Throwable e) { + return null; + } + }).blockingSubscribe(); + fail("Should have thrown"); + } catch (CompositeException ex) { + List<Throwable> errors = ex.getExceptions(); + TestHelper.assertError(errors, 0, TestException.class); + TestHelper.assertError(errors, 1, NullPointerException.class); + assertEquals(2, errors.size()); + } + } + + @Test + public void onErrorReturnFunctionReturnsNull() { + try { + Flowable.error(new TestException()).onErrorReturn(new Function<Throwable, Object>() { + @Override + public Object apply(Throwable e) { + return null; + } + }).blockingSubscribe(); + fail("Should have thrown"); + } catch (CompositeException ex) { + List<Throwable> errors = TestHelper.compositeList(ex); + + TestHelper.assertError(errors, 0, TestException.class); + TestHelper.assertError(errors, 1, NullPointerException.class, "The valueSupplier returned a null value"); + } + } + + @Test(expected = NullPointerException.class) + public void publishFunctionReturnsNull() { + just1.publish(new Function<Flowable<Integer>, Publisher<Object>>() { + @Override + public Publisher<Object> apply(Flowable<Integer> v) { + return null; + } + }).blockingSubscribe(); + } + + @Test(expected = NullPointerException.class) + public void reduceFunctionReturnsNull() { + Flowable.just(1, 1).reduce(new BiFunction<Integer, Integer, Integer>() { + @Override + public Integer apply(Integer a, Integer b) { + return null; + } + }).toFlowable().blockingSubscribe(); + } + + @Test(expected = NullPointerException.class) + public void reduceSeedFunctionReturnsNull() { + just1.reduce(1, new BiFunction<Integer, Integer, Integer>() { + @Override + public Integer apply(Integer a, Integer b) { + return null; + } + }).blockingGet(); + } + + @Test(expected = NullPointerException.class) + public void reduceWithSeedNull() { + just1.reduceWith(null, new BiFunction<Object, Integer, Object>() { + @Override + public Object apply(Object a, Integer b) { + return 1; + } + }); + } + + @Test(expected = NullPointerException.class) + public void reduceWithSeedReturnsNull() { + just1.reduceWith(new Supplier<Object>() { + @Override + public Object get() { + return null; + } + }, new BiFunction<Object, Integer, Object>() { + @Override + public Object apply(Object a, Integer b) { + return 1; + } + }).blockingGet(); + } + + @Test(expected = NullPointerException.class) + public void repeatWhenFunctionReturnsNull() { + just1.repeatWhen(new Function<Flowable<Object>, Publisher<Object>>() { + @Override + public Publisher<Object> apply(Flowable<Object> v) { + return null; + } + }).blockingSubscribe(); + } + + @Test(expected = NullPointerException.class) + public void replaySelectorNull() { + just1.replay((Function<Flowable<Integer>, Flowable<Integer>>)null); + } + + @Test(expected = NullPointerException.class) + public void replaySelectorReturnsNull() { + just1.replay(new Function<Flowable<Integer>, Publisher<Object>>() { + @Override + public Publisher<Object> apply(Flowable<Integer> f) { + return null; + } + }).blockingSubscribe(); + } + + @Test(expected = NullPointerException.class) + public void replayBoundedSelectorReturnsNull() { + just1.replay(new Function<Flowable<Integer>, Publisher<Object>>() { + @Override + public Publisher<Object> apply(Flowable<Integer> v) { + return null; + } + }, 1, 1, TimeUnit.SECONDS).blockingSubscribe(); + } + + @Test(expected = NullPointerException.class) + public void replayTimeBoundedSelectorReturnsNull() { + just1.replay(new Function<Flowable<Integer>, Publisher<Object>>() { + @Override + public Publisher<Object> apply(Flowable<Integer> v) { + return null; + } + }, 1, TimeUnit.SECONDS, Schedulers.single()).blockingSubscribe(); + } + + @Test(expected = NullPointerException.class) + public void retryWhenFunctionReturnsNull() { + Flowable.error(new TestException()).retryWhen(new Function<Flowable<? extends Throwable>, Publisher<Object>>() { + @Override + public Publisher<Object> apply(Flowable<? extends Throwable> f) { + return null; + } + }).blockingSubscribe(); + } + + @Test(expected = NullPointerException.class) + public void scanFunctionReturnsNull() { + Flowable.just(1, 1).scan(new BiFunction<Integer, Integer, Integer>() { + @Override + public Integer apply(Integer a, Integer b) { + return null; + } + }).blockingSubscribe(); + } + + @Test(expected = NullPointerException.class) + public void scanSeedNull() { + just1.scan(null, new BiFunction<Object, Integer, Object>() { + @Override + public Object apply(Object a, Integer b) { + return 1; + } + }); + } + + @Test(expected = NullPointerException.class) + public void scanSeedFunctionReturnsNull() { + just1.scan(1, new BiFunction<Integer, Integer, Integer>() { + @Override + public Integer apply(Integer a, Integer b) { + return null; + } + }).blockingSubscribe(); + } + + @Test(expected = NullPointerException.class) + public void scanSeedSupplierReturnsNull() { + just1.scanWith(new Supplier<Object>() { + @Override + public Object get() { + return null; + } + }, new BiFunction<Object, Integer, Object>() { + @Override + public Object apply(Object a, Integer b) { + return 1; + } + }).blockingSubscribe(); + } + + @Test(expected = NullPointerException.class) + public void scanSeedSupplierFunctionReturnsNull() { + just1.scanWith(new Supplier<Object>() { + @Override + public Object get() { + return 1; + } + }, new BiFunction<Object, Integer, Object>() { + @Override + public Object apply(Object a, Integer b) { + return null; + } + }).blockingSubscribe(); + } + + @Test(expected = NullPointerException.class) + public void startWithIterableIteratorNull() { + just1.startWithIterable(new Iterable<Integer>() { + @Override + public Iterator<Integer> iterator() { + return null; + } + }).blockingSubscribe(); + } + + @Test(expected = NullPointerException.class) + public void startWithIterableOneNull() { + just1.startWithIterable(Arrays.asList(1, null)).blockingSubscribe(); + } + + @Test(expected = NullPointerException.class) + public void startWithArrayOneNull() { + just1.startWithArray(1, null).blockingSubscribe(); + } + + @Test(expected = NullPointerException.class) + public void switchMapFunctionReturnsNull() { + just1.switchMap(new Function<Integer, Publisher<Object>>() { + @Override + public Publisher<Object> apply(Integer v) { + return null; + } + }).blockingSubscribe(); + } + + @Test(expected = NullPointerException.class) + public void timeoutSelectorReturnsNull() { + just1.timeout(new Function<Integer, Publisher<Object>>() { + @Override + public Publisher<Object> apply(Integer v) { + return null; + } + }).blockingSubscribe(); + } + + @Test(expected = NullPointerException.class) + public void timeoutSelectorOtherNull() { + just1.timeout(new Function<Integer, Publisher<Integer>>() { + @Override + public Publisher<Integer> apply(Integer v) { + return just1; + } + }, null); + } + + @Test(expected = NullPointerException.class) + public void timeoutFirstItemReturnsNull() { + just1.timeout(Flowable.never(), new Function<Integer, Publisher<Object>>() { + @Override + public Publisher<Object> apply(Integer v) { + return null; + } + }).blockingSubscribe(); + } + + @Test(expected = NullPointerException.class) + public void timestampUnitNull() { + just1.timestamp(null, Schedulers.single()); + } + + @Test(expected = NullPointerException.class) + public void timestampSchedulerNull() { + just1.timestamp(TimeUnit.SECONDS, null); + } + + @Test(expected = NullPointerException.class) + public void toListSupplierReturnsNull() { + just1.toList(new Supplier<Collection<Integer>>() { + @Override + public Collection<Integer> get() { + return null; + } + }).toFlowable().blockingSubscribe(); + } + + @Test(expected = NullPointerException.class) + public void toListSupplierReturnsNullSingle() { + just1.toList(new Supplier<Collection<Integer>>() { + @Override + public Collection<Integer> get() { + return null; + } + }).blockingGet(); + } + + @Test + public void toMapValueSelectorReturnsNull() { + just1.toMap(new Function<Integer, Object>() { + @Override + public Object apply(Integer v) { + return v; + } + }, new Function<Integer, Object>() { + @Override + public Object apply(Integer v) { + return null; + } + }).blockingGet(); + } + + @Test(expected = NullPointerException.class) + public void toMapMapSupplierReturnsNull() { + just1.toMap(new Function<Integer, Object>() { + @Override + public Object apply(Integer v) { + return v; + } + }, new Function<Integer, Object>() { + @Override + public Object apply(Integer v) { + return v; + } + }, new Supplier<Map<Object, Object>>() { + @Override + public Map<Object, Object> get() { + return null; + } + }).blockingGet(); + } + + @Test + public void toMultiMapValueSelectorReturnsNullAllowed() { + just1.toMap(new Function<Integer, Object>() { + @Override + public Object apply(Integer v) { + return v; + } + }, new Function<Integer, Object>() { + @Override + public Object apply(Integer v) { + return null; + } + }).blockingGet(); + } + + @Test(expected = NullPointerException.class) + public void toMultimapMapSupplierReturnsNull() { + just1.toMultimap(new Function<Integer, Object>() { + @Override + public Object apply(Integer v) { + return v; + } + }, new Function<Integer, Object>() { + @Override + public Object apply(Integer v) { + return v; + } + }, new Supplier<Map<Object, Collection<Object>>>() { + @Override + public Map<Object, Collection<Object>> get() { + return null; + } + }).blockingGet(); + } + + @Test(expected = NullPointerException.class) + public void toMultimapMapCollectionSupplierReturnsNull() { + just1.toMultimap(new Function<Integer, Integer>() { + @Override + public Integer apply(Integer v) { + return v; + } + }, new Function<Integer, Integer>() { + @Override + public Integer apply(Integer v) { + return v; + } + }, new Supplier<Map<Integer, Collection<Integer>>>() { + @Override + public Map<Integer, Collection<Integer>> get() { + return new HashMap<>(); + } + }, new Function<Integer, Collection<Integer>>() { + @Override + public Collection<Integer> apply(Integer v) { + return null; + } + }).blockingGet(); + } + + @Test(expected = NullPointerException.class) + public void windowOpenCloseOpenNull() { + just1.window(null, new Function<Object, Publisher<Integer>>() { + @Override + public Publisher<Integer> apply(Object v) { + return just1; + } + }); + } + + @Test(expected = NullPointerException.class) + public void windowOpenCloseCloseReturnsNull() { + Flowable.never().window(just1, new Function<Integer, Publisher<Object>>() { + @Override + public Publisher<Object> apply(Integer v) { + return null; + } + }).blockingSubscribe(); + } + + @Test(expected = NullPointerException.class) + public void withLatestFromOtherNull() { + just1.withLatestFrom(null, new BiFunction<Integer, Object, Object>() { + @Override + public Object apply(Integer a, Object b) { + return 1; + } + }); + } + + @Test(expected = NullPointerException.class) + public void withLatestFromCombinerReturnsNull() { + just1.withLatestFrom(just1, new BiFunction<Integer, Integer, Object>() { + @Override + public Object apply(Integer a, Integer b) { + return null; + } + }).blockingSubscribe(); + } + + @Test(expected = NullPointerException.class) + public void zipWithIterableNull() { + just1.zipWith((Iterable<Integer>)null, new BiFunction<Integer, Integer, Object>() { + @Override + public Object apply(Integer a, Integer b) { + return 1; + } + }); + } + + @Test(expected = NullPointerException.class) + public void zipWithIterableCombinerReturnsNull() { + just1.zipWith(Arrays.asList(1), new BiFunction<Integer, Integer, Object>() { + @Override + public Object apply(Integer a, Integer b) { + return null; + } + }).blockingSubscribe(); + } + + @Test(expected = NullPointerException.class) + public void zipWithIterableIteratorNull() { + just1.zipWith(new Iterable<Object>() { + @Override + public Iterator<Object> iterator() { + return null; + } + }, new BiFunction<Integer, Object, Object>() { + @Override + public Object apply(Integer a, Object b) { + return 1; + } + }).blockingSubscribe(); + } + + @Test(expected = NullPointerException.class) + public void zipWithIterableOneIsNull() { + Flowable.just(1, 2).zipWith(Arrays.asList(1, null), new BiFunction<Integer, Integer, Object>() { + @Override + public Object apply(Integer a, Integer b) { + return 1; + } + }).blockingSubscribe(); + } + + @Test(expected = NullPointerException.class) + public void zipWithPublisherNull() { + just1.zipWith((Publisher<Integer>)null, new BiFunction<Integer, Integer, Object>() { + @Override + public Object apply(Integer a, Integer b) { + return 1; + } + }); + } + + @Test(expected = NullPointerException.class) + public void zipWithCombinerReturnsNull() { + just1.zipWith(just1, new BiFunction<Integer, Integer, Object>() { + @Override + public Object apply(Integer a, Integer b) { + return null; + } + }).blockingSubscribe(); + } + + //********************************************* + // Subject null tests + //********************************************* + + @Test(expected = NullPointerException.class) + public void asyncSubjectOnNextNull() { + FlowableProcessor<Integer> processor = AsyncProcessor.create(); + processor.onNext(null); + processor.blockingSubscribe(); + } + + @Test(expected = NullPointerException.class) + public void asyncSubjectOnErrorNull() { + FlowableProcessor<Integer> processor = AsyncProcessor.create(); + processor.onError(null); + processor.blockingSubscribe(); + } + + @Test(expected = NullPointerException.class) + public void behaviorSubjectOnNextNull() { + FlowableProcessor<Integer> processor = BehaviorProcessor.create(); + processor.onNext(null); + processor.blockingSubscribe(); + } + + @Test(expected = NullPointerException.class) + public void behaviorSubjectOnErrorNull() { + FlowableProcessor<Integer> processor = BehaviorProcessor.create(); + processor.onError(null); + processor.blockingSubscribe(); + } + + @Test(expected = NullPointerException.class) + public void publishSubjectOnNextNull() { + FlowableProcessor<Integer> processor = PublishProcessor.create(); + processor.onNext(null); + processor.blockingSubscribe(); + } + + @Test(expected = NullPointerException.class) + public void publishSubjectOnErrorNull() { + FlowableProcessor<Integer> processor = PublishProcessor.create(); + processor.onError(null); + processor.blockingSubscribe(); + } + + @Test(expected = NullPointerException.class) + public void replaycSubjectOnNextNull() { + FlowableProcessor<Integer> processor = ReplayProcessor.create(); + processor.onNext(null); + processor.blockingSubscribe(); + } + + @Test(expected = NullPointerException.class) + public void replaySubjectOnErrorNull() { + FlowableProcessor<Integer> processor = ReplayProcessor.create(); + processor.onError(null); + processor.blockingSubscribe(); + } + + @Test(expected = NullPointerException.class) + public void serializedcSubjectOnNextNull() { + FlowableProcessor<Integer> processor = PublishProcessor.<Integer>create().toSerialized(); + processor.onNext(null); + processor.blockingSubscribe(); + } + + @Test(expected = NullPointerException.class) + public void serializedSubjectOnErrorNull() { + FlowableProcessor<Integer> processor = PublishProcessor.<Integer>create().toSerialized(); + processor.onError(null); + processor.blockingSubscribe(); + } + + @Test(expected = NullPointerException.class) + public void combineLatestDelayErrorIterableFunctionReturnsNull() { + Flowable.combineLatestDelayError(Arrays.asList(just1), new Function<Object[], Object>() { + @Override + public Object apply(Object[] v) { + return null; + } + }, 128).blockingLast(); + } + + @Test(expected = NullPointerException.class) + public void combineLatestDelayErrorIterableIteratorNull() { + Flowable.combineLatestDelayError(new Iterable<Flowable<Object>>() { + @Override + public Iterator<Flowable<Object>> iterator() { + return null; + } + }, new Function<Object[], Object>() { + @Override + public Object apply(Object[] v) { + return 1; + } + }, 128).blockingLast(); + } + + @Test(expected = NullPointerException.class) + public void combineLatestDelayErrorIterableOneIsNull() { + Flowable.combineLatestDelayError(Arrays.asList(Flowable.never(), null), new Function<Object[], Object>() { + @Override + public Object apply(Object[] v) { + return 1; + } + }, 128).blockingLast(); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/flowable/FlowableReduceTests.java b/src/test/java/io/reactivex/rxjava3/flowable/FlowableReduceTests.java new file mode 100644 index 0000000000..5dd3c28cb2 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/flowable/FlowableReduceTests.java @@ -0,0 +1,159 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.flowable; + +import static org.junit.Assert.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.flowable.FlowableCovarianceTest.*; +import io.reactivex.rxjava3.functions.BiFunction; + +public class FlowableReduceTests extends RxJavaTest { + + @Test + public void reduceIntsFlowable() { + Flowable<Integer> f = Flowable.just(1, 2, 3); + int value = f.reduce(new BiFunction<Integer, Integer, Integer>() { + @Override + public Integer apply(Integer t1, Integer t2) { + return t1 + t2; + } + }).toFlowable().blockingSingle(); + + assertEquals(6, value); + } + + @SuppressWarnings("unused") + @Test + public void reduceWithObjectsFlowable() { + Flowable<Movie> horrorMovies = Flowable.<Movie> just(new HorrorMovie()); + + Flowable<Movie> reduceResult = horrorMovies.scan(new BiFunction<Movie, Movie, Movie>() { + @Override + public Movie apply(Movie t1, Movie t2) { + return t2; + } + }).takeLast(1); + + Flowable<Movie> reduceResult2 = horrorMovies.reduce(new BiFunction<Movie, Movie, Movie>() { + @Override + public Movie apply(Movie t1, Movie t2) { + return t2; + } + }).toFlowable(); + + assertNotNull(reduceResult2); + } + + /** + * Reduce consumes and produces T so can't do covariance. + * + * https://github.com/ReactiveX/RxJava/issues/360#issuecomment-24203016 + */ + @Test + public void reduceWithCovariantObjectsFlowable() { + Flowable<Movie> horrorMovies = Flowable.<Movie> just(new HorrorMovie()); + + Flowable<Movie> reduceResult2 = horrorMovies.reduce(new BiFunction<Movie, Movie, Movie>() { + @Override + public Movie apply(Movie t1, Movie t2) { + return t2; + } + }).toFlowable(); + + assertNotNull(reduceResult2); + } + + @Test + public void reduceInts() { + Flowable<Integer> f = Flowable.just(1, 2, 3); + int value = f.reduce(new BiFunction<Integer, Integer, Integer>() { + @Override + public Integer apply(Integer t1, Integer t2) { + return t1 + t2; + } + }).toFlowable().blockingSingle(); + + assertEquals(6, value); + } + + @SuppressWarnings("unused") + @Test + public void reduceWithObjects() { + Flowable<Movie> horrorMovies = Flowable.<Movie> just(new HorrorMovie()); + + Flowable<Movie> reduceResult = horrorMovies.scan(new BiFunction<Movie, Movie, Movie>() { + @Override + public Movie apply(Movie t1, Movie t2) { + return t2; + } + }).takeLast(1); + + Maybe<Movie> reduceResult2 = horrorMovies.reduce(new BiFunction<Movie, Movie, Movie>() { + @Override + public Movie apply(Movie t1, Movie t2) { + return t2; + } + }); + + assertNotNull(reduceResult2); + } + + /** + * Reduce consumes and produces T so can't do covariance. + * + * https://github.com/ReactiveX/RxJava/issues/360#issuecomment-24203016 + */ + @Test + public void reduceWithCovariantObjects() { + Flowable<Movie> horrorMovies = Flowable.<Movie> just(new HorrorMovie()); + + Maybe<Movie> reduceResult2 = horrorMovies.reduce(new BiFunction<Movie, Movie, Movie>() { + @Override + public Movie apply(Movie t1, Movie t2) { + return t2; + } + }); + + assertNotNull(reduceResult2); + } + + /** + * Reduce consumes and produces T so can't do covariance. + * + * https://github.com/ReactiveX/RxJava/issues/360#issuecomment-24203016 + */ + @Test + public void reduceCovariance() { + // must type it to <Movie> + Flowable<Movie> horrorMovies = Flowable.<Movie> just(new HorrorMovie()); + libraryFunctionActingOnMovieObservables(horrorMovies); + } + + /* + * This accepts <Movie> instead of <? super Movie> since `reduce` can't handle covariants + */ + public void libraryFunctionActingOnMovieObservables(Flowable<Movie> obs) { + + obs.reduce(new BiFunction<Movie, Movie, Movie>() { + @Override + public Movie apply(Movie t1, Movie t2) { + return t2; + } + }); + } + +} diff --git a/src/test/java/io/reactivex/rxjava3/flowable/FlowableStartWithTests.java b/src/test/java/io/reactivex/rxjava3/flowable/FlowableStartWithTests.java new file mode 100644 index 0000000000..28549b1a90 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/flowable/FlowableStartWithTests.java @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.flowable; + +import static org.junit.Assert.assertEquals; + +import java.util.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; + +public class FlowableStartWithTests extends RxJavaTest { + + @Test + public void startWith1() { + List<String> values = Flowable.just("one", "two") + .startWithArray("zero").toList().blockingGet(); + + assertEquals("zero", values.get(0)); + assertEquals("two", values.get(2)); + } + + @Test + public void startWithIterable() { + List<String> li = new ArrayList<>(); + li.add("alpha"); + li.add("beta"); + List<String> values = Flowable.just("one", "two").startWithIterable(li).toList().blockingGet(); + + assertEquals("alpha", values.get(0)); + assertEquals("beta", values.get(1)); + assertEquals("one", values.get(2)); + assertEquals("two", values.get(3)); + } + + @Test + public void startWithObservable() { + List<String> li = new ArrayList<>(); + li.add("alpha"); + li.add("beta"); + List<String> values = Flowable.just("one", "two") + .startWith(Flowable.fromIterable(li)) + .toList() + .blockingGet(); + + assertEquals("alpha", values.get(0)); + assertEquals("beta", values.get(1)); + assertEquals("one", values.get(2)); + assertEquals("two", values.get(3)); + } + + @Test + public void startWithEmpty() { + Flowable.just(1).startWithArray().test().assertResult(1); + } + +} diff --git a/src/test/java/io/reactivex/rxjava3/flowable/FlowableSubscriberTest.java b/src/test/java/io/reactivex/rxjava3/flowable/FlowableSubscriberTest.java new file mode 100644 index 0000000000..b1eb9ce600 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/flowable/FlowableSubscriberTest.java @@ -0,0 +1,816 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.flowable; + +import static org.junit.Assert.*; + +import java.util.*; +import java.util.concurrent.atomic.*; + +import org.junit.Test; +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.internal.subscribers.ForEachWhileSubscriber; +import io.reactivex.rxjava3.internal.subscriptions.BooleanSubscription; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.processors.PublishProcessor; +import io.reactivex.rxjava3.subscribers.*; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class FlowableSubscriberTest { + + /** + * Should request n for whatever the final Subscriber asks for. + */ + @Test + public void requestFromFinalSubscribeWithRequestValue() { + TestSubscriber<String> s = new TestSubscriber<>(0L); + s.request(10); + final AtomicLong r = new AtomicLong(); + s.onSubscribe(new Subscription() { + + @Override + public void request(long n) { + r.set(n); + } + + @Override + public void cancel() { + + } + + }); + assertEquals(10, r.get()); + } + + /** + * Should request -1 for infinite. + */ + @Test + public void requestFromFinalSubscribeWithoutRequestValue() { + TestSubscriber<String> s = new TestSubscriber<>(); + final AtomicLong r = new AtomicLong(); + s.onSubscribe(new Subscription() { + + @Override + public void request(long n) { + r.set(n); + } + + @Override + public void cancel() { + + } + + }); + assertEquals(Long.MAX_VALUE, r.get()); + } + + @Test + public void requestFromChainedOperator() throws Throwable { + TestSubscriber<String> s = new TestSubscriber<>(10L); + FlowableOperator<String, String> o = new FlowableOperator<String, String>() { + @Override + public Subscriber<? super String> apply(final Subscriber<? super String> s1) { + return new FlowableSubscriber<String>() { + + @Override + public void onSubscribe(Subscription a) { + s1.onSubscribe(a); + } + + @Override + public void onComplete() { + + } + + @Override + public void onError(Throwable e) { + + } + + @Override + public void onNext(String t) { + + } + + }; + } + }; + Subscriber<? super String> ns = o.apply(s); + + final AtomicLong r = new AtomicLong(); + // set set the producer at the top of the chain (ns) and it should flow through the operator to the (s) subscriber + // and then it should request up with the value set on the final Subscriber (s) + ns.onSubscribe(new Subscription() { + + @Override + public void request(long n) { + r.set(n); + } + + @Override + public void cancel() { + + } + + }); + assertEquals(10, r.get()); + } + + @Test + public void requestFromDecoupledOperator() throws Throwable { + TestSubscriber<String> s = new TestSubscriber<>(0L); + FlowableOperator<String, String> o = new FlowableOperator<String, String>() { + @Override + public Subscriber<? super String> apply(final Subscriber<? super String> s1) { + return new FlowableSubscriber<String>() { + + @Override + public void onSubscribe(Subscription a) { + s1.onSubscribe(a); + } + + @Override + public void onComplete() { + + } + + @Override + public void onError(Throwable e) { + + } + + @Override + public void onNext(String t) { + + } + + }; + } + }; + s.request(10); + Subscriber<? super String> ns = o.apply(s); + + final AtomicLong r = new AtomicLong(); + // set set the producer at the top of the chain (ns) and it should flow through the operator to the (s) subscriber + // and then it should request up with the value set on the final Subscriber (s) + ns.onSubscribe(new Subscription() { + + @Override + public void request(long n) { + r.set(n); + } + + @Override + public void cancel() { + + } + + }); + assertEquals(10, r.get()); + } + + @Test + public void requestFromDecoupledOperatorThatRequestsN() throws Throwable { + TestSubscriber<String> s = new TestSubscriber<>(10L); + final AtomicLong innerR = new AtomicLong(); + FlowableOperator<String, String> o = new FlowableOperator<String, String>() { + @Override + public Subscriber<? super String> apply(Subscriber<? super String> child) { + // we want to decouple the chain so set our own Producer on the child instead of it coming from the parent + child.onSubscribe(new Subscription() { + + @Override + public void request(long n) { + innerR.set(n); + } + + @Override + public void cancel() { + + } + + }); + + ResourceSubscriber<String> as = new ResourceSubscriber<String>() { + + @Override + protected void onStart() { + // we request 99 up to the parent + request(99); + } + + @Override + public void onComplete() { + + } + + @Override + public void onError(Throwable e) { + + } + + @Override + public void onNext(String t) { + + } + }; + + return as; + } + }; + Subscriber<? super String> ns = o.apply(s); + + final AtomicLong r = new AtomicLong(); + // set set the producer at the top of the chain (ns) and it should flow through the operator to the (s) subscriber + // and then it should request up with the value set on the final Subscriber (s) + ns.onSubscribe(new Subscription() { + + @Override + public void request(long n) { + r.set(n); + } + + @Override + public void cancel() { + + } + + }); + assertEquals(99, r.get()); + assertEquals(10, innerR.get()); + } + + @Test + public void requestToFlowable() { + TestSubscriber<Integer> ts = new TestSubscriber<>(3L); + final AtomicLong requested = new AtomicLong(); + Flowable.<Integer>unsafeCreate(new Publisher<Integer>() { + @Override + public void subscribe(Subscriber<? super Integer> s) { + s.onSubscribe(new Subscription() { + + @Override + public void request(long n) { + requested.set(n); + } + + @Override + public void cancel() { + + } + }); + } + }).subscribe(ts); + assertEquals(3, requested.get()); + } + + @Test + public void requestThroughMap() { + TestSubscriber<Integer> ts = new TestSubscriber<>(0L); + ts.request(3); + final AtomicLong requested = new AtomicLong(); + Flowable.<Integer>unsafeCreate(new Publisher<Integer>() { + @Override + public void subscribe(Subscriber<? super Integer> s) { + s.onSubscribe(new Subscription() { + + @Override + public void request(long n) { + requested.set(n); + } + + @Override + public void cancel() { + + } + }); + } + }).map(Functions.<Integer>identity()).subscribe(ts); + assertEquals(3, requested.get()); + } + + @Test + public void requestThroughTakeThatReducesRequest() { + TestSubscriber<Integer> ts = new TestSubscriber<>(0L); + ts.request(3); + final AtomicLong requested = new AtomicLong(); + Flowable.<Integer>unsafeCreate(new Publisher<Integer>() { + @Override + public void subscribe(Subscriber<? super Integer> s) { + s.onSubscribe(new Subscription() { + + @Override + public void request(long n) { + requested.set(n); + } + + @Override + public void cancel() { + + } + + }); + } + }).take(2).subscribe(ts); + + assertEquals(2, requested.get()); + } + + @Test + public void requestThroughTakeWhereRequestIsSmallerThanTake() { + TestSubscriber<Integer> ts = new TestSubscriber<>(0L); + ts.request(3); + final AtomicLong requested = new AtomicLong(); + Flowable.<Integer>unsafeCreate(new Publisher<Integer>() { + @Override + public void subscribe(Subscriber<? super Integer> s) { + s.onSubscribe(new Subscription() { + + @Override + public void request(long n) { + requested.set(n); + } + + @Override + public void cancel() { + + } + + }); + } + }).take(10).subscribe(ts); + assertEquals(3, requested.get()); + } + + @Test + public void onStartCalledOnceViaSubscribe() { + final AtomicInteger c = new AtomicInteger(); + Flowable.just(1, 2, 3, 4).take(2).subscribe(new DefaultSubscriber<Integer>() { + + @Override + public void onStart() { + c.incrementAndGet(); + request(1); + } + + @Override + public void onComplete() { + + } + + @Override + public void onError(Throwable e) { + + } + + @Override + public void onNext(Integer t) { + request(1); + } + + }); + + assertEquals(1, c.get()); + } + + @Test + public void onStartCalledOnceViaUnsafeSubscribe() { + final AtomicInteger c = new AtomicInteger(); + Flowable.just(1, 2, 3, 4).take(2).subscribe(new DefaultSubscriber<Integer>() { + + @Override + public void onStart() { + c.incrementAndGet(); + request(1); + } + + @Override + public void onComplete() { + + } + + @Override + public void onError(Throwable e) { + + } + + @Override + public void onNext(Integer t) { + request(1); + } + + }); + + assertEquals(1, c.get()); + } + + @Test + public void onStartCalledOnceViaLift() { + final AtomicInteger c = new AtomicInteger(); + Flowable.just(1, 2, 3, 4).lift(new FlowableOperator<Integer, Integer>() { + + @Override + public Subscriber<? super Integer> apply(final Subscriber<? super Integer> child) { + return new DefaultSubscriber<Integer>() { + + @Override + public void onStart() { + c.incrementAndGet(); + request(1); + } + + @Override + public void onComplete() { + child.onComplete(); + } + + @Override + public void onError(Throwable e) { + child.onError(e); + } + + @Override + public void onNext(Integer t) { + child.onNext(t); + request(1); + } + + }; + } + + }).subscribe(); + + assertEquals(1, c.get()); + } + + @Test + public void onStartRequestsAreAdditive() { + final List<Integer> list = new ArrayList<>(); + Flowable.just(1, 2, 3, 4, 5) + .subscribe(new DefaultSubscriber<Integer>() { + @Override + public void onStart() { + request(3); + request(2); + } + + @Override + public void onComplete() { + + } + + @Override + public void onError(Throwable e) { + + } + + @Override + public void onNext(Integer t) { + list.add(t); + }}); + assertEquals(Arrays.asList(1, 2, 3, 4, 5), list); + } + + @Test + public void onStartRequestsAreAdditiveAndOverflowBecomesMaxValue() { + final List<Integer> list = new ArrayList<>(); + Flowable.just(1, 2, 3, 4, 5).subscribe(new DefaultSubscriber<Integer>() { + @Override + public void onStart() { + request(2); + request(Long.MAX_VALUE - 1); + } + + @Override + public void onComplete() { + + } + + @Override + public void onError(Throwable e) { + + } + + @Override + public void onNext(Integer t) { + list.add(t); + }}); + assertEquals(Arrays.asList(1, 2, 3, 4, 5), list); + } + + @Test + public void forEachWhile() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + + final List<Integer> list = new ArrayList<>(); + + Disposable d = pp.forEachWhile(new Predicate<Integer>() { + @Override + public boolean test(Integer v) throws Exception { + list.add(v); + return v < 3; + } + }); + + assertFalse(d.isDisposed()); + + pp.onNext(1); + pp.onNext(2); + pp.onNext(3); + + assertFalse(pp.hasSubscribers()); + + assertEquals(Arrays.asList(1, 2, 3), list); + } + + @Test + public void doubleSubscribe() { + ForEachWhileSubscriber<Integer> s = new ForEachWhileSubscriber<>(new Predicate<Integer>() { + @Override + public boolean test(Integer v) throws Exception { + return true; + } + }, Functions.<Throwable>emptyConsumer(), Functions.EMPTY_ACTION); + + List<Throwable> list = TestHelper.trackPluginErrors(); + + try { + s.onSubscribe(new BooleanSubscription()); + + BooleanSubscription bs = new BooleanSubscription(); + s.onSubscribe(bs); + + assertTrue(bs.isCancelled()); + + TestHelper.assertError(list, 0, IllegalStateException.class, "Subscription already set!"); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void suppressAfterCompleteEvents() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + final TestSubscriber<Integer> ts = new TestSubscriber<>(); + ts.onSubscribe(new BooleanSubscription()); + + ForEachWhileSubscriber<Integer> s = new ForEachWhileSubscriber<>(new Predicate<Integer>() { + @Override + public boolean test(Integer v) throws Exception { + ts.onNext(v); + return true; + } + }, new Consumer<Throwable>() { + @Override + public void accept(Throwable e) throws Exception { + ts.onError(e); + } + }, new Action() { + @Override + public void run() throws Exception { + ts.onComplete(); + } + }); + + s.onComplete(); + s.onNext(1); + s.onError(new TestException()); + s.onComplete(); + + ts.assertResult(); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void onNextCrashes() { + final TestSubscriber<Integer> ts = new TestSubscriber<>(); + ts.onSubscribe(new BooleanSubscription()); + + ForEachWhileSubscriber<Integer> s = new ForEachWhileSubscriber<>(new Predicate<Integer>() { + @Override + public boolean test(Integer v) throws Exception { + throw new TestException(); + } + }, new Consumer<Throwable>() { + @Override + public void accept(Throwable e) throws Exception { + ts.onError(e); + } + }, new Action() { + @Override + public void run() throws Exception { + ts.onComplete(); + } + }); + + BooleanSubscription b = new BooleanSubscription(); + + s.onSubscribe(b); + s.onNext(1); + + assertTrue(b.isCancelled()); + ts.assertFailure(TestException.class); + } + + @Test + public void onErrorThrows() { + ForEachWhileSubscriber<Integer> s = new ForEachWhileSubscriber<>(new Predicate<Integer>() { + @Override + public boolean test(Integer v) throws Exception { + return true; + } + }, new Consumer<Throwable>() { + @Override + public void accept(Throwable e) throws Exception { + throw new TestException("Inner"); + } + }, new Action() { + @Override + public void run() throws Exception { + + } + }); + + List<Throwable> list = TestHelper.trackPluginErrors(); + + try { + s.onSubscribe(new BooleanSubscription()); + + s.onError(new TestException("Outer")); + + TestHelper.assertError(list, 0, CompositeException.class); + List<Throwable> cel = TestHelper.compositeList(list.get(0)); + TestHelper.assertError(cel, 0, TestException.class, "Outer"); + TestHelper.assertError(cel, 1, TestException.class, "Inner"); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void onCompleteThrows() { + ForEachWhileSubscriber<Integer> s = new ForEachWhileSubscriber<>(new Predicate<Integer>() { + @Override + public boolean test(Integer v) throws Exception { + return true; + } + }, new Consumer<Throwable>() { + @Override + public void accept(Throwable e) throws Exception { + } + }, new Action() { + @Override + public void run() throws Exception { + throw new TestException("Inner"); + } + }); + + List<Throwable> list = TestHelper.trackPluginErrors(); + + try { + s.onSubscribe(new BooleanSubscription()); + + s.onComplete(); + + TestHelper.assertUndeliverable(list, 0, TestException.class, "Inner"); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void subscribeConsumerConsumerWithError() { + final List<Integer> list = new ArrayList<>(); + + Flowable.<Integer>error(new TestException()).subscribe(new Consumer<Integer>() { + @Override + public void accept(Integer v) throws Exception { + list.add(v); + } + }, new Consumer<Throwable>() { + @Override + public void accept(Throwable e) throws Exception { + list.add(100); + } + }); + + assertEquals(Arrays.asList(100), list); + } + + @Test + public void methodTestCancelled() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + + pp.test(Long.MAX_VALUE, true); + + assertFalse(pp.hasSubscribers()); + } + + @Test + public void safeSubscriberAlreadySafe() { + TestSubscriber<Integer> ts = new TestSubscriber<>(); + Flowable.just(1).safeSubscribe(new SafeSubscriber<>(ts)); + + ts.assertResult(1); + } + + @Test + public void methodTestNoCancel() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + + pp.test(Long.MAX_VALUE, false); + + assertTrue(pp.hasSubscribers()); + } + + @Test + public void subscribeConsumerConsumer() { + final List<Integer> list = new ArrayList<>(); + + Flowable.just(1).subscribe(new Consumer<Integer>() { + @Override + public void accept(Integer v) throws Exception { + list.add(v); + } + }, new Consumer<Throwable>() { + @Override + public void accept(Throwable e) throws Exception { + list.add(100); + } + }); + + assertEquals(Arrays.asList(1), list); + } + + @SuppressWarnings("rawtypes") + @Test + public void pluginNull() { + RxJavaPlugins.setOnFlowableSubscribe(new BiFunction<Flowable, Subscriber, Subscriber>() { + @Override + public Subscriber apply(Flowable a, Subscriber b) throws Exception { + return null; + } + }); + + try { + try { + + Flowable.just(1).test(); + fail("Should have thrown"); + } catch (NullPointerException ex) { + assertEquals("The RxJavaPlugins.onSubscribe hook returned a null FlowableSubscriber. Please check the handler provided to RxJavaPlugins.setOnFlowableSubscribe for invalid null returns. Further reading: https://github.com/ReactiveX/RxJava/wiki/Plugins", ex.getMessage()); + } + } finally { + RxJavaPlugins.reset(); + } + } + + static final class BadFlowable extends Flowable<Integer> { + @Override + protected void subscribeActual(Subscriber<? super Integer> s) { + throw new IllegalArgumentException(); + } + } + + @Test + public void subscribeActualThrows() { + List<Throwable> list = TestHelper.trackPluginErrors(); + try { + try { + new BadFlowable().test(); + fail("Should have thrown!"); + } catch (NullPointerException ex) { + if (!(ex.getCause() instanceof IllegalArgumentException)) { + fail(ex.toString() + ": Should be NPE(IAE)"); + } + } + + TestHelper.assertError(list, 0, IllegalArgumentException.class); + } finally { + RxJavaPlugins.reset(); + } + } +} diff --git a/src/test/java/io/reactivex/rxjava3/flowable/FlowableTests.java b/src/test/java/io/reactivex/rxjava3/flowable/FlowableTests.java new file mode 100644 index 0000000000..1fa80701be --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/flowable/FlowableTests.java @@ -0,0 +1,1097 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.flowable; + +import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +import java.util.*; +import java.util.concurrent.*; +import java.util.concurrent.atomic.*; + +import org.junit.*; +import org.mockito.InOrder; +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.flowables.ConnectableFlowable; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.internal.subscriptions.BooleanSubscription; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.processors.*; +import io.reactivex.rxjava3.schedulers.*; +import io.reactivex.rxjava3.subscribers.*; +import io.reactivex.rxjava3.testsupport.*; + +public class FlowableTests extends RxJavaTest { + + Subscriber<Number> w; + + SingleObserver<Number> wo; + + MaybeObserver<Number> wm; + + private static final Predicate<Integer> IS_EVEN = new Predicate<Integer>() { + @Override + public boolean test(Integer v) { + return v % 2 == 0; + } + }; + + @Before + public void before() { + w = TestHelper.mockSubscriber(); + wo = TestHelper.mockSingleObserver(); + wm = TestHelper.mockMaybeObserver(); + } + + @Test + public void fromArray() { + String[] items = new String[] { "one", "two", "three" }; + assertEquals((Long)3L, Flowable.fromArray(items).count().blockingGet()); + assertEquals("two", Flowable.fromArray(items).skip(1).take(1).blockingSingle()); + assertEquals("three", Flowable.fromArray(items).takeLast(1).blockingSingle()); + } + + @Test + public void fromIterable() { + ArrayList<String> items = new ArrayList<>(); + items.add("one"); + items.add("two"); + items.add("three"); + + assertEquals((Long)3L, Flowable.fromIterable(items).count().blockingGet()); + assertEquals("two", Flowable.fromIterable(items).skip(1).take(1).blockingSingle()); + assertEquals("three", Flowable.fromIterable(items).takeLast(1).blockingSingle()); + } + + @Test + public void fromArityArgs3() { + Flowable<String> items = Flowable.just("one", "two", "three"); + + assertEquals((Long)3L, items.count().blockingGet()); + assertEquals("two", items.skip(1).take(1).blockingSingle()); + assertEquals("three", items.takeLast(1).blockingSingle()); + } + + @Test + public void fromArityArgs1() { + Flowable<String> items = Flowable.just("one"); + + assertEquals((Long)1L, items.count().blockingGet()); + assertEquals("one", items.takeLast(1).blockingSingle()); + } + + @Test + public void create() { + + Flowable<String> flowable = Flowable.just("one", "two", "three"); + + Subscriber<String> subscriber = TestHelper.mockSubscriber(); + + flowable.subscribe(subscriber); + + verify(subscriber, times(1)).onNext("one"); + verify(subscriber, times(1)).onNext("two"); + verify(subscriber, times(1)).onNext("three"); + verify(subscriber, never()).onError(any(Throwable.class)); + verify(subscriber, times(1)).onComplete(); + } + + @Test + public void countAFewItemsFlowable() { + Flowable<String> flowable = Flowable.just("a", "b", "c", "d"); + + flowable.count().toFlowable().subscribe(w); + + // we should be called only once + verify(w).onNext(4L); + verify(w, never()).onError(any(Throwable.class)); + verify(w, times(1)).onComplete(); + } + + @Test + public void countZeroItemsFlowable() { + Flowable<String> flowable = Flowable.empty(); + flowable.count().toFlowable().subscribe(w); + // we should be called only once + verify(w).onNext(0L); + verify(w, never()).onError(any(Throwable.class)); + verify(w, times(1)).onComplete(); + } + + @Test + public void countErrorFlowable() { + Flowable<String> f = Flowable.error(new Supplier<Throwable>() { + @Override + public Throwable get() { + return new RuntimeException(); + } + }); + + f.count().toFlowable().subscribe(w); + verify(w, never()).onNext(anyInt()); + verify(w, never()).onComplete(); + verify(w, times(1)).onError(any(RuntimeException.class)); + } + + @Test + public void countAFewItems() { + Flowable<String> flowable = Flowable.just("a", "b", "c", "d"); + + flowable.count().subscribe(wo); + + // we should be called only once + verify(wo).onSuccess(4L); + verify(wo, never()).onError(any(Throwable.class)); + } + + @Test + public void countZeroItems() { + Flowable<String> flowable = Flowable.empty(); + flowable.count().subscribe(wo); + // we should be called only once + verify(wo).onSuccess(0L); + verify(wo, never()).onError(any(Throwable.class)); + } + + @Test + public void countError() { + Flowable<String> f = Flowable.error(new Supplier<Throwable>() { + @Override + public Throwable get() { + return new RuntimeException(); + } + }); + + f.count().subscribe(wo); + verify(wo, never()).onSuccess(anyInt()); + verify(wo, times(1)).onError(any(RuntimeException.class)); + } + + @Test + public void takeFirstWithPredicateOfSome() { + Flowable<Integer> flowable = Flowable.just(1, 3, 5, 4, 6, 3); + flowable.filter(IS_EVEN).take(1).subscribe(w); + verify(w, times(1)).onNext(anyInt()); + verify(w).onNext(4); + verify(w, times(1)).onComplete(); + verify(w, never()).onError(any(Throwable.class)); + } + + @Test + public void takeFirstWithPredicateOfNoneMatchingThePredicate() { + Flowable<Integer> flowable = Flowable.just(1, 3, 5, 7, 9, 7, 5, 3, 1); + flowable.filter(IS_EVEN).take(1).subscribe(w); + verify(w, never()).onNext(anyInt()); + verify(w, times(1)).onComplete(); + verify(w, never()).onError(any(Throwable.class)); + } + + @Test + public void takeFirstOfSome() { + Flowable<Integer> flowable = Flowable.just(1, 2, 3); + flowable.take(1).subscribe(w); + verify(w, times(1)).onNext(anyInt()); + verify(w).onNext(1); + verify(w, times(1)).onComplete(); + verify(w, never()).onError(any(Throwable.class)); + } + + @Test + public void takeFirstOfNone() { + Flowable<Integer> flowable = Flowable.empty(); + flowable.take(1).subscribe(w); + verify(w, never()).onNext(anyInt()); + verify(w, times(1)).onComplete(); + verify(w, never()).onError(any(Throwable.class)); + } + + @Test + public void firstOfNoneFlowable() { + Flowable<Integer> flowable = Flowable.empty(); + flowable.firstElement().toFlowable().subscribe(w); + verify(w, never()).onNext(anyInt()); + verify(w).onComplete(); + verify(w, never()).onError(any(Throwable.class)); + } + + @Test + public void firstWithPredicateOfNoneMatchingThePredicateFlowable() { + Flowable<Integer> flowable = Flowable.just(1, 3, 5, 7, 9, 7, 5, 3, 1); + flowable.filter(IS_EVEN).firstElement().toFlowable().subscribe(w); + verify(w, never()).onNext(anyInt()); + verify(w).onComplete(); + verify(w, never()).onError(any(Throwable.class)); + } + + @Test + public void firstOfNone() { + Flowable<Integer> flowable = Flowable.empty(); + flowable.firstElement().subscribe(wm); + verify(wm, never()).onSuccess(anyInt()); + verify(wm).onComplete(); + verify(wm, never()).onError(isA(NoSuchElementException.class)); + } + + @Test + public void firstWithPredicateOfNoneMatchingThePredicate() { + Flowable<Integer> flowable = Flowable.just(1, 3, 5, 7, 9, 7, 5, 3, 1); + flowable.filter(IS_EVEN).firstElement().subscribe(wm); + verify(wm, never()).onSuccess(anyInt()); + verify(wm, times(1)).onComplete(); + verify(wm, never()).onError(isA(NoSuchElementException.class)); + } + + @Test + public void reduce() { + Flowable<Integer> flowable = Flowable.just(1, 2, 3, 4); + flowable.reduce(new BiFunction<Integer, Integer, Integer>() { + @Override + public Integer apply(Integer t1, Integer t2) { + return t1 + t2; + } + }) + .toFlowable() + .subscribe(w); + // we should be called only once + verify(w, times(1)).onNext(anyInt()); + verify(w).onNext(10); + } + + @Test + public void reduceWithEmptyObservable() { + Flowable<Integer> flowable = Flowable.range(1, 0); + flowable.reduce(new BiFunction<Integer, Integer, Integer>() { + @Override + public Integer apply(Integer t1, Integer t2) { + return t1 + t2; + } + }) + .toFlowable() + .test() + .assertResult(); + } + + /** + * A reduce on an empty Observable and a seed should just pass the seed through. + * + * This is confirmed at https://github.com/ReactiveX/RxJava/issues/423#issuecomment-27642456 + */ + @Test + public void reduceWithEmptyObservableAndSeed() { + Flowable<Integer> flowable = Flowable.range(1, 0); + int value = flowable.reduce(1, new BiFunction<Integer, Integer, Integer>() { + @Override + public Integer apply(Integer t1, Integer t2) { + return t1 + t2; + } + }) + .blockingGet(); + + assertEquals(1, value); + } + + @Test + public void reduceWithInitialValue() { + Flowable<Integer> flowable = Flowable.just(1, 2, 3, 4); + flowable.reduce(50, new BiFunction<Integer, Integer, Integer>() { + @Override + public Integer apply(Integer t1, Integer t2) { + return t1 + t2; + } + }) + .subscribe(wo); + // we should be called only once + verify(wo, times(1)).onSuccess(anyInt()); + verify(wo).onSuccess(60); + } + + @Test + public void materializeDematerializeChaining() { + Flowable<Integer> obs = Flowable.just(1); + Flowable<Integer> chained = obs.materialize() + .dematerialize(Functions.<Notification<Integer>>identity()); + + Subscriber<Integer> subscriber = TestHelper.mockSubscriber(); + + chained.subscribe(subscriber); + + verify(subscriber, times(1)).onNext(1); + verify(subscriber, times(1)).onComplete(); + verify(subscriber, times(0)).onError(any(Throwable.class)); + } + + /** + * The error from the user provided Observer is not handled by the subscribe method try/catch. + * + * It is handled by the AtomicObserver that wraps the provided Observer. + * + * Result: Passes (if AtomicObserver functionality exists) + * @throws InterruptedException if the test is interrupted + */ + @Test + public void customObservableWithErrorInObserverAsynchronous() throws InterruptedException { + final CountDownLatch latch = new CountDownLatch(1); + final AtomicInteger count = new AtomicInteger(); + final AtomicReference<Throwable> error = new AtomicReference<>(); + + // FIXME custom built??? + Flowable.just("1", "2", "three", "4") + .subscribeOn(Schedulers.newThread()) + .safeSubscribe(new DefaultSubscriber<String>() { + @Override + public void onComplete() { + System.out.println("completed"); + latch.countDown(); + } + + @Override + public void onError(Throwable e) { + error.set(e); + System.out.println("error"); + e.printStackTrace(); + latch.countDown(); + } + + @Override + public void onNext(String v) { + int num = Integer.parseInt(v); + System.out.println(num); + // doSomething(num); + count.incrementAndGet(); + } + + }); + + // wait for async sequence to complete + latch.await(); + + assertEquals(2, count.get()); + assertNotNull(error.get()); + if (!(error.get() instanceof NumberFormatException)) { + fail("It should be a NumberFormatException"); + } + } + + /** + * The error from the user provided Observer is handled by the subscribe try/catch because this is synchronous. + * + * Result: Passes + */ + @Test + public void customObservableWithErrorInObserverSynchronous() { + final AtomicInteger count = new AtomicInteger(); + final AtomicReference<Throwable> error = new AtomicReference<>(); + + // FIXME custom built??? + Flowable.just("1", "2", "three", "4") + .safeSubscribe(new DefaultSubscriber<String>() { + + @Override + public void onComplete() { + System.out.println("completed"); + } + + @Override + public void onError(Throwable e) { + error.set(e); + System.out.println("error"); + e.printStackTrace(); + } + + @Override + public void onNext(String v) { + int num = Integer.parseInt(v); + System.out.println(num); + // doSomething(num); + count.incrementAndGet(); + } + + }); + assertEquals(2, count.get()); + assertNotNull(error.get()); + if (!(error.get() instanceof NumberFormatException)) { + fail("It should be a NumberFormatException"); + } + } + + /** + * The error from the user provided Observable is handled by the subscribe try/catch because this is synchronous. + * + * Result: Passes + */ + @Test + public void customObservableWithErrorInObservableSynchronous() { + final AtomicInteger count = new AtomicInteger(); + final AtomicReference<Throwable> error = new AtomicReference<>(); + // FIXME custom built??? + Flowable.just("1", "2").concatWith(Flowable.<String>error(new Supplier<Throwable>() { + @Override + public Throwable get() { + return new NumberFormatException(); + } + })) + .subscribe(new DefaultSubscriber<String>() { + + @Override + public void onComplete() { + System.out.println("completed"); + } + + @Override + public void onError(Throwable e) { + error.set(e); + System.out.println("error"); + e.printStackTrace(); + } + + @Override + public void onNext(String v) { + System.out.println(v); + count.incrementAndGet(); + } + + }); + assertEquals(2, count.get()); + assertNotNull(error.get()); + if (!(error.get() instanceof NumberFormatException)) { + fail("It should be a NumberFormatException"); + } + } + + @Test + public void publishLast() throws InterruptedException { + final AtomicInteger count = new AtomicInteger(); + ConnectableFlowable<String> connectable = Flowable.<String>unsafeCreate(new Publisher<String>() { + @Override + public void subscribe(final Subscriber<? super String> subscriber) { + subscriber.onSubscribe(new BooleanSubscription()); + count.incrementAndGet(); + new Thread(new Runnable() { + @Override + public void run() { + subscriber.onNext("first"); + subscriber.onNext("last"); + subscriber.onComplete(); + } + }).start(); + } + }).takeLast(1).publish(); + + // subscribe once + final CountDownLatch latch = new CountDownLatch(1); + connectable.subscribe(new Consumer<String>() { + @Override + public void accept(String value) { + assertEquals("last", value); + latch.countDown(); + } + }); + + // subscribe twice + connectable.subscribe(); + + Disposable subscription = connectable.connect(); + assertTrue(latch.await(1000, TimeUnit.MILLISECONDS)); + assertEquals(1, count.get()); + subscription.dispose(); + } + + @Test + public void replay() throws InterruptedException { + final AtomicInteger counter = new AtomicInteger(); + ConnectableFlowable<String> f = Flowable.<String>unsafeCreate(new Publisher<String>() { + @Override + public void subscribe(final Subscriber<? super String> subscriber) { + subscriber.onSubscribe(new BooleanSubscription()); + new Thread(new Runnable() { + + @Override + public void run() { + counter.incrementAndGet(); + subscriber.onNext("one"); + subscriber.onComplete(); + } + }).start(); + } + }).replay(); + + // we connect immediately and it will emit the value + Disposable connection = f.connect(); + try { + + // we then expect the following 2 subscriptions to get that same value + final CountDownLatch latch = new CountDownLatch(2); + + // subscribe once + f.subscribe(new Consumer<String>() { + @Override + public void accept(String v) { + assertEquals("one", v); + latch.countDown(); + } + }); + + // subscribe again + f.subscribe(new Consumer<String>() { + @Override + public void accept(String v) { + assertEquals("one", v); + latch.countDown(); + } + }); + + if (!latch.await(1000, TimeUnit.MILLISECONDS)) { + fail("subscriptions did not receive values"); + } + assertEquals(1, counter.get()); + } finally { + connection.dispose(); + } + } + + @Test + public void cache() throws InterruptedException { + final AtomicInteger counter = new AtomicInteger(); + Flowable<String> f = Flowable.<String>unsafeCreate(new Publisher<String>() { + @Override + public void subscribe(final Subscriber<? super String> subscriber) { + subscriber.onSubscribe(new BooleanSubscription()); + new Thread(new Runnable() { + @Override + public void run() { + counter.incrementAndGet(); + subscriber.onNext("one"); + subscriber.onComplete(); + } + }).start(); + } + }).cache(); + + // we then expect the following 2 subscriptions to get that same value + final CountDownLatch latch = new CountDownLatch(2); + + // subscribe once + f.subscribe(new Consumer<String>() { + @Override + public void accept(String v) { + assertEquals("one", v); + latch.countDown(); + } + }); + + // subscribe again + f.subscribe(new Consumer<String>() { + @Override + public void accept(String v) { + assertEquals("one", v); + latch.countDown(); + } + }); + + if (!latch.await(1000, TimeUnit.MILLISECONDS)) { + fail("subscriptions did not receive values"); + } + assertEquals(1, counter.get()); + } + + @Test + public void cacheWithCapacity() throws InterruptedException { + final AtomicInteger counter = new AtomicInteger(); + Flowable<String> f = Flowable.<String>unsafeCreate(new Publisher<String>() { + @Override + public void subscribe(final Subscriber<? super String> subscriber) { + subscriber.onSubscribe(new BooleanSubscription()); + new Thread(new Runnable() { + @Override + public void run() { + counter.incrementAndGet(); + subscriber.onNext("one"); + subscriber.onComplete(); + } + }).start(); + } + }).cacheWithInitialCapacity(1); + + // we then expect the following 2 subscriptions to get that same value + final CountDownLatch latch = new CountDownLatch(2); + + // subscribe once + f.subscribe(new Consumer<String>() { + @Override + public void accept(String v) { + assertEquals("one", v); + latch.countDown(); + } + }); + + // subscribe again + f.subscribe(new Consumer<String>() { + @Override + public void accept(String v) { + assertEquals("one", v); + latch.countDown(); + } + }); + + if (!latch.await(1000, TimeUnit.MILLISECONDS)) { + fail("subscriptions did not receive values"); + } + assertEquals(1, counter.get()); + } + + @Test + public void takeWithErrorInObserver() { + final AtomicInteger count = new AtomicInteger(); + final AtomicReference<Throwable> error = new AtomicReference<>(); + Flowable.just("1", "2", "three", "4").take(3) + .safeSubscribe(new DefaultSubscriber<String>() { + + @Override + public void onComplete() { + System.out.println("completed"); + } + + @Override + public void onError(Throwable e) { + error.set(e); + System.out.println("error"); + e.printStackTrace(); + } + + @Override + public void onNext(String v) { + int num = Integer.parseInt(v); + System.out.println(num); + // doSomething(num); + count.incrementAndGet(); + } + + }); + assertEquals(2, count.get()); + assertNotNull(error.get()); + if (!(error.get() instanceof NumberFormatException)) { + fail("It should be a NumberFormatException"); + } + } + + @Test + public void ofType() { + Flowable<String> flowable = Flowable.just(1, "abc", false, 2L).ofType(String.class); + + Subscriber<Object> subscriber = TestHelper.mockSubscriber(); + + flowable.subscribe(subscriber); + + verify(subscriber, never()).onNext(1); + verify(subscriber, times(1)).onNext("abc"); + verify(subscriber, never()).onNext(false); + verify(subscriber, never()).onNext(2L); + verify(subscriber, never()).onError(any(Throwable.class)); + verify(subscriber, times(1)).onComplete(); + } + + @Test + public void ofTypeWithPolymorphism() { + ArrayList<Integer> l1 = new ArrayList<>(); + l1.add(1); + LinkedList<Integer> l2 = new LinkedList<>(); + l2.add(2); + + @SuppressWarnings("rawtypes") + Flowable<List> flowable = Flowable.<Object> just(l1, l2, "123").ofType(List.class); + + Subscriber<Object> subscriber = TestHelper.mockSubscriber(); + + flowable.subscribe(subscriber); + + verify(subscriber, times(1)).onNext(l1); + verify(subscriber, times(1)).onNext(l2); + verify(subscriber, never()).onNext("123"); + verify(subscriber, never()).onError(any(Throwable.class)); + verify(subscriber, times(1)).onComplete(); + } + + @Test + public void containsFlowable() { + Flowable<Boolean> flowable = Flowable.just("a", "b", "c").contains("b").toFlowable(); + + FlowableSubscriber<Boolean> subscriber = TestHelper.mockSubscriber(); + + flowable.subscribe(subscriber); + + verify(subscriber, times(1)).onNext(true); + verify(subscriber, never()).onNext(false); + verify(subscriber, never()).onError(any(Throwable.class)); + verify(subscriber, times(1)).onComplete(); + } + + @Test + public void containsWithInexistenceFlowable() { + Flowable<Boolean> flowable = Flowable.just("a", "b").contains("c").toFlowable(); + + Subscriber<Object> subscriber = TestHelper.mockSubscriber(); + + flowable.subscribe(subscriber); + + verify(subscriber, times(1)).onNext(false); + verify(subscriber, never()).onNext(true); + verify(subscriber, never()).onError(any(Throwable.class)); + verify(subscriber, times(1)).onComplete(); + } + + @Test + public void containsWithEmptyObservableFlowable() { + Flowable<Boolean> flowable = Flowable.<String> empty().contains("a").toFlowable(); + + FlowableSubscriber<Object> subscriber = TestHelper.mockSubscriber(); + + flowable.subscribe(subscriber); + + verify(subscriber, times(1)).onNext(false); + verify(subscriber, never()).onNext(true); + verify(subscriber, never()).onError(any(Throwable.class)); + verify(subscriber, times(1)).onComplete(); + } + + @Test + public void contains() { + Single<Boolean> single = Flowable.just("a", "b", "c").contains("b"); // FIXME nulls not allowed, changed to "c" + + SingleObserver<Boolean> observer = TestHelper.mockSingleObserver(); + + single.subscribe(observer); + + verify(observer, times(1)).onSuccess(true); + verify(observer, never()).onSuccess(false); + verify(observer, never()).onError( + any(Throwable.class)); + } + + @Test + public void containsWithInexistence() { + Single<Boolean> single = Flowable.just("a", "b").contains("c"); // FIXME null values are not allowed, removed + + SingleObserver<Boolean> observer = TestHelper.mockSingleObserver(); + + single.subscribe(observer); + + verify(observer, times(1)).onSuccess(false); + verify(observer, never()).onSuccess(true); + verify(observer, never()).onError( + any(Throwable.class)); + } + + @Test + public void containsWithEmptyObservable() { + Single<Boolean> single = Flowable.<String> empty().contains("a"); + + SingleObserver<Boolean> observer = TestHelper.mockSingleObserver(); + + single.subscribe(observer); + + verify(observer, times(1)).onSuccess(false); + verify(observer, never()).onSuccess(true); + verify(observer, never()).onError( + any(Throwable.class)); + } + + @Test + public void ignoreElementsFlowable() { + Flowable<Integer> flowable = Flowable.just(1, 2, 3).ignoreElements().toFlowable(); + + Subscriber<Object> subscriber = TestHelper.mockSubscriber(); + + flowable.subscribe(subscriber); + + verify(subscriber, never()).onNext(any(Integer.class)); + verify(subscriber, never()).onError(any(Throwable.class)); + verify(subscriber, times(1)).onComplete(); + } + + @Test + public void ignoreElements() { + Completable completable = Flowable.just(1, 2, 3).ignoreElements(); + + CompletableObserver observer = TestHelper.mockCompletableObserver(); + + completable.subscribe(observer); + + verify(observer, never()).onError(any(Throwable.class)); + verify(observer, times(1)).onComplete(); + } + + @Test + public void justWithScheduler() { + TestScheduler scheduler = new TestScheduler(); + Flowable<Integer> flowable = Flowable.fromArray(1, 2).subscribeOn(scheduler); + + Subscriber<Integer> subscriber = TestHelper.mockSubscriber(); + + flowable.subscribe(subscriber); + + scheduler.advanceTimeBy(1, TimeUnit.MILLISECONDS); + + InOrder inOrder = inOrder(subscriber); + inOrder.verify(subscriber, times(1)).onNext(1); + inOrder.verify(subscriber, times(1)).onNext(2); + inOrder.verify(subscriber, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void startWithWithScheduler() { + TestScheduler scheduler = new TestScheduler(); + Flowable<Integer> flowable = Flowable.just(3, 4).startWithIterable(Arrays.asList(1, 2)).subscribeOn(scheduler); + + Subscriber<Integer> subscriber = TestHelper.mockSubscriber(); + + flowable.subscribe(subscriber); + + scheduler.advanceTimeBy(1, TimeUnit.MILLISECONDS); + + InOrder inOrder = inOrder(subscriber); + inOrder.verify(subscriber, times(1)).onNext(1); + inOrder.verify(subscriber, times(1)).onNext(2); + inOrder.verify(subscriber, times(1)).onNext(3); + inOrder.verify(subscriber, times(1)).onNext(4); + inOrder.verify(subscriber, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void rangeWithScheduler() { + TestScheduler scheduler = new TestScheduler(); + Flowable<Integer> flowable = Flowable.range(3, 4).subscribeOn(scheduler); + + Subscriber<Integer> subscriber = TestHelper.mockSubscriber(); + + flowable.subscribe(subscriber); + + scheduler.advanceTimeBy(1, TimeUnit.MILLISECONDS); + + InOrder inOrder = inOrder(subscriber); + inOrder.verify(subscriber, times(1)).onNext(3); + inOrder.verify(subscriber, times(1)).onNext(4); + inOrder.verify(subscriber, times(1)).onNext(5); + inOrder.verify(subscriber, times(1)).onNext(6); + inOrder.verify(subscriber, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void mergeWith() { + TestSubscriber<Integer> ts = new TestSubscriber<>(); + Flowable.just(1).mergeWith(Flowable.just(2)).subscribe(ts); + ts.assertValues(1, 2); + } + + @Test + public void concatWith() { + TestSubscriber<Integer> ts = new TestSubscriber<>(); + Flowable.just(1).concatWith(Flowable.just(2)).subscribe(ts); + ts.assertValues(1, 2); + } + + @Test + public void ambWith() { + TestSubscriber<Integer> ts = new TestSubscriber<>(); + Flowable.just(1).ambWith(Flowable.just(2)).subscribe(ts); + ts.assertValue(1); + } + + @Test + public void takeWhileToList() { + final int expectedCount = 3; + final AtomicInteger count = new AtomicInteger(); + for (int i = 0; i < expectedCount; i++) { + Flowable + .just(Boolean.TRUE, Boolean.FALSE) + .takeWhile(new Predicate<Boolean>() { + @Override + public boolean test(Boolean v) { + return v; + } + }) + .toList() + .doOnSuccess(new Consumer<List<Boolean>>() { + @Override + public void accept(List<Boolean> booleans) { + count.incrementAndGet(); + } + }) + .subscribe(); + } + assertEquals(expectedCount, count.get()); + } + + @Test + public void compose() { + TestSubscriberEx<String> ts = new TestSubscriberEx<>(); + Flowable.just(1, 2, 3).compose(new FlowableTransformer<Integer, String>() { + @Override + public Publisher<String> apply(Flowable<Integer> t1) { + return t1.map(new Function<Integer, String>() { + @Override + public String apply(Integer v) { + return String.valueOf(v); + } + }); + } + }) + .subscribe(ts); + ts.assertTerminated(); + ts.assertNoErrors(); + ts.assertValues("1", "2", "3"); + } + + @Test + public void errorThrownIssue1685() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + FlowableProcessor<Object> processor = ReplayProcessor.create(); + + Flowable.error(new RuntimeException("oops")) + .materialize() + .delay(1, TimeUnit.SECONDS) + .dematerialize(Functions.<Notification<Object>>identity()) + .subscribe(processor); + + processor.subscribe(); + processor.materialize().blockingFirst(); + + System.out.println("Done"); + + TestHelper.assertError(errors, 0, OnErrorNotImplementedException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void emptyIdentity() { + assertEquals(Flowable.empty(), Flowable.empty()); + } + + @Test + public void emptyIsEmpty() { + Flowable.<Integer>empty().subscribe(w); + + verify(w).onComplete(); + verify(w, never()).onNext(any(Integer.class)); + verify(w, never()).onError(any(Throwable.class)); + } + + @Test + public void extend() { + final TestSubscriber<Object> subscriber = new TestSubscriber<>(); + final Object value = new Object(); + Object returned = Flowable.just(value).to(new FlowableConverter<Object, Object>() { + @Override + public Object apply(Flowable<Object> onSubscribe) { + onSubscribe.subscribe(subscriber); + subscriber.assertNoErrors(); + subscriber.assertComplete(); + subscriber.assertValue(value); + return subscriber.values().get(0); + } + }); + assertSame(returned, value); + } + + @Test + public void asExtend() { + final TestSubscriber<Object> subscriber = new TestSubscriber<>(); + final Object value = new Object(); + Object returned = Flowable.just(value).to(new FlowableConverter<Object, Object>() { + @Override + public Object apply(Flowable<Object> onSubscribe) { + onSubscribe.subscribe(subscriber); + subscriber.assertNoErrors(); + subscriber.assertComplete(); + subscriber.assertValue(value); + return subscriber.values().get(0); + } + }); + assertSame(returned, value); + } + + @Test + public void as() { + Flowable.just(1).to(new FlowableConverter<Integer, Observable<Integer>>() { + @Override + public Observable<Integer> apply(Flowable<Integer> v) { + return v.toObservable(); + } + }) + .test() + .assertResult(1); + } + + @Test + public void toObservableEmpty() { + Flowable.empty().toObservable().test().assertResult(); + } + + @Test + public void toObservableJust() { + Flowable.just(1).toObservable().test().assertResult(1); + } + + @Test + public void toObservableRange() { + Flowable.range(1, 5).toObservable().test().assertResult(1, 2, 3, 4, 5); + } + + @Test + public void toObservableError() { + Flowable.error(new TestException()).toObservable().test().assertFailure(TestException.class); + } + + @Test + public void zipIterableObject() { + final List<Flowable<Integer>> flowables = Arrays.asList(Flowable.just(1, 2, 3), Flowable.just(1, 2, 3)); + Flowable.zip(flowables, new Function<Object[], Object>() { + @Override + public Object apply(Object[] o) throws Exception { + int sum = 0; + for (Object i : o) { + sum += (Integer) i; + } + return sum; + } + }).test().assertResult(2, 4, 6); + } + + @Test + public void combineLatestObject() { + final List<Flowable<Integer>> flowables = Arrays.asList(Flowable.just(1, 2, 3), Flowable.just(1, 2, 3)); + Flowable.combineLatest(flowables, new Function<Object[], Object>() { + @Override + public Object apply(final Object[] o) throws Exception { + int sum = 1; + for (Object i : o) { + sum *= (Integer) i; + } + return sum; + } + }).test().assertResult(3, 6, 9); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/flowable/FlowableThrottleLastTests.java b/src/test/java/io/reactivex/rxjava3/flowable/FlowableThrottleLastTests.java new file mode 100644 index 0000000000..c5c858c430 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/flowable/FlowableThrottleLastTests.java @@ -0,0 +1,132 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.flowable; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import java.util.concurrent.TimeUnit; + +import io.reactivex.rxjava3.core.Observer; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.Action; +import org.junit.Test; +import org.mockito.InOrder; +import org.reactivestreams.Subscriber; + +import io.reactivex.rxjava3.core.RxJavaTest; +import io.reactivex.rxjava3.processors.PublishProcessor; +import io.reactivex.rxjava3.schedulers.TestScheduler; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class FlowableThrottleLastTests extends RxJavaTest { + + @Test + public void throttleWithDroppedCallbackException() throws Throwable { + Subscriber<Integer> subscriber = TestHelper.mockSubscriber(); + Action whenDisposed = mock(Action.class); + + TestScheduler s = new TestScheduler(); + PublishProcessor<Integer> o = PublishProcessor.create(); + o.doOnCancel(whenDisposed) + .throttleLast(500, TimeUnit.MILLISECONDS, s, e-> { + if (e == 1) { + throw new TestException("forced"); + } + }) + .subscribe(subscriber); + + // send events with simulated time increments + s.advanceTimeTo(0, TimeUnit.MILLISECONDS); + o.onNext(1); // skip + o.onNext(2); // deliver + s.advanceTimeTo(501, TimeUnit.MILLISECONDS); + + InOrder inOrder = inOrder(subscriber); + inOrder.verify(subscriber).onError(any(TestException.class)); + inOrder.verifyNoMoreInteractions(); + verify(whenDisposed).run(); + } + + @Test + public void throttleWithDroppedCallback() { + Subscriber<Integer> subscriber = TestHelper.mockSubscriber(); + Observer<Object> dropCallbackObserver = TestHelper.mockObserver(); + + TestScheduler s = new TestScheduler(); + PublishProcessor<Integer> o = PublishProcessor.create(); + o.throttleLast(500, TimeUnit.MILLISECONDS, s, dropCallbackObserver::onNext).subscribe(subscriber); + + // send events with simulated time increments + s.advanceTimeTo(0, TimeUnit.MILLISECONDS); + o.onNext(1); // skip + o.onNext(2); // deliver + s.advanceTimeTo(501, TimeUnit.MILLISECONDS); + o.onNext(3); // skip + s.advanceTimeTo(600, TimeUnit.MILLISECONDS); + o.onNext(4); // skip + s.advanceTimeTo(700, TimeUnit.MILLISECONDS); + o.onNext(5); // skip + o.onNext(6); // deliver + s.advanceTimeTo(1001, TimeUnit.MILLISECONDS); + o.onNext(7); // deliver + s.advanceTimeTo(1501, TimeUnit.MILLISECONDS); + o.onComplete(); + + InOrder inOrder = inOrder(subscriber); + InOrder dropCallbackOrder = inOrder(dropCallbackObserver); + dropCallbackOrder.verify(dropCallbackObserver).onNext(1); + inOrder.verify(subscriber).onNext(2); + dropCallbackOrder.verify(dropCallbackObserver).onNext(3); + dropCallbackOrder.verify(dropCallbackObserver).onNext(4); + dropCallbackOrder.verify(dropCallbackObserver).onNext(5); + inOrder.verify(subscriber).onNext(6); + inOrder.verify(subscriber).onNext(7); + inOrder.verify(subscriber).onComplete(); + inOrder.verifyNoMoreInteractions(); + dropCallbackOrder.verifyNoMoreInteractions(); + } + + @Test + public void throttle() { + Subscriber<Integer> subscriber = TestHelper.mockSubscriber(); + + TestScheduler s = new TestScheduler(); + PublishProcessor<Integer> o = PublishProcessor.create(); + o.throttleLast(500, TimeUnit.MILLISECONDS, s).subscribe(subscriber); + + // send events with simulated time increments + s.advanceTimeTo(0, TimeUnit.MILLISECONDS); + o.onNext(1); // skip + o.onNext(2); // deliver + s.advanceTimeTo(501, TimeUnit.MILLISECONDS); + o.onNext(3); // skip + s.advanceTimeTo(600, TimeUnit.MILLISECONDS); + o.onNext(4); // skip + s.advanceTimeTo(700, TimeUnit.MILLISECONDS); + o.onNext(5); // skip + o.onNext(6); // deliver + s.advanceTimeTo(1001, TimeUnit.MILLISECONDS); + o.onNext(7); // deliver + s.advanceTimeTo(1501, TimeUnit.MILLISECONDS); + o.onComplete(); + + InOrder inOrder = inOrder(subscriber); + inOrder.verify(subscriber).onNext(2); + inOrder.verify(subscriber).onNext(6); + inOrder.verify(subscriber).onNext(7); + inOrder.verify(subscriber).onComplete(); + inOrder.verifyNoMoreInteractions(); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/flowable/FlowableThrottleWithTimeoutTests.java b/src/test/java/io/reactivex/rxjava3/flowable/FlowableThrottleWithTimeoutTests.java new file mode 100644 index 0000000000..be94bf9d38 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/flowable/FlowableThrottleWithTimeoutTests.java @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.flowable; + +import static org.mockito.Mockito.inOrder; + +import java.util.concurrent.TimeUnit; + +import org.junit.Test; +import org.mockito.InOrder; +import org.reactivestreams.Subscriber; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.processors.PublishProcessor; +import io.reactivex.rxjava3.schedulers.TestScheduler; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class FlowableThrottleWithTimeoutTests extends RxJavaTest { + + @Test + public void throttle() { + Subscriber<Integer> subscriber = TestHelper.mockSubscriber(); + + TestScheduler s = new TestScheduler(); + PublishProcessor<Integer> o = PublishProcessor.create(); + o.throttleWithTimeout(500, TimeUnit.MILLISECONDS, s) + .subscribe(subscriber); + + // send events with simulated time increments + s.advanceTimeTo(0, TimeUnit.MILLISECONDS); + o.onNext(1); // skip + o.onNext(2); // deliver + s.advanceTimeTo(501, TimeUnit.MILLISECONDS); + o.onNext(3); // skip + s.advanceTimeTo(600, TimeUnit.MILLISECONDS); + o.onNext(4); // skip + s.advanceTimeTo(700, TimeUnit.MILLISECONDS); + o.onNext(5); // skip + o.onNext(6); // deliver at 1300 after 500ms has passed since onNext(5) + s.advanceTimeTo(1300, TimeUnit.MILLISECONDS); + o.onNext(7); // deliver + s.advanceTimeTo(1800, TimeUnit.MILLISECONDS); + o.onComplete(); + + InOrder inOrder = inOrder(subscriber); + inOrder.verify(subscriber).onNext(2); + inOrder.verify(subscriber).onNext(6); + inOrder.verify(subscriber).onNext(7); + inOrder.verify(subscriber).onComplete(); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void throttleFirstDefaultScheduler() { + Flowable.just(1).throttleWithTimeout(100, TimeUnit.MILLISECONDS) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(1); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/flowable/FlowableWindowTests.java b/src/test/java/io/reactivex/rxjava3/flowable/FlowableWindowTests.java new file mode 100644 index 0000000000..2ba648282f --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/flowable/FlowableWindowTests.java @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.flowable; + +import static org.junit.Assert.*; + +import java.util.*; +import java.util.concurrent.TimeUnit; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.processors.PublishProcessor; +import io.reactivex.rxjava3.schedulers.TestScheduler; +import io.reactivex.rxjava3.subscribers.TestSubscriber; + +public class FlowableWindowTests extends RxJavaTest { + + @Test + public void window() { + final ArrayList<List<Integer>> lists = new ArrayList<>(); + + Flowable.concat( + Flowable.just(1, 2, 3, 4, 5, 6) + .window(3) + .map(new Function<Flowable<Integer>, Flowable<List<Integer>>>() { + @Override + public Flowable<List<Integer>> apply(Flowable<Integer> xs) { + return xs.toList().toFlowable(); + } + }) + ) + .blockingForEach(new Consumer<List<Integer>>() { + @Override + public void accept(List<Integer> xs) { + lists.add(xs); + } + }); + + assertArrayEquals(lists.get(0).toArray(new Integer[3]), new Integer[] { 1, 2, 3 }); + assertArrayEquals(lists.get(1).toArray(new Integer[3]), new Integer[] { 4, 5, 6 }); + assertEquals(2, lists.size()); + + } + + @Test + public void timeSizeWindowAlternatingBounds() { + TestScheduler scheduler = new TestScheduler(); + PublishProcessor<Integer> pp = PublishProcessor.create(); + + TestSubscriber<List<Integer>> ts = pp.window(5, TimeUnit.SECONDS, scheduler, 2) + .flatMapSingle(new Function<Flowable<Integer>, SingleSource<List<Integer>>>() { + @Override + public SingleSource<List<Integer>> apply(Flowable<Integer> v) throws Throwable { + return v.toList(); + } + }) + .test(); + + pp.onNext(1); + pp.onNext(2); + ts.assertValueCount(1); // size bound hit + + scheduler.advanceTimeBy(1, TimeUnit.SECONDS); + pp.onNext(3); + scheduler.advanceTimeBy(6, TimeUnit.SECONDS); + ts.assertValueCount(2); // time bound hit + + pp.onNext(4); + pp.onNext(5); + + ts.assertValueCount(3); // size bound hit again + + pp.onNext(4); + + scheduler.advanceTimeBy(6, TimeUnit.SECONDS); + + ts.assertValueCount(4) + .assertNoErrors() + .assertNotComplete(); + + ts.cancel(); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/flowable/FlowableZipTests.java b/src/test/java/io/reactivex/rxjava3/flowable/FlowableZipTests.java new file mode 100644 index 0000000000..7f77992bfe --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/flowable/FlowableZipTests.java @@ -0,0 +1,157 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.flowable; + +import static org.junit.Assert.*; + +import java.util.*; + +import org.junit.Test; +import org.reactivestreams.Publisher; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.flowable.FlowableCovarianceTest.*; +import io.reactivex.rxjava3.flowable.FlowableEventStream.Event; +import io.reactivex.rxjava3.flowables.GroupedFlowable; +import io.reactivex.rxjava3.functions.*; + +public class FlowableZipTests extends RxJavaTest { + + @Test + public void zipObservableOfObservables() { + FlowableEventStream.getEventStream("HTTP-ClusterB", 20) + .groupBy(new Function<Event, String>() { + @Override + public String apply(Event e) { + return e.instanceId; + } + }) + // now we have streams of cluster+instanceId + .flatMap(new Function<GroupedFlowable<String, Event>, Publisher<HashMap<String, String>>>() { + @Override + public Publisher<HashMap<String, String>> apply(final GroupedFlowable<String, Event> ge) { + return ge.scan(new HashMap<>(), new BiFunction<HashMap<String, String>, Event, HashMap<String, String>>() { + @Override + public HashMap<String, String> apply(HashMap<String, String> accum, + Event perInstanceEvent) { + synchronized (accum) { + accum.put("instance", ge.getKey()); + } + return accum; + } + }); + } + }) + .take(10) + .blockingForEach(new Consumer<HashMap<String, String>>() { + @Override + public void accept(HashMap<String, String> v) { + synchronized (v) { + System.out.println(v); + } + } + }); + + System.out.println("**** finished"); + } + + /** + * This won't compile if super/extends isn't done correctly on generics. + */ + @Test + public void covarianceOfZip() { + Flowable<HorrorMovie> horrors = Flowable.just(new HorrorMovie()); + Flowable<CoolRating> ratings = Flowable.just(new CoolRating()); + + Flowable.<Movie, CoolRating, Result> zip(horrors, ratings, combine).blockingForEach(action); + Flowable.<Movie, CoolRating, Result> zip(horrors, ratings, combine).blockingForEach(action); + Flowable.<Media, Rating, ExtendedResult> zip(horrors, ratings, combine).blockingForEach(extendedAction); + Flowable.<Media, Rating, Result> zip(horrors, ratings, combine).blockingForEach(action); + Flowable.<Media, Rating, ExtendedResult> zip(horrors, ratings, combine).blockingForEach(action); + + Flowable.<Movie, CoolRating, Result> zip(horrors, ratings, combine); + } + + /** + * Occasionally zip may be invoked with 0 observables. Test that we don't block indefinitely instead + * of immediately invoking zip with 0 argument. + * + * We now expect an NoSuchElementException since last() requires at least one value and nothing will be emitted. + */ + @Test(expected = NoSuchElementException.class) + public void nonBlockingObservable() { + + final Object invoked = new Object(); + + Collection<Flowable<Object>> observables = Collections.emptyList(); + + Flowable<Object> result = Flowable.zip(observables, new Function<Object[], Object>() { + @Override + public Object apply(Object[] args) { + System.out.println("received: " + args); + assertEquals("No argument should have been passed", 0, args.length); + return invoked; + } + }); + + assertSame(invoked, result.blockingLast()); + } + + BiFunction<Media, Rating, ExtendedResult> combine = new BiFunction<Media, Rating, ExtendedResult>() { + @Override + public ExtendedResult apply(Media m, Rating r) { + return new ExtendedResult(); + } + }; + + Consumer<Result> action = new Consumer<Result>() { + @Override + public void accept(Result t1) { + System.out.println("Result: " + t1); + } + }; + + Consumer<ExtendedResult> extendedAction = new Consumer<ExtendedResult>() { + @Override + public void accept(ExtendedResult t1) { + System.out.println("Result: " + t1); + } + }; + + @Test + public void zipWithDelayError() { + Flowable.just(1) + .zipWith(Flowable.just(2), new BiFunction<Integer, Integer, Integer>() { + @Override + public Integer apply(Integer a, Integer b) throws Exception { + return a + b; + } + }, true) + .test() + .assertResult(3); + } + + @Test + public void zipWithDelayErrorBufferSize() { + Flowable.just(1) + .zipWith(Flowable.just(2), new BiFunction<Integer, Integer, Integer>() { + @Override + public Integer apply(Integer a, Integer b) throws Exception { + return a + b; + } + }, true, 16) + .test() + .assertResult(3); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/SubscribeWithTest.java b/src/test/java/io/reactivex/rxjava3/internal/SubscribeWithTest.java new file mode 100644 index 0000000000..a24562a854 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/SubscribeWithTest.java @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.subscribers.TestSubscriber; + +public class SubscribeWithTest extends RxJavaTest { + + @Test + public void withFlowable() { + Flowable.range(1, 10) + .subscribeWith(new TestSubscriber<>()) + .assertResult(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + } + + @Test + public void withObservable() { + Observable.range(1, 10) + .subscribeWith(new TestObserver<>()) + .assertResult(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + } + + class ObserverImpl implements SingleObserver<Object>, CompletableObserver { + Object value; + + @Override + public void onSubscribe(Disposable d) { + + } + + @Override + public void onComplete() { + this.value = 100; + } + + @Override + public void onSuccess(Object value) { + this.value = value; + } + + @Override + public void onError(Throwable e) { + this.value = e; + } + } + + @Test + public void withSingle() { + assertEquals(1, Single.just(1).subscribeWith(new ObserverImpl()).value); + } + + @Test + public void withCompletable() { + assertEquals(100, Completable.complete().subscribeWith(new ObserverImpl()).value); + } + +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/disposables/ArrayCompositeDisposableTest.java b/src/test/java/io/reactivex/rxjava3/internal/disposables/ArrayCompositeDisposableTest.java new file mode 100644 index 0000000000..c0319dfd3a --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/disposables/ArrayCompositeDisposableTest.java @@ -0,0 +1,119 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.disposables; + +import static org.junit.Assert.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.RxJavaTest; +import io.reactivex.rxjava3.disposables.*; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class ArrayCompositeDisposableTest extends RxJavaTest { + + @Test + public void normal() { + ArrayCompositeDisposable acd = new ArrayCompositeDisposable(2); + + Disposable d1 = Disposable.empty(); + Disposable d2 = Disposable.empty(); + + assertTrue(acd.setResource(0, d1)); + assertTrue(acd.setResource(1, d2)); + + Disposable d3 = Disposable.empty(); + Disposable d4 = Disposable.empty(); + + acd.replaceResource(0, d3); + acd.replaceResource(1, d4); + + assertFalse(d1.isDisposed()); + assertFalse(d2.isDisposed()); + + acd.setResource(0, d1); + acd.setResource(1, d2); + + assertTrue(d3.isDisposed()); + assertTrue(d4.isDisposed()); + + assertFalse(acd.isDisposed()); + + acd.dispose(); + acd.dispose(); + + assertTrue(acd.isDisposed()); + + assertTrue(d1.isDisposed()); + assertTrue(d2.isDisposed()); + + Disposable d5 = Disposable.empty(); + Disposable d6 = Disposable.empty(); + + assertFalse(acd.setResource(0, d5)); + acd.replaceResource(1, d6); + + assertTrue(d5.isDisposed()); + assertTrue(d6.isDisposed()); + } + + @Test + public void disposeRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final ArrayCompositeDisposable acd = new ArrayCompositeDisposable(2); + + Runnable r = new Runnable() { + @Override + public void run() { + acd.dispose(); + } + }; + + TestHelper.race(r, r); + } + } + + @Test + public void replaceRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final ArrayCompositeDisposable acd = new ArrayCompositeDisposable(2); + + Runnable r = new Runnable() { + @Override + public void run() { + acd.replaceResource(0, Disposable.empty()); + } + }; + + TestHelper.race(r, r); + } + } + + @Test + public void setRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final ArrayCompositeDisposable acd = new ArrayCompositeDisposable(2); + + Runnable r = new Runnable() { + @Override + public void run() { + acd.setResource(0, Disposable.empty()); + } + }; + + TestHelper.race(r, r); + } + } + +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/disposables/CancellableDisposableTest.java b/src/test/java/io/reactivex/rxjava3/internal/disposables/CancellableDisposableTest.java new file mode 100644 index 0000000000..ab40e7c03c --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/disposables/CancellableDisposableTest.java @@ -0,0 +1,112 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.disposables; + +import static org.junit.Assert.*; + +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.RxJavaTest; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.Cancellable; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class CancellableDisposableTest extends RxJavaTest { + + @Test + public void normal() { + final AtomicInteger count = new AtomicInteger(); + + Cancellable c = new Cancellable() { + @Override + public void cancel() throws Exception { + count.getAndIncrement(); + } + }; + + CancellableDisposable cd = new CancellableDisposable(c); + + assertFalse(cd.isDisposed()); + + cd.dispose(); + cd.dispose(); + + assertTrue(cd.isDisposed()); + + assertEquals(1, count.get()); + } + + @Test + public void cancelThrows() { + final AtomicInteger count = new AtomicInteger(); + + Cancellable c = new Cancellable() { + @Override + public void cancel() throws Exception { + count.getAndIncrement(); + throw new TestException(); + } + }; + + CancellableDisposable cd = new CancellableDisposable(c); + + assertFalse(cd.isDisposed()); + + List<Throwable> list = TestHelper.trackPluginErrors(); + try { + cd.dispose(); + cd.dispose(); + + TestHelper.assertUndeliverable(list, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + assertTrue(cd.isDisposed()); + + assertEquals(1, count.get()); + } + + @Test + public void disposeRace() { + + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final AtomicInteger count = new AtomicInteger(); + + Cancellable c = new Cancellable() { + @Override + public void cancel() throws Exception { + count.getAndIncrement(); + } + }; + + final CancellableDisposable cd = new CancellableDisposable(c); + + Runnable r = new Runnable() { + @Override + public void run() { + cd.dispose(); + } + }; + + TestHelper.race(r, r); + + assertEquals(1, count.get()); + } + } + +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/disposables/DisposableHelperTest.java b/src/test/java/io/reactivex/rxjava3/internal/disposables/DisposableHelperTest.java new file mode 100644 index 0000000000..a3102ea44f --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/disposables/DisposableHelperTest.java @@ -0,0 +1,146 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.disposables; + +import static org.junit.Assert.*; + +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.RxJavaTest; +import io.reactivex.rxjava3.disposables.*; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class DisposableHelperTest extends RxJavaTest { + @Test + public void enumMethods() { + assertEquals(1, DisposableHelper.values().length); + assertNotNull(DisposableHelper.valueOf("DISPOSED")); + } + + @Test + public void innerDisposed() { + assertTrue(DisposableHelper.DISPOSED.isDisposed()); + DisposableHelper.DISPOSED.dispose(); + assertTrue(DisposableHelper.DISPOSED.isDisposed()); + } + + @Test + public void validationNull() { + List<Throwable> list = TestHelper.trackPluginErrors(); + try { + assertFalse(DisposableHelper.validate(null, null)); + + TestHelper.assertError(list, 0, NullPointerException.class, "next is null"); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void disposeRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final AtomicReference<Disposable> d = new AtomicReference<>(); + + Runnable r = new Runnable() { + @Override + public void run() { + DisposableHelper.dispose(d); + } + }; + + TestHelper.race(r, r); + } + } + + @Test + public void setReplace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final AtomicReference<Disposable> d = new AtomicReference<>(); + + Runnable r = new Runnable() { + @Override + public void run() { + DisposableHelper.replace(d, Disposable.empty()); + } + }; + + TestHelper.race(r, r); + } + } + + @Test + public void setRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final AtomicReference<Disposable> d = new AtomicReference<>(); + + Runnable r = new Runnable() { + @Override + public void run() { + DisposableHelper.set(d, Disposable.empty()); + } + }; + + TestHelper.race(r, r); + } + } + + @Test + public void setReplaceNull() { + final AtomicReference<Disposable> d = new AtomicReference<>(); + + DisposableHelper.dispose(d); + + assertFalse(DisposableHelper.set(d, null)); + assertFalse(DisposableHelper.replace(d, null)); + } + + @Test + public void dispose() { + Disposable u = Disposable.empty(); + final AtomicReference<Disposable> d = new AtomicReference<>(u); + + DisposableHelper.dispose(d); + + assertTrue(u.isDisposed()); + } + + @Test + public void trySet() { + AtomicReference<Disposable> ref = new AtomicReference<>(); + + Disposable d1 = Disposable.empty(); + + assertTrue(DisposableHelper.trySet(ref, d1)); + + Disposable d2 = Disposable.empty(); + + assertFalse(DisposableHelper.trySet(ref, d2)); + + assertFalse(d1.isDisposed()); + + assertFalse(d2.isDisposed()); + + DisposableHelper.dispose(ref); + + Disposable d3 = Disposable.empty(); + + assertFalse(DisposableHelper.trySet(ref, d3)); + + assertTrue(d3.isDisposed()); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/disposables/EmptyDisposableTest.java b/src/test/java/io/reactivex/rxjava3/internal/disposables/EmptyDisposableTest.java new file mode 100644 index 0000000000..1f0d370dcd --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/disposables/EmptyDisposableTest.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.disposables; + +import static org.junit.Assert.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.RxJavaTest; +import io.reactivex.rxjava3.operators.QueueFuseable; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class EmptyDisposableTest extends RxJavaTest { + + @Test + public void noOffer() { + TestHelper.assertNoOffer(EmptyDisposable.INSTANCE); + } + + @Test + public void asyncFusion() { + assertEquals(QueueFuseable.NONE, EmptyDisposable.INSTANCE.requestFusion(QueueFuseable.SYNC)); + assertEquals(QueueFuseable.ASYNC, EmptyDisposable.INSTANCE.requestFusion(QueueFuseable.ASYNC)); + } + + @Test + public void checkEnum() { + assertEquals(2, EmptyDisposable.values().length); + assertNotNull(EmptyDisposable.valueOf("INSTANCE")); + assertNotNull(EmptyDisposable.valueOf("NEVER")); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/disposables/ListCompositeDisposableTest.java b/src/test/java/io/reactivex/rxjava3/internal/disposables/ListCompositeDisposableTest.java new file mode 100644 index 0000000000..1a0070e22d --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/disposables/ListCompositeDisposableTest.java @@ -0,0 +1,414 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.disposables; + +import static org.junit.Assert.*; + +import java.util.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.RxJavaTest; +import io.reactivex.rxjava3.disposables.*; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class ListCompositeDisposableTest extends RxJavaTest { + + @Test + public void constructorAndAddVarargs() { + Disposable d1 = Disposable.empty(); + Disposable d2 = Disposable.empty(); + + ListCompositeDisposable lcd = new ListCompositeDisposable(d1, d2); + + lcd.clear(); + + assertFalse(lcd.isDisposed()); + + assertTrue(d1.isDisposed()); + assertTrue(d2.isDisposed()); + + d1 = Disposable.empty(); + d2 = Disposable.empty(); + + lcd.addAll(d1, d2); + + lcd.dispose(); + + assertTrue(lcd.isDisposed()); + assertTrue(d1.isDisposed()); + assertTrue(d2.isDisposed()); + } + + @Test + public void constructorIterable() { + Disposable d1 = Disposable.empty(); + Disposable d2 = Disposable.empty(); + + ListCompositeDisposable lcd = new ListCompositeDisposable(Arrays.asList(d1, d2)); + + lcd.clear(); + + assertFalse(lcd.isDisposed()); + + assertTrue(d1.isDisposed()); + assertTrue(d2.isDisposed()); + + d1 = Disposable.empty(); + d2 = Disposable.empty(); + + lcd.add(d1); + lcd.addAll(d2); + + lcd.dispose(); + + assertTrue(lcd.isDisposed()); + assertTrue(d1.isDisposed()); + assertTrue(d2.isDisposed()); + } + + @Test + public void empty() { + ListCompositeDisposable lcd = new ListCompositeDisposable(); + + assertFalse(lcd.isDisposed()); + + lcd.clear(); + + assertFalse(lcd.isDisposed()); + + lcd.dispose(); + + lcd.dispose(); + + lcd.clear(); + + assertTrue(lcd.isDisposed()); + } + + @Test + public void afterDispose() { + ListCompositeDisposable lcd = new ListCompositeDisposable(); + lcd.dispose(); + + Disposable d = Disposable.empty(); + assertFalse(lcd.add(d)); + assertTrue(d.isDisposed()); + + d = Disposable.empty(); + assertFalse(lcd.addAll(d)); + assertTrue(d.isDisposed()); + } + + @Test + public void disposeThrows() { + Disposable d = new Disposable() { + + @Override + public void dispose() { + throw new TestException(); + } + + @Override + public boolean isDisposed() { + return false; + } + + }; + + ListCompositeDisposable lcd = new ListCompositeDisposable(d, d); + + try { + lcd.dispose(); + fail("Should have thrown!"); + } catch (CompositeException ex) { + List<Throwable> list = ex.getExceptions(); + TestHelper.assertError(list, 0, TestException.class); + TestHelper.assertError(list, 1, TestException.class); + } + + lcd = new ListCompositeDisposable(d); + + try { + lcd.dispose(); + fail("Should have thrown!"); + } catch (TestException ex) { + // expected + } + } + + @Test + public void remove() { + ListCompositeDisposable lcd = new ListCompositeDisposable(); + Disposable d = Disposable.empty(); + + lcd.add(d); + + assertTrue(lcd.delete(d)); + + assertFalse(d.isDisposed()); + + lcd.add(d); + + assertTrue(lcd.remove(d)); + + assertTrue(d.isDisposed()); + + assertFalse(lcd.remove(d)); + + assertFalse(lcd.delete(d)); + + lcd = new ListCompositeDisposable(); + + assertFalse(lcd.remove(d)); + + assertFalse(lcd.delete(d)); + } + + @Test + public void disposeRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final ListCompositeDisposable cd = new ListCompositeDisposable(); + + Runnable run = new Runnable() { + @Override + public void run() { + cd.dispose(); + } + }; + + TestHelper.race(run, run); + } + } + + @Test + public void addRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final ListCompositeDisposable cd = new ListCompositeDisposable(); + + Runnable run = new Runnable() { + @Override + public void run() { + cd.add(Disposable.empty()); + } + }; + + TestHelper.race(run, run); + } + } + + @Test + public void addAllRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final ListCompositeDisposable cd = new ListCompositeDisposable(); + + Runnable run = new Runnable() { + @Override + public void run() { + cd.addAll(Disposable.empty()); + } + }; + + TestHelper.race(run, run); + } + } + + @Test + public void removeRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final ListCompositeDisposable cd = new ListCompositeDisposable(); + + final Disposable d1 = Disposable.empty(); + + cd.add(d1); + + Runnable run = new Runnable() { + @Override + public void run() { + cd.remove(d1); + } + }; + + TestHelper.race(run, run); + } + } + + @Test + public void deleteRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final ListCompositeDisposable cd = new ListCompositeDisposable(); + + final Disposable d1 = Disposable.empty(); + + cd.add(d1); + + Runnable run = new Runnable() { + @Override + public void run() { + cd.delete(d1); + } + }; + + TestHelper.race(run, run); + } + } + + @Test + public void clearRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final ListCompositeDisposable cd = new ListCompositeDisposable(); + + final Disposable d1 = Disposable.empty(); + + cd.add(d1); + + Runnable run = new Runnable() { + @Override + public void run() { + cd.clear(); + } + }; + + TestHelper.race(run, run); + } + } + + @Test + public void addDisposeRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final ListCompositeDisposable cd = new ListCompositeDisposable(); + + Runnable run = new Runnable() { + @Override + public void run() { + cd.dispose(); + } + }; + + Runnable run2 = new Runnable() { + @Override + public void run() { + cd.add(Disposable.empty()); + } + }; + + TestHelper.race(run, run2); + } + } + + @Test + public void addAllDisposeRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final ListCompositeDisposable cd = new ListCompositeDisposable(); + + Runnable run = new Runnable() { + @Override + public void run() { + cd.dispose(); + } + }; + + Runnable run2 = new Runnable() { + @Override + public void run() { + cd.addAll(Disposable.empty()); + } + }; + + TestHelper.race(run, run2); + } + } + + @Test + public void removeDisposeRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final ListCompositeDisposable cd = new ListCompositeDisposable(); + + final Disposable d1 = Disposable.empty(); + + cd.add(d1); + + Runnable run = new Runnable() { + @Override + public void run() { + cd.dispose(); + } + }; + + Runnable run2 = new Runnable() { + @Override + public void run() { + cd.remove(d1); + } + }; + + TestHelper.race(run, run2); + } + } + + @Test + public void deleteDisposeRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final ListCompositeDisposable cd = new ListCompositeDisposable(); + + final Disposable d1 = Disposable.empty(); + + cd.add(d1); + + Runnable run = new Runnable() { + @Override + public void run() { + cd.dispose(); + } + }; + + Runnable run2 = new Runnable() { + @Override + public void run() { + cd.delete(d1); + } + }; + + TestHelper.race(run, run2); + } + } + + @Test + public void clearDisposeRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final ListCompositeDisposable cd = new ListCompositeDisposable(); + + final Disposable d1 = Disposable.empty(); + + cd.add(d1); + + Runnable run = new Runnable() { + @Override + public void run() { + cd.dispose(); + } + }; + + Runnable run2 = new Runnable() { + @Override + public void run() { + cd.clear(); + } + }; + + TestHelper.race(run, run2); + } + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/functions/FunctionsTest.java b/src/test/java/io/reactivex/rxjava3/internal/functions/FunctionsTest.java new file mode 100644 index 0000000000..635fcc4472 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/functions/FunctionsTest.java @@ -0,0 +1,208 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.functions; + +import static org.junit.Assert.*; + +import java.lang.reflect.Method; +import java.util.List; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.RxJavaTest; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.functions.Functions.*; +import io.reactivex.rxjava3.internal.util.ExceptionHelper; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class FunctionsTest extends RxJavaTest { + @Test + public void utilityClass() { + TestHelper.checkUtilityClass(Functions.class); + } + + @SuppressWarnings("unchecked") + @Test + public void hashSetCallableEnum() { + // inlined TestHelper.checkEnum due to access restrictions + try { + Method m = Functions.HashSetSupplier.class.getMethod("values"); + m.setAccessible(true); + Method e = Functions.HashSetSupplier.class.getMethod("valueOf", String.class); + e.setAccessible(true); + + for (Enum<HashSetSupplier> o : (Enum<HashSetSupplier>[])m.invoke(null)) { + assertSame(o, e.invoke(null, o.name())); + } + + } catch (Throwable ex) { + throw ExceptionHelper.wrapOrThrow(ex); + } + } + + @SuppressWarnings("unchecked") + @Test + public void naturalComparatorEnum() { + // inlined TestHelper.checkEnum due to access restrictions + try { + Method m = Functions.NaturalComparator.class.getMethod("values"); + m.setAccessible(true); + Method e = Functions.NaturalComparator.class.getMethod("valueOf", String.class); + e.setAccessible(true); + + for (Enum<NaturalComparator> o : (Enum<NaturalComparator>[])m.invoke(null)) { + assertSame(o, e.invoke(null, o.name())); + } + + } catch (Throwable ex) { + throw ExceptionHelper.wrapOrThrow(ex); + } + } + + @Test + public void booleanSupplierPredicateReverse() throws Throwable { + BooleanSupplier s = new BooleanSupplier() { + @Override + public boolean getAsBoolean() throws Exception { + return false; + } + }; + + assertTrue(Functions.predicateReverseFor(s).test(1)); + + s = new BooleanSupplier() { + @Override + public boolean getAsBoolean() throws Exception { + return true; + } + }; + + assertFalse(Functions.predicateReverseFor(s).test(1)); + } + + @Test(expected = IllegalArgumentException.class) + public void toFunction2() throws Throwable { + Functions.toFunction(new BiFunction<Integer, Integer, Integer>() { + @Override + public Integer apply(Integer t1, Integer t2) throws Exception { + return null; + } + }).apply(new Object[20]); + } + + @Test(expected = IllegalArgumentException.class) + public void toFunction3() throws Throwable { + Functions.toFunction(new Function3<Integer, Integer, Integer, Integer>() { + @Override + public Integer apply(Integer t1, Integer t2, Integer t3) throws Exception { + return null; + } + }).apply(new Object[20]); + } + + @Test(expected = IllegalArgumentException.class) + public void toFunction4() throws Throwable { + Functions.toFunction(new Function4<Integer, Integer, Integer, Integer, Integer>() { + @Override + public Integer apply(Integer t1, Integer t2, Integer t3, Integer t4) throws Exception { + return null; + } + }).apply(new Object[20]); + } + + @Test(expected = IllegalArgumentException.class) + public void toFunction5() throws Throwable { + Functions.toFunction(new Function5<Integer, Integer, Integer, Integer, Integer, Integer>() { + @Override + public Integer apply(Integer t1, Integer t2, Integer t3, Integer t4, Integer t5) throws Exception { + return null; + } + }).apply(new Object[20]); + } + + @Test(expected = IllegalArgumentException.class) + public void toFunction6() throws Throwable { + Functions.toFunction(new Function6<Integer, Integer, Integer, Integer, Integer, Integer, Integer>() { + @Override + public Integer apply(Integer t1, Integer t2, Integer t3, Integer t4, Integer t5, Integer t6) throws Exception { + return null; + } + }).apply(new Object[20]); + } + + @Test(expected = IllegalArgumentException.class) + public void toFunction7() throws Throwable { + Functions.toFunction(new Function7<Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer>() { + @Override + public Integer apply(Integer t1, Integer t2, Integer t3, Integer t4, Integer t5, Integer t6, Integer t7) throws Exception { + return null; + } + }).apply(new Object[20]); + } + + @Test(expected = IllegalArgumentException.class) + public void toFunction8() throws Throwable { + Functions.toFunction(new Function8<Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer>() { + @Override + public Integer apply(Integer t1, Integer t2, Integer t3, Integer t4, Integer t5, Integer t6, Integer t7, Integer t8) throws Exception { + return null; + } + }).apply(new Object[20]); + } + + @Test(expected = IllegalArgumentException.class) + public void toFunction9() throws Throwable { + Functions.toFunction(new Function9<Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer>() { + @Override + public Integer apply(Integer t1, Integer t2, Integer t3, Integer t4, Integer t5, Integer t6, Integer t7, Integer t8, Integer t9) throws Exception { + return null; + } + }).apply(new Object[20]); + } + + @Test + public void identityFunctionToString() { + assertEquals("IdentityFunction", Functions.identity().toString()); + } + + @Test + public void emptyActionToString() { + assertEquals("EmptyAction", Functions.EMPTY_ACTION.toString()); + } + + @Test + public void emptyRunnableToString() { + assertEquals("EmptyRunnable", Functions.EMPTY_RUNNABLE.toString()); + } + + @Test + public void emptyConsumerToString() { + assertEquals("EmptyConsumer", Functions.EMPTY_CONSUMER.toString()); + } + + @Test + public void errorConsumerEmpty() throws Throwable { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + Functions.ERROR_CONSUMER.accept(new TestException()); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + assertEquals(errors.toString(), 1, errors.size()); + } finally { + RxJavaPlugins.reset(); + } + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/functions/ObjectHelperTest.java b/src/test/java/io/reactivex/rxjava3/internal/functions/ObjectHelperTest.java new file mode 100644 index 0000000000..fb7e912da4 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/functions/ObjectHelperTest.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.functions; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.RxJavaTest; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class ObjectHelperTest extends RxJavaTest { + + @Test + public void utilityClass() { + TestHelper.checkUtilityClass(ObjectHelper.class); + } + + @Test + public void verifyPositiveInt() throws Exception { + assertEquals(1, ObjectHelper.verifyPositive(1, "param")); + } + + @Test + public void verifyPositiveLong() throws Exception { + assertEquals(1L, ObjectHelper.verifyPositive(1L, "param")); + } + + @Test(expected = IllegalArgumentException.class) + public void verifyPositiveIntFail() throws Exception { + assertEquals(-1, ObjectHelper.verifyPositive(-1, "param")); + } + + @Test(expected = IllegalArgumentException.class) + public void verifyPositiveLongFail() throws Exception { + assertEquals(-1L, ObjectHelper.verifyPositive(-1L, "param")); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/fuseable/CancellableQueueFuseableTest.java b/src/test/java/io/reactivex/rxjava3/internal/fuseable/CancellableQueueFuseableTest.java new file mode 100644 index 0000000000..67144abea2 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/fuseable/CancellableQueueFuseableTest.java @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.fuseable; + +import static org.junit.Assert.*; +import org.junit.Test; + +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class CancellableQueueFuseableTest { + + @Test + public void offer() { + TestHelper.assertNoOffer(new CancellableQueueFuseable<>()); + } + + @Test + public void pollClear() throws Throwable { + CancellableQueueFuseable<Object> qs = new CancellableQueueFuseable<>(); + + assertNull(qs.poll()); + + qs.clear(); + assertNull(qs.poll()); + } + + @Test + public void cancel() { + CancellableQueueFuseable<Object> qs = new CancellableQueueFuseable<>(); + + assertFalse(qs.isDisposed()); + + qs.cancel(); + + assertTrue(qs.isDisposed()); + + qs.cancel(); + + assertTrue(qs.isDisposed()); + } + + @Test + public void dispose() { + CancellableQueueFuseable<Object> qs = new CancellableQueueFuseable<>(); + + assertFalse(qs.isDisposed()); + + qs.dispose(); + + assertTrue(qs.isDisposed()); + + qs.dispose(); + + assertTrue(qs.isDisposed()); + } + + @Test + public void cancel2() { + AbstractEmptyQueueFuseable<Object> qs = new AbstractEmptyQueueFuseable<Object>() { }; + + assertFalse(qs.isDisposed()); + + qs.cancel(); + } + + @Test + public void dispose2() { + AbstractEmptyQueueFuseable<Object> qs = new AbstractEmptyQueueFuseable<Object>() { }; + + assertFalse(qs.isDisposed()); + + qs.dispose(); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/jdk8/CollectWithCollectorTckTest.java b/src/test/java/io/reactivex/rxjava3/internal/jdk8/CollectWithCollectorTckTest.java new file mode 100644 index 0000000000..4745b9c809 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/jdk8/CollectWithCollectorTckTest.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.jdk8; + +import java.util.List; +import java.util.stream.Collectors; + +import org.reactivestreams.Publisher; +import org.testng.annotations.Test; + +import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.tck.BaseTck; + +@Test +public class CollectWithCollectorTckTest extends BaseTck<List<Integer>> { + + @Override + public Publisher<List<Integer>> createPublisher(final long elements) { + return + Flowable.range(0, (int)elements).collect(Collectors.toList()).toFlowable() + ; + } + + @Override + public Publisher<List<Integer>> createFailedPublisher() { + return Flowable.<Integer>error(new TestException()).collect(Collectors.toList()).toFlowable(); + } + + @Override + public long maxElementsFromPublisher() { + return 1; + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/jdk8/CompletableFromCompletionStageTest.java b/src/test/java/io/reactivex/rxjava3/internal/jdk8/CompletableFromCompletionStageTest.java new file mode 100644 index 0000000000..6e9bf78e1d --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/jdk8/CompletableFromCompletionStageTest.java @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.jdk8; + +import java.util.concurrent.CompletableFuture; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class CompletableFromCompletionStageTest extends RxJavaTest { + + @Test + public void syncSuccess() { + Completable.fromCompletionStage(CompletableFuture.completedFuture(1)) + .test() + .assertResult(); + } + + @Test + public void syncSuccessNull() { + Completable.fromCompletionStage(CompletableFuture.completedFuture(null)) + .test() + .assertResult(); + } + + @Test + public void syncFailure() { + CompletableFuture<Integer> cf = new CompletableFuture<>(); + cf.completeExceptionally(new TestException()); + + Completable.fromCompletionStage(cf) + .test() + .assertFailure(TestException.class); + } + + @Test + public void syncNull() { + Completable.fromCompletionStage(CompletableFuture.<Integer>completedFuture(null)) + .test() + .assertResult(); + } + + @Test + public void dispose() { + CompletableFuture<Integer> cf = new CompletableFuture<>(); + + TestObserver<Void> to = Completable.fromCompletionStage(cf) + .test(); + + to.assertEmpty(); + + to.dispose(); + + cf.complete(1); + + to.assertEmpty(); + } + + @Test + public void dispose2() { + TestHelper.checkDisposed(Completable.fromCompletionStage(new CompletableFuture<>())); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/jdk8/CompletableToCompletionStageTest.java b/src/test/java/io/reactivex/rxjava3/internal/jdk8/CompletableToCompletionStageTest.java new file mode 100644 index 0000000000..8917b1c675 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/jdk8/CompletableToCompletionStageTest.java @@ -0,0 +1,154 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.jdk8; + +import static org.junit.Assert.*; + +import java.util.concurrent.CompletableFuture; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.subjects.CompletableSubject; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class CompletableToCompletionStageTest extends RxJavaTest { + + @Test + public void complete() throws Exception { + Object v = Completable.complete() + .toCompletionStage(null) + .toCompletableFuture() + .get(); + + assertNull(v); + } + + @Test + public void completableFutureCancels() throws Exception { + CompletableSubject source = CompletableSubject.create(); + + CompletableFuture<Object> cf = source + .toCompletionStage(null) + .toCompletableFuture(); + + assertTrue(source.hasObservers()); + + cf.cancel(true); + + assertTrue(cf.isCancelled()); + + assertFalse(source.hasObservers()); + } + + @Test + public void completableManualCompleteCancels() throws Exception { + CompletableSubject source = CompletableSubject.create(); + + CompletableFuture<Object> cf = source + .toCompletionStage(null) + .toCompletableFuture(); + + assertTrue(source.hasObservers()); + + cf.complete(1); + + assertTrue(cf.isDone()); + assertFalse(cf.isCompletedExceptionally()); + assertFalse(cf.isCancelled()); + + assertFalse(source.hasObservers()); + + assertEquals(1, cf.get()); + } + + @Test + public void completableManualCompleteExceptionallyCancels() throws Exception { + CompletableSubject source = CompletableSubject.create(); + + CompletableFuture<Object> cf = source + .toCompletionStage(null) + .toCompletableFuture(); + + assertTrue(source.hasObservers()); + + cf.completeExceptionally(new TestException()); + + assertTrue(cf.isDone()); + assertTrue(cf.isCompletedExceptionally()); + assertFalse(cf.isCancelled()); + + assertFalse(source.hasObservers()); + + TestHelper.assertError(cf, TestException.class); + } + + @Test + public void error() throws Exception { + CompletableFuture<Object> cf = Completable.error(new TestException()) + .toCompletionStage(null) + .toCompletableFuture(); + + assertTrue(cf.isDone()); + assertTrue(cf.isCompletedExceptionally()); + assertFalse(cf.isCancelled()); + + TestHelper.assertError(cf, TestException.class); + } + + @Test + public void sourceIgnoresCancel() throws Throwable { + TestHelper.withErrorTracking(errors -> { + Object v = new Completable() { + @Override + protected void subscribeActual(CompletableObserver observer) { + observer.onSubscribe(Disposable.empty()); + observer.onComplete(); + observer.onError(new TestException()); + observer.onComplete(); + } + } + .toCompletionStage(null) + .toCompletableFuture() + .get(); + + assertNull(v); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + }); + } + + @Test + public void doubleOnSubscribe() throws Throwable { + TestHelper.withErrorTracking(errors -> { + Object v = new Completable() { + @Override + protected void subscribeActual(CompletableObserver observer) { + observer.onSubscribe(Disposable.empty()); + observer.onSubscribe(Disposable.empty()); + observer.onComplete(); + } + } + .toCompletionStage(null) + .toCompletableFuture() + .get(); + + assertNull(v); + + TestHelper.assertError(errors, 0, ProtocolViolationException.class); + }); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/jdk8/FlatMapStream0HTckTest.java b/src/test/java/io/reactivex/rxjava3/internal/jdk8/FlatMapStream0HTckTest.java new file mode 100644 index 0000000000..e499ac1b9a --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/jdk8/FlatMapStream0HTckTest.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.jdk8; + +import java.util.stream.*; + +import org.reactivestreams.Publisher; +import org.testng.annotations.Test; + +import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.tck.BaseTck; + +@Test +public class FlatMapStream0HTckTest extends BaseTck<Integer> { + + @Override + public Publisher<Integer> createPublisher(final long elements) { + return + Flowable.just(1).hide().flatMapStream(v -> IntStream.range(0, (int)elements).boxed()) + ; + } + + @Override + public Publisher<Integer> createFailedPublisher() { + Stream<Integer> stream = Stream.of(1); + stream.forEach(v -> { }); + return Flowable.just(1).hide().flatMapStream(v -> stream); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/jdk8/FlatMapStream0TckTest.java b/src/test/java/io/reactivex/rxjava3/internal/jdk8/FlatMapStream0TckTest.java new file mode 100644 index 0000000000..fa33b67ebf --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/jdk8/FlatMapStream0TckTest.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.jdk8; + +import java.util.stream.*; + +import org.reactivestreams.Publisher; +import org.testng.annotations.Test; + +import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.tck.BaseTck; + +@Test +public class FlatMapStream0TckTest extends BaseTck<Integer> { + + @Override + public Publisher<Integer> createPublisher(final long elements) { + return + Flowable.just(1).flatMapStream(v -> IntStream.range(0, (int)elements).boxed()) + ; + } + + @Override + public Publisher<Integer> createFailedPublisher() { + Stream<Integer> stream = Stream.of(1); + stream.forEach(v -> { }); + return Flowable.just(1).flatMapStream(v -> stream); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/jdk8/FlatMapStream1HTckTest.java b/src/test/java/io/reactivex/rxjava3/internal/jdk8/FlatMapStream1HTckTest.java new file mode 100644 index 0000000000..ee1a27b548 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/jdk8/FlatMapStream1HTckTest.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.jdk8; + +import java.util.stream.*; + +import org.reactivestreams.Publisher; +import org.testng.annotations.Test; + +import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.tck.BaseTck; + +@Test +public class FlatMapStream1HTckTest extends BaseTck<Integer> { + + @Override + public Publisher<Integer> createPublisher(final long elements) { + return + Flowable.range(1, (int)elements).hide().flatMapStream(v -> Stream.of(v)) + ; + } + + @Override + public Publisher<Integer> createFailedPublisher() { + Stream<Integer> stream = Stream.of(1); + stream.forEach(v -> { }); + return Flowable.just(1).hide().flatMapStream(v -> stream); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/jdk8/FlatMapStream1TckTest.java b/src/test/java/io/reactivex/rxjava3/internal/jdk8/FlatMapStream1TckTest.java new file mode 100644 index 0000000000..21492d61b8 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/jdk8/FlatMapStream1TckTest.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.jdk8; + +import java.util.stream.*; + +import org.reactivestreams.Publisher; +import org.testng.annotations.Test; + +import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.tck.BaseTck; + +@Test +public class FlatMapStream1TckTest extends BaseTck<Integer> { + + @Override + public Publisher<Integer> createPublisher(final long elements) { + return + Flowable.range(1, (int)elements).flatMapStream(v -> Stream.of(v)) + ; + } + + @Override + public Publisher<Integer> createFailedPublisher() { + Stream<Integer> stream = Stream.of(1); + stream.forEach(v -> { }); + return Flowable.just(1).flatMapStream(v -> stream); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/jdk8/FlatMapStream2HTckTest.java b/src/test/java/io/reactivex/rxjava3/internal/jdk8/FlatMapStream2HTckTest.java new file mode 100644 index 0000000000..14e44d69ee --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/jdk8/FlatMapStream2HTckTest.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.jdk8; + +import java.util.stream.*; + +import org.reactivestreams.Publisher; +import org.testng.annotations.Test; + +import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.tck.BaseTck; + +@Test +public class FlatMapStream2HTckTest extends BaseTck<Integer> { + + @Override + public Publisher<Integer> createPublisher(final long elements) { + if (elements % 2 == 0) { + return Flowable.range(0, (int)elements / 2).hide().flatMapStream(v -> Stream.of(v, v + 1)); + } + return + Flowable.range(-1, 1 + (int)elements / 2).hide().flatMapStream(v -> { + if (v != -1) { + return Stream.of(v, v + 1); + } + return Stream.of(v); + }); + } + + @Override + public Publisher<Integer> createFailedPublisher() { + Stream<Integer> stream = Stream.of(1); + stream.forEach(v -> { }); + return Flowable.just(1).hide().flatMapStream(v -> stream); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/jdk8/FlatMapStream2TckTest.java b/src/test/java/io/reactivex/rxjava3/internal/jdk8/FlatMapStream2TckTest.java new file mode 100644 index 0000000000..c42e6f7a80 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/jdk8/FlatMapStream2TckTest.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.jdk8; + +import java.util.stream.*; + +import org.reactivestreams.Publisher; +import org.testng.annotations.Test; + +import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.tck.BaseTck; + +@Test +public class FlatMapStream2TckTest extends BaseTck<Integer> { + + @Override + public Publisher<Integer> createPublisher(final long elements) { + if (elements % 2 == 0) { + return Flowable.range(0, (int)elements / 2).flatMapStream(v -> Stream.of(v, v + 1)); + } + return + Flowable.range(-1, 1 + (int)elements / 2).flatMapStream(v -> { + if (v != -1) { + return Stream.of(v, v + 1); + } + return Stream.of(v); + }); + } + + @Override + public Publisher<Integer> createFailedPublisher() { + Stream<Integer> stream = Stream.of(1); + stream.forEach(v -> { }); + return Flowable.just(1).flatMapStream(v -> stream); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/jdk8/FlowableBlockingStreamTest.java b/src/test/java/io/reactivex/rxjava3/internal/jdk8/FlowableBlockingStreamTest.java new file mode 100644 index 0000000000..be49e2ed11 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/jdk8/FlowableBlockingStreamTest.java @@ -0,0 +1,107 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.jdk8; + +import static org.junit.Assert.*; + +import java.util.List; +import java.util.stream.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.processors.UnicastProcessor; +import io.reactivex.rxjava3.schedulers.Schedulers; + +public class FlowableBlockingStreamTest extends RxJavaTest { + + @Test + public void empty() { + try (Stream<Integer> stream = Flowable.<Integer>empty().blockingStream()) { + assertEquals(0, stream.toArray().length); + } + } + + @Test + public void just() { + try (Stream<Integer> stream = Flowable.just(1).blockingStream()) { + assertArrayEquals(new Integer[] { 1 }, stream.toArray(Integer[]::new)); + } + } + + @Test + public void range() { + try (Stream<Integer> stream = Flowable.range(1, 5).blockingStream()) { + assertArrayEquals(new Integer[] { 1, 2, 3, 4, 5 }, stream.toArray(Integer[]::new)); + } + } + + @Test + public void rangeBackpressured() { + try (Stream<Integer> stream = Flowable.range(1, 5).blockingStream(1)) { + assertArrayEquals(new Integer[] { 1, 2, 3, 4, 5 }, stream.toArray(Integer[]::new)); + } + } + + @Test + public void rangeAsyncBackpressured() { + try (Stream<Integer> stream = Flowable.range(1, 1000).subscribeOn(Schedulers.computation()).blockingStream()) { + List<Integer> list = stream.collect(Collectors.toList()); + + assertEquals(1000, list.size()); + for (int i = 1; i <= 1000; i++) { + assertEquals(i, list.get(i - 1).intValue()); + } + } + } + + @Test + public void rangeAsyncBackpressured1() { + try (Stream<Integer> stream = Flowable.range(1, 1000).subscribeOn(Schedulers.computation()).blockingStream(1)) { + List<Integer> list = stream.collect(Collectors.toList()); + + assertEquals(1000, list.size()); + for (int i = 1; i <= 1000; i++) { + assertEquals(i, list.get(i - 1).intValue()); + } + } + } + + @Test + public void error() { + try (Stream<Integer> stream = Flowable.<Integer>error(new TestException()).blockingStream()) { + stream.toArray(Integer[]::new); + fail("Should have thrown!"); + } catch (TestException expected) { + // expected + } + } + + @Test + public void close() { + UnicastProcessor<Integer> up = UnicastProcessor.create(); + up.onNext(1); + up.onNext(2); + up.onNext(3); + up.onNext(4); + up.onNext(5); + + try (Stream<Integer> stream = up.blockingStream()) { + assertArrayEquals(new Integer[] { 1, 2, 3 }, stream.limit(3).toArray(Integer[]::new)); + } + + assertFalse(up.hasSubscribers()); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/jdk8/FlowableCollectWithCollectorTest.java b/src/test/java/io/reactivex/rxjava3/internal/jdk8/FlowableCollectWithCollectorTest.java new file mode 100644 index 0000000000..3a8a1d5b19 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/jdk8/FlowableCollectWithCollectorTest.java @@ -0,0 +1,420 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.jdk8; + +import static org.junit.Assert.assertFalse; + +import java.io.IOException; +import java.util.*; +import java.util.function.*; +import java.util.stream.*; + +import org.junit.Test; +import org.reactivestreams.Subscriber; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.internal.subscriptions.BooleanSubscription; +import io.reactivex.rxjava3.processors.*; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class FlowableCollectWithCollectorTest extends RxJavaTest { + + @Test + public void basic() { + Flowable.range(1, 5) + .collect(Collectors.toList()) + .test() + .assertResult(Arrays.asList(1, 2, 3, 4, 5)); + } + + @Test + public void empty() { + Flowable.empty() + .collect(Collectors.toList()) + .test() + .assertResult(Collections.emptyList()); + } + + @Test + public void error() { + Flowable.error(new TestException()) + .collect(Collectors.toList()) + .test() + .assertFailure(TestException.class); + } + + @Test + public void collectorSupplierCrash() { + Flowable.range(1, 5) + .collect(new Collector<Integer, Integer, Integer>() { + + @Override + public Supplier<Integer> supplier() { + throw new TestException(); + } + + @Override + public BiConsumer<Integer, Integer> accumulator() { + return (a, b) -> { }; + } + + @Override + public BinaryOperator<Integer> combiner() { + return (a, b) -> a + b; + } + + @Override + public Function<Integer, Integer> finisher() { + return a -> a; + } + + @Override + public Set<Characteristics> characteristics() { + return Collections.emptySet(); + } + }) + .test() + .assertFailure(TestException.class); + } + + @Test + public void collectorAccumulatorCrash() { + BehaviorProcessor<Integer> source = BehaviorProcessor.createDefault(1); + + source + .collect(new Collector<Integer, Integer, Integer>() { + + @Override + public Supplier<Integer> supplier() { + return () -> 1; + } + + @Override + public BiConsumer<Integer, Integer> accumulator() { + return (a, b) -> { throw new TestException(); }; + } + + @Override + public BinaryOperator<Integer> combiner() { + return (a, b) -> a + b; + } + + @Override + public Function<Integer, Integer> finisher() { + return a -> a; + } + + @Override + public Set<Characteristics> characteristics() { + return Collections.emptySet(); + } + }) + .test() + .assertFailure(TestException.class); + + assertFalse(source.hasSubscribers()); + } + + @Test + public void collectorFinisherCrash() { + Flowable.range(1, 5) + .collect(new Collector<Integer, Integer, Integer>() { + + @Override + public Supplier<Integer> supplier() { + return () -> 1; + } + + @Override + public BiConsumer<Integer, Integer> accumulator() { + return (a, b) -> { }; + } + + @Override + public BinaryOperator<Integer> combiner() { + return (a, b) -> a + b; + } + + @Override + public Function<Integer, Integer> finisher() { + return a -> { throw new TestException(); }; + } + + @Override + public Set<Characteristics> characteristics() { + return Collections.emptySet(); + } + }) + .test() + .assertFailure(TestException.class); + } + + @Test + public void collectorAccumulatorDropSignals() throws Throwable { + TestHelper.withErrorTracking(errors -> { + Flowable<Integer> source = new Flowable<Integer>() { + @Override + protected void subscribeActual(Subscriber<? super Integer> s) { + s.onSubscribe(new BooleanSubscription()); + s.onNext(1); + s.onNext(2); + s.onError(new IOException()); + s.onComplete(); + } + }; + + source + .collect(new Collector<Integer, Integer, Integer>() { + + @Override + public Supplier<Integer> supplier() { + return () -> 1; + } + + @Override + public BiConsumer<Integer, Integer> accumulator() { + return (a, b) -> { throw new TestException(); }; + } + + @Override + public BinaryOperator<Integer> combiner() { + return (a, b) -> a + b; + } + + @Override + public Function<Integer, Integer> finisher() { + return a -> a; + } + + @Override + public Set<Characteristics> characteristics() { + return Collections.emptySet(); + } + }) + .test() + .assertFailure(TestException.class); + + TestHelper.assertUndeliverable(errors, 0, IOException.class); + }); + } + + @Test + public void dispose() { + TestHelper.checkDisposed(PublishProcessor.create() + .collect(Collectors.toList())); + } + + @Test + public void onSubscribe() { + TestHelper.checkDoubleOnSubscribeFlowableToSingle(f -> f.collect(Collectors.toList())); + } + + @Test + public void basicToFlowable() { + Flowable.range(1, 5) + .collect(Collectors.toList()) + .toFlowable() + .test() + .assertResult(Arrays.asList(1, 2, 3, 4, 5)); + } + + @Test + public void emptyToFlowable() { + Flowable.empty() + .collect(Collectors.toList()) + .toFlowable() + .test() + .assertResult(Collections.emptyList()); + } + + @Test + public void errorToFlowable() { + Flowable.error(new TestException()) + .collect(Collectors.toList()) + .toFlowable() + .test() + .assertFailure(TestException.class); + } + + @Test + public void collectorSupplierCrashToFlowable() { + Flowable.range(1, 5) + .collect(new Collector<Integer, Integer, Integer>() { + + @Override + public Supplier<Integer> supplier() { + throw new TestException(); + } + + @Override + public BiConsumer<Integer, Integer> accumulator() { + return (a, b) -> { }; + } + + @Override + public BinaryOperator<Integer> combiner() { + return (a, b) -> a + b; + } + + @Override + public Function<Integer, Integer> finisher() { + return a -> a; + } + + @Override + public Set<Characteristics> characteristics() { + return Collections.emptySet(); + } + }) + .toFlowable() + .test() + .assertFailure(TestException.class); + } + + @Test + public void collectorAccumulatorCrashToFlowable() { + BehaviorProcessor<Integer> source = BehaviorProcessor.createDefault(1); + + source + .collect(new Collector<Integer, Integer, Integer>() { + + @Override + public Supplier<Integer> supplier() { + return () -> 1; + } + + @Override + public BiConsumer<Integer, Integer> accumulator() { + return (a, b) -> { throw new TestException(); }; + } + + @Override + public BinaryOperator<Integer> combiner() { + return (a, b) -> a + b; + } + + @Override + public Function<Integer, Integer> finisher() { + return a -> a; + } + + @Override + public Set<Characteristics> characteristics() { + return Collections.emptySet(); + } + }) + .toFlowable() + .test() + .assertFailure(TestException.class); + + assertFalse(source.hasSubscribers()); + } + + @Test + public void collectorFinisherCrashToFlowable() { + Flowable.range(1, 5) + .collect(new Collector<Integer, Integer, Integer>() { + + @Override + public Supplier<Integer> supplier() { + return () -> 1; + } + + @Override + public BiConsumer<Integer, Integer> accumulator() { + return (a, b) -> { }; + } + + @Override + public BinaryOperator<Integer> combiner() { + return (a, b) -> a + b; + } + + @Override + public Function<Integer, Integer> finisher() { + return a -> { throw new TestException(); }; + } + + @Override + public Set<Characteristics> characteristics() { + return Collections.emptySet(); + } + }) + .toFlowable() + .test() + .assertFailure(TestException.class); + } + + @Test + public void collectorAccumulatorDropSignalsToFlowable() throws Throwable { + TestHelper.withErrorTracking(errors -> { + Flowable<Integer> source = new Flowable<Integer>() { + @Override + protected void subscribeActual(Subscriber<? super Integer> s) { + s.onSubscribe(new BooleanSubscription()); + s.onNext(1); + s.onNext(2); + s.onError(new IOException()); + s.onComplete(); + } + }; + + source + .collect(new Collector<Integer, Integer, Integer>() { + + @Override + public Supplier<Integer> supplier() { + return () -> 1; + } + + @Override + public BiConsumer<Integer, Integer> accumulator() { + return (a, b) -> { throw new TestException(); }; + } + + @Override + public BinaryOperator<Integer> combiner() { + return (a, b) -> a + b; + } + + @Override + public Function<Integer, Integer> finisher() { + return a -> a; + } + + @Override + public Set<Characteristics> characteristics() { + return Collections.emptySet(); + } + }) + .toFlowable() + .test() + .assertFailure(TestException.class); + + TestHelper.assertUndeliverable(errors, 0, IOException.class); + }); + } + + @Test + public void disposeToFlowable() { + TestHelper.checkDisposed(PublishProcessor.create() + .collect(Collectors.toList()).toFlowable()); + } + + @Test + public void onSubscribeToFlowable() { + TestHelper.checkDoubleOnSubscribeFlowable(f -> f.collect(Collectors.toList()).toFlowable()); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/jdk8/FlowableFlatMapStreamTest.java b/src/test/java/io/reactivex/rxjava3/internal/jdk8/FlowableFlatMapStreamTest.java new file mode 100644 index 0000000000..c865d35a52 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/jdk8/FlowableFlatMapStreamTest.java @@ -0,0 +1,451 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.jdk8; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +import java.io.IOException; +import java.util.Iterator; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.*; + +import org.junit.Test; +import org.reactivestreams.Subscriber; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.internal.subscriptions.BooleanSubscription; +import io.reactivex.rxjava3.processors.*; +import io.reactivex.rxjava3.subscribers.TestSubscriber; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class FlowableFlatMapStreamTest extends RxJavaTest { + + @Test + public void empty() { + Flowable.empty() + .flatMapStream(v -> Stream.of(1, 2, 3, 4, 5)) + .test() + .assertResult(); + } + + @Test + public void emptyHidden() { + Flowable.empty() + .hide() + .flatMapStream(v -> Stream.of(1, 2, 3, 4, 5)) + .test() + .assertResult(); + } + + @Test + public void just() { + Flowable.just(1) + .flatMapStream(v -> Stream.of(v + 1, v + 2, v + 3, v + 4, v + 5)) + .test() + .assertResult(2, 3, 4, 5, 6); + } + + @Test + public void justHidden() { + Flowable.just(1).hide() + .flatMapStream(v -> Stream.of(v + 1, v + 2, v + 3, v + 4, v + 5)) + .test() + .assertResult(2, 3, 4, 5, 6); + } + + @Test + public void error() { + Flowable.error(new TestException()) + .flatMapStream(v -> Stream.of(1, 2, 3, 4, 5)) + .test() + .assertFailure(TestException.class); + } + + @Test + public void supplierFusedError() { + Flowable.fromCallable(() -> { throw new TestException(); }) + .flatMapStream(v -> Stream.of(1, 2, 3, 4, 5)) + .test() + .assertFailure(TestException.class); + } + + @Test + public void errorHidden() { + Flowable.error(new TestException()) + .hide() + .flatMapStream(v -> Stream.of(1, 2, 3, 4, 5)) + .test() + .assertFailure(TestException.class); + } + + @Test + public void range() { + Flowable.range(1, 5) + .flatMapStream(v -> IntStream.range(v * 10, v * 10 + 5).boxed()) + .test() + .assertResult( + 10, 11, 12, 13, 14, + 20, 21, 22, 23, 24, + 30, 31, 32, 33, 34, + 40, 41, 42, 43, 44, + 50, 51, 52, 53, 54 + ); + } + + @Test + public void rangeHidden() { + Flowable.range(1, 5) + .hide() + .flatMapStream(v -> IntStream.range(v * 10, v * 10 + 5).boxed()) + .test() + .assertResult( + 10, 11, 12, 13, 14, + 20, 21, 22, 23, 24, + 30, 31, 32, 33, 34, + 40, 41, 42, 43, 44, + 50, 51, 52, 53, 54 + ); + } + + @Test + public void rangeToEmpty() { + Flowable.range(1, 5) + .flatMapStream(v -> Stream.of()) + .test() + .assertResult(); + } + + @Test + public void rangeTake() { + Flowable.range(1, 5) + .flatMapStream(v -> IntStream.range(v * 10, v * 10 + 5).boxed()) + .take(12) + .test() + .assertResult( + 10, 11, 12, 13, 14, + 20, 21, 22, 23, 24, + 30, 31 + ); + } + + @Test + public void rangeTakeHidden() { + Flowable.range(1, 5) + .hide() + .flatMapStream(v -> IntStream.range(v * 10, v * 10 + 5).boxed()) + .take(12) + .test() + .assertResult( + 10, 11, 12, 13, 14, + 20, 21, 22, 23, 24, + 30, 31 + ); + } + + @Test + public void upstreamCancelled() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + + AtomicInteger calls = new AtomicInteger(); + + TestSubscriber<Integer> ts = pp + .flatMapStream(v -> Stream.of(v + 1, v + 2).onClose(() -> calls.getAndIncrement())) + .test(1); + + assertTrue(pp.hasSubscribers()); + + pp.onNext(1); + + ts.assertValuesOnly(2); + ts.cancel(); + + assertFalse(pp.hasSubscribers()); + + assertEquals(1, calls.get()); + } + + @Test + public void upstreamCancelledCloseCrash() throws Throwable { + TestHelper.withErrorTracking(errors -> { + PublishProcessor<Integer> pp = PublishProcessor.create(); + + TestSubscriber<Integer> ts = pp + .flatMapStream(v -> Stream.of(v + 1, v + 2).onClose(() -> { throw new TestException(); })) + .test(1); + + assertTrue(pp.hasSubscribers()); + + pp.onNext(1); + + ts.assertValuesOnly(2); + ts.cancel(); + + assertFalse(pp.hasSubscribers()); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + }); + } + + @Test + public void crossMap() { + Flowable.range(1, 1000) + .flatMapStream(v -> IntStream.range(v * 1000, v * 1000 + 1000).boxed()) + .test() + .assertValueCount(1_000_000) + .assertNoErrors() + .assertComplete(); + } + + @Test + public void crossMapHidden() { + Flowable.range(1, 1000) + .hide() + .flatMapStream(v -> IntStream.range(v * 1000, v * 1000 + 1000).boxed()) + .test() + .assertValueCount(1_000_000) + .assertNoErrors() + .assertComplete(); + } + + @Test + public void crossMapBackpressured() { + for (int n = 1; n < 2048; n *= 2) { + Flowable.range(1, 1000) + .flatMapStream(v -> IntStream.range(v * 1000, v * 1000 + 1000).boxed()) + .rebatchRequests(n) + .test() + .withTag("rebatch: " + n) + .assertValueCount(1_000_000) + .assertNoErrors() + .assertComplete(); + } + } + + @Test + public void crossMapBackpressuredHidden() { + for (int n = 1; n < 2048; n *= 2) { + Flowable.range(1, 1000) + .hide() + .flatMapStream(v -> IntStream.range(v * 1000, v * 1000 + 1000).boxed()) + .rebatchRequests(n) + .test() + .withTag("rebatch: " + n) + .assertValueCount(1_000_000) + .assertNoErrors() + .assertComplete(); + } + } + + @Test + public void onSubscribe() { + TestHelper.checkDoubleOnSubscribeFlowable(f -> f.flatMapStream(v -> Stream.of(1, 2))); + } + + @Test + public void badRequest() { + TestHelper.assertBadRequestReported(UnicastProcessor.create().flatMapStream(v -> Stream.of(1, 2))); + } + + @Test + public void queueOverflow() throws Throwable { + TestHelper.withErrorTracking(errors -> { + new Flowable<Integer>() { + @Override + protected void subscribeActual(Subscriber<? super Integer> s) { + s.onSubscribe(new BooleanSubscription()); + s.onNext(1); + s.onNext(2); + s.onNext(3); + s.onError(new TestException()); + } + } + .flatMapStream(v -> Stream.of(1, 2), 1) + .test(0) + .assertFailure(QueueOverflowException.class); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + }); + } + + @Test + public void mapperThrows() { + Flowable.just(1).hide() + .concatMapStream(v -> { throw new TestException(); }) + .test() + .assertFailure(TestException.class); + } + + @Test + public void mapperNull() { + Flowable.just(1).hide() + .concatMapStream(v -> null) + .test() + .assertFailure(NullPointerException.class); + } + + @Test + public void streamNull() { + Flowable.just(1).hide() + .concatMapStream(v -> Stream.of(1, null)) + .test() + .assertFailure(NullPointerException.class, 1); + } + + @Test + public void hasNextThrows() { + Flowable.just(1).hide() + .concatMapStream(v -> Stream.generate(() -> { throw new TestException(); })) + .test() + .assertFailure(TestException.class); + } + + @Test + public void hasNextThrowsLater() { + AtomicInteger counter = new AtomicInteger(); + Flowable.just(1).hide() + .concatMapStream(v -> Stream.generate(() -> { + if (counter.getAndIncrement() == 0) { + return 1; + } + throw new TestException(); + })) + .test() + .assertFailure(TestException.class, 1); + } + + @Test + public void mapperThrowsWhenUpstreamErrors() throws Throwable { + TestHelper.withErrorTracking(errors -> { + PublishProcessor<Integer> pp = PublishProcessor.create(); + + AtomicInteger counter = new AtomicInteger(); + + TestSubscriber<Integer> ts = pp.hide() + .concatMapStream(v -> { + if (counter.getAndIncrement() == 0) { + return Stream.of(1, 2); + } + pp.onError(new IOException()); + throw new TestException(); + }) + .test(); + + pp.onNext(1); + pp.onNext(2); + + ts + .assertFailure(IOException.class, 1, 2); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + }); + } + + @Test + public void rangeBackpressured() { + Flowable.range(1, 5) + .hide() + .concatMapStream(v -> Stream.of(v), 1) + .test(0) + .assertEmpty() + .requestMore(5) + .assertResult(1, 2, 3, 4, 5); + } + + @Test + public void cancelAfterIteratorNext() throws Exception { + TestSubscriber<Integer> ts = new TestSubscriber<>(); + + @SuppressWarnings("unchecked") + Stream<Integer> stream = mock(Stream.class); + when(stream.iterator()).thenReturn(new Iterator<Integer>() { + + @Override + public boolean hasNext() { + return true; + } + + @Override + public Integer next() { + ts.cancel(); + return 1; + } + }); + + Flowable.just(1) + .hide() + .concatMapStream(v -> stream) + .subscribe(ts); + + ts.assertEmpty(); + } + + @Test + public void asyncUpstreamFused() { + UnicastProcessor<Integer> up = UnicastProcessor.create(); + + TestSubscriber<Integer> ts = up.flatMapStream(v -> Stream.of(1, 2)) + .test(); + + assertTrue(up.hasSubscribers()); + + up.onNext(1); + + ts.assertValuesOnly(1, 2); + + up.onComplete(); + + ts.assertResult(1, 2); + } + + @Test + public void asyncUpstreamFusionBoundary() { + UnicastProcessor<Integer> up = UnicastProcessor.create(); + + TestSubscriber<Integer> ts = up + .map(v -> v + 1) + .flatMapStream(v -> Stream.of(1, 2)) + .test(); + + assertTrue(up.hasSubscribers()); + + up.onNext(1); + + ts.assertValuesOnly(1, 2); + + up.onComplete(); + + ts.assertResult(1, 2); + } + + @Test + public void fusedPollCrash() { + UnicastProcessor<Integer> up = UnicastProcessor.create(); + + TestSubscriber<Integer> ts = up + .map(v -> { throw new TestException(); }) + .compose(TestHelper.flowableStripBoundary()) + .flatMapStream(v -> Stream.of(1, 2)) + .test(); + + assertTrue(up.hasSubscribers()); + + up.onNext(1); + + assertFalse(up.hasSubscribers()); + + ts.assertFailure(TestException.class); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/jdk8/FlowableFromCompletionStageTest.java b/src/test/java/io/reactivex/rxjava3/internal/jdk8/FlowableFromCompletionStageTest.java new file mode 100644 index 0000000000..aebeea58b6 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/jdk8/FlowableFromCompletionStageTest.java @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.jdk8; + +import java.util.concurrent.CompletableFuture; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.subscribers.TestSubscriber; + +public class FlowableFromCompletionStageTest extends RxJavaTest { + + @Test + public void syncSuccess() { + Flowable.fromCompletionStage(CompletableFuture.completedFuture(1)) + .test() + .assertResult(1); + } + + @Test + public void syncFailure() { + CompletableFuture<Integer> cf = new CompletableFuture<>(); + cf.completeExceptionally(new TestException()); + + Flowable.fromCompletionStage(cf) + .test() + .assertFailure(TestException.class); + } + + @Test + public void syncNull() { + Flowable.fromCompletionStage(CompletableFuture.<Integer>completedFuture(null)) + .test() + .assertFailure(NullPointerException.class); + } + + @Test + public void cancel() { + CompletableFuture<Integer> cf = new CompletableFuture<>(); + + TestSubscriber<Integer> ts = Flowable.fromCompletionStage(cf) + .test(); + + ts.assertEmpty(); + + ts.cancel(); + + cf.complete(1); + + ts.assertEmpty(); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/jdk8/FlowableFromOptionalTest.java b/src/test/java/io/reactivex/rxjava3/internal/jdk8/FlowableFromOptionalTest.java new file mode 100644 index 0000000000..743dc0f573 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/jdk8/FlowableFromOptionalTest.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.jdk8; + +import java.util.Optional; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; + +public class FlowableFromOptionalTest extends RxJavaTest { + + @Test + public void hasValue() { + Flowable.fromOptional(Optional.of(1)) + .test() + .assertResult(1); + } + + @Test + public void empty() { + Flowable.fromOptional(Optional.empty()) + .test() + .assertResult(); + } + +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/jdk8/FlowableFromStreamTest.java b/src/test/java/io/reactivex/rxjava3/internal/jdk8/FlowableFromStreamTest.java new file mode 100644 index 0000000000..57a8caf6bf --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/jdk8/FlowableFromStreamTest.java @@ -0,0 +1,559 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.jdk8; + +import static org.junit.Assert.*; + +import java.util.*; +import java.util.concurrent.*; +import java.util.concurrent.atomic.*; +import java.util.stream.*; + +import org.junit.Test; +import org.reactivestreams.Subscription; + +import io.reactivex.rxjava3.annotations.NonNull; +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.operators.QueueFuseable; +import io.reactivex.rxjava3.operators.QueueSubscription; +import io.reactivex.rxjava3.operators.SimpleQueue; +import io.reactivex.rxjava3.schedulers.Schedulers; +import io.reactivex.rxjava3.testsupport.*; + +public class FlowableFromStreamTest extends RxJavaTest { + + @Test + public void empty() { + Flowable.fromStream(Stream.<Integer>of()) + .test() + .assertResult(); + } + + @Test + public void just() { + Flowable.fromStream(Stream.<Integer>of(1)) + .test() + .assertResult(1); + } + + @Test + public void many() { + Flowable.fromStream(Stream.<Integer>of(1, 2, 3, 4, 5)) + .test() + .assertResult(1, 2, 3, 4, 5); + } + + @Test + public void manyBackpressured() { + Flowable.fromStream(Stream.<Integer>of(1, 2, 3, 4, 5)) + .test(0L) + .assertEmpty() + .requestMore(1) + .assertValuesOnly(1) + .requestMore(2) + .assertValuesOnly(1, 2, 3) + .requestMore(2) + .assertResult(1, 2, 3, 4, 5); + } + + @Test + public void noReuse() { + Flowable<Integer> source = Flowable.fromStream(Stream.<Integer>of(1, 2, 3, 4, 5)); + + source + .test() + .assertResult(1, 2, 3, 4, 5); + + source + .test() + .assertFailure(IllegalStateException.class); + } + + @Test + public void take() { + Flowable.fromStream(IntStream.rangeClosed(1, 10).boxed()) + .take(5) + .test() + .assertResult(1, 2, 3, 4, 5); + } + + @Test + public void emptyConditional() { + Flowable.fromStream(Stream.<Integer>of()) + .filter(v -> true) + .test() + .assertResult(); + } + + @Test + public void justConditional() { + Flowable.fromStream(Stream.<Integer>of(1)) + .filter(v -> true) + .test() + .assertResult(1); + } + + @Test + public void manyConditional() { + Flowable.fromStream(Stream.<Integer>of(1, 2, 3, 4, 5)) + .filter(v -> true) + .test() + .assertResult(1, 2, 3, 4, 5); + } + + @Test + public void manyBackpressuredConditional() { + Flowable.fromStream(Stream.<Integer>of(1, 2, 3, 4, 5)) + .filter(v -> true) + .test(0L) + .assertEmpty() + .requestMore(1) + .assertValuesOnly(1) + .requestMore(2) + .assertValuesOnly(1, 2, 3) + .requestMore(2) + .assertResult(1, 2, 3, 4, 5); + } + + @Test + public void manyConditionalSkip() { + Flowable.fromStream(IntStream.rangeClosed(1, 10).boxed()) + .filter(v -> v % 2 == 0) + .test() + .assertResult(2, 4, 6, 8, 10); + } + + @Test + public void takeConditional() { + Flowable.fromStream(IntStream.rangeClosed(1, 10).boxed()) + .filter(v -> true) + .take(5) + .test() + .assertResult(1, 2, 3, 4, 5); + } + + @Test + public void noOfferNoCrashAfterClear() throws Throwable { + AtomicReference<SimpleQueue<?>> queue = new AtomicReference<>(); + + Flowable.fromStream(IntStream.rangeClosed(1, 10).boxed()) + .subscribe(new FlowableSubscriber<Integer>() { + @Override + public void onSubscribe(@NonNull Subscription s) { + queue.set((SimpleQueue<?>)s); + } + + @Override + public void onNext(Integer t) { + } + + @Override + public void onError(Throwable t) { + } + + @Override + public void onComplete() { + } + }); + + SimpleQueue<?> q = queue.get(); + TestHelper.assertNoOffer(q); + + assertFalse(q.isEmpty()); + + q.clear(); + + assertNull(q.poll()); + + assertTrue(q.isEmpty()); + + q.clear(); + + assertNull(q.poll()); + + assertTrue(q.isEmpty()); + } + + @Test + public void fusedPoll() throws Throwable { + AtomicReference<SimpleQueue<?>> queue = new AtomicReference<>(); + AtomicInteger calls = new AtomicInteger(); + + Flowable.fromStream(Stream.of(1).onClose(() -> calls.getAndIncrement())) + .subscribe(new FlowableSubscriber<Integer>() { + @Override + public void onSubscribe(@NonNull Subscription s) { + queue.set((SimpleQueue<?>)s); + ((QueueSubscription<?>)s).requestFusion(QueueFuseable.ANY); + } + + @Override + public void onNext(Integer t) { + } + + @Override + public void onError(Throwable t) { + } + + @Override + public void onComplete() { + } + }); + + SimpleQueue<?> q = queue.get(); + + assertFalse(q.isEmpty()); + + assertEquals(1, q.poll()); + + assertTrue(q.isEmpty()); + + assertEquals(1, calls.get()); + } + + @Test + public void streamOfNull() { + Flowable.fromStream(Stream.of((Integer)null)) + .test() + .assertFailure(NullPointerException.class); + } + + @Test + public void streamOfNullConditional() { + Flowable.fromStream(Stream.of((Integer)null)) + .filter(v -> true) + .test() + .assertFailure(NullPointerException.class); + } + + @Test + public void syncFusionSupport() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(); + ts.setInitialFusionMode(QueueFuseable.ANY); + + Flowable.fromStream(IntStream.rangeClosed(1, 10).boxed()) + .subscribeWith(ts) + .assertFuseable() + .assertFusionMode(QueueFuseable.SYNC) + .assertResult(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + } + + @Test + public void asyncFusionNotSupported() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(); + ts.setInitialFusionMode(QueueFuseable.ASYNC); + + Flowable.fromStream(IntStream.rangeClosed(1, 10).boxed()) + .subscribeWith(ts) + .assertFuseable() + .assertFusionMode(QueueFuseable.NONE) + .assertResult(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + } + + @Test + public void fusedForParallel() { + Flowable.fromStream(IntStream.rangeClosed(1, 1000).boxed()) + .parallel() + .runOn(Schedulers.computation(), 1) + .map(v -> v + 1) + .sequential() + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertValueCount(1000) + .assertNoErrors() + .assertComplete(); + } + + @Test + public void runToEndCloseCrash() throws Throwable { + TestHelper.withErrorTracking(errors -> { + Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5).onClose(() -> { throw new TestException(); }); + + Flowable.fromStream(stream) + .test() + .assertResult(1, 2, 3, 4, 5); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + }); + } + + @Test + public void takeCloseCrash() throws Throwable { + TestHelper.withErrorTracking(errors -> { + Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5).onClose(() -> { throw new TestException(); }); + + Flowable.fromStream(stream) + .take(3) + .test() + .assertResult(1, 2, 3); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + }); + } + + @Test + public void hasNextCrash() { + AtomicInteger v = new AtomicInteger(); + Flowable.fromStream(Stream.<Integer>generate(() -> { + int value = v.getAndIncrement(); + if (value == 1) { + throw new TestException(); + } + return value; + })) + .test() + .assertFailure(TestException.class, 0); + } + + @Test + public void hasNextCrashConditional() { + AtomicInteger counter = new AtomicInteger(); + Flowable.fromStream(Stream.<Integer>generate(() -> { + int value = counter.getAndIncrement(); + if (value == 1) { + throw new TestException(); + } + return value; + })) + .filter(v -> true) + .test() + .assertFailure(TestException.class, 0); + } + + void requestOneByOneBase(boolean conditional) { + List<Object> list = new ArrayList<>(); + + Flowable<Integer> source = Flowable.fromStream(IntStream.rangeClosed(1, 10).boxed()); + if (conditional) { + source = source.filter(v -> true); + } + + source.subscribe(new FlowableSubscriber<Integer>() { + + @NonNull Subscription upstream; + + @Override + public void onSubscribe(@NonNull Subscription s) { + this.upstream = s; + s.request(1); + } + + @Override + public void onNext(Integer t) { + list.add(t); + upstream.request(1); + } + + @Override + public void onError(Throwable t) { + list.add(t); + } + + @Override + public void onComplete() { + list.add(100); + } + }); + + assertEquals(Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 100), list); + } + + @Test + public void requestOneByOne() { + requestOneByOneBase(false); + } + + @Test + public void requestOneByOneConditional() { + requestOneByOneBase(true); + } + + void requestRaceBase(boolean conditional) throws Exception { + ExecutorService exec = Executors.newCachedThreadPool(); + try { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + + AtomicInteger counter = new AtomicInteger(); + + int max = 100; + + Flowable<Integer> source = Flowable.fromStream(IntStream.rangeClosed(1, max).boxed()); + if (conditional) { + source = source.filter(v -> true); + } + + CountDownLatch cdl = new CountDownLatch(1); + + source + .subscribe(new FlowableSubscriber<Integer>() { + + @NonNull Subscription upstream; + + @Override + public void onSubscribe(@NonNull Subscription s) { + + this.upstream = s; + s.request(1); + + } + + @Override + public void onNext(Integer t) { + counter.getAndIncrement(); + + AtomicInteger sync = new AtomicInteger(2); + exec.submit(() -> { + if (sync.decrementAndGet() != 0) { + while (sync.get() != 0) { } + } + upstream.request(1); + }); + + if (sync.decrementAndGet() != 0) { + while (sync.get() != 0) { } + } + } + + @Override + public void onError(Throwable t) { + t.printStackTrace(); + cdl.countDown(); + } + + @Override + public void onComplete() { + counter.getAndIncrement(); + cdl.countDown(); + } + }); + + assertTrue(cdl.await(60, TimeUnit.SECONDS)); + + assertEquals(max + 1, counter.get()); + } + } finally { + exec.shutdown(); + } + } + + @Test + public void requestRace() throws Exception { + requestRaceBase(false); + } + + @Test + public void requestRaceConditional() throws Exception { + requestRaceBase(true); + } + + @Test + public void closeCalledOnEmpty() { + AtomicInteger calls = new AtomicInteger(); + + Flowable.fromStream(Stream.of().onClose(() -> calls.getAndIncrement())) + .test() + .assertResult(); + + assertEquals(1, calls.get()); + } + + @Test + public void closeCalledAfterItems() { + AtomicInteger calls = new AtomicInteger(); + + Flowable.fromStream(Stream.of(1, 2, 3, 4, 5).onClose(() -> calls.getAndIncrement())) + .test() + .assertResult(1, 2, 3, 4, 5); + + assertEquals(1, calls.get()); + } + + @Test + public void closeCalledOnCancel() { + AtomicInteger calls = new AtomicInteger(); + + Flowable.fromStream(Stream.of(1, 2, 3, 4, 5).onClose(() -> calls.getAndIncrement())) + .take(3) + .test() + .assertResult(1, 2, 3); + + assertEquals(1, calls.get()); + } + + @Test + public void closeCalledOnItemCrash() { + AtomicInteger calls = new AtomicInteger(); + AtomicInteger counter = new AtomicInteger(); + Flowable.fromStream(Stream.<Integer>generate(() -> { + int value = counter.getAndIncrement(); + if (value == 1) { + throw new TestException(); + } + return value; + }).onClose(() -> calls.getAndIncrement())) + .test() + .assertFailure(TestException.class, 0); + + assertEquals(1, calls.get()); + } + + @Test + public void closeCalledAfterItemsConditional() { + AtomicInteger calls = new AtomicInteger(); + + Flowable.fromStream(Stream.of(1, 2, 3, 4, 5).onClose(() -> calls.getAndIncrement())) + .filter(v -> true) + .test() + .assertResult(1, 2, 3, 4, 5); + + assertEquals(1, calls.get()); + } + + @Test + public void closeCalledOnCancelConditional() { + AtomicInteger calls = new AtomicInteger(); + + Flowable.fromStream(Stream.of(1, 2, 3, 4, 5).onClose(() -> calls.getAndIncrement())) + .filter(v -> true) + .take(3) + .test() + .assertResult(1, 2, 3); + + assertEquals(1, calls.get()); + } + + @Test + public void closeCalledOnItemCrashConditional() { + AtomicInteger calls = new AtomicInteger(); + AtomicInteger counter = new AtomicInteger(); + Flowable.fromStream(Stream.<Integer>generate(() -> { + int value = counter.getAndIncrement(); + if (value == 1) { + throw new TestException(); + } + return value; + }).onClose(() -> calls.getAndIncrement())) + .filter(v -> true) + .test() + .assertFailure(TestException.class, 0); + + assertEquals(1, calls.get()); + } + + @Test + public void badRequest() { + TestHelper.assertBadRequestReported(Flowable.fromStream(Stream.of(1))); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/jdk8/FlowableMapOptionalTest.java b/src/test/java/io/reactivex/rxjava3/internal/jdk8/FlowableMapOptionalTest.java new file mode 100644 index 0000000000..fbffd79001 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/jdk8/FlowableMapOptionalTest.java @@ -0,0 +1,487 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.jdk8; + +import static org.junit.Assert.assertFalse; + +import java.util.Optional; + +import org.junit.Test; +import org.reactivestreams.Subscriber; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.schedulers.ImmediateThinScheduler; +import io.reactivex.rxjava3.internal.subscriptions.BooleanSubscription; +import io.reactivex.rxjava3.operators.QueueFuseable; +import io.reactivex.rxjava3.processors.*; +import io.reactivex.rxjava3.testsupport.*; + +public class FlowableMapOptionalTest extends RxJavaTest { + + static final Function<? super Integer, Optional<? extends Integer>> MODULO = v -> v % 2 == 0 ? Optional.of(v) : Optional.<Integer>empty(); + + @Test + public void allPresent() { + Flowable.range(1, 5) + .mapOptional(Optional::of) + .test() + .assertResult(1, 2, 3, 4, 5); + } + + @Test + public void allEmpty() { + Flowable.range(1, 5) + .mapOptional(v -> Optional.<Integer>empty()) + .test() + .assertResult(); + } + + @Test + public void mixed() { + Flowable.range(1, 10) + .mapOptional(MODULO) + .test() + .assertResult(2, 4, 6, 8, 10); + } + + @Test + public void mapperChash() { + BehaviorProcessor<Integer> source = BehaviorProcessor.createDefault(1); + + source + .mapOptional(v -> { throw new TestException(); }) + .test() + .assertFailure(TestException.class); + + assertFalse(source.hasSubscribers()); + } + + @Test + public void mapperNull() { + BehaviorProcessor<Integer> source = BehaviorProcessor.createDefault(1); + + source + .mapOptional(v -> null) + .test() + .assertFailure(NullPointerException.class); + + assertFalse(source.hasSubscribers()); + } + + @Test + public void crashDropsOnNexts() { + Flowable<Integer> source = new Flowable<Integer>() { + @Override + protected void subscribeActual(Subscriber<? super Integer> s) { + s.onSubscribe(new BooleanSubscription()); + s.onNext(1); + s.onNext(2); + } + }; + + source + .mapOptional(v -> { throw new TestException(); }) + .test() + .assertFailure(TestException.class); + } + + @Test + public void backpressureAll() { + Flowable.range(1, 5) + .mapOptional(Optional::of) + .test(0L) + .assertEmpty() + .requestMore(2) + .assertValuesOnly(1, 2) + .requestMore(2) + .assertValuesOnly(1, 2, 3, 4) + .requestMore(1) + .assertResult(1, 2, 3, 4, 5); + } + + @Test + public void backpressureNone() { + Flowable.range(1, 5) + .mapOptional(v -> Optional.empty()) + .test(1L) + .assertResult(); + } + + @Test + public void backpressureMixed() { + Flowable.range(1, 10) + .mapOptional(MODULO) + .test(0L) + .assertEmpty() + .requestMore(2) + .assertValuesOnly(2, 4) + .requestMore(2) + .assertValuesOnly(2, 4, 6, 8) + .requestMore(1) + .assertResult(2, 4, 6, 8, 10); + } + + @Test + public void syncFusedAll() { + Flowable.range(1, 5) + .mapOptional(Optional::of) + .to(TestHelper.testConsumer(false, QueueFuseable.SYNC)) + .assertFuseable() + .assertFusionMode(QueueFuseable.SYNC) + .assertResult(1, 2, 3, 4, 5); + } + + @Test + public void asyncFusedAll() { + UnicastProcessor<Integer> up = UnicastProcessor.create(); + TestHelper.emit(up, 1, 2, 3, 4, 5); + + up + .mapOptional(Optional::of) + .to(TestHelper.testConsumer(false, QueueFuseable.ASYNC)) + .assertFuseable() + .assertFusionMode(QueueFuseable.ASYNC) + .assertResult(1, 2, 3, 4, 5); + } + + @Test + public void boundaryFusedAll() { + UnicastProcessor<Integer> up = UnicastProcessor.create(); + TestHelper.emit(up, 1, 2, 3, 4, 5); + + up + .mapOptional(Optional::of) + .to(TestHelper.testConsumer(false, QueueFuseable.ASYNC | QueueFuseable.BOUNDARY)) + .assertFuseable() + .assertFusionMode(QueueFuseable.NONE) + .assertResult(1, 2, 3, 4, 5); + } + + @Test + public void syncFusedNone() { + Flowable.range(1, 5) + .mapOptional(v -> Optional.empty()) + .to(TestHelper.testConsumer(false, QueueFuseable.SYNC)) + .assertFuseable() + .assertFusionMode(QueueFuseable.SYNC) + .assertResult(); + } + + @Test + public void asyncFusedNone() { + UnicastProcessor<Integer> up = UnicastProcessor.create(); + TestHelper.emit(up, 1, 2, 3, 4, 5); + + up + .mapOptional(v -> Optional.empty()) + .to(TestHelper.testConsumer(false, QueueFuseable.ASYNC)) + .assertFuseable() + .assertFusionMode(QueueFuseable.ASYNC) + .assertResult(); + } + + @Test + public void boundaryFusedNone() { + UnicastProcessor<Integer> up = UnicastProcessor.create(); + TestHelper.emit(up, 1, 2, 3, 4, 5); + + up + .mapOptional(v -> Optional.empty()) + .to(TestHelper.testConsumer(false, QueueFuseable.ASYNC | QueueFuseable.BOUNDARY)) + .assertFuseable() + .assertFusionMode(QueueFuseable.NONE) + .assertResult(); + } + + @Test + public void syncFusedMixed() { + Flowable.range(1, 10) + .mapOptional(MODULO) + .to(TestHelper.testConsumer(false, QueueFuseable.SYNC)) + .assertFuseable() + .assertFusionMode(QueueFuseable.SYNC) + .assertResult(2, 4, 6, 8, 10); + } + + @Test + public void asyncFusedMixed() { + UnicastProcessor<Integer> up = UnicastProcessor.create(); + TestHelper.emit(up, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + + up + .mapOptional(MODULO) + .to(TestHelper.testConsumer(false, QueueFuseable.ASYNC)) + .assertFuseable() + .assertFusionMode(QueueFuseable.ASYNC) + .assertResult(2, 4, 6, 8, 10); + } + + @Test + public void boundaryFusedMixed() { + UnicastProcessor<Integer> up = UnicastProcessor.create(); + TestHelper.emit(up, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + + up + .mapOptional(MODULO) + .to(TestHelper.testConsumer(false, QueueFuseable.ASYNC | QueueFuseable.BOUNDARY)) + .assertFuseable() + .assertFusionMode(QueueFuseable.NONE) + .assertResult(2, 4, 6, 8, 10); + } + + @Test + public void allPresentConditional() { + Flowable.range(1, 5) + .mapOptional(Optional::of) + .filter(v -> true) + .test() + .assertResult(1, 2, 3, 4, 5); + } + + @Test + public void allEmptyConditional() { + Flowable.range(1, 5) + .mapOptional(v -> Optional.<Integer>empty()) + .filter(v -> true) + .test() + .assertResult(); + } + + @Test + public void mixedConditional() { + Flowable.range(1, 10) + .mapOptional(MODULO) + .filter(v -> true) + .test() + .assertResult(2, 4, 6, 8, 10); + } + + @Test + public void mapperChashConditional() { + BehaviorProcessor<Integer> source = BehaviorProcessor.createDefault(1); + + source + .mapOptional(v -> { throw new TestException(); }) + .filter(v -> true) + .test() + .assertFailure(TestException.class); + + assertFalse(source.hasSubscribers()); + } + + @Test + public void mapperNullConditional() { + BehaviorProcessor<Integer> source = BehaviorProcessor.createDefault(1); + + source + .mapOptional(v -> null) + .filter(v -> true) + .test() + .assertFailure(NullPointerException.class); + + assertFalse(source.hasSubscribers()); + } + + @Test + public void crashDropsOnNextsConditional() { + Flowable<Integer> source = new Flowable<Integer>() { + @Override + protected void subscribeActual(Subscriber<? super Integer> s) { + s.onSubscribe(new BooleanSubscription()); + s.onNext(1); + s.onNext(2); + } + }; + + source + .mapOptional(v -> { throw new TestException(); }) + .filter(v -> true) + .test() + .assertFailure(TestException.class); + } + + @Test + public void backpressureAllConditional() { + Flowable.range(1, 5) + .mapOptional(Optional::of) + .filter(v -> true) + .test(0L) + .assertEmpty() + .requestMore(2) + .assertValuesOnly(1, 2) + .requestMore(2) + .assertValuesOnly(1, 2, 3, 4) + .requestMore(1) + .assertResult(1, 2, 3, 4, 5); + } + + @Test + public void backpressureNoneConditional() { + Flowable.range(1, 5) + .mapOptional(v -> Optional.empty()) + .filter(v -> true) + .test(1L) + .assertResult(); + } + + @Test + public void backpressureMixedConditional() { + Flowable.range(1, 10) + .mapOptional(MODULO) + .filter(v -> true) + .test(0L) + .assertEmpty() + .requestMore(2) + .assertValuesOnly(2, 4) + .requestMore(2) + .assertValuesOnly(2, 4, 6, 8) + .requestMore(1) + .assertResult(2, 4, 6, 8, 10); + } + + @Test + public void syncFusedAllConditional() { + Flowable.range(1, 5) + .mapOptional(Optional::of) + .filter(v -> true) + .to(TestHelper.testConsumer(false, QueueFuseable.SYNC)) + .assertFuseable() + .assertFusionMode(QueueFuseable.SYNC) + .assertResult(1, 2, 3, 4, 5); + } + + @Test + public void asyncFusedAllConditional() { + UnicastProcessor<Integer> up = UnicastProcessor.create(); + TestHelper.emit(up, 1, 2, 3, 4, 5); + + up + .mapOptional(Optional::of) + .filter(v -> true) + .to(TestHelper.testConsumer(false, QueueFuseable.ASYNC)) + .assertFuseable() + .assertFusionMode(QueueFuseable.ASYNC) + .assertResult(1, 2, 3, 4, 5); + } + + @Test + public void boundaryFusedAllConditiona() { + UnicastProcessor<Integer> up = UnicastProcessor.create(); + TestHelper.emit(up, 1, 2, 3, 4, 5); + + up + .mapOptional(Optional::of) + .filter(v -> true) + .to(TestHelper.testConsumer(false, QueueFuseable.ASYNC | QueueFuseable.BOUNDARY)) + .assertFuseable() + .assertFusionMode(QueueFuseable.NONE) + .assertResult(1, 2, 3, 4, 5); + } + + @Test + public void syncFusedNoneConditional() { + Flowable.range(1, 5) + .mapOptional(v -> Optional.empty()) + .filter(v -> true) + .to(TestHelper.testConsumer(false, QueueFuseable.SYNC)) + .assertFuseable() + .assertFusionMode(QueueFuseable.SYNC) + .assertResult(); + } + + @Test + public void asyncFusedNoneConditional() { + UnicastProcessor<Integer> up = UnicastProcessor.create(); + TestHelper.emit(up, 1, 2, 3, 4, 5); + + up + .mapOptional(v -> Optional.empty()) + .filter(v -> true) + .to(TestHelper.testConsumer(false, QueueFuseable.ASYNC)) + .assertFuseable() + .assertFusionMode(QueueFuseable.ASYNC) + .assertResult(); + } + + @Test + public void boundaryFusedNoneConditional() { + UnicastProcessor<Integer> up = UnicastProcessor.create(); + TestHelper.emit(up, 1, 2, 3, 4, 5); + + up + .mapOptional(v -> Optional.empty()) + .filter(v -> true) + .to(TestHelper.testConsumer(false, QueueFuseable.ASYNC | QueueFuseable.BOUNDARY)) + .assertFuseable() + .assertFusionMode(QueueFuseable.NONE) + .assertResult(); + } + + @Test + public void syncFusedMixedConditional() { + Flowable.range(1, 10) + .mapOptional(MODULO) + .filter(v -> true) + .to(TestHelper.testConsumer(false, QueueFuseable.SYNC)) + .assertFuseable() + .assertFusionMode(QueueFuseable.SYNC) + .assertResult(2, 4, 6, 8, 10); + } + + @Test + public void asyncFusedMixedConditional() { + UnicastProcessor<Integer> up = UnicastProcessor.create(); + TestHelper.emit(up, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + + up + .mapOptional(MODULO) + .filter(v -> true) + .to(TestHelper.testConsumer(false, QueueFuseable.ASYNC)) + .assertFuseable() + .assertFusionMode(QueueFuseable.ASYNC) + .assertResult(2, 4, 6, 8, 10); + } + + @Test + public void boundaryFusedMixedConditional() { + UnicastProcessor<Integer> up = UnicastProcessor.create(); + TestHelper.emit(up, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + + up + .mapOptional(MODULO) + .filter(v -> true) + .to(TestHelper.testConsumer(false, QueueFuseable.ASYNC | QueueFuseable.BOUNDARY)) + .assertFuseable() + .assertFusionMode(QueueFuseable.NONE) + .assertResult(2, 4, 6, 8, 10); + } + + @Test + public void conditionalFusionNoNPE() { + TestSubscriberEx<Object> ts = new TestSubscriberEx<>() + .setInitialFusionMode(QueueFuseable.ANY); + + Flowable.empty() + .observeOn(ImmediateThinScheduler.INSTANCE) + .filter(v -> true) + .mapOptional(Optional::of) + .filter(v -> true) + .subscribe(ts) + ; + + ts.assertResult(); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/jdk8/FlowableStageSubscriberOrDefaultTest.java b/src/test/java/io/reactivex/rxjava3/internal/jdk8/FlowableStageSubscriberOrDefaultTest.java new file mode 100644 index 0000000000..d63a81c8ef --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/jdk8/FlowableStageSubscriberOrDefaultTest.java @@ -0,0 +1,476 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.jdk8; + +import static org.junit.Assert.*; + +import java.util.concurrent.*; + +import org.junit.Test; +import org.reactivestreams.Subscriber; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.internal.subscriptions.BooleanSubscription; +import io.reactivex.rxjava3.processors.*; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class FlowableStageSubscriberOrDefaultTest extends RxJavaTest { + + @Test + public void firstJust() throws Exception { + Integer v = Flowable.just(1) + .firstStage(null) + .toCompletableFuture() + .get(); + + assertEquals((Integer)1, v); + } + + @Test + public void firstEmpty() throws Exception { + Integer v = Flowable.<Integer>empty() + .firstStage(2) + .toCompletableFuture() + .get(); + + assertEquals((Integer)2, v); + } + + @Test + public void firstCancels() throws Exception { + BehaviorProcessor<Integer> source = BehaviorProcessor.createDefault(1); + + Integer v = source + .firstStage(null) + .toCompletableFuture() + .get(); + + assertEquals((Integer)1, v); + + assertFalse(source.hasSubscribers()); + } + + @Test + public void firstCompletableFutureCancels() throws Exception { + PublishProcessor<Integer> source = PublishProcessor.create(); + + CompletableFuture<Integer> cf = source + .firstStage(null) + .toCompletableFuture(); + + assertTrue(source.hasSubscribers()); + + cf.cancel(true); + + assertTrue(cf.isCancelled()); + + assertFalse(source.hasSubscribers()); + } + + @Test + public void firstCompletableManualCompleteCancels() throws Exception { + PublishProcessor<Integer> source = PublishProcessor.create(); + + CompletableFuture<Integer> cf = source + .firstStage(null) + .toCompletableFuture(); + + assertTrue(source.hasSubscribers()); + + cf.complete(1); + + assertTrue(cf.isDone()); + assertFalse(cf.isCompletedExceptionally()); + assertFalse(cf.isCancelled()); + + assertFalse(source.hasSubscribers()); + + assertEquals((Integer)1, cf.get()); + } + + @Test + public void firstCompletableManualCompleteExceptionallyCancels() throws Exception { + PublishProcessor<Integer> source = PublishProcessor.create(); + + CompletableFuture<Integer> cf = source + .firstStage(null) + .toCompletableFuture(); + + assertTrue(source.hasSubscribers()); + + cf.completeExceptionally(new TestException()); + + assertTrue(cf.isDone()); + assertTrue(cf.isCompletedExceptionally()); + assertFalse(cf.isCancelled()); + + assertFalse(source.hasSubscribers()); + + TestHelper.assertError(cf, TestException.class); + } + + @Test + public void firstError() throws Exception { + CompletableFuture<Integer> cf = Flowable.<Integer>error(new TestException()) + .firstStage(null) + .toCompletableFuture(); + + assertTrue(cf.isDone()); + assertTrue(cf.isCompletedExceptionally()); + assertFalse(cf.isCancelled()); + + TestHelper.assertError(cf, TestException.class); + } + + @Test + public void firstSourceIgnoresCancel() throws Throwable { + TestHelper.withErrorTracking(errors -> { + Integer v = new Flowable<Integer>() { + @Override + protected void subscribeActual(Subscriber<? super Integer> s) { + s.onSubscribe(new BooleanSubscription()); + s.onNext(1); + s.onError(new TestException()); + s.onComplete(); + } + } + .firstStage(null) + .toCompletableFuture() + .get(); + + assertEquals((Integer)1, v); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + }); + } + + @Test + public void firstDoubleOnSubscribe() throws Throwable { + TestHelper.withErrorTracking(errors -> { + Integer v = new Flowable<Integer>() { + @Override + protected void subscribeActual(Subscriber<? super Integer> s) { + s.onSubscribe(new BooleanSubscription()); + s.onSubscribe(new BooleanSubscription()); + s.onNext(1); + } + } + .firstStage(null) + .toCompletableFuture() + .get(); + + assertEquals((Integer)1, v); + + TestHelper.assertError(errors, 0, ProtocolViolationException.class); + }); + } + + @Test + public void singleJust() throws Exception { + Integer v = Flowable.just(1) + .singleStage(null) + .toCompletableFuture() + .get(); + + assertEquals((Integer)1, v); + } + + @Test + public void singleEmpty() throws Exception { + Integer v = Flowable.<Integer>empty() + .singleStage(2) + .toCompletableFuture() + .get(); + + assertEquals((Integer)2, v); + } + + @Test + public void singleTooManyCancels() throws Exception { + ReplayProcessor<Integer> source = ReplayProcessor.create(); + source.onNext(1); + source.onNext(2); + + TestHelper.assertError(source + .singleStage(null) + .toCompletableFuture(), IllegalArgumentException.class); + + assertFalse(source.hasSubscribers()); + } + + @Test + public void singleCompletableFutureCancels() throws Exception { + PublishProcessor<Integer> source = PublishProcessor.create(); + + CompletableFuture<Integer> cf = source + .singleStage(null) + .toCompletableFuture(); + + assertTrue(source.hasSubscribers()); + + cf.cancel(true); + + assertTrue(cf.isCancelled()); + + assertFalse(source.hasSubscribers()); + } + + @Test + public void singleCompletableManualCompleteCancels() throws Exception { + PublishProcessor<Integer> source = PublishProcessor.create(); + + CompletableFuture<Integer> cf = source + .singleStage(null) + .toCompletableFuture(); + + assertTrue(source.hasSubscribers()); + + cf.complete(1); + + assertTrue(cf.isDone()); + assertFalse(cf.isCompletedExceptionally()); + assertFalse(cf.isCancelled()); + + assertFalse(source.hasSubscribers()); + + assertEquals((Integer)1, cf.get()); + } + + @Test + public void singleCompletableManualCompleteExceptionallyCancels() throws Exception { + PublishProcessor<Integer> source = PublishProcessor.create(); + + CompletableFuture<Integer> cf = source + .singleStage(null) + .toCompletableFuture(); + + assertTrue(source.hasSubscribers()); + + cf.completeExceptionally(new TestException()); + + assertTrue(cf.isDone()); + assertTrue(cf.isCompletedExceptionally()); + assertFalse(cf.isCancelled()); + + assertFalse(source.hasSubscribers()); + + TestHelper.assertError(cf, TestException.class); + } + + @Test + public void singleError() throws Exception { + CompletableFuture<Integer> cf = Flowable.<Integer>error(new TestException()) + .singleStage(null) + .toCompletableFuture(); + + assertTrue(cf.isDone()); + assertTrue(cf.isCompletedExceptionally()); + assertFalse(cf.isCancelled()); + + TestHelper.assertError(cf, TestException.class); + } + + @Test + public void singleSourceIgnoresCancel() throws Throwable { + TestHelper.withErrorTracking(errors -> { + Integer v = new Flowable<Integer>() { + @Override + protected void subscribeActual(Subscriber<? super Integer> s) { + s.onSubscribe(new BooleanSubscription()); + s.onNext(1); + s.onComplete(); + s.onError(new TestException()); + s.onComplete(); + } + } + .singleStage(null) + .toCompletableFuture() + .get(); + + assertEquals((Integer)1, v); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + }); + } + + @Test + public void singleDoubleOnSubscribe() throws Throwable { + TestHelper.withErrorTracking(errors -> { + Integer v = new Flowable<Integer>() { + @Override + protected void subscribeActual(Subscriber<? super Integer> s) { + s.onSubscribe(new BooleanSubscription()); + s.onSubscribe(new BooleanSubscription()); + s.onNext(1); + s.onComplete(); + } + } + .singleStage(null) + .toCompletableFuture() + .get(); + + assertEquals((Integer)1, v); + + TestHelper.assertError(errors, 0, ProtocolViolationException.class); + }); + } + + @Test + public void lastJust() throws Exception { + Integer v = Flowable.just(1) + .lastStage(null) + .toCompletableFuture() + .get(); + + assertEquals((Integer)1, v); + } + + @Test + public void lastRange() throws Exception { + Integer v = Flowable.range(1, 5) + .lastStage(null) + .toCompletableFuture() + .get(); + + assertEquals((Integer)5, v); + } + + @Test + public void lastEmpty() throws Exception { + Integer v = Flowable.<Integer>empty() + .lastStage(2) + .toCompletableFuture() + .get(); + + assertEquals((Integer)2, v); + } + + @Test + public void lastCompletableFutureCancels() throws Exception { + PublishProcessor<Integer> source = PublishProcessor.create(); + + CompletableFuture<Integer> cf = source + .lastStage(null) + .toCompletableFuture(); + + assertTrue(source.hasSubscribers()); + + cf.cancel(true); + + assertTrue(cf.isCancelled()); + + assertFalse(source.hasSubscribers()); + } + + @Test + public void lastCompletableManualCompleteCancels() throws Exception { + PublishProcessor<Integer> source = PublishProcessor.create(); + + CompletableFuture<Integer> cf = source + .lastStage(null) + .toCompletableFuture(); + + assertTrue(source.hasSubscribers()); + + cf.complete(1); + + assertTrue(cf.isDone()); + assertFalse(cf.isCompletedExceptionally()); + assertFalse(cf.isCancelled()); + + assertFalse(source.hasSubscribers()); + + assertEquals((Integer)1, cf.get()); + } + + @Test + public void lastCompletableManualCompleteExceptionallyCancels() throws Exception { + PublishProcessor<Integer> source = PublishProcessor.create(); + + CompletableFuture<Integer> cf = source + .lastStage(null) + .toCompletableFuture(); + + assertTrue(source.hasSubscribers()); + + cf.completeExceptionally(new TestException()); + + assertTrue(cf.isDone()); + assertTrue(cf.isCompletedExceptionally()); + assertFalse(cf.isCancelled()); + + assertFalse(source.hasSubscribers()); + + TestHelper.assertError(cf, TestException.class); + } + + @Test + public void lastError() throws Exception { + CompletableFuture<Integer> cf = Flowable.<Integer>error(new TestException()) + .lastStage(null) + .toCompletableFuture(); + + assertTrue(cf.isDone()); + assertTrue(cf.isCompletedExceptionally()); + assertFalse(cf.isCancelled()); + + TestHelper.assertError(cf, TestException.class); + } + + @Test + public void lastSourceIgnoresCancel() throws Throwable { + TestHelper.withErrorTracking(errors -> { + Integer v = new Flowable<Integer>() { + @Override + protected void subscribeActual(Subscriber<? super Integer> s) { + s.onSubscribe(new BooleanSubscription()); + s.onNext(1); + s.onComplete(); + s.onError(new TestException()); + s.onComplete(); + } + } + .lastStage(null) + .toCompletableFuture() + .get(); + + assertEquals((Integer)1, v); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + }); + } + + @Test + public void lastDoubleOnSubscribe() throws Throwable { + TestHelper.withErrorTracking(errors -> { + Integer v = new Flowable<Integer>() { + @Override + protected void subscribeActual(Subscriber<? super Integer> s) { + s.onSubscribe(new BooleanSubscription()); + s.onSubscribe(new BooleanSubscription()); + s.onNext(1); + s.onComplete(); + } + } + .lastStage(null) + .toCompletableFuture() + .get(); + + assertEquals((Integer)1, v); + + TestHelper.assertError(errors, 0, ProtocolViolationException.class); + }); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/jdk8/FlowableStageSubscriberOrErrorTest.java b/src/test/java/io/reactivex/rxjava3/internal/jdk8/FlowableStageSubscriberOrErrorTest.java new file mode 100644 index 0000000000..763a1e0444 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/jdk8/FlowableStageSubscriberOrErrorTest.java @@ -0,0 +1,470 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.jdk8; + +import static org.junit.Assert.*; + +import java.util.NoSuchElementException; +import java.util.concurrent.*; + +import org.junit.Test; +import org.reactivestreams.Subscriber; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.internal.subscriptions.BooleanSubscription; +import io.reactivex.rxjava3.processors.*; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class FlowableStageSubscriberOrErrorTest extends RxJavaTest { + + @Test + public void firstJust() throws Exception { + Integer v = Flowable.just(1) + .firstOrErrorStage() + .toCompletableFuture() + .get(); + + assertEquals((Integer)1, v); + } + + @Test + public void firstEmpty() throws Exception { + TestHelper.assertError( + Flowable.<Integer>empty() + .firstOrErrorStage() + .toCompletableFuture(), NoSuchElementException.class); + } + + @Test + public void firstCancels() throws Exception { + BehaviorProcessor<Integer> source = BehaviorProcessor.createDefault(1); + + Integer v = source + .firstOrErrorStage() + .toCompletableFuture() + .get(); + + assertEquals((Integer)1, v); + + assertFalse(source.hasSubscribers()); + } + + @Test + public void firstCompletableFutureCancels() throws Exception { + PublishProcessor<Integer> source = PublishProcessor.create(); + + CompletableFuture<Integer> cf = source + .firstOrErrorStage() + .toCompletableFuture(); + + assertTrue(source.hasSubscribers()); + + cf.cancel(true); + + assertTrue(cf.isCancelled()); + + assertFalse(source.hasSubscribers()); + } + + @Test + public void firstCompletableManualCompleteCancels() throws Exception { + PublishProcessor<Integer> source = PublishProcessor.create(); + + CompletableFuture<Integer> cf = source + .firstOrErrorStage() + .toCompletableFuture(); + + assertTrue(source.hasSubscribers()); + + cf.complete(1); + + assertTrue(cf.isDone()); + assertFalse(cf.isCompletedExceptionally()); + assertFalse(cf.isCancelled()); + + assertFalse(source.hasSubscribers()); + + assertEquals((Integer)1, cf.get()); + } + + @Test + public void firstCompletableManualCompleteExceptionallyCancels() throws Exception { + PublishProcessor<Integer> source = PublishProcessor.create(); + + CompletableFuture<Integer> cf = source + .firstOrErrorStage() + .toCompletableFuture(); + + assertTrue(source.hasSubscribers()); + + cf.completeExceptionally(new TestException()); + + assertTrue(cf.isDone()); + assertTrue(cf.isCompletedExceptionally()); + assertFalse(cf.isCancelled()); + + assertFalse(source.hasSubscribers()); + + TestHelper.assertError(cf, TestException.class); + } + + @Test + public void firstError() throws Exception { + CompletableFuture<Integer> cf = Flowable.<Integer>error(new TestException()) + .firstOrErrorStage() + .toCompletableFuture(); + + assertTrue(cf.isDone()); + assertTrue(cf.isCompletedExceptionally()); + assertFalse(cf.isCancelled()); + + TestHelper.assertError(cf, TestException.class); + } + + @Test + public void firstSourceIgnoresCancel() throws Throwable { + TestHelper.withErrorTracking(errors -> { + Integer v = new Flowable<Integer>() { + @Override + protected void subscribeActual(Subscriber<? super Integer> s) { + s.onSubscribe(new BooleanSubscription()); + s.onNext(1); + s.onError(new TestException()); + s.onComplete(); + } + } + .firstOrErrorStage() + .toCompletableFuture() + .get(); + + assertEquals((Integer)1, v); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + }); + } + + @Test + public void firstDoubleOnSubscribe() throws Throwable { + TestHelper.withErrorTracking(errors -> { + Integer v = new Flowable<Integer>() { + @Override + protected void subscribeActual(Subscriber<? super Integer> s) { + s.onSubscribe(new BooleanSubscription()); + s.onSubscribe(new BooleanSubscription()); + s.onNext(1); + } + } + .firstOrErrorStage() + .toCompletableFuture() + .get(); + + assertEquals((Integer)1, v); + + TestHelper.assertError(errors, 0, ProtocolViolationException.class); + }); + } + + @Test + public void singleJust() throws Exception { + Integer v = Flowable.just(1) + .singleOrErrorStage() + .toCompletableFuture() + .get(); + + assertEquals((Integer)1, v); + } + + @Test + public void singleEmpty() throws Exception { + TestHelper.assertError( + Flowable.<Integer>empty() + .singleOrErrorStage() + .toCompletableFuture(), NoSuchElementException.class); + } + + @Test + public void singleTooManyCancels() throws Exception { + ReplayProcessor<Integer> source = ReplayProcessor.create(); + source.onNext(1); + source.onNext(2); + + TestHelper.assertError(source + .singleOrErrorStage() + .toCompletableFuture(), IllegalArgumentException.class); + + assertFalse(source.hasSubscribers()); + } + + @Test + public void singleCompletableFutureCancels() throws Exception { + PublishProcessor<Integer> source = PublishProcessor.create(); + + CompletableFuture<Integer> cf = source + .singleOrErrorStage() + .toCompletableFuture(); + + assertTrue(source.hasSubscribers()); + + cf.cancel(true); + + assertTrue(cf.isCancelled()); + + assertFalse(source.hasSubscribers()); + } + + @Test + public void singleCompletableManualCompleteCancels() throws Exception { + PublishProcessor<Integer> source = PublishProcessor.create(); + + CompletableFuture<Integer> cf = source + .singleOrErrorStage() + .toCompletableFuture(); + + assertTrue(source.hasSubscribers()); + + cf.complete(1); + + assertTrue(cf.isDone()); + assertFalse(cf.isCompletedExceptionally()); + assertFalse(cf.isCancelled()); + + assertFalse(source.hasSubscribers()); + + assertEquals((Integer)1, cf.get()); + } + + @Test + public void singleCompletableManualCompleteExceptionallyCancels() throws Exception { + PublishProcessor<Integer> source = PublishProcessor.create(); + + CompletableFuture<Integer> cf = source + .singleOrErrorStage() + .toCompletableFuture(); + + assertTrue(source.hasSubscribers()); + + cf.completeExceptionally(new TestException()); + + assertTrue(cf.isDone()); + assertTrue(cf.isCompletedExceptionally()); + assertFalse(cf.isCancelled()); + + assertFalse(source.hasSubscribers()); + + TestHelper.assertError(cf, TestException.class); + } + + @Test + public void singleError() throws Exception { + CompletableFuture<Integer> cf = Flowable.<Integer>error(new TestException()) + .singleOrErrorStage() + .toCompletableFuture(); + + assertTrue(cf.isDone()); + assertTrue(cf.isCompletedExceptionally()); + assertFalse(cf.isCancelled()); + + TestHelper.assertError(cf, TestException.class); + } + + @Test + public void singleSourceIgnoresCancel() throws Throwable { + TestHelper.withErrorTracking(errors -> { + Integer v = new Flowable<Integer>() { + @Override + protected void subscribeActual(Subscriber<? super Integer> s) { + s.onSubscribe(new BooleanSubscription()); + s.onNext(1); + s.onComplete(); + s.onError(new TestException()); + s.onComplete(); + } + } + .singleOrErrorStage() + .toCompletableFuture() + .get(); + + assertEquals((Integer)1, v); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + }); + } + + @Test + public void singleDoubleOnSubscribe() throws Throwable { + TestHelper.withErrorTracking(errors -> { + Integer v = new Flowable<Integer>() { + @Override + protected void subscribeActual(Subscriber<? super Integer> s) { + s.onSubscribe(new BooleanSubscription()); + s.onSubscribe(new BooleanSubscription()); + s.onNext(1); + s.onComplete(); + } + } + .singleOrErrorStage() + .toCompletableFuture() + .get(); + + assertEquals((Integer)1, v); + + TestHelper.assertError(errors, 0, ProtocolViolationException.class); + }); + } + + @Test + public void lastJust() throws Exception { + Integer v = Flowable.just(1) + .lastOrErrorStage() + .toCompletableFuture() + .get(); + + assertEquals((Integer)1, v); + } + + @Test + public void lastRange() throws Exception { + Integer v = Flowable.range(1, 5) + .lastOrErrorStage() + .toCompletableFuture() + .get(); + + assertEquals((Integer)5, v); + } + + @Test + public void lastEmpty() throws Exception { + TestHelper.assertError(Flowable.<Integer>empty() + .lastOrErrorStage() + .toCompletableFuture(), NoSuchElementException.class); + } + + @Test + public void lastCompletableFutureCancels() throws Exception { + PublishProcessor<Integer> source = PublishProcessor.create(); + + CompletableFuture<Integer> cf = source + .lastOrErrorStage() + .toCompletableFuture(); + + assertTrue(source.hasSubscribers()); + + cf.cancel(true); + + assertTrue(cf.isCancelled()); + + assertFalse(source.hasSubscribers()); + } + + @Test + public void lastCompletableManualCompleteCancels() throws Exception { + PublishProcessor<Integer> source = PublishProcessor.create(); + + CompletableFuture<Integer> cf = source + .lastOrErrorStage() + .toCompletableFuture(); + + assertTrue(source.hasSubscribers()); + + cf.complete(1); + + assertTrue(cf.isDone()); + assertFalse(cf.isCompletedExceptionally()); + assertFalse(cf.isCancelled()); + + assertFalse(source.hasSubscribers()); + + assertEquals((Integer)1, cf.get()); + } + + @Test + public void lastCompletableManualCompleteExceptionallyCancels() throws Exception { + PublishProcessor<Integer> source = PublishProcessor.create(); + + CompletableFuture<Integer> cf = source + .lastOrErrorStage() + .toCompletableFuture(); + + assertTrue(source.hasSubscribers()); + + cf.completeExceptionally(new TestException()); + + assertTrue(cf.isDone()); + assertTrue(cf.isCompletedExceptionally()); + assertFalse(cf.isCancelled()); + + assertFalse(source.hasSubscribers()); + + TestHelper.assertError(cf, TestException.class); + } + + @Test + public void lastError() throws Exception { + CompletableFuture<Integer> cf = Flowable.<Integer>error(new TestException()) + .lastOrErrorStage() + .toCompletableFuture(); + + assertTrue(cf.isDone()); + assertTrue(cf.isCompletedExceptionally()); + assertFalse(cf.isCancelled()); + + TestHelper.assertError(cf, TestException.class); + } + + @Test + public void lastSourceIgnoresCancel() throws Throwable { + TestHelper.withErrorTracking(errors -> { + Integer v = new Flowable<Integer>() { + @Override + protected void subscribeActual(Subscriber<? super Integer> s) { + s.onSubscribe(new BooleanSubscription()); + s.onNext(1); + s.onComplete(); + s.onError(new TestException()); + s.onComplete(); + } + } + .lastOrErrorStage() + .toCompletableFuture() + .get(); + + assertEquals((Integer)1, v); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + }); + } + + @Test + public void lastDoubleOnSubscribe() throws Throwable { + TestHelper.withErrorTracking(errors -> { + Integer v = new Flowable<Integer>() { + @Override + protected void subscribeActual(Subscriber<? super Integer> s) { + s.onSubscribe(new BooleanSubscription()); + s.onSubscribe(new BooleanSubscription()); + s.onNext(1); + s.onComplete(); + } + } + .lastOrErrorStage() + .toCompletableFuture() + .get(); + + assertEquals((Integer)1, v); + + TestHelper.assertError(errors, 0, ProtocolViolationException.class); + }); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/jdk8/FromCompletionStageTckTest.java b/src/test/java/io/reactivex/rxjava3/internal/jdk8/FromCompletionStageTckTest.java new file mode 100644 index 0000000000..6d2e9d2155 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/jdk8/FromCompletionStageTckTest.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.jdk8; + +import java.util.concurrent.*; + +import org.reactivestreams.Publisher; +import org.testng.annotations.Test; + +import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.tck.BaseTck; + +@Test +public class FromCompletionStageTckTest extends BaseTck<Long> { + + @Override + public Publisher<Long> createPublisher(final long elements) { + return + Flowable.fromCompletionStage(CompletableFuture.completedFuture(1L)) + ; + } + + @Override + public Publisher<Long> createFailedPublisher() { + CompletableFuture<Long> cf = new CompletableFuture<>(); + cf.completeExceptionally(new TestException()); + return + Flowable.fromCompletionStage(cf) + ; + } + + @Override + public long maxElementsFromPublisher() { + return 1; + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/jdk8/FromOptional0TckTest.java b/src/test/java/io/reactivex/rxjava3/internal/jdk8/FromOptional0TckTest.java new file mode 100644 index 0000000000..ff30aa4b2d --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/jdk8/FromOptional0TckTest.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.jdk8; + +import java.util.Optional; + +import org.reactivestreams.Publisher; +import org.testng.annotations.Test; + +import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.tck.BaseTck; + +/** + * Test Optional.empty() wrapping. + * @see FromOptional1TckTest + */ +@Test +public class FromOptional0TckTest extends BaseTck<Long> { + + @Override + public Publisher<Long> createPublisher(final long elements) { + return + Flowable.fromOptional(Optional.empty()) + ; + } + + @Override + public long maxElementsFromPublisher() { + return 0; + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/jdk8/FromOptional1TckTest.java b/src/test/java/io/reactivex/rxjava3/internal/jdk8/FromOptional1TckTest.java new file mode 100644 index 0000000000..f6b56a582d --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/jdk8/FromOptional1TckTest.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.jdk8; + +import java.util.Optional; + +import org.reactivestreams.Publisher; +import org.testng.annotations.Test; + +import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.tck.BaseTck; + +/** + * Test Optional.of wrapping. + * @see FromOptional0TckTest + */ +@Test +public class FromOptional1TckTest extends BaseTck<Long> { + + @Override + public Publisher<Long> createPublisher(final long elements) { + return + Flowable.fromOptional(Optional.of(1L)) + ; + } + + @Override + public long maxElementsFromPublisher() { + return 1; + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/jdk8/FromStreamTckTest.java b/src/test/java/io/reactivex/rxjava3/internal/jdk8/FromStreamTckTest.java new file mode 100644 index 0000000000..a0995dad39 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/jdk8/FromStreamTckTest.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.jdk8; + +import java.util.stream.*; + +import org.reactivestreams.Publisher; +import org.testng.annotations.Test; + +import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.tck.BaseTck; + +/** + * Test Optional.of wrapping. + * @see FromOptional0TckTest + */ +@Test +public class FromStreamTckTest extends BaseTck<Integer> { + + @Override + public Publisher<Integer> createPublisher(final long elements) { + return + Flowable.fromStream(IntStream.rangeClosed(1, (int)elements).boxed()) + ; + } + + @Override + public Publisher<Integer> createFailedPublisher() { + Stream<Integer> stream = Stream.of(1); + stream.forEach(v -> { }); + return Flowable.fromStream(stream); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/jdk8/MapOptionalTckTest.java b/src/test/java/io/reactivex/rxjava3/internal/jdk8/MapOptionalTckTest.java new file mode 100644 index 0000000000..345159da4a --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/jdk8/MapOptionalTckTest.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.jdk8; + +import java.util.Optional; + +import org.reactivestreams.Publisher; +import org.testng.annotations.Test; + +import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.tck.BaseTck; + +@Test +public class MapOptionalTckTest extends BaseTck<Integer> { + + @Override + public Publisher<Integer> createPublisher(final long elements) { + return + Flowable.range(0, (int)(2 * elements)).mapOptional(v -> v % 2 == 0 ? Optional.of(v) : Optional.empty()) + ; + } + + @Override + public Publisher<Integer> createFailedPublisher() { + return Flowable.just(1).<Integer>mapOptional(v -> null).onBackpressureDrop(); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/jdk8/MaybeFlattenStreamAsFlowableTckTest.java b/src/test/java/io/reactivex/rxjava3/internal/jdk8/MaybeFlattenStreamAsFlowableTckTest.java new file mode 100644 index 0000000000..55e6778d3f --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/jdk8/MaybeFlattenStreamAsFlowableTckTest.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.jdk8; + +import java.util.stream.*; + +import org.reactivestreams.Publisher; +import org.testng.annotations.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.tck.BaseTck; + +@Test +public class MaybeFlattenStreamAsFlowableTckTest extends BaseTck<Integer> { + + @Override + public Publisher<Integer> createPublisher(final long elements) { + return + Maybe.just(1).flattenStreamAsFlowable(v -> IntStream.range(0, (int)elements).boxed()) + ; + } + + @Override + public Publisher<Integer> createFailedPublisher() { + Stream<Integer> stream = Stream.of(1); + stream.forEach(v -> { }); + return Maybe.just(1).flattenStreamAsFlowable(v -> stream); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/jdk8/MaybeFlattenStreamAsFlowableTest.java b/src/test/java/io/reactivex/rxjava3/internal/jdk8/MaybeFlattenStreamAsFlowableTest.java new file mode 100644 index 0000000000..d8377eac60 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/jdk8/MaybeFlattenStreamAsFlowableTest.java @@ -0,0 +1,454 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.jdk8; + +import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import java.util.Iterator; +import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.*; + +import org.junit.Test; +import org.reactivestreams.Subscription; + +import io.reactivex.rxjava3.annotations.NonNull; +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.subscriptions.BooleanSubscription; +import io.reactivex.rxjava3.operators.QueueFuseable; +import io.reactivex.rxjava3.operators.QueueSubscription; +import io.reactivex.rxjava3.subjects.MaybeSubject; +import io.reactivex.rxjava3.subscribers.TestSubscriber; +import io.reactivex.rxjava3.testsupport.*; + +public class MaybeFlattenStreamAsFlowableTest extends RxJavaTest { + + @Test + public void successJust() { + Maybe.just(1) + .flattenStreamAsFlowable(Stream::of) + .test() + .assertResult(1); + } + + @Test + public void successEmpty() { + Maybe.just(1) + .flattenStreamAsFlowable(v -> Stream.of()) + .test() + .assertResult(); + } + + @Test + public void successMany() { + Maybe.just(1) + .flattenStreamAsFlowable(v -> Stream.of(2, 3, 4, 5, 6)) + .test() + .assertResult(2, 3, 4, 5, 6); + } + + @Test + public void successManyTake() { + Maybe.just(1) + .flattenStreamAsFlowable(v -> Stream.of(2, 3, 4, 5, 6)) + .take(3) + .test() + .assertResult(2, 3, 4); + } + + @Test + public void empty() throws Throwable { + @SuppressWarnings("unchecked") + Function<? super Integer, Stream<? extends Integer>> f = mock(Function.class); + + Maybe.<Integer>empty() + .flattenStreamAsFlowable(f) + .test() + .assertResult(); + + verify(f, never()).apply(any()); + } + + @Test + public void error() throws Throwable { + @SuppressWarnings("unchecked") + Function<? super Integer, Stream<? extends Integer>> f = mock(Function.class); + + Maybe.<Integer>error(new TestException()) + .flattenStreamAsFlowable(f) + .test() + .assertFailure(TestException.class); + + verify(f, never()).apply(any()); + } + + @Test + public void mapperCrash() { + Maybe.just(1) + .flattenStreamAsFlowable(v -> { throw new TestException(); }) + .test() + .assertFailure(TestException.class); + } + + @Test + public void dispose() { + TestHelper.checkDisposed(Maybe.never().flattenStreamAsFlowable(Stream::of)); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeMaybeToFlowable(m -> m.flattenStreamAsFlowable(Stream::of)); + } + + @Test + public void badRequest() { + TestHelper.assertBadRequestReported(MaybeSubject.create().flattenStreamAsFlowable(Stream::of)); + } + + @Test + public void fusedEmpty() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(); + ts.setInitialFusionMode(QueueFuseable.ANY); + + Maybe.just(1) + .flattenStreamAsFlowable(v -> Stream.<Integer>of()) + .subscribe(ts); + + ts.assertFuseable() + .assertFusionMode(QueueFuseable.ASYNC) + .assertResult(); + } + + @Test + public void fusedJust() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(); + ts.setInitialFusionMode(QueueFuseable.ANY); + + Maybe.just(1) + .flattenStreamAsFlowable(v -> Stream.<Integer>of(v)) + .subscribe(ts); + + ts.assertFuseable() + .assertFusionMode(QueueFuseable.ASYNC) + .assertResult(1); + } + + @Test + public void fusedMany() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(); + ts.setInitialFusionMode(QueueFuseable.ANY); + + Maybe.just(1) + .flattenStreamAsFlowable(v -> Stream.<Integer>of(v, v + 1, v + 2)) + .subscribe(ts); + + ts.assertFuseable() + .assertFusionMode(QueueFuseable.ASYNC) + .assertResult(1, 2, 3); + } + + @Test + public void fusedManyRejected() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(); + ts.setInitialFusionMode(QueueFuseable.SYNC); + + Maybe.just(1) + .flattenStreamAsFlowable(v -> Stream.<Integer>of(v, v + 1, v + 2)) + .subscribe(ts); + + ts.assertFuseable() + .assertFusionMode(QueueFuseable.NONE) + .assertResult(1, 2, 3); + } + + @Test + public void manyBackpressured() { + Maybe.just(1) + .flattenStreamAsFlowable(v -> IntStream.rangeClosed(1, 5).boxed()) + .test(0L) + .assertEmpty() + .requestMore(2) + .assertValuesOnly(1, 2) + .requestMore(2) + .assertValuesOnly(1, 2, 3, 4) + .requestMore(1) + .assertResult(1, 2, 3, 4, 5); + } + + @Test + public void manyBackpressured2() { + Maybe.just(1) + .flattenStreamAsFlowable(v -> IntStream.rangeClosed(1, 5).boxed()) + .rebatchRequests(1) + .test(0L) + .assertEmpty() + .requestMore(2) + .assertValuesOnly(1, 2) + .requestMore(2) + .assertValuesOnly(1, 2, 3, 4) + .requestMore(1) + .assertResult(1, 2, 3, 4, 5); + } + + @Test + public void fusedStreamAvailableLater() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(); + ts.setInitialFusionMode(QueueFuseable.ANY); + + MaybeSubject<Integer> ms = MaybeSubject.create(); + + ms + .flattenStreamAsFlowable(v -> Stream.<Integer>of(v, v + 1, v + 2)) + .subscribe(ts); + + ts.assertFuseable() + .assertFusionMode(QueueFuseable.ASYNC) + .assertEmpty(); + + ms.onSuccess(1); + + ts + .assertResult(1, 2, 3); + } + + @Test + public void fused() throws Throwable { + AtomicReference<QueueSubscription<Integer>> qsr = new AtomicReference<>(); + + MaybeSubject<Integer> ms = MaybeSubject.create(); + + ms + .flattenStreamAsFlowable(Stream::of) + .subscribe(new FlowableSubscriber<Integer>() { + + @Override + public void onNext(Integer t) { + } + + @Override + public void onError(Throwable t) { + } + + @Override + public void onComplete() { + } + + @Override + @SuppressWarnings("unchecked") + public void onSubscribe(@NonNull Subscription s) { + qsr.set((QueueSubscription<Integer>)s); + } + }); + + QueueSubscription<Integer> qs = qsr.get(); + + assertEquals(QueueFuseable.ASYNC, qs.requestFusion(QueueFuseable.ASYNC)); + + assertTrue(qs.isEmpty()); + assertNull(qs.poll()); + + ms.onSuccess(1); + + assertFalse(qs.isEmpty()); + assertEquals(1, qs.poll().intValue()); + + assertTrue(qs.isEmpty()); + assertNull(qs.poll()); + + qs.cancel(); + + assertTrue(qs.isEmpty()); + assertNull(qs.poll()); + } + + @Test + public void requestOneByOne() { + TestSubscriber<Integer> ts = new TestSubscriber<>(); + + Maybe.just(1) + .flattenStreamAsFlowable(v -> Stream.of(1, 2, 3, 4, 5)) + .subscribe(new FlowableSubscriber<Integer>() { + + Subscription upstream; + + @Override + public void onSubscribe(@NonNull Subscription s) { + ts.onSubscribe(new BooleanSubscription()); + upstream = s; + s.request(1); + } + + @Override + public void onNext(Integer t) { + ts.onNext(t); + upstream.request(1); + } + + @Override + public void onError(Throwable t) { + ts.onError(t); + } + + @Override + public void onComplete() { + ts.onComplete(); + } + }); + + ts.assertResult(1, 2, 3, 4, 5); + } + + @Test + public void streamCloseCrash() throws Throwable { + TestHelper.withErrorTracking(errors -> { + Maybe.just(1) + .flattenStreamAsFlowable(v -> Stream.of(v).onClose(() -> { throw new TestException(); })) + .test() + .assertResult(1); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + }); + } + + @Test + public void hasNextThrowsInDrain() { + @SuppressWarnings("unchecked") + Stream<Integer> stream = mock(Stream.class); + when(stream.iterator()).thenReturn(new Iterator<Integer>() { + + int count; + + @Override + public boolean hasNext() { + if (count++ > 0) { + throw new TestException(); + } + return true; + } + + @Override + public Integer next() { + return 1; + } + }); + + Maybe.just(1) + .flattenStreamAsFlowable(v -> stream) + .test() + .assertFailure(TestException.class, 1); + } + + @Test + public void nextThrowsInDrain() { + @SuppressWarnings("unchecked") + Stream<Integer> stream = mock(Stream.class); + when(stream.iterator()).thenReturn(new Iterator<Integer>() { + + @Override + public boolean hasNext() { + return true; + } + + @Override + public Integer next() { + throw new TestException(); + } + }); + + Maybe.just(1) + .flattenStreamAsFlowable(v -> stream) + .test() + .assertFailure(TestException.class); + } + + @Test + public void cancelAfterHasNextInDrain() { + @SuppressWarnings("unchecked") + Stream<Integer> stream = mock(Stream.class); + + TestSubscriber<Integer> ts = new TestSubscriber<>(); + + when(stream.iterator()).thenReturn(new Iterator<Integer>() { + + int count; + + @Override + public boolean hasNext() { + if (count++ > 0) { + ts.cancel(); + } + return true; + } + + @Override + public Integer next() { + return 1; + } + }); + + Maybe.just(1) + .flattenStreamAsFlowable(v -> stream) + .subscribeWith(ts) + .assertValuesOnly(1); + } + + @Test + public void cancelAfterNextInDrain() { + @SuppressWarnings("unchecked") + Stream<Integer> stream = mock(Stream.class); + + TestSubscriber<Integer> ts = new TestSubscriber<>(); + + when(stream.iterator()).thenReturn(new Iterator<Integer>() { + + @Override + public boolean hasNext() { + return true; + } + + @Override + public Integer next() { + ts.cancel(); + return 1; + } + }); + + Maybe.just(1) + .flattenStreamAsFlowable(v -> stream) + .subscribeWith(ts) + .assertEmpty(); + } + + @Test + public void requestSuccessRace() { + for (int i = 0; i < TestHelper.RACE_LONG_LOOPS; i++) { + MaybeSubject<Integer> ms = MaybeSubject.create(); + + TestSubscriber<Integer> ts = new TestSubscriber<>(0L); + + ms.flattenStreamAsFlowable(Stream::of) + .subscribe(ts); + + Runnable r1 = () -> ms.onSuccess(1); + Runnable r2 = () -> ts.request(1); + + TestHelper.race(r1, r2); + + ts.assertResult(1); + } + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/jdk8/MaybeFlattenStreamAsObservableTest.java b/src/test/java/io/reactivex/rxjava3/internal/jdk8/MaybeFlattenStreamAsObservableTest.java new file mode 100644 index 0000000000..145112987d --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/jdk8/MaybeFlattenStreamAsObservableTest.java @@ -0,0 +1,432 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.jdk8; + +import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import java.util.Iterator; +import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Stream; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.operators.QueueDisposable; +import io.reactivex.rxjava3.operators.QueueFuseable; +import io.reactivex.rxjava3.subjects.MaybeSubject; +import io.reactivex.rxjava3.testsupport.*; + +public class MaybeFlattenStreamAsObservableTest extends RxJavaTest { + + @Test + public void successJust() { + Maybe.just(1) + .flattenStreamAsObservable(Stream::of) + .test() + .assertResult(1); + } + + @Test + public void successEmpty() { + Maybe.just(1) + .flattenStreamAsObservable(v -> Stream.of()) + .test() + .assertResult(); + } + + @Test + public void successMany() { + Maybe.just(1) + .flattenStreamAsObservable(v -> Stream.of(2, 3, 4, 5, 6)) + .test() + .assertResult(2, 3, 4, 5, 6); + } + + @Test + public void successManyTake() { + Maybe.just(1) + .flattenStreamAsObservable(v -> Stream.of(2, 3, 4, 5, 6)) + .take(3) + .test() + .assertResult(2, 3, 4); + } + + @Test + public void empty() throws Throwable { + @SuppressWarnings("unchecked") + Function<? super Integer, Stream<? extends Integer>> f = mock(Function.class); + + Maybe.<Integer>empty() + .flattenStreamAsObservable(f) + .test() + .assertResult(); + + verify(f, never()).apply(any()); + } + + @Test + public void error() throws Throwable { + @SuppressWarnings("unchecked") + Function<? super Integer, Stream<? extends Integer>> f = mock(Function.class); + + Maybe.<Integer>error(new TestException()) + .flattenStreamAsObservable(f) + .test() + .assertFailure(TestException.class); + + verify(f, never()).apply(any()); + } + + @Test + public void mapperCrash() { + Maybe.just(1) + .flattenStreamAsObservable(v -> { throw new TestException(); }) + .test() + .assertFailure(TestException.class); + } + + @Test + public void dispose() { + TestHelper.checkDisposed(Maybe.never().flattenStreamAsObservable(Stream::of)); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeMaybeToObservable(m -> m.flattenStreamAsObservable(Stream::of)); + } + + @Test + public void fusedEmpty() { + TestObserverEx<Integer> to = new TestObserverEx<>(); + to.setInitialFusionMode(QueueFuseable.ANY); + + Maybe.just(1) + .flattenStreamAsObservable(v -> Stream.<Integer>of()) + .subscribe(to); + + to.assertFuseable() + .assertFusionMode(QueueFuseable.ASYNC) + .assertResult(); + } + + @Test + public void fusedJust() { + TestObserverEx<Integer> to = new TestObserverEx<>(); + to.setInitialFusionMode(QueueFuseable.ANY); + + Maybe.just(1) + .flattenStreamAsObservable(v -> Stream.<Integer>of(v)) + .subscribe(to); + + to.assertFuseable() + .assertFusionMode(QueueFuseable.ASYNC) + .assertResult(1); + } + + @Test + public void fusedMany() { + TestObserverEx<Integer> to = new TestObserverEx<>(); + to.setInitialFusionMode(QueueFuseable.ANY); + + Maybe.just(1) + .flattenStreamAsObservable(v -> Stream.<Integer>of(v, v + 1, v + 2)) + .subscribe(to); + + to.assertFuseable() + .assertFusionMode(QueueFuseable.ASYNC) + .assertResult(1, 2, 3); + } + + @Test + public void fusedManyRejected() { + TestObserverEx<Integer> to = new TestObserverEx<>(); + to.setInitialFusionMode(QueueFuseable.SYNC); + + Maybe.just(1) + .flattenStreamAsObservable(v -> Stream.<Integer>of(v, v + 1, v + 2)) + .subscribe(to); + + to.assertFuseable() + .assertFusionMode(QueueFuseable.NONE) + .assertResult(1, 2, 3); + } + + @Test + public void fusedStreamAvailableLater() { + TestObserverEx<Integer> to = new TestObserverEx<>(); + to.setInitialFusionMode(QueueFuseable.ANY); + + MaybeSubject<Integer> ms = MaybeSubject.create(); + + ms + .flattenStreamAsObservable(v -> Stream.<Integer>of(v, v + 1, v + 2)) + .subscribe(to); + + to.assertFuseable() + .assertFusionMode(QueueFuseable.ASYNC) + .assertEmpty(); + + ms.onSuccess(1); + + to + .assertResult(1, 2, 3); + } + + @Test + public void fused() throws Throwable { + AtomicReference<QueueDisposable<Integer>> qdr = new AtomicReference<>(); + + MaybeSubject<Integer> ms = MaybeSubject.create(); + + ms + .flattenStreamAsObservable(Stream::of) + .subscribe(new Observer<Integer>() { + + @Override + public void onNext(Integer t) { + } + + @Override + public void onError(Throwable t) { + } + + @Override + public void onComplete() { + } + + @Override + @SuppressWarnings("unchecked") + public void onSubscribe(Disposable d) { + qdr.set((QueueDisposable<Integer>)d); + } + }); + + QueueDisposable<Integer> qd = qdr.get(); + + assertEquals(QueueFuseable.ASYNC, qd.requestFusion(QueueFuseable.ASYNC)); + + assertTrue(qd.isEmpty()); + assertNull(qd.poll()); + + ms.onSuccess(1); + + assertFalse(qd.isEmpty()); + assertEquals(1, qd.poll().intValue()); + + assertTrue(qd.isEmpty()); + assertNull(qd.poll()); + + qd.dispose(); + + assertTrue(qd.isEmpty()); + assertNull(qd.poll()); + } + + @Test + public void fused2() throws Throwable { + AtomicReference<QueueDisposable<Integer>> qdr = new AtomicReference<>(); + + MaybeSubject<Integer> ms = MaybeSubject.create(); + + ms + .flattenStreamAsObservable(v -> Stream.of(v, v + 1)) + .subscribe(new Observer<Integer>() { + + @Override + public void onNext(Integer t) { + } + + @Override + public void onError(Throwable t) { + } + + @Override + public void onComplete() { + } + + @Override + @SuppressWarnings("unchecked") + public void onSubscribe(Disposable d) { + qdr.set((QueueDisposable<Integer>)d); + } + }); + + QueueDisposable<Integer> qd = qdr.get(); + + assertEquals(QueueFuseable.ASYNC, qd.requestFusion(QueueFuseable.ASYNC)); + + assertTrue(qd.isEmpty()); + assertNull(qd.poll()); + + ms.onSuccess(1); + + assertFalse(qd.isEmpty()); + assertEquals(1, qd.poll().intValue()); + + assertFalse(qd.isEmpty()); + assertEquals(2, qd.poll().intValue()); + + assertTrue(qd.isEmpty()); + assertNull(qd.poll()); + + qd.dispose(); + + assertTrue(qd.isEmpty()); + assertNull(qd.poll()); + } + + @Test + public void streamCloseCrash() throws Throwable { + TestHelper.withErrorTracking(errors -> { + Maybe.just(1) + .flattenStreamAsObservable(v -> Stream.of(v).onClose(() -> { throw new TestException(); })) + .test() + .assertResult(1); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + }); + } + + @Test + public void hasNextThrowsInDrain() { + @SuppressWarnings("unchecked") + Stream<Integer> stream = mock(Stream.class); + when(stream.iterator()).thenReturn(new Iterator<Integer>() { + + int count; + + @Override + public boolean hasNext() { + if (count++ > 0) { + throw new TestException(); + } + return true; + } + + @Override + public Integer next() { + return 1; + } + }); + + Maybe.just(1) + .flattenStreamAsObservable(v -> stream) + .test() + .assertFailure(TestException.class, 1); + } + + @Test + public void nextThrowsInDrain() { + @SuppressWarnings("unchecked") + Stream<Integer> stream = mock(Stream.class); + when(stream.iterator()).thenReturn(new Iterator<Integer>() { + + @Override + public boolean hasNext() { + return true; + } + + @Override + public Integer next() { + throw new TestException(); + } + }); + + Maybe.just(1) + .flattenStreamAsObservable(v -> stream) + .test() + .assertFailure(TestException.class); + } + + @Test + public void cancelAfterHasNextInDrain() { + @SuppressWarnings("unchecked") + Stream<Integer> stream = mock(Stream.class); + + TestObserver<Integer> to = new TestObserver<>(); + + when(stream.iterator()).thenReturn(new Iterator<Integer>() { + + int count; + + @Override + public boolean hasNext() { + if (count++ > 0) { + to.dispose(); + } + return true; + } + + @Override + public Integer next() { + return 1; + } + }); + + Maybe.just(1) + .flattenStreamAsObservable(v -> stream) + .subscribeWith(to) + .assertValuesOnly(1); + } + + @Test + public void cancelAfterNextInDrain() { + @SuppressWarnings("unchecked") + Stream<Integer> stream = mock(Stream.class); + + TestObserver<Integer> to = new TestObserver<>(); + + when(stream.iterator()).thenReturn(new Iterator<Integer>() { + + @Override + public boolean hasNext() { + return true; + } + + @Override + public Integer next() { + to.dispose(); + return 1; + } + }); + + Maybe.just(1) + .flattenStreamAsObservable(v -> stream) + .subscribeWith(to) + .assertEmpty(); + } + + @Test + public void cancelSuccessRace() { + for (int i = 0; i < TestHelper.RACE_LONG_LOOPS; i++) { + MaybeSubject<Integer> ms = MaybeSubject.create(); + + TestObserver<Integer> to = new TestObserver<>(); + + ms.flattenStreamAsObservable(Stream::of) + .subscribe(to); + + Runnable r1 = () -> ms.onSuccess(1); + Runnable r2 = () -> to.dispose(); + + TestHelper.race(r1, r2); + } + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/jdk8/MaybeFromCompletionStageTest.java b/src/test/java/io/reactivex/rxjava3/internal/jdk8/MaybeFromCompletionStageTest.java new file mode 100644 index 0000000000..552a4afc10 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/jdk8/MaybeFromCompletionStageTest.java @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.jdk8; + +import java.util.concurrent.CompletableFuture; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class MaybeFromCompletionStageTest extends RxJavaTest { + + @Test + public void syncSuccess() { + Maybe.fromCompletionStage(CompletableFuture.completedFuture(1)) + .test() + .assertResult(1); + } + + @Test + public void syncFailure() { + CompletableFuture<Integer> cf = new CompletableFuture<>(); + cf.completeExceptionally(new TestException()); + + Maybe.fromCompletionStage(cf) + .test() + .assertFailure(TestException.class); + } + + @Test + public void syncNull() { + Maybe.fromCompletionStage(CompletableFuture.<Integer>completedFuture(null)) + .test() + .assertResult(); + } + + @Test + public void dispose() { + CompletableFuture<Integer> cf = new CompletableFuture<>(); + + TestObserver<Integer> to = Maybe.fromCompletionStage(cf) + .test(); + + to.assertEmpty(); + + to.dispose(); + + cf.complete(1); + + to.assertEmpty(); + } + + @Test + public void dispose2() { + TestHelper.checkDisposed(Maybe.fromCompletionStage(new CompletableFuture<>())); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/jdk8/MaybeFromOptionalTest.java b/src/test/java/io/reactivex/rxjava3/internal/jdk8/MaybeFromOptionalTest.java new file mode 100644 index 0000000000..8af5faf5d7 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/jdk8/MaybeFromOptionalTest.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.jdk8; + +import java.util.Optional; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; + +public class MaybeFromOptionalTest extends RxJavaTest { + + @Test + public void hasValue() { + Maybe.fromOptional(Optional.of(1)) + .test() + .assertResult(1); + } + + @Test + public void empty() { + Maybe.fromOptional(Optional.empty()) + .test() + .assertResult(); + } + +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/jdk8/MaybeMapOptionalTest.java b/src/test/java/io/reactivex/rxjava3/internal/jdk8/MaybeMapOptionalTest.java new file mode 100644 index 0000000000..a8adbcb6f8 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/jdk8/MaybeMapOptionalTest.java @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.jdk8; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import java.util.Optional; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class MaybeMapOptionalTest extends RxJavaTest { + + @Test + public void successSuccess() { + Maybe.just(1) + .mapOptional(Optional::of) + .test() + .assertResult(1); + } + + @Test + public void successEmpty() { + Maybe.just(1) + .mapOptional(v -> Optional.empty()) + .test() + .assertResult(); + } + + @Test + public void empty() throws Throwable { + @SuppressWarnings("unchecked") + Function<? super Integer, Optional<? extends Integer>> f = mock(Function.class); + + Maybe.<Integer>empty() + .mapOptional(f) + .test() + .assertResult(); + + verify(f, never()).apply(any()); + } + + @Test + public void error() throws Throwable { + @SuppressWarnings("unchecked") + Function<? super Integer, Optional<? extends Integer>> f = mock(Function.class); + + Maybe.<Integer>error(new TestException()) + .mapOptional(f) + .test() + .assertFailure(TestException.class); + + verify(f, never()).apply(any()); + } + + @Test + public void mapperCrash() { + Maybe.just(1) + .mapOptional(v -> { throw new TestException(); }) + .test() + .assertFailure(TestException.class); + } + + @Test + public void dispose() { + TestHelper.checkDisposed(Maybe.never().mapOptional(Optional::of)); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeMaybe(m -> m.mapOptional(Optional::of)); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/jdk8/MaybeToCompletionStageTest.java b/src/test/java/io/reactivex/rxjava3/internal/jdk8/MaybeToCompletionStageTest.java new file mode 100644 index 0000000000..12e4ca5f1e --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/jdk8/MaybeToCompletionStageTest.java @@ -0,0 +1,174 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.jdk8; + +import static org.junit.Assert.*; + +import java.util.NoSuchElementException; +import java.util.concurrent.CompletableFuture; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.subjects.MaybeSubject; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class MaybeToCompletionStageTest extends RxJavaTest { + + @Test + public void just() throws Exception { + Integer v = Maybe.just(1) + .toCompletionStage(null) + .toCompletableFuture() + .get(); + + assertEquals((Integer)1, v); + } + + @Test + public void empty() throws Exception { + Integer v = Maybe.<Integer>empty() + .toCompletionStage(2) + .toCompletableFuture() + .get(); + + assertEquals((Integer)2, v); + } + + @Test + public void emptyError() throws Exception { + CompletableFuture<Integer> cf = Maybe.<Integer>empty() + .toCompletionStage() + .toCompletableFuture(); + + TestHelper.assertError(cf, NoSuchElementException.class); + } + + @Test + public void completableFutureCancels() throws Exception { + MaybeSubject<Integer> source = MaybeSubject.create(); + + CompletableFuture<Integer> cf = source + .toCompletionStage(null) + .toCompletableFuture(); + + assertTrue(source.hasObservers()); + + cf.cancel(true); + + assertTrue(cf.isCancelled()); + + assertFalse(source.hasObservers()); + } + + @Test + public void completableManualCompleteCancels() throws Exception { + MaybeSubject<Integer> source = MaybeSubject.create(); + + CompletableFuture<Integer> cf = source + .toCompletionStage(null) + .toCompletableFuture(); + + assertTrue(source.hasObservers()); + + cf.complete(1); + + assertTrue(cf.isDone()); + assertFalse(cf.isCompletedExceptionally()); + assertFalse(cf.isCancelled()); + + assertFalse(source.hasObservers()); + + assertEquals((Integer)1, cf.get()); + } + + @Test + public void completableManualCompleteExceptionallyCancels() throws Exception { + MaybeSubject<Integer> source = MaybeSubject.create(); + + CompletableFuture<Integer> cf = source + .toCompletionStage(null) + .toCompletableFuture(); + + assertTrue(source.hasObservers()); + + cf.completeExceptionally(new TestException()); + + assertTrue(cf.isDone()); + assertTrue(cf.isCompletedExceptionally()); + assertFalse(cf.isCancelled()); + + assertFalse(source.hasObservers()); + + TestHelper.assertError(cf, TestException.class); + } + + @Test + public void error() throws Exception { + CompletableFuture<Integer> cf = Maybe.<Integer>error(new TestException()) + .toCompletionStage(null) + .toCompletableFuture(); + + assertTrue(cf.isDone()); + assertTrue(cf.isCompletedExceptionally()); + assertFalse(cf.isCancelled()); + + TestHelper.assertError(cf, TestException.class); + } + + @Test + public void sourceIgnoresCancel() throws Throwable { + TestHelper.withErrorTracking(errors -> { + Integer v = new Maybe<Integer>() { + @Override + protected void subscribeActual(MaybeObserver<? super Integer> observer) { + observer.onSubscribe(Disposable.empty()); + observer.onSuccess(1); + observer.onError(new TestException()); + observer.onComplete(); + } + } + .toCompletionStage(null) + .toCompletableFuture() + .get(); + + assertEquals((Integer)1, v); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + }); + } + + @Test + public void doubleOnSubscribe() throws Throwable { + TestHelper.withErrorTracking(errors -> { + Integer v = new Maybe<Integer>() { + @Override + protected void subscribeActual(MaybeObserver<? super Integer> observer) { + observer.onSubscribe(Disposable.empty()); + observer.onSubscribe(Disposable.empty()); + observer.onSuccess(1); + } + } + .toCompletionStage(null) + .toCompletableFuture() + .get(); + + assertEquals((Integer)1, v); + + TestHelper.assertError(errors, 0, ProtocolViolationException.class); + }); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/jdk8/ObservableBlockingStreamTest.java b/src/test/java/io/reactivex/rxjava3/internal/jdk8/ObservableBlockingStreamTest.java new file mode 100644 index 0000000000..275b38a57b --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/jdk8/ObservableBlockingStreamTest.java @@ -0,0 +1,107 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.jdk8; + +import static org.junit.Assert.*; + +import java.util.List; +import java.util.stream.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.processors.UnicastProcessor; +import io.reactivex.rxjava3.schedulers.Schedulers; + +public class ObservableBlockingStreamTest extends RxJavaTest { + + @Test + public void empty() { + try (Stream<Integer> stream = Observable.<Integer>empty().blockingStream()) { + assertEquals(0, stream.toArray().length); + } + } + + @Test + public void just() { + try (Stream<Integer> stream = Observable.just(1).blockingStream()) { + assertArrayEquals(new Integer[] { 1 }, stream.toArray(Integer[]::new)); + } + } + + @Test + public void range() { + try (Stream<Integer> stream = Observable.range(1, 5).blockingStream()) { + assertArrayEquals(new Integer[] { 1, 2, 3, 4, 5 }, stream.toArray(Integer[]::new)); + } + } + + @Test + public void rangeBackpressured() { + try (Stream<Integer> stream = Observable.range(1, 5).blockingStream(1)) { + assertArrayEquals(new Integer[] { 1, 2, 3, 4, 5 }, stream.toArray(Integer[]::new)); + } + } + + @Test + public void rangeAsyncBackpressured() { + try (Stream<Integer> stream = Observable.range(1, 1000).subscribeOn(Schedulers.computation()).blockingStream()) { + List<Integer> list = stream.collect(Collectors.toList()); + + assertEquals(1000, list.size()); + for (int i = 1; i <= 1000; i++) { + assertEquals(i, list.get(i - 1).intValue()); + } + } + } + + @Test + public void rangeAsyncBackpressured1() { + try (Stream<Integer> stream = Observable.range(1, 1000).subscribeOn(Schedulers.computation()).blockingStream(1)) { + List<Integer> list = stream.collect(Collectors.toList()); + + assertEquals(1000, list.size()); + for (int i = 1; i <= 1000; i++) { + assertEquals(i, list.get(i - 1).intValue()); + } + } + } + + @Test + public void error() { + try (Stream<Integer> stream = Observable.<Integer>error(new TestException()).blockingStream()) { + stream.toArray(Integer[]::new); + fail("Should have thrown!"); + } catch (TestException expected) { + // expected + } + } + + @Test + public void close() { + UnicastProcessor<Integer> up = UnicastProcessor.create(); + up.onNext(1); + up.onNext(2); + up.onNext(3); + up.onNext(4); + up.onNext(5); + + try (Stream<Integer> stream = up.blockingStream()) { + assertArrayEquals(new Integer[] { 1, 2, 3 }, stream.limit(3).toArray(Integer[]::new)); + } + + assertFalse(up.hasSubscribers()); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/jdk8/ObservableCollectWithCollectorTest.java b/src/test/java/io/reactivex/rxjava3/internal/jdk8/ObservableCollectWithCollectorTest.java new file mode 100644 index 0000000000..f18f42a3d0 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/jdk8/ObservableCollectWithCollectorTest.java @@ -0,0 +1,444 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.jdk8; + +import static org.junit.Assert.assertFalse; + +import java.io.IOException; +import java.util.*; +import java.util.function.*; +import java.util.stream.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.core.Observer; +import io.reactivex.rxjava3.core.RxJavaTest; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.processors.*; +import io.reactivex.rxjava3.subjects.PublishSubject; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class ObservableCollectWithCollectorTest extends RxJavaTest { + + @Test + public void basic() { + Observable.range(1, 5) + .collect(Collectors.toList()) + .test() + .assertResult(Arrays.asList(1, 2, 3, 4, 5)); + } + + @Test + public void empty() { + Observable.empty() + .collect(Collectors.toList()) + .test() + .assertResult(Collections.emptyList()); + } + + @Test + public void error() { + Observable.error(new TestException()) + .collect(Collectors.toList()) + .test() + .assertFailure(TestException.class); + } + + @Test + public void collectorSupplierCrash() { + Observable.range(1, 5) + .collect(new Collector<Integer, Integer, Integer>() { + + @Override + public Supplier<Integer> supplier() { + throw new TestException(); + } + + @Override + public BiConsumer<Integer, Integer> accumulator() { + return (a, b) -> { }; + } + + @Override + public BinaryOperator<Integer> combiner() { + return (a, b) -> a + b; + } + + @Override + public Function<Integer, Integer> finisher() { + return a -> a; + } + + @Override + public Set<Characteristics> characteristics() { + return Collections.emptySet(); + } + }) + .test() + .assertFailure(TestException.class); + } + + @Test + public void collectorAccumulatorCrash() { + BehaviorProcessor<Integer> source = BehaviorProcessor.createDefault(1); + + source + .collect(new Collector<Integer, Integer, Integer>() { + + @Override + public Supplier<Integer> supplier() { + return () -> 1; + } + + @Override + public BiConsumer<Integer, Integer> accumulator() { + return (a, b) -> { throw new TestException(); }; + } + + @Override + public BinaryOperator<Integer> combiner() { + return (a, b) -> a + b; + } + + @Override + public Function<Integer, Integer> finisher() { + return a -> a; + } + + @Override + public Set<Characteristics> characteristics() { + return Collections.emptySet(); + } + }) + .test() + .assertFailure(TestException.class); + + assertFalse(source.hasSubscribers()); + } + + @Test + public void collectorFinisherCrash() { + Observable.range(1, 5) + .collect(new Collector<Integer, Integer, Integer>() { + + @Override + public Supplier<Integer> supplier() { + return () -> 1; + } + + @Override + public BiConsumer<Integer, Integer> accumulator() { + return (a, b) -> { }; + } + + @Override + public BinaryOperator<Integer> combiner() { + return (a, b) -> a + b; + } + + @Override + public Function<Integer, Integer> finisher() { + return a -> { throw new TestException(); }; + } + + @Override + public Set<Characteristics> characteristics() { + return Collections.emptySet(); + } + }) + .test() + .assertFailure(TestException.class); + } + + @Test + public void collectorAccumulatorDropSignals() throws Throwable { + TestHelper.withErrorTracking(errors -> { + Observable<Integer> source = new Observable<Integer>() { + @Override + protected void subscribeActual(Observer<? super Integer> observer) { + observer.onSubscribe(Disposable.empty()); + observer.onNext(1); + observer.onNext(2); + observer.onError(new IOException()); + observer.onComplete(); + } + }; + + source + .collect(new Collector<Integer, Integer, Integer>() { + + @Override + public Supplier<Integer> supplier() { + return () -> 1; + } + + @Override + public BiConsumer<Integer, Integer> accumulator() { + return (a, b) -> { throw new TestException(); }; + } + + @Override + public BinaryOperator<Integer> combiner() { + return (a, b) -> a + b; + } + + @Override + public Function<Integer, Integer> finisher() { + return a -> a; + } + + @Override + public Set<Characteristics> characteristics() { + return Collections.emptySet(); + } + }) + .test() + .assertFailure(TestException.class); + + TestHelper.assertUndeliverable(errors, 0, IOException.class); + }); + } + + @Test + public void dispose() { + TestHelper.checkDisposed(PublishSubject.create() + .collect(Collectors.toList())); + } + + @Test + public void onSubscribe() { + TestHelper.checkDoubleOnSubscribeObservableToSingle(f -> f.collect(Collectors.toList())); + } + + @Test + public void basicToObservable() { + Observable.range(1, 5) + .collect(Collectors.toList()) + .toObservable() + .test() + .assertResult(Arrays.asList(1, 2, 3, 4, 5)); + } + + @Test + public void emptyToObservable() { + Observable.empty() + .collect(Collectors.toList()) + .toObservable() + .test() + .assertResult(Collections.emptyList()); + } + + @Test + public void errorToObservable() { + Observable.error(new TestException()) + .collect(Collectors.toList()) + .toObservable() + .test() + .assertFailure(TestException.class); + } + + @Test + public void collectorSupplierCrashToObservable() { + Observable.range(1, 5) + .collect(new Collector<Integer, Integer, Integer>() { + + @Override + public Supplier<Integer> supplier() { + throw new TestException(); + } + + @Override + public BiConsumer<Integer, Integer> accumulator() { + return (a, b) -> { }; + } + + @Override + public BinaryOperator<Integer> combiner() { + return (a, b) -> a + b; + } + + @Override + public Function<Integer, Integer> finisher() { + return a -> a; + } + + @Override + public Set<Characteristics> characteristics() { + return Collections.emptySet(); + } + }) + .toObservable() + .test() + .assertFailure(TestException.class); + } + + @Test + public void collectorAccumulatorCrashToObservable() { + BehaviorProcessor<Integer> source = BehaviorProcessor.createDefault(1); + + source + .collect(new Collector<Integer, Integer, Integer>() { + + @Override + public Supplier<Integer> supplier() { + return () -> 1; + } + + @Override + public BiConsumer<Integer, Integer> accumulator() { + return (a, b) -> { throw new TestException(); }; + } + + @Override + public BinaryOperator<Integer> combiner() { + return (a, b) -> a + b; + } + + @Override + public Function<Integer, Integer> finisher() { + return a -> a; + } + + @Override + public Set<Characteristics> characteristics() { + return Collections.emptySet(); + } + }) + .toObservable() + .test() + .assertFailure(TestException.class); + + assertFalse(source.hasSubscribers()); + } + + @Test + public void collectorFinisherCrashToObservable() { + Observable.range(1, 5) + .collect(new Collector<Integer, Integer, Integer>() { + + @Override + public Supplier<Integer> supplier() { + return () -> 1; + } + + @Override + public BiConsumer<Integer, Integer> accumulator() { + return (a, b) -> { }; + } + + @Override + public BinaryOperator<Integer> combiner() { + return (a, b) -> a + b; + } + + @Override + public Function<Integer, Integer> finisher() { + return a -> { throw new TestException(); }; + } + + @Override + public Set<Characteristics> characteristics() { + return Collections.emptySet(); + } + }) + .toObservable() + .test() + .assertFailure(TestException.class); + } + + @Test + public void collectorAccumulatorDropSignalsToObservable() throws Throwable { + TestHelper.withErrorTracking(errors -> { + Observable<Integer> source = new Observable<Integer>() { + @Override + protected void subscribeActual(Observer<? super Integer> observer) { + observer.onSubscribe(Disposable.empty()); + observer.onNext(1); + observer.onNext(2); + observer.onError(new IOException()); + observer.onComplete(); + } + }; + + source + .collect(new Collector<Integer, Integer, Integer>() { + + @Override + public Supplier<Integer> supplier() { + return () -> 1; + } + + @Override + public BiConsumer<Integer, Integer> accumulator() { + return (a, b) -> { throw new TestException(); }; + } + + @Override + public BinaryOperator<Integer> combiner() { + return (a, b) -> a + b; + } + + @Override + public Function<Integer, Integer> finisher() { + return a -> a; + } + + @Override + public Set<Characteristics> characteristics() { + return Collections.emptySet(); + } + }) + .toObservable() + .test() + .assertFailure(TestException.class); + + TestHelper.assertUndeliverable(errors, 0, IOException.class); + }); + } + + @Test + public void disposeToObservable() { + TestHelper.checkDisposed(PublishProcessor.create() + .collect(Collectors.toList()).toObservable()); + } + + @Test + public void onSubscribeToObservable() { + TestHelper.checkDoubleOnSubscribeObservable(f -> f.collect(Collectors.toList()).toObservable()); + } + + @Test + public void toObservableTake() { + Observable.range(1, 5) + .collect(Collectors.toList()) + .toObservable() + .take(1) + .test() + .assertResult(Arrays.asList(1, 2, 3, 4, 5)); + } + + @Test + public void disposeBeforeEnd() { + TestObserver<List<Integer>> to = Observable.range(1, 5).concatWith(Observable.never()) + .collect(Collectors.toList()) + .test(); + + to.dispose(); + + to.assertEmpty(); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/jdk8/ObservableFlatMapStreamTest.java b/src/test/java/io/reactivex/rxjava3/internal/jdk8/ObservableFlatMapStreamTest.java new file mode 100644 index 0000000000..5afcfa33ff --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/jdk8/ObservableFlatMapStreamTest.java @@ -0,0 +1,466 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.jdk8; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +import java.io.IOException; +import java.util.Iterator; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.annotations.NonNull; +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.subjects.*; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class ObservableFlatMapStreamTest extends RxJavaTest { + + @Test + public void empty() { + Observable.empty() + .flatMapStream(v -> Stream.of(1, 2, 3, 4, 5)) + .test() + .assertResult(); + } + + @Test + public void emptyHidden() { + Observable.empty() + .hide() + .flatMapStream(v -> Stream.of(1, 2, 3, 4, 5)) + .test() + .assertResult(); + } + + @Test + public void just() { + Observable.just(1) + .flatMapStream(v -> Stream.of(v + 1, v + 2, v + 3, v + 4, v + 5)) + .test() + .assertResult(2, 3, 4, 5, 6); + } + + @Test + public void justHidden() { + Observable.just(1).hide() + .flatMapStream(v -> Stream.of(v + 1, v + 2, v + 3, v + 4, v + 5)) + .test() + .assertResult(2, 3, 4, 5, 6); + } + + @Test + public void error() { + Observable.error(new TestException()) + .flatMapStream(v -> Stream.of(1, 2, 3, 4, 5)) + .test() + .assertFailure(TestException.class); + } + + @Test + public void supplierFusedError() { + Observable.fromCallable(() -> { throw new TestException(); }) + .flatMapStream(v -> Stream.of(1, 2, 3, 4, 5)) + .test() + .assertFailure(TestException.class); + } + + @Test + public void errorHidden() { + Observable.error(new TestException()) + .hide() + .flatMapStream(v -> Stream.of(1, 2, 3, 4, 5)) + .test() + .assertFailure(TestException.class); + } + + @Test + public void range() { + Observable.range(1, 5) + .flatMapStream(v -> IntStream.range(v * 10, v * 10 + 5).boxed()) + .test() + .assertResult( + 10, 11, 12, 13, 14, + 20, 21, 22, 23, 24, + 30, 31, 32, 33, 34, + 40, 41, 42, 43, 44, + 50, 51, 52, 53, 54 + ); + } + + @Test + public void rangeHidden() { + Observable.range(1, 5) + .hide() + .flatMapStream(v -> IntStream.range(v * 10, v * 10 + 5).boxed()) + .test() + .assertResult( + 10, 11, 12, 13, 14, + 20, 21, 22, 23, 24, + 30, 31, 32, 33, 34, + 40, 41, 42, 43, 44, + 50, 51, 52, 53, 54 + ); + } + + @Test + public void rangeToEmpty() { + Observable.range(1, 5) + .flatMapStream(v -> Stream.of()) + .test() + .assertResult(); + } + + @Test + public void rangeTake() { + Observable.range(1, 5) + .flatMapStream(v -> IntStream.range(v * 10, v * 10 + 5).boxed()) + .take(12) + .test() + .assertResult( + 10, 11, 12, 13, 14, + 20, 21, 22, 23, 24, + 30, 31 + ); + } + + @Test + public void rangeTakeHidden() { + Observable.range(1, 5) + .hide() + .flatMapStream(v -> IntStream.range(v * 10, v * 10 + 5).boxed()) + .take(12) + .test() + .assertResult( + 10, 11, 12, 13, 14, + 20, 21, 22, 23, 24, + 30, 31 + ); + } + + @Test + public void upstreamCancelled() { + PublishSubject<Integer> ps = PublishSubject.create(); + + AtomicInteger calls = new AtomicInteger(); + + TestObserver<Integer> to = ps + .flatMapStream(v -> Stream.of(v + 1, v + 2).onClose(() -> calls.getAndIncrement())) + .take(1) + .test(); + + assertTrue(ps.hasObservers()); + + ps.onNext(1); + + to.assertResult(2); + + assertFalse(ps.hasObservers()); + + assertEquals(1, calls.get()); + } + + @Test + public void upstreamCancelledCloseCrash() throws Throwable { + TestHelper.withErrorTracking(errors -> { + PublishSubject<Integer> ps = PublishSubject.create(); + + TestObserver<Integer> to = ps + .flatMapStream(v -> Stream.of(v + 1, v + 2).onClose(() -> { throw new TestException(); })) + .take(1) + .test(); + + assertTrue(ps.hasObservers()); + + ps.onNext(1); + + to.assertResult(2); + + assertFalse(ps.hasObservers()); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + }); + } + + @Test + public void crossMap() { + Observable.range(1, 1000) + .flatMapStream(v -> IntStream.range(v * 1000, v * 1000 + 1000).boxed()) + .test() + .assertValueCount(1_000_000) + .assertNoErrors() + .assertComplete(); + } + + @Test + public void crossMapHidden() { + Observable.range(1, 1000) + .hide() + .flatMapStream(v -> IntStream.range(v * 1000, v * 1000 + 1000).boxed()) + .test() + .assertValueCount(1_000_000) + .assertNoErrors() + .assertComplete(); + } + + @Test + public void onSubscribe() { + TestHelper.checkDoubleOnSubscribeObservable(f -> f.flatMapStream(v -> Stream.of(1, 2))); + } + + @Test + public void mapperThrows() { + Observable.just(1).hide() + .concatMapStream(v -> { throw new TestException(); }) + .test() + .assertFailure(TestException.class); + } + + @Test + public void mapperNull() { + Observable.just(1).hide() + .concatMapStream(v -> null) + .test() + .assertFailure(NullPointerException.class); + } + + @Test + public void streamNull() { + Observable.just(1).hide() + .concatMapStream(v -> Stream.of(1, null)) + .test() + .assertFailure(NullPointerException.class, 1); + } + + @Test + public void hasNextThrows() { + Observable.just(1).hide() + .concatMapStream(v -> Stream.generate(() -> { throw new TestException(); })) + .test() + .assertFailure(TestException.class); + } + + @Test + public void hasNextThrowsLater() { + AtomicInteger counter = new AtomicInteger(); + Observable.just(1).hide() + .concatMapStream(v -> Stream.generate(() -> { + if (counter.getAndIncrement() == 0) { + return 1; + } + throw new TestException(); + })) + .test() + .assertFailure(TestException.class, 1); + } + + @Test + public void mapperThrowsWhenUpstreamErrors() throws Throwable { + TestHelper.withErrorTracking(errors -> { + PublishSubject<Integer> ps = PublishSubject.create(); + + AtomicInteger counter = new AtomicInteger(); + + TestObserver<Integer> to = ps.hide() + .concatMapStream(v -> { + if (counter.getAndIncrement() == 0) { + return Stream.of(1, 2); + } + ps.onError(new IOException()); + throw new TestException(); + }) + .test(); + + ps.onNext(1); + ps.onNext(2); + + to + .assertFailure(IOException.class, 1, 2); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + }); + } + + @Test + public void cancelAfterIteratorNext() throws Exception { + TestObserver<Integer> to = new TestObserver<>(); + + @SuppressWarnings("unchecked") + Stream<Integer> stream = mock(Stream.class); + when(stream.iterator()).thenReturn(new Iterator<Integer>() { + + @Override + public boolean hasNext() { + return true; + } + + @Override + public Integer next() { + to.dispose(); + return 1; + } + }); + + Observable.just(1) + .hide() + .concatMapStream(v -> stream) + .subscribe(to); + + to.assertEmpty(); + } + + @Test + public void cancelAfterIteratorHasNext() throws Exception { + TestObserver<Integer> to = new TestObserver<>(); + + @SuppressWarnings("unchecked") + Stream<Integer> stream = mock(Stream.class); + when(stream.iterator()).thenReturn(new Iterator<Integer>() { + + @Override + public boolean hasNext() { + to.dispose(); + return true; + } + + @Override + public Integer next() { + return 1; + } + }); + + Observable.just(1) + .hide() + .concatMapStream(v -> stream) + .subscribe(to); + + to.assertEmpty(); + } + + @Test + public void asyncUpstreamFused() { + UnicastSubject<Integer> us = UnicastSubject.create(); + + TestObserver<Integer> to = us.flatMapStream(v -> Stream.of(1, 2)) + .test(); + + assertTrue(us.hasObservers()); + + us.onNext(1); + + to.assertValuesOnly(1, 2); + + us.onComplete(); + + to.assertResult(1, 2); + } + + @Test + public void asyncUpstreamFusionBoundary() { + UnicastSubject<Integer> us = UnicastSubject.create(); + + TestObserver<Integer> to = us + .map(v -> v + 1) + .flatMapStream(v -> Stream.of(1, 2)) + .test(); + + assertTrue(us.hasObservers()); + + us.onNext(1); + + to.assertValuesOnly(1, 2); + + us.onComplete(); + + to.assertResult(1, 2); + } + + @Test + public void fusedPollCrash() { + UnicastSubject<Integer> us = UnicastSubject.create(); + + TestObserver<Integer> to = us + .map(v -> { throw new TestException(); }) + .compose(TestHelper.observableStripBoundary()) + .flatMapStream(v -> Stream.of(1, 2)) + .test(); + + assertTrue(us.hasObservers()); + + us.onNext(1); + + assertFalse(us.hasObservers()); + + to.assertFailure(TestException.class); + } + + @Test + public void dispose() { + TestHelper.checkDisposed(PublishSubject.create().flatMapStream(v -> Stream.of(1))); + } + + @Test + public void eventsIgnoredAfterCrash() { + AtomicInteger calls = new AtomicInteger(); + + new Observable<Integer>() { + @Override + protected void subscribeActual(@NonNull Observer<? super Integer> observer) { + observer.onSubscribe(Disposable.empty()); + observer.onNext(1); + observer.onNext(2); + observer.onComplete(); + } + } + .flatMapStream(v -> { + calls.getAndIncrement(); + throw new TestException(); + }) + .take(1) + .test() + .assertFailure(TestException.class); + + assertEquals(1, calls.get()); + } + + @Test + public void eventsIgnoredAfterDispose() { + AtomicInteger calls = new AtomicInteger(); + + new Observable<Integer>() { + @Override + protected void subscribeActual(@NonNull Observer<? super Integer> observer) { + observer.onSubscribe(Disposable.empty()); + observer.onNext(1); + observer.onNext(2); + observer.onComplete(); + } + } + .flatMapStream(v -> { + calls.getAndIncrement(); + return Stream.of(1); + }) + .take(1) + .test() + .assertResult(1); + + assertEquals(1, calls.get()); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/jdk8/ObservableFromCompletionStageTest.java b/src/test/java/io/reactivex/rxjava3/internal/jdk8/ObservableFromCompletionStageTest.java new file mode 100644 index 0000000000..2be5ecf510 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/jdk8/ObservableFromCompletionStageTest.java @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.jdk8; + +import java.util.concurrent.CompletableFuture; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.observers.TestObserver; + +public class ObservableFromCompletionStageTest extends RxJavaTest { + + @Test + public void syncSuccess() { + Observable.fromCompletionStage(CompletableFuture.completedFuture(1)) + .test() + .assertResult(1); + } + + @Test + public void syncFailure() { + CompletableFuture<Integer> cf = new CompletableFuture<>(); + cf.completeExceptionally(new TestException()); + + Observable.fromCompletionStage(cf) + .test() + .assertFailure(TestException.class); + } + + @Test + public void syncNull() { + Observable.fromCompletionStage(CompletableFuture.<Integer>completedFuture(null)) + .test() + .assertFailure(NullPointerException.class); + } + + @Test + public void cancel() { + CompletableFuture<Integer> cf = new CompletableFuture<>(); + + TestObserver<Integer> to = Observable.fromCompletionStage(cf) + .test(); + + to.assertEmpty(); + + to.dispose(); + + cf.complete(1); + + to.assertEmpty(); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/jdk8/ObservableFromOptionalTest.java b/src/test/java/io/reactivex/rxjava3/internal/jdk8/ObservableFromOptionalTest.java new file mode 100644 index 0000000000..5fbeb1f2ff --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/jdk8/ObservableFromOptionalTest.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.jdk8; + +import java.util.Optional; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; + +public class ObservableFromOptionalTest extends RxJavaTest { + + @Test + public void hasValue() { + Observable.fromOptional(Optional.of(1)) + .test() + .assertResult(1); + } + + @Test + public void empty() { + Observable.fromOptional(Optional.empty()) + .test() + .assertResult(); + } + +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/jdk8/ObservableFromStreamTest.java b/src/test/java/io/reactivex/rxjava3/internal/jdk8/ObservableFromStreamTest.java new file mode 100644 index 0000000000..2b943e5338 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/jdk8/ObservableFromStreamTest.java @@ -0,0 +1,490 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.jdk8; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +import java.util.Iterator; +import java.util.concurrent.atomic.*; +import java.util.stream.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.annotations.NonNull; +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.operators.QueueDisposable; +import io.reactivex.rxjava3.operators.QueueFuseable; +import io.reactivex.rxjava3.operators.SimpleQueue; +import io.reactivex.rxjava3.testsupport.*; + +public class ObservableFromStreamTest extends RxJavaTest { + + @Test + public void empty() { + Observable.fromStream(Stream.<Integer>of()) + .test() + .assertResult(); + } + + @Test + public void just() { + Observable.fromStream(Stream.<Integer>of(1)) + .test() + .assertResult(1); + } + + @Test + public void many() { + Observable.fromStream(Stream.<Integer>of(1, 2, 3, 4, 5)) + .test() + .assertResult(1, 2, 3, 4, 5); + } + + @Test + public void noReuse() { + Observable<Integer> source = Observable.fromStream(Stream.<Integer>of(1, 2, 3, 4, 5)); + + source + .test() + .assertResult(1, 2, 3, 4, 5); + + source + .test() + .assertFailure(IllegalStateException.class); + } + + @Test + public void take() { + Observable.fromStream(IntStream.rangeClosed(1, 10).boxed()) + .take(5) + .test() + .assertResult(1, 2, 3, 4, 5); + } + + @Test + public void emptyConditional() { + Observable.fromStream(Stream.<Integer>of()) + .filter(v -> true) + .test() + .assertResult(); + } + + @Test + public void justConditional() { + Observable.fromStream(Stream.<Integer>of(1)) + .filter(v -> true) + .test() + .assertResult(1); + } + + @Test + public void manyConditional() { + Observable.fromStream(Stream.<Integer>of(1, 2, 3, 4, 5)) + .filter(v -> true) + .test() + .assertResult(1, 2, 3, 4, 5); + } + + @Test + public void manyConditionalSkip() { + Observable.fromStream(IntStream.rangeClosed(1, 10).boxed()) + .filter(v -> v % 2 == 0) + .test() + .assertResult(2, 4, 6, 8, 10); + } + + @Test + public void takeConditional() { + Observable.fromStream(IntStream.rangeClosed(1, 10).boxed()) + .filter(v -> true) + .take(5) + .test() + .assertResult(1, 2, 3, 4, 5); + } + + @Test + public void noOfferNoCrashAfterClear() throws Throwable { + AtomicReference<SimpleQueue<?>> queue = new AtomicReference<>(); + + Observable.fromStream(IntStream.rangeClosed(1, 10).boxed()) + .subscribe(new Observer<Integer>() { + @Override + public void onSubscribe(@NonNull Disposable d) { + queue.set((SimpleQueue<?>)d); + ((QueueDisposable<?>)d).requestFusion(QueueFuseable.ANY); + } + + @Override + public void onNext(Integer t) { + } + + @Override + public void onError(Throwable t) { + } + + @Override + public void onComplete() { + } + }); + + SimpleQueue<?> q = queue.get(); + TestHelper.assertNoOffer(q); + + assertFalse(q.isEmpty()); + + q.clear(); + + assertNull(q.poll()); + + assertTrue(q.isEmpty()); + + q.clear(); + + assertNull(q.poll()); + + assertTrue(q.isEmpty()); + } + + @Test + public void fusedPoll() throws Throwable { + AtomicReference<SimpleQueue<?>> queue = new AtomicReference<>(); + AtomicInteger calls = new AtomicInteger(); + + Observable.fromStream(Stream.of(1).onClose(() -> calls.getAndIncrement())) + .subscribe(new Observer<Integer>() { + @Override + public void onSubscribe(@NonNull Disposable d) { + queue.set((SimpleQueue<?>)d); + ((QueueDisposable<?>)d).requestFusion(QueueFuseable.ANY); + } + + @Override + public void onNext(Integer t) { + } + + @Override + public void onError(Throwable t) { + } + + @Override + public void onComplete() { + } + }); + + SimpleQueue<?> q = queue.get(); + + assertFalse(q.isEmpty()); + + assertEquals(1, q.poll()); + + assertTrue(q.isEmpty()); + + assertEquals(1, calls.get()); + } + + @Test + public void fusedPoll2() throws Throwable { + AtomicReference<SimpleQueue<?>> queue = new AtomicReference<>(); + AtomicInteger calls = new AtomicInteger(); + + Observable.fromStream(Stream.of(1, 2).onClose(() -> calls.getAndIncrement())) + .subscribe(new Observer<Integer>() { + @Override + public void onSubscribe(@NonNull Disposable d) { + queue.set((SimpleQueue<?>)d); + ((QueueDisposable<?>)d).requestFusion(QueueFuseable.ANY); + } + + @Override + public void onNext(Integer t) { + } + + @Override + public void onError(Throwable t) { + } + + @Override + public void onComplete() { + } + }); + + SimpleQueue<?> q = queue.get(); + + assertFalse(q.isEmpty()); + + assertEquals(1, q.poll()); + + assertFalse(q.isEmpty()); + + assertEquals(2, q.poll()); + + assertTrue(q.isEmpty()); + + assertEquals(1, calls.get()); + } + + @Test + public void streamOfNull() { + Observable.fromStream(Stream.of((Integer)null)) + .test() + .assertFailure(NullPointerException.class); + } + + @Test + public void streamOfNullConditional() { + Observable.fromStream(Stream.of((Integer)null)) + .filter(v -> true) + .test() + .assertFailure(NullPointerException.class); + } + + @Test + public void syncFusionSupport() { + TestObserverEx<Integer> to = new TestObserverEx<>(); + to.setInitialFusionMode(QueueFuseable.ANY); + + Observable.fromStream(IntStream.rangeClosed(1, 10).boxed()) + .subscribeWith(to) + .assertFuseable() + .assertFusionMode(QueueFuseable.SYNC) + .assertResult(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + } + + @Test + public void asyncFusionNotSupported() { + TestObserverEx<Integer> to = new TestObserverEx<>(); + to.setInitialFusionMode(QueueFuseable.ASYNC); + + Observable.fromStream(IntStream.rangeClosed(1, 10).boxed()) + .subscribeWith(to) + .assertFuseable() + .assertFusionMode(QueueFuseable.NONE) + .assertResult(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + } + + @Test + public void runToEndCloseCrash() throws Throwable { + TestHelper.withErrorTracking(errors -> { + Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5).onClose(() -> { throw new TestException(); }); + + Observable.fromStream(stream) + .test() + .assertResult(1, 2, 3, 4, 5); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + }); + } + + @Test + public void takeCloseCrash() throws Throwable { + TestHelper.withErrorTracking(errors -> { + Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5).onClose(() -> { throw new TestException(); }); + + Observable.fromStream(stream) + .take(3) + .test() + .assertResult(1, 2, 3); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + }); + } + + @Test + public void hasNextCrash() { + AtomicInteger v = new AtomicInteger(); + Observable.fromStream(Stream.<Integer>generate(() -> { + int value = v.getAndIncrement(); + if (value == 1) { + throw new TestException(); + } + return value; + })) + .test() + .assertFailure(TestException.class, 0); + } + + @Test + public void hasNextCrashConditional() { + AtomicInteger counter = new AtomicInteger(); + Observable.fromStream(Stream.<Integer>generate(() -> { + int value = counter.getAndIncrement(); + if (value == 1) { + throw new TestException(); + } + return value; + })) + .filter(v -> true) + .test() + .assertFailure(TestException.class, 0); + } + + @Test + public void closeCalledOnEmpty() { + AtomicInteger calls = new AtomicInteger(); + + Observable.fromStream(Stream.of().onClose(() -> calls.getAndIncrement())) + .test() + .assertResult(); + + assertEquals(1, calls.get()); + } + + @Test + public void closeCalledAfterItems() { + AtomicInteger calls = new AtomicInteger(); + + Observable.fromStream(Stream.of(1, 2, 3, 4, 5).onClose(() -> calls.getAndIncrement())) + .test() + .assertResult(1, 2, 3, 4, 5); + + assertEquals(1, calls.get()); + } + + @Test + public void closeCalledOnCancel() { + AtomicInteger calls = new AtomicInteger(); + + Observable.fromStream(Stream.of(1, 2, 3, 4, 5).onClose(() -> calls.getAndIncrement())) + .take(3) + .test() + .assertResult(1, 2, 3); + + assertEquals(1, calls.get()); + } + + @Test + public void closeCalledOnItemCrash() { + AtomicInteger calls = new AtomicInteger(); + AtomicInteger counter = new AtomicInteger(); + Observable.fromStream(Stream.<Integer>generate(() -> { + int value = counter.getAndIncrement(); + if (value == 1) { + throw new TestException(); + } + return value; + }).onClose(() -> calls.getAndIncrement())) + .test() + .assertFailure(TestException.class, 0); + + assertEquals(1, calls.get()); + } + + @Test + public void closeCalledAfterItemsConditional() { + AtomicInteger calls = new AtomicInteger(); + + Observable.fromStream(Stream.of(1, 2, 3, 4, 5).onClose(() -> calls.getAndIncrement())) + .filter(v -> true) + .test() + .assertResult(1, 2, 3, 4, 5); + + assertEquals(1, calls.get()); + } + + @Test + public void closeCalledOnCancelConditional() { + AtomicInteger calls = new AtomicInteger(); + + Observable.fromStream(Stream.of(1, 2, 3, 4, 5).onClose(() -> calls.getAndIncrement())) + .filter(v -> true) + .take(3) + .test() + .assertResult(1, 2, 3); + + assertEquals(1, calls.get()); + } + + @Test + public void closeCalledOnItemCrashConditional() { + AtomicInteger calls = new AtomicInteger(); + AtomicInteger counter = new AtomicInteger(); + Observable.fromStream(Stream.<Integer>generate(() -> { + int value = counter.getAndIncrement(); + if (value == 1) { + throw new TestException(); + } + return value; + }).onClose(() -> calls.getAndIncrement())) + .filter(v -> true) + .test() + .assertFailure(TestException.class, 0); + + assertEquals(1, calls.get()); + } + + @Test + public void dispose() { + TestHelper.checkDisposed(Observable.fromStream(Stream.of(1))); + } + + @Test + public void cancelAfterIteratorNext() throws Exception { + TestObserver<Integer> to = new TestObserver<>(); + + @SuppressWarnings("unchecked") + Stream<Integer> stream = mock(Stream.class); + when(stream.iterator()).thenReturn(new Iterator<Integer>() { + + @Override + public boolean hasNext() { + return true; + } + + @Override + public Integer next() { + to.dispose(); + return 1; + } + }); + + Observable.fromStream(stream) + .subscribe(to); + + to.assertEmpty(); + } + + @Test + public void cancelAfterIteratorHasNext() throws Exception { + TestObserver<Integer> to = new TestObserver<>(); + + @SuppressWarnings("unchecked") + Stream<Integer> stream = mock(Stream.class); + when(stream.iterator()).thenReturn(new Iterator<Integer>() { + + int calls; + + @Override + public boolean hasNext() { + if (++calls == 1) { + to.dispose(); + } + return true; + } + + @Override + public Integer next() { + return 1; + } + }); + + Observable.fromStream(stream) + .subscribe(to); + + to.assertEmpty(); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/jdk8/ObservableMapOptionalTest.java b/src/test/java/io/reactivex/rxjava3/internal/jdk8/ObservableMapOptionalTest.java new file mode 100644 index 0000000000..1b0a7f0a7d --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/jdk8/ObservableMapOptionalTest.java @@ -0,0 +1,394 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.jdk8; + +import static org.junit.Assert.assertFalse; + +import java.util.Optional; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.operators.QueueFuseable; +import io.reactivex.rxjava3.subjects.*; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class ObservableMapOptionalTest extends RxJavaTest { + + static final Function<? super Integer, Optional<? extends Integer>> MODULO = v -> v % 2 == 0 ? Optional.of(v) : Optional.<Integer>empty(); + + @Test + public void allPresent() { + Observable.range(1, 5) + .mapOptional(Optional::of) + .test() + .assertResult(1, 2, 3, 4, 5); + } + + @Test + public void allEmpty() { + Observable.range(1, 5) + .mapOptional(v -> Optional.<Integer>empty()) + .test() + .assertResult(); + } + + @Test + public void mixed() { + Observable.range(1, 10) + .mapOptional(MODULO) + .test() + .assertResult(2, 4, 6, 8, 10); + } + + @Test + public void mapperChash() { + BehaviorSubject<Integer> source = BehaviorSubject.createDefault(1); + + source + .mapOptional(v -> { throw new TestException(); }) + .test() + .assertFailure(TestException.class); + + assertFalse(source.hasObservers()); + } + + @Test + public void mapperNull() { + BehaviorSubject<Integer> source = BehaviorSubject.createDefault(1); + + source + .mapOptional(v -> null) + .test() + .assertFailure(NullPointerException.class); + + assertFalse(source.hasObservers()); + } + + @Test + public void crashDropsOnNexts() { + Observable<Integer> source = new Observable<Integer>() { + @Override + protected void subscribeActual(Observer<? super Integer> observer) { + observer.onSubscribe(Disposable.empty()); + observer.onNext(1); + observer.onNext(2); + } + }; + + source + .mapOptional(v -> { throw new TestException(); }) + .test() + .assertFailure(TestException.class); + } + + @Test + public void syncFusedAll() { + Observable.range(1, 5) + .mapOptional(Optional::of) + .to(TestHelper.testConsumer(false, QueueFuseable.SYNC)) + .assertFuseable() + .assertFusionMode(QueueFuseable.SYNC) + .assertResult(1, 2, 3, 4, 5); + } + + @Test + public void asyncFusedAll() { + UnicastSubject<Integer> us = UnicastSubject.create(); + TestHelper.emit(us, 1, 2, 3, 4, 5); + + us + .mapOptional(Optional::of) + .to(TestHelper.testConsumer(false, QueueFuseable.ASYNC)) + .assertFuseable() + .assertFusionMode(QueueFuseable.ASYNC) + .assertResult(1, 2, 3, 4, 5); + } + + @Test + public void boundaryFusedAll() { + UnicastSubject<Integer> us = UnicastSubject.create(); + TestHelper.emit(us, 1, 2, 3, 4, 5); + + us + .mapOptional(Optional::of) + .to(TestHelper.testConsumer(false, QueueFuseable.ASYNC | QueueFuseable.BOUNDARY)) + .assertFuseable() + .assertFusionMode(QueueFuseable.NONE) + .assertResult(1, 2, 3, 4, 5); + } + + @Test + public void syncFusedNone() { + Observable.range(1, 5) + .mapOptional(v -> Optional.empty()) + .to(TestHelper.testConsumer(false, QueueFuseable.SYNC)) + .assertFuseable() + .assertFusionMode(QueueFuseable.SYNC) + .assertResult(); + } + + @Test + public void asyncFusedNone() { + UnicastSubject<Integer> us = UnicastSubject.create(); + TestHelper.emit(us, 1, 2, 3, 4, 5); + + us + .mapOptional(v -> Optional.empty()) + .to(TestHelper.testConsumer(false, QueueFuseable.ASYNC)) + .assertFuseable() + .assertFusionMode(QueueFuseable.ASYNC) + .assertResult(); + } + + @Test + public void boundaryFusedNone() { + UnicastSubject<Integer> us = UnicastSubject.create(); + TestHelper.emit(us, 1, 2, 3, 4, 5); + + us + .mapOptional(v -> Optional.empty()) + .to(TestHelper.testConsumer(false, QueueFuseable.ASYNC | QueueFuseable.BOUNDARY)) + .assertFuseable() + .assertFusionMode(QueueFuseable.NONE) + .assertResult(); + } + + @Test + public void syncFusedMixed() { + Observable.range(1, 10) + .mapOptional(MODULO) + .to(TestHelper.testConsumer(false, QueueFuseable.SYNC)) + .assertFuseable() + .assertFusionMode(QueueFuseable.SYNC) + .assertResult(2, 4, 6, 8, 10); + } + + @Test + public void asyncFusedMixed() { + UnicastSubject<Integer> us = UnicastSubject.create(); + TestHelper.emit(us, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + + us + .mapOptional(MODULO) + .to(TestHelper.testConsumer(false, QueueFuseable.ASYNC)) + .assertFuseable() + .assertFusionMode(QueueFuseable.ASYNC) + .assertResult(2, 4, 6, 8, 10); + } + + @Test + public void boundaryFusedMixed() { + UnicastSubject<Integer> us = UnicastSubject.create(); + TestHelper.emit(us, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + + us + .mapOptional(MODULO) + .to(TestHelper.testConsumer(false, QueueFuseable.ASYNC | QueueFuseable.BOUNDARY)) + .assertFuseable() + .assertFusionMode(QueueFuseable.NONE) + .assertResult(2, 4, 6, 8, 10); + } + + @Test + public void allPresentConditional() { + Observable.range(1, 5) + .mapOptional(Optional::of) + .filter(v -> true) + .test() + .assertResult(1, 2, 3, 4, 5); + } + + @Test + public void allEmptyConditional() { + Observable.range(1, 5) + .mapOptional(v -> Optional.<Integer>empty()) + .filter(v -> true) + .test() + .assertResult(); + } + + @Test + public void mixedConditional() { + Observable.range(1, 10) + .mapOptional(MODULO) + .filter(v -> true) + .test() + .assertResult(2, 4, 6, 8, 10); + } + + @Test + public void mapperChashConditional() { + BehaviorSubject<Integer> source = BehaviorSubject.createDefault(1); + + source + .mapOptional(v -> { throw new TestException(); }) + .filter(v -> true) + .test() + .assertFailure(TestException.class); + + assertFalse(source.hasObservers()); + } + + @Test + public void mapperNullConditional() { + BehaviorSubject<Integer> source = BehaviorSubject.createDefault(1); + + source + .mapOptional(v -> null) + .filter(v -> true) + .test() + .assertFailure(NullPointerException.class); + + assertFalse(source.hasObservers()); + } + + @Test + public void crashDropsOnNextsConditional() { + Observable<Integer> source = new Observable<Integer>() { + @Override + protected void subscribeActual(Observer<? super Integer> observer) { + observer.onSubscribe(Disposable.empty()); + observer.onNext(1); + observer.onNext(2); + } + }; + + source + .mapOptional(v -> { throw new TestException(); }) + .filter(v -> true) + .test() + .assertFailure(TestException.class); + } + + @Test + public void syncFusedAllConditional() { + Observable.range(1, 5) + .mapOptional(Optional::of) + .filter(v -> true) + .to(TestHelper.testConsumer(false, QueueFuseable.SYNC)) + .assertFuseable() + .assertFusionMode(QueueFuseable.SYNC) + .assertResult(1, 2, 3, 4, 5); + } + + @Test + public void asyncFusedAllConditional() { + UnicastSubject<Integer> us = UnicastSubject.create(); + TestHelper.emit(us, 1, 2, 3, 4, 5); + + us + .mapOptional(Optional::of) + .filter(v -> true) + .to(TestHelper.testConsumer(false, QueueFuseable.ASYNC)) + .assertFuseable() + .assertFusionMode(QueueFuseable.ASYNC) + .assertResult(1, 2, 3, 4, 5); + } + + @Test + public void boundaryFusedAllConditiona() { + UnicastSubject<Integer> us = UnicastSubject.create(); + TestHelper.emit(us, 1, 2, 3, 4, 5); + + us + .mapOptional(Optional::of) + .filter(v -> true) + .to(TestHelper.testConsumer(false, QueueFuseable.ASYNC | QueueFuseable.BOUNDARY)) + .assertFuseable() + .assertFusionMode(QueueFuseable.NONE) + .assertResult(1, 2, 3, 4, 5); + } + + @Test + public void syncFusedNoneConditional() { + Observable.range(1, 5) + .mapOptional(v -> Optional.empty()) + .filter(v -> true) + .to(TestHelper.testConsumer(false, QueueFuseable.SYNC)) + .assertFuseable() + .assertFusionMode(QueueFuseable.SYNC) + .assertResult(); + } + + @Test + public void asyncFusedNoneConditional() { + UnicastSubject<Integer> us = UnicastSubject.create(); + TestHelper.emit(us, 1, 2, 3, 4, 5); + + us + .mapOptional(v -> Optional.empty()) + .filter(v -> true) + .to(TestHelper.testConsumer(false, QueueFuseable.ASYNC)) + .assertFuseable() + .assertFusionMode(QueueFuseable.ASYNC) + .assertResult(); + } + + @Test + public void boundaryFusedNoneConditional() { + UnicastSubject<Integer> us = UnicastSubject.create(); + TestHelper.emit(us, 1, 2, 3, 4, 5); + + us + .mapOptional(v -> Optional.empty()) + .filter(v -> true) + .to(TestHelper.testConsumer(false, QueueFuseable.ASYNC | QueueFuseable.BOUNDARY)) + .assertFuseable() + .assertFusionMode(QueueFuseable.NONE) + .assertResult(); + } + + @Test + public void syncFusedMixedConditional() { + Observable.range(1, 10) + .mapOptional(MODULO) + .filter(v -> true) + .to(TestHelper.testConsumer(false, QueueFuseable.SYNC)) + .assertFuseable() + .assertFusionMode(QueueFuseable.SYNC) + .assertResult(2, 4, 6, 8, 10); + } + + @Test + public void asyncFusedMixedConditional() { + UnicastSubject<Integer> us = UnicastSubject.create(); + TestHelper.emit(us, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + + us + .mapOptional(MODULO) + .filter(v -> true) + .to(TestHelper.testConsumer(false, QueueFuseable.ASYNC)) + .assertFuseable() + .assertFusionMode(QueueFuseable.ASYNC) + .assertResult(2, 4, 6, 8, 10); + } + + @Test + public void boundaryFusedMixedConditional() { + UnicastSubject<Integer> us = UnicastSubject.create(); + TestHelper.emit(us, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + + us + .mapOptional(MODULO) + .filter(v -> true) + .to(TestHelper.testConsumer(false, QueueFuseable.ASYNC | QueueFuseable.BOUNDARY)) + .assertFuseable() + .assertFusionMode(QueueFuseable.NONE) + .assertResult(2, 4, 6, 8, 10); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/jdk8/ObservableStageSubscriberOrDefaultTest.java b/src/test/java/io/reactivex/rxjava3/internal/jdk8/ObservableStageSubscriberOrDefaultTest.java new file mode 100644 index 0000000000..f652c9b327 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/jdk8/ObservableStageSubscriberOrDefaultTest.java @@ -0,0 +1,475 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.jdk8; + +import static org.junit.Assert.*; + +import java.util.concurrent.CompletableFuture; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.subjects.*; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class ObservableStageSubscriberOrDefaultTest extends RxJavaTest { + + @Test + public void firstJust() throws Exception { + Integer v = Observable.just(1) + .firstStage(null) + .toCompletableFuture() + .get(); + + assertEquals((Integer)1, v); + } + + @Test + public void firstEmpty() throws Exception { + Integer v = Observable.<Integer>empty() + .firstStage(2) + .toCompletableFuture() + .get(); + + assertEquals((Integer)2, v); + } + + @Test + public void firstCancels() throws Exception { + BehaviorSubject<Integer> source = BehaviorSubject.createDefault(1); + + Integer v = source + .firstStage(null) + .toCompletableFuture() + .get(); + + assertEquals((Integer)1, v); + + assertFalse(source.hasObservers()); + } + + @Test + public void firstCompletableFutureCancels() throws Exception { + PublishSubject<Integer> source = PublishSubject.create(); + + CompletableFuture<Integer> cf = source + .firstStage(null) + .toCompletableFuture(); + + assertTrue(source.hasObservers()); + + cf.cancel(true); + + assertTrue(cf.isCancelled()); + + assertFalse(source.hasObservers()); + } + + @Test + public void firstCompletableManualCompleteCancels() throws Exception { + PublishSubject<Integer> source = PublishSubject.create(); + + CompletableFuture<Integer> cf = source + .firstStage(null) + .toCompletableFuture(); + + assertTrue(source.hasObservers()); + + cf.complete(1); + + assertTrue(cf.isDone()); + assertFalse(cf.isCompletedExceptionally()); + assertFalse(cf.isCancelled()); + + assertFalse(source.hasObservers()); + + assertEquals((Integer)1, cf.get()); + } + + @Test + public void firstCompletableManualCompleteExceptionallyCancels() throws Exception { + PublishSubject<Integer> source = PublishSubject.create(); + + CompletableFuture<Integer> cf = source + .firstStage(null) + .toCompletableFuture(); + + assertTrue(source.hasObservers()); + + cf.completeExceptionally(new TestException()); + + assertTrue(cf.isDone()); + assertTrue(cf.isCompletedExceptionally()); + assertFalse(cf.isCancelled()); + + assertFalse(source.hasObservers()); + + TestHelper.assertError(cf, TestException.class); + } + + @Test + public void firstError() throws Exception { + CompletableFuture<Integer> cf = Observable.<Integer>error(new TestException()) + .firstStage(null) + .toCompletableFuture(); + + assertTrue(cf.isDone()); + assertTrue(cf.isCompletedExceptionally()); + assertFalse(cf.isCancelled()); + + TestHelper.assertError(cf, TestException.class); + } + + @Test + public void firstSourceIgnoresCancel() throws Throwable { + TestHelper.withErrorTracking(errors -> { + Integer v = new Observable<Integer>() { + @Override + protected void subscribeActual(Observer<? super Integer> observer) { + observer.onSubscribe(Disposable.empty()); + observer.onNext(1); + observer.onError(new TestException()); + observer.onComplete(); + } + } + .firstStage(null) + .toCompletableFuture() + .get(); + + assertEquals((Integer)1, v); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + }); + } + + @Test + public void firstDoubleOnSubscribe() throws Throwable { + TestHelper.withErrorTracking(errors -> { + Integer v = new Observable<Integer>() { + @Override + protected void subscribeActual(Observer<? super Integer> observer) { + observer.onSubscribe(Disposable.empty()); + observer.onSubscribe(Disposable.empty()); + observer.onNext(1); + } + } + .firstStage(null) + .toCompletableFuture() + .get(); + + assertEquals((Integer)1, v); + + TestHelper.assertError(errors, 0, ProtocolViolationException.class); + }); + } + + @Test + public void singleJust() throws Exception { + Integer v = Observable.just(1) + .singleStage(null) + .toCompletableFuture() + .get(); + + assertEquals((Integer)1, v); + } + + @Test + public void singleEmpty() throws Exception { + Integer v = Observable.<Integer>empty() + .singleStage(2) + .toCompletableFuture() + .get(); + + assertEquals((Integer)2, v); + } + + @Test + public void singleTooManyCancels() throws Exception { + ReplaySubject<Integer> source = ReplaySubject.create(); + source.onNext(1); + source.onNext(2); + + TestHelper.assertError(source + .singleStage(null) + .toCompletableFuture(), IllegalArgumentException.class); + + assertFalse(source.hasObservers()); + } + + @Test + public void singleCompletableFutureCancels() throws Exception { + PublishSubject<Integer> source = PublishSubject.create(); + + CompletableFuture<Integer> cf = source + .singleStage(null) + .toCompletableFuture(); + + assertTrue(source.hasObservers()); + + cf.cancel(true); + + assertTrue(cf.isCancelled()); + + assertFalse(source.hasObservers()); + } + + @Test + public void singleCompletableManualCompleteCancels() throws Exception { + PublishSubject<Integer> source = PublishSubject.create(); + + CompletableFuture<Integer> cf = source + .singleStage(null) + .toCompletableFuture(); + + assertTrue(source.hasObservers()); + + cf.complete(1); + + assertTrue(cf.isDone()); + assertFalse(cf.isCompletedExceptionally()); + assertFalse(cf.isCancelled()); + + assertFalse(source.hasObservers()); + + assertEquals((Integer)1, cf.get()); + } + + @Test + public void singleCompletableManualCompleteExceptionallyCancels() throws Exception { + PublishSubject<Integer> source = PublishSubject.create(); + + CompletableFuture<Integer> cf = source + .singleStage(null) + .toCompletableFuture(); + + assertTrue(source.hasObservers()); + + cf.completeExceptionally(new TestException()); + + assertTrue(cf.isDone()); + assertTrue(cf.isCompletedExceptionally()); + assertFalse(cf.isCancelled()); + + assertFalse(source.hasObservers()); + + TestHelper.assertError(cf, TestException.class); + } + + @Test + public void singleError() throws Exception { + CompletableFuture<Integer> cf = Observable.<Integer>error(new TestException()) + .singleStage(null) + .toCompletableFuture(); + + assertTrue(cf.isDone()); + assertTrue(cf.isCompletedExceptionally()); + assertFalse(cf.isCancelled()); + + TestHelper.assertError(cf, TestException.class); + } + + @Test + public void singleSourceIgnoresCancel() throws Throwable { + TestHelper.withErrorTracking(errors -> { + Integer v = new Observable<Integer>() { + @Override + protected void subscribeActual(Observer<? super Integer> observer) { + observer.onSubscribe(Disposable.empty()); + observer.onNext(1); + observer.onComplete(); + observer.onError(new TestException()); + observer.onComplete(); + } + } + .singleStage(null) + .toCompletableFuture() + .get(); + + assertEquals((Integer)1, v); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + }); + } + + @Test + public void singleDoubleOnSubscribe() throws Throwable { + TestHelper.withErrorTracking(errors -> { + Integer v = new Observable<Integer>() { + @Override + protected void subscribeActual(Observer<? super Integer> observer) { + observer.onSubscribe(Disposable.empty()); + observer.onSubscribe(Disposable.empty()); + observer.onNext(1); + observer.onComplete(); + } + } + .singleStage(null) + .toCompletableFuture() + .get(); + + assertEquals((Integer)1, v); + + TestHelper.assertError(errors, 0, ProtocolViolationException.class); + }); + } + + @Test + public void lastJust() throws Exception { + Integer v = Observable.just(1) + .lastStage(null) + .toCompletableFuture() + .get(); + + assertEquals((Integer)1, v); + } + + @Test + public void lastRange() throws Exception { + Integer v = Observable.range(1, 5) + .lastStage(null) + .toCompletableFuture() + .get(); + + assertEquals((Integer)5, v); + } + + @Test + public void lastEmpty() throws Exception { + Integer v = Observable.<Integer>empty() + .lastStage(2) + .toCompletableFuture() + .get(); + + assertEquals((Integer)2, v); + } + + @Test + public void lastCompletableFutureCancels() throws Exception { + PublishSubject<Integer> source = PublishSubject.create(); + + CompletableFuture<Integer> cf = source + .lastStage(null) + .toCompletableFuture(); + + assertTrue(source.hasObservers()); + + cf.cancel(true); + + assertTrue(cf.isCancelled()); + + assertFalse(source.hasObservers()); + } + + @Test + public void lastCompletableManualCompleteCancels() throws Exception { + PublishSubject<Integer> source = PublishSubject.create(); + + CompletableFuture<Integer> cf = source + .lastStage(null) + .toCompletableFuture(); + + assertTrue(source.hasObservers()); + + cf.complete(1); + + assertTrue(cf.isDone()); + assertFalse(cf.isCompletedExceptionally()); + assertFalse(cf.isCancelled()); + + assertFalse(source.hasObservers()); + + assertEquals((Integer)1, cf.get()); + } + + @Test + public void lastCompletableManualCompleteExceptionallyCancels() throws Exception { + PublishSubject<Integer> source = PublishSubject.create(); + + CompletableFuture<Integer> cf = source + .lastStage(null) + .toCompletableFuture(); + + assertTrue(source.hasObservers()); + + cf.completeExceptionally(new TestException()); + + assertTrue(cf.isDone()); + assertTrue(cf.isCompletedExceptionally()); + assertFalse(cf.isCancelled()); + + assertFalse(source.hasObservers()); + + TestHelper.assertError(cf, TestException.class); + } + + @Test + public void lastError() throws Exception { + CompletableFuture<Integer> cf = Observable.<Integer>error(new TestException()) + .lastStage(null) + .toCompletableFuture(); + + assertTrue(cf.isDone()); + assertTrue(cf.isCompletedExceptionally()); + assertFalse(cf.isCancelled()); + + TestHelper.assertError(cf, TestException.class); + } + + @Test + public void lastSourceIgnoresCancel() throws Throwable { + TestHelper.withErrorTracking(errors -> { + Integer v = new Observable<Integer>() { + @Override + protected void subscribeActual(Observer<? super Integer> observer) { + observer.onSubscribe(Disposable.empty()); + observer.onNext(1); + observer.onComplete(); + observer.onError(new TestException()); + observer.onComplete(); + } + } + .lastStage(null) + .toCompletableFuture() + .get(); + + assertEquals((Integer)1, v); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + }); + } + + @Test + public void lastDoubleOnSubscribe() throws Throwable { + TestHelper.withErrorTracking(errors -> { + Integer v = new Observable<Integer>() { + @Override + protected void subscribeActual(Observer<? super Integer> observer) { + observer.onSubscribe(Disposable.empty()); + observer.onSubscribe(Disposable.empty()); + observer.onNext(1); + observer.onComplete(); + } + } + .lastStage(null) + .toCompletableFuture() + .get(); + + assertEquals((Integer)1, v); + + TestHelper.assertError(errors, 0, ProtocolViolationException.class); + }); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/jdk8/ObservableStageSubscriberOrErrorTest.java b/src/test/java/io/reactivex/rxjava3/internal/jdk8/ObservableStageSubscriberOrErrorTest.java new file mode 100644 index 0000000000..5646fc3d59 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/jdk8/ObservableStageSubscriberOrErrorTest.java @@ -0,0 +1,469 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.jdk8; + +import static org.junit.Assert.*; + +import java.util.NoSuchElementException; +import java.util.concurrent.CompletableFuture; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.subjects.*; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class ObservableStageSubscriberOrErrorTest extends RxJavaTest { + + @Test + public void firstJust() throws Exception { + Integer v = Observable.just(1) + .firstOrErrorStage() + .toCompletableFuture() + .get(); + + assertEquals((Integer)1, v); + } + + @Test + public void firstEmpty() throws Exception { + TestHelper.assertError( + Observable.<Integer>empty() + .firstOrErrorStage() + .toCompletableFuture(), NoSuchElementException.class); + } + + @Test + public void firstCancels() throws Exception { + BehaviorSubject<Integer> source = BehaviorSubject.createDefault(1); + + Integer v = source + .firstOrErrorStage() + .toCompletableFuture() + .get(); + + assertEquals((Integer)1, v); + + assertFalse(source.hasObservers()); + } + + @Test + public void firstCompletableFutureCancels() throws Exception { + PublishSubject<Integer> source = PublishSubject.create(); + + CompletableFuture<Integer> cf = source + .firstOrErrorStage() + .toCompletableFuture(); + + assertTrue(source.hasObservers()); + + cf.cancel(true); + + assertTrue(cf.isCancelled()); + + assertFalse(source.hasObservers()); + } + + @Test + public void firstCompletableManualCompleteCancels() throws Exception { + PublishSubject<Integer> source = PublishSubject.create(); + + CompletableFuture<Integer> cf = source + .firstOrErrorStage() + .toCompletableFuture(); + + assertTrue(source.hasObservers()); + + cf.complete(1); + + assertTrue(cf.isDone()); + assertFalse(cf.isCompletedExceptionally()); + assertFalse(cf.isCancelled()); + + assertFalse(source.hasObservers()); + + assertEquals((Integer)1, cf.get()); + } + + @Test + public void firstCompletableManualCompleteExceptionallyCancels() throws Exception { + PublishSubject<Integer> source = PublishSubject.create(); + + CompletableFuture<Integer> cf = source + .firstOrErrorStage() + .toCompletableFuture(); + + assertTrue(source.hasObservers()); + + cf.completeExceptionally(new TestException()); + + assertTrue(cf.isDone()); + assertTrue(cf.isCompletedExceptionally()); + assertFalse(cf.isCancelled()); + + assertFalse(source.hasObservers()); + + TestHelper.assertError(cf, TestException.class); + } + + @Test + public void firstError() throws Exception { + CompletableFuture<Integer> cf = Observable.<Integer>error(new TestException()) + .firstOrErrorStage() + .toCompletableFuture(); + + assertTrue(cf.isDone()); + assertTrue(cf.isCompletedExceptionally()); + assertFalse(cf.isCancelled()); + + TestHelper.assertError(cf, TestException.class); + } + + @Test + public void firstSourceIgnoresCancel() throws Throwable { + TestHelper.withErrorTracking(errors -> { + Integer v = new Observable<Integer>() { + @Override + protected void subscribeActual(Observer<? super Integer> observer) { + observer.onSubscribe(Disposable.empty()); + observer.onNext(1); + observer.onError(new TestException()); + observer.onComplete(); + } + } + .firstOrErrorStage() + .toCompletableFuture() + .get(); + + assertEquals((Integer)1, v); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + }); + } + + @Test + public void firstDoubleOnSubscribe() throws Throwable { + TestHelper.withErrorTracking(errors -> { + Integer v = new Observable<Integer>() { + @Override + protected void subscribeActual(Observer<? super Integer> observer) { + observer.onSubscribe(Disposable.empty()); + observer.onSubscribe(Disposable.empty()); + observer.onNext(1); + } + } + .firstOrErrorStage() + .toCompletableFuture() + .get(); + + assertEquals((Integer)1, v); + + TestHelper.assertError(errors, 0, ProtocolViolationException.class); + }); + } + + @Test + public void singleJust() throws Exception { + Integer v = Observable.just(1) + .singleOrErrorStage() + .toCompletableFuture() + .get(); + + assertEquals((Integer)1, v); + } + + @Test + public void singleEmpty() throws Exception { + TestHelper.assertError( + Observable.<Integer>empty() + .singleOrErrorStage() + .toCompletableFuture(), NoSuchElementException.class); + } + + @Test + public void singleTooManyCancels() throws Exception { + ReplaySubject<Integer> source = ReplaySubject.create(); + source.onNext(1); + source.onNext(2); + + TestHelper.assertError(source + .singleOrErrorStage() + .toCompletableFuture(), IllegalArgumentException.class); + + assertFalse(source.hasObservers()); + } + + @Test + public void singleCompletableFutureCancels() throws Exception { + PublishSubject<Integer> source = PublishSubject.create(); + + CompletableFuture<Integer> cf = source + .singleOrErrorStage() + .toCompletableFuture(); + + assertTrue(source.hasObservers()); + + cf.cancel(true); + + assertTrue(cf.isCancelled()); + + assertFalse(source.hasObservers()); + } + + @Test + public void singleCompletableManualCompleteCancels() throws Exception { + PublishSubject<Integer> source = PublishSubject.create(); + + CompletableFuture<Integer> cf = source + .singleOrErrorStage() + .toCompletableFuture(); + + assertTrue(source.hasObservers()); + + cf.complete(1); + + assertTrue(cf.isDone()); + assertFalse(cf.isCompletedExceptionally()); + assertFalse(cf.isCancelled()); + + assertFalse(source.hasObservers()); + + assertEquals((Integer)1, cf.get()); + } + + @Test + public void singleCompletableManualCompleteExceptionallyCancels() throws Exception { + PublishSubject<Integer> source = PublishSubject.create(); + + CompletableFuture<Integer> cf = source + .singleOrErrorStage() + .toCompletableFuture(); + + assertTrue(source.hasObservers()); + + cf.completeExceptionally(new TestException()); + + assertTrue(cf.isDone()); + assertTrue(cf.isCompletedExceptionally()); + assertFalse(cf.isCancelled()); + + assertFalse(source.hasObservers()); + + TestHelper.assertError(cf, TestException.class); + } + + @Test + public void singleError() throws Exception { + CompletableFuture<Integer> cf = Observable.<Integer>error(new TestException()) + .singleOrErrorStage() + .toCompletableFuture(); + + assertTrue(cf.isDone()); + assertTrue(cf.isCompletedExceptionally()); + assertFalse(cf.isCancelled()); + + TestHelper.assertError(cf, TestException.class); + } + + @Test + public void singleSourceIgnoresCancel() throws Throwable { + TestHelper.withErrorTracking(errors -> { + Integer v = new Observable<Integer>() { + @Override + protected void subscribeActual(Observer<? super Integer> observer) { + observer.onSubscribe(Disposable.empty()); + observer.onNext(1); + observer.onComplete(); + observer.onError(new TestException()); + observer.onComplete(); + } + } + .singleOrErrorStage() + .toCompletableFuture() + .get(); + + assertEquals((Integer)1, v); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + }); + } + + @Test + public void singleDoubleOnSubscribe() throws Throwable { + TestHelper.withErrorTracking(errors -> { + Integer v = new Observable<Integer>() { + @Override + protected void subscribeActual(Observer<? super Integer> observer) { + observer.onSubscribe(Disposable.empty()); + observer.onSubscribe(Disposable.empty()); + observer.onNext(1); + observer.onComplete(); + } + } + .singleOrErrorStage() + .toCompletableFuture() + .get(); + + assertEquals((Integer)1, v); + + TestHelper.assertError(errors, 0, ProtocolViolationException.class); + }); + } + + @Test + public void lastJust() throws Exception { + Integer v = Observable.just(1) + .lastOrErrorStage() + .toCompletableFuture() + .get(); + + assertEquals((Integer)1, v); + } + + @Test + public void lastRange() throws Exception { + Integer v = Observable.range(1, 5) + .lastOrErrorStage() + .toCompletableFuture() + .get(); + + assertEquals((Integer)5, v); + } + + @Test + public void lastEmpty() throws Exception { + TestHelper.assertError(Observable.<Integer>empty() + .lastOrErrorStage() + .toCompletableFuture(), NoSuchElementException.class); + } + + @Test + public void lastCompletableFutureCancels() throws Exception { + PublishSubject<Integer> source = PublishSubject.create(); + + CompletableFuture<Integer> cf = source + .lastOrErrorStage() + .toCompletableFuture(); + + assertTrue(source.hasObservers()); + + cf.cancel(true); + + assertTrue(cf.isCancelled()); + + assertFalse(source.hasObservers()); + } + + @Test + public void lastCompletableManualCompleteCancels() throws Exception { + PublishSubject<Integer> source = PublishSubject.create(); + + CompletableFuture<Integer> cf = source + .lastOrErrorStage() + .toCompletableFuture(); + + assertTrue(source.hasObservers()); + + cf.complete(1); + + assertTrue(cf.isDone()); + assertFalse(cf.isCompletedExceptionally()); + assertFalse(cf.isCancelled()); + + assertFalse(source.hasObservers()); + + assertEquals((Integer)1, cf.get()); + } + + @Test + public void lastCompletableManualCompleteExceptionallyCancels() throws Exception { + PublishSubject<Integer> source = PublishSubject.create(); + + CompletableFuture<Integer> cf = source + .lastOrErrorStage() + .toCompletableFuture(); + + assertTrue(source.hasObservers()); + + cf.completeExceptionally(new TestException()); + + assertTrue(cf.isDone()); + assertTrue(cf.isCompletedExceptionally()); + assertFalse(cf.isCancelled()); + + assertFalse(source.hasObservers()); + + TestHelper.assertError(cf, TestException.class); + } + + @Test + public void lastError() throws Exception { + CompletableFuture<Integer> cf = Observable.<Integer>error(new TestException()) + .lastOrErrorStage() + .toCompletableFuture(); + + assertTrue(cf.isDone()); + assertTrue(cf.isCompletedExceptionally()); + assertFalse(cf.isCancelled()); + + TestHelper.assertError(cf, TestException.class); + } + + @Test + public void lastSourceIgnoresCancel() throws Throwable { + TestHelper.withErrorTracking(errors -> { + Integer v = new Observable<Integer>() { + @Override + protected void subscribeActual(Observer<? super Integer> observer) { + observer.onSubscribe(Disposable.empty()); + observer.onNext(1); + observer.onComplete(); + observer.onError(new TestException()); + observer.onComplete(); + } + } + .lastOrErrorStage() + .toCompletableFuture() + .get(); + + assertEquals((Integer)1, v); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + }); + } + + @Test + public void lastDoubleOnSubscribe() throws Throwable { + TestHelper.withErrorTracking(errors -> { + Integer v = new Observable<Integer>() { + @Override + protected void subscribeActual(Observer<? super Integer> observer) { + observer.onSubscribe(Disposable.empty()); + observer.onSubscribe(Disposable.empty()); + observer.onNext(1); + observer.onComplete(); + } + } + .lastOrErrorStage() + .toCompletableFuture() + .get(); + + assertEquals((Integer)1, v); + + TestHelper.assertError(errors, 0, ProtocolViolationException.class); + }); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/jdk8/ParallelCollectorTest.java b/src/test/java/io/reactivex/rxjava3/internal/jdk8/ParallelCollectorTest.java new file mode 100644 index 0000000000..9d0fc09f62 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/jdk8/ParallelCollectorTest.java @@ -0,0 +1,306 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.jdk8; + +import static org.junit.Assert.*; + +import java.util.*; +import java.util.concurrent.TimeUnit; +import java.util.function.*; +import java.util.stream.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.parallel.ParallelInvalid; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.processors.BehaviorProcessor; +import io.reactivex.rxjava3.schedulers.Schedulers; +import io.reactivex.rxjava3.subscribers.TestSubscriber; +import io.reactivex.rxjava3.testsupport.*; + +public class ParallelCollectorTest extends RxJavaTest { + + static Set<Integer> set(int count) { + return IntStream.rangeClosed(1, count) + .boxed() + .collect(Collectors.toSet()); + } + + @Test + public void basic() { + TestSubscriberEx<List<Integer>> ts = Flowable.range(1, 5) + .parallel() + .collect(Collectors.toList()) + .subscribeWith(new TestSubscriberEx<>()); + + ts + .assertValueCount(1) + .assertNoErrors() + .assertComplete(); + + assertEquals(5, ts.values().get(0).size()); + assertTrue(ts.values().get(0).containsAll(set(5))); + } + + @Test + public void empty() { + Flowable.empty() + .parallel() + .collect(Collectors.toList()) + .test() + .assertResult(Collections.emptyList()); + } + + @Test + public void error() { + Flowable.error(new TestException()) + .parallel() + .collect(Collectors.toList()) + .test() + .assertFailure(TestException.class); + } + + @Test + public void collectorSupplierCrash() { + Flowable.range(1, 5) + .parallel() + .collect(new Collector<Integer, Integer, Integer>() { + + @Override + public Supplier<Integer> supplier() { + throw new TestException(); + } + + @Override + public BiConsumer<Integer, Integer> accumulator() { + return (a, b) -> { }; + } + + @Override + public BinaryOperator<Integer> combiner() { + return (a, b) -> a + b; + } + + @Override + public Function<Integer, Integer> finisher() { + return a -> a; + } + + @Override + public Set<Characteristics> characteristics() { + return Collections.emptySet(); + } + }) + .test() + .assertFailure(TestException.class); + } + + @Test + public void collectorAccumulatorCrash() { + BehaviorProcessor<Integer> source = BehaviorProcessor.createDefault(1); + + source + .parallel() + .collect(new Collector<Integer, Integer, Integer>() { + + @Override + public Supplier<Integer> supplier() { + return () -> 1; + } + + @Override + public BiConsumer<Integer, Integer> accumulator() { + return (a, b) -> { throw new TestException(); }; + } + + @Override + public BinaryOperator<Integer> combiner() { + return (a, b) -> a + b; + } + + @Override + public Function<Integer, Integer> finisher() { + return a -> a; + } + + @Override + public Set<Characteristics> characteristics() { + return Collections.emptySet(); + } + }) + .test() + .assertFailure(TestException.class); + + assertFalse(source.hasSubscribers()); + } + + @Test + @SuppressUndeliverable + public void collectorCombinerCrash() { + Flowable.range(1, 5) + .parallel() + .collect(new Collector<Integer, Integer, Integer>() { + + @Override + public Supplier<Integer> supplier() { + return () -> 1; + } + + @Override + public BiConsumer<Integer, Integer> accumulator() { + return (a, b) -> { }; + } + + @Override + public BinaryOperator<Integer> combiner() { + return (a, b) -> { throw new TestException(); }; + } + + @Override + public Function<Integer, Integer> finisher() { + return a -> a; + } + + @Override + public Set<Characteristics> characteristics() { + return Collections.emptySet(); + } + }) + .test() + .assertFailure(TestException.class); + } + + @Test + public void collectorFinisherCrash() { + Flowable.range(1, 5) + .parallel() + .collect(new Collector<Integer, Integer, Integer>() { + + @Override + public Supplier<Integer> supplier() { + return () -> 1; + } + + @Override + public BiConsumer<Integer, Integer> accumulator() { + return (a, b) -> { }; + } + + @Override + public BinaryOperator<Integer> combiner() { + return (a, b) -> a + b; + } + + @Override + public Function<Integer, Integer> finisher() { + return a -> { throw new TestException(); }; + } + + @Override + public Set<Characteristics> characteristics() { + return Collections.emptySet(); + } + }) + .test() + .assertFailure(TestException.class); + } + + @Test + public void async() { + for (int i = 1; i < 32; i++) { + TestSubscriber<List<Integer>> ts = Flowable.range(1, 1000) + .parallel(i) + .runOn(Schedulers.computation()) + .collect(Collectors.toList()) + .test() + .withTag("Parallelism: " + i) + .awaitDone(5, TimeUnit.SECONDS) + .assertValueCount(1) + .assertNoErrors() + .assertComplete(); + + assertEquals(1000, ts.values().get(0).size()); + + assertTrue(ts.values().get(0).containsAll(set(1000))); + } + } + + @Test + public void asyncHidden() { + for (int i = 1; i < 32; i++) { + TestSubscriber<List<Integer>> ts = Flowable.range(1, 1000) + .hide() + .parallel(i) + .runOn(Schedulers.computation()) + .collect(Collectors.toList()) + .test() + .withTag("Parallelism: " + i) + .awaitDone(5, TimeUnit.SECONDS) + .assertValueCount(1) + .assertNoErrors() + .assertComplete(); + + assertEquals(1000, ts.values().get(0).size()); + + assertTrue(ts.values().get(0).containsAll(set(1000))); + } + } + + @Test + public void doubleError() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + new ParallelInvalid() + .collect(Collectors.toList()) + .test() + .assertFailure(TestException.class); + + assertFalse(errors.isEmpty()); + for (Throwable ex : errors) { + assertTrue(ex.toString(), ex.getCause() instanceof TestException); + } + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void asyncSum() { + long n = 1_000; + for (int i = 1; i < 32; i++) { + Flowable.rangeLong(1, n) + .parallel(i) + .runOn(Schedulers.computation()) + .collect(Collectors.summingLong(v -> v)) + .test() + .withTag("Parallelism: " + i) + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(n * (n + 1) / 2); + } + } + + @Test + public void asyncSumLong() { + long n = 1_000_000; + Flowable.rangeLong(1, n) + .parallel() + .runOn(Schedulers.computation()) + .collect(Collectors.summingLong(v -> v)) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(n * (n + 1) / 2); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/jdk8/ParallelFlatMapStreamTest.java b/src/test/java/io/reactivex/rxjava3/internal/jdk8/ParallelFlatMapStreamTest.java new file mode 100644 index 0000000000..f1d34491d5 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/jdk8/ParallelFlatMapStreamTest.java @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.jdk8; + +import java.util.stream.Stream; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.parallel.ParallelFlowableTest; + +public class ParallelFlatMapStreamTest extends RxJavaTest { + + @Test + public void subscriberCount() { + ParallelFlowableTest.checkSubscriberCount(Flowable.range(1, 5).parallel() + .flatMapStream(v -> Stream.of(1, 2, 3))); + } + + @Test + public void normal() { + for (int i = 1; i < 32; i++) { + Flowable.range(1, 1000) + .parallel(i) + .flatMapStream(v -> Stream.of(v, v + 1)) + .sequential() + .test() + .withTag("Parallelism: " + i) + .assertValueCount(2000) + .assertNoErrors() + .assertComplete(); + } + } + + @Test + public void none() { + for (int i = 1; i < 32; i++) { + Flowable.range(1, 1000) + .parallel(i) + .flatMapStream(v -> Stream.of()) + .sequential() + .test() + .withTag("Parallelism: " + i) + .assertResult(); + } + } + + @Test + public void mixed() { + for (int i = 1; i < 32; i++) { + Flowable.range(1, 1000) + .parallel(i) + .flatMapStream(v -> v % 2 == 0 ? Stream.of(v) : Stream.of()) + .sequential() + .test() + .withTag("Parallelism: " + i) + .assertValueCount(500) + .assertNoErrors() + .assertComplete(); + } + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/jdk8/ParallelMapOptionalTest.java b/src/test/java/io/reactivex/rxjava3/internal/jdk8/ParallelMapOptionalTest.java new file mode 100644 index 0000000000..ca7385b7f6 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/jdk8/ParallelMapOptionalTest.java @@ -0,0 +1,228 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.jdk8; + +import static org.junit.Assert.*; + +import java.util.*; +import java.util.concurrent.TimeUnit; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.parallel.*; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.schedulers.Schedulers; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class ParallelMapOptionalTest extends RxJavaTest { + + @Test + public void doubleFilter() { + Flowable.range(1, 10) + .parallel() + .mapOptional(Optional::of) + .filter(new Predicate<Integer>() { + @Override + public boolean test(Integer v) throws Exception { + return v % 2 == 0; + } + }) + .filter(new Predicate<Integer>() { + @Override + public boolean test(Integer v) throws Exception { + return v % 3 == 0; + } + }) + .sequential() + .test() + .assertResult(6); + } + + @Test + public void doubleFilterAsync() { + Flowable.range(1, 10) + .parallel() + .runOn(Schedulers.computation()) + .mapOptional(Optional::of) + .filter(new Predicate<Integer>() { + @Override + public boolean test(Integer v) throws Exception { + return v % 2 == 0; + } + }) + .filter(new Predicate<Integer>() { + @Override + public boolean test(Integer v) throws Exception { + return v % 3 == 0; + } + }) + .sequential() + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(6); + } + + @Test + public void doubleError() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + new ParallelInvalid() + .mapOptional(Optional::of) + .sequential() + .test() + .assertFailure(TestException.class); + + assertFalse(errors.isEmpty()); + for (Throwable ex : errors) { + assertTrue(ex.toString(), ex.getCause() instanceof TestException); + } + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void doubleError2() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + new ParallelInvalid() + .mapOptional(Optional::of) + .filter(Functions.alwaysTrue()) + .sequential() + .test() + .assertFailure(TestException.class); + + assertFalse(errors.isEmpty()); + for (Throwable ex : errors) { + assertTrue(ex.toString(), ex.getCause() instanceof TestException); + } + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void error() { + Flowable.error(new TestException()) + .parallel() + .mapOptional(Optional::of) + .sequential() + .test() + .assertFailure(TestException.class); + } + + @Test + public void mapCrash() { + Flowable.just(1) + .parallel() + .mapOptional(v -> { throw new TestException(); }) + .sequential() + .test() + .assertFailure(TestException.class); + } + + @Test + public void mapCrashConditional() { + Flowable.just(1) + .parallel() + .mapOptional(v -> { throw new TestException(); }) + .filter(Functions.alwaysTrue()) + .sequential() + .test() + .assertFailure(TestException.class); + } + + @Test + public void mapCrashConditional2() { + Flowable.just(1) + .parallel() + .runOn(Schedulers.computation()) + .mapOptional(v -> { throw new TestException(); }) + .filter(Functions.alwaysTrue()) + .sequential() + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertFailure(TestException.class); + } + + @Test + public void allNone() { + Flowable.range(1, 1000) + .parallel() + .mapOptional(v -> Optional.empty()) + .sequential() + .test() + .assertResult(); + } + + @Test + public void allNoneConditional() { + Flowable.range(1, 1000) + .parallel() + .mapOptional(v -> Optional.empty()) + .filter(v -> true) + .sequential() + .test() + .assertResult(); + } + + @Test + public void mixed() { + Flowable.range(1, 1000) + .parallel() + .mapOptional(v -> v % 2 == 0 ? Optional.of(v) : Optional.empty()) + .sequential() + .test() + .assertValueCount(500) + .assertNoErrors() + .assertComplete(); + } + + @Test + public void mixedConditional() { + Flowable.range(1, 1000) + .parallel() + .mapOptional(v -> v % 2 == 0 ? Optional.of(v) : Optional.empty()) + .filter(v -> true) + .sequential() + .test() + .assertValueCount(500) + .assertNoErrors() + .assertComplete(); + } + + @Test + public void invalidSubscriberCount() { + TestHelper.checkInvalidParallelSubscribers( + Flowable.range(1, 10).parallel() + .mapOptional(Optional::of) + ); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeParallel( + p -> p.mapOptional(Optional::of) + ); + + TestHelper.checkDoubleOnSubscribeParallel( + p -> p.mapOptional(Optional::of) + .filter(v -> true) + ); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/jdk8/ParallelMapTryOptionalTest.java b/src/test/java/io/reactivex/rxjava3/internal/jdk8/ParallelMapTryOptionalTest.java new file mode 100644 index 0000000000..459b3f884a --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/jdk8/ParallelMapTryOptionalTest.java @@ -0,0 +1,378 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.jdk8; + +import java.util.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.parallel.*; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.testsupport.*; + +public class ParallelMapTryOptionalTest extends RxJavaTest implements Consumer<Object> { + + volatile int calls; + + @Override + public void accept(Object t) throws Exception { + calls++; + } + + @Test + public void mapNoError() { + for (ParallelFailureHandling e : ParallelFailureHandling.values()) { + Flowable.just(1) + .parallel(1) + .mapOptional(Optional::of, e) + .sequential() + .test() + .assertResult(1); + } + } + + @Test + public void mapErrorNoError() { + for (ParallelFailureHandling e : ParallelFailureHandling.values()) { + Flowable.<Integer>error(new TestException()) + .parallel(1) + .mapOptional(Optional::of, e) + .sequential() + .test() + .assertFailure(TestException.class); + } + } + + @Test + public void mapConditionalNoError() { + for (ParallelFailureHandling e : ParallelFailureHandling.values()) { + Flowable.just(1) + .parallel(1) + .mapOptional(Optional::of, e) + .filter(Functions.alwaysTrue()) + .sequential() + .test() + .assertResult(1); + } + } + + @Test + public void mapErrorConditionalNoError() { + for (ParallelFailureHandling e : ParallelFailureHandling.values()) { + Flowable.<Integer>error(new TestException()) + .parallel(1) + .mapOptional(Optional::of, e) + .filter(Functions.alwaysTrue()) + .sequential() + .test() + .assertFailure(TestException.class); + } + } + + @Test + public void mapFailWithError() { + Flowable.range(0, 2) + .parallel(1) + .mapOptional(v -> Optional.of(1 / v), ParallelFailureHandling.ERROR) + .sequential() + .test() + .assertFailure(ArithmeticException.class); + } + + @Test + public void mapFailWithStop() { + Flowable.range(0, 2) + .parallel(1) + .mapOptional(v -> Optional.of(1 / v), ParallelFailureHandling.STOP) + .sequential() + .test() + .assertResult(); + } + + @Test + public void mapFailWithRetry() { + Flowable.range(0, 2) + .parallel(1) + .mapOptional(new Function<Integer, Optional<? extends Integer>>() { + int count; + @Override + public Optional<? extends Integer> apply(Integer v) throws Exception { + if (count++ == 1) { + return Optional.of(-1); + } + return Optional.of(1 / v); + } + }, ParallelFailureHandling.RETRY) + .sequential() + .test() + .assertResult(-1, 1); + } + + @Test + public void mapFailWithRetryLimited() { + Flowable.range(0, 2) + .parallel(1) + .mapOptional(v -> Optional.of(1 / v), new BiFunction<Long, Throwable, ParallelFailureHandling>() { + @Override + public ParallelFailureHandling apply(Long n, Throwable e) throws Exception { + return n < 5 ? ParallelFailureHandling.RETRY : ParallelFailureHandling.SKIP; + } + }) + .sequential() + .test() + .assertResult(1); + } + + @Test + public void mapFailWithSkip() { + Flowable.range(0, 2) + .parallel(1) + .mapOptional(v -> Optional.of(1 / v), ParallelFailureHandling.SKIP) + .sequential() + .test() + .assertResult(1); + } + + @Test + public void mapFailHandlerThrows() { + TestSubscriberEx<Integer> ts = Flowable.range(0, 2) + .parallel(1) + .mapOptional(v -> Optional.of(1 / v), new BiFunction<Long, Throwable, ParallelFailureHandling>() { + @Override + public ParallelFailureHandling apply(Long n, Throwable e) throws Exception { + throw new TestException(); + } + }) + .sequential() + .to(TestHelper.<Integer>testConsumer()) + .assertFailure(CompositeException.class); + + TestHelper.assertCompositeExceptions(ts, ArithmeticException.class, TestException.class); + } + + @Test + public void mapInvalidSource() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + new ParallelInvalid() + .mapOptional(Optional::of, ParallelFailureHandling.ERROR) + .sequential() + .test(); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void mapFailWithErrorConditional() { + Flowable.range(0, 2) + .parallel(1) + .mapOptional(v -> Optional.of(1 / v), ParallelFailureHandling.ERROR) + .filter(Functions.alwaysTrue()) + .sequential() + .test() + .assertFailure(ArithmeticException.class); + } + + @Test + public void mapFailWithStopConditional() { + Flowable.range(0, 2) + .parallel(1) + .mapOptional(v -> Optional.of(1 / v), ParallelFailureHandling.STOP) + .filter(Functions.alwaysTrue()) + .sequential() + .test() + .assertResult(); + } + + @Test + public void mapFailWithRetryConditional() { + Flowable.range(0, 2) + .parallel(1) + .mapOptional(new Function<Integer, Optional<? extends Integer>>() { + int count; + @Override + public Optional<? extends Integer> apply(Integer v) throws Exception { + if (count++ == 1) { + return Optional.of(-1); + } + return Optional.of(1 / v); + } + }, ParallelFailureHandling.RETRY) + .filter(Functions.alwaysTrue()) + .sequential() + .test() + .assertResult(-1, 1); + } + + @Test + public void mapFailWithRetryLimitedConditional() { + Flowable.range(0, 2) + .parallel(1) + .mapOptional(v -> Optional.of(1 / v), new BiFunction<Long, Throwable, ParallelFailureHandling>() { + @Override + public ParallelFailureHandling apply(Long n, Throwable e) throws Exception { + return n < 5 ? ParallelFailureHandling.RETRY : ParallelFailureHandling.SKIP; + } + }) + .filter(Functions.alwaysTrue()) + .sequential() + .test() + .assertResult(1); + } + + @Test + public void mapFailWithSkipConditional() { + Flowable.range(0, 2) + .parallel(1) + .mapOptional(v -> Optional.of(1 / v), ParallelFailureHandling.SKIP) + .filter(Functions.alwaysTrue()) + .sequential() + .test() + .assertResult(1); + } + + @Test + public void mapFailHandlerThrowsConditional() { + TestSubscriberEx<Integer> ts = Flowable.range(0, 2) + .parallel(1) + .mapOptional(v -> Optional.of(1 / v), new BiFunction<Long, Throwable, ParallelFailureHandling>() { + @Override + public ParallelFailureHandling apply(Long n, Throwable e) throws Exception { + throw new TestException(); + } + }) + .filter(Functions.alwaysTrue()) + .sequential() + .to(TestHelper.<Integer>testConsumer()) + .assertFailure(CompositeException.class); + + TestHelper.assertCompositeExceptions(ts, ArithmeticException.class, TestException.class); + } + + @Test + public void mapWrongParallelismConditional() { + TestHelper.checkInvalidParallelSubscribers( + Flowable.just(1).parallel(1) + .mapOptional(Optional::of, ParallelFailureHandling.ERROR) + .filter(Functions.alwaysTrue()) + ); + } + + @Test + public void mapInvalidSourceConditional() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + new ParallelInvalid() + .mapOptional(Optional::of, ParallelFailureHandling.ERROR) + .filter(Functions.alwaysTrue()) + .sequential() + .test(); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void failureHandlingEnum() { + TestHelper.checkEnum(ParallelFailureHandling.class); + } + + @Test + public void allNone() { + Flowable.range(1, 1000) + .parallel() + .mapOptional(v -> Optional.empty(), ParallelFailureHandling.SKIP) + .sequential() + .test() + .assertResult(); + } + + @Test + public void allNoneConditional() { + Flowable.range(1, 1000) + .parallel() + .mapOptional(v -> Optional.empty(), ParallelFailureHandling.SKIP) + .filter(v -> true) + .sequential() + .test() + .assertResult(); + } + + @Test + public void mixed() { + Flowable.range(1, 1000) + .parallel() + .mapOptional(v -> v % 2 == 0 ? Optional.of(v) : Optional.empty(), ParallelFailureHandling.SKIP) + .sequential() + .test() + .assertValueCount(500) + .assertNoErrors() + .assertComplete(); + } + + @Test + public void mixedConditional() { + Flowable.range(1, 1000) + .parallel() + .mapOptional(v -> v % 2 == 0 ? Optional.of(v) : Optional.empty(), ParallelFailureHandling.SKIP) + .filter(v -> true) + .sequential() + .test() + .assertValueCount(500) + .assertNoErrors() + .assertComplete(); + } + + @Test + public void mixedConditional2() { + Flowable.range(1, 1000) + .parallel() + .mapOptional(v -> v % 2 == 0 ? Optional.of(v) : Optional.empty(), ParallelFailureHandling.SKIP) + .filter(v -> v % 4 == 0) + .sequential() + .test() + .assertValueCount(250) + .assertNoErrors() + .assertComplete(); + } + + @Test + public void invalidSubscriberCount() { + TestHelper.checkInvalidParallelSubscribers( + Flowable.range(1, 10).parallel() + .mapOptional(Optional::of, ParallelFailureHandling.SKIP) + ); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeParallel( + p -> p.mapOptional(Optional::of, ParallelFailureHandling.ERROR) + ); + + TestHelper.checkDoubleOnSubscribeParallel( + p -> p.mapOptional(Optional::of, ParallelFailureHandling.ERROR) + .filter(v -> true) + ); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/jdk8/SingleFlattenStreamAsFlowableTckTest.java b/src/test/java/io/reactivex/rxjava3/internal/jdk8/SingleFlattenStreamAsFlowableTckTest.java new file mode 100644 index 0000000000..53c4206510 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/jdk8/SingleFlattenStreamAsFlowableTckTest.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.jdk8; + +import java.util.stream.*; + +import org.reactivestreams.Publisher; +import org.testng.annotations.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.tck.BaseTck; + +@Test +public class SingleFlattenStreamAsFlowableTckTest extends BaseTck<Integer> { + + @Override + public Publisher<Integer> createPublisher(final long elements) { + return + Single.just(1).flattenStreamAsFlowable(v -> IntStream.range(0, (int)elements).boxed()) + ; + } + + @Override + public Publisher<Integer> createFailedPublisher() { + Stream<Integer> stream = Stream.of(1); + stream.forEach(v -> { }); + return Single.just(1).flattenStreamAsFlowable(v -> stream); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/jdk8/SingleFlattenStreamAsFlowableTest.java b/src/test/java/io/reactivex/rxjava3/internal/jdk8/SingleFlattenStreamAsFlowableTest.java new file mode 100644 index 0000000000..6e74b42370 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/jdk8/SingleFlattenStreamAsFlowableTest.java @@ -0,0 +1,441 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.jdk8; + +import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import java.util.Iterator; +import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.*; + +import org.junit.Test; +import org.reactivestreams.Subscription; + +import io.reactivex.rxjava3.annotations.NonNull; +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.subscriptions.BooleanSubscription; +import io.reactivex.rxjava3.operators.QueueFuseable; +import io.reactivex.rxjava3.operators.QueueSubscription; +import io.reactivex.rxjava3.subjects.SingleSubject; +import io.reactivex.rxjava3.subscribers.TestSubscriber; +import io.reactivex.rxjava3.testsupport.*; + +public class SingleFlattenStreamAsFlowableTest extends RxJavaTest { + + @Test + public void successJust() { + Single.just(1) + .flattenStreamAsFlowable(Stream::of) + .test() + .assertResult(1); + } + + @Test + public void successEmpty() { + Single.just(1) + .flattenStreamAsFlowable(v -> Stream.of()) + .test() + .assertResult(); + } + + @Test + public void successMany() { + Single.just(1) + .flattenStreamAsFlowable(v -> Stream.of(2, 3, 4, 5, 6)) + .test() + .assertResult(2, 3, 4, 5, 6); + } + + @Test + public void successManyTake() { + Single.just(1) + .flattenStreamAsFlowable(v -> Stream.of(2, 3, 4, 5, 6)) + .take(3) + .test() + .assertResult(2, 3, 4); + } + + @Test + public void error() throws Throwable { + @SuppressWarnings("unchecked") + Function<? super Integer, Stream<? extends Integer>> f = mock(Function.class); + + Single.<Integer>error(new TestException()) + .flattenStreamAsFlowable(f) + .test() + .assertFailure(TestException.class); + + verify(f, never()).apply(any()); + } + + @Test + public void mapperCrash() { + Single.just(1) + .flattenStreamAsFlowable(v -> { throw new TestException(); }) + .test() + .assertFailure(TestException.class); + } + + @Test + public void dispose() { + TestHelper.checkDisposed(Single.never().flattenStreamAsFlowable(Stream::of)); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeSingleToFlowable(m -> m.flattenStreamAsFlowable(Stream::of)); + } + + @Test + public void badRequest() { + TestHelper.assertBadRequestReported(SingleSubject.create().flattenStreamAsFlowable(Stream::of)); + } + + @Test + public void fusedEmpty() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(); + ts.setInitialFusionMode(QueueFuseable.ANY); + + Single.just(1) + .flattenStreamAsFlowable(v -> Stream.<Integer>of()) + .subscribe(ts); + + ts.assertFuseable() + .assertFusionMode(QueueFuseable.ASYNC) + .assertResult(); + } + + @Test + public void fusedJust() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(); + ts.setInitialFusionMode(QueueFuseable.ANY); + + Single.just(1) + .flattenStreamAsFlowable(v -> Stream.<Integer>of(v)) + .subscribe(ts); + + ts.assertFuseable() + .assertFusionMode(QueueFuseable.ASYNC) + .assertResult(1); + } + + @Test + public void fusedMany() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(); + ts.setInitialFusionMode(QueueFuseable.ANY); + + Single.just(1) + .flattenStreamAsFlowable(v -> Stream.<Integer>of(v, v + 1, v + 2)) + .subscribe(ts); + + ts.assertFuseable() + .assertFusionMode(QueueFuseable.ASYNC) + .assertResult(1, 2, 3); + } + + @Test + public void fusedManyRejected() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(); + ts.setInitialFusionMode(QueueFuseable.SYNC); + + Single.just(1) + .flattenStreamAsFlowable(v -> Stream.<Integer>of(v, v + 1, v + 2)) + .subscribe(ts); + + ts.assertFuseable() + .assertFusionMode(QueueFuseable.NONE) + .assertResult(1, 2, 3); + } + + @Test + public void manyBackpressured() { + Single.just(1) + .flattenStreamAsFlowable(v -> IntStream.rangeClosed(1, 5).boxed()) + .test(0L) + .assertEmpty() + .requestMore(2) + .assertValuesOnly(1, 2) + .requestMore(2) + .assertValuesOnly(1, 2, 3, 4) + .requestMore(1) + .assertResult(1, 2, 3, 4, 5); + } + + @Test + public void manyBackpressured2() { + Single.just(1) + .flattenStreamAsFlowable(v -> IntStream.rangeClosed(1, 5).boxed()) + .rebatchRequests(1) + .test(0L) + .assertEmpty() + .requestMore(2) + .assertValuesOnly(1, 2) + .requestMore(2) + .assertValuesOnly(1, 2, 3, 4) + .requestMore(1) + .assertResult(1, 2, 3, 4, 5); + } + + @Test + public void fusedStreamAvailableLater() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(); + ts.setInitialFusionMode(QueueFuseable.ANY); + + SingleSubject<Integer> ss = SingleSubject.create(); + + ss + .flattenStreamAsFlowable(v -> Stream.<Integer>of(v, v + 1, v + 2)) + .subscribe(ts); + + ts.assertFuseable() + .assertFusionMode(QueueFuseable.ASYNC) + .assertEmpty(); + + ss.onSuccess(1); + + ts + .assertResult(1, 2, 3); + } + + @Test + public void fused() throws Throwable { + AtomicReference<QueueSubscription<Integer>> qsr = new AtomicReference<>(); + + SingleSubject<Integer> ss = SingleSubject.create(); + + ss + .flattenStreamAsFlowable(Stream::of) + .subscribe(new FlowableSubscriber<Integer>() { + + @Override + public void onNext(Integer t) { + } + + @Override + public void onError(Throwable t) { + } + + @Override + public void onComplete() { + } + + @Override + @SuppressWarnings("unchecked") + public void onSubscribe(@NonNull Subscription s) { + qsr.set((QueueSubscription<Integer>)s); + } + }); + + QueueSubscription<Integer> qs = qsr.get(); + + assertEquals(QueueFuseable.ASYNC, qs.requestFusion(QueueFuseable.ASYNC)); + + assertTrue(qs.isEmpty()); + assertNull(qs.poll()); + + ss.onSuccess(1); + + assertFalse(qs.isEmpty()); + assertEquals(1, qs.poll().intValue()); + + assertTrue(qs.isEmpty()); + assertNull(qs.poll()); + + qs.cancel(); + + assertTrue(qs.isEmpty()); + assertNull(qs.poll()); + } + + @Test + public void requestOneByOne() { + TestSubscriber<Integer> ts = new TestSubscriber<>(); + + Single.just(1) + .flattenStreamAsFlowable(v -> Stream.of(1, 2, 3, 4, 5)) + .subscribe(new FlowableSubscriber<Integer>() { + + Subscription upstream; + + @Override + public void onSubscribe(@NonNull Subscription s) { + ts.onSubscribe(new BooleanSubscription()); + upstream = s; + s.request(1); + } + + @Override + public void onNext(Integer t) { + ts.onNext(t); + upstream.request(1); + } + + @Override + public void onError(Throwable t) { + ts.onError(t); + } + + @Override + public void onComplete() { + ts.onComplete(); + } + }); + + ts.assertResult(1, 2, 3, 4, 5); + } + + @Test + public void streamCloseCrash() throws Throwable { + TestHelper.withErrorTracking(errors -> { + Single.just(1) + .flattenStreamAsFlowable(v -> Stream.of(v).onClose(() -> { throw new TestException(); })) + .test() + .assertResult(1); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + }); + } + + @Test + public void hasNextThrowsInDrain() { + @SuppressWarnings("unchecked") + Stream<Integer> stream = mock(Stream.class); + when(stream.iterator()).thenReturn(new Iterator<Integer>() { + + int count; + + @Override + public boolean hasNext() { + if (count++ > 0) { + throw new TestException(); + } + return true; + } + + @Override + public Integer next() { + return 1; + } + }); + + Single.just(1) + .flattenStreamAsFlowable(v -> stream) + .test() + .assertFailure(TestException.class, 1); + } + + @Test + public void nextThrowsInDrain() { + @SuppressWarnings("unchecked") + Stream<Integer> stream = mock(Stream.class); + when(stream.iterator()).thenReturn(new Iterator<Integer>() { + + @Override + public boolean hasNext() { + return true; + } + + @Override + public Integer next() { + throw new TestException(); + } + }); + + Single.just(1) + .flattenStreamAsFlowable(v -> stream) + .test() + .assertFailure(TestException.class); + } + + @Test + public void cancelAfterHasNextInDrain() { + @SuppressWarnings("unchecked") + Stream<Integer> stream = mock(Stream.class); + + TestSubscriber<Integer> ts = new TestSubscriber<>(); + + when(stream.iterator()).thenReturn(new Iterator<Integer>() { + + int count; + + @Override + public boolean hasNext() { + if (count++ > 0) { + ts.cancel(); + } + return true; + } + + @Override + public Integer next() { + return 1; + } + }); + + Single.just(1) + .flattenStreamAsFlowable(v -> stream) + .subscribeWith(ts) + .assertValuesOnly(1); + } + + @Test + public void cancelAfterNextInDrain() { + @SuppressWarnings("unchecked") + Stream<Integer> stream = mock(Stream.class); + + TestSubscriber<Integer> ts = new TestSubscriber<>(); + + when(stream.iterator()).thenReturn(new Iterator<Integer>() { + + @Override + public boolean hasNext() { + return true; + } + + @Override + public Integer next() { + ts.cancel(); + return 1; + } + }); + + Single.just(1) + .flattenStreamAsFlowable(v -> stream) + .subscribeWith(ts) + .assertEmpty(); + } + + @Test + public void requestSuccessRace() { + for (int i = 0; i < TestHelper.RACE_LONG_LOOPS; i++) { + SingleSubject<Integer> ss = SingleSubject.create(); + + TestSubscriber<Integer> ts = new TestSubscriber<>(0L); + + ss.flattenStreamAsFlowable(Stream::of) + .subscribe(ts); + + Runnable r1 = () -> ss.onSuccess(1); + Runnable r2 = () -> ts.request(1); + + TestHelper.race(r1, r2); + + ts.assertResult(1); + } + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/jdk8/SingleFlattenStreamAsObservableTest.java b/src/test/java/io/reactivex/rxjava3/internal/jdk8/SingleFlattenStreamAsObservableTest.java new file mode 100644 index 0000000000..2c50634823 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/jdk8/SingleFlattenStreamAsObservableTest.java @@ -0,0 +1,419 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.jdk8; + +import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import java.util.Iterator; +import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Stream; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.operators.QueueDisposable; +import io.reactivex.rxjava3.operators.QueueFuseable; +import io.reactivex.rxjava3.subjects.SingleSubject; +import io.reactivex.rxjava3.testsupport.*; + +public class SingleFlattenStreamAsObservableTest extends RxJavaTest { + + @Test + public void successJust() { + Single.just(1) + .flattenStreamAsObservable(Stream::of) + .test() + .assertResult(1); + } + + @Test + public void successEmpty() { + Single.just(1) + .flattenStreamAsObservable(v -> Stream.of()) + .test() + .assertResult(); + } + + @Test + public void successMany() { + Single.just(1) + .flattenStreamAsObservable(v -> Stream.of(2, 3, 4, 5, 6)) + .test() + .assertResult(2, 3, 4, 5, 6); + } + + @Test + public void successManyTake() { + Single.just(1) + .flattenStreamAsObservable(v -> Stream.of(2, 3, 4, 5, 6)) + .take(3) + .test() + .assertResult(2, 3, 4); + } + + @Test + public void error() throws Throwable { + @SuppressWarnings("unchecked") + Function<? super Integer, Stream<? extends Integer>> f = mock(Function.class); + + Single.<Integer>error(new TestException()) + .flattenStreamAsObservable(f) + .test() + .assertFailure(TestException.class); + + verify(f, never()).apply(any()); + } + + @Test + public void mapperCrash() { + Single.just(1) + .flattenStreamAsObservable(v -> { throw new TestException(); }) + .test() + .assertFailure(TestException.class); + } + + @Test + public void dispose() { + TestHelper.checkDisposed(Single.never().flattenStreamAsObservable(Stream::of)); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeSingleToObservable(m -> m.flattenStreamAsObservable(Stream::of)); + } + + @Test + public void fusedEmpty() { + TestObserverEx<Integer> to = new TestObserverEx<>(); + to.setInitialFusionMode(QueueFuseable.ANY); + + Single.just(1) + .flattenStreamAsObservable(v -> Stream.<Integer>of()) + .subscribe(to); + + to.assertFuseable() + .assertFusionMode(QueueFuseable.ASYNC) + .assertResult(); + } + + @Test + public void fusedJust() { + TestObserverEx<Integer> to = new TestObserverEx<>(); + to.setInitialFusionMode(QueueFuseable.ANY); + + Single.just(1) + .flattenStreamAsObservable(v -> Stream.<Integer>of(v)) + .subscribe(to); + + to.assertFuseable() + .assertFusionMode(QueueFuseable.ASYNC) + .assertResult(1); + } + + @Test + public void fusedMany() { + TestObserverEx<Integer> to = new TestObserverEx<>(); + to.setInitialFusionMode(QueueFuseable.ANY); + + Single.just(1) + .flattenStreamAsObservable(v -> Stream.<Integer>of(v, v + 1, v + 2)) + .subscribe(to); + + to.assertFuseable() + .assertFusionMode(QueueFuseable.ASYNC) + .assertResult(1, 2, 3); + } + + @Test + public void fusedManyRejected() { + TestObserverEx<Integer> to = new TestObserverEx<>(); + to.setInitialFusionMode(QueueFuseable.SYNC); + + Single.just(1) + .flattenStreamAsObservable(v -> Stream.<Integer>of(v, v + 1, v + 2)) + .subscribe(to); + + to.assertFuseable() + .assertFusionMode(QueueFuseable.NONE) + .assertResult(1, 2, 3); + } + + @Test + public void fusedStreamAvailableLater() { + TestObserverEx<Integer> to = new TestObserverEx<>(); + to.setInitialFusionMode(QueueFuseable.ANY); + + SingleSubject<Integer> ss = SingleSubject.create(); + + ss + .flattenStreamAsObservable(v -> Stream.<Integer>of(v, v + 1, v + 2)) + .subscribe(to); + + to.assertFuseable() + .assertFusionMode(QueueFuseable.ASYNC) + .assertEmpty(); + + ss.onSuccess(1); + + to + .assertResult(1, 2, 3); + } + + @Test + public void fused() throws Throwable { + AtomicReference<QueueDisposable<Integer>> qdr = new AtomicReference<>(); + + SingleSubject<Integer> ss = SingleSubject.create(); + + ss + .flattenStreamAsObservable(Stream::of) + .subscribe(new Observer<Integer>() { + + @Override + public void onNext(Integer t) { + } + + @Override + public void onError(Throwable t) { + } + + @Override + public void onComplete() { + } + + @Override + @SuppressWarnings("unchecked") + public void onSubscribe(Disposable d) { + qdr.set((QueueDisposable<Integer>)d); + } + }); + + QueueDisposable<Integer> qd = qdr.get(); + + assertEquals(QueueFuseable.ASYNC, qd.requestFusion(QueueFuseable.ASYNC)); + + assertTrue(qd.isEmpty()); + assertNull(qd.poll()); + + ss.onSuccess(1); + + assertFalse(qd.isEmpty()); + assertEquals(1, qd.poll().intValue()); + + assertTrue(qd.isEmpty()); + assertNull(qd.poll()); + + qd.dispose(); + + assertTrue(qd.isEmpty()); + assertNull(qd.poll()); + } + + @Test + public void fused2() throws Throwable { + AtomicReference<QueueDisposable<Integer>> qdr = new AtomicReference<>(); + + SingleSubject<Integer> ss = SingleSubject.create(); + + ss + .flattenStreamAsObservable(v -> Stream.of(v, v + 1)) + .subscribe(new Observer<Integer>() { + + @Override + public void onNext(Integer t) { + } + + @Override + public void onError(Throwable t) { + } + + @Override + public void onComplete() { + } + + @Override + @SuppressWarnings("unchecked") + public void onSubscribe(Disposable d) { + qdr.set((QueueDisposable<Integer>)d); + } + }); + + QueueDisposable<Integer> qd = qdr.get(); + + assertEquals(QueueFuseable.ASYNC, qd.requestFusion(QueueFuseable.ASYNC)); + + assertTrue(qd.isEmpty()); + assertNull(qd.poll()); + + ss.onSuccess(1); + + assertFalse(qd.isEmpty()); + assertEquals(1, qd.poll().intValue()); + + assertFalse(qd.isEmpty()); + assertEquals(2, qd.poll().intValue()); + + assertTrue(qd.isEmpty()); + assertNull(qd.poll()); + + qd.dispose(); + + assertTrue(qd.isEmpty()); + assertNull(qd.poll()); + } + + @Test + public void streamCloseCrash() throws Throwable { + TestHelper.withErrorTracking(errors -> { + Single.just(1) + .flattenStreamAsObservable(v -> Stream.of(v).onClose(() -> { throw new TestException(); })) + .test() + .assertResult(1); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + }); + } + + @Test + public void hasNextThrowsInDrain() { + @SuppressWarnings("unchecked") + Stream<Integer> stream = mock(Stream.class); + when(stream.iterator()).thenReturn(new Iterator<Integer>() { + + int count; + + @Override + public boolean hasNext() { + if (count++ > 0) { + throw new TestException(); + } + return true; + } + + @Override + public Integer next() { + return 1; + } + }); + + Single.just(1) + .flattenStreamAsObservable(v -> stream) + .test() + .assertFailure(TestException.class, 1); + } + + @Test + public void nextThrowsInDrain() { + @SuppressWarnings("unchecked") + Stream<Integer> stream = mock(Stream.class); + when(stream.iterator()).thenReturn(new Iterator<Integer>() { + + @Override + public boolean hasNext() { + return true; + } + + @Override + public Integer next() { + throw new TestException(); + } + }); + + Single.just(1) + .flattenStreamAsObservable(v -> stream) + .test() + .assertFailure(TestException.class); + } + + @Test + public void cancelAfterHasNextInDrain() { + @SuppressWarnings("unchecked") + Stream<Integer> stream = mock(Stream.class); + + TestObserver<Integer> to = new TestObserver<>(); + + when(stream.iterator()).thenReturn(new Iterator<Integer>() { + + int count; + + @Override + public boolean hasNext() { + if (count++ > 0) { + to.dispose(); + } + return true; + } + + @Override + public Integer next() { + return 1; + } + }); + + Single.just(1) + .flattenStreamAsObservable(v -> stream) + .subscribeWith(to) + .assertValuesOnly(1); + } + + @Test + public void cancelAfterNextInDrain() { + @SuppressWarnings("unchecked") + Stream<Integer> stream = mock(Stream.class); + + TestObserver<Integer> to = new TestObserver<>(); + + when(stream.iterator()).thenReturn(new Iterator<Integer>() { + + @Override + public boolean hasNext() { + return true; + } + + @Override + public Integer next() { + to.dispose(); + return 1; + } + }); + + Single.just(1) + .flattenStreamAsObservable(v -> stream) + .subscribeWith(to) + .assertEmpty(); + } + + @Test + public void cancelSuccessRace() { + for (int i = 0; i < TestHelper.RACE_LONG_LOOPS; i++) { + SingleSubject<Integer> ss = SingleSubject.create(); + + TestObserver<Integer> to = new TestObserver<>(); + + ss.flattenStreamAsObservable(Stream::of) + .subscribe(to); + + Runnable r1 = () -> ss.onSuccess(1); + Runnable r2 = () -> to.dispose(); + + TestHelper.race(r1, r2); + } + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/jdk8/SingleFromCompletionStageTest.java b/src/test/java/io/reactivex/rxjava3/internal/jdk8/SingleFromCompletionStageTest.java new file mode 100644 index 0000000000..c62996e586 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/jdk8/SingleFromCompletionStageTest.java @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.jdk8; + +import java.util.concurrent.CompletableFuture; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class SingleFromCompletionStageTest extends RxJavaTest { + + @Test + public void syncSuccess() { + Single.fromCompletionStage(CompletableFuture.completedFuture(1)) + .test() + .assertResult(1); + } + + @Test + public void syncFailure() { + CompletableFuture<Integer> cf = new CompletableFuture<>(); + cf.completeExceptionally(new TestException()); + + Single.fromCompletionStage(cf) + .test() + .assertFailure(TestException.class); + } + + @Test + public void syncNull() { + Single.fromCompletionStage(CompletableFuture.<Integer>completedFuture(null)) + .test() + .assertFailure(NullPointerException.class); + } + + @Test + public void dispose() { + CompletableFuture<Integer> cf = new CompletableFuture<>(); + + TestObserver<Integer> to = Single.fromCompletionStage(cf) + .test(); + + to.assertEmpty(); + + to.dispose(); + + cf.complete(1); + + to.assertEmpty(); + } + + @Test + public void dispose2() { + TestHelper.checkDisposed(Single.fromCompletionStage(new CompletableFuture<>())); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/jdk8/SingleMapOptionalTest.java b/src/test/java/io/reactivex/rxjava3/internal/jdk8/SingleMapOptionalTest.java new file mode 100644 index 0000000000..56bc5dbae5 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/jdk8/SingleMapOptionalTest.java @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.jdk8; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import java.util.Optional; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class SingleMapOptionalTest extends RxJavaTest { + + @Test + public void successSuccess() { + Single.just(1) + .mapOptional(Optional::of) + .test() + .assertResult(1); + } + + @Test + public void successEmpty() { + Single.just(1) + .mapOptional(v -> Optional.empty()) + .test() + .assertResult(); + } + + @Test + public void error() throws Throwable { + @SuppressWarnings("unchecked") + Function<? super Integer, Optional<? extends Integer>> f = mock(Function.class); + + Single.<Integer>error(new TestException()) + .mapOptional(f) + .test() + .assertFailure(TestException.class); + + verify(f, never()).apply(any()); + } + + @Test + public void mapperCrash() { + Single.just(1) + .mapOptional(v -> { throw new TestException(); }) + .test() + .assertFailure(TestException.class); + } + + @Test + public void dispose() { + TestHelper.checkDisposed(Single.never().mapOptional(Optional::of)); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeSingleToMaybe(m -> m.mapOptional(Optional::of)); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/jdk8/SingleToCompletionStageTest.java b/src/test/java/io/reactivex/rxjava3/internal/jdk8/SingleToCompletionStageTest.java new file mode 100644 index 0000000000..2709b6ee40 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/jdk8/SingleToCompletionStageTest.java @@ -0,0 +1,153 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.jdk8; + +import static org.junit.Assert.*; + +import java.util.concurrent.CompletableFuture; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.subjects.SingleSubject; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class SingleToCompletionStageTest extends RxJavaTest { + + @Test + public void just() throws Exception { + Integer v = Single.just(1) + .toCompletionStage() + .toCompletableFuture() + .get(); + + assertEquals((Integer)1, v); + } + + @Test + public void completableFutureCancels() throws Exception { + SingleSubject<Integer> source = SingleSubject.create(); + + CompletableFuture<Integer> cf = source + .toCompletionStage() + .toCompletableFuture(); + + assertTrue(source.hasObservers()); + + cf.cancel(true); + + assertTrue(cf.isCancelled()); + + assertFalse(source.hasObservers()); + } + + @Test + public void completableManualCompleteCancels() throws Exception { + SingleSubject<Integer> source = SingleSubject.create(); + + CompletableFuture<Integer> cf = source + .toCompletionStage() + .toCompletableFuture(); + + assertTrue(source.hasObservers()); + + cf.complete(1); + + assertTrue(cf.isDone()); + assertFalse(cf.isCompletedExceptionally()); + assertFalse(cf.isCancelled()); + + assertFalse(source.hasObservers()); + + assertEquals((Integer)1, cf.get()); + } + + @Test + public void completableManualCompleteExceptionallyCancels() throws Exception { + SingleSubject<Integer> source = SingleSubject.create(); + + CompletableFuture<Integer> cf = source + .toCompletionStage() + .toCompletableFuture(); + + assertTrue(source.hasObservers()); + + cf.completeExceptionally(new TestException()); + + assertTrue(cf.isDone()); + assertTrue(cf.isCompletedExceptionally()); + assertFalse(cf.isCancelled()); + + assertFalse(source.hasObservers()); + + TestHelper.assertError(cf, TestException.class); + } + + @Test + public void error() throws Exception { + CompletableFuture<Integer> cf = Single.<Integer>error(new TestException()) + .toCompletionStage() + .toCompletableFuture(); + + assertTrue(cf.isDone()); + assertTrue(cf.isCompletedExceptionally()); + assertFalse(cf.isCancelled()); + + TestHelper.assertError(cf, TestException.class); + } + + @Test + public void sourceIgnoresCancel() throws Throwable { + TestHelper.withErrorTracking(errors -> { + Integer v = new Single<Integer>() { + @Override + protected void subscribeActual(SingleObserver<? super Integer> observer) { + observer.onSubscribe(Disposable.empty()); + observer.onSuccess(1); + observer.onError(new TestException()); + } + } + .toCompletionStage() + .toCompletableFuture() + .get(); + + assertEquals((Integer)1, v); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + }); + } + + @Test + public void doubleOnSubscribe() throws Throwable { + TestHelper.withErrorTracking(errors -> { + Integer v = new Single<Integer>() { + @Override + protected void subscribeActual(SingleObserver<? super Integer> observer) { + observer.onSubscribe(Disposable.empty()); + observer.onSubscribe(Disposable.empty()); + observer.onSuccess(1); + } + } + .toCompletionStage() + .toCompletableFuture() + .get(); + + assertEquals((Integer)1, v); + + TestHelper.assertError(errors, 0, ProtocolViolationException.class); + }); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/observers/BasicFuseableObserverTest.java b/src/test/java/io/reactivex/rxjava3/internal/observers/BasicFuseableObserverTest.java new file mode 100644 index 0000000000..f48d0a574e --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/observers/BasicFuseableObserverTest.java @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.observers; + +import io.reactivex.rxjava3.disposables.Disposable; +import org.junit.Test; + +import io.reactivex.rxjava3.annotations.Nullable; +import io.reactivex.rxjava3.core.RxJavaTest; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.testsupport.TestObserverEx; + +public class BasicFuseableObserverTest extends RxJavaTest { + + @Test(expected = UnsupportedOperationException.class) + public void offer() { + TestObserverEx<Integer> to = new TestObserverEx<>(); + BasicFuseableObserver<Integer, Integer> o = new BasicFuseableObserver<Integer, Integer>(to) { + @Nullable + @Override + public Integer poll() throws Exception { + return null; + } + + @Override + public int requestFusion(int mode) { + return 0; + } + + @Override + public void onNext(Integer value) { + } + + @Override + protected boolean beforeDownstream() { + return false; + } + }; + + o.onSubscribe(Disposable.disposed()); + + to.assertNotSubscribed(); + + o.offer(1); + } + + @Test(expected = UnsupportedOperationException.class) + public void offer2() { + BasicFuseableObserver<Integer, Integer> o = new BasicFuseableObserver<Integer, Integer>(new TestObserver<>()) { + @Nullable + @Override + public Integer poll() throws Exception { + return null; + } + + @Override + public int requestFusion(int mode) { + return 0; + } + + @Override + public void onNext(Integer value) { + } + }; + + o.offer(1, 2); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/observers/BasicQueueDisposableTest.java b/src/test/java/io/reactivex/rxjava3/internal/observers/BasicQueueDisposableTest.java new file mode 100644 index 0000000000..d9bf994746 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/observers/BasicQueueDisposableTest.java @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.observers; + +import org.junit.Test; + +import io.reactivex.rxjava3.annotations.Nullable; +import io.reactivex.rxjava3.core.RxJavaTest; + +public class BasicQueueDisposableTest extends RxJavaTest { + + BasicQueueDisposable<Integer> q = new BasicQueueDisposable<Integer>() { + + @Override + public boolean isDisposed() { + return false; + } + + @Override + public void dispose() { + + } + + @Nullable + @Override + public Integer poll() throws Exception { + return null; + } + + @Override + public boolean isEmpty() { + return false; + } + + @Override + public void clear() { + + } + + @Override + public int requestFusion(int mode) { + return 0; + } + }; + + @Test(expected = UnsupportedOperationException.class) + public void offer() { + q.offer(1); + } + + @Test(expected = UnsupportedOperationException.class) + public void offer2() { + q.offer(1, 2); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/observers/BlockingFirstObserverTest.java b/src/test/java/io/reactivex/rxjava3/internal/observers/BlockingFirstObserverTest.java new file mode 100644 index 0000000000..3a65016574 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/observers/BlockingFirstObserverTest.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.observers; + +import static org.junit.Assert.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.RxJavaTest; +import io.reactivex.rxjava3.disposables.*; +import io.reactivex.rxjava3.exceptions.TestException; + +public class BlockingFirstObserverTest extends RxJavaTest { + + @Test + public void firstValueOnly() { + BlockingFirstObserver<Integer> bf = new BlockingFirstObserver<>(); + Disposable d = Disposable.empty(); + bf.onSubscribe(d); + + bf.onNext(1); + + assertTrue(d.isDisposed()); + + assertEquals(1, bf.value.intValue()); + assertEquals(0, bf.getCount()); + + bf.onNext(2); + + assertEquals(1, bf.value.intValue()); + assertEquals(0, bf.getCount()); + + bf.onError(new TestException()); + assertEquals(1, bf.value.intValue()); + assertNull(bf.error); + assertEquals(0, bf.getCount()); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/observers/BlockingMultiObserverTest.java b/src/test/java/io/reactivex/rxjava3/internal/observers/BlockingMultiObserverTest.java new file mode 100644 index 0000000000..0ea5b468ac --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/observers/BlockingMultiObserverTest.java @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.observers; + +import static org.junit.Assert.*; + +import java.util.concurrent.TimeUnit; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.RxJavaTest; +import io.reactivex.rxjava3.disposables.*; +import io.reactivex.rxjava3.schedulers.Schedulers; + +public class BlockingMultiObserverTest extends RxJavaTest { + + @Test + public void dispose() { + BlockingMultiObserver<Integer> bmo = new BlockingMultiObserver<>(); + bmo.dispose(); + + Disposable d = Disposable.empty(); + + bmo.onSubscribe(d); + } + + @Test + public void blockingGetDefault() { + final BlockingMultiObserver<Integer> bmo = new BlockingMultiObserver<>(); + + Schedulers.single().scheduleDirect(new Runnable() { + @Override + public void run() { + bmo.onSuccess(1); + } + }, 100, TimeUnit.MILLISECONDS); + + assertEquals(1, bmo.blockingGet(0).intValue()); + } + + @Test + public void blockingAwait() { + final BlockingMultiObserver<Integer> bmo = new BlockingMultiObserver<>(); + + Schedulers.single().scheduleDirect(new Runnable() { + @Override + public void run() { + bmo.onSuccess(1); + } + }, 100, TimeUnit.MILLISECONDS); + + assertTrue(bmo.blockingAwait(1, TimeUnit.MINUTES)); + } + + @Test + public void blockingGetDefaultInterrupt() { + final BlockingMultiObserver<Integer> bmo = new BlockingMultiObserver<>(); + + Thread.currentThread().interrupt(); + try { + bmo.blockingGet(0); + fail("Should have thrown"); + } catch (RuntimeException ex) { + assertTrue(ex.getCause() instanceof InterruptedException); + } finally { + Thread.interrupted(); + } + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/observers/BlockingObserverTest.java b/src/test/java/io/reactivex/rxjava3/internal/observers/BlockingObserverTest.java new file mode 100644 index 0000000000..cbf8b7ce7c --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/observers/BlockingObserverTest.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.observers; + +import static org.junit.Assert.*; + +import java.util.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.RxJavaTest; + +public class BlockingObserverTest extends RxJavaTest { + + @Test + public void dispose() { + Queue<Object> q = new ArrayDeque<>(); + + BlockingObserver<Object> bo = new BlockingObserver<>(q); + + bo.dispose(); + + assertEquals(BlockingObserver.TERMINATED, q.poll()); + + bo.dispose(); + + assertNull(q.poll()); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/observers/CallbackCompletableObserverTest.java b/src/test/java/io/reactivex/rxjava3/internal/observers/CallbackCompletableObserverTest.java new file mode 100644 index 0000000000..46c4cc4463 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/observers/CallbackCompletableObserverTest.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.observers; + +import static org.junit.Assert.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.RxJavaTest; +import io.reactivex.rxjava3.internal.functions.Functions; + +public final class CallbackCompletableObserverTest extends RxJavaTest { + + @Test + public void emptyActionShouldReportNoCustomOnError() { + CallbackCompletableObserver o = new CallbackCompletableObserver(Functions.ON_ERROR_MISSING, Functions.EMPTY_ACTION); + + assertFalse(o.hasCustomOnError()); + } + + @Test + public void customOnErrorShouldReportCustomOnError() { + CallbackCompletableObserver o = new CallbackCompletableObserver(Functions.<Throwable>emptyConsumer(), + Functions.EMPTY_ACTION); + + assertTrue(o.hasCustomOnError()); + } + +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/observers/CompletableConsumersTest.java b/src/test/java/io/reactivex/rxjava3/internal/observers/CompletableConsumersTest.java new file mode 100644 index 0000000000..eb17ead3db --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/observers/CompletableConsumersTest.java @@ -0,0 +1,221 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +/* + * Copyright 2016-2019 David Karnok + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.reactivex.rxjava3.internal.observers; + +import static org.junit.Assert.*; + +import java.io.IOException; +import java.util.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.*; +import io.reactivex.rxjava3.exceptions.CompositeException; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.observers.LambdaConsumerIntrospection; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.subjects.CompletableSubject; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class CompletableConsumersTest implements Consumer<Object>, Action { + + final CompositeDisposable composite = new CompositeDisposable(); + + final CompletableSubject processor = CompletableSubject.create(); + + final List<Object> events = new ArrayList<>(); + + @Override + public void run() throws Exception { + events.add("OnComplete"); + } + + @Override + public void accept(Object t) throws Exception { + events.add(t); + } + + @Test + public void onErrorNormal() { + + processor.subscribe(this, this, composite); + + assertTrue(composite.size() > 0); + + assertTrue(events.toString(), events.isEmpty()); + + processor.onComplete(); + + assertEquals(0, composite.size()); + + assertEquals(Arrays.<Object>asList("OnComplete"), events); + + } + + @Test + public void onErrorError() { + + Disposable d = processor.subscribe(this, this, composite); + + assertTrue(d.getClass().toString(), ((LambdaConsumerIntrospection)d).hasCustomOnError()); + + assertTrue(composite.size() > 0); + + assertTrue(events.toString(), events.isEmpty()); + + processor.onError(new IOException()); + + assertTrue(events.toString(), events.get(0) instanceof IOException); + + assertEquals(0, composite.size()); + } + + @Test + public void onCompleteNormal() { + + processor.subscribe(this, this, composite); + + assertTrue(composite.size() > 0); + + assertTrue(events.toString(), events.isEmpty()); + + processor.onComplete(); + + assertEquals(0, composite.size()); + + assertEquals(Arrays.<Object>asList("OnComplete"), events); + + } + + @Test + public void onCompleteError() { + + processor.subscribe(this, this, composite); + + assertTrue(composite.size() > 0); + + assertTrue(events.toString(), events.isEmpty()); + + processor.onError(new IOException()); + + assertTrue(events.toString(), events.get(0) instanceof IOException); + + assertEquals(0, composite.size()); + } + + @Test + public void onCompleteDispose() { + + Disposable d = processor.subscribe(this, this, composite); + + assertTrue(composite.size() > 0); + + assertTrue(events.toString(), events.isEmpty()); + + assertFalse(d.isDisposed()); + + d.dispose(); + d.dispose(); + + assertTrue(d.isDisposed()); + + assertEquals(0, composite.size()); + + assertFalse(processor.hasObservers()); + } + + @Test + public void onErrorCrash() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + processor.subscribe(this, t -> { + throw new IOException(t); + }, composite); + + processor.onError(new IllegalArgumentException()); + + assertTrue(events.toString(), events.isEmpty()); + + TestHelper.assertError(errors, 0, CompositeException.class); + List<Throwable> inners = TestHelper.compositeList(errors.get(0)); + TestHelper.assertError(inners, 0, IllegalArgumentException.class); + TestHelper.assertError(inners, 1, IOException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void onCompleteCrash() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + processor.subscribe(new Action() { + @Override + public void run() throws Exception { + throw new IOException(); + } + }, this, composite); + + processor.onComplete(); + + assertTrue(events.toString(), events.isEmpty()); + + TestHelper.assertUndeliverable(errors, 0, IOException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void badSource() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + new Completable() { + @Override + protected void subscribeActual( + CompletableObserver observer) { + observer.onSubscribe(Disposable.empty()); + observer.onComplete(); + + observer.onSubscribe(Disposable.empty()); + observer.onComplete(); + observer.onError(new IOException()); + } + }.subscribe(this, this, composite); + + assertEquals(Arrays.<Object>asList("OnComplete"), events); + + TestHelper.assertUndeliverable(errors, 0, IOException.class); + } finally { + RxJavaPlugins.reset(); + } + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/observers/ConsumerSingleObserverTest.java b/src/test/java/io/reactivex/rxjava3/internal/observers/ConsumerSingleObserverTest.java new file mode 100644 index 0000000000..f6ebdb498f --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/observers/ConsumerSingleObserverTest.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.observers; + +import static org.junit.Assert.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.RxJavaTest; +import io.reactivex.rxjava3.internal.functions.Functions; + +public final class ConsumerSingleObserverTest extends RxJavaTest { + + @Test + public void onErrorMissingShouldReportNoCustomOnError() { + ConsumerSingleObserver<Integer> o = new ConsumerSingleObserver<>(Functions.<Integer>emptyConsumer(), + Functions.ON_ERROR_MISSING); + + assertFalse(o.hasCustomOnError()); + } + + @Test + public void customOnErrorShouldReportCustomOnError() { + ConsumerSingleObserver<Integer> o = new ConsumerSingleObserver<>(Functions.<Integer>emptyConsumer(), + Functions.<Throwable>emptyConsumer()); + + assertTrue(o.hasCustomOnError()); + } + +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/observers/DeferredScalarDisposableTest.java b/src/test/java/io/reactivex/rxjava3/internal/observers/DeferredScalarDisposableTest.java new file mode 100644 index 0000000000..19c5b7db27 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/observers/DeferredScalarDisposableTest.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.observers; + +import static org.junit.Assert.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.RxJavaTest; +import io.reactivex.rxjava3.observers.TestObserver; + +public class DeferredScalarDisposableTest extends RxJavaTest { + + @Test + public void tryDispose() { + TestObserver<Integer> to = new TestObserver<>(); + + DeferredScalarDisposable<Integer> d = new DeferredScalarDisposable<>(to); + to.onSubscribe(d); + + assertTrue(d.tryDispose()); + assertFalse(d.tryDispose()); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/observers/DeferredScalarObserverTest.java b/src/test/java/io/reactivex/rxjava3/internal/observers/DeferredScalarObserverTest.java new file mode 100644 index 0000000000..165de9b467 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/observers/DeferredScalarObserverTest.java @@ -0,0 +1,513 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.observers; + +import static org.junit.Assert.*; + +import java.util.List; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.*; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.operators.QueueDisposable; +import io.reactivex.rxjava3.operators.QueueFuseable; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.testsupport.*; + +public class DeferredScalarObserverTest extends RxJavaTest { + + static final class TakeFirst extends DeferredScalarObserver<Integer, Integer> { + + private static final long serialVersionUID = -2793723002312330530L; + + TakeFirst(Observer<? super Integer> downstream) { + super(downstream); + } + + @Override + public void onNext(Integer value) { + upstream.dispose(); + complete(value); + complete(value); + } + + } + + @Test + public void normal() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + TestObserver<Integer> to = new TestObserver<>(); + + TakeFirst source = new TakeFirst(to); + + source.onSubscribe(Disposable.empty()); + + Disposable d = Disposable.empty(); + source.onSubscribe(d); + + assertTrue(d.isDisposed()); + + source.onNext(1); + + to.assertResult(1); + + TestHelper.assertError(errors, 0, ProtocolViolationException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void error() { + TestObserver<Integer> to = new TestObserver<>(); + + TakeFirst source = new TakeFirst(to); + + source.onSubscribe(Disposable.empty()); + source.onError(new TestException()); + + to.assertFailure(TestException.class); + } + + @Test + public void complete() { + TestObserver<Integer> to = new TestObserver<>(); + + TakeFirst source = new TakeFirst(to); + + source.onSubscribe(Disposable.empty()); + source.onComplete(); + + to.assertResult(); + } + + @Test + public void dispose() { + TestObserver<Integer> to = new TestObserver<>(); + + TakeFirst source = new TakeFirst(to); + + Disposable d = Disposable.empty(); + + source.onSubscribe(d); + + assertFalse(d.isDisposed()); + + to.dispose(); + + assertTrue(d.isDisposed()); + + assertTrue(source.isDisposed()); + } + + @Test + public void fused() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + TestObserverEx<Integer> to = new TestObserverEx<>(QueueFuseable.ANY); + + TakeFirst source = new TakeFirst(to); + + Disposable d = Disposable.empty(); + + source.onSubscribe(d); + + to.assertFuseable(); + to.assertFusionMode(QueueFuseable.ASYNC); + + source.onNext(1); + source.onNext(1); + source.onError(new TestException()); + source.onComplete(); + + assertTrue(d.isDisposed()); + + to.assertResult(1); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void fusedReject() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + TestObserverEx<Integer> to = new TestObserverEx<>(QueueFuseable.SYNC); + + TakeFirst source = new TakeFirst(to); + + Disposable d = Disposable.empty(); + + source.onSubscribe(d); + + to.assertFuseable(); + to.assertFusionMode(QueueFuseable.NONE); + + source.onNext(1); + source.onNext(1); + source.onError(new TestException()); + source.onComplete(); + + assertTrue(d.isDisposed()); + + to.assertResult(1); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + static final class TakeLast extends DeferredScalarObserver<Integer, Integer> { + + private static final long serialVersionUID = -2793723002312330530L; + + TakeLast(Observer<? super Integer> downstream) { + super(downstream); + } + + @Override + public void onNext(Integer value) { + this.value = value; + } + + } + + @Test + public void nonfusedTerminateMore() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + TestObserverEx<Integer> to = new TestObserverEx<>(QueueFuseable.NONE); + + TakeLast source = new TakeLast(to); + + Disposable d = Disposable.empty(); + + source.onSubscribe(d); + + source.onNext(1); + source.onComplete(); + source.onComplete(); + source.onError(new TestException()); + + to.assertResult(1); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void nonfusedError() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + TestObserverEx<Integer> to = new TestObserverEx<>(QueueFuseable.NONE); + + TakeLast source = new TakeLast(to); + + Disposable d = Disposable.empty(); + + source.onSubscribe(d); + + source.onNext(1); + source.onError(new TestException()); + source.onError(new TestException("second")); + source.onComplete(); + + to.assertFailure(TestException.class); + + TestHelper.assertUndeliverable(errors, 0, TestException.class, "second"); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void fusedTerminateMore() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + TestObserverEx<Integer> to = new TestObserverEx<>(QueueFuseable.ANY); + + TakeLast source = new TakeLast(to); + + Disposable d = Disposable.empty(); + + source.onSubscribe(d); + + source.onNext(1); + source.onComplete(); + source.onComplete(); + source.onError(new TestException()); + + to.assertResult(1); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void fusedError() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + TestObserverEx<Integer> to = new TestObserverEx<>(QueueFuseable.ANY); + + TakeLast source = new TakeLast(to); + + Disposable d = Disposable.empty(); + + source.onSubscribe(d); + + source.onNext(1); + source.onError(new TestException()); + source.onError(new TestException("second")); + source.onComplete(); + + to.assertFailure(TestException.class); + + TestHelper.assertUndeliverable(errors, 0, TestException.class, "second"); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void disposed() { + TestObserverEx<Integer> to = new TestObserverEx<>(QueueFuseable.NONE); + + TakeLast source = new TakeLast(to); + + Disposable d = Disposable.empty(); + + source.onSubscribe(d); + + to.dispose(); + + source.onNext(1); + source.onComplete(); + + to.assertNoValues().assertNoErrors().assertNotComplete(); + } + + @Test + public void disposedAfterOnNext() { + final TestObserver<Integer> to = new TestObserver<>(); + + TakeLast source = new TakeLast(new Observer<Integer>() { + Disposable upstream; + + @Override + public void onSubscribe(Disposable d) { + this.upstream = d; + to.onSubscribe(d); + } + + @Override + public void onNext(Integer value) { + to.onNext(value); + upstream.dispose(); + } + + @Override + public void onError(Throwable e) { + to.onError(e); + } + + @Override + public void onComplete() { + to.onComplete(); + } + }); + + source.onSubscribe(Disposable.empty()); + source.onNext(1); + source.onComplete(); + + to.assertValue(1).assertNoErrors().assertNotComplete(); + } + + @Test + public void fusedEmpty() { + TestObserverEx<Integer> to = new TestObserverEx<>(QueueFuseable.ANY); + + TakeLast source = new TakeLast(to); + + Disposable d = Disposable.empty(); + + source.onSubscribe(d); + + source.onComplete(); + + to.assertResult(); + } + + @Test + public void nonfusedEmpty() { + TestObserverEx<Integer> to = new TestObserverEx<>(QueueFuseable.NONE); + + TakeLast source = new TakeLast(to); + + Disposable d = Disposable.empty(); + + source.onSubscribe(d); + + source.onComplete(); + + to.assertResult(); + } + + @Test + public void customFusion() { + final TestObserver<Integer> to = new TestObserver<>(); + + TakeLast source = new TakeLast(new Observer<Integer>() { + QueueDisposable<Integer> d; + + @SuppressWarnings("unchecked") + @Override + public void onSubscribe(Disposable d) { + this.d = (QueueDisposable<Integer>)d; + to.onSubscribe(d); + this.d.requestFusion(QueueFuseable.ANY); + } + + @Override + public void onNext(Integer value) { + if (!d.isEmpty()) { + Integer v = null; + try { + to.onNext(d.poll()); + + v = d.poll(); + } catch (Throwable ex) { + to.onError(ex); + } + + assertNull(v); + assertTrue(d.isEmpty()); + } + } + + @Override + public void onError(Throwable e) { + to.onError(e); + } + + @Override + public void onComplete() { + to.onComplete(); + } + }); + + source.onSubscribe(Disposable.empty()); + source.onNext(1); + source.onComplete(); + + to.assertResult(1); + } + + @Test + public void customFusionClear() { + final TestObserver<Integer> to = new TestObserver<>(); + + TakeLast source = new TakeLast(new Observer<Integer>() { + QueueDisposable<Integer> d; + + @SuppressWarnings("unchecked") + @Override + public void onSubscribe(Disposable d) { + this.d = (QueueDisposable<Integer>)d; + to.onSubscribe(d); + this.d.requestFusion(QueueFuseable.ANY); + } + + @Override + public void onNext(Integer value) { + d.clear(); + assertTrue(d.isEmpty()); + } + + @Override + public void onError(Throwable e) { + to.onError(e); + } + + @Override + public void onComplete() { + to.onComplete(); + } + }); + + source.onSubscribe(Disposable.empty()); + source.onNext(1); + source.onComplete(); + + to.assertNoValues().assertNoErrors().assertComplete(); + } + + @Test + public void offerThrow() { + TestObserverEx<Integer> to = new TestObserverEx<>(QueueFuseable.NONE); + + TakeLast source = new TakeLast(to); + + TestHelper.assertNoOffer(source); + } + + @Test + public void customFusionDontConsume() { + final TestObserver<Integer> to = new TestObserver<>(); + + TakeFirst source = new TakeFirst(new Observer<Integer>() { + QueueDisposable<Integer> d; + + @SuppressWarnings("unchecked") + @Override + public void onSubscribe(Disposable d) { + this.d = (QueueDisposable<Integer>)d; + to.onSubscribe(d); + this.d.requestFusion(QueueFuseable.ANY); + } + + @Override + public void onNext(Integer value) { + // not consuming + } + + @Override + public void onError(Throwable e) { + to.onError(e); + } + + @Override + public void onComplete() { + to.onComplete(); + } + }); + + source.onSubscribe(Disposable.empty()); + source.onNext(1); + + to.assertNoValues().assertNoErrors().assertComplete(); + } + +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/observers/DisposableLambdaObserverTest.java b/src/test/java/io/reactivex/rxjava3/internal/observers/DisposableLambdaObserverTest.java new file mode 100644 index 0000000000..294892507b --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/observers/DisposableLambdaObserverTest.java @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.observers; + +import static org.junit.Assert.*; + +import java.util.List; + +import io.reactivex.rxjava3.disposables.Disposable; +import org.junit.Test; + +import io.reactivex.rxjava3.core.RxJavaTest; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.Action; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class DisposableLambdaObserverTest extends RxJavaTest { + + @Test + public void doubleOnSubscribe() { + TestHelper.doubleOnSubscribe(new DisposableLambdaObserver<>( + new TestObserver<>(), Functions.emptyConsumer(), Functions.EMPTY_ACTION + )); + } + + @Test + public void disposeCrash() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + DisposableLambdaObserver<Integer> o = new DisposableLambdaObserver<>( + new TestObserver<>(), Functions.emptyConsumer(), + new Action() { + @Override + public void run() throws Exception { + throw new TestException(); + } + } + ); + + o.onSubscribe(Disposable.empty()); + + assertFalse(o.isDisposed()); + + o.dispose(); + + assertTrue(o.isDisposed()); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/observers/EmptyCompletableObserverTest.java b/src/test/java/io/reactivex/rxjava3/internal/observers/EmptyCompletableObserverTest.java new file mode 100644 index 0000000000..c58de8f0f9 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/observers/EmptyCompletableObserverTest.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.observers; + +import static org.junit.Assert.assertFalse; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.RxJavaTest; + +public final class EmptyCompletableObserverTest extends RxJavaTest { + + @Test + public void defaultShouldReportNoCustomOnError() { + EmptyCompletableObserver o = new EmptyCompletableObserver(); + + assertFalse(o.hasCustomOnError()); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/observers/FutureMultiObserverTest.java b/src/test/java/io/reactivex/rxjava3/internal/observers/FutureMultiObserverTest.java new file mode 100644 index 0000000000..7499b99ca1 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/observers/FutureMultiObserverTest.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.observers; + +import static org.junit.Assert.*; +import org.junit.Test; + +import io.reactivex.rxjava3.core.RxJavaTest; +import io.reactivex.rxjava3.disposables.Disposable; + +public class FutureMultiObserverTest extends RxJavaTest { + + @Test + public void cancelBeforeOnSubscribe() { + FutureMultiObserver<Integer> f = new FutureMultiObserver<>(); + + assertTrue(f.cancel(true)); + + Disposable d = Disposable.empty(); + + f.onSubscribe(d); + + assertTrue(d.isDisposed()); + } + + @Test + public void onCompleteJustAfterDispose() { + FutureMultiObserver<Integer> f = new FutureMultiObserver<>(); + Disposable d = Disposable.empty(); + f.onSubscribe(d); + assertTrue(f.cancel(true)); + + f.onComplete(); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/observers/FutureObserverTest.java b/src/test/java/io/reactivex/rxjava3/internal/observers/FutureObserverTest.java new file mode 100644 index 0000000000..8c475eb038 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/observers/FutureObserverTest.java @@ -0,0 +1,399 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.observers; + +import static io.reactivex.rxjava3.internal.util.ExceptionHelper.timeoutMessage; +import static org.junit.Assert.*; + +import java.util.*; +import java.util.concurrent.*; + +import org.junit.*; + +import io.reactivex.rxjava3.core.RxJavaTest; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.schedulers.Schedulers; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class FutureObserverTest extends RxJavaTest { + FutureObserver<Integer> fo; + + @Before + public void before() { + fo = new FutureObserver<>(); + } + + @Test + public void cancel2() { + + fo.dispose(); + + assertFalse(fo.isCancelled()); + assertFalse(fo.isDisposed()); + assertFalse(fo.isDone()); + + for (int i = 0; i < 2; i++) { + fo.cancel(i == 0); + + assertTrue(fo.isCancelled()); + assertTrue(fo.isDisposed()); + assertTrue(fo.isDone()); + } + + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + fo.onNext(1); + fo.onError(new TestException("First")); + fo.onError(new TestException("Second")); + fo.onComplete(); + + assertTrue(fo.isCancelled()); + assertTrue(fo.isDisposed()); + assertTrue(fo.isDone()); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + TestHelper.assertUndeliverable(errors, 1, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void cancel() throws Exception { + assertFalse(fo.isDone()); + + assertFalse(fo.isCancelled()); + + fo.cancel(false); + + assertTrue(fo.isDone()); + + assertTrue(fo.isCancelled()); + + try { + fo.get(); + fail("Should have thrown"); + } catch (CancellationException ex) { + // expected + } + + try { + fo.get(1, TimeUnit.MILLISECONDS); + fail("Should have thrown"); + } catch (CancellationException ex) { + // expected + } + } + + @Test + public void onError() throws Exception { + List<Throwable> errors = TestHelper.trackPluginErrors(); + + try { + fo.onError(new TestException("One")); + + fo.onError(new TestException("Two")); + + try { + fo.get(5, TimeUnit.MILLISECONDS); + } catch (ExecutionException ex) { + assertTrue(ex.toString(), ex.getCause() instanceof TestException); + assertEquals("One", ex.getCause().getMessage()); + } + + TestHelper.assertUndeliverable(errors, 0, TestException.class, "Two"); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void onNext() throws Exception { + fo.onNext(1); + fo.onComplete(); + + assertEquals(1, fo.get(5, TimeUnit.MILLISECONDS).intValue()); + } + + @Test + public void onSubscribe() throws Exception { + List<Throwable> errors = TestHelper.trackPluginErrors(); + + try { + + Disposable d1 = Disposable.empty(); + + fo.onSubscribe(d1); + + Disposable d2 = Disposable.empty(); + + fo.onSubscribe(d2); + + assertFalse(d1.isDisposed()); + assertTrue(d2.isDisposed()); + + TestHelper.assertError(errors, 0, IllegalStateException.class, "Disposable already set!"); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void cancelRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final FutureObserver<Integer> fo = new FutureObserver<>(); + + Runnable r = new Runnable() { + @Override + public void run() { + fo.cancel(false); + } + }; + + TestHelper.race(r, r); + } + } + + @Test + public void await() throws Exception { + Schedulers.single().scheduleDirect(new Runnable() { + @Override + public void run() { + fo.onNext(1); + fo.onComplete(); + } + }, 100, TimeUnit.MILLISECONDS); + + assertEquals(1, fo.get(5, TimeUnit.SECONDS).intValue()); + } + + @Test + public void onErrorCancelRace() { + RxJavaPlugins.setErrorHandler(Functions.emptyConsumer()); + try { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final FutureObserver<Integer> fo = new FutureObserver<>(); + + final TestException ex = new TestException(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + fo.cancel(false); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + fo.onError(ex); + } + }; + + TestHelper.race(r1, r2); + } + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void onCompleteCancelRace() { + RxJavaPlugins.setErrorHandler(Functions.emptyConsumer()); + try { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final FutureObserver<Integer> fo = new FutureObserver<>(); + + if (i % 3 == 0) { + fo.onSubscribe(Disposable.empty()); + } + + if (i % 2 == 0) { + fo.onNext(1); + } + + Runnable r1 = new Runnable() { + @Override + public void run() { + fo.cancel(false); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + fo.onComplete(); + } + }; + + TestHelper.race(r1, r2); + } + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void onErrorOnComplete() throws Exception { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + fo.onError(new TestException("One")); + fo.onComplete(); + + try { + fo.get(5, TimeUnit.MILLISECONDS); + } catch (ExecutionException ex) { + assertTrue(ex.toString(), ex.getCause() instanceof TestException); + assertEquals("One", ex.getCause().getMessage()); + } + + TestHelper.assertUndeliverable(errors, 0, NoSuchElementException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void onCompleteOnError() throws Exception { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + fo.onComplete(); + fo.onError(new TestException("One")); + + try { + assertNull(fo.get(5, TimeUnit.MILLISECONDS)); + } catch (ExecutionException ex) { + assertTrue(ex.toString(), ex.getCause() instanceof NoSuchElementException); + } + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void onNextCompleteOnError() throws Exception { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + fo.onNext(1); + fo.onComplete(); + fo.onError(new TestException("One")); + + assertEquals((Integer)1, fo.get(5, TimeUnit.MILLISECONDS)); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void cancelOnError() throws Exception { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + fo.cancel(true); + fo.onError(new TestException("One")); + + try { + fo.get(5, TimeUnit.MILLISECONDS); + fail("Should have thrown"); + } catch (CancellationException ex) { + // expected + } + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void cancelOnComplete() throws Exception { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + fo.cancel(true); + fo.onComplete(); + + try { + fo.get(5, TimeUnit.MILLISECONDS); + fail("Should have thrown"); + } catch (CancellationException ex) { + // expected + } + + TestHelper.assertUndeliverable(errors, 0, NoSuchElementException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void onNextThenOnCompleteTwice() throws Exception { + fo.onNext(1); + fo.onComplete(); + fo.onComplete(); + + assertEquals(1, fo.get(5, TimeUnit.MILLISECONDS).intValue()); + } + + @Test(expected = InterruptedException.class) + public void getInterrupted() throws Exception { + Thread.currentThread().interrupt(); + fo.get(); + } + + @Test + public void completeAsync() throws Exception { + Schedulers.single().scheduleDirect(new Runnable() { + @Override + public void run() { + fo.onNext(1); + fo.onComplete(); + } + }, 500, TimeUnit.MILLISECONDS); + + assertEquals(1, fo.get().intValue()); + } + + @Test + public void getTimedOut() throws Exception { + try { + fo.get(1, TimeUnit.NANOSECONDS); + fail("Should have thrown"); + } catch (TimeoutException expected) { + assertEquals(timeoutMessage(1, TimeUnit.NANOSECONDS), expected.getMessage()); + } + } + + @Test + public void cancelOnSubscribeRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final FutureObserver<Integer> fo = new FutureObserver<>(); + + Runnable r = new Runnable() { + @Override + public void run() { + fo.cancel(false); + } + }; + + Disposable d = Disposable.empty(); + + TestHelper.race(r, () -> fo.onSubscribe(d)); + } + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/observers/FutureSingleObserverTest.java b/src/test/java/io/reactivex/rxjava3/internal/observers/FutureSingleObserverTest.java new file mode 100644 index 0000000000..db8b414347 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/observers/FutureSingleObserverTest.java @@ -0,0 +1,192 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.observers; + +import static io.reactivex.rxjava3.internal.util.ExceptionHelper.timeoutMessage; +import static org.junit.Assert.*; + +import java.util.concurrent.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.subjects.PublishSubject; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class FutureSingleObserverTest extends RxJavaTest { + + @Test + public void cancel() { + final Future<?> f = Single.never().toFuture(); + + assertFalse(f.isCancelled()); + assertFalse(f.isDone()); + + f.cancel(true); + + assertTrue(f.isCancelled()); + assertTrue(f.isDone()); + + try { + f.get(); + fail("Should have thrown!"); + } catch (CancellationException ex) { + // expected + } catch (InterruptedException ex) { + throw new AssertionError(ex); + } catch (ExecutionException ex) { + throw new AssertionError(ex); + } + + try { + f.get(5, TimeUnit.SECONDS); + fail("Should have thrown!"); + } catch (CancellationException ex) { + // expected + } catch (InterruptedException ex) { + throw new AssertionError(ex); + } catch (ExecutionException ex) { + throw new AssertionError(ex); + } catch (TimeoutException ex) { + throw new AssertionError(ex); + } + } + + @Test + public void cancelRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final Future<?> f = Single.never().toFuture(); + + Runnable r = new Runnable() { + @Override + public void run() { + f.cancel(true); + } + }; + + TestHelper.race(r, r); + } + } + + @Test + public void timeout() throws Exception { + + Future<?> f = Single.never().toFuture(); + + try { + f.get(100, TimeUnit.MILLISECONDS); + fail("Should have thrown"); + } catch (TimeoutException expected) { + assertEquals(timeoutMessage(100, TimeUnit.MILLISECONDS), expected.getMessage()); + } + } + + @Test + public void dispose() { + Future<Integer> f = Single.just(1).toFuture(); + + ((Disposable)f).dispose(); + + assertTrue(((Disposable)f).isDisposed()); + } + + @Test + public void errorGetWithTimeout() throws Exception { + Future<?> f = Single.error(new TestException()).toFuture(); + + try { + f.get(5, TimeUnit.SECONDS); + fail("Should have thrown"); + } catch (ExecutionException ex) { + assertTrue(ex.toString(), ex.getCause() instanceof TestException); + } + } + + @Test + public void normalGetWitHTimeout() throws Exception { + Future<Integer> f = Single.just(1).toFuture(); + + assertEquals(1, f.get(5, TimeUnit.SECONDS).intValue()); + } + + @Test + public void getAwait() throws Exception { + Future<Integer> f = Single.just(1).delay(100, TimeUnit.MILLISECONDS).toFuture(); + + assertEquals(1, f.get(5, TimeUnit.SECONDS).intValue()); + } + + @Test + public void onSuccessCancelRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final PublishSubject<Integer> ps = PublishSubject.create(); + + final Future<?> f = ps.single(-99).toFuture(); + + ps.onNext(1); + + Runnable r1 = new Runnable() { + @Override + public void run() { + f.cancel(true); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + ps.onComplete(); + } + }; + + TestHelper.race(r1, r2); + } + } + + @Test + public void onErrorCancelRace() { + RxJavaPlugins.setErrorHandler(Functions.emptyConsumer()); + try { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final PublishSubject<Integer> ps = PublishSubject.create(); + + final Future<?> f = ps.single(-99).toFuture(); + + final TestException ex = new TestException(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + f.cancel(true); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + ps.onError(ex); + } + }; + + TestHelper.race(r1, r2); + } + } finally { + RxJavaPlugins.reset(); + } + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/observers/InnerQueuedObserverTest.java b/src/test/java/io/reactivex/rxjava3/internal/observers/InnerQueuedObserverTest.java new file mode 100644 index 0000000000..549c72534a --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/observers/InnerQueuedObserverTest.java @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.observers; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.RxJavaTest; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class InnerQueuedObserverTest extends RxJavaTest { + + @Test + public void dispose() { + TestHelper.checkDisposed(new InnerQueuedObserver<>(null, 1)); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/observers/LambdaObserverTest.java b/src/test/java/io/reactivex/rxjava3/internal/observers/LambdaObserverTest.java new file mode 100644 index 0000000000..7ce4307419 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/observers/LambdaObserverTest.java @@ -0,0 +1,411 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.observers; + +import static org.junit.Assert.*; + +import java.io.IOException; +import java.util.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.core.Observer; +import io.reactivex.rxjava3.core.RxJavaTest; +import io.reactivex.rxjava3.disposables.*; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.subjects.PublishSubject; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class LambdaObserverTest extends RxJavaTest { + + @Test + public void onSubscribeThrows() { + final List<Object> received = new ArrayList<>(); + + LambdaObserver<Object> o = new LambdaObserver<>(new Consumer<Object>() { + @Override + public void accept(Object v) throws Exception { + received.add(v); + } + }, + new Consumer<Throwable>() { + @Override + public void accept(Throwable e) throws Exception { + received.add(e); + } + }, new Action() { + @Override + public void run() throws Exception { + received.add(100); + } + }, new Consumer<Disposable>() { + @Override + public void accept(Disposable d) throws Exception { + throw new TestException(); + } + }); + + assertFalse(o.isDisposed()); + + Observable.just(1).subscribe(o); + + assertTrue(received.toString(), received.get(0) instanceof TestException); + assertEquals(received.toString(), 1, received.size()); + + assertTrue(o.isDisposed()); + } + + @Test + public void onNextThrows() { + final List<Object> received = new ArrayList<>(); + + LambdaObserver<Object> o = new LambdaObserver<>(new Consumer<Object>() { + @Override + public void accept(Object v) throws Exception { + throw new TestException(); + } + }, + new Consumer<Throwable>() { + @Override + public void accept(Throwable e) throws Exception { + received.add(e); + } + }, new Action() { + @Override + public void run() throws Exception { + received.add(100); + } + }, new Consumer<Disposable>() { + @Override + public void accept(Disposable d) throws Exception { + } + }); + + assertFalse(o.isDisposed()); + + Observable.just(1).subscribe(o); + + assertTrue(received.toString(), received.get(0) instanceof TestException); + assertEquals(received.toString(), 1, received.size()); + + assertTrue(o.isDisposed()); + } + + @Test + public void onErrorThrows() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + + try { + final List<Object> received = new ArrayList<>(); + + LambdaObserver<Object> o = new LambdaObserver<>(new Consumer<Object>() { + @Override + public void accept(Object v) throws Exception { + received.add(v); + } + }, + new Consumer<Throwable>() { + @Override + public void accept(Throwable e) throws Exception { + throw new TestException("Inner"); + } + }, new Action() { + @Override + public void run() throws Exception { + received.add(100); + } + }, new Consumer<Disposable>() { + @Override + public void accept(Disposable d) throws Exception { + } + }); + + assertFalse(o.isDisposed()); + + Observable.<Integer>error(new TestException("Outer")).subscribe(o); + + assertTrue(received.toString(), received.isEmpty()); + + assertTrue(o.isDisposed()); + + TestHelper.assertError(errors, 0, CompositeException.class); + List<Throwable> ce = TestHelper.compositeList(errors.get(0)); + TestHelper.assertError(ce, 0, TestException.class, "Outer"); + TestHelper.assertError(ce, 1, TestException.class, "Inner"); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void onCompleteThrows() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + + try { + final List<Object> received = new ArrayList<>(); + + LambdaObserver<Object> o = new LambdaObserver<>(new Consumer<Object>() { + @Override + public void accept(Object v) throws Exception { + received.add(v); + } + }, + new Consumer<Throwable>() { + @Override + public void accept(Throwable e) throws Exception { + received.add(e); + } + }, new Action() { + @Override + public void run() throws Exception { + throw new TestException(); + } + }, new Consumer<Disposable>() { + @Override + public void accept(Disposable d) throws Exception { + } + }); + + assertFalse(o.isDisposed()); + + Observable.<Integer>empty().subscribe(o); + + assertTrue(received.toString(), received.isEmpty()); + + assertTrue(o.isDisposed()); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void badSourceOnSubscribe() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + Observable<Integer> source = new Observable<Integer>() { + @Override + public void subscribeActual(Observer<? super Integer> observer) { + Disposable d1 = Disposable.empty(); + observer.onSubscribe(d1); + Disposable d2 = Disposable.empty(); + observer.onSubscribe(d2); + + assertFalse(d1.isDisposed()); + assertTrue(d2.isDisposed()); + + observer.onNext(1); + observer.onComplete(); + } + }; + + final List<Object> received = new ArrayList<>(); + + LambdaObserver<Object> o = new LambdaObserver<>(new Consumer<Object>() { + @Override + public void accept(Object v) throws Exception { + received.add(v); + } + }, + new Consumer<Throwable>() { + @Override + public void accept(Throwable e) throws Exception { + received.add(e); + } + }, new Action() { + @Override + public void run() throws Exception { + received.add(100); + } + }, new Consumer<Disposable>() { + @Override + public void accept(Disposable d) throws Exception { + } + }); + + source.subscribe(o); + + assertEquals(Arrays.asList(1, 100), received); + + TestHelper.assertError(errors, 0, ProtocolViolationException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void badSourceEmitAfterDone() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + Observable<Integer> source = new Observable<Integer>() { + @Override + public void subscribeActual(Observer<? super Integer> observer) { + observer.onSubscribe(Disposable.empty()); + + observer.onNext(1); + observer.onComplete(); + observer.onNext(2); + observer.onError(new TestException()); + observer.onComplete(); + } + }; + + final List<Object> received = new ArrayList<>(); + + LambdaObserver<Object> o = new LambdaObserver<>(new Consumer<Object>() { + @Override + public void accept(Object v) throws Exception { + received.add(v); + } + }, + new Consumer<Throwable>() { + @Override + public void accept(Throwable e) throws Exception { + received.add(e); + } + }, new Action() { + @Override + public void run() throws Exception { + received.add(100); + } + }, new Consumer<Disposable>() { + @Override + public void accept(Disposable d) throws Exception { + } + }); + + source.subscribe(o); + + assertEquals(Arrays.asList(1, 100), received); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void onNextThrowsCancelsUpstream() { + PublishSubject<Integer> ps = PublishSubject.create(); + + final List<Throwable> errors = new ArrayList<>(); + + ps.subscribe(new Consumer<Integer>() { + @Override + public void accept(Integer v) throws Exception { + throw new TestException(); + } + }, new Consumer<Throwable>() { + @Override + public void accept(Throwable e) throws Exception { + errors.add(e); + } + }); + + assertTrue("No observers?!", ps.hasObservers()); + assertTrue("Has errors already?!", errors.isEmpty()); + + ps.onNext(1); + + assertFalse("Has observers?!", ps.hasObservers()); + assertFalse("No errors?!", errors.isEmpty()); + + assertTrue(errors.toString(), errors.get(0) instanceof TestException); + } + + @Test + public void onSubscribeThrowsCancelsUpstream() { + PublishSubject<Integer> ps = PublishSubject.create(); + + final List<Throwable> errors = new ArrayList<>(); + + ps.subscribe(new LambdaObserver<>(new Consumer<Integer>() { + @Override + public void accept(Integer v) throws Exception { + } + }, new Consumer<Throwable>() { + @Override + public void accept(Throwable e) throws Exception { + errors.add(e); + } + }, new Action() { + @Override + public void run() throws Exception { + } + }, new Consumer<Disposable>() { + @Override + public void accept(Disposable d) throws Exception { + throw new TestException(); + } + })); + + assertFalse("Has observers?!", ps.hasObservers()); + assertFalse("No errors?!", errors.isEmpty()); + + assertTrue(errors.toString(), errors.get(0) instanceof TestException); + } + + @Test + public void onErrorMissingShouldReportNoCustomOnError() { + LambdaObserver<Integer> o = new LambdaObserver<>(Functions.<Integer>emptyConsumer(), + Functions.ON_ERROR_MISSING, + Functions.EMPTY_ACTION, + Functions.<Disposable>emptyConsumer()); + + assertFalse(o.hasCustomOnError()); + } + + @Test + public void customOnErrorShouldReportCustomOnError() { + LambdaObserver<Integer> o = new LambdaObserver<>(Functions.<Integer>emptyConsumer(), + Functions.<Throwable>emptyConsumer(), + Functions.EMPTY_ACTION, + Functions.<Disposable>emptyConsumer()); + + assertTrue(o.hasCustomOnError()); + } + + @Test + public void disposedObserverShouldReportErrorOnGlobalErrorHandler() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + final List<Throwable> observerErrors = Collections.synchronizedList(new ArrayList<>()); + + LambdaObserver<Integer> o = new LambdaObserver<>(Functions.<Integer>emptyConsumer(), + new Consumer<Throwable>() { + @Override + public void accept(Throwable t) { + observerErrors.add(t); + } + }, + Functions.EMPTY_ACTION, + Functions.<Disposable>emptyConsumer()); + + o.dispose(); + o.onError(new IOException()); + o.onError(new IOException()); + + assertTrue(observerErrors.isEmpty()); + TestHelper.assertUndeliverable(errors, 0, IOException.class); + TestHelper.assertUndeliverable(errors, 1, IOException.class); + } finally { + RxJavaPlugins.reset(); + } + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/observers/MaybeConsumersTest.java b/src/test/java/io/reactivex/rxjava3/internal/observers/MaybeConsumersTest.java new file mode 100644 index 0000000000..92b32c000e --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/observers/MaybeConsumersTest.java @@ -0,0 +1,273 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +/* + * Copyright 2016-2019 David Karnok + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.reactivex.rxjava3.internal.observers; + +import static org.junit.Assert.*; + +import java.io.IOException; +import java.util.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.*; +import io.reactivex.rxjava3.exceptions.CompositeException; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.observers.LambdaConsumerIntrospection; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.subjects.MaybeSubject; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class MaybeConsumersTest implements Consumer<Object>, Action { + + final CompositeDisposable composite = new CompositeDisposable(); + + final MaybeSubject<Integer> processor = MaybeSubject.create(); + + final List<Object> events = new ArrayList<>(); + + @Override + public void run() throws Exception { + events.add("OnComplete"); + } + + @Override + public void accept(Object t) throws Exception { + events.add(t); + } + + static <T> Disposable subscribeAutoDispose(Maybe<T> source, CompositeDisposable composite, + Consumer<? super T> onSuccess, Consumer<? super Throwable> onError, Action onComplete) { + return source.subscribe(onSuccess, onError, onComplete, composite); + } + + @Test + public void onSuccessNormal() { + + Disposable d = subscribeAutoDispose(processor, composite, this, Functions.ON_ERROR_MISSING, () -> { }); + + assertFalse(d.getClass().toString(), ((LambdaConsumerIntrospection)d).hasCustomOnError()); + + assertTrue(composite.size() > 0); + + assertTrue(events.toString(), events.isEmpty()); + + processor.onSuccess(1); + + assertEquals(0, composite.size()); + + assertEquals(Arrays.<Object>asList(1), events); + + } + + @Test + public void onErrorNormal() { + + subscribeAutoDispose(processor, composite, this, this, this); + + assertTrue(composite.size() > 0); + + assertTrue(events.toString(), events.isEmpty()); + + processor.onSuccess(1); + + assertEquals(0, composite.size()); + + assertEquals(Arrays.<Object>asList(1), events); + + } + + @Test + public void onErrorError() { + + Disposable d = subscribeAutoDispose(processor, composite, this, this, this); + + assertTrue(d.getClass().toString(), ((LambdaConsumerIntrospection)d).hasCustomOnError()); + + assertTrue(composite.size() > 0); + + assertTrue(events.toString(), events.isEmpty()); + + processor.onError(new IOException()); + + assertTrue(events.toString(), events.get(0) instanceof IOException); + + assertEquals(0, composite.size()); + } + + @Test + public void onCompleteNormal() { + + subscribeAutoDispose(processor, composite, this, this, this); + + assertTrue(composite.size() > 0); + + assertTrue(events.toString(), events.isEmpty()); + + processor.onComplete(); + + assertEquals(0, composite.size()); + + assertEquals(Arrays.<Object>asList("OnComplete"), events); + + } + + @Test + public void onCompleteError() { + + subscribeAutoDispose(processor, composite, this, this, this); + + assertTrue(composite.size() > 0); + + assertTrue(events.toString(), events.isEmpty()); + + processor.onError(new IOException()); + + assertTrue(events.toString(), events.get(0) instanceof IOException); + + assertEquals(0, composite.size()); + } + + @Test + public void onCompleteDispose() { + + Disposable d = subscribeAutoDispose(processor, composite, this, this, this); + + assertTrue(composite.size() > 0); + + assertTrue(events.toString(), events.isEmpty()); + + assertFalse(d.isDisposed()); + + d.dispose(); + d.dispose(); + + assertTrue(d.isDisposed()); + + assertEquals(0, composite.size()); + + assertFalse(processor.hasObservers()); + } + + @Test + public void onSuccessCrash() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + subscribeAutoDispose(processor, composite, new Consumer<Object>() { + @Override + public void accept(Object t) throws Exception { + throw new IOException(); + } + }, this, this); + + processor.onSuccess(1); + + assertTrue(events.toString(), events.isEmpty()); + + TestHelper.assertUndeliverable(errors, 0, IOException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void onErrorCrash() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + subscribeAutoDispose(processor, composite, this, new Consumer<Throwable>() { + @Override + public void accept(Throwable t) throws Exception { + throw new IOException(t); + } + }, this); + + processor.onError(new IllegalArgumentException()); + + assertTrue(events.toString(), events.isEmpty()); + + TestHelper.assertError(errors, 0, CompositeException.class); + List<Throwable> inners = TestHelper.compositeList(errors.get(0)); + TestHelper.assertError(inners, 0, IllegalArgumentException.class); + TestHelper.assertError(inners, 1, IOException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void onCompleteCrash() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + subscribeAutoDispose(processor, composite, this, this, new Action() { + @Override + public void run() throws Exception { + throw new IOException(); + } + }); + + processor.onComplete(); + + assertTrue(events.toString(), events.isEmpty()); + + TestHelper.assertUndeliverable(errors, 0, IOException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void badSource() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + subscribeAutoDispose( + new Maybe<Integer>() { + @Override + protected void subscribeActual( + MaybeObserver<? super Integer> observer) { + observer.onSubscribe(Disposable.empty()); + observer.onComplete(); + + observer.onSubscribe(Disposable.empty()); + observer.onSuccess(2); + observer.onComplete(); + observer.onError(new IOException()); + } + }, composite, this, this, this + ); + + assertEquals(Arrays.<Object>asList("OnComplete"), events); + + TestHelper.assertUndeliverable(errors, 0, IOException.class); + } finally { + RxJavaPlugins.reset(); + } + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/observers/ObservableConsumersTest.java b/src/test/java/io/reactivex/rxjava3/internal/observers/ObservableConsumersTest.java new file mode 100644 index 0000000000..b5d26340e7 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/observers/ObservableConsumersTest.java @@ -0,0 +1,324 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +/* + * Copyright 2016-2019 David Karnok + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.reactivex.rxjava3.internal.observers; + +import static org.junit.Assert.*; + +import java.io.IOException; +import java.util.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.core.Observer; +import io.reactivex.rxjava3.disposables.*; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.observers.LambdaConsumerIntrospection; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.subjects.PublishSubject; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class ObservableConsumersTest implements Consumer<Object>, Action { + + final CompositeDisposable composite = new CompositeDisposable(); + + final PublishSubject<Integer> processor = PublishSubject.create(); + + final List<Object> events = new ArrayList<>(); + + @Override + public void run() throws Exception { + events.add("OnComplete"); + } + + @Override + public void accept(Object t) throws Exception { + events.add(t); + } + + static <T> Disposable subscribeAutoDispose(Observable<T> source, CompositeDisposable composite, + Consumer<? super T> onNext, Consumer<? super Throwable> onError, Action onComplete) { + return source.subscribe(onNext, onError, onComplete, composite); + } + + @Test + public void onNextNormal() { + + Disposable d = subscribeAutoDispose(processor, composite, this, Functions.ON_ERROR_MISSING, () -> { }); + + assertFalse(d.getClass().toString(), ((LambdaConsumerIntrospection)d).hasCustomOnError()); + + assertTrue(composite.size() > 0); + + assertTrue(events.toString(), events.isEmpty()); + + processor.onNext(1); + + assertTrue(composite.size() > 0); + + assertEquals(Arrays.<Object>asList(1), events); + + processor.onComplete(); + + assertEquals(Arrays.<Object>asList(1), events); + + assertEquals(0, composite.size()); + } + + @Test + public void onErrorNormal() { + + subscribeAutoDispose(processor, composite, this, Functions.ON_ERROR_MISSING, () -> { }); + + assertTrue(composite.size() > 0); + + assertTrue(events.toString(), events.isEmpty()); + + processor.onNext(1); + + assertTrue(composite.size() > 0); + + assertEquals(Arrays.<Object>asList(1), events); + + processor.onComplete(); + + assertEquals(Arrays.<Object>asList(1), events); + + assertEquals(0, composite.size()); + } + + @Test + public void onErrorError() { + + Disposable d = subscribeAutoDispose(processor, composite, this, this, this); + + assertTrue(d.getClass().toString(), ((LambdaConsumerIntrospection)d).hasCustomOnError()); + + assertTrue(composite.size() > 0); + + assertTrue(events.toString(), events.isEmpty()); + + processor.onNext(1); + + assertTrue(composite.size() > 0); + + assertEquals(Arrays.<Object>asList(1), events); + + processor.onError(new IOException()); + + assertEquals(events.toString(), 1, events.get(0)); + assertTrue(events.toString(), events.get(1) instanceof IOException); + + assertEquals(0, composite.size()); + } + + @Test + public void onCompleteNormal() { + + subscribeAutoDispose(processor, composite, this, this, this); + + assertTrue(composite.size() > 0); + + assertTrue(events.toString(), events.isEmpty()); + + processor.onNext(1); + + assertTrue(composite.size() > 0); + + assertEquals(Arrays.<Object>asList(1), events); + + processor.onComplete(); + + assertEquals(Arrays.<Object>asList(1, "OnComplete"), events); + + assertEquals(0, composite.size()); + } + + @Test + public void onCompleteError() { + + subscribeAutoDispose(processor, composite, this, this, this); + + assertTrue(composite.size() > 0); + + assertTrue(events.toString(), events.isEmpty()); + + processor.onNext(1); + + assertTrue(composite.size() > 0); + + assertEquals(Arrays.<Object>asList(1), events); + + processor.onError(new IOException()); + + assertEquals(events.toString(), 1, events.get(0)); + assertTrue(events.toString(), events.get(1) instanceof IOException); + + assertEquals(0, composite.size()); + } + + @Test + public void onCompleteDispose() { + + Disposable d = subscribeAutoDispose(processor, composite, this, this, this); + + assertTrue(composite.size() > 0); + + assertTrue(events.toString(), events.isEmpty()); + + assertFalse(d.isDisposed()); + + d.dispose(); + d.dispose(); + + assertTrue(d.isDisposed()); + + assertEquals(0, composite.size()); + + assertFalse(processor.hasObservers()); + } + + @Test + public void onNextCrash() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + subscribeAutoDispose(processor, composite, new Consumer<Object>() { + @Override + public void accept(Object t) throws Exception { + throw new IOException(); + } + }, this, this); + + processor.onNext(1); + + assertTrue(errors.toString(), errors.isEmpty()); + + assertTrue(events.toString(), events.get(0) instanceof IOException); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void onNextCrashOnError() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + subscribeAutoDispose(processor, composite, this, new Consumer<Throwable>() { + @Override + public void accept(Throwable t) throws Exception { + throw new IOException(t); + } + }, this); + + processor.onError(new IllegalArgumentException()); + + assertTrue(events.toString(), events.isEmpty()); + + TestHelper.assertError(errors, 0, CompositeException.class); + List<Throwable> inners = TestHelper.compositeList(errors.get(0)); + TestHelper.assertError(inners, 0, IllegalArgumentException.class); + TestHelper.assertError(inners, 1, IOException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void onNextCrashNoError() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + subscribeAutoDispose(processor, composite, t -> { + throw new IOException(); + }, Functions.ON_ERROR_MISSING, () -> { }); + + processor.onNext(1); + + assertTrue(events.toString(), events.isEmpty()); + + TestHelper.assertError(errors, 0, OnErrorNotImplementedException.class); + assertTrue(errors.get(0).getCause() instanceof IOException); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void onCompleteCrash() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + subscribeAutoDispose(processor, composite, this, this, new Action() { + @Override + public void run() throws Exception { + throw new IOException(); + } + }); + + processor.onNext(1); + processor.onComplete(); + + assertEquals(Arrays.asList(1), events); + + TestHelper.assertUndeliverable(errors, 0, IOException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void badSource() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + subscribeAutoDispose( + new Observable<Integer>() { + @Override + protected void subscribeActual( + Observer<? super Integer> observer) { + observer.onSubscribe(Disposable.empty()); + observer.onNext(1); + observer.onComplete(); + + observer.onSubscribe(Disposable.empty()); + observer.onNext(2); + observer.onComplete(); + observer.onError(new IOException()); + } + }, composite, this, this, this + ); + + assertEquals(Arrays.<Object>asList(1, "OnComplete"), events); + + TestHelper.assertUndeliverable(errors, 0, IOException.class); + } finally { + RxJavaPlugins.reset(); + } + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/observers/QueueDrainObserverTest.java b/src/test/java/io/reactivex/rxjava3/internal/observers/QueueDrainObserverTest.java new file mode 100644 index 0000000000..eb2523a466 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/observers/QueueDrainObserverTest.java @@ -0,0 +1,163 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.observers; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.*; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.operators.SpscArrayQueue; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class QueueDrainObserverTest extends RxJavaTest { + + static final QueueDrainObserver<Integer, Integer, Integer> createUnordered(TestObserver<Integer> to, final Disposable d) { + return new QueueDrainObserver<Integer, Integer, Integer>(to, new SpscArrayQueue<>(4)) { + @Override + public void onNext(Integer t) { + fastPathEmit(t, false, d); + } + + @Override + public void onError(Throwable t) { + } + + @Override + public void onComplete() { + } + + @Override + public void onSubscribe(Disposable d) { + } + + @Override + public void accept(Observer<? super Integer> a, Integer v) { + super.accept(a, v); + a.onNext(v); + } + }; + } + + static final QueueDrainObserver<Integer, Integer, Integer> createOrdered(TestObserver<Integer> to, final Disposable d) { + return new QueueDrainObserver<Integer, Integer, Integer>(to, new SpscArrayQueue<>(4)) { + @Override + public void onNext(Integer t) { + fastPathOrderedEmit(t, false, d); + } + + @Override + public void onError(Throwable t) { + } + + @Override + public void onComplete() { + } + + @Override + public void onSubscribe(Disposable d) { + } + + @Override + public void accept(Observer<? super Integer> a, Integer v) { + super.accept(a, v); + a.onNext(v); + } + }; + } + + @Test + public void unorderedSlowPath() { + TestObserver<Integer> to = new TestObserver<>(); + Disposable d = Disposable.empty(); + QueueDrainObserver<Integer, Integer, Integer> qd = createUnordered(to, d); + to.onSubscribe(Disposable.empty()); + + qd.enter(); + qd.onNext(1); + + to.assertEmpty(); + } + + @Test + public void orderedSlowPath() { + TestObserver<Integer> to = new TestObserver<>(); + Disposable d = Disposable.empty(); + QueueDrainObserver<Integer, Integer, Integer> qd = createOrdered(to, d); + to.onSubscribe(Disposable.empty()); + + qd.enter(); + qd.onNext(1); + + to.assertEmpty(); + } + + @Test + public void orderedSlowPathNonEmptyQueue() { + TestObserver<Integer> to = new TestObserver<>(); + Disposable d = Disposable.empty(); + QueueDrainObserver<Integer, Integer, Integer> qd = createOrdered(to, d); + to.onSubscribe(Disposable.empty()); + + qd.queue.offer(0); + qd.onNext(1); + + to.assertValuesOnly(0, 1); + } + + @Test + public void unorderedOnNextRace() { + for (int i = 0; i < TestHelper.RACE_LONG_LOOPS; i++) { + + TestObserver<Integer> to = new TestObserver<>(); + Disposable d = Disposable.empty(); + final QueueDrainObserver<Integer, Integer, Integer> qd = createUnordered(to, d); + to.onSubscribe(Disposable.empty()); + + Runnable r1 = new Runnable() { + @Override + public void run() { + qd.onNext(1); + } + }; + + TestHelper.race(r1, r1); + + to.assertValuesOnly(1, 1); + } + } + + @Test + public void orderedOnNextRace() { + for (int i = 0; i < TestHelper.RACE_LONG_LOOPS; i++) { + + TestObserver<Integer> to = new TestObserver<>(); + Disposable d = Disposable.empty(); + final QueueDrainObserver<Integer, Integer, Integer> qd = createOrdered(to, d); + to.onSubscribe(Disposable.empty()); + + Runnable r1 = new Runnable() { + @Override + public void run() { + qd.onNext(1); + } + }; + + TestHelper.race(r1, r1); + + to.assertValuesOnly(1, 1); + } + } + +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/observers/SingleConsumersTest.java b/src/test/java/io/reactivex/rxjava3/internal/observers/SingleConsumersTest.java new file mode 100644 index 0000000000..a840d8f71a --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/observers/SingleConsumersTest.java @@ -0,0 +1,192 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +/* + * Copyright 2016-2019 David Karnok + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.reactivex.rxjava3.internal.observers; + +import static org.junit.Assert.*; + +import java.io.IOException; +import java.util.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.*; +import io.reactivex.rxjava3.exceptions.CompositeException; +import io.reactivex.rxjava3.functions.Consumer; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.observers.LambdaConsumerIntrospection; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.subjects.SingleSubject; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class SingleConsumersTest implements Consumer<Object> { + + final CompositeDisposable composite = new CompositeDisposable(); + + final SingleSubject<Integer> processor = SingleSubject.create(); + + final List<Object> events = new ArrayList<>(); + + @Override + public void accept(Object t) throws Exception { + events.add(t); + } + + static <T> Disposable subscribeAutoDispose(Single<T> source, CompositeDisposable composite, + Consumer<? super T> onSuccess, Consumer<? super Throwable> onError) { + return source.subscribe(onSuccess, onError, composite); + } + + @Test + public void onSuccessNormal() { + + Disposable d = subscribeAutoDispose(processor, composite, this, Functions.ON_ERROR_MISSING); + + assertFalse(d.getClass().toString(), ((LambdaConsumerIntrospection)d).hasCustomOnError()); + + assertTrue(composite.size() > 0); + + assertTrue(events.toString(), events.isEmpty()); + + processor.onSuccess(1); + + assertEquals(0, composite.size()); + + assertEquals(Arrays.<Object>asList(1), events); + + } + + @Test + public void onErrorNormal() { + + subscribeAutoDispose(processor, composite, this, this); + + assertTrue(composite.size() > 0); + + assertTrue(events.toString(), events.isEmpty()); + + processor.onSuccess(1); + + assertEquals(0, composite.size()); + + assertEquals(Arrays.<Object>asList(1), events); + + } + + @Test + public void onErrorError() { + + Disposable d = subscribeAutoDispose(processor, composite, this, this); + + assertTrue(d.getClass().toString(), ((LambdaConsumerIntrospection)d).hasCustomOnError()); + + assertTrue(composite.size() > 0); + + assertTrue(events.toString(), events.isEmpty()); + + processor.onError(new IOException()); + + assertTrue(events.toString(), events.get(0) instanceof IOException); + + assertEquals(0, composite.size()); + } + + @Test + public void onSuccessCrash() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + subscribeAutoDispose(processor, composite, new Consumer<Object>() { + @Override + public void accept(Object t) throws Exception { + throw new IOException(); + } + }, this); + + processor.onSuccess(1); + + assertTrue(events.toString(), events.isEmpty()); + + TestHelper.assertUndeliverable(errors, 0, IOException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void onErrorCrash() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + subscribeAutoDispose(processor, composite, this, new Consumer<Throwable>() { + @Override + public void accept(Throwable t) throws Exception { + throw new IOException(t); + } + }); + + processor.onError(new IllegalArgumentException()); + + assertTrue(events.toString(), events.isEmpty()); + + TestHelper.assertError(errors, 0, CompositeException.class); + List<Throwable> inners = TestHelper.compositeList(errors.get(0)); + TestHelper.assertError(inners, 0, IllegalArgumentException.class); + TestHelper.assertError(inners, 1, IOException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void badSource() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + subscribeAutoDispose( + new Single<Integer>() { + @Override + protected void subscribeActual( + SingleObserver<? super Integer> observer) { + observer.onSubscribe(Disposable.empty()); + observer.onSuccess(1); + + observer.onSubscribe(Disposable.empty()); + observer.onSuccess(2); + observer.onError(new IOException()); + } + }, composite, this, this + ); + + assertEquals(Arrays.<Object>asList(1), events); + + TestHelper.assertUndeliverable(errors, 0, IOException.class); + } finally { + RxJavaPlugins.reset(); + } + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/completable/CompletableAmbTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/completable/CompletableAmbTest.java new file mode 100644 index 0000000000..0c9971017b --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/completable/CompletableAmbTest.java @@ -0,0 +1,332 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.completable; + +import static org.junit.Assert.*; + +import java.util.*; +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.internal.operators.completable.CompletableAmb.Amb; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.processors.PublishProcessor; +import io.reactivex.rxjava3.schedulers.Schedulers; +import io.reactivex.rxjava3.subjects.*; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class CompletableAmbTest extends RxJavaTest { + + @Test + public void ambLots() { + List<Completable> ms = new ArrayList<>(); + + for (int i = 0; i < 32; i++) { + ms.add(Completable.never()); + } + + ms.add(Completable.complete()); + + Completable.amb(ms) + .test() + .assertResult(); + } + + @Test + public void ambFirstDone() { + Completable.amb(Arrays.asList(Completable.complete(), Completable.complete())) + .test() + .assertResult(); + } + + @Test + public void dispose() { + PublishProcessor<Integer> pp1 = PublishProcessor.create(); + PublishProcessor<Integer> pp2 = PublishProcessor.create(); + + TestObserver<Void> to = Completable.amb(Arrays.asList(pp1.ignoreElements(), pp2.ignoreElements())) + .test(); + + assertTrue(pp1.hasSubscribers()); + assertTrue(pp2.hasSubscribers()); + + to.dispose(); + + assertFalse(pp1.hasSubscribers()); + assertFalse(pp2.hasSubscribers()); + } + + @Test + public void innerErrorRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + final PublishProcessor<Integer> pp0 = PublishProcessor.create(); + final PublishProcessor<Integer> pp1 = PublishProcessor.create(); + + final TestObserver<Void> to = Completable.amb(Arrays.asList(pp0.ignoreElements(), pp1.ignoreElements())) + .test(); + + final TestException ex = new TestException(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + pp0.onError(ex); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + pp1.onError(ex); + } + }; + + TestHelper.race(r1, r2); + + to.assertFailure(TestException.class); + + if (!errors.isEmpty()) { + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } + } finally { + RxJavaPlugins.reset(); + } + } + } + + @Test + public void nullSourceSuccessRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + List<Throwable> errors = TestHelper.trackPluginErrors(); + + try { + + final Subject<Integer> ps = ReplaySubject.create(); + ps.onNext(1); + + final Completable source = Completable.ambArray(ps.ignoreElements(), Completable.never(), Completable.never(), null); + + Runnable r1 = new Runnable() { + @Override + public void run() { + source.test(); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + ps.onComplete(); + } + }; + + TestHelper.race(r1, r2); + + if (!errors.isEmpty()) { + TestHelper.assertError(errors, 0, NullPointerException.class); + } + } finally { + RxJavaPlugins.reset(); + } + } + } + + @Test + public void ambWithOrder() { + Completable error = Completable.error(new RuntimeException()); + Completable.complete().ambWith(error).test().assertComplete(); + } + + @Test + public void ambIterableOrder() { + Completable error = Completable.error(new RuntimeException()); + Completable.amb(Arrays.asList(Completable.complete(), error)).test().assertComplete(); + } + + @Test + public void ambArrayOrder() { + Completable error = Completable.error(new RuntimeException()); + Completable.ambArray(Completable.complete(), error).test().assertComplete(); + } + + @Test + public void ambRace() { + TestObserver<Void> to = new TestObserver<>(); + to.onSubscribe(Disposable.empty()); + + CompositeDisposable cd = new CompositeDisposable(); + AtomicBoolean once = new AtomicBoolean(); + Amb a = new Amb(once, cd, to); + a.onSubscribe(Disposable.empty()); + + a.onComplete(); + a.onComplete(); + + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + a.onError(new TestException()); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void untilCompletableMainComplete() { + CompletableSubject main = CompletableSubject.create(); + CompletableSubject other = CompletableSubject.create(); + + TestObserver<Void> to = main.ambWith(other).test(); + + assertTrue("Main no observers?", main.hasObservers()); + assertTrue("Other no observers?", other.hasObservers()); + + main.onComplete(); + + assertFalse("Main has observers?", main.hasObservers()); + assertFalse("Other has observers?", other.hasObservers()); + + to.assertResult(); + } + + @Test + public void untilCompletableMainError() { + CompletableSubject main = CompletableSubject.create(); + CompletableSubject other = CompletableSubject.create(); + + TestObserver<Void> to = main.ambWith(other).test(); + + assertTrue("Main no observers?", main.hasObservers()); + assertTrue("Other no observers?", other.hasObservers()); + + main.onError(new TestException()); + + assertFalse("Main has observers?", main.hasObservers()); + assertFalse("Other has observers?", other.hasObservers()); + + to.assertFailure(TestException.class); + } + + @Test + public void untilCompletableOtherOnComplete() { + CompletableSubject main = CompletableSubject.create(); + CompletableSubject other = CompletableSubject.create(); + + TestObserver<Void> to = main.ambWith(other).test(); + + assertTrue("Main no observers?", main.hasObservers()); + assertTrue("Other no observers?", other.hasObservers()); + + other.onComplete(); + + assertFalse("Main has observers?", main.hasObservers()); + assertFalse("Other has observers?", other.hasObservers()); + + to.assertResult(); + } + + @Test + public void untilCompletableOtherError() { + CompletableSubject main = CompletableSubject.create(); + CompletableSubject other = CompletableSubject.create(); + + TestObserver<Void> to = main.ambWith(other).test(); + + assertTrue("Main no observers?", main.hasObservers()); + assertTrue("Other no observers?", other.hasObservers()); + + other.onError(new TestException()); + + assertFalse("Main has observers?", main.hasObservers()); + assertFalse("Other has observers?", other.hasObservers()); + + to.assertFailure(TestException.class); + } + + @Test + public void noWinnerErrorDispose() throws Exception { + final TestException ex = new TestException(); + for (int i = 0; i < TestHelper.RACE_LONG_LOOPS; i++) { + final AtomicBoolean interrupted = new AtomicBoolean(); + final CountDownLatch cdl = new CountDownLatch(1); + + Completable.ambArray( + Completable.error(ex) + .subscribeOn(Schedulers.single()) + .observeOn(Schedulers.computation()), + Completable.never() + ) + .subscribe(Functions.EMPTY_ACTION, new Consumer<Throwable>() { + @Override + public void accept(Throwable e) throws Exception { + interrupted.set(Thread.currentThread().isInterrupted()); + cdl.countDown(); + } + }); + + assertTrue(cdl.await(500, TimeUnit.SECONDS)); + assertFalse("Interrupted!", interrupted.get()); + } + } + + @Test + public void noWinnerCompleteDispose() throws Exception { + for (int i = 0; i < TestHelper.RACE_LONG_LOOPS; i++) { + final AtomicBoolean interrupted = new AtomicBoolean(); + final CountDownLatch cdl = new CountDownLatch(1); + + Completable.ambArray( + Completable.complete() + .subscribeOn(Schedulers.single()) + .observeOn(Schedulers.computation()), + Completable.never() + ) + .subscribe(new Action() { + @Override + public void run() throws Exception { + interrupted.set(Thread.currentThread().isInterrupted()); + cdl.countDown(); + } + }); + + assertTrue(cdl.await(500, TimeUnit.SECONDS)); + assertFalse("Interrupted!", interrupted.get()); + } + } + + @Test + public void completableSourcesInIterable() { + CompletableSource source = new CompletableSource() { + @Override + public void subscribe(CompletableObserver observer) { + Completable.complete().subscribe(observer); + } + }; + + Completable.amb(Arrays.asList(source, source)) + .test() + .assertResult(); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/completable/CompletableAndThenCompletableTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/completable/CompletableAndThenCompletableTest.java new file mode 100644 index 0000000000..070fa8cb4d --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/completable/CompletableAndThenCompletableTest.java @@ -0,0 +1,185 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.completable; + +import static org.junit.Assert.*; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.Action; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.schedulers.Schedulers; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class CompletableAndThenCompletableTest extends RxJavaTest { + @Test + public void andThenCompletableCompleteComplete() { + Completable.complete() + .andThen(Completable.complete()) + .test() + .assertComplete(); + } + + @Test + public void andThenCompletableCompleteError() { + Completable.complete() + .andThen(Completable.error(new TestException("test"))) + .to(TestHelper.testConsumer()) + .assertNotComplete() + .assertNoValues() + .assertError(TestException.class) + .assertErrorMessage("test"); + } + + @Test + public void andThenCompletableCompleteNever() { + Completable.complete() + .andThen(Completable.never()) + .test() + .assertNoValues() + .assertNoErrors() + .assertNotComplete(); + } + + @Test + public void andThenCompletableErrorComplete() { + Completable.error(new TestException("bla")) + .andThen(Completable.complete()) + .to(TestHelper.testConsumer()) + .assertNotComplete() + .assertNoValues() + .assertError(TestException.class) + .assertErrorMessage("bla"); + } + + @Test + public void andThenCompletableErrorNever() { + Completable.error(new TestException("bla")) + .andThen(Completable.never()) + .to(TestHelper.testConsumer()) + .assertNotComplete() + .assertNoValues() + .assertError(TestException.class) + .assertErrorMessage("bla"); + } + + @Test + public void andThenCompletableErrorError() { + Completable.error(new TestException("error1")) + .andThen(Completable.error(new TestException("error2"))) + .to(TestHelper.testConsumer()) + .assertNotComplete() + .assertNoValues() + .assertError(TestException.class) + .assertErrorMessage("error1"); + } + + @Test + public void andThenCanceled() { + final AtomicInteger completableRunCount = new AtomicInteger(); + Completable.fromRunnable(new Runnable() { + @Override + public void run() { + completableRunCount.incrementAndGet(); + } + }) + .andThen(Completable.complete()) + .test(true) + .assertEmpty(); + assertEquals(0, completableRunCount.get()); + } + + @Test + public void andThenFirstCancels() { + final TestObserver<Void> to = new TestObserver<>(); + Completable.fromRunnable(new Runnable() { + @Override + public void run() { + to.dispose(); + } + }) + .andThen(Completable.complete()) + .subscribe(to); + to + .assertNotComplete() + .assertNoErrors(); + } + + @Test + public void andThenSecondCancels() { + final TestObserver<Void> to = new TestObserver<>(); + Completable.complete() + .andThen(Completable.fromRunnable(new Runnable() { + @Override + public void run() { + to.dispose(); + } + })) + .subscribe(to); + to + .assertNotComplete() + .assertNoErrors(); + } + + @Test + public void andThenDisposed() { + TestHelper.checkDisposed(Completable.complete() + .andThen(Completable.complete())); + } + + @Test + public void andThenNoInterrupt() throws InterruptedException { + for (int k = 0; k < 100; k++) { + final int count = 10; + final CountDownLatch latch = new CountDownLatch(count); + final boolean[] interrupted = {false}; + + for (int i = 0; i < count; i++) { + Completable.complete() + .subscribeOn(Schedulers.io()) + .observeOn(Schedulers.io()) + .andThen(Completable.fromAction(new Action() { + @Override + public void run() throws Exception { + try { + Thread.sleep(30); + } catch (InterruptedException e) { + System.out.println("Interrupted! " + Thread.currentThread()); + interrupted[0] = true; + } + } + })) + .subscribe(new Action() { + @Override + public void run() throws Exception { + latch.countDown(); + } + }); + } + + latch.await(); + assertFalse("The second Completable was interrupted!", interrupted[0]); + } + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeCompletable(c -> c.andThen(c)); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/completable/CompletableAndThenTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/completable/CompletableAndThenTest.java new file mode 100644 index 0000000000..943d44b9f2 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/completable/CompletableAndThenTest.java @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.completable; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class CompletableAndThenTest extends RxJavaTest { + @Test + public void andThenMaybeCompleteValue() { + Completable.complete() + .andThen(Maybe.just(1)) + .test() + .assertResult(1); + } + + @Test + public void andThenMaybeCompleteError() { + Completable.complete() + .andThen(Maybe.error(new RuntimeException("test"))) + .to(TestHelper.testConsumer()) + .assertNotComplete() + .assertNoValues() + .assertError(RuntimeException.class) + .assertErrorMessage("test"); + } + + @Test + public void andThenMaybeCompleteEmpty() { + Completable.complete() + .andThen(Maybe.empty()) + .test() + .assertNoValues() + .assertNoErrors() + .assertComplete(); + } + + @Test + public void andThenMaybeError() { + Completable.error(new RuntimeException("bla")) + .andThen(Maybe.empty()) + .to(TestHelper.testConsumer()) + .assertNotComplete() + .assertNoValues() + .assertError(RuntimeException.class) + .assertErrorMessage("bla"); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/completable/CompletableAwaitTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/completable/CompletableAwaitTest.java new file mode 100644 index 0000000000..b614e2d79e --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/completable/CompletableAwaitTest.java @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.completable; + +import static org.junit.Assert.*; + +import java.util.concurrent.TimeUnit; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.RxJavaTest; +import io.reactivex.rxjava3.processors.PublishProcessor; + +public class CompletableAwaitTest extends RxJavaTest { + + @Test + public void awaitInterrupted() { + + Thread.currentThread().interrupt(); + + try { + PublishProcessor.create().ignoreElements().blockingAwait(); + fail("Should have thrown RuntimeException"); + } catch (RuntimeException ex) { + if (!(ex.getCause() instanceof InterruptedException)) { + fail("Wrong cause: " + ex.getCause()); + } + } + + } + + @Test + public void awaitTimeoutInterrupted() { + + Thread.currentThread().interrupt(); + + try { + PublishProcessor.create().ignoreElements().blockingAwait(1, TimeUnit.SECONDS); + fail("Should have thrown RuntimeException"); + } catch (RuntimeException ex) { + if (!(ex.getCause() instanceof InterruptedException)) { + fail("Wrong cause: " + ex.getCause()); + } + } + + } + + @Test + public void awaitTimeout() { + assertFalse(PublishProcessor.create().ignoreElements().blockingAwait(100, TimeUnit.MILLISECONDS)); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/completable/CompletableBlockingSubscribeTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/completable/CompletableBlockingSubscribeTest.java new file mode 100644 index 0000000000..a84f78ad73 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/completable/CompletableBlockingSubscribeTest.java @@ -0,0 +1,331 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.completable; + +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import java.util.concurrent.TimeUnit; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.Completable; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.schedulers.Schedulers; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class CompletableBlockingSubscribeTest { + + @Test + public void noArgComplete() { + Completable.complete() + .blockingSubscribe(); + } + + @Test + public void noArgCompleteAsync() { + Completable.complete() + .delay(100, TimeUnit.MILLISECONDS) + .blockingSubscribe(); + } + + @Test + public void noArgError() throws Throwable { + TestHelper.withErrorTracking(errors -> { + Completable.error(new TestException()) + .blockingSubscribe(); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + }); + } + + @Test + public void noArgErrorAsync() throws Throwable { + TestHelper.withErrorTracking(errors -> { + Completable.error(new TestException()) + .delay(100, TimeUnit.MILLISECONDS, Schedulers.computation(), true) + .blockingSubscribe(); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + }); + } + + @Test + public void oneArgComplete() throws Throwable { + Action action = mock(Action.class); + + Completable.complete() + .blockingSubscribe(action); + + verify(action).run(); + } + + @Test + public void oneArgCompleteAsync() throws Throwable { + Action action = mock(Action.class); + + Completable.complete() + .delay(50, TimeUnit.MILLISECONDS) + .blockingSubscribe(action); + + verify(action).run(); + } + + @Test + public void oneArgCompleteFails() throws Throwable { + TestHelper.withErrorTracking(errors -> { + Action action = mock(Action.class); + doThrow(new TestException()).when(action).run(); + + Completable.complete() + .blockingSubscribe(action); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + + verify(action).run(); + }); + } + + @Test + public void oneArgError() throws Throwable { + TestHelper.withErrorTracking(errors -> { + Action action = mock(Action.class); + + Completable.error(new TestException()) + .blockingSubscribe(action); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + + verify(action, never()).run(); + }); + } + + @Test + public void oneArgErrorAsync() throws Throwable { + TestHelper.withErrorTracking(errors -> { + Action action = mock(Action.class); + + Completable.error(new TestException()) + .delay(50, TimeUnit.MILLISECONDS, Schedulers.computation(), true) + .blockingSubscribe(action); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + + verify(action, never()).run(); + }); + } + + @Test + public void twoArgComplete() throws Throwable { + Action action = mock(Action.class); + @SuppressWarnings("unchecked") + Consumer<? super Throwable> consumer = mock(Consumer.class); + + Completable.complete() + .blockingSubscribe(action, consumer); + + verify(action).run(); + verify(consumer, never()).accept(any()); + } + + @Test + public void twoArgCompleteAsync() throws Throwable { + Action action = mock(Action.class); + @SuppressWarnings("unchecked") + Consumer<? super Throwable> consumer = mock(Consumer.class); + + Completable.complete() + .delay(50, TimeUnit.MILLISECONDS) + .blockingSubscribe(action, consumer); + + verify(action).run(); + verify(consumer, never()).accept(any()); + } + + @Test + public void twoArgCompleteFails() throws Throwable { + TestHelper.withErrorTracking(errors -> { + Action action = mock(Action.class); + doThrow(new TestException()).when(action).run(); + @SuppressWarnings("unchecked") + Consumer<? super Throwable> consumer = mock(Consumer.class); + + Completable.complete() + .blockingSubscribe(action, consumer); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + + verify(action).run(); + verify(consumer, never()).accept(any()); + }); + } + + @Test + public void twoArgError() throws Throwable { + TestHelper.withErrorTracking(errors -> { + Action action = mock(Action.class); + @SuppressWarnings("unchecked") + Consumer<? super Throwable> consumer = mock(Consumer.class); + + Completable.error(new TestException()) + .blockingSubscribe(action, consumer); + + assertTrue("" + errors, errors.isEmpty()); + + verify(action, never()).run(); + verify(consumer).accept(any(TestException.class)); + }); + } + + @Test + public void twoArgErrorAsync() throws Throwable { + TestHelper.withErrorTracking(errors -> { + Action action = mock(Action.class); + @SuppressWarnings("unchecked") + Consumer<? super Throwable> consumer = mock(Consumer.class); + + Completable.error(new TestException()) + .delay(50, TimeUnit.MILLISECONDS, Schedulers.computation(), true) + .blockingSubscribe(action, consumer); + + assertTrue("" + errors, errors.isEmpty()); + + verify(action, never()).run(); + verify(consumer).accept(any(TestException.class)); + }); + } + + @Test + public void twoArgErrorFails() throws Throwable { + TestHelper.withErrorTracking(errors -> { + Action action = mock(Action.class); + @SuppressWarnings("unchecked") + Consumer<? super Throwable> consumer = mock(Consumer.class); + doThrow(new TestException()).when(consumer).accept(any()); + + Completable.error(new TestException()) + .delay(50, TimeUnit.MILLISECONDS, Schedulers.computation(), true) + .blockingSubscribe(action, consumer); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + + verify(action, never()).run(); + verify(consumer).accept(any(TestException.class)); + }); + } + + @Test + public void twoArgInterrupted() throws Throwable { + TestHelper.withErrorTracking(errors -> { + Action onDispose = mock(Action.class); + + Action action = mock(Action.class); + @SuppressWarnings("unchecked") + Consumer<? super Throwable> consumer = mock(Consumer.class); + + Thread.currentThread().interrupt(); + + Completable.never() + .doOnDispose(onDispose) + .blockingSubscribe(action, consumer); + + assertTrue("" + errors, errors.isEmpty()); + + verify(onDispose).run(); + verify(action, never()).run(); + verify(consumer).accept(any(InterruptedException.class)); + }); + } + + @Test + public void observerComplete() { + TestObserver<Void> to = new TestObserver<>(); + + Completable.complete() + .blockingSubscribe(to); + + to.assertResult(); + } + + @Test + public void observerCompleteAsync() { + TestObserver<Void> to = new TestObserver<>(); + + Completable.complete() + .delay(50, TimeUnit.MILLISECONDS, Schedulers.computation(), true) + .blockingSubscribe(to); + + to.assertResult(); + } + + @Test + public void observerError() { + TestObserver<Void> to = new TestObserver<>(); + + Completable.error(new TestException()) + .blockingSubscribe(to); + + to.assertFailure(TestException.class); + } + + @Test + public void observerErrorAsync() { + TestObserver<Void> to = new TestObserver<>(); + + Completable.error(new TestException()) + .delay(50, TimeUnit.MILLISECONDS, Schedulers.computation(), true) + .blockingSubscribe(to); + + to.assertFailure(TestException.class); + } + + @Test + public void observerDispose() throws Throwable { + Action onDispose = mock(Action.class); + + TestObserver<Void> to = new TestObserver<>(); + to.dispose(); + + Completable.never() + .doOnDispose(onDispose) + .blockingSubscribe(to); + + to.assertEmpty(); + + verify(onDispose).run(); + } + + @Test + public void ovserverInterrupted() throws Throwable { + TestHelper.withErrorTracking(errors -> { + Action onDispose = mock(Action.class); + + TestObserver<Void> to = new TestObserver<>(); + + Thread.currentThread().interrupt(); + + Completable.never() + .doOnDispose(onDispose) + .blockingSubscribe(to); + + assertTrue("" + errors, errors.isEmpty()); + + verify(onDispose).run(); + to.assertFailure(InterruptedException.class); + }); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/completable/CompletableCacheTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/completable/CompletableCacheTest.java new file mode 100644 index 0000000000..3e6193a2e9 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/completable/CompletableCacheTest.java @@ -0,0 +1,267 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.completable; + +import static org.junit.Assert.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.disposables.EmptyDisposable; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.subjects.PublishSubject; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class CompletableCacheTest extends RxJavaTest implements Consumer<Object>, Action { + + volatile int count; + + @Override + public void accept(Object t) throws Exception { + count++; + } + + @Override + public void run() throws Exception { + count++; + } + + @Test + public void normal() { + Completable c = Completable.complete() + .doOnSubscribe(this) + .cache(); + + assertEquals(0, count); + + c.test().assertResult(); + + assertEquals(1, count); + + c.test().assertResult(); + + assertEquals(1, count); + + c.test().assertResult(); + + assertEquals(1, count); + } + + @Test + public void error() { + Completable c = Completable.error(new TestException()) + .doOnSubscribe(this) + .cache(); + + assertEquals(0, count); + + c.test().assertFailure(TestException.class); + + assertEquals(1, count); + + c.test().assertFailure(TestException.class); + + assertEquals(1, count); + + c.test().assertFailure(TestException.class); + + assertEquals(1, count); + } + + @Test + public void crossDispose() { + PublishSubject<Integer> ps = PublishSubject.create(); + + final TestObserver<Void> to1 = new TestObserver<>(); + + final TestObserver<Void> to2 = new TestObserver<Void>() { + @Override + public void onComplete() { + super.onComplete(); + to1.dispose(); + } + }; + + Completable c = ps.ignoreElements().cache(); + + c.subscribe(to2); + c.subscribe(to1); + + ps.onComplete(); + + to1.assertEmpty(); + to2.assertResult(); + } + + @Test + public void crossDisposeOnError() { + PublishSubject<Integer> ps = PublishSubject.create(); + + final TestObserver<Void> to1 = new TestObserver<>(); + + final TestObserver<Void> to2 = new TestObserver<Void>() { + @Override + public void onError(Throwable ex) { + super.onError(ex); + to1.dispose(); + } + }; + + Completable c = ps.ignoreElements().cache(); + + c.subscribe(to2); + c.subscribe(to1); + + ps.onError(new TestException()); + + to1.assertEmpty(); + to2.assertFailure(TestException.class); + } + + @Test + public void dispose() { + PublishSubject<Integer> ps = PublishSubject.create(); + + Completable c = ps.ignoreElements().cache(); + + assertFalse(ps.hasObservers()); + + TestObserver<Void> to1 = c.test(); + + assertTrue(ps.hasObservers()); + + to1.dispose(); + + assertTrue(ps.hasObservers()); + + TestObserver<Void> to2 = c.test(); + + TestObserver<Void> to3 = c.test(); + to3.dispose(); + + TestObserver<Void> to4 = c.test(true); + to3.dispose(); + + ps.onComplete(); + + to1.assertEmpty(); + + to2.assertResult(); + + to3.assertEmpty(); + + to4.assertEmpty(); + } + + @Test + public void subscribeRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + PublishSubject<Integer> ps = PublishSubject.create(); + + final Completable c = ps.ignoreElements().cache(); + + final TestObserver<Void> to1 = new TestObserver<>(); + + final TestObserver<Void> to2 = new TestObserver<>(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + c.subscribe(to1); + } + }; + Runnable r2 = new Runnable() { + @Override + public void run() { + c.subscribe(to2); + } + }; + + TestHelper.race(r1, r2); + + ps.onComplete(); + + to1.assertResult(); + to2.assertResult(); + } + } + + @Test + public void subscribeDisposeRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + PublishSubject<Integer> ps = PublishSubject.create(); + + final Completable c = ps.ignoreElements().cache(); + + final TestObserver<Void> to1 = c.test(); + + final TestObserver<Void> to2 = new TestObserver<>(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + to1.dispose(); + } + }; + Runnable r2 = new Runnable() { + @Override + public void run() { + c.subscribe(to2); + } + }; + + TestHelper.race(r1, r2); + + ps.onComplete(); + + to1.assertEmpty(); + to2.assertResult(); + } + } + + @Test + public void doubleDispose() { + PublishSubject<Integer> ps = PublishSubject.create(); + + final TestObserver<Void> to = new TestObserver<>(); + + ps.ignoreElements().cache() + .subscribe(new CompletableObserver() { + + @Override + public void onSubscribe(Disposable d) { + to.onSubscribe(EmptyDisposable.INSTANCE); + d.dispose(); + d.dispose(); + } + + @Override + public void onComplete() { + to.onComplete(); + } + + @Override + public void onError(Throwable e) { + to.onError(e); + } + }); + + ps.onComplete(); + + to.assertEmpty(); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/completable/CompletableConcatArrayDelayErrorTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/completable/CompletableConcatArrayDelayErrorTest.java new file mode 100644 index 0000000000..418afc70cc --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/completable/CompletableConcatArrayDelayErrorTest.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.completable; + +import static org.mockito.Mockito.*; +import org.junit.Test; + +import io.reactivex.rxjava3.core.Completable; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.Action; + +public class CompletableConcatArrayDelayErrorTest { + + @Test + public void normal() throws Throwable { + Action action1 = mock(Action.class); + Action action2 = mock(Action.class); + + Completable.concatArrayDelayError( + Completable.fromAction(action1), + Completable.error(new TestException()), + Completable.fromAction(action2) + ) + .test() + .assertFailure(TestException.class); + + verify(action1).run(); + + verify(action2).run(); + } + +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/completable/CompletableConcatDelayErrorTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/completable/CompletableConcatDelayErrorTest.java new file mode 100644 index 0000000000..8a9f9477d8 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/completable/CompletableConcatDelayErrorTest.java @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.completable; + +import static org.mockito.Mockito.*; + +import java.util.Arrays; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.Action; + +public class CompletableConcatDelayErrorTest { + + @Test + public void normalIterable() throws Throwable { + Action action1 = mock(Action.class); + Action action2 = mock(Action.class); + + Completable.concatDelayError(Arrays.asList( + Completable.fromAction(action1), + Completable.error(new TestException()), + Completable.fromAction(action2) + )) + .test() + .assertFailure(TestException.class); + + verify(action1).run(); + + verify(action2).run(); + } + + @Test + public void normalPublisher() throws Throwable { + Action action1 = mock(Action.class); + Action action2 = mock(Action.class); + + Completable.concatDelayError(Flowable.fromArray( + Completable.fromAction(action1), + Completable.error(new TestException()), + Completable.fromAction(action2) + )) + .test() + .assertFailure(TestException.class); + + verify(action1).run(); + + verify(action2).run(); + } + + @Test + public void normalPublisherPrefetch() throws Throwable { + Action action1 = mock(Action.class); + Action action2 = mock(Action.class); + + Completable.concatDelayError(Flowable.fromArray( + Completable.fromAction(action1), + Completable.error(new TestException()), + Completable.fromAction(action2) + ), 1) + .test() + .assertFailure(TestException.class); + + verify(action1).run(); + + verify(action2).run(); + } + +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/completable/CompletableConcatTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/completable/CompletableConcatTest.java new file mode 100644 index 0000000000..11bc1a9b06 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/completable/CompletableConcatTest.java @@ -0,0 +1,308 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.completable; + +import static org.junit.Assert.*; + +import java.util.*; +import java.util.concurrent.CountDownLatch; + +import io.reactivex.rxjava3.disposables.Disposable; +import org.junit.Test; +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.subscriptions.BooleanSubscription; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.processors.*; +import io.reactivex.rxjava3.schedulers.Schedulers; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class CompletableConcatTest extends RxJavaTest { + + @Test + public void overflowReported() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + Completable.concat( + Flowable.fromPublisher(new Publisher<Completable>() { + @Override + public void subscribe(Subscriber<? super Completable> s) { + s.onSubscribe(new BooleanSubscription()); + s.onNext(Completable.never()); + s.onNext(Completable.never()); + s.onNext(Completable.never()); + s.onNext(Completable.never()); + s.onComplete(); + } + }), 1 + ) + .test() + .assertFailure(QueueOverflowException.class); + + TestHelper.assertError(errors, 0, QueueOverflowException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void invalidPrefetch() { + try { + Completable.concat(Flowable.just(Completable.complete()), -99); + fail("Should have thrown IllegalArgumentExceptio"); + } catch (IllegalArgumentException ex) { + assertEquals("prefetch > 0 required but it was -99", ex.getMessage()); + } + } + + @Test + public void dispose() { + TestHelper.checkDisposed(Completable.concat(Flowable.just(Completable.complete()))); + } + + @Test + public void errorRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + List<Throwable> errors = TestHelper.trackPluginErrors(); + + try { + final PublishProcessor<Integer> pp1 = PublishProcessor.create(); + final PublishProcessor<Integer> pp2 = PublishProcessor.create(); + + TestObserver<Void> to = Completable.concat(pp1.map(new Function<Integer, Completable>() { + @Override + public Completable apply(Integer v) throws Exception { + return pp2.ignoreElements(); + } + })).test(); + + pp1.onNext(1); + + final TestException ex = new TestException(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + pp1.onError(ex); + } + }; + Runnable r2 = new Runnable() { + @Override + public void run() { + pp2.onError(ex); + } + }; + + TestHelper.race(r1, r2); + + to.assertFailure(TestException.class); + + if (!errors.isEmpty()) { + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } + } finally { + RxJavaPlugins.reset(); + } + } + } + + @Test + public void synchronousFusedCrash() { + Completable.concat(Flowable.range(1, 2).map(new Function<Integer, Completable>() { + @Override + public Completable apply(Integer v) throws Exception { + throw new TestException(); + } + })) + .test() + .assertFailure(TestException.class); + } + + @Test + public void unboundedIn() { + Completable.concat(Flowable.just(Completable.complete()).hide(), Integer.MAX_VALUE) + .test() + .assertResult(); + } + + @Test + public void syncFusedUnboundedIn() { + Completable.concat(Flowable.just(Completable.complete()), Integer.MAX_VALUE) + .test() + .assertResult(); + } + + @Test + public void asyncFusedUnboundedIn() { + UnicastProcessor<Completable> up = UnicastProcessor.create(); + up.onNext(Completable.complete()); + up.onComplete(); + + Completable.concat(up, Integer.MAX_VALUE) + .test() + .assertResult(); + } + + @Test + public void arrayCancelled() { + Completable.concatArray(Completable.complete(), Completable.complete()) + .test(true) + .assertEmpty(); + } + + @Test + public void arrayFirstCancels() { + final TestObserver<Void> to = new TestObserver<>(); + + Completable.concatArray(new Completable() { + @Override + protected void subscribeActual(CompletableObserver observer) { + observer.onSubscribe(Disposable.empty()); + to.dispose(); + observer.onComplete(); + } + }, Completable.complete()) + .subscribe(to); + + to.assertEmpty(); + } + + @Test + public void iterableCancelled() { + Completable.concat(Arrays.asList(Completable.complete(), Completable.complete())) + .test(true) + .assertEmpty(); + } + + @Test + public void iterableFirstCancels() { + final TestObserver<Void> to = new TestObserver<>(); + + Completable.concat(Arrays.asList(new Completable() { + @Override + protected void subscribeActual(CompletableObserver observer) { + observer.onSubscribe(Disposable.empty()); + to.dispose(); + observer.onComplete(); + } + }, Completable.complete())) + .subscribe(to); + + to.assertEmpty(); + } + + @Test + public void arrayCancelRace() { + Completable[] a = new Completable[1024]; + Arrays.fill(a, Completable.complete()); + + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + + final Completable c = Completable.concatArray(a); + + final TestObserver<Void> to = new TestObserver<>(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + c.subscribe(to); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + to.dispose(); + } + }; + + TestHelper.race(r1, r2); + } + } + + @Test + public void iterableCancelRace() { + Completable[] a = new Completable[1024]; + Arrays.fill(a, Completable.complete()); + + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + + final Completable c = Completable.concat(Arrays.asList(a)); + + final TestObserver<Void> to = new TestObserver<>(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + c.subscribe(to); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + to.dispose(); + } + }; + + TestHelper.race(r1, r2); + } + } + + @Test + public void noInterrupt() throws InterruptedException { + for (int k = 0; k < 100; k++) { + final int count = 10; + final CountDownLatch latch = new CountDownLatch(count); + final boolean[] interrupted = { false }; + + for (int i = 0; i < count; i++) { + Completable c0 = Completable.fromAction(new Action() { + @Override + public void run() throws Exception { + try { + Thread.sleep(30); + } catch (InterruptedException e) { + System.out.println("Interrupted! " + Thread.currentThread()); + interrupted[0] = true; + } + } + }); + Completable.concat(Arrays.asList(Completable.complete() + .subscribeOn(Schedulers.io()) + .observeOn(Schedulers.io()), + c0) + ) + .subscribe(new Action() { + @Override + public void run() throws Exception { + latch.countDown(); + } + }); + } + + latch.await(); + assertFalse("The second Completable was interrupted!", interrupted[0]); + } + } + + @Test + public void doubleOnSubscribe() { + TestHelper.<Completable>checkDoubleOnSubscribeFlowableToCompletable(f -> Completable.concat(f)); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/completable/CompletableCreateTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/completable/CompletableCreateTest.java new file mode 100644 index 0000000000..bda1166ef9 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/completable/CompletableCreateTest.java @@ -0,0 +1,327 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.completable; + +import static org.junit.Assert.*; + +import java.io.IOException; +import java.util.List; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.Cancellable; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class CompletableCreateTest extends RxJavaTest { + + @Test + public void basic() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + final Disposable d = Disposable.empty(); + + Completable.create(new CompletableOnSubscribe() { + @Override + public void subscribe(CompletableEmitter e) throws Exception { + e.setDisposable(d); + + e.onComplete(); + e.onError(new TestException()); + e.onComplete(); + } + }) + .test() + .assertResult(); + + assertTrue(d.isDisposed()); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void basicWithCancellable() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + final Disposable d1 = Disposable.empty(); + final Disposable d2 = Disposable.empty(); + + Completable.create(new CompletableOnSubscribe() { + @Override + public void subscribe(CompletableEmitter e) throws Exception { + e.setDisposable(d1); + e.setCancellable(new Cancellable() { + @Override + public void cancel() throws Exception { + d2.dispose(); + } + }); + + e.onComplete(); + e.onError(new TestException()); + e.onComplete(); + } + }) + .test() + .assertResult(); + + assertTrue(d1.isDisposed()); + assertTrue(d2.isDisposed()); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void basicWithError() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + final Disposable d = Disposable.empty(); + + Completable.create(new CompletableOnSubscribe() { + @Override + public void subscribe(CompletableEmitter e) throws Exception { + e.setDisposable(d); + + e.onError(new TestException()); + e.onComplete(); + e.onError(new TestException("second")); + } + }) + .test() + .assertFailure(TestException.class); + + assertTrue(d.isDisposed()); + + TestHelper.assertUndeliverable(errors, 0, TestException.class, "second"); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void callbackThrows() { + Completable.create(new CompletableOnSubscribe() { + @Override + public void subscribe(CompletableEmitter e) throws Exception { + throw new TestException(); + } + }) + .test() + .assertFailure(TestException.class); + } + + @Test + public void onErrorNull() { + Completable.create(new CompletableOnSubscribe() { + @Override + public void subscribe(CompletableEmitter e) throws Exception { + e.onError(null); + } + }) + .test() + .assertFailure(NullPointerException.class); + } + + @Test + public void dispose() { + TestHelper.checkDisposed(Completable.create(new CompletableOnSubscribe() { + @Override + public void subscribe(CompletableEmitter e) throws Exception { + e.onComplete(); + } + })); + } + + @Test + public void onErrorThrows() { + Completable.create(new CompletableOnSubscribe() { + @Override + public void subscribe(CompletableEmitter e) throws Exception { + Disposable d = Disposable.empty(); + e.setDisposable(d); + + try { + e.onError(new IOException()); + fail("Should have thrown"); + } catch (TestException ex) { + // expected + } + + assertTrue(d.isDisposed()); + assertTrue(e.isDisposed()); + } + }).subscribe(new CompletableObserver() { + + @Override + public void onSubscribe(Disposable d) { + + } + + @Override + public void onError(Throwable e) { + throw new TestException(); + } + + @Override + public void onComplete() { + + } + }); + } + + @Test + public void onCompleteThrows() { + Completable.create(new CompletableOnSubscribe() { + @Override + public void subscribe(CompletableEmitter e) throws Exception { + Disposable d = Disposable.empty(); + e.setDisposable(d); + + try { + e.onComplete(); + fail("Should have thrown"); + } catch (TestException ex) { + // expected + } + + assertTrue(d.isDisposed()); + assertTrue(e.isDisposed()); + } + }).subscribe(new CompletableObserver() { + + @Override + public void onSubscribe(Disposable d) { + + } + + @Override + public void onError(Throwable e) { + + } + + @Override + public void onComplete() { + throw new TestException(); + } + }); + } + + @Test + public void onErrorThrows2() { + Completable.create(new CompletableOnSubscribe() { + @Override + public void subscribe(CompletableEmitter e) throws Exception { + try { + e.onError(new IOException()); + fail("Should have thrown"); + } catch (TestException ex) { + // expected + } + + assertTrue(e.isDisposed()); + } + }).subscribe(new CompletableObserver() { + + @Override + public void onSubscribe(Disposable d) { + + } + + @Override + public void onError(Throwable e) { + throw new TestException(); + } + + @Override + public void onComplete() { + + } + }); + } + + @Test + public void onCompleteThrows2() { + Completable.create(new CompletableOnSubscribe() { + @Override + public void subscribe(CompletableEmitter e) throws Exception { + try { + e.onComplete(); + fail("Should have thrown"); + } catch (TestException ex) { + // expected + } + + assertTrue(e.isDisposed()); + } + }).subscribe(new CompletableObserver() { + + @Override + public void onSubscribe(Disposable d) { + + } + + @Override + public void onError(Throwable e) { + + } + + @Override + public void onComplete() { + throw new TestException(); + } + }); + } + + @Test + public void tryOnError() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + final Boolean[] response = { null }; + Completable.create(new CompletableOnSubscribe() { + @Override + public void subscribe(CompletableEmitter e) throws Exception { + e.onComplete(); + response[0] = e.tryOnError(new TestException()); + } + }) + .test() + .assertResult(); + + assertFalse(response[0]); + + assertTrue(errors.toString(), errors.isEmpty()); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void emitterHasToString() { + Completable.create(new CompletableOnSubscribe() { + @Override + public void subscribe(CompletableEmitter emitter) throws Exception { + assertTrue(emitter.toString().contains(CompletableCreate.Emitter.class.getSimpleName())); + } + }).test().assertEmpty(); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/completable/CompletableDelaySubscriptionTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/completable/CompletableDelaySubscriptionTest.java new file mode 100644 index 0000000000..6e524eff27 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/completable/CompletableDelaySubscriptionTest.java @@ -0,0 +1,169 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.completable; + +import static org.junit.Assert.*; + +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.Action; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.schedulers.TestScheduler; +import io.reactivex.rxjava3.subjects.CompletableSubject; + +public class CompletableDelaySubscriptionTest extends RxJavaTest { + + @Test + public void normal() { + final AtomicInteger counter = new AtomicInteger(); + + Completable.fromAction(new Action() { + @Override + public void run() throws Exception { + counter.incrementAndGet(); + } + }) + .delaySubscription(100, TimeUnit.MILLISECONDS) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(); + + assertEquals(1, counter.get()); + } + + @Test + public void error() { + final AtomicInteger counter = new AtomicInteger(); + + Completable.fromAction(new Action() { + @Override + public void run() throws Exception { + counter.incrementAndGet(); + + throw new TestException(); + } + }) + .delaySubscription(100, TimeUnit.MILLISECONDS) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertFailure(TestException.class); + + assertEquals(1, counter.get()); + } + + @Test + public void disposeBeforeTime() { + TestScheduler scheduler = new TestScheduler(); + + final AtomicInteger counter = new AtomicInteger(); + + Completable result = Completable.fromAction(new Action() { + @Override + public void run() throws Exception { + counter.incrementAndGet(); + } + }) + .delaySubscription(100, TimeUnit.MILLISECONDS, scheduler); + TestObserver<Void> to = result.test(); + + to.assertEmpty(); + + scheduler.advanceTimeBy(90, TimeUnit.MILLISECONDS); + + to.dispose(); + + scheduler.advanceTimeBy(15, TimeUnit.MILLISECONDS); + + to.assertEmpty(); + + assertEquals(0, counter.get()); + } + + @Test + public void timestep() { + TestScheduler scheduler = new TestScheduler(); + final AtomicInteger counter = new AtomicInteger(); + + Completable result = Completable.fromAction(new Action() { + @Override + public void run() throws Exception { + counter.incrementAndGet(); + } + }) + .delaySubscription(100, TimeUnit.MILLISECONDS, scheduler); + + TestObserver<Void> to = result.test(); + + scheduler.advanceTimeBy(90, TimeUnit.MILLISECONDS); + to.assertEmpty(); + scheduler.advanceTimeBy(15, TimeUnit.MILLISECONDS); + to.assertResult(); + + assertEquals(1, counter.get()); + } + + @Test + public void timestepError() { + TestScheduler scheduler = new TestScheduler(); + final AtomicInteger counter = new AtomicInteger(); + + Completable result = Completable.fromAction(new Action() { + @Override + public void run() throws Exception { + counter.incrementAndGet(); + + throw new TestException(); + } + }) + .delaySubscription(100, TimeUnit.MILLISECONDS, scheduler); + + TestObserver<Void> to = result.test(); + + scheduler.advanceTimeBy(90, TimeUnit.MILLISECONDS); + + to.assertEmpty(); + + scheduler.advanceTimeBy(15, TimeUnit.MILLISECONDS); + + to.assertFailure(TestException.class); + + assertEquals(1, counter.get()); + } + + @Test + public void disposeMain() { + CompletableSubject cs = CompletableSubject.create(); + + TestScheduler scheduler = new TestScheduler(); + + TestObserver<Void> to = cs + .delaySubscription(1, TimeUnit.SECONDS, scheduler) + .test(); + + assertFalse(cs.hasObservers()); + + scheduler.advanceTimeBy(1, TimeUnit.SECONDS); + + assertTrue(cs.hasObservers()); + + to.dispose(); + + assertFalse(cs.hasObservers()); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/completable/CompletableDelayTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/completable/CompletableDelayTest.java new file mode 100644 index 0000000000..c84972863d --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/completable/CompletableDelayTest.java @@ -0,0 +1,124 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.completable; + +import static org.junit.Assert.assertNotEquals; + +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicReference; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.schedulers.*; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class CompletableDelayTest extends RxJavaTest { + + @Test + public void delayCustomScheduler() { + + Completable.complete() + .delay(100, TimeUnit.MILLISECONDS, Schedulers.trampoline()) + .test() + .assertResult(); + } + + @Test + public void onErrorCalledOnScheduler() throws Exception { + final CountDownLatch latch = new CountDownLatch(1); + final AtomicReference<Thread> thread = new AtomicReference<>(); + + Completable.error(new Exception()) + .delay(0, TimeUnit.MILLISECONDS, Schedulers.newThread()) + .doOnError(new Consumer<Throwable>() { + @Override + public void accept(Throwable throwable) throws Exception { + thread.set(Thread.currentThread()); + latch.countDown(); + } + }) + .onErrorComplete() + .subscribe(); + + latch.await(); + + assertNotEquals(Thread.currentThread(), thread.get()); + } + + @Test + public void disposed() { + TestHelper.checkDisposed(Completable.never().delay(1, TimeUnit.MINUTES)); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeCompletable(new Function<Completable, CompletableSource>() { + @Override + public CompletableSource apply(Completable c) throws Exception { + return c.delay(1, TimeUnit.MINUTES); + } + }); + } + + @Test + public void normal() { + Completable.complete() + .delay(1, TimeUnit.MILLISECONDS) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(); + } + + @Test + public void errorNotDelayed() { + TestScheduler scheduler = new TestScheduler(); + + TestObserver<Void> to = Completable.error(new TestException()) + .delay(100, TimeUnit.MILLISECONDS, scheduler, false) + .test(); + + to.assertEmpty(); + + scheduler.advanceTimeBy(1, TimeUnit.MILLISECONDS); + + to.assertFailure(TestException.class); + + scheduler.advanceTimeBy(100, TimeUnit.MILLISECONDS); + + to.assertFailure(TestException.class); + } + + @Test + public void errorDelayed() { + TestScheduler scheduler = new TestScheduler(); + + TestObserver<Void> to = Completable.error(new TestException()) + .delay(100, TimeUnit.MILLISECONDS, scheduler, true) + .test(); + + to.assertEmpty(); + + scheduler.advanceTimeBy(1, TimeUnit.MILLISECONDS); + + to.assertEmpty(); + + scheduler.advanceTimeBy(99, TimeUnit.MILLISECONDS); + + to.assertFailure(TestException.class); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/completable/CompletableDetachTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/completable/CompletableDetachTest.java new file mode 100644 index 0000000000..5373499d30 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/completable/CompletableDetachTest.java @@ -0,0 +1,142 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.completable; + +import static org.junit.Assert.assertNull; + +import java.io.IOException; +import java.lang.ref.WeakReference; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.processors.PublishProcessor; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class CompletableDetachTest extends RxJavaTest { + + @Test + public void doubleSubscribe() { + + TestHelper.checkDoubleOnSubscribeCompletable(new Function<Completable, CompletableSource>() { + @Override + public CompletableSource apply(Completable m) throws Exception { + return m.onTerminateDetach(); + } + }); + } + + @Test + public void dispose() { + TestHelper.checkDisposed(PublishProcessor.create().ignoreElements().onTerminateDetach()); + } + + @Test + public void onError() { + Completable.error(new TestException()) + .onTerminateDetach() + .test() + .assertFailure(TestException.class); + } + + @Test + public void onComplete() { + Completable.complete() + .onTerminateDetach() + .test() + .assertResult(); + } + + @Test + public void cancelDetaches() throws Exception { + Disposable d = Disposable.empty(); + final WeakReference<Disposable> wr = new WeakReference<>(d); + + TestObserver<Void> to = new Completable() { + @Override + protected void subscribeActual(CompletableObserver observer) { + observer.onSubscribe(wr.get()); + }; + } + .onTerminateDetach() + .test(); + + d = null; + + to.dispose(); + + System.gc(); + Thread.sleep(200); + + to.assertEmpty(); + + assertNull(wr.get()); + } + + @Test + public void completeDetaches() throws Exception { + Disposable d = Disposable.empty(); + final WeakReference<Disposable> wr = new WeakReference<>(d); + + TestObserver<Void> to = new Completable() { + @Override + protected void subscribeActual(CompletableObserver observer) { + observer.onSubscribe(wr.get()); + observer.onComplete(); + observer.onComplete(); + }; + } + .onTerminateDetach() + .test(); + + d = null; + + System.gc(); + Thread.sleep(200); + + to.assertResult(); + + assertNull(wr.get()); + } + + @Test + public void errorDetaches() throws Exception { + Disposable d = Disposable.empty(); + final WeakReference<Disposable> wr = new WeakReference<>(d); + + TestObserver<Void> to = new Completable() { + @Override + protected void subscribeActual(CompletableObserver observer) { + observer.onSubscribe(wr.get()); + observer.onError(new TestException()); + observer.onError(new IOException()); + }; + } + .onTerminateDetach() + .test(); + + d = null; + + System.gc(); + Thread.sleep(200); + + to.assertFailure(TestException.class); + + assertNull(wr.get()); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/completable/CompletableDisposeOnTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/completable/CompletableDisposeOnTest.java new file mode 100644 index 0000000000..73a4e58c77 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/completable/CompletableDisposeOnTest.java @@ -0,0 +1,146 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.completable; + +import static org.junit.Assert.*; + +import java.util.List; +import java.util.concurrent.TimeUnit; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.Action; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.schedulers.*; +import io.reactivex.rxjava3.subjects.PublishSubject; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class CompletableDisposeOnTest extends RxJavaTest { + + @Test + public void cancelDelayed() { + TestScheduler scheduler = new TestScheduler(); + + PublishSubject<Integer> ps = PublishSubject.create(); + + ps.ignoreElements() + .unsubscribeOn(scheduler) + .test() + .dispose(); + + assertTrue(ps.hasObservers()); + + scheduler.advanceTimeBy(1, TimeUnit.SECONDS); + + assertFalse(ps.hasObservers()); + } + + @Test + public void dispose() { + TestHelper.checkDisposed(PublishSubject.create().ignoreElements().unsubscribeOn(new TestScheduler())); + } + + @Test + public void completeAfterCancel() { + TestScheduler scheduler = new TestScheduler(); + + PublishSubject<Integer> ps = PublishSubject.create(); + + TestObserver<Void> to = ps.ignoreElements() + .unsubscribeOn(scheduler) + .test(); + + to.dispose(); + + ps.onComplete(); + + to.assertEmpty(); + } + + @Test + public void errorAfterCancel() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + TestScheduler scheduler = new TestScheduler(); + + PublishSubject<Integer> ps = PublishSubject.create(); + + TestObserver<Void> to = ps.ignoreElements() + .unsubscribeOn(scheduler) + .test(); + + to.dispose(); + + ps.onError(new TestException()); + + to.assertEmpty(); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void normal() { + TestScheduler scheduler = new TestScheduler(); + + final int[] call = { 0 }; + + Completable.complete() + .doOnDispose(new Action() { + @Override + public void run() throws Exception { + call[0]++; + } + }) + .unsubscribeOn(scheduler) + .test() + .assertResult(); + + scheduler.triggerActions(); + + assertEquals(0, call[0]); + } + + @Test + public void error() { + TestScheduler scheduler = new TestScheduler(); + + final int[] call = { 0 }; + + Completable.error(new TestException()) + .doOnDispose(new Action() { + @Override + public void run() throws Exception { + call[0]++; + } + }) + .unsubscribeOn(scheduler) + .test() + .assertFailure(TestException.class); + + scheduler.triggerActions(); + + assertEquals(0, call[0]); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeCompletable(c -> c.unsubscribeOn(Schedulers.computation())); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/completable/CompletableDoFinallyTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/completable/CompletableDoFinallyTest.java new file mode 100644 index 0000000000..14bd307b37 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/completable/CompletableDoFinallyTest.java @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.completable; + +import static org.junit.Assert.assertEquals; + +import java.util.List; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.subjects.PublishSubject; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class CompletableDoFinallyTest extends RxJavaTest implements Action { + + int calls; + + @Override + public void run() throws Exception { + calls++; + } + + @Test + public void normalEmpty() { + Completable.complete() + .doFinally(this) + .test() + .assertResult(); + + assertEquals(1, calls); + } + + @Test + public void normalError() { + Completable.error(new TestException()) + .doFinally(this) + .test() + .assertFailure(TestException.class); + + assertEquals(1, calls); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeCompletable(new Function<Completable, Completable>() { + @Override + public Completable apply(Completable f) throws Exception { + return f.doFinally(CompletableDoFinallyTest.this); + } + }); + } + + @Test + public void actionThrows() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + Completable.complete() + .doFinally(new Action() { + @Override + public void run() throws Exception { + throw new TestException(); + } + }) + .test() + .assertResult() + .dispose(); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void disposed() { + TestHelper.checkDisposed(PublishSubject.create().ignoreElements().doFinally(this)); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/completable/CompletableDoOnLifecycleTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/completable/CompletableDoOnLifecycleTest.java new file mode 100644 index 0000000000..1f66038f19 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/completable/CompletableDoOnLifecycleTest.java @@ -0,0 +1,153 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.completable; + +import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.subjects.CompletableSubject; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class CompletableDoOnLifecycleTest extends RxJavaTest { + + @Test + public void empty() throws Throwable { + @SuppressWarnings("unchecked") + Consumer<? super Disposable> onSubscribe = mock(Consumer.class); + Action onDispose = mock(Action.class); + + Completable.complete() + .doOnLifecycle(onSubscribe, onDispose) + .test() + .assertResult(); + + verify(onSubscribe).accept(any()); + verify(onDispose, never()).run(); + } + + @Test + public void error() throws Throwable { + @SuppressWarnings("unchecked") + Consumer<? super Disposable> onSubscribe = mock(Consumer.class); + Action onDispose = mock(Action.class); + + Completable.error(new TestException()) + .doOnLifecycle(onSubscribe, onDispose) + .test() + .assertFailure(TestException.class); + + verify(onSubscribe).accept(any()); + verify(onDispose, never()).run(); + } + + @Test + public void onSubscribeCrash() throws Throwable { + TestHelper.withErrorTracking(errors -> { + @SuppressWarnings("unchecked") + Consumer<? super Disposable> onSubscribe = mock(Consumer.class); + Action onDispose = mock(Action.class); + + doThrow(new TestException("First")).when(onSubscribe).accept(any()); + + Disposable bs = Disposable.empty(); + + new Completable() { + @Override + protected void subscribeActual(CompletableObserver observer) { + observer.onSubscribe(bs); + observer.onError(new TestException("Second")); + observer.onComplete(); + } + } + .doOnLifecycle(onSubscribe, onDispose) + .to(TestHelper.<Integer>testConsumer()) + .assertFailureAndMessage(TestException.class, "First"); + + assertTrue(bs.isDisposed()); + + TestHelper.assertUndeliverable(errors, 0, TestException.class, "Second"); + + verify(onSubscribe).accept(any()); + verify(onDispose, never()).run(); + }); + } + + @Test + public void onDisposeCrash() throws Throwable { + TestHelper.withErrorTracking(errors -> { + @SuppressWarnings("unchecked") + Consumer<? super Disposable> onSubscribe = mock(Consumer.class); + Action onDispose = mock(Action.class); + + doThrow(new TestException("First")).when(onDispose).run(); + + CompletableSubject cs = CompletableSubject.create(); + + TestObserver<Void> to = cs + .doOnLifecycle(onSubscribe, onDispose) + .test(); + + assertTrue(cs.hasObservers()); + + to.dispose(); + + assertFalse(cs.hasObservers()); + + TestHelper.assertUndeliverable(errors, 0, TestException.class, "First"); + + verify(onSubscribe).accept(any()); + verify(onDispose).run(); + }); + } + + @Test + public void dispose() throws Throwable { + @SuppressWarnings("unchecked") + Consumer<? super Disposable> onSubscribe = mock(Consumer.class); + Action onDispose = mock(Action.class); + + CompletableSubject cs = CompletableSubject.create(); + + TestObserver<Void> to = cs + .doOnLifecycle(onSubscribe, onDispose) + .test(); + + assertTrue(cs.hasObservers()); + + to.dispose(); + + assertFalse(cs.hasObservers()); + + verify(onSubscribe).accept(any()); + verify(onDispose).run(); + } + + @Test + public void isDisposed() { + TestHelper.checkDisposed(CompletableSubject.create().doOnLifecycle(d -> { }, () -> { })); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeCompletable(m -> m.doOnLifecycle(d -> { }, () -> { })); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/completable/CompletableDoOnTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/completable/CompletableDoOnTest.java new file mode 100644 index 0000000000..6f9698afbf --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/completable/CompletableDoOnTest.java @@ -0,0 +1,111 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.completable; + +import static org.junit.Assert.*; + +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.*; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.testsupport.*; + +public class CompletableDoOnTest extends RxJavaTest { + + @Test + public void successAcceptThrows() { + Completable.complete().doOnEvent(new Consumer<Throwable>() { + @Override + public void accept(Throwable e) throws Exception { + throw new TestException(); + } + }) + .test() + .assertFailure(TestException.class); + } + + @Test + public void errorAcceptThrows() { + TestObserverEx<Void> to = Completable.error(new TestException("Outer")).doOnEvent(new Consumer<Throwable>() { + @Override + public void accept(Throwable e) throws Exception { + throw new TestException("Inner"); + } + }) + .to(TestHelper.<Void>testConsumer()) + .assertFailure(CompositeException.class); + + List<Throwable> errors = TestHelper.compositeList(to.errors().get(0)); + + TestHelper.assertError(errors, 0, TestException.class, "Outer"); + TestHelper.assertError(errors, 1, TestException.class, "Inner"); + } + + @Test + public void doOnDisposeCalled() { + final AtomicBoolean atomicBoolean = new AtomicBoolean(); + + assertFalse(atomicBoolean.get()); + + Completable.complete() + .doOnDispose(new Action() { + @Override + public void run() throws Exception { + atomicBoolean.set(true); + } + }) + .test() + .assertResult() + .dispose(); + + assertTrue(atomicBoolean.get()); + } + + @Test + public void onSubscribeCrash() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + final Disposable bs = Disposable.empty(); + + new Completable() { + @Override + protected void subscribeActual(CompletableObserver observer) { + observer.onSubscribe(bs); + observer.onError(new TestException("Second")); + observer.onComplete(); + } + } + .doOnSubscribe(new Consumer<Disposable>() { + @Override + public void accept(Disposable d) throws Exception { + throw new TestException("First"); + } + }) + .to(TestHelper.<Void>testConsumer()) + .assertFailureAndMessage(TestException.class, "First"); + + assertTrue(bs.isDisposed()); + + TestHelper.assertUndeliverable(errors, 0, TestException.class, "Second"); + } finally { + RxJavaPlugins.reset(); + } + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/completable/CompletableFromActionTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/completable/CompletableFromActionTest.java new file mode 100644 index 0000000000..e697a91d34 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/completable/CompletableFromActionTest.java @@ -0,0 +1,170 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.completable; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.*; + +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.Action; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class CompletableFromActionTest extends RxJavaTest { + @Test + public void fromAction() { + final AtomicInteger atomicInteger = new AtomicInteger(); + + Completable.fromAction(new Action() { + @Override + public void run() throws Exception { + atomicInteger.incrementAndGet(); + } + }) + .test() + .assertResult(); + + assertEquals(1, atomicInteger.get()); + } + + @Test + public void fromActionTwice() { + final AtomicInteger atomicInteger = new AtomicInteger(); + + Action run = new Action() { + @Override + public void run() throws Exception { + atomicInteger.incrementAndGet(); + } + }; + + Completable.fromAction(run) + .test() + .assertResult(); + + assertEquals(1, atomicInteger.get()); + + Completable.fromAction(run) + .test() + .assertResult(); + + assertEquals(2, atomicInteger.get()); + } + + @Test + public void fromActionInvokesLazy() { + final AtomicInteger atomicInteger = new AtomicInteger(); + + Completable completable = Completable.fromAction(new Action() { + @Override + public void run() throws Exception { + atomicInteger.incrementAndGet(); + } + }); + + assertEquals(0, atomicInteger.get()); + + completable + .test() + .assertResult(); + + assertEquals(1, atomicInteger.get()); + } + + @Test + public void fromActionThrows() { + Completable.fromAction(new Action() { + @Override + public void run() throws Exception { + throw new UnsupportedOperationException(); + } + }) + .test() + .assertFailure(UnsupportedOperationException.class); + } + + @Test + public void fromActionDisposed() { + final AtomicInteger calls = new AtomicInteger(); + Completable.fromAction(new Action() { + @Override + public void run() throws Exception { + calls.incrementAndGet(); + } + }) + .test(true) + .assertEmpty(); + + assertEquals(0, calls.get()); + } + + @Test + public void fromActionErrorsDisposed() { + final AtomicInteger calls = new AtomicInteger(); + Completable.fromAction(new Action() { + @Override + public void run() throws Exception { + calls.incrementAndGet(); + throw new TestException(); + } + }) + .test(true) + .assertEmpty(); + + assertEquals(0, calls.get()); + } + + @Test + public void disposedUpfront() throws Throwable { + Action run = mock(Action.class); + + Completable.fromAction(run) + .test(true) + .assertEmpty(); + + verify(run, never()).run(); + } + + @Test + public void disposeWhileRunningComplete() { + TestObserver<Void> to = new TestObserver<>(); + + Completable.fromAction(() -> { + to.dispose(); + }) + .subscribeWith(to) + .assertEmpty(); + } + + @Test + public void disposeWhileRunningError() throws Throwable { + TestHelper.withErrorTracking(errors -> { + TestObserver<Void> to = new TestObserver<>(); + + Completable.fromAction(() -> { + to.dispose(); + throw new TestException(); + }) + .subscribeWith(to) + .assertEmpty(); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + }); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/completable/CompletableFromCallableTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/completable/CompletableFromCallableTest.java new file mode 100644 index 0000000000..940444f7a9 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/completable/CompletableFromCallableTest.java @@ -0,0 +1,180 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.completable; + +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.Test; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.schedulers.Schedulers; +import io.reactivex.rxjava3.testsupport.*; + +public class CompletableFromCallableTest extends RxJavaTest { + + @Test + public void fromCallable() { + final AtomicInteger atomicInteger = new AtomicInteger(); + + Completable.fromCallable(new Callable<Object>() { + @Override + public Object call() throws Exception { + atomicInteger.incrementAndGet(); + return null; + } + }) + .test() + .assertResult(); + + assertEquals(1, atomicInteger.get()); + } + + @Test + public void fromCallableTwice() { + final AtomicInteger atomicInteger = new AtomicInteger(); + + Callable<Object> callable = new Callable<Object>() { + @Override + public Object call() throws Exception { + atomicInteger.incrementAndGet(); + return null; + } + }; + + Completable.fromCallable(callable) + .test() + .assertResult(); + + assertEquals(1, atomicInteger.get()); + + Completable.fromCallable(callable) + .test() + .assertResult(); + + assertEquals(2, atomicInteger.get()); + } + + @Test + public void fromCallableInvokesLazy() { + final AtomicInteger atomicInteger = new AtomicInteger(); + + Completable completable = Completable.fromCallable(new Callable<Object>() { + @Override + public Object call() throws Exception { + atomicInteger.incrementAndGet(); + return null; + } + }); + + assertEquals(0, atomicInteger.get()); + + completable + .test() + .assertResult(); + + assertEquals(1, atomicInteger.get()); + } + + @Test + public void fromCallableThrows() { + Completable.fromCallable(new Callable<Object>() { + @Override + public Object call() throws Exception { + throw new UnsupportedOperationException(); + } + }) + .test() + .assertFailure(UnsupportedOperationException.class); + } + + @SuppressWarnings("unchecked") + @Test + public void shouldNotDeliverResultIfSubscriberUnsubscribedBeforeEmission() throws Exception { + Callable<String> func = mock(Callable.class); + + final CountDownLatch funcLatch = new CountDownLatch(1); + final CountDownLatch observerLatch = new CountDownLatch(1); + + when(func.call()).thenAnswer(new Answer<String>() { + @Override + public String answer(InvocationOnMock invocation) throws Throwable { + observerLatch.countDown(); + + try { + funcLatch.await(); + } catch (InterruptedException e) { + // It's okay, unsubscription causes Thread interruption + + // Restoring interruption status of the Thread + Thread.currentThread().interrupt(); + } + + return "should_not_be_delivered"; + } + }); + + Completable fromCallableObservable = Completable.fromCallable(func); + + Observer<Object> observer = TestHelper.mockObserver(); + + TestObserver<String> outer = new TestObserver<>(observer); + + fromCallableObservable + .subscribeOn(Schedulers.computation()) + .subscribe(outer); + + // Wait until func will be invoked + observerLatch.await(); + + // Unsubscribing before emission + outer.dispose(); + + // Emitting result + funcLatch.countDown(); + + // func must be invoked + verify(func).call(); + + // Observer must not be notified at all + verify(observer).onSubscribe(any(Disposable.class)); + verifyNoMoreInteractions(observer); + } + + @Test + @SuppressUndeliverable + public void fromActionErrorsDisposed() { + final AtomicInteger calls = new AtomicInteger(); + Completable.fromCallable(new Callable<Object>() { + @Override + public Object call() throws Exception { + calls.incrementAndGet(); + throw new TestException(); + } + }) + .test(true) + .assertEmpty(); + + assertEquals(1, calls.get()); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/completable/CompletableFromMaybeTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/completable/CompletableFromMaybeTest.java new file mode 100644 index 0000000000..db201e07a7 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/completable/CompletableFromMaybeTest.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.completable; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; + +public class CompletableFromMaybeTest extends RxJavaTest { + @Test + public void fromMaybe() { + Completable.fromMaybe(Maybe.just(1)) + .test() + .assertResult(); + } + + @Test + public void fromMaybeEmpty() { + Completable.fromMaybe(Maybe.<Integer>empty()) + .test() + .assertResult(); + } + + @Test + public void fromMaybeError() { + Completable.fromMaybe(Maybe.error(new UnsupportedOperationException())) + .test() + .assertFailure(UnsupportedOperationException.class); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/completable/CompletableFromObservableTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/completable/CompletableFromObservableTest.java new file mode 100644 index 0000000000..5fc8ba08c6 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/completable/CompletableFromObservableTest.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.completable; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; + +public class CompletableFromObservableTest extends RxJavaTest { + @Test + public void fromObservable() { + Completable.fromObservable(Observable.just(1)) + .test() + .assertResult(); + } + + @Test + public void fromObservableEmpty() { + Completable.fromObservable(Observable.empty()) + .test() + .assertResult(); + } + + @Test + public void fromObservableError() { + Completable.fromObservable(Observable.error(new UnsupportedOperationException())) + .test() + .assertFailure(UnsupportedOperationException.class); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/completable/CompletableFromPublisherTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/completable/CompletableFromPublisherTest.java new file mode 100644 index 0000000000..177dc4e156 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/completable/CompletableFromPublisherTest.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.completable; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class CompletableFromPublisherTest extends RxJavaTest { + @Test + public void fromPublisher() { + Completable.fromPublisher(Flowable.just(1)) + .test() + .assertResult(); + } + + @Test + public void fromPublisherEmpty() { + Completable.fromPublisher(Flowable.empty()) + .test() + .assertResult(); + } + + @Test + public void fromPublisherThrows() { + Completable.fromPublisher(Flowable.error(new UnsupportedOperationException())) + .test() + .assertFailure(UnsupportedOperationException.class); + } + + @Test + public void dispose() { + TestHelper.checkDisposed(Completable.fromPublisher(Flowable.just(1))); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeFlowableToCompletable(new Function<Flowable<Object>, Completable>() { + @Override + public Completable apply(Flowable<Object> f) throws Exception { + return Completable.fromPublisher(f); + } + }); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/completable/CompletableFromRunnableTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/completable/CompletableFromRunnableTest.java new file mode 100644 index 0000000000..2b3e9a6514 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/completable/CompletableFromRunnableTest.java @@ -0,0 +1,169 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.completable; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.*; + +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class CompletableFromRunnableTest extends RxJavaTest { + @Test + public void fromRunnable() { + final AtomicInteger atomicInteger = new AtomicInteger(); + + Completable.fromRunnable(new Runnable() { + @Override + public void run() { + atomicInteger.incrementAndGet(); + } + }) + .test() + .assertResult(); + + assertEquals(1, atomicInteger.get()); + } + + @Test + public void fromRunnableTwice() { + final AtomicInteger atomicInteger = new AtomicInteger(); + + Runnable run = new Runnable() { + @Override + public void run() { + atomicInteger.incrementAndGet(); + } + }; + + Completable.fromRunnable(run) + .test() + .assertResult(); + + assertEquals(1, atomicInteger.get()); + + Completable.fromRunnable(run) + .test() + .assertResult(); + + assertEquals(2, atomicInteger.get()); + } + + @Test + public void fromRunnableInvokesLazy() { + final AtomicInteger atomicInteger = new AtomicInteger(); + + Completable completable = Completable.fromRunnable(new Runnable() { + @Override + public void run() { + atomicInteger.incrementAndGet(); + } + }); + + assertEquals(0, atomicInteger.get()); + + completable + .test() + .assertResult(); + + assertEquals(1, atomicInteger.get()); + } + + @Test + public void fromRunnableThrows() { + Completable.fromRunnable(new Runnable() { + @Override + public void run() { + throw new UnsupportedOperationException(); + } + }) + .test() + .assertFailure(UnsupportedOperationException.class); + } + + @Test + public void fromRunnableDisposed() { + final AtomicInteger calls = new AtomicInteger(); + Completable.fromRunnable(new Runnable() { + @Override + public void run() { + calls.incrementAndGet(); + } + }) + .test(true) + .assertEmpty(); + + assertEquals(0, calls.get()); + } + + @Test + public void fromRunnableErrorsDisposed() { + final AtomicInteger calls = new AtomicInteger(); + Completable.fromRunnable(new Runnable() { + @Override + public void run() { + calls.incrementAndGet(); + throw new TestException(); + } + }) + .test(true) + .assertEmpty(); + + assertEquals(0, calls.get()); + } + + @Test + public void disposedUpfront() throws Throwable { + Runnable run = mock(Runnable.class); + + Completable.fromRunnable(run) + .test(true) + .assertEmpty(); + + verify(run, never()).run(); + } + + @Test + public void disposeWhileRunningComplete() { + TestObserver<Void> to = new TestObserver<>(); + + Completable.fromRunnable(() -> { + to.dispose(); + }) + .subscribeWith(to) + .assertEmpty(); + } + + @Test + public void disposeWhileRunningError() throws Throwable { + TestHelper.withErrorTracking(errors -> { + TestObserver<Void> to = new TestObserver<>(); + + Completable.fromRunnable(() -> { + to.dispose(); + throw new TestException(); + }) + .subscribeWith(to) + .assertEmpty(); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + }); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/completable/CompletableFromSingleTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/completable/CompletableFromSingleTest.java new file mode 100644 index 0000000000..14e5a29ac9 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/completable/CompletableFromSingleTest.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.completable; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; + +public class CompletableFromSingleTest extends RxJavaTest { + @Test + public void fromSingle() { + Completable.fromSingle(Single.just(1)) + .test() + .assertResult(); + } + + @Test + public void fromSingleError() { + Completable.fromSingle(Single.error(new UnsupportedOperationException())) + .test() + .assertFailure(UnsupportedOperationException.class); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/completable/CompletableFromSupplierTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/completable/CompletableFromSupplierTest.java new file mode 100644 index 0000000000..453fc4c2c0 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/completable/CompletableFromSupplierTest.java @@ -0,0 +1,181 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.completable; + +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.Test; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.Supplier; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.schedulers.Schedulers; +import io.reactivex.rxjava3.testsupport.*; + +public class CompletableFromSupplierTest extends RxJavaTest { + + @Test + public void fromSupplier() { + final AtomicInteger atomicInteger = new AtomicInteger(); + + Completable.fromSupplier(new Supplier<Object>() { + @Override + public Object get() throws Exception { + atomicInteger.incrementAndGet(); + return null; + } + }) + .test() + .assertResult(); + + assertEquals(1, atomicInteger.get()); + } + + @Test + public void fromSupplierTwice() { + final AtomicInteger atomicInteger = new AtomicInteger(); + + Supplier<Object> supplier = new Supplier<Object>() { + @Override + public Object get() throws Exception { + atomicInteger.incrementAndGet(); + return null; + } + }; + + Completable.fromSupplier(supplier) + .test() + .assertResult(); + + assertEquals(1, atomicInteger.get()); + + Completable.fromSupplier(supplier) + .test() + .assertResult(); + + assertEquals(2, atomicInteger.get()); + } + + @Test + public void fromSupplierInvokesLazy() { + final AtomicInteger atomicInteger = new AtomicInteger(); + + Completable completable = Completable.fromSupplier(new Supplier<Object>() { + @Override + public Object get() throws Exception { + atomicInteger.incrementAndGet(); + return null; + } + }); + + assertEquals(0, atomicInteger.get()); + + completable + .test() + .assertResult(); + + assertEquals(1, atomicInteger.get()); + } + + @Test + public void fromSupplierThrows() { + Completable.fromSupplier(new Supplier<Object>() { + @Override + public Object get() throws Exception { + throw new UnsupportedOperationException(); + } + }) + .test() + .assertFailure(UnsupportedOperationException.class); + } + + @SuppressWarnings("unchecked") + @Test + public void shouldNotDeliverResultIfSubscriberUnsubscribedBeforeEmission() throws Throwable { + Supplier<String> func = mock(Supplier.class); + + final CountDownLatch funcLatch = new CountDownLatch(1); + final CountDownLatch observerLatch = new CountDownLatch(1); + + when(func.get()).thenAnswer(new Answer<String>() { + @Override + public String answer(InvocationOnMock invocation) throws Throwable { + observerLatch.countDown(); + + try { + funcLatch.await(); + } catch (InterruptedException e) { + // It's okay, unsubscription causes Thread interruption + + // Restoring interruption status of the Thread + Thread.currentThread().interrupt(); + } + + return "should_not_be_delivered"; + } + }); + + Completable fromSupplierObservable = Completable.fromSupplier(func); + + Observer<Object> observer = TestHelper.mockObserver(); + + TestObserver<String> outer = new TestObserver<>(observer); + + fromSupplierObservable + .subscribeOn(Schedulers.computation()) + .subscribe(outer); + + // Wait until func will be invoked + observerLatch.await(); + + // Unsubscribing before emission + outer.dispose(); + + // Emitting result + funcLatch.countDown(); + + // func must be invoked + verify(func).get(); + + // Observer must not be notified at all + verify(observer).onSubscribe(any(Disposable.class)); + verifyNoMoreInteractions(observer); + } + + @Test + @SuppressUndeliverable + public void fromActionErrorsDisposed() { + final AtomicInteger calls = new AtomicInteger(); + Completable.fromSupplier(new Supplier<Object>() { + @Override + public Object get() throws Exception { + calls.incrementAndGet(); + throw new TestException(); + } + }) + .test(true) + .assertEmpty(); + + assertEquals(1, calls.get()); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/completable/CompletableHideTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/completable/CompletableHideTest.java new file mode 100644 index 0000000000..16afcaa075 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/completable/CompletableHideTest.java @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.completable; + +import static org.junit.Assert.assertFalse; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.processors.PublishProcessor; +import io.reactivex.rxjava3.subjects.CompletableSubject; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class CompletableHideTest extends RxJavaTest { + + @Test + public void never() { + Completable.never() + .hide() + .test() + .assertNotComplete() + .assertNoErrors(); + } + + @Test + public void complete() { + Completable.complete() + .hide() + .test() + .assertResult(); + } + + @Test + public void error() { + Completable.error(new TestException()) + .hide() + .test() + .assertFailure(TestException.class); + } + + @Test + public void hidden() { + assertFalse(CompletableSubject.create().hide() instanceof CompletableSubject); + } + + @Test + public void dispose() { + TestHelper.checkDisposedCompletable(new Function<Completable, CompletableSource>() { + @Override + public CompletableSource apply(Completable m) throws Exception { + return m.hide(); + } + }); + } + + @Test + public void isDisposed() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + + TestHelper.checkDisposed(pp.ignoreElements().hide()); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeCompletable(new Function<Completable, Completable>() { + @Override + public Completable apply(Completable f) throws Exception { + return f.hide(); + } + }); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/completable/CompletableLiftTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/completable/CompletableLiftTest.java new file mode 100644 index 0000000000..21cf3fb305 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/completable/CompletableLiftTest.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.completable; + +import java.util.List; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class CompletableLiftTest extends RxJavaTest { + + @Test + public void callbackThrows() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + Completable.complete() + .lift(new CompletableOperator() { + @Override + public CompletableObserver apply(CompletableObserver o) throws Exception { + throw new TestException(); + } + }) + .test(); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/completable/CompletableMaterializeTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/completable/CompletableMaterializeTest.java new file mode 100644 index 0000000000..8c301f50fa --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/completable/CompletableMaterializeTest.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.completable; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.subjects.CompletableSubject; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class CompletableMaterializeTest extends RxJavaTest { + + @Test + public void error() { + TestException ex = new TestException(); + Completable.error(ex) + .materialize() + .test() + .assertResult(Notification.createOnError(ex)); + } + + @Test + public void empty() { + Completable.complete() + .materialize() + .test() + .assertResult(Notification.createOnComplete()); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeCompletableToSingle(new Function<Completable, SingleSource<Notification<Object>>>() { + @Override + public SingleSource<Notification<Object>> apply(Completable v) throws Exception { + return v.materialize(); + } + }); + } + + @Test + public void dispose() { + TestHelper.checkDisposed(CompletableSubject.create().materialize()); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/completable/CompletableMergeIterableTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/completable/CompletableMergeIterableTest.java new file mode 100644 index 0000000000..cf3dc03c69 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/completable/CompletableMergeIterableTest.java @@ -0,0 +1,135 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.completable; + +import java.util.*; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.CompositeDisposable; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.internal.operators.completable.CompletableMergeIterable.MergeCompletableObserver; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.subjects.PublishSubject; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class CompletableMergeIterableTest extends RxJavaTest { + + @Test + public void errorRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + final PublishSubject<Integer> ps1 = PublishSubject.create(); + final PublishSubject<Integer> ps2 = PublishSubject.create(); + + TestObserver<Void> to = Completable.merge( + Arrays.asList(ps1.ignoreElements(), ps2.ignoreElements())).test(); + + final TestException ex = new TestException(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + ps1.onError(ex); + } + }; + Runnable r2 = new Runnable() { + @Override + public void run() { + ps2.onError(ex); + } + }; + + TestHelper.race(r1, r2); + + to.assertFailure(TestException.class); + + if (!errors.isEmpty()) { + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } + } finally { + RxJavaPlugins.reset(); + } + } + } + + @Test + public void cancelAfterHasNext() { + final TestObserver<Void> to = new TestObserver<>(); + + Completable.merge(new Iterable<Completable>() { + @Override + public Iterator<Completable> iterator() { + return new Iterator<Completable>() { + @Override + public boolean hasNext() { + to.dispose(); + return true; + } + + @Override + public Completable next() { + return Completable.complete(); + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + }; + } + }).subscribe(to); + + to.assertEmpty(); + } + + @Test + public void cancelAfterNext() { + final TestObserver<Void> to = new TestObserver<>(); + + Completable.merge(new Iterable<Completable>() { + @Override + public Iterator<Completable> iterator() { + return new Iterator<Completable>() { + @Override + public boolean hasNext() { + return true; + } + + @Override + public Completable next() { + to.dispose(); + return Completable.complete(); + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + }; + } + }).subscribe(to); + + to.assertEmpty(); + } + + @Test + public void dispose() { + TestHelper.checkDisposed(new MergeCompletableObserver(new TestObserver<Void>(), new CompositeDisposable(), new AtomicInteger())); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/completable/CompletableMergeTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/completable/CompletableMergeTest.java new file mode 100644 index 0000000000..38166ea659 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/completable/CompletableMergeTest.java @@ -0,0 +1,628 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.completable; + +import static org.junit.Assert.*; + +import java.util.*; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.junit.Test; +import org.reactivestreams.Subscriber; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.*; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.subscriptions.BooleanSubscription; +import io.reactivex.rxjava3.internal.util.AtomicThrowable; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.processors.PublishProcessor; +import io.reactivex.rxjava3.subjects.CompletableSubject; +import io.reactivex.rxjava3.testsupport.*; + +public class CompletableMergeTest extends RxJavaTest { + @Test + public void invalidPrefetch() { + try { + Completable.merge(Flowable.just(Completable.complete()), -99); + fail("Should have thrown IllegalArgumentExceptio"); + } catch (IllegalArgumentException ex) { + assertEquals("maxConcurrency > 0 required but it was -99", ex.getMessage()); + } + } + + @Test + public void cancelAfterFirst() { + final TestObserver<Void> to = new TestObserver<>(); + + Completable.mergeArray(new Completable() { + @Override + protected void subscribeActual(CompletableObserver observer) { + observer.onSubscribe(Disposable.empty()); + observer.onComplete(); + to.dispose(); + } + }, Completable.complete()) + .subscribe(to); + + to.assertEmpty(); + } + + @Test + public void cancelAfterFirstDelayError() { + final TestObserver<Void> to = new TestObserver<>(); + + Completable.mergeArrayDelayError(new Completable() { + @Override + protected void subscribeActual(CompletableObserver observer) { + observer.onSubscribe(Disposable.empty()); + observer.onComplete(); + to.dispose(); + } + }, Completable.complete()) + .subscribe(to); + + to.assertEmpty(); + } + + @Test + public void onErrorAfterComplete() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + final CompletableObserver[] co = { null }; + + Completable.mergeArrayDelayError(Completable.complete(), new Completable() { + @Override + protected void subscribeActual(CompletableObserver observer) { + observer.onSubscribe(Disposable.empty()); + observer.onComplete(); + co[0] = observer; + } + }) + .test() + .assertResult(); + + co[0].onError(new TestException()); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void completeAfterMain() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + + TestObserver<Void> to = Completable.mergeArray(Completable.complete(), pp.ignoreElements()) + .test(); + + pp.onComplete(); + + to.assertResult(); + } + + @Test + public void completeAfterMainDelayError() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + + TestObserver<Void> to = Completable.mergeArrayDelayError(Completable.complete(), pp.ignoreElements()) + .test(); + + pp.onComplete(); + + to.assertResult(); + } + + @Test + public void errorAfterMainDelayError() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + + TestObserver<Void> to = Completable.mergeArrayDelayError(Completable.complete(), pp.ignoreElements()) + .test(); + + pp.onError(new TestException()); + + to.assertFailure(TestException.class); + } + + @Test + public void dispose() { + TestHelper.checkDisposed(Completable.merge(Flowable.just(Completable.complete()))); + } + + @Test + public void disposePropagates() { + + PublishProcessor<Integer> pp = PublishProcessor.create(); + + TestObserver<Void> to = Completable.merge(Flowable.just(pp.ignoreElements())).test(); + + assertTrue(pp.hasSubscribers()); + + to.dispose(); + + assertFalse(pp.hasSubscribers()); + + to.assertEmpty(); + } + + @Test + public void innerComplete() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + + TestObserver<Void> to = Completable.merge(Flowable.just(pp.ignoreElements())).test(); + + pp.onComplete(); + + to.assertResult(); + } + + @Test + public void innerError() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + + TestObserver<Void> to = Completable.merge(Flowable.just(pp.ignoreElements())).test(); + + pp.onError(new TestException()); + + to.assertFailure(TestException.class); + } + + @Test + public void innerErrorDelayError() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + + TestObserver<Void> to = Completable.mergeDelayError(Flowable.just(pp.ignoreElements())).test(); + + pp.onError(new TestException()); + + to.assertFailure(TestException.class); + } + + @Test + public void mainErrorInnerErrorRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + final PublishProcessor<Integer> pp1 = PublishProcessor.create(); + final PublishProcessor<Integer> pp2 = PublishProcessor.create(); + + TestObserverEx<Void> to = Completable.merge(pp1.map(new Function<Integer, Completable>() { + @Override + public Completable apply(Integer v) throws Exception { + return pp2.ignoreElements(); + } + })).to(TestHelper.<Void>testConsumer()); + + pp1.onNext(1); + + final Throwable ex1 = new TestException(); + final Throwable ex2 = new TestException(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + pp1.onError(ex1); + } + }; + Runnable r2 = new Runnable() { + @Override + public void run() { + pp2.onError(ex2); + } + }; + + TestHelper.race(r1, r2); + + Throwable ex = to.errors().get(0); + if (ex instanceof CompositeException) { + to.assertSubscribed().assertNoValues().assertNotComplete(); + + errors = TestHelper.compositeList(ex); + TestHelper.assertError(errors, 0, TestException.class); + TestHelper.assertError(errors, 1, TestException.class); + } else { + to.assertFailure(TestException.class); + + if (!errors.isEmpty()) { + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } + } + } finally { + RxJavaPlugins.reset(); + } + } + } + + @Test + public void mainErrorInnerErrorDelayedRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final PublishProcessor<Integer> pp1 = PublishProcessor.create(); + final PublishProcessor<Integer> pp2 = PublishProcessor.create(); + + TestObserverEx<Void> to = Completable.mergeDelayError(pp1.map(new Function<Integer, Completable>() { + @Override + public Completable apply(Integer v) throws Exception { + return pp2.ignoreElements(); + } + })).to(TestHelper.<Void>testConsumer()); + + pp1.onNext(1); + + final Throwable ex1 = new TestException(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + pp1.onError(ex1); + } + }; + + final Throwable ex2 = new TestException(); + Runnable r2 = new Runnable() { + @Override + public void run() { + pp2.onError(ex2); + } + }; + + TestHelper.race(r1, r2); + + to.assertFailure(CompositeException.class); + + List<Throwable> errors = TestHelper.compositeList(to.errors().get(0)); + + TestHelper.assertError(errors, 0, TestException.class); + TestHelper.assertError(errors, 1, TestException.class); + } + } + + @Test + public void maxConcurrencyOne() { + final PublishProcessor<Integer> pp1 = PublishProcessor.create(); + final PublishProcessor<Integer> pp2 = PublishProcessor.create(); + + TestObserver<Void> to = Completable.merge(Flowable.just(pp1.ignoreElements(), pp2.ignoreElements()), 1) + .test(); + + assertTrue(pp1.hasSubscribers()); + assertFalse(pp2.hasSubscribers()); + + pp1.onComplete(); + + assertTrue(pp2.hasSubscribers()); + + pp2.onComplete(); + + to.assertResult(); + } + + @Test + public void maxConcurrencyOneDelayError() { + final PublishProcessor<Integer> pp1 = PublishProcessor.create(); + final PublishProcessor<Integer> pp2 = PublishProcessor.create(); + + TestObserver<Void> to = Completable.mergeDelayError(Flowable.just(pp1.ignoreElements(), pp2.ignoreElements()), 1) + .test(); + + assertTrue(pp1.hasSubscribers()); + assertFalse(pp2.hasSubscribers()); + + pp1.onComplete(); + + assertTrue(pp2.hasSubscribers()); + + pp2.onComplete(); + + to.assertResult(); + } + + @Test + public void maxConcurrencyOneDelayErrorFirst() { + final PublishProcessor<Integer> pp1 = PublishProcessor.create(); + final PublishProcessor<Integer> pp2 = PublishProcessor.create(); + + TestObserver<Void> to = Completable.mergeDelayError(Flowable.just(pp1.ignoreElements(), pp2.ignoreElements()), 1) + .test(); + + assertTrue(pp1.hasSubscribers()); + assertFalse(pp2.hasSubscribers()); + + pp1.onError(new TestException()); + + assertTrue(pp2.hasSubscribers()); + + pp2.onComplete(); + + to.assertFailure(TestException.class); + } + + @Test + public void maxConcurrencyOneDelayMainErrors() { + final PublishProcessor<PublishProcessor<Integer>> pp0 = PublishProcessor.create(); + final PublishProcessor<Integer> pp1 = PublishProcessor.create(); + final PublishProcessor<Integer> pp2 = PublishProcessor.create(); + + TestObserver<Void> to = Completable.mergeDelayError( + pp0.map(new Function<PublishProcessor<Integer>, Completable>() { + @Override + public Completable apply(PublishProcessor<Integer> v) throws Exception { + return v.ignoreElements(); + } + }), 1) + .test(); + + pp0.onNext(pp1); + + assertTrue(pp1.hasSubscribers()); + assertFalse(pp2.hasSubscribers()); + + pp1.onComplete(); + + pp0.onNext(pp2); + pp0.onError(new TestException()); + + assertTrue(pp2.hasSubscribers()); + + pp2.onComplete(); + + to.assertFailure(TestException.class); + } + + @Test + public void mainDoubleOnError() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + Completable.mergeDelayError(new Flowable<Completable>() { + @Override + protected void subscribeActual(Subscriber<? super Completable> s) { + s.onSubscribe(new BooleanSubscription()); + s.onNext(Completable.complete()); + s.onError(new TestException("First")); + s.onError(new TestException("Second")); + } + }) + .to(TestHelper.<Void>testConsumer()) + .assertFailureAndMessage(TestException.class, "First"); + + TestHelper.assertUndeliverable(errors, 0, TestException.class, "Second"); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void innerDoubleOnError() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + final CompletableObserver[] o = { null }; + Completable.mergeDelayError(Flowable.just(new Completable() { + @Override + protected void subscribeActual(CompletableObserver observer) { + observer.onSubscribe(Disposable.empty()); + observer.onError(new TestException("First")); + o[0] = observer; + } + })) + .to(TestHelper.<Void>testConsumer()) + .assertFailureAndMessage(TestException.class, "First"); + + o[0].onError(new TestException("Second")); + + TestHelper.assertUndeliverable(errors, 0, TestException.class, "Second"); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void innerIsDisposed() { + final TestObserver<Void> to = new TestObserver<>(); + + Completable.mergeDelayError(Flowable.just(new Completable() { + @Override + protected void subscribeActual(CompletableObserver observer) { + observer.onSubscribe(Disposable.empty()); + assertFalse(((Disposable)observer).isDisposed()); + + to.dispose(); + + assertTrue(((Disposable)observer).isDisposed()); + } + })) + .subscribe(to); + } + + @Test + public void mergeArrayInnerErrorRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + final PublishProcessor<Integer> pp1 = PublishProcessor.create(); + final PublishProcessor<Integer> pp2 = PublishProcessor.create(); + + TestObserver<Void> to = Completable.mergeArray(pp1.ignoreElements(), pp2.ignoreElements()).test(); + + pp1.onNext(1); + + final Throwable ex1 = new TestException(); + final Throwable ex2 = new TestException(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + pp1.onError(ex1); + } + }; + Runnable r2 = new Runnable() { + @Override + public void run() { + pp2.onError(ex2); + } + }; + + TestHelper.race(r1, r2); + + to.assertFailure(TestException.class); + + if (!errors.isEmpty()) { + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } + } finally { + RxJavaPlugins.reset(); + } + } + } + + @Test + public void delayErrorIterableCancel() { + Completable.mergeDelayError(Arrays.asList(Completable.complete())) + .test(true) + .assertEmpty(); + } + + @Test + public void delayErrorIterableCancelAfterHasNext() { + final TestObserver<Void> to = new TestObserver<>(); + + Completable.mergeDelayError(new Iterable<Completable>() { + @Override + public Iterator<Completable> iterator() { + return new Iterator<Completable>() { + @Override + public boolean hasNext() { + to.dispose(); + return true; + } + + @Override + public Completable next() { + return Completable.complete(); + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + }; + } + }) + .subscribe(to); + + to.assertEmpty(); + } + + @Test + public void delayErrorIterableCancelAfterNext() { + final TestObserver<Void> to = new TestObserver<>(); + + Completable.mergeDelayError(new Iterable<Completable>() { + @Override + public Iterator<Completable> iterator() { + return new Iterator<Completable>() { + @Override + public boolean hasNext() { + return true; + } + + @Override + public Completable next() { + to.dispose(); + return Completable.complete(); + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + }; + } + }) + .subscribe(to); + + to.assertEmpty(); + } + + @Test + public void arrayUndeliverableUponCancel() { + TestHelper.checkUndeliverableUponCancel(new FlowableConverter<Integer, Completable>() { + @Override + public Completable apply(Flowable<Integer> upstream) { + return Completable.mergeArray(upstream.ignoreElements(), Completable.complete().hide()); + } + }); + } + + @Test + public void iterableUndeliverableUponCancel() { + TestHelper.checkUndeliverableUponCancel(new FlowableConverter<Integer, Completable>() { + @Override + public Completable apply(Flowable<Integer> upstream) { + return Completable.merge(Arrays.asList(upstream.ignoreElements(), Completable.complete().hide())); + } + }); + } + + @Test + public void arrayUndeliverableUponCancelDelayError() { + TestHelper.checkUndeliverableUponCancel(new FlowableConverter<Integer, Completable>() { + @Override + public Completable apply(Flowable<Integer> upstream) { + return Completable.mergeArrayDelayError(upstream.ignoreElements(), Completable.complete().hide()); + } + }); + } + + @Test + public void iterableUndeliverableUponCancelDelayError() { + TestHelper.checkUndeliverableUponCancel(new FlowableConverter<Integer, Completable>() { + @Override + public Completable apply(Flowable<Integer> upstream) { + return Completable.mergeDelayError(Arrays.asList(upstream.ignoreElements(), Completable.complete().hide())); + } + }); + } + + @Test + public void iterableCompleteLater() { + CompletableSubject cs = CompletableSubject.create(); + + TestObserver<Void> to = Completable.mergeDelayError(Arrays.asList(cs, cs, cs)) + .test(); + + to.assertEmpty(); + + cs.onComplete(); + + to.assertResult(); + } + + @Test + public void terminalDisposed() { + TestHelper.checkDisposed(new CompletableMergeArrayDelayError.TryTerminateAndReportDisposable(new AtomicThrowable())); + } + + @Test + public void innerDisposed() { + TestHelper.checkDisposed(new CompletableMergeArray.InnerCompletableObserver(new TestObserver<Void>(), new AtomicBoolean(), new CompositeDisposable(), 1)); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.<Completable>checkDoubleOnSubscribeFlowableToCompletable(f -> Completable.merge(f)); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/completable/CompletableObserveOnTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/completable/CompletableObserveOnTest.java new file mode 100644 index 0000000000..f4613f5c59 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/completable/CompletableObserveOnTest.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.completable; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.schedulers.Schedulers; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class CompletableObserveOnTest extends RxJavaTest { + + @Test + public void dispose() { + TestHelper.checkDisposed(Completable.complete().observeOn(Schedulers.single())); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeCompletable(new Function<Completable, CompletableSource>() { + @Override + public CompletableSource apply(Completable c) throws Exception { + return c.observeOn(Schedulers.single()); + } + }); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/completable/CompletableOnErrorXTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/completable/CompletableOnErrorXTest.java new file mode 100644 index 0000000000..d7b1cae653 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/completable/CompletableOnErrorXTest.java @@ -0,0 +1,106 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.completable; + +import static org.junit.Assert.assertEquals; + +import java.io.IOException; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.subjects.CompletableSubject; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class CompletableOnErrorXTest extends RxJavaTest { + + @Test + public void normalReturn() { + Completable.complete() + .onErrorComplete() + .test() + .assertResult(); + } + + @Test + public void normalResumeNext() { + final int[] call = { 0 }; + Completable.complete() + .onErrorResumeNext(new Function<Throwable, CompletableSource>() { + @Override + public CompletableSource apply(Throwable e) throws Exception { + call[0]++; + return Completable.complete(); + } + }) + .test() + .assertResult(); + + assertEquals(0, call[0]); + } + + @Test + public void onErrorReturnConst() { + Completable.error(new TestException()) + .onErrorReturnItem(1) + .test() + .assertResult(1); + } + + @Test + public void onErrorReturn() { + Completable.error(new TestException()) + .onErrorReturn(Functions.justFunction(1)) + .test() + .assertResult(1); + } + + @Test + public void onErrorReturnFunctionThrows() { + TestHelper.assertCompositeExceptions(Completable.error(new TestException()) + .onErrorReturn(new Function<Throwable, Object>() { + @Override + public Object apply(Throwable v) throws Exception { + throw new IOException(); + } + }) + .to(TestHelper.testConsumer()), TestException.class, IOException.class); + } + + @Test + public void onErrorReturnEmpty() { + Completable.complete() + .onErrorReturnItem(2) + .test() + .assertResult(); + } + + @Test + public void onErrorReturnDispose() { + TestHelper.checkDisposed(CompletableSubject.create().onErrorReturnItem(1)); + } + + @Test + public void onErrorReturnDoubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeCompletableToMaybe(new Function<Completable, MaybeSource<Object>>() { + @Override + public MaybeSource<Object> apply(Completable v) throws Exception { + return v.onErrorReturnItem(1); + } + }); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/completable/CompletablePeekTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/completable/CompletablePeekTest.java new file mode 100644 index 0000000000..827599e353 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/completable/CompletablePeekTest.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.completable; + +import java.util.List; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.Action; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.subjects.CompletableSubject; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class CompletablePeekTest extends RxJavaTest { + + @Test + public void onAfterTerminateCrashes() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + Completable.complete() + .doAfterTerminate(new Action() { + @Override + public void run() throws Exception { + throw new TestException(); + } + }) + .test() + .assertResult(); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void disposed() { + TestHelper.checkDisposed(CompletableSubject.create().doOnComplete(Functions.EMPTY_ACTION)); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/completable/CompletableRepeatWhenTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/completable/CompletableRepeatWhenTest.java new file mode 100644 index 0000000000..d9a262b317 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/completable/CompletableRepeatWhenTest.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.completable; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; +import org.reactivestreams.Publisher; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.functions.*; + +public class CompletableRepeatWhenTest extends RxJavaTest { + @Test + public void whenCounted() { + + final int[] counter = { 0 }; + + Completable.fromAction(new Action() { + @Override + public void run() throws Exception { + counter[0]++; + } + }) + .repeatWhen(new Function<Flowable<Object>, Publisher<Object>>() { + @Override + public Publisher<Object> apply(Flowable<Object> f) throws Exception { + final int[] j = { 3 }; + return f.takeWhile(new Predicate<Object>() { + @Override + public boolean test(Object v) throws Exception { + return j[0]-- != 0; + } + }); + } + }) + .subscribe(); + + assertEquals(4, counter[0]); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/completable/CompletableResumeNextTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/completable/CompletableResumeNextTest.java new file mode 100644 index 0000000000..a8f0390c63 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/completable/CompletableResumeNextTest.java @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.completable; + +import static org.mockito.Mockito.*; +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class CompletableResumeNextTest extends RxJavaTest { + + @Test + public void resumeNextError() { + Completable.error(new TestException()) + .onErrorResumeNext(Functions.justFunction(Completable.error(new TestException("second")))) + .to(TestHelper.<Object>testConsumer()) + .assertFailureAndMessage(TestException.class, "second"); + } + + @Test + public void disposeInMain() { + TestHelper.checkDisposedCompletable(new Function<Completable, CompletableSource>() { + @Override + public CompletableSource apply(Completable c) throws Exception { + return c.onErrorResumeNext(Functions.justFunction(Completable.complete())); + } + }); + } + + @Test + public void disposeInResume() { + TestHelper.checkDisposedCompletable(new Function<Completable, CompletableSource>() { + @Override + public CompletableSource apply(Completable c) throws Exception { + return Completable.error(new TestException()).onErrorResumeNext(Functions.justFunction(c)); + } + }); + } + + @Test + public void disposed() { + TestHelper.checkDisposed( + Completable.error(new TestException()) + .onErrorResumeNext(Functions.justFunction(Completable.never())) + ); + } + + @Test + public void resumeWithNoError() throws Throwable { + Action action = mock(Action.class); + + Completable.complete() + .onErrorResumeWith(Completable.fromAction(action)) + .test() + .assertResult(); + + verify(action, never()).run(); + } + + @Test + public void resumeWithError() throws Throwable { + Action action = mock(Action.class); + + Completable.error(new TestException()) + .onErrorResumeWith(Completable.fromAction(action)) + .test() + .assertResult(); + + verify(action).run(); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/completable/CompletableSafeSubscribeTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/completable/CompletableSafeSubscribeTest.java new file mode 100644 index 0000000000..40c618ac0a --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/completable/CompletableSafeSubscribeTest.java @@ -0,0 +1,148 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.completable; + +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import java.io.IOException; + +import org.junit.Test; +import org.mockito.InOrder; + +import io.reactivex.rxjava3.annotations.NonNull; +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class CompletableSafeSubscribeTest { + + @Test + public void normalError() throws Throwable { + TestHelper.withErrorTracking(errors -> { + CompletableObserver consumer = mock(CompletableObserver.class); + + Completable.error(new TestException()) + .safeSubscribe(consumer); + + InOrder order = inOrder(consumer); + order.verify(consumer).onSubscribe(any(Disposable.class)); + order.verify(consumer).onError(any(TestException.class)); + order.verifyNoMoreInteractions(); + + assertTrue("" + errors, errors.isEmpty()); + }); + } + + @Test + public void normalEmpty() throws Throwable { + TestHelper.withErrorTracking(errors -> { + CompletableObserver consumer = mock(CompletableObserver.class); + + Completable.complete() + .safeSubscribe(consumer); + + InOrder order = inOrder(consumer); + order.verify(consumer).onSubscribe(any(Disposable.class)); + order.verify(consumer).onComplete(); + order.verifyNoMoreInteractions(); + }); + } + + @Test + public void onSubscribeCrash() throws Throwable { + TestHelper.withErrorTracking(errors -> { + CompletableObserver consumer = mock(CompletableObserver.class); + doThrow(new TestException()).when(consumer).onSubscribe(any()); + + Disposable d = Disposable.empty(); + + new Completable() { + @Override + protected void subscribeActual(@NonNull CompletableObserver observer) { + observer.onSubscribe(d); + // none of the following should arrive at the consumer + observer.onError(new IOException()); + observer.onComplete(); + } + } + .safeSubscribe(consumer); + + InOrder order = inOrder(consumer); + order.verify(consumer).onSubscribe(any(Disposable.class)); + order.verifyNoMoreInteractions(); + + assertTrue(d.isDisposed()); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + TestHelper.assertUndeliverable(errors, 1, IOException.class); + }); + } + + @Test + public void onErrorCrash() throws Throwable { + TestHelper.withErrorTracking(errors -> { + CompletableObserver consumer = mock(CompletableObserver.class); + doThrow(new TestException()).when(consumer).onError(any()); + + new Completable() { + @Override + protected void subscribeActual(@NonNull CompletableObserver observer) { + observer.onSubscribe(Disposable.empty()); + // none of the following should arrive at the consumer + observer.onError(new IOException()); + } + } + .safeSubscribe(consumer); + + InOrder order = inOrder(consumer); + order.verify(consumer).onSubscribe(any(Disposable.class)); + order.verify(consumer).onError(any(IOException.class)); + order.verifyNoMoreInteractions(); + + TestHelper.assertError(errors, 0, CompositeException.class); + + CompositeException compositeException = (CompositeException)errors.get(0); + TestHelper.assertError(compositeException.getExceptions(), 0, IOException.class); + TestHelper.assertError(compositeException.getExceptions(), 1, TestException.class); + }); + } + + @Test + public void onCompleteCrash() throws Throwable { + TestHelper.withErrorTracking(errors -> { + CompletableObserver consumer = mock(CompletableObserver.class); + doThrow(new TestException()).when(consumer).onComplete(); + + new Completable() { + @Override + protected void subscribeActual(@NonNull CompletableObserver observer) { + observer.onSubscribe(Disposable.empty()); + // none of the following should arrive at the consumer + observer.onComplete(); + } + } + .safeSubscribe(consumer); + + InOrder order = inOrder(consumer); + order.verify(consumer).onSubscribe(any(Disposable.class)); + order.verify(consumer).onComplete(); + order.verifyNoMoreInteractions(); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + }); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/completable/CompletableSequenceEqualTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/completable/CompletableSequenceEqualTest.java new file mode 100644 index 0000000000..9e3fd56b99 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/completable/CompletableSequenceEqualTest.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.completable; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.Completable; +import io.reactivex.rxjava3.exceptions.TestException; + +public class CompletableSequenceEqualTest { + + @Test + public void bothComplete() { + Completable.sequenceEqual(Completable.complete(), Completable.complete()) + .test() + .assertResult(true); + } + + @Test + public void firstFails() { + Completable.sequenceEqual(Completable.error(new TestException()), Completable.complete()) + .test() + .assertFailure(TestException.class); + } + + @Test + public void secondFails() { + Completable.sequenceEqual(Completable.complete(), Completable.error(new TestException())) + .test() + .assertFailure(TestException.class); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/completable/CompletableStartWithTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/completable/CompletableStartWithTest.java new file mode 100644 index 0000000000..4b2ed6d588 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/completable/CompletableStartWithTest.java @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.completable; + +import static org.mockito.Mockito.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; + +public class CompletableStartWithTest { + + @Test + public void singleNormal() { + Completable.complete().startWith(Single.just(1)) + .test() + .assertResult(1); + } + + @Test + public void singleError() { + Runnable run = mock(Runnable.class); + + Completable.fromRunnable(run).startWith(Single.error(new TestException())) + .test() + .assertFailure(TestException.class); + + verify(run, never()).run(); + } + + @Test + public void maybeNormal() { + Completable.complete().startWith(Maybe.just(1)) + .test() + .assertResult(1); + } + + @Test + public void maybeEmptyNormal() { + Completable.complete().startWith(Maybe.empty()) + .test() + .assertResult(); + } + + @Test + public void maybeError() { + Runnable run = mock(Runnable.class); + + Completable.fromRunnable(run).startWith(Maybe.error(new TestException())) + .test() + .assertFailure(TestException.class); + + verify(run, never()).run(); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/completable/CompletableSubscribeOnTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/completable/CompletableSubscribeOnTest.java new file mode 100644 index 0000000000..3c20a12588 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/completable/CompletableSubscribeOnTest.java @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.completable; + +import static org.junit.Assert.assertTrue; + +import java.util.List; +import java.util.concurrent.TimeUnit; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.schedulers.*; +import io.reactivex.rxjava3.subjects.PublishSubject; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class CompletableSubscribeOnTest extends RxJavaTest { + + @Test + public void normal() { + List<Throwable> list = TestHelper.trackPluginErrors(); + try { + TestScheduler scheduler = new TestScheduler(); + + TestObserver<Void> to = Completable.complete() + .subscribeOn(scheduler) + .test(); + + scheduler.advanceTimeBy(1, TimeUnit.SECONDS); + + to.assertResult(); + + assertTrue(list.toString(), list.isEmpty()); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void dispose() { + TestHelper.checkDisposed(PublishSubject.create().ignoreElements().subscribeOn(new TestScheduler())); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeCompletable(new Function<Completable, CompletableSource>() { + @Override + public CompletableSource apply(Completable c) throws Exception { + return c.subscribeOn(Schedulers.single()); + } + }); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/completable/CompletableSubscribeTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/completable/CompletableSubscribeTest.java new file mode 100644 index 0000000000..f0fded5996 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/completable/CompletableSubscribeTest.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.completable; + +import static org.junit.Assert.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.RxJavaTest; +import io.reactivex.rxjava3.processors.PublishProcessor; +import io.reactivex.rxjava3.subjects.PublishSubject; + +public class CompletableSubscribeTest extends RxJavaTest { + @Test + public void subscribeAlreadyCancelled() { + + PublishProcessor<Integer> pp = PublishProcessor.create(); + + pp.ignoreElements().test(true); + + assertFalse(pp.hasSubscribers()); + } + + @Test + public void methodTestNoCancel() { + PublishSubject<Integer> ps = PublishSubject.create(); + + ps.ignoreElements().test(false); + + assertTrue(ps.hasObservers()); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/completable/CompletableSwitchOnNextTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/completable/CompletableSwitchOnNextTest.java new file mode 100644 index 0000000000..312f9653ed --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/completable/CompletableSwitchOnNextTest.java @@ -0,0 +1,132 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.completable; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.processors.PublishProcessor; +import io.reactivex.rxjava3.subjects.CompletableSubject; + +public class CompletableSwitchOnNextTest extends RxJavaTest { + + @Test + public void normal() { + Runnable run = mock(Runnable.class); + + Completable.switchOnNext( + Flowable.range(1, 10) + .map(v -> { + if (v % 2 == 0) { + return Completable.fromRunnable(run); + } + return Completable.complete(); + }) + ) + .test() + .assertResult(); + + verify(run, times(5)).run(); + } + + @Test + public void normalDelayError() { + Runnable run = mock(Runnable.class); + + Completable.switchOnNextDelayError( + Flowable.range(1, 10) + .map(v -> { + if (v % 2 == 0) { + return Completable.fromRunnable(run); + } + return Completable.complete(); + }) + ) + .test() + .assertResult(); + + verify(run, times(5)).run(); + } + + @Test + public void noDelaySwitch() { + PublishProcessor<Completable> pp = PublishProcessor.create(); + + TestObserver<Void> to = Completable.switchOnNext(pp).test(); + + assertTrue(pp.hasSubscribers()); + + to.assertEmpty(); + + CompletableSubject cs1 = CompletableSubject.create(); + CompletableSubject cs2 = CompletableSubject.create(); + + pp.onNext(cs1); + + assertTrue(cs1.hasObservers()); + + pp.onNext(cs2); + + assertFalse(cs1.hasObservers()); + assertTrue(cs2.hasObservers()); + + pp.onComplete(); + + assertTrue(cs2.hasObservers()); + + cs2.onComplete(); + + to.assertResult(); + } + + @Test + public void delaySwitch() { + PublishProcessor<Completable> pp = PublishProcessor.create(); + + TestObserver<Void> to = Completable.switchOnNextDelayError(pp).test(); + + assertTrue(pp.hasSubscribers()); + + to.assertEmpty(); + + CompletableSubject cs1 = CompletableSubject.create(); + CompletableSubject cs2 = CompletableSubject.create(); + + pp.onNext(cs1); + + assertTrue(cs1.hasObservers()); + + pp.onNext(cs2); + + assertFalse(cs1.hasObservers()); + assertTrue(cs2.hasObservers()); + + assertTrue(cs2.hasObservers()); + + cs2.onError(new TestException()); + + assertTrue(pp.hasSubscribers()); + + to.assertEmpty(); + + pp.onComplete(); + + to.assertFailure(TestException.class); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/completable/CompletableTakeUntilTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/completable/CompletableTakeUntilTest.java new file mode 100644 index 0000000000..1eefff1956 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/completable/CompletableTakeUntilTest.java @@ -0,0 +1,236 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.completable; + +import static org.junit.Assert.*; + +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; + +import io.reactivex.rxjava3.disposables.Disposable; +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.subjects.CompletableSubject; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class CompletableTakeUntilTest extends RxJavaTest { + + @Test + public void consumerDisposes() { + CompletableSubject cs1 = CompletableSubject.create(); + CompletableSubject cs2 = CompletableSubject.create(); + + TestObserver<Void> to = cs1.takeUntil(cs2).test(); + + to.assertEmpty(); + + assertTrue(cs1.hasObservers()); + assertTrue(cs2.hasObservers()); + + to.dispose(); + + assertFalse(cs1.hasObservers()); + assertFalse(cs2.hasObservers()); + } + + @Test + public void mainCompletes() { + CompletableSubject cs1 = CompletableSubject.create(); + CompletableSubject cs2 = CompletableSubject.create(); + + TestObserver<Void> to = cs1.takeUntil(cs2).test(); + + to.assertEmpty(); + + assertTrue(cs1.hasObservers()); + assertTrue(cs2.hasObservers()); + + cs1.onComplete(); + + assertFalse(cs1.hasObservers()); + assertFalse(cs2.hasObservers()); + + to.assertResult(); + } + + @Test + public void otherCompletes() { + CompletableSubject cs1 = CompletableSubject.create(); + CompletableSubject cs2 = CompletableSubject.create(); + + TestObserver<Void> to = cs1.takeUntil(cs2).test(); + + to.assertEmpty(); + + assertTrue(cs1.hasObservers()); + assertTrue(cs2.hasObservers()); + + cs2.onComplete(); + + assertFalse(cs1.hasObservers()); + assertFalse(cs2.hasObservers()); + + to.assertResult(); + } + + @Test + public void mainErrors() { + CompletableSubject cs1 = CompletableSubject.create(); + CompletableSubject cs2 = CompletableSubject.create(); + + TestObserver<Void> to = cs1.takeUntil(cs2).test(); + + to.assertEmpty(); + + assertTrue(cs1.hasObservers()); + assertTrue(cs2.hasObservers()); + + cs1.onError(new TestException()); + + assertFalse(cs1.hasObservers()); + assertFalse(cs2.hasObservers()); + + to.assertFailure(TestException.class); + } + + @Test + public void otherErrors() { + CompletableSubject cs1 = CompletableSubject.create(); + CompletableSubject cs2 = CompletableSubject.create(); + + TestObserver<Void> to = cs1.takeUntil(cs2).test(); + + to.assertEmpty(); + + assertTrue(cs1.hasObservers()); + assertTrue(cs2.hasObservers()); + + cs2.onError(new TestException()); + + assertFalse(cs1.hasObservers()); + assertFalse(cs2.hasObservers()); + + to.assertFailure(TestException.class); + } + + @Test + public void isDisposed() { + CompletableSubject cs1 = CompletableSubject.create(); + CompletableSubject cs2 = CompletableSubject.create(); + + TestHelper.checkDisposed(cs1.takeUntil(cs2)); + } + + @Test + public void mainErrorLate() { + + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + + new Completable() { + @Override + protected void subscribeActual(CompletableObserver observer) { + observer.onSubscribe(Disposable.empty()); + observer.onError(new TestException()); + } + }.takeUntil(Completable.complete()) + .test() + .assertResult(); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void mainCompleteLate() { + + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + + new Completable() { + @Override + protected void subscribeActual(CompletableObserver observer) { + observer.onSubscribe(Disposable.empty()); + observer.onComplete(); + } + }.takeUntil(Completable.complete()) + .test() + .assertResult(); + + assertTrue(errors.isEmpty()); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void otherErrorLate() { + + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + + final AtomicReference<CompletableObserver> ref = new AtomicReference<>(); + + Completable.complete() + .takeUntil(new Completable() { + @Override + protected void subscribeActual(CompletableObserver observer) { + observer.onSubscribe(Disposable.empty()); + ref.set(observer); + } + }) + .test() + .assertResult(); + + ref.get().onError(new TestException()); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void otherCompleteLate() { + + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + + final AtomicReference<CompletableObserver> ref = new AtomicReference<>(); + + Completable.complete() + .takeUntil(new Completable() { + @Override + protected void subscribeActual(CompletableObserver observer) { + observer.onSubscribe(Disposable.empty()); + ref.set(observer); + } + }) + .test() + .assertResult(); + + ref.get().onComplete(); + + assertTrue(errors.isEmpty()); + } finally { + RxJavaPlugins.reset(); + } + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/completable/CompletableTimeoutTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/completable/CompletableTimeoutTest.java new file mode 100644 index 0000000000..86f2f2890a --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/completable/CompletableTimeoutTest.java @@ -0,0 +1,177 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.completable; + +import static io.reactivex.rxjava3.internal.util.ExceptionHelper.timeoutMessage; +import static org.junit.Assert.*; + +import java.util.List; +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.Action; +import io.reactivex.rxjava3.internal.operators.completable.CompletableTimeout.TimeOutObserver; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.schedulers.*; +import io.reactivex.rxjava3.subjects.PublishSubject; +import io.reactivex.rxjava3.testsupport.*; + +public class CompletableTimeoutTest extends RxJavaTest { + + @Test + public void timeoutException() throws Exception { + + Completable.never() + .timeout(100, TimeUnit.MILLISECONDS, Schedulers.io()) + .to(TestHelper.<Void>testConsumer()) + .awaitDone(5, TimeUnit.SECONDS) + .assertFailureAndMessage(TimeoutException.class, timeoutMessage(100, TimeUnit.MILLISECONDS)); + } + + @Test + public void timeoutContinueOther() throws Exception { + + final int[] call = { 0 }; + + Completable other = Completable.fromAction(new Action() { + @Override + public void run() throws Exception { + call[0]++; + } + }); + + Completable.never() + .timeout(100, TimeUnit.MILLISECONDS, Schedulers.io(), other) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(); + + assertEquals(1, call[0]); + } + + @Test + public void shouldUnsubscribeFromUnderlyingSubscriptionOnDispose() { + final PublishSubject<String> subject = PublishSubject.create(); + final TestScheduler scheduler = new TestScheduler(); + + final TestObserver<Void> observer = subject.ignoreElements() + .timeout(100, TimeUnit.MILLISECONDS, scheduler) + .test(); + + assertTrue(subject.hasObservers()); + + observer.dispose(); + + assertFalse(subject.hasObservers()); + } + + @Test + public void otherErrors() { + Completable.never() + .timeout(1, TimeUnit.MILLISECONDS, Completable.error(new TestException())) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertFailure(TestException.class); + } + + @Test + public void mainSuccess() { + Completable.complete() + .timeout(1, TimeUnit.DAYS) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(); + } + + @Test + public void mainError() { + Completable.error(new TestException()) + .timeout(1, TimeUnit.DAYS) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertFailure(TestException.class); + } + + @Test + public void errorTimeoutRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + List<Throwable> errors = TestHelper.trackPluginErrors(); + + try { + final TestScheduler scheduler = new TestScheduler(); + + final PublishSubject<Integer> ps = PublishSubject.create(); + + TestObserverEx<Void> to = ps.ignoreElements() + .timeout(1, TimeUnit.MILLISECONDS, scheduler, Completable.complete()) + .to(TestHelper.<Void>testConsumer()); + + final TestException ex = new TestException(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + ps.onError(ex); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + scheduler.advanceTimeBy(1, TimeUnit.MILLISECONDS); + } + }; + + TestHelper.race(r1, r2); + + to.assertTerminated(); + + if (!errors.isEmpty()) { + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } + + } finally { + RxJavaPlugins.reset(); + } + } + } + + @Test + public void ambRace() { + TestObserver<Void> to = new TestObserver<>(); + to.onSubscribe(Disposable.empty()); + + CompositeDisposable cd = new CompositeDisposable(); + AtomicBoolean once = new AtomicBoolean(); + TimeOutObserver a = new TimeOutObserver(cd, once, to); + + a.onComplete(); + a.onComplete(); + + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + a.onError(new TestException()); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/completable/CompletableTimerTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/completable/CompletableTimerTest.java new file mode 100644 index 0000000000..bfd87a1af6 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/completable/CompletableTimerTest.java @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.completable; + +import static org.junit.Assert.assertTrue; + +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.functions.Action; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.schedulers.*; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class CompletableTimerTest extends RxJavaTest { + + @Test + public void dispose() { + TestHelper.checkDisposed(Completable.timer(1, TimeUnit.SECONDS, new TestScheduler())); + } + + @Test + public void timerInterruptible() throws Exception { + ScheduledExecutorService exec = Executors.newSingleThreadScheduledExecutor(); + try { + for (Scheduler s : new Scheduler[] { Schedulers.single(), Schedulers.computation(), Schedulers.newThread(), Schedulers.io(), Schedulers.from(exec, true) }) { + final AtomicBoolean interrupted = new AtomicBoolean(); + TestObserver<Void> to = Completable.timer(1, TimeUnit.MILLISECONDS, s) + .doOnComplete(new Action() { + @Override + public void run() throws Exception { + try { + Thread.sleep(3000); + } catch (InterruptedException ex) { + interrupted.set(true); + } + } + }) + .test(); + + Thread.sleep(500); + + to.dispose(); + + Thread.sleep(500); + + assertTrue(s.getClass().getSimpleName(), interrupted.get()); + } + } finally { + exec.shutdown(); + } + } + +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/completable/CompletableToFlowableTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/completable/CompletableToFlowableTest.java new file mode 100644 index 0000000000..8c10ea5225 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/completable/CompletableToFlowableTest.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.completable; + +import org.junit.Test; +import org.reactivestreams.Publisher; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class CompletableToFlowableTest extends RxJavaTest { + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeCompletableToFlowable(new Function<Completable, Publisher<?>>() { + @Override + public Publisher<?> apply(Completable c) throws Exception { + return c.toFlowable(); + } + }); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/completable/CompletableToFutureTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/completable/CompletableToFutureTest.java new file mode 100644 index 0000000000..c857e95ebb --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/completable/CompletableToFutureTest.java @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.completable; + +import static org.junit.Assert.*; + +import java.util.concurrent.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.schedulers.Schedulers; +import io.reactivex.rxjava3.subjects.CompletableSubject; + +public class CompletableToFutureTest extends RxJavaTest { + + @Test + public void empty() throws Exception { + assertNull(Completable.complete() + .subscribeOn(Schedulers.computation()) + .toFuture() + .get()); + } + + @Test + public void error() throws InterruptedException { + try { + Completable.error(new TestException()) + .subscribeOn(Schedulers.computation()) + .toFuture() + .get(); + + fail("Should have thrown!"); + } catch (ExecutionException ex) { + assertTrue("" + ex.getCause(), ex.getCause() instanceof TestException); + } + } + + @Test + public void cancel() { + CompletableSubject cs = CompletableSubject.create(); + + Future<Void> f = cs.toFuture(); + + assertTrue(cs.hasObservers()); + + f.cancel(true); + + assertFalse(cs.hasObservers()); + } + + @Test + public void cancel2() { + CompletableSubject cs = CompletableSubject.create(); + + Future<Void> f = cs.toFuture(); + + assertTrue(cs.hasObservers()); + + f.cancel(false); + + assertFalse(cs.hasObservers()); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/completable/CompletableToObservableTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/completable/CompletableToObservableTest.java new file mode 100644 index 0000000000..7ad93e9bef --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/completable/CompletableToObservableTest.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.completable; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class CompletableToObservableTest extends RxJavaTest { + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeCompletableToObservable(new Function<Completable, Observable<?>>() { + @Override + public Observable<?> apply(Completable c) throws Exception { + return c.toObservable(); + } + }); + } + +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/completable/CompletableUnsafeTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/completable/CompletableUnsafeTest.java new file mode 100644 index 0000000000..72b28edea0 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/completable/CompletableUnsafeTest.java @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.completable; + +import static org.junit.Assert.*; + +import java.util.List; + +import io.reactivex.rxjava3.disposables.Disposable; +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class CompletableUnsafeTest extends RxJavaTest { + + @Test(expected = IllegalArgumentException.class) + public void unsafeCreateRejectsCompletable() { + Completable.unsafeCreate(Completable.complete()); + } + + @Test + public void wrapAlreadyCompletable() { + assertSame(Completable.complete(), Completable.wrap(Completable.complete())); + } + + @Test + public void wrapCustomCompletable() { + + Completable.wrap(new CompletableSource() { + @Override + public void subscribe(CompletableObserver observer) { + observer.onSubscribe(Disposable.empty()); + observer.onComplete(); + } + }) + .test() + .assertResult(); + } + + @Test(expected = NullPointerException.class) + public void unsafeCreateThrowsNPE() { + Completable.unsafeCreate(new CompletableSource() { + @Override + public void subscribe(CompletableObserver observer) { + throw new NullPointerException(); + } + }).test(); + } + + @Test + public void unsafeCreateThrowsIAE() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + Completable.unsafeCreate(new CompletableSource() { + @Override + public void subscribe(CompletableObserver observer) { + throw new IllegalArgumentException(); + } + }).test(); + fail("Should have thrown!"); + } catch (NullPointerException ex) { + if (!(ex.getCause() instanceof IllegalArgumentException)) { + fail(ex.toString() + ": should have thrown NPA(IAE)"); + } + + TestHelper.assertError(errors, 0, IllegalArgumentException.class); + } finally { + RxJavaPlugins.reset(); + } + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/completable/CompletableUsingTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/completable/CompletableUsingTest.java new file mode 100644 index 0000000000..72e4d16491 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/completable/CompletableUsingTest.java @@ -0,0 +1,636 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.completable; + +import static org.junit.Assert.*; + +import java.util.List; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.*; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.subjects.PublishSubject; +import io.reactivex.rxjava3.testsupport.*; + +public class CompletableUsingTest extends RxJavaTest { + + @Test + public void resourceSupplierThrows() { + + Completable.using(new Supplier<Object>() { + @Override + public Object get() throws Exception { + throw new TestException(); + } + }, new Function<Object, CompletableSource>() { + @Override + public CompletableSource apply(Object v) throws Exception { + return Completable.complete(); + } + }, new Consumer<Object>() { + @Override + public void accept(Object d) throws Exception { + + } + }) + .test() + .assertFailure(TestException.class); + } + + @Test + public void errorEager() { + + Completable.using(new Supplier<Object>() { + @Override + public Object get() throws Exception { + return 1; + } + }, new Function<Object, CompletableSource>() { + @Override + public CompletableSource apply(Object v) throws Exception { + return Completable.error(new TestException()); + } + }, new Consumer<Object>() { + @Override + public void accept(Object d) throws Exception { + + } + }, true) + .test() + .assertFailure(TestException.class); + } + + @Test + public void emptyEager() { + + Completable.using(new Supplier<Object>() { + @Override + public Object get() throws Exception { + return 1; + } + }, new Function<Object, CompletableSource>() { + @Override + public CompletableSource apply(Object v) throws Exception { + return Completable.complete(); + } + }, new Consumer<Object>() { + @Override + public void accept(Object d) throws Exception { + + } + }, true) + .test() + .assertResult(); + } + + @Test + public void errorNonEager() { + + Completable.using(new Supplier<Object>() { + @Override + public Object get() throws Exception { + return 1; + } + }, new Function<Object, CompletableSource>() { + @Override + public CompletableSource apply(Object v) throws Exception { + return Completable.error(new TestException()); + } + }, new Consumer<Object>() { + @Override + public void accept(Object d) throws Exception { + + } + }, false) + .test() + .assertFailure(TestException.class); + } + + @Test + public void emptyNonEager() { + + Completable.using(new Supplier<Object>() { + @Override + public Object get() throws Exception { + return 1; + } + }, new Function<Object, CompletableSource>() { + @Override + public CompletableSource apply(Object v) throws Exception { + return Completable.complete(); + } + }, new Consumer<Object>() { + @Override + public void accept(Object d) throws Exception { + + } + }, false) + .test() + .assertResult(); + } + + @Test + public void supplierCrashEager() { + + Completable.using(new Supplier<Object>() { + @Override + public Object get() throws Exception { + return 1; + } + }, new Function<Object, CompletableSource>() { + @Override + public CompletableSource apply(Object v) throws Exception { + throw new TestException(); + } + }, new Consumer<Object>() { + @Override + public void accept(Object d) throws Exception { + + } + }, true) + .test() + .assertFailure(TestException.class); + } + + @Test + public void supplierCrashNonEager() { + + Completable.using(new Supplier<Object>() { + @Override + public Object get() throws Exception { + return 1; + } + }, new Function<Object, CompletableSource>() { + @Override + public CompletableSource apply(Object v) throws Exception { + throw new TestException(); + } + }, new Consumer<Object>() { + @Override + public void accept(Object d) throws Exception { + + } + }, false) + .test() + .assertFailure(TestException.class); + } + + @Test + public void supplierAndDisposerCrashEager() { + TestObserverEx<Void> to = Completable.using(new Supplier<Object>() { + @Override + public Object get() throws Exception { + return 1; + } + }, new Function<Object, CompletableSource>() { + @Override + public CompletableSource apply(Object v) throws Exception { + throw new TestException("Main"); + } + }, new Consumer<Object>() { + @Override + public void accept(Object d) throws Exception { + throw new TestException("Disposer"); + } + }, true) + .to(TestHelper.<Void>testConsumer()) + .assertFailure(CompositeException.class); + + List<Throwable> list = TestHelper.compositeList(to.errors().get(0)); + + TestHelper.assertError(list, 0, TestException.class, "Main"); + TestHelper.assertError(list, 1, TestException.class, "Disposer"); + } + + @Test + public void supplierAndDisposerCrashNonEager() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + Completable.using(new Supplier<Object>() { + @Override + public Object get() throws Exception { + return 1; + } + }, new Function<Object, CompletableSource>() { + @Override + public CompletableSource apply(Object v) throws Exception { + throw new TestException("Main"); + } + }, new Consumer<Object>() { + @Override + public void accept(Object d) throws Exception { + throw new TestException("Disposer"); + } + }, false) + .to(TestHelper.<Void>testConsumer()) + .assertFailureAndMessage(TestException.class, "Main"); + + TestHelper.assertUndeliverable(errors, 0, TestException.class, "Disposer"); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void dispose() { + final int[] call = {0 }; + + TestObserverEx<Void> to = Completable.using(new Supplier<Object>() { + @Override + public Object get() throws Exception { + return 1; + } + }, new Function<Object, CompletableSource>() { + @Override + public CompletableSource apply(Object v) throws Exception { + return Completable.never(); + } + }, new Consumer<Object>() { + @Override + public void accept(Object d) throws Exception { + call[0]++; + } + }, false) + .to(TestHelper.<Void>testConsumer()); + + to.dispose(); + + assertEquals(1, call[0]); + } + + @Test + public void disposeCrashes() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + TestObserver<Void> to = Completable.using(new Supplier<Object>() { + @Override + public Object get() throws Exception { + return 1; + } + }, new Function<Object, CompletableSource>() { + @Override + public CompletableSource apply(Object v) throws Exception { + return Completable.never(); + } + }, new Consumer<Object>() { + @Override + public void accept(Object d) throws Exception { + throw new TestException(); + } + }, false) + .test(); + + to.dispose(); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void isDisposed() { + TestHelper.checkDisposed(Completable.using(new Supplier<Object>() { + @Override + public Object get() throws Exception { + return 1; + } + }, new Function<Object, CompletableSource>() { + @Override + public CompletableSource apply(Object v) throws Exception { + return Completable.never(); + } + }, new Consumer<Object>() { + @Override + public void accept(Object d) throws Exception { + + } + }, false)); + } + + @Test + public void justDisposerCrashes() { + Completable.using(new Supplier<Object>() { + @Override + public Object get() throws Exception { + return 1; + } + }, new Function<Object, CompletableSource>() { + @Override + public CompletableSource apply(Object v) throws Exception { + return Completable.complete(); + } + }, new Consumer<Object>() { + @Override + public void accept(Object d) throws Exception { + throw new TestException("Disposer"); + } + }, true) + .test() + .assertFailure(TestException.class); + } + + @Test + public void emptyDisposerCrashes() { + Completable.using(new Supplier<Object>() { + @Override + public Object get() throws Exception { + return 1; + } + }, new Function<Object, CompletableSource>() { + @Override + public CompletableSource apply(Object v) throws Exception { + return Completable.complete(); + } + }, new Consumer<Object>() { + @Override + public void accept(Object d) throws Exception { + throw new TestException("Disposer"); + } + }, true) + .test() + .assertFailure(TestException.class); + } + + @Test + public void errorDisposerCrash() { + TestObserverEx<Void> to = Completable.using(new Supplier<Object>() { + @Override + public Object get() throws Exception { + return 1; + } + }, new Function<Object, CompletableSource>() { + @Override + public CompletableSource apply(Object v) throws Exception { + return Completable.error(new TestException("Main")); + } + }, new Consumer<Object>() { + @Override + public void accept(Object d) throws Exception { + throw new TestException("Disposer"); + } + }, true) + .to(TestHelper.<Void>testConsumer()) + .assertFailure(CompositeException.class); + + List<Throwable> list = TestHelper.compositeList(to.errors().get(0)); + + TestHelper.assertError(list, 0, TestException.class, "Main"); + TestHelper.assertError(list, 1, TestException.class, "Disposer"); + } + + @Test + public void doubleOnSubscribe() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + Completable.using(new Supplier<Object>() { + @Override + public Object get() throws Exception { + return 1; + } + }, new Function<Object, CompletableSource>() { + @Override + public CompletableSource apply(Object v) throws Exception { + return Completable.wrap(new CompletableSource() { + @Override + public void subscribe(CompletableObserver observer) { + Disposable d1 = Disposable.empty(); + + observer.onSubscribe(d1); + + Disposable d2 = Disposable.empty(); + + observer.onSubscribe(d2); + + assertFalse(d1.isDisposed()); + + assertTrue(d2.isDisposed()); + } + }); + } + }, new Consumer<Object>() { + @Override + public void accept(Object d) throws Exception { + + } + }, false).test(); + TestHelper.assertError(errors, 0, IllegalStateException.class, "Disposable already set!"); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void successDisposeRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + + final PublishSubject<Integer> ps = PublishSubject.create(); + + final TestObserverEx<Void> to = Completable.using(new Supplier<Object>() { + @Override + public Object get() throws Exception { + return 1; + } + }, new Function<Object, CompletableSource>() { + @Override + public CompletableSource apply(Object v) throws Exception { + return ps.ignoreElements(); + } + }, new Consumer<Object>() { + @Override + public void accept(Object d) throws Exception { + } + }, true) + .to(TestHelper.<Void>testConsumer()); + + ps.onNext(1); + + Runnable r1 = new Runnable() { + @Override + public void run() { + to.dispose(); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + ps.onComplete(); + } + }; + + TestHelper.race(r1, r2); + } + } + + @Test + public void errorDisposeRace() { + RxJavaPlugins.setErrorHandler(Functions.emptyConsumer()); + try { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + + final PublishSubject<Integer> ps = PublishSubject.create(); + + final TestObserver<Void> to = Completable.using(new Supplier<Object>() { + @Override + public Object get() throws Exception { + return 1; + } + }, new Function<Object, CompletableSource>() { + @Override + public CompletableSource apply(Object v) throws Exception { + return ps.ignoreElements(); + } + }, new Consumer<Object>() { + @Override + public void accept(Object d) throws Exception { + } + }, true) + .test(); + + final TestException ex = new TestException(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + to.dispose(); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + ps.onError(ex); + } + }; + + TestHelper.race(r1, r2); + } + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void emptyDisposeRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + + final PublishSubject<Integer> ps = PublishSubject.create(); + + final TestObserver<Void> to = Completable.using(new Supplier<Object>() { + @Override + public Object get() throws Exception { + return 1; + } + }, new Function<Object, CompletableSource>() { + @Override + public CompletableSource apply(Object v) throws Exception { + return ps.ignoreElements(); + } + }, new Consumer<Object>() { + @Override + public void accept(Object d) throws Exception { + + } + }, true) + .test(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + to.dispose(); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + ps.onComplete(); + } + }; + + TestHelper.race(r1, r2); + } + } + + @Test + public void eagerDisposeResourceThenDisposeUpstream() { + final StringBuilder sb = new StringBuilder(); + + TestObserver<Void> to = Completable.using(Functions.justSupplier(1), + new Function<Integer, Completable>() { + @Override + public Completable apply(Integer t) throws Throwable { + return Completable.never() + .doOnDispose(new Action() { + @Override + public void run() throws Throwable { + sb.append("Dispose"); + } + }) + ; + } + }, new Consumer<Integer>() { + @Override + public void accept(Integer t) throws Throwable { + sb.append("Resource"); + } + }, true) + .test() + ; + to.assertEmpty(); + + to.dispose(); + + assertEquals("ResourceDispose", sb.toString()); + } + + @Test + public void nonEagerDisposeUpstreamThenDisposeResource() { + final StringBuilder sb = new StringBuilder(); + + TestObserver<Void> to = Completable.using(Functions.justSupplier(1), + new Function<Integer, Completable>() { + @Override + public Completable apply(Integer t) throws Throwable { + return Completable.never() + .doOnDispose(new Action() { + @Override + public void run() throws Throwable { + sb.append("Dispose"); + } + }) + ; + } + }, new Consumer<Integer>() { + @Override + public void accept(Integer t) throws Throwable { + sb.append("Resource"); + } + }, false) + .test() + ; + to.assertEmpty(); + + to.dispose(); + + assertEquals("DisposeResource", sb.toString()); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/AbstractFlowableWithUpstreamTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/AbstractFlowableWithUpstreamTest.java new file mode 100644 index 0000000000..65f593211c --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/AbstractFlowableWithUpstreamTest.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import static org.junit.Assert.assertSame; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.internal.fuseable.HasUpstreamPublisher; + +public class AbstractFlowableWithUpstreamTest extends RxJavaTest { + + @SuppressWarnings("unchecked") + @Test + public void source() { + Flowable<Integer> f = Flowable.just(1); + + assertSame(f, ((HasUpstreamPublisher<Integer>)f.map(Functions.<Integer>identity())).source()); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/BlockingFlowableLatestTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/BlockingFlowableLatestTest.java new file mode 100644 index 0000000000..a842c39c2b --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/BlockingFlowableLatestTest.java @@ -0,0 +1,229 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import static org.junit.Assert.*; + +import java.util.*; +import java.util.concurrent.TimeUnit; + +import org.junit.*; +import org.reactivestreams.Subscriber; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.processors.PublishProcessor; +import io.reactivex.rxjava3.schedulers.TestScheduler; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class BlockingFlowableLatestTest extends RxJavaTest { + @Test + public void simple() { + TestScheduler scheduler = new TestScheduler(); + + Flowable<Long> source = Flowable.interval(1, TimeUnit.SECONDS, scheduler).take(10); + + Iterable<Long> iter = source.blockingLatest(); + + Iterator<Long> it = iter.iterator(); + + // only 9 because take(10) will immediately call onComplete when receiving the 10th item + // which onComplete will overwrite the previous value + for (int i = 0; i < 9; i++) { + scheduler.advanceTimeBy(1, TimeUnit.SECONDS); + + Assert.assertTrue(it.hasNext()); + + Assert.assertEquals(Long.valueOf(i), it.next()); + } + + scheduler.advanceTimeBy(1, TimeUnit.SECONDS); + Assert.assertFalse(it.hasNext()); + } + + @Test + public void sameSourceMultipleIterators() { + TestScheduler scheduler = new TestScheduler(); + + Flowable<Long> source = Flowable.interval(1, TimeUnit.SECONDS, scheduler).take(10); + + Iterable<Long> iter = source.blockingLatest(); + + for (int j = 0; j < 3; j++) { + Iterator<Long> it = iter.iterator(); + + // only 9 because take(10) will immediately call onComplete when receiving the 10th item + // which onComplete will overwrite the previous value + for (int i = 0; i < 9; i++) { + scheduler.advanceTimeBy(1, TimeUnit.SECONDS); + + Assert.assertTrue(it.hasNext()); + + Assert.assertEquals(Long.valueOf(i), it.next()); + } + + scheduler.advanceTimeBy(1, TimeUnit.SECONDS); + Assert.assertFalse(it.hasNext()); + } + } + + @Test(expected = NoSuchElementException.class) + public void empty() { + Flowable<Long> source = Flowable.<Long> empty(); + + Iterable<Long> iter = source.blockingLatest(); + + Iterator<Long> it = iter.iterator(); + + Assert.assertFalse(it.hasNext()); + + it.next(); + } + + @Test(expected = NoSuchElementException.class) + public void simpleJustNext() { + TestScheduler scheduler = new TestScheduler(); + + Flowable<Long> source = Flowable.interval(1, TimeUnit.SECONDS, scheduler).take(10); + + Iterable<Long> iter = source.blockingLatest(); + + Iterator<Long> it = iter.iterator(); + + // only 9 because take(10) will immediately call onComplete when receiving the 10th item + // which onComplete will overwrite the previous value + for (int i = 0; i < 10; i++) { + scheduler.advanceTimeBy(1, TimeUnit.SECONDS); + + Assert.assertEquals(Long.valueOf(i), it.next()); + } + } + + @Test(expected = RuntimeException.class) + public void hasNextThrows() { + TestScheduler scheduler = new TestScheduler(); + + Flowable<Long> source = Flowable.<Long> error(new RuntimeException("Forced failure!")).subscribeOn(scheduler); + + Iterable<Long> iter = source.blockingLatest(); + + Iterator<Long> it = iter.iterator(); + + scheduler.advanceTimeBy(1, TimeUnit.SECONDS); + + it.hasNext(); + } + + @Test(expected = RuntimeException.class) + public void nextThrows() { + TestScheduler scheduler = new TestScheduler(); + + Flowable<Long> source = Flowable.<Long> error(new RuntimeException("Forced failure!")).subscribeOn(scheduler); + + Iterable<Long> iter = source.blockingLatest(); + Iterator<Long> it = iter.iterator(); + + scheduler.advanceTimeBy(1, TimeUnit.SECONDS); + + it.next(); + } + + @Test + public void fasterSource() { + PublishProcessor<Integer> source = PublishProcessor.create(); + Flowable<Integer> blocker = source; + + Iterable<Integer> iter = blocker.blockingLatest(); + Iterator<Integer> it = iter.iterator(); + + source.onNext(1); + + Assert.assertEquals(Integer.valueOf(1), it.next()); + + source.onNext(2); + source.onNext(3); + + Assert.assertEquals(Integer.valueOf(3), it.next()); + + source.onNext(4); + source.onNext(5); + source.onNext(6); + + Assert.assertEquals(Integer.valueOf(6), it.next()); + + source.onNext(7); + source.onComplete(); + + Assert.assertFalse(it.hasNext()); + } + + @Test(expected = UnsupportedOperationException.class) + public void remove() { + Flowable.never().blockingLatest().iterator().remove(); + } + + @Test + public void interrupted() { + Iterator<Object> it = Flowable.never().blockingLatest().iterator(); + + Thread.currentThread().interrupt(); + + try { + it.hasNext(); + } catch (RuntimeException ex) { + assertTrue(ex.toString(), ex.getCause() instanceof InterruptedException); + } + Thread.interrupted(); + } + + @Test(expected = NoSuchElementException.class) + public void empty2() { + Flowable.empty().blockingLatest().iterator().next(); + } + + @Test(expected = TestException.class) + public void error() { + Flowable.error(new TestException()).blockingLatest().iterator().next(); + } + + @Test + public void error2() { + Iterator<Object> it = Flowable.error(new TestException()).blockingLatest().iterator(); + + for (int i = 0; i < 3; i++) { + try { + it.hasNext(); + fail("Should have thrown"); + } catch (TestException ex) { + // expected + } + } + } + + @SuppressWarnings("unchecked") + @Test + public void onError() { + Iterator<Object> it = Flowable.never().blockingLatest().iterator(); + + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + ((Subscriber<Object>)it).onError(new TestException()); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/BlockingFlowableMostRecentTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/BlockingFlowableMostRecentTest.java new file mode 100644 index 0000000000..92513b3072 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/BlockingFlowableMostRecentTest.java @@ -0,0 +1,116 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import static org.junit.Assert.*; + +import java.util.*; +import java.util.concurrent.TimeUnit; + +import org.junit.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.processors.*; +import io.reactivex.rxjava3.schedulers.TestScheduler; + +public class BlockingFlowableMostRecentTest extends RxJavaTest { + + @Test + public void mostRecent() { + FlowableProcessor<String> s = PublishProcessor.create(); + + Iterator<String> it = s.blockingMostRecent("default").iterator(); + + assertTrue(it.hasNext()); + assertEquals("default", it.next()); + assertEquals("default", it.next()); + + s.onNext("one"); + assertTrue(it.hasNext()); + assertEquals("one", it.next()); + assertEquals("one", it.next()); + + s.onNext("two"); + assertTrue(it.hasNext()); + assertEquals("two", it.next()); + assertEquals("two", it.next()); + + s.onComplete(); + assertFalse(it.hasNext()); + + } + + @Test(expected = TestException.class) + public void mostRecentWithException() { + FlowableProcessor<String> s = PublishProcessor.create(); + + Iterator<String> it = s.blockingMostRecent("default").iterator(); + + assertTrue(it.hasNext()); + assertEquals("default", it.next()); + assertEquals("default", it.next()); + + s.onError(new TestException()); + assertTrue(it.hasNext()); + + it.next(); + } + + @Test + public void singleSourceManyIterators() { + TestScheduler scheduler = new TestScheduler(); + Flowable<Long> source = Flowable.interval(1, TimeUnit.SECONDS, scheduler).take(10); + + Iterable<Long> iter = source.blockingMostRecent(-1L); + + for (int j = 0; j < 3; j++) { + Iterator<Long> it = iter.iterator(); + + Assert.assertEquals(Long.valueOf(-1), it.next()); + + for (int i = 0; i < 9; i++) { + scheduler.advanceTimeBy(1, TimeUnit.SECONDS); + + Assert.assertTrue(it.hasNext()); + Assert.assertEquals(Long.valueOf(i), it.next()); + } + scheduler.advanceTimeBy(1, TimeUnit.SECONDS); + + Assert.assertFalse(it.hasNext()); + } + + } + + @Test + public void empty() { + Iterator<Integer> it = Flowable.<Integer>empty() + .blockingMostRecent(1) + .iterator(); + + try { + it.next(); + fail("Should have thrown"); + } catch (NoSuchElementException ex) { + // expected + } + + try { + it.remove(); + fail("Should have thrown"); + } catch (UnsupportedOperationException ex) { + // expected + } + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/BlockingFlowableNextTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/BlockingFlowableNextTest.java new file mode 100644 index 0000000000..718a7ba6a1 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/BlockingFlowableNextTest.java @@ -0,0 +1,393 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import static org.junit.Assert.*; + +import java.util.*; +import java.util.concurrent.*; +import java.util.concurrent.atomic.*; + +import org.junit.*; +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.SerialDisposable; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.internal.operators.flowable.BlockingFlowableNext.NextSubscriber; +import io.reactivex.rxjava3.internal.subscriptions.BooleanSubscription; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.processors.*; +import io.reactivex.rxjava3.schedulers.Schedulers; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class BlockingFlowableNextTest extends RxJavaTest { + + private void fireOnNextInNewThread(final FlowableProcessor<String> o, final String value) { + new Thread() { + @Override + public void run() { + try { + Thread.sleep(500); + } catch (InterruptedException e) { + // ignore + } + o.onNext(value); + } + }.start(); + } + + private void fireOnErrorInNewThread(final FlowableProcessor<String> o) { + new Thread() { + @Override + public void run() { + try { + Thread.sleep(500); + } catch (InterruptedException e) { + // ignore + } + o.onError(new TestException()); + } + }.start(); + } + + @Test + public void next() { + FlowableProcessor<String> obs = PublishProcessor.create(); + Iterator<String> it = obs.blockingNext().iterator(); + fireOnNextInNewThread(obs, "one"); + assertTrue(it.hasNext()); + assertEquals("one", it.next()); + + fireOnNextInNewThread(obs, "two"); + assertTrue(it.hasNext()); + assertEquals("two", it.next()); + + fireOnNextInNewThread(obs, "three"); + try { + assertEquals("three", it.next()); + } catch (NoSuchElementException e) { + fail("Calling next() without hasNext() should wait for next fire"); + } + + obs.onComplete(); + assertFalse(it.hasNext()); + try { + it.next(); + fail("At the end of an iterator should throw a NoSuchElementException"); + } catch (NoSuchElementException e) { + } + + // If the observable is completed, hasNext always returns false and next always throw a NoSuchElementException. + assertFalse(it.hasNext()); + try { + it.next(); + fail("At the end of an iterator should throw a NoSuchElementException"); + } catch (NoSuchElementException e) { + } + } + + @Test + public void nextWithError() { + FlowableProcessor<String> obs = PublishProcessor.create(); + Iterator<String> it = obs.blockingNext().iterator(); + fireOnNextInNewThread(obs, "one"); + assertTrue(it.hasNext()); + assertEquals("one", it.next()); + + fireOnErrorInNewThread(obs); + try { + it.hasNext(); + fail("Expected an TestException"); + } catch (TestException e) { + } + + assertErrorAfterObservableFail(it); + } + + @Test + public void nextWithEmpty() { + Flowable<String> obs = Flowable.<String> empty().observeOn(Schedulers.newThread()); + Iterator<String> it = obs.blockingNext().iterator(); + + assertFalse(it.hasNext()); + try { + it.next(); + fail("At the end of an iterator should throw a NoSuchElementException"); + } catch (NoSuchElementException e) { + } + + // If the observable is completed, hasNext always returns false and next always throw a NoSuchElementException. + assertFalse(it.hasNext()); + try { + it.next(); + fail("At the end of an iterator should throw a NoSuchElementException"); + } catch (NoSuchElementException e) { + } + } + + @Test + public void onError() throws Throwable { + FlowableProcessor<String> obs = PublishProcessor.create(); + Iterator<String> it = obs.blockingNext().iterator(); + + obs.onError(new TestException()); + try { + it.hasNext(); + fail("Expected an TestException"); + } catch (TestException e) { + // successful + } + + assertErrorAfterObservableFail(it); + } + + @Test + public void onErrorInNewThread() { + FlowableProcessor<String> obs = PublishProcessor.create(); + Iterator<String> it = obs.blockingNext().iterator(); + + fireOnErrorInNewThread(obs); + + try { + it.hasNext(); + fail("Expected an TestException"); + } catch (TestException e) { + // successful + } + + assertErrorAfterObservableFail(it); + } + + private void assertErrorAfterObservableFail(Iterator<String> it) { + // After the observable fails, hasNext and next always throw the exception. + try { + it.hasNext(); + fail("hasNext should throw a TestException"); + } catch (TestException e) { + } + try { + it.next(); + fail("next should throw a TestException"); + } catch (TestException e) { + } + } + + @Test + public void nextWithOnlyUsingNextMethod() { + FlowableProcessor<String> obs = PublishProcessor.create(); + Iterator<String> it = obs.blockingNext().iterator(); + fireOnNextInNewThread(obs, "one"); + assertEquals("one", it.next()); + + fireOnNextInNewThread(obs, "two"); + assertEquals("two", it.next()); + + obs.onComplete(); + try { + it.next(); + fail("At the end of an iterator should throw a NoSuchElementException"); + } catch (NoSuchElementException e) { + } + } + + @Test + public void nextWithCallingHasNextMultipleTimes() { + FlowableProcessor<String> obs = PublishProcessor.create(); + Iterator<String> it = obs.blockingNext().iterator(); + fireOnNextInNewThread(obs, "one"); + assertTrue(it.hasNext()); + assertTrue(it.hasNext()); + assertTrue(it.hasNext()); + assertTrue(it.hasNext()); + assertEquals("one", it.next()); + + obs.onComplete(); + try { + it.next(); + fail("At the end of an iterator should throw a NoSuchElementException"); + } catch (NoSuchElementException e) { + } + } + + /** + * Confirm that no buffering or blocking of the Observable onNext calls occurs and it just grabs the next emitted value. + * <p> + * This results in output such as {@code => a: 1 b: 2 c: 89} + * + * @throws Throwable some method call is declared throws + */ + @Test + public void noBufferingOrBlockingOfSequence() throws Throwable { + int repeat = 0; + for (;;) { + final SerialDisposable task = new SerialDisposable(); + try { + final CountDownLatch finished = new CountDownLatch(1); + final int COUNT = 30; + final CountDownLatch timeHasPassed = new CountDownLatch(COUNT); + final AtomicBoolean running = new AtomicBoolean(true); + final AtomicInteger count = new AtomicInteger(0); + final Flowable<Integer> obs = Flowable.unsafeCreate(new Publisher<Integer>() { + + @Override + public void subscribe(final Subscriber<? super Integer> subscriber) { + subscriber.onSubscribe(new BooleanSubscription()); + task.replace(Schedulers.single().scheduleDirect(new Runnable() { + + @Override + public void run() { + try { + while (running.get() && !task.isDisposed()) { + subscriber.onNext(count.incrementAndGet()); + timeHasPassed.countDown(); + } + subscriber.onComplete(); + } catch (Throwable e) { + subscriber.onError(e); + } finally { + finished.countDown(); + } + } + })); + } + + }); + + Iterator<Integer> it = obs.blockingNext().iterator(); + + assertTrue(it.hasNext()); + int a = it.next(); + assertTrue(it.hasNext()); + int b = it.next(); + // we should have a different value + assertTrue("a and b should be different", a != b); + + // wait for some time (if times out we are blocked somewhere so fail ... set very high for very slow, constrained machines) + timeHasPassed.await(8000, TimeUnit.MILLISECONDS); + + assertTrue(it.hasNext()); + int c = it.next(); + + assertTrue("c should not just be the next in sequence", c != (b + 1)); + assertTrue("expected that c [" + c + "] is higher than or equal to " + COUNT, c >= COUNT); + + assertTrue(it.hasNext()); + int d = it.next(); + assertTrue(d > c); + + // shut down the thread + running.set(false); + + finished.await(); + + assertFalse(it.hasNext()); + + System.out.println("a: " + a + " b: " + b + " c: " + c); + break; + } catch (AssertionError ex) { + if (++repeat == 3) { + throw ex; + } + Thread.sleep((int)(1000 * Math.pow(2, repeat - 1))); + } finally { + task.dispose(); + } + } + } + + @Test + public void singleSourceManyIterators() throws InterruptedException { + Flowable<Long> f = Flowable.interval(250, TimeUnit.MILLISECONDS); + PublishProcessor<Integer> terminal = PublishProcessor.create(); + Flowable<Long> source = f.takeUntil(terminal); + + Iterable<Long> iter = source.blockingNext(); + + for (int j = 0; j < 3; j++) { + BlockingFlowableNext.NextIterator<Long> it = (BlockingFlowableNext.NextIterator<Long>)iter.iterator(); + + for (long i = 0; i < 10; i++) { + Assert.assertTrue(it.hasNext()); + Assert.assertEquals(j + "th iteration next", Long.valueOf(i), it.next()); + } + terminal.onNext(1); + } + } + + @Test + public void synchronousNext() { + assertEquals(1, BehaviorProcessor.createDefault(1).take(1).blockingSingle().intValue()); + assertEquals(2, BehaviorProcessor.createDefault(2).blockingIterable().iterator().next().intValue()); + assertEquals(3, BehaviorProcessor.createDefault(3).blockingNext().iterator().next().intValue()); + } + + @Test(expected = UnsupportedOperationException.class) + public void remove() { + Flowable.never().blockingNext().iterator().remove(); + } + + @Test + public void interrupt() { + Iterator<Object> it = Flowable.never().blockingNext().iterator(); + + try { + Thread.currentThread().interrupt(); + it.next(); + } catch (RuntimeException ex) { + assertTrue(ex.toString(), ex.getCause() instanceof InterruptedException); + } + } + + @Test + public void nextObserverError() { + NextSubscriber<Integer> no = new NextSubscriber<>(); + + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + no.onError(new TestException()); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void nextObserverOnNext() throws Exception { + NextSubscriber<Integer> no = new NextSubscriber<>(); + + no.setWaiting(); + no.onNext(Notification.createOnNext(1)); + + no.setWaiting(); + no.onNext(Notification.createOnNext(1)); + + assertEquals(1, no.takeNext().getValue().intValue()); + } + + @Test + public void nextObserverOnCompleteOnNext() throws Exception { + NextSubscriber<Integer> no = new NextSubscriber<>(); + + no.setWaiting(); + no.onNext(Notification.<Integer>createOnComplete()); + + no.setWaiting(); + no.onNext(Notification.createOnNext(1)); + + assertTrue(no.takeNext().isOnComplete()); + } + +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/BlockingFlowableToFutureTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/BlockingFlowableToFutureTest.java new file mode 100644 index 0000000000..6da74eea03 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/BlockingFlowableToFutureTest.java @@ -0,0 +1,110 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import static org.junit.Assert.*; + +import java.util.*; +import java.util.concurrent.*; + +import org.junit.Test; +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.internal.subscriptions.BooleanSubscription; + +public class BlockingFlowableToFutureTest { + + @Test + public void toFuture() throws InterruptedException, ExecutionException { + Flowable<String> obs = Flowable.just("one"); + Future<String> f = obs.toFuture(); + assertEquals("one", f.get()); + } + + @Test + public void toFutureList() throws InterruptedException, ExecutionException { + Flowable<String> obs = Flowable.just("one", "two", "three"); + Future<List<String>> f = obs.toList().toFuture(); + assertEquals("one", f.get().get(0)); + assertEquals("two", f.get().get(1)); + assertEquals("three", f.get().get(2)); + } + + @Test(expected = IndexOutOfBoundsException.class) + public void exceptionWithMoreThanOneElement() throws Throwable { + Flowable<String> obs = Flowable.just("one", "two"); + Future<String> f = obs.toFuture(); + try { + // we expect an exception since there are more than 1 element + f.get(); + fail("Should have thrown!"); + } + catch (ExecutionException e) { + throw e.getCause(); + } + } + + @Test + public void toFutureWithException() { + Flowable<String> obs = Flowable.unsafeCreate(new Publisher<String>() { + + @Override + public void subscribe(Subscriber<? super String> subscriber) { + subscriber.onSubscribe(new BooleanSubscription()); + subscriber.onNext("one"); + subscriber.onError(new TestException()); + } + }); + + Future<String> f = obs.toFuture(); + try { + f.get(); + fail("expected exception"); + } catch (Throwable e) { + assertEquals(TestException.class, e.getCause().getClass()); + } + } + + @Test(expected = CancellationException.class) + public void getAfterCancel() throws Exception { + Flowable<String> obs = Flowable.never(); + Future<String> f = obs.toFuture(); + boolean cancelled = f.cancel(true); + assertTrue(cancelled); // because OperationNeverComplete never does + f.get(); // Future.get() docs require this to throw + } + + @Test(expected = CancellationException.class) + public void getWithTimeoutAfterCancel() throws Exception { + Flowable<String> obs = Flowable.never(); + Future<String> f = obs.toFuture(); + boolean cancelled = f.cancel(true); + assertTrue(cancelled); // because OperationNeverComplete never does + f.get(Long.MAX_VALUE, TimeUnit.NANOSECONDS); // Future.get() docs require this to throw + } + + @Test(expected = NoSuchElementException.class) + public void getWithEmptyFlowable() throws Throwable { + Flowable<String> obs = Flowable.empty(); + Future<String> f = obs.toFuture(); + try { + f.get(); + } + catch (ExecutionException e) { + throw e.getCause(); + } + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/BlockingFlowableToIteratorTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/BlockingFlowableToIteratorTest.java new file mode 100644 index 0000000000..e5ad7806d3 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/BlockingFlowableToIteratorTest.java @@ -0,0 +1,194 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import static org.junit.Assert.*; + +import java.util.*; +import java.util.concurrent.TimeUnit; + +import org.junit.Test; +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.internal.operators.flowable.BlockingFlowableIterable.BlockingFlowableIterator; +import io.reactivex.rxjava3.internal.subscriptions.BooleanSubscription; +import io.reactivex.rxjava3.processors.PublishProcessor; +import io.reactivex.rxjava3.schedulers.Schedulers; + +public class BlockingFlowableToIteratorTest extends RxJavaTest { + + @Test + public void toIterator() { + Flowable<String> obs = Flowable.just("one", "two", "three"); + + Iterator<String> it = obs.blockingIterable().iterator(); + + assertTrue(it.hasNext()); + assertEquals("one", it.next()); + + assertTrue(it.hasNext()); + assertEquals("two", it.next()); + + assertTrue(it.hasNext()); + assertEquals("three", it.next()); + + assertFalse(it.hasNext()); + + } + + @Test(expected = TestException.class) + public void toIteratorWithException() { + Flowable<String> obs = Flowable.unsafeCreate(new Publisher<String>() { + + @Override + public void subscribe(Subscriber<? super String> subscriber) { + subscriber.onSubscribe(new BooleanSubscription()); + subscriber.onNext("one"); + subscriber.onError(new TestException()); + } + }); + + Iterator<String> it = obs.blockingIterable().iterator(); + + assertTrue(it.hasNext()); + assertEquals("one", it.next()); + + assertTrue(it.hasNext()); + it.next(); + } + + @Test + public void iteratorExertBackpressure() { + final Counter src = new Counter(); + + Flowable<Integer> obs = Flowable.fromIterable(new Iterable<Integer>() { + @Override + public Iterator<Integer> iterator() { + return src; + } + }); + + Iterator<Integer> it = obs.blockingIterable().iterator(); + while (it.hasNext()) { + // Correct backpressure should cause this interleaved behavior. + // We first request RxRingBuffer.SIZE. Then in increments of + // SubscriberIterator.LIMIT. + int i = it.next(); + int expected = i - (i % (Flowable.bufferSize() - (Flowable.bufferSize() >> 2))) + Flowable.bufferSize(); + expected = Math.min(expected, Counter.MAX); + + assertEquals(expected, src.count); + } + } + + public static final class Counter implements Iterator<Integer> { + static final int MAX = 5 * Flowable.bufferSize(); + public int count; + + @Override + public boolean hasNext() { + return count < MAX; + } + + @Override + public Integer next() { + return ++count; + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + } + + @Test(expected = UnsupportedOperationException.class) + public void remove() { + BlockingFlowableIterator<Integer> it = new BlockingFlowableIterator<>(128); + it.remove(); + } + + @Test + public void dispose() { + BlockingFlowableIterator<Integer> it = new BlockingFlowableIterator<>(128); + + assertFalse(it.isDisposed()); + + it.dispose(); + + assertTrue(it.isDisposed()); + } + + @Test + public void interruptWait() { + BlockingFlowableIterator<Integer> it = new BlockingFlowableIterator<>(128); + + try { + Thread.currentThread().interrupt(); + + it.hasNext(); + } catch (RuntimeException ex) { + assertTrue(ex.toString(), ex.getCause() instanceof InterruptedException); + } + } + + @Test(expected = NoSuchElementException.class) + public void emptyThrowsNoSuch() { + BlockingFlowableIterator<Integer> it = new BlockingFlowableIterator<>(128); + it.onComplete(); + it.next(); + } + + @Test(expected = QueueOverflowException.class) + public void overflowQueue() { + Iterator<Integer> it = new Flowable<Integer>() { + @Override + protected void subscribeActual(Subscriber<? super Integer> s) { + s.onSubscribe(new BooleanSubscription()); + s.onNext(1); + s.onNext(2); + } + } + .blockingIterable(1) + .iterator(); + + it.next(); + } + + @Test(expected = NoSuchElementException.class) + public void disposedIteratorHasNextReturns() { + Iterator<Integer> it = PublishProcessor.<Integer>create() + .blockingIterable().iterator(); + ((Disposable)it).dispose(); + assertFalse(it.hasNext()); + it.next(); + } + + @Test + public void asyncDisposeUnblocks() { + final Iterator<Integer> it = PublishProcessor.<Integer>create() + .blockingIterable().iterator(); + + Schedulers.single().scheduleDirect(new Runnable() { + @Override + public void run() { + ((Disposable)it).dispose(); + } + }, 1, TimeUnit.SECONDS); + + assertFalse(it.hasNext()); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/BufferUntilSubscriberTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/BufferUntilSubscriberTest.java new file mode 100644 index 0000000000..6cdc17d210 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/BufferUntilSubscriberTest.java @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import java.util.List; +import java.util.concurrent.*; +import java.util.concurrent.atomic.*; + +import org.junit.*; +import org.reactivestreams.Publisher; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.processors.PublishProcessor; +import io.reactivex.rxjava3.schedulers.Schedulers; + +public class BufferUntilSubscriberTest extends RxJavaTest { + + @Test + public void issue1677() throws InterruptedException { + final AtomicLong counter = new AtomicLong(); + final Integer[] numbers = new Integer[5000]; + for (int i = 0; i < numbers.length; i++) { + numbers[i] = i + 1; + } + final int NITERS = 250; + final CountDownLatch latch = new CountDownLatch(NITERS); + for (int iters = 0; iters < NITERS; iters++) { + final CountDownLatch innerLatch = new CountDownLatch(1); + final PublishProcessor<Void> s = PublishProcessor.create(); + final AtomicBoolean completed = new AtomicBoolean(); + Flowable.fromArray(numbers) + .takeUntil(s) + .window(50) + .flatMap(new Function<Flowable<Integer>, Publisher<Object>>() { + @Override + public Publisher<Object> apply(Flowable<Integer> integerObservable) { + return integerObservable + .subscribeOn(Schedulers.computation()) + .map(new Function<Integer, Object>() { + @Override + public Object apply(Integer integer) { + if (integer >= 5 && completed.compareAndSet(false, true)) { + s.onComplete(); + } + // do some work + Math.pow(Math.random(), Math.random()); + return integer * 2; + } + }); + } + }) + .toList() + .doOnSuccess(new Consumer<List<Object>>() { + @Override + public void accept(List<Object> integers) { + counter.incrementAndGet(); + latch.countDown(); + innerLatch.countDown(); + } + }) + .subscribe(); + if (!innerLatch.await(30, TimeUnit.SECONDS)) { + Assert.fail("Failed inner latch wait, iteration " + iters); + } + } + if (!latch.await(30, TimeUnit.SECONDS)) { + Assert.fail("Incomplete! Went through " + latch.getCount() + " iterations"); + } else { + Assert.assertEquals(NITERS, counter.get()); + } + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableAllTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableAllTest.java new file mode 100644 index 0000000000..2b66a48bd0 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableAllTest.java @@ -0,0 +1,468 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import java.util.List; +import java.util.concurrent.TimeUnit; + +import org.junit.Test; +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.internal.subscriptions.BooleanSubscription; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.subscribers.TestSubscriber; +import io.reactivex.rxjava3.testsupport.*; + +public class FlowableAllTest extends RxJavaTest { + + @Test + public void all() { + Flowable<String> obs = Flowable.just("one", "two", "six"); + + SingleObserver<Boolean> observer = TestHelper.mockSingleObserver(); + + obs.all(new Predicate<String>() { + @Override + public boolean test(String s) { + return s.length() == 3; + } + }) + .subscribe(observer); + + verify(observer).onSubscribe((Disposable)any()); + verify(observer).onSuccess(true); + verifyNoMoreInteractions(observer); + } + + @Test + public void notAll() { + Flowable<String> obs = Flowable.just("one", "two", "three", "six"); + + SingleObserver<Boolean> observer = TestHelper.mockSingleObserver(); + + obs.all(new Predicate<String>() { + @Override + public boolean test(String s) { + return s.length() == 3; + } + }) + .subscribe(observer); + + verify(observer).onSubscribe((Disposable)any()); + verify(observer).onSuccess(false); + verifyNoMoreInteractions(observer); + } + + @Test + public void empty() { + Flowable<String> obs = Flowable.empty(); + + SingleObserver<Boolean> observer = TestHelper.mockSingleObserver(); + + obs.all(new Predicate<String>() { + @Override + public boolean test(String s) { + return s.length() == 3; + } + }) + .subscribe(observer); + + verify(observer).onSubscribe((Disposable)any()); + verify(observer).onSuccess(true); + verifyNoMoreInteractions(observer); + } + + @Test + public void error() { + Throwable error = new Throwable(); + Flowable<String> obs = Flowable.error(error); + + SingleObserver<Boolean> observer = TestHelper.mockSingleObserver(); + + obs.all(new Predicate<String>() { + @Override + public boolean test(String s) { + return s.length() == 3; + } + }) + .subscribe(observer); + + verify(observer).onSubscribe((Disposable)any()); + verify(observer).onError(error); + verifyNoMoreInteractions(observer); + } + + @Test + public void followingFirst() { + Flowable<Integer> f = Flowable.fromArray(1, 3, 5, 6); + Single<Boolean> allOdd = f.all(new Predicate<Integer>() { + @Override + public boolean test(Integer i) { + return i % 2 == 1; + } + }); + + assertFalse(allOdd.blockingGet()); + } + + @Test + public void issue1935NoUnsubscribeDownstream() { + Flowable<Integer> source = Flowable.just(1) + .all(new Predicate<Integer>() { + @Override + public boolean test(Integer t1) { + return false; + } + }) + .flatMapPublisher(new Function<Boolean, Publisher<Integer>>() { + @Override + public Publisher<Integer> apply(Boolean t1) { + return Flowable.just(2).delay(500, TimeUnit.MILLISECONDS); + } + }); + + assertEquals((Object)2, source.blockingFirst()); + } + + @Test + public void backpressureIfOneRequestedOneShouldBeDelivered() { + TestObserverEx<Boolean> to = new TestObserverEx<>(); + + Flowable.empty().all(new Predicate<Object>() { + @Override + public boolean test(Object t) { + return false; + } + }).subscribe(to); + + to.assertTerminated(); + to.assertNoErrors(); + to.assertComplete(); + + to.assertValue(true); + } + + @Test + public void predicateThrowsExceptionAndValueInCauseMessage() { + TestObserverEx<Boolean> to = new TestObserverEx<>(); + + final IllegalArgumentException ex = new IllegalArgumentException(); + + Flowable.just("Boo!").all(new Predicate<String>() { + @Override + public boolean test(String v) { + throw ex; + } + }) + .subscribe(to); + + to.assertTerminated(); + to.assertNoValues(); + to.assertNotComplete(); + to.assertError(ex); + // FIXME need to decide about adding the value that probably caused the crash in some way +// assertTrue(ex.getCause().getMessage().contains("Boo!")); + } + + @Test + public void allFlowable() { + Flowable<String> obs = Flowable.just("one", "two", "six"); + + Subscriber<Boolean> subscriber = TestHelper.mockSubscriber(); + + obs.all(new Predicate<String>() { + @Override + public boolean test(String s) { + return s.length() == 3; + } + }) + .toFlowable() + .subscribe(subscriber); + + verify(subscriber).onSubscribe((Subscription)any()); + verify(subscriber).onNext(true); + verify(subscriber).onComplete(); + verifyNoMoreInteractions(subscriber); + } + + @Test + public void notAllFlowable() { + Flowable<String> obs = Flowable.just("one", "two", "three", "six"); + + Subscriber <Boolean> subscriber = TestHelper.mockSubscriber(); + + obs.all(new Predicate<String>() { + @Override + public boolean test(String s) { + return s.length() == 3; + } + }) + .toFlowable() + .subscribe(subscriber); + + verify(subscriber).onSubscribe((Subscription)any()); + verify(subscriber).onNext(false); + verify(subscriber).onComplete(); + verifyNoMoreInteractions(subscriber); + } + + @Test + public void emptyFlowable() { + Flowable<String> obs = Flowable.empty(); + + Subscriber <Boolean> subscriber = TestHelper.mockSubscriber(); + + obs.all(new Predicate<String>() { + @Override + public boolean test(String s) { + return s.length() == 3; + } + }) + .toFlowable() + .subscribe(subscriber); + + verify(subscriber).onSubscribe((Subscription)any()); + verify(subscriber).onNext(true); + verify(subscriber).onComplete(); + verifyNoMoreInteractions(subscriber); + } + + @Test + public void errorFlowable() { + Throwable error = new Throwable(); + Flowable<String> obs = Flowable.error(error); + + Subscriber <Boolean> subscriber = TestHelper.mockSubscriber(); + + obs.all(new Predicate<String>() { + @Override + public boolean test(String s) { + return s.length() == 3; + } + }) + .toFlowable() + .subscribe(subscriber); + + verify(subscriber).onSubscribe((Subscription)any()); + verify(subscriber).onError(error); + verifyNoMoreInteractions(subscriber); + } + + @Test + public void followingFirstFlowable() { + Flowable<Integer> f = Flowable.fromArray(1, 3, 5, 6); + Flowable<Boolean> allOdd = f.all(new Predicate<Integer>() { + @Override + public boolean test(Integer i) { + return i % 2 == 1; + } + }) + .toFlowable() + ; + + assertFalse(allOdd.blockingFirst()); + } + + @Test + public void issue1935NoUnsubscribeDownstreamFlowable() { + Flowable<Integer> source = Flowable.just(1) + .all(new Predicate<Integer>() { + @Override + public boolean test(Integer t1) { + return false; + } + }) + .toFlowable() + .flatMap(new Function<Boolean, Publisher<Integer>>() { + @Override + public Publisher<Integer> apply(Boolean t1) { + return Flowable.just(2).delay(500, TimeUnit.MILLISECONDS); + } + }) + ; + + assertEquals((Object)2, source.blockingFirst()); + } + + @Test + public void backpressureIfNoneRequestedNoneShouldBeDeliveredFlowable() { + TestSubscriber<Boolean> ts = new TestSubscriber<>(0L); + Flowable.empty().all(new Predicate<Object>() { + @Override + public boolean test(Object t1) { + return false; + } + }) + .toFlowable() + .subscribe(ts); + + ts.assertNoValues(); + ts.assertNoErrors(); + ts.assertNotComplete(); + } + + @Test + public void backpressureIfOneRequestedOneShouldBeDeliveredFlowable() { + TestSubscriberEx<Boolean> ts = new TestSubscriberEx<>(1L); + + Flowable.empty().all(new Predicate<Object>() { + @Override + public boolean test(Object t) { + return false; + } + }) + .toFlowable() + .subscribe(ts); + + ts.assertTerminated(); + ts.assertNoErrors(); + ts.assertComplete(); + + ts.assertValue(true); + } + + @Test + public void predicateThrowsExceptionAndValueInCauseMessageFlowable() { + TestSubscriberEx<Boolean> ts = new TestSubscriberEx<>(); + + final IllegalArgumentException ex = new IllegalArgumentException(); + + Flowable.just("Boo!").all(new Predicate<String>() { + @Override + public boolean test(String v) { + throw ex; + } + }) + .toFlowable() + .subscribe(ts); + + ts.assertTerminated(); + ts.assertNoValues(); + ts.assertNotComplete(); + ts.assertError(ex); + // FIXME need to decide about adding the value that probably caused the crash in some way +// assertTrue(ex.getCause().getMessage().contains("Boo!")); + } + + @Test + public void dispose() { + TestHelper.checkDisposed(Flowable.just(1).all(Functions.alwaysTrue()).toFlowable()); + + TestHelper.checkDisposed(Flowable.just(1).all(Functions.alwaysTrue())); + } + + @Test + public void predicateThrows() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + new Flowable<Integer>() { + @Override + protected void subscribeActual(Subscriber<? super Integer> subscriber) { + subscriber.onSubscribe(new BooleanSubscription()); + + subscriber.onNext(1); + subscriber.onNext(2); + subscriber.onError(new TestException()); + subscriber.onComplete(); + } + } + .all(new Predicate<Integer>() { + @Override + public boolean test(Integer v) throws Exception { + throw new TestException(); + } + }) + .toFlowable() + .test() + .assertFailure(TestException.class); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void predicateThrowsObservable() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + new Flowable<Integer>() { + @Override + protected void subscribeActual(Subscriber<? super Integer> subscriber) { + subscriber.onSubscribe(new BooleanSubscription()); + + subscriber.onNext(1); + subscriber.onNext(2); + subscriber.onError(new TestException()); + subscriber.onComplete(); + } + } + .all(new Predicate<Integer>() { + @Override + public boolean test(Integer v) throws Exception { + throw new TestException(); + } + }) + .toFlowable() + .test() + .assertFailure(TestException.class); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void badSource() { + TestHelper.checkBadSourceFlowable(new Function<Flowable<Integer>, Object>() { + @Override + public Object apply(Flowable<Integer> f) throws Exception { + return f.all(Functions.alwaysTrue()); + } + }, false, 1, 1, true); + + TestHelper.checkBadSourceFlowable(new Function<Flowable<Integer>, Object>() { + @Override + public Object apply(Flowable<Integer> f) throws Exception { + return f.all(Functions.alwaysTrue()).toFlowable(); + } + }, false, 1, 1, true); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeFlowable(new Function<Flowable<Object>, Publisher<Boolean>>() { + @Override + public Publisher<Boolean> apply(Flowable<Object> f) throws Exception { + return f.all(Functions.alwaysTrue()).toFlowable(); + } + }); + TestHelper.checkDoubleOnSubscribeFlowableToSingle(new Function<Flowable<Object>, Single<Boolean>>() { + @Override + public Single<Boolean> apply(Flowable<Object> f) throws Exception { + return f.all(Functions.alwaysTrue()); + } + }); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableAmbTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableAmbTest.java new file mode 100644 index 0000000000..ba86c69719 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableAmbTest.java @@ -0,0 +1,694 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +import java.io.IOException; +import java.util.*; +import java.util.concurrent.*; +import java.util.concurrent.atomic.*; + +import org.junit.*; +import org.mockito.InOrder; +import org.reactivestreams.*; + +import io.reactivex.rxjava3.annotations.NonNull; +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.CompositeDisposable; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.internal.util.CrashingMappedIterable; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.processors.PublishProcessor; +import io.reactivex.rxjava3.schedulers.*; +import io.reactivex.rxjava3.subscribers.TestSubscriber; +import io.reactivex.rxjava3.testsupport.*; + +public class FlowableAmbTest extends RxJavaTest { + + private TestScheduler scheduler; + private Scheduler.Worker innerScheduler; + + @Before + public void setUp() { + scheduler = new TestScheduler(); + innerScheduler = scheduler.createWorker(); + } + + private Flowable<String> createFlowable(final String[] values, + final long interval, final Throwable e) { + return Flowable.unsafeCreate(new Publisher<String>() { + + @Override + public void subscribe(final Subscriber<? super String> subscriber) { + final CompositeDisposable parentSubscription = new CompositeDisposable(); + + subscriber.onSubscribe(new Subscription() { + @Override + public void request(long n) { + + } + + @Override + public void cancel() { + parentSubscription.dispose(); + } + }); + + long delay = interval; + for (final String value : values) { + parentSubscription.add(innerScheduler.schedule(new Runnable() { + @Override + public void run() { + subscriber.onNext(value); + } + } + , delay, TimeUnit.MILLISECONDS)); + delay += interval; + } + parentSubscription.add(innerScheduler.schedule(new Runnable() { + @Override + public void run() { + if (e == null) { + subscriber.onComplete(); + } else { + subscriber.onError(e); + } + } + }, delay, TimeUnit.MILLISECONDS)); + } + }); + } + + @Test + public void amb() { + Flowable<String> flowable1 = createFlowable(new String[] { + "1", "11", "111", "1111" }, 2000, null); + Flowable<String> flowable2 = createFlowable(new String[] { + "2", "22", "222", "2222" }, 1000, null); + Flowable<String> flowable3 = createFlowable(new String[] { + "3", "33", "333", "3333" }, 3000, null); + + Flowable<String> f = Flowable.ambArray(flowable1, + flowable2, flowable3); + + Subscriber<String> subscriber = TestHelper.mockSubscriber(); + f.subscribe(subscriber); + + scheduler.advanceTimeBy(100000, TimeUnit.MILLISECONDS); + + InOrder inOrder = inOrder(subscriber); + inOrder.verify(subscriber, times(1)).onNext("2"); + inOrder.verify(subscriber, times(1)).onNext("22"); + inOrder.verify(subscriber, times(1)).onNext("222"); + inOrder.verify(subscriber, times(1)).onNext("2222"); + inOrder.verify(subscriber, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void amb2() { + IOException expectedException = new IOException( + "fake exception"); + Flowable<String> flowable1 = createFlowable(new String[] {}, + 2000, new IOException("fake exception")); + Flowable<String> flowable2 = createFlowable(new String[] { + "2", "22", "222", "2222" }, 1000, expectedException); + Flowable<String> flowable3 = createFlowable(new String[] {}, + 3000, new IOException("fake exception")); + + Flowable<String> f = Flowable.ambArray(flowable1, + flowable2, flowable3); + + Subscriber<String> subscriber = TestHelper.mockSubscriber(); + f.subscribe(subscriber); + + scheduler.advanceTimeBy(100000, TimeUnit.MILLISECONDS); + + InOrder inOrder = inOrder(subscriber); + inOrder.verify(subscriber, times(1)).onNext("2"); + inOrder.verify(subscriber, times(1)).onNext("22"); + inOrder.verify(subscriber, times(1)).onNext("222"); + inOrder.verify(subscriber, times(1)).onNext("2222"); + inOrder.verify(subscriber, times(1)).onError(expectedException); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void amb3() { + Flowable<String> flowable1 = createFlowable(new String[] { + "1" }, 2000, null); + Flowable<String> flowable2 = createFlowable(new String[] {}, + 1000, null); + Flowable<String> flowable3 = createFlowable(new String[] { + "3" }, 3000, null); + + Flowable<String> f = Flowable.ambArray(flowable1, + flowable2, flowable3); + + Subscriber<String> subscriber = TestHelper.mockSubscriber(); + f.subscribe(subscriber); + + scheduler.advanceTimeBy(100000, TimeUnit.MILLISECONDS); + InOrder inOrder = inOrder(subscriber); + inOrder.verify(subscriber, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void producerRequestThroughAmb() { + TestSubscriber<Integer> ts = new TestSubscriber<>(0L); + ts.request(3); + final AtomicLong requested1 = new AtomicLong(); + final AtomicLong requested2 = new AtomicLong(); + Flowable<Integer> f1 = Flowable.unsafeCreate(new Publisher<Integer>() { + + @Override + public void subscribe(Subscriber<? super Integer> s) { + s.onSubscribe(new Subscription() { + + @Override + public void request(long n) { + System.out.println("1-requested: " + n); + requested1.set(n); + } + + @Override + public void cancel() { + + } + }); + } + + }); + Flowable<Integer> f2 = Flowable.unsafeCreate(new Publisher<Integer>() { + + @Override + public void subscribe(Subscriber<? super Integer> s) { + s.onSubscribe(new Subscription() { + + @Override + public void request(long n) { + System.out.println("2-requested: " + n); + requested2.set(n); + } + + @Override + public void cancel() { + + } + }); + } + + }); + Flowable.ambArray(f1, f2).subscribe(ts); + assertEquals(3, requested1.get()); + assertEquals(3, requested2.get()); + } + + @Test + public void backpressure() { + TestSubscriber<Integer> ts = new TestSubscriber<>(); + Flowable.range(0, Flowable.bufferSize() * 2) + .ambWith(Flowable.range(0, Flowable.bufferSize() * 2)) + .observeOn(Schedulers.computation()) // observeOn has a backpressured RxRingBuffer + .delay(1, TimeUnit.MICROSECONDS) // make it a slightly slow consumer + .subscribe(ts); + + ts.awaitDone(5, TimeUnit.SECONDS); + ts.assertNoErrors(); + assertEquals(Flowable.bufferSize() * 2, ts.values().size()); + } + + @Test + public void subscriptionOnlyHappensOnce() throws InterruptedException { + final AtomicLong count = new AtomicLong(); + Consumer<Subscription> incrementer = new Consumer<Subscription>() { + @Override + public void accept(Subscription s) { + count.incrementAndGet(); + } + }; + + //this aync stream should emit first + Flowable<Integer> f1 = Flowable.just(1).doOnSubscribe(incrementer) + .delay(100, TimeUnit.MILLISECONDS).subscribeOn(Schedulers.computation()); + //this stream emits second + Flowable<Integer> f2 = Flowable.just(1).doOnSubscribe(incrementer) + .delay(100, TimeUnit.MILLISECONDS).subscribeOn(Schedulers.computation()); + TestSubscriber<Integer> ts = new TestSubscriber<>(); + Flowable.ambArray(f1, f2).subscribe(ts); + ts.request(1); + ts.awaitDone(5, TimeUnit.SECONDS); + ts.assertNoErrors(); + assertEquals(2, count.get()); + } + + @Test + public void secondaryRequestsPropagatedToChildren() throws InterruptedException { + //this aync stream should emit first + Flowable<Integer> f1 = Flowable.fromArray(1, 2, 3) + .delay(100, TimeUnit.MILLISECONDS).subscribeOn(Schedulers.computation()); + //this stream emits second + Flowable<Integer> f2 = Flowable.fromArray(4, 5, 6) + .delay(200, TimeUnit.MILLISECONDS).subscribeOn(Schedulers.computation()); + TestSubscriber<Integer> ts = new TestSubscriber<>(1L); + + Flowable.ambArray(f1, f2).subscribe(ts); + // before first emission request 20 more + // this request should suffice to emit all + ts.request(20); + //ensure stream does not hang + ts.awaitDone(5, TimeUnit.SECONDS); + ts.assertNoErrors(); + } + + @Test + public void synchronousSources() { + // under async subscription the second Flowable would complete before + // the first but because this is a synchronous subscription to sources + // then second Flowable does not get subscribed to before first + // subscription completes hence first Flowable emits result through + // amb + int result = Flowable.just(1).doOnNext(new Consumer<Integer>() { + @Override + public void accept(Integer t) { + try { + Thread.sleep(100); + } catch (InterruptedException e) { + // + } + } + }).ambWith(Flowable.just(2)).blockingSingle(); + assertEquals(1, result); + } + + @Test + public void ambCancelsOthers() { + PublishProcessor<Integer> source1 = PublishProcessor.create(); + PublishProcessor<Integer> source2 = PublishProcessor.create(); + PublishProcessor<Integer> source3 = PublishProcessor.create(); + + TestSubscriber<Integer> ts = new TestSubscriber<>(); + + Flowable.ambArray(source1, source2, source3).subscribe(ts); + + assertTrue("Source 1 doesn't have subscribers!", source1.hasSubscribers()); + assertTrue("Source 2 doesn't have subscribers!", source2.hasSubscribers()); + assertTrue("Source 3 doesn't have subscribers!", source3.hasSubscribers()); + + source1.onNext(1); + + assertTrue("Source 1 doesn't have subscribers!", source1.hasSubscribers()); + assertFalse("Source 2 still has subscribers!", source2.hasSubscribers()); + assertFalse("Source 2 still has subscribers!", source3.hasSubscribers()); + + } + + @Test + public void multipleUse() { + TestSubscriber<Long> ts1 = new TestSubscriber<>(); + TestSubscriber<Long> ts2 = new TestSubscriber<>(); + + Flowable<Long> amb = Flowable.timer(100, TimeUnit.MILLISECONDS).ambWith(Flowable.timer(200, TimeUnit.MILLISECONDS)); + + amb.subscribe(ts1); + amb.subscribe(ts2); + + ts1.awaitDone(5, TimeUnit.SECONDS); + ts2.awaitDone(5, TimeUnit.SECONDS); + + ts1.assertValue(0L); + ts1.assertComplete(); + ts1.assertNoErrors(); + + ts2.assertValue(0L); + ts2.assertComplete(); + ts2.assertNoErrors(); + } + + @Test + public void ambIterable() { + PublishProcessor<Integer> pp1 = PublishProcessor.create(); + PublishProcessor<Integer> pp2 = PublishProcessor.create(); + + TestSubscriber<Integer> ts = TestSubscriber.create(); + + Flowable.amb(Arrays.asList(pp1, pp2)).subscribe(ts); + + ts.assertNoValues(); + + pp1.onNext(1); + pp1.onComplete(); + + assertFalse(pp1.hasSubscribers()); + assertFalse(pp2.hasSubscribers()); + + ts.assertValue(1); + ts.assertNoErrors(); + ts.assertComplete(); + } + + @Test + public void ambIterable2() { + PublishProcessor<Integer> pp1 = PublishProcessor.create(); + PublishProcessor<Integer> pp2 = PublishProcessor.create(); + + TestSubscriber<Integer> ts = TestSubscriber.create(); + + Flowable.amb(Arrays.asList(pp1, pp2)).subscribe(ts); + + ts.assertNoValues(); + + pp2.onNext(2); + pp2.onComplete(); + + assertFalse(pp1.hasSubscribers()); + assertFalse(pp2.hasSubscribers()); + + ts.assertValue(2); + ts.assertNoErrors(); + ts.assertComplete(); + } + + @Test + public void ambArrayEmpty() { + assertSame(Flowable.empty(), Flowable.ambArray()); + } + + @Test + public void ambArraySingleElement() { + assertSame(Flowable.never(), Flowable.ambArray(Flowable.never())); + } + + @Test + public void disposed() { + TestHelper.checkDisposed(Flowable.ambArray(Flowable.never(), Flowable.never())); + } + + @Test + public void manySources() { + Flowable<?>[] a = new Flowable[32]; + Arrays.fill(a, Flowable.never()); + a[31] = Flowable.just(1); + + Flowable.amb(Arrays.asList(a)) + .test() + .assertResult(1); + } + + @Test + public void emptyIterable() { + Flowable.amb(Collections.<Flowable<Integer>>emptyList()) + .test() + .assertResult(); + } + + @Test + public void singleIterable() { + Flowable.amb(Collections.singletonList(Flowable.just(1))) + .test() + .assertResult(1); + } + + @Test + public void onNextRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final PublishProcessor<Integer> pp1 = PublishProcessor.create(); + final PublishProcessor<Integer> pp2 = PublishProcessor.create(); + + TestSubscriberEx<Integer> ts = Flowable.ambArray(pp1, pp2).to(TestHelper.<Integer>testConsumer()); + + Runnable r1 = new Runnable() { + @Override + public void run() { + pp1.onNext(1); + } + }; + Runnable r2 = new Runnable() { + @Override + public void run() { + pp2.onNext(1); + } + }; + + TestHelper.race(r1, r2); + + ts.assertSubscribed().assertNoErrors() + .assertNotComplete().assertValueCount(1); + } + } + + @Test + public void onCompleteRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final PublishProcessor<Integer> pp1 = PublishProcessor.create(); + final PublishProcessor<Integer> pp2 = PublishProcessor.create(); + + TestSubscriber<Integer> ts = Flowable.ambArray(pp1, pp2).test(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + pp1.onComplete(); + } + }; + Runnable r2 = new Runnable() { + @Override + public void run() { + pp2.onComplete(); + } + }; + + TestHelper.race(r1, r2); + + ts.assertResult(); + } + } + + @Test + public void onErrorRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final PublishProcessor<Integer> pp1 = PublishProcessor.create(); + final PublishProcessor<Integer> pp2 = PublishProcessor.create(); + + TestSubscriber<Integer> ts = Flowable.ambArray(pp1, pp2).test(); + + final Throwable ex = new TestException(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + pp1.onError(ex); + } + }; + Runnable r2 = new Runnable() { + @Override + public void run() { + pp2.onError(ex); + } + }; + + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + TestHelper.race(r1, r2); + } finally { + RxJavaPlugins.reset(); + } + + ts.assertFailure(TestException.class); + if (!errors.isEmpty()) { + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } + } + } + + @Test + public void nullIterableElement() { + Flowable.amb(Arrays.asList(Flowable.never(), null, Flowable.never())) + .test() + .assertFailure(NullPointerException.class); + } + + @Test + public void iteratorThrows() { + Flowable.amb(new CrashingMappedIterable<>(1, 100, 100, new Function<Integer, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Integer v) throws Exception { + return Flowable.never(); + } + })) + .to(TestHelper.<Integer>testConsumer()) + .assertFailureAndMessage(TestException.class, "iterator()"); + + Flowable.amb(new CrashingMappedIterable<>(100, 1, 100, new Function<Integer, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Integer v) throws Exception { + return Flowable.never(); + } + })) + .to(TestHelper.<Integer>testConsumer()) + .assertFailureAndMessage(TestException.class, "hasNext()"); + + Flowable.amb(new CrashingMappedIterable<>(100, 100, 1, new Function<Integer, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Integer v) throws Exception { + return Flowable.never(); + } + })) + .to(TestHelper.<Integer>testConsumer()) + .assertFailureAndMessage(TestException.class, "next()"); + } + + @Test + public void ambWithOrder() { + Flowable<Integer> error = Flowable.error(new RuntimeException()); + Flowable.just(1).ambWith(error).test().assertValue(1).assertComplete(); + } + + @Test + public void ambIterableOrder() { + Flowable<Integer> error = Flowable.error(new RuntimeException()); + Flowable.amb(Arrays.asList(Flowable.just(1), error)).test().assertValue(1).assertComplete(); + } + + @Test + public void ambArrayOrder() { + Flowable<Integer> error = Flowable.error(new RuntimeException()); + Flowable.ambArray(Flowable.just(1), error).test().assertValue(1).assertComplete(); + } + + @Test + public void noWinnerSuccessDispose() throws Exception { + for (int i = 0; i < TestHelper.RACE_LONG_LOOPS; i++) { + final AtomicBoolean interrupted = new AtomicBoolean(); + final CountDownLatch cdl = new CountDownLatch(1); + + Flowable.ambArray( + Flowable.just(1) + .subscribeOn(Schedulers.single()) + .observeOn(Schedulers.computation()), + Flowable.never() + ) + .subscribe(new Consumer<Object>() { + @Override + public void accept(Object v) throws Exception { + interrupted.set(Thread.currentThread().isInterrupted()); + cdl.countDown(); + } + }); + + assertTrue(cdl.await(500, TimeUnit.SECONDS)); + assertFalse("Interrupted!", interrupted.get()); + } + } + + @Test + public void noWinnerErrorDispose() throws Exception { + final TestException ex = new TestException(); + for (int i = 0; i < TestHelper.RACE_LONG_LOOPS; i++) { + final AtomicBoolean interrupted = new AtomicBoolean(); + final CountDownLatch cdl = new CountDownLatch(1); + + Flowable.ambArray( + Flowable.error(ex) + .subscribeOn(Schedulers.single()) + .observeOn(Schedulers.computation()), + Flowable.never() + ) + .subscribe(Functions.emptyConsumer(), new Consumer<Throwable>() { + @Override + public void accept(Throwable e) throws Exception { + interrupted.set(Thread.currentThread().isInterrupted()); + cdl.countDown(); + } + }); + + assertTrue(cdl.await(500, TimeUnit.SECONDS)); + assertFalse("Interrupted!", interrupted.get()); + } + } + + @Test + public void noWinnerCompleteDispose() throws Exception { + for (int i = 0; i < TestHelper.RACE_LONG_LOOPS; i++) { + final AtomicBoolean interrupted = new AtomicBoolean(); + final CountDownLatch cdl = new CountDownLatch(1); + + Flowable.ambArray( + Flowable.empty() + .subscribeOn(Schedulers.single()) + .observeOn(Schedulers.computation()), + Flowable.never() + ) + .subscribe(Functions.emptyConsumer(), Functions.emptyConsumer(), new Action() { + @Override + public void run() throws Exception { + interrupted.set(Thread.currentThread().isInterrupted()); + cdl.countDown(); + } + }); + + assertTrue(cdl.await(500, TimeUnit.SECONDS)); + assertFalse("Interrupted!", interrupted.get()); + } + } + + @Test + public void publishersInIterable() { + Publisher<Integer> source = new Publisher<Integer>() { + @Override + public void subscribe(Subscriber<? super Integer> subscriber) { + Flowable.just(1).subscribe(subscriber); + } + }; + + Flowable.amb(Arrays.asList(source, source)) + .test() + .assertResult(1); + } + + @Test + public void badRequest() { + TestHelper.assertBadRequestReported(Flowable.amb(Arrays.asList(Flowable.never(), Flowable.never()))); + } + + @Test + public void requestAfterCancel() { + Flowable.amb(Arrays.asList(Flowable.never(), Flowable.never())) + .subscribe(new FlowableSubscriber<Object>() { + + @Override + public void onNext(@NonNull Object t) { + } + + @Override + public void onError(Throwable t) { + } + + @Override + public void onComplete() { + } + + @Override + public void onSubscribe(@NonNull Subscription s) { + s.cancel(); + s.request(1); + } + }); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableAnyTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableAnyTest.java new file mode 100644 index 0000000000..2f18a90c64 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableAnyTest.java @@ -0,0 +1,621 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import java.io.IOException; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import org.junit.Test; +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.internal.subscriptions.BooleanSubscription; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.subscribers.TestSubscriber; +import io.reactivex.rxjava3.testsupport.*; + +public class FlowableAnyTest extends RxJavaTest { + + @Test + public void anyWithTwoItems() { + Flowable<Integer> w = Flowable.just(1, 2); + Single<Boolean> single = w.any(new Predicate<Integer>() { + @Override + public boolean test(Integer v) { + return true; + } + }); + + SingleObserver<Boolean> observer = TestHelper.mockSingleObserver(); + + single.subscribe(observer); + + verify(observer, never()).onSuccess(false); + verify(observer, times(1)).onSuccess(true); + verify(observer, never()).onError(any(Throwable.class)); + } + + @Test + public void isEmptyWithTwoItems() { + Flowable<Integer> w = Flowable.just(1, 2); + Single<Boolean> single = w.isEmpty(); + + SingleObserver<Boolean> observer = TestHelper.mockSingleObserver(); + + single.subscribe(observer); + + verify(observer, never()).onSuccess(true); + verify(observer, times(1)).onSuccess(false); + verify(observer, never()).onError(any(Throwable.class)); + } + + @Test + public void anyWithOneItem() { + Flowable<Integer> w = Flowable.just(1); + Single<Boolean> single = w.any(new Predicate<Integer>() { + @Override + public boolean test(Integer v) { + return true; + } + }); + + SingleObserver<Boolean> observer = TestHelper.mockSingleObserver(); + + single.subscribe(observer); + + verify(observer, never()).onSuccess(false); + verify(observer, times(1)).onSuccess(true); + verify(observer, never()).onError(any(Throwable.class)); + } + + @Test + public void isEmptyWithOneItem() { + Flowable<Integer> w = Flowable.just(1); + Single<Boolean> single = w.isEmpty(); + + SingleObserver<Boolean> observer = TestHelper.mockSingleObserver(); + + single.subscribe(observer); + + verify(observer, never()).onSuccess(true); + verify(observer, times(1)).onSuccess(false); + verify(observer, never()).onError(any(Throwable.class)); + } + + @Test + public void anyWithEmpty() { + Flowable<Integer> w = Flowable.empty(); + Single<Boolean> single = w.any(new Predicate<Integer>() { + @Override + public boolean test(Integer v) { + return true; + } + }); + + SingleObserver<Boolean> observer = TestHelper.mockSingleObserver(); + + single.subscribe(observer); + + verify(observer, times(1)).onSuccess(false); + verify(observer, never()).onSuccess(true); + verify(observer, never()).onError(any(Throwable.class)); + } + + @Test + public void isEmptyWithEmpty() { + Flowable<Integer> w = Flowable.empty(); + Single<Boolean> single = w.isEmpty(); + + SingleObserver<Boolean> observer = TestHelper.mockSingleObserver(); + + single.subscribe(observer); + + verify(observer, times(1)).onSuccess(true); + verify(observer, never()).onSuccess(false); + verify(observer, never()).onError(any(Throwable.class)); + } + + @Test + public void anyWithPredicate1() { + Flowable<Integer> w = Flowable.just(1, 2, 3); + Single<Boolean> single = w.any(new Predicate<Integer>() { + @Override + public boolean test(Integer t1) { + return t1 < 2; + } + }); + + SingleObserver<Boolean> observer = TestHelper.mockSingleObserver(); + + single.subscribe(observer); + + verify(observer, never()).onSuccess(false); + verify(observer, times(1)).onSuccess(true); + verify(observer, never()).onError(any(Throwable.class)); + } + + @Test + public void exists1() { + Flowable<Integer> w = Flowable.just(1, 2, 3); + Single<Boolean> single = w.any(new Predicate<Integer>() { + @Override + public boolean test(Integer t1) { + return t1 < 2; + } + }); + + SingleObserver<Boolean> observer = TestHelper.mockSingleObserver(); + + single.subscribe(observer); + + verify(observer, never()).onSuccess(false); + verify(observer, times(1)).onSuccess(true); + verify(observer, never()).onError(any(Throwable.class)); + } + + @Test + public void anyWithPredicate2() { + Flowable<Integer> w = Flowable.just(1, 2, 3); + Single<Boolean> single = w.any(new Predicate<Integer>() { + @Override + public boolean test(Integer t1) { + return t1 < 1; + } + }); + + SingleObserver<Boolean> observer = TestHelper.mockSingleObserver(); + + single.subscribe(observer); + + verify(observer, times(1)).onSuccess(false); + verify(observer, never()).onSuccess(true); + verify(observer, never()).onError(any(Throwable.class)); + } + + @Test + public void anyWithEmptyAndPredicate() { + // If the source is empty, always output false. + Flowable<Integer> w = Flowable.empty(); + Single<Boolean> single = w.any(new Predicate<Integer>() { + @Override + public boolean test(Integer t) { + return true; + } + }); + + SingleObserver<Boolean> observer = TestHelper.mockSingleObserver(); + + single.subscribe(observer); + + verify(observer, times(1)).onSuccess(false); + verify(observer, never()).onSuccess(true); + verify(observer, never()).onError(any(Throwable.class)); + } + + @Test + public void withFollowingFirst() { + Flowable<Integer> f = Flowable.fromArray(1, 3, 5, 6); + Single<Boolean> anyEven = f.any(new Predicate<Integer>() { + @Override + public boolean test(Integer i) { + return i % 2 == 0; + } + }); + + assertTrue(anyEven.blockingGet()); + } + + @Test + public void issue1935NoUnsubscribeDownstream() { + Flowable<Integer> source = Flowable.just(1).isEmpty() + .flatMapPublisher(new Function<Boolean, Publisher<Integer>>() { + @Override + public Publisher<Integer> apply(Boolean t1) { + return Flowable.just(2).delay(500, TimeUnit.MILLISECONDS); + } + }); + + assertEquals((Object)2, source.blockingFirst()); + } + + @Test + public void backpressureIfOneRequestedOneShouldBeDelivered() { + TestObserverEx<Boolean> to = new TestObserverEx<>(); + Flowable.just(1).any(new Predicate<Integer>() { + @Override + public boolean test(Integer v) { + return true; + } + }).subscribe(to); + + to.assertTerminated(); + to.assertNoErrors(); + to.assertComplete(); + to.assertValue(true); + } + + @Test + public void predicateThrowsExceptionAndValueInCauseMessage() { + TestObserverEx<Boolean> to = new TestObserverEx<>(); + final IllegalArgumentException ex = new IllegalArgumentException(); + + Flowable.just("Boo!").any(new Predicate<String>() { + @Override + public boolean test(String v) { + throw ex; + } + }).subscribe(to); + + to.assertTerminated(); + to.assertNoValues(); + to.assertNotComplete(); + to.assertError(ex); + // FIXME value as last cause? +// assertTrue(ex.getCause().getMessage().contains("Boo!")); + } + + @Test + public void anyWithTwoItemsFlowable() { + Flowable<Integer> w = Flowable.just(1, 2); + Flowable<Boolean> flowable = w.any(new Predicate<Integer>() { + @Override + public boolean test(Integer v) { + return true; + } + }) + .toFlowable() + ; + + Subscriber<Boolean> subscriber = TestHelper.mockSubscriber(); + + flowable.subscribe(subscriber); + + verify(subscriber, never()).onNext(false); + verify(subscriber, times(1)).onNext(true); + verify(subscriber, never()).onError(any(Throwable.class)); + verify(subscriber, times(1)).onComplete(); + } + + @Test + public void isEmptyWithTwoItemsFlowable() { + Flowable<Integer> w = Flowable.just(1, 2); + Flowable<Boolean> flowable = w.isEmpty().toFlowable(); + + Subscriber<Boolean> subscriber = TestHelper.mockSubscriber(); + + flowable.subscribe(subscriber); + + verify(subscriber, never()).onNext(true); + verify(subscriber, times(1)).onNext(false); + verify(subscriber, never()).onError(any(Throwable.class)); + verify(subscriber, times(1)).onComplete(); + } + + @Test + public void anyWithOneItemFlowable() { + Flowable<Integer> w = Flowable.just(1); + Flowable<Boolean> flowable = w.any(new Predicate<Integer>() { + @Override + public boolean test(Integer v) { + return true; + } + }).toFlowable(); + + Subscriber<Boolean> subscriber = TestHelper.mockSubscriber(); + + flowable.subscribe(subscriber); + + verify(subscriber, never()).onNext(false); + verify(subscriber, times(1)).onNext(true); + verify(subscriber, never()).onError(any(Throwable.class)); + verify(subscriber, times(1)).onComplete(); + } + + @Test + public void isEmptyWithOneItemFlowable() { + Flowable<Integer> w = Flowable.just(1); + Single<Boolean> single = w.isEmpty(); + + SingleObserver<Boolean> observer = TestHelper.mockSingleObserver(); + + single.subscribe(observer); + + verify(observer, never()).onSuccess(true); + verify(observer, times(1)).onSuccess(false); + verify(observer, never()).onError(any(Throwable.class)); + } + + @Test + public void anyWithEmptyFlowable() { + Flowable<Integer> w = Flowable.empty(); + Flowable<Boolean> flowable = w.any(new Predicate<Integer>() { + @Override + public boolean test(Integer v) { + return true; + } + }).toFlowable(); + + Subscriber<Boolean> subscriber = TestHelper.mockSubscriber(); + + flowable.subscribe(subscriber); + + verify(subscriber, times(1)).onNext(false); + verify(subscriber, never()).onNext(true); + verify(subscriber, never()).onError(any(Throwable.class)); + verify(subscriber, times(1)).onComplete(); + } + + @Test + public void isEmptyWithEmptyFlowable() { + Flowable<Integer> w = Flowable.empty(); + Flowable<Boolean> flowable = w.isEmpty().toFlowable(); + + Subscriber<Boolean> subscriber = TestHelper.mockSubscriber(); + + flowable.subscribe(subscriber); + + verify(subscriber, times(1)).onNext(true); + verify(subscriber, never()).onNext(false); + verify(subscriber, never()).onError(any(Throwable.class)); + verify(subscriber, times(1)).onComplete(); + } + + @Test + public void anyWithPredicate1Flowable() { + Flowable<Integer> w = Flowable.just(1, 2, 3); + Flowable<Boolean> flowable = w.any(new Predicate<Integer>() { + @Override + public boolean test(Integer t1) { + return t1 < 2; + } + }).toFlowable(); + + Subscriber<Boolean> subscriber = TestHelper.mockSubscriber(); + + flowable.subscribe(subscriber); + + verify(subscriber, never()).onNext(false); + verify(subscriber, times(1)).onNext(true); + verify(subscriber, never()).onError(any(Throwable.class)); + verify(subscriber, times(1)).onComplete(); + } + + @Test + public void exists1Flowable() { + Flowable<Integer> w = Flowable.just(1, 2, 3); + Flowable<Boolean> flowable = w.any(new Predicate<Integer>() { + @Override + public boolean test(Integer t1) { + return t1 < 2; + } + }).toFlowable(); + + Subscriber<Boolean> subscriber = TestHelper.mockSubscriber(); + + flowable.subscribe(subscriber); + + verify(subscriber, never()).onNext(false); + verify(subscriber, times(1)).onNext(true); + verify(subscriber, never()).onError(any(Throwable.class)); + verify(subscriber, times(1)).onComplete(); + } + + @Test + public void anyWithPredicate2Flowable() { + Flowable<Integer> w = Flowable.just(1, 2, 3); + Flowable<Boolean> flowable = w.any(new Predicate<Integer>() { + @Override + public boolean test(Integer t1) { + return t1 < 1; + } + }).toFlowable(); + + Subscriber<Boolean> subscriber = TestHelper.mockSubscriber(); + + flowable.subscribe(subscriber); + + verify(subscriber, times(1)).onNext(false); + verify(subscriber, never()).onNext(true); + verify(subscriber, never()).onError(any(Throwable.class)); + verify(subscriber, times(1)).onComplete(); + } + + @Test + public void anyWithEmptyAndPredicateFlowable() { + // If the source is empty, always output false. + Flowable<Integer> w = Flowable.empty(); + Flowable<Boolean> flowable = w.any(new Predicate<Integer>() { + @Override + public boolean test(Integer t) { + return true; + } + }).toFlowable(); + + Subscriber<Boolean> subscriber = TestHelper.mockSubscriber(); + + flowable.subscribe(subscriber); + + verify(subscriber, times(1)).onNext(false); + verify(subscriber, never()).onNext(true); + verify(subscriber, never()).onError(any(Throwable.class)); + verify(subscriber, times(1)).onComplete(); + } + + @Test + public void withFollowingFirstFlowable() { + Flowable<Integer> f = Flowable.fromArray(1, 3, 5, 6); + Flowable<Boolean> anyEven = f.any(new Predicate<Integer>() { + @Override + public boolean test(Integer i) { + return i % 2 == 0; + } + }).toFlowable(); + + assertTrue(anyEven.blockingFirst()); + } + + @Test + public void issue1935NoUnsubscribeDownstreamFlowable() { + Flowable<Integer> source = Flowable.just(1).isEmpty() + .flatMapPublisher(new Function<Boolean, Publisher<Integer>>() { + @Override + public Publisher<Integer> apply(Boolean t1) { + return Flowable.just(2).delay(500, TimeUnit.MILLISECONDS); + } + }); + + assertEquals((Object)2, source.blockingFirst()); + } + + @Test + public void backpressureIfNoneRequestedNoneShouldBeDeliveredFlowable() { + TestSubscriber<Boolean> ts = new TestSubscriber<>(0L); + + Flowable.just(1).any(new Predicate<Integer>() { + @Override + public boolean test(Integer t) { + return true; + } + }).toFlowable() + .subscribe(ts); + + ts.assertNoValues(); + ts.assertNoErrors(); + ts.assertNotComplete(); + } + + @Test + public void backpressureIfOneRequestedOneShouldBeDeliveredFlowable() { + TestSubscriberEx<Boolean> ts = new TestSubscriberEx<>(1L); + Flowable.just(1).any(new Predicate<Integer>() { + @Override + public boolean test(Integer v) { + return true; + } + }).toFlowable().subscribe(ts); + + ts.assertTerminated(); + ts.assertNoErrors(); + ts.assertComplete(); + ts.assertValue(true); + } + + @Test + public void predicateThrowsExceptionAndValueInCauseMessageFlowable() { + TestSubscriberEx<Boolean> ts = new TestSubscriberEx<>(); + final IllegalArgumentException ex = new IllegalArgumentException(); + + Flowable.just("Boo!").any(new Predicate<String>() { + @Override + public boolean test(String v) { + throw ex; + } + }).toFlowable().subscribe(ts); + + ts.assertTerminated(); + ts.assertNoValues(); + ts.assertNotComplete(); + ts.assertError(ex); + // FIXME value as last cause? +// assertTrue(ex.getCause().getMessage().contains("Boo!")); + } + + @Test + public void dispose() { + TestHelper.checkDisposed(Flowable.just(1).any(Functions.alwaysTrue()).toFlowable()); + + TestHelper.checkDisposed(Flowable.just(1).any(Functions.alwaysTrue())); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeFlowable(new Function<Flowable<Object>, Publisher<Boolean>>() { + @Override + public Publisher<Boolean> apply(Flowable<Object> f) throws Exception { + return f.any(Functions.alwaysTrue()).toFlowable(); + } + }); + + TestHelper.checkDoubleOnSubscribeFlowableToSingle(new Function<Flowable<Object>, Single<Boolean>>() { + @Override + public Single<Boolean> apply(Flowable<Object> f) throws Exception { + return f.any(Functions.alwaysTrue()); + } + }); + } + + @Test + public void predicateThrowsSuppressOthers() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + new Flowable<Integer>() { + @Override + protected void subscribeActual(Subscriber<? super Integer> subscriber) { + subscriber.onSubscribe(new BooleanSubscription()); + + subscriber.onNext(1); + subscriber.onNext(2); + subscriber.onError(new IOException()); + subscriber.onComplete(); + } + } + .any(new Predicate<Integer>() { + @Override + public boolean test(Integer v) throws Exception { + throw new TestException(); + } + }) + .toFlowable() + .test() + .assertFailure(TestException.class); + + TestHelper.assertUndeliverable(errors, 0, IOException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void badSourceSingle() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + new Flowable<Integer>() { + @Override + protected void subscribeActual(Subscriber<? super Integer> subscriber) { + subscriber.onSubscribe(new BooleanSubscription()); + subscriber.onError(new TestException("First")); + + subscriber.onNext(1); + subscriber.onError(new TestException("Second")); + subscriber.onComplete(); + } + } + .any(Functions.alwaysTrue()) + .to(TestHelper.<Boolean>testConsumer()) + .assertFailureAndMessage(TestException.class, "First"); + + TestHelper.assertUndeliverable(errors, 0, TestException.class, "Second"); + } finally { + RxJavaPlugins.reset(); + } + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableAsObservableTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableAsObservableTest.java new file mode 100644 index 0000000000..d922ef48c3 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableAsObservableTest.java @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import static org.junit.Assert.assertFalse; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import org.junit.Test; +import org.mockito.Mockito; +import org.reactivestreams.Subscriber; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.processors.PublishProcessor; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class FlowableAsObservableTest extends RxJavaTest { + @Test + public void hiding() { + PublishProcessor<Integer> src = PublishProcessor.create(); + + Flowable<Integer> dst = src.hide(); + + assertFalse(dst instanceof PublishProcessor); + + Subscriber<Object> subscriber = TestHelper.mockSubscriber(); + + dst.subscribe(subscriber); + + src.onNext(1); + src.onComplete(); + + verify(subscriber).onNext(1); + verify(subscriber).onComplete(); + verify(subscriber, never()).onError(any(Throwable.class)); + } + + @Test + public void hidingError() { + PublishProcessor<Integer> src = PublishProcessor.create(); + + Flowable<Integer> dst = src.hide(); + + assertFalse(dst instanceof PublishProcessor); + + Subscriber<Integer> subscriber = TestHelper.mockSubscriber(); + + dst.subscribe(subscriber); + + src.onError(new TestException()); + + verify(subscriber, never()).onNext(Mockito.<Integer>any()); + verify(subscriber, never()).onComplete(); + verify(subscriber).onError(any(TestException.class)); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableAutoConnectTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableAutoConnectTest.java new file mode 100644 index 0000000000..07d1e66214 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableAutoConnectTest.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import static org.junit.Assert.assertTrue; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.RxJavaTest; +import io.reactivex.rxjava3.processors.PublishProcessor; + +public class FlowableAutoConnectTest extends RxJavaTest { + + @Test + public void autoConnectImmediately() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + + pp.publish().autoConnect(0); + + assertTrue(pp.hasSubscribers()); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableBlockingTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableBlockingTest.java new file mode 100644 index 0000000000..a1facf4046 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableBlockingTest.java @@ -0,0 +1,546 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import static org.junit.Assert.assertEquals; + +import java.util.*; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.Test; +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.internal.subscriptions.BooleanSubscription; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.processors.PublishProcessor; +import io.reactivex.rxjava3.schedulers.Schedulers; +import io.reactivex.rxjava3.subscribers.TestSubscriber; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class FlowableBlockingTest extends RxJavaTest { + + @Test + public void blockingFirst() { + assertEquals(1, Flowable.range(1, 10) + .subscribeOn(Schedulers.computation()).blockingFirst().intValue()); + } + + @Test + public void blockingFirstDefault() { + assertEquals(1, Flowable.<Integer>empty() + .subscribeOn(Schedulers.computation()).blockingFirst(1).intValue()); + } + + @Test + public void blockingSubscribeConsumer() { + final List<Integer> list = new ArrayList<>(); + + Flowable.range(1, 5) + .subscribeOn(Schedulers.computation()) + .blockingSubscribe(new Consumer<Integer>() { + @Override + public void accept(Integer v) throws Exception { + list.add(v); + } + }); + + assertEquals(Arrays.asList(1, 2, 3, 4, 5), list); + } + + @Test + public void boundedBlockingSubscribeConsumer() { + final List<Integer> list = new ArrayList<>(); + + Flowable.range(1, 5) + .subscribeOn(Schedulers.computation()) + .blockingSubscribe(new Consumer<Integer>() { + @Override + public void accept(Integer v) throws Exception { + list.add(v); + } + }, 128); + + assertEquals(Arrays.asList(1, 2, 3, 4, 5), list); + } + + @Test + public void boundedBlockingSubscribeConsumerBufferExceed() { + final List<Integer> list = new ArrayList<>(); + + Flowable.range(1, 5) + .subscribeOn(Schedulers.computation()) + .blockingSubscribe(new Consumer<Integer>() { + @Override + public void accept(Integer v) throws Exception { + list.add(v); + } + }, 3); + + assertEquals(Arrays.asList(1, 2, 3, 4, 5), list); + } + + @Test + public void blockingSubscribeConsumerConsumer() { + final List<Object> list = new ArrayList<>(); + + Flowable.range(1, 5) + .subscribeOn(Schedulers.computation()) + .blockingSubscribe(new Consumer<Integer>() { + @Override + public void accept(Integer v) throws Exception { + list.add(v); + } + }, Functions.emptyConsumer()); + + assertEquals(Arrays.asList(1, 2, 3, 4, 5), list); + } + + @Test + public void boundedBlockingSubscribeConsumerConsumer() { + final List<Object> list = new ArrayList<>(); + + Flowable.range(1, 5) + .subscribeOn(Schedulers.computation()) + .blockingSubscribe(new Consumer<Integer>() { + @Override + public void accept(Integer v) throws Exception { + list.add(v); + } + }, Functions.emptyConsumer(), 128); + + assertEquals(Arrays.asList(1, 2, 3, 4, 5), list); + } + + @Test + public void boundedBlockingSubscribeConsumerConsumerBufferExceed() { + final List<Object> list = new ArrayList<>(); + + Flowable.range(1, 5) + .subscribeOn(Schedulers.computation()) + .blockingSubscribe(new Consumer<Integer>() { + @Override + public void accept(Integer v) throws Exception { + list.add(v); + } + }, Functions.emptyConsumer(), 3); + + assertEquals(Arrays.asList(1, 2, 3, 4, 5), list); + } + + @Test + public void blockingSubscribeConsumerConsumerError() { + final List<Object> list = new ArrayList<>(); + + TestException ex = new TestException(); + + Consumer<Object> cons = new Consumer<Object>() { + @Override + public void accept(Object v) throws Exception { + list.add(v); + } + }; + + Flowable.range(1, 5).concatWith(Flowable.<Integer>error(ex)) + .subscribeOn(Schedulers.computation()) + .blockingSubscribe(cons, cons); + + assertEquals(Arrays.asList(1, 2, 3, 4, 5, ex), list); + } + + @Test + public void boundedBlockingSubscribeConsumerConsumerError() { + final List<Object> list = new ArrayList<>(); + + TestException ex = new TestException(); + + Consumer<Object> cons = new Consumer<Object>() { + @Override + public void accept(Object v) throws Exception { + list.add(v); + } + }; + + Flowable.range(1, 5).concatWith(Flowable.<Integer>error(ex)) + .subscribeOn(Schedulers.computation()) + .blockingSubscribe(cons, cons, 128); + + assertEquals(Arrays.asList(1, 2, 3, 4, 5, ex), list); + } + + @Test + public void blockingSubscribeConsumerConsumerAction() { + final List<Object> list = new ArrayList<>(); + + Consumer<Object> cons = new Consumer<Object>() { + @Override + public void accept(Object v) throws Exception { + list.add(v); + } + }; + + Flowable.range(1, 5) + .subscribeOn(Schedulers.computation()) + .blockingSubscribe(cons, cons, new Action() { + @Override + public void run() throws Exception { + list.add(100); + } + }); + + assertEquals(Arrays.asList(1, 2, 3, 4, 5, 100), list); + } + + @Test + public void boundedBlockingSubscribeConsumerConsumerAction() { + final List<Object> list = new ArrayList<>(); + + Consumer<Object> cons = new Consumer<Object>() { + @Override + public void accept(Object v) throws Exception { + list.add(v); + } + }; + + Action action = new Action() { + @Override + public void run() throws Exception { + list.add(100); + } + }; + + Flowable.range(1, 5) + .subscribeOn(Schedulers.computation()) + .blockingSubscribe(cons, cons, action, 128); + + assertEquals(Arrays.asList(1, 2, 3, 4, 5, 100), list); + } + + @Test + public void boundedBlockingSubscribeConsumerConsumerActionBufferExceed() { + final List<Object> list = new ArrayList<>(); + + Consumer<Object> cons = new Consumer<Object>() { + @Override + public void accept(Object v) throws Exception { + list.add(v); + } + }; + + Action action = new Action() { + @Override + public void run() throws Exception { + list.add(100); + } + }; + + Flowable.range(1, 5) + .subscribeOn(Schedulers.computation()) + .blockingSubscribe(cons, cons, action, 3); + + assertEquals(Arrays.asList(1, 2, 3, 4, 5, 100), list); + } + + @Test + public void boundedBlockingSubscribeConsumerConsumerActionBufferExceedMillionItem() { + final List<Object> list = new ArrayList<>(); + + Consumer<Object> cons = new Consumer<Object>() { + @Override + public void accept(Object v) throws Exception { + list.add(v); + } + }; + + Action action = new Action() { + @Override + public void run() throws Exception { + list.add(1000001); + } + }; + + Flowable.range(1, 1000000) + .subscribeOn(Schedulers.computation()) + .blockingSubscribe(cons, cons, action, 128); + + assertEquals(1000000 + 1, list.size()); + } + + @Test + public void blockingSubscribeObserver() { + final List<Object> list = new ArrayList<>(); + + Flowable.range(1, 5) + .subscribeOn(Schedulers.computation()) + .blockingSubscribe(new FlowableSubscriber<Object>() { + + @Override + public void onSubscribe(Subscription s) { + s.request(Long.MAX_VALUE); + } + + @Override + public void onNext(Object value) { + list.add(value); + } + + @Override + public void onError(Throwable e) { + list.add(e); + } + + @Override + public void onComplete() { + list.add(100); + } + + }); + + assertEquals(Arrays.asList(1, 2, 3, 4, 5, 100), list); + } + + @Test + public void blockingSubscribeObserverError() { + final List<Object> list = new ArrayList<>(); + + final TestException ex = new TestException(); + + Flowable.range(1, 5).concatWith(Flowable.<Integer>error(ex)) + .subscribeOn(Schedulers.computation()) + .blockingSubscribe(new FlowableSubscriber<Object>() { + + @Override + public void onSubscribe(Subscription s) { + s.request(Long.MAX_VALUE); + } + + @Override + public void onNext(Object value) { + list.add(value); + } + + @Override + public void onError(Throwable e) { + list.add(e); + } + + @Override + public void onComplete() { + list.add(100); + } + + }); + + assertEquals(Arrays.asList(1, 2, 3, 4, 5, ex), list); + } + + @Test(expected = TestException.class) + public void blockingForEachThrows() { + Flowable.just(1) + .blockingForEach(new Consumer<Integer>() { + @Override + public void accept(Integer e) throws Exception { + throw new TestException(); + } + }); + } + + @Test(expected = NoSuchElementException.class) + public void blockingFirstEmpty() { + Flowable.empty().blockingFirst(); + } + + @Test(expected = NoSuchElementException.class) + public void blockingLastEmpty() { + Flowable.empty().blockingLast(); + } + + @Test + public void blockingFirstNormal() { + assertEquals(1, Flowable.just(1, 2).blockingFirst(3).intValue()); + } + + @Test + public void blockingLastNormal() { + assertEquals(2, Flowable.just(1, 2).blockingLast(3).intValue()); + } + + @Test + public void firstFgnoredCancelAndOnNext() { + Flowable<Integer> source = Flowable.fromPublisher(new Publisher<Integer>() { + @Override + public void subscribe(Subscriber<? super Integer> s) { + s.onSubscribe(new BooleanSubscription()); + s.onNext(1); + s.onNext(2); + } + }); + + assertEquals(1, source.blockingFirst().intValue()); + } + + @Test + public void firstIgnoredCancelAndOnError() { + List<Throwable> list = TestHelper.trackPluginErrors(); + try { + Flowable<Integer> source = Flowable.fromPublisher(new Publisher<Integer>() { + @Override + public void subscribe(Subscriber<? super Integer> s) { + s.onSubscribe(new BooleanSubscription()); + s.onNext(1); + s.onError(new TestException()); + } + }); + + assertEquals(1, source.blockingFirst().intValue()); + + TestHelper.assertUndeliverable(list, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test(expected = TestException.class) + public void firstOnError() { + Flowable<Integer> source = Flowable.fromPublisher(new Publisher<Integer>() { + @Override + public void subscribe(Subscriber<? super Integer> s) { + s.onSubscribe(new BooleanSubscription()); + s.onError(new TestException()); + } + }); + + source.blockingFirst(); + } + + @Test + public void interrupt() { + TestSubscriber<Integer> ts = new TestSubscriber<>(0L); + + Thread.currentThread().interrupt(); + + try { + Flowable.just(1) + .blockingSubscribe(ts); + + ts.assertFailure(InterruptedException.class); + } finally { + Thread.interrupted(); // clear interrupted status just in case + } + } + + @Test(expected = NoSuchElementException.class) + public void blockingSingleEmpty() { + Flowable.empty().blockingSingle(); + } + + @Test + public void onCompleteDelayed() { + TestSubscriber<Object> ts = new TestSubscriber<>(); + + Flowable.empty().delay(100, TimeUnit.MILLISECONDS) + .blockingSubscribe(ts); + + ts.assertResult(); + } + + @Test + public void utilityClass() { + TestHelper.checkUtilityClass(FlowableBlockingSubscribe.class); + } + + @Test + public void disposeUpFront() { + TestSubscriber<Object> ts = new TestSubscriber<>(); + ts.cancel(); + Flowable.just(1).blockingSubscribe(ts); + + ts.assertEmpty(); + } + + @SuppressWarnings("rawtypes") + @Test + public void delayed() throws Exception { + final TestSubscriber<Object> ts = new TestSubscriber<>(); + final Subscriber[] s = { null }; + + Schedulers.single().scheduleDirect(new Runnable() { + @SuppressWarnings("unchecked") + @Override + public void run() { + ts.cancel(); + s[0].onNext(1); + } + }, 200, TimeUnit.MILLISECONDS); + + new Flowable<Integer>() { + @Override + protected void subscribeActual(Subscriber<? super Integer> subscriber) { + subscriber.onSubscribe(new BooleanSubscription()); + s[0] = subscriber; + } + }.blockingSubscribe(ts); + + while (!ts.isCancelled()) { + Thread.sleep(100); + } + + ts.assertEmpty(); + } + + @Test + public void blockinsSubscribeCancelAsync() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final TestSubscriber<Integer> ts = new TestSubscriber<>(); + + final PublishProcessor<Integer> pp = PublishProcessor.create(); + + final Runnable r1 = new Runnable() { + @Override + public void run() { + ts.cancel(); + } + }; + + final Runnable r2 = new Runnable() { + @Override + public void run() { + pp.onNext(1); + } + }; + + final AtomicInteger c = new AtomicInteger(2); + + Schedulers.computation().scheduleDirect(new Runnable() { + @Override + public void run() { + c.decrementAndGet(); + while (c.get() != 0 && !pp.hasSubscribers()) { } + + TestHelper.race(r1, r2); + } + }); + + c.decrementAndGet(); + while (c.get() != 0) { } + + pp + .blockingSubscribe(ts); + } + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableBufferTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableBufferTest.java new file mode 100644 index 0000000000..c1b08990bb --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableBufferTest.java @@ -0,0 +1,2499 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import java.io.IOException; +import java.util.*; +import java.util.concurrent.*; +import java.util.concurrent.atomic.*; + +import org.junit.*; +import org.mockito.*; +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.internal.operators.flowable.FlowableBufferTimed.*; +import io.reactivex.rxjava3.internal.subscriptions.BooleanSubscription; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.processors.*; +import io.reactivex.rxjava3.schedulers.*; +import io.reactivex.rxjava3.subscribers.*; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class FlowableBufferTest extends RxJavaTest { + + private Subscriber<List<String>> subscriber; + private TestScheduler scheduler; + private Scheduler.Worker innerScheduler; + + @Before + public void before() { + subscriber = TestHelper.mockSubscriber(); + scheduler = new TestScheduler(); + innerScheduler = scheduler.createWorker(); + } + + @Test + public void complete() { + Flowable<String> source = Flowable.empty(); + + Flowable<List<String>> buffered = source.buffer(3, 3); + buffered.subscribe(subscriber); + + Mockito.verify(subscriber, Mockito.never()).onNext(Mockito.<String>anyList()); + Mockito.verify(subscriber, Mockito.never()).onError(Mockito.any(Throwable.class)); + Mockito.verify(subscriber, Mockito.times(1)).onComplete(); + } + + @Test + public void skipAndCountOverlappingBuffers() { + Flowable<String> source = Flowable.unsafeCreate(new Publisher<String>() { + @Override + public void subscribe(Subscriber<? super String> subscriber) { + subscriber.onSubscribe(new BooleanSubscription()); + subscriber.onNext("one"); + subscriber.onNext("two"); + subscriber.onNext("three"); + subscriber.onNext("four"); + subscriber.onNext("five"); + } + }); + + Flowable<List<String>> buffered = source.buffer(3, 1); + buffered.subscribe(subscriber); + + InOrder inOrder = Mockito.inOrder(subscriber); + inOrder.verify(subscriber, Mockito.times(1)).onNext(list("one", "two", "three")); + inOrder.verify(subscriber, Mockito.times(1)).onNext(list("two", "three", "four")); + inOrder.verify(subscriber, Mockito.times(1)).onNext(list("three", "four", "five")); + inOrder.verify(subscriber, Mockito.never()).onNext(Mockito.<String>anyList()); + inOrder.verify(subscriber, Mockito.never()).onError(Mockito.any(Throwable.class)); + inOrder.verify(subscriber, Mockito.never()).onComplete(); + } + + @Test + public void skipAndCountGaplessBuffers() { + Flowable<String> source = Flowable.just("one", "two", "three", "four", "five"); + + Flowable<List<String>> buffered = source.buffer(3, 3); + buffered.subscribe(subscriber); + + InOrder inOrder = Mockito.inOrder(subscriber); + inOrder.verify(subscriber, Mockito.times(1)).onNext(list("one", "two", "three")); + inOrder.verify(subscriber, Mockito.times(1)).onNext(list("four", "five")); + inOrder.verify(subscriber, Mockito.never()).onNext(Mockito.<String>anyList()); + inOrder.verify(subscriber, Mockito.never()).onError(Mockito.any(Throwable.class)); + inOrder.verify(subscriber, Mockito.times(1)).onComplete(); + } + + @Test + public void skipAndCountBuffersWithGaps() { + Flowable<String> source = Flowable.just("one", "two", "three", "four", "five"); + + Flowable<List<String>> buffered = source.buffer(2, 3); + buffered.subscribe(subscriber); + + InOrder inOrder = Mockito.inOrder(subscriber); + inOrder.verify(subscriber, Mockito.times(1)).onNext(list("one", "two")); + inOrder.verify(subscriber, Mockito.times(1)).onNext(list("four", "five")); + inOrder.verify(subscriber, Mockito.never()).onNext(Mockito.<String>anyList()); + inOrder.verify(subscriber, Mockito.never()).onError(Mockito.any(Throwable.class)); + inOrder.verify(subscriber, Mockito.times(1)).onComplete(); + } + + @Test + public void timedAndCount() { + Flowable<String> source = Flowable.unsafeCreate(new Publisher<String>() { + @Override + public void subscribe(Subscriber<? super String> subscriber) { + subscriber.onSubscribe(new BooleanSubscription()); + push(subscriber, "one", 10); + push(subscriber, "two", 90); + push(subscriber, "three", 110); + push(subscriber, "four", 190); + push(subscriber, "five", 210); + complete(subscriber, 250); + } + }); + + Flowable<List<String>> buffered = source.buffer(100, TimeUnit.MILLISECONDS, scheduler, 2); + buffered.subscribe(subscriber); + + InOrder inOrder = Mockito.inOrder(subscriber); + scheduler.advanceTimeTo(100, TimeUnit.MILLISECONDS); + inOrder.verify(subscriber, Mockito.times(1)).onNext(list("one", "two")); + + scheduler.advanceTimeTo(200, TimeUnit.MILLISECONDS); + inOrder.verify(subscriber, Mockito.times(1)).onNext(list("three", "four")); + + scheduler.advanceTimeTo(300, TimeUnit.MILLISECONDS); + inOrder.verify(subscriber, Mockito.times(1)).onNext(list("five")); + inOrder.verify(subscriber, Mockito.never()).onNext(Mockito.<String>anyList()); + inOrder.verify(subscriber, Mockito.never()).onError(Mockito.any(Throwable.class)); + inOrder.verify(subscriber, Mockito.times(1)).onComplete(); + } + + @Test + public void timed() { + Flowable<String> source = Flowable.unsafeCreate(new Publisher<String>() { + @Override + public void subscribe(Subscriber<? super String> subscriber) { + subscriber.onSubscribe(new BooleanSubscription()); + push(subscriber, "one", 97); + push(subscriber, "two", 98); + /** + * Changed from 100. Because scheduling the cut to 100ms happens before this + * Flowable even runs due how lift works, pushing at 100ms would execute after the + * buffer cut. + */ + push(subscriber, "three", 99); + push(subscriber, "four", 101); + push(subscriber, "five", 102); + complete(subscriber, 150); + } + }); + + Flowable<List<String>> buffered = source.buffer(100, TimeUnit.MILLISECONDS, scheduler); + buffered.subscribe(subscriber); + + InOrder inOrder = Mockito.inOrder(subscriber); + scheduler.advanceTimeTo(101, TimeUnit.MILLISECONDS); + inOrder.verify(subscriber, Mockito.times(1)).onNext(list("one", "two", "three")); + + scheduler.advanceTimeTo(201, TimeUnit.MILLISECONDS); + inOrder.verify(subscriber, Mockito.times(1)).onNext(list("four", "five")); + inOrder.verify(subscriber, Mockito.never()).onNext(Mockito.<String>anyList()); + inOrder.verify(subscriber, Mockito.never()).onError(Mockito.any(Throwable.class)); + inOrder.verify(subscriber, Mockito.times(1)).onComplete(); + } + + @Test + public void flowableBasedOpenerAndCloser() { + Flowable<String> source = Flowable.unsafeCreate(new Publisher<String>() { + @Override + public void subscribe(Subscriber<? super String> subscriber) { + subscriber.onSubscribe(new BooleanSubscription()); + push(subscriber, "one", 10); + push(subscriber, "two", 60); + push(subscriber, "three", 110); + push(subscriber, "four", 160); + push(subscriber, "five", 210); + complete(subscriber, 500); + } + }); + + Flowable<Object> openings = Flowable.unsafeCreate(new Publisher<Object>() { + @Override + public void subscribe(Subscriber<Object> subscriber) { + subscriber.onSubscribe(new BooleanSubscription()); + push(subscriber, new Object(), 50); + push(subscriber, new Object(), 200); + complete(subscriber, 250); + } + }); + + Function<Object, Flowable<Object>> closer = new Function<Object, Flowable<Object>>() { + @Override + public Flowable<Object> apply(Object opening) { + return Flowable.unsafeCreate(new Publisher<Object>() { + @Override + public void subscribe(Subscriber<? super Object> subscriber) { + subscriber.onSubscribe(new BooleanSubscription()); + push(subscriber, new Object(), 100); + complete(subscriber, 101); + } + }); + } + }; + + Flowable<List<String>> buffered = source.buffer(openings, closer); + buffered.subscribe(subscriber); + + InOrder inOrder = Mockito.inOrder(subscriber); + scheduler.advanceTimeTo(500, TimeUnit.MILLISECONDS); + inOrder.verify(subscriber, Mockito.times(1)).onNext(list("two", "three")); + inOrder.verify(subscriber, Mockito.times(1)).onNext(list("five")); + inOrder.verify(subscriber, Mockito.never()).onNext(Mockito.<String>anyList()); + inOrder.verify(subscriber, Mockito.never()).onError(Mockito.any(Throwable.class)); + inOrder.verify(subscriber, Mockito.times(1)).onComplete(); + } + + @Test + public void longTimeAction() throws InterruptedException { + final CountDownLatch latch = new CountDownLatch(1); + LongTimeAction action = new LongTimeAction(latch); + Flowable.just(1).buffer(10, TimeUnit.MILLISECONDS, 10) + .subscribe(action); + latch.await(); + assertFalse(action.fail); + } + + static final class LongTimeAction implements Consumer<List<Integer>> { + + CountDownLatch latch; + boolean fail; + + LongTimeAction(CountDownLatch latch) { + this.latch = latch; + } + + @Override + public void accept(List<Integer> t1) { + try { + if (fail) { + return; + } + Thread.sleep(200); + } catch (InterruptedException e) { + fail = true; + } finally { + latch.countDown(); + } + } + } + + private List<String> list(String... args) { + List<String> list = new ArrayList<>(); + for (String arg : args) { + list.add(arg); + } + return list; + } + + private <T> void push(final Subscriber<T> subscriber, final T value, int delay) { + innerScheduler.schedule(new Runnable() { + @Override + public void run() { + subscriber.onNext(value); + } + }, delay, TimeUnit.MILLISECONDS); + } + + private void complete(final Subscriber<?> subscriber, int delay) { + innerScheduler.schedule(new Runnable() { + @Override + public void run() { + subscriber.onComplete(); + } + }, delay, TimeUnit.MILLISECONDS); + } + + @Test + public void bufferStopsWhenUnsubscribed1() { + Flowable<Integer> source = Flowable.never(); + + Subscriber<List<Integer>> subscriber = TestHelper.mockSubscriber(); + TestSubscriber<List<Integer>> ts = new TestSubscriber<>(subscriber, 0L); + + source.buffer(100, 200, TimeUnit.MILLISECONDS, scheduler) + .doOnNext(new Consumer<List<Integer>>() { + @Override + public void accept(List<Integer> pv) { + System.out.println(pv); + } + }) + .subscribe(ts); + + InOrder inOrder = Mockito.inOrder(subscriber); + + scheduler.advanceTimeBy(1001, TimeUnit.MILLISECONDS); + + inOrder.verify(subscriber, times(5)).onNext(Arrays.<Integer> asList()); + + ts.cancel(); + + scheduler.advanceTimeBy(999, TimeUnit.MILLISECONDS); + + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void bufferWithBONormal1() { + PublishProcessor<Integer> source = PublishProcessor.create(); + PublishProcessor<Integer> boundary = PublishProcessor.create(); + + Subscriber<Object> subscriber = TestHelper.mockSubscriber(); + InOrder inOrder = Mockito.inOrder(subscriber); + + source.buffer(boundary).subscribe(subscriber); + + source.onNext(1); + source.onNext(2); + source.onNext(3); + + boundary.onNext(1); + + inOrder.verify(subscriber, times(1)).onNext(Arrays.asList(1, 2, 3)); + + source.onNext(4); + source.onNext(5); + + boundary.onNext(2); + + inOrder.verify(subscriber, times(1)).onNext(Arrays.asList(4, 5)); + + source.onNext(6); + boundary.onComplete(); + + inOrder.verify(subscriber, times(1)).onNext(Arrays.asList(6)); + + inOrder.verify(subscriber).onComplete(); + + verify(subscriber, never()).onError(any(Throwable.class)); + } + + @Test + public void bufferWithBOEmptyLastViaBoundary() { + PublishProcessor<Integer> source = PublishProcessor.create(); + PublishProcessor<Integer> boundary = PublishProcessor.create(); + + Subscriber<Object> subscriber = TestHelper.mockSubscriber(); + InOrder inOrder = Mockito.inOrder(subscriber); + + source.buffer(boundary).subscribe(subscriber); + + boundary.onComplete(); + + inOrder.verify(subscriber, times(1)).onNext(Arrays.asList()); + + inOrder.verify(subscriber).onComplete(); + + verify(subscriber, never()).onError(any(Throwable.class)); + } + + @Test + public void bufferWithBOEmptyLastViaSource() { + PublishProcessor<Integer> source = PublishProcessor.create(); + PublishProcessor<Integer> boundary = PublishProcessor.create(); + + Subscriber<Object> subscriber = TestHelper.mockSubscriber(); + InOrder inOrder = Mockito.inOrder(subscriber); + + source.buffer(boundary).subscribe(subscriber); + + source.onComplete(); + + inOrder.verify(subscriber, times(1)).onNext(Arrays.asList()); + + inOrder.verify(subscriber).onComplete(); + + verify(subscriber, never()).onError(any(Throwable.class)); + } + + @Test + public void bufferWithBOEmptyLastViaBoth() { + PublishProcessor<Integer> source = PublishProcessor.create(); + PublishProcessor<Integer> boundary = PublishProcessor.create(); + + Subscriber<Object> subscriber = TestHelper.mockSubscriber(); + InOrder inOrder = Mockito.inOrder(subscriber); + + source.buffer(boundary).subscribe(subscriber); + + source.onComplete(); + boundary.onComplete(); + + inOrder.verify(subscriber, times(1)).onNext(Arrays.asList()); + + inOrder.verify(subscriber).onComplete(); + + verify(subscriber, never()).onError(any(Throwable.class)); + } + + @Test + public void bufferWithBOSourceThrows() { + PublishProcessor<Integer> source = PublishProcessor.create(); + PublishProcessor<Integer> boundary = PublishProcessor.create(); + + Subscriber<Object> subscriber = TestHelper.mockSubscriber(); + + source.buffer(boundary).subscribe(subscriber); + source.onNext(1); + source.onError(new TestException()); + + verify(subscriber).onError(any(TestException.class)); + verify(subscriber, never()).onComplete(); + verify(subscriber, never()).onNext(any()); + } + + @Test + public void bufferWithBOBoundaryThrows() { + PublishProcessor<Integer> source = PublishProcessor.create(); + PublishProcessor<Integer> boundary = PublishProcessor.create(); + + Subscriber<Object> subscriber = TestHelper.mockSubscriber(); + + source.buffer(boundary).subscribe(subscriber); + + source.onNext(1); + boundary.onError(new TestException()); + + verify(subscriber).onError(any(TestException.class)); + verify(subscriber, never()).onComplete(); + verify(subscriber, never()).onNext(any()); + } + + @Test + public void bufferWithSizeTake1() { + Flowable<Integer> source = Flowable.just(1).repeat(); + + Flowable<List<Integer>> result = source.buffer(2).take(1); + + Subscriber<Object> subscriber = TestHelper.mockSubscriber(); + + result.subscribe(subscriber); + + verify(subscriber).onNext(Arrays.asList(1, 1)); + verify(subscriber).onComplete(); + verify(subscriber, never()).onError(any(Throwable.class)); + } + + @Test + public void bufferWithSizeSkipTake1() { + Flowable<Integer> source = Flowable.just(1).repeat(); + + Flowable<List<Integer>> result = source.buffer(2, 3).take(1); + + Subscriber<Object> subscriber = TestHelper.mockSubscriber(); + + result.subscribe(subscriber); + + verify(subscriber).onNext(Arrays.asList(1, 1)); + verify(subscriber).onComplete(); + verify(subscriber, never()).onError(any(Throwable.class)); + } + + @Test + public void bufferWithTimeTake1() { + Flowable<Long> source = Flowable.interval(40, 40, TimeUnit.MILLISECONDS, scheduler); + + Flowable<List<Long>> result = source.buffer(100, TimeUnit.MILLISECONDS, scheduler).take(1); + + Subscriber<Object> subscriber = TestHelper.mockSubscriber(); + + result.subscribe(subscriber); + + scheduler.advanceTimeBy(5, TimeUnit.SECONDS); + + verify(subscriber).onNext(Arrays.asList(0L, 1L)); + verify(subscriber).onComplete(); + verify(subscriber, never()).onError(any(Throwable.class)); + } + + @Test + public void bufferWithTimeSkipTake2() { + Flowable<Long> source = Flowable.interval(40, 40, TimeUnit.MILLISECONDS, scheduler); + + Flowable<List<Long>> result = source.buffer(100, 60, TimeUnit.MILLISECONDS, scheduler).take(2); + + Subscriber<Object> subscriber = TestHelper.mockSubscriber(); + InOrder inOrder = inOrder(subscriber); + + result.subscribe(subscriber); + + scheduler.advanceTimeBy(5, TimeUnit.SECONDS); + + inOrder.verify(subscriber).onNext(Arrays.asList(0L, 1L)); + inOrder.verify(subscriber).onNext(Arrays.asList(1L, 2L)); + inOrder.verify(subscriber).onComplete(); + verify(subscriber, never()).onError(any(Throwable.class)); + } + + @Test + public void bufferWithBoundaryTake2() { + Flowable<Long> boundary = Flowable.interval(60, 60, TimeUnit.MILLISECONDS, scheduler); + Flowable<Long> source = Flowable.interval(40, 40, TimeUnit.MILLISECONDS, scheduler); + + Flowable<List<Long>> result = source.buffer(boundary).take(2); + + Subscriber<Object> subscriber = TestHelper.mockSubscriber(); + InOrder inOrder = inOrder(subscriber); + + result.subscribe(subscriber); + + scheduler.advanceTimeBy(5, TimeUnit.SECONDS); + + inOrder.verify(subscriber).onNext(Arrays.asList(0L)); + inOrder.verify(subscriber).onNext(Arrays.asList(1L)); + inOrder.verify(subscriber).onComplete(); + verify(subscriber, never()).onError(any(Throwable.class)); + + } + + @Test + public void bufferWithStartEndBoundaryTake2() { + Flowable<Long> start = Flowable.interval(61, 61, TimeUnit.MILLISECONDS, scheduler); + Function<Long, Flowable<Long>> end = new Function<Long, Flowable<Long>>() { + @Override + public Flowable<Long> apply(Long t1) { + return Flowable.interval(100, 100, TimeUnit.MILLISECONDS, scheduler); + } + }; + + Flowable<Long> source = Flowable.interval(40, 40, TimeUnit.MILLISECONDS, scheduler); + + Flowable<List<Long>> result = source.buffer(start, end).take(2); + + Subscriber<Object> subscriber = TestHelper.mockSubscriber(); + InOrder inOrder = inOrder(subscriber); + + result + .doOnNext(new Consumer<List<Long>>() { + @Override + public void accept(List<Long> pv) { + System.out.println(pv); + } + }) + .subscribe(subscriber); + + scheduler.advanceTimeBy(5, TimeUnit.SECONDS); + + inOrder.verify(subscriber).onNext(Arrays.asList(1L, 2L, 3L)); + inOrder.verify(subscriber).onNext(Arrays.asList(3L, 4L)); + inOrder.verify(subscriber).onComplete(); + verify(subscriber, never()).onError(any(Throwable.class)); + } + + @Test + public void bufferWithSizeThrows() { + PublishProcessor<Integer> source = PublishProcessor.create(); + + Flowable<List<Integer>> result = source.buffer(2); + + Subscriber<Object> subscriber = TestHelper.mockSubscriber(); + + InOrder inOrder = inOrder(subscriber); + + result.subscribe(subscriber); + + source.onNext(1); + source.onNext(2); + source.onNext(3); + source.onError(new TestException()); + + inOrder.verify(subscriber).onNext(Arrays.asList(1, 2)); + inOrder.verify(subscriber).onError(any(TestException.class)); + inOrder.verifyNoMoreInteractions(); + verify(subscriber, never()).onNext(Arrays.asList(3)); + verify(subscriber, never()).onComplete(); + + } + + @Test + public void bufferWithTimeThrows() { + PublishProcessor<Integer> source = PublishProcessor.create(); + + Flowable<List<Integer>> result = source.buffer(100, TimeUnit.MILLISECONDS, scheduler); + + Subscriber<Object> subscriber = TestHelper.mockSubscriber(); + InOrder inOrder = inOrder(subscriber); + + result.subscribe(subscriber); + + source.onNext(1); + source.onNext(2); + scheduler.advanceTimeBy(100, TimeUnit.MILLISECONDS); + source.onNext(3); + source.onError(new TestException()); + scheduler.advanceTimeBy(100, TimeUnit.MILLISECONDS); + + inOrder.verify(subscriber).onNext(Arrays.asList(1, 2)); + inOrder.verify(subscriber).onError(any(TestException.class)); + inOrder.verifyNoMoreInteractions(); + verify(subscriber, never()).onNext(Arrays.asList(3)); + verify(subscriber, never()).onComplete(); + + } + + @Test + public void bufferWithTimeAndSize() { + Flowable<Long> source = Flowable.interval(30, 30, TimeUnit.MILLISECONDS, scheduler); + + Flowable<List<Long>> result = source.buffer(100, TimeUnit.MILLISECONDS, scheduler, 2).take(3); + + Subscriber<Object> subscriber = TestHelper.mockSubscriber(); + InOrder inOrder = inOrder(subscriber); + + result.subscribe(subscriber); + + scheduler.advanceTimeBy(5, TimeUnit.SECONDS); + + inOrder.verify(subscriber).onNext(Arrays.asList(0L, 1L)); + inOrder.verify(subscriber).onNext(Arrays.asList(2L)); + inOrder.verify(subscriber).onComplete(); + verify(subscriber, never()).onError(any(Throwable.class)); + } + + @Test + public void bufferWithStartEndStartThrows() { + PublishProcessor<Integer> start = PublishProcessor.create(); + + Function<Integer, Flowable<Integer>> end = new Function<Integer, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Integer t1) { + return Flowable.never(); + } + }; + + PublishProcessor<Integer> source = PublishProcessor.create(); + + Flowable<List<Integer>> result = source.buffer(start, end); + + Subscriber<Object> subscriber = TestHelper.mockSubscriber(); + + result.subscribe(subscriber); + + start.onNext(1); + source.onNext(1); + source.onNext(2); + start.onError(new TestException()); + + verify(subscriber, never()).onNext(any()); + verify(subscriber, never()).onComplete(); + verify(subscriber).onError(any(TestException.class)); + } + + @Test + public void bufferWithStartEndEndFunctionThrows() { + PublishProcessor<Integer> start = PublishProcessor.create(); + + Function<Integer, Flowable<Integer>> end = new Function<Integer, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Integer t1) { + throw new TestException(); + } + }; + + PublishProcessor<Integer> source = PublishProcessor.create(); + + Flowable<List<Integer>> result = source.buffer(start, end); + + Subscriber<Object> subscriber = TestHelper.mockSubscriber(); + + result.subscribe(subscriber); + + start.onNext(1); + source.onNext(1); + source.onNext(2); + + verify(subscriber, never()).onNext(any()); + verify(subscriber, never()).onComplete(); + verify(subscriber).onError(any(TestException.class)); + } + + @Test + public void bufferWithStartEndEndThrows() { + PublishProcessor<Integer> start = PublishProcessor.create(); + + Function<Integer, Flowable<Integer>> end = new Function<Integer, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Integer t1) { + return Flowable.error(new TestException()); + } + }; + + PublishProcessor<Integer> source = PublishProcessor.create(); + + Flowable<List<Integer>> result = source.buffer(start, end); + + Subscriber<Object> subscriber = TestHelper.mockSubscriber(); + + result.subscribe(subscriber); + + start.onNext(1); + source.onNext(1); + source.onNext(2); + + verify(subscriber, never()).onNext(any()); + verify(subscriber, never()).onComplete(); + verify(subscriber).onError(any(TestException.class)); + } + + @Test + public void producerRequestThroughBufferWithSize1() { + TestSubscriber<List<Integer>> ts = new TestSubscriber<>(3L); + + final AtomicLong requested = new AtomicLong(); + Flowable.unsafeCreate(new Publisher<Integer>() { + + @Override + public void subscribe(Subscriber<? super Integer> s) { + s.onSubscribe(new Subscription() { + + @Override + public void request(long n) { + requested.set(n); + } + + @Override + public void cancel() { + + } + + }); + } + + }).buffer(5, 5).subscribe(ts); + assertEquals(15, requested.get()); + + ts.request(4); + assertEquals(20, requested.get()); + } + + @Test + public void producerRequestThroughBufferWithSize2() { + TestSubscriber<List<Integer>> ts = new TestSubscriber<>(); + final AtomicLong requested = new AtomicLong(); + + Flowable.unsafeCreate(new Publisher<Integer>() { + + @Override + public void subscribe(Subscriber<? super Integer> s) { + s.onSubscribe(new Subscription() { + + @Override + public void request(long n) { + requested.set(n); + } + + @Override + public void cancel() { + + } + + }); + } + + }).buffer(5, 5).subscribe(ts); + assertEquals(Long.MAX_VALUE, requested.get()); + } + + @Test + public void producerRequestThroughBufferWithSize3() { + TestSubscriber<List<Integer>> ts = new TestSubscriber<>(3L); + final AtomicLong requested = new AtomicLong(); + Flowable.unsafeCreate(new Publisher<Integer>() { + + @Override + public void subscribe(Subscriber<? super Integer> s) { + s.onSubscribe(new Subscription() { + + @Override + public void request(long n) { + requested.set(n); + } + + @Override + public void cancel() { + + } + + }); + } + + }).buffer(5, 2).subscribe(ts); + assertEquals(9, requested.get()); + ts.request(3); + assertEquals(6, requested.get()); + } + + @Test + public void producerRequestThroughBufferWithSize4() { + TestSubscriber<List<Integer>> ts = new TestSubscriber<>(); + final AtomicLong requested = new AtomicLong(); + Flowable.unsafeCreate(new Publisher<Integer>() { + + @Override + public void subscribe(Subscriber<? super Integer> s) { + s.onSubscribe(new Subscription() { + + @Override + public void request(long n) { + requested.set(n); + } + + @Override + public void cancel() { + + } + + }); + } + + }).buffer(5, 2).subscribe(ts); + assertEquals(Long.MAX_VALUE, requested.get()); + } + + @Test + public void producerRequestOverflowThroughBufferWithSize1() { + TestSubscriber<List<Integer>> ts = new TestSubscriber<>(Long.MAX_VALUE >> 1); + + final AtomicLong requested = new AtomicLong(); + + Flowable.unsafeCreate(new Publisher<Integer>() { + + @Override + public void subscribe(Subscriber<? super Integer> s) { + s.onSubscribe(new Subscription() { + + @Override + public void request(long n) { + requested.set(n); + } + + @Override + public void cancel() { + + } + + }); + } + + }).buffer(3, 3).subscribe(ts); + assertEquals(Long.MAX_VALUE, requested.get()); + } + + @Test + public void producerRequestOverflowThroughBufferWithSize2() { + TestSubscriber<List<Integer>> ts = new TestSubscriber<>(Long.MAX_VALUE >> 1); + + final AtomicLong requested = new AtomicLong(); + + Flowable.unsafeCreate(new Publisher<Integer>() { + + @Override + public void subscribe(Subscriber<? super Integer> s) { + s.onSubscribe(new Subscription() { + + @Override + public void request(long n) { + requested.set(n); + } + + @Override + public void cancel() { + + } + + }); + } + + }).buffer(3, 2).subscribe(ts); + assertEquals(Long.MAX_VALUE, requested.get()); + } + + @Test + public void producerRequestOverflowThroughBufferWithSize3() { + final AtomicLong requested = new AtomicLong(); + Flowable.unsafeCreate(new Publisher<Integer>() { + + @Override + public void subscribe(final Subscriber<? super Integer> s) { + s.onSubscribe(new Subscription() { + AtomicBoolean once = new AtomicBoolean(); + @Override + public void request(long n) { + requested.set(n); + if (once.compareAndSet(false, true)) { + s.onNext(1); + s.onNext(2); + s.onNext(3); + } + } + + @Override + public void cancel() { + + } + + }); + } + + }).buffer(3, 2).subscribe(new DefaultSubscriber<List<Integer>>() { + + @Override + public void onStart() { + request(Long.MAX_VALUE / 2 - 4); + } + + @Override + public void onComplete() { + } + + @Override + public void onError(Throwable e) { + } + + @Override + public void onNext(List<Integer> t) { + request(Long.MAX_VALUE / 2); + } + + }); + // FIXME I'm not sure why this is MAX_VALUE in 1.x because MAX_VALUE/2 is even and thus can't overflow when multiplied by 2 + assertEquals(Long.MAX_VALUE - 1, requested.get()); + } + + @Test + public void bufferWithTimeDoesntUnsubscribeDownstream() throws InterruptedException { + final Subscriber<Object> subscriber = TestHelper.mockSubscriber(); + + final CountDownLatch cdl = new CountDownLatch(1); + ResourceSubscriber<Object> s = new ResourceSubscriber<Object>() { + @Override + public void onNext(Object t) { + subscriber.onNext(t); + } + + @Override + public void onError(Throwable e) { + subscriber.onError(e); + cdl.countDown(); + } + + @Override + public void onComplete() { + subscriber.onComplete(); + cdl.countDown(); + } + }; + + Flowable.range(1, 1).delay(1, TimeUnit.SECONDS).buffer(2, TimeUnit.SECONDS).subscribe(s); + + cdl.await(); + + verify(subscriber).onNext(Arrays.asList(1)); + verify(subscriber).onComplete(); + verify(subscriber, never()).onError(any(Throwable.class)); + + assertFalse(s.isDisposed()); + } + + @Test + public void postCompleteBackpressure() { + Flowable<List<Integer>> source = Flowable.range(1, 10).buffer(3, 1); + + TestSubscriber<List<Integer>> ts = TestSubscriber.create(0L); + + source.subscribe(ts); + + ts.assertNoValues(); + ts.assertNotComplete(); + ts.assertNoErrors(); + + ts.request(7); + + ts.assertValues( + Arrays.asList(1, 2, 3), + Arrays.asList(2, 3, 4), + Arrays.asList(3, 4, 5), + Arrays.asList(4, 5, 6), + Arrays.asList(5, 6, 7), + Arrays.asList(6, 7, 8), + Arrays.asList(7, 8, 9) + ); + ts.assertNotComplete(); + ts.assertNoErrors(); + + ts.request(1); + + ts.assertValues( + Arrays.asList(1, 2, 3), + Arrays.asList(2, 3, 4), + Arrays.asList(3, 4, 5), + Arrays.asList(4, 5, 6), + Arrays.asList(5, 6, 7), + Arrays.asList(6, 7, 8), + Arrays.asList(7, 8, 9), + Arrays.asList(8, 9, 10) + ); + ts.assertNotComplete(); + ts.assertNoErrors(); + + ts.request(1); + + ts.assertValues( + Arrays.asList(1, 2, 3), + Arrays.asList(2, 3, 4), + Arrays.asList(3, 4, 5), + Arrays.asList(4, 5, 6), + Arrays.asList(5, 6, 7), + Arrays.asList(6, 7, 8), + Arrays.asList(7, 8, 9), + Arrays.asList(8, 9, 10), + Arrays.asList(9, 10) + ); + ts.assertNotComplete(); + ts.assertNoErrors(); + + ts.request(1); + + ts.assertValues( + Arrays.asList(1, 2, 3), + Arrays.asList(2, 3, 4), + Arrays.asList(3, 4, 5), + Arrays.asList(4, 5, 6), + Arrays.asList(5, 6, 7), + Arrays.asList(6, 7, 8), + Arrays.asList(7, 8, 9), + Arrays.asList(8, 9, 10), + Arrays.asList(9, 10), + Arrays.asList(10) + ); + ts.assertComplete(); + ts.assertNoErrors(); + } + + @Test + public void timeAndSkipOverlap() { + + PublishProcessor<Integer> pp = PublishProcessor.create(); + + TestSubscriber<List<Integer>> ts = TestSubscriber.create(); + + pp.buffer(2, 1, TimeUnit.SECONDS, scheduler).subscribe(ts); + + pp.onNext(1); + + scheduler.advanceTimeBy(1, TimeUnit.SECONDS); + + pp.onNext(2); + + scheduler.advanceTimeBy(1, TimeUnit.SECONDS); + + pp.onNext(3); + + scheduler.advanceTimeBy(1, TimeUnit.SECONDS); + + pp.onNext(4); + + scheduler.advanceTimeBy(1, TimeUnit.SECONDS); + + pp.onComplete(); + + ts.assertValues( + Arrays.asList(1, 2), + Arrays.asList(2, 3), + Arrays.asList(3, 4), + Arrays.asList(4), + Collections.<Integer>emptyList() + ); + + ts.assertNoErrors(); + ts.assertComplete(); + } + + @Test + public void timeAndSkipSkip() { + + PublishProcessor<Integer> pp = PublishProcessor.create(); + + TestSubscriber<List<Integer>> ts = TestSubscriber.create(); + + pp.buffer(2, 3, TimeUnit.SECONDS, scheduler).subscribe(ts); + + pp.onNext(1); + + scheduler.advanceTimeBy(1, TimeUnit.SECONDS); + + pp.onNext(2); + + scheduler.advanceTimeBy(1, TimeUnit.SECONDS); + + pp.onNext(3); + + scheduler.advanceTimeBy(1, TimeUnit.SECONDS); + + pp.onNext(4); + + scheduler.advanceTimeBy(1, TimeUnit.SECONDS); + + pp.onComplete(); + + ts.assertValues( + Arrays.asList(1, 2), + Arrays.asList(4) + ); + + ts.assertNoErrors(); + ts.assertComplete(); + } + + @Test + public void timeAndSkipOverlapScheduler() { + + RxJavaPlugins.setComputationSchedulerHandler(new Function<Scheduler, Scheduler>() { + @Override + public Scheduler apply(Scheduler t) { + return scheduler; + } + }); + + try { + PublishProcessor<Integer> pp = PublishProcessor.create(); + + TestSubscriber<List<Integer>> ts = TestSubscriber.create(); + + pp.buffer(2, 1, TimeUnit.SECONDS).subscribe(ts); + + pp.onNext(1); + + scheduler.advanceTimeBy(1, TimeUnit.SECONDS); + + pp.onNext(2); + + scheduler.advanceTimeBy(1, TimeUnit.SECONDS); + + pp.onNext(3); + + scheduler.advanceTimeBy(1, TimeUnit.SECONDS); + + pp.onNext(4); + + scheduler.advanceTimeBy(1, TimeUnit.SECONDS); + + pp.onComplete(); + + ts.assertValues( + Arrays.asList(1, 2), + Arrays.asList(2, 3), + Arrays.asList(3, 4), + Arrays.asList(4), + Collections.<Integer>emptyList() + ); + + ts.assertNoErrors(); + ts.assertComplete(); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void timeAndSkipSkipDefaultScheduler() { + RxJavaPlugins.setComputationSchedulerHandler(new Function<Scheduler, Scheduler>() { + @Override + public Scheduler apply(Scheduler t) { + return scheduler; + } + }); + + try { + + PublishProcessor<Integer> pp = PublishProcessor.create(); + + TestSubscriber<List<Integer>> ts = TestSubscriber.create(); + + pp.buffer(2, 3, TimeUnit.SECONDS).subscribe(ts); + + pp.onNext(1); + + scheduler.advanceTimeBy(1, TimeUnit.SECONDS); + + pp.onNext(2); + + scheduler.advanceTimeBy(1, TimeUnit.SECONDS); + + pp.onNext(3); + + scheduler.advanceTimeBy(1, TimeUnit.SECONDS); + + pp.onNext(4); + + scheduler.advanceTimeBy(1, TimeUnit.SECONDS); + + pp.onComplete(); + + ts.assertValues( + Arrays.asList(1, 2), + Arrays.asList(4) + ); + + ts.assertNoErrors(); + ts.assertComplete(); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void bufferBoundaryHint() { + Flowable.range(1, 5).buffer(Flowable.timer(1, TimeUnit.MINUTES), 2) + .test() + .assertResult(Arrays.asList(1, 2, 3, 4, 5)); + } + + static HashSet<Integer> set(Integer... values) { + return new HashSet<>(Arrays.asList(values)); + } + + @Test + public void bufferIntoCustomCollection() { + Flowable.just(1, 1, 2, 2, 3, 3, 4, 4) + .buffer(3, new Supplier<Collection<Integer>>() { + @Override + public Collection<Integer> get() throws Exception { + return new HashSet<>(); + } + }) + .test() + .assertResult(set(1, 2), set(2, 3), set(4)); + } + + @Test + public void bufferSkipIntoCustomCollection() { + Flowable.just(1, 1, 2, 2, 3, 3, 4, 4) + .buffer(3, 3, new Supplier<Collection<Integer>>() { + @Override + public Collection<Integer> get() throws Exception { + return new HashSet<>(); + } + }) + .test() + .assertResult(set(1, 2), set(2, 3), set(4)); + } + + @Test + public void bufferTimeSkipDefault() { + Flowable.range(1, 5).buffer(1, 1, TimeUnit.MINUTES) + .test() + .assertResult(Arrays.asList(1, 2, 3, 4, 5)); + } + + @Test + public void dispose() { + TestHelper.checkDisposed(Flowable.range(1, 5).buffer(1, TimeUnit.DAYS, Schedulers.single())); + + TestHelper.checkDisposed(Flowable.range(1, 5).buffer(2, 1, TimeUnit.DAYS, Schedulers.single())); + + TestHelper.checkDisposed(Flowable.range(1, 5).buffer(1, 2, TimeUnit.DAYS, Schedulers.single())); + + TestHelper.checkDisposed(Flowable.range(1, 5) + .buffer(1, TimeUnit.DAYS, Schedulers.single(), 2, Functions.<Integer>createArrayList(16), true)); + + TestHelper.checkDisposed(Flowable.range(1, 5).buffer(1)); + + TestHelper.checkDisposed(Flowable.range(1, 5).buffer(2, 1)); + + TestHelper.checkDisposed(Flowable.range(1, 5).buffer(1, 2)); + } + + @Test + public void supplierReturnsNull() { + Flowable.<Integer>never() + .buffer(1, TimeUnit.MILLISECONDS, Schedulers.single(), Integer.MAX_VALUE, new Supplier<Collection<Integer>>() { + int count; + @Override + public Collection<Integer> get() throws Exception { + if (count++ == 1) { + return null; + } else { + return new ArrayList<>(); + } + } + }, false) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertFailure(NullPointerException.class); + } + + @Test + public void supplierReturnsNull2() { + Flowable.<Integer>never() + .buffer(1, TimeUnit.MILLISECONDS, Schedulers.single(), 10, new Supplier<Collection<Integer>>() { + int count; + @Override + public Collection<Integer> get() throws Exception { + if (count++ == 1) { + return null; + } else { + return new ArrayList<>(); + } + } + }, false) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertFailure(NullPointerException.class); + } + + @Test + public void supplierReturnsNull3() { + Flowable.<Integer>never() + .buffer(2, 1, TimeUnit.MILLISECONDS, Schedulers.single(), new Supplier<Collection<Integer>>() { + int count; + @Override + public Collection<Integer> get() throws Exception { + if (count++ == 1) { + return null; + } else { + return new ArrayList<>(); + } + } + }) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertFailure(NullPointerException.class); + } + + @Test + public void supplierThrows() { + Flowable.just(1) + .buffer(1, TimeUnit.SECONDS, Schedulers.single(), Integer.MAX_VALUE, new Supplier<Collection<Integer>>() { + @Override + public Collection<Integer> get() throws Exception { + throw new TestException(); + } + }, false) + .test() + .assertFailure(TestException.class); + } + + @Test + public void supplierThrows2() { + Flowable.just(1) + .buffer(1, TimeUnit.SECONDS, Schedulers.single(), 10, new Supplier<Collection<Integer>>() { + @Override + public Collection<Integer> get() throws Exception { + throw new TestException(); + } + }, false) + .test() + .assertFailure(TestException.class); + } + + @Test + public void supplierThrows3() { + Flowable.just(1) + .buffer(2, 1, TimeUnit.SECONDS, Schedulers.single(), new Supplier<Collection<Integer>>() { + @Override + public Collection<Integer> get() throws Exception { + throw new TestException(); + } + }) + .test() + .assertFailure(TestException.class); + } + + @Test + public void supplierThrows4() { + Flowable.<Integer>never() + .buffer(1, TimeUnit.MILLISECONDS, Schedulers.single(), Integer.MAX_VALUE, new Supplier<Collection<Integer>>() { + int count; + @Override + public Collection<Integer> get() throws Exception { + if (count++ == 1) { + throw new TestException(); + } else { + return new ArrayList<>(); + } + } + }, false) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertFailure(TestException.class); + } + + @Test + public void supplierThrows5() { + Flowable.<Integer>never() + .buffer(1, TimeUnit.MILLISECONDS, Schedulers.single(), 10, new Supplier<Collection<Integer>>() { + int count; + @Override + public Collection<Integer> get() throws Exception { + if (count++ == 1) { + throw new TestException(); + } else { + return new ArrayList<>(); + } + } + }, false) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertFailure(TestException.class); + } + + @Test + public void supplierThrows6() { + Flowable.<Integer>never() + .buffer(2, 1, TimeUnit.MILLISECONDS, Schedulers.single(), new Supplier<Collection<Integer>>() { + int count; + @Override + public Collection<Integer> get() throws Exception { + if (count++ == 1) { + throw new TestException(); + } else { + return new ArrayList<>(); + } + } + }) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertFailure(TestException.class); + } + + @Test + public void restartTimer() { + Flowable.range(1, 5) + .buffer(1, TimeUnit.DAYS, Schedulers.single(), 2, Functions.<Integer>createArrayList(16), true) + .test() + .assertResult(Arrays.asList(1, 2), Arrays.asList(3, 4), Arrays.asList(5)); + } + + @Test + public void bufferSkipError() { + Flowable.<Integer>error(new TestException()) + .buffer(2, 1) + .test() + .assertFailure(TestException.class); + } + + @Test + public void bufferSupplierCrash2() { + Flowable.range(1, 2) + .buffer(1, new Supplier<List<Integer>>() { + int calls; + @Override + public List<Integer> get() throws Exception { + if (++calls == 2) { + throw new TestException(); + } + return new ArrayList<>(); + } + }) + .test() + .assertFailure(TestException.class, Arrays.asList(1)); + } + + @Test + public void bufferSkipSupplierCrash2() { + Flowable.range(1, 2) + .buffer(1, 2, new Supplier<List<Integer>>() { + int calls; + @Override + public List<Integer> get() throws Exception { + if (++calls == 1) { + throw new TestException(); + } + return new ArrayList<>(); + } + }) + .test() + .assertFailure(TestException.class); + } + + @Test + public void bufferOverlapSupplierCrash2() { + Flowable.range(1, 2) + .buffer(2, 1, new Supplier<List<Integer>>() { + int calls; + @Override + public List<Integer> get() throws Exception { + if (++calls == 2) { + throw new TestException(); + } + return new ArrayList<>(); + } + }) + .test() + .assertFailure(TestException.class); + } + + @Test + public void bufferSkipOverlap() { + Flowable.range(1, 5) + .buffer(5, 1) + .test() + .assertResult( + Arrays.asList(1, 2, 3, 4, 5), + Arrays.asList(2, 3, 4, 5), + Arrays.asList(3, 4, 5), + Arrays.asList(4, 5), + Arrays.asList(5) + ); + } + + @Test + public void bufferTimedExactError() { + Flowable.error(new TestException()) + .buffer(1, TimeUnit.DAYS) + .test() + .assertFailure(TestException.class); + } + + @Test + public void bufferTimedSkipError() { + Flowable.error(new TestException()) + .buffer(1, 2, TimeUnit.DAYS) + .test() + .assertFailure(TestException.class); + } + + @Test + public void bufferTimedOverlapError() { + Flowable.error(new TestException()) + .buffer(2, 1, TimeUnit.DAYS) + .test() + .assertFailure(TestException.class); + } + + @Test + public void bufferTimedExactEmpty() { + Flowable.empty() + .buffer(1, TimeUnit.DAYS) + .test() + .assertResult(Collections.emptyList()); + } + + @Test + public void bufferTimedSkipEmpty() { + Flowable.empty() + .buffer(1, 2, TimeUnit.DAYS) + .test() + .assertResult(Collections.emptyList()); + } + + @Test + public void bufferTimedOverlapEmpty() { + Flowable.empty() + .buffer(2, 1, TimeUnit.DAYS) + .test() + .assertResult(Collections.emptyList()); + } + + @Test + public void bufferTimedExactSupplierCrash() { + TestScheduler scheduler = new TestScheduler(); + + PublishProcessor<Integer> pp = PublishProcessor.create(); + + TestSubscriber<List<Integer>> ts = pp + .buffer(1, TimeUnit.MILLISECONDS, scheduler, 1, new Supplier<List<Integer>>() { + int calls; + @Override + public List<Integer> get() throws Exception { + if (++calls == 2) { + throw new TestException(); + } + return new ArrayList<>(); + } + }, true) + .test(); + + pp.onNext(1); + + scheduler.advanceTimeBy(1, TimeUnit.MILLISECONDS); + + pp.onNext(2); + + ts + .assertFailure(TestException.class, Arrays.asList(1)); + } + + @Test + public void bufferTimedExactBoundedError() { + TestScheduler scheduler = new TestScheduler(); + + PublishProcessor<Integer> pp = PublishProcessor.create(); + + TestSubscriber<List<Integer>> ts = pp + .buffer(1, TimeUnit.MILLISECONDS, scheduler, 1, Functions.<Integer>createArrayList(16), true) + .test(); + + pp.onError(new TestException()); + + ts + .assertFailure(TestException.class); + } + + @Test + public void badSource() { + TestHelper.checkBadSourceFlowable(new Function<Flowable<Integer>, Object>() { + @Override + public Object apply(Flowable<Integer> f) throws Exception { + return f.buffer(1); + } + }, false, 1, 1, Arrays.asList(1)); + + TestHelper.checkBadSourceFlowable(new Function<Flowable<Integer>, Object>() { + @Override + public Object apply(Flowable<Integer> f) throws Exception { + return f.buffer(1, 2); + } + }, false, 1, 1, Arrays.asList(1)); + + TestHelper.checkBadSourceFlowable(new Function<Flowable<Integer>, Object>() { + @Override + public Object apply(Flowable<Integer> f) throws Exception { + return f.buffer(2, 1); + } + }, false, 1, 1, Arrays.asList(1)); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeFlowable(new Function<Flowable<Object>, Publisher<List<Object>>>() { + @Override + public Publisher<List<Object>> apply(Flowable<Object> f) throws Exception { + return f.buffer(1); + } + }); + + TestHelper.checkDoubleOnSubscribeFlowable(new Function<Flowable<Object>, Publisher<List<Object>>>() { + @Override + public Publisher<List<Object>> apply(Flowable<Object> f) throws Exception { + return f.buffer(1, 2); + } + }); + + TestHelper.checkDoubleOnSubscribeFlowable(new Function<Flowable<Object>, Publisher<List<Object>>>() { + @Override + public Publisher<List<Object>> apply(Flowable<Object> f) throws Exception { + return f.buffer(2, 1); + } + }); + } + + @Test + public void badRequest() { + TestHelper.assertBadRequestReported(PublishProcessor.create().buffer(1)); + + TestHelper.assertBadRequestReported(PublishProcessor.create().buffer(1, 2)); + + TestHelper.assertBadRequestReported(PublishProcessor.create().buffer(2, 1)); + } + + @Test + public void skipError() { + Flowable.error(new TestException()) + .buffer(1, 2) + .test() + .assertFailure(TestException.class); + } + + @Test + public void skipSingleResult() { + Flowable.just(1) + .buffer(2, 3) + .test() + .assertResult(Arrays.asList(1)); + } + + @Test + public void skipBackpressure() { + Flowable.range(1, 10) + .buffer(2, 3) + .rebatchRequests(1) + .test() + .assertResult(Arrays.asList(1, 2), Arrays.asList(4, 5), Arrays.asList(7, 8), Arrays.asList(10)); + } + + @Test + public void withTimeAndSizeCapacityRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final TestScheduler scheduler = new TestScheduler(); + + final PublishProcessor<Object> pp = PublishProcessor.create(); + + TestSubscriber<List<Object>> ts = pp.buffer(1, TimeUnit.SECONDS, scheduler, 5).test(); + + pp.onNext(1); + pp.onNext(2); + pp.onNext(3); + pp.onNext(4); + + Runnable r1 = new Runnable() { + @Override + public void run() { + pp.onNext(5); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + scheduler.advanceTimeBy(1, TimeUnit.SECONDS); + } + }; + + TestHelper.race(r1, r2); + + pp.onComplete(); + + int items = 0; + for (List<Object> o : ts.values()) { + items += o.size(); + } + + assertEquals("Round: " + i, 5, items); + } + } + + @Test + public void noCompletionCancelExact() { + final AtomicInteger counter = new AtomicInteger(); + + Flowable.<Integer>empty() + .doOnCancel(new Action() { + @Override + public void run() throws Exception { + counter.getAndIncrement(); + } + }) + .buffer(5, TimeUnit.SECONDS) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(Collections.<Integer>emptyList()); + + assertEquals(0, counter.get()); + } + + @Test + public void noCompletionCancelSkip() { + final AtomicInteger counter = new AtomicInteger(); + + Flowable.<Integer>empty() + .doOnCancel(new Action() { + @Override + public void run() throws Exception { + counter.getAndIncrement(); + } + }) + .buffer(5, 10, TimeUnit.SECONDS) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(Collections.<Integer>emptyList()); + + assertEquals(0, counter.get()); + } + + @Test + public void noCompletionCancelOverlap() { + final AtomicInteger counter = new AtomicInteger(); + + Flowable.<Integer>empty() + .doOnCancel(new Action() { + @Override + public void run() throws Exception { + counter.getAndIncrement(); + } + }) + .buffer(10, 5, TimeUnit.SECONDS) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(Collections.<Integer>emptyList()); + + assertEquals(0, counter.get()); + } + + @Test + public void boundaryOpenCloseDisposedOnComplete() { + PublishProcessor<Integer> source = PublishProcessor.create(); + + PublishProcessor<Integer> openIndicator = PublishProcessor.create(); + + PublishProcessor<Integer> closeIndicator = PublishProcessor.create(); + + TestSubscriber<List<Integer>> ts = source + .buffer(openIndicator, Functions.justFunction(closeIndicator)) + .test(); + + assertTrue(source.hasSubscribers()); + assertTrue(openIndicator.hasSubscribers()); + assertFalse(closeIndicator.hasSubscribers()); + + openIndicator.onNext(1); + + assertTrue(openIndicator.hasSubscribers()); + assertTrue(closeIndicator.hasSubscribers()); + + source.onComplete(); + + ts.assertResult(Collections.<Integer>emptyList()); + + assertFalse(openIndicator.hasSubscribers()); + assertFalse(closeIndicator.hasSubscribers()); + } + + @Test + public void bufferedCanCompleteIfOpenNeverCompletesDropping() { + Flowable.range(1, 50) + .zipWith(Flowable.interval(5, TimeUnit.MILLISECONDS), + new BiFunction<Integer, Long, Integer>() { + @Override + public Integer apply(Integer integer, Long aLong) { + return integer; + } + }) + .buffer(Flowable.interval(0, 200, TimeUnit.MILLISECONDS), + new Function<Long, Publisher<?>>() { + @Override + public Publisher<?> apply(Long a) { + return Flowable.just(a).delay(100, TimeUnit.MILLISECONDS); + } + }) + .to(TestHelper.<List<Integer>>testConsumer()) + .assertSubscribed() + .awaitDone(3, TimeUnit.SECONDS) + .assertComplete(); + } + + @Test + public void bufferedCanCompleteIfOpenNeverCompletesOverlapping() { + Flowable.range(1, 50) + .zipWith(Flowable.interval(5, TimeUnit.MILLISECONDS), + new BiFunction<Integer, Long, Integer>() { + @Override + public Integer apply(Integer integer, Long aLong) { + return integer; + } + }) + .buffer(Flowable.interval(0, 100, TimeUnit.MILLISECONDS), + new Function<Long, Publisher<?>>() { + @Override + public Publisher<?> apply(Long a) { + return Flowable.just(a).delay(200, TimeUnit.MILLISECONDS); + } + }) + .to(TestHelper.<List<Integer>>testConsumer()) + .assertSubscribed() + .awaitDone(3, TimeUnit.SECONDS) + .assertComplete(); + } + + @Test + public void openClosemainError() { + Flowable.error(new TestException()) + .buffer(Flowable.never(), Functions.justFunction(Flowable.never())) + .test() + .assertFailure(TestException.class); + } + + @Test + public void openClosebadSource() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + new Flowable<Object>() { + @Override + protected void subscribeActual(Subscriber<? super Object> s) { + BooleanSubscription bs1 = new BooleanSubscription(); + BooleanSubscription bs2 = new BooleanSubscription(); + + s.onSubscribe(bs1); + + assertFalse(bs1.isCancelled()); + assertFalse(bs2.isCancelled()); + + s.onSubscribe(bs2); + + assertFalse(bs1.isCancelled()); + assertTrue(bs2.isCancelled()); + + s.onError(new IOException()); + s.onComplete(); + s.onNext(1); + s.onError(new TestException()); + } + } + .buffer(Flowable.never(), Functions.justFunction(Flowable.never())) + .test() + .assertFailure(IOException.class); + + TestHelper.assertError(errors, 0, ProtocolViolationException.class); + TestHelper.assertUndeliverable(errors, 1, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void openCloseOpenCompletes() { + PublishProcessor<Integer> source = PublishProcessor.create(); + + PublishProcessor<Integer> openIndicator = PublishProcessor.create(); + + PublishProcessor<Integer> closeIndicator = PublishProcessor.create(); + + TestSubscriber<List<Integer>> ts = source + .buffer(openIndicator, Functions.justFunction(closeIndicator)) + .test(); + + openIndicator.onNext(1); + + assertTrue(closeIndicator.hasSubscribers()); + + openIndicator.onComplete(); + + assertTrue(source.hasSubscribers()); + assertTrue(closeIndicator.hasSubscribers()); + + closeIndicator.onComplete(); + + assertFalse(source.hasSubscribers()); + + ts.assertResult(Collections.<Integer>emptyList()); + } + + @Test + public void openCloseOpenCompletesNoBuffers() { + PublishProcessor<Integer> source = PublishProcessor.create(); + + PublishProcessor<Integer> openIndicator = PublishProcessor.create(); + + PublishProcessor<Integer> closeIndicator = PublishProcessor.create(); + + TestSubscriber<List<Integer>> ts = source + .buffer(openIndicator, Functions.justFunction(closeIndicator)) + .test(); + + openIndicator.onNext(1); + + assertTrue(closeIndicator.hasSubscribers()); + + closeIndicator.onComplete(); + + assertTrue(source.hasSubscribers()); + assertTrue(openIndicator.hasSubscribers()); + + openIndicator.onComplete(); + + assertFalse(source.hasSubscribers()); + + ts.assertResult(Collections.<Integer>emptyList()); + } + + @Test + public void openCloseTake() { + PublishProcessor<Integer> source = PublishProcessor.create(); + + PublishProcessor<Integer> openIndicator = PublishProcessor.create(); + + PublishProcessor<Integer> closeIndicator = PublishProcessor.create(); + + TestSubscriber<List<Integer>> ts = source + .buffer(openIndicator, Functions.justFunction(closeIndicator)) + .take(1) + .test(2); + + openIndicator.onNext(1); + closeIndicator.onComplete(); + + assertFalse(source.hasSubscribers()); + assertFalse(openIndicator.hasSubscribers()); + assertFalse(closeIndicator.hasSubscribers()); + + ts.assertResult(Collections.<Integer>emptyList()); + } + + @Test + public void openCloseEmptyBackpressure() { + PublishProcessor<Integer> source = PublishProcessor.create(); + + PublishProcessor<Integer> openIndicator = PublishProcessor.create(); + + PublishProcessor<Integer> closeIndicator = PublishProcessor.create(); + + TestSubscriber<List<Integer>> ts = source + .buffer(openIndicator, Functions.justFunction(closeIndicator)) + .test(0); + + source.onComplete(); + + assertFalse(openIndicator.hasSubscribers()); + assertFalse(closeIndicator.hasSubscribers()); + + ts.assertResult(); + } + + @Test + public void openCloseErrorBackpressure() { + PublishProcessor<Integer> source = PublishProcessor.create(); + + PublishProcessor<Integer> openIndicator = PublishProcessor.create(); + + PublishProcessor<Integer> closeIndicator = PublishProcessor.create(); + + TestSubscriber<List<Integer>> ts = source + .buffer(openIndicator, Functions.justFunction(closeIndicator)) + .test(0); + + source.onError(new TestException()); + + assertFalse(openIndicator.hasSubscribers()); + assertFalse(closeIndicator.hasSubscribers()); + + ts.assertFailure(TestException.class); + } + + @Test + public void openCloseBadOpen() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + Flowable.never() + .buffer(new Flowable<Object>() { + @Override + protected void subscribeActual(Subscriber<? super Object> s) { + + assertFalse(((Disposable)s).isDisposed()); + + BooleanSubscription bs1 = new BooleanSubscription(); + BooleanSubscription bs2 = new BooleanSubscription(); + + s.onSubscribe(bs1); + + assertFalse(bs1.isCancelled()); + assertFalse(bs2.isCancelled()); + + s.onSubscribe(bs2); + + assertFalse(bs1.isCancelled()); + assertTrue(bs2.isCancelled()); + + s.onError(new IOException()); + + assertTrue(((Disposable)s).isDisposed()); + + s.onComplete(); + s.onNext(1); + s.onError(new TestException()); + } + }, Functions.justFunction(Flowable.never())) + .test() + .assertFailure(IOException.class); + + TestHelper.assertError(errors, 0, ProtocolViolationException.class); + TestHelper.assertUndeliverable(errors, 1, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void openCloseBadClose() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + Flowable.never() + .buffer(Flowable.just(1).concatWith(Flowable.<Integer>never()), + Functions.justFunction(new Flowable<Object>() { + @Override + protected void subscribeActual(Subscriber<? super Object> s) { + + assertFalse(((Disposable)s).isDisposed()); + + BooleanSubscription bs1 = new BooleanSubscription(); + BooleanSubscription bs2 = new BooleanSubscription(); + + s.onSubscribe(bs1); + + assertFalse(bs1.isCancelled()); + assertFalse(bs2.isCancelled()); + + s.onSubscribe(bs2); + + assertFalse(bs1.isCancelled()); + assertTrue(bs2.isCancelled()); + + s.onError(new IOException()); + + assertTrue(((Disposable)s).isDisposed()); + + s.onComplete(); + s.onNext(1); + s.onError(new TestException()); + } + })) + .test() + .assertFailure(IOException.class); + + TestHelper.assertError(errors, 0, ProtocolViolationException.class); + TestHelper.assertUndeliverable(errors, 1, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void bufferExactBoundaryDoubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeFlowable( + new Function<Flowable<Object>, Flowable<List<Object>>>() { + @Override + public Flowable<List<Object>> apply(Flowable<Object> f) + throws Exception { + return f.buffer(Flowable.never()); + } + } + ); + } + + @Test + public void bufferExactBoundarySecondBufferCrash() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + PublishProcessor<Integer> b = PublishProcessor.create(); + + TestSubscriber<List<Integer>> ts = pp.buffer(b, new Supplier<List<Integer>>() { + int calls; + @Override + public List<Integer> get() throws Exception { + if (++calls == 2) { + throw new TestException(); + } + return new ArrayList<>(); + } + }).test(); + + b.onNext(1); + + ts.assertFailure(TestException.class); + } + + @Test + public void bufferExactBoundaryBadSource() { + Flowable<Integer> pp = new Flowable<Integer>() { + @Override + protected void subscribeActual(Subscriber<? super Integer> subscriber) { + subscriber.onSubscribe(new BooleanSubscription()); + subscriber.onComplete(); + subscriber.onNext(1); + subscriber.onComplete(); + } + }; + + final AtomicReference<Subscriber<? super Integer>> ref = new AtomicReference<>(); + Flowable<Integer> b = new Flowable<Integer>() { + @Override + protected void subscribeActual(Subscriber<? super Integer> subscriber) { + subscriber.onSubscribe(new BooleanSubscription()); + ref.set(subscriber); + } + }; + + TestSubscriber<List<Integer>> ts = pp.buffer(b).test(); + + ref.get().onNext(1); + + ts.assertResult(Collections.<Integer>emptyList()); + } + + @Test + public void bufferExactBoundaryCancelUpfront() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + PublishProcessor<Integer> b = PublishProcessor.create(); + + pp.buffer(b).test(0L, true) + .assertEmpty(); + + assertFalse(pp.hasSubscribers()); + assertFalse(b.hasSubscribers()); + } + + @Test + public void bufferExactBoundaryDisposed() { + Flowable<Integer> pp = new Flowable<Integer>() { + @Override + protected void subscribeActual(Subscriber<? super Integer> s) { + s.onSubscribe(new BooleanSubscription()); + + Disposable d = (Disposable)s; + + assertFalse(d.isDisposed()); + + d.dispose(); + + assertTrue(d.isDisposed()); + } + }; + PublishProcessor<Integer> b = PublishProcessor.create(); + + pp.buffer(b).test(); + } + + @Test + public void timedDoubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeFlowable(new Function<Flowable<Object>, Publisher<List<Object>>>() { + @Override + public Publisher<List<Object>> apply(Flowable<Object> f) + throws Exception { + return f.buffer(1, TimeUnit.SECONDS); + } + }); + } + + @Test + public void timedCancelledUpfront() { + TestScheduler sch = new TestScheduler(); + + TestSubscriber<List<Object>> ts = Flowable.never() + .buffer(1, TimeUnit.MILLISECONDS, sch) + .test(1L, true); + + sch.advanceTimeBy(1, TimeUnit.MILLISECONDS); + + ts.assertEmpty(); + } + + @Test + public void timedInternalState() { + TestScheduler sch = new TestScheduler(); + + TestSubscriber<List<Integer>> ts = new TestSubscriber<>(); + + BufferExactUnboundedSubscriber<Integer, List<Integer>> sub = new BufferExactUnboundedSubscriber<>( + ts, Functions.justSupplier((List<Integer>) new ArrayList<Integer>()), 1, TimeUnit.SECONDS, sch); + + sub.onSubscribe(new BooleanSubscription()); + + assertFalse(sub.isDisposed()); + + sub.onError(new TestException()); + sub.onNext(1); + sub.onComplete(); + + sub.run(); + + sub.dispose(); + + assertTrue(sub.isDisposed()); + + sub.buffer = new ArrayList<>(); + sub.enter(); + sub.onComplete(); + } + + @Test + public void timedSkipDoubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeFlowable(new Function<Flowable<Object>, Publisher<List<Object>>>() { + @Override + public Publisher<List<Object>> apply(Flowable<Object> f) + throws Exception { + return f.buffer(2, 1, TimeUnit.SECONDS); + } + }); + } + + @Test + public void timedSizedDoubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeFlowable(new Function<Flowable<Object>, Publisher<List<Object>>>() { + @Override + public Publisher<List<Object>> apply(Flowable<Object> f) + throws Exception { + return f.buffer(2, TimeUnit.SECONDS, 10); + } + }); + } + + @Test + public void timedSkipInternalState() { + TestScheduler sch = new TestScheduler(); + + TestSubscriber<List<Integer>> ts = new TestSubscriber<>(); + + BufferSkipBoundedSubscriber<Integer, List<Integer>> sub = new BufferSkipBoundedSubscriber<>( + ts, Functions.justSupplier((List<Integer>) new ArrayList<Integer>()), 1, 1, TimeUnit.SECONDS, sch.createWorker()); + + sub.onSubscribe(new BooleanSubscription()); + + sub.enter(); + sub.onComplete(); + + sub.cancel(); + + sub.run(); + } + + @Test + public void timedSkipCancelWhenSecondBuffer() { + TestScheduler sch = new TestScheduler(); + + final TestSubscriber<List<Integer>> ts = new TestSubscriber<>(); + + BufferSkipBoundedSubscriber<Integer, List<Integer>> sub = new BufferSkipBoundedSubscriber<>( + ts, new Supplier<List<Integer>>() { + int calls; + + @Override + public List<Integer> get() throws Exception { + if (++calls == 2) { + ts.cancel(); + } + return new ArrayList<>(); + } + }, 1, 1, TimeUnit.SECONDS, sch.createWorker()); + + sub.onSubscribe(new BooleanSubscription()); + + sub.run(); + + assertTrue(ts.isCancelled()); + } + + @Test + public void timedSizeBufferAlreadyCleared() { + TestScheduler sch = new TestScheduler(); + + TestSubscriber<List<Integer>> ts = new TestSubscriber<>(); + + BufferExactBoundedSubscriber<Integer, List<Integer>> sub = + new BufferExactBoundedSubscriber<>( + ts, Functions.justSupplier((List<Integer>) new ArrayList<Integer>()), + 1, TimeUnit.SECONDS, 1, false, sch.createWorker()) + ; + + BooleanSubscription bs = new BooleanSubscription(); + + sub.onSubscribe(bs); + + sub.producerIndex++; + + sub.run(); + + assertFalse(sub.isDisposed()); + + sub.enter(); + sub.onComplete(); + + assertTrue(sub.isDisposed()); + + sub.run(); + } + + @Test + public void bufferExactFailingSupplier() { + Flowable.empty() + .buffer(1, TimeUnit.SECONDS, Schedulers.computation(), 10, new Supplier<List<Object>>() { + @Override + public List<Object> get() throws Exception { + throw new TestException(); + } + }, false) + .test() + .awaitDone(1, TimeUnit.SECONDS) + .assertFailure(TestException.class) + ; + } + + @Test + public void exactBadRequest() { + TestHelper.assertBadRequestReported(Flowable.never().buffer(1)); + } + + @Test + public void skipBadRequest() { + TestHelper.assertBadRequestReported(Flowable.never().buffer(1, 2)); + } + + @Test + public void overlapBadRequest() { + TestHelper.assertBadRequestReported(Flowable.never().buffer(2, 1)); + } + + @Test + public void bufferExactBoundedOnNextAfterDispose() { + TestSubscriber<Object> ts = new TestSubscriber<>(); + + Flowable.unsafeCreate(s -> { + s.onSubscribe(new BooleanSubscription()); + ts.cancel(); + s.onNext(1); + }) + .buffer(1, TimeUnit.MINUTES, 2) + .subscribe(ts); + + ts.assertEmpty(); + } + + @Test + public void boundaryCloseCompleteRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + BehaviorProcessor<Integer> bp = BehaviorProcessor.createDefault(1); + PublishProcessor<Integer> pp = PublishProcessor.create(); + + TestSubscriber<List<Integer>> ts = bp + .buffer(BehaviorProcessor.createDefault(0), v -> pp) + .test(); + + TestHelper.race( + () -> bp.onComplete(), + () -> pp.onComplete() + ); + + ts.assertResult(Arrays.asList(1)); + } + } + + @Test + public void doubleOnSubscribeStartEnd() { + TestHelper.checkDoubleOnSubscribeFlowable(f -> f.buffer(Flowable.never(), v -> Flowable.never())); + } + + @Test + public void cancel() { + TestHelper.checkDisposed(Flowable.never().buffer(Flowable.never(), v -> Flowable.never())); + } + + @Test + public void startEndCancelAfterOneBuffer() { + BehaviorProcessor.createDefault(1) + .buffer(BehaviorProcessor.createDefault(2), v -> Flowable.just(1)) + .takeUntil(v -> true) + .test() + .assertResult(Arrays.asList()); + } + + @Test + public void startEndCompleteOnBoundary() { + Flowable.empty() + .buffer(Flowable.never(), v -> Flowable.just(1)) + .take(1) + .test() + .assertResult(); + } + + @Test + public void startEndBackpressure() { + BehaviorProcessor.createDefault(1) + .buffer(BehaviorProcessor.createDefault(2), v -> Flowable.just(1)) + .test(1L) + .assertValuesOnly(Arrays.asList()); + } + + @Test + public void startEndBackpressureMoreWork() { + PublishProcessor<Integer> bp = PublishProcessor.create(); + PublishProcessor<Integer> pp = PublishProcessor.create(); + AtomicInteger counter = new AtomicInteger(); + + TestSubscriber<List<Integer>> ts = bp + .buffer(pp, v -> Flowable.just(1)) + .doOnNext(v -> { + if (counter.getAndIncrement() == 0) { + pp.onNext(2); + pp.onComplete(); + } + }) + .test(1L); + + pp.onNext(1); + bp.onNext(1); + + ts + .assertValuesOnly(Arrays.asList()); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableCacheTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableCacheTest.java new file mode 100644 index 0000000000..e71315351d --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableCacheTest.java @@ -0,0 +1,528 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +import java.util.*; +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.Test; +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.subscriptions.BooleanSubscription; +import io.reactivex.rxjava3.processors.PublishProcessor; +import io.reactivex.rxjava3.schedulers.Schedulers; +import io.reactivex.rxjava3.subscribers.TestSubscriber; +import io.reactivex.rxjava3.testsupport.*; + +public class FlowableCacheTest extends RxJavaTest { + @Test + public void coldReplayNoBackpressure() { + FlowableCache<Integer> source = new FlowableCache<>(Flowable.range(0, 1000), 16); + + assertFalse("Source is connected!", source.isConnected()); + + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(); + + source.subscribe(ts); + + assertTrue("Source is not connected!", source.isConnected()); + assertFalse("Subscribers retained!", source.hasSubscribers()); + + ts.assertNoErrors(); + ts.assertTerminated(); + List<Integer> onNextEvents = ts.values(); + assertEquals(1000, onNextEvents.size()); + + for (int i = 0; i < 1000; i++) { + assertEquals((Integer)i, onNextEvents.get(i)); + } + } + + @Test + public void coldReplayBackpressure() { + FlowableCache<Integer> source = new FlowableCache<>(Flowable.range(0, 1000), 16); + + assertFalse("Source is connected!", source.isConnected()); + + TestSubscriber<Integer> ts = new TestSubscriber<>(0L); + ts.request(10); + + source.subscribe(ts); + + assertTrue("Source is not connected!", source.isConnected()); + assertFalse("Subscribers retained!", source.hasSubscribers()); + + ts.assertNoErrors(); + ts.assertNotComplete(); + List<Integer> onNextEvents = ts.values(); + assertEquals(10, onNextEvents.size()); + + for (int i = 0; i < 10; i++) { + assertEquals((Integer)i, onNextEvents.get(i)); + } + + ts.cancel(); + assertFalse("Subscribers retained!", source.hasSubscribers()); + } + + @Test + public void cache() throws InterruptedException { + final AtomicInteger counter = new AtomicInteger(); + Flowable<String> f = Flowable.unsafeCreate(new Publisher<String>() { + + @Override + public void subscribe(final Subscriber<? super String> subscriber) { + subscriber.onSubscribe(new BooleanSubscription()); + new Thread(new Runnable() { + + @Override + public void run() { + counter.incrementAndGet(); + System.out.println("published observable being executed"); + subscriber.onNext("one"); + subscriber.onComplete(); + } + }).start(); + } + }).cache(); + + // we then expect the following 2 subscriptions to get that same value + final CountDownLatch latch = new CountDownLatch(2); + + // subscribe once + f.subscribe(new Consumer<String>() { + @Override + public void accept(String v) { + assertEquals("one", v); + System.out.println("v: " + v); + latch.countDown(); + } + }); + + // subscribe again + f.subscribe(new Consumer<String>() { + @Override + public void accept(String v) { + assertEquals("one", v); + System.out.println("v: " + v); + latch.countDown(); + } + }); + + if (!latch.await(1000, TimeUnit.MILLISECONDS)) { + fail("subscriptions did not receive values"); + } + assertEquals(1, counter.get()); + } + + @Test + public void unsubscribeSource() throws Throwable { + Action unsubscribe = mock(Action.class); + Flowable<Integer> f = Flowable.just(1).doOnCancel(unsubscribe).cache(); + f.subscribe(); + f.subscribe(); + f.subscribe(); + verify(unsubscribe, never()).run(); + } + + @Test + public void take() { + TestSubscriber<Integer> ts = new TestSubscriber<>(); + + FlowableCache<Integer> cached = new FlowableCache<>(Flowable.range(1, 100), 16); + cached.take(10).subscribe(ts); + + ts.assertNoErrors(); + ts.assertComplete(); + ts.assertValues(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + assertFalse(cached.hasSubscribers()); + } + + @Test + public void async() { + Flowable<Integer> source = Flowable.range(1, 10000); + for (int i = 0; i < 100; i++) { + TestSubscriber<Integer> ts1 = new TestSubscriber<>(); + + FlowableCache<Integer> cached = new FlowableCache<>(source, 16); + + cached.observeOn(Schedulers.computation()).subscribe(ts1); + + ts1.awaitDone(2, TimeUnit.SECONDS); + ts1.assertNoErrors(); + ts1.assertComplete(); + assertEquals(10000, ts1.values().size()); + + TestSubscriber<Integer> ts2 = new TestSubscriber<>(); + cached.observeOn(Schedulers.computation()).subscribe(ts2); + + ts2.awaitDone(2, TimeUnit.SECONDS); + ts2.assertNoErrors(); + ts2.assertComplete(); + assertEquals(10000, ts2.values().size()); + } + } + + @Test + public void asyncComeAndGo() { + Flowable<Long> source = Flowable.interval(1, 1, TimeUnit.MILLISECONDS) + .take(1000) + .subscribeOn(Schedulers.io()); + FlowableCache<Long> cached = new FlowableCache<>(source, 16); + + Flowable<Long> output = cached.observeOn(Schedulers.computation()); + + List<TestSubscriber<Long>> list = new ArrayList<>(100); + for (int i = 0; i < 100; i++) { + TestSubscriber<Long> ts = new TestSubscriber<>(); + list.add(ts); + output.skip(i * 10).take(10).subscribe(ts); + } + + List<Long> expected = new ArrayList<>(); + for (int i = 0; i < 10; i++) { + expected.add((long)(i - 10)); + } + int j = 0; + for (TestSubscriber<Long> ts : list) { + ts.awaitDone(3, TimeUnit.SECONDS); + ts.assertNoErrors(); + ts.assertComplete(); + + for (int i = j * 10; i < j * 10 + 10; i++) { + expected.set(i - j * 10, (long)i); + } + + ts.assertValueSequence(expected); + + j++; + } + } + + @Test + public void noMissingBackpressureException() { + final int m = 4 * 1000 * 1000; + Flowable<Integer> firehose = Flowable.unsafeCreate(new Publisher<Integer>() { + @Override + public void subscribe(Subscriber<? super Integer> t) { + t.onSubscribe(new BooleanSubscription()); + for (int i = 0; i < m; i++) { + t.onNext(i); + } + t.onComplete(); + } + }); + + TestSubscriber<Integer> ts = new TestSubscriber<>(); + firehose.cache().observeOn(Schedulers.computation()).takeLast(100).subscribe(ts); + + ts.awaitDone(3, TimeUnit.SECONDS); + ts.assertNoErrors(); + ts.assertComplete(); + + assertEquals(100, ts.values().size()); + } + + @Test + public void valuesAndThenError() { + Flowable<Integer> source = Flowable.range(1, 10) + .concatWith(Flowable.<Integer>error(new TestException())) + .cache(); + + TestSubscriber<Integer> ts = new TestSubscriber<>(); + source.subscribe(ts); + + ts.assertValues(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + ts.assertNotComplete(); + ts.assertError(TestException.class); + + TestSubscriber<Integer> ts2 = new TestSubscriber<>(); + source.subscribe(ts2); + + ts2.assertValues(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + ts2.assertNotComplete(); + ts2.assertError(TestException.class); + } + + @Test + public void take2() { + Flowable<Integer> cache = Flowable.range(1, 5).cache(); + + cache.take(2).test().assertResult(1, 2); + cache.take(3).test().assertResult(1, 2, 3); + } + + @Test + public void dispose() { + TestHelper.checkDisposed(Flowable.range(1, 5).cache()); + } + + @Test + public void disposeOnArrival2() { + Flowable<Integer> f = PublishProcessor.<Integer>create().cache(); + + f.test(); + + f.test(0L, true) + .assertEmpty(); + } + + @Test + public void subscribeEmitRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final PublishProcessor<Integer> pp = PublishProcessor.<Integer>create(); + + final Flowable<Integer> cache = pp.cache(); + + cache.test(); + + final TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + cache.subscribe(ts); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + for (int j = 0; j < 500; j++) { + pp.onNext(j); + } + pp.onComplete(); + } + }; + + TestHelper.race(r1, r2); + + ts + .awaitDone(5, TimeUnit.SECONDS) + .assertSubscribed().assertValueCount(500).assertComplete().assertNoErrors(); + } + } + + @Test + public void observers() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + FlowableCache<Integer> cache = (FlowableCache<Integer>)Flowable.range(1, 5).concatWith(pp).cache(); + + assertFalse(cache.hasSubscribers()); + + assertEquals(0, cache.cachedEventCount()); + + TestSubscriber<Integer> ts = cache.test(); + + assertTrue(cache.hasSubscribers()); + + assertEquals(5, cache.cachedEventCount()); + + pp.onComplete(); + + ts.assertResult(1, 2, 3, 4, 5); + } + + @Test + public void disposeOnArrival() { + Flowable.range(1, 5).cache() + .test(0L, true) + .assertEmpty(); + } + + @Test + public void badSource() { + TestHelper.checkBadSourceFlowable(new Function<Flowable<Object>, Object>() { + @Override + public Object apply(Flowable<Object> f) throws Exception { + return f.cache(); + } + }, false, 1, 1, 1); + } + + @Test + public void badRequest() { + TestHelper.assertBadRequestReported(Flowable.never().cache()); + } + + @Test + public void take1() { + Flowable<Integer> cache = Flowable.just(1, 2) + .cache(); + + cache.test(); + + cache + .take(1) + .test() + .assertResult(1); + } + + @Test + public void empty() { + Flowable.empty() + .cache() + .test(0L) + .assertResult(); + } + + @Test + public void error() { + Flowable.error(new TestException()) + .cache() + .test(0L) + .assertFailure(TestException.class); + } + + @Test + public void cancelledUpFrontConnectAnyway() { + final AtomicInteger call = new AtomicInteger(); + Flowable.fromCallable(new Callable<Object>() { + @Override + public Object call() throws Exception { + return call.incrementAndGet(); + } + }) + .cache() + .test(1L, true) + .assertNoValues(); + + assertEquals(1, call.get()); + } + + @Test + public void cancelledUpFront() { + final AtomicInteger call = new AtomicInteger(); + Flowable<Object> f = Flowable.fromCallable(new Callable<Object>() { + @Override + public Object call() throws Exception { + return call.incrementAndGet(); + } + }).concatWith(Flowable.never()) + .cache(); + + f.test().assertValuesOnly(1); + + f.test(1L, true) + .assertEmpty(); + + assertEquals(1, call.get()); + } + + @Test + public void subscribeSubscribeRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final Flowable<Integer> cache = Flowable.range(1, 500).cache(); + + final TestSubscriberEx<Integer> ts1 = new TestSubscriberEx<>(); + final TestSubscriberEx<Integer> ts2 = new TestSubscriberEx<>(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + cache.subscribe(ts1); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + cache.subscribe(ts2); + } + }; + + TestHelper.race(r1, r2); + + ts1 + .awaitDone(5, TimeUnit.SECONDS) + .assertSubscribed() + .assertValueCount(500) + .assertComplete() + .assertNoErrors(); + + ts2 + .awaitDone(5, TimeUnit.SECONDS) + .assertSubscribed() + .assertValueCount(500) + .assertComplete() + .assertNoErrors(); + } + } + + @Test + public void subscribeCompleteRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final PublishProcessor<Integer> pp = PublishProcessor.<Integer>create(); + + final Flowable<Integer> cache = pp.cache(); + + cache.test(); + + final TestSubscriber<Integer> ts = new TestSubscriber<>(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + cache.subscribe(ts); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + pp.onComplete(); + } + }; + + TestHelper.race(r1, r2); + + ts + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(); + } + } + + @Test + public void backpressure() { + Flowable.range(1, 5) + .cache() + .test(0) + .assertEmpty() + .requestMore(2) + .assertValuesOnly(1, 2) + .requestMore(3) + .assertResult(1, 2, 3, 4, 5); + } + + @Test + public void addRemoveRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + Flowable<Object> f = Flowable.never().cache(); + + TestSubscriber<Object> ts = f.test(); + + TestHelper.race( + () -> ts.cancel(), + () -> f.test() + ); + } + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableCastTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableCastTest.java new file mode 100644 index 0000000000..dc26fdccaa --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableCastTest.java @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import org.junit.*; +import org.reactivestreams.Subscriber; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.processors.PublishProcessor; +import io.reactivex.rxjava3.subscribers.TestSubscriber; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class FlowableCastTest extends RxJavaTest { + + @Test + public void cast() { + Flowable<?> source = Flowable.just(1, 2); + Flowable<Integer> flowable = source.cast(Integer.class); + + Subscriber<Integer> subscriber = TestHelper.mockSubscriber(); + + flowable.subscribe(subscriber); + + verify(subscriber, times(1)).onNext(1); + verify(subscriber, times(1)).onNext(1); + verify(subscriber, never()).onError(any(Throwable.class)); + verify(subscriber, times(1)).onComplete(); + } + + @Test + public void castWithWrongType() { + Flowable<?> source = Flowable.just(1, 2); + Flowable<Boolean> flowable = source.cast(Boolean.class); + + Subscriber<Boolean> subscriber = TestHelper.mockSubscriber(); + + flowable.subscribe(subscriber); + + verify(subscriber, times(1)).onError(any(ClassCastException.class)); + } + + @Test + public void castCrashUnsubscribes() { + + PublishProcessor<Integer> pp = PublishProcessor.create(); + + TestSubscriber<String> ts = TestSubscriber.create(); + + pp.cast(String.class).subscribe(ts); + + Assert.assertTrue("Not subscribed?", pp.hasSubscribers()); + + pp.onNext(1); + + Assert.assertFalse("Subscribed?", pp.hasSubscribers()); + + ts.assertError(ClassCastException.class); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableCombineLatestTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableCombineLatestTest.java new file mode 100644 index 0000000000..0f07148179 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableCombineLatestTest.java @@ -0,0 +1,1788 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +import java.lang.reflect.*; +import java.util.*; +import java.util.concurrent.*; +import java.util.concurrent.atomic.*; + +import org.junit.Test; +import org.mockito.*; +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.internal.operators.flowable.FlowableZipTest.ArgsToString; +import io.reactivex.rxjava3.internal.subscriptions.BooleanSubscription; +import io.reactivex.rxjava3.operators.QueueFuseable; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.processors.PublishProcessor; +import io.reactivex.rxjava3.schedulers.*; +import io.reactivex.rxjava3.subscribers.*; +import io.reactivex.rxjava3.testsupport.*; + +public class FlowableCombineLatestTest extends RxJavaTest { + + @Test + public void combineLatestWithFunctionThatThrowsAnException() { + Subscriber<String> w = TestHelper.mockSubscriber(); + + PublishProcessor<String> w1 = PublishProcessor.create(); + PublishProcessor<String> w2 = PublishProcessor.create(); + + Flowable<String> combined = Flowable.combineLatest(w1, w2, new BiFunction<String, String, String>() { + @Override + public String apply(String v1, String v2) { + throw new RuntimeException("I don't work."); + } + }); + combined.subscribe(w); + + w1.onNext("first value of w1"); + w2.onNext("first value of w2"); + + verify(w, never()).onNext(anyString()); + verify(w, never()).onComplete(); + verify(w, times(1)).onError(Mockito.<RuntimeException> any()); + } + + @Test + public void combineLatestDifferentLengthFlowableSequences1() { + Subscriber<String> w = TestHelper.mockSubscriber(); + + PublishProcessor<String> w1 = PublishProcessor.create(); + PublishProcessor<String> w2 = PublishProcessor.create(); + PublishProcessor<String> w3 = PublishProcessor.create(); + + Flowable<String> combineLatestW = Flowable.combineLatest(w1, w2, w3, + getConcat3StringsCombineLatestFunction()); + combineLatestW.subscribe(w); + + /* simulate sending data */ + // once for w1 + w1.onNext("1a"); + w2.onNext("2a"); + w3.onNext("3a"); + w1.onComplete(); + // twice for w2 + w2.onNext("2b"); + w2.onComplete(); + // 4 times for w3 + w3.onNext("3b"); + w3.onNext("3c"); + w3.onNext("3d"); + w3.onComplete(); + + /* we should have been called 4 times on the Observer */ + InOrder inOrder = inOrder(w); + inOrder.verify(w).onNext("1a2a3a"); + inOrder.verify(w).onNext("1a2b3a"); + inOrder.verify(w).onNext("1a2b3b"); + inOrder.verify(w).onNext("1a2b3c"); + inOrder.verify(w).onNext("1a2b3d"); + inOrder.verify(w, never()).onNext(anyString()); + inOrder.verify(w, times(1)).onComplete(); + } + + @Test + public void combineLatestDifferentLengthFlowableSequences2() { + Subscriber<String> w = TestHelper.mockSubscriber(); + + PublishProcessor<String> w1 = PublishProcessor.create(); + PublishProcessor<String> w2 = PublishProcessor.create(); + PublishProcessor<String> w3 = PublishProcessor.create(); + + Flowable<String> combineLatestW = Flowable.combineLatest(w1, w2, w3, getConcat3StringsCombineLatestFunction()); + combineLatestW.subscribe(w); + + /* simulate sending data */ + // 4 times for w1 + w1.onNext("1a"); + w1.onNext("1b"); + w1.onNext("1c"); + w1.onNext("1d"); + w1.onComplete(); + // twice for w2 + w2.onNext("2a"); + w2.onNext("2b"); + w2.onComplete(); + // 1 times for w3 + w3.onNext("3a"); + w3.onComplete(); + + /* we should have been called 1 time only on the Observer since we only combine the "latest" we don't go back and loop through others once completed */ + InOrder inOrder = inOrder(w); + inOrder.verify(w, times(1)).onNext("1d2b3a"); + inOrder.verify(w, never()).onNext(anyString()); + + inOrder.verify(w, times(1)).onComplete(); + + } + + @Test + public void combineLatestWithInterleavingSequences() { + Subscriber<String> w = TestHelper.mockSubscriber(); + + PublishProcessor<String> w1 = PublishProcessor.create(); + PublishProcessor<String> w2 = PublishProcessor.create(); + PublishProcessor<String> w3 = PublishProcessor.create(); + + Flowable<String> combineLatestW = Flowable.combineLatest(w1, w2, w3, getConcat3StringsCombineLatestFunction()); + combineLatestW.subscribe(w); + + /* simulate sending data */ + w1.onNext("1a"); + w2.onNext("2a"); + w2.onNext("2b"); + w3.onNext("3a"); + + w1.onNext("1b"); + w2.onNext("2c"); + w2.onNext("2d"); + w3.onNext("3b"); + + w1.onComplete(); + w2.onComplete(); + w3.onComplete(); + + /* we should have been called 5 times on the Observer */ + InOrder inOrder = inOrder(w); + inOrder.verify(w).onNext("1a2b3a"); + inOrder.verify(w).onNext("1b2b3a"); + inOrder.verify(w).onNext("1b2c3a"); + inOrder.verify(w).onNext("1b2d3a"); + inOrder.verify(w).onNext("1b2d3b"); + + inOrder.verify(w, never()).onNext(anyString()); + inOrder.verify(w, times(1)).onComplete(); + } + + @Test + public void combineLatest2Types() { + BiFunction<String, Integer, String> combineLatestFunction = getConcatStringIntegerCombineLatestFunction(); + + /* define an Observer to receive aggregated events */ + Subscriber<String> subscriber = TestHelper.mockSubscriber(); + + Flowable<String> w = Flowable.combineLatest(Flowable.just("one", "two"), Flowable.just(2, 3, 4), combineLatestFunction); + w.subscribe(subscriber); + + verify(subscriber, never()).onError(any(Throwable.class)); + verify(subscriber, times(1)).onComplete(); + verify(subscriber, times(1)).onNext("two2"); + verify(subscriber, times(1)).onNext("two3"); + verify(subscriber, times(1)).onNext("two4"); + } + + @Test + public void combineLatest3TypesA() { + Function3<String, Integer, int[], String> combineLatestFunction = getConcatStringIntegerIntArrayCombineLatestFunction(); + + /* define an Observer to receive aggregated events */ + Subscriber<String> subscriber = TestHelper.mockSubscriber(); + + Flowable<String> w = Flowable.combineLatest(Flowable.just("one", "two"), Flowable.just(2), Flowable.just(new int[] { 4, 5, 6 }), combineLatestFunction); + w.subscribe(subscriber); + + verify(subscriber, never()).onError(any(Throwable.class)); + verify(subscriber, times(1)).onComplete(); + verify(subscriber, times(1)).onNext("two2[4, 5, 6]"); + } + + @Test + public void combineLatest3TypesB() { + Function3<String, Integer, int[], String> combineLatestFunction = getConcatStringIntegerIntArrayCombineLatestFunction(); + + /* define an Observer to receive aggregated events */ + Subscriber<String> subscriber = TestHelper.mockSubscriber(); + + Flowable<String> w = Flowable.combineLatest(Flowable.just("one"), Flowable.just(2), Flowable.just(new int[] { 4, 5, 6 }, new int[] { 7, 8 }), combineLatestFunction); + w.subscribe(subscriber); + + verify(subscriber, never()).onError(any(Throwable.class)); + verify(subscriber, times(1)).onComplete(); + verify(subscriber, times(1)).onNext("one2[4, 5, 6]"); + verify(subscriber, times(1)).onNext("one2[7, 8]"); + } + + private Function3<String, String, String, String> getConcat3StringsCombineLatestFunction() { + Function3<String, String, String, String> combineLatestFunction = new Function3<String, String, String, String>() { + @Override + public String apply(String a1, String a2, String a3) { + if (a1 == null) { + a1 = ""; + } + if (a2 == null) { + a2 = ""; + } + if (a3 == null) { + a3 = ""; + } + return a1 + a2 + a3; + } + }; + return combineLatestFunction; + } + + private BiFunction<String, Integer, String> getConcatStringIntegerCombineLatestFunction() { + BiFunction<String, Integer, String> combineLatestFunction = new BiFunction<String, Integer, String>() { + @Override + public String apply(String s, Integer i) { + return getStringValue(s) + getStringValue(i); + } + }; + return combineLatestFunction; + } + + private Function3<String, Integer, int[], String> getConcatStringIntegerIntArrayCombineLatestFunction() { + return new Function3<String, Integer, int[], String>() { + @Override + public String apply(String s, Integer i, int[] iArray) { + return getStringValue(s) + getStringValue(i) + getStringValue(iArray); + } + }; + } + + private static String getStringValue(Object o) { + if (o == null) { + return ""; + } else { + if (o instanceof int[]) { + return Arrays.toString((int[]) o); + } else { + return String.valueOf(o); + } + } + } + + BiFunction<Integer, Integer, Integer> or = new BiFunction<Integer, Integer, Integer>() { + @Override + public Integer apply(Integer t1, Integer t2) { + return t1 | t2; + } + }; + + @Test + public void combineSimple() { + PublishProcessor<Integer> a = PublishProcessor.create(); + PublishProcessor<Integer> b = PublishProcessor.create(); + + Flowable<Integer> source = Flowable.combineLatest(a, b, or); + + Subscriber<Object> subscriber = TestHelper.mockSubscriber(); + InOrder inOrder = inOrder(subscriber); + + source.subscribe(subscriber); + + a.onNext(1); + + inOrder.verify(subscriber, never()).onNext(any()); + + a.onNext(2); + + inOrder.verify(subscriber, never()).onNext(any()); + + b.onNext(0x10); + + inOrder.verify(subscriber, times(1)).onNext(0x12); + + b.onNext(0x20); + inOrder.verify(subscriber, times(1)).onNext(0x22); + + b.onComplete(); + + inOrder.verify(subscriber, never()).onComplete(); + + a.onComplete(); + + inOrder.verify(subscriber, times(1)).onComplete(); + + a.onNext(3); + b.onNext(0x30); + a.onComplete(); + b.onComplete(); + + inOrder.verifyNoMoreInteractions(); + verify(subscriber, never()).onError(any(Throwable.class)); + } + + @Test + public void combineMultipleObservers() { + PublishProcessor<Integer> a = PublishProcessor.create(); + PublishProcessor<Integer> b = PublishProcessor.create(); + + Flowable<Integer> source = Flowable.combineLatest(a, b, or); + + Subscriber<Object> subscriber1 = TestHelper.mockSubscriber(); + + Subscriber<Object> subscriber2 = TestHelper.mockSubscriber(); + + InOrder inOrder1 = inOrder(subscriber1); + InOrder inOrder2 = inOrder(subscriber2); + + source.subscribe(subscriber1); + source.subscribe(subscriber2); + + a.onNext(1); + + inOrder1.verify(subscriber1, never()).onNext(any()); + inOrder2.verify(subscriber2, never()).onNext(any()); + + a.onNext(2); + + inOrder1.verify(subscriber1, never()).onNext(any()); + inOrder2.verify(subscriber2, never()).onNext(any()); + + b.onNext(0x10); + + inOrder1.verify(subscriber1, times(1)).onNext(0x12); + inOrder2.verify(subscriber2, times(1)).onNext(0x12); + + b.onNext(0x20); + inOrder1.verify(subscriber1, times(1)).onNext(0x22); + inOrder2.verify(subscriber2, times(1)).onNext(0x22); + + b.onComplete(); + + inOrder1.verify(subscriber1, never()).onComplete(); + inOrder2.verify(subscriber2, never()).onComplete(); + + a.onComplete(); + + inOrder1.verify(subscriber1, times(1)).onComplete(); + inOrder2.verify(subscriber2, times(1)).onComplete(); + + a.onNext(3); + b.onNext(0x30); + a.onComplete(); + b.onComplete(); + + inOrder1.verifyNoMoreInteractions(); + inOrder2.verifyNoMoreInteractions(); + verify(subscriber1, never()).onError(any(Throwable.class)); + verify(subscriber2, never()).onError(any(Throwable.class)); + } + + @Test + public void firstNeverProduces() { + PublishProcessor<Integer> a = PublishProcessor.create(); + PublishProcessor<Integer> b = PublishProcessor.create(); + + Flowable<Integer> source = Flowable.combineLatest(a, b, or); + + Subscriber<Object> subscriber = TestHelper.mockSubscriber(); + InOrder inOrder = inOrder(subscriber); + + source.subscribe(subscriber); + + b.onNext(0x10); + b.onNext(0x20); + + a.onComplete(); + + inOrder.verify(subscriber, times(1)).onComplete(); + verify(subscriber, never()).onNext(any()); + verify(subscriber, never()).onError(any(Throwable.class)); + } + + @Test + public void secondNeverProduces() { + PublishProcessor<Integer> a = PublishProcessor.create(); + PublishProcessor<Integer> b = PublishProcessor.create(); + + Flowable<Integer> source = Flowable.combineLatest(a, b, or); + + Subscriber<Object> subscriber = TestHelper.mockSubscriber(); + InOrder inOrder = inOrder(subscriber); + + source.subscribe(subscriber); + + a.onNext(0x1); + a.onNext(0x2); + + b.onComplete(); + a.onComplete(); + + inOrder.verify(subscriber, times(1)).onComplete(); + verify(subscriber, never()).onNext(any()); + verify(subscriber, never()).onError(any(Throwable.class)); + } + + @Test + public void oneToNSources() { + int n = 30; + Function<Object[], List<Object>> func = new Function<Object[], List<Object>>() { + + @Override + public List<Object> apply(Object[] args) { + return Arrays.asList(args); + } + }; + for (int i = 1; i <= n; i++) { + System.out.println("test1ToNSources: " + i + " sources"); + List<Flowable<Integer>> sources = new ArrayList<>(); + List<Object> values = new ArrayList<>(); + for (int j = 0; j < i; j++) { + sources.add(Flowable.just(j)); + values.add(j); + } + + Flowable<List<Object>> result = Flowable.combineLatest(sources, func); + + Subscriber<List<Object>> subscriber = TestHelper.mockSubscriber(); + + result.subscribe(subscriber); + + verify(subscriber).onNext(values); + verify(subscriber).onComplete(); + verify(subscriber, never()).onError(any(Throwable.class)); + } + } + + @Test + public void oneToNSourcesScheduled() throws InterruptedException { + int n = 10; + Function<Object[], List<Object>> func = new Function<Object[], List<Object>>() { + + @Override + public List<Object> apply(Object[] args) { + return Arrays.asList(args); + } + }; + for (int i = 1; i <= n; i++) { + System.out.println("test1ToNSourcesScheduled: " + i + " sources"); + List<Flowable<Integer>> sources = new ArrayList<>(); + List<Object> values = new ArrayList<>(); + for (int j = 0; j < i; j++) { + sources.add(Flowable.just(j).subscribeOn(Schedulers.io())); + values.add(j); + } + + Flowable<List<Object>> result = Flowable.combineLatest(sources, func); + + final Subscriber<List<Object>> subscriber = TestHelper.mockSubscriber(); + + final CountDownLatch cdl = new CountDownLatch(1); + + Subscriber<List<Object>> s = new DefaultSubscriber<List<Object>>() { + + @Override + public void onNext(List<Object> t) { + subscriber.onNext(t); + } + + @Override + public void onError(Throwable e) { + subscriber.onError(e); + cdl.countDown(); + } + + @Override + public void onComplete() { + subscriber.onComplete(); + cdl.countDown(); + } + }; + + result.subscribe(s); + + cdl.await(); + + verify(subscriber).onNext(values); + verify(subscriber).onComplete(); + verify(subscriber, never()).onError(any(Throwable.class)); + } + } + + @Test + public void twoSourcesOverload() { + Flowable<Integer> s1 = Flowable.just(1); + Flowable<Integer> s2 = Flowable.just(2); + + Flowable<List<Integer>> result = Flowable.combineLatest(s1, s2, + new BiFunction<Integer, Integer, List<Integer>>() { + @Override + public List<Integer> apply(Integer t1, Integer t2) { + return Arrays.asList(t1, t2); + } + }); + + Subscriber<Object> subscriber = TestHelper.mockSubscriber(); + + result.subscribe(subscriber); + + verify(subscriber).onNext(Arrays.asList(1, 2)); + verify(subscriber).onComplete(); + verify(subscriber, never()).onError(any(Throwable.class)); + } + + @Test + public void threeSourcesOverload() { + Flowable<Integer> s1 = Flowable.just(1); + Flowable<Integer> s2 = Flowable.just(2); + Flowable<Integer> s3 = Flowable.just(3); + + Flowable<List<Integer>> result = Flowable.combineLatest(s1, s2, s3, + new Function3<Integer, Integer, Integer, List<Integer>>() { + @Override + public List<Integer> apply(Integer t1, Integer t2, Integer t3) { + return Arrays.asList(t1, t2, t3); + } + }); + + Subscriber<Object> subscriber = TestHelper.mockSubscriber(); + + result.subscribe(subscriber); + + verify(subscriber).onNext(Arrays.asList(1, 2, 3)); + verify(subscriber).onComplete(); + verify(subscriber, never()).onError(any(Throwable.class)); + } + + @Test + public void fourSourcesOverload() { + Flowable<Integer> s1 = Flowable.just(1); + Flowable<Integer> s2 = Flowable.just(2); + Flowable<Integer> s3 = Flowable.just(3); + Flowable<Integer> s4 = Flowable.just(4); + + Flowable<List<Integer>> result = Flowable.combineLatest(s1, s2, s3, s4, + new Function4<Integer, Integer, Integer, Integer, List<Integer>>() { + @Override + public List<Integer> apply(Integer t1, Integer t2, Integer t3, Integer t4) { + return Arrays.asList(t1, t2, t3, t4); + } + }); + + Subscriber<Object> subscriber = TestHelper.mockSubscriber(); + + result.subscribe(subscriber); + + verify(subscriber).onNext(Arrays.asList(1, 2, 3, 4)); + verify(subscriber).onComplete(); + verify(subscriber, never()).onError(any(Throwable.class)); + } + + @Test + public void fiveSourcesOverload() { + Flowable<Integer> s1 = Flowable.just(1); + Flowable<Integer> s2 = Flowable.just(2); + Flowable<Integer> s3 = Flowable.just(3); + Flowable<Integer> s4 = Flowable.just(4); + Flowable<Integer> s5 = Flowable.just(5); + + Flowable<List<Integer>> result = Flowable.combineLatest(s1, s2, s3, s4, s5, + new Function5<Integer, Integer, Integer, Integer, Integer, List<Integer>>() { + @Override + public List<Integer> apply(Integer t1, Integer t2, Integer t3, Integer t4, Integer t5) { + return Arrays.asList(t1, t2, t3, t4, t5); + } + }); + + Subscriber<Object> subscriber = TestHelper.mockSubscriber(); + + result.subscribe(subscriber); + + verify(subscriber).onNext(Arrays.asList(1, 2, 3, 4, 5)); + verify(subscriber).onComplete(); + verify(subscriber, never()).onError(any(Throwable.class)); + } + + @Test + public void sixSourcesOverload() { + Flowable<Integer> s1 = Flowable.just(1); + Flowable<Integer> s2 = Flowable.just(2); + Flowable<Integer> s3 = Flowable.just(3); + Flowable<Integer> s4 = Flowable.just(4); + Flowable<Integer> s5 = Flowable.just(5); + Flowable<Integer> s6 = Flowable.just(6); + + Flowable<List<Integer>> result = Flowable.combineLatest(s1, s2, s3, s4, s5, s6, + new Function6<Integer, Integer, Integer, Integer, Integer, Integer, List<Integer>>() { + @Override + public List<Integer> apply(Integer t1, Integer t2, Integer t3, Integer t4, Integer t5, Integer t6) { + return Arrays.asList(t1, t2, t3, t4, t5, t6); + } + }); + + Subscriber<Object> subscriber = TestHelper.mockSubscriber(); + + result.subscribe(subscriber); + + verify(subscriber).onNext(Arrays.asList(1, 2, 3, 4, 5, 6)); + verify(subscriber).onComplete(); + verify(subscriber, never()).onError(any(Throwable.class)); + } + + @Test + public void sevenSourcesOverload() { + Flowable<Integer> s1 = Flowable.just(1); + Flowable<Integer> s2 = Flowable.just(2); + Flowable<Integer> s3 = Flowable.just(3); + Flowable<Integer> s4 = Flowable.just(4); + Flowable<Integer> s5 = Flowable.just(5); + Flowable<Integer> s6 = Flowable.just(6); + Flowable<Integer> s7 = Flowable.just(7); + + Flowable<List<Integer>> result = Flowable.combineLatest(s1, s2, s3, s4, s5, s6, s7, + new Function7<Integer, Integer, Integer, Integer, Integer, Integer, Integer, List<Integer>>() { + @Override + public List<Integer> apply(Integer t1, Integer t2, Integer t3, Integer t4, Integer t5, Integer t6, Integer t7) { + return Arrays.asList(t1, t2, t3, t4, t5, t6, t7); + } + }); + + Subscriber<Object> subscriber = TestHelper.mockSubscriber(); + + result.subscribe(subscriber); + + verify(subscriber).onNext(Arrays.asList(1, 2, 3, 4, 5, 6, 7)); + verify(subscriber).onComplete(); + verify(subscriber, never()).onError(any(Throwable.class)); + } + + @Test + public void eightSourcesOverload() { + Flowable<Integer> s1 = Flowable.just(1); + Flowable<Integer> s2 = Flowable.just(2); + Flowable<Integer> s3 = Flowable.just(3); + Flowable<Integer> s4 = Flowable.just(4); + Flowable<Integer> s5 = Flowable.just(5); + Flowable<Integer> s6 = Flowable.just(6); + Flowable<Integer> s7 = Flowable.just(7); + Flowable<Integer> s8 = Flowable.just(8); + + Flowable<List<Integer>> result = Flowable.combineLatest(s1, s2, s3, s4, s5, s6, s7, s8, + new Function8<Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, List<Integer>>() { + @Override + public List<Integer> apply(Integer t1, Integer t2, Integer t3, Integer t4, Integer t5, Integer t6, Integer t7, Integer t8) { + return Arrays.asList(t1, t2, t3, t4, t5, t6, t7, t8); + } + }); + + Subscriber<Object> subscriber = TestHelper.mockSubscriber(); + + result.subscribe(subscriber); + + verify(subscriber).onNext(Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8)); + verify(subscriber).onComplete(); + verify(subscriber, never()).onError(any(Throwable.class)); + } + + @Test + public void nineSourcesOverload() { + Flowable<Integer> s1 = Flowable.just(1); + Flowable<Integer> s2 = Flowable.just(2); + Flowable<Integer> s3 = Flowable.just(3); + Flowable<Integer> s4 = Flowable.just(4); + Flowable<Integer> s5 = Flowable.just(5); + Flowable<Integer> s6 = Flowable.just(6); + Flowable<Integer> s7 = Flowable.just(7); + Flowable<Integer> s8 = Flowable.just(8); + Flowable<Integer> s9 = Flowable.just(9); + + Flowable<List<Integer>> result = Flowable.combineLatest(s1, s2, s3, s4, s5, s6, s7, s8, s9, + new Function9<Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, List<Integer>>() { + @Override + public List<Integer> apply(Integer t1, Integer t2, Integer t3, Integer t4, Integer t5, Integer t6, Integer t7, Integer t8, Integer t9) { + return Arrays.asList(t1, t2, t3, t4, t5, t6, t7, t8, t9); + } + }); + + Subscriber<Object> subscriber = TestHelper.mockSubscriber(); + + result.subscribe(subscriber); + + verify(subscriber).onNext(Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9)); + verify(subscriber).onComplete(); + verify(subscriber, never()).onError(any(Throwable.class)); + } + + @Test + public void zeroSources() { + Flowable<Object> result = Flowable.combineLatest( + Collections.<Flowable<Object>> emptyList(), new Function<Object[], Object>() { + + @Override + public Object apply(Object[] args) { + return args; + } + + }); + + Subscriber<Object> subscriber = TestHelper.mockSubscriber(); + + result.subscribe(subscriber); + + verify(subscriber).onComplete(); + verify(subscriber, never()).onNext(any()); + verify(subscriber, never()).onError(any(Throwable.class)); + + } + + @Test + public void backpressureLoop() { + for (int i = 0; i < 5000; i++) { + backpressure(); + } + } + + @Test + public void backpressure() { + BiFunction<String, Integer, String> combineLatestFunction = getConcatStringIntegerCombineLatestFunction(); + + int num = Flowable.bufferSize() * 4; + TestSubscriber<String> ts = new TestSubscriber<>(); + Flowable.combineLatest( + Flowable.just("one", "two"), + Flowable.range(2, num), + combineLatestFunction + ) + .observeOn(Schedulers.computation()) + .subscribe(ts); + + ts.awaitDone(5, TimeUnit.SECONDS); + ts.assertNoErrors(); + List<String> events = ts.values(); + assertEquals("two2", events.get(0)); + assertEquals("two3", events.get(1)); + assertEquals("two4", events.get(2)); + assertEquals(num, events.size()); + } + + @Test + public void withCombineLatestIssue1717() throws InterruptedException { + final CountDownLatch latch = new CountDownLatch(1); + final AtomicInteger count = new AtomicInteger(); + final int SIZE = 2000; + Flowable<Long> timer = Flowable.interval(0, 1, TimeUnit.MILLISECONDS) + .observeOn(Schedulers.newThread()) + .doOnEach(new Consumer<Notification<Long>>() { + @Override + public void accept(Notification<Long> n) { + // System.out.println(n); + if (count.incrementAndGet() >= SIZE) { + latch.countDown(); + } + } + }).take(SIZE); + + TestSubscriber<Long> ts = new TestSubscriber<>(); + + Flowable.combineLatest(timer, Flowable.<Integer> never(), new BiFunction<Long, Integer, Long>() { + @Override + public Long apply(Long t1, Integer t2) { + return t1; + } + }).subscribe(ts); + + if (!latch.await(SIZE + 2000, TimeUnit.MILLISECONDS)) { + fail("timed out"); + } + + assertEquals(SIZE, count.get()); + } + + @Test + public void combineLatestRequestOverflow() throws InterruptedException { + List<Flowable<Integer>> sources = Arrays.asList(Flowable.fromArray(1, 2, 3, 4), + Flowable.fromArray(5, 6, 7, 8)); + Flowable<Integer> f = Flowable.combineLatest(sources, new Function<Object[], Integer>() { + @Override + public Integer apply(Object[] args) { + return (Integer) args[0]; + }}); + //should get at least 4 + final CountDownLatch latch = new CountDownLatch(4); + f.subscribeOn(Schedulers.computation()).subscribe(new DefaultSubscriber<Integer>() { + + @Override + public void onStart() { + request(2); + } + + @Override + public void onComplete() { + //ignore + } + + @Override + public void onError(Throwable e) { + throw new RuntimeException(e); + } + + @Override + public void onNext(Integer t) { + latch.countDown(); + request(Long.MAX_VALUE - 1); + }}); + assertTrue(latch.await(10, TimeUnit.SECONDS)); + } + + private static final Function<Object[], Integer> THROW_NON_FATAL = new Function<Object[], Integer>() { + @Override + public Integer apply(Object[] args) { + throw new RuntimeException(); + } + + }; + + @Test + public void nonFatalExceptionThrownByCombinatorForSingleSourceIsNotReportedByUpstreamOperator() { + final AtomicBoolean errorOccurred = new AtomicBoolean(false); + TestSubscriber<Integer> ts = TestSubscriber.create(1); + Flowable<Integer> source = Flowable.just(1) + // if haven't caught exception in combineLatest operator then would incorrectly + // be picked up by this call to doOnError + .doOnError(new Consumer<Throwable>() { + @Override + public void accept(Throwable t) { + errorOccurred.set(true); + } + }); + Flowable + .combineLatest(Collections.singletonList(source), THROW_NON_FATAL) + .subscribe(ts); + assertFalse(errorOccurred.get()); + } + + @Test + public void combineLatestIterable() { + Flowable<Integer> source = Flowable.just(1); + + TestSubscriber<Integer> ts = TestSubscriber.create(); + + Flowable.combineLatest(Arrays.asList(source, source), + new Function<Object[], Integer>() { + @Override + public Integer apply(Object[] args) { + return (Integer)args[0] + (Integer)args[1]; + } + }) + .subscribe(ts); + + ts.assertValue(2); + ts.assertNoErrors(); + ts.assertComplete(); + } + + @Test + public void combineMany() { + int n = Flowable.bufferSize() * 3; + + List<Flowable<Integer>> sources = new ArrayList<>(); + + StringBuilder expected = new StringBuilder(n * 2); + + for (int i = 0; i < n; i++) { + sources.add(Flowable.just(i)); + expected.append(i); + } + + TestSubscriber<String> ts = TestSubscriber.create(); + + Flowable.combineLatest(sources, new Function<Object[], String>() { + @Override + public String apply(Object[] args) { + StringBuilder b = new StringBuilder(); + for (Object o : args) { + b.append(o); + } + return b.toString(); + } + }).subscribe(ts); + + ts.assertNoErrors(); + ts.assertValue(expected.toString()); + ts.assertComplete(); + } + + @Test + public void firstJustError() { + TestSubscriber<Integer> ts = TestSubscriber.create(); + + Flowable.combineLatestDelayError( + Arrays.asList(Flowable.just(1), Flowable.<Integer>error(new TestException())), + new Function<Object[], Integer>() { + @Override + public Integer apply(Object[] args) { + return ((Integer)args[0]) + ((Integer)args[1]); + } + } + ).subscribe(ts); + + ts.assertNoValues(); + ts.assertError(TestException.class); + ts.assertNotComplete(); + } + + @Test + public void secondJustError() { + TestSubscriber<Integer> ts = TestSubscriber.create(); + + Flowable.combineLatestDelayError( + Arrays.asList(Flowable.<Integer>error(new TestException()), Flowable.just(1)), + new Function<Object[], Integer>() { + @Override + public Integer apply(Object[] args) { + return ((Integer)args[0]) + ((Integer)args[1]); + } + } + ).subscribe(ts); + + ts.assertNoValues(); + ts.assertError(TestException.class); + ts.assertNotComplete(); + } + + @Test + public void oneErrors() { + TestSubscriber<Integer> ts = TestSubscriber.create(); + + Flowable.combineLatestDelayError( + Arrays.asList(Flowable.just(10).concatWith(Flowable.<Integer>error(new TestException())), Flowable.just(1)), + new Function<Object[], Integer>() { + @Override + public Integer apply(Object[] args) { + return ((Integer)args[0]) + ((Integer)args[1]); + } + } + ).subscribe(ts); + + ts.assertValues(11); + ts.assertError(TestException.class); + ts.assertNotComplete(); + } + + @Test + public void twoErrors() { + TestSubscriber<Integer> ts = TestSubscriber.create(); + + Flowable.combineLatestDelayError( + Arrays.asList(Flowable.just(1), Flowable.just(10).concatWith(Flowable.<Integer>error(new TestException()))), + new Function<Object[], Integer>() { + @Override + public Integer apply(Object[] args) { + return ((Integer)args[0]) + ((Integer)args[1]); + } + } + ).subscribe(ts); + + ts.assertValues(11); + ts.assertError(TestException.class); + ts.assertNotComplete(); + } + + @Test + public void bothError() { + TestSubscriber<Integer> ts = TestSubscriber.create(); + + Flowable.combineLatestDelayError( + Arrays.asList(Flowable.just(1).concatWith(Flowable.<Integer>error(new TestException())), + Flowable.just(10).concatWith(Flowable.<Integer>error(new TestException()))), + new Function<Object[], Integer>() { + @Override + public Integer apply(Object[] args) { + return ((Integer)args[0]) + ((Integer)args[1]); + } + } + ).subscribe(ts); + + ts.assertValues(11); + ts.assertError(CompositeException.class); + ts.assertNotComplete(); + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + @Test + public void combineLatestNArguments() throws Exception { + Flowable source = Flowable.just(1); + + for (int i = 2; i < 10; i++) { + Class<?>[] types = new Class[i + 1]; + Arrays.fill(types, Publisher.class); + types[i] = i == 2 ? BiFunction.class : Class.forName("io.reactivex.rxjava3.functions.Function" + i); + + Method m = Flowable.class.getMethod("combineLatest", types); + + Object[] params = new Object[i + 1]; + Arrays.fill(params, source); + params[i] = ArgsToString.INSTANCE; + + StringBuilder b = new StringBuilder(); + for (int j = 0; j < i; j++) { + b.append('1'); + } + + ((Flowable)m.invoke(null, params)).test().assertResult(b.toString()); + + for (int j = 0; j < params.length; j++) { + Object[] params0 = params.clone(); + params0[j] = null; + + try { + m.invoke(null, params0); + fail("Should have thrown @ " + m); + } catch (InvocationTargetException ex) { + assertTrue(ex.toString(), ex.getCause() instanceof NullPointerException); + + if (j < i) { + assertEquals("source" + (j + 1) + " is null", ex.getCause().getMessage()); + } else { + assertEquals("combiner is null", ex.getCause().getMessage()); + } + } + } + } + } + + @SuppressWarnings("unchecked") + @Test + public void combineLatestArrayNSources() { + for (int i = 1; i < 100; i++) { + Flowable<Integer>[] sources = new Flowable[i]; + Arrays.fill(sources, Flowable.just(1)); + List<Object> expected = new ArrayList<>(i); + for (int j = 1; j <= i; j++) { + expected.add(1); + } + + Flowable.combineLatestArray(sources, new Function<Object[], List<Object>>() { + @Override + public List<Object> apply(Object[] t) throws Exception { + return Arrays.asList(t); + } + }) + .test() + .assertResult(expected); + + Flowable.combineLatestArrayDelayError(sources, new Function<Object[], List<Object>>() { + @Override + public List<Object> apply(Object[] t) throws Exception { + return Arrays.asList(t); + } + }) + .test() + .assertResult(expected); + } + } + + @SuppressWarnings("unchecked") + @Test + public void combineLatestArrayOfSources() { + + Flowable.combineLatestArray(new Flowable[] { + Flowable.just(1), Flowable.just(2) + }, new Function<Object[], Object>() { + @Override + public Object apply(Object[] a) throws Exception { + return Arrays.toString(a); + } + }) + .test() + .assertResult("[1, 2]"); + } + + @Test + @SuppressWarnings("unchecked") + public void combineLatestDelayErrorArrayOfSources() { + + Flowable.combineLatestArrayDelayError(new Flowable[] { + Flowable.just(1), Flowable.just(2) + }, new Function<Object[], Object>() { + @Override + public Object apply(Object[] a) throws Exception { + return Arrays.toString(a); + } + }) + .test() + .assertResult("[1, 2]"); + } + + @Test + @SuppressWarnings("unchecked") + public void combineLatestDelayErrorArrayOfSourcesWithError() { + + Flowable.combineLatestArrayDelayError(new Flowable[] { + Flowable.just(1), Flowable.just(2).concatWith(Flowable.<Integer>error(new TestException())) + }, new Function<Object[], Object>() { + @Override + public Object apply(Object[] a) throws Exception { + return Arrays.toString(a); + } + }) + .test() + .assertFailure(TestException.class, "[1, 2]"); + } + + @Test + public void combineLatestDelayErrorIterableOfSources() { + + Flowable.combineLatestDelayError(Arrays.asList( + Flowable.just(1), Flowable.just(2) + ), new Function<Object[], Object>() { + @Override + public Object apply(Object[] a) throws Exception { + return Arrays.toString(a); + } + }) + .test() + .assertResult("[1, 2]"); + } + + @Test + public void combineLatestDelayErrorIterableOfSourcesWithError() { + + Flowable.combineLatestDelayError(Arrays.asList( + Flowable.just(1), Flowable.just(2).concatWith(Flowable.<Integer>error(new TestException())) + ), new Function<Object[], Object>() { + @Override + public Object apply(Object[] a) throws Exception { + return Arrays.toString(a); + } + }) + .test() + .assertFailure(TestException.class, "[1, 2]"); + } + + @SuppressWarnings("unchecked") + @Test + public void combineLatestArrayEmpty() { + assertSame(Flowable.empty(), Flowable.combineLatestArray(new Flowable[0], Functions.<Object[]>identity(), 16)); + } + + @SuppressWarnings("unchecked") + @Test + public void combineLatestDelayErrorEmpty() { + assertSame(Flowable.empty(), Flowable.combineLatestArrayDelayError(new Flowable[0], Functions.<Object[]>identity(), 16)); + } + + @Test + public void error() { + Flowable.combineLatest(Flowable.never(), Flowable.error(new TestException()), new BiFunction<Object, Object, Object>() { + @Override + public Object apply(Object a, Object b) throws Exception { + return a; + } + }) + .test() + .assertFailure(TestException.class); + } + + @Test + public void disposed() { + TestHelper.checkDisposed(Flowable.combineLatest(Flowable.never(), Flowable.never(), new BiFunction<Object, Object, Object>() { + @Override + public Object apply(Object a, Object b) throws Exception { + return a; + } + })); + } + + @Test + public void cancelWhileSubscribing() { + final TestSubscriber<Object> ts = new TestSubscriber<>(); + + Flowable.combineLatest( + Flowable.just(1) + .doOnNext(new Consumer<Integer>() { + @Override + public void accept(Integer v) throws Exception { + ts.cancel(); + } + }), + Flowable.never(), + new BiFunction<Object, Object, Object>() { + @Override + public Object apply(Object a, Object b) throws Exception { + return a; + } + }) + .subscribe(ts); + } + + @Test + public void onErrorRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + final PublishProcessor<Integer> pp1 = PublishProcessor.create(); + final PublishProcessor<Integer> pp2 = PublishProcessor.create(); + + TestSubscriberEx<Integer> ts = Flowable.combineLatest(pp1, pp2, new BiFunction<Integer, Integer, Integer>() { + @Override + public Integer apply(Integer a, Integer b) throws Exception { + return a; + } + }).to(TestHelper.<Integer>testConsumer()); + + final TestException ex1 = new TestException(); + final TestException ex2 = new TestException(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + pp1.onError(ex1); + } + }; + Runnable r2 = new Runnable() { + @Override + public void run() { + pp2.onError(ex2); + } + }; + + TestHelper.race(r1, r2); + + if (ts.errors().size() != 0) { + if (ts.errors().get(0) instanceof CompositeException) { + ts.assertSubscribed() + .assertNotComplete() + .assertNoValues(); + + for (Throwable e : TestHelper.errorList(ts)) { + assertTrue(e.toString(), e instanceof TestException); + } + + } else { + ts.assertFailure(TestException.class); + } + } + + for (Throwable e : errors) { + assertTrue(e.toString(), e.getCause() instanceof TestException); + } + } finally { + RxJavaPlugins.reset(); + } + } + } + + @Test + public void combineAsync() { + Flowable<Integer> source = Flowable.range(1, 1000).subscribeOn(Schedulers.computation()); + + Flowable.combineLatest(source, source, new BiFunction<Object, Object, Object>() { + @Override + public Object apply(Object a, Object b) throws Exception { + return a; + } + }) + .take(500) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertNoErrors() + .assertComplete(); + } + + @SuppressWarnings("unchecked") + @Test + public void errorDelayed() { + Flowable.combineLatestArrayDelayError( + new Publisher[] { Flowable.error(new TestException()), Flowable.just(1) }, + new Function<Object[], Object>() { + @Override + public Object apply(Object[] a) throws Exception { + return a; + } + }, + 128 + ) + .test() + .assertFailure(TestException.class); + } + + @SuppressWarnings("unchecked") + @Test + public void errorDelayed2() { + Flowable.combineLatestArrayDelayError( + new Publisher[] { Flowable.error(new TestException()).startWithItem(1), Flowable.empty() }, + new Function<Object[], Object>() { + @Override + public Object apply(Object[] a) throws Exception { + return a; + } + }, + 128 + ) + .test() + .assertFailure(TestException.class); + } + + @Test + public void dontSubscribeIfDone() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + final int[] count = { 0 }; + + Flowable.combineLatest(Flowable.empty(), + Flowable.error(new TestException()) + .doOnSubscribe(new Consumer<Subscription>() { + @Override + public void accept(Subscription s) throws Exception { + count[0]++; + } + }), + new BiFunction<Object, Object, Object>() { + @Override + public Object apply(Object a, Object b) throws Exception { + return 0; + } + }) + .test() + .assertResult(); + + assertEquals(0, count[0]); + + assertTrue(errors.toString(), errors.isEmpty()); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void dontSubscribeIfDone2() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + final int[] count = { 0 }; + + Flowable.combineLatestDelayError( + Arrays.asList(Flowable.empty(), + Flowable.error(new TestException()) + .doOnSubscribe(new Consumer<Subscription>() { + @Override + public void accept(Subscription s) throws Exception { + count[0]++; + } + }) + ), + new Function<Object[], Object>() { + @Override + public Object apply(Object[] a) throws Exception { + return 0; + } + }) + .test() + .assertResult(); + + assertEquals(0, count[0]); + + assertTrue(errors.toString(), errors.isEmpty()); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void combine2Flowable2Errors() throws Exception { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + TestSubscriber<Object> testSubscriber = TestSubscriber.create(); + + TestScheduler testScheduler = new TestScheduler(); + + Flowable<Integer> emptyFlowable = Flowable.timer(10, TimeUnit.MILLISECONDS, testScheduler) + .flatMap(new Function<Long, Publisher<Integer>>() { + @Override + public Publisher<Integer> apply(Long aLong) throws Exception { + return Flowable.error(new Exception()); + } + }); + Flowable<Object> errorFlowable = Flowable.timer(100, TimeUnit.MILLISECONDS, testScheduler).map(new Function<Long, Object>() { + @Override + public Object apply(Long aLong) throws Exception { + throw new Exception(); + } + }); + + Flowable.combineLatestDelayError( + Arrays.asList( + emptyFlowable + .doOnEach(new Consumer<Notification<Integer>>() { + @Override + public void accept(Notification<Integer> integerNotification) throws Exception { + System.out.println("emptyFlowable: " + integerNotification); + } + }) + .doFinally(new Action() { + @Override + public void run() throws Exception { + System.out.println("emptyFlowable: doFinally"); + } + }), + errorFlowable + .doOnEach(new Consumer<Notification<Object>>() { + @Override + public void accept(Notification<Object> integerNotification) throws Exception { + System.out.println("errorFlowable: " + integerNotification); + } + }) + .doFinally(new Action() { + @Override + public void run() throws Exception { + System.out.println("errorFlowable: doFinally"); + } + })), + new Function<Object[], Object>() { + @Override + public Object apply(Object[] objects) throws Exception { + return 0; + } + } + ) + .doOnEach(new Consumer<Notification<Object>>() { + @Override + public void accept(Notification<Object> integerNotification) throws Exception { + System.out.println("combineLatestDelayError: " + integerNotification); + } + }) + .doFinally(new Action() { + @Override + public void run() throws Exception { + System.out.println("combineLatestDelayError: doFinally"); + } + }) + .subscribe(testSubscriber); + + testScheduler.advanceTimeBy(100, TimeUnit.MILLISECONDS); + + testSubscriber.awaitDone(5, TimeUnit.SECONDS); + + assertTrue(errors.toString(), errors.isEmpty()); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void eagerDispose() { + final PublishProcessor<Integer> pp1 = PublishProcessor.create(); + final PublishProcessor<Integer> pp2 = PublishProcessor.create(); + + TestSubscriber<Integer> ts = new TestSubscriber<Integer>() { + @Override + public void onNext(Integer t) { + super.onNext(t); + cancel(); + if (pp1.hasSubscribers()) { + onError(new IllegalStateException("pp1 not disposed")); + } else + if (pp2.hasSubscribers()) { + onError(new IllegalStateException("pp2 not disposed")); + } else { + onComplete(); + } + } + }; + + Flowable.combineLatest(pp1, pp2, new BiFunction<Integer, Integer, Integer>() { + @Override + public Integer apply(Integer t1, Integer t2) throws Exception { + return t1 + t2; + } + }) + .subscribe(ts); + + pp1.onNext(1); + pp2.onNext(2); + ts.assertResult(3); + } + + @Test + public void fusedNullCheck() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<Integer>().setInitialFusionMode(QueueFuseable.ASYNC); + + Flowable.combineLatest(Flowable.just(1), Flowable.just(2), new BiFunction<Integer, Integer, Integer>() { + @Override + public Integer apply(Integer t1, Integer t2) throws Exception { + return null; + } + }) + .subscribe(ts); + + ts + .assertFusionMode(QueueFuseable.ASYNC) + .assertFailureAndMessage(NullPointerException.class, "The combiner returned a null value"); + } + + @Test + public void syncFirstErrorsAfterItemDelayError() { + Flowable.combineLatestDelayError(Arrays.asList( + Flowable.just(21).concatWith(Flowable.<Integer>error(new TestException())), + Flowable.just(21).delay(100, TimeUnit.MILLISECONDS) + ), + new Function<Object[], Object>() { + @Override + public Object apply(Object[] a) throws Exception { + return (Integer)a[0] + (Integer)a[1]; + } + } + ) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertFailure(TestException.class, 42); + } + + @Test + public void publishersInIterable() { + Publisher<Integer> source = new Publisher<Integer>() { + @Override + public void subscribe(Subscriber<? super Integer> subscriber) { + Flowable.just(1).subscribe(subscriber); + } + }; + + Flowable.combineLatest(Arrays.asList(source, source), new Function<Object[], Integer>() { + @Override + public Integer apply(Object[] t) throws Throwable { + return 2; + } + }) + .test() + .assertResult(2); + } + + @Test + public void FlowableSourcesInIterable() { + Flowable<Integer> source = new Flowable<Integer>() { + @Override + public void subscribeActual(Subscriber<? super Integer> s) { + Flowable.just(1).subscribe(s); + } + }; + + Flowable.combineLatest(Arrays.asList(source, source), new Function<Object[], Integer>() { + @Override + public Integer apply(Object[] t) throws Throwable { + return 2; + } + }) + .test() + .assertResult(2); + } + + @Test + public void onCompleteDisposeRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + + TestSubscriber<Integer> ts = new TestSubscriber<>(); + PublishProcessor<Integer> pp = PublishProcessor.create(); + + Flowable.combineLatest(pp, Flowable.never(), (a, b) -> a) + .subscribe(ts); + + TestHelper.race(() -> pp.onComplete(), () -> ts.cancel()); + } + } + + @Test + public void onErrorDisposeDelayErrorRace() throws Throwable { + TestHelper.withErrorTracking(errors -> { + TestException ex = new TestException(); + + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + + TestSubscriberEx<Object[]> ts = new TestSubscriberEx<>(); + AtomicReference<Subscriber<? super Object>> ref = new AtomicReference<>(); + Flowable<Object> f = new Flowable<Object>() { + @Override + public void subscribeActual(Subscriber<? super Object> s) { + ref.set(s); + } + }; + + Flowable.combineLatestDelayError(Arrays.asList(f, Flowable.never()), (a) -> a) + .subscribe(ts); + + ref.get().onSubscribe(new BooleanSubscription()); + + TestHelper.race(() -> ref.get().onError(ex), () -> ts.cancel()); + + if (ts.errors().isEmpty()) { + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } + } + }); + } + + @Test + public void doneButNotEmpty() { + PublishProcessor<Integer> pp1 = PublishProcessor.create(); + PublishProcessor<Integer> pp2 = PublishProcessor.create(); + + TestSubscriber<Integer> ts = Flowable.combineLatest(pp1, pp2, (a, b) -> a + b) + .doOnNext(v -> { + if (v == 2) { + pp2.onNext(3); + pp2.onComplete(); + pp1.onComplete(); + } + }) + .test(); + + pp1.onNext(1); + pp2.onNext(1); + + ts.assertResult(2, 4); + } + + @Test + public void iterableNullPublisher() { + Flowable.combineLatest(Arrays.asList(Flowable.never(), null), (a) -> a) + .test() + .assertFailure(NullPointerException.class); + } + + @Test + public void badRequest() { + TestHelper.assertBadRequestReported(Flowable.combineLatest(Flowable.never(), Flowable.never(), (a, b) -> a)); + } + + @Test + public void syncFusionRejected() { + TestSubscriberEx<Object> ts = new TestSubscriberEx<>(); + ts.setInitialFusionMode(QueueFuseable.SYNC); + + Flowable.combineLatest(Flowable.never(), Flowable.never(), (a, b) -> a) + .subscribe(ts); + + ts.assertFuseable() + .assertFusionMode(QueueFuseable.NONE); + } + + @Test + public void bounderyFusionRejected() { + TestSubscriberEx<Object> ts = new TestSubscriberEx<>(); + ts.setInitialFusionMode(QueueFuseable.ANY | QueueFuseable.BOUNDARY); + + Flowable.combineLatest(Flowable.never(), Flowable.never(), (a, b) -> a) + .subscribe(ts); + + ts.assertFuseable() + .assertFusionMode(QueueFuseable.NONE); + } + + @Test + public void fusedNormal() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(); + ts.setInitialFusionMode(QueueFuseable.ANY); + + Flowable.combineLatest(Flowable.just(1), Flowable.just(2), (a, b) -> a + b) + .subscribeWith(ts) + .assertFuseable() + .assertFusionMode(QueueFuseable.ASYNC) + .assertResult(3); + } + + @Test + public void fusedToParallel() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(); + ts.setInitialFusionMode(QueueFuseable.ANY); + + Flowable.combineLatest(Flowable.just(1), Flowable.just(2), (a, b) -> a + b) + .parallel() + .sequential() + .subscribeWith(ts) + .assertResult(3); + } + + @Test + public void fusedToParallel2() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(); + ts.setInitialFusionMode(QueueFuseable.ANY); + + Flowable.combineLatest(Flowable.just(1), Flowable.just(2), (a, b) -> a + b) + .compose(TestHelper.flowableStripBoundary()) + .parallel() + .sequential() + .subscribeWith(ts) + .assertResult(3); + } + + @Test + public void fusedError() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(); + ts.setInitialFusionMode(QueueFuseable.ANY); + + Flowable.combineLatest(Flowable.just(1), Flowable.<Integer>error(new TestException()), (a, b) -> a + b) + .subscribeWith(ts) + .assertFuseable() + .assertFusionMode(QueueFuseable.ASYNC) + .assertFailure(TestException.class); + } + + @Test + public void nonFusedMoreWorkBeforeTermination() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + + TestSubscriber<Integer> ts = Flowable.combineLatest(pp, Flowable.just(1), (a, b) -> a + b) + .doOnNext(v -> { + if (v == 1) { + pp.onNext(2); + pp.onComplete(); + } + }) + .test(); + + pp.onNext(0); + + ts.assertResult(1, 3); + } + + @Test + public void nonFusedDelayErrorMoreWorkBeforeTermination() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + + TestSubscriber<List<Object>> ts = Flowable.combineLatestDelayError(Arrays.asList(pp, Flowable.just(1)), a -> Arrays.asList(a)) + .doOnNext(v -> { + if (((Integer)v.get(0)) == 0) { + pp.onNext(2); + pp.onComplete(); + } + }) + .test(); + + pp.onNext(0); + + ts.assertResult(Arrays.asList(0, 1), Arrays.asList(2, 1)); + } + + @Test + public void fusedCombinerCrashError() { + TestSubscriberEx<Object> ts = new TestSubscriberEx<>(); + ts.setInitialFusionMode(QueueFuseable.ANY); + + Flowable.combineLatest(Flowable.just(1), Flowable.just(1), (a, b) -> { throw new TestException(); }) + .subscribeWith(ts) + .assertFuseable() + .assertFusionMode(QueueFuseable.ASYNC) + .assertFailure(TestException.class); + } + + @Test + public void fusedCombinerCrashError2() { + Flowable.combineLatest(Flowable.just(1), Flowable.just(1), (a, b) -> { throw new TestException(); }) + .compose(TestHelper.flowableStripBoundary()) + .rebatchRequests(10) + .test() + .assertFailure(TestException.class); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableConcatDelayErrorTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableConcatDelayErrorTest.java new file mode 100644 index 0000000000..39d6cf6ff5 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableConcatDelayErrorTest.java @@ -0,0 +1,283 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import static org.junit.Assert.*; + +import java.util.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.processors.PublishProcessor; +import io.reactivex.rxjava3.subscribers.TestSubscriber; +import io.reactivex.rxjava3.testsupport.TestSubscriberEx; + +public class FlowableConcatDelayErrorTest extends RxJavaTest { + + @Test + public void mainCompletes() { + PublishProcessor<Integer> source = PublishProcessor.create(); + + TestSubscriber<Integer> ts = TestSubscriber.create(); + + source.concatMapDelayError(new Function<Integer, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Integer v) { + return Flowable.range(v, 2); + } + }).subscribe(ts); + + source.onNext(1); + source.onNext(2); + source.onComplete(); + + ts.assertValues(1, 2, 2, 3); + ts.assertNoErrors(); + ts.assertComplete(); + } + + @Test + public void mainErrors() { + PublishProcessor<Integer> source = PublishProcessor.create(); + + TestSubscriber<Integer> ts = TestSubscriber.create(); + + source.concatMapDelayError(new Function<Integer, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Integer v) { + return Flowable.range(v, 2); + } + }).subscribe(ts); + + source.onNext(1); + source.onNext(2); + source.onError(new TestException()); + + ts.assertValues(1, 2, 2, 3); + ts.assertError(TestException.class); + ts.assertNotComplete(); + } + + @Test + public void innerErrors() { + final Flowable<Integer> inner = Flowable.range(1, 2) + .concatWith(Flowable.<Integer>error(new TestException())); + + TestSubscriber<Integer> ts = TestSubscriber.create(); + + Flowable.range(1, 3).concatMapDelayError(new Function<Integer, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Integer v) { + return inner; + } + }).subscribe(ts); + + ts.assertValues(1, 2, 1, 2, 1, 2); + ts.assertError(CompositeException.class); + ts.assertNotComplete(); + } + + @Test + public void singleInnerErrors() { + final Flowable<Integer> inner = Flowable.range(1, 2).concatWith(Flowable.<Integer>error(new TestException())); + + TestSubscriber<Integer> ts = TestSubscriber.create(); + + Flowable.just(1) + .hide() // prevent scalar optimization + .concatMapDelayError(new Function<Integer, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Integer v) { + return inner; + } + }).subscribe(ts); + + ts.assertValues(1, 2); + ts.assertError(TestException.class); + ts.assertNotComplete(); + } + + @Test + public void innerNull() { + TestSubscriber<Integer> ts = TestSubscriber.create(); + + Flowable.just(1) + .hide() // prevent scalar optimization + .concatMapDelayError(new Function<Integer, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Integer v) { + return null; + } + }).subscribe(ts); + + ts.assertNoValues(); + ts.assertError(NullPointerException.class); + ts.assertNotComplete(); + } + + @Test + public void innerThrows() { + TestSubscriber<Integer> ts = TestSubscriber.create(); + + Flowable.just(1) + .hide() // prevent scalar optimization + .concatMapDelayError(new Function<Integer, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Integer v) { + throw new TestException(); + } + }).subscribe(ts); + + ts.assertNoValues(); + ts.assertError(TestException.class); + ts.assertNotComplete(); + } + + @Test + public void innerWithEmpty() { + TestSubscriber<Integer> ts = TestSubscriber.create(); + + Flowable.range(1, 3) + .concatMapDelayError(new Function<Integer, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Integer v) { + return v == 2 ? Flowable.<Integer>empty() : Flowable.range(1, 2); + } + }).subscribe(ts); + + ts.assertValues(1, 2, 1, 2); + ts.assertNoErrors(); + ts.assertComplete(); + } + + @Test + public void innerWithScalar() { + TestSubscriber<Integer> ts = TestSubscriber.create(); + + Flowable.range(1, 3) + .concatMapDelayError(new Function<Integer, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Integer v) { + return v == 2 ? Flowable.just(3) : Flowable.range(1, 2); + } + }).subscribe(ts); + + ts.assertValues(1, 2, 3, 1, 2); + ts.assertNoErrors(); + ts.assertComplete(); + } + + @Test + public void backpressure() { + TestSubscriber<Integer> ts = TestSubscriber.create(0); + + Flowable.range(1, 3).concatMapDelayError(new Function<Integer, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Integer v) { + return Flowable.range(v, 2); + } + }).subscribe(ts); + + ts.assertNoValues(); + ts.assertNoErrors(); + ts.assertNotComplete(); + + ts.request(1); + ts.assertValues(1); + ts.assertNoErrors(); + ts.assertNotComplete(); + + ts.request(3); + ts.assertValues(1, 2, 2, 3); + ts.assertNoErrors(); + ts.assertNotComplete(); + + ts.request(2); + + ts.assertValues(1, 2, 2, 3, 3, 4); + ts.assertNoErrors(); + ts.assertComplete(); + } + + static <T> Flowable<T> withError(Flowable<T> source) { + return source.concatWith(Flowable.<T>error(new TestException())); + } + + @Test + public void concatDelayErrorFlowable() { + TestSubscriber<Integer> ts = TestSubscriber.create(); + + Flowable.concatDelayError( + Flowable.just(Flowable.just(1), Flowable.just(2))) + .subscribe(ts); + + ts.assertValues(1, 2); + ts.assertNoErrors(); + ts.assertComplete(); + } + + @Test + public void concatDelayErrorFlowableError() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(); + + Flowable.concatDelayError( + withError(Flowable.just(withError(Flowable.just(1)), withError(Flowable.just(2))))) + .subscribe(ts); + + ts.assertValues(1, 2); + ts.assertError(CompositeException.class); + ts.assertNotComplete(); + + CompositeException ce = (CompositeException)ts.errors().get(0); + List<Throwable> cex = ce.getExceptions(); + + assertEquals(3, cex.size()); + + assertTrue(cex.get(0).toString(), cex.get(0) instanceof TestException); + assertTrue(cex.get(1).toString(), cex.get(1) instanceof TestException); + assertTrue(cex.get(2).toString(), cex.get(2) instanceof TestException); + } + + @Test + public void concatDelayErrorIterable() { + TestSubscriber<Integer> ts = TestSubscriber.create(); + + Flowable.concatDelayError( + Arrays.asList(Flowable.just(1), Flowable.just(2))) + .subscribe(ts); + + ts.assertValues(1, 2); + ts.assertNoErrors(); + ts.assertComplete(); + } + + @Test + public void concatDelayErrorIterableError() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(); + + Flowable.concatDelayError( + Arrays.asList(withError(Flowable.just(1)), withError(Flowable.just(2)))) + .subscribe(ts); + + ts.assertValues(1, 2); + ts.assertError(CompositeException.class); + ts.assertNotComplete(); + + assertEquals(2, ((CompositeException)ts.errors().get(0)).getExceptions().size()); + } + +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableConcatMapEagerTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableConcatMapEagerTest.java new file mode 100644 index 0000000000..3711aecec4 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableConcatMapEagerTest.java @@ -0,0 +1,1434 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import static org.junit.Assert.*; + +import java.util.*; +import java.util.concurrent.*; +import java.util.concurrent.atomic.*; + +import org.junit.*; +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.internal.subscriptions.BooleanSubscription; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.processors.*; +import io.reactivex.rxjava3.schedulers.Schedulers; +import io.reactivex.rxjava3.subscribers.TestSubscriber; +import io.reactivex.rxjava3.testsupport.*; + +public class FlowableConcatMapEagerTest extends RxJavaTest { + + @Test + public void normal() { + Flowable.range(1, 5) + .concatMapEager(new Function<Integer, Publisher<Integer>>() { + @Override + public Publisher<Integer> apply(Integer t) { + return Flowable.range(t, 2); + } + }) + .test() + .assertResult(1, 2, 2, 3, 3, 4, 4, 5, 5, 6); + } + + @Test + public void normalBackpressured() { + TestSubscriber<Integer> ts = Flowable.range(1, 5) + .concatMapEager(new Function<Integer, Publisher<Integer>>() { + @Override + public Publisher<Integer> apply(Integer t) { + return Flowable.range(t, 2); + } + }) + .test(3); + + ts.assertValues(1, 2, 2); + + ts.request(1); + + ts.assertValues(1, 2, 2, 3); + + ts.request(1); + + ts.assertValues(1, 2, 2, 3, 3); + + ts.request(5); + + ts.assertResult(1, 2, 2, 3, 3, 4, 4, 5, 5, 6); + } + + @Test + public void normalDelayBoundary() { + Flowable.range(1, 5) + .concatMapEagerDelayError(new Function<Integer, Publisher<Integer>>() { + @Override + public Publisher<Integer> apply(Integer t) { + return Flowable.range(t, 2); + } + }, false) + .test() + .assertResult(1, 2, 2, 3, 3, 4, 4, 5, 5, 6); + } + + @Test + public void normalDelayBoundaryBackpressured() { + TestSubscriber<Integer> ts = Flowable.range(1, 5) + .concatMapEagerDelayError(new Function<Integer, Publisher<Integer>>() { + @Override + public Publisher<Integer> apply(Integer t) { + return Flowable.range(t, 2); + } + }, false) + .test(3); + + ts.assertValues(1, 2, 2); + + ts.request(1); + + ts.assertValues(1, 2, 2, 3); + + ts.request(1); + + ts.assertValues(1, 2, 2, 3, 3); + + ts.request(5); + + ts.assertResult(1, 2, 2, 3, 3, 4, 4, 5, 5, 6); + } + + @Test + public void normalDelayEnd() { + Flowable.range(1, 5) + .concatMapEagerDelayError(new Function<Integer, Publisher<Integer>>() { + @Override + public Publisher<Integer> apply(Integer t) { + return Flowable.range(t, 2); + } + }, true) + .test() + .assertResult(1, 2, 2, 3, 3, 4, 4, 5, 5, 6); + } + + @Test + public void normalDelayEndBackpressured() { + TestSubscriber<Integer> ts = Flowable.range(1, 5) + .concatMapEagerDelayError(new Function<Integer, Publisher<Integer>>() { + @Override + public Publisher<Integer> apply(Integer t) { + return Flowable.range(t, 2); + } + }, true) + .test(3); + + ts.assertValues(1, 2, 2); + + ts.request(1); + + ts.assertValues(1, 2, 2, 3); + + ts.request(1); + + ts.assertValues(1, 2, 2, 3, 3); + + ts.request(5); + + ts.assertResult(1, 2, 2, 3, 3, 4, 4, 5, 5, 6); + } + + @Test + public void mainErrorsDelayBoundary() { + PublishProcessor<Integer> main = PublishProcessor.create(); + final PublishProcessor<Integer> inner = PublishProcessor.create(); + + TestSubscriberEx<Integer> ts = main.concatMapEagerDelayError( + new Function<Integer, Publisher<Integer>>() { + @Override + public Publisher<Integer> apply(Integer t) { + return inner; + } + }, false).to(TestHelper.<Integer>testConsumer()); + + main.onNext(1); + + inner.onNext(2); + + ts.assertValue(2); + + main.onError(new TestException("Forced failure")); + + ts.assertNoErrors(); + + inner.onNext(3); + inner.onComplete(); + + ts.assertFailureAndMessage(TestException.class, "Forced failure", 2, 3); + } + + @Test + public void mainErrorsDelayEnd() { + PublishProcessor<Integer> main = PublishProcessor.create(); + final PublishProcessor<Integer> inner = PublishProcessor.create(); + + TestSubscriberEx<Integer> ts = main.concatMapEagerDelayError( + new Function<Integer, Publisher<Integer>>() { + @Override + public Publisher<Integer> apply(Integer t) { + return inner; + } + }, true).to(TestHelper.<Integer>testConsumer()); + + main.onNext(1); + main.onNext(2); + + inner.onNext(2); + + ts.assertValue(2); + + main.onError(new TestException("Forced failure")); + + ts.assertNoErrors(); + + inner.onNext(3); + inner.onComplete(); + + ts.assertFailureAndMessage(TestException.class, "Forced failure", 2, 3, 2, 3); + } + + @Test + public void mainErrorsImmediate() { + PublishProcessor<Integer> main = PublishProcessor.create(); + final PublishProcessor<Integer> inner = PublishProcessor.create(); + + TestSubscriberEx<Integer> ts = main.concatMapEager( + new Function<Integer, Publisher<Integer>>() { + @Override + public Publisher<Integer> apply(Integer t) { + return inner; + } + }).to(TestHelper.<Integer>testConsumer()); + + main.onNext(1); + main.onNext(2); + + inner.onNext(2); + + ts.assertValue(2); + + main.onError(new TestException("Forced failure")); + + assertFalse("inner has subscribers?", inner.hasSubscribers()); + + inner.onNext(3); + inner.onComplete(); + + ts.assertFailureAndMessage(TestException.class, "Forced failure", 2); + } + + @Test + public void longEager() { + + Flowable.range(1, 2 * Flowable.bufferSize()) + .concatMapEager(new Function<Integer, Publisher<Integer>>() { + @Override + public Publisher<Integer> apply(Integer v) { + return Flowable.just(1); + } + }) + .test() + .assertValueCount(2 * Flowable.bufferSize()) + .assertNoErrors() + .assertComplete(); + } + + TestSubscriber<Object> ts; + TestSubscriber<Object> tsBp; + + Function<Integer, Flowable<Integer>> toJust = new Function<Integer, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Integer t) { + return Flowable.just(t); + } + }; + + Function<Integer, Flowable<Integer>> toRange = new Function<Integer, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Integer t) { + return Flowable.range(t, 2); + } + }; + + @Before + public void before() { + ts = new TestSubscriber<>(); + tsBp = new TestSubscriber<>(0L); + } + + @Test + public void simple() { + Flowable.range(1, 100).concatMapEager(toJust).subscribe(ts); + + ts.assertNoErrors(); + ts.assertValueCount(100); + ts.assertComplete(); + } + + @Test + public void simple2() { + Flowable.range(1, 100).concatMapEager(toRange).subscribe(ts); + + ts.assertNoErrors(); + ts.assertValueCount(200); + ts.assertComplete(); + } + + @Test + public void eagerness2() { + final AtomicInteger count = new AtomicInteger(); + Flowable<Integer> source = Flowable.just(1).doOnNext(new Consumer<Integer>() { + @Override + public void accept(Integer t) { + count.getAndIncrement(); + } + }).hide(); + + Flowable.concatArrayEager(source, source).subscribe(tsBp); + + Assert.assertEquals(2, count.get()); + tsBp.assertNoErrors(); + tsBp.assertNotComplete(); + tsBp.assertNoValues(); + + tsBp.request(Long.MAX_VALUE); + + tsBp.assertValueCount(count.get()); + tsBp.assertNoErrors(); + tsBp.assertComplete(); + } + + @Test + public void eagerness3() { + final AtomicInteger count = new AtomicInteger(); + Flowable<Integer> source = Flowable.just(1).doOnNext(new Consumer<Integer>() { + @Override + public void accept(Integer t) { + count.getAndIncrement(); + } + }).hide(); + + Flowable.concatArrayEager(source, source, source).subscribe(tsBp); + + Assert.assertEquals(3, count.get()); + tsBp.assertNoErrors(); + tsBp.assertNotComplete(); + tsBp.assertNoValues(); + + tsBp.request(Long.MAX_VALUE); + + tsBp.assertValueCount(count.get()); + tsBp.assertNoErrors(); + tsBp.assertComplete(); + } + + @Test + public void eagerness4() { + final AtomicInteger count = new AtomicInteger(); + Flowable<Integer> source = Flowable.just(1).doOnNext(new Consumer<Integer>() { + @Override + public void accept(Integer t) { + count.getAndIncrement(); + } + }).hide(); + + Flowable.concatArrayEager(source, source, source, source).subscribe(tsBp); + + Assert.assertEquals(4, count.get()); + tsBp.assertNoErrors(); + tsBp.assertNotComplete(); + tsBp.assertNoValues(); + + tsBp.request(Long.MAX_VALUE); + + tsBp.assertValueCount(count.get()); + tsBp.assertNoErrors(); + tsBp.assertComplete(); + } + + @Test + public void eagerness5() { + final AtomicInteger count = new AtomicInteger(); + Flowable<Integer> source = Flowable.just(1).doOnNext(new Consumer<Integer>() { + @Override + public void accept(Integer t) { + count.getAndIncrement(); + } + }).hide(); + + Flowable.concatArrayEager(source, source, source, source, source).subscribe(tsBp); + + Assert.assertEquals(5, count.get()); + tsBp.assertNoErrors(); + tsBp.assertNotComplete(); + tsBp.assertNoValues(); + + tsBp.request(Long.MAX_VALUE); + + tsBp.assertValueCount(count.get()); + tsBp.assertNoErrors(); + tsBp.assertComplete(); + } + + @Test + public void eagerness6() { + final AtomicInteger count = new AtomicInteger(); + Flowable<Integer> source = Flowable.just(1).doOnNext(new Consumer<Integer>() { + @Override + public void accept(Integer t) { + count.getAndIncrement(); + } + }).hide(); + + Flowable.concatArrayEager(source, source, source, source, source, source).subscribe(tsBp); + + Assert.assertEquals(6, count.get()); + tsBp.assertNoErrors(); + tsBp.assertNotComplete(); + tsBp.assertNoValues(); + + tsBp.request(Long.MAX_VALUE); + + tsBp.assertValueCount(count.get()); + tsBp.assertNoErrors(); + tsBp.assertComplete(); + } + + @Test + public void eagerness7() { + final AtomicInteger count = new AtomicInteger(); + Flowable<Integer> source = Flowable.just(1).doOnNext(new Consumer<Integer>() { + @Override + public void accept(Integer t) { + count.getAndIncrement(); + } + }).hide(); + + Flowable.concatArrayEager(source, source, source, source, source, source, source).subscribe(tsBp); + + Assert.assertEquals(7, count.get()); + tsBp.assertNoErrors(); + tsBp.assertNotComplete(); + tsBp.assertNoValues(); + + tsBp.request(Long.MAX_VALUE); + + tsBp.assertValueCount(count.get()); + tsBp.assertNoErrors(); + tsBp.assertComplete(); + } + + @Test + public void eagerness8() { + final AtomicInteger count = new AtomicInteger(); + Flowable<Integer> source = Flowable.just(1).doOnNext(new Consumer<Integer>() { + @Override + public void accept(Integer t) { + count.getAndIncrement(); + } + }).hide(); + + Flowable.concatArrayEager(source, source, source, source, source, source, source, source).subscribe(tsBp); + + Assert.assertEquals(8, count.get()); + tsBp.assertNoErrors(); + tsBp.assertNotComplete(); + tsBp.assertNoValues(); + + tsBp.request(Long.MAX_VALUE); + + tsBp.assertValueCount(count.get()); + tsBp.assertNoErrors(); + tsBp.assertComplete(); + } + + @Test + public void eagerness9() { + final AtomicInteger count = new AtomicInteger(); + Flowable<Integer> source = Flowable.just(1).doOnNext(new Consumer<Integer>() { + @Override + public void accept(Integer t) { + count.getAndIncrement(); + } + }).hide(); + + Flowable.concatArrayEager(source, source, source, source, source, source, source, source, source).subscribe(tsBp); + + Assert.assertEquals(9, count.get()); + tsBp.assertNoErrors(); + tsBp.assertNotComplete(); + tsBp.assertNoValues(); + + tsBp.request(Long.MAX_VALUE); + + tsBp.assertValueCount(count.get()); + tsBp.assertNoErrors(); + tsBp.assertComplete(); + } + + @Test + public void mainError() { + Flowable.<Integer>error(new TestException()).concatMapEager(toJust).subscribe(ts); + + ts.assertNoValues(); + ts.assertError(TestException.class); + ts.assertNotComplete(); + } + + @Test + public void innerError() { + Flowable.concatArrayEager(Flowable.just(1), Flowable.error(new TestException())).subscribe(ts); + + ts.assertValue(1); + ts.assertError(TestException.class); + ts.assertNotComplete(); + } + + @Test + public void innerEmpty() { + Flowable.concatArrayEager(Flowable.empty(), Flowable.empty()).subscribe(ts); + + ts.assertNoValues(); + ts.assertNoErrors(); + ts.assertComplete(); + } + + @Test + public void mapperThrows() { + Flowable.just(1).concatMapEager(new Function<Integer, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Integer t) { + throw new TestException(); + } + }).subscribe(ts); + + ts.assertNoValues(); + ts.assertNotComplete(); + ts.assertError(TestException.class); + } + + @Test(expected = IllegalArgumentException.class) + public void invalidMaxConcurrent() { + Flowable.just(1).concatMapEager(toJust, 0, Flowable.bufferSize()); + } + + @Test(expected = IllegalArgumentException.class) + public void invalidCapacityHint() { + Flowable.just(1).concatMapEager(toJust, Flowable.bufferSize(), 0); + } + + @Test + public void backpressure() { + Flowable.concatArrayEager(Flowable.just(1), Flowable.just(1)).subscribe(tsBp); + + tsBp.assertNoErrors(); + tsBp.assertNoValues(); + tsBp.assertNotComplete(); + + tsBp.request(1); + tsBp.assertValue(1); + tsBp.assertNoErrors(); + tsBp.assertNotComplete(); + + tsBp.request(1); + tsBp.assertValues(1, 1); + tsBp.assertNoErrors(); + tsBp.assertComplete(); + } + + @Test + public void asynchronousRun() { + Flowable.range(1, 2).concatMapEager(new Function<Integer, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Integer t) { + return Flowable.range(1, 1000).subscribeOn(Schedulers.computation()); + } + }).observeOn(Schedulers.single()) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertNoErrors() + .assertValueCount(2000) + .assertComplete(); + } + + @Test + public void reentrantWork() { + final PublishProcessor<Integer> processor = PublishProcessor.create(); + + final AtomicBoolean once = new AtomicBoolean(); + + processor.concatMapEager(new Function<Integer, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Integer t) { + return Flowable.just(t); + } + }) + .doOnNext(new Consumer<Integer>() { + @Override + public void accept(Integer t) { + if (once.compareAndSet(false, true)) { + processor.onNext(2); + } + } + }) + .subscribe(ts); + + processor.onNext(1); + + ts.assertNoErrors(); + ts.assertNotComplete(); + ts.assertValues(1, 2); + } + + @Test + public void prefetchIsBounded() { + final AtomicInteger count = new AtomicInteger(); + + TestSubscriber<Object> ts = TestSubscriber.create(0); + + Flowable.just(1).concatMapEager(new Function<Integer, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Integer t) { + return Flowable.range(1, Flowable.bufferSize() * 2) + .doOnNext(new Consumer<Integer>() { + @Override + public void accept(Integer t) { + count.getAndIncrement(); + } + }).hide(); + } + }).subscribe(ts); + + ts.assertNoErrors(); + ts.assertNoValues(); + ts.assertNotComplete(); + Assert.assertEquals(Flowable.bufferSize(), count.get()); + } + + @Test + public void maxConcurrent5() { + final List<Long> requests = new ArrayList<>(); + Flowable.range(1, 100).doOnRequest(new LongConsumer() { + @Override + public void accept(long reqCount) { + requests.add(reqCount); + } + }).concatMapEager(toJust, 5, Flowable.bufferSize()).subscribe(ts); + + ts.assertNoErrors(); + ts.assertValueCount(100); + ts.assertComplete(); + + Assert.assertEquals(5, (long) requests.get(0)); + Assert.assertEquals(1, (long) requests.get(1)); + Assert.assertEquals(1, (long) requests.get(2)); + Assert.assertEquals(1, (long) requests.get(3)); + Assert.assertEquals(1, (long) requests.get(4)); + Assert.assertEquals(1, (long) requests.get(5)); + } + + @Test + public void capacityHint() { + Flowable<Integer> source = Flowable.just(1); + TestSubscriber<Integer> ts = TestSubscriber.create(); + + Flowable.concatEager(Arrays.asList(source, source, source), 1, 1).subscribe(ts); + + ts.assertValues(1, 1, 1); + ts.assertNoErrors(); + ts.assertComplete(); + } + + @Test + public void flowable() { + Flowable<Integer> source = Flowable.just(1); + TestSubscriber<Integer> ts = TestSubscriber.create(); + + Flowable.concatEager(Flowable.just(source, source, source)).subscribe(ts); + + ts.assertValues(1, 1, 1); + ts.assertNoErrors(); + ts.assertComplete(); + } + + @Test + public void flowableCapacityHint() { + Flowable<Integer> source = Flowable.just(1); + TestSubscriber<Integer> ts = TestSubscriber.create(); + + Flowable.concatEager(Flowable.just(source, source, source), 1, 1).subscribe(ts); + + ts.assertValues(1, 1, 1); + ts.assertNoErrors(); + ts.assertComplete(); + } + + @Test + public void badCapacityHint() throws Exception { + Flowable<Integer> source = Flowable.just(1); + try { + Flowable.concatEager(Arrays.asList(source, source, source), 1, -99); + } catch (IllegalArgumentException ex) { + assertEquals("prefetch > 0 required but it was -99", ex.getMessage()); + } + + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + @Test + public void mappingBadCapacityHint() throws Exception { + Flowable<Integer> source = Flowable.just(1); + try { + Flowable.just(source, source, source).concatMapEager((Function)Functions.identity(), 10, -99); + } catch (IllegalArgumentException ex) { + assertEquals("prefetch > 0 required but it was -99", ex.getMessage()); + } + + } + + @Test + public void concatEagerZero() { + Flowable.concatEager(Collections.<Flowable<Integer>>emptyList()) + .test() + .assertResult(); + } + + @Test + public void concatEagerOne() { + Flowable.concatEager(Arrays.asList(Flowable.just(1))) + .test() + .assertResult(1); + } + + @Test + public void concatEagerTwo() { + Flowable.concatEager(Arrays.asList(Flowable.just(1), Flowable.just(2))) + .test() + .assertResult(1, 2); + } + + @Test + public void Flowable() { + Flowable<Integer> source = Flowable.just(1); + TestSubscriber<Integer> ts = TestSubscriber.create(); + + Flowable.concatEager(Flowable.just(source, source, source)).subscribe(ts); + + ts.assertValues(1, 1, 1); + ts.assertNoErrors(); + ts.assertComplete(); + } + + @Test + public void publisherCapacityHint() { + Flowable<Integer> source = Flowable.just(1); + TestSubscriber<Integer> ts = TestSubscriber.create(); + + Flowable.concatEager(Flowable.just(source, source, source), 1, 1).subscribe(ts); + + ts.assertValues(1, 1, 1); + ts.assertNoErrors(); + ts.assertComplete(); + } + + @Test + public void concatEagerIterable() { + Flowable.concatEager(Arrays.asList(Flowable.just(1), Flowable.just(2))) + .test() + .assertResult(1, 2); + } + + @Test + public void empty() { + Flowable.<Integer>empty().hide().concatMapEager(new Function<Integer, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Integer v) throws Exception { + return Flowable.range(1, 2); + } + }) + .test() + .assertResult(); + } + + @Test + public void dispose() { + TestHelper.checkDisposed(Flowable.just(1).hide().concatMapEager(new Function<Integer, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Integer v) throws Exception { + return Flowable.range(1, 2); + } + })); + } + + @Test + public void innerError2() { + Flowable.<Integer>just(1).hide().concatMapEager(new Function<Integer, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Integer v) throws Exception { + return Flowable.error(new TestException()); + } + }) + .test() + .assertFailure(TestException.class); + } + + @Test + public void innerOuterRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + final PublishProcessor<Integer> pp1 = PublishProcessor.create(); + final PublishProcessor<Integer> pp2 = PublishProcessor.create(); + + TestSubscriberEx<Integer> ts = pp1.concatMapEager(new Function<Integer, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Integer v) throws Exception { + return pp2; + } + }).to(TestHelper.<Integer>testConsumer()); + + final TestException ex1 = new TestException(); + final TestException ex2 = new TestException(); + + pp1.onNext(1); + + Runnable r1 = new Runnable() { + @Override + public void run() { + pp1.onError(ex1); + } + }; + Runnable r2 = new Runnable() { + @Override + public void run() { + pp2.onError(ex2); + } + }; + + TestHelper.race(r1, r2); + + ts.assertSubscribed().assertNoValues().assertNotComplete(); + + Throwable ex = ts.errors().get(0); + + if (ex instanceof CompositeException) { + List<Throwable> es = TestHelper.errorList(ts); + TestHelper.assertError(es, 0, TestException.class); + TestHelper.assertError(es, 1, TestException.class); + } else { + ts.assertError(TestException.class); + if (!errors.isEmpty()) { + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } + } + } finally { + RxJavaPlugins.reset(); + } + } + } + + @Test + public void innerErrorMaxConcurrency() { + Flowable.<Integer>just(1).hide().concatMapEager(new Function<Integer, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Integer v) throws Exception { + return Flowable.error(new TestException()); + } + }, 1, 128) + .test() + .assertFailure(TestException.class); + } + + @Test + public void innerCallableThrows() { + Flowable.<Integer>just(1).hide().concatMapEager(new Function<Integer, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Integer v) throws Exception { + return Flowable.fromCallable(new Callable<Integer>() { + @Override + public Integer call() throws Exception { + throw new TestException(); + } + }); + } + }) + .test() + .assertFailure(TestException.class); + } + + @Test + public void innerErrorAfterPoll() { + final UnicastProcessor<Integer> up = UnicastProcessor.create(); + up.onNext(1); + + TestSubscriber<Integer> ts = new TestSubscriber<Integer>() { + @Override + public void onNext(Integer t) { + super.onNext(t); + up.onError(new TestException()); + } + }; + + Flowable.<Integer>just(1).hide() + .concatMapEager(new Function<Integer, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Integer v) throws Exception { + return up; + } + }, 1, 128) + .subscribe(ts); + + ts + .assertFailure(TestException.class, 1); + } + + @Test + public void nextCancelRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final PublishProcessor<Integer> pp1 = PublishProcessor.create(); + + final TestSubscriber<Integer> ts = pp1.concatMapEager(new Function<Integer, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Integer v) throws Exception { + return Flowable.never(); + } + }).test(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + pp1.onNext(1); + } + }; + Runnable r2 = new Runnable() { + @Override + public void run() { + ts.cancel(); + } + }; + + TestHelper.race(r1, r2); + + ts.assertEmpty(); + } + } + + @Test + public void mapperCancels() { + final TestSubscriber<Integer> ts = new TestSubscriber<>(); + + Flowable.just(1).hide() + .concatMapEager(new Function<Integer, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Integer v) throws Exception { + ts.cancel(); + return Flowable.never(); + } + }, 1, 128) + .subscribe(ts); + + ts.assertEmpty(); + } + + @Test + public void innerErrorFused() { + Flowable.<Integer>just(1).hide().concatMapEager(new Function<Integer, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Integer v) throws Exception { + return Flowable.range(1, 2).map(new Function<Integer, Integer>() { + @Override + public Integer apply(Integer v) throws Exception { + throw new TestException(); + } + }); + } + }) + .test() + .assertFailure(TestException.class); + } + + @Test + public void fuseAndTake() { + UnicastProcessor<Integer> up = UnicastProcessor.create(); + + up.onNext(1); + up.onComplete(); + + up.concatMapEager(new Function<Integer, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Integer v) throws Exception { + return Flowable.just(1); + } + }) + .take(1) + .test() + .assertResult(1); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeFlowable(new Function<Flowable<Object>, Flowable<Object>>() { + @Override + public Flowable<Object> apply(Flowable<Object> f) throws Exception { + return f.concatMapEager(new Function<Object, Flowable<Object>>() { + @Override + public Flowable<Object> apply(Object v) throws Exception { + return Flowable.just(v); + } + }); + } + }); + } + + @Test + public void doubleOnError() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + @SuppressWarnings("rawtypes") + final Subscriber[] sub = { null }; + + new Flowable<Integer>() { + @Override + protected void subscribeActual(Subscriber<? super Integer> s) { + sub[0] = s; + s.onSubscribe(new BooleanSubscription()); + s.onNext(1); + s.onError(new TestException("First")); + } + } + .concatMapEager(Functions.justFunction(Flowable.just(1))) + .to(TestHelper.<Integer>testConsumer()) + .assertFailureAndMessage(TestException.class, "First", 1); + + sub[0].onError(new TestException("Second")); + + TestHelper.assertUndeliverable(errors, 0, TestException.class, "Second"); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void innerOverflow() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + Flowable.just(1) + .concatMapEager(new Function<Integer, Publisher<Integer>>() { + @Override + public Publisher<Integer> apply(Integer v) throws Exception { + return new Flowable<Integer>() { + @Override + protected void subscribeActual(Subscriber<? super Integer> s) { + s.onSubscribe(new BooleanSubscription()); + s.onNext(1); + s.onNext(2); + s.onError(new TestException()); + } + }; + } + }, 1, 1) + .test(0L) + .assertFailure(MissingBackpressureException.class); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void unboundedIn() { + int n = Flowable.bufferSize() * 2; + Flowable.range(1, n) + .concatMapEager(new Function<Integer, Publisher<Integer>>() { + @Override + public Publisher<Integer> apply(Integer v) throws Exception { + return Flowable.just(1); + } + }, Integer.MAX_VALUE, 16) + .test() + .assertValueCount(n) + .assertComplete() + .assertNoErrors(); + } + + @Test + public void drainCancelRaceOnEmpty() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final PublishProcessor<Integer> pp = PublishProcessor.create(); + + final TestSubscriber<Integer> ts = new TestSubscriber<>(0L); + + Flowable.just(1) + .concatMapEager(Functions.justFunction(pp)) + .subscribe(ts); + + Runnable r1 = new Runnable() { + @Override + public void run() { + pp.onComplete(); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + ts.cancel(); + } + }; + + TestHelper.race(r1, r2); + } + } + + @Test + public void innerLong() { + int n = Flowable.bufferSize() * 2; + + Flowable.just(1).hide() + .concatMapEager(Functions.justFunction(Flowable.range(1, n).hide())) + .rebatchRequests(1) + .test() + .assertValueCount(n) + .assertComplete() + .assertNoErrors(); + } + + @Test + public void oneDelayed() { + Flowable.just(1, 2, 3, 4, 5) + .concatMapEager(new Function<Integer, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Integer i) throws Exception { + return i == 3 ? Flowable.just(i) : Flowable + .just(i) + .delay(1, TimeUnit.MILLISECONDS, Schedulers.io()); + } + }) + .observeOn(Schedulers.io()) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(1, 2, 3, 4, 5) + ; + } + + @Test + @SuppressWarnings("unchecked") + public void maxConcurrencyOf2() { + List<Integer>[] list = new ArrayList[100]; + for (int i = 0; i < 100; i++) { + List<Integer> lst = new ArrayList<>(); + list[i] = lst; + for (int k = 1; k <= 10; k++) { + lst.add((i) * 10 + k); + } + } + + Flowable.range(1, 1000) + .buffer(10) + .concatMapEager(new Function<List<Integer>, Flowable<List<Integer>>>() { + @Override + public Flowable<List<Integer>> apply(List<Integer> v) + throws Exception { + return Flowable.just(v) + .subscribeOn(Schedulers.io()) + .doOnNext(new Consumer<List<Integer>>() { + @Override + public void accept(List<Integer> v) + throws Exception { + Thread.sleep(new Random().nextInt(20)); + } + }); + } + } + , 2, 3) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(list); + } + + @Test + public void arrayDelayErrorDefault() { + PublishProcessor<Integer> pp1 = PublishProcessor.create(); + PublishProcessor<Integer> pp2 = PublishProcessor.create(); + PublishProcessor<Integer> pp3 = PublishProcessor.create(); + + TestSubscriber<Integer> ts = Flowable.concatArrayEagerDelayError(pp1, pp2, pp3) + .test(); + + ts.assertEmpty(); + + assertTrue(pp1.hasSubscribers()); + assertTrue(pp2.hasSubscribers()); + assertTrue(pp3.hasSubscribers()); + + pp2.onNext(2); + pp2.onComplete(); + + ts.assertEmpty(); + + pp1.onNext(1); + + ts.assertValuesOnly(1); + + pp1.onComplete(); + + ts.assertValuesOnly(1, 2); + + pp3.onComplete(); + + ts.assertResult(1, 2); + } + + @Test + public void arrayDelayErrorMaxConcurrency() { + PublishProcessor<Integer> pp1 = PublishProcessor.create(); + PublishProcessor<Integer> pp2 = PublishProcessor.create(); + PublishProcessor<Integer> pp3 = PublishProcessor.create(); + + TestSubscriber<Integer> ts = Flowable.concatArrayEagerDelayError(2, 2, pp1, pp2, pp3) + .test(); + + ts.assertEmpty(); + + assertTrue(pp1.hasSubscribers()); + assertTrue(pp2.hasSubscribers()); + assertFalse(pp3.hasSubscribers()); + + pp2.onNext(2); + pp2.onComplete(); + + ts.assertEmpty(); + + pp1.onNext(1); + + ts.assertValuesOnly(1); + + pp1.onComplete(); + + assertTrue(pp3.hasSubscribers()); + + ts.assertValuesOnly(1, 2); + + pp3.onComplete(); + + ts.assertResult(1, 2); + } + + @Test + public void arrayDelayErrorMaxConcurrencyErrorDelayed() { + PublishProcessor<Integer> pp1 = PublishProcessor.create(); + PublishProcessor<Integer> pp2 = PublishProcessor.create(); + PublishProcessor<Integer> pp3 = PublishProcessor.create(); + + TestSubscriber<Integer> ts = Flowable.concatArrayEagerDelayError(2, 2, pp1, pp2, pp3) + .test(); + + ts.assertEmpty(); + + assertTrue(pp1.hasSubscribers()); + assertTrue(pp2.hasSubscribers()); + assertFalse(pp3.hasSubscribers()); + + pp2.onNext(2); + pp2.onError(new TestException()); + + ts.assertEmpty(); + + pp1.onNext(1); + + ts.assertValuesOnly(1); + + pp1.onComplete(); + + assertTrue(pp3.hasSubscribers()); + + ts.assertValuesOnly(1, 2); + + pp3.onComplete(); + + ts.assertFailure(TestException.class, 1, 2); + } + + @Test + public void cancelActive() { + PublishProcessor<Integer> pp1 = PublishProcessor.create(); + PublishProcessor<Integer> pp2 = PublishProcessor.create(); + + TestSubscriber<Integer> ts = Flowable + .concatEager(Flowable.just(pp1, pp2)) + .test(); + + assertTrue(pp1.hasSubscribers()); + assertTrue(pp2.hasSubscribers()); + + ts.cancel(); + + assertFalse(pp1.hasSubscribers()); + assertFalse(pp2.hasSubscribers()); + } + + @Test + public void cancelNoInnerYet() { + PublishProcessor<Flowable<Integer>> pp1 = PublishProcessor.create(); + + TestSubscriber<Integer> ts = Flowable + .concatEager(pp1) + .test(); + + assertTrue(pp1.hasSubscribers()); + + ts.cancel(); + + assertFalse(pp1.hasSubscribers()); + } + + @Test + public void undeliverableUponCancel() { + TestHelper.checkUndeliverableUponCancel(new FlowableConverter<Integer, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Flowable<Integer> upstream) { + return upstream.concatMapEager(new Function<Integer, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Integer v) throws Throwable { + return Flowable.just(v).hide(); + } + }); + } + }); + } + + @Test + public void undeliverableUponCancelDelayError() { + TestHelper.checkUndeliverableUponCancel(new FlowableConverter<Integer, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Flowable<Integer> upstream) { + return upstream.concatMapEagerDelayError(new Function<Integer, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Integer v) throws Throwable { + return Flowable.just(v).hide(); + } + }, false); + } + }); + } + + @Test + public void undeliverableUponCancelDelayErrorTillEnd() { + TestHelper.checkUndeliverableUponCancel(new FlowableConverter<Integer, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Flowable<Integer> upstream) { + return upstream.concatMapEagerDelayError(new Function<Integer, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Integer v) throws Throwable { + return Flowable.just(v).hide(); + } + }, true); + } + }); + } + + @Test + public void iterableDelayError() { + Flowable.concatEagerDelayError(Arrays.asList( + Flowable.range(1, 2), + Flowable.error(new TestException()), + Flowable.range(3, 3) + )) + .test() + .assertFailure(TestException.class, 1, 2, 3, 4, 5); + } + + @Test + public void iterableDelayErrorMaxConcurrency() { + Flowable.concatEagerDelayError(Arrays.asList( + Flowable.range(1, 2), + Flowable.error(new TestException()), + Flowable.range(3, 3) + ), 1, 1) + .test() + .assertFailure(TestException.class, 1, 2, 3, 4, 5); + } + + @Test + public void publisherDelayError() { + Flowable.concatEagerDelayError(Flowable.fromArray( + Flowable.range(1, 2), + Flowable.error(new TestException()), + Flowable.range(3, 3) + )) + .test() + .assertFailure(TestException.class, 1, 2, 3, 4, 5); + } + + @Test + public void publisherDelayErrorMaxConcurrency() { + Flowable.concatEagerDelayError(Flowable.fromArray( + Flowable.range(1, 2), + Flowable.error(new TestException()), + Flowable.range(3, 3) + ), 1, 1) + .test() + .assertFailure(TestException.class, 1, 2, 3, 4, 5); + } + + @Test + public void innerSyncFused() { + Flowable.just(1) + .hide() + .concatMapEagerDelayError(v -> Flowable.range(1, 10), true, 1, 1) + .test() + .assertResult(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + } + + @Test + public void badRequest() { + TestHelper.assertBadRequestReported(Flowable.never().concatMapEagerDelayError(v -> Flowable.never(), false)); + } + + @Test + public void cancelAfterOnNext() { + Flowable.just(1) + .hide() + .concatMapEagerDelayError(v -> Flowable.range(1, 5).hide(), true) + .takeUntil(v -> true) + .test() + .assertResult(1); + } + + @Test + public void noInnerQueue() { + Flowable.just(1) + .hide() + .concatMapEagerDelayError(v -> Flowable.fromPublisher(s -> { }), true) + .test(0L) + .assertEmpty() + .requestMore(1L) + .assertEmpty() + ; + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableConcatMapSchedulerTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableConcatMapSchedulerTest.java new file mode 100644 index 0000000000..027157ce02 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableConcatMapSchedulerTest.java @@ -0,0 +1,1283 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import static org.junit.Assert.*; + +import java.lang.reflect.Method; +import java.util.*; +import java.util.concurrent.*; +import java.util.concurrent.atomic.*; + +import org.junit.Test; +import org.reactivestreams.*; + +import io.reactivex.rxjava3.annotations.NonNull; +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.internal.schedulers.ImmediateThinScheduler; +import io.reactivex.rxjava3.internal.subscriptions.*; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.processors.*; +import io.reactivex.rxjava3.schedulers.Schedulers; +import io.reactivex.rxjava3.subscribers.*; +import io.reactivex.rxjava3.testsupport.*; + +public class FlowableConcatMapSchedulerTest extends RxJavaTest { + + @Test + public void boundaryFusion() { + Flowable.range(1, 10000) + .observeOn(Schedulers.single()) + .map(new Function<Integer, String>() { + @Override + public String apply(Integer t) throws Exception { + String name = Thread.currentThread().getName(); + if (name.contains("RxSingleScheduler")) { + return "RxSingleScheduler"; + } + return name; + } + }) + .concatMap(new Function<String, Publisher<? extends Object>>() { + @Override + public Publisher<? extends Object> apply(String v) + throws Exception { + return Flowable.just(v); + } + }, 2, ImmediateThinScheduler.INSTANCE) + .observeOn(Schedulers.computation()) + .distinct() + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertResult("RxSingleScheduler"); + } + + @Test + public void innerScalarRequestRace() { + Flowable<Integer> just = Flowable.just(1); + int n = 1000; + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + PublishProcessor<Flowable<Integer>> source = PublishProcessor.create(); + + TestSubscriber<Integer> ts = source + .concatMap(v -> v, n + 1, ImmediateThinScheduler.INSTANCE) + .test(1L); + + TestHelper.race(() -> { + for (int j = 0; j < n; j++) { + source.onNext(just); + } + }, () -> { + for (int j = 0; j < n; j++) { + ts.request(1); + } + }); + + ts.assertValueCount(n); + } + } + + @Test + public void innerScalarRequestRaceDelayError() { + Flowable<Integer> just = Flowable.just(1); + int n = 1000; + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + PublishProcessor<Flowable<Integer>> source = PublishProcessor.create(); + + TestSubscriber<Integer> ts = source + .concatMapDelayError(v -> v, true, n + 1, ImmediateThinScheduler.INSTANCE) + .test(1L); + + TestHelper.race(() -> { + for (int j = 0; j < n; j++) { + source.onNext(just); + } + }, () -> { + for (int j = 0; j < n; j++) { + ts.request(1); + } + }); + + ts.assertValueCount(n); + } + } + + @Test + public void boundaryFusionDelayError() { + Flowable.range(1, 10000) + .observeOn(Schedulers.single()) + .map(new Function<Integer, String>() { + @Override + public String apply(Integer t) throws Exception { + String name = Thread.currentThread().getName(); + if (name.contains("RxSingleScheduler")) { + return "RxSingleScheduler"; + } + return name; + } + }) + .concatMapDelayError(new Function<String, Publisher<? extends Object>>() { + @Override + public Publisher<? extends Object> apply(String v) + throws Exception { + return Flowable.just(v); + } + }, true, 2, ImmediateThinScheduler.INSTANCE) + .observeOn(Schedulers.computation()) + .distinct() + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertResult("RxSingleScheduler"); + } + + @Test + public void pollThrows() { + Flowable.just(1) + .map(new Function<Integer, Integer>() { + @Override + public Integer apply(Integer v) throws Exception { + throw new TestException(); + } + }) + .compose(TestHelper.<Integer>flowableStripBoundary()) + .concatMap(new Function<Integer, Publisher<Integer>>() { + @Override + public Publisher<Integer> apply(Integer v) + throws Exception { + return Flowable.just(v); + } + }, 2, ImmediateThinScheduler.INSTANCE) + .test() + .assertFailure(TestException.class); + } + + @Test + public void pollThrowsDelayError() { + Flowable.just(1) + .map(new Function<Integer, Integer>() { + @Override + public Integer apply(Integer v) throws Exception { + throw new TestException(); + } + }) + .compose(TestHelper.<Integer>flowableStripBoundary()) + .concatMapDelayError(new Function<Integer, Publisher<Integer>>() { + @Override + public Publisher<Integer> apply(Integer v) + throws Exception { + return Flowable.just(v); + } + }, true, 2, ImmediateThinScheduler.INSTANCE) + .test() + .assertFailure(TestException.class); + } + + @Test + public void noCancelPrevious() { + final AtomicInteger counter = new AtomicInteger(); + + Flowable.range(1, 5) + .concatMap(new Function<Integer, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Integer v) throws Exception { + return Flowable.just(v).doOnCancel(new Action() { + @Override + public void run() throws Exception { + counter.getAndIncrement(); + } + }); + } + }, 2, ImmediateThinScheduler.INSTANCE) + .test() + .assertResult(1, 2, 3, 4, 5); + + assertEquals(0, counter.get()); + } + + @Test + public void delayErrorCallableTillTheEnd() { + Flowable.just(1, 2, 3, 101, 102, 23, 890, 120, 32) + .concatMapDelayError(new Function<Integer, Flowable<Integer>>() { + @Override public Flowable<Integer> apply(final Integer integer) throws Exception { + return Flowable.fromCallable(new Callable<Integer>() { + @Override public Integer call() throws Exception { + if (integer >= 100) { + throw new NullPointerException("test null exp"); + } + return integer; + } + }); + } + }, true, 2, ImmediateThinScheduler.INSTANCE) + .test() + .assertFailure(CompositeException.class, 1, 2, 3, 23, 32); + } + + @Test + public void delayErrorCallableEager() { + Flowable.just(1, 2, 3, 101, 102, 23, 890, 120, 32) + .concatMapDelayError(new Function<Integer, Flowable<Integer>>() { + @Override public Flowable<Integer> apply(final Integer integer) throws Exception { + return Flowable.fromCallable(new Callable<Integer>() { + @Override public Integer call() throws Exception { + if (integer >= 100) { + throw new NullPointerException("test null exp"); + } + return integer; + } + }); + } + }, false, 2, ImmediateThinScheduler.INSTANCE) + .test() + .assertFailure(NullPointerException.class, 1, 2, 3); + } + + @Test + public void mapperScheduled() { + TestSubscriber<String> ts = Flowable.just(1) + .concatMap(new Function<Integer, Flowable<String>>() { + @Override + public Flowable<String> apply(Integer t) throws Throwable { + return Flowable.just(Thread.currentThread().getName()); + } + }, 2, Schedulers.single()) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertValueCount(1) + .assertNoErrors() + .assertComplete(); + + assertTrue(ts.values().toString(), ts.values().get(0).startsWith("RxSingleScheduler-")); + } + + @Test + public void mapperScheduledHidden() { + TestSubscriber<String> ts = Flowable.just(1) + .concatMap(new Function<Integer, Flowable<String>>() { + @Override + public Flowable<String> apply(Integer t) throws Throwable { + return Flowable.just(Thread.currentThread().getName()).hide(); + } + }, 2, Schedulers.single()) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertValueCount(1) + .assertNoErrors() + .assertComplete(); + + assertTrue(ts.values().toString(), ts.values().get(0).startsWith("RxSingleScheduler-")); + } + + @Test + public void mapperDelayErrorScheduled() { + TestSubscriber<String> ts = Flowable.just(1) + .concatMapDelayError(new Function<Integer, Flowable<String>>() { + @Override + public Flowable<String> apply(Integer t) throws Throwable { + return Flowable.just(Thread.currentThread().getName()); + } + }, false, 2, Schedulers.single()) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertValueCount(1) + .assertNoErrors() + .assertComplete(); + + assertTrue(ts.values().toString(), ts.values().get(0).startsWith("RxSingleScheduler-")); + } + + @Test + public void mapperDelayErrorScheduledHidden() { + TestSubscriber<String> ts = Flowable.just(1) + .concatMapDelayError(new Function<Integer, Flowable<String>>() { + @Override + public Flowable<String> apply(Integer t) throws Throwable { + return Flowable.just(Thread.currentThread().getName()).hide(); + } + }, false, 2, Schedulers.single()) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertValueCount(1) + .assertNoErrors() + .assertComplete(); + + assertTrue(ts.values().toString(), ts.values().get(0).startsWith("RxSingleScheduler-")); + } + + @Test + public void mapperDelayError2Scheduled() { + TestSubscriber<String> ts = Flowable.just(1) + .concatMapDelayError(new Function<Integer, Flowable<String>>() { + @Override + public Flowable<String> apply(Integer t) throws Throwable { + return Flowable.just(Thread.currentThread().getName()); + } + }, true, 2, Schedulers.single()) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertValueCount(1) + .assertNoErrors() + .assertComplete(); + + assertTrue(ts.values().toString(), ts.values().get(0).startsWith("RxSingleScheduler-")); + } + + @Test + public void mapperDelayError2ScheduledHidden() { + TestSubscriber<String> ts = Flowable.just(1) + .concatMapDelayError(new Function<Integer, Flowable<String>>() { + @Override + public Flowable<String> apply(Integer t) throws Throwable { + return Flowable.just(Thread.currentThread().getName()).hide(); + } + }, true, 2, Schedulers.single()) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertValueCount(1) + .assertNoErrors() + .assertComplete(); + + assertTrue(ts.values().toString(), ts.values().get(0).startsWith("RxSingleScheduler-")); + } + + @Test + public void issue2890NoStackoverflow() throws InterruptedException, TimeoutException { + final ExecutorService executor = Executors.newFixedThreadPool(2); + final Scheduler sch = Schedulers.from(executor); + + Function<Integer, Flowable<Integer>> func = new Function<Integer, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Integer t) { + Flowable<Integer> flowable = Flowable.just(t) + .subscribeOn(sch) + ; + FlowableProcessor<Integer> processor = UnicastProcessor.create(); + flowable.subscribe(processor); + return processor; + } + }; + + int n = 5000; + final AtomicInteger counter = new AtomicInteger(); + + Flowable.range(1, n).concatMap(func, 2, ImmediateThinScheduler.INSTANCE).subscribe(new DefaultSubscriber<Integer>() { + @Override + public void onNext(Integer t) { + // Consume after sleep for 1 ms + try { + Thread.sleep(1); + } catch (InterruptedException e) { + // ignored + } + if (counter.getAndIncrement() % 100 == 0) { + System.out.print("testIssue2890NoStackoverflow -> "); + System.out.println(counter.get()); + }; + } + + @Override + public void onComplete() { + executor.shutdown(); + } + + @Override + public void onError(Throwable e) { + executor.shutdown(); + } + }); + + long awaitTerminationTimeoutMillis = 100_000; + if (!executor.awaitTermination(awaitTerminationTimeoutMillis, TimeUnit.MILLISECONDS)) { + throw new TimeoutException("Completed " + counter.get() + "/" + n + " before timed out after " + + awaitTerminationTimeoutMillis + " milliseconds."); + } + + assertEquals(n, counter.get()); + } + + @Test + public void concatMapRangeAsyncLoopIssue2876() { + final long durationSeconds = 2; + final long startTime = System.currentTimeMillis(); + for (int i = 0;; i++) { + //only run this for a max of ten seconds + if (System.currentTimeMillis() - startTime > TimeUnit.SECONDS.toMillis(durationSeconds)) { + return; + } + if (i % 1000 == 0) { + System.out.println("concatMapRangeAsyncLoop > " + i); + } + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(); + Flowable.range(0, 1000) + .concatMap(new Function<Integer, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Integer t) { + return Flowable.fromIterable(Arrays.asList(t)); + } + }, 2, ImmediateThinScheduler.INSTANCE) + .observeOn(Schedulers.computation()).subscribe(ts); + + ts.awaitDone(2500, TimeUnit.MILLISECONDS); + ts.assertTerminated(); + ts.assertNoErrors(); + assertEquals(1000, ts.values().size()); + assertEquals((Integer)999, ts.values().get(999)); + } + } + + @SuppressWarnings("unchecked") + @Test + public void concatArray() throws Exception { + for (int i = 2; i < 10; i++) { + Flowable<Integer>[] obs = new Flowable[i]; + Arrays.fill(obs, Flowable.just(1)); + + Integer[] expected = new Integer[i]; + Arrays.fill(expected, 1); + + Method m = Flowable.class.getMethod("concatArray", Publisher[].class); + + TestSubscriber<Integer> ts = TestSubscriber.create(); + + ((Flowable<Integer>)m.invoke(null, new Object[]{obs})).subscribe(ts); + + ts.assertValues(expected); + ts.assertNoErrors(); + ts.assertComplete(); + } + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + @Test + public void concatMapJustJust() { + TestSubscriber<Integer> ts = TestSubscriber.create(); + + Flowable.just(Flowable.just(1)).concatMap((Function)Functions.identity(), 2, ImmediateThinScheduler.INSTANCE).subscribe(ts); + + ts.assertValue(1); + ts.assertNoErrors(); + ts.assertComplete(); + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + @Test + public void concatMapJustRange() { + TestSubscriber<Integer> ts = TestSubscriber.create(); + + Flowable.just(Flowable.range(1, 5)).concatMap((Function)Functions.identity(), 2, ImmediateThinScheduler.INSTANCE).subscribe(ts); + + ts.assertValues(1, 2, 3, 4, 5); + ts.assertNoErrors(); + ts.assertComplete(); + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + @Test + public void concatMapDelayErrorJustJust() { + TestSubscriber<Integer> ts = TestSubscriber.create(); + + Flowable.just(Flowable.just(1)).concatMapDelayError((Function)Functions.identity(), true, 2, ImmediateThinScheduler.INSTANCE).subscribe(ts); + + ts.assertValue(1); + ts.assertNoErrors(); + ts.assertComplete(); + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + @Test + public void concatMapDelayErrorJustRange() { + TestSubscriber<Integer> ts = TestSubscriber.create(); + + Flowable.just(Flowable.range(1, 5)).concatMapDelayError((Function)Functions.identity(), true, 2, ImmediateThinScheduler.INSTANCE).subscribe(ts); + + ts.assertValues(1, 2, 3, 4, 5); + ts.assertNoErrors(); + ts.assertComplete(); + } + + @SuppressWarnings("unchecked") + @Test + public void startWithArray() throws Exception { + for (int i = 2; i < 10; i++) { + Object[] obs = new Object[i]; + Arrays.fill(obs, 1); + + Integer[] expected = new Integer[i]; + Arrays.fill(expected, 1); + + Method m = Flowable.class.getMethod("startWithArray", Object[].class); + + TestSubscriber<Integer> ts = TestSubscriber.create(); + + ((Flowable<Integer>)m.invoke(Flowable.empty(), new Object[]{obs})).subscribe(ts); + + ts.assertValues(expected); + ts.assertNoErrors(); + ts.assertComplete(); + } + } + + @Test + public void concatMapDelayError() { + Flowable.just(Flowable.just(1), Flowable.just(2)) + .concatMapDelayError(Functions.<Flowable<Integer>>identity(), true, 2, ImmediateThinScheduler.INSTANCE) + .test() + .assertResult(1, 2); + } + + @Test + public void concatMapDelayErrorJustSource() { + Flowable.just(0) + .concatMapDelayError(new Function<Object, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Object v) throws Exception { + return Flowable.just(1); + } + }, true, 16, ImmediateThinScheduler.INSTANCE) + .test() + .assertResult(1); + + } + + @Test + public void concatMapJustSource() { + Flowable.just(0).hide() + .concatMap(new Function<Object, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Object v) throws Exception { + return Flowable.just(1); + } + }, 16, ImmediateThinScheduler.INSTANCE) + .test() + .assertResult(1); + } + + @Test + public void concatMapJustSourceDelayError() { + Flowable.just(0).hide() + .concatMapDelayError(new Function<Object, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Object v) throws Exception { + return Flowable.just(1); + } + }, false, 16, ImmediateThinScheduler.INSTANCE) + .test() + .assertResult(1); + } + + @Test + public void concatMapScalarBackpressured() { + Flowable.just(1).hide() + .concatMap(Functions.justFunction(Flowable.just(2)), 2, ImmediateThinScheduler.INSTANCE) + .test(1L) + .assertResult(2); + } + + @Test + public void concatMapScalarBackpressuredDelayError() { + Flowable.just(1).hide() + .concatMapDelayError(Functions.justFunction(Flowable.just(2)), true, 2, ImmediateThinScheduler.INSTANCE) + .test(1L) + .assertResult(2); + } + + @Test + public void concatMapEmpty() { + Flowable.just(1).hide() + .concatMap(Functions.justFunction(Flowable.empty()), 2, ImmediateThinScheduler.INSTANCE) + .test() + .assertResult(); + } + + @Test + public void concatMapEmptyDelayError() { + Flowable.just(1).hide() + .concatMapDelayError(Functions.justFunction(Flowable.empty()), true, 2, ImmediateThinScheduler.INSTANCE) + .test() + .assertResult(); + } + + @Test + public void ignoreBackpressure() { + new Flowable<Integer>() { + @Override + protected void subscribeActual(Subscriber<? super Integer> s) { + s.onSubscribe(new BooleanSubscription()); + for (int i = 0; i < 10; i++) { + s.onNext(i); + } + } + } + .concatMap(Functions.justFunction(Flowable.just(2)), 8, ImmediateThinScheduler.INSTANCE) + .test(0L) + .assertFailure(QueueOverflowException.class); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeFlowable(new Function<Flowable<Object>, Publisher<Integer>>() { + @Override + public Publisher<Integer> apply(Flowable<Object> f) throws Exception { + return f.concatMap(Functions.justFunction(Flowable.just(2)), 2, ImmediateThinScheduler.INSTANCE); + } + }); + TestHelper.checkDoubleOnSubscribeFlowable(new Function<Flowable<Object>, Publisher<Integer>>() { + @Override + public Publisher<Integer> apply(Flowable<Object> f) throws Exception { + return f.concatMapDelayError(Functions.justFunction(Flowable.just(2)), true, 2, ImmediateThinScheduler.INSTANCE); + } + }); + } + + @Test + public void immediateInnerNextOuterError() { + final PublishProcessor<Integer> pp = PublishProcessor.create(); + + final TestSubscriberEx<Integer> ts = new TestSubscriberEx<Integer>() { + @Override + public void onNext(Integer t) { + super.onNext(t); + if (t == 1) { + pp.onError(new TestException("First")); + } + } + }; + + pp.concatMap(Functions.justFunction(Flowable.just(1)), 2, ImmediateThinScheduler.INSTANCE) + .subscribe(ts); + + pp.onNext(1); + + assertFalse(pp.hasSubscribers()); + + ts.assertFailureAndMessage(TestException.class, "First", 1); + } + + @Test + public void immediateInnerNextOuterError2() { + final PublishProcessor<Integer> pp = PublishProcessor.create(); + + final TestSubscriberEx<Integer> ts = new TestSubscriberEx<Integer>() { + @Override + public void onNext(Integer t) { + super.onNext(t); + if (t == 1) { + pp.onError(new TestException("First")); + } + } + }; + + pp.concatMap(Functions.justFunction(Flowable.just(1).hide()), 2, ImmediateThinScheduler.INSTANCE) + .subscribe(ts); + + pp.onNext(1); + + assertFalse(pp.hasSubscribers()); + + ts.assertFailureAndMessage(TestException.class, "First", 1); + } + + @Test + public void concatMapInnerError() { + Flowable.just(1).hide() + .concatMap(Functions.justFunction(Flowable.error(new TestException())), 2, ImmediateThinScheduler.INSTANCE) + .test() + .assertFailure(TestException.class); + } + + @Test + public void concatMapInnerErrorDelayError() { + Flowable.just(1).hide() + .concatMapDelayError(Functions.justFunction(Flowable.error(new TestException())), true, 2, ImmediateThinScheduler.INSTANCE) + .test() + .assertFailure(TestException.class); + } + + @Test + public void badSource() { + TestHelper.checkBadSourceFlowable(new Function<Flowable<Integer>, Object>() { + @Override + public Object apply(Flowable<Integer> f) throws Exception { + return f.concatMap(Functions.justFunction(Flowable.just(1).hide()), 2, ImmediateThinScheduler.INSTANCE); + } + }, true, 1, 1, 1); + } + + @Test + public void badInnerSource() { + @SuppressWarnings("rawtypes") + final Subscriber[] ts0 = { null }; + TestSubscriberEx<Integer> ts = Flowable.just(1).hide().concatMap(Functions.justFunction(new Flowable<Integer>() { + @Override + protected void subscribeActual(Subscriber<? super Integer> s) { + ts0[0] = s; + s.onSubscribe(new BooleanSubscription()); + s.onError(new TestException("First")); + } + }), 2, ImmediateThinScheduler.INSTANCE) + .to(TestHelper.<Integer>testConsumer()); + + ts.assertFailureAndMessage(TestException.class, "First"); + + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + ts0[0].onError(new TestException("Second")); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void badInnerSourceDelayError() { + @SuppressWarnings("rawtypes") + final Subscriber[] ts0 = { null }; + TestSubscriberEx<Integer> ts = Flowable.just(1).hide().concatMapDelayError(Functions.justFunction(new Flowable<Integer>() { + @Override + protected void subscribeActual(Subscriber<? super Integer> s) { + ts0[0] = s; + s.onSubscribe(new BooleanSubscription()); + s.onError(new TestException("First")); + } + }), true, 2, ImmediateThinScheduler.INSTANCE) + .to(TestHelper.<Integer>testConsumer()); + + ts.assertFailureAndMessage(TestException.class, "First"); + + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + ts0[0].onError(new TestException("Second")); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void badSourceDelayError() { + TestHelper.checkBadSourceFlowable(new Function<Flowable<Integer>, Object>() { + @Override + public Object apply(Flowable<Integer> f) throws Exception { + return f.concatMapDelayError(Functions.justFunction(Flowable.just(1).hide()), true, 2, ImmediateThinScheduler.INSTANCE); + } + }, true, 1, 1, 1); + } + + @Test + public void fusedCrash() { + Flowable.range(1, 2) + .map(new Function<Integer, Object>() { + @Override + public Object apply(Integer v) throws Exception { throw new TestException(); } + }) + .concatMap(Functions.justFunction(Flowable.just(1)), 2, ImmediateThinScheduler.INSTANCE) + .test() + .assertFailure(TestException.class); + } + + @Test + public void fusedCrashDelayError() { + Flowable.range(1, 2) + .map(new Function<Integer, Object>() { + @Override + public Object apply(Integer v) throws Exception { throw new TestException(); } + }) + .concatMapDelayError(Functions.justFunction(Flowable.just(1)), true, 2, ImmediateThinScheduler.INSTANCE) + .test() + .assertFailure(TestException.class); + } + + @Test + public void callableCrash() { + Flowable.just(1).hide() + .concatMap(Functions.justFunction(Flowable.fromCallable(new Callable<Object>() { + @Override + public Object call() throws Exception { + throw new TestException(); + } + })), 2, ImmediateThinScheduler.INSTANCE) + .test() + .assertFailure(TestException.class); + } + + @Test + public void callableCrashDelayError() { + Flowable.just(1).hide() + .concatMapDelayError(Functions.justFunction(Flowable.fromCallable(new Callable<Object>() { + @Override + public Object call() throws Exception { + throw new TestException(); + } + })), true, 2, ImmediateThinScheduler.INSTANCE) + .test() + .assertFailure(TestException.class); + } + + @Test + public void dispose() { + TestHelper.checkDisposed(Flowable.range(1, 2) + .concatMap(Functions.justFunction(Flowable.just(1)), 2, ImmediateThinScheduler.INSTANCE)); + + TestHelper.checkDisposed(Flowable.range(1, 2) + .concatMapDelayError(Functions.justFunction(Flowable.just(1)), true, 2, ImmediateThinScheduler.INSTANCE)); + } + + @Test + public void notVeryEnd() { + Flowable.range(1, 2) + .concatMapDelayError(Functions.justFunction(Flowable.error(new TestException())), false, 16, ImmediateThinScheduler.INSTANCE) + .test() + .assertFailure(TestException.class); + } + + @Test + public void error() { + Flowable.error(new TestException()) + .concatMapDelayError(Functions.justFunction(Flowable.just(2)), false, 16, ImmediateThinScheduler.INSTANCE) + .test() + .assertFailure(TestException.class); + } + + @Test + public void mapperThrows() { + Flowable.range(1, 2) + .concatMap(new Function<Integer, Publisher<Object>>() { + @Override + public Publisher<Object> apply(Integer v) throws Exception { + throw new TestException(); + } + }, 2, ImmediateThinScheduler.INSTANCE) + .test() + .assertFailure(TestException.class); + } + + @Test + public void mainErrors() { + PublishProcessor<Integer> source = PublishProcessor.create(); + + TestSubscriber<Integer> ts = TestSubscriber.create(); + + source.concatMapDelayError(new Function<Integer, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Integer v) { + return Flowable.range(v, 2); + } + }, true, 2, ImmediateThinScheduler.INSTANCE).subscribe(ts); + + source.onNext(1); + source.onNext(2); + source.onError(new TestException()); + + ts.assertValues(1, 2, 2, 3); + ts.assertError(TestException.class); + ts.assertNotComplete(); + } + + @Test + public void innerErrors() { + final Flowable<Integer> inner = Flowable.range(1, 2) + .concatWith(Flowable.<Integer>error(new TestException())); + + TestSubscriber<Integer> ts = TestSubscriber.create(); + + Flowable.range(1, 3).concatMapDelayError(new Function<Integer, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Integer v) { + return inner; + } + }, true, 2, ImmediateThinScheduler.INSTANCE).subscribe(ts); + + ts.assertValues(1, 2, 1, 2, 1, 2); + ts.assertError(CompositeException.class); + ts.assertNotComplete(); + } + + @Test + public void singleInnerErrors() { + final Flowable<Integer> inner = Flowable.range(1, 2).concatWith(Flowable.<Integer>error(new TestException())); + + TestSubscriber<Integer> ts = TestSubscriber.create(); + + Flowable.just(1) + .hide() // prevent scalar optimization + .concatMapDelayError(new Function<Integer, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Integer v) { + return inner; + } + }, true, 2, ImmediateThinScheduler.INSTANCE).subscribe(ts); + + ts.assertValues(1, 2); + ts.assertError(TestException.class); + ts.assertNotComplete(); + } + + @Test + public void innerNull() { + TestSubscriber<Integer> ts = TestSubscriber.create(); + + Flowable.just(1) + .hide() // prevent scalar optimization + .concatMapDelayError(new Function<Integer, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Integer v) { + return null; + } + }, true, 2, ImmediateThinScheduler.INSTANCE).subscribe(ts); + + ts.assertNoValues(); + ts.assertError(NullPointerException.class); + ts.assertNotComplete(); + } + + @Test + public void innerThrows() { + TestSubscriber<Integer> ts = TestSubscriber.create(); + + Flowable.just(1) + .hide() // prevent scalar optimization + .concatMapDelayError(new Function<Integer, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Integer v) { + throw new TestException(); + } + }, true, 2, ImmediateThinScheduler.INSTANCE).subscribe(ts); + + ts.assertNoValues(); + ts.assertError(TestException.class); + ts.assertNotComplete(); + } + + @Test + public void innerWithEmpty() { + TestSubscriber<Integer> ts = TestSubscriber.create(); + + Flowable.range(1, 3) + .concatMapDelayError(new Function<Integer, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Integer v) { + return v == 2 ? Flowable.<Integer>empty() : Flowable.range(1, 2); + } + }, true, 2, ImmediateThinScheduler.INSTANCE).subscribe(ts); + + ts.assertValues(1, 2, 1, 2); + ts.assertNoErrors(); + ts.assertComplete(); + } + + @Test + public void innerWithScalar() { + TestSubscriber<Integer> ts = TestSubscriber.create(); + + Flowable.range(1, 3) + .concatMapDelayError(new Function<Integer, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Integer v) { + return v == 2 ? Flowable.just(3) : Flowable.range(1, 2); + } + }, true, 2, ImmediateThinScheduler.INSTANCE).subscribe(ts); + + ts.assertValues(1, 2, 3, 1, 2); + ts.assertNoErrors(); + ts.assertComplete(); + } + + @Test + public void backpressure() { + TestSubscriber<Integer> ts = TestSubscriber.create(0); + + Flowable.range(1, 3).concatMapDelayError(new Function<Integer, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Integer v) { + return Flowable.range(v, 2); + } + }, true, 2, ImmediateThinScheduler.INSTANCE).subscribe(ts); + + ts.assertNoValues(); + ts.assertNoErrors(); + ts.assertNotComplete(); + + ts.request(1); + ts.assertValues(1); + ts.assertNoErrors(); + ts.assertNotComplete(); + + ts.request(3); + ts.assertValues(1, 2, 2, 3); + ts.assertNoErrors(); + ts.assertNotComplete(); + + ts.request(2); + + ts.assertValues(1, 2, 2, 3, 3, 4); + ts.assertNoErrors(); + ts.assertComplete(); + } + + @Test + public void mapperScheduledLong() { + TestSubscriber<String> ts = Flowable.range(1, 1000) + .hide() + .observeOn(Schedulers.computation()) + .concatMap(new Function<Integer, Flowable<String>>() { + @Override + public Flowable<String> apply(Integer t) throws Throwable { + return Flowable.just(Thread.currentThread().getName()) + .repeat(1000) + .observeOn(Schedulers.io()); + } + }, 2, Schedulers.single()) + .distinct() + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertValueCount(1) + .assertNoErrors() + .assertComplete(); + + assertTrue(ts.values().toString(), ts.values().get(0).startsWith("RxSingleScheduler-")); + } + + @Test + public void mapperDelayErrorScheduledLong() { + TestSubscriber<String> ts = Flowable.range(1, 1000) + .hide() + .observeOn(Schedulers.computation()) + .concatMapDelayError(new Function<Integer, Flowable<String>>() { + @Override + public Flowable<String> apply(Integer t) throws Throwable { + return Flowable.just(Thread.currentThread().getName()) + .repeat(1000) + .observeOn(Schedulers.io()); + } + }, false, 2, Schedulers.single()) + .distinct() + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertValueCount(1) + .assertNoErrors() + .assertComplete(); + + assertTrue(ts.values().toString(), ts.values().get(0).startsWith("RxSingleScheduler-")); + } + + @Test + public void mapperDelayError2ScheduledLong() { + TestSubscriber<String> ts = Flowable.range(1, 1000) + .hide() + .observeOn(Schedulers.computation()) + .concatMapDelayError(new Function<Integer, Flowable<String>>() { + @Override + public Flowable<String> apply(Integer t) throws Throwable { + return Flowable.just(Thread.currentThread().getName()) + .repeat(1000) + .observeOn(Schedulers.io()); + } + }, true, 2, Schedulers.single()) + .distinct() + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertValueCount(1) + .assertNoErrors() + .assertComplete(); + + assertTrue(ts.values().toString(), ts.values().get(0).startsWith("RxSingleScheduler-")); + } + + @Test + public void undeliverableUponCancel() { + TestHelper.checkUndeliverableUponCancel(new FlowableConverter<Integer, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Flowable<Integer> upstream) { + return upstream.concatMap(new Function<Integer, Publisher<Integer>>() { + @Override + public Publisher<Integer> apply(Integer v) throws Throwable { + return Flowable.just(v).hide(); + } + }, 2, ImmediateThinScheduler.INSTANCE); + } + }); + } + + @Test + public void undeliverableUponCancelDelayError() { + TestHelper.checkUndeliverableUponCancel(new FlowableConverter<Integer, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Flowable<Integer> upstream) { + return upstream.concatMapDelayError(new Function<Integer, Publisher<Integer>>() { + @Override + public Publisher<Integer> apply(Integer v) throws Throwable { + return Flowable.just(v).hide(); + } + }, false, 2, ImmediateThinScheduler.INSTANCE); + } + }); + } + + @Test + public void undeliverableUponCancelDelayErrorTillEnd() { + TestHelper.checkUndeliverableUponCancel(new FlowableConverter<Integer, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Flowable<Integer> upstream) { + return upstream.concatMapDelayError(new Function<Integer, Publisher<Integer>>() { + @Override + public Publisher<Integer> apply(Integer v) throws Throwable { + return Flowable.just(v).hide(); + } + }, true, 2, ImmediateThinScheduler.INSTANCE); + } + }); + } + + @Test + public void fusionRejected() { + TestSubscriberEx<Object> ts = new TestSubscriberEx<>(); + + TestHelper.rejectFlowableFusion() + .concatMap(v -> Flowable.never(), 2, ImmediateThinScheduler.INSTANCE) + .subscribe(ts); + } + + @Test + public void fusionRejectedDelayErrorr() { + TestSubscriberEx<Object> ts = new TestSubscriberEx<>(); + + TestHelper.rejectFlowableFusion() + .concatMapDelayError(v -> Flowable.never(), true, 2, ImmediateThinScheduler.INSTANCE) + .subscribe(ts); + } + + @Test + public void scalarInnerJustDispose() { + TestSubscriber<Integer> ts = new TestSubscriber<>(); + + Flowable.just(1) + .hide() + .concatMap(v -> Flowable.fromCallable(() -> { + ts.cancel(); + return 1; + }), 2, ImmediateThinScheduler.INSTANCE) + .subscribe(ts); + + ts.assertEmpty(); + } + + @Test + public void scalarInnerJustDisposeDelayError() { + TestSubscriber<Integer> ts = new TestSubscriber<>(); + + Flowable.just(1) + .hide() + .concatMapDelayError(v -> Flowable.fromCallable(() -> { + ts.cancel(); + return 1; + }), true, 2, ImmediateThinScheduler.INSTANCE) + .subscribe(ts); + + ts.assertEmpty(); + } + + static final class EmptyDisposingFlowable extends Flowable<Object> + implements Supplier<Object> { + final TestSubscriber<Object> ts; + EmptyDisposingFlowable(TestSubscriber<Object> ts) { + this.ts = ts; + } + + @Override + protected void subscribeActual(@NonNull Subscriber<? super @NonNull Object> subscriber) { + EmptySubscription.complete(subscriber); + } + + @Override + public @NonNull Object get() throws Throwable { + ts.cancel(); + return null; + } + } + + @Test + public void scalarInnerEmptyDisposeDelayError() { + TestSubscriber<Object> ts = new TestSubscriber<>(); + + Flowable.just(1) + .hide() + .concatMapDelayError(v -> new EmptyDisposingFlowable(ts), + true, 2, ImmediateThinScheduler.INSTANCE + ) + .subscribe(ts); + + ts.assertEmpty(); + } + + @Test + public void mainErrorInnerNextIgnoreCancel() { + AtomicReference<Subscriber<? super Integer>> ref = new AtomicReference<>(); + + Flowable.just(1).concatWith(Flowable.<Integer>error(new TestException())) + .concatMap(v -> Flowable.<Integer>fromPublisher(ref::set), 2, ImmediateThinScheduler.INSTANCE) + .doOnError(e -> { + ref.get().onSubscribe(new BooleanSubscription()); + ref.get().onNext(1); + }) + .test() + .assertFailure(TestException.class); + } + + @Test + public void scalarSupplierMainError() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + + TestSubscriber<Integer> ts = pp.concatMap(v -> Flowable.fromCallable(() -> { + pp.onError(new TestException()); + return 2; + }), 2, ImmediateThinScheduler.INSTANCE) + .test() + ; + + pp.onNext(1); + + ts.assertFailure(TestException.class); + } + + @Test + public void mainErrorInnerErrorRace() throws Throwable { + TestHelper.withErrorTracking(errors -> { + TestException ex1 = new TestException(); + TestException ex2 = new TestException(); + + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + AtomicReference<Subscriber<? super Integer>> ref1 = new AtomicReference<>(); + AtomicReference<Subscriber<? super Integer>> ref2 = new AtomicReference<>(); + + TestSubscriber<Integer> ts = Flowable.<Integer>fromPublisher(ref1::set) + .concatMap(v -> Flowable.<Integer>fromPublisher(ref2::set), 2, ImmediateThinScheduler.INSTANCE) + .test(); + + ref1.get().onSubscribe(new BooleanSubscription()); + ref1.get().onNext(1); + ref2.get().onSubscribe(new BooleanSubscription()); + + TestHelper.race(() -> ref1.get().onError(ex1), () -> ref2.get().onError(ex2)); + + ts.assertError(RuntimeException.class); + errors.clear(); + } + }); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableConcatMapTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableConcatMapTest.java new file mode 100644 index 0000000000..10df043ba5 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableConcatMapTest.java @@ -0,0 +1,325 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import static org.junit.Assert.assertEquals; + +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.Test; +import org.reactivestreams.Publisher; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.operators.flowable.FlowableConcatMap.SimpleScalarSubscription; +import io.reactivex.rxjava3.processors.*; +import io.reactivex.rxjava3.schedulers.Schedulers; +import io.reactivex.rxjava3.subscribers.TestSubscriber; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class FlowableConcatMapTest extends RxJavaTest { + + @Test + public void simpleSubscriptionRequest() { + TestSubscriber<Integer> ts = new TestSubscriber<>(0); + SimpleScalarSubscription<Integer> ws = new SimpleScalarSubscription<>(1, ts); + ts.onSubscribe(ws); + + ws.request(0); + + ts.assertEmpty(); + + ws.request(1); + + ts.assertResult(1); + + ws.request(1); + + ts.assertResult(1); + } + + @Test + public void boundaryFusion() { + Flowable.range(1, 10000) + .observeOn(Schedulers.single()) + .map(new Function<Integer, String>() { + @Override + public String apply(Integer t) throws Exception { + String name = Thread.currentThread().getName(); + if (name.contains("RxSingleScheduler")) { + return "RxSingleScheduler"; + } + return name; + } + }) + .concatMap(new Function<String, Publisher<? extends Object>>() { + @Override + public Publisher<? extends Object> apply(String v) + throws Exception { + return Flowable.just(v); + } + }) + .observeOn(Schedulers.computation()) + .distinct() + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertResult("RxSingleScheduler"); + } + + @Test + public void innerScalarRequestRace() { + Flowable<Integer> just = Flowable.just(1); + int n = 1000; + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + PublishProcessor<Flowable<Integer>> source = PublishProcessor.create(); + + TestSubscriber<Integer> ts = source + .concatMap(v -> v, n + 1) + .test(1L); + + TestHelper.race(() -> { + for (int j = 0; j < n; j++) { + source.onNext(just); + } + }, () -> { + for (int j = 0; j < n; j++) { + ts.request(1); + } + }); + + ts.assertValueCount(n); + } + } + + @Test + public void innerScalarRequestRaceDelayError() { + Flowable<Integer> just = Flowable.just(1); + int n = 1000; + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + PublishProcessor<Flowable<Integer>> source = PublishProcessor.create(); + + TestSubscriber<Integer> ts = source + .concatMapDelayError(v -> v, true, n + 1) + .test(1L); + + TestHelper.race(() -> { + for (int j = 0; j < n; j++) { + source.onNext(just); + } + }, () -> { + for (int j = 0; j < n; j++) { + ts.request(1); + } + }); + + ts.assertValueCount(n); + } + } + + @Test + public void boundaryFusionDelayError() { + Flowable.range(1, 10000) + .observeOn(Schedulers.single()) + .map(new Function<Integer, String>() { + @Override + public String apply(Integer t) throws Exception { + String name = Thread.currentThread().getName(); + if (name.contains("RxSingleScheduler")) { + return "RxSingleScheduler"; + } + return name; + } + }) + .concatMapDelayError(new Function<String, Publisher<? extends Object>>() { + @Override + public Publisher<? extends Object> apply(String v) + throws Exception { + return Flowable.just(v); + } + }) + .observeOn(Schedulers.computation()) + .distinct() + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertResult("RxSingleScheduler"); + } + + @Test + public void pollThrows() { + Flowable.just(1) + .map(new Function<Integer, Integer>() { + @Override + public Integer apply(Integer v) throws Exception { + throw new TestException(); + } + }) + .compose(TestHelper.<Integer>flowableStripBoundary()) + .concatMap(new Function<Integer, Publisher<Integer>>() { + @Override + public Publisher<Integer> apply(Integer v) + throws Exception { + return Flowable.just(v); + } + }) + .test() + .assertFailure(TestException.class); + } + + @Test + public void pollThrowsDelayError() { + Flowable.just(1) + .map(new Function<Integer, Integer>() { + @Override + public Integer apply(Integer v) throws Exception { + throw new TestException(); + } + }) + .compose(TestHelper.<Integer>flowableStripBoundary()) + .concatMapDelayError(new Function<Integer, Publisher<Integer>>() { + @Override + public Publisher<Integer> apply(Integer v) + throws Exception { + return Flowable.just(v); + } + }) + .test() + .assertFailure(TestException.class); + } + + @Test + public void noCancelPrevious() { + final AtomicInteger counter = new AtomicInteger(); + + Flowable.range(1, 5) + .concatMap(new Function<Integer, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Integer v) throws Exception { + return Flowable.just(v).doOnCancel(new Action() { + @Override + public void run() throws Exception { + counter.getAndIncrement(); + } + }); + } + }) + .test() + .assertResult(1, 2, 3, 4, 5); + + assertEquals(0, counter.get()); + } + + @Test + public void delayErrorCallableTillTheEnd() { + Flowable.just(1, 2, 3, 101, 102, 23, 890, 120, 32) + .concatMapDelayError(new Function<Integer, Flowable<Integer>>() { + @Override public Flowable<Integer> apply(final Integer integer) throws Exception { + return Flowable.fromCallable(new Callable<Integer>() { + @Override public Integer call() throws Exception { + if (integer >= 100) { + throw new NullPointerException("test null exp"); + } + return integer; + } + }); + } + }) + .test() + .assertFailure(CompositeException.class, 1, 2, 3, 23, 32); + } + + @Test + public void delayErrorCallableEager() { + Flowable.just(1, 2, 3, 101, 102, 23, 890, 120, 32) + .concatMapDelayError(new Function<Integer, Flowable<Integer>>() { + @Override public Flowable<Integer> apply(final Integer integer) throws Exception { + return Flowable.fromCallable(new Callable<Integer>() { + @Override public Integer call() throws Exception { + if (integer >= 100) { + throw new NullPointerException("test null exp"); + } + return integer; + } + }); + } + }, false, 2) + .test() + .assertFailure(NullPointerException.class, 1, 2, 3); + } + + @Test + public void undeliverableUponCancel() { + TestHelper.checkUndeliverableUponCancel(new FlowableConverter<Integer, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Flowable<Integer> upstream) { + return upstream.concatMap(new Function<Integer, Publisher<Integer>>() { + @Override + public Publisher<Integer> apply(Integer v) throws Throwable { + return Flowable.just(v).hide(); + } + }); + } + }); + } + + @Test + public void undeliverableUponCancelDelayError() { + TestHelper.checkUndeliverableUponCancel(new FlowableConverter<Integer, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Flowable<Integer> upstream) { + return upstream.concatMapDelayError(new Function<Integer, Publisher<Integer>>() { + @Override + public Publisher<Integer> apply(Integer v) throws Throwable { + return Flowable.just(v).hide(); + } + }, false, 2); + } + }); + } + + @Test + public void undeliverableUponCancelDelayErrorTillEnd() { + TestHelper.checkUndeliverableUponCancel(new FlowableConverter<Integer, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Flowable<Integer> upstream) { + return upstream.concatMapDelayError(new Function<Integer, Publisher<Integer>>() { + @Override + public Publisher<Integer> apply(Integer v) throws Throwable { + return Flowable.just(v).hide(); + } + }, true, 2); + } + }); + } + + @Test + public void asyncFusedSource() { + UnicastProcessor<Integer> up = UnicastProcessor.create(); + up.onNext(1); + up.onComplete(); + + up.concatMap(v -> Flowable.just(1).hide()) + .test() + .assertResult(1); + } + + @Test + public void scalarCallableSource() { + Flowable.fromCallable(() -> 1) + .concatMap(v -> Flowable.just(1)) + .test() + .assertResult(1); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableConcatTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableConcatTest.java new file mode 100644 index 0000000000..5a0634ffb6 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableConcatTest.java @@ -0,0 +1,1676 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +import java.io.IOException; +import java.lang.reflect.Method; +import java.util.*; +import java.util.concurrent.*; +import java.util.concurrent.atomic.*; + +import org.junit.Test; +import org.mockito.InOrder; +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.internal.subscriptions.BooleanSubscription; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.processors.*; +import io.reactivex.rxjava3.schedulers.*; +import io.reactivex.rxjava3.subscribers.*; +import io.reactivex.rxjava3.testsupport.*; + +public class FlowableConcatTest { + + @Test + public void concat() { + Subscriber<String> subscriber = TestHelper.mockSubscriber(); + + final String[] o = { "1", "3", "5", "7" }; + final String[] e = { "2", "4", "6" }; + + final Flowable<String> odds = Flowable.fromArray(o); + final Flowable<String> even = Flowable.fromArray(e); + + Flowable<String> concat = Flowable.concat(odds, even); + concat.subscribe(subscriber); + + verify(subscriber, times(7)).onNext(anyString()); + } + + @Test + public void concatWithList() { + Subscriber<String> subscriber = TestHelper.mockSubscriber(); + + final String[] o = { "1", "3", "5", "7" }; + final String[] e = { "2", "4", "6" }; + + final Flowable<String> odds = Flowable.fromArray(o); + final Flowable<String> even = Flowable.fromArray(e); + final List<Flowable<String>> list = new ArrayList<>(); + list.add(odds); + list.add(even); + Flowable<String> concat = Flowable.concat(Flowable.fromIterable(list)); + concat.subscribe(subscriber); + + verify(subscriber, times(7)).onNext(anyString()); + } + + @Test + public void concatObservableOfObservables() { + Subscriber<String> subscriber = TestHelper.mockSubscriber(); + + final String[] o = { "1", "3", "5", "7" }; + final String[] e = { "2", "4", "6" }; + + final Flowable<String> odds = Flowable.fromArray(o); + final Flowable<String> even = Flowable.fromArray(e); + + Flowable<Flowable<String>> flowableOfFlowables = Flowable.unsafeCreate(new Publisher<Flowable<String>>() { + + @Override + public void subscribe(Subscriber<? super Flowable<String>> subscriber) { + subscriber.onSubscribe(new BooleanSubscription()); + // simulate what would happen in an observable + subscriber.onNext(odds); + subscriber.onNext(even); + subscriber.onComplete(); + } + + }); + Flowable<String> concat = Flowable.concat(flowableOfFlowables); + + concat.subscribe(subscriber); + + verify(subscriber, times(7)).onNext(anyString()); + } + + /** + * Simple concat of 2 asynchronous observables ensuring it emits in correct order. + */ + @Test + public void simpleAsyncConcat() { + Subscriber<String> subscriber = TestHelper.mockSubscriber(); + + TestObservable<String> o1 = new TestObservable<>("one", "two", "three"); + TestObservable<String> o2 = new TestObservable<>("four", "five", "six"); + + Flowable.concat(Flowable.unsafeCreate(o1), Flowable.unsafeCreate(o2)).subscribe(subscriber); + + try { + // wait for async observables to complete + o1.t.join(); + o2.t.join(); + } catch (Throwable e) { + throw new RuntimeException("failed waiting on threads"); + } + + InOrder inOrder = inOrder(subscriber); + inOrder.verify(subscriber, times(1)).onNext("one"); + inOrder.verify(subscriber, times(1)).onNext("two"); + inOrder.verify(subscriber, times(1)).onNext("three"); + inOrder.verify(subscriber, times(1)).onNext("four"); + inOrder.verify(subscriber, times(1)).onNext("five"); + inOrder.verify(subscriber, times(1)).onNext("six"); + } + + @Test + public void nestedAsyncConcatLoop() throws Throwable { + for (int i = 0; i < 500; i++) { + if (i % 10 == 0) { + System.out.println("testNestedAsyncConcat >> " + i); + } + nestedAsyncConcat(); + } + } + + /** + * Test an async Flowable that emits more async Observables. + * @throws InterruptedException if the test is interrupted + */ + @Test + public void nestedAsyncConcat() throws InterruptedException { + Subscriber<String> subscriber = TestHelper.mockSubscriber(); + + final TestObservable<String> o1 = new TestObservable<>("one", "two", "three"); + final TestObservable<String> o2 = new TestObservable<>("four", "five", "six"); + final TestObservable<String> o3 = new TestObservable<>("seven", "eight", "nine"); + final CountDownLatch allowThird = new CountDownLatch(1); + + final AtomicReference<Thread> parent = new AtomicReference<>(); + final CountDownLatch parentHasStarted = new CountDownLatch(1); + final CountDownLatch parentHasFinished = new CountDownLatch(1); + + Flowable<Flowable<String>> observableOfObservables = Flowable.unsafeCreate(new Publisher<Flowable<String>>() { + + @Override + public void subscribe(final Subscriber<? super Flowable<String>> subscriber) { + final Disposable d = Disposable.empty(); + subscriber.onSubscribe(new Subscription() { + @Override + public void request(long n) { + + } + + @Override + public void cancel() { + d.dispose(); + } + }); + parent.set(new Thread(new Runnable() { + + @Override + public void run() { + try { + // emit first + if (!d.isDisposed()) { + System.out.println("Emit o1"); + subscriber.onNext(Flowable.unsafeCreate(o1)); + } + // emit second + if (!d.isDisposed()) { + System.out.println("Emit o2"); + subscriber.onNext(Flowable.unsafeCreate(o2)); + } + + // wait until sometime later and emit third + try { + allowThird.await(); + } catch (InterruptedException e) { + subscriber.onError(e); + } + if (!d.isDisposed()) { + System.out.println("Emit o3"); + subscriber.onNext(Flowable.unsafeCreate(o3)); + } + + } catch (Throwable e) { + subscriber.onError(e); + } finally { + System.out.println("Done parent Flowable"); + subscriber.onComplete(); + parentHasFinished.countDown(); + } + } + })); + parent.get().start(); + parentHasStarted.countDown(); + } + }); + + Flowable.concat(observableOfObservables).subscribe(subscriber); + + // wait for parent to start + parentHasStarted.await(); + + try { + // wait for first 2 async observables to complete + System.out.println("Thread1 is starting ... waiting for it to complete ..."); + o1.waitForThreadDone(); + System.out.println("Thread2 is starting ... waiting for it to complete ..."); + o2.waitForThreadDone(); + } catch (Throwable e) { + throw new RuntimeException("failed waiting on threads", e); + } + + InOrder inOrder = inOrder(subscriber); + inOrder.verify(subscriber, times(1)).onNext("one"); + inOrder.verify(subscriber, times(1)).onNext("two"); + inOrder.verify(subscriber, times(1)).onNext("three"); + inOrder.verify(subscriber, times(1)).onNext("four"); + inOrder.verify(subscriber, times(1)).onNext("five"); + inOrder.verify(subscriber, times(1)).onNext("six"); + // we shouldn't have the following 3 yet + inOrder.verify(subscriber, never()).onNext("seven"); + inOrder.verify(subscriber, never()).onNext("eight"); + inOrder.verify(subscriber, never()).onNext("nine"); + // we should not be completed yet + verify(subscriber, never()).onComplete(); + verify(subscriber, never()).onError(any(Throwable.class)); + + // now allow the third + allowThird.countDown(); + + try { + // wait for 3rd to complete + o3.waitForThreadDone(); + } catch (Throwable e) { + throw new RuntimeException("failed waiting on threads", e); + } + + try { + // wait for the parent to complete + if (!parentHasFinished.await(5, TimeUnit.SECONDS)) { + fail("Parent didn't finish within the time limit"); + } + } catch (Throwable e) { + throw new RuntimeException("failed waiting on threads", e); + } + + inOrder.verify(subscriber, times(1)).onNext("seven"); + inOrder.verify(subscriber, times(1)).onNext("eight"); + inOrder.verify(subscriber, times(1)).onNext("nine"); + + verify(subscriber, never()).onError(any(Throwable.class)); + inOrder.verify(subscriber, times(1)).onComplete(); + } + + @Test + public void blockedObservableOfObservables() { + Subscriber<String> subscriber = TestHelper.mockSubscriber(); + + final String[] o = { "1", "3", "5", "7" }; + final String[] e = { "2", "4", "6" }; + final Flowable<String> odds = Flowable.fromArray(o); + final Flowable<String> even = Flowable.fromArray(e); + final CountDownLatch callOnce = new CountDownLatch(1); + final CountDownLatch okToContinue = new CountDownLatch(1); + + TestObservable<Flowable<String>> observableOfObservables = new TestObservable<>(callOnce, okToContinue, odds, even); + Flowable<String> concatF = Flowable.concat(Flowable.unsafeCreate(observableOfObservables)); + concatF.subscribe(subscriber); + try { + //Block main thread to allow observables to serve up o1. + callOnce.await(); + } catch (Throwable ex) { + ex.printStackTrace(); + fail(ex.getMessage()); + } + // The concated observable should have served up all of the odds. + verify(subscriber, times(1)).onNext("1"); + verify(subscriber, times(1)).onNext("3"); + verify(subscriber, times(1)).onNext("5"); + verify(subscriber, times(1)).onNext("7"); + + try { + // unblock observables so it can serve up o2 and complete + okToContinue.countDown(); + observableOfObservables.t.join(); + } catch (Throwable ex) { + ex.printStackTrace(); + fail(ex.getMessage()); + } + // The concatenated observable should now have served up all the evens. + verify(subscriber, times(1)).onNext("2"); + verify(subscriber, times(1)).onNext("4"); + verify(subscriber, times(1)).onNext("6"); + } + + @Test + public void concatConcurrentWithInfinity() { + final TestObservable<String> w1 = new TestObservable<>("one", "two", "three"); + //This observable will send "hello" MAX_VALUE time. + final TestObservable<String> w2 = new TestObservable<>("hello", Integer.MAX_VALUE); + + Subscriber<String> subscriber = TestHelper.mockSubscriber(); + + TestObservable<Flowable<String>> observableOfObservables = new TestObservable<>(Flowable.unsafeCreate(w1), Flowable.unsafeCreate(w2)); + Flowable<String> concatF = Flowable.concat(Flowable.unsafeCreate(observableOfObservables)); + + concatF.take(50).subscribe(subscriber); + + //Wait for the thread to start up. + try { + w1.waitForThreadDone(); + w2.waitForThreadDone(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + + InOrder inOrder = inOrder(subscriber); + inOrder.verify(subscriber, times(1)).onNext("one"); + inOrder.verify(subscriber, times(1)).onNext("two"); + inOrder.verify(subscriber, times(1)).onNext("three"); + inOrder.verify(subscriber, times(47)).onNext("hello"); + verify(subscriber, times(1)).onComplete(); + verify(subscriber, never()).onError(any(Throwable.class)); + } + + @Test + public void concatNonBlockingObservables() { + + final CountDownLatch okToContinueW1 = new CountDownLatch(1); + final CountDownLatch okToContinueW2 = new CountDownLatch(1); + + final TestObservable<String> w1 = new TestObservable<>(null, okToContinueW1, "one", "two", "three"); + final TestObservable<String> w2 = new TestObservable<>(null, okToContinueW2, "four", "five", "six"); + + Subscriber<String> subscriber = TestHelper.mockSubscriber(); + + Flowable<Flowable<String>> observableOfObservables = Flowable.unsafeCreate(new Publisher<Flowable<String>>() { + + @Override + public void subscribe(Subscriber<? super Flowable<String>> subscriber) { + subscriber.onSubscribe(new BooleanSubscription()); + // simulate what would happen in an observable + subscriber.onNext(Flowable.unsafeCreate(w1)); + subscriber.onNext(Flowable.unsafeCreate(w2)); + subscriber.onComplete(); + } + + }); + Flowable<String> concat = Flowable.concat(observableOfObservables); + concat.subscribe(subscriber); + + verify(subscriber, times(0)).onComplete(); + + try { + // release both threads + okToContinueW1.countDown(); + okToContinueW2.countDown(); + // wait for both to finish + w1.t.join(); + w2.t.join(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + + InOrder inOrder = inOrder(subscriber); + inOrder.verify(subscriber, times(1)).onNext("one"); + inOrder.verify(subscriber, times(1)).onNext("two"); + inOrder.verify(subscriber, times(1)).onNext("three"); + inOrder.verify(subscriber, times(1)).onNext("four"); + inOrder.verify(subscriber, times(1)).onNext("five"); + inOrder.verify(subscriber, times(1)).onNext("six"); + verify(subscriber, times(1)).onComplete(); + + } + + /** + * Test unsubscribing the concatenated Flowable in a single thread. + */ + @Test + public void concatUnsubscribe() { + final CountDownLatch callOnce = new CountDownLatch(1); + final CountDownLatch okToContinue = new CountDownLatch(1); + final TestObservable<String> w1 = new TestObservable<>("one", "two", "three"); + final TestObservable<String> w2 = new TestObservable<>(callOnce, okToContinue, "four", "five", "six"); + + Subscriber<String> subscriber = TestHelper.mockSubscriber(); + TestSubscriber<String> ts = new TestSubscriber<>(subscriber, 0L); + + final Flowable<String> concat = Flowable.concat(Flowable.unsafeCreate(w1), Flowable.unsafeCreate(w2)); + + try { + // Subscribe + concat.subscribe(ts); + //Block main thread to allow observable "w1" to complete and observable "w2" to call onNext once. + callOnce.await(); + // Unsubcribe + ts.cancel(); + //Unblock the observable to continue. + okToContinue.countDown(); + w1.t.join(); + w2.t.join(); + } catch (Throwable e) { + e.printStackTrace(); + fail(e.getMessage()); + } + + InOrder inOrder = inOrder(subscriber); + inOrder.verify(subscriber, times(1)).onNext("one"); + inOrder.verify(subscriber, times(1)).onNext("two"); + inOrder.verify(subscriber, times(1)).onNext("three"); + inOrder.verify(subscriber, times(1)).onNext("four"); + inOrder.verify(subscriber, never()).onNext("five"); + inOrder.verify(subscriber, never()).onNext("six"); + inOrder.verify(subscriber, never()).onComplete(); + + } + + /** + * All observables will be running in different threads so subscribe() is unblocked. CountDownLatch is only used in order to call unsubscribe() in a predictable manner. + */ + @Test + public void concatUnsubscribeConcurrent() { + final CountDownLatch callOnce = new CountDownLatch(1); + final CountDownLatch okToContinue = new CountDownLatch(1); + final TestObservable<String> w1 = new TestObservable<>("one", "two", "three"); + final TestObservable<String> w2 = new TestObservable<>(callOnce, okToContinue, "four", "five", "six"); + + Subscriber<String> subscriber = TestHelper.mockSubscriber(); + TestSubscriber<String> ts = new TestSubscriber<>(subscriber, 0L); + + TestObservable<Flowable<String>> observableOfObservables = new TestObservable<>(Flowable.unsafeCreate(w1), Flowable.unsafeCreate(w2)); + Flowable<String> concatF = Flowable.concat(Flowable.unsafeCreate(observableOfObservables)); + + concatF.subscribe(ts); + + try { + //Block main thread to allow observable "w1" to complete and observable "w2" to call onNext exactly once. + callOnce.await(); + //"four" from w2 has been processed by onNext() + ts.cancel(); + //"five" and "six" will NOT be processed by onNext() + //Unblock the observable to continue. + okToContinue.countDown(); + w1.t.join(); + w2.t.join(); + } catch (Throwable e) { + e.printStackTrace(); + fail(e.getMessage()); + } + + InOrder inOrder = inOrder(subscriber); + inOrder.verify(subscriber, times(1)).onNext("one"); + inOrder.verify(subscriber, times(1)).onNext("two"); + inOrder.verify(subscriber, times(1)).onNext("three"); + inOrder.verify(subscriber, times(1)).onNext("four"); + inOrder.verify(subscriber, never()).onNext("five"); + inOrder.verify(subscriber, never()).onNext("six"); + verify(subscriber, never()).onComplete(); + verify(subscriber, never()).onError(any(Throwable.class)); + } + + private static class TestObservable<T> implements Publisher<T> { + + private final Subscription s = new Subscription() { + + @Override + public void request(long n) { + + } + + @Override + public void cancel() { + subscribed = false; + } + }; + private final List<T> values; + private Thread t; + private int count; + private volatile boolean subscribed = true; + private final CountDownLatch once; + private final CountDownLatch okToContinue; + private final CountDownLatch threadHasStarted = new CountDownLatch(1); + private final T seed; + private final int size; + + @SafeVarargs + TestObservable(T... values) { + this(null, null, values); + } + + @SafeVarargs + TestObservable(CountDownLatch once, CountDownLatch okToContinue, T... values) { + this.values = Arrays.asList(values); + this.size = this.values.size(); + this.once = once; + this.okToContinue = okToContinue; + this.seed = null; + } + + TestObservable(T seed, int size) { + values = null; + once = null; + okToContinue = null; + this.seed = seed; + this.size = size; + } + + @Override + public void subscribe(final Subscriber<? super T> subscriber) { + subscriber.onSubscribe(s); + t = new Thread(new Runnable() { + + @Override + public void run() { + try { + while (count < size && subscribed) { + if (null != values) { + subscriber.onNext(values.get(count)); + } else { + subscriber.onNext(seed); + } + count++; + //Unblock the main thread to call unsubscribe. + if (null != once) { + once.countDown(); + } + //Block until the main thread has called unsubscribe. + if (null != okToContinue) { + okToContinue.await(5, TimeUnit.SECONDS); + } + } + if (subscribed) { + subscriber.onComplete(); + } + } catch (InterruptedException e) { + e.printStackTrace(); + fail(e.getMessage()); + } + } + + }); + t.start(); + threadHasStarted.countDown(); + } + + void waitForThreadDone() throws InterruptedException { + threadHasStarted.await(); + t.join(); + } + } + + @Test + public void multipleObservers() { + Subscriber<Object> subscriber1 = TestHelper.mockSubscriber(); + Subscriber<Object> subscriber2 = TestHelper.mockSubscriber(); + + TestScheduler s = new TestScheduler(); + + Flowable<Long> timer = Flowable.interval(500, TimeUnit.MILLISECONDS, s).take(2); + Flowable<Long> f = Flowable.concat(timer, timer); + + f.subscribe(subscriber1); + f.subscribe(subscriber2); + + InOrder inOrder1 = inOrder(subscriber1); + InOrder inOrder2 = inOrder(subscriber2); + + s.advanceTimeBy(500, TimeUnit.MILLISECONDS); + + inOrder1.verify(subscriber1, times(1)).onNext(0L); + inOrder2.verify(subscriber2, times(1)).onNext(0L); + + s.advanceTimeBy(500, TimeUnit.MILLISECONDS); + + inOrder1.verify(subscriber1, times(1)).onNext(1L); + inOrder2.verify(subscriber2, times(1)).onNext(1L); + + s.advanceTimeBy(500, TimeUnit.MILLISECONDS); + + inOrder1.verify(subscriber1, times(1)).onNext(0L); + inOrder2.verify(subscriber2, times(1)).onNext(0L); + + s.advanceTimeBy(500, TimeUnit.MILLISECONDS); + + inOrder1.verify(subscriber1, times(1)).onNext(1L); + inOrder2.verify(subscriber2, times(1)).onNext(1L); + + inOrder1.verify(subscriber1, times(1)).onComplete(); + inOrder2.verify(subscriber2, times(1)).onComplete(); + + verify(subscriber1, never()).onError(any(Throwable.class)); + verify(subscriber2, never()).onError(any(Throwable.class)); + } + + @Test + public void concatVeryLongObservableOfObservables() { + final int n = 10000; + Flowable<Flowable<Integer>> source = Flowable.range(0, n).map(new Function<Integer, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Integer v) { + return Flowable.just(v); + } + }); + + Single<List<Integer>> result = Flowable.concat(source).toList(); + + SingleObserver<List<Integer>> o = TestHelper.mockSingleObserver(); + InOrder inOrder = inOrder(o); + + result.subscribe(o); + + List<Integer> list = new ArrayList<>(n); + for (int i = 0; i < n; i++) { + list.add(i); + } + inOrder.verify(o).onSuccess(list); + verify(o, never()).onError(any(Throwable.class)); + } + + @Test + public void concatVeryLongObservableOfObservablesTakeHalf() { + final int n = 10000; + Flowable<Flowable<Integer>> source = Flowable.range(0, n).map(new Function<Integer, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Integer v) { + return Flowable.just(v); + } + }); + + Single<List<Integer>> result = Flowable.concat(source).take(n / 2).toList(); + + SingleObserver<List<Integer>> o = TestHelper.mockSingleObserver(); + InOrder inOrder = inOrder(o); + + result.subscribe(o); + + List<Integer> list = new ArrayList<>(n); + for (int i = 0; i < n / 2; i++) { + list.add(i); + } + inOrder.verify(o).onSuccess(list); + verify(o, never()).onError(any(Throwable.class)); + } + + @Test + public void concatOuterBackpressure() { + assertEquals(1, + (int) Flowable.<Integer> empty() + .concatWith(Flowable.just(1)) + .take(1) + .blockingSingle()); + } + + @Test + public void innerBackpressureWithAlignedBoundaries() { + TestSubscriber<Integer> ts = new TestSubscriber<>(); + Flowable.range(0, Flowable.bufferSize() * 2) + .concatWith(Flowable.range(0, Flowable.bufferSize() * 2)) + .observeOn(Schedulers.computation()) // observeOn has a backpressured RxRingBuffer + .subscribe(ts); + + ts.awaitDone(5, TimeUnit.SECONDS); + ts.assertNoErrors(); + assertEquals(Flowable.bufferSize() * 4, ts.values().size()); + } + + /* + * Testing without counts aligned with buffer sizes because concat must prevent the subscription + * to the next Flowable if request == 0 which can happen at the end of a subscription + * if the request size == emitted size. It needs to delay subscription until the next request when aligned, + * when not aligned, it just subscribesNext with the outstanding request amount. + */ + @Test + public void innerBackpressureWithoutAlignedBoundaries() { + TestSubscriber<Integer> ts = new TestSubscriber<>(); + Flowable.range(0, (Flowable.bufferSize() * 2) + 10) + .concatWith(Flowable.range(0, (Flowable.bufferSize() * 2) + 10)) + .observeOn(Schedulers.computation()) // observeOn has a backpressured RxRingBuffer + .subscribe(ts); + + ts.awaitDone(5, TimeUnit.SECONDS); + ts.assertNoErrors(); + assertEquals((Flowable.bufferSize() * 4) + 20, ts.values().size()); + } + + // https://github.com/ReactiveX/RxJava/issues/1818 + @Test + public void concatWithNonCompliantSourceDoubleOnComplete() { + Flowable<String> f = Flowable.unsafeCreate(new Publisher<String>() { + + @Override + public void subscribe(Subscriber<? super String> s) { + s.onSubscribe(new BooleanSubscription()); + s.onNext("hello"); + s.onComplete(); + s.onComplete(); + } + + }); + + TestSubscriberEx<String> ts = new TestSubscriberEx<>(); + Flowable.concat(f, f).subscribe(ts); + ts.awaitDone(500, TimeUnit.MILLISECONDS); + ts.assertTerminated(); + ts.assertNoErrors(); + ts.assertValues("hello", "hello"); + } + + @Test + public void issue2890NoStackoverflow() throws InterruptedException, TimeoutException { + final ExecutorService executor = Executors.newFixedThreadPool(2); + final Scheduler sch = Schedulers.from(executor); + + Function<Integer, Flowable<Integer>> func = new Function<Integer, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Integer t) { + Flowable<Integer> flowable = Flowable.just(t) + .subscribeOn(sch) + ; + FlowableProcessor<Integer> processor = UnicastProcessor.create(); + flowable.subscribe(processor); + return processor; + } + }; + + int n = 5000; + final AtomicInteger counter = new AtomicInteger(); + + Flowable.range(1, n).concatMap(func).subscribe(new DefaultSubscriber<Integer>() { + @Override + public void onNext(Integer t) { + // Consume after sleep for 1 ms + try { + Thread.sleep(1); + } catch (InterruptedException e) { + // ignored + } + if (counter.getAndIncrement() % 100 == 0) { + System.out.print("testIssue2890NoStackoverflow -> "); + System.out.println(counter.get()); + }; + } + + @Override + public void onComplete() { + executor.shutdown(); + } + + @Override + public void onError(Throwable e) { + executor.shutdown(); + } + }); + + long awaitTerminationTimeoutMillis = 100_000; + if (!executor.awaitTermination(awaitTerminationTimeoutMillis, TimeUnit.MILLISECONDS)) { + throw new TimeoutException("Completed " + counter.get() + "/" + n + " before timed out after " + + awaitTerminationTimeoutMillis + " milliseconds."); + } + + assertEquals(n, counter.get()); + } + + @Test + public void requestOverflowDoesNotStallStream() { + Flowable<Integer> f1 = Flowable.just(1, 2, 3); + Flowable<Integer> f2 = Flowable.just(4, 5, 6); + final AtomicBoolean completed = new AtomicBoolean(false); + f1.concatWith(f2).subscribe(new DefaultSubscriber<Integer>() { + + @Override + public void onComplete() { + completed.set(true); + } + + @Override + public void onError(Throwable e) { + + } + + @Override + public void onNext(Integer t) { + request(2); + }}); + + assertTrue(completed.get()); + } + + @Test + public void concatMapRangeAsyncLoopIssue2876() { + final long durationSeconds = 2; + final long startTime = System.currentTimeMillis(); + for (int i = 0;; i++) { + //only run this for a max of ten seconds + if (System.currentTimeMillis() - startTime > TimeUnit.SECONDS.toMillis(durationSeconds)) { + return; + } + if (i % 1000 == 0) { + System.out.println("concatMapRangeAsyncLoop > " + i); + } + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(); + Flowable.range(0, 1000) + .concatMap(new Function<Integer, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Integer t) { + return Flowable.fromIterable(Arrays.asList(t)); + } + }) + .observeOn(Schedulers.computation()).subscribe(ts); + + ts.awaitDone(2500, TimeUnit.MILLISECONDS); + ts.assertTerminated(); + ts.assertNoErrors(); + assertEquals(1000, ts.values().size()); + assertEquals((Integer)999, ts.values().get(999)); + } + } + + @SuppressWarnings("unchecked") + @Test + public void arrayDelayError() { + Publisher<Integer>[] sources = new Publisher[] { + Flowable.just(1), + null, + Flowable.range(2, 3), + Flowable.error(new TestException()), + Flowable.empty() + }; + + TestSubscriberEx<Integer> ts = Flowable.concatArrayDelayError(sources).to(TestHelper.<Integer>testConsumer()); + + ts.assertFailure(CompositeException.class, 1, 2, 3, 4); + + CompositeException composite = (CompositeException)ts.errors().get(0); + List<Throwable> list = composite.getExceptions(); + assertTrue(list.get(0).toString(), list.get(0) instanceof NullPointerException); + assertTrue(list.get(1).toString(), list.get(1) instanceof TestException); + } + + @Test + public void scalarAndRangeBackpressured() { + TestSubscriber<Integer> ts = TestSubscriber.create(0); + + Flowable.just(1).concatWith(Flowable.range(2, 3)).subscribe(ts); + + ts.assertNoValues(); + + ts.request(5); + + ts.assertValues(1, 2, 3, 4); + ts.assertComplete(); + ts.assertNoErrors(); + } + + @Test + public void scalarAndEmptyBackpressured() { + TestSubscriber<Integer> ts = TestSubscriber.create(0); + + Flowable.just(1).concatWith(Flowable.<Integer>empty()).subscribe(ts); + + ts.assertNoValues(); + + ts.request(5); + + ts.assertValue(1); + ts.assertComplete(); + ts.assertNoErrors(); + } + + @Test + public void rangeAndEmptyBackpressured() { + TestSubscriber<Integer> ts = TestSubscriber.create(0); + + Flowable.range(1, 2).concatWith(Flowable.<Integer>empty()).subscribe(ts); + + ts.assertNoValues(); + + ts.request(5); + + ts.assertValues(1, 2); + ts.assertComplete(); + ts.assertNoErrors(); + } + + @Test + public void emptyAndScalarBackpressured() { + TestSubscriber<Integer> ts = TestSubscriber.create(0); + + Flowable.<Integer>empty().concatWith(Flowable.just(1)).subscribe(ts); + + ts.assertNoValues(); + + ts.request(5); + + ts.assertValue(1); + ts.assertComplete(); + ts.assertNoErrors(); + } + + @SuppressWarnings("unchecked") + @Test + public void concatArray() throws Exception { + for (int i = 2; i < 10; i++) { + Flowable<Integer>[] obs = new Flowable[i]; + Arrays.fill(obs, Flowable.just(1)); + + Integer[] expected = new Integer[i]; + Arrays.fill(expected, 1); + + Method m = Flowable.class.getMethod("concatArray", Publisher[].class); + + TestSubscriber<Integer> ts = TestSubscriber.create(); + + ((Flowable<Integer>)m.invoke(null, new Object[]{obs})).subscribe(ts); + + ts.assertValues(expected); + ts.assertNoErrors(); + ts.assertComplete(); + } + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + @Test + public void concatMapJustJust() { + TestSubscriber<Integer> ts = TestSubscriber.create(); + + Flowable.just(Flowable.just(1)).concatMap((Function)Functions.identity()).subscribe(ts); + + ts.assertValue(1); + ts.assertNoErrors(); + ts.assertComplete(); + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + @Test + public void concatMapJustRange() { + TestSubscriber<Integer> ts = TestSubscriber.create(); + + Flowable.just(Flowable.range(1, 5)).concatMap((Function)Functions.identity()).subscribe(ts); + + ts.assertValues(1, 2, 3, 4, 5); + ts.assertNoErrors(); + ts.assertComplete(); + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + @Test + public void concatMapDelayErrorJustJust() { + TestSubscriber<Integer> ts = TestSubscriber.create(); + + Flowable.just(Flowable.just(1)).concatMapDelayError((Function)Functions.identity()).subscribe(ts); + + ts.assertValue(1); + ts.assertNoErrors(); + ts.assertComplete(); + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + @Test + public void concatMapDelayErrorJustRange() { + TestSubscriber<Integer> ts = TestSubscriber.create(); + + Flowable.just(Flowable.range(1, 5)).concatMapDelayError((Function)Functions.identity()).subscribe(ts); + + ts.assertValues(1, 2, 3, 4, 5); + ts.assertNoErrors(); + ts.assertComplete(); + } + + @SuppressWarnings("unchecked") + @Test + public void startWithArray() throws Exception { + for (int i = 2; i < 10; i++) { + Object[] obs = new Object[i]; + Arrays.fill(obs, 1); + + Integer[] expected = new Integer[i]; + Arrays.fill(expected, 1); + + Method m = Flowable.class.getMethod("startWithArray", Object[].class); + + TestSubscriber<Integer> ts = TestSubscriber.create(); + + ((Flowable<Integer>)m.invoke(Flowable.empty(), new Object[]{obs})).subscribe(ts); + + ts.assertValues(expected); + ts.assertNoErrors(); + ts.assertComplete(); + } + } + + static final class InfiniteIterator implements Iterator<Integer>, Iterable<Integer> { + + int count; + + @Override + public boolean hasNext() { + return true; + } + + @Override + public Integer next() { + return count++; + } + + @Override + public void remove() { + } + + @Override + public Iterator<Integer> iterator() { + return this; + } + } + + @Test + public void veryLongTake() { + Flowable.fromIterable(new InfiniteIterator()).concatWith(Flowable.<Integer>empty()).take(10) + .test() + .assertResult(0, 1, 2, 3, 4, 5, 6, 7, 8, 9); + } + + @Test + public void concat3() { + Flowable<Integer> source = Flowable.just(1); + + Flowable.concat(source, source, source) + .test() + .assertResult(1, 1, 1); + } + + @Test + public void concat4() { + Flowable<Integer> source = Flowable.just(1); + + Flowable.concat(source, source, source, source) + .test() + .assertResult(1, 1, 1, 1); + } + + @Test + public void concatArrayDelayError() { + Flowable.concatArrayDelayError(Flowable.just(1), Flowable.just(2), + Flowable.just(3), Flowable.just(4)) + .test() + .assertResult(1, 2, 3, 4); + } + + @Test + public void concatArrayDelayErrorWithError() { + Flowable.concatArrayDelayError(Flowable.just(1), Flowable.just(2), + Flowable.just(3).concatWith(Flowable.<Integer>error(new TestException())), + Flowable.just(4)) + .test() + .assertFailure(TestException.class, 1, 2, 3, 4); + } + + @Test + public void concatIterableDelayError() { + Flowable.concatDelayError( + Arrays.asList(Flowable.just(1), Flowable.just(2), + Flowable.just(3), Flowable.just(4))) + .test() + .assertResult(1, 2, 3, 4); + } + + @Test + public void concatIterableDelayErrorWithError() { + Flowable.concatDelayError( + Arrays.asList(Flowable.just(1), Flowable.just(2), + Flowable.just(3).concatWith(Flowable.<Integer>error(new TestException())), + Flowable.just(4))) + .test() + .assertFailure(TestException.class, 1, 2, 3, 4); + } + + @Test + public void concatObservableDelayError() { + Flowable.concatDelayError( + Flowable.just(Flowable.just(1), Flowable.just(2), + Flowable.just(3), Flowable.just(4))) + .test() + .assertResult(1, 2, 3, 4); + } + + @Test + public void concatObservableDelayErrorWithError() { + Flowable.concatDelayError( + Flowable.just(Flowable.just(1), Flowable.just(2), + Flowable.just(3).concatWith(Flowable.<Integer>error(new TestException())), + Flowable.just(4))) + .test() + .assertFailure(TestException.class, 1, 2, 3, 4); + } + + @Test + public void concatObservableDelayErrorBoundary() { + Flowable.concatDelayError( + Flowable.just(Flowable.just(1), Flowable.just(2), + Flowable.just(3).concatWith(Flowable.<Integer>error(new TestException())), + Flowable.just(4)), 2, false) + .test() + .assertFailure(TestException.class, 1, 2, 3); + } + + @Test + public void concatObservableDelayErrorTillEnd() { + Flowable.concatDelayError( + Flowable.just(Flowable.just(1), Flowable.just(2), + Flowable.just(3).concatWith(Flowable.<Integer>error(new TestException())), + Flowable.just(4)), 2, true) + .test() + .assertFailure(TestException.class, 1, 2, 3, 4); + } + + @Test + public void concatMapDelayError() { + Flowable.just(Flowable.just(1), Flowable.just(2)) + .concatMapDelayError(Functions.<Flowable<Integer>>identity()) + .test() + .assertResult(1, 2); + } + + @Test + public void concatMapDelayErrorWithError() { + Flowable.just(Flowable.just(1).concatWith(Flowable.<Integer>error(new TestException())), Flowable.just(2)) + .concatMapDelayError(Functions.<Flowable<Integer>>identity()) + .test() + .assertFailure(TestException.class, 1, 2); + } + + @Test + public void concatMapIterableBufferSize() { + + Flowable.just(1, 2).concatMapIterable(new Function<Integer, Iterable<Integer>>() { + @Override + public Iterable<Integer> apply(Integer v) throws Exception { + return Arrays.asList(1, 2, 3, 4, 5); + } + }, 1) + .test() + .assertResult(1, 2, 3, 4, 5, 1, 2, 3, 4, 5); + } + + @Test + public void emptyArray() { + assertSame(Flowable.empty(), Flowable.concatArrayDelayError()); + } + + @Test + public void singleElementArray() { + assertSame(Flowable.never(), Flowable.concatArrayDelayError(Flowable.never())); + } + + @Test + public void concatMapDelayErrorEmptySource() { + assertSame(Flowable.empty(), Flowable.<Object>empty() + .concatMapDelayError(new Function<Object, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Object v) throws Exception { + return Flowable.just(1); + } + }, true, 16)); + } + + @Test + public void concatMapDelayErrorJustSource() { + Flowable.just(0) + .concatMapDelayError(new Function<Object, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Object v) throws Exception { + return Flowable.just(1); + } + }, true, 16) + .test() + .assertResult(1); + + } + + @Test + public void concatArrayEmpty() { + assertSame(Flowable.empty(), Flowable.concatArray()); + } + + @Test + public void concatArraySingleElement() { + assertSame(Flowable.never(), Flowable.concatArray(Flowable.never())); + } + + @Test + public void concatMapErrorEmptySource() { + assertSame(Flowable.empty(), Flowable.<Object>empty() + .concatMap(new Function<Object, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Object v) throws Exception { + return Flowable.just(1); + } + }, 16)); + } + + @Test + public void concatMapJustSource() { + Flowable.just(0).hide() + .concatMap(new Function<Object, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Object v) throws Exception { + return Flowable.just(1); + } + }, 16) + .test() + .assertResult(1); + } + + @Test + public void concatMapJustSourceDelayError() { + Flowable.just(0).hide() + .concatMapDelayError(new Function<Object, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Object v) throws Exception { + return Flowable.just(1); + } + }, false, 16) + .test() + .assertResult(1); + } + + @Test + public void concatMapScalarBackpressured() { + Flowable.just(1).hide() + .concatMap(Functions.justFunction(Flowable.just(2))) + .test(1L) + .assertResult(2); + } + + @Test + public void concatMapScalarBackpressuredDelayError() { + Flowable.just(1).hide() + .concatMapDelayError(Functions.justFunction(Flowable.just(2))) + .test(1L) + .assertResult(2); + } + + @Test + public void concatMapEmpty() { + Flowable.just(1).hide() + .concatMap(Functions.justFunction(Flowable.empty())) + .test() + .assertResult(); + } + + @Test + public void concatMapEmptyDelayError() { + Flowable.just(1).hide() + .concatMapDelayError(Functions.justFunction(Flowable.empty())) + .test() + .assertResult(); + } + + @Test + public void ignoreBackpressure() { + new Flowable<Integer>() { + @Override + protected void subscribeActual(Subscriber<? super Integer> s) { + s.onSubscribe(new BooleanSubscription()); + for (int i = 0; i < 10; i++) { + s.onNext(i); + } + } + } + .concatMap(Functions.justFunction(Flowable.just(2)), 8) + .test(0L) + .assertFailure(QueueOverflowException.class); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeFlowable(new Function<Flowable<Object>, Publisher<Integer>>() { + @Override + public Publisher<Integer> apply(Flowable<Object> f) throws Exception { + return f.concatMap(Functions.justFunction(Flowable.just(2))); + } + }); + TestHelper.checkDoubleOnSubscribeFlowable(new Function<Flowable<Object>, Publisher<Integer>>() { + @Override + public Publisher<Integer> apply(Flowable<Object> f) throws Exception { + return f.concatMapDelayError(Functions.justFunction(Flowable.just(2))); + } + }); + } + + @Test + public void immediateInnerNextOuterError() { + final PublishProcessor<Integer> pp = PublishProcessor.create(); + + final TestSubscriberEx<Integer> ts = new TestSubscriberEx<Integer>() { + @Override + public void onNext(Integer t) { + super.onNext(t); + if (t == 1) { + pp.onError(new TestException("First")); + } + } + }; + + pp.concatMap(Functions.justFunction(Flowable.just(1))) + .subscribe(ts); + + pp.onNext(1); + + assertFalse(pp.hasSubscribers()); + + ts.assertFailureAndMessage(TestException.class, "First", 1); + } + + @Test + public void immediateInnerNextOuterError2() { + final PublishProcessor<Integer> pp = PublishProcessor.create(); + + final TestSubscriberEx<Integer> ts = new TestSubscriberEx<Integer>() { + @Override + public void onNext(Integer t) { + super.onNext(t); + if (t == 1) { + pp.onError(new TestException("First")); + } + } + }; + + pp.concatMap(Functions.justFunction(Flowable.just(1).hide())) + .subscribe(ts); + + pp.onNext(1); + + assertFalse(pp.hasSubscribers()); + + ts.assertFailureAndMessage(TestException.class, "First", 1); + } + + @Test + public void concatMapInnerError() { + Flowable.just(1).hide() + .concatMap(Functions.justFunction(Flowable.error(new TestException()))) + .test() + .assertFailure(TestException.class); + } + + @Test + public void concatMapInnerErrorDelayError() { + Flowable.just(1).hide() + .concatMapDelayError(Functions.justFunction(Flowable.error(new TestException()))) + .test() + .assertFailure(TestException.class); + } + + @Test + public void badSource() { + TestHelper.checkBadSourceFlowable(new Function<Flowable<Integer>, Object>() { + @Override + public Object apply(Flowable<Integer> f) throws Exception { + return f.concatMap(Functions.justFunction(Flowable.just(1).hide())); + } + }, true, 1, 1, 1); + } + + @Test + public void badInnerSource() { + @SuppressWarnings("rawtypes") + final Subscriber[] ts0 = { null }; + TestSubscriberEx<Integer> ts = Flowable.just(1).hide().concatMap(Functions.justFunction(new Flowable<Integer>() { + @Override + protected void subscribeActual(Subscriber<? super Integer> s) { + ts0[0] = s; + s.onSubscribe(new BooleanSubscription()); + s.onError(new TestException("First")); + } + })) + .to(TestHelper.<Integer>testConsumer()); + + ts.assertFailureAndMessage(TestException.class, "First"); + + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + ts0[0].onError(new TestException("Second")); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void badInnerSourceDelayError() { + @SuppressWarnings("rawtypes") + final Subscriber[] ts0 = { null }; + TestSubscriberEx<Integer> ts = Flowable.just(1).hide().concatMapDelayError(Functions.justFunction(new Flowable<Integer>() { + @Override + protected void subscribeActual(Subscriber<? super Integer> s) { + ts0[0] = s; + s.onSubscribe(new BooleanSubscription()); + s.onError(new TestException("First")); + } + })) + .to(TestHelper.<Integer>testConsumer()); + + ts.assertFailureAndMessage(TestException.class, "First"); + + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + ts0[0].onError(new TestException("Second")); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void badSourceDelayError() { + TestHelper.checkBadSourceFlowable(new Function<Flowable<Integer>, Object>() { + @Override + public Object apply(Flowable<Integer> f) throws Exception { + return f.concatMapDelayError(Functions.justFunction(Flowable.just(1).hide())); + } + }, true, 1, 1, 1); + } + + @Test + public void fusedCrash() { + Flowable.range(1, 2) + .map(new Function<Integer, Object>() { + @Override + public Object apply(Integer v) throws Exception { throw new TestException(); } + }) + .concatMap(Functions.justFunction(Flowable.just(1))) + .test() + .assertFailure(TestException.class); + } + + @Test + public void fusedCrashDelayError() { + Flowable.range(1, 2) + .map(new Function<Integer, Object>() { + @Override + public Object apply(Integer v) throws Exception { throw new TestException(); } + }) + .concatMapDelayError(Functions.justFunction(Flowable.just(1))) + .test() + .assertFailure(TestException.class); + } + + @Test + public void callableCrash() { + Flowable.just(1).hide() + .concatMap(Functions.justFunction(Flowable.fromCallable(new Callable<Object>() { + @Override + public Object call() throws Exception { + throw new TestException(); + } + }))) + .test() + .assertFailure(TestException.class); + } + + @Test + public void callableCrashDelayError() { + Flowable.just(1).hide() + .concatMapDelayError(Functions.justFunction(Flowable.fromCallable(new Callable<Object>() { + @Override + public Object call() throws Exception { + throw new TestException(); + } + }))) + .test() + .assertFailure(TestException.class); + } + + @Test + public void dispose() { + TestHelper.checkDisposed(Flowable.range(1, 2) + .concatMap(Functions.justFunction(Flowable.just(1)))); + + TestHelper.checkDisposed(Flowable.range(1, 2) + .concatMapDelayError(Functions.justFunction(Flowable.just(1)))); + } + + @Test + public void notVeryEnd() { + Flowable.range(1, 2) + .concatMapDelayError(Functions.justFunction(Flowable.error(new TestException())), false, 16) + .test() + .assertFailure(TestException.class); + } + + @Test + public void error() { + Flowable.error(new TestException()) + .concatMapDelayError(Functions.justFunction(Flowable.just(2)), false, 16) + .test() + .assertFailure(TestException.class); + } + + @Test + public void mapperThrows() { + Flowable.range(1, 2) + .concatMap(new Function<Integer, Publisher<Object>>() { + @Override + public Publisher<Object> apply(Integer v) throws Exception { + throw new TestException(); + } + }) + .test() + .assertFailure(TestException.class); + } + + @Test + public void noSubsequentSubscription() { + final int[] calls = { 0 }; + + Flowable<Integer> source = Flowable.create(new FlowableOnSubscribe<Integer>() { + @Override + public void subscribe(FlowableEmitter<Integer> s) throws Exception { + calls[0]++; + s.onNext(1); + s.onComplete(); + } + }, BackpressureStrategy.MISSING); + + Flowable.concatArray(source, source).firstElement() + .test() + .assertResult(1); + + assertEquals(1, calls[0]); + } + + @Test + public void noSubsequentSubscriptionDelayError() { + final int[] calls = { 0 }; + + Flowable<Integer> source = Flowable.create(new FlowableOnSubscribe<Integer>() { + @Override + public void subscribe(FlowableEmitter<Integer> s) throws Exception { + calls[0]++; + s.onNext(1); + s.onComplete(); + } + }, BackpressureStrategy.MISSING); + + Flowable.concatArrayDelayError(source, source).firstElement() + .test() + .assertResult(1); + + assertEquals(1, calls[0]); + } + + @Test + public void noSubsequentSubscriptionIterable() { + final int[] calls = { 0 }; + + Flowable<Integer> source = Flowable.create(new FlowableOnSubscribe<Integer>() { + @Override + public void subscribe(FlowableEmitter<Integer> s) throws Exception { + calls[0]++; + s.onNext(1); + s.onComplete(); + } + }, BackpressureStrategy.MISSING); + + Flowable.concat(Arrays.asList(source, source)).firstElement() + .test() + .assertResult(1); + + assertEquals(1, calls[0]); + } + + @Test + public void noSubsequentSubscriptionDelayErrorIterable() { + final int[] calls = { 0 }; + + Flowable<Integer> source = Flowable.create(new FlowableOnSubscribe<Integer>() { + @Override + public void subscribe(FlowableEmitter<Integer> s) throws Exception { + calls[0]++; + s.onNext(1); + s.onComplete(); + } + }, BackpressureStrategy.MISSING); + + Flowable.concatDelayError(Arrays.asList(source, source)).firstElement() + .test() + .assertResult(1); + + assertEquals(1, calls[0]); + } + + @Test + public void noCancelPreviousArray() { + final AtomicInteger counter = new AtomicInteger(); + + Flowable<Integer> source = Flowable.just(1).doOnCancel(new Action() { + @Override + public void run() throws Exception { + counter.getAndIncrement(); + } + }); + + Flowable.concatArray(source, source, source, source, source) + .test() + .assertResult(1, 1, 1, 1, 1); + + assertEquals(0, counter.get()); + } + + @Test + public void noCancelPreviousIterable() { + final AtomicInteger counter = new AtomicInteger(); + + Flowable<Integer> source = Flowable.just(1).doOnCancel(new Action() { + @Override + public void run() throws Exception { + counter.getAndIncrement(); + } + }); + + Flowable.concat(Arrays.asList(source, source, source, source, source)) + .test() + .assertResult(1, 1, 1, 1, 1); + + assertEquals(0, counter.get()); + } + + @Test + public void arrayDelayErrorMultipleErrors() { + TestSubscriberEx<Object> ts = new TestSubscriberEx<>(); + + Flowable.concatArrayDelayError(Flowable.error(new IOException()), Flowable.error(new TestException())) + .subscribe(ts); + + ts.assertFailure(CompositeException.class); + + TestHelper.assertCompositeExceptions(ts, IOException.class, TestException.class); + } + + @Test + public void arrayDelayErrorMultipleNullErrors() { + TestSubscriberEx<Object> ts = new TestSubscriberEx<>(); + + Flowable.concatArrayDelayError(null, null) + .subscribe(ts); + + ts.assertFailure(CompositeException.class); + + TestHelper.assertCompositeExceptions(ts, NullPointerException.class, NullPointerException.class); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableConcatWithCompletableTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableConcatWithCompletableTest.java new file mode 100644 index 0000000000..845c814dbc --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableConcatWithCompletableTest.java @@ -0,0 +1,136 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import static org.junit.Assert.*; + +import java.util.List; + +import org.junit.Test; +import org.reactivestreams.Subscriber; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.Action; +import io.reactivex.rxjava3.internal.subscriptions.BooleanSubscription; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.subjects.CompletableSubject; +import io.reactivex.rxjava3.subscribers.TestSubscriber; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class FlowableConcatWithCompletableTest extends RxJavaTest { + + @Test + public void normal() { + final TestSubscriber<Integer> ts = new TestSubscriber<>(); + + Flowable.range(1, 5) + .concatWith(Completable.fromAction(new Action() { + @Override + public void run() throws Exception { + ts.onNext(100); + } + })) + .subscribe(ts); + + ts.assertResult(1, 2, 3, 4, 5, 100); + } + + @Test + public void mainError() { + final TestSubscriber<Integer> ts = new TestSubscriber<>(); + + Flowable.<Integer>error(new TestException()) + .concatWith(Completable.fromAction(new Action() { + @Override + public void run() throws Exception { + ts.onNext(100); + } + })) + .subscribe(ts); + + ts.assertFailure(TestException.class); + } + + @Test + public void otherError() { + final TestSubscriber<Integer> ts = new TestSubscriber<>(); + + Flowable.range(1, 5) + .concatWith(Completable.error(new TestException())) + .subscribe(ts); + + ts.assertFailure(TestException.class, 1, 2, 3, 4, 5); + } + + @Test + public void takeMain() { + final TestSubscriber<Integer> ts = new TestSubscriber<>(); + + Flowable.range(1, 5) + .concatWith(Completable.fromAction(new Action() { + @Override + public void run() throws Exception { + ts.onNext(100); + } + })) + .take(3) + .subscribe(ts); + + ts.assertResult(1, 2, 3); + } + + @Test + public void cancelOther() { + CompletableSubject other = CompletableSubject.create(); + + TestSubscriber<Object> ts = Flowable.empty() + .concatWith(other) + .test(); + + assertTrue(other.hasObservers()); + + ts.cancel(); + + assertFalse(other.hasObservers()); + } + + @Test + public void badSource() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + new Flowable<Integer>() { + @Override + protected void subscribeActual(Subscriber<? super Integer> s) { + BooleanSubscription bs1 = new BooleanSubscription(); + s.onSubscribe(bs1); + + BooleanSubscription bs2 = new BooleanSubscription(); + s.onSubscribe(bs2); + + assertFalse(bs1.isCancelled()); + assertTrue(bs2.isCancelled()); + + s.onComplete(); + } + }.concatWith(Completable.complete()) + .test() + .assertResult(); + + TestHelper.assertError(errors, 0, ProtocolViolationException.class); + } finally { + RxJavaPlugins.reset(); + } + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableConcatWithMaybeTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableConcatWithMaybeTest.java new file mode 100644 index 0000000000..5b45eafc7f --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableConcatWithMaybeTest.java @@ -0,0 +1,128 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import static org.junit.Assert.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.Action; +import io.reactivex.rxjava3.subjects.MaybeSubject; +import io.reactivex.rxjava3.subscribers.TestSubscriber; + +public class FlowableConcatWithMaybeTest extends RxJavaTest { + + @Test + public void normalEmpty() { + final TestSubscriber<Integer> ts = new TestSubscriber<>(); + + Flowable.range(1, 5) + .concatWith(Maybe.<Integer>fromAction(new Action() { + @Override + public void run() throws Exception { + ts.onNext(100); + } + })) + .subscribe(ts); + + ts.assertResult(1, 2, 3, 4, 5, 100); + } + + @Test + public void normalNonEmpty() { + final TestSubscriber<Integer> ts = new TestSubscriber<>(); + + Flowable.range(1, 5) + .concatWith(Maybe.just(100)) + .subscribe(ts); + + ts.assertResult(1, 2, 3, 4, 5, 100); + } + + @Test + public void backpressure() { + Flowable.range(1, 5) + .concatWith(Maybe.just(100)) + .test(0) + .assertEmpty() + .requestMore(3) + .assertValues(1, 2, 3) + .requestMore(2) + .assertValues(1, 2, 3, 4, 5) + .requestMore(1) + .assertResult(1, 2, 3, 4, 5, 100); + } + + @Test + public void mainError() { + final TestSubscriber<Integer> ts = new TestSubscriber<>(); + + Flowable.<Integer>error(new TestException()) + .concatWith(Maybe.<Integer>fromAction(new Action() { + @Override + public void run() throws Exception { + ts.onNext(100); + } + })) + .subscribe(ts); + + ts.assertFailure(TestException.class); + } + + @Test + public void otherError() { + final TestSubscriber<Integer> ts = new TestSubscriber<>(); + + Flowable.range(1, 5) + .concatWith(Maybe.<Integer>error(new TestException())) + .subscribe(ts); + + ts.assertFailure(TestException.class, 1, 2, 3, 4, 5); + } + + @Test + public void takeMain() { + final TestSubscriber<Integer> ts = new TestSubscriber<>(); + + Flowable.range(1, 5) + .concatWith(Maybe.<Integer>fromAction(new Action() { + @Override + public void run() throws Exception { + ts.onNext(100); + } + })) + .take(3) + .subscribe(ts); + + ts.assertResult(1, 2, 3); + } + + @Test + public void cancelOther() { + MaybeSubject<Object> other = MaybeSubject.create(); + + TestSubscriber<Object> ts = Flowable.empty() + .concatWith(other) + .test(); + + assertTrue(other.hasObservers()); + + ts.cancel(); + + assertFalse(other.hasObservers()); + } + +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableConcatWithSingleTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableConcatWithSingleTest.java new file mode 100644 index 0000000000..8a7ed3cd65 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableConcatWithSingleTest.java @@ -0,0 +1,101 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import static org.junit.Assert.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.subjects.SingleSubject; +import io.reactivex.rxjava3.subscribers.TestSubscriber; + +public class FlowableConcatWithSingleTest extends RxJavaTest { + + @Test + public void normal() { + final TestSubscriber<Integer> ts = new TestSubscriber<>(); + + Flowable.range(1, 5) + .concatWith(Single.just(100)) + .subscribe(ts); + + ts.assertResult(1, 2, 3, 4, 5, 100); + } + + @Test + public void backpressure() { + Flowable.range(1, 5) + .concatWith(Single.just(100)) + .test(0) + .assertEmpty() + .requestMore(3) + .assertValues(1, 2, 3) + .requestMore(2) + .assertValues(1, 2, 3, 4, 5) + .requestMore(1) + .assertResult(1, 2, 3, 4, 5, 100); + } + + @Test + public void mainError() { + final TestSubscriber<Integer> ts = new TestSubscriber<>(); + + Flowable.<Integer>error(new TestException()) + .concatWith(Single.just(100)) + .subscribe(ts); + + ts.assertFailure(TestException.class); + } + + @Test + public void otherError() { + final TestSubscriber<Integer> ts = new TestSubscriber<>(); + + Flowable.range(1, 5) + .concatWith(Single.<Integer>error(new TestException())) + .subscribe(ts); + + ts.assertFailure(TestException.class, 1, 2, 3, 4, 5); + } + + @Test + public void takeMain() { + final TestSubscriber<Integer> ts = new TestSubscriber<>(); + + Flowable.range(1, 5) + .concatWith(Single.just(100)) + .take(3) + .subscribe(ts); + + ts.assertResult(1, 2, 3); + } + + @Test + public void cancelOther() { + SingleSubject<Object> other = SingleSubject.create(); + + TestSubscriber<Object> ts = Flowable.empty() + .concatWith(other) + .test(); + + assertTrue(other.hasObservers()); + + ts.cancel(); + + assertFalse(other.hasObservers()); + } + +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableCountTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableCountTest.java new file mode 100644 index 0000000000..608138e357 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableCountTest.java @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import org.junit.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class FlowableCountTest extends RxJavaTest { + @Test + public void simpleFlowable() { + Assert.assertEquals(0, Flowable.empty().count().toFlowable().blockingLast().intValue()); + + Assert.assertEquals(1, Flowable.just(1).count().toFlowable().blockingLast().intValue()); + + Assert.assertEquals(10, Flowable.range(1, 10).count().toFlowable().blockingLast().intValue()); + + } + + @Test + public void simple() { + Assert.assertEquals(0, Flowable.empty().count().blockingGet().intValue()); + + Assert.assertEquals(1, Flowable.just(1).count().blockingGet().intValue()); + + Assert.assertEquals(10, Flowable.range(1, 10).count().blockingGet().intValue()); + + } + + @Test + public void dispose() { + TestHelper.checkDisposed(Flowable.just(1).count()); + + TestHelper.checkDisposed(Flowable.just(1).count().toFlowable()); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeFlowable(new Function<Flowable<Object>, Flowable<Long>>() { + @Override + public Flowable<Long> apply(Flowable<Object> f) throws Exception { + return f.count().toFlowable(); + } + }); + + TestHelper.checkDoubleOnSubscribeFlowableToSingle(new Function<Flowable<Object>, SingleSource<Long>>() { + @Override + public SingleSource<Long> apply(Flowable<Object> f) throws Exception { + return f.count(); + } + }); + } + +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableCreateTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableCreateTest.java new file mode 100644 index 0000000000..4c3b735476 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableCreateTest.java @@ -0,0 +1,1130 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import static org.junit.Assert.*; + +import java.io.IOException; +import java.util.*; +import java.util.concurrent.atomic.AtomicReference; + +import org.junit.Test; +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.Cancellable; +import io.reactivex.rxjava3.internal.subscriptions.BooleanSubscription; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.subscribers.TestSubscriber; +import io.reactivex.rxjava3.testsupport.*; + +public class FlowableCreateTest extends RxJavaTest { + + @Test + public void basic() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + final Disposable d = Disposable.empty(); + + Flowable.<Integer>create(new FlowableOnSubscribe<Integer>() { + @Override + public void subscribe(FlowableEmitter<Integer> e) throws Exception { + e.setDisposable(d); + + e.onNext(1); + e.onNext(2); + e.onNext(3); + e.onComplete(); + e.onError(new TestException("first")); + e.onNext(4); + e.onError(new TestException("second")); + e.onComplete(); + } + }, BackpressureStrategy.BUFFER) + .test() + .assertResult(1, 2, 3); + + assertTrue(d.isDisposed()); + + TestHelper.assertUndeliverable(errors, 0, TestException.class, "first"); + TestHelper.assertUndeliverable(errors, 1, TestException.class, "second"); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void basicWithCancellable() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + final Disposable d1 = Disposable.empty(); + final Disposable d2 = Disposable.empty(); + + Flowable.<Integer>create(new FlowableOnSubscribe<Integer>() { + @Override + public void subscribe(FlowableEmitter<Integer> e) throws Exception { + e.setDisposable(d1); + e.setCancellable(new Cancellable() { + @Override + public void cancel() throws Exception { + d2.dispose(); + } + }); + + e.onNext(1); + e.onNext(2); + e.onNext(3); + e.onComplete(); + e.onError(new TestException("first")); + e.onNext(4); + e.onError(new TestException("second")); + e.onComplete(); + } + }, BackpressureStrategy.BUFFER) + .test() + .assertResult(1, 2, 3); + + assertTrue(d1.isDisposed()); + assertTrue(d2.isDisposed()); + + TestHelper.assertUndeliverable(errors, 0, TestException.class, "first"); + TestHelper.assertUndeliverable(errors, 1, TestException.class, "second"); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void basicWithError() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + final Disposable d = Disposable.empty(); + + Flowable.<Integer>create(new FlowableOnSubscribe<Integer>() { + @Override + public void subscribe(FlowableEmitter<Integer> e) throws Exception { + e.setDisposable(d); + + e.onNext(1); + e.onNext(2); + e.onNext(3); + e.onError(new TestException()); + e.onComplete(); + e.onNext(4); + e.onError(new TestException("second")); + } + }, BackpressureStrategy.BUFFER) + .test() + .assertFailure(TestException.class, 1, 2, 3); + + assertTrue(d.isDisposed()); + + TestHelper.assertUndeliverable(errors, 0, TestException.class, "second"); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void basicSerialized() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + final Disposable d = Disposable.empty(); + + Flowable.<Integer>create(new FlowableOnSubscribe<Integer>() { + @Override + public void subscribe(FlowableEmitter<Integer> e) throws Exception { + e = e.serialize(); + + e.setDisposable(d); + + e.onNext(1); + e.onNext(2); + e.onNext(3); + e.onComplete(); + e.onError(new TestException("first")); + e.onNext(4); + e.onError(new TestException("second")); + e.onComplete(); + } + }, BackpressureStrategy.BUFFER) + .test() + .assertResult(1, 2, 3); + + assertTrue(d.isDisposed()); + + TestHelper.assertUndeliverable(errors, 0, TestException.class, "first"); + TestHelper.assertUndeliverable(errors, 1, TestException.class, "second"); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void basicWithErrorSerialized() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + final Disposable d = Disposable.empty(); + + Flowable.<Integer>create(new FlowableOnSubscribe<Integer>() { + @Override + public void subscribe(FlowableEmitter<Integer> e) throws Exception { + e = e.serialize(); + + e.setDisposable(d); + + e.onNext(1); + e.onNext(2); + e.onNext(3); + e.onError(new TestException()); + e.onComplete(); + e.onNext(4); + e.onError(new TestException("second")); + } + }, BackpressureStrategy.BUFFER) + .test() + .assertFailure(TestException.class, 1, 2, 3); + + assertTrue(d.isDisposed()); + + TestHelper.assertUndeliverable(errors, 0, TestException.class, "second"); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void wrap() { + Flowable.fromPublisher(new Publisher<Integer>() { + @Override + public void subscribe(Subscriber<? super Integer> subscriber) { + subscriber.onSubscribe(new BooleanSubscription()); + subscriber.onNext(1); + subscriber.onNext(2); + subscriber.onNext(3); + subscriber.onNext(4); + subscriber.onNext(5); + subscriber.onComplete(); + } + }) + .test() + .assertResult(1, 2, 3, 4, 5); + } + + @Test + public void unsafe() { + Flowable.unsafeCreate(new Publisher<Integer>() { + @Override + public void subscribe(Subscriber<? super Integer> subscriber) { + subscriber.onSubscribe(new BooleanSubscription()); + subscriber.onNext(1); + subscriber.onNext(2); + subscriber.onNext(3); + subscriber.onNext(4); + subscriber.onNext(5); + subscriber.onComplete(); + } + }) + .test() + .assertResult(1, 2, 3, 4, 5); + } + + @Test(expected = IllegalArgumentException.class) + public void unsafeWithFlowable() { + Flowable.unsafeCreate(Flowable.just(1)); + } + + @Test + public void createNullValueBuffer() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + + final Throwable[] error = { null }; + + Flowable.create(new FlowableOnSubscribe<Integer>() { + @Override + public void subscribe(FlowableEmitter<Integer> e) throws Exception { + try { + e.onNext(null); + e.onNext(1); + e.onError(new TestException()); + e.onComplete(); + } catch (Throwable ex) { + error[0] = ex; + } + } + }, BackpressureStrategy.BUFFER) + .test() + .assertFailure(NullPointerException.class); + + assertNull(error[0]); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void createNullValueLatest() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + final Throwable[] error = { null }; + + Flowable.create(new FlowableOnSubscribe<Integer>() { + @Override + public void subscribe(FlowableEmitter<Integer> e) throws Exception { + try { + e.onNext(null); + e.onNext(1); + e.onError(new TestException()); + e.onComplete(); + } catch (Throwable ex) { + error[0] = ex; + } + } + }, BackpressureStrategy.LATEST) + .test() + .assertFailure(NullPointerException.class); + + assertNull(error[0]); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void createNullValueError() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + final Throwable[] error = { null }; + + Flowable.create(new FlowableOnSubscribe<Integer>() { + @Override + public void subscribe(FlowableEmitter<Integer> e) throws Exception { + try { + e.onNext(null); + e.onNext(1); + e.onError(new TestException()); + e.onComplete(); + } catch (Throwable ex) { + error[0] = ex; + } + } + }, BackpressureStrategy.ERROR) + .test() + .assertFailure(NullPointerException.class); + + assertNull(error[0]); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void createNullValueDrop() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + final Throwable[] error = { null }; + + Flowable.create(new FlowableOnSubscribe<Integer>() { + @Override + public void subscribe(FlowableEmitter<Integer> e) throws Exception { + try { + e.onNext(null); + e.onNext(1); + e.onError(new TestException()); + e.onComplete(); + } catch (Throwable ex) { + error[0] = ex; + } + } + }, BackpressureStrategy.DROP) + .test() + .assertFailure(NullPointerException.class); + + assertNull(error[0]); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void createNullValueMissing() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + final Throwable[] error = { null }; + + Flowable.create(new FlowableOnSubscribe<Integer>() { + @Override + public void subscribe(FlowableEmitter<Integer> e) throws Exception { + try { + e.onNext(null); + e.onNext(1); + e.onError(new TestException()); + e.onComplete(); + } catch (Throwable ex) { + error[0] = ex; + } + } + }, BackpressureStrategy.MISSING) + .test() + .assertFailure(NullPointerException.class); + + assertNull(error[0]); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void createNullValueBufferSerialized() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + final Throwable[] error = { null }; + + Flowable.create(new FlowableOnSubscribe<Integer>() { + @Override + public void subscribe(FlowableEmitter<Integer> e) throws Exception { + e = e.serialize(); + try { + e.onNext(null); + e.onNext(1); + e.onError(new TestException()); + e.onComplete(); + } catch (Throwable ex) { + error[0] = ex; + } + } + }, BackpressureStrategy.BUFFER) + .test() + .assertFailure(NullPointerException.class); + + assertNull(error[0]); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void createNullValueLatestSerialized() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + final Throwable[] error = { null }; + + Flowable.create(new FlowableOnSubscribe<Integer>() { + @Override + public void subscribe(FlowableEmitter<Integer> e) throws Exception { + e = e.serialize(); + try { + e.onNext(null); + e.onNext(1); + e.onError(new TestException()); + e.onComplete(); + } catch (Throwable ex) { + error[0] = ex; + } + } + }, BackpressureStrategy.LATEST) + .test() + .assertFailure(NullPointerException.class); + + assertNull(error[0]); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void createNullValueErrorSerialized() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + final Throwable[] error = { null }; + + Flowable.create(new FlowableOnSubscribe<Integer>() { + @Override + public void subscribe(FlowableEmitter<Integer> e) throws Exception { + e = e.serialize(); + try { + e.onNext(null); + e.onNext(1); + e.onError(new TestException()); + e.onComplete(); + } catch (Throwable ex) { + error[0] = ex; + } + } + }, BackpressureStrategy.ERROR) + .test() + .assertFailure(NullPointerException.class); + + assertNull(error[0]); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void createNullValueDropSerialized() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + final Throwable[] error = { null }; + + Flowable.create(new FlowableOnSubscribe<Integer>() { + @Override + public void subscribe(FlowableEmitter<Integer> e) throws Exception { + e = e.serialize(); + try { + e.onNext(null); + e.onNext(1); + e.onError(new TestException()); + e.onComplete(); + } catch (Throwable ex) { + error[0] = ex; + } + } + }, BackpressureStrategy.DROP) + .test() + .assertFailure(NullPointerException.class); + + assertNull(error[0]); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void createNullValueMissingSerialized() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + final Throwable[] error = { null }; + + Flowable.create(new FlowableOnSubscribe<Integer>() { + @Override + public void subscribe(FlowableEmitter<Integer> e) throws Exception { + e = e.serialize(); + try { + e.onNext(null); + e.onNext(1); + e.onError(new TestException()); + e.onComplete(); + } catch (Throwable ex) { + error[0] = ex; + } + } + }, BackpressureStrategy.MISSING) + .test() + .assertFailure(NullPointerException.class); + + assertNull(error[0]); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void onErrorRace() { + for (BackpressureStrategy m : BackpressureStrategy.values()) { + Flowable<Object> source = Flowable.create(new FlowableOnSubscribe<Object>() { + @Override + public void subscribe(FlowableEmitter<Object> e) throws Exception { + final FlowableEmitter<Object> f = e.serialize(); + + final TestException ex = new TestException(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + f.onError(null); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + f.onError(ex); + } + }; + + TestHelper.race(r1, r2); + } + }, m); + + List<Throwable> errors = TestHelper.trackPluginErrors(); + + try { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + source + .test() + .assertFailure(Throwable.class); + } + } finally { + RxJavaPlugins.reset(); + } + assertFalse(errors.isEmpty()); + } + } + + @Test + public void onCompleteRace() { + for (BackpressureStrategy m : BackpressureStrategy.values()) { + Flowable<Object> source = Flowable.create(new FlowableOnSubscribe<Object>() { + @Override + public void subscribe(FlowableEmitter<Object> e) throws Exception { + final FlowableEmitter<Object> f = e.serialize(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + f.onComplete(); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + f.onComplete(); + } + }; + + TestHelper.race(r1, r2); + } + }, m); + + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + source + .test() + .assertResult(); + } + } + } + + @Test + public void nullValue() { + for (BackpressureStrategy m : BackpressureStrategy.values()) { + Flowable.create(new FlowableOnSubscribe<Object>() { + @Override + public void subscribe(FlowableEmitter<Object> e) throws Exception { + e.onNext(null); + } + }, m) + .test() + .assertFailure(NullPointerException.class); + } + } + + @Test + public void nullThrowable() { + for (BackpressureStrategy m : BackpressureStrategy.values()) { + System.out.println(m); + Flowable.create(new FlowableOnSubscribe<Object>() { + @Override + public void subscribe(FlowableEmitter<Object> e) throws Exception { + e.onError(null); + } + }, m) + .test() + .assertFailure(NullPointerException.class); + } + } + + @Test + public void serializedConcurrentOnNextOnError() { + for (BackpressureStrategy m : BackpressureStrategy.values()) { + Flowable.create(new FlowableOnSubscribe<Object>() { + @Override + public void subscribe(FlowableEmitter<Object> e) throws Exception { + final FlowableEmitter<Object> f = e.serialize(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + for (int i = 0; i < 1000; i++) { + f.onNext(1); + } + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + for (int i = 0; i < 100; i++) { + f.onNext(1); + } + f.onError(new TestException()); + } + }; + + TestHelper.race(r1, r2); + } + }, m) + .to(TestHelper.<Object>testConsumer()) + .assertSubscribed() + .assertNotComplete() + .assertError(TestException.class); + } + } + + @Test + public void callbackThrows() { + for (BackpressureStrategy m : BackpressureStrategy.values()) { + Flowable.create(new FlowableOnSubscribe<Object>() { + @Override + public void subscribe(FlowableEmitter<Object> e) throws Exception { + throw new TestException(); + } + }, m) + .test() + .assertFailure(TestException.class); + } + } + + @Test + public void nullValueSync() { + for (BackpressureStrategy m : BackpressureStrategy.values()) { + Flowable.create(new FlowableOnSubscribe<Object>() { + @Override + public void subscribe(FlowableEmitter<Object> e) throws Exception { + e.serialize().onNext(null); + } + }, m) + .test() + .assertFailure(NullPointerException.class); + } + } + + @Test + public void createNullValue() { + for (BackpressureStrategy m : BackpressureStrategy.values()) { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + final Throwable[] error = { null }; + + Flowable.create(new FlowableOnSubscribe<Integer>() { + @Override + public void subscribe(FlowableEmitter<Integer> e) throws Exception { + try { + e.onNext(null); + e.onNext(1); + e.onError(new TestException()); + e.onComplete(); + } catch (Throwable ex) { + error[0] = ex; + } + } + }, m) + .test() + .assertFailure(NullPointerException.class); + + assertNull(error[0]); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + } + + @Test + public void onErrorCrash() { + for (BackpressureStrategy m : BackpressureStrategy.values()) { + Flowable.create(new FlowableOnSubscribe<Object>() { + @Override + public void subscribe(FlowableEmitter<Object> e) throws Exception { + Disposable d = Disposable.empty(); + e.setDisposable(d); + try { + e.onError(new IOException()); + fail("Should have thrown"); + } catch (TestException ex) { + // expected + } + assertTrue(d.isDisposed()); + } + }, m) + .subscribe(new FlowableSubscriber<Object>() { + @Override + public void onSubscribe(Subscription s) { + } + + @Override + public void onNext(Object value) { + } + + @Override + public void onError(Throwable e) { + throw new TestException(); + } + + @Override + public void onComplete() { + } + }); + } + } + + @Test + public void onCompleteCrash() { + for (BackpressureStrategy m : BackpressureStrategy.values()) { + Flowable.create(new FlowableOnSubscribe<Object>() { + @Override + public void subscribe(FlowableEmitter<Object> e) throws Exception { + Disposable d = Disposable.empty(); + e.setDisposable(d); + try { + e.onComplete(); + fail("Should have thrown"); + } catch (TestException ex) { + // expected + } + assertTrue(d.isDisposed()); + } + }, m) + .subscribe(new FlowableSubscriber<Object>() { + @Override + public void onSubscribe(Subscription s) { + } + + @Override + public void onNext(Object value) { + } + + @Override + public void onError(Throwable e) { + } + + @Override + public void onComplete() { + throw new TestException(); + } + }); + } + } + + @Test + public void createNullValueSerialized() { + for (BackpressureStrategy m : BackpressureStrategy.values()) { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + final Throwable[] error = { null }; + + Flowable.create(new FlowableOnSubscribe<Integer>() { + @Override + public void subscribe(FlowableEmitter<Integer> e) throws Exception { + e = e.serialize(); + try { + e.onNext(null); + e.onNext(1); + e.onError(new TestException()); + e.onComplete(); + } catch (Throwable ex) { + error[0] = ex; + } + } + }, m) + .test() + .assertFailure(NullPointerException.class); + + assertNull(error[0]); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + } + + @Test + public void nullThrowableSync() { + for (BackpressureStrategy m : BackpressureStrategy.values()) { + Flowable.create(new FlowableOnSubscribe<Object>() { + @Override + public void subscribe(FlowableEmitter<Object> e) throws Exception { + e.serialize().onError(null); + } + }, m) + .test() + .assertFailure(NullPointerException.class); + } + } + + @Test + public void serializedConcurrentOnNext() { + for (BackpressureStrategy m : BackpressureStrategy.values()) { + Flowable.create(new FlowableOnSubscribe<Object>() { + @Override + public void subscribe(FlowableEmitter<Object> e) throws Exception { + final FlowableEmitter<Object> f = e.serialize(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + f.onNext(1); + } + } + }; + + TestHelper.race(r1, r1); + } + }, m) + .take(TestHelper.RACE_DEFAULT_LOOPS) + .to(TestHelper.<Object>testConsumer()) + .assertSubscribed() + .assertValueCount(TestHelper.RACE_DEFAULT_LOOPS) + .assertComplete() + .assertNoErrors(); + } + } + + @Test + public void serializedConcurrentOnNextOnComplete() { + for (BackpressureStrategy m : BackpressureStrategy.values()) { + TestSubscriberEx<Object> ts = Flowable.create(new FlowableOnSubscribe<Object>() { + @Override + public void subscribe(FlowableEmitter<Object> e) throws Exception { + final FlowableEmitter<Object> f = e.serialize(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + for (int i = 0; i < 1000; i++) { + f.onNext(1); + } + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + for (int i = 0; i < 100; i++) { + f.onNext(1); + } + f.onComplete(); + } + }; + + TestHelper.race(r1, r2); + } + }, m) + .to(TestHelper.<Object>testConsumer()) + .assertSubscribed() + .assertComplete() + .assertNoErrors(); + + int c = ts.values().size(); + assertTrue("" + c, c >= 100); + } + } + + @Test + public void serialized() { + for (BackpressureStrategy m : BackpressureStrategy.values()) { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + Flowable.create(new FlowableOnSubscribe<Object>() { + @Override + public void subscribe(FlowableEmitter<Object> e) throws Exception { + FlowableEmitter<Object> f = e.serialize(); + + assertSame(f, f.serialize()); + + assertFalse(f.isCancelled()); + + final int[] calls = { 0 }; + + f.setCancellable(new Cancellable() { + @Override + public void cancel() throws Exception { + calls[0]++; + } + }); + + e.onComplete(); + + assertTrue(f.isCancelled()); + + assertEquals(1, calls[0]); + } + }, m) + .test() + .assertResult(); + + assertTrue(errors.toString(), errors.isEmpty()); + } finally { + RxJavaPlugins.reset(); + } + } + } + + @Test + public void tryOnError() { + for (BackpressureStrategy strategy : BackpressureStrategy.values()) { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + final Boolean[] response = { null }; + Flowable.create(new FlowableOnSubscribe<Object>() { + @Override + public void subscribe(FlowableEmitter<Object> e) throws Exception { + e.onNext(1); + response[0] = e.tryOnError(new TestException()); + } + }, strategy) + .take(1) + .test() + .withTag(strategy.toString()) + .assertResult(1); + + assertFalse(response[0]); + + assertTrue(strategy + ": " + errors.toString(), errors.isEmpty()); + } finally { + RxJavaPlugins.reset(); + } + } + } + + @Test + public void tryOnErrorSerialized() { + for (BackpressureStrategy strategy : BackpressureStrategy.values()) { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + final Boolean[] response = { null }; + Flowable.create(new FlowableOnSubscribe<Object>() { + @Override + public void subscribe(FlowableEmitter<Object> e) throws Exception { + e = e.serialize(); + e.onNext(1); + response[0] = e.tryOnError(new TestException()); + } + }, strategy) + .take(1) + .test() + .withTag(strategy.toString()) + .assertResult(1); + + assertFalse(response[0]); + + assertTrue(strategy + ": " + errors.toString(), errors.isEmpty()); + } finally { + RxJavaPlugins.reset(); + } + } + } + + @SuppressWarnings("rawtypes") + @Test + public void emittersHasToString() { + Map<BackpressureStrategy, Class<? extends FlowableEmitter>> emitterMap = + new HashMap<>(); + + emitterMap.put(BackpressureStrategy.MISSING, FlowableCreate.MissingEmitter.class); + emitterMap.put(BackpressureStrategy.ERROR, FlowableCreate.ErrorAsyncEmitter.class); + emitterMap.put(BackpressureStrategy.DROP, FlowableCreate.DropAsyncEmitter.class); + emitterMap.put(BackpressureStrategy.LATEST, FlowableCreate.LatestAsyncEmitter.class); + emitterMap.put(BackpressureStrategy.BUFFER, FlowableCreate.BufferAsyncEmitter.class); + + for (final Map.Entry<BackpressureStrategy, Class<? extends FlowableEmitter>> entry : emitterMap.entrySet()) { + Flowable.create(new FlowableOnSubscribe<Object>() { + @Override + public void subscribe(FlowableEmitter<Object> emitter) throws Exception { + assertTrue(emitter.toString().contains(entry.getValue().getSimpleName())); + assertTrue(emitter.serialize().toString().contains(entry.getValue().getSimpleName())); + } + }, entry.getKey()).test().assertEmpty(); + } + } + + @Test + public void serializedMissingMoreWorkWithComplete() { + AtomicReference<FlowableEmitter<Integer>> ref = new AtomicReference<>(); + + Flowable.<Integer>create(emitter -> { + emitter = emitter.serialize(); + ref.set(emitter); + assertEquals(Long.MAX_VALUE, emitter.requested()); + emitter.onNext(1); + }, BackpressureStrategy.MISSING) + .doOnNext(v -> { + if (v == 1) { + ref.get().onNext(2); + ref.get().onComplete(); + } + }) + .test() + .assertResult(1, 2); + } + + @Test + public void badRequest() { + TestHelper.assertBadRequestReported(Flowable.create(e -> { }, BackpressureStrategy.BUFFER)); + } + + @Test + public void tryOnErrorNull() { + Flowable.create(emitter -> emitter.tryOnError(null), BackpressureStrategy.MISSING) + .test() + .assertFailure(NullPointerException.class); + } + + @Test + public void serializedCompleteOnNext() { + TestSubscriber<Integer> ts = new TestSubscriber<>(); + + Flowable.<Integer>create(emitter -> { + emitter = emitter.serialize(); + + emitter.onComplete(); + emitter.onNext(1); + }, BackpressureStrategy.MISSING) + .subscribe(ts); + + ts.assertResult(); + } + + @Test + public void serializedCancelOnNext() { + TestSubscriber<Integer> ts = new TestSubscriber<>(); + + Flowable.<Integer>create(emitter -> { + emitter = emitter.serialize(); + + ts.cancel(); + emitter.onNext(1); + }, BackpressureStrategy.MISSING) + .subscribe(ts); + + ts.assertEmpty(); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableDebounceTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableDebounceTest.java new file mode 100644 index 0000000000..01122106c8 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableDebounceTest.java @@ -0,0 +1,632 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +import io.reactivex.rxjava3.functions.Action; +import org.junit.*; +import org.mockito.InOrder; +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.internal.operators.flowable.FlowableDebounceTimed.*; +import io.reactivex.rxjava3.internal.subscriptions.BooleanSubscription; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.processors.*; +import io.reactivex.rxjava3.schedulers.TestScheduler; +import io.reactivex.rxjava3.subscribers.TestSubscriber; +import io.reactivex.rxjava3.testsupport.*; + +public class FlowableDebounceTest extends RxJavaTest { + + private TestScheduler scheduler; + private Subscriber<String> subscriber; + private Scheduler.Worker innerScheduler; + + @Before + public void before() { + scheduler = new TestScheduler(); + subscriber = TestHelper.mockSubscriber(); + innerScheduler = scheduler.createWorker(); + } + + @Test + public void debounceWithOnDroppedCallbackWithEx() throws Throwable { + Flowable<String> source = Flowable.unsafeCreate(new Publisher<String>() { + @Override + public void subscribe(Subscriber<? super String> subscriber) { + subscriber.onSubscribe(new BooleanSubscription()); + publishNext(subscriber, 100, "one"); // Should be skipped since "two" will arrive before the timeout expires. + publishNext(subscriber, 400, "two"); // Should be published since "three" will arrive after the timeout expires. + publishNext(subscriber, 900, "three"); // Should be skipped since "four" will arrive before the timout expires. + publishNext(subscriber, 999, "four"); // Should be skipped since onComplete will arrive before the timeout expires. + publishCompleted(subscriber, 1000); // Should be published as soon as the timeout expires. + } + }); + + Action whenDisposed = mock(Action.class); + + Flowable<String> sampled = source + .doOnCancel(whenDisposed) + .debounce(400, TimeUnit.MILLISECONDS, scheduler, e -> { + if ("three".equals(e)) { + throw new TestException("forced"); + } + }); + sampled.subscribe(subscriber); + + InOrder inOrder = inOrder(subscriber); + + scheduler.advanceTimeTo(1000, TimeUnit.MILLISECONDS); + inOrder.verify(subscriber, times(1)).onNext("two"); + inOrder.verify(subscriber, times(1)).onError(any(TestException.class)); + inOrder.verify(subscriber, times(0)).onNext("three"); + inOrder.verify(subscriber, times(0)).onNext("four"); + inOrder.verify(subscriber, times(0)).onComplete(); + inOrder.verifyNoMoreInteractions(); + verify(whenDisposed).run(); + } + + @Test + public void debounceWithOnDroppedCallback() { + Flowable<String> source = Flowable.unsafeCreate(new Publisher<String>() { + @Override + public void subscribe(Subscriber<? super String> subscriber) { + subscriber.onSubscribe(new BooleanSubscription()); + publishNext(subscriber, 100, "one"); // Should be skipped since "two" will arrive before the timeout expires. + publishNext(subscriber, 400, "two"); // Should be published since "three" will arrive after the timeout expires. + publishNext(subscriber, 900, "three"); // Should be skipped since "four" will arrive before the timout expires. + publishNext(subscriber, 999, "four"); // Should be skipped since onComplete will arrive before the timeout expires. + publishCompleted(subscriber, 1000); // Should be published as soon as the timeout expires. + } + }); + + Observer<Object> dropCallbackObserver = TestHelper.mockObserver(); + Flowable<String> sampled = source.debounce(400, TimeUnit.MILLISECONDS, scheduler, dropCallbackObserver::onNext); + sampled.subscribe(subscriber); + + scheduler.advanceTimeTo(0, TimeUnit.MILLISECONDS); + InOrder inOrder = inOrder(subscriber); + InOrder dropCallbackOrder = inOrder(dropCallbackObserver); + + // must go to 800 since it must be 400 after when two is sent, which is at 400 + scheduler.advanceTimeTo(800, TimeUnit.MILLISECONDS); + inOrder.verify(subscriber, times(1)).onNext("two"); + dropCallbackOrder.verify(dropCallbackObserver, times(1)).onNext("one"); + scheduler.advanceTimeTo(1000, TimeUnit.MILLISECONDS); + dropCallbackOrder.verify(dropCallbackObserver, times(1)).onNext("three"); + inOrder.verify(subscriber, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + dropCallbackOrder.verifyNoMoreInteractions(); + } + + @Test + public void debounceWithCompleted() { + Flowable<String> source = Flowable.unsafeCreate(new Publisher<String>() { + @Override + public void subscribe(Subscriber<? super String> subscriber) { + subscriber.onSubscribe(new BooleanSubscription()); + publishNext(subscriber, 100, "one"); // Should be skipped since "two" will arrive before the timeout expires. + publishNext(subscriber, 400, "two"); // Should be published since "three" will arrive after the timeout expires. + publishNext(subscriber, 900, "three"); // Should be skipped since onComplete will arrive before the timeout expires. + publishCompleted(subscriber, 1000); // Should be published as soon as the timeout expires. + } + }); + + Flowable<String> sampled = source.debounce(400, TimeUnit.MILLISECONDS, scheduler); + sampled.subscribe(subscriber); + + scheduler.advanceTimeTo(0, TimeUnit.MILLISECONDS); + InOrder inOrder = inOrder(subscriber); + // must go to 800 since it must be 400 after when two is sent, which is at 400 + scheduler.advanceTimeTo(800, TimeUnit.MILLISECONDS); + inOrder.verify(subscriber, times(1)).onNext("two"); + scheduler.advanceTimeTo(1000, TimeUnit.MILLISECONDS); + inOrder.verify(subscriber, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void debounceNeverEmits() { + Flowable<String> source = Flowable.unsafeCreate(new Publisher<String>() { + @Override + public void subscribe(Subscriber<? super String> subscriber) { + subscriber.onSubscribe(new BooleanSubscription()); + // all should be skipped since they are happening faster than the 200ms timeout + publishNext(subscriber, 100, "a"); // Should be skipped + publishNext(subscriber, 200, "b"); // Should be skipped + publishNext(subscriber, 300, "c"); // Should be skipped + publishNext(subscriber, 400, "d"); // Should be skipped + publishNext(subscriber, 500, "e"); // Should be skipped + publishNext(subscriber, 600, "f"); // Should be skipped + publishNext(subscriber, 700, "g"); // Should be skipped + publishNext(subscriber, 800, "h"); // Should be skipped + publishCompleted(subscriber, 900); // Should be published as soon as the timeout expires. + } + }); + + Flowable<String> sampled = source.debounce(200, TimeUnit.MILLISECONDS, scheduler); + sampled.subscribe(subscriber); + + scheduler.advanceTimeTo(0, TimeUnit.MILLISECONDS); + InOrder inOrder = inOrder(subscriber); + inOrder.verify(subscriber, times(0)).onNext(anyString()); + scheduler.advanceTimeTo(1000, TimeUnit.MILLISECONDS); + inOrder.verify(subscriber, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void debounceWithError() { + Flowable<String> source = Flowable.unsafeCreate(new Publisher<String>() { + @Override + public void subscribe(Subscriber<? super String> subscriber) { + subscriber.onSubscribe(new BooleanSubscription()); + Exception error = new TestException(); + publishNext(subscriber, 100, "one"); // Should be published since "two" will arrive after the timeout expires. + publishNext(subscriber, 600, "two"); // Should be skipped since onError will arrive before the timeout expires. + publishError(subscriber, 700, error); // Should be published as soon as the timeout expires. + } + }); + + Flowable<String> sampled = source.debounce(400, TimeUnit.MILLISECONDS, scheduler); + sampled.subscribe(subscriber); + + scheduler.advanceTimeTo(0, TimeUnit.MILLISECONDS); + InOrder inOrder = inOrder(subscriber); + // 100 + 400 means it triggers at 500 + scheduler.advanceTimeTo(500, TimeUnit.MILLISECONDS); + inOrder.verify(subscriber).onNext("one"); + scheduler.advanceTimeTo(701, TimeUnit.MILLISECONDS); + inOrder.verify(subscriber).onError(any(TestException.class)); + inOrder.verifyNoMoreInteractions(); + } + + private <T> void publishCompleted(final Subscriber<T> subscriber, long delay) { + innerScheduler.schedule(new Runnable() { + @Override + public void run() { + subscriber.onComplete(); + } + }, delay, TimeUnit.MILLISECONDS); + } + + private <T> void publishError(final Subscriber<T> subscriber, long delay, final Exception error) { + innerScheduler.schedule(new Runnable() { + @Override + public void run() { + subscriber.onError(error); + } + }, delay, TimeUnit.MILLISECONDS); + } + + private <T> void publishNext(final Subscriber<T> subscriber, final long delay, final T value) { + innerScheduler.schedule(new Runnable() { + @Override + public void run() { + subscriber.onNext(value); + } + }, delay, TimeUnit.MILLISECONDS); + } + + @Test + public void debounceSelectorNormal1() { + PublishProcessor<Integer> source = PublishProcessor.create(); + final PublishProcessor<Integer> debouncer = PublishProcessor.create(); + Function<Integer, Flowable<Integer>> debounceSel = new Function<Integer, Flowable<Integer>>() { + + @Override + public Flowable<Integer> apply(Integer t1) { + return debouncer; + } + }; + + Subscriber<Object> subscriber = TestHelper.mockSubscriber(); + InOrder inOrder = inOrder(subscriber); + + source.debounce(debounceSel).subscribe(subscriber); + + source.onNext(1); + debouncer.onNext(1); + + source.onNext(2); + source.onNext(3); + source.onNext(4); + + debouncer.onNext(2); + + source.onNext(5); + source.onComplete(); + + inOrder.verify(subscriber).onNext(1); + inOrder.verify(subscriber).onNext(4); + inOrder.verify(subscriber).onNext(5); + inOrder.verify(subscriber).onComplete(); + + verify(subscriber, never()).onError(any(Throwable.class)); + } + + @Test + public void debounceSelectorFuncThrows() { + PublishProcessor<Integer> source = PublishProcessor.create(); + Function<Integer, Flowable<Integer>> debounceSel = new Function<Integer, Flowable<Integer>>() { + + @Override + public Flowable<Integer> apply(Integer t1) { + throw new TestException(); + } + }; + + Subscriber<Object> subscriber = TestHelper.mockSubscriber(); + + source.debounce(debounceSel).subscribe(subscriber); + + source.onNext(1); + + verify(subscriber, never()).onNext(any()); + verify(subscriber, never()).onComplete(); + verify(subscriber).onError(any(TestException.class)); + } + + @Test + public void debounceSelectorFlowableThrows() { + PublishProcessor<Integer> source = PublishProcessor.create(); + Function<Integer, Flowable<Integer>> debounceSel = new Function<Integer, Flowable<Integer>>() { + + @Override + public Flowable<Integer> apply(Integer t1) { + return Flowable.error(new TestException()); + } + }; + + Subscriber<Object> subscriber = TestHelper.mockSubscriber(); + + source.debounce(debounceSel).subscribe(subscriber); + + source.onNext(1); + + verify(subscriber, never()).onNext(any()); + verify(subscriber, never()).onComplete(); + verify(subscriber).onError(any(TestException.class)); + } + + @Test + public void debounceTimedLastIsNotLost() { + PublishProcessor<Integer> source = PublishProcessor.create(); + + Subscriber<Object> subscriber = TestHelper.mockSubscriber(); + + source.debounce(100, TimeUnit.MILLISECONDS, scheduler).subscribe(subscriber); + + source.onNext(1); + source.onComplete(); + + scheduler.advanceTimeBy(1, TimeUnit.SECONDS); + + verify(subscriber).onNext(1); + verify(subscriber).onComplete(); + verify(subscriber, never()).onError(any(Throwable.class)); + } + + @Test + public void debounceSelectorLastIsNotLost() { + PublishProcessor<Integer> source = PublishProcessor.create(); + final PublishProcessor<Integer> debouncer = PublishProcessor.create(); + + Function<Integer, Flowable<Integer>> debounceSel = new Function<Integer, Flowable<Integer>>() { + + @Override + public Flowable<Integer> apply(Integer t1) { + return debouncer; + } + }; + + Subscriber<Object> subscriber = TestHelper.mockSubscriber(); + + source.debounce(debounceSel).subscribe(subscriber); + + source.onNext(1); + source.onComplete(); + + debouncer.onComplete(); + + verify(subscriber).onNext(1); + verify(subscriber).onComplete(); + verify(subscriber, never()).onError(any(Throwable.class)); + } + + @Test + public void debounceWithTimeBackpressure() throws InterruptedException { + TestScheduler scheduler = new TestScheduler(); + TestSubscriberEx<Integer> subscriber = new TestSubscriberEx<>(); + Flowable.merge( + Flowable.just(1), + Flowable.just(2).delay(10, TimeUnit.MILLISECONDS, scheduler) + ).debounce(20, TimeUnit.MILLISECONDS, scheduler).take(1).subscribe(subscriber); + + scheduler.advanceTimeBy(30, TimeUnit.MILLISECONDS); + + subscriber.assertValue(2); + subscriber.assertTerminated(); + subscriber.assertNoErrors(); + } + + @Test + public void debounceDefaultScheduler() throws Exception { + + TestSubscriber<Integer> ts = new TestSubscriber<>(); + + Flowable.range(1, 1000).debounce(1, TimeUnit.SECONDS).subscribe(ts); + + ts.awaitDone(5, TimeUnit.SECONDS); + ts.assertValue(1000); + ts.assertNoErrors(); + ts.assertComplete(); + } + + @Test + public void debounceDefault() throws Exception { + + Flowable.just(1).debounce(1, TimeUnit.SECONDS) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(1); + } + + @Test + public void dispose() { + TestHelper.checkDisposed(PublishProcessor.create().debounce(1, TimeUnit.SECONDS, new TestScheduler())); + + TestHelper.checkDisposed(PublishProcessor.create().debounce(Functions.justFunction(Flowable.never()))); + + Disposable d = new FlowableDebounceTimed.DebounceEmitter<>(1, 1, null); + assertFalse(d.isDisposed()); + + d.dispose(); + + assertTrue(d.isDisposed()); + } + + @Test + public void badSource() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + new Flowable<Integer>() { + @Override + protected void subscribeActual(Subscriber<? super Integer> subscriber) { + subscriber.onSubscribe(new BooleanSubscription()); + subscriber.onComplete(); + subscriber.onNext(1); + subscriber.onError(new TestException()); + subscriber.onComplete(); + } + } + .debounce(1, TimeUnit.SECONDS, new TestScheduler()) + .test() + .assertResult(); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void badSourceSelector() { + TestHelper.checkBadSourceFlowable(new Function<Flowable<Integer>, Object>() { + @Override + public Object apply(Flowable<Integer> f) throws Exception { + return f.debounce(new Function<Integer, Flowable<Long>>() { + @Override + public Flowable<Long> apply(Integer v) throws Exception { + return Flowable.timer(1, TimeUnit.SECONDS); + } + }); + } + }, false, 1, 1, 1); + + TestHelper.checkBadSourceFlowable(new Function<Flowable<Integer>, Object>() { + @Override + public Object apply(final Flowable<Integer> f) throws Exception { + return Flowable.just(1).debounce(new Function<Integer, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Integer v) throws Exception { + return f; + } + }); + } + }, false, 1, 1, 1); + } + + @Test + public void debounceWithEmpty() { + Flowable.just(1).debounce(Functions.justFunction(Flowable.empty())) + .test() + .assertResult(1); + } + + @Test + public void backpressureNoRequest() { + Flowable.just(1) + .debounce(Functions.justFunction(Flowable.timer(1, TimeUnit.MILLISECONDS))) + .test(0L) + .awaitDone(5, TimeUnit.SECONDS) + .assertFailure(MissingBackpressureException.class); + } + + @Test + public void backpressureNoRequestTimed() { + Flowable.just(1) + .debounce(1, TimeUnit.MILLISECONDS) + .test(0L) + .awaitDone(5, TimeUnit.SECONDS) + .assertFailure(MissingBackpressureException.class); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeFlowable(new Function<Flowable<Object>, Flowable<Object>>() { + @Override + public Flowable<Object> apply(Flowable<Object> f) throws Exception { + return f.debounce(Functions.justFunction(Flowable.never())); + } + }); + } + + @Test + public void disposeInOnNext() { + final TestSubscriber<Integer> ts = new TestSubscriber<>(); + + BehaviorProcessor.createDefault(1) + .debounce(new Function<Integer, Flowable<Object>>() { + @Override + public Flowable<Object> apply(Integer o) throws Exception { + ts.cancel(); + return Flowable.never(); + } + }) + .subscribeWith(ts) + .assertEmpty(); + + assertTrue(ts.isCancelled()); + } + + @Test + public void disposedInOnComplete() { + final TestSubscriber<Integer> ts = new TestSubscriber<>(); + + new Flowable<Integer>() { + @Override + protected void subscribeActual(Subscriber<? super Integer> subscriber) { + subscriber.onSubscribe(new BooleanSubscription()); + ts.cancel(); + subscriber.onComplete(); + } + } + .debounce(Functions.justFunction(Flowable.never())) + .subscribeWith(ts) + .assertEmpty(); + } + + @Test + public void emitLate() { + final AtomicReference<Subscriber<? super Integer>> ref = new AtomicReference<>(); + + TestSubscriber<Integer> ts = Flowable.range(1, 2) + .debounce(new Function<Integer, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Integer o) throws Exception { + if (o != 1) { + return Flowable.never(); + } + return new Flowable<Integer>() { + @Override + protected void subscribeActual(Subscriber<? super Integer> subscriber) { + subscriber.onSubscribe(new BooleanSubscription()); + ref.set(subscriber); + } + }; + } + }) + .test(); + + ref.get().onNext(1); + + ts + .assertResult(2); + } + + @Test + public void badRequestReported() { + TestHelper.assertBadRequestReported(Flowable.never().debounce(Functions.justFunction(Flowable.never()))); + } + + @Test + public void timedDoubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeFlowable(new Function<Flowable<Object>, Publisher<Object>>() { + @Override + public Publisher<Object> apply(Flowable<Object> f) + throws Exception { + return f.debounce(1, TimeUnit.SECONDS); + } + }); + } + + @Test + public void timedDisposedIgnoredBySource() { + final TestSubscriber<Integer> ts = new TestSubscriber<>(); + + new Flowable<Integer>() { + @Override + protected void subscribeActual( + org.reactivestreams.Subscriber<? super Integer> s) { + s.onSubscribe(new BooleanSubscription()); + ts.cancel(); + s.onNext(1); + s.onComplete(); + } + } + .debounce(1, TimeUnit.SECONDS) + .subscribe(ts); + } + + @Test + public void timedBadRequest() { + TestHelper.assertBadRequestReported(Flowable.never().debounce(1, TimeUnit.SECONDS)); + } + + @Test + public void timedLateEmit() { + TestSubscriber<Integer> ts = new TestSubscriber<>(); + DebounceTimedSubscriber<Integer> sub = new DebounceTimedSubscriber<>( + ts, 1, TimeUnit.SECONDS, new TestScheduler().createWorker(), null); + + sub.onSubscribe(new BooleanSubscription()); + + DebounceEmitter<Integer> de = new DebounceEmitter<>(1, 50, sub); + de.emit(); + de.emit(); + + ts.assertEmpty(); + } + + @Test + public void timedError() { + Flowable.error(new TestException()) + .debounce(1, TimeUnit.SECONDS) + .test() + .assertFailure(TestException.class); + } + + @Test + public void debounceOnEmpty() { + Flowable.empty().debounce(new Function<Object, Publisher<Object>>() { + @Override + public Publisher<Object> apply(Object o) { + return Flowable.just(new Object()); + } + }).subscribe(); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableDefaultIfEmptyTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableDefaultIfEmptyTest.java new file mode 100644 index 0000000000..fc2c7e15a8 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableDefaultIfEmptyTest.java @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import org.junit.Test; +import org.reactivestreams.Subscriber; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.testsupport.*; + +public class FlowableDefaultIfEmptyTest extends RxJavaTest { + + @Test + public void defaultIfEmpty() { + Flowable<Integer> source = Flowable.just(1, 2, 3); + Flowable<Integer> flowable = source.defaultIfEmpty(10); + + Subscriber<Integer> subscriber = TestHelper.mockSubscriber(); + + flowable.subscribe(subscriber); + + verify(subscriber, never()).onNext(10); + verify(subscriber).onNext(1); + verify(subscriber).onNext(2); + verify(subscriber).onNext(3); + verify(subscriber).onComplete(); + verify(subscriber, never()).onError(any(Throwable.class)); + } + + @Test + public void defaultIfEmptyWithEmpty() { + Flowable<Integer> source = Flowable.empty(); + Flowable<Integer> flowable = source.defaultIfEmpty(10); + + Subscriber<Integer> subscriber = TestHelper.mockSubscriber(); + + flowable.subscribe(subscriber); + + verify(subscriber).onNext(10); + verify(subscriber).onComplete(); + verify(subscriber, never()).onError(any(Throwable.class)); + } + + @Test + public void backpressureEmpty() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(0L); + Flowable.<Integer>empty().defaultIfEmpty(1).subscribe(ts); + ts.assertNoValues(); + ts.assertNotTerminated(); + ts.request(1); + ts.assertValue(1); + ts.assertNoErrors(); + ts.assertComplete(); + } + + @Test + public void backpressureNonEmpty() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(0L); + Flowable.just(1, 2, 3).defaultIfEmpty(1).subscribe(ts); + ts.assertNoValues(); + ts.assertNotTerminated(); + ts.request(2); + ts.assertValues(1, 2); + ts.request(1); + ts.assertValues(1, 2, 3); + ts.assertNoErrors(); + ts.assertComplete(); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableDeferTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableDeferTest.java new file mode 100644 index 0000000000..caec2443fc --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableDeferTest.java @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import org.junit.Test; +import org.reactivestreams.Subscriber; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.Supplier; +import io.reactivex.rxjava3.testsupport.TestHelper; + +@SuppressWarnings("unchecked") +public class FlowableDeferTest extends RxJavaTest { + + @Test + public void defer() throws Throwable { + + Supplier<Flowable<String>> factory = mock(Supplier.class); + + Flowable<String> firstObservable = Flowable.just("one", "two"); + Flowable<String> secondObservable = Flowable.just("three", "four"); + when(factory.get()).thenReturn(firstObservable, secondObservable); + + Flowable<String> deferred = Flowable.defer(factory); + + verifyNoInteractions(factory); + + Subscriber<String> firstSubscriber = TestHelper.mockSubscriber(); + deferred.subscribe(firstSubscriber); + + verify(factory, times(1)).get(); + verify(firstSubscriber, times(1)).onNext("one"); + verify(firstSubscriber, times(1)).onNext("two"); + verify(firstSubscriber, times(0)).onNext("three"); + verify(firstSubscriber, times(0)).onNext("four"); + verify(firstSubscriber, times(1)).onComplete(); + + Subscriber<String> secondSubscriber = TestHelper.mockSubscriber(); + deferred.subscribe(secondSubscriber); + + verify(factory, times(2)).get(); + verify(secondSubscriber, times(0)).onNext("one"); + verify(secondSubscriber, times(0)).onNext("two"); + verify(secondSubscriber, times(1)).onNext("three"); + verify(secondSubscriber, times(1)).onNext("four"); + verify(secondSubscriber, times(1)).onComplete(); + + } + + @Test + public void deferFunctionThrows() throws Throwable { + Supplier<Flowable<String>> factory = mock(Supplier.class); + + when(factory.get()).thenThrow(new TestException()); + + Flowable<String> result = Flowable.defer(factory); + + Subscriber<String> subscriber = TestHelper.mockSubscriber(); + + result.subscribe(subscriber); + + verify(subscriber).onError(any(TestException.class)); + verify(subscriber, never()).onNext(any(String.class)); + verify(subscriber, never()).onComplete(); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableDelaySubscriptionOtherTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableDelaySubscriptionOtherTest.java new file mode 100644 index 0000000000..57a8af4586 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableDelaySubscriptionOtherTest.java @@ -0,0 +1,362 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import java.util.concurrent.*; +import java.util.concurrent.atomic.*; + +import org.junit.*; +import org.reactivestreams.Subscription; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.processors.PublishProcessor; +import io.reactivex.rxjava3.schedulers.Schedulers; +import io.reactivex.rxjava3.subscribers.TestSubscriber; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class FlowableDelaySubscriptionOtherTest extends RxJavaTest { + @Test + public void noPrematureSubscription() { + PublishProcessor<Object> other = PublishProcessor.create(); + + TestSubscriber<Integer> ts = new TestSubscriber<>(); + + final AtomicInteger subscribed = new AtomicInteger(); + + Flowable.just(1) + .doOnSubscribe(new Consumer<Subscription>() { + @Override + public void accept(Subscription s) { + subscribed.getAndIncrement(); + } + }) + .delaySubscription(other) + .subscribe(ts); + + ts.assertNotComplete(); + ts.assertNoErrors(); + ts.assertNoValues(); + + Assert.assertEquals("Premature subscription", 0, subscribed.get()); + + other.onNext(1); + + Assert.assertEquals("No subscription", 1, subscribed.get()); + + ts.assertValue(1); + ts.assertNoErrors(); + ts.assertComplete(); + } + + @Test + public void noMultipleSubscriptions() { + PublishProcessor<Object> other = PublishProcessor.create(); + + TestSubscriber<Integer> ts = new TestSubscriber<>(); + + final AtomicInteger subscribed = new AtomicInteger(); + + Flowable.just(1) + .doOnSubscribe(new Consumer<Subscription>() { + @Override + public void accept(Subscription s) { + subscribed.getAndIncrement(); + } + }) + .delaySubscription(other) + .subscribe(ts); + + ts.assertNotComplete(); + ts.assertNoErrors(); + ts.assertNoValues(); + + Assert.assertEquals("Premature subscription", 0, subscribed.get()); + + other.onNext(1); + other.onNext(2); + + Assert.assertEquals("No subscription", 1, subscribed.get()); + + ts.assertValue(1); + ts.assertNoErrors(); + ts.assertComplete(); + } + + @Test + public void completeTriggersSubscription() { + PublishProcessor<Object> other = PublishProcessor.create(); + + TestSubscriber<Integer> ts = new TestSubscriber<>(); + + final AtomicInteger subscribed = new AtomicInteger(); + + Flowable.just(1) + .doOnSubscribe(new Consumer<Subscription>() { + @Override + public void accept(Subscription s) { + subscribed.getAndIncrement(); + } + }) + .delaySubscription(other) + .subscribe(ts); + + ts.assertNotComplete(); + ts.assertNoErrors(); + ts.assertNoValues(); + + Assert.assertEquals("Premature subscription", 0, subscribed.get()); + + other.onComplete(); + + Assert.assertEquals("No subscription", 1, subscribed.get()); + + ts.assertValue(1); + ts.assertNoErrors(); + ts.assertComplete(); + } + + @Test + public void noPrematureSubscriptionToError() { + PublishProcessor<Object> other = PublishProcessor.create(); + + TestSubscriber<Integer> ts = new TestSubscriber<>(); + + final AtomicInteger subscribed = new AtomicInteger(); + + Flowable.<Integer>error(new TestException()) + .doOnSubscribe(new Consumer<Subscription>() { + @Override + public void accept(Subscription s) { + subscribed.getAndIncrement(); + } + }) + .delaySubscription(other) + .subscribe(ts); + + ts.assertNotComplete(); + ts.assertNoErrors(); + ts.assertNoValues(); + + Assert.assertEquals("Premature subscription", 0, subscribed.get()); + + other.onComplete(); + + Assert.assertEquals("No subscription", 1, subscribed.get()); + + ts.assertNoValues(); + ts.assertNotComplete(); + ts.assertError(TestException.class); + } + + @Test + public void noSubscriptionIfOtherErrors() { + PublishProcessor<Object> other = PublishProcessor.create(); + + TestSubscriber<Integer> ts = new TestSubscriber<>(); + + final AtomicInteger subscribed = new AtomicInteger(); + + Flowable.<Integer>error(new TestException()) + .doOnSubscribe(new Consumer<Subscription>() { + @Override + public void accept(Subscription s) { + subscribed.getAndIncrement(); + } + }) + .delaySubscription(other) + .subscribe(ts); + + ts.assertNotComplete(); + ts.assertNoErrors(); + ts.assertNoValues(); + + Assert.assertEquals("Premature subscription", 0, subscribed.get()); + + other.onError(new TestException()); + + Assert.assertEquals("Premature subscription", 0, subscribed.get()); + + ts.assertNoValues(); + ts.assertNotComplete(); + ts.assertError(TestException.class); + } + + @Test + public void backpressurePassesThrough() { + + PublishProcessor<Object> other = PublishProcessor.create(); + + TestSubscriber<Integer> ts = new TestSubscriber<>(0L); + + final AtomicInteger subscribed = new AtomicInteger(); + + Flowable.just(1, 2, 3, 4, 5) + .doOnSubscribe(new Consumer<Subscription>() { + @Override + public void accept(Subscription s) { + subscribed.getAndIncrement(); + } + }) + .delaySubscription(other) + .subscribe(ts); + + ts.assertNotComplete(); + ts.assertNoErrors(); + ts.assertNoValues(); + + Assert.assertEquals("Premature subscription", 0, subscribed.get()); + + other.onNext(1); + + Assert.assertEquals("No subscription", 1, subscribed.get()); + + Assert.assertFalse("Not unsubscribed from other", other.hasSubscribers()); + + ts.assertNotComplete(); + ts.assertNoErrors(); + ts.assertNoValues(); + + ts.request(1); + ts.assertValue(1); + ts.assertNoErrors(); + ts.assertNotComplete(); + + ts.request(2); + ts.assertValues(1, 2, 3); + ts.assertNoErrors(); + ts.assertNotComplete(); + + ts.request(10); + ts.assertValues(1, 2, 3, 4, 5); + ts.assertNoErrors(); + ts.assertComplete(); + } + + @Test + public void unsubscriptionPropagatesBeforeSubscribe() { + PublishProcessor<Integer> source = PublishProcessor.create(); + PublishProcessor<Integer> other = PublishProcessor.create(); + + TestSubscriber<Integer> ts = new TestSubscriber<>(); + + source.delaySubscription(other).subscribe(ts); + + Assert.assertFalse("source subscribed?", source.hasSubscribers()); + Assert.assertTrue("other not subscribed?", other.hasSubscribers()); + + ts.cancel(); + + Assert.assertFalse("source subscribed?", source.hasSubscribers()); + Assert.assertFalse("other still subscribed?", other.hasSubscribers()); + } + + @Test + public void unsubscriptionPropagatesAfterSubscribe() { + PublishProcessor<Integer> source = PublishProcessor.create(); + PublishProcessor<Integer> other = PublishProcessor.create(); + + TestSubscriber<Integer> ts = new TestSubscriber<>(); + + source.delaySubscription(other).subscribe(ts); + + Assert.assertFalse("source subscribed?", source.hasSubscribers()); + Assert.assertTrue("other not subscribed?", other.hasSubscribers()); + + other.onComplete(); + + Assert.assertTrue("source not subscribed?", source.hasSubscribers()); + Assert.assertFalse("other still subscribed?", other.hasSubscribers()); + + ts.cancel(); + + Assert.assertFalse("source subscribed?", source.hasSubscribers()); + Assert.assertFalse("other still subscribed?", other.hasSubscribers()); + } + + @Test + public void delayAndTakeUntilNeverSubscribeToSource() { + PublishProcessor<Integer> delayUntil = PublishProcessor.create(); + PublishProcessor<Integer> interrupt = PublishProcessor.create(); + final AtomicBoolean subscribed = new AtomicBoolean(false); + + Flowable.just(1) + .doOnSubscribe(new Consumer<Subscription>() { + @Override + public void accept(Subscription s) { + subscribed.set(true); + } + }) + .delaySubscription(delayUntil) + .takeUntil(interrupt) + .subscribe(); + + interrupt.onNext(9000); + delayUntil.onNext(1); + + Assert.assertFalse(subscribed.get()); + } + + @Test + public void badSourceOther() { + TestHelper.checkBadSourceFlowable(new Function<Flowable<Integer>, Object>() { + @Override + public Object apply(Flowable<Integer> f) throws Exception { + return Flowable.just(1).delaySubscription(f); + } + }, false, 1, 1, 1); + } + + @Test + public void afterDelayNoInterrupt() { + ScheduledExecutorService exec = Executors.newSingleThreadScheduledExecutor(); + try { + for (Scheduler s : new Scheduler[] { Schedulers.single(), Schedulers.computation(), Schedulers.newThread(), Schedulers.io(), Schedulers.from(exec) }) { + final TestSubscriber<Boolean> ts = TestSubscriber.create(); + ts.withTag(s.getClass().getSimpleName()); + + Flowable.<Boolean>create(new FlowableOnSubscribe<Boolean>() { + @Override + public void subscribe(FlowableEmitter<Boolean> emitter) throws Exception { + emitter.onNext(Thread.interrupted()); + emitter.onComplete(); + } + }, BackpressureStrategy.MISSING) + .delaySubscription(100, TimeUnit.MILLISECONDS, s) + .subscribe(ts); + + ts.awaitDone(5, TimeUnit.SECONDS); + ts.assertValue(false); + } + } finally { + exec.shutdown(); + } + } + + @Test + public void doubleOnSubscribeMain() { + TestHelper.checkDoubleOnSubscribeFlowable(f -> f.delaySubscription(Flowable.empty())); + } + + @Test + public void doubleOnSubscribeOther() { + TestHelper.checkDoubleOnSubscribeFlowable(f -> PublishProcessor.create().delaySubscription(f)); + } + + @Test + public void badRequest() { + TestHelper.assertBadRequestReported(PublishProcessor.create().delaySubscription(Flowable.empty())); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableDelayTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableDelayTest.java new file mode 100644 index 0000000000..3160f61173 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableDelayTest.java @@ -0,0 +1,1033 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +import java.util.*; +import java.util.concurrent.*; +import java.util.concurrent.atomic.*; + +import org.junit.*; +import org.mockito.InOrder; +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.processors.PublishProcessor; +import io.reactivex.rxjava3.schedulers.*; +import io.reactivex.rxjava3.subscribers.*; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class FlowableDelayTest extends RxJavaTest { + private Subscriber<Long> subscriber; + private Subscriber<Long> subscriber2; + + private TestScheduler scheduler; + + @Before + public void before() { + subscriber = TestHelper.mockSubscriber(); + subscriber2 = TestHelper.mockSubscriber(); + + scheduler = new TestScheduler(); + } + + @Test + public void delay() { + Flowable<Long> source = Flowable.interval(1L, TimeUnit.SECONDS, scheduler).take(3); + Flowable<Long> delayed = source.delay(500L, TimeUnit.MILLISECONDS, scheduler); + delayed.subscribe(subscriber); + + InOrder inOrder = inOrder(subscriber); + scheduler.advanceTimeTo(1499L, TimeUnit.MILLISECONDS); + verify(subscriber, never()).onNext(anyLong()); + verify(subscriber, never()).onComplete(); + verify(subscriber, never()).onError(any(Throwable.class)); + + scheduler.advanceTimeTo(1500L, TimeUnit.MILLISECONDS); + inOrder.verify(subscriber, times(1)).onNext(0L); + inOrder.verify(subscriber, never()).onNext(anyLong()); + verify(subscriber, never()).onComplete(); + verify(subscriber, never()).onError(any(Throwable.class)); + + scheduler.advanceTimeTo(2400L, TimeUnit.MILLISECONDS); + inOrder.verify(subscriber, never()).onNext(anyLong()); + verify(subscriber, never()).onComplete(); + verify(subscriber, never()).onError(any(Throwable.class)); + + scheduler.advanceTimeTo(2500L, TimeUnit.MILLISECONDS); + inOrder.verify(subscriber, times(1)).onNext(1L); + inOrder.verify(subscriber, never()).onNext(anyLong()); + verify(subscriber, never()).onComplete(); + verify(subscriber, never()).onError(any(Throwable.class)); + + scheduler.advanceTimeTo(3400L, TimeUnit.MILLISECONDS); + inOrder.verify(subscriber, never()).onNext(anyLong()); + verify(subscriber, never()).onComplete(); + verify(subscriber, never()).onError(any(Throwable.class)); + + scheduler.advanceTimeTo(3500L, TimeUnit.MILLISECONDS); + inOrder.verify(subscriber, times(1)).onNext(2L); + verify(subscriber, times(1)).onComplete(); + verify(subscriber, never()).onError(any(Throwable.class)); + } + + @Test + public void longDelay() { + Flowable<Long> source = Flowable.interval(1L, TimeUnit.SECONDS, scheduler).take(3); + Flowable<Long> delayed = source.delay(5L, TimeUnit.SECONDS, scheduler); + delayed.subscribe(subscriber); + + InOrder inOrder = inOrder(subscriber); + + scheduler.advanceTimeTo(5999L, TimeUnit.MILLISECONDS); + verify(subscriber, never()).onNext(anyLong()); + verify(subscriber, never()).onComplete(); + verify(subscriber, never()).onError(any(Throwable.class)); + + scheduler.advanceTimeTo(6000L, TimeUnit.MILLISECONDS); + inOrder.verify(subscriber, times(1)).onNext(0L); + scheduler.advanceTimeTo(6999L, TimeUnit.MILLISECONDS); + inOrder.verify(subscriber, never()).onNext(anyLong()); + scheduler.advanceTimeTo(7000L, TimeUnit.MILLISECONDS); + inOrder.verify(subscriber, times(1)).onNext(1L); + scheduler.advanceTimeTo(7999L, TimeUnit.MILLISECONDS); + inOrder.verify(subscriber, never()).onNext(anyLong()); + scheduler.advanceTimeTo(8000L, TimeUnit.MILLISECONDS); + inOrder.verify(subscriber, times(1)).onNext(2L); + inOrder.verify(subscriber, times(1)).onComplete(); + inOrder.verify(subscriber, never()).onNext(anyLong()); + inOrder.verify(subscriber, never()).onComplete(); + verify(subscriber, never()).onError(any(Throwable.class)); + } + + @Test + public void delayWithError() { + Flowable<Long> source = Flowable.interval(1L, TimeUnit.SECONDS, scheduler) + .map(new Function<Long, Long>() { + @Override + public Long apply(Long value) { + if (value == 1L) { + throw new RuntimeException("error!"); + } + return value; + } + }); + Flowable<Long> delayed = source.delay(1L, TimeUnit.SECONDS, scheduler); + delayed.subscribe(subscriber); + + InOrder inOrder = inOrder(subscriber); + + scheduler.advanceTimeTo(1999L, TimeUnit.MILLISECONDS); + verify(subscriber, never()).onNext(anyLong()); + verify(subscriber, never()).onComplete(); + verify(subscriber, never()).onError(any(Throwable.class)); + + scheduler.advanceTimeTo(2000L, TimeUnit.MILLISECONDS); + inOrder.verify(subscriber, times(1)).onError(any(Throwable.class)); + inOrder.verify(subscriber, never()).onNext(anyLong()); + verify(subscriber, never()).onComplete(); + + scheduler.advanceTimeTo(5000L, TimeUnit.MILLISECONDS); + inOrder.verify(subscriber, never()).onNext(anyLong()); + inOrder.verify(subscriber, never()).onError(any(Throwable.class)); + verify(subscriber, never()).onComplete(); + } + + @Test + public void delayWithMultipleSubscriptions() { + Flowable<Long> source = Flowable.interval(1L, TimeUnit.SECONDS, scheduler).take(3); + Flowable<Long> delayed = source.delay(500L, TimeUnit.MILLISECONDS, scheduler); + delayed.subscribe(subscriber); + delayed.subscribe(subscriber2); + + InOrder inOrder = inOrder(subscriber); + InOrder inOrder2 = inOrder(subscriber2); + + scheduler.advanceTimeTo(1499L, TimeUnit.MILLISECONDS); + verify(subscriber, never()).onNext(anyLong()); + verify(subscriber2, never()).onNext(anyLong()); + + scheduler.advanceTimeTo(1500L, TimeUnit.MILLISECONDS); + inOrder.verify(subscriber, times(1)).onNext(0L); + inOrder2.verify(subscriber2, times(1)).onNext(0L); + + scheduler.advanceTimeTo(2499L, TimeUnit.MILLISECONDS); + inOrder.verify(subscriber, never()).onNext(anyLong()); + inOrder2.verify(subscriber2, never()).onNext(anyLong()); + + scheduler.advanceTimeTo(2500L, TimeUnit.MILLISECONDS); + inOrder.verify(subscriber, times(1)).onNext(1L); + inOrder2.verify(subscriber2, times(1)).onNext(1L); + + verify(subscriber, never()).onComplete(); + verify(subscriber2, never()).onComplete(); + + scheduler.advanceTimeTo(3500L, TimeUnit.MILLISECONDS); + inOrder.verify(subscriber, times(1)).onNext(2L); + inOrder2.verify(subscriber2, times(1)).onNext(2L); + inOrder.verify(subscriber, never()).onNext(anyLong()); + inOrder2.verify(subscriber2, never()).onNext(anyLong()); + inOrder.verify(subscriber, times(1)).onComplete(); + inOrder2.verify(subscriber2, times(1)).onComplete(); + + verify(subscriber, never()).onError(any(Throwable.class)); + verify(subscriber2, never()).onError(any(Throwable.class)); + } + + @Test + public void delaySubscription() { + Flowable<Integer> result = Flowable.just(1, 2, 3).delaySubscription(100, TimeUnit.MILLISECONDS, scheduler); + + Subscriber<Object> subscriber = TestHelper.mockSubscriber(); + InOrder inOrder = inOrder(subscriber); + + result.subscribe(subscriber); + + inOrder.verify(subscriber, never()).onNext(any()); + inOrder.verify(subscriber, never()).onComplete(); + + scheduler.advanceTimeBy(100, TimeUnit.MILLISECONDS); + + inOrder.verify(subscriber, times(1)).onNext(1); + inOrder.verify(subscriber, times(1)).onNext(2); + inOrder.verify(subscriber, times(1)).onNext(3); + inOrder.verify(subscriber, times(1)).onComplete(); + + verify(subscriber, never()).onError(any(Throwable.class)); + } + + @Test + public void delaySubscriptionCancelBeforeTime() { + Flowable<Integer> result = Flowable.just(1, 2, 3).delaySubscription(100, TimeUnit.MILLISECONDS, scheduler); + + Subscriber<Object> subscriber = TestHelper.mockSubscriber(); + TestSubscriber<Object> ts = new TestSubscriber<>(subscriber); + + result.subscribe(ts); + ts.cancel(); + scheduler.advanceTimeBy(100, TimeUnit.MILLISECONDS); + + verify(subscriber, never()).onNext(any()); + verify(subscriber, never()).onComplete(); + verify(subscriber, never()).onError(any(Throwable.class)); + } + + @Test + public void delayWithFlowableNormal1() { + PublishProcessor<Integer> source = PublishProcessor.create(); + final List<PublishProcessor<Integer>> delays = new ArrayList<>(); + final int n = 10; + for (int i = 0; i < n; i++) { + PublishProcessor<Integer> delay = PublishProcessor.create(); + delays.add(delay); + } + + Function<Integer, Flowable<Integer>> delayFunc = new Function<Integer, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Integer t1) { + return delays.get(t1); + } + }; + + Subscriber<Object> subscriber = TestHelper.mockSubscriber(); + InOrder inOrder = inOrder(subscriber); + + source.delay(delayFunc).subscribe(subscriber); + + for (int i = 0; i < n; i++) { + source.onNext(i); + delays.get(i).onNext(i); + inOrder.verify(subscriber).onNext(i); + } + source.onComplete(); + + inOrder.verify(subscriber).onComplete(); + inOrder.verifyNoMoreInteractions(); + + verify(subscriber, never()).onError(any(Throwable.class)); + } + + @Test + public void delayWithFlowableSingleSend1() { + PublishProcessor<Integer> source = PublishProcessor.create(); + final PublishProcessor<Integer> delay = PublishProcessor.create(); + + Function<Integer, Flowable<Integer>> delayFunc = new Function<Integer, Flowable<Integer>>() { + + @Override + public Flowable<Integer> apply(Integer t1) { + return delay; + } + }; + Subscriber<Object> subscriber = TestHelper.mockSubscriber(); + InOrder inOrder = inOrder(subscriber); + + source.delay(delayFunc).subscribe(subscriber); + + source.onNext(1); + delay.onNext(1); + delay.onNext(2); + + inOrder.verify(subscriber).onNext(1); + inOrder.verifyNoMoreInteractions(); + verify(subscriber, never()).onError(any(Throwable.class)); + } + + @Test + public void delayWithFlowableSourceThrows() { + PublishProcessor<Integer> source = PublishProcessor.create(); + final PublishProcessor<Integer> delay = PublishProcessor.create(); + + Function<Integer, Flowable<Integer>> delayFunc = new Function<Integer, Flowable<Integer>>() { + + @Override + public Flowable<Integer> apply(Integer t1) { + return delay; + } + }; + Subscriber<Object> subscriber = TestHelper.mockSubscriber(); + InOrder inOrder = inOrder(subscriber); + + source.delay(delayFunc).subscribe(subscriber); + source.onNext(1); + source.onError(new TestException()); + delay.onNext(1); + + inOrder.verify(subscriber).onError(any(TestException.class)); + inOrder.verifyNoMoreInteractions(); + verify(subscriber, never()).onNext(any()); + verify(subscriber, never()).onComplete(); + } + + @Test + public void delayWithFlowableDelayFunctionThrows() { + PublishProcessor<Integer> source = PublishProcessor.create(); + + Function<Integer, Flowable<Integer>> delayFunc = new Function<Integer, Flowable<Integer>>() { + + @Override + public Flowable<Integer> apply(Integer t1) { + throw new TestException(); + } + }; + Subscriber<Object> subscriber = TestHelper.mockSubscriber(); + InOrder inOrder = inOrder(subscriber); + + source.delay(delayFunc).subscribe(subscriber); + source.onNext(1); + + inOrder.verify(subscriber).onError(any(TestException.class)); + inOrder.verifyNoMoreInteractions(); + verify(subscriber, never()).onNext(any()); + verify(subscriber, never()).onComplete(); + } + + @Test + public void delayWithFlowableDelayThrows() { + PublishProcessor<Integer> source = PublishProcessor.create(); + final PublishProcessor<Integer> delay = PublishProcessor.create(); + + Function<Integer, Flowable<Integer>> delayFunc = new Function<Integer, Flowable<Integer>>() { + + @Override + public Flowable<Integer> apply(Integer t1) { + return delay; + } + }; + Subscriber<Object> subscriber = TestHelper.mockSubscriber(); + InOrder inOrder = inOrder(subscriber); + + source.delay(delayFunc).subscribe(subscriber); + source.onNext(1); + delay.onError(new TestException()); + + inOrder.verify(subscriber).onError(any(TestException.class)); + inOrder.verifyNoMoreInteractions(); + verify(subscriber, never()).onNext(any()); + verify(subscriber, never()).onComplete(); + } + + @Test + public void delayWithFlowableSubscriptionNormal() { + PublishProcessor<Integer> source = PublishProcessor.create(); + final PublishProcessor<Integer> delay = PublishProcessor.create(); + Function<Integer, Flowable<Integer>> delayFunc = new Function<Integer, Flowable<Integer>>() { + + @Override + public Flowable<Integer> apply(Integer t1) { + return delay; + } + }; + + Subscriber<Object> subscriber = TestHelper.mockSubscriber(); + InOrder inOrder = inOrder(subscriber); + + source.delay(delay, delayFunc).subscribe(subscriber); + + source.onNext(1); + delay.onNext(1); + + source.onNext(2); + delay.onNext(2); + + inOrder.verify(subscriber).onNext(2); + inOrder.verifyNoMoreInteractions(); + verify(subscriber, never()).onError(any(Throwable.class)); + verify(subscriber, never()).onComplete(); + } + + @Test + public void delayWithFlowableSubscriptionFunctionThrows() { + PublishProcessor<Integer> source = PublishProcessor.create(); + final PublishProcessor<Integer> delay = PublishProcessor.create(); + Supplier<Flowable<Integer>> subFunc = new Supplier<Flowable<Integer>>() { + @Override + public Flowable<Integer> get() { + throw new TestException(); + } + }; + Function<Integer, Flowable<Integer>> delayFunc = new Function<Integer, Flowable<Integer>>() { + + @Override + public Flowable<Integer> apply(Integer t1) { + return delay; + } + }; + + Subscriber<Object> subscriber = TestHelper.mockSubscriber(); + InOrder inOrder = inOrder(subscriber); + + source.delay(Flowable.defer(subFunc), delayFunc).subscribe(subscriber); + + source.onNext(1); + delay.onNext(1); + + source.onNext(2); + + inOrder.verify(subscriber).onError(any(TestException.class)); + inOrder.verifyNoMoreInteractions(); + verify(subscriber, never()).onNext(any()); + verify(subscriber, never()).onComplete(); + } + + @Test + public void delayWithFlowableSubscriptionThrows() { + PublishProcessor<Integer> source = PublishProcessor.create(); + final PublishProcessor<Integer> delay = PublishProcessor.create(); + Supplier<Flowable<Integer>> subFunc = new Supplier<Flowable<Integer>>() { + @Override + public Flowable<Integer> get() { + return delay; + } + }; + Function<Integer, Flowable<Integer>> delayFunc = new Function<Integer, Flowable<Integer>>() { + + @Override + public Flowable<Integer> apply(Integer t1) { + return delay; + } + }; + + Subscriber<Object> subscriber = TestHelper.mockSubscriber(); + InOrder inOrder = inOrder(subscriber); + + source.delay(Flowable.defer(subFunc), delayFunc).subscribe(subscriber); + + source.onNext(1); + delay.onError(new TestException()); + + source.onNext(2); + + inOrder.verify(subscriber).onError(any(TestException.class)); + inOrder.verifyNoMoreInteractions(); + verify(subscriber, never()).onNext(any()); + verify(subscriber, never()).onComplete(); + } + + @Test + public void delayWithFlowableEmptyDelayer() { + PublishProcessor<Integer> source = PublishProcessor.create(); + + Function<Integer, Flowable<Integer>> delayFunc = new Function<Integer, Flowable<Integer>>() { + + @Override + public Flowable<Integer> apply(Integer t1) { + return Flowable.empty(); + } + }; + Subscriber<Object> subscriber = TestHelper.mockSubscriber(); + InOrder inOrder = inOrder(subscriber); + + source.delay(delayFunc).subscribe(subscriber); + + source.onNext(1); + source.onComplete(); + + inOrder.verify(subscriber).onNext(1); + inOrder.verify(subscriber).onComplete(); + inOrder.verifyNoMoreInteractions(); + verify(subscriber, never()).onError(any(Throwable.class)); + } + + @Test + public void delayWithFlowableSubscriptionRunCompletion() { + PublishProcessor<Integer> source = PublishProcessor.create(); + final PublishProcessor<Integer> sdelay = PublishProcessor.create(); + final PublishProcessor<Integer> delay = PublishProcessor.create(); + Supplier<Flowable<Integer>> subFunc = new Supplier<Flowable<Integer>>() { + @Override + public Flowable<Integer> get() { + return sdelay; + } + }; + Function<Integer, Flowable<Integer>> delayFunc = new Function<Integer, Flowable<Integer>>() { + + @Override + public Flowable<Integer> apply(Integer t1) { + return delay; + } + }; + + Subscriber<Object> subscriber = TestHelper.mockSubscriber(); + InOrder inOrder = inOrder(subscriber); + + source.delay(Flowable.defer(subFunc), delayFunc).subscribe(subscriber); + + source.onNext(1); + sdelay.onComplete(); + + source.onNext(2); + delay.onNext(2); + + inOrder.verify(subscriber).onNext(2); + inOrder.verifyNoMoreInteractions(); + verify(subscriber, never()).onError(any(Throwable.class)); + verify(subscriber, never()).onComplete(); + } + + @Test + public void delayWithFlowableAsTimed() { + Flowable<Long> source = Flowable.interval(1L, TimeUnit.SECONDS, scheduler).take(3); + + final Flowable<Long> delayer = Flowable.timer(500L, TimeUnit.MILLISECONDS, scheduler); + + Function<Long, Flowable<Long>> delayFunc = new Function<Long, Flowable<Long>>() { + @Override + public Flowable<Long> apply(Long t1) { + return delayer; + } + }; + + Flowable<Long> delayed = source.delay(delayFunc); + delayed.subscribe(subscriber); + + InOrder inOrder = inOrder(subscriber); + scheduler.advanceTimeTo(1499L, TimeUnit.MILLISECONDS); + verify(subscriber, never()).onNext(anyLong()); + verify(subscriber, never()).onComplete(); + verify(subscriber, never()).onError(any(Throwable.class)); + + scheduler.advanceTimeTo(1500L, TimeUnit.MILLISECONDS); + inOrder.verify(subscriber, times(1)).onNext(0L); + inOrder.verify(subscriber, never()).onNext(anyLong()); + verify(subscriber, never()).onComplete(); + verify(subscriber, never()).onError(any(Throwable.class)); + + scheduler.advanceTimeTo(2400L, TimeUnit.MILLISECONDS); + inOrder.verify(subscriber, never()).onNext(anyLong()); + verify(subscriber, never()).onComplete(); + verify(subscriber, never()).onError(any(Throwable.class)); + + scheduler.advanceTimeTo(2500L, TimeUnit.MILLISECONDS); + inOrder.verify(subscriber, times(1)).onNext(1L); + inOrder.verify(subscriber, never()).onNext(anyLong()); + verify(subscriber, never()).onComplete(); + verify(subscriber, never()).onError(any(Throwable.class)); + + scheduler.advanceTimeTo(3400L, TimeUnit.MILLISECONDS); + inOrder.verify(subscriber, never()).onNext(anyLong()); + verify(subscriber, never()).onComplete(); + verify(subscriber, never()).onError(any(Throwable.class)); + + scheduler.advanceTimeTo(3500L, TimeUnit.MILLISECONDS); + inOrder.verify(subscriber, times(1)).onNext(2L); + verify(subscriber, times(1)).onComplete(); + verify(subscriber, never()).onError(any(Throwable.class)); + } + + @Test + public void delayWithFlowableReorder() { + int n = 3; + + PublishProcessor<Integer> source = PublishProcessor.create(); + final List<PublishProcessor<Integer>> subjects = new ArrayList<>(); + for (int i = 0; i < n; i++) { + subjects.add(PublishProcessor.<Integer> create()); + } + + Flowable<Integer> result = source.delay(new Function<Integer, Flowable<Integer>>() { + + @Override + public Flowable<Integer> apply(Integer t1) { + return subjects.get(t1); + } + }); + + Subscriber<Object> subscriber = TestHelper.mockSubscriber(); + InOrder inOrder = inOrder(subscriber); + + result.subscribe(subscriber); + + for (int i = 0; i < n; i++) { + source.onNext(i); + } + source.onComplete(); + + inOrder.verify(subscriber, never()).onNext(anyInt()); + inOrder.verify(subscriber, never()).onComplete(); + + for (int i = n - 1; i >= 0; i--) { + subjects.get(i).onComplete(); + inOrder.verify(subscriber).onNext(i); + } + + inOrder.verify(subscriber).onComplete(); + + verify(subscriber, never()).onError(any(Throwable.class)); + } + + @Test + public void delayEmitsEverything() { + Flowable<Integer> source = Flowable.range(1, 5); + Flowable<Integer> delayed = source.delay(500L, TimeUnit.MILLISECONDS, scheduler); + delayed = delayed.doOnEach(new Consumer<Notification<Integer>>() { + + @Override + public void accept(Notification<Integer> t1) { + System.out.println(t1); + } + + }); + TestSubscriber<Integer> ts = new TestSubscriber<>(); + delayed.subscribe(ts); + // all will be delivered after 500ms since range does not delay between them + scheduler.advanceTimeBy(500L, TimeUnit.MILLISECONDS); + ts.assertValues(1, 2, 3, 4, 5); + } + + @Test + public void backpressureWithTimedDelay() { + TestSubscriber<Integer> ts = new TestSubscriber<>(); + Flowable.range(1, Flowable.bufferSize() * 2) + .delay(100, TimeUnit.MILLISECONDS) + .observeOn(Schedulers.computation()) + .map(new Function<Integer, Integer>() { + + int c; + + @Override + public Integer apply(Integer t) { + if (c++ <= 0) { + try { + Thread.sleep(500); + } catch (InterruptedException e) { + } + } + return t; + } + + }).subscribe(ts); + + ts.awaitDone(5, TimeUnit.SECONDS); + ts.assertNoErrors(); + assertEquals(Flowable.bufferSize() * 2, ts.values().size()); + } + + @Test + public void backpressureWithSubscriptionTimedDelay() { + TestSubscriber<Integer> ts = new TestSubscriber<>(); + Flowable.range(1, Flowable.bufferSize() * 2) + .delaySubscription(100, TimeUnit.MILLISECONDS) + .delay(100, TimeUnit.MILLISECONDS) + .observeOn(Schedulers.computation()) + .map(new Function<Integer, Integer>() { + + int c; + + @Override + public Integer apply(Integer t) { + if (c++ <= 0) { + try { + Thread.sleep(500); + } catch (InterruptedException e) { + } + } + return t; + } + + }).subscribe(ts); + + ts.awaitDone(5, TimeUnit.SECONDS); + ts.assertNoErrors(); + assertEquals(Flowable.bufferSize() * 2, ts.values().size()); + } + + @Test + public void backpressureWithSelectorDelay() { + TestSubscriber<Integer> ts = new TestSubscriber<>(); + Flowable.range(1, Flowable.bufferSize() * 2) + .delay(new Function<Integer, Flowable<Long>>() { + + @Override + public Flowable<Long> apply(Integer i) { + return Flowable.timer(100, TimeUnit.MILLISECONDS); + } + + }) + .observeOn(Schedulers.computation()) + .map(new Function<Integer, Integer>() { + + int c; + + @Override + public Integer apply(Integer t) { + if (c++ <= 0) { + try { + Thread.sleep(500); + } catch (InterruptedException e) { + } + } + return t; + } + + }).subscribe(ts); + + ts.awaitDone(5, TimeUnit.SECONDS); + ts.assertNoErrors(); + assertEquals(Flowable.bufferSize() * 2, ts.values().size()); + } + + @Test + public void backpressureWithSelectorDelayAndSubscriptionDelay() { + TestSubscriber<Integer> ts = new TestSubscriber<>(); + Flowable.range(1, Flowable.bufferSize() * 2) + .delay(Flowable.defer(new Supplier<Flowable<Long>>() { + + @Override + public Flowable<Long> get() { + return Flowable.timer(500, TimeUnit.MILLISECONDS); + } + }), new Function<Integer, Flowable<Long>>() { + + @Override + public Flowable<Long> apply(Integer i) { + return Flowable.timer(100, TimeUnit.MILLISECONDS); + } + + }) + .observeOn(Schedulers.computation()) + .map(new Function<Integer, Integer>() { + + int c; + + @Override + public Integer apply(Integer t) { + if (c++ <= 0) { + try { + Thread.sleep(500); + } catch (InterruptedException e) { + } + } + return t; + } + + }).subscribe(ts); + + ts.awaitDone(5, TimeUnit.SECONDS); + ts.assertNoErrors(); + assertEquals(Flowable.bufferSize() * 2, ts.values().size()); + } + + @Test + public void errorRunsBeforeOnNext() { + TestScheduler test = new TestScheduler(); + + PublishProcessor<Integer> pp = PublishProcessor.create(); + + TestSubscriber<Integer> ts = new TestSubscriber<>(); + + pp.delay(1, TimeUnit.SECONDS, test).subscribe(ts); + + pp.onNext(1); + + test.advanceTimeBy(500, TimeUnit.MILLISECONDS); + + pp.onError(new TestException()); + + test.advanceTimeBy(1, TimeUnit.SECONDS); + + ts.assertNoValues(); + ts.assertError(TestException.class); + ts.assertNotComplete(); + } + + @Test + public void delaySupplierSimple() { + final PublishProcessor<Integer> pp = PublishProcessor.create(); + + Flowable<Integer> source = Flowable.range(1, 5); + + TestSubscriber<Integer> ts = new TestSubscriber<>(); + + source.delaySubscription(Flowable.defer(new Supplier<Publisher<Integer>>() { + @Override + public Publisher<Integer> get() { + return pp; + } + })).subscribe(ts); + + ts.assertNoValues(); + ts.assertNoErrors(); + ts.assertNotComplete(); + + pp.onNext(1); + + ts.assertValues(1, 2, 3, 4, 5); + ts.assertComplete(); + ts.assertNoErrors(); + } + + @Test + public void delaySupplierCompletes() { + final PublishProcessor<Integer> pp = PublishProcessor.create(); + + Flowable<Integer> source = Flowable.range(1, 5); + + TestSubscriber<Integer> ts = new TestSubscriber<>(); + + source.delaySubscription(Flowable.defer(new Supplier<Publisher<Integer>>() { + @Override + public Publisher<Integer> get() { + return pp; + } + })).subscribe(ts); + + ts.assertNoValues(); + ts.assertNoErrors(); + ts.assertNotComplete(); + + // FIXME should this complete the source instead of consuming it? + pp.onComplete(); + + ts.assertValues(1, 2, 3, 4, 5); + ts.assertComplete(); + ts.assertNoErrors(); + } + + @Test + public void delaySupplierErrors() { + final PublishProcessor<Integer> pp = PublishProcessor.create(); + + Flowable<Integer> source = Flowable.range(1, 5); + + TestSubscriber<Integer> ts = new TestSubscriber<>(); + + source.delaySubscription(Flowable.defer(new Supplier<Publisher<Integer>>() { + @Override + public Publisher<Integer> get() { + return pp; + } + })).subscribe(ts); + + ts.assertNoValues(); + ts.assertNoErrors(); + ts.assertNotComplete(); + + pp.onError(new TestException()); + + ts.assertNoValues(); + ts.assertNotComplete(); + ts.assertError(TestException.class); + } + + @Test + public void delayAndTakeUntilNeverSubscribeToSource() { + PublishProcessor<Integer> delayUntil = PublishProcessor.create(); + PublishProcessor<Integer> interrupt = PublishProcessor.create(); + final AtomicBoolean subscribed = new AtomicBoolean(false); + + Flowable.just(1) + .doOnSubscribe(new Consumer<Object>() { + @Override + public void accept(Object o) { + subscribed.set(true); + } + }) + .delaySubscription(delayUntil) + .takeUntil(interrupt) + .subscribe(); + + interrupt.onNext(9000); + delayUntil.onNext(1); + + Assert.assertFalse(subscribed.get()); + } + + @Test + public void delayWithTimeDelayError() throws Exception { + Flowable.just(1).concatWith(Flowable.<Integer>error(new TestException())) + .delay(100, TimeUnit.MILLISECONDS, true) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertFailure(TestException.class, 1); + } + + @Test + public void delaySubscriptionDisposeBeforeTime() { + Flowable<Integer> result = Flowable.just(1, 2, 3).delaySubscription(100, TimeUnit.MILLISECONDS, scheduler); + + Subscriber<Object> subscriber = TestHelper.mockSubscriber(); + TestSubscriber<Object> ts = new TestSubscriber<>(subscriber); + + result.subscribe(ts); + ts.cancel(); + scheduler.advanceTimeBy(100, TimeUnit.MILLISECONDS); + + verify(subscriber, never()).onNext(any()); + verify(subscriber, never()).onComplete(); + verify(subscriber, never()).onError(any(Throwable.class)); + } + + @Test + public void onErrorCalledOnScheduler() throws Exception { + final CountDownLatch latch = new CountDownLatch(1); + final AtomicReference<Thread> thread = new AtomicReference<>(); + + Flowable.<String>error(new Exception()) + .delay(0, TimeUnit.MILLISECONDS, Schedulers.newThread()) + .doOnError(new Consumer<Throwable>() { + @Override + public void accept(Throwable throwable) throws Exception { + thread.set(Thread.currentThread()); + latch.countDown(); + } + }) + .onErrorResumeWith(Flowable.<String>empty()) + .subscribe(); + + latch.await(); + + assertNotEquals(Thread.currentThread(), thread.get()); + } + + @Test + public void dispose() { + TestHelper.checkDisposed(PublishProcessor.create().delay(1, TimeUnit.SECONDS)); + + TestHelper.checkDisposed(PublishProcessor.create().delay(Functions.justFunction(Flowable.never()))); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeFlowable(new Function<Flowable<Object>, Flowable<Object>>() { + @Override + public Flowable<Object> apply(Flowable<Object> f) throws Exception { + return f.delay(1, TimeUnit.SECONDS); + } + }); + + TestHelper.checkDoubleOnSubscribeFlowable(new Function<Flowable<Object>, Flowable<Object>>() { + @Override + public Flowable<Object> apply(Flowable<Object> f) throws Exception { + return f.delay(Functions.justFunction(Flowable.never())); + } + }); + } + + @Test + public void onCompleteFinal() { + TestScheduler scheduler = new TestScheduler(); + + Flowable.empty() + .delay(1, TimeUnit.MILLISECONDS, scheduler) + .subscribe(new DisposableSubscriber<Object>() { + @Override + public void onNext(Object value) { + } + + @Override + public void onError(Throwable e) { + } + + @Override + public void onComplete() { + throw new TestException(); + } + }); + + try { + scheduler.advanceTimeBy(1, TimeUnit.SECONDS); + fail("Should have thrown"); + } catch (TestException ex) { + // expected + } + } + + @Test + public void onErrorFinal() { + TestScheduler scheduler = new TestScheduler(); + + Flowable.error(new TestException()) + .delay(1, TimeUnit.MILLISECONDS, scheduler) + .subscribe(new DisposableSubscriber<Object>() { + @Override + public void onNext(Object value) { + } + + @Override + public void onError(Throwable e) { + throw new TestException(); + } + + @Override + public void onComplete() { + } + }); + + try { + scheduler.advanceTimeBy(1, TimeUnit.SECONDS); + fail("Should have thrown"); + } catch (TestException ex) { + // expected + } + } + + @Test + public void itemDelayReturnsNull() { + Flowable.just(1).delay(new Function<Integer, Publisher<Object>>() { + @Override + public Publisher<Object> apply(Integer t) throws Exception { + return null; + } + }) + .to(TestHelper.<Integer>testConsumer()) + .assertFailureAndMessage(NullPointerException.class, "The itemDelay returned a null Publisher"); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableDematerializeTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableDematerializeTest.java new file mode 100644 index 0000000000..89a1454614 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableDematerializeTest.java @@ -0,0 +1,255 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import java.util.List; + +import org.junit.Test; +import org.reactivestreams.Subscriber; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.internal.subscriptions.BooleanSubscription; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.testsupport.*; + +public class FlowableDematerializeTest extends RxJavaTest { + + @Test + public void simpleSelector() { + Flowable<Notification<Integer>> notifications = Flowable.just(1, 2).materialize(); + Flowable<Integer> dematerialize = notifications.dematerialize(Functions.<Notification<Integer>>identity()); + + Subscriber<Integer> subscriber = TestHelper.mockSubscriber(); + + dematerialize.subscribe(subscriber); + + verify(subscriber, times(1)).onNext(1); + verify(subscriber, times(1)).onNext(2); + verify(subscriber, times(1)).onComplete(); + verify(subscriber, never()).onError(any(Throwable.class)); + } + + @Test + public void selectorCrash() { + Flowable.just(1, 2) + .materialize() + .dematerialize(new Function<Notification<Integer>, Notification<Object>>() { + @Override + public Notification<Object> apply(Notification<Integer> v) throws Exception { + throw new TestException(); + } + }) + .test() + .assertFailure(TestException.class); + } + + @Test + public void selectorNull() { + Flowable.just(1, 2) + .materialize() + .dematerialize(new Function<Notification<Integer>, Notification<Object>>() { + @Override + public Notification<Object> apply(Notification<Integer> v) throws Exception { + return null; + } + }) + .test() + .assertFailure(NullPointerException.class); + } + + @Test + public void dematerialize1() { + Flowable<Notification<Integer>> notifications = Flowable.just(1, 2).materialize(); + Flowable<Integer> dematerialize = notifications.dematerialize(Functions.<Notification<Integer>>identity()); + + Subscriber<Integer> subscriber = TestHelper.mockSubscriber(); + + dematerialize.subscribe(subscriber); + + verify(subscriber, times(1)).onNext(1); + verify(subscriber, times(1)).onNext(2); + verify(subscriber, times(1)).onComplete(); + verify(subscriber, never()).onError(any(Throwable.class)); + } + + @Test + public void dematerialize2() { + Throwable exception = new Throwable("test"); + Flowable<Integer> flowable = Flowable.error(exception); + Flowable<Integer> dematerialize = flowable.materialize().dematerialize(Functions.<Notification<Integer>>identity()); + + Subscriber<Integer> subscriber = TestHelper.mockSubscriber(); + + dematerialize.subscribe(subscriber); + + verify(subscriber, times(1)).onError(exception); + verify(subscriber, times(0)).onComplete(); + verify(subscriber, times(0)).onNext(any(Integer.class)); + } + + @Test + public void dematerialize3() { + Exception exception = new Exception("test"); + Flowable<Integer> flowable = Flowable.error(exception); + Flowable<Integer> dematerialize = flowable.materialize().dematerialize(Functions.<Notification<Integer>>identity()); + + Subscriber<Integer> subscriber = TestHelper.mockSubscriber(); + + dematerialize.subscribe(subscriber); + + verify(subscriber, times(1)).onError(exception); + verify(subscriber, times(0)).onComplete(); + verify(subscriber, times(0)).onNext(any(Integer.class)); + } + + @Test + public void errorPassThru() { + Exception exception = new Exception("test"); + Flowable<Notification<Integer>> flowable = Flowable.error(exception); + Flowable<Integer> dematerialize = flowable.dematerialize(Functions.<Notification<Integer>>identity()); + + Subscriber<Integer> subscriber = TestHelper.mockSubscriber(); + + dematerialize.subscribe(subscriber); + + verify(subscriber, times(1)).onError(exception); + verify(subscriber, times(0)).onComplete(); + verify(subscriber, times(0)).onNext(any(Integer.class)); + } + + @Test + public void completePassThru() { + Flowable<Notification<Integer>> flowable = Flowable.empty(); + Flowable<Integer> dematerialize = flowable.dematerialize(Functions.<Notification<Integer>>identity()); + + Subscriber<Integer> subscriber = TestHelper.mockSubscriber(); + + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(subscriber); + dematerialize.subscribe(ts); + + System.out.println(ts.errors()); + + verify(subscriber, never()).onError(any(Throwable.class)); + verify(subscriber, times(1)).onComplete(); + verify(subscriber, times(0)).onNext(any(Integer.class)); + } + + @Test + public void honorsContractWhenCompleted() { + Flowable<Integer> source = Flowable.just(1); + + Flowable<Integer> result = source.materialize().dematerialize(Functions.<Notification<Integer>>identity()); + + Subscriber<Integer> subscriber = TestHelper.mockSubscriber(); + + result.subscribe(subscriber); + + verify(subscriber).onNext(1); + verify(subscriber).onComplete(); + verify(subscriber, never()).onError(any(Throwable.class)); + } + + @Test + public void honorsContractWhenThrows() { + Flowable<Integer> source = Flowable.error(new TestException()); + + Flowable<Integer> result = source.materialize().dematerialize(Functions.<Notification<Integer>>identity()); + + Subscriber<Integer> subscriber = TestHelper.mockSubscriber(); + + result.subscribe(subscriber); + + verify(subscriber, never()).onNext(any(Integer.class)); + verify(subscriber, never()).onComplete(); + verify(subscriber).onError(any(TestException.class)); + } + + @Test + public void dispose() { + TestHelper.checkDisposed(Flowable.just(Notification.createOnComplete()) + .dematerialize(Functions.<Notification<Object>>identity())); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeFlowable(new Function<Flowable<Notification<Object>>, Flowable<Object>>() { + @Override + public Flowable<Object> apply(Flowable<Notification<Object>> f) throws Exception { + return f.dematerialize(Functions.<Notification<Object>>identity()); + } + }); + } + + @Test + public void eventsAfterDematerializedTerminal() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + new Flowable<Notification<Object>>() { + @Override + protected void subscribeActual(Subscriber<? super Notification<Object>> subscriber) { + subscriber.onSubscribe(new BooleanSubscription()); + subscriber.onNext(Notification.createOnComplete()); + subscriber.onNext(Notification.<Object>createOnNext(1)); + subscriber.onNext(Notification.createOnError(new TestException("First"))); + subscriber.onError(new TestException("Second")); + } + } + .dematerialize(Functions.<Notification<Object>>identity()) + .test() + .assertResult(); + + TestHelper.assertUndeliverable(errors, 0, TestException.class, "First"); + TestHelper.assertUndeliverable(errors, 1, TestException.class, "Second"); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void notificationInstanceAfterDispose() { + new Flowable<Notification<Object>>() { + @Override + protected void subscribeActual(Subscriber<? super Notification<Object>> subscriber) { + subscriber.onSubscribe(new BooleanSubscription()); + subscriber.onNext(Notification.createOnComplete()); + subscriber.onNext(Notification.<Object>createOnNext(1)); + } + } + .dematerialize(Functions.<Notification<Object>>identity()) + .test() + .assertResult(); + } + + @Test + @SuppressWarnings("unchecked") + public void nonNotificationInstanceAfterDispose() { + new Flowable<Object>() { + @Override + protected void subscribeActual(Subscriber<? super Object> subscriber) { + subscriber.onSubscribe(new BooleanSubscription()); + subscriber.onNext(Notification.createOnComplete()); + subscriber.onNext(1); + } + } + .dematerialize(v -> (Notification<Object>)v) + .test() + .assertResult(); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableDetachTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableDetachTest.java new file mode 100644 index 0000000000..23d895ca08 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableDetachTest.java @@ -0,0 +1,174 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import java.lang.ref.WeakReference; +import java.util.concurrent.atomic.AtomicReference; + +import org.junit.*; +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.subscribers.TestSubscriber; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class FlowableDetachTest extends RxJavaTest { + + Object o; + + @Test + public void just() throws Exception { + o = new Object(); + + WeakReference<Object> wr = new WeakReference<>(o); + + TestSubscriber<Object> ts = new TestSubscriber<>(); + + Flowable.just(o).count().toFlowable().onTerminateDetach().subscribe(ts); + + ts.assertValue(1L); + ts.assertComplete(); + ts.assertNoErrors(); + + o = null; + + System.gc(); + Thread.sleep(200); + + Assert.assertNull("Object retained!", wr.get()); + + } + + @Test + public void error() { + TestSubscriber<Object> ts = new TestSubscriber<>(); + + Flowable.error(new TestException()).onTerminateDetach().subscribe(ts); + + ts.assertNoValues(); + ts.assertError(TestException.class); + ts.assertNotComplete(); + } + + @Test + public void empty() { + TestSubscriber<Object> ts = new TestSubscriber<>(); + + Flowable.empty().onTerminateDetach().subscribe(ts); + + ts.assertNoValues(); + ts.assertNoErrors(); + ts.assertComplete(); + } + + @Test + public void range() { + TestSubscriber<Object> ts = new TestSubscriber<>(); + + Flowable.range(1, 1000).onTerminateDetach().subscribe(ts); + + ts.assertValueCount(1000); + ts.assertNoErrors(); + ts.assertComplete(); + } + + @Test + public void backpressured() throws Exception { + o = new Object(); + + WeakReference<Object> wr = new WeakReference<>(o); + + TestSubscriber<Object> ts = new TestSubscriber<>(0L); + + Flowable.just(o).count().toFlowable().onTerminateDetach().subscribe(ts); + + ts.assertNoValues(); + + ts.request(1); + + ts.assertValue(1L); + ts.assertComplete(); + ts.assertNoErrors(); + + o = null; + + System.gc(); + Thread.sleep(200); + + Assert.assertNull("Object retained!", wr.get()); + } + + @Test + public void justUnsubscribed() throws Exception { + o = new Object(); + + WeakReference<Object> wr = new WeakReference<>(o); + + TestSubscriber<Object> ts = new TestSubscriber<>(0); + + Flowable.just(o).count().toFlowable().onTerminateDetach().subscribe(ts); + + ts.cancel(); + o = null; + + System.gc(); + Thread.sleep(200); + + Assert.assertNull("Object retained!", wr.get()); + + } + + @Test + public void deferredUpstreamProducer() { + final AtomicReference<Subscriber<? super Object>> subscriber = new AtomicReference<>(); + + TestSubscriber<Object> ts = new TestSubscriber<>(0); + + Flowable.unsafeCreate(new Publisher<Object>() { + @Override + public void subscribe(Subscriber<? super Object> t) { + subscriber.set(t); + } + }).onTerminateDetach().subscribe(ts); + + ts.request(2); + + new FlowableRange(1, 3).subscribe(subscriber.get()); + + ts.assertValues(1, 2); + + ts.request(1); + + ts.assertValues(1, 2, 3); + ts.assertComplete(); + ts.assertNoErrors(); + } + + @Test + public void dispose() { + TestHelper.checkDisposed(Flowable.never().onTerminateDetach()); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeFlowable(new Function<Flowable<Object>, Flowable<Object>>() { + @Override + public Flowable<Object> apply(Flowable<Object> f) throws Exception { + return f.onTerminateDetach(); + } + }); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableDistinctTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableDistinctTest.java new file mode 100644 index 0000000000..362283c719 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableDistinctTest.java @@ -0,0 +1,226 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +import java.util.*; + +import org.junit.*; +import org.mockito.InOrder; +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.internal.subscriptions.BooleanSubscription; +import io.reactivex.rxjava3.internal.util.ExceptionHelper; +import io.reactivex.rxjava3.operators.QueueFuseable; +import io.reactivex.rxjava3.operators.QueueSubscription; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.processors.UnicastProcessor; +import io.reactivex.rxjava3.testsupport.*; + +public class FlowableDistinctTest extends RxJavaTest { + + Subscriber<String> w; + + // nulls lead to exceptions + final Function<String, String> TO_UPPER_WITH_EXCEPTION = new Function<String, String>() { + @Override + public String apply(String s) { + if (s.equals("x")) { + return "XX"; + } + return s.toUpperCase(); + } + }; + + @Before + public void before() { + w = TestHelper.mockSubscriber(); + } + + @Test + public void distinctOfNone() { + Flowable<String> src = Flowable.empty(); + src.distinct().subscribe(w); + + verify(w, never()).onNext(anyString()); + verify(w, never()).onError(any(Throwable.class)); + verify(w, times(1)).onComplete(); + } + + @Test + public void distinctOfNoneWithKeySelector() { + Flowable<String> src = Flowable.empty(); + src.distinct(TO_UPPER_WITH_EXCEPTION).subscribe(w); + + verify(w, never()).onNext(anyString()); + verify(w, never()).onError(any(Throwable.class)); + verify(w, times(1)).onComplete(); + } + + @Test + public void distinctOfNormalSource() { + Flowable<String> src = Flowable.just("a", "b", "c", "c", "c", "b", "b", "a", "e"); + src.distinct().subscribe(w); + + InOrder inOrder = inOrder(w); + inOrder.verify(w, times(1)).onNext("a"); + inOrder.verify(w, times(1)).onNext("b"); + inOrder.verify(w, times(1)).onNext("c"); + inOrder.verify(w, times(1)).onNext("e"); + inOrder.verify(w, times(1)).onComplete(); + inOrder.verify(w, never()).onNext(anyString()); + verify(w, never()).onError(any(Throwable.class)); + } + + @Test + public void distinctOfNormalSourceWithKeySelector() { + Flowable<String> src = Flowable.just("a", "B", "c", "C", "c", "B", "b", "a", "E"); + src.distinct(TO_UPPER_WITH_EXCEPTION).subscribe(w); + + InOrder inOrder = inOrder(w); + inOrder.verify(w, times(1)).onNext("a"); + inOrder.verify(w, times(1)).onNext("B"); + inOrder.verify(w, times(1)).onNext("c"); + inOrder.verify(w, times(1)).onNext("E"); + inOrder.verify(w, times(1)).onComplete(); + inOrder.verify(w, never()).onNext(anyString()); + verify(w, never()).onError(any(Throwable.class)); + } + + @Test + public void error() { + Flowable.error(new TestException()) + .distinct() + .test() + .assertFailure(TestException.class); + } + + @Test + public void fusedSync() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<Integer>().setInitialFusionMode(QueueFuseable.ANY); + + Flowable.just(1, 1, 2, 1, 3, 2, 4, 5, 4) + .distinct() + .subscribe(ts); + + ts.assertFusionMode(QueueFuseable.SYNC) + .assertResult(1, 2, 3, 4, 5); + } + + @Test + public void fusedAsync() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<Integer>().setInitialFusionMode(QueueFuseable.ANY); + + UnicastProcessor<Integer> up = UnicastProcessor.create(); + + up + .distinct() + .subscribe(ts); + + TestHelper.emit(up, 1, 1, 2, 1, 3, 2, 4, 5, 4); + + ts.assertFusionMode(QueueFuseable.ASYNC) + .assertResult(1, 2, 3, 4, 5); + } + + @Test + public void fusedClear() { + Flowable.just(1, 1, 2, 1, 3, 2, 4, 5, 4) + .distinct() + .subscribe(new FlowableSubscriber<Integer>() { + @Override + public void onSubscribe(Subscription s) { + QueueSubscription<?> qs = (QueueSubscription<?>)s; + + assertFalse(qs.isEmpty()); + + qs.clear(); + + assertTrue(qs.isEmpty()); + } + + @Override + public void onNext(Integer value) { + } + + @Override + public void onError(Throwable e) { + } + + @Override + public void onComplete() { + } + }); + } + + @Test + public void collectionSupplierThrows() { + Flowable.just(1) + .distinct(Functions.identity(), new Supplier<Collection<Object>>() { + @Override + public Collection<Object> get() throws Exception { + throw new TestException(); + } + }) + .test() + .assertFailure(TestException.class); + } + + @Test + public void collectionSupplierIsNull() { + Flowable.just(1) + .distinct(Functions.identity(), new Supplier<Collection<Object>>() { + @Override + public Collection<Object> get() throws Exception { + return null; + } + }) + .to(TestHelper.<Integer>testConsumer()) + .assertFailure(NullPointerException.class) + .assertErrorMessage(ExceptionHelper.nullWarning("The collectionSupplier returned a null Collection.")); + } + + @Test + public void badSource() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + new Flowable<Integer>() { + @Override + protected void subscribeActual(Subscriber<? super Integer> subscriber) { + subscriber.onSubscribe(new BooleanSubscription()); + + subscriber.onNext(1); + subscriber.onComplete(); + subscriber.onNext(2); + subscriber.onError(new TestException()); + subscriber.onComplete(); + } + } + .distinct() + .test() + .assertResult(1); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableDistinctUntilChangedTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableDistinctUntilChangedTest.java new file mode 100644 index 0000000000..00525b562b --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableDistinctUntilChangedTest.java @@ -0,0 +1,457 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +import java.io.IOException; +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.junit.*; +import org.mockito.InOrder; +import org.reactivestreams.Subscriber; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.internal.subscriptions.BooleanSubscription; +import io.reactivex.rxjava3.operators.QueueFuseable; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.processors.*; +import io.reactivex.rxjava3.subscribers.TestSubscriber; +import io.reactivex.rxjava3.testsupport.*; + +public class FlowableDistinctUntilChangedTest extends RxJavaTest { + + Subscriber<String> w; + Subscriber<String> w2; + + // nulls lead to exceptions + final Function<String, String> TO_UPPER_WITH_EXCEPTION = new Function<String, String>() { + @Override + public String apply(String s) { + if (s.equals("x")) { + return "xx"; + } + return s.toUpperCase(); + } + }; + + @Before + public void before() { + w = TestHelper.mockSubscriber(); + w2 = TestHelper.mockSubscriber(); + } + + @Test + public void distinctUntilChangedOfNone() { + Flowable<String> src = Flowable.empty(); + src.distinctUntilChanged().subscribe(w); + + verify(w, never()).onNext(anyString()); + verify(w, never()).onError(any(Throwable.class)); + verify(w, times(1)).onComplete(); + } + + @Test + public void distinctUntilChangedOfNoneWithKeySelector() { + Flowable<String> src = Flowable.empty(); + src.distinctUntilChanged(TO_UPPER_WITH_EXCEPTION).subscribe(w); + + verify(w, never()).onNext(anyString()); + verify(w, never()).onError(any(Throwable.class)); + verify(w, times(1)).onComplete(); + } + + @Test + public void distinctUntilChangedOfNormalSource() { + Flowable<String> src = Flowable.just("a", "b", "c", "c", "c", "b", "b", "a", "e"); + src.distinctUntilChanged().subscribe(w); + + InOrder inOrder = inOrder(w); + inOrder.verify(w, times(1)).onNext("a"); + inOrder.verify(w, times(1)).onNext("b"); + inOrder.verify(w, times(1)).onNext("c"); + inOrder.verify(w, times(1)).onNext("b"); + inOrder.verify(w, times(1)).onNext("a"); + inOrder.verify(w, times(1)).onNext("e"); + inOrder.verify(w, times(1)).onComplete(); + inOrder.verify(w, never()).onNext(anyString()); + verify(w, never()).onError(any(Throwable.class)); + } + + @Test + public void distinctUntilChangedOfNormalSourceWithKeySelector() { + Flowable<String> src = Flowable.just("a", "b", "c", "C", "c", "B", "b", "a", "e"); + src.distinctUntilChanged(TO_UPPER_WITH_EXCEPTION).subscribe(w); + + InOrder inOrder = inOrder(w); + inOrder.verify(w, times(1)).onNext("a"); + inOrder.verify(w, times(1)).onNext("b"); + inOrder.verify(w, times(1)).onNext("c"); + inOrder.verify(w, times(1)).onNext("B"); + inOrder.verify(w, times(1)).onNext("a"); + inOrder.verify(w, times(1)).onNext("e"); + inOrder.verify(w, times(1)).onComplete(); + inOrder.verify(w, never()).onNext(anyString()); + verify(w, never()).onError(any(Throwable.class)); + } + + @Test + public void directComparer() { + Flowable.fromArray(1, 2, 2, 3, 2, 4, 1, 1, 2) + .distinctUntilChanged(new BiPredicate<Integer, Integer>() { + @Override + public boolean test(Integer a, Integer b) { + return a.equals(b); + } + }) + .test() + .assertResult(1, 2, 3, 2, 4, 1, 2); + } + + @Test + public void directComparerConditional() { + Flowable.fromArray(1, 2, 2, 3, 2, 4, 1, 1, 2) + .distinctUntilChanged(new BiPredicate<Integer, Integer>() { + @Override + public boolean test(Integer a, Integer b) { + return a.equals(b); + } + }) + .filter(new Predicate<Integer>() { + @Override + public boolean test(Integer v) { + return true; + } + }) + .test() + .assertResult(1, 2, 3, 2, 4, 1, 2); + } + + @Test + public void directComparerFused() { + Flowable.fromArray(1, 2, 2, 3, 2, 4, 1, 1, 2) + .distinctUntilChanged(new BiPredicate<Integer, Integer>() { + @Override + public boolean test(Integer a, Integer b) { + return a.equals(b); + } + }) + .to(TestHelper.<Integer>testSubscriber(Long.MAX_VALUE, QueueFuseable.ANY, false)) + .assertFuseable() + .assertFusionMode(QueueFuseable.SYNC) + .assertResult(1, 2, 3, 2, 4, 1, 2); + } + + @Test + public void directComparerConditionalFused() { + Flowable.fromArray(1, 2, 2, 3, 2, 4, 1, 1, 2) + .distinctUntilChanged(new BiPredicate<Integer, Integer>() { + @Override + public boolean test(Integer a, Integer b) { + return a.equals(b); + } + }) + .filter(new Predicate<Integer>() { + @Override + public boolean test(Integer v) { + return true; + } + }) + .to(TestHelper.<Integer>testSubscriber(Long.MAX_VALUE, QueueFuseable.ANY, false)) + .assertFuseable() + .assertFusionMode(QueueFuseable.SYNC) + .assertResult(1, 2, 3, 2, 4, 1, 2); + } + + private static final Function<String, String> THROWS_NON_FATAL = new Function<String, String>() { + @Override + public String apply(String s) { + throw new RuntimeException(); + } + }; + + @Test + public void distinctUntilChangedWhenNonFatalExceptionThrownByKeySelectorIsNotReportedByUpstream() { + Flowable<String> src = Flowable.just("a", "b", "null", "c"); + final AtomicBoolean errorOccurred = new AtomicBoolean(false); + src + .doOnError(new Consumer<Throwable>() { + @Override + public void accept(Throwable t) { + errorOccurred.set(true); + } + }) + .distinctUntilChanged(THROWS_NON_FATAL) + .subscribe(w); + Assert.assertFalse(errorOccurred.get()); + } + + @Test + public void customComparator() { + Flowable<String> source = Flowable.just("a", "b", "B", "A", "a", "C"); + + TestSubscriber<String> ts = TestSubscriber.create(); + + source.distinctUntilChanged(new BiPredicate<String, String>() { + @Override + public boolean test(String a, String b) { + return a.compareToIgnoreCase(b) == 0; + } + }) + .subscribe(ts); + + ts.assertValues("a", "b", "A", "C"); + ts.assertNoErrors(); + ts.assertComplete(); + } + + @Test + public void customComparatorThrows() { + Flowable<String> source = Flowable.just("a", "b", "B", "A", "a", "C"); + + TestSubscriber<String> ts = TestSubscriber.create(); + + source.distinctUntilChanged(new BiPredicate<String, String>() { + @Override + public boolean test(String a, String b) { + throw new TestException(); + } + }) + .subscribe(ts); + + ts.assertValue("a"); + ts.assertNotComplete(); + ts.assertError(TestException.class); + } + + @Test + public void fused() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<Integer>().setInitialFusionMode(QueueFuseable.ANY); + + Flowable.just(1, 2, 2, 3, 3, 4, 5) + .distinctUntilChanged(new BiPredicate<Integer, Integer>() { + @Override + public boolean test(Integer a, Integer b) throws Exception { + return a.equals(b); + } + }) + .subscribe(ts); + + ts.assertFuseable() + .assertFusionMode(QueueFuseable.SYNC) + .assertResult(1, 2, 3, 4, 5) + ; + } + + @Test + public void fusedAsync() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<Integer>().setInitialFusionMode(QueueFuseable.ANY); + + UnicastProcessor<Integer> up = UnicastProcessor.create(); + + up + .distinctUntilChanged(new BiPredicate<Integer, Integer>() { + @Override + public boolean test(Integer a, Integer b) throws Exception { + return a.equals(b); + } + }) + .subscribe(ts); + + TestHelper.emit(up, 1, 2, 2, 3, 3, 4, 5); + + ts.assertFuseable() + .assertFusionMode(QueueFuseable.ASYNC) + .assertResult(1, 2, 3, 4, 5) + ; + } + + @Test + public void ignoreCancel() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + + try { + new Flowable<Integer>() { + @Override + public void subscribeActual(Subscriber<? super Integer> s) { + s.onSubscribe(new BooleanSubscription()); + s.onNext(1); + s.onNext(2); + s.onNext(3); + s.onError(new IOException()); + s.onComplete(); + } + } + .distinctUntilChanged(new BiPredicate<Integer, Integer>() { + @Override + public boolean test(Integer a, Integer b) throws Exception { + throw new TestException(); + } + }) + .test() + .assertFailure(TestException.class, 1); + + TestHelper.assertUndeliverable(errors, 0, IOException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + class Mutable { + int value; + } + + @Test + public void mutableWithSelector() { + Mutable m = new Mutable(); + + PublishProcessor<Mutable> pp = PublishProcessor.create(); + + TestSubscriber<Mutable> ts = pp.distinctUntilChanged(new Function<Mutable, Object>() { + @Override + public Object apply(Mutable m) throws Exception { + return m.value; + } + }) + .test(); + + pp.onNext(m); + m.value = 1; + pp.onNext(m); + pp.onComplete(); + + ts.assertResult(m, m); + } + + @Test + public void conditionalNormal() { + Flowable.just(1, 2, 1, 3, 3, 4, 3, 5, 5) + .distinctUntilChanged() + .filter(new Predicate<Integer>() { + @Override + public boolean test(Integer v) throws Exception { + return v % 2 == 0; + } + }) + .test() + .assertResult(2, 4); + } + + @Test + public void conditionalNormal2() { + Flowable.just(1, 2, 1, 3, 3, 4, 3, 5, 5).hide() + .distinctUntilChanged() + .filter(new Predicate<Integer>() { + @Override + public boolean test(Integer v) throws Exception { + return v % 2 == 0; + } + }) + .test() + .assertResult(2, 4); + } + + @Test + public void conditionalNormal3() { + UnicastProcessor<Integer> up = UnicastProcessor.create(); + + TestSubscriber<Integer> ts = up.hide() + .distinctUntilChanged() + .filter(new Predicate<Integer>() { + @Override + public boolean test(Integer v) throws Exception { + return v % 2 == 0; + } + }) + .test(); + + TestHelper.emit(up, 1, 2, 1, 3, 3, 4, 3, 5, 5); + + ts + .assertResult(2, 4); + } + + @Test + public void conditionalSelectorCrash() { + Flowable.just(1, 2, 1, 3, 3, 4, 3, 5, 5) + .distinctUntilChanged(new BiPredicate<Integer, Integer>() { + @Override + public boolean test(Integer a, Integer b) throws Exception { + throw new TestException(); + } + }) + .filter(new Predicate<Integer>() { + @Override + public boolean test(Integer v) throws Exception { + return v % 2 == 0; + } + }) + .test() + .assertFailure(TestException.class); + } + + @Test + public void conditionalFused() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<Integer>().setInitialFusionMode(QueueFuseable.ANY); + + Flowable.just(1, 2, 1, 3, 3, 4, 3, 5, 5) + .distinctUntilChanged() + .filter(new Predicate<Integer>() { + @Override + public boolean test(Integer v) throws Exception { + return v % 2 == 0; + } + }) + .subscribe(ts); + + ts.assertFusionMode(QueueFuseable.SYNC) + .assertResult(2, 4); + } + + @Test + public void conditionalAsyncFused() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<Integer>().setInitialFusionMode(QueueFuseable.ANY); + UnicastProcessor<Integer> up = UnicastProcessor.create(); + + up + .distinctUntilChanged() + .filter(new Predicate<Integer>() { + @Override + public boolean test(Integer v) throws Exception { + return v % 2 == 0; + } + }) + .subscribe(ts); + + TestHelper.emit(up, 1, 2, 1, 3, 3, 4, 3, 5, 5); + + ts.assertFusionMode(QueueFuseable.ASYNC) + .assertResult(2, 4); + } + + @Test + public void badSource() { + TestHelper.checkBadSourceFlowable(new Function<Flowable<Integer>, Object>() { + @Override + public Object apply(Flowable<Integer> f) throws Exception { + return f.distinctUntilChanged().filter(Functions.alwaysTrue()); + } + }, false, 1, 1, 1); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableDoAfterNextTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableDoAfterNextTest.java new file mode 100644 index 0000000000..1e36eee49f --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableDoAfterNextTest.java @@ -0,0 +1,269 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import static org.junit.Assert.*; + +import java.util.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.Consumer; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.operators.QueueFuseable; +import io.reactivex.rxjava3.processors.UnicastProcessor; +import io.reactivex.rxjava3.subscribers.TestSubscriber; +import io.reactivex.rxjava3.testsupport.*; + +public class FlowableDoAfterNextTest extends RxJavaTest { + + final List<Integer> values = new ArrayList<>(); + + final Consumer<Integer> afterNext = new Consumer<Integer>() { + @Override + public void accept(Integer e) throws Exception { + values.add(-e); + } + }; + + final TestSubscriber<Integer> ts = new TestSubscriber<Integer>() { + @Override + public void onNext(Integer t) { + super.onNext(t); + FlowableDoAfterNextTest.this.values.add(t); + } + }; + + @Test + public void just() { + Flowable.just(1) + .doAfterNext(afterNext) + .subscribeWith(ts) + .assertResult(1); + + assertEquals(Arrays.asList(1, -1), values); + } + + @Test + public void range() { + Flowable.range(1, 5) + .doAfterNext(afterNext) + .subscribeWith(ts) + .assertResult(1, 2, 3, 4, 5); + + assertEquals(Arrays.asList(1, -1, 2, -2, 3, -3, 4, -4, 5, -5), values); + } + + @Test + public void error() { + Flowable.<Integer>error(new TestException()) + .doAfterNext(afterNext) + .subscribeWith(ts) + .assertFailure(TestException.class); + + assertTrue(values.isEmpty()); + } + + @Test + public void empty() { + Flowable.<Integer>empty() + .doAfterNext(afterNext) + .subscribeWith(ts) + .assertResult(); + + assertTrue(values.isEmpty()); + } + + @Test + public void syncFused() { + TestSubscriberEx<Integer> ts0 = new TestSubscriberEx<Integer>().setInitialFusionMode(QueueFuseable.SYNC); + + Flowable.range(1, 5) + .doAfterNext(afterNext) + .subscribe(ts0); + + ts0.assertFusionMode(QueueFuseable.SYNC) + .assertResult(1, 2, 3, 4, 5); + + assertEquals(Arrays.asList(-1, -2, -3, -4, -5), values); + } + + @Test + public void asyncFusedRejected() { + TestSubscriberEx<Integer> ts0 = new TestSubscriberEx<Integer>().setInitialFusionMode(QueueFuseable.ASYNC); + + Flowable.range(1, 5) + .doAfterNext(afterNext) + .subscribe(ts0); + + ts0.assertFusionMode(QueueFuseable.NONE) + .assertResult(1, 2, 3, 4, 5); + + assertEquals(Arrays.asList(-1, -2, -3, -4, -5), values); + } + + @Test + public void asyncFused() { + TestSubscriberEx<Integer> ts0 = new TestSubscriberEx<Integer>().setInitialFusionMode(QueueFuseable.ASYNC); + + UnicastProcessor<Integer> up = UnicastProcessor.create(); + + TestHelper.emit(up, 1, 2, 3, 4, 5); + + up + .doAfterNext(afterNext) + .subscribe(ts0); + + ts0.assertFusionMode(QueueFuseable.ASYNC) + .assertResult(1, 2, 3, 4, 5); + + assertEquals(Arrays.asList(-1, -2, -3, -4, -5), values); + } + + @Test + public void justConditional() { + Flowable.just(1) + .doAfterNext(afterNext) + .filter(Functions.alwaysTrue()) + .subscribeWith(ts) + .assertResult(1); + + assertEquals(Arrays.asList(1, -1), values); + } + + @Test + public void rangeConditional() { + Flowable.range(1, 5) + .doAfterNext(afterNext) + .filter(Functions.alwaysTrue()) + .subscribeWith(ts) + .assertResult(1, 2, 3, 4, 5); + + assertEquals(Arrays.asList(1, -1, 2, -2, 3, -3, 4, -4, 5, -5), values); + } + + @Test + public void errorConditional() { + Flowable.<Integer>error(new TestException()) + .doAfterNext(afterNext) + .filter(Functions.alwaysTrue()) + .subscribeWith(ts) + .assertFailure(TestException.class); + + assertTrue(values.isEmpty()); + } + + @Test + public void emptyConditional() { + Flowable.<Integer>empty() + .doAfterNext(afterNext) + .filter(Functions.alwaysTrue()) + .subscribeWith(ts) + .assertResult(); + + assertTrue(values.isEmpty()); + } + + @Test + public void syncFusedConditional() { + TestSubscriberEx<Integer> ts0 = new TestSubscriberEx<Integer>().setInitialFusionMode(QueueFuseable.SYNC); + + Flowable.range(1, 5) + .doAfterNext(afterNext) + .filter(Functions.alwaysTrue()) + .subscribe(ts0); + + ts0.assertFusionMode(QueueFuseable.SYNC) + .assertResult(1, 2, 3, 4, 5); + + assertEquals(Arrays.asList(-1, -2, -3, -4, -5), values); + } + + @Test + public void asyncFusedRejectedConditional() { + TestSubscriberEx<Integer> ts0 = new TestSubscriberEx<Integer>().setInitialFusionMode(QueueFuseable.ASYNC); + + Flowable.range(1, 5) + .doAfterNext(afterNext) + .filter(Functions.alwaysTrue()) + .subscribe(ts0); + + ts0.assertFusionMode(QueueFuseable.NONE) + .assertResult(1, 2, 3, 4, 5); + + assertEquals(Arrays.asList(-1, -2, -3, -4, -5), values); + } + + @Test + public void asyncFusedConditional() { + TestSubscriberEx<Integer> ts0 = new TestSubscriberEx<Integer>().setInitialFusionMode(QueueFuseable.ASYNC); + + UnicastProcessor<Integer> up = UnicastProcessor.create(); + + TestHelper.emit(up, 1, 2, 3, 4, 5); + + up + .doAfterNext(afterNext) + .filter(Functions.alwaysTrue()) + .subscribe(ts0); + + ts0.assertFusionMode(QueueFuseable.ASYNC) + .assertResult(1, 2, 3, 4, 5); + + assertEquals(Arrays.asList(-1, -2, -3, -4, -5), values); + } + + @Test + public void consumerThrows() { + Flowable.just(1, 2) + .doAfterNext(new Consumer<Integer>() { + @Override + public void accept(Integer e) throws Exception { + throw new TestException(); + } + }) + .test() + .assertFailure(TestException.class, 1); + } + + @Test + public void consumerThrowsConditional() { + Flowable.just(1, 2) + .doAfterNext(new Consumer<Integer>() { + @Override + public void accept(Integer e) throws Exception { + throw new TestException(); + } + }) + .filter(Functions.alwaysTrue()) + .test() + .assertFailure(TestException.class, 1); + } + + @Test + public void consumerThrowsConditional2() { + Flowable.just(1, 2).hide() + .doAfterNext(new Consumer<Integer>() { + @Override + public void accept(Integer e) throws Exception { + throw new TestException(); + } + }) + .filter(Functions.alwaysTrue()) + .test() + .assertFailure(TestException.class, 1); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableDoAfterTerminateTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableDoAfterTerminateTest.java new file mode 100644 index 0000000000..ab1a3bd8ab --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableDoAfterTerminateTest.java @@ -0,0 +1,111 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +import java.util.List; + +import org.junit.*; +import org.mockito.Mockito; +import org.reactivestreams.Subscriber; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.functions.Action; +import io.reactivex.rxjava3.internal.util.ExceptionHelper; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.subscribers.TestSubscriber; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class FlowableDoAfterTerminateTest extends RxJavaTest { + + private Action aAction0; + private Subscriber<String> subscriber; + + @Before + public void before() { + aAction0 = Mockito.mock(Action.class); + subscriber = TestHelper.mockSubscriber(); + } + + private void checkActionCalled(Flowable<String> input) { + input.doAfterTerminate(aAction0).subscribe(subscriber); + try { + verify(aAction0, times(1)).run(); + } catch (Throwable ex) { + throw ExceptionHelper.wrapOrThrow(ex); + } + } + + @Test + public void doAfterTerminateCalledOnComplete() { + checkActionCalled(Flowable.fromArray("1", "2", "3")); + } + + @Test + public void doAfterTerminateCalledOnError() { + checkActionCalled(Flowable.<String> error(new RuntimeException("expected"))); + } + + @Test + public void nullActionShouldBeCheckedInConstructor() { + try { + Flowable.empty().doAfterTerminate(null); + fail("Should have thrown NullPointerException"); + } catch (NullPointerException expected) { + assertEquals("onAfterTerminate is null", expected.getMessage()); + } + } + + @Test + public void nullFinallyActionShouldBeCheckedASAP() { + try { + Flowable + .just("value") + .doAfterTerminate(null); + + fail(); + } catch (NullPointerException expected) { + + } + } + + @Test + public void ifFinallyActionThrowsExceptionShouldNotBeSwallowedAndActionShouldBeCalledOnce() throws Throwable { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + Action finallyAction = Mockito.mock(Action.class); + doThrow(new IllegalStateException()).when(finallyAction).run(); + + TestSubscriber<String> testSubscriber = new TestSubscriber<>(); + + Flowable + .just("value") + .doAfterTerminate(finallyAction) + .subscribe(testSubscriber); + + testSubscriber.assertValue("value"); + + verify(finallyAction).run(); + + TestHelper.assertError(errors, 0, IllegalStateException.class); + // Actual result: + // Not only IllegalStateException was swallowed + // But finallyAction was called twice! + } finally { + RxJavaPlugins.reset(); + } + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableDoFinallyTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableDoFinallyTest.java new file mode 100644 index 0000000000..79cba18b3d --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableDoFinallyTest.java @@ -0,0 +1,543 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import static org.junit.Assert.*; + +import java.util.*; + +import org.junit.Test; +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.operators.QueueFuseable; +import io.reactivex.rxjava3.operators.QueueSubscription; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.processors.UnicastProcessor; +import io.reactivex.rxjava3.testsupport.*; + +public class FlowableDoFinallyTest extends RxJavaTest implements Action { + + int calls; + + @Override + public void run() throws Exception { + calls++; + } + + @Test + public void normalJust() { + Flowable.just(1) + .doFinally(this) + .test() + .assertResult(1); + + assertEquals(1, calls); + } + + @Test + public void normalEmpty() { + Flowable.empty() + .doFinally(this) + .test() + .assertResult(); + + assertEquals(1, calls); + } + + @Test + public void normalError() { + Flowable.error(new TestException()) + .doFinally(this) + .test() + .assertFailure(TestException.class); + + assertEquals(1, calls); + } + + @Test + public void normalTake() { + Flowable.range(1, 10) + .doFinally(this) + .take(5) + .test() + .assertResult(1, 2, 3, 4, 5); + + assertEquals(1, calls); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeFlowable(new Function<Flowable<Object>, Publisher<Object>>() { + @Override + public Publisher<Object> apply(Flowable<Object> f) throws Exception { + return f.doFinally(FlowableDoFinallyTest.this); + } + }); + TestHelper.checkDoubleOnSubscribeFlowable(new Function<Flowable<Object>, Publisher<Object>>() { + @Override + public Publisher<Object> apply(Flowable<Object> f) throws Exception { + return f.doFinally(FlowableDoFinallyTest.this).filter(Functions.alwaysTrue()); + } + }); + } + + @Test + public void syncFused() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<Integer>().setInitialFusionMode(QueueFuseable.SYNC); + + Flowable.range(1, 5) + .doFinally(this) + .subscribe(ts); + + ts.assertFusionMode(QueueFuseable.SYNC) + .assertResult(1, 2, 3, 4, 5); + + assertEquals(1, calls); + } + + @Test + public void syncFusedBoundary() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<Integer>().setInitialFusionMode(QueueFuseable.SYNC | QueueFuseable.BOUNDARY); + + Flowable.range(1, 5) + .doFinally(this) + .subscribe(ts); + + ts.assertFusionMode(QueueFuseable.NONE) + .assertResult(1, 2, 3, 4, 5); + + assertEquals(1, calls); + } + + @Test + public void asyncFused() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<Integer>().setInitialFusionMode(QueueFuseable.ASYNC); + + UnicastProcessor<Integer> up = UnicastProcessor.create(); + TestHelper.emit(up, 1, 2, 3, 4, 5); + + up + .doFinally(this) + .subscribe(ts); + + ts.assertFusionMode(QueueFuseable.ASYNC) + .assertResult(1, 2, 3, 4, 5); + + assertEquals(1, calls); + } + + @Test + public void asyncFusedBoundary() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<Integer>().setInitialFusionMode(QueueFuseable.ASYNC | QueueFuseable.BOUNDARY); + + UnicastProcessor<Integer> up = UnicastProcessor.create(); + TestHelper.emit(up, 1, 2, 3, 4, 5); + + up + .doFinally(this) + .subscribe(ts); + + ts.assertFusionMode(QueueFuseable.NONE) + .assertResult(1, 2, 3, 4, 5); + + assertEquals(1, calls); + } + + @Test + public void normalJustConditional() { + Flowable.just(1) + .doFinally(this) + .filter(Functions.alwaysTrue()) + .test() + .assertResult(1); + + assertEquals(1, calls); + } + + @Test + public void normalEmptyConditional() { + Flowable.empty() + .doFinally(this) + .filter(Functions.alwaysTrue()) + .test() + .assertResult(); + + assertEquals(1, calls); + } + + @Test + public void normalErrorConditional() { + Flowable.error(new TestException()) + .doFinally(this) + .filter(Functions.alwaysTrue()) + .test() + .assertFailure(TestException.class); + + assertEquals(1, calls); + } + + @Test + public void normalTakeConditional() { + Flowable.range(1, 10) + .doFinally(this) + .filter(Functions.alwaysTrue()) + .take(5) + .test() + .assertResult(1, 2, 3, 4, 5); + + assertEquals(1, calls); + } + + @Test + public void syncFusedConditional() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<Integer>().setInitialFusionMode(QueueFuseable.SYNC); + + Flowable.range(1, 5) + .doFinally(this) + .compose(TestHelper.conditional()) + .subscribe(ts); + + ts.assertFusionMode(QueueFuseable.SYNC) + .assertResult(1, 2, 3, 4, 5); + + assertEquals(1, calls); + } + + @Test + public void nonFused() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<Integer>().setInitialFusionMode(QueueFuseable.SYNC); + + Flowable.range(1, 5).hide() + .doFinally(this) + .subscribe(ts); + + ts.assertFusionMode(QueueFuseable.NONE) + .assertResult(1, 2, 3, 4, 5); + + assertEquals(1, calls); + } + + @Test + public void nonFusedConditional() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<Integer>().setInitialFusionMode(QueueFuseable.SYNC); + + Flowable.range(1, 5).hide() + .doFinally(this) + .compose(TestHelper.conditional()) + .subscribe(ts); + + ts.assertFusionMode(QueueFuseable.NONE) + .assertResult(1, 2, 3, 4, 5); + + assertEquals(1, calls); + } + + @Test + public void syncFusedBoundaryConditional() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<Integer>().setInitialFusionMode(QueueFuseable.SYNC | QueueFuseable.BOUNDARY); + + Flowable.range(1, 5) + .doFinally(this) + .compose(TestHelper.conditional()) + .subscribe(ts); + + ts.assertFusionMode(QueueFuseable.NONE) + .assertResult(1, 2, 3, 4, 5); + + assertEquals(1, calls); + } + + @Test + public void asyncFusedConditional() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<Integer>().setInitialFusionMode(QueueFuseable.ASYNC); + + UnicastProcessor<Integer> up = UnicastProcessor.create(); + TestHelper.emit(up, 1, 2, 3, 4, 5); + + up + .doFinally(this) + .compose(TestHelper.conditional()) + .subscribe(ts); + + ts.assertFusionMode(QueueFuseable.ASYNC) + .assertResult(1, 2, 3, 4, 5); + + assertEquals(1, calls); + } + + @Test + public void asyncFusedBoundaryConditional() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<Integer>().setInitialFusionMode(QueueFuseable.ASYNC | QueueFuseable.BOUNDARY); + + UnicastProcessor<Integer> up = UnicastProcessor.create(); + TestHelper.emit(up, 1, 2, 3, 4, 5); + + up + .doFinally(this) + .compose(TestHelper.conditional()) + .subscribe(ts); + + ts.assertFusionMode(QueueFuseable.NONE) + .assertResult(1, 2, 3, 4, 5); + + assertEquals(1, calls); + } + + @Test + public void actionThrows() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + Flowable.just(1) + .doFinally(new Action() { + @Override + public void run() throws Exception { + throw new TestException(); + } + }) + .test() + .assertResult(1) + .cancel(); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void actionThrowsConditional() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + Flowable.just(1) + .doFinally(new Action() { + @Override + public void run() throws Exception { + throw new TestException(); + } + }) + .filter(Functions.alwaysTrue()) + .test() + .assertResult(1) + .cancel(); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void clearIsEmpty() { + Flowable.range(1, 5) + .doFinally(this) + .subscribe(new FlowableSubscriber<Integer>() { + + @Override + public void onSubscribe(Subscription s) { + @SuppressWarnings("unchecked") + QueueSubscription<Integer> qs = (QueueSubscription<Integer>)s; + + qs.requestFusion(QueueFuseable.ANY); + + assertFalse(qs.isEmpty()); + + try { + assertEquals(1, qs.poll().intValue()); + } catch (Throwable ex) { + throw new RuntimeException(ex); + } + + assertFalse(qs.isEmpty()); + + qs.clear(); + + assertTrue(qs.isEmpty()); + + qs.cancel(); + } + + @Override + public void onNext(Integer t) { + } + + @Override + public void onError(Throwable t) { + } + + @Override + public void onComplete() { + } + }); + + assertEquals(1, calls); + } + + @Test + public void clearIsEmptyConditional() { + Flowable.range(1, 5) + .doFinally(this) + .filter(Functions.alwaysTrue()) + .subscribe(new FlowableSubscriber<Integer>() { + + @Override + public void onSubscribe(Subscription s) { + @SuppressWarnings("unchecked") + QueueSubscription<Integer> qs = (QueueSubscription<Integer>)s; + + qs.requestFusion(QueueFuseable.ANY); + + assertFalse(qs.isEmpty()); + + try { + assertEquals(1, qs.poll().intValue()); + } catch (Throwable ex) { + throw new RuntimeException(ex); + } + + assertFalse(qs.isEmpty()); + + qs.clear(); + + assertTrue(qs.isEmpty()); + + qs.cancel(); + } + + @Override + public void onNext(Integer t) { + } + + @Override + public void onError(Throwable t) { + } + + @Override + public void onComplete() { + } + }); + + assertEquals(1, calls); + } + + @Test + public void eventOrdering() { + final List<String> list = new ArrayList<>(); + + Flowable.error(new TestException()) + .doOnCancel(new Action() { + @Override + public void run() throws Exception { + list.add("cancel"); + } + }) + .doFinally(new Action() { + @Override + public void run() throws Exception { + list.add("finally"); + } + }) + .subscribe( + new Consumer<Object>() { + @Override + public void accept(Object v) throws Exception { + list.add("onNext"); + } + }, + new Consumer<Throwable>() { + @Override + public void accept(Throwable e) throws Exception { + list.add("onError"); + } + }, + new Action() { + @Override + public void run() throws Exception { + list.add("onComplete"); + } + }); + + assertEquals(Arrays.asList("onError", "finally"), list); + } + + @Test + public void eventOrdering2() { + final List<String> list = new ArrayList<>(); + + Flowable.just(1) + .doOnCancel(new Action() { + @Override + public void run() throws Exception { + list.add("cancel"); + } + }) + .doFinally(new Action() { + @Override + public void run() throws Exception { + list.add("finally"); + } + }) + .subscribe( + new Consumer<Object>() { + @Override + public void accept(Object v) throws Exception { + list.add("onNext"); + } + }, + new Consumer<Throwable>() { + @Override + public void accept(Throwable e) throws Exception { + list.add("onError"); + } + }, + new Action() { + @Override + public void run() throws Exception { + list.add("onComplete"); + } + }); + + assertEquals(Arrays.asList("onNext", "onComplete", "finally"), list); + } + + @Test + public void fusionRejected() { + TestSubscriberEx<Object> ts = new TestSubscriberEx<>(); + ts.setInitialFusionMode(QueueFuseable.ANY); + + TestHelper.rejectFlowableFusion() + .doFinally(() -> { }) + .subscribeWith(ts); + + ts.assertFuseable() + .assertFusionMode(QueueFuseable.NONE); + } + + @Test + public void fusionRejectedConditional() { + TestSubscriberEx<Object> ts = new TestSubscriberEx<>(); + ts.setInitialFusionMode(QueueFuseable.ANY); + + TestHelper.rejectFlowableFusion() + .doFinally(() -> { }) + .compose(TestHelper.conditional()) + .subscribeWith(ts); + + ts.assertFuseable() + .assertFusionMode(QueueFuseable.NONE); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableDoOnEachTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableDoOnEachTest.java new file mode 100644 index 0000000000..484d62c19e --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableDoOnEachTest.java @@ -0,0 +1,900 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import java.io.IOException; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.*; +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.flowables.ConnectableFlowable; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.internal.subscriptions.BooleanSubscription; +import io.reactivex.rxjava3.operators.ConditionalSubscriber; +import io.reactivex.rxjava3.operators.QueueFuseable; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.processors.UnicastProcessor; +import io.reactivex.rxjava3.subscribers.TestSubscriber; +import io.reactivex.rxjava3.testsupport.*; + +public class FlowableDoOnEachTest extends RxJavaTest { + + Subscriber<String> subscribedSubscriber; + Subscriber<String> sideEffectSubscriber; + + @Before + public void before() { + subscribedSubscriber = TestHelper.mockSubscriber(); + sideEffectSubscriber = TestHelper.mockSubscriber(); + } + + @Test + public void doOnEach() { + Flowable<String> base = Flowable.just("a", "b", "c"); + Flowable<String> doOnEach = base.doOnEach(sideEffectSubscriber); + + doOnEach.subscribe(subscribedSubscriber); + + // ensure the leaf observer is still getting called + verify(subscribedSubscriber, never()).onError(any(Throwable.class)); + verify(subscribedSubscriber, times(1)).onNext("a"); + verify(subscribedSubscriber, times(1)).onNext("b"); + verify(subscribedSubscriber, times(1)).onNext("c"); + verify(subscribedSubscriber, times(1)).onComplete(); + + // ensure our injected observer is getting called + verify(sideEffectSubscriber, never()).onError(any(Throwable.class)); + verify(sideEffectSubscriber, times(1)).onNext("a"); + verify(sideEffectSubscriber, times(1)).onNext("b"); + verify(sideEffectSubscriber, times(1)).onNext("c"); + verify(sideEffectSubscriber, times(1)).onComplete(); + } + + @Test + public void doOnEachWithError() { + Flowable<String> base = Flowable.just("one", "fail", "two", "three", "fail"); + Flowable<String> errs = base.map(new Function<String, String>() { + @Override + public String apply(String s) { + if ("fail".equals(s)) { + throw new RuntimeException("Forced Failure"); + } + return s; + } + }); + + Flowable<String> doOnEach = errs.doOnEach(sideEffectSubscriber); + + doOnEach.subscribe(subscribedSubscriber); + verify(subscribedSubscriber, times(1)).onNext("one"); + verify(subscribedSubscriber, never()).onNext("two"); + verify(subscribedSubscriber, never()).onNext("three"); + verify(subscribedSubscriber, never()).onComplete(); + verify(subscribedSubscriber, times(1)).onError(any(Throwable.class)); + + verify(sideEffectSubscriber, times(1)).onNext("one"); + verify(sideEffectSubscriber, never()).onNext("two"); + verify(sideEffectSubscriber, never()).onNext("three"); + verify(sideEffectSubscriber, never()).onComplete(); + verify(sideEffectSubscriber, times(1)).onError(any(Throwable.class)); + } + + @Test + public void doOnEachWithErrorInCallback() { + Flowable<String> base = Flowable.just("one", "two", "fail", "three"); + Flowable<String> doOnEach = base.doOnNext(new Consumer<String>() { + @Override + public void accept(String s) { + if ("fail".equals(s)) { + throw new RuntimeException("Forced Failure"); + } + } + }); + + doOnEach.subscribe(subscribedSubscriber); + verify(subscribedSubscriber, times(1)).onNext("one"); + verify(subscribedSubscriber, times(1)).onNext("two"); + verify(subscribedSubscriber, never()).onNext("three"); + verify(subscribedSubscriber, never()).onComplete(); + verify(subscribedSubscriber, times(1)).onError(any(Throwable.class)); + + } + + @Test + public void issue1451Case1() { + // https://github.com/Netflix/RxJava/issues/1451 + final int expectedCount = 3; + final AtomicInteger count = new AtomicInteger(); + for (int i = 0; i < expectedCount; i++) { + Flowable + .just(Boolean.TRUE, Boolean.FALSE) + .takeWhile(new Predicate<Boolean>() { + @Override + public boolean test(Boolean value) { + return value; + } + }) + .toList() + .doOnSuccess(new Consumer<List<Boolean>>() { + @Override + public void accept(List<Boolean> booleans) { + count.incrementAndGet(); + } + }) + .subscribe(); + } + assertEquals(expectedCount, count.get()); + } + + @Test + public void issue1451Case2() { + // https://github.com/Netflix/RxJava/issues/1451 + final int expectedCount = 3; + final AtomicInteger count = new AtomicInteger(); + for (int i = 0; i < expectedCount; i++) { + Flowable + .just(Boolean.TRUE, Boolean.FALSE, Boolean.FALSE) + .takeWhile(new Predicate<Boolean>() { + @Override + public boolean test(Boolean value) { + return value; + } + }) + .toList() + .doOnSuccess(new Consumer<List<Boolean>>() { + @Override + public void accept(List<Boolean> booleans) { + count.incrementAndGet(); + } + }) + .subscribe(); + } + assertEquals(expectedCount, count.get()); + } + + @Test + public void onErrorThrows() { + TestSubscriberEx<Object> ts = new TestSubscriberEx<>(); + + Flowable.error(new TestException()) + .doOnError(new Consumer<Throwable>() { + @Override + public void accept(Throwable e) { + throw new TestException(); + } + }).subscribe(ts); + + ts.assertNoValues(); + ts.assertNotComplete(); + ts.assertError(CompositeException.class); + + CompositeException ex = (CompositeException)ts.errors().get(0); + + List<Throwable> exceptions = ex.getExceptions(); + assertEquals(2, exceptions.size()); + Assert.assertTrue(exceptions.get(0) instanceof TestException); + Assert.assertTrue(exceptions.get(1) instanceof TestException); + } + + @Test + public void ignoreCancel() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + + try { + Flowable.fromPublisher(new Publisher<Object>() { + @Override + public void subscribe(Subscriber<? super Object> s) { + s.onSubscribe(new BooleanSubscription()); + s.onNext(1); + s.onNext(2); + s.onError(new IOException()); + s.onComplete(); + } + }) + .doOnNext(new Consumer<Object>() { + @Override + public void accept(Object e) throws Exception { + throw new TestException(); + } + }) + .test() + .assertFailure(TestException.class); + + TestHelper.assertUndeliverable(errors, 0, IOException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void onErrorAfterCrash() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + + try { + Flowable.fromPublisher(new Publisher<Object>() { + @Override + public void subscribe(Subscriber<? super Object> s) { + s.onSubscribe(new BooleanSubscription()); + s.onError(new TestException()); + } + }) + .doAfterTerminate(new Action() { + @Override + public void run() throws Exception { + throw new IOException(); + } + }) + .test() + .assertFailure(TestException.class); + + TestHelper.assertUndeliverable(errors, 0, IOException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void onCompleteAfterCrash() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + + try { + Flowable.fromPublisher(new Publisher<Object>() { + @Override + public void subscribe(Subscriber<? super Object> s) { + s.onSubscribe(new BooleanSubscription()); + s.onComplete(); + } + }) + .doAfterTerminate(new Action() { + @Override + public void run() throws Exception { + throw new IOException(); + } + }) + .test() + .assertResult(); + + TestHelper.assertUndeliverable(errors, 0, IOException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void onCompleteCrash() { + Flowable.fromPublisher(new Publisher<Object>() { + @Override + public void subscribe(Subscriber<? super Object> s) { + s.onSubscribe(new BooleanSubscription()); + s.onComplete(); + } + }) + .doOnComplete(new Action() { + @Override + public void run() throws Exception { + throw new IOException(); + } + }) + .test() + .assertFailure(IOException.class); + } + + @Test + public void ignoreCancelConditional() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + + try { + Flowable.fromPublisher(new Publisher<Object>() { + @Override + public void subscribe(Subscriber<? super Object> s) { + s.onSubscribe(new BooleanSubscription()); + s.onNext(1); + s.onNext(2); + s.onError(new IOException()); + s.onComplete(); + } + }) + .doOnNext(new Consumer<Object>() { + @Override + public void accept(Object e) throws Exception { + throw new TestException(); + } + }) + .filter(Functions.alwaysTrue()) + .test() + .assertFailure(TestException.class); + + TestHelper.assertUndeliverable(errors, 0, IOException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void ignoreCancelConditional2() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + + try { + Flowable.fromPublisher(new Publisher<Object>() { + @Override + public void subscribe(Subscriber<? super Object> s) { + ConditionalSubscriber<? super Object> cs = (ConditionalSubscriber<? super Object>)s; + cs.onSubscribe(new BooleanSubscription()); + cs.tryOnNext(1); + cs.tryOnNext(2); + cs.onError(new IOException()); + cs.onComplete(); + } + }) + .doOnNext(new Consumer<Object>() { + @Override + public void accept(Object e) throws Exception { + throw new TestException(); + } + }) + .filter(Functions.alwaysTrue()) + .test() + .assertFailure(TestException.class); + + TestHelper.assertUndeliverable(errors, 0, IOException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void onErrorAfterCrashConditional() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + + try { + Flowable.fromPublisher(new Publisher<Object>() { + @Override + public void subscribe(Subscriber<? super Object> s) { + s.onSubscribe(new BooleanSubscription()); + s.onError(new TestException()); + } + }) + .doAfterTerminate(new Action() { + @Override + public void run() throws Exception { + throw new IOException(); + } + }) + .filter(Functions.alwaysTrue()) + .test() + .assertFailure(TestException.class); + + TestHelper.assertUndeliverable(errors, 0, IOException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void onCompleteAfter() { + final int[] call = { 0 }; + Flowable.just(1) + .doAfterTerminate(new Action() { + @Override + public void run() throws Exception { + call[0]++; + } + }) + .test() + .assertResult(1); + + assertEquals(1, call[0]); + } + + @Test + public void onCompleteAfterCrashConditional() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + + try { + Flowable.fromPublisher(new Publisher<Object>() { + @Override + public void subscribe(Subscriber<? super Object> s) { + s.onSubscribe(new BooleanSubscription()); + s.onComplete(); + } + }) + .doAfterTerminate(new Action() { + @Override + public void run() throws Exception { + throw new IOException(); + } + }) + .filter(Functions.alwaysTrue()) + .test() + .assertResult(); + + TestHelper.assertUndeliverable(errors, 0, IOException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void onCompleteCrashConditional() { + Flowable.fromPublisher(new Publisher<Object>() { + @Override + public void subscribe(Subscriber<? super Object> s) { + s.onSubscribe(new BooleanSubscription()); + s.onComplete(); + } + }) + .doOnComplete(new Action() { + @Override + public void run() throws Exception { + throw new IOException(); + } + }) + .filter(Functions.alwaysTrue()) + .test() + .assertFailure(IOException.class); + } + + @Test + public void onErrorOnErrorCrashConditional() { + TestSubscriberEx<Object> ts = Flowable.error(new TestException("Outer")) + .doOnError(new Consumer<Throwable>() { + @Override + public void accept(Throwable e) throws Exception { + throw new TestException("Inner"); + } + }) + .filter(Functions.alwaysTrue()) + .to(TestHelper.testConsumer()) + .assertFailure(CompositeException.class); + + List<Throwable> errors = TestHelper.compositeList(ts.errors().get(0)); + + TestHelper.assertError(errors, 0, TestException.class, "Outer"); + TestHelper.assertError(errors, 1, TestException.class, "Inner"); + } + + @Test + public void fused() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<Integer>().setInitialFusionMode(QueueFuseable.ANY); + + final int[] call = { 0, 0 }; + + Flowable.range(1, 5) + .doOnNext(new Consumer<Integer>() { + @Override + public void accept(Integer v) throws Exception { + call[0]++; + } + }) + .doOnComplete(new Action() { + @Override + public void run() throws Exception { + call[1]++; + } + }) + .subscribe(ts); + + ts.assertFuseable() + .assertFusionMode(QueueFuseable.SYNC) + .assertResult(1, 2, 3, 4, 5); + + assertEquals(5, call[0]); + assertEquals(1, call[1]); + } + + @Test + public void fusedOnErrorCrash() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<Integer>().setInitialFusionMode(QueueFuseable.ANY); + + final int[] call = { 0 }; + + Flowable.range(1, 5) + .doOnNext(new Consumer<Integer>() { + @Override + public void accept(Integer v) throws Exception { + throw new TestException(); + } + }) + .doOnComplete(new Action() { + @Override + public void run() throws Exception { + call[0]++; + } + }) + .subscribe(ts); + + ts.assertFuseable() + .assertFusionMode(QueueFuseable.SYNC) + .assertFailure(TestException.class); + + assertEquals(0, call[0]); + } + + @Test + public void fusedConditional() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<Integer>().setInitialFusionMode(QueueFuseable.ANY); + + final int[] call = { 0, 0 }; + + Flowable.range(1, 5) + .doOnNext(new Consumer<Integer>() { + @Override + public void accept(Integer v) throws Exception { + call[0]++; + } + }) + .doOnComplete(new Action() { + @Override + public void run() throws Exception { + call[1]++; + } + }) + .filter(Functions.alwaysTrue()) + .subscribe(ts); + + ts.assertFuseable() + .assertFusionMode(QueueFuseable.SYNC) + .assertResult(1, 2, 3, 4, 5); + + assertEquals(5, call[0]); + assertEquals(1, call[1]); + } + + @Test + public void fusedOnErrorCrashConditional() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<Integer>().setInitialFusionMode(QueueFuseable.ANY); + + final int[] call = { 0 }; + + Flowable.range(1, 5) + .doOnNext(new Consumer<Integer>() { + @Override + public void accept(Integer v) throws Exception { + throw new TestException(); + } + }) + .doOnComplete(new Action() { + @Override + public void run() throws Exception { + call[0]++; + } + }) + .filter(Functions.alwaysTrue()) + .subscribe(ts); + + ts.assertFuseable() + .assertFusionMode(QueueFuseable.SYNC) + .assertFailure(TestException.class); + + assertEquals(0, call[0]); + } + + @Test + public void fusedAsync() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<Integer>().setInitialFusionMode(QueueFuseable.ANY); + + final int[] call = { 0, 0 }; + + UnicastProcessor<Integer> up = UnicastProcessor.create(); + + up + .doOnNext(new Consumer<Integer>() { + @Override + public void accept(Integer v) throws Exception { + call[0]++; + } + }) + .doOnComplete(new Action() { + @Override + public void run() throws Exception { + call[1]++; + } + }) + .subscribe(ts); + + TestHelper.emit(up, 1, 2, 3, 4, 5); + + ts.assertFuseable() + .assertFusionMode(QueueFuseable.ASYNC) + .assertResult(1, 2, 3, 4, 5); + + assertEquals(5, call[0]); + assertEquals(1, call[1]); + } + + @Test + public void fusedAsyncConditional() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<Integer>().setInitialFusionMode(QueueFuseable.ANY); + + final int[] call = { 0, 0 }; + + UnicastProcessor<Integer> up = UnicastProcessor.create(); + + up + .doOnNext(new Consumer<Integer>() { + @Override + public void accept(Integer v) throws Exception { + call[0]++; + } + }) + .doOnComplete(new Action() { + @Override + public void run() throws Exception { + call[1]++; + } + }) + .filter(Functions.alwaysTrue()) + .subscribe(ts); + + TestHelper.emit(up, 1, 2, 3, 4, 5); + + ts.assertFuseable() + .assertFusionMode(QueueFuseable.ASYNC) + .assertResult(1, 2, 3, 4, 5); + + assertEquals(5, call[0]); + assertEquals(1, call[1]); + } + + @Test + public void fusedAsyncConditional2() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<Integer>().setInitialFusionMode(QueueFuseable.ANY); + + final int[] call = { 0, 0 }; + + UnicastProcessor<Integer> up = UnicastProcessor.create(); + + up.hide() + .doOnNext(new Consumer<Integer>() { + @Override + public void accept(Integer v) throws Exception { + call[0]++; + } + }) + .doOnComplete(new Action() { + @Override + public void run() throws Exception { + call[1]++; + } + }) + .filter(Functions.alwaysTrue()) + .subscribe(ts); + + TestHelper.emit(up, 1, 2, 3, 4, 5); + + ts.assertFuseable() + .assertFusionMode(QueueFuseable.NONE) + .assertResult(1, 2, 3, 4, 5); + + assertEquals(5, call[0]); + assertEquals(1, call[1]); + } + + @Test + public void dispose() { + TestHelper.checkDisposed(Flowable.just(1).doOnEach(new TestSubscriber<>())); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeFlowable(new Function<Flowable<Object>, Flowable<Object>>() { + @Override + public Flowable<Object> apply(Flowable<Object> f) throws Exception { + return f.doOnEach(new TestSubscriber<>()); + } + }); + } + + @Test + public void doOnNextDoOnErrorFused() { + ConnectableFlowable<Integer> cf = Flowable.just(1) + .doOnNext(new Consumer<Integer>() { + @Override + public void accept(Integer v) throws Exception { + throw new TestException("First"); + } + }) + .doOnError(new Consumer<Throwable>() { + @Override + public void accept(Throwable e) throws Exception { + throw new TestException("Second"); + } + }) + .publish(); + + TestSubscriberEx<Integer> ts = cf.to(TestHelper.<Integer>testConsumer()); + cf.connect(); + + ts.assertFailure(CompositeException.class); + + TestHelper.assertError(ts, 0, TestException.class, "First"); + TestHelper.assertError(ts, 1, TestException.class, "Second"); + } + + @Test + public void doOnNextDoOnErrorCombinedFused() { + ConnectableFlowable<Integer> cf = Flowable.just(1) + .compose(new FlowableTransformer<Integer, Integer>() { + @Override + public Publisher<Integer> apply(Flowable<Integer> v) { + return new FlowableDoOnEach<>(v, + new Consumer<Integer>() { + @Override + public void accept(Integer v) throws Exception { + throw new TestException("First"); + } + }, + new Consumer<Throwable>() { + @Override + public void accept(Throwable e) throws Exception { + throw new TestException("Second"); + } + }, + Functions.EMPTY_ACTION + , + Functions.EMPTY_ACTION + ); + } + }) + .publish(); + + TestSubscriberEx<Integer> ts = cf.to(TestHelper.<Integer>testConsumer()); + cf.connect(); + + ts.assertFailure(CompositeException.class); + + TestHelper.assertError(ts, 0, TestException.class, "First"); + TestHelper.assertError(ts, 1, TestException.class, "Second"); + } + + @Test + public void doOnNextDoOnErrorFused2() { + ConnectableFlowable<Integer> cf = Flowable.just(1) + .doOnNext(new Consumer<Integer>() { + @Override + public void accept(Integer v) throws Exception { + throw new TestException("First"); + } + }) + .doOnError(new Consumer<Throwable>() { + @Override + public void accept(Throwable e) throws Exception { + throw new TestException("Second"); + } + }) + .doOnError(new Consumer<Throwable>() { + @Override + public void accept(Throwable e) throws Exception { + throw new TestException("Third"); + } + }) + .publish(); + + TestSubscriberEx<Integer> ts = cf.to(TestHelper.<Integer>testConsumer()); + cf.connect(); + + ts.assertFailure(CompositeException.class); + + TestHelper.assertError(ts, 0, TestException.class, "First"); + TestHelper.assertError(ts, 1, TestException.class, "Second"); + TestHelper.assertError(ts, 2, TestException.class, "Third"); + } + + @Test + public void doOnNextDoOnErrorFusedConditional() { + ConnectableFlowable<Integer> cf = Flowable.just(1) + .doOnNext(new Consumer<Integer>() { + @Override + public void accept(Integer v) throws Exception { + throw new TestException("First"); + } + }) + .doOnError(new Consumer<Throwable>() { + @Override + public void accept(Throwable e) throws Exception { + throw new TestException("Second"); + } + }) + .filter(Functions.alwaysTrue()) + .publish(); + + TestSubscriberEx<Integer> ts = cf.to(TestHelper.<Integer>testConsumer()); + cf.connect(); + + ts.assertFailure(CompositeException.class); + + TestHelper.assertError(ts, 0, TestException.class, "First"); + TestHelper.assertError(ts, 1, TestException.class, "Second"); + } + + @Test + public void doOnNextDoOnErrorFusedConditional2() { + ConnectableFlowable<Integer> cf = Flowable.just(1) + .doOnNext(new Consumer<Integer>() { + @Override + public void accept(Integer v) throws Exception { + throw new TestException("First"); + } + }) + .doOnError(new Consumer<Throwable>() { + @Override + public void accept(Throwable e) throws Exception { + throw new TestException("Second"); + } + }) + .doOnError(new Consumer<Throwable>() { + @Override + public void accept(Throwable e) throws Exception { + throw new TestException("Third"); + } + }) + .filter(Functions.alwaysTrue()) + .publish(); + + TestSubscriberEx<Integer> ts = cf.to(TestHelper.<Integer>testConsumer()); + cf.connect(); + + ts.assertFailure(CompositeException.class); + + TestHelper.assertError(ts, 0, TestException.class, "First"); + TestHelper.assertError(ts, 1, TestException.class, "Second"); + TestHelper.assertError(ts, 2, TestException.class, "Third"); + } + + @Test + public void doOnNextDoOnErrorCombinedFusedConditional() { + ConnectableFlowable<Integer> cf = Flowable.just(1) + .compose(new FlowableTransformer<Integer, Integer>() { + @Override + public Publisher<Integer> apply(Flowable<Integer> v) { + return new FlowableDoOnEach<>(v, + new Consumer<Integer>() { + @Override + public void accept(Integer v) throws Exception { + throw new TestException("First"); + } + }, + new Consumer<Throwable>() { + @Override + public void accept(Throwable e) throws Exception { + throw new TestException("Second"); + } + }, + Functions.EMPTY_ACTION + , + Functions.EMPTY_ACTION + ); + } + }) + .filter(Functions.alwaysTrue()) + .publish(); + + TestSubscriberEx<Integer> ts = cf.to(TestHelper.<Integer>testConsumer()); + cf.connect(); + + ts.assertFailure(CompositeException.class); + + TestHelper.assertError(ts, 0, TestException.class, "First"); + TestHelper.assertError(ts, 1, TestException.class, "Second"); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableDoOnLifecycleTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableDoOnLifecycleTest.java new file mode 100644 index 0000000000..e1df09d7ee --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableDoOnLifecycleTest.java @@ -0,0 +1,169 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import static org.junit.Assert.*; + +import java.util.List; + +import org.junit.Test; +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.internal.subscriptions.BooleanSubscription; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class FlowableDoOnLifecycleTest extends RxJavaTest { + + @Test + public void onSubscribeCrashed() { + Flowable.just(1) + .doOnLifecycle(new Consumer<Subscription>() { + @Override + public void accept(Subscription s) throws Exception { + throw new TestException(); + } + }, Functions.EMPTY_LONG_CONSUMER, Functions.EMPTY_ACTION) + .test() + .assertFailure(TestException.class); + } + + @Test + public void doubleOnSubscribe() { + final int[] calls = { 0, 0 }; + + TestHelper.checkDoubleOnSubscribeFlowable(new Function<Flowable<Object>, Publisher<Object>>() { + @Override + public Publisher<Object> apply(Flowable<Object> f) throws Exception { + return f + .doOnLifecycle(new Consumer<Subscription>() { + @Override + public void accept(Subscription s) throws Exception { + calls[0]++; + } + }, Functions.EMPTY_LONG_CONSUMER, new Action() { + @Override + public void run() throws Exception { + calls[1]++; + } + }); + } + }); + + assertEquals(2, calls[0]); + assertEquals(0, calls[1]); + } + + @Test + public void dispose() { + final int[] calls = { 0, 0 }; + + TestHelper.checkDisposed(Flowable.just(1) + .doOnLifecycle(new Consumer<Subscription>() { + @Override + public void accept(Subscription s) throws Exception { + calls[0]++; + } + }, Functions.EMPTY_LONG_CONSUMER, new Action() { + @Override + public void run() throws Exception { + calls[1]++; + } + }) + ); + + assertEquals(1, calls[0]); + assertEquals(1, calls[1]); + } + + @Test + public void requestCrashed() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + Flowable.just(1) + .doOnLifecycle(Functions.emptyConsumer(), + new LongConsumer() { + @Override + public void accept(long v) throws Exception { + throw new TestException(); + } + }, + Functions.EMPTY_ACTION) + .test() + .assertResult(1); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void cancelCrashed() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + Flowable.just(1) + .doOnLifecycle(Functions.emptyConsumer(), + Functions.EMPTY_LONG_CONSUMER, + new Action() { + @Override + public void run() throws Exception { + throw new TestException(); + } + }) + .take(1) + .test() + .assertResult(1); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void onSubscribeCrash() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + final BooleanSubscription bs = new BooleanSubscription(); + + new Flowable<Integer>() { + @Override + protected void subscribeActual(Subscriber<? super Integer> s) { + s.onSubscribe(bs); + s.onError(new TestException("Second")); + s.onComplete(); + } + } + .doOnSubscribe(new Consumer<Subscription>() { + @Override + public void accept(Subscription s) throws Exception { + throw new TestException("First"); + } + }) + .to(TestHelper.<Integer>testConsumer()) + .assertFailureAndMessage(TestException.class, "First"); + + assertTrue(bs.isCancelled()); + + TestHelper.assertUndeliverable(errors, 0, TestException.class, "Second"); + } finally { + RxJavaPlugins.reset(); + } + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableDoOnRequestTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableDoOnRequestTest.java new file mode 100644 index 0000000000..3e123089dc --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableDoOnRequestTest.java @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import static org.junit.Assert.*; + +import java.util.*; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.subscribers.DefaultSubscriber; + +public class FlowableDoOnRequestTest extends RxJavaTest { + + @Test + public void unsubscribeHappensAgainstParent() { + final AtomicBoolean unsubscribed = new AtomicBoolean(false); + Flowable.just(1).concatWith(Flowable.<Integer>never()) + // + .doOnCancel(new Action() { + @Override + public void run() { + unsubscribed.set(true); + } + }) + // + .doOnRequest(new LongConsumer() { + @Override + public void accept(long n) { + // do nothing + } + }) + // + .subscribe().dispose(); + assertTrue(unsubscribed.get()); + } + + @Test + public void doRequest() { + final List<Long> requests = new ArrayList<>(); + Flowable.range(1, 5) + // + .doOnRequest(new LongConsumer() { + @Override + public void accept(long n) { + requests.add(n); + } + }) + // + .subscribe(new DefaultSubscriber<Integer>() { + + @Override + public void onStart() { + request(3); + } + + @Override + public void onComplete() { + + } + + @Override + public void onError(Throwable e) { + + } + + @Override + public void onNext(Integer t) { + request(t); + } + }); + assertEquals(Arrays.asList(3L, 1L, 2L, 3L, 4L, 5L), requests); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableDoOnSubscribeTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableDoOnSubscribeTest.java new file mode 100644 index 0000000000..6dff7a5321 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableDoOnSubscribeTest.java @@ -0,0 +1,107 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import static org.junit.Assert.assertEquals; + +import java.util.concurrent.atomic.*; + +import org.junit.Test; +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.functions.Consumer; +import io.reactivex.rxjava3.internal.subscriptions.BooleanSubscription; + +public class FlowableDoOnSubscribeTest extends RxJavaTest { + + @Test + public void doOnSubscribe() throws Exception { + final AtomicInteger count = new AtomicInteger(); + Flowable<Integer> f = Flowable.just(1).doOnSubscribe(new Consumer<Subscription>() { + @Override + public void accept(Subscription s) { + count.incrementAndGet(); + } + }); + + f.subscribe(); + f.subscribe(); + f.subscribe(); + assertEquals(3, count.get()); + } + + @Test + public void doOnSubscribe2() throws Exception { + final AtomicInteger count = new AtomicInteger(); + Flowable<Integer> f = Flowable.just(1).doOnSubscribe(new Consumer<Subscription>() { + @Override + public void accept(Subscription s) { + count.incrementAndGet(); + } + }).take(1).doOnSubscribe(new Consumer<Subscription>() { + @Override + public void accept(Subscription s) { + count.incrementAndGet(); + } + }); + + f.subscribe(); + assertEquals(2, count.get()); + } + + @Test + public void doOnUnSubscribeWorksWithRefCount() throws Exception { + final AtomicInteger onSubscribed = new AtomicInteger(); + final AtomicInteger countBefore = new AtomicInteger(); + final AtomicInteger countAfter = new AtomicInteger(); + final AtomicReference<Subscriber<? super Integer>> sref = new AtomicReference<>(); + Flowable<Integer> f = Flowable.unsafeCreate(new Publisher<Integer>() { + + @Override + public void subscribe(Subscriber<? super Integer> s) { + s.onSubscribe(new BooleanSubscription()); + onSubscribed.incrementAndGet(); + sref.set(s); + } + + }).doOnSubscribe(new Consumer<Subscription>() { + @Override + public void accept(Subscription s) { + countBefore.incrementAndGet(); + } + }).publish().refCount() + .doOnSubscribe(new Consumer<Subscription>() { + @Override + public void accept(Subscription s) { + countAfter.incrementAndGet(); + } + }); + + f.subscribe(); + f.subscribe(); + f.subscribe(); + assertEquals(1, countBefore.get()); + assertEquals(1, onSubscribed.get()); + assertEquals(3, countAfter.get()); + sref.get().onComplete(); + f.subscribe(); + f.subscribe(); + f.subscribe(); + assertEquals(2, countBefore.get()); + assertEquals(2, onSubscribed.get()); + assertEquals(6, countAfter.get()); + } + +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableDoOnUnsubscribeTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableDoOnUnsubscribeTest.java new file mode 100644 index 0000000000..d060928dee --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableDoOnUnsubscribeTest.java @@ -0,0 +1,172 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import static org.junit.Assert.assertEquals; + +import java.util.*; +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.*; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.processors.BehaviorProcessor; +import io.reactivex.rxjava3.subscribers.TestSubscriber; + +public class FlowableDoOnUnsubscribeTest extends RxJavaTest { + + @Test + public void doOnUnsubscribe() throws Exception { + int subCount = 3; + final CountDownLatch upperLatch = new CountDownLatch(subCount); + final CountDownLatch lowerLatch = new CountDownLatch(subCount); + final CountDownLatch onNextLatch = new CountDownLatch(subCount); + + final AtomicInteger upperCount = new AtomicInteger(); + final AtomicInteger lowerCount = new AtomicInteger(); + Flowable<Long> longs = Flowable + // The stream needs to be infinite to ensure the stream does not terminate + // before it is unsubscribed + .interval(50, TimeUnit.MILLISECONDS) + .doOnCancel(new Action() { + @Override + public void run() { + // Test that upper stream will be notified for un-subscription + // from a child subscriber + upperLatch.countDown(); + upperCount.incrementAndGet(); + } + }) + .doOnNext(new Consumer<Long>() { + @Override + public void accept(Long aLong) { + // Ensure there is at least some onNext events before un-subscription happens + onNextLatch.countDown(); + } + }) + .doOnCancel(new Action() { + @Override + public void run() { + // Test that lower stream will be notified for a direct un-subscription + lowerLatch.countDown(); + lowerCount.incrementAndGet(); + } + }); + + List<Disposable> subscriptions = new ArrayList<>(); + List<TestSubscriber<Long>> subscribers = new ArrayList<>(); + + for (int i = 0; i < subCount; ++i) { + TestSubscriber<Long> subscriber = new TestSubscriber<>(); + subscriptions.add(Disposable.fromSubscription(subscriber)); + longs.subscribe(subscriber); + subscribers.add(subscriber); + } + + onNextLatch.await(); + for (int i = 0; i < subCount; ++i) { + subscriptions.get(i).dispose(); + // Test that unsubscribe() method is not affected in any way + } + + upperLatch.await(); + lowerLatch.await(); + assertEquals(String.format("There should exactly %d un-subscription events for upper stream", subCount), subCount, upperCount.get()); + assertEquals(String.format("There should exactly %d un-subscription events for lower stream", subCount), subCount, lowerCount.get()); + } + + @Test + public void doOnUnSubscribeWorksWithRefCount() throws Exception { + int subCount = 3; + final CountDownLatch upperLatch = new CountDownLatch(1); + final CountDownLatch lowerLatch = new CountDownLatch(1); + final CountDownLatch onNextLatch = new CountDownLatch(subCount); + + final AtomicInteger upperCount = new AtomicInteger(); + final AtomicInteger lowerCount = new AtomicInteger(); + Flowable<Long> longs = Flowable + // The stream needs to be infinite to ensure the stream does not terminate + // before it is unsubscribed + .interval(50, TimeUnit.MILLISECONDS) + .doOnCancel(new Action() { + @Override + public void run() { + // Test that upper stream will be notified for un-subscription + upperLatch.countDown(); + upperCount.incrementAndGet(); + } + }) + .doOnNext(new Consumer<Long>() { + @Override + public void accept(Long aLong) { + // Ensure there is at least some onNext events before un-subscription happens + onNextLatch.countDown(); + } + }) + .doOnCancel(new Action() { + @Override + public void run() { + // Test that lower stream will be notified for un-subscription + lowerLatch.countDown(); + lowerCount.incrementAndGet(); + } + }) + .publish() + .refCount(); + + List<Disposable> subscriptions = new ArrayList<>(); + List<TestSubscriber<Long>> subscribers = new ArrayList<>(); + + for (int i = 0; i < subCount; ++i) { + TestSubscriber<Long> subscriber = new TestSubscriber<>(); + longs.subscribe(subscriber); + subscriptions.add(Disposable.fromSubscription(subscriber)); + subscribers.add(subscriber); + } + + onNextLatch.await(); + for (int i = 0; i < subCount; ++i) { + subscriptions.get(i).dispose(); + // Test that unsubscribe() method is not affected in any way + } + + upperLatch.await(); + lowerLatch.await(); + assertEquals("There should exactly 1 un-subscription events for upper stream", 1, upperCount.get()); + assertEquals("There should exactly 1 un-subscription events for lower stream", 1, lowerCount.get()); + } + + @Test + public void noReentrantDispose() { + + final AtomicInteger cancelCalled = new AtomicInteger(); + + final BehaviorProcessor<Integer> p = BehaviorProcessor.create(); + p.doOnCancel(new Action() { + @Override + public void run() throws Exception { + cancelCalled.incrementAndGet(); + p.onNext(2); + } + }) + .firstOrError() + .subscribe() + .dispose(); + + assertEquals(1, cancelCalled.get()); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableElementAtTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableElementAtTest.java new file mode 100644 index 0000000000..0e36e075a8 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableElementAtTest.java @@ -0,0 +1,390 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import static org.junit.Assert.*; + +import java.util.*; + +import io.reactivex.rxjava3.disposables.Disposable; +import org.junit.Test; +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.core.Observer; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.subscriptions.BooleanSubscription; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.processors.PublishProcessor; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class FlowableElementAtTest extends RxJavaTest { + + @Test + public void elementAtFlowable() { + assertEquals(2, Flowable.fromArray(1, 2).elementAt(1).toFlowable().blockingSingle() + .intValue()); + } + + @Test(expected = IndexOutOfBoundsException.class) + public void elementAtWithMinusIndexFlowable() { + Flowable.fromArray(1, 2).elementAt(-1); + } + + @Test + public void elementAtWithIndexOutOfBoundsFlowable() { + assertEquals(-100, Flowable.fromArray(1, 2).elementAt(2).toFlowable().blockingFirst(-100).intValue()); + } + + @Test + public void elementAtOrDefaultFlowable() { + assertEquals(2, Flowable.fromArray(1, 2).elementAt(1, 0).toFlowable().blockingSingle().intValue()); + } + + @Test + public void elementAtOrDefaultWithIndexOutOfBoundsFlowable() { + assertEquals(0, Flowable.fromArray(1, 2).elementAt(2, 0).toFlowable().blockingSingle().intValue()); + } + + @Test(expected = IndexOutOfBoundsException.class) + public void elementAtOrDefaultWithMinusIndexFlowable() { + Flowable.fromArray(1, 2).elementAt(-1, 0); + } + + @Test + public void elementAt() { + assertEquals(2, Flowable.fromArray(1, 2).elementAt(1).blockingGet() + .intValue()); + } + + @Test + public void elementAtConstrainsUpstreamRequests() { + final List<Long> requests = new ArrayList<>(); + Flowable.fromArray(1, 2, 3, 4) + .doOnRequest(new LongConsumer() { + @Override + public void accept(long n) throws Throwable { + requests.add(n); + } + }) + .elementAt(2) + .blockingGet() + .intValue(); + assertEquals(Arrays.asList(3L), requests); + } + + @Test + public void elementAtWithDefaultConstrainsUpstreamRequests() { + final List<Long> requests = new ArrayList<>(); + Flowable.fromArray(1, 2, 3, 4) + .doOnRequest(new LongConsumer() { + @Override + public void accept(long n) throws Throwable { + requests.add(n); + } + }) + .elementAt(2, 100) + .blockingGet() + .intValue(); + assertEquals(Arrays.asList(3L), requests); + } + + @Test(expected = IndexOutOfBoundsException.class) + public void elementAtWithMinusIndex() { + Flowable.fromArray(1, 2).elementAt(-1); + } + + @Test + public void elementAtWithIndexOutOfBounds() { + assertNull(Flowable.fromArray(1, 2).elementAt(2).blockingGet()); + } + + @Test + public void elementAtOrDefault() { + assertEquals(2, Flowable.fromArray(1, 2).elementAt(1, 0).blockingGet().intValue()); + } + + @Test + public void elementAtOrDefaultWithIndexOutOfBounds() { + assertEquals(0, Flowable.fromArray(1, 2).elementAt(2, 0).blockingGet().intValue()); + } + + @Test(expected = IndexOutOfBoundsException.class) + public void elementAtOrDefaultWithMinusIndex() { + Flowable.fromArray(1, 2).elementAt(-1, 0); + } + + @Test(expected = IndexOutOfBoundsException.class) + public void elementAtOrErrorNegativeIndex() { + Flowable.empty() + .elementAtOrError(-1); + } + + @Test + public void elementAtOrErrorNoElement() { + Flowable.empty() + .elementAtOrError(0) + .test() + .assertNoValues() + .assertError(NoSuchElementException.class); + } + + @Test + public void elementAtOrErrorOneElement() { + Flowable.just(1) + .elementAtOrError(0) + .test() + .assertNoErrors() + .assertValue(1); + } + + @Test + public void elementAtOrErrorMultipleElements() { + Flowable.just(1, 2, 3) + .elementAtOrError(1) + .test() + .assertNoErrors() + .assertValue(2); + } + + @Test + public void elementAtOrErrorInvalidIndex() { + Flowable.just(1, 2, 3) + .elementAtOrError(3) + .test() + .assertNoValues() + .assertError(NoSuchElementException.class); + } + + @Test + public void elementAtOrErrorError() { + Flowable.error(new RuntimeException("error")) + .elementAtOrError(0) + .to(TestHelper.testConsumer()) + .assertNoValues() + .assertErrorMessage("error") + .assertError(RuntimeException.class); + } + + @Test + public void elementAtIndex0OnEmptySource() { + Flowable.empty() + .elementAt(0) + .test() + .assertResult(); + } + + @Test + public void elementAtIndex0WithDefaultOnEmptySource() { + Flowable.empty() + .elementAt(0, 5) + .test() + .assertResult(5); + } + + @Test + public void elementAtIndex1OnEmptySource() { + Flowable.empty() + .elementAt(1) + .test() + .assertResult(); + } + + @Test + public void elementAtIndex1WithDefaultOnEmptySource() { + Flowable.empty() + .elementAt(1, 10) + .test() + .assertResult(10); + } + + @Test + public void elementAtOrErrorIndex1OnEmptySource() { + Flowable.empty() + .elementAtOrError(1) + .test() + .assertFailure(NoSuchElementException.class); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeFlowable(new Function<Flowable<Object>, Publisher<Object>>() { + @Override + public Publisher<Object> apply(Flowable<Object> f) throws Exception { + return f.elementAt(0).toFlowable(); + } + }); + + TestHelper.checkDoubleOnSubscribeFlowableToMaybe(new Function<Flowable<Object>, Maybe<Object>>() { + @Override + public Maybe<Object> apply(Flowable<Object> f) throws Exception { + return f.elementAt(0); + } + }); + + TestHelper.checkDoubleOnSubscribeFlowableToSingle(new Function<Flowable<Object>, Single<Object>>() { + @Override + public Single<Object> apply(Flowable<Object> f) throws Exception { + return f.elementAt(0, 1); + } + }); + } + + @Test + public void elementAtIndex1WithDefaultOnEmptySourceObservable() { + Flowable.empty() + .elementAt(1, 10) + .toFlowable() + .test() + .assertResult(10); + } + + @Test + public void errorFlowable() { + Flowable.error(new TestException()) + .elementAt(1, 10) + .toFlowable() + .test() + .assertFailure(TestException.class); + } + + @Test + public void error() { + Flowable.error(new TestException()) + .elementAt(1, 10) + .test() + .assertFailure(TestException.class); + + Flowable.error(new TestException()) + .elementAt(1) + .test() + .assertFailure(TestException.class); + } + + @Test + public void badSource() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + new Flowable<Integer>() { + @Override + protected void subscribeActual(Subscriber<? super Integer> subscriber) { + subscriber.onSubscribe(new BooleanSubscription()); + + subscriber.onNext(1); + subscriber.onNext(2); + subscriber.onError(new TestException()); + subscriber.onComplete(); + } + } + .elementAt(0) + .toFlowable() + .test() + .assertResult(1); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + + TestHelper.checkBadSourceFlowable(new Function<Flowable<Integer>, Object>() { + @Override + public Object apply(Flowable<Integer> f) throws Exception { + return f.elementAt(0); + } + }, false, null, 1); + + TestHelper.checkBadSourceFlowable(new Function<Flowable<Integer>, Object>() { + @Override + public Object apply(Flowable<Integer> f) throws Exception { + return f.elementAt(0, 1); + } + }, false, null, 1, 1); + + TestHelper.checkBadSourceFlowable(new Function<Flowable<Integer>, Object>() { + @Override + public Object apply(Flowable<Integer> f) throws Exception { + return f.elementAt(0).toFlowable(); + } + }, false, null, 1); + + TestHelper.checkBadSourceFlowable(new Function<Flowable<Integer>, Object>() { + @Override + public Object apply(Flowable<Integer> f) throws Exception { + return f.elementAt(0, 1).toFlowable(); + } + }, false, null, 1, 1); + } + + @Test + public void dispose() { + TestHelper.checkDisposed(PublishProcessor.create().elementAt(0).toFlowable()); + TestHelper.checkDisposed(PublishProcessor.create().elementAt(0, 1).toFlowable()); + + TestHelper.checkDisposed(PublishProcessor.create().elementAt(0)); + TestHelper.checkDisposed(PublishProcessor.create().elementAt(0, 1)); + } + + @Test + public void badSourceObservable() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + new Observable<Integer>() { + @Override + protected void subscribeActual(Observer<? super Integer> observer) { + observer.onSubscribe(Disposable.empty()); + + observer.onNext(1); + observer.onNext(2); + observer.onError(new TestException()); + observer.onComplete(); + } + } + .elementAt(0) + .toFlowable() + .test() + .assertResult(1); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void badSource2() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + new Flowable<Integer>() { + @Override + protected void subscribeActual(Subscriber<? super Integer> subscriber) { + subscriber.onSubscribe(new BooleanSubscription()); + + subscriber.onNext(1); + subscriber.onNext(2); + subscriber.onError(new TestException()); + subscriber.onComplete(); + } + } + .elementAt(0, 1) + .test() + .assertResult(1); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableFilterTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableFilterTest.java new file mode 100644 index 0000000000..4b62f1e2a4 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableFilterTest.java @@ -0,0 +1,607 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import java.io.IOException; +import java.util.List; +import java.util.concurrent.CountDownLatch; + +import org.junit.*; +import org.mockito.Mockito; +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.internal.subscriptions.BooleanSubscription; +import io.reactivex.rxjava3.operators.ConditionalSubscriber; +import io.reactivex.rxjava3.operators.QueueFuseable; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.processors.*; +import io.reactivex.rxjava3.subscribers.TestSubscriber; +import io.reactivex.rxjava3.testsupport.*; + +public class FlowableFilterTest extends RxJavaTest { + + @Test + public void filter() { + Flowable<String> w = Flowable.just("one", "two", "three"); + Flowable<String> flowable = w.filter(new Predicate<String>() { + + @Override + public boolean test(String t1) { + return t1.equals("two"); + } + }); + + Subscriber<String> subscriber = TestHelper.mockSubscriber(); + + flowable.subscribe(subscriber); + + verify(subscriber, Mockito.never()).onNext("one"); + verify(subscriber, times(1)).onNext("two"); + verify(subscriber, Mockito.never()).onNext("three"); + verify(subscriber, Mockito.never()).onError(any(Throwable.class)); + verify(subscriber, times(1)).onComplete(); + } + + /** + * Make sure we are adjusting subscriber.request() for filtered items. + * @throws InterruptedException if the test is interrupted + * @throws InterruptedException if the test is interrupted + */ + @Test + public void withBackpressure() throws InterruptedException { + Flowable<String> w = Flowable.just("one", "two", "three"); + Flowable<String> f = w.filter(new Predicate<String>() { + + @Override + public boolean test(String t1) { + return t1.equals("three"); + } + }); + + final CountDownLatch latch = new CountDownLatch(1); + TestSubscriber<String> ts = new TestSubscriber<String>() { + + @Override + public void onComplete() { + System.out.println("onComplete"); + latch.countDown(); + } + + @Override + public void onError(Throwable e) { + e.printStackTrace(); + latch.countDown(); + } + + @Override + public void onNext(String t) { + System.out.println("Received: " + t); + // request more each time we receive + request(1); + } + + }; + // this means it will only request "one" and "two", expecting to receive them before requesting more + ts.request(2); + + f.subscribe(ts); + + // this will wait forever unless OperatorTake handles the request(n) on filtered items + latch.await(); + } + + /** + * Make sure we are adjusting subscriber.request() for filtered items. + * @throws InterruptedException if the test is interrupted + */ + @Test + public void withBackpressure2() throws InterruptedException { + Flowable<Integer> w = Flowable.range(1, Flowable.bufferSize() * 2); + Flowable<Integer> f = w.filter(new Predicate<Integer>() { + + @Override + public boolean test(Integer t1) { + return t1 > 100; + } + }); + + final CountDownLatch latch = new CountDownLatch(1); + final TestSubscriber<Integer> ts = new TestSubscriber<Integer>() { + + @Override + public void onComplete() { + System.out.println("onComplete"); + latch.countDown(); + } + + @Override + public void onError(Throwable e) { + e.printStackTrace(); + latch.countDown(); + } + + @Override + public void onNext(Integer t) { + System.out.println("Received: " + t); + // request more each time we receive + request(1); + } + }; + // this means it will only request 1 item and expect to receive more + ts.request(1); + + f.subscribe(ts); + + // this will wait forever unless OperatorTake handles the request(n) on filtered items + latch.await(); + } + + @Test + public void functionCrashUnsubscribes() { + + PublishProcessor<Integer> pp = PublishProcessor.create(); + + TestSubscriber<Integer> ts = new TestSubscriber<>(); + + pp.filter(new Predicate<Integer>() { + @Override + public boolean test(Integer v) { + throw new TestException(); + } + }).subscribe(ts); + + Assert.assertTrue("Not subscribed?", pp.hasSubscribers()); + + pp.onNext(1); + + Assert.assertFalse("Subscribed?", pp.hasSubscribers()); + + ts.assertError(TestException.class); + } + + @Test + public void doesntRequestOnItsOwn() { + TestSubscriber<Integer> ts = TestSubscriber.create(0L); + + Flowable.range(1, 10).filter(Functions.alwaysTrue()).subscribe(ts); + + ts.assertNoValues(); + + ts.request(10); + + ts.assertValues(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + ts.assertNoErrors(); + ts.assertComplete(); + } + + @Test + public void conditional() { + Flowable.range(1, 5) + .filter(Functions.alwaysTrue()) + .filter(Functions.alwaysTrue()) + .test() + .assertResult(1, 2, 3, 4, 5); + } + + @Test + public void conditionalNone() { + Flowable.range(1, 5) + .filter(Functions.alwaysTrue()) + .filter(Functions.alwaysFalse()) + .test() + .assertResult(); + } + + @Test + public void conditionalNone2() { + Flowable.range(1, 5) + .filter(Functions.alwaysFalse()) + .filter(Functions.alwaysFalse()) + .test() + .assertResult(); + } + + @Test + public void conditionalFusedSync() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<Integer>().setInitialFusionMode(QueueFuseable.ANY); + + Flowable.range(1, 5) + .filter(Functions.alwaysTrue()) + .filter(Functions.alwaysTrue()) + .subscribe(ts); + + ts.assertFuseable() + .assertFusionMode(QueueFuseable.SYNC) + .assertResult(1, 2, 3, 4, 5); + } + + @Test + public void conditionalFusedSync2() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<Integer>().setInitialFusionMode(QueueFuseable.ANY); + + Flowable.range(1, 5) + .filter(Functions.alwaysFalse()) + .filter(Functions.alwaysFalse()) + .subscribe(ts); + + ts.assertFuseable() + .assertFusionMode(QueueFuseable.SYNC) + .assertResult(); + } + + @Test + public void conditionalFusedAsync() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<Integer>().setInitialFusionMode(QueueFuseable.ANY); + + UnicastProcessor<Integer> up = UnicastProcessor.create(); + + up + .filter(Functions.alwaysTrue()) + .filter(Functions.alwaysTrue()) + .subscribe(ts); + + up.onNext(1); + up.onNext(2); + up.onNext(3); + up.onNext(4); + up.onNext(5); + up.onComplete(); + + ts.assertFuseable() + .assertFusionMode(QueueFuseable.ASYNC) + .assertResult(1, 2, 3, 4, 5); + } + + @Test + public void conditionalFusedNoneAsync() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<Integer>().setInitialFusionMode(QueueFuseable.ANY); + + UnicastProcessor<Integer> up = UnicastProcessor.create(); + + up + .filter(Functions.alwaysTrue()) + .filter(Functions.alwaysFalse()) + .subscribe(ts); + + up.onNext(1); + up.onNext(2); + up.onNext(3); + up.onNext(4); + up.onNext(5); + up.onComplete(); + + ts.assertFuseable() + .assertFusionMode(QueueFuseable.ASYNC) + .assertResult(); + } + + @Test + public void conditionalFusedNoneAsync2() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<Integer>().setInitialFusionMode(QueueFuseable.ANY); + + UnicastProcessor<Integer> up = UnicastProcessor.create(); + + up + .filter(Functions.alwaysFalse()) + .filter(Functions.alwaysFalse()) + .subscribe(ts); + + up.onNext(1); + up.onNext(2); + up.onNext(3); + up.onNext(4); + up.onNext(5); + up.onComplete(); + + ts.assertFuseable() + .assertFusionMode(QueueFuseable.ASYNC) + .assertResult(); + } + + @Test + public void sourceIgnoresCancelConditional() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + + try { + Flowable.fromPublisher(new Publisher<Integer>() { + @Override + public void subscribe(Subscriber<? super Integer> s) { + ConditionalSubscriber<? super Integer> cs = (ConditionalSubscriber<? super Integer>)s; + cs.onSubscribe(new BooleanSubscription()); + cs.tryOnNext(1); + cs.tryOnNext(2); + cs.onError(new IOException()); + cs.onComplete(); + } + }) + .filter(new Predicate<Integer>() { + @Override + public boolean test(Integer v) throws Exception { + return true; + } + }) + .filter(new Predicate<Integer>() { + @Override + public boolean test(Integer v) throws Exception { + throw new TestException(); + } + }) + .test() + .assertFailure(TestException.class); + + TestHelper.assertUndeliverable(errors, 0, IOException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void mapCrashesBeforeFilter() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + + try { + Flowable.fromPublisher(new Publisher<Integer>() { + @Override + public void subscribe(Subscriber<? super Integer> s) { + s.onSubscribe(new BooleanSubscription()); + s.onNext(1); + s.onNext(2); + s.onError(new IOException()); + s.onComplete(); + } + }) + .map(new Function<Integer, Integer>() { + @Override + public Integer apply(Integer v) throws Exception { + throw new TestException(); + } + }) + .filter(new Predicate<Integer>() { + @Override + public boolean test(Integer v) throws Exception { + return true; + } + }) + .test() + .assertFailure(TestException.class); + + TestHelper.assertUndeliverable(errors, 0, IOException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void syncFused() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<Integer>().setInitialFusionMode(QueueFuseable.ANY); + + Flowable.range(1, 5) + .filter(Functions.alwaysTrue()) + .subscribe(ts); + + ts.assertFuseable() + .assertFusionMode(QueueFuseable.SYNC) + .assertResult(1, 2, 3, 4, 5); + } + + @Test + public void syncNoneFused() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<Integer>().setInitialFusionMode(QueueFuseable.ANY); + + Flowable.range(1, 5) + .filter(Functions.alwaysFalse()) + .subscribe(ts); + + ts.assertFuseable() + .assertFusionMode(QueueFuseable.SYNC) + .assertResult(); + } + + @Test + public void syncNoneFused2() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<Integer>().setInitialFusionMode(QueueFuseable.ANY); + + Flowable.range(1, 5) + .filter(Functions.alwaysFalse()) + .filter(Functions.alwaysFalse()) + .subscribe(ts); + + ts.assertFuseable() + .assertFusionMode(QueueFuseable.SYNC) + .assertResult(); + } + + @Test + public void sourceIgnoresCancel() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + + try { + Flowable.fromPublisher(new Publisher<Integer>() { + @Override + public void subscribe(Subscriber<? super Integer> s) { + s.onSubscribe(new BooleanSubscription()); + s.onNext(1); + s.onNext(2); + s.onError(new IOException()); + s.onComplete(); + } + }) + .filter(new Predicate<Integer>() { + @Override + public boolean test(Integer v) throws Exception { + throw new TestException(); + } + }) + .test() + .assertFailure(TestException.class); + + TestHelper.assertUndeliverable(errors, 0, IOException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void sourceIgnoresCancel2() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + + try { + Flowable.fromPublisher(new Publisher<Integer>() { + @Override + public void subscribe(Subscriber<? super Integer> s) { + s.onSubscribe(new BooleanSubscription()); + s.onNext(1); + s.onNext(2); + s.onError(new IOException()); + s.onComplete(); + } + }) + .filter(new Predicate<Integer>() { + @Override + public boolean test(Integer v) throws Exception { + throw new TestException(); + } + }) + .filter(Functions.alwaysTrue()) + .test() + .assertFailure(TestException.class); + + TestHelper.assertUndeliverable(errors, 0, IOException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void sourceIgnoresCancelConditional2() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + + try { + Flowable.fromPublisher(new Publisher<Integer>() { + @Override + public void subscribe(Subscriber<? super Integer> s) { + ConditionalSubscriber<? super Integer> cs = (ConditionalSubscriber<? super Integer>)s; + cs.onSubscribe(new BooleanSubscription()); + cs.tryOnNext(1); + cs.tryOnNext(2); + cs.onError(new IOException()); + cs.onComplete(); + } + }) + .filter(new Predicate<Integer>() { + @Override + public boolean test(Integer v) throws Exception { + throw new TestException(); + } + }) + .filter(Functions.alwaysTrue()) + .test() + .assertFailure(TestException.class); + + TestHelper.assertUndeliverable(errors, 0, IOException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void dispose() { + TestHelper.checkDisposed(Flowable.range(1, 5).filter(Functions.alwaysTrue())); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeFlowable(new Function<Flowable<Object>, Flowable<Object>>() { + @Override + public Flowable<Object> apply(Flowable<Object> f) throws Exception { + return f.filter(Functions.alwaysTrue()); + } + }); + } + + @Test + public void fusedSync() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<Integer>().setInitialFusionMode(QueueFuseable.ANY); + + Flowable.range(1, 5) + .filter(new Predicate<Integer>() { + @Override + public boolean test(Integer v) throws Exception { + return v % 2 == 0; + } + }) + .subscribe(ts); + + ts.assertFusionMode(QueueFuseable.SYNC) + .assertResult(2, 4); + } + + @Test + public void fusedAsync() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<Integer>().setInitialFusionMode(QueueFuseable.ANY); + + UnicastProcessor<Integer> up = UnicastProcessor.create(); + + up + .filter(new Predicate<Integer>() { + @Override + public boolean test(Integer v) throws Exception { + return v % 2 == 0; + } + }) + .subscribe(ts); + + TestHelper.emit(up, 1, 2, 3, 4, 5); + + ts.assertFusionMode(QueueFuseable.ASYNC) + .assertResult(2, 4); + } + + @Test + public void fusedReject() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<Integer>().setInitialFusionMode(QueueFuseable.ANY | QueueFuseable.BOUNDARY); + + Flowable.range(1, 5) + .filter(new Predicate<Integer>() { + @Override + public boolean test(Integer v) throws Exception { + return v % 2 == 0; + } + }) + .subscribe(ts); + + ts.assertFusionMode(QueueFuseable.NONE) + .assertResult(2, 4); + } + + @Test + public void filterThrows() { + Flowable.range(1, 5) + .filter(new Predicate<Integer>() { + @Override + public boolean test(Integer v) throws Exception { + throw new TestException(); + } + }) + .test() + .assertFailure(TestException.class); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableFirstTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableFirstTest.java new file mode 100644 index 0000000000..b6100ddeb3 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableFirstTest.java @@ -0,0 +1,590 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +import java.util.NoSuchElementException; + +import org.junit.*; +import org.mockito.InOrder; +import org.reactivestreams.Subscriber; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.functions.Predicate; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class FlowableFirstTest extends RxJavaTest { + + Subscriber<String> w; + + SingleObserver<Object> wo; + + MaybeObserver<Object> wm; + + private static final Predicate<String> IS_D = new Predicate<String>() { + @Override + public boolean test(String value) { + return "d".equals(value); + } + }; + + @Before + public void before() { + w = TestHelper.mockSubscriber(); + wo = TestHelper.mockSingleObserver(); + wm = TestHelper.mockMaybeObserver(); + } + + @Test + public void firstOrElseOfNoneFlowable() { + Flowable<String> src = Flowable.empty(); + src.first("default").toFlowable().subscribe(w); + + verify(w, times(1)).onNext(anyString()); + verify(w, times(1)).onNext("default"); + verify(w, never()).onError(any(Throwable.class)); + verify(w, times(1)).onComplete(); + } + + @Test + public void firstOrElseOfSomeFlowable() { + Flowable<String> src = Flowable.just("a", "b", "c"); + src.first("default").toFlowable().subscribe(w); + + verify(w, times(1)).onNext(anyString()); + verify(w, times(1)).onNext("a"); + verify(w, never()).onError(any(Throwable.class)); + verify(w, times(1)).onComplete(); + } + + @Test + public void firstOrElseWithPredicateOfNoneMatchingThePredicateFlowable() { + Flowable<String> src = Flowable.just("a", "b", "c"); + src.filter(IS_D).first("default").toFlowable().subscribe(w); + + verify(w, times(1)).onNext(anyString()); + verify(w, times(1)).onNext("default"); + verify(w, never()).onError(any(Throwable.class)); + verify(w, times(1)).onComplete(); + } + + @Test + public void firstOrElseWithPredicateOfSomeFlowable() { + Flowable<String> src = Flowable.just("a", "b", "c", "d", "e", "f"); + src.filter(IS_D).first("default").toFlowable().subscribe(w); + + verify(w, times(1)).onNext(anyString()); + verify(w, times(1)).onNext("d"); + verify(w, never()).onError(any(Throwable.class)); + verify(w, times(1)).onComplete(); + } + + @Test + public void firstFlowable() { + Flowable<Integer> flowable = Flowable.just(1, 2, 3).firstElement().toFlowable(); + + Subscriber<Integer> subscriber = TestHelper.mockSubscriber(); + flowable.subscribe(subscriber); + + InOrder inOrder = inOrder(subscriber); + inOrder.verify(subscriber, times(1)).onNext(1); + inOrder.verify(subscriber, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void firstWithOneElementFlowable() { + Flowable<Integer> flowable = Flowable.just(1).firstElement().toFlowable(); + + Subscriber<Integer> subscriber = TestHelper.mockSubscriber(); + flowable.subscribe(subscriber); + + InOrder inOrder = inOrder(subscriber); + inOrder.verify(subscriber, times(1)).onNext(1); + inOrder.verify(subscriber, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void firstWithEmptyFlowable() { + Flowable<Integer> flowable = Flowable.<Integer> empty().firstElement().toFlowable(); + + Subscriber<Integer> subscriber = TestHelper.mockSubscriber(); + flowable.subscribe(subscriber); + + InOrder inOrder = inOrder(subscriber); + inOrder.verify(subscriber).onComplete(); + inOrder.verify(subscriber, never()).onError(any(Throwable.class)); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void firstWithPredicateFlowable() { + Flowable<Integer> flowable = Flowable.just(1, 2, 3, 4, 5, 6) + .filter(new Predicate<Integer>() { + @Override + public boolean test(Integer t1) { + return t1 % 2 == 0; + } + }) + .firstElement().toFlowable(); + + Subscriber<Integer> subscriber = TestHelper.mockSubscriber(); + flowable.subscribe(subscriber); + + InOrder inOrder = inOrder(subscriber); + inOrder.verify(subscriber, times(1)).onNext(2); + inOrder.verify(subscriber, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void firstWithPredicateAndOneElementFlowable() { + Flowable<Integer> flowable = Flowable.just(1, 2) + .filter(new Predicate<Integer>() { + @Override + public boolean test(Integer t1) { + return t1 % 2 == 0; + } + }) + .firstElement().toFlowable(); + + Subscriber<Integer> subscriber = TestHelper.mockSubscriber(); + flowable.subscribe(subscriber); + + InOrder inOrder = inOrder(subscriber); + inOrder.verify(subscriber, times(1)).onNext(2); + inOrder.verify(subscriber, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void firstWithPredicateAndEmptyFlowable() { + Flowable<Integer> flowable = Flowable.just(1) + .filter(new Predicate<Integer>() { + @Override + public boolean test(Integer t1) { + return t1 % 2 == 0; + } + }) + .firstElement().toFlowable(); + + Subscriber<Integer> subscriber = TestHelper.mockSubscriber(); + flowable.subscribe(subscriber); + + InOrder inOrder = inOrder(subscriber); + inOrder.verify(subscriber).onComplete(); + inOrder.verify(subscriber, never()).onError(any(Throwable.class)); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void firstOrDefaultFlowable() { + Flowable<Integer> flowable = Flowable.just(1, 2, 3) + .first(4).toFlowable(); + + Subscriber<Integer> subscriber = TestHelper.mockSubscriber(); + flowable.subscribe(subscriber); + + InOrder inOrder = inOrder(subscriber); + inOrder.verify(subscriber, times(1)).onNext(1); + inOrder.verify(subscriber, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void firstOrDefaultWithOneElementFlowable() { + Flowable<Integer> flowable = Flowable.just(1).first(2).toFlowable(); + + Subscriber<Integer> subscriber = TestHelper.mockSubscriber(); + flowable.subscribe(subscriber); + + InOrder inOrder = inOrder(subscriber); + inOrder.verify(subscriber, times(1)).onNext(1); + inOrder.verify(subscriber, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void firstOrDefaultWithEmptyFlowable() { + Flowable<Integer> flowable = Flowable.<Integer> empty() + .first(1).toFlowable(); + + Subscriber<Integer> subscriber = TestHelper.mockSubscriber(); + flowable.subscribe(subscriber); + + InOrder inOrder = inOrder(subscriber); + inOrder.verify(subscriber, times(1)).onNext(1); + inOrder.verify(subscriber, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void firstOrDefaultWithPredicateFlowable() { + Flowable<Integer> flowable = Flowable.just(1, 2, 3, 4, 5, 6) + .filter(new Predicate<Integer>() { + @Override + public boolean test(Integer t1) { + return t1 % 2 == 0; + } + }) + .first(8).toFlowable(); + + Subscriber<Integer> subscriber = TestHelper.mockSubscriber(); + flowable.subscribe(subscriber); + + InOrder inOrder = inOrder(subscriber); + inOrder.verify(subscriber, times(1)).onNext(2); + inOrder.verify(subscriber, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void firstOrDefaultWithPredicateAndOneElementFlowable() { + Flowable<Integer> flowable = Flowable.just(1, 2) + .filter(new Predicate<Integer>() { + @Override + public boolean test(Integer t1) { + return t1 % 2 == 0; + } + }) + .first(4).toFlowable(); + + Subscriber<Integer> subscriber = TestHelper.mockSubscriber(); + flowable.subscribe(subscriber); + + InOrder inOrder = inOrder(subscriber); + inOrder.verify(subscriber, times(1)).onNext(2); + inOrder.verify(subscriber, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void firstOrDefaultWithPredicateAndEmptyFlowable() { + Flowable<Integer> flowable = Flowable.just(1) + .filter(new Predicate<Integer>() { + @Override + public boolean test(Integer t1) { + return t1 % 2 == 0; + } + }) + .first(2).toFlowable(); + + Subscriber<Integer> subscriber = TestHelper.mockSubscriber(); + flowable.subscribe(subscriber); + + InOrder inOrder = inOrder(subscriber); + inOrder.verify(subscriber, times(1)).onNext(2); + inOrder.verify(subscriber, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void firstOrElseOfNone() { + Flowable<String> src = Flowable.empty(); + src.first("default").subscribe(wo); + + verify(wo, times(1)).onSuccess(anyString()); + verify(wo, times(1)).onSuccess("default"); + verify(wo, never()).onError(any(Throwable.class)); + } + + @Test + public void firstOrElseOfSome() { + Flowable<String> src = Flowable.just("a", "b", "c"); + src.first("default").subscribe(wo); + + verify(wo, times(1)).onSuccess(anyString()); + verify(wo, times(1)).onSuccess("a"); + verify(wo, never()).onError(any(Throwable.class)); + } + + @Test + public void firstOrElseWithPredicateOfNoneMatchingThePredicate() { + Flowable<String> src = Flowable.just("a", "b", "c"); + src.filter(IS_D).first("default").subscribe(wo); + + verify(wo, times(1)).onSuccess(anyString()); + verify(wo, times(1)).onSuccess("default"); + verify(wo, never()).onError(any(Throwable.class)); + } + + @Test + public void firstOrElseWithPredicateOfSome() { + Flowable<String> src = Flowable.just("a", "b", "c", "d", "e", "f"); + src.filter(IS_D).first("default").subscribe(wo); + + verify(wo, times(1)).onSuccess(anyString()); + verify(wo, times(1)).onSuccess("d"); + verify(wo, never()).onError(any(Throwable.class)); + } + + @Test + public void first() { + Maybe<Integer> maybe = Flowable.just(1, 2, 3).firstElement(); + + maybe.subscribe(wm); + + InOrder inOrder = inOrder(wm); + inOrder.verify(wm, times(1)).onSuccess(1); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void firstWithOneElement() { + Maybe<Integer> maybe = Flowable.just(1).firstElement(); + + maybe.subscribe(wm); + + InOrder inOrder = inOrder(wm); + inOrder.verify(wm, times(1)).onSuccess(1); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void firstWithEmpty() { + Maybe<Integer> maybe = Flowable.<Integer> empty().firstElement(); + + maybe.subscribe(wm); + + InOrder inOrder = inOrder(wm); + inOrder.verify(wm).onComplete(); + inOrder.verify(wm, never()).onError(any(Throwable.class)); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void firstWithPredicate() { + Maybe<Integer> maybe = Flowable.just(1, 2, 3, 4, 5, 6) + .filter(new Predicate<Integer>() { + @Override + public boolean test(Integer t1) { + return t1 % 2 == 0; + } + }) + .firstElement(); + + maybe.subscribe(wm); + + InOrder inOrder = inOrder(wm); + inOrder.verify(wm, times(1)).onSuccess(2); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void firstWithPredicateAndOneElement() { + Maybe<Integer> maybe = Flowable.just(1, 2) + .filter(new Predicate<Integer>() { + @Override + public boolean test(Integer t1) { + return t1 % 2 == 0; + } + }) + .firstElement(); + + maybe.subscribe(wm); + + InOrder inOrder = inOrder(wm); + inOrder.verify(wm, times(1)).onSuccess(2); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void firstWithPredicateAndEmpty() { + Maybe<Integer> maybe = Flowable.just(1) + .filter(new Predicate<Integer>() { + @Override + public boolean test(Integer t1) { + return t1 % 2 == 0; + } + }) + .firstElement(); + + maybe.subscribe(wm); + + InOrder inOrder = inOrder(wm); + inOrder.verify(wm).onComplete(); + inOrder.verify(wm, never()).onError(any(Throwable.class)); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void firstOrDefault() { + Single<Integer> single = Flowable.just(1, 2, 3) + .first(4); + + single.subscribe(wo); + + InOrder inOrder = inOrder(wo); + inOrder.verify(wo, times(1)).onSuccess(1); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void firstOrDefaultWithOneElement() { + Single<Integer> single = Flowable.just(1).first(2); + + single.subscribe(wo); + + InOrder inOrder = inOrder(wo); + inOrder.verify(wo, times(1)).onSuccess(1); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void firstOrDefaultWithEmpty() { + Single<Integer> single = Flowable.<Integer> empty() + .first(1); + + single.subscribe(wo); + + InOrder inOrder = inOrder(wo); + inOrder.verify(wo, times(1)).onSuccess(1); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void firstOrDefaultWithPredicate() { + Single<Integer> single = Flowable.just(1, 2, 3, 4, 5, 6) + .filter(new Predicate<Integer>() { + @Override + public boolean test(Integer t1) { + return t1 % 2 == 0; + } + }) + .first(8); + + single.subscribe(wo); + + InOrder inOrder = inOrder(wo); + inOrder.verify(wo, times(1)).onSuccess(2); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void firstOrDefaultWithPredicateAndOneElement() { + Single<Integer> single = Flowable.just(1, 2) + .filter(new Predicate<Integer>() { + @Override + public boolean test(Integer t1) { + return t1 % 2 == 0; + } + }) + .first(4); + + single.subscribe(wo); + + InOrder inOrder = inOrder(wo); + inOrder.verify(wo, times(1)).onSuccess(2); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void firstOrDefaultWithPredicateAndEmpty() { + Single<Integer> single = Flowable.just(1) + .filter(new Predicate<Integer>() { + @Override + public boolean test(Integer t1) { + return t1 % 2 == 0; + } + }) + .first(2); + + single.subscribe(wo); + + InOrder inOrder = inOrder(wo); + inOrder.verify(wo, times(1)).onSuccess(2); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void firstOrErrorNoElement() { + Flowable.empty() + .firstOrError() + .test() + .assertNoValues() + .assertError(NoSuchElementException.class); + } + + @Test + public void firstOrErrorOneElement() { + Flowable.just(1) + .firstOrError() + .test() + .assertNoErrors() + .assertValue(1); + } + + @Test + public void firstOrErrorMultipleElements() { + Flowable.just(1, 2, 3) + .firstOrError() + .test() + .assertNoErrors() + .assertValue(1); + } + + @Test + public void firstOrErrorError() { + Flowable.error(new RuntimeException("error")) + .firstOrError() + .to(TestHelper.testConsumer()) + .assertNoValues() + .assertErrorMessage("error") + .assertError(RuntimeException.class); + } + + @Test + public void firstOrErrorNoElementFlowable() { + Flowable.empty() + .firstOrError() + .toFlowable() + .test() + .assertNoValues() + .assertError(NoSuchElementException.class); + } + + @Test + public void firstOrErrorOneElementFlowable() { + Flowable.just(1) + .firstOrError() + .toFlowable() + .test() + .assertNoErrors() + .assertValue(1); + } + + @Test + public void firstOrErrorMultipleElementsFlowable() { + Flowable.just(1, 2, 3) + .firstOrError() + .toFlowable() + .test() + .assertNoErrors() + .assertValue(1); + } + + @Test + public void firstOrErrorErrorFlowable() { + Flowable.error(new RuntimeException("error")) + .firstOrError() + .toFlowable() + .to(TestHelper.testConsumer()) + .assertNoValues() + .assertErrorMessage("error") + .assertError(RuntimeException.class); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableFlatMapCompletableTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableFlatMapCompletableTest.java new file mode 100644 index 0000000000..5e4e5c8f77 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableFlatMapCompletableTest.java @@ -0,0 +1,646 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import static org.junit.Assert.*; + +import java.util.List; +import java.util.concurrent.*; + +import org.junit.Test; +import org.reactivestreams.Subscription; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.operators.QueueFuseable; +import io.reactivex.rxjava3.operators.QueueSubscription; +import io.reactivex.rxjava3.processors.PublishProcessor; +import io.reactivex.rxjava3.schedulers.Schedulers; +import io.reactivex.rxjava3.subscribers.TestSubscriber; +import io.reactivex.rxjava3.testsupport.*; + +public class FlowableFlatMapCompletableTest extends RxJavaTest { + + @Test + public void normalFlowable() { + Flowable.range(1, 10) + .flatMapCompletable(new Function<Integer, CompletableSource>() { + @Override + public CompletableSource apply(Integer v) throws Exception { + return Completable.complete(); + } + }).toFlowable() + .test() + .assertResult(); + } + + @Test + public void mapperThrowsFlowable() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + + TestSubscriber<Integer> ts = pp + .flatMapCompletable(new Function<Integer, CompletableSource>() { + @Override + public CompletableSource apply(Integer v) throws Exception { + throw new TestException(); + } + }).<Integer>toFlowable() + .test(); + + assertTrue(pp.hasSubscribers()); + + pp.onNext(1); + + ts.assertFailure(TestException.class); + + assertFalse(pp.hasSubscribers()); + } + + @Test + public void mapperReturnsNullFlowable() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + + TestSubscriber<Integer> ts = pp + .flatMapCompletable(new Function<Integer, CompletableSource>() { + @Override + public CompletableSource apply(Integer v) throws Exception { + return null; + } + }).<Integer>toFlowable() + .test(); + + assertTrue(pp.hasSubscribers()); + + pp.onNext(1); + + ts.assertFailure(NullPointerException.class); + + assertFalse(pp.hasSubscribers()); + } + + @Test + public void normalDelayErrorFlowable() { + Flowable.range(1, 10) + .flatMapCompletable(new Function<Integer, CompletableSource>() { + @Override + public CompletableSource apply(Integer v) throws Exception { + return Completable.complete(); + } + }, true, Integer.MAX_VALUE).toFlowable() + .test() + .assertResult(); + } + + @Test + public void normalAsyncFlowable() { + Flowable.range(1, 1000) + .flatMapCompletable(new Function<Integer, CompletableSource>() { + @Override + public CompletableSource apply(Integer v) throws Exception { + return Flowable.range(1, 100).subscribeOn(Schedulers.computation()).ignoreElements(); + } + }).toFlowable() + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(); + } + + @Test + public void normalAsyncFlowableMaxConcurrency() { + Flowable.range(1, 1000) + .flatMapCompletable(new Function<Integer, CompletableSource>() { + @Override + public CompletableSource apply(Integer v) throws Exception { + return Flowable.range(1, 100).subscribeOn(Schedulers.computation()).ignoreElements(); + } + }, false, 3).toFlowable() + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(); + } + + @Test + public void normalDelayErrorAllFlowable() { + TestSubscriberEx<Integer> ts = Flowable.range(1, 10).concatWith(Flowable.<Integer>error(new TestException())) + .flatMapCompletable(new Function<Integer, CompletableSource>() { + @Override + public CompletableSource apply(Integer v) throws Exception { + return Completable.error(new TestException()); + } + }, true, Integer.MAX_VALUE).<Integer>toFlowable() + .to(TestHelper.<Integer>testConsumer()) + .assertFailure(CompositeException.class); + + List<Throwable> errors = TestHelper.compositeList(ts.errors().get(0)); + + for (int i = 0; i < 11; i++) { + TestHelper.assertError(errors, i, TestException.class); + } + } + + @Test + public void normalDelayInnerErrorAllFlowable() { + TestSubscriberEx<Integer> ts = Flowable.range(1, 10) + .flatMapCompletable(new Function<Integer, CompletableSource>() { + @Override + public CompletableSource apply(Integer v) throws Exception { + return Completable.error(new TestException()); + } + }, true, Integer.MAX_VALUE).<Integer>toFlowable() + .to(TestHelper.<Integer>testConsumer()) + .assertFailure(CompositeException.class); + + List<Throwable> errors = TestHelper.compositeList(ts.errors().get(0)); + + for (int i = 0; i < 10; i++) { + TestHelper.assertError(errors, i, TestException.class); + } + } + + @Test + public void normalNonDelayErrorOuterFlowable() { + Flowable.range(1, 10).concatWith(Flowable.<Integer>error(new TestException())) + .flatMapCompletable(new Function<Integer, CompletableSource>() { + @Override + public CompletableSource apply(Integer v) throws Exception { + return Completable.complete(); + } + }, false, Integer.MAX_VALUE).toFlowable() + .test() + .assertFailure(TestException.class); + } + + @Test + public void fusedFlowable() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<Integer>().setInitialFusionMode(QueueFuseable.ANY); + + Flowable.range(1, 10) + .flatMapCompletable(new Function<Integer, CompletableSource>() { + @Override + public CompletableSource apply(Integer v) throws Exception { + return Completable.complete(); + } + }).<Integer>toFlowable() + .subscribe(ts); + + ts + .assertFuseable() + .assertFusionMode(QueueFuseable.ASYNC) + .assertResult(); + } + + @Test + public void normal() { + Flowable.range(1, 10) + .flatMapCompletable(new Function<Integer, CompletableSource>() { + @Override + public CompletableSource apply(Integer v) throws Exception { + return Completable.complete(); + } + }) + .test() + .assertResult(); + } + + @Test + public void mapperThrows() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + + TestObserver<Void> to = pp + .flatMapCompletable(new Function<Integer, CompletableSource>() { + @Override + public CompletableSource apply(Integer v) throws Exception { + throw new TestException(); + } + }) + .test(); + + assertTrue(pp.hasSubscribers()); + + pp.onNext(1); + + to.assertFailure(TestException.class); + + assertFalse(pp.hasSubscribers()); + } + + @Test + public void mapperReturnsNull() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + + TestObserver<Void> to = pp + .flatMapCompletable(new Function<Integer, CompletableSource>() { + @Override + public CompletableSource apply(Integer v) throws Exception { + return null; + } + }) + .test(); + + assertTrue(pp.hasSubscribers()); + + pp.onNext(1); + + to.assertFailure(NullPointerException.class); + + assertFalse(pp.hasSubscribers()); + } + + @Test + public void normalDelayError() { + Flowable.range(1, 10) + .flatMapCompletable(new Function<Integer, CompletableSource>() { + @Override + public CompletableSource apply(Integer v) throws Exception { + return Completable.complete(); + } + }, true, Integer.MAX_VALUE) + .test() + .assertResult(); + } + + @Test + public void normalAsync() { + Flowable.range(1, 1000) + .flatMapCompletable(new Function<Integer, CompletableSource>() { + @Override + public CompletableSource apply(Integer v) throws Exception { + return Flowable.range(1, 100).subscribeOn(Schedulers.computation()).ignoreElements(); + } + }) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(); + } + + @Test + public void normalDelayErrorAll() { + TestObserverEx<Void> to = Flowable.range(1, 10).concatWith(Flowable.<Integer>error(new TestException())) + .flatMapCompletable(new Function<Integer, CompletableSource>() { + @Override + public CompletableSource apply(Integer v) throws Exception { + return Completable.error(new TestException()); + } + }, true, Integer.MAX_VALUE) + .to(TestHelper.<Integer>testConsumer()) + .assertFailure(CompositeException.class); + + List<Throwable> errors = TestHelper.compositeList(to.errors().get(0)); + + for (int i = 0; i < 11; i++) { + TestHelper.assertError(errors, i, TestException.class); + } + } + + @Test + public void normalDelayInnerErrorAll() { + TestObserverEx<Void> to = Flowable.range(1, 10) + .flatMapCompletable(new Function<Integer, CompletableSource>() { + @Override + public CompletableSource apply(Integer v) throws Exception { + return Completable.error(new TestException()); + } + }, true, Integer.MAX_VALUE) + .to(TestHelper.<Integer>testConsumer()) + .assertFailure(CompositeException.class); + + List<Throwable> errors = TestHelper.compositeList(to.errors().get(0)); + + for (int i = 0; i < 10; i++) { + TestHelper.assertError(errors, i, TestException.class); + } + } + + @Test + public void normalNonDelayErrorOuter() { + Flowable.range(1, 10).concatWith(Flowable.<Integer>error(new TestException())) + .flatMapCompletable(new Function<Integer, CompletableSource>() { + @Override + public CompletableSource apply(Integer v) throws Exception { + return Completable.complete(); + } + }, false, Integer.MAX_VALUE) + .test() + .assertFailure(TestException.class); + } + + @Test + public void fused() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<Integer>().setInitialFusionMode(QueueFuseable.ANY); + + Flowable.range(1, 10) + .flatMapCompletable(new Function<Integer, CompletableSource>() { + @Override + public CompletableSource apply(Integer v) throws Exception { + return Completable.complete(); + } + }) + .<Integer>toFlowable() + .subscribe(ts); + + ts + .assertFuseable() + .assertFusionMode(QueueFuseable.ASYNC) + .assertResult(); + } + + @Test + public void disposed() { + TestHelper.checkDisposed(Flowable.range(1, 10) + .flatMapCompletable(new Function<Integer, CompletableSource>() { + @Override + public CompletableSource apply(Integer v) throws Exception { + return Completable.complete(); + } + })); + } + + @Test + public void normalAsyncMaxConcurrency() { + Flowable.range(1, 1000) + .flatMapCompletable(new Function<Integer, CompletableSource>() { + @Override + public CompletableSource apply(Integer v) throws Exception { + return Flowable.range(1, 100).subscribeOn(Schedulers.computation()).ignoreElements(); + } + }, false, 3) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(); + } + + @Test + public void disposedFlowable() { + TestHelper.checkDisposed(Flowable.range(1, 10) + .flatMapCompletable(new Function<Integer, CompletableSource>() { + @Override + public CompletableSource apply(Integer v) throws Exception { + return Completable.complete(); + } + }).toFlowable()); + } + + @Test + public void badSource() { + TestHelper.checkBadSourceFlowable(new Function<Flowable<Integer>, Object>() { + @Override + public Object apply(Flowable<Integer> f) throws Exception { + return f.flatMapCompletable(new Function<Integer, CompletableSource>() { + @Override + public CompletableSource apply(Integer v) throws Exception { + return Completable.complete(); + } + }); + } + }, false, 1, null); + } + + @Test + public void fusedInternalsFlowable() { + Flowable.range(1, 10) + .flatMapCompletable(new Function<Integer, CompletableSource>() { + @Override + public CompletableSource apply(Integer v) throws Exception { + return Completable.complete(); + } + }) + .toFlowable() + .subscribe(new FlowableSubscriber<Object>() { + @Override + public void onSubscribe(Subscription s) { + QueueSubscription<?> qs = (QueueSubscription<?>)s; + try { + assertNull(qs.poll()); + } catch (Throwable ex) { + throw new RuntimeException(ex); + } + assertTrue(qs.isEmpty()); + qs.clear(); + } + + @Override + public void onNext(Object t) { + } + + @Override + public void onError(Throwable t) { + } + + @Override + public void onComplete() { + } + }); + } + + @Test + public void innerObserverFlowable() { + Flowable.range(1, 3) + .flatMapCompletable(new Function<Integer, CompletableSource>() { + @Override + public CompletableSource apply(Integer v) throws Exception { + return new Completable() { + @Override + protected void subscribeActual(CompletableObserver observer) { + observer.onSubscribe(Disposable.empty()); + + assertFalse(((Disposable)observer).isDisposed()); + + ((Disposable)observer).dispose(); + + assertTrue(((Disposable)observer).isDisposed()); + } + }; + } + }) + .toFlowable() + .test(); + } + + @Test + public void badSourceFlowable() { + TestHelper.checkBadSourceFlowable(new Function<Flowable<Integer>, Object>() { + @Override + public Object apply(Flowable<Integer> f) throws Exception { + return f.flatMapCompletable(new Function<Integer, CompletableSource>() { + @Override + public CompletableSource apply(Integer v) throws Exception { + return Completable.complete(); + } + }).toFlowable(); + } + }, false, 1, null); + } + + @Test + public void innerObserver() { + Flowable.range(1, 3) + .flatMapCompletable(new Function<Integer, CompletableSource>() { + @Override + public CompletableSource apply(Integer v) throws Exception { + return new Completable() { + @Override + protected void subscribeActual(CompletableObserver observer) { + observer.onSubscribe(Disposable.empty()); + + assertFalse(((Disposable)observer).isDisposed()); + + ((Disposable)observer).dispose(); + + assertTrue(((Disposable)observer).isDisposed()); + } + }; + } + }) + .test(); + } + + @Test + public void delayErrorMaxConcurrency() { + Flowable.range(1, 3) + .flatMapCompletable(new Function<Integer, CompletableSource>() { + @Override + public CompletableSource apply(Integer v) throws Exception { + if (v == 2) { + return Completable.error(new TestException()); + } + return Completable.complete(); + } + }, true, 1) + .toFlowable() + .test() + .assertFailure(TestException.class); + } + + @Test + public void delayErrorMaxConcurrencyCompletable() { + Flowable.range(1, 3) + .flatMapCompletable(new Function<Integer, CompletableSource>() { + @Override + public CompletableSource apply(Integer v) throws Exception { + if (v == 2) { + return Completable.error(new TestException()); + } + return Completable.complete(); + } + }, true, 1) + .test() + .assertFailure(TestException.class); + } + + @Test + public void asyncMaxConcurrency() { + for (int itemCount = 1; itemCount <= 100000; itemCount *= 10) { + for (int concurrency = 1; concurrency <= 256; concurrency *= 2) { + Flowable.range(1, itemCount) + .flatMapCompletable( + Functions.justFunction(Completable.complete() + .subscribeOn(Schedulers.computation())) + , false, concurrency) + .test() + .withTag("itemCount=" + itemCount + ", concurrency=" + concurrency) + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(); + } + } + } + + @Test + public void undeliverableUponCancel() { + TestHelper.checkUndeliverableUponCancel(new FlowableConverter<Integer, Completable>() { + @Override + public Completable apply(Flowable<Integer> upstream) { + return upstream.flatMapCompletable(new Function<Integer, Completable>() { + @Override + public Completable apply(Integer v) throws Throwable { + return Completable.complete().hide(); + } + }); + } + }); + } + + @Test + public void undeliverableUponCancelDelayError() { + TestHelper.checkUndeliverableUponCancel(new FlowableConverter<Integer, Completable>() { + @Override + public Completable apply(Flowable<Integer> upstream) { + return upstream.flatMapCompletable(new Function<Integer, Completable>() { + @Override + public Completable apply(Integer v) throws Throwable { + return Completable.complete().hide(); + } + }, true, 2); + } + }); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeFlowable(f -> f.flatMapCompletable(v -> Completable.never()).toFlowable()); + } + + @Test + public void doubleOnSubscribeCompletable() { + TestHelper.checkDoubleOnSubscribeFlowableToCompletable(f -> f.flatMapCompletable(v -> Completable.never())); + } + + @Test + public void cancelWhileMapping() throws Throwable { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + PublishProcessor<Integer> pp1 = PublishProcessor.create(); + + TestSubscriber<Object> ts = new TestSubscriber<>(); + CountDownLatch cdl = new CountDownLatch(1); + + pp1.flatMapCompletable(v -> { + TestHelper.raceOther(() -> { + ts.cancel(); + }, cdl); + return Completable.complete(); + }) + .toFlowable() + .subscribe(ts); + + pp1.onNext(1); + + cdl.await(); + } + } + + @Test + public void cancelWhileMappingCompletable() throws Throwable { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + PublishProcessor<Integer> pp1 = PublishProcessor.create(); + + TestObserver<Void> to = new TestObserver<>(); + CountDownLatch cdl = new CountDownLatch(1); + + pp1.flatMapCompletable(v -> { + TestHelper.raceOther(() -> { + to.dispose(); + }, cdl); + return Completable.complete(); + }) + .subscribe(to); + + pp1.onNext(1); + + cdl.await(); + } + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableFlatMapMaybeTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableFlatMapMaybeTest.java new file mode 100644 index 0000000000..a377761f0c --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableFlatMapMaybeTest.java @@ -0,0 +1,698 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import static org.junit.Assert.*; + +import java.util.List; +import java.util.concurrent.*; + +import org.junit.Test; +import org.reactivestreams.Subscriber; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.internal.subscriptions.BooleanSubscription; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.processors.PublishProcessor; +import io.reactivex.rxjava3.schedulers.Schedulers; +import io.reactivex.rxjava3.subjects.MaybeSubject; +import io.reactivex.rxjava3.subscribers.TestSubscriber; +import io.reactivex.rxjava3.testsupport.*; + +public class FlowableFlatMapMaybeTest extends RxJavaTest { + + @Test + public void normal() { + Flowable.range(1, 10) + .flatMapMaybe(new Function<Integer, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Integer v) throws Exception { + return Maybe.just(v); + } + }) + .test() + .assertResult(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + } + + @Test + public void normalEmpty() { + Flowable.range(1, 10) + .flatMapMaybe(new Function<Integer, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Integer v) throws Exception { + return Maybe.empty(); + } + }) + .test() + .assertResult(); + } + + @Test + public void normalDelayError() { + Flowable.range(1, 10) + .flatMapMaybe(new Function<Integer, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Integer v) throws Exception { + return Maybe.just(v); + } + }, true, Integer.MAX_VALUE) + .test() + .assertResult(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + } + + @Test + public void normalAsync() { + TestSubscriberEx<Integer> ts = Flowable.range(1, 10) + .flatMapMaybe(new Function<Integer, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Integer v) throws Exception { + return Maybe.just(v).subscribeOn(Schedulers.computation()); + } + }) + .to(TestHelper.<Integer>testConsumer()) + .awaitDone(5, TimeUnit.SECONDS) + .assertSubscribed() + .assertValueCount(10) + .assertNoErrors() + .assertComplete(); + + TestHelper.assertValueSet(ts, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + } + + @Test + public void normalAsyncMaxConcurrency() { + TestSubscriberEx<Integer> ts = Flowable.range(1, 10) + .flatMapMaybe(new Function<Integer, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Integer v) throws Exception { + return Maybe.just(v).subscribeOn(Schedulers.computation()); + } + }, false, 3) + .to(TestHelper.<Integer>testConsumer()) + .awaitDone(5, TimeUnit.SECONDS) + .assertSubscribed() + .assertValueCount(10) + .assertNoErrors() + .assertComplete(); + + TestHelper.assertValueSet(ts, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + } + + @Test + public void normalAsyncMaxConcurrency1() { + Flowable.range(1, 10) + .flatMapMaybe(new Function<Integer, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Integer v) throws Exception { + return Maybe.just(v).subscribeOn(Schedulers.computation()); + } + }, false, 1) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + } + + @Test + public void mapperThrowsFlowable() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + + TestSubscriber<Integer> ts = pp + .flatMapMaybe(new Function<Integer, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Integer v) throws Exception { + throw new TestException(); + } + }) + .test(); + + assertTrue(pp.hasSubscribers()); + + pp.onNext(1); + + ts.assertFailure(TestException.class); + + assertFalse(pp.hasSubscribers()); + } + + @Test + public void mapperReturnsNullFlowable() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + + TestSubscriber<Integer> ts = pp + .flatMapMaybe(new Function<Integer, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Integer v) throws Exception { + return null; + } + }) + .test(); + + assertTrue(pp.hasSubscribers()); + + pp.onNext(1); + + ts.assertFailure(NullPointerException.class); + + assertFalse(pp.hasSubscribers()); + } + + @Test + public void normalDelayErrorAll() { + TestSubscriberEx<Integer> ts = Flowable.range(1, 10).concatWith(Flowable.<Integer>error(new TestException())) + .flatMapMaybe(new Function<Integer, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Integer v) throws Exception { + return Maybe.error(new TestException()); + } + }, true, Integer.MAX_VALUE) + .to(TestHelper.<Integer>testConsumer()) + .assertFailure(CompositeException.class); + + List<Throwable> errors = TestHelper.compositeList(ts.errors().get(0)); + + for (int i = 0; i < 11; i++) { + TestHelper.assertError(errors, i, TestException.class); + } + } + + @Test + public void normalBackpressured() { + Flowable.range(1, 10) + .flatMapMaybe(new Function<Integer, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Integer v) throws Exception { + return Maybe.just(v); + } + }) + .rebatchRequests(1) + .test() + .assertResult(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + } + + @Test + public void normalMaxConcurrent1Backpressured() { + Flowable.range(1, 10) + .flatMapMaybe(new Function<Integer, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Integer v) throws Exception { + return Maybe.just(v); + } + }, false, 1) + .rebatchRequests(1) + .test() + .assertResult(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + } + + @Test + public void normalMaxConcurrent2Backpressured() { + Flowable.range(1, 10) + .flatMapMaybe(new Function<Integer, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Integer v) throws Exception { + return Maybe.just(v); + } + }, false, 2) + .rebatchRequests(1) + .test() + .assertResult(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + } + + @Test + public void takeAsync() { + TestSubscriberEx<Integer> ts = Flowable.range(1, 10) + .flatMapMaybe(new Function<Integer, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Integer v) throws Exception { + return Maybe.just(v).subscribeOn(Schedulers.computation()); + } + }) + .take(2) + .to(TestHelper.<Integer>testConsumer()) + .awaitDone(5, TimeUnit.SECONDS) + .assertSubscribed() + .assertValueCount(2) + .assertNoErrors() + .assertComplete(); + + TestHelper.assertValueSet(ts, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + } + + @Test + public void take() { + Flowable.range(1, 10) + .flatMapMaybe(new Function<Integer, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Integer v) throws Exception { + return Maybe.just(v); + } + }) + .take(2) + .test() + .assertResult(1, 2); + } + + @Test + public void middleError() { + Flowable.fromArray(new String[]{"1", "a", "2"}) + .flatMapMaybe(new Function<String, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(final String s) throws NumberFormatException { + //return Maybe.just(Integer.valueOf(s)); //This works + return Maybe.fromCallable(new Callable<Integer>() { + @Override + public Integer call() throws NumberFormatException { + return Integer.valueOf(s); + } + }); + } + }) + .test() + .assertFailure(NumberFormatException.class, 1); + } + + @Test + public void disposed() { + TestHelper.checkDisposed(PublishProcessor.<Integer>create().flatMapMaybe(new Function<Integer, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Integer v) throws Exception { + return Maybe.<Integer>empty(); + } + })); + } + + @Test + public void asyncFlatten() { + Flowable.range(1, 1000) + .flatMapMaybe(new Function<Integer, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Integer v) throws Exception { + return Maybe.just(1).subscribeOn(Schedulers.computation()); + } + }) + .take(500) + .to(TestHelper.<Integer>testConsumer()) + .awaitDone(5, TimeUnit.SECONDS) + .assertSubscribed() + .assertValueCount(500) + .assertNoErrors() + .assertComplete(); + } + + @Test + public void asyncFlattenNone() { + Flowable.range(1, 1000) + .flatMapMaybe(new Function<Integer, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Integer v) throws Exception { + return Maybe.<Integer>empty().subscribeOn(Schedulers.computation()); + } + }) + .take(500) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(); + } + + @Test + public void asyncFlattenNoneMaxConcurrency() { + Flowable.range(1, 1000) + .flatMapMaybe(new Function<Integer, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Integer v) throws Exception { + return Maybe.<Integer>empty().subscribeOn(Schedulers.computation()); + } + }, false, 128) + .take(500) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(); + } + + @Test + public void asyncFlattenErrorMaxConcurrency() { + Flowable.range(1, 1000) + .flatMapMaybe(new Function<Integer, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Integer v) throws Exception { + return Maybe.<Integer>error(new TestException()).subscribeOn(Schedulers.computation()); + } + }, true, 128) + .take(500) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertFailure(CompositeException.class); + } + + @Test + public void successError() { + final PublishProcessor<Integer> pp = PublishProcessor.create(); + + TestSubscriber<Integer> ts = Flowable.range(1, 2) + .flatMapMaybe(new Function<Integer, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Integer v) throws Exception { + if (v == 2) { + return pp.singleElement(); + } + return Maybe.error(new TestException()); + } + }, true, Integer.MAX_VALUE) + .test(); + + pp.onNext(1); + pp.onComplete(); + + ts + .assertFailure(TestException.class, 1); + } + + @Test + public void completeError() { + final PublishProcessor<Integer> pp = PublishProcessor.create(); + + TestSubscriber<Integer> ts = Flowable.range(1, 2) + .flatMapMaybe(new Function<Integer, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Integer v) throws Exception { + if (v == 2) { + return pp.singleElement(); + } + return Maybe.error(new TestException()); + } + }, true, Integer.MAX_VALUE) + .test(); + + pp.onComplete(); + + ts + .assertFailure(TestException.class); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeFlowable(new Function<Flowable<Object>, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Flowable<Object> f) throws Exception { + return f.flatMapMaybe(Functions.justFunction(Maybe.just(2))); + } + }); + } + + @Test + public void badSource() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + new Flowable<Integer>() { + @Override + protected void subscribeActual(Subscriber<? super Integer> subscriber) { + subscriber.onSubscribe(new BooleanSubscription()); + subscriber.onError(new TestException("First")); + subscriber.onError(new TestException("Second")); + } + } + .flatMapMaybe(Functions.justFunction(Maybe.just(2))) + .to(TestHelper.<Integer>testConsumer()) + .assertFailureAndMessage(TestException.class, "First"); + + TestHelper.assertUndeliverable(errors, 0, TestException.class, "Second"); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void badInnerSource() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + Flowable.just(1) + .flatMapMaybe(Functions.justFunction(new Maybe<Integer>() { + @Override + protected void subscribeActual(MaybeObserver<? super Integer> observer) { + observer.onSubscribe(Disposable.empty()); + observer.onError(new TestException("First")); + observer.onError(new TestException("Second")); + } + })) + .to(TestHelper.<Integer>testConsumer()) + .assertFailureAndMessage(TestException.class, "First"); + + TestHelper.assertUndeliverable(errors, 0, TestException.class, "Second"); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void emissionQueueTrigger() { + final PublishProcessor<Integer> pp1 = PublishProcessor.create(); + final PublishProcessor<Integer> pp2 = PublishProcessor.create(); + + TestSubscriber<Integer> ts = new TestSubscriber<Integer>() { + @Override + public void onNext(Integer t) { + super.onNext(t); + if (t == 1) { + pp2.onNext(2); + pp2.onComplete(); + } + } + }; + + Flowable.just(pp1, pp2) + .flatMapMaybe(new Function<PublishProcessor<Integer>, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(PublishProcessor<Integer> v) throws Exception { + return v.singleElement(); + } + }) + .subscribe(ts); + + pp1.onNext(1); + pp1.onComplete(); + + ts.assertResult(1, 2); + } + + @Test + public void emissionQueueTrigger2() { + final PublishProcessor<Integer> pp1 = PublishProcessor.create(); + final PublishProcessor<Integer> pp2 = PublishProcessor.create(); + final PublishProcessor<Integer> pp3 = PublishProcessor.create(); + + TestSubscriber<Integer> ts = new TestSubscriber<Integer>() { + @Override + public void onNext(Integer t) { + super.onNext(t); + if (t == 1) { + pp2.onNext(2); + pp2.onComplete(); + } + } + }; + + Flowable.just(pp1, pp2, pp3) + .flatMapMaybe(new Function<PublishProcessor<Integer>, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(PublishProcessor<Integer> v) throws Exception { + return v.singleElement(); + } + }) + .subscribe(ts); + + pp1.onNext(1); + pp1.onComplete(); + + pp3.onComplete(); + + ts.assertResult(1, 2); + } + + @Test + public void disposeInner() { + final TestSubscriber<Object> ts = new TestSubscriber<>(); + + Flowable.just(1).flatMapMaybe(new Function<Integer, MaybeSource<Object>>() { + @Override + public MaybeSource<Object> apply(Integer v) throws Exception { + return new Maybe<Object>() { + @Override + protected void subscribeActual(MaybeObserver<? super Object> observer) { + observer.onSubscribe(Disposable.empty()); + + assertFalse(((Disposable)observer).isDisposed()); + + ts.cancel(); + + assertTrue(((Disposable)observer).isDisposed()); + } + }; + } + }) + .subscribe(ts); + + ts + .assertEmpty(); + } + + @Test + public void innerSuccessCompletesAfterMain() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + + TestSubscriber<Integer> ts = Flowable.just(1).flatMapMaybe(Functions.justFunction(pp.singleElement())) + .test(); + + pp.onNext(2); + pp.onComplete(); + + ts + .assertResult(2); + } + + @Test + public void backpressure() { + TestSubscriber<Integer> ts = Flowable.just(1) + .flatMapMaybe(Functions.justFunction(Maybe.just(2))) + .test(0L) + .assertEmpty(); + + ts.request(1); + ts.assertResult(2); + } + + @Test + public void error() { + Flowable.just(1) + .flatMapMaybe(Functions.justFunction(Maybe.<Integer>error(new TestException()))) + .test(0L) + .assertFailure(TestException.class); + } + + @Test + public void errorDelayed() { + Flowable.just(1) + .flatMapMaybe(Functions.justFunction(Maybe.<Integer>error(new TestException())), true, 16) + .test(0L) + .assertFailure(TestException.class); + } + + @Test + public void requestCancelRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final TestSubscriber<Integer> ts = Flowable.just(1).concatWith(Flowable.<Integer>never()) + .flatMapMaybe(Functions.justFunction(Maybe.just(2))).test(0); + + Runnable r1 = new Runnable() { + @Override + public void run() { + ts.request(1); + } + }; + Runnable r2 = new Runnable() { + @Override + public void run() { + ts.cancel(); + } + }; + + TestHelper.race(r1, r2); + } + } + + @Test + public void undeliverableUponCancel() { + TestHelper.checkUndeliverableUponCancel(new FlowableConverter<Integer, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Flowable<Integer> upstream) { + return upstream.flatMapMaybe(new Function<Integer, Maybe<Integer>>() { + @Override + public Maybe<Integer> apply(Integer v) throws Throwable { + return Maybe.just(v).hide(); + } + }); + } + }); + } + + @Test + public void undeliverableUponCancelDelayError() { + TestHelper.checkUndeliverableUponCancel(new FlowableConverter<Integer, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Flowable<Integer> upstream) { + return upstream.flatMapMaybe(new Function<Integer, Maybe<Integer>>() { + @Override + public Maybe<Integer> apply(Integer v) throws Throwable { + return Maybe.just(v).hide(); + } + }, true, 2); + } + }); + } + + @Test + public void badRequest() { + TestHelper.assertBadRequestReported(Flowable.never().flatMapMaybe(v -> Maybe.never())); + } + + @Test + public void successRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + MaybeSubject<Integer> ss1 = MaybeSubject.create(); + MaybeSubject<Integer> ss2 = MaybeSubject.create(); + + TestSubscriber<Integer> ts = Flowable.just(ss1, ss2).flatMapMaybe(v -> v) + .test(); + + TestHelper.race( + () -> ss1.onSuccess(1), + () -> ss2.onSuccess(1) + ); + + ts.assertResult(1, 1); + } + } + + @Test + public void successCompleteRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + MaybeSubject<Integer> ss1 = MaybeSubject.create(); + MaybeSubject<Integer> ss2 = MaybeSubject.create(); + + TestSubscriber<Integer> ts = Flowable.just(ss1, ss2).flatMapMaybe(v -> v) + .test(); + + TestHelper.race( + () -> ss1.onSuccess(1), + () -> ss2.onComplete() + ); + + ts.assertResult(1); + } + } + + @Test + public void successShortcut() { + MaybeSubject<Integer> ss1 = MaybeSubject.create(); + + TestSubscriber<Integer> ts = Flowable.just(ss1).hide().flatMapMaybe(v -> v) + .test(); + + ss1.onSuccess(1); + + ts.assertResult(1); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableFlatMapSingleTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableFlatMapSingleTest.java new file mode 100644 index 0000000000..b13e8316af --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableFlatMapSingleTest.java @@ -0,0 +1,580 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import static org.junit.Assert.*; + +import java.util.List; +import java.util.concurrent.*; + +import org.junit.Test; +import org.reactivestreams.Subscriber; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.internal.subscriptions.BooleanSubscription; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.processors.PublishProcessor; +import io.reactivex.rxjava3.schedulers.Schedulers; +import io.reactivex.rxjava3.subjects.SingleSubject; +import io.reactivex.rxjava3.subscribers.TestSubscriber; +import io.reactivex.rxjava3.testsupport.*; + +public class FlowableFlatMapSingleTest extends RxJavaTest { + + @Test + public void normal() { + Flowable.range(1, 10) + .flatMapSingle(new Function<Integer, SingleSource<Integer>>() { + @Override + public SingleSource<Integer> apply(Integer v) throws Exception { + return Single.just(v); + } + }) + .test() + .assertResult(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + } + + @Test + public void normalDelayError() { + Flowable.range(1, 10) + .flatMapSingle(new Function<Integer, SingleSource<Integer>>() { + @Override + public SingleSource<Integer> apply(Integer v) throws Exception { + return Single.just(v); + } + }, true, Integer.MAX_VALUE) + .test() + .assertResult(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + } + + @Test + public void normalAsync() { + TestSubscriberEx<Integer> ts = Flowable.range(1, 10) + .flatMapSingle(new Function<Integer, SingleSource<Integer>>() { + @Override + public SingleSource<Integer> apply(Integer v) throws Exception { + return Single.just(v).subscribeOn(Schedulers.computation()); + } + }) + .to(TestHelper.<Integer>testConsumer()) + .awaitDone(5, TimeUnit.SECONDS) + .assertSubscribed() + .assertValueCount(10) + .assertNoErrors() + .assertComplete(); + + TestHelper.assertValueSet(ts, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + } + + @Test + public void normalAsyncMaxConcurrency() { + TestSubscriberEx<Integer> ts = Flowable.range(1, 10) + .flatMapSingle(new Function<Integer, SingleSource<Integer>>() { + @Override + public SingleSource<Integer> apply(Integer v) throws Exception { + return Single.just(v).subscribeOn(Schedulers.computation()); + } + }, false, 3) + .to(TestHelper.<Integer>testConsumer()) + .awaitDone(5, TimeUnit.SECONDS) + .assertSubscribed() + .assertNoErrors() + .assertComplete(); + + TestHelper.assertValueSet(ts, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + } + + @Test + public void normalAsyncMaxConcurrency1() { + Flowable.range(1, 10) + .flatMapSingle(new Function<Integer, SingleSource<Integer>>() { + @Override + public SingleSource<Integer> apply(Integer v) throws Exception { + return Single.just(v).subscribeOn(Schedulers.computation()); + } + }, false, 1) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + } + + @Test + public void mapperThrowsFlowable() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + + TestSubscriber<Integer> ts = pp + .flatMapSingle(new Function<Integer, SingleSource<Integer>>() { + @Override + public SingleSource<Integer> apply(Integer v) throws Exception { + throw new TestException(); + } + }) + .test(); + + assertTrue(pp.hasSubscribers()); + + pp.onNext(1); + + ts.assertFailure(TestException.class); + + assertFalse(pp.hasSubscribers()); + } + + @Test + public void mapperReturnsNullFlowable() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + + TestSubscriber<Integer> ts = pp + .flatMapSingle(new Function<Integer, SingleSource<Integer>>() { + @Override + public SingleSource<Integer> apply(Integer v) throws Exception { + return null; + } + }) + .test(); + + assertTrue(pp.hasSubscribers()); + + pp.onNext(1); + + ts.assertFailure(NullPointerException.class); + + assertFalse(pp.hasSubscribers()); + } + + @Test + public void normalDelayErrorAll() { + TestSubscriberEx<Integer> ts = Flowable.range(1, 10).concatWith(Flowable.<Integer>error(new TestException())) + .flatMapSingle(new Function<Integer, SingleSource<Integer>>() { + @Override + public SingleSource<Integer> apply(Integer v) throws Exception { + return Single.error(new TestException()); + } + }, true, Integer.MAX_VALUE) + .to(TestHelper.<Integer>testConsumer()) + .assertFailure(CompositeException.class); + + List<Throwable> errors = TestHelper.compositeList(ts.errors().get(0)); + + for (int i = 0; i < 11; i++) { + TestHelper.assertError(errors, i, TestException.class); + } + } + + @Test + public void normalBackpressured() { + Flowable.range(1, 10) + .flatMapSingle(new Function<Integer, SingleSource<Integer>>() { + @Override + public SingleSource<Integer> apply(Integer v) throws Exception { + return Single.just(v); + } + }) + .rebatchRequests(1) + .test() + .assertResult(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + } + + @Test + public void normalMaxConcurrent1Backpressured() { + Flowable.range(1, 10) + .flatMapSingle(new Function<Integer, SingleSource<Integer>>() { + @Override + public SingleSource<Integer> apply(Integer v) throws Exception { + return Single.just(v); + } + }, false, 1) + .rebatchRequests(1) + .test() + .assertResult(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + } + + @Test + public void normalMaxConcurrent2Backpressured() { + Flowable.range(1, 10) + .flatMapSingle(new Function<Integer, SingleSource<Integer>>() { + @Override + public SingleSource<Integer> apply(Integer v) throws Exception { + return Single.just(v); + } + }, false, 2) + .rebatchRequests(1) + .test() + .assertResult(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + } + + @Test + public void takeAsync() { + TestSubscriberEx<Integer> ts = Flowable.range(1, 10) + .flatMapSingle(new Function<Integer, SingleSource<Integer>>() { + @Override + public SingleSource<Integer> apply(Integer v) throws Exception { + return Single.just(v).subscribeOn(Schedulers.computation()); + } + }) + .take(2) + .to(TestHelper.<Integer>testConsumer()) + .awaitDone(5, TimeUnit.SECONDS) + .assertSubscribed() + .assertValueCount(2) + .assertNoErrors() + .assertComplete(); + + TestHelper.assertValueSet(ts, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + } + + @Test + public void take() { + Flowable.range(1, 10) + .flatMapSingle(new Function<Integer, SingleSource<Integer>>() { + @Override + public SingleSource<Integer> apply(Integer v) throws Exception { + return Single.just(v); + } + }) + .take(2) + .test() + .assertResult(1, 2); + } + + @Test + public void middleError() { + Flowable.fromArray(new String[]{"1", "a", "2"}) + .flatMapSingle(new Function<String, SingleSource<Integer>>() { + @Override + public SingleSource<Integer> apply(final String s) throws NumberFormatException { + //return Single.just(Integer.valueOf(s)); //This works + return Single.fromCallable(new Callable<Integer>() { + @Override + public Integer call() throws NumberFormatException { + return Integer.valueOf(s); + } + }); + } + }) + .test() + .assertFailure(NumberFormatException.class, 1); + } + + @Test + public void asyncFlatten() { + Flowable.range(1, 1000) + .flatMapSingle(new Function<Integer, SingleSource<Integer>>() { + @Override + public SingleSource<Integer> apply(Integer v) throws Exception { + return Single.just(1).subscribeOn(Schedulers.computation()); + } + }) + .take(500) + .to(TestHelper.<Integer>testConsumer()) + .awaitDone(5, TimeUnit.SECONDS) + .assertSubscribed() + .assertValueCount(500) + .assertNoErrors() + .assertComplete(); + } + + @Test + public void successError() { + final PublishProcessor<Integer> pp = PublishProcessor.create(); + + TestSubscriber<Integer> ts = Flowable.range(1, 2) + .flatMapSingle(new Function<Integer, SingleSource<Integer>>() { + @Override + public SingleSource<Integer> apply(Integer v) throws Exception { + if (v == 2) { + return pp.singleOrError(); + } + return Single.error(new TestException()); + } + }, true, Integer.MAX_VALUE) + .test(); + + pp.onNext(1); + pp.onComplete(); + + ts + .assertFailure(TestException.class, 1); + } + + @Test + public void disposed() { + TestHelper.checkDisposed(PublishProcessor.<Integer>create().flatMapSingle(new Function<Integer, SingleSource<Integer>>() { + @Override + public SingleSource<Integer> apply(Integer v) throws Exception { + return Single.<Integer>just(1); + } + })); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeFlowable(new Function<Flowable<Object>, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Flowable<Object> f) throws Exception { + return f.flatMapSingle(Functions.justFunction(Single.just(2))); + } + }); + } + + @Test + public void badSource() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + new Flowable<Integer>() { + @Override + protected void subscribeActual(Subscriber<? super Integer> subscriber) { + subscriber.onSubscribe(new BooleanSubscription()); + subscriber.onError(new TestException("First")); + subscriber.onError(new TestException("Second")); + } + } + .flatMapSingle(Functions.justFunction(Single.just(2))) + .to(TestHelper.<Integer>testConsumer()) + .assertFailureAndMessage(TestException.class, "First"); + + TestHelper.assertUndeliverable(errors, 0, TestException.class, "Second"); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void badInnerSource() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + Flowable.just(1) + .flatMapSingle(Functions.justFunction(new Single<Integer>() { + @Override + protected void subscribeActual(SingleObserver<? super Integer> observer) { + observer.onSubscribe(Disposable.empty()); + observer.onError(new TestException("First")); + observer.onError(new TestException("Second")); + } + })) + .to(TestHelper.<Integer>testConsumer()) + .assertFailureAndMessage(TestException.class, "First"); + + TestHelper.assertUndeliverable(errors, 0, TestException.class, "Second"); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void emissionQueueTrigger() { + final PublishProcessor<Integer> pp1 = PublishProcessor.create(); + final PublishProcessor<Integer> pp2 = PublishProcessor.create(); + + TestSubscriber<Integer> ts = new TestSubscriber<Integer>() { + @Override + public void onNext(Integer t) { + super.onNext(t); + if (t == 1) { + pp2.onNext(2); + pp2.onComplete(); + } + } + }; + + Flowable.just(pp1, pp2) + .flatMapSingle(new Function<PublishProcessor<Integer>, SingleSource<Integer>>() { + @Override + public SingleSource<Integer> apply(PublishProcessor<Integer> v) throws Exception { + return v.singleOrError(); + } + }) + .subscribe(ts); + + pp1.onNext(1); + pp1.onComplete(); + + ts.assertResult(1, 2); + } + + @Test + public void disposeInner() { + final TestSubscriber<Object> ts = new TestSubscriber<>(); + + Flowable.just(1).flatMapSingle(new Function<Integer, SingleSource<Object>>() { + @Override + public SingleSource<Object> apply(Integer v) throws Exception { + return new Single<Object>() { + @Override + protected void subscribeActual(SingleObserver<? super Object> observer) { + observer.onSubscribe(Disposable.empty()); + + assertFalse(((Disposable)observer).isDisposed()); + + ts.cancel(); + + assertTrue(((Disposable)observer).isDisposed()); + } + }; + } + }) + .subscribe(ts); + + ts + .assertEmpty(); + } + + @Test + public void innerSuccessCompletesAfterMain() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + + TestSubscriber<Integer> ts = Flowable.just(1).flatMapSingle(Functions.justFunction(pp.singleOrError())) + .test(); + + pp.onNext(2); + pp.onComplete(); + + ts + .assertResult(2); + } + + @Test + public void backpressure() { + TestSubscriber<Integer> ts = Flowable.just(1) + .flatMapSingle(Functions.justFunction(Single.just(2))) + .test(0L) + .assertEmpty(); + + ts.request(1); + ts.assertResult(2); + } + + @Test + public void error() { + Flowable.just(1) + .flatMapSingle(Functions.justFunction(Single.<Integer>error(new TestException()))) + .test(0L) + .assertFailure(TestException.class); + } + + @Test + public void errorDelayed() { + Flowable.just(1) + .flatMapSingle(Functions.justFunction(Single.<Integer>error(new TestException())), true, 16) + .test(0L) + .assertFailure(TestException.class); + } + + @Test + public void requestCancelRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final TestSubscriber<Integer> ts = Flowable.just(1).concatWith(Flowable.<Integer>never()) + .flatMapSingle(Functions.justFunction(Single.just(2))).test(0); + + Runnable r1 = new Runnable() { + @Override + public void run() { + ts.request(1); + } + }; + Runnable r2 = new Runnable() { + @Override + public void run() { + ts.cancel(); + } + }; + + TestHelper.race(r1, r2); + } + } + + @Test + public void asyncFlattenErrorMaxConcurrency() { + Flowable.range(1, 1000) + .flatMapMaybe(new Function<Integer, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Integer v) throws Exception { + return Maybe.<Integer>error(new TestException()).subscribeOn(Schedulers.computation()); + } + }, true, 128) + .take(500) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertFailure(CompositeException.class); + } + + @Test + public void undeliverableUponCancel() { + TestHelper.checkUndeliverableUponCancel(new FlowableConverter<Integer, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Flowable<Integer> upstream) { + return upstream.flatMapSingle(new Function<Integer, Single<Integer>>() { + @Override + public Single<Integer> apply(Integer v) throws Throwable { + return Single.just(v).hide(); + } + }); + } + }); + } + + @Test + public void undeliverableUponCancelDelayError() { + TestHelper.checkUndeliverableUponCancel(new FlowableConverter<Integer, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Flowable<Integer> upstream) { + return upstream.flatMapSingle(new Function<Integer, Single<Integer>>() { + @Override + public Single<Integer> apply(Integer v) throws Throwable { + return Single.just(v).hide(); + } + }, true, 2); + } + }); + } + + @Test + public void badRequest() { + TestHelper.assertBadRequestReported(Flowable.never().flatMapSingle(v -> Single.never())); + } + + @Test + public void successRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + SingleSubject<Integer> ss1 = SingleSubject.create(); + SingleSubject<Integer> ss2 = SingleSubject.create(); + + TestSubscriber<Integer> ts = Flowable.just(ss1, ss2).flatMapSingle(v -> v) + .test(); + + TestHelper.race( + () -> ss1.onSuccess(1), + () -> ss2.onSuccess(1) + ); + + ts.assertResult(1, 1); + } + } + + @Test + public void successShortcut() { + SingleSubject<Integer> ss1 = SingleSubject.create(); + + TestSubscriber<Integer> ts = Flowable.just(ss1).hide().flatMapSingle(v -> v) + .test(); + + ss1.onSuccess(1); + + ts.assertResult(1); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableFlatMapTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableFlatMapTest.java new file mode 100644 index 0000000000..be94c5e57c --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableFlatMapTest.java @@ -0,0 +1,1505 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import java.io.IOException; +import java.util.*; +import java.util.concurrent.*; +import java.util.concurrent.atomic.*; + +import org.junit.*; +import org.reactivestreams.*; + +import io.reactivex.rxjava3.annotations.NonNull; +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.internal.subscriptions.*; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.processors.*; +import io.reactivex.rxjava3.schedulers.Schedulers; +import io.reactivex.rxjava3.subscribers.TestSubscriber; +import io.reactivex.rxjava3.testsupport.*; + +public class FlowableFlatMapTest extends RxJavaTest { + @Test + public void normal() { + Subscriber<Object> subscriber = TestHelper.mockSubscriber(); + + final List<Integer> list = Arrays.asList(1, 2, 3); + + Function<Integer, List<Integer>> func = new Function<Integer, List<Integer>>() { + @Override + public List<Integer> apply(Integer t1) { + return list; + } + }; + BiFunction<Integer, Integer, Integer> resFunc = new BiFunction<Integer, Integer, Integer>() { + + @Override + public Integer apply(Integer t1, Integer t2) { + return t1 | t2; + } + }; + + List<Integer> source = Arrays.asList(16, 32, 64); + + Flowable.fromIterable(source).flatMapIterable(func, resFunc).subscribe(subscriber); + + for (Integer s : source) { + for (Integer v : list) { + verify(subscriber).onNext(s | v); + } + } + verify(subscriber).onComplete(); + verify(subscriber, never()).onError(any(Throwable.class)); + } + + @Test + public void collectionFunctionThrows() { + Subscriber<Object> subscriber = TestHelper.mockSubscriber(); + + Function<Integer, List<Integer>> func = new Function<Integer, List<Integer>>() { + @Override + public List<Integer> apply(Integer t1) { + throw new TestException(); + } + }; + BiFunction<Integer, Integer, Integer> resFunc = new BiFunction<Integer, Integer, Integer>() { + + @Override + public Integer apply(Integer t1, Integer t2) { + return t1 | t2; + } + }; + + List<Integer> source = Arrays.asList(16, 32, 64); + + Flowable.fromIterable(source).flatMapIterable(func, resFunc).subscribe(subscriber); + + verify(subscriber, never()).onComplete(); + verify(subscriber, never()).onNext(any()); + verify(subscriber).onError(any(TestException.class)); + } + + @Test + public void resultFunctionThrows() { + Subscriber<Object> subscriber = TestHelper.mockSubscriber(); + + final List<Integer> list = Arrays.asList(1, 2, 3); + + Function<Integer, List<Integer>> func = new Function<Integer, List<Integer>>() { + @Override + public List<Integer> apply(Integer t1) { + return list; + } + }; + BiFunction<Integer, Integer, Integer> resFunc = new BiFunction<Integer, Integer, Integer>() { + + @Override + public Integer apply(Integer t1, Integer t2) { + throw new TestException(); + } + }; + + List<Integer> source = Arrays.asList(16, 32, 64); + + Flowable.fromIterable(source).flatMapIterable(func, resFunc).subscribe(subscriber); + + verify(subscriber, never()).onComplete(); + verify(subscriber, never()).onNext(any()); + verify(subscriber).onError(any(TestException.class)); + } + + @Test + public void mergeError() { + Subscriber<Object> subscriber = TestHelper.mockSubscriber(); + + Function<Integer, Flowable<Integer>> func = new Function<Integer, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Integer t1) { + return Flowable.error(new TestException()); + } + }; + BiFunction<Integer, Integer, Integer> resFunc = new BiFunction<Integer, Integer, Integer>() { + + @Override + public Integer apply(Integer t1, Integer t2) { + return t1 | t2; + } + }; + + List<Integer> source = Arrays.asList(16, 32, 64); + + Flowable.fromIterable(source).flatMap(func, resFunc).subscribe(subscriber); + + verify(subscriber, never()).onComplete(); + verify(subscriber, never()).onNext(any()); + verify(subscriber).onError(any(TestException.class)); + } + + <T, R> Function<T, R> just(final R value) { + return new Function<T, R>() { + + @Override + public R apply(T t1) { + return value; + } + }; + } + + <R> Supplier<R> just0(final R value) { + return new Supplier<R>() { + + @Override + public R get() { + return value; + } + }; + } + + @Test + public void flatMapTransformsNormal() { + Flowable<Integer> onNext = Flowable.fromIterable(Arrays.asList(1, 2, 3)); + Flowable<Integer> onComplete = Flowable.fromIterable(Arrays.asList(4)); + Flowable<Integer> onError = Flowable.fromIterable(Arrays.asList(5)); + + Flowable<Integer> source = Flowable.fromIterable(Arrays.asList(10, 20, 30)); + + Subscriber<Object> subscriber = TestHelper.mockSubscriber(); + + source.flatMap(just(onNext), just(onError), just0(onComplete)).subscribe(subscriber); + + verify(subscriber, times(3)).onNext(1); + verify(subscriber, times(3)).onNext(2); + verify(subscriber, times(3)).onNext(3); + verify(subscriber).onNext(4); + verify(subscriber).onComplete(); + + verify(subscriber, never()).onNext(5); + verify(subscriber, never()).onError(any(Throwable.class)); + } + + @Test + public void flatMapTransformsException() { + Flowable<Integer> onNext = Flowable.fromIterable(Arrays.asList(1, 2, 3)); + Flowable<Integer> onComplete = Flowable.fromIterable(Arrays.asList(4)); + Flowable<Integer> onError = Flowable.fromIterable(Arrays.asList(5)); + + Flowable<Integer> source = Flowable.concat( + Flowable.fromIterable(Arrays.asList(10, 20, 30)), + Flowable.<Integer> error(new RuntimeException("Forced failure!")) + ); + + Subscriber<Object> subscriber = TestHelper.mockSubscriber(); + + source.flatMap(just(onNext), just(onError), just0(onComplete)).subscribe(subscriber); + + verify(subscriber, times(3)).onNext(1); + verify(subscriber, times(3)).onNext(2); + verify(subscriber, times(3)).onNext(3); + verify(subscriber).onNext(5); + verify(subscriber).onComplete(); + verify(subscriber, never()).onNext(4); + + verify(subscriber, never()).onError(any(Throwable.class)); + } + + <R> Supplier<R> funcThrow0(R r) { + return new Supplier<R>() { + @Override + public R get() { + throw new TestException(); + } + }; + } + + <T, R> Function<T, R> funcThrow(T t, R r) { + return new Function<T, R>() { + @Override + public R apply(T t) { + throw new TestException(); + } + }; + } + + @Test + public void flatMapTransformsOnNextFuncThrows() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + Flowable<Integer> onComplete = Flowable.fromIterable(Arrays.asList(4)); + Flowable<Integer> onError = Flowable.fromIterable(Arrays.asList(5)); + + Flowable<Integer> source = Flowable.fromIterable(Arrays.asList(10, 20, 30)); + + Subscriber<Object> subscriber = TestHelper.mockSubscriber(); + + source.flatMap(funcThrow(1, onError), just(onError), just0(onComplete)).subscribe(subscriber); + + verify(subscriber).onError(any(TestException.class)); + verify(subscriber, never()).onNext(any()); + verify(subscriber, never()).onComplete(); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void flatMapTransformsOnErrorFuncThrows() { + Flowable<Integer> onNext = Flowable.fromIterable(Arrays.asList(1, 2, 3)); + Flowable<Integer> onComplete = Flowable.fromIterable(Arrays.asList(4)); + Flowable<Integer> onError = Flowable.fromIterable(Arrays.asList(5)); + + Flowable<Integer> source = Flowable.error(new TestException()); + + Subscriber<Object> subscriber = TestHelper.mockSubscriber(); + + source.flatMap(just(onNext), funcThrow((Throwable) null, onError), just0(onComplete)).subscribe(subscriber); + + verify(subscriber).onError(any(CompositeException.class)); + verify(subscriber, never()).onNext(any()); + verify(subscriber, never()).onComplete(); + } + + @Test + public void flatMapTransformsOnCompletedFuncThrows() { + Flowable<Integer> onNext = Flowable.fromIterable(Arrays.asList(1, 2, 3)); + Flowable<Integer> onComplete = Flowable.fromIterable(Arrays.asList(4)); + Flowable<Integer> onError = Flowable.fromIterable(Arrays.asList(5)); + + Flowable<Integer> source = Flowable.fromIterable(Arrays.<Integer> asList()); + + Subscriber<Object> subscriber = TestHelper.mockSubscriber(); + + source.flatMap(just(onNext), just(onError), funcThrow0(onComplete)).subscribe(subscriber); + + verify(subscriber).onError(any(TestException.class)); + verify(subscriber, never()).onNext(any()); + verify(subscriber, never()).onComplete(); + } + + @Test + public void flatMapTransformsMergeException() { + Flowable<Integer> onNext = Flowable.error(new TestException()); + Flowable<Integer> onComplete = Flowable.fromIterable(Arrays.asList(4)); + Flowable<Integer> onError = Flowable.fromIterable(Arrays.asList(5)); + + Flowable<Integer> source = Flowable.fromIterable(Arrays.asList(10, 20, 30)); + + Subscriber<Object> subscriber = TestHelper.mockSubscriber(); + + source.flatMap(just(onNext), just(onError), funcThrow0(onComplete)).subscribe(subscriber); + + verify(subscriber).onError(any(TestException.class)); + verify(subscriber, never()).onNext(any()); + verify(subscriber, never()).onComplete(); + } + + private static <T> Flowable<T> composer(Flowable<T> source, final AtomicInteger subscriptionCount, final int m) { + return source.doOnSubscribe(new Consumer<Subscription>() { + @Override + public void accept(Subscription s) { + int n = subscriptionCount.getAndIncrement(); + if (n >= m) { + Assert.fail("Too many subscriptions! " + (n + 1)); + } + } + }).doOnComplete(new Action() { + @Override + public void run() { + int n = subscriptionCount.decrementAndGet(); + if (n < 0) { + Assert.fail("Too many unsubscriptions! " + (n - 1)); + } + } + }); + } + + @Test + public void flatMapMaxConcurrent() { + final int m = 4; + final AtomicInteger subscriptionCount = new AtomicInteger(); + Flowable<Integer> source = Flowable.range(1, 10) + .flatMap(new Function<Integer, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Integer t1) { + return composer(Flowable.range(t1 * 10, 2), subscriptionCount, m) + .subscribeOn(Schedulers.computation()); + } + }, m); + + TestSubscriber<Integer> ts = new TestSubscriber<>(); + + source.subscribe(ts); + + ts.awaitDone(5, TimeUnit.SECONDS); + ts.assertNoErrors(); + Set<Integer> expected = new HashSet<>(Arrays.asList( + 10, 11, 20, 21, 30, 31, 40, 41, 50, 51, 60, 61, 70, 71, 80, 81, 90, 91, 100, 101 + )); + Assert.assertEquals(expected.size(), ts.values().size()); + Assert.assertTrue(expected.containsAll(ts.values())); + } + + @Test + public void flatMapSelectorMaxConcurrent() { + final int m = 4; + final AtomicInteger subscriptionCount = new AtomicInteger(); + Flowable<Integer> source = Flowable.range(1, 10) + .flatMap(new Function<Integer, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Integer t1) { + return composer(Flowable.range(t1 * 10, 2), subscriptionCount, m) + .subscribeOn(Schedulers.computation()); + } + }, new BiFunction<Integer, Integer, Integer>() { + @Override + public Integer apply(Integer t1, Integer t2) { + return t1 * 1000 + t2; + } + }, m); + + TestSubscriber<Integer> ts = new TestSubscriber<>(); + + source.subscribe(ts); + + ts.awaitDone(5, TimeUnit.SECONDS); + ts.assertNoErrors(); + Set<Integer> expected = new HashSet<>(Arrays.asList( + 1010, 1011, 2020, 2021, 3030, 3031, 4040, 4041, 5050, 5051, + 6060, 6061, 7070, 7071, 8080, 8081, 9090, 9091, 10100, 10101 + )); + Assert.assertEquals(expected.size(), ts.values().size()); + System.out.println("--> testFlatMapSelectorMaxConcurrent: " + ts.values()); + Assert.assertTrue(expected.containsAll(ts.values())); + } + + @Test + public void flatMapTransformsMaxConcurrentNormalLoop() { + for (int i = 0; i < 1000; i++) { + if (i % 100 == 0) { + System.out.println("testFlatMapTransformsMaxConcurrentNormalLoop => " + i); + } + flatMapTransformsMaxConcurrentNormal(); + } + } + + @Test + public void flatMapTransformsMaxConcurrentNormal() { + final int m = 2; + final AtomicInteger subscriptionCount = new AtomicInteger(); + Flowable<Integer> onNext = + composer( + Flowable.fromIterable(Arrays.asList(1, 2, 3)) + .observeOn(Schedulers.computation()) + , + subscriptionCount, m) + .subscribeOn(Schedulers.computation()) + ; + + Flowable<Integer> onComplete = composer(Flowable.fromIterable(Arrays.asList(4)), subscriptionCount, m) + .subscribeOn(Schedulers.computation()); + + Flowable<Integer> onError = Flowable.fromIterable(Arrays.asList(5)); + + Flowable<Integer> source = Flowable.fromIterable(Arrays.asList(10, 20, 30)); + + Subscriber<Object> subscriber = TestHelper.mockSubscriber(); + TestSubscriberEx<Object> ts = new TestSubscriberEx<>(subscriber); + + Function<Integer, Flowable<Integer>> just = just(onNext); + Function<Throwable, Flowable<Integer>> just2 = just(onError); + Supplier<Flowable<Integer>> just0 = just0(onComplete); + source.flatMap(just, just2, just0, m).subscribe(ts); + + ts.awaitDone(1, TimeUnit.SECONDS); + ts.assertNoErrors(); + ts.assertTerminated(); + + verify(subscriber, times(3)).onNext(1); + verify(subscriber, times(3)).onNext(2); + verify(subscriber, times(3)).onNext(3); + verify(subscriber).onNext(4); + verify(subscriber).onComplete(); + + verify(subscriber, never()).onNext(5); + verify(subscriber, never()).onError(any(Throwable.class)); + } + + @Test + public void flatMapRangeMixedAsyncLoop() { + for (int i = 0; i < 2000; i++) { + if (i % 10 == 0) { + System.out.println("flatMapRangeAsyncLoop > " + i); + } + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(); + Flowable.range(0, 1000) + .flatMap(new Function<Integer, Flowable<Integer>>() { + final Random rnd = new Random(); + @Override + public Flowable<Integer> apply(Integer t) { + Flowable<Integer> r = Flowable.just(t); + if (rnd.nextBoolean()) { + r = r.hide(); + } + return r; + } + }) + .observeOn(Schedulers.computation()) + .subscribe(ts); + + ts.awaitDone(2500, TimeUnit.MILLISECONDS); + if (ts.completions() == 0) { + System.out.println(ts.values().size()); + } + ts.assertTerminated(); + ts.assertNoErrors(); + List<Integer> list = ts.values(); + if (list.size() < 1000) { + Set<Integer> set = new HashSet<>(list); + for (int j = 0; j < 1000; j++) { + if (!set.contains(j)) { + System.out.println(j + " missing"); + } + } + } + assertEquals(1000, list.size()); + } + } + + @Test + public void flatMapIntPassthruAsync() { + for (int i = 0; i < 1000; i++) { + TestSubscriber<Integer> ts = new TestSubscriber<>(); + + Flowable.range(1, 1000).flatMap(new Function<Integer, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Integer t) { + return Flowable.just(1).subscribeOn(Schedulers.computation()); + } + }).subscribe(ts); + + ts.awaitDone(5, TimeUnit.SECONDS); + ts.assertNoErrors(); + ts.assertComplete(); + ts.assertValueCount(1000); + } + } + + @Test + public void flatMapTwoNestedSync() { + for (final int n : new int[] { 1, 1000, 1000000 }) { + TestSubscriber<Integer> ts = new TestSubscriber<>(); + + Flowable.just(1, 2).flatMap(new Function<Integer, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Integer t) { + return Flowable.range(1, n); + } + }).subscribe(ts); + + System.out.println("flatMapTwoNestedSync >> @ " + n); + ts.assertNoErrors(); + ts.assertComplete(); + ts.assertValueCount(n * 2); + } + } + + @Test + public void justEmptyMixture() { + TestSubscriber<Integer> ts = TestSubscriber.create(); + + Flowable.range(0, 4 * Flowable.bufferSize()) + .flatMap(new Function<Integer, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Integer v) { + return (v & 1) == 0 ? Flowable.<Integer>empty() : Flowable.just(v); + } + }) + .subscribe(ts); + + ts.assertValueCount(2 * Flowable.bufferSize()); + ts.assertNoErrors(); + ts.assertComplete(); + + int j = 1; + for (Integer v : ts.values()) { + Assert.assertEquals(j, v.intValue()); + + j += 2; + } + } + + @Test + public void rangeEmptyMixture() { + TestSubscriber<Integer> ts = TestSubscriber.create(); + + Flowable.range(0, 4 * Flowable.bufferSize()) + .flatMap(new Function<Integer, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Integer v) { + return (v & 1) == 0 ? Flowable.<Integer>empty() : Flowable.range(v, 2); + } + }) + .subscribe(ts); + + ts.assertValueCount(4 * Flowable.bufferSize()); + ts.assertNoErrors(); + ts.assertComplete(); + + int j = 1; + List<Integer> list = ts.values(); + for (int i = 0; i < list.size(); i += 2) { + Assert.assertEquals(j, list.get(i).intValue()); + Assert.assertEquals(j + 1, list.get(i + 1).intValue()); + + j += 2; + } + } + + @Test + public void justEmptyMixtureMaxConcurrent() { + TestSubscriber<Integer> ts = TestSubscriber.create(); + + Flowable.range(0, 4 * Flowable.bufferSize()) + .flatMap(new Function<Integer, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Integer v) { + return (v & 1) == 0 ? Flowable.<Integer>empty() : Flowable.just(v); + } + }, 16) + .subscribe(ts); + + ts.assertValueCount(2 * Flowable.bufferSize()); + ts.assertNoErrors(); + ts.assertComplete(); + + int j = 1; + for (Integer v : ts.values()) { + Assert.assertEquals(j, v.intValue()); + + j += 2; + } + } + + @Test + public void rangeEmptyMixtureMaxConcurrent() { + TestSubscriber<Integer> ts = TestSubscriber.create(); + + Flowable.range(0, 4 * Flowable.bufferSize()) + .flatMap(new Function<Integer, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Integer v) { + return (v & 1) == 0 ? Flowable.<Integer>empty() : Flowable.range(v, 2); + } + }, 16) + .subscribe(ts); + + ts.assertValueCount(4 * Flowable.bufferSize()); + ts.assertNoErrors(); + ts.assertComplete(); + + int j = 1; + List<Integer> list = ts.values(); + for (int i = 0; i < list.size(); i += 2) { + Assert.assertEquals(j, list.get(i).intValue()); + Assert.assertEquals(j + 1, list.get(i + 1).intValue()); + + j += 2; + } + } + + @Test + public void castCrashUnsubscribes() { + + PublishProcessor<Integer> pp = PublishProcessor.create(); + + TestSubscriber<Integer> ts = TestSubscriber.create(); + + pp.flatMap(new Function<Integer, Publisher<Integer>>() { + @Override + public Publisher<Integer> apply(Integer t) { + throw new TestException(); + } + }, new BiFunction<Integer, Integer, Integer>() { + @Override + public Integer apply(Integer t1, Integer t2) { + return t1; + } + }).subscribe(ts); + + Assert.assertTrue("Not subscribed?", pp.hasSubscribers()); + + pp.onNext(1); + + Assert.assertFalse("Subscribed?", pp.hasSubscribers()); + + ts.assertError(TestException.class); + } + + @Test + public void flatMapBiMapper() { + Flowable.just(1) + .flatMap(new Function<Integer, Publisher<Integer>>() { + @Override + public Publisher<Integer> apply(Integer v) throws Exception { + return Flowable.just(v * 10); + } + }, new BiFunction<Integer, Integer, Integer>() { + @Override + public Integer apply(Integer a, Integer b) throws Exception { + return a + b; + } + }, true) + .test() + .assertResult(11); + } + + @Test + public void flatMapBiMapperWithError() { + Flowable.just(1) + .flatMap(new Function<Integer, Publisher<Integer>>() { + @Override + public Publisher<Integer> apply(Integer v) throws Exception { + return Flowable.just(v * 10).concatWith(Flowable.<Integer>error(new TestException())); + } + }, new BiFunction<Integer, Integer, Integer>() { + @Override + public Integer apply(Integer a, Integer b) throws Exception { + return a + b; + } + }, true) + .test() + .assertFailure(TestException.class, 11); + } + + @Test + public void flatMapBiMapperMaxConcurrency() { + Flowable.just(1, 2) + .flatMap(new Function<Integer, Publisher<Integer>>() { + @Override + public Publisher<Integer> apply(Integer v) throws Exception { + return Flowable.just(v * 10); + } + }, new BiFunction<Integer, Integer, Integer>() { + @Override + public Integer apply(Integer a, Integer b) throws Exception { + return a + b; + } + }, true, 1) + .test() + .assertResult(11, 22); + } + + @Test + public void flatMapEmpty() { + assertSame(Flowable.empty(), Flowable.empty().flatMap(new Function<Object, Publisher<Object>>() { + @Override + public Publisher<Object> apply(Object v) throws Exception { + return Flowable.just(v); + } + })); + } + + @Test + public void mergeScalar() { + Flowable.merge(Flowable.just(Flowable.just(1))) + .test() + .assertResult(1); + } + + @Test + public void mergeScalar2() { + Flowable.merge(Flowable.just(Flowable.just(1)).hide()) + .test() + .assertResult(1); + } + + @Test + public void mergeScalarEmpty() { + Flowable.merge(Flowable.just(Flowable.empty()).hide()) + .test() + .assertResult(); + } + + @Test + public void mergeScalarError() { + Flowable.merge(Flowable.just(Flowable.fromCallable(new Callable<Object>() { + @Override + public Object call() throws Exception { + throw new TestException(); + } + })).hide()) + .test() + .assertFailure(TestException.class); + } + + @Test + public void scalarReentrant() { + final PublishProcessor<Flowable<Integer>> pp = PublishProcessor.create(); + + TestSubscriber<Integer> ts = new TestSubscriber<Integer>() { + @Override + public void onNext(Integer t) { + super.onNext(t); + if (t == 1) { + pp.onNext(Flowable.just(2)); + } + } + }; + + Flowable.merge(pp) + .subscribe(ts); + + pp.onNext(Flowable.just(1)); + pp.onComplete(); + + ts.assertResult(1, 2); + } + + @Test + public void scalarReentrant2() { + final PublishProcessor<Flowable<Integer>> pp = PublishProcessor.create(); + + TestSubscriber<Integer> ts = new TestSubscriber<Integer>() { + @Override + public void onNext(Integer t) { + super.onNext(t); + if (t == 1) { + pp.onNext(Flowable.just(2)); + } + } + }; + + Flowable.merge(pp, 2) + .subscribe(ts); + + pp.onNext(Flowable.just(1)); + pp.onComplete(); + + ts.assertResult(1, 2); + } + + @Test + public void innerCompleteCancelRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final PublishProcessor<Integer> pp = PublishProcessor.create(); + + final TestSubscriber<Integer> ts = Flowable.merge(Flowable.just(pp)).test(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + pp.onComplete(); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + ts.cancel(); + } + }; + + TestHelper.race(r1, r2); + } + } + + @Test + public void fusedInnerThrows() { + Flowable.just(1).hide() + .flatMap(new Function<Integer, Flowable<Object>>() { + @Override + public Flowable<Object> apply(Integer v) throws Exception { + return Flowable.range(1, 2).map(new Function<Integer, Object>() { + @Override + public Object apply(Integer w) throws Exception { + throw new TestException(); + } + }); + } + }) + .test() + .assertFailure(TestException.class); + } + + @Test + public void fusedInnerThrows2() { + TestSubscriberEx<Integer> ts = Flowable.range(1, 2).hide() + .flatMap(new Function<Integer, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Integer v) throws Exception { + return Flowable.range(1, 2).map(new Function<Integer, Integer>() { + @Override + public Integer apply(Integer w) throws Exception { + throw new TestException(); + } + }); + } + }, true) + .to(TestHelper.<Integer>testConsumer()) + .assertFailure(CompositeException.class); + + List<Throwable> errors = TestHelper.errorList(ts); + + TestHelper.assertError(errors, 0, TestException.class); + + TestHelper.assertError(errors, 1, TestException.class); + } + + @Test + public void scalarXMap() { + Flowable.fromCallable(Functions.justCallable(1)) + .flatMap(Functions.justFunction(Flowable.fromCallable(Functions.justCallable(2)))) + .test() + .assertResult(2); + } + + @Test + public void noCrossBoundaryFusion() { + for (int i = 0; i < 500; i++) { + TestSubscriber<Object> ts = Flowable.merge( + Flowable.just(1).observeOn(Schedulers.single()).map(new Function<Integer, Object>() { + @Override + public Object apply(Integer v) throws Exception { + return Thread.currentThread().getName().substring(0, 4); + } + }), + Flowable.just(1).observeOn(Schedulers.computation()).map(new Function<Integer, Object>() { + @Override + public Object apply(Integer v) throws Exception { + return Thread.currentThread().getName().substring(0, 4); + } + }) + ) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertValueCount(2); + + List<Object> list = ts.values(); + + assertTrue(list.toString(), list.contains("RxSi")); + assertTrue(list.toString(), list.contains("RxCo")); + } + } + + @Test + public void cancelScalarDrainRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + + final PublishProcessor<Flowable<Integer>> pp = PublishProcessor.create(); + + final TestSubscriber<Integer> ts = pp.flatMap(Functions.<Flowable<Integer>>identity()).test(0); + + Runnable r1 = new Runnable() { + @Override + public void run() { + ts.cancel(); + } + }; + Runnable r2 = new Runnable() { + @Override + public void run() { + pp.onComplete(); + } + }; + + TestHelper.race(r1, r2); + + assertTrue(errors.toString(), errors.isEmpty()); + } finally { + RxJavaPlugins.reset(); + } + } + } + + @Test + public void cancelDrainRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + for (int j = 1; j < 50; j += 5) { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + + final PublishProcessor<Flowable<Integer>> pp = PublishProcessor.create(); + + final TestSubscriber<Integer> ts = pp.flatMap(Functions.<Flowable<Integer>>identity()).test(0); + + final PublishProcessor<Integer> just = PublishProcessor.create(); + pp.onNext(just); + + Runnable r1 = new Runnable() { + @Override + public void run() { + ts.request(1); + ts.cancel(); + } + }; + Runnable r2 = new Runnable() { + @Override + public void run() { + just.onNext(1); + } + }; + + TestHelper.race(r1, r2); + + assertTrue(errors.toString(), errors.isEmpty()); + } finally { + RxJavaPlugins.reset(); + } + } + } + } + + @Test + public void iterableMapperFunctionReturnsNull() { + Flowable.just(1) + .flatMapIterable(new Function<Integer, Iterable<Object>>() { + @Override + public Iterable<Object> apply(Integer v) throws Exception { + return null; + } + }, new BiFunction<Integer, Object, Object>() { + @Override + public Object apply(Integer v, Object w) throws Exception { + return v; + } + }) + .to(TestHelper.<Object>testConsumer()) + .assertFailureAndMessage(NullPointerException.class, "The mapper returned a null Iterable"); + } + + @Test + public void combinerMapperFunctionReturnsNull() { + Flowable.just(1) + .flatMap(new Function<Integer, Publisher<Object>>() { + @Override + public Publisher<Object> apply(Integer v) throws Exception { + return null; + } + }, new BiFunction<Integer, Object, Object>() { + @Override + public Object apply(Integer v, Object w) throws Exception { + return v; + } + }) + .to(TestHelper.<Object>testConsumer()) + .assertFailureAndMessage(NullPointerException.class, "The mapper returned a null Publisher"); + } + + @Test + public void failingFusedInnerCancelsSource() { + final AtomicInteger counter = new AtomicInteger(); + Flowable.range(1, 5) + .doOnNext(new Consumer<Integer>() { + @Override + public void accept(Integer v) throws Exception { + counter.getAndIncrement(); + } + }) + .flatMap(new Function<Integer, Publisher<Integer>>() { + @Override + public Publisher<Integer> apply(Integer v) + throws Exception { + return Flowable.<Integer>fromIterable(new Iterable<Integer>() { + @Override + public Iterator<Integer> iterator() { + return new Iterator<Integer>() { + @Override + public boolean hasNext() { + return true; + } + + @Override + public Integer next() { + throw new TestException(); + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + }; + } + }); + } + }) + .test() + .assertFailure(TestException.class); + + assertEquals(1, counter.get()); + } + + @Test + public void maxConcurrencySustained() { + final PublishProcessor<Integer> pp1 = PublishProcessor.create(); + final PublishProcessor<Integer> pp2 = PublishProcessor.create(); + PublishProcessor<Integer> pp3 = PublishProcessor.create(); + PublishProcessor<Integer> pp4 = PublishProcessor.create(); + + TestSubscriber<Integer> ts = Flowable.just(pp1, pp2, pp3, pp4) + .flatMap(new Function<PublishProcessor<Integer>, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(PublishProcessor<Integer> v) throws Exception { + return v; + } + }, 2) + .doOnNext(new Consumer<Integer>() { + @Override + public void accept(Integer v) throws Exception { + if (v == 1) { + // this will make sure the drain loop detects two completed + // inner sources and replaces them with fresh ones + pp1.onComplete(); + pp2.onComplete(); + } + } + }) + .test(); + + pp1.onNext(1); + + assertFalse(pp1.hasSubscribers()); + assertFalse(pp2.hasSubscribers()); + assertTrue(pp3.hasSubscribers()); + assertTrue(pp4.hasSubscribers()); + + ts.cancel(); + + assertFalse(pp3.hasSubscribers()); + assertFalse(pp4.hasSubscribers()); + } + + @Test + public void undeliverableUponCancel() { + TestHelper.checkUndeliverableUponCancel(new FlowableConverter<Integer, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Flowable<Integer> upstream) { + return upstream.flatMap(new Function<Integer, Publisher<Integer>>() { + @Override + public Publisher<Integer> apply(Integer v) throws Throwable { + return Flowable.just(v).hide(); + } + }); + } + }); + } + + @Test + public void undeliverableUponCancelDelayError() { + TestHelper.checkUndeliverableUponCancel(new FlowableConverter<Integer, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Flowable<Integer> upstream) { + return upstream.flatMap(new Function<Integer, Publisher<Integer>>() { + @Override + public Publisher<Integer> apply(Integer v) throws Throwable { + return Flowable.just(v).hide(); + } + }, true); + } + }); + } + + @Test + public void mainErrorsInnerCancelled() { + PublishProcessor<Integer> pp1 = PublishProcessor.create(); + PublishProcessor<Integer> pp2 = PublishProcessor.create(); + + pp1 + .flatMap(v -> pp2) + .test(); + + pp1.onNext(1); + assertTrue("No subscribers?", pp2.hasSubscribers()); + + pp1.onError(new TestException()); + + assertFalse("Has subscribers?", pp2.hasSubscribers()); + } + + @Test + public void innerErrorsMainCancelled() { + PublishProcessor<Integer> pp1 = PublishProcessor.create(); + PublishProcessor<Integer> pp2 = PublishProcessor.create(); + + pp1 + .flatMap(v -> pp2) + .test(); + + pp1.onNext(1); + assertTrue("No subscribers?", pp2.hasSubscribers()); + + pp2.onError(new TestException()); + + assertFalse("Has subscribers?", pp1.hasSubscribers()); + } + + @Test + public void innerIsDisposed() { + FlowableFlatMap.InnerSubscriber<Integer, Integer> inner = new FlowableFlatMap.InnerSubscriber<>(null, 10, 0L); + + assertFalse(inner.isDisposed()); + + inner.dispose(); + + assertTrue(inner.isDisposed()); + } + + @Test + public void badRequest() { + TestHelper.assertBadRequestReported(Flowable.never().flatMap(v -> Flowable.never())); + } + + @Test + public void signalsAfterMapperCrash() throws Throwable { + TestHelper.withErrorTracking(errors -> { + new Flowable<Integer>() { + @Override + protected void subscribeActual(@NonNull Subscriber<? super @NonNull Integer> s) { + s.onSubscribe(new BooleanSubscription()); + s.onNext(1); + s.onNext(2); + s.onComplete(); + s.onError(new IOException()); + } + } + .flatMap(v -> { + throw new TestException(); + }) + .test() + .assertFailure(TestException.class); + + TestHelper.assertUndeliverable(errors, 0, IOException.class); + }); + } + + @Test + public void scalarQueueTerminate() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + TestSubscriber<Integer> ts = new TestSubscriber<>(); + + pp + .flatMap(v -> Flowable.just(v)) + .doOnNext(v -> { + if (v == 1) { + pp.onNext(2); + pp.onNext(3); + } + }) + .take(2) + .subscribe(ts); + + pp.onNext(1); + + ts.assertResult(1, 2); + } + + @Test + public void scalarQueueCompleteMain() throws Exception { + PublishProcessor<Integer> pp = PublishProcessor.create(); + TestSubscriber<Integer> ts = new TestSubscriber<>(); + CountDownLatch cdl = new CountDownLatch(1); + pp + .flatMap(v -> Flowable.just(v)) + .doOnNext(v -> { + if (v == 1) { + pp.onNext(2); + TestHelper.raceOther(() -> pp.onComplete(), cdl); + } + }) + .subscribe(ts); + + pp.onNext(1); + + cdl.await(); + ts.assertResult(1, 2); + } + + @Test + public void fusedInnerCrash() { + UnicastProcessor<Integer> up = UnicastProcessor.create(); + PublishProcessor<Integer> pp = PublishProcessor.create(); + + TestSubscriber<Integer> ts = Flowable.just( + pp, + up.map(v -> { + if (v == 10) { + throw new TestException(); + } + return v; + }) + .compose(TestHelper.flowableStripBoundary()) + ) + .flatMap(v -> v, true) + .doOnNext(v -> { + if (v == 1) { + pp.onNext(2); + up.onNext(10); + } + }) + .test(); + + pp.onNext(1); + pp.onComplete(); + + ts.assertFailure(TestException.class, 1, 2); + } + + @Test + public void fusedInnerCrash2() { + UnicastProcessor<Integer> up = UnicastProcessor.create(); + PublishProcessor<Integer> pp = PublishProcessor.create(); + + TestSubscriber<Integer> ts = Flowable.just( + up.map(v -> { + if (v == 10) { + throw new TestException(); + } + return v; + }) + .compose(TestHelper.flowableStripBoundary()) + , pp + ) + .flatMap(v -> v, true) + .doOnNext(v -> { + if (v == 1) { + pp.onNext(2); + up.onNext(10); + } + }) + .test(); + + pp.onNext(1); + pp.onComplete(); + + ts.assertFailure(TestException.class, 1, 2); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeFlowable(f -> f.flatMap(v -> Flowable.never())); + } + + @Test + public void allConcurrency() { + Flowable.just(1) + .hide() + .flatMap(v -> Flowable.just(2).hide(), Integer.MAX_VALUE) + .test() + .assertResult(2); + } + + @Test + public void allConcurrencyScalarInner() { + Flowable.just(1) + .hide() + .flatMap(v -> Flowable.just(2), Integer.MAX_VALUE) + .test() + .assertResult(2); + } + + @Test + public void allConcurrencyScalarInnerEmpty() { + Flowable.just(1) + .hide() + .flatMap(v -> Flowable.empty(), Integer.MAX_VALUE) + .test() + .assertResult(); + } + + static final class ScalarEmptyCancel extends Flowable<Integer> implements Supplier<Integer> { + final TestSubscriber<?> ts; + + ScalarEmptyCancel(TestSubscriber<?> ts) { + this.ts = ts; + } + + @Override + public @NonNull Integer get() throws Throwable { + ts.cancel(); + return null; + } + + @Override + protected void subscribeActual(@NonNull Subscriber<@NonNull ? super @NonNull Integer> subscriber) { + EmptySubscription.complete(subscriber); + } + } + + @Test + public void someConcurrencyScalarInnerCancel() { + TestSubscriber<Integer> ts = new TestSubscriber<>(); + + Flowable.just(1) + .hide() + .flatMap(v -> new ScalarEmptyCancel(ts)) + .subscribeWith(ts) + .assertEmpty(); + } + + @Test + public void allConcurrencyBackpressured() { + Flowable.just(1) + .hide() + .flatMap(v -> Flowable.just(2), Integer.MAX_VALUE) + .test(0L) + .assertEmpty() + .requestMore(1) + .assertResult(2); + } + + @Test + public void someConcurrencyInnerScalarCancel() { + Flowable.just(1) + .hide() + .flatMap(v -> Flowable.just(2), 2) + .takeUntil(v -> true) + .test() + .assertResult(2); + } + + @Test + public void scalarInnerOuterOverflow() { + new Flowable<Integer>() { + @Override + protected void subscribeActual(@NonNull Subscriber<@NonNull ? super @NonNull Integer> subscriber) { + subscriber.onSubscribe(new BooleanSubscription()); + subscriber.onNext(1); + subscriber.onNext(2); + subscriber.onNext(3); + } + } + .flatMap(v -> Flowable.just(v), 1) + .test(0L) + .assertFailure(QueueOverflowException.class); + } + + @Test + public void scalarInnerOuterOverflowSlowPath() { + AtomicReference<Subscriber<? super Integer>> ref = new AtomicReference<>(); + new Flowable<Integer>() { + @Override + protected void subscribeActual(@NonNull Subscriber<@NonNull ? super @NonNull Integer> subscriber) { + subscriber.onSubscribe(new BooleanSubscription()); + ref.set(subscriber); + subscriber.onNext(1); + } + } + .flatMap(v -> Flowable.just(v), 1) + .doOnNext(v -> { + if (v == 1) { + ref.get().onNext(2); + ref.get().onNext(3); + } + }) + .test() + .assertFailure(QueueOverflowException.class, 1); + } + + @Test + public void innerFastPathEmitOverflow() { + Flowable.just(1) + .hide() + .flatMap(v -> new Flowable<Integer>() { + @Override + protected void subscribeActual(@NonNull Subscriber<@NonNull ? super @NonNull Integer> subscriber) { + subscriber.onSubscribe(new BooleanSubscription()); + subscriber.onNext(1); + subscriber.onNext(2); + subscriber.onNext(3); + } + }, false, 1, 1) + .test(0L) + .assertFailure(QueueOverflowException.class); + } + + @Test + public void takeFromScalarQueue() { + Flowable.just(1) + .hide() + .flatMap(v -> Flowable.just(2), 2) + .takeUntil(v -> true) + .test(0L) + .requestMore(2) + .assertResult(2); + } + + @Test + public void scalarInnerQueueEmpty() { + Flowable.just(1) + .concatWith(Flowable.never()) + .hide() + .flatMap(v -> Flowable.just(2), 2) + .test(0L) + .requestMore(2) + .assertValuesOnly(2); + } + + @Test + public void innerCompletesAfterOnNextInDrainThenCancels() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + + TestSubscriber<Integer> ts = new TestSubscriber<>(0L); + + Flowable.just(1) + .hide() + .flatMap(v -> pp) + .doOnNext(v -> { + if (v == 1) { + pp.onComplete(); + ts.cancel(); + } + }) + .subscribe(ts); + + pp.onNext(1); + + ts + .requestMore(1) + .assertValuesOnly(1); + } + + @Test(timeout = 5000) + public void mixedScalarAsync() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + Flowable + .range(0, 20) + .flatMap( + integer -> { + if (integer % 5 != 0) { + return Flowable + .just(integer); + } + + return Flowable + .just(-integer) + .observeOn(Schedulers.computation()); + }, + false, + 1 + ) + .ignoreElements() + .blockingAwait(); + } + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableFlattenIterableTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableFlattenIterableTest.java new file mode 100644 index 0000000000..2cb9392e19 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableFlattenIterableTest.java @@ -0,0 +1,1102 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import static org.junit.Assert.*; + +import java.util.*; +import java.util.concurrent.Callable; +import java.util.concurrent.atomic.*; + +import org.junit.*; +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.internal.operators.flowable.FlowableFlattenIterable.FlattenIterableSubscriber; +import io.reactivex.rxjava3.internal.subscriptions.BooleanSubscription; +import io.reactivex.rxjava3.internal.util.ExceptionHelper; +import io.reactivex.rxjava3.operators.QueueFuseable; +import io.reactivex.rxjava3.operators.QueueSubscription; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.processors.PublishProcessor; +import io.reactivex.rxjava3.subscribers.TestSubscriber; +import io.reactivex.rxjava3.testsupport.*; + +public class FlowableFlattenIterableTest extends RxJavaTest { + + @Test + public void normal0() { + + TestSubscriber<Integer> ts = new TestSubscriber<>(); + + Flowable.range(1, 2) + .reduce(new BiFunction<Integer, Integer, Integer>() { + @Override + public Integer apply(Integer a, Integer b) { + return Math.max(a, b); + } + }) + .toFlowable() + .flatMapIterable(new Function<Integer, Iterable<Integer>>() { + @Override + public Iterable<Integer> apply(Integer v) { + return Arrays.asList(v, v + 1); + } + }) + .subscribe(ts); + + ts.assertValues(2, 3) + .assertNoErrors() + .assertComplete(); + } + + final Function<Integer, Iterable<Integer>> mapper = new Function<Integer, Iterable<Integer>>() { + @Override + public Iterable<Integer> apply(Integer v) { + return Arrays.asList(v, v + 1); + } + }; + + @Test + public void normal() { + TestSubscriber<Integer> ts = new TestSubscriber<>(); + + Flowable.range(1, 5).concatMapIterable(mapper) + .subscribe(ts); + + ts.assertValues(1, 2, 2, 3, 3, 4, 4, 5, 5, 6); + ts.assertNoErrors(); + ts.assertComplete(); + } + + @Test + public void normalViaFlatMap() { + TestSubscriber<Integer> ts = new TestSubscriber<>(); + + Flowable.range(1, 5).flatMapIterable(mapper) + .subscribe(ts); + + ts.assertValues(1, 2, 2, 3, 3, 4, 4, 5, 5, 6); + ts.assertNoErrors(); + ts.assertComplete(); + } + + @Test + public void normalBackpressured() { + TestSubscriber<Integer> ts = new TestSubscriber<>(0); + + Flowable.range(1, 5).concatMapIterable(mapper) + .subscribe(ts); + + ts.assertNoValues(); + ts.assertNoErrors(); + ts.assertNotComplete(); + + ts.request(1); + + ts.assertValue(1); + ts.assertNoErrors(); + ts.assertNotComplete(); + + ts.request(2); + + ts.assertValues(1, 2, 2); + ts.assertNoErrors(); + ts.assertNotComplete(); + + ts.request(7); + + ts.assertValues(1, 2, 2, 3, 3, 4, 4, 5, 5, 6); + ts.assertNoErrors(); + ts.assertComplete(); + } + + @Test + public void longRunning() { + TestSubscriber<Integer> ts = new TestSubscriber<>(); + + int n = 1000 * 1000; + + Flowable.range(1, n).concatMapIterable(mapper) + .subscribe(ts); + + ts.assertValueCount(n * 2); + ts.assertNoErrors(); + ts.assertComplete(); + } + + @Test + public void asIntermediate() { + TestSubscriber<Integer> ts = new TestSubscriber<>(); + + int n = 1000 * 1000; + + Flowable.range(1, n).concatMapIterable(mapper).concatMap(new Function<Integer, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Integer v) { + return Flowable.just(v); + } + }) + .subscribe(ts); + + ts.assertValueCount(n * 2); + ts.assertNoErrors(); + ts.assertComplete(); + } + + @Test + public void just() { + TestSubscriber<Integer> ts = new TestSubscriber<>(); + + Flowable.just(1).concatMapIterable(mapper) + .subscribe(ts); + + ts.assertValues(1, 2); + ts.assertNoErrors(); + ts.assertComplete(); + } + + @Test + public void justHidden() { + TestSubscriber<Integer> ts = new TestSubscriber<>(); + + Flowable.just(1).hide().concatMapIterable(mapper) + .subscribe(ts); + + ts.assertValues(1, 2); + ts.assertNoErrors(); + ts.assertComplete(); + } + + @Test + public void empty() { + TestSubscriber<Integer> ts = new TestSubscriber<>(); + + Flowable.<Integer>empty().concatMapIterable(mapper) + .subscribe(ts); + + ts.assertNoValues(); + ts.assertNoErrors(); + ts.assertComplete(); + } + + @Test + public void error() { + TestSubscriber<Integer> ts = new TestSubscriber<>(); + + Flowable.<Integer>just(1).concatWith(Flowable.<Integer>error(new TestException())) + .concatMapIterable(mapper) + .subscribe(ts); + + ts.assertValues(1, 2); + ts.assertError(TestException.class); + ts.assertNotComplete(); + } + + @Test + public void iteratorHasNextThrowsImmediately() { + TestSubscriber<Integer> ts = new TestSubscriber<>(); + + final Iterable<Integer> it = new Iterable<Integer>() { + @Override + public Iterator<Integer> iterator() { + return new Iterator<Integer>() { + @Override + public boolean hasNext() { + throw new TestException(); + } + + @Override + public Integer next() { + return 1; + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + }; + } + }; + + Flowable.range(1, 2) + .concatMapIterable(new Function<Integer, Iterable<Integer>>() { + @Override + public Iterable<Integer> apply(Integer v) { + return it; + } + }) + .subscribe(ts); + + ts.assertNoValues(); + ts.assertError(TestException.class); + ts.assertNotComplete(); + } + + @Test + public void iteratorHasNextThrowsImmediatelyJust() { + TestSubscriber<Integer> ts = new TestSubscriber<>(); + + final Iterable<Integer> it = new Iterable<Integer>() { + @Override + public Iterator<Integer> iterator() { + return new Iterator<Integer>() { + @Override + public boolean hasNext() { + throw new TestException(); + } + + @Override + public Integer next() { + return 1; + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + }; + } + }; + + Flowable.just(1) + .concatMapIterable(new Function<Integer, Iterable<Integer>>() { + @Override + public Iterable<Integer> apply(Integer v) { + return it; + } + }) + .subscribe(ts); + + ts.assertNoValues(); + ts.assertError(TestException.class); + ts.assertNotComplete(); + } + + @Test + public void iteratorHasNextThrowsSecondCall() { + TestSubscriber<Integer> ts = new TestSubscriber<>(); + + final Iterable<Integer> it = new Iterable<Integer>() { + @Override + public Iterator<Integer> iterator() { + return new Iterator<Integer>() { + int count; + @Override + public boolean hasNext() { + if (++count >= 2) { + throw new TestException(); + } + return true; + } + + @Override + public Integer next() { + return 1; + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + }; + } + }; + + Flowable.range(1, 2) + .concatMapIterable(new Function<Integer, Iterable<Integer>>() { + @Override + public Iterable<Integer> apply(Integer v) { + return it; + } + }) + .subscribe(ts); + + ts.assertValue(1); + ts.assertError(TestException.class); + ts.assertNotComplete(); + } + + @Test + public void iteratorNextThrows() { + TestSubscriber<Integer> ts = new TestSubscriber<>(); + + final Iterable<Integer> it = new Iterable<Integer>() { + @Override + public Iterator<Integer> iterator() { + return new Iterator<Integer>() { + @Override + public boolean hasNext() { + return true; + } + + @Override + public Integer next() { + throw new TestException(); + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + }; + } + }; + + Flowable.range(1, 2) + .concatMapIterable(new Function<Integer, Iterable<Integer>>() { + @Override + public Iterable<Integer> apply(Integer v) { + return it; + } + }) + .subscribe(ts); + + ts.assertNoValues(); + ts.assertError(TestException.class); + ts.assertNotComplete(); + } + + @Test + public void iteratorNextThrowsAndUnsubscribes() { + TestSubscriber<Integer> ts = new TestSubscriber<>(); + + final Iterable<Integer> it = new Iterable<Integer>() { + @Override + public Iterator<Integer> iterator() { + return new Iterator<Integer>() { + @Override + public boolean hasNext() { + return true; + } + + @Override + public Integer next() { + throw new TestException(); + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + }; + } + }; + + PublishProcessor<Integer> pp = PublishProcessor.create(); + + pp + .concatMapIterable(new Function<Integer, Iterable<Integer>>() { + @Override + public Iterable<Integer> apply(Integer v) { + return it; + } + }) + .subscribe(ts); + + pp.onNext(1); + + ts.assertNoValues(); + ts.assertError(TestException.class); + ts.assertNotComplete(); + + Assert.assertFalse("PublishProcessor has Subscribers?!", pp.hasSubscribers()); + } + + @Test + public void mixture() { + TestSubscriber<Integer> ts = new TestSubscriber<>(); + + Flowable.range(0, 1000) + .concatMapIterable(new Function<Integer, Iterable<Integer>>() { + @Override + public Iterable<Integer> apply(Integer v) { + return (v % 2) == 0 ? Collections.singleton(1) : Collections.<Integer>emptySet(); + } + }) + .subscribe(ts); + + ts.assertValueCount(500); + ts.assertNoErrors(); + ts.assertComplete(); + } + + @Test + public void emptyInnerThenSingleBackpressured() { + TestSubscriber<Integer> ts = new TestSubscriber<>(1); + + Flowable.range(1, 2) + .concatMapIterable(new Function<Integer, Iterable<Integer>>() { + @Override + public Iterable<Integer> apply(Integer v) { + return v == 2 ? Collections.singleton(1) : Collections.<Integer>emptySet(); + } + }) + .subscribe(ts); + + ts.assertValue(1); + ts.assertNoErrors(); + ts.assertComplete(); + } + + @Test + public void manyEmptyInnerThenSingleBackpressured() { + TestSubscriber<Integer> ts = new TestSubscriber<>(1); + + Flowable.range(1, 1000) + .concatMapIterable(new Function<Integer, Iterable<Integer>>() { + @Override + public Iterable<Integer> apply(Integer v) { + return v == 1000 ? Collections.singleton(1) : Collections.<Integer>emptySet(); + } + }) + .subscribe(ts); + + ts.assertValue(1); + ts.assertNoErrors(); + ts.assertComplete(); + } + + @Test + public void hasNextIsNotCalledAfterChildUnsubscribedOnNext() { + TestSubscriber<Integer> ts = new TestSubscriber<>(); + + final AtomicInteger counter = new AtomicInteger(); + + final Iterable<Integer> it = new Iterable<Integer>() { + @Override + public Iterator<Integer> iterator() { + return new Iterator<Integer>() { + @Override + public boolean hasNext() { + counter.getAndIncrement(); + return true; + } + + @Override + public Integer next() { + return 1; + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + }; + } + }; + + PublishProcessor<Integer> pp = PublishProcessor.create(); + + pp + .concatMapIterable(new Function<Integer, Iterable<Integer>>() { + @Override + public Iterable<Integer> apply(Integer v) { + return it; + } + }) + .take(1) + .subscribe(ts); + + pp.onNext(1); + + ts.assertValue(1); + ts.assertNoErrors(); + ts.assertComplete(); + + Assert.assertFalse("PublishProcessor has Subscribers?!", pp.hasSubscribers()); + Assert.assertEquals(1, counter.get()); + } + + @Test + public void normalPrefetchViaFlatMap() { + TestSubscriber<Integer> ts = new TestSubscriber<>(); + + Flowable.range(1, 5).flatMapIterable(mapper, 2) + .subscribe(ts); + + ts.assertValues(1, 2, 2, 3, 3, 4, 4, 5, 5, 6); + ts.assertNoErrors(); + ts.assertComplete(); + } + + @Test + public void withResultSelectorMaxConcurrent() { + TestSubscriber<Integer> ts = TestSubscriber.create(); + + Flowable.range(1, 5) + .flatMapIterable(new Function<Integer, Iterable<Integer>>() { + @Override + public Iterable<Integer> apply(Integer v) { + return Collections.singletonList(1); + } + }, new BiFunction<Integer, Integer, Integer>() { + @Override + public Integer apply(Integer a, Integer b) { + return a * 10 + b; + } + }, 2) + .subscribe(ts) + ; + + ts.assertValues(11, 21, 31, 41, 51); + ts.assertNoErrors(); + ts.assertComplete(); + } + + @Test + public void flatMapIterablePrefetch() { + Flowable.just(1, 2) + .flatMapIterable(new Function<Integer, Iterable<Integer>>() { + @Override + public Iterable<Integer> apply(Integer t) throws Exception { + return Arrays.asList(t * 10); + } + }, 1) + .test() + .assertResult(10, 20); + } + + @Test + public void dispose() { + TestHelper.checkDisposed(PublishProcessor.create().flatMapIterable(new Function<Object, Iterable<Integer>>() { + @Override + public Iterable<Integer> apply(Object v) throws Exception { + return Arrays.asList(10, 20); + } + })); + } + + @Test + public void badSource() { + TestHelper.checkBadSourceFlowable(new Function<Flowable<Integer>, Object>() { + @Override + public Object apply(Flowable<Integer> f) throws Exception { + return f.flatMapIterable(new Function<Object, Iterable<Integer>>() { + @Override + public Iterable<Integer> apply(Object v) throws Exception { + return Arrays.asList(10, 20); + } + }); + } + }, false, 1, 1, 10, 20); + } + + @Test + public void callableThrows() { + Flowable.fromCallable(new Callable<Object>() { + @Override + public Object call() throws Exception { + throw new TestException(); + } + }) + .flatMapIterable(Functions.justFunction(Arrays.asList(1, 2, 3))) + .test() + .assertFailure(TestException.class); + } + + @Test + public void fusionMethods() { + Flowable.just(1, 2) + .flatMapIterable(Functions.justFunction(Arrays.asList(1, 2, 3))) + .subscribe(new FlowableSubscriber<Integer>() { + @Override + public void onSubscribe(Subscription s) { + @SuppressWarnings("unchecked") + QueueSubscription<Integer> qs = (QueueSubscription<Integer>)s; + + assertEquals(QueueFuseable.SYNC, qs.requestFusion(QueueFuseable.ANY)); + + try { + assertFalse("Source reports being empty!", qs.isEmpty()); + + assertEquals(1, qs.poll().intValue()); + + assertFalse("Source reports being empty!", qs.isEmpty()); + + assertEquals(2, qs.poll().intValue()); + + assertFalse("Source reports being empty!", qs.isEmpty()); + + qs.clear(); + + assertTrue("Source reports not empty!", qs.isEmpty()); + + assertNull(qs.poll()); + } catch (Throwable ex) { + throw ExceptionHelper.wrapOrThrow(ex); + } + } + + @Override + public void onNext(Integer t) { + } + + @Override + public void onError(Throwable t) { + } + + @Override + public void onComplete() { + } + }); + } + + @Test + public void smallPrefetch() { + Flowable.just(1, 2, 3) + .flatMapIterable(Functions.justFunction(Arrays.asList(1, 2, 3)), 1) + .test() + .assertResult(1, 2, 3, 1, 2, 3, 1, 2, 3); + } + + @Test + public void smallPrefetch2() { + Flowable.just(1, 2, 3).hide() + .flatMapIterable(Functions.justFunction(Collections.emptyList()), 1) + .test() + .assertResult(); + } + + @Test + public void mixedInnerSource() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<Integer>().setInitialFusionMode(QueueFuseable.ANY); + + Flowable.just(1, 2, 3) + .flatMapIterable(new Function<Integer, Iterable<Integer>>() { + @Override + public Iterable<Integer> apply(Integer v) throws Exception { + if ((v & 1) == 0) { + return Collections.emptyList(); + } + return Arrays.asList(1, 2); + } + }) + .subscribe(ts); + + ts.assertFusionMode(QueueFuseable.SYNC) + .assertResult(1, 2, 1, 2); + } + + @Test + public void mixedInnerSource2() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<Integer>().setInitialFusionMode(QueueFuseable.ANY); + + Flowable.just(1, 2, 3) + .flatMapIterable(new Function<Integer, Iterable<Integer>>() { + @Override + public Iterable<Integer> apply(Integer v) throws Exception { + if ((v & 1) == 1) { + return Collections.emptyList(); + } + return Arrays.asList(1, 2); + } + }) + .subscribe(ts); + + ts.assertFusionMode(QueueFuseable.SYNC) + .assertResult(1, 2); + } + + @Test + public void fusionRejected() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<Integer>().setInitialFusionMode(QueueFuseable.ANY); + + Flowable.just(1, 2, 3).hide() + .flatMapIterable(new Function<Integer, Iterable<Integer>>() { + @Override + public Iterable<Integer> apply(Integer v) throws Exception { + return Arrays.asList(1, 2); + } + }) + .subscribe(ts); + + ts.assertFusionMode(QueueFuseable.NONE) + .assertResult(1, 2, 1, 2, 1, 2); + } + + @Test + public void fusedIsEmptyWithEmptySource() { + Flowable.just(1, 2, 3) + .flatMapIterable(new Function<Integer, Iterable<Integer>>() { + @Override + public Iterable<Integer> apply(Integer v) throws Exception { + if ((v & 1) == 0) { + return Collections.emptyList(); + } + return Arrays.asList(v); + } + }) + .subscribe(new FlowableSubscriber<Integer>() { + @Override + public void onSubscribe(Subscription s) { + @SuppressWarnings("unchecked") + QueueSubscription<Integer> qs = (QueueSubscription<Integer>)s; + + assertEquals(QueueFuseable.SYNC, qs.requestFusion(QueueFuseable.ANY)); + + try { + assertFalse("Source reports being empty!", qs.isEmpty()); + + assertEquals(1, qs.poll().intValue()); + + assertFalse("Source reports being empty!", qs.isEmpty()); + + assertEquals(3, qs.poll().intValue()); + + assertTrue("Source reports being non-empty!", qs.isEmpty()); + } catch (Throwable ex) { + throw ExceptionHelper.wrapOrThrow(ex); + } + } + + @Override + public void onNext(Integer t) { + } + + @Override + public void onError(Throwable t) { + } + + @Override + public void onComplete() { + } + }); + } + + @Test + public void fusedSourceCrash() { + Flowable.range(1, 3) + .map(new Function<Integer, Object>() { + @Override + public Object apply(Integer v) throws Exception { + throw new TestException(); + } + }) + .flatMapIterable(Functions.justFunction(Collections.emptyList()), 1) + .test() + .assertFailure(TestException.class); + } + + @Test + public void take() { + Flowable.range(1, 3) + .flatMapIterable(Functions.justFunction(Arrays.asList(1)), 1) + .take(1) + .test() + .assertResult(1); + } + + @Test + public void overflowSource() { + new Flowable<Integer>() { + @Override + protected void subscribeActual(Subscriber<? super Integer> s) { + s.onSubscribe(new BooleanSubscription()); + s.onNext(1); + s.onNext(2); + s.onNext(3); + } + } + .flatMapIterable(Functions.justFunction(Arrays.asList(1)), 1) + .test(0L) + .assertFailure(QueueOverflowException.class); + } + + @Test + public void oneByOne() { + Flowable.range(1, 3).hide() + .flatMapIterable(Functions.justFunction(Arrays.asList(1)), 1) + .rebatchRequests(1) + .test() + .assertResult(1, 1, 1); + } + + @Test + public void cancelAfterHasNext() { + final TestSubscriber<Integer> ts = new TestSubscriber<>(); + + Flowable.range(1, 3).hide() + .flatMapIterable(new Function<Integer, Iterable<Integer>>() { + @Override + public Iterable<Integer> apply(Integer v) throws Exception { + return new Iterable<Integer>() { + int count; + @Override + public Iterator<Integer> iterator() { + return new Iterator<Integer>() { + + @Override + public boolean hasNext() { + if (++count == 2) { + ts.cancel(); + ts.onComplete(); + } + return true; + } + + @Override + public Integer next() { + return 1; + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + }; + } + }; + } + }) + .subscribe(ts); + + ts.assertResult(1); + } + + @Test + public void doubleShare() { + Iterable<Integer> it = Flowable.range(1, 300).blockingIterable(); + Flowable.just(it, it) + .flatMapIterable(Functions.<Iterable<Integer>>identity()) + .share() + .share() + .count() + .test() + .assertResult(600L); + } + + @Test + public void multiShare() { + Iterable<Integer> it = Flowable.range(1, 300).blockingIterable(); + for (int i = 0; i < 5; i++) { + Flowable<Integer> f = Flowable.just(it, it) + .flatMapIterable(Functions.<Iterable<Integer>>identity()); + + for (int j = 0; j < i; j++) { + f = f.share(); + } + + f + .count() + .test() + .withTag("Share: " + i) + .assertResult(600L); + } + } + + @Test + public void multiShareHidden() { + Iterable<Integer> it = Flowable.range(1, 300).blockingIterable(); + for (int i = 0; i < 5; i++) { + Flowable<Integer> f = Flowable.just(it, it) + .flatMapIterable(Functions.<Iterable<Integer>>identity()) + .hide(); + + for (int j = 0; j < i; j++) { + f = f.share(); + } + + f + .count() + .test() + .withTag("Share: " + i) + .assertResult(600L); + } + } + + @Test + public void failingInnerCancelsSource() { + final AtomicInteger counter = new AtomicInteger(); + Flowable.range(1, 5) + .doOnNext(new Consumer<Integer>() { + @Override + public void accept(Integer v) throws Exception { + counter.getAndIncrement(); + } + }) + .flatMapIterable(new Function<Integer, Iterable<Integer>>() { + @Override + public Iterable<Integer> apply(Integer v) + throws Exception { + return new Iterable<Integer>() { + @Override + public Iterator<Integer> iterator() { + return new Iterator<Integer>() { + @Override + public boolean hasNext() { + return true; + } + + @Override + public Integer next() { + throw new TestException(); + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + }; + } + }; + } + }) + .test() + .assertFailure(TestException.class); + + assertEquals(1, counter.get()); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeFlowable(new Function<Flowable<Object>, Publisher<Object>>() { + @Override + public Publisher<Object> apply(Flowable<Object> f) + throws Exception { + return f.flatMapIterable(Functions.justFunction(Collections.emptyList())); + } + }); + } + + @Test + public void upstreamFusionRejected() { + TestSubscriber<Integer> ts = new TestSubscriber<>(); + FlattenIterableSubscriber<Integer, Integer> f = new FlattenIterableSubscriber<>(ts, + Functions.justFunction(Collections.<Integer>emptyList()), 128); + + final AtomicLong requested = new AtomicLong(); + + f.onSubscribe(new QueueSubscription<Integer>() { + + @Override + public int requestFusion(int mode) { + return 0; + } + + @Override + public boolean offer(Integer value) { + return false; + } + + @Override + public boolean offer(Integer v1, Integer v2) { + return false; + } + + @Override + public Integer poll() throws Exception { + return null; + } + + @Override + public boolean isEmpty() { + return false; + } + + @Override + public void clear() { + } + + @Override + public void request(long n) { + requested.set(n); + } + + @Override + public void cancel() { + } + }); + + assertEquals(128, requested.get()); + assertNotNull(f.queue); + + ts.assertEmpty(); + } + + @Test + public void onErrorLate() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(); + FlattenIterableSubscriber<Integer, Integer> f = new FlattenIterableSubscriber<>(ts, + Functions.justFunction(Collections.<Integer>emptyList()), 128); + + f.onSubscribe(new BooleanSubscription()); + + f.onError(new TestException("first")); + + ts.assertFailureAndMessage(TestException.class, "first"); + + assertTrue(errors.isEmpty()); + + f.done = false; + f.onError(new TestException("second")); + + TestHelper.assertUndeliverable(errors, 0, TestException.class, "second"); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void badRequest() { + TestHelper.assertBadRequestReported(Flowable.never().flatMapIterable(Functions.justFunction(Collections.emptyList()))); + } + + @Test + public void fusedCurrentIteratorEmpty() throws Throwable { + TestSubscriber<Integer> ts = new TestSubscriber<>(0); + FlattenIterableSubscriber<Integer, Integer> f = new FlattenIterableSubscriber<>(ts, + Functions.justFunction(Arrays.<Integer>asList(1, 2)), 128); + + f.onSubscribe(new BooleanSubscription()); + + f.onNext(1); + + assertFalse(f.isEmpty()); + + assertEquals(1, f.poll().intValue()); + + assertFalse(f.isEmpty()); + + assertEquals(2, f.poll().intValue()); + + assertTrue(f.isEmpty()); + } + + @Test + public void fusionRequestedState() throws Exception { + TestSubscriber<Integer> ts = new TestSubscriber<>(0); + FlattenIterableSubscriber<Integer, Integer> f = new FlattenIterableSubscriber<>(ts, + Functions.justFunction(Arrays.<Integer>asList(1, 2)), 128); + + f.onSubscribe(new BooleanSubscription()); + + f.fusionMode = QueueFuseable.NONE; + + assertEquals(QueueFuseable.NONE, f.requestFusion(QueueFuseable.SYNC)); + + assertEquals(QueueFuseable.NONE, f.requestFusion(QueueFuseable.ASYNC)); + + f.fusionMode = QueueFuseable.SYNC; + + assertEquals(QueueFuseable.SYNC, f.requestFusion(QueueFuseable.SYNC)); + + assertEquals(QueueFuseable.NONE, f.requestFusion(QueueFuseable.ASYNC)); +} +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableForEachTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableForEachTest.java new file mode 100644 index 0000000000..e74d5caf26 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableForEachTest.java @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import static org.junit.Assert.assertEquals; + +import java.util.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class FlowableForEachTest extends RxJavaTest { + + @Test + public void forEachWile() { + final List<Object> list = new ArrayList<>(); + + Flowable.range(1, 5) + .doOnNext(new Consumer<Integer>() { + @Override + public void accept(Integer v) throws Exception { + list.add(v); + } + }) + .forEachWhile(new Predicate<Integer>() { + @Override + public boolean test(Integer v) throws Exception { + return v < 3; + } + }); + + assertEquals(Arrays.asList(1, 2, 3), list); + } + + @Test + public void forEachWileWithError() { + final List<Object> list = new ArrayList<>(); + + Flowable.range(1, 5).concatWith(Flowable.<Integer>error(new TestException())) + .doOnNext(new Consumer<Integer>() { + @Override + public void accept(Integer v) throws Exception { + list.add(v); + } + }) + .forEachWhile(new Predicate<Integer>() { + @Override + public boolean test(Integer v) throws Exception { + return true; + } + }, new Consumer<Throwable>() { + @Override + public void accept(Throwable e) throws Exception { + list.add(100); + } + }); + + assertEquals(Arrays.asList(1, 2, 3, 4, 5, 100), list); + } + + @Test + public void dispose() { + TestHelper.checkDisposed( + Flowable.never() + .forEachWhile(v -> true) + ); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableFromActionTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableFromActionTest.java new file mode 100644 index 0000000000..5395ae3068 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableFromActionTest.java @@ -0,0 +1,214 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +import java.util.List; +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.operators.QueueFuseable; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.schedulers.Schedulers; +import io.reactivex.rxjava3.subscribers.TestSubscriber; +import io.reactivex.rxjava3.testsupport.*; + +public class FlowableFromActionTest extends RxJavaTest { + @Test + public void fromAction() { + final AtomicInteger atomicInteger = new AtomicInteger(); + + Flowable.fromAction(new Action() { + @Override + public void run() throws Exception { + atomicInteger.incrementAndGet(); + } + }) + .test() + .assertResult(); + + assertEquals(1, atomicInteger.get()); + } + + @Test + public void fromActionTwice() { + final AtomicInteger atomicInteger = new AtomicInteger(); + + Action run = new Action() { + @Override + public void run() throws Exception { + atomicInteger.incrementAndGet(); + } + }; + + Flowable.fromAction(run) + .test() + .assertResult(); + + assertEquals(1, atomicInteger.get()); + + Flowable.fromAction(run) + .test() + .assertResult(); + + assertEquals(2, atomicInteger.get()); + } + + @Test + public void fromActionInvokesLazy() { + final AtomicInteger atomicInteger = new AtomicInteger(); + + Flowable<Object> source = Flowable.fromAction(new Action() { + @Override + public void run() throws Exception { + atomicInteger.incrementAndGet(); + } + }); + + assertEquals(0, atomicInteger.get()); + + source + .test() + .assertResult(); + + assertEquals(1, atomicInteger.get()); + } + + @Test + public void fromActionThrows() { + Flowable.fromAction(new Action() { + @Override + public void run() throws Exception { + throw new UnsupportedOperationException(); + } + }) + .test() + .assertFailure(UnsupportedOperationException.class); + } + + @SuppressWarnings("unchecked") + @Test + public void callable() throws Throwable { + final int[] counter = { 0 }; + + Flowable<Void> m = Flowable.fromAction(new Action() { + @Override + public void run() throws Exception { + counter[0]++; + } + }); + + assertTrue(m.getClass().toString(), m instanceof Supplier); + + assertNull(((Supplier<Void>)m).get()); + + assertEquals(1, counter[0]); + } + + @Test + public void noErrorLoss() throws Exception { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + final CountDownLatch cdl1 = new CountDownLatch(1); + final CountDownLatch cdl2 = new CountDownLatch(1); + + TestSubscriber<Object> ts = Flowable.fromAction(new Action() { + @Override + public void run() throws Exception { + cdl1.countDown(); + cdl2.await(5, TimeUnit.SECONDS); + } + }).subscribeOn(Schedulers.single()).test(); + + assertTrue(cdl1.await(5, TimeUnit.SECONDS)); + + ts.cancel(); + + int timeout = 10; + + while (timeout-- > 0 && errors.isEmpty()) { + Thread.sleep(100); + } + + TestHelper.assertUndeliverable(errors, 0, InterruptedException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void disposedUpfront() throws Throwable { + Action run = mock(Action.class); + + Flowable.fromAction(run) + .test(1L, true) + .assertEmpty(); + + verify(run, never()).run(); + } + + @Test + public void cancelWhileRunning() { + final TestSubscriber<Object> ts = new TestSubscriber<>(); + + Flowable.fromAction(new Action() { + @Override + public void run() throws Exception { + ts.cancel(); + } + }) + .subscribeWith(ts) + .assertEmpty(); + + assertTrue(ts.isCancelled()); + } + + @Test + public void asyncFused() throws Throwable { + TestSubscriberEx<Object> ts = new TestSubscriberEx<>(); + ts.setInitialFusionMode(QueueFuseable.ASYNC); + + Action action = mock(Action.class); + + Flowable.fromAction(action) + .subscribe(ts); + + ts.assertFusionMode(QueueFuseable.ASYNC) + .assertResult(); + + verify(action).run(); + } + + @Test + public void syncFusedRejected() throws Throwable { + TestSubscriberEx<Object> ts = new TestSubscriberEx<>(); + ts.setInitialFusionMode(QueueFuseable.SYNC); + + Action action = mock(Action.class); + + Flowable.fromAction(action) + .subscribe(ts); + + ts.assertFusionMode(QueueFuseable.NONE) + .assertResult(); + + verify(action).run(); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableFromArrayTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableFromArrayTest.java new file mode 100644 index 0000000000..d3195ad8b2 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableFromArrayTest.java @@ -0,0 +1,193 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import org.junit.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.functions.Predicate; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.operators.ScalarSupplier; +import io.reactivex.rxjava3.subscribers.TestSubscriber; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class FlowableFromArrayTest extends RxJavaTest { + + Flowable<Integer> create(int n) { + Integer[] array = new Integer[n]; + for (int i = 0; i < n; i++) { + array[i] = i; + } + return Flowable.fromArray(array); + } + + @Test + public void simple() { + TestSubscriber<Integer> ts = new TestSubscriber<>(); + + create(1000).subscribe(ts); + + ts.assertNoErrors(); + ts.assertValueCount(1000); + ts.assertComplete(); + } + + @Test + public void backpressure() { + TestSubscriber<Integer> ts = TestSubscriber.create(0); + + create(1000).subscribe(ts); + + ts.assertNoErrors(); + ts.assertNoValues(); + ts.assertNotComplete(); + + ts.request(10); + + ts.assertNoErrors(); + ts.assertValueCount(10); + ts.assertNotComplete(); + + ts.request(1000); + + ts.assertNoErrors(); + ts.assertValueCount(1000); + ts.assertComplete(); + } + + @Test + public void conditionalBackpressure() { + TestSubscriber<Integer> ts = TestSubscriber.create(0); + + create(1000) + .filter(Functions.alwaysTrue()) + .subscribe(ts); + + ts.assertNoErrors(); + ts.assertNoValues(); + ts.assertNotComplete(); + + ts.request(10); + + ts.assertNoErrors(); + ts.assertValueCount(10); + ts.assertNotComplete(); + + ts.request(1000); + + ts.assertNoErrors(); + ts.assertValueCount(1000); + ts.assertComplete(); + } + + @Test + public void empty() { + Assert.assertSame(Flowable.empty(), Flowable.fromArray(new Object[0])); + } + + @Test + public void just() { + Flowable<Integer> source = Flowable.fromArray(new Integer[] { 1 }); + Assert.assertTrue(source.getClass().toString(), source instanceof ScalarSupplier); + } + + @Test + public void just10Arguments() { + Flowable.just(1, 2, 3, 4, 5, 6, 7, 8, 9, 10) + .test() + .assertResult(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + } + + @Test + public void badRequest() { + TestHelper.assertBadRequestReported(Flowable.just(1, 2, 3)); + } + + @Test + public void conditionalOneIsNull() { + Flowable.fromArray(new Integer[] { null, 1 }) + .filter(Functions.alwaysTrue()) + .test() + .assertFailure(NullPointerException.class); + } + + @Test + public void conditionalOneIsNullSlowPath() { + Flowable.fromArray(new Integer[] { null, 1 }) + .filter(Functions.alwaysTrue()) + .test(2L) + .assertFailure(NullPointerException.class); + } + + @Test + public void conditionalOneByOne() { + Flowable.fromArray(new Integer[] { 1, 2, 3, 4, 5 }) + .filter(Functions.alwaysTrue()) + .rebatchRequests(1) + .test() + .assertResult(1, 2, 3, 4, 5); + } + + @Test + public void conditionalFiltered() { + Flowable.fromArray(new Integer[] { 1, 2, 3, 4, 5 }) + .filter(new Predicate<Integer>() { + @Override + public boolean test(Integer v) throws Exception { + return v % 2 == 0; + } + }) + .test() + .assertResult(2, 4); + } + + @Test + public void conditionalSlowPathCancel() { + Flowable.fromArray(new Integer[] { 1, 2, 3, 4, 5 }) + .filter(Functions.alwaysTrue()) + .subscribeWith(new TestSubscriber<Integer>(5L) { + @Override + public void onNext(Integer t) { + super.onNext(t); + if (t == 1) { + cancel(); + onComplete(); + } + } + }) + .assertResult(1); + } + + @Test + public void conditionalSlowPathSkipCancel() { + Flowable.fromArray(new Integer[] { 1, 2, 3, 4, 5 }) + .filter(new Predicate<Integer>() { + @Override + public boolean test(Integer v) throws Exception { + return v < 2; + } + }) + .subscribeWith(new TestSubscriber<Integer>(5L) { + @Override + public void onNext(Integer t) { + super.onNext(t); + if (t == 1) { + cancel(); + onComplete(); + } + } + }) + .assertResult(1); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableFromCallableTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableFromCallableTest.java new file mode 100644 index 0000000000..b603fd2d9b --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableFromCallableTest.java @@ -0,0 +1,266 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import java.util.List; +import java.util.concurrent.*; + +import org.junit.Test; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.schedulers.Schedulers; +import io.reactivex.rxjava3.subscribers.TestSubscriber; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class FlowableFromCallableTest extends RxJavaTest { + + @SuppressWarnings("unchecked") + @Test + public void shouldNotInvokeFuncUntilSubscription() throws Exception { + Callable<Object> func = mock(Callable.class); + + when(func.call()).thenReturn(new Object()); + + Flowable<Object> fromCallableFlowable = Flowable.fromCallable(func); + + verifyNoInteractions(func); + + fromCallableFlowable.subscribe(); + + verify(func).call(); + } + + @SuppressWarnings("unchecked") + @Test + public void shouldCallOnNextAndOnCompleted() throws Exception { + Callable<String> func = mock(Callable.class); + + when(func.call()).thenReturn("test_value"); + + Flowable<String> fromCallableFlowable = Flowable.fromCallable(func); + + Subscriber<String> subscriber = TestHelper.mockSubscriber(); + + fromCallableFlowable.subscribe(subscriber); + + verify(subscriber).onNext("test_value"); + verify(subscriber).onComplete(); + verify(subscriber, never()).onError(any(Throwable.class)); + } + + @SuppressWarnings("unchecked") + @Test + public void shouldCallOnError() throws Exception { + Callable<Object> func = mock(Callable.class); + + Throwable throwable = new IllegalStateException("Test exception"); + when(func.call()).thenThrow(throwable); + + Flowable<Object> fromCallableFlowable = Flowable.fromCallable(func); + + Subscriber<Object> subscriber = TestHelper.mockSubscriber(); + + fromCallableFlowable.subscribe(subscriber); + + verify(subscriber, never()).onNext(any()); + verify(subscriber, never()).onComplete(); + verify(subscriber).onError(throwable); + } + + @SuppressWarnings("unchecked") + @Test + public void shouldNotDeliverResultIfSubscriberUnsubscribedBeforeEmission() throws Exception { + Callable<String> func = mock(Callable.class); + + final CountDownLatch funcLatch = new CountDownLatch(1); + final CountDownLatch observerLatch = new CountDownLatch(1); + + when(func.call()).thenAnswer(new Answer<String>() { + @Override + public String answer(InvocationOnMock invocation) throws Throwable { + observerLatch.countDown(); + + try { + funcLatch.await(); + } catch (InterruptedException e) { + // It's okay, unsubscription causes Thread interruption + + // Restoring interruption status of the Thread + Thread.currentThread().interrupt(); + } + + return "should_not_be_delivered"; + } + }); + + Flowable<String> fromCallableFlowable = Flowable.fromCallable(func); + + Subscriber<String> subscriber = TestHelper.mockSubscriber(); + + TestSubscriber<String> outer = new TestSubscriber<>(subscriber); + + fromCallableFlowable + .subscribeOn(Schedulers.computation()) + .subscribe(outer); + + // Wait until func will be invoked + observerLatch.await(); + + // Unsubscribing before emission + outer.cancel(); + + // Emitting result + funcLatch.countDown(); + + // func must be invoked + verify(func).call(); + + // Observer must not be notified at all + verify(subscriber).onSubscribe(any(Subscription.class)); + verifyNoMoreInteractions(subscriber); + } + + @Test + public void shouldAllowToThrowCheckedException() { + final Exception checkedException = new Exception("test exception"); + + Flowable<Object> fromCallableFlowable = Flowable.fromCallable(new Callable<Object>() { + @Override + public Object call() throws Exception { + throw checkedException; + } + }); + + Subscriber<Object> subscriber = TestHelper.mockSubscriber(); + + fromCallableFlowable.subscribe(subscriber); + + verify(subscriber).onSubscribe(any(Subscription.class)); + verify(subscriber).onError(checkedException); + verifyNoMoreInteractions(subscriber); + } + + @Test + public void fusedFlatMapExecution() { + final int[] calls = { 0 }; + + Flowable.just(1).flatMap(new Function<Integer, Publisher<? extends Object>>() { + @Override + public Publisher<? extends Object> apply(Integer v) + throws Exception { + return Flowable.fromCallable(new Callable<Object>() { + @Override + public Object call() throws Exception { + return ++calls[0]; + } + }); + } + }) + .test() + .assertResult(1); + + assertEquals(1, calls[0]); + } + + @Test + public void fusedFlatMapExecutionHidden() { + final int[] calls = { 0 }; + + Flowable.just(1).hide().flatMap(new Function<Integer, Publisher<? extends Object>>() { + @Override + public Publisher<? extends Object> apply(Integer v) + throws Exception { + return Flowable.fromCallable(new Callable<Object>() { + @Override + public Object call() throws Exception { + return ++calls[0]; + } + }); + } + }) + .test() + .assertResult(1); + + assertEquals(1, calls[0]); + } + + @Test + public void fusedFlatMapNull() { + Flowable.just(1).flatMap(new Function<Integer, Publisher<? extends Object>>() { + @Override + public Publisher<? extends Object> apply(Integer v) + throws Exception { + return Flowable.fromCallable(new Callable<Object>() { + @Override + public Object call() throws Exception { + return null; + } + }); + } + }) + .test() + .assertFailure(NullPointerException.class); + } + + @Test + public void fusedFlatMapNullHidden() { + Flowable.just(1).hide().flatMap(new Function<Integer, Publisher<? extends Object>>() { + @Override + public Publisher<? extends Object> apply(Integer v) + throws Exception { + return Flowable.fromCallable(new Callable<Object>() { + @Override + public Object call() throws Exception { + return null; + } + }); + } + }) + .test() + .assertFailure(NullPointerException.class); + } + + @Test + public void undeliverableUponCancellation() throws Exception { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + final TestSubscriber<Integer> ts = new TestSubscriber<>(); + + Flowable.fromCallable(new Callable<Integer>() { + @Override + public Integer call() throws Exception { + ts.cancel(); + throw new TestException(); + } + }) + .subscribe(ts); + + ts.assertEmpty(); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableFromCompletableTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableFromCompletableTest.java new file mode 100644 index 0000000000..fc8e4ec977 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableFromCompletableTest.java @@ -0,0 +1,204 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +import java.util.List; +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.fuseable.*; +import io.reactivex.rxjava3.operators.QueueFuseable; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.schedulers.Schedulers; +import io.reactivex.rxjava3.subscribers.TestSubscriber; +import io.reactivex.rxjava3.testsupport.*; + +public class FlowableFromCompletableTest extends RxJavaTest { + @Test + public void fromCompletable() { + final AtomicInteger atomicInteger = new AtomicInteger(); + + Flowable.fromCompletable(Completable.fromAction(new Action() { + @Override + public void run() throws Exception { + atomicInteger.incrementAndGet(); + } + })) + .test() + .assertResult(); + + assertEquals(1, atomicInteger.get()); + } + + @Test + public void fromCompletableTwice() { + final AtomicInteger atomicInteger = new AtomicInteger(); + + Action run = new Action() { + @Override + public void run() throws Exception { + atomicInteger.incrementAndGet(); + } + }; + + Flowable.fromCompletable(Completable.fromAction(run)) + .test() + .assertResult(); + + assertEquals(1, atomicInteger.get()); + + Flowable.fromCompletable(Completable.fromAction(run)) + .test() + .assertResult(); + + assertEquals(2, atomicInteger.get()); + } + + @Test + public void fromCompletableInvokesLazy() { + final AtomicInteger atomicInteger = new AtomicInteger(); + + Flowable<Object> source = Flowable.fromCompletable(Completable.fromAction(new Action() { + @Override + public void run() throws Exception { + atomicInteger.incrementAndGet(); + } + })); + + assertEquals(0, atomicInteger.get()); + + source + .test() + .assertResult(); + + assertEquals(1, atomicInteger.get()); + } + + @Test + public void fromCompletableThrows() { + Flowable.fromCompletable(Completable.fromAction(new Action() { + @Override + public void run() throws Exception { + throw new UnsupportedOperationException(); + } + })) + .test() + .assertFailure(UnsupportedOperationException.class); + } + + @Test + public void noErrorLoss() throws Exception { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + final CountDownLatch cdl1 = new CountDownLatch(1); + final CountDownLatch cdl2 = new CountDownLatch(1); + + TestSubscriber<Object> ts = Flowable.fromCompletable(Completable.fromAction(new Action() { + @Override + public void run() throws Exception { + cdl1.countDown(); + cdl2.await(5, TimeUnit.SECONDS); + } + })) + .subscribeOn(Schedulers.single()).test(); + + assertTrue(cdl1.await(5, TimeUnit.SECONDS)); + + ts.cancel(); + + int timeout = 10; + + while (timeout-- > 0 && errors.isEmpty()) { + Thread.sleep(100); + } + + TestHelper.assertUndeliverable(errors, 0, InterruptedException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void disposedUpfront() throws Throwable { + Action run = mock(Action.class); + + Flowable.fromCompletable(Completable.fromAction(run)) + .test(1L, true) + .assertEmpty(); + + verify(run, never()).run(); + } + + @Test + public void cancelWhileRunning() { + final TestSubscriber<Object> ts = new TestSubscriber<>(); + + Flowable.fromCompletable(Completable.fromAction(new Action() { + @Override + public void run() throws Exception { + ts.cancel(); + } + })) + .subscribeWith(ts) + .assertEmpty(); + + assertTrue(ts.isCancelled()); + } + + @Test + public void asyncFused() throws Throwable { + TestSubscriberEx<Object> ts = new TestSubscriberEx<>(); + ts.setInitialFusionMode(QueueFuseable.ASYNC); + + Action action = mock(Action.class); + + Flowable.fromCompletable(Completable.fromAction(action)) + .subscribe(ts); + + ts.assertFusionMode(QueueFuseable.ASYNC) + .assertResult(); + + verify(action).run(); + } + + @Test + public void syncFusedRejected() throws Throwable { + TestSubscriberEx<Object> ts = new TestSubscriberEx<>(); + ts.setInitialFusionMode(QueueFuseable.SYNC); + + Action action = mock(Action.class); + + Flowable.fromCompletable(Completable.fromAction(action)) + .subscribe(ts); + + ts.assertFusionMode(QueueFuseable.NONE) + .assertResult(); + + verify(action).run(); + } + + @Test + public void upstream() { + Flowable<?> f = Flowable.fromCompletable(Completable.never()); + assertTrue(f instanceof HasUpstreamCompletableSource); + assertSame(Completable.never(), ((HasUpstreamCompletableSource)f).source()); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableFromIterableTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableFromIterableTest.java new file mode 100644 index 0000000000..5fe804a310 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableFromIterableTest.java @@ -0,0 +1,1223 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import java.util.*; +import java.util.concurrent.*; +import java.util.concurrent.atomic.*; + +import org.junit.Test; +import org.mockito.Mockito; +import org.reactivestreams.*; + +import io.reactivex.rxjava3.annotations.NonNull; +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.internal.util.CrashingIterable; +import io.reactivex.rxjava3.operators.QueueFuseable; +import io.reactivex.rxjava3.operators.QueueSubscription; +import io.reactivex.rxjava3.operators.SimpleQueue; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.schedulers.Schedulers; +import io.reactivex.rxjava3.subscribers.*; +import io.reactivex.rxjava3.testsupport.*; + +public class FlowableFromIterableTest extends RxJavaTest { + + @Test + public void listIterable() { + Flowable<String> flowable = Flowable.fromIterable(Arrays.<String> asList("one", "two", "three")); + + Subscriber<String> subscriber = TestHelper.mockSubscriber(); + + flowable.subscribe(subscriber); + + verify(subscriber, times(1)).onNext("one"); + verify(subscriber, times(1)).onNext("two"); + verify(subscriber, times(1)).onNext("three"); + verify(subscriber, Mockito.never()).onError(any(Throwable.class)); + verify(subscriber, times(1)).onComplete(); + } + + /** + * This tests the path that can not optimize based on size so must use setProducer. + */ + @Test + public void rawIterable() { + Iterable<String> it = new Iterable<String>() { + + @Override + public Iterator<String> iterator() { + return new Iterator<String>() { + + int i; + + @Override + public boolean hasNext() { + return i < 3; + } + + @Override + public String next() { + return String.valueOf(++i); + } + + @Override + public void remove() { + } + + }; + } + + }; + Flowable<String> flowable = Flowable.fromIterable(it); + + Subscriber<String> subscriber = TestHelper.mockSubscriber(); + + flowable.subscribe(subscriber); + + verify(subscriber, times(1)).onNext("1"); + verify(subscriber, times(1)).onNext("2"); + verify(subscriber, times(1)).onNext("3"); + verify(subscriber, Mockito.never()).onError(any(Throwable.class)); + verify(subscriber, times(1)).onComplete(); + } + + @Test + public void observableFromIterable() { + Flowable<String> flowable = Flowable.fromIterable(Arrays.<String> asList("one", "two", "three")); + + Subscriber<String> subscriber = TestHelper.mockSubscriber(); + + flowable.subscribe(subscriber); + + verify(subscriber, times(1)).onNext("one"); + verify(subscriber, times(1)).onNext("two"); + verify(subscriber, times(1)).onNext("three"); + verify(subscriber, Mockito.never()).onError(any(Throwable.class)); + verify(subscriber, times(1)).onComplete(); + } + + @Test + public void backpressureViaRequest() { + ArrayList<Integer> list = new ArrayList<>(Flowable.bufferSize()); + for (int i = 1; i <= Flowable.bufferSize() + 1; i++) { + list.add(i); + } + Flowable<Integer> f = Flowable.fromIterable(list); + + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(0L); + + ts.assertNoValues(); + ts.request(1); + + f.subscribe(ts); + + ts.assertValue(1); + ts.request(2); + ts.assertValues(1, 2, 3); + ts.request(3); + ts.assertValues(1, 2, 3, 4, 5, 6); + ts.request(list.size()); + ts.assertTerminated(); + } + + @Test + public void noBackpressure() { + Flowable<Integer> f = Flowable.fromIterable(Arrays.asList(1, 2, 3, 4, 5)); + + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(0L); + + ts.assertNoValues(); + ts.request(Long.MAX_VALUE); // infinite + + f.subscribe(ts); + + ts.assertValues(1, 2, 3, 4, 5); + ts.assertTerminated(); + } + + @Test + public void subscribeMultipleTimes() { + Flowable<Integer> f = Flowable.fromIterable(Arrays.asList(1, 2, 3)); + + for (int i = 0; i < 10; i++) { + TestSubscriber<Integer> ts = new TestSubscriber<>(); + + f.subscribe(ts); + + ts.assertValues(1, 2, 3); + ts.assertNoErrors(); + ts.assertComplete(); + } + } + + @Test + public void fromIterableRequestOverflow() throws InterruptedException { + Flowable<Integer> f = Flowable.fromIterable(Arrays.asList(1, 2, 3, 4)); + + final int expectedCount = 4; + final CountDownLatch latch = new CountDownLatch(expectedCount); + + f.subscribeOn(Schedulers.computation()) + .subscribe(new DefaultSubscriber<Integer>() { + + @Override + public void onStart() { + request(2); + } + + @Override + public void onComplete() { + //ignore + } + + @Override + public void onError(Throwable e) { + throw new RuntimeException(e); + } + + @Override + public void onNext(Integer t) { + latch.countDown(); + request(Long.MAX_VALUE - 1); + }}); + assertTrue(latch.await(10, TimeUnit.SECONDS)); + } + + @Test + public void fromEmptyIterableWhenZeroRequestedShouldStillEmitOnCompletedEagerly() { + + final AtomicBoolean completed = new AtomicBoolean(false); + + Flowable.fromIterable(Collections.emptyList()).subscribe(new DefaultSubscriber<Object>() { + + @Override + public void onStart() { +// request(0); + } + + @Override + public void onComplete() { + completed.set(true); + } + + @Override + public void onError(Throwable e) { + + } + + @Override + public void onNext(Object t) { + + }}); + assertTrue(completed.get()); + } + + @Test + public void doesNotCallIteratorHasNextMoreThanRequiredWithBackpressure() { + final AtomicBoolean called = new AtomicBoolean(false); + Iterable<Integer> iterable = new Iterable<Integer>() { + + @Override + public Iterator<Integer> iterator() { + return new Iterator<Integer>() { + + int count = 1; + + @Override + public void remove() { + // ignore + } + + @Override + public boolean hasNext() { + if (count > 1) { + called.set(true); + return false; + } + return true; + } + + @Override + public Integer next() { + return count++; + } + + }; + } + }; + Flowable.fromIterable(iterable).take(1).subscribe(); + assertFalse(called.get()); + } + + @Test + public void doesNotCallIteratorHasNextMoreThanRequiredFastPath() { + final AtomicBoolean called = new AtomicBoolean(false); + Iterable<Integer> iterable = new Iterable<Integer>() { + + @Override + public Iterator<Integer> iterator() { + return new Iterator<Integer>() { + + @Override + public void remove() { + // ignore + } + + int count = 1; + + @Override + public boolean hasNext() { + if (count > 1) { + called.set(true); + return false; + } + return true; + } + + @Override + public Integer next() { + return count++; + } + + }; + } + }; + Flowable.fromIterable(iterable).subscribe(new DefaultSubscriber<Integer>() { + + @Override + public void onComplete() { + + } + + @Override + public void onError(Throwable e) { + + } + + @Override + public void onNext(Integer t) { + // unsubscribe on first emission + cancel(); + } + }); + assertFalse(called.get()); + } + + @Test + public void getIteratorThrows() { + Iterable<Integer> it = new Iterable<Integer>() { + @Override + public Iterator<Integer> iterator() { + throw new TestException("Forced failure"); + } + }; + + TestSubscriber<Integer> ts = new TestSubscriber<>(); + + Flowable.fromIterable(it).subscribe(ts); + + ts.assertNoValues(); + ts.assertError(TestException.class); + ts.assertNotComplete(); + } + + @Test + public void hasNextThrowsImmediately() { + Iterable<Integer> it = new Iterable<Integer>() { + @Override + public Iterator<Integer> iterator() { + return new Iterator<Integer>() { + @Override + public boolean hasNext() { + throw new TestException("Forced failure"); + } + + @Override + public Integer next() { + return null; + } + + @Override + public void remove() { + // ignored + } + }; + } + }; + + TestSubscriber<Integer> ts = new TestSubscriber<>(); + + Flowable.fromIterable(it).subscribe(ts); + + ts.assertNoValues(); + ts.assertError(TestException.class); + ts.assertNotComplete(); + } + + @Test + public void hasNextThrowsSecondTimeFastpath() { + Iterable<Integer> it = new Iterable<Integer>() { + @Override + public Iterator<Integer> iterator() { + return new Iterator<Integer>() { + int count; + @Override + public boolean hasNext() { + if (++count >= 2) { + throw new TestException("Forced failure"); + } + return true; + } + + @Override + public Integer next() { + return 1; + } + + @Override + public void remove() { + // ignored + } + }; + } + }; + + TestSubscriber<Integer> ts = new TestSubscriber<>(); + + Flowable.fromIterable(it).subscribe(ts); + + ts.assertValues(1); + ts.assertError(TestException.class); + ts.assertNotComplete(); + } + + @Test + public void hasNextThrowsSecondTimeSlowpath() { + Iterable<Integer> it = new Iterable<Integer>() { + @Override + public Iterator<Integer> iterator() { + return new Iterator<Integer>() { + int count; + @Override + public boolean hasNext() { + if (++count >= 2) { + throw new TestException("Forced failure"); + } + return true; + } + + @Override + public Integer next() { + return 1; + } + + @Override + public void remove() { + // ignored + } + }; + } + }; + + TestSubscriber<Integer> ts = new TestSubscriber<>(5); + + Flowable.fromIterable(it).subscribe(ts); + + ts.assertValues(1); + ts.assertError(TestException.class); + ts.assertNotComplete(); + } + + @Test + public void nextThrowsFastpath() { + Iterable<Integer> it = new Iterable<Integer>() { + @Override + public Iterator<Integer> iterator() { + return new Iterator<Integer>() { + @Override + public boolean hasNext() { + return true; + } + + @Override + public Integer next() { + throw new TestException("Forced failure"); + } + + @Override + public void remove() { + // ignored + } + }; + } + }; + + TestSubscriber<Integer> ts = new TestSubscriber<>(); + + Flowable.fromIterable(it).subscribe(ts); + + ts.assertNoValues(); + ts.assertError(TestException.class); + ts.assertNotComplete(); + } + + @Test + public void nextThrowsSlowpath() { + Iterable<Integer> it = new Iterable<Integer>() { + @Override + public Iterator<Integer> iterator() { + return new Iterator<Integer>() { + @Override + public boolean hasNext() { + return true; + } + + @Override + public Integer next() { + throw new TestException("Forced failure"); + } + + @Override + public void remove() { + // ignored + } + }; + } + }; + + TestSubscriber<Integer> ts = new TestSubscriber<>(5); + + Flowable.fromIterable(it).subscribe(ts); + + ts.assertNoValues(); + ts.assertError(TestException.class); + ts.assertNotComplete(); + } + + @Test + public void deadOnArrival() { + Iterable<Integer> it = new Iterable<Integer>() { + @Override + public Iterator<Integer> iterator() { + return new Iterator<Integer>() { + @Override + public boolean hasNext() { + return true; + } + + @Override + public Integer next() { + throw new NoSuchElementException(); + } + + @Override + public void remove() { + // ignored + } + }; + } + }; + + TestSubscriber<Integer> ts = new TestSubscriber<>(5); + ts.cancel(); + + Flowable.fromIterable(it).subscribe(ts); + + ts.assertNoValues(); + ts.assertNoErrors(); + ts.assertNotComplete(); + + } + + @Test + public void fusionWithConcatMap() { + TestSubscriber<Integer> ts = new TestSubscriber<>(); + + Flowable.fromIterable(Arrays.asList(1, 2, 3, 4)).concatMap( + new Function<Integer, Flowable <Integer>>() { + @Override + public Flowable<Integer> apply(Integer v) { + return Flowable.range(v, 2); + } + }).subscribe(ts); + + ts.assertValues(1, 2, 2, 3, 3, 4, 4, 5); + ts.assertNoErrors(); + ts.assertComplete(); + } + + @Test + public void fusedAPICalls() { + Flowable.fromIterable(Arrays.asList(1, 2, 3)) + .subscribe(new FlowableSubscriber<Integer>() { + + @Override + public void onSubscribe(Subscription s) { + @SuppressWarnings("unchecked") + QueueSubscription<Integer> qs = (QueueSubscription<Integer>)s; + + assertFalse(qs.isEmpty()); + + try { + assertEquals(1, qs.poll().intValue()); + } catch (Throwable ex) { + throw new AssertionError(ex); + } + + assertFalse(qs.isEmpty()); + + qs.clear(); + + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + qs.request(-99); + + TestHelper.assertError(errors, 0, IllegalArgumentException.class, "n > 0 required but it was -99"); + } finally { + RxJavaPlugins.reset(); + } + } + + @Override + public void onNext(Integer t) { + } + + @Override + public void onError(Throwable t) { + } + + @Override + public void onComplete() { + } + }); + } + + @Test + public void normalConditional() { + Flowable.fromIterable(Arrays.asList(1, 2, 3, 4, 5)) + .filter(Functions.alwaysTrue()) + .test() + .assertResult(1, 2, 3, 4, 5); + } + + @Test + public void normalConditionalBackpressured() { + Flowable.fromIterable(Arrays.asList(1, 2, 3, 4, 5)) + .filter(Functions.alwaysTrue()) + .test(5L) + .assertResult(1, 2, 3, 4, 5); + } + + @Test + public void normalConditionalBackpressured2() { + Flowable.fromIterable(Arrays.asList(1, 2, 3, 4, 5)) + .filter(Functions.alwaysTrue()) + .to(TestHelper.<Integer>testSubscriber(4L)) + .assertSubscribed() + .assertValues(1, 2, 3, 4) + .assertNoErrors() + .assertNotComplete(); + } + + @Test + public void emptyConditional() { + Flowable.fromIterable(Arrays.asList(1, 2, 3, 4, 5)) + .filter(Functions.alwaysFalse()) + .test() + .assertResult(); + } + + @Test + public void nullConditional() { + Flowable.fromIterable(Arrays.asList(1, null, 3, 4, 5)) + .filter(Functions.alwaysTrue()) + .test() + .assertFailure(NullPointerException.class, 1); + } + + @Test + public void nullConditionalBackpressured() { + Flowable.fromIterable(Arrays.asList(1, null, 3, 4, 5)) + .filter(Functions.alwaysTrue()) + .test(5L) + .assertFailure(NullPointerException.class, 1); + } + + @Test + public void normalConditionalCrash() { + Flowable.fromIterable(new CrashingIterable(100, 2, 100)) + .filter(Functions.alwaysTrue()) + .test() + .assertFailure(TestException.class, 0); + } + + @Test + public void normalConditionalCrash2() { + Flowable.fromIterable(new CrashingIterable(100, 100, 2)) + .filter(Functions.alwaysTrue()) + .test() + .assertFailure(TestException.class, 0); + } + + @Test + public void normalConditionalCrashBackpressured() { + Flowable.fromIterable(new CrashingIterable(100, 2, 100)) + .filter(Functions.alwaysTrue()) + .test(5L) + .assertFailure(TestException.class, 0); + } + + @Test + public void normalConditionalCrashBackpressured2() { + Flowable.fromIterable(new CrashingIterable(100, 100, 2)) + .filter(Functions.alwaysTrue()) + .test(5L) + .assertFailure(TestException.class, 0); + } + + @Test + public void normalConditionalLong() { + Flowable.fromIterable(new CrashingIterable(100, 10 * 1000 * 1000, 10 * 1000 * 1000)) + .filter(Functions.alwaysTrue()) + .take(1000 * 1000) + .to(TestHelper.<Integer>testConsumer()) + .assertSubscribed() + .assertValueCount(1000 * 1000) + .assertNoErrors() + .assertComplete(); + } + + @Test + public void normalConditionalLong2() { + Flowable.fromIterable(new CrashingIterable(100, 10 * 1000 * 1000, 10 * 1000 * 1000)) + .filter(Functions.alwaysTrue()) + .rebatchRequests(128) + .take(1000 * 1000) + .to(TestHelper.<Integer>testConsumer()) + .assertSubscribed() + .assertValueCount(1000 * 1000) + .assertNoErrors() + .assertComplete(); + } + + @Test + public void requestRaceConditional() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final TestSubscriber<Integer> ts = new TestSubscriber<>(0L); + + Runnable r = new Runnable() { + @Override + public void run() { + ts.request(1); + } + }; + + Flowable.fromIterable(Arrays.asList(1, 2, 3, 4)) + .filter(Functions.alwaysTrue()) + .subscribe(ts); + + TestHelper.race(r, r); + } + } + + @Test + public void requestRaceConditional2() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final TestSubscriber<Integer> ts = new TestSubscriber<>(0L); + + Runnable r = new Runnable() { + @Override + public void run() { + ts.request(1); + } + }; + + Flowable.fromIterable(Arrays.asList(1, 2, 3, 4)) + .filter(Functions.alwaysFalse()) + .subscribe(ts); + + TestHelper.race(r, r); + } + } + + @Test + public void requestCancelConditionalRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final TestSubscriber<Integer> ts = new TestSubscriber<>(0L); + + Runnable r1 = new Runnable() { + @Override + public void run() { + ts.request(1); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + ts.cancel(); + } + }; + + Flowable.fromIterable(Arrays.asList(1, 2, 3, 4)) + .filter(Functions.alwaysTrue()) + .subscribe(ts); + + TestHelper.race(r1, r2); + } + } + + @Test + public void requestCancelConditionalRace2() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final TestSubscriber<Integer> ts = new TestSubscriber<>(0L); + + Runnable r1 = new Runnable() { + @Override + public void run() { + ts.request(Long.MAX_VALUE); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + ts.cancel(); + } + }; + + Flowable.fromIterable(Arrays.asList(1, 2, 3, 4)) + .filter(Functions.alwaysTrue()) + .subscribe(ts); + + TestHelper.race(r1, r2); + } + } + + @Test + public void requestCancelRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final TestSubscriber<Integer> ts = new TestSubscriber<>(0L); + + Runnable r1 = new Runnable() { + @Override + public void run() { + ts.request(1); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + ts.cancel(); + } + }; + + Flowable.fromIterable(Arrays.asList(1, 2, 3, 4)) + .subscribe(ts); + + TestHelper.race(r1, r2); + } + } + + @Test + public void requestCancelRace2() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final TestSubscriber<Integer> ts = new TestSubscriber<>(0L); + + Runnable r1 = new Runnable() { + @Override + public void run() { + ts.request(Long.MAX_VALUE); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + ts.cancel(); + } + }; + + Flowable.fromIterable(Arrays.asList(1, 2, 3, 4)) + .subscribe(ts); + + TestHelper.race(r1, r2); + } + } + + @Test + public void fusionRejected() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<Integer>().setInitialFusionMode(QueueFuseable.ASYNC); + + Flowable.fromIterable(Arrays.asList(1, 2, 3)) + .subscribe(ts); + + ts.assertFusionMode(QueueFuseable.NONE) + .assertResult(1, 2, 3); + } + + @Test + public void fusionClear() { + Flowable.fromIterable(Arrays.asList(1, 2, 3)) + .subscribe(new FlowableSubscriber<Integer>() { + @Override + public void onSubscribe(Subscription s) { + @SuppressWarnings("unchecked") + QueueSubscription<Integer> qs = (QueueSubscription<Integer>)s; + + qs.requestFusion(QueueFuseable.ANY); + + try { + assertEquals(1, qs.poll().intValue()); + } catch (Throwable ex) { + fail(ex.toString()); + } + + qs.clear(); + try { + assertNull(qs.poll()); + } catch (Throwable ex) { + fail(ex.toString()); + } + } + + @Override + public void onNext(Integer value) { + } + + @Override + public void onError(Throwable e) { + } + + @Override + public void onComplete() { + } + }); + } + + @Test + public void iteratorThrows() { + Flowable.fromIterable(new CrashingIterable(1, 100, 100)) + .to(TestHelper.<Integer>testConsumer()) + .assertFailureAndMessage(TestException.class, "iterator()"); + } + + @Test + public void hasNext2Throws() { + Flowable.fromIterable(new CrashingIterable(100, 2, 100)) + .to(TestHelper.<Integer>testConsumer()) + .assertFailureAndMessage(TestException.class, "hasNext()", 0); + } + + @Test + public void hasNextCancels() { + final TestSubscriber<Integer> ts = new TestSubscriber<>(); + + Flowable.fromIterable(new Iterable<Integer>() { + @Override + public Iterator<Integer> iterator() { + return new Iterator<Integer>() { + int count; + + @Override + public boolean hasNext() { + if (++count == 2) { + ts.cancel(); + } + return true; + } + + @Override + public Integer next() { + return 1; + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + }; + } + }) + .subscribe(ts); + + ts.assertValue(1) + .assertNoErrors() + .assertNotComplete(); + } + + @Test + public void hasNextCancelsAndCompletesFastPath() { + final TestSubscriber<Integer> ts = new TestSubscriber<>(); + + Flowable.fromIterable(new Iterable<Integer>() { + @Override + public Iterator<Integer> iterator() { + return new Iterator<Integer>() { + int count; + + @Override + public boolean hasNext() { + if (++count == 2) { + ts.cancel(); + return false; + } + return true; + } + + @Override + public Integer next() { + return 1; + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + }; + } + }) + .subscribe(ts); + + ts.assertValue(1) + .assertNoErrors() + .assertNotComplete(); + } + + @Test + public void hasNextCancelsAndCompletesSlowPath() { + final TestSubscriber<Integer> ts = new TestSubscriber<>(10L); + + Flowable.fromIterable(new Iterable<Integer>() { + @Override + public Iterator<Integer> iterator() { + return new Iterator<Integer>() { + int count; + + @Override + public boolean hasNext() { + if (++count == 2) { + ts.cancel(); + return false; + } + return true; + } + + @Override + public Integer next() { + return 1; + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + }; + } + }) + .subscribe(ts); + + ts.assertValue(1) + .assertNoErrors() + .assertNotComplete(); + } + + @Test + public void hasNextCancelsAndCompletesFastPathConditional() { + final TestSubscriber<Integer> ts = new TestSubscriber<>(); + + Flowable.fromIterable(new Iterable<Integer>() { + @Override + public Iterator<Integer> iterator() { + return new Iterator<Integer>() { + int count; + + @Override + public boolean hasNext() { + if (++count == 2) { + ts.cancel(); + return false; + } + return true; + } + + @Override + public Integer next() { + return 1; + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + }; + } + }) + .filter(v -> true) + .subscribe(ts); + + ts.assertValue(1) + .assertNoErrors() + .assertNotComplete(); + } + + @Test + public void hasNextCancelsAndCompletesSlowPathConditional() { + final TestSubscriber<Integer> ts = new TestSubscriber<>(10); + + Flowable.fromIterable(new Iterable<Integer>() { + @Override + public Iterator<Integer> iterator() { + return new Iterator<Integer>() { + int count; + + @Override + public boolean hasNext() { + if (++count == 2) { + ts.cancel(); + return false; + } + return true; + } + + @Override + public Integer next() { + return 1; + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + }; + } + }) + .filter(v -> true) + .subscribe(ts); + + ts.assertValue(1) + .assertNoErrors() + .assertNotComplete(); + } + + @Test + public void fusedPoll() throws Throwable { + AtomicReference<SimpleQueue<?>> queue = new AtomicReference<>(); + + Flowable.fromIterable(Arrays.asList(1)) + .subscribe(new FlowableSubscriber<Integer>() { + @Override + public void onSubscribe(@NonNull Subscription s) { + queue.set((SimpleQueue<?>)s); + ((QueueSubscription<?>)s).requestFusion(QueueFuseable.ANY); + } + + @Override + public void onNext(Integer t) { + } + + @Override + public void onError(Throwable t) { + } + + @Override + public void onComplete() { + } + }); + + SimpleQueue<?> q = queue.get(); + + assertFalse(q.isEmpty()); + + assertEquals(1, q.poll()); + + assertTrue(q.isEmpty()); + + q.clear(); + + assertTrue(q.isEmpty()); + } + + @Test + public void disposeWhileIteratorNext() { + final TestSubscriber<Integer> ts = new TestSubscriber<>(10); + + Flowable.fromIterable(new Iterable<Integer>() { + @Override + public Iterator<Integer> iterator() { + return new Iterator<Integer>() { + @Override + public boolean hasNext() { + return true; + } + + @Override + public Integer next() { + ts.cancel(); + return 1; + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + }; + } + }) + .subscribe(ts); + + ts.assertEmpty(); + } + + @Test + public void disposeWhileIteratorNextConditional() { + final TestSubscriber<Integer> ts = new TestSubscriber<>(10); + + Flowable.fromIterable(new Iterable<Integer>() { + @Override + public Iterator<Integer> iterator() { + return new Iterator<Integer>() { + @Override + public boolean hasNext() { + return true; + } + + @Override + public Integer next() { + ts.cancel(); + return 1; + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + }; + } + }) + .filter(v -> true) + .subscribe(ts); + + ts.assertEmpty(); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableFromMaybeTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableFromMaybeTest.java new file mode 100644 index 0000000000..d33b9f1e40 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableFromMaybeTest.java @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import static org.junit.Assert.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.operators.QueueFuseable; +import io.reactivex.rxjava3.subjects.MaybeSubject; +import io.reactivex.rxjava3.subscribers.TestSubscriber; +import io.reactivex.rxjava3.testsupport.TestSubscriberEx; + +public class FlowableFromMaybeTest extends RxJavaTest { + + @Test + public void success() { + Flowable.fromMaybe(Maybe.just(1).hide()) + .test() + .assertResult(1); + } + + @Test + public void empty() { + Flowable.fromMaybe(Maybe.empty().hide()) + .test() + .assertResult(); + } + + @Test + public void error() { + Flowable.fromMaybe(Maybe.error(new TestException()).hide()) + .test() + .assertFailure(TestException.class); + } + + @Test + public void cancelComposes() { + MaybeSubject<Integer> ms = MaybeSubject.create(); + + TestSubscriber<Integer> ts = Flowable.fromMaybe(ms) + .test(); + + ts.assertEmpty(); + + assertTrue(ms.hasObservers()); + + ts.cancel(); + + assertFalse(ms.hasObservers()); + } + + @Test + public void asyncFusion() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(); + ts.setInitialFusionMode(QueueFuseable.ASYNC); + + Flowable.fromMaybe(Maybe.just(1)) + .subscribe(ts); + + ts + .assertFuseable() + .assertFusionMode(QueueFuseable.ASYNC) + .assertResult(1); + } + + @Test + public void syncFusionRejected() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(); + ts.setInitialFusionMode(QueueFuseable.SYNC); + + Flowable.fromMaybe(Maybe.just(1)) + .subscribe(ts); + + ts + .assertFuseable() + .assertFusionMode(QueueFuseable.NONE) + .assertResult(1); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableFromObservableTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableFromObservableTest.java new file mode 100644 index 0000000000..f9f666d7fd --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableFromObservableTest.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class FlowableFromObservableTest extends RxJavaTest { + @Test + public void dispose() { + TestHelper.checkDisposed(Observable.just(1).toFlowable(BackpressureStrategy.MISSING)); + } + + @Test + public void error() { + Observable.error(new TestException()) + .toFlowable(BackpressureStrategy.MISSING) + .test() + .assertFailure(TestException.class); + } + + @Test + public void all() { + for (BackpressureStrategy mode : BackpressureStrategy.values()) { + Flowable.fromObservable(Observable.range(1, 5), mode) + .test() + .withTag("mode: " + mode) + .assertResult(1, 2, 3, 4, 5); + } + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableFromRunnableTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableFromRunnableTest.java new file mode 100644 index 0000000000..19ef5eb018 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableFromRunnableTest.java @@ -0,0 +1,220 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +import java.util.List; +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.Supplier; +import io.reactivex.rxjava3.operators.QueueFuseable; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.schedulers.Schedulers; +import io.reactivex.rxjava3.subscribers.TestSubscriber; +import io.reactivex.rxjava3.testsupport.*; + +public class FlowableFromRunnableTest extends RxJavaTest { + @Test + public void fromRunnable() { + final AtomicInteger atomicInteger = new AtomicInteger(); + + Flowable.fromRunnable(new Runnable() { + @Override + public void run() { + atomicInteger.incrementAndGet(); + } + }) + .test() + .assertResult(); + + assertEquals(1, atomicInteger.get()); + } + + @Test + public void fromRunnableTwice() { + final AtomicInteger atomicInteger = new AtomicInteger(); + + Runnable run = new Runnable() { + @Override + public void run() { + atomicInteger.incrementAndGet(); + } + }; + + Flowable.fromRunnable(run) + .test() + .assertResult(); + + assertEquals(1, atomicInteger.get()); + + Flowable.fromRunnable(run) + .test() + .assertResult(); + + assertEquals(2, atomicInteger.get()); + } + + @Test + public void fromRunnableInvokesLazy() { + final AtomicInteger atomicInteger = new AtomicInteger(); + + Flowable<Object> source = Flowable.fromRunnable(new Runnable() { + @Override + public void run() { + atomicInteger.incrementAndGet(); + } + }); + + assertEquals(0, atomicInteger.get()); + + source + .test() + .assertResult(); + + assertEquals(1, atomicInteger.get()); + } + + @Test + public void fromRunnableThrows() { + Flowable.fromRunnable(new Runnable() { + @Override + public void run() { + throw new UnsupportedOperationException(); + } + }) + .test() + .assertFailure(UnsupportedOperationException.class); + } + + @SuppressWarnings("unchecked") + @Test + public void callable() throws Throwable { + final int[] counter = { 0 }; + + Flowable<Void> m = Flowable.fromRunnable(new Runnable() { + @Override + public void run() { + counter[0]++; + } + }); + + assertTrue(m.getClass().toString(), m instanceof Supplier); + + assertNull(((Supplier<Void>)m).get()); + + assertEquals(1, counter[0]); + } + + @Test + public void noErrorLoss() throws Exception { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + final CountDownLatch cdl1 = new CountDownLatch(1); + final CountDownLatch cdl2 = new CountDownLatch(1); + + TestSubscriber<Object> ts = Flowable.fromRunnable(new Runnable() { + @Override + public void run() { + cdl1.countDown(); + try { + cdl2.await(5, TimeUnit.SECONDS); + } catch (InterruptedException e) { + e.printStackTrace(); + throw new TestException(e); + } + } + }).subscribeOn(Schedulers.single()).test(); + + assertTrue(cdl1.await(5, TimeUnit.SECONDS)); + + ts.cancel(); + + int timeout = 10; + + while (timeout-- > 0 && errors.isEmpty()) { + Thread.sleep(100); + } + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void disposedUpfront() throws Throwable { + Runnable run = mock(Runnable.class); + + Flowable.fromRunnable(run) + .test(1L, true) + .assertEmpty(); + + verify(run, never()).run(); + } + + @Test + public void cancelWhileRunning() { + final TestSubscriber<Object> ts = new TestSubscriber<>(); + + Flowable.fromRunnable(new Runnable() { + @Override + public void run() { + ts.cancel(); + } + }) + .subscribeWith(ts) + .assertEmpty(); + + assertTrue(ts.isCancelled()); + } + + @Test + public void asyncFused() throws Throwable { + TestSubscriberEx<Object> ts = new TestSubscriberEx<>(); + ts.setInitialFusionMode(QueueFuseable.ASYNC); + + Runnable action = mock(Runnable.class); + + Flowable.fromRunnable(action) + .subscribe(ts); + + ts.assertFusionMode(QueueFuseable.ASYNC) + .assertResult(); + + verify(action).run(); + } + + @Test + public void syncFusedRejected() throws Throwable { + TestSubscriberEx<Object> ts = new TestSubscriberEx<>(); + ts.setInitialFusionMode(QueueFuseable.SYNC); + + Runnable action = mock(Runnable.class); + + Flowable.fromRunnable(action) + .subscribe(ts); + + ts.assertFusionMode(QueueFuseable.NONE) + .assertResult(); + + verify(action).run(); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableFromSingleTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableFromSingleTest.java new file mode 100644 index 0000000000..1aa1503299 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableFromSingleTest.java @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import static org.junit.Assert.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.operators.QueueFuseable; +import io.reactivex.rxjava3.subjects.SingleSubject; +import io.reactivex.rxjava3.subscribers.TestSubscriber; +import io.reactivex.rxjava3.testsupport.TestSubscriberEx; + +public class FlowableFromSingleTest extends RxJavaTest { + + @Test + public void success() { + Flowable.fromSingle(Single.just(1).hide()) + .test() + .assertResult(1); + } + + @Test + public void error() { + Flowable.fromSingle(Single.error(new TestException()).hide()) + .test() + .assertFailure(TestException.class); + } + + @Test + public void cancelComposes() { + SingleSubject<Integer> ms = SingleSubject.create(); + + TestSubscriber<Integer> ts = Flowable.fromSingle(ms) + .test(); + + ts.assertEmpty(); + + assertTrue(ms.hasObservers()); + + ts.cancel(); + + assertFalse(ms.hasObservers()); + } + + @Test + public void asyncFusion() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(); + ts.setInitialFusionMode(QueueFuseable.ASYNC); + + Flowable.fromSingle(Single.just(1)) + .subscribe(ts); + + ts + .assertFuseable() + .assertFusionMode(QueueFuseable.ASYNC) + .assertResult(1); + } + + @Test + public void syncFusionRejected() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(); + ts.setInitialFusionMode(QueueFuseable.SYNC); + + Flowable.fromSingle(Single.just(1)) + .subscribe(ts); + + ts + .assertFuseable() + .assertFusionMode(QueueFuseable.NONE) + .assertResult(1); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableFromSourceTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableFromSourceTest.java new file mode 100644 index 0000000000..6506c010ea --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableFromSourceTest.java @@ -0,0 +1,784 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import java.util.List; + +import org.junit.*; +import org.reactivestreams.Subscription; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.Cancellable; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.processors.PublishProcessor; +import io.reactivex.rxjava3.subscribers.*; +import io.reactivex.rxjava3.testsupport.*; + +public class FlowableFromSourceTest extends RxJavaTest { + + PublishAsyncEmitter source; + + PublishAsyncEmitterNoCancel sourceNoCancel; + + TestSubscriberEx<Integer> ts; + + @Before + public void before() { + source = new PublishAsyncEmitter(); + sourceNoCancel = new PublishAsyncEmitterNoCancel(); + ts = new TestSubscriberEx<>(0L); + } + + @Test + public void normalBuffered() { + Flowable.create(source, BackpressureStrategy.BUFFER).subscribe(ts); + + source.onNext(1); + source.onNext(2); + source.onComplete(); + + ts.request(1); + + ts.assertValue(1); + + Assert.assertEquals(0, source.requested()); + + ts.request(1); + + ts.assertValues(1, 2); + ts.assertNoErrors(); + ts.assertComplete(); + } + + @Test + public void normalDrop() { + Flowable.create(source, BackpressureStrategy.DROP).subscribe(ts); + + source.onNext(1); + + ts.request(1); + + ts.assertNoValues(); + + source.onNext(2); + source.onComplete(); + + ts.assertValues(2); + ts.assertNoErrors(); + ts.assertComplete(); + } + + @Test + public void normalLatest() { + Flowable.create(source, BackpressureStrategy.LATEST).subscribe(ts); + + source.onNext(1); + + source.onNext(2); + source.onComplete(); + + ts.assertNoValues(); + + ts.request(1); + + ts.assertValues(2); + ts.assertNoErrors(); + ts.assertComplete(); + } + + @Test + public void normalMissing() { + Flowable.create(source, BackpressureStrategy.MISSING).subscribe(ts); + + source.onNext(1); + source.onNext(2); + source.onComplete(); + + ts.assertValues(1, 2); + ts.assertNoErrors(); + ts.assertComplete(); + } + + @Test + public void normalMissingRequested() { + Flowable.create(source, BackpressureStrategy.MISSING).subscribe(ts); + ts.request(2); + + source.onNext(1); + source.onNext(2); + source.onComplete(); + + ts.assertValues(1, 2); + ts.assertNoErrors(); + ts.assertComplete(); + } + + @Test + public void normalError() { + Flowable.create(source, BackpressureStrategy.ERROR).subscribe(ts); + + source.onNext(1); + source.onNext(2); + source.onComplete(); + + ts.assertNoValues(); + ts.assertError(MissingBackpressureException.class); + ts.assertNotComplete(); + + Assert.assertEquals("create: " + MissingBackpressureException.DEFAULT_MESSAGE, ts.errors().get(0).getMessage()); + } + + @Test + public void errorBuffered() { + Flowable.create(source, BackpressureStrategy.BUFFER).subscribe(ts); + + source.onNext(1); + source.onNext(2); + source.onError(new TestException()); + + ts.request(1); + + ts.assertValue(1); + + ts.request(1); + + ts.assertValues(1, 2); + ts.assertError(TestException.class); + ts.assertNotComplete(); + } + + @Test + public void errorLatest() { + Flowable.create(source, BackpressureStrategy.LATEST).subscribe(ts); + + source.onNext(1); + source.onNext(2); + source.onError(new TestException()); + + ts.assertNoValues(); + + ts.request(1); + + ts.assertValues(2); + ts.assertError(TestException.class); + ts.assertNotComplete(); + } + + @Test + public void errorMissing() { + Flowable.create(source, BackpressureStrategy.MISSING).subscribe(ts); + + source.onNext(1); + source.onNext(2); + source.onError(new TestException()); + + ts.request(1); + + ts.assertValues(1, 2); + ts.assertError(TestException.class); + ts.assertNotComplete(); + } + + @Test + public void unsubscribedBuffer() { + Flowable.create(source, BackpressureStrategy.BUFFER).subscribe(ts); + ts.cancel(); + + source.onNext(1); + source.onNext(2); + source.onError(new TestException()); + + ts.request(1); + + ts.assertNoValues(); + ts.assertNoErrors(); + ts.assertNotComplete(); + } + + @Test + public void unsubscribedLatest() { + Flowable.create(source, BackpressureStrategy.LATEST).subscribe(ts); + ts.cancel(); + + source.onNext(1); + source.onNext(2); + source.onError(new TestException()); + + ts.request(1); + + ts.assertNoValues(); + ts.assertNoErrors(); + ts.assertNotComplete(); + } + + @Test + public void unsubscribedError() { + Flowable.create(source, BackpressureStrategy.ERROR).subscribe(ts); + ts.cancel(); + + source.onNext(1); + source.onNext(2); + source.onError(new TestException()); + + ts.request(1); + + ts.assertNoValues(); + ts.assertNoErrors(); + ts.assertNotComplete(); + } + + @Test + public void unsubscribedDrop() { + Flowable.create(source, BackpressureStrategy.DROP).subscribe(ts); + ts.cancel(); + + source.onNext(1); + source.onNext(2); + source.onError(new TestException()); + + ts.request(1); + + ts.assertNoValues(); + ts.assertNoErrors(); + ts.assertNotComplete(); + } + + @Test + public void unsubscribedMissing() { + Flowable.create(source, BackpressureStrategy.MISSING).subscribe(ts); + ts.cancel(); + + source.onNext(1); + source.onNext(2); + source.onError(new TestException()); + + ts.request(1); + + ts.assertNoValues(); + ts.assertNoErrors(); + ts.assertNotComplete(); + } + + @Test + public void unsubscribedNoCancelBuffer() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + Flowable.create(sourceNoCancel, BackpressureStrategy.BUFFER).subscribe(ts); + ts.cancel(); + + sourceNoCancel.onNext(1); + sourceNoCancel.onNext(2); + sourceNoCancel.onError(new TestException()); + + ts.request(1); + + ts.assertNoValues(); + ts.assertNoErrors(); + ts.assertNotComplete(); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void unsubscribedNoCancelLatest() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + Flowable.create(sourceNoCancel, BackpressureStrategy.LATEST).subscribe(ts); + ts.cancel(); + + sourceNoCancel.onNext(1); + sourceNoCancel.onNext(2); + sourceNoCancel.onError(new TestException()); + + ts.request(1); + + ts.assertNoValues(); + ts.assertNoErrors(); + ts.assertNotComplete(); + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void unsubscribedNoCancelError() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + Flowable.create(sourceNoCancel, BackpressureStrategy.ERROR).subscribe(ts); + ts.cancel(); + + sourceNoCancel.onNext(1); + sourceNoCancel.onNext(2); + sourceNoCancel.onError(new TestException()); + + ts.request(1); + + ts.assertNoValues(); + ts.assertNoErrors(); + ts.assertNotComplete(); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void unsubscribedNoCancelDrop() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + Flowable.create(sourceNoCancel, BackpressureStrategy.DROP).subscribe(ts); + ts.cancel(); + + sourceNoCancel.onNext(1); + sourceNoCancel.onNext(2); + sourceNoCancel.onError(new TestException()); + + ts.request(1); + + ts.assertNoValues(); + ts.assertNoErrors(); + ts.assertNotComplete(); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void unsubscribedNoCancelMissing() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + Flowable.create(sourceNoCancel, BackpressureStrategy.MISSING).subscribe(ts); + ts.cancel(); + + sourceNoCancel.onNext(1); + sourceNoCancel.onNext(2); + sourceNoCancel.onError(new TestException()); + + ts.request(1); + + ts.assertNoValues(); + ts.assertNoErrors(); + ts.assertNotComplete(); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void deferredRequest() { + Flowable.create(source, BackpressureStrategy.BUFFER).subscribe(ts); + + source.onNext(1); + source.onNext(2); + source.onComplete(); + + ts.request(2); + + ts.assertValues(1, 2); + ts.assertNoErrors(); + ts.assertComplete(); + } + + @Test + public void take() { + Flowable.create(source, BackpressureStrategy.BUFFER).take(2).subscribe(ts); + + source.onNext(1); + source.onNext(2); + source.onComplete(); + + ts.request(2); + + ts.assertValues(1, 2); + ts.assertNoErrors(); + ts.assertComplete(); + } + + @Test + public void takeOne() { + Flowable.create(source, BackpressureStrategy.BUFFER).take(1).subscribe(ts); + ts.request(2); + + source.onNext(1); + source.onNext(2); + source.onComplete(); + + ts.assertValues(1); + ts.assertNoErrors(); + ts.assertComplete(); + } + + @Test + public void requestExact() { + Flowable.create(source, BackpressureStrategy.BUFFER).subscribe(ts); + ts.request(2); + + source.onNext(1); + source.onNext(2); + source.onComplete(); + + ts.assertValues(1, 2); + ts.assertNoErrors(); + ts.assertComplete(); + } + + @Test + public void takeNoCancel() { + Flowable.create(sourceNoCancel, BackpressureStrategy.BUFFER).take(2).subscribe(ts); + + sourceNoCancel.onNext(1); + sourceNoCancel.onNext(2); + sourceNoCancel.onComplete(); + + ts.request(2); + + ts.assertValues(1, 2); + ts.assertNoErrors(); + ts.assertComplete(); + } + + @Test + public void takeOneNoCancel() { + Flowable.create(sourceNoCancel, BackpressureStrategy.BUFFER).take(1).subscribe(ts); + ts.request(2); + + sourceNoCancel.onNext(1); + sourceNoCancel.onNext(2); + sourceNoCancel.onComplete(); + + ts.assertValues(1); + ts.assertNoErrors(); + ts.assertComplete(); + } + + @Test + public void unsubscribeNoCancel() { + Flowable.create(sourceNoCancel, BackpressureStrategy.BUFFER).subscribe(ts); + ts.request(2); + + sourceNoCancel.onNext(1); + + ts.cancel(); + + sourceNoCancel.onNext(2); + + ts.assertValues(1); + ts.assertNoErrors(); + ts.assertNotComplete(); + } + + @Test + public void unsubscribeInline() { + TestSubscriber<Integer> ts1 = new TestSubscriber<Integer>() { + @Override + public void onNext(Integer t) { + super.onNext(t); + cancel(); + } + }; + + Flowable.create(sourceNoCancel, BackpressureStrategy.BUFFER).subscribe(ts1); + + sourceNoCancel.onNext(1); + + ts1.assertValues(1); + ts1.assertNoErrors(); + ts1.assertNotComplete(); + } + + @Test + public void completeInline() { + Flowable.create(sourceNoCancel, BackpressureStrategy.BUFFER).subscribe(ts); + + sourceNoCancel.onNext(1); + sourceNoCancel.onComplete(); + + ts.request(2); + + ts.assertValues(1); + ts.assertNoErrors(); + ts.assertComplete(); + } + + @Test + public void errorInline() { + Flowable.create(sourceNoCancel, BackpressureStrategy.BUFFER).subscribe(ts); + + sourceNoCancel.onNext(1); + sourceNoCancel.onError(new TestException()); + + ts.request(2); + + ts.assertValues(1); + ts.assertError(TestException.class); + ts.assertNotComplete(); + } + + @Test + public void requestInline() { + TestSubscriber<Integer> ts1 = new TestSubscriber<Integer>(1L) { + @Override + public void onNext(Integer t) { + super.onNext(t); + request(1); + } + }; + + Flowable.create(sourceNoCancel, BackpressureStrategy.BUFFER).subscribe(ts1); + + sourceNoCancel.onNext(1); + sourceNoCancel.onNext(2); + + ts1.assertValues(1, 2); + ts1.assertNoErrors(); + ts1.assertNotComplete(); + } + + @Test + public void unsubscribeInlineLatest() { + TestSubscriber<Integer> ts1 = new TestSubscriber<Integer>() { + @Override + public void onNext(Integer t) { + super.onNext(t); + cancel(); + } + }; + + Flowable.create(sourceNoCancel, BackpressureStrategy.LATEST).subscribe(ts1); + + sourceNoCancel.onNext(1); + + ts1.assertValues(1); + ts1.assertNoErrors(); + ts1.assertNotComplete(); + } + + @Test + public void unsubscribeInlineExactLatest() { + TestSubscriber<Integer> ts1 = new TestSubscriber<Integer>(1L) { + @Override + public void onNext(Integer t) { + super.onNext(t); + cancel(); + } + }; + + Flowable.create(sourceNoCancel, BackpressureStrategy.LATEST).subscribe(ts1); + + sourceNoCancel.onNext(1); + + ts1.assertValues(1); + ts1.assertNoErrors(); + ts1.assertNotComplete(); + } + + @Test + public void completeInlineLatest() { + Flowable.create(sourceNoCancel, BackpressureStrategy.LATEST).subscribe(ts); + + sourceNoCancel.onNext(1); + sourceNoCancel.onComplete(); + + ts.request(2); + + ts.assertValues(1); + ts.assertNoErrors(); + ts.assertComplete(); + } + + @Test + public void completeInlineExactLatest() { + Flowable.create(sourceNoCancel, BackpressureStrategy.LATEST).subscribe(ts); + + sourceNoCancel.onNext(1); + sourceNoCancel.onComplete(); + + ts.request(1); + + ts.assertValues(1); + ts.assertNoErrors(); + ts.assertComplete(); + } + + @Test + public void errorInlineLatest() { + Flowable.create(sourceNoCancel, BackpressureStrategy.LATEST).subscribe(ts); + + sourceNoCancel.onNext(1); + sourceNoCancel.onError(new TestException()); + + ts.request(2); + + ts.assertValues(1); + ts.assertError(TestException.class); + ts.assertNotComplete(); + } + + @Test + public void requestInlineLatest() { + TestSubscriber<Integer> ts1 = new TestSubscriber<Integer>(1L) { + @Override + public void onNext(Integer t) { + super.onNext(t); + request(1); + } + }; + + Flowable.create(sourceNoCancel, BackpressureStrategy.LATEST).subscribe(ts1); + + sourceNoCancel.onNext(1); + sourceNoCancel.onNext(2); + + ts1.assertValues(1, 2); + ts1.assertNoErrors(); + ts1.assertNotComplete(); + } + + static final class PublishAsyncEmitter implements FlowableOnSubscribe<Integer>, FlowableSubscriber<Integer> { + + final PublishProcessor<Integer> processor; + + FlowableEmitter<Integer> current; + + PublishAsyncEmitter() { + this.processor = PublishProcessor.create(); + } + + long requested() { + return current.requested(); + } + + @Override + public void subscribe(final FlowableEmitter<Integer> t) { + + this.current = t; + + final ResourceSubscriber<Integer> as = new ResourceSubscriber<Integer>() { + + @Override + public void onComplete() { + t.onComplete(); + } + + @Override + public void onError(Throwable e) { + t.onError(e); + } + + @Override + public void onNext(Integer v) { + t.onNext(v); + } + + }; + + processor.subscribe(as); + + t.setCancellable(new Cancellable() { + @Override + public void cancel() throws Exception { + as.dispose(); + } + });; + } + + @Override + public void onSubscribe(Subscription s) { + s.request(Long.MAX_VALUE); + } + + @Override + public void onNext(Integer t) { + processor.onNext(t); + } + + @Override + public void onError(Throwable e) { + processor.onError(e); + } + + @Override + public void onComplete() { + processor.onComplete(); + } + } + + static final class PublishAsyncEmitterNoCancel implements FlowableOnSubscribe<Integer>, FlowableSubscriber<Integer> { + + final PublishProcessor<Integer> processor; + + PublishAsyncEmitterNoCancel() { + this.processor = PublishProcessor.create(); + } + + @Override + public void subscribe(final FlowableEmitter<Integer> t) { + + processor.subscribe(new FlowableSubscriber<Integer>() { + + @Override + public void onSubscribe(Subscription s) { + s.request(Long.MAX_VALUE); + } + + @Override + public void onComplete() { + t.onComplete(); + } + + @Override + public void onError(Throwable e) { + t.onError(e); + } + + @Override + public void onNext(Integer v) { + t.onNext(v); + } + + }); + } + + @Override + public void onSubscribe(Subscription s) { + s.request(Long.MAX_VALUE); + } + + @Override + public void onNext(Integer t) { + processor.onNext(t); + } + + @Override + public void onError(Throwable e) { + processor.onError(e); + } + + @Override + public void onComplete() { + processor.onComplete(); + } + } + +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableFromSupplierTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableFromSupplierTest.java new file mode 100644 index 0000000000..423b1409c1 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableFromSupplierTest.java @@ -0,0 +1,266 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import java.util.List; +import java.util.concurrent.CountDownLatch; + +import org.junit.Test; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.schedulers.Schedulers; +import io.reactivex.rxjava3.subscribers.TestSubscriber; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class FlowableFromSupplierTest extends RxJavaTest { + + @SuppressWarnings("unchecked") + @Test + public void shouldNotInvokeFuncUntilSubscription() throws Throwable { + Supplier<Object> func = mock(Supplier.class); + + when(func.get()).thenReturn(new Object()); + + Flowable<Object> fromSupplierFlowable = Flowable.fromSupplier(func); + + verifyNoInteractions(func); + + fromSupplierFlowable.subscribe(); + + verify(func).get(); + } + + @SuppressWarnings("unchecked") + @Test + public void shouldCallOnNextAndOnCompleted() throws Throwable { + Supplier<String> func = mock(Supplier.class); + + when(func.get()).thenReturn("test_value"); + + Flowable<String> fromSupplierFlowable = Flowable.fromSupplier(func); + + Subscriber<String> subscriber = TestHelper.mockSubscriber(); + + fromSupplierFlowable.subscribe(subscriber); + + verify(subscriber).onNext("test_value"); + verify(subscriber).onComplete(); + verify(subscriber, never()).onError(any(Throwable.class)); + } + + @SuppressWarnings("unchecked") + @Test + public void shouldCallOnError() throws Throwable { + Supplier<Object> func = mock(Supplier.class); + + Throwable throwable = new IllegalStateException("Test exception"); + when(func.get()).thenThrow(throwable); + + Flowable<Object> fromSupplierFlowable = Flowable.fromSupplier(func); + + Subscriber<Object> subscriber = TestHelper.mockSubscriber(); + + fromSupplierFlowable.subscribe(subscriber); + + verify(subscriber, never()).onNext(any()); + verify(subscriber, never()).onComplete(); + verify(subscriber).onError(throwable); + } + + @SuppressWarnings("unchecked") + @Test + public void shouldNotDeliverResultIfSubscriberUnsubscribedBeforeEmission() throws Throwable { + Supplier<String> func = mock(Supplier.class); + + final CountDownLatch funcLatch = new CountDownLatch(1); + final CountDownLatch observerLatch = new CountDownLatch(1); + + when(func.get()).thenAnswer(new Answer<String>() { + @Override + public String answer(InvocationOnMock invocation) throws Throwable { + observerLatch.countDown(); + + try { + funcLatch.await(); + } catch (InterruptedException e) { + // It's okay, unsubscription causes Thread interruption + + // Restoring interruption status of the Thread + Thread.currentThread().interrupt(); + } + + return "should_not_be_delivered"; + } + }); + + Flowable<String> fromSupplierFlowable = Flowable.fromSupplier(func); + + Subscriber<String> subscriber = TestHelper.mockSubscriber(); + + TestSubscriber<String> outer = new TestSubscriber<>(subscriber); + + fromSupplierFlowable + .subscribeOn(Schedulers.computation()) + .subscribe(outer); + + // Wait until func will be invoked + observerLatch.await(); + + // Unsubscribing before emission + outer.cancel(); + + // Emitting result + funcLatch.countDown(); + + // func must be invoked + verify(func).get(); + + // Observer must not be notified at all + verify(subscriber).onSubscribe(any(Subscription.class)); + verifyNoMoreInteractions(subscriber); + } + + @Test + public void shouldAllowToThrowCheckedException() { + final Exception checkedException = new Exception("test exception"); + + Flowable<Object> fromSupplierFlowable = Flowable.fromSupplier(new Supplier<Object>() { + @Override + public Object get() throws Exception { + throw checkedException; + } + }); + + Subscriber<Object> subscriber = TestHelper.mockSubscriber(); + + fromSupplierFlowable.subscribe(subscriber); + + verify(subscriber).onSubscribe(any(Subscription.class)); + verify(subscriber).onError(checkedException); + verifyNoMoreInteractions(subscriber); + } + + @Test + public void fusedFlatMapExecution() { + final int[] calls = { 0 }; + + Flowable.just(1).flatMap(new Function<Integer, Publisher<? extends Object>>() { + @Override + public Publisher<? extends Object> apply(Integer v) + throws Exception { + return Flowable.fromSupplier(new Supplier<Object>() { + @Override + public Object get() throws Exception { + return ++calls[0]; + } + }); + } + }) + .test() + .assertResult(1); + + assertEquals(1, calls[0]); + } + + @Test + public void fusedFlatMapExecutionHidden() { + final int[] calls = { 0 }; + + Flowable.just(1).hide().flatMap(new Function<Integer, Publisher<? extends Object>>() { + @Override + public Publisher<? extends Object> apply(Integer v) + throws Exception { + return Flowable.fromSupplier(new Supplier<Object>() { + @Override + public Object get() throws Exception { + return ++calls[0]; + } + }); + } + }) + .test() + .assertResult(1); + + assertEquals(1, calls[0]); + } + + @Test + public void fusedFlatMapNull() { + Flowable.just(1).flatMap(new Function<Integer, Publisher<? extends Object>>() { + @Override + public Publisher<? extends Object> apply(Integer v) + throws Exception { + return Flowable.fromSupplier(new Supplier<Object>() { + @Override + public Object get() throws Exception { + return null; + } + }); + } + }) + .test() + .assertFailure(NullPointerException.class); + } + + @Test + public void fusedFlatMapNullHidden() { + Flowable.just(1).hide().flatMap(new Function<Integer, Publisher<? extends Object>>() { + @Override + public Publisher<? extends Object> apply(Integer v) + throws Exception { + return Flowable.fromSupplier(new Supplier<Object>() { + @Override + public Object get() throws Exception { + return null; + } + }); + } + }) + .test() + .assertFailure(NullPointerException.class); + } + + @Test + public void undeliverableUponCancellation() throws Exception { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + final TestSubscriber<Integer> ts = new TestSubscriber<>(); + + Flowable.fromSupplier(new Supplier<Integer>() { + @Override + public Integer get() throws Exception { + ts.cancel(); + throw new TestException(); + } + }) + .subscribe(ts); + + ts.assertEmpty(); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableGenerateTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableGenerateTest.java new file mode 100644 index 0000000000..9274685118 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableGenerateTest.java @@ -0,0 +1,298 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import static org.junit.Assert.assertEquals; + +import java.util.List; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.subscribers.TestSubscriber; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class FlowableGenerateTest extends RxJavaTest { + + @Test + public void statefulBiconsumer() { + Flowable.generate(new Supplier<Object>() { + @Override + public Object get() throws Exception { + return 10; + } + }, new BiConsumer<Object, Emitter<Object>>() { + @Override + public void accept(Object s, Emitter<Object> e) throws Exception { + e.onNext(s); + } + }, new Consumer<Object>() { + @Override + public void accept(Object d) throws Exception { + + } + }) + .take(5) + .test() + .assertResult(10, 10, 10, 10, 10); + } + + @Test + public void stateSupplierThrows() { + Flowable.generate(new Supplier<Object>() { + @Override + public Object get() throws Exception { + throw new TestException(); + } + }, new BiConsumer<Object, Emitter<Object>>() { + @Override + public void accept(Object s, Emitter<Object> e) throws Exception { + e.onNext(s); + } + }, Functions.emptyConsumer()) + .test() + .assertFailure(TestException.class); + } + + @Test + public void generatorThrows() { + Flowable.generate(new Supplier<Object>() { + @Override + public Object get() throws Exception { + return 1; + } + }, new BiConsumer<Object, Emitter<Object>>() { + @Override + public void accept(Object s, Emitter<Object> e) throws Exception { + throw new TestException(); + } + }, Functions.emptyConsumer()) + .test() + .assertFailure(TestException.class); + } + + @Test + public void disposerThrows() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + Flowable.generate(new Supplier<Object>() { + @Override + public Object get() throws Exception { + return 1; + } + }, new BiConsumer<Object, Emitter<Object>>() { + @Override + public void accept(Object s, Emitter<Object> e) throws Exception { + e.onComplete(); + } + }, new Consumer<Object>() { + @Override + public void accept(Object d) throws Exception { + throw new TestException(); + } + }) + .test() + .assertResult(); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void dispose() { + TestHelper.checkDisposed(Flowable.generate(new Supplier<Object>() { + @Override + public Object get() throws Exception { + return 1; + } + }, new BiConsumer<Object, Emitter<Object>>() { + @Override + public void accept(Object s, Emitter<Object> e) throws Exception { + e.onComplete(); + } + }, Functions.emptyConsumer())); + } + + @Test + public void nullError() { + final int[] call = { 0 }; + Flowable.generate(Functions.justSupplier(1), + new BiConsumer<Integer, Emitter<Object>>() { + @Override + public void accept(Integer s, Emitter<Object> e) throws Exception { + try { + e.onError(null); + } catch (NullPointerException ex) { + call[0]++; + } + } + }, Functions.emptyConsumer()) + .test() + .assertFailure(NullPointerException.class); + + assertEquals(0, call[0]); + } + + @Test + public void badRequest() { + TestHelper.assertBadRequestReported(Flowable.generate(new Supplier<Object>() { + @Override + public Object get() throws Exception { + return 1; + } + }, new BiConsumer<Object, Emitter<Object>>() { + @Override + public void accept(Object s, Emitter<Object> e) throws Exception { + e.onComplete(); + } + }, Functions.emptyConsumer())); + } + + @Test + public void rebatchAndTake() { + Flowable.generate(new Supplier<Object>() { + @Override + public Object get() throws Exception { + return 1; + } + }, new BiConsumer<Object, Emitter<Object>>() { + @Override + public void accept(Object s, Emitter<Object> e) throws Exception { + e.onNext(1); + } + }, Functions.emptyConsumer()) + .rebatchRequests(1) + .take(5) + .test() + .assertResult(1, 1, 1, 1, 1); + } + + @Test + public void backpressure() { + Flowable.generate(new Supplier<Object>() { + @Override + public Object get() throws Exception { + return 1; + } + }, new BiConsumer<Object, Emitter<Object>>() { + @Override + public void accept(Object s, Emitter<Object> e) throws Exception { + e.onNext(1); + } + }, Functions.emptyConsumer()) + .rebatchRequests(1) + .to(TestHelper.<Object>testSubscriber(5L)) + .assertSubscribed() + .assertValues(1, 1, 1, 1, 1) + .assertNoErrors() + .assertNotComplete(); + } + + @Test + public void requestRace() { + Flowable<Object> source = Flowable.generate(new Supplier<Object>() { + @Override + public Object get() throws Exception { + return 1; + } + }, new BiConsumer<Object, Emitter<Object>>() { + @Override + public void accept(Object s, Emitter<Object> e) throws Exception { + e.onNext(1); + } + }, Functions.emptyConsumer()); + + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final TestSubscriber<Object> ts = source.test(0L); + + Runnable r = new Runnable() { + @Override + public void run() { + for (int j = 0; j < 500; j++) { + ts.request(1); + } + } + }; + + TestHelper.race(r, r); + + ts.assertValueCount(1000); + } + } + + @Test + public void multipleOnNext() { + Flowable.generate(new Consumer<Emitter<Object>>() { + @Override + public void accept(Emitter<Object> e) throws Exception { + e.onNext(1); + e.onNext(2); + } + }) + .test(1) + .assertFailure(IllegalStateException.class, 1); + } + + @Test + public void multipleOnError() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + Flowable.generate(new Consumer<Emitter<Object>>() { + @Override + public void accept(Emitter<Object> e) throws Exception { + e.onError(new TestException("First")); + e.onError(new TestException("Second")); + } + }) + .test(1) + .assertFailure(TestException.class); + + TestHelper.assertUndeliverable(errors, 0, TestException.class, "Second"); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void multipleOnComplete() { + Flowable.generate(new Consumer<Emitter<Object>>() { + @Override + public void accept(Emitter<Object> e) throws Exception { + e.onComplete(); + e.onComplete(); + } + }) + .test(1) + .assertResult(); + } + + @Test + public void onNextAfterOnComplete() { + Flowable.generate(new Consumer<Emitter<Object>>() { + @Override + public void accept(Emitter<Object> e) throws Exception { + e.onComplete(); + e.onNext(1); + } + }) + .test() + .assertResult(); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableGroupByTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableGroupByTest.java new file mode 100644 index 0000000000..d0d7dbea44 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableGroupByTest.java @@ -0,0 +1,2958 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +import java.io.IOException; +import java.time.Duration; +import java.util.*; +import java.util.concurrent.*; +import java.util.concurrent.atomic.*; + +import org.junit.Test; +import org.mockito.Mockito; +import org.reactivestreams.*; + +import com.google.common.base.Ticker; +import com.google.common.cache.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.flowables.GroupedFlowable; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.internal.schedulers.ImmediateThinScheduler; +import io.reactivex.rxjava3.internal.subscriptions.BooleanSubscription; +import io.reactivex.rxjava3.operators.QueueFuseable; +import io.reactivex.rxjava3.processors.PublishProcessor; +import io.reactivex.rxjava3.schedulers.Schedulers; +import io.reactivex.rxjava3.subjects.PublishSubject; +import io.reactivex.rxjava3.subscribers.*; +import io.reactivex.rxjava3.testsupport.*; + +public class FlowableGroupByTest extends RxJavaTest { + + static Function<GroupedFlowable<Integer, Integer>, Flowable<Integer>> FLATTEN_INTEGER = new Function<GroupedFlowable<Integer, Integer>, Flowable<Integer>>() { + + @Override + public Flowable<Integer> apply(GroupedFlowable<Integer, Integer> t) { + return t; + } + + }; + + final Function<String, Integer> length = new Function<String, Integer>() { + @Override + public Integer apply(String s) { + return s.length(); + } + }; + + @Test + public void groupBy() { + Flowable<String> source = Flowable.just("one", "two", "three", "four", "five", "six"); + Flowable<GroupedFlowable<Integer, String>> grouped = source.groupBy(length); + + Map<Integer, Collection<String>> map = toMap(grouped); + + assertEquals(3, map.size()); + assertArrayEquals(Arrays.asList("one", "two", "six").toArray(), map.get(3).toArray()); + assertArrayEquals(Arrays.asList("four", "five").toArray(), map.get(4).toArray()); + assertArrayEquals(Arrays.asList("three").toArray(), map.get(5).toArray()); + } + + @Test + public void groupByWithElementSelector() { + Flowable<String> source = Flowable.just("one", "two", "three", "four", "five", "six"); + Flowable<GroupedFlowable<Integer, Integer>> grouped = source.groupBy(length, length); + + Map<Integer, Collection<Integer>> map = toMap(grouped); + + assertEquals(3, map.size()); + assertArrayEquals(Arrays.asList(3, 3, 3).toArray(), map.get(3).toArray()); + assertArrayEquals(Arrays.asList(4, 4).toArray(), map.get(4).toArray()); + assertArrayEquals(Arrays.asList(5).toArray(), map.get(5).toArray()); + } + + @Test + public void groupByWithElementSelector2() { + Flowable<String> source = Flowable.just("one", "two", "three", "four", "five", "six"); + Flowable<GroupedFlowable<Integer, Integer>> grouped = source.groupBy(length, length); + + Map<Integer, Collection<Integer>> map = toMap(grouped); + + assertEquals(3, map.size()); + assertArrayEquals(Arrays.asList(3, 3, 3).toArray(), map.get(3).toArray()); + assertArrayEquals(Arrays.asList(4, 4).toArray(), map.get(4).toArray()); + assertArrayEquals(Arrays.asList(5).toArray(), map.get(5).toArray()); + } + + @Test + public void empty() { + Flowable<String> source = Flowable.empty(); + Flowable<GroupedFlowable<Integer, String>> grouped = source.groupBy(length); + + Map<Integer, Collection<String>> map = toMap(grouped); + + assertTrue(map.isEmpty()); + } + + @Test + @SuppressUndeliverable + public void error() { + Flowable<String> sourceStrings = Flowable.just("one", "two", "three", "four", "five", "six"); + Flowable<String> errorSource = Flowable.error(new TestException("forced failure")); + Flowable<String> source = Flowable.concat(sourceStrings, errorSource); + + Flowable<GroupedFlowable<Integer, String>> grouped = source.groupBy(length); + + final AtomicInteger groupCounter = new AtomicInteger(); + final AtomicInteger eventCounter = new AtomicInteger(); + final AtomicReference<Throwable> error = new AtomicReference<>(); + + grouped.flatMap(new Function<GroupedFlowable<Integer, String>, Flowable<String>>() { + + @Override + public Flowable<String> apply(final GroupedFlowable<Integer, String> f) { + groupCounter.incrementAndGet(); + return f.map(new Function<String, String>() { + + @Override + public String apply(String v) { + return "Event => key: " + f.getKey() + " value: " + v; + } + }); + } + }).subscribe(new DefaultSubscriber<String>() { + + @Override + public void onComplete() { + + } + + @Override + public void onError(Throwable e) { +// e.printStackTrace(); + error.set(e); + } + + @Override + public void onNext(String v) { + eventCounter.incrementAndGet(); + System.out.println(v); + + } + }); + + assertEquals(3, groupCounter.get()); + assertEquals(6, eventCounter.get()); + assertNotNull(error.get()); + assertTrue("" + error.get(), error.get() instanceof TestException); + assertEquals(error.get().getMessage(), "forced failure"); + } + + private static <K, V> Map<K, Collection<V>> toMap(Flowable<GroupedFlowable<K, V>> flowable) { + + final ConcurrentHashMap<K, Collection<V>> result = new ConcurrentHashMap<>(); + + flowable.doOnNext(new Consumer<GroupedFlowable<K, V>>() { + + @Override + public void accept(final GroupedFlowable<K, V> f) { + result.put(f.getKey(), new ConcurrentLinkedQueue<>()); + f.subscribe(new Consumer<V>() { + + @Override + public void accept(V v) { + result.get(f.getKey()).add(v); + } + + }); + } + }).blockingSubscribe(); + + return result; + } + + /** + * Assert that only a single subscription to a stream occurs and that all events are received. + * + * @throws Throwable some method call is declared throws + */ + @Test + public void groupedEventStream() throws Throwable { + + final AtomicInteger eventCounter = new AtomicInteger(); + final AtomicInteger subscribeCounter = new AtomicInteger(); + final AtomicInteger groupCounter = new AtomicInteger(); + final CountDownLatch latch = new CountDownLatch(1); + final int count = 100; + final int groupCount = 2; + + Flowable<Event> es = Flowable.unsafeCreate(new Publisher<Event>() { + + @Override + public void subscribe(final Subscriber<? super Event> subscriber) { + subscriber.onSubscribe(new BooleanSubscription()); + System.out.println("*** Subscribing to EventStream ***"); + subscribeCounter.incrementAndGet(); + new Thread(new Runnable() { + + @Override + public void run() { + for (int i = 0; i < count; i++) { + Event e = new Event(); + e.source = i % groupCount; + e.message = "Event-" + i; + subscriber.onNext(e); + } + subscriber.onComplete(); + } + + }).start(); + } + + }); + + es.groupBy(new Function<Event, Integer>() { + + @Override + public Integer apply(Event e) { + return e.source; + } + }).flatMap(new Function<GroupedFlowable<Integer, Event>, Flowable<String>>() { + + @Override + public Flowable<String> apply(GroupedFlowable<Integer, Event> eventGroupedFlowable) { + System.out.println("GroupedFlowable Key: " + eventGroupedFlowable.getKey()); + groupCounter.incrementAndGet(); + + return eventGroupedFlowable.map(new Function<Event, String>() { + + @Override + public String apply(Event event) { + return "Source: " + event.source + " Message: " + event.message; + } + }); + + } + }).subscribe(new DefaultSubscriber<String>() { + + @Override + public void onComplete() { + latch.countDown(); + } + + @Override + public void onError(Throwable e) { + e.printStackTrace(); + latch.countDown(); + } + + @Override + public void onNext(String outputMessage) { + System.out.println(outputMessage); + eventCounter.incrementAndGet(); + } + }); + + latch.await(5000, TimeUnit.MILLISECONDS); + assertEquals(1, subscribeCounter.get()); + assertEquals(groupCount, groupCounter.get()); + assertEquals(count, eventCounter.get()); + + } + + /* + * We will only take 1 group with 20 events from it and then unsubscribe. + */ + @Test + public void unsubscribeOnNestedTakeAndSyncInfiniteStream() throws InterruptedException { + final AtomicInteger subscribeCounter = new AtomicInteger(); + final AtomicInteger sentEventCounter = new AtomicInteger(); + doTestUnsubscribeOnNestedTakeAndAsyncInfiniteStream(SYNC_INFINITE_OBSERVABLE_OF_EVENT(2, subscribeCounter, sentEventCounter), subscribeCounter); + Thread.sleep(500); + assertEquals(39, sentEventCounter.get()); + } + + /* + * We will only take 1 group with 20 events from it and then unsubscribe. + */ + @Test + public void unsubscribeOnNestedTakeAndAsyncInfiniteStream() throws InterruptedException { + final AtomicInteger subscribeCounter = new AtomicInteger(); + final AtomicInteger sentEventCounter = new AtomicInteger(); + doTestUnsubscribeOnNestedTakeAndAsyncInfiniteStream(ASYNC_INFINITE_OBSERVABLE_OF_EVENT(2, subscribeCounter, sentEventCounter), subscribeCounter); + Thread.sleep(500); + assertEquals(39, sentEventCounter.get()); + } + + private void doTestUnsubscribeOnNestedTakeAndAsyncInfiniteStream(Flowable<Event> es, AtomicInteger subscribeCounter) throws InterruptedException { + final AtomicInteger eventCounter = new AtomicInteger(); + final AtomicInteger groupCounter = new AtomicInteger(); + final CountDownLatch latch = new CountDownLatch(1); + + es.groupBy(new Function<Event, Integer>() { + + @Override + public Integer apply(Event e) { + return e.source; + } + }) + .take(1) // we want only the first group + .flatMap(new Function<GroupedFlowable<Integer, Event>, Flowable<String>>() { + + @Override + public Flowable<String> apply(GroupedFlowable<Integer, Event> eventGroupedFlowable) { + System.out.println("testUnsubscribe => GroupedFlowable Key: " + eventGroupedFlowable.getKey()); + groupCounter.incrementAndGet(); + + return eventGroupedFlowable + .take(20) // limit to only 20 events on this group + .map(new Function<Event, String>() { + + @Override + public String apply(Event event) { + return "testUnsubscribe => Source: " + event.source + " Message: " + event.message; + } + }); + + } + }).subscribe(new DefaultSubscriber<String>() { + + @Override + public void onComplete() { + latch.countDown(); + } + + @Override + public void onError(Throwable e) { + e.printStackTrace(); + latch.countDown(); + } + + @Override + public void onNext(String outputMessage) { + System.out.println(outputMessage); + eventCounter.incrementAndGet(); + } + }); + + if (!latch.await(2000, TimeUnit.MILLISECONDS)) { + fail("timed out so likely did not unsubscribe correctly"); + } + assertEquals(1, subscribeCounter.get()); + assertEquals(1, groupCounter.get()); + assertEquals(20, eventCounter.get()); + // sentEvents will go until 'eventCounter' hits 20 and then unsubscribes + // which means it will also send (but ignore) the 19/20 events for the other group + // It will not however send all 100 events. + } + + @Test + public void unsubscribeViaTakeOnGroupThenMergeAndTake() { + final AtomicInteger subscribeCounter = new AtomicInteger(); + final AtomicInteger sentEventCounter = new AtomicInteger(); + final AtomicInteger eventCounter = new AtomicInteger(); + + SYNC_INFINITE_OBSERVABLE_OF_EVENT(4, subscribeCounter, sentEventCounter) + .groupBy(new Function<Event, Integer>() { + + @Override + public Integer apply(Event e) { + return e.source; + } + }) + // take 2 of the 4 groups + .take(2) + .flatMap(new Function<GroupedFlowable<Integer, Event>, Flowable<String>>() { + + @Override + public Flowable<String> apply(GroupedFlowable<Integer, Event> eventGroupedFlowable) { + return eventGroupedFlowable + .map(new Function<Event, String>() { + + @Override + public String apply(Event event) { + return "testUnsubscribe => Source: " + event.source + " Message: " + event.message; + } + }); + + } + }) + .take(30).subscribe(new Consumer<String>() { + + @Override + public void accept(String s) { + eventCounter.incrementAndGet(); + System.out.println("=> " + s); + } + + }); + + assertEquals(30, eventCounter.get()); + // we should send 28 additional events that are filtered out as they are in the groups we skip + assertEquals(58, sentEventCounter.get()); + } + + @Test + public void unsubscribeViaTakeOnGroupThenTakeOnInner() { + final AtomicInteger subscribeCounter = new AtomicInteger(); + final AtomicInteger sentEventCounter = new AtomicInteger(); + final AtomicInteger eventCounter = new AtomicInteger(); + + SYNC_INFINITE_OBSERVABLE_OF_EVENT(4, subscribeCounter, sentEventCounter) + .groupBy(new Function<Event, Integer>() { + + @Override + public Integer apply(Event e) { + return e.source; + } + }) + // take 2 of the 4 groups + .take(2) + .flatMap(new Function<GroupedFlowable<Integer, Event>, Flowable<String>>() { + + @Override + public Flowable<String> apply(GroupedFlowable<Integer, Event> eventGroupedFlowable) { + int numToTake = 0; + if (eventGroupedFlowable.getKey() == 1) { + numToTake = 10; + } else if (eventGroupedFlowable.getKey() == 2) { + numToTake = 5; + } + return eventGroupedFlowable + .take(numToTake) + .map(new Function<Event, String>() { + + @Override + public String apply(Event event) { + return "testUnsubscribe => Source: " + event.source + " Message: " + event.message; + } + }); + + } + }) + .subscribe(new Consumer<String>() { + + @Override + public void accept(String s) { + eventCounter.incrementAndGet(); + System.out.println("=> " + s); + } + + }); + + assertEquals(15, eventCounter.get()); + // we should send 22 additional events that are filtered out as they are skipped while taking the 15 we want + assertEquals(37, sentEventCounter.get()); + } + + @Test + public void staggeredCompletion() throws InterruptedException { + final AtomicInteger eventCounter = new AtomicInteger(); + final CountDownLatch latch = new CountDownLatch(1); + Flowable.range(0, 100) + .groupBy(new Function<Integer, Integer>() { + + @Override + public Integer apply(Integer i) { + return i % 2; + } + }) + .flatMap(new Function<GroupedFlowable<Integer, Integer>, Flowable<Integer>>() { + + @Override + public Flowable<Integer> apply(GroupedFlowable<Integer, Integer> group) { + if (group.getKey() == 0) { + return group.delay(100, TimeUnit.MILLISECONDS).map(new Function<Integer, Integer>() { + @Override + public Integer apply(Integer t) { + return t * 10; + } + + }); + } else { + return group; + } + } + }) + .subscribe(new DefaultSubscriber<Integer>() { + + @Override + public void onComplete() { + System.out.println("=> onComplete"); + latch.countDown(); + } + + @Override + public void onError(Throwable e) { + e.printStackTrace(); + latch.countDown(); + } + + @Override + public void onNext(Integer s) { + eventCounter.incrementAndGet(); + System.out.println("=> " + s); + } + }); + + if (!latch.await(3000, TimeUnit.MILLISECONDS)) { + fail("timed out"); + } + + assertEquals(100, eventCounter.get()); + } + + @Test + public void completionIfInnerNotSubscribed() throws InterruptedException { + final CountDownLatch latch = new CountDownLatch(1); + final AtomicInteger eventCounter = new AtomicInteger(); + Flowable.range(0, 100) + .groupBy(new Function<Integer, Integer>() { + + @Override + public Integer apply(Integer i) { + return i % 2; + } + }) + .subscribe(new DefaultSubscriber<GroupedFlowable<Integer, Integer>>() { + + @Override + public void onComplete() { + latch.countDown(); + } + + @Override + public void onError(Throwable e) { + e.printStackTrace(); + latch.countDown(); + } + + @Override + public void onNext(GroupedFlowable<Integer, Integer> s) { + eventCounter.incrementAndGet(); + System.out.println("=> " + s); + } + }); + if (!latch.await(500, TimeUnit.MILLISECONDS)) { + fail("timed out - never got completion"); + } + // Behavior change: groups not subscribed immediately will be automatically abandoned + // so this leads to group recreation + assertEquals(100, eventCounter.get()); + } + + @Test + public void ignoringGroups() { + final AtomicInteger subscribeCounter = new AtomicInteger(); + final AtomicInteger sentEventCounter = new AtomicInteger(); + final AtomicInteger eventCounter = new AtomicInteger(); + + SYNC_INFINITE_OBSERVABLE_OF_EVENT(4, subscribeCounter, sentEventCounter) + .groupBy(new Function<Event, Integer>() { + + @Override + public Integer apply(Event e) { + return e.source; + } + }) + .flatMap(new Function<GroupedFlowable<Integer, Event>, Flowable<String>>() { + + @Override + public Flowable<String> apply(GroupedFlowable<Integer, Event> eventGroupedFlowable) { + Flowable<Event> eventStream = eventGroupedFlowable; + if (eventGroupedFlowable.getKey() >= 2) { + // filter these + eventStream = eventGroupedFlowable.filter(new Predicate<Event>() { + @Override + public boolean test(Event t1) { + return false; + } + }); + } + + return eventStream + .map(new Function<Event, String>() { + + @Override + public String apply(Event event) { + return "testUnsubscribe => Source: " + event.source + " Message: " + event.message; + } + }); + + } + }) + .take(30).subscribe(new Consumer<String>() { + + @Override + public void accept(String s) { + eventCounter.incrementAndGet(); + System.out.println("=> " + s); + } + + }); + + assertEquals(30, eventCounter.get()); + // we should send 30 additional events that are filtered out as they are in the groups we skip + assertEquals(60, sentEventCounter.get()); + } + + @Test + public void firstGroupsCompleteAndParentSlowToThenEmitFinalGroupsAndThenComplete() throws InterruptedException { + final CountDownLatch first = new CountDownLatch(2); // there are two groups to first complete + final ArrayList<String> results = new ArrayList<>(); + Flowable.unsafeCreate(new Publisher<Integer>() { + + @Override + public void subscribe(Subscriber<? super Integer> sub) { + sub.onSubscribe(new BooleanSubscription()); + sub.onNext(1); + sub.onNext(2); + sub.onNext(1); + sub.onNext(2); + try { + first.await(); + } catch (InterruptedException e) { + sub.onError(e); + return; + } + sub.onNext(3); + sub.onNext(3); + sub.onComplete(); + } + + }).groupBy(new Function<Integer, Integer>() { + + @Override + public Integer apply(Integer t) { + return t; + } + + }).flatMap(new Function<GroupedFlowable<Integer, Integer>, Flowable<String>>() { + + @Override + public Flowable<String> apply(final GroupedFlowable<Integer, Integer> group) { + if (group.getKey() < 3) { + return group.map(new Function<Integer, String>() { + + @Override + public String apply(Integer t1) { + return "first groups: " + t1; + } + + }) + // must take(2) so an onComplete + unsubscribe happens on these first 2 groups + .take(2).doOnComplete(new Action() { + + @Override + public void run() { + first.countDown(); + } + + }); + } else { + return group.map(new Function<Integer, String>() { + + @Override + public String apply(Integer t1) { + return "last group: " + t1; + } + + }); + } + } + + }).blockingForEach(new Consumer<String>() { + + @Override + public void accept(String s) { + results.add(s); + } + + }); + + System.out.println("Results: " + results); + assertEquals(6, results.size()); + } + + @Test + public void firstGroupsCompleteAndParentSlowToThenEmitFinalGroupsWhichThenSubscribesOnAndDelaysAndThenCompletes() throws InterruptedException { + System.err.println("----------------------------------------------------------------------------------------------"); + final CountDownLatch first = new CountDownLatch(2); // there are two groups to first complete + final ArrayList<String> results = new ArrayList<>(); + Flowable.unsafeCreate(new Publisher<Integer>() { + + @Override + public void subscribe(Subscriber<? super Integer> sub) { + sub.onSubscribe(new BooleanSubscription()); + sub.onNext(1); + sub.onNext(2); + sub.onNext(1); + sub.onNext(2); + try { + first.await(); + } catch (InterruptedException e) { + sub.onError(e); + return; + } + sub.onNext(3); + sub.onNext(3); + sub.onComplete(); + } + + }).groupBy(new Function<Integer, Integer>() { + + @Override + public Integer apply(Integer t) { + return t; + } + + }).flatMap(new Function<GroupedFlowable<Integer, Integer>, Flowable<String>>() { + + @Override + public Flowable<String> apply(final GroupedFlowable<Integer, Integer> group) { + if (group.getKey() < 3) { + return group.map(new Function<Integer, String>() { + + @Override + public String apply(Integer t1) { + return "first groups: " + t1; + } + + }) + // must take(2) so an onComplete + unsubscribe happens on these first 2 groups + .take(2).doOnComplete(new Action() { + + @Override + public void run() { + first.countDown(); + } + + }); + } else { + return group.subscribeOn(Schedulers.newThread()).delay(400, TimeUnit.MILLISECONDS).map(new Function<Integer, String>() { + + @Override + public String apply(Integer t1) { + return "last group: " + t1; + } + + }).doOnEach(new Consumer<Notification<String>>() { + + @Override + public void accept(Notification<String> t1) { + System.err.println("subscribeOn notification => " + t1); + } + + }); + } + } + + }).doOnEach(new Consumer<Notification<String>>() { + + @Override + public void accept(Notification<String> t1) { + System.err.println("outer notification => " + t1); + } + + }).blockingForEach(new Consumer<String>() { + + @Override + public void accept(String s) { + results.add(s); + } + + }); + + System.out.println("Results: " + results); + assertEquals(6, results.size()); + } + + @Test + public void firstGroupsCompleteAndParentSlowToThenEmitFinalGroupsWhichThenObservesOnAndDelaysAndThenCompletes() throws InterruptedException { + final CountDownLatch first = new CountDownLatch(2); // there are two groups to first complete + final ArrayList<String> results = new ArrayList<>(); + Flowable.unsafeCreate(new Publisher<Integer>() { + + @Override + public void subscribe(Subscriber<? super Integer> sub) { + sub.onSubscribe(new BooleanSubscription()); + sub.onNext(1); + sub.onNext(2); + sub.onNext(1); + sub.onNext(2); + try { + first.await(); + } catch (InterruptedException e) { + sub.onError(e); + return; + } + sub.onNext(3); + sub.onNext(3); + sub.onComplete(); + } + + }).groupBy(new Function<Integer, Integer>() { + + @Override + public Integer apply(Integer t) { + return t; + } + + }).flatMap(new Function<GroupedFlowable<Integer, Integer>, Flowable<String>>() { + + @Override + public Flowable<String> apply(final GroupedFlowable<Integer, Integer> group) { + if (group.getKey() < 3) { + return group.map(new Function<Integer, String>() { + + @Override + public String apply(Integer t1) { + return "first groups: " + t1; + } + + }) + // must take(2) so an onComplete + unsubscribe happens on these first 2 groups + .take(2).doOnComplete(new Action() { + + @Override + public void run() { + first.countDown(); + } + + }); + } else { + return group.observeOn(Schedulers.newThread()).delay(400, TimeUnit.MILLISECONDS).map(new Function<Integer, String>() { + + @Override + public String apply(Integer t1) { + return "last group: " + t1; + } + + }); + } + } + + }).blockingForEach(new Consumer<String>() { + + @Override + public void accept(String s) { + results.add(s); + } + + }); + + System.out.println("Results: " + results); + assertEquals(6, results.size()); + } + + @Test + public void groupsWithNestedSubscribeOn() throws InterruptedException { + final ArrayList<String> results = new ArrayList<>(); + Flowable.unsafeCreate(new Publisher<Integer>() { + + @Override + public void subscribe(Subscriber<? super Integer> sub) { + sub.onSubscribe(new BooleanSubscription()); + sub.onNext(1); + sub.onNext(2); + sub.onNext(1); + sub.onNext(2); + sub.onComplete(); + } + + }).groupBy(new Function<Integer, Integer>() { + + @Override + public Integer apply(Integer t) { + return t; + } + + }).flatMap(new Function<GroupedFlowable<Integer, Integer>, Flowable<String>>() { + + @Override + public Flowable<String> apply(final GroupedFlowable<Integer, Integer> group) { + return group.subscribeOn(Schedulers.newThread()).map(new Function<Integer, String>() { + + @Override + public String apply(Integer t1) { + System.out.println("Received: " + t1 + " on group : " + group.getKey()); + return "first groups: " + t1; + } + + }); + } + + }).doOnEach(new Consumer<Notification<String>>() { + + @Override + public void accept(Notification<String> t1) { + System.out.println("notification => " + t1); + } + + }).blockingForEach(new Consumer<String>() { + + @Override + public void accept(String s) { + results.add(s); + } + + }); + + System.out.println("Results: " + results); + assertEquals(4, results.size()); + } + + @Test + public void groupsWithNestedObserveOn() throws InterruptedException { + final ArrayList<String> results = new ArrayList<>(); + Flowable.unsafeCreate(new Publisher<Integer>() { + + @Override + public void subscribe(Subscriber<? super Integer> sub) { + sub.onSubscribe(new BooleanSubscription()); + sub.onNext(1); + sub.onNext(2); + sub.onNext(1); + sub.onNext(2); + sub.onComplete(); + } + + }).groupBy(new Function<Integer, Integer>() { + + @Override + public Integer apply(Integer t) { + return t; + } + + }).flatMap(new Function<GroupedFlowable<Integer, Integer>, Flowable<String>>() { + + @Override + public Flowable<String> apply(final GroupedFlowable<Integer, Integer> group) { + return group.observeOn(Schedulers.newThread()).delay(400, TimeUnit.MILLISECONDS).map(new Function<Integer, String>() { + + @Override + public String apply(Integer t1) { + return "first groups: " + t1; + } + + }); + } + + }).blockingForEach(new Consumer<String>() { + + @Override + public void accept(String s) { + results.add(s); + } + + }); + + System.out.println("Results: " + results); + assertEquals(4, results.size()); + } + + private static class Event { + int source; + String message; + + @Override + public String toString() { + return "Event => source: " + source + " message: " + message; + } + } + + Flowable<Event> ASYNC_INFINITE_OBSERVABLE_OF_EVENT(final int numGroups, final AtomicInteger subscribeCounter, final AtomicInteger sentEventCounter) { + return SYNC_INFINITE_OBSERVABLE_OF_EVENT(numGroups, subscribeCounter, sentEventCounter).subscribeOn(Schedulers.newThread()); + }; + + Flowable<Event> SYNC_INFINITE_OBSERVABLE_OF_EVENT(final int numGroups, final AtomicInteger subscribeCounter, final AtomicInteger sentEventCounter) { + return Flowable.unsafeCreate(new Publisher<Event>() { + + @Override + public void subscribe(final Subscriber<? super Event> op) { + BooleanSubscription bs = new BooleanSubscription(); + op.onSubscribe(bs); + subscribeCounter.incrementAndGet(); + int i = 0; + while (!bs.isCancelled()) { + i++; + Event e = new Event(); + e.source = i % numGroups; + e.message = "Event-" + i; + op.onNext(e); + sentEventCounter.incrementAndGet(); + } + op.onComplete(); + } + + }); + }; + + @Test + public void groupByOnAsynchronousSourceAcceptsMultipleSubscriptions() throws InterruptedException { + + // choose an asynchronous source + Flowable<Long> source = Flowable.interval(10, TimeUnit.MILLISECONDS).take(1); + + // apply groupBy to the source + Flowable<GroupedFlowable<Boolean, Long>> stream = source.groupBy(IS_EVEN); + + // create two observers + Subscriber<GroupedFlowable<Boolean, Long>> f1 = TestHelper.mockSubscriber(); + Subscriber<GroupedFlowable<Boolean, Long>> f2 = TestHelper.mockSubscriber(); + + // subscribe with the observers + stream.subscribe(f1); + stream.subscribe(f2); + + // check that subscriptions were successful + verify(f1, never()).onError(Mockito.<Throwable> any()); + verify(f2, never()).onError(Mockito.<Throwable> any()); + } + + private static Function<Long, Boolean> IS_EVEN = new Function<Long, Boolean>() { + + @Override + public Boolean apply(Long n) { + return n % 2 == 0; + } + }; + + private static Function<Integer, Boolean> IS_EVEN2 = new Function<Integer, Boolean>() { + + @Override + public Boolean apply(Integer n) { + return n % 2 == 0; + } + }; + + @Test + public void groupByBackpressure() throws InterruptedException { + + TestSubscriber<String> ts = new TestSubscriber<>(); + + Flowable.range(1, 4000) + .groupBy(IS_EVEN2) + .flatMap(new Function<GroupedFlowable<Boolean, Integer>, Flowable<String>>() { + + @Override + public Flowable<String> apply(final GroupedFlowable<Boolean, Integer> g) { + return g.observeOn(Schedulers.computation()).map(new Function<Integer, String>() { + + @Override + public String apply(Integer l) { + if (g.getKey()) { + try { + Thread.sleep(1); + } catch (InterruptedException e) { + } + return l + " is even."; + } else { + return l + " is odd."; + } + } + + }); + } + + }).subscribe(ts); + ts.awaitDone(5, TimeUnit.SECONDS); + ts.assertNoErrors(); + } + + <T, R> Function<T, R> just(final R value) { + return new Function<T, R>() { + @Override + public R apply(T t1) { + return value; + } + }; + } + + <T> Function<Integer, T> fail(T dummy) { + return new Function<Integer, T>() { + @Override + public T apply(Integer t1) { + throw new RuntimeException("Forced failure"); + } + }; + } + + <T, R> Function<T, R> fail2(R dummy2) { + return new Function<T, R>() { + @Override + public R apply(T t1) { + throw new RuntimeException("Forced failure"); + } + }; + } + + Function<Integer, Integer> dbl = new Function<Integer, Integer>() { + @Override + public Integer apply(Integer t1) { + return t1 * 2; + } + }; + Function<Integer, Integer> identity = new Function<Integer, Integer>() { + @Override + public Integer apply(Integer v) { + return v; + } + }; + + @Test + public void normalBehavior() { + Flowable<String> source = Flowable.fromIterable(Arrays.asList( + " foo", + " FoO ", + "baR ", + "foO ", + " Baz ", + " qux ", + " bar", + " BAR ", + "FOO ", + "baz ", + " bAZ ", + " fOo " + )); + + /* + * foo FoO foO FOO fOo + * baR bar BAR + * Baz baz bAZ + * qux + * + */ + Function<String, String> keysel = new Function<String, String>() { + @Override + public String apply(String t1) { + return t1.trim().toLowerCase(); + } + }; + Function<String, String> valuesel = new Function<String, String>() { + @Override + public String apply(String t1) { + return t1 + t1; + } + }; + + Flowable<String> m = source.groupBy(keysel, valuesel) + .flatMap(new Function<GroupedFlowable<String, String>, Publisher<String>>() { + @Override + public Publisher<String> apply(final GroupedFlowable<String, String> g) { + System.out.println("-----------> NEXT: " + g.getKey()); + return g.take(2).map(new Function<String, String>() { + + int count; + + @Override + public String apply(String v) { + System.out.println(v); + return g.getKey() + "-" + count++; + } + + }); + } + }); + + TestSubscriber<String> ts = new TestSubscriber<>(); + m.subscribe(ts); + ts.awaitDone(5, TimeUnit.SECONDS); + System.out.println("ts .get " + ts.values()); + ts.assertNoErrors(); + assertEquals(ts.values(), + Arrays.asList("foo-0", "foo-1", "bar-0", "foo-0", "baz-0", "qux-0", "bar-1", "bar-0", "foo-1", "baz-1", "baz-0", "foo-0")); + + } + + @Test + public void keySelectorThrows() { + Flowable<Integer> source = Flowable.just(0, 1, 2, 3, 4, 5, 6); + + Flowable<Integer> m = source.groupBy(fail(0), dbl).flatMap(FLATTEN_INTEGER); + + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(); + m.subscribe(ts); + ts.awaitDone(5, TimeUnit.SECONDS); + assertEquals(1, ts.errors().size()); + ts.assertNoValues(); + } + + @Test + @SuppressUndeliverable + public void valueSelectorThrows() { + Flowable<Integer> source = Flowable.just(0, 1, 2, 3, 4, 5, 6); + + Flowable<Integer> m = source.groupBy(identity, fail(0)).flatMap(FLATTEN_INTEGER); + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(); + m.subscribe(ts); + ts.awaitDone(5, TimeUnit.SECONDS); + assertEquals(1, ts.errors().size()); + ts.assertNoValues(); + + } + + @Test + public void innerEscapeCompleted() { + Flowable<Integer> source = Flowable.just(0); + + Flowable<Integer> m = source.groupBy(identity, dbl).flatMap(FLATTEN_INTEGER); + + TestSubscriber<Object> ts = new TestSubscriber<>(); + m.subscribe(ts); + ts.awaitDone(5, TimeUnit.SECONDS); + ts.assertNoErrors(); + System.out.println(ts.values()); + } + + /** + * Assert we get an IllegalStateException if trying to subscribe to an inner GroupedFlowable more than once. + */ + @Test + public void exceptionIfSubscribeToChildMoreThanOnce() { + Flowable<Integer> source = Flowable.just(0); + + final AtomicReference<GroupedFlowable<Integer, Integer>> inner = new AtomicReference<>(); + + Flowable<GroupedFlowable<Integer, Integer>> m = source.groupBy(identity, dbl); + + m.subscribe(new Consumer<GroupedFlowable<Integer, Integer>>() { + @Override + public void accept(GroupedFlowable<Integer, Integer> t1) { + inner.set(t1); + } + }); + + inner.get().subscribe(); + + Subscriber<Integer> subscriber2 = TestHelper.mockSubscriber(); + + inner.get().subscribe(subscriber2); + + verify(subscriber2, never()).onComplete(); + verify(subscriber2, never()).onNext(anyInt()); + verify(subscriber2).onError(any(IllegalStateException.class)); + } + + @Test + @SuppressUndeliverable + public void error2() { + Flowable<Integer> source = Flowable.concat(Flowable.just(0), + Flowable.<Integer> error(new TestException("Forced failure"))); + + Flowable<Integer> m = source.groupBy(identity, dbl).flatMap(FLATTEN_INTEGER); + + TestSubscriberEx<Object> ts = new TestSubscriberEx<>(); + m.subscribe(ts); + ts.awaitDone(5, TimeUnit.SECONDS); + assertEquals(1, ts.errors().size()); + ts.assertValueCount(1); + } + + @Test + public void groupByBackpressure3() throws InterruptedException { + TestSubscriber<String> ts = new TestSubscriber<>(); + + Flowable.range(1, 4000).groupBy(IS_EVEN2).flatMap(new Function<GroupedFlowable<Boolean, Integer>, Flowable<String>>() { + + @Override + public Flowable<String> apply(final GroupedFlowable<Boolean, Integer> g) { + return g.doOnComplete(new Action() { + + @Override + public void run() { + System.out.println("//////////////////// COMPLETED-A"); + } + + }).observeOn(Schedulers.computation()).map(new Function<Integer, String>() { + + int c; + + @Override + public String apply(Integer l) { + if (g.getKey()) { + if (c++ < 400) { + try { + Thread.sleep(1); + } catch (InterruptedException e) { + } + } + return l + " is even."; + } else { + return l + " is odd."; + } + } + + }).doOnComplete(new Action() { + + @Override + public void run() { + System.out.println("//////////////////// COMPLETED-B"); + } + + }); + } + + }).doOnEach(new Consumer<Notification<String>>() { + + @Override + public void accept(Notification<String> t1) { + System.out.println("NEXT: " + t1); + } + + }).subscribe(ts); + ts.awaitDone(5, TimeUnit.SECONDS); + ts.assertNoErrors(); + } + + @Test + public void groupByBackpressure2() throws InterruptedException { + + TestSubscriber<String> ts = new TestSubscriber<>(); + + Flowable.range(1, 4000) + .doOnNext(new Consumer<Integer>() { + @Override + public void accept(Integer v) { + System.out.println("testgroupByBackpressure2 >> " + v); + } + }) + .groupBy(IS_EVEN2) + .flatMap(new Function<GroupedFlowable<Boolean, Integer>, Flowable<String>>() { + @Override + public Flowable<String> apply(final GroupedFlowable<Boolean, Integer> g) { + return g.take(2) + .observeOn(Schedulers.computation()) + .map(new Function<Integer, String>() { + @Override + public String apply(Integer l) { + if (g.getKey()) { + try { + Thread.sleep(1); + } catch (InterruptedException e) { + } + return l + " is even."; + } else { + return l + " is odd."; + } + } + }); + } + }, 4000) // a lot of groups are created due to take(2) + .subscribe(ts); + + ts.awaitDone(5, TimeUnit.SECONDS); + ts.assertNoErrors(); + } + + @Test + public void groupByWithNullKey() { + final String[] key = new String[]{"uninitialized"}; + final List<String> values = new ArrayList<>(); + Flowable.just("a", "b", "c").groupBy(new Function<String, String>() { + + @Override + public String apply(String value) { + return null; + } + }).subscribe(new Consumer<GroupedFlowable<String, String>>() { + + @Override + public void accept(GroupedFlowable<String, String> groupedFlowable) { + key[0] = groupedFlowable.getKey(); + groupedFlowable.subscribe(new Consumer<String>() { + + @Override + public void accept(String s) { + values.add(s); + } + }); + } + }); + assertNull(key[0]); + assertEquals(Arrays.asList("a", "b", "c"), values); + } + + @Test + public void groupByUnsubscribe() { + final Subscription s = mock(Subscription.class); + Flowable<Integer> f = Flowable.unsafeCreate( + new Publisher<Integer>() { + @Override + public void subscribe(Subscriber<? super Integer> subscriber) { + subscriber.onSubscribe(s); + } + } + ); + TestSubscriber<Object> ts = new TestSubscriber<>(); + + f.groupBy(new Function<Integer, Integer>() { + + @Override + public Integer apply(Integer integer) { + return null; + } + }).subscribe(ts); + + ts.cancel(); + + verify(s).cancel(); + } + + @Test + public void groupByShouldPropagateError() { + final Throwable e = new RuntimeException("Oops"); + final TestSubscriberEx<Integer> inner1 = new TestSubscriberEx<>(); + final TestSubscriberEx<Integer> inner2 = new TestSubscriberEx<>(); + + final TestSubscriberEx<GroupedFlowable<Integer, Integer>> outer + = new TestSubscriberEx<>(new DefaultSubscriber<GroupedFlowable<Integer, Integer>>() { + + @Override + public void onComplete() { + } + + @Override + public void onError(Throwable e) { + } + + @Override + public void onNext(GroupedFlowable<Integer, Integer> f) { + if (f.getKey() == 0) { + f.subscribe(inner1); + } else { + f.subscribe(inner2); + } + } + }); + Flowable.unsafeCreate( + new Publisher<Integer>() { + @Override + public void subscribe(Subscriber<? super Integer> subscriber) { + subscriber.onSubscribe(new BooleanSubscription()); + subscriber.onNext(0); + subscriber.onNext(1); + subscriber.onError(e); + } + } + ).groupBy(new Function<Integer, Integer>() { + + @Override + public Integer apply(Integer i) { + return i % 2; + } + }).subscribe(outer); + assertEquals(Arrays.asList(e), outer.errors()); + assertEquals(Arrays.asList(e), inner1.errors()); + assertEquals(Arrays.asList(e), inner2.errors()); + } + + @Test + public void requestOverflow() { + final AtomicBoolean completed = new AtomicBoolean(false); + Flowable + .just(1, 2, 3) + // group into one group + .groupBy(new Function<Integer, Integer>() { + @Override + public Integer apply(Integer t) { + return 1; + } + }) + // flatten + .concatMap(new Function<GroupedFlowable<Integer, Integer>, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(GroupedFlowable<Integer, Integer> g) { + return g; + } + }) + .subscribe(new DefaultSubscriber<Integer>() { + + @Override + public void onStart() { + request(2); + } + + @Override + public void onComplete() { + completed.set(true); + + } + + @Override + public void onError(Throwable e) { + + } + + @Override + public void onNext(Integer t) { + System.out.println(t); + //provoke possible request overflow + request(Long.MAX_VALUE - 1); + }}); + assertTrue(completed.get()); + } + + /** + * Issue #3425. + * + * The problem is that a request of 1 may create a new group, emit to the desired group + * or emit to a completely different group. In this test, the merge requests N which + * must be produced by the range, however it will create a bunch of groups before the actual + * group receives a value. + * + * 12/03/2019: this test produces abandoned groups and as such keeps producing new groups + * that have to be ready to be received by observeOn and merge. + */ + @Test + public void backpressureObserveOnOuter() { + int n = 500; + for (int j = 0; j < 1000; j++) { + Flowable.merge( + Flowable.range(0, n) + .groupBy(new Function<Integer, Object>() { + @Override + public Object apply(Integer i) { + return i % (Flowable.bufferSize() + 2); + } + }) + .observeOn(Schedulers.computation(), false, n) + , n) + .blockingLast(); + } + } + + @Test(expected = MissingBackpressureException.class) + public void backpressureObserveOnOuterMissingBackpressure() { + for (int j = 0; j < 1000; j++) { + Flowable.merge( + Flowable.range(0, 500) + .groupBy(new Function<Integer, Object>() { + @Override + public Object apply(Integer i) { + return i % (Flowable.bufferSize() + 2); + } + }) + .observeOn(Schedulers.computation()) + ).blockingLast(); + } + } + + /** + * Synchronous verification of issue #3425. + */ + @Test + public void backpressureInnerDoesntOverflowOuter() { + TestSubscriber<GroupedFlowable<Integer, Integer>> ts = new TestSubscriber<>(0L); + + PublishProcessor<Integer> pp = PublishProcessor.create(); + + pp.groupBy(new Function<Integer, Integer>() { + @Override + public Integer apply(Integer v) { + return v; + } + }) + .doOnNext(new Consumer<GroupedFlowable<Integer, Integer>>() { + @Override + public void accept(GroupedFlowable<Integer, Integer> g) { + g.subscribe(); + } + }) // this will request Long.MAX_VALUE + .subscribe(ts) + ; + ts.request(1); + + pp.onNext(1); + + ts.assertNotComplete(); + ts.assertNoErrors(); + ts.assertValueCount(1); + } + + @Test + public void backpressureInnerDoesntOverflowOuterMissingBackpressure() { + TestSubscriber<GroupedFlowable<Integer, Integer>> ts = new TestSubscriber<>(1); + + Flowable.fromArray(1, 2) + .groupBy(new Function<Integer, Integer>() { + @Override + public Integer apply(Integer v) { + return v; + } + }) + .doOnNext(new Consumer<GroupedFlowable<Integer, Integer>>() { + @Override + public void accept(GroupedFlowable<Integer, Integer> g) { + g.subscribe(); + } + }) // this will request Long.MAX_VALUE + .subscribe(ts) + ; + ts.assertValueCount(1) + .assertError(MissingBackpressureException.class) + .assertNotComplete(); + } + + @Test + public void oneGroupInnerRequestsTwiceBuffer() { + // FIXME: delayed requesting in groupBy results in group abandonment + TestSubscriber<Object> ts1 = new TestSubscriber<>(1L); + + final TestSubscriber<Object> ts2 = new TestSubscriber<>(0L); + + Flowable.range(1, Flowable.bufferSize() * 2) + .groupBy(new Function<Integer, Object>() { + @Override + public Object apply(Integer v) { + return 1; + } + }) + .doOnNext(new Consumer<GroupedFlowable<Object, Integer>>() { + @Override + public void accept(GroupedFlowable<Object, Integer> g) { + g.subscribe(ts2); + } + }) + .subscribe(ts1); + + ts1.assertValueCount(1); + ts1.assertNoErrors(); + ts1.assertNotComplete(); + + ts2.assertNoValues(); + ts2.assertNoErrors(); + ts2.assertNotComplete(); + + ts2.request(Flowable.bufferSize() * 2); + + ts2.assertValueCount(Flowable.bufferSize() * 2); + ts2.assertNoErrors(); + ts2.assertComplete(); + } + + @Test + public void outerInnerFusion() { + final TestSubscriberEx<Integer> ts1 = new TestSubscriberEx<Integer>().setInitialFusionMode(QueueFuseable.ANY); + + final TestSubscriberEx<GroupedFlowable<Integer, Integer>> ts2 = new TestSubscriberEx<GroupedFlowable<Integer, Integer>>().setInitialFusionMode(QueueFuseable.ANY); + + Flowable.range(1, 10).groupBy(new Function<Integer, Integer>() { + @Override + public Integer apply(Integer v) { + return 1; + } + }, new Function<Integer, Integer>() { + @Override + public Integer apply(Integer v) { + return v + 1; + } + }) + .doOnNext(new Consumer<GroupedFlowable<Integer, Integer>>() { + @Override + public void accept(GroupedFlowable<Integer, Integer> g) { + g.subscribe(ts1); + } + }) + .subscribe(ts2); + + ts1 + // FIXME fusion mode causes hangs + //.assertFusionMode(QueueFuseable.ASYNC) + .assertFusionMode(QueueFuseable.NONE) + .assertValues(2, 3, 4, 5, 6, 7, 8, 9, 10, 11) + .assertNoErrors() + .assertComplete(); + + ts2 + .assertValueCount(1) + .assertNoErrors() + .assertComplete(); + } + + @Test + @SuppressUndeliverable + public void keySelectorAndDelayError() { + Flowable.just(1).concatWith(Flowable.<Integer>error(new TestException())) + .groupBy(Functions.<Integer>identity(), true) + .flatMap(new Function<GroupedFlowable<Integer, Integer>, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(GroupedFlowable<Integer, Integer> g) throws Exception { + return g; + } + }) + .test() + .assertFailure(TestException.class, 1); + } + + @Test + @SuppressUndeliverable + public void keyAndValueSelectorAndDelayError() { + Flowable.just(1).concatWith(Flowable.<Integer>error(new TestException())) + .groupBy(Functions.<Integer>identity(), Functions.<Integer>identity(), true) + .flatMap(new Function<GroupedFlowable<Integer, Integer>, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(GroupedFlowable<Integer, Integer> g) throws Exception { + return g; + } + }) + .test() + .assertFailure(TestException.class, 1); + } + + @Test + public void dispose() { + TestHelper.checkDisposed(Flowable.just(1).groupBy(Functions.justFunction(1))); + + Flowable.just(1) + .groupBy(Functions.justFunction(1)) + .doOnNext(new Consumer<GroupedFlowable<Integer, Integer>>() { + @Override + public void accept(GroupedFlowable<Integer, Integer> g) throws Exception { + TestHelper.checkDisposed(g); + } + }) + .test(); + } + + @Test + public void reentrantComplete() { + final PublishProcessor<Integer> pp = PublishProcessor.create(); + + TestSubscriber<Integer> ts = new TestSubscriber<Integer>() { + @Override + public void onNext(Integer t) { + super.onNext(t); + if (t == 1) { + pp.onComplete(); + } + } + }; + + Flowable.merge(pp.groupBy(Functions.justFunction(1))) + .subscribe(ts); + + pp.onNext(1); + + ts.assertResult(1); + } + + @Test + public void reentrantCompleteCancel() { + final PublishProcessor<Integer> pp = PublishProcessor.create(); + + TestSubscriberEx<Integer> ts = new TestSubscriberEx<Integer>() { + @Override + public void onNext(Integer t) { + super.onNext(t); + if (t == 1) { + pp.onComplete(); + dispose(); + } + } + }; + + Flowable.merge(pp.groupBy(Functions.justFunction(1))) + .subscribe(ts); + + pp.onNext(1); + + ts.assertSubscribed().assertValue(1).assertNoErrors().assertNotComplete(); + } + + @Test + public void delayErrorSimpleComplete() { + Flowable.just(1) + .groupBy(Functions.justFunction(1), true) + .flatMap(Functions.<Flowable<Integer>>identity()) + .test() + .assertResult(1); + } + + @Test + public void mainFusionRejected() { + TestSubscriberEx<Flowable<Integer>> ts = new TestSubscriberEx<Flowable<Integer>>().setInitialFusionMode(QueueFuseable.SYNC); + + Flowable.just(1) + .groupBy(Functions.justFunction(1)) + .subscribe(ts); + + ts.assertFusionMode(QueueFuseable.NONE) + .assertValueCount(1) + .assertComplete() + .assertNoErrors(); + } + + @Test + public void badSource() { + TestHelper.checkBadSourceFlowable(new Function<Flowable<Object>, Object>() { + @Override + public Object apply(Flowable<Object> f) throws Exception { + return f.groupBy(Functions.justFunction(1)); + } + }, false, 1, 1, (Object[])null); + } + + @Test + public void badRequest() { + TestHelper.assertBadRequestReported(Flowable.just(1).hide() + .groupBy(Functions.justFunction(1))); + } + + @Test + public void badRequestInner() { + Flowable.just(1).hide() + .groupBy(Functions.justFunction(1)) + .doOnNext(g -> { + TestHelper.assertBadRequestReported(g); + }) + .test() + .assertNoErrors(); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeFlowable(new Function<Flowable<Object>, Publisher<GroupedFlowable<Integer, Object>>>() { + @Override + public Publisher<GroupedFlowable<Integer, Object>> apply(Flowable<Object> f) throws Exception { + return f.groupBy(Functions.justFunction(1)); + } + }); + } + + @Test + public void nullKeyTakeInner() { + Flowable.just(1) + .groupBy(new Function<Integer, Object>() { + @Override + public Object apply(Integer v) throws Exception { + return null; + } + }) + .flatMap(new Function<GroupedFlowable<Object, Integer>, Publisher<Integer>>() { + @Override + public Publisher<Integer> apply(GroupedFlowable<Object, Integer> g) throws Exception { + return g.take(1); + } + }) + .test() + .assertResult(1); + } + + @Test + @SuppressUndeliverable + public void groupError() { + Flowable.just(1).concatWith(Flowable.<Integer>error(new TestException())) + .groupBy(Functions.justFunction(1), true) + .flatMap(new Function<GroupedFlowable<Integer, Integer>, Publisher<Integer>>() { + @Override + public Publisher<Integer> apply(GroupedFlowable<Integer, Integer> g) throws Exception { + return g.hide(); + } + }) + .test() + .assertFailure(TestException.class, 1); + } + + @Test + public void groupComplete() { + Flowable.just(1) + .groupBy(Functions.justFunction(1), true) + .flatMap(new Function<GroupedFlowable<Integer, Integer>, Publisher<Integer>>() { + @Override + public Publisher<Integer> apply(GroupedFlowable<Integer, Integer> g) throws Exception { + return g.hide(); + } + }) + .test() + .assertResult(1); + } + + @Test + public void mapFactoryThrows() { + final IOException ex = new IOException("boo"); + Function<Consumer<Object>, Map<Integer, Object>> evictingMapFactory = // + new Function<Consumer<Object>, Map<Integer, Object>>() { + + @Override + public Map<Integer, Object> apply(final Consumer<Object> notify) throws Exception { + throw ex; + } + }; + Flowable.just(1) + .groupBy(Functions.<Integer>identity(), Functions.identity(), true, 16, evictingMapFactory) + .test() + .assertNoValues() + .assertError(ex); + } + // ----------------------------------------------------------------------------------------------------------------------- + + private static final Function<Integer, Integer> mod5 = new Function<Integer, Integer>() { + + @Override + public Integer apply(Integer n) throws Exception { + return n % 5; + } + }; + + private static Function<GroupedFlowable<Integer, Integer>, Publisher<? extends Integer>> addCompletedKey( + final List<Integer> completed) { + return new Function<GroupedFlowable<Integer, Integer>, Publisher<? extends Integer>>() { + @Override + public Publisher<? extends Integer> apply(final GroupedFlowable<Integer, Integer> g) throws Exception { + return g.doOnComplete(new Action() { + @Override + public void run() throws Exception { + completed.add(g.getKey()); + } + }); + } + }; + } + + private static final class TestTicker extends Ticker { + long tick; + + @Override + public long read() { + return tick; + } + } + + @Test + public void mapFactoryExpiryCompletesGroupedFlowable() { + final List<Integer> completed = new CopyOnWriteArrayList<>(); + Function<Consumer<Object>, Map<Integer, Object>> evictingMapFactory = createEvictingMapFactorySynchronousOnly(1); + PublishSubject<Integer> subject = PublishSubject.create(); + TestSubscriberEx<Integer> ts = subject.toFlowable(BackpressureStrategy.BUFFER) + .groupBy(Functions.<Integer>identity(), Functions.<Integer>identity(), true, 16, evictingMapFactory) + .flatMap(addCompletedKey(completed)) + .to(TestHelper.<Integer>testConsumer()); + subject.onNext(1); + subject.onNext(2); + subject.onNext(3); + ts.assertValues(1, 2, 3) + .assertNotTerminated(); + assertEquals(Arrays.asList(1, 2), completed); + //ensure coverage of the code that clears the evicted queue + subject.onComplete(); + ts.assertComplete(); + ts.assertValueCount(3); + } + + @Test + public void mapFactoryEvictionQueueClearedOnErrorCoverageOnly() { + Function<Consumer<Object>, Map<Integer, Object>> evictingMapFactory = createEvictingMapFactorySynchronousOnly(1); + PublishSubject<Integer> subject = PublishSubject.create(); + TestSubscriber<Integer> ts = subject + .toFlowable(BackpressureStrategy.BUFFER) + .groupBy(Functions.<Integer>identity(), Functions.<Integer>identity(), true, 16, evictingMapFactory) + .flatMap(new Function<GroupedFlowable<Integer, Integer>, Publisher<Integer>>() { + @Override + public Publisher<Integer> apply(GroupedFlowable<Integer, Integer> g) throws Exception { + return g; + } + }) + .test(); + RuntimeException ex = new RuntimeException(); + //ensure coverage of the code that clears the evicted queue + subject.onError(ex); + ts.assertNoValues() + .assertError(ex); + } + + @Test + public void mapFactoryWithExpiringGuavaCacheDemonstrationCodeForUseInJavadoc() { + //javadoc will be a version of this using lambdas and without assertions + final List<Integer> completed = new CopyOnWriteArrayList<>(); + + AtomicReference<Cache<Integer, Object>> cacheOut = new AtomicReference<>(); + + //size should be less than 5 to notice the effect + Function<Consumer<Object>, Map<Integer, Object>> evictingMapFactory = createEvictingMapFactoryGuava(3, cacheOut); + int numValues = 1000; + TestSubscriber<Integer> ts = + Flowable.range(1, numValues) + .groupBy(mod5, Functions.<Integer>identity(), true, 16, evictingMapFactory) + .flatMap(addCompletedKey(completed)) + .test() + .assertComplete() + ; + ts.assertValueCount(numValues); + //the exact eviction behaviour of the guava cache is not specified so we make some approximate tests + assertTrue(completed.size() > numValues * 0.9); + + cacheOut.get().invalidateAll(); + } + + @Test + public void groupByEvictionCancellationOfSource5933() { + PublishProcessor<Integer> source = PublishProcessor.create(); + final TestTicker testTicker = new TestTicker(); + + Function<Consumer<Object>, Map<Integer, Object>> mapFactory = new Function<Consumer<Object>, Map<Integer, Object>>() { + @Override + public Map<Integer, Object> apply(final Consumer<Object> action) throws Exception { + return CacheBuilder.newBuilder() // + .expireAfterAccess(5, TimeUnit.SECONDS).removalListener(new RemovalListener<Object, Object>() { + @Override + public void onRemoval(RemovalNotification<Object, Object> notification) { + try { + action.accept(notification.getValue()); + } catch (Throwable ex) { + throw new RuntimeException(ex); + } + } + }).ticker(testTicker) // + .<Integer, Object>build().asMap(); + } + }; + + final List<String> list = new CopyOnWriteArrayList<>(); + Flowable<Integer> stream = source // + .doOnCancel(new Action() { + @Override + public void run() throws Exception { + list.add("Source canceled"); + } + }) + .<Integer, Integer>groupBy(Functions.<Integer>identity(), Functions.<Integer>identity(), false, + Flowable.bufferSize(), mapFactory) // + .flatMap(new Function<GroupedFlowable<Integer, Integer>, Publisher<? extends Integer>>() { + @Override + public Publisher<? extends Integer> apply(GroupedFlowable<Integer, Integer> group) + throws Exception { + return group // + .doOnComplete(new Action() { + @Override + public void run() throws Exception { + list.add("Group completed"); + } + }).doOnCancel(new Action() { + @Override + public void run() throws Exception { + list.add("Group canceled"); + } + }); + } + }); + TestSubscriber<Integer> ts = stream // + .doOnCancel(new Action() { + @Override + public void run() throws Exception { + list.add("Outer group by canceled"); + } + }).test(); + + // Send 3 in the same group and wait for them to be seen + source.onNext(1); + source.onNext(1); + source.onNext(1); + ts.awaitCount(3); + + // Advance time far enough to evict the group. + // NOTE -- Comment this line out to make the test "pass". + testTicker.tick = TimeUnit.SECONDS.toNanos(6); + + // Send more data in the group (triggering eviction and recreation) + source.onNext(1); + + // Wait for the last 2 and then cancel the subscription + ts.awaitCount(4); + ts.cancel(); + + // Observe the result. Note that right now the result differs depending on whether eviction occurred or + // not. The observed sequence in that case is: Group completed, Outer group by canceled., Group canceled. + // The addition of the "Group completed" is actually fine, but the fact that the cancel doesn't reach the + // source seems like a bug. Commenting out the setting of "tick" above will produce the "expected" sequence. + System.out.println(list); + assertTrue(list.contains("Source canceled")); + assertEquals(Arrays.asList( + "Group completed", // this is here when eviction occurs + "Outer group by canceled", + "Group canceled", + "Source canceled" // This is *not* here when eviction occurs + ), list); + } + + //not thread safe + private static final class SingleThreadEvictingHashMap<K, V> implements Map<K, V> { + + private final List<K> list = new ArrayList<>(); + private final Map<K, V> map = new HashMap<>(); + private final int maxSize; + private final Consumer<V> evictedListener; + + SingleThreadEvictingHashMap(int maxSize, Consumer<V> evictedListener) { + this.maxSize = maxSize; + this.evictedListener = evictedListener; + } + + @Override + public int size() { + return map.size(); + } + + @Override + public boolean isEmpty() { + return map.isEmpty(); + } + + @Override + public boolean containsKey(Object key) { + return map.containsKey(key); + } + + @Override + public boolean containsValue(Object value) { + return map.containsValue(value); + } + + @Override + public V get(Object key) { + return map.get(key); + } + + @Override + public V put(K key, V value) { + list.remove(key); + V v; + if (maxSize > 0 && list.size() == maxSize) { + //remove first + K k = list.get(0); + list.remove(0); + v = map.remove(k); + } else { + v = null; + } + list.add(key); + V result = map.put(key, value); + if (v != null) { + try { + evictedListener.accept(v); + } catch (Throwable e) { + throw new RuntimeException(e); + } + } + return result; + } + + @Override + public V remove(Object key) { + list.remove(key); + return map.remove(key); + } + + @Override + public void putAll(Map<? extends K, ? extends V> m) { + for (Entry<? extends K, ? extends V> entry: m.entrySet()) { + put(entry.getKey(), entry.getValue()); + } + } + + @Override + public void clear() { + list.clear(); + map.clear(); + } + + @Override + public Set<K> keySet() { + return map.keySet(); + } + + @Override + public Collection<V> values() { + return map.values(); + } + + @Override + public Set<Entry<K, V>> entrySet() { + return map.entrySet(); + } + } + + private static Function<Consumer<Object>, Map<Integer, Object>> createEvictingMapFactoryGuava(final int maxSize, + final AtomicReference<Cache<Integer, Object>> cacheOut) { + Function<Consumer<Object>, Map<Integer, Object>> evictingMapFactory = // + new Function<Consumer<Object>, Map<Integer, Object>>() { + + @Override + public Map<Integer, Object> apply(final Consumer<Object> notify) throws Exception { + Cache<Integer, Object> cache = CacheBuilder.newBuilder() // + .maximumSize(maxSize) // + .removalListener(new RemovalListener<Integer, Object>() { + @Override + public void onRemoval(RemovalNotification<Integer, Object> notification) { + try { + notify.accept(notification.getValue()); + } catch (Throwable e) { + throw new RuntimeException(e); + } + }}) + .<Integer, Object> build(); + cacheOut.set(cache); + return cache.asMap(); + }}; + return evictingMapFactory; + } + + private static Function<Consumer<Object>, Map<Integer, Object>> createEvictingMapFactorySynchronousOnly(final int maxSize) { + Function<Consumer<Object>, Map<Integer, Object>> evictingMapFactory = // + new Function<Consumer<Object>, Map<Integer, Object>>() { + + @Override + public Map<Integer, Object> apply(final Consumer<Object> notify) throws Exception { + return new SingleThreadEvictingHashMap<>(maxSize, new Consumer<Object>() { + @Override + public void accept(Object object) { + try { + notify.accept(object); + } catch (Throwable e) { + throw new RuntimeException(e); + } + } + }); + }}; + return evictingMapFactory; + } + + // ----------------------------------------------------------------------------------------------------------------------- + + @Test + public void cancellationOfUpstreamWhenGroupedFlowableCompletes() { + final AtomicBoolean cancelled = new AtomicBoolean(); + Flowable.just(1).repeat().doOnCancel(new Action() { + @Override + public void run() throws Exception { + cancelled.set(true); + } + }) + .groupBy(Functions.<Integer>identity(), Functions.<Integer>identity()) // + .flatMap(new Function<GroupedFlowable<Integer, Integer>, Publisher<? extends Object>>() { + @Override + public Publisher<? extends Object> apply(GroupedFlowable<Integer, Integer> g) throws Exception { + return g.first(0).toFlowable(); + } + }) + .take(4) // + .test() // + .assertComplete(); + assertTrue(cancelled.get()); + } + + @Test + public void cancelOverFlatmapRace() { + for (int i = 0; i < TestHelper.RACE_LONG_LOOPS; i++) { + + final TestSubscriber<Integer> ts = new TestSubscriber<>(); + + final PublishProcessor<Integer> pp = PublishProcessor.create(); + + pp.groupBy(new Function<Integer, Integer>() { + @Override + public Integer apply(Integer v) throws Throwable { + return v % 10; + } + }, Functions.<Integer>identity(), false, 2048) + .flatMap(new Function<GroupedFlowable<Integer, Integer>, GroupedFlowable<Integer, Integer>>() { + @Override + public GroupedFlowable<Integer, Integer> apply(GroupedFlowable<Integer, Integer> v) + throws Throwable { + return v; + } + }) + .subscribe(ts); + + Runnable r1 = new Runnable() { + @Override + public void run() { + for (int j = 0; j < 1000; j++) { + pp.onNext(j); + } + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + ts.cancel(); + } + }; + + TestHelper.race(r1, r2); + + assertFalse("Round " + i, pp.hasSubscribers()); + } + } + + @Test + public void abandonedGroupsNoDataloss() { + final List<GroupedFlowable<Integer, Integer>> groups = new ArrayList<>(); + + Flowable.range(1, 1000) + .groupBy(new Function<Integer, Integer>() { + @Override + public Integer apply(Integer v) throws Throwable { + return v % 10; + } + }) + .doOnNext(new Consumer<GroupedFlowable<Integer, Integer>>() { + @Override + public void accept(GroupedFlowable<Integer, Integer> v) throws Throwable { + groups.add(v); + } + }) + .test() + .assertValueCount(1000) + .assertComplete() + .assertNoErrors(); + + Flowable.concat(groups) + .test() + .assertValueCount(1000) + .assertNoErrors() + .assertComplete(); + } + + @Test + public void newGroupValueSelectorFails() { + TestSubscriber<Object> ts1 = new TestSubscriber<>(); + final TestSubscriber<Object> ts2 = new TestSubscriber<>(); + + Flowable.just(1) + .groupBy(Functions.<Integer>identity(), new Function<Integer, Object>() { + @Override + public Object apply(Integer v) throws Throwable { + throw new TestException(); + } + }) + .doOnNext(new Consumer<GroupedFlowable<Integer, Object>>() { + @Override + public void accept(GroupedFlowable<Integer, Object> g) throws Throwable { + g.subscribe(ts2); + } + }) + .subscribe(ts1); + + ts1.assertValueCount(1) + .assertError(TestException.class) + .assertNotComplete(); + + ts2.assertFailure(TestException.class); + } + + @Test + public void existingGroupValueSelectorFails() { + TestSubscriber<Object> ts1 = new TestSubscriber<>(); + final TestSubscriber<Object> ts2 = new TestSubscriber<>(); + + Flowable.just(1, 2) + .groupBy(Functions.justFunction(1), new Function<Integer, Object>() { + @Override + public Object apply(Integer v) throws Throwable { + if (v == 2) { + throw new TestException(); + } + return v; + } + }) + .doOnNext(new Consumer<GroupedFlowable<Integer, Object>>() { + @Override + public void accept(GroupedFlowable<Integer, Object> g) throws Throwable { + g.subscribe(ts2); + } + }) + .subscribe(ts1); + + ts1.assertValueCount(1) + .assertError(TestException.class) + .assertNotComplete(); + + ts2.assertFailure(TestException.class, 1); + } + + @Test + public void fusedParallelGroupProcessing() { + Flowable.range(0, 500000) + .subscribeOn(Schedulers.single()) + .groupBy(new Function<Integer, Integer>() { + @Override + public Integer apply(Integer i) throws Throwable { + return i % 2; + } + }) + .flatMap(new Function<GroupedFlowable<Integer, Integer>, Publisher<Integer>>() { + @Override + public Publisher<Integer> apply(GroupedFlowable<Integer, Integer> g) { + return g.getKey() == 0 + ? g + .parallel() + .runOn(Schedulers.computation()) + .map(Functions.<Integer>identity()) + .sequential() + : g.map(Functions.<Integer>identity()) // no need to use hide + ; + } + }) + .test() + .awaitDone(20, TimeUnit.SECONDS) + .assertValueCount(500000) + .assertComplete() + .assertNoErrors(); + } + + @Test + public void valueSelectorCrashAndMissingBackpressure() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + + TestSubscriberEx<GroupedFlowable<Integer, Integer>> ts = pp.groupBy(Functions.justFunction(1), new Function<Integer, Integer>() { + @Override + public Integer apply(Integer t) throws Throwable { + throw new TestException(); + } + }) + .subscribeWith(new TestSubscriberEx<>(0L)); + + assertTrue(pp.offer(1)); + + ts.assertFailure(MissingBackpressureException.class); + + assertTrue("" + ts.errors().get(0).getCause(), ts.errors().get(0).getCause() instanceof TestException); + } + + @Test + public void fusedGroupClearedOnCancel() { + Flowable.just(1) + .groupBy(Functions.<Integer>identity()) + .flatMap(new Function<GroupedFlowable<Integer, Integer>, Publisher<Integer>>() { + @Override + public Publisher<Integer> apply(GroupedFlowable<Integer, Integer> g) throws Throwable { + return g.observeOn(ImmediateThinScheduler.INSTANCE).take(1); + } + }) + .test() + .assertResult(1); + } + + @Test + public void fusedGroupClearedOnCancelDelayed() { + Flowable.range(1, 100) + .groupBy(Functions.<Integer, Integer>justFunction(1)) + .flatMap(new Function<GroupedFlowable<Integer, Integer>, Publisher<Integer>>() { + @Override + public Publisher<Integer> apply(GroupedFlowable<Integer, Integer> g) throws Throwable { + return g.observeOn(Schedulers.io()) + .doOnNext(new Consumer<Integer>() { + @Override + public void accept(Integer v) throws Throwable { + Thread.sleep(100); + } + }) + .take(1); + } + }) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertNoErrors() + .assertComplete(); + } + + @Test + public void cancelledGroupResumesRequesting() { + final List<TestSubscriber<Integer>> tss = new ArrayList<>(); + final AtomicInteger counter = new AtomicInteger(); + final AtomicBoolean done = new AtomicBoolean(); + Flowable.range(1, 1000) + .doOnNext(new Consumer<Integer>() { + @Override + public void accept(Integer v) throws Exception { + counter.getAndIncrement(); + } + }) + .groupBy(Functions.justFunction(1)) + .subscribe(new Consumer<GroupedFlowable<Integer, Integer>>() { + @Override + public void accept(GroupedFlowable<Integer, Integer> v) throws Exception { + TestSubscriber<Integer> ts = TestSubscriber.create(0L); + tss.add(ts); + v.subscribe(ts); + } + }, Functions.emptyConsumer(), new Action() { + @Override + public void run() throws Exception { + done.set(true); + } + }); + + while (!done.get()) { + tss.remove(0).cancel(); + } + + assertEquals(1000, counter.get()); + } + + @Test + public void delayErrorCompleteMoreWorkInGroup() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + + TestSubscriber<Integer> ts = pp.groupBy(v -> 1, true) + .flatMap(g -> g.doOnNext(v -> { + if (v == 1) { + pp.onNext(2); + pp.onComplete(); + } + }) + ) + .test() + ; + + pp.onNext(1); + + ts.assertResult(1, 2); + } + + @Test + public void groupSyncFusionRejected() { + Flowable.just(1) + .groupBy(v -> 1) + .doOnNext(g -> { + g.subscribeWith(new TestSubscriberEx<Integer>().setInitialFusionMode(QueueFuseable.SYNC)) + .assertFuseable() + .assertFusionMode(QueueFuseable.NONE); + }) + .test() + .assertComplete(); + } + + @Test + public void subscribeAbandonRace() throws Throwable { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + PublishProcessor<Integer> pp = PublishProcessor.create(); + + TestSubscriber<Integer> ts = TestSubscriber.create(); + + CountDownLatch cdl = new CountDownLatch(1); + + pp.groupBy(v -> 1) + .doOnNext(g -> { + TestHelper.raceOther(() -> { + g.subscribe(ts); + }, cdl); + }) + .test(); + + pp.onNext(1); + + cdl.await(); + + ts.assertValueCount(1); + } + } + + @Test + public void issue6974() { + + FlowableTransformer<Integer, Integer> operation = + source -> source.publish(shared -> + shared + .firstElement() + .flatMapPublisher(firstElement -> + Flowable.just(firstElement).concatWith(shared) + ) + ); + + issue6974Run(20, 500_000, 20 - 1, 20 * 2, operation, false); + + issue6974Run(20, 500_000, 20, 20 * 2, operation, false); + } + + static void issue6974Run(int groups, int iterations, int sizeCap, int flatMapConcurrency, + FlowableTransformer<Integer, Integer> operation, boolean notifyOnExplicitRevoke) { + TestSubscriber<Integer> test = Flowable + .range(1, groups) + .repeat(iterations / groups) + .groupBy(i -> i, i -> i, false, 128, sizeCap(sizeCap, notifyOnExplicitRevoke)) + .flatMap(gf -> gf.compose(operation), flatMapConcurrency) + .test(); + test.awaitDone(5, TimeUnit.SECONDS); + test.assertValueCount(iterations); + } + + static <T> Function<Consumer<Object>, Map<T, Object>> sizeCap(int maxCapacity, boolean notifyOnExplicit) { + return itemEvictConsumer -> + CacheBuilder + .newBuilder() + .maximumSize(maxCapacity) + .removalListener(notification -> { + if (notification.getCause() != RemovalCause.EXPLICIT || notifyOnExplicit) { + try { + itemEvictConsumer.accept(notification.getValue()); + } catch (Throwable throwable) { + throw new RuntimeException(throwable); + } + } + }) + .<T, Object>build().asMap(); + } + + static void issue6974RunPart2(int groupByBufferSize, int flatMapMaxConcurrency, int groups, + boolean notifyOnExplicitEviction) { + TestSubscriber<Integer> ts = Flowable + .range(1, 500_000) + .map(i -> i % groups) + .groupBy(i -> i, i -> i, false, groupByBufferSize, + // set cap too high + sizeCap(groups * 100, notifyOnExplicitEviction)) + .flatMap(gf -> gf + .take(10, TimeUnit.MILLISECONDS) + , flatMapMaxConcurrency) + .test(); + + ts + .awaitDone(5, TimeUnit.SECONDS) + .assertNoErrors() + .assertComplete(); + } + + @Test + public void issue6974Part2Case1() { + final int groups = 20; + + // Not completed (Timed out), buffer is too small + int groupByBufferSize = groups * 2; + int flatMapMaxConcurrency = 2 * groups; + boolean notifyOnExplicitEviction = false; + issue6974RunPart2(groupByBufferSize, flatMapMaxConcurrency, groups, notifyOnExplicitEviction); + } + + @Test + public void issue6974Part2Case2() { + final int groups = 20; + + // Timeout... explicit eviction notification makes difference + int groupByBufferSize = groups * 30; + int flatMapMaxConcurrency = 2 * groups; + boolean notifyOnExplicitEviction = true; + issue6974RunPart2(groupByBufferSize, flatMapMaxConcurrency, groups, notifyOnExplicitEviction); + } + + /* + * Disabled: Takes very long. Run it locally only. + @Test + public void issue6974Part2Case2Loop() { + for (int i = 0; i < 1000; i++) { + issue6974Part2Case2(); + } + } + */ + + static void issue6974RunPart2NoEvict(int groupByBufferSize, int flatMapMaxConcurrency, int groups, + boolean notifyOnExplicitEviction) { + + Flowable + .range(1, 500_000) + .map(i -> i % groups) + .groupBy(i -> i) + .flatMap(gf -> gf + .take(10, TimeUnit.MILLISECONDS) + , flatMapMaxConcurrency) + .subscribeWith(new TestSubscriberEx<>()) + .awaitDone(5, TimeUnit.SECONDS) + .assertTerminated(); // MBE is possible if the async group closing is slow + } + + @Test + public void issue6974Part2Case1NoEvict() { + final int groups = 20; + + // Not completed (Timed out), buffer is too small + int groupByBufferSize = groups * 2; + int flatMapMaxConcurrency = 2 * groups; + boolean notifyOnExplicitEviction = false; + issue6974RunPart2NoEvict(groupByBufferSize, flatMapMaxConcurrency, groups, notifyOnExplicitEviction); + } + + /* + * Disabled: Takes very long. Run it locally only. + @Test + public void issue6974Part2Case1NoEvictLoop() { + for (int i = 0; i < 1000; i++) { + issue6974Part2Case1NoEvict(); + } + } + */ + + @Test + public void issue6974Part2Case1ObserveOn() { + final int groups = 20; + + // Not completed (Timed out), buffer is too small + int groupByBufferSize = groups * 2; + int flatMapMaxConcurrency = 2 * groups; + boolean notifyOnExplicitEviction = false; + + Flowable + .range(1, 500_000) + .map(i -> i % groups) + .doOnCancel(() -> { + System.out.println("Cancelling upstream"); + }) + .groupBy(i -> i, i -> i, false, groupByBufferSize, + sizeCap(groups * 2, notifyOnExplicitEviction)) + .flatMap(gf -> gf + .observeOn(Schedulers.computation()) + // .take(10) + .take(10, TimeUnit.MILLISECONDS) + , flatMapMaxConcurrency) + .subscribeWith(new TestSubscriberEx<>()) + .awaitDone(5, TimeUnit.SECONDS) + .assertTerminated(); // MBE is possible if the async group closing is slow + } + + @Test + public void issue6974Part2Case1ObserveOnHide() { + final int groups = 20; + + // Not completed (Timed out), buffer is too small + int groupByBufferSize = groups * 2; + int flatMapMaxConcurrency = 2 * groups; + boolean notifyOnExplicitEviction = false; + + Flowable + .range(1, 500_000) + .map(i -> i % groups) + .doOnCancel(() -> System.out.println("Cancelling upstream")) + .groupBy(i -> i, i -> i, false, groupByBufferSize, + sizeCap(groups * 2, notifyOnExplicitEviction)) + .flatMap(gf -> gf + .hide() + .observeOn(Schedulers.computation()) + // .take(10) + .take(10, TimeUnit.MILLISECONDS) + , flatMapMaxConcurrency) + .subscribeWith(new TestSubscriberEx<>()) + .awaitDone(5, TimeUnit.SECONDS) + .assertTerminated(); // MBE is possible if the async group closing is slow + } + + @Test + public void issue6974Part2Case1ObserveOnNoCap() { + final int groups = 20; + + // Not completed (Timed out), buffer is too small + int flatMapMaxConcurrency = 1_000_000; + + Flowable + .range(1, 500_000) + .map(i -> i % groups) + .doOnRequest(v -> { + System.out.println("Source: " + v); + }) + .groupBy(i -> i) + .flatMap(gf -> gf + .observeOn(Schedulers.computation()) + // .take(10) + .take(10, TimeUnit.MILLISECONDS) + , flatMapMaxConcurrency) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertNoErrors() + .assertComplete(); + } + + @Test + public void issue6974Part2Case1ObserveOnNoCapHide() { + final int groups = 20; + + // Not completed (Timed out), buffer is too small + int flatMapMaxConcurrency = 1_000_000; + + Flowable + .range(1, 500_000) + .map(i -> i % groups) + .doOnRequest(v -> { + System.out.println("Source: " + v); + }) + .groupBy(i -> i) + .flatMap(gf -> gf + .hide() + .observeOn(Schedulers.computation()) + // .take(10) + .take(10, TimeUnit.MILLISECONDS) + , flatMapMaxConcurrency) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertNoErrors() + .assertComplete(); + } + + /* + * Disabled: Takes very long. Run it locally only. + @Test + public void issue6974Part2Case1ObserveOnNoCapHideLoop() { + for (int i = 0; i < 100; i++) { + issue6974Part2Case1ObserveOnNoCapHide(); + } + } + */ + + @Test + public void issue6974Part2Case1ObserveOnConditional() { + final int groups = 20; + + // Not completed (Timed out), buffer is too small + int groupByBufferSize = groups * 2; + int flatMapMaxConcurrency = 2 * groups; + boolean notifyOnExplicitEviction = false; + + Flowable + .range(1, 500_000) + .map(i -> i % groups) + .doOnCancel(() -> System.out.println("Cancelling upstream")) + .groupBy(i -> i, i -> i, false, groupByBufferSize, + sizeCap(groups * 2, notifyOnExplicitEviction)) + .flatMap(gf -> gf + .observeOn(Schedulers.computation()) + .filter(v -> true) + // .take(10) + .take(10, TimeUnit.MILLISECONDS) + , flatMapMaxConcurrency) + .subscribeWith(new TestSubscriberEx<>()) + .awaitDone(5, TimeUnit.SECONDS) + .assertTerminated(); // MBE is possible if the async group closing is slow + } + + @Test + public void issue6974Part2Case1ObserveOnConditionalHide() { + final int groups = 20; + + // Not completed (Timed out), buffer is too small + int groupByBufferSize = groups * 2; + int flatMapMaxConcurrency = 2 * groups; + boolean notifyOnExplicitEviction = false; + + Flowable + .range(1, 500_000) + .map(i -> i % groups) + .doOnCancel(() -> System.out.println("Cancelling upstream")) + .groupBy(i -> i, i -> i, false, groupByBufferSize, + sizeCap(groups * 2, notifyOnExplicitEviction)) + .flatMap(gf -> gf + .hide() + .observeOn(Schedulers.computation()) + .filter(v -> true) + // .take(10) + .take(10, TimeUnit.MILLISECONDS) + , flatMapMaxConcurrency) + .subscribeWith(new TestSubscriberEx<>()) + .awaitDone(5, TimeUnit.SECONDS) + .assertTerminated(); // MBE is possible if the async group closing is slow + } + + /* + * Disabled: Takes very long. Run it locally only. + @Test + public void issue6974Part2Case1ObserveOnHideLoop() { + for (int i = 0; i < 100; i++) { + issue6974Part2Case1ObserveOnHide(); + } + } + */ + + static <T> Function<Consumer<Object>, ConcurrentMap<T, Object>> ttlCapGuava(Duration ttl) { + return itemEvictConsumer -> + CacheBuilder + .newBuilder() + .expireAfterWrite(ttl) + .removalListener(n -> { + if (n.getCause() != com.google.common.cache.RemovalCause.EXPLICIT) { + try { + itemEvictConsumer.accept(n.getValue()); + } catch (Throwable throwable) { + throw new RuntimeException(throwable); + } + } + }).<T, Object>build().asMap(); + } + + @Test + public void issue6982Case1() { + final int groups = 20; + + int groupByBufferSize = 2; + int flatMapMaxConcurrency = 200 * groups; + + // ~50% of executions - Not completed (latch = 1, values = 500000, errors = 0, completions = 0, timeout!, + // disposed!) + + Flowable + .range(1, 500_000) + .map(i -> i % groups) + .groupBy(i -> i, i -> i, false, groupByBufferSize, ttlCapGuava(Duration.ofMillis(10))) + .flatMap(gf -> gf.observeOn(Schedulers.computation()), flatMapMaxConcurrency) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertNoErrors() + .assertComplete(); + } + + /* + * Disabled: Takes very long. Run it locally only. + @Test + public void issue6982Case1Loop() { + for (int i = 0; i < 200; i++) { + System.out.println("issue6982Case1Loop " + i); + issue6982Case1(); + } + } + */ + + @Test + public void issue6982Case2() { + final int groups = 20; + + int groupByBufferSize = groups * 30; + int flatMapMaxConcurrency = groups * 500; + // Always : Not completed (latch = 1, values = 14100, errors = 0, completions = 0, timeout!, disposed!) + + Flowable + .range(1, 500_000) + .map(i -> i % groups) + .groupBy(i -> i, i -> i, false, groupByBufferSize, ttlCapGuava(Duration.ofMillis(10))) + .flatMap(gf -> gf.observeOn(Schedulers.computation()), flatMapMaxConcurrency) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertNoErrors() + .assertComplete(); + } + + /* + * Disabled: Takes very long. Run it locally only. + @Test + public void issue6982Case2Loop() { + for (int i = 0; i < 200; i++) { + System.out.println("issue6982Case2Loop " + i); + issue6982Case2(); + } + } + */ +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableGroupJoinTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableGroupJoinTest.java new file mode 100644 index 0000000000..fe83ad9fff --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableGroupJoinTest.java @@ -0,0 +1,784 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import java.util.*; + +import org.junit.*; +import org.mockito.MockitoAnnotations; +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.internal.operators.flowable.FlowableGroupJoin.*; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.processors.*; +import io.reactivex.rxjava3.subscribers.TestSubscriber; +import io.reactivex.rxjava3.testsupport.*; + +public class FlowableGroupJoinTest extends RxJavaTest { + + Subscriber<Object> subscriber = TestHelper.mockSubscriber(); + + BiFunction<Integer, Integer, Integer> add = new BiFunction<Integer, Integer, Integer>() { + @Override + public Integer apply(Integer t1, Integer t2) { + return t1 + t2; + } + }; + + <T> Function<Integer, Flowable<T>> just(final Flowable<T> flowable) { + return new Function<Integer, Flowable<T>>() { + @Override + public Flowable<T> apply(Integer t1) { + return flowable; + } + }; + } + + <T, R> Function<T, Flowable<R>> just2(final Flowable<R> flowable) { + return new Function<T, Flowable<R>>() { + @Override + public Flowable<R> apply(T t1) { + return flowable; + } + }; + } + + BiFunction<Integer, Flowable<Integer>, Flowable<Integer>> add2 = new BiFunction<Integer, Flowable<Integer>, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(final Integer leftValue, Flowable<Integer> rightValues) { + return rightValues.map(new Function<Integer, Integer>() { + @Override + public Integer apply(Integer rightValue) throws Throwable { + return add.apply(leftValue, rightValue); + } + }); + } + + }; + + @Before + public void before() { + MockitoAnnotations.openMocks(this); + } + + @Test + public void behaveAsJoin() { + PublishProcessor<Integer> source1 = PublishProcessor.create(); + PublishProcessor<Integer> source2 = PublishProcessor.create(); + + Flowable<Integer> m = Flowable.merge(source1.groupJoin(source2, + just(Flowable.never()), + just(Flowable.never()), add2)); + + m.subscribe(subscriber); + + source1.onNext(1); + source1.onNext(2); + source1.onNext(4); + + source2.onNext(16); + source2.onNext(32); + source2.onNext(64); + + source1.onComplete(); + source2.onComplete(); + + verify(subscriber, times(1)).onNext(17); + verify(subscriber, times(1)).onNext(18); + verify(subscriber, times(1)).onNext(20); + verify(subscriber, times(1)).onNext(33); + verify(subscriber, times(1)).onNext(34); + verify(subscriber, times(1)).onNext(36); + verify(subscriber, times(1)).onNext(65); + verify(subscriber, times(1)).onNext(66); + verify(subscriber, times(1)).onNext(68); + + verify(subscriber, times(1)).onComplete(); //Never emitted? + verify(subscriber, never()).onError(any(Throwable.class)); + } + + class Person { + final int id; + final String name; + + Person(int id, String name) { + this.id = id; + this.name = name; + } + } + + class PersonFruit { + final int personId; + final String fruit; + + PersonFruit(int personId, String fruit) { + this.personId = personId; + this.fruit = fruit; + } + } + + class PPF { + final Person person; + final Flowable<PersonFruit> fruits; + + PPF(Person person, Flowable<PersonFruit> fruits) { + this.person = person; + this.fruits = fruits; + } + } + + @Test + public void normal1() { + Flowable<Person> source1 = Flowable.fromIterable(Arrays.asList( + new Person(1, "Joe"), + new Person(2, "Mike"), + new Person(3, "Charlie") + )); + + Flowable<PersonFruit> source2 = Flowable.fromIterable(Arrays.asList( + new PersonFruit(1, "Strawberry"), + new PersonFruit(1, "Apple"), + new PersonFruit(3, "Peach") + )); + + Flowable<PPF> q = source1.groupJoin( + source2, + just2(Flowable.<Object> never()), + just2(Flowable.<Object> never()), + new BiFunction<Person, Flowable<PersonFruit>, PPF>() { + @Override + public PPF apply(Person t1, Flowable<PersonFruit> t2) { + return new PPF(t1, t2); + } + }); + + q.subscribe( + new FlowableSubscriber<PPF>() { + @Override + public void onNext(final PPF ppf) { + ppf.fruits.filter(new Predicate<PersonFruit>() { + @Override + public boolean test(PersonFruit t1) { + return ppf.person.id == t1.personId; + } + }).subscribe(new Consumer<PersonFruit>() { + @Override + public void accept(PersonFruit t1) { + subscriber.onNext(Arrays.asList(ppf.person.name, t1.fruit)); + } + }); + } + + @Override + public void onError(Throwable e) { + subscriber.onError(e); + } + + @Override + public void onComplete() { + subscriber.onComplete(); + } + + @Override + public void onSubscribe(Subscription s) { + s.request(Long.MAX_VALUE); + } + + } + ); + + verify(subscriber, times(1)).onNext(Arrays.asList("Joe", "Strawberry")); + verify(subscriber, times(1)).onNext(Arrays.asList("Joe", "Apple")); + verify(subscriber, times(1)).onNext(Arrays.asList("Charlie", "Peach")); + + verify(subscriber, times(1)).onComplete(); + verify(subscriber, never()).onError(any(Throwable.class)); + } + + @Test + public void leftThrows() { + PublishProcessor<Integer> source1 = PublishProcessor.create(); + PublishProcessor<Integer> source2 = PublishProcessor.create(); + + Flowable<Flowable<Integer>> m = source1.groupJoin(source2, + just(Flowable.never()), + just(Flowable.never()), add2); + + m.subscribe(subscriber); + + source2.onNext(1); + source1.onError(new RuntimeException("Forced failure")); + + verify(subscriber, times(1)).onError(any(Throwable.class)); + verify(subscriber, never()).onComplete(); + verify(subscriber, never()).onNext(any()); + } + + @Test + public void rightThrows() { + PublishProcessor<Integer> source1 = PublishProcessor.create(); + PublishProcessor<Integer> source2 = PublishProcessor.create(); + + Flowable<Flowable<Integer>> m = source1.groupJoin(source2, + just(Flowable.never()), + just(Flowable.never()), add2); + + m.subscribe(subscriber); + + source1.onNext(1); + source2.onError(new RuntimeException("Forced failure")); + + verify(subscriber, times(1)).onNext(any(Flowable.class)); + verify(subscriber, times(1)).onError(any(Throwable.class)); + verify(subscriber, never()).onComplete(); + } + + @Test + public void leftDurationThrows() { + PublishProcessor<Integer> source1 = PublishProcessor.create(); + PublishProcessor<Integer> source2 = PublishProcessor.create(); + + Flowable<Integer> duration1 = Flowable.<Integer> error(new RuntimeException("Forced failure")); + + Flowable<Flowable<Integer>> m = source1.groupJoin(source2, + just(duration1), + just(Flowable.never()), add2); + m.subscribe(subscriber); + + source1.onNext(1); + + verify(subscriber, times(1)).onError(any(Throwable.class)); + verify(subscriber, never()).onComplete(); + verify(subscriber, never()).onNext(any()); + } + + @Test + public void rightDurationThrows() { + PublishProcessor<Integer> source1 = PublishProcessor.create(); + PublishProcessor<Integer> source2 = PublishProcessor.create(); + + Flowable<Integer> duration1 = Flowable.<Integer> error(new RuntimeException("Forced failure")); + + Flowable<Flowable<Integer>> m = source1.groupJoin(source2, + just(Flowable.never()), + just(duration1), add2); + m.subscribe(subscriber); + + source2.onNext(1); + + verify(subscriber, times(1)).onError(any(Throwable.class)); + verify(subscriber, never()).onComplete(); + verify(subscriber, never()).onNext(any()); + } + + @Test + public void leftDurationSelectorThrows() { + PublishProcessor<Integer> source1 = PublishProcessor.create(); + PublishProcessor<Integer> source2 = PublishProcessor.create(); + + Function<Integer, Flowable<Integer>> fail = new Function<Integer, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Integer t1) { + throw new RuntimeException("Forced failure"); + } + }; + + Flowable<Flowable<Integer>> m = source1.groupJoin(source2, + fail, + just(Flowable.never()), add2); + m.subscribe(subscriber); + + source1.onNext(1); + + verify(subscriber, times(1)).onError(any(Throwable.class)); + verify(subscriber, never()).onComplete(); + verify(subscriber, never()).onNext(any()); + } + + @Test + public void rightDurationSelectorThrows() { + PublishProcessor<Integer> source1 = PublishProcessor.create(); + PublishProcessor<Integer> source2 = PublishProcessor.create(); + + Function<Integer, Flowable<Integer>> fail = new Function<Integer, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Integer t1) { + throw new RuntimeException("Forced failure"); + } + }; + + Flowable<Flowable<Integer>> m = source1.groupJoin(source2, + just(Flowable.never()), + fail, add2); + m.subscribe(subscriber); + + source2.onNext(1); + + verify(subscriber, times(1)).onError(any(Throwable.class)); + verify(subscriber, never()).onComplete(); + verify(subscriber, never()).onNext(any()); + } + + @Test + public void resultSelectorThrows() { + PublishProcessor<Integer> source1 = PublishProcessor.create(); + PublishProcessor<Integer> source2 = PublishProcessor.create(); + + BiFunction<Integer, Flowable<Integer>, Integer> fail = new BiFunction<Integer, Flowable<Integer>, Integer>() { + @Override + public Integer apply(Integer t1, Flowable<Integer> t2) { + throw new RuntimeException("Forced failure"); + } + }; + + Flowable<Integer> m = source1.groupJoin(source2, + just(Flowable.never()), + just(Flowable.never()), fail); + m.subscribe(subscriber); + + source1.onNext(1); + source2.onNext(2); + + verify(subscriber, times(1)).onError(any(Throwable.class)); + verify(subscriber, never()).onComplete(); + verify(subscriber, never()).onNext(any()); + } + + @Test + public void dispose() { + TestHelper.checkDisposed(Flowable.just(1).groupJoin( + Flowable.just(2), + new Function<Integer, Flowable<Object>>() { + @Override + public Flowable<Object> apply(Integer left) throws Exception { + return Flowable.never(); + } + }, + new Function<Integer, Flowable<Object>>() { + @Override + public Flowable<Object> apply(Integer right) throws Exception { + return Flowable.never(); + } + }, + new BiFunction<Integer, Flowable<Integer>, Object>() { + @Override + public Object apply(Integer r, Flowable<Integer> l) throws Exception { + return l; + } + } + )); + } + + @Test + public void innerCompleteLeft() { + Flowable.just(1) + .groupJoin( + Flowable.just(2), + new Function<Integer, Flowable<Object>>() { + @Override + public Flowable<Object> apply(Integer left) throws Exception { + return Flowable.empty(); + } + }, + new Function<Integer, Flowable<Object>>() { + @Override + public Flowable<Object> apply(Integer right) throws Exception { + return Flowable.never(); + } + }, + new BiFunction<Integer, Flowable<Integer>, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Integer r, Flowable<Integer> l) throws Exception { + return l; + } + } + ) + .flatMap(Functions.<Flowable<Integer>>identity()) + .test() + .assertResult(); + } + + @Test + public void innerErrorLeft() { + Flowable.just(1) + .groupJoin( + Flowable.just(2), + new Function<Integer, Flowable<Object>>() { + @Override + public Flowable<Object> apply(Integer left) throws Exception { + return Flowable.error(new TestException()); + } + }, + new Function<Integer, Flowable<Object>>() { + @Override + public Flowable<Object> apply(Integer right) throws Exception { + return Flowable.never(); + } + }, + new BiFunction<Integer, Flowable<Integer>, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Integer r, Flowable<Integer> l) throws Exception { + return l; + } + } + ) + .flatMap(Functions.<Flowable<Integer>>identity()) + .test() + .assertFailure(TestException.class); + } + + @Test + public void innerCompleteRight() { + Flowable.just(1) + .groupJoin( + Flowable.just(2), + new Function<Integer, Flowable<Object>>() { + @Override + public Flowable<Object> apply(Integer left) throws Exception { + return Flowable.never(); + } + }, + new Function<Integer, Flowable<Object>>() { + @Override + public Flowable<Object> apply(Integer right) throws Exception { + return Flowable.empty(); + } + }, + new BiFunction<Integer, Flowable<Integer>, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Integer r, Flowable<Integer> l) throws Exception { + return l; + } + } + ) + .flatMap(Functions.<Flowable<Integer>>identity()) + .test() + .assertResult(2); + } + + @Test + public void innerErrorRight() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + Flowable.just(1) + .groupJoin( + Flowable.just(2), + new Function<Integer, Flowable<Object>>() { + @Override + public Flowable<Object> apply(Integer left) throws Exception { + return Flowable.never(); + } + }, + new Function<Integer, Flowable<Object>>() { + @Override + public Flowable<Object> apply(Integer right) throws Exception { + return Flowable.error(new TestException()); + } + }, + new BiFunction<Integer, Flowable<Integer>, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Integer r, Flowable<Integer> l) throws Exception { + return l; + } + } + ) + .flatMap(Functions.<Flowable<Integer>>identity()) + .test() + .assertFailure(TestException.class); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void innerErrorRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final PublishProcessor<Object> pp1 = PublishProcessor.create(); + final PublishProcessor<Object> pp2 = PublishProcessor.create(); + + List<Throwable> errors = TestHelper.trackPluginErrors(); + + try { + TestSubscriberEx<Flowable<Integer>> ts = Flowable.just(1) + .groupJoin( + Flowable.just(2).concatWith(Flowable.<Integer>never()), + new Function<Integer, Flowable<Object>>() { + @Override + public Flowable<Object> apply(Integer left) throws Exception { + return pp1; + } + }, + new Function<Integer, Flowable<Object>>() { + @Override + public Flowable<Object> apply(Integer right) throws Exception { + return pp2; + } + }, + new BiFunction<Integer, Flowable<Integer>, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Integer r, Flowable<Integer> l) throws Exception { + return l; + } + } + ) + .to(TestHelper.<Flowable<Integer>>testConsumer()); + + final TestException ex1 = new TestException(); + final TestException ex2 = new TestException(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + pp1.onError(ex1); + } + }; + Runnable r2 = new Runnable() { + @Override + public void run() { + pp2.onError(ex2); + } + }; + + TestHelper.race(r1, r2); + + ts.assertError(Throwable.class).assertSubscribed().assertNotComplete().assertValueCount(1); + + Throwable exc = ts.errors().get(0); + + if (exc instanceof CompositeException) { + List<Throwable> es = TestHelper.compositeList(exc); + TestHelper.assertError(es, 0, TestException.class); + TestHelper.assertError(es, 1, TestException.class); + } else { + ts.assertError(TestException.class); + } + + if (!errors.isEmpty()) { + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } + } finally { + RxJavaPlugins.reset(); + } + } + } + + @Test + public void outerErrorRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final PublishProcessor<Object> pp1 = PublishProcessor.create(); + final PublishProcessor<Object> pp2 = PublishProcessor.create(); + + List<Throwable> errors = TestHelper.trackPluginErrors(); + + try { + TestSubscriberEx<Object> ts = pp1 + .groupJoin( + pp2, + new Function<Object, Flowable<Object>>() { + @Override + public Flowable<Object> apply(Object left) throws Exception { + return Flowable.never(); + } + }, + new Function<Object, Flowable<Object>>() { + @Override + public Flowable<Object> apply(Object right) throws Exception { + return Flowable.never(); + } + }, + new BiFunction<Object, Flowable<Object>, Flowable<Object>>() { + @Override + public Flowable<Object> apply(Object r, Flowable<Object> l) throws Exception { + return l; + } + } + ) + .flatMap(Functions.<Flowable<Object>>identity()) + .to(TestHelper.<Object>testConsumer()); + + final TestException ex1 = new TestException(); + final TestException ex2 = new TestException(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + pp1.onError(ex1); + } + }; + Runnable r2 = new Runnable() { + @Override + public void run() { + pp2.onError(ex2); + } + }; + + TestHelper.race(r1, r2); + + ts.assertError(Throwable.class).assertSubscribed().assertNotComplete().assertNoValues(); + + Throwable exc = ts.errors().get(0); + + if (exc instanceof CompositeException) { + List<Throwable> es = TestHelper.compositeList(exc); + TestHelper.assertError(es, 0, TestException.class); + TestHelper.assertError(es, 1, TestException.class); + } else { + ts.assertError(TestException.class); + } + + if (!errors.isEmpty()) { + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } + } finally { + RxJavaPlugins.reset(); + } + } + } + + @Test + public void rightEmission() { + final PublishProcessor<Object> pp1 = PublishProcessor.create(); + final PublishProcessor<Object> pp2 = PublishProcessor.create(); + + TestSubscriber<Object> ts = pp1 + .groupJoin( + pp2, + new Function<Object, Flowable<Object>>() { + @Override + public Flowable<Object> apply(Object left) throws Exception { + return Flowable.never(); + } + }, + new Function<Object, Flowable<Object>>() { + @Override + public Flowable<Object> apply(Object right) throws Exception { + return Flowable.never(); + } + }, + new BiFunction<Object, Flowable<Object>, Flowable<Object>>() { + @Override + public Flowable<Object> apply(Object r, Flowable<Object> l) throws Exception { + return l; + } + } + ) + .flatMap(Functions.<Flowable<Object>>identity()) + .test(); + + pp2.onNext(2); + + pp1.onNext(1); + pp1.onComplete(); + + pp2.onComplete(); + + ts.assertResult(2); + } + + @Test + public void leftRightState() { + JoinSupport js = mock(JoinSupport.class); + + LeftRightSubscriber o = new LeftRightSubscriber(js, false); + + assertFalse(o.isDisposed()); + + o.onNext(1); + o.onNext(2); + + o.dispose(); + + assertTrue(o.isDisposed()); + + verify(js).innerValue(false, 1); + verify(js).innerValue(false, 2); + } + + @Test + public void leftRightEndState() { + JoinSupport js = mock(JoinSupport.class); + + LeftRightEndSubscriber o = new LeftRightEndSubscriber(js, false, 0); + + assertFalse(o.isDisposed()); + + o.onNext(1); + o.onNext(2); + + assertTrue(o.isDisposed()); + + verify(js).innerClose(false, o); + } + + @Test + public void disposeAfterOnNext() { + PublishProcessor<Integer> pp1 = PublishProcessor.create(); + PublishProcessor<Integer> pp2 = PublishProcessor.create(); + + TestSubscriber<Integer> ts = new TestSubscriber<>(); + + pp1.groupJoin(pp2, v -> Flowable.never(), v -> Flowable.never(), (a, b) -> a) + .doOnNext(v -> { + ts.cancel(); + }) + .subscribe(ts); + + pp2.onNext(1); + pp1.onNext(1); + } + + @Test + public void completeWithMoreWork() { + PublishProcessor<Integer> pp1 = PublishProcessor.create(); + PublishProcessor<Integer> pp2 = PublishProcessor.create(); + + TestSubscriber<Integer> ts = new TestSubscriber<>(); + + pp1.groupJoin(pp2, v -> Flowable.never(), v -> Flowable.never(), (a, b) -> a) + .doOnNext(v -> { + if (v == 1) { + pp2.onNext(2); + pp1.onComplete(); + pp2.onComplete(); + } + }) + .subscribe(ts); + + pp2.onNext(1); + pp1.onNext(1); + } + + @Test + public void badRequest() { + TestHelper.assertBadRequestReported(Flowable.never().groupJoin(Flowable.never(), v -> Flowable.never(), v -> Flowable.never(), (a, b) -> a)); + } + + @Test + public void missingBackpressure() { + Flowable.just(1) + .groupJoin(Flowable.never(), v -> BehaviorProcessor.createDefault(1), v -> Flowable.never(), (a, b) -> a) + .test(0) + .assertFailure(MissingBackpressureException.class); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableHideTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableHideTest.java new file mode 100644 index 0000000000..f95775ebda --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableHideTest.java @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import static org.junit.Assert.assertFalse; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import org.junit.Test; +import org.reactivestreams.Subscriber; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.processors.PublishProcessor; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class FlowableHideTest extends RxJavaTest { + @Test + public void hiding() { + PublishProcessor<Integer> src = PublishProcessor.create(); + + Flowable<Integer> dst = src.hide(); + + assertFalse(dst instanceof PublishProcessor); + + Subscriber<Object> subscriber = TestHelper.mockSubscriber(); + + dst.subscribe(subscriber); + + src.onNext(1); + src.onComplete(); + + verify(subscriber).onNext(1); + verify(subscriber).onComplete(); + verify(subscriber, never()).onError(any(Throwable.class)); + } + + @Test + public void hidingError() { + PublishProcessor<Integer> src = PublishProcessor.create(); + + Flowable<Integer> dst = src.hide(); + + assertFalse(dst instanceof PublishProcessor); + + Subscriber<Object> subscriber = TestHelper.mockSubscriber(); + + dst.subscribe(subscriber); + + src.onError(new TestException()); + + verify(subscriber, never()).onNext(any()); + verify(subscriber, never()).onComplete(); + verify(subscriber).onError(any(TestException.class)); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeFlowable(new Function<Flowable<Object>, Flowable<Object>>() { + @Override + public Flowable<Object> apply(Flowable<Object> f) + throws Exception { + return f.hide(); + } + }); + } + + @Test + public void disposed() { + TestHelper.checkDisposed(PublishProcessor.create().hide()); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableIgnoreElementsTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableIgnoreElementsTest.java new file mode 100644 index 0000000000..dfc0e11680 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableIgnoreElementsTest.java @@ -0,0 +1,347 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import static org.junit.Assert.*; + +import java.util.concurrent.atomic.*; + +import org.junit.Test; +import org.reactivestreams.Subscription; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.observers.DisposableCompletableObserver; +import io.reactivex.rxjava3.operators.QueueFuseable; +import io.reactivex.rxjava3.operators.QueueSubscription; +import io.reactivex.rxjava3.processors.PublishProcessor; +import io.reactivex.rxjava3.subscribers.*; +import io.reactivex.rxjava3.testsupport.*; + +public class FlowableIgnoreElementsTest extends RxJavaTest { + + @Test + public void withEmptyFlowable() { + assertTrue(Flowable.empty().ignoreElements().toFlowable().isEmpty().blockingGet()); + } + + @Test + public void withNonEmptyFlowable() { + assertTrue(Flowable.just(1, 2, 3).ignoreElements().toFlowable().isEmpty().blockingGet()); + } + + @Test + public void upstreamIsProcessedButIgnoredFlowable() { + final int num = 10; + final AtomicInteger upstreamCount = new AtomicInteger(); + long count = Flowable.range(1, num) + .doOnNext(new Consumer<Integer>() { + @Override + public void accept(Integer t) { + upstreamCount.incrementAndGet(); + } + }) + .ignoreElements() + .toFlowable() + .count().blockingGet(); + assertEquals(num, upstreamCount.get()); + assertEquals(0, count); + } + + @Test + public void completedOkFlowable() { + TestSubscriberEx<Object> ts = new TestSubscriberEx<>(); + Flowable.range(1, 10).ignoreElements().toFlowable().subscribe(ts); + ts.assertNoErrors(); + ts.assertNoValues(); + ts.assertTerminated(); + } + + @Test + public void errorReceivedFlowable() { + TestSubscriberEx<Object> ts = new TestSubscriberEx<>(); + TestException ex = new TestException("boo"); + Flowable.error(ex).ignoreElements().toFlowable().subscribe(ts); + ts.assertNoValues(); + ts.assertTerminated(); + ts.assertError(TestException.class); + ts.assertErrorMessage("boo"); + } + + @Test + public void unsubscribesFromUpstreamFlowable() { + final AtomicBoolean unsub = new AtomicBoolean(); + Flowable.range(1, 10).concatWith(Flowable.<Integer>never()) + .doOnCancel(new Action() { + @Override + public void run() { + unsub.set(true); + }}) + .ignoreElements() + .toFlowable() + .subscribe().dispose(); + + assertTrue(unsub.get()); + } + + @Test + public void doesNotHangAndProcessesAllUsingBackpressureFlowable() { + final AtomicInteger upstreamCount = new AtomicInteger(); + final AtomicInteger count = new AtomicInteger(0); + int num = 10; + Flowable.range(1, num) + // + .doOnNext(new Consumer<Integer>() { + @Override + public void accept(Integer t) { + upstreamCount.incrementAndGet(); + } + }) + // + .ignoreElements() + .<Integer>toFlowable() + // + .doOnNext(new Consumer<Integer>() { + + @Override + public void accept(Integer t) { + upstreamCount.incrementAndGet(); + } + }) + // + .subscribe(new DefaultSubscriber<Integer>() { + + @Override + public void onStart() { + request(1); + } + + @Override + public void onComplete() { + + } + + @Override + public void onError(Throwable e) { + } + + @Override + public void onNext(Integer t) { + count.incrementAndGet(); + } + }); + assertEquals(num, upstreamCount.get()); + assertEquals(0, count.get()); + } + + @Test + public void withEmpty() { + Flowable.empty().ignoreElements().blockingAwait(); + } + + @Test + public void withNonEmpty() { + Flowable.just(1, 2, 3).ignoreElements().blockingAwait(); + } + + @Test + public void upstreamIsProcessedButIgnored() { + final int num = 10; + final AtomicInteger upstreamCount = new AtomicInteger(); + Flowable.range(1, num) + .doOnNext(new Consumer<Integer>() { + @Override + public void accept(Integer t) { + upstreamCount.incrementAndGet(); + } + }) + .ignoreElements() + .blockingAwait(); + assertEquals(num, upstreamCount.get()); + } + + @Test + public void completedOk() { + TestObserverEx<Object> to = new TestObserverEx<>(); + Flowable.range(1, 10).ignoreElements().subscribe(to); + to.assertNoErrors(); + to.assertNoValues(); + to.assertTerminated(); + } + + @Test + public void errorReceived() { + TestObserverEx<Object> to = new TestObserverEx<>(); + TestException ex = new TestException("boo"); + Flowable.error(ex).ignoreElements().subscribe(to); + to.assertNoValues(); + to.assertTerminated(); + to.assertError(TestException.class); + to.assertErrorMessage("boo"); + } + + @Test + public void unsubscribesFromUpstream() { + final AtomicBoolean unsub = new AtomicBoolean(); + Flowable.range(1, 10).concatWith(Flowable.<Integer>never()) + .doOnCancel(new Action() { + @Override + public void run() { + unsub.set(true); + }}) + .ignoreElements() + .subscribe().dispose(); + + assertTrue(unsub.get()); + } + + @Test + public void doesNotHangAndProcessesAllUsingBackpressure() { + final AtomicInteger upstreamCount = new AtomicInteger(); + final AtomicInteger count = new AtomicInteger(0); + int num = 10; + Flowable.range(1, num) + // + .doOnNext(new Consumer<Integer>() { + @Override + public void accept(Integer t) { + upstreamCount.incrementAndGet(); + } + }) + // + .ignoreElements() + // + .subscribe(new DisposableCompletableObserver() { + @Override + public void onComplete() { + } + + @Override + public void onError(Throwable e) { + } + }); + assertEquals(num, upstreamCount.get()); + assertEquals(0, count.get()); + } + + @Test + public void cancel() { + + PublishProcessor<Integer> pp = PublishProcessor.create(); + + TestSubscriber<Integer> ts = pp.ignoreElements().<Integer>toFlowable().test(); + + assertTrue(pp.hasSubscribers()); + + ts.cancel(); + + assertFalse(pp.hasSubscribers()); + } + + @Test + public void fused() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<Integer>().setInitialFusionMode(QueueFuseable.ANY); + + Flowable.just(1).hide().ignoreElements().<Integer>toFlowable() + .subscribe(ts); + + ts.assertFuseable() + .assertFusionMode(QueueFuseable.ASYNC) + .assertResult(); + } + + @Test + public void fusedAPICalls() { + Flowable.just(1).hide().ignoreElements().<Integer>toFlowable() + .subscribe(new FlowableSubscriber<Integer>() { + + @Override + public void onSubscribe(Subscription s) { + @SuppressWarnings("unchecked") + QueueSubscription<Integer> qs = (QueueSubscription<Integer>)s; + + try { + assertNull(qs.poll()); + } catch (Throwable ex) { + throw new AssertionError(ex); + } + + assertTrue(qs.isEmpty()); + + qs.clear(); + + assertTrue(qs.isEmpty()); + + try { + assertNull(qs.poll()); + } catch (Throwable ex) { + throw new AssertionError(ex); + } + + try { + qs.offer(1); + fail("Should have thrown!"); + } catch (UnsupportedOperationException ex) { + // expected + } + + try { + qs.offer(1, 2); + fail("Should have thrown!"); + } catch (UnsupportedOperationException ex) { + // expected + } + } + + @Override + public void onNext(Integer t) { + } + + @Override + public void onError(Throwable t) { + } + + @Override + public void onComplete() { + } + }); + } + + @Test + public void dispose() { + TestHelper.checkDisposed(Flowable.just(1).ignoreElements()); + + TestHelper.checkDisposed(Flowable.just(1).ignoreElements().toFlowable()); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeFlowable(new Function<Flowable<Object>, Flowable<Object>>() { + @Override + public Flowable<Object> apply(Flowable<Object> f) + throws Exception { + return f.ignoreElements().toFlowable(); + } + }); + + TestHelper.checkDoubleOnSubscribeFlowableToCompletable(new Function<Flowable<Object>, Completable>() { + @Override + public Completable apply(Flowable<Object> f) + throws Exception { + return f.ignoreElements(); + } + }); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableInternalHelperTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableInternalHelperTest.java new file mode 100644 index 0000000000..1549f01d63 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableInternalHelperTest.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.RxJavaTest; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class FlowableInternalHelperTest extends RxJavaTest { + + @Test + public void utilityClass() { + TestHelper.checkUtilityClass(FlowableInternalHelper.class); + } + + @Test + public void requestMaxEnum() { + TestHelper.checkEnum(FlowableInternalHelper.RequestMax.class); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableIntervalRangeTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableIntervalRangeTest.java new file mode 100644 index 0000000000..f8bc3355e9 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableIntervalRangeTest.java @@ -0,0 +1,126 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import static org.junit.Assert.*; + +import java.util.concurrent.TimeUnit; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.MissingBackpressureException; +import io.reactivex.rxjava3.schedulers.Schedulers; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class FlowableIntervalRangeTest extends RxJavaTest { + @Test + public void simple() throws Exception { + Flowable.intervalRange(5, 5, 50, 50, TimeUnit.MILLISECONDS) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(5L, 6L, 7L, 8L, 9L); + } + + @Test + public void customScheduler() { + Flowable.intervalRange(1, 5, 1, 1, TimeUnit.MILLISECONDS, Schedulers.single()) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(1L, 2L, 3L, 4L, 5L); + } + + @Test + public void countZero() { + Flowable.intervalRange(1, 0, 1, 1, TimeUnit.MILLISECONDS) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(); + } + + @Test + public void countNegative() { + try { + Flowable.intervalRange(1, -1, 1, 1, TimeUnit.MILLISECONDS); + fail("Should have thrown!"); + } catch (IllegalArgumentException ex) { + assertEquals("count >= 0 required but it was -1", ex.getMessage()); + } + } + + @Test + public void longOverflow() { + Flowable.intervalRange(Long.MAX_VALUE - 1, 2, 1, 1, TimeUnit.MILLISECONDS); + + Flowable.intervalRange(Long.MIN_VALUE, Long.MAX_VALUE, 1, 1, TimeUnit.MILLISECONDS); + + try { + Flowable.intervalRange(Long.MAX_VALUE - 1, 3, 1, 1, TimeUnit.MILLISECONDS); + fail("Should have thrown!"); + } catch (IllegalArgumentException ex) { + assertEquals("Overflow! start + count is bigger than Long.MAX_VALUE", ex.getMessage()); + } + } + + @Test + public void dispose() { + TestHelper.checkDisposed(Flowable.intervalRange(1, 2, 1, 1, TimeUnit.MILLISECONDS)); + } + + @Test + public void backpressureBounded() { + Flowable.intervalRange(1, 2, 1, 1, TimeUnit.MILLISECONDS) + .test(2L) + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(1L, 2L); + } + + @Test + public void backpressureOverflow() { + Flowable.intervalRange(1, 3, 1, 1, TimeUnit.MILLISECONDS) + .test(2L) + .awaitDone(5, TimeUnit.SECONDS) + .assertFailure(MissingBackpressureException.class, 1L, 2L); + } + + @Test + public void badRequest() { + TestHelper.assertBadRequestReported(Flowable.intervalRange(1, 3, 1, 1, TimeUnit.MILLISECONDS)); + } + + @Test + public void take() { + Flowable.intervalRange(1, 2, 1, 1, TimeUnit.MILLISECONDS) + .take(1) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(1L); + } + + @Test + public void cancel() { + Flowable.intervalRange(0, 20, 1, 1, TimeUnit.MILLISECONDS, Schedulers.trampoline()) + .take(10) + .test() + .assertResult(0L, 1L, 2L, 3L, 4L, 5L, 6L, 7L, 8L, 9L); + } + + @Test + public void takeSameAsRange() { + Flowable.intervalRange(0, 2, 1, 1, TimeUnit.MILLISECONDS, Schedulers.trampoline()) + .take(2) + .test() + .assertResult(0L, 1L); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableIntervalTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableIntervalTest.java new file mode 100644 index 0000000000..7fde08d47f --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableIntervalTest.java @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import java.util.concurrent.TimeUnit; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.internal.operators.flowable.FlowableInterval.IntervalSubscriber; +import io.reactivex.rxjava3.schedulers.Schedulers; +import io.reactivex.rxjava3.subscribers.TestSubscriber; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class FlowableIntervalTest extends RxJavaTest { + + @Test + public void cancel() { + Flowable.interval(1, TimeUnit.MILLISECONDS, Schedulers.trampoline()) + .take(10) + .test() + .assertResult(0L, 1L, 2L, 3L, 4L, 5L, 6L, 7L, 8L, 9L); + } + + @Test + public void badRequest() { + TestHelper.assertBadRequestReported(Flowable.interval(1, TimeUnit.MILLISECONDS, Schedulers.trampoline())); + } + + @Test + public void cancelledOnRun() { + TestSubscriber<Long> ts = new TestSubscriber<>(); + IntervalSubscriber is = new IntervalSubscriber(ts); + ts.onSubscribe(is); + + is.cancel(); + + is.run(); + + ts.assertEmpty(); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableJoinTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableJoinTest.java new file mode 100644 index 0000000000..fe28a93320 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableJoinTest.java @@ -0,0 +1,519 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import java.util.List; + +import org.junit.*; +import org.mockito.MockitoAnnotations; +import org.reactivestreams.Subscriber; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.internal.subscriptions.BooleanSubscription; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.processors.PublishProcessor; +import io.reactivex.rxjava3.subscribers.TestSubscriber; +import io.reactivex.rxjava3.testsupport.*; + +public class FlowableJoinTest extends RxJavaTest { + Subscriber<Object> subscriber = TestHelper.mockSubscriber(); + + BiFunction<Integer, Integer, Integer> add = new BiFunction<Integer, Integer, Integer>() { + @Override + public Integer apply(Integer t1, Integer t2) { + return t1 + t2; + } + }; + + <T> Function<Integer, Flowable<T>> just(final Flowable<T> flowable) { + return new Function<Integer, Flowable<T>>() { + @Override + public Flowable<T> apply(Integer t1) { + return flowable; + } + }; + } + + @Before + public void before() { + MockitoAnnotations.openMocks(this); + } + + @Test + public void normal1() { + PublishProcessor<Integer> source1 = PublishProcessor.create(); + PublishProcessor<Integer> source2 = PublishProcessor.create(); + + Flowable<Integer> m = source1.join(source2, + just(Flowable.never()), + just(Flowable.never()), add); + + m.subscribe(subscriber); + + source1.onNext(1); + source1.onNext(2); + source1.onNext(4); + + source2.onNext(16); + source2.onNext(32); + source2.onNext(64); + + source1.onComplete(); + source2.onComplete(); + + verify(subscriber, times(1)).onNext(17); + verify(subscriber, times(1)).onNext(18); + verify(subscriber, times(1)).onNext(20); + verify(subscriber, times(1)).onNext(33); + verify(subscriber, times(1)).onNext(34); + verify(subscriber, times(1)).onNext(36); + verify(subscriber, times(1)).onNext(65); + verify(subscriber, times(1)).onNext(66); + verify(subscriber, times(1)).onNext(68); + + verify(subscriber, times(1)).onComplete(); + verify(subscriber, never()).onError(any(Throwable.class)); + } + + @Test + public void normal1WithDuration() { + PublishProcessor<Integer> source1 = PublishProcessor.create(); + PublishProcessor<Integer> source2 = PublishProcessor.create(); + + PublishProcessor<Integer> duration1 = PublishProcessor.create(); + + Flowable<Integer> m = source1.join(source2, + just(duration1), + just(Flowable.never()), add); + m.subscribe(subscriber); + + source1.onNext(1); + source1.onNext(2); + source2.onNext(16); + + duration1.onNext(1); + + source1.onNext(4); + source1.onNext(8); + + source1.onComplete(); + source2.onComplete(); + + verify(subscriber, times(1)).onNext(17); + verify(subscriber, times(1)).onNext(18); + verify(subscriber, times(1)).onNext(20); + verify(subscriber, times(1)).onNext(24); + + verify(subscriber, times(1)).onComplete(); + verify(subscriber, never()).onError(any(Throwable.class)); + + } + + @Test + public void normal2() { + PublishProcessor<Integer> source1 = PublishProcessor.create(); + PublishProcessor<Integer> source2 = PublishProcessor.create(); + + Flowable<Integer> m = source1.join(source2, + just(Flowable.never()), + just(Flowable.never()), add); + + m.subscribe(subscriber); + + source1.onNext(1); + source1.onNext(2); + source1.onComplete(); + + source2.onNext(16); + source2.onNext(32); + source2.onNext(64); + + source2.onComplete(); + + verify(subscriber, times(1)).onNext(17); + verify(subscriber, times(1)).onNext(18); + verify(subscriber, times(1)).onNext(33); + verify(subscriber, times(1)).onNext(34); + verify(subscriber, times(1)).onNext(65); + verify(subscriber, times(1)).onNext(66); + + verify(subscriber, times(1)).onComplete(); + verify(subscriber, never()).onError(any(Throwable.class)); + } + + @Test + public void leftThrows() { + PublishProcessor<Integer> source1 = PublishProcessor.create(); + PublishProcessor<Integer> source2 = PublishProcessor.create(); + + Flowable<Integer> m = source1.join(source2, + just(Flowable.never()), + just(Flowable.never()), add); + + m.subscribe(subscriber); + + source2.onNext(1); + source1.onError(new RuntimeException("Forced failure")); + + verify(subscriber, times(1)).onError(any(Throwable.class)); + verify(subscriber, never()).onComplete(); + verify(subscriber, never()).onNext(any()); + } + + @Test + public void rightThrows() { + PublishProcessor<Integer> source1 = PublishProcessor.create(); + PublishProcessor<Integer> source2 = PublishProcessor.create(); + + Flowable<Integer> m = source1.join(source2, + just(Flowable.never()), + just(Flowable.never()), add); + + m.subscribe(subscriber); + + source1.onNext(1); + source2.onError(new RuntimeException("Forced failure")); + + verify(subscriber, times(1)).onError(any(Throwable.class)); + verify(subscriber, never()).onComplete(); + verify(subscriber, never()).onNext(any()); + } + + @Test + public void leftDurationThrows() { + PublishProcessor<Integer> source1 = PublishProcessor.create(); + PublishProcessor<Integer> source2 = PublishProcessor.create(); + + Flowable<Integer> duration1 = Flowable.<Integer> error(new RuntimeException("Forced failure")); + + Flowable<Integer> m = source1.join(source2, + just(duration1), + just(Flowable.never()), add); + m.subscribe(subscriber); + + source1.onNext(1); + + verify(subscriber, times(1)).onError(any(Throwable.class)); + verify(subscriber, never()).onComplete(); + verify(subscriber, never()).onNext(any()); + } + + @Test + public void rightDurationThrows() { + PublishProcessor<Integer> source1 = PublishProcessor.create(); + PublishProcessor<Integer> source2 = PublishProcessor.create(); + + Flowable<Integer> duration1 = Flowable.<Integer> error(new RuntimeException("Forced failure")); + + Flowable<Integer> m = source1.join(source2, + just(Flowable.never()), + just(duration1), add); + m.subscribe(subscriber); + + source2.onNext(1); + + verify(subscriber, times(1)).onError(any(Throwable.class)); + verify(subscriber, never()).onComplete(); + verify(subscriber, never()).onNext(any()); + } + + @Test + public void leftDurationSelectorThrows() { + PublishProcessor<Integer> source1 = PublishProcessor.create(); + PublishProcessor<Integer> source2 = PublishProcessor.create(); + + Function<Integer, Flowable<Integer>> fail = new Function<Integer, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Integer t1) { + throw new RuntimeException("Forced failure"); + } + }; + + Flowable<Integer> m = source1.join(source2, + fail, + just(Flowable.never()), add); + m.subscribe(subscriber); + + source1.onNext(1); + + verify(subscriber, times(1)).onError(any(Throwable.class)); + verify(subscriber, never()).onComplete(); + verify(subscriber, never()).onNext(any()); + } + + @Test + public void rightDurationSelectorThrows() { + PublishProcessor<Integer> source1 = PublishProcessor.create(); + PublishProcessor<Integer> source2 = PublishProcessor.create(); + + Function<Integer, Flowable<Integer>> fail = new Function<Integer, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Integer t1) { + throw new RuntimeException("Forced failure"); + } + }; + + Flowable<Integer> m = source1.join(source2, + just(Flowable.never()), + fail, add); + m.subscribe(subscriber); + + source2.onNext(1); + + verify(subscriber, times(1)).onError(any(Throwable.class)); + verify(subscriber, never()).onComplete(); + verify(subscriber, never()).onNext(any()); + } + + @Test + public void resultSelectorThrows() { + PublishProcessor<Integer> source1 = PublishProcessor.create(); + PublishProcessor<Integer> source2 = PublishProcessor.create(); + + BiFunction<Integer, Integer, Integer> fail = new BiFunction<Integer, Integer, Integer>() { + @Override + public Integer apply(Integer t1, Integer t2) { + throw new RuntimeException("Forced failure"); + } + }; + + Flowable<Integer> m = source1.join(source2, + just(Flowable.never()), + just(Flowable.never()), fail); + m.subscribe(subscriber); + + source1.onNext(1); + source2.onNext(2); + + verify(subscriber, times(1)).onError(any(Throwable.class)); + verify(subscriber, never()).onComplete(); + verify(subscriber, never()).onNext(any()); + } + + @Test + public void dispose() { + TestHelper.checkDisposed(PublishProcessor.<Integer>create().join(Flowable.just(1), + Functions.justFunction(Flowable.never()), + Functions.justFunction(Flowable.never()), new BiFunction<Integer, Integer, Integer>() { + @Override + public Integer apply(Integer a, Integer b) throws Exception { + return a + b; + } + })); + } + + @Test + public void take() { + Flowable.just(1).join( + Flowable.just(2), + Functions.justFunction(Flowable.never()), + Functions.justFunction(Flowable.never()), + new BiFunction<Integer, Integer, Integer>() { + @Override + public Integer apply(Integer a, Integer b) throws Exception { + return a + b; + } + }) + .take(1) + .test() + .assertResult(3); + } + + @Test + public void rightClose() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + + TestSubscriber<Integer> ts = pp.join(Flowable.just(2), + Functions.justFunction(Flowable.never()), + Functions.justFunction(Flowable.empty()), + new BiFunction<Integer, Integer, Integer>() { + @Override + public Integer apply(Integer a, Integer b) throws Exception { + return a + b; + } + }) + .test() + .assertEmpty(); + + pp.onNext(1); + + ts.assertEmpty(); + } + + @Test + public void resultSelectorThrows2() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + + TestSubscriber<Integer> ts = pp.join( + Flowable.just(2), + Functions.justFunction(Flowable.never()), + Functions.justFunction(Flowable.never()), + new BiFunction<Integer, Integer, Integer>() { + @Override + public Integer apply(Integer a, Integer b) throws Exception { + throw new TestException(); + } + }) + .test(); + + pp.onNext(1); + pp.onComplete(); + + ts.assertFailure(TestException.class); + } + + @Test + public void badOuterSource() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + new Flowable<Integer>() { + @Override + protected void subscribeActual(Subscriber<? super Integer> subscriber) { + subscriber.onSubscribe(new BooleanSubscription()); + subscriber.onError(new TestException("First")); + subscriber.onError(new TestException("Second")); + } + } + .join(Flowable.just(2), + Functions.justFunction(Flowable.never()), + Functions.justFunction(Flowable.never()), + new BiFunction<Integer, Integer, Integer>() { + @Override + public Integer apply(Integer a, Integer b) throws Exception { + return a + b; + } + }) + .to(TestHelper.<Integer>testConsumer()) + .assertFailureAndMessage(TestException.class, "First"); + + TestHelper.assertUndeliverable(errors, 0, TestException.class, "Second"); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void badEndSource() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + @SuppressWarnings("rawtypes") + final Subscriber[] o = { null }; + + TestSubscriberEx<Integer> ts = Flowable.just(1) + .join(Flowable.just(2), + Functions.justFunction(Flowable.never()), + Functions.justFunction(new Flowable<Integer>() { + @Override + protected void subscribeActual(Subscriber<? super Integer> subscriber) { + o[0] = subscriber; + subscriber.onSubscribe(new BooleanSubscription()); + subscriber.onError(new TestException("First")); + } + }), + new BiFunction<Integer, Integer, Integer>() { + @Override + public Integer apply(Integer a, Integer b) throws Exception { + return a + b; + } + }) + .to(TestHelper.<Integer>testConsumer()); + + o[0].onError(new TestException("Second")); + + ts + .assertFailureAndMessage(TestException.class, "First"); + + TestHelper.assertUndeliverable(errors, 0, TestException.class, "Second"); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void backpressureOverflowRight() { + PublishProcessor<Integer> pp1 = PublishProcessor.create(); + PublishProcessor<Integer> pp2 = PublishProcessor.create(); + + TestSubscriber<Object> ts = pp1.join(pp2, Functions.justFunction(Flowable.never()), Functions.justFunction(Flowable.never()), + new BiFunction<Integer, Integer, Object>() { + @Override + public Object apply(Integer a, Integer b) throws Exception { + return a + b; + } + }) + .test(0L); + + pp1.onNext(1); + pp2.onNext(2); + + ts.assertFailure(MissingBackpressureException.class); + } + + @Test + public void backpressureOverflowLeft() { + PublishProcessor<Integer> pp1 = PublishProcessor.create(); + PublishProcessor<Integer> pp2 = PublishProcessor.create(); + + TestSubscriber<Object> ts = pp1.join(pp2, Functions.justFunction(Flowable.never()), Functions.justFunction(Flowable.never()), + new BiFunction<Integer, Integer, Object>() { + @Override + public Object apply(Integer a, Integer b) throws Exception { + return a + b; + } + }) + .test(0L); + + pp2.onNext(2); + pp1.onNext(1); + + ts.assertFailure(MissingBackpressureException.class); + } + + @Test + public void badRequest() { + PublishProcessor<Integer> pp1 = PublishProcessor.create(); + PublishProcessor<Integer> pp2 = PublishProcessor.create(); + + TestHelper.assertBadRequestReported(pp1.join(pp2, Functions.justFunction(Flowable.never()), Functions.justFunction(Flowable.never()), (a, b) -> a + b)); + } + + @Test + public void bothTerminateWithWorkRemaining() { + PublishProcessor<Integer> pp1 = PublishProcessor.create(); + PublishProcessor<Integer> pp2 = PublishProcessor.create(); + + TestSubscriber<Integer> ts = pp1.join( + pp2, + v -> Flowable.never(), + v -> Flowable.never(), + (a, b) -> a + b) + .doOnNext(v -> { + pp1.onComplete(); + pp2.onNext(2); + pp2.onComplete(); + }) + .test(); + + pp1.onNext(0); + pp2.onNext(1); + + ts.assertComplete(); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableLastTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableLastTest.java new file mode 100644 index 0000000000..3164802ab6 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableLastTest.java @@ -0,0 +1,380 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import java.util.NoSuchElementException; + +import org.junit.Test; +import org.mockito.InOrder; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class FlowableLastTest extends RxJavaTest { + + @Test + public void lastWithElements() { + Maybe<Integer> last = Flowable.just(1, 2, 3).lastElement(); + assertEquals(3, last.blockingGet().intValue()); + } + + @Test + public void lastWithNoElements() { + Maybe<?> last = Flowable.empty().lastElement(); + assertNull(last.blockingGet()); + } + + @Test + public void lastMultiSubscribe() { + Maybe<Integer> last = Flowable.just(1, 2, 3).lastElement(); + assertEquals(3, last.blockingGet().intValue()); + assertEquals(3, last.blockingGet().intValue()); + } + + @Test + public void lastViaFlowable() { + Flowable.just(1, 2, 3).lastElement(); + } + + @Test + public void last() { + Maybe<Integer> maybe = Flowable.just(1, 2, 3).lastElement(); + + MaybeObserver<Integer> observer = TestHelper.mockMaybeObserver(); + maybe.subscribe(observer); + + InOrder inOrder = inOrder(observer); + inOrder.verify(observer, times(1)).onSuccess(3); +// inOrder.verify(observer, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void lastWithOneElement() { + Maybe<Integer> maybe = Flowable.just(1).lastElement(); + + MaybeObserver<Integer> observer = TestHelper.mockMaybeObserver(); + maybe.subscribe(observer); + + InOrder inOrder = inOrder(observer); + inOrder.verify(observer, times(1)).onSuccess(1); +// inOrder.verify(observer, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void lastWithEmpty() { + Maybe<Integer> maybe = Flowable.<Integer> empty().lastElement(); + + MaybeObserver<Integer> observer = TestHelper.mockMaybeObserver(); + maybe.subscribe(observer); + + InOrder inOrder = inOrder(observer); + inOrder.verify(observer).onComplete(); + inOrder.verify(observer, never()).onError(any(Throwable.class)); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void lastWithPredicate() { + Maybe<Integer> maybe = Flowable.just(1, 2, 3, 4, 5, 6) + .filter(new Predicate<Integer>() { + + @Override + public boolean test(Integer t1) { + return t1 % 2 == 0; + } + }) + .lastElement(); + + MaybeObserver<Integer> observer = TestHelper.mockMaybeObserver(); + maybe.subscribe(observer); + + InOrder inOrder = inOrder(observer); + inOrder.verify(observer, times(1)).onSuccess(6); +// inOrder.verify(observer, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void lastWithPredicateAndOneElement() { + Maybe<Integer> maybe = Flowable.just(1, 2) + .filter( + new Predicate<Integer>() { + + @Override + public boolean test(Integer t1) { + return t1 % 2 == 0; + } + }) + .lastElement(); + + MaybeObserver<Integer> observer = TestHelper.mockMaybeObserver(); + maybe.subscribe(observer); + + InOrder inOrder = inOrder(observer); + inOrder.verify(observer, times(1)).onSuccess(2); +// inOrder.verify(observer, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void lastWithPredicateAndEmpty() { + Maybe<Integer> maybe = Flowable.just(1) + .filter( + new Predicate<Integer>() { + + @Override + public boolean test(Integer t1) { + return t1 % 2 == 0; + } + }).lastElement(); + + MaybeObserver<Integer> observer = TestHelper.mockMaybeObserver(); + maybe.subscribe(observer); + + InOrder inOrder = inOrder(observer); + inOrder.verify(observer).onComplete(); + inOrder.verify(observer, never()).onError(any(Throwable.class)); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void lastOrDefault() { + Single<Integer> single = Flowable.just(1, 2, 3) + .last(4); + + SingleObserver<Integer> observer = TestHelper.mockSingleObserver(); + single.subscribe(observer); + + InOrder inOrder = inOrder(observer); + inOrder.verify(observer, times(1)).onSuccess(3); +// inOrder.verify(observer, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void lastOrDefaultWithOneElement() { + Single<Integer> single = Flowable.just(1).last(2); + + SingleObserver<Integer> observer = TestHelper.mockSingleObserver(); + single.subscribe(observer); + + InOrder inOrder = inOrder(observer); + inOrder.verify(observer, times(1)).onSuccess(1); +// inOrder.verify(observer, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void lastOrDefaultWithEmpty() { + Single<Integer> single = Flowable.<Integer> empty() + .last(1); + + SingleObserver<Integer> observer = TestHelper.mockSingleObserver(); + single.subscribe(observer); + + InOrder inOrder = inOrder(observer); + inOrder.verify(observer, times(1)).onSuccess(1); +// inOrder.verify(observer, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void lastOrDefaultWithPredicate() { + Single<Integer> single = Flowable.just(1, 2, 3, 4, 5, 6) + .filter(new Predicate<Integer>() { + + @Override + public boolean test(Integer t1) { + return t1 % 2 == 0; + } + }) + .last(8); + + SingleObserver<Integer> observer = TestHelper.mockSingleObserver(); + single.subscribe(observer); + + InOrder inOrder = inOrder(observer); + inOrder.verify(observer, times(1)).onSuccess(6); +// inOrder.verify(observer, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void lastOrDefaultWithPredicateAndOneElement() { + Single<Integer> single = Flowable.just(1, 2) + .filter(new Predicate<Integer>() { + + @Override + public boolean test(Integer t1) { + return t1 % 2 == 0; + } + }) + .last(4); + + SingleObserver<Integer> observer = TestHelper.mockSingleObserver(); + single.subscribe(observer); + + InOrder inOrder = inOrder(observer); + inOrder.verify(observer, times(1)).onSuccess(2); +// inOrder.verify(observer, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void lastOrDefaultWithPredicateAndEmpty() { + Single<Integer> single = Flowable.just(1) + .filter( + new Predicate<Integer>() { + + @Override + public boolean test(Integer t1) { + return t1 % 2 == 0; + } + }) + .last(2); + + SingleObserver<Integer> observer = TestHelper.mockSingleObserver(); + single.subscribe(observer); + + InOrder inOrder = inOrder(observer); + inOrder.verify(observer, times(1)).onSuccess(2); +// inOrder.verify(observer, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void lastOrErrorNoElement() { + Flowable.empty() + .lastOrError() + .test() + .assertNoValues() + .assertError(NoSuchElementException.class); + } + + @Test + public void lastOrErrorOneElement() { + Flowable.just(1) + .lastOrError() + .test() + .assertNoErrors() + .assertValue(1); + } + + @Test + public void lastOrErrorMultipleElements() { + Flowable.just(1, 2, 3) + .lastOrError() + .test() + .assertNoErrors() + .assertValue(3); + } + + @Test + public void lastOrErrorError() { + Flowable.error(new RuntimeException("error")) + .lastOrError() + .to(TestHelper.testConsumer()) + .assertNoValues() + .assertErrorMessage("error") + .assertError(RuntimeException.class); + } + + @Test + public void dispose() { + TestHelper.checkDisposed(Flowable.never().lastElement().toFlowable()); + TestHelper.checkDisposed(Flowable.never().lastElement()); + + TestHelper.checkDisposed(Flowable.just(1).lastOrError().toFlowable()); + TestHelper.checkDisposed(Flowable.just(1).lastOrError()); + + TestHelper.checkDisposed(Flowable.just(1).last(2).toFlowable()); + TestHelper.checkDisposed(Flowable.just(1).last(2)); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeFlowableToMaybe(new Function<Flowable<Object>, MaybeSource<Object>>() { + @Override + public MaybeSource<Object> apply(Flowable<Object> f) throws Exception { + return f.lastElement(); + } + }); + TestHelper.checkDoubleOnSubscribeFlowable(new Function<Flowable<Object>, Flowable<Object>>() { + @Override + public Flowable<Object> apply(Flowable<Object> f) throws Exception { + return f.lastElement().toFlowable(); + } + }); + + TestHelper.checkDoubleOnSubscribeFlowableToSingle(new Function<Flowable<Object>, SingleSource<Object>>() { + @Override + public SingleSource<Object> apply(Flowable<Object> f) throws Exception { + return f.lastOrError(); + } + }); + TestHelper.checkDoubleOnSubscribeFlowable(new Function<Flowable<Object>, Flowable<Object>>() { + @Override + public Flowable<Object> apply(Flowable<Object> f) throws Exception { + return f.lastOrError().toFlowable(); + } + }); + + TestHelper.checkDoubleOnSubscribeFlowableToSingle(new Function<Flowable<Object>, SingleSource<Object>>() { + @Override + public SingleSource<Object> apply(Flowable<Object> f) throws Exception { + return f.last(2); + } + }); + TestHelper.checkDoubleOnSubscribeFlowable(new Function<Flowable<Object>, Flowable<Object>>() { + @Override + public Flowable<Object> apply(Flowable<Object> f) throws Exception { + return f.last(2).toFlowable(); + } + }); + } + + @Test + public void error() { + Flowable.error(new TestException()) + .lastElement() + .test() + .assertFailure(TestException.class); + } + + @Test + public void errorLastOrErrorFlowable() { + Flowable.error(new TestException()) + .lastOrError() + .toFlowable() + .test() + .assertFailure(TestException.class); + } + + @Test + public void emptyLastOrErrorFlowable() { + Flowable.empty() + .lastOrError() + .toFlowable() + .test() + .assertFailure(NoSuchElementException.class); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableLiftTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableLiftTest.java new file mode 100644 index 0000000000..68227e8dfc --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableLiftTest.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import static org.junit.Assert.*; + +import java.util.List; + +import org.junit.Test; +import org.reactivestreams.Subscriber; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class FlowableLiftTest extends RxJavaTest { + + @Test + public void callbackCrash() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + Flowable.just(1) + .lift(new FlowableOperator<Object, Integer>() { + @Override + public Subscriber<? super Integer> apply(Subscriber<? super Object> subscriber) throws Exception { + throw new TestException(); + } + }) + .test(); + fail("Should have thrown"); + } catch (NullPointerException ex) { + assertTrue(ex.toString(), ex.getCause() instanceof TestException); + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableMapNotificationTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableMapNotificationTest.java new file mode 100644 index 0000000000..905cda5548 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableMapNotificationTest.java @@ -0,0 +1,198 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import org.junit.Test; +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.internal.operators.flowable.FlowableMapNotification.MapNotificationSubscriber; +import io.reactivex.rxjava3.internal.subscriptions.BooleanSubscription; +import io.reactivex.rxjava3.processors.PublishProcessor; +import io.reactivex.rxjava3.subscribers.TestSubscriber; +import io.reactivex.rxjava3.testsupport.*; + +public class FlowableMapNotificationTest extends RxJavaTest { + @Test + public void just() { + TestSubscriber<Object> ts = new TestSubscriber<>(); + Flowable.just(1) + .flatMap( + new Function<Integer, Flowable<Object>>() { + @Override + public Flowable<Object> apply(Integer item) { + return Flowable.just((Object)(item + 1)); + } + }, + new Function<Throwable, Flowable<Object>>() { + @Override + public Flowable<Object> apply(Throwable e) { + return Flowable.error(e); + } + }, + new Supplier<Flowable<Object>>() { + @Override + public Flowable<Object> get() { + return Flowable.never(); + } + } + ).subscribe(ts); + + ts.assertNoErrors(); + ts.assertNotComplete(); + ts.assertValue(2); + } + + @Test + public void backpressure() { + TestSubscriber<Object> ts = TestSubscriber.create(0L); + + new FlowableMapNotification<>(Flowable.range(1, 3), + new Function<Integer, Integer>() { + @Override + public Integer apply(Integer item) { + return item + 1; + } + }, + new Function<Throwable, Integer>() { + @Override + public Integer apply(Throwable e) { + return 0; + } + }, + new Supplier<Integer>() { + @Override + public Integer get() { + return 5; + } + } + ).subscribe(ts); + + ts.assertNoValues(); + ts.assertNoErrors(); + ts.assertNotComplete(); + + ts.request(3); + + ts.assertValues(2, 3, 4); + ts.assertNoErrors(); + ts.assertNotComplete(); + + ts.request(1); + + ts.assertValues(2, 3, 4, 5); + ts.assertNoErrors(); + ts.assertComplete(); + } + + @Test + public void noBackpressure() { + TestSubscriber<Object> ts = TestSubscriber.create(0L); + + PublishProcessor<Integer> pp = PublishProcessor.create(); + + new FlowableMapNotification<>(pp, + new Function<Integer, Integer>() { + @Override + public Integer apply(Integer item) { + return item + 1; + } + }, + new Function<Throwable, Integer>() { + @Override + public Integer apply(Throwable e) { + return 0; + } + }, + new Supplier<Integer>() { + @Override + public Integer get() { + return 5; + } + } + ).subscribe(ts); + + ts.assertNoValues(); + ts.assertNoErrors(); + ts.assertNotComplete(); + + pp.onNext(1); + pp.onNext(2); + pp.onNext(3); + pp.onComplete(); + + ts.assertNoValues(); + ts.assertNoErrors(); + ts.assertNotComplete(); + + ts.request(1); + + ts.assertValue(0); + ts.assertNoErrors(); + ts.assertComplete(); + + } + + @Test + public void dispose() { + TestHelper.checkDisposed(new Flowable<Integer>() { + @SuppressWarnings({ "rawtypes", "unchecked" }) + @Override + protected void subscribeActual(Subscriber<? super Integer> subscriber) { + MapNotificationSubscriber mn = new MapNotificationSubscriber( + subscriber, + Functions.justFunction(Flowable.just(1)), + Functions.justFunction(Flowable.just(2)), + Functions.justSupplier(Flowable.just(3)) + ); + mn.onSubscribe(new BooleanSubscription()); + } + }); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeFlowable(new Function<Flowable<Object>, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Flowable<Object> f) throws Exception { + return f.flatMap( + Functions.justFunction(Flowable.just(1)), + Functions.justFunction(Flowable.just(2)), + Functions.justSupplier(Flowable.just(3)) + ); + } + }); + } + + @Test + public void onErrorCrash() { + TestSubscriberEx<Integer> ts = Flowable.<Integer>error(new TestException("Outer")) + .flatMap(Functions.justFunction(Flowable.just(1)), + new Function<Throwable, Publisher<Integer>>() { + @Override + public Publisher<Integer> apply(Throwable t) throws Exception { + throw new TestException("Inner"); + } + }, + Functions.justSupplier(Flowable.just(3))) + .to(TestHelper.<Integer>testConsumer()) + .assertFailure(CompositeException.class); + + TestHelper.assertError(ts, 0, TestException.class, "Outer"); + TestHelper.assertError(ts, 1, TestException.class, "Inner"); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableMapTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableMapTest.java new file mode 100644 index 0000000000..3d77fe5d46 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableMapTest.java @@ -0,0 +1,637 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import static org.junit.Assert.assertNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import java.io.IOException; +import java.util.*; + +import org.junit.*; +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.internal.schedulers.ImmediateThinScheduler; +import io.reactivex.rxjava3.internal.subscriptions.BooleanSubscription; +import io.reactivex.rxjava3.operators.ConditionalSubscriber; +import io.reactivex.rxjava3.operators.QueueFuseable; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.processors.*; +import io.reactivex.rxjava3.schedulers.Schedulers; +import io.reactivex.rxjava3.subscribers.TestSubscriber; +import io.reactivex.rxjava3.testsupport.*; + +public class FlowableMapTest extends RxJavaTest { + + Subscriber<String> stringSubscriber; + Subscriber<String> stringSubscriber2; + + static final BiFunction<String, Integer, String> APPEND_INDEX = new BiFunction<String, Integer, String>() { + @Override + public String apply(String value, Integer index) { + return value + index; + } + }; + + @Before + public void before() { + stringSubscriber = TestHelper.mockSubscriber(); + stringSubscriber2 = TestHelper.mockSubscriber(); + } + + @Test + public void map() { + Map<String, String> m1 = getMap("One"); + Map<String, String> m2 = getMap("Two"); + Flowable<Map<String, String>> flowable = Flowable.just(m1, m2); + + Flowable<String> m = flowable.map(new Function<Map<String, String>, String>() { + @Override + public String apply(Map<String, String> map) { + return map.get("firstName"); + } + }); + + m.subscribe(stringSubscriber); + + verify(stringSubscriber, never()).onError(any(Throwable.class)); + verify(stringSubscriber, times(1)).onNext("OneFirst"); + verify(stringSubscriber, times(1)).onNext("TwoFirst"); + verify(stringSubscriber, times(1)).onComplete(); + } + + @Test + public void mapMany() { + /* simulate a top-level async call which returns IDs */ + Flowable<Integer> ids = Flowable.just(1, 2); + + /* now simulate the behavior to take those IDs and perform nested async calls based on them */ + Flowable<String> m = ids.flatMap(new Function<Integer, Flowable<String>>() { + + @Override + public Flowable<String> apply(Integer id) { + /* simulate making a nested async call which creates another Flowable */ + Flowable<Map<String, String>> subFlowable = null; + if (id == 1) { + Map<String, String> m1 = getMap("One"); + Map<String, String> m2 = getMap("Two"); + subFlowable = Flowable.just(m1, m2); + } else { + Map<String, String> m3 = getMap("Three"); + Map<String, String> m4 = getMap("Four"); + subFlowable = Flowable.just(m3, m4); + } + + /* simulate kicking off the async call and performing a select on it to transform the data */ + return subFlowable.map(new Function<Map<String, String>, String>() { + @Override + public String apply(Map<String, String> map) { + return map.get("firstName"); + } + }); + } + + }); + m.subscribe(stringSubscriber); + + verify(stringSubscriber, never()).onError(any(Throwable.class)); + verify(stringSubscriber, times(1)).onNext("OneFirst"); + verify(stringSubscriber, times(1)).onNext("TwoFirst"); + verify(stringSubscriber, times(1)).onNext("ThreeFirst"); + verify(stringSubscriber, times(1)).onNext("FourFirst"); + verify(stringSubscriber, times(1)).onComplete(); + } + + @Test + public void mapMany2() { + Map<String, String> m1 = getMap("One"); + Map<String, String> m2 = getMap("Two"); + Flowable<Map<String, String>> flowable1 = Flowable.just(m1, m2); + + Map<String, String> m3 = getMap("Three"); + Map<String, String> m4 = getMap("Four"); + Flowable<Map<String, String>> flowable2 = Flowable.just(m3, m4); + + Flowable<Flowable<Map<String, String>>> f = Flowable.just(flowable1, flowable2); + + Flowable<String> m = f.flatMap(new Function<Flowable<Map<String, String>>, Flowable<String>>() { + + @Override + public Flowable<String> apply(Flowable<Map<String, String>> f) { + return f.map(new Function<Map<String, String>, String>() { + + @Override + public String apply(Map<String, String> map) { + return map.get("firstName"); + } + }); + } + + }); + m.subscribe(stringSubscriber); + + verify(stringSubscriber, never()).onError(any(Throwable.class)); + verify(stringSubscriber, times(1)).onNext("OneFirst"); + verify(stringSubscriber, times(1)).onNext("TwoFirst"); + verify(stringSubscriber, times(1)).onNext("ThreeFirst"); + verify(stringSubscriber, times(1)).onNext("FourFirst"); + verify(stringSubscriber, times(1)).onComplete(); + + } + + @Test + public void mapWithError() { + final List<Throwable> errors = new ArrayList<>(); + + Flowable<String> w = Flowable.just("one", "fail", "two", "three", "fail"); + Flowable<String> m = w.map(new Function<String, String>() { + @Override + public String apply(String s) { + if ("fail".equals(s)) { + throw new TestException("Forced Failure"); + } + return s; + } + }).doOnError(new Consumer<Throwable>() { + + @Override + public void accept(Throwable t1) { + errors.add(t1); + } + + }); + + m.subscribe(stringSubscriber); + verify(stringSubscriber, times(1)).onNext("one"); + verify(stringSubscriber, never()).onNext("two"); + verify(stringSubscriber, never()).onNext("three"); + verify(stringSubscriber, never()).onComplete(); + verify(stringSubscriber, times(1)).onError(any(TestException.class)); + + TestHelper.assertError(errors, 0, TestException.class, "Forced Failure"); + } + + @Test(expected = IllegalArgumentException.class) + public void mapWithIssue417() { + Flowable.just(1).observeOn(Schedulers.computation()) + .map(new Function<Integer, Integer>() { + @Override + public Integer apply(Integer arg0) { + throw new IllegalArgumentException("any error"); + } + }).blockingSingle(); + } + + @Test(expected = IllegalArgumentException.class) + public void mapWithErrorInFuncAndThreadPoolScheduler() throws InterruptedException { + // The error will throw in one of threads in the thread pool. + // If map does not handle it, the error will disappear. + // so map needs to handle the error by itself. + Flowable<String> m = Flowable.just("one") + .observeOn(Schedulers.computation()) + .map(new Function<String, String>() { + @Override + public String apply(String arg0) { + throw new IllegalArgumentException("any error"); + } + }); + + // block for response, expecting exception thrown + m.blockingLast(); + } + + /** + * While mapping over range(1,0).last() we expect NoSuchElementException since the sequence is empty. + */ + @Test + public void errorPassesThruMap() { + assertNull(Flowable.range(1, 0).lastElement().map(new Function<Integer, Integer>() { + + @Override + public Integer apply(Integer i) { + return i; + } + + }).blockingGet()); + } + + /** + * We expect IllegalStateException to pass thru map. + */ + @Test(expected = IllegalStateException.class) + public void errorPassesThruMap2() { + Flowable.error(new IllegalStateException()).map(new Function<Object, Object>() { + + @Override + public Object apply(Object i) { + return i; + } + + }).blockingSingle(); + } + + /** + * We expect an ArithmeticException exception here because last() emits a single value + * but then we divide by 0. + */ + @Test(expected = ArithmeticException.class) + public void mapWithErrorInFunc() { + Flowable.range(1, 1).lastElement().map(new Function<Integer, Integer>() { + + @Override + public Integer apply(Integer i) { + return i / 0; + } + + }).blockingGet(); + } + + private static Map<String, String> getMap(String prefix) { + Map<String, String> m = new HashMap<>(); + m.put("firstName", prefix + "First"); + m.put("lastName", prefix + "Last"); + return m; + } + + @Test + public void functionCrashUnsubscribes() { + + PublishProcessor<Integer> pp = PublishProcessor.create(); + + TestSubscriber<Integer> ts = new TestSubscriber<>(); + + pp.map(new Function<Integer, Integer>() { + @Override + public Integer apply(Integer v) { + throw new TestException(); + } + }).subscribe(ts); + + Assert.assertTrue("Not subscribed?", pp.hasSubscribers()); + + pp.onNext(1); + + Assert.assertFalse("Subscribed?", pp.hasSubscribers()); + + ts.assertError(TestException.class); + } + + @Test + public void mapFilter() { + Flowable.range(1, 2) + .map(new Function<Integer, Integer>() { + @Override + public Integer apply(Integer v) throws Exception { + return v + 1; + } + }) + .filter(new Predicate<Integer>() { + @Override + public boolean test(Integer v) throws Exception { + return true; + } + }) + .test() + .assertResult(2, 3); + } + + @Test + public void mapFilterMapperCrash() { + Flowable.range(1, 2) + .map(new Function<Integer, Integer>() { + @Override + public Integer apply(Integer v) throws Exception { + throw new TestException(); + } + }) + .filter(new Predicate<Integer>() { + @Override + public boolean test(Integer v) throws Exception { + return true; + } + }) + .test() + .assertFailure(TestException.class); + } + + @Test + public void mapFilterHidden() { + Flowable.range(1, 2).hide() + .map(new Function<Integer, Integer>() { + @Override + public Integer apply(Integer v) throws Exception { + return v + 1; + } + }) + .filter(new Predicate<Integer>() { + @Override + public boolean test(Integer v) throws Exception { + return true; + } + }) + .test() + .assertResult(2, 3); + } + + @Test + public void mapFilterFused() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<Integer>().setInitialFusionMode(QueueFuseable.ANY); + + Flowable.range(1, 2) + .map(new Function<Integer, Integer>() { + @Override + public Integer apply(Integer v) throws Exception { + return v + 1; + } + }) + .filter(new Predicate<Integer>() { + @Override + public boolean test(Integer v) throws Exception { + return true; + } + }) + .subscribe(ts); + + ts.assertFuseable() + .assertFusionMode(QueueFuseable.SYNC) + .assertResult(2, 3); + } + + @Test + public void mapFilterFusedHidden() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<Integer>().setInitialFusionMode(QueueFuseable.ANY); + + Flowable.range(1, 2).hide() + .map(new Function<Integer, Integer>() { + @Override + public Integer apply(Integer v) throws Exception { + return v + 1; + } + }) + .filter(new Predicate<Integer>() { + @Override + public boolean test(Integer v) throws Exception { + return true; + } + }) + .subscribe(ts); + + ts.assertFuseable() + .assertFusionMode(QueueFuseable.NONE) + .assertResult(2, 3); + } + + @Test + public void sourceIgnoresCancel() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + + try { + Flowable.fromPublisher(new Publisher<Integer>() { + @Override + public void subscribe(Subscriber<? super Integer> s) { + s.onSubscribe(new BooleanSubscription()); + s.onNext(1); + s.onNext(2); + s.onError(new IOException()); + s.onComplete(); + } + }) + .map(new Function<Integer, Object>() { + @Override + public Object apply(Integer v) throws Exception { + throw new TestException(); + } + }) + .test() + .assertFailure(TestException.class); + + TestHelper.assertUndeliverable(errors, 0, IOException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void mapFilterMapperCrashFused() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<Integer>().setInitialFusionMode(QueueFuseable.ANY); + + Flowable.range(1, 2).hide() + .map(new Function<Integer, Integer>() { + @Override + public Integer apply(Integer v) throws Exception { + throw new TestException(); + } + }) + .filter(new Predicate<Integer>() { + @Override + public boolean test(Integer v) throws Exception { + return true; + } + }) + .subscribe(ts); + + ts.assertFuseable() + .assertFusionMode(QueueFuseable.NONE) + .assertFailure(TestException.class); + } + + @Test + public void sourceIgnoresCancelFilter() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + + try { + Flowable.fromPublisher(new Publisher<Integer>() { + @Override + public void subscribe(Subscriber<? super Integer> s) { + s.onSubscribe(new BooleanSubscription()); + s.onNext(1); + s.onNext(2); + s.onError(new IOException()); + s.onComplete(); + } + }) + .map(new Function<Integer, Integer>() { + @Override + public Integer apply(Integer v) throws Exception { + throw new TestException(); + } + }) + .filter(new Predicate<Integer>() { + @Override + public boolean test(Integer v) throws Exception { + return true; + } + }) + .test() + .assertFailure(TestException.class); + + TestHelper.assertUndeliverable(errors, 0, IOException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void mapFilterFused2() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<Integer>().setInitialFusionMode(QueueFuseable.ANY); + + UnicastProcessor<Integer> up = UnicastProcessor.create(); + + up + .map(new Function<Integer, Integer>() { + @Override + public Integer apply(Integer v) throws Exception { + return v + 1; + } + }) + .filter(new Predicate<Integer>() { + @Override + public boolean test(Integer v) throws Exception { + return true; + } + }) + .subscribe(ts); + + up.onNext(1); + up.onNext(2); + up.onComplete(); + + ts.assertFuseable() + .assertFusionMode(QueueFuseable.ASYNC) + .assertResult(2, 3); + } + + @Test + public void sourceIgnoresCancelConditional() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + + try { + Flowable.fromPublisher(new Publisher<Integer>() { + @Override + public void subscribe(Subscriber<? super Integer> s) { + ConditionalSubscriber<? super Integer> cs = (ConditionalSubscriber<? super Integer>)s; + cs.onSubscribe(new BooleanSubscription()); + cs.tryOnNext(1); + cs.tryOnNext(2); + cs.onError(new IOException()); + cs.onComplete(); + } + }) + .map(new Function<Integer, Integer>() { + @Override + public Integer apply(Integer v) throws Exception { + throw new TestException(); + } + }) + .filter(new Predicate<Integer>() { + @Override + public boolean test(Integer v) throws Exception { + return true; + } + }) + .test() + .assertFailure(TestException.class); + + TestHelper.assertUndeliverable(errors, 0, IOException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void dispose() { + TestHelper.checkDisposed(Flowable.range(1, 5).map(Functions.identity())); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeFlowable(new Function<Flowable<Object>, Flowable<Object>>() { + @Override + public Flowable<Object> apply(Flowable<Object> f) throws Exception { + return f.map(Functions.identity()); + } + }); + } + + @Test + public void fusedSync() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<Integer>().setInitialFusionMode(QueueFuseable.ANY); + + Flowable.range(1, 5) + .map(Functions.<Integer>identity()) + .subscribe(ts); + + ts.assertFusionMode(QueueFuseable.SYNC) + .assertResult(1, 2, 3, 4, 5); + } + + @Test + public void fusedAsync() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<Integer>().setInitialFusionMode(QueueFuseable.ANY); + + UnicastProcessor<Integer> up = UnicastProcessor.create(); + + up + .map(Functions.<Integer>identity()) + .subscribe(ts); + + TestHelper.emit(up, 1, 2, 3, 4, 5); + + ts.assertFusionMode(QueueFuseable.ASYNC) + .assertResult(1, 2, 3, 4, 5); + } + + @Test + public void fusedReject() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<Integer>().setInitialFusionMode(QueueFuseable.ANY | QueueFuseable.BOUNDARY); + + Flowable.range(1, 5) + .map(Functions.<Integer>identity()) + .subscribe(ts); + + ts.assertFusionMode(QueueFuseable.NONE) + .assertResult(1, 2, 3, 4, 5); + } + + @Test + public void badSource() { + TestHelper.checkBadSourceFlowable(new Function<Flowable<Object>, Object>() { + @Override + public Object apply(Flowable<Object> f) throws Exception { + return f.map(Functions.identity()); + } + }, false, 1, 1, 1); + } + + @Test + public void conditionalFusionNoNPE() { + TestSubscriberEx<Object> ts = new TestSubscriberEx<>() + .setInitialFusionMode(QueueFuseable.ANY); + + Flowable.empty() + .observeOn(ImmediateThinScheduler.INSTANCE) + .filter(v -> true) + .map(v -> v) + .filter(v -> true) + .subscribe(ts) + ; + + ts.assertResult(); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableMaterializeTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableMaterializeTest.java new file mode 100644 index 0000000000..0f9c3df225 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableMaterializeTest.java @@ -0,0 +1,314 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import static org.junit.Assert.*; + +import java.util.*; +import java.util.concurrent.ExecutionException; + +import org.junit.Test; +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.subscriptions.BooleanSubscription; +import io.reactivex.rxjava3.schedulers.Schedulers; +import io.reactivex.rxjava3.subscribers.*; +import io.reactivex.rxjava3.testsupport.*; + +public class FlowableMaterializeTest extends RxJavaTest { + + @Test + public void materialize1() { + // null will cause onError to be triggered before "three" can be + // returned + final TestAsyncErrorObservable o1 = new TestAsyncErrorObservable("one", "two", null, + "three"); + + TestNotificationSubscriber observer = new TestNotificationSubscriber(); + Flowable<Notification<String>> m = Flowable.unsafeCreate(o1).materialize(); + m.subscribe(observer); + + try { + o1.t.join(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + + assertFalse(observer.onError); + assertTrue(observer.onComplete); + assertEquals(3, observer.notifications.size()); + + assertTrue(observer.notifications.get(0).isOnNext()); + assertEquals("one", observer.notifications.get(0).getValue()); + + assertTrue(observer.notifications.get(1).isOnNext()); + assertEquals("two", observer.notifications.get(1).getValue()); + + assertTrue(observer.notifications.get(2).isOnError()); + assertEquals(NullPointerException.class, observer.notifications.get(2).getError().getClass()); + } + + @Test + public void materialize2() { + final TestAsyncErrorObservable o1 = new TestAsyncErrorObservable("one", "two", "three"); + + TestNotificationSubscriber subscriber = new TestNotificationSubscriber(); + Flowable<Notification<String>> m = Flowable.unsafeCreate(o1).materialize(); + m.subscribe(subscriber); + + try { + o1.t.join(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + + assertFalse(subscriber.onError); + assertTrue(subscriber.onComplete); + assertEquals(4, subscriber.notifications.size()); + assertTrue(subscriber.notifications.get(0).isOnNext()); + assertEquals("one", subscriber.notifications.get(0).getValue()); + + assertTrue(subscriber.notifications.get(1).isOnNext()); + assertEquals("two", subscriber.notifications.get(1).getValue()); + + assertTrue(subscriber.notifications.get(2).isOnNext()); + assertEquals("three", subscriber.notifications.get(2).getValue()); + + assertTrue(subscriber.notifications.get(3).isOnComplete()); + } + + @Test + public void multipleSubscribes() throws InterruptedException, ExecutionException { + final TestAsyncErrorObservable o = new TestAsyncErrorObservable("one", "two", null, "three"); + + Flowable<Notification<String>> m = Flowable.unsafeCreate(o).materialize(); + + assertEquals(3, m.toList().toFuture().get().size()); + assertEquals(3, m.toList().toFuture().get().size()); + } + + @Test + public void backpressureOnEmptyStream() { + TestSubscriber<Notification<Integer>> ts = new TestSubscriber<>(0L); + Flowable.<Integer> empty().materialize().subscribe(ts); + ts.assertNoValues(); + ts.request(1); + ts.assertValueCount(1); + assertTrue(ts.values().get(0).isOnComplete()); + ts.assertComplete(); + } + + @Test + public void backpressureNoError() { + TestSubscriber<Notification<Integer>> ts = new TestSubscriber<>(0L); + Flowable.just(1, 2, 3).materialize().subscribe(ts); + ts.assertNoValues(); + ts.request(1); + ts.assertValueCount(1); + ts.request(2); + ts.assertValueCount(3); + ts.request(1); + ts.assertValueCount(4); + ts.assertComplete(); + } + + @Test + public void backpressureNoErrorAsync() throws InterruptedException { + TestSubscriber<Notification<Integer>> ts = new TestSubscriber<>(0L); + Flowable.just(1, 2, 3) + .materialize() + .subscribeOn(Schedulers.computation()) + .subscribe(ts); + Thread.sleep(100); + ts.assertNoValues(); + ts.request(1); + Thread.sleep(100); + ts.assertValueCount(1); + ts.request(2); + Thread.sleep(100); + ts.assertValueCount(3); + ts.request(1); + Thread.sleep(100); + ts.assertValueCount(4); + ts.assertComplete(); + } + + @Test + public void backpressureWithError() { + TestSubscriber<Notification<Integer>> ts = new TestSubscriber<>(0L); + Flowable.<Integer> error(new IllegalArgumentException()).materialize().subscribe(ts); + ts.assertNoValues(); + ts.request(1); + ts.assertValueCount(1); + ts.assertComplete(); + } + + @Test + public void backpressureWithEmissionThenError() { + TestSubscriber<Notification<Integer>> ts = new TestSubscriber<>(0L); + IllegalArgumentException ex = new IllegalArgumentException(); + Flowable.fromIterable(Arrays.asList(1)).concatWith(Flowable.<Integer> error(ex)).materialize() + .subscribe(ts); + ts.assertNoValues(); + ts.request(1); + ts.assertValueCount(1); + assertTrue(ts.values().get(0).isOnNext()); + ts.request(1); + ts.assertValueCount(2); + assertTrue(ts.values().get(1).isOnError()); + assertEquals(ex, ts.values().get(1).getError()); + ts.assertComplete(); + } + + @Test + public void withCompletionCausingError() { + TestSubscriberEx<Notification<Integer>> ts = new TestSubscriberEx<>(); + final RuntimeException ex = new RuntimeException("boo"); + Flowable.<Integer>empty().materialize().doOnNext(new Consumer<Object>() { + @Override + public void accept(Object t) { + throw ex; + } + }).subscribe(ts); + ts.assertError(ex); + ts.assertNoValues(); + ts.assertTerminated(); + } + + @Test + public void unsubscribeJustBeforeCompletionNotificationShouldPreventThatNotificationArriving() { + TestSubscriber<Notification<Integer>> ts = new TestSubscriber<>(0L); + + Flowable.<Integer>empty().materialize() + .subscribe(ts); + ts.assertNoValues(); + ts.cancel(); + ts.request(1); + ts.assertNoValues(); + } + + private static class TestNotificationSubscriber extends DefaultSubscriber<Notification<String>> { + + boolean onComplete; + boolean onError; + List<Notification<String>> notifications = new Vector<>(); + + @Override + public void onComplete() { + this.onComplete = true; + } + + @Override + public void onError(Throwable e) { + this.onError = true; + } + + @Override + public void onNext(Notification<String> value) { + this.notifications.add(value); + } + + } + + private static class TestAsyncErrorObservable implements Publisher<String> { + + String[] valuesToReturn; + + TestAsyncErrorObservable(String... values) { + valuesToReturn = values; + } + + volatile Thread t; + + @Override + public void subscribe(final Subscriber<? super String> subscriber) { + subscriber.onSubscribe(new BooleanSubscription()); + t = new Thread(new Runnable() { + + @Override + public void run() { + for (String s : valuesToReturn) { + if (s == null) { + System.out.println("throwing exception"); + try { + Thread.sleep(100); + } catch (Throwable e) { + + } + subscriber.onError(new NullPointerException()); + return; + } else { + subscriber.onNext(s); + } + } + System.out.println("subscription complete"); + subscriber.onComplete(); + } + + }); + t.start(); + } + } + + @Test + public void backpressure() { + TestSubscriber<Notification<Integer>> ts = Flowable.range(1, 5).materialize().test(0); + + ts.assertEmpty(); + + ts.request(5); + + ts.assertValueCount(5) + .assertNoErrors() + .assertNotComplete(); + + ts.request(1); + + ts.assertValueCount(6) + .assertNoErrors() + .assertComplete(); + } + + @Test + public void dispose() { + TestHelper.checkDisposed(Flowable.just(1).materialize()); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeFlowable(new Function<Flowable<Object>, Flowable<Notification<Object>>>() { + @Override + public Flowable<Notification<Object>> apply(Flowable<Object> f) throws Exception { + return f.materialize(); + } + }); + } + + @Test + public void badSource() { + TestHelper.checkBadSourceFlowable(new Function<Flowable<Object>, Object>() { + @Override + public Object apply(Flowable<Object> f) throws Exception { + return f.materialize(); + } + }, false, null, null, Notification.createOnComplete()); + } + + @Test + public void badRequest() { + TestHelper.assertBadRequestReported(Flowable.just(1).materialize()); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableMergeDelayErrorTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableMergeDelayErrorTest.java new file mode 100644 index 0000000000..1959921168 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableMergeDelayErrorTest.java @@ -0,0 +1,723 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import java.util.*; +import java.util.concurrent.*; + +import org.junit.*; +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.LongConsumer; +import io.reactivex.rxjava3.internal.subscriptions.BooleanSubscription; +import io.reactivex.rxjava3.processors.PublishProcessor; +import io.reactivex.rxjava3.subscribers.*; +import io.reactivex.rxjava3.testsupport.*; + +public class FlowableMergeDelayErrorTest extends RxJavaTest { + + Subscriber<String> stringSubscriber; + + @Before + public void before() { + stringSubscriber = TestHelper.mockSubscriber(); + } + + @Test + public void errorDelayed1() { + final Flowable<String> f1 = Flowable.unsafeCreate(new TestErrorFlowable("four", null, "six")); // we expect to lose "six" from the source (and it should never be sent by the source since onError was called + final Flowable<String> f2 = Flowable.unsafeCreate(new TestErrorFlowable("one", "two", "three")); + + Flowable<String> m = Flowable.mergeDelayError(f1, f2); + m.subscribe(stringSubscriber); + + verify(stringSubscriber, times(1)).onError(any(NullPointerException.class)); + verify(stringSubscriber, never()).onComplete(); + verify(stringSubscriber, times(1)).onNext("one"); + verify(stringSubscriber, times(1)).onNext("two"); + verify(stringSubscriber, times(1)).onNext("three"); + verify(stringSubscriber, times(1)).onNext("four"); + verify(stringSubscriber, times(0)).onNext("five"); + // despite not expecting it ... we don't do anything to prevent it if the source Flowable keeps sending after onError + // inner Flowable errors are considered terminal for that source +// verify(stringSubscriber, times(1)).onNext("six"); + // inner Flowable errors are considered terminal for that source + } + + @Test + public void errorDelayed2() { + final Flowable<String> f1 = Flowable.unsafeCreate(new TestErrorFlowable("one", "two", "three")); + final Flowable<String> f2 = Flowable.unsafeCreate(new TestErrorFlowable("four", null, "six")); // we expect to lose "six" from the source (and it should never be sent by the source since onError was called + final Flowable<String> f3 = Flowable.unsafeCreate(new TestErrorFlowable("seven", "eight", null)); + final Flowable<String> f4 = Flowable.unsafeCreate(new TestErrorFlowable("nine")); + + Flowable<String> m = Flowable.mergeDelayError(f1, f2, f3, f4); + m.subscribe(stringSubscriber); + + verify(stringSubscriber, times(1)).onError(any(CompositeException.class)); + verify(stringSubscriber, never()).onComplete(); + verify(stringSubscriber, times(1)).onNext("one"); + verify(stringSubscriber, times(1)).onNext("two"); + verify(stringSubscriber, times(1)).onNext("three"); + verify(stringSubscriber, times(1)).onNext("four"); + verify(stringSubscriber, times(0)).onNext("five"); + // despite not expecting it ... we don't do anything to prevent it if the source Flowable keeps sending after onError + // inner Flowable errors are considered terminal for that source +// verify(stringSubscriber, times(1)).onNext("six"); + verify(stringSubscriber, times(1)).onNext("seven"); + verify(stringSubscriber, times(1)).onNext("eight"); + verify(stringSubscriber, times(1)).onNext("nine"); + } + + @Test + public void errorDelayed3() { + final Flowable<String> f1 = Flowable.unsafeCreate(new TestErrorFlowable("one", "two", "three")); + final Flowable<String> f2 = Flowable.unsafeCreate(new TestErrorFlowable("four", "five", "six")); + final Flowable<String> f3 = Flowable.unsafeCreate(new TestErrorFlowable("seven", "eight", null)); + final Flowable<String> f4 = Flowable.unsafeCreate(new TestErrorFlowable("nine")); + + Flowable<String> m = Flowable.mergeDelayError(f1, f2, f3, f4); + m.subscribe(stringSubscriber); + + verify(stringSubscriber, times(1)).onError(any(NullPointerException.class)); + verify(stringSubscriber, never()).onComplete(); + verify(stringSubscriber, times(1)).onNext("one"); + verify(stringSubscriber, times(1)).onNext("two"); + verify(stringSubscriber, times(1)).onNext("three"); + verify(stringSubscriber, times(1)).onNext("four"); + verify(stringSubscriber, times(1)).onNext("five"); + verify(stringSubscriber, times(1)).onNext("six"); + verify(stringSubscriber, times(1)).onNext("seven"); + verify(stringSubscriber, times(1)).onNext("eight"); + verify(stringSubscriber, times(1)).onNext("nine"); + } + + @Test + public void errorDelayed4() { + final Flowable<String> f1 = Flowable.unsafeCreate(new TestErrorFlowable("one", "two", "three")); + final Flowable<String> f2 = Flowable.unsafeCreate(new TestErrorFlowable("four", "five", "six")); + final Flowable<String> f3 = Flowable.unsafeCreate(new TestErrorFlowable("seven", "eight")); + final Flowable<String> f4 = Flowable.unsafeCreate(new TestErrorFlowable("nine", null)); + + Flowable<String> m = Flowable.mergeDelayError(f1, f2, f3, f4); + m.subscribe(stringSubscriber); + + verify(stringSubscriber, times(1)).onError(any(NullPointerException.class)); + verify(stringSubscriber, never()).onComplete(); + verify(stringSubscriber, times(1)).onNext("one"); + verify(stringSubscriber, times(1)).onNext("two"); + verify(stringSubscriber, times(1)).onNext("three"); + verify(stringSubscriber, times(1)).onNext("four"); + verify(stringSubscriber, times(1)).onNext("five"); + verify(stringSubscriber, times(1)).onNext("six"); + verify(stringSubscriber, times(1)).onNext("seven"); + verify(stringSubscriber, times(1)).onNext("eight"); + verify(stringSubscriber, times(1)).onNext("nine"); + } + + @Test + public void errorDelayed4WithThreading() { + final TestAsyncErrorFlowable f1 = new TestAsyncErrorFlowable("one", "two", "three"); + final TestAsyncErrorFlowable f2 = new TestAsyncErrorFlowable("four", "five", "six"); + final TestAsyncErrorFlowable f3 = new TestAsyncErrorFlowable("seven", "eight"); + // throw the error at the very end so no onComplete will be called after it + final TestAsyncErrorFlowable f4 = new TestAsyncErrorFlowable("nine", null); + + Flowable<String> m = Flowable.mergeDelayError(Flowable.unsafeCreate(f1), Flowable.unsafeCreate(f2), Flowable.unsafeCreate(f3), Flowable.unsafeCreate(f4)); + m.subscribe(stringSubscriber); + + try { + f1.t.join(); + f2.t.join(); + f3.t.join(); + f4.t.join(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + + verify(stringSubscriber, times(1)).onNext("one"); + verify(stringSubscriber, times(1)).onNext("two"); + verify(stringSubscriber, times(1)).onNext("three"); + verify(stringSubscriber, times(1)).onNext("four"); + verify(stringSubscriber, times(1)).onNext("five"); + verify(stringSubscriber, times(1)).onNext("six"); + verify(stringSubscriber, times(1)).onNext("seven"); + verify(stringSubscriber, times(1)).onNext("eight"); + verify(stringSubscriber, times(1)).onNext("nine"); + verify(stringSubscriber, times(1)).onError(any(NullPointerException.class)); + verify(stringSubscriber, never()).onComplete(); + } + + @Test + public void compositeErrorDelayed1() { + final Flowable<String> f1 = Flowable.unsafeCreate(new TestErrorFlowable("four", null, "six")); // we expect to lose "six" from the source (and it should never be sent by the source since onError was called + final Flowable<String> f2 = Flowable.unsafeCreate(new TestErrorFlowable("one", "two", null)); + + Flowable<String> m = Flowable.mergeDelayError(f1, f2); + m.subscribe(stringSubscriber); + + verify(stringSubscriber, times(1)).onError(any(Throwable.class)); + verify(stringSubscriber, never()).onComplete(); + verify(stringSubscriber, times(1)).onNext("one"); + verify(stringSubscriber, times(1)).onNext("two"); + verify(stringSubscriber, times(0)).onNext("three"); + verify(stringSubscriber, times(1)).onNext("four"); + verify(stringSubscriber, times(0)).onNext("five"); + // despite not expecting it ... we don't do anything to prevent it if the source Flowable keeps sending after onError + // inner Flowable errors are considered terminal for that source +// verify(stringSubscriber, times(1)).onNext("six"); + } + + @Test + public void compositeErrorDelayed2() { + final Flowable<String> f1 = Flowable.unsafeCreate(new TestErrorFlowable("four", null, "six")); // we expect to lose "six" from the source (and it should never be sent by the source since onError was called + final Flowable<String> f2 = Flowable.unsafeCreate(new TestErrorFlowable("one", "two", null)); + + Flowable<String> m = Flowable.mergeDelayError(f1, f2); + CaptureObserver w = new CaptureObserver(); + m.subscribe(w); + + assertNotNull(w.e); + + int size = ((CompositeException)w.e).size(); + if (size != 2) { + w.e.printStackTrace(); + } + assertEquals(2, size); + +// if (w.e instanceof CompositeException) { +// assertEquals(2, ((CompositeException) w.e).getExceptions().size()); +// w.e.printStackTrace(); +// } else { +// fail("Expecting CompositeException"); +// } + + } + + /** + * The unit tests below are from OperationMerge and should ensure the normal merge functionality is correct. + */ + + @Test + public void mergeFlowableOfFlowables() { + final Flowable<String> f1 = Flowable.unsafeCreate(new TestSynchronousFlowable()); + final Flowable<String> f2 = Flowable.unsafeCreate(new TestSynchronousFlowable()); + + Flowable<Flowable<String>> flowableOfFlowables = Flowable.unsafeCreate(new Publisher<Flowable<String>>() { + + @Override + public void subscribe(Subscriber<? super Flowable<String>> subscriber) { + subscriber.onSubscribe(new BooleanSubscription()); + // simulate what would happen in a Flowable + subscriber.onNext(f1); + subscriber.onNext(f2); + subscriber.onComplete(); + } + + }); + Flowable<String> m = Flowable.mergeDelayError(flowableOfFlowables); + m.subscribe(stringSubscriber); + + verify(stringSubscriber, never()).onError(any(Throwable.class)); + verify(stringSubscriber, times(1)).onComplete(); + verify(stringSubscriber, times(2)).onNext("hello"); + } + + @Test + public void mergeArray() { + final Flowable<String> f1 = Flowable.unsafeCreate(new TestSynchronousFlowable()); + final Flowable<String> f2 = Flowable.unsafeCreate(new TestSynchronousFlowable()); + + Flowable<String> m = Flowable.mergeDelayError(f1, f2); + m.subscribe(stringSubscriber); + + verify(stringSubscriber, never()).onError(any(Throwable.class)); + verify(stringSubscriber, times(2)).onNext("hello"); + verify(stringSubscriber, times(1)).onComplete(); + } + + @Test + public void mergeList() { + final Flowable<String> f1 = Flowable.unsafeCreate(new TestSynchronousFlowable()); + final Flowable<String> f2 = Flowable.unsafeCreate(new TestSynchronousFlowable()); + List<Flowable<String>> listOfFlowables = new ArrayList<>(); + listOfFlowables.add(f1); + listOfFlowables.add(f2); + + Flowable<String> m = Flowable.mergeDelayError(Flowable.fromIterable(listOfFlowables)); + m.subscribe(stringSubscriber); + + verify(stringSubscriber, never()).onError(any(Throwable.class)); + verify(stringSubscriber, times(1)).onComplete(); + verify(stringSubscriber, times(2)).onNext("hello"); + } + + @Test + public void mergeArrayWithThreading() { + final TestASynchronousFlowable f1 = new TestASynchronousFlowable(); + final TestASynchronousFlowable f2 = new TestASynchronousFlowable(); + + Flowable<String> m = Flowable.mergeDelayError(Flowable.unsafeCreate(f1), Flowable.unsafeCreate(f2)); + m.subscribe(stringSubscriber); + + try { + f1.t.join(); + f2.t.join(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + + verify(stringSubscriber, never()).onError(any(Throwable.class)); + verify(stringSubscriber, times(2)).onNext("hello"); + verify(stringSubscriber, times(1)).onComplete(); + } + + @Test + public void synchronousError() { + final Flowable<Flowable<String>> f1 = Flowable.error(new RuntimeException("unit test")); + + final CountDownLatch latch = new CountDownLatch(1); + Flowable.mergeDelayError(f1).subscribe(new DefaultSubscriber<String>() { + @Override + public void onComplete() { + fail("Expected onError path"); + } + + @Override + public void onError(Throwable e) { + latch.countDown(); + } + + @Override + public void onNext(String s) { + fail("Expected onError path"); + } + }); + + try { + latch.await(); + } catch (InterruptedException ex) { + fail("interrupted"); + } + } + + private static class TestSynchronousFlowable implements Publisher<String> { + + @Override + public void subscribe(Subscriber<? super String> subscriber) { + subscriber.onSubscribe(new BooleanSubscription()); + subscriber.onNext("hello"); + subscriber.onComplete(); + } + } + + private static class TestASynchronousFlowable implements Publisher<String> { + Thread t; + + @Override + public void subscribe(final Subscriber<? super String> subscriber) { + subscriber.onSubscribe(new BooleanSubscription()); + t = new Thread(new Runnable() { + + @Override + public void run() { + subscriber.onNext("hello"); + subscriber.onComplete(); + } + + }); + t.start(); + } + } + + private static class TestErrorFlowable implements Publisher<String> { + + String[] valuesToReturn; + + TestErrorFlowable(String... values) { + valuesToReturn = values; + } + + @Override + public void subscribe(Subscriber<? super String> subscriber) { + subscriber.onSubscribe(new BooleanSubscription()); + boolean errorThrown = false; + for (String s : valuesToReturn) { + if (s == null) { + System.out.println("throwing exception"); + subscriber.onError(new NullPointerException()); + errorThrown = true; + // purposefully not returning here so it will continue calling onNext + // so that we also test that we handle bad sequences like this + } else { + subscriber.onNext(s); + } + } + if (!errorThrown) { + subscriber.onComplete(); + } + } + } + + private static class TestAsyncErrorFlowable implements Publisher<String> { + + String[] valuesToReturn; + + TestAsyncErrorFlowable(String... values) { + valuesToReturn = values; + } + + Thread t; + + @Override + public void subscribe(final Subscriber<? super String> subscriber) { + subscriber.onSubscribe(new BooleanSubscription()); + t = new Thread(new Runnable() { + + @Override + public void run() { + for (String s : valuesToReturn) { + if (s == null) { + System.out.println("throwing exception"); + try { + Thread.sleep(100); + } catch (Throwable e) { + + } + subscriber.onError(new NullPointerException()); + return; + } else { + subscriber.onNext(s); + } + } + System.out.println("subscription complete"); + subscriber.onComplete(); + } + + }); + t.start(); + } + } + + private static class CaptureObserver extends DefaultSubscriber<String> { + volatile Throwable e; + + @Override + public void onComplete() { + + } + + @Override + public void onError(Throwable e) { + this.e = e; + } + + @Override + public void onNext(String args) { + + } + + } + + @Test + public void errorInParentFlowable() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(); + Flowable.mergeDelayError( + Flowable.just(Flowable.just(1), Flowable.just(2)) + .startWithItem(Flowable.<Integer> error(new RuntimeException())) + ).subscribe(ts); + ts.awaitDone(5, TimeUnit.SECONDS); + ts.assertTerminated(); + ts.assertValues(1, 2); + assertEquals(1, ts.errors().size()); + + } + + @Test + public void errorInParentFlowableDelayed() throws Exception { + for (int i = 0; i < 50; i++) { + final TestASynchronous1sDelayedFlowable f1 = new TestASynchronous1sDelayedFlowable(); + final TestASynchronous1sDelayedFlowable f2 = new TestASynchronous1sDelayedFlowable(); + Flowable<Flowable<String>> parentFlowable = Flowable.unsafeCreate(new Publisher<Flowable<String>>() { + @Override + public void subscribe(Subscriber<? super Flowable<String>> op) { + op.onSubscribe(new BooleanSubscription()); + op.onNext(Flowable.unsafeCreate(f1)); + op.onNext(Flowable.unsafeCreate(f2)); + op.onError(new NullPointerException("throwing exception in parent")); + } + }); + + stringSubscriber = TestHelper.mockSubscriber(); + + TestSubscriberEx<String> ts = new TestSubscriberEx<>(stringSubscriber); + Flowable<String> m = Flowable.mergeDelayError(parentFlowable); + m.subscribe(ts); + System.out.println("testErrorInParentFlowableDelayed | " + i); + ts.awaitDone(2000, TimeUnit.MILLISECONDS); + ts.assertTerminated(); + + verify(stringSubscriber, times(2)).onNext("hello"); + verify(stringSubscriber, times(1)).onError(any(NullPointerException.class)); + verify(stringSubscriber, never()).onComplete(); + } + } + + private static class TestASynchronous1sDelayedFlowable implements Publisher<String> { + Thread t; + + @Override + public void subscribe(final Subscriber<? super String> subscriber) { + subscriber.onSubscribe(new BooleanSubscription()); + t = new Thread(new Runnable() { + + @Override + public void run() { + try { + Thread.sleep(100); + } catch (InterruptedException e) { + subscriber.onError(e); + } + subscriber.onNext("hello"); + subscriber.onComplete(); + } + + }); + t.start(); + } + } + + @Test + public void delayErrorMaxConcurrent() { + final List<Long> requests = new ArrayList<>(); + Flowable<Integer> source = Flowable.mergeDelayError(Flowable.just( + Flowable.just(1).hide(), + Flowable.<Integer>error(new TestException())) + .doOnRequest(new LongConsumer() { + @Override + public void accept(long t1) { + requests.add(t1); + } + }), 1); + + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(); + + source.subscribe(ts); + + ts.assertValue(1); + ts.assertTerminated(); + ts.assertError(TestException.class); + assertEquals(Arrays.asList(1L, 1L, 1L), requests); + } + + // This is pretty much a clone of testMergeList but with the overloaded MergeDelayError for Iterables + @Test + public void mergeIterable() { + final Flowable<String> f1 = Flowable.unsafeCreate(new TestSynchronousFlowable()); + final Flowable<String> f2 = Flowable.unsafeCreate(new TestSynchronousFlowable()); + List<Flowable<String>> listOfFlowables = new ArrayList<>(); + listOfFlowables.add(f1); + listOfFlowables.add(f2); + + Flowable<String> m = Flowable.mergeDelayError(listOfFlowables); + m.subscribe(stringSubscriber); + + verify(stringSubscriber, never()).onError(any(Throwable.class)); + verify(stringSubscriber, times(1)).onComplete(); + verify(stringSubscriber, times(2)).onNext("hello"); + } + + @Test + public void iterableMaxConcurrent() { + TestSubscriber<Integer> ts = TestSubscriber.create(); + + PublishProcessor<Integer> pp1 = PublishProcessor.create(); + PublishProcessor<Integer> pp2 = PublishProcessor.create(); + + Flowable.mergeDelayError(Arrays.asList(pp1, pp2), 1).subscribe(ts); + + assertTrue("ps1 has no subscribers?!", pp1.hasSubscribers()); + assertFalse("ps2 has subscribers?!", pp2.hasSubscribers()); + + pp1.onNext(1); + pp1.onComplete(); + + assertFalse("ps1 has subscribers?!", pp1.hasSubscribers()); + assertTrue("ps2 has no subscribers?!", pp2.hasSubscribers()); + + pp2.onNext(2); + pp2.onComplete(); + + ts.assertValues(1, 2); + ts.assertNoErrors(); + ts.assertComplete(); + } + + @Test + public void iterableMaxConcurrentError() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(); + + PublishProcessor<Integer> pp1 = PublishProcessor.create(); + PublishProcessor<Integer> pp2 = PublishProcessor.create(); + + Flowable.mergeDelayError(Arrays.asList(pp1, pp2), 1).subscribe(ts); + + assertTrue("ps1 has no subscribers?!", pp1.hasSubscribers()); + assertFalse("ps2 has subscribers?!", pp2.hasSubscribers()); + + pp1.onNext(1); + pp1.onError(new TestException()); + + assertFalse("ps1 has subscribers?!", pp1.hasSubscribers()); + assertTrue("ps2 has no subscribers?!", pp2.hasSubscribers()); + + pp2.onNext(2); + pp2.onError(new TestException()); + + ts.assertValues(1, 2); + ts.assertError(CompositeException.class); + ts.assertNotComplete(); + + CompositeException ce = (CompositeException)ts.errors().get(0); + + assertEquals(2, ce.getExceptions().size()); + } + + static <T> Flowable<T> withError(Flowable<T> source) { + return source.concatWith(Flowable.<T>error(new TestException())); + } + + @Test + public void array() { + for (int i = 1; i < 100; i++) { + + @SuppressWarnings("unchecked") + Flowable<Integer>[] sources = new Flowable[i]; + Arrays.fill(sources, Flowable.just(1)); + Integer[] expected = new Integer[i]; + for (int j = 0; j < i; j++) { + expected[j] = 1; + } + + Flowable.mergeArrayDelayError(sources) + .test() + .assertResult(expected); + } + } + + @Test + public void mergeArrayDelayError() { + Flowable.mergeArrayDelayError(Flowable.just(1), Flowable.just(2)) + .test() + .assertResult(1, 2); + } + + @Test + public void mergeIterableDelayErrorWithError() { + Flowable.mergeDelayError( + Arrays.asList(Flowable.just(1).concatWith(Flowable.<Integer>error(new TestException())), + Flowable.just(2))) + .test() + .assertFailure(TestException.class, 1, 2); + } + + @Test + public void mergeDelayError() { + Flowable.mergeDelayError( + Flowable.just(Flowable.just(1), + Flowable.just(2))) + .test() + .assertResult(1, 2); + } + + @Test + public void mergeDelayErrorWithError() { + Flowable.mergeDelayError( + Flowable.just(Flowable.just(1).concatWith(Flowable.<Integer>error(new TestException())), + Flowable.just(2))) + .test() + .assertFailure(TestException.class, 1, 2); + } + + @Test + public void mergeDelayErrorMaxConcurrency() { + Flowable.mergeDelayError( + Flowable.just(Flowable.just(1), + Flowable.just(2)), 1) + .test() + .assertResult(1, 2); + } + + @Test + public void mergeDelayErrorWithErrorMaxConcurrency() { + Flowable.mergeDelayError( + Flowable.just(Flowable.just(1).concatWith(Flowable.<Integer>error(new TestException())), + Flowable.just(2)), 1) + .test() + .assertFailure(TestException.class, 1, 2); + } + + @Test + public void mergeIterableDelayErrorMaxConcurrency() { + Flowable.mergeDelayError( + Arrays.asList(Flowable.just(1), + Flowable.just(2)), 1) + .test() + .assertResult(1, 2); + } + + @Test + public void mergeIterableDelayErrorWithErrorMaxConcurrency() { + Flowable.mergeDelayError( + Arrays.asList(Flowable.just(1).concatWith(Flowable.<Integer>error(new TestException())), + Flowable.just(2)), 1) + .test() + .assertFailure(TestException.class, 1, 2); + } + + @Test + public void mergeDelayError3() { + Flowable.mergeDelayError( + Flowable.just(1), + Flowable.just(2), + Flowable.just(3) + ) + .test() + .assertResult(1, 2, 3); + } + + @Test + public void mergeDelayError3WithError() { + Flowable.mergeDelayError( + Flowable.just(1), + Flowable.just(2).concatWith(Flowable.<Integer>error(new TestException())), + Flowable.just(3) + ) + .test() + .assertFailure(TestException.class, 1, 2, 3); + } + + @Test + public void mergeIterableDelayError() { + Flowable.mergeDelayError(Arrays.asList(Flowable.just(1), Flowable.just(2))) + .test() + .assertResult(1, 2); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableMergeMaxConcurrentTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableMergeMaxConcurrentTest.java new file mode 100644 index 0000000000..25b9cd825a --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableMergeMaxConcurrentTest.java @@ -0,0 +1,301 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import static org.junit.Assert.*; + +import java.util.*; +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.Test; +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.internal.schedulers.IoScheduler; +import io.reactivex.rxjava3.internal.subscriptions.BooleanSubscription; +import io.reactivex.rxjava3.schedulers.Schedulers; +import io.reactivex.rxjava3.subscribers.TestSubscriber; +import io.reactivex.rxjava3.testsupport.TestSubscriberEx; + +public class FlowableMergeMaxConcurrentTest extends RxJavaTest { + + @Test + public void whenMaxConcurrentIsOne() { + for (int i = 0; i < 100; i++) { + List<Flowable<String>> os = new ArrayList<>(); + os.add(Flowable.just("one", "two", "three", "four", "five").subscribeOn(Schedulers.newThread())); + os.add(Flowable.just("one", "two", "three", "four", "five").subscribeOn(Schedulers.newThread())); + os.add(Flowable.just("one", "two", "three", "four", "five").subscribeOn(Schedulers.newThread())); + + List<String> expected = Arrays.asList("one", "two", "three", "four", "five", "one", "two", "three", "four", "five", "one", "two", "three", "four", "five"); + Iterator<String> iter = Flowable.merge(os, 1).blockingIterable().iterator(); + List<String> actual = new ArrayList<>(); + while (iter.hasNext()) { + actual.add(iter.next()); + } + assertEquals(expected, actual); + } + } + + @Test + public void maxConcurrent() { + for (int times = 0; times < 100; times++) { + int observableCount = 100; + // Test maxConcurrent from 2 to 12 + int maxConcurrent = 2 + (times % 10); + AtomicInteger subscriptionCount = new AtomicInteger(0); + + List<Flowable<String>> os = new ArrayList<>(); + List<SubscriptionCheckObservable> scos = new ArrayList<>(); + for (int i = 0; i < observableCount; i++) { + SubscriptionCheckObservable sco = new SubscriptionCheckObservable(subscriptionCount, maxConcurrent); + scos.add(sco); + os.add(Flowable.unsafeCreate(sco)); + } + + Iterator<String> iter = Flowable.merge(os, maxConcurrent).blockingIterable().iterator(); + List<String> actual = new ArrayList<>(); + while (iter.hasNext()) { + actual.add(iter.next()); + } + // System.out.println("actual: " + actual); + assertEquals(5 * observableCount, actual.size()); + for (SubscriptionCheckObservable sco : scos) { + assertFalse(sco.failed); + } + } + } + + private static class SubscriptionCheckObservable implements Publisher<String> { + + private final AtomicInteger subscriptionCount; + private final int maxConcurrent; + volatile boolean failed; + + SubscriptionCheckObservable(AtomicInteger subscriptionCount, int maxConcurrent) { + this.subscriptionCount = subscriptionCount; + this.maxConcurrent = maxConcurrent; + } + + @Override + public void subscribe(final Subscriber<? super String> t1) { + t1.onSubscribe(new BooleanSubscription()); + new Thread(new Runnable() { + + @Override + public void run() { + if (subscriptionCount.incrementAndGet() > maxConcurrent) { + failed = true; + } + t1.onNext("one"); + t1.onNext("two"); + t1.onNext("three"); + t1.onNext("four"); + t1.onNext("five"); + // We could not decrement subscriptionCount in the unsubscribe method + // as "unsubscribe" is not guaranteed to be called before the next "subscribe". + subscriptionCount.decrementAndGet(); + t1.onComplete(); + } + + }).start(); + } + + } + + @Test + public void mergeALotOfSourcesOneByOneSynchronously() { + int n = 10000; + List<Flowable<Integer>> sourceList = new ArrayList<>(n); + for (int i = 0; i < n; i++) { + sourceList.add(Flowable.just(i)); + } + Iterator<Integer> it = Flowable.merge(Flowable.fromIterable(sourceList), 1).blockingIterable().iterator(); + int j = 0; + while (it.hasNext()) { + assertEquals((Integer)j, it.next()); + j++; + } + assertEquals(j, n); + } + + @Test + public void mergeALotOfSourcesOneByOneSynchronouslyTakeHalf() { + int n = 10000; + List<Flowable<Integer>> sourceList = new ArrayList<>(n); + for (int i = 0; i < n; i++) { + sourceList.add(Flowable.just(i)); + } + Iterator<Integer> it = Flowable.merge(Flowable.fromIterable(sourceList), 1).take(n / 2).blockingIterable().iterator(); + int j = 0; + while (it.hasNext()) { + assertEquals((Integer)j, it.next()); + j++; + } + assertEquals(j, n / 2); + } + + @Test + public void simple() { + for (int i = 1; i < 100; i++) { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(); + List<Flowable<Integer>> sourceList = new ArrayList<>(i); + List<Integer> result = new ArrayList<>(i); + for (int j = 1; j <= i; j++) { + sourceList.add(Flowable.just(j)); + result.add(j); + } + + Flowable.merge(sourceList, i).subscribe(ts); + + ts.assertNoErrors(); + ts.assertTerminated(); + ts.assertValueSequence(result); + } + } + + @Test + public void simpleOneLess() { + for (int i = 2; i < 100; i++) { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(); + List<Flowable<Integer>> sourceList = new ArrayList<>(i); + List<Integer> result = new ArrayList<>(i); + for (int j = 1; j <= i; j++) { + sourceList.add(Flowable.just(j)); + result.add(j); + } + + Flowable.merge(sourceList, i - 1).subscribe(ts); + + ts.assertNoErrors(); + ts.assertTerminated(); + ts.assertValueSequence(result); + } + } + + @Test + public void simpleAsyncLoop() { + IoScheduler ios = (IoScheduler)Schedulers.io(); + int c = ios.size(); + for (int i = 0; i < 200; i++) { + simpleAsync(); + int c1 = ios.size(); + if (c + 60 < c1) { + throw new AssertionError("Worker leak: " + c + " - " + c1); + } + } + } + + @Test + public void simpleAsync() { + for (int i = 1; i < 50; i++) { + TestSubscriber<Integer> ts = new TestSubscriber<>(); + List<Flowable<Integer>> sourceList = new ArrayList<>(i); + Set<Integer> expected = new HashSet<>(i); + for (int j = 1; j <= i; j++) { + sourceList.add(Flowable.just(j).subscribeOn(Schedulers.io())); + expected.add(j); + } + + Flowable.merge(sourceList, i).subscribe(ts); + + ts.awaitDone(1, TimeUnit.SECONDS); + ts.assertNoErrors(); + Set<Integer> actual = new HashSet<>(ts.values()); + + assertEquals(expected, actual); + } + } + + @Test + public void simpleOneLessAsyncLoop() { + for (int i = 0; i < 200; i++) { + simpleOneLessAsync(); + } + } + + @Test + public void simpleOneLessAsync() { + long t = System.currentTimeMillis(); + for (int i = 2; i < 50; i++) { + if (System.currentTimeMillis() - t > TimeUnit.SECONDS.toMillis(9)) { + break; + } + TestSubscriber<Integer> ts = new TestSubscriber<>(); + List<Flowable<Integer>> sourceList = new ArrayList<>(i); + Set<Integer> expected = new HashSet<>(i); + for (int j = 1; j <= i; j++) { + sourceList.add(Flowable.just(j).subscribeOn(Schedulers.io())); + expected.add(j); + } + + Flowable.merge(sourceList, i - 1).subscribe(ts); + + ts.awaitDone(1, TimeUnit.SECONDS); + ts.assertNoErrors(); + Set<Integer> actual = new HashSet<>(ts.values()); + + assertEquals(expected, actual); + } + } + + @Test + public void backpressureHonored() throws Exception { + List<Flowable<Integer>> sourceList = new ArrayList<>(3); + + sourceList.add(Flowable.range(0, 100000).subscribeOn(Schedulers.io())); + sourceList.add(Flowable.range(0, 100000).subscribeOn(Schedulers.io())); + sourceList.add(Flowable.range(0, 100000).subscribeOn(Schedulers.io())); + + final CountDownLatch cdl = new CountDownLatch(5); + + TestSubscriber<Integer> ts = new TestSubscriber<Integer>(0L) { + @Override + public void onNext(Integer t) { + super.onNext(t); + cdl.countDown(); + } + }; + + Flowable.merge(sourceList, 2).subscribe(ts); + + ts.request(5); + + cdl.await(); + + ts.assertNoErrors(); + ts.assertValueCount(5); + ts.assertNotComplete(); + + ts.cancel(); + } + + @Test + public void take() throws Exception { + List<Flowable<Integer>> sourceList = new ArrayList<>(3); + + sourceList.add(Flowable.range(0, 100000).subscribeOn(Schedulers.io())); + sourceList.add(Flowable.range(0, 100000).subscribeOn(Schedulers.io())); + sourceList.add(Flowable.range(0, 100000).subscribeOn(Schedulers.io())); + + TestSubscriber<Integer> ts = new TestSubscriber<>(); + + Flowable.merge(sourceList, 2).take(5).subscribe(ts); + + ts.awaitDone(5, TimeUnit.SECONDS); + ts.assertNoErrors(); + ts.assertValueCount(5); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableMergeTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableMergeTest.java new file mode 100644 index 0000000000..57b7a4a667 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableMergeTest.java @@ -0,0 +1,1544 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import static java.util.Arrays.asList; +import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import java.util.*; +import java.util.concurrent.*; +import java.util.concurrent.atomic.*; + +import org.junit.*; +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.core.Scheduler.Worker; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.internal.subscriptions.*; +import io.reactivex.rxjava3.internal.util.*; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.processors.PublishProcessor; +import io.reactivex.rxjava3.schedulers.*; +import io.reactivex.rxjava3.subscribers.*; +import io.reactivex.rxjava3.testsupport.*; + +public class FlowableMergeTest extends RxJavaTest { + + Subscriber<String> stringSubscriber; + + int count; + + @Before + public void before() { + stringSubscriber = TestHelper.mockSubscriber(); + + for (Thread t : Thread.getAllStackTraces().keySet()) { + if (t.getName().startsWith("RxNewThread")) { + count++; + } + } + } + + @After + public void after() { + try { + Thread.sleep(100); + } catch (InterruptedException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + for (Thread t : Thread.getAllStackTraces().keySet()) { + if (t.getName().startsWith("RxNewThread")) { + --count; + } + } + if (count != 0) { + throw new IllegalStateException("NewThread leak!"); + } + } + + @Test + public void mergeFlowableOfFlowables() { + final Flowable<String> f1 = Flowable.unsafeCreate(new TestSynchronousFlowable()); + final Flowable<String> f2 = Flowable.unsafeCreate(new TestSynchronousFlowable()); + + Flowable<Flowable<String>> flowableOfFlowables = Flowable.unsafeCreate(new Publisher<Flowable<String>>() { + + @Override + public void subscribe(Subscriber<? super Flowable<String>> subscriber) { + subscriber.onSubscribe(new BooleanSubscription()); + // simulate what would happen in a Flowable + subscriber.onNext(f1); + subscriber.onNext(f2); + subscriber.onComplete(); + } + + }); + Flowable<String> m = Flowable.merge(flowableOfFlowables); + m.subscribe(stringSubscriber); + + verify(stringSubscriber, never()).onError(any(Throwable.class)); + verify(stringSubscriber, times(1)).onComplete(); + verify(stringSubscriber, times(2)).onNext("hello"); + } + + @Test + public void mergeArray() { + final Flowable<String> f1 = Flowable.unsafeCreate(new TestSynchronousFlowable()); + final Flowable<String> f2 = Flowable.unsafeCreate(new TestSynchronousFlowable()); + + Flowable<String> m = Flowable.merge(f1, f2); + m.subscribe(stringSubscriber); + + verify(stringSubscriber, never()).onError(any(Throwable.class)); + verify(stringSubscriber, times(2)).onNext("hello"); + verify(stringSubscriber, times(1)).onComplete(); + } + + @Test + public void mergeList() { + final Flowable<String> f1 = Flowable.unsafeCreate(new TestSynchronousFlowable()); + final Flowable<String> f2 = Flowable.unsafeCreate(new TestSynchronousFlowable()); + List<Flowable<String>> listOfFlowables = new ArrayList<>(); + listOfFlowables.add(f1); + listOfFlowables.add(f2); + + Flowable<String> m = Flowable.merge(listOfFlowables); + m.subscribe(stringSubscriber); + + verify(stringSubscriber, never()).onError(any(Throwable.class)); + verify(stringSubscriber, times(1)).onComplete(); + verify(stringSubscriber, times(2)).onNext("hello"); + } + + @Test + public void unSubscribeFlowableOfFlowables() throws InterruptedException { + + final AtomicBoolean unsubscribed = new AtomicBoolean(); + final CountDownLatch latch = new CountDownLatch(1); + + Flowable<Flowable<Long>> source = Flowable.unsafeCreate(new Publisher<Flowable<Long>>() { + + @Override + public void subscribe(final Subscriber<? super Flowable<Long>> subscriber) { + // verbose on purpose so I can track the inside of it + final Subscription s = new Subscription() { + + @Override + public void request(long n) { + + } + + @Override + public void cancel() { + System.out.println("*** unsubscribed"); + unsubscribed.set(true); + } + + }; + subscriber.onSubscribe(s); + + new Thread(new Runnable() { + + @Override + public void run() { + + while (!unsubscribed.get()) { + subscriber.onNext(Flowable.just(1L, 2L)); + } + System.out.println("Done looping after unsubscribe: " + unsubscribed.get()); + subscriber.onComplete(); + + // mark that the thread is finished + latch.countDown(); + } + }).start(); + } + + }); + + final AtomicInteger count = new AtomicInteger(); + Flowable.merge(source).take(6).blockingForEach(new Consumer<Long>() { + + @Override + public void accept(Long v) { + System.out.println("Value: " + v); + int c = count.incrementAndGet(); + if (c > 6) { + fail("Should be only 6"); + } + + } + }); + + latch.await(1000, TimeUnit.MILLISECONDS); + + System.out.println("unsubscribed: " + unsubscribed.get()); + + assertTrue(unsubscribed.get()); + + } + + @Test + public void mergeArrayWithThreading() { + final TestASynchronousFlowable f1 = new TestASynchronousFlowable(); + final TestASynchronousFlowable f2 = new TestASynchronousFlowable(); + + Flowable<String> m = Flowable.merge(Flowable.unsafeCreate(f1), Flowable.unsafeCreate(f2)); + TestSubscriber<String> ts = new TestSubscriber<>(stringSubscriber); + m.subscribe(ts); + + ts.awaitDone(10, TimeUnit.SECONDS); + ts.assertNoErrors(); + + verify(stringSubscriber, never()).onError(any(Throwable.class)); + verify(stringSubscriber, times(2)).onNext("hello"); + verify(stringSubscriber, times(1)).onComplete(); + } + + @Test + public void synchronizationOfMultipleSequencesLoop() throws Throwable { + for (int i = 0; i < 100; i++) { + System.out.println("testSynchronizationOfMultipleSequencesLoop > " + i); + synchronizationOfMultipleSequences(); + } + } + + @Test + public void synchronizationOfMultipleSequences() throws Throwable { + final TestASynchronousFlowable f1 = new TestASynchronousFlowable(); + final TestASynchronousFlowable f2 = new TestASynchronousFlowable(); + + // use this latch to cause onNext to wait until we're ready to let it go + final CountDownLatch endLatch = new CountDownLatch(1); + + final AtomicInteger concurrentCounter = new AtomicInteger(); + final AtomicInteger totalCounter = new AtomicInteger(); + + final AtomicReference<Throwable> error = new AtomicReference<>(); + + Flowable<String> m = Flowable.merge(Flowable.unsafeCreate(f1), Flowable.unsafeCreate(f2)); + m.subscribe(new DefaultSubscriber<String>() { + + @Override + public void onComplete() { + + } + + @Override + public void onError(Throwable e) { + error.set(e); + } + + @Override + public void onNext(String v) { + totalCounter.incrementAndGet(); + concurrentCounter.incrementAndGet(); + try { + // avoid deadlocking the main thread + if (Thread.currentThread().getName().equals("TestASynchronousFlowable")) { + // wait here until we're done asserting + endLatch.await(); + } + } catch (InterruptedException e) { + e.printStackTrace(); + throw new RuntimeException("failed", e); + } finally { + concurrentCounter.decrementAndGet(); + } + } + + }); + + // wait for both Flowables to send (one should be blocked) + f1.onNextBeingSent.await(); + f2.onNextBeingSent.await(); + + // I can't think of a way to know for sure that both threads have or are trying to send onNext + // since I can't use a CountDownLatch for "after" onNext since I want to catch during it + // but I can't know for sure onNext is invoked + // so I'm unfortunately reverting to using a Thread.sleep to allow the process scheduler time + // to make sure after o1.onNextBeingSent and o2.onNextBeingSent are hit that the following + // onNext is invoked. + + int timeout = 20; + + while (timeout-- > 0 && concurrentCounter.get() != 1) { + Thread.sleep(100); + } + + try { // in try/finally so threads are released via latch countDown even if assertion fails + if (error.get() != null) { + throw ExceptionHelper.wrapOrThrow(error.get()); + } + + assertEquals(1, concurrentCounter.get()); + } finally { + // release so it can finish + endLatch.countDown(); + } + + try { + f1.t.join(); + f2.t.join(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + + assertEquals(2, totalCounter.get()); + assertEquals(0, concurrentCounter.get()); + } + + /** + * Unit test from OperationMergeDelayError backported here to show how these use cases work with normal merge. + */ + @Test + public void error1() { + // we are using synchronous execution to test this exactly rather than non-deterministic concurrent behavior + final Flowable<String> f1 = Flowable.unsafeCreate(new TestErrorFlowable("four", null, "six")); // we expect to lose "six" + final Flowable<String> f2 = Flowable.unsafeCreate(new TestErrorFlowable("one", "two", "three")); // we expect to lose all of these since o1 is done first and fails + + Flowable<String> m = Flowable.merge(f1, f2); + m.subscribe(stringSubscriber); + + verify(stringSubscriber, times(1)).onError(any(NullPointerException.class)); + verify(stringSubscriber, never()).onComplete(); + verify(stringSubscriber, times(0)).onNext("one"); + verify(stringSubscriber, times(0)).onNext("two"); + verify(stringSubscriber, times(0)).onNext("three"); + verify(stringSubscriber, times(1)).onNext("four"); + verify(stringSubscriber, times(0)).onNext("five"); + verify(stringSubscriber, times(0)).onNext("six"); + } + + /** + * Unit test from OperationMergeDelayError backported here to show how these use cases work with normal merge. + */ + @Test + public void error2() { + // we are using synchronous execution to test this exactly rather than non-deterministic concurrent behavior + final Flowable<String> f1 = Flowable.unsafeCreate(new TestErrorFlowable("one", "two", "three")); + final Flowable<String> f2 = Flowable.unsafeCreate(new TestErrorFlowable("four", null, "six")); // we expect to lose "six" + final Flowable<String> f3 = Flowable.unsafeCreate(new TestErrorFlowable("seven", "eight", null)); // we expect to lose all of these since o2 is done first and fails + final Flowable<String> f4 = Flowable.unsafeCreate(new TestErrorFlowable("nine")); // we expect to lose all of these since o2 is done first and fails + + Flowable<String> m = Flowable.merge(f1, f2, f3, f4); + m.subscribe(stringSubscriber); + + verify(stringSubscriber, times(1)).onError(any(NullPointerException.class)); + verify(stringSubscriber, never()).onComplete(); + verify(stringSubscriber, times(1)).onNext("one"); + verify(stringSubscriber, times(1)).onNext("two"); + verify(stringSubscriber, times(1)).onNext("three"); + verify(stringSubscriber, times(1)).onNext("four"); + verify(stringSubscriber, times(0)).onNext("five"); + verify(stringSubscriber, times(0)).onNext("six"); + verify(stringSubscriber, times(0)).onNext("seven"); + verify(stringSubscriber, times(0)).onNext("eight"); + verify(stringSubscriber, times(0)).onNext("nine"); + } + + private static class TestSynchronousFlowable implements Publisher<String> { + + @Override + public void subscribe(Subscriber<? super String> subscriber) { + subscriber.onSubscribe(new BooleanSubscription()); + subscriber.onNext("hello"); + subscriber.onComplete(); + } + } + + private static class TestASynchronousFlowable implements Publisher<String> { + Thread t; + final CountDownLatch onNextBeingSent = new CountDownLatch(1); + + @Override + public void subscribe(final Subscriber<? super String> subscriber) { + subscriber.onSubscribe(new BooleanSubscription()); + t = new Thread(new Runnable() { + + @Override + public void run() { + onNextBeingSent.countDown(); + try { + subscriber.onNext("hello"); + // I can't use a countDownLatch to prove we are actually sending 'onNext' + // since it will block if synchronized and I'll deadlock + subscriber.onComplete(); + } catch (Exception e) { + subscriber.onError(e); + } + } + + }, "TestASynchronousFlowable"); + t.start(); + } + } + + private static class TestErrorFlowable implements Publisher<String> { + + String[] valuesToReturn; + + TestErrorFlowable(String... values) { + valuesToReturn = values; + } + + @Override + public void subscribe(Subscriber<? super String> subscriber) { + subscriber.onSubscribe(new BooleanSubscription()); + for (String s : valuesToReturn) { + if (s == null) { + System.out.println("throwing exception"); + subscriber.onError(new NullPointerException()); + } else { + subscriber.onNext(s); + } + } + subscriber.onComplete(); + } + } + + @Test + public void unsubscribeAsFlowablesComplete() { + TestScheduler scheduler1 = new TestScheduler(); + AtomicBoolean os1 = new AtomicBoolean(false); + Flowable<Long> f1 = createFlowableOf5IntervalsOf1SecondIncrementsWithSubscriptionHook(scheduler1, os1); + + TestScheduler scheduler2 = new TestScheduler(); + AtomicBoolean os2 = new AtomicBoolean(false); + Flowable<Long> f2 = createFlowableOf5IntervalsOf1SecondIncrementsWithSubscriptionHook(scheduler2, os2); + + TestSubscriberEx<Long> ts = new TestSubscriberEx<>(); + Flowable.merge(f1, f2).subscribe(ts); + + // we haven't incremented time so nothing should be received yet + ts.assertNoValues(); + + scheduler1.advanceTimeBy(3, TimeUnit.SECONDS); + scheduler2.advanceTimeBy(2, TimeUnit.SECONDS); + + ts.assertValues(0L, 1L, 2L, 0L, 1L); + // not unsubscribed yet + assertFalse(os1.get()); + assertFalse(os2.get()); + + // advance to the end at which point it should complete + scheduler1.advanceTimeBy(3, TimeUnit.SECONDS); + + ts.assertValues(0L, 1L, 2L, 0L, 1L, 3L, 4L); + assertTrue(os1.get()); + assertFalse(os2.get()); + + // both should be completed now + scheduler2.advanceTimeBy(3, TimeUnit.SECONDS); + + ts.assertValues(0L, 1L, 2L, 0L, 1L, 3L, 4L, 2L, 3L, 4L); + assertTrue(os1.get()); + assertTrue(os2.get()); + + ts.assertTerminated(); + } + + @Test + public void earlyUnsubscribe() { + for (int i = 0; i < 10; i++) { + TestScheduler scheduler1 = new TestScheduler(); + AtomicBoolean os1 = new AtomicBoolean(false); + Flowable<Long> f1 = createFlowableOf5IntervalsOf1SecondIncrementsWithSubscriptionHook(scheduler1, os1); + + TestScheduler scheduler2 = new TestScheduler(); + AtomicBoolean os2 = new AtomicBoolean(false); + Flowable<Long> f2 = createFlowableOf5IntervalsOf1SecondIncrementsWithSubscriptionHook(scheduler2, os2); + + TestSubscriber<Long> ts = new TestSubscriber<>(); + Flowable.merge(f1, f2).subscribe(ts); + + // we haven't incremented time so nothing should be received yet + ts.assertNoValues(); + + scheduler1.advanceTimeBy(3, TimeUnit.SECONDS); + scheduler2.advanceTimeBy(2, TimeUnit.SECONDS); + + ts.assertValues(0L, 1L, 2L, 0L, 1L); + // not unsubscribed yet + assertFalse(os1.get()); + assertFalse(os2.get()); + + // early unsubscribe + ts.cancel(); + + assertTrue(os1.get()); + assertTrue(os2.get()); + + ts.assertValues(0L, 1L, 2L, 0L, 1L); + } + } + + private Flowable<Long> createFlowableOf5IntervalsOf1SecondIncrementsWithSubscriptionHook(final Scheduler scheduler, final AtomicBoolean unsubscribed) { + return Flowable.unsafeCreate(new Publisher<Long>() { + + @Override + public void subscribe(final Subscriber<? super Long> child) { + Flowable.interval(1, TimeUnit.SECONDS, scheduler) + .take(5) + .subscribe(new FlowableSubscriber<Long>() { + @Override + public void onSubscribe(final Subscription s) { + child.onSubscribe(new Subscription() { + @Override + public void request(long n) { + s.request(n); + } + + @Override + public void cancel() { + unsubscribed.set(true); + s.cancel(); + } + }); + } + + @Override + public void onNext(Long t) { + child.onNext(t); + } + + @Override + public void onError(Throwable t) { + unsubscribed.set(true); + child.onError(t); + } + + @Override + public void onComplete() { + unsubscribed.set(true); + child.onComplete(); + } + + }); + } + }); + } + + @Test + public void concurrency() { + Flowable<Integer> f = Flowable.range(1, 10000).subscribeOn(Schedulers.newThread()); + + for (int i = 0; i < 10; i++) { + Flowable<Integer> merge = Flowable.merge(f.onBackpressureBuffer(), f.onBackpressureBuffer(), f.onBackpressureBuffer()); + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(); + merge.subscribe(ts); + + ts.awaitDone(3, TimeUnit.SECONDS); + ts.assertTerminated(); + ts.assertNoErrors(); + ts.assertComplete(); + List<Integer> onNextEvents = ts.values(); + assertEquals(30000, onNextEvents.size()); + // System.out.println("onNext: " + onNextEvents.size() + " onComplete: " + ts.getOnCompletedEvents().size()); + } + } + + @Test + public void concurrencyWithSleeping() { + + Flowable<Integer> f = Flowable.unsafeCreate(new Publisher<Integer>() { + + @Override + public void subscribe(final Subscriber<? super Integer> s) { + Worker inner = Schedulers.newThread().createWorker(); + final AsyncSubscription as = new AsyncSubscription(); + as.setSubscription(new BooleanSubscription()); + as.setResource(inner); + + s.onSubscribe(as); + + inner.schedule(new Runnable() { + + @Override + public void run() { + try { + for (int i = 0; i < 100; i++) { + s.onNext(1); + try { + Thread.sleep(1); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + } catch (Exception e) { + s.onError(e); + } + as.dispose(); + s.onComplete(); + } + + }); + } + }); + + for (int i = 0; i < 10; i++) { + Flowable<Integer> merge = Flowable.merge(f, f, f); + TestSubscriber<Integer> ts = new TestSubscriber<>(); + merge.subscribe(ts); + + ts.awaitDone(10, TimeUnit.SECONDS); + ts.assertComplete(); + List<Integer> onNextEvents = ts.values(); + assertEquals(300, onNextEvents.size()); + // System.out.println("onNext: " + onNextEvents.size() + " onComplete: " + ts.getOnCompletedEvents().size()); + } + } + + @Test + public void concurrencyWithBrokenOnCompleteContract() { + Flowable<Integer> f = Flowable.unsafeCreate(new Publisher<Integer>() { + + @Override + public void subscribe(final Subscriber<? super Integer> s) { + Worker inner = Schedulers.newThread().createWorker(); + final AsyncSubscription as = new AsyncSubscription(); + as.setSubscription(new BooleanSubscription()); + as.setResource(inner); + + s.onSubscribe(as); + + inner.schedule(new Runnable() { + + @Override + public void run() { + try { + for (int i = 0; i < 10000; i++) { + s.onNext(i); + } + } catch (Exception e) { + s.onError(e); + } + as.dispose(); + s.onComplete(); + s.onComplete(); + s.onComplete(); + } + + }); + } + }); + + for (int i = 0; i < 10; i++) { + Flowable<Integer> merge = Flowable.merge(f.onBackpressureBuffer(), f.onBackpressureBuffer(), f.onBackpressureBuffer()); + TestSubscriber<Integer> ts = new TestSubscriber<>(); + merge.subscribe(ts); + + ts.awaitDone(10, TimeUnit.SECONDS); + ts.assertNoErrors(); + ts.assertComplete(); + List<Integer> onNextEvents = ts.values(); + assertEquals(30000, onNextEvents.size()); + // System.out.println("onNext: " + onNextEvents.size() + " onComplete: " + ts.getOnCompletedEvents().size()); + } + } + + @Test + public void backpressureUpstream() throws InterruptedException { + final AtomicInteger generated1 = new AtomicInteger(); + Flowable<Integer> f1 = createInfiniteFlowable(generated1).subscribeOn(Schedulers.computation()); + final AtomicInteger generated2 = new AtomicInteger(); + Flowable<Integer> f2 = createInfiniteFlowable(generated2).subscribeOn(Schedulers.computation()); + + TestSubscriberEx<Integer> testSubscriber = new TestSubscriberEx<Integer>() { + @Override + public void onNext(Integer t) { + System.err.println("testSubscriber received => " + t + " on thread " + Thread.currentThread()); + super.onNext(t); + } + }; + + Flowable.merge(f1.take(Flowable.bufferSize() * 2), f2.take(Flowable.bufferSize() * 2)).subscribe(testSubscriber); + testSubscriber.awaitDone(5, TimeUnit.SECONDS); + if (testSubscriber.errors().size() > 0) { + testSubscriber.errors().get(0).printStackTrace(); + } + testSubscriber.assertNoErrors(); + System.err.println(testSubscriber.values()); + assertEquals(Flowable.bufferSize() * 4, testSubscriber.values().size()); + // it should be between the take num and requested batch size across the async boundary + System.out.println("Generated 1: " + generated1.get()); + System.out.println("Generated 2: " + generated2.get()); + assertTrue(generated1.get() >= Flowable.bufferSize() * 2 && generated1.get() <= Flowable.bufferSize() * 4); + } + + @Test + public void backpressureUpstream2InLoop() throws InterruptedException { + for (int i = 0; i < 1000; i++) { + System.err.flush(); + System.out.println("---"); + System.out.flush(); + backpressureUpstream2(); + } + } + + @Test + public void backpressureUpstream2() throws InterruptedException { + final AtomicInteger generated1 = new AtomicInteger(); + Flowable<Integer> f1 = createInfiniteFlowable(generated1).subscribeOn(Schedulers.computation()); + + TestSubscriberEx<Integer> testSubscriber = new TestSubscriberEx<Integer>() { + @Override + public void onNext(Integer t) { + super.onNext(t); + } + }; + + Flowable.merge(f1.take(Flowable.bufferSize() * 2), Flowable.just(-99)).subscribe(testSubscriber); + testSubscriber.awaitDone(10, TimeUnit.SECONDS); + + List<Integer> onNextEvents = testSubscriber.values(); + + System.out.println("Generated 1: " + generated1.get() + " / received: " + onNextEvents.size()); + System.out.println(onNextEvents); + + if (testSubscriber.errors().size() > 0) { + testSubscriber.errors().get(0).printStackTrace(); + } + testSubscriber.assertNoErrors(); + assertEquals(Flowable.bufferSize() * 2 + 1, onNextEvents.size()); + // it should be between the take num and requested batch size across the async boundary + assertTrue(generated1.get() >= Flowable.bufferSize() * 2 && generated1.get() <= Flowable.bufferSize() * 3); + } + + /** + * This is the same as the upstreams ones, but now adds the downstream as well by using observeOn. + * + * This requires merge to also obey the Product.request values coming from it's child subscriber. + * @throws InterruptedException if the test is interrupted + */ + @Test + public void backpressureDownstreamWithConcurrentStreams() throws InterruptedException { + final AtomicInteger generated1 = new AtomicInteger(); + Flowable<Integer> f1 = createInfiniteFlowable(generated1).subscribeOn(Schedulers.computation()); + final AtomicInteger generated2 = new AtomicInteger(); + Flowable<Integer> f2 = createInfiniteFlowable(generated2).subscribeOn(Schedulers.computation()); + + TestSubscriberEx<Integer> testSubscriber = new TestSubscriberEx<Integer>() { + @Override + public void onNext(Integer t) { + if (t < 100) { + try { + // force a slow consumer + Thread.sleep(1); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + // System.err.println("testSubscriber received => " + t + " on thread " + Thread.currentThread()); + super.onNext(t); + } + }; + + Flowable.merge(f1.take(Flowable.bufferSize() * 2), f2.take(Flowable.bufferSize() * 2)).observeOn(Schedulers.computation()).subscribe(testSubscriber); + testSubscriber.awaitDone(10, TimeUnit.SECONDS); + if (testSubscriber.errors().size() > 0) { + testSubscriber.errors().get(0).printStackTrace(); + } + testSubscriber.assertNoErrors(); + System.err.println(testSubscriber.values()); + assertEquals(Flowable.bufferSize() * 4, testSubscriber.values().size()); + // it should be between the take num and requested batch size across the async boundary + System.out.println("Generated 1: " + generated1.get()); + System.out.println("Generated 2: " + generated2.get()); + assertTrue(generated1.get() >= Flowable.bufferSize() * 2 && generated1.get() <= Flowable.bufferSize() * 4); + } + + @Test + public void backpressureBothUpstreamAndDownstreamWithSynchronousScalarFlowables() throws InterruptedException { + final AtomicInteger generated1 = new AtomicInteger(); + Flowable<Flowable<Integer>> f1 = createInfiniteFlowable(generated1) + .map(new Function<Integer, Flowable<Integer>>() { + + @Override + public Flowable<Integer> apply(Integer t1) { + return Flowable.just(t1); + } + + }); + + TestSubscriberEx<Integer> testSubscriber = new TestSubscriberEx<Integer>() { + @Override + public void onNext(Integer t) { + if (t < 100) { + try { + // force a slow consumer + Thread.sleep(2); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + // System.err.println("testSubscriber received => " + t + " on thread " + Thread.currentThread()); + super.onNext(t); + } + }; + + Flowable.merge(f1).observeOn(Schedulers.computation()).take(Flowable.bufferSize() * 2).subscribe(testSubscriber); + testSubscriber.awaitDone(10, TimeUnit.SECONDS); + if (testSubscriber.errors().size() > 0) { + testSubscriber.errors().get(0).printStackTrace(); + } + testSubscriber.assertNoErrors(); + System.out.println("Generated 1: " + generated1.get()); + System.err.println(testSubscriber.values()); + assertEquals(Flowable.bufferSize() * 2, testSubscriber.values().size()); + // it should be between the take num and requested batch size across the async boundary + assertTrue(generated1.get() >= Flowable.bufferSize() * 2 && generated1.get() <= Flowable.bufferSize() * 4); + } + + /** + * Currently there is no solution to this ... we can't exert backpressure on the outer Flowable if we + * can't know if the ones we've received so far are going to emit or not, otherwise we could starve the system. + * + * For example, 10,000 Flowables are being merged (bad use case to begin with, but ...) and it's only one of them + * that will ever emit. If backpressure only allowed the first 1,000 to be sent, we would hang and never receive an event. + * + * Thus, we must allow all Flowables to be sent. The ScalarSynchronousFlowable use case is an exception to this since + * we can grab the value synchronously. + * + * @throws InterruptedException if the await is interrupted + */ + @Test + public void backpressureBothUpstreamAndDownstreamWithRegularFlowables() throws InterruptedException { + final AtomicInteger generated1 = new AtomicInteger(); + Flowable<Flowable<Integer>> f1 = createInfiniteFlowable(generated1).map(new Function<Integer, Flowable<Integer>>() { + + @Override + public Flowable<Integer> apply(Integer t1) { + return Flowable.just(1, 2, 3); + } + + }); + + TestSubscriberEx<Integer> testSubscriber = new TestSubscriberEx<Integer>() { + int i; + + @Override + public void onNext(Integer t) { + if (i++ < 400) { + try { + // force a slow consumer + Thread.sleep(1); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + // System.err.println("testSubscriber received => " + t + " on thread " + Thread.currentThread()); + super.onNext(t); + } + }; + + Flowable.merge(f1).observeOn(Schedulers.computation()).take(Flowable.bufferSize() * 2).subscribe(testSubscriber); + testSubscriber.awaitDone(10, TimeUnit.SECONDS); + if (testSubscriber.errors().size() > 0) { + testSubscriber.errors().get(0).printStackTrace(); + } + testSubscriber.assertNoErrors(); + System.out.println("Generated 1: " + generated1.get()); + System.err.println(testSubscriber.values()); + System.out.println("done1 testBackpressureBothUpstreamAndDownstreamWithRegularFlowables "); + assertEquals(Flowable.bufferSize() * 2, testSubscriber.values().size()); + System.out.println("done2 testBackpressureBothUpstreamAndDownstreamWithRegularFlowables "); + // we can't restrict this ... see comment above + // assertTrue(generated1.get() >= Flowable.bufferSize() && generated1.get() <= Flowable.bufferSize() * 4); + } + + @Test + public void merge1AsyncStreamOf1() { + TestSubscriber<Integer> ts = new TestSubscriber<>(); + mergeNAsyncStreamsOfN(1, 1).subscribe(ts); + ts.awaitDone(10, TimeUnit.SECONDS); + ts.assertNoErrors(); + assertEquals(1, ts.values().size()); + } + + @Test + public void merge1AsyncStreamOf1000() { + TestSubscriber<Integer> ts = new TestSubscriber<>(); + mergeNAsyncStreamsOfN(1, 1000).subscribe(ts); + ts.awaitDone(10, TimeUnit.SECONDS); + ts.assertNoErrors(); + assertEquals(1000, ts.values().size()); + } + + @Test + public void merge10AsyncStreamOf1000() { + TestSubscriber<Integer> ts = new TestSubscriber<>(); + mergeNAsyncStreamsOfN(10, 1000).subscribe(ts); + ts.awaitDone(10, TimeUnit.SECONDS); + ts.assertNoErrors(); + assertEquals(10000, ts.values().size()); + } + + @Test + public void merge1000AsyncStreamOf1000() { + TestSubscriber<Integer> ts = new TestSubscriber<>(); + mergeNAsyncStreamsOfN(1000, 1000).subscribe(ts); + ts.awaitDone(10, TimeUnit.SECONDS); + ts.assertNoErrors(); + assertEquals(1000000, ts.values().size()); + } + + @Test + public void merge2000AsyncStreamOf100() { + TestSubscriber<Integer> ts = new TestSubscriber<>(); + mergeNAsyncStreamsOfN(2000, 100).subscribe(ts); + ts.awaitDone(10, TimeUnit.SECONDS); + ts.assertNoErrors(); + assertEquals(200000, ts.values().size()); + } + + @Test + public void merge100AsyncStreamOf1() { + TestSubscriber<Integer> ts = new TestSubscriber<>(); + mergeNAsyncStreamsOfN(100, 1).subscribe(ts); + ts.awaitDone(10, TimeUnit.SECONDS); + ts.assertNoErrors(); + assertEquals(100, ts.values().size()); + } + + private Flowable<Integer> mergeNAsyncStreamsOfN(final int outerSize, final int innerSize) { + Flowable<Flowable<Integer>> os = Flowable.range(1, outerSize) + .map(new Function<Integer, Flowable<Integer>>() { + + @Override + public Flowable<Integer> apply(Integer i) { + return Flowable.range(1, innerSize).subscribeOn(Schedulers.computation()); + } + + }); + return Flowable.merge(os); + } + + @Test + public void merge1SyncStreamOf1() { + TestSubscriber<Integer> ts = new TestSubscriber<>(); + mergeNSyncStreamsOfN(1, 1).subscribe(ts); + ts.awaitDone(10, TimeUnit.SECONDS); + ts.assertNoErrors(); + assertEquals(1, ts.values().size()); + } + + @Test + public void merge1SyncStreamOf1000000() { + TestSubscriber<Integer> ts = new TestSubscriber<>(); + mergeNSyncStreamsOfN(1, 1000000).subscribe(ts); + ts.awaitDone(10, TimeUnit.SECONDS); + ts.assertNoErrors(); + assertEquals(1000000, ts.values().size()); + } + + @Test + public void merge1000SyncStreamOf1000() { + TestSubscriber<Integer> ts = new TestSubscriber<>(); + mergeNSyncStreamsOfN(1000, 1000).subscribe(ts); + ts.awaitDone(10, TimeUnit.SECONDS); + ts.assertNoErrors(); + assertEquals(1000000, ts.values().size()); + } + + @Test + public void merge10000SyncStreamOf10() { + TestSubscriber<Integer> ts = new TestSubscriber<>(); + mergeNSyncStreamsOfN(10000, 10).subscribe(ts); + ts.awaitDone(10, TimeUnit.SECONDS); + ts.assertNoErrors(); + assertEquals(100000, ts.values().size()); + } + + @Test + public void merge1000000SyncStreamOf1() { + TestSubscriber<Integer> ts = new TestSubscriber<>(); + mergeNSyncStreamsOfN(1000000, 1).subscribe(ts); + ts.awaitDone(10, TimeUnit.SECONDS); + ts.assertNoErrors(); + assertEquals(1000000, ts.values().size()); + } + + private Flowable<Integer> mergeNSyncStreamsOfN(final int outerSize, final int innerSize) { + Flowable<Flowable<Integer>> os = Flowable.range(1, outerSize) + .map(new Function<Integer, Flowable<Integer>>() { + + @Override + public Flowable<Integer> apply(Integer i) { + return Flowable.range(1, innerSize); + } + + }); + return Flowable.merge(os); + } + + private Flowable<Integer> createInfiniteFlowable(final AtomicInteger generated) { + Flowable<Integer> flowable = Flowable.fromIterable(new Iterable<Integer>() { + @Override + public Iterator<Integer> iterator() { + return new Iterator<Integer>() { + + @Override + public void remove() { + } + + @Override + public Integer next() { + return generated.getAndIncrement(); + } + + @Override + public boolean hasNext() { + return true; + } + }; + } + }); + return flowable; + } + + @Test + public void mergeManyAsyncSingle() { + TestSubscriber<Integer> ts = new TestSubscriber<>(); + Flowable<Flowable<Integer>> os = Flowable.range(1, 10000) + .map(new Function<Integer, Flowable<Integer>>() { + + @Override + public Flowable<Integer> apply(final Integer i) { + return Flowable.unsafeCreate(new Publisher<Integer>() { + + @Override + public void subscribe(Subscriber<? super Integer> s) { + s.onSubscribe(new BooleanSubscription()); + if (i < 500) { + try { + Thread.sleep(1); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + s.onNext(i); + s.onComplete(); + } + + }).subscribeOn(Schedulers.computation()).cache(); + } + + }); + Flowable.merge(os).subscribe(ts); + ts.awaitDone(10, TimeUnit.SECONDS); + ts.assertNoErrors(); + assertEquals(10000, ts.values().size()); + } + + @Test + public void shouldCompleteAfterApplyingBackpressure_NormalPath() { + Flowable<Integer> source = Flowable.mergeDelayError(Flowable.just(Flowable.range(1, 2))); + TestSubscriberEx<Integer> subscriber = new TestSubscriberEx<>(0L); + source.subscribe(subscriber); + subscriber.request(3); // 1, 2, <complete> - with request(2) we get the 1 and 2 but not the <complete> + subscriber.assertValues(1, 2); + subscriber.assertTerminated(); + } + + @Test + public void shouldCompleteAfterApplyingBackpressure_FastPath() { + Flowable<Integer> source = Flowable.mergeDelayError(Flowable.just(Flowable.just(1))); + TestSubscriberEx<Integer> subscriber = new TestSubscriberEx<>(0L); + source.subscribe(subscriber); + subscriber.request(2); // 1, <complete> - should work as per .._NormalPath above + subscriber.assertValue(1); + subscriber.assertTerminated(); + } + + @Test + public void shouldNotCompleteIfThereArePendingScalarSynchronousEmissionsWhenTheLastInnerSubscriberCompletes() { + TestScheduler scheduler = new TestScheduler(); + Flowable<Long> source = Flowable.mergeDelayError(Flowable.just(1L), Flowable.timer(1, TimeUnit.SECONDS, scheduler).skip(1)); + TestSubscriberEx<Long> subscriber = new TestSubscriberEx<>(0L); + source.subscribe(subscriber); + scheduler.advanceTimeBy(1, TimeUnit.SECONDS); + subscriber.assertNoValues(); + subscriber.assertNotComplete(); + subscriber.request(1); + subscriber.assertValue(1L); +// TODO: it should be acceptable to get a completion event without requests +// assertEquals(Collections.<Notification<Long>>emptyList(), subscriber.getOnCompletedEvents()); +// subscriber.request(1); + subscriber.assertTerminated(); + } + + @Test + public void delayedErrorsShouldBeEmittedWhenCompleteAfterApplyingBackpressure_NormalPath() { + Throwable exception = new Throwable(); + Flowable<Integer> source = Flowable.mergeDelayError(Flowable.range(1, 2), Flowable.<Integer>error(exception)); + TestSubscriberEx<Integer> subscriber = new TestSubscriberEx<>(0L); + source.subscribe(subscriber); + subscriber.request(3); // 1, 2, <error> + subscriber.assertValues(1, 2); + subscriber.assertTerminated(); + assertEquals(asList(exception), subscriber.errors()); + } + + @Test + public void delayedErrorsShouldBeEmittedWhenCompleteAfterApplyingBackpressure_FastPath() { + Throwable exception = new Throwable(); + Flowable<Integer> source = Flowable.mergeDelayError(Flowable.just(1), Flowable.<Integer>error(exception)); + TestSubscriberEx<Integer> subscriber = new TestSubscriberEx<>(0L); + source.subscribe(subscriber); + subscriber.request(2); // 1, <error> + subscriber.assertValue(1); + subscriber.assertTerminated(); + assertEquals(asList(exception), subscriber.errors()); + } + + @Test + public void shouldNotCompleteWhileThereAreStillScalarSynchronousEmissionsInTheQueue() { + Flowable<Integer> source = Flowable.merge(Flowable.just(1), Flowable.just(2)); + TestSubscriber<Integer> subscriber = new TestSubscriber<>(1L); + source.subscribe(subscriber); + subscriber.assertValue(1); + subscriber.request(1); + subscriber.assertValues(1, 2); + } + + @Test + public void shouldNotReceivedDelayedErrorWhileThereAreStillScalarSynchronousEmissionsInTheQueue() { + Throwable exception = new Throwable(); + Flowable<Integer> source = Flowable.mergeDelayError(Flowable.just(1), Flowable.just(2), Flowable.<Integer>error(exception)); + TestSubscriberEx<Integer> subscriber = new TestSubscriberEx<>(0L); + subscriber.request(1); + source.subscribe(subscriber); + subscriber.assertValue(1); + assertEquals(Collections.<Throwable>emptyList(), subscriber.errors()); + subscriber.request(1); + subscriber.assertValues(1, 2); + assertEquals(asList(exception), subscriber.errors()); + } + + @Test + public void shouldNotReceivedDelayedErrorWhileThereAreStillNormalEmissionsInTheQueue() { + Throwable exception = new Throwable(); + Flowable<Integer> source = Flowable.mergeDelayError(Flowable.range(1, 2), Flowable.range(3, 2), Flowable.<Integer>error(exception)); + TestSubscriberEx<Integer> subscriber = new TestSubscriberEx<>(0L); + subscriber.request(3); + source.subscribe(subscriber); + subscriber.assertValues(1, 2, 3); + assertEquals(Collections.<Throwable>emptyList(), subscriber.errors()); + subscriber.request(2); + subscriber.assertValues(1, 2, 3, 4); + assertEquals(asList(exception), subscriber.errors()); + } + + @Test + public void mergeKeepsRequesting() throws InterruptedException { + //for (int i = 0; i < 5000; i++) { + //System.out.println(i + "......................................................................."); + final CountDownLatch latch = new CountDownLatch(1); + final ConcurrentLinkedQueue<String> messages = new ConcurrentLinkedQueue<>(); + + Flowable.range(1, 2) + // produce many integers per second + .flatMap(new Function<Integer, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(final Integer number) { + return Flowable.range(1, Integer.MAX_VALUE) + .doOnRequest(new LongConsumer() { + + @Override + public void accept(long n) { + messages.add(">>>>>>>> A requested[" + number + "]: " + n); + } + + }) + // pause a bit + .doOnNext(pauseForMs(3)) + // buffer on backpressure + .onBackpressureBuffer() + // do in parallel + .subscribeOn(Schedulers.computation()) + .doOnRequest(new LongConsumer() { + + @Override + public void accept(long n) { + messages.add(">>>>>>>> B requested[" + number + "]: " + n); + } + + }); + } + + }) + // take a number bigger than 2* Flowable.bufferSize() (used by OperatorMerge) + .take(Flowable.bufferSize() * 2 + 1) + // log count + .doOnNext(printCount()) + // release latch + .doOnComplete(new Action() { + @Override + public void run() { + latch.countDown(); + } + }).subscribe(); + boolean a = latch.await(10, TimeUnit.SECONDS); + if (!a) { + for (String s : messages) { + System.out.println("DEBUG => " + s); + } + } + assertTrue(a); + //} + } + + @Test + public void mergeRequestOverflow() throws InterruptedException { + //do a non-trivial merge so that future optimisations with EMPTY don't invalidate this test + Flowable<Integer> f = Flowable.fromIterable(Arrays.asList(1, 2)) + .mergeWith(Flowable.fromIterable(Arrays.asList(3, 4))); + final int expectedCount = 4; + final CountDownLatch latch = new CountDownLatch(expectedCount); + f.subscribeOn(Schedulers.computation()).subscribe(new DefaultSubscriber<Integer>() { + + @Override + public void onStart() { + request(1); + } + + @Override + public void onComplete() { + //ignore + } + + @Override + public void onError(Throwable e) { + throw new RuntimeException(e); + } + + @Override + public void onNext(Integer t) { + latch.countDown(); + request(2); + request(Long.MAX_VALUE - 1); + }}); + assertTrue(latch.await(10, TimeUnit.SECONDS)); + } + + private static Consumer<Integer> printCount() { + return new Consumer<Integer>() { + long count; + + @Override + public void accept(Integer t1) { + count++; + System.out.println("count=" + count); + } + }; + } + + private static Consumer<Integer> pauseForMs(final long time) { + return new Consumer<Integer>() { + @Override + public void accept(Integer s) { + try { + Thread.sleep(time); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + }; + } + + Function<Integer, Flowable<Integer>> toScalar = new Function<Integer, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Integer v) { + return Flowable.just(v); + } + }; + + Function<Integer, Flowable<Integer>> toHiddenScalar = new Function<Integer, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Integer t) { + return Flowable.just(t).hide(); + } + }; + ; + + void runMerge(Function<Integer, Flowable<Integer>> func, TestSubscriberEx<Integer> ts) { + List<Integer> list = new ArrayList<>(); + for (int i = 0; i < 1000; i++) { + list.add(i); + } + Flowable<Integer> source = Flowable.fromIterable(list); + source.flatMap(func).subscribe(ts); + + if (ts.values().size() != 1000) { + System.out.println(ts.values()); + } + + ts.assertTerminated(); + ts.assertNoErrors(); + ts.assertValueSequence(list); + } + + @Test + public void fastMergeFullScalar() { + runMerge(toScalar, new TestSubscriberEx<>()); + } + + @Test + public void fastMergeHiddenScalar() { + runMerge(toHiddenScalar, new TestSubscriberEx<>()); + } + + @Test + public void slowMergeFullScalar() { + for (final int req : new int[] { 16, 32, 64, 128, 256 }) { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<Integer>(req) { + int remaining = req; + + @Override + public void onNext(Integer t) { + super.onNext(t); + if (--remaining == 0) { + remaining = req; + request(req); + } + } + }; + runMerge(toScalar, ts); + } + } + + @Test + public void slowMergeHiddenScalar() { + for (final int req : new int[] { 16, 32, 64, 128, 256 }) { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<Integer>(req) { + int remaining = req; + @Override + public void onNext(Integer t) { + super.onNext(t); + if (--remaining == 0) { + remaining = req; + request(req); + } + } + }; + runMerge(toHiddenScalar, ts); + } + } + + @Test + public void negativeMaxConcurrent() { + try { + Flowable.merge(Arrays.asList(Flowable.just(1), Flowable.just(2)), -1); + fail("Expected IllegalArgumentException"); + } catch (IllegalArgumentException e) { + assertEquals("maxConcurrency > 0 required but it was -1", e.getMessage()); + } + } + + @Test + public void zeroMaxConcurrent() { + try { + Flowable.merge(Arrays.asList(Flowable.just(1), Flowable.just(2)), 0); + fail("Expected IllegalArgumentException"); + } catch (IllegalArgumentException e) { + assertEquals("maxConcurrency > 0 required but it was 0", e.getMessage()); + } + } + + @Test + public void mergeConcurrentJustJust() { + TestSubscriber<Integer> ts = TestSubscriber.create(); + + Flowable.merge(Flowable.just(Flowable.just(1)), 5).subscribe(ts); + + ts.assertValue(1); + ts.assertNoErrors(); + ts.assertComplete(); + } + + @Test + public void mergeConcurrentJustRange() { + TestSubscriber<Integer> ts = TestSubscriber.create(); + + Flowable.merge(Flowable.just(Flowable.range(1, 5)), 5).subscribe(ts); + + ts.assertValues(1, 2, 3, 4, 5); + ts.assertNoErrors(); + ts.assertComplete(); + } + + @SuppressWarnings("unchecked") + @Test + public void mergeArrayMaxConcurrent() { + TestSubscriber<Integer> ts = TestSubscriber.create(); + + PublishProcessor<Integer> pp1 = PublishProcessor.create(); + PublishProcessor<Integer> pp2 = PublishProcessor.create(); + + Flowable.mergeArray(1, 128, new Flowable[] { pp1, pp2 }).subscribe(ts); + + assertTrue("ps1 has no subscribers?!", pp1.hasSubscribers()); + assertFalse("ps2 has subscribers?!", pp2.hasSubscribers()); + + pp1.onNext(1); + pp1.onComplete(); + + assertFalse("ps1 has subscribers?!", pp1.hasSubscribers()); + assertTrue("ps2 has no subscribers?!", pp2.hasSubscribers()); + + pp2.onNext(2); + pp2.onComplete(); + + ts.assertValues(1, 2); + ts.assertNoErrors(); + ts.assertComplete(); + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + @Test + public void flatMapJustJust() { + TestSubscriber<Integer> ts = TestSubscriber.create(); + + Flowable.just(Flowable.just(1)).flatMap((Function)Functions.identity()).subscribe(ts); + + ts.assertValue(1); + ts.assertNoErrors(); + ts.assertComplete(); + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + @Test + public void flatMapJustRange() { + TestSubscriber<Integer> ts = TestSubscriber.create(); + + Flowable.just(Flowable.range(1, 5)).flatMap((Function)Functions.identity()).subscribe(ts); + + ts.assertValues(1, 2, 3, 4, 5); + ts.assertNoErrors(); + ts.assertComplete(); + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + @Test + public void flatMapMaxConcurrentJustJust() { + TestSubscriber<Integer> ts = TestSubscriber.create(); + + Flowable.just(Flowable.just(1)).flatMap((Function)Functions.identity(), 5).subscribe(ts); + + ts.assertValue(1); + ts.assertNoErrors(); + ts.assertComplete(); + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + @Test + public void flatMapMaxConcurrentJustRange() { + TestSubscriber<Integer> ts = TestSubscriber.create(); + + Flowable.just(Flowable.range(1, 5)).flatMap((Function)Functions.identity(), 5).subscribe(ts); + + ts.assertValues(1, 2, 3, 4, 5); + ts.assertNoErrors(); + ts.assertComplete(); + } + + @Test + public void noInnerReordering() { + TestSubscriber<Integer> ts = TestSubscriber.create(0); + FlowableFlatMap.MergeSubscriber<Publisher<Integer>, Integer> ms = + new FlowableFlatMap.MergeSubscriber<>(ts, Functions.<Publisher<Integer>>identity(), false, 128, 128); + ms.onSubscribe(new BooleanSubscription()); + + PublishProcessor<Integer> pp = PublishProcessor.create(); + + ms.onNext(pp); + + pp.onNext(1); + + BackpressureHelper.add(ms.requested, 2); + + pp.onNext(2); + + ms.drain(); + + ts.assertValues(1, 2); + } + + @Test + public void noOuterScalarReordering() { + TestSubscriber<Integer> ts = TestSubscriber.create(0); + FlowableFlatMap.MergeSubscriber<Publisher<Integer>, Integer> ms = + new FlowableFlatMap.MergeSubscriber<>(ts, Functions.<Publisher<Integer>>identity(), false, 128, 128); + ms.onSubscribe(new BooleanSubscription()); + + ms.onNext(Flowable.just(1)); + + BackpressureHelper.add(ms.requested, 2); + + ms.onNext(Flowable.just(2)); + + ms.drain(); + + ts.assertValues(1, 2); + } + + @Test + public void array() { + for (int i = 1; i < 100; i++) { + + @SuppressWarnings("unchecked") + Flowable<Integer>[] sources = new Flowable[i]; + Arrays.fill(sources, Flowable.just(1)); + Integer[] expected = new Integer[i]; + for (int j = 0; j < i; j++) { + expected[j] = 1; + } + + Flowable.mergeArray(sources) + .test() + .assertResult(expected); + } + } + + @Test + public void mergeArray2() { + Flowable.mergeArray(Flowable.just(1), Flowable.just(2)) + .test() + .assertResult(1, 2); + } + + @Test + public void mergeErrors() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + Flowable<Integer> source1 = Flowable.error(new TestException("First")); + Flowable<Integer> source2 = Flowable.error(new TestException("Second")); + + Flowable.merge(source1, source2) + .to(TestHelper.<Integer>testConsumer()) + .assertFailureAndMessage(TestException.class, "First"); + + assertTrue(errors.toString(), errors.isEmpty()); + } finally { + RxJavaPlugins.reset(); + } + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableMergeWithCompletableTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableMergeWithCompletableTest.java new file mode 100644 index 0000000000..8eab622ccf --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableMergeWithCompletableTest.java @@ -0,0 +1,186 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import static org.junit.Assert.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.Action; +import io.reactivex.rxjava3.processors.PublishProcessor; +import io.reactivex.rxjava3.subjects.CompletableSubject; +import io.reactivex.rxjava3.subscribers.TestSubscriber; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class FlowableMergeWithCompletableTest extends RxJavaTest { + + @Test + public void normal() { + final TestSubscriber<Integer> ts = new TestSubscriber<>(); + + Flowable.range(1, 5).mergeWith( + Completable.fromAction(new Action() { + @Override + public void run() throws Exception { + ts.onNext(100); + } + }) + ) + .subscribe(ts); + + ts.assertResult(1, 2, 3, 4, 5, 100); + } + + @Test + public void take() { + Flowable.range(1, 5) + .mergeWith(Completable.complete()) + .take(3) + .test() + .assertResult(1, 2, 3); + } + + @Test + public void cancel() { + final PublishProcessor<Integer> pp = PublishProcessor.create(); + final CompletableSubject cs = CompletableSubject.create(); + + TestSubscriber<Integer> ts = pp.mergeWith(cs).test(); + + assertTrue(pp.hasSubscribers()); + assertTrue(cs.hasObservers()); + + ts.cancel(); + + assertFalse(pp.hasSubscribers()); + assertFalse(cs.hasObservers()); + } + + @Test + public void normalBackpressured() { + final TestSubscriber<Integer> ts = new TestSubscriber<>(0L); + + Flowable.range(1, 5).mergeWith( + Completable.fromAction(new Action() { + @Override + public void run() throws Exception { + ts.onNext(100); + } + }) + ) + .subscribe(ts); + + ts + .assertValue(100) + .requestMore(2) + .assertValues(100, 1, 2) + .requestMore(2) + .assertValues(100, 1, 2, 3, 4) + .requestMore(1) + .assertResult(100, 1, 2, 3, 4, 5); + } + + @Test + public void mainError() { + Flowable.error(new TestException()) + .mergeWith(Completable.complete()) + .test() + .assertFailure(TestException.class); + } + + @Test + public void otherError() { + Flowable.never() + .mergeWith(Completable.error(new TestException())) + .test() + .assertFailure(TestException.class); + } + + @Test + public void completeRace() { + for (int i = 0; i < 1000; i++) { + final PublishProcessor<Integer> pp = PublishProcessor.create(); + final CompletableSubject cs = CompletableSubject.create(); + + TestSubscriber<Integer> ts = pp.mergeWith(cs).test(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + pp.onNext(1); + pp.onComplete(); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + cs.onComplete(); + } + }; + + TestHelper.race(r1, r2); + + ts.assertResult(1); + } + } + + @Test + public void cancelOtherOnMainError() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + CompletableSubject cs = CompletableSubject.create(); + + TestSubscriber<Integer> ts = pp.mergeWith(cs).test(); + + assertTrue(pp.hasSubscribers()); + assertTrue(cs.hasObservers()); + + pp.onError(new TestException()); + + ts.assertFailure(TestException.class); + + assertFalse("main has observers!", pp.hasSubscribers()); + assertFalse("other has observers", cs.hasObservers()); + } + + @Test + public void cancelMainOnOtherError() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + CompletableSubject cs = CompletableSubject.create(); + + TestSubscriber<Integer> ts = pp.mergeWith(cs).test(); + + assertTrue(pp.hasSubscribers()); + assertTrue(cs.hasObservers()); + + cs.onError(new TestException()); + + ts.assertFailure(TestException.class); + + assertFalse("main has observers!", pp.hasSubscribers()); + assertFalse("other has observers", cs.hasObservers()); + } + + @Test + public void undeliverableUponCancel() { + TestHelper.checkUndeliverableUponCancel(new FlowableConverter<Integer, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Flowable<Integer> upstream) { + return upstream.mergeWith(Completable.complete().hide()); + } + }); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableMergeWithMaybeTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableMergeWithMaybeTest.java new file mode 100644 index 0000000000..aeef5bb09b --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableMergeWithMaybeTest.java @@ -0,0 +1,469 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import static org.junit.Assert.*; + +import java.io.IOException; +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; + +import org.junit.Test; +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.subscriptions.BooleanSubscription; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.processors.PublishProcessor; +import io.reactivex.rxjava3.subjects.*; +import io.reactivex.rxjava3.subscribers.TestSubscriber; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class FlowableMergeWithMaybeTest extends RxJavaTest { + + @Test + public void normal() { + Flowable.range(1, 5) + .mergeWith(Maybe.just(100)) + .test() + .assertResult(1, 2, 3, 4, 5, 100); + } + + @Test + public void emptyOther() { + Flowable.range(1, 5) + .mergeWith(Maybe.<Integer>empty()) + .test() + .assertResult(1, 2, 3, 4, 5); + } + + @Test + public void normalLong() { + Flowable.range(1, 512) + .mergeWith(Maybe.just(100)) + .test() + .assertValueCount(513) + .assertComplete(); + } + + @Test + public void normalLongRequestExact() { + Flowable.range(1, 512) + .mergeWith(Maybe.just(100)) + .test(513) + .assertValueCount(513) + .assertComplete(); + } + + @Test + public void take() { + Flowable.range(1, 5) + .mergeWith(Maybe.just(100)) + .take(3) + .test() + .assertResult(1, 2, 3); + } + + @Test + public void cancel() { + final PublishProcessor<Integer> pp = PublishProcessor.create(); + final MaybeSubject<Integer> cs = MaybeSubject.create(); + + TestSubscriber<Integer> ts = pp.mergeWith(cs).test(); + + assertTrue(pp.hasSubscribers()); + assertTrue(cs.hasObservers()); + + ts.cancel(); + + assertFalse(pp.hasSubscribers()); + assertFalse(cs.hasObservers()); + } + + @Test + public void normalBackpressured() { + Flowable.range(1, 5).mergeWith( + Maybe.just(100) + ) + .test(0L) + .assertEmpty() + .requestMore(2) + .assertValues(100, 1) + .requestMore(2) + .assertValues(100, 1, 2, 3) + .requestMore(2) + .assertResult(100, 1, 2, 3, 4, 5); + } + + @Test + public void mainError() { + Flowable.error(new TestException()) + .mergeWith(Maybe.just(100)) + .test() + .assertFailure(TestException.class); + } + + @Test + public void otherError() { + Flowable.never() + .mergeWith(Maybe.error(new TestException())) + .test() + .assertFailure(TestException.class); + } + + @Test + public void completeRace() { + for (int i = 0; i < 10000; i++) { + final PublishProcessor<Integer> pp = PublishProcessor.create(); + final MaybeSubject<Integer> cs = MaybeSubject.create(); + + TestSubscriber<Integer> ts = pp.mergeWith(cs).test(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + pp.onNext(1); + pp.onComplete(); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + cs.onSuccess(1); + } + }; + + TestHelper.race(r1, r2); + + ts.assertResult(1, 1); + } + } + + @Test + public void onNextSlowPath() { + final PublishProcessor<Integer> pp = PublishProcessor.create(); + final MaybeSubject<Integer> cs = MaybeSubject.create(); + + TestSubscriber<Integer> ts = pp.mergeWith(cs).subscribeWith(new TestSubscriber<Integer>() { + @Override + public void onNext(Integer t) { + super.onNext(t); + if (t == 1) { + pp.onNext(2); + } + } + }); + + pp.onNext(1); + cs.onSuccess(3); + + pp.onNext(4); + pp.onComplete(); + + ts.assertResult(1, 2, 3, 4); + } + + @Test + public void onSuccessSlowPath() { + final PublishProcessor<Integer> pp = PublishProcessor.create(); + final MaybeSubject<Integer> cs = MaybeSubject.create(); + + TestSubscriber<Integer> ts = pp.mergeWith(cs).subscribeWith(new TestSubscriber<Integer>() { + @Override + public void onNext(Integer t) { + super.onNext(t); + if (t == 1) { + cs.onSuccess(2); + } + } + }); + + pp.onNext(1); + + pp.onNext(3); + pp.onComplete(); + + ts.assertResult(1, 2, 3); + } + + @Test + public void onSuccessSlowPathBackpressured() { + final PublishProcessor<Integer> pp = PublishProcessor.create(); + final MaybeSubject<Integer> cs = MaybeSubject.create(); + + TestSubscriber<Integer> ts = pp.mergeWith(cs).subscribeWith(new TestSubscriber<Integer>(1) { + @Override + public void onNext(Integer t) { + super.onNext(t); + if (t == 1) { + cs.onSuccess(2); + } + } + }); + + pp.onNext(1); + + pp.onNext(3); + pp.onComplete(); + + ts.request(2); + ts.assertResult(1, 2, 3); + } + + @Test + public void onSuccessFastPathBackpressuredRace() { + for (int i = 0; i < 10000; i++) { + final PublishProcessor<Integer> pp = PublishProcessor.create(); + final MaybeSubject<Integer> cs = MaybeSubject.create(); + + final TestSubscriber<Integer> ts = pp.mergeWith(cs).subscribeWith(new TestSubscriber<>(0)); + + Runnable r1 = new Runnable() { + @Override + public void run() { + cs.onSuccess(1); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + ts.request(2); + } + }; + + TestHelper.race(r1, r2); + + pp.onNext(2); + pp.onComplete(); + + ts.assertResult(1, 2); + } + } + + @Test + public void onErrorMainOverflow() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + final AtomicReference<Subscriber<?>> subscriber = new AtomicReference<>(); + TestSubscriber<Integer> ts = new Flowable<Integer>() { + @Override + protected void subscribeActual(Subscriber<? super Integer> s) { + s.onSubscribe(new BooleanSubscription()); + subscriber.set(s); + } + } + .mergeWith(Maybe.<Integer>error(new IOException())) + .test(); + + subscriber.get().onError(new TestException()); + + ts.assertFailure(IOException.class) + ; + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void onErrorOtherOverflow() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + Flowable.error(new IOException()) + .mergeWith(Maybe.error(new TestException())) + .test() + .assertFailure(IOException.class) + ; + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void onNextRequestRace() { + for (int i = 0; i < 10000; i++) { + final PublishProcessor<Integer> pp = PublishProcessor.create(); + final MaybeSubject<Integer> cs = MaybeSubject.create(); + + final TestSubscriber<Integer> ts = pp.mergeWith(cs).test(0); + + pp.onNext(0); + + Runnable r1 = new Runnable() { + @Override + public void run() { + pp.onNext(1); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + ts.request(3); + } + }; + + TestHelper.race(r1, r2); + + cs.onSuccess(1); + pp.onComplete(); + + ts.assertResult(0, 1, 1); + } + } + + @Test + public void doubleOnSubscribeMain() { + TestHelper.checkDoubleOnSubscribeFlowable( + new Function<Flowable<Object>, Publisher<Object>>() { + @Override + public Publisher<Object> apply(Flowable<Object> f) + throws Exception { + return f.mergeWith(Maybe.just(1)); + } + } + ); + } + + @Test + public void noRequestOnError() { + Flowable.empty() + .mergeWith(Maybe.error(new TestException())) + .test(0) + .assertFailure(TestException.class); + } + + @Test + public void drainExactRequestCancel() { + final PublishProcessor<Integer> pp = PublishProcessor.create(); + final MaybeSubject<Integer> cs = MaybeSubject.create(); + + TestSubscriber<Integer> ts = pp.mergeWith(cs) + .take(2) + .subscribeWith(new TestSubscriber<Integer>(2) { + @Override + public void onNext(Integer t) { + super.onNext(t); + if (t == 1) { + cs.onSuccess(2); + } + } + }); + + pp.onNext(1); + + pp.onComplete(); + + ts.request(2); + ts.assertResult(1, 2); + } + + @Test + public void drainRequestWhenLimitReached() { + final PublishProcessor<Integer> pp = PublishProcessor.create(); + final MaybeSubject<Integer> cs = MaybeSubject.create(); + + TestSubscriber<Integer> ts = pp.mergeWith(cs) + .subscribeWith(new TestSubscriber<Integer>() { + @Override + public void onNext(Integer t) { + super.onNext(t); + if (t == 1) { + for (int i = 0; i < Flowable.bufferSize() - 1; i++) { + pp.onNext(i + 2); + } + } + } + }); + + cs.onSuccess(1); + + pp.onComplete(); + + ts.request(2); + ts.assertValueCount(Flowable.bufferSize()); + ts.assertComplete(); + } + + @Test + public void cancelOtherOnMainError() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + MaybeSubject<Integer> ms = MaybeSubject.create(); + + TestSubscriber<Integer> ts = pp.mergeWith(ms).test(); + + assertTrue(pp.hasSubscribers()); + assertTrue(ms.hasObservers()); + + pp.onError(new TestException()); + + ts.assertFailure(TestException.class); + + assertFalse("main has observers!", pp.hasSubscribers()); + assertFalse("other has observers", ms.hasObservers()); + } + + @Test + public void cancelMainOnOtherError() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + MaybeSubject<Integer> ms = MaybeSubject.create(); + + TestSubscriber<Integer> ts = pp.mergeWith(ms).test(); + + assertTrue(pp.hasSubscribers()); + assertTrue(ms.hasObservers()); + + ms.onError(new TestException()); + + ts.assertFailure(TestException.class); + + assertFalse("main has observers!", pp.hasSubscribers()); + assertFalse("other has observers", ms.hasObservers()); + } + + @Test + public void undeliverableUponCancel() { + TestHelper.checkUndeliverableUponCancel(new FlowableConverter<Integer, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Flowable<Integer> upstream) { + return upstream.mergeWith(Maybe.just(1).hide()); + } + }); + } + + @Test + public void drainMoreWorkBeforeCancel() { + MaybeSubject<Integer> ms = MaybeSubject.create(); + + TestSubscriber<Integer> ts = new TestSubscriber<>(); + + Flowable.range(1, 5).mergeWith(ms) + .doOnNext(v -> { + if (v == 1) { + ms.onSuccess(6); + ts.cancel(); + } + }) + .subscribe(ts); + + ts.assertValuesOnly(1); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableMergeWithSingleTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableMergeWithSingleTest.java new file mode 100644 index 0000000000..23612f5754 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableMergeWithSingleTest.java @@ -0,0 +1,465 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import static org.junit.Assert.*; + +import java.io.IOException; +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; + +import org.junit.Test; +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.subscriptions.BooleanSubscription; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.processors.PublishProcessor; +import io.reactivex.rxjava3.subjects.SingleSubject; +import io.reactivex.rxjava3.subscribers.TestSubscriber; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class FlowableMergeWithSingleTest extends RxJavaTest { + + @Test + public void normal() { + Flowable.range(1, 5) + .mergeWith(Single.just(100)) + .test() + .assertResult(1, 2, 3, 4, 5, 100); + } + + @Test + public void normalLong() { + Flowable.range(1, 512) + .mergeWith(Single.just(100)) + .test() + .assertValueCount(513) + .assertComplete(); + } + + @Test + public void normalLongRequestExact() { + Flowable.range(1, 512) + .mergeWith(Single.just(100)) + .test(513) + .assertValueCount(513) + .assertComplete(); + } + + @Test + public void take() { + Flowable.range(1, 5) + .mergeWith(Single.just(100)) + .take(3) + .test() + .assertResult(1, 2, 3); + } + + @Test + public void cancel() { + final PublishProcessor<Integer> pp = PublishProcessor.create(); + final SingleSubject<Integer> cs = SingleSubject.create(); + + TestSubscriber<Integer> ts = pp.mergeWith(cs).test(); + + assertTrue(pp.hasSubscribers()); + assertTrue(cs.hasObservers()); + + ts.cancel(); + + assertFalse(pp.hasSubscribers()); + assertFalse(cs.hasObservers()); + } + + @Test + public void normalBackpressured() { + final TestSubscriber<Integer> ts = new TestSubscriber<>(0L); + + Flowable.range(1, 5).mergeWith( + Single.just(100) + ) + .subscribe(ts); + + ts + .assertEmpty() + .requestMore(2) + .assertValues(100, 1) + .requestMore(2) + .assertValues(100, 1, 2, 3) + .requestMore(2) + .assertResult(100, 1, 2, 3, 4, 5); + } + + @Test + public void mainError() { + Flowable.error(new TestException()) + .mergeWith(Single.just(100)) + .test() + .assertFailure(TestException.class); + } + + @Test + public void otherError() { + Flowable.never() + .mergeWith(Single.error(new TestException())) + .test() + .assertFailure(TestException.class); + } + + @Test + public void completeRace() { + for (int i = 0; i < 10000; i++) { + final PublishProcessor<Integer> pp = PublishProcessor.create(); + final SingleSubject<Integer> cs = SingleSubject.create(); + + TestSubscriber<Integer> ts = pp.mergeWith(cs).test(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + pp.onNext(1); + pp.onComplete(); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + cs.onSuccess(1); + } + }; + + TestHelper.race(r1, r2); + + ts.assertResult(1, 1); + } + } + + @Test + public void onNextSlowPath() { + final PublishProcessor<Integer> pp = PublishProcessor.create(); + final SingleSubject<Integer> cs = SingleSubject.create(); + + TestSubscriber<Integer> ts = pp.mergeWith(cs).subscribeWith(new TestSubscriber<Integer>() { + @Override + public void onNext(Integer t) { + super.onNext(t); + if (t == 1) { + pp.onNext(2); + } + } + }); + + pp.onNext(1); + cs.onSuccess(3); + + pp.onNext(4); + pp.onComplete(); + + ts.assertResult(1, 2, 3, 4); + } + + @Test + public void onSuccessSlowPath() { + final PublishProcessor<Integer> pp = PublishProcessor.create(); + final SingleSubject<Integer> cs = SingleSubject.create(); + + TestSubscriber<Integer> ts = pp.mergeWith(cs).subscribeWith(new TestSubscriber<Integer>() { + @Override + public void onNext(Integer t) { + super.onNext(t); + if (t == 1) { + cs.onSuccess(2); + } + } + }); + + pp.onNext(1); + + pp.onNext(3); + pp.onComplete(); + + ts.assertResult(1, 2, 3); + } + + @Test + public void onSuccessSlowPathBackpressured() { + final PublishProcessor<Integer> pp = PublishProcessor.create(); + final SingleSubject<Integer> cs = SingleSubject.create(); + + TestSubscriber<Integer> ts = pp.mergeWith(cs).subscribeWith(new TestSubscriber<Integer>(1) { + @Override + public void onNext(Integer t) { + super.onNext(t); + if (t == 1) { + cs.onSuccess(2); + } + } + }); + + pp.onNext(1); + + pp.onNext(3); + pp.onComplete(); + + ts.request(2); + ts.assertResult(1, 2, 3); + } + + @Test + public void onSuccessFastPathBackpressuredRace() { + for (int i = 0; i < 10000; i++) { + final PublishProcessor<Integer> pp = PublishProcessor.create(); + final SingleSubject<Integer> cs = SingleSubject.create(); + + final TestSubscriber<Integer> ts = pp.mergeWith(cs).subscribeWith(new TestSubscriber<>(0)); + + Runnable r1 = new Runnable() { + @Override + public void run() { + cs.onSuccess(1); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + ts.request(2); + } + }; + + TestHelper.race(r1, r2); + + pp.onNext(2); + pp.onComplete(); + + ts.assertResult(1, 2); + } + } + + @Test + public void onErrorMainOverflow() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + final AtomicReference<Subscriber<?>> subscriber = new AtomicReference<>(); + TestSubscriber<Integer> ts = new Flowable<Integer>() { + @Override + protected void subscribeActual(Subscriber<? super Integer> s) { + s.onSubscribe(new BooleanSubscription()); + subscriber.set(s); + } + } + .mergeWith(Single.<Integer>error(new IOException())) + .test(); + + subscriber.get().onError(new TestException()); + + ts.assertFailure(IOException.class) + ; + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void onErrorOtherOverflow() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + Flowable.error(new IOException()) + .mergeWith(Single.error(new TestException())) + .test() + .assertFailure(IOException.class) + ; + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void onNextRequestRace() { + for (int i = 0; i < 10000; i++) { + final PublishProcessor<Integer> pp = PublishProcessor.create(); + final SingleSubject<Integer> cs = SingleSubject.create(); + + final TestSubscriber<Integer> ts = pp.mergeWith(cs).test(0); + + pp.onNext(0); + + Runnable r1 = new Runnable() { + @Override + public void run() { + pp.onNext(1); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + ts.request(3); + } + }; + + TestHelper.race(r1, r2); + + cs.onSuccess(1); + pp.onComplete(); + + ts.assertResult(0, 1, 1); + } + } + + @Test + public void doubleOnSubscribeMain() { + TestHelper.checkDoubleOnSubscribeFlowable( + new Function<Flowable<Object>, Publisher<Object>>() { + @Override + public Publisher<Object> apply(Flowable<Object> f) + throws Exception { + return f.mergeWith(Single.just(1)); + } + } + ); + } + + @Test + public void noRequestOnError() { + Flowable.empty() + .mergeWith(Single.error(new TestException())) + .test(0) + .assertFailure(TestException.class); + } + + @Test + public void drainExactRequestCancel() { + final PublishProcessor<Integer> pp = PublishProcessor.create(); + final SingleSubject<Integer> cs = SingleSubject.create(); + + TestSubscriber<Integer> ts = pp.mergeWith(cs) + .take(2) + .subscribeWith(new TestSubscriber<Integer>(2) { + @Override + public void onNext(Integer t) { + super.onNext(t); + if (t == 1) { + cs.onSuccess(2); + } + } + }); + + pp.onNext(1); + + pp.onComplete(); + + ts.request(2); + ts.assertResult(1, 2); + } + + @Test + public void drainRequestWhenLimitReached() { + final PublishProcessor<Integer> pp = PublishProcessor.create(); + final SingleSubject<Integer> cs = SingleSubject.create(); + + TestSubscriber<Integer> ts = pp.mergeWith(cs) + .subscribeWith(new TestSubscriber<Integer>() { + @Override + public void onNext(Integer t) { + super.onNext(t); + if (t == 1) { + for (int i = 0; i < Flowable.bufferSize() - 1; i++) { + pp.onNext(i + 2); + } + } + } + }); + + cs.onSuccess(1); + + pp.onComplete(); + + ts.request(2); + ts.assertValueCount(Flowable.bufferSize()); + ts.assertComplete(); + } + + @Test + public void cancelOtherOnMainError() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + SingleSubject<Integer> ss = SingleSubject.create(); + + TestSubscriber<Integer> ts = pp.mergeWith(ss).test(); + + assertTrue(pp.hasSubscribers()); + assertTrue(ss.hasObservers()); + + pp.onError(new TestException()); + + ts.assertFailure(TestException.class); + + assertFalse("main has observers!", pp.hasSubscribers()); + assertFalse("other has observers", ss.hasObservers()); + } + + @Test + public void cancelMainOnOtherError() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + SingleSubject<Integer> ss = SingleSubject.create(); + + TestSubscriber<Integer> ts = pp.mergeWith(ss).test(); + + assertTrue(pp.hasSubscribers()); + assertTrue(ss.hasObservers()); + + ss.onError(new TestException()); + + ts.assertFailure(TestException.class); + + assertFalse("main has observers!", pp.hasSubscribers()); + assertFalse("other has observers", ss.hasObservers()); + } + + @Test + public void undeliverableUponCancel() { + TestHelper.checkUndeliverableUponCancel(new FlowableConverter<Integer, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Flowable<Integer> upstream) { + return upstream.mergeWith(Single.just(1).hide()); + } + }); + } + + @Test + public void drainMoreWorkBeforeCancel() { + SingleSubject<Integer> ss = SingleSubject.create(); + + TestSubscriber<Integer> ts = new TestSubscriber<>(); + + Flowable.range(1, 5).mergeWith(ss) + .doOnNext(v -> { + if (v == 1) { + ss.onSuccess(6); + ts.cancel(); + } + }) + .subscribe(ts); + + ts.assertValuesOnly(1); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableObserveOnTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableObserveOnTest.java new file mode 100644 index 0000000000..e4e3d6f3fe --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableObserveOnTest.java @@ -0,0 +1,2119 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +import java.util.*; +import java.util.concurrent.*; +import java.util.concurrent.atomic.*; + +import org.junit.Test; +import org.mockito.InOrder; +import org.reactivestreams.*; + +import io.reactivex.rxjava3.annotations.Nullable; +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.*; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.internal.operators.flowable.FlowableObserveOn.BaseObserveOnSubscriber; +import io.reactivex.rxjava3.internal.schedulers.ImmediateThinScheduler; +import io.reactivex.rxjava3.internal.subscriptions.BooleanSubscription; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.operators.QueueFuseable; +import io.reactivex.rxjava3.operators.QueueSubscription; +import io.reactivex.rxjava3.operators.SimpleQueue; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.processors.*; +import io.reactivex.rxjava3.schedulers.*; +import io.reactivex.rxjava3.subscribers.*; +import io.reactivex.rxjava3.testsupport.*; + +public class FlowableObserveOnTest extends RxJavaTest { + + /** + * This is testing a no-op path since it uses Schedulers.immediate() which will not do scheduling. + */ + @Test + public void observeOn() { + Subscriber<Integer> subscriber = TestHelper.mockSubscriber(); + Flowable.just(1, 2, 3).observeOn(ImmediateThinScheduler.INSTANCE).subscribe(subscriber); + + verify(subscriber, times(1)).onNext(1); + verify(subscriber, times(1)).onNext(2); + verify(subscriber, times(1)).onNext(3); + verify(subscriber, times(1)).onComplete(); + } + + @Test + public void ordering() throws InterruptedException { + Flowable<String> obs = Flowable.just("one", "null", "two", "three", "four"); + + Subscriber<String> subscriber = TestHelper.mockSubscriber(); + + InOrder inOrder = inOrder(subscriber); + TestSubscriberEx<String> ts = new TestSubscriberEx<>(subscriber); + + obs.observeOn(Schedulers.computation()).subscribe(ts); + + ts.awaitDone(1000, TimeUnit.MILLISECONDS); + if (ts.errors().size() > 0) { + for (Throwable t : ts.errors()) { + t.printStackTrace(); + } + fail("failed with exception"); + } + + inOrder.verify(subscriber, times(1)).onNext("one"); + inOrder.verify(subscriber, times(1)).onNext("null"); + inOrder.verify(subscriber, times(1)).onNext("two"); + inOrder.verify(subscriber, times(1)).onNext("three"); + inOrder.verify(subscriber, times(1)).onNext("four"); + inOrder.verify(subscriber, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void threadName() throws InterruptedException { + System.out.println("Main Thread: " + Thread.currentThread().getName()); + Flowable<String> obs = Flowable.just("one", "null", "two", "three", "four"); + + Subscriber<String> subscriber = TestHelper.mockSubscriber(); + final String parentThreadName = Thread.currentThread().getName(); + + final CountDownLatch completedLatch = new CountDownLatch(1); + + // assert subscribe is on main thread + obs = obs.doOnNext(new Consumer<String>() { + + @Override + public void accept(String s) { + String threadName = Thread.currentThread().getName(); + System.out.println("Source ThreadName: " + threadName + " Expected => " + parentThreadName); + assertEquals(parentThreadName, threadName); + } + + }); + + // assert observe is on new thread + obs.observeOn(Schedulers.newThread()).doOnNext(new Consumer<String>() { + + @Override + public void accept(String t1) { + String threadName = Thread.currentThread().getName(); + boolean correctThreadName = threadName.startsWith("RxNewThreadScheduler"); + System.out.println("ObserveOn ThreadName: " + threadName + " Correct => " + correctThreadName); + assertTrue(correctThreadName); + } + + }).doAfterTerminate(new Action() { + + @Override + public void run() { + completedLatch.countDown(); + + } + }).subscribe(subscriber); + + if (!completedLatch.await(1000, TimeUnit.MILLISECONDS)) { + fail("timed out waiting"); + } + + verify(subscriber, never()).onError(any(Throwable.class)); + verify(subscriber, times(5)).onNext(any(String.class)); + verify(subscriber, times(1)).onComplete(); + } + + @Test + public void observeOnTheSameSchedulerTwice() { + Scheduler scheduler = ImmediateThinScheduler.INSTANCE; + + Flowable<Integer> f = Flowable.just(1, 2, 3); + Flowable<Integer> f2 = f.observeOn(scheduler); + + Subscriber<Object> subscriber1 = TestHelper.mockSubscriber(); + Subscriber<Object> subscriber2 = TestHelper.mockSubscriber(); + + InOrder inOrder1 = inOrder(subscriber1); + InOrder inOrder2 = inOrder(subscriber2); + + f2.subscribe(subscriber1); + f2.subscribe(subscriber2); + + inOrder1.verify(subscriber1, times(1)).onNext(1); + inOrder1.verify(subscriber1, times(1)).onNext(2); + inOrder1.verify(subscriber1, times(1)).onNext(3); + inOrder1.verify(subscriber1, times(1)).onComplete(); + verify(subscriber1, never()).onError(any(Throwable.class)); + inOrder1.verifyNoMoreInteractions(); + + inOrder2.verify(subscriber2, times(1)).onNext(1); + inOrder2.verify(subscriber2, times(1)).onNext(2); + inOrder2.verify(subscriber2, times(1)).onNext(3); + inOrder2.verify(subscriber2, times(1)).onComplete(); + verify(subscriber2, never()).onError(any(Throwable.class)); + inOrder2.verifyNoMoreInteractions(); + } + + @Test + public void observeSameOnMultipleSchedulers() { + TestScheduler scheduler1 = new TestScheduler(); + TestScheduler scheduler2 = new TestScheduler(); + + Flowable<Integer> f = Flowable.just(1, 2, 3); + Flowable<Integer> f1 = f.observeOn(scheduler1); + Flowable<Integer> f2 = f.observeOn(scheduler2); + + Subscriber<Object> subscriber1 = TestHelper.mockSubscriber(); + Subscriber<Object> subscriber2 = TestHelper.mockSubscriber(); + + InOrder inOrder1 = inOrder(subscriber1); + InOrder inOrder2 = inOrder(subscriber2); + + f1.subscribe(subscriber1); + f2.subscribe(subscriber2); + + scheduler1.advanceTimeBy(1, TimeUnit.SECONDS); + scheduler2.advanceTimeBy(1, TimeUnit.SECONDS); + + inOrder1.verify(subscriber1, times(1)).onNext(1); + inOrder1.verify(subscriber1, times(1)).onNext(2); + inOrder1.verify(subscriber1, times(1)).onNext(3); + inOrder1.verify(subscriber1, times(1)).onComplete(); + verify(subscriber1, never()).onError(any(Throwable.class)); + inOrder1.verifyNoMoreInteractions(); + + inOrder2.verify(subscriber2, times(1)).onNext(1); + inOrder2.verify(subscriber2, times(1)).onNext(2); + inOrder2.verify(subscriber2, times(1)).onNext(3); + inOrder2.verify(subscriber2, times(1)).onComplete(); + verify(subscriber2, never()).onError(any(Throwable.class)); + inOrder2.verifyNoMoreInteractions(); + } + + /** + * Confirm that running on a NewThreadScheduler uses the same thread for the entire stream. + */ + @Test + public void observeOnWithNewThreadScheduler() { + final AtomicInteger count = new AtomicInteger(); + final int _multiple = 99; + + Flowable.range(1, 100000).map(new Function<Integer, Integer>() { + + @Override + public Integer apply(Integer t1) { + return t1 * _multiple; + } + + }).observeOn(Schedulers.newThread()) + .blockingForEach(new Consumer<Integer>() { + + @Override + public void accept(Integer t1) { + assertEquals(count.incrementAndGet() * _multiple, t1.intValue()); + // FIXME toBlocking methods run on the current thread + String name = Thread.currentThread().getName(); + assertFalse("Wrong thread name: " + name, name.startsWith("Rx")); + } + + }); + + } + + /** + * Confirm that running on a ThreadPoolScheduler allows multiple threads but is still ordered. + */ + @Test + public void observeOnWithThreadPoolScheduler() { + final AtomicInteger count = new AtomicInteger(); + final int _multiple = 99; + + Flowable.range(1, 100000).map(new Function<Integer, Integer>() { + + @Override + public Integer apply(Integer t1) { + return t1 * _multiple; + } + + }).observeOn(Schedulers.computation()) + .blockingForEach(new Consumer<Integer>() { + + @Override + public void accept(Integer t1) { + assertEquals(count.incrementAndGet() * _multiple, t1.intValue()); + // FIXME toBlocking methods run on the caller's thread + String name = Thread.currentThread().getName(); + assertFalse("Wrong thread name: " + name, name.startsWith("Rx")); + } + + }); + } + + /** + * Attempts to confirm that when pauses exist between events, the ScheduledObserver + * does not lose or reorder any events since the scheduler will not block, but will + * be re-scheduled when it receives new events after each pause. + * + * + * This is non-deterministic in proving success, but if it ever fails (non-deterministically) + * it is a sign of potential issues as thread-races and scheduling should not affect output. + */ + @Test + public void observeOnOrderingConcurrency() { + final AtomicInteger count = new AtomicInteger(); + final int _multiple = 99; + + Flowable.range(1, 10000).map(new Function<Integer, Integer>() { + + @Override + public Integer apply(Integer t1) { + if (randomIntFrom0to100() > 98) { + try { + Thread.sleep(2); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + return t1 * _multiple; + } + + }).observeOn(Schedulers.computation()) + .blockingForEach(new Consumer<Integer>() { + + @Override + public void accept(Integer t1) { + assertEquals(count.incrementAndGet() * _multiple, t1.intValue()); +// assertTrue(name.startsWith("RxComputationThreadPool")); + // FIXME toBlocking now runs its methods on the caller thread + String name = Thread.currentThread().getName(); + assertFalse("Wrong thread name: " + name, name.startsWith("Rx")); + } + + }); + } + + @Test + public void nonBlockingOuterWhileBlockingOnNext() throws InterruptedException { + + final CountDownLatch completedLatch = new CountDownLatch(1); + final CountDownLatch nextLatch = new CountDownLatch(1); + final AtomicLong completeTime = new AtomicLong(); + // use subscribeOn to make async, observeOn to move + Flowable.range(1, 2).subscribeOn(Schedulers.newThread()).observeOn(Schedulers.newThread()).subscribe(new DefaultSubscriber<Integer>() { + + @Override + public void onComplete() { + System.out.println("onComplete"); + completeTime.set(System.nanoTime()); + completedLatch.countDown(); + } + + @Override + public void onError(Throwable e) { + + } + + @Override + public void onNext(Integer t) { + // don't let this thing finish yet + try { + if (!nextLatch.await(1000, TimeUnit.MILLISECONDS)) { + throw new RuntimeException("it shouldn't have timed out"); + } + } catch (InterruptedException e) { + throw new RuntimeException("it shouldn't have failed"); + } + } + + }); + + long afterSubscribeTime = System.nanoTime(); + System.out.println("After subscribe: " + completedLatch.getCount()); + assertEquals(1, completedLatch.getCount()); + nextLatch.countDown(); + completedLatch.await(1000, TimeUnit.MILLISECONDS); + assertTrue(completeTime.get() > afterSubscribeTime); + System.out.println("onComplete nanos after subscribe: " + (completeTime.get() - afterSubscribeTime)); + } + + private static int randomIntFrom0to100() { + // XORShift instead of Math.random http://javamex.com/tutorials/random_numbers/xorshift.shtml + long x = System.nanoTime(); + x ^= (x << 21); + x ^= (x >>> 35); + x ^= (x << 4); + return Math.abs((int) x % 100); + } + + @Test + public void delayedErrorDeliveryWhenSafeSubscriberUnsubscribes() { + TestScheduler testScheduler = new TestScheduler(); + + Flowable<Integer> source = Flowable.concat(Flowable.<Integer> error(new TestException()), Flowable.just(1)); + + Subscriber<Integer> subscriber = TestHelper.mockSubscriber(); + InOrder inOrder = inOrder(subscriber); + + source.observeOn(testScheduler).subscribe(subscriber); + + inOrder.verify(subscriber, never()).onError(any(TestException.class)); + + testScheduler.advanceTimeBy(1, TimeUnit.SECONDS); + + inOrder.verify(subscriber).onError(any(TestException.class)); + inOrder.verify(subscriber, never()).onNext(anyInt()); + inOrder.verify(subscriber, never()).onComplete(); + } + + @Test + public void afterUnsubscribeCalledThenObserverOnNextNeverCalled() { + final TestScheduler testScheduler = new TestScheduler(); + + final Subscriber<Integer> subscriber = TestHelper.mockSubscriber(); + TestSubscriber<Integer> ts = new TestSubscriber<>(subscriber); + + Flowable.just(1, 2, 3) + .observeOn(testScheduler) + .subscribe(ts); + + ts.cancel(); + testScheduler.advanceTimeBy(1, TimeUnit.SECONDS); + + final InOrder inOrder = inOrder(subscriber); + + inOrder.verify(subscriber, never()).onNext(anyInt()); + inOrder.verify(subscriber, never()).onError(any(Exception.class)); + inOrder.verify(subscriber, never()).onComplete(); + } + + @Test + public void backpressureWithTakeAfter() { + final AtomicInteger generated = new AtomicInteger(); + Flowable<Integer> flowable = Flowable.fromIterable(new Iterable<Integer>() { + @Override + public Iterator<Integer> iterator() { + return new Iterator<Integer>() { + + @Override + public void remove() { + } + + @Override + public Integer next() { + return generated.getAndIncrement(); + } + + @Override + public boolean hasNext() { + return true; + } + }; + } + }); + + TestSubscriber<Integer> testSubscriber = new TestSubscriber<Integer>() { + @Override + public void onNext(Integer t) { + System.err.println("c t = " + t + " thread " + Thread.currentThread()); + super.onNext(t); + try { + Thread.sleep(10); + } catch (InterruptedException e) { + } + } + }; + + flowable + .observeOn(Schedulers.newThread()) + .take(3) + .subscribe(testSubscriber); + testSubscriber.awaitDone(5, TimeUnit.SECONDS); + System.err.println(testSubscriber.values()); + testSubscriber.assertValues(0, 1, 2); + // it should be between the take num and requested batch size across the async boundary + System.out.println("Generated: " + generated.get()); + assertTrue(generated.get() >= 3 && generated.get() <= Flowable.bufferSize()); + } + + @Test + public void backpressureWithTakeAfterAndMultipleBatches() { + int numForBatches = Flowable.bufferSize() * 3 + 1; // should be 4 batches == ((3*n)+1) items + final AtomicInteger generated = new AtomicInteger(); + Flowable<Integer> flowable = Flowable.fromIterable(new Iterable<Integer>() { + @Override + public Iterator<Integer> iterator() { + return new Iterator<Integer>() { + + @Override + public void remove() { + } + + @Override + public Integer next() { + return generated.getAndIncrement(); + } + + @Override + public boolean hasNext() { + return true; + } + }; + } + }); + + TestSubscriber<Integer> testSubscriber = new TestSubscriber<Integer>() { + @Override + public void onNext(Integer t) { + // System.err.println("c t = " + t + " thread " + Thread.currentThread()); + super.onNext(t); + } + }; + + flowable + .observeOn(Schedulers.newThread()) + .take(numForBatches) + .subscribe(testSubscriber); + testSubscriber.awaitDone(5, TimeUnit.SECONDS); + System.err.println(testSubscriber.values()); + // it should be between the take num and requested batch size across the async boundary + System.out.println("Generated: " + generated.get()); + assertTrue(generated.get() >= numForBatches && generated.get() <= numForBatches + Flowable.bufferSize()); + } + + @Test + public void backpressureWithTakeBefore() { + final AtomicInteger generated = new AtomicInteger(); + Flowable<Integer> flowable = Flowable.fromIterable(new Iterable<Integer>() { + @Override + public Iterator<Integer> iterator() { + return new Iterator<Integer>() { + + @Override + public void remove() { + } + + @Override + public Integer next() { + return generated.getAndIncrement(); + } + + @Override + public boolean hasNext() { + return true; + } + }; + } + }); + + TestSubscriber<Integer> testSubscriber = new TestSubscriber<>(); + flowable + .take(7) + .observeOn(Schedulers.newThread()) + .subscribe(testSubscriber); + + testSubscriber.awaitDone(5, TimeUnit.SECONDS); + testSubscriber.assertValues(0, 1, 2, 3, 4, 5, 6); + assertEquals(7, generated.get()); + } + + @Test + public void queueFullEmitsError() { + final CountDownLatch latch = new CountDownLatch(1); + Flowable<Integer> flowable = Flowable.unsafeCreate(new Publisher<Integer>() { + + @Override + public void subscribe(Subscriber<? super Integer> subscriber) { + subscriber.onSubscribe(new BooleanSubscription()); + for (int i = 0; i < Flowable.bufferSize() + 10; i++) { + subscriber.onNext(i); + } + latch.countDown(); + subscriber.onComplete(); + } + + }); + + TestSubscriberEx<Integer> testSubscriber = new TestSubscriberEx<>(new DefaultSubscriber<Integer>() { + + @Override + public void onComplete() { + + } + + @Override + public void onError(Throwable e) { + + } + + @Override + public void onNext(Integer t) { + // force it to be slow and wait until we have queued everything + try { + latch.await(500, TimeUnit.MILLISECONDS); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + + }); + flowable.observeOn(Schedulers.newThread()).subscribe(testSubscriber); + + testSubscriber.awaitDone(5, TimeUnit.SECONDS); + List<Throwable> errors = testSubscriber.errors(); + assertEquals(1, errors.size()); + System.out.println("Errors: " + errors); + Throwable t = errors.get(0); + if (t instanceof QueueOverflowException) { + // success, we expect this + } else { + if (t.getCause() instanceof QueueOverflowException) { + // this is also okay + } else { + fail("Expecting QueueOverflowException"); + } + } + } + + @Test + public void asyncChild() { + TestSubscriber<Integer> ts = new TestSubscriber<>(); + Flowable.range(0, 100000).observeOn(Schedulers.newThread()).observeOn(Schedulers.newThread()).subscribe(ts); + ts.awaitDone(5, TimeUnit.SECONDS); + ts.assertNoErrors(); + } + + @Test + public void onErrorCutsAheadOfOnNext() { + for (int i = 0; i < 50; i++) { + final PublishProcessor<Long> processor = PublishProcessor.create(); + + final AtomicLong counter = new AtomicLong(); + TestSubscriberEx<Long> ts = new TestSubscriberEx<>(new DefaultSubscriber<Long>() { + + @Override + public void onComplete() { + + } + + @Override + public void onError(Throwable e) { + + } + + @Override + public void onNext(Long t) { + // simulate slow consumer to force backpressure failure + try { + Thread.sleep(1); + } catch (InterruptedException e) { + } + } + + }); + processor.observeOn(Schedulers.computation()).subscribe(ts); + + // this will blow up with backpressure + while (counter.get() < 102400) { + processor.onNext(counter.get()); + counter.incrementAndGet(); + } + + ts.awaitDone(5, TimeUnit.SECONDS); + assertEquals(1, ts.errors().size()); + ts.assertError(MissingBackpressureException.class); + // assert that the values are sequential, that cutting in didn't allow skipping some but emitting others. + // example [0, 1, 2] not [0, 1, 4] + List<Long> onNextEvents = ts.values(); + assertTrue(onNextEvents.isEmpty() || onNextEvents.size() == onNextEvents.get(onNextEvents.size() - 1) + 1); + // we should emit the error without emitting the full buffer size + assertTrue(onNextEvents.size() < Flowable.bufferSize()); + } + } + + /** + * Make sure we get a MissingBackpressureException propagated through when we have a fast temporal (hot) producer. + */ + @Test + public void hotOperatorBackpressure() { + TestSubscriberEx<String> ts = new TestSubscriberEx<>(); + Flowable.interval(0, 1, TimeUnit.MICROSECONDS) + .observeOn(Schedulers.computation()) + .map(new Function<Long, String>() { + + @Override + public String apply(Long t1) { + System.out.println(t1); + try { + Thread.sleep(100); + } catch (InterruptedException e) { + } + return t1 + " slow value"; + } + + }).subscribe(ts); + + ts.awaitDone(5, TimeUnit.SECONDS); + System.out.println("Errors: " + ts.errors()); + assertEquals(1, ts.errors().size()); + assertEquals(MissingBackpressureException.class, ts.errors().get(0).getClass()); + } + + @Test + public void errorPropagatesWhenNoOutstandingRequests() { + Flowable<Long> timer = Flowable.interval(0, 1, TimeUnit.MICROSECONDS) + .doOnEach(new Consumer<Notification<Long>>() { + + @Override + public void accept(Notification<Long> n) { +// System.out.println("BEFORE " + n); + } + + }) + .observeOn(Schedulers.newThread()) + .doOnEach(new Consumer<Notification<Long>>() { + + @Override + public void accept(Notification<Long> n) { + try { + Thread.sleep(100); + } catch (InterruptedException e) { + } +// System.out.println("AFTER " + n); + } + + }); + + TestSubscriberEx<Long> ts = new TestSubscriberEx<>(); + + Flowable.combineLatest(timer, Flowable.<Integer> never(), new BiFunction<Long, Integer, Long>() { + + @Override + public Long apply(Long t1, Integer t2) { + return t1; + } + + }).take(Flowable.bufferSize() * 2).subscribe(ts); + + ts.awaitDone(5, TimeUnit.SECONDS); + assertEquals(1, ts.errors().size()); + assertEquals(MissingBackpressureException.class, ts.errors().get(0).getClass()); + } + + @Test + public void requestOverflow() throws InterruptedException { + + final CountDownLatch latch = new CountDownLatch(1); + final AtomicInteger count = new AtomicInteger(); + Flowable.range(1, 100).observeOn(Schedulers.computation()) + .subscribe(new DefaultSubscriber<Integer>() { + + boolean first = true; + + @Override + public void onStart() { + request(2); + } + + @Override + public void onComplete() { + latch.countDown(); + } + + @Override + public void onError(Throwable e) { + + } + + @Override + public void onNext(Integer t) { + count.incrementAndGet(); + if (first) { + request(Long.MAX_VALUE - 1); + request(Long.MAX_VALUE - 1); + request(10); + first = false; + } + } + }); + assertTrue(latch.await(10, TimeUnit.SECONDS)); + assertEquals(100, count.get()); + + } + + @Test + public void noMoreRequestsAfterUnsubscribe() throws InterruptedException { + final CountDownLatch latch = new CountDownLatch(1); + final List<Long> requests = Collections.synchronizedList(new ArrayList<>()); + Flowable.range(1, 1000000) + .doOnRequest(new LongConsumer() { + + @Override + public void accept(long n) { + requests.add(n); + } + }) + .observeOn(Schedulers.io()) + .subscribe(new DefaultSubscriber<Integer>() { + + @Override + public void onStart() { + request(1); + } + + @Override + public void onComplete() { + } + + @Override + public void onError(Throwable e) { + } + + @Override + public void onNext(Integer t) { + cancel(); + latch.countDown(); + } + }); + assertTrue(latch.await(10, TimeUnit.SECONDS)); + // FIXME observeOn requests bufferSize at first always + assertEquals(Arrays.asList(128L), requests); + } + + @Test + public void errorDelayed() { + TestScheduler s = new TestScheduler(); + + Flowable<Integer> source = Flowable.just(1, 2, 3) + .concatWith(Flowable.<Integer>error(new TestException())); + + TestSubscriber<Integer> ts = TestSubscriber.create(0); + + source.observeOn(s, true).subscribe(ts); + + ts.assertNoValues(); + ts.assertNoErrors(); + ts.assertNotComplete(); + + s.advanceTimeBy(1, TimeUnit.SECONDS); + + ts.assertNoValues(); + ts.assertNoErrors(); + ts.assertNotComplete(); + + ts.request(1); + s.advanceTimeBy(1, TimeUnit.SECONDS); + + ts.assertValues(1); + ts.assertNoErrors(); + ts.assertNotComplete(); + + ts.request(3); // requesting 2 doesn't switch to the error() source for some reason in concat. + s.advanceTimeBy(1, TimeUnit.SECONDS); + + ts.assertValues(1, 2, 3); + ts.assertError(TestException.class); + ts.assertNotComplete(); + } + + @Test + public void errorDelayedAsync() { + Flowable<Integer> source = Flowable.just(1, 2, 3) + .concatWith(Flowable.<Integer>error(new TestException())); + + TestSubscriber<Integer> ts = TestSubscriber.create(); + + source.observeOn(Schedulers.computation(), true).subscribe(ts); + + ts.awaitDone(2, TimeUnit.SECONDS); + ts.assertValues(1, 2, 3); + ts.assertError(TestException.class); + ts.assertNotComplete(); + } + + @Test + public void requestExactCompletesImmediately() { + TestSubscriber<Integer> ts = TestSubscriber.create(0); + + TestScheduler test = new TestScheduler(); + + Flowable.range(1, 10).observeOn(test).subscribe(ts); + + test.advanceTimeBy(1, TimeUnit.SECONDS); + + ts.assertNoValues(); + ts.assertNoErrors(); + ts.assertNotComplete(); + + ts.request(10); + + test.advanceTimeBy(1, TimeUnit.SECONDS); + + ts.assertValueCount(10); + ts.assertNoErrors(); + ts.assertComplete(); + } + + @Test + public void fixedReplenishPattern() { + TestSubscriber<Integer> ts = TestSubscriber.create(0); + + TestScheduler test = new TestScheduler(); + + final List<Long> requests = new ArrayList<>(); + + Flowable.range(1, 100) + .doOnRequest(new LongConsumer() { + @Override + public void accept(long v) { + requests.add(v); + } + }) + .observeOn(test, false, 16).subscribe(ts); + + test.advanceTimeBy(1, TimeUnit.SECONDS); + ts.request(20); + test.advanceTimeBy(1, TimeUnit.SECONDS); + ts.request(10); + test.advanceTimeBy(1, TimeUnit.SECONDS); + ts.request(50); + test.advanceTimeBy(1, TimeUnit.SECONDS); + ts.request(35); + test.advanceTimeBy(1, TimeUnit.SECONDS); + + ts.assertValueCount(100); + ts.assertComplete(); + ts.assertNoErrors(); + + assertEquals(Arrays.asList(16L, 12L, 12L, 12L, 12L, 12L, 12L, 12L, 12L), requests); + } + + @Test + public void bufferSizesWork() { + for (int i = 1; i <= 1024; i = i * 2) { + TestSubscriber<Integer> ts = TestSubscriber.create(); + + Flowable.range(1, 1000 * 1000).observeOn(Schedulers.computation(), false, i) + .subscribe(ts); + + ts.awaitDone(5, TimeUnit.SECONDS); + ts.assertValueCount(1000 * 1000); + ts.assertComplete(); + ts.assertNoErrors(); + } + } + + @Test + public void synchronousRebatching() { + final List<Long> requests = new ArrayList<>(); + + TestSubscriber<Integer> ts = new TestSubscriber<>(); + + Flowable.range(1, 50) + .doOnRequest(new LongConsumer() { + @Override + public void accept(long r) { + requests.add(r); + } + }) + .rebatchRequests(20) + .subscribe(ts); + + ts.assertValueCount(50); + ts.assertNoErrors(); + ts.assertComplete(); + + assertEquals(Arrays.asList(20L, 15L, 15L, 15L), requests); + } + + @Test + public void rebatchRequestsArgumentCheck() { + try { + Flowable.never().rebatchRequests(-99); + fail("Didn't throw IAE"); + } catch (IllegalArgumentException ex) { + assertEquals("bufferSize > 0 required but it was -99", ex.getMessage()); + } + } + + @Test + public void delayError() { + Flowable.range(1, 5).concatWith(Flowable.<Integer>error(new TestException())) + .observeOn(Schedulers.computation(), true) + .doOnNext(new Consumer<Integer>() { + @Override + public void accept(Integer v) throws Exception { + if (v == 1) { + Thread.sleep(100); + } + } + }) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertFailure(TestException.class, 1, 2, 3, 4, 5); + } + + @Test + public void conditionalConsumer() { + Flowable.range(1, 5) + .observeOn(Schedulers.single()) + .filter(new Predicate<Integer>() { + @Override + public boolean test(Integer v) throws Exception { + return v % 2 == 0; + } + }) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(2, 4); + } + + @Test + public void take() { + Flowable.range(1, 5) + .observeOn(Schedulers.single()) + .take(3) + .take(3) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(1, 2, 3); + } + + @Test + public void cancelCleanup() { + TestSubscriber<Integer> ts = Flowable.range(1, 5) + .observeOn(Schedulers.single()) + .test(0L); + + ts.cancel(); + } + + @Test + public void conditionalConsumerFused() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<Integer>().setInitialFusionMode(QueueFuseable.ANY); + + Flowable.range(1, 5) + .observeOn(Schedulers.single()) + .filter(new Predicate<Integer>() { + @Override + public boolean test(Integer v) throws Exception { + return v % 2 == 0; + } + }) + .subscribe(ts); + + ts + .assertFuseable() + .assertFusionMode(QueueFuseable.ASYNC) + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(2, 4); + } + + @Test + public void conditionalConsumerFusedReject() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<Integer>().setInitialFusionMode(QueueFuseable.SYNC); + + Flowable.range(1, 5) + .observeOn(Schedulers.single()) + .filter(new Predicate<Integer>() { + @Override + public boolean test(Integer v) throws Exception { + return v % 2 == 0; + } + }) + .subscribe(ts); + + ts + .assertFuseable() + .assertFusionMode(QueueFuseable.NONE) + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(2, 4); + } + + @Test + public void requestOne() throws Exception { + TestSubscriberEx<Integer> ts = Flowable.range(1, 5) + .observeOn(Schedulers.single()) + .to(TestHelper.<Integer>testSubscriber(1L)); + + Thread.sleep(100); + + ts.assertSubscribed().assertValue(1).assertNoErrors().assertNotComplete(); + } + + @Test + public void requestOneConditional() throws Exception { + TestSubscriberEx<Integer> ts = Flowable.range(1, 5) + .observeOn(Schedulers.single()) + .filter(Functions.alwaysTrue()) + .to(TestHelper.<Integer>testSubscriber(1L)); + + Thread.sleep(100); + + ts.assertSubscribed().assertValue(1).assertNoErrors().assertNotComplete(); + } + + @Test + public void conditionalConsumerFusedAsync() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<Integer>().setInitialFusionMode(QueueFuseable.ANY); + + UnicastProcessor<Integer> up = UnicastProcessor.create(); + + up + .observeOn(Schedulers.single()) + .filter(new Predicate<Integer>() { + @Override + public boolean test(Integer v) throws Exception { + return v % 2 == 0; + } + }) + .subscribe(ts); + + up.onNext(1); + up.onNext(2); + up.onNext(3); + up.onNext(4); + up.onNext(5); + up.onComplete(); + + ts + .assertFuseable() + .assertFusionMode(QueueFuseable.ASYNC) + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(2, 4); + } + + @Test + public void conditionalConsumerHidden() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<Integer>().setInitialFusionMode(QueueFuseable.ANY); + + Flowable.range(1, 5).hide() + .observeOn(Schedulers.single()) + .filter(new Predicate<Integer>() { + @Override + public boolean test(Integer v) throws Exception { + return v % 2 == 0; + } + }) + .subscribe(ts); + + ts + .assertFuseable() + .assertFusionMode(QueueFuseable.ASYNC) + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(2, 4); + } + + @Test + public void conditionalConsumerBarrier() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<Integer>().setInitialFusionMode(QueueFuseable.ANY); + + Flowable.range(1, 5) + .map(Functions.<Integer>identity()) + .observeOn(Schedulers.single()) + .filter(new Predicate<Integer>() { + @Override + public boolean test(Integer v) throws Exception { + return v % 2 == 0; + } + }) + .subscribe(ts); + + ts + .assertFuseable() + .assertFusionMode(QueueFuseable.ASYNC) + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(2, 4); + } + + @Test + public void dispose() { + TestHelper.checkDisposed(PublishProcessor.create().observeOn(new TestScheduler())); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeFlowable(new Function<Flowable<Object>, Flowable<Object>>() { + @Override + public Flowable<Object> apply(Flowable<Object> f) throws Exception { + return f.observeOn(new TestScheduler()); + } + }); + } + + @Test + public void doubleOnSubscribeConditional() { + TestHelper.checkDoubleOnSubscribeFlowable(new Function<Flowable<Object>, Flowable<Object>>() { + @Override + public Flowable<Object> apply(Flowable<Object> f) throws Exception { + return f.observeOn(new TestScheduler()).compose(TestHelper.conditional()); + } + }); + } + + @Test + public void badSource() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + TestScheduler scheduler = new TestScheduler(); + TestSubscriber<Integer> ts = new Flowable<Integer>() { + @Override + protected void subscribeActual(Subscriber<? super Integer> subscriber) { + subscriber.onSubscribe(new BooleanSubscription()); + subscriber.onComplete(); + subscriber.onNext(1); + subscriber.onError(new TestException()); + subscriber.onComplete(); + } + } + .observeOn(scheduler) + .test(); + + scheduler.triggerActions(); + + ts.assertResult(); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void inputSyncFused() { + Flowable.range(1, 5) + .observeOn(Schedulers.single()) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(1, 2, 3, 4, 5); + } + + @Test + public void inputAsyncFused() { + UnicastProcessor<Integer> up = UnicastProcessor.create(); + + TestSubscriber<Integer> ts = up.observeOn(Schedulers.single()).test(); + + TestHelper.emit(up, 1, 2, 3, 4, 5); + + ts + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(1, 2, 3, 4, 5); + } + + @Test + public void inputAsyncFusedError() { + UnicastProcessor<Integer> up = UnicastProcessor.create(); + + TestSubscriber<Integer> ts = up.observeOn(Schedulers.single()).test(); + + up.onError(new TestException()); + + ts + .awaitDone(5, TimeUnit.SECONDS) + .assertFailure(TestException.class); + } + + @Test + public void inputAsyncFusedErrorDelayed() { + UnicastProcessor<Integer> up = UnicastProcessor.create(); + + TestSubscriber<Integer> ts = up.observeOn(Schedulers.single(), true).test(); + + up.onError(new TestException()); + + ts + .awaitDone(5, TimeUnit.SECONDS) + .assertFailure(TestException.class); + } + + @Test + public void outputFused() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<Integer>().setInitialFusionMode(QueueFuseable.ANY); + + Flowable.range(1, 5).hide() + .observeOn(Schedulers.single()) + .subscribe(ts); + + ts.assertFusionMode(QueueFuseable.ASYNC) + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(1, 2, 3, 4, 5); + } + + @Test + public void outputFusedReject() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<Integer>().setInitialFusionMode(QueueFuseable.SYNC); + + Flowable.range(1, 5).hide() + .observeOn(Schedulers.single()) + .subscribe(ts); + + ts.assertFusionMode(QueueFuseable.NONE) + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(1, 2, 3, 4, 5); + } + + @Test + public void inputOutputAsyncFusedError() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<Integer>().setInitialFusionMode(QueueFuseable.ANY); + + UnicastProcessor<Integer> up = UnicastProcessor.create(); + + up.observeOn(Schedulers.single()) + .subscribe(ts); + + up.onError(new TestException()); + + ts + .awaitDone(5, TimeUnit.SECONDS) + .assertFailure(TestException.class); + + ts.assertFusionMode(QueueFuseable.ASYNC) + .awaitDone(5, TimeUnit.SECONDS) + .assertFailure(TestException.class); + } + + @Test + public void inputOutputAsyncFusedErrorDelayed() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<Integer>().setInitialFusionMode(QueueFuseable.ANY); + + UnicastProcessor<Integer> up = UnicastProcessor.create(); + + up.observeOn(Schedulers.single(), true) + .subscribe(ts); + + up.onError(new TestException()); + + ts + .awaitDone(5, TimeUnit.SECONDS) + .assertFailure(TestException.class); + + ts.assertFusionMode(QueueFuseable.ASYNC) + .awaitDone(5, TimeUnit.SECONDS) + .assertFailure(TestException.class); + } + + @Test + public void outputFusedCancelReentrant() throws Exception { + final UnicastProcessor<Integer> up = UnicastProcessor.create(); + + final CountDownLatch cdl = new CountDownLatch(1); + + up.observeOn(Schedulers.single()) + .subscribe(new FlowableSubscriber<Integer>() { + Subscription upstream; + int count; + @Override + public void onSubscribe(Subscription s) { + this.upstream = s; + ((QueueSubscription<?>)s).requestFusion(QueueFuseable.ANY); + } + + @Override + public void onNext(Integer value) { + if (++count == 1) { + up.onNext(2); + upstream.cancel(); + cdl.countDown(); + } + } + + @Override + public void onError(Throwable e) { + + } + + @Override + public void onComplete() { + + } + }); + + up.onNext(1); + + cdl.await(); + } + + @Test + public void nonFusedPollThrows() { + new Flowable<Integer>() { + @Override + protected void subscribeActual(Subscriber<? super Integer> subscriber) { + subscriber.onSubscribe(new BooleanSubscription()); + + @SuppressWarnings("unchecked") + BaseObserveOnSubscriber<Integer> oo = (BaseObserveOnSubscriber<Integer>)subscriber; + + oo.sourceMode = QueueFuseable.SYNC; + oo.requested.lazySet(1); + oo.queue = new SimpleQueue<Integer>() { + + @Override + public boolean offer(Integer value) { + return false; + } + + @Override + public boolean offer(Integer v1, Integer v2) { + return false; + } + + @Nullable + @Override + public Integer poll() throws Exception { + throw new TestException(); + } + + @Override + public boolean isEmpty() { + return false; + } + + @Override + public void clear() { + } + }; + + oo.clear(); + + oo.trySchedule(); + } + } + .observeOn(Schedulers.single()) + .test(0L) + .awaitDone(5, TimeUnit.SECONDS) + .assertFailure(TestException.class); + } + + @Test + public void conditionalNonFusedPollThrows() { + new Flowable<Integer>() { + @Override + protected void subscribeActual(Subscriber<? super Integer> subscriber) { + subscriber.onSubscribe(new BooleanSubscription()); + + @SuppressWarnings("unchecked") + BaseObserveOnSubscriber<Integer> oo = (BaseObserveOnSubscriber<Integer>)subscriber; + + oo.sourceMode = QueueFuseable.SYNC; + oo.requested.lazySet(1); + oo.queue = new SimpleQueue<Integer>() { + + @Override + public boolean offer(Integer value) { + return false; + } + + @Override + public boolean offer(Integer v1, Integer v2) { + return false; + } + + @Nullable + @Override + public Integer poll() throws Exception { + throw new TestException(); + } + + @Override + public boolean isEmpty() { + return false; + } + + @Override + public void clear() { + } + }; + + oo.clear(); + + oo.trySchedule(); + } + } + .observeOn(Schedulers.single()) + .filter(Functions.alwaysTrue()) + .test(0L) + .awaitDone(5, TimeUnit.SECONDS) + .assertFailure(TestException.class); + } + + @Test + public void asycFusedPollThrows() { + new Flowable<Integer>() { + @Override + protected void subscribeActual(Subscriber<? super Integer> subscriber) { + subscriber.onSubscribe(new BooleanSubscription()); + + @SuppressWarnings("unchecked") + BaseObserveOnSubscriber<Integer> oo = (BaseObserveOnSubscriber<Integer>)subscriber; + + oo.sourceMode = QueueFuseable.ASYNC; + oo.requested.lazySet(1); + oo.queue = new SimpleQueue<Integer>() { + + @Override + public boolean offer(Integer value) { + return false; + } + + @Override + public boolean offer(Integer v1, Integer v2) { + return false; + } + + @Nullable + @Override + public Integer poll() throws Exception { + throw new TestException(); + } + + @Override + public boolean isEmpty() { + return false; + } + + @Override + public void clear() { + } + }; + + oo.clear(); + + oo.trySchedule(); + } + } + .observeOn(Schedulers.single()) + .test(0L) + .awaitDone(5, TimeUnit.SECONDS) + .assertFailure(TestException.class); + } + + @Test + public void conditionalAsyncFusedPollThrows() { + new Flowable<Integer>() { + @Override + protected void subscribeActual(Subscriber<? super Integer> subscriber) { + subscriber.onSubscribe(new BooleanSubscription()); + + @SuppressWarnings("unchecked") + BaseObserveOnSubscriber<Integer> oo = (BaseObserveOnSubscriber<Integer>)subscriber; + + oo.sourceMode = QueueFuseable.ASYNC; + oo.requested.lazySet(1); + oo.queue = new SimpleQueue<Integer>() { + + @Override + public boolean offer(Integer value) { + return false; + } + + @Override + public boolean offer(Integer v1, Integer v2) { + return false; + } + + @Nullable + @Override + public Integer poll() throws Exception { + throw new TestException(); + } + + @Override + public boolean isEmpty() { + return false; + } + + @Override + public void clear() { + } + }; + + oo.clear(); + + oo.trySchedule(); + } + } + .observeOn(Schedulers.single()) + .filter(Functions.alwaysTrue()) + .test(0L) + .awaitDone(5, TimeUnit.SECONDS) + .assertFailure(TestException.class); + } + + @Test + public void trampolineScheduler() { + Flowable.just(1) + .observeOn(Schedulers.trampoline()) + .test() + .assertResult(1); + } + + @Test + public void conditionalNormal() { + Flowable.range(1, 1000).hide() + .observeOn(Schedulers.single()) + .filter(new Predicate<Integer>() { + @Override + public boolean test(Integer v) throws Exception { + return v % 2 == 0; + } + }) + .take(250) + .to(TestHelper.<Integer>testConsumer()) + .awaitDone(5, TimeUnit.SECONDS) + .assertSubscribed() + .assertValueCount(250) + .assertNoErrors() + .assertComplete(); + } + + @Test + public void syncFusedCancelAfterRequest() { + final TestSubscriber<Integer> ts = new TestSubscriber<Integer>(2L) { + @Override + public void onNext(Integer t) { + super.onNext(t); + if (t == 2) { + cancel(); + onComplete(); + } + } + }; + + Flowable.range(1, 3) + .observeOn(Schedulers.single()) + .subscribe(ts); + + ts + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(1, 2); + } + + @Test + public void syncFusedCancelAfterRequest2() { + final TestSubscriber<Integer> ts = new TestSubscriber<>(2L); + + Flowable.range(1, 2) + .observeOn(Schedulers.single()) + .subscribe(ts); + + ts + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(1, 2); + } + + @Test + public void syncFusedCancelAfterRequestConditional() { + final TestSubscriber<Integer> ts = new TestSubscriber<Integer>(2L) { + @Override + public void onNext(Integer t) { + super.onNext(t); + if (t == 2) { + cancel(); + onComplete(); + } + } + }; + + Flowable.range(1, 3) + .observeOn(Schedulers.single()) + .filter(Functions.alwaysTrue()) + .subscribe(ts); + + ts + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(1, 2); + } + + @Test + public void syncFusedCancelAfterRequestConditional2() { + final TestSubscriber<Integer> ts = new TestSubscriber<>(2L); + + Flowable.range(1, 2) + .observeOn(Schedulers.single()) + .filter(Functions.alwaysTrue()) + .subscribe(ts); + + ts + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(1, 2); + } + + @Test + public void nonFusedCancelAfterRequestConditional2() { + final TestSubscriber<Integer> ts = new TestSubscriber<>(2L); + + Flowable.range(1, 2).hide() + .observeOn(Schedulers.single()) + .filter(Functions.alwaysTrue()) + .subscribe(ts); + + ts + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(1, 2); + } + + @Test + public void doubleObserveOn() { + Flowable.just(1).hide() + .observeOn(Schedulers.computation()) + .observeOn(Schedulers.single()) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(1); + } + + @Test + public void doubleObserveOnError() { + Flowable.error(new TestException()) + .observeOn(Schedulers.computation()) + .observeOn(Schedulers.single()) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertFailure(TestException.class); + } + + @Test + public void doubleObserveOnConditional() { + Flowable.just(1).hide() + .observeOn(Schedulers.computation()) + .distinct() + .observeOn(Schedulers.single()) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(1); + } + + @Test + public void doubleObserveOnErrorConditional() { + Flowable.error(new TestException()) + .observeOn(Schedulers.computation()) + .distinct() + .observeOn(Schedulers.single()) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertFailure(TestException.class); + } + + @Test + public void request1Conditional() { + Flowable.range(1, 10).hide() + .observeOn(ImmediateThinScheduler.INSTANCE) + .filter(Functions.alwaysTrue()) + .test(1L) + .assertValue(1); + } + + @Test + public void backFusedConditional() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<Integer>().setInitialFusionMode(QueueFuseable.ANY); + + Flowable.range(1, 100).hide() + .observeOn(ImmediateThinScheduler.INSTANCE) + .filter(Functions.alwaysTrue()) + .subscribe(ts); + + ts.assertFusionMode(QueueFuseable.ASYNC) + .assertValueCount(100) + .assertComplete() + .assertNoErrors(); + } + + @Test + public void backFusedErrorConditional() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<Integer>().setInitialFusionMode(QueueFuseable.ANY); + + Flowable.<Integer>error(new TestException()) + .observeOn(ImmediateThinScheduler.INSTANCE) + .filter(Functions.alwaysTrue()) + .subscribe(ts); + + ts.assertFusionMode(QueueFuseable.ASYNC) + .assertFailure(TestException.class); + } + + @Test + public void backFusedCancelConditional() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final TestSubscriberEx<Integer> ts = new TestSubscriberEx<Integer>().setInitialFusionMode(QueueFuseable.ANY); + + final TestScheduler scheduler = new TestScheduler(); + + Flowable.just(1).hide() + .observeOn(scheduler) + .filter(Functions.alwaysTrue()) + .subscribe(ts); + + Runnable r1 = new Runnable() { + @Override + public void run() { + ts.cancel(); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + scheduler.triggerActions(); + } + }; + + TestHelper.race(r1, r2); + + ts.assertFusionMode(QueueFuseable.ASYNC); + + if (ts.values().size() != 0) { + ts.assertResult(1); + } + } + } + + @Test + public void syncFusedRequestOneByOneConditional() { + Flowable.range(1, 5) + .observeOn(ImmediateThinScheduler.INSTANCE) + .filter(Functions.alwaysTrue()) + .rebatchRequests(1) + .test() + .assertResult(1, 2, 3, 4, 5); + } + + public static final class DisposeTrackingScheduler extends Scheduler { + + public final AtomicInteger disposedCount = new AtomicInteger(); + + @Override + public Worker createWorker() { + return new TrackingWorker(); + } + + final class TrackingWorker extends Scheduler.Worker { + + @Override + public void dispose() { + disposedCount.getAndIncrement(); + } + + @Override + public boolean isDisposed() { + return false; + } + + @Override + public Disposable schedule(Runnable run, long delay, + TimeUnit unit) { + run.run(); + return Disposable.empty(); + } + } + } + + @Test + public void workerNotDisposedPrematurelyNormalInNormalOut() { + DisposeTrackingScheduler s = new DisposeTrackingScheduler(); + + Flowable.concat( + Flowable.just(1).hide().observeOn(s), + Flowable.just(2) + ) + .test() + .assertResult(1, 2); + + assertEquals(1, s.disposedCount.get()); + } + + @Test + public void workerNotDisposedPrematurelySyncInNormalOut() { + DisposeTrackingScheduler s = new DisposeTrackingScheduler(); + + Flowable.concat( + Flowable.just(1).observeOn(s), + Flowable.just(2) + ) + .test() + .assertResult(1, 2); + + assertEquals(1, s.disposedCount.get()); + } + + @Test + public void workerNotDisposedPrematurelyAsyncInNormalOut() { + DisposeTrackingScheduler s = new DisposeTrackingScheduler(); + + UnicastProcessor<Integer> up = UnicastProcessor.create(); + up.onNext(1); + up.onComplete(); + + Flowable.concat( + up.observeOn(s), + Flowable.just(2) + ) + .test() + .assertResult(1, 2); + + assertEquals(1, s.disposedCount.get()); + } + + static final class TestSubscriberFusedCanceling + extends TestSubscriberEx<Integer> { + + TestSubscriberFusedCanceling() { + super(); + initialFusionMode = QueueFuseable.ANY; + } + + @Override + public void onComplete() { + cancel(); + super.onComplete(); + } + } + + @Test + public void workerNotDisposedPrematurelyNormalInAsyncOut() { + DisposeTrackingScheduler s = new DisposeTrackingScheduler(); + + TestSubscriberEx<Integer> ts = new TestSubscriberFusedCanceling(); + + Flowable.just(1).hide().observeOn(s).subscribe(ts); + + assertEquals(1, s.disposedCount.get()); + } + + @Test + public void workerNotDisposedPrematurelyNormalInNormalOutConditional() { + DisposeTrackingScheduler s = new DisposeTrackingScheduler(); + + Flowable.concat( + Flowable.just(1).hide().observeOn(s).filter(Functions.alwaysTrue()), + Flowable.just(2) + ) + .test() + .assertResult(1, 2); + + assertEquals(1, s.disposedCount.get()); + } + + @Test + public void workerNotDisposedPrematurelySyncInNormalOutConditional() { + DisposeTrackingScheduler s = new DisposeTrackingScheduler(); + + Flowable.concat( + Flowable.just(1).observeOn(s).filter(Functions.alwaysTrue()), + Flowable.just(2) + ) + .test() + .assertResult(1, 2); + + assertEquals(1, s.disposedCount.get()); + } + + @Test + public void workerNotDisposedPrematurelyAsyncInNormalOutConditional() { + DisposeTrackingScheduler s = new DisposeTrackingScheduler(); + + UnicastProcessor<Integer> up = UnicastProcessor.create(); + up.onNext(1); + up.onComplete(); + + Flowable.concat( + up.observeOn(s).filter(Functions.alwaysTrue()), + Flowable.just(2) + ) + .test() + .assertResult(1, 2); + + assertEquals(1, s.disposedCount.get()); + } + + @Test + public void workerNotDisposedPrematurelyNormalInAsyncOutConditional() { + DisposeTrackingScheduler s = new DisposeTrackingScheduler(); + + TestSubscriberEx<Integer> ts = new TestSubscriberFusedCanceling(); + + Flowable.just(1).hide().observeOn(s).filter(Functions.alwaysTrue()).subscribe(ts); + + assertEquals(1, s.disposedCount.get()); + } + + @Test + public void fusedNoConcurrentCleanDueToCancel() { + for (int j = 0; j < TestHelper.RACE_LONG_LOOPS; j++) { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + final UnicastProcessor<Integer> up = UnicastProcessor.create(); + + TestObserver<Integer> to = up.hide() + .observeOn(Schedulers.io()) + .observeOn(Schedulers.single()) + .unsubscribeOn(Schedulers.computation()) + .firstOrError() + .test(); + + for (int i = 0; up.hasSubscribers() && i < 10000; i++) { + up.onNext(i); + } + + to + .awaitDone(5, TimeUnit.SECONDS) + ; + + if (!errors.isEmpty()) { + throw new CompositeException(errors); + } + + to.assertResult(0); + } finally { + RxJavaPlugins.reset(); + } + } + } + + @Test + public void fusedParallelProcessing() { + Flowable.range(0, 500000) + .subscribeOn(Schedulers.single()) + .observeOn(Schedulers.computation()) + .parallel() + .runOn(Schedulers.computation()) + .map(Functions.<Integer>identity()) + .sequential() + .test() + .awaitDone(20, TimeUnit.SECONDS) + .assertValueCount(500000) + .assertComplete() + .assertNoErrors(); + } + + @Test + public void badRequest() { + TestHelper.assertBadRequestReported(Flowable.never().observeOn(ImmediateThinScheduler.INSTANCE)); + } + + @Test + public void syncFusedCancelAfterPoll() { + TestSubscriber<Integer> ts = new TestSubscriber<>(); + + Flowable.just(1) + .map(v -> { + ts.cancel(); + return v + 1; + }) + .compose(TestHelper.flowableStripBoundary()) + .observeOn(ImmediateThinScheduler.INSTANCE) + .subscribe(ts); + + ts.assertEmpty(); + } + + @Test + public void syncFusedCancelAfterPollConditional() { + TestSubscriber<Integer> ts = new TestSubscriber<>(); + + Flowable.just(1) + .map(v -> { + ts.cancel(); + return v + 1; + }) + .compose(TestHelper.flowableStripBoundary()) + .observeOn(ImmediateThinScheduler.INSTANCE) + .compose(TestHelper.conditional()) + .subscribe(ts); + + ts.assertEmpty(); + } + + @Test + public void backFusedMoreWork() { + final TestSubscriberEx<Integer> ts = new TestSubscriberEx<Integer>().setInitialFusionMode(QueueFuseable.ANY); + + PublishProcessor<Integer> pp = PublishProcessor.create(); + + pp.observeOn(ImmediateThinScheduler.INSTANCE) + .doOnNext(v -> { + if (v == 1) { + pp.onNext(2); + } + }) + .subscribe(ts); + + pp.onNext(1); + + ts.assertValuesOnly(1, 2); + } + + @Test + public void moreWorkInRunAsync() { + final TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(); + + PublishProcessor<Integer> pp = PublishProcessor.create(); + + pp.observeOn(ImmediateThinScheduler.INSTANCE) + .doOnNext(v -> { + if (v == 1) { + pp.onNext(2); + } + }) + .subscribe(ts); + + pp.onNext(1); + + ts.assertValuesOnly(1, 2); + } + + @Test + public void backFusedConditionalMoreWork() { + final TestSubscriberEx<Integer> ts = new TestSubscriberEx<Integer>().setInitialFusionMode(QueueFuseable.ANY); + + PublishProcessor<Integer> pp = PublishProcessor.create(); + + pp.observeOn(ImmediateThinScheduler.INSTANCE) + .doOnNext(v -> { + if (v == 1) { + pp.onNext(2); + } + }) + .compose(TestHelper.conditional()) + .subscribe(ts); + + pp.onNext(1); + + ts.assertValuesOnly(1, 2); + } + + @Test + public void conditionalMoreWorkInRunAsync() { + final TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(); + + PublishProcessor<Integer> pp = PublishProcessor.create(); + + pp.observeOn(ImmediateThinScheduler.INSTANCE) + .doOnNext(v -> { + if (v == 1) { + pp.onNext(2); + } + }) + .compose(TestHelper.conditional()) + .subscribe(ts); + + pp.onNext(1); + + ts.assertValuesOnly(1, 2); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableOnBackpressureBufferStrategyTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableOnBackpressureBufferStrategyTest.java new file mode 100644 index 0000000000..217162c206 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableOnBackpressureBufferStrategyTest.java @@ -0,0 +1,325 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import static io.reactivex.rxjava3.core.BackpressureOverflowStrategy.*; +import static io.reactivex.rxjava3.internal.functions.Functions.EMPTY_ACTION; +import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.Test; +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.internal.subscriptions.BooleanSubscription; +import io.reactivex.rxjava3.processors.PublishProcessor; +import io.reactivex.rxjava3.subscribers.*; +import io.reactivex.rxjava3.testsupport.*; + +public class FlowableOnBackpressureBufferStrategyTest extends RxJavaTest { + + @Test + public void backpressureWithBufferDropOldest() throws InterruptedException { + int bufferSize = 3; + final AtomicInteger droppedCount = new AtomicInteger(0); + Action incrementOnDrop = new Action() { + @Override + public void run() throws Exception { + droppedCount.incrementAndGet(); + } + }; + TestSubscriber<Long> ts = createTestSubscriber(); + Flowable.fromPublisher(send500ValuesAndComplete.onBackpressureBuffer(bufferSize, incrementOnDrop, DROP_OLDEST)) + .subscribe(ts); + // we request 10 but only 3 should come from the buffer + ts.request(10); + ts.awaitDone(5, TimeUnit.SECONDS); + assertEquals(bufferSize, ts.values().size()); + ts.assertNoErrors(); + assertEquals(497, ts.values().get(0).intValue()); + assertEquals(498, ts.values().get(1).intValue()); + assertEquals(499, ts.values().get(2).intValue()); + assertEquals(droppedCount.get(), 500 - bufferSize); + } + + private TestSubscriber<Long> createTestSubscriber() { + return new TestSubscriber<>(new DefaultSubscriber<Long>() { + + @Override + protected void onStart() { + } + + @Override + public void onComplete() { + } + + @Override + public void onError(Throwable e) { + } + + @Override + public void onNext(Long t) { + } + + }, 0L); + } + + @Test + public void backpressureWithBufferDropLatest() throws InterruptedException { + int bufferSize = 3; + final AtomicInteger droppedCount = new AtomicInteger(0); + Action incrementOnDrop = new Action() { + @Override + public void run() throws Exception { + droppedCount.incrementAndGet(); + } + }; + TestSubscriber<Long> ts = createTestSubscriber(); + Flowable.fromPublisher(send500ValuesAndComplete.onBackpressureBuffer(bufferSize, incrementOnDrop, DROP_LATEST)) + .subscribe(ts); + // we request 10 but only 3 should come from the buffer + ts.request(10); + ts.awaitDone(5, TimeUnit.SECONDS); + assertEquals(bufferSize, ts.values().size()); + ts.assertNoErrors(); + assertEquals(0, ts.values().get(0).intValue()); + assertEquals(1, ts.values().get(1).intValue()); + assertEquals(499, ts.values().get(2).intValue()); + assertEquals(droppedCount.get(), 500 - bufferSize); + } + + private static final Flowable<Long> send500ValuesAndComplete = Flowable.unsafeCreate(new Publisher<Long>() { + @Override + public void subscribe(Subscriber<? super Long> s) { + BooleanSubscription bs = new BooleanSubscription(); + s.onSubscribe(bs); + long i = 0; + while (!bs.isCancelled() && i < 500) { + s.onNext(i++); + } + if (!bs.isCancelled()) { + s.onComplete(); + } + } + }); + + @Test(expected = IllegalArgumentException.class) + public void backpressureBufferNegativeCapacity() throws InterruptedException { + Flowable.empty().onBackpressureBuffer(-1, EMPTY_ACTION , DROP_OLDEST); + } + + @Test(expected = IllegalArgumentException.class) + public void backpressureBufferZeroCapacity() throws InterruptedException { + Flowable.empty().onBackpressureBuffer(0, EMPTY_ACTION , DROP_OLDEST); + } + + @Test + public void dispose() { + TestHelper.checkDisposed(Flowable.just(1) + .onBackpressureBuffer(16, Functions.EMPTY_ACTION, BackpressureOverflowStrategy.ERROR)); + } + + @Test + public void error() { + Flowable + .error(new TestException()) + .onBackpressureBuffer(16, Functions.EMPTY_ACTION, BackpressureOverflowStrategy.ERROR) + .test() + .assertFailure(TestException.class); + } + + @Test + public void overflowError() { + Flowable.range(1, 20) + .onBackpressureBuffer(8, Functions.EMPTY_ACTION, BackpressureOverflowStrategy.ERROR) + .test(0L) + .assertFailure(MissingBackpressureException.class); + } + + @Test + public void badSource() { + TestHelper.checkBadSourceFlowable(new Function<Flowable<Object>, Object>() { + @Override + public Object apply(Flowable<Object> f) throws Exception { + return f.onBackpressureBuffer(8, Functions.EMPTY_ACTION, BackpressureOverflowStrategy.ERROR); + } + }, false, 1, 1, 1); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeFlowable(new Function<Flowable<Object>, Flowable<Object>>() { + @Override + public Flowable<Object> apply(Flowable<Object> f) throws Exception { + return f.onBackpressureBuffer(8, Functions.EMPTY_ACTION, BackpressureOverflowStrategy.ERROR); + } + }); + } + + @Test + public void overflowCrashes() { + Flowable.range(1, 20) + .onBackpressureBuffer(8, new Action() { + @Override + public void run() throws Exception { + throw new TestException(); + } + }, BackpressureOverflowStrategy.DROP_OLDEST) + .test(0L) + .assertFailure(TestException.class); + } + + @Test + public void badRequest() { + TestHelper.assertBadRequestReported(Flowable.just(1) + .onBackpressureBuffer(16, Functions.EMPTY_ACTION, BackpressureOverflowStrategy.ERROR)); + } + + @Test + public void empty() { + Flowable.empty() + .onBackpressureBuffer(16, Functions.EMPTY_ACTION, BackpressureOverflowStrategy.ERROR) + .test(0L) + .assertResult(); + } + + @Test + public void justTake() { + Flowable.just(1) + .onBackpressureBuffer(16, Functions.EMPTY_ACTION, BackpressureOverflowStrategy.ERROR) + .take(1) + .test() + .assertResult(1); + } + + @Test + public void overflowNullAction() { + Flowable.range(1, 5) + .onBackpressureBuffer(1, null, BackpressureOverflowStrategy.DROP_OLDEST) + .test(0L) + .assertEmpty(); + } + + @Test + public void cancelOnDrain() { + Flowable.range(1, 5) + .onBackpressureBuffer(10, null, BackpressureOverflowStrategy.DROP_OLDEST) + .takeUntil(v -> true) + .test(0L) + .assertEmpty() + .requestMore(10) + .assertResult(1); + } + + @Test + public void onDroppedNormalDropOldest() throws Throwable { + PublishProcessor<Integer> pp = PublishProcessor.create(); + + @SuppressWarnings("unchecked") + Consumer<Integer> onDropped = mock(Consumer.class); + + TestSubscriber<Integer> ts = pp.onBackpressureBuffer(1, null, BackpressureOverflowStrategy.DROP_OLDEST, onDropped) + .test(0L); + + ts.assertEmpty(); + + pp.onNext(1); + + ts.assertEmpty(); + verify(onDropped, never()).accept(any()); + + pp.onNext(2); + + ts.assertEmpty(); + + verify(onDropped).accept(1); + } + + @Test + public void onDroppedNormalDropLatest() throws Throwable { + PublishProcessor<Integer> pp = PublishProcessor.create(); + + @SuppressWarnings("unchecked") + Consumer<Integer> onDropped = mock(Consumer.class); + + TestSubscriber<Integer> ts = pp.onBackpressureBuffer(2, null, BackpressureOverflowStrategy.DROP_LATEST, onDropped) + .test(0L); + + ts.assertEmpty(); + + pp.onNext(1); + + pp.onNext(2); + + ts.assertEmpty(); + verify(onDropped, never()).accept(any()); + + pp.onNext(3); + + ts.assertEmpty(); + + verify(onDropped).accept(2); + } + + @Test + public void onDroppedNormalError() throws Throwable { + PublishProcessor<Integer> pp = PublishProcessor.create(); + + @SuppressWarnings("unchecked") + Consumer<Integer> onDropped = mock(Consumer.class); + + TestSubscriber<Integer> ts = pp.onBackpressureBuffer(1, null, BackpressureOverflowStrategy.ERROR, onDropped) + .test(0L); + + ts.assertEmpty(); + + pp.onNext(1); + + ts.assertEmpty(); + verify(onDropped, never()).accept(any()); + + pp.onNext(2); + + ts.assertFailure(MissingBackpressureException.class); + + verify(onDropped).accept(2); + } + + @Test + public void onDroppedCrash() throws Throwable { + PublishProcessor<Integer> pp = PublishProcessor.create(); + + Consumer<Integer> onDropped = v -> { throw new TestException(); }; + + TestSubscriberEx<Integer> ts = pp.onBackpressureBuffer(1, null, BackpressureOverflowStrategy.DROP_OLDEST, onDropped) + .subscribeWith(new TestSubscriberEx<Integer>(0L)); + + ts.assertEmpty(); + + pp.onNext(1); + + ts.assertEmpty(); + + pp.onNext(2); + + ts.assertFailure(TestException.class); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableOnBackpressureBufferTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableOnBackpressureBufferTest.java new file mode 100644 index 0000000000..657ce36f1b --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableOnBackpressureBufferTest.java @@ -0,0 +1,401 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import java.util.List; +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.junit.Test; +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.internal.subscriptions.BooleanSubscription; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.operators.QueueFuseable; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.processors.PublishProcessor; +import io.reactivex.rxjava3.schedulers.Schedulers; +import io.reactivex.rxjava3.subscribers.*; +import io.reactivex.rxjava3.testsupport.*; + +public class FlowableOnBackpressureBufferTest extends RxJavaTest { + + @Test + public void noBackpressureSupport() { + TestSubscriber<Long> ts = new TestSubscriber<>(0L); + // this will be ignored + ts.request(100); + // we take 500 so it unsubscribes + infinite.take(500).subscribe(ts); + // it completely ignores the `request(100)` and we get 500 + assertEquals(500, ts.values().size()); + ts.assertNoErrors(); + } + + @Test + public void fixBackpressureWithBuffer() throws InterruptedException { + final CountDownLatch l1 = new CountDownLatch(100); + final CountDownLatch l2 = new CountDownLatch(150); + TestSubscriber<Long> ts = new TestSubscriber<>(new DefaultSubscriber<Long>() { + + @Override + protected void onStart() { + } + + @Override + public void onComplete() { + } + + @Override + public void onError(Throwable e) { + } + + @Override + public void onNext(Long t) { + l1.countDown(); + l2.countDown(); + } + + }, 0L); + // this will be ignored + ts.request(100); + // we take 500 so it unsubscribes + infinite.subscribeOn(Schedulers.computation()) + .onBackpressureBuffer() + .take(500) + .subscribe(ts); + + // it completely ignores the `request(100)` and we get 500 + l1.await(); + assertEquals(100, ts.values().size()); + ts.request(50); + l2.await(); + assertEquals(150, ts.values().size()); + ts.request(350); + ts.awaitDone(5, TimeUnit.SECONDS); + assertEquals(500, ts.values().size()); + ts.assertNoErrors(); + assertEquals(0, ts.values().get(0).intValue()); + assertEquals(499, ts.values().get(499).intValue()); + } + + @Test(expected = IllegalArgumentException.class) + public void fixBackpressureBufferNegativeCapacity() throws InterruptedException { + Flowable.empty().onBackpressureBuffer(-1); + } + + @Test(expected = IllegalArgumentException.class) + public void fixBackpressureBufferZeroCapacity() throws InterruptedException { + Flowable.empty().onBackpressureBuffer(0); + } + + @Test + public void fixBackpressureBoundedBuffer() throws InterruptedException { + final CountDownLatch l1 = new CountDownLatch(100); + final CountDownLatch backpressureCallback = new CountDownLatch(1); + TestSubscriber<Long> ts = new TestSubscriber<>(new DefaultSubscriber<Long>() { + + @Override + protected void onStart() { + } + + @Override + public void onComplete() { + } + + @Override + public void onError(Throwable e) { + } + + @Override + public void onNext(Long t) { + l1.countDown(); + } + + }, 0L); + + ts.request(100); + infinite.subscribeOn(Schedulers.computation()) + .onBackpressureBuffer(500, new Action() { + @Override + public void run() { + backpressureCallback.countDown(); + } + }) + /*.take(1000)*/ + .subscribe(ts); + l1.await(); + + ts.request(50); + + assertTrue(backpressureCallback.await(500, TimeUnit.MILLISECONDS)); + ts.awaitDone(1, TimeUnit.SECONDS); + ts.assertError(MissingBackpressureException.class); + + int size = ts.values().size(); + assertTrue(size <= 150); // will get up to 50 more + assertEquals((long)ts.values().get(size - 1), size - 1); + } + + static final Flowable<Long> infinite = Flowable.unsafeCreate(new Publisher<Long>() { + + @Override + public void subscribe(Subscriber<? super Long> s) { + BooleanSubscription bs = new BooleanSubscription(); + s.onSubscribe(bs); + long i = 0; + while (!bs.isCancelled()) { + s.onNext(i++); + } + } + + }); + + private static final Action THROWS_NON_FATAL = new Action() { + + @Override + public void run() { + throw new RuntimeException(); + } + }; + + @Test + public void nonFatalExceptionThrownByOnOverflowIsNotReportedByUpstream() { + final AtomicBoolean errorOccurred = new AtomicBoolean(false); + TestSubscriber<Long> ts = TestSubscriber.create(0); + infinite + .subscribeOn(Schedulers.computation()) + .doOnError(new Consumer<Throwable>() { + @Override + public void accept(Throwable t) { + errorOccurred.set(true); + } + }) + .onBackpressureBuffer(1, THROWS_NON_FATAL) + .subscribe(ts); + ts.awaitDone(5, TimeUnit.SECONDS); + assertFalse(errorOccurred.get()); + } + + @Test + public void maxSize() { + TestSubscriber<Integer> ts = TestSubscriber.create(0); + + Flowable.range(1, 10).onBackpressureBuffer(1).subscribe(ts); + + ts.assertNoValues(); + ts.assertError(MissingBackpressureException.class); + ts.assertNotComplete(); + } + + @Test(expected = IllegalArgumentException.class) + public void fixBackpressureBufferNegativeCapacity2() throws InterruptedException { + Flowable.empty().onBackpressureBuffer(-1); + } + + @Test(expected = IllegalArgumentException.class) + public void fixBackpressureBufferZeroCapacity2() throws InterruptedException { + Flowable.empty().onBackpressureBuffer(0); + } + + @Test + public void noDelayError() { + + Flowable.just(1).concatWith(Flowable.<Integer>error(new TestException())) + .onBackpressureBuffer(false) + .test(0L) + .assertFailure(TestException.class); + } + + @Test + public void delayError() { + TestSubscriber<Integer> ts = Flowable.just(1).concatWith(Flowable.<Integer>error(new TestException())) + .onBackpressureBuffer(true) + .test(0L) + .assertEmpty(); + + ts.request(1); + ts.assertFailure(TestException.class, 1); + + } + + @Test + public void delayErrorBuffer() { + TestSubscriber<Integer> ts = Flowable.just(1).concatWith(Flowable.<Integer>error(new TestException())) + .onBackpressureBuffer(16, true) + .test(0L) + .assertEmpty(); + + ts.request(1); + ts.assertFailure(TestException.class, 1); + } + + @Test + public void fusedNormal() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<Integer>().setInitialFusionMode(QueueFuseable.ANY); + + Flowable.range(1, 10).onBackpressureBuffer().subscribe(ts); + + ts.assertFuseable() + .assertFusionMode(QueueFuseable.ASYNC) + .assertResult(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + } + + @Test + public void fusedError() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<Integer>().setInitialFusionMode(QueueFuseable.ANY); + + Flowable.<Integer>error(new TestException()).onBackpressureBuffer().subscribe(ts); + + ts.assertFuseable() + .assertFusionMode(QueueFuseable.ASYNC) + .assertFailure(TestException.class); + } + + @Test + public void fusedPreconsume() throws Exception { + + TestSubscriber<Integer> ts = Flowable.range(1, 1000 * 1000) + .onBackpressureBuffer() + .observeOn(Schedulers.single()) + .test(0L); + + ts.assertEmpty(); + + Thread.sleep(100); + + ts.request(1000 * 1000); + + ts + .awaitDone(5, TimeUnit.SECONDS) + .assertValueCount(1000 * 1000) + .assertNoErrors() + .assertComplete(); + } + + @Test + public void emptyDelayError() { + Flowable.empty() + .onBackpressureBuffer(true) + .test() + .assertResult(); + } + + @Test + public void fusionRejected() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<Integer>().setInitialFusionMode(QueueFuseable.SYNC); + + Flowable.<Integer>never().onBackpressureBuffer().subscribe(ts); + + ts.assertFusionMode(QueueFuseable.NONE) + .assertEmpty(); + } + + @Test + public void fusedNoConcurrentCleanDueToCancel() { + for (int j = 0; j < TestHelper.RACE_LONG_LOOPS; j++) { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + final PublishProcessor<Integer> pp = PublishProcessor.create(); + + TestObserver<Integer> to = pp.onBackpressureBuffer(4, false, true) + .observeOn(Schedulers.io()) + .map(Functions.<Integer>identity()) + .observeOn(Schedulers.single()) + .firstOrError() + .test(); + + for (int i = 0; pp.hasSubscribers(); i++) { + pp.onNext(i); + } + + to + .awaitDone(5, TimeUnit.SECONDS) + ; + + if (!errors.isEmpty()) { + throw new CompositeException(errors); + } + + to.assertResult(0); + } finally { + RxJavaPlugins.reset(); + } + } + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeFlowable(f -> f.onBackpressureBuffer()); + } + + @Test + public void badRequest() { + TestHelper.assertBadRequestReported(Flowable.never().onBackpressureBuffer()); + } + + @Test + public void onDroppedNormal() throws Throwable { + PublishProcessor<Integer> pp = PublishProcessor.create(); + + @SuppressWarnings("unchecked") + Consumer<Integer> onDropped = mock(Consumer.class); + + TestSubscriber<Integer> ts = pp.onBackpressureBuffer(1, false, false, () -> { }, onDropped) + .test(0L); + + ts.assertEmpty(); + + pp.onNext(1); + + ts.assertEmpty(); + verify(onDropped, never()).accept(any()); + + pp.onNext(2); + + ts.assertFailure(MissingBackpressureException.class); + + verify(onDropped).accept(2); + } + + @Test + public void onDroppedCrash() throws Throwable { + PublishProcessor<Integer> pp = PublishProcessor.create(); + + Consumer<Integer> onDropped = v -> { throw new TestException(); }; + + TestSubscriberEx<Integer> ts = pp.onBackpressureBuffer(1, false, false, () -> { }, onDropped) + .subscribeWith(new TestSubscriberEx<Integer>(0L)); + + ts.assertEmpty(); + + pp.onNext(1); + + ts.assertEmpty(); + + pp.onNext(2); + + ts.assertFailure(MissingBackpressureException.class); + + assertTrue(ts.errors().get(0).getCause() instanceof TestException); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableOnBackpressureDropTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableOnBackpressureDropTest.java new file mode 100644 index 0000000000..0dfec9422c --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableOnBackpressureDropTest.java @@ -0,0 +1,208 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import static org.junit.Assert.*; + +import java.util.concurrent.*; +import java.util.concurrent.atomic.*; + +import org.junit.Test; +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.subscriptions.BooleanSubscription; +import io.reactivex.rxjava3.schedulers.Schedulers; +import io.reactivex.rxjava3.subscribers.*; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class FlowableOnBackpressureDropTest extends RxJavaTest { + + @Test + public void noBackpressureSupport() { + TestSubscriber<Long> ts = new TestSubscriber<>(0L); + // this will be ignored + ts.request(100); + // we take 500 so it unsubscribes + infinite.take(500).subscribe(ts); + // it completely ignores the `request(100)` and we get 500 + assertEquals(500, ts.values().size()); + ts.assertNoErrors(); + } + + @Test + public void withObserveOn() throws InterruptedException { + TestSubscriber<Integer> ts = new TestSubscriber<>(); + Flowable.range(0, Flowable.bufferSize() * 10).onBackpressureDrop().observeOn(Schedulers.io()).subscribe(ts); + ts.awaitDone(5, TimeUnit.SECONDS); + } + + @Test + public void fixBackpressureWithBuffer() throws InterruptedException { + final CountDownLatch l1 = new CountDownLatch(100); + final CountDownLatch l2 = new CountDownLatch(150); + TestSubscriber<Long> ts = new TestSubscriber<>(new DefaultSubscriber<Long>() { + + @Override + protected void onStart() { + } + + @Override + public void onComplete() { + } + + @Override + public void onError(Throwable e) { + } + + @Override + public void onNext(Long t) { + l1.countDown(); + l2.countDown(); + } + + }, 0L); + // this will be ignored + ts.request(100); + // we take 500 so it unsubscribes + infinite.subscribeOn(Schedulers.computation()).onBackpressureDrop().take(500).subscribe(ts); + // it completely ignores the `request(100)` and we get 500 + l1.await(); + assertEquals(100, ts.values().size()); + ts.request(50); + l2.await(); + assertEquals(150, ts.values().size()); + ts.request(350); + ts.awaitDone(5, TimeUnit.SECONDS); + assertEquals(500, ts.values().size()); + ts.assertNoErrors(); + assertEquals(0, ts.values().get(0).intValue()); + } + + @Test + public void requestOverflow() throws InterruptedException { + final AtomicInteger count = new AtomicInteger(); + int n = 10; + range(n).onBackpressureDrop().subscribe(new DefaultSubscriber<Long>() { + + @Override + public void onStart() { + request(10); + } + + @Override + public void onComplete() { + } + + @Override + public void onError(Throwable e) { + throw new RuntimeException(e); + } + + @Override + public void onNext(Long t) { + count.incrementAndGet(); + //cause overflow of requested if not handled properly in onBackpressureDrop operator + request(Long.MAX_VALUE - 1); + }}); + assertEquals(n, count.get()); + } + + static final Flowable<Long> infinite = Flowable.unsafeCreate(new Publisher<Long>() { + + @Override + public void subscribe(Subscriber<? super Long> s) { + BooleanSubscription bs = new BooleanSubscription(); + s.onSubscribe(bs); + long i = 0; + while (!bs.isCancelled()) { + s.onNext(i++); + } + } + + }); + + private static Flowable<Long> range(final long n) { + return Flowable.unsafeCreate(new Publisher<Long>() { + + @Override + public void subscribe(Subscriber<? super Long> s) { + BooleanSubscription bs = new BooleanSubscription(); + s.onSubscribe(bs); + for (long i = 0; i < n; i++) { + if (bs.isCancelled()) { + break; + } + s.onNext(i); + } + s.onComplete(); + } + + }); + } + + private static final Consumer<Long> THROW_NON_FATAL = new Consumer<Long>() { + @Override + public void accept(Long n) { + throw new RuntimeException(); + } + }; + + @Test + public void nonFatalExceptionFromOverflowActionIsNotReportedFromUpstreamOperator() { + final AtomicBoolean errorOccurred = new AtomicBoolean(false); + //request 0 + TestSubscriber<Long> ts = TestSubscriber.create(0); + //range method emits regardless of requests so should trigger onBackpressureDrop action + range(2) + // if haven't caught exception in onBackpressureDrop operator then would incorrectly + // be picked up by this call to doOnError + .doOnError(new Consumer<Throwable>() { + @Override + public void accept(Throwable t) { + errorOccurred.set(true); + } + }) + .onBackpressureDrop(THROW_NON_FATAL) + .subscribe(ts); + + assertFalse(errorOccurred.get()); + } + + @Test + public void badSource() { + TestHelper.checkBadSourceFlowable(new Function<Flowable<Integer>, Object>() { + @Override + public Object apply(Flowable<Integer> f) throws Exception { + return f.onBackpressureDrop(); + } + }, false, 1, 1, 1); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeFlowable(new Function<Flowable<Object>, Publisher<Object>>() { + @Override + public Publisher<Object> apply(Flowable<Object> f) throws Exception { + return f.onBackpressureDrop(); + } + }); + } + + @Test + public void badRequest() { + TestHelper.assertBadRequestReported(Flowable.just(1).onBackpressureDrop()); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableOnBackpressureErrorTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableOnBackpressureErrorTest.java new file mode 100644 index 0000000000..2693bf3014 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableOnBackpressureErrorTest.java @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import static org.junit.Assert.*; + +import org.junit.Test; +import org.reactivestreams.Publisher; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.MissingBackpressureException; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.subjects.PublishSubject; +import io.reactivex.rxjava3.subscribers.TestSubscriber; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class FlowableOnBackpressureErrorTest extends RxJavaTest { + + @Test + public void dispose() { + TestHelper.checkDisposed(Observable.just(1).toFlowable(BackpressureStrategy.ERROR)); + } + + @Test + public void badRequest() { + TestHelper.assertBadRequestReported(Observable.just(1).toFlowable(BackpressureStrategy.ERROR)); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeFlowable(new Function<Flowable<Object>, Publisher<Object>>() { + @Override + public Publisher<Object> apply(Flowable<Object> f) throws Exception { + return new FlowableOnBackpressureError<>(f); + } + }); + } + + @Test + public void badSource() { + TestHelper.<Integer>checkBadSourceFlowable(new Function<Flowable<Integer>, Object>() { + @Override + public Object apply(Flowable<Integer> f) throws Exception { + return new FlowableOnBackpressureError<>(f); + } + }, false, 1, 1, 1); + } + + @Test + public void overflowCancels() { + PublishSubject<Integer> ps = PublishSubject.create(); + + TestSubscriber<Integer> ts = ps.toFlowable(BackpressureStrategy.ERROR) + .test(0L); + + assertTrue(ps.hasObservers()); + + ps.onNext(1); + + assertFalse(ps.hasObservers()); + + ts.assertFailure(MissingBackpressureException.class); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableOnBackpressureLatestTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableOnBackpressureLatestTest.java new file mode 100644 index 0000000000..f0d50994ed --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableOnBackpressureLatestTest.java @@ -0,0 +1,237 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import java.util.Random; +import java.util.concurrent.TimeUnit; + +import org.junit.*; +import org.mockito.InOrder; +import org.reactivestreams.Publisher; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.processors.PublishProcessor; +import io.reactivex.rxjava3.schedulers.Schedulers; +import io.reactivex.rxjava3.subscribers.TestSubscriber; +import io.reactivex.rxjava3.testsupport.*; + +import static org.mockito.Mockito.inOrder; + +public class FlowableOnBackpressureLatestTest extends RxJavaTest { + @Test + public void simple() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(); + + Flowable.range(1, 5).onBackpressureLatest().subscribe(ts); + + ts.assertNoErrors(); + ts.assertTerminated(); + ts.assertValues(1, 2, 3, 4, 5); + } + + @Test + public void simpleError() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(); + + Flowable.range(1, 5).concatWith(Flowable.<Integer>error(new TestException())) + .onBackpressureLatest().subscribe(ts); + + ts.assertTerminated(); + ts.assertError(TestException.class); + ts.assertValues(1, 2, 3, 4, 5); + } + + @Test + public void simpleBackpressure() { + TestSubscriber<Integer> ts = new TestSubscriber<>(2L); + + Flowable.range(1, 5).onBackpressureLatest().subscribe(ts); + + ts.assertNoErrors(); + ts.assertValues(1, 2); + ts.assertNotComplete(); + } + + @Test + public void simpleBackpressureWithOnDroppedCallback() { + PublishProcessor<Integer> source = PublishProcessor.create(); + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(0L); + + Observer<Object> dropCallbackObserver = TestHelper.mockObserver(); + + source.onBackpressureLatest(dropCallbackObserver::onNext) + .subscribe(ts); + + ts.assertNoValues(); + + source.onNext(1); + source.onNext(2); + source.onNext(3); + + ts.request(1); + + ts.assertValues(3); + + source.onNext(4); + source.onNext(5); + + ts.request(2); + + ts.assertValues(3,5); + + InOrder dropCallbackOrder = inOrder(dropCallbackObserver); + dropCallbackOrder.verify(dropCallbackObserver).onNext(1); + dropCallbackOrder.verify(dropCallbackObserver).onNext(2); + dropCallbackOrder.verify(dropCallbackObserver).onNext(4); + dropCallbackOrder.verifyNoMoreInteractions(); + } + + @Test + public void simpleBackpressureWithOnDroppedCallbackEx() { + PublishProcessor<Integer> source = PublishProcessor.create(); + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(0L); + + source.onBackpressureLatest(e -> { + if (e == 3) { + throw new TestException("forced"); + } + }) + .subscribe(ts); + + ts.assertNoValues(); + + source.onNext(1); + source.onNext(2); + + ts.request(1); + + ts.assertValues(2); + + source.onNext(3); + source.onNext(4); + + ts.assertError(TestException.class); + ts.assertValues(2); + } + + @Test + public void synchronousDrop() { + PublishProcessor<Integer> source = PublishProcessor.create(); + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(0L); + + source.onBackpressureLatest().subscribe(ts); + + ts.assertNoValues(); + + source.onNext(1); + ts.request(2); + + ts.assertValue(1); + + source.onNext(2); + + ts.assertValues(1, 2); + + source.onNext(3); + source.onNext(4); + source.onNext(5); + source.onNext(6); + + ts.request(2); + + ts.assertValues(1, 2, 6); + + source.onNext(7); + + ts.assertValues(1, 2, 6, 7); + + source.onNext(8); + source.onNext(9); + source.onComplete(); + + ts.request(1); + + ts.assertValues(1, 2, 6, 7, 9); + ts.assertNoErrors(); + ts.assertTerminated(); + } + + @Test + public void asynchronousDrop() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<Integer>(1L) { + final Random rnd = new Random(); + @Override + public void onNext(Integer t) { + super.onNext(t); + if (rnd.nextDouble() < 0.001) { + try { + Thread.sleep(1); + } catch (InterruptedException ex) { + ex.printStackTrace(); + } + } + request(1); + } + }; + int m = 100000; + Flowable.range(1, m) + .subscribeOn(Schedulers.computation()) + .onBackpressureLatest() + .observeOn(Schedulers.io()) + .subscribe(ts); + + ts.awaitDone(2, TimeUnit.SECONDS); + ts.assertTerminated(); + int n = ts.values().size(); + System.out.println("testAsynchronousDrop -> " + n); + Assert.assertTrue("All events received?", n < m); + int previous = 0; + for (Integer current : ts.values()) { + Assert.assertTrue("The sequence must be increasing [current value=" + previous + + ", previous value=" + current + "]", previous <= current); + previous = current; + } + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeFlowable(new Function<Flowable<Object>, Publisher<Object>>() { + @Override + public Publisher<Object> apply(Flowable<Object> f) throws Exception { + return f.onBackpressureLatest(); + } + }); + } + + @Test + public void take() { + Flowable.just(1, 2) + .onBackpressureLatest() + .take(1) + .test() + .assertResult(1); + } + + @Test + public void dispose() { + TestHelper.checkDisposed(Flowable.never().onBackpressureLatest()); + } + + @Test + public void badRequest() { + TestHelper.assertBadRequestReported(Flowable.never().onBackpressureLatest()); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableOnBackpressureReduceTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableOnBackpressureReduceTest.java new file mode 100644 index 0000000000..5f7b7ad957 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableOnBackpressureReduceTest.java @@ -0,0 +1,261 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.core.RxJavaTest; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.BiFunction; +import io.reactivex.rxjava3.processors.PublishProcessor; +import io.reactivex.rxjava3.schedulers.Schedulers; +import io.reactivex.rxjava3.subscribers.TestSubscriber; +import io.reactivex.rxjava3.testsupport.TestHelper; +import io.reactivex.rxjava3.testsupport.TestSubscriberEx; +import org.junit.Assert; +import org.junit.Test; + +import java.util.Random; +import java.util.concurrent.TimeUnit; + +public class FlowableOnBackpressureReduceTest extends RxJavaTest { + + static final BiFunction<Integer, Integer, Integer> TEST_INT_REDUCER = (previous, current) -> previous + current + 50; + + static final BiFunction<Object, Object, Object> TEST_OBJECT_REDUCER = (previous, current) -> current; + + @Test + public void simple() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(); + + Flowable.range(1, 5).onBackpressureReduce(TEST_INT_REDUCER).subscribe(ts); + + ts.assertNoErrors(); + ts.assertTerminated(); + ts.assertValues(1, 2, 3, 4, 5); + } + + @Test + public void simpleError() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(); + + Flowable.range(1, 5).concatWith(Flowable.error(new TestException())) + .onBackpressureReduce(TEST_INT_REDUCER).subscribe(ts); + + ts.assertTerminated(); + ts.assertError(TestException.class); + ts.assertValues(1, 2, 3, 4, 5); + } + + @Test + public void simpleBackpressure() { + TestSubscriber<Integer> ts = new TestSubscriber<>(2L); + + Flowable.range(1, 5).onBackpressureReduce(TEST_INT_REDUCER).subscribe(ts); + + ts.assertNoErrors(); + ts.assertValues(1, 2); + ts.assertNotComplete(); + } + + @Test + public void synchronousDrop() { + PublishProcessor<Integer> source = PublishProcessor.create(); + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(0L); + + source.onBackpressureReduce(TEST_INT_REDUCER).subscribe(ts); + + ts.assertNoValues(); + + source.onNext(1); + ts.request(2); + + ts.assertValue(1); + + source.onNext(2); + + ts.assertValues(1, 2); + + source.onNext(3); + source.onNext(4); //3 + 4 + 50 == 57 + source.onNext(5); //57 + 5 + 50 == 112 + source.onNext(6); //112 + 6 + 50 == 168 + + ts.request(2); + + ts.assertValues(1, 2, 168); + + source.onNext(7); + + ts.assertValues(1, 2, 168, 7); + + source.onNext(8); + source.onNext(9); //8 + 9 + 50 == 67 + source.onComplete(); + + ts.request(1); + + ts.assertValues(1, 2, 168, 7, 67); + ts.assertNoErrors(); + ts.assertTerminated(); + } + + @Test + public void reduceBackpressuredSync() { + PublishProcessor<Integer> source = PublishProcessor.create(); + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(0L); + + source.onBackpressureReduce(Integer::sum).subscribe(ts); + + source.onNext(1); + source.onNext(2); + source.onNext(3); + + ts.request(1); + + ts.assertValuesOnly(6); + + source.onNext(4); + source.onComplete(); + + ts.assertValuesOnly(6); + + ts.request(1); + ts.assertResult(6, 4); + } + + private <T> TestSubscriberEx<T> createDelayedSubscriber() { + return new TestSubscriberEx<T>(1L) { + final Random rnd = new Random(); + + @Override + public void onNext(T t) { + super.onNext(t); + if (rnd.nextDouble() < 0.001) { + try { + Thread.sleep(1); + } catch (InterruptedException ex) { + ex.printStackTrace(); + } + } + request(1); + } + }; + } + + private <T> void assertValuesDropped(TestSubscriberEx<T> ts, int totalValues) { + int n = ts.values().size(); + System.out.println("testAsynchronousDrop -> " + n); + Assert.assertTrue("All events received?", n < totalValues); + } + + private void assertIncreasingSequence(TestSubscriberEx<Integer> ts) { + int previous = 0; + for (Integer current : ts.values()) { + Assert.assertTrue("The sequence must be increasing [current value=" + previous + + ", previous value=" + current + "]", previous <= current); + previous = current; + } + } + + @Test + public void asynchronousDrop() { + TestSubscriberEx<Integer> ts = createDelayedSubscriber(); + int m = 100000; + Flowable.range(1, m) + .subscribeOn(Schedulers.computation()) + .onBackpressureReduce((previous, current) -> { + //in that case it works like onBackpressureLatest + //the output sequence of number must be increasing + return current; + }) + .observeOn(Schedulers.io()) + .subscribe(ts); + + ts.awaitDone(2, TimeUnit.SECONDS); + ts.assertTerminated(); + assertValuesDropped(ts, m); + assertIncreasingSequence(ts); + } + + @Test + public void asynchronousDrop2() { + TestSubscriberEx<Long> ts = createDelayedSubscriber(); + int m = 100000; + Flowable.rangeLong(1, m) + .subscribeOn(Schedulers.computation()) + .onBackpressureReduce(Long::sum) + .observeOn(Schedulers.io()) + .subscribe(ts); + + ts.awaitDone(2, TimeUnit.SECONDS); + ts.assertTerminated(); + assertValuesDropped(ts, m); + long sum = 0; + for (Long i : ts.values()) { + sum += i; + } + //sum = (A1 + An) * n / 2 = 100_001 * 50_000 = 50_000_00000 + 50_000 = 50_000_50_000 + Assert.assertEquals("Wrong sum: " + sum, 5000050000L, sum); + } + + @Test + public void nullPointerFromReducer() { + PublishProcessor<Integer> source = PublishProcessor.create(); + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(0); + source.onBackpressureReduce((l, r) -> null).subscribe(ts); + + source.onNext(1); + source.onNext(2); + + TestHelper.assertError(ts.errors(), 0, NullPointerException.class, "The reducer returned a null value"); + } + + @Test + public void exceptionFromReducer() { + PublishProcessor<Integer> source = PublishProcessor.create(); + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(0); + source.onBackpressureReduce((l, r) -> { + throw new TestException("Test exception"); + }).subscribe(ts); + + source.onNext(1); + source.onNext(2); + + TestHelper.assertError(ts.errors(), 0, TestException.class, "Test exception"); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeFlowable(f -> f.onBackpressureReduce(TEST_OBJECT_REDUCER)); + } + + @Test + public void take() { + Flowable.just(1, 2) + .onBackpressureReduce(TEST_INT_REDUCER) + .take(1) + .test() + .assertResult(1); + } + + @Test + public void dispose() { + TestHelper.checkDisposed(Flowable.never().onBackpressureReduce(TEST_OBJECT_REDUCER)); + } + + @Test + public void badRequest() { + TestHelper.assertBadRequestReported(Flowable.never().onBackpressureReduce(TEST_OBJECT_REDUCER)); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableOnBackpressureReduceWithTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableOnBackpressureReduceWithTest.java new file mode 100644 index 0000000000..32cee35603 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableOnBackpressureReduceWithTest.java @@ -0,0 +1,331 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.core.RxJavaTest; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.BiFunction; +import io.reactivex.rxjava3.functions.Supplier; +import io.reactivex.rxjava3.processors.PublishProcessor; +import io.reactivex.rxjava3.schedulers.Schedulers; +import io.reactivex.rxjava3.testsupport.TestHelper; +import io.reactivex.rxjava3.testsupport.TestSubscriberEx; +import org.junit.Assert; +import org.junit.Test; + +import java.util.*; +import java.util.concurrent.TimeUnit; + +public class FlowableOnBackpressureReduceWithTest extends RxJavaTest { + + private static <T> BiFunction<List<T>, T, List<T>> createTestReducer() { + return (list, number) -> { + list.add(number); + return list; + }; + } + + private static <T> Supplier<List<T>> createTestSupplier() { + return ArrayList::new; + } + + @Test + public void simple() { + TestSubscriberEx<List<Integer>> ts = new TestSubscriberEx<>(); + + Flowable.range(1, 5).onBackpressureReduce(createTestSupplier(), createTestReducer()).subscribe(ts); + + ts.assertNoErrors(); + ts.assertTerminated(); + ts.assertValues( + Collections.singletonList(1), + Collections.singletonList(2), + Collections.singletonList(3), + Collections.singletonList(4), + Collections.singletonList(5) + ); + } + + @Test + public void simpleError() { + TestSubscriberEx<List<Integer>> ts = new TestSubscriberEx<>(); + + Flowable.range(1, 5).concatWith(Flowable.error(new TestException())) + .onBackpressureReduce(createTestSupplier(), createTestReducer()).subscribe(ts); + + ts.assertTerminated(); + ts.assertError(TestException.class); + ts.assertValues( + Collections.singletonList(1), + Collections.singletonList(2), + Collections.singletonList(3), + Collections.singletonList(4), + Collections.singletonList(5) + ); + } + + @Test + public void simpleBackpressure() { + TestSubscriberEx<List<Integer>> ts = new TestSubscriberEx<>(2L); + + Flowable.range(1, 5).onBackpressureReduce(createTestSupplier(), createTestReducer()).subscribe(ts); + + ts.assertNoErrors(); + ts.assertValues( + Collections.singletonList(1), + Collections.singletonList(2) + ); + ts.assertNotComplete(); + } + + @Test + public void reduceBackpressuredSync() { + PublishProcessor<Integer> source = PublishProcessor.create(); + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(0L); + + source.onBackpressureReduce(() -> 0, Integer::sum).subscribe(ts); + + source.onNext(1); + source.onNext(2); + source.onNext(3); + + ts.request(1); + + ts.assertValuesOnly(6); + + source.onNext(4); + source.onComplete(); + + ts.assertValuesOnly(6); + + ts.request(1); + ts.assertResult(6, 4); + } + + @Test + public void synchronousDrop() { + PublishProcessor<Integer> source = PublishProcessor.create(); + TestSubscriberEx<List<Integer>> ts = new TestSubscriberEx<>(0L); + + source.onBackpressureReduce(createTestSupplier(), createTestReducer()).subscribe(ts); + + ts.assertNoValues(); + + source.onNext(1); + ts.request(2); + + ts.assertValues(Collections.singletonList(1)); + + source.onNext(2); + + ts.assertValues( + Collections.singletonList(1), + Collections.singletonList(2) + ); + + source.onNext(3); + source.onNext(4); + source.onNext(5); + source.onNext(6); + + ts.request(2); + + ts.assertValues( + Collections.singletonList(1), + Collections.singletonList(2), + Arrays.asList(3, 4, 5, 6) + ); + + source.onNext(7); + + ts.assertValues( + Collections.singletonList(1), + Collections.singletonList(2), + Arrays.asList(3, 4, 5, 6), + Collections.singletonList(7) + ); + + source.onNext(8); + source.onNext(9); + source.onComplete(); + + ts.request(1); + + ts.assertValues( + Collections.singletonList(1), + Collections.singletonList(2), + Arrays.asList(3, 4, 5, 6), + Collections.singletonList(7), + Arrays.asList(8, 9) + ); + ts.assertNoErrors(); + ts.assertTerminated(); + } + + private <T> TestSubscriberEx<T> createDelayedSubscriber() { + return new TestSubscriberEx<T>(1L) { + final Random rnd = new Random(); + + @Override + public void onNext(T t) { + super.onNext(t); + if (rnd.nextDouble() < 0.001) { + try { + Thread.sleep(1); + } catch (InterruptedException ex) { + ex.printStackTrace(); + } + } + request(1); + } + }; + } + + private <T> void assertValuesDropped(TestSubscriberEx<T> ts, int totalValues) { + int n = ts.values().size(); + System.out.println("testAsynchronousDrop -> " + n); + Assert.assertTrue("All events received?", n < totalValues); + } + + private void assertIncreasingSequence(TestSubscriberEx<Integer> ts) { + int previous = 0; + for (Integer current : ts.values()) { + Assert.assertTrue("The sequence must be increasing [current value=" + previous + + ", previous value=" + current + "]", previous <= current); + previous = current; + } + } + + @Test + public void asynchronousDrop() { + TestSubscriberEx<Integer> ts = createDelayedSubscriber(); + int m = 100000; + Flowable.range(1, m) + .subscribeOn(Schedulers.computation()) + .onBackpressureReduce((Supplier<List<Integer>>) Collections::emptyList, (list, current) -> { + //in that case it works like onBackpressureLatest + //the output sequence of number must be increasing + return Collections.singletonList(current); + }) + .observeOn(Schedulers.io()) + .concatMap(Flowable::fromIterable) + .subscribe(ts); + + ts.awaitDone(2, TimeUnit.SECONDS); + ts.assertTerminated(); + assertValuesDropped(ts, m); + assertIncreasingSequence(ts); + } + + @Test + public void asynchronousDrop2() { + TestSubscriberEx<Long> ts = createDelayedSubscriber(); + int m = 100000; + Flowable.rangeLong(1, m) + .subscribeOn(Schedulers.computation()) + .onBackpressureReduce(createTestSupplier(), createTestReducer()) + .observeOn(Schedulers.io()) + .concatMap(list -> Flowable.just(list.stream().reduce(Long::sum).orElseThrow(() -> { + throw new IllegalArgumentException("No value in list"); + }))) + .subscribe(ts); + + ts.awaitDone(2, TimeUnit.SECONDS); + ts.assertTerminated(); + assertValuesDropped(ts, m); + long sum = 0; + for (Long i : ts.values()) { + sum += i; + } + //sum = (A1 + An) * n / 2 = 100_001 * 50_000 = 50_000_00000 + 50_000 = 50_000_50_000 + Assert.assertEquals("Wrong sum: " + sum, 5000050000L, sum); + } + + @Test + public void nullPointerFromReducer() { + PublishProcessor<Integer> source = PublishProcessor.create(); + TestSubscriberEx<List<Integer>> ts = new TestSubscriberEx<>(0L); + source.onBackpressureReduce(createTestSupplier(), (BiFunction<List<Integer>, ? super Integer, List<Integer>>) (list, number) -> null).subscribe(ts); + + source.onNext(1); + source.onNext(2); + + TestHelper.assertError(ts.errors(), 0, NullPointerException.class, "The reducer returned a null value"); + } + + @Test + public void nullPointerFromSupplier() { + PublishProcessor<Integer> source = PublishProcessor.create(); + TestSubscriberEx<List<Integer>> ts = new TestSubscriberEx<>(0L); + source.onBackpressureReduce(() -> null, createTestReducer()).subscribe(ts); + + source.onNext(1); + source.onNext(2); + + TestHelper.assertError(ts.errors(), 0, NullPointerException.class, "The supplier returned a null value"); + } + + @Test + public void exceptionFromReducer() { + PublishProcessor<Integer> source = PublishProcessor.create(); + TestSubscriberEx<List<Integer>> ts = new TestSubscriberEx<>(0L); + source.onBackpressureReduce(createTestSupplier(), (BiFunction<List<Integer>, ? super Integer, List<Integer>>) (l, r) -> { + throw new TestException("Test exception"); + }).subscribe(ts); + + source.onNext(1); + source.onNext(2); + + TestHelper.assertError(ts.errors(), 0, TestException.class, "Test exception"); + } + + @Test + public void exceptionFromSupplier() { + PublishProcessor<Integer> source = PublishProcessor.create(); + TestSubscriberEx<List<Integer>> ts = new TestSubscriberEx<>(0L); + source.onBackpressureReduce(() -> { + throw new TestException("Test exception"); + }, createTestReducer()).subscribe(ts); + + source.onNext(1); + source.onNext(2); + + TestHelper.assertError(ts.errors(), 0, TestException.class, "Test exception"); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeFlowable(f -> f.onBackpressureReduce(createTestSupplier(), createTestReducer())); + } + + @Test + public void take() { + Flowable.just(1, 2) + .onBackpressureReduce(createTestSupplier(), createTestReducer()) + .take(1) + .test() + .assertResult(Collections.singletonList(1)); + } + + @Test + public void dispose() { + TestHelper.checkDisposed(Flowable.never().onBackpressureReduce(createTestSupplier(), createTestReducer())); + } + + @Test + public void badRequest() { + TestHelper.assertBadRequestReported(Flowable.never().onBackpressureReduce(createTestSupplier(), createTestReducer())); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableOnErrorCompleteTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableOnErrorCompleteTest.java new file mode 100644 index 0000000000..0a6f137ce2 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableOnErrorCompleteTest.java @@ -0,0 +1,143 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import static org.junit.Assert.*; + +import java.io.IOException; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.processors.PublishProcessor; +import io.reactivex.rxjava3.subscribers.TestSubscriber; +import io.reactivex.rxjava3.testsupport.*; + +public class FlowableOnErrorCompleteTest { + + @Test + public void normal() { + Flowable.range(1, 10) + .onErrorComplete() + .test() + .assertResult(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + } + + @Test + public void normalBackpressured() { + Flowable.range(1, 10) + .onErrorComplete() + .test(0) + .assertEmpty() + .requestMore(3) + .assertValuesOnly(1, 2, 3) + .requestMore(3) + .assertValuesOnly(1, 2, 3, 4, 5, 6) + .requestMore(4) + .assertResult(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + } + + @Test + public void empty() { + Flowable.empty() + .onErrorComplete() + .test() + .assertResult(); + } + + @Test + public void error() throws Throwable { + TestHelper.withErrorTracking(errors -> { + Flowable.error(new TestException()) + .onErrorComplete() + .test() + .assertResult(); + + assertTrue("" + errors, errors.isEmpty()); + }); + } + + @Test + public void errorMatches() throws Throwable { + TestHelper.withErrorTracking(errors -> { + Flowable.error(new TestException()) + .onErrorComplete(error -> error instanceof TestException) + .test() + .assertResult(); + + assertTrue("" + errors, errors.isEmpty()); + }); + } + + @Test + public void errorNotMatches() throws Throwable { + TestHelper.withErrorTracking(errors -> { + Flowable.error(new IOException()) + .onErrorComplete(error -> error instanceof TestException) + .test() + .assertFailure(IOException.class); + + assertTrue("" + errors, errors.isEmpty()); + }); + } + + @Test + public void errorPredicateCrash() throws Throwable { + TestHelper.withErrorTracking(errors -> { + TestSubscriberEx<Object> ts = Flowable.error(new IOException()) + .onErrorComplete(error -> { throw new TestException(); }) + .subscribeWith(new TestSubscriberEx<>()) + .assertFailure(CompositeException.class); + + TestHelper.assertError(ts, 0, IOException.class); + TestHelper.assertError(ts, 1, TestException.class); + + assertTrue("" + errors, errors.isEmpty()); + }); + } + + @Test + public void itemsThenError() throws Throwable { + TestHelper.withErrorTracking(errors -> { + Flowable.range(1, 5) + .map(v -> 4 / (3 - v)) + .onErrorComplete() + .test() + .assertResult(2, 4); + + assertTrue("" + errors, errors.isEmpty()); + }); + } + + @Test + public void cancel() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + + TestSubscriber<Integer> ts = pp + .onErrorComplete() + .test(); + + assertTrue("No subscribers?!", pp.hasSubscribers()); + + ts.cancel(); + + assertFalse("Still subscribers?!", pp.hasSubscribers()); + } + + @Test + public void onSubscribe() { + TestHelper.checkDoubleOnSubscribeFlowable(f -> f.onErrorComplete()); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableOnErrorResumeNextViaFlowableTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableOnErrorResumeNextViaFlowableTest.java new file mode 100644 index 0000000000..8b94b0d6f0 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableOnErrorResumeNextViaFlowableTest.java @@ -0,0 +1,202 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import static org.junit.Assert.fail; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import java.util.concurrent.TimeUnit; + +import org.junit.Test; +import org.mockito.Mockito; +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.processors.PublishProcessor; +import io.reactivex.rxjava3.schedulers.Schedulers; +import io.reactivex.rxjava3.subscribers.TestSubscriber; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class FlowableOnErrorResumeNextViaFlowableTest extends RxJavaTest { + + @Test + public void resumeNext() { + Subscription s = mock(Subscription.class); + // Trigger failure on second element + TestObservable f = new TestObservable(s, "one", "fail", "two", "three"); + Flowable<String> w = Flowable.unsafeCreate(f); + Flowable<String> resume = Flowable.just("twoResume", "threeResume"); + Flowable<String> flowable = w.onErrorResumeWith(resume); + + Subscriber<String> subscriber = TestHelper.mockSubscriber(); + flowable.subscribe(subscriber); + + try { + f.t.join(); + } catch (InterruptedException e) { + fail(e.getMessage()); + } + + verify(subscriber, Mockito.never()).onError(any(Throwable.class)); + verify(subscriber, times(1)).onComplete(); + verify(subscriber, times(1)).onNext("one"); + verify(subscriber, Mockito.never()).onNext("two"); + verify(subscriber, Mockito.never()).onNext("three"); + verify(subscriber, times(1)).onNext("twoResume"); + verify(subscriber, times(1)).onNext("threeResume"); + } + + @Test + public void mapResumeAsyncNext() { + Subscription sr = mock(Subscription.class); + // Trigger multiple failures + Flowable<String> w = Flowable.just("one", "fail", "two", "three", "fail"); + // Resume Observable is async + TestObservable f = new TestObservable(sr, "twoResume", "threeResume"); + Flowable<String> resume = Flowable.unsafeCreate(f); + + // Introduce map function that fails intermittently (Map does not prevent this when the observer is a + // rx.operator incl onErrorResumeNextViaObservable) + w = w.map(new Function<String, String>() { + @Override + public String apply(String s) { + if ("fail".equals(s)) { + throw new RuntimeException("Forced Failure"); + } + System.out.println("BadMapper:" + s); + return s; + } + }); + + Flowable<String> flowable = w.onErrorResumeWith(resume); + + Subscriber<String> subscriber = TestHelper.mockSubscriber(); + + flowable.subscribe(subscriber); + + try { + f.t.join(); + } catch (InterruptedException e) { + fail(e.getMessage()); + } + + verify(subscriber, Mockito.never()).onError(any(Throwable.class)); + verify(subscriber, times(1)).onComplete(); + verify(subscriber, times(1)).onNext("one"); + verify(subscriber, Mockito.never()).onNext("two"); + verify(subscriber, Mockito.never()).onNext("three"); + verify(subscriber, times(1)).onNext("twoResume"); + verify(subscriber, times(1)).onNext("threeResume"); + } + + static final class TestObservable implements Publisher<String> { + + final Subscription upstream; + final String[] values; + Thread t; + + TestObservable(Subscription s, String... values) { + this.upstream = s; + this.values = values; + } + + @Override + public void subscribe(final Subscriber<? super String> subscriber) { + System.out.println("TestObservable subscribed to ..."); + subscriber.onSubscribe(upstream); + t = new Thread(new Runnable() { + + @Override + public void run() { + try { + System.out.println("running TestObservable thread"); + for (String s : values) { + if ("fail".equals(s)) { + throw new RuntimeException("Forced Failure"); + } + System.out.println("TestObservable onNext: " + s); + subscriber.onNext(s); + } + System.out.println("TestObservable onComplete"); + subscriber.onComplete(); + } catch (Throwable e) { + System.out.println("TestObservable onError: " + e); + subscriber.onError(e); + } + } + + }); + System.out.println("starting TestObservable thread"); + t.start(); + System.out.println("done starting TestObservable thread"); + } + } + + @Test + public void backpressure() { + TestSubscriber<Integer> ts = new TestSubscriber<>(); + Flowable.range(0, 100000) + .onErrorResumeWith(Flowable.just(1)) + .observeOn(Schedulers.computation()) + .map(new Function<Integer, Integer>() { + int c; + + @Override + public Integer apply(Integer t1) { + if (c++ <= 1) { + // slow + try { + Thread.sleep(500); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + return t1; + } + + }) + .subscribe(ts); + ts.awaitDone(5, TimeUnit.SECONDS); + ts.assertNoErrors(); + } + + @Test + public void normalBackpressure() { + TestSubscriber<Integer> ts = TestSubscriber.create(0); + + PublishProcessor<Integer> pp = PublishProcessor.create(); + + pp.onErrorResumeWith(Flowable.range(3, 2)).subscribe(ts); + + ts.request(2); + + pp.onNext(1); + pp.onNext(2); + pp.onError(new TestException("Forced failure")); + + ts.assertValues(1, 2); + ts.assertNoErrors(); + ts.assertNotComplete(); + + ts.request(2); + + ts.assertValues(1, 2, 3, 4); + ts.assertNoErrors(); + ts.assertComplete(); + } + +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableOnErrorResumeNextViaFunctionTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableOnErrorResumeNextViaFunctionTest.java new file mode 100644 index 0000000000..982ffecd1d --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableOnErrorResumeNextViaFunctionTest.java @@ -0,0 +1,305 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import java.io.IOException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +import org.junit.Test; +import org.mockito.Mockito; +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.internal.subscriptions.BooleanSubscription; +import io.reactivex.rxjava3.processors.PublishProcessor; +import io.reactivex.rxjava3.schedulers.Schedulers; +import io.reactivex.rxjava3.subscribers.TestSubscriber; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class FlowableOnErrorResumeNextViaFunctionTest extends RxJavaTest { + + @Test + public void resumeNextWithSynchronousExecution() { + final AtomicReference<Throwable> receivedException = new AtomicReference<>(); + Flowable<String> w = Flowable.unsafeCreate(new Publisher<String>() { + + @Override + public void subscribe(Subscriber<? super String> subscriber) { + subscriber.onSubscribe(new BooleanSubscription()); + subscriber.onNext("one"); + subscriber.onError(new Throwable("injected failure")); + subscriber.onNext("two"); + subscriber.onNext("three"); + } + }); + + Function<Throwable, Flowable<String>> resume = new Function<Throwable, Flowable<String>>() { + + @Override + public Flowable<String> apply(Throwable t1) { + receivedException.set(t1); + return Flowable.just("twoResume", "threeResume"); + } + + }; + Flowable<String> flowable = w.onErrorResumeNext(resume); + + Subscriber<String> subscriber = TestHelper.mockSubscriber(); + + flowable.subscribe(subscriber); + + verify(subscriber, Mockito.never()).onError(any(Throwable.class)); + verify(subscriber, times(1)).onComplete(); + verify(subscriber, times(1)).onNext("one"); + verify(subscriber, Mockito.never()).onNext("two"); + verify(subscriber, Mockito.never()).onNext("three"); + verify(subscriber, times(1)).onNext("twoResume"); + verify(subscriber, times(1)).onNext("threeResume"); + assertNotNull(receivedException.get()); + } + + @Test + public void resumeNextWithAsyncExecution() { + final AtomicReference<Throwable> receivedException = new AtomicReference<>(); + Subscription s = mock(Subscription.class); + TestFlowable w = new TestFlowable(s, "one"); + Function<Throwable, Flowable<String>> resume = new Function<Throwable, Flowable<String>>() { + + @Override + public Flowable<String> apply(Throwable t1) { + receivedException.set(t1); + return Flowable.just("twoResume", "threeResume"); + } + + }; + Flowable<String> flowable = Flowable.unsafeCreate(w).onErrorResumeNext(resume); + + Subscriber<String> subscriber = TestHelper.mockSubscriber(); + + flowable.subscribe(subscriber); + + try { + w.t.join(); + } catch (InterruptedException e) { + fail(e.getMessage()); + } + + verify(subscriber, Mockito.never()).onError(any(Throwable.class)); + verify(subscriber, times(1)).onComplete(); + verify(subscriber, times(1)).onNext("one"); + verify(subscriber, Mockito.never()).onNext("two"); + verify(subscriber, Mockito.never()).onNext("three"); + verify(subscriber, times(1)).onNext("twoResume"); + verify(subscriber, times(1)).onNext("threeResume"); + assertNotNull(receivedException.get()); + } + + /** + * Test that when a function throws an exception this is propagated through onError. + */ + @Test + public void functionThrowsError() { + Subscription s = mock(Subscription.class); + TestFlowable w = new TestFlowable(s, "one"); + Function<Throwable, Flowable<String>> resume = new Function<Throwable, Flowable<String>>() { + + @Override + public Flowable<String> apply(Throwable t1) { + throw new RuntimeException("exception from function"); + } + + }; + Flowable<String> flowable = Flowable.unsafeCreate(w).onErrorResumeNext(resume); + + Subscriber<String> subscriber = TestHelper.mockSubscriber(); + flowable.subscribe(subscriber); + + try { + w.t.join(); + } catch (InterruptedException e) { + fail(e.getMessage()); + } + + // we should get the "one" value before the error + verify(subscriber, times(1)).onNext("one"); + + // we should have received an onError call on the Observer since the resume function threw an exception + verify(subscriber, times(1)).onError(any(Throwable.class)); + verify(subscriber, times(0)).onComplete(); + } + + @Test + public void mapResumeAsyncNext() { + // Trigger multiple failures + Flowable<String> w = Flowable.just("one", "fail", "two", "three", "fail"); + + // Introduce map function that fails intermittently (Map does not prevent this when the observer is a + // rx.operator incl onErrorResumeNextViaFlowable) + w = w.map(new Function<String, String>() { + @Override + public String apply(String s) { + if ("fail".equals(s)) { + throw new RuntimeException("Forced Failure"); + } + System.out.println("BadMapper:" + s); + return s; + } + }); + + Flowable<String> flowable = w.onErrorResumeNext(new Function<Throwable, Flowable<String>>() { + + @Override + public Flowable<String> apply(Throwable t1) { + return Flowable.just("twoResume", "threeResume").subscribeOn(Schedulers.computation()); + } + + }); + + Subscriber<String> subscriber = TestHelper.mockSubscriber(); + + TestSubscriber<String> ts = new TestSubscriber<>(subscriber, Long.MAX_VALUE); + flowable.subscribe(ts); + ts.awaitDone(5, TimeUnit.SECONDS); + + verify(subscriber, Mockito.never()).onError(any(Throwable.class)); + verify(subscriber, times(1)).onComplete(); + verify(subscriber, times(1)).onNext("one"); + verify(subscriber, Mockito.never()).onNext("two"); + verify(subscriber, Mockito.never()).onNext("three"); + verify(subscriber, times(1)).onNext("twoResume"); + verify(subscriber, times(1)).onNext("threeResume"); + } + + private static class TestFlowable implements Publisher<String> { + + final String[] values; + Thread t; + + TestFlowable(Subscription s, String... values) { + this.values = values; + } + + @Override + public void subscribe(final Subscriber<? super String> subscriber) { + System.out.println("TestFlowable subscribed to ..."); + subscriber.onSubscribe(new BooleanSubscription()); + t = new Thread(new Runnable() { + + @Override + public void run() { + try { + System.out.println("running TestFlowable thread"); + for (String s : values) { + System.out.println("TestFlowable onNext: " + s); + subscriber.onNext(s); + } + throw new RuntimeException("Forced Failure"); + } catch (Throwable e) { + subscriber.onError(e); + } + } + + }); + System.out.println("starting TestFlowable thread"); + t.start(); + System.out.println("done starting TestFlowable thread"); + } + + } + + @Test + public void backpressure() { + TestSubscriber<Integer> ts = new TestSubscriber<>(); + Flowable.range(0, 100000) + .onErrorResumeNext(new Function<Throwable, Flowable<Integer>>() { + + @Override + public Flowable<Integer> apply(Throwable t1) { + return Flowable.just(1); + } + + }) + .observeOn(Schedulers.computation()) + .map(new Function<Integer, Integer>() { + int c; + + @Override + public Integer apply(Integer t1) { + if (c++ <= 1) { + // slow + try { + Thread.sleep(500); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + return t1; + } + + }) + .subscribe(ts); + ts.awaitDone(5, TimeUnit.SECONDS); + ts.assertNoErrors(); + } + + @Test + public void normalBackpressure() { + TestSubscriber<Integer> ts = TestSubscriber.create(0); + + PublishProcessor<Integer> pp = PublishProcessor.create(); + + pp.onErrorResumeNext(new Function<Throwable, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Throwable v) { + return Flowable.range(3, 2); + } + }).subscribe(ts); + + ts.request(2); + + pp.onNext(1); + pp.onNext(2); + pp.onError(new TestException("Forced failure")); + + ts.assertValues(1, 2); + ts.assertNoErrors(); + ts.assertNotComplete(); + + ts.request(2); + + ts.assertValues(1, 2, 3, 4); + ts.assertNoErrors(); + ts.assertComplete(); + } + + @Test + public void badOtherSource() { + TestHelper.checkBadSourceFlowable(new Function<Flowable<Integer>, Object>() { + @Override + public Object apply(Flowable<Integer> f) throws Exception { + return Flowable.error(new IOException()) + .onErrorResumeNext(Functions.justFunction(f)); + } + }, false, 1, 1, 1); + } + +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableOnErrorReturnTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableOnErrorReturnTest.java new file mode 100644 index 0000000000..15007f0228 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableOnErrorReturnTest.java @@ -0,0 +1,286 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +import org.junit.Test; +import org.mockito.Mockito; +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.subscriptions.BooleanSubscription; +import io.reactivex.rxjava3.processors.PublishProcessor; +import io.reactivex.rxjava3.schedulers.Schedulers; +import io.reactivex.rxjava3.subscribers.TestSubscriber; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class FlowableOnErrorReturnTest extends RxJavaTest { + + @Test + public void resumeNext() { + TestFlowable f = new TestFlowable("one"); + Flowable<String> w = Flowable.unsafeCreate(f); + final AtomicReference<Throwable> capturedException = new AtomicReference<>(); + + Flowable<String> flowable = w.onErrorReturn(new Function<Throwable, String>() { + + @Override + public String apply(Throwable e) { + capturedException.set(e); + return "failure"; + } + + }); + + Subscriber<String> subscriber = TestHelper.mockSubscriber(); + flowable.subscribe(subscriber); + + try { + f.t.join(); + } catch (InterruptedException e) { + fail(e.getMessage()); + } + + verify(subscriber, Mockito.never()).onError(any(Throwable.class)); + verify(subscriber, times(1)).onNext("one"); + verify(subscriber, times(1)).onNext("failure"); + verify(subscriber, times(1)).onComplete(); + assertNotNull(capturedException.get()); + } + + /** + * Test that when a function throws an exception this is propagated through onError. + */ + @Test + public void functionThrowsError() { + TestFlowable f = new TestFlowable("one"); + Flowable<String> w = Flowable.unsafeCreate(f); + final AtomicReference<Throwable> capturedException = new AtomicReference<>(); + + Flowable<String> flowable = w.onErrorReturn(new Function<Throwable, String>() { + + @Override + public String apply(Throwable e) { + capturedException.set(e); + throw new RuntimeException("exception from function"); + } + + }); + + Subscriber<String> subscriber = TestHelper.mockSubscriber(); + flowable.subscribe(subscriber); + + try { + f.t.join(); + } catch (InterruptedException e) { + fail(e.getMessage()); + } + + // we should get the "one" value before the error + verify(subscriber, times(1)).onNext("one"); + + // we should have received an onError call on the Observer since the resume function threw an exception + verify(subscriber, times(1)).onError(any(Throwable.class)); + verify(subscriber, times(0)).onComplete(); + assertNotNull(capturedException.get()); + } + + @Test + public void mapResumeAsyncNext() { + // Trigger multiple failures + Flowable<String> w = Flowable.just("one", "fail", "two", "three", "fail"); + + // Introduce map function that fails intermittently (Map does not prevent this when the observer is a + // rx.operator incl onErrorResumeNextViaFlowable) + w = w.map(new Function<String, String>() { + @Override + public String apply(String s) { + if ("fail".equals(s)) { + throw new RuntimeException("Forced Failure"); + } + System.out.println("BadMapper:" + s); + return s; + } + }); + + Flowable<String> flowable = w.onErrorReturn(new Function<Throwable, String>() { + + @Override + public String apply(Throwable t1) { + return "resume"; + } + + }); + + Subscriber<String> subscriber = TestHelper.mockSubscriber(); + TestSubscriber<String> ts = new TestSubscriber<>(subscriber, Long.MAX_VALUE); + flowable.subscribe(ts); + ts.awaitDone(5, TimeUnit.SECONDS); + + verify(subscriber, Mockito.never()).onError(any(Throwable.class)); + verify(subscriber, times(1)).onComplete(); + verify(subscriber, times(1)).onNext("one"); + verify(subscriber, Mockito.never()).onNext("two"); + verify(subscriber, Mockito.never()).onNext("three"); + verify(subscriber, times(1)).onNext("resume"); + } + + @Test + public void backpressure() { + TestSubscriber<Integer> ts = new TestSubscriber<>(); + Flowable.range(0, 100000) + .onErrorReturn(new Function<Throwable, Integer>() { + + @Override + public Integer apply(Throwable t1) { + return 1; + } + + }) + .observeOn(Schedulers.computation()) + .map(new Function<Integer, Integer>() { + int c; + + @Override + public Integer apply(Integer t1) { + if (c++ <= 1) { + // slow + try { + Thread.sleep(500); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + return t1; + } + + }) + .subscribe(ts); + ts.awaitDone(5, TimeUnit.SECONDS); + ts.assertNoErrors(); + } + + private static class TestFlowable implements Publisher<String> { + + final String[] values; + Thread t; + + TestFlowable(String... values) { + this.values = values; + } + + @Override + public void subscribe(final Subscriber<? super String> subscriber) { + subscriber.onSubscribe(new BooleanSubscription()); + System.out.println("TestFlowable subscribed to ..."); + t = new Thread(new Runnable() { + + @Override + public void run() { + try { + System.out.println("running TestFlowable thread"); + for (String s : values) { + System.out.println("TestFlowable onNext: " + s); + subscriber.onNext(s); + } + throw new RuntimeException("Forced Failure"); + } catch (Throwable e) { + subscriber.onError(e); + } + } + + }); + System.out.println("starting TestFlowable thread"); + t.start(); + System.out.println("done starting TestFlowable thread"); + } + } + + @Test + public void normalBackpressure() { + TestSubscriber<Integer> ts = TestSubscriber.create(0); + + PublishProcessor<Integer> pp = PublishProcessor.create(); + + pp.onErrorReturn(new Function<Throwable, Integer>() { + @Override + public Integer apply(Throwable e) { + return 3; + } + }).subscribe(ts); + + ts.request(2); + + pp.onNext(1); + pp.onNext(2); + pp.onError(new TestException("Forced failure")); + + ts.assertValues(1, 2); + ts.assertNoErrors(); + ts.assertNotComplete(); + + ts.request(2); + + ts.assertValues(1, 2, 3); + ts.assertNoErrors(); + ts.assertComplete(); + } + + @Test + public void returnItem() { + Flowable.error(new TestException()) + .onErrorReturnItem(1) + .test() + .assertResult(1); + } + + @Test + public void dispose() { + TestHelper.checkDisposed(Flowable.just(1).onErrorReturnItem(1)); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeFlowable(new Function<Flowable<Object>, Flowable<Object>>() { + @Override + public Flowable<Object> apply(Flowable<Object> f) throws Exception { + return f.onErrorReturnItem(1); + } + }); + } + + @Test + public void doubleOnError() { + new Flowable<Integer>() { + @Override + protected void subscribeActual(Subscriber<? super Integer> s) { + s.onSubscribe(new BooleanSubscription()); + s.onError(new TestException()); + s.onError(new TestException()); + } + } + .onErrorReturnItem(1) + .test() + .assertResult(1); + } + +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowablePublishFunctionTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowablePublishFunctionTest.java new file mode 100644 index 0000000000..5511fd0123 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowablePublishFunctionTest.java @@ -0,0 +1,450 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import static org.junit.Assert.*; + +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.*; +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.internal.subscriptions.BooleanSubscription; +import io.reactivex.rxjava3.processors.PublishProcessor; +import io.reactivex.rxjava3.schedulers.Schedulers; +import io.reactivex.rxjava3.subscribers.TestSubscriber; +import io.reactivex.rxjava3.testsupport.*; + +public class FlowablePublishFunctionTest extends RxJavaTest { + @Test + public void concatTakeFirstLastCompletes() { + TestSubscriber<Integer> ts = new TestSubscriber<>(); + + Flowable.range(1, 3) + .publish(f -> Flowable.concat(f.take(5), f.takeLast(5))) + .subscribe(ts); + + ts.assertValues(1, 2, 3); + ts.assertNoErrors(); + ts.assertComplete(); + } + + @Test + public void concatTakeFirstLastBackpressureCompletes() { + TestSubscriber<Integer> ts = TestSubscriber.create(0L); + + Flowable.range(1, 6) + .publish(f -> Flowable.concat(f.take(5), f.takeLast(5))) + .subscribe(ts); + + ts.assertNoValues(); + ts.assertNoErrors(); + ts.assertNotComplete(); + + ts.request(1); // make sure take() doesn't go unbounded + ts.request(4); + + ts.assertValues(1, 2, 3, 4, 5); + ts.assertNoErrors(); + ts.assertNotComplete(); + + ts.request(5); + + ts.assertValues(1, 2, 3, 4, 5, 6); + ts.assertNoErrors(); + ts.assertComplete(); + } + + @Test + public void canBeCancelled() { + TestSubscriber<Integer> ts = TestSubscriber.create(); + + PublishProcessor<Integer> pp = PublishProcessor.create(); + + pp.publish(f -> Flowable.concat(f.take(5), f.takeLast(5))).subscribe(ts); + + pp.onNext(1); + pp.onNext(2); + + ts.assertValues(1, 2); + ts.assertNoErrors(); + ts.assertNotComplete(); + + ts.cancel(); + + Assert.assertFalse("Source has subscribers?", pp.hasSubscribers()); + } + + @Test + public void invalidPrefetch() { + try { + Flowable.<Integer>never().publish(Functions.identity(), -99); + fail("Didn't throw IllegalArgumentException"); + } catch (IllegalArgumentException ex) { + Assert.assertEquals("prefetch > 0 required but it was -99", ex.getMessage()); + } + } + + @Test + public void takeCompletes() { + TestSubscriber<Integer> ts = TestSubscriber.create(); + + PublishProcessor<Integer> pp = PublishProcessor.create(); + + pp.publish(f -> f.take(1)).subscribe(ts); + + pp.onNext(1); + + ts.assertValues(1); + ts.assertNoErrors(); + ts.assertComplete(); + + Assert.assertFalse("Source has subscribers?", pp.hasSubscribers()); + + } + + @Test + public void oneStartOnly() { + + final AtomicInteger startCount = new AtomicInteger(); + + TestSubscriber<Integer> ts = new TestSubscriber<Integer>() { + @Override + public void onStart() { + startCount.incrementAndGet(); + } + }; + + PublishProcessor<Integer> pp = PublishProcessor.create(); + + pp.publish(f -> f.take(1)).subscribe(ts); + + Assert.assertEquals(1, startCount.get()); + } + + @Test + public void takeCompletesUnsafe() { + TestSubscriber<Integer> ts = TestSubscriber.create(); + + PublishProcessor<Integer> pp = PublishProcessor.create(); + + pp.publish(f -> f.take(1)).subscribe(ts); + + pp.onNext(1); + + ts.assertValues(1); + ts.assertNoErrors(); + ts.assertComplete(); + + Assert.assertFalse("Source has subscribers?", pp.hasSubscribers()); + } + + @Test + public void directCompletesUnsafe() { + TestSubscriber<Integer> ts = TestSubscriber.create(); + + PublishProcessor<Integer> pp = PublishProcessor.create(); + + pp.publish(Functions.identity()).subscribe(ts); + + pp.onNext(1); + pp.onComplete(); + + ts.assertValues(1); + ts.assertNoErrors(); + ts.assertComplete(); + + Assert.assertFalse("Source has subscribers?", pp.hasSubscribers()); + } + + @Test + public void overflowMissingBackpressureException() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(0); + + PublishProcessor<Integer> pp = PublishProcessor.create(); + + pp.publish(Functions.identity()).subscribe(ts); + + for (int i = 0; i < Flowable.bufferSize() * 2; i++) { + pp.onNext(i); + } + + ts.assertNoValues(); + ts.assertError(MissingBackpressureException.class); + ts.assertNotComplete(); + + Assert.assertEquals(MissingBackpressureException.DEFAULT_MESSAGE, + ts.errors().get(0).getMessage()); + Assert.assertFalse("Source has subscribers?", pp.hasSubscribers()); + } + + @Test + public void overflowMissingBackpressureExceptionDelayed() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(0); + + PublishProcessor<Integer> pp = PublishProcessor.create(); + + new FlowablePublishMulticast<>(pp, Functions.identity(), Flowable.bufferSize(), true).subscribe(ts); + + for (int i = 0; i < Flowable.bufferSize() * 2; i++) { + pp.onNext(i); + } + + ts.request(Flowable.bufferSize()); + + ts.assertValueCount(Flowable.bufferSize()); + ts.assertError(MissingBackpressureException.class); + ts.assertNotComplete(); + + Assert.assertEquals(MissingBackpressureException.DEFAULT_MESSAGE, ts.errors().get(0).getMessage()); + Assert.assertFalse("Source has subscribers?", pp.hasSubscribers()); + } + + @Test + public void emptyIdentityMapped() { + Flowable.empty() + .publish(Functions.identity()) + .test() + .assertResult(); + } + + @Test + public void independentlyMapped() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + + TestSubscriber<Integer> ts = pp.publish(v -> Flowable.range(1, 5)).test(0); + + assertTrue("pp has no Subscribers?!", pp.hasSubscribers()); + + ts.assertNoValues() + .assertNoErrors() + .assertNotComplete(); + + ts.request(5); + + ts.assertResult(1, 2, 3, 4, 5); + + assertFalse("pp has Subscribers?!", pp.hasSubscribers()); + } + + @Test + public void badSource() { + TestHelper.checkBadSourceFlowable(f -> f.publish(Functions.identity()), false, 1, 1, 1); + } + + @Test + public void frontOverflow() { + new Flowable<Integer>() { + @Override + protected void subscribeActual(Subscriber<? super Integer> s) { + s.onSubscribe(new BooleanSubscription()); + for (int i = 0; i < 9; i++) { + s.onNext(i); + } + } + } + .publish(Functions.identity(), 8) + .test(0) + .assertFailure(MissingBackpressureException.class); + } + + @Test + public void errorResubscribe() { + Flowable.error(new TestException()) + .publish(f -> f.onErrorResumeWith(f)) + .test() + .assertFailure(TestException.class); + } + + @Test + public void fusedInputCrash() { + Flowable.just(1) + .map(v -> { + throw new TestException(); + }) + .publish(Functions.identity()) + .test() + .assertFailure(TestException.class); + } + + @Test + public void error() { + new FlowablePublishMulticast<>(Flowable.just(1).concatWith(Flowable.error(new TestException())), + Functions.identity(), 16, true) + .test() + .assertFailure(TestException.class, 1); + } + + @Test + public void backpressuredEmpty() { + Flowable.<Integer>empty() + .publish(Functions.identity()) + .test(0L) + .assertResult(); + } + + @Test + public void oneByOne() { + Flowable.range(1, 10) + .publish(Functions.identity()) + .rebatchRequests(1) + .test() + .assertResult(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + } + + @Test + public void completeCancelRaceNoRequest() { + final PublishProcessor<Integer> pp = PublishProcessor.create(); + + final TestSubscriber<Integer> ts = new TestSubscriber<Integer>(1L) { + @Override + public void onNext(Integer t) { + super.onNext(t); + if (t == 1) { + cancel(); + onComplete(); + } + } + }; + + pp.publish(Functions.identity()).subscribe(ts); + + pp.onNext(1); + + assertFalse(pp.hasSubscribers()); + + ts.assertResult(1); + } + + @Test + public void inputOutputSubscribeRace() { + Flowable<Integer> source = Flowable.just(1) + .publish(f -> f.subscribeOn(Schedulers.single())); + + for (int i = 0; i < 500; i++) { + source.test() + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(1); + } + } + + @Test + public void inputOutputSubscribeRace2() { + Flowable<Integer> source = Flowable.just(1).subscribeOn(Schedulers.single()) + .publish(Functions.identity()); + + for (int i = 0; i < 500; i++) { + source.test() + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(1); + } + } + + @Test + public void sourceSubscriptionDelayed() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final TestSubscriber<Integer> ts1 = new TestSubscriber<>(0L); + + Flowable.just(1) + .publish(f -> { + Runnable r1 = () -> f.subscribe(ts1); + + Runnable r2 = () -> { + for (int j = 0; j < 100; j++) { + ts1.request(1); + } + }; + + TestHelper.race(r1, r2); + return f; + }).test() + .assertResult(1); + + ts1.assertResult(1); + } + } + + @Test + public void longFlow() { + Flowable.range(1, 1000000) + .publish(v -> Flowable.mergeArray( + v.filter(w -> w % 2 == 0), + v.filter(w -> w % 2 != 0))) + .takeLast(1) + .test() + .assertResult(1000000); + } + + @Test + public void longFlow2() { + Flowable.range(1, 100000) + .publish(v -> Flowable.mergeArray( + v.filter(w -> w % 2 == 0), + v.filter(w -> w % 2 != 0))) + .test() + .assertValueCount(100000) + .assertNoErrors() + .assertComplete(); + } + + @Test + public void longFlowHidden() { + Flowable.range(1, 1000000).hide() + .publish(v -> Flowable.mergeArray( + v.filter(w -> w % 2 == 0), + v.filter(w -> w % 2 != 0))) + .takeLast(1) + .test() + .assertResult(1000000); + } + + @Test + public void noUpstreamCancelOnCasualChainClose() { + AtomicBoolean parentUpstreamCancelled = new AtomicBoolean(false); + Flowable.range(1, 10) + .doOnCancel(() -> parentUpstreamCancelled.set(true)) + .publish(Functions.identity()) + .test() + .awaitDone(1, TimeUnit.SECONDS); + assertFalse("Unnecessary upstream .cancel() call in FlowablePublishMulticast", parentUpstreamCancelled.get()); + } + + @Test + public void noUpstreamCancelOnCasualChainCloseWithInnerCancels() { + AtomicBoolean parentUpstreamCancelled = new AtomicBoolean(false); + Flowable.range(1, 10) + .doOnCancel(() -> parentUpstreamCancelled.set(true)) + .publish(v -> Flowable.concat(v.take(1), v.skip(5))) + .test() + .awaitDone(1, TimeUnit.SECONDS); + assertFalse("Unnecessary upstream .cancel() call in FlowablePublishMulticast", parentUpstreamCancelled.get()); + } + + @Test + public void upstreamCancelOnDownstreamCancel() { + AtomicBoolean parentUpstreamCancelled = new AtomicBoolean(false); + Flowable.range(1, 10) + .doOnCancel(() -> parentUpstreamCancelled.set(true)) + .publish(Functions.identity()) + .take(1) + .test() + .awaitDone(1, TimeUnit.SECONDS); + assertTrue("Upstream .cancel() not called in FlowablePublishMulticast", parentUpstreamCancelled.get()); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowablePublishMulticastTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowablePublishMulticastTest.java new file mode 100644 index 0000000000..0e1bdbbdc0 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowablePublishMulticastTest.java @@ -0,0 +1,224 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import static org.junit.Assert.*; + +import java.util.List; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.RxJavaTest; +import io.reactivex.rxjava3.internal.operators.flowable.FlowablePublishMulticast.*; +import io.reactivex.rxjava3.internal.subscriptions.BooleanSubscription; +import io.reactivex.rxjava3.operators.QueueSubscription; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.processors.UnicastProcessor; +import io.reactivex.rxjava3.subscribers.TestSubscriber; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class FlowablePublishMulticastTest extends RxJavaTest { + + @Test + public void asyncFusedInput() { + MulticastProcessor<Integer> mp = new MulticastProcessor<>(128, true); + + UnicastProcessor<Integer> up = UnicastProcessor.create(); + + up.subscribe(mp); + + TestSubscriber<Integer> ts1 = mp.test(); + TestSubscriber<Integer> ts2 = mp.take(1).test(); + + up.onNext(1); + up.onNext(2); + up.onComplete(); + + ts1.assertResult(1, 2); + ts2.assertResult(1); + } + + @Test + public void fusionRejectedInput() { + MulticastProcessor<Integer> mp = new MulticastProcessor<>(128, true); + + mp.onSubscribe(new QueueSubscription<Integer>() { + + @Override + public int requestFusion(int mode) { + return 0; + } + + @Override + public boolean offer(Integer value) { + return false; + } + + @Override + public boolean offer(Integer v1, Integer v2) { + return false; + } + + @Override + public Integer poll() throws Exception { + return null; + } + + @Override + public boolean isEmpty() { + return false; + } + + @Override + public void clear() { + } + + @Override + public void request(long n) { + } + + @Override + public void cancel() { + } + }); + + TestSubscriber<Integer> ts = mp.test(); + + mp.onNext(1); + mp.onNext(2); + mp.onComplete(); + + ts.assertResult(1, 2); + } + + @Test + public void addRemoveRace() { + for (int i = 0; i < TestHelper.RACE_LONG_LOOPS; i++) { + + final MulticastProcessor<Integer> mp = new MulticastProcessor<>(128, true); + + final MulticastSubscription<Integer> ms1 = new MulticastSubscription<>(null, mp); + final MulticastSubscription<Integer> ms2 = new MulticastSubscription<>(null, mp); + + assertTrue(mp.add(ms1)); + + Runnable r1 = new Runnable() { + @Override + public void run() { + mp.remove(ms1); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + mp.add(ms2); + } + }; + + TestHelper.race(r1, r2); + } + } + + @Test + public void removeNotFound() { + MulticastProcessor<Integer> mp = new MulticastProcessor<>(128, true); + + MulticastSubscription<Integer> ms1 = new MulticastSubscription<>(null, mp); + assertTrue(mp.add(ms1)); + + mp.remove(null); + } + + @Test + public void errorAllCancelled() { + MulticastProcessor<Integer> mp = new MulticastProcessor<>(128, true); + + MulticastSubscription<Integer> ms1 = new MulticastSubscription<>(null, mp); + assertTrue(mp.add(ms1)); + + ms1.set(Long.MIN_VALUE); + + mp.errorAll(null); + } + + @Test + public void completeAllCancelled() { + MulticastProcessor<Integer> mp = new MulticastProcessor<>(128, true); + + MulticastSubscription<Integer> ms1 = new MulticastSubscription<>(null, mp); + assertTrue(mp.add(ms1)); + + ms1.set(Long.MIN_VALUE); + + mp.completeAll(); + } + + @Test + public void cancelledWhileFindingRequests() { + final MulticastProcessor<Integer> mp = new MulticastProcessor<>(128, true); + + final MulticastSubscription<Integer> ms1 = new MulticastSubscription<>(null, mp); + + assertTrue(mp.add(ms1)); + + mp.onSubscribe(new BooleanSubscription()); + + ms1.set(Long.MIN_VALUE); + + mp.drain(); + } + + @Test + public void negativeRequest() { + final MulticastProcessor<Integer> mp = new MulticastProcessor<>(128, true); + + final MulticastSubscription<Integer> ms1 = new MulticastSubscription<>(null, mp); + + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + ms1.request(-1); + + TestHelper.assertError(errors, 0, IllegalArgumentException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void outputCancellerDoubleOnSubscribe() { + TestHelper.doubleOnSubscribe(new OutputCanceller<>(new TestSubscriber<>(), null)); + } + + @Test + public void dontDropItemsWhenNoReadyConsumers() { + final MulticastProcessor<Integer> mp = new MulticastProcessor<>(128, true); + + mp.onSubscribe(new BooleanSubscription()); + + mp.onNext(1); + + TestSubscriber<Integer> ts = new TestSubscriber<>(); + final MulticastSubscription<Integer> ms1 = new MulticastSubscription<>(ts, mp); + ts.onSubscribe(ms1); + + assertTrue(mp.add(ms1)); + + ms1.set(Long.MIN_VALUE); + + mp.drain(); + + assertFalse(mp.queue.isEmpty()); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowablePublishTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowablePublishTest.java new file mode 100644 index 0000000000..c3355e9a38 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowablePublishTest.java @@ -0,0 +1,1806 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import static org.junit.Assert.*; + +import java.util.*; +import java.util.concurrent.*; +import java.util.concurrent.atomic.*; + +import org.junit.Test; +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.flowables.ConnectableFlowable; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.internal.fuseable.HasUpstreamPublisher; +import io.reactivex.rxjava3.internal.operators.flowable.FlowablePublish.*; +import io.reactivex.rxjava3.internal.schedulers.ImmediateThinScheduler; +import io.reactivex.rxjava3.internal.subscriptions.BooleanSubscription; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.processors.PublishProcessor; +import io.reactivex.rxjava3.schedulers.*; +import io.reactivex.rxjava3.subscribers.TestSubscriber; +import io.reactivex.rxjava3.testsupport.*; + +public class FlowablePublishTest extends RxJavaTest { + + @Test + public void publish() throws InterruptedException { + final AtomicInteger counter = new AtomicInteger(); + ConnectableFlowable<String> f = Flowable.unsafeCreate(new Publisher<String>() { + + @Override + public void subscribe(final Subscriber<? super String> subscriber) { + subscriber.onSubscribe(new BooleanSubscription()); + new Thread(new Runnable() { + + @Override + public void run() { + counter.incrementAndGet(); + subscriber.onNext("one"); + subscriber.onComplete(); + } + }).start(); + } + }).publish(); + + final CountDownLatch latch = new CountDownLatch(2); + + // subscribe once + f.subscribe(new Consumer<String>() { + + @Override + public void accept(String v) { + assertEquals("one", v); + latch.countDown(); + } + }); + + // subscribe again + f.subscribe(new Consumer<String>() { + + @Override + public void accept(String v) { + assertEquals("one", v); + latch.countDown(); + } + }); + + Disposable connection = f.connect(); + try { + if (!latch.await(1000, TimeUnit.MILLISECONDS)) { + fail("subscriptions did not receive values"); + } + assertEquals(1, counter.get()); + } finally { + connection.dispose(); + } + } + + @Test + public void backpressureFastSlow() { + ConnectableFlowable<Integer> is = Flowable.range(1, Flowable.bufferSize() * 2).publish(); + Flowable<Integer> fast = is.observeOn(Schedulers.computation()) + .doOnComplete(new Action() { + @Override + public void run() { + System.out.println("^^^^^^^^^^^^^ completed FAST"); + } + }); + + Flowable<Integer> slow = is.observeOn(Schedulers.computation()).map(new Function<Integer, Integer>() { + int c; + + @Override + public Integer apply(Integer i) { + if (c == 0) { + try { + Thread.sleep(500); + } catch (InterruptedException e) { + } + } + c++; + return i; + } + + }).doOnComplete(new Action() { + + @Override + public void run() { + System.out.println("^^^^^^^^^^^^^ completed SLOW"); + } + + }); + + TestSubscriber<Integer> ts = new TestSubscriber<>(); + Flowable.merge(fast, slow).subscribe(ts); + is.connect(); + ts.awaitDone(5, TimeUnit.SECONDS); + ts.assertNoErrors(); + assertEquals(Flowable.bufferSize() * 4, ts.values().size()); + } + + // use case from https://github.com/ReactiveX/RxJava/issues/1732 + @Test + public void takeUntilWithPublishedStreamUsingSelector() { + final AtomicInteger emitted = new AtomicInteger(); + Flowable<Integer> xs = Flowable.range(0, Flowable.bufferSize() * 2).doOnNext(new Consumer<Integer>() { + + @Override + public void accept(Integer t1) { + emitted.incrementAndGet(); + } + + }); + TestSubscriber<Integer> ts = new TestSubscriber<>(); + xs.publish(new Function<Flowable<Integer>, Flowable<Integer>>() { + + @Override + public Flowable<Integer> apply(Flowable<Integer> xs) { + return xs.takeUntil(xs.skipWhile(new Predicate<Integer>() { + + @Override + public boolean test(Integer i) { + return i <= 3; + } + + })); + } + + }).subscribe(ts); + ts.awaitDone(5, TimeUnit.SECONDS); + ts.assertNoErrors(); + ts.assertValues(0, 1, 2, 3); + assertEquals(5, emitted.get()); + System.out.println(ts.values()); + } + + // use case from https://github.com/ReactiveX/RxJava/issues/1732 + @Test + public void takeUntilWithPublishedStream() { + Flowable<Integer> xs = Flowable.range(0, Flowable.bufferSize() * 2); + TestSubscriber<Integer> ts = new TestSubscriber<>(); + ConnectableFlowable<Integer> xsp = xs.publish(); + xsp.takeUntil(xsp.skipWhile(new Predicate<Integer>() { + + @Override + public boolean test(Integer i) { + return i <= 3; + } + + })).subscribe(ts); + xsp.connect(); + System.out.println(ts.values()); + } + + @Test + public void backpressureTwoConsumers() { + final AtomicInteger sourceEmission = new AtomicInteger(); + final AtomicBoolean sourceUnsubscribed = new AtomicBoolean(); + final Flowable<Integer> source = Flowable.range(1, 100) + .doOnNext(new Consumer<Integer>() { + @Override + public void accept(Integer t1) { + sourceEmission.incrementAndGet(); + } + }) + .doOnCancel(new Action() { + @Override + public void run() { + sourceUnsubscribed.set(true); + } + }).share(); + ; + + final AtomicBoolean child1Unsubscribed = new AtomicBoolean(); + final AtomicBoolean child2Unsubscribed = new AtomicBoolean(); + + final TestSubscriber<Integer> ts2 = new TestSubscriber<>(); + + final TestSubscriber<Integer> ts1 = new TestSubscriber<Integer>() { + @Override + public void onNext(Integer t) { + if (values().size() == 2) { + source.doOnCancel(new Action() { + @Override + public void run() { + child2Unsubscribed.set(true); + } + }).take(5).subscribe(ts2); + } + super.onNext(t); + } + }; + + source.doOnCancel(new Action() { + @Override + public void run() { + child1Unsubscribed.set(true); + } + }).take(5) + .subscribe(ts1); + + ts1.awaitDone(5, TimeUnit.SECONDS); + ts2.awaitDone(5, TimeUnit.SECONDS); + + ts1.assertNoErrors(); + ts2.assertNoErrors(); + + assertTrue(sourceUnsubscribed.get()); + assertTrue(child1Unsubscribed.get()); + assertTrue(child2Unsubscribed.get()); + + ts1.assertValues(1, 2, 3, 4, 5); + ts2.assertValues(4, 5, 6, 7, 8); + + assertEquals(8, sourceEmission.get()); + } + + @Test + public void connectWithNoSubscriber() { + TestScheduler scheduler = new TestScheduler(); + ConnectableFlowable<Long> cf = Flowable.interval(10, 10, TimeUnit.MILLISECONDS, scheduler).take(3).publish(); + cf.connect(); + // Emit 0 + scheduler.advanceTimeBy(15, TimeUnit.MILLISECONDS); + TestSubscriber<Long> subscriber = new TestSubscriber<>(); + cf.subscribe(subscriber); + // Emit 1 and 2 + scheduler.advanceTimeBy(50, TimeUnit.MILLISECONDS); + // 3.x: Flowable.publish no longer drains the input buffer if there are no subscribers + subscriber.assertResult(0L, 1L, 2L); + } + + @Test + public void subscribeAfterDisconnectThenConnect() { + ConnectableFlowable<Integer> source = Flowable.just(1).publish(); + + TestSubscriberEx<Integer> ts1 = new TestSubscriberEx<>(); + + source.subscribe(ts1); + + Disposable connection = source.connect(); + + ts1.assertValue(1); + ts1.assertNoErrors(); + ts1.assertTerminated(); + + source.reset(); + + TestSubscriberEx<Integer> ts2 = new TestSubscriberEx<>(); + + source.subscribe(ts2); + + Disposable connection2 = source.connect(); + + ts2.assertValue(1); + ts2.assertNoErrors(); + ts2.assertTerminated(); + + System.out.println(connection); + System.out.println(connection2); + } + + @Test + public void noSubscriberRetentionOnCompleted() { + FlowablePublish<Integer> source = (FlowablePublish<Integer>)Flowable.just(1).publish(); + + TestSubscriberEx<Integer> ts1 = new TestSubscriberEx<>(); + + source.subscribe(ts1); + + ts1.assertNoValues(); + ts1.assertNoErrors(); + ts1.assertNotComplete(); + + source.connect(); + + ts1.assertValue(1); + ts1.assertNoErrors(); + ts1.assertTerminated(); + + assertEquals(0, source.current.get().subscribers.get().length); + } + + @Test + public void nonNullConnection() { + ConnectableFlowable<Object> source = Flowable.never().publish(); + + assertNotNull(source.connect()); + assertNotNull(source.connect()); + } + + @Test + public void noDisconnectSomeoneElse() { + ConnectableFlowable<Object> source = Flowable.never().publish(); + + Disposable connection1 = source.connect(); + Disposable connection2 = source.connect(); + + connection1.dispose(); + + Disposable connection3 = source.connect(); + + connection2.dispose(); + + assertTrue(checkPublishDisposed(connection1)); + assertTrue(checkPublishDisposed(connection2)); + assertFalse(checkPublishDisposed(connection3)); + } + + @SuppressWarnings("unchecked") + static boolean checkPublishDisposed(Disposable d) { + return ((FlowablePublish.PublishConnection<Object>)d).isDisposed(); + } + + @Test + public void zeroRequested() { + ConnectableFlowable<Integer> source = Flowable.just(1).publish(); + + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(0L); + + source.subscribe(ts); + + ts.assertNoValues(); + ts.assertNoErrors(); + ts.assertNotComplete(); + + source.connect(); + + ts.assertNoValues(); + ts.assertNoErrors(); + ts.assertNotComplete(); + + ts.request(5); + + ts.assertValue(1); + ts.assertNoErrors(); + ts.assertTerminated(); + } + + @Test + public void connectIsIdempotent() { + final AtomicInteger calls = new AtomicInteger(); + Flowable<Integer> source = Flowable.unsafeCreate(new Publisher<Integer>() { + @Override + public void subscribe(Subscriber<? super Integer> t) { + t.onSubscribe(new BooleanSubscription()); + calls.getAndIncrement(); + } + }); + + ConnectableFlowable<Integer> conn = source.publish(); + + assertEquals(0, calls.get()); + + conn.connect(); + conn.connect(); + + assertEquals(1, calls.get()); + + conn.connect().dispose(); + + conn.connect(); + conn.connect(); + + assertEquals(2, calls.get()); + } + + @Test + public void syncFusedObserveOn() { + ConnectableFlowable<Integer> cf = Flowable.range(0, 1000).publish(); + Flowable<Integer> obs = cf.observeOn(Schedulers.computation()); + for (int i = 0; i < 1000; i++) { + for (int j = 1; j < 6; j++) { + List<TestSubscriberEx<Integer>> tss = new ArrayList<>(); + for (int k = 1; k < j; k++) { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(); + tss.add(ts); + obs.subscribe(ts); + } + + Disposable connection = cf.connect(); + + for (TestSubscriberEx<Integer> ts : tss) { + ts.awaitDone(5, TimeUnit.SECONDS) + .assertSubscribed() + .assertValueCount(1000) + .assertNoErrors() + .assertComplete(); + } + connection.dispose(); + } + } + } + + @Test + public void syncFusedObserveOn2() { + ConnectableFlowable<Integer> cf = Flowable.range(0, 1000).publish(); + Flowable<Integer> obs = cf.observeOn(ImmediateThinScheduler.INSTANCE); + for (int i = 0; i < 1000; i++) { + for (int j = 1; j < 6; j++) { + List<TestSubscriberEx<Integer>> tss = new ArrayList<>(); + for (int k = 1; k < j; k++) { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(); + tss.add(ts); + obs.subscribe(ts); + } + + Disposable connection = cf.connect(); + + for (TestSubscriberEx<Integer> ts : tss) { + ts.awaitDone(5, TimeUnit.SECONDS) + .assertSubscribed() + .assertValueCount(1000) + .assertNoErrors() + .assertComplete(); + } + connection.dispose(); + } + } + } + + @Test + public void asyncFusedObserveOn() { + ConnectableFlowable<Integer> cf = Flowable.range(0, 1000).observeOn(ImmediateThinScheduler.INSTANCE).publish(); + for (int i = 0; i < 1000; i++) { + for (int j = 1; j < 6; j++) { + List<TestSubscriberEx<Integer>> tss = new ArrayList<>(); + for (int k = 1; k < j; k++) { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(); + tss.add(ts); + cf.subscribe(ts); + } + + Disposable connection = cf.connect(); + + for (TestSubscriberEx<Integer> ts : tss) { + ts.awaitDone(5, TimeUnit.SECONDS) + .assertSubscribed() + .assertValueCount(1000) + .assertNoErrors() + .assertComplete(); + } + connection.dispose(); + } + } + } + + @Test + public void observeOn() { + ConnectableFlowable<Integer> cf = Flowable.range(0, 1000).hide().publish(); + Flowable<Integer> obs = cf.observeOn(Schedulers.computation()); + for (int i = 0; i < 1000; i++) { + for (int j = 1; j < 6; j++) { + List<TestSubscriberEx<Integer>> tss = new ArrayList<>(); + for (int k = 1; k < j; k++) { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(); + tss.add(ts); + obs.subscribe(ts); + } + + Disposable connection = cf.connect(); + + for (TestSubscriberEx<Integer> ts : tss) { + ts.awaitDone(5, TimeUnit.SECONDS) + .assertSubscribed() + .assertValueCount(1000) + .assertNoErrors() + .assertComplete(); + } + connection.dispose(); + } + } + } + + @Test + public void source() { + Flowable<Integer> f = Flowable.never(); + + assertSame(f, (((HasUpstreamPublisher<?>)f.publish()).source())); + } + + @Test + public void connectThrows() { + ConnectableFlowable<Integer> cf = Flowable.<Integer>empty().publish(); + try { + cf.connect(new Consumer<Disposable>() { + @Override + public void accept(Disposable d) throws Exception { + throw new TestException(); + } + }); + } catch (TestException ex) { + // expected + } + } + + @Test + public void addRemoveRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + + final ConnectableFlowable<Integer> cf = Flowable.<Integer>empty().publish(); + + final TestSubscriber<Integer> ts = cf.test(); + + final TestSubscriber<Integer> ts2 = new TestSubscriber<>(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + cf.subscribe(ts2); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + ts.cancel(); + } + }; + + TestHelper.race(r1, r2); + } + } + + @Test + public void disposeOnArrival() { + ConnectableFlowable<Integer> cf = Flowable.<Integer>empty().publish(); + + cf.test(Long.MAX_VALUE, true).assertEmpty(); + } + + @Test + public void disposeOnArrival2() { + Flowable<Integer> co = Flowable.<Integer>never().publish().autoConnect(); + + co.test(Long.MAX_VALUE, true).assertEmpty(); + } + + @Test + public void dispose() { + TestHelper.checkDisposed(Flowable.never().publish()); + + TestHelper.checkDisposed(Flowable.never().publish(Functions.<Flowable<Object>>identity())); + } + + @Test + public void empty() { + ConnectableFlowable<Integer> cf = Flowable.<Integer>empty().publish(); + + cf.connect(); + } + + @Test + public void take() { + ConnectableFlowable<Integer> cf = Flowable.range(1, 2).publish(); + + TestSubscriber<Integer> ts = cf.take(1).test(); + + cf.connect(); + + ts.assertResult(1); + } + + @Test + public void just() { + final PublishProcessor<Integer> pp = PublishProcessor.create(); + + ConnectableFlowable<Integer> cf = pp.publish(); + + TestSubscriber<Integer> ts = new TestSubscriber<Integer>() { + @Override + public void onNext(Integer t) { + super.onNext(t); + pp.onComplete(); + } + }; + + cf.subscribe(ts); + cf.connect(); + + pp.onNext(1); + + ts.assertResult(1); + } + + @Test + public void nextCancelRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + + final PublishProcessor<Integer> pp = PublishProcessor.create(); + + final ConnectableFlowable<Integer> cf = pp.publish(); + + final TestSubscriber<Integer> ts = cf.test(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + pp.onNext(1); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + ts.cancel(); + } + }; + + TestHelper.race(r1, r2); + } + } + + @Test + public void badSource() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + new Flowable<Integer>() { + @Override + protected void subscribeActual(Subscriber<? super Integer> subscriber) { + subscriber.onSubscribe(new BooleanSubscription()); + subscriber.onNext(1); + subscriber.onComplete(); + subscriber.onNext(2); + subscriber.onError(new TestException()); + subscriber.onComplete(); + } + } + .publish() + .autoConnect() + .test() + .assertResult(1); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void noErrorLoss() { + ConnectableFlowable<Object> cf = Flowable.error(new TestException()).publish(); + + cf.connect(); + + // 3.x: terminal events are always kept until reset. + cf.test() + .assertFailure(TestException.class); + } + + @Test + public void subscribeDisconnectRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + + final PublishProcessor<Integer> pp = PublishProcessor.create(); + + final ConnectableFlowable<Integer> cf = pp.publish(); + + final Disposable d = cf.connect(); + final TestSubscriber<Integer> ts = new TestSubscriber<>(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + d.dispose(); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + cf.subscribe(ts); + } + }; + + TestHelper.race(r1, r2); + } + } + + @Test + public void selectorDisconnectsIndependentSource() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + + pp.publish(new Function<Flowable<Integer>, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Flowable<Integer> v) throws Exception { + return Flowable.range(1, 2); + } + }) + .test() + .assertResult(1, 2); + + assertFalse(pp.hasSubscribers()); + } + + @Test + public void selectorLatecommer() { + Flowable.range(1, 5) + .publish(new Function<Flowable<Integer>, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Flowable<Integer> v) throws Exception { + return v.concatWith(v); + } + }) + .test() + .assertResult(1, 2, 3, 4, 5); + } + + @Test + public void mainError() { + Flowable.error(new TestException()) + .publish(Functions.<Flowable<Object>>identity()) + .test() + .assertFailure(TestException.class); + } + + @Test + public void selectorInnerError() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + + pp.publish(new Function<Flowable<Integer>, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Flowable<Integer> v) throws Exception { + return Flowable.error(new TestException()); + } + }) + .test() + .assertFailure(TestException.class); + + assertFalse(pp.hasSubscribers()); + } + + @Test + public void preNextConnect() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + + final ConnectableFlowable<Integer> cf = Flowable.<Integer>empty().publish(); + + cf.connect(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + cf.test(); + } + }; + + TestHelper.race(r1, r1); + } + } + + @Test + public void connectRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + + final ConnectableFlowable<Integer> cf = Flowable.<Integer>empty().publish(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + cf.connect(); + } + }; + + TestHelper.race(r1, r1); + } + } + + @Test + public void selectorCrash() { + Flowable.just(1).publish(new Function<Flowable<Integer>, Flowable<Object>>() { + @Override + public Flowable<Object> apply(Flowable<Integer> v) throws Exception { + throw new TestException(); + } + }) + .test() + .assertFailure(TestException.class); + } + + @Test + public void pollThrows() { + Flowable.just(1) + .map(new Function<Integer, Object>() { + @Override + public Object apply(Integer v) throws Exception { + throw new TestException(); + } + }) + .compose(TestHelper.flowableStripBoundary()) + .publish() + .autoConnect() + .test() + .assertFailure(TestException.class); + } + + @Test + public void pollThrowsNoSubscribers() { + ConnectableFlowable<Integer> cf = Flowable.just(1, 2) + .map(new Function<Integer, Integer>() { + @Override + public Integer apply(Integer v) throws Exception { + if (v == 2) { + throw new TestException(); + } + return v; + } + }) + .compose(TestHelper.<Integer>flowableStripBoundary()) + .publish(); + + TestSubscriber<Integer> ts = cf.take(1) + .test(); + + cf.connect(); + + ts.assertResult(1); + } + + @Test + public void dryRunCrash() { + final TestSubscriber<Object> ts = new TestSubscriber<Object>(1L) { + @Override + public void onNext(Object t) { + super.onNext(t); + onComplete(); + cancel(); + } + }; + + Flowable<Object> source = Flowable.range(1, 10) + .map(new Function<Integer, Object>() { + @Override + public Object apply(Integer v) throws Exception { + if (v == 2) { + throw new TestException(); + } + return v; + } + }) + .publish() + .autoConnect(); + + source.subscribe(ts); + + ts + .assertResult(1); + + // 3.x: terminal events remain observable until reset + source.test() + .assertFailure(TestException.class); + } + + @Test + public void overflowQueue() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + Flowable.create(new FlowableOnSubscribe<Object>() { + @Override + public void subscribe(FlowableEmitter<Object> s) throws Exception { + for (int i = 0; i < 10; i++) { + s.onNext(i); + } + } + }, BackpressureStrategy.MISSING) + .publish(8) + .autoConnect() + .test(0L) + // 3.x emits errors last, even the full queue errors + .requestMore(10) + .assertFailure(QueueOverflowException.class, 0, 1, 2, 3, 4, 5, 6, 7); + + TestHelper.assertError(errors, 0, QueueOverflowException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void delayedUpstreamOnSubscribe() { + final Subscriber<?>[] sub = { null }; + + new Flowable<Integer>() { + @Override + protected void subscribeActual(Subscriber<? super Integer> s) { + sub[0] = s; + } + } + .publish() + .connect() + .dispose(); + + BooleanSubscription bs = new BooleanSubscription(); + + sub[0].onSubscribe(bs); + + assertTrue(bs.isCancelled()); + } + + @Test + public void disposeRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + + final AtomicReference<Disposable> ref = new AtomicReference<>(); + + final ConnectableFlowable<Integer> cf = new Flowable<Integer>() { + @Override + protected void subscribeActual(Subscriber<? super Integer> s) { + s.onSubscribe(new BooleanSubscription()); + ref.set((Disposable)s); + } + }.publish(); + + cf.connect(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + ref.get().dispose(); + } + }; + + TestHelper.race(r1, r1); + } + } + + @Test + public void removeNotPresent() { + final AtomicReference<PublishConnection<Integer>> ref = new AtomicReference<>(); + + final ConnectableFlowable<Integer> cf = new Flowable<Integer>() { + @Override + @SuppressWarnings("unchecked") + protected void subscribeActual(Subscriber<? super Integer> s) { + s.onSubscribe(new BooleanSubscription()); + ref.set((PublishConnection<Integer>)s); + } + }.publish(); + + cf.connect(); + + ref.get().add(new InnerSubscription<>(new TestSubscriber<>(), ref.get())); + ref.get().remove(null); + } + + @Test + public void subscriberSwap() { + final ConnectableFlowable<Integer> cf = Flowable.range(1, 5).publish(); + + cf.connect(); + + TestSubscriber<Integer> ts1 = new TestSubscriber<Integer>() { + @Override + public void onNext(Integer t) { + super.onNext(t); + cancel(); + onComplete(); + } + }; + + cf.subscribe(ts1); + + ts1.assertResult(1); + + TestSubscriber<Integer> ts2 = new TestSubscriber<>(0); + cf.subscribe(ts2); + + ts2 + .assertEmpty() + .requestMore(4) + .assertResult(2, 3, 4, 5); + } + + @Test + public void subscriberLiveSwap() { + final ConnectableFlowable<Integer> cf = Flowable.range(1, 5).publish(); + + final TestSubscriber<Integer> ts2 = new TestSubscriber<>(0); + + TestSubscriber<Integer> ts1 = new TestSubscriber<Integer>() { + @Override + public void onNext(Integer t) { + super.onNext(t); + cancel(); + onComplete(); + cf.subscribe(ts2); + } + }; + + cf.subscribe(ts1); + + cf.connect(); + + ts1.assertResult(1); + + ts2 + .assertEmpty() + .requestMore(4) + .assertResult(2, 3, 4, 5); + } + + @Test + public void selectorSubscriberSwap() { + final AtomicReference<Flowable<Integer>> ref = new AtomicReference<>(); + + Flowable.range(1, 5).publish(new Function<Flowable<Integer>, Publisher<Integer>>() { + @Override + public Publisher<Integer> apply(Flowable<Integer> f) throws Exception { + ref.set(f); + return Flowable.never(); + } + }).test(); + + ref.get().take(2).test().assertResult(1, 2); + + ref.get() + .test(0) + .assertEmpty() + .requestMore(2) + .assertValuesOnly(3, 4) + .requestMore(1) + .assertResult(3, 4, 5); + } + + @Test + public void leavingSubscriberOverrequests() { + final AtomicReference<Flowable<Integer>> ref = new AtomicReference<>(); + + PublishProcessor<Integer> pp = PublishProcessor.create(); + + pp.publish(new Function<Flowable<Integer>, Publisher<Integer>>() { + @Override + public Publisher<Integer> apply(Flowable<Integer> f) throws Exception { + ref.set(f); + return Flowable.never(); + } + }).test(); + + TestSubscriber<Integer> ts1 = ref.get().take(2).test(); + + pp.onNext(1); + pp.onNext(2); + + ts1.assertResult(1, 2); + + pp.onNext(3); + pp.onNext(4); + + TestSubscriber<Integer> ts2 = ref.get().test(0L); + + ts2.assertEmpty(); + + ts2.requestMore(2); + + ts2.assertValuesOnly(3, 4); + } + + // call a transformer only if the input is non-empty + @Test + public void composeIfNotEmpty() { + final FlowableTransformer<Integer, Integer> transformer = new FlowableTransformer<Integer, Integer>() { + @Override + public Publisher<Integer> apply(Flowable<Integer> g) { + return g.map(new Function<Integer, Integer>() { + @Override + public Integer apply(Integer v) throws Exception { + return v + 1; + } + }); + } + }; + + final AtomicInteger calls = new AtomicInteger(); + Flowable.range(1, 5) + .publish(new Function<Flowable<Integer>, Publisher<Integer>>() { + @Override + public Publisher<Integer> apply(final Flowable<Integer> shared) + throws Exception { + return shared.take(1).concatMap(new Function<Integer, Publisher<? extends Integer>>() { + @Override + public Publisher<? extends Integer> apply(Integer first) + throws Exception { + calls.incrementAndGet(); + return transformer.apply(Flowable.just(first).concatWith(shared)); + } + }); + } + }) + .test() + .assertResult(2, 3, 4, 5, 6); + + assertEquals(1, calls.get()); + } + + // call a transformer only if the input is non-empty + @Test + public void composeIfNotEmptyNotFused() { + final FlowableTransformer<Integer, Integer> transformer = new FlowableTransformer<Integer, Integer>() { + @Override + public Publisher<Integer> apply(Flowable<Integer> g) { + return g.map(new Function<Integer, Integer>() { + @Override + public Integer apply(Integer v) throws Exception { + return v + 1; + } + }); + } + }; + + final AtomicInteger calls = new AtomicInteger(); + Flowable.range(1, 5).hide() + .publish(new Function<Flowable<Integer>, Publisher<Integer>>() { + @Override + public Publisher<Integer> apply(final Flowable<Integer> shared) + throws Exception { + return shared.take(1).concatMap(new Function<Integer, Publisher<? extends Integer>>() { + @Override + public Publisher<? extends Integer> apply(Integer first) + throws Exception { + calls.incrementAndGet(); + return transformer.apply(Flowable.just(first).concatWith(shared)); + } + }); + } + }) + .test() + .assertResult(2, 3, 4, 5, 6); + + assertEquals(1, calls.get()); + } + + // call a transformer only if the input is non-empty + @Test + public void composeIfNotEmptyIsEmpty() { + final FlowableTransformer<Integer, Integer> transformer = new FlowableTransformer<Integer, Integer>() { + @Override + public Publisher<Integer> apply(Flowable<Integer> g) { + return g.map(new Function<Integer, Integer>() { + @Override + public Integer apply(Integer v) throws Exception { + return v + 1; + } + }); + } + }; + + final AtomicInteger calls = new AtomicInteger(); + Flowable.<Integer>empty().hide() + .publish(new Function<Flowable<Integer>, Publisher<Integer>>() { + @Override + public Publisher<Integer> apply(final Flowable<Integer> shared) + throws Exception { + return shared.take(1).concatMap(new Function<Integer, Publisher<? extends Integer>>() { + @Override + public Publisher<? extends Integer> apply(Integer first) + throws Exception { + calls.incrementAndGet(); + return transformer.apply(Flowable.just(first).concatWith(shared)); + } + }); + } + }) + .test() + .assertResult(); + + assertEquals(0, calls.get()); + } + + @Test + public void publishFunctionCancelOuterAfterOneInner() { + final AtomicReference<Flowable<Integer>> ref = new AtomicReference<>(); + + PublishProcessor<Integer> pp = PublishProcessor.create(); + + final TestSubscriber<Integer> ts = pp.publish(new Function<Flowable<Integer>, Publisher<Integer>>() { + @Override + public Publisher<Integer> apply(Flowable<Integer> f) throws Exception { + ref.set(f); + return Flowable.never(); + } + }).test(); + + ref.get().subscribe(new TestSubscriber<Integer>() { + @Override + public void onNext(Integer t) { + super.onNext(t); + onComplete(); + ts.cancel(); + } + }); + + pp.onNext(1); + } + + @Test + public void publishFunctionCancelOuterAfterOneInnerBackpressured() { + final AtomicReference<Flowable<Integer>> ref = new AtomicReference<>(); + + PublishProcessor<Integer> pp = PublishProcessor.create(); + + final TestSubscriber<Integer> ts = pp.publish(new Function<Flowable<Integer>, Publisher<Integer>>() { + @Override + public Publisher<Integer> apply(Flowable<Integer> f) throws Exception { + ref.set(f); + return Flowable.never(); + } + }).test(); + + ref.get().subscribe(new TestSubscriber<Integer>(1L) { + @Override + public void onNext(Integer t) { + super.onNext(t); + onComplete(); + ts.cancel(); + } + }); + + pp.onNext(1); + } + + @Test + public void publishCancelOneAsync() { + for (int i = 0; i < TestHelper.RACE_LONG_LOOPS; i++) { + + final PublishProcessor<Integer> pp = PublishProcessor.create(); + + final AtomicReference<Flowable<Integer>> ref = new AtomicReference<>(); + + pp.publish(new Function<Flowable<Integer>, Publisher<Integer>>() { + @Override + public Publisher<Integer> apply(Flowable<Integer> f) throws Exception { + ref.set(f); + return Flowable.never(); + } + }).test(); + + final TestSubscriber<Integer> ts1 = ref.get().test(); + TestSubscriber<Integer> ts2 = ref.get().test(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + pp.onNext(1); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + ts1.cancel(); + } + }; + + TestHelper.race(r1, r2); + + ts2.assertValuesOnly(1); + } + } + + @Test + public void publishCancelOneAsync2() { + final PublishProcessor<Integer> pp = PublishProcessor.create(); + + ConnectableFlowable<Integer> cf = pp.publish(); + + final TestSubscriber<Integer> ts1 = new TestSubscriber<>(); + + final AtomicReference<InnerSubscription<Integer>> ref = new AtomicReference<>(); + + cf.subscribe(new FlowableSubscriber<Integer>() { + @SuppressWarnings("unchecked") + @Override + public void onSubscribe(Subscription s) { + ts1.onSubscribe(new BooleanSubscription()); + // pretend to be cancelled without removing it from the subscriber list + ref.set((InnerSubscription<Integer>)s); + } + + @Override + public void onNext(Integer t) { + ts1.onNext(t); + } + + @Override + public void onError(Throwable t) { + ts1.onError(t); + } + + @Override + public void onComplete() { + ts1.onComplete(); + } + }); + TestSubscriber<Integer> ts2 = cf.test(); + + cf.connect(); + + ref.get().set(Long.MIN_VALUE); + + pp.onNext(1); + + ts1.assertEmpty(); + ts2.assertValuesOnly(1); + } + + @Test + public void boundaryFusion() { + Flowable.range(1, 10000) + .observeOn(Schedulers.single()) + .map(new Function<Integer, String>() { + @Override + public String apply(Integer t) throws Exception { + String name = Thread.currentThread().getName(); + if (name.contains("RxSingleScheduler")) { + return "RxSingleScheduler"; + } + return name; + } + }) + .share() + .observeOn(Schedulers.computation()) + .distinct() + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertResult("RxSingleScheduler"); + } + + @Test + public void badRequest() { + TestHelper.assertBadRequestReported(Flowable.range(1, 5).publish()); + } + + @Test + public void splitCombineSubscriberChangeAfterOnNext() { + Flowable<Integer> source = Flowable.range(0, 20) + .doOnSubscribe(new Consumer<Subscription>() { + @Override + public void accept(Subscription v) throws Exception { + System.out.println("Subscribed"); + } + }) + .publish(10) + .refCount() + ; + + Flowable<Integer> evenNumbers = source.filter(new Predicate<Integer>() { + @Override + public boolean test(Integer v) throws Exception { + return v % 2 == 0; + } + }); + + Flowable<Integer> oddNumbers = source.filter(new Predicate<Integer>() { + @Override + public boolean test(Integer v) throws Exception { + return v % 2 != 0; + } + }); + + final Single<Integer> getNextOdd = oddNumbers.first(0); + + TestSubscriber<List<Integer>> ts = evenNumbers.concatMap(new Function<Integer, Publisher<List<Integer>>>() { + @Override + public Publisher<List<Integer>> apply(Integer v) throws Exception { + return Single.zip( + Single.just(v), getNextOdd, + new BiFunction<Integer, Integer, List<Integer>>() { + @Override + public List<Integer> apply(Integer a, Integer b) throws Exception { + return Arrays.asList( a, b ); + } + } + ) + .toFlowable(); + } + }) + .takeWhile(new Predicate<List<Integer>>() { + @Override + public boolean test(List<Integer> v) throws Exception { + return v.get(0) < 20; + } + }) + .test(); + + ts + .assertResult( + Arrays.asList(0, 1), + Arrays.asList(2, 3), + Arrays.asList(4, 5), + Arrays.asList(6, 7), + Arrays.asList(8, 9), + Arrays.asList(10, 11), + Arrays.asList(12, 13), + Arrays.asList(14, 15), + Arrays.asList(16, 17), + Arrays.asList(18, 19) + ); + } + + @Test + public void splitCombineSubscriberChangeAfterOnNextFused() { + Flowable<Integer> source = Flowable.range(0, 20) + .publish(10) + .refCount() + ; + + Flowable<Integer> evenNumbers = source.filter(new Predicate<Integer>() { + @Override + public boolean test(Integer v) throws Exception { + return v % 2 == 0; + } + }); + + Flowable<Integer> oddNumbers = source.filter(new Predicate<Integer>() { + @Override + public boolean test(Integer v) throws Exception { + return v % 2 != 0; + } + }); + + final Single<Integer> getNextOdd = oddNumbers.first(0); + + TestSubscriber<List<Integer>> ts = evenNumbers.concatMap(new Function<Integer, Publisher<List<Integer>>>() { + @Override + public Publisher<List<Integer>> apply(Integer v) throws Exception { + return Single.zip( + Single.just(v), getNextOdd, + new BiFunction<Integer, Integer, List<Integer>>() { + @Override + public List<Integer> apply(Integer a, Integer b) throws Exception { + return Arrays.asList( a, b ); + } + } + ) + .toFlowable(); + } + }) + .takeWhile(new Predicate<List<Integer>>() { + @Override + public boolean test(List<Integer> v) throws Exception { + return v.get(0) < 20; + } + }) + .test(); + + ts + .assertResult( + Arrays.asList(0, 1), + Arrays.asList(2, 3), + Arrays.asList(4, 5), + Arrays.asList(6, 7), + Arrays.asList(8, 9), + Arrays.asList(10, 11), + Arrays.asList(12, 13), + Arrays.asList(14, 15), + Arrays.asList(16, 17), + Arrays.asList(18, 19) + ); + } + + @Test + public void altConnectCrash() { + try { + new FlowablePublish<>(Flowable.<Integer>empty(), 128) + .connect(new Consumer<Disposable>() { + @Override + public void accept(Disposable t) throws Exception { + throw new TestException(); + } + }); + fail("Should have thrown"); + } catch (TestException expected) { + // expected + } + } + + @Test + public void altConnectRace() { + for (int i = 0; i < TestHelper.RACE_LONG_LOOPS; i++) { + final ConnectableFlowable<Integer> cf = + new FlowablePublish<>(Flowable.<Integer>never(), 128); + + Runnable r = new Runnable() { + @Override + public void run() { + cf.connect(); + } + }; + + TestHelper.race(r, r); + } + } + + @Test + public void fusedPollCrash() { + Flowable.range(1, 5) + .map(new Function<Integer, Object>() { + @Override + public Object apply(Integer v) throws Exception { + throw new TestException(); + } + }) + .compose(TestHelper.flowableStripBoundary()) + .publish() + .refCount() + .test() + .assertFailure(TestException.class); + } + + @Test + public void syncFusedNoRequest() { + Flowable.range(1, 5) + .publish(1) + .refCount() + .test() + .assertResult(1, 2, 3, 4, 5); + } + + @Test + public void normalBackpressuredPolls() { + Flowable.range(1, 5) + .hide() + .publish(1) + .refCount() + .test() + .assertResult(1, 2, 3, 4, 5); + } + + @Test + public void emptyHidden() { + Flowable.empty() + .hide() + .publish(1) + .refCount() + .test() + .assertResult(); + } + + @Test + public void emptyFused() { + Flowable.empty() + .publish(1) + .refCount() + .test() + .assertResult(); + } + + @Test + public void overflowQueueRefCount() { + new Flowable<Integer>() { + @Override + protected void subscribeActual(Subscriber<? super Integer> s) { + s.onSubscribe(new BooleanSubscription()); + s.onNext(1); + s.onNext(2); + } + } + .publish(1) + .refCount() + .test(0) + .requestMore(1) + .assertFailure(QueueOverflowException.class, 1); + } + + @Test + public void doubleErrorRefCount() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + new Flowable<Integer>() { + @Override + protected void subscribeActual(Subscriber<? super Integer> s) { + s.onSubscribe(new BooleanSubscription()); + s.onError(new TestException("one")); + s.onError(new TestException("two")); + } + } + .publish(1) + .refCount() + .to(TestHelper.<Integer>testSubscriber(0L)) + .assertFailureAndMessage(TestException.class, "one"); + + TestHelper.assertUndeliverable(errors, 0, TestException.class, "two"); + assertEquals(1, errors.size()); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void onCompleteAvailableUntilReset() { + ConnectableFlowable<Integer> cf = Flowable.just(1).publish(); + + TestSubscriber<Integer> ts = cf.test(); + ts.assertEmpty(); + + cf.connect(); + + ts.assertResult(1); + + cf.test().assertResult(); + + cf.reset(); + + ts = cf.test(); + ts.assertEmpty(); + + cf.connect(); + + ts.assertResult(1); + } + + @Test + public void onErrorAvailableUntilReset() { + ConnectableFlowable<Integer> cf = Flowable.just(1) + .concatWith(Flowable.<Integer>error(new TestException())) + .publish(); + + TestSubscriber<Integer> ts = cf.test(); + ts.assertEmpty(); + + cf.connect(); + + ts.assertFailure(TestException.class, 1); + + cf.test().assertFailure(TestException.class); + + cf.reset(); + + ts = cf.test(); + ts.assertEmpty(); + + cf.connect(); + + ts.assertFailure(TestException.class, 1); + } + + @Test + public void disposeResets() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + + ConnectableFlowable<Integer> cf = pp.publish(); + + assertFalse(pp.hasSubscribers()); + + Disposable d = cf.connect(); + + assertTrue(pp.hasSubscribers()); + + d.dispose(); + + assertFalse(pp.hasSubscribers()); + + TestSubscriber<Integer> ts = cf.test(); + + cf.connect(); + + assertTrue(pp.hasSubscribers()); + + pp.onNext(1); + + ts.assertValuesOnly(1); + } + + @Test(expected = TestException.class) + public void connectDisposeCrash() { + ConnectableFlowable<Object> cf = Flowable.never().publish(); + + cf.connect(); + + cf.connect(d -> { throw new TestException(); }); + } + + @Test + public void resetWhileNotConnectedIsNoOp() { + ConnectableFlowable<Object> cf = Flowable.never().publish(); + + cf.reset(); + } + + @Test + public void resetWhileActiveIsNoOp() { + ConnectableFlowable<Object> cf = Flowable.never().publish(); + + cf.connect(); + + cf.reset(); + } + + @Test + public void crossCancelOnComplete() { + TestSubscriber<Integer> ts1 = new TestSubscriber<>(); + TestSubscriber<Integer> ts2 = new TestSubscriber<Integer>() { + @Override + public void onComplete() { + super.onComplete(); + ts1.cancel(); + } + }; + + PublishProcessor<Integer> pp = PublishProcessor.create(); + + ConnectableFlowable<Integer> cf = pp.publish(); + + cf.subscribe(ts2); + cf.subscribe(ts1); + + cf.connect(); + + pp.onComplete(); + + ts2.assertResult(); + + ts1.assertEmpty(); + } + + @Test + public void crossCancelOnError() { + TestSubscriber<Integer> ts1 = new TestSubscriber<>(); + TestSubscriber<Integer> ts2 = new TestSubscriber<Integer>() { + @Override + public void onError(Throwable t) { + super.onError(t); + ts1.cancel(); + } + }; + + PublishProcessor<Integer> pp = PublishProcessor.create(); + + ConnectableFlowable<Integer> cf = pp.publish(); + + cf.subscribe(ts2); + cf.subscribe(ts1); + + cf.connect(); + + pp.onError(new TestException()); + + ts2.assertFailure(TestException.class); + + ts1.assertEmpty(); + } + + @Test + public void disposeNoNeedForReset() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + + ConnectableFlowable<Integer> cf = pp.publish(); + + TestSubscriber<Integer> ts = cf.test(); + + Disposable d = cf.connect(); + + pp.onNext(1); + + d.dispose(); + + ts = cf.test(); + + ts.assertEmpty(); + + cf.connect(); + + ts.assertEmpty(); + + pp.onNext(2); + + ts.assertValuesOnly(2); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableRangeLongTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableRangeLongTest.java new file mode 100644 index 0000000000..bc24ab787c --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableRangeLongTest.java @@ -0,0 +1,582 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import java.util.*; +import java.util.concurrent.atomic.*; + +import org.junit.Test; +import org.reactivestreams.Subscriber; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.operators.QueueFuseable; +import io.reactivex.rxjava3.subscribers.*; +import io.reactivex.rxjava3.testsupport.*; + +public class FlowableRangeLongTest extends RxJavaTest { + + @Test + public void rangeStartAt2Count3() { + Subscriber<Long> subscriber = TestHelper.mockSubscriber(); + + Flowable.rangeLong(2, 3).subscribe(subscriber); + + verify(subscriber, times(1)).onNext(2L); + verify(subscriber, times(1)).onNext(3L); + verify(subscriber, times(1)).onNext(4L); + verify(subscriber, never()).onNext(5L); + verify(subscriber, never()).onError(any(Throwable.class)); + verify(subscriber, times(1)).onComplete(); + } + + @Test + public void rangeUnsubscribe() { + Subscriber<Long> subscriber = TestHelper.mockSubscriber(); + + final AtomicInteger count = new AtomicInteger(); + + Flowable.rangeLong(1, 1000).doOnNext(new Consumer<Long>() { + @Override + public void accept(Long t1) { + count.incrementAndGet(); + } + }) + .take(3).subscribe(subscriber); + + verify(subscriber, times(1)).onNext(1L); + verify(subscriber, times(1)).onNext(2L); + verify(subscriber, times(1)).onNext(3L); + verify(subscriber, never()).onNext(4L); + verify(subscriber, never()).onError(any(Throwable.class)); + verify(subscriber, times(1)).onComplete(); + assertEquals(3, count.get()); + } + + @Test + public void rangeWithZero() { + Flowable.rangeLong(1, 0); + } + + @Test + public void rangeWithOverflow2() { + Flowable.rangeLong(Long.MAX_VALUE, 0); + } + + @Test + public void rangeWithOverflow3() { + Flowable.rangeLong(1, Long.MAX_VALUE); + } + + @Test(expected = IllegalArgumentException.class) + public void rangeWithOverflow4() { + Flowable.rangeLong(2, Long.MAX_VALUE); + } + + @Test + public void rangeWithOverflow5() { + assertFalse(Flowable.rangeLong(Long.MIN_VALUE, 0).blockingIterable().iterator().hasNext()); + } + + @Test + public void backpressureViaRequest() { + Flowable<Long> f = Flowable.rangeLong(1, Flowable.bufferSize()); + + TestSubscriberEx<Long> ts = new TestSubscriberEx<>(0L); + + ts.assertNoValues(); + ts.request(1); + + f.subscribe(ts); + + ts.assertValue(1L); + + ts.request(2); + ts.assertValues(1L, 2L, 3L); + + ts.request(3); + ts.assertValues(1L, 2L, 3L, 4L, 5L, 6L); + + ts.request(Flowable.bufferSize()); + ts.assertTerminated(); + } + + @Test + public void noBackpressure() { + ArrayList<Long> list = new ArrayList<>(Flowable.bufferSize() * 2); + for (long i = 1; i <= Flowable.bufferSize() * 2 + 1; i++) { + list.add(i); + } + + Flowable<Long> f = Flowable.rangeLong(1, list.size()); + + TestSubscriberEx<Long> ts = new TestSubscriberEx<>(0L); + + ts.assertNoValues(); + ts.request(Long.MAX_VALUE); // infinite + + f.subscribe(ts); + + ts.assertValueSequence(list); + ts.assertTerminated(); + } + void withBackpressureOneByOne(long start) { + Flowable<Long> source = Flowable.rangeLong(start, 100); + + TestSubscriberEx<Long> ts = new TestSubscriberEx<>(0L); + ts.request(1); + source.subscribe(ts); + + List<Long> list = new ArrayList<>(100); + for (long i = 0; i < 100; i++) { + list.add(i + start); + ts.request(1); + } + ts.assertValueSequence(list); + ts.assertTerminated(); + } + void withBackpressureAllAtOnce(long start) { + Flowable<Long> source = Flowable.rangeLong(start, 100); + + TestSubscriberEx<Long> ts = new TestSubscriberEx<>(0L); + ts.request(100); + source.subscribe(ts); + + List<Long> list = new ArrayList<>(100); + for (long i = 0; i < 100; i++) { + list.add(i + start); + } + ts.assertValueSequence(list); + ts.assertTerminated(); + } + + @Test + public void withBackpressure1() { + for (long i = 0; i < 100; i++) { + withBackpressureOneByOne(i); + } + } + + @Test + public void withBackpressureAllAtOnce() { + for (long i = 0; i < 100; i++) { + withBackpressureAllAtOnce(i); + } + } + + @Test + public void withBackpressureRequestWayMore() { + Flowable<Long> source = Flowable.rangeLong(50, 100); + + TestSubscriberEx<Long> ts = new TestSubscriberEx<>(0L); + ts.request(150); + source.subscribe(ts); + + List<Long> list = new ArrayList<>(100); + for (long i = 0; i < 100; i++) { + list.add(i + 50); + } + + ts.request(50); // and then some + + ts.assertValueSequence(list); + ts.assertTerminated(); + } + + @Test + public void requestOverflow() { + final AtomicInteger count = new AtomicInteger(); + int n = 10; + Flowable.rangeLong(1, n).subscribe(new DefaultSubscriber<Long>() { + + @Override + public void onStart() { + request(2); + } + + @Override + public void onComplete() { + //do nothing + } + + @Override + public void onError(Throwable e) { + throw new RuntimeException(e); + } + + @Override + public void onNext(Long t) { + count.incrementAndGet(); + request(Long.MAX_VALUE - 1); + }}); + assertEquals(n, count.get()); + } + + @Test + public void emptyRangeSendsOnCompleteEagerlyWithRequestZero() { + final AtomicBoolean completed = new AtomicBoolean(false); + Flowable.rangeLong(1, 0).subscribe(new DefaultSubscriber<Long>() { + + @Override + public void onStart() { +// request(0); + } + + @Override + public void onComplete() { + completed.set(true); + } + + @Override + public void onError(Throwable e) { + + } + + @Override + public void onNext(Long t) { + + }}); + assertTrue(completed.get()); + } + + @Test + public void nearMaxValueWithoutBackpressure() { + TestSubscriber<Long> ts = new TestSubscriber<>(); + Flowable.rangeLong(Long.MAX_VALUE - 1L, 2L).subscribe(ts); + + ts.assertComplete(); + ts.assertNoErrors(); + ts.assertValues(Long.MAX_VALUE - 1L, Long.MAX_VALUE); + } + + @Test + public void nearMaxValueWithBackpressure() { + TestSubscriber<Long> ts = new TestSubscriber<>(3L); + Flowable.rangeLong(Long.MAX_VALUE - 1L, 2L).subscribe(ts); + + ts.assertComplete(); + ts.assertNoErrors(); + ts.assertValues(Long.MAX_VALUE - 1L, Long.MAX_VALUE); + } + + @Test + public void negativeCount() { + try { + Flowable.rangeLong(1L, -1L); + fail("Should have thrown IllegalArgumentException"); + } catch (IllegalArgumentException ex) { + assertEquals("count >= 0 required but it was -1", ex.getMessage()); + } + } + + @Test + public void countOne() { + Flowable.rangeLong(5495454L, 1L) + .test() + .assertResult(5495454L); + } + + @Test + public void fused() { + TestSubscriberEx<Long> ts = new TestSubscriberEx<Long>().setInitialFusionMode(QueueFuseable.ANY); + + Flowable.rangeLong(1, 2).subscribe(ts); + + ts.assertFusionMode(QueueFuseable.SYNC) + .assertResult(1L, 2L); + } + + @Test + public void fusedReject() { + TestSubscriberEx<Long> ts = new TestSubscriberEx<Long>().setInitialFusionMode(QueueFuseable.ASYNC); + + Flowable.rangeLong(1, 2).subscribe(ts); + + ts.assertFusionMode(QueueFuseable.NONE) + .assertResult(1L, 2L); + } + + @Test + public void disposed() { + TestHelper.checkDisposed(Flowable.rangeLong(1, 2)); + } + + @Test + public void fusedClearIsEmpty() { + TestHelper.checkFusedIsEmptyClear(Flowable.rangeLong(1, 2)); + } + + @Test + public void noOverflow() { + Flowable.rangeLong(Long.MAX_VALUE - 1, 2); + Flowable.rangeLong(Long.MIN_VALUE, 2); + Flowable.rangeLong(Long.MIN_VALUE, Long.MAX_VALUE); + } + + @Test + public void conditionalNormal() { + Flowable.rangeLong(1L, 5L) + .filter(Functions.alwaysTrue()) + .test() + .assertResult(1L, 2L, 3L, 4L, 5L); + } + + @Test + public void badRequest() { + TestHelper.assertBadRequestReported(Flowable.rangeLong(1L, 5L)); + + TestHelper.assertBadRequestReported(Flowable.rangeLong(1L, 5L).filter(Functions.alwaysTrue())); + } + + @Test + public void conditionalNormalSlowpath() { + Flowable.rangeLong(1L, 5L) + .filter(Functions.alwaysTrue()) + .test(5) + .assertResult(1L, 2L, 3L, 4L, 5L); + } + + @Test + public void conditionalSlowPathTakeExact() { + Flowable.rangeLong(1L, 5L) + .filter(Functions.alwaysTrue()) + .take(5) + .test() + .assertResult(1L, 2L, 3L, 4L, 5L); + } + + @Test + public void slowPathTakeExact() { + Flowable.rangeLong(1L, 5L) + .filter(Functions.alwaysTrue()) + .take(5) + .test() + .assertResult(1L, 2L, 3L, 4L, 5L); + } + + @Test + public void conditionalSlowPathRebatch() { + Flowable.rangeLong(1L, 5L) + .filter(Functions.alwaysTrue()) + .rebatchRequests(1) + .test() + .assertResult(1L, 2L, 3L, 4L, 5L); + } + + @Test + public void slowPathRebatch() { + Flowable.rangeLong(1L, 5L) + .rebatchRequests(1) + .test() + .assertResult(1L, 2L, 3L, 4L, 5L); + } + + @Test + public void slowPathCancel() { + TestSubscriber<Long> ts = new TestSubscriber<Long>(2L) { + @Override + public void onNext(Long t) { + super.onNext(t); + cancel(); + onComplete(); + } + }; + + Flowable.rangeLong(1L, 5L) + .subscribe(ts); + + ts.assertResult(1L); + } + + @Test + public void fastPathCancel() { + TestSubscriber<Long> ts = new TestSubscriber<Long>() { + @Override + public void onNext(Long t) { + super.onNext(t); + cancel(); + onComplete(); + } + }; + + Flowable.rangeLong(1L, 5L) + .subscribe(ts); + + ts.assertResult(1L); + } + + @Test + public void conditionalSlowPathCancel() { + TestSubscriber<Long> ts = new TestSubscriber<Long>(1L) { + @Override + public void onNext(Long t) { + super.onNext(t); + cancel(); + onComplete(); + } + }; + + Flowable.rangeLong(1L, 5L) + .filter(Functions.alwaysTrue()) + .subscribe(ts); + + ts.assertResult(1L); + } + + @Test + public void conditionalFastPathCancel() { + TestSubscriber<Long> ts = new TestSubscriber<Long>() { + @Override + public void onNext(Long t) { + super.onNext(t); + cancel(); + onComplete(); + } + }; + + Flowable.rangeLong(1L, 5L) + .filter(Functions.alwaysTrue()) + .subscribe(ts); + + ts.assertResult(1L); + } + + @Test + public void conditionalRequestOneByOne() { + TestSubscriber<Long> ts = new TestSubscriber<Long>(1L) { + @Override + public void onNext(Long t) { + super.onNext(t); + request(1); + } + }; + + Flowable.rangeLong(1L, 5L) + .filter(new Predicate<Long>() { + @Override + public boolean test(Long v) throws Exception { + return v % 2 == 0; + } + }) + .subscribe(ts); + + ts.assertResult(2L, 4L); + } + + @Test + public void conditionalRequestOneByOne2() { + TestSubscriber<Long> ts = new TestSubscriber<Long>(1L) { + @Override + public void onNext(Long t) { + super.onNext(t); + request(1); + } + }; + + Flowable.rangeLong(1L, 5L) + .filter(Functions.alwaysTrue()) + .subscribe(ts); + + ts.assertResult(1L, 2L, 3L, 4L, 5L); + } + + @Test + public void fastPathCancelExact() { + TestSubscriber<Long> ts = new TestSubscriber<Long>() { + @Override + public void onNext(Long t) { + super.onNext(t); + if (t == 5L) { + cancel(); + onComplete(); + } + } + }; + + Flowable.rangeLong(1L, 5L) + .subscribe(ts); + + ts.assertResult(1L, 2L, 3L, 4L, 5L); + } + + @Test + public void conditionalFastPathCancelExact() { + TestSubscriber<Long> ts = new TestSubscriber<Long>() { + @Override + public void onNext(Long t) { + super.onNext(t); + if (t == 5L) { + cancel(); + onComplete(); + } + } + }; + + Flowable.rangeLong(1L, 5L) + .filter(new Predicate<Long>() { + @Override + public boolean test(Long v) throws Exception { + return v % 2 == 0; + } + }) + .subscribe(ts); + + ts.assertResult(2L, 4L); + } + + @Test + public void slowPathCancelBeforeComplete() { + Flowable.rangeLong(1, 2) + .take(2) + .test() + .assertResult(1L, 2L); + } + + @Test + public void conditionalFastPathCancelBeforeComplete() { + TestSubscriber<Long> ts = new TestSubscriber<>(); + + Flowable.rangeLong(1, 2) + .compose(TestHelper.conditional()) + .doOnNext(v -> { + if (v == 2L) { + ts.cancel(); + } + }) + .subscribe(ts); + + ts.assertValuesOnly(1L, 2L); + } + + @Test + public void conditionalSlowPathTake() { + TestSubscriber<Long> ts = new TestSubscriber<>(4); + + Flowable.rangeLong(1, 3) + .compose(TestHelper.conditional()) + .doOnNext(v -> { + if (v == 2L) { + ts.cancel(); + } + }) + .subscribe(ts); + + ts.assertValuesOnly(1L, 2L); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableRangeTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableRangeTest.java new file mode 100644 index 0000000000..53264bddc0 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableRangeTest.java @@ -0,0 +1,617 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import java.util.*; +import java.util.concurrent.atomic.*; + +import org.junit.Test; +import org.reactivestreams.Subscriber; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.operators.QueueFuseable; +import io.reactivex.rxjava3.subscribers.*; +import io.reactivex.rxjava3.testsupport.*; + +public class FlowableRangeTest extends RxJavaTest { + + @Test + public void rangeStartAt2Count3() { + Subscriber<Integer> subscriber = TestHelper.mockSubscriber(); + + Flowable.range(2, 3).subscribe(subscriber); + + verify(subscriber, times(1)).onNext(2); + verify(subscriber, times(1)).onNext(3); + verify(subscriber, times(1)).onNext(4); + verify(subscriber, never()).onNext(5); + verify(subscriber, never()).onError(any(Throwable.class)); + verify(subscriber, times(1)).onComplete(); + } + + @Test + public void rangeUnsubscribe() { + Subscriber<Integer> subscriber = TestHelper.mockSubscriber(); + + final AtomicInteger count = new AtomicInteger(); + + Flowable.range(1, 1000).doOnNext(new Consumer<Integer>() { + @Override + public void accept(Integer t1) { + count.incrementAndGet(); + } + }) + .take(3).subscribe(subscriber); + + verify(subscriber, times(1)).onNext(1); + verify(subscriber, times(1)).onNext(2); + verify(subscriber, times(1)).onNext(3); + verify(subscriber, never()).onNext(4); + verify(subscriber, never()).onError(any(Throwable.class)); + verify(subscriber, times(1)).onComplete(); + assertEquals(3, count.get()); + } + + @Test + public void rangeWithZero() { + Flowable.range(1, 0); + } + + @Test + public void rangeWithOverflow2() { + Flowable.range(Integer.MAX_VALUE, 0); + } + + @Test + public void rangeWithOverflow3() { + Flowable.range(1, Integer.MAX_VALUE); + } + + @Test(expected = IllegalArgumentException.class) + public void rangeWithOverflow4() { + Flowable.range(2, Integer.MAX_VALUE); + } + + @Test + public void rangeWithOverflow5() { + assertFalse(Flowable.range(Integer.MIN_VALUE, 0).blockingIterable().iterator().hasNext()); + } + + @Test + public void backpressureViaRequest() { + Flowable<Integer> f = Flowable.range(1, Flowable.bufferSize()); + + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(0L); + + ts.assertNoValues(); + ts.request(1); + + f.subscribe(ts); + + ts.assertValue(1); + + ts.request(2); + ts.assertValues(1, 2, 3); + + ts.request(3); + ts.assertValues(1, 2, 3, 4, 5, 6); + + ts.request(Flowable.bufferSize()); + ts.assertTerminated(); + } + + @Test + public void noBackpressure() { + ArrayList<Integer> list = new ArrayList<>(Flowable.bufferSize() * 2); + for (int i = 1; i <= Flowable.bufferSize() * 2 + 1; i++) { + list.add(i); + } + + Flowable<Integer> f = Flowable.range(1, list.size()); + + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(0L); + + ts.assertNoValues(); + ts.request(Long.MAX_VALUE); // infinite + + f.subscribe(ts); + + ts.assertValueSequence(list); + ts.assertTerminated(); + } + void withBackpressureOneByOne(int start) { + Flowable<Integer> source = Flowable.range(start, 100); + + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(0L); + ts.request(1); + source.subscribe(ts); + + List<Integer> list = new ArrayList<>(100); + for (int i = 0; i < 100; i++) { + list.add(i + start); + ts.request(1); + } + ts.assertValueSequence(list); + ts.assertTerminated(); + } + void withBackpressureAllAtOnce(int start) { + Flowable<Integer> source = Flowable.range(start, 100); + + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(0L); + ts.request(100); + source.subscribe(ts); + + List<Integer> list = new ArrayList<>(100); + for (int i = 0; i < 100; i++) { + list.add(i + start); + } + ts.assertValueSequence(list); + ts.assertTerminated(); + } + + @Test + public void withBackpressure1() { + for (int i = 0; i < 100; i++) { + withBackpressureOneByOne(i); + } + } + + @Test + public void withBackpressureAllAtOnce() { + for (int i = 0; i < 100; i++) { + withBackpressureAllAtOnce(i); + } + } + + @Test + public void withBackpressureRequestWayMore() { + Flowable<Integer> source = Flowable.range(50, 100); + + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(0L); + ts.request(150); + source.subscribe(ts); + + List<Integer> list = new ArrayList<>(100); + for (int i = 0; i < 100; i++) { + list.add(i + 50); + } + + ts.request(50); // and then some + + ts.assertValueSequence(list); + ts.assertTerminated(); + } + + @Test + public void requestOverflow() { + final AtomicInteger count = new AtomicInteger(); + int n = 10; + Flowable.range(1, n).subscribe(new DefaultSubscriber<Integer>() { + + @Override + public void onStart() { + request(2); + } + + @Override + public void onComplete() { + //do nothing + } + + @Override + public void onError(Throwable e) { + throw new RuntimeException(e); + } + + @Override + public void onNext(Integer t) { + count.incrementAndGet(); + request(Long.MAX_VALUE - 1); + }}); + assertEquals(n, count.get()); + } + + @Test + public void emptyRangeSendsOnCompleteEagerlyWithRequestZero() { + final AtomicBoolean completed = new AtomicBoolean(false); + Flowable.range(1, 0).subscribe(new DefaultSubscriber<Integer>() { + + @Override + public void onStart() { +// request(0); + } + + @Override + public void onComplete() { + completed.set(true); + } + + @Override + public void onError(Throwable e) { + + } + + @Override + public void onNext(Integer t) { + + }}); + assertTrue(completed.get()); + } + + @Test + public void nearMaxValueWithoutBackpressure() { + TestSubscriber<Integer> ts = new TestSubscriber<>(); + Flowable.range(Integer.MAX_VALUE - 1, 2).subscribe(ts); + + ts.assertComplete(); + ts.assertNoErrors(); + ts.assertValues(Integer.MAX_VALUE - 1, Integer.MAX_VALUE); + } + + @Test + public void nearMaxValueWithBackpressure() { + TestSubscriber<Integer> ts = new TestSubscriber<>(3L); + Flowable.range(Integer.MAX_VALUE - 1, 2).subscribe(ts); + + ts.assertComplete(); + ts.assertNoErrors(); + ts.assertValues(Integer.MAX_VALUE - 1, Integer.MAX_VALUE); + } + + @Test + public void negativeCount() { + try { + Flowable.range(1, -1); + fail("Should have thrown IllegalArgumentException"); + } catch (IllegalArgumentException ex) { + assertEquals("count >= 0 required but it was -1", ex.getMessage()); + } + } + + @Test + public void requestWrongFusion() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<Integer>().setInitialFusionMode(QueueFuseable.ASYNC); + + Flowable.range(1, 5) + .subscribe(ts); + + ts.assertFusionMode(QueueFuseable.NONE) + .assertResult(1, 2, 3, 4, 5); + } + + @Test + public void countOne() { + Flowable.range(5495454, 1) + .test() + .assertResult(5495454); + } + + @Test + public void fused() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<Integer>().setInitialFusionMode(QueueFuseable.ANY); + + Flowable.range(1, 2).subscribe(ts); + + ts.assertFusionMode(QueueFuseable.SYNC) + .assertResult(1, 2); + } + + @Test + public void fusedReject() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<Integer>().setInitialFusionMode(QueueFuseable.ASYNC); + + Flowable.range(1, 2).subscribe(ts); + + ts.assertFusionMode(QueueFuseable.NONE) + .assertResult(1, 2); + } + + @Test + public void disposed() { + TestHelper.checkDisposed(Flowable.range(1, 2)); + } + + @Test + public void fusedClearIsEmpty() { + TestHelper.checkFusedIsEmptyClear(Flowable.range(1, 2)); + } + + @Test + public void noOverflow() { + Flowable.range(Integer.MAX_VALUE - 1, 2); + Flowable.range(Integer.MIN_VALUE, 2); + Flowable.range(Integer.MIN_VALUE, Integer.MAX_VALUE); + } + + @Test + public void conditionalNormal() { + Flowable.range(1, 5) + .filter(Functions.alwaysTrue()) + .test() + .assertResult(1, 2, 3, 4, 5); + } + + @Test + public void badRequest() { + TestHelper.assertBadRequestReported(Flowable.range(1, 5)); + + TestHelper.assertBadRequestReported(Flowable.range(1, 5).filter(Functions.alwaysTrue())); + } + + @Test + public void conditionalNormalSlowpath() { + Flowable.range(1, 5) + .filter(Functions.alwaysTrue()) + .test(5) + .assertResult(1, 2, 3, 4, 5); + } + + @Test + public void conditionalSlowPathTakeExact() { + Flowable.range(1, 5) + .filter(Functions.alwaysTrue()) + .take(5) + .test() + .assertResult(1, 2, 3, 4, 5); + } + + @Test + public void slowPathTakeExact() { + Flowable.range(1, 5) + .filter(Functions.alwaysTrue()) + .take(5) + .test() + .assertResult(1, 2, 3, 4, 5); + } + + @Test + public void conditionalSlowPathRebatch() { + Flowable.range(1, 5) + .filter(Functions.alwaysTrue()) + .rebatchRequests(1) + .test() + .assertResult(1, 2, 3, 4, 5); + } + + @Test + public void slowPathRebatch() { + Flowable.range(1, 5) + .rebatchRequests(1) + .test() + .assertResult(1, 2, 3, 4, 5); + } + + @Test + public void slowPathCancel() { + TestSubscriber<Integer> ts = new TestSubscriber<Integer>(2L) { + @Override + public void onNext(Integer t) { + super.onNext(t); + cancel(); + onComplete(); + } + }; + + Flowable.range(1, 5) + .subscribe(ts); + + ts.assertResult(1); + } + + @Test + public void fastPathCancel() { + TestSubscriber<Integer> ts = new TestSubscriber<Integer>() { + @Override + public void onNext(Integer t) { + super.onNext(t); + cancel(); + onComplete(); + } + }; + + Flowable.range(1, 5) + .subscribe(ts); + + ts.assertResult(1); + } + + @Test + public void conditionalSlowPathCancel() { + TestSubscriber<Integer> ts = new TestSubscriber<Integer>(1L) { + @Override + public void onNext(Integer t) { + super.onNext(t); + cancel(); + onComplete(); + } + }; + + Flowable.range(1, 5) + .filter(Functions.alwaysTrue()) + .subscribe(ts); + + ts.assertResult(1); + } + + @Test + public void conditionalFastPathCancel() { + TestSubscriber<Integer> ts = new TestSubscriber<Integer>() { + @Override + public void onNext(Integer t) { + super.onNext(t); + cancel(); + onComplete(); + } + }; + + Flowable.range(1, 5) + .filter(Functions.alwaysTrue()) + .subscribe(ts); + + ts.assertResult(1); + } + + @Test + public void conditionalRequestOneByOne() { + TestSubscriber<Integer> ts = new TestSubscriber<Integer>(1L) { + @Override + public void onNext(Integer t) { + super.onNext(t); + request(1); + } + }; + + Flowable.range(1, 5) + .filter(new Predicate<Integer>() { + @Override + public boolean test(Integer v) throws Exception { + return v % 2 == 0; + } + }) + .subscribe(ts); + + ts.assertResult(2, 4); + } + + @Test + public void conditionalRequestOneByOne2() { + TestSubscriber<Integer> ts = new TestSubscriber<Integer>(1L) { + @Override + public void onNext(Integer t) { + super.onNext(t); + request(1); + } + }; + + Flowable.range(1, 5) + .filter(Functions.alwaysTrue()) + .subscribe(ts); + + ts.assertResult(1, 2, 3, 4, 5); + } + + @Test + public void fastPathCancelExact() { + TestSubscriber<Integer> ts = new TestSubscriber<Integer>() { + @Override + public void onNext(Integer t) { + super.onNext(t); + if (t == 5L) { + cancel(); + onComplete(); + } + } + }; + + Flowable.range(1, 5) + .subscribe(ts); + + ts.assertResult(1, 2, 3, 4, 5); + } + + @Test + public void conditionalFastPathCancelExact() { + TestSubscriber<Integer> ts = new TestSubscriber<Integer>() { + @Override + public void onNext(Integer t) { + super.onNext(t); + if (t == 5L) { + cancel(); + onComplete(); + } + } + }; + + Flowable.range(1, 5) + .filter(new Predicate<Integer>() { + @Override + public boolean test(Integer v) throws Exception { + return v % 2 == 0; + } + }) + .subscribe(ts); + + ts.assertResult(2, 4); + } + + @Test + public void conditionalCancel1() { + TestSubscriber<Integer> ts = new TestSubscriber<Integer>(2L) { + @Override + public void onNext(Integer t) { + super.onNext(t); + if (t == 1) { + cancel(); + onComplete(); + } + } + }; + + Flowable.range(1, 2) + .filter(Functions.alwaysTrue()) + .subscribe(ts); + + ts.assertResult(1); + } + + @Test + public void conditionalCancel2() { + TestSubscriber<Integer> ts = new TestSubscriber<Integer>(2L) { + @Override + public void onNext(Integer t) { + super.onNext(t); + if (t == 2) { + cancel(); + onComplete(); + } + } + }; + + Flowable.range(1, 2) + .filter(Functions.alwaysTrue()) + .subscribe(ts); + + ts.assertResult(1, 2); + } + + @Test + public void slowPathCancelBeforeComplete() { + Flowable.range(1, 2) + .take(2) + .test() + .assertResult(1, 2); + } + + @Test + public void conditionalFastPatchCancelBeforeComplete() { + TestSubscriber<Integer> ts = new TestSubscriber<>(); + + Flowable.range(1, 2) + .compose(TestHelper.conditional()) + .doOnNext(v -> { + if (v == 2) { + ts.cancel(); + } + }) + .subscribe(ts); + + ts.assertValuesOnly(1, 2); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableReduceTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableReduceTest.java new file mode 100644 index 0000000000..16b491a352 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableReduceTest.java @@ -0,0 +1,538 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.*; +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.fuseable.HasUpstreamPublisher; +import io.reactivex.rxjava3.internal.subscriptions.BooleanSubscription; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.processors.PublishProcessor; +import io.reactivex.rxjava3.schedulers.Schedulers; +import io.reactivex.rxjava3.subscribers.TestSubscriber; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class FlowableReduceTest extends RxJavaTest { + Subscriber<Object> subscriber; + + SingleObserver<Object> singleObserver; + + @Before + public void before() { + subscriber = TestHelper.mockSubscriber(); + singleObserver = TestHelper.mockSingleObserver(); + } + + BiFunction<Integer, Integer, Integer> sum = new BiFunction<Integer, Integer, Integer>() { + @Override + public Integer apply(Integer t1, Integer t2) { + return t1 + t2; + } + }; + + @Test + public void aggregateAsIntSumFlowable() { + + Flowable<Integer> result = Flowable.just(1, 2, 3, 4, 5).reduce(0, sum).toFlowable() + .map(new Function<Integer, Integer>() { + @Override + public Integer apply(Integer v) { + return v; + } + }); + + result.subscribe(subscriber); + + verify(subscriber).onNext(1 + 2 + 3 + 4 + 5); + verify(subscriber).onComplete(); + verify(subscriber, never()).onError(any(Throwable.class)); + } + + @Test + public void aggregateAsIntSumSourceThrowsFlowable() { + Flowable<Integer> result = Flowable.concat(Flowable.just(1, 2, 3, 4, 5), + Flowable.<Integer> error(new TestException())) + .reduce(0, sum).toFlowable().map(new Function<Integer, Integer>() { + @Override + public Integer apply(Integer v) { + return v; + } + }); + + result.subscribe(subscriber); + + verify(subscriber, never()).onNext(any()); + verify(subscriber, never()).onComplete(); + verify(subscriber, times(1)).onError(any(TestException.class)); + } + + @Test + public void aggregateAsIntSumAccumulatorThrowsFlowable() { + BiFunction<Integer, Integer, Integer> sumErr = new BiFunction<Integer, Integer, Integer>() { + @Override + public Integer apply(Integer t1, Integer t2) { + throw new TestException(); + } + }; + + Flowable<Integer> result = Flowable.just(1, 2, 3, 4, 5) + .reduce(0, sumErr).toFlowable().map(new Function<Integer, Integer>() { + @Override + public Integer apply(Integer v) { + return v; + } + }); + + result.subscribe(subscriber); + + verify(subscriber, never()).onNext(any()); + verify(subscriber, never()).onComplete(); + verify(subscriber, times(1)).onError(any(TestException.class)); + } + + @Test + public void aggregateAsIntSumResultSelectorThrowsFlowable() { + + Function<Integer, Integer> error = new Function<Integer, Integer>() { + + @Override + public Integer apply(Integer t1) { + throw new TestException(); + } + }; + + Flowable<Integer> result = Flowable.just(1, 2, 3, 4, 5) + .reduce(0, sum).toFlowable().map(error); + + result.subscribe(subscriber); + + verify(subscriber, never()).onNext(any()); + verify(subscriber, never()).onComplete(); + verify(subscriber, times(1)).onError(any(TestException.class)); + } + + @Test + public void backpressureWithInitialValueFlowable() throws InterruptedException { + Flowable<Integer> source = Flowable.just(1, 2, 3, 4, 5, 6); + Flowable<Integer> reduced = source.reduce(0, sum).toFlowable(); + + Integer r = reduced.blockingFirst(); + assertEquals(21, r.intValue()); + } + + @Test + public void aggregateAsIntSum() { + + Single<Integer> result = Flowable.just(1, 2, 3, 4, 5).reduce(0, sum) + .map(new Function<Integer, Integer>() { + @Override + public Integer apply(Integer v) { + return v; + } + }); + + result.subscribe(singleObserver); + + verify(singleObserver).onSuccess(1 + 2 + 3 + 4 + 5); + verify(singleObserver, never()).onError(any(Throwable.class)); + } + + @Test + public void aggregateAsIntSumSourceThrows() { + Single<Integer> result = Flowable.concat(Flowable.just(1, 2, 3, 4, 5), + Flowable.<Integer> error(new TestException())) + .reduce(0, sum).map(new Function<Integer, Integer>() { + @Override + public Integer apply(Integer v) { + return v; + } + }); + + result.subscribe(singleObserver); + + verify(singleObserver, never()).onSuccess(any()); + verify(singleObserver, times(1)).onError(any(TestException.class)); + } + + @Test + public void aggregateAsIntSumAccumulatorThrows() { + BiFunction<Integer, Integer, Integer> sumErr = new BiFunction<Integer, Integer, Integer>() { + @Override + public Integer apply(Integer t1, Integer t2) { + throw new TestException(); + } + }; + + Single<Integer> result = Flowable.just(1, 2, 3, 4, 5) + .reduce(0, sumErr).map(new Function<Integer, Integer>() { + @Override + public Integer apply(Integer v) { + return v; + } + }); + + result.subscribe(singleObserver); + + verify(singleObserver, never()).onSuccess(any()); + verify(singleObserver, times(1)).onError(any(TestException.class)); + } + + @Test + public void aggregateAsIntSumResultSelectorThrows() { + + Function<Integer, Integer> error = new Function<Integer, Integer>() { + + @Override + public Integer apply(Integer t1) { + throw new TestException(); + } + }; + + Single<Integer> result = Flowable.just(1, 2, 3, 4, 5) + .reduce(0, sum).map(error); + + result.subscribe(singleObserver); + + verify(singleObserver, never()).onSuccess(any()); + verify(singleObserver, times(1)).onError(any(TestException.class)); + } + + @Test + public void backpressureWithNoInitialValue() throws InterruptedException { + Flowable<Integer> source = Flowable.just(1, 2, 3, 4, 5, 6); + Maybe<Integer> reduced = source.reduce(sum); + + Integer r = reduced.blockingGet(); + assertEquals(21, r.intValue()); + } + + @Test + public void backpressureWithInitialValue() throws InterruptedException { + Flowable<Integer> source = Flowable.just(1, 2, 3, 4, 5, 6); + Single<Integer> reduced = source.reduce(0, sum); + + Integer r = reduced.blockingGet(); + assertEquals(21, r.intValue()); + } + + @Test + public void reducerCrashSuppressOnError() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + + try { + Flowable.<Integer>fromPublisher(new Publisher<Integer>() { + @Override + public void subscribe(Subscriber<? super Integer> s) { + s.onSubscribe(new BooleanSubscription()); + s.onNext(1); + s.onNext(1); + s.onError(new TestException("Source")); + s.onComplete(); + } + }) + .reduce(new BiFunction<Integer, Integer, Integer>() { + @Override + public Integer apply(Integer a, Integer b) throws Exception { + throw new TestException("Reducer"); + } + }) + .toFlowable() + .to(TestHelper.<Integer>testConsumer()) + .assertFailureAndMessage(TestException.class, "Reducer"); + + TestHelper.assertUndeliverable(errors, 0, TestException.class, "Source"); + } finally { + RxJavaPlugins.reset(); + } + + } + + @Test + public void cancel() { + + TestSubscriber<Integer> ts = Flowable.just(1) + .concatWith(Flowable.<Integer>never()) + .reduce(new BiFunction<Integer, Integer, Integer>() { + @Override + public Integer apply(Integer a, Integer b) throws Exception { + return a + b; + } + }).toFlowable() + .test(); + + ts.assertEmpty(); + + ts.cancel(); + + ts.assertEmpty(); + + } + + @Test + public void backpressureWithNoInitialValueObservable() throws InterruptedException { + Flowable<Integer> source = Flowable.just(1, 2, 3, 4, 5, 6); + Flowable<Integer> reduced = source.reduce(sum).toFlowable(); + + Integer r = reduced.blockingFirst(); + assertEquals(21, r.intValue()); + } + + @Test + public void source() { + Flowable<Integer> source = Flowable.just(1); + + assertSame(source, (((HasUpstreamPublisher<?>)source.reduce(sum))).source()); + } + + @Test + public void dispose() { + TestHelper.checkDisposed(Flowable.range(1, 2).reduce(sum)); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeFlowableToMaybe(new Function<Flowable<Integer>, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Flowable<Integer> f) throws Exception { + return f.reduce(sum); + } + }); + } + + @Test + public void error() { + Flowable.<Integer>error(new TestException()) + .reduce(sum) + .test() + .assertFailure(TestException.class); + } + + @Test + public void errorFlowable() { + Flowable.<Integer>error(new TestException()) + .reduce(sum) + .toFlowable() + .test() + .assertFailure(TestException.class); + } + + @Test + public void empty() { + Flowable.<Integer>empty() + .reduce(sum) + .test() + .assertResult(); + } + + @Test + public void emptyFlowable() { + Flowable.<Integer>empty() + .reduce(sum) + .toFlowable() + .test() + .assertResult(); + } + + @Test + public void badSource() { + TestHelper.checkBadSourceFlowable(new Function<Flowable<Integer>, Object>() { + @Override + public Object apply(Flowable<Integer> f) throws Exception { + return f.reduce(sum); + } + }, false, 1, 1, 1); + } + + @Test + public void badSourceFlowable() { + TestHelper.checkBadSourceFlowable(new Function<Flowable<Integer>, Object>() { + @Override + public Object apply(Flowable<Integer> f) throws Exception { + return f.reduce(sum).toFlowable(); + } + }, false, 1, 1, 1); + } + + @Test + public void reducerThrows() { + Flowable.just(1, 2) + .reduce(new BiFunction<Integer, Integer, Integer>() { + @Override + public Integer apply(Integer a, Integer b) throws Exception { + throw new TestException(); + } + }) + .test() + .assertFailure(TestException.class); + } + + /** + * Make sure an asynchronous reduce with flatMap works. + * Original Reactor-Core test case: https://gist.github.com/jurna/353a2bd8ff83f0b24f0b5bc772077d61 + */ + @Test + public void shouldReduceTo10Events() { + final AtomicInteger count = new AtomicInteger(); + + Flowable.range(0, 10).flatMap(new Function<Integer, Publisher<String>>() { + @Override + public Publisher<String> apply(final Integer x) throws Exception { + return Flowable.range(0, 2) + .map(new Function<Integer, String>() { + @Override + public String apply(Integer y) throws Exception { + return blockingOp(x, y); + } + }).subscribeOn(Schedulers.io()) + .reduce(new BiFunction<String, String, String>() { + @Override + public String apply(String l, String r) throws Exception { + return l + "_" + r; + } + }) + .doOnSuccess(new Consumer<String>() { + @Override + public void accept(String s) throws Exception { + count.incrementAndGet(); + System.out.println("Completed with " + s); + } + }) + .toFlowable(); + } + } + ).blockingLast(); + + assertEquals(10, count.get()); + } + + /** + * Make sure an asynchronous reduce with flatMap works. + * Original Reactor-Core test case: https://gist.github.com/jurna/353a2bd8ff83f0b24f0b5bc772077d61 + */ + @Test + public void shouldReduceTo10EventsFlowable() { + final AtomicInteger count = new AtomicInteger(); + + Flowable.range(0, 10).flatMap(new Function<Integer, Publisher<String>>() { + @Override + public Publisher<String> apply(final Integer x) throws Exception { + return Flowable.range(0, 2) + .map(new Function<Integer, String>() { + @Override + public String apply(Integer y) throws Exception { + return blockingOp(x, y); + } + }).subscribeOn(Schedulers.io()) + .reduce(new BiFunction<String, String, String>() { + @Override + public String apply(String l, String r) throws Exception { + return l + "_" + r; + } + }) + .toFlowable() + .doOnNext(new Consumer<String>() { + @Override + public void accept(String s) throws Exception { + count.incrementAndGet(); + System.out.println("Completed with " + s); + } + }) + ; + } + } + ).blockingLast(); + + assertEquals(10, count.get()); + } + + static String blockingOp(Integer x, Integer y) { + try { + Thread.sleep(100); + } catch (InterruptedException e) { + e.printStackTrace(); + } + return "x" + x + "y" + y; + } + + @Test + public void seedDoubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeFlowableToSingle(new Function<Flowable<Integer>, SingleSource<Integer>>() { + @Override + public SingleSource<Integer> apply(Flowable<Integer> f) + throws Exception { + return f.reduce(0, new BiFunction<Integer, Integer, Integer>() { + @Override + public Integer apply(Integer a, Integer b) throws Exception { + return a; + } + }); + } + }); + } + + @Test + public void seedDisposed() { + TestHelper.checkDisposed(PublishProcessor.<Integer>create().reduce(0, new BiFunction<Integer, Integer, Integer>() { + @Override + public Integer apply(Integer a, Integer b) throws Exception { + return a; + } + })); + } + + @Test + public void seedBadSource() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + new Flowable<Integer>() { + @Override + protected void subscribeActual(Subscriber<? super Integer> subscriber) { + subscriber.onSubscribe(new BooleanSubscription()); + subscriber.onComplete(); + subscriber.onNext(1); + subscriber.onError(new TestException()); + subscriber.onComplete(); + } + } + .reduce(0, new BiFunction<Integer, Integer, Integer>() { + @Override + public Integer apply(Integer a, Integer b) throws Exception { + return a; + } + }) + .test() + .assertResult(0); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void doubleOnSubscribeFlowable() { + TestHelper.checkDoubleOnSubscribeFlowable(f -> f.reduce((a, b) -> a).toFlowable()); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableReduceWithSingleTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableReduceWithSingleTest.java new file mode 100644 index 0000000000..3de991aa2e --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableReduceWithSingleTest.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.functions.BiFunction; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class FlowableReduceWithSingleTest extends RxJavaTest { + + @Test + public void normal() { + Flowable.range(1, 5) + .reduceWith(Functions.justSupplier(1), new BiFunction<Integer, Integer, Integer>() { + @Override + public Integer apply(Integer a, Integer b) throws Exception { + return a + b; + } + }) + .test() + .assertResult(16); + } + + @Test + public void disposed() { + TestHelper.checkDisposed(Flowable.range(1, 5) + .reduceWith(Functions.justSupplier(1), new BiFunction<Integer, Integer, Integer>() { + @Override + public Integer apply(Integer a, Integer b) throws Exception { + return a + b; + } + })); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableRefCountTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableRefCountTest.java new file mode 100644 index 0000000000..48fbb633ae --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableRefCountTest.java @@ -0,0 +1,1480 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import java.io.IOException; +import java.lang.management.ManagementFactory; +import java.util.*; +import java.util.concurrent.*; +import java.util.concurrent.atomic.*; + +import org.junit.Test; +import org.mockito.InOrder; +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.*; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.flowables.ConnectableFlowable; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.internal.operators.flowable.FlowableRefCount.RefConnection; +import io.reactivex.rxjava3.internal.subscriptions.BooleanSubscription; +import io.reactivex.rxjava3.internal.util.ExceptionHelper; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.processors.*; +import io.reactivex.rxjava3.schedulers.*; +import io.reactivex.rxjava3.subscribers.TestSubscriber; +import io.reactivex.rxjava3.testsupport.*; + +public class FlowableRefCountTest extends RxJavaTest { + + @Test + public void refCountAsync() throws InterruptedException { + // Flaky + for (int i = 0; i < 10; i++) { + try { + refCountAsyncActual(); + return; + } catch (AssertionError ex) { + if (i == 9) { + throw ex; + } + Thread.sleep((int)(200 * (Math.random() * 10 + 1))); + } + } + } + + /** + * Tries to coordinate async counting but it is flaky due to the low 10s of milliseconds. + */ + void refCountAsyncActual() { + final AtomicInteger subscribeCount = new AtomicInteger(); + final AtomicInteger nextCount = new AtomicInteger(); + Flowable<Long> r = Flowable.interval(0, 20, TimeUnit.MILLISECONDS) + .doOnSubscribe(new Consumer<Subscription>() { + @Override + public void accept(Subscription s) { + subscribeCount.incrementAndGet(); + } + }) + .doOnNext(new Consumer<Long>() { + @Override + public void accept(Long l) { + nextCount.incrementAndGet(); + } + }) + .publish().refCount(); + + final AtomicInteger receivedCount = new AtomicInteger(); + Disposable d1 = r.subscribe(new Consumer<Long>() { + @Override + public void accept(Long l) { + receivedCount.incrementAndGet(); + } + }); + + Disposable d2 = r.subscribe(); + + try { + Thread.sleep(10); + } catch (InterruptedException e) { + } + + for (;;) { + int a = nextCount.get(); + int b = receivedCount.get(); + if (a > 10 && a < 20 && a == b) { + break; + } + if (a >= 20) { + break; + } + try { + Thread.sleep(20); + } catch (InterruptedException e) { + } + } + // give time to emit + + // now unsubscribe + d2.dispose(); // unsubscribe s2 first as we're counting in 1 and there can be a race between unsubscribe and one subscriber getting a value but not the other + d1.dispose(); + + System.out.println("onNext: " + nextCount.get()); + + // should emit once for both subscribers + assertEquals(nextCount.get(), receivedCount.get()); + // only 1 subscribe + assertEquals(1, subscribeCount.get()); + } + + @Test + public void refCountSynchronous() { + final AtomicInteger subscribeCount = new AtomicInteger(); + final AtomicInteger nextCount = new AtomicInteger(); + Flowable<Integer> r = Flowable.just(1, 2, 3, 4, 5, 6, 7, 8, 9) + .doOnSubscribe(new Consumer<Subscription>() { + @Override + public void accept(Subscription s) { + subscribeCount.incrementAndGet(); + } + }) + .doOnNext(new Consumer<Integer>() { + @Override + public void accept(Integer l) { + nextCount.incrementAndGet(); + } + }) + .publish().refCount(); + + final AtomicInteger receivedCount = new AtomicInteger(); + Disposable d1 = r.subscribe(new Consumer<Integer>() { + @Override + public void accept(Integer l) { + receivedCount.incrementAndGet(); + } + }); + + Disposable d2 = r.subscribe(); + + // give time to emit + try { + Thread.sleep(50); + } catch (InterruptedException e) { + } + + // now unsubscribe + d2.dispose(); // unsubscribe s2 first as we're counting in 1 and there can be a race between unsubscribe and one subscriber getting a value but not the other + d1.dispose(); + + System.out.println("onNext Count: " + nextCount.get()); + + // it will emit twice because it is synchronous + assertEquals(nextCount.get(), receivedCount.get() * 2); + // it will subscribe twice because it is synchronous + assertEquals(2, subscribeCount.get()); + } + + @Test + public void refCountSynchronousTake() { + final AtomicInteger nextCount = new AtomicInteger(); + Flowable<Integer> r = Flowable.just(1, 2, 3, 4, 5, 6, 7, 8, 9) + .doOnNext(new Consumer<Integer>() { + @Override + public void accept(Integer l) { + System.out.println("onNext --------> " + l); + nextCount.incrementAndGet(); + } + }) + .take(4) + .publish().refCount(); + + final AtomicInteger receivedCount = new AtomicInteger(); + r.subscribe(new Consumer<Integer>() { + @Override + public void accept(Integer l) { + receivedCount.incrementAndGet(); + } + }); + + System.out.println("onNext: " + nextCount.get()); + + assertEquals(4, receivedCount.get()); + assertEquals(4, receivedCount.get()); + } + + @Test + public void repeat() { + final AtomicInteger subscribeCount = new AtomicInteger(); + final AtomicInteger unsubscribeCount = new AtomicInteger(); + Flowable<Long> r = Flowable.interval(0, 1, TimeUnit.MILLISECONDS) + .doOnSubscribe(new Consumer<Subscription>() { + @Override + public void accept(Subscription s) { + System.out.println("******************************* Subscribe received"); + // when we are subscribed + subscribeCount.incrementAndGet(); + } + }) + .doOnCancel(new Action() { + @Override + public void run() { + System.out.println("******************************* Unsubscribe received"); + // when we are unsubscribed + unsubscribeCount.incrementAndGet(); + } + }) + .publish().refCount(); + + for (int i = 0; i < 10; i++) { + TestSubscriber<Long> ts1 = new TestSubscriber<>(); + TestSubscriber<Long> ts2 = new TestSubscriber<>(); + r.subscribe(ts1); + r.subscribe(ts2); + try { + Thread.sleep(50); + } catch (InterruptedException e) { + } + ts1.cancel(); + ts2.cancel(); + ts1.assertNoErrors(); + ts2.assertNoErrors(); + assertTrue(ts1.values().size() > 0); + assertTrue(ts2.values().size() > 0); + } + + assertEquals(10, subscribeCount.get()); + assertEquals(10, unsubscribeCount.get()); + } + + @Test + public void connectUnsubscribe() throws InterruptedException { + final CountDownLatch unsubscribeLatch = new CountDownLatch(1); + final CountDownLatch subscribeLatch = new CountDownLatch(1); + + Flowable<Long> f = synchronousInterval() + .doOnSubscribe(new Consumer<Subscription>() { + @Override + public void accept(Subscription s) { + System.out.println("******************************* Subscribe received"); + // when we are subscribed + subscribeLatch.countDown(); + } + }) + .doOnCancel(new Action() { + @Override + public void run() { + System.out.println("******************************* Unsubscribe received"); + // when we are unsubscribed + unsubscribeLatch.countDown(); + } + }); + + TestSubscriberEx<Long> s = new TestSubscriberEx<>(); + f.publish().refCount().subscribeOn(Schedulers.newThread()).subscribe(s); + System.out.println("send unsubscribe"); + // wait until connected + subscribeLatch.await(); + // now unsubscribe + s.cancel(); + System.out.println("DONE sending unsubscribe ... now waiting"); + if (!unsubscribeLatch.await(3000, TimeUnit.MILLISECONDS)) { + System.out.println("Errors: " + s.errors()); + if (s.errors().size() > 0) { + s.errors().get(0).printStackTrace(); + } + fail("timed out waiting for unsubscribe"); + } + s.assertNoErrors(); + } + + @Test + public void connectUnsubscribeRaceConditionLoop() throws InterruptedException { + for (int i = 0; i < 100; i++) { + connectUnsubscribeRaceCondition(); + } + } + + @Test + public void connectUnsubscribeRaceCondition() throws InterruptedException { + final AtomicInteger subUnsubCount = new AtomicInteger(); + Flowable<Long> f = synchronousInterval() + .doOnCancel(new Action() { + @Override + public void run() { + System.out.println("******************************* Unsubscribe received"); + // when we are unsubscribed + subUnsubCount.decrementAndGet(); + } + }) + .doOnSubscribe(new Consumer<Subscription>() { + @Override + public void accept(Subscription s) { + System.out.println("******************************* SUBSCRIBE received"); + subUnsubCount.incrementAndGet(); + } + }); + + TestSubscriberEx<Long> s = new TestSubscriberEx<>(); + + f.publish().refCount().subscribeOn(Schedulers.computation()).subscribe(s); + System.out.println("send unsubscribe"); + // now immediately unsubscribe while subscribeOn is racing to subscribe + s.cancel(); + // this generally will mean it won't even subscribe as it is already unsubscribed by the time connect() gets scheduled + // give time to the counter to update + Thread.sleep(10); + // either we subscribed and then unsubscribed, or we didn't ever even subscribe + assertEquals(0, subUnsubCount.get()); + + System.out.println("DONE sending unsubscribe ... now waiting"); + System.out.println("Errors: " + s.errors()); + if (s.errors().size() > 0) { + s.errors().get(0).printStackTrace(); + } + s.assertNoErrors(); + } + + private Flowable<Long> synchronousInterval() { + return Flowable.unsafeCreate(new Publisher<Long>() { + @Override + public void subscribe(Subscriber<? super Long> subscriber) { + final AtomicBoolean cancel = new AtomicBoolean(); + subscriber.onSubscribe(new Subscription() { + @Override + public void request(long n) { + + } + + @Override + public void cancel() { + cancel.set(true); + } + + }); + for (;;) { + if (cancel.get()) { + break; + } + try { + Thread.sleep(100); + } catch (InterruptedException e) { + } + subscriber.onNext(1L); + } + } + }); + } + + @Test + public void onlyFirstShouldSubscribeAndLastUnsubscribe() { + final AtomicInteger subscriptionCount = new AtomicInteger(); + final AtomicInteger unsubscriptionCount = new AtomicInteger(); + Flowable<Integer> flowable = Flowable.unsafeCreate(new Publisher<Integer>() { + @Override + public void subscribe(Subscriber<? super Integer> subscriber) { + subscriptionCount.incrementAndGet(); + subscriber.onSubscribe(new Subscription() { + @Override + public void request(long n) { + + } + + @Override + public void cancel() { + unsubscriptionCount.incrementAndGet(); + } + }); + } + }); + Flowable<Integer> refCounted = flowable.publish().refCount(); + + Disposable first = refCounted.subscribe(); + assertEquals(1, subscriptionCount.get()); + + Disposable second = refCounted.subscribe(); + assertEquals(1, subscriptionCount.get()); + + first.dispose(); + assertEquals(0, unsubscriptionCount.get()); + + second.dispose(); + assertEquals(1, unsubscriptionCount.get()); + } + + @Test + public void refCount() { + TestScheduler s = new TestScheduler(); + Flowable<Long> interval = Flowable.interval(100, TimeUnit.MILLISECONDS, s).publish().refCount(); + + // subscribe list1 + final List<Long> list1 = new ArrayList<>(); + Disposable d1 = interval.subscribe(new Consumer<Long>() { + @Override + public void accept(Long t1) { + list1.add(t1); + } + }); + + s.advanceTimeBy(200, TimeUnit.MILLISECONDS); + + assertEquals(2, list1.size()); + assertEquals(0L, list1.get(0).longValue()); + assertEquals(1L, list1.get(1).longValue()); + + // subscribe list2 + final List<Long> list2 = new ArrayList<>(); + Disposable d2 = interval.subscribe(new Consumer<Long>() { + @Override + public void accept(Long t1) { + list2.add(t1); + } + }); + + s.advanceTimeBy(300, TimeUnit.MILLISECONDS); + + // list 1 should have 5 items + assertEquals(5, list1.size()); + assertEquals(2L, list1.get(2).longValue()); + assertEquals(3L, list1.get(3).longValue()); + assertEquals(4L, list1.get(4).longValue()); + + // list 2 should only have 3 items + assertEquals(3, list2.size()); + assertEquals(2L, list2.get(0).longValue()); + assertEquals(3L, list2.get(1).longValue()); + assertEquals(4L, list2.get(2).longValue()); + + // unsubscribe list1 + d1.dispose(); + + // advance further + s.advanceTimeBy(300, TimeUnit.MILLISECONDS); + + // list 1 should still have 5 items + assertEquals(5, list1.size()); + + // list 2 should have 6 items + assertEquals(6, list2.size()); + assertEquals(5L, list2.get(3).longValue()); + assertEquals(6L, list2.get(4).longValue()); + assertEquals(7L, list2.get(5).longValue()); + + // unsubscribe list2 + d2.dispose(); + + // advance further + s.advanceTimeBy(1000, TimeUnit.MILLISECONDS); + + // subscribing a new one should start over because the source should have been unsubscribed + // subscribe list3 + final List<Long> list3 = new ArrayList<>(); + interval.subscribe(new Consumer<Long>() { + @Override + public void accept(Long t1) { + list3.add(t1); + } + }); + + s.advanceTimeBy(200, TimeUnit.MILLISECONDS); + + assertEquals(2, list3.size()); + assertEquals(0L, list3.get(0).longValue()); + assertEquals(1L, list3.get(1).longValue()); + + } + + @Test + public void alreadyUnsubscribedClient() { + Subscriber<Integer> done = CancelledSubscriber.INSTANCE; + + Subscriber<Integer> subscriber = TestHelper.mockSubscriber(); + + Flowable<Integer> result = Flowable.just(1).publish().refCount(); + + result.subscribe(done); + + result.subscribe(subscriber); + + verify(subscriber).onNext(1); + verify(subscriber).onComplete(); + verify(subscriber, never()).onError(any(Throwable.class)); + } + + @Test + public void alreadyUnsubscribedInterleavesWithClient() { + ReplayProcessor<Integer> source = ReplayProcessor.create(); + + Subscriber<Integer> done = CancelledSubscriber.INSTANCE; + + Subscriber<Integer> subscriber = TestHelper.mockSubscriber(); + InOrder inOrder = inOrder(subscriber); + + Flowable<Integer> result = source.publish().refCount(); + + result.subscribe(subscriber); + + source.onNext(1); + + result.subscribe(done); + + source.onNext(2); + source.onComplete(); + + inOrder.verify(subscriber).onNext(1); + inOrder.verify(subscriber).onNext(2); + inOrder.verify(subscriber).onComplete(); + verify(subscriber, never()).onError(any(Throwable.class)); + } + + @Test + public void connectDisconnectConnectAndSubjectState() { + Flowable<Integer> f1 = Flowable.just(10); + Flowable<Integer> f2 = Flowable.just(20); + Flowable<Integer> combined = Flowable.combineLatest(f1, f2, new BiFunction<Integer, Integer, Integer>() { + @Override + public Integer apply(Integer t1, Integer t2) { + return t1 + t2; + } + }) + .publish().refCount(); + + TestSubscriberEx<Integer> ts1 = new TestSubscriberEx<>(); + TestSubscriberEx<Integer> ts2 = new TestSubscriberEx<>(); + + combined.subscribe(ts1); + combined.subscribe(ts2); + + ts1.assertTerminated(); + ts1.assertNoErrors(); + ts1.assertValue(30); + + ts2.assertTerminated(); + ts2.assertNoErrors(); + ts2.assertValue(30); + } + + @Test + public void upstreamErrorAllowsRetry() throws InterruptedException { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + final AtomicInteger intervalSubscribed = new AtomicInteger(); + Flowable<String> interval = + Flowable.interval(200, TimeUnit.MILLISECONDS) + .doOnSubscribe(new Consumer<Subscription>() { + @Override + public void accept(Subscription s) { + System.out.println("Subscribing to interval " + intervalSubscribed.incrementAndGet()); + } + } + ) + .flatMap(new Function<Long, Publisher<String>>() { + @Override + public Publisher<String> apply(Long t1) { + return Flowable.defer(new Supplier<Publisher<String>>() { + @Override + public Publisher<String> get() { + return Flowable.<String>error(new TestException("Some exception")); + } + }); + } + }) + .onErrorResumeNext(new Function<Throwable, Publisher<String>>() { + @Override + public Publisher<String> apply(Throwable t1) { + return Flowable.error(t1); + } + }) + .publish() + .refCount(); + + interval + .doOnError(new Consumer<Throwable>() { + @Override + public void accept(Throwable t1) { + System.out.println("Subscriber 1 onError: " + t1); + } + }) + .retry(5) + .subscribe(new Consumer<String>() { + @Override + public void accept(String t1) { + System.out.println("Subscriber 1: " + t1); + } + }); + Thread.sleep(100); + interval + .doOnError(new Consumer<Throwable>() { + @Override + public void accept(Throwable t1) { + System.out.println("Subscriber 2 onError: " + t1); + } + }) + .retry(5) + .subscribe(new Consumer<String>() { + @Override + public void accept(String t1) { + System.out.println("Subscriber 2: " + t1); + } + }); + + Thread.sleep(1300); + + System.out.println(intervalSubscribed.get()); + assertEquals(6, intervalSubscribed.get()); + + TestHelper.assertError(errors, 0, OnErrorNotImplementedException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + private enum CancelledSubscriber implements FlowableSubscriber<Integer> { + INSTANCE; + + @Override public void onSubscribe(Subscription s) { + s.cancel(); + } + + @Override public void onNext(Integer o) { + } + + @Override public void onError(Throwable t) { + } + + @Override public void onComplete() { + } + } + + @Test + public void disposed() { + TestHelper.checkDisposed(Flowable.just(1).publish().refCount()); + } + + @Test + public void noOpConnect() { + final int[] calls = { 0 }; + Flowable<Integer> f = new ConnectableFlowable<Integer>() { + @Override + public void connect(Consumer<? super Disposable> connection) { + calls[0]++; + } + + @Override + public void reset() { + // nothing to do in this test + } + + @Override + protected void subscribeActual(Subscriber<? super Integer> subscriber) { + subscriber.onSubscribe(new BooleanSubscription()); + } + }.refCount(); + + f.test(); + f.test(); + + assertEquals(1, calls[0]); + } + + Flowable<Object> source; + + @Test + public void replayNoLeak() throws Exception { + Thread.sleep(100); + System.gc(); + Thread.sleep(100); + + long start = ManagementFactory.getMemoryMXBean().getHeapMemoryUsage().getUsed(); + + source = Flowable.fromCallable(new Callable<Object>() { + @Override + public Object call() throws Exception { + return new byte[100 * 1000 * 1000]; + } + }) + .replay(1) + .refCount(); + + source.subscribe(); + + long after = TestHelper.awaitGC(GC_SLEEP_TIME, 20, start + 20 * 1000 * 1000); + + source = null; + assertTrue(String.format("%,3d -> %,3d%n", start, after), start + 20 * 1000 * 1000 > after); + } + + @Test + public void replayNoLeak2() throws Exception { + Thread.sleep(100); + System.gc(); + Thread.sleep(100); + + long start = ManagementFactory.getMemoryMXBean().getHeapMemoryUsage().getUsed(); + + source = Flowable.fromCallable(new Callable<Object>() { + @Override + public Object call() throws Exception { + return new byte[100 * 1000 * 1000]; + } + }).concatWith(Flowable.never()) + .replay(1) + .refCount(); + + Disposable d1 = source.subscribe(); + Disposable d2 = source.subscribe(); + + d1.dispose(); + d2.dispose(); + + d1 = null; + d2 = null; + + long after = TestHelper.awaitGC(GC_SLEEP_TIME, 20, start + 20 * 1000 * 1000); + + source = null; + assertTrue(String.format("%,3d -> %,3d%n", start, after), start + 20 * 1000 * 1000 > after); + } + + static final class ExceptionData extends Exception { + private static final long serialVersionUID = -6763898015338136119L; + + public final Object data; + + ExceptionData(Object data) { + this.data = data; + } + } + + static final int GC_SLEEP_TIME = 250; + + @Test + public void publishNoLeak() throws Exception { + System.gc(); + Thread.sleep(GC_SLEEP_TIME); + + source = Flowable.fromCallable(new Callable<Object>() { + @Override + public Object call() throws Exception { + throw new ExceptionData(new byte[100 * 1000 * 1000]); + } + }) + .publish() + .refCount(); + + long start = ManagementFactory.getMemoryMXBean().getHeapMemoryUsage().getUsed(); + + source.subscribe(Functions.emptyConsumer(), Functions.emptyConsumer()); + + long after = TestHelper.awaitGC(GC_SLEEP_TIME, 20, start + 20 * 1000 * 1000); + + source = null; + + assertTrue(String.format("%,3d -> %,3d%n", start, after), start + 20 * 1000 * 1000 > after); + } + + @Test + public void publishNoLeak2() throws Exception { + System.gc(); + Thread.sleep(GC_SLEEP_TIME); + + long start = ManagementFactory.getMemoryMXBean().getHeapMemoryUsage().getUsed(); + + source = Flowable.fromCallable(new Callable<Object>() { + @Override + public Object call() throws Exception { + return new byte[100 * 1000 * 1000]; + } + }).concatWith(Flowable.never()) + .publish() + .refCount(); + + TestSubscriber<Object> d1 = source.test(); + TestSubscriber<Object> d2 = source.test(); + + d1.cancel(); + d2.cancel(); + + d1 = null; + d2 = null; + + long after = TestHelper.awaitGC(GC_SLEEP_TIME, 20, start + 20 * 1000 * 1000); + + source = null; + assertTrue(String.format("%,3d -> %,3d%n", start, after), start + 20 * 1000 * 1000 > after); + } + + @Test + public void replayIsUnsubscribed() { + ConnectableFlowable<Integer> cf = Flowable.just(1) + .replay(); + + if (cf instanceof Disposable) { + assertTrue(((Disposable)cf).isDisposed()); + + Disposable connection = cf.connect(); + + assertFalse(((Disposable)cf).isDisposed()); + + connection.dispose(); + + assertTrue(((Disposable)cf).isDisposed()); + } + } + + static final class BadFlowableSubscribe extends ConnectableFlowable<Object> { + + @Override + public void connect(Consumer<? super Disposable> connection) { + try { + connection.accept(Disposable.empty()); + } catch (Throwable ex) { + throw ExceptionHelper.wrapOrThrow(ex); + } + } + + @Override + public void reset() { + // nothing to do in this test + } + + @Override + protected void subscribeActual(Subscriber<? super Object> subscriber) { + throw new TestException("subscribeActual"); + } + } + + static final class BadFlowableDispose extends ConnectableFlowable<Object> { + + @Override + public void connect(Consumer<? super Disposable> connection) { + try { + connection.accept(Disposable.empty()); + } catch (Throwable ex) { + throw ExceptionHelper.wrapOrThrow(ex); + } + } + + @Override + public void reset() { + throw new TestException("dispose"); + } + + @Override + protected void subscribeActual(Subscriber<? super Object> subscriber) { + subscriber.onSubscribe(new BooleanSubscription()); + } + } + + static final class BadFlowableConnect extends ConnectableFlowable<Object> { + + @Override + public void connect(Consumer<? super Disposable> connection) { + throw new TestException("connect"); + } + + @Override + public void reset() { + // nothing to do in this test + } + + @Override + protected void subscribeActual(Subscriber<? super Object> subscriber) { + subscriber.onSubscribe(new BooleanSubscription()); + } + } + + @Test + public void badSourceSubscribe() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + BadFlowableSubscribe bo = new BadFlowableSubscribe(); + + try { + bo.refCount() + .test(); + fail("Should have thrown"); + } catch (NullPointerException ex) { + assertTrue(ex.getCause() instanceof TestException); + } + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void badSourceDispose() { + BadFlowableDispose bf = new BadFlowableDispose(); + + try { + bf.refCount() + .test() + .cancel(); + fail("Should have thrown"); + } catch (TestException expected) { + } + } + + @Test + public void badSourceConnect() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + BadFlowableConnect bf = new BadFlowableConnect(); + + try { + bf.refCount() + .test(); + fail("Should have thrown"); + } catch (NullPointerException ex) { + assertTrue(ex.getCause() instanceof TestException); + } + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + static final class BadFlowableSubscribe2 extends ConnectableFlowable<Object> { + + int count; + + @Override + public void connect(Consumer<? super Disposable> connection) { + try { + connection.accept(Disposable.empty()); + } catch (Throwable ex) { + throw ExceptionHelper.wrapOrThrow(ex); + } + } + + @Override + public void reset() { + // nothing to do in this test + } + + @Override + protected void subscribeActual(Subscriber<? super Object> subscriber) { + if (++count == 1) { + subscriber.onSubscribe(new BooleanSubscription()); + } else { + throw new TestException("subscribeActual"); + } + } + } + + @Test + public void badSourceSubscribe2() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + BadFlowableSubscribe2 bf = new BadFlowableSubscribe2(); + + Flowable<Object> f = bf.refCount(); + f.test(); + try { + f.test(); + fail("Should have thrown"); + } catch (NullPointerException ex) { + assertTrue(ex.getCause() instanceof TestException); + } + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + static final class BadFlowableConnect2 extends ConnectableFlowable<Object> { + + @Override + public void connect(Consumer<? super Disposable> connection) { + try { + connection.accept(Disposable.empty()); + } catch (Throwable ex) { + throw ExceptionHelper.wrapOrThrow(ex); + } + } + + @Override + public void reset() { + throw new TestException("dispose"); + } + + @Override + protected void subscribeActual(Subscriber<? super Object> subscriber) { + subscriber.onSubscribe(new BooleanSubscription()); + subscriber.onComplete(); + } + } + + @Test + public void badSourceCompleteDisconnect() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + BadFlowableConnect2 bf = new BadFlowableConnect2(); + + try { + bf.refCount() + .test(); + fail("Should have thrown"); + } catch (NullPointerException ex) { + assertTrue(ex.getCause() instanceof TestException); + } + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void blockingSourceAsnycCancel() throws Exception { + BehaviorProcessor<Integer> bp = BehaviorProcessor.createDefault(1); + + Flowable<Integer> f = bp + .replay(1) + .refCount(); + + f.subscribe(); + + final AtomicBoolean interrupted = new AtomicBoolean(); + + f.switchMap(new Function<Integer, Publisher<? extends Object>>() { + @Override + public Publisher<? extends Object> apply(Integer v) throws Exception { + return Flowable.create(new FlowableOnSubscribe<Object>() { + @Override + public void subscribe(FlowableEmitter<Object> emitter) throws Exception { + while (!emitter.isCancelled()) { + Thread.sleep(100); + } + interrupted.set(true); + } + }, BackpressureStrategy.MISSING); + } + }) + .takeUntil(Flowable.timer(500, TimeUnit.MILLISECONDS)) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(); + + assertTrue(interrupted.get()); + } + + @Test + public void byCount() { + final int[] subscriptions = { 0 }; + + Flowable<Integer> source = Flowable.range(1, 5) + .doOnSubscribe(new Consumer<Subscription>() { + @Override + public void accept(Subscription s) throws Exception { + subscriptions[0]++; + } + }) + .publish() + .refCount(2); + + for (int i = 0; i < 3; i++) { + TestSubscriber<Integer> ts1 = source.test(); + + ts1.assertEmpty(); + + TestSubscriber<Integer> ts2 = source.test(); + + ts1.assertResult(1, 2, 3, 4, 5); + ts2.assertResult(1, 2, 3, 4, 5); + } + + assertEquals(3, subscriptions[0]); + } + + @Test + public void resubscribeBeforeTimeout() throws Exception { + final int[] subscriptions = { 0 }; + + PublishProcessor<Integer> pp = PublishProcessor.create(); + + Flowable<Integer> source = pp + .doOnSubscribe(new Consumer<Subscription>() { + @Override + public void accept(Subscription s) throws Exception { + subscriptions[0]++; + } + }) + .publish() + .refCount(500, TimeUnit.MILLISECONDS); + + TestSubscriber<Integer> ts1 = source.test(0); + + assertEquals(1, subscriptions[0]); + + ts1.cancel(); + + Thread.sleep(100); + + ts1 = source.test(0); + + assertEquals(1, subscriptions[0]); + + Thread.sleep(500); + + assertEquals(1, subscriptions[0]); + + pp.onNext(1); + pp.onNext(2); + pp.onNext(3); + pp.onNext(4); + pp.onNext(5); + pp.onComplete(); + + ts1.requestMore(5) + .assertResult(1, 2, 3, 4, 5); + } + + @Test + public void letitTimeout() throws Exception { + final int[] subscriptions = { 0 }; + + PublishProcessor<Integer> pp = PublishProcessor.create(); + + Flowable<Integer> source = pp + .doOnSubscribe(new Consumer<Subscription>() { + @Override + public void accept(Subscription s) throws Exception { + subscriptions[0]++; + } + }) + .publish() + .refCount(1, 100, TimeUnit.MILLISECONDS); + + TestSubscriber<Integer> ts1 = source.test(0); + + assertEquals(1, subscriptions[0]); + + ts1.cancel(); + + assertTrue(pp.hasSubscribers()); + + Thread.sleep(200); + + assertFalse(pp.hasSubscribers()); + } + + @Test + public void error() { + Flowable.<Integer>error(new IOException()) + .publish() + .refCount(500, TimeUnit.MILLISECONDS) + .test() + .assertFailure(IOException.class); + } + + @Test + public void comeAndGo() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + + Flowable<Integer> source = pp + .publish() + .refCount(1); + + TestSubscriber<Integer> ts1 = source.test(0); + + assertTrue(pp.hasSubscribers()); + + for (int i = 0; i < 3; i++) { + TestSubscriber<Integer> ts2 = source.test(); + ts1.cancel(); + ts1 = ts2; + } + + ts1.cancel(); + + assertFalse(pp.hasSubscribers()); + } + + @Test + public void unsubscribeSubscribeRace() { + for (int i = 0; i < 1000; i++) { + + final Flowable<Integer> source = Flowable.range(1, 5) + .replay() + .refCount(1) + ; + + final TestSubscriber<Integer> ts1 = source.test(0); + + final TestSubscriber<Integer> ts2 = new TestSubscriber<>(0); + + Runnable r1 = new Runnable() { + @Override + public void run() { + ts1.cancel(); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + source.subscribe(ts2); + } + }; + + TestHelper.race(r1, r2, Schedulers.single()); + + ts2.requestMore(6) // FIXME RxJava replay() doesn't issue onComplete without request + .withTag("Round: " + i) + .assertResult(1, 2, 3, 4, 5); + } + } + + static final class BadFlowableDoubleOnX extends ConnectableFlowable<Object> + implements Disposable { + + @Override + public void connect(Consumer<? super Disposable> connection) { + try { + connection.accept(Disposable.empty()); + } catch (Throwable ex) { + throw ExceptionHelper.wrapOrThrow(ex); + } + } + + @Override + public void reset() { + // nothing to do in this test + } + + @Override + protected void subscribeActual(Subscriber<? super Object> subscriber) { + subscriber.onSubscribe(new BooleanSubscription()); + subscriber.onSubscribe(new BooleanSubscription()); + subscriber.onComplete(); + subscriber.onComplete(); + subscriber.onError(new TestException()); + } + + @Override + public void dispose() { + } + + @Override + public boolean isDisposed() { + return false; + } + } + + @Test + public void doubleOnX() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + new BadFlowableDoubleOnX() + .refCount() + .test() + .assertResult(); + + TestHelper.assertError(errors, 0, ProtocolViolationException.class); + TestHelper.assertUndeliverable(errors, 1, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void doubleOnXCount() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + new BadFlowableDoubleOnX() + .refCount(1) + .test() + .assertResult(); + + TestHelper.assertError(errors, 0, ProtocolViolationException.class); + TestHelper.assertUndeliverable(errors, 1, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void doubleOnXTime() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + new BadFlowableDoubleOnX() + .refCount(5, TimeUnit.SECONDS, Schedulers.single()) + .test() + .assertResult(); + + TestHelper.assertError(errors, 0, ProtocolViolationException.class); + TestHelper.assertUndeliverable(errors, 1, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void cancelTerminateStateExclusion() { + FlowableRefCount<Object> o = (FlowableRefCount<Object>)PublishProcessor.create() + .publish() + .refCount(); + + o.cancel(null); + + RefConnection rc = new RefConnection(o); + o.connection = null; + rc.subscriberCount = 0; + o.timeout(rc); + + rc.subscriberCount = 1; + o.timeout(rc); + + o.connection = rc; + o.timeout(rc); + + rc.subscriberCount = 0; + o.timeout(rc); + + // ------------------- + + rc.subscriberCount = 2; + rc.connected = false; + o.connection = rc; + o.cancel(rc); + + rc.subscriberCount = 1; + rc.connected = false; + o.connection = rc; + o.cancel(rc); + + rc.subscriberCount = 2; + rc.connected = true; + o.connection = rc; + o.cancel(rc); + + rc.subscriberCount = 1; + rc.connected = true; + o.connection = rc; + rc.set(null); + o.cancel(rc); + + o.connection = rc; + o.cancel(new RefConnection(o)); + } + + @Test + public void replayRefCountShallBeThreadSafe() { + for (int i = 0; i < TestHelper.RACE_LONG_LOOPS; i++) { + Flowable<Integer> flowable = Flowable.just(1).replay(1).refCount(); + + TestSubscriber<Integer> ts1 = flowable + .subscribeOn(Schedulers.io()) + .test(); + + TestSubscriber<Integer> ts2 = flowable + .subscribeOn(Schedulers.io()) + .test(); + + ts1 + .withTag("" + i) + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(1); + + ts2 + .withTag("" + i) + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(1); + } + } + + static final class TestConnectableFlowable<T> extends ConnectableFlowable<T> { + + volatile boolean reset; + + @Override + public void reset() { + reset = true; + } + + @Override + public void connect(Consumer<? super Disposable> connection) { + // not relevant + } + + @Override + protected void subscribeActual(Subscriber<? super T> subscriber) { + // not relevant + } + } + + @Test + public void timeoutResetsSource() { + TestConnectableFlowable<Object> tcf = new TestConnectableFlowable<>(); + FlowableRefCount<Object> o = (FlowableRefCount<Object>)tcf.refCount(); + + RefConnection rc = new RefConnection(o); + rc.set(Disposable.empty()); + o.connection = rc; + + o.timeout(rc); + + assertTrue(tcf.reset); + } + + @Test + public void disconnectBeforeConnect() { + BehaviorProcessor<Integer> processor = BehaviorProcessor.create(); + + Flowable<Integer> flowable = processor + .replay(1) + .refCount(); + + flowable.takeUntil(Flowable.just(1)).test(); + + processor.onNext(2); + + flowable.take(1).test().assertResult(2); + } + + @Test + public void publishRefCountShallBeThreadSafe() { + for (int i = 0; i < TestHelper.RACE_LONG_LOOPS; i++) { + Flowable<Integer> flowable = Flowable.just(1).publish().refCount(); + + TestSubscriber<Integer> subscriber1 = flowable + .subscribeOn(Schedulers.io()) + .test(); + + TestSubscriber<Integer> subscriber2 = flowable + .subscribeOn(Schedulers.io()) + .test(); + + subscriber1 + .withTag("subscriber1 " + i) + .awaitDone(5, TimeUnit.SECONDS) + .assertNoErrors() + .assertComplete(); + + subscriber2 + .withTag("subscriber2 " + i) + .awaitDone(5, TimeUnit.SECONDS) + .assertNoErrors() + .assertComplete(); + } + } + + @Test + public void upstreamTerminationTriggersAnotherCancel() throws Exception { + ReplayProcessor<Integer> rp = ReplayProcessor.create(); + rp.onNext(1); + rp.onComplete(); + + Flowable<Integer> shared = rp.share(); + + shared + .buffer(shared.debounce(5, TimeUnit.SECONDS)) + .test() + .assertValueCount(2); + + shared + .buffer(shared.debounce(5, TimeUnit.SECONDS)) + .test() + .assertValueCount(2); + }} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableRepeatTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableRepeatTest.java new file mode 100644 index 0000000000..9086c32a87 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableRepeatTest.java @@ -0,0 +1,498 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import java.util.*; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.Test; +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.subscriptions.BooleanSubscription; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.processors.PublishProcessor; +import io.reactivex.rxjava3.schedulers.Schedulers; +import io.reactivex.rxjava3.subscribers.TestSubscriber; +import io.reactivex.rxjava3.testsupport.*; + +public class FlowableRepeatTest { + + @Test + public void repetition() { + int num = 10; + final AtomicInteger count = new AtomicInteger(); + int value = Flowable.unsafeCreate(new Publisher<Integer>() { + + @Override + public void subscribe(final Subscriber<? super Integer> subscriber) { + subscriber.onNext(count.incrementAndGet()); + subscriber.onComplete(); + } + }).repeat().subscribeOn(Schedulers.computation()) + .take(num).blockingLast(); + + assertEquals(num, value); + } + + @Test + public void repeatTake() { + Flowable<Integer> xs = Flowable.just(1, 2); + Object[] ys = xs.repeat().subscribeOn(Schedulers.newThread()).take(4).toList().blockingGet().toArray(); + assertArrayEquals(new Object[] { 1, 2, 1, 2 }, ys); + } + + @Test + public void noStackOverFlow() { + Flowable.just(1).repeat().subscribeOn(Schedulers.newThread()).take(100000).blockingLast(); + } + + @Test + public void repeatTakeWithSubscribeOn() throws InterruptedException { + + final AtomicInteger counter = new AtomicInteger(); + Flowable<Integer> oi = Flowable.unsafeCreate(new Publisher<Integer>() { + + @Override + public void subscribe(Subscriber<? super Integer> sub) { + sub.onSubscribe(new BooleanSubscription()); + counter.incrementAndGet(); + sub.onNext(1); + sub.onNext(2); + sub.onComplete(); + } + }).subscribeOn(Schedulers.newThread()); + + Object[] ys = oi.repeat().subscribeOn(Schedulers.newThread()).map(new Function<Integer, Integer>() { + + @Override + public Integer apply(Integer t1) { + try { + Thread.sleep(50); + } catch (InterruptedException e) { + e.printStackTrace(); + } + return t1; + } + + }).take(4).toList().blockingGet().toArray(); + + assertEquals(2, counter.get()); + assertArrayEquals(new Object[] { 1, 2, 1, 2 }, ys); + } + + @Test + public void repeatAndTake() { + Subscriber<Object> subscriber = TestHelper.mockSubscriber(); + + Flowable.just(1).repeat().take(10).subscribe(subscriber); + + verify(subscriber, times(10)).onNext(1); + verify(subscriber).onComplete(); + verify(subscriber, never()).onError(any(Throwable.class)); + } + + @Test + public void repeatLimited() { + Subscriber<Object> subscriber = TestHelper.mockSubscriber(); + + Flowable.just(1).repeat(10).subscribe(subscriber); + + verify(subscriber, times(10)).onNext(1); + verify(subscriber).onComplete(); + verify(subscriber, never()).onError(any(Throwable.class)); + } + + @Test + public void repeatError() { + Subscriber<Object> subscriber = TestHelper.mockSubscriber(); + + Flowable.error(new TestException()).repeat(10).subscribe(subscriber); + + verify(subscriber).onError(any(TestException.class)); + verify(subscriber, never()).onNext(any()); + verify(subscriber, never()).onComplete(); + + } + + @Test + public void repeatZero() { + Subscriber<Object> subscriber = TestHelper.mockSubscriber(); + + Flowable.just(1).repeat(0).subscribe(subscriber); + + verify(subscriber).onComplete(); + verify(subscriber, never()).onNext(any()); + verify(subscriber, never()).onError(any(Throwable.class)); + } + + @Test + public void repeatOne() { + Subscriber<Object> subscriber = TestHelper.mockSubscriber(); + + Flowable.just(1).repeat(1).subscribe(subscriber); + + verify(subscriber).onComplete(); + verify(subscriber, times(1)).onNext(any()); + verify(subscriber, never()).onError(any(Throwable.class)); + } + + /** Issue #2587. */ + @Test + public void repeatAndDistinctUnbounded() { + Flowable<Integer> src = Flowable.fromIterable(Arrays.asList(1, 2, 3, 4, 5)) + .take(3) + .repeat(3) + .distinct(); + + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(); + + src.subscribe(ts); + + ts.assertNoErrors(); + ts.assertTerminated(); + ts.assertValues(1, 2, 3); + } + + /** Issue #2844: wrong target of request. */ + @Test + public void repeatRetarget() { + final List<Integer> concatBase = new ArrayList<>(); + TestSubscriber<Integer> ts = new TestSubscriber<>(); + Flowable.just(1, 2) + .repeat(5) + .concatMap(new Function<Integer, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Integer x) { + System.out.println("testRepeatRetarget -> " + x); + concatBase.add(x); + return Flowable.<Integer>empty() + .delay(200, TimeUnit.MILLISECONDS); + } + }) + .subscribe(ts); + + ts.awaitDone(5, TimeUnit.SECONDS); + ts.assertNoErrors(); + ts.assertNoValues(); + + assertEquals(Arrays.asList(1, 2, 1, 2, 1, 2, 1, 2, 1, 2), concatBase); + } + + @Test + public void repeatScheduled() { + + TestSubscriber<Integer> ts = TestSubscriber.create(); + + Flowable.just(1).subscribeOn(Schedulers.computation()).repeat(5).subscribe(ts); + + ts.awaitDone(5, TimeUnit.SECONDS); + ts.assertValues(1, 1, 1, 1, 1); + ts.assertNoErrors(); + ts.assertComplete(); + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + @Test + public void repeatWhenDefaultScheduler() { + TestSubscriber<Integer> ts = TestSubscriber.create(); + + Flowable.just(1).repeatWhen((Function)new Function<Flowable, Flowable>() { + @Override + public Flowable apply(Flowable f) { + return f.take(2); + } + }).subscribe(ts); + + ts.assertValues(1, 1); + ts.assertNoErrors(); + ts.assertComplete(); + + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + @Test + public void repeatWhenTrampolineScheduler() { + TestSubscriber<Integer> ts = TestSubscriber.create(); + + Flowable.just(1).subscribeOn(Schedulers.trampoline()) + .repeatWhen((Function)new Function<Flowable, Flowable>() { + @Override + public Flowable apply(Flowable f) { + return f.take(2); + } + }).subscribe(ts); + + ts.assertValues(1, 1); + ts.assertNoErrors(); + ts.assertComplete(); + } + + @Test + public void repeatUntil() { + Flowable.just(1) + .repeatUntil(new BooleanSupplier() { + @Override + public boolean getAsBoolean() throws Exception { + return false; + } + }) + .take(5) + .test() + .assertResult(1, 1, 1, 1, 1); + } + + @Test + public void repeatUntilCancel() { + Flowable.just(1) + .repeatUntil(new BooleanSupplier() { + @Override + public boolean getAsBoolean() throws Exception { + return true; + } + }) + .test(2L, true) + .assertEmpty(); + } + + @Test + public void repeatLongPredicateInvalid() { + try { + Flowable.just(1).repeat(-99); + fail("Should have thrown"); + } catch (IllegalArgumentException ex) { + assertEquals("times >= 0 required but it was -99", ex.getMessage()); + } + } + + @Test + public void repeatUntilError() { + Flowable.error(new TestException()) + .repeatUntil(new BooleanSupplier() { + @Override + public boolean getAsBoolean() throws Exception { + return true; + } + }) + .test() + .assertFailure(TestException.class); + } + + @Test + public void repeatUntilFalse() { + Flowable.just(1) + .repeatUntil(new BooleanSupplier() { + @Override + public boolean getAsBoolean() throws Exception { + return true; + } + }) + .test() + .assertResult(1); + } + + @Test + public void repeatUntilSupplierCrash() { + Flowable.just(1) + .repeatUntil(new BooleanSupplier() { + @Override + public boolean getAsBoolean() throws Exception { + throw new TestException(); + } + }) + .test() + .assertFailure(TestException.class, 1); + } + + @Test + public void shouldDisposeInnerObservable() { + final PublishProcessor<Object> processor = PublishProcessor.create(); + final Disposable disposable = Flowable.just("Leak") + .repeatWhen(new Function<Flowable<Object>, Flowable<Object>>() { + @Override + public Flowable<Object> apply(Flowable<Object> completions) throws Exception { + return completions.switchMap(new Function<Object, Flowable<Object>>() { + @Override + public Flowable<Object> apply(Object ignore) throws Exception { + return processor; + } + }); + } + }) + .subscribe(); + + assertTrue(processor.hasSubscribers()); + disposable.dispose(); + assertFalse(processor.hasSubscribers()); + } + + @Test + public void repeatWhen() { + Flowable.error(new TestException()) + .repeatWhen(new Function<Flowable<Object>, Flowable<Object>>() { + @Override + public Flowable<Object> apply(Flowable<Object> v) throws Exception { + return v.delay(10, TimeUnit.SECONDS); + } + }) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertFailure(TestException.class); + } + + @Test + public void whenTake() { + Flowable.range(1, 3).repeatWhen(new Function<Flowable<Object>, Flowable<Object>>() { + @Override + public Flowable<Object> apply(Flowable<Object> handler) throws Exception { + return handler.take(2); + } + }) + .test() + .assertResult(1, 2, 3, 1, 2, 3); + } + + @Test + public void noCancelPreviousRepeat() { + final AtomicInteger counter = new AtomicInteger(); + + Flowable<Integer> source = Flowable.just(1).doOnCancel(new Action() { + @Override + public void run() throws Exception { + counter.getAndIncrement(); + } + }); + + source.repeat(5) + .test() + .assertResult(1, 1, 1, 1, 1); + + assertEquals(0, counter.get()); + } + + @Test + public void noCancelPreviousRepeatUntil() { + final AtomicInteger counter = new AtomicInteger(); + + Flowable<Integer> source = Flowable.just(1).doOnCancel(new Action() { + @Override + public void run() throws Exception { + counter.getAndIncrement(); + } + }); + + final AtomicInteger times = new AtomicInteger(); + + source.repeatUntil(new BooleanSupplier() { + @Override + public boolean getAsBoolean() throws Exception { + return times.getAndIncrement() == 4; + } + }) + .test() + .assertResult(1, 1, 1, 1, 1); + + assertEquals(0, counter.get()); + } + + @Test + public void noCancelPreviousRepeatWhen() { + final AtomicInteger counter = new AtomicInteger(); + + Flowable<Integer> source = Flowable.just(1).doOnCancel(new Action() { + @Override + public void run() throws Exception { + counter.getAndIncrement(); + } + }); + + final AtomicInteger times = new AtomicInteger(); + + source.repeatWhen(new Function<Flowable<Object>, Flowable<?>>() { + @Override + public Flowable<?> apply(Flowable<Object> e) throws Exception { + return e.takeWhile(new Predicate<Object>() { + @Override + public boolean test(Object v) throws Exception { + return times.getAndIncrement() < 4; + } + }); + } + }) + .test() + .assertResult(1, 1, 1, 1, 1); + + assertEquals(0, counter.get()); + } + + @Test + public void repeatFloodNoSubscriptionError() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + + try { + final PublishProcessor<Integer> source = PublishProcessor.create(); + final PublishProcessor<Integer> signaller = PublishProcessor.create(); + + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + + TestSubscriber<Integer> ts = source.take(1) + .repeatWhen(new Function<Flowable<Object>, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Flowable<Object> v) + throws Exception { + return signaller; + } + }).test(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + source.onNext(1); + } + } + }; + Runnable r2 = new Runnable() { + @Override + public void run() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + signaller.offer(1); + } + } + }; + + TestHelper.race(r1, r2); + + ts.cancel(); + } + + if (!errors.isEmpty()) { + for (Throwable e : errors) { + e.printStackTrace(); + } + fail(errors + ""); + } + } finally { + RxJavaPlugins.reset(); + } + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableReplayEagerTruncateTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableReplayEagerTruncateTest.java new file mode 100644 index 0000000000..d2a4ba56da --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableReplayEagerTruncateTest.java @@ -0,0 +1,2346 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +import java.lang.management.*; +import java.util.*; +import java.util.concurrent.*; +import java.util.concurrent.atomic.*; + +import org.junit.*; +import org.mockito.InOrder; +import org.reactivestreams.*; + +import io.reactivex.rxjava3.annotations.NonNull; +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.core.Scheduler.Worker; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.flowables.ConnectableFlowable; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.internal.fuseable.HasUpstreamPublisher; +import io.reactivex.rxjava3.internal.operators.flowable.FlowableReplay.*; +import io.reactivex.rxjava3.internal.subscriptions.BooleanSubscription; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.processors.PublishProcessor; +import io.reactivex.rxjava3.schedulers.*; +import io.reactivex.rxjava3.subscribers.TestSubscriber; +import io.reactivex.rxjava3.testsupport.*; + +public class FlowableReplayEagerTruncateTest extends RxJavaTest { + @Test + public void bufferedReplay() { + PublishProcessor<Integer> source = PublishProcessor.create(); + + ConnectableFlowable<Integer> cf = source.replay(3, true); + cf.connect(); + + { + Subscriber<Object> subscriber1 = TestHelper.mockSubscriber(); + InOrder inOrder = inOrder(subscriber1); + + cf.subscribe(subscriber1); + + source.onNext(1); + source.onNext(2); + source.onNext(3); + + inOrder.verify(subscriber1, times(1)).onNext(1); + inOrder.verify(subscriber1, times(1)).onNext(2); + inOrder.verify(subscriber1, times(1)).onNext(3); + + source.onNext(4); + source.onComplete(); + inOrder.verify(subscriber1, times(1)).onNext(4); + inOrder.verify(subscriber1, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + verify(subscriber1, never()).onError(any(Throwable.class)); + + } + + { + Subscriber<Object> subscriber1 = TestHelper.mockSubscriber(); + InOrder inOrder = inOrder(subscriber1); + + cf.subscribe(subscriber1); + + inOrder.verify(subscriber1, times(1)).onNext(2); + inOrder.verify(subscriber1, times(1)).onNext(3); + inOrder.verify(subscriber1, times(1)).onNext(4); + inOrder.verify(subscriber1, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + verify(subscriber1, never()).onError(any(Throwable.class)); + } + } + + @Test + public void bufferedWindowReplay() { + PublishProcessor<Integer> source = PublishProcessor.create(); + TestScheduler scheduler = new TestScheduler(); + ConnectableFlowable<Integer> cf = source.replay(3, 100, TimeUnit.MILLISECONDS, scheduler, true); + cf.connect(); + + { + Subscriber<Object> subscriber1 = TestHelper.mockSubscriber(); + InOrder inOrder = inOrder(subscriber1); + + cf.subscribe(subscriber1); + + source.onNext(1); + scheduler.advanceTimeBy(10, TimeUnit.MILLISECONDS); + source.onNext(2); + scheduler.advanceTimeBy(10, TimeUnit.MILLISECONDS); + source.onNext(3); + scheduler.advanceTimeBy(10, TimeUnit.MILLISECONDS); + + inOrder.verify(subscriber1, times(1)).onNext(1); + inOrder.verify(subscriber1, times(1)).onNext(2); + inOrder.verify(subscriber1, times(1)).onNext(3); + + source.onNext(4); + source.onNext(5); + scheduler.advanceTimeBy(90, TimeUnit.MILLISECONDS); + + inOrder.verify(subscriber1, times(1)).onNext(4); + + inOrder.verify(subscriber1, times(1)).onNext(5); + + inOrder.verifyNoMoreInteractions(); + verify(subscriber1, never()).onError(any(Throwable.class)); + + } + + { + Subscriber<Object> subscriber1 = TestHelper.mockSubscriber(); + InOrder inOrder = inOrder(subscriber1); + + cf.subscribe(subscriber1); + + inOrder.verify(subscriber1, times(1)).onNext(4); + inOrder.verify(subscriber1, times(1)).onNext(5); + inOrder.verifyNoMoreInteractions(); + verify(subscriber1, never()).onError(any(Throwable.class)); + } + } + + @Test + public void windowedReplay() { + TestScheduler scheduler = new TestScheduler(); + + PublishProcessor<Integer> source = PublishProcessor.create(); + + ConnectableFlowable<Integer> cf = source.replay(100, TimeUnit.MILLISECONDS, scheduler, true); + cf.connect(); + + { + Subscriber<Object> subscriber1 = TestHelper.mockSubscriber(); + InOrder inOrder = inOrder(subscriber1); + + cf.subscribe(subscriber1); + + source.onNext(1); + scheduler.advanceTimeBy(60, TimeUnit.MILLISECONDS); + source.onNext(2); + scheduler.advanceTimeBy(60, TimeUnit.MILLISECONDS); + source.onNext(3); + scheduler.advanceTimeBy(60, TimeUnit.MILLISECONDS); + source.onComplete(); + scheduler.advanceTimeBy(60, TimeUnit.MILLISECONDS); + + inOrder.verify(subscriber1, times(1)).onNext(1); + inOrder.verify(subscriber1, times(1)).onNext(2); + inOrder.verify(subscriber1, times(1)).onNext(3); + + inOrder.verify(subscriber1, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + verify(subscriber1, never()).onError(any(Throwable.class)); + + } + { + Subscriber<Object> subscriber1 = TestHelper.mockSubscriber(); + InOrder inOrder = inOrder(subscriber1); + + cf.subscribe(subscriber1); + inOrder.verify(subscriber1, never()).onNext(3); + + inOrder.verify(subscriber1, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + verify(subscriber1, never()).onError(any(Throwable.class)); + } + } + + @Test + public void replaySelector() { + final Function<Integer, Integer> dbl = new Function<Integer, Integer>() { + + @Override + public Integer apply(Integer t1) { + return t1 * 2; + } + + }; + + Function<Flowable<Integer>, Flowable<Integer>> selector = new Function<Flowable<Integer>, Flowable<Integer>>() { + + @Override + public Flowable<Integer> apply(Flowable<Integer> t1) { + return t1.map(dbl); + } + + }; + + PublishProcessor<Integer> source = PublishProcessor.create(); + + Flowable<Integer> co = source.replay(selector); + + { + Subscriber<Object> subscriber1 = TestHelper.mockSubscriber(); + InOrder inOrder = inOrder(subscriber1); + + co.subscribe(subscriber1); + + source.onNext(1); + source.onNext(2); + source.onNext(3); + + inOrder.verify(subscriber1, times(1)).onNext(2); + inOrder.verify(subscriber1, times(1)).onNext(4); + inOrder.verify(subscriber1, times(1)).onNext(6); + + source.onNext(4); + source.onComplete(); + inOrder.verify(subscriber1, times(1)).onNext(8); + inOrder.verify(subscriber1, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + verify(subscriber1, never()).onError(any(Throwable.class)); + + } + + { + Subscriber<Object> subscriber1 = TestHelper.mockSubscriber(); + InOrder inOrder = inOrder(subscriber1); + + co.subscribe(subscriber1); + + inOrder.verify(subscriber1, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + verify(subscriber1, never()).onError(any(Throwable.class)); + + } + + } + + @Test + public void bufferedReplaySelector() { + + final Function<Integer, Integer> dbl = new Function<Integer, Integer>() { + + @Override + public Integer apply(Integer t1) { + return t1 * 2; + } + + }; + + Function<Flowable<Integer>, Flowable<Integer>> selector = new Function<Flowable<Integer>, Flowable<Integer>>() { + + @Override + public Flowable<Integer> apply(Flowable<Integer> t1) { + return t1.map(dbl); + } + + }; + + PublishProcessor<Integer> source = PublishProcessor.create(); + + Flowable<Integer> co = source.replay(selector, 3, true); + + { + Subscriber<Object> subscriber1 = TestHelper.mockSubscriber(); + InOrder inOrder = inOrder(subscriber1); + + co.subscribe(subscriber1); + + source.onNext(1); + source.onNext(2); + source.onNext(3); + + inOrder.verify(subscriber1, times(1)).onNext(2); + inOrder.verify(subscriber1, times(1)).onNext(4); + inOrder.verify(subscriber1, times(1)).onNext(6); + + source.onNext(4); + source.onComplete(); + inOrder.verify(subscriber1, times(1)).onNext(8); + inOrder.verify(subscriber1, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + verify(subscriber1, never()).onError(any(Throwable.class)); + + } + + { + Subscriber<Object> subscriber1 = TestHelper.mockSubscriber(); + InOrder inOrder = inOrder(subscriber1); + + co.subscribe(subscriber1); + + inOrder.verify(subscriber1, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + verify(subscriber1, never()).onError(any(Throwable.class)); + } + } + + @Test + public void windowedReplaySelector() { + + final Function<Integer, Integer> dbl = new Function<Integer, Integer>() { + + @Override + public Integer apply(Integer t1) { + return t1 * 2; + } + + }; + + Function<Flowable<Integer>, Flowable<Integer>> selector = new Function<Flowable<Integer>, Flowable<Integer>>() { + + @Override + public Flowable<Integer> apply(Flowable<Integer> t1) { + return t1.map(dbl); + } + + }; + + TestScheduler scheduler = new TestScheduler(); + + PublishProcessor<Integer> source = PublishProcessor.create(); + + Flowable<Integer> co = source.replay(selector, 100, TimeUnit.MILLISECONDS, scheduler, true); + + { + Subscriber<Object> subscriber1 = TestHelper.mockSubscriber(); + InOrder inOrder = inOrder(subscriber1); + + co.subscribe(subscriber1); + + source.onNext(1); + scheduler.advanceTimeBy(60, TimeUnit.MILLISECONDS); + source.onNext(2); + scheduler.advanceTimeBy(60, TimeUnit.MILLISECONDS); + source.onNext(3); + scheduler.advanceTimeBy(60, TimeUnit.MILLISECONDS); + source.onComplete(); + scheduler.advanceTimeBy(60, TimeUnit.MILLISECONDS); + + inOrder.verify(subscriber1, times(1)).onNext(2); + inOrder.verify(subscriber1, times(1)).onNext(4); + inOrder.verify(subscriber1, times(1)).onNext(6); + + inOrder.verify(subscriber1, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + verify(subscriber1, never()).onError(any(Throwable.class)); + + } + { + Subscriber<Object> subscriber1 = TestHelper.mockSubscriber(); + InOrder inOrder = inOrder(subscriber1); + + co.subscribe(subscriber1); + + inOrder.verify(subscriber1, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + verify(subscriber1, never()).onError(any(Throwable.class)); + } + } + + @Test + public void bufferedReplayError() { + PublishProcessor<Integer> source = PublishProcessor.create(); + + ConnectableFlowable<Integer> cf = source.replay(3, true); + cf.connect(); + + { + Subscriber<Object> subscriber1 = TestHelper.mockSubscriber(); + InOrder inOrder = inOrder(subscriber1); + + cf.subscribe(subscriber1); + + source.onNext(1); + source.onNext(2); + source.onNext(3); + + inOrder.verify(subscriber1, times(1)).onNext(1); + inOrder.verify(subscriber1, times(1)).onNext(2); + inOrder.verify(subscriber1, times(1)).onNext(3); + + source.onNext(4); + source.onError(new RuntimeException("Forced failure")); + + inOrder.verify(subscriber1, times(1)).onNext(4); + inOrder.verify(subscriber1, times(1)).onError(any(RuntimeException.class)); + inOrder.verifyNoMoreInteractions(); + verify(subscriber1, never()).onComplete(); + + } + + { + Subscriber<Object> subscriber1 = TestHelper.mockSubscriber(); + InOrder inOrder = inOrder(subscriber1); + + cf.subscribe(subscriber1); + + inOrder.verify(subscriber1, times(1)).onNext(2); + inOrder.verify(subscriber1, times(1)).onNext(3); + inOrder.verify(subscriber1, times(1)).onNext(4); + inOrder.verify(subscriber1, times(1)).onError(any(RuntimeException.class)); + inOrder.verifyNoMoreInteractions(); + verify(subscriber1, never()).onComplete(); + } + } + + @Test + public void windowedReplayError() { + TestScheduler scheduler = new TestScheduler(); + + PublishProcessor<Integer> source = PublishProcessor.create(); + + ConnectableFlowable<Integer> cf = source.replay(100, TimeUnit.MILLISECONDS, scheduler, true); + cf.connect(); + + { + Subscriber<Object> subscriber1 = TestHelper.mockSubscriber(); + InOrder inOrder = inOrder(subscriber1); + + cf.subscribe(subscriber1); + + source.onNext(1); + scheduler.advanceTimeBy(60, TimeUnit.MILLISECONDS); + source.onNext(2); + scheduler.advanceTimeBy(60, TimeUnit.MILLISECONDS); + source.onNext(3); + scheduler.advanceTimeBy(60, TimeUnit.MILLISECONDS); + source.onError(new RuntimeException("Forced failure")); + scheduler.advanceTimeBy(60, TimeUnit.MILLISECONDS); + + inOrder.verify(subscriber1, times(1)).onNext(1); + inOrder.verify(subscriber1, times(1)).onNext(2); + inOrder.verify(subscriber1, times(1)).onNext(3); + + inOrder.verify(subscriber1, times(1)).onError(any(RuntimeException.class)); + inOrder.verifyNoMoreInteractions(); + verify(subscriber1, never()).onComplete(); + + } + { + Subscriber<Object> subscriber1 = TestHelper.mockSubscriber(); + InOrder inOrder = inOrder(subscriber1); + + cf.subscribe(subscriber1); + inOrder.verify(subscriber1, never()).onNext(3); + + inOrder.verify(subscriber1, times(1)).onError(any(RuntimeException.class)); + inOrder.verifyNoMoreInteractions(); + verify(subscriber1, never()).onComplete(); + } + } + + @Test + public void synchronousDisconnect() { + final AtomicInteger effectCounter = new AtomicInteger(); + Flowable<Integer> source = Flowable.just(1, 2, 3, 4) + .doOnNext(new Consumer<Integer>() { + @Override + public void accept(Integer v) { + effectCounter.incrementAndGet(); + System.out.println("Sideeffect #" + v); + } + }); + + Flowable<Integer> result = source.replay( + new Function<Flowable<Integer>, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Flowable<Integer> f) { + return f.take(2); + } + }); + + for (int i = 1; i < 3; i++) { + effectCounter.set(0); + System.out.printf("- %d -%n", i); + result.subscribe(new Consumer<Integer>() { + + @Override + public void accept(Integer t1) { + System.out.println(t1); + } + + }, new Consumer<Throwable>() { + + @Override + public void accept(Throwable t1) { + t1.printStackTrace(); + } + }, + new Action() { + @Override + public void run() { + System.out.println("Done"); + } + }); + assertEquals(2, effectCounter.get()); + } + } + + /* + * test the basic expectation of OperatorMulticast via replay + */ + @SuppressWarnings("unchecked") + @Test + public void issue2191_UnsubscribeSource() throws Throwable { + // setup mocks + Consumer<Integer> sourceNext = mock(Consumer.class); + Action sourceCompleted = mock(Action.class); + Action sourceUnsubscribed = mock(Action.class); + Subscriber<Integer> spiedSubscriberBeforeConnect = TestHelper.mockSubscriber(); + Subscriber<Integer> spiedSubscriberAfterConnect = TestHelper.mockSubscriber(); + + // Flowable under test + Flowable<Integer> source = Flowable.just(1, 2); + + ConnectableFlowable<Integer> replay = source + .doOnNext(sourceNext) + .doOnCancel(sourceUnsubscribed) + .doOnComplete(sourceCompleted) + .replay(); + + replay.subscribe(spiedSubscriberBeforeConnect); + replay.subscribe(spiedSubscriberBeforeConnect); + replay.connect(); + replay.subscribe(spiedSubscriberAfterConnect); + replay.subscribe(spiedSubscriberAfterConnect); + + verify(spiedSubscriberBeforeConnect, times(2)).onSubscribe((Subscription)any()); + verify(spiedSubscriberAfterConnect, times(2)).onSubscribe((Subscription)any()); + + // verify interactions + verify(sourceNext, times(1)).accept(1); + verify(sourceNext, times(1)).accept(2); + verify(sourceCompleted, times(1)).run(); + verifyObserverMock(spiedSubscriberBeforeConnect, 2, 4); + verifyObserverMock(spiedSubscriberAfterConnect, 2, 4); + + verify(sourceUnsubscribed, never()).run(); + + verifyNoMoreInteractions(sourceNext); + verifyNoMoreInteractions(sourceCompleted); + verifyNoMoreInteractions(sourceUnsubscribed); + verifyNoMoreInteractions(spiedSubscriberBeforeConnect); + verifyNoMoreInteractions(spiedSubscriberAfterConnect); + + } + + /** + * Specifically test interaction with a Scheduler with subscribeOn. + * + * @throws Throwable functional interfaces declare throws Exception + */ + @SuppressWarnings("unchecked") + @Test + public void issue2191_SchedulerUnsubscribe() throws Throwable { + // setup mocks + Consumer<Integer> sourceNext = mock(Consumer.class); + Action sourceCompleted = mock(Action.class); + Action sourceUnsubscribed = mock(Action.class); + final Scheduler mockScheduler = mock(Scheduler.class); + final Disposable mockSubscription = mock(Disposable.class); + Worker spiedWorker = workerSpy(mockSubscription); + Subscriber<Integer> mockObserverBeforeConnect = TestHelper.mockSubscriber(); + Subscriber<Integer> mockObserverAfterConnect = TestHelper.mockSubscriber(); + + when(mockScheduler.createWorker()).thenReturn(spiedWorker); + + // Flowable under test + ConnectableFlowable<Integer> replay = Flowable.just(1, 2, 3) + .doOnNext(sourceNext) + .doOnCancel(sourceUnsubscribed) + .doOnComplete(sourceCompleted) + .subscribeOn(mockScheduler).replay(); + + replay.subscribe(mockObserverBeforeConnect); + replay.subscribe(mockObserverBeforeConnect); + replay.connect(); + replay.subscribe(mockObserverAfterConnect); + replay.subscribe(mockObserverAfterConnect); + + verify(mockObserverBeforeConnect, times(2)).onSubscribe((Subscription)any()); + verify(mockObserverAfterConnect, times(2)).onSubscribe((Subscription)any()); + + // verify interactions + verify(sourceNext, times(1)).accept(1); + verify(sourceNext, times(1)).accept(2); + verify(sourceNext, times(1)).accept(3); + verify(sourceCompleted, times(1)).run(); + verify(mockScheduler, times(1)).createWorker(); + verify(spiedWorker, times(1)).schedule((Runnable)notNull()); + verifyObserverMock(mockObserverBeforeConnect, 2, 6); + verifyObserverMock(mockObserverAfterConnect, 2, 6); + + // FIXME publish calls cancel too + verify(spiedWorker, times(1)).dispose(); + verify(sourceUnsubscribed, never()).run(); + + verifyNoMoreInteractions(sourceNext); + verifyNoMoreInteractions(sourceCompleted); + verifyNoMoreInteractions(sourceUnsubscribed); + verifyNoMoreInteractions(spiedWorker); + verifyNoMoreInteractions(mockSubscription); + verifyNoMoreInteractions(mockScheduler); + verifyNoMoreInteractions(mockObserverBeforeConnect); + verifyNoMoreInteractions(mockObserverAfterConnect); + } + + /** + * Specifically test interaction with a Scheduler with subscribeOn. + * + * @throws Throwable functional interfaces declare throws Exception + */ + @SuppressWarnings("unchecked") + @Test + public void issue2191_SchedulerUnsubscribeOnError() throws Throwable { + // setup mocks + Consumer<Integer> sourceNext = mock(Consumer.class); + Action sourceCompleted = mock(Action.class); + Consumer<Throwable> sourceError = mock(Consumer.class); + Action sourceUnsubscribed = mock(Action.class); + final Scheduler mockScheduler = mock(Scheduler.class); + final Disposable mockSubscription = mock(Disposable.class); + Worker spiedWorker = workerSpy(mockSubscription); + Subscriber<Integer> mockObserverBeforeConnect = TestHelper.mockSubscriber(); + Subscriber<Integer> mockObserverAfterConnect = TestHelper.mockSubscriber(); + + when(mockScheduler.createWorker()).thenReturn(spiedWorker); + + // Flowable under test + Function<Integer, Integer> mockFunc = mock(Function.class); + IllegalArgumentException illegalArgumentException = new IllegalArgumentException(); + when(mockFunc.apply(1)).thenReturn(1); + when(mockFunc.apply(2)).thenThrow(illegalArgumentException); + ConnectableFlowable<Integer> replay = Flowable.just(1, 2, 3).map(mockFunc) + .doOnNext(sourceNext) + .doOnCancel(sourceUnsubscribed) + .doOnComplete(sourceCompleted) + .doOnError(sourceError) + .subscribeOn(mockScheduler).replay(); + + replay.subscribe(mockObserverBeforeConnect); + replay.subscribe(mockObserverBeforeConnect); + replay.connect(); + replay.subscribe(mockObserverAfterConnect); + replay.subscribe(mockObserverAfterConnect); + + verify(mockObserverBeforeConnect, times(2)).onSubscribe((Subscription)any()); + verify(mockObserverAfterConnect, times(2)).onSubscribe((Subscription)any()); + + // verify interactions + verify(mockScheduler, times(1)).createWorker(); + verify(spiedWorker, times(1)).schedule((Runnable)notNull()); + verify(sourceNext, times(1)).accept(1); + verify(sourceError, times(1)).accept(illegalArgumentException); + verifyObserver(mockObserverBeforeConnect, 2, 2, illegalArgumentException); + verifyObserver(mockObserverAfterConnect, 2, 2, illegalArgumentException); + + // FIXME publish also calls cancel + verify(spiedWorker, times(1)).dispose(); + verify(sourceUnsubscribed, never()).run(); + + verifyNoMoreInteractions(sourceNext); + verifyNoMoreInteractions(sourceCompleted); + verifyNoMoreInteractions(sourceError); + verifyNoMoreInteractions(sourceUnsubscribed); + verifyNoMoreInteractions(spiedWorker); + verifyNoMoreInteractions(mockSubscription); + verifyNoMoreInteractions(mockScheduler); + verifyNoMoreInteractions(mockObserverBeforeConnect); + verifyNoMoreInteractions(mockObserverAfterConnect); + } + + private static void verifyObserverMock(Subscriber<Integer> mock, int numSubscriptions, int numItemsExpected) { + verify(mock, times(numItemsExpected)).onNext((Integer) notNull()); + verify(mock, times(numSubscriptions)).onComplete(); + verifyNoMoreInteractions(mock); + } + + private static void verifyObserver(Subscriber<Integer> mock, int numSubscriptions, int numItemsExpected, Throwable error) { + verify(mock, times(numItemsExpected)).onNext((Integer) notNull()); + verify(mock, times(numSubscriptions)).onError(error); + verifyNoMoreInteractions(mock); + } + + public static Worker workerSpy(final Disposable mockDisposable) { + return spy(new InprocessWorker(mockDisposable)); + } + + private static class InprocessWorker extends Worker { + private final Disposable mockDisposable; + public boolean unsubscribed; + + InprocessWorker(Disposable mockDisposable) { + this.mockDisposable = mockDisposable; + } + + @NonNull + @Override + public Disposable schedule(@NonNull Runnable action) { + action.run(); + return mockDisposable; // this subscription is returned but discarded + } + + @NonNull + @Override + public Disposable schedule(@NonNull Runnable action, long delayTime, @NonNull TimeUnit unit) { + action.run(); + return mockDisposable; + } + + @Override + public void dispose() { + unsubscribed = true; + } + + @Override + public boolean isDisposed() { + return unsubscribed; + } + } + + @Test + public void boundedReplayBuffer() { + BoundedReplayBuffer<Integer> buf = new BoundedReplayBuffer<Integer>(true) { + private static final long serialVersionUID = -9081211580719235896L; + + @Override + void truncate() { + } + }; + + buf.addLast(new Node(1, 0)); + buf.addLast(new Node(2, 1)); + buf.addLast(new Node(3, 2)); + buf.addLast(new Node(4, 3)); + buf.addLast(new Node(5, 4)); + + List<Integer> values = new ArrayList<>(); + buf.collect(values); + + Assert.assertEquals(Arrays.asList(1, 2, 3, 4, 5), values); + + buf.removeSome(2); + buf.removeFirst(); + buf.removeSome(2); + + values.clear(); + buf.collect(values); + Assert.assertTrue(values.isEmpty()); + + buf.addLast(new Node(5, 5)); + buf.addLast(new Node(6, 6)); + buf.collect(values); + + Assert.assertEquals(Arrays.asList(5, 6), values); + } + + @Test + public void timedAndSizedTruncation() { + TestScheduler test = new TestScheduler(); + SizeAndTimeBoundReplayBuffer<Integer> buf = new SizeAndTimeBoundReplayBuffer<>(2, 2000, TimeUnit.MILLISECONDS, test, true); + List<Integer> values = new ArrayList<>(); + + buf.next(1); + test.advanceTimeBy(1, TimeUnit.SECONDS); + buf.next(2); + test.advanceTimeBy(1, TimeUnit.SECONDS); + buf.collect(values); + Assert.assertEquals(Arrays.asList(2), values); + + buf.next(3); + buf.next(4); + values.clear(); + buf.collect(values); + Assert.assertEquals(Arrays.asList(3, 4), values); + + test.advanceTimeBy(2, TimeUnit.SECONDS); + buf.next(5); + + values.clear(); + buf.collect(values); + Assert.assertEquals(Arrays.asList(5), values); + + test.advanceTimeBy(2, TimeUnit.SECONDS); + buf.complete(); + + values.clear(); + buf.collect(values); + Assert.assertTrue(values.isEmpty()); + + Assert.assertEquals(1, buf.size); + Assert.assertTrue(buf.hasCompleted()); + } + + @Test + public void backpressure() { + final AtomicLong requested = new AtomicLong(); + Flowable<Integer> source = Flowable.range(1, 1000) + .doOnRequest(new LongConsumer() { + @Override + public void accept(long t) { + requested.addAndGet(t); + } + }); + ConnectableFlowable<Integer> cf = source.replay(); + + TestSubscriberEx<Integer> ts1 = new TestSubscriberEx<>(10L); + TestSubscriberEx<Integer> ts2 = new TestSubscriberEx<>(90L); + + cf.subscribe(ts1); + cf.subscribe(ts2); + + ts2.request(10); + + cf.connect(); + + ts1.assertValueCount(10); + ts1.assertNotTerminated(); + + ts2.assertValueCount(100); + ts2.assertNotTerminated(); + + Assert.assertEquals(100, requested.get()); + } + + @Test + public void backpressureBounded() { + final AtomicLong requested = new AtomicLong(); + Flowable<Integer> source = Flowable.range(1, 1000) + .doOnRequest(new LongConsumer() { + @Override + public void accept(long t) { + requested.addAndGet(t); + } + }); + ConnectableFlowable<Integer> cf = source.replay(50, true); + + TestSubscriberEx<Integer> ts1 = new TestSubscriberEx<>(10L); + TestSubscriberEx<Integer> ts2 = new TestSubscriberEx<>(90L); + + cf.subscribe(ts1); + cf.subscribe(ts2); + + ts2.request(10); + + cf.connect(); + + ts1.assertValueCount(10); + ts1.assertNotTerminated(); + + ts2.assertValueCount(100); + ts2.assertNotTerminated(); + + Assert.assertEquals(100, requested.get()); + } + + @Test + public void coldReplayNoBackpressure() { + Flowable<Integer> source = Flowable.range(0, 1000).replay().autoConnect(); + + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(); + + source.subscribe(ts); + + ts.assertNoErrors(); + ts.assertTerminated(); + List<Integer> onNextEvents = ts.values(); + assertEquals(1000, onNextEvents.size()); + + for (int i = 0; i < 1000; i++) { + assertEquals((Integer)i, onNextEvents.get(i)); + } + } + + @Test + public void coldReplayBackpressure() { + Flowable<Integer> source = Flowable.range(0, 1000).replay().autoConnect(); + + TestSubscriber<Integer> ts = new TestSubscriber<>(0L); + ts.request(10); + + source.subscribe(ts); + + ts.assertNoErrors(); + ts.assertNotComplete(); + List<Integer> onNextEvents = ts.values(); + assertEquals(10, onNextEvents.size()); + + for (int i = 0; i < 10; i++) { + assertEquals((Integer)i, onNextEvents.get(i)); + } + + ts.cancel(); + } + + @Test + public void cache() throws InterruptedException { + final AtomicInteger counter = new AtomicInteger(); + Flowable<String> f = Flowable.unsafeCreate(new Publisher<String>() { + + @Override + public void subscribe(final Subscriber<? super String> subscriber) { + subscriber.onSubscribe(new BooleanSubscription()); + new Thread(new Runnable() { + + @Override + public void run() { + counter.incrementAndGet(); + System.out.println("published observable being executed"); + subscriber.onNext("one"); + subscriber.onComplete(); + } + }).start(); + } + }).replay().autoConnect(); + + // we then expect the following 2 subscriptions to get that same value + final CountDownLatch latch = new CountDownLatch(2); + + // subscribe once + f.subscribe(new Consumer<String>() { + + @Override + public void accept(String v) { + assertEquals("one", v); + System.out.println("v: " + v); + latch.countDown(); + } + }); + + // subscribe again + f.subscribe(new Consumer<String>() { + + @Override + public void accept(String v) { + assertEquals("one", v); + System.out.println("v: " + v); + latch.countDown(); + } + }); + + if (!latch.await(1000, TimeUnit.MILLISECONDS)) { + fail("subscriptions did not receive values"); + } + assertEquals(1, counter.get()); + } + + @Test + public void unsubscribeSource() throws Throwable { + Action unsubscribe = mock(Action.class); + Flowable<Integer> f = Flowable.just(1).doOnCancel(unsubscribe).replay().autoConnect(); + f.subscribe(); + f.subscribe(); + f.subscribe(); + verify(unsubscribe, never()).run(); + } + + @Test + public void take() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(); + + Flowable<Integer> cached = Flowable.range(1, 100).replay().autoConnect(); + cached.take(10).subscribe(ts); + + ts.assertNoErrors(); + ts.assertTerminated(); + ts.assertValues(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + } + + @Test + public void async() { + Flowable<Integer> source = Flowable.range(1, 10000); + for (int i = 0; i < 100; i++) { + TestSubscriberEx<Integer> ts1 = new TestSubscriberEx<>(); + + Flowable<Integer> cached = source.replay().autoConnect(); + + cached.observeOn(Schedulers.computation()).subscribe(ts1); + + ts1.awaitDone(2, TimeUnit.SECONDS); + ts1.assertNoErrors(); + ts1.assertTerminated(); + assertEquals(10000, ts1.values().size()); + + TestSubscriberEx<Integer> ts2 = new TestSubscriberEx<>(); + cached.observeOn(Schedulers.computation()).subscribe(ts2); + + ts2.awaitDone(2, TimeUnit.SECONDS); + ts2.assertNoErrors(); + ts2.assertTerminated(); + assertEquals(10000, ts2.values().size()); + } + } + + @Test + public void asyncComeAndGo() { + Flowable<Long> source = Flowable.interval(1, 1, TimeUnit.MILLISECONDS) + .take(1000) + .subscribeOn(Schedulers.io()); + Flowable<Long> cached = source.replay().autoConnect(); + + Flowable<Long> output = cached.observeOn(Schedulers.computation(), false, 1024); + + List<TestSubscriberEx<Long>> list = new ArrayList<>(100); + for (int i = 0; i < 100; i++) { + TestSubscriberEx<Long> ts = new TestSubscriberEx<>(); + list.add(ts); + output.skip(i * 10).take(10).subscribe(ts); + } + + List<Long> expected = new ArrayList<>(); + for (int i = 0; i < 10; i++) { + expected.add((long)(i - 10)); + } + int j = 0; + for (TestSubscriberEx<Long> ts : list) { + ts.awaitDone(3, TimeUnit.SECONDS); + ts.assertNoErrors(); + ts.assertTerminated(); + + for (int i = j * 10; i < j * 10 + 10; i++) { + expected.set(i - j * 10, (long)i); + } + + ts.assertValueSequence(expected); + + j++; + } + } + + @Test + public void noMissingBackpressureException() { + final int m = 4 * 1000 * 1000; + Flowable<Integer> firehose = Flowable.unsafeCreate(new Publisher<Integer>() { + @Override + public void subscribe(Subscriber<? super Integer> t) { + t.onSubscribe(new BooleanSubscription()); + for (int i = 0; i < m; i++) { + t.onNext(i); + } + t.onComplete(); + } + }); + + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(); + firehose.replay().autoConnect().observeOn(Schedulers.computation()).takeLast(100).subscribe(ts); + + ts.awaitDone(3, TimeUnit.SECONDS); + ts.assertNoErrors(); + ts.assertTerminated(); + + assertEquals(100, ts.values().size()); + } + + @Test + public void valuesAndThenError() { + Flowable<Integer> source = Flowable.range(1, 10) + .concatWith(Flowable.<Integer>error(new TestException())) + .replay().autoConnect(); + + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(); + source.subscribe(ts); + + ts.assertValues(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + ts.assertNotComplete(); + Assert.assertEquals(1, ts.errors().size()); + + TestSubscriberEx<Integer> ts2 = new TestSubscriberEx<>(); + source.subscribe(ts2); + + ts2.assertValues(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + ts2.assertNotComplete(); + Assert.assertEquals(1, ts2.errors().size()); + } + + @Test + public void unsafeChildThrows() { + final AtomicInteger count = new AtomicInteger(); + + Flowable<Integer> source = Flowable.range(1, 100) + .doOnNext(new Consumer<Integer>() { + @Override + public void accept(Integer t) { + count.getAndIncrement(); + } + }) + .replay().autoConnect(); + + TestSubscriber<Integer> ts = new TestSubscriber<Integer>() { + @Override + public void onNext(Integer t) { + throw new TestException(); + } + }; + + source.subscribe(ts); + + Assert.assertEquals(100, count.get()); + + ts.assertNoValues(); + ts.assertNotComplete(); + ts.assertError(TestException.class); + } + + @Test + public void unboundedLeavesEarly() { + PublishProcessor<Integer> source = PublishProcessor.create(); + + final List<Long> requests = new ArrayList<>(); + + Flowable<Integer> out = source + .doOnRequest(new LongConsumer() { + @Override + public void accept(long t) { + requests.add(t); + } + }).replay().autoConnect(); + + TestSubscriber<Integer> ts1 = new TestSubscriber<>(5L); + TestSubscriber<Integer> ts2 = new TestSubscriber<>(10L); + + out.subscribe(ts1); + out.subscribe(ts2); + ts2.cancel(); + + Assert.assertEquals(Arrays.asList(5L, 5L), requests); + } + + @Test + public void subscribersComeAndGoAtRequestBoundaries() { + ConnectableFlowable<Integer> source = Flowable.range(1, 10).replay(1, true); + source.connect(); + + TestSubscriber<Integer> ts1 = new TestSubscriber<>(2L); + + source.subscribe(ts1); + + ts1.assertValues(1, 2); + ts1.assertNoErrors(); + ts1.cancel(); + + TestSubscriber<Integer> ts2 = new TestSubscriber<>(2L); + + source.subscribe(ts2); + + ts2.assertValues(2, 3); + ts2.assertNoErrors(); + ts2.cancel(); + + TestSubscriber<Integer> ts21 = new TestSubscriber<>(1L); + + source.subscribe(ts21); + + ts21.assertValues(3); + ts21.assertNoErrors(); + ts21.cancel(); + + TestSubscriber<Integer> ts22 = new TestSubscriber<>(1L); + + source.subscribe(ts22); + + ts22.assertValues(3); + ts22.assertNoErrors(); + ts22.cancel(); + + TestSubscriber<Integer> ts3 = new TestSubscriber<>(); + + source.subscribe(ts3); + + ts3.assertNoErrors(); + System.out.println(ts3.values()); + ts3.assertValues(3, 4, 5, 6, 7, 8, 9, 10); + ts3.assertComplete(); + } + + @Test + public void subscribersComeAndGoAtRequestBoundaries2() { + ConnectableFlowable<Integer> source = Flowable.range(1, 10).replay(2, true); + source.connect(); + + TestSubscriber<Integer> ts1 = new TestSubscriber<>(2L); + + source.subscribe(ts1); + + ts1.assertValues(1, 2); + ts1.assertNoErrors(); + ts1.cancel(); + + TestSubscriber<Integer> ts11 = new TestSubscriber<>(2L); + + source.subscribe(ts11); + + ts11.assertValues(1, 2); + ts11.assertNoErrors(); + ts11.cancel(); + + TestSubscriber<Integer> ts2 = new TestSubscriber<>(3L); + + source.subscribe(ts2); + + ts2.assertValues(1, 2, 3); + ts2.assertNoErrors(); + ts2.cancel(); + + TestSubscriber<Integer> ts21 = new TestSubscriber<>(1L); + + source.subscribe(ts21); + + ts21.assertValues(2); + ts21.assertNoErrors(); + ts21.cancel(); + + TestSubscriber<Integer> ts22 = new TestSubscriber<>(1L); + + source.subscribe(ts22); + + ts22.assertValues(2); + ts22.assertNoErrors(); + ts22.cancel(); + + TestSubscriber<Integer> ts3 = new TestSubscriber<>(); + + source.subscribe(ts3); + + ts3.assertNoErrors(); + System.out.println(ts3.values()); + ts3.assertValues(2, 3, 4, 5, 6, 7, 8, 9, 10); + ts3.assertComplete(); + } + + @Test + public void replayTime() { + Flowable.just(1).replay(1, TimeUnit.MINUTES, Schedulers.computation(), true) + .autoConnect() + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(1); + } + + @Test + public void replaySizeAndTime() { + Flowable.just(1).replay(1, 1, TimeUnit.MILLISECONDS, Schedulers.computation(), true) + .autoConnect() + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(1); + } + + @Test + public void replaySelectorTime() { + Flowable.just(1).replay(Functions.<Flowable<Integer>>identity(), 1, TimeUnit.MINUTES, Schedulers.computation(), true) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(1); + } + + @Test + public void source() { + Flowable<Integer> source = Flowable.range(1, 3); + + assertSame(source, (((HasUpstreamPublisher<?>)source.replay())).source()); + } + + @Test + public void connectRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final ConnectableFlowable<Integer> cf = Flowable.range(1, 3).replay(); + + Runnable r = new Runnable() { + @Override + public void run() { + cf.connect(); + } + }; + + TestHelper.race(r, r); + } + } + + @Test + public void subscribeRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final ConnectableFlowable<Integer> cf = Flowable.range(1, 3).replay(); + + final TestSubscriber<Integer> ts1 = new TestSubscriber<>(); + final TestSubscriber<Integer> ts2 = new TestSubscriber<>(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + cf.subscribe(ts1); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + cf.subscribe(ts2); + } + }; + + TestHelper.race(r1, r2); + } + } + + @Test + public void addRemoveRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final ConnectableFlowable<Integer> cf = Flowable.range(1, 3).replay(); + + final TestSubscriber<Integer> ts1 = new TestSubscriber<>(); + final TestSubscriber<Integer> ts2 = new TestSubscriber<>(); + + cf.subscribe(ts1); + + Runnable r1 = new Runnable() { + @Override + public void run() { + ts1.cancel(); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + cf.subscribe(ts2); + } + }; + + TestHelper.race(r1, r2); + } + } + + @Test + public void cancelOnArrival() { + Flowable.range(1, 2) + .replay(Integer.MAX_VALUE, true) + .autoConnect() + .test(Long.MAX_VALUE, true) + .assertEmpty(); + } + + @Test + public void cancelOnArrival2() { + ConnectableFlowable<Integer> cf = PublishProcessor.<Integer>create() + .replay(Integer.MAX_VALUE, true); + + cf.test(); + + cf + .autoConnect() + .test(Long.MAX_VALUE, true) + .assertEmpty(); + } + + @Test + public void connectConsumerThrows() { + ConnectableFlowable<Integer> cf = Flowable.range(1, 2) + .replay(); + + try { + cf.connect(new Consumer<Disposable>() { + @Override + public void accept(Disposable t) throws Exception { + throw new TestException(); + } + }); + fail("Should have thrown"); + } catch (TestException ex) { + // expected + } + + cf.test().assertEmpty().cancel(); + + cf.connect(); + + cf.test().assertResult(1, 2); + } + + @Test + public void badSource() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + new Flowable<Integer>() { + @Override + protected void subscribeActual(Subscriber<? super Integer> subscriber) { + subscriber.onSubscribe(new BooleanSubscription()); + subscriber.onError(new TestException("First")); + subscriber.onNext(1); + subscriber.onError(new TestException("Second")); + subscriber.onComplete(); + } + }.replay() + .autoConnect() + .to(TestHelper.<Integer>testConsumer()) + .assertFailureAndMessage(TestException.class, "First"); + + TestHelper.assertUndeliverable(errors, 0, TestException.class, "Second"); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void subscribeOnNextRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final PublishProcessor<Integer> pp = PublishProcessor.create(); + + final ConnectableFlowable<Integer> cf = pp.replay(); + + final TestSubscriber<Integer> ts1 = new TestSubscriber<>(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + cf.subscribe(ts1); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + for (int j = 0; j < 1000; j++) { + pp.onNext(j); + } + } + }; + + TestHelper.race(r1, r2); + } + } + + @Test + public void unsubscribeOnNextRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final PublishProcessor<Integer> pp = PublishProcessor.create(); + + final ConnectableFlowable<Integer> cf = pp.replay(); + + final TestSubscriber<Integer> ts1 = new TestSubscriber<>(); + + cf.subscribe(ts1); + + Runnable r1 = new Runnable() { + @Override + public void run() { + ts1.cancel(); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + for (int j = 0; j < 1000; j++) { + pp.onNext(j); + } + } + }; + + TestHelper.race(r1, r2); + } + } + + @Test + public void unsubscribeReplayRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final ConnectableFlowable<Integer> cf = Flowable.range(1, 1000).replay(); + + final TestSubscriber<Integer> ts1 = new TestSubscriber<>(); + + cf.connect(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + cf.subscribe(ts1); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + ts1.cancel(); + } + }; + + TestHelper.race(r1, r2); + } + } + + @Test + public void reentrantOnNext() { + final PublishProcessor<Integer> pp = PublishProcessor.create(); + + TestSubscriber<Integer> ts = new TestSubscriber<Integer>() { + @Override + public void onNext(Integer t) { + if (t == 1) { + pp.onNext(2); + pp.onComplete(); + } + super.onNext(t); + } + }; + + pp.replay().autoConnect().subscribe(ts); + + pp.onNext(1); + + ts.assertResult(1, 2); + } + + @Test + public void reentrantOnNextBound() { + final PublishProcessor<Integer> pp = PublishProcessor.create(); + + TestSubscriber<Integer> ts = new TestSubscriber<Integer>() { + @Override + public void onNext(Integer t) { + if (t == 1) { + pp.onNext(2); + pp.onComplete(); + } + super.onNext(t); + } + }; + + pp.replay(10, true).autoConnect().subscribe(ts); + + pp.onNext(1); + + ts.assertResult(1, 2); + } + + @Test + public void reentrantOnNextCancel() { + final PublishProcessor<Integer> pp = PublishProcessor.create(); + + TestSubscriber<Integer> ts = new TestSubscriber<Integer>() { + @Override + public void onNext(Integer t) { + if (t == 1) { + pp.onNext(2); + cancel(); + } + super.onNext(t); + } + }; + + pp.replay().autoConnect().subscribe(ts); + + pp.onNext(1); + + ts.assertValues(1); + } + + @Test + public void reentrantOnNextCancelBounded() { + final PublishProcessor<Integer> pp = PublishProcessor.create(); + + TestSubscriber<Integer> ts = new TestSubscriber<Integer>() { + @Override + public void onNext(Integer t) { + if (t == 1) { + pp.onNext(2); + cancel(); + } + super.onNext(t); + } + }; + + pp.replay(10, true).autoConnect().subscribe(ts); + + pp.onNext(1); + + ts.assertValues(1); + } + + @Test + public void replayMaxInt() { + Flowable.range(1, 2) + .replay(Integer.MAX_VALUE, true) + .autoConnect() + .test() + .assertResult(1, 2); + } + + @Test + public void timedAndSizedTruncationError() { + TestScheduler test = new TestScheduler(); + SizeAndTimeBoundReplayBuffer<Integer> buf = new SizeAndTimeBoundReplayBuffer<>(2, 2000, TimeUnit.MILLISECONDS, test, true); + + Assert.assertFalse(buf.hasCompleted()); + Assert.assertFalse(buf.hasError()); + + List<Integer> values = new ArrayList<>(); + + buf.next(1); + test.advanceTimeBy(1, TimeUnit.SECONDS); + buf.next(2); + test.advanceTimeBy(1, TimeUnit.SECONDS); + buf.collect(values); + Assert.assertEquals(Arrays.asList(2), values); + + buf.next(3); + buf.next(4); + values.clear(); + buf.collect(values); + Assert.assertEquals(Arrays.asList(3, 4), values); + + test.advanceTimeBy(2, TimeUnit.SECONDS); + buf.next(5); + + values.clear(); + buf.collect(values); + Assert.assertEquals(Arrays.asList(5), values); + Assert.assertFalse(buf.hasCompleted()); + Assert.assertFalse(buf.hasError()); + + test.advanceTimeBy(2, TimeUnit.SECONDS); + buf.error(new TestException()); + + values.clear(); + buf.collect(values); + Assert.assertTrue(values.isEmpty()); + + Assert.assertEquals(1, buf.size); + Assert.assertFalse(buf.hasCompleted()); + Assert.assertTrue(buf.hasError()); + } + + @Test + public void sizedTruncation() { + SizeBoundReplayBuffer<Integer> buf = new SizeBoundReplayBuffer<>(2, true); + List<Integer> values = new ArrayList<>(); + + buf.next(1); + buf.next(2); + buf.collect(values); + Assert.assertEquals(Arrays.asList(1, 2), values); + + buf.next(3); + buf.next(4); + values.clear(); + buf.collect(values); + Assert.assertEquals(Arrays.asList(3, 4), values); + + buf.next(5); + + values.clear(); + buf.collect(values); + Assert.assertEquals(Arrays.asList(4, 5), values); + Assert.assertFalse(buf.hasCompleted()); + + buf.complete(); + + values.clear(); + buf.collect(values); + Assert.assertEquals(Arrays.asList(4, 5), values); + + Assert.assertEquals(3, buf.size); + Assert.assertTrue(buf.hasCompleted()); + Assert.assertFalse(buf.hasError()); + } + + @Test + public void delayedUpstreamOnSubscribe() { + final Subscriber<?>[] sub = { null }; + + new Flowable<Integer>() { + @Override + protected void subscribeActual(Subscriber<? super Integer> s) { + sub[0] = s; + } + } + .replay() + .connect() + .dispose(); + + BooleanSubscription bs = new BooleanSubscription(); + + sub[0].onSubscribe(bs); + + assertTrue(bs.isCancelled()); + } + + @Test + public void timedNoOutdatedData() { + TestScheduler scheduler = new TestScheduler(); + + Flowable<Integer> source = Flowable.just(1) + .replay(2, TimeUnit.SECONDS, scheduler, true) + .autoConnect(); + + source.test().assertResult(1); + + source.test().assertResult(1); + + scheduler.advanceTimeBy(3, TimeUnit.SECONDS); + + source.test().assertResult(); + } + + @Test + public void multicastSelectorCallableConnectableCrash() { + FlowableReplay.multicastSelector(new Supplier<ConnectableFlowable<Object>>() { + @Override + public ConnectableFlowable<Object> get() throws Exception { + throw new TestException(); + } + }, Functions.<Flowable<Object>>identity()) + .test() + .assertFailure(TestException.class); + } + + @Test + public void badRequest() { + TestHelper.assertBadRequestReported( + Flowable.never() + .replay() + ); + } + + @Test + public void noHeadRetentionCompleteSize() { + PublishProcessor<Integer> source = PublishProcessor.create(); + + FlowableReplay<Integer> co = (FlowableReplay<Integer>)source + .replay(1, true); + + // the backpressure coordination would not accept items from source otherwise + co.test(); + + co.connect(); + + BoundedReplayBuffer<Integer> buf = (BoundedReplayBuffer<Integer>)(co.current.get().buffer); + + source.onNext(1); + source.onNext(2); + source.onComplete(); + + assertNull(buf.get().value); + + Object o = buf.get(); + + buf.trimHead(); + + assertSame(o, buf.get()); + } + + @Test + public void noHeadRetentionErrorSize() { + PublishProcessor<Integer> source = PublishProcessor.create(); + + FlowableReplay<Integer> co = (FlowableReplay<Integer>)source + .replay(1, true); + + co.test(); + + co.connect(); + + BoundedReplayBuffer<Integer> buf = (BoundedReplayBuffer<Integer>)(co.current.get().buffer); + + source.onNext(1); + source.onNext(2); + source.onError(new TestException()); + + assertNull(buf.get().value); + + Object o = buf.get(); + + buf.trimHead(); + + assertSame(o, buf.get()); + } + + @Test + public void noHeadRetentionSize() { + PublishProcessor<Integer> source = PublishProcessor.create(); + + FlowableReplay<Integer> co = (FlowableReplay<Integer>)source + .replay(1, true); + + co.test(); + + co.connect(); + + BoundedReplayBuffer<Integer> buf = (BoundedReplayBuffer<Integer>)(co.current.get().buffer); + + source.onNext(1); + source.onNext(2); + + assertNull(buf.get().value); + + buf.trimHead(); + + assertNull(buf.get().value); + + Object o = buf.get(); + + buf.trimHead(); + + assertSame(o, buf.get()); + } + + @Test + public void noHeadRetentionCompleteTime() { + PublishProcessor<Integer> source = PublishProcessor.create(); + + FlowableReplay<Integer> co = (FlowableReplay<Integer>)source + .replay(1, TimeUnit.MINUTES, Schedulers.computation(), true); + + co.test(); + + co.connect(); + + BoundedReplayBuffer<Integer> buf = (BoundedReplayBuffer<Integer>)(co.current.get().buffer); + + source.onNext(1); + source.onNext(2); + source.onComplete(); + + assertNull(buf.get().value); + + Object o = buf.get(); + + buf.trimHead(); + + assertSame(o, buf.get()); + } + + @Test + public void noHeadRetentionErrorTime() { + PublishProcessor<Integer> source = PublishProcessor.create(); + + FlowableReplay<Integer> co = (FlowableReplay<Integer>)source + .replay(1, TimeUnit.MINUTES, Schedulers.computation(), true); + + co.test(); + + co.connect(); + + BoundedReplayBuffer<Integer> buf = (BoundedReplayBuffer<Integer>)(co.current.get().buffer); + + source.onNext(1); + source.onNext(2); + source.onError(new TestException()); + + assertNull(buf.get().value); + + Object o = buf.get(); + + buf.trimHead(); + + assertSame(o, buf.get()); + } + + @Test + public void noHeadRetentionTime() { + TestScheduler sch = new TestScheduler(); + + PublishProcessor<Integer> source = PublishProcessor.create(); + + FlowableReplay<Integer> co = (FlowableReplay<Integer>)source + .replay(1, TimeUnit.MILLISECONDS, sch, true); + + co.test(); + + co.connect(); + + BoundedReplayBuffer<Integer> buf = (BoundedReplayBuffer<Integer>)(co.current.get().buffer); + + source.onNext(1); + + sch.advanceTimeBy(2, TimeUnit.MILLISECONDS); + + source.onNext(2); + + assertNull(buf.get().value); + + buf.trimHead(); + + assertNull(buf.get().value); + + Object o = buf.get(); + + buf.trimHead(); + + assertSame(o, buf.get()); + } + + @Test(expected = TestException.class) + public void createBufferFactoryCrash() { + FlowableReplay.create(Flowable.just(1), new Supplier<ReplayBuffer<Integer>>() { + @Override + public ReplayBuffer<Integer> get() throws Exception { + throw new TestException(); + } + }) + .connect(); + } + + @Test + public void createBufferFactoryCrashOnSubscribe() { + FlowableReplay.create(Flowable.just(1), new Supplier<ReplayBuffer<Integer>>() { + @Override + public ReplayBuffer<Integer> get() throws Exception { + throw new TestException(); + } + }) + .test() + .assertFailure(TestException.class); + } + + @Test + public void noBoundedRetentionViaThreadLocal() throws Exception { + Flowable<byte[]> source = Flowable.range(1, 200) + .map(new Function<Integer, byte[]>() { + @Override + public byte[] apply(Integer v) throws Exception { + return new byte[1024 * 1024]; + } + }) + .replay(new Function<Flowable<byte[]>, Publisher<byte[]>>() { + @Override + public Publisher<byte[]> apply(final Flowable<byte[]> f) throws Exception { + return f.take(1) + .concatMap(new Function<byte[], Publisher<byte[]>>() { + @Override + public Publisher<byte[]> apply(byte[] v) throws Exception { + return f; + } + }); + } + }, 1, true) + .takeLast(1) + ; + + System.out.println("Bounded Replay Leak check: Wait before GC"); + Thread.sleep(1000); + + System.out.println("Bounded Replay Leak check: GC"); + System.gc(); + + Thread.sleep(500); + + final MemoryMXBean memoryMXBean = ManagementFactory.getMemoryMXBean(); + MemoryUsage memHeap = memoryMXBean.getHeapMemoryUsage(); + long initial = memHeap.getUsed(); + + System.out.printf("Bounded Replay Leak check: Starting: %.3f MB%n", initial / 1024.0 / 1024.0); + + final AtomicLong after = new AtomicLong(); + + source.subscribe(new Consumer<byte[]>() { + @Override + public void accept(byte[] v) throws Exception { + System.out.println("Bounded Replay Leak check: Wait before GC 2"); + Thread.sleep(1000); + + System.out.println("Bounded Replay Leak check: GC 2"); + System.gc(); + + Thread.sleep(500); + + after.set(memoryMXBean.getHeapMemoryUsage().getUsed()); + } + }); + + System.out.printf("Bounded Replay Leak check: After: %.3f MB%n", after.get() / 1024.0 / 1024.0); + + if (initial + 100 * 1024 * 1024 < after.get()) { + Assert.fail("Bounded Replay Leak check: Memory leak detected: " + (initial / 1024.0 / 1024.0) + + " -> " + after.get() / 1024.0 / 1024.0); + } + } + + @Test + public void sizeBoundEagerTruncate() throws Exception { + + PublishProcessor<int[]> pp = PublishProcessor.create(); + + ConnectableFlowable<int[]> cf = pp.replay(1, true); + + TestSubscriber<int[]> ts = cf.test(); + + cf.connect(); + + Thread.sleep(200); + System.gc(); + Thread.sleep(200); + + final MemoryMXBean memoryMXBean = ManagementFactory.getMemoryMXBean(); + long initial = memoryMXBean.getHeapMemoryUsage().getUsed(); + + System.out.printf("Bounded Replay Leak check: Starting: %.3f MB%n", initial / 1024.0 / 1024.0); + + pp.onNext(new int[100 * 1024 * 1024]); + + ts.assertValueCount(1); + ts.values().clear(); + + pp.onNext(new int[0]); + + Thread.sleep(200); + System.gc(); + Thread.sleep(200); + + long after = memoryMXBean.getHeapMemoryUsage().getUsed(); + + ts.cancel(); + + System.out.printf("Bounded Replay Leak check: After: %.3f MB%n", after / 1024.0 / 1024.0); + + if (initial + 100 * 1024 * 1024 < after) { + Assert.fail("Bounded Replay Leak check: Memory leak detected: " + (initial / 1024.0 / 1024.0) + + " -> " + after / 1024.0 / 1024.0); + } + } + + @Test + public void timeBoundEagerTruncate() throws Exception { + + PublishProcessor<int[]> pp = PublishProcessor.create(); + + TestScheduler scheduler = new TestScheduler(); + + ConnectableFlowable<int[]> cf = pp.replay(1, TimeUnit.SECONDS, scheduler, true); + + TestSubscriber<int[]> ts = cf.test(); + + cf.connect(); + + Thread.sleep(200); + System.gc(); + Thread.sleep(200); + + final MemoryMXBean memoryMXBean = ManagementFactory.getMemoryMXBean(); + long initial = memoryMXBean.getHeapMemoryUsage().getUsed(); + + System.out.printf("Bounded Replay Leak check: Starting: %.3f MB%n", initial / 1024.0 / 1024.0); + + pp.onNext(new int[100 * 1024 * 1024]); + + ts.assertValueCount(1); + ts.values().clear(); + + scheduler.advanceTimeBy(2, TimeUnit.SECONDS); + + pp.onNext(new int[0]); + + Thread.sleep(200); + System.gc(); + Thread.sleep(200); + + long after = memoryMXBean.getHeapMemoryUsage().getUsed(); + + ts.cancel(); + + System.out.printf("Bounded Replay Leak check: After: %.3f MB%n", after / 1024.0 / 1024.0); + + if (initial + 100 * 1024 * 1024 < after) { + Assert.fail("Bounded Replay Leak check: Memory leak detected: " + (initial / 1024.0 / 1024.0) + + " -> " + after / 1024.0 / 1024.0); + } + } + + @Test + public void timeAndSizeBoundEagerTruncate() throws Exception { + + PublishProcessor<int[]> pp = PublishProcessor.create(); + + TestScheduler scheduler = new TestScheduler(); + + ConnectableFlowable<int[]> cf = pp.replay(1, 5, TimeUnit.SECONDS, scheduler, true); + + TestSubscriber<int[]> ts = cf.test(); + + cf.connect(); + + Thread.sleep(200); + System.gc(); + Thread.sleep(200); + + final MemoryMXBean memoryMXBean = ManagementFactory.getMemoryMXBean(); + long initial = memoryMXBean.getHeapMemoryUsage().getUsed(); + + System.out.printf("Bounded Replay Leak check: Starting: %.3f MB%n", initial / 1024.0 / 1024.0); + + pp.onNext(new int[100 * 1024 * 1024]); + + ts.assertValueCount(1); + ts.values().clear(); + + scheduler.advanceTimeBy(2, TimeUnit.SECONDS); + + pp.onNext(new int[0]); + + Thread.sleep(200); + System.gc(); + Thread.sleep(200); + + long after = memoryMXBean.getHeapMemoryUsage().getUsed(); + + ts.cancel(); + + System.out.printf("Bounded Replay Leak check: After: %.3f MB%n", after / 1024.0 / 1024.0); + + if (initial + 100 * 1024 * 1024 < after) { + Assert.fail("Bounded Replay Leak check: Memory leak detected: " + (initial / 1024.0 / 1024.0) + + " -> " + after / 1024.0 / 1024.0); + } + } + + @Test + public void sizeBoundSelectorEagerTruncate() throws Exception { + + PublishProcessor<int[]> pp = PublishProcessor.create(); + + Flowable<int[]> cf = pp.replay(Functions.<Flowable<int[]>>identity(), 1, true); + + TestSubscriber<int[]> ts = cf.test(); + + Thread.sleep(200); + System.gc(); + Thread.sleep(200); + + final MemoryMXBean memoryMXBean = ManagementFactory.getMemoryMXBean(); + long initial = memoryMXBean.getHeapMemoryUsage().getUsed(); + + System.out.printf("Bounded Replay Leak check: Starting: %.3f MB%n", initial / 1024.0 / 1024.0); + + pp.onNext(new int[100 * 1024 * 1024]); + + ts.assertValueCount(1); + ts.values().clear(); + + pp.onNext(new int[0]); + + Thread.sleep(200); + System.gc(); + Thread.sleep(200); + + long after = memoryMXBean.getHeapMemoryUsage().getUsed(); + + ts.cancel(); + + System.out.printf("Bounded Replay Leak check: After: %.3f MB%n", after / 1024.0 / 1024.0); + + if (initial + 100 * 1024 * 1024 < after) { + Assert.fail("Bounded Replay Leak check: Memory leak detected: " + (initial / 1024.0 / 1024.0) + + " -> " + after / 1024.0 / 1024.0); + } + } + + @Test + public void timeBoundSelectorEagerTruncate() throws Exception { + + PublishProcessor<int[]> pp = PublishProcessor.create(); + + TestScheduler scheduler = new TestScheduler(); + + Flowable<int[]> cf = pp.replay(Functions.<Flowable<int[]>>identity(), 1, TimeUnit.SECONDS, scheduler, true); + + TestSubscriber<int[]> ts = cf.test(); + + Thread.sleep(200); + System.gc(); + Thread.sleep(200); + + final MemoryMXBean memoryMXBean = ManagementFactory.getMemoryMXBean(); + long initial = memoryMXBean.getHeapMemoryUsage().getUsed(); + + System.out.printf("Bounded Replay Leak check: Starting: %.3f MB%n", initial / 1024.0 / 1024.0); + + pp.onNext(new int[100 * 1024 * 1024]); + + ts.assertValueCount(1); + ts.values().clear(); + + scheduler.advanceTimeBy(2, TimeUnit.SECONDS); + + pp.onNext(new int[0]); + + Thread.sleep(200); + System.gc(); + Thread.sleep(200); + + long after = memoryMXBean.getHeapMemoryUsage().getUsed(); + + ts.cancel(); + + System.out.printf("Bounded Replay Leak check: After: %.3f MB%n", after / 1024.0 / 1024.0); + + if (initial + 100 * 1024 * 1024 < after) { + Assert.fail("Bounded Replay Leak check: Memory leak detected: " + (initial / 1024.0 / 1024.0) + + " -> " + after / 1024.0 / 1024.0); + } + } + + @Test + public void timeAndSizeBoundSelectorEagerTruncate() throws Exception { + + PublishProcessor<int[]> pp = PublishProcessor.create(); + + TestScheduler scheduler = new TestScheduler(); + + Flowable<int[]> cf = pp.replay(Functions.<Flowable<int[]>>identity(), 1, 5, TimeUnit.SECONDS, scheduler, true); + + TestSubscriber<int[]> ts = cf.test(); + + Thread.sleep(200); + System.gc(); + Thread.sleep(200); + + final MemoryMXBean memoryMXBean = ManagementFactory.getMemoryMXBean(); + long initial = memoryMXBean.getHeapMemoryUsage().getUsed(); + + System.out.printf("Bounded Replay Leak check: Starting: %.3f MB%n", initial / 1024.0 / 1024.0); + + pp.onNext(new int[100 * 1024 * 1024]); + + ts.assertValueCount(1); + ts.values().clear(); + + scheduler.advanceTimeBy(2, TimeUnit.SECONDS); + + pp.onNext(new int[0]); + + Thread.sleep(200); + System.gc(); + Thread.sleep(200); + + long after = memoryMXBean.getHeapMemoryUsage().getUsed(); + + ts.cancel(); + + System.out.printf("Bounded Replay Leak check: After: %.3f MB%n", after / 1024.0 / 1024.0); + + if (initial + 100 * 1024 * 1024 < after) { + Assert.fail("Bounded Replay Leak check: Memory leak detected: " + (initial / 1024.0 / 1024.0) + + " -> " + after / 1024.0 / 1024.0); + } + } + + @Test + public void timeAndSizeNoTerminalTruncationOnTimechange() { + Flowable.just(1).replay(1, 1, TimeUnit.SECONDS, new TimesteppingScheduler(), true) + .autoConnect() + .test() + .assertComplete() + .assertNoErrors(); + } + + @Test + public void disposeNoNeedForResetSizeBound() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + + ConnectableFlowable<Integer> cf = pp.replay(10, true); + + TestSubscriber<Integer> ts = cf.test(); + + Disposable d = cf.connect(); + + pp.onNext(1); + + d.dispose(); + + ts = cf.test(); + + ts.assertEmpty(); + + cf.connect(); + + ts.assertEmpty(); + + pp.onNext(2); + + ts.assertValuesOnly(2); + } + + @Test + public void disposeNoNeedForResetTimeBound() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + + ConnectableFlowable<Integer> cf = pp.replay(10, TimeUnit.MINUTES, Schedulers.single(), true); + + TestSubscriber<Integer> ts = cf.test(); + + Disposable d = cf.connect(); + + pp.onNext(1); + + d.dispose(); + + ts = cf.test(); + + ts.assertEmpty(); + + cf.connect(); + + ts.assertEmpty(); + + pp.onNext(2); + + ts.assertValuesOnly(2); + } + + @Test + public void disposeNoNeedForResetTimeAndSIzeBound() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + + ConnectableFlowable<Integer> cf = pp.replay(10, 10, TimeUnit.MINUTES, Schedulers.single(), true); + + TestSubscriber<Integer> ts = cf.test(); + + Disposable d = cf.connect(); + + pp.onNext(1); + + d.dispose(); + + ts = cf.test(); + + ts.assertEmpty(); + + cf.connect(); + + ts.assertEmpty(); + + pp.onNext(2); + + ts.assertValuesOnly(2); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableReplayTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableReplayTest.java new file mode 100644 index 0000000000..b644629a6d --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableReplayTest.java @@ -0,0 +1,2289 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +import java.io.IOException; +import java.lang.management.*; +import java.util.*; +import java.util.concurrent.*; +import java.util.concurrent.atomic.*; + +import org.junit.*; +import org.mockito.InOrder; +import org.reactivestreams.*; + +import io.reactivex.rxjava3.annotations.NonNull; +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.core.Scheduler.Worker; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.flowables.ConnectableFlowable; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.internal.fuseable.HasUpstreamPublisher; +import io.reactivex.rxjava3.internal.operators.flowable.FlowableReplay.*; +import io.reactivex.rxjava3.internal.subscriptions.BooleanSubscription; +import io.reactivex.rxjava3.internal.util.BackpressureHelper; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.processors.PublishProcessor; +import io.reactivex.rxjava3.schedulers.*; +import io.reactivex.rxjava3.subscribers.TestSubscriber; +import io.reactivex.rxjava3.testsupport.*; + +public class FlowableReplayTest extends RxJavaTest { + + @Test + public void bufferedReplay() { + PublishProcessor<Integer> source = PublishProcessor.create(); + + ConnectableFlowable<Integer> cf = source.replay(3); + cf.connect(); + + { + Subscriber<Object> subscriber1 = TestHelper.mockSubscriber(); + InOrder inOrder = inOrder(subscriber1); + + cf.subscribe(subscriber1); + + source.onNext(1); + source.onNext(2); + source.onNext(3); + + inOrder.verify(subscriber1, times(1)).onNext(1); + inOrder.verify(subscriber1, times(1)).onNext(2); + inOrder.verify(subscriber1, times(1)).onNext(3); + + source.onNext(4); + source.onComplete(); + inOrder.verify(subscriber1, times(1)).onNext(4); + inOrder.verify(subscriber1, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + verify(subscriber1, never()).onError(any(Throwable.class)); + + } + + { + Subscriber<Object> subscriber1 = TestHelper.mockSubscriber(); + InOrder inOrder = inOrder(subscriber1); + + cf.subscribe(subscriber1); + + inOrder.verify(subscriber1, times(1)).onNext(2); + inOrder.verify(subscriber1, times(1)).onNext(3); + inOrder.verify(subscriber1, times(1)).onNext(4); + inOrder.verify(subscriber1, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + verify(subscriber1, never()).onError(any(Throwable.class)); + } + } + + @Test + public void bufferedWindowReplay() { + PublishProcessor<Integer> source = PublishProcessor.create(); + TestScheduler scheduler = new TestScheduler(); + ConnectableFlowable<Integer> cf = source.replay(3, 100, TimeUnit.MILLISECONDS, scheduler); + cf.connect(); + + { + Subscriber<Object> subscriber1 = TestHelper.mockSubscriber(); + InOrder inOrder = inOrder(subscriber1); + + cf.subscribe(subscriber1); + + source.onNext(1); + scheduler.advanceTimeBy(10, TimeUnit.MILLISECONDS); + source.onNext(2); + scheduler.advanceTimeBy(10, TimeUnit.MILLISECONDS); + source.onNext(3); + scheduler.advanceTimeBy(10, TimeUnit.MILLISECONDS); + + inOrder.verify(subscriber1, times(1)).onNext(1); + inOrder.verify(subscriber1, times(1)).onNext(2); + inOrder.verify(subscriber1, times(1)).onNext(3); + + source.onNext(4); + source.onNext(5); + scheduler.advanceTimeBy(90, TimeUnit.MILLISECONDS); + + inOrder.verify(subscriber1, times(1)).onNext(4); + + inOrder.verify(subscriber1, times(1)).onNext(5); + + inOrder.verifyNoMoreInteractions(); + verify(subscriber1, never()).onError(any(Throwable.class)); + + } + + { + Subscriber<Object> subscriber1 = TestHelper.mockSubscriber(); + InOrder inOrder = inOrder(subscriber1); + + cf.subscribe(subscriber1); + + inOrder.verify(subscriber1, times(1)).onNext(4); + inOrder.verify(subscriber1, times(1)).onNext(5); + inOrder.verifyNoMoreInteractions(); + verify(subscriber1, never()).onError(any(Throwable.class)); + } + } + + @Test + public void windowedReplay() { + TestScheduler scheduler = new TestScheduler(); + + PublishProcessor<Integer> source = PublishProcessor.create(); + + ConnectableFlowable<Integer> cf = source.replay(100, TimeUnit.MILLISECONDS, scheduler); + cf.connect(); + + { + Subscriber<Object> subscriber1 = TestHelper.mockSubscriber(); + InOrder inOrder = inOrder(subscriber1); + + cf.subscribe(subscriber1); + + source.onNext(1); + scheduler.advanceTimeBy(60, TimeUnit.MILLISECONDS); + source.onNext(2); + scheduler.advanceTimeBy(60, TimeUnit.MILLISECONDS); + source.onNext(3); + scheduler.advanceTimeBy(60, TimeUnit.MILLISECONDS); + source.onComplete(); + scheduler.advanceTimeBy(60, TimeUnit.MILLISECONDS); + + inOrder.verify(subscriber1, times(1)).onNext(1); + inOrder.verify(subscriber1, times(1)).onNext(2); + inOrder.verify(subscriber1, times(1)).onNext(3); + + inOrder.verify(subscriber1, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + verify(subscriber1, never()).onError(any(Throwable.class)); + + } + { + Subscriber<Object> subscriber1 = TestHelper.mockSubscriber(); + InOrder inOrder = inOrder(subscriber1); + + cf.subscribe(subscriber1); + inOrder.verify(subscriber1, never()).onNext(3); + + inOrder.verify(subscriber1, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + verify(subscriber1, never()).onError(any(Throwable.class)); + } + } + + @Test + public void replaySelector() { + final Function<Integer, Integer> dbl = new Function<Integer, Integer>() { + + @Override + public Integer apply(Integer t1) { + return t1 * 2; + } + + }; + + Function<Flowable<Integer>, Flowable<Integer>> selector = new Function<Flowable<Integer>, Flowable<Integer>>() { + + @Override + public Flowable<Integer> apply(Flowable<Integer> t1) { + return t1.map(dbl); + } + + }; + + PublishProcessor<Integer> source = PublishProcessor.create(); + + Flowable<Integer> co = source.replay(selector); + + { + Subscriber<Object> subscriber1 = TestHelper.mockSubscriber(); + InOrder inOrder = inOrder(subscriber1); + + co.subscribe(subscriber1); + + source.onNext(1); + source.onNext(2); + source.onNext(3); + + inOrder.verify(subscriber1, times(1)).onNext(2); + inOrder.verify(subscriber1, times(1)).onNext(4); + inOrder.verify(subscriber1, times(1)).onNext(6); + + source.onNext(4); + source.onComplete(); + inOrder.verify(subscriber1, times(1)).onNext(8); + inOrder.verify(subscriber1, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + verify(subscriber1, never()).onError(any(Throwable.class)); + + } + + { + Subscriber<Object> subscriber1 = TestHelper.mockSubscriber(); + InOrder inOrder = inOrder(subscriber1); + + co.subscribe(subscriber1); + + inOrder.verify(subscriber1, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + verify(subscriber1, never()).onError(any(Throwable.class)); + + } + + } + + @Test + public void bufferedReplaySelector() { + + final Function<Integer, Integer> dbl = new Function<Integer, Integer>() { + + @Override + public Integer apply(Integer t1) { + return t1 * 2; + } + + }; + + Function<Flowable<Integer>, Flowable<Integer>> selector = new Function<Flowable<Integer>, Flowable<Integer>>() { + + @Override + public Flowable<Integer> apply(Flowable<Integer> t1) { + return t1.map(dbl); + } + + }; + + PublishProcessor<Integer> source = PublishProcessor.create(); + + Flowable<Integer> co = source.replay(selector, 3); + + { + Subscriber<Object> subscriber1 = TestHelper.mockSubscriber(); + InOrder inOrder = inOrder(subscriber1); + + co.subscribe(subscriber1); + + source.onNext(1); + source.onNext(2); + source.onNext(3); + + inOrder.verify(subscriber1, times(1)).onNext(2); + inOrder.verify(subscriber1, times(1)).onNext(4); + inOrder.verify(subscriber1, times(1)).onNext(6); + + source.onNext(4); + source.onComplete(); + inOrder.verify(subscriber1, times(1)).onNext(8); + inOrder.verify(subscriber1, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + verify(subscriber1, never()).onError(any(Throwable.class)); + + } + + { + Subscriber<Object> subscriber1 = TestHelper.mockSubscriber(); + InOrder inOrder = inOrder(subscriber1); + + co.subscribe(subscriber1); + + inOrder.verify(subscriber1, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + verify(subscriber1, never()).onError(any(Throwable.class)); + } + } + + @Test + public void windowedReplaySelector() { + + final Function<Integer, Integer> dbl = new Function<Integer, Integer>() { + + @Override + public Integer apply(Integer t1) { + return t1 * 2; + } + + }; + + Function<Flowable<Integer>, Flowable<Integer>> selector = new Function<Flowable<Integer>, Flowable<Integer>>() { + + @Override + public Flowable<Integer> apply(Flowable<Integer> t1) { + return t1.map(dbl); + } + + }; + + TestScheduler scheduler = new TestScheduler(); + + PublishProcessor<Integer> source = PublishProcessor.create(); + + Flowable<Integer> co = source.replay(selector, 100, TimeUnit.MILLISECONDS, scheduler); + + { + Subscriber<Object> subscriber1 = TestHelper.mockSubscriber(); + InOrder inOrder = inOrder(subscriber1); + + co.subscribe(subscriber1); + + source.onNext(1); + scheduler.advanceTimeBy(60, TimeUnit.MILLISECONDS); + source.onNext(2); + scheduler.advanceTimeBy(60, TimeUnit.MILLISECONDS); + source.onNext(3); + scheduler.advanceTimeBy(60, TimeUnit.MILLISECONDS); + source.onComplete(); + scheduler.advanceTimeBy(60, TimeUnit.MILLISECONDS); + + inOrder.verify(subscriber1, times(1)).onNext(2); + inOrder.verify(subscriber1, times(1)).onNext(4); + inOrder.verify(subscriber1, times(1)).onNext(6); + + inOrder.verify(subscriber1, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + verify(subscriber1, never()).onError(any(Throwable.class)); + + } + { + Subscriber<Object> subscriber1 = TestHelper.mockSubscriber(); + InOrder inOrder = inOrder(subscriber1); + + co.subscribe(subscriber1); + + inOrder.verify(subscriber1, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + verify(subscriber1, never()).onError(any(Throwable.class)); + } + } + + @Test + public void bufferedReplayError() { + PublishProcessor<Integer> source = PublishProcessor.create(); + + ConnectableFlowable<Integer> cf = source.replay(3); + cf.connect(); + + { + Subscriber<Object> subscriber1 = TestHelper.mockSubscriber(); + InOrder inOrder = inOrder(subscriber1); + + cf.subscribe(subscriber1); + + source.onNext(1); + source.onNext(2); + source.onNext(3); + + inOrder.verify(subscriber1, times(1)).onNext(1); + inOrder.verify(subscriber1, times(1)).onNext(2); + inOrder.verify(subscriber1, times(1)).onNext(3); + + source.onNext(4); + source.onError(new RuntimeException("Forced failure")); + + inOrder.verify(subscriber1, times(1)).onNext(4); + inOrder.verify(subscriber1, times(1)).onError(any(RuntimeException.class)); + inOrder.verifyNoMoreInteractions(); + verify(subscriber1, never()).onComplete(); + + } + + { + Subscriber<Object> subscriber1 = TestHelper.mockSubscriber(); + InOrder inOrder = inOrder(subscriber1); + + cf.subscribe(subscriber1); + + inOrder.verify(subscriber1, times(1)).onNext(2); + inOrder.verify(subscriber1, times(1)).onNext(3); + inOrder.verify(subscriber1, times(1)).onNext(4); + inOrder.verify(subscriber1, times(1)).onError(any(RuntimeException.class)); + inOrder.verifyNoMoreInteractions(); + verify(subscriber1, never()).onComplete(); + } + } + + @Test + public void windowedReplayError() { + TestScheduler scheduler = new TestScheduler(); + + PublishProcessor<Integer> source = PublishProcessor.create(); + + ConnectableFlowable<Integer> cf = source.replay(100, TimeUnit.MILLISECONDS, scheduler); + cf.connect(); + + { + Subscriber<Object> subscriber1 = TestHelper.mockSubscriber(); + InOrder inOrder = inOrder(subscriber1); + + cf.subscribe(subscriber1); + + source.onNext(1); + scheduler.advanceTimeBy(60, TimeUnit.MILLISECONDS); + source.onNext(2); + scheduler.advanceTimeBy(60, TimeUnit.MILLISECONDS); + source.onNext(3); + scheduler.advanceTimeBy(60, TimeUnit.MILLISECONDS); + source.onError(new RuntimeException("Forced failure")); + scheduler.advanceTimeBy(60, TimeUnit.MILLISECONDS); + + inOrder.verify(subscriber1, times(1)).onNext(1); + inOrder.verify(subscriber1, times(1)).onNext(2); + inOrder.verify(subscriber1, times(1)).onNext(3); + + inOrder.verify(subscriber1, times(1)).onError(any(RuntimeException.class)); + inOrder.verifyNoMoreInteractions(); + verify(subscriber1, never()).onComplete(); + + } + { + Subscriber<Object> subscriber1 = TestHelper.mockSubscriber(); + InOrder inOrder = inOrder(subscriber1); + + cf.subscribe(subscriber1); + inOrder.verify(subscriber1, never()).onNext(3); + + inOrder.verify(subscriber1, times(1)).onError(any(RuntimeException.class)); + inOrder.verifyNoMoreInteractions(); + verify(subscriber1, never()).onComplete(); + } + } + + @Test + public void synchronousDisconnect() { + final AtomicInteger effectCounter = new AtomicInteger(); + Flowable<Integer> source = Flowable.just(1, 2, 3, 4) + .doOnNext(new Consumer<Integer>() { + @Override + public void accept(Integer v) { + effectCounter.incrementAndGet(); + System.out.println("Sideeffect #" + v); + } + }); + + Flowable<Integer> result = source.replay( + new Function<Flowable<Integer>, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Flowable<Integer> f) { + return f.take(2); + } + }); + + for (int i = 1; i < 3; i++) { + effectCounter.set(0); + System.out.printf("- %d -%n", i); + result.subscribe(new Consumer<Integer>() { + + @Override + public void accept(Integer t1) { + System.out.println(t1); + } + + }, new Consumer<Throwable>() { + + @Override + public void accept(Throwable t1) { + t1.printStackTrace(); + } + }, + new Action() { + @Override + public void run() { + System.out.println("Done"); + } + }); + assertEquals(2, effectCounter.get()); + } + } + + /* + * test the basic expectation of OperatorMulticast via replay + */ + @SuppressWarnings("unchecked") + @Test + public void issue2191_UnsubscribeSource() throws Throwable { + // setup mocks + Consumer<Integer> sourceNext = mock(Consumer.class); + Action sourceCompleted = mock(Action.class); + Action sourceUnsubscribed = mock(Action.class); + Subscriber<Integer> spiedSubscriberBeforeConnect = TestHelper.mockSubscriber(); + Subscriber<Integer> spiedSubscriberAfterConnect = TestHelper.mockSubscriber(); + + // Flowable under test + Flowable<Integer> source = Flowable.just(1, 2); + + ConnectableFlowable<Integer> replay = source + .doOnNext(sourceNext) + .doOnCancel(sourceUnsubscribed) + .doOnComplete(sourceCompleted) + .replay(); + + replay.subscribe(spiedSubscriberBeforeConnect); + replay.subscribe(spiedSubscriberBeforeConnect); + replay.connect(); + replay.subscribe(spiedSubscriberAfterConnect); + replay.subscribe(spiedSubscriberAfterConnect); + + verify(spiedSubscriberBeforeConnect, times(2)).onSubscribe((Subscription)any()); + verify(spiedSubscriberAfterConnect, times(2)).onSubscribe((Subscription)any()); + + // verify interactions + verify(sourceNext, times(1)).accept(1); + verify(sourceNext, times(1)).accept(2); + verify(sourceCompleted, times(1)).run(); + verifyObserverMock(spiedSubscriberBeforeConnect, 2, 4); + verifyObserverMock(spiedSubscriberAfterConnect, 2, 4); + + verify(sourceUnsubscribed, never()).run(); + + verifyNoMoreInteractions(sourceNext); + verifyNoMoreInteractions(sourceCompleted); + verifyNoMoreInteractions(sourceUnsubscribed); + verifyNoMoreInteractions(spiedSubscriberBeforeConnect); + verifyNoMoreInteractions(spiedSubscriberAfterConnect); + + } + + /** + * Specifically test interaction with a Scheduler with subscribeOn. + * + * @throws Throwable functional interfaces declare throws Exception + */ + @SuppressWarnings("unchecked") + @Test + public void issue2191_SchedulerUnsubscribe() throws Throwable { + // setup mocks + Consumer<Integer> sourceNext = mock(Consumer.class); + Action sourceCompleted = mock(Action.class); + Action sourceUnsubscribed = mock(Action.class); + final Scheduler mockScheduler = mock(Scheduler.class); + final Disposable mockSubscription = mock(Disposable.class); + Worker spiedWorker = workerSpy(mockSubscription); + Subscriber<Integer> mockObserverBeforeConnect = TestHelper.mockSubscriber(); + Subscriber<Integer> mockObserverAfterConnect = TestHelper.mockSubscriber(); + + when(mockScheduler.createWorker()).thenReturn(spiedWorker); + + // Flowable under test + ConnectableFlowable<Integer> replay = Flowable.just(1, 2, 3) + .doOnNext(sourceNext) + .doOnCancel(sourceUnsubscribed) + .doOnComplete(sourceCompleted) + .subscribeOn(mockScheduler).replay(); + + replay.subscribe(mockObserverBeforeConnect); + replay.subscribe(mockObserverBeforeConnect); + replay.connect(); + replay.subscribe(mockObserverAfterConnect); + replay.subscribe(mockObserverAfterConnect); + + verify(mockObserverBeforeConnect, times(2)).onSubscribe((Subscription)any()); + verify(mockObserverAfterConnect, times(2)).onSubscribe((Subscription)any()); + + // verify interactions + verify(sourceNext, times(1)).accept(1); + verify(sourceNext, times(1)).accept(2); + verify(sourceNext, times(1)).accept(3); + verify(sourceCompleted, times(1)).run(); + verify(mockScheduler, times(1)).createWorker(); + verify(spiedWorker, times(1)).schedule((Runnable)notNull()); + verifyObserverMock(mockObserverBeforeConnect, 2, 6); + verifyObserverMock(mockObserverAfterConnect, 2, 6); + + // FIXME publish calls cancel too + verify(spiedWorker, times(1)).dispose(); + verify(sourceUnsubscribed, never()).run(); + + verifyNoMoreInteractions(sourceNext); + verifyNoMoreInteractions(sourceCompleted); + verifyNoMoreInteractions(sourceUnsubscribed); + verifyNoMoreInteractions(spiedWorker); + verifyNoMoreInteractions(mockSubscription); + verifyNoMoreInteractions(mockScheduler); + verifyNoMoreInteractions(mockObserverBeforeConnect); + verifyNoMoreInteractions(mockObserverAfterConnect); + } + + /** + * Specifically test interaction with a Scheduler with subscribeOn. + * + * @throws Throwable functional interfaces declare throws Exception + */ + @SuppressWarnings("unchecked") + @Test + public void issue2191_SchedulerUnsubscribeOnError() throws Throwable { + // setup mocks + Consumer<Integer> sourceNext = mock(Consumer.class); + Action sourceCompleted = mock(Action.class); + Consumer<Throwable> sourceError = mock(Consumer.class); + Action sourceUnsubscribed = mock(Action.class); + final Scheduler mockScheduler = mock(Scheduler.class); + final Disposable mockSubscription = mock(Disposable.class); + Worker spiedWorker = workerSpy(mockSubscription); + Subscriber<Integer> mockObserverBeforeConnect = TestHelper.mockSubscriber(); + Subscriber<Integer> mockObserverAfterConnect = TestHelper.mockSubscriber(); + + when(mockScheduler.createWorker()).thenReturn(spiedWorker); + + // Flowable under test + Function<Integer, Integer> mockFunc = mock(Function.class); + IllegalArgumentException illegalArgumentException = new IllegalArgumentException(); + when(mockFunc.apply(1)).thenReturn(1); + when(mockFunc.apply(2)).thenThrow(illegalArgumentException); + ConnectableFlowable<Integer> replay = Flowable.just(1, 2, 3).map(mockFunc) + .doOnNext(sourceNext) + .doOnCancel(sourceUnsubscribed) + .doOnComplete(sourceCompleted) + .doOnError(sourceError) + .subscribeOn(mockScheduler).replay(); + + replay.subscribe(mockObserverBeforeConnect); + replay.subscribe(mockObserverBeforeConnect); + replay.connect(); + replay.subscribe(mockObserverAfterConnect); + replay.subscribe(mockObserverAfterConnect); + + verify(mockObserverBeforeConnect, times(2)).onSubscribe((Subscription)any()); + verify(mockObserverAfterConnect, times(2)).onSubscribe((Subscription)any()); + + // verify interactions + verify(mockScheduler, times(1)).createWorker(); + verify(spiedWorker, times(1)).schedule((Runnable)notNull()); + verify(sourceNext, times(1)).accept(1); + verify(sourceError, times(1)).accept(illegalArgumentException); + verifyObserver(mockObserverBeforeConnect, 2, 2, illegalArgumentException); + verifyObserver(mockObserverAfterConnect, 2, 2, illegalArgumentException); + + // FIXME publish also calls cancel + verify(spiedWorker, times(1)).dispose(); + verify(sourceUnsubscribed, never()).run(); + + verifyNoMoreInteractions(sourceNext); + verifyNoMoreInteractions(sourceCompleted); + verifyNoMoreInteractions(sourceError); + verifyNoMoreInteractions(sourceUnsubscribed); + verifyNoMoreInteractions(spiedWorker); + verifyNoMoreInteractions(mockSubscription); + verifyNoMoreInteractions(mockScheduler); + verifyNoMoreInteractions(mockObserverBeforeConnect); + verifyNoMoreInteractions(mockObserverAfterConnect); + } + + private static void verifyObserverMock(Subscriber<Integer> mock, int numSubscriptions, int numItemsExpected) { + verify(mock, times(numItemsExpected)).onNext((Integer) notNull()); + verify(mock, times(numSubscriptions)).onComplete(); + verifyNoMoreInteractions(mock); + } + + private static void verifyObserver(Subscriber<Integer> mock, int numSubscriptions, int numItemsExpected, Throwable error) { + verify(mock, times(numItemsExpected)).onNext((Integer) notNull()); + verify(mock, times(numSubscriptions)).onError(error); + verifyNoMoreInteractions(mock); + } + + public static Worker workerSpy(final Disposable mockDisposable) { + return spy(new InprocessWorker(mockDisposable)); + } + + private static class InprocessWorker extends Worker { + private final Disposable mockDisposable; + public boolean unsubscribed; + + InprocessWorker(Disposable mockDisposable) { + this.mockDisposable = mockDisposable; + } + + @NonNull + @Override + public Disposable schedule(@NonNull Runnable action) { + action.run(); + return mockDisposable; // this subscription is returned but discarded + } + + @NonNull + @Override + public Disposable schedule(@NonNull Runnable action, long delayTime, @NonNull TimeUnit unit) { + action.run(); + return mockDisposable; + } + + @Override + public void dispose() { + unsubscribed = true; + } + + @Override + public boolean isDisposed() { + return unsubscribed; + } + } + + @Test + public void boundedReplayBuffer() { + BoundedReplayBuffer<Integer> buf = new BoundedReplayBuffer<Integer>(false) { + private static final long serialVersionUID = -9081211580719235896L; + + @Override + void truncate() { + } + }; + + buf.addLast(new Node(1, 0)); + buf.addLast(new Node(2, 1)); + buf.addLast(new Node(3, 2)); + buf.addLast(new Node(4, 3)); + buf.addLast(new Node(5, 4)); + + List<Integer> values = new ArrayList<>(); + buf.collect(values); + + Assert.assertEquals(Arrays.asList(1, 2, 3, 4, 5), values); + + buf.removeSome(2); + buf.removeFirst(); + buf.removeSome(2); + + values.clear(); + buf.collect(values); + Assert.assertTrue(values.isEmpty()); + + buf.addLast(new Node(5, 5)); + buf.addLast(new Node(6, 6)); + buf.collect(values); + + Assert.assertEquals(Arrays.asList(5, 6), values); + + } + + @Test(expected = IllegalStateException.class) + public void boundedRemoveFirstOneItemOnly() { + BoundedReplayBuffer<Integer> buf = new BoundedReplayBuffer<Integer>(false) { + private static final long serialVersionUID = -9081211580719235896L; + + @Override + void truncate() { + } + }; + + buf.removeFirst(); + } + + @Test + public void timedAndSizedTruncation() { + TestScheduler test = new TestScheduler(); + SizeAndTimeBoundReplayBuffer<Integer> buf = new SizeAndTimeBoundReplayBuffer<>(2, 2000, TimeUnit.MILLISECONDS, test, false); + List<Integer> values = new ArrayList<>(); + + buf.next(1); + test.advanceTimeBy(1, TimeUnit.SECONDS); + buf.next(2); + test.advanceTimeBy(1, TimeUnit.SECONDS); + buf.collect(values); + Assert.assertEquals(Arrays.asList(2), values); + + buf.next(3); + buf.next(4); + values.clear(); + buf.collect(values); + Assert.assertEquals(Arrays.asList(3, 4), values); + + test.advanceTimeBy(2, TimeUnit.SECONDS); + buf.next(5); + + values.clear(); + buf.collect(values); + Assert.assertEquals(Arrays.asList(5), values); + + test.advanceTimeBy(2, TimeUnit.SECONDS); + buf.complete(); + + values.clear(); + buf.collect(values); + Assert.assertTrue(values.isEmpty()); + + Assert.assertEquals(1, buf.size); + Assert.assertTrue(buf.hasCompleted()); + } + + @Test + public void backpressure() { + final AtomicLong requested = new AtomicLong(); + Flowable<Integer> source = Flowable.range(1, 1000) + .doOnRequest(new LongConsumer() { + @Override + public void accept(long t) { + requested.addAndGet(t); + } + }); + ConnectableFlowable<Integer> cf = source.replay(); + + TestSubscriberEx<Integer> ts1 = new TestSubscriberEx<>(10L); + TestSubscriberEx<Integer> ts2 = new TestSubscriberEx<>(90L); + + cf.subscribe(ts1); + cf.subscribe(ts2); + + ts2.request(10); + + cf.connect(); + + ts1.assertValueCount(10); + ts1.assertNotTerminated(); + + ts2.assertValueCount(100); + ts2.assertNotTerminated(); + + Assert.assertEquals(100, requested.get()); + } + + @Test + public void backpressureBounded() { + final AtomicLong requested = new AtomicLong(); + Flowable<Integer> source = Flowable.range(1, 1000) + .doOnRequest(new LongConsumer() { + @Override + public void accept(long t) { + requested.addAndGet(t); + } + }); + ConnectableFlowable<Integer> cf = source.replay(50); + + TestSubscriberEx<Integer> ts1 = new TestSubscriberEx<>(10L); + TestSubscriberEx<Integer> ts2 = new TestSubscriberEx<>(90L); + + cf.subscribe(ts1); + cf.subscribe(ts2); + + ts2.request(10); + + cf.connect(); + + ts1.assertValueCount(10); + ts1.assertNotTerminated(); + + ts2.assertValueCount(100); + ts2.assertNotTerminated(); + + Assert.assertEquals(100, requested.get()); + } + + @Test + public void coldReplayNoBackpressure() { + Flowable<Integer> source = Flowable.range(0, 1000).replay().autoConnect(); + + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(); + + source.subscribe(ts); + + ts.assertNoErrors(); + ts.assertTerminated(); + List<Integer> onNextEvents = ts.values(); + assertEquals(1000, onNextEvents.size()); + + for (int i = 0; i < 1000; i++) { + assertEquals((Integer)i, onNextEvents.get(i)); + } + } + + @Test + public void coldReplayBackpressure() { + Flowable<Integer> source = Flowable.range(0, 1000).replay().autoConnect(); + + TestSubscriber<Integer> ts = new TestSubscriber<>(0L); + ts.request(10); + + source.subscribe(ts); + + ts.assertNoErrors(); + ts.assertNotComplete(); + List<Integer> onNextEvents = ts.values(); + assertEquals(10, onNextEvents.size()); + + for (int i = 0; i < 10; i++) { + assertEquals((Integer)i, onNextEvents.get(i)); + } + + ts.cancel(); + } + + @Test + public void cache() throws InterruptedException { + final AtomicInteger counter = new AtomicInteger(); + Flowable<String> f = Flowable.unsafeCreate(new Publisher<String>() { + + @Override + public void subscribe(final Subscriber<? super String> subscriber) { + subscriber.onSubscribe(new BooleanSubscription()); + new Thread(new Runnable() { + + @Override + public void run() { + counter.incrementAndGet(); + System.out.println("published observable being executed"); + subscriber.onNext("one"); + subscriber.onComplete(); + } + }).start(); + } + }).replay().autoConnect(); + + // we then expect the following 2 subscriptions to get that same value + final CountDownLatch latch = new CountDownLatch(2); + + // subscribe once + f.subscribe(new Consumer<String>() { + + @Override + public void accept(String v) { + assertEquals("one", v); + System.out.println("v: " + v); + latch.countDown(); + } + }); + + // subscribe again + f.subscribe(new Consumer<String>() { + + @Override + public void accept(String v) { + assertEquals("one", v); + System.out.println("v: " + v); + latch.countDown(); + } + }); + + if (!latch.await(1000, TimeUnit.MILLISECONDS)) { + fail("subscriptions did not receive values"); + } + assertEquals(1, counter.get()); + } + + @Test + public void unsubscribeSource() throws Throwable { + Action unsubscribe = mock(Action.class); + Flowable<Integer> f = Flowable.just(1).doOnCancel(unsubscribe).replay().autoConnect(); + f.subscribe(); + f.subscribe(); + f.subscribe(); + verify(unsubscribe, never()).run(); + } + + @Test + public void take() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(); + + Flowable<Integer> cached = Flowable.range(1, 100).replay().autoConnect(); + cached + .take(10) + .subscribe(ts); + + ts.assertNoErrors(); + ts.assertTerminated(); + ts.assertValues(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + } + + @Test + public void async() { + Flowable<Integer> source = Flowable.range(1, 10000); + for (int i = 0; i < 100; i++) { + TestSubscriberEx<Integer> ts1 = new TestSubscriberEx<>(); + + Flowable<Integer> cached = source.replay().autoConnect(); + + cached.observeOn(Schedulers.computation()).subscribe(ts1); + + ts1.awaitDone(2, TimeUnit.SECONDS); + ts1.assertNoErrors(); + ts1.assertTerminated(); + assertEquals(10000, ts1.values().size()); + + TestSubscriberEx<Integer> ts2 = new TestSubscriberEx<>(); + cached.observeOn(Schedulers.computation()).subscribe(ts2); + + ts2.awaitDone(2, TimeUnit.SECONDS); + ts2.assertNoErrors(); + ts2.assertTerminated(); + assertEquals(10000, ts2.values().size()); + } + } + + @Test + public void asyncComeAndGo() { + Flowable<Long> source = Flowable.interval(1, 1, TimeUnit.MILLISECONDS) + .take(1000) + .subscribeOn(Schedulers.io()); + Flowable<Long> cached = source.replay().autoConnect(); + + Flowable<Long> output = cached.observeOn(Schedulers.computation(), false, 1024); + + List<TestSubscriberEx<Long>> list = new ArrayList<>(100); + for (int i = 0; i < 100; i++) { + TestSubscriberEx<Long> ts = new TestSubscriberEx<>(); + list.add(ts); + output.skip(i * 10).take(10).subscribe(ts); + } + + List<Long> expected = new ArrayList<>(); + for (int i = 0; i < 10; i++) { + expected.add((long)(i - 10)); + } + int j = 0; + for (TestSubscriberEx<Long> ts : list) { + ts.awaitDone(3, TimeUnit.SECONDS); + ts.assertNoErrors(); + ts.assertTerminated(); + + for (int i = j * 10; i < j * 10 + 10; i++) { + expected.set(i - j * 10, (long)i); + } + + ts.assertValueSequence(expected); + + j++; + } + } + + @Test + public void noMissingBackpressureException() { + final int m = 4 * 1000 * 1000; + Flowable<Integer> firehose = Flowable.unsafeCreate(new Publisher<Integer>() { + @Override + public void subscribe(Subscriber<? super Integer> t) { + t.onSubscribe(new BooleanSubscription()); + for (int i = 0; i < m; i++) { + t.onNext(i); + } + t.onComplete(); + } + }); + + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(); + firehose.replay().autoConnect().observeOn(Schedulers.computation()).takeLast(100).subscribe(ts); + + ts.awaitDone(3, TimeUnit.SECONDS); + ts.assertNoErrors(); + ts.assertTerminated(); + + assertEquals(100, ts.values().size()); + } + + @Test + public void valuesAndThenError() { + Flowable<Integer> source = Flowable.range(1, 10) + .concatWith(Flowable.<Integer>error(new TestException())) + .replay().autoConnect(); + + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(); + source.subscribe(ts); + + ts.assertValues(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + ts.assertNotComplete(); + Assert.assertEquals(1, ts.errors().size()); + + TestSubscriberEx<Integer> ts2 = new TestSubscriberEx<>(); + source.subscribe(ts2); + + ts2.assertValues(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + ts2.assertNotComplete(); + Assert.assertEquals(1, ts2.errors().size()); + } + + @Test + public void unsafeChildOnNextThrows() { + final AtomicInteger count = new AtomicInteger(); + + Flowable<Integer> source = Flowable.range(1, 100) + .doOnNext(new Consumer<Integer>() { + @Override + public void accept(Integer t) { + count.getAndIncrement(); + } + }) + .replay().autoConnect(); + + TestSubscriber<Integer> ts = new TestSubscriber<Integer>() { + @Override + public void onNext(Integer t) { + throw new TestException(); + } + }; + + source.subscribe(ts); + + Assert.assertEquals(100, count.get()); + + ts.assertNoValues(); + ts.assertNotComplete(); + ts.assertError(TestException.class); + } + + @Test + public void unsafeChildOnErrorThrows() throws Throwable { + TestHelper.withErrorTracking(errors -> { + Flowable<Integer> source = Flowable.<Integer>error(new IOException()) + .replay() + .autoConnect(); + + TestSubscriber<Integer> ts = new TestSubscriber<Integer>() { + @Override + public void onError(Throwable t) { + super.onError(t); + throw new TestException(); + } + }; + + source.subscribe(ts); + + ts.assertFailure(IOException.class); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + }); + } + + @Test + public void unsafeChildOnCompleteThrows() throws Throwable { + TestHelper.withErrorTracking(errors -> { + Flowable<Integer> source = Flowable.<Integer>empty() + .replay() + .autoConnect(); + + TestSubscriber<Integer> ts = new TestSubscriber<Integer>() { + @Override + public void onComplete() { + super.onComplete(); + throw new TestException(); + } + }; + + source.subscribe(ts); + + ts.assertResult(); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + }); + } + + @Test + public void unboundedLeavesEarly() { + PublishProcessor<Integer> source = PublishProcessor.create(); + + final List<Long> requests = new ArrayList<>(); + + Flowable<Integer> out = source + .doOnRequest(new LongConsumer() { + @Override + public void accept(long t) { + requests.add(t); + } + }).replay().autoConnect(); + + TestSubscriber<Integer> ts1 = new TestSubscriber<>(5L); + TestSubscriber<Integer> ts2 = new TestSubscriber<>(10L); + + out.subscribe(ts1); + out.subscribe(ts2); + ts2.cancel(); + + Assert.assertEquals(Arrays.asList(5L, 5L), requests); + } + + @Test + public void subscribersComeAndGoAtRequestBoundaries() { + ConnectableFlowable<Integer> source = Flowable.range(1, 10).replay(1); + source.connect(); + + TestSubscriber<Integer> ts1 = new TestSubscriber<>(2L); + + source.subscribe(ts1); + + ts1.assertValues(1, 2); + ts1.assertNoErrors(); + ts1.cancel(); + + TestSubscriber<Integer> ts2 = new TestSubscriber<>(2L); + + source.subscribe(ts2); + + ts2.assertValues(2, 3); + ts2.assertNoErrors(); + ts2.cancel(); + + TestSubscriber<Integer> ts21 = new TestSubscriber<>(1L); + + source.subscribe(ts21); + + ts21.assertValues(3); + ts21.assertNoErrors(); + ts21.cancel(); + + TestSubscriber<Integer> ts22 = new TestSubscriber<>(1L); + + source.subscribe(ts22); + + ts22.assertValues(3); + ts22.assertNoErrors(); + ts22.cancel(); + + TestSubscriber<Integer> ts3 = new TestSubscriber<>(); + + source.subscribe(ts3); + + ts3.assertNoErrors(); + System.out.println(ts3.values()); + ts3.assertValues(3, 4, 5, 6, 7, 8, 9, 10); + ts3.assertComplete(); + } + + @Test + public void subscribersComeAndGoAtRequestBoundaries2() { + ConnectableFlowable<Integer> source = Flowable.range(1, 10).replay(2); + source.connect(); + + TestSubscriber<Integer> ts1 = new TestSubscriber<>(2L); + + source.subscribe(ts1); + + ts1.assertValues(1, 2); + ts1.assertNoErrors(); + ts1.cancel(); + + TestSubscriber<Integer> ts11 = new TestSubscriber<>(2L); + + source.subscribe(ts11); + + ts11.assertValues(1, 2); + ts11.assertNoErrors(); + ts11.cancel(); + + TestSubscriber<Integer> ts2 = new TestSubscriber<>(3L); + + source.subscribe(ts2); + + ts2.assertValues(1, 2, 3); + ts2.assertNoErrors(); + ts2.cancel(); + + TestSubscriber<Integer> ts21 = new TestSubscriber<>(1L); + + source.subscribe(ts21); + + ts21.assertValues(2); + ts21.assertNoErrors(); + ts21.cancel(); + + TestSubscriber<Integer> ts22 = new TestSubscriber<>(1L); + + source.subscribe(ts22); + + ts22.assertValues(2); + ts22.assertNoErrors(); + ts22.cancel(); + + TestSubscriber<Integer> ts3 = new TestSubscriber<>(); + + source.subscribe(ts3); + + ts3.assertNoErrors(); + System.out.println(ts3.values()); + ts3.assertValues(2, 3, 4, 5, 6, 7, 8, 9, 10); + ts3.assertComplete(); + } + + @Test + public void replayTime() { + Flowable.just(1).replay(1, TimeUnit.MINUTES) + .autoConnect() + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(1); + } + + @Test + public void replaySizeAndTime() { + Flowable.just(1).replay(1, 1, TimeUnit.MILLISECONDS) + .autoConnect() + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(1); + } + + @Test + public void replaySelectorTime() { + Flowable.just(1).replay(Functions.<Flowable<Integer>>identity(), 1, TimeUnit.MINUTES) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(1); + } + + @Test + public void source() { + Flowable<Integer> source = Flowable.range(1, 3); + + assertSame(source, (((HasUpstreamPublisher<?>)source.replay())).source()); + } + + @Test + public void connectRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final ConnectableFlowable<Integer> cf = Flowable.range(1, 3).replay(); + + Runnable r = new Runnable() { + @Override + public void run() { + cf.connect(); + } + }; + + TestHelper.race(r, r); + } + } + + @Test + public void subscribeRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final ConnectableFlowable<Integer> cf = Flowable.range(1, 3).replay(); + + final TestSubscriber<Integer> ts1 = new TestSubscriber<>(); + final TestSubscriber<Integer> ts2 = new TestSubscriber<>(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + cf.subscribe(ts1); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + cf.subscribe(ts2); + } + }; + + TestHelper.race(r1, r2); + } + } + + @Test + public void addRemoveRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final ConnectableFlowable<Integer> cf = Flowable.range(1, 3).replay(); + + final TestSubscriber<Integer> ts1 = new TestSubscriber<>(); + final TestSubscriber<Integer> ts2 = new TestSubscriber<>(); + + cf.subscribe(ts1); + + Runnable r1 = new Runnable() { + @Override + public void run() { + ts1.cancel(); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + cf.subscribe(ts2); + } + }; + + TestHelper.race(r1, r2); + } + } + + @Test + public void cancelOnArrival() { + Flowable.range(1, 2) + .replay(Integer.MAX_VALUE) + .autoConnect() + .test(Long.MAX_VALUE, true) + .assertEmpty(); + } + + @Test + public void cancelOnArrival2() { + ConnectableFlowable<Integer> cf = PublishProcessor.<Integer>create() + .replay(Integer.MAX_VALUE); + + cf.test(); + + cf + .autoConnect() + .test(Long.MAX_VALUE, true) + .assertEmpty(); + } + + @Test + public void connectConsumerThrows() { + ConnectableFlowable<Integer> cf = Flowable.range(1, 2) + .replay(); + + try { + cf.connect(new Consumer<Disposable>() { + @Override + public void accept(Disposable t) throws Exception { + throw new TestException(); + } + }); + fail("Should have thrown"); + } catch (TestException ex) { + // expected + } + + cf.test().assertEmpty().cancel(); + + cf.connect(); + + cf.test().assertResult(1, 2); + } + + @Test + public void badSource() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + new Flowable<Integer>() { + @Override + protected void subscribeActual(Subscriber<? super Integer> subscriber) { + subscriber.onSubscribe(new BooleanSubscription()); + subscriber.onError(new TestException("First")); + subscriber.onNext(1); + subscriber.onError(new TestException("Second")); + subscriber.onComplete(); + } + }.replay() + .autoConnect() + .to(TestHelper.<Integer>testConsumer()) + .assertFailureAndMessage(TestException.class, "First"); + + TestHelper.assertUndeliverable(errors, 0, TestException.class, "Second"); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void subscribeOnNextRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final PublishProcessor<Integer> pp = PublishProcessor.create(); + + final ConnectableFlowable<Integer> cf = pp.replay(); + + final TestSubscriber<Integer> ts1 = new TestSubscriber<>(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + cf.subscribe(ts1); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + for (int j = 0; j < 1000; j++) { + pp.onNext(j); + } + } + }; + + TestHelper.race(r1, r2); + } + } + + @Test + public void unsubscribeOnNextRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final PublishProcessor<Integer> pp = PublishProcessor.create(); + + final ConnectableFlowable<Integer> cf = pp.replay(); + + final TestSubscriber<Integer> ts1 = new TestSubscriber<>(); + + cf.subscribe(ts1); + + Runnable r1 = new Runnable() { + @Override + public void run() { + ts1.cancel(); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + for (int j = 0; j < 1000; j++) { + pp.onNext(j); + } + } + }; + + TestHelper.race(r1, r2); + } + } + + @Test + public void unsubscribeReplayRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final ConnectableFlowable<Integer> cf = Flowable.range(1, 1000).replay(); + + final TestSubscriber<Integer> ts1 = new TestSubscriber<>(); + + cf.connect(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + cf.subscribe(ts1); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + ts1.cancel(); + } + }; + + TestHelper.race(r1, r2); + } + } + + @Test + public void reentrantOnNext() { + final PublishProcessor<Integer> pp = PublishProcessor.create(); + + TestSubscriber<Integer> ts = new TestSubscriber<Integer>() { + @Override + public void onNext(Integer t) { + if (t == 1) { + pp.onNext(2); + pp.onComplete(); + } + super.onNext(t); + } + }; + + pp.replay().autoConnect().subscribe(ts); + + pp.onNext(1); + + ts.assertResult(1, 2); + } + + @Test + public void reentrantOnNextBound() { + final PublishProcessor<Integer> pp = PublishProcessor.create(); + + TestSubscriber<Integer> ts = new TestSubscriber<Integer>() { + @Override + public void onNext(Integer t) { + if (t == 1) { + pp.onNext(2); + pp.onComplete(); + } + super.onNext(t); + } + }; + + pp.replay(10).autoConnect().subscribe(ts); + + pp.onNext(1); + + ts.assertResult(1, 2); + } + + @Test + public void reentrantOnNextCancel() { + final PublishProcessor<Integer> pp = PublishProcessor.create(); + + TestSubscriber<Integer> ts = new TestSubscriber<Integer>() { + @Override + public void onNext(Integer t) { + if (t == 1) { + pp.onNext(2); + cancel(); + } + super.onNext(t); + } + }; + + pp.replay().autoConnect().subscribe(ts); + + pp.onNext(1); + + ts.assertValues(1); + } + + @Test + public void reentrantOnNextCancelBounded() { + final PublishProcessor<Integer> pp = PublishProcessor.create(); + + TestSubscriber<Integer> ts = new TestSubscriber<Integer>() { + @Override + public void onNext(Integer t) { + if (t == 1) { + pp.onNext(2); + cancel(); + } + super.onNext(t); + } + }; + + pp.replay(10).autoConnect().subscribe(ts); + + pp.onNext(1); + + ts.assertValues(1); + } + + @Test + public void replayMaxInt() { + Flowable.range(1, 2) + .replay(Integer.MAX_VALUE) + .autoConnect() + .test() + .assertResult(1, 2); + } + + @Test + public void timedAndSizedTruncationError() { + TestScheduler test = new TestScheduler(); + SizeAndTimeBoundReplayBuffer<Integer> buf = new SizeAndTimeBoundReplayBuffer<>(2, 2000, TimeUnit.MILLISECONDS, test, false); + + Assert.assertFalse(buf.hasCompleted()); + Assert.assertFalse(buf.hasError()); + + List<Integer> values = new ArrayList<>(); + + buf.next(1); + test.advanceTimeBy(1, TimeUnit.SECONDS); + buf.next(2); + test.advanceTimeBy(1, TimeUnit.SECONDS); + buf.collect(values); + Assert.assertEquals(Arrays.asList(2), values); + + buf.next(3); + buf.next(4); + values.clear(); + buf.collect(values); + Assert.assertEquals(Arrays.asList(3, 4), values); + + test.advanceTimeBy(2, TimeUnit.SECONDS); + buf.next(5); + + values.clear(); + buf.collect(values); + Assert.assertEquals(Arrays.asList(5), values); + Assert.assertFalse(buf.hasCompleted()); + Assert.assertFalse(buf.hasError()); + + test.advanceTimeBy(2, TimeUnit.SECONDS); + buf.error(new TestException()); + + values.clear(); + buf.collect(values); + Assert.assertTrue(values.isEmpty()); + + Assert.assertEquals(1, buf.size); + Assert.assertFalse(buf.hasCompleted()); + Assert.assertTrue(buf.hasError()); + } + + @Test + public void sizedTruncation() { + SizeBoundReplayBuffer<Integer> buf = new SizeBoundReplayBuffer<>(2, false); + List<Integer> values = new ArrayList<>(); + + buf.next(1); + buf.next(2); + buf.collect(values); + Assert.assertEquals(Arrays.asList(1, 2), values); + + buf.next(3); + buf.next(4); + values.clear(); + buf.collect(values); + Assert.assertEquals(Arrays.asList(3, 4), values); + + buf.next(5); + + values.clear(); + buf.collect(values); + Assert.assertEquals(Arrays.asList(4, 5), values); + Assert.assertFalse(buf.hasCompleted()); + + buf.complete(); + + values.clear(); + buf.collect(values); + Assert.assertEquals(Arrays.asList(4, 5), values); + + Assert.assertEquals(3, buf.size); + Assert.assertTrue(buf.hasCompleted()); + Assert.assertFalse(buf.hasError()); + } + + @Test + public void delayedUpstreamOnSubscribe() { + final Subscriber<?>[] sub = { null }; + + new Flowable<Integer>() { + @Override + protected void subscribeActual(Subscriber<? super Integer> s) { + sub[0] = s; + } + } + .replay() + .connect() + .dispose(); + + BooleanSubscription bs = new BooleanSubscription(); + + sub[0].onSubscribe(bs); + + assertTrue(bs.isCancelled()); + } + + @Test + public void timedNoOutdatedData() { + TestScheduler scheduler = new TestScheduler(); + + Flowable<Integer> source = Flowable.just(1) + .replay(2, TimeUnit.SECONDS, scheduler) + .autoConnect(); + + source.test().assertResult(1); + + source.test().assertResult(1); + + scheduler.advanceTimeBy(3, TimeUnit.SECONDS); + + source.test().assertResult(); + } + + @Test + public void multicastSelectorCallableConnectableCrash() { + FlowableReplay.multicastSelector(new Supplier<ConnectableFlowable<Object>>() { + @Override + public ConnectableFlowable<Object> get() throws Exception { + throw new TestException(); + } + }, Functions.<Flowable<Object>>identity()) + .test() + .assertFailure(TestException.class); + } + + @Test + public void badRequest() { + TestHelper.assertBadRequestReported( + Flowable.never() + .replay() + ); + } + + @Test + public void noHeadRetentionCompleteSize() { + PublishProcessor<Integer> source = PublishProcessor.create(); + + FlowableReplay<Integer> co = (FlowableReplay<Integer>)source + .replay(1); + + // the backpressure coordination would not accept items from source otherwise + co.test(); + + co.connect(); + + BoundedReplayBuffer<Integer> buf = (BoundedReplayBuffer<Integer>)(co.current.get().buffer); + + source.onNext(1); + source.onNext(2); + source.onComplete(); + + assertNull(buf.get().value); + + Object o = buf.get(); + + buf.trimHead(); + + assertSame(o, buf.get()); + } + + @Test + public void noHeadRetentionErrorSize() { + PublishProcessor<Integer> source = PublishProcessor.create(); + + FlowableReplay<Integer> co = (FlowableReplay<Integer>)source + .replay(1); + + co.test(); + + co.connect(); + + BoundedReplayBuffer<Integer> buf = (BoundedReplayBuffer<Integer>)(co.current.get().buffer); + + source.onNext(1); + source.onNext(2); + source.onError(new TestException()); + + assertNull(buf.get().value); + + Object o = buf.get(); + + buf.trimHead(); + + assertSame(o, buf.get()); + } + + @Test + public void noHeadRetentionSize() { + PublishProcessor<Integer> source = PublishProcessor.create(); + + FlowableReplay<Integer> co = (FlowableReplay<Integer>)source + .replay(1); + + co.test(); + + co.connect(); + + BoundedReplayBuffer<Integer> buf = (BoundedReplayBuffer<Integer>)(co.current.get().buffer); + + source.onNext(1); + source.onNext(2); + + assertNotNull(buf.get().value); + + buf.trimHead(); + + assertNull(buf.get().value); + + Object o = buf.get(); + + buf.trimHead(); + + assertSame(o, buf.get()); + } + + @Test + public void noHeadRetentionCompleteTime() { + PublishProcessor<Integer> source = PublishProcessor.create(); + + FlowableReplay<Integer> co = (FlowableReplay<Integer>)source + .replay(1, TimeUnit.MINUTES, Schedulers.computation()); + + co.test(); + + co.connect(); + + BoundedReplayBuffer<Integer> buf = (BoundedReplayBuffer<Integer>)(co.current.get().buffer); + + source.onNext(1); + source.onNext(2); + source.onComplete(); + + assertNull(buf.get().value); + + Object o = buf.get(); + + buf.trimHead(); + + assertSame(o, buf.get()); + } + + @Test + public void noHeadRetentionErrorTime() { + PublishProcessor<Integer> source = PublishProcessor.create(); + + FlowableReplay<Integer> co = (FlowableReplay<Integer>)source + .replay(1, TimeUnit.MINUTES, Schedulers.computation()); + + co.test(); + + co.connect(); + + BoundedReplayBuffer<Integer> buf = (BoundedReplayBuffer<Integer>)(co.current.get().buffer); + + source.onNext(1); + source.onNext(2); + source.onError(new TestException()); + + assertNull(buf.get().value); + + Object o = buf.get(); + + buf.trimHead(); + + assertSame(o, buf.get()); + } + + @Test + public void noHeadRetentionTime() { + TestScheduler sch = new TestScheduler(); + + PublishProcessor<Integer> source = PublishProcessor.create(); + + FlowableReplay<Integer> co = (FlowableReplay<Integer>)source + .replay(1, TimeUnit.MILLISECONDS, sch); + + co.test(); + + co.connect(); + + BoundedReplayBuffer<Integer> buf = (BoundedReplayBuffer<Integer>)(co.current.get().buffer); + + source.onNext(1); + + sch.advanceTimeBy(2, TimeUnit.MILLISECONDS); + + source.onNext(2); + + assertNotNull(buf.get().value); + + buf.trimHead(); + + assertNull(buf.get().value); + + Object o = buf.get(); + + buf.trimHead(); + + assertSame(o, buf.get()); + } + + @Test(expected = TestException.class) + public void createBufferFactoryCrash() { + FlowableReplay.create(Flowable.just(1), new Supplier<ReplayBuffer<Integer>>() { + @Override + public ReplayBuffer<Integer> get() throws Exception { + throw new TestException(); + } + }) + .connect(); + } + + @Test + public void createBufferFactoryCrashOnSubscribe() { + FlowableReplay.create(Flowable.just(1), new Supplier<ReplayBuffer<Integer>>() { + @Override + public ReplayBuffer<Integer> get() throws Exception { + throw new TestException(); + } + }) + .test() + .assertFailure(TestException.class); + } + + @Test + public void noBoundedRetentionViaThreadLocal() throws Exception { + Flowable<byte[]> source = Flowable.range(1, 200) + .map(new Function<Integer, byte[]>() { + @Override + public byte[] apply(Integer v) throws Exception { + return new byte[1024 * 1024]; + } + }) + .replay(new Function<Flowable<byte[]>, Publisher<byte[]>>() { + @Override + public Publisher<byte[]> apply(final Flowable<byte[]> f) throws Exception { + return f.take(1) + .concatMap(new Function<byte[], Publisher<byte[]>>() { + @Override + public Publisher<byte[]> apply(byte[] v) throws Exception { + return f; + } + }); + } + }, 1) + .takeLast(1) + ; + + System.out.println("Bounded Replay Leak check: Wait before GC"); + Thread.sleep(1000); + + System.out.println("Bounded Replay Leak check: GC"); + System.gc(); + + Thread.sleep(500); + + final MemoryMXBean memoryMXBean = ManagementFactory.getMemoryMXBean(); + MemoryUsage memHeap = memoryMXBean.getHeapMemoryUsage(); + long initial = memHeap.getUsed(); + + System.out.printf("Bounded Replay Leak check: Starting: %.3f MB%n", initial / 1024.0 / 1024.0); + + final AtomicLong after = new AtomicLong(); + + source.subscribe(new Consumer<byte[]>() { + @Override + public void accept(byte[] v) throws Exception { + System.out.println("Bounded Replay Leak check: Wait before GC 2"); + Thread.sleep(1000); + + System.out.println("Bounded Replay Leak check: GC 2"); + System.gc(); + + Thread.sleep(500); + + after.set(memoryMXBean.getHeapMemoryUsage().getUsed()); + } + }); + + System.out.printf("Bounded Replay Leak check: After: %.3f MB%n", after.get() / 1024.0 / 1024.0); + + if (initial + 100 * 1024 * 1024 < after.get()) { + Assert.fail("Bounded Replay Leak check: Memory leak detected: " + (initial / 1024.0 / 1024.0) + + " -> " + after.get() / 1024.0 / 1024.0); + } + } + + @Test + public void unsafeChildOnNextThrowsSizeBound() { + final AtomicInteger count = new AtomicInteger(); + + Flowable<Integer> source = Flowable.range(1, 100) + .doOnNext(new Consumer<Integer>() { + @Override + public void accept(Integer t) { + count.getAndIncrement(); + } + }) + .replay(1000).autoConnect(); + + TestSubscriber<Integer> ts = new TestSubscriber<Integer>() { + @Override + public void onNext(Integer t) { + throw new TestException(); + } + }; + + source.subscribe(ts); + + Assert.assertEquals(100, count.get()); + + ts.assertNoValues(); + ts.assertNotComplete(); + ts.assertError(TestException.class); + } + + @Test + public void unsafeChildOnErrorThrowsSizeBound() throws Throwable { + TestHelper.withErrorTracking(errors -> { + Flowable<Integer> source = Flowable.<Integer>error(new IOException()) + .replay(1000) + .autoConnect(); + + TestSubscriber<Integer> ts = new TestSubscriber<Integer>() { + @Override + public void onError(Throwable t) { + super.onError(t); + throw new TestException(); + } + }; + + source.subscribe(ts); + + ts.assertFailure(IOException.class); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + }); + } + + @Test + public void unsafeChildOnCompleteThrowsSizeBound() throws Throwable { + TestHelper.withErrorTracking(errors -> { + Flowable<Integer> source = Flowable.<Integer>empty() + .replay(1000) + .autoConnect(); + + TestSubscriber<Integer> ts = new TestSubscriber<Integer>() { + @Override + public void onComplete() { + super.onComplete(); + throw new TestException(); + } + }; + + source.subscribe(ts); + + ts.assertResult(); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + }); + } + + @Test(expected = TestException.class) + public void connectDisposeCrash() { + ConnectableFlowable<Object> cf = Flowable.never().replay(); + + cf.connect(); + + cf.connect(d -> { throw new TestException(); }); + } + + @Test + public void resetWhileNotConnectedIsNoOp() { + ConnectableFlowable<Object> cf = Flowable.never().replay(); + + cf.reset(); + } + + @Test + public void resetWhileActiveIsNoOp() { + ConnectableFlowable<Object> cf = Flowable.never().replay(); + + cf.connect(); + + cf.reset(); + } + + @Test + public void delayedUpstreamSubscription() { + AtomicReference<Subscriber<? super Integer>> ref = new AtomicReference<>(); + Flowable<Integer> f = Flowable.<Integer>unsafeCreate(ref::set); + + TestSubscriber<Integer> ts = f.replay() + .autoConnect() + .test(); + + AtomicLong requested = new AtomicLong(); + + ref.get().onSubscribe(new Subscription() { + @Override + public void request(long n) { + BackpressureHelper.add(requested, n); + } + + @Override + public void cancel() { + } + }); + + assertEquals(Long.MAX_VALUE, requested.get()); + ref.get().onComplete(); + + ts.assertResult(); + } + + @Test + public void disposeNoNeedForReset() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + + ConnectableFlowable<Integer> cf = pp.replay(); + + TestSubscriber<Integer> ts = cf.test(); + + Disposable d = cf.connect(); + + pp.onNext(1); + + d.dispose(); + + ts = cf.test(); + + ts.assertEmpty(); + + cf.connect(); + + ts.assertEmpty(); + + pp.onNext(2); + + ts.assertValuesOnly(2); + } + + @Test + public void disposeNoNeedForResetSizeBound() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + + ConnectableFlowable<Integer> cf = pp.replay(10); + + TestSubscriber<Integer> ts = cf.test(); + + Disposable d = cf.connect(); + + pp.onNext(1); + + d.dispose(); + + ts = cf.test(); + + ts.assertEmpty(); + + cf.connect(); + + ts.assertEmpty(); + + pp.onNext(2); + + ts.assertValuesOnly(2); + } + + @Test + public void disposeNoNeedForResetTimeBound() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + + ConnectableFlowable<Integer> cf = pp.replay(10, TimeUnit.MINUTES); + + TestSubscriber<Integer> ts = cf.test(); + + Disposable d = cf.connect(); + + pp.onNext(1); + + d.dispose(); + + ts = cf.test(); + + ts.assertEmpty(); + + cf.connect(); + + ts.assertEmpty(); + + pp.onNext(2); + + ts.assertValuesOnly(2); + } + + @Test + public void disposeNoNeedForResetTimeAndSIzeBound() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + + ConnectableFlowable<Integer> cf = pp.replay(10, 10, TimeUnit.MINUTES); + + TestSubscriber<Integer> ts = cf.test(); + + Disposable d = cf.connect(); + + pp.onNext(1); + + d.dispose(); + + ts = cf.test(); + + ts.assertEmpty(); + + cf.connect(); + + ts.assertEmpty(); + + pp.onNext(2); + + ts.assertValuesOnly(2); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableRetryTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableRetryTest.java new file mode 100644 index 0000000000..224ae32d40 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableRetryTest.java @@ -0,0 +1,1286 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +import java.util.*; +import java.util.concurrent.*; +import java.util.concurrent.atomic.*; + +import org.junit.Test; +import org.mockito.*; +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.flowables.GroupedFlowable; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.internal.subscriptions.BooleanSubscription; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.processors.PublishProcessor; +import io.reactivex.rxjava3.schedulers.Schedulers; +import io.reactivex.rxjava3.subscribers.*; +import io.reactivex.rxjava3.testsupport.*; + +public class FlowableRetryTest extends RxJavaTest { + + @Test + public void iterativeBackoff() { + Subscriber<String> consumer = TestHelper.mockSubscriber(); + + Flowable<String> producer = Flowable.unsafeCreate(new Publisher<String>() { + + private AtomicInteger count = new AtomicInteger(4); + long last = System.currentTimeMillis(); + + @Override + public void subscribe(Subscriber<? super String> t1) { + t1.onSubscribe(new BooleanSubscription()); + System.out.println(count.get() + " @ " + String.valueOf(last - System.currentTimeMillis())); + last = System.currentTimeMillis(); + if (count.getAndDecrement() == 0) { + t1.onNext("hello"); + t1.onComplete(); + } else { + t1.onError(new RuntimeException()); + } + } + + }); + TestSubscriber<String> ts = new TestSubscriber<>(consumer); + producer.retryWhen(new Function<Flowable<? extends Throwable>, Flowable<Object>>() { + + @Override + public Flowable<Object> apply(Flowable<? extends Throwable> attempts) { + // Worker w = Schedulers.computation().createWorker(); + return attempts + .map(new Function<Throwable, Tuple>() { + @Override + public Tuple apply(Throwable n) { + return new Tuple(1L, n); + }}) + .scan(new BiFunction<Tuple, Tuple, Tuple>() { + @Override + public Tuple apply(Tuple t, Tuple n) { + return new Tuple(t.count + n.count, n.n); + }}) + .flatMap(new Function<Tuple, Flowable<Object>>() { + @Override + public Flowable<Object> apply(Tuple t) { + System.out.println("Retry # " + t.count); + return t.count > 20 ? + Flowable.<Object>error(t.n) : + Flowable.timer(t.count * 1L, TimeUnit.MILLISECONDS) + .cast(Object.class); + }}); + } + }).subscribe(ts); + ts.awaitDone(5, TimeUnit.SECONDS); + ts.assertNoErrors(); + + InOrder inOrder = inOrder(consumer); + inOrder.verify(consumer, never()).onError(any(Throwable.class)); + inOrder.verify(consumer, times(1)).onNext("hello"); + inOrder.verify(consumer, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + + } + + public static class Tuple { + Long count; + Throwable n; + + Tuple(Long c, Throwable n) { + count = c; + this.n = n; + } + } + + @Test + public void retryIndefinitely() { + Subscriber<String> subscriber = TestHelper.mockSubscriber(); + int numRetries = 20; + Flowable<String> origin = Flowable.unsafeCreate(new FuncWithErrors(numRetries)); + origin.retry().subscribe(new TestSubscriber<>(subscriber)); + + InOrder inOrder = inOrder(subscriber); + // should show 3 attempts + inOrder.verify(subscriber, times(numRetries + 1)).onNext("beginningEveryTime"); + // should have no errors + inOrder.verify(subscriber, never()).onError(any(Throwable.class)); + // should have a single success + inOrder.verify(subscriber, times(1)).onNext("onSuccessOnly"); + // should have a single successful onComplete + inOrder.verify(subscriber, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void schedulingNotificationHandler() { + Subscriber<String> subscriber = TestHelper.mockSubscriber(); + int numRetries = 2; + Flowable<String> origin = Flowable.unsafeCreate(new FuncWithErrors(numRetries)); + TestSubscriber<String> ts = new TestSubscriber<>(subscriber); + origin.retryWhen(new Function<Flowable<? extends Throwable>, Flowable<Object>>() { + @Override + public Flowable<Object> apply(Flowable<? extends Throwable> t1) { + return t1.observeOn(Schedulers.computation()).map(new Function<Throwable, Integer>() { + @Override + public Integer apply(Throwable t1) { + return 1; + } + }).startWithItem(1).cast(Object.class); + } + }) + .doOnError(new Consumer<Throwable>() { + @Override + public void accept(Throwable e) { + e.printStackTrace(); + } + }) + .subscribe(ts); + + ts.awaitDone(5, TimeUnit.SECONDS); + InOrder inOrder = inOrder(subscriber); + // should show 3 attempts + inOrder.verify(subscriber, times(1 + numRetries)).onNext("beginningEveryTime"); + // should have no errors + inOrder.verify(subscriber, never()).onError(any(Throwable.class)); + // should have a single success + inOrder.verify(subscriber, times(1)).onNext("onSuccessOnly"); + // should have a single successful onComplete + inOrder.verify(subscriber, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void onNextFromNotificationHandler() { + Subscriber<String> subscriber = TestHelper.mockSubscriber(); + int numRetries = 2; + Flowable<String> origin = Flowable.unsafeCreate(new FuncWithErrors(numRetries)); + origin.retryWhen(new Function<Flowable<? extends Throwable>, Flowable<Object>>() { + @Override + public Flowable<Object> apply(Flowable<? extends Throwable> t1) { + return t1.map(new Function<Throwable, Integer>() { + + @Override + public Integer apply(Throwable t1) { + return 0; + } + }).startWithItem(0).cast(Object.class); + } + }).subscribe(subscriber); + + InOrder inOrder = inOrder(subscriber); + // should show 3 attempts + inOrder.verify(subscriber, times(numRetries + 1)).onNext("beginningEveryTime"); + // should have no errors + inOrder.verify(subscriber, never()).onError(any(Throwable.class)); + // should have a single success + inOrder.verify(subscriber, times(1)).onNext("onSuccessOnly"); + // should have a single successful onComplete + inOrder.verify(subscriber, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void onCompletedFromNotificationHandler() { + Subscriber<String> subscriber = TestHelper.mockSubscriber(); + Flowable<String> origin = Flowable.unsafeCreate(new FuncWithErrors(1)); + TestSubscriber<String> ts = new TestSubscriber<>(subscriber); + origin.retryWhen(new Function<Flowable<? extends Throwable>, Flowable<Object>>() { + @Override + public Flowable<Object> apply(Flowable<? extends Throwable> t1) { + return Flowable.empty(); + } + }).subscribe(ts); + + InOrder inOrder = inOrder(subscriber); + inOrder.verify(subscriber).onSubscribe((Subscription)notNull()); + inOrder.verify(subscriber, never()).onNext("beginningEveryTime"); + inOrder.verify(subscriber, never()).onNext("onSuccessOnly"); + inOrder.verify(subscriber, times(1)).onComplete(); + inOrder.verify(subscriber, never()).onError(any(Exception.class)); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void onErrorFromNotificationHandler() { + Subscriber<String> subscriber = TestHelper.mockSubscriber(); + Flowable<String> origin = Flowable.unsafeCreate(new FuncWithErrors(2)); + origin.retryWhen(new Function<Flowable<? extends Throwable>, Flowable<Object>>() { + @Override + public Flowable<Object> apply(Flowable<? extends Throwable> t1) { + return Flowable.error(new RuntimeException()); + } + }).subscribe(subscriber); + + InOrder inOrder = inOrder(subscriber); + inOrder.verify(subscriber).onSubscribe((Subscription)notNull()); + inOrder.verify(subscriber, never()).onNext("beginningEveryTime"); + inOrder.verify(subscriber, never()).onNext("onSuccessOnly"); + inOrder.verify(subscriber, never()).onComplete(); + inOrder.verify(subscriber, times(1)).onError(any(RuntimeException.class)); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void singleSubscriptionOnFirst() throws Exception { + final AtomicInteger inc = new AtomicInteger(0); + Publisher<Integer> onSubscribe = new Publisher<Integer>() { + @Override + public void subscribe(Subscriber<? super Integer> subscriber) { + subscriber.onSubscribe(new BooleanSubscription()); + final int emit = inc.incrementAndGet(); + subscriber.onNext(emit); + subscriber.onComplete(); + } + }; + + int first = Flowable.unsafeCreate(onSubscribe) + .retryWhen(new Function<Flowable<? extends Throwable>, Flowable<Object>>() { + @Override + public Flowable<Object> apply(Flowable<? extends Throwable> attempt) { + return attempt.zipWith(Flowable.just(1), new BiFunction<Throwable, Integer, Object>() { + @Override + public Object apply(Throwable o, Integer integer) { + return 0; + } + }); + } + }) + .blockingFirst(); + + assertEquals("Observer did not receive the expected output", 1, first); + assertEquals("Subscribe was not called once", 1, inc.get()); + } + + @Test + public void originFails() { + Subscriber<String> subscriber = TestHelper.mockSubscriber(); + Flowable<String> origin = Flowable.unsafeCreate(new FuncWithErrors(1)); + origin.subscribe(subscriber); + + InOrder inOrder = inOrder(subscriber); + inOrder.verify(subscriber, times(1)).onNext("beginningEveryTime"); + inOrder.verify(subscriber, times(1)).onError(any(RuntimeException.class)); + inOrder.verify(subscriber, never()).onNext("onSuccessOnly"); + inOrder.verify(subscriber, never()).onComplete(); + } + + @Test + public void retryFail() { + int numRetries = 1; + int numFailures = 2; + Subscriber<String> subscriber = TestHelper.mockSubscriber(); + Flowable<String> origin = Flowable.unsafeCreate(new FuncWithErrors(numFailures)); + origin.retry(numRetries).subscribe(subscriber); + + InOrder inOrder = inOrder(subscriber); + // should show 2 attempts (first time fail, second time (1st retry) fail) + inOrder.verify(subscriber, times(1 + numRetries)).onNext("beginningEveryTime"); + // should only retry once, fail again and emit onError + inOrder.verify(subscriber, times(1)).onError(any(RuntimeException.class)); + // no success + inOrder.verify(subscriber, never()).onNext("onSuccessOnly"); + inOrder.verify(subscriber, never()).onComplete(); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void retrySuccess() { + int numFailures = 1; + Subscriber<String> subscriber = TestHelper.mockSubscriber(); + Flowable<String> origin = Flowable.unsafeCreate(new FuncWithErrors(numFailures)); + origin.retry(3).subscribe(subscriber); + + InOrder inOrder = inOrder(subscriber); + // should show 3 attempts + inOrder.verify(subscriber, times(1 + numFailures)).onNext("beginningEveryTime"); + // should have no errors + inOrder.verify(subscriber, never()).onError(any(Throwable.class)); + // should have a single success + inOrder.verify(subscriber, times(1)).onNext("onSuccessOnly"); + // should have a single successful onComplete + inOrder.verify(subscriber, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void infiniteRetry() { + int numFailures = 20; + Subscriber<String> subscriber = TestHelper.mockSubscriber(); + Flowable<String> origin = Flowable.unsafeCreate(new FuncWithErrors(numFailures)); + origin.retry().subscribe(subscriber); + + InOrder inOrder = inOrder(subscriber); + // should show 3 attempts + inOrder.verify(subscriber, times(1 + numFailures)).onNext("beginningEveryTime"); + // should have no errors + inOrder.verify(subscriber, never()).onError(any(Throwable.class)); + // should have a single success + inOrder.verify(subscriber, times(1)).onNext("onSuccessOnly"); + // should have a single successful onComplete + inOrder.verify(subscriber, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + } + + /* + * Checks in a simple and synchronous way that retry resubscribes + * after error. This test fails against 0.16.1-0.17.4, hangs on 0.17.5 and + * passes in 0.17.6 thanks to fix for issue #1027. + */ + @SuppressWarnings("unchecked") + @Test + public void retrySubscribesAgainAfterError() throws Throwable { + + // record emitted values with this action + Consumer<Integer> record = mock(Consumer.class); + InOrder inOrder = inOrder(record); + + // always throw an exception with this action + Consumer<Integer> throwException = mock(Consumer.class); + doThrow(new RuntimeException()).when(throwException).accept(Mockito.anyInt()); + + // create a retrying Flowable based on a PublishProcessor + PublishProcessor<Integer> processor = PublishProcessor.create(); + processor + // record item + .doOnNext(record) + // throw a RuntimeException + .doOnNext(throwException) + // retry on error + .retry() + // subscribe and ignore + .subscribe(); + + inOrder.verifyNoMoreInteractions(); + + processor.onNext(1); + inOrder.verify(record).accept(1); + + processor.onNext(2); + inOrder.verify(record).accept(2); + + processor.onNext(3); + inOrder.verify(record).accept(3); + + inOrder.verifyNoMoreInteractions(); + } + + public static class FuncWithErrors implements Publisher<String> { + + private final int numFailures; + private final AtomicInteger count = new AtomicInteger(0); + + FuncWithErrors(int count) { + this.numFailures = count; + } + + @Override + public void subscribe(final Subscriber<? super String> subscriber) { + subscriber.onSubscribe(new Subscription() { + final AtomicLong req = new AtomicLong(); + // 0 = not set, 1 = fast path, 2 = backpressure + final AtomicInteger path = new AtomicInteger(0); + volatile boolean done; + + @Override + public void request(long n) { + if (n == Long.MAX_VALUE && path.compareAndSet(0, 1)) { + subscriber.onNext("beginningEveryTime"); + int i = count.getAndIncrement(); + if (i < numFailures) { + subscriber.onError(new RuntimeException("forced failure: " + (i + 1))); + } else { + subscriber.onNext("onSuccessOnly"); + subscriber.onComplete(); + } + return; + } + if (n > 0 && req.getAndAdd(n) == 0 && (path.get() == 2 || path.compareAndSet(0, 2)) && !done) { + int i = count.getAndIncrement(); + if (i < numFailures) { + subscriber.onNext("beginningEveryTime"); + subscriber.onError(new RuntimeException("forced failure: " + (i + 1))); + done = true; + } else { + do { + if (i == numFailures) { + subscriber.onNext("beginningEveryTime"); + } else + if (i > numFailures) { + subscriber.onNext("onSuccessOnly"); + subscriber.onComplete(); + done = true; + break; + } + i = count.getAndIncrement(); + } while (req.decrementAndGet() > 0); + } + } + } + + @Override + public void cancel() { + // TODO Auto-generated method stub + + } + }); + } + } + + @Test + public void unsubscribeFromRetry() { + PublishProcessor<Integer> processor = PublishProcessor.create(); + final AtomicInteger count = new AtomicInteger(0); + Disposable sub = processor.retry().subscribe(new Consumer<Integer>() { + @Override + public void accept(Integer n) { + count.incrementAndGet(); + } + }); + processor.onNext(1); + sub.dispose(); + processor.onNext(2); + assertEquals(1, count.get()); + } + + @Test + public void retryAllowsSubscriptionAfterAllSubscriptionsUnsubscribed() throws InterruptedException { + final AtomicInteger subsCount = new AtomicInteger(0); + Publisher<String> onSubscribe = new Publisher<String>() { + @Override + public void subscribe(Subscriber<? super String> s) { + subsCount.incrementAndGet(); + s.onSubscribe(new Subscription() { + + @Override + public void request(long n) { + + } + + @Override + public void cancel() { + subsCount.decrementAndGet(); + } + }); + + } + }; + Flowable<String> stream = Flowable.unsafeCreate(onSubscribe); + Flowable<String> streamWithRetry = stream.retry(); + Disposable sub = streamWithRetry.subscribe(); + assertEquals(1, subsCount.get()); + sub.dispose(); + assertEquals(0, subsCount.get()); + streamWithRetry.subscribe(); + assertEquals(1, subsCount.get()); + } + + @Test + public void sourceFlowableCallsUnsubscribe() throws InterruptedException { + final AtomicInteger subsCount = new AtomicInteger(0); + + final TestSubscriber<String> ts = new TestSubscriber<>(); + + Publisher<String> onSubscribe = new Publisher<String>() { + @Override + public void subscribe(Subscriber<? super String> s) { + BooleanSubscription bs = new BooleanSubscription(); + // if isUnsubscribed is true that means we have a bug such as + // https://github.com/ReactiveX/RxJava/issues/1024 + if (!bs.isCancelled()) { + subsCount.incrementAndGet(); + s.onError(new RuntimeException("failed")); + // it unsubscribes the child directly + // this simulates various error/completion scenarios that could occur + // or just a source that proactively triggers cleanup + // FIXME can't unsubscribe child +// s.unsubscribe(); + bs.cancel(); + } else { + s.onError(new RuntimeException()); + } + } + }; + + Flowable.unsafeCreate(onSubscribe).retry(3).subscribe(ts); + assertEquals(4, subsCount.get()); // 1 + 3 retries + } + + @Test + public void sourceFlowableRetry1() throws InterruptedException { + final AtomicInteger subsCount = new AtomicInteger(0); + + final TestSubscriber<String> ts = new TestSubscriber<>(); + + Publisher<String> onSubscribe = new Publisher<String>() { + @Override + public void subscribe(Subscriber<? super String> s) { + s.onSubscribe(new BooleanSubscription()); + subsCount.incrementAndGet(); + s.onError(new RuntimeException("failed")); + } + }; + + Flowable.unsafeCreate(onSubscribe).retry(1).subscribe(ts); + assertEquals(2, subsCount.get()); + } + + @Test + public void sourceFlowableRetry0() throws InterruptedException { + final AtomicInteger subsCount = new AtomicInteger(0); + + final TestSubscriber<String> ts = new TestSubscriber<>(); + + Publisher<String> onSubscribe = new Publisher<String>() { + @Override + public void subscribe(Subscriber<? super String> s) { + s.onSubscribe(new BooleanSubscription()); + subsCount.incrementAndGet(); + s.onError(new RuntimeException("failed")); + } + }; + + Flowable.unsafeCreate(onSubscribe).retry(0).subscribe(ts); + assertEquals(1, subsCount.get()); + } + + static final class SlowFlowable implements Publisher<Long> { + + final AtomicInteger efforts = new AtomicInteger(0); + final AtomicInteger active = new AtomicInteger(0); + final AtomicInteger maxActive = new AtomicInteger(0); + final AtomicInteger nextBeforeFailure; + final String context; + + private final int emitDelay; + + SlowFlowable(int emitDelay, int countNext, String context) { + this.emitDelay = emitDelay; + this.nextBeforeFailure = new AtomicInteger(countNext); + this.context = context; + } + + @Override + public void subscribe(final Subscriber<? super Long> subscriber) { + final AtomicBoolean terminate = new AtomicBoolean(false); + subscriber.onSubscribe(new Subscription() { + @Override + public void request(long n) { + // TODO Auto-generated method stub + + } + + @Override + public void cancel() { + terminate.set(true); + active.decrementAndGet(); + } + }); + efforts.getAndIncrement(); + active.getAndIncrement(); + maxActive.set(Math.max(active.get(), maxActive.get())); + final Thread thread = new Thread(context) { + @Override + public void run() { + long nr = 0; + try { + while (!terminate.get()) { + Thread.sleep(emitDelay); + if (nextBeforeFailure.getAndDecrement() > 0) { + subscriber.onNext(nr++); + } else { + active.decrementAndGet(); + subscriber.onError(new RuntimeException("expected-failed")); + break; + } + } + } catch (InterruptedException t) { + } + } + }; + thread.start(); + } + } + + /** Observer for listener on separate thread. */ + static final class AsyncSubscriber<T> extends DefaultSubscriber<T> { + + protected CountDownLatch latch = new CountDownLatch(1); + + protected Subscriber<T> target; + + /** + * Wrap existing Observer. + * @param target the target subscriber + */ + AsyncSubscriber(Subscriber<T> target) { + this.target = target; + } + + /** Wait. */ + public void await() { + try { + latch.await(); + } catch (InterruptedException e) { + fail("Test interrupted"); + } + } + + // Observer implementation + + @Override + public void onComplete() { + target.onComplete(); + latch.countDown(); + } + + @Override + public void onError(Throwable t) { + target.onError(t); + latch.countDown(); + } + + @Override + public void onNext(T v) { + target.onNext(v); + } + } + + @Test + public void unsubscribeAfterError() { + + Subscriber<Long> subscriber = TestHelper.mockSubscriber(); + + // Flowable that always fails after 100ms + SlowFlowable so = new SlowFlowable(100, 0, "testUnsubscribeAfterError"); + Flowable<Long> f = Flowable.unsafeCreate(so).retry(5); + + AsyncSubscriber<Long> async = new AsyncSubscriber<>(subscriber); + + f.subscribe(async); + + async.await(); + + InOrder inOrder = inOrder(subscriber); + // Should fail once + inOrder.verify(subscriber, times(1)).onError(any(Throwable.class)); + inOrder.verify(subscriber, never()).onComplete(); + + assertEquals("Start 6 threads, retry 5 then fail on 6", 6, so.efforts.get()); + assertEquals("Only 1 active subscription", 1, so.maxActive.get()); + } + + @Test + public void timeoutWithRetry() { + + Subscriber<Long> subscriber = TestHelper.mockSubscriber(); + + // Flowable that sends every 100ms (timeout fails instead) + SlowFlowable sf = new SlowFlowable(100, 10, "testTimeoutWithRetry"); + Flowable<Long> f = Flowable.unsafeCreate(sf).timeout(80, TimeUnit.MILLISECONDS).retry(5); + + AsyncSubscriber<Long> async = new AsyncSubscriber<>(subscriber); + + f.subscribe(async); + + async.await(); + + InOrder inOrder = inOrder(subscriber); + // Should fail once + inOrder.verify(subscriber, times(1)).onError(any(Throwable.class)); + inOrder.verify(subscriber, never()).onComplete(); + + assertEquals("Start 6 threads, retry 5 then fail on 6", 6, sf.efforts.get()); + } + + @Test + public void retryWithBackpressure() throws InterruptedException { + final int NUM_LOOPS = 1; + for (int j = 0; j < NUM_LOOPS; j++) { + final int numRetries = Flowable.bufferSize() * 2; + for (int i = 0; i < 400; i++) { + Subscriber<String> subscriber = TestHelper.mockSubscriber(); + Flowable<String> origin = Flowable.unsafeCreate(new FuncWithErrors(numRetries)); + TestSubscriberEx<String> ts = new TestSubscriberEx<>(subscriber); + origin.retry().observeOn(Schedulers.computation()).subscribe(ts); + ts.awaitDone(5, TimeUnit.SECONDS); + + InOrder inOrder = inOrder(subscriber); + // should have no errors + verify(subscriber, never()).onError(any(Throwable.class)); + // should show numRetries attempts + inOrder.verify(subscriber, times(numRetries + 1)).onNext("beginningEveryTime"); + // should have a single success + inOrder.verify(subscriber, times(1)).onNext("onSuccessOnly"); + // should have a single successful onComplete + inOrder.verify(subscriber, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + } + } + } + + @Test + public void retryWithBackpressureParallel() throws InterruptedException { + final int NUM_LOOPS = 1; + final int numRetries = Flowable.bufferSize() * 2; + int ncpu = Runtime.getRuntime().availableProcessors(); + ExecutorService exec = Executors.newFixedThreadPool(Math.max(ncpu / 2, 2)); + try { + for (int r = 0; r < NUM_LOOPS; r++) { + if (r % 10 == 0) { + System.out.println("testRetryWithBackpressureParallelLoop -> " + r); + } + + final AtomicInteger timeouts = new AtomicInteger(); + final Map<Integer, List<String>> data = new ConcurrentHashMap<>(); + + int m = 5000; + final CountDownLatch cdl = new CountDownLatch(m); + for (int i = 0; i < m; i++) { + final int j = i; + exec.execute(new Runnable() { + @Override + public void run() { + final AtomicInteger nexts = new AtomicInteger(); + try { + Flowable<String> origin = Flowable.unsafeCreate(new FuncWithErrors(numRetries)); + TestSubscriberEx<String> ts = new TestSubscriberEx<>(); + origin.retry() + .observeOn(Schedulers.computation()).subscribe(ts); + ts.awaitDone(2500, TimeUnit.MILLISECONDS); + List<String> onNextEvents = new ArrayList<>(ts.values()); + if (onNextEvents.size() != numRetries + 2) { + for (Throwable t : ts.errors()) { + onNextEvents.add(t.toString()); + } + for (long err = ts.completions(); err != 0; err--) { + onNextEvents.add("onComplete"); + } + data.put(j, onNextEvents); + } + } catch (Throwable t) { + timeouts.incrementAndGet(); + System.out.println(j + " | " + cdl.getCount() + " !!! " + nexts.get()); + } + cdl.countDown(); + } + }); + } + cdl.await(); + assertEquals(0, timeouts.get()); + if (data.size() > 0) { + fail("Data content mismatch: " + allSequenceFrequency(data)); + } + } + } finally { + exec.shutdown(); + } + } + static <T> StringBuilder allSequenceFrequency(Map<Integer, List<T>> its) { + StringBuilder b = new StringBuilder(); + for (Map.Entry<Integer, List<T>> e : its.entrySet()) { + if (b.length() > 0) { + b.append(", "); + } + b.append(e.getKey()).append("={"); + b.append(sequenceFrequency(e.getValue())); + b.append("}"); + } + return b; + } + static <T> StringBuilder sequenceFrequency(Iterable<T> it) { + StringBuilder sb = new StringBuilder(); + + Object prev = null; + int cnt = 0; + + for (Object curr : it) { + if (sb.length() > 0) { + if (!curr.equals(prev)) { + if (cnt > 1) { + sb.append(" x ").append(cnt); + cnt = 1; + } + sb.append(", "); + sb.append(curr); + } else { + cnt++; + } + } else { + sb.append(curr); + cnt++; + } + prev = curr; + } + if (cnt > 1) { + sb.append(" x ").append(cnt); + } + + return sb; + } + + @Test + public void issue1900() throws InterruptedException { + Subscriber<String> subscriber = TestHelper.mockSubscriber(); + final int NUM_MSG = 1034; + final AtomicInteger count = new AtomicInteger(); + + Flowable<String> origin = Flowable.range(0, NUM_MSG) + .map(new Function<Integer, String>() { + @Override + public String apply(Integer t1) { + return "msg: " + count.incrementAndGet(); + } + }); + + origin.retry() + .groupBy(new Function<String, String>() { + @Override + public String apply(String t1) { + return t1; + } + }) + .flatMap(new Function<GroupedFlowable<String, String>, Flowable<String>>() { + @Override + public Flowable<String> apply(GroupedFlowable<String, String> t1) { + return t1.take(1); + } + }, NUM_MSG) // Must request as many groups as groupBy produces to avoid MBE + .subscribe(new TestSubscriber<>(subscriber)); + + InOrder inOrder = inOrder(subscriber); + // should show 3 attempts + inOrder.verify(subscriber, times(NUM_MSG)).onNext(any(java.lang.String.class)); + // // should have no errors + inOrder.verify(subscriber, never()).onError(any(Throwable.class)); + // should have a single success + //inOrder.verify(observer, times(1)).onNext("onSuccessOnly"); + // should have a single successful onComplete + inOrder.verify(subscriber, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void issue1900SourceNotSupportingBackpressure() { + Subscriber<String> subscriber = TestHelper.mockSubscriber(); + final int NUM_MSG = 1034; + final AtomicInteger count = new AtomicInteger(); + + Flowable<String> origin = Flowable.unsafeCreate(new Publisher<String>() { + + @Override + public void subscribe(Subscriber<? super String> subscriber) { + subscriber.onSubscribe(new BooleanSubscription()); + for (int i = 0; i < NUM_MSG; i++) { + subscriber.onNext("msg:" + count.incrementAndGet()); + } + subscriber.onComplete(); + } + }); + + origin.retry() + .groupBy(new Function<String, String>() { + @Override + public String apply(String t1) { + return t1; + } + }) + .flatMap(new Function<GroupedFlowable<String, String>, Flowable<String>>() { + @Override + public Flowable<String> apply(GroupedFlowable<String, String> t1) { + return t1.take(1); + } + }) + .subscribe(new TestSubscriber<>(subscriber)); + + InOrder inOrder = inOrder(subscriber); + // should show 3 attempts + inOrder.verify(subscriber, times(NUM_MSG)).onNext(any(java.lang.String.class)); + // // should have no errors + inOrder.verify(subscriber, never()).onError(any(Throwable.class)); + // should have a single success + //inOrder.verify(observer, times(1)).onNext("onSuccessOnly"); + // should have a single successful onComplete + inOrder.verify(subscriber, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + @Test + public void retryWhenDefaultScheduler() { + TestSubscriber<Integer> ts = TestSubscriber.create(); + + Flowable.just(1) + .concatWith(Flowable.<Integer>error(new TestException())) + .retryWhen((Function)new Function<Flowable, Flowable>() { + @Override + public Flowable apply(Flowable f) { + return f.take(2); + } + }).subscribe(ts); + + ts.assertValues(1, 1); + ts.assertNoErrors(); + ts.assertComplete(); + + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + @Test + public void retryWhenTrampolineScheduler() { + TestSubscriber<Integer> ts = TestSubscriber.create(); + + Flowable.just(1) + .concatWith(Flowable.<Integer>error(new TestException())) + .subscribeOn(Schedulers.trampoline()) + .retryWhen((Function)new Function<Flowable, Flowable>() { + @Override + public Flowable apply(Flowable f) { + return f.take(2); + } + }).subscribe(ts); + + ts.assertValues(1, 1); + ts.assertNoErrors(); + ts.assertComplete(); + } + + @Test + public void retryPredicate() { + Flowable.just(1).concatWith(Flowable.<Integer>error(new TestException())) + .retry(new Predicate<Throwable>() { + @Override + public boolean test(Throwable v) throws Exception { + return true; + } + }) + .take(5) + .test() + .assertResult(1, 1, 1, 1, 1); + } + + @Test + public void retryLongPredicateInvalid() { + try { + Flowable.just(1).retry(-99, new Predicate<Throwable>() { + @Override + public boolean test(Throwable e) throws Exception { + return true; + } + }); + fail("Should have thrown"); + } catch (IllegalArgumentException ex) { + assertEquals("times >= 0 required but it was -99", ex.getMessage()); + } + } + + @Test + public void retryUntil() { + Flowable.just(1).concatWith(Flowable.<Integer>error(new TestException())) + .retryUntil(new BooleanSupplier() { + @Override + public boolean getAsBoolean() throws Exception { + return false; + } + }) + .take(5) + .test() + .assertResult(1, 1, 1, 1, 1); + } + + @Test + public void shouldDisposeInnerFlowable() { + final PublishProcessor<Object> processor = PublishProcessor.create(); + final Disposable disposable = Flowable.error(new RuntimeException("Leak")) + .retryWhen(new Function<Flowable<Throwable>, Flowable<Object>>() { + @Override + public Flowable<Object> apply(Flowable<Throwable> errors) throws Exception { + return errors.switchMap(new Function<Throwable, Flowable<Object>>() { + @Override + public Flowable<Object> apply(Throwable ignore) throws Exception { + return processor; + } + }); + } + }) + .subscribe(); + + assertTrue(processor.hasSubscribers()); + disposable.dispose(); + assertFalse(processor.hasSubscribers()); + } + + @Test + public void noCancelPreviousRetry() { + final AtomicInteger counter = new AtomicInteger(); + + final AtomicInteger times = new AtomicInteger(); + + Flowable<Integer> source = Flowable.defer(new Supplier<Flowable<Integer>>() { + @Override + public Flowable<Integer> get() throws Exception { + if (times.getAndIncrement() < 4) { + return Flowable.error(new TestException()); + } + return Flowable.just(1); + } + }) + .doOnCancel(new Action() { + @Override + public void run() throws Exception { + counter.getAndIncrement(); + } + }); + + source.retry(5) + .test() + .assertResult(1); + + assertEquals(0, counter.get()); + } + + @Test + public void noCancelPreviousRetryWhile() { + final AtomicInteger counter = new AtomicInteger(); + + final AtomicInteger times = new AtomicInteger(); + + Flowable<Integer> source = Flowable.defer(new Supplier<Flowable<Integer>>() { + @Override + public Flowable<Integer> get() throws Exception { + if (times.getAndIncrement() < 4) { + return Flowable.error(new TestException()); + } + return Flowable.just(1); + } + }) + .doOnCancel(new Action() { + @Override + public void run() throws Exception { + counter.getAndIncrement(); + } + }); + + source.retry(5, Functions.alwaysTrue()) + .test() + .assertResult(1); + + assertEquals(0, counter.get()); + } + + @Test + public void noCancelPreviousRetryWhile2() { + final AtomicInteger counter = new AtomicInteger(); + + final AtomicInteger times = new AtomicInteger(); + + Flowable<Integer> source = Flowable.defer(new Supplier<Flowable<Integer>>() { + @Override + public Flowable<Integer> get() throws Exception { + if (times.getAndIncrement() < 4) { + return Flowable.error(new TestException()); + } + return Flowable.just(1); + } + }) + .doOnCancel(new Action() { + @Override + public void run() throws Exception { + counter.getAndIncrement(); + } + }); + + source.retry(new BiPredicate<Integer, Throwable>() { + @Override + public boolean test(Integer a, Throwable b) throws Exception { + return a < 5; + } + }) + .test() + .assertResult(1); + + assertEquals(0, counter.get()); + } + + @Test + public void noCancelPreviousRetryUntil() { + final AtomicInteger counter = new AtomicInteger(); + + final AtomicInteger times = new AtomicInteger(); + + Flowable<Integer> source = Flowable.defer(new Supplier<Flowable<Integer>>() { + @Override + public Flowable<Integer> get() throws Exception { + if (times.getAndIncrement() < 4) { + return Flowable.error(new TestException()); + } + return Flowable.just(1); + } + }) + .doOnCancel(new Action() { + @Override + public void run() throws Exception { + counter.getAndIncrement(); + } + }); + + source.retryUntil(new BooleanSupplier() { + @Override + public boolean getAsBoolean() throws Exception { + return false; + } + }) + .test() + .assertResult(1); + + assertEquals(0, counter.get()); + } + + @Test + public void noCancelPreviousRepeatWhen() { + final AtomicInteger counter = new AtomicInteger(); + + final AtomicInteger times = new AtomicInteger(); + + Flowable<Integer> source = Flowable.defer(new Supplier<Flowable<Integer>>() { + @Override + public Flowable<Integer> get() throws Exception { + if (times.get() < 4) { + return Flowable.error(new TestException()); + } + return Flowable.just(1); + } + }).doOnCancel(new Action() { + @Override + public void run() throws Exception { + counter.getAndIncrement(); + } + }); + + source.retryWhen(new Function<Flowable<Throwable>, Flowable<?>>() { + @Override + public Flowable<?> apply(Flowable<Throwable> e) throws Exception { + return e.takeWhile(new Predicate<Object>() { + @Override + public boolean test(Object v) throws Exception { + return times.getAndIncrement() < 4; + } + }); + } + }) + .test() + .assertResult(1); + + assertEquals(0, counter.get()); + } + + @Test + public void noCancelPreviousRepeatWhen2() { + final AtomicInteger counter = new AtomicInteger(); + + final AtomicInteger times = new AtomicInteger(); + + Flowable<Integer> source = Flowable.<Integer>error(new TestException()) + .doOnCancel(new Action() { + @Override + public void run() throws Exception { + counter.getAndIncrement(); + } + }); + + source.retryWhen(new Function<Flowable<Throwable>, Flowable<?>>() { + @Override + public Flowable<?> apply(Flowable<Throwable> e) throws Exception { + return e.takeWhile(new Predicate<Object>() { + @Override + public boolean test(Object v) throws Exception { + return times.getAndIncrement() < 4; + } + }); + } + }) + .test() + .assertResult(); + + assertEquals(0, counter.get()); + } + + @Test + public void repeatFloodNoSubscriptionError() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + + final TestException error = new TestException(); + + try { + final PublishProcessor<Integer> source = PublishProcessor.create(); + final PublishProcessor<Integer> signaller = PublishProcessor.create(); + + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + + TestSubscriber<Integer> ts = source.take(1) + .map(new Function<Integer, Integer>() { + @Override + public Integer apply(Integer v) throws Exception { + throw error; + } + }) + .retryWhen(new Function<Flowable<Throwable>, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Flowable<Throwable> v) + throws Exception { + return signaller; + } + }).test(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + source.onNext(1); + } + } + }; + Runnable r2 = new Runnable() { + @Override + public void run() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + signaller.offer(1); + } + } + }; + + TestHelper.race(r1, r2); + + ts.cancel(); + } + + if (!errors.isEmpty()) { + for (Throwable e : errors) { + e.printStackTrace(); + } + fail(errors + ""); + } + } finally { + RxJavaPlugins.reset(); + } + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableRetryWithPredicateTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableRetryWithPredicateTest.java new file mode 100644 index 0000000000..2180ac8873 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableRetryWithPredicateTest.java @@ -0,0 +1,512 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import java.io.IOException; +import java.util.*; +import java.util.concurrent.*; +import java.util.concurrent.atomic.*; + +import org.junit.Test; +import org.mockito.InOrder; +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.internal.subscriptions.BooleanSubscription; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.processors.PublishProcessor; +import io.reactivex.rxjava3.subscribers.TestSubscriber; +import io.reactivex.rxjava3.testsupport.*; + +public class FlowableRetryWithPredicateTest extends RxJavaTest { + BiPredicate<Integer, Throwable> retryTwice = new BiPredicate<Integer, Throwable>() { + @Override + public boolean test(Integer t1, Throwable t2) { + return t1 <= 2; + } + }; + BiPredicate<Integer, Throwable> retry5 = new BiPredicate<Integer, Throwable>() { + @Override + public boolean test(Integer t1, Throwable t2) { + return t1 <= 5; + } + }; + BiPredicate<Integer, Throwable> retryOnTestException = new BiPredicate<Integer, Throwable>() { + @Override + public boolean test(Integer t1, Throwable t2) { + return t2 instanceof IOException; + } + }; + @Test + public void withNothingToRetry() { + Flowable<Integer> source = Flowable.range(0, 3); + + Subscriber<Integer> subscriber = TestHelper.mockSubscriber(); + InOrder inOrder = inOrder(subscriber); + + source.retry(retryTwice).subscribe(subscriber); + + inOrder.verify(subscriber).onNext(0); + inOrder.verify(subscriber).onNext(1); + inOrder.verify(subscriber).onNext(2); + inOrder.verify(subscriber).onComplete(); + verify(subscriber, never()).onError(any(Throwable.class)); + } + + @Test + public void retryTwice() { + Flowable<Integer> source = Flowable.unsafeCreate(new Publisher<Integer>() { + int count; + @Override + public void subscribe(Subscriber<? super Integer> t1) { + t1.onSubscribe(new BooleanSubscription()); + count++; + t1.onNext(0); + t1.onNext(1); + if (count == 1) { + t1.onError(new TestException()); + return; + } + t1.onNext(2); + t1.onNext(3); + t1.onComplete(); + } + }); + + Subscriber<Integer> subscriber = TestHelper.mockSubscriber(); + InOrder inOrder = inOrder(subscriber); + + source.retry(retryTwice).subscribe(subscriber); + + inOrder.verify(subscriber).onNext(0); + inOrder.verify(subscriber).onNext(1); + inOrder.verify(subscriber).onNext(0); + inOrder.verify(subscriber).onNext(1); + inOrder.verify(subscriber).onNext(2); + inOrder.verify(subscriber).onNext(3); + inOrder.verify(subscriber).onComplete(); + verify(subscriber, never()).onError(any(Throwable.class)); + + } + + @Test + public void retryTwiceAndGiveUp() { + Flowable<Integer> source = Flowable.unsafeCreate(new Publisher<Integer>() { + @Override + public void subscribe(Subscriber<? super Integer> t1) { + t1.onSubscribe(new BooleanSubscription()); + t1.onNext(0); + t1.onNext(1); + t1.onError(new TestException()); + } + }); + + Subscriber<Integer> subscriber = TestHelper.mockSubscriber(); + InOrder inOrder = inOrder(subscriber); + + source.retry(retryTwice).subscribe(subscriber); + + inOrder.verify(subscriber).onNext(0); + inOrder.verify(subscriber).onNext(1); + inOrder.verify(subscriber).onNext(0); + inOrder.verify(subscriber).onNext(1); + inOrder.verify(subscriber).onNext(0); + inOrder.verify(subscriber).onNext(1); + inOrder.verify(subscriber).onError(any(TestException.class)); + verify(subscriber, never()).onComplete(); + + } + + @Test + public void retryOnSpecificException() { + Flowable<Integer> source = Flowable.unsafeCreate(new Publisher<Integer>() { + int count; + @Override + public void subscribe(Subscriber<? super Integer> t1) { + t1.onSubscribe(new BooleanSubscription()); + count++; + t1.onNext(0); + t1.onNext(1); + if (count == 1) { + t1.onError(new IOException()); + return; + } + t1.onNext(2); + t1.onNext(3); + t1.onComplete(); + } + }); + + Subscriber<Integer> subscriber = TestHelper.mockSubscriber(); + InOrder inOrder = inOrder(subscriber); + + source.retry(retryOnTestException).subscribe(subscriber); + + inOrder.verify(subscriber).onNext(0); + inOrder.verify(subscriber).onNext(1); + inOrder.verify(subscriber).onNext(0); + inOrder.verify(subscriber).onNext(1); + inOrder.verify(subscriber).onNext(2); + inOrder.verify(subscriber).onNext(3); + inOrder.verify(subscriber).onComplete(); + verify(subscriber, never()).onError(any(Throwable.class)); + } + + @Test + public void retryOnSpecificExceptionAndNotOther() { + final IOException ioe = new IOException(); + final TestException te = new TestException(); + Flowable<Integer> source = Flowable.unsafeCreate(new Publisher<Integer>() { + int count; + @Override + public void subscribe(Subscriber<? super Integer> t1) { + t1.onSubscribe(new BooleanSubscription()); + count++; + t1.onNext(0); + t1.onNext(1); + if (count == 1) { + t1.onError(ioe); + return; + } + t1.onNext(2); + t1.onNext(3); + t1.onError(te); + } + }); + + Subscriber<Integer> subscriber = TestHelper.mockSubscriber(); + InOrder inOrder = inOrder(subscriber); + + source.retry(retryOnTestException).subscribe(subscriber); + + inOrder.verify(subscriber).onNext(0); + inOrder.verify(subscriber).onNext(1); + inOrder.verify(subscriber).onNext(0); + inOrder.verify(subscriber).onNext(1); + inOrder.verify(subscriber).onNext(2); + inOrder.verify(subscriber).onNext(3); + inOrder.verify(subscriber).onError(te); + verify(subscriber, never()).onError(ioe); + verify(subscriber, never()).onComplete(); + } + + @Test + public void unsubscribeFromRetry() { + PublishProcessor<Integer> processor = PublishProcessor.create(); + final AtomicInteger count = new AtomicInteger(0); + Disposable sub = processor.retry(retryTwice).subscribe(new Consumer<Integer>() { + @Override + public void accept(Integer n) { + count.incrementAndGet(); + } + }); + processor.onNext(1); + sub.dispose(); + processor.onNext(2); + assertEquals(1, count.get()); + } + + @Test + public void unsubscribeAfterError() { + + Subscriber<Long> subscriber = TestHelper.mockSubscriber(); + + // Flowable that always fails after 100ms + FlowableRetryTest.SlowFlowable so = new FlowableRetryTest.SlowFlowable(100, 0, "testUnsubscribeAfterError"); + Flowable<Long> f = Flowable + .unsafeCreate(so) + .retry(retry5); + + FlowableRetryTest.AsyncSubscriber<Long> async = new FlowableRetryTest.AsyncSubscriber<>(subscriber); + + f.subscribe(async); + + async.await(); + + InOrder inOrder = inOrder(subscriber); + // Should fail once + inOrder.verify(subscriber, times(1)).onError(any(Throwable.class)); + inOrder.verify(subscriber, never()).onComplete(); + + assertEquals("Start 6 threads, retry 5 then fail on 6", 6, so.efforts.get()); + assertEquals("Only 1 active subscription", 1, so.maxActive.get()); + } + + @Test + public void timeoutWithRetry() { + + Subscriber<Long> subscriber = TestHelper.mockSubscriber(); + + // Flowable that sends every 100ms (timeout fails instead) + FlowableRetryTest.SlowFlowable so = new FlowableRetryTest.SlowFlowable(100, 10, "testTimeoutWithRetry"); + Flowable<Long> f = Flowable + .unsafeCreate(so) + .timeout(80, TimeUnit.MILLISECONDS) + .retry(retry5); + + FlowableRetryTest.AsyncSubscriber<Long> async = new FlowableRetryTest.AsyncSubscriber<>(subscriber); + + f.subscribe(async); + + async.await(); + + InOrder inOrder = inOrder(subscriber); + // Should fail once + inOrder.verify(subscriber, times(1)).onError(any(Throwable.class)); + inOrder.verify(subscriber, never()).onComplete(); + + assertEquals("Start 6 threads, retry 5 then fail on 6", 6, so.efforts.get()); + } + + @Test + public void issue2826() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(); + final RuntimeException e = new RuntimeException("You shall not pass"); + final AtomicInteger c = new AtomicInteger(); + Flowable.just(1).map(new Function<Integer, Integer>() { + @Override + public Integer apply(Integer t1) { + c.incrementAndGet(); + throw e; + } + }).retry(retry5).subscribe(ts); + + ts.assertTerminated(); + assertEquals(6, c.get()); + assertEquals(Collections.singletonList(e), ts.errors()); + } + + @Test + public void justAndRetry() throws Exception { + final AtomicBoolean throwException = new AtomicBoolean(true); + int value = Flowable.just(1).map(new Function<Integer, Integer>() { + @Override + public Integer apply(Integer t1) { + if (throwException.compareAndSet(true, false)) { + throw new TestException(); + } + return t1; + } + }).retry(1).blockingSingle(); + + assertEquals(1, value); + } + + @Test + public void issue3008RetryWithPredicate() { + final List<Long> list = new CopyOnWriteArrayList<>(); + final AtomicBoolean isFirst = new AtomicBoolean(true); + Flowable.<Long> just(1L, 2L, 3L).map(new Function<Long, Long>() { + @Override + public Long apply(Long x) { + System.out.println("map " + x); + if (x == 2 && isFirst.getAndSet(false)) { + throw new RuntimeException("retryable error"); + } + return x; + }}) + .retry(new BiPredicate<Integer, Throwable>() { + @Override + public boolean test(Integer t1, Throwable t2) { + return true; + }}) + .forEach(new Consumer<Long>() { + + @Override + public void accept(Long t) { + System.out.println(t); + list.add(t); + }}); + assertEquals(Arrays.asList(1L, 1L, 2L, 3L), list); + } + + @Test + public void issue3008RetryInfinite() { + final List<Long> list = new CopyOnWriteArrayList<>(); + final AtomicBoolean isFirst = new AtomicBoolean(true); + Flowable.<Long> just(1L, 2L, 3L).map(new Function<Long, Long>() { + @Override + public Long apply(Long x) { + System.out.println("map " + x); + if (x == 2 && isFirst.getAndSet(false)) { + throw new RuntimeException("retryable error"); + } + return x; + }}) + .retry() + .forEach(new Consumer<Long>() { + + @Override + public void accept(Long t) { + System.out.println(t); + list.add(t); + }}); + assertEquals(Arrays.asList(1L, 1L, 2L, 3L), list); + } + + @Test + public void backpressure() { + final List<Long> requests = new ArrayList<>(); + + Flowable<Integer> source = Flowable + .just(1) + .concatWith(Flowable.<Integer>error(new TestException())) + .doOnRequest(new LongConsumer() { + @Override + public void accept(long t) { + requests.add(t); + } + }); + + TestSubscriber<Integer> ts = new TestSubscriber<>(3L); + source + .retry(new BiPredicate<Integer, Throwable>() { + @Override + public boolean test(Integer t1, Throwable t2) { + return t1 < 4; // FIXME was 3 in 1.x for some reason + } + }).subscribe(ts); + + assertEquals(Arrays.asList(3L, 2L, 1L), requests); + ts.assertValues(1, 1, 1); + ts.assertNotComplete(); + ts.assertNoErrors(); + } + + @Test + public void predicateThrows() { + + TestSubscriberEx<Object> ts = Flowable.error(new TestException("Outer")) + .retry(new Predicate<Throwable>() { + @Override + public boolean test(Throwable e) throws Exception { + throw new TestException("Inner"); + } + }) + .to(TestHelper.<Object>testConsumer()) + .assertFailure(CompositeException.class); + + List<Throwable> errors = TestHelper.compositeList(ts.errors().get(0)); + + TestHelper.assertError(errors, 0, TestException.class, "Outer"); + TestHelper.assertError(errors, 1, TestException.class, "Inner"); + } + + @Test + public void dontRetry() { + Flowable.error(new TestException("Outer")) + .retry(Functions.alwaysFalse()) + .to(TestHelper.<Object>testConsumer()) + .assertFailureAndMessage(TestException.class, "Outer"); + } + + @Test + public void retryDisposeRace() { + final TestException ex = new TestException(); + RxJavaPlugins.setErrorHandler(Functions.emptyConsumer()); + try { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final PublishProcessor<Integer> pp = PublishProcessor.create(); + + final TestSubscriber<Integer> ts = pp.retry(Functions.alwaysTrue()).test(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + pp.onError(ex); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + ts.cancel(); + } + }; + + TestHelper.race(r1, r2); + + ts.assertEmpty(); + } + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void bipredicateThrows() { + + TestSubscriberEx<Object> ts = Flowable.error(new TestException("Outer")) + .retry(new BiPredicate<Integer, Throwable>() { + @Override + public boolean test(Integer n, Throwable e) throws Exception { + throw new TestException("Inner"); + } + }) + .to(TestHelper.<Object>testConsumer()) + .assertFailure(CompositeException.class); + + List<Throwable> errors = TestHelper.compositeList(ts.errors().get(0)); + + TestHelper.assertError(errors, 0, TestException.class, "Outer"); + TestHelper.assertError(errors, 1, TestException.class, "Inner"); + } + + @Test + public void retryBiPredicateDisposeRace() { + RxJavaPlugins.setErrorHandler(Functions.emptyConsumer()); + try { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final PublishProcessor<Integer> pp = PublishProcessor.create(); + + final TestSubscriber<Integer> ts = pp.retry(new BiPredicate<Object, Object>() { + @Override + public boolean test(Object t1, Object t2) throws Exception { + return true; + } + }).test(); + + final TestException ex = new TestException(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + pp.onError(ex); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + ts.cancel(); + } + }; + + TestHelper.race(r1, r2); + + ts.assertEmpty(); + } + } finally { + RxJavaPlugins.reset(); + } + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableSampleTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableSampleTest.java new file mode 100644 index 0000000000..1bc6456695 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableSampleTest.java @@ -0,0 +1,490 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import static org.junit.Assert.assertFalse; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import java.util.concurrent.TimeUnit; + +import org.junit.*; +import org.mockito.InOrder; +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.subscriptions.BooleanSubscription; +import io.reactivex.rxjava3.processors.*; +import io.reactivex.rxjava3.schedulers.*; +import io.reactivex.rxjava3.subscribers.TestSubscriber; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class FlowableSampleTest extends RxJavaTest { + private TestScheduler scheduler; + private Scheduler.Worker innerScheduler; + private Subscriber<Long> subscriber; + private Subscriber<Object> subscriber2; + + @Before + // due to mocking + public void before() { + scheduler = new TestScheduler(); + innerScheduler = scheduler.createWorker(); + subscriber = TestHelper.mockSubscriber(); + subscriber2 = TestHelper.mockSubscriber(); + } + + @Test + public void sample() { + Flowable<Long> source = Flowable.unsafeCreate(new Publisher<Long>() { + @Override + public void subscribe(final Subscriber<? super Long> subscriber1) { + subscriber1.onSubscribe(new BooleanSubscription()); + innerScheduler.schedule(new Runnable() { + @Override + public void run() { + subscriber1.onNext(1L); + } + }, 1, TimeUnit.SECONDS); + innerScheduler.schedule(new Runnable() { + @Override + public void run() { + subscriber1.onNext(2L); + } + }, 2, TimeUnit.SECONDS); + innerScheduler.schedule(new Runnable() { + @Override + public void run() { + subscriber1.onComplete(); + } + }, 3, TimeUnit.SECONDS); + } + }); + + Flowable<Long> sampled = source.sample(400L, TimeUnit.MILLISECONDS, scheduler); + sampled.subscribe(subscriber); + + InOrder inOrder = inOrder(subscriber); + + scheduler.advanceTimeTo(800L, TimeUnit.MILLISECONDS); + verify(subscriber, never()).onNext(any(Long.class)); + verify(subscriber, never()).onComplete(); + verify(subscriber, never()).onError(any(Throwable.class)); + + scheduler.advanceTimeTo(1200L, TimeUnit.MILLISECONDS); + inOrder.verify(subscriber, times(1)).onNext(1L); + verify(subscriber, never()).onNext(2L); + verify(subscriber, never()).onComplete(); + verify(subscriber, never()).onError(any(Throwable.class)); + + scheduler.advanceTimeTo(1600L, TimeUnit.MILLISECONDS); + inOrder.verify(subscriber, never()).onNext(1L); + verify(subscriber, never()).onNext(2L); + verify(subscriber, never()).onComplete(); + verify(subscriber, never()).onError(any(Throwable.class)); + + scheduler.advanceTimeTo(2000L, TimeUnit.MILLISECONDS); + inOrder.verify(subscriber, never()).onNext(1L); + inOrder.verify(subscriber, times(1)).onNext(2L); + verify(subscriber, never()).onComplete(); + verify(subscriber, never()).onError(any(Throwable.class)); + + scheduler.advanceTimeTo(3000L, TimeUnit.MILLISECONDS); + inOrder.verify(subscriber, never()).onNext(1L); + inOrder.verify(subscriber, never()).onNext(2L); + verify(subscriber, times(1)).onComplete(); + verify(subscriber, never()).onError(any(Throwable.class)); + } + + @Test + public void sampleWithSamplerNormal() { + PublishProcessor<Integer> source = PublishProcessor.create(); + PublishProcessor<Integer> sampler = PublishProcessor.create(); + + Flowable<Integer> m = source.sample(sampler); + m.subscribe(subscriber2); + + source.onNext(1); + source.onNext(2); + sampler.onNext(1); + source.onNext(3); + source.onNext(4); + sampler.onNext(2); + source.onComplete(); + sampler.onNext(3); + + InOrder inOrder = inOrder(subscriber2); + inOrder.verify(subscriber2, never()).onNext(1); + inOrder.verify(subscriber2, times(1)).onNext(2); + inOrder.verify(subscriber2, never()).onNext(3); + inOrder.verify(subscriber2, times(1)).onNext(4); + inOrder.verify(subscriber2, times(1)).onComplete(); + verify(subscriber, never()).onError(any(Throwable.class)); + } + + @Test + public void sampleWithSamplerNoDuplicates() { + PublishProcessor<Integer> source = PublishProcessor.create(); + PublishProcessor<Integer> sampler = PublishProcessor.create(); + + Flowable<Integer> m = source.sample(sampler); + m.subscribe(subscriber2); + + source.onNext(1); + source.onNext(2); + sampler.onNext(1); + sampler.onNext(1); + + source.onNext(3); + source.onNext(4); + sampler.onNext(2); + sampler.onNext(2); + + source.onComplete(); + sampler.onNext(3); + + InOrder inOrder = inOrder(subscriber2); + inOrder.verify(subscriber2, never()).onNext(1); + inOrder.verify(subscriber2, times(1)).onNext(2); + inOrder.verify(subscriber2, never()).onNext(3); + inOrder.verify(subscriber2, times(1)).onNext(4); + inOrder.verify(subscriber2, times(1)).onComplete(); + verify(subscriber, never()).onError(any(Throwable.class)); + } + + @Test + public void sampleWithSamplerTerminatingEarly() { + PublishProcessor<Integer> source = PublishProcessor.create(); + PublishProcessor<Integer> sampler = PublishProcessor.create(); + + Flowable<Integer> m = source.sample(sampler); + m.subscribe(subscriber2); + + source.onNext(1); + source.onNext(2); + sampler.onNext(1); + sampler.onComplete(); + + source.onNext(3); + source.onNext(4); + + InOrder inOrder = inOrder(subscriber2); + inOrder.verify(subscriber2, never()).onNext(1); + inOrder.verify(subscriber2, times(1)).onNext(2); + inOrder.verify(subscriber2, times(1)).onComplete(); + inOrder.verify(subscriber2, never()).onNext(any()); + verify(subscriber, never()).onError(any(Throwable.class)); + } + + @Test + public void sampleWithSamplerEmitAndTerminate() { + PublishProcessor<Integer> source = PublishProcessor.create(); + PublishProcessor<Integer> sampler = PublishProcessor.create(); + + Flowable<Integer> m = source.sample(sampler); + m.subscribe(subscriber2); + + source.onNext(1); + source.onNext(2); + sampler.onNext(1); + source.onNext(3); + source.onComplete(); + sampler.onNext(2); + sampler.onComplete(); + + InOrder inOrder = inOrder(subscriber2); + inOrder.verify(subscriber2, never()).onNext(1); + inOrder.verify(subscriber2, times(1)).onNext(2); + inOrder.verify(subscriber2, never()).onNext(3); + inOrder.verify(subscriber2, times(1)).onComplete(); + inOrder.verify(subscriber2, never()).onNext(any()); + verify(subscriber, never()).onError(any(Throwable.class)); + } + + @Test + public void sampleWithSamplerEmptySource() { + PublishProcessor<Integer> source = PublishProcessor.create(); + PublishProcessor<Integer> sampler = PublishProcessor.create(); + + Flowable<Integer> m = source.sample(sampler); + m.subscribe(subscriber2); + + source.onComplete(); + sampler.onNext(1); + + InOrder inOrder = inOrder(subscriber2); + inOrder.verify(subscriber2, times(1)).onComplete(); + verify(subscriber2, never()).onNext(any()); + verify(subscriber, never()).onError(any(Throwable.class)); + } + + @Test + public void sampleWithSamplerSourceThrows() { + PublishProcessor<Integer> source = PublishProcessor.create(); + PublishProcessor<Integer> sampler = PublishProcessor.create(); + + Flowable<Integer> m = source.sample(sampler); + m.subscribe(subscriber2); + + source.onNext(1); + source.onError(new RuntimeException("Forced failure!")); + sampler.onNext(1); + + InOrder inOrder = inOrder(subscriber2); + inOrder.verify(subscriber2, times(1)).onError(any(Throwable.class)); + verify(subscriber2, never()).onNext(any()); + verify(subscriber, never()).onComplete(); + } + + @Test + public void sampleWithSamplerThrows() { + PublishProcessor<Integer> source = PublishProcessor.create(); + PublishProcessor<Integer> sampler = PublishProcessor.create(); + + Flowable<Integer> m = source.sample(sampler); + m.subscribe(subscriber2); + + source.onNext(1); + sampler.onNext(1); + sampler.onError(new RuntimeException("Forced failure!")); + + InOrder inOrder = inOrder(subscriber2); + inOrder.verify(subscriber2, times(1)).onNext(1); + inOrder.verify(subscriber2, times(1)).onError(any(RuntimeException.class)); + verify(subscriber, never()).onComplete(); + } + + @Test + public void sampleUnsubscribe() { + final Subscription s = mock(Subscription.class); + Flowable<Integer> f = Flowable.unsafeCreate( + new Publisher<Integer>() { + @Override + public void subscribe(Subscriber<? super Integer> subscriber) { + subscriber.onSubscribe(s); + } + } + ); + f.throttleLast(1, TimeUnit.MILLISECONDS).subscribe().dispose(); + verify(s).cancel(); + } + + @Test + public void dispose() { + TestHelper.checkDisposed(PublishProcessor.create().sample(1, TimeUnit.SECONDS, new TestScheduler())); + + TestHelper.checkDisposed(PublishProcessor.create().sample(Flowable.never())); + } + + @Test + public void error() { + Flowable.error(new TestException()) + .sample(1, TimeUnit.SECONDS) + .test() + .assertFailure(TestException.class); + } + + @Test + public void backpressureOverflow() { + BehaviorProcessor.createDefault(1) + .sample(1, TimeUnit.MILLISECONDS) + .test(0L) + .awaitDone(5, TimeUnit.SECONDS) + .assertFailure(MissingBackpressureException.class); + } + + @Test + public void backpressureOverflowWithOtherPublisher() { + PublishProcessor<Integer> pp1 = PublishProcessor.create(); + PublishProcessor<Integer> pp2 = PublishProcessor.create(); + + TestSubscriber<Integer> ts = pp1 + .sample(pp2) + .test(0L); + + pp1.onNext(1); + pp2.onNext(2); + + ts.assertFailure(MissingBackpressureException.class); + + assertFalse(pp1.hasSubscribers()); + assertFalse(pp2.hasSubscribers()); + } + + @Test + public void emitLastTimed() { + Flowable.just(1) + .sample(1, TimeUnit.DAYS, true) + .test() + .assertResult(1); + } + + @Test + public void emitLastTimedEmpty() { + Flowable.empty() + .sample(1, TimeUnit.DAYS, true) + .test() + .assertResult(); + } + + @Test + public void emitLastTimedCustomScheduler() { + Flowable.just(1) + .sample(1, TimeUnit.DAYS, Schedulers.single(), true) + .test() + .assertResult(1); + } + + @Test + public void emitLastTimedRunCompleteRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final TestScheduler scheduler = new TestScheduler(); + + final PublishProcessor<Integer> pp = PublishProcessor.create(); + + TestSubscriber<Integer> ts = pp.sample(1, TimeUnit.SECONDS, scheduler, true) + .test(); + + pp.onNext(1); + + Runnable r1 = new Runnable() { + @Override + public void run() { + pp.onComplete(); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + scheduler.advanceTimeBy(1, TimeUnit.SECONDS); + } + }; + + TestHelper.race(r1, r2); + + ts.assertResult(1); + } + } + + @Test + public void emitLastOther() { + Flowable.just(1) + .sample(Flowable.timer(1, TimeUnit.DAYS), true) + .test() + .assertResult(1); + } + + @Test + public void emitLastOtherEmpty() { + Flowable.empty() + .sample(Flowable.timer(1, TimeUnit.DAYS), true) + .test() + .assertResult(); + } + + @Test + public void emitLastOtherRunCompleteRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final PublishProcessor<Integer> pp = PublishProcessor.create(); + final PublishProcessor<Integer> sampler = PublishProcessor.create(); + + TestSubscriber<Integer> ts = pp.sample(sampler, true) + .test(); + + pp.onNext(1); + + Runnable r1 = new Runnable() { + @Override + public void run() { + pp.onComplete(); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + sampler.onNext(1); + } + }; + + TestHelper.race(r1, r2); + + ts.assertResult(1); + } + } + + @Test + public void emitLastOtherCompleteCompleteRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final PublishProcessor<Integer> pp = PublishProcessor.create(); + final PublishProcessor<Integer> sampler = PublishProcessor.create(); + + TestSubscriber<Integer> ts = pp.sample(sampler, true).test(); + + pp.onNext(1); + + Runnable r1 = new Runnable() { + @Override + public void run() { + pp.onComplete(); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + sampler.onComplete(); + } + }; + + TestHelper.race(r1, r2); + + ts.assertResult(1); + } + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeFlowable(new Function<Flowable<Object>, Flowable<Object>>() { + @Override + public Flowable<Object> apply(Flowable<Object> f) + throws Exception { + return f.sample(1, TimeUnit.SECONDS); + } + }); + + TestHelper.checkDoubleOnSubscribeFlowable(new Function<Flowable<Object>, Flowable<Object>>() { + @Override + public Flowable<Object> apply(Flowable<Object> f) + throws Exception { + return f.sample(PublishProcessor.create()); + } + }); + } + + @Test + public void badRequest() { + TestHelper.assertBadRequestReported(PublishProcessor.create() + .sample(PublishProcessor.create())); + } + + @Test + public void badRequestTimed() { + TestHelper.assertBadRequestReported(PublishProcessor.create() + .sample(1, TimeUnit.MINUTES)); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableScalarXMapTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableScalarXMapTest.java new file mode 100644 index 0000000000..fab387470e --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableScalarXMapTest.java @@ -0,0 +1,254 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import static org.junit.Assert.*; + +import org.junit.Test; +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.subscriptions.*; +import io.reactivex.rxjava3.subscribers.TestSubscriber; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class FlowableScalarXMapTest extends RxJavaTest { + + @Test + public void utilityClass() { + TestHelper.checkUtilityClass(FlowableScalarXMap.class); + } + + static final class CallablePublisher implements Publisher<Integer>, Supplier<Integer> { + @Override + public void subscribe(Subscriber<? super Integer> s) { + EmptySubscription.error(new TestException(), s); + } + + @Override + public Integer get() throws Exception { + throw new TestException(); + } + } + + static final class EmptyCallablePublisher implements Publisher<Integer>, Supplier<Integer> { + @Override + public void subscribe(Subscriber<? super Integer> s) { + EmptySubscription.complete(s); + } + + @Override + public Integer get() throws Exception { + return null; + } + } + + static final class OneCallablePublisher implements Publisher<Integer>, Supplier<Integer> { + @Override + public void subscribe(Subscriber<? super Integer> s) { + s.onSubscribe(new ScalarSubscription<>(s, 1)); + } + + @Override + public Integer get() throws Exception { + return 1; + } + } + + @Test + public void tryScalarXMap() { + TestSubscriber<Integer> ts = new TestSubscriber<>(); + assertTrue(FlowableScalarXMap.tryScalarXMapSubscribe(new CallablePublisher(), ts, new Function<Integer, Publisher<Integer>>() { + @Override + public Publisher<Integer> apply(Integer f) throws Exception { + return Flowable.just(1); + } + })); + + ts.assertFailure(TestException.class); + } + + @Test + public void emptyXMap() { + TestSubscriber<Integer> ts = new TestSubscriber<>(); + + assertTrue(FlowableScalarXMap.tryScalarXMapSubscribe(new EmptyCallablePublisher(), ts, new Function<Integer, Publisher<Integer>>() { + @Override + public Publisher<Integer> apply(Integer f) throws Exception { + return Flowable.just(1); + } + })); + + ts.assertResult(); + } + + @Test + public void mapperCrashes() { + TestSubscriber<Integer> ts = new TestSubscriber<>(); + + assertTrue(FlowableScalarXMap.tryScalarXMapSubscribe(new OneCallablePublisher(), ts, new Function<Integer, Publisher<Integer>>() { + @Override + public Publisher<Integer> apply(Integer f) throws Exception { + throw new TestException(); + } + })); + + ts.assertFailure(TestException.class); + } + + @Test + public void mapperToJust() { + TestSubscriber<Integer> ts = new TestSubscriber<>(); + + assertTrue(FlowableScalarXMap.tryScalarXMapSubscribe(new OneCallablePublisher(), ts, new Function<Integer, Publisher<Integer>>() { + @Override + public Publisher<Integer> apply(Integer f) throws Exception { + return Flowable.just(1); + } + })); + + ts.assertResult(1); + } + + @Test + public void mapperToEmpty() { + TestSubscriber<Integer> ts = new TestSubscriber<>(); + + assertTrue(FlowableScalarXMap.tryScalarXMapSubscribe(new OneCallablePublisher(), ts, new Function<Integer, Publisher<Integer>>() { + @Override + public Publisher<Integer> apply(Integer f) throws Exception { + return Flowable.empty(); + } + })); + + ts.assertResult(); + } + + @Test + public void mapperToCrashingCallable() { + TestSubscriber<Integer> ts = new TestSubscriber<>(); + + assertTrue(FlowableScalarXMap.tryScalarXMapSubscribe(new OneCallablePublisher(), ts, new Function<Integer, Publisher<Integer>>() { + @Override + public Publisher<Integer> apply(Integer f) throws Exception { + return new CallablePublisher(); + } + })); + + ts.assertFailure(TestException.class); + } + + @Test + public void scalarMapToEmpty() { + FlowableScalarXMap.scalarXMap(1, new Function<Integer, Publisher<Integer>>() { + @Override + public Publisher<Integer> apply(Integer v) throws Exception { + return Flowable.empty(); + } + }) + .test() + .assertResult(); + } + + @Test + public void scalarMapToCrashingCallable() { + FlowableScalarXMap.scalarXMap(1, new Function<Integer, Publisher<Integer>>() { + @Override + public Publisher<Integer> apply(Integer v) throws Exception { + return new CallablePublisher(); + } + }) + .test() + .assertFailure(TestException.class); + } + + @Test + public void scalarDisposableStateCheck() { + TestSubscriber<Integer> ts = new TestSubscriber<>(); + ScalarSubscription<Integer> sd = new ScalarSubscription<>(ts, 1); + ts.onSubscribe(sd); + + assertFalse(sd.isCancelled()); + + assertTrue(sd.isEmpty()); + + sd.request(1); + + assertFalse(sd.isCancelled()); + + assertTrue(sd.isEmpty()); + + ts.assertResult(1); + + try { + sd.offer(1); + fail("Should have thrown"); + } catch (UnsupportedOperationException ex) { + // expected + } + + try { + sd.offer(1, 2); + fail("Should have thrown"); + } catch (UnsupportedOperationException ex) { + // expected + } + } + + @Test + public void scalarDisposableRunDisposeRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + TestSubscriber<Integer> ts = new TestSubscriber<>(); + final ScalarSubscription<Integer> sd = new ScalarSubscription<>(ts, 1); + ts.onSubscribe(sd); + + Runnable r1 = new Runnable() { + @Override + public void run() { + sd.request(1); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + sd.cancel(); + } + }; + + TestHelper.race(r1, r2); + } + } + + @Test + public void cancelled() { + ScalarSubscription<Integer> scalar = new ScalarSubscription<>(new TestSubscriber<>(), 1); + + assertFalse(scalar.isCancelled()); + + scalar.cancel(); + + assertTrue(scalar.isCancelled()); + } + + @Test + public void mapToNonScalar() { + Flowable.fromCallable(() -> 1) + .concatMap(v -> Flowable.range(1, 5)) + .test() + .assertResult(1, 2, 3, 4, 5); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableScanTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableScanTest.java new file mode 100644 index 0000000000..5c6e4fa901 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableScanTest.java @@ -0,0 +1,702 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +import java.util.*; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.atomic.*; + +import org.junit.*; +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.flowable.*; +import io.reactivex.rxjava3.flowable.FlowableEventStream.Event; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.processors.PublishProcessor; +import io.reactivex.rxjava3.subscribers.*; +import io.reactivex.rxjava3.testsupport.*; + +public class FlowableScanTest extends RxJavaTest { + + @Test + public void scanIntegersWithInitialValue() { + Subscriber<String> subscriber = TestHelper.mockSubscriber(); + + Flowable<Integer> flowable = Flowable.just(1, 2, 3); + + Flowable<String> m = flowable.scan("", new BiFunction<String, Integer, String>() { + + @Override + public String apply(String s, Integer n) { + return s + n.toString(); + } + + }); + m.subscribe(subscriber); + + verify(subscriber, never()).onError(any(Throwable.class)); + verify(subscriber, times(1)).onNext(""); + verify(subscriber, times(1)).onNext("1"); + verify(subscriber, times(1)).onNext("12"); + verify(subscriber, times(1)).onNext("123"); + verify(subscriber, times(4)).onNext(anyString()); + verify(subscriber, times(1)).onComplete(); + verify(subscriber, never()).onError(any(Throwable.class)); + } + + @Test + public void scanIntegersWithoutInitialValue() { + Subscriber<Integer> subscriber = TestHelper.mockSubscriber(); + + Flowable<Integer> flowable = Flowable.just(1, 2, 3); + + Flowable<Integer> m = flowable.scan(new BiFunction<Integer, Integer, Integer>() { + + @Override + public Integer apply(Integer t1, Integer t2) { + return t1 + t2; + } + + }); + m.subscribe(subscriber); + + verify(subscriber, never()).onError(any(Throwable.class)); + verify(subscriber, never()).onNext(0); + verify(subscriber, times(1)).onNext(1); + verify(subscriber, times(1)).onNext(3); + verify(subscriber, times(1)).onNext(6); + verify(subscriber, times(3)).onNext(anyInt()); + verify(subscriber, times(1)).onComplete(); + verify(subscriber, never()).onError(any(Throwable.class)); + } + + @Test + public void scanIntegersWithoutInitialValueAndOnlyOneValue() { + Subscriber<Integer> subscriber = TestHelper.mockSubscriber(); + + Flowable<Integer> flowable = Flowable.just(1); + + Flowable<Integer> m = flowable.scan(new BiFunction<Integer, Integer, Integer>() { + + @Override + public Integer apply(Integer t1, Integer t2) { + return t1 + t2; + } + + }); + m.subscribe(subscriber); + + verify(subscriber, never()).onError(any(Throwable.class)); + verify(subscriber, never()).onNext(0); + verify(subscriber, times(1)).onNext(1); + verify(subscriber, times(1)).onNext(anyInt()); + verify(subscriber, times(1)).onComplete(); + verify(subscriber, never()).onError(any(Throwable.class)); + } + + @Test + public void shouldNotEmitUntilAfterSubscription() { + TestSubscriber<Integer> ts = new TestSubscriber<>(); + Flowable.range(1, 100).scan(0, new BiFunction<Integer, Integer, Integer>() { + + @Override + public Integer apply(Integer t1, Integer t2) { + return t1 + t2; + } + + }).filter(new Predicate<Integer>() { + + @Override + public boolean test(Integer t1) { + // this will cause request(1) when 0 is emitted + return t1 > 0; + } + + }).subscribe(ts); + + assertEquals(100, ts.values().size()); + } + + @Test + public void backpressureWithInitialValue() { + final AtomicInteger count = new AtomicInteger(); + Flowable.range(1, 100) + .scan(0, new BiFunction<Integer, Integer, Integer>() { + + @Override + public Integer apply(Integer t1, Integer t2) { + return t1 + t2; + } + + }) + .subscribe(new DefaultSubscriber<Integer>() { + + @Override + public void onStart() { + request(10); + } + + @Override + public void onComplete() { + + } + + @Override + public void onError(Throwable e) { + Assert.fail(e.getMessage()); + e.printStackTrace(); + } + + @Override + public void onNext(Integer t) { + count.incrementAndGet(); + } + + }); + + // we only expect to receive 10 since we request(10) + assertEquals(10, count.get()); + } + + @Test + public void backpressureWithoutInitialValue() { + final AtomicInteger count = new AtomicInteger(); + Flowable.range(1, 100) + .scan(new BiFunction<Integer, Integer, Integer>() { + + @Override + public Integer apply(Integer t1, Integer t2) { + return t1 + t2; + } + + }) + .subscribe(new DefaultSubscriber<Integer>() { + + @Override + public void onStart() { + request(10); + } + + @Override + public void onComplete() { + + } + + @Override + public void onError(Throwable e) { + Assert.fail(e.getMessage()); + e.printStackTrace(); + } + + @Override + public void onNext(Integer t) { + count.incrementAndGet(); + } + + }); + + // we only expect to receive 10 since we request(10) + assertEquals(10, count.get()); + } + + @Test + public void noBackpressureWithInitialValue() { + final AtomicInteger count = new AtomicInteger(); + Flowable.range(1, 100) + .scan(0, new BiFunction<Integer, Integer, Integer>() { + + @Override + public Integer apply(Integer t1, Integer t2) { + return t1 + t2; + } + + }) + .subscribe(new DefaultSubscriber<Integer>() { + + @Override + public void onComplete() { + + } + + @Override + public void onError(Throwable e) { + Assert.fail(e.getMessage()); + e.printStackTrace(); + } + + @Override + public void onNext(Integer t) { + count.incrementAndGet(); + } + + }); + + // we only expect to receive 101 as we'll receive all 100 + the initial value + assertEquals(101, count.get()); + } + + /** + * This uses the public API collect which uses scan under the covers. + */ + @Test + public void seedFactory() { + Single<List<Integer>> o = Flowable.range(1, 10) + .collect(new Supplier<List<Integer>>() { + + @Override + public List<Integer> get() { + return new ArrayList<>(); + } + + }, new BiConsumer<List<Integer>, Integer>() { + + @Override + public void accept(List<Integer> list, Integer t2) { + list.add(t2); + } + + }); + + assertEquals(Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10), o.blockingGet()); + assertEquals(Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10), o.blockingGet()); + } + + /** + * This uses the public API collect which uses scan under the covers. + */ + @Test + public void seedFactoryFlowable() { + Flowable<List<Integer>> f = Flowable.range(1, 10) + .collect(new Supplier<List<Integer>>() { + + @Override + public List<Integer> get() { + return new ArrayList<>(); + } + + }, new BiConsumer<List<Integer>, Integer>() { + + @Override + public void accept(List<Integer> list, Integer t2) { + list.add(t2); + } + + }).toFlowable().takeLast(1); + + assertEquals(Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10), f.blockingSingle()); + assertEquals(Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10), f.blockingSingle()); + } + + @Test + public void scanWithRequestOne() { + Flowable<Integer> f = Flowable.just(1, 2).scan(0, new BiFunction<Integer, Integer, Integer>() { + + @Override + public Integer apply(Integer t1, Integer t2) { + return t1 + t2; + } + + }).take(1); + TestSubscriberEx<Integer> subscriber = new TestSubscriberEx<>(); + f.subscribe(subscriber); + subscriber.assertValue(0); + subscriber.assertTerminated(); + subscriber.assertNoErrors(); + } + + @Test + public void scanShouldNotRequestZero() { + final AtomicReference<Subscription> producer = new AtomicReference<>(); + Flowable<Integer> f = Flowable.unsafeCreate(new Publisher<Integer>() { + @Override + public void subscribe(final Subscriber<? super Integer> subscriber) { + Subscription p = spy(new Subscription() { + + private AtomicBoolean requested = new AtomicBoolean(false); + + @Override + public void request(long n) { + if (requested.compareAndSet(false, true)) { + subscriber.onNext(1); + subscriber.onComplete(); + } + } + + @Override + public void cancel() { + + } + }); + producer.set(p); + subscriber.onSubscribe(p); + } + }).scan(100, new BiFunction<Integer, Integer, Integer>() { + + @Override + public Integer apply(Integer t1, Integer t2) { + return t1 + t2; + } + + }); + + f.subscribe(new TestSubscriber<Integer>(1L) { + + @Override + public void onNext(Integer integer) { + request(1); + } + }); + + verify(producer.get(), never()).request(0); + verify(producer.get(), times(1)).request(Flowable.bufferSize() - 1); + } + + @Test + public void dispose() { + TestHelper.checkDisposed(PublishProcessor.create().scan(new BiFunction<Object, Object, Object>() { + @Override + public Object apply(Object a, Object b) throws Exception { + return a; + } + })); + + TestHelper.checkDisposed(PublishProcessor.<Integer>create().scan(0, new BiFunction<Integer, Integer, Integer>() { + @Override + public Integer apply(Integer a, Integer b) throws Exception { + return a + b; + } + })); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeFlowable(new Function<Flowable<Object>, Flowable<Object>>() { + @Override + public Flowable<Object> apply(Flowable<Object> f) throws Exception { + return f.scan(new BiFunction<Object, Object, Object>() { + @Override + public Object apply(Object a, Object b) throws Exception { + return a; + } + }); + } + }); + + TestHelper.checkDoubleOnSubscribeFlowable(new Function<Flowable<Object>, Flowable<Object>>() { + @Override + public Flowable<Object> apply(Flowable<Object> f) throws Exception { + return f.scan(0, new BiFunction<Object, Object, Object>() { + @Override + public Object apply(Object a, Object b) throws Exception { + return a; + } + }); + } + }); + } + + @Test + public void error() { + Flowable.error(new TestException()) + .scan(new BiFunction<Object, Object, Object>() { + @Override + public Object apply(Object a, Object b) throws Exception { + return a; + } + }) + .test() + .assertFailure(TestException.class); + } + + @Test + public void neverSource() { + Flowable.<Integer>never() + .scan(0, new BiFunction<Integer, Integer, Integer>() { + @Override + public Integer apply(Integer a, Integer b) throws Exception { + return a + b; + } + }) + .test() + .assertValue(0) + .assertNoErrors() + .assertNotComplete(); + } + + @Test + public void unsubscribeScan() { + + FlowableEventStream.getEventStream("HTTP-ClusterB", 20) + .scan(new HashMap<>(), new BiFunction<HashMap<String, String>, Event, HashMap<String, String>>() { + @Override + public HashMap<String, String> apply(HashMap<String, String> accum, Event perInstanceEvent) { + accum.put("instance", perInstanceEvent.instanceId); + return accum; + } + }) + .take(10) + .blockingForEach(new Consumer<HashMap<String, String>>() { + @Override + public void accept(HashMap<String, String> v) { + System.out.println(v); + } + }); + } + + @Test + public void scanWithSeedDoesNotEmitErrorTwiceIfScanFunctionThrows() { + final List<Throwable> list = new CopyOnWriteArrayList<>(); + Consumer<Throwable> errorConsumer = new Consumer<Throwable>() { + @Override + public void accept(Throwable t) throws Exception { + list.add(t); + }}; + try { + RxJavaPlugins.setErrorHandler(errorConsumer); + final RuntimeException e = new RuntimeException(); + final RuntimeException e2 = new RuntimeException(); + Burst.items(1).error(e2) + .scan(0, throwingBiFunction(e)) + .test() + .assertValues(0) + .assertError(e); + + assertEquals("" + list, 1, list.size()); + assertTrue("" + list, list.get(0) instanceof UndeliverableException); + assertEquals(e2, list.get(0).getCause()); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void scanWithSeedDoesNotEmitTerminalEventTwiceIfScanFunctionThrows() { + final RuntimeException e = new RuntimeException(); + Burst.item(1).create() + .scan(0, throwingBiFunction(e)) + .test() + .assertValue(0) + .assertError(e); + } + + @Test + public void scanWithSeedDoesNotProcessOnNextAfterTerminalEventIfScanFunctionThrows() { + final RuntimeException e = new RuntimeException(); + final AtomicInteger count = new AtomicInteger(); + Burst.items(1, 2).create().scan(0, new BiFunction<Integer, Integer, Integer>() { + + @Override + public Integer apply(Integer n1, Integer n2) throws Exception { + count.incrementAndGet(); + throw e; + }}) + .test() + .assertValues(0) + .assertError(e); + assertEquals(1, count.get()); + } + + @Test + public void scanWithSeedCompletesNormally() { + Flowable.just(1, 2, 3).scan(0, SUM) + .test() + .assertValues(0, 1, 3, 6) + .assertComplete(); + } + + @Test + public void scanWithSeedWhenScanSeedProviderThrows() { + final RuntimeException e = new RuntimeException(); + Flowable.just(1, 2, 3).scanWith(throwingSupplier(e), + SUM) + .test() + .assertError(e) + .assertNoValues(); + } + + @Test + public void scanNoSeed() { + Flowable.just(1, 2, 3) + .scan(SUM) + .test() + .assertValues(1, 3, 6) + .assertComplete(); + } + + @Test + public void scanNoSeedDoesNotEmitErrorTwiceIfScanFunctionThrows() { + final List<Throwable> list = new CopyOnWriteArrayList<>(); + Consumer<Throwable> errorConsumer = new Consumer<Throwable>() { + @Override + public void accept(Throwable t) throws Exception { + list.add(t); + }}; + try { + RxJavaPlugins.setErrorHandler(errorConsumer); + final RuntimeException e = new RuntimeException(); + final RuntimeException e2 = new RuntimeException(); + Burst.items(1, 2).error(e2) + .scan(throwingBiFunction(e)) + .test() + .assertValue(1) + .assertError(e); + + assertEquals("" + list, 1, list.size()); + assertTrue("" + list, list.get(0) instanceof UndeliverableException); + assertEquals(e2, list.get(0).getCause()); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void scanNoSeedDoesNotEmitTerminalEventTwiceIfScanFunctionThrows() { + final RuntimeException e = new RuntimeException(); + Burst.items(1, 2).create() + .scan(throwingBiFunction(e)) + .test() + .assertValue(1) + .assertError(e); + } + + @Test + public void scanNoSeedDoesNotProcessOnNextAfterTerminalEventIfScanFunctionThrows() { + final RuntimeException e = new RuntimeException(); + final AtomicInteger count = new AtomicInteger(); + Burst.items(1, 2, 3).create().scan(new BiFunction<Integer, Integer, Integer>() { + + @Override + public Integer apply(Integer n1, Integer n2) throws Exception { + count.incrementAndGet(); + throw e; + }}) + .test() + .assertValue(1) + .assertError(e); + assertEquals(1, count.get()); + } + + private static BiFunction<Integer, Integer, Integer> throwingBiFunction(final RuntimeException e) { + return new BiFunction<Integer, Integer, Integer>() { + @Override + public Integer apply(Integer n1, Integer n2) throws Exception { + throw e; + } + }; + } + + private static final BiFunction<Integer, Integer, Integer> SUM = new BiFunction<Integer, Integer, Integer>() { + + @Override + public Integer apply(Integer t1, Integer t2) throws Exception { + return t1 + t2; + } + }; + + private static Supplier<Integer> throwingSupplier(final RuntimeException e) { + return new Supplier<Integer>() { + @Override + public Integer get() throws Exception { + throw e; + } + }; + } + + @Test + public void scanEmptyBackpressured() { + Flowable.<Integer>empty() + .scan(0, SUM) + .test(1) + .assertResult(0); + } + + @Test + public void scanErrorBackpressured() { + Flowable.<Integer>error(new TestException()) + .scan(0, SUM) + .test(0) + .assertFailure(TestException.class); + } + + @Test + public void scanTake() { + TestSubscriber<Integer> ts = new TestSubscriber<Integer>() { + @Override + public void onNext(Integer t) { + super.onNext(t); + onComplete(); + cancel(); + } + }; + + Flowable.range(1, 10) + .scan(0, SUM) + .subscribe(ts) + ; + + ts.assertResult(0); + } + + @Test + public void scanLong() { + int n = 2 * Flowable.bufferSize(); + + for (int b = 1; b <= n; b *= 2) { + List<Integer> list = Flowable.range(1, n) + .scan(0, new BiFunction<Integer, Integer, Integer>() { + @Override + public Integer apply(Integer a, Integer b) throws Exception { + return b; + } + }) + .rebatchRequests(b) + .toList() + .blockingGet(); + + for (int i = 0; i <= n; i++) { + assertEquals(i, list.get(i).intValue()); + } + } + } + + @Test + public void badRequest() { + TestHelper.assertBadRequestReported(Flowable.<Integer>never().scanWith(() -> 1, (a, b) -> a + b)); + } + + @Test + public void drainMoreWork() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + + TestSubscriber<Integer> ts = pp.scanWith(() -> 0, (a, b) -> a + b) + .doOnNext(v -> { + if (v == 1) { + pp.onNext(2); + pp.onComplete(); + } + }) + .test(); + + pp.onNext(1); + + ts.assertResult(0, 1, 3); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableSequenceEqualTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableSequenceEqualTest.java new file mode 100644 index 0000000000..c73b57777b --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableSequenceEqualTest.java @@ -0,0 +1,616 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import static org.mockito.ArgumentMatchers.isA; +import static org.mockito.Mockito.*; + +import java.util.List; +import java.util.concurrent.TimeUnit; + +import org.junit.Test; +import org.mockito.InOrder; +import org.reactivestreams.Subscriber; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.subscriptions.BooleanSubscription; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.processors.PublishProcessor; +import io.reactivex.rxjava3.schedulers.Schedulers; +import io.reactivex.rxjava3.subscribers.TestSubscriber; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class FlowableSequenceEqualTest extends RxJavaTest { + + @Test + public void flowable1() { + Flowable<Boolean> flowable = Flowable.sequenceEqual( + Flowable.just("one", "two", "three"), + Flowable.just("one", "two", "three")).toFlowable(); + verifyResult(flowable, true); + } + + @Test + public void flowable2() { + Flowable<Boolean> flowable = Flowable.sequenceEqual( + Flowable.just("one", "two", "three"), + Flowable.just("one", "two", "three", "four")).toFlowable(); + verifyResult(flowable, false); + } + + @Test + public void flowable3() { + Flowable<Boolean> flowable = Flowable.sequenceEqual( + Flowable.just("one", "two", "three", "four"), + Flowable.just("one", "two", "three")).toFlowable(); + verifyResult(flowable, false); + } + + @Test + public void withError1Flowable() { + Flowable<Boolean> flowable = Flowable.sequenceEqual( + Flowable.concat(Flowable.just("one"), + Flowable.<String> error(new TestException())), + Flowable.just("one", "two", "three")).toFlowable(); + verifyError(flowable); + } + + @Test + public void withError2Flowable() { + Flowable<Boolean> flowable = Flowable.sequenceEqual( + Flowable.just("one", "two", "three"), + Flowable.concat(Flowable.just("one"), + Flowable.<String> error(new TestException()))).toFlowable(); + verifyError(flowable); + } + + @Test + public void withError3Flowable() { + Flowable<Boolean> flowable = Flowable.sequenceEqual( + Flowable.concat(Flowable.just("one"), + Flowable.<String> error(new TestException())), + Flowable.concat(Flowable.just("one"), + Flowable.<String> error(new TestException()))).toFlowable(); + verifyError(flowable); + } + + @Test + public void withEmpty1Flowable() { + Flowable<Boolean> flowable = Flowable.sequenceEqual( + Flowable.<String> empty(), + Flowable.just("one", "two", "three")).toFlowable(); + verifyResult(flowable, false); + } + + @Test + public void withEmpty2Flowable() { + Flowable<Boolean> flowable = Flowable.sequenceEqual( + Flowable.just("one", "two", "three"), + Flowable.<String> empty()).toFlowable(); + verifyResult(flowable, false); + } + + @Test + public void withEmpty3Flowable() { + Flowable<Boolean> flowable = Flowable.sequenceEqual( + Flowable.<String> empty(), Flowable.<String> empty()).toFlowable(); + verifyResult(flowable, true); + } + + @Test + public void withEqualityErrorFlowable() { + Flowable<Boolean> flowable = Flowable.sequenceEqual( + Flowable.just("one"), Flowable.just("one"), + new BiPredicate<String, String>() { + @Override + public boolean test(String t1, String t2) { + throw new TestException(); + } + }).toFlowable(); + verifyError(flowable); + } + + @Test + public void one() { + Single<Boolean> single = Flowable.sequenceEqual( + Flowable.just("one", "two", "three"), + Flowable.just("one", "two", "three")); + verifyResult(single, true); + } + + @Test + public void two() { + Single<Boolean> single = Flowable.sequenceEqual( + Flowable.just("one", "two", "three"), + Flowable.just("one", "two", "three", "four")); + verifyResult(single, false); + } + + @Test + public void three() { + Single<Boolean> single = Flowable.sequenceEqual( + Flowable.just("one", "two", "three", "four"), + Flowable.just("one", "two", "three")); + verifyResult(single, false); + } + + @Test + public void withError1() { + Single<Boolean> single = Flowable.sequenceEqual( + Flowable.concat(Flowable.just("one"), + Flowable.<String> error(new TestException())), + Flowable.just("one", "two", "three")); + verifyError(single); + } + + @Test + public void withError2() { + Single<Boolean> single = Flowable.sequenceEqual( + Flowable.just("one", "two", "three"), + Flowable.concat(Flowable.just("one"), + Flowable.<String> error(new TestException()))); + verifyError(single); + } + + @Test + public void withError3() { + Single<Boolean> single = Flowable.sequenceEqual( + Flowable.concat(Flowable.just("one"), + Flowable.<String> error(new TestException())), + Flowable.concat(Flowable.just("one"), + Flowable.<String> error(new TestException()))); + verifyError(single); + } + + @Test + public void withEmpty1() { + Single<Boolean> single = Flowable.sequenceEqual( + Flowable.<String> empty(), + Flowable.just("one", "two", "three")); + verifyResult(single, false); + } + + @Test + public void withEmpty2() { + Single<Boolean> single = Flowable.sequenceEqual( + Flowable.just("one", "two", "three"), + Flowable.<String> empty()); + verifyResult(single, false); + } + + @Test + public void withEmpty3() { + Single<Boolean> single = Flowable.sequenceEqual( + Flowable.<String> empty(), Flowable.<String> empty()); + verifyResult(single, true); + } + + @Test + public void withEqualityError() { + Single<Boolean> single = Flowable.sequenceEqual( + Flowable.just("one"), Flowable.just("one"), + new BiPredicate<String, String>() { + @Override + public boolean test(String t1, String t2) { + throw new TestException(); + } + }); + verifyError(single); + } + + private void verifyResult(Flowable<Boolean> flowable, boolean result) { + Subscriber<Boolean> subscriber = TestHelper.mockSubscriber(); + + flowable.subscribe(subscriber); + + InOrder inOrder = inOrder(subscriber); + inOrder.verify(subscriber, times(1)).onNext(result); + inOrder.verify(subscriber).onComplete(); + inOrder.verifyNoMoreInteractions(); + } + + private void verifyResult(Single<Boolean> single, boolean result) { + SingleObserver<Boolean> observer = TestHelper.mockSingleObserver(); + + single.subscribe(observer); + + InOrder inOrder = inOrder(observer); + inOrder.verify(observer, times(1)).onSuccess(result); + inOrder.verifyNoMoreInteractions(); + } + + private void verifyError(Flowable<Boolean> flowable) { + Subscriber<Boolean> subscriber = TestHelper.mockSubscriber(); + flowable.subscribe(subscriber); + + InOrder inOrder = inOrder(subscriber); + inOrder.verify(subscriber, times(1)).onError(isA(TestException.class)); + inOrder.verifyNoMoreInteractions(); + } + + private void verifyError(Single<Boolean> single) { + SingleObserver<Boolean> observer = TestHelper.mockSingleObserver(); + single.subscribe(observer); + + InOrder inOrder = inOrder(observer); + inOrder.verify(observer, times(1)).onError(isA(TestException.class)); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void prefetch() { + + Flowable.sequenceEqual(Flowable.range(1, 20), Flowable.range(1, 20), 2) + .test() + .assertResult(true); + } + + @Test + public void disposed() { + TestHelper.checkDisposed(Flowable.sequenceEqual(Flowable.just(1), Flowable.just(2))); + } + + @Test + public void simpleInequal() { + Flowable.sequenceEqual(Flowable.just(1), Flowable.just(2)) + .test() + .assertResult(false); + } + + @Test + public void simpleInequalObservable() { + Flowable.sequenceEqual(Flowable.just(1), Flowable.just(2)) + .toFlowable() + .test() + .assertResult(false); + } + + @Test + public void onNextCancelRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final PublishProcessor<Integer> pp = PublishProcessor.create(); + + final TestObserver<Boolean> to = Flowable.sequenceEqual(Flowable.never(), pp).test(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + to.dispose(); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + pp.onNext(1); + } + }; + + TestHelper.race(r1, r2); + + to.assertEmpty(); + } + } + + @Test + public void onNextCancelRaceObservable() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final PublishProcessor<Integer> pp = PublishProcessor.create(); + + final TestSubscriber<Boolean> ts = Flowable.sequenceEqual(Flowable.never(), pp).toFlowable().test(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + ts.cancel(); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + pp.onNext(1); + } + }; + + TestHelper.race(r1, r2); + + ts.assertEmpty(); + } + } + + @Test + public void disposedFlowable() { + TestHelper.checkDisposed(Flowable.sequenceEqual(Flowable.just(1), Flowable.just(2)).toFlowable()); + } + + @Test + public void prefetchFlowable() { + Flowable.sequenceEqual(Flowable.range(1, 20), Flowable.range(1, 20), 2) + .toFlowable() + .test() + .assertResult(true); + } + + @Test + public void longSequenceEqualsFlowable() { + Flowable<Integer> source = Flowable.range(1, Flowable.bufferSize() * 4).subscribeOn(Schedulers.computation()); + + Flowable.sequenceEqual(source, source) + .toFlowable() + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(true); + } + + @Test + public void syncFusedCrashFlowable() { + Flowable<Integer> source = Flowable.range(1, 10).map(new Function<Integer, Integer>() { + @Override + public Integer apply(Integer v) throws Exception { throw new TestException(); } + }); + + Flowable.sequenceEqual(source, Flowable.range(1, 10).hide()) + .toFlowable() + .test() + .assertFailure(TestException.class); + + Flowable.sequenceEqual(Flowable.range(1, 10).hide(), source) + .toFlowable() + .test() + .assertFailure(TestException.class); + } + + @Test + public void cancelAndDrainRaceFlowable() { + Flowable<Object> neverNever = new Flowable<Object>() { + @Override + protected void subscribeActual(Subscriber<? super Object> s) { + } + }; + + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final TestSubscriber<Boolean> ts = new TestSubscriber<>(); + + final PublishProcessor<Integer> pp = PublishProcessor.create(); + + boolean swap = (i & 1) == 0; + + Flowable.sequenceEqual(swap ? pp : neverNever, swap ? neverNever : pp) + .toFlowable() + .subscribe(ts); + + Runnable r1 = new Runnable() { + @Override + public void run() { + pp.onNext(1); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + ts.cancel(); + } + }; + + TestHelper.race(r1, r2); + + ts.assertEmpty(); + } + } + + @Test + public void sourceOverflowsFlowable() { + Flowable.sequenceEqual(Flowable.never(), new Flowable<Object>() { + @Override + protected void subscribeActual(Subscriber<? super Object> s) { + s.onSubscribe(new BooleanSubscription()); + for (int i = 0; i < 10; i++) { + s.onNext(i); + } + } + }, 8) + .toFlowable() + .test() + .assertFailure(MissingBackpressureException.class); + } + + @Test + public void doubleErrorFlowable() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + Flowable.sequenceEqual(Flowable.never(), new Flowable<Object>() { + @Override + protected void subscribeActual(Subscriber<? super Object> s) { + s.onSubscribe(new BooleanSubscription()); + s.onError(new TestException("First")); + s.onError(new TestException("Second")); + } + }, 8) + .toFlowable() + .to(TestHelper.<Boolean>testConsumer()) + .assertFailureAndMessage(TestException.class, "First"); + + TestHelper.assertUndeliverable(errors, 0, TestException.class, "Second"); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void longSequenceEquals() { + Flowable<Integer> source = Flowable.range(1, Flowable.bufferSize() * 4).subscribeOn(Schedulers.computation()); + + Flowable.sequenceEqual(source, source) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(true); + } + + @Test + public void syncFusedCrash() { + Flowable<Integer> source = Flowable.range(1, 10).map(new Function<Integer, Integer>() { + @Override + public Integer apply(Integer v) throws Exception { throw new TestException(); } + }); + + Flowable.sequenceEqual(source, Flowable.range(1, 10).hide()) + .test() + .assertFailure(TestException.class); + + Flowable.sequenceEqual(Flowable.range(1, 10).hide(), source) + .test() + .assertFailure(TestException.class); + } + + @Test + public void cancelAndDrainRace() { + Flowable<Object> neverNever = new Flowable<Object>() { + @Override + protected void subscribeActual(Subscriber<? super Object> s) { + } + }; + + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final TestObserver<Boolean> to = new TestObserver<>(); + + final PublishProcessor<Integer> pp = PublishProcessor.create(); + + boolean swap = (i & 1) == 0; + + Flowable.sequenceEqual(swap ? pp : neverNever, swap ? neverNever : pp) + .subscribe(to); + + Runnable r1 = new Runnable() { + @Override + public void run() { + pp.onNext(1); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + to.dispose(); + } + }; + + TestHelper.race(r1, r2); + + to.assertEmpty(); + } + } + + @Test + public void sourceOverflows() { + Flowable.sequenceEqual(Flowable.never(), new Flowable<Object>() { + @Override + protected void subscribeActual(Subscriber<? super Object> s) { + s.onSubscribe(new BooleanSubscription()); + for (int i = 0; i < 10; i++) { + s.onNext(i); + } + } + }, 8) + .test() + .assertFailure(MissingBackpressureException.class); + } + + @Test + public void doubleError() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + Flowable.sequenceEqual(Flowable.never(), new Flowable<Object>() { + @Override + protected void subscribeActual(Subscriber<? super Object> s) { + s.onSubscribe(new BooleanSubscription()); + s.onError(new TestException("First")); + s.onError(new TestException("Second")); + } + }, 8) + .to(TestHelper.<Boolean>testConsumer()) + .assertFailureAndMessage(TestException.class, "First"); + + TestHelper.assertUndeliverable(errors, 0, TestException.class, "Second"); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void undeliverableUponCancel() { + TestHelper.checkUndeliverableUponCancel(new FlowableConverter<Integer, Single<Boolean>>() { + @Override + public Single<Boolean> apply(Flowable<Integer> upstream) { + return Flowable.sequenceEqual(Flowable.just(1).hide(), upstream); + } + }); + } + + @Test + public void undeliverableUponCancelAsFlowable() { + TestHelper.checkUndeliverableUponCancel(new FlowableConverter<Integer, Flowable<Boolean>>() { + @Override + public Flowable<Boolean> apply(Flowable<Integer> upstream) { + return Flowable.sequenceEqual(Flowable.just(1).hide(), upstream).toFlowable(); + } + }); + } + + @Test + public void undeliverableUponCancel2() { + TestHelper.checkUndeliverableUponCancel(new FlowableConverter<Integer, Single<Boolean>>() { + @Override + public Single<Boolean> apply(Flowable<Integer> upstream) { + return Flowable.sequenceEqual(upstream, Flowable.just(1).hide()); + } + }); + } + + @Test + public void undeliverableUponCancelAsFlowable2() { + TestHelper.checkUndeliverableUponCancel(new FlowableConverter<Integer, Flowable<Boolean>>() { + @Override + public Flowable<Boolean> apply(Flowable<Integer> upstream) { + return Flowable.sequenceEqual(upstream, Flowable.just(1).hide()).toFlowable(); + } + }); + } + + @Test + public void fusionRejected() { + Flowable.sequenceEqual(TestHelper.rejectFlowableFusion(), Flowable.never()) + .test() + .assertEmpty(); + } + + @Test + public void fusionRejectedFlowable() { + Flowable.sequenceEqual(TestHelper.rejectFlowableFusion(), Flowable.never()) + .toFlowable() + .test() + .assertEmpty(); + } + + @Test + public void asyncSourceCompare() { + Flowable.sequenceEqual(Flowable.fromCallable(() -> 1), Flowable.just(1)) + .test() + .assertResult(true); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableSerializeTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableSerializeTest.java new file mode 100644 index 0000000000..913c598126 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableSerializeTest.java @@ -0,0 +1,440 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.*; +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.internal.subscriptions.BooleanSubscription; +import io.reactivex.rxjava3.subscribers.DefaultSubscriber; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class FlowableSerializeTest extends RxJavaTest { + + Subscriber<String> subscriber; + + @Before + public void before() { + subscriber = TestHelper.mockSubscriber(); + } + + @Test + public void singleThreadedBasic() { + TestSingleThreadedObservable onSubscribe = new TestSingleThreadedObservable("one", "two", "three"); + Flowable<String> w = Flowable.unsafeCreate(onSubscribe); + + w.serialize().subscribe(subscriber); + onSubscribe.waitToFinish(); + + verify(subscriber, times(1)).onNext("one"); + verify(subscriber, times(1)).onNext("two"); + verify(subscriber, times(1)).onNext("three"); + verify(subscriber, never()).onError(any(Throwable.class)); + verify(subscriber, times(1)).onComplete(); + // non-deterministic because unsubscribe happens after 'waitToFinish' releases + // so commenting out for now as this is not a critical thing to test here + // verify(s, times(1)).unsubscribe(); + } + + @Test + public void multiThreadedBasic() { + TestMultiThreadedObservable onSubscribe = new TestMultiThreadedObservable("one", "two", "three"); + Flowable<String> w = Flowable.unsafeCreate(onSubscribe); + + BusyObserver busyobserver = new BusyObserver(); + + w.serialize().subscribe(busyobserver); + onSubscribe.waitToFinish(); + + assertEquals(3, busyobserver.onNextCount.get()); + assertFalse(busyobserver.onError); + assertTrue(busyobserver.onComplete); + // non-deterministic because unsubscribe happens after 'waitToFinish' releases + // so commenting out for now as this is not a critical thing to test here + // verify(s, times(1)).unsubscribe(); + + // we can have concurrency ... + assertTrue(onSubscribe.maxConcurrentThreads.get() > 1); + // ... but the onNext execution should be single threaded + assertEquals(1, busyobserver.maxConcurrentThreads.get()); + } + + @Test + public void multiThreadedWithNPEFlaky() throws InterruptedException { + int max = 9; + for (int i = 0; i <= max; i++) { + try { + multiThreadedWithNPE(); + return; + } catch (AssertionError ex) { + if (i == max) { + throw ex; + } + } + Thread.sleep((long)(1000 * Math.random() + 100)); + } + } + + void multiThreadedWithNPE() { + TestMultiThreadedObservable onSubscribe = new TestMultiThreadedObservable("one", "two", "three", null); + Flowable<String> w = Flowable.unsafeCreate(onSubscribe); + + BusyObserver busyobserver = new BusyObserver(); + + w.serialize().subscribe(busyobserver); + onSubscribe.waitToFinish(); + + System.out.println("maxConcurrentThreads: " + onSubscribe.maxConcurrentThreads.get()); + + // we can't know how many onNext calls will occur since they each run on a separate thread + // that depends on thread scheduling so 0, 1, 2 and 3 are all valid options + // assertEquals(3, busyobserver.onNextCount.get()); + assertTrue(busyobserver.onNextCount.get() < 4); + assertTrue(busyobserver.onError); + // no onComplete because onError was invoked + assertFalse(busyobserver.onComplete); + // non-deterministic because unsubscribe happens after 'waitToFinish' releases + // so commenting out for now as this is not a critical thing to test here + //verify(s, times(1)).unsubscribe(); + + // we can have concurrency ... + assertTrue(onSubscribe.maxConcurrentThreads.get() > 1); + // ... but the onNext execution should be single threaded + assertEquals(1, busyobserver.maxConcurrentThreads.get()); + } + + @Test + public void multiThreadedWithNPEinMiddleFlaky() throws InterruptedException { + int max = 9; + for (int i = 0; i <= max; i++) { + try { + multiThreadedWithNPEinMiddle(); + return; + } catch (AssertionError ex) { + if (i == max) { + throw ex; + } + } + Thread.sleep((long)(1000 * Math.random() + 100)); + } + } + + void multiThreadedWithNPEinMiddle() { + boolean lessThan9 = false; + for (int i = 0; i < 3; i++) { + TestMultiThreadedObservable onSubscribe = new TestMultiThreadedObservable("one", "two", "three", null, "four", "five", "six", "seven", "eight", "nine"); + Flowable<String> w = Flowable.unsafeCreate(onSubscribe); + + BusyObserver busyobserver = new BusyObserver(); + + w.serialize().subscribe(busyobserver); + onSubscribe.waitToFinish(); + + System.out.println("maxConcurrentThreads: " + onSubscribe.maxConcurrentThreads.get()); + // this should not always be the full number of items since the error should (very often) + // stop it before it completes all 9 + System.out.println("onNext count: " + busyobserver.onNextCount.get()); + if (busyobserver.onNextCount.get() < 9) { + lessThan9 = true; + } + assertTrue(busyobserver.onError); + // no onComplete because onError was invoked + assertFalse(busyobserver.onComplete); + // non-deterministic because unsubscribe happens after 'waitToFinish' releases + // so commenting out for now as this is not a critical thing to test here + // verify(s, times(1)).unsubscribe(); + + // we can have concurrency ... + assertTrue(onSubscribe.maxConcurrentThreads.get() > 1); + // ... but the onNext execution should be single threaded + assertEquals(1, busyobserver.maxConcurrentThreads.get()); + } + assertTrue(lessThan9); + } + + /** + * A thread that will pass data to onNext. + */ + static class OnNextThread implements Runnable { + + private final DefaultSubscriber<String> subscriber; + private final int numStringsToSend; + + OnNextThread(DefaultSubscriber<String> subscriber, int numStringsToSend) { + this.subscriber = subscriber; + this.numStringsToSend = numStringsToSend; + } + + @Override + public void run() { + for (int i = 0; i < numStringsToSend; i++) { + subscriber.onNext("aString"); + } + } + } + + /** + * A thread that will call onError or onNext. + */ + static class CompletionThread implements Runnable { + + private final DefaultSubscriber<String> subscriber; + private final TestConcurrencyobserverEvent event; + private final Future<?>[] waitOnThese; + + CompletionThread(DefaultSubscriber<String> subscriber, TestConcurrencyobserverEvent event, Future<?>... waitOnThese) { + this.subscriber = subscriber; + this.event = event; + this.waitOnThese = waitOnThese; + } + + @Override + public void run() { + /* if we have 'waitOnThese' futures, we'll wait on them before proceeding */ + if (waitOnThese != null) { + for (Future<?> f : waitOnThese) { + try { + f.get(); + } catch (Throwable e) { + System.err.println("Error while waiting on future in CompletionThread"); + } + } + } + + /* send the event */ + if (event == TestConcurrencyobserverEvent.onError) { + subscriber.onError(new RuntimeException("mocked exception")); + } else if (event == TestConcurrencyobserverEvent.onComplete) { + subscriber.onComplete(); + + } else { + throw new IllegalArgumentException("Expecting either onError or onComplete"); + } + } + } + + enum TestConcurrencyobserverEvent { + onComplete, onError, onNext + } + + /** + * This spawns a single thread for the subscribe execution. + */ + private static class TestSingleThreadedObservable implements Publisher<String> { + + final String[] values; + private Thread t; + + TestSingleThreadedObservable(final String... values) { + this.values = values; + + } + + @Override + public void subscribe(final Subscriber<? super String> subscriber) { + subscriber.onSubscribe(new BooleanSubscription()); + System.out.println("TestSingleThreadedObservable subscribed to ..."); + t = new Thread(new Runnable() { + + @Override + public void run() { + try { + System.out.println("running TestSingleThreadedObservable thread"); + for (String s : values) { + System.out.println("TestSingleThreadedObservable onNext: " + s); + subscriber.onNext(s); + } + subscriber.onComplete(); + } catch (Throwable e) { + throw new RuntimeException(e); + } + } + + }); + System.out.println("starting TestSingleThreadedObservable thread"); + t.start(); + System.out.println("done starting TestSingleThreadedObservable thread"); + } + + public void waitToFinish() { + try { + t.join(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + + } + + /** + * This spawns a thread for the subscription, then a separate thread for each onNext call. + */ + private static class TestMultiThreadedObservable implements Publisher<String> { + final String[] values; + Thread t; + AtomicInteger threadsRunning = new AtomicInteger(); + AtomicInteger maxConcurrentThreads = new AtomicInteger(); + ExecutorService threadPool; + + TestMultiThreadedObservable(String... values) { + this.values = values; + this.threadPool = Executors.newCachedThreadPool(); + } + + @Override + public void subscribe(final Subscriber<? super String> subscriber) { + subscriber.onSubscribe(new BooleanSubscription()); + System.out.println("TestMultiThreadedObservable subscribed to ..."); + final NullPointerException npe = new NullPointerException(); + t = new Thread(new Runnable() { + + @Override + public void run() { + try { + System.out.println("running TestMultiThreadedObservable thread"); + for (final String s : values) { + threadPool.execute(new Runnable() { + + @Override + public void run() { + threadsRunning.incrementAndGet(); + try { + // perform onNext call + if (s == null) { + System.out.println("TestMultiThreadedObservable onNext: null"); + // force an error + throw npe; + } else { + try { + Thread.sleep(10); + } catch (InterruptedException ex) { + // ignored + } + System.out.println("TestMultiThreadedObservable onNext: " + s); + } + subscriber.onNext(s); + // capture 'maxThreads' + int concurrentThreads = threadsRunning.get(); + int maxThreads = maxConcurrentThreads.get(); + if (concurrentThreads > maxThreads) { + maxConcurrentThreads.compareAndSet(maxThreads, concurrentThreads); + } + } catch (Throwable e) { + subscriber.onError(e); + } finally { + threadsRunning.decrementAndGet(); + } + } + }); + } + // we are done spawning threads + threadPool.shutdown(); + } catch (Throwable e) { + throw new RuntimeException(e); + } + + // wait until all threads are done, then mark it as COMPLETED + try { + // wait for all the threads to finish + threadPool.awaitTermination(2, TimeUnit.SECONDS); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + subscriber.onComplete(); + } + }); + System.out.println("starting TestMultiThreadedObservable thread"); + t.start(); + System.out.println("done starting TestMultiThreadedObservable thread"); + } + + public void waitToFinish() { + try { + t.join(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + } + + private static class BusyObserver extends DefaultSubscriber<String> { + volatile boolean onComplete; + volatile boolean onError; + AtomicInteger onNextCount = new AtomicInteger(); + AtomicInteger threadsRunning = new AtomicInteger(); + AtomicInteger maxConcurrentThreads = new AtomicInteger(); + + @Override + public void onComplete() { + threadsRunning.incrementAndGet(); + + System.out.println(">>> Busyobserver received onComplete"); + onComplete = true; + + int concurrentThreads = threadsRunning.get(); + int maxThreads = maxConcurrentThreads.get(); + if (concurrentThreads > maxThreads) { + maxConcurrentThreads.compareAndSet(maxThreads, concurrentThreads); + } + threadsRunning.decrementAndGet(); + } + + @Override + public void onError(Throwable e) { + threadsRunning.incrementAndGet(); + + System.out.println(">>> Busyobserver received onError: " + e.getMessage()); + onError = true; + + int concurrentThreads = threadsRunning.get(); + int maxThreads = maxConcurrentThreads.get(); + if (concurrentThreads > maxThreads) { + maxConcurrentThreads.compareAndSet(maxThreads, concurrentThreads); + } + threadsRunning.decrementAndGet(); + } + + @Override + public void onNext(String args) { + threadsRunning.incrementAndGet(); + try { + onNextCount.incrementAndGet(); + System.out.println(">>> Busyobserver received onNext: " + args); + try { + // simulate doing something computational + Thread.sleep(200); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } finally { + // capture 'maxThreads' + int concurrentThreads = threadsRunning.get(); + int maxThreads = maxConcurrentThreads.get(); + if (concurrentThreads > maxThreads) { + maxConcurrentThreads.compareAndSet(maxThreads, concurrentThreads); + } + threadsRunning.decrementAndGet(); + } + } + + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableSingleTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableSingleTest.java new file mode 100644 index 0000000000..85fbfbc00d --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableSingleTest.java @@ -0,0 +1,813 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +import java.util.*; +import java.util.concurrent.atomic.*; + +import org.junit.Test; +import org.mockito.InOrder; +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.processors.PublishProcessor; +import io.reactivex.rxjava3.subscribers.*; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class FlowableSingleTest extends RxJavaTest { + + @Test + public void singleFlowable() { + Flowable<Integer> flowable = Flowable.just(1).singleElement().toFlowable(); + + Subscriber<Integer> subscriber = TestHelper.mockSubscriber(); + flowable.subscribe(subscriber); + + InOrder inOrder = inOrder(subscriber); + inOrder.verify(subscriber, times(1)).onNext(1); + inOrder.verify(subscriber, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void singleWithTooManyElementsFlowable() { + Flowable<Integer> flowable = Flowable.just(1, 2).singleElement().toFlowable(); + + Subscriber<Integer> subscriber = TestHelper.mockSubscriber(); + flowable.subscribe(subscriber); + + InOrder inOrder = inOrder(subscriber); + inOrder.verify(subscriber, times(1)).onError( + isA(IllegalArgumentException.class)); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void singleWithEmptyFlowable() { + Flowable<Integer> flowable = Flowable.<Integer> empty().singleElement().toFlowable(); + + Subscriber<Integer> subscriber = TestHelper.mockSubscriber(); + flowable.subscribe(subscriber); + + InOrder inOrder = inOrder(subscriber); + inOrder.verify(subscriber).onComplete(); + inOrder.verify(subscriber, never()).onError(any(Throwable.class)); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void singleDoesNotRequestMoreThanItNeedsIf1Then2RequestedFlowable() { + final List<Long> requests = new ArrayList<>(); + Flowable.just(1) + // + .doOnRequest(new LongConsumer() { + @Override + public void accept(long n) { + requests.add(n); + } + }) + // + .singleElement() + // + .toFlowable() + .subscribe(new DefaultSubscriber<Integer>() { + + @Override + public void onStart() { + request(1); + } + + @Override + public void onComplete() { + + } + + @Override + public void onError(Throwable e) { + + } + + @Override + public void onNext(Integer t) { + request(2); + } + }); + // FIXME single now triggers fast-path + assertEquals(Arrays.asList(Long.MAX_VALUE), requests); + } + + @Test + public void singleDoesNotRequestMoreThanItNeedsIf3RequestedFlowable() { + final List<Long> requests = new ArrayList<>(); + Flowable.just(1) + // + .doOnRequest(new LongConsumer() { + @Override + public void accept(long n) { + requests.add(n); + } + }) + // + .singleElement() + // + .toFlowable() + .subscribe(new DefaultSubscriber<Integer>() { + + @Override + public void onStart() { + request(3); + } + + @Override + public void onComplete() { + + } + + @Override + public void onError(Throwable e) { + + } + + @Override + public void onNext(Integer t) { + } + }); + // FIXME single now triggers fast-path + assertEquals(Arrays.asList(Long.MAX_VALUE), requests); + } + + @Test + public void singleRequestsExactlyWhatItNeedsIf1RequestedFlowable() { + final List<Long> requests = new ArrayList<>(); + Flowable.just(1) + // + .doOnRequest(new LongConsumer() { + @Override + public void accept(long n) { + requests.add(n); + } + }) + // + .singleElement() + // + .toFlowable() + .subscribe(new DefaultSubscriber<Integer>() { + + @Override + public void onStart() { + request(1); + } + + @Override + public void onComplete() { + + } + + @Override + public void onError(Throwable e) { + + } + + @Override + public void onNext(Integer t) { + } + }); + // FIXME single now triggers fast-path + assertEquals(Arrays.asList(Long.MAX_VALUE), requests); + } + + @Test + public void singleWithPredicateFlowable() { + Flowable<Integer> flowable = Flowable.just(1, 2) + .filter( + new Predicate<Integer>() { + + @Override + public boolean test(Integer t1) { + return t1 % 2 == 0; + } + }) + .singleElement().toFlowable(); + + Subscriber<Integer> subscriber = TestHelper.mockSubscriber(); + flowable.subscribe(subscriber); + + InOrder inOrder = inOrder(subscriber); + inOrder.verify(subscriber, times(1)).onNext(2); + inOrder.verify(subscriber, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void singleWithPredicateAndTooManyElementsFlowable() { + Flowable<Integer> flowable = Flowable.just(1, 2, 3, 4) + .filter( + new Predicate<Integer>() { + + @Override + public boolean test(Integer t1) { + return t1 % 2 == 0; + } + }) + .singleElement().toFlowable(); + + Subscriber<Integer> subscriber = TestHelper.mockSubscriber(); + flowable.subscribe(subscriber); + + InOrder inOrder = inOrder(subscriber); + inOrder.verify(subscriber, times(1)).onError( + isA(IllegalArgumentException.class)); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void singleWithPredicateAndEmptyFlowable() { + Flowable<Integer> flowable = Flowable.just(1) + .filter( + new Predicate<Integer>() { + + @Override + public boolean test(Integer t1) { + return t1 % 2 == 0; + } + }) + .singleElement().toFlowable(); + Subscriber<Integer> subscriber = TestHelper.mockSubscriber(); + flowable.subscribe(subscriber); + + InOrder inOrder = inOrder(subscriber); + inOrder.verify(subscriber).onComplete(); + inOrder.verify(subscriber, never()).onError(any(Throwable.class)); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void singleOrDefaultFlowable() { + Flowable<Integer> flowable = Flowable.just(1).single(2).toFlowable(); + + Subscriber<Integer> subscriber = TestHelper.mockSubscriber(); + flowable.subscribe(subscriber); + + InOrder inOrder = inOrder(subscriber); + inOrder.verify(subscriber, times(1)).onNext(1); + inOrder.verify(subscriber, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void singleOrDefaultWithTooManyElementsFlowable() { + Flowable<Integer> flowable = Flowable.just(1, 2).single(3).toFlowable(); + + Subscriber<Integer> subscriber = TestHelper.mockSubscriber(); + flowable.subscribe(subscriber); + + InOrder inOrder = inOrder(subscriber); + inOrder.verify(subscriber, times(1)).onError( + isA(IllegalArgumentException.class)); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void singleOrDefaultWithEmptyFlowable() { + Flowable<Integer> flowable = Flowable.<Integer> empty() + .single(1).toFlowable(); + + Subscriber<Integer> subscriber = TestHelper.mockSubscriber(); + flowable.subscribe(subscriber); + + InOrder inOrder = inOrder(subscriber); + inOrder.verify(subscriber, times(1)).onNext(1); + inOrder.verify(subscriber, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void singleOrDefaultWithPredicateFlowable() { + Flowable<Integer> flowable = Flowable.just(1, 2) + .filter(new Predicate<Integer>() { + @Override + public boolean test(Integer t1) { + return t1 % 2 == 0; + } + }) + .single(4).toFlowable(); + + Subscriber<Integer> subscriber = TestHelper.mockSubscriber(); + flowable.subscribe(subscriber); + + InOrder inOrder = inOrder(subscriber); + inOrder.verify(subscriber, times(1)).onNext(2); + inOrder.verify(subscriber, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void singleOrDefaultWithPredicateAndTooManyElementsFlowable() { + Flowable<Integer> flowable = Flowable.just(1, 2, 3, 4) + .filter(new Predicate<Integer>() { + @Override + public boolean test(Integer t1) { + return t1 % 2 == 0; + } + }) + .single(6).toFlowable(); + + Subscriber<Integer> subscriber = TestHelper.mockSubscriber(); + flowable.subscribe(subscriber); + + InOrder inOrder = inOrder(subscriber); + inOrder.verify(subscriber, times(1)).onError( + isA(IllegalArgumentException.class)); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void singleOrDefaultWithPredicateAndEmptyFlowable() { + Flowable<Integer> flowable = Flowable.just(1) + .filter(new Predicate<Integer>() { + @Override + public boolean test(Integer t1) { + return t1 % 2 == 0; + } + }) + .single(2).toFlowable(); + + Subscriber<Integer> subscriber = TestHelper.mockSubscriber(); + flowable.subscribe(subscriber); + + InOrder inOrder = inOrder(subscriber); + inOrder.verify(subscriber, times(1)).onNext(2); + inOrder.verify(subscriber, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void singleWithBackpressureFlowable() { + Flowable<Integer> flowable = Flowable.just(1, 2).singleElement().toFlowable(); + + Subscriber<Integer> subscriber = spy(new DefaultSubscriber<Integer>() { + + @Override + public void onStart() { + request(1); + } + + @Override + public void onComplete() { + + } + + @Override + public void onError(Throwable e) { + + } + + @Override + public void onNext(Integer integer) { + request(1); + } + }); + flowable.subscribe(subscriber); + + InOrder inOrder = inOrder(subscriber); + inOrder.verify(subscriber, times(1)).onError(isA(IllegalArgumentException.class)); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void single() { + Maybe<Integer> maybe = Flowable.just(1).singleElement(); + + MaybeObserver<Integer> observer = TestHelper.mockMaybeObserver(); + maybe.subscribe(observer); + + InOrder inOrder = inOrder(observer); + inOrder.verify(observer, times(1)).onSuccess(1); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void singleWithTooManyElements() { + Maybe<Integer> maybe = Flowable.just(1, 2).singleElement(); + + MaybeObserver<Integer> observer = TestHelper.mockMaybeObserver(); + maybe.subscribe(observer); + + InOrder inOrder = inOrder(observer); + inOrder.verify(observer, times(1)).onError( + isA(IllegalArgumentException.class)); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void singleWithEmpty() { + Maybe<Integer> maybe = Flowable.<Integer> empty().singleElement(); + + MaybeObserver<Integer> observer = TestHelper.mockMaybeObserver(); + maybe.subscribe(observer); + + InOrder inOrder = inOrder(observer); + inOrder.verify(observer).onComplete(); + inOrder.verify(observer, never()).onError(any(Throwable.class)); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void singleDoesNotRequestMoreThanItNeedsToEmitItem() { + final AtomicLong request = new AtomicLong(); + Flowable.just(1).doOnRequest(new LongConsumer() { + @Override + public void accept(long n) { + request.addAndGet(n); + } + }).blockingSingle(); + // FIXME single now triggers fast-path + assertEquals(Long.MAX_VALUE, request.get()); + } + + @Test + public void singleDoesNotRequestMoreThanItNeedsToEmitErrorFromEmpty() { + final AtomicLong request = new AtomicLong(); + try { + Flowable.empty().doOnRequest(new LongConsumer() { + @Override + public void accept(long n) { + request.addAndGet(n); + } + }).blockingSingle(); + } catch (NoSuchElementException e) { + // FIXME single now triggers fast-path + assertEquals(Long.MAX_VALUE, request.get()); + } + } + + @Test + public void singleDoesNotRequestMoreThanItNeedsToEmitErrorFromMoreThanOne() { + final AtomicLong request = new AtomicLong(); + try { + Flowable.just(1, 2).doOnRequest(new LongConsumer() { + @Override + public void accept(long n) { + request.addAndGet(n); + } + }).blockingSingle(); + } catch (IllegalArgumentException e) { + // FIXME single now triggers fast-path + assertEquals(Long.MAX_VALUE, request.get()); + } + } + + @Test + public void singleWithPredicate() { + Maybe<Integer> maybe = Flowable.just(1, 2) + .filter( + new Predicate<Integer>() { + + @Override + public boolean test(Integer t1) { + return t1 % 2 == 0; + } + }) + .singleElement(); + + MaybeObserver<Integer> observer = TestHelper.mockMaybeObserver(); + maybe.subscribe(observer); + + InOrder inOrder = inOrder(observer); + inOrder.verify(observer, times(1)).onSuccess(2); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void singleWithPredicateAndTooManyElements() { + Maybe<Integer> maybe = Flowable.just(1, 2, 3, 4) + .filter( + new Predicate<Integer>() { + + @Override + public boolean test(Integer t1) { + return t1 % 2 == 0; + } + }) + .singleElement(); + + MaybeObserver<Integer> observer = TestHelper.mockMaybeObserver(); + maybe.subscribe(observer); + + InOrder inOrder = inOrder(observer); + inOrder.verify(observer, times(1)).onError( + isA(IllegalArgumentException.class)); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void singleWithPredicateAndEmpty() { + Maybe<Integer> maybe = Flowable.just(1) + .filter( + new Predicate<Integer>() { + + @Override + public boolean test(Integer t1) { + return t1 % 2 == 0; + } + }) + .singleElement(); + + MaybeObserver<Integer> observer = TestHelper.mockMaybeObserver(); + maybe.subscribe(observer); + + InOrder inOrder = inOrder(observer); + inOrder.verify(observer).onComplete(); + inOrder.verify(observer, never()).onError(any(Throwable.class)); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void singleOrDefault() { + Single<Integer> single = Flowable.just(1).single(2); + + SingleObserver<Integer> observer = TestHelper.mockSingleObserver(); + single.subscribe(observer); + + InOrder inOrder = inOrder(observer); + inOrder.verify(observer, times(1)).onSuccess(1); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void singleOrDefaultWithTooManyElements() { + Single<Integer> single = Flowable.just(1, 2).single(3); + + SingleObserver<Integer> observer = TestHelper.mockSingleObserver(); + single.subscribe(observer); + + InOrder inOrder = inOrder(observer); + inOrder.verify(observer, times(1)).onError( + isA(IllegalArgumentException.class)); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void singleOrDefaultWithEmpty() { + Single<Integer> single = Flowable.<Integer> empty() + .single(1); + + SingleObserver<Integer> observer = TestHelper.mockSingleObserver(); + single.subscribe(observer); + + InOrder inOrder = inOrder(observer); + inOrder.verify(observer, times(1)).onSuccess(1); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void singleOrDefaultWithPredicate() { + Single<Integer> single = Flowable.just(1, 2) + .filter(new Predicate<Integer>() { + @Override + public boolean test(Integer t1) { + return t1 % 2 == 0; + } + }) + .single(4); + + SingleObserver<Integer> observer = TestHelper.mockSingleObserver(); + single.subscribe(observer); + + InOrder inOrder = inOrder(observer); + inOrder.verify(observer, times(1)).onSuccess(2); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void singleOrDefaultWithPredicateAndTooManyElements() { + Single<Integer> single = Flowable.just(1, 2, 3, 4) + .filter(new Predicate<Integer>() { + @Override + public boolean test(Integer t1) { + return t1 % 2 == 0; + } + }) + .single(6); + + SingleObserver<Integer> observer = TestHelper.mockSingleObserver(); + single.subscribe(observer); + + InOrder inOrder = inOrder(observer); + inOrder.verify(observer, times(1)).onError( + isA(IllegalArgumentException.class)); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void singleOrDefaultWithPredicateAndEmpty() { + Single<Integer> single = Flowable.just(1) + .filter(new Predicate<Integer>() { + @Override + public boolean test(Integer t1) { + return t1 % 2 == 0; + } + }) + .single(2); + + SingleObserver<Integer> observer = TestHelper.mockSingleObserver(); + single.subscribe(observer); + + InOrder inOrder = inOrder(observer); + inOrder.verify(observer, times(1)).onSuccess(2); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void issue1527() throws InterruptedException { + //https://github.com/ReactiveX/RxJava/pull/1527 + Flowable<Integer> source = Flowable.just(1, 2, 3, 4, 5, 6); + Maybe<Integer> reduced = source.reduce(new BiFunction<Integer, Integer, Integer>() { + @Override + public Integer apply(Integer i1, Integer i2) { + return i1 + i2; + } + }); + + Integer r = reduced.blockingGet(); + assertEquals(21, r.intValue()); + } + + @Test + public void singleOrErrorNoElement() { + Flowable.empty() + .singleOrError() + .test() + .assertNoValues() + .assertError(NoSuchElementException.class); + } + + @Test + public void singleOrErrorOneElement() { + Flowable.just(1) + .singleOrError() + .test() + .assertNoErrors() + .assertValue(1); + } + + @Test + public void singleOrErrorMultipleElements() { + Flowable.just(1, 2, 3) + .singleOrError() + .test() + .assertNoValues() + .assertError(IllegalArgumentException.class); + } + + @Test + public void singleOrErrorError() { + Flowable.error(new RuntimeException("error")) + .singleOrError() + .to(TestHelper.testConsumer()) + .assertNoValues() + .assertErrorMessage("error") + .assertError(RuntimeException.class); + } + + @Test + public void issue1527Flowable() throws InterruptedException { + //https://github.com/ReactiveX/RxJava/pull/1527 + Flowable<Integer> source = Flowable.just(1, 2, 3, 4, 5, 6); + Flowable<Integer> reduced = source.reduce(new BiFunction<Integer, Integer, Integer>() { + @Override + public Integer apply(Integer i1, Integer i2) { + return i1 + i2; + } + }).toFlowable(); + + Integer r = reduced.blockingFirst(); + assertEquals(21, r.intValue()); + } + + @Test + public void singleElementOperatorDoNotSwallowExceptionWhenDone() { + final Throwable exception = new RuntimeException("some error"); + final AtomicReference<Throwable> error = new AtomicReference<>(); + + try { + RxJavaPlugins.setErrorHandler(new Consumer<Throwable>() { + @Override public void accept(final Throwable throwable) throws Exception { + error.set(throwable); + } + }); + + Flowable.unsafeCreate(new Publisher<Integer>() { + @Override public void subscribe(final Subscriber<? super Integer> subscriber) { + subscriber.onComplete(); + subscriber.onError(exception); + } + }).singleElement().test().assertComplete(); + + assertSame(exception, error.get().getCause()); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void badSource() { + TestHelper.checkBadSourceFlowable(new Function<Flowable<Object>, Object>() { + @Override + public Object apply(Flowable<Object> f) throws Exception { + return f.singleOrError(); + } + }, false, 1, 1, 1); + + TestHelper.checkBadSourceFlowable(new Function<Flowable<Object>, Object>() { + @Override + public Object apply(Flowable<Object> f) throws Exception { + return f.singleElement(); + } + }, false, 1, 1, 1); + + TestHelper.checkBadSourceFlowable(new Function<Flowable<Object>, Object>() { + @Override + public Object apply(Flowable<Object> f) throws Exception { + return f.singleOrError().toFlowable(); + } + }, false, 1, 1, 1); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeFlowableToSingle(new Function<Flowable<Object>, SingleSource<Object>>() { + @Override + public SingleSource<Object> apply(Flowable<Object> f) throws Exception { + return f.singleOrError(); + } + }); + + TestHelper.checkDoubleOnSubscribeFlowable(new Function<Flowable<Object>, Flowable<Object>>() { + @Override + public Flowable<Object> apply(Flowable<Object> f) throws Exception { + return f.singleOrError().toFlowable(); + } + }); + + TestHelper.checkDoubleOnSubscribeFlowableToMaybe(new Function<Flowable<Object>, MaybeSource<Object>>() { + @Override + public MaybeSource<Object> apply(Flowable<Object> f) throws Exception { + return f.singleElement(); + } + }); + + TestHelper.checkDoubleOnSubscribeFlowable(new Function<Flowable<Object>, Flowable<Object>>() { + @Override + public Flowable<Object> apply(Flowable<Object> f) throws Exception { + return f.singleElement().toFlowable(); + } + }); + } + + @Test + public void cancelAsFlowable() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + + TestSubscriber<Integer> ts = pp.singleOrError().toFlowable().test(); + + assertTrue(pp.hasSubscribers()); + + ts.assertEmpty(); + + ts.cancel(); + + assertFalse(pp.hasSubscribers()); + } + + @Test + public void singleOrError() { + Flowable.empty() + .singleOrError() + .toFlowable() + .test() + .assertFailure(NoSuchElementException.class); + } + + @Test + public void dispose() { + TestHelper.checkDisposed(PublishProcessor.create().single(1)); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableSkipLastTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableSkipLastTest.java new file mode 100644 index 0000000000..69eac72c54 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableSkipLastTest.java @@ -0,0 +1,129 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import java.util.Arrays; +import java.util.concurrent.TimeUnit; + +import org.junit.Test; +import org.mockito.InOrder; +import org.reactivestreams.Subscriber; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.schedulers.Schedulers; +import io.reactivex.rxjava3.subscribers.TestSubscriber; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class FlowableSkipLastTest extends RxJavaTest { + + @Test + public void skipLastEmpty() { + Flowable<String> flowable = Flowable.<String> empty().skipLast(2); + + Subscriber<String> subscriber = TestHelper.mockSubscriber(); + flowable.subscribe(subscriber); + + verify(subscriber, never()).onNext(any(String.class)); + verify(subscriber, never()).onError(any(Throwable.class)); + verify(subscriber, times(1)).onComplete(); + } + + @Test + public void skipLast1() { + Flowable<String> flowable = Flowable.fromIterable(Arrays.asList("one", "two", "three")).skipLast(2); + + Subscriber<String> subscriber = TestHelper.mockSubscriber(); + InOrder inOrder = inOrder(subscriber); + flowable.subscribe(subscriber); + + inOrder.verify(subscriber, never()).onNext("two"); + inOrder.verify(subscriber, never()).onNext("three"); + verify(subscriber, times(1)).onNext("one"); + verify(subscriber, never()).onError(any(Throwable.class)); + verify(subscriber, times(1)).onComplete(); + } + + @Test + public void skipLast2() { + Flowable<String> flowable = Flowable.fromIterable(Arrays.asList("one", "two")).skipLast(2); + + Subscriber<String> subscriber = TestHelper.mockSubscriber(); + flowable.subscribe(subscriber); + + verify(subscriber, never()).onNext(any(String.class)); + verify(subscriber, never()).onError(any(Throwable.class)); + verify(subscriber, times(1)).onComplete(); + } + + @Test + public void skipLastWithZeroCount() { + Flowable<String> w = Flowable.just("one", "two"); + Flowable<String> flowable = w.skipLast(0); + + Subscriber<String> subscriber = TestHelper.mockSubscriber(); + flowable.subscribe(subscriber); + + verify(subscriber, times(1)).onNext("one"); + verify(subscriber, times(1)).onNext("two"); + verify(subscriber, never()).onError(any(Throwable.class)); + verify(subscriber, times(1)).onComplete(); + } + + @Test + public void skipLastWithBackpressure() { + Flowable<Integer> f = Flowable.range(0, Flowable.bufferSize() * 2).skipLast(Flowable.bufferSize() + 10); + TestSubscriber<Integer> ts = new TestSubscriber<>(); + f.observeOn(Schedulers.computation()).subscribe(ts); + ts.awaitDone(5, TimeUnit.SECONDS); + ts.assertNoErrors(); + assertEquals((Flowable.bufferSize()) - 10, ts.values().size()); + + } + + @Test(expected = IllegalArgumentException.class) + public void skipLastWithNegativeCount() { + Flowable.just("one").skipLast(-1); + } + + @Test + public void dispose() { + TestHelper.checkDisposed(Flowable.just(1).skipLast(1)); + } + + @Test + public void error() { + Flowable.error(new TestException()) + .skipLast(1) + .test() + .assertFailure(TestException.class); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeFlowable(new Function<Flowable<Object>, Flowable<Object>>() { + @Override + public Flowable<Object> apply(Flowable<Object> f) + throws Exception { + return f.skipLast(1); + } + }); + } + +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableSkipLastTimedTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableSkipLastTimedTest.java new file mode 100644 index 0000000000..f2f1ff2598 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableSkipLastTimedTest.java @@ -0,0 +1,276 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import java.util.concurrent.TimeUnit; + +import org.junit.Test; +import org.mockito.InOrder; +import org.reactivestreams.Subscriber; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.processors.PublishProcessor; +import io.reactivex.rxjava3.schedulers.*; +import io.reactivex.rxjava3.subscribers.TestSubscriber; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class FlowableSkipLastTimedTest extends RxJavaTest { + + @Test + public void skipLastTimed() { + TestScheduler scheduler = new TestScheduler(); + + PublishProcessor<Integer> source = PublishProcessor.create(); + + // FIXME the timeunit now matters due to rounding + Flowable<Integer> result = source.skipLast(1000, TimeUnit.MILLISECONDS, scheduler); + + Subscriber<Object> subscriber = TestHelper.mockSubscriber(); + + result.subscribe(subscriber); + + source.onNext(1); + source.onNext(2); + source.onNext(3); + + scheduler.advanceTimeBy(500, TimeUnit.MILLISECONDS); + + source.onNext(4); + source.onNext(5); + source.onNext(6); + + scheduler.advanceTimeBy(950, TimeUnit.MILLISECONDS); + source.onComplete(); + + InOrder inOrder = inOrder(subscriber); + inOrder.verify(subscriber).onNext(1); + inOrder.verify(subscriber).onNext(2); + inOrder.verify(subscriber).onNext(3); + inOrder.verify(subscriber, never()).onNext(4); + inOrder.verify(subscriber, never()).onNext(5); + inOrder.verify(subscriber, never()).onNext(6); + inOrder.verify(subscriber).onComplete(); + inOrder.verifyNoMoreInteractions(); + + verify(subscriber, never()).onError(any(Throwable.class)); + } + + @Test + public void skipLastTimedErrorBeforeTime() { + TestScheduler scheduler = new TestScheduler(); + + PublishProcessor<Integer> source = PublishProcessor.create(); + + Flowable<Integer> result = source.skipLast(1, TimeUnit.SECONDS, scheduler); + + Subscriber<Object> subscriber = TestHelper.mockSubscriber(); + + result.subscribe(subscriber); + + source.onNext(1); + source.onNext(2); + source.onNext(3); + source.onError(new TestException()); + + scheduler.advanceTimeBy(1050, TimeUnit.MILLISECONDS); + + verify(subscriber).onError(any(TestException.class)); + + verify(subscriber, never()).onComplete(); + verify(subscriber, never()).onNext(any()); + } + + @Test + public void skipLastTimedCompleteBeforeTime() { + TestScheduler scheduler = new TestScheduler(); + + PublishProcessor<Integer> source = PublishProcessor.create(); + + Flowable<Integer> result = source.skipLast(1, TimeUnit.SECONDS, scheduler); + + Subscriber<Object> subscriber = TestHelper.mockSubscriber(); + + result.subscribe(subscriber); + + source.onNext(1); + source.onNext(2); + source.onNext(3); + + scheduler.advanceTimeBy(500, TimeUnit.MILLISECONDS); + + source.onComplete(); + + InOrder inOrder = inOrder(subscriber); + inOrder.verify(subscriber).onComplete(); + inOrder.verifyNoMoreInteractions(); + + verify(subscriber, never()).onNext(any()); + verify(subscriber, never()).onError(any(Throwable.class)); + } + + @Test + public void skipLastTimedWhenAllElementsAreValid() { + TestScheduler scheduler = new TestScheduler(); + + PublishProcessor<Integer> source = PublishProcessor.create(); + + Flowable<Integer> result = source.skipLast(1, TimeUnit.MILLISECONDS, scheduler); + + Subscriber<Object> subscriber = TestHelper.mockSubscriber(); + + result.subscribe(subscriber); + + source.onNext(1); + source.onNext(2); + source.onNext(3); + + scheduler.advanceTimeBy(500, TimeUnit.MILLISECONDS); + + source.onComplete(); + + InOrder inOrder = inOrder(subscriber); + inOrder.verify(subscriber).onNext(1); + inOrder.verify(subscriber).onNext(2); + inOrder.verify(subscriber).onNext(3); + inOrder.verify(subscriber).onComplete(); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void skipLastTimedDefaultScheduler() { + Flowable.just(1).concatWith(Flowable.just(2).delay(500, TimeUnit.MILLISECONDS)) + .skipLast(300, TimeUnit.MILLISECONDS) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(1); + } + + @Test + public void skipLastTimedDefaultSchedulerDelayError() { + Flowable.just(1).concatWith(Flowable.just(2).delay(500, TimeUnit.MILLISECONDS)) + .skipLast(300, TimeUnit.MILLISECONDS, true) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(1); + } + + @Test + public void skipLastTimedCustomSchedulerDelayError() { + Flowable.just(1).concatWith(Flowable.just(2).delay(500, TimeUnit.MILLISECONDS)) + .skipLast(300, TimeUnit.MILLISECONDS, Schedulers.io(), true) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(1); + } + + @Test + public void dispose() { + TestHelper.checkDisposed(PublishProcessor.create().skipLast(1, TimeUnit.DAYS)); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeFlowable(new Function<Flowable<Object>, Flowable<Object>>() { + @Override + public Flowable<Object> apply(Flowable<Object> f) throws Exception { + return f.skipLast(1, TimeUnit.DAYS); + } + }); + } + + @Test + public void onNextDisposeRace() { + TestScheduler scheduler = new TestScheduler(); + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final PublishProcessor<Integer> pp = PublishProcessor.create(); + + final TestSubscriber<Integer> ts = pp.skipLast(1, TimeUnit.DAYS, scheduler).test(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + pp.onComplete(); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + ts.cancel(); + } + }; + + TestHelper.race(r1, r2); + } + } + + @Test + public void errorDelayed() { + Flowable.error(new TestException()) + .skipLast(1, TimeUnit.DAYS, new TestScheduler(), true) + .test() + .assertFailure(TestException.class); + } + + @Test + public void take() { + Flowable.just(1) + .skipLast(0, TimeUnit.SECONDS) + .take(1) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(1); + } + + @Test + public void observeOn() { + Flowable.range(1, 1000) + .skipLast(0, TimeUnit.SECONDS) + .observeOn(Schedulers.single(), false, 16) + .to(TestHelper.<Integer>testConsumer()) + .awaitDone(5, TimeUnit.SECONDS) + .assertSubscribed() + .assertValueCount(1000) + .assertComplete() + .assertNoErrors(); + } + + @Test + public void badRequest() { + TestHelper.assertBadRequestReported(Flowable.never().skipLast(1, TimeUnit.MINUTES)); + } + + @Test + public void delayErrorMoreWork() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + + TestSubscriber<Integer> ts = pp.skipLast(0, TimeUnit.MILLISECONDS, true) + .doOnNext(v -> { + if (v == 1) { + pp.onNext(1); + pp.onComplete(); + } + }) + .test(); + + pp.onNext(1); + + ts.assertComplete(); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableSkipTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableSkipTest.java new file mode 100644 index 0000000000..eac3c94df8 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableSkipTest.java @@ -0,0 +1,189 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import java.util.Arrays; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; + +import org.junit.Test; +import org.reactivestreams.Subscriber; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.subscribers.TestSubscriber; +import io.reactivex.rxjava3.testsupport.*; + +public class FlowableSkipTest extends RxJavaTest { + + @Test(expected = IllegalArgumentException.class) + public void skipNegativeElements() { + + Flowable<String> skip = Flowable.just("one", "two", "three").skip(-99); + + Subscriber<String> subscriber = TestHelper.mockSubscriber(); + skip.subscribe(subscriber); + verify(subscriber, times(1)).onNext("one"); + verify(subscriber, times(1)).onNext("two"); + verify(subscriber, times(1)).onNext("three"); + verify(subscriber, never()).onError(any(Throwable.class)); + verify(subscriber, times(1)).onComplete(); + } + + @Test + public void skipZeroElements() { + + Flowable<String> skip = Flowable.just("one", "two", "three").skip(0); + + Subscriber<String> subscriber = TestHelper.mockSubscriber(); + skip.subscribe(subscriber); + verify(subscriber, times(1)).onNext("one"); + verify(subscriber, times(1)).onNext("two"); + verify(subscriber, times(1)).onNext("three"); + verify(subscriber, never()).onError(any(Throwable.class)); + verify(subscriber, times(1)).onComplete(); + } + + @Test + public void skipOneElement() { + + Flowable<String> skip = Flowable.just("one", "two", "three").skip(1); + + Subscriber<String> subscriber = TestHelper.mockSubscriber(); + skip.subscribe(subscriber); + verify(subscriber, never()).onNext("one"); + verify(subscriber, times(1)).onNext("two"); + verify(subscriber, times(1)).onNext("three"); + verify(subscriber, never()).onError(any(Throwable.class)); + verify(subscriber, times(1)).onComplete(); + } + + @Test + public void skipTwoElements() { + + Flowable<String> skip = Flowable.just("one", "two", "three").skip(2); + + Subscriber<String> subscriber = TestHelper.mockSubscriber(); + skip.subscribe(subscriber); + verify(subscriber, never()).onNext("one"); + verify(subscriber, never()).onNext("two"); + verify(subscriber, times(1)).onNext("three"); + verify(subscriber, never()).onError(any(Throwable.class)); + verify(subscriber, times(1)).onComplete(); + } + + @Test + public void skipEmptyStream() { + + Flowable<String> w = Flowable.empty(); + Flowable<String> skip = w.skip(1); + + Subscriber<String> subscriber = TestHelper.mockSubscriber(); + skip.subscribe(subscriber); + verify(subscriber, never()).onNext(any(String.class)); + verify(subscriber, never()).onError(any(Throwable.class)); + verify(subscriber, times(1)).onComplete(); + } + + @Test + public void skipMultipleObservers() { + + Flowable<String> skip = Flowable.just("one", "two", "three") + .skip(2); + + Subscriber<String> subscriber1 = TestHelper.mockSubscriber(); + skip.subscribe(subscriber1); + + Subscriber<String> subscriber2 = TestHelper.mockSubscriber(); + skip.subscribe(subscriber2); + + verify(subscriber1, times(1)).onNext(any(String.class)); + verify(subscriber1, never()).onError(any(Throwable.class)); + verify(subscriber1, times(1)).onComplete(); + + verify(subscriber2, times(1)).onNext(any(String.class)); + verify(subscriber2, never()).onError(any(Throwable.class)); + verify(subscriber2, times(1)).onComplete(); + } + + @Test + public void skipError() { + + Exception e = new Exception(); + + Flowable<String> ok = Flowable.just("one"); + Flowable<String> error = Flowable.error(e); + + Flowable<String> skip = Flowable.concat(ok, error).skip(100); + + Subscriber<String> subscriber = TestHelper.mockSubscriber(); + skip.subscribe(subscriber); + + verify(subscriber, never()).onNext(any(String.class)); + verify(subscriber, times(1)).onError(e); + verify(subscriber, never()).onComplete(); + + } + + @Test + public void backpressureMultipleSmallAsyncRequests() throws InterruptedException { + final AtomicLong requests = new AtomicLong(0); + TestSubscriber<Long> ts = new TestSubscriber<>(0L); + Flowable.interval(100, TimeUnit.MILLISECONDS) + .doOnRequest(new LongConsumer() { + @Override + public void accept(long n) { + requests.addAndGet(n); + } + }).skip(4).subscribe(ts); + Thread.sleep(100); + ts.request(1); + ts.request(1); + Thread.sleep(100); + ts.cancel(); + ts.assertNoErrors(); + assertEquals(6, requests.get()); + } + + @Test + public void requestOverflowDoesNotOccur() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(Long.MAX_VALUE - 1); + Flowable.range(1, 10).skip(5).subscribe(ts); + ts.assertTerminated(); + ts.assertComplete(); + ts.assertNoErrors(); + assertEquals(Arrays.asList(6, 7, 8, 9, 10), ts.values()); + } + + @Test + public void dispose() { + TestHelper.checkDisposed(Flowable.just(1).skip(2)); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeFlowable(new Function<Flowable<Object>, Flowable<Object>>() { + @Override + public Flowable<Object> apply(Flowable<Object> f) + throws Exception { + return f.skip(1); + } + }); + } + +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableSkipTimedTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableSkipTimedTest.java new file mode 100644 index 0000000000..82012097e7 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableSkipTimedTest.java @@ -0,0 +1,169 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import java.util.concurrent.TimeUnit; + +import org.junit.Test; +import org.mockito.InOrder; +import org.reactivestreams.Subscriber; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.processors.PublishProcessor; +import io.reactivex.rxjava3.schedulers.TestScheduler; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class FlowableSkipTimedTest extends RxJavaTest { + + @Test + public void skipTimed() { + TestScheduler scheduler = new TestScheduler(); + + PublishProcessor<Integer> source = PublishProcessor.create(); + + Flowable<Integer> result = source.skip(1, TimeUnit.SECONDS, scheduler); + + Subscriber<Object> subscriber = TestHelper.mockSubscriber(); + + result.subscribe(subscriber); + + source.onNext(1); + source.onNext(2); + source.onNext(3); + + scheduler.advanceTimeBy(1, TimeUnit.SECONDS); + + source.onNext(4); + source.onNext(5); + source.onNext(6); + + source.onComplete(); + + InOrder inOrder = inOrder(subscriber); + + inOrder.verify(subscriber, never()).onNext(1); + inOrder.verify(subscriber, never()).onNext(2); + inOrder.verify(subscriber, never()).onNext(3); + inOrder.verify(subscriber).onNext(4); + inOrder.verify(subscriber).onNext(5); + inOrder.verify(subscriber).onNext(6); + inOrder.verify(subscriber).onComplete(); + inOrder.verifyNoMoreInteractions(); + verify(subscriber, never()).onError(any(Throwable.class)); + } + + @Test + public void skipTimedFinishBeforeTime() { + TestScheduler scheduler = new TestScheduler(); + + PublishProcessor<Integer> source = PublishProcessor.create(); + + Flowable<Integer> result = source.skip(1, TimeUnit.SECONDS, scheduler); + + Subscriber<Object> subscriber = TestHelper.mockSubscriber(); + + result.subscribe(subscriber); + + source.onNext(1); + source.onNext(2); + source.onNext(3); + source.onComplete(); + + scheduler.advanceTimeBy(1, TimeUnit.SECONDS); + + InOrder inOrder = inOrder(subscriber); + + inOrder.verify(subscriber).onComplete(); + inOrder.verifyNoMoreInteractions(); + verify(subscriber, never()).onNext(any()); + verify(subscriber, never()).onError(any(Throwable.class)); + } + + @Test + public void skipTimedErrorBeforeTime() { + TestScheduler scheduler = new TestScheduler(); + + PublishProcessor<Integer> source = PublishProcessor.create(); + + Flowable<Integer> result = source.skip(1, TimeUnit.SECONDS, scheduler); + + Subscriber<Object> subscriber = TestHelper.mockSubscriber(); + + result.subscribe(subscriber); + + source.onNext(1); + source.onNext(2); + source.onNext(3); + source.onError(new TestException()); + + scheduler.advanceTimeBy(1, TimeUnit.SECONDS); + + InOrder inOrder = inOrder(subscriber); + + inOrder.verify(subscriber).onError(any(TestException.class)); + inOrder.verifyNoMoreInteractions(); + verify(subscriber, never()).onNext(any()); + verify(subscriber, never()).onComplete(); + } + + @Test + public void skipTimedErrorAfterTime() { + TestScheduler scheduler = new TestScheduler(); + + PublishProcessor<Integer> source = PublishProcessor.create(); + + Flowable<Integer> result = source.skip(1, TimeUnit.SECONDS, scheduler); + + Subscriber<Object> subscriber = TestHelper.mockSubscriber(); + + result.subscribe(subscriber); + + source.onNext(1); + source.onNext(2); + source.onNext(3); + + scheduler.advanceTimeBy(1, TimeUnit.SECONDS); + + source.onNext(4); + source.onNext(5); + source.onNext(6); + + source.onError(new TestException()); + + InOrder inOrder = inOrder(subscriber); + + inOrder.verify(subscriber, never()).onNext(1); + inOrder.verify(subscriber, never()).onNext(2); + inOrder.verify(subscriber, never()).onNext(3); + inOrder.verify(subscriber).onNext(4); + inOrder.verify(subscriber).onNext(5); + inOrder.verify(subscriber).onNext(6); + inOrder.verify(subscriber).onError(any(TestException.class)); + inOrder.verifyNoMoreInteractions(); + verify(subscriber, never()).onComplete(); + + } + + @Test + public void skipTimedDefaultScheduler() { + Flowable.just(1).skip(1, TimeUnit.MINUTES) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableSkipUntilTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableSkipUntilTest.java new file mode 100644 index 0000000000..d5bbdf969a --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableSkipUntilTest.java @@ -0,0 +1,180 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import org.junit.*; +import org.reactivestreams.Subscriber; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.processors.PublishProcessor; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class FlowableSkipUntilTest extends RxJavaTest { + Subscriber<Object> subscriber; + + @Before + public void before() { + subscriber = TestHelper.mockSubscriber(); + } + + @Test + public void normal1() { + PublishProcessor<Integer> source = PublishProcessor.create(); + PublishProcessor<Integer> other = PublishProcessor.create(); + + Flowable<Integer> m = source.skipUntil(other); + m.subscribe(subscriber); + + source.onNext(0); + source.onNext(1); + + other.onNext(100); + + source.onNext(2); + source.onNext(3); + source.onNext(4); + source.onComplete(); + + verify(subscriber, never()).onError(any(Throwable.class)); + verify(subscriber, times(1)).onNext(2); + verify(subscriber, times(1)).onNext(3); + verify(subscriber, times(1)).onNext(4); + verify(subscriber, times(1)).onComplete(); + } + + @Test + public void otherNeverFires() { + PublishProcessor<Integer> source = PublishProcessor.create(); + + Flowable<Integer> m = source.skipUntil(Flowable.never()); + + m.subscribe(subscriber); + + source.onNext(0); + source.onNext(1); + source.onNext(2); + source.onNext(3); + source.onNext(4); + source.onComplete(); + + verify(subscriber, never()).onError(any(Throwable.class)); + verify(subscriber, never()).onNext(any()); + verify(subscriber, times(1)).onComplete(); + } + + @Test + public void otherEmpty() { + PublishProcessor<Integer> source = PublishProcessor.create(); + + Flowable<Integer> m = source.skipUntil(Flowable.empty()); + + m.subscribe(subscriber); + + verify(subscriber, never()).onError(any(Throwable.class)); + verify(subscriber, never()).onNext(any()); + verify(subscriber, never()).onComplete(); + } + + @Test + public void otherFiresAndCompletes() { + PublishProcessor<Integer> source = PublishProcessor.create(); + PublishProcessor<Integer> other = PublishProcessor.create(); + + Flowable<Integer> m = source.skipUntil(other); + m.subscribe(subscriber); + + source.onNext(0); + source.onNext(1); + + other.onNext(100); + other.onComplete(); + + source.onNext(2); + source.onNext(3); + source.onNext(4); + source.onComplete(); + + verify(subscriber, never()).onError(any(Throwable.class)); + verify(subscriber, times(1)).onNext(2); + verify(subscriber, times(1)).onNext(3); + verify(subscriber, times(1)).onNext(4); + verify(subscriber, times(1)).onComplete(); + } + + @Test + public void sourceThrows() { + PublishProcessor<Integer> source = PublishProcessor.create(); + PublishProcessor<Integer> other = PublishProcessor.create(); + + Flowable<Integer> m = source.skipUntil(other); + m.subscribe(subscriber); + + source.onNext(0); + source.onNext(1); + + other.onNext(100); + other.onComplete(); + + source.onNext(2); + source.onError(new RuntimeException("Forced failure")); + + verify(subscriber, times(1)).onNext(2); + verify(subscriber, times(1)).onError(any(Throwable.class)); + verify(subscriber, never()).onComplete(); + } + + @Test + public void otherThrowsImmediately() { + PublishProcessor<Integer> source = PublishProcessor.create(); + PublishProcessor<Integer> other = PublishProcessor.create(); + + Flowable<Integer> m = source.skipUntil(other); + m.subscribe(subscriber); + + source.onNext(0); + source.onNext(1); + + other.onError(new RuntimeException("Forced failure")); + + verify(subscriber, never()).onNext(any()); + verify(subscriber, times(1)).onError(any(Throwable.class)); + verify(subscriber, never()).onComplete(); + } + + @Test + public void dispose() { + TestHelper.checkDisposed(PublishProcessor.create().skipUntil(PublishProcessor.create())); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeFlowable(new Function<Flowable<Object>, Flowable<Object>>() { + @Override + public Flowable<Object> apply(Flowable<Object> f) throws Exception { + return f.skipUntil(Flowable.never()); + } + }); + + TestHelper.checkDoubleOnSubscribeFlowable(new Function<Flowable<Object>, Flowable<Object>>() { + @Override + public Flowable<Object> apply(Flowable<Object> f) throws Exception { + return Flowable.never().skipUntil(f); + } + }); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableSkipWhileTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableSkipWhileTest.java new file mode 100644 index 0000000000..3fd42350d2 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableSkipWhileTest.java @@ -0,0 +1,160 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +import org.junit.Test; +import org.mockito.InOrder; +import org.reactivestreams.Subscriber; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.processors.PublishProcessor; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class FlowableSkipWhileTest extends RxJavaTest { + + Subscriber<Integer> w = TestHelper.mockSubscriber(); + + private static final Predicate<Integer> LESS_THAN_FIVE = new Predicate<Integer>() { + @Override + public boolean test(Integer v) { + if (v == 42) { + throw new RuntimeException("that's not the answer to everything!"); + } + return v < 5; + } + }; + + private static final Predicate<Integer> INDEX_LESS_THAN_THREE = new Predicate<Integer>() { + int index; + @Override + public boolean test(Integer value) { + return index++ < 3; + } + }; + + @Test + public void skipWithIndex() { + Flowable<Integer> src = Flowable.just(1, 2, 3, 4, 5); + src.skipWhile(INDEX_LESS_THAN_THREE).subscribe(w); + + InOrder inOrder = inOrder(w); + inOrder.verify(w, times(1)).onNext(4); + inOrder.verify(w, times(1)).onNext(5); + inOrder.verify(w, times(1)).onComplete(); + inOrder.verify(w, never()).onError(any(Throwable.class)); + } + + @Test + public void skipEmpty() { + Flowable<Integer> src = Flowable.empty(); + src.skipWhile(LESS_THAN_FIVE).subscribe(w); + verify(w, never()).onNext(anyInt()); + verify(w, never()).onError(any(Throwable.class)); + verify(w, times(1)).onComplete(); + } + + @Test + public void skipEverything() { + Flowable<Integer> src = Flowable.just(1, 2, 3, 4, 3, 2, 1); + src.skipWhile(LESS_THAN_FIVE).subscribe(w); + verify(w, never()).onNext(anyInt()); + verify(w, never()).onError(any(Throwable.class)); + verify(w, times(1)).onComplete(); + } + + @Test + public void skipNothing() { + Flowable<Integer> src = Flowable.just(5, 3, 1); + src.skipWhile(LESS_THAN_FIVE).subscribe(w); + + InOrder inOrder = inOrder(w); + inOrder.verify(w, times(1)).onNext(5); + inOrder.verify(w, times(1)).onNext(3); + inOrder.verify(w, times(1)).onNext(1); + inOrder.verify(w, times(1)).onComplete(); + inOrder.verify(w, never()).onError(any(Throwable.class)); + } + + @Test + public void skipSome() { + Flowable<Integer> src = Flowable.just(1, 2, 3, 4, 5, 3, 1, 5); + src.skipWhile(LESS_THAN_FIVE).subscribe(w); + + InOrder inOrder = inOrder(w); + inOrder.verify(w, times(1)).onNext(5); + inOrder.verify(w, times(1)).onNext(3); + inOrder.verify(w, times(1)).onNext(1); + inOrder.verify(w, times(1)).onNext(5); + inOrder.verify(w, times(1)).onComplete(); + inOrder.verify(w, never()).onError(any(Throwable.class)); + } + + @Test + public void skipError() { + Flowable<Integer> src = Flowable.just(1, 2, 42, 5, 3, 1); + src.skipWhile(LESS_THAN_FIVE).subscribe(w); + + InOrder inOrder = inOrder(w); + inOrder.verify(w, never()).onNext(anyInt()); + inOrder.verify(w, never()).onComplete(); + inOrder.verify(w, times(1)).onError(any(RuntimeException.class)); + } + + @Test + public void skipManySubscribers() { + Flowable<Integer> src = Flowable.range(1, 10).skipWhile(LESS_THAN_FIVE); + int n = 5; + for (int i = 0; i < n; i++) { + Subscriber<Object> subscriber = TestHelper.mockSubscriber(); + InOrder inOrder = inOrder(subscriber); + + src.subscribe(subscriber); + + for (int j = 5; j < 10; j++) { + inOrder.verify(subscriber).onNext(j); + } + inOrder.verify(subscriber).onComplete(); + verify(subscriber, never()).onError(any(Throwable.class)); + } + } + + @Test + public void dispose() { + TestHelper.checkDisposed(PublishProcessor.create().skipWhile(Functions.alwaysFalse())); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeFlowable(new Function<Flowable<Object>, Flowable<Object>>() { + @Override + public Flowable<Object> apply(Flowable<Object> f) throws Exception { + return f.skipWhile(Functions.alwaysFalse()); + } + }); + } + + @Test + public void error() { + Flowable.error(new TestException()) + .skipWhile(Functions.alwaysFalse()) + .test() + .assertFailure(TestException.class); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableStartWithTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableStartWithTest.java new file mode 100644 index 0000000000..6aa442c00b --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableStartWithTest.java @@ -0,0 +1,153 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import static org.mockito.Mockito.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; + +public class FlowableStartWithTest { + + @Test + public void justCompletableComplete() { + Flowable.just(1).startWith(Completable.complete()) + .test() + .assertResult(1); + } + + @Test + public void emptyCompletableComplete() { + Flowable.empty().startWith(Completable.complete()) + .test() + .assertResult(); + } + + @Test + public void runCompletableError() { + Runnable run = mock(Runnable.class); + + Flowable.fromRunnable(run).startWith(Completable.error(new TestException())) + .test() + .assertFailure(TestException.class); + + verify(run, never()).run(); + } + + @Test + public void justSingleJust() { + Flowable.just(1).startWith(Single.just(2)) + .test() + .assertResult(2, 1); + } + + @Test + public void emptySingleJust() { + Runnable run = mock(Runnable.class); + + Flowable.fromRunnable(run) + .startWith(Single.just(2)) + .test() + .assertResult(2); + + verify(run).run(); + } + + @Test + public void runSingleError() { + Runnable run = mock(Runnable.class); + + Flowable.fromRunnable(run).startWith(Single.error(new TestException())) + .test() + .assertFailure(TestException.class); + + verify(run, never()).run(); + } + + @Test + public void justMaybeJust() { + Flowable.just(1).startWith(Maybe.just(2)) + .test() + .assertResult(2, 1); + } + + @Test + public void emptyMaybeJust() { + Runnable run = mock(Runnable.class); + + Flowable.fromRunnable(run) + .startWith(Maybe.just(2)) + .test() + .assertResult(2); + + verify(run).run(); + } + + @Test + public void runMaybeError() { + Runnable run = mock(Runnable.class); + + Flowable.fromRunnable(run).startWith(Maybe.error(new TestException())) + .test() + .assertFailure(TestException.class); + + verify(run, never()).run(); + } + + @Test + public void justFlowableJust() { + Flowable.just(1).startWith(Flowable.just(2, 3, 4, 5)) + .test() + .assertResult(2, 3, 4, 5, 1); + } + + @Test + public void emptyFlowableJust() { + Runnable run = mock(Runnable.class); + + Flowable.fromRunnable(run) + .startWith(Flowable.just(2, 3, 4, 5)) + .test() + .assertResult(2, 3, 4, 5); + + verify(run).run(); + } + + @Test + public void emptyFlowableEmpty() { + Runnable run = mock(Runnable.class); + Runnable run2 = mock(Runnable.class); + + Flowable.fromRunnable(run) + .startWith(Flowable.fromRunnable(run2)) + .test() + .assertResult(); + + verify(run).run(); + verify(run2).run(); + } + + @Test + public void runFlowableError() { + Runnable run = mock(Runnable.class); + + Flowable.fromRunnable(run).startWith(Flowable.error(new TestException())) + .test() + .assertFailure(TestException.class); + + verify(run, never()).run(); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableSubscribeOnTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableSubscribeOnTest.java new file mode 100644 index 0000000000..01ee614fd5 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableSubscribeOnTest.java @@ -0,0 +1,415 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import static org.junit.Assert.*; + +import java.util.concurrent.*; +import java.util.concurrent.atomic.*; + +import org.junit.Test; +import org.reactivestreams.*; + +import io.reactivex.rxjava3.annotations.NonNull; +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.core.Scheduler.Worker; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.internal.operators.flowable.FlowableSubscribeOn.SubscribeOnSubscriber; +import io.reactivex.rxjava3.internal.schedulers.ImmediateThinScheduler; +import io.reactivex.rxjava3.internal.subscriptions.BooleanSubscription; +import io.reactivex.rxjava3.schedulers.*; +import io.reactivex.rxjava3.subscribers.*; +import io.reactivex.rxjava3.testsupport.*; + +public class FlowableSubscribeOnTest extends RxJavaTest { + + @Test + public void issue813() throws InterruptedException { + // https://github.com/ReactiveX/RxJava/issues/813 + final CountDownLatch scheduled = new CountDownLatch(1); + final CountDownLatch latch = new CountDownLatch(1); + final CountDownLatch doneLatch = new CountDownLatch(1); + + TestSubscriber<Integer> ts = new TestSubscriber<>(); + + Flowable + .unsafeCreate(new Publisher<Integer>() { + @Override + public void subscribe( + final Subscriber<? super Integer> subscriber) { + subscriber.onSubscribe(new BooleanSubscription()); + scheduled.countDown(); + try { + try { + latch.await(); + } catch (InterruptedException e) { + // this means we were unsubscribed (Scheduler shut down and interrupts) + // ... but we'll pretend we are like many Flowables that ignore interrupts + } + + subscriber.onComplete(); + } catch (Throwable e) { + subscriber.onError(e); + } finally { + doneLatch.countDown(); + } + } + }).subscribeOn(Schedulers.computation()).subscribe(ts); + + // wait for scheduling + scheduled.await(); + // trigger unsubscribe + ts.cancel(); + latch.countDown(); + doneLatch.await(); + ts.assertNoErrors(); + ts.assertComplete(); + } + + @Test + public void onError() { + TestSubscriberEx<String> ts = new TestSubscriberEx<>(); + Flowable.unsafeCreate(new Publisher<String>() { + + @Override + public void subscribe(Subscriber<? super String> s) { + s.onSubscribe(new BooleanSubscription()); + s.onError(new RuntimeException("fail")); + } + + }).subscribeOn(Schedulers.computation()).subscribe(ts); + ts.awaitDone(1000, TimeUnit.MILLISECONDS); + ts.assertTerminated(); + } + + public static class SlowScheduler extends Scheduler { + final Scheduler actual; + final long delay; + final TimeUnit unit; + + public SlowScheduler() { + this(Schedulers.computation(), 2, TimeUnit.SECONDS); + } + + public SlowScheduler(Scheduler actual, long delay, TimeUnit unit) { + this.actual = actual; + this.delay = delay; + this.unit = unit; + } + + @NonNull + @Override + public Worker createWorker() { + return new SlowInner(actual.createWorker()); + } + + private final class SlowInner extends Worker { + + private final Scheduler.Worker actualInner; + + private SlowInner(Worker actual) { + this.actualInner = actual; + } + + @Override + public void dispose() { + actualInner.dispose(); + } + + @Override + public boolean isDisposed() { + return actualInner.isDisposed(); + } + + @NonNull + @Override + public Disposable schedule(@NonNull final Runnable action) { + return actualInner.schedule(action, delay, unit); + } + + @NonNull + @Override + public Disposable schedule(@NonNull final Runnable action, final long delayTime, @NonNull final TimeUnit delayUnit) { + TimeUnit common = delayUnit.compareTo(unit) < 0 ? delayUnit : unit; + long t = common.convert(delayTime, delayUnit) + common.convert(delay, unit); + return actualInner.schedule(action, t, common); + } + + } + + } + + @Test + public void unsubscribeInfiniteStream() throws InterruptedException { + TestSubscriber<Integer> ts = new TestSubscriber<>(); + final AtomicInteger count = new AtomicInteger(); + Flowable.unsafeCreate(new Publisher<Integer>() { + + @Override + public void subscribe(Subscriber<? super Integer> sub) { + BooleanSubscription bs = new BooleanSubscription(); + sub.onSubscribe(bs); + for (int i = 1; !bs.isCancelled(); i++) { + count.incrementAndGet(); + sub.onNext(i); + } + } + + }).subscribeOn(Schedulers.newThread()).take(10).subscribe(ts); + + ts.awaitDone(1000, TimeUnit.MILLISECONDS); + ts.cancel(); + Thread.sleep(200); // give time for the loop to continue + ts.assertValues(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + assertEquals(10, count.get()); + } + + @Test + public void backpressureReschedulesCorrectly() throws InterruptedException { + final CountDownLatch latch = new CountDownLatch(10); + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(new DefaultSubscriber<Integer>() { + + @Override + public void onComplete() { + } + + @Override + public void onError(Throwable e) { + } + + @Override + public void onNext(Integer t) { + latch.countDown(); + } + + }); + ts.request(10); + Flowable.range(1, 10000000).subscribeOn(Schedulers.newThread()).take(20).subscribe(ts); + latch.await(); + Thread t = ts.lastThread(); + System.out.println("First schedule: " + t); + assertTrue(t.getName().startsWith("Rx")); + ts.request(10); + ts.awaitDone(20, TimeUnit.SECONDS); + System.out.println("After reschedule: " + ts.lastThread()); + assertEquals(t, ts.lastThread()); + } + + @Test + public void setProducerSynchronousRequest() { + TestSubscriber<Integer> ts = new TestSubscriber<>(); + Flowable.just(1, 2, 3).lift(new FlowableOperator<Integer, Integer>() { + + @Override + public Subscriber<? super Integer> apply(final Subscriber<? super Integer> child) { + final AtomicLong requested = new AtomicLong(); + child.onSubscribe(new Subscription() { + + @Override + public void request(long n) { + if (!requested.compareAndSet(0, n)) { + child.onError(new RuntimeException("Expected to receive request before onNext but didn't")); + } + } + + @Override + public void cancel() { + + } + + }); + Subscriber<Integer> parent = new DefaultSubscriber<Integer>() { + + @Override + public void onComplete() { + child.onComplete(); + } + + @Override + public void onError(Throwable e) { + child.onError(e); + } + + @Override + public void onNext(Integer t) { + if (requested.compareAndSet(0, -99)) { + child.onError(new RuntimeException("Got values before requested")); + } + } + }; + + return parent; + } + + }).subscribeOn(Schedulers.newThread()).subscribe(ts); + ts.awaitDone(20, TimeUnit.SECONDS); + ts.assertNoErrors(); + } + + @Test + public void cancelBeforeActualSubscribe() { + TestScheduler test = new TestScheduler(); + + TestSubscriberEx<Integer> ts = Flowable.just(1) + .hide() + .subscribeOn(test) + .to(TestHelper.<Integer>testConsumer(true)); + + test.advanceTimeBy(1, TimeUnit.SECONDS); + + ts + .assertSubscribed() + .assertNoValues() + .assertNotTerminated(); + } + + @Test + public void dispose() { + TestHelper.checkDisposed(Flowable.just(1).subscribeOn(Schedulers.single())); + } + + @Test + public void deferredRequestRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + + final TestSubscriber<Integer> ts = new TestSubscriber<>(0L); + + Worker w = Schedulers.computation().createWorker(); + + final SubscribeOnSubscriber<Integer> so = new SubscribeOnSubscriber<>(ts, w, Flowable.<Integer>never(), true); + ts.onSubscribe(so); + + final BooleanSubscription bs = new BooleanSubscription(); + + try { + Runnable r1 = new Runnable() { + @Override + public void run() { + so.onSubscribe(bs); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + so.request(1); + } + }; + + TestHelper.race(r1, r2); + } finally { + w.dispose(); + } + } + } + + @Test + public void nonScheduledRequests() { + TestSubscriber<Object> ts = Flowable.create(new FlowableOnSubscribe<Object>() { + @Override + public void subscribe(FlowableEmitter<Object> s) throws Exception { + for (int i = 1; i < 1001; i++) { + s.onNext(i); + Thread.sleep(1); + } + s.onComplete(); + } + }, BackpressureStrategy.DROP) + .subscribeOn(Schedulers.single()) + .observeOn(Schedulers.computation()) + .test() + .awaitDone(20, TimeUnit.SECONDS) + .assertNoErrors() + .assertComplete(); + + int c = ts.values().size(); + + assertTrue("" + c, c > Flowable.bufferSize()); + } + + @Test + public void scheduledRequests() { + Flowable.create(new FlowableOnSubscribe<Object>() { + @Override + public void subscribe(FlowableEmitter<Object> s) throws Exception { + for (int i = 1; i < 1001; i++) { + s.onNext(i); + Thread.sleep(1); + } + s.onComplete(); + } + }, BackpressureStrategy.DROP) + .map(Functions.identity()) + .subscribeOn(Schedulers.single()) + .observeOn(Schedulers.computation()) + .test() + .awaitDone(20, TimeUnit.SECONDS) + .assertValueCount(Flowable.bufferSize()) + .assertNoErrors() + .assertComplete(); + } + + @Test + public void nonScheduledRequestsNotSubsequentSubscribeOn() { + TestSubscriber<Object> ts = Flowable.create(new FlowableOnSubscribe<Object>() { + @Override + public void subscribe(FlowableEmitter<Object> s) throws Exception { + for (int i = 1; i < 1001; i++) { + s.onNext(i); + Thread.sleep(1); + } + s.onComplete(); + } + }, BackpressureStrategy.DROP) + .map(Functions.identity()) + .subscribeOn(Schedulers.single(), false) + .observeOn(Schedulers.computation()) + .test() + .awaitDone(20, TimeUnit.SECONDS) + .assertNoErrors() + .assertComplete(); + + int c = ts.values().size(); + + assertTrue("" + c, c > Flowable.bufferSize()); + } + + @Test + public void scheduledRequestsNotSubsequentSubscribeOn() { + Flowable.create(new FlowableOnSubscribe<Object>() { + @Override + public void subscribe(FlowableEmitter<Object> s) throws Exception { + for (int i = 1; i < 1001; i++) { + s.onNext(i); + Thread.sleep(1); + } + s.onComplete(); + } + }, BackpressureStrategy.DROP) + .map(Functions.identity()) + .subscribeOn(Schedulers.single(), true) + .observeOn(Schedulers.computation()) + .test() + .awaitDone(20, TimeUnit.SECONDS) + .assertValueCount(Flowable.bufferSize()) + .assertNoErrors() + .assertComplete(); + } + + @Test + public void badRequest() { + TestHelper.assertBadRequestReported(Flowable.never().subscribeOn(ImmediateThinScheduler.INSTANCE)); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableSwitchIfEmptyTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableSwitchIfEmptyTest.java new file mode 100644 index 0000000000..d38a657b99 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableSwitchIfEmptyTest.java @@ -0,0 +1,214 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import static org.junit.Assert.*; + +import java.util.Arrays; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.junit.Test; +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.functions.Consumer; +import io.reactivex.rxjava3.internal.subscriptions.BooleanSubscription; +import io.reactivex.rxjava3.schedulers.Schedulers; +import io.reactivex.rxjava3.subscribers.*; + +public class FlowableSwitchIfEmptyTest extends RxJavaTest { + + @Test + public void switchWhenNotEmpty() throws Exception { + final AtomicBoolean subscribed = new AtomicBoolean(false); + final Flowable<Integer> flowable = Flowable.just(4) + .switchIfEmpty(Flowable.just(2) + .doOnSubscribe(new Consumer<Subscription>() { + @Override + public void accept(Subscription s) { + subscribed.set(true); + } + })); + + assertEquals(4, flowable.blockingSingle().intValue()); + assertFalse(subscribed.get()); + } + + @Test + public void switchWhenEmpty() throws Exception { + final Flowable<Integer> flowable = Flowable.<Integer>empty() + .switchIfEmpty(Flowable.fromIterable(Arrays.asList(42))); + + assertEquals(42, flowable.blockingSingle().intValue()); + } + + @Test + public void switchWithProducer() throws Exception { + final AtomicBoolean emitted = new AtomicBoolean(false); + Flowable<Long> withProducer = Flowable.unsafeCreate(new Publisher<Long>() { + @Override + public void subscribe(final Subscriber<? super Long> subscriber) { + subscriber.onSubscribe(new Subscription() { + @Override + public void request(long n) { + if (n > 0 && emitted.compareAndSet(false, true)) { + emitted.set(true); + subscriber.onNext(42L); + subscriber.onComplete(); + } + } + + @Override + public void cancel() { + + } + }); + } + }); + + final Flowable<Long> flowable = Flowable.<Long>empty().switchIfEmpty(withProducer); + assertEquals(42, flowable.blockingSingle().intValue()); + } + + @Test + public void switchTriggerUnsubscribe() throws Exception { + + final BooleanSubscription bs = new BooleanSubscription(); + + Flowable<Long> withProducer = Flowable.unsafeCreate(new Publisher<Long>() { + @Override + public void subscribe(final Subscriber<? super Long> subscriber) { + subscriber.onSubscribe(bs); + subscriber.onNext(42L); + } + }); + + Flowable.<Long>empty() + .switchIfEmpty(withProducer) + .lift(new FlowableOperator<Long, Long>() { + @Override + public Subscriber<? super Long> apply(final Subscriber<? super Long> child) { + return new DefaultSubscriber<Long>() { + @Override + public void onComplete() { + + } + + @Override + public void onError(Throwable e) { + + } + + @Override + public void onNext(Long aLong) { + cancel(); + } + + }; + } + }).subscribe(); + + assertTrue(bs.isCancelled()); + // FIXME no longer assertable +// assertTrue(sub.isUnsubscribed()); + } + + @Test + public void switchShouldNotTriggerUnsubscribe() { + final BooleanSubscription bs = new BooleanSubscription(); + + Flowable.unsafeCreate(new Publisher<Long>() { + @Override + public void subscribe(final Subscriber<? super Long> subscriber) { + subscriber.onSubscribe(bs); + subscriber.onComplete(); + } + }).switchIfEmpty(Flowable.<Long>never()).subscribe(); + assertFalse(bs.isCancelled()); + } + + @Test + public void switchRequestAlternativeObservableWithBackpressure() { + + TestSubscriber<Integer> ts = new TestSubscriber<>(1L); + + Flowable.<Integer>empty().switchIfEmpty(Flowable.just(1, 2, 3)).subscribe(ts); + + assertEquals(Arrays.asList(1), ts.values()); + ts.assertNoErrors(); + ts.request(1); + ts.assertValueCount(2); + ts.request(1); + ts.assertValueCount(3); + } + + @Test + public void backpressureNoRequest() { + TestSubscriber<Integer> ts = new TestSubscriber<>(0L); + Flowable.<Integer>empty().switchIfEmpty(Flowable.just(1, 2, 3)).subscribe(ts); + ts.assertNoValues(); + ts.assertNoErrors(); + } + + @Test + public void backpressureOnFirstObservable() { + TestSubscriber<Integer> ts = new TestSubscriber<>(0L); + Flowable.just(1, 2, 3).switchIfEmpty(Flowable.just(4, 5, 6)).subscribe(ts); + ts.assertNotComplete(); + ts.assertNoErrors(); + ts.assertNoValues(); + } + + @Test + public void requestsNotLost() throws InterruptedException { + final TestSubscriber<Long> ts = new TestSubscriber<>(0L); + Flowable.unsafeCreate(new Publisher<Long>() { + + @Override + public void subscribe(final Subscriber<? super Long> subscriber) { + subscriber.onSubscribe(new Subscription() { + final AtomicBoolean completed = new AtomicBoolean(false); + @Override + public void request(long n) { + if (n > 0 && completed.compareAndSet(false, true)) { + Schedulers.io().createWorker().schedule(new Runnable() { + @Override + public void run() { + subscriber.onComplete(); + }}, 100, TimeUnit.MILLISECONDS); + } + } + + @Override + public void cancel() { + + } + }); + }}) + .switchIfEmpty(Flowable.fromIterable(Arrays.asList(1L, 2L, 3L))) + .subscribeOn(Schedulers.computation()) + .subscribe(ts); + + Thread.sleep(50); + //request while first observable is still finishing (as empty) + ts.request(1); + ts.request(1); + Thread.sleep(500); + ts.assertNotComplete(); + ts.assertNoErrors(); + ts.assertValueCount(2); + ts.cancel(); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableSwitchTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableSwitchTest.java new file mode 100644 index 0000000000..9f200e3c56 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableSwitchTest.java @@ -0,0 +1,1395 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +import java.util.*; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.*; + +import org.junit.*; +import org.mockito.InOrder; +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.internal.subscriptions.BooleanSubscription; +import io.reactivex.rxjava3.internal.util.ExceptionHelper; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.processors.*; +import io.reactivex.rxjava3.schedulers.*; +import io.reactivex.rxjava3.subscribers.*; +import io.reactivex.rxjava3.testsupport.*; + +public class FlowableSwitchTest extends RxJavaTest { + + private TestScheduler scheduler; + private Scheduler.Worker innerScheduler; + private Subscriber<String> subscriber; + + @Before + public void before() { + scheduler = new TestScheduler(); + innerScheduler = scheduler.createWorker(); + subscriber = TestHelper.mockSubscriber(); + } + + @Test + public void switchWhenOuterCompleteBeforeInner() { + Flowable<Flowable<String>> source = Flowable.unsafeCreate(new Publisher<Flowable<String>>() { + @Override + public void subscribe(Subscriber<? super Flowable<String>> subscriber) { + subscriber.onSubscribe(new BooleanSubscription()); + publishNext(subscriber, 50, Flowable.unsafeCreate(new Publisher<String>() { + @Override + public void subscribe(Subscriber<? super String> subscriber) { + subscriber.onSubscribe(new BooleanSubscription()); + publishNext(subscriber, 70, "one"); + publishNext(subscriber, 100, "two"); + publishCompleted(subscriber, 200); + } + })); + publishCompleted(subscriber, 60); + } + }); + + Flowable<String> sampled = Flowable.switchOnNext(source); + sampled.subscribe(subscriber); + + InOrder inOrder = inOrder(subscriber); + + scheduler.advanceTimeTo(350, TimeUnit.MILLISECONDS); + inOrder.verify(subscriber, times(2)).onNext(anyString()); + inOrder.verify(subscriber, times(1)).onComplete(); + } + + @Test + public void switchWhenInnerCompleteBeforeOuter() { + Flowable<Flowable<String>> source = Flowable.unsafeCreate(new Publisher<Flowable<String>>() { + @Override + public void subscribe(Subscriber<? super Flowable<String>> subscriber) { + subscriber.onSubscribe(new BooleanSubscription()); + publishNext(subscriber, 10, Flowable.unsafeCreate(new Publisher<String>() { + @Override + public void subscribe(Subscriber<? super String> subscriber) { + subscriber.onSubscribe(new BooleanSubscription()); + publishNext(subscriber, 0, "one"); + publishNext(subscriber, 10, "two"); + publishCompleted(subscriber, 20); + } + })); + + publishNext(subscriber, 100, Flowable.unsafeCreate(new Publisher<String>() { + @Override + public void subscribe(Subscriber<? super String> subscriber) { + subscriber.onSubscribe(new BooleanSubscription()); + publishNext(subscriber, 0, "three"); + publishNext(subscriber, 10, "four"); + publishCompleted(subscriber, 20); + } + })); + publishCompleted(subscriber, 200); + } + }); + + Flowable<String> sampled = Flowable.switchOnNext(source); + sampled.subscribe(subscriber); + + InOrder inOrder = inOrder(subscriber); + + scheduler.advanceTimeTo(150, TimeUnit.MILLISECONDS); + inOrder.verify(subscriber, never()).onComplete(); + inOrder.verify(subscriber, times(1)).onNext("one"); + inOrder.verify(subscriber, times(1)).onNext("two"); + inOrder.verify(subscriber, times(1)).onNext("three"); + inOrder.verify(subscriber, times(1)).onNext("four"); + + scheduler.advanceTimeTo(250, TimeUnit.MILLISECONDS); + inOrder.verify(subscriber, never()).onNext(anyString()); + inOrder.verify(subscriber, times(1)).onComplete(); + } + + @Test + public void switchWithComplete() { + Flowable<Flowable<String>> source = Flowable.unsafeCreate(new Publisher<Flowable<String>>() { + @Override + public void subscribe(Subscriber<? super Flowable<String>> subscriber) { + subscriber.onSubscribe(new BooleanSubscription()); + publishNext(subscriber, 50, Flowable.unsafeCreate(new Publisher<String>() { + @Override + public void subscribe(final Subscriber<? super String> subscriber) { + subscriber.onSubscribe(new BooleanSubscription()); + publishNext(subscriber, 60, "one"); + publishNext(subscriber, 100, "two"); + } + })); + + publishNext(subscriber, 200, Flowable.unsafeCreate(new Publisher<String>() { + @Override + public void subscribe(final Subscriber<? super String> subscriber) { + subscriber.onSubscribe(new BooleanSubscription()); + publishNext(subscriber, 0, "three"); + publishNext(subscriber, 100, "four"); + } + })); + + publishCompleted(subscriber, 250); + } + }); + + Flowable<String> sampled = Flowable.switchOnNext(source); + sampled.subscribe(subscriber); + + InOrder inOrder = inOrder(subscriber); + + scheduler.advanceTimeTo(90, TimeUnit.MILLISECONDS); + inOrder.verify(subscriber, never()).onNext(anyString()); + verify(subscriber, never()).onComplete(); + verify(subscriber, never()).onError(any(Throwable.class)); + + scheduler.advanceTimeTo(125, TimeUnit.MILLISECONDS); + inOrder.verify(subscriber, times(1)).onNext("one"); + verify(subscriber, never()).onComplete(); + verify(subscriber, never()).onError(any(Throwable.class)); + + scheduler.advanceTimeTo(175, TimeUnit.MILLISECONDS); + inOrder.verify(subscriber, times(1)).onNext("two"); + verify(subscriber, never()).onComplete(); + verify(subscriber, never()).onError(any(Throwable.class)); + + scheduler.advanceTimeTo(225, TimeUnit.MILLISECONDS); + inOrder.verify(subscriber, times(1)).onNext("three"); + verify(subscriber, never()).onComplete(); + verify(subscriber, never()).onError(any(Throwable.class)); + + scheduler.advanceTimeTo(350, TimeUnit.MILLISECONDS); + inOrder.verify(subscriber, times(1)).onNext("four"); + verify(subscriber, never()).onComplete(); + verify(subscriber, never()).onError(any(Throwable.class)); + } + + @Test + public void switchWithError() { + Flowable<Flowable<String>> source = Flowable.unsafeCreate(new Publisher<Flowable<String>>() { + @Override + public void subscribe(Subscriber<? super Flowable<String>> subscriber) { + subscriber.onSubscribe(new BooleanSubscription()); + publishNext(subscriber, 50, Flowable.unsafeCreate(new Publisher<String>() { + @Override + public void subscribe(final Subscriber<? super String> subscriber) { + subscriber.onSubscribe(new BooleanSubscription()); + publishNext(subscriber, 50, "one"); + publishNext(subscriber, 100, "two"); + } + })); + + publishNext(subscriber, 200, Flowable.unsafeCreate(new Publisher<String>() { + @Override + public void subscribe(Subscriber<? super String> subscriber) { + subscriber.onSubscribe(new BooleanSubscription()); + publishNext(subscriber, 0, "three"); + publishNext(subscriber, 100, "four"); + } + })); + + publishError(subscriber, 250, new TestException()); + } + }); + + Flowable<String> sampled = Flowable.switchOnNext(source); + sampled.subscribe(subscriber); + + InOrder inOrder = inOrder(subscriber); + + scheduler.advanceTimeTo(90, TimeUnit.MILLISECONDS); + inOrder.verify(subscriber, never()).onNext(anyString()); + verify(subscriber, never()).onComplete(); + verify(subscriber, never()).onError(any(Throwable.class)); + + scheduler.advanceTimeTo(125, TimeUnit.MILLISECONDS); + inOrder.verify(subscriber, times(1)).onNext("one"); + verify(subscriber, never()).onComplete(); + verify(subscriber, never()).onError(any(Throwable.class)); + + scheduler.advanceTimeTo(175, TimeUnit.MILLISECONDS); + inOrder.verify(subscriber, times(1)).onNext("two"); + verify(subscriber, never()).onComplete(); + verify(subscriber, never()).onError(any(Throwable.class)); + + scheduler.advanceTimeTo(225, TimeUnit.MILLISECONDS); + inOrder.verify(subscriber, times(1)).onNext("three"); + verify(subscriber, never()).onComplete(); + verify(subscriber, never()).onError(any(Throwable.class)); + + scheduler.advanceTimeTo(350, TimeUnit.MILLISECONDS); + inOrder.verify(subscriber, never()).onNext(anyString()); + verify(subscriber, never()).onComplete(); + verify(subscriber, times(1)).onError(any(TestException.class)); + } + + @Test + public void switchWithSubsequenceComplete() { + Flowable<Flowable<String>> source = Flowable.unsafeCreate(new Publisher<Flowable<String>>() { + @Override + public void subscribe(Subscriber<? super Flowable<String>> subscriber) { + subscriber.onSubscribe(new BooleanSubscription()); + publishNext(subscriber, 50, Flowable.unsafeCreate(new Publisher<String>() { + @Override + public void subscribe(Subscriber<? super String> subscriber) { + subscriber.onSubscribe(new BooleanSubscription()); + publishNext(subscriber, 50, "one"); + publishNext(subscriber, 100, "two"); + } + })); + + publishNext(subscriber, 130, Flowable.unsafeCreate(new Publisher<String>() { + @Override + public void subscribe(Subscriber<? super String> subscriber) { + subscriber.onSubscribe(new BooleanSubscription()); + publishCompleted(subscriber, 0); + } + })); + + publishNext(subscriber, 150, Flowable.unsafeCreate(new Publisher<String>() { + @Override + public void subscribe(Subscriber<? super String> subscriber) { + subscriber.onSubscribe(new BooleanSubscription()); + publishNext(subscriber, 50, "three"); + } + })); + } + }); + + Flowable<String> sampled = Flowable.switchOnNext(source); + sampled.subscribe(subscriber); + + InOrder inOrder = inOrder(subscriber); + + scheduler.advanceTimeTo(90, TimeUnit.MILLISECONDS); + inOrder.verify(subscriber, never()).onNext(anyString()); + verify(subscriber, never()).onComplete(); + verify(subscriber, never()).onError(any(Throwable.class)); + + scheduler.advanceTimeTo(125, TimeUnit.MILLISECONDS); + inOrder.verify(subscriber, times(1)).onNext("one"); + verify(subscriber, never()).onComplete(); + verify(subscriber, never()).onError(any(Throwable.class)); + + scheduler.advanceTimeTo(250, TimeUnit.MILLISECONDS); + inOrder.verify(subscriber, times(1)).onNext("three"); + verify(subscriber, never()).onComplete(); + verify(subscriber, never()).onError(any(Throwable.class)); + } + + @Test + public void switchWithSubsequenceError() { + Flowable<Flowable<String>> source = Flowable.unsafeCreate(new Publisher<Flowable<String>>() { + @Override + public void subscribe(Subscriber<? super Flowable<String>> subscriber) { + subscriber.onSubscribe(new BooleanSubscription()); + publishNext(subscriber, 50, Flowable.unsafeCreate(new Publisher<String>() { + @Override + public void subscribe(Subscriber<? super String> subscriber) { + subscriber.onSubscribe(new BooleanSubscription()); + publishNext(subscriber, 50, "one"); + publishNext(subscriber, 100, "two"); + } + })); + + publishNext(subscriber, 130, Flowable.unsafeCreate(new Publisher<String>() { + @Override + public void subscribe(Subscriber<? super String> subscriber) { + subscriber.onSubscribe(new BooleanSubscription()); + publishError(subscriber, 0, new TestException()); + } + })); + + publishNext(subscriber, 150, Flowable.unsafeCreate(new Publisher<String>() { + @Override + public void subscribe(Subscriber<? super String> subscriber) { + subscriber.onSubscribe(new BooleanSubscription()); + publishNext(subscriber, 50, "three"); + } + })); + + } + }); + + Flowable<String> sampled = Flowable.switchOnNext(source); + sampled.subscribe(subscriber); + + InOrder inOrder = inOrder(subscriber); + + scheduler.advanceTimeTo(90, TimeUnit.MILLISECONDS); + inOrder.verify(subscriber, never()).onNext(anyString()); + verify(subscriber, never()).onComplete(); + verify(subscriber, never()).onError(any(Throwable.class)); + + scheduler.advanceTimeTo(125, TimeUnit.MILLISECONDS); + inOrder.verify(subscriber, times(1)).onNext("one"); + verify(subscriber, never()).onComplete(); + verify(subscriber, never()).onError(any(Throwable.class)); + + scheduler.advanceTimeTo(250, TimeUnit.MILLISECONDS); + inOrder.verify(subscriber, never()).onNext("three"); + verify(subscriber, never()).onComplete(); + verify(subscriber, times(1)).onError(any(TestException.class)); + } + + private <T> void publishCompleted(final Subscriber<T> subscriber, long delay) { + innerScheduler.schedule(new Runnable() { + @Override + public void run() { + subscriber.onComplete(); + } + }, delay, TimeUnit.MILLISECONDS); + } + + private <T> void publishError(final Subscriber<T> subscriber, long delay, final Throwable error) { + innerScheduler.schedule(new Runnable() { + @Override + public void run() { + subscriber.onError(error); + } + }, delay, TimeUnit.MILLISECONDS); + } + + private <T> void publishNext(final Subscriber<T> subscriber, long delay, final T value) { + innerScheduler.schedule(new Runnable() { + @Override + public void run() { + subscriber.onNext(value); + } + }, delay, TimeUnit.MILLISECONDS); + } + + @Test + public void switchIssue737() { + // https://github.com/ReactiveX/RxJava/issues/737 + Flowable<Flowable<String>> source = Flowable.unsafeCreate(new Publisher<Flowable<String>>() { + @Override + public void subscribe(Subscriber<? super Flowable<String>> subscriber) { + subscriber.onSubscribe(new BooleanSubscription()); + publishNext(subscriber, 0, Flowable.unsafeCreate(new Publisher<String>() { + @Override + public void subscribe(Subscriber<? super String> subscriber) { + subscriber.onSubscribe(new BooleanSubscription()); + publishNext(subscriber, 10, "1-one"); + publishNext(subscriber, 20, "1-two"); + // The following events will be ignored + publishNext(subscriber, 30, "1-three"); + publishCompleted(subscriber, 40); + } + })); + publishNext(subscriber, 25, Flowable.unsafeCreate(new Publisher<String>() { + @Override + public void subscribe(Subscriber<? super String> subscriber) { + subscriber.onSubscribe(new BooleanSubscription()); + publishNext(subscriber, 10, "2-one"); + publishNext(subscriber, 20, "2-two"); + publishNext(subscriber, 30, "2-three"); + publishCompleted(subscriber, 40); + } + })); + publishCompleted(subscriber, 30); + } + }); + + Flowable<String> sampled = Flowable.switchOnNext(source); + sampled.subscribe(subscriber); + + scheduler.advanceTimeTo(1000, TimeUnit.MILLISECONDS); + + InOrder inOrder = inOrder(subscriber); + inOrder.verify(subscriber, times(1)).onNext("1-one"); + inOrder.verify(subscriber, times(1)).onNext("1-two"); + inOrder.verify(subscriber, times(1)).onNext("2-one"); + inOrder.verify(subscriber, times(1)).onNext("2-two"); + inOrder.verify(subscriber, times(1)).onNext("2-three"); + inOrder.verify(subscriber, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void backpressure() { + + PublishProcessor<String> o1 = PublishProcessor.create(); + PublishProcessor<String> o2 = PublishProcessor.create(); + PublishProcessor<String> o3 = PublishProcessor.create(); + + PublishProcessor<PublishProcessor<String>> o = PublishProcessor.create(); + + publishNext(o, 0, o1); + publishNext(o, 5, o2); + publishNext(o, 10, o3); + publishCompleted(o, 15); + + for (int i = 0; i < 10; i++) { + publishNext(o1, i * 5, "a" + (i + 1)); + publishNext(o2, 5 + i * 5, "b" + (i + 1)); + publishNext(o3, 10 + i * 5, "c" + (i + 1)); + } + + publishCompleted(o1, 45); + publishCompleted(o2, 50); + publishCompleted(o3, 55); + + final TestSubscriberEx<String> testSubscriber = new TestSubscriberEx<>(); + Flowable.switchOnNext(o).subscribe(new DefaultSubscriber<String>() { + + private int requested; + + @Override + public void onStart() { + requested = 3; + request(3); + testSubscriber.onSubscribe(new BooleanSubscription()); + } + + @Override + public void onComplete() { + testSubscriber.onComplete(); + } + + @Override + public void onError(Throwable e) { + testSubscriber.onError(e); + } + + @Override + public void onNext(String s) { + testSubscriber.onNext(s); + requested--; + if (requested == 0) { + requested = 3; + request(3); + } + } + }); + scheduler.advanceTimeBy(1, TimeUnit.SECONDS); + testSubscriber.assertValues("a1", "b1", "c1", "c2", "c3", "c4", "c5", "c6", "c7", "c8", "c9", "c10"); + testSubscriber.assertNoErrors(); + testSubscriber.assertTerminated(); + } + + @Test + public void unsubscribe() { + final AtomicBoolean isUnsubscribed = new AtomicBoolean(); + Flowable.switchOnNext( + Flowable.unsafeCreate(new Publisher<Flowable<Integer>>() { + @Override + public void subscribe(final Subscriber<? super Flowable<Integer>> subscriber) { + BooleanSubscription bs = new BooleanSubscription(); + subscriber.onSubscribe(bs); + subscriber.onNext(Flowable.just(1)); + isUnsubscribed.set(bs.isCancelled()); + } + }) + ).take(1).subscribe(); + assertTrue("Switch doesn't propagate 'unsubscribe'", isUnsubscribed.get()); + } + /** The upstream producer hijacked the switch producer stopping the requests aimed at the inner observables. */ + @Test + public void issue2654() { + Flowable<String> oneItem = Flowable.just("Hello").mergeWith(Flowable.<String>never()); + + Flowable<String> src = oneItem.switchMap(new Function<String, Flowable<String>>() { + @Override + public Flowable<String> apply(final String s) { + return Flowable.just(s) + .mergeWith(Flowable.interval(10, TimeUnit.MILLISECONDS) + .map(new Function<Long, String>() { + @Override + public String apply(Long i) { + return s + " " + i; + } + })).take(250); + } + }) + .share() + ; + + TestSubscriberEx<String> ts = new TestSubscriberEx<String>() { + @Override + public void onNext(String t) { + super.onNext(t); + if (values().size() == 250) { + onComplete(); + dispose(); + } + } + }; + src.subscribe(ts); + + ts.awaitDone(10, TimeUnit.SECONDS); + + System.out.println("> testIssue2654: " + ts.values().size()); + + ts.assertTerminated(); + ts.assertNoErrors(); + + Assert.assertEquals(250, ts.values().size()); + } + + @Test + public void initialRequestsAreAdditive() { + TestSubscriber<Long> ts = new TestSubscriber<>(0L); + Flowable.switchOnNext( + Flowable.interval(100, TimeUnit.MILLISECONDS) + .map( + new Function<Long, Flowable<Long>>() { + @Override + public Flowable<Long> apply(Long t) { + return Flowable.just(1L, 2L, 3L); + } + } + ).take(3)) + .subscribe(ts); + ts.request(Long.MAX_VALUE - 100); + ts.request(1); + ts.awaitDone(5, TimeUnit.SECONDS); + } + + @Test + public void initialRequestsDontOverflow() { + TestSubscriber<Long> ts = new TestSubscriber<>(0L); + Flowable.switchOnNext( + Flowable.interval(100, TimeUnit.MILLISECONDS) + .map(new Function<Long, Flowable<Long>>() { + @Override + public Flowable<Long> apply(Long t) { + return Flowable.fromIterable(Arrays.asList(1L, 2L, 3L)).hide(); + } + }).take(3)).subscribe(ts); + ts.request(Long.MAX_VALUE - 1); + ts.request(2); + ts.awaitDone(5, TimeUnit.SECONDS); + assertTrue(ts.values().size() > 0); + } + + @Test + public void secondaryRequestsDontOverflow() throws InterruptedException { + TestSubscriber<Long> ts = new TestSubscriber<>(0L); + Flowable.switchOnNext( + Flowable.interval(100, TimeUnit.MILLISECONDS) + .map(new Function<Long, Flowable<Long>>() { + @Override + public Flowable<Long> apply(Long t) { + return Flowable.fromIterable(Arrays.asList(1L, 2L, 3L)).hide(); + } + }).take(3)).subscribe(ts); + ts.request(1); + //we will miss two of the first observable + Thread.sleep(250); + ts.request(Long.MAX_VALUE - 1); + ts.request(Long.MAX_VALUE - 1); + ts.awaitDone(5, TimeUnit.SECONDS); + ts.assertValueCount(7); + } + + @Test + public void delayErrors() { + PublishProcessor<Publisher<Integer>> source = PublishProcessor.create(); + + TestSubscriberEx<Integer> ts = source.switchMapDelayError(Functions.<Publisher<Integer>>identity()) + .to(TestHelper.<Integer>testConsumer()); + + ts.assertNoValues() + .assertNoErrors() + .assertNotComplete(); + + source.onNext(Flowable.just(1)); + + source.onNext(Flowable.<Integer>error(new TestException("Forced failure 1"))); + + source.onNext(Flowable.just(2, 3, 4)); + + source.onNext(Flowable.<Integer>error(new TestException("Forced failure 2"))); + + source.onNext(Flowable.just(5)); + + source.onError(new TestException("Forced failure 3")); + + ts.assertValues(1, 2, 3, 4, 5) + .assertNotComplete() + .assertError(CompositeException.class); + + List<Throwable> errors = ExceptionHelper.flatten(ts.errors().get(0)); + + TestHelper.assertError(errors, 0, TestException.class, "Forced failure 1"); + TestHelper.assertError(errors, 1, TestException.class, "Forced failure 2"); + TestHelper.assertError(errors, 2, TestException.class, "Forced failure 3"); + } + + @Test + public void switchOnNextPrefetch() { + final List<Integer> list = new ArrayList<>(); + + Flowable<Integer> source = Flowable.range(1, 10).hide().doOnNext(new Consumer<Integer>() { + @Override + public void accept(Integer v) throws Exception { + list.add(v); + } + }); + + Flowable.switchOnNext(Flowable.just(source).hide(), 2) + .test(1); + + assertEquals(Arrays.asList(1, 2, 3), list); + } + + @Test + public void switchOnNextDelayError() { + final List<Integer> list = new ArrayList<>(); + + Flowable<Integer> source = Flowable.range(1, 10).hide().doOnNext(new Consumer<Integer>() { + @Override + public void accept(Integer v) throws Exception { + list.add(v); + } + }); + + Flowable.switchOnNextDelayError(Flowable.just(source).hide()) + .test(1); + + assertEquals(Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10), list); + } + + @Test + public void switchOnNextDelayErrorPrefetch() { + final List<Integer> list = new ArrayList<>(); + + Flowable<Integer> source = Flowable.range(1, 10).hide().doOnNext(new Consumer<Integer>() { + @Override + public void accept(Integer v) throws Exception { + list.add(v); + } + }); + + Flowable.switchOnNextDelayError(Flowable.just(source).hide(), 2) + .test(1); + + assertEquals(Arrays.asList(1, 2, 3), list); + } + + @Test + public void switchOnNextDelayErrorWithError() { + PublishProcessor<Flowable<Integer>> pp = PublishProcessor.create(); + + TestSubscriber<Integer> ts = Flowable.switchOnNextDelayError(pp).test(); + + pp.onNext(Flowable.just(1)); + pp.onNext(Flowable.<Integer>error(new TestException())); + pp.onNext(Flowable.range(2, 4)); + pp.onComplete(); + + ts.assertFailure(TestException.class, 1, 2, 3, 4, 5); + } + + @Test + public void switchOnNextDelayErrorBufferSize() { + PublishProcessor<Flowable<Integer>> pp = PublishProcessor.create(); + + TestSubscriber<Integer> ts = Flowable.switchOnNextDelayError(pp, 2).test(); + + pp.onNext(Flowable.just(1)); + pp.onNext(Flowable.range(2, 4)); + pp.onComplete(); + + ts.assertResult(1, 2, 3, 4, 5); + } + + @Test + public void switchMapDelayErrorEmptySource() { + assertSame(Flowable.empty(), Flowable.<Object>empty() + .switchMapDelayError(new Function<Object, Publisher<Integer>>() { + @Override + public Publisher<Integer> apply(Object v) throws Exception { + return Flowable.just(1); + } + }, 16)); + } + + @Test + public void switchMapDelayErrorJustSource() { + Flowable.just(0) + .switchMapDelayError(new Function<Object, Publisher<Integer>>() { + @Override + public Publisher<Integer> apply(Object v) throws Exception { + return Flowable.just(1); + } + }, 16) + .test() + .assertResult(1); + + } + + @Test + public void switchMapErrorEmptySource() { + assertSame(Flowable.empty(), Flowable.<Object>empty() + .switchMap(new Function<Object, Publisher<Integer>>() { + @Override + public Publisher<Integer> apply(Object v) throws Exception { + return Flowable.just(1); + } + }, 16)); + } + + @Test + public void switchMapJustSource() { + Flowable.just(0) + .switchMap(new Function<Object, Publisher<Integer>>() { + @Override + public Publisher<Integer> apply(Object v) throws Exception { + return Flowable.just(1); + } + }, 16) + .test() + .assertResult(1); + + } + + @Test + public void switchMapInnerCancelled() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + + TestSubscriber<Integer> ts = Flowable.just(1) + .switchMap(Functions.justFunction(pp)) + .test(); + + assertTrue(pp.hasSubscribers()); + + ts.cancel(); + + assertFalse(pp.hasSubscribers()); + } + + @Test + public void dispose() { + TestHelper.checkDisposed(Flowable.switchOnNext( + Flowable.just(Flowable.just(1)).hide())); + } + + @Test + public void nextSourceErrorRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + + final PublishProcessor<Integer> pp1 = PublishProcessor.create(); + final PublishProcessor<Integer> pp2 = PublishProcessor.create(); + + pp1.switchMap(new Function<Integer, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Integer v) throws Exception { + if (v == 1) { + return pp2; + } + return Flowable.never(); + } + }) + .test(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + pp1.onNext(2); + } + }; + + final TestException ex = new TestException(); + + Runnable r2 = new Runnable() { + @Override + public void run() { + pp2.onError(ex); + } + }; + + TestHelper.race(r1, r2); + + for (Throwable e : errors) { + assertTrue(e.toString(), e instanceof TestException); + } + } finally { + RxJavaPlugins.reset(); + } + } + } + + @Test + public void outerInnerErrorRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + + final PublishProcessor<Integer> pp1 = PublishProcessor.create(); + final PublishProcessor<Integer> pp2 = PublishProcessor.create(); + + pp1.switchMap(new Function<Integer, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Integer v) throws Exception { + if (v == 1) { + return pp2; + } + return Flowable.never(); + } + }) + .test(); + + final TestException ex1 = new TestException(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + pp1.onError(ex1); + } + }; + + final TestException ex2 = new TestException(); + + Runnable r2 = new Runnable() { + @Override + public void run() { + pp2.onError(ex2); + } + }; + + TestHelper.race(r1, r2); + + for (Throwable e : errors) { + assertTrue(e.toString(), e instanceof TestException); + } + } finally { + RxJavaPlugins.reset(); + } + } + } + + @Test + public void nextCancelRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final PublishProcessor<Integer> pp1 = PublishProcessor.create(); + + final TestSubscriber<Integer> ts = pp1.switchMap(new Function<Integer, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Integer v) throws Exception { + return Flowable.never(); + } + }) + .test(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + pp1.onNext(2); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + ts.cancel(); + } + }; + + TestHelper.race(r1, r2); + } + } + + @Test + public void mapperThrows() { + Flowable.just(1).hide() + .switchMap(new Function<Integer, Flowable<Object>>() { + @Override + public Flowable<Object> apply(Integer v) throws Exception { + throw new TestException(); + } + }) + .test() + .assertFailure(TestException.class); + } + + @Test + public void badMainSource() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + new Flowable<Integer>() { + @Override + protected void subscribeActual(Subscriber<? super Integer> subscriber) { + subscriber.onSubscribe(new BooleanSubscription()); + subscriber.onComplete(); + subscriber.onError(new TestException()); + subscriber.onComplete(); + } + } + .switchMap(Functions.justFunction(Flowable.never())) + .test() + .assertResult(); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void emptyInner() { + Flowable.range(1, 5) + .switchMap(Functions.justFunction(Flowable.empty())) + .test() + .assertResult(); + } + + @Test + public void justInner() { + Flowable.range(1, 5) + .switchMap(Functions.justFunction(Flowable.just(1))) + .test() + .assertResult(1, 1, 1, 1, 1); + } + + @Test + public void badInnerSource() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + Flowable.just(1).hide() + .switchMap(Functions.justFunction(new Flowable<Integer>() { + @Override + protected void subscribeActual(Subscriber<? super Integer> subscriber) { + subscriber.onSubscribe(new BooleanSubscription()); + subscriber.onError(new TestException()); + subscriber.onComplete(); + subscriber.onError(new TestException()); + subscriber.onComplete(); + } + })) + .test() + .assertFailure(TestException.class); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void innerCompletesReentrant() { + final PublishProcessor<Integer> pp = PublishProcessor.create(); + + TestSubscriber<Integer> ts = new TestSubscriber<Integer>() { + @Override + public void onNext(Integer t) { + super.onNext(t); + pp.onComplete(); + } + }; + + Flowable.just(1).hide() + .switchMap(Functions.justFunction(pp)) + .subscribe(ts); + + pp.onNext(1); + + ts.assertResult(1); + } + + @Test + public void innerErrorsReentrant() { + final PublishProcessor<Integer> pp = PublishProcessor.create(); + + TestSubscriber<Integer> ts = new TestSubscriber<Integer>() { + @Override + public void onNext(Integer t) { + super.onNext(t); + pp.onError(new TestException()); + } + }; + + Flowable.just(1).hide() + .switchMap(Functions.justFunction(pp)) + .subscribe(ts); + + pp.onNext(1); + + ts.assertFailure(TestException.class, 1); + } + + @Test + public void scalarMap() { + Flowable.switchOnNext(Flowable.just(Flowable.just(1))) + .test() + .assertResult(1); + } + + @Test + public void scalarMapDelayError() { + Flowable.switchOnNextDelayError(Flowable.just(Flowable.just(1))) + .test() + .assertResult(1); + } + + @Test + public void scalarXMap() { + Flowable.fromCallable(Functions.justCallable(1)) + .switchMap(Functions.justFunction(Flowable.just(1))) + .test() + .assertResult(1); + } + + @Test + public void badSource() { + TestHelper.checkBadSourceFlowable(new Function<Flowable<Integer>, Object>() { + @Override + public Object apply(Flowable<Integer> f) throws Exception { + return f.switchMap(Functions.justFunction(Flowable.just(1))); + } + }, false, 1, 1, 1); + } + + @Test + public void innerOverflow() { + Flowable.just(1).hide() + .switchMap(Functions.justFunction(new Flowable<Integer>() { + @Override + protected void subscribeActual(Subscriber<? super Integer> s) { + s.onSubscribe(new BooleanSubscription()); + for (int i = 0; i < 10; i++) { + s.onNext(i); + } + } + }), 8) + .test(1L) + .assertFailure(QueueOverflowException.class, 0); + } + + @Test + public void drainCancelRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final TestSubscriber<Integer> ts = new TestSubscriber<>(); + + final PublishProcessor<Integer> pp = PublishProcessor.create(); + + Flowable.just(1).hide() + .switchMap(Functions.justFunction(pp)) + .subscribe(ts); + + Runnable r1 = new Runnable() { + @Override + public void run() { + ts.cancel(); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + pp.onNext(1); + } + }; + + TestHelper.race(r1, r2); + } + } + + @Test + public void fusedInnerCrash() { + Flowable.just(1).hide() + .switchMap(Functions.justFunction(Flowable.just(1) + .map(new Function<Integer, Object>() { + @Override + public Object apply(Integer v) throws Exception { + throw new TestException(); + } + }) + .compose(TestHelper.<Object>flowableStripBoundary()) + ) + ) + .test() + .assertFailure(TestException.class); + } + + @Test + public void innerCancelledOnMainError() { + final PublishProcessor<Integer> main = PublishProcessor.create(); + final PublishProcessor<Integer> inner = PublishProcessor.create(); + + TestSubscriber<Integer> ts = main.switchMap(Functions.justFunction(inner)) + .test(); + + assertTrue(main.hasSubscribers()); + + main.onNext(1); + + assertTrue(inner.hasSubscribers()); + + main.onError(new TestException()); + + assertFalse(inner.hasSubscribers()); + + ts.assertFailure(TestException.class); + } + + @Test + public void fusedBoundary() { + String thread = Thread.currentThread().getName(); + + Flowable.range(1, 10000) + .switchMap(new Function<Integer, Flowable<Object>>() { + @Override + public Flowable<Object> apply(Integer v) + throws Exception { + return Flowable.just(2).hide() + .observeOn(Schedulers.single()) + .map(new Function<Integer, Object>() { + @Override + public Object apply(Integer w) throws Exception { + return Thread.currentThread().getName(); + } + }); + } + }) + .to(TestHelper.<Object>testConsumer()) + .awaitDone(5, TimeUnit.SECONDS) + .assertNever(thread) + .assertNoErrors() + .assertComplete(); + } + + @Test + public void undeliverableUponCancel() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + final TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(); + + Flowable.just(1) + .map(new Function<Integer, Integer>() { + @Override + public Integer apply(Integer v) throws Throwable { + ts.cancel(); + throw new TestException(); + } + }) + .switchMap(new Function<Integer, Publisher<Integer>>() { + @Override + public Publisher<Integer> apply(Integer v) throws Throwable { + return Flowable.just(v).hide(); + } + }) + .subscribe(ts); + + ts.assertEmpty(); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void switchMapFusedIterable() { + Flowable.range(1, 2) + .switchMap(new Function<Integer, Publisher<Integer>>() { + @Override + public Publisher<Integer> apply(Integer v) + throws Throwable { + return Flowable.fromIterable(Arrays.asList(v * 10)); + } + }) + .test() + .assertResult(10, 20); + } + + @Test + public void switchMapHiddenIterable() { + Flowable.range(1, 2) + .switchMap(new Function<Integer, Publisher<Integer>>() { + @Override + public Publisher<Integer> apply(Integer v) + throws Throwable { + return Flowable.fromIterable(Arrays.asList(v * 10)).hide(); + } + }) + .test() + .assertResult(10, 20); + } + + @Test + public void asyncFusedInner() { + Flowable.just(1) + .hide() + .switchMap(v -> Flowable.fromCallable(() -> 1)) + .test() + .assertResult(1); + } + + @Test + public void innerIgnoresCancelAndErrors() throws Throwable { + TestHelper.withErrorTracking(errors -> { + PublishProcessor<Integer> pp = PublishProcessor.create(); + + TestSubscriber<Object> ts = pp + .switchMap(v -> { + if (v == 1) { + return Flowable.unsafeCreate(s -> { + s.onSubscribe(new BooleanSubscription()); + pp.onNext(2); + s.onError(new TestException()); + }); + } + return Flowable.never(); + }) + .test(); + + pp.onNext(1); + + ts.assertEmpty(); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + }); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeFlowable(f -> f.switchMap(v -> Flowable.never())); + } + + @Test + public void badRequest() { + TestHelper.assertBadRequestReported(Flowable.never().switchMap(v -> Flowable.never())); + } + + @Test + public void innerFailed() { + BehaviorProcessor.createDefault(Flowable.error(new TestException())) + .switchMap(v -> v) + .test() + .assertFailure(TestException.class) + ; + } + + @Test + public void innerCompleted() { + BehaviorProcessor.createDefault(Flowable.empty().hide()) + .switchMap(v -> v) + .test() + .assertEmpty() + ; + } + + @Test + public void innerCompletedBackpressureBoundary() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + + TestSubscriber<Integer> ts = BehaviorProcessor.createDefault(pp) + .onBackpressureBuffer() + .switchMap(v -> v) + .test(1L) + ; + + ts.assertEmpty(); + + pp.onNext(1); + pp.onComplete(); + + ts.assertValuesOnly(1); + } + + @Test + public void innerCompletedDelayError() { + BehaviorProcessor.createDefault(Flowable.empty().hide()) + .switchMapDelayError(v -> v) + .test() + .assertEmpty() + ; + } + + @Test + public void innerCompletedBackpressureBoundaryDelayError() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + + TestSubscriber<Integer> ts = BehaviorProcessor.createDefault(pp) + .onBackpressureBuffer() + .switchMapDelayError(v -> v) + .test(1L) + ; + + ts.assertEmpty(); + + pp.onNext(1); + pp.onComplete(); + + ts.assertValuesOnly(1); + } + + @Test + public void cancellationShouldTriggerInnerCancellationRace() throws Throwable { + AtomicInteger outer = new AtomicInteger(); + AtomicInteger inner = new AtomicInteger(); + + int n = 10_000; + for (int i = 0; i < n; i++) { + Flowable.<Integer>create(it -> { + it.onNext(0); + }, BackpressureStrategy.MISSING) + .switchMap(v -> createFlowable(inner)) + .observeOn(Schedulers.computation()) + .doFinally(() -> { + outer.incrementAndGet(); + }) + .take(1) + .blockingSubscribe(v -> { }, Throwable::printStackTrace); + } + + Thread.sleep(100); + assertEquals(inner.get(), outer.get()); + assertEquals(n, inner.get()); + } + + Flowable<Integer> createFlowable(AtomicInteger inner) { + return Flowable.<Integer>unsafeCreate(s -> { + SerializedSubscriber<Integer> it = new SerializedSubscriber<>(s); + it.onSubscribe(new BooleanSubscription()); + Schedulers.io().scheduleDirect(() -> { + it.onNext(1); + }, 0, TimeUnit.MILLISECONDS); + Schedulers.io().scheduleDirect(() -> { + it.onNext(2); + }, 0, TimeUnit.MILLISECONDS); + }) + .doFinally(() -> { + inner.incrementAndGet(); + }); + } + + @Test + public void innerOnSubscribeOuterCancelRace() { + TestSubscriber<Integer> ts = new TestSubscriber<Integer>(); + + Flowable.just(1) + .hide() + .switchMap(v -> Flowable.just(1) + .doOnSubscribe(d -> ts.cancel()) + .scan(1, (a, b) -> a) + ) + .subscribe(ts); + + ts.assertEmpty(); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableTakeLastOneTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableTakeLastOneTest.java new file mode 100644 index 0000000000..1e51a40249 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableTakeLastOneTest.java @@ -0,0 +1,166 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import static org.junit.Assert.*; + +import java.util.*; +import java.util.concurrent.atomic.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.subscribers.DefaultSubscriber; +import io.reactivex.rxjava3.testsupport.*; + +public class FlowableTakeLastOneTest extends RxJavaTest { + + @Test + public void lastOfManyReturnsLast() { + TestSubscriberEx<Integer> s = new TestSubscriberEx<>(); + Flowable.range(1, 10).takeLast(1).subscribe(s); + s.assertValue(10); + s.assertNoErrors(); + s.assertTerminated(); + // NO longer assertable +// s.assertUnsubscribed(); + } + + @Test + public void lastOfEmptyReturnsEmpty() { + TestSubscriberEx<Object> s = new TestSubscriberEx<>(); + Flowable.empty().takeLast(1).subscribe(s); + s.assertNoValues(); + s.assertNoErrors(); + s.assertTerminated(); + // NO longer assertable +// s.assertUnsubscribed(); + } + + @Test + public void lastOfOneReturnsLast() { + TestSubscriberEx<Integer> s = new TestSubscriberEx<>(); + Flowable.just(1).takeLast(1).subscribe(s); + s.assertValue(1); + s.assertNoErrors(); + s.assertTerminated(); + // NO longer assertable +// s.assertUnsubscribed(); + } + + @Test + public void unsubscribesFromUpstream() { + final AtomicBoolean unsubscribed = new AtomicBoolean(false); + Action unsubscribeAction = new Action() { + @Override + public void run() { + unsubscribed.set(true); + } + }; + + Flowable.just(1).concatWith(Flowable.<Integer>never()) + .doOnCancel(unsubscribeAction) + .takeLast(1) + .subscribe().dispose(); + + assertTrue(unsubscribed.get()); + } + + @Test + public void lastWithBackpressure() { + MySubscriber<Integer> s = new MySubscriber<>(0); + Flowable.just(1).takeLast(1).subscribe(s); + assertEquals(0, s.list.size()); + s.requestMore(1); + assertEquals(1, s.list.size()); + } + + @Test + public void takeLastZeroProcessesAllItemsButIgnoresThem() { + final AtomicInteger upstreamCount = new AtomicInteger(); + final int num = 10; + long count = Flowable.range(1, num).doOnNext(new Consumer<Integer>() { + + @Override + public void accept(Integer t) { + upstreamCount.incrementAndGet(); + }}) + .takeLast(0).count().blockingGet(); + assertEquals(num, upstreamCount.get()); + assertEquals(0L, count); + } + + private static class MySubscriber<T> extends DefaultSubscriber<T> { + + private long initialRequest; + + MySubscriber(long initialRequest) { + this.initialRequest = initialRequest; + } + + final List<T> list = new ArrayList<>(); + + public void requestMore(long n) { + request(n); + } + + @Override + public void onStart() { + if (initialRequest > 0) { + request(initialRequest); + } + } + + @Override + public void onComplete() { + + } + + @Override + public void onError(Throwable e) { + + } + + @Override + public void onNext(T t) { + list.add(t); + } + + } + + @Test + public void dispose() { + TestHelper.checkDisposed(Flowable.just(1).takeLast(1)); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeFlowable(new Function<Flowable<Object>, Flowable<Object>>() { + @Override + public Flowable<Object> apply(Flowable<Object> f) throws Exception { + return f.takeLast(1); + } + }); + } + + @Test + public void error() { + Flowable.error(new TestException()) + .takeLast(1) + .test() + .assertFailure(TestException.class); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableTakeLastTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableTakeLastTest.java new file mode 100644 index 0000000000..6008461ed5 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableTakeLastTest.java @@ -0,0 +1,394 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import java.util.*; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.Test; +import org.mockito.InOrder; +import org.reactivestreams.*; + +import io.reactivex.rxjava3.annotations.NonNull; +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.schedulers.Schedulers; +import io.reactivex.rxjava3.subscribers.*; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class FlowableTakeLastTest extends RxJavaTest { + + @Test + public void takeLastEmpty() { + Flowable<String> w = Flowable.empty(); + Flowable<String> take = w.takeLast(2); + + Subscriber<String> subscriber = TestHelper.mockSubscriber(); + take.subscribe(subscriber); + + verify(subscriber, never()).onNext(any(String.class)); + verify(subscriber, never()).onError(any(Throwable.class)); + verify(subscriber, times(1)).onComplete(); + } + + @Test + public void takeLast1() { + Flowable<String> w = Flowable.just("one", "two", "three"); + Flowable<String> take = w.takeLast(2); + + Subscriber<String> subscriber = TestHelper.mockSubscriber(); + InOrder inOrder = inOrder(subscriber); + take.subscribe(subscriber); + + inOrder.verify(subscriber, times(1)).onNext("two"); + inOrder.verify(subscriber, times(1)).onNext("three"); + verify(subscriber, never()).onNext("one"); + verify(subscriber, never()).onError(any(Throwable.class)); + verify(subscriber, times(1)).onComplete(); + } + + @Test + public void takeLast2() { + Flowable<String> w = Flowable.just("one"); + Flowable<String> take = w.takeLast(10); + + Subscriber<String> subscriber = TestHelper.mockSubscriber(); + take.subscribe(subscriber); + + verify(subscriber, times(1)).onNext("one"); + verify(subscriber, never()).onError(any(Throwable.class)); + verify(subscriber, times(1)).onComplete(); + } + + @Test + public void takeLastWithZeroCount() { + Flowable<String> w = Flowable.just("one"); + Flowable<String> take = w.takeLast(0); + + Subscriber<String> subscriber = TestHelper.mockSubscriber(); + take.subscribe(subscriber); + + verify(subscriber, never()).onNext("one"); + verify(subscriber, never()).onError(any(Throwable.class)); + verify(subscriber, times(1)).onComplete(); + } + + @Test(expected = IllegalArgumentException.class) + public void takeLastWithNegativeCount() { + Flowable.just("one").takeLast(-1); + } + + @Test + public void backpressure1() { + TestSubscriber<Integer> ts = new TestSubscriber<>(); + Flowable.range(1, 100000).takeLast(1) + .observeOn(Schedulers.newThread()) + .map(newSlowProcessor()).subscribe(ts); + ts.awaitDone(5, TimeUnit.SECONDS); + ts.assertNoErrors(); + ts.assertValue(100000); + } + + @Test + public void backpressure2() { + TestSubscriber<Integer> ts = new TestSubscriber<>(); + Flowable.range(1, 100000).takeLast(Flowable.bufferSize() * 4) + .observeOn(Schedulers.newThread()).map(newSlowProcessor()).subscribe(ts); + ts.awaitDone(5, TimeUnit.SECONDS); + ts.assertNoErrors(); + assertEquals(Flowable.bufferSize() * 4, ts.values().size()); + } + + private Function<Integer, Integer> newSlowProcessor() { + return new Function<Integer, Integer>() { + int c; + + @Override + public Integer apply(Integer i) { + if (c++ < 100) { + try { + Thread.sleep(1); + } catch (InterruptedException e) { + } + } + return i; + } + + }; + } + + @Test + public void issue1522() { + // https://github.com/ReactiveX/RxJava/issues/1522 + assertEquals(0, Flowable + .empty() + .count() + .toFlowable() + .filter(new Predicate<Long>() { + @Override + public boolean test(Long v) { + return false; + } + }) + .toList() + .blockingGet().size()); + } + + @Test + public void ignoreRequest1() { + // If `takeLast` does not ignore `request` properly, StackOverflowError will be thrown. + Flowable.range(0, 100000).takeLast(100000).subscribe(new DefaultSubscriber<Integer>() { + + @Override + public void onStart() { + request(Long.MAX_VALUE); + } + + @Override + public void onComplete() { + + } + + @Override + public void onError(Throwable e) { + } + + @Override + public void onNext(Integer integer) { + request(Long.MAX_VALUE); + } + }); + } + + @Test + public void ignoreRequest2() { + // If `takeLast` does not ignore `request` properly, StackOverflowError will be thrown. + Flowable.range(0, 100000).takeLast(100000).subscribe(new DefaultSubscriber<Integer>() { + + @Override + public void onStart() { + request(1); + } + + @Override + public void onComplete() { + } + + @Override + public void onError(Throwable e) { + } + + @Override + public void onNext(Integer integer) { + request(1); + } + }); + } + + @Test + public void ignoreRequest3() { + // If `takeLast` does not ignore `request` properly, it will enter an infinite loop. + Flowable.range(0, 100000).takeLast(100000).subscribe(new DefaultSubscriber<Integer>() { + + @Override + public void onStart() { + request(1); + } + + @Override + public void onComplete() { + + } + + @Override + public void onError(Throwable e) { + } + + @Override + public void onNext(Integer integer) { + request(Long.MAX_VALUE); + } + }); + } + + @Test + public void ignoreRequest4() { + // If `takeLast` does not ignore `request` properly, StackOverflowError will be thrown. + Flowable.range(0, 100000).takeLast(100000).subscribe(new DefaultSubscriber<Integer>() { + + @Override + public void onStart() { + request(Long.MAX_VALUE); + } + + @Override + public void onComplete() { + + } + + @Override + public void onError(Throwable e) { + } + + @Override + public void onNext(Integer integer) { + request(1); + } + }); + } + + @Test + public void unsubscribeTakesEffectEarlyOnFastPath() { + final AtomicInteger count = new AtomicInteger(); + Flowable.range(0, 100000).takeLast(100000).subscribe(new DefaultSubscriber<Integer>() { + + @Override + public void onStart() { + request(Long.MAX_VALUE); + } + + @Override + public void onComplete() { + + } + + @Override + public void onError(Throwable e) { + } + + @Override + public void onNext(Integer integer) { + count.incrementAndGet(); + cancel(); + } + }); + assertEquals(1, count.get()); + } + + @Test + public void requestOverflow() { + final List<Integer> list = new ArrayList<>(); + Flowable.range(1, 100).takeLast(50).subscribe(new DefaultSubscriber<Integer>() { + + @Override + public void onStart() { + request(2); + } + + @Override + public void onComplete() { + + } + + @Override + public void onError(Throwable e) { + + } + + @Override + public void onNext(Integer t) { + list.add(t); + request(Long.MAX_VALUE - 1); + }}); + assertEquals(50, list.size()); + } + + @Test + public void dispose() { + TestHelper.checkDisposed(Flowable.range(1, 10).takeLast(5)); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeFlowable(new Function<Flowable<Object>, Flowable<Object>>() { + @Override + public Flowable<Object> apply(Flowable<Object> f) throws Exception { + return f.takeLast(5); + } + }); + } + + @Test + public void error() { + Flowable.error(new TestException()) + .takeLast(5) + .test() + .assertFailure(TestException.class); + } + + @Test + public void takeLastTake() { + Flowable.range(1, 10) + .takeLast(5) + .take(2) + .test() + .assertResult(6, 7); + } + + @Test + public void badRequest() { + TestHelper.assertBadRequestReported(Flowable.never().takeLast(2)); + } + + @Test + public void cancelThenRequest() { + Flowable.never().takeLast(2) + .subscribe(new FlowableSubscriber<Object>() { + + @Override + public void onNext(@NonNull Object t) { + } + + @Override + public void onError(Throwable t) { + } + + @Override + public void onComplete() { + } + + @Override + public void onSubscribe(@NonNull Subscription s) { + s.cancel(); + s.request(1); + } + }); + } + + @Test + public void noRequestEmpty() { + Flowable.empty() + .takeLast(2) + .test(0L) + .assertResult(); + } + + @Test + public void moreValuesRemainingThanRequested() { + Flowable.range(1, 4) + .takeLast(3) + .test(0L) + .assertEmpty() + .requestMore(2) + .assertValuesOnly(2, 3) + .requestMore(2) + .assertResult(2, 3, 4); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableTakeLastTimedTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableTakeLastTimedTest.java new file mode 100644 index 0000000000..83814dd797 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableTakeLastTimedTest.java @@ -0,0 +1,364 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import java.util.concurrent.TimeUnit; + +import org.junit.Test; +import org.mockito.InOrder; +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.processors.PublishProcessor; +import io.reactivex.rxjava3.schedulers.*; +import io.reactivex.rxjava3.subscribers.TestSubscriber; +import io.reactivex.rxjava3.testsupport.*; + +public class FlowableTakeLastTimedTest extends RxJavaTest { + + @Test(expected = IllegalArgumentException.class) + public void takeLastTimedWithNegativeCount() { + Flowable.just("one").takeLast(-1, 1, TimeUnit.SECONDS); + } + + @Test + public void takeLastTimed() { + TestScheduler scheduler = new TestScheduler(); + + PublishProcessor<Object> source = PublishProcessor.create(); + + // FIXME time unit now matters! + Flowable<Object> result = source.takeLast(1000, TimeUnit.MILLISECONDS, scheduler); + + Subscriber<Object> subscriber = TestHelper.mockSubscriber(); + + InOrder inOrder = inOrder(subscriber); + + result.subscribe(subscriber); + + source.onNext(1); // T: 0ms + scheduler.advanceTimeBy(250, TimeUnit.MILLISECONDS); + source.onNext(2); // T: 250ms + scheduler.advanceTimeBy(250, TimeUnit.MILLISECONDS); + source.onNext(3); // T: 500ms + scheduler.advanceTimeBy(250, TimeUnit.MILLISECONDS); + source.onNext(4); // T: 750ms + scheduler.advanceTimeBy(250, TimeUnit.MILLISECONDS); + source.onNext(5); // T: 1000ms + scheduler.advanceTimeBy(250, TimeUnit.MILLISECONDS); + source.onComplete(); // T: 1250ms + + inOrder.verify(subscriber, times(1)).onNext(2); + inOrder.verify(subscriber, times(1)).onNext(3); + inOrder.verify(subscriber, times(1)).onNext(4); + inOrder.verify(subscriber, times(1)).onNext(5); + inOrder.verify(subscriber, times(1)).onComplete(); + + verify(subscriber, never()).onError(any(Throwable.class)); + } + + @Test + public void takeLastTimedDelayCompletion() { + TestScheduler scheduler = new TestScheduler(); + + PublishProcessor<Object> source = PublishProcessor.create(); + + // FIXME time unit now matters + Flowable<Object> result = source.takeLast(1000, TimeUnit.MILLISECONDS, scheduler); + + Subscriber<Object> subscriber = TestHelper.mockSubscriber(); + + InOrder inOrder = inOrder(subscriber); + + result.subscribe(subscriber); + + source.onNext(1); // T: 0ms + scheduler.advanceTimeBy(250, TimeUnit.MILLISECONDS); + source.onNext(2); // T: 250ms + scheduler.advanceTimeBy(250, TimeUnit.MILLISECONDS); + source.onNext(3); // T: 500ms + scheduler.advanceTimeBy(250, TimeUnit.MILLISECONDS); + source.onNext(4); // T: 750ms + scheduler.advanceTimeBy(250, TimeUnit.MILLISECONDS); + source.onNext(5); // T: 1000ms + scheduler.advanceTimeBy(1250, TimeUnit.MILLISECONDS); + source.onComplete(); // T: 2250ms + + inOrder.verify(subscriber, times(1)).onComplete(); + + verify(subscriber, never()).onNext(any()); + verify(subscriber, never()).onError(any(Throwable.class)); + } + + @Test + public void takeLastTimedWithCapacity() { + TestScheduler scheduler = new TestScheduler(); + + PublishProcessor<Object> source = PublishProcessor.create(); + + // FIXME time unit now matters! + Flowable<Object> result = source.takeLast(2, 1000, TimeUnit.MILLISECONDS, scheduler); + + Subscriber<Object> subscriber = TestHelper.mockSubscriber(); + + InOrder inOrder = inOrder(subscriber); + + result.subscribe(subscriber); + + source.onNext(1); // T: 0ms + scheduler.advanceTimeBy(250, TimeUnit.MILLISECONDS); + source.onNext(2); // T: 250ms + scheduler.advanceTimeBy(250, TimeUnit.MILLISECONDS); + source.onNext(3); // T: 500ms + scheduler.advanceTimeBy(250, TimeUnit.MILLISECONDS); + source.onNext(4); // T: 750ms + scheduler.advanceTimeBy(250, TimeUnit.MILLISECONDS); + source.onNext(5); // T: 1000ms + scheduler.advanceTimeBy(250, TimeUnit.MILLISECONDS); + source.onComplete(); // T: 1250ms + + inOrder.verify(subscriber, times(1)).onNext(4); + inOrder.verify(subscriber, times(1)).onNext(5); + inOrder.verify(subscriber, times(1)).onComplete(); + + verify(subscriber, never()).onError(any(Throwable.class)); + } + + @Test + public void takeLastTimedThrowingSource() { + TestScheduler scheduler = new TestScheduler(); + + PublishProcessor<Object> source = PublishProcessor.create(); + + Flowable<Object> result = source.takeLast(1, TimeUnit.SECONDS, scheduler); + + Subscriber<Object> subscriber = TestHelper.mockSubscriber(); + + InOrder inOrder = inOrder(subscriber); + + result.subscribe(subscriber); + + source.onNext(1); // T: 0ms + scheduler.advanceTimeBy(250, TimeUnit.MILLISECONDS); + source.onNext(2); // T: 250ms + scheduler.advanceTimeBy(250, TimeUnit.MILLISECONDS); + source.onNext(3); // T: 500ms + scheduler.advanceTimeBy(250, TimeUnit.MILLISECONDS); + source.onNext(4); // T: 750ms + scheduler.advanceTimeBy(250, TimeUnit.MILLISECONDS); + source.onNext(5); // T: 1000ms + scheduler.advanceTimeBy(250, TimeUnit.MILLISECONDS); + source.onError(new TestException()); // T: 1250ms + + inOrder.verify(subscriber, times(1)).onError(any(TestException.class)); + + verify(subscriber, never()).onNext(any()); + verify(subscriber, never()).onComplete(); + } + + @Test + public void takeLastTimedWithZeroCapacity() { + TestScheduler scheduler = new TestScheduler(); + + PublishProcessor<Object> source = PublishProcessor.create(); + + Flowable<Object> result = source.takeLast(0, 1, TimeUnit.SECONDS, scheduler); + + Subscriber<Object> subscriber = TestHelper.mockSubscriber(); + + InOrder inOrder = inOrder(subscriber); + + result.subscribe(subscriber); + + source.onNext(1); // T: 0ms + scheduler.advanceTimeBy(250, TimeUnit.MILLISECONDS); + source.onNext(2); // T: 250ms + scheduler.advanceTimeBy(250, TimeUnit.MILLISECONDS); + source.onNext(3); // T: 500ms + scheduler.advanceTimeBy(250, TimeUnit.MILLISECONDS); + source.onNext(4); // T: 750ms + scheduler.advanceTimeBy(250, TimeUnit.MILLISECONDS); + source.onNext(5); // T: 1000ms + scheduler.advanceTimeBy(250, TimeUnit.MILLISECONDS); + source.onComplete(); // T: 1250ms + + inOrder.verify(subscriber, times(1)).onComplete(); + + verify(subscriber, never()).onNext(any()); + verify(subscriber, never()).onError(any(Throwable.class)); + } + + @Test + public void continuousDelivery() { + TestScheduler scheduler = new TestScheduler(); + + TestSubscriber<Integer> ts = new TestSubscriber<>(0L); + + PublishProcessor<Integer> pp = PublishProcessor.create(); + + pp.takeLast(1000, TimeUnit.MILLISECONDS, scheduler).subscribe(ts); + + pp.onNext(1); + scheduler.advanceTimeBy(500, TimeUnit.MILLISECONDS); + pp.onNext(2); + scheduler.advanceTimeBy(500, TimeUnit.MILLISECONDS); + pp.onNext(3); + scheduler.advanceTimeBy(500, TimeUnit.MILLISECONDS); + pp.onNext(4); + scheduler.advanceTimeBy(500, TimeUnit.MILLISECONDS); + pp.onComplete(); + scheduler.advanceTimeBy(500, TimeUnit.MILLISECONDS); + + ts.assertNoValues(); + + ts.request(1); + + ts.assertValue(3); + + scheduler.advanceTimeBy(500, TimeUnit.MILLISECONDS); + ts.request(1); + + ts.assertValues(3, 4); + ts.assertComplete(); + ts.assertNoErrors(); + + } + + @Test + public void takeLastTimeAndSize() { + Flowable.just(1, 2) + .takeLast(1, 1, TimeUnit.MINUTES) + .test() + .assertResult(2); + } + + @Test + public void takeLastTime() { + Flowable.just(1, 2) + .takeLast(1, TimeUnit.MINUTES) + .test() + .assertResult(1, 2); + } + + @Test + public void takeLastTimeDelayError() { + Flowable.just(1, 2).concatWith(Flowable.<Integer>error(new TestException())) + .takeLast(1, TimeUnit.MINUTES, true) + .test() + .assertFailure(TestException.class, 1, 2); + } + + @Test + public void takeLastTimeDelayErrorCustomScheduler() { + Flowable.just(1, 2).concatWith(Flowable.<Integer>error(new TestException())) + .takeLast(1, TimeUnit.MINUTES, Schedulers.io(), true) + .test() + .assertFailure(TestException.class, 1, 2); + } + + @Test + public void disposed() { + TestHelper.checkDisposed(PublishProcessor.create().takeLast(1, TimeUnit.MINUTES)); + } + + @Test + public void observeOn() { + Observable.range(1, 1000) + .takeLast(1, TimeUnit.DAYS) + .take(500) + .observeOn(Schedulers.single(), true, 1) + .to(TestHelper.<Integer>testConsumer()) + .awaitDone(5, TimeUnit.SECONDS) + .assertSubscribed() + .assertValueCount(500) + .assertNoErrors() + .assertComplete(); + } + + @Test + public void cancelCompleteRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final PublishProcessor<Integer> pp = PublishProcessor.create(); + + final TestSubscriber<Integer> ts = pp.takeLast(1, TimeUnit.DAYS).test(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + pp.onComplete(); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + ts.cancel(); + } + }; + + TestHelper.race(r1, r2); + } + } + + @Test + public void emptyDelayError() { + Flowable.empty() + .takeLast(1, TimeUnit.DAYS, true) + .test() + .assertResult(); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeFlowable(new Function<Flowable<Object>, Publisher<Object>>() { + @Override + public Publisher<Object> apply(Flowable<Object> f) throws Exception { + return f.takeLast(1, TimeUnit.SECONDS); + } + }); + } + + @Test + public void badRequest() { + TestHelper.assertBadRequestReported(PublishProcessor.create().takeLast(1, TimeUnit.SECONDS)); + } + + @Test + public void lastWindowIsFixedInTime() { + TimesteppingScheduler scheduler = new TimesteppingScheduler(); + scheduler.stepEnabled = false; + + PublishProcessor<Integer> pp = PublishProcessor.create(); + + TestSubscriber<Integer> ts = pp + .takeLast(2, TimeUnit.SECONDS, scheduler) + .test(); + + pp.onNext(1); + pp.onNext(2); + pp.onNext(3); + pp.onNext(4); + + scheduler.stepEnabled = true; + + pp.onComplete(); + + ts.assertResult(1, 2, 3, 4); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableTakeTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableTakeTest.java new file mode 100644 index 0000000000..50728f6206 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableTakeTest.java @@ -0,0 +1,519 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +import java.util.*; +import java.util.concurrent.*; +import java.util.concurrent.atomic.*; + +import org.junit.Test; +import org.mockito.InOrder; +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.subscriptions.BooleanSubscription; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.processors.PublishProcessor; +import io.reactivex.rxjava3.schedulers.Schedulers; +import io.reactivex.rxjava3.subscribers.TestSubscriber; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class FlowableTakeTest extends RxJavaTest { + + @Test + public void take1() { + Flowable<String> w = Flowable.fromIterable(Arrays.asList("one", "two", "three")); + Flowable<String> take = w.take(2); + + Subscriber<String> subscriber = TestHelper.mockSubscriber(); + take.subscribe(subscriber); + + verify(subscriber, times(1)).onNext("one"); + verify(subscriber, times(1)).onNext("two"); + verify(subscriber, never()).onNext("three"); + verify(subscriber, never()).onError(any(Throwable.class)); + verify(subscriber, times(1)).onComplete(); + } + + @Test + public void take2() { + Flowable<String> w = Flowable.fromIterable(Arrays.asList("one", "two", "three")); + Flowable<String> take = w.take(1); + + Subscriber<String> subscriber = TestHelper.mockSubscriber(); + take.subscribe(subscriber); + + verify(subscriber, times(1)).onNext("one"); + verify(subscriber, never()).onNext("two"); + verify(subscriber, never()).onNext("three"); + verify(subscriber, never()).onError(any(Throwable.class)); + verify(subscriber, times(1)).onComplete(); + } + + @Test(expected = IllegalArgumentException.class) + public void takeWithError() { + Flowable.fromIterable(Arrays.asList(1, 2, 3)).take(1) + .map(new Function<Integer, Integer>() { + @Override + public Integer apply(Integer t1) { + throw new IllegalArgumentException("some error"); + } + }).blockingSingle(); + } + + @Test + public void takeWithErrorHappeningInOnNext() { + Flowable<Integer> w = Flowable.fromIterable(Arrays.asList(1, 2, 3)) + .take(2).map(new Function<Integer, Integer>() { + @Override + public Integer apply(Integer t1) { + throw new IllegalArgumentException("some error"); + } + }); + + Subscriber<Integer> subscriber = TestHelper.mockSubscriber(); + w.subscribe(subscriber); + + InOrder inOrder = inOrder(subscriber); + inOrder.verify(subscriber, times(1)).onError(any(IllegalArgumentException.class)); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void takeWithErrorHappeningInTheLastOnNext() { + Flowable<Integer> w = Flowable.fromIterable(Arrays.asList(1, 2, 3)).take(1).map(new Function<Integer, Integer>() { + @Override + public Integer apply(Integer t1) { + throw new IllegalArgumentException("some error"); + } + }); + + Subscriber<Integer> subscriber = TestHelper.mockSubscriber(); + w.subscribe(subscriber); + + InOrder inOrder = inOrder(subscriber); + inOrder.verify(subscriber, times(1)).onError(any(IllegalArgumentException.class)); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void takeDoesntLeakErrors() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + Flowable<String> source = Flowable.unsafeCreate(new Publisher<String>() { + @Override + public void subscribe(Subscriber<? super String> subscriber) { + subscriber.onSubscribe(new BooleanSubscription()); + subscriber.onNext("one"); + subscriber.onError(new Throwable("test failed")); + } + }); + + Subscriber<String> subscriber = TestHelper.mockSubscriber(); + + source.take(1).subscribe(subscriber); + + verify(subscriber).onSubscribe((Subscription)notNull()); + verify(subscriber, times(1)).onNext("one"); + // even though onError is called we take(1) so shouldn't see it + verify(subscriber, never()).onError(any(Throwable.class)); + verify(subscriber, times(1)).onComplete(); + verifyNoMoreInteractions(subscriber); + + TestHelper.assertUndeliverable(errors, 0, Throwable.class, "test failed"); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void takeEmitsErrors() { + Flowable.error(new TestException()) + .take(1) + .test() + .assertNoValues() + .assertError(TestException.class); + } + + @Test + public void takeRequestOverflow() { + TestSubscriber<Integer> ts = Flowable.just(1, 2, 3) + .take(3) + .test(0); + ts.requestMore(1) + .assertValues(1) + .assertNotComplete() + .requestMore(Long.MAX_VALUE) + .assertValues(1, 2, 3); + } + + @Test + public void unsubscribeAfterTake() { + TestFlowableFunc f = new TestFlowableFunc("one", "two", "three"); + Flowable<String> w = Flowable.unsafeCreate(f); + + Subscriber<String> subscriber = TestHelper.mockSubscriber(); + + Flowable<String> take = w.take(1); + take.subscribe(subscriber); + + // wait for the Flowable to complete + try { + f.t.join(); + } catch (Throwable e) { + e.printStackTrace(); + fail(e.getMessage()); + } + + System.out.println("TestFlowable thread finished"); + verify(subscriber).onSubscribe((Subscription)notNull()); + verify(subscriber, times(1)).onNext("one"); + verify(subscriber, never()).onNext("two"); + verify(subscriber, never()).onNext("three"); + verify(subscriber, times(1)).onComplete(); + // FIXME no longer assertable +// verify(s, times(1)).unsubscribe(); + verifyNoMoreInteractions(subscriber); + } + + @Test + public void unsubscribeFromSynchronousInfiniteFlowable() { + final AtomicLong count = new AtomicLong(); + INFINITE_OBSERVABLE.take(10).subscribe(new Consumer<Long>() { + + @Override + public void accept(Long l) { + count.set(l); + } + + }); + assertEquals(10, count.get()); + } + + @Test + public void multiTake() { + final AtomicInteger count = new AtomicInteger(); + Flowable.unsafeCreate(new Publisher<Integer>() { + + @Override + public void subscribe(Subscriber<? super Integer> s) { + BooleanSubscription bs = new BooleanSubscription(); + s.onSubscribe(bs); + for (int i = 0; !bs.isCancelled(); i++) { + System.out.println("Emit: " + i); + count.incrementAndGet(); + s.onNext(i); + } + } + + }).take(100).take(1).blockingForEach(new Consumer<Integer>() { + + @Override + public void accept(Integer t1) { + System.out.println("Receive: " + t1); + + } + + }); + + assertEquals(1, count.get()); + } + + static class TestFlowableFunc implements Publisher<String> { + + final String[] values; + Thread t; + + TestFlowableFunc(String... values) { + this.values = values; + } + + @Override + public void subscribe(final Subscriber<? super String> subscriber) { + subscriber.onSubscribe(new BooleanSubscription()); + System.out.println("TestFlowable subscribed to ..."); + t = new Thread(new Runnable() { + + @Override + public void run() { + try { + System.out.println("running TestFlowable thread"); + for (String s : values) { + System.out.println("TestFlowable onNext: " + s); + subscriber.onNext(s); + } + subscriber.onComplete(); + } catch (Throwable e) { + throw new RuntimeException(e); + } + } + + }); + System.out.println("starting TestFlowable thread"); + t.start(); + System.out.println("done starting TestFlowable thread"); + } + } + + private static Flowable<Long> INFINITE_OBSERVABLE = Flowable.unsafeCreate(new Publisher<Long>() { + + @Override + public void subscribe(Subscriber<? super Long> op) { + BooleanSubscription bs = new BooleanSubscription(); + op.onSubscribe(bs); + long l = 1; + while (!bs.isCancelled()) { + op.onNext(l++); + } + op.onComplete(); + } + + }); + + @Test + public void takeObserveOn() { + Subscriber<Object> subscriber = TestHelper.mockSubscriber(); + TestSubscriber<Object> ts = new TestSubscriber<>(subscriber); + + INFINITE_OBSERVABLE.onBackpressureDrop() + .observeOn(Schedulers.newThread()).take(1).subscribe(ts); + ts.awaitDone(5, TimeUnit.SECONDS); + ts.assertNoErrors(); + + verify(subscriber).onNext(1L); + verify(subscriber, never()).onNext(2L); + verify(subscriber).onComplete(); + verify(subscriber, never()).onError(any(Throwable.class)); + } + + @Test + public void producerRequestThroughTake() { + TestSubscriber<Integer> ts = new TestSubscriber<>(3); + final AtomicLong requested = new AtomicLong(); + Flowable.unsafeCreate(new Publisher<Integer>() { + + @Override + public void subscribe(Subscriber<? super Integer> s) { + s.onSubscribe(new Subscription() { + + @Override + public void request(long n) { + requested.set(n); + } + + @Override + public void cancel() { + + } + }); + } + + }).take(3).subscribe(ts); + assertEquals(3, requested.get()); + } + + @Test + public void producerRequestThroughTakeIsModified() { + TestSubscriber<Integer> ts = new TestSubscriber<>(3); + final AtomicLong requested = new AtomicLong(); + Flowable.unsafeCreate(new Publisher<Integer>() { + + @Override + public void subscribe(Subscriber<? super Integer> s) { + s.onSubscribe(new Subscription() { + + @Override + public void request(long n) { + requested.set(n); + } + + @Override + public void cancel() { + + } + }); + } + + }).take(1).subscribe(ts); + //FIXME take triggers fast path if downstream requests more than the limit + assertEquals(1, requested.get()); + } + + @Test + public void interrupt() throws InterruptedException { + final AtomicReference<Object> exception = new AtomicReference<>(); + final CountDownLatch latch = new CountDownLatch(1); + Flowable.just(1).subscribeOn(Schedulers.computation()).take(1) + .subscribe(new Consumer<Integer>() { + + @Override + public void accept(Integer t1) { + try { + Thread.sleep(100); + } catch (Exception e) { + exception.set(e); + e.printStackTrace(); + } finally { + latch.countDown(); + } + } + + }); + + latch.await(); + assertNull(exception.get()); + } + + @Test + public void doesntRequestMoreThanNeededFromUpstream() throws InterruptedException { + final AtomicLong requests = new AtomicLong(); + TestSubscriber<Long> ts = new TestSubscriber<>(0L); + Flowable.interval(100, TimeUnit.MILLISECONDS) + // + .doOnRequest(new LongConsumer() { + @Override + public void accept(long n) { + System.out.println(n); + requests.addAndGet(n); + }}) + // + .take(2) + // + .subscribe(ts); + Thread.sleep(50); + ts.request(1); + ts.request(1); + ts.request(1); + ts.awaitDone(5, TimeUnit.SECONDS); + ts.assertComplete(); + ts.assertNoErrors(); + assertEquals(2, requests.get()); + } + + @Test + public void takeFinalValueThrows() { + Flowable<Integer> source = Flowable.just(1).take(1); + + TestSubscriber<Integer> ts = new TestSubscriber<Integer>() { + @Override + public void onNext(Integer t) { + throw new TestException(); + } + }; + + source.safeSubscribe(ts); + + ts.assertNoValues(); + ts.assertError(TestException.class); + ts.assertNotComplete(); + } + + @Test + public void reentrantTake() { + final PublishProcessor<Integer> source = PublishProcessor.create(); + + TestSubscriber<Integer> ts = new TestSubscriber<>(); + + source.take(1).doOnNext(new Consumer<Integer>() { + @Override + public void accept(Integer v) { + source.onNext(2); + } + }).subscribe(ts); + + source.onNext(1); + + ts.assertValue(1); + ts.assertNoErrors(); + ts.assertComplete(); + } + + @Test + public void takeNegative() { + try { + Flowable.just(1).take(-99); + fail("Should have thrown"); + } catch (IllegalArgumentException ex) { + assertEquals("count >= 0 required but it was -99", ex.getMessage()); + } + } + + @Test + public void takeZero() { + Flowable.just(1) + .take(0) + .test() + .assertResult(); + } + + @Test + public void dispose() { + TestHelper.checkDisposed(PublishProcessor.create().take(2)); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeFlowable(new Function<Flowable<Object>, Flowable<Object>>() { + @Override + public Flowable<Object> apply(Flowable<Object> f) throws Exception { + return f.take(2); + } + }); + } + + @Test + public void badRequest() { + TestHelper.assertBadRequestReported(Flowable.never().take(1)); + } + + @Test + public void requestRace() { + for (int i = 0; i < TestHelper.RACE_LONG_LOOPS; i++) { + + final TestSubscriber<Integer> ts = Flowable.range(1, 2).take(2).test(0L); + + Runnable r1 = new Runnable() { + @Override + public void run() { + ts.request(1); + } + }; + + TestHelper.race(r1, r1); + + ts.assertResult(1, 2); + } + } + + @Test + public void errorAfterLimitReached() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + Flowable.error(new TestException()) + .take(0) + .test() + .assertResult(); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableTakeTest2.java b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableTakeTest2.java new file mode 100644 index 0000000000..da2aa5657e --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableTakeTest2.java @@ -0,0 +1,227 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import static org.junit.Assert.*; + +import java.util.*; + +import org.junit.Test; +import org.reactivestreams.Subscriber; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.subscriptions.BooleanSubscription; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.processors.PublishProcessor; +import io.reactivex.rxjava3.subscribers.TestSubscriber; +import io.reactivex.rxjava3.testsupport.TestHelper; + +// moved tests from FlowableLimitTest to here (limit removed as operator) +public class FlowableTakeTest2 extends RxJavaTest implements LongConsumer, Action { + + final List<Long> requests = new ArrayList<>(); + + static final Long CANCELLED = -100L; + + @Override + public void accept(long t) throws Exception { + requests.add(t); + } + + @Override + public void run() throws Exception { + requests.add(CANCELLED); + } + + @Test + public void shorterSequence() { + Flowable.range(1, 5) + .doOnRequest(this) + .take(6) + .test() + .assertResult(1, 2, 3, 4, 5); + + assertEquals(6, requests.get(0).intValue()); + } + + @Test + public void exactSequence() { + Flowable.range(1, 5) + .doOnRequest(this) + .doOnCancel(this) + .take(5) + .test() + .assertResult(1, 2, 3, 4, 5); + + assertEquals(2, requests.size()); + assertEquals(5, requests.get(0).intValue()); + assertEquals(CANCELLED, requests.get(1)); + } + + @Test + public void longerSequence() { + Flowable.range(1, 6) + .doOnRequest(this) + .take(5) + .test() + .assertResult(1, 2, 3, 4, 5); + + assertEquals(5, requests.get(0).intValue()); + } + + @Test + public void error() { + Flowable.error(new TestException()) + .take(5) + .test() + .assertFailure(TestException.class); + } + + @Test + public void takeZero() { + Flowable.range(1, 5) + .doOnCancel(this) + .doOnRequest(this) + .take(0) + .test() + .assertResult(); + + assertEquals(1, requests.size()); + assertEquals(CANCELLED, requests.get(0)); + } + + @Test + public void takeStep() { + TestSubscriber<Integer> ts = Flowable.range(1, 6) + .doOnRequest(this) + .take(5) + .test(0L); + + assertEquals(0, requests.size()); + + ts.request(1); + ts.assertValue(1); + + ts.request(2); + ts.assertValues(1, 2, 3); + + ts.request(3); + ts.assertResult(1, 2, 3, 4, 5); + + assertEquals(Arrays.asList(1L, 2L, 2L), requests); + } + + @Test + public void takeThenTake() { + Flowable.range(1, 5) + .doOnCancel(this) + .doOnRequest(this) + .take(6) + .take(5) + .test() + .assertResult(1, 2, 3, 4, 5); + + assertEquals(Arrays.asList(5L, CANCELLED), requests); + } + + @Test + public void noOverrequest() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + + TestSubscriber<Integer> ts = pp + .doOnRequest(this) + .take(5) + .test(0L); + + ts.request(5); + ts.request(10); + + assertTrue(pp.offer(1)); + pp.onComplete(); + + ts.assertResult(1); + } + + @Test + public void cancelIgnored() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + new Flowable<Integer>() { + @Override + protected void subscribeActual(Subscriber<? super Integer> s) { + BooleanSubscription bs = new BooleanSubscription(); + s.onSubscribe(bs); + + assertTrue(bs.isCancelled()); + + s.onNext(1); + s.onComplete(); + s.onError(new TestException()); + + s.onSubscribe(null); + } + } + .take(0) + .test() + .assertResult(); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + TestHelper.assertError(errors, 1, NullPointerException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void badRequest() { + TestHelper.assertBadRequestReported(Flowable.range(1, 5).take(3)); + } + + @Test + public void requestRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final TestSubscriber<Integer> ts = Flowable.range(1, 10) + .take(5) + .test(0L); + + Runnable r = new Runnable() { + @Override + public void run() { + ts.request(3); + } + }; + + TestHelper.race(r, r); + + ts.assertResult(1, 2, 3, 4, 5); + } + } + + @Test + public void errorAfterLimitReached() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + Flowable.error(new TestException()) + .take(0) + .test() + .assertResult(); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableTakeTimedTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableTakeTimedTest.java new file mode 100644 index 0000000000..776236a7cd --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableTakeTimedTest.java @@ -0,0 +1,135 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import java.util.concurrent.TimeUnit; + +import org.junit.Test; +import org.mockito.InOrder; +import org.reactivestreams.Subscriber; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.processors.PublishProcessor; +import io.reactivex.rxjava3.schedulers.TestScheduler; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class FlowableTakeTimedTest extends RxJavaTest { + + @Test + public void takeTimed() { + TestScheduler scheduler = new TestScheduler(); + + PublishProcessor<Integer> source = PublishProcessor.create(); + + Flowable<Integer> result = source.take(1, TimeUnit.SECONDS, scheduler); + + Subscriber<Object> subscriber = TestHelper.mockSubscriber(); + + result.subscribe(subscriber); + + source.onNext(1); + source.onNext(2); + source.onNext(3); + + scheduler.advanceTimeBy(1, TimeUnit.SECONDS); + + source.onNext(4); + + InOrder inOrder = inOrder(subscriber); + inOrder.verify(subscriber).onNext(1); + inOrder.verify(subscriber).onNext(2); + inOrder.verify(subscriber).onNext(3); + inOrder.verify(subscriber).onComplete(); + inOrder.verifyNoMoreInteractions(); + + verify(subscriber, never()).onNext(4); + verify(subscriber, never()).onError(any(Throwable.class)); + } + + @Test + public void takeTimedErrorBeforeTime() { + TestScheduler scheduler = new TestScheduler(); + + PublishProcessor<Integer> source = PublishProcessor.create(); + + Flowable<Integer> result = source.take(1, TimeUnit.SECONDS, scheduler); + + Subscriber<Object> subscriber = TestHelper.mockSubscriber(); + + result.subscribe(subscriber); + + source.onNext(1); + source.onNext(2); + source.onNext(3); + source.onError(new TestException()); + + scheduler.advanceTimeBy(1, TimeUnit.SECONDS); + + source.onNext(4); + + InOrder inOrder = inOrder(subscriber); + inOrder.verify(subscriber).onNext(1); + inOrder.verify(subscriber).onNext(2); + inOrder.verify(subscriber).onNext(3); + inOrder.verify(subscriber).onError(any(TestException.class)); + inOrder.verifyNoMoreInteractions(); + + verify(subscriber, never()).onComplete(); + verify(subscriber, never()).onNext(4); + } + + @Test + public void takeTimedErrorAfterTime() { + TestScheduler scheduler = new TestScheduler(); + + PublishProcessor<Integer> source = PublishProcessor.create(); + + Flowable<Integer> result = source.take(1, TimeUnit.SECONDS, scheduler); + + Subscriber<Object> subscriber = TestHelper.mockSubscriber(); + + result.subscribe(subscriber); + + source.onNext(1); + source.onNext(2); + source.onNext(3); + + scheduler.advanceTimeBy(1, TimeUnit.SECONDS); + + source.onNext(4); + source.onError(new TestException()); + + InOrder inOrder = inOrder(subscriber); + inOrder.verify(subscriber).onNext(1); + inOrder.verify(subscriber).onNext(2); + inOrder.verify(subscriber).onNext(3); + inOrder.verify(subscriber).onComplete(); + inOrder.verifyNoMoreInteractions(); + + verify(subscriber, never()).onNext(4); + verify(subscriber, never()).onError(any(TestException.class)); + } + + @Test + public void timedDefaultScheduler() { + Flowable.range(1, 5).take(1, TimeUnit.MINUTES) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(1, 2, 3, 4, 5); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableTakeUntilPredicateTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableTakeUntilPredicateTest.java new file mode 100644 index 0000000000..f88b7e27ae --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableTakeUntilPredicateTest.java @@ -0,0 +1,216 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import java.util.List; + +import org.junit.Test; +import org.reactivestreams.Subscriber; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.internal.subscriptions.BooleanSubscription; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.processors.PublishProcessor; +import io.reactivex.rxjava3.subscribers.TestSubscriber; +import io.reactivex.rxjava3.testsupport.*; + +public class FlowableTakeUntilPredicateTest extends RxJavaTest { + @Test + public void takeEmpty() { + Subscriber<Object> subscriber = TestHelper.mockSubscriber(); + + Flowable.empty().takeUntil(new Predicate<Object>() { + @Override + public boolean test(Object v) { + return true; + } + }).subscribe(subscriber); + + verify(subscriber, never()).onNext(any()); + verify(subscriber, never()).onError(any(Throwable.class)); + verify(subscriber).onComplete(); + } + + @Test + public void takeAll() { + Subscriber<Object> subscriber = TestHelper.mockSubscriber(); + + Flowable.just(1, 2).takeUntil(new Predicate<Integer>() { + @Override + public boolean test(Integer v) { + return false; + } + }).subscribe(subscriber); + + verify(subscriber).onNext(1); + verify(subscriber).onNext(2); + verify(subscriber, never()).onError(any(Throwable.class)); + verify(subscriber).onComplete(); + } + + @Test + public void takeFirst() { + Subscriber<Object> subscriber = TestHelper.mockSubscriber(); + + Flowable.just(1, 2).takeUntil(new Predicate<Integer>() { + @Override + public boolean test(Integer v) { + return true; + } + }).subscribe(subscriber); + + verify(subscriber).onNext(1); + verify(subscriber, never()).onNext(2); + verify(subscriber, never()).onError(any(Throwable.class)); + verify(subscriber).onComplete(); + } + + @Test + public void takeSome() { + Subscriber<Object> subscriber = TestHelper.mockSubscriber(); + + Flowable.just(1, 2, 3).takeUntil(new Predicate<Integer>() { + @Override + public boolean test(Integer t1) { + return t1 == 2; + } + }) + .subscribe(subscriber); + + verify(subscriber).onNext(1); + verify(subscriber).onNext(2); + verify(subscriber, never()).onNext(3); + verify(subscriber, never()).onError(any(Throwable.class)); + verify(subscriber).onComplete(); + } + + @Test + public void functionThrows() { + Subscriber<Object> subscriber = TestHelper.mockSubscriber(); + + Predicate<Integer> predicate = new Predicate<Integer>() { + @Override + public boolean test(Integer t1) { + throw new TestException("Forced failure"); + } + }; + Flowable.just(1, 2, 3).takeUntil(predicate).subscribe(subscriber); + + verify(subscriber).onNext(1); + verify(subscriber, never()).onNext(2); + verify(subscriber, never()).onNext(3); + verify(subscriber).onError(any(TestException.class)); + verify(subscriber, never()).onComplete(); + } + + @Test + public void sourceThrows() { + Subscriber<Object> subscriber = TestHelper.mockSubscriber(); + + Flowable.just(1) + .concatWith(Flowable.<Integer>error(new TestException())) + .concatWith(Flowable.just(2)) + .takeUntil(new Predicate<Integer>() { + @Override + public boolean test(Integer v) { + return false; + } + }).subscribe(subscriber); + + verify(subscriber).onNext(1); + verify(subscriber, never()).onNext(2); + verify(subscriber).onError(any(TestException.class)); + verify(subscriber, never()).onComplete(); + } + + @Test + public void backpressure() { + TestSubscriber<Integer> ts = new TestSubscriber<>(5L); + + Flowable.range(1, 1000).takeUntil(new Predicate<Integer>() { + @Override + public boolean test(Integer v) { + return false; + } + }).subscribe(ts); + + ts.assertNoErrors(); + ts.assertValues(1, 2, 3, 4, 5); + ts.assertNotComplete(); + } + + @Test + public void errorIncludesLastValueAsCause() { + TestSubscriberEx<String> ts = new TestSubscriberEx<>(); + final TestException e = new TestException("Forced failure"); + Predicate<String> predicate = new Predicate<String>() { + @Override + public boolean test(String t) { + throw e; + } + }; + Flowable.just("abc").takeUntil(predicate).subscribe(ts); + + ts.assertTerminated(); + ts.assertNotComplete(); + ts.assertError(TestException.class); + // FIXME last cause value is not saved +// assertTrue(ts.errors().get(0).getCause().getMessage().contains("abc")); + } + + @Test + public void dispose() { + TestHelper.checkDisposed(PublishProcessor.create().takeUntil(Functions.alwaysFalse())); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeFlowable(new Function<Flowable<Object>, Flowable<Object>>() { + @Override + public Flowable<Object> apply(Flowable<Object> f) throws Exception { + return f.takeUntil(Functions.alwaysFalse()); + } + }); + } + + @Test + public void badSource() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + new Flowable<Integer>() { + @Override + protected void subscribeActual(Subscriber<? super Integer> subscriber) { + subscriber.onSubscribe(new BooleanSubscription()); + subscriber.onComplete(); + subscriber.onNext(1); + subscriber.onError(new TestException()); + subscriber.onComplete(); + } + } + .takeUntil(Functions.alwaysFalse()) + .test() + .assertResult(); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableTakeUntilTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableTakeUntilTest.java new file mode 100644 index 0000000000..a1f7fe9d42 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableTakeUntilTest.java @@ -0,0 +1,429 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +import org.junit.Test; +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.processors.PublishProcessor; +import io.reactivex.rxjava3.subscribers.TestSubscriber; +import io.reactivex.rxjava3.testsupport.*; + +public class FlowableTakeUntilTest extends RxJavaTest { + + @Test + public void takeUntil() { + Subscription sSource = mock(Subscription.class); + Subscription sOther = mock(Subscription.class); + TestObservable source = new TestObservable(sSource); + TestObservable other = new TestObservable(sOther); + + Subscriber<String> result = TestHelper.mockSubscriber(); + Flowable<String> stringObservable = Flowable.unsafeCreate(source) + .takeUntil(Flowable.unsafeCreate(other)); + stringObservable.subscribe(result); + source.sendOnNext("one"); + source.sendOnNext("two"); + other.sendOnNext("three"); + source.sendOnNext("four"); + source.sendOnCompleted(); + other.sendOnCompleted(); + + verify(result, times(1)).onNext("one"); + verify(result, times(1)).onNext("two"); + verify(result, times(0)).onNext("three"); + verify(result, times(0)).onNext("four"); + verify(sSource, times(1)).cancel(); + verify(sOther, times(1)).cancel(); + + } + + @Test + public void takeUntilSourceCompleted() { + Subscription sSource = mock(Subscription.class); + Subscription sOther = mock(Subscription.class); + TestObservable source = new TestObservable(sSource); + TestObservable other = new TestObservable(sOther); + + Subscriber<String> result = TestHelper.mockSubscriber(); + Flowable<String> stringObservable = Flowable.unsafeCreate(source).takeUntil(Flowable.unsafeCreate(other)); + stringObservable.subscribe(result); + source.sendOnNext("one"); + source.sendOnNext("two"); + source.sendOnCompleted(); + + verify(result, times(1)).onNext("one"); + verify(result, times(1)).onNext("two"); + verify(sSource, never()).cancel(); + verify(sOther, times(1)).cancel(); + + } + + @Test + public void takeUntilSourceError() { + Subscription sSource = mock(Subscription.class); + Subscription sOther = mock(Subscription.class); + TestObservable source = new TestObservable(sSource); + TestObservable other = new TestObservable(sOther); + Throwable error = new Throwable(); + + Subscriber<String> result = TestHelper.mockSubscriber(); + Flowable<String> stringObservable = Flowable.unsafeCreate(source).takeUntil(Flowable.unsafeCreate(other)); + stringObservable.subscribe(result); + source.sendOnNext("one"); + source.sendOnNext("two"); + source.sendOnError(error); + source.sendOnNext("three"); + + verify(result, times(1)).onNext("one"); + verify(result, times(1)).onNext("two"); + verify(result, times(0)).onNext("three"); + verify(result, times(1)).onError(error); + verify(sSource, never()).cancel(); + verify(sOther, times(1)).cancel(); + + } + + @Test + public void takeUntilOtherError() { + Subscription sSource = mock(Subscription.class); + Subscription sOther = mock(Subscription.class); + TestObservable source = new TestObservable(sSource); + TestObservable other = new TestObservable(sOther); + Throwable error = new Throwable(); + + Subscriber<String> result = TestHelper.mockSubscriber(); + Flowable<String> stringObservable = Flowable.unsafeCreate(source).takeUntil(Flowable.unsafeCreate(other)); + stringObservable.subscribe(result); + source.sendOnNext("one"); + source.sendOnNext("two"); + other.sendOnError(error); + source.sendOnNext("three"); + + verify(result, times(1)).onNext("one"); + verify(result, times(1)).onNext("two"); + verify(result, times(0)).onNext("three"); + verify(result, times(1)).onError(error); + verify(result, times(0)).onComplete(); + verify(sSource, times(1)).cancel(); + verify(sOther, never()).cancel(); + + } + + /** + * If the 'other' onCompletes then we unsubscribe from the source and onComplete. + */ + @Test + public void takeUntilOtherCompleted() { + Subscription sSource = mock(Subscription.class); + Subscription sOther = mock(Subscription.class); + TestObservable source = new TestObservable(sSource); + TestObservable other = new TestObservable(sOther); + + Subscriber<String> result = TestHelper.mockSubscriber(); + Flowable<String> stringObservable = Flowable.unsafeCreate(source).takeUntil(Flowable.unsafeCreate(other)); + stringObservable.subscribe(result); + source.sendOnNext("one"); + source.sendOnNext("two"); + other.sendOnCompleted(); + source.sendOnNext("three"); + + verify(result, times(1)).onNext("one"); + verify(result, times(1)).onNext("two"); + verify(result, times(0)).onNext("three"); + verify(result, times(1)).onComplete(); + verify(sSource, times(1)).cancel(); + verify(sOther, never()).cancel(); // unsubscribed since SafeSubscriber unsubscribes after onComplete + + } + + private static class TestObservable implements Publisher<String> { + + Subscriber<? super String> subscriber; + Subscription upstream; + + TestObservable(Subscription s) { + this.upstream = s; + } + + /* used to simulate subscription */ + public void sendOnCompleted() { + subscriber.onComplete(); + } + + /* used to simulate subscription */ + public void sendOnNext(String value) { + subscriber.onNext(value); + } + + /* used to simulate subscription */ + public void sendOnError(Throwable e) { + subscriber.onError(e); + } + + @Override + public void subscribe(Subscriber<? super String> subscriber) { + this.subscriber = subscriber; + subscriber.onSubscribe(upstream); + } + } + + @Test + public void untilFires() { + PublishProcessor<Integer> source = PublishProcessor.create(); + PublishProcessor<Integer> until = PublishProcessor.create(); + + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(); + + source.takeUntil(until).subscribe(ts); + + assertTrue(source.hasSubscribers()); + assertTrue(until.hasSubscribers()); + + source.onNext(1); + + ts.assertValue(1); + until.onNext(1); + + ts.assertValue(1); + ts.assertNoErrors(); + ts.assertTerminated(); + + assertFalse("Source still has observers", source.hasSubscribers()); + assertFalse("Until still has observers", until.hasSubscribers()); + assertFalse("TestSubscriber is unsubscribed", ts.isCancelled()); + } + + @Test + public void mainCompletes() { + PublishProcessor<Integer> source = PublishProcessor.create(); + PublishProcessor<Integer> until = PublishProcessor.create(); + + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(); + + source.takeUntil(until).subscribe(ts); + + assertTrue(source.hasSubscribers()); + assertTrue(until.hasSubscribers()); + + source.onNext(1); + source.onComplete(); + + ts.assertValue(1); + ts.assertNoErrors(); + ts.assertTerminated(); + + assertFalse("Source still has observers", source.hasSubscribers()); + assertFalse("Until still has observers", until.hasSubscribers()); + assertFalse("TestSubscriber is unsubscribed", ts.isCancelled()); + } + + @Test + public void downstreamUnsubscribes() { + PublishProcessor<Integer> source = PublishProcessor.create(); + PublishProcessor<Integer> until = PublishProcessor.create(); + + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(); + + source.takeUntil(until).take(1).subscribe(ts); + + assertTrue(source.hasSubscribers()); + assertTrue(until.hasSubscribers()); + + source.onNext(1); + + ts.assertValue(1); + ts.assertNoErrors(); + ts.assertTerminated(); + + assertFalse("Source still has observers", source.hasSubscribers()); + assertFalse("Until still has observers", until.hasSubscribers()); + assertFalse("TestSubscriber is unsubscribed", ts.isCancelled()); + } + + @Test + public void backpressure() { + PublishProcessor<Integer> until = PublishProcessor.create(); + + TestSubscriber<Integer> ts = new TestSubscriber<>(0L); + + Flowable.range(1, 10).takeUntil(until).subscribe(ts); + + assertTrue(until.hasSubscribers()); + + ts.request(1); + + ts.assertValue(1); + ts.assertNoErrors(); + ts.assertNotComplete(); + + until.onNext(5); + + ts.assertComplete(); + ts.assertNoErrors(); + + assertFalse("Until still has observers", until.hasSubscribers()); + assertFalse("TestSubscriber is unsubscribed", ts.isCancelled()); + } + + @Test + public void dispose() { + TestHelper.checkDisposed(PublishProcessor.create().takeUntil(Flowable.never())); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeFlowable(new Function<Flowable<Integer>, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Flowable<Integer> c) throws Exception { + return c.takeUntil(Flowable.never()); + } + }); + } + + @Test + public void untilPublisherMainSuccess() { + PublishProcessor<Integer> main = PublishProcessor.create(); + PublishProcessor<Integer> other = PublishProcessor.create(); + + TestSubscriber<Integer> ts = main.takeUntil(other).test(); + + assertTrue("Main no subscribers?", main.hasSubscribers()); + assertTrue("Other no subscribers?", other.hasSubscribers()); + + main.onNext(1); + main.onNext(2); + main.onComplete(); + + assertFalse("Main has subscribers?", main.hasSubscribers()); + assertFalse("Other has subscribers?", other.hasSubscribers()); + + ts.assertResult(1, 2); + } + + @Test + public void untilPublisherMainComplete() { + PublishProcessor<Integer> main = PublishProcessor.create(); + PublishProcessor<Integer> other = PublishProcessor.create(); + + TestSubscriber<Integer> ts = main.takeUntil(other).test(); + + assertTrue("Main no subscribers?", main.hasSubscribers()); + assertTrue("Other no subscribers?", other.hasSubscribers()); + + main.onComplete(); + + assertFalse("Main has subscribers?", main.hasSubscribers()); + assertFalse("Other has subscribers?", other.hasSubscribers()); + + ts.assertResult(); + } + + @Test + public void untilPublisherMainError() { + PublishProcessor<Integer> main = PublishProcessor.create(); + PublishProcessor<Integer> other = PublishProcessor.create(); + + TestSubscriber<Integer> ts = main.takeUntil(other).test(); + + assertTrue("Main no subscribers?", main.hasSubscribers()); + assertTrue("Other no subscribers?", other.hasSubscribers()); + + main.onError(new TestException()); + + assertFalse("Main has subscribers?", main.hasSubscribers()); + assertFalse("Other has subscribers?", other.hasSubscribers()); + + ts.assertFailure(TestException.class); + } + + @Test + public void untilPublisherOtherOnNext() { + PublishProcessor<Integer> main = PublishProcessor.create(); + PublishProcessor<Integer> other = PublishProcessor.create(); + + TestSubscriber<Integer> ts = main.takeUntil(other).test(); + + assertTrue("Main no subscribers?", main.hasSubscribers()); + assertTrue("Other no subscribers?", other.hasSubscribers()); + + other.onNext(1); + + assertFalse("Main has subscribers?", main.hasSubscribers()); + assertFalse("Other has subscribers?", other.hasSubscribers()); + + ts.assertResult(); + } + + @Test + public void untilPublisherOtherOnComplete() { + PublishProcessor<Integer> main = PublishProcessor.create(); + PublishProcessor<Integer> other = PublishProcessor.create(); + + TestSubscriber<Integer> ts = main.takeUntil(other).test(); + + assertTrue("Main no subscribers?", main.hasSubscribers()); + assertTrue("Other no subscribers?", other.hasSubscribers()); + + other.onComplete(); + + assertFalse("Main has subscribers?", main.hasSubscribers()); + assertFalse("Other has subscribers?", other.hasSubscribers()); + + ts.assertResult(); + } + + @Test + public void untilPublisherOtherError() { + PublishProcessor<Integer> main = PublishProcessor.create(); + PublishProcessor<Integer> other = PublishProcessor.create(); + + TestSubscriber<Integer> ts = main.takeUntil(other).test(); + + assertTrue("Main no subscribers?", main.hasSubscribers()); + assertTrue("Other no subscribers?", other.hasSubscribers()); + + other.onError(new TestException()); + + assertFalse("Main has subscribers?", main.hasSubscribers()); + assertFalse("Other has subscribers?", other.hasSubscribers()); + + ts.assertFailure(TestException.class); + } + + @Test + public void untilPublisherDispose() { + PublishProcessor<Integer> main = PublishProcessor.create(); + PublishProcessor<Integer> other = PublishProcessor.create(); + + TestSubscriber<Integer> ts = main.takeUntil(other).test(); + + assertTrue("Main no subscribers?", main.hasSubscribers()); + assertTrue("Other no subscribers?", other.hasSubscribers()); + + ts.cancel(); + + assertFalse("Main has subscribers?", main.hasSubscribers()); + assertFalse("Other has subscribers?", other.hasSubscribers()); + + ts.assertEmpty(); + } + +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableTakeWhileTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableTakeWhileTest.java new file mode 100644 index 0000000000..805420f094 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableTakeWhileTest.java @@ -0,0 +1,317 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import static org.junit.Assert.fail; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import java.util.List; + +import org.junit.*; +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.internal.subscriptions.BooleanSubscription; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.processors.*; +import io.reactivex.rxjava3.subscribers.TestSubscriber; +import io.reactivex.rxjava3.testsupport.*; + +public class FlowableTakeWhileTest extends RxJavaTest { + + @Test + public void takeWhile1() { + Flowable<Integer> w = Flowable.just(1, 2, 3); + Flowable<Integer> take = w.takeWhile(new Predicate<Integer>() { + @Override + public boolean test(Integer input) { + return input < 3; + } + }); + + Subscriber<Integer> subscriber = TestHelper.mockSubscriber(); + take.subscribe(subscriber); + + verify(subscriber, times(1)).onNext(1); + verify(subscriber, times(1)).onNext(2); + verify(subscriber, never()).onNext(3); + verify(subscriber, never()).onError(any(Throwable.class)); + verify(subscriber, times(1)).onComplete(); + } + + @Test + public void takeWhileOnSubject1() { + FlowableProcessor<Integer> s = PublishProcessor.create(); + Flowable<Integer> take = s.takeWhile(new Predicate<Integer>() { + @Override + public boolean test(Integer input) { + return input < 3; + } + }); + + Subscriber<Integer> subscriber = TestHelper.mockSubscriber(); + take.subscribe(subscriber); + + s.onNext(1); + s.onNext(2); + s.onNext(3); + s.onNext(4); + s.onNext(5); + s.onComplete(); + + verify(subscriber, times(1)).onNext(1); + verify(subscriber, times(1)).onNext(2); + verify(subscriber, never()).onNext(3); + verify(subscriber, never()).onNext(4); + verify(subscriber, never()).onNext(5); + verify(subscriber, never()).onError(any(Throwable.class)); + verify(subscriber, times(1)).onComplete(); + } + + @Test + public void takeWhile2() { + Flowable<String> w = Flowable.just("one", "two", "three"); + Flowable<String> take = w.takeWhile(new Predicate<String>() { + int index; + + @Override + public boolean test(String input) { + return index++ < 2; + } + }); + + Subscriber<String> subscriber = TestHelper.mockSubscriber(); + take.subscribe(subscriber); + + verify(subscriber, times(1)).onNext("one"); + verify(subscriber, times(1)).onNext("two"); + verify(subscriber, never()).onNext("three"); + verify(subscriber, never()).onError(any(Throwable.class)); + verify(subscriber, times(1)).onComplete(); + } + + @Test + public void takeWhileDoesntLeakErrors() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + Flowable<String> source = Flowable.unsafeCreate(new Publisher<String>() { + @Override + public void subscribe(Subscriber<? super String> subscriber) { + subscriber.onSubscribe(new BooleanSubscription()); + subscriber.onNext("one"); + subscriber.onError(new TestException("test failed")); + } + }); + + source.takeWhile(new Predicate<String>() { + @Override + public boolean test(String s) { + return false; + } + }).blockingLast(""); + + TestHelper.assertUndeliverable(errors, 0, TestException.class, "test failed"); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void takeWhileProtectsPredicateCall() { + TestFlowable source = new TestFlowable(mock(Subscription.class), "one"); + final RuntimeException testException = new RuntimeException("test exception"); + + Subscriber<String> subscriber = TestHelper.mockSubscriber(); + Flowable<String> take = Flowable.unsafeCreate(source) + .takeWhile(new Predicate<String>() { + @Override + public boolean test(String s) { + throw testException; + } + }); + take.subscribe(subscriber); + + // wait for the Flowable to complete + try { + source.t.join(); + } catch (Throwable e) { + e.printStackTrace(); + fail(e.getMessage()); + } + + verify(subscriber, never()).onNext(any(String.class)); + verify(subscriber, times(1)).onError(testException); + } + + @Test + public void unsubscribeAfterTake() { + Subscription s = mock(Subscription.class); + TestFlowable w = new TestFlowable(s, "one", "two", "three"); + + Subscriber<String> subscriber = TestHelper.mockSubscriber(); + Flowable<String> take = Flowable.unsafeCreate(w) + .takeWhile(new Predicate<String>() { + int index; + + @Override + public boolean test(String s) { + return index++ < 1; + } + }); + take.subscribe(subscriber); + + // wait for the Flowable to complete + try { + w.t.join(); + } catch (Throwable e) { + e.printStackTrace(); + fail(e.getMessage()); + } + + System.out.println("TestFlowable thread finished"); + verify(subscriber, times(1)).onNext("one"); + verify(subscriber, never()).onNext("two"); + verify(subscriber, never()).onNext("three"); + verify(s, times(1)).cancel(); + } + + private static class TestFlowable implements Publisher<String> { + + final Subscription upstream; + final String[] values; + Thread t; + + TestFlowable(Subscription s, String... values) { + this.upstream = s; + this.values = values; + } + + @Override + public void subscribe(final Subscriber<? super String> subscriber) { + System.out.println("TestFlowable subscribed to ..."); + subscriber.onSubscribe(upstream); + t = new Thread(new Runnable() { + + @Override + public void run() { + try { + System.out.println("running TestFlowable thread"); + for (String s : values) { + System.out.println("TestFlowable onNext: " + s); + subscriber.onNext(s); + } + subscriber.onComplete(); + } catch (Throwable e) { + throw new RuntimeException(e); + } + } + + }); + System.out.println("starting TestFlowable thread"); + t.start(); + System.out.println("done starting TestFlowable thread"); + } + } + + @Test + public void backpressure() { + Flowable<Integer> source = Flowable.range(1, 1000).takeWhile(new Predicate<Integer>() { + @Override + public boolean test(Integer t1) { + return t1 < 100; + } + }); + TestSubscriber<Integer> ts = new TestSubscriber<>(5L); + + source.subscribe(ts); + + ts.assertNoErrors(); + ts.assertValues(1, 2, 3, 4, 5); + + ts.request(5); + + ts.assertNoErrors(); + ts.assertValues(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + } + + @Test + public void noUnsubscribeDownstream() { + Flowable<Integer> source = Flowable.range(1, 1000).takeWhile(new Predicate<Integer>() { + @Override + public boolean test(Integer t1) { + return t1 < 2; + } + }); + TestSubscriber<Integer> ts = new TestSubscriber<>(); + + source.subscribe(ts); + + ts.assertNoErrors(); + ts.assertValue(1); + + Assert.assertFalse("Unsubscribed!", ts.isCancelled()); + } + + @Test + public void errorCauseIncludesLastValue() { + TestSubscriberEx<String> ts = new TestSubscriberEx<>(); + Flowable.just("abc").takeWhile(new Predicate<String>() { + @Override + public boolean test(String t1) { + throw new TestException(); + } + }).subscribe(ts); + + ts.assertTerminated(); + ts.assertNoValues(); + ts.assertError(TestException.class); + // FIXME last cause value not recorded +// assertTrue(ts.getOnErrorEvents().get(0).getCause().getMessage().contains("abc")); + } + + @Test + public void dispose() { + TestHelper.checkDisposed(PublishProcessor.create().takeWhile(Functions.alwaysTrue())); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeFlowable(new Function<Flowable<Object>, Flowable<Object>>() { + @Override + public Flowable<Object> apply(Flowable<Object> f) throws Exception { + return f.takeWhile(Functions.alwaysTrue()); + } + }); + } + + @Test + public void badSource() { + new Flowable<Integer>() { + @Override + protected void subscribeActual(Subscriber<? super Integer> subscriber) { + subscriber.onSubscribe(new BooleanSubscription()); + subscriber.onComplete(); + subscriber.onComplete(); + } + } + .takeWhile(Functions.alwaysTrue()) + .test() + .assertResult(); + } + +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableThrottleFirstTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableThrottleFirstTest.java new file mode 100644 index 0000000000..68a559e3cd --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableThrottleFirstTest.java @@ -0,0 +1,285 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import java.util.List; +import java.util.concurrent.TimeUnit; + +import io.reactivex.rxjava3.functions.Action; +import org.junit.*; +import org.mockito.InOrder; +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.internal.subscriptions.BooleanSubscription; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.processors.PublishProcessor; +import io.reactivex.rxjava3.schedulers.TestScheduler; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class FlowableThrottleFirstTest extends RxJavaTest { + + private TestScheduler scheduler; + private Scheduler.Worker innerScheduler; + private Subscriber<String> subscriber; + + @Before + public void before() { + scheduler = new TestScheduler(); + innerScheduler = scheduler.createWorker(); + subscriber = TestHelper.mockSubscriber(); + } + + @Test + public void throttlingWithDropCallbackCrashes() throws Throwable { + Flowable<String> source = Flowable.unsafeCreate(new Publisher<String>() { + @Override + public void subscribe(Subscriber<? super String> subscriber) { + subscriber.onSubscribe(new BooleanSubscription()); + publishNext(subscriber, 100, "one"); // publish as it's first + publishNext(subscriber, 300, "two"); // skip as it's last within the first 400 + publishNext(subscriber, 900, "three"); // publish + publishNext(subscriber, 905, "four"); // skip + publishCompleted(subscriber, 1000); // Should be published as soon as the timeout expires. + } + }); + + Action whenDisposed = mock(Action.class); + + Flowable<String> sampled = source + .doOnCancel(whenDisposed) + .throttleFirst(400, TimeUnit.MILLISECONDS, scheduler, e -> { + if ("two".equals(e)) { + throw new TestException("forced"); + } + }); + sampled.subscribe(subscriber); + + InOrder inOrder = inOrder(subscriber); + + scheduler.advanceTimeTo(1000, TimeUnit.MILLISECONDS); + inOrder.verify(subscriber, times(1)).onNext("one"); + inOrder.verify(subscriber, times(1)).onError(any(TestException.class)); + inOrder.verify(subscriber, times(0)).onNext("two"); + inOrder.verify(subscriber, times(0)).onNext("three"); + inOrder.verify(subscriber, times(0)).onNext("four"); + inOrder.verify(subscriber, times(0)).onComplete(); + inOrder.verifyNoMoreInteractions(); + verify(whenDisposed).run(); + } + + @Test + public void throttlingWithDropCallback() { + Flowable<String> source = Flowable.unsafeCreate(new Publisher<String>() { + @Override + public void subscribe(Subscriber<? super String> subscriber) { + subscriber.onSubscribe(new BooleanSubscription()); + publishNext(subscriber, 100, "one"); // publish as it's first + publishNext(subscriber, 300, "two"); // skip as it's last within the first 400 + publishNext(subscriber, 900, "three"); // publish + publishNext(subscriber, 905, "four"); // skip + publishCompleted(subscriber, 1000); // Should be published as soon as the timeout expires. + } + }); + + Observer<Object> dropCallbackObserver = TestHelper.mockObserver(); + Flowable<String> sampled = source.throttleFirst(400, TimeUnit.MILLISECONDS, scheduler, dropCallbackObserver::onNext); + sampled.subscribe(subscriber); + + InOrder inOrder = inOrder(subscriber); + InOrder dropCallbackOrder = inOrder(dropCallbackObserver); + + scheduler.advanceTimeTo(1000, TimeUnit.MILLISECONDS); + inOrder.verify(subscriber, times(1)).onNext("one"); + inOrder.verify(subscriber, times(0)).onNext("two"); + dropCallbackOrder.verify(dropCallbackObserver, times(1)).onNext("two"); + inOrder.verify(subscriber, times(1)).onNext("three"); + inOrder.verify(subscriber, times(0)).onNext("four"); + dropCallbackOrder.verify(dropCallbackObserver, times(1)).onNext("four"); + inOrder.verify(subscriber, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + dropCallbackOrder.verifyNoMoreInteractions(); + } + + @Test + public void throttlingWithCompleted() { + Flowable<String> source = Flowable.unsafeCreate(new Publisher<String>() { + @Override + public void subscribe(Subscriber<? super String> subscriber) { + subscriber.onSubscribe(new BooleanSubscription()); + publishNext(subscriber, 100, "one"); // publish as it's first + publishNext(subscriber, 300, "two"); // skip as it's last within the first 400 + publishNext(subscriber, 900, "three"); // publish + publishNext(subscriber, 905, "four"); // skip + publishCompleted(subscriber, 1000); // Should be published as soon as the timeout expires. + } + }); + + Flowable<String> sampled = source.throttleFirst(400, TimeUnit.MILLISECONDS, scheduler); + sampled.subscribe(subscriber); + + InOrder inOrder = inOrder(subscriber); + + scheduler.advanceTimeTo(1000, TimeUnit.MILLISECONDS); + inOrder.verify(subscriber, times(1)).onNext("one"); + inOrder.verify(subscriber, times(0)).onNext("two"); + inOrder.verify(subscriber, times(1)).onNext("three"); + inOrder.verify(subscriber, times(0)).onNext("four"); + inOrder.verify(subscriber, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void throttlingWithError() { + Flowable<String> source = Flowable.unsafeCreate(new Publisher<String>() { + @Override + public void subscribe(Subscriber<? super String> subscriber) { + subscriber.onSubscribe(new BooleanSubscription()); + Exception error = new TestException(); + publishNext(subscriber, 100, "one"); // Should be published since it is first + publishNext(subscriber, 200, "two"); // Should be skipped since onError will arrive before the timeout expires + publishError(subscriber, 300, error); // Should be published as soon as the timeout expires. + } + }); + + Flowable<String> sampled = source.throttleFirst(400, TimeUnit.MILLISECONDS, scheduler); + sampled.subscribe(subscriber); + + InOrder inOrder = inOrder(subscriber); + + scheduler.advanceTimeTo(400, TimeUnit.MILLISECONDS); + inOrder.verify(subscriber).onNext("one"); + inOrder.verify(subscriber).onError(any(TestException.class)); + inOrder.verifyNoMoreInteractions(); + } + + private <T> void publishCompleted(final Subscriber<T> subscriber, long delay) { + innerScheduler.schedule(new Runnable() { + @Override + public void run() { + subscriber.onComplete(); + } + }, delay, TimeUnit.MILLISECONDS); + } + + private <T> void publishError(final Subscriber<T> subscriber, long delay, final Exception error) { + innerScheduler.schedule(new Runnable() { + @Override + public void run() { + subscriber.onError(error); + } + }, delay, TimeUnit.MILLISECONDS); + } + + private <T> void publishNext(final Subscriber<T> subscriber, long delay, final T value) { + innerScheduler.schedule(new Runnable() { + @Override + public void run() { + subscriber.onNext(value); + } + }, delay, TimeUnit.MILLISECONDS); + } + + @Test + public void throttle() { + Subscriber<Integer> subscriber = TestHelper.mockSubscriber(); + TestScheduler s = new TestScheduler(); + PublishProcessor<Integer> o = PublishProcessor.create(); + o.throttleFirst(500, TimeUnit.MILLISECONDS, s).subscribe(subscriber); + + // send events with simulated time increments + s.advanceTimeTo(0, TimeUnit.MILLISECONDS); + o.onNext(1); // deliver + o.onNext(2); // skip + s.advanceTimeTo(501, TimeUnit.MILLISECONDS); + o.onNext(3); // deliver + s.advanceTimeTo(600, TimeUnit.MILLISECONDS); + o.onNext(4); // skip + s.advanceTimeTo(700, TimeUnit.MILLISECONDS); + o.onNext(5); // skip + o.onNext(6); // skip + s.advanceTimeTo(1001, TimeUnit.MILLISECONDS); + o.onNext(7); // deliver + s.advanceTimeTo(1501, TimeUnit.MILLISECONDS); + o.onComplete(); + + InOrder inOrder = inOrder(subscriber); + inOrder.verify(subscriber).onNext(1); + inOrder.verify(subscriber).onNext(3); + inOrder.verify(subscriber).onNext(7); + inOrder.verify(subscriber).onComplete(); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void throttleFirstDefaultScheduler() { + Flowable.just(1).throttleFirst(100, TimeUnit.MILLISECONDS) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(1); + } + + @Test + public void dispose() { + TestHelper.checkDisposed(Flowable.just(1).throttleFirst(1, TimeUnit.DAYS)); + } + + @Test + public void badSource() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + new Flowable<Integer>() { + @Override + protected void subscribeActual(Subscriber<? super Integer> subscriber) { + subscriber.onSubscribe(new BooleanSubscription()); + subscriber.onNext(1); + subscriber.onNext(2); + subscriber.onComplete(); + subscriber.onNext(3); + subscriber.onError(new TestException()); + subscriber.onComplete(); + } + } + .throttleFirst(1, TimeUnit.DAYS) + .test() + .assertResult(1); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void backpressureNoRequest() { + Flowable.range(1, 3) + .throttleFirst(1, TimeUnit.MINUTES) + .test(0L) + .assertFailure(MissingBackpressureException.class); + } + + @Test + public void badRequest() { + TestHelper.assertBadRequestReported(Flowable.never().throttleFirst(1, TimeUnit.MINUTES)); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeFlowable(f -> f.throttleFirst(1, TimeUnit.MINUTES)); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableThrottleLatestTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableThrottleLatestTest.java new file mode 100644 index 0000000000..2b26ec98d4 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableThrottleLatestTest.java @@ -0,0 +1,758 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import static org.mockito.Mockito.*; + +import java.util.concurrent.TimeUnit; + +import org.junit.Test; +import org.reactivestreams.Publisher; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.subscriptions.EmptySubscription; +import io.reactivex.rxjava3.processors.PublishProcessor; +import io.reactivex.rxjava3.schedulers.TestScheduler; +import io.reactivex.rxjava3.subscribers.TestSubscriber; +import io.reactivex.rxjava3.testsupport.*; + +public class FlowableThrottleLatestTest extends RxJavaTest { + + @Test + public void just() { + Flowable.just(1) + .throttleLatest(1, TimeUnit.MINUTES) + .test() + .assertResult(1); + } + + @Test + public void range() { + Flowable.range(1, 5) + .throttleLatest(1, TimeUnit.MINUTES) + .test() + .assertResult(1); + } + + @Test + public void rangeEmitLatest() { + Flowable.range(1, 5) + .throttleLatest(1, TimeUnit.MINUTES, true) + .test() + .assertResult(1, 5); + } + + @Test + public void error() { + Flowable.error(new TestException()) + .throttleLatest(1, TimeUnit.MINUTES) + .test() + .assertFailure(TestException.class); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeFlowable(new Function<Flowable<Object>, Publisher<Object>>() { + @Override + public Publisher<Object> apply(Flowable<Object> f) throws Exception { + return f.throttleLatest(1, TimeUnit.MINUTES); + } + }); + } + + @Test + public void badRequest() { + TestHelper.assertBadRequestReported( + Flowable.never() + .throttleLatest(1, TimeUnit.MINUTES) + ); + } + + @Test + public void normal() { + TestScheduler sch = new TestScheduler(); + PublishProcessor<Integer> pp = PublishProcessor.create(); + + TestSubscriber<Integer> ts = pp.throttleLatest(1, TimeUnit.SECONDS, sch).test(); + + pp.onNext(1); + + ts.assertValuesOnly(1); + + pp.onNext(2); + + ts.assertValuesOnly(1); + + pp.onNext(3); + + ts.assertValuesOnly(1); + + sch.advanceTimeBy(1, TimeUnit.SECONDS); + + ts.assertValuesOnly(1, 3); + + pp.onNext(4); + + ts.assertValuesOnly(1, 3); + + pp.onNext(5); + + sch.advanceTimeBy(1, TimeUnit.SECONDS); + + ts.assertValuesOnly(1, 3, 5); + + sch.advanceTimeBy(1, TimeUnit.SECONDS); + + ts.assertValuesOnly(1, 3, 5); + + pp.onNext(6); + + ts.assertValuesOnly(1, 3, 5, 6); + + pp.onNext(7); + pp.onComplete(); + + ts.assertResult(1, 3, 5, 6); + + sch.advanceTimeBy(1, TimeUnit.SECONDS); + + ts.assertResult(1, 3, 5, 6); + } + + @Test + public void normalEmitLast() { + TestScheduler sch = new TestScheduler(); + PublishProcessor<Integer> pp = PublishProcessor.create(); + + TestSubscriber<Integer> ts = pp.throttleLatest(1, TimeUnit.SECONDS, sch, true).test(); + + pp.onNext(1); + + ts.assertValuesOnly(1); + + pp.onNext(2); + + ts.assertValuesOnly(1); + + pp.onNext(3); + + ts.assertValuesOnly(1); + + sch.advanceTimeBy(1, TimeUnit.SECONDS); + + ts.assertValuesOnly(1, 3); + + pp.onNext(4); + + ts.assertValuesOnly(1, 3); + + pp.onNext(5); + + sch.advanceTimeBy(1, TimeUnit.SECONDS); + + ts.assertValuesOnly(1, 3, 5); + + sch.advanceTimeBy(1, TimeUnit.SECONDS); + + ts.assertValuesOnly(1, 3, 5); + + pp.onNext(6); + + ts.assertValuesOnly(1, 3, 5, 6); + + pp.onNext(7); + pp.onComplete(); + + ts.assertResult(1, 3, 5, 6, 7); + + sch.advanceTimeBy(1, TimeUnit.SECONDS); + + ts.assertResult(1, 3, 5, 6, 7); + } + + @Test + public void missingBackpressureExceptionFirst() throws Throwable { + TestScheduler sch = new TestScheduler(); + Action onCancel = mock(Action.class); + + Flowable.just(1, 2) + .doOnCancel(onCancel) + .throttleLatest(1, TimeUnit.MINUTES, sch) + .test(0) + .assertFailure(MissingBackpressureException.class); + + verify(onCancel).run(); + } + + @Test + public void missingBackpressureExceptionLatest() throws Throwable { + TestScheduler sch = new TestScheduler(); + Action onCancel = mock(Action.class); + + TestSubscriber<Integer> ts = Flowable.just(1, 2) + .concatWith(Flowable.<Integer>never()) + .doOnCancel(onCancel) + .throttleLatest(1, TimeUnit.SECONDS, sch, true) + .test(1); + + sch.advanceTimeBy(1, TimeUnit.SECONDS); + + ts.assertFailure(MissingBackpressureException.class, 1); + + verify(onCancel).run(); + } + + @Test + public void missingBackpressureExceptionLatestComplete() throws Throwable { + TestScheduler sch = new TestScheduler(); + Action onCancel = mock(Action.class); + + PublishProcessor<Integer> pp = PublishProcessor.create(); + + TestSubscriber<Integer> ts = pp + .doOnCancel(onCancel) + .throttleLatest(1, TimeUnit.SECONDS, sch, true) + .test(1); + + pp.onNext(1); + pp.onNext(2); + + ts.assertValuesOnly(1); + + pp.onComplete(); + + ts.assertFailure(MissingBackpressureException.class, 1); + + verify(onCancel, never()).run(); + } + + @Test + public void take() throws Throwable { + Action onCancel = mock(Action.class); + + Flowable.range(1, 5) + .doOnCancel(onCancel) + .throttleLatest(1, TimeUnit.MINUTES) + .take(1) + .test() + .assertResult(1); + + verify(onCancel).run(); + } + + @Test + public void reentrantComplete() { + TestScheduler sch = new TestScheduler(); + final PublishProcessor<Integer> pp = PublishProcessor.create(); + + TestSubscriber<Integer> ts = new TestSubscriber<Integer>() { + @Override + public void onNext(Integer t) { + super.onNext(t); + if (t == 1) { + pp.onNext(2); + } + if (t == 2) { + pp.onComplete(); + } + } + }; + + pp.throttleLatest(1, TimeUnit.SECONDS, sch).subscribe(ts); + + pp.onNext(1); + + sch.advanceTimeBy(1, TimeUnit.SECONDS); + + ts.assertResult(1, 2); + } + + /** Emit 1, 2, 3, then advance time by a second; 1 and 3 should end up in downstream, 2 should be dropped. */ + @Test + public void onDroppedBasicNoEmitLast() { + PublishProcessor<Integer> pp =PublishProcessor.create(); + + TestScheduler sch = new TestScheduler(); + + TestSubscriber<Object> drops = new TestSubscriber<>(); + drops.onSubscribe(EmptySubscription.INSTANCE); + + TestSubscriber<Integer> ts = pp.throttleLatest(1, TimeUnit.SECONDS, sch, false, drops::onNext) + .test(); + + ts.assertEmpty(); + drops.assertEmpty(); + + pp.onNext(1); + + ts.assertValuesOnly(1); + drops.assertEmpty(); + + pp.onNext(2); + + ts.assertValuesOnly(1); + drops.assertEmpty(); + + pp.onNext(3); + + ts.assertValuesOnly(1); + drops.assertValuesOnly(2); + + sch.advanceTimeBy(1, TimeUnit.SECONDS); + + ts.assertValuesOnly(1, 3); + drops.assertValuesOnly(2); + + pp.onComplete(); + + ts.assertResult(1, 3); + + drops.assertValuesOnly(2); + } + + /** Emit 1, 2, 3; 1 should end up in downstream, 2, 3 should be dropped. */ + @Test + public void onDroppedBasicNoEmitLastDropLast() { + PublishProcessor<Integer> pp =PublishProcessor.create(); + + TestScheduler sch = new TestScheduler(); + + TestSubscriber<Object> drops = new TestSubscriber<>(); + drops.onSubscribe(EmptySubscription.INSTANCE); + + TestSubscriber<Integer> ts = pp.throttleLatest(1, TimeUnit.SECONDS, sch, false, drops::onNext) + .test(); + + ts.assertEmpty(); + drops.assertEmpty(); + + pp.onNext(1); + + ts.assertValuesOnly(1); + drops.assertEmpty(); + + pp.onNext(2); + + ts.assertValuesOnly(1); + drops.assertEmpty(); + + pp.onNext(3); + + ts.assertValuesOnly(1); + drops.assertValuesOnly(2); + + pp.onComplete(); + + ts.assertResult(1); + + drops.assertValuesOnly(2, 3); + } + + /** Emit 1, 2, 3; 1 and 3 should end up in downstream, 2 should be dropped. */ + @Test + public void onDroppedBasicEmitLast() { + PublishProcessor<Integer> pp =PublishProcessor.create(); + + TestScheduler sch = new TestScheduler(); + + TestSubscriber<Object> drops = new TestSubscriber<>(); + drops.onSubscribe(EmptySubscription.INSTANCE); + + TestSubscriber<Integer> ts = pp.throttleLatest(1, TimeUnit.SECONDS, sch, true, drops::onNext) + .test(); + + ts.assertEmpty(); + drops.assertEmpty(); + + pp.onNext(1); + + ts.assertValuesOnly(1); + drops.assertEmpty(); + + pp.onNext(2); + + ts.assertValuesOnly(1); + drops.assertEmpty(); + + pp.onNext(3); + + ts.assertValuesOnly(1); + drops.assertValuesOnly(2); + + pp.onComplete(); + + ts.assertResult(1, 3); + + drops.assertValuesOnly(2); + } + + /** Emit 1, 2, 3; 3 should trigger an error to the downstream because 2 is dropped and the callback crashes. */ + @Test + public void onDroppedBasicNoEmitLastFirstDropCrash() throws Throwable { + PublishProcessor<Integer> pp =PublishProcessor.create(); + + TestScheduler sch = new TestScheduler(); + + Action whenDisposed = mock(Action.class); + + TestSubscriber<Integer> ts = pp + .doOnCancel(whenDisposed) + .throttleLatest(1, TimeUnit.SECONDS, sch, false, d -> { + if (d == 2) { + throw new TestException("forced"); + } + }) + .test(); + + ts.assertEmpty(); + + pp.onNext(1); + + ts.assertValuesOnly(1); + + pp.onNext(2); + + ts.assertValuesOnly(1); + + pp.onNext(3); + + ts.assertFailure(TestException.class, 1); + + verify(whenDisposed).run(); + } + + /** + * Emit 1, 2, Error; the error should trigger the drop callback and crash it too, + * downstream gets 1, composite(source, drop-crash). + */ + @Test + public void onDroppedBasicNoEmitLastOnErrorDropCrash() throws Throwable { + PublishProcessor<Integer> pp =PublishProcessor.create(); + + TestScheduler sch = new TestScheduler(); + + Action whenDisposed = mock(Action.class); + + TestSubscriberEx<Integer> ts = pp + .doOnCancel(whenDisposed) + .throttleLatest(1, TimeUnit.SECONDS, sch, false, d -> { throw new TestException("forced " + d); }) + .subscribeWith(new TestSubscriberEx<>()); + + ts.assertEmpty(); + + pp.onNext(1); + + ts.assertValuesOnly(1); + + pp.onNext(2); + + ts.assertValuesOnly(1); + + pp.onError(new TestException("source")); + + ts.assertFailure(CompositeException.class, 1); + + TestHelper.assertCompositeExceptions(ts, TestException.class, "source", TestException.class, "forced 2"); + + verify(whenDisposed, never()).run(); + } + + /** + * Emit 1, 2, 3; 3 should trigger a drop-crash for 2, which then would trigger the error path and drop-crash for 3, + * the last item not delivered, downstream gets 1, composite(drop-crash 2, drop-crash 3). + */ + @Test + public void onDroppedBasicEmitLastOnErrorDropCrash() throws Throwable { + PublishProcessor<Integer> pp =PublishProcessor.create(); + + TestScheduler sch = new TestScheduler(); + + Action whenDisposed = mock(Action.class); + + TestSubscriberEx<Integer> ts = pp + .doOnCancel(whenDisposed) + .throttleLatest(1, TimeUnit.SECONDS, sch, true, d -> { throw new TestException("forced " + d); }) + .subscribeWith(new TestSubscriberEx<>()); + + ts.assertEmpty(); + + pp.onNext(1); + + ts.assertValuesOnly(1); + + pp.onNext(2); + + ts.assertValuesOnly(1); + + pp.onNext(3); + + ts.assertFailure(CompositeException.class, 1); + + TestHelper.assertCompositeExceptions(ts, TestException.class, "forced 2", TestException.class, "forced 3"); + + verify(whenDisposed).run(); + } + + /** Emit 1, complete; Downstream gets 1, complete, no drops. */ + @Test + public void onDroppedBasicNoEmitLastNoLastToDrop() { + PublishProcessor<Integer> pp =PublishProcessor.create(); + + TestScheduler sch = new TestScheduler(); + + TestSubscriber<Object> drops = new TestSubscriber<>(); + drops.onSubscribe(EmptySubscription.INSTANCE); + + TestSubscriber<Integer> ts = pp.throttleLatest(1, TimeUnit.SECONDS, sch, false, drops::onNext) + .test(); + + ts.assertEmpty(); + drops.assertEmpty(); + + pp.onNext(1); + + ts.assertValuesOnly(1); + drops.assertEmpty(); + + pp.onComplete(); + + ts.assertResult(1); + drops.assertEmpty(); + } + + /** Emit 1, error; Downstream gets 1, error, no drops. */ + @Test + public void onDroppedErrorNoEmitLastNoLastToDrop() { + PublishProcessor<Integer> pp =PublishProcessor.create(); + + TestScheduler sch = new TestScheduler(); + + TestSubscriber<Object> drops = new TestSubscriber<>(); + drops.onSubscribe(EmptySubscription.INSTANCE); + + TestSubscriber<Integer> ts = pp.throttleLatest(1, TimeUnit.SECONDS, sch, false, drops::onNext) + .test(); + + ts.assertEmpty(); + drops.assertEmpty(); + + pp.onNext(1); + + ts.assertValuesOnly(1); + drops.assertEmpty(); + + pp.onError(new TestException()); + + ts.assertFailure(TestException.class, 1); + drops.assertEmpty(); + } + + /** + * Emit 1, 2, complete; complete should crash drop, downstream gets 1, drop-crash 2. + */ + @Test + public void onDroppedHasLastNoEmitLastDropCrash() throws Throwable { + PublishProcessor<Integer> pp =PublishProcessor.create(); + + TestScheduler sch = new TestScheduler(); + + Action whenDisposed = mock(Action.class); + + TestSubscriberEx<Integer> ts = pp + .doOnCancel(whenDisposed) + .throttleLatest(1, TimeUnit.SECONDS, sch, false, d -> { throw new TestException("forced " + d); }) + .subscribeWith(new TestSubscriberEx<>()); + + ts.assertEmpty(); + + pp.onNext(1); + + ts.assertValuesOnly(1); + + pp.onNext(2); + + ts.assertValuesOnly(1); + + pp.onComplete(); + + ts.assertFailureAndMessage(TestException.class, "forced 2", 1); + + verify(whenDisposed, never()).run(); + } + + /** + * Emit 1, 2 then dispose the sequence; downstream gets 1, drop should get for 2. + */ + @Test + public void onDroppedDisposeDrops() throws Throwable { + PublishProcessor<Integer> pp =PublishProcessor.create(); + + TestScheduler sch = new TestScheduler(); + + Action whenDisposed = mock(Action.class); + + TestSubscriber<Object> drops = new TestSubscriber<>(); + drops.onSubscribe(EmptySubscription.INSTANCE); + + TestSubscriberEx<Integer> ts = pp + .doOnCancel(whenDisposed) + .throttleLatest(1, TimeUnit.SECONDS, sch, false, drops::onNext) + .subscribeWith(new TestSubscriberEx<>()); + + ts.assertEmpty(); + + pp.onNext(1); + + ts.assertValuesOnly(1); + + pp.onNext(2); + + ts.assertValuesOnly(1); + + ts.cancel(); + + ts.assertValuesOnly(1); + drops.assertValuesOnly(2); + + verify(whenDisposed).run(); + } + + /** + * Emit 1 then dispose the sequence; downstream gets 1, drop should not get called. + */ + @Test + public void onDroppedDisposeNoDrops() throws Throwable { + PublishProcessor<Integer> pp =PublishProcessor.create(); + + TestScheduler sch = new TestScheduler(); + + Action whenDisposed = mock(Action.class); + + TestSubscriber<Object> drops = new TestSubscriber<>(); + drops.onSubscribe(EmptySubscription.INSTANCE); + + TestSubscriberEx<Integer> ts = pp + .doOnCancel(whenDisposed) + .throttleLatest(1, TimeUnit.SECONDS, sch, false, drops::onNext) + .subscribeWith(new TestSubscriberEx<>()); + + ts.assertEmpty(); + + pp.onNext(1); + + ts.assertValuesOnly(1); + + ts.cancel(); + + ts.assertValuesOnly(1); + drops.assertEmpty(); + + verify(whenDisposed).run(); + } + + /** + * Emit 1, 2 then dispose the sequence; downstream gets 1, global error handler should get drop-crash 2. + */ + @Test + public void onDroppedDisposeCrashesDrop() throws Throwable { + TestHelper.withErrorTracking(errors -> { + PublishProcessor<Integer> pp =PublishProcessor.create(); + + TestScheduler sch = new TestScheduler(); + + Action whenDisposed = mock(Action.class); + + TestSubscriberEx<Integer> ts = pp + .doOnCancel(whenDisposed) + .throttleLatest(1, TimeUnit.SECONDS, sch, false, d -> { throw new TestException("forced " + d); }) + .subscribeWith(new TestSubscriberEx<>()); + + ts.assertEmpty(); + + pp.onNext(1); + + ts.assertValuesOnly(1); + + pp.onNext(2); + + ts.assertValuesOnly(1); + + ts.cancel(); + + ts.assertValuesOnly(1); + + verify(whenDisposed).run(); + + TestHelper.assertUndeliverable(errors, 0, TestException.class, "forced 2"); + }); + } + + /** Emit 1 but downstream is backpressured; downstream gets MBE, drops gets 1. */ + @Test + public void onDroppedBackpressured() throws Throwable { + PublishProcessor<Integer> pp =PublishProcessor.create(); + + TestScheduler sch = new TestScheduler(); + + TestSubscriber<Object> drops = new TestSubscriber<>(); + drops.onSubscribe(EmptySubscription.INSTANCE); + + Action whenDisposed = mock(Action.class); + + TestSubscriber<Integer> ts = pp + .doOnCancel(whenDisposed) + .throttleLatest(1, TimeUnit.SECONDS, sch, false, drops::onNext) + .test(0L); + + ts.assertEmpty(); + drops.assertEmpty(); + + pp.onNext(1); + + ts.assertFailure(MissingBackpressureException.class); + + drops.assertValuesOnly(1); + + verify(whenDisposed).run(); + } + + /** Emit 1 but downstream is backpressured; drop crashes, downstream gets composite(MBE, drop-crash 1). */ + @Test + public void onDroppedBackpressuredDropCrash() throws Throwable { + PublishProcessor<Integer> pp =PublishProcessor.create(); + + TestScheduler sch = new TestScheduler(); + + Action whenDisposed = mock(Action.class); + + TestSubscriberEx<Integer> ts = pp + .doOnCancel(whenDisposed) + .throttleLatest(1, TimeUnit.SECONDS, sch, false, d -> { throw new TestException("forced " + d); }) + .subscribeWith(new TestSubscriberEx<>(0L)); + + ts.assertEmpty(); + + pp.onNext(1); + + ts.assertFailure(CompositeException.class); + + TestHelper.assertCompositeExceptions(ts, + MissingBackpressureException.class, "Could not emit value due to lack of requests", + TestException.class, "forced 1"); + + verify(whenDisposed).run(); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableTimeIntervalTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableTimeIntervalTest.java new file mode 100644 index 0000000000..e2e9550388 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableTimeIntervalTest.java @@ -0,0 +1,150 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import static org.mockito.Mockito.*; + +import java.util.concurrent.TimeUnit; + +import org.junit.*; +import org.mockito.InOrder; +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.processors.PublishProcessor; +import io.reactivex.rxjava3.schedulers.*; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class FlowableTimeIntervalTest extends RxJavaTest { + + private static final TimeUnit TIME_UNIT = TimeUnit.MILLISECONDS; + + private Subscriber<Timed<Integer>> subscriber; + + private TestScheduler testScheduler; + private PublishProcessor<Integer> processor; + private Flowable<Timed<Integer>> flowable; + + @Before + public void setUp() { + subscriber = TestHelper.mockSubscriber(); + testScheduler = new TestScheduler(); + processor = PublishProcessor.create(); + flowable = processor.timeInterval(testScheduler); + } + + @Test + public void timeInterval() { + InOrder inOrder = inOrder(subscriber); + flowable.subscribe(subscriber); + + testScheduler.advanceTimeBy(1000, TIME_UNIT); + processor.onNext(1); + testScheduler.advanceTimeBy(2000, TIME_UNIT); + processor.onNext(2); + testScheduler.advanceTimeBy(3000, TIME_UNIT); + processor.onNext(3); + processor.onComplete(); + + inOrder.verify(subscriber, times(1)).onNext( + new Timed<>(1, 1000, TIME_UNIT)); + inOrder.verify(subscriber, times(1)).onNext( + new Timed<>(2, 2000, TIME_UNIT)); + inOrder.verify(subscriber, times(1)).onNext( + new Timed<>(3, 3000, TIME_UNIT)); + inOrder.verify(subscriber, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void timeIntervalDefault() { + final TestScheduler scheduler = new TestScheduler(); + + RxJavaPlugins.setComputationSchedulerHandler(new Function<Scheduler, Scheduler>() { + @Override + public Scheduler apply(Scheduler v) throws Exception { + return scheduler; + } + }); + + try { + Flowable.range(1, 5) + .timeInterval() + .map(new Function<Timed<Integer>, Long>() { + @Override + public Long apply(Timed<Integer> v) throws Exception { + return v.time(); + } + }) + .test() + .assertResult(0L, 0L, 0L, 0L, 0L); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void timeIntervalDefaultSchedulerCustomUnit() { + final TestScheduler scheduler = new TestScheduler(); + + RxJavaPlugins.setComputationSchedulerHandler(new Function<Scheduler, Scheduler>() { + @Override + public Scheduler apply(Scheduler v) throws Exception { + return scheduler; + } + }); + + try { + Flowable.range(1, 5) + .timeInterval(TimeUnit.SECONDS) + .map(new Function<Timed<Integer>, Long>() { + @Override + public Long apply(Timed<Integer> v) throws Exception { + return v.time(); + } + }) + .test() + .assertResult(0L, 0L, 0L, 0L, 0L); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void dispose() { + TestHelper.checkDisposed(Flowable.just(1).timeInterval()); + } + + @Test + public void error() { + Flowable.error(new TestException()) + .timeInterval() + .test() + .assertFailure(TestException.class); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeFlowable(new Function<Flowable<Object>, Publisher<Timed<Object>>>() { + @Override + public Publisher<Timed<Object>> apply(Flowable<Object> f) + throws Exception { + return f.timeInterval(); + } + }); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableTimeoutTests.java b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableTimeoutTests.java new file mode 100644 index 0000000000..070bb620b0 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableTimeoutTests.java @@ -0,0 +1,536 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import static io.reactivex.rxjava3.internal.util.ExceptionHelper.timeoutMessage; +import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import java.util.List; +import java.util.concurrent.*; + +import org.junit.*; +import org.mockito.InOrder; +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.internal.subscriptions.BooleanSubscription; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.processors.PublishProcessor; +import io.reactivex.rxjava3.schedulers.*; +import io.reactivex.rxjava3.subscribers.TestSubscriber; +import io.reactivex.rxjava3.testsupport.*; + +public class FlowableTimeoutTests extends RxJavaTest { + private PublishProcessor<String> underlyingSubject; + private TestScheduler testScheduler; + private Flowable<String> withTimeout; + private static final long TIMEOUT = 3; + private static final TimeUnit TIME_UNIT = TimeUnit.SECONDS; + + @Before + public void setUp() { + + underlyingSubject = PublishProcessor.create(); + testScheduler = new TestScheduler(); + withTimeout = underlyingSubject.timeout(TIMEOUT, TIME_UNIT, testScheduler); + } + + @Test + public void shouldNotTimeoutIfOnNextWithinTimeout() { + Subscriber<String> subscriber = TestHelper.mockSubscriber(); + TestSubscriber<String> ts = new TestSubscriber<>(subscriber); + + withTimeout.subscribe(ts); + + testScheduler.advanceTimeBy(2, TimeUnit.SECONDS); + underlyingSubject.onNext("One"); + verify(subscriber).onNext("One"); + testScheduler.advanceTimeBy(2, TimeUnit.SECONDS); + verify(subscriber, never()).onError(any(Throwable.class)); + ts.cancel(); + } + + @Test + public void shouldNotTimeoutIfSecondOnNextWithinTimeout() { + Subscriber<String> subscriber = TestHelper.mockSubscriber(); + TestSubscriber<String> ts = new TestSubscriber<>(subscriber); + + withTimeout.subscribe(ts); + + testScheduler.advanceTimeBy(2, TimeUnit.SECONDS); + underlyingSubject.onNext("One"); + testScheduler.advanceTimeBy(2, TimeUnit.SECONDS); + underlyingSubject.onNext("Two"); + verify(subscriber).onNext("Two"); + testScheduler.advanceTimeBy(2, TimeUnit.SECONDS); + verify(subscriber, never()).onError(any(Throwable.class)); + ts.cancel(); + } + + @Test + public void shouldTimeoutIfOnNextNotWithinTimeout() { + TestSubscriberEx<String> subscriber = new TestSubscriberEx<>(); + + withTimeout.subscribe(subscriber); + + testScheduler.advanceTimeBy(TIMEOUT + 1, TimeUnit.SECONDS); + subscriber.assertFailureAndMessage(TimeoutException.class, timeoutMessage(TIMEOUT, TIME_UNIT)); + } + + @Test + public void shouldTimeoutIfSecondOnNextNotWithinTimeout() { + TestSubscriberEx<String> subscriber = new TestSubscriberEx<>(); + TestSubscriber<String> ts = new TestSubscriber<>(subscriber); + withTimeout.subscribe(subscriber); + testScheduler.advanceTimeBy(2, TimeUnit.SECONDS); + underlyingSubject.onNext("One"); + subscriber.assertValue("One"); + testScheduler.advanceTimeBy(TIMEOUT + 1, TimeUnit.SECONDS); + subscriber.assertFailureAndMessage(TimeoutException.class, timeoutMessage(TIMEOUT, TIME_UNIT), "One"); + ts.cancel(); + } + + @Test + public void shouldCompleteIfUnderlyingComletes() { + Subscriber<String> subscriber = TestHelper.mockSubscriber(); + TestSubscriber<String> ts = new TestSubscriber<>(subscriber); + withTimeout.subscribe(subscriber); + testScheduler.advanceTimeBy(2, TimeUnit.SECONDS); + underlyingSubject.onComplete(); + testScheduler.advanceTimeBy(2, TimeUnit.SECONDS); + verify(subscriber).onComplete(); + verify(subscriber, never()).onError(any(Throwable.class)); + ts.cancel(); + } + + @Test + public void shouldErrorIfUnderlyingErrors() { + Subscriber<String> subscriber = TestHelper.mockSubscriber(); + TestSubscriber<String> ts = new TestSubscriber<>(subscriber); + withTimeout.subscribe(subscriber); + testScheduler.advanceTimeBy(2, TimeUnit.SECONDS); + underlyingSubject.onError(new UnsupportedOperationException()); + testScheduler.advanceTimeBy(2, TimeUnit.SECONDS); + verify(subscriber).onError(any(UnsupportedOperationException.class)); + ts.cancel(); + } + + @Test + public void shouldSwitchToOtherIfOnNextNotWithinTimeout() { + Flowable<String> other = Flowable.just("a", "b", "c"); + Flowable<String> source = underlyingSubject.timeout(TIMEOUT, TIME_UNIT, testScheduler, other); + + Subscriber<String> subscriber = TestHelper.mockSubscriber(); + TestSubscriber<String> ts = new TestSubscriber<>(subscriber); + source.subscribe(ts); + + testScheduler.advanceTimeBy(2, TimeUnit.SECONDS); + underlyingSubject.onNext("One"); + testScheduler.advanceTimeBy(4, TimeUnit.SECONDS); + underlyingSubject.onNext("Two"); + InOrder inOrder = inOrder(subscriber); + inOrder.verify(subscriber, times(1)).onNext("One"); + inOrder.verify(subscriber, times(1)).onNext("a"); + inOrder.verify(subscriber, times(1)).onNext("b"); + inOrder.verify(subscriber, times(1)).onNext("c"); + inOrder.verify(subscriber, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + ts.cancel(); + } + + @Test + public void shouldSwitchToOtherIfOnErrorNotWithinTimeout() { + Flowable<String> other = Flowable.just("a", "b", "c"); + Flowable<String> source = underlyingSubject.timeout(TIMEOUT, TIME_UNIT, testScheduler, other); + + Subscriber<String> subscriber = TestHelper.mockSubscriber(); + TestSubscriber<String> ts = new TestSubscriber<>(subscriber); + source.subscribe(ts); + + testScheduler.advanceTimeBy(2, TimeUnit.SECONDS); + underlyingSubject.onNext("One"); + testScheduler.advanceTimeBy(4, TimeUnit.SECONDS); + underlyingSubject.onError(new UnsupportedOperationException()); + InOrder inOrder = inOrder(subscriber); + inOrder.verify(subscriber, times(1)).onNext("One"); + inOrder.verify(subscriber, times(1)).onNext("a"); + inOrder.verify(subscriber, times(1)).onNext("b"); + inOrder.verify(subscriber, times(1)).onNext("c"); + inOrder.verify(subscriber, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + ts.cancel(); + } + + @Test + public void shouldSwitchToOtherIfOnCompletedNotWithinTimeout() { + Flowable<String> other = Flowable.just("a", "b", "c"); + Flowable<String> source = underlyingSubject.timeout(TIMEOUT, TIME_UNIT, testScheduler, other); + + Subscriber<String> subscriber = TestHelper.mockSubscriber(); + TestSubscriber<String> ts = new TestSubscriber<>(subscriber); + source.subscribe(ts); + + testScheduler.advanceTimeBy(2, TimeUnit.SECONDS); + underlyingSubject.onNext("One"); + testScheduler.advanceTimeBy(4, TimeUnit.SECONDS); + underlyingSubject.onComplete(); + InOrder inOrder = inOrder(subscriber); + inOrder.verify(subscriber, times(1)).onNext("One"); + inOrder.verify(subscriber, times(1)).onNext("a"); + inOrder.verify(subscriber, times(1)).onNext("b"); + inOrder.verify(subscriber, times(1)).onNext("c"); + inOrder.verify(subscriber, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + ts.cancel(); + } + + @Test + public void shouldSwitchToOtherAndCanBeUnsubscribedIfOnNextNotWithinTimeout() { + PublishProcessor<String> other = PublishProcessor.create(); + Flowable<String> source = underlyingSubject.timeout(TIMEOUT, TIME_UNIT, testScheduler, other); + + Subscriber<String> subscriber = TestHelper.mockSubscriber(); + TestSubscriber<String> ts = new TestSubscriber<>(subscriber); + source.subscribe(ts); + + testScheduler.advanceTimeBy(2, TimeUnit.SECONDS); + underlyingSubject.onNext("One"); + testScheduler.advanceTimeBy(4, TimeUnit.SECONDS); + underlyingSubject.onNext("Two"); + + other.onNext("a"); + other.onNext("b"); + ts.cancel(); + + // The following messages should not be delivered. + other.onNext("c"); + other.onNext("d"); + other.onComplete(); + + InOrder inOrder = inOrder(subscriber); + inOrder.verify(subscriber, times(1)).onNext("One"); + inOrder.verify(subscriber, times(1)).onNext("a"); + inOrder.verify(subscriber, times(1)).onNext("b"); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void shouldTimeoutIfSynchronizedFlowableEmitFirstOnNextNotWithinTimeout() + throws InterruptedException { + final CountDownLatch exit = new CountDownLatch(1); + final CountDownLatch timeoutSetuped = new CountDownLatch(1); + + final TestSubscriberEx<String> subscriber = new TestSubscriberEx<>(); + + new Thread(new Runnable() { + + @Override + public void run() { + Flowable.unsafeCreate(new Publisher<String>() { + + @Override + public void subscribe(Subscriber<? super String> subscriber) { + subscriber.onSubscribe(new BooleanSubscription()); + try { + timeoutSetuped.countDown(); + exit.await(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + subscriber.onNext("a"); + subscriber.onComplete(); + } + + }).timeout(1, TimeUnit.SECONDS, testScheduler) + .subscribe(subscriber); + } + }).start(); + + timeoutSetuped.await(); + testScheduler.advanceTimeBy(2, TimeUnit.SECONDS); + + subscriber.assertFailureAndMessage(TimeoutException.class, timeoutMessage(1, TimeUnit.SECONDS)); + + exit.countDown(); // exit the thread + } + + @Test + public void shouldUnsubscribeFromUnderlyingSubscriptionOnTimeout() throws InterruptedException { + // From https://github.com/ReactiveX/RxJava/pull/951 + final Subscription s = mock(Subscription.class); + + Flowable<String> never = Flowable.unsafeCreate(new Publisher<String>() { + @Override + public void subscribe(Subscriber<? super String> subscriber) { + subscriber.onSubscribe(s); + } + }); + + TestScheduler testScheduler = new TestScheduler(); + Flowable<String> observableWithTimeout = never.timeout(1000, TimeUnit.MILLISECONDS, testScheduler); + + TestSubscriberEx<String> subscriber = new TestSubscriberEx<>(); + observableWithTimeout.subscribe(subscriber); + + testScheduler.advanceTimeBy(2000, TimeUnit.MILLISECONDS); + + subscriber.assertFailureAndMessage(TimeoutException.class, timeoutMessage(1000, TimeUnit.MILLISECONDS)); + + verify(s, times(1)).cancel(); + } + + @Test + public void shouldUnsubscribeFromUnderlyingSubscriptionOnDispose() { + final PublishProcessor<String> processor = PublishProcessor.create(); + final TestScheduler scheduler = new TestScheduler(); + + final TestSubscriber<String> subscriber = processor + .timeout(100, TimeUnit.MILLISECONDS, scheduler) + .test(); + + assertTrue(processor.hasSubscribers()); + + subscriber.cancel(); + + assertFalse(processor.hasSubscribers()); + } + + @Test + public void timedAndOther() { + Flowable.never().timeout(100, TimeUnit.MILLISECONDS, Flowable.just(1)) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(1); + } + + @Test + public void disposed() { + TestHelper.checkDisposed(PublishProcessor.create().timeout(1, TimeUnit.DAYS)); + + TestHelper.checkDisposed(PublishProcessor.create().timeout(1, TimeUnit.DAYS, Flowable.just(1))); + } + + @Test + public void timedErrorOther() { + Flowable.error(new TestException()) + .timeout(1, TimeUnit.DAYS, Flowable.just(1)) + .test() + .assertFailure(TestException.class); + } + + @Test + public void timedError() { + Flowable.error(new TestException()) + .timeout(1, TimeUnit.DAYS) + .test() + .assertFailure(TestException.class); + } + + @Test + public void timedEmptyOther() { + Flowable.empty() + .timeout(1, TimeUnit.DAYS, Flowable.just(1)) + .test() + .assertResult(); + } + + @Test + public void timedEmpty() { + Flowable.empty() + .timeout(1, TimeUnit.DAYS) + .test() + .assertResult(); + } + + @Test + public void badSource() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + new Flowable<Integer>() { + @Override + protected void subscribeActual(Subscriber<? super Integer> subscriber) { + subscriber.onSubscribe(new BooleanSubscription()); + + subscriber.onNext(1); + subscriber.onComplete(); + subscriber.onNext(2); + subscriber.onError(new TestException()); + subscriber.onComplete(); + } + } + .timeout(1, TimeUnit.DAYS) + .test() + .assertResult(1); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void badSourceOther() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + new Flowable<Integer>() { + @Override + protected void subscribeActual(Subscriber<? super Integer> subscriber) { + subscriber.onSubscribe(new BooleanSubscription()); + + subscriber.onNext(1); + subscriber.onComplete(); + subscriber.onNext(2); + subscriber.onError(new TestException()); + subscriber.onComplete(); + } + } + .timeout(1, TimeUnit.DAYS, Flowable.just(3)) + .test() + .assertResult(1); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void timedTake() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + + TestSubscriber<Integer> ts = pp.timeout(1, TimeUnit.DAYS) + .take(1) + .test(); + + assertTrue(pp.hasSubscribers()); + + pp.onNext(1); + + assertFalse(pp.hasSubscribers()); + + ts.assertResult(1); + } + + @Test + public void timedFallbackTake() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + + TestSubscriber<Integer> ts = pp.timeout(1, TimeUnit.DAYS, Flowable.just(2)) + .take(1) + .test(); + + assertTrue(pp.hasSubscribers()); + + pp.onNext(1); + + assertFalse(pp.hasSubscribers()); + + ts.assertResult(1); + } + + @Test + public void fallbackErrors() { + Flowable.never() + .timeout(1, TimeUnit.MILLISECONDS, Flowable.error(new TestException())) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertFailure(TestException.class); + } + + @Test + public void onNextOnTimeoutRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final TestScheduler sch = new TestScheduler(); + + final PublishProcessor<Integer> pp = PublishProcessor.create(); + + TestSubscriberEx<Integer> ts = pp.timeout(1, TimeUnit.SECONDS, sch).to(TestHelper.<Integer>testConsumer()); + + Runnable r1 = new Runnable() { + @Override + public void run() { + pp.onNext(1); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + sch.advanceTimeBy(1, TimeUnit.SECONDS); + } + }; + + TestHelper.race(r1, r2); + + if (ts.values().size() != 0) { + if (ts.errors().size() != 0) { + ts.assertFailure(TimeoutException.class, 1); + ts.assertErrorMessage(timeoutMessage(1, TimeUnit.SECONDS)); + } else { + ts.assertValuesOnly(1); + } + } else { + ts.assertFailure(TimeoutException.class); + ts.assertErrorMessage(timeoutMessage(1, TimeUnit.SECONDS)); + } + } + } + + @Test + public void onNextOnTimeoutRaceFallback() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final TestScheduler sch = new TestScheduler(); + + final PublishProcessor<Integer> pp = PublishProcessor.create(); + + TestSubscriberEx<Integer> ts = pp.timeout(1, TimeUnit.SECONDS, sch, Flowable.just(2)).to(TestHelper.<Integer>testConsumer()); + + Runnable r1 = new Runnable() { + @Override + public void run() { + pp.onNext(1); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + sch.advanceTimeBy(1, TimeUnit.SECONDS); + } + }; + + TestHelper.race(r1, r2); + + if (ts.isTerminated()) { + int c = ts.values().size(); + if (c == 1) { + int v = ts.values().get(0); + assertTrue("" + v, v == 1 || v == 2); + } else { + ts.assertResult(1, 2); + } + } else { + ts.assertValuesOnly(1); + } + } + } + + @Test + public void doubleOnSubscribeFallback() { + TestHelper.checkDoubleOnSubscribeFlowable(f -> f.timeout(1, TimeUnit.MINUTES, Flowable.<Object>never())); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableTimeoutWithSelectorTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableTimeoutWithSelectorTest.java new file mode 100644 index 0000000000..538a8e566a --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableTimeoutWithSelectorTest.java @@ -0,0 +1,907 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +import java.util.*; +import java.util.concurrent.*; +import java.util.concurrent.atomic.*; + +import org.junit.Test; +import org.mockito.InOrder; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.internal.operators.flowable.FlowableTimeout.TimeoutConsumer; +import io.reactivex.rxjava3.internal.subscriptions.BooleanSubscription; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.processors.*; +import io.reactivex.rxjava3.schedulers.Schedulers; +import io.reactivex.rxjava3.subscribers.TestSubscriber; +import io.reactivex.rxjava3.testsupport.*; + +public class FlowableTimeoutWithSelectorTest extends RxJavaTest { + @Test + public void timeoutSelectorNormal1() { + PublishProcessor<Integer> source = PublishProcessor.create(); + final PublishProcessor<Integer> timeout = PublishProcessor.create(); + + Function<Integer, Flowable<Integer>> timeoutFunc = new Function<Integer, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Integer t1) { + return timeout; + } + }; + + Flowable<Integer> other = Flowable.fromIterable(Arrays.asList(100)); + + Subscriber<Object> subscriber = TestHelper.mockSubscriber(); + InOrder inOrder = inOrder(subscriber); + + source.timeout(timeout, timeoutFunc, other).subscribe(subscriber); + + source.onNext(1); + source.onNext(2); + source.onNext(3); + timeout.onNext(1); + + inOrder.verify(subscriber).onNext(1); + inOrder.verify(subscriber).onNext(2); + inOrder.verify(subscriber).onNext(3); + inOrder.verify(subscriber).onNext(100); + inOrder.verify(subscriber).onComplete(); + verify(subscriber, never()).onError(any(Throwable.class)); + + } + + @Test + public void timeoutSelectorTimeoutFirst() throws InterruptedException { + Flowable<Integer> source = Flowable.<Integer>never(); + final PublishProcessor<Integer> timeout = PublishProcessor.create(); + + Function<Integer, Flowable<Integer>> timeoutFunc = new Function<Integer, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Integer t1) { + return timeout; + } + }; + + Flowable<Integer> other = Flowable.fromIterable(Arrays.asList(100)); + + Subscriber<Object> subscriber = TestHelper.mockSubscriber(); + InOrder inOrder = inOrder(subscriber); + + source.timeout(timeout, timeoutFunc, other).subscribe(subscriber); + + timeout.onNext(1); + + inOrder.verify(subscriber).onNext(100); + inOrder.verify(subscriber).onComplete(); + verify(subscriber, never()).onError(any(Throwable.class)); + + } + + @Test + public void timeoutSelectorFirstThrows() { + Flowable<Integer> source = Flowable.<Integer>never(); + final PublishProcessor<Integer> timeout = PublishProcessor.create(); + + Function<Integer, Flowable<Integer>> timeoutFunc = new Function<Integer, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Integer t1) { + return timeout; + } + }; + + Supplier<Flowable<Integer>> firstTimeoutFunc = new Supplier<Flowable<Integer>>() { + @Override + public Flowable<Integer> get() { + throw new TestException(); + } + }; + + Flowable<Integer> other = Flowable.fromIterable(Arrays.asList(100)); + + Subscriber<Object> subscriber = TestHelper.mockSubscriber(); + + source.timeout(Flowable.defer(firstTimeoutFunc), timeoutFunc, other).subscribe(subscriber); + + verify(subscriber).onError(any(TestException.class)); + verify(subscriber, never()).onNext(any()); + verify(subscriber, never()).onComplete(); + + } + + @Test + public void timeoutSelectorSubsequentThrows() { + PublishProcessor<Integer> source = PublishProcessor.create(); + final PublishProcessor<Integer> timeout = PublishProcessor.create(); + + Function<Integer, Flowable<Integer>> timeoutFunc = new Function<Integer, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Integer t1) { + throw new TestException(); + } + }; + + Flowable<Integer> other = Flowable.fromIterable(Arrays.asList(100)); + + Subscriber<Object> subscriber = TestHelper.mockSubscriber(); + InOrder inOrder = inOrder(subscriber); + + source.timeout(timeout, timeoutFunc, other).subscribe(subscriber); + + source.onNext(1); + + inOrder.verify(subscriber).onNext(1); + inOrder.verify(subscriber).onError(any(TestException.class)); + verify(subscriber, never()).onComplete(); + + } + + @Test + public void timeoutSelectorFirstFlowableThrows() { + PublishProcessor<Integer> source = PublishProcessor.create(); + final PublishProcessor<Integer> timeout = PublishProcessor.create(); + + Function<Integer, Flowable<Integer>> timeoutFunc = new Function<Integer, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Integer t1) { + return timeout; + } + }; + + Flowable<Integer> other = Flowable.fromIterable(Arrays.asList(100)); + + Subscriber<Object> subscriber = TestHelper.mockSubscriber(); + + source.timeout(Flowable.<Integer> error(new TestException()), timeoutFunc, other).subscribe(subscriber); + + verify(subscriber).onError(any(TestException.class)); + verify(subscriber, never()).onNext(any()); + verify(subscriber, never()).onComplete(); + + } + + @Test + public void timeoutSelectorSubsequentFlowableThrows() { + PublishProcessor<Integer> source = PublishProcessor.create(); + final PublishProcessor<Integer> timeout = PublishProcessor.create(); + + Function<Integer, Flowable<Integer>> timeoutFunc = new Function<Integer, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Integer t1) { + return Flowable.<Integer> error(new TestException()); + } + }; + + Flowable<Integer> other = Flowable.fromIterable(Arrays.asList(100)); + + Subscriber<Object> subscriber = TestHelper.mockSubscriber(); + InOrder inOrder = inOrder(subscriber); + + source.timeout(timeout, timeoutFunc, other).subscribe(subscriber); + + source.onNext(1); + + inOrder.verify(subscriber).onNext(1); + inOrder.verify(subscriber).onError(any(TestException.class)); + verify(subscriber, never()).onComplete(); + + } + + @Test + public void timeoutSelectorWithFirstTimeoutFirstAndNoOtherFlowable() { + PublishProcessor<Integer> source = PublishProcessor.create(); + final PublishProcessor<Integer> timeout = PublishProcessor.create(); + + Function<Integer, Flowable<Integer>> timeoutFunc = new Function<Integer, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Integer t1) { + return PublishProcessor.create(); + } + }; + + Subscriber<Object> subscriber = TestHelper.mockSubscriber(); + source.timeout(timeout, timeoutFunc).subscribe(subscriber); + + timeout.onNext(1); + + InOrder inOrder = inOrder(subscriber); + inOrder.verify(subscriber).onError(isA(TimeoutException.class)); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void timeoutSelectorWithTimeoutFirstAndNoOtherFlowable() { + PublishProcessor<Integer> source = PublishProcessor.create(); + final PublishProcessor<Integer> timeout = PublishProcessor.create(); + + Function<Integer, Flowable<Integer>> timeoutFunc = new Function<Integer, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Integer t1) { + return timeout; + } + }; + + Subscriber<Object> subscriber = TestHelper.mockSubscriber(); + source.timeout(PublishProcessor.create(), timeoutFunc).subscribe(subscriber); + source.onNext(1); + + timeout.onNext(1); + + InOrder inOrder = inOrder(subscriber); + inOrder.verify(subscriber).onNext(1); + inOrder.verify(subscriber).onError(isA(TimeoutException.class)); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void timeoutSelectorWithTimeoutAndOnNextRaceCondition() throws InterruptedException { + // Thread 1 Thread 2 + // + // observer.onNext(1) + // start timeout + // unsubscribe timeout in thread 2 start to do some long-time work in "unsubscribe" + // observer.onNext(2) + // timeout.onNext(1) + // "unsubscribe" done + // + // + // In the above case, the timeout operator should ignore "timeout.onNext(1)" + // since "observer" has already seen 2. + final CountDownLatch observerReceivedTwo = new CountDownLatch(1); + final CountDownLatch timeoutEmittedOne = new CountDownLatch(1); + final CountDownLatch observerCompleted = new CountDownLatch(1); + final CountDownLatch enteredTimeoutOne = new CountDownLatch(1); + final AtomicBoolean latchTimeout = new AtomicBoolean(false); + + final Function<Integer, Flowable<Integer>> timeoutFunc = new Function<Integer, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Integer t1) { + if (t1 == 1) { + // Force "unsubscribe" run on another thread + return Flowable.unsafeCreate(new Publisher<Integer>() { + @Override + public void subscribe(Subscriber<? super Integer> subscriber) { + subscriber.onSubscribe(new BooleanSubscription()); + enteredTimeoutOne.countDown(); + // force the timeout message be sent after observer.onNext(2) + while (true) { + try { + if (!observerReceivedTwo.await(30, TimeUnit.SECONDS)) { + // CountDownLatch timeout + // There should be something wrong + latchTimeout.set(true); + } + break; + } catch (InterruptedException e) { + // Since we just want to emulate a busy method, + // we ignore the interrupt signal from Scheduler. + } + } + subscriber.onNext(1); + timeoutEmittedOne.countDown(); + } + }).subscribeOn(Schedulers.newThread()); + } else { + return PublishProcessor.create(); + } + } + }; + + final Subscriber<Integer> subscriber = TestHelper.mockSubscriber(); + doAnswer(new Answer<Void>() { + + @Override + public Void answer(InvocationOnMock invocation) throws Throwable { + observerReceivedTwo.countDown(); + return null; + } + + }).when(subscriber).onNext(2); + doAnswer(new Answer<Void>() { + + @Override + public Void answer(InvocationOnMock invocation) throws Throwable { + observerCompleted.countDown(); + return null; + } + + }).when(subscriber).onComplete(); + + final TestSubscriber<Integer> ts = new TestSubscriber<>(subscriber); + + new Thread(new Runnable() { + + @Override + public void run() { + PublishProcessor<Integer> source = PublishProcessor.create(); + source.timeout(timeoutFunc, Flowable.just(3)).subscribe(ts); + source.onNext(1); // start timeout + try { + if (!enteredTimeoutOne.await(30, TimeUnit.SECONDS)) { + latchTimeout.set(true); + } + } catch (InterruptedException e) { + e.printStackTrace(); + } + source.onNext(2); // disable timeout + try { + if (!timeoutEmittedOne.await(30, TimeUnit.SECONDS)) { + latchTimeout.set(true); + } + } catch (InterruptedException e) { + e.printStackTrace(); + } + source.onComplete(); + } + + }).start(); + + if (!observerCompleted.await(30, TimeUnit.SECONDS)) { + latchTimeout.set(true); + } + + assertFalse("CoundDownLatch timeout", latchTimeout.get()); + + InOrder inOrder = inOrder(subscriber); + inOrder.verify(subscriber).onSubscribe((Subscription)notNull()); + inOrder.verify(subscriber).onNext(1); + inOrder.verify(subscriber).onNext(2); + inOrder.verify(subscriber, never()).onNext(3); + inOrder.verify(subscriber).onComplete(); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void dispose() { + TestHelper.checkDisposed(PublishProcessor.create().timeout(Functions.justFunction(Flowable.never()))); + + TestHelper.checkDisposed(PublishProcessor.create().timeout(Functions.justFunction(Flowable.never()), Flowable.never())); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeFlowable(new Function<Flowable<Object>, Flowable<Object>>() { + @Override + public Flowable<Object> apply(Flowable<Object> f) throws Exception { + return f.timeout(Functions.justFunction(Flowable.never())); + } + }); + + TestHelper.checkDoubleOnSubscribeFlowable(new Function<Flowable<Object>, Flowable<Object>>() { + @Override + public Flowable<Object> apply(Flowable<Object> f) throws Exception { + return f.timeout(Functions.justFunction(Flowable.never()), Flowable.never()); + } + }); + } + + @Test + public void empty() { + Flowable.empty() + .timeout(Functions.justFunction(Flowable.never())) + .test() + .assertResult(); + } + + @Test + public void error() { + Flowable.error(new TestException()) + .timeout(Functions.justFunction(Flowable.never())) + .test() + .assertFailure(TestException.class); + } + + @Test + public void emptyInner() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + + TestSubscriber<Integer> ts = pp + .timeout(Functions.justFunction(Flowable.empty())) + .test(); + + pp.onNext(1); + + ts.assertFailure(TimeoutException.class, 1); + } + + @Test + public void badInnerSource() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + PublishProcessor<Integer> pp = PublishProcessor.create(); + + TestSubscriberEx<Integer> ts = pp + .timeout(Functions.justFunction(new Flowable<Integer>() { + @Override + protected void subscribeActual(Subscriber<? super Integer> subscriber) { + subscriber.onSubscribe(new BooleanSubscription()); + subscriber.onError(new TestException("First")); + subscriber.onNext(2); + subscriber.onError(new TestException("Second")); + subscriber.onComplete(); + } + })) + .to(TestHelper.<Integer>testConsumer()); + + pp.onNext(1); + + ts.assertFailureAndMessage(TestException.class, "First", 1); + + TestHelper.assertUndeliverable(errors, 0, TestException.class, "Second"); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void badInnerSourceOther() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + PublishProcessor<Integer> pp = PublishProcessor.create(); + + TestSubscriberEx<Integer> ts = pp + .timeout(Functions.justFunction(new Flowable<Integer>() { + @Override + protected void subscribeActual(Subscriber<? super Integer> subscriber) { + subscriber.onSubscribe(new BooleanSubscription()); + subscriber.onError(new TestException("First")); + subscriber.onNext(2); + subscriber.onError(new TestException("Second")); + subscriber.onComplete(); + } + }), Flowable.just(2)) + .to(TestHelper.<Integer>testConsumer()); + + pp.onNext(1); + + ts.assertFailureAndMessage(TestException.class, "First", 1); + + TestHelper.assertUndeliverable(errors, 0, TestException.class, "Second"); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void withOtherMainError() { + Flowable.error(new TestException()) + .timeout(Functions.justFunction(Flowable.never()), Flowable.never()) + .test() + .assertFailure(TestException.class); + } + + @Test + public void badSourceTimeout() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + new Flowable<Integer>() { + @Override + protected void subscribeActual(Subscriber<? super Integer> subscriber) { + subscriber.onSubscribe(new BooleanSubscription()); + subscriber.onNext(1); + subscriber.onNext(2); + subscriber.onError(new TestException("First")); + subscriber.onNext(3); + subscriber.onComplete(); + subscriber.onError(new TestException("Second")); + } + } + .timeout(Functions.justFunction(Flowable.never()), Flowable.<Integer>never()) + .take(1) + .test() + .assertResult(1); + + TestHelper.assertUndeliverable(errors, 0, TestException.class, "First"); + TestHelper.assertUndeliverable(errors, 1, TestException.class, "Second"); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void selectorTake() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + + TestSubscriber<Integer> ts = pp + .timeout(Functions.justFunction(Flowable.never())) + .take(1) + .test(); + + assertTrue(pp.hasSubscribers()); + + pp.onNext(1); + + assertFalse(pp.hasSubscribers()); + + ts.assertResult(1); + } + + @Test + public void selectorFallbackTake() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + + TestSubscriber<Integer> ts = pp + .timeout(Functions.justFunction(Flowable.never()), Flowable.just(2)) + .take(1) + .test(); + + assertTrue(pp.hasSubscribers()); + + pp.onNext(1); + + assertFalse(pp.hasSubscribers()); + + ts.assertResult(1); + } + + @Test + public void lateOnTimeoutError() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + final PublishProcessor<Integer> pp = PublishProcessor.create(); + + final Subscriber<?>[] sub = { null, null }; + + final Flowable<Integer> pp2 = new Flowable<Integer>() { + + int count; + + @Override + protected void subscribeActual( + Subscriber<? super Integer> s) { + s.onSubscribe(new BooleanSubscription()); + sub[count++] = s; + } + }; + + TestSubscriber<Integer> ts = pp.timeout(Functions.justFunction(pp2)).test(); + + pp.onNext(0); + + Runnable r1 = new Runnable() { + @Override + public void run() { + pp.onNext(1); + } + }; + + final Throwable ex = new TestException(); + + Runnable r2 = new Runnable() { + @Override + public void run() { + sub[0].onError(ex); + } + }; + + TestHelper.race(r1, r2); + + ts.assertValueAt(0, 0); + + if (!errors.isEmpty()) { + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } + } finally { + RxJavaPlugins.reset(); + } + } + } + + @Test + public void lateOnTimeoutFallbackRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + final PublishProcessor<Integer> pp = PublishProcessor.create(); + + final Subscriber<?>[] sub = { null, null }; + + final Flowable<Integer> pp2 = new Flowable<Integer>() { + + int count; + + @Override + protected void subscribeActual( + Subscriber<? super Integer> s) { + assertFalse(((Disposable)s).isDisposed()); + s.onSubscribe(new BooleanSubscription()); + sub[count++] = s; + } + }; + + TestSubscriber<Integer> ts = pp.timeout(Functions.justFunction(pp2), Flowable.<Integer>never()).test(); + + pp.onNext(0); + + Runnable r1 = new Runnable() { + @Override + public void run() { + pp.onNext(1); + } + }; + + final Throwable ex = new TestException(); + + Runnable r2 = new Runnable() { + @Override + public void run() { + sub[0].onError(ex); + } + }; + + TestHelper.race(r1, r2); + + ts.assertValueAt(0, 0); + + if (!errors.isEmpty()) { + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } + } finally { + RxJavaPlugins.reset(); + } + } + } + + @Test + public void onErrorOnTimeoutRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + final PublishProcessor<Integer> pp = PublishProcessor.create(); + + final Subscriber<?>[] sub = { null, null }; + + final Flowable<Integer> pp2 = new Flowable<Integer>() { + + int count; + + @Override + protected void subscribeActual( + Subscriber<? super Integer> s) { + assertFalse(((Disposable)s).isDisposed()); + s.onSubscribe(new BooleanSubscription()); + sub[count++] = s; + } + }; + + TestSubscriber<Integer> ts = pp.timeout(Functions.justFunction(pp2)).test(); + + pp.onNext(0); + + final Throwable ex = new TestException(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + pp.onError(ex); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + sub[0].onComplete(); + } + }; + + TestHelper.race(r1, r2); + + ts.assertValueAt(0, 0); + + if (!errors.isEmpty()) { + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } + } finally { + RxJavaPlugins.reset(); + } + } + } + + @Test + public void onCompleteOnTimeoutRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + final PublishProcessor<Integer> pp = PublishProcessor.create(); + + final Subscriber<?>[] sub = { null, null }; + + final Flowable<Integer> pp2 = new Flowable<Integer>() { + + int count; + + @Override + protected void subscribeActual( + Subscriber<? super Integer> s) { + assertFalse(((Disposable)s).isDisposed()); + s.onSubscribe(new BooleanSubscription()); + sub[count++] = s; + } + }; + + TestSubscriber<Integer> ts = pp.timeout(Functions.justFunction(pp2)).test(); + + pp.onNext(0); + + Runnable r1 = new Runnable() { + @Override + public void run() { + pp.onComplete(); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + sub[0].onComplete(); + } + }; + + TestHelper.race(r1, r2); + + ts.assertValueAt(0, 0); + + if (!errors.isEmpty()) { + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } + } finally { + RxJavaPlugins.reset(); + } + } + } + + @Test + public void onCompleteOnTimeoutRaceFallback() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + final PublishProcessor<Integer> pp = PublishProcessor.create(); + + final Subscriber<?>[] sub = { null, null }; + + final Flowable<Integer> pp2 = new Flowable<Integer>() { + + int count; + + @Override + protected void subscribeActual( + Subscriber<? super Integer> s) { + assertFalse(((Disposable)s).isDisposed()); + s.onSubscribe(new BooleanSubscription()); + sub[count++] = s; + } + }; + + TestSubscriber<Integer> ts = pp.timeout(Functions.justFunction(pp2), Flowable.<Integer>never()).test(); + + pp.onNext(0); + + Runnable r1 = new Runnable() { + @Override + public void run() { + pp.onComplete(); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + sub[0].onComplete(); + } + }; + + TestHelper.race(r1, r2); + + ts.assertValueAt(0, 0); + + if (!errors.isEmpty()) { + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } + } finally { + RxJavaPlugins.reset(); + } + } + } + + @Test + public void disposedUpfront() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + final AtomicInteger counter = new AtomicInteger(); + + Flowable<Object> timeoutAndFallback = Flowable.never().doOnSubscribe(new Consumer<Subscription>() { + @Override + public void accept(Subscription s) throws Exception { + counter.incrementAndGet(); + } + }); + + pp + .timeout(timeoutAndFallback, Functions.justFunction(timeoutAndFallback)) + .test(1, true) + .assertEmpty(); + + assertEquals(0, counter.get()); + } + + @Test + public void disposedUpfrontFallback() { + PublishProcessor<Object> pp = PublishProcessor.create(); + final AtomicInteger counter = new AtomicInteger(); + + Flowable<Object> timeoutAndFallback = Flowable.never().doOnSubscribe(new Consumer<Subscription>() { + @Override + public void accept(Subscription s) throws Exception { + counter.incrementAndGet(); + } + }); + + pp + .timeout(timeoutAndFallback, Functions.justFunction(timeoutAndFallback), timeoutAndFallback) + .test(1, true) + .assertEmpty(); + + assertEquals(0, counter.get()); + } + + @Test + public void timeoutConsumerDoubleOnSubscribe() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + BehaviorProcessor.createDefault(1) + .timeout(Functions.justFunction(new Flowable<Integer>() { + @Override + protected void subscribeActual(Subscriber<? super Integer> s) { + BooleanSubscription bs1 = new BooleanSubscription(); + s.onSubscribe(bs1); + + BooleanSubscription bs2 = new BooleanSubscription(); + s.onSubscribe(bs2); + + assertFalse(bs1.isCancelled()); + assertTrue(bs2.isCancelled()); + + s.onComplete(); + } + })) + .test() + .assertFailure(TimeoutException.class, 1); + + TestHelper.assertError(errors, 0, ProtocolViolationException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void timeoutConsumerIsDisposed() { + TimeoutConsumer consumer = new TimeoutConsumer(0, null); + + assertFalse(consumer.isDisposed()); + consumer.dispose(); + assertTrue(consumer.isDisposed()); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableTimerTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableTimerTest.java new file mode 100644 index 0000000000..4f0216e612 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableTimerTest.java @@ -0,0 +1,388 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +import java.util.List; +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.junit.*; +import org.mockito.*; +import org.reactivestreams.Subscriber; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.flowables.ConnectableFlowable; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.schedulers.*; +import io.reactivex.rxjava3.subscribers.*; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class FlowableTimerTest extends RxJavaTest { + @Mock + Subscriber<Object> subscriber; + @Mock + Subscriber<Long> subscriber2; + + TestScheduler scheduler; + + @Before + public void before() { + subscriber = TestHelper.mockSubscriber(); + + subscriber2 = TestHelper.mockSubscriber(); + + scheduler = new TestScheduler(); + } + + @Test + public void timerOnce() { + Flowable.timer(100, TimeUnit.MILLISECONDS, scheduler).subscribe(subscriber); + scheduler.advanceTimeBy(100, TimeUnit.MILLISECONDS); + + verify(subscriber, times(1)).onNext(0L); + verify(subscriber, times(1)).onComplete(); + verify(subscriber, never()).onError(any(Throwable.class)); + } + + @Test + public void timerPeriodically() { + TestSubscriber<Long> ts = new TestSubscriber<>(); + + Flowable.interval(100, 100, TimeUnit.MILLISECONDS, scheduler).subscribe(ts); + + scheduler.advanceTimeBy(100, TimeUnit.MILLISECONDS); + + ts.assertValue(0L); + + scheduler.advanceTimeBy(100, TimeUnit.MILLISECONDS); + ts.assertValues(0L, 1L); + + scheduler.advanceTimeBy(100, TimeUnit.MILLISECONDS); + ts.assertValues(0L, 1L, 2L); + + scheduler.advanceTimeBy(100, TimeUnit.MILLISECONDS); + ts.assertValues(0L, 1L, 2L, 3L); + + ts.cancel(); + scheduler.advanceTimeBy(100, TimeUnit.MILLISECONDS); + ts.assertValues(0L, 1L, 2L, 3L); + + ts.assertNotComplete(); + ts.assertNoErrors(); + } + + @Test + public void interval() { + Flowable<Long> w = Flowable.interval(1, TimeUnit.SECONDS, scheduler); + TestSubscriber<Long> ts = new TestSubscriber<>(); + w.subscribe(ts); + + ts.assertNoValues(); + ts.assertNoErrors(); + ts.assertNotComplete(); + + scheduler.advanceTimeTo(2, TimeUnit.SECONDS); + + ts.assertValues(0L, 1L); + ts.assertNoErrors(); + ts.assertNotComplete(); + + ts.cancel(); + + scheduler.advanceTimeTo(4, TimeUnit.SECONDS); + ts.assertValues(0L, 1L); + ts.assertNoErrors(); + ts.assertNotComplete(); + } + + @Test + public void withMultipleSubscribersStartingAtSameTime() { + Flowable<Long> w = Flowable.interval(1, TimeUnit.SECONDS, scheduler); + + TestSubscriber<Long> ts1 = new TestSubscriber<>(); + TestSubscriber<Long> ts2 = new TestSubscriber<>(); + + w.subscribe(ts1); + w.subscribe(ts2); + + ts1.assertNoValues(); + ts2.assertNoValues(); + + scheduler.advanceTimeTo(2, TimeUnit.SECONDS); + + ts1.assertValues(0L, 1L); + ts1.assertNoErrors(); + ts1.assertNotComplete(); + + ts2.assertValues(0L, 1L); + ts2.assertNoErrors(); + ts2.assertNotComplete(); + + ts1.cancel(); + ts2.cancel(); + + scheduler.advanceTimeTo(4, TimeUnit.SECONDS); + + ts1.assertValues(0L, 1L); + ts1.assertNoErrors(); + ts1.assertNotComplete(); + + ts2.assertValues(0L, 1L); + ts2.assertNoErrors(); + ts2.assertNotComplete(); + } + + @Test + public void withMultipleStaggeredSubscribers() { + Flowable<Long> w = Flowable.interval(1, TimeUnit.SECONDS, scheduler); + + TestSubscriber<Long> ts1 = new TestSubscriber<>(); + + w.subscribe(ts1); + + ts1.assertNoErrors(); + + scheduler.advanceTimeTo(2, TimeUnit.SECONDS); + + TestSubscriber<Long> ts2 = new TestSubscriber<>(); + + w.subscribe(ts2); + + ts1.assertValues(0L, 1L); + ts1.assertNoErrors(); + ts1.assertNotComplete(); + + ts2.assertNoValues(); + + scheduler.advanceTimeTo(4, TimeUnit.SECONDS); + + ts1.assertValues(0L, 1L, 2L, 3L); + + ts2.assertValues(0L, 1L); + + ts1.cancel(); + ts2.cancel(); + + ts1.assertValues(0L, 1L, 2L, 3L); + ts1.assertNoErrors(); + ts1.assertNotComplete(); + + ts2.assertValues(0L, 1L); + ts2.assertNoErrors(); + ts2.assertNotComplete(); + } + + @Test + public void withMultipleStaggeredSubscribersAndPublish() { + ConnectableFlowable<Long> w = Flowable.interval(1, TimeUnit.SECONDS, scheduler).publish(); + + TestSubscriber<Long> ts1 = new TestSubscriber<>(); + + w.subscribe(ts1); + w.connect(); + + ts1.assertNoValues(); + + scheduler.advanceTimeTo(2, TimeUnit.SECONDS); + + TestSubscriber<Long> ts2 = new TestSubscriber<>(); + w.subscribe(ts2); + + ts1.assertValues(0L, 1L); + ts1.assertNoErrors(); + ts1.assertNotComplete(); + + ts2.assertNoValues(); + + scheduler.advanceTimeTo(4, TimeUnit.SECONDS); + + ts1.assertValues(0L, 1L, 2L, 3L); + + ts2.assertValues(2L, 3L); + + ts1.cancel(); + ts2.cancel(); + + ts1.assertValues(0L, 1L, 2L, 3L); + ts1.assertNoErrors(); + ts1.assertNotComplete(); + + ts2.assertValues(2L, 3L); + ts2.assertNoErrors(); + ts2.assertNotComplete(); + } + + @Test + public void onceObserverThrows() { + Flowable<Long> source = Flowable.timer(100, TimeUnit.MILLISECONDS, scheduler); + + source.safeSubscribe(new DefaultSubscriber<Long>() { + + @Override + public void onNext(Long t) { + throw new TestException(); + } + + @Override + public void onError(Throwable e) { + subscriber.onError(e); + } + + @Override + public void onComplete() { + subscriber.onComplete(); + } + }); + + scheduler.advanceTimeBy(1, TimeUnit.SECONDS); + + verify(subscriber).onError(any(TestException.class)); + verify(subscriber, never()).onNext(anyLong()); + verify(subscriber, never()).onComplete(); + } + + @Test + public void periodicObserverThrows() { + Flowable<Long> source = Flowable.interval(100, 100, TimeUnit.MILLISECONDS, scheduler); + + InOrder inOrder = inOrder(subscriber); + + source.safeSubscribe(new DefaultSubscriber<Long>() { + + @Override + public void onNext(Long t) { + if (t > 0) { + throw new TestException(); + } + subscriber.onNext(t); + } + + @Override + public void onError(Throwable e) { + subscriber.onError(e); + } + + @Override + public void onComplete() { + subscriber.onComplete(); + } + }); + + scheduler.advanceTimeBy(1, TimeUnit.SECONDS); + + inOrder.verify(subscriber).onNext(0L); + inOrder.verify(subscriber).onError(any(TestException.class)); + inOrder.verifyNoMoreInteractions(); + verify(subscriber, never()).onComplete(); + } + + @Test + public void disposed() { + TestHelper.checkDisposed(Flowable.timer(1, TimeUnit.DAYS)); + } + + @Test + public void backpressureNotReady() { + Flowable.timer(1, TimeUnit.MILLISECONDS) + .test(0L) + .awaitDone(5, TimeUnit.SECONDS) + .assertFailure(MissingBackpressureException.class); + } + + @Test + public void timerCancelRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final TestSubscriber<Long> ts = new TestSubscriber<>(); + + final TestScheduler scheduler = new TestScheduler(); + + Flowable.timer(1, TimeUnit.SECONDS, scheduler) + .subscribe(ts); + + Runnable r1 = new Runnable() { + @Override + public void run() { + scheduler.advanceTimeBy(1, TimeUnit.SECONDS); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + ts.cancel(); + } + }; + + TestHelper.race(r1, r2); + } + } + + @Test + public void timerDelayZero() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + for (int i = 0; i < 1000; i++) { + Flowable.timer(0, TimeUnit.MILLISECONDS).blockingFirst(); + } + + assertTrue(errors.toString(), errors.isEmpty()); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void timerInterruptible() throws Exception { + ScheduledExecutorService exec = Executors.newSingleThreadScheduledExecutor(); + try { + for (Scheduler s : new Scheduler[] { Schedulers.single(), Schedulers.computation(), Schedulers.newThread(), Schedulers.io(), Schedulers.from(exec, true) }) { + final AtomicBoolean interrupted = new AtomicBoolean(); + TestSubscriber<Long> ts = Flowable.timer(1, TimeUnit.MILLISECONDS, s) + .map(new Function<Long, Long>() { + @Override + public Long apply(Long v) throws Exception { + try { + Thread.sleep(3000); + } catch (InterruptedException ex) { + interrupted.set(true); + } + return v; + } + }) + .test(); + + Thread.sleep(500); + + ts.cancel(); + + Thread.sleep(500); + + assertTrue(s.getClass().getSimpleName(), interrupted.get()); + } + } finally { + exec.shutdown(); + } + } + + @Test + public void badRequest() { + TestHelper.assertBadRequestReported(Flowable.timer(1, TimeUnit.MINUTES)); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableTimestampTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableTimestampTest.java new file mode 100644 index 0000000000..6d98c0c414 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableTimestampTest.java @@ -0,0 +1,142 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import java.util.concurrent.TimeUnit; + +import org.junit.*; +import org.mockito.InOrder; +import org.reactivestreams.Subscriber; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.processors.PublishProcessor; +import io.reactivex.rxjava3.schedulers.*; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class FlowableTimestampTest extends RxJavaTest { + Subscriber<Object> subscriber; + + @Before + public void before() { + subscriber = TestHelper.mockSubscriber(); + } + + @Test + public void timestampWithScheduler() { + TestScheduler scheduler = new TestScheduler(); + + PublishProcessor<Integer> source = PublishProcessor.create(); + Flowable<Timed<Integer>> m = source.timestamp(scheduler); + m.subscribe(subscriber); + + source.onNext(1); + scheduler.advanceTimeBy(100, TimeUnit.MILLISECONDS); + source.onNext(2); + scheduler.advanceTimeBy(100, TimeUnit.MILLISECONDS); + source.onNext(3); + + InOrder inOrder = inOrder(subscriber); + + inOrder.verify(subscriber, times(1)).onNext(new Timed<>(1, 0, TimeUnit.MILLISECONDS)); + inOrder.verify(subscriber, times(1)).onNext(new Timed<>(2, 100, TimeUnit.MILLISECONDS)); + inOrder.verify(subscriber, times(1)).onNext(new Timed<>(3, 200, TimeUnit.MILLISECONDS)); + + verify(subscriber, never()).onError(any(Throwable.class)); + verify(subscriber, never()).onComplete(); + } + + @Test + public void timestampWithScheduler2() { + TestScheduler scheduler = new TestScheduler(); + + PublishProcessor<Integer> source = PublishProcessor.create(); + Flowable<Timed<Integer>> m = source.timestamp(scheduler); + m.subscribe(subscriber); + + source.onNext(1); + source.onNext(2); + scheduler.advanceTimeBy(100, TimeUnit.MILLISECONDS); + scheduler.advanceTimeBy(100, TimeUnit.MILLISECONDS); + source.onNext(3); + + InOrder inOrder = inOrder(subscriber); + + inOrder.verify(subscriber, times(1)).onNext(new Timed<>(1, 0, TimeUnit.MILLISECONDS)); + inOrder.verify(subscriber, times(1)).onNext(new Timed<>(2, 0, TimeUnit.MILLISECONDS)); + inOrder.verify(subscriber, times(1)).onNext(new Timed<>(3, 200, TimeUnit.MILLISECONDS)); + + verify(subscriber, never()).onError(any(Throwable.class)); + verify(subscriber, never()).onComplete(); + } + + @Test + public void timeIntervalDefault() { + final TestScheduler scheduler = new TestScheduler(); + + RxJavaPlugins.setComputationSchedulerHandler(new Function<Scheduler, Scheduler>() { + @Override + public Scheduler apply(Scheduler v) throws Exception { + return scheduler; + } + }); + + try { + Flowable.range(1, 5) + .timestamp() + .map(new Function<Timed<Integer>, Long>() { + @Override + public Long apply(Timed<Integer> v) throws Exception { + return v.time(); + } + }) + .test() + .assertResult(0L, 0L, 0L, 0L, 0L); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void timeIntervalDefaultSchedulerCustomUnit() { + final TestScheduler scheduler = new TestScheduler(); + + RxJavaPlugins.setComputationSchedulerHandler(new Function<Scheduler, Scheduler>() { + @Override + public Scheduler apply(Scheduler v) throws Exception { + return scheduler; + } + }); + + try { + Flowable.range(1, 5) + .timestamp(TimeUnit.SECONDS) + .map(new Function<Timed<Integer>, Long>() { + @Override + public Long apply(Timed<Integer> v) throws Exception { + return v.time(); + } + }) + .test() + .assertResult(0L, 0L, 0L, 0L, 0L); + } finally { + RxJavaPlugins.reset(); + } + } + +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableToCompletableTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableToCompletableTest.java new file mode 100644 index 0000000000..dff5a7462b --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableToCompletableTest.java @@ -0,0 +1,99 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import static org.junit.Assert.assertFalse; + +import java.util.concurrent.atomic.AtomicBoolean; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.functions.Action; +import io.reactivex.rxjava3.subscribers.TestSubscriber; +import io.reactivex.rxjava3.testsupport.TestSubscriberEx; + +public class FlowableToCompletableTest extends RxJavaTest { + + @Test + public void justSingleItemObservable() { + TestSubscriber<String> subscriber = TestSubscriber.create(); + Completable cmp = Flowable.just("Hello World!").ignoreElements(); + cmp.<String>toFlowable().subscribe(subscriber); + + subscriber.assertNoValues(); + subscriber.assertComplete(); + subscriber.assertNoErrors(); + } + + @Test + public void errorObservable() { + TestSubscriber<String> subscriber = TestSubscriber.create(); + IllegalArgumentException error = new IllegalArgumentException("Error"); + Completable cmp = Flowable.<String>error(error).ignoreElements(); + cmp.<String>toFlowable().subscribe(subscriber); + + subscriber.assertError(error); + subscriber.assertNoValues(); + } + + @Test + public void justTwoEmissionsObservableThrowsError() { + TestSubscriber<String> subscriber = TestSubscriber.create(); + Completable cmp = Flowable.just("First", "Second").ignoreElements(); + cmp.<String>toFlowable().subscribe(subscriber); + + subscriber.assertNoErrors(); + subscriber.assertNoValues(); + } + + @Test + public void emptyObservable() { + TestSubscriber<String> subscriber = TestSubscriber.create(); + Completable cmp = Flowable.<String>empty().ignoreElements(); + cmp.<String>toFlowable().subscribe(subscriber); + + subscriber.assertNoErrors(); + subscriber.assertNoValues(); + subscriber.assertComplete(); + } + + @Test + public void neverObservable() { + TestSubscriberEx<String> subscriber = new TestSubscriberEx<>(); + Completable cmp = Flowable.<String>never().ignoreElements(); + cmp.<String>toFlowable().subscribe(subscriber); + + subscriber.assertNotTerminated(); + subscriber.assertNoValues(); + } + + @Test + public void shouldUseUnsafeSubscribeInternallyNotSubscribe() { + TestSubscriber<String> subscriber = TestSubscriber.create(); + final AtomicBoolean unsubscribed = new AtomicBoolean(false); + Completable cmp = Flowable.just("Hello World!").doOnCancel(new Action() { + + @Override + public void run() { + unsubscribed.set(true); + }}).ignoreElements(); + + cmp.<String>toFlowable().subscribe(subscriber); + + subscriber.assertComplete(); + + assertFalse(unsubscribed.get()); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableToFutureTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableToFutureTest.java new file mode 100644 index 0000000000..8501a22e74 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableToFutureTest.java @@ -0,0 +1,246 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.junit.Test; +import org.reactivestreams.Subscriber; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.schedulers.*; +import io.reactivex.rxjava3.subscribers.TestSubscriber; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class FlowableToFutureTest extends RxJavaTest { + + @Test + public void success() throws Exception { + @SuppressWarnings("unchecked") + Future<Object> future = mock(Future.class); + Object value = new Object(); + when(future.get()).thenReturn(value); + + Subscriber<Object> subscriber = TestHelper.mockSubscriber(); + + TestSubscriber<Object> ts = new TestSubscriber<>(subscriber); + + Flowable.fromFuture(future).subscribe(ts); + + ts.cancel(); + + verify(subscriber, times(1)).onNext(value); + verify(subscriber, times(1)).onComplete(); + verify(subscriber, never()).onError(any(Throwable.class)); + verify(future, never()).cancel(anyBoolean()); + } + + @Test + public void successOperatesOnSuppliedScheduler() throws Exception { + @SuppressWarnings("unchecked") + Future<Object> future = mock(Future.class); + Object value = new Object(); + when(future.get()).thenReturn(value); + + Subscriber<Object> subscriber = TestHelper.mockSubscriber(); + + TestScheduler scheduler = new TestScheduler(); + TestSubscriber<Object> ts = new TestSubscriber<>(subscriber); + + Flowable.fromFuture(future).subscribeOn(scheduler).subscribe(ts); + + verify(subscriber, never()).onNext(value); + + scheduler.triggerActions(); + + verify(subscriber, times(1)).onNext(value); + } + + @Test + public void failure() throws Exception { + @SuppressWarnings("unchecked") + Future<Object> future = mock(Future.class); + RuntimeException e = new RuntimeException(); + when(future.get()).thenThrow(e); + + Subscriber<Object> subscriber = TestHelper.mockSubscriber(); + + TestSubscriber<Object> ts = new TestSubscriber<>(subscriber); + + Flowable.fromFuture(future).subscribe(ts); + + ts.cancel(); + + verify(subscriber, never()).onNext(null); + verify(subscriber, never()).onComplete(); + verify(subscriber, times(1)).onError(e); + verify(future, never()).cancel(anyBoolean()); + } + + @Test + public void cancelledBeforeSubscribe() throws Exception { + @SuppressWarnings("unchecked") + Future<Object> future = mock(Future.class); + CancellationException e = new CancellationException("unit test synthetic cancellation"); + when(future.get()).thenThrow(e); + + Subscriber<Object> subscriber = TestHelper.mockSubscriber(); + + TestSubscriber<Object> ts = new TestSubscriber<>(subscriber); + ts.cancel(); + + Flowable.fromFuture(future).subscribe(ts); + + ts.assertNoErrors(); + ts.assertNotComplete(); + } + + @Test + public void cancellationDuringFutureGet() throws Exception { + Future<Object> future = new Future<Object>() { + private AtomicBoolean isCancelled = new AtomicBoolean(false); + private AtomicBoolean isDone = new AtomicBoolean(false); + + @Override + public boolean cancel(boolean mayInterruptIfRunning) { + isCancelled.compareAndSet(false, true); + return true; + } + + @Override + public boolean isCancelled() { + return isCancelled.get(); + } + + @Override + public boolean isDone() { + return isCancelled() || isDone.get(); + } + + @Override + public Object get() throws InterruptedException, ExecutionException { + Thread.sleep(500); + isDone.compareAndSet(false, true); + return "foo"; + } + + @Override + public Object get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException { + return get(); + } + }; + + Subscriber<Object> subscriber = TestHelper.mockSubscriber(); + + TestSubscriber<Object> ts = new TestSubscriber<>(subscriber); + Flowable<Object> futureObservable = Flowable.fromFuture(future); + + futureObservable.subscribeOn(Schedulers.computation()).subscribe(ts); + + Thread.sleep(100); + + ts.cancel(); + + ts.assertNoErrors(); + ts.assertNoValues(); + ts.assertNotComplete(); + } + + @Test + public void backpressure() { + TestSubscriber<Integer> ts = new TestSubscriber<>(0); + + FutureTask<Integer> f = new FutureTask<>(new Runnable() { + @Override + public void run() { + + } + }, 1); + + f.run(); + + Flowable.fromFuture(f).subscribe(ts); + + ts.assertNoValues(); + + ts.request(1); + + ts.assertValue(1); + ts.assertNoErrors(); + ts.assertComplete(); + } + + @Test + public void withTimeoutNoTimeout() { + FutureTask<Integer> task = new FutureTask<>(new Runnable() { + @Override + public void run() { + + } + }, 1); + + task.run(); + + TestSubscriber<Integer> ts = TestSubscriber.create(); + + Flowable.fromFuture(task, 1, TimeUnit.SECONDS).subscribe(ts); + + ts.assertValue(1); + ts.assertNoErrors(); + ts.assertComplete(); + } + + @Test + public void withTimeoutTimeout() { + FutureTask<Integer> task = new FutureTask<>(new Runnable() { + @Override + public void run() { + + } + }, 1); + + TestSubscriber<Integer> ts = TestSubscriber.create(); + + Flowable.fromFuture(task, 10, TimeUnit.MILLISECONDS).subscribe(ts); + + ts.assertNoValues(); + ts.assertError(TimeoutException.class); + ts.assertNotComplete(); + } + + @Test + public void withTimeoutNoTimeoutScheduler() { + FutureTask<Integer> task = new FutureTask<>(new Runnable() { + @Override + public void run() { + + } + }, 1); + + TestSubscriber<Integer> ts = TestSubscriber.create(); + + Flowable.fromFuture(task).subscribeOn(Schedulers.computation()).subscribe(ts); + + task.run(); + + ts.awaitDone(5, TimeUnit.SECONDS); + ts.assertValue(1); + ts.assertNoErrors(); + ts.assertComplete(); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableToListTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableToListTest.java new file mode 100644 index 0000000000..208f371bb7 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableToListTest.java @@ -0,0 +1,366 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import java.util.*; +import java.util.concurrent.*; + +import org.junit.*; +import org.mockito.Mockito; +import org.reactivestreams.Subscriber; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.util.ExceptionHelper; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.processors.PublishProcessor; +import io.reactivex.rxjava3.subscribers.TestSubscriber; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class FlowableToListTest extends RxJavaTest { + + @Test + public void listFlowable() { + Flowable<String> w = Flowable.fromIterable(Arrays.asList("one", "two", "three")); + Flowable<List<String>> flowable = w.toList().toFlowable(); + + Subscriber<List<String>> subscriber = TestHelper.mockSubscriber(); + flowable.subscribe(subscriber); + + verify(subscriber, times(1)).onNext(Arrays.asList("one", "two", "three")); + verify(subscriber, Mockito.never()).onError(any(Throwable.class)); + verify(subscriber, times(1)).onComplete(); + } + + @Test + public void listViaFlowableFlowable() { + Flowable<String> w = Flowable.fromIterable(Arrays.asList("one", "two", "three")); + Flowable<List<String>> flowable = w.toList().toFlowable(); + + Subscriber<List<String>> subscriber = TestHelper.mockSubscriber(); + flowable.subscribe(subscriber); + + verify(subscriber, times(1)).onNext(Arrays.asList("one", "two", "three")); + verify(subscriber, Mockito.never()).onError(any(Throwable.class)); + verify(subscriber, times(1)).onComplete(); + } + + @Test + public void listMultipleSubscribersFlowable() { + Flowable<String> w = Flowable.fromIterable(Arrays.asList("one", "two", "three")); + Flowable<List<String>> flowable = w.toList().toFlowable(); + + Subscriber<List<String>> subscriber1 = TestHelper.mockSubscriber(); + flowable.subscribe(subscriber1); + + Subscriber<List<String>> subscriber2 = TestHelper.mockSubscriber(); + flowable.subscribe(subscriber2); + + List<String> expected = Arrays.asList("one", "two", "three"); + + verify(subscriber1, times(1)).onNext(expected); + verify(subscriber1, Mockito.never()).onError(any(Throwable.class)); + verify(subscriber1, times(1)).onComplete(); + + verify(subscriber2, times(1)).onNext(expected); + verify(subscriber2, Mockito.never()).onError(any(Throwable.class)); + verify(subscriber2, times(1)).onComplete(); + } + + @Test + public void listWithBlockingFirstFlowable() { + Flowable<String> f = Flowable.fromIterable(Arrays.asList("one", "two", "three")); + List<String> actual = f.toList().toFlowable().blockingFirst(); + Assert.assertEquals(Arrays.asList("one", "two", "three"), actual); + } + + @Test + public void backpressureHonoredFlowable() { + Flowable<List<Integer>> w = Flowable.just(1, 2, 3, 4, 5).toList().toFlowable(); + TestSubscriber<List<Integer>> ts = new TestSubscriber<>(0L); + + w.subscribe(ts); + + ts.assertNoValues(); + ts.assertNoErrors(); + ts.assertNotComplete(); + + ts.request(1); + + ts.assertValue(Arrays.asList(1, 2, 3, 4, 5)); + ts.assertNoErrors(); + ts.assertComplete(); + + ts.request(1); + + ts.assertValue(Arrays.asList(1, 2, 3, 4, 5)); + ts.assertNoErrors(); + ts.assertComplete(); + } + + @Test + public void capacityHintFlowable() { + Flowable.range(1, 10) + .toList(4) + .toFlowable() + .test() + .assertResult(Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)); + } + + @Test + public void list() { + Flowable<String> w = Flowable.fromIterable(Arrays.asList("one", "two", "three")); + Single<List<String>> single = w.toList(); + + SingleObserver<List<String>> observer = TestHelper.mockSingleObserver(); + single.subscribe(observer); + verify(observer, times(1)).onSuccess(Arrays.asList("one", "two", "three")); + verify(observer, Mockito.never()).onError(any(Throwable.class)); + } + + @Test + public void listViaFlowable() { + Flowable<String> w = Flowable.fromIterable(Arrays.asList("one", "two", "three")); + Single<List<String>> single = w.toList(); + + SingleObserver<List<String>> observer = TestHelper.mockSingleObserver(); + single.subscribe(observer); + verify(observer, times(1)).onSuccess(Arrays.asList("one", "two", "three")); + verify(observer, Mockito.never()).onError(any(Throwable.class)); + } + + @Test + public void listMultipleSubscribers() { + Flowable<String> w = Flowable.fromIterable(Arrays.asList("one", "two", "three")); + Single<List<String>> single = w.toList(); + + SingleObserver<List<String>> o1 = TestHelper.mockSingleObserver(); + single.subscribe(o1); + + SingleObserver<List<String>> o2 = TestHelper.mockSingleObserver(); + single.subscribe(o2); + + List<String> expected = Arrays.asList("one", "two", "three"); + + verify(o1, times(1)).onSuccess(expected); + verify(o1, Mockito.never()).onError(any(Throwable.class)); + + verify(o2, times(1)).onSuccess(expected); + verify(o2, Mockito.never()).onError(any(Throwable.class)); + } + + @Test + public void listWithBlockingFirst() { + Flowable<String> f = Flowable.fromIterable(Arrays.asList("one", "two", "three")); + List<String> actual = f.toList().blockingGet(); + Assert.assertEquals(Arrays.asList("one", "two", "three"), actual); + } + + static void await(CyclicBarrier cb) { + try { + cb.await(); + } catch (InterruptedException ex) { + ex.printStackTrace(); + } catch (BrokenBarrierException ex) { + ex.printStackTrace(); + } + } + + @Test + public void capacityHint() { + Flowable.range(1, 10) + .toList(4) + .test() + .assertResult(Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)); + } + + @Test + public void dispose() { + TestHelper.checkDisposed(Flowable.just(1).toList().toFlowable()); + + TestHelper.checkDisposed(Flowable.just(1).toList()); + } + + @Test + public void error() { + Flowable.error(new TestException()) + .toList() + .toFlowable() + .test() + .assertFailure(TestException.class); + } + + @Test + public void errorSingle() { + Flowable.error(new TestException()) + .toList() + .test() + .assertFailure(TestException.class); + } + + @Test + public void collectionSupplierThrows() { + Flowable.just(1) + .toList(new Supplier<Collection<Integer>>() { + @Override + public Collection<Integer> get() throws Exception { + throw new TestException(); + } + }) + .toFlowable() + .test() + .assertFailure(TestException.class); + } + + @Test + public void collectionSupplierReturnsNull() { + Flowable.just(1) + .toList(new Supplier<Collection<Integer>>() { + @Override + public Collection<Integer> get() throws Exception { + return null; + } + }) + .toFlowable() + .to(TestHelper.<Collection<Integer>>testConsumer()) + .assertFailure(NullPointerException.class) + .assertErrorMessage(ExceptionHelper.nullWarning("The collectionSupplier returned a null Collection.")); + } + + @Test + public void singleCollectionSupplierThrows() { + Flowable.just(1) + .toList(new Supplier<Collection<Integer>>() { + @Override + public Collection<Integer> get() throws Exception { + throw new TestException(); + } + }) + .test() + .assertFailure(TestException.class); + } + + @Test + public void singleCollectionSupplierReturnsNull() { + Flowable.just(1) + .toList(new Supplier<Collection<Integer>>() { + @Override + public Collection<Integer> get() throws Exception { + return null; + } + }) + .to(TestHelper.<Collection<Integer>>testConsumer()) + .assertFailure(NullPointerException.class) + .assertErrorMessage(ExceptionHelper.nullWarning("The collectionSupplier returned a null Collection.")); + } + + @Test + public void onNextCancelRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final PublishProcessor<Integer> pp = PublishProcessor.create(); + final TestObserver<List<Integer>> to = pp.toList().test(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + pp.onNext(1); + } + }; + Runnable r2 = new Runnable() { + @Override + public void run() { + to.dispose(); + } + }; + + TestHelper.race(r1, r2); + } + } + + @Test + public void onNextCancelRaceFlowable() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final PublishProcessor<Integer> pp = PublishProcessor.create(); + final TestSubscriber<List<Integer>> ts = pp.toList().toFlowable().test(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + pp.onNext(1); + } + }; + Runnable r2 = new Runnable() { + @Override + public void run() { + ts.cancel(); + } + }; + + TestHelper.race(r1, r2); + } + + } + + @Test + public void onCompleteCancelRaceFlowable() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final PublishProcessor<Integer> pp = PublishProcessor.create(); + final TestSubscriber<List<Integer>> ts = pp.toList().toFlowable().test(); + + pp.onNext(1); + + Runnable r1 = new Runnable() { + @Override + public void run() { + pp.onComplete(); + } + }; + Runnable r2 = new Runnable() { + @Override + public void run() { + ts.cancel(); + } + }; + + TestHelper.race(r1, r2); + + if (ts.values().size() != 0) { + ts.assertValue(Arrays.asList(1)) + .assertNoErrors(); + } + } + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeFlowable(new Function<Flowable<Object>, Flowable<List<Object>>>() { + @Override + public Flowable<List<Object>> apply(Flowable<Object> f) + throws Exception { + return f.toList().toFlowable(); + } + }); + TestHelper.checkDoubleOnSubscribeFlowableToSingle(new Function<Flowable<Object>, Single<List<Object>>>() { + @Override + public Single<List<Object>> apply(Flowable<Object> f) + throws Exception { + return f.toList(); + } + }); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableToMapTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableToMapTest.java new file mode 100644 index 0000000000..736cf4caa5 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableToMapTest.java @@ -0,0 +1,399 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import java.util.*; + +import org.junit.*; +import org.reactivestreams.Subscriber; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class FlowableToMapTest extends RxJavaTest { + Subscriber<Object> objectSubscriber; + SingleObserver<Object> singleObserver; + + @Before + public void before() { + objectSubscriber = TestHelper.mockSubscriber(); + singleObserver = TestHelper.mockSingleObserver(); + } + + Function<String, Integer> lengthFunc = new Function<String, Integer>() { + @Override + public Integer apply(String t1) { + return t1.length(); + } + }; + Function<String, String> duplicate = new Function<String, String>() { + @Override + public String apply(String t1) { + return t1 + t1; + } + }; + + @Test + public void toMapFlowable() { + Flowable<String> source = Flowable.just("a", "bb", "ccc", "dddd"); + + Flowable<Map<Integer, String>> mapped = source.toMap(lengthFunc).toFlowable(); + + Map<Integer, String> expected = new HashMap<>(); + expected.put(1, "a"); + expected.put(2, "bb"); + expected.put(3, "ccc"); + expected.put(4, "dddd"); + + mapped.subscribe(objectSubscriber); + + verify(objectSubscriber, never()).onError(any(Throwable.class)); + verify(objectSubscriber, times(1)).onNext(expected); + verify(objectSubscriber, times(1)).onComplete(); + } + + @Test + public void toMapWithValueSelectorFlowable() { + Flowable<String> source = Flowable.just("a", "bb", "ccc", "dddd"); + + Flowable<Map<Integer, String>> mapped = source.toMap(lengthFunc, duplicate).toFlowable(); + + Map<Integer, String> expected = new HashMap<>(); + expected.put(1, "aa"); + expected.put(2, "bbbb"); + expected.put(3, "cccccc"); + expected.put(4, "dddddddd"); + + mapped.subscribe(objectSubscriber); + + verify(objectSubscriber, never()).onError(any(Throwable.class)); + verify(objectSubscriber, times(1)).onNext(expected); + verify(objectSubscriber, times(1)).onComplete(); + } + + @Test + public void toMapWithErrorFlowable() { + Flowable<String> source = Flowable.just("a", "bb", "ccc", "dddd"); + + Function<String, Integer> lengthFuncErr = new Function<String, Integer>() { + @Override + public Integer apply(String t1) { + if ("bb".equals(t1)) { + throw new RuntimeException("Forced Failure"); + } + return t1.length(); + } + }; + Flowable<Map<Integer, String>> mapped = source.toMap(lengthFuncErr).toFlowable(); + + Map<Integer, String> expected = new HashMap<>(); + expected.put(1, "a"); + expected.put(2, "bb"); + expected.put(3, "ccc"); + expected.put(4, "dddd"); + + mapped.subscribe(objectSubscriber); + + verify(objectSubscriber, never()).onNext(expected); + verify(objectSubscriber, never()).onComplete(); + verify(objectSubscriber, times(1)).onError(any(Throwable.class)); + + } + + @Test + public void toMapWithErrorInValueSelectorFlowable() { + Flowable<String> source = Flowable.just("a", "bb", "ccc", "dddd"); + + Function<String, String> duplicateErr = new Function<String, String>() { + @Override + public String apply(String t1) { + if ("bb".equals(t1)) { + throw new RuntimeException("Forced failure"); + } + return t1 + t1; + } + }; + + Flowable<Map<Integer, String>> mapped = source.toMap(lengthFunc, duplicateErr).toFlowable(); + + Map<Integer, String> expected = new HashMap<>(); + expected.put(1, "aa"); + expected.put(2, "bbbb"); + expected.put(3, "cccccc"); + expected.put(4, "dddddddd"); + + mapped.subscribe(objectSubscriber); + + verify(objectSubscriber, never()).onNext(expected); + verify(objectSubscriber, never()).onComplete(); + verify(objectSubscriber, times(1)).onError(any(Throwable.class)); + + } + + @Test + public void toMapWithFactoryFlowable() { + Flowable<String> source = Flowable.just("a", "bb", "ccc", "dddd"); + + Supplier<Map<Integer, String>> mapFactory = new Supplier<Map<Integer, String>>() { + @Override + public Map<Integer, String> get() { + return new LinkedHashMap<Integer, String>() { + + private static final long serialVersionUID = -3296811238780863394L; + + @Override + protected boolean removeEldestEntry(Map.Entry<Integer, String> eldest) { + return size() > 3; + } + }; + } + }; + + Function<String, Integer> lengthFunc = new Function<String, Integer>() { + @Override + public Integer apply(String t1) { + return t1.length(); + } + }; + Flowable<Map<Integer, String>> mapped = source.toMap(lengthFunc, new Function<String, String>() { + @Override + public String apply(String v) { + return v; + } + }, mapFactory).toFlowable(); + + Map<Integer, String> expected = new LinkedHashMap<>(); + expected.put(2, "bb"); + expected.put(3, "ccc"); + expected.put(4, "dddd"); + + mapped.subscribe(objectSubscriber); + + verify(objectSubscriber, never()).onError(any(Throwable.class)); + verify(objectSubscriber, times(1)).onNext(expected); + verify(objectSubscriber, times(1)).onComplete(); + } + + @Test + public void toMapWithErrorThrowingFactoryFlowable() { + Flowable<String> source = Flowable.just("a", "bb", "ccc", "dddd"); + + Supplier<Map<Integer, String>> mapFactory = new Supplier<Map<Integer, String>>() { + @Override + public Map<Integer, String> get() { + throw new RuntimeException("Forced failure"); + } + }; + + Function<String, Integer> lengthFunc = new Function<String, Integer>() { + @Override + public Integer apply(String t1) { + return t1.length(); + } + }; + Flowable<Map<Integer, String>> mapped = source.toMap(lengthFunc, new Function<String, String>() { + @Override + public String apply(String v) { + return v; + } + }, mapFactory).toFlowable(); + + Map<Integer, String> expected = new LinkedHashMap<>(); + expected.put(2, "bb"); + expected.put(3, "ccc"); + expected.put(4, "dddd"); + + mapped.subscribe(objectSubscriber); + + verify(objectSubscriber, never()).onNext(expected); + verify(objectSubscriber, never()).onComplete(); + verify(objectSubscriber, times(1)).onError(any(Throwable.class)); + } + + @Test + public void toMap() { + Flowable<String> source = Flowable.just("a", "bb", "ccc", "dddd"); + + Single<Map<Integer, String>> mapped = source.toMap(lengthFunc); + + Map<Integer, String> expected = new HashMap<>(); + expected.put(1, "a"); + expected.put(2, "bb"); + expected.put(3, "ccc"); + expected.put(4, "dddd"); + + mapped.subscribe(singleObserver); + + verify(singleObserver, never()).onError(any(Throwable.class)); + verify(singleObserver, times(1)).onSuccess(expected); + } + + @Test + public void toMapWithValueSelector() { + Flowable<String> source = Flowable.just("a", "bb", "ccc", "dddd"); + + Single<Map<Integer, String>> mapped = source.toMap(lengthFunc, duplicate); + + Map<Integer, String> expected = new HashMap<>(); + expected.put(1, "aa"); + expected.put(2, "bbbb"); + expected.put(3, "cccccc"); + expected.put(4, "dddddddd"); + + mapped.subscribe(singleObserver); + + verify(singleObserver, never()).onError(any(Throwable.class)); + verify(singleObserver, times(1)).onSuccess(expected); + } + + @Test + public void toMapWithError() { + Flowable<String> source = Flowable.just("a", "bb", "ccc", "dddd"); + + Function<String, Integer> lengthFuncErr = new Function<String, Integer>() { + @Override + public Integer apply(String t1) { + if ("bb".equals(t1)) { + throw new RuntimeException("Forced Failure"); + } + return t1.length(); + } + }; + Single<Map<Integer, String>> mapped = source.toMap(lengthFuncErr); + + Map<Integer, String> expected = new HashMap<>(); + expected.put(1, "a"); + expected.put(2, "bb"); + expected.put(3, "ccc"); + expected.put(4, "dddd"); + + mapped.subscribe(singleObserver); + + verify(singleObserver, never()).onSuccess(expected); + verify(singleObserver, times(1)).onError(any(Throwable.class)); + + } + + @Test + public void toMapWithErrorInValueSelector() { + Flowable<String> source = Flowable.just("a", "bb", "ccc", "dddd"); + + Function<String, String> duplicateErr = new Function<String, String>() { + @Override + public String apply(String t1) { + if ("bb".equals(t1)) { + throw new RuntimeException("Forced failure"); + } + return t1 + t1; + } + }; + + Single<Map<Integer, String>> mapped = source.toMap(lengthFunc, duplicateErr); + + Map<Integer, String> expected = new HashMap<>(); + expected.put(1, "aa"); + expected.put(2, "bbbb"); + expected.put(3, "cccccc"); + expected.put(4, "dddddddd"); + + mapped.subscribe(singleObserver); + + verify(singleObserver, never()).onSuccess(expected); + verify(singleObserver, times(1)).onError(any(Throwable.class)); + + } + + @Test + public void toMapWithFactory() { + Flowable<String> source = Flowable.just("a", "bb", "ccc", "dddd"); + + Supplier<Map<Integer, String>> mapFactory = new Supplier<Map<Integer, String>>() { + @Override + public Map<Integer, String> get() { + return new LinkedHashMap<Integer, String>() { + + private static final long serialVersionUID = -3296811238780863394L; + + @Override + protected boolean removeEldestEntry(Map.Entry<Integer, String> eldest) { + return size() > 3; + } + }; + } + }; + + Function<String, Integer> lengthFunc = new Function<String, Integer>() { + @Override + public Integer apply(String t1) { + return t1.length(); + } + }; + Single<Map<Integer, String>> mapped = source.toMap(lengthFunc, new Function<String, String>() { + @Override + public String apply(String v) { + return v; + } + }, mapFactory); + + Map<Integer, String> expected = new LinkedHashMap<>(); + expected.put(2, "bb"); + expected.put(3, "ccc"); + expected.put(4, "dddd"); + + mapped.subscribe(singleObserver); + + verify(singleObserver, never()).onError(any(Throwable.class)); + verify(singleObserver, times(1)).onSuccess(expected); + } + + @Test + public void toMapWithErrorThrowingFactory() { + Flowable<String> source = Flowable.just("a", "bb", "ccc", "dddd"); + + Supplier<Map<Integer, String>> mapFactory = new Supplier<Map<Integer, String>>() { + @Override + public Map<Integer, String> get() { + throw new RuntimeException("Forced failure"); + } + }; + + Function<String, Integer> lengthFunc = new Function<String, Integer>() { + @Override + public Integer apply(String t1) { + return t1.length(); + } + }; + Single<Map<Integer, String>> mapped = source.toMap(lengthFunc, new Function<String, String>() { + @Override + public String apply(String v) { + return v; + } + }, mapFactory); + + Map<Integer, String> expected = new LinkedHashMap<>(); + expected.put(2, "bb"); + expected.put(3, "ccc"); + expected.put(4, "dddd"); + + mapped.subscribe(singleObserver); + + verify(singleObserver, never()).onSuccess(expected); + verify(singleObserver, times(1)).onError(any(Throwable.class)); + } + +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableToMultimapTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableToMultimapTest.java new file mode 100644 index 0000000000..63db446ee6 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableToMultimapTest.java @@ -0,0 +1,540 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import java.util.*; + +import org.junit.*; +import org.reactivestreams.Subscriber; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class FlowableToMultimapTest extends RxJavaTest { + Subscriber<Object> objectSubscriber; + + SingleObserver<Object> singleObserver; + + @Before + public void before() { + objectSubscriber = TestHelper.mockSubscriber(); + singleObserver = TestHelper.mockSingleObserver(); + } + + Function<String, Integer> lengthFunc = new Function<String, Integer>() { + @Override + public Integer apply(String t1) { + return t1.length(); + } + }; + Function<String, String> duplicate = new Function<String, String>() { + @Override + public String apply(String t1) { + return t1 + t1; + } + }; + + @Test + public void toMultimapFlowable() { + Flowable<String> source = Flowable.just("a", "b", "cc", "dd"); + + Flowable<Map<Integer, Collection<String>>> mapped = source.toMultimap(lengthFunc).toFlowable(); + + Map<Integer, Collection<String>> expected = new HashMap<>(); + expected.put(1, Arrays.asList("a", "b")); + expected.put(2, Arrays.asList("cc", "dd")); + + mapped.subscribe(objectSubscriber); + + verify(objectSubscriber, never()).onError(any(Throwable.class)); + verify(objectSubscriber, times(1)).onNext(expected); + verify(objectSubscriber, times(1)).onComplete(); + } + + @Test + public void toMultimapWithValueSelectorFlowable() { + Flowable<String> source = Flowable.just("a", "b", "cc", "dd"); + + Flowable<Map<Integer, Collection<String>>> mapped = source.toMultimap(lengthFunc, duplicate).toFlowable(); + + Map<Integer, Collection<String>> expected = new HashMap<>(); + expected.put(1, Arrays.asList("aa", "bb")); + expected.put(2, Arrays.asList("cccc", "dddd")); + + mapped.subscribe(objectSubscriber); + + verify(objectSubscriber, never()).onError(any(Throwable.class)); + verify(objectSubscriber, times(1)).onNext(expected); + verify(objectSubscriber, times(1)).onComplete(); + } + + @Test + public void toMultimapWithMapFactoryFlowable() { + Flowable<String> source = Flowable.just("a", "b", "cc", "dd", "eee", "fff"); + + Supplier<Map<Integer, Collection<String>>> mapFactory = new Supplier<Map<Integer, Collection<String>>>() { + @Override + public Map<Integer, Collection<String>> get() { + return new LinkedHashMap<Integer, Collection<String>>() { + + private static final long serialVersionUID = -2084477070717362859L; + + @Override + protected boolean removeEldestEntry(Map.Entry<Integer, Collection<String>> eldest) { + return size() > 2; + } + }; + } + }; + + Function<String, String> identity = new Function<String, String>() { + @Override + public String apply(String v) { + return v; + } + }; + + Flowable<Map<Integer, Collection<String>>> mapped = source.toMultimap( + lengthFunc, identity, + mapFactory, new Function<Integer, Collection<String>>() { + @Override + public Collection<String> apply(Integer e) { + return new ArrayList<>(); + } + }).toFlowable(); + + Map<Integer, Collection<String>> expected = new HashMap<>(); + expected.put(2, Arrays.asList("cc", "dd")); + expected.put(3, Arrays.asList("eee", "fff")); + + mapped.subscribe(objectSubscriber); + + verify(objectSubscriber, never()).onError(any(Throwable.class)); + verify(objectSubscriber, times(1)).onNext(expected); + verify(objectSubscriber, times(1)).onComplete(); + } + + @Test + public void toMultimapWithCollectionFactoryFlowable() { + Flowable<String> source = Flowable.just("cc", "dd", "eee", "eee"); + + Function<Integer, Collection<String>> collectionFactory = new Function<Integer, Collection<String>>() { + @Override + public Collection<String> apply(Integer t1) { + if (t1 == 2) { + return new ArrayList<>(); + } else { + return new HashSet<>(); + } + } + }; + + Function<String, String> identity = new Function<String, String>() { + @Override + public String apply(String v) { + return v; + } + }; + Supplier<Map<Integer, Collection<String>>> mapSupplier = new Supplier<Map<Integer, Collection<String>>>() { + @Override + public Map<Integer, Collection<String>> get() { + return new HashMap<>(); + } + }; + + Flowable<Map<Integer, Collection<String>>> mapped = source + .toMultimap(lengthFunc, identity, mapSupplier, collectionFactory).toFlowable(); + + Map<Integer, Collection<String>> expected = new HashMap<>(); + expected.put(2, Arrays.asList("cc", "dd")); + expected.put(3, new HashSet<>(Arrays.asList("eee"))); + + mapped.subscribe(objectSubscriber); + + verify(objectSubscriber, never()).onError(any(Throwable.class)); + verify(objectSubscriber, times(1)).onNext(expected); + verify(objectSubscriber, times(1)).onComplete(); + } + + @Test + public void toMultimapWithErrorFlowable() { + Flowable<String> source = Flowable.just("a", "b", "cc", "dd"); + + Function<String, Integer> lengthFuncErr = new Function<String, Integer>() { + @Override + public Integer apply(String t1) { + if ("b".equals(t1)) { + throw new RuntimeException("Forced Failure"); + } + return t1.length(); + } + }; + + Flowable<Map<Integer, Collection<String>>> mapped = source.toMultimap(lengthFuncErr).toFlowable(); + + Map<Integer, Collection<String>> expected = new HashMap<>(); + expected.put(1, Arrays.asList("a", "b")); + expected.put(2, Arrays.asList("cc", "dd")); + + mapped.subscribe(objectSubscriber); + + verify(objectSubscriber, times(1)).onError(any(Throwable.class)); + verify(objectSubscriber, never()).onNext(expected); + verify(objectSubscriber, never()).onComplete(); + } + + @Test + public void toMultimapWithErrorInValueSelectorFlowable() { + Flowable<String> source = Flowable.just("a", "b", "cc", "dd"); + + Function<String, String> duplicateErr = new Function<String, String>() { + @Override + public String apply(String t1) { + if ("b".equals(t1)) { + throw new RuntimeException("Forced failure"); + } + return t1 + t1; + } + }; + + Flowable<Map<Integer, Collection<String>>> mapped = source.toMultimap(lengthFunc, duplicateErr).toFlowable(); + + Map<Integer, Collection<String>> expected = new HashMap<>(); + expected.put(1, Arrays.asList("aa", "bb")); + expected.put(2, Arrays.asList("cccc", "dddd")); + + mapped.subscribe(objectSubscriber); + + verify(objectSubscriber, times(1)).onError(any(Throwable.class)); + verify(objectSubscriber, never()).onNext(expected); + verify(objectSubscriber, never()).onComplete(); + } + + @Test + public void toMultimapWithMapThrowingFactoryFlowable() { + Flowable<String> source = Flowable.just("a", "b", "cc", "dd", "eee", "fff"); + + Supplier<Map<Integer, Collection<String>>> mapFactory = new Supplier<Map<Integer, Collection<String>>>() { + @Override + public Map<Integer, Collection<String>> get() { + throw new RuntimeException("Forced failure"); + } + }; + + Flowable<Map<Integer, Collection<String>>> mapped = source + .toMultimap(lengthFunc, new Function<String, String>() { + @Override + public String apply(String v) { + return v; + } + }, mapFactory).toFlowable(); + + Map<Integer, Collection<String>> expected = new HashMap<>(); + expected.put(2, Arrays.asList("cc", "dd")); + expected.put(3, Arrays.asList("eee", "fff")); + + mapped.subscribe(objectSubscriber); + + verify(objectSubscriber, times(1)).onError(any(Throwable.class)); + verify(objectSubscriber, never()).onNext(expected); + verify(objectSubscriber, never()).onComplete(); + } + + @Test + public void toMultimapWithThrowingCollectionFactoryFlowable() { + Flowable<String> source = Flowable.just("cc", "cc", "eee", "eee"); + + Function<Integer, Collection<String>> collectionFactory = new Function<Integer, Collection<String>>() { + @Override + public Collection<String> apply(Integer t1) { + if (t1 == 2) { + throw new RuntimeException("Forced failure"); + } else { + return new HashSet<>(); + } + } + }; + + Function<String, String> identity = new Function<String, String>() { + @Override + public String apply(String v) { + return v; + } + }; + Supplier<Map<Integer, Collection<String>>> mapSupplier = new Supplier<Map<Integer, Collection<String>>>() { + @Override + public Map<Integer, Collection<String>> get() { + return new HashMap<>(); + } + }; + + Flowable<Map<Integer, Collection<String>>> mapped = source.toMultimap(lengthFunc, + identity, mapSupplier, collectionFactory).toFlowable(); + + Map<Integer, Collection<String>> expected = new HashMap<>(); + expected.put(2, Arrays.asList("cc", "dd")); + expected.put(3, Collections.singleton("eee")); + + mapped.subscribe(objectSubscriber); + + verify(objectSubscriber, times(1)).onError(any(Throwable.class)); + verify(objectSubscriber, never()).onNext(expected); + verify(objectSubscriber, never()).onComplete(); + } + + @Test + public void toMultimap() { + Flowable<String> source = Flowable.just("a", "b", "cc", "dd"); + + Single<Map<Integer, Collection<String>>> mapped = source.toMultimap(lengthFunc); + + Map<Integer, Collection<String>> expected = new HashMap<>(); + expected.put(1, Arrays.asList("a", "b")); + expected.put(2, Arrays.asList("cc", "dd")); + + mapped.subscribe(singleObserver); + + verify(singleObserver, never()).onError(any(Throwable.class)); + verify(singleObserver, times(1)).onSuccess(expected); + } + + @Test + public void toMultimapWithValueSelector() { + Flowable<String> source = Flowable.just("a", "b", "cc", "dd"); + + Single<Map<Integer, Collection<String>>> mapped = source.toMultimap(lengthFunc, duplicate); + + Map<Integer, Collection<String>> expected = new HashMap<>(); + expected.put(1, Arrays.asList("aa", "bb")); + expected.put(2, Arrays.asList("cccc", "dddd")); + + mapped.subscribe(singleObserver); + + verify(singleObserver, never()).onError(any(Throwable.class)); + verify(singleObserver, times(1)).onSuccess(expected); + } + + @Test + public void toMultimapWithMapFactory() { + Flowable<String> source = Flowable.just("a", "b", "cc", "dd", "eee", "fff"); + + Supplier<Map<Integer, Collection<String>>> mapFactory = new Supplier<Map<Integer, Collection<String>>>() { + @Override + public Map<Integer, Collection<String>> get() { + return new LinkedHashMap<Integer, Collection<String>>() { + + private static final long serialVersionUID = -2084477070717362859L; + + @Override + protected boolean removeEldestEntry(Map.Entry<Integer, Collection<String>> eldest) { + return size() > 2; + } + }; + } + }; + + Function<String, String> identity = new Function<String, String>() { + @Override + public String apply(String v) { + return v; + } + }; + + Single<Map<Integer, Collection<String>>> mapped = source.toMultimap( + lengthFunc, identity, + mapFactory, new Function<Integer, Collection<String>>() { + @Override + public Collection<String> apply(Integer e) { + return new ArrayList<>(); + } + }); + + Map<Integer, Collection<String>> expected = new HashMap<>(); + expected.put(2, Arrays.asList("cc", "dd")); + expected.put(3, Arrays.asList("eee", "fff")); + + mapped.subscribe(singleObserver); + + verify(singleObserver, never()).onError(any(Throwable.class)); + verify(singleObserver, times(1)).onSuccess(expected); + } + + @Test + public void toMultimapWithCollectionFactory() { + Flowable<String> source = Flowable.just("cc", "dd", "eee", "eee"); + + Function<Integer, Collection<String>> collectionFactory = new Function<Integer, Collection<String>>() { + @Override + public Collection<String> apply(Integer t1) { + if (t1 == 2) { + return new ArrayList<>(); + } else { + return new HashSet<>(); + } + } + }; + + Function<String, String> identity = new Function<String, String>() { + @Override + public String apply(String v) { + return v; + } + }; + Supplier<Map<Integer, Collection<String>>> mapSupplier = new Supplier<Map<Integer, Collection<String>>>() { + @Override + public Map<Integer, Collection<String>> get() { + return new HashMap<>(); + } + }; + + Single<Map<Integer, Collection<String>>> mapped = source + .toMultimap(lengthFunc, identity, mapSupplier, collectionFactory); + + Map<Integer, Collection<String>> expected = new HashMap<>(); + expected.put(2, Arrays.asList("cc", "dd")); + expected.put(3, new HashSet<>(Arrays.asList("eee"))); + + mapped.subscribe(singleObserver); + + verify(singleObserver, never()).onError(any(Throwable.class)); + verify(singleObserver, times(1)).onSuccess(expected); + } + + @Test + public void toMultimapWithError() { + Flowable<String> source = Flowable.just("a", "b", "cc", "dd"); + + Function<String, Integer> lengthFuncErr = new Function<String, Integer>() { + @Override + public Integer apply(String t1) { + if ("b".equals(t1)) { + throw new RuntimeException("Forced Failure"); + } + return t1.length(); + } + }; + + Single<Map<Integer, Collection<String>>> mapped = source.toMultimap(lengthFuncErr); + + Map<Integer, Collection<String>> expected = new HashMap<>(); + expected.put(1, Arrays.asList("a", "b")); + expected.put(2, Arrays.asList("cc", "dd")); + + mapped.subscribe(singleObserver); + + verify(singleObserver, times(1)).onError(any(Throwable.class)); + verify(singleObserver, never()).onSuccess(expected); + } + + @Test + public void toMultimapWithErrorInValueSelector() { + Flowable<String> source = Flowable.just("a", "b", "cc", "dd"); + + Function<String, String> duplicateErr = new Function<String, String>() { + @Override + public String apply(String t1) { + if ("b".equals(t1)) { + throw new RuntimeException("Forced failure"); + } + return t1 + t1; + } + }; + + Single<Map<Integer, Collection<String>>> mapped = source.toMultimap(lengthFunc, duplicateErr); + + Map<Integer, Collection<String>> expected = new HashMap<>(); + expected.put(1, Arrays.asList("aa", "bb")); + expected.put(2, Arrays.asList("cccc", "dddd")); + + mapped.subscribe(singleObserver); + + verify(singleObserver, times(1)).onError(any(Throwable.class)); + verify(singleObserver, never()).onSuccess(expected); + } + + @Test + public void toMultimapWithMapThrowingFactory() { + Flowable<String> source = Flowable.just("a", "b", "cc", "dd", "eee", "fff"); + + Supplier<Map<Integer, Collection<String>>> mapFactory = new Supplier<Map<Integer, Collection<String>>>() { + @Override + public Map<Integer, Collection<String>> get() { + throw new RuntimeException("Forced failure"); + } + }; + + Single<Map<Integer, Collection<String>>> mapped = source + .toMultimap(lengthFunc, new Function<String, String>() { + @Override + public String apply(String v) { + return v; + } + }, mapFactory); + + Map<Integer, Collection<String>> expected = new HashMap<>(); + expected.put(2, Arrays.asList("cc", "dd")); + expected.put(3, Arrays.asList("eee", "fff")); + + mapped.subscribe(singleObserver); + + verify(singleObserver, times(1)).onError(any(Throwable.class)); + verify(singleObserver, never()).onSuccess(expected); + } + + @Test + public void toMultimapWithThrowingCollectionFactory() { + Flowable<String> source = Flowable.just("cc", "cc", "eee", "eee"); + + Function<Integer, Collection<String>> collectionFactory = new Function<Integer, Collection<String>>() { + @Override + public Collection<String> apply(Integer t1) { + if (t1 == 2) { + throw new RuntimeException("Forced failure"); + } else { + return new HashSet<>(); + } + } + }; + + Function<String, String> identity = new Function<String, String>() { + @Override + public String apply(String v) { + return v; + } + }; + Supplier<Map<Integer, Collection<String>>> mapSupplier = new Supplier<Map<Integer, Collection<String>>>() { + @Override + public Map<Integer, Collection<String>> get() { + return new HashMap<>(); + } + }; + + Single<Map<Integer, Collection<String>>> mapped = source.toMultimap(lengthFunc, + identity, mapSupplier, collectionFactory); + + Map<Integer, Collection<String>> expected = new HashMap<>(); + expected.put(2, Arrays.asList("cc", "dd")); + expected.put(3, Collections.singleton("eee")); + + mapped.subscribe(singleObserver); + + verify(singleObserver, times(1)).onError(any(Throwable.class)); + verify(singleObserver, never()).onSuccess(expected); + } + +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableToSingleTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableToSingleTest.java new file mode 100644 index 0000000000..d064589b1d --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableToSingleTest.java @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import java.util.concurrent.atomic.AtomicBoolean; + +import org.junit.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.functions.Action; +import io.reactivex.rxjava3.subscribers.TestSubscriber; + +public class FlowableToSingleTest extends RxJavaTest { + + @Test + public void justSingleItemObservable() { + TestSubscriber<String> subscriber = TestSubscriber.create(); + Single<String> single = Flowable.just("Hello World!").single(""); + single.toFlowable().subscribe(subscriber); + + subscriber.assertResult("Hello World!"); + } + + @Test + public void errorObservable() { + TestSubscriber<String> subscriber = TestSubscriber.create(); + IllegalArgumentException error = new IllegalArgumentException("Error"); + Single<String> single = Flowable.<String>error(error).single(""); + single.toFlowable().subscribe(subscriber); + + subscriber.assertError(error); + } + + @Test + public void justTwoEmissionsObservableThrowsError() { + TestSubscriber<String> subscriber = TestSubscriber.create(); + Single<String> single = Flowable.just("First", "Second").single(""); + single.toFlowable().subscribe(subscriber); + + subscriber.assertError(IllegalArgumentException.class); + } + + @Test + public void emptyObservable() { + TestSubscriber<String> subscriber = TestSubscriber.create(); + Single<String> single = Flowable.<String>empty().single(""); + single.toFlowable().subscribe(subscriber); + + subscriber.assertResult(""); + } + + @Test + public void repeatObservableThrowsError() { + TestSubscriber<String> subscriber = TestSubscriber.create(); + Single<String> single = Flowable.just("First", "Second").repeat().single(""); + single.toFlowable().subscribe(subscriber); + + subscriber.assertError(IllegalArgumentException.class); + } + + @Test + public void shouldUseUnsafeSubscribeInternallyNotSubscribe() { + TestSubscriber<String> subscriber = TestSubscriber.create(); + final AtomicBoolean unsubscribed = new AtomicBoolean(false); + Single<String> single = Flowable.just("Hello World!").doOnCancel(new Action() { + + @Override + public void run() { + unsubscribed.set(true); + }}).single(""); + single.toFlowable().subscribe(subscriber); + subscriber.assertComplete(); + Assert.assertFalse(unsubscribed.get()); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableToSortedListTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableToSortedListTest.java new file mode 100644 index 0000000000..cadcb572b6 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableToSortedListTest.java @@ -0,0 +1,197 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import java.util.*; +import java.util.concurrent.*; + +import org.junit.Test; +import org.mockito.Mockito; +import org.reactivestreams.Subscriber; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.subscribers.TestSubscriber; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class FlowableToSortedListTest extends RxJavaTest { + + @Test + public void sortedListFlowable() { + Flowable<Integer> w = Flowable.just(1, 3, 2, 5, 4); + Flowable<List<Integer>> flowable = w.toSortedList().toFlowable(); + + Subscriber<List<Integer>> subscriber = TestHelper.mockSubscriber(); + flowable.subscribe(subscriber); + + verify(subscriber, times(1)).onNext(Arrays.asList(1, 2, 3, 4, 5)); + verify(subscriber, Mockito.never()).onError(any(Throwable.class)); + verify(subscriber, times(1)).onComplete(); + } + + @Test + public void sortedListWithCustomFunctionFlowable() { + Flowable<Integer> w = Flowable.just(1, 3, 2, 5, 4); + Flowable<List<Integer>> flowable = w.toSortedList(new Comparator<Integer>() { + + @Override + public int compare(Integer t1, Integer t2) { + return t2 - t1; + } + + }).toFlowable(); + + Subscriber<List<Integer>> subscriber = TestHelper.mockSubscriber(); + flowable.subscribe(subscriber); + + verify(subscriber, times(1)).onNext(Arrays.asList(5, 4, 3, 2, 1)); + verify(subscriber, Mockito.never()).onError(any(Throwable.class)); + verify(subscriber, times(1)).onComplete(); + } + + @Test + public void withFollowingFirstFlowable() { + Flowable<Integer> f = Flowable.just(1, 3, 2, 5, 4); + assertEquals(Arrays.asList(1, 2, 3, 4, 5), f.toSortedList().toFlowable().blockingFirst()); + } + + @Test + public void backpressureHonoredFlowable() { + Flowable<List<Integer>> w = Flowable.just(1, 3, 2, 5, 4).toSortedList().toFlowable(); + TestSubscriber<List<Integer>> ts = new TestSubscriber<>(0L); + + w.subscribe(ts); + + ts.assertNoValues(); + ts.assertNoErrors(); + ts.assertNotComplete(); + + ts.request(1); + + ts.assertValue(Arrays.asList(1, 2, 3, 4, 5)); + ts.assertNoErrors(); + ts.assertComplete(); + + ts.request(1); + + ts.assertValue(Arrays.asList(1, 2, 3, 4, 5)); + ts.assertNoErrors(); + ts.assertComplete(); + } + + @Test + public void sorted() { + Flowable.just(5, 1, 2, 4, 3).sorted() + .test() + .assertResult(1, 2, 3, 4, 5); + } + + @Test + public void sortedComparator() { + Flowable.just(5, 1, 2, 4, 3).sorted(new Comparator<Integer>() { + @Override + public int compare(Integer a, Integer b) { + return b - a; + } + }) + .test() + .assertResult(5, 4, 3, 2, 1); + } + + @Test + public void toSortedListCapacityFlowable() { + Flowable.just(5, 1, 2, 4, 3).toSortedList(4).toFlowable() + .test() + .assertResult(Arrays.asList(1, 2, 3, 4, 5)); + } + + @Test + public void toSortedListComparatorCapacityFlowable() { + Flowable.just(5, 1, 2, 4, 3).toSortedList(new Comparator<Integer>() { + @Override + public int compare(Integer a, Integer b) { + return b - a; + } + }, 4).toFlowable() + .test() + .assertResult(Arrays.asList(5, 4, 3, 2, 1)); + } + + @Test + public void sortedList() { + Flowable<Integer> w = Flowable.just(1, 3, 2, 5, 4); + Single<List<Integer>> single = w.toSortedList(); + + SingleObserver<List<Integer>> observer = TestHelper.mockSingleObserver(); + single.subscribe(observer); + verify(observer, times(1)).onSuccess(Arrays.asList(1, 2, 3, 4, 5)); + verify(observer, Mockito.never()).onError(any(Throwable.class)); + } + + @Test + public void sortedListWithCustomFunction() { + Flowable<Integer> w = Flowable.just(1, 3, 2, 5, 4); + Single<List<Integer>> single = w.toSortedList(new Comparator<Integer>() { + + @Override + public int compare(Integer t1, Integer t2) { + return t2 - t1; + } + + }); + + SingleObserver<List<Integer>> observer = TestHelper.mockSingleObserver(); + single.subscribe(observer); + verify(observer, times(1)).onSuccess(Arrays.asList(5, 4, 3, 2, 1)); + verify(observer, Mockito.never()).onError(any(Throwable.class)); + } + + @Test + public void withFollowingFirst() { + Flowable<Integer> f = Flowable.just(1, 3, 2, 5, 4); + assertEquals(Arrays.asList(1, 2, 3, 4, 5), f.toSortedList().blockingGet()); + } + + static void await(CyclicBarrier cb) { + try { + cb.await(); + } catch (InterruptedException ex) { + ex.printStackTrace(); + } catch (BrokenBarrierException ex) { + ex.printStackTrace(); + } + } + + @Test + public void toSortedListCapacity() { + Flowable.just(5, 1, 2, 4, 3).toSortedList(4) + .test() + .assertResult(Arrays.asList(1, 2, 3, 4, 5)); + } + + @Test + public void toSortedListComparatorCapacity() { + Flowable.just(5, 1, 2, 4, 3).toSortedList(new Comparator<Integer>() { + @Override + public int compare(Integer a, Integer b) { + return b - a; + } + }, 4) + .test() + .assertResult(Arrays.asList(5, 4, 3, 2, 1)); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableUnsubscribeOnTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableUnsubscribeOnTest.java new file mode 100644 index 0000000000..2a8efa4593 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableUnsubscribeOnTest.java @@ -0,0 +1,283 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import static org.junit.Assert.*; + +import java.util.List; +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicReference; + +import org.junit.Test; +import org.reactivestreams.*; + +import io.reactivex.rxjava3.annotations.NonNull; +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.Action; +import io.reactivex.rxjava3.internal.schedulers.ImmediateThinScheduler; +import io.reactivex.rxjava3.internal.subscriptions.BooleanSubscription; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.schedulers.Schedulers; +import io.reactivex.rxjava3.testsupport.*; + +public class FlowableUnsubscribeOnTest extends RxJavaTest { + + @Test + public void unsubscribeWhenSubscribeOnAndUnsubscribeOnAreOnSameThread() throws InterruptedException { + UIEventLoopScheduler uiEventLoop = new UIEventLoopScheduler(); + try { + final ThreadSubscription subscription = new ThreadSubscription(); + final AtomicReference<Thread> subscribeThread = new AtomicReference<>(); + Flowable<Integer> w = Flowable.unsafeCreate(new Publisher<Integer>() { + + @Override + public void subscribe(Subscriber<? super Integer> t1) { + subscribeThread.set(Thread.currentThread()); + t1.onSubscribe(subscription); + t1.onNext(1); + t1.onNext(2); + // observeOn will prevent canceling the upstream upon its termination now + // this call is racing for that state in this test + // not doing it will make sure the unsubscribeOn always gets through + // t1.onComplete(); + } + }); + + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(); + w.subscribeOn(uiEventLoop).observeOn(Schedulers.computation()) + .unsubscribeOn(uiEventLoop) + .take(2) + .subscribe(ts); + + ts.awaitDone(1, TimeUnit.SECONDS); + + Thread unsubscribeThread = subscription.getThread(); + + assertNotNull(unsubscribeThread); + assertNotSame(Thread.currentThread(), unsubscribeThread); + + assertNotNull(subscribeThread.get()); + assertNotSame(Thread.currentThread(), subscribeThread.get()); + // True for Schedulers.newThread() + + System.out.println("unsubscribeThread: " + unsubscribeThread); + System.out.println("subscribeThread.get(): " + subscribeThread.get()); + assertSame(unsubscribeThread, uiEventLoop.getThread()); + + ts.assertValues(1, 2); + ts.assertTerminated(); + } finally { + uiEventLoop.shutdown(); + } + } + + @Test + public void unsubscribeWhenSubscribeOnAndUnsubscribeOnAreOnDifferentThreads() throws InterruptedException { + UIEventLoopScheduler uiEventLoop = new UIEventLoopScheduler(); + try { + final ThreadSubscription subscription = new ThreadSubscription(); + final AtomicReference<Thread> subscribeThread = new AtomicReference<>(); + Flowable<Integer> w = Flowable.unsafeCreate(new Publisher<Integer>() { + + @Override + public void subscribe(Subscriber<? super Integer> t1) { + subscribeThread.set(Thread.currentThread()); + t1.onSubscribe(subscription); + t1.onNext(1); + t1.onNext(2); + // observeOn will prevent canceling the upstream upon its termination now + // this call is racing for that state in this test + // not doing it will make sure the unsubscribeOn always gets through + // t1.onComplete(); + } + }); + + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(); + w.subscribeOn(Schedulers.newThread()).observeOn(Schedulers.computation()) + .unsubscribeOn(uiEventLoop) + .take(2) + .subscribe(ts); + + ts.awaitDone(1, TimeUnit.SECONDS); + + Thread unsubscribeThread = subscription.getThread(); + + assertNotNull(unsubscribeThread); + assertNotSame(Thread.currentThread(), unsubscribeThread); + + assertNotNull(subscribeThread.get()); + assertNotSame(Thread.currentThread(), subscribeThread.get()); + // True for Schedulers.newThread() + + System.out.println("UI Thread: " + uiEventLoop.getThread()); + System.out.println("unsubscribeThread: " + unsubscribeThread); + System.out.println("subscribeThread.get(): " + subscribeThread.get()); + assertSame(unsubscribeThread, uiEventLoop.getThread()); + + ts.assertValues(1, 2); + ts.assertTerminated(); + } finally { + uiEventLoop.shutdown(); + } + } + + private static class ThreadSubscription implements Subscription { + private volatile Thread thread; + + private final CountDownLatch latch = new CountDownLatch(1); + + @Override + public void cancel() { + System.out.println("unsubscribe invoked: " + Thread.currentThread()); + thread = Thread.currentThread(); + latch.countDown(); + } + + public Thread getThread() throws InterruptedException { + latch.await(); + return thread; + } + + @Override + public void request(long n) { + + } + } + + public static class UIEventLoopScheduler extends Scheduler { + + private final Scheduler eventLoop; + private volatile Thread t; + + public UIEventLoopScheduler() { + + eventLoop = Schedulers.single(); + + /* + * DON'T DO THIS IN PRODUCTION CODE + */ + final CountDownLatch latch = new CountDownLatch(1); + eventLoop.scheduleDirect(new Runnable() { + + @Override + public void run() { + t = Thread.currentThread(); + latch.countDown(); + } + + }); + try { + latch.await(); + } catch (InterruptedException e) { + throw new RuntimeException("failed to initialize and get inner thread"); + } + } + + @NonNull + @Override + public Worker createWorker() { + return eventLoop.createWorker(); + } + + public Thread getThread() { + return t; + } + + } + + @Test + public void takeHalf() { + int elements = 1024; + Flowable.range(0, elements * 2).unsubscribeOn(Schedulers.single()) + .take(elements) + .to(TestHelper.<Integer>testConsumer()) + .awaitDone(5, TimeUnit.SECONDS) + .assertValueCount(elements) + .assertComplete() + .assertNoErrors() + .assertSubscribed(); + } + + @Test + public void dispose() { + TestHelper.checkDisposed(Flowable.just(1).unsubscribeOn(Schedulers.single())); + } + + @Test + public void normal() { + final int[] calls = { 0 }; + + Flowable.just(1) + .doOnCancel(new Action() { + @Override + public void run() throws Exception { + calls[0]++; + } + }) + .unsubscribeOn(Schedulers.single()) + .test() + .assertResult(1); + + assertEquals(0, calls[0]); + } + + @Test + public void error() { + final int[] calls = { 0 }; + + Flowable.error(new TestException()) + .doOnCancel(new Action() { + @Override + public void run() throws Exception { + calls[0]++; + } + }) + .unsubscribeOn(Schedulers.single()) + .test() + .assertFailure(TestException.class); + + assertEquals(0, calls[0]); + } + + @Test + public void signalAfterDispose() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + new Flowable<Integer>() { + @Override + protected void subscribeActual(Subscriber<? super Integer> subscriber) { + subscriber.onSubscribe(new BooleanSubscription()); + subscriber.onNext(1); + subscriber.onNext(2); + subscriber.onError(new TestException()); + subscriber.onComplete(); + } + } + .unsubscribeOn(Schedulers.single()) + .take(1) + .test() + .assertResult(1); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeFlowable(f -> f.unsubscribeOn(ImmediateThinScheduler.INSTANCE)); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableUsingTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableUsingTest.java new file mode 100644 index 0000000000..d744c32747 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableUsingTest.java @@ -0,0 +1,693 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +import java.util.*; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.*; +import org.mockito.InOrder; +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.*; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.internal.subscriptions.BooleanSubscription; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.subscribers.TestSubscriber; +import io.reactivex.rxjava3.testsupport.*; + +public class FlowableUsingTest extends RxJavaTest { + + private interface Resource { + String getTextFromWeb(); + + void dispose(); + } + + private static class DisposeAction implements Consumer<Resource> { + + @Override + public void accept(Resource r) { + r.dispose(); + } + + } + + private final Consumer<Disposable> disposeSubscription = new Consumer<Disposable>() { + + @Override + public void accept(Disposable d) { + d.dispose(); + } + + }; + + @Test + public void using() { + performTestUsing(false); + } + + @Test + public void usingEagerly() { + performTestUsing(true); + } + + private void performTestUsing(boolean disposeEagerly) { + final Resource resource = mock(Resource.class); + when(resource.getTextFromWeb()).thenReturn("Hello world!"); + + Supplier<Resource> resourceFactory = new Supplier<Resource>() { + @Override + public Resource get() { + return resource; + } + }; + + Function<Resource, Flowable<String>> observableFactory = new Function<Resource, Flowable<String>>() { + @Override + public Flowable<String> apply(Resource res) { + return Flowable.fromArray(res.getTextFromWeb().split(" ")); + } + }; + + Subscriber<String> subscriber = TestHelper.mockSubscriber(); + + Flowable<String> flowable = Flowable.using(resourceFactory, observableFactory, + new DisposeAction(), disposeEagerly); + flowable.subscribe(subscriber); + + InOrder inOrder = inOrder(subscriber); + inOrder.verify(subscriber, times(1)).onNext("Hello"); + inOrder.verify(subscriber, times(1)).onNext("world!"); + inOrder.verify(subscriber, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + + // The resouce should be closed + verify(resource, times(1)).dispose(); + } + + @Test + public void usingWithSubscribingTwice() { + performTestUsingWithSubscribingTwice(false); + } + + @Test + public void usingWithSubscribingTwiceDisposeEagerly() { + performTestUsingWithSubscribingTwice(true); + } + + private void performTestUsingWithSubscribingTwice(boolean disposeEagerly) { + // When subscribe is called, a new resource should be created. + Supplier<Resource> resourceFactory = new Supplier<Resource>() { + @Override + public Resource get() { + return new Resource() { + + boolean first = true; + + @Override + public String getTextFromWeb() { + if (first) { + first = false; + return "Hello world!"; + } + return "Nothing"; + } + + @Override + public void dispose() { + // do nothing + } + + }; + } + }; + + Function<Resource, Flowable<String>> observableFactory = new Function<Resource, Flowable<String>>() { + @Override + public Flowable<String> apply(Resource res) { + return Flowable.fromArray(res.getTextFromWeb().split(" ")); + } + }; + + Subscriber<String> subscriber = TestHelper.mockSubscriber(); + + Flowable<String> flowable = Flowable.using(resourceFactory, observableFactory, + new DisposeAction(), disposeEagerly); + flowable.subscribe(subscriber); + flowable.subscribe(subscriber); + + InOrder inOrder = inOrder(subscriber); + + inOrder.verify(subscriber, times(1)).onNext("Hello"); + inOrder.verify(subscriber, times(1)).onNext("world!"); + inOrder.verify(subscriber, times(1)).onComplete(); + + inOrder.verify(subscriber, times(1)).onNext("Hello"); + inOrder.verify(subscriber, times(1)).onNext("world!"); + inOrder.verify(subscriber, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + } + + @Test(expected = TestException.class) + public void usingWithResourceFactoryError() { + performTestUsingWithResourceFactoryError(false); + } + + @Test(expected = TestException.class) + public void usingWithResourceFactoryErrorDisposeEagerly() { + performTestUsingWithResourceFactoryError(true); + } + + private void performTestUsingWithResourceFactoryError(boolean disposeEagerly) { + Supplier<Disposable> resourceFactory = new Supplier<Disposable>() { + @Override + public Disposable get() { + throw new TestException(); + } + }; + + Function<Disposable, Flowable<Integer>> observableFactory = new Function<Disposable, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Disposable d) { + return Flowable.empty(); + } + }; + + Flowable.using(resourceFactory, observableFactory, disposeSubscription) + .blockingLast(); + } + + @Test + public void usingWithFlowableFactoryError() { + performTestUsingWithFlowableFactoryError(false); + } + + @Test + public void usingWithFlowableFactoryErrorDisposeEagerly() { + performTestUsingWithFlowableFactoryError(true); + } + + private void performTestUsingWithFlowableFactoryError(boolean disposeEagerly) { + final Runnable unsubscribe = mock(Runnable.class); + Supplier<Disposable> resourceFactory = new Supplier<Disposable>() { + @Override + public Disposable get() { + return Disposable.fromRunnable(unsubscribe); + } + }; + + Function<Disposable, Flowable<Integer>> observableFactory = new Function<Disposable, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Disposable subscription) { + throw new TestException(); + } + }; + + try { + Flowable.using(resourceFactory, observableFactory, disposeSubscription).blockingLast(); + fail("Should throw a TestException when the observableFactory throws it"); + } catch (TestException e) { + // Make sure that unsubscribe is called so that users can close + // the resource if some error happens. + verify(unsubscribe, times(1)).run(); + } + } + + @Test + public void usingDisposesEagerlyBeforeCompletion() { + final List<String> events = new ArrayList<>(); + Supplier<Resource> resourceFactory = createResourceFactory(events); + final Action completion = createOnCompletedAction(events); + final Action unsub = createUnsubAction(events); + + Function<Resource, Flowable<String>> observableFactory = new Function<Resource, Flowable<String>>() { + @Override + public Flowable<String> apply(Resource resource) { + return Flowable.fromArray(resource.getTextFromWeb().split(" ")); + } + }; + + Subscriber<String> subscriber = TestHelper.mockSubscriber(); + + Flowable<String> flowable = Flowable.using(resourceFactory, observableFactory, + new DisposeAction(), true) + .doOnCancel(unsub) + .doOnComplete(completion); + + flowable.safeSubscribe(subscriber); + + assertEquals(Arrays.asList("disposed", "completed"), events); + + } + + @Test + public void usingDoesNotDisposesEagerlyBeforeCompletion() { + final List<String> events = new ArrayList<>(); + Supplier<Resource> resourceFactory = createResourceFactory(events); + final Action completion = createOnCompletedAction(events); + final Action unsub = createUnsubAction(events); + + Function<Resource, Flowable<String>> observableFactory = new Function<Resource, Flowable<String>>() { + @Override + public Flowable<String> apply(Resource resource) { + return Flowable.fromArray(resource.getTextFromWeb().split(" ")); + } + }; + + Subscriber<String> subscriber = TestHelper.mockSubscriber(); + + Flowable<String> flowable = Flowable.using(resourceFactory, observableFactory, + new DisposeAction(), false) + .doOnCancel(unsub) + .doOnComplete(completion); + + flowable.safeSubscribe(subscriber); + + assertEquals(Arrays.asList("completed", "disposed"), events); + + } + + @Test + public void usingDisposesEagerlyBeforeError() { + final List<String> events = new ArrayList<>(); + Supplier<Resource> resourceFactory = createResourceFactory(events); + final Consumer<Throwable> onError = createOnErrorAction(events); + final Action unsub = createUnsubAction(events); + + Function<Resource, Flowable<String>> observableFactory = new Function<Resource, Flowable<String>>() { + @Override + public Flowable<String> apply(Resource resource) { + return Flowable.fromArray(resource.getTextFromWeb().split(" ")) + .concatWith(Flowable.<String>error(new RuntimeException())); + } + }; + + Subscriber<String> subscriber = TestHelper.mockSubscriber(); + + Flowable<String> flowable = Flowable.using(resourceFactory, observableFactory, + new DisposeAction(), true) + .doOnCancel(unsub) + .doOnError(onError); + + flowable.safeSubscribe(subscriber); + + assertEquals(Arrays.asList("disposed", "error"), events); + + } + + @Test + public void usingDoesNotDisposesEagerlyBeforeError() { + final List<String> events = new ArrayList<>(); + final Supplier<Resource> resourceFactory = createResourceFactory(events); + final Consumer<Throwable> onError = createOnErrorAction(events); + final Action unsub = createUnsubAction(events); + + Function<Resource, Flowable<String>> observableFactory = new Function<Resource, Flowable<String>>() { + @Override + public Flowable<String> apply(Resource resource) { + return Flowable.fromArray(resource.getTextFromWeb().split(" ")) + .concatWith(Flowable.<String>error(new RuntimeException())); + } + }; + + Subscriber<String> subscriber = TestHelper.mockSubscriber(); + + Flowable<String> flowable = Flowable.using(resourceFactory, observableFactory, + new DisposeAction(), false) + .doOnCancel(unsub) + .doOnError(onError); + + flowable.safeSubscribe(subscriber); + + assertEquals(Arrays.asList("error", "disposed"), events); + } + + private static Action createUnsubAction(final List<String> events) { + return new Action() { + @Override + public void run() { + events.add("unsub"); + } + }; + } + + private static Consumer<Throwable> createOnErrorAction(final List<String> events) { + return new Consumer<Throwable>() { + @Override + public void accept(Throwable t) { + events.add("error"); + } + }; + } + + private static Supplier<Resource> createResourceFactory(final List<String> events) { + return new Supplier<Resource>() { + @Override + public Resource get() { + return new Resource() { + + @Override + public String getTextFromWeb() { + return "hello world"; + } + + @Override + public void dispose() { + events.add("disposed"); + } + }; + } + }; + } + + private static Action createOnCompletedAction(final List<String> events) { + return new Action() { + @Override + public void run() { + events.add("completed"); + } + }; + } + + @Test + public void factoryThrows() { + + TestSubscriber<Integer> ts = TestSubscriber.create(); + + final AtomicInteger count = new AtomicInteger(); + + Flowable.<Integer, Integer>using( + new Supplier<Integer>() { + @Override + public Integer get() { + return 1; + } + }, + new Function<Integer, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Integer v) { + throw new TestException("forced failure"); + } + }, + new Consumer<Integer>() { + @Override + public void accept(Integer c) { + count.incrementAndGet(); + } + } + ) + .subscribe(ts); + + ts.assertError(TestException.class); + + Assert.assertEquals(1, count.get()); + } + + @Test + public void nonEagerTermination() { + + TestSubscriber<Integer> ts = TestSubscriber.create(); + + final AtomicInteger count = new AtomicInteger(); + + Flowable.<Integer, Integer>using( + new Supplier<Integer>() { + @Override + public Integer get() { + return 1; + } + }, + new Function<Integer, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Integer v) { + return Flowable.just(v); + } + }, + new Consumer<Integer>() { + @Override + public void accept(Integer c) { + count.incrementAndGet(); + } + }, false + ) + .subscribe(ts); + + ts.assertValue(1); + ts.assertNoErrors(); + ts.assertComplete(); + + Assert.assertEquals(1, count.get()); + } + + @Test + public void dispose() { + TestHelper.checkDisposed(Flowable.using( + new Supplier<Object>() { + @Override + public Object get() throws Exception { + return 1; + } + }, + new Function<Object, Flowable<Object>>() { + @Override + public Flowable<Object> apply(Object v) throws Exception { + return Flowable.never(); + } + }, + Functions.emptyConsumer() + )); + } + + @Test + public void supplierDisposerCrash() { + TestSubscriberEx<Object> ts = Flowable.using(new Supplier<Object>() { + @Override + public Object get() throws Exception { + return 1; + } + }, new Function<Object, Flowable<Object>>() { + @Override + public Flowable<Object> apply(Object v) throws Exception { + throw new TestException("First"); + } + }, new Consumer<Object>() { + @Override + public void accept(Object e) throws Exception { + throw new TestException("Second"); + } + }) + .to(TestHelper.<Object>testConsumer()) + .assertFailure(CompositeException.class); + + List<Throwable> errors = TestHelper.compositeList(ts.errors().get(0)); + + TestHelper.assertError(errors, 0, TestException.class, "First"); + TestHelper.assertError(errors, 1, TestException.class, "Second"); + } + + @Test + public void eagerOnErrorDisposerCrash() { + TestSubscriberEx<Object> ts = Flowable.using(new Supplier<Object>() { + @Override + public Object get() throws Exception { + return 1; + } + }, new Function<Object, Flowable<Object>>() { + @Override + public Flowable<Object> apply(Object v) throws Exception { + return Flowable.error(new TestException("First")); + } + }, new Consumer<Object>() { + @Override + public void accept(Object e) throws Exception { + throw new TestException("Second"); + } + }) + .to(TestHelper.<Object>testConsumer()) + .assertFailure(CompositeException.class); + + List<Throwable> errors = TestHelper.compositeList(ts.errors().get(0)); + + TestHelper.assertError(errors, 0, TestException.class, "First"); + TestHelper.assertError(errors, 1, TestException.class, "Second"); + } + + @Test + public void eagerOnCompleteDisposerCrash() { + Flowable.using(new Supplier<Object>() { + @Override + public Object get() throws Exception { + return 1; + } + }, new Function<Object, Flowable<Object>>() { + @Override + public Flowable<Object> apply(Object v) throws Exception { + return Flowable.empty(); + } + }, new Consumer<Object>() { + @Override + public void accept(Object e) throws Exception { + throw new TestException("Second"); + } + }) + .to(TestHelper.<Object>testConsumer()) + .assertFailureAndMessage(TestException.class, "Second"); + } + + @Test + public void nonEagerDisposerCrash() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + Flowable.using(new Supplier<Object>() { + @Override + public Object get() throws Exception { + return 1; + } + }, new Function<Object, Flowable<Object>>() { + @Override + public Flowable<Object> apply(Object v) throws Exception { + return Flowable.empty(); + } + }, new Consumer<Object>() { + @Override + public void accept(Object e) throws Exception { + throw new TestException("Second"); + } + }, false) + .test() + .assertResult(); + + TestHelper.assertUndeliverable(errors, 0, TestException.class, "Second"); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void sourceSupplierReturnsNull() { + Flowable.using(Functions.justSupplier(1), + Functions.justFunction((Publisher<Object>)null), + Functions.emptyConsumer()) + .to(TestHelper.<Object>testConsumer()) + .assertFailureAndMessage(NullPointerException.class, "The sourceSupplier returned a null Publisher") + ; + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeFlowable(new Function<Flowable<Object>, Flowable<Object>>() { + @Override + public Flowable<Object> apply(Flowable<Object> f) + throws Exception { + return Flowable.using(Functions.justSupplier(1), Functions.justFunction(f), Functions.emptyConsumer()); + } + }); + } + + @Test + public void eagerDisposedOnComplete() { + final TestSubscriber<Integer> ts = new TestSubscriber<>(); + + Flowable.using(Functions.justSupplier(1), Functions.justFunction(new Flowable<Integer>() { + @Override + protected void subscribeActual(Subscriber<? super Integer> subscriber) { + subscriber.onSubscribe(new BooleanSubscription()); + ts.cancel(); + subscriber.onComplete(); + } + }), Functions.emptyConsumer(), true) + .subscribe(ts); + } + + @Test + public void eagerDisposedOnError() { + final TestSubscriber<Integer> ts = new TestSubscriber<>(); + + Flowable.using(Functions.justSupplier(1), Functions.justFunction(new Flowable<Integer>() { + @Override + protected void subscribeActual(Subscriber<? super Integer> subscriber) { + subscriber.onSubscribe(new BooleanSubscription()); + ts.cancel(); + subscriber.onError(new TestException()); + } + }), Functions.emptyConsumer(), true) + .subscribe(ts); + } + + @Test + public void eagerDisposeResourceThenDisposeUpstream() { + final StringBuilder sb = new StringBuilder(); + + Flowable.using(Functions.justSupplier(1), + new Function<Integer, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Integer t) throws Throwable { + return Flowable.range(1, 2) + .doOnCancel(new Action() { + @Override + public void run() throws Throwable { + sb.append("Cancel"); + } + }) + ; + } + }, new Consumer<Integer>() { + @Override + public void accept(Integer t) throws Throwable { + sb.append("Resource"); + } + }, true) + .take(1) + .test() + .assertResult(1); + + assertEquals("ResourceCancel", sb.toString()); + } + + @Test + public void nonEagerDisposeUpstreamThenDisposeResource() { + final StringBuilder sb = new StringBuilder(); + + Flowable.using(Functions.justSupplier(1), + new Function<Integer, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Integer t) throws Throwable { + return Flowable.range(1, 2) + .doOnCancel(new Action() { + @Override + public void run() throws Throwable { + sb.append("Cancel"); + } + }); + } + }, new Consumer<Integer>() { + @Override + public void accept(Integer t) throws Throwable { + sb.append("Resource"); + } + }, false) + .take(1) + .test() + .assertResult(1); + + assertEquals("CancelResource", sb.toString()); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableWindowWithFlowableTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableWindowWithFlowableTest.java new file mode 100644 index 0000000000..5f5d97af1e --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableWindowWithFlowableTest.java @@ -0,0 +1,743 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import java.util.*; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.*; + +import org.junit.Test; +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.internal.subscriptions.BooleanSubscription; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.processors.*; +import io.reactivex.rxjava3.subscribers.*; +import io.reactivex.rxjava3.testsupport.*; + +public class FlowableWindowWithFlowableTest extends RxJavaTest { + + @Test + public void windowViaFlowableNormal1() { + PublishProcessor<Integer> source = PublishProcessor.create(); + PublishProcessor<Integer> boundary = PublishProcessor.create(); + + final Subscriber<Object> subscriber = TestHelper.mockSubscriber(); + + final List<Subscriber<Object>> values = new ArrayList<>(); + + Subscriber<Flowable<Integer>> wo = new DefaultSubscriber<Flowable<Integer>>() { + @Override + public void onNext(Flowable<Integer> args) { + final Subscriber<Object> mo = TestHelper.mockSubscriber(); + values.add(mo); + + args.subscribe(mo); + } + + @Override + public void onError(Throwable e) { + subscriber.onError(e); + } + + @Override + public void onComplete() { + subscriber.onComplete(); + } + }; + + source.window(boundary).subscribe(wo); + + int n = 30; + for (int i = 0; i < n; i++) { + source.onNext(i); + if (i % 3 == 2 && i < n - 1) { + boundary.onNext(i / 3); + } + } + source.onComplete(); + + verify(subscriber, never()).onError(any(Throwable.class)); + + assertEquals(n / 3, values.size()); + + int j = 0; + for (Subscriber<Object> mo : values) { + verify(mo, never()).onError(any(Throwable.class)); + for (int i = 0; i < 3; i++) { + verify(mo).onNext(j + i); + } + verify(mo).onComplete(); + j += 3; + } + + verify(subscriber).onComplete(); + } + + @Test + public void windowViaFlowableBoundaryCompletes() { + PublishProcessor<Integer> source = PublishProcessor.create(); + PublishProcessor<Integer> boundary = PublishProcessor.create(); + + final Subscriber<Object> subscriber = TestHelper.mockSubscriber(); + + final List<Subscriber<Object>> values = new ArrayList<>(); + + Subscriber<Flowable<Integer>> wo = new DefaultSubscriber<Flowable<Integer>>() { + @Override + public void onNext(Flowable<Integer> args) { + final Subscriber<Object> mo = TestHelper.mockSubscriber(); + values.add(mo); + + args.subscribe(mo); + } + + @Override + public void onError(Throwable e) { + subscriber.onError(e); + } + + @Override + public void onComplete() { + subscriber.onComplete(); + } + }; + + source.window(boundary).subscribe(wo); + + int n = 30; + for (int i = 0; i < n; i++) { + source.onNext(i); + if (i % 3 == 2 && i < n - 1) { + boundary.onNext(i / 3); + } + } + boundary.onComplete(); + + assertEquals(n / 3, values.size()); + + int j = 0; + for (Subscriber<Object> mo : values) { + for (int i = 0; i < 3; i++) { + verify(mo).onNext(j + i); + } + verify(mo).onComplete(); + verify(mo, never()).onError(any(Throwable.class)); + j += 3; + } + + verify(subscriber).onComplete(); + verify(subscriber, never()).onError(any(Throwable.class)); + } + + @Test + public void windowViaFlowableBoundaryThrows() { + PublishProcessor<Integer> source = PublishProcessor.create(); + PublishProcessor<Integer> boundary = PublishProcessor.create(); + + final Subscriber<Object> subscriber = TestHelper.mockSubscriber(); + + final List<Subscriber<Object>> values = new ArrayList<>(); + + Subscriber<Flowable<Integer>> wo = new DefaultSubscriber<Flowable<Integer>>() { + @Override + public void onNext(Flowable<Integer> args) { + final Subscriber<Object> mo = TestHelper.mockSubscriber(); + values.add(mo); + + args.subscribe(mo); + } + + @Override + public void onError(Throwable e) { + subscriber.onError(e); + } + + @Override + public void onComplete() { + subscriber.onComplete(); + } + }; + + source.window(boundary).subscribe(wo); + + source.onNext(0); + source.onNext(1); + source.onNext(2); + + boundary.onError(new TestException()); + + assertEquals(1, values.size()); + + Subscriber<Object> mo = values.get(0); + + verify(mo).onNext(0); + verify(mo).onNext(1); + verify(mo).onNext(2); + verify(mo).onError(any(TestException.class)); + + verify(subscriber, never()).onComplete(); + verify(subscriber).onError(any(TestException.class)); + } + + @Test + public void windowViaFlowableThrows() { + PublishProcessor<Integer> source = PublishProcessor.create(); + PublishProcessor<Integer> boundary = PublishProcessor.create(); + + final Subscriber<Object> subscriber = TestHelper.mockSubscriber(); + + final List<Subscriber<Object>> values = new ArrayList<>(); + + Subscriber<Flowable<Integer>> wo = new DefaultSubscriber<Flowable<Integer>>() { + @Override + public void onNext(Flowable<Integer> args) { + final Subscriber<Object> mo = TestHelper.mockSubscriber(); + values.add(mo); + + args.subscribe(mo); + } + + @Override + public void onError(Throwable e) { + subscriber.onError(e); + } + + @Override + public void onComplete() { + subscriber.onComplete(); + } + }; + + source.window(boundary).subscribe(wo); + + source.onNext(0); + source.onNext(1); + source.onNext(2); + + source.onError(new TestException()); + + assertEquals(1, values.size()); + + Subscriber<Object> mo = values.get(0); + + verify(mo).onNext(0); + verify(mo).onNext(1); + verify(mo).onNext(2); + verify(mo).onError(any(TestException.class)); + + verify(subscriber, never()).onComplete(); + verify(subscriber).onError(any(TestException.class)); + } + + @Test + public void boundaryDispose() { + TestHelper.checkDisposed(Flowable.never().window(Flowable.never())); + } + + @Test + public void boundaryOnError() { + TestSubscriberEx<Object> ts = Flowable.error(new TestException()) + .window(Flowable.never()) + .flatMap(Functions.<Flowable<Object>>identity(), true) + .to(TestHelper.<Object>testConsumer()) + .assertFailure(CompositeException.class); + + List<Throwable> errors = TestHelper.compositeList(ts.errors().get(0)); + + TestHelper.assertError(errors, 0, TestException.class); + } + + @Test + public void innerBadSource() { + TestHelper.checkBadSourceFlowable(new Function<Flowable<Integer>, Object>() { + @Override + public Object apply(Flowable<Integer> f) throws Exception { + return Flowable.just(1).window(f).flatMap(new Function<Flowable<Integer>, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Flowable<Integer> v) throws Exception { + return v; + } + }); + } + }, false, 1, 1, (Object[])null); + } + + @Test + public void reentrant() { + final FlowableProcessor<Integer> ps = PublishProcessor.<Integer>create(); + + TestSubscriber<Integer> ts = new TestSubscriber<Integer>() { + @Override + public void onNext(Integer t) { + super.onNext(t); + if (t == 1) { + ps.onNext(2); + ps.onComplete(); + } + } + }; + + ps.window(BehaviorProcessor.createDefault(1)) + .flatMap(new Function<Flowable<Integer>, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Flowable<Integer> v) throws Exception { + return v; + } + }) + .subscribe(ts); + + ps.onNext(1); + + ts + .awaitDone(1, TimeUnit.SECONDS) + .assertResult(1, 2); + } + + @Test + public void badSource() { + TestHelper.checkBadSourceFlowable(new Function<Flowable<Object>, Object>() { + @Override + public Object apply(Flowable<Object> f) throws Exception { + return f.window(Flowable.never()).flatMap(new Function<Flowable<Object>, Flowable<Object>>() { + @Override + public Flowable<Object> apply(Flowable<Object> v) throws Exception { + return v; + } + }); + } + }, false, 1, 1, 1); + } + + @Test + public void boundaryDirectMissingBackpressure() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + BehaviorProcessor.create() + .window(Flowable.error(new TestException())) + .test(0) + .assertFailure(MissingBackpressureException.class); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void boundaryDirectMissingBackpressureNoNullPointerException() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + BehaviorProcessor.createDefault(1) + .window(Flowable.error(new TestException())) + .test(0) + .assertFailure(MissingBackpressureException.class); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void boundaryDirectSecondMissingBackpressure() { + BehaviorProcessor.createDefault(1) + .window(Flowable.just(1)) + .test(1) + .assertError(MissingBackpressureException.class) + .assertNotComplete(); + } + + @Test + public void boundaryDirectDoubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeFlowable(new Function<Flowable<Object>, Publisher<Flowable<Object>>>() { + @Override + public Publisher<Flowable<Object>> apply(Flowable<Object> f) + throws Exception { + return f.window(Flowable.never()).takeLast(1); + } + }); + } + + @Test + public void upstreamDisposedWhenOutputsDisposed() { + PublishProcessor<Integer> source = PublishProcessor.create(); + PublishProcessor<Integer> boundary = PublishProcessor.create(); + + TestSubscriber<Integer> ts = source.window(boundary) + .take(1) + .flatMap(new Function<Flowable<Integer>, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply( + Flowable<Integer> w) throws Exception { + return w.take(1); + } + }) + .test(); + + source.onNext(1); + + assertFalse("source not disposed", source.hasSubscribers()); + assertFalse("boundary not disposed", boundary.hasSubscribers()); + + ts.assertResult(1); + } + + @Test + public void mainAndBoundaryBothError() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + final AtomicReference<Subscriber<? super Object>> ref = new AtomicReference<>(); + + TestSubscriberEx<Flowable<Object>> ts = Flowable.error(new TestException("main")) + .window(new Flowable<Object>() { + @Override + protected void subscribeActual(Subscriber<? super Object> subscriber) { + subscriber.onSubscribe(new BooleanSubscription()); + ref.set(subscriber); + } + }) + .doOnNext(new Consumer<Flowable<Object>>() { + @Override + public void accept(Flowable<Object> w) throws Throwable { + w.subscribe(Functions.emptyConsumer(), Functions.emptyConsumer()); // avoid abandonment + } + }) + .to(TestHelper.<Flowable<Object>>testConsumer()); + + ts + .assertValueCount(1) + .assertError(TestException.class) + .assertErrorMessage("main") + .assertNotComplete(); + + ref.get().onError(new TestException("inner")); + + TestHelper.assertUndeliverable(errors, 0, TestException.class, "inner"); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void mainCompleteBoundaryErrorRace() { + final TestException ex = new TestException(); + + for (int i = 0; i < TestHelper.RACE_LONG_LOOPS; i++) { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + final AtomicReference<Subscriber<? super Object>> refMain = new AtomicReference<>(); + final AtomicReference<Subscriber<? super Object>> ref = new AtomicReference<>(); + + TestSubscriberEx<Flowable<Object>> ts = new Flowable<Object>() { + @Override + protected void subscribeActual(Subscriber<? super Object> subscriber) { + subscriber.onSubscribe(new BooleanSubscription()); + refMain.set(subscriber); + } + } + .window(new Flowable<Object>() { + @Override + protected void subscribeActual(Subscriber<? super Object> subscriber) { + subscriber.onSubscribe(new BooleanSubscription()); + ref.set(subscriber); + } + }) + .to(TestHelper.<Flowable<Object>>testConsumer()); + + Runnable r1 = new Runnable() { + @Override + public void run() { + refMain.get().onComplete(); + } + }; + Runnable r2 = new Runnable() { + @Override + public void run() { + ref.get().onError(ex); + } + }; + + TestHelper.race(r1, r2); + + ts + .assertValueCount(1) + .assertTerminated(); + + if (!errors.isEmpty()) { + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } + } finally { + RxJavaPlugins.reset(); + } + } + } + + @Test + public void mainNextBoundaryNextRace() { + for (int i = 0; i < TestHelper.RACE_LONG_LOOPS; i++) { + final AtomicReference<Subscriber<? super Object>> refMain = new AtomicReference<>(); + final AtomicReference<Subscriber<? super Object>> ref = new AtomicReference<>(); + + TestSubscriber<Flowable<Object>> ts = new Flowable<Object>() { + @Override + protected void subscribeActual(Subscriber<? super Object> subscriber) { + subscriber.onSubscribe(new BooleanSubscription()); + refMain.set(subscriber); + } + } + .window(new Flowable<Object>() { + @Override + protected void subscribeActual(Subscriber<? super Object> subscriber) { + subscriber.onSubscribe(new BooleanSubscription()); + ref.set(subscriber); + } + }) + .test(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + refMain.get().onNext(1); + } + }; + Runnable r2 = new Runnable() { + @Override + public void run() { + ref.get().onNext(1); + } + }; + + TestHelper.race(r1, r2); + + ts + .assertValueCount(2) + .assertNotComplete() + .assertNoErrors(); + } + } + + @Test + public void takeOneAnotherBoundary() { + final AtomicReference<Subscriber<? super Object>> refMain = new AtomicReference<>(); + final AtomicReference<Subscriber<? super Object>> ref = new AtomicReference<>(); + + TestSubscriberEx<Flowable<Object>> ts = new Flowable<Object>() { + @Override + protected void subscribeActual(Subscriber<? super Object> subscriber) { + subscriber.onSubscribe(new BooleanSubscription()); + refMain.set(subscriber); + } + } + .window(new Flowable<Object>() { + @Override + protected void subscribeActual(Subscriber<? super Object> subscriber) { + subscriber.onSubscribe(new BooleanSubscription()); + ref.set(subscriber); + } + }) + .to(TestHelper.<Flowable<Object>>testConsumer()); + + ts.assertValueCount(1) + .assertNotTerminated() + .cancel(); + + ref.get().onNext(1); + + ts.assertValueCount(1) + .assertNotTerminated(); + } + + @Test + public void disposeMainBoundaryCompleteRace() { + for (int i = 0; i < TestHelper.RACE_LONG_LOOPS; i++) { + final AtomicReference<Subscriber<? super Object>> refMain = new AtomicReference<>(); + final AtomicReference<Subscriber<? super Object>> ref = new AtomicReference<>(); + + final TestSubscriber<Flowable<Object>> ts = new Flowable<Object>() { + @Override + protected void subscribeActual(Subscriber<? super Object> subscriber) { + subscriber.onSubscribe(new BooleanSubscription()); + refMain.set(subscriber); + } + } + .window(new Flowable<Object>() { + @Override + protected void subscribeActual(Subscriber<? super Object> subscriber) { + final AtomicInteger counter = new AtomicInteger(); + subscriber.onSubscribe(new Subscription() { + + @Override + public void cancel() { + // about a microsecond + for (int i = 0; i < 100; i++) { + counter.incrementAndGet(); + } + } + + @Override + public void request(long n) { + } + }); + ref.set(subscriber); + } + }) + .test(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + ts.cancel(); + } + }; + Runnable r2 = new Runnable() { + @Override + public void run() { + Subscriber<Object> subscriber = ref.get(); + subscriber.onNext(1); + subscriber.onComplete(); + } + }; + + TestHelper.race(r1, r2); + } + } + + @Test + @SuppressUndeliverable + public void disposeMainBoundaryErrorRace() { + final TestException ex = new TestException(); + + for (int i = 0; i < TestHelper.RACE_LONG_LOOPS; i++) { + final AtomicReference<Subscriber<? super Object>> refMain = new AtomicReference<>(); + final AtomicReference<Subscriber<? super Object>> ref = new AtomicReference<>(); + + final TestSubscriber<Flowable<Object>> ts = new Flowable<Object>() { + @Override + protected void subscribeActual(Subscriber<? super Object> subscriber) { + subscriber.onSubscribe(new BooleanSubscription()); + refMain.set(subscriber); + } + } + .window(new Flowable<Object>() { + @Override + protected void subscribeActual(Subscriber<? super Object> subscriber) { + final AtomicInteger counter = new AtomicInteger(); + subscriber.onSubscribe(new Subscription() { + + @Override + public void cancel() { + // about a microsecond + for (int i = 0; i < 100; i++) { + counter.incrementAndGet(); + } + } + + @Override + public void request(long n) { + } + }); + ref.set(subscriber); + } + }) + .test(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + ts.cancel(); + } + }; + Runnable r2 = new Runnable() { + @Override + public void run() { + Subscriber<Object> subscriber = ref.get(); + subscriber.onNext(1); + subscriber.onError(ex); + } + }; + + TestHelper.race(r1, r2); + } + } + + @Test + public void cancellingWindowCancelsUpstream() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + + TestSubscriber<Integer> ts = pp.window(Flowable.just(1).concatWith(Flowable.<Integer>never())) + .take(1) + .flatMap(new Function<Flowable<Integer>, Publisher<Integer>>() { + @Override + public Publisher<Integer> apply(Flowable<Integer> w) throws Throwable { + return w.take(1); + } + }) + .test(); + + assertTrue(pp.hasSubscribers()); + + pp.onNext(1); + + ts + .assertResult(1); + + assertFalse("Processor still has subscribers!", pp.hasSubscribers()); + } + + @Test + public void windowAbandonmentCancelsUpstream() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + + final AtomicReference<Flowable<Integer>> inner = new AtomicReference<>(); + + TestSubscriber<Flowable<Integer>> ts = pp.window(Flowable.<Integer>never()) + .doOnNext(new Consumer<Flowable<Integer>>() { + @Override + public void accept(Flowable<Integer> v) throws Throwable { + inner.set(v); + } + }) + .test(); + + assertTrue(pp.hasSubscribers()); + + ts + .assertValueCount(1) + ; + + pp.onNext(1); + + assertTrue(pp.hasSubscribers()); + + ts.cancel(); + + ts + .assertValueCount(1) + .assertNoErrors() + .assertNotComplete(); + + assertFalse("Processor still has subscribers!", pp.hasSubscribers()); + + inner.get().test().assertResult(); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableWindowWithSizeTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableWindowWithSizeTest.java new file mode 100644 index 0000000000..bd50a20a2a --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableWindowWithSizeTest.java @@ -0,0 +1,815 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import java.util.*; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.*; + +import org.junit.Test; +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.subscriptions.BooleanSubscription; +import io.reactivex.rxjava3.processors.PublishProcessor; +import io.reactivex.rxjava3.schedulers.Schedulers; +import io.reactivex.rxjava3.subscribers.*; +import io.reactivex.rxjava3.testsupport.*; + +public class FlowableWindowWithSizeTest extends RxJavaTest { + + private static <T> List<List<T>> toLists(Flowable<Flowable<T>> observables) { + + return observables.flatMapSingle(new Function<Flowable<T>, SingleSource<List<T>>>() { + @Override + public SingleSource<List<T>> apply(Flowable<T> w) throws Throwable { + return w.toList(); + } + }).toList().blockingGet(); + } + + @Test + public void nonOverlappingWindows() { + Flowable<String> subject = Flowable.just("one", "two", "three", "four", "five"); + Flowable<Flowable<String>> windowed = subject.window(3); + + List<List<String>> windows = toLists(windowed); + + assertEquals(2, windows.size()); + assertEquals(list("one", "two", "three"), windows.get(0)); + assertEquals(list("four", "five"), windows.get(1)); + } + + @Test + public void skipAndCountGaplessWindows() { + Flowable<String> subject = Flowable.just("one", "two", "three", "four", "five"); + Flowable<Flowable<String>> windowed = subject.window(3, 3); + + List<List<String>> windows = toLists(windowed); + + assertEquals(2, windows.size()); + assertEquals(list("one", "two", "three"), windows.get(0)); + assertEquals(list("four", "five"), windows.get(1)); + } + + @Test + public void overlappingWindows() { + Flowable<String> subject = Flowable.fromArray(new String[] { "zero", "one", "two", "three", "four", "five" }); + Flowable<Flowable<String>> windowed = subject.window(3, 1); + + List<List<String>> windows = toLists(windowed); + + assertEquals(6, windows.size()); + assertEquals(list("zero", "one", "two"), windows.get(0)); + assertEquals(list("one", "two", "three"), windows.get(1)); + assertEquals(list("two", "three", "four"), windows.get(2)); + assertEquals(list("three", "four", "five"), windows.get(3)); + assertEquals(list("four", "five"), windows.get(4)); + assertEquals(list("five"), windows.get(5)); + } + + @Test + public void skipAndCountWindowsWithGaps() { + Flowable<String> subject = Flowable.just("one", "two", "three", "four", "five"); + Flowable<Flowable<String>> windowed = subject.window(2, 3); + + List<List<String>> windows = toLists(windowed); + + assertEquals(2, windows.size()); + assertEquals(list("one", "two"), windows.get(0)); + assertEquals(list("four", "five"), windows.get(1)); + } + + @Test + public void windowUnsubscribeNonOverlapping() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(); + + final AtomicInteger count = new AtomicInteger(); + Flowable.merge(Flowable.range(1, 10000).doOnNext(new Consumer<Integer>() { + + @Override + public void accept(Integer t1) { + count.incrementAndGet(); + } + + }).window(5).take(2)) + .subscribe(ts); + + ts.awaitDone(500, TimeUnit.MILLISECONDS); + ts.assertTerminated(); + ts.assertValues(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + // System.out.println(ts.getOnNextEvents()); + assertEquals(10, count.get()); + } + + @Test + public void windowUnsubscribeNonOverlappingAsyncSource() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(); + final AtomicInteger count = new AtomicInteger(); + Flowable.merge(Flowable.range(1, 100000) + .doOnNext(new Consumer<Integer>() { + + @Override + public void accept(Integer t1) { + count.incrementAndGet(); + } + + }) + .observeOn(Schedulers.computation()) + .window(5) + .take(2)) + .subscribe(ts); + ts.awaitDone(500, TimeUnit.MILLISECONDS); + ts.assertTerminated(); + ts.assertValues(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + // make sure we don't emit all values ... the unsubscribe should propagate + assertTrue(count.get() < 100000); + } + + @Test + public void windowUnsubscribeOverlapping() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(); + final AtomicInteger count = new AtomicInteger(); + Flowable.merge(Flowable.range(1, 10000).doOnNext(new Consumer<Integer>() { + + @Override + public void accept(Integer t1) { + count.incrementAndGet(); + } + + }).window(5, 4).take(2)).subscribe(ts); + ts.awaitDone(500, TimeUnit.MILLISECONDS); + ts.assertTerminated(); + // System.out.println(ts.getOnNextEvents()); + ts.assertValues(1, 2, 3, 4, 5, 5, 6, 7, 8, 9); + assertEquals(9, count.get()); + } + + @Test + public void windowUnsubscribeOverlappingAsyncSource() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(); + final AtomicInteger count = new AtomicInteger(); + Flowable.merge(Flowable.range(1, 100000) + .doOnNext(new Consumer<Integer>() { + + @Override + public void accept(Integer t1) { + count.incrementAndGet(); + } + + }) + .observeOn(Schedulers.computation()) + .window(5, 4) + .take(2), 128) + .subscribe(ts); + ts.awaitDone(500, TimeUnit.MILLISECONDS); + ts.assertTerminated(); + ts.assertValues(1, 2, 3, 4, 5, 5, 6, 7, 8, 9); + // make sure we don't emit all values ... the unsubscribe should propagate + // assertTrue(count.get() < 100000); // disabled: a small hiccup in the consumption may allow the source to run to completion + } + + private List<String> list(String... args) { + List<String> list = new ArrayList<>(); + for (String arg : args) { + list.add(arg); + } + return list; + } + + @Test + public void backpressureOuter() { + Flowable<Flowable<Integer>> source = Flowable.range(1, 10).window(3); + + final List<Integer> list = new ArrayList<>(); + + final Subscriber<Integer> subscriber = TestHelper.mockSubscriber(); + + source.subscribe(new DefaultSubscriber<Flowable<Integer>>() { + @Override + public void onStart() { + request(1); + } + + @Override + public void onNext(Flowable<Integer> t) { + t.subscribe(new DefaultSubscriber<Integer>() { + @Override + public void onNext(Integer t) { + list.add(t); + } + + @Override + public void onError(Throwable e) { + subscriber.onError(e); + } + + @Override + public void onComplete() { + subscriber.onComplete(); + } + }); + } + + @Override + public void onError(Throwable e) { + subscriber.onError(e); + } + + @Override + public void onComplete() { + subscriber.onComplete(); + } + }); + + assertEquals(Arrays.asList(1, 2, 3), list); + + verify(subscriber, never()).onError(any(Throwable.class)); + verify(subscriber, times(1)).onComplete(); // 1 inner + } + + public static Flowable<Integer> hotStream() { + return Flowable.unsafeCreate(new Publisher<Integer>() { + @Override + public void subscribe(Subscriber<? super Integer> s) { + BooleanSubscription bs = new BooleanSubscription(); + s.onSubscribe(bs); + while (!bs.isCancelled()) { + // burst some number of items + for (int i = 0; i < Math.random() * 20; i++) { + s.onNext(i); + } + try { + // sleep for a random amount of time + // NOTE: Only using Thread.sleep here as an artificial demo. + Thread.sleep((long) (Math.random() * 200)); + } catch (Exception e) { + // do nothing + } + } + System.out.println("Hot done."); + } + }).subscribeOn(Schedulers.newThread()); // use newThread since we are using sleep to block + } + + @Test + public void takeFlatMapCompletes() { + TestSubscriber<Integer> ts = new TestSubscriber<>(); + + final int indicator = 999999999; + + hotStream() + .window(10) + .take(2) + .flatMap(new Function<Flowable<Integer>, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Flowable<Integer> w) { + return w.startWithItem(indicator); + } + }).subscribe(ts); + + ts.awaitDone(2, TimeUnit.SECONDS); + ts.assertComplete(); + ts.assertValueCount(22); + } + + @Test + public void backpressureOuterInexact() { + TestSubscriber<List<Integer>> ts = new TestSubscriber<>(0L); + + Flowable.range(1, 5) + .window(2, 1) + .map(new Function<Flowable<Integer>, Flowable<List<Integer>>>() { + @Override + public Flowable<List<Integer>> apply(Flowable<Integer> t) { + return t.toList().toFlowable(); + } + }) + .concatMapEager(new Function<Flowable<List<Integer>>, Publisher<List<Integer>>>() { + @Override + public Publisher<List<Integer>> apply(Flowable<List<Integer>> v) { + return v; + } + }) + .subscribe(ts); + + ts.assertNoErrors(); + ts.assertNoValues(); + ts.assertNotComplete(); + + ts.request(2); + + ts.assertValues(Arrays.asList(1, 2), Arrays.asList(2, 3)); + ts.assertNoErrors(); + ts.assertNotComplete(); + + ts.request(5); + + System.out.println(ts.values()); + + ts.assertValues(Arrays.asList(1, 2), Arrays.asList(2, 3), + Arrays.asList(3, 4), Arrays.asList(4, 5), Arrays.asList(5)); + ts.assertNoErrors(); + ts.assertComplete(); + } + + @Test + public void dispose() { + TestHelper.checkDisposed(PublishProcessor.create().window(1)); + + TestHelper.checkDisposed(PublishProcessor.create().window(2, 1)); + + TestHelper.checkDisposed(PublishProcessor.create().window(1, 2)); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeFlowable(new Function<Flowable<Object>, Flowable<Flowable<Object>>>() { + @Override + public Flowable<Flowable<Object>> apply(Flowable<Object> f) throws Exception { + return f.window(1); + } + }); + + TestHelper.checkDoubleOnSubscribeFlowable(new Function<Flowable<Object>, Flowable<Flowable<Object>>>() { + @Override + public Flowable<Flowable<Object>> apply(Flowable<Object> f) throws Exception { + return f.window(2, 1); + } + }); + + TestHelper.checkDoubleOnSubscribeFlowable(new Function<Flowable<Object>, Flowable<Flowable<Object>>>() { + @Override + public Flowable<Flowable<Object>> apply(Flowable<Object> f) throws Exception { + return f.window(1, 2); + } + }); + } + + @Test + public void errorExact() { + Flowable.error(new TestException()) + .window(1) + .test() + .assertFailure(TestException.class); + } + + @Test + public void errorSkip() { + Flowable.error(new TestException()) + .window(1, 2) + .test() + .assertFailure(TestException.class); + } + + @Test + public void errorOverlap() { + Flowable.error(new TestException()) + .window(2, 1) + .test() + .assertFailure(TestException.class); + } + + @SuppressWarnings("unchecked") + @Test + public void errorExactInner() { + @SuppressWarnings("rawtypes") + final TestSubscriber[] to = { null }; + Flowable.just(1).concatWith(Flowable.<Integer>error(new TestException())) + .window(2) + .doOnNext(new Consumer<Flowable<Integer>>() { + @Override + public void accept(Flowable<Integer> w) throws Exception { + to[0] = w.test(); + } + }) + .test() + .assertError(TestException.class); + + to[0].assertFailure(TestException.class, 1); + } + + @SuppressWarnings("unchecked") + @Test + public void errorSkipInner() { + @SuppressWarnings("rawtypes") + final TestSubscriber[] to = { null }; + Flowable.just(1).concatWith(Flowable.<Integer>error(new TestException())) + .window(2, 3) + .doOnNext(new Consumer<Flowable<Integer>>() { + @Override + public void accept(Flowable<Integer> w) throws Exception { + to[0] = w.test(); + } + }) + .test() + .assertError(TestException.class); + + to[0].assertFailure(TestException.class, 1); + } + + @SuppressWarnings("unchecked") + @Test + public void errorOverlapInner() { + @SuppressWarnings("rawtypes") + final TestSubscriber[] to = { null }; + Flowable.just(1).concatWith(Flowable.<Integer>error(new TestException())) + .window(3, 2) + .doOnNext(new Consumer<Flowable<Integer>>() { + @Override + public void accept(Flowable<Integer> w) throws Exception { + to[0] = w.test(); + } + }) + .test() + .assertError(TestException.class); + + to[0].assertFailure(TestException.class, 1); + } + + @Test + public void cancellingWindowCancelsUpstreamSize() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + + TestSubscriber<Integer> ts = pp.window(10) + .take(1) + .flatMap(new Function<Flowable<Integer>, Publisher<Integer>>() { + @Override + public Publisher<Integer> apply(Flowable<Integer> w) throws Throwable { + return w.take(1); + } + }) + .test(); + + assertTrue(pp.hasSubscribers()); + + pp.onNext(1); + + ts + .assertResult(1); + + assertFalse("Processor still has subscribers!", pp.hasSubscribers()); + } + + @Test + public void windowAbandonmentCancelsUpstreamSize() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + + final AtomicReference<Flowable<Integer>> inner = new AtomicReference<>(); + + TestSubscriber<Flowable<Integer>> ts = pp.window(10) + .take(1) + .doOnNext(new Consumer<Flowable<Integer>>() { + @Override + public void accept(Flowable<Integer> v) throws Throwable { + inner.set(v); + } + }) + .test(); + + assertTrue(pp.hasSubscribers()); + + pp.onNext(1); + + ts + .assertValueCount(1) + .assertNoErrors() + .assertComplete(); + + assertFalse("Processor still has subscribers!", pp.hasSubscribers()); + + inner.get().test().assertResult(1); + } + + @Test + public void cancellingWindowCancelsUpstreamSkip() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + + TestSubscriber<Integer> ts = pp.window(5, 10) + .take(1) + .flatMap(new Function<Flowable<Integer>, Publisher<Integer>>() { + @Override + public Publisher<Integer> apply(Flowable<Integer> w) throws Throwable { + return w.take(1); + } + }) + .test(); + + assertTrue(pp.hasSubscribers()); + + pp.onNext(1); + + ts + .assertResult(1); + + assertFalse("Processor still has subscribers!", pp.hasSubscribers()); + } + + @Test + public void windowAbandonmentCancelsUpstreamSkip() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + + final AtomicReference<Flowable<Integer>> inner = new AtomicReference<>(); + + TestSubscriber<Flowable<Integer>> ts = pp.window(5, 10) + .take(1) + .doOnNext(new Consumer<Flowable<Integer>>() { + @Override + public void accept(Flowable<Integer> v) throws Throwable { + inner.set(v); + } + }) + .test(); + + assertTrue(pp.hasSubscribers()); + + pp.onNext(1); + + ts + .assertValueCount(1) + .assertNoErrors() + .assertComplete(); + + assertFalse("Processor still has subscribers!", pp.hasSubscribers()); + + inner.get().test().assertResult(1); + } + + @Test + public void cancellingWindowCancelsUpstreamOverlap() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + + TestSubscriber<Integer> ts = pp.window(5, 3) + .take(1) + .flatMap(new Function<Flowable<Integer>, Publisher<Integer>>() { + @Override + public Publisher<Integer> apply(Flowable<Integer> w) throws Throwable { + return w.take(1); + } + }) + .test(); + + assertTrue(pp.hasSubscribers()); + + pp.onNext(1); + + ts + .assertResult(1); + + assertFalse("Processor still has subscribers!", pp.hasSubscribers()); + } + + @Test + public void windowAbandonmentCancelsUpstreamOverlap() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + + final AtomicReference<Flowable<Integer>> inner = new AtomicReference<>(); + + TestSubscriber<Flowable<Integer>> ts = pp.window(5, 3) + .take(1) + .doOnNext(new Consumer<Flowable<Integer>>() { + @Override + public void accept(Flowable<Integer> v) throws Throwable { + inner.set(v); + } + }) + .test(); + + assertTrue(pp.hasSubscribers()); + + pp.onNext(1); + + ts + .assertValueCount(1) + .assertNoErrors() + .assertComplete(); + + assertFalse("Processor still has subscribers!", pp.hasSubscribers()); + + inner.get().test().assertResult(1); + } + + @Test + public void badRequestExact() { + TestHelper.assertBadRequestReported(Flowable.never().window(1)); + } + + @Test + public void badRequestSkip() { + TestHelper.assertBadRequestReported(Flowable.never().window(1, 2)); + } + + @Test + public void badRequestOverlap() { + TestHelper.assertBadRequestReported(Flowable.never().window(2, 1)); + } + + @Test + public void skipEmpty() { + Flowable.empty() + .window(1, 2) + .test() + .assertResult(); + } + + @Test + public void exactEmpty() { + Flowable.empty() + .window(2) + .test() + .assertResult(); + } + + @Test + public void skipMultipleRequests() { + Flowable.range(1, 10) + .window(1, 2) + .doOnNext(w -> w.test()) + .rebatchRequests(1) + .test() + .assertComplete(); + } + + @Test + public void skipOne() { + Flowable.just(1) + .window(2, 3) + .flatMap(v -> v) + .test() + .assertResult(1); + } + + @Test + public void overlapMultipleRequests() { + Flowable.range(1, 10) + .window(2, 1) + .doOnNext(w -> w.test()) + .rebatchRequests(1) + .test() + .assertComplete(); + } + + @Test + public void overlapCancelAfterWindow() { + Flowable.range(1, 10) + .window(2, 1) + .takeUntil(v -> true) + .doOnNext(w -> w.test()) + .test(0L) + .requestMore(10) + .assertComplete(); + } + + @Test + public void overlapEmpty() { + Flowable.empty() + .window(2, 1) + .test() + .assertResult(); + } + + @Test + public void overlapEmptyNoRequest() { + Flowable.empty() + .window(2, 1) + .test(0L) + .assertResult(); + } + + @Test + public void overlapMoreWorkAfterOnNext() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + AtomicBoolean once = new AtomicBoolean(); + + TestSubscriber<Flowable<Integer>> ts = pp.window(2, 1) + .doOnNext(v -> { + v.test(); + if (once.compareAndSet(false, true)) { + pp.onNext(2); + pp.onComplete(); + } + }) + .test(); + + pp.onNext(1); + + ts.assertComplete(); + } + + @Test + public void moreQueuedClean() { + Flowable.range(1, 10) + .window(5, 1) + .doOnNext(w -> w.test()) + .test(3) + .cancel(); + } + + @Test + public void cancelWithoutWindowSize() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + + TestSubscriber<Flowable<Integer>> ts = pp.window(10) + .test(); + + assertTrue(pp.hasSubscribers()); + + ts.cancel(); + + assertFalse("Subject still has subscribers!", pp.hasSubscribers()); + } + + @Test + public void cancelAfterAbandonmentSize() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + + TestSubscriber<Flowable<Integer>> ts = pp.window(10) + .test(); + + assertTrue(pp.hasSubscribers()); + + pp.onNext(1); + + ts.cancel(); + + assertFalse("Subject still has subscribers!", pp.hasSubscribers()); + } + + @Test + public void cancelWithoutWindowSkip() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + + TestSubscriber<Flowable<Integer>> ts = pp.window(10, 15) + .test(); + + assertTrue(pp.hasSubscribers()); + + ts.cancel(); + + assertFalse("Subject still has subscribers!", pp.hasSubscribers()); + } + + @Test + public void cancelAfterAbandonmentSkip() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + + TestSubscriber<Flowable<Integer>> ts = pp.window(10, 15) + .test(); + + assertTrue(pp.hasSubscribers()); + + pp.onNext(1); + + ts.cancel(); + + assertFalse("Subject still has subscribers!", pp.hasSubscribers()); + } + + @Test + public void cancelWithoutWindowOverlap() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + + TestSubscriber<Flowable<Integer>> ts = pp.window(10, 5) + .test(); + + assertTrue(pp.hasSubscribers()); + + ts.cancel(); + + assertFalse("Subject still has subscribers!", pp.hasSubscribers()); + } + + @Test + public void cancelAfterAbandonmentOverlap() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + + TestSubscriber<Flowable<Integer>> ts = pp.window(10, 5) + .test(); + + assertTrue(pp.hasSubscribers()); + + pp.onNext(1); + + ts.cancel(); + + assertFalse("Subject still has subscribers!", pp.hasSubscribers()); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableWindowWithStartEndFlowableTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableWindowWithStartEndFlowableTest.java new file mode 100644 index 0000000000..f2431eb8db --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableWindowWithStartEndFlowableTest.java @@ -0,0 +1,719 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import static org.junit.Assert.*; + +import java.io.IOException; +import java.util.*; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.*; + +import org.junit.*; +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.internal.subscriptions.BooleanSubscription; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.processors.*; +import io.reactivex.rxjava3.schedulers.TestScheduler; +import io.reactivex.rxjava3.subscribers.*; +import io.reactivex.rxjava3.testsupport.*; + +public class FlowableWindowWithStartEndFlowableTest extends RxJavaTest { + + private TestScheduler scheduler; + private Scheduler.Worker innerScheduler; + + @Before + public void before() { + scheduler = new TestScheduler(); + innerScheduler = scheduler.createWorker(); + } + + @Test + public void flowableBasedOpenerAndCloser() { + final List<String> list = new ArrayList<>(); + final List<List<String>> lists = new ArrayList<>(); + + Flowable<String> source = Flowable.unsafeCreate(new Publisher<String>() { + @Override + public void subscribe(Subscriber<? super String> subscriber) { + subscriber.onSubscribe(new BooleanSubscription()); + push(subscriber, "one", 10); + push(subscriber, "two", 60); + push(subscriber, "three", 110); + push(subscriber, "four", 160); + push(subscriber, "five", 210); + complete(subscriber, 500); + } + }); + + Flowable<Object> openings = Flowable.unsafeCreate(new Publisher<Object>() { + @Override + public void subscribe(Subscriber<? super Object> subscriber) { + subscriber.onSubscribe(new BooleanSubscription()); + push(subscriber, new Object(), 50); + push(subscriber, new Object(), 200); + complete(subscriber, 250); + } + }); + + Function<Object, Flowable<Object>> closer = new Function<Object, Flowable<Object>>() { + @Override + public Flowable<Object> apply(Object opening) { + return Flowable.unsafeCreate(new Publisher<Object>() { + @Override + public void subscribe(Subscriber<? super Object> subscriber) { + subscriber.onSubscribe(new BooleanSubscription()); + push(subscriber, new Object(), 100); + complete(subscriber, 101); + } + }); + } + }; + + Flowable<Flowable<String>> windowed = source.window(openings, closer); + windowed.subscribe(observeWindow(list, lists)); + + scheduler.advanceTimeTo(500, TimeUnit.MILLISECONDS); + assertEquals(2, lists.size()); + assertEquals(lists.get(0), list("two", "three")); + assertEquals(lists.get(1), list("five")); + } + + private List<String> list(String... args) { + List<String> list = new ArrayList<>(); + for (String arg : args) { + list.add(arg); + } + return list; + } + + private <T> void push(final Subscriber<T> subscriber, final T value, int delay) { + innerScheduler.schedule(new Runnable() { + @Override + public void run() { + subscriber.onNext(value); + } + }, delay, TimeUnit.MILLISECONDS); + } + + private void complete(final Subscriber<?> subscriber, int delay) { + innerScheduler.schedule(new Runnable() { + @Override + public void run() { + subscriber.onComplete(); + } + }, delay, TimeUnit.MILLISECONDS); + } + + private Consumer<Flowable<String>> observeWindow(final List<String> list, final List<List<String>> lists) { + return new Consumer<Flowable<String>>() { + @Override + public void accept(Flowable<String> stringFlowable) { + stringFlowable.subscribe(new DefaultSubscriber<String>() { + @Override + public void onComplete() { + lists.add(new ArrayList<>(list)); + list.clear(); + } + + @Override + public void onError(Throwable e) { + fail(e.getMessage()); + } + + @Override + public void onNext(String args) { + list.add(args); + } + }); + } + }; + } + + @Test + public void noUnsubscribeAndNoLeak() { + PublishProcessor<Integer> source = PublishProcessor.create(); + + PublishProcessor<Integer> open = PublishProcessor.create(); + final PublishProcessor<Integer> close = PublishProcessor.create(); + + TestSubscriber<Flowable<Integer>> ts = new TestSubscriber<>(); + + source.window(open, new Function<Integer, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Integer t) { + return close; + } + }) + .doOnNext(new Consumer<Flowable<Integer>>() { + @Override + public void accept(Flowable<Integer> w) throws Throwable { + w.subscribe(Functions.emptyConsumer(), Functions.emptyConsumer()); // avoid abandonment + } + }) + .subscribe(ts); + + open.onNext(1); + source.onNext(1); + + assertTrue(open.hasSubscribers()); + assertTrue(close.hasSubscribers()); + + close.onNext(1); + + assertFalse(close.hasSubscribers()); + + source.onComplete(); + + ts.assertComplete(); + ts.assertNoErrors(); + ts.assertValueCount(1); + + assertFalse(ts.isCancelled()); + assertFalse(open.hasSubscribers()); + assertFalse(close.hasSubscribers()); + } + + @Test + public void unsubscribeAll() { + PublishProcessor<Integer> source = PublishProcessor.create(); + + PublishProcessor<Integer> open = PublishProcessor.create(); + final PublishProcessor<Integer> close = PublishProcessor.create(); + + TestSubscriber<Flowable<Integer>> ts = new TestSubscriber<>(); + + source.window(open, new Function<Integer, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Integer t) { + return close; + } + }) + .doOnNext(new Consumer<Flowable<Integer>>() { + @Override + public void accept(Flowable<Integer> w) throws Throwable { + w.subscribe(Functions.emptyConsumer(), Functions.emptyConsumer()); // avoid abandonment + } + }) + .subscribe(ts); + + open.onNext(1); + + assertTrue(open.hasSubscribers()); + assertTrue(close.hasSubscribers()); + + ts.cancel(); + + // Disposing the outer sequence stops the opening of new windows + assertFalse(open.hasSubscribers()); + // FIXME subject has subscribers because of the open window + assertTrue(close.hasSubscribers()); + } + + @Test + public void dispose() { + TestHelper.checkDisposed(Flowable.just(1).window(Flowable.just(2), Functions.justFunction(Flowable.never()))); + } + + @Test + public void reentrant() { + final FlowableProcessor<Integer> pp = PublishProcessor.<Integer>create(); + + TestSubscriber<Integer> ts = new TestSubscriber<Integer>() { + @Override + public void onNext(Integer t) { + super.onNext(t); + if (t == 1) { + pp.onNext(2); + pp.onComplete(); + } + } + }; + + pp.window(BehaviorProcessor.createDefault(1), Functions.justFunction(Flowable.never())) + .flatMap(new Function<Flowable<Integer>, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Flowable<Integer> v) throws Exception { + return v; + } + }) + .subscribe(ts); + + pp.onNext(1); + + ts + .awaitDone(1, TimeUnit.SECONDS) + .assertResult(1, 2); + } + + @Test + public void boundarySelectorNormal() { + PublishProcessor<Integer> source = PublishProcessor.create(); + PublishProcessor<Integer> start = PublishProcessor.create(); + final PublishProcessor<Integer> end = PublishProcessor.create(); + + TestSubscriber<Integer> ts = source.window(start, new Function<Integer, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Integer v) throws Exception { + return end; + } + }) + .flatMap(Functions.<Flowable<Integer>>identity()) + .test(); + + start.onNext(0); + + source.onNext(1); + source.onNext(2); + source.onNext(3); + source.onNext(4); + + start.onNext(1); + + source.onNext(5); + source.onNext(6); + + end.onNext(1); + + start.onNext(2); + + TestHelper.emit(source, 7, 8); + + ts.assertResult(1, 2, 3, 4, 5, 5, 6, 6, 7, 8); + } + + @Test + public void startError() { + PublishProcessor<Integer> source = PublishProcessor.create(); + PublishProcessor<Integer> start = PublishProcessor.create(); + final PublishProcessor<Integer> end = PublishProcessor.create(); + + TestSubscriber<Integer> ts = source.window(start, new Function<Integer, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Integer v) throws Exception { + return end; + } + }) + .flatMap(Functions.<Flowable<Integer>>identity()) + .test(); + + start.onError(new TestException()); + + ts.assertFailure(TestException.class); + + assertFalse("Source has observers!", source.hasSubscribers()); + assertFalse("Start has observers!", start.hasSubscribers()); + assertFalse("End has observers!", end.hasSubscribers()); + } + + @Test + @SuppressUndeliverable + public void endError() { + PublishProcessor<Integer> source = PublishProcessor.create(); + PublishProcessor<Integer> start = PublishProcessor.create(); + final PublishProcessor<Integer> end = PublishProcessor.create(); + + TestSubscriber<Integer> ts = source.window(start, new Function<Integer, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Integer v) throws Exception { + return end; + } + }) + .flatMap(Functions.<Flowable<Integer>>identity()) + .test(); + + start.onNext(1); + end.onError(new TestException()); + + ts.assertFailure(TestException.class); + + assertFalse("Source has observers!", source.hasSubscribers()); + assertFalse("Start has observers!", start.hasSubscribers()); + assertFalse("End has observers!", end.hasSubscribers()); + } + + @Test + public void mainError() { + Flowable.<Integer>error(new TestException()) + .window(Flowable.never(), Functions.justFunction(Flowable.just(1))) + .flatMap(Functions.<Flowable<Integer>>identity()) + .test() + .assertFailure(TestException.class); + } + + @Test + public void windowCloseIngoresCancel() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + BehaviorProcessor.createDefault(1) + .window(BehaviorProcessor.createDefault(1), new Function<Integer, Publisher<Integer>>() { + @Override + public Publisher<Integer> apply(Integer f) throws Exception { + return new Flowable<Integer>() { + @Override + protected void subscribeActual( + Subscriber<? super Integer> s) { + s.onSubscribe(new BooleanSubscription()); + s.onNext(1); + s.onNext(2); + s.onError(new TestException()); + } + }; + } + }) + .doOnNext(new Consumer<Flowable<Integer>>() { + @Override + public void accept(Flowable<Integer> w) throws Throwable { + w.subscribe(Functions.emptyConsumer(), Functions.emptyConsumer()); // avoid abandonment + } + }) + .test() + .assertValueCount(1) + .assertNoErrors() + .assertNotComplete(); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + static Flowable<Integer> flowableDisposed(final AtomicBoolean ref) { + return Flowable.just(1).concatWith(Flowable.<Integer>never()) + .doOnCancel(new Action() { + @Override + public void run() throws Exception { + ref.set(true); + } + }); + } + + @Test + public void mainAndBoundaryDisposeOnNoWindows() { + AtomicBoolean mainDisposed = new AtomicBoolean(); + AtomicBoolean openDisposed = new AtomicBoolean(); + final AtomicBoolean closeDisposed = new AtomicBoolean(); + + flowableDisposed(mainDisposed) + .window(flowableDisposed(openDisposed), new Function<Integer, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Integer v) throws Exception { + return flowableDisposed(closeDisposed); + } + }) + .doOnNext(new Consumer<Flowable<Integer>>() { + @Override + public void accept(Flowable<Integer> w) throws Throwable { + w.subscribe(Functions.emptyConsumer(), Functions.emptyConsumer()); // avoid abandonment + } + }) + .to(TestHelper.<Flowable<Integer>>testConsumer()) + .assertSubscribed() + .assertNoErrors() + .assertNotComplete() + .cancel(); + + assertTrue(mainDisposed.get()); + assertTrue(openDisposed.get()); + assertTrue(closeDisposed.get()); + } + + @Test + public void mainWindowMissingBackpressure() { + PublishProcessor<Integer> source = PublishProcessor.create(); + PublishProcessor<Integer> boundary = PublishProcessor.create(); + + TestSubscriber<Flowable<Integer>> ts = source.window(boundary, Functions.justFunction(Flowable.never())) + .test(0L) + ; + + ts.assertEmpty(); + + boundary.onNext(1); + + ts.assertFailure(MissingBackpressureException.class); + + assertFalse(source.hasSubscribers()); + assertFalse(boundary.hasSubscribers()); + } + + @Test + public void cancellingWindowCancelsUpstream() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + + TestSubscriber<Integer> ts = pp.window(Flowable.just(1).concatWith(Flowable.<Integer>never()), Functions.justFunction(Flowable.never())) + .take(1) + .flatMap(new Function<Flowable<Integer>, Publisher<Integer>>() { + @Override + public Publisher<Integer> apply(Flowable<Integer> w) throws Throwable { + return w.take(1); + } + }) + .test(); + + assertTrue(pp.hasSubscribers()); + + pp.onNext(1); + + ts + .assertResult(1); + + assertFalse("Processor still has subscribers!", pp.hasSubscribers()); + } + + @Test + public void windowAbandonmentCancelsUpstream() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + + final AtomicReference<Flowable<Integer>> inner = new AtomicReference<>(); + + TestSubscriber<Flowable<Integer>> ts = pp.window(Flowable.<Integer>just(1).concatWith(Flowable.<Integer>never()), + Functions.justFunction(Flowable.never())) + .doOnNext(new Consumer<Flowable<Integer>>() { + @Override + public void accept(Flowable<Integer> v) throws Throwable { + inner.set(v); + } + }) + .test(); + + assertTrue(pp.hasSubscribers()); + + ts + .assertValueCount(1) + ; + + pp.onNext(1); + + assertTrue(pp.hasSubscribers()); + + ts.cancel(); + + ts + .assertValueCount(1) + .assertNoErrors() + .assertNotComplete(); + + assertFalse("Processor still has subscribers!", pp.hasSubscribers()); + + inner.get().test().assertResult(); + } + + @Test + public void closingIndicatorFunctionCrash() { + + PublishProcessor<Integer> source = PublishProcessor.create(); + PublishProcessor<Integer> boundary = PublishProcessor.create(); + + TestSubscriber<Flowable<Integer>> ts = source.window(boundary, new Function<Integer, Publisher<Object>>() { + @Override + public Publisher<Object> apply(Integer end) throws Throwable { + throw new TestException(); + } + }) + .test() + ; + + ts.assertEmpty(); + + boundary.onNext(1); + + ts.assertFailure(TestException.class); + + assertFalse(source.hasSubscribers()); + assertFalse(boundary.hasSubscribers()); + + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeFlowable(o -> o.window(Flowable.never(), v -> Flowable.never())); + } + + @Test + public void openError() throws Throwable { + TestHelper.withErrorTracking(errors -> { + TestException ex1 = new TestException(); + TestException ex2 = new TestException(); + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + AtomicReference<Subscriber<? super Integer>> ref1 = new AtomicReference<>(); + AtomicReference<Subscriber<? super Integer>> ref2 = new AtomicReference<>(); + + Flowable<Integer> f1 = Flowable.<Integer>fromPublisher(ref1::set); + Flowable<Integer> f2 = Flowable.<Integer>fromPublisher(ref2::set); + + TestSubscriber<Flowable<Integer>> ts = BehaviorProcessor.createDefault(1) + .window(f1, v -> f2) + .doOnNext(w -> w.test()) + .test(); + + ref1.get().onSubscribe(new BooleanSubscription()); + ref1.get().onNext(1); + ref2.get().onSubscribe(new BooleanSubscription()); + + TestHelper.race( + () -> ref1.get().onError(ex1), + () -> ref2.get().onError(ex2) + ); + + ts.assertError(RuntimeException.class); + + if (!errors.isEmpty()) { + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } + + errors.clear(); + } + }); + } + + @Test + public void closeError() throws Throwable { + TestHelper.withErrorTracking(errors -> { + AtomicReference<Subscriber<? super Integer>> ref1 = new AtomicReference<>(); + AtomicReference<Subscriber<? super Integer>> ref2 = new AtomicReference<>(); + + Flowable<Integer> f1 = Flowable.<Integer>unsafeCreate(ref1::set); + Flowable<Integer> f2 = Flowable.<Integer>unsafeCreate(ref2::set); + + TestSubscriber<Integer> ts = BehaviorProcessor.createDefault(1) + .window(f1, v -> f2) + .flatMap(v -> v) + .test(); + + ref1.get().onSubscribe(new BooleanSubscription()); + ref1.get().onNext(1); + ref2.get().onSubscribe(new BooleanSubscription()); + + ref2.get().onError(new TestException()); + ref2.get().onError(new TestException()); + + ts.assertFailure(TestException.class); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + }); + } + + @Test + public void upstreamFailsBeforeFirstWindow() { + Flowable.error(new TestException()) + .window(Flowable.never(), v -> Flowable.never()) + .test() + .assertFailure(TestException.class); + } + + @Test + public void windowOpenMainCompletes() { + AtomicReference<Subscriber<? super Integer>> ref1 = new AtomicReference<>(); + + PublishProcessor<Object> pp = PublishProcessor.create(); + Flowable<Integer> f1 = Flowable.<Integer>unsafeCreate(ref1::set); + + AtomicInteger counter = new AtomicInteger(); + + TestSubscriber<Flowable<Object>> ts = pp + .window(f1, v -> Flowable.never()) + .doOnNext(w -> { + if (counter.getAndIncrement() == 0) { + ref1.get().onNext(2); + pp.onNext(1); + pp.onComplete(); + } + w.test(); + }) + .test(); + + ref1.get().onSubscribe(new BooleanSubscription()); + ref1.get().onNext(1); + + ts.assertComplete(); + } + + @Test + public void windowOpenMainError() { + AtomicReference<Subscriber<? super Integer>> ref1 = new AtomicReference<>(); + + PublishProcessor<Object> pp = PublishProcessor.create(); + Flowable<Integer> f1 = Flowable.<Integer>unsafeCreate(ref1::set); + + AtomicInteger counter = new AtomicInteger(); + + TestSubscriber<Flowable<Object>> ts = pp + .window(f1, v -> Flowable.never()) + .doOnNext(w -> { + if (counter.getAndIncrement() == 0) { + ref1.get().onNext(2); + pp.onNext(1); + pp.onError(new TestException()); + } + w.test(); + }) + .test(); + + ref1.get().onSubscribe(new BooleanSubscription()); + ref1.get().onNext(1); + + ts.assertError(TestException.class); + } + + @Test + public void windowOpenIgnoresDispose() { + AtomicReference<Subscriber<? super Integer>> ref1 = new AtomicReference<>(); + + PublishProcessor<Object> pp = PublishProcessor.create(); + Flowable<Integer> f1 = Flowable.<Integer>unsafeCreate(ref1::set); + + TestSubscriber<Flowable<Object>> ts = pp + .window(f1, v -> Flowable.never()) + .take(1) + .doOnNext(w -> { + w.test(); + }) + .test(); + + ref1.get().onSubscribe(new BooleanSubscription()); + ref1.get().onNext(1); + ref1.get().onNext(2); + + ts.assertValueCount(1); + } + + @Test + public void badRequest() { + TestHelper.assertBadRequestReported(Flowable.never().window(Flowable.never(), v -> Flowable.never())); + } + + @Test + public void mainIgnoresCancelBeforeOnError() throws Throwable { + TestHelper.withErrorTracking(errors -> { + Flowable.fromPublisher(s -> { + s.onSubscribe(new BooleanSubscription()); + s.onNext(1); + s.onError(new IOException()); + }) + .window(BehaviorProcessor.createDefault(1), v -> Flowable.error(new TestException())) + .doOnNext(w -> w.test()) + .test() + .assertError(TestException.class); + + TestHelper.assertUndeliverable(errors, 0, IOException.class); + }); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableWindowWithTimeTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableWindowWithTimeTest.java new file mode 100644 index 0000000000..6982b00cea --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableWindowWithTimeTest.java @@ -0,0 +1,1351 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import static org.junit.Assert.*; + +import java.util.*; +import java.util.concurrent.*; +import java.util.concurrent.atomic.*; + +import org.junit.*; +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.internal.subscriptions.BooleanSubscription; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.processors.*; +import io.reactivex.rxjava3.schedulers.*; +import io.reactivex.rxjava3.subscribers.*; +import io.reactivex.rxjava3.testsupport.*; + +public class FlowableWindowWithTimeTest extends RxJavaTest { + + private TestScheduler scheduler; + private Scheduler.Worker innerScheduler; + + @Before + public void before() { + scheduler = new TestScheduler(); + innerScheduler = scheduler.createWorker(); + } + + @Test + public void timedAndCount() { + final List<String> list = new ArrayList<>(); + final List<List<String>> lists = new ArrayList<>(); + + Flowable<String> source = Flowable.unsafeCreate(new Publisher<String>() { + @Override + public void subscribe(Subscriber<? super String> subscriber) { + subscriber.onSubscribe(new BooleanSubscription()); + push(subscriber, "one", 10); + push(subscriber, "two", 90); + push(subscriber, "three", 110); + push(subscriber, "four", 190); + push(subscriber, "five", 210); + complete(subscriber, 250); + } + }); + + Flowable<Flowable<String>> windowed = source.window(100, TimeUnit.MILLISECONDS, scheduler, 2); + windowed.subscribe(observeWindow(list, lists)); + + scheduler.advanceTimeTo(95, TimeUnit.MILLISECONDS); + assertEquals(1, lists.size()); + assertEquals(lists.get(0), list("one", "two")); + + scheduler.advanceTimeTo(195, TimeUnit.MILLISECONDS); + assertEquals(3, lists.size()); + assertTrue(lists.get(1).isEmpty()); + assertEquals(lists.get(2), list("three", "four")); + + scheduler.advanceTimeTo(300, TimeUnit.MILLISECONDS); + assertEquals(5, lists.size()); + assertTrue(lists.get(3).isEmpty()); + assertEquals(lists.get(4), list("five")); + } + + @Test + public void timed() { + final List<String> list = new ArrayList<>(); + final List<List<String>> lists = new ArrayList<>(); + + Flowable<String> source = Flowable.unsafeCreate(new Publisher<String>() { + @Override + public void subscribe(Subscriber<? super String> subscriber) { + subscriber.onSubscribe(new BooleanSubscription()); + push(subscriber, "one", 98); + push(subscriber, "two", 99); + push(subscriber, "three", 99); // FIXME happens after the window is open + push(subscriber, "four", 101); + push(subscriber, "five", 102); + complete(subscriber, 150); + } + }); + + Flowable<Flowable<String>> windowed = source.window(100, TimeUnit.MILLISECONDS, scheduler); + windowed.subscribe(observeWindow(list, lists)); + + scheduler.advanceTimeTo(101, TimeUnit.MILLISECONDS); + assertEquals(1, lists.size()); + assertEquals(lists.get(0), list("one", "two", "three")); + + scheduler.advanceTimeTo(201, TimeUnit.MILLISECONDS); + assertEquals(2, lists.size()); + assertEquals(lists.get(1), list("four", "five")); + } + + private List<String> list(String... args) { + List<String> list = new ArrayList<>(); + for (String arg : args) { + list.add(arg); + } + return list; + } + + private <T> void push(final Subscriber<T> subscriber, final T value, int delay) { + innerScheduler.schedule(new Runnable() { + @Override + public void run() { + subscriber.onNext(value); + } + }, delay, TimeUnit.MILLISECONDS); + } + + private void complete(final Subscriber<?> subscriber, int delay) { + innerScheduler.schedule(new Runnable() { + @Override + public void run() { + subscriber.onComplete(); + } + }, delay, TimeUnit.MILLISECONDS); + } + + private <T> Consumer<Flowable<T>> observeWindow(final List<T> list, final List<List<T>> lists) { + return new Consumer<Flowable<T>>() { + @Override + public void accept(Flowable<T> stringFlowable) { + stringFlowable.subscribe(new DefaultSubscriber<T>() { + @Override + public void onComplete() { + lists.add(new ArrayList<>(list)); + list.clear(); + } + + @Override + public void onError(Throwable e) { + Assert.fail(e.getMessage()); + } + + @Override + public void onNext(T args) { + list.add(args); + } + }); + } + }; + } + + @Test + public void exactWindowSize() { + Flowable<Flowable<Integer>> source = Flowable.range(1, 10) + .window(1, TimeUnit.MINUTES, scheduler, 3); + + final List<Integer> list = new ArrayList<>(); + final List<List<Integer>> lists = new ArrayList<>(); + + source.subscribe(observeWindow(list, lists)); + + assertEquals(4, lists.size()); + assertEquals(3, lists.get(0).size()); + assertEquals(Arrays.asList(1, 2, 3), lists.get(0)); + assertEquals(3, lists.get(1).size()); + assertEquals(Arrays.asList(4, 5, 6), lists.get(1)); + assertEquals(3, lists.get(2).size()); + assertEquals(Arrays.asList(7, 8, 9), lists.get(2)); + assertEquals(1, lists.get(3).size()); + assertEquals(Arrays.asList(10), lists.get(3)); + } + + @Test + public void takeFlatMapCompletes() { + TestSubscriber<Integer> ts = new TestSubscriber<>(); + + final AtomicInteger wip = new AtomicInteger(); + + final int indicator = 999999999; + + FlowableWindowWithSizeTest.hotStream() + .window(300, TimeUnit.MILLISECONDS) + .take(10) + .doOnComplete(new Action() { + @Override + public void run() { + System.out.println("Main done!"); + } + }) + .flatMap(new Function<Flowable<Integer>, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Flowable<Integer> w) { + return w.startWithItem(indicator) + .doOnComplete(new Action() { + @Override + public void run() { + System.out.println("inner done: " + wip.incrementAndGet()); + } + }) + ; + } + }) + .doOnNext(new Consumer<Integer>() { + @Override + public void accept(Integer pv) { + System.out.println(pv); + } + }) + .subscribe(ts); + + ts.awaitDone(5, TimeUnit.SECONDS); + ts.assertComplete(); + Assert.assertTrue(ts.values().size() != 0); + } + + @Test + public void timespanTimeskipCustomSchedulerBufferSize() { + Flowable.range(1, 10) + .window(1, 1, TimeUnit.MINUTES, Schedulers.io(), 2) + .flatMap(Functions.<Flowable<Integer>>identity()) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + } + + @Test + public void timespanDefaultSchedulerSize() { + Flowable.range(1, 10) + .window(1, TimeUnit.MINUTES, 20) + .flatMap(Functions.<Flowable<Integer>>identity()) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + } + + @Test + public void timespanDefaultSchedulerSizeRestart() { + Flowable.range(1, 10) + .window(1, TimeUnit.MINUTES, 20, true) + .flatMap(Functions.<Flowable<Integer>>identity(), true) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + } + + @Test + public void invalidSpan() { + try { + Flowable.just(1).window(-99, 1, TimeUnit.SECONDS); + fail("Should have thrown!"); + } catch (IllegalArgumentException ex) { + assertEquals("timespan > 0 required but it was -99", ex.getMessage()); + } + } + + @Test + public void timespanTimeskipDefaultScheduler() { + Flowable.just(1) + .window(1, 1, TimeUnit.MINUTES) + .flatMap(Functions.<Flowable<Integer>>identity()) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(1); + } + + @Test + public void timespanTimeskipCustomScheduler() { + Flowable.just(1) + .window(1, 1, TimeUnit.MINUTES, Schedulers.io()) + .flatMap(Functions.<Flowable<Integer>>identity()) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(1); + } + + @Test + public void timeskipJustOverlap() { + Flowable.just(1) + .window(2, 1, TimeUnit.MINUTES, Schedulers.single()) + .flatMap(Functions.<Flowable<Integer>>identity()) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(1); + } + + @Test + public void timeskipJustSkip() { + Flowable.just(1) + .window(1, 2, TimeUnit.MINUTES, Schedulers.single()) + .flatMap(Functions.<Flowable<Integer>>identity()) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(1); + } + + @Test + public void timeskipSkipping() { + TestScheduler scheduler = new TestScheduler(); + + PublishProcessor<Integer> pp = PublishProcessor.create(); + + TestSubscriber<Integer> ts = pp.window(1, 2, TimeUnit.SECONDS, scheduler) + .flatMap(Functions.<Flowable<Integer>>identity()) + .test(); + + pp.onNext(1); + pp.onNext(2); + + scheduler.advanceTimeBy(1, TimeUnit.SECONDS); + + pp.onNext(3); + pp.onNext(4); + + scheduler.advanceTimeBy(1, TimeUnit.SECONDS); + + pp.onNext(5); + pp.onNext(6); + + scheduler.advanceTimeBy(1, TimeUnit.SECONDS); + + pp.onNext(7); + pp.onComplete(); + + ts.assertResult(1, 2, 5, 6); + } + + @Test + public void timeskipOverlapping() { + TestScheduler scheduler = new TestScheduler(); + + PublishProcessor<Integer> pp = PublishProcessor.create(); + + TestSubscriber<Integer> ts = pp.window(2, 1, TimeUnit.SECONDS, scheduler) + .flatMap(Functions.<Flowable<Integer>>identity()) + .test(); + + pp.onNext(1); + pp.onNext(2); + + scheduler.advanceTimeBy(1, TimeUnit.SECONDS); + + pp.onNext(3); + pp.onNext(4); + + scheduler.advanceTimeBy(1, TimeUnit.SECONDS); + + pp.onNext(5); + pp.onNext(6); + + scheduler.advanceTimeBy(1, TimeUnit.SECONDS); + + pp.onNext(7); + pp.onComplete(); + + ts.assertResult(1, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7); + } + + @Test + public void exactOnError() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + TestScheduler scheduler = new TestScheduler(); + + PublishProcessor<Integer> pp = PublishProcessor.create(); + + TestSubscriber<Integer> ts = pp.window(1, 1, TimeUnit.SECONDS, scheduler) + .flatMap(Functions.<Flowable<Integer>>identity()) + .test(); + + pp.onError(new TestException()); + + ts.assertFailure(TestException.class); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void overlappingOnError() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + TestScheduler scheduler = new TestScheduler(); + + PublishProcessor<Integer> pp = PublishProcessor.create(); + + TestSubscriber<Integer> ts = pp.window(2, 1, TimeUnit.SECONDS, scheduler) + .flatMap(Functions.<Flowable<Integer>>identity()) + .test(); + + pp.onError(new TestException()); + + ts.assertFailure(TestException.class); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void skipOnError() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + TestScheduler scheduler = new TestScheduler(); + + PublishProcessor<Integer> pp = PublishProcessor.create(); + + TestSubscriber<Integer> ts = pp.window(1, 2, TimeUnit.SECONDS, scheduler) + .flatMap(Functions.<Flowable<Integer>>identity()) + .test(); + + pp.onError(new TestException()); + + ts.assertFailure(TestException.class); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void exactBackpressure() { + TestScheduler scheduler = new TestScheduler(); + + PublishProcessor<Integer> pp = PublishProcessor.create(); + + TestSubscriber<Flowable<Integer>> ts = pp.window(1, 1, TimeUnit.SECONDS, scheduler) + .test(0L); + + scheduler.advanceTimeBy(1, TimeUnit.SECONDS); + + ts.assertFailure(MissingBackpressureException.class); + } + + @Test + public void skipBackpressure() { + TestScheduler scheduler = new TestScheduler(); + + PublishProcessor<Integer> pp = PublishProcessor.create(); + + TestSubscriber<Flowable<Integer>> ts = pp.window(1, 2, TimeUnit.SECONDS, scheduler) + .test(0L); + + scheduler.advanceTimeBy(1, TimeUnit.SECONDS); + + ts.assertFailure(MissingBackpressureException.class); + } + + @Test + public void overlapBackpressure() { + TestScheduler scheduler = new TestScheduler(); + + PublishProcessor<Integer> pp = PublishProcessor.create(); + + TestSubscriber<Flowable<Integer>> ts = pp.window(2, 1, TimeUnit.SECONDS, scheduler) + .test(0L); + + scheduler.advanceTimeBy(1, TimeUnit.SECONDS); + + ts.assertFailure(MissingBackpressureException.class); + } + + @Test + public void exactBackpressure2() { + TestScheduler scheduler = new TestScheduler(); + + PublishProcessor<Integer> pp = PublishProcessor.create(); + + TestSubscriber<Flowable<Integer>> ts = pp.window(1, 1, TimeUnit.SECONDS, scheduler) + .test(1L); + + scheduler.advanceTimeBy(2, TimeUnit.SECONDS); + + ts.assertError(MissingBackpressureException.class); + } + + @Test + public void skipBackpressure2() { + TestScheduler scheduler = new TestScheduler(); + + PublishProcessor<Integer> pp = PublishProcessor.create(); + + TestSubscriber<Flowable<Integer>> ts = pp.window(1, 2, TimeUnit.SECONDS, scheduler) + .test(1L); + + scheduler.advanceTimeBy(2, TimeUnit.SECONDS); + + ts.assertError(MissingBackpressureException.class); + } + + @Test + public void overlapBackpressure2() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + TestScheduler scheduler = new TestScheduler(); + + PublishProcessor<Integer> pp = PublishProcessor.create(); + + final TestSubscriber<Integer> tsInner = new TestSubscriber<>(); + + TestSubscriber<Flowable<Integer>> ts = pp.window(2, 1, TimeUnit.SECONDS, scheduler) + .doOnNext(new Consumer<Flowable<Integer>>() { + @Override + public void accept(Flowable<Integer> w) throws Throwable { + w.subscribe(tsInner); + } + }) // avoid abandonment + .test(1L); + + scheduler.advanceTimeBy(2, TimeUnit.SECONDS); + + ts.assertError(MissingBackpressureException.class); + + tsInner.assertError(MissingBackpressureException.class); + + assertTrue(errors.isEmpty()); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void dispose() { + TestHelper.checkDisposed(Flowable.range(1, 5).window(1, TimeUnit.DAYS, Schedulers.single()).onBackpressureDrop()); + + TestHelper.checkDisposed(Flowable.range(1, 5).window(2, 1, TimeUnit.DAYS, Schedulers.single()).onBackpressureDrop()); + + TestHelper.checkDisposed(Flowable.range(1, 5).window(1, 2, TimeUnit.DAYS, Schedulers.single()).onBackpressureDrop()); + + TestHelper.checkDisposed(Flowable.never() + .window(1, TimeUnit.DAYS, Schedulers.single(), 2, true).onBackpressureDrop()); + } + + @Test + public void restartTimer() { + Flowable.range(1, 5) + .window(1, TimeUnit.DAYS, Schedulers.single(), 2, true) + .flatMap(Functions.<Flowable<Integer>>identity()) + .test() + .assertResult(1, 2, 3, 4, 5); + } + + @Test + @SuppressUndeliverable + public void exactBoundaryError() { + Flowable.error(new TestException()) + .window(1, TimeUnit.DAYS, Schedulers.single(), 2, true) + .to(TestHelper.<Flowable<Object>>testConsumer()) + .assertSubscribed() + .assertError(TestException.class) + .assertNotComplete(); + } + + @Test + public void restartTimerMany() throws Exception { + final AtomicBoolean cancel1 = new AtomicBoolean(); + Flowable.intervalRange(1, 1000, 1, 1, TimeUnit.MILLISECONDS) + .doOnCancel(new Action() { + @Override + public void run() throws Exception { + cancel1.set(true); + } + }) + .window(1, TimeUnit.MILLISECONDS, Schedulers.single(), 2, true) + .flatMap(Functions.<Flowable<Long>>identity()) + .take(500) + .to(TestHelper.<Long>testConsumer()) + .awaitDone(5, TimeUnit.SECONDS) + .assertSubscribed() + .assertValueCount(500) + .assertNoErrors() + .assertComplete(); + + int timeout = 20; + while (timeout-- > 0 && !cancel1.get()) { + Thread.sleep(100); + } + + assertTrue("intervalRange was not cancelled!", cancel1.get()); + } + + @Test + public void exactUnboundedReentrant() { + TestScheduler scheduler = new TestScheduler(); + + final FlowableProcessor<Integer> ps = PublishProcessor.<Integer>create(); + + TestSubscriber<Integer> ts = new TestSubscriber<Integer>() { + @Override + public void onNext(Integer t) { + super.onNext(t); + if (t == 1) { + ps.onNext(2); + ps.onComplete(); + } + } + }; + + ps.window(1, TimeUnit.MILLISECONDS, scheduler) + .flatMap(new Function<Flowable<Integer>, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Flowable<Integer> v) throws Exception { + return v; + } + }) + .subscribe(ts); + + ps.onNext(1); + + ts + .awaitDone(1, TimeUnit.SECONDS) + .assertResult(1, 2); + } + + @Test + public void exactBoundedReentrant() { + TestScheduler scheduler = new TestScheduler(); + + final FlowableProcessor<Integer> ps = PublishProcessor.<Integer>create(); + + TestSubscriber<Integer> ts = new TestSubscriber<Integer>() { + @Override + public void onNext(Integer t) { + super.onNext(t); + if (t == 1) { + ps.onNext(2); + ps.onComplete(); + } + } + }; + + ps.window(1, TimeUnit.MILLISECONDS, scheduler, 10, true) + .flatMap(new Function<Flowable<Integer>, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Flowable<Integer> v) throws Exception { + return v; + } + }) + .subscribe(ts); + + ps.onNext(1); + + ts + .awaitDone(1, TimeUnit.SECONDS) + .assertResult(1, 2); + } + + @Test + public void exactBoundedReentrant2() { + TestScheduler scheduler = new TestScheduler(); + + final FlowableProcessor<Integer> ps = PublishProcessor.<Integer>create(); + + TestSubscriber<Integer> ts = new TestSubscriber<Integer>() { + @Override + public void onNext(Integer t) { + super.onNext(t); + if (t == 1) { + ps.onNext(2); + ps.onComplete(); + } + } + }; + + ps.window(1, TimeUnit.MILLISECONDS, scheduler, 2, true) + .flatMap(new Function<Flowable<Integer>, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Flowable<Integer> v) throws Exception { + return v; + } + }) + .subscribe(ts); + + ps.onNext(1); + + ts + .awaitDone(1, TimeUnit.SECONDS) + .assertResult(1, 2); + } + + @Test + public void skipReentrant() { + TestScheduler scheduler = new TestScheduler(); + + final FlowableProcessor<Integer> ps = PublishProcessor.<Integer>create(); + + TestSubscriber<Integer> ts = new TestSubscriber<Integer>() { + @Override + public void onNext(Integer t) { + super.onNext(t); + if (t == 1) { + ps.onNext(2); + ps.onComplete(); + } + } + }; + + ps.window(1, 2, TimeUnit.MILLISECONDS, scheduler) + .flatMap(new Function<Flowable<Integer>, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Flowable<Integer> v) throws Exception { + return v; + } + }) + .subscribe(ts); + + ps.onNext(1); + + ts + .awaitDone(1, TimeUnit.SECONDS) + .assertResult(1, 2); + } + + @Test + public void sizeTimeTimeout() { + TestScheduler scheduler = new TestScheduler(); + PublishProcessor<Integer> pp = PublishProcessor.<Integer>create(); + + TestSubscriber<Flowable<Integer>> ts = pp.window(5, TimeUnit.MILLISECONDS, scheduler, 100) + .test() + .assertValueCount(1); + + scheduler.advanceTimeBy(5, TimeUnit.MILLISECONDS); + + ts.assertValueCount(2) + .assertNoErrors() + .assertNotComplete(); + + ts.values().get(0).test().assertResult(); + } + + @Test + public void periodicWindowCompletion() { + TestScheduler scheduler = new TestScheduler(); + FlowableProcessor<Integer> ps = PublishProcessor.<Integer>create(); + + TestSubscriber<Flowable<Integer>> ts = ps.window(5, TimeUnit.MILLISECONDS, scheduler, Long.MAX_VALUE, false) + .test(); + + scheduler.advanceTimeBy(100, TimeUnit.MILLISECONDS); + + ts.assertValueCount(21) + .assertNoErrors() + .assertNotComplete(); + } + + @Test + public void periodicWindowCompletionRestartTimer() { + TestScheduler scheduler = new TestScheduler(); + FlowableProcessor<Integer> ps = PublishProcessor.<Integer>create(); + + TestSubscriber<Flowable<Integer>> ts = ps.window(5, TimeUnit.MILLISECONDS, scheduler, Long.MAX_VALUE, true) + .test(); + + scheduler.advanceTimeBy(100, TimeUnit.MILLISECONDS); + + ts.assertValueCount(21) + .assertNoErrors() + .assertNotComplete(); + } + + @Test + public void periodicWindowCompletionBounded() { + TestScheduler scheduler = new TestScheduler(); + FlowableProcessor<Integer> ps = PublishProcessor.<Integer>create(); + + TestSubscriber<Flowable<Integer>> ts = ps.window(5, TimeUnit.MILLISECONDS, scheduler, 5, false) + .test(); + + scheduler.advanceTimeBy(100, TimeUnit.MILLISECONDS); + + ts.assertValueCount(21) + .assertNoErrors() + .assertNotComplete(); + } + + @Test + public void periodicWindowCompletionRestartTimerBounded() { + TestScheduler scheduler = new TestScheduler(); + FlowableProcessor<Integer> ps = PublishProcessor.<Integer>create(); + + TestSubscriber<Flowable<Integer>> ts = ps.window(5, TimeUnit.MILLISECONDS, scheduler, 5, true) + .test(); + + scheduler.advanceTimeBy(100, TimeUnit.MILLISECONDS); + + ts.assertValueCount(21) + .assertNoErrors() + .assertNotComplete(); + } + + @Test + public void periodicWindowCompletionRestartTimerBoundedSomeData() { + TestScheduler scheduler = new TestScheduler(); + FlowableProcessor<Integer> ps = PublishProcessor.<Integer>create(); + + TestSubscriber<Flowable<Integer>> ts = ps.window(5, TimeUnit.MILLISECONDS, scheduler, 2, true) + .test(); + + ps.onNext(1); + ps.onNext(2); + + scheduler.advanceTimeBy(100, TimeUnit.MILLISECONDS); + + ts.assertValueCount(22) + .assertNoErrors() + .assertNotComplete(); + } + + @Test + public void countRestartsOnTimeTick() { + TestScheduler scheduler = new TestScheduler(); + FlowableProcessor<Integer> ps = PublishProcessor.<Integer>create(); + + TestSubscriber<Flowable<Integer>> ts = ps.window(5, TimeUnit.MILLISECONDS, scheduler, 5, true) + .doOnNext(new Consumer<Flowable<Integer>>() { + @Override + public void accept(Flowable<Integer> w) throws Throwable { + w.subscribe(); + } + }) // avoid abandonment + .test(); + + // window #1 + ps.onNext(1); + ps.onNext(2); + + scheduler.advanceTimeBy(5, TimeUnit.MILLISECONDS); + + // window #2 + ps.onNext(3); + ps.onNext(4); + ps.onNext(5); + ps.onNext(6); + + ts.assertValueCount(2) + .assertNoErrors() + .assertNotComplete(); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeFlowable(new Function<Flowable<Object>, Publisher<Flowable<Object>>>() { + @Override + public Publisher<Flowable<Object>> apply(Flowable<Object> f) + throws Exception { + return f.window(1, TimeUnit.SECONDS, 1).takeLast(0); + } + }); + } + + @Test + public void firstWindowMissingBackpressure() { + Flowable.never() + .window(1, TimeUnit.SECONDS, 1) + .test(0L) + .assertFailure(MissingBackpressureException.class); + } + + @Test + public void nextWindowMissingBackpressure() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + + TestSubscriber<Flowable<Integer>> ts = pp.window(1, TimeUnit.SECONDS, 1) + .test(1L); + + pp.onNext(1); + + ts.assertValueCount(1) + .assertError(MissingBackpressureException.class) + .assertNotComplete(); + } + + @Test + public void cancelUpfront() { + Flowable.never() + .window(1, TimeUnit.SECONDS, 1) + .test(0L, true) + .assertEmpty(); + } + + @Test + public void nextWindowMissingBackpressureDrainOnSize() { + final PublishProcessor<Integer> pp = PublishProcessor.create(); + + TestSubscriber<Flowable<Integer>> ts = pp.window(1, TimeUnit.MINUTES, 1) + .subscribeWith(new TestSubscriber<Flowable<Integer>>(2) { + int calls; + @Override + public void onNext(Flowable<Integer> t) { + super.onNext(t); + if (++calls == 2) { + pp.onNext(2); + } + } + }); + + pp.onNext(1); + + ts.assertValueCount(2) + .assertError(MissingBackpressureException.class) + .assertNotComplete(); + } + + @Test + public void nextWindowMissingBackpressureDrainOnTime() { + final PublishProcessor<Integer> pp = PublishProcessor.create(); + + final TestScheduler sch = new TestScheduler(); + + TestSubscriber<Flowable<Integer>> ts = pp.window(1, TimeUnit.MILLISECONDS, sch, 10) + .test(1); + + sch.advanceTimeBy(1, TimeUnit.MILLISECONDS); + + ts.assertValueCount(1) + .assertError(MissingBackpressureException.class) + .assertNotComplete(); + } + + @Test + public void exactTimeBoundNoInterruptWindowOutputOnComplete() throws Exception { + final AtomicBoolean isInterrupted = new AtomicBoolean(); + + final PublishProcessor<Integer> pp = PublishProcessor.create(); + + final CountDownLatch doOnNextDone = new CountDownLatch(1); + final CountDownLatch secondWindowProcessing = new CountDownLatch(1); + + pp.window(100, TimeUnit.MILLISECONDS) + .doOnNext(new Consumer<Flowable<Integer>>() { + int count; + @Override + public void accept(Flowable<Integer> v) throws Exception { + System.out.println(Thread.currentThread()); + if (count++ == 1) { + secondWindowProcessing.countDown(); + try { + Thread.sleep(200); + isInterrupted.set(Thread.interrupted()); + } catch (InterruptedException ex) { + isInterrupted.set(true); + } + doOnNextDone.countDown(); + } + } + }) + .test(); + + pp.onNext(1); + + assertTrue(secondWindowProcessing.await(5, TimeUnit.SECONDS)); + + pp.onComplete(); + + assertTrue(doOnNextDone.await(5, TimeUnit.SECONDS)); + + assertFalse("The doOnNext got interrupted!", isInterrupted.get()); + } + + @Test + @SuppressUndeliverable + public void exactTimeBoundNoInterruptWindowOutputOnError() throws Exception { + final AtomicBoolean isInterrupted = new AtomicBoolean(); + + final PublishProcessor<Integer> pp = PublishProcessor.create(); + + final CountDownLatch doOnNextDone = new CountDownLatch(1); + final CountDownLatch secondWindowProcessing = new CountDownLatch(1); + + pp.window(100, TimeUnit.MILLISECONDS) + .doOnNext(new Consumer<Flowable<Integer>>() { + int count; + @Override + public void accept(Flowable<Integer> v) throws Exception { + System.out.println(Thread.currentThread()); + if (count++ == 1) { + secondWindowProcessing.countDown(); + try { + Thread.sleep(200); + isInterrupted.set(Thread.interrupted()); + } catch (InterruptedException ex) { + isInterrupted.set(true); + } + doOnNextDone.countDown(); + } + } + }) + .test(); + + pp.onNext(1); + + assertTrue(secondWindowProcessing.await(5, TimeUnit.SECONDS)); + + pp.onError(new TestException()); + + assertTrue(doOnNextDone.await(5, TimeUnit.SECONDS)); + + assertFalse("The doOnNext got interrupted!", isInterrupted.get()); + } + + @Test + public void exactTimeAndSizeBoundNoInterruptWindowOutputOnComplete() throws Exception { + final AtomicBoolean isInterrupted = new AtomicBoolean(); + + final PublishProcessor<Integer> pp = PublishProcessor.create(); + + final CountDownLatch doOnNextDone = new CountDownLatch(1); + final CountDownLatch secondWindowProcessing = new CountDownLatch(1); + + pp.window(100, TimeUnit.MILLISECONDS, 10) + .doOnNext(new Consumer<Flowable<Integer>>() { + int count; + @Override + public void accept(Flowable<Integer> v) throws Exception { + System.out.println(Thread.currentThread()); + if (count++ == 1) { + secondWindowProcessing.countDown(); + try { + Thread.sleep(200); + isInterrupted.set(Thread.interrupted()); + } catch (InterruptedException ex) { + isInterrupted.set(true); + } + doOnNextDone.countDown(); + } + } + }) + .test(); + + pp.onNext(1); + + assertTrue(secondWindowProcessing.await(5, TimeUnit.SECONDS)); + + pp.onComplete(); + + assertTrue(doOnNextDone.await(5, TimeUnit.SECONDS)); + + assertFalse("The doOnNext got interrupted!", isInterrupted.get()); + } + + @Test + @SuppressUndeliverable + public void exactTimeAndSizeBoundNoInterruptWindowOutputOnError() throws Exception { + final AtomicBoolean isInterrupted = new AtomicBoolean(); + + final PublishProcessor<Integer> pp = PublishProcessor.create(); + + final CountDownLatch doOnNextDone = new CountDownLatch(1); + final CountDownLatch secondWindowProcessing = new CountDownLatch(1); + + pp.window(100, TimeUnit.MILLISECONDS, 10) + .doOnNext(new Consumer<Flowable<Integer>>() { + int count; + @Override + public void accept(Flowable<Integer> v) throws Exception { + System.out.println(Thread.currentThread()); + if (count++ == 1) { + secondWindowProcessing.countDown(); + try { + Thread.sleep(200); + isInterrupted.set(Thread.interrupted()); + } catch (InterruptedException ex) { + isInterrupted.set(true); + } + doOnNextDone.countDown(); + } + } + }) + .test(); + + pp.onNext(1); + + assertTrue(secondWindowProcessing.await(5, TimeUnit.SECONDS)); + + pp.onError(new TestException()); + + assertTrue(doOnNextDone.await(5, TimeUnit.SECONDS)); + + assertFalse("The doOnNext got interrupted!", isInterrupted.get()); + } + + @Test + public void skipTimeAndSizeBoundNoInterruptWindowOutputOnComplete() throws Exception { + final AtomicBoolean isInterrupted = new AtomicBoolean(); + + final PublishProcessor<Integer> pp = PublishProcessor.create(); + + final CountDownLatch doOnNextDone = new CountDownLatch(1); + final CountDownLatch secondWindowProcessing = new CountDownLatch(1); + + pp.window(90, 100, TimeUnit.MILLISECONDS) + .doOnNext(new Consumer<Flowable<Integer>>() { + int count; + @Override + public void accept(Flowable<Integer> v) throws Exception { + System.out.println(Thread.currentThread()); + if (count++ == 1) { + secondWindowProcessing.countDown(); + try { + Thread.sleep(200); + isInterrupted.set(Thread.interrupted()); + } catch (InterruptedException ex) { + isInterrupted.set(true); + } + doOnNextDone.countDown(); + } + } + }) + .test(); + + pp.onNext(1); + + assertTrue(secondWindowProcessing.await(5, TimeUnit.SECONDS)); + + pp.onComplete(); + + assertTrue(doOnNextDone.await(5, TimeUnit.SECONDS)); + + assertFalse("The doOnNext got interrupted!", isInterrupted.get()); + } + + @Test + @SuppressUndeliverable + public void skipTimeAndSizeBoundNoInterruptWindowOutputOnError() throws Exception { + final AtomicBoolean isInterrupted = new AtomicBoolean(); + + final PublishProcessor<Integer> pp = PublishProcessor.create(); + + final CountDownLatch doOnNextDone = new CountDownLatch(1); + final CountDownLatch secondWindowProcessing = new CountDownLatch(1); + + pp.window(90, 100, TimeUnit.MILLISECONDS) + .doOnNext(new Consumer<Flowable<Integer>>() { + int count; + @Override + public void accept(Flowable<Integer> v) throws Exception { + System.out.println(Thread.currentThread()); + if (count++ == 1) { + secondWindowProcessing.countDown(); + try { + Thread.sleep(200); + isInterrupted.set(Thread.interrupted()); + } catch (InterruptedException ex) { + isInterrupted.set(true); + } + doOnNextDone.countDown(); + } + } + }) + .test(); + + pp.onNext(1); + + assertTrue(secondWindowProcessing.await(5, TimeUnit.SECONDS)); + + pp.onError(new TestException()); + + assertTrue(doOnNextDone.await(5, TimeUnit.SECONDS)); + + assertFalse("The doOnNext got interrupted!", isInterrupted.get()); + } + + @Test + public void cancellingWindowCancelsUpstreamExactTime() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + + TestSubscriber<Integer> ts = pp.window(10, TimeUnit.MINUTES) + .take(1) + .flatMap(new Function<Flowable<Integer>, Publisher<Integer>>() { + @Override + public Publisher<Integer> apply(Flowable<Integer> w) throws Throwable { + return w.take(1); + } + }) + .test(); + + assertTrue(pp.hasSubscribers()); + + pp.onNext(1); + + ts + .assertResult(1); + + assertFalse("Processor still has subscribers!", pp.hasSubscribers()); + } + + @Test + public void windowAbandonmentCancelsUpstreamExactTime() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + + final AtomicReference<Flowable<Integer>> inner = new AtomicReference<>(); + + TestSubscriber<Flowable<Integer>> ts = pp.window(10, TimeUnit.MINUTES) + .take(1) + .doOnNext(new Consumer<Flowable<Integer>>() { + @Override + public void accept(Flowable<Integer> v) throws Throwable { + inner.set(v); + } + }) + .test(); + + assertFalse("Processor still has subscribers!", pp.hasSubscribers()); + + ts + .assertValueCount(1) + .assertNoErrors() + .assertComplete(); + + inner.get().test().assertResult(); + } + + @Test + public void cancellingWindowCancelsUpstreamExactTimeAndSize() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + + TestSubscriber<Integer> ts = pp.window(10, TimeUnit.MINUTES, 100) + .take(1) + .flatMap(new Function<Flowable<Integer>, Publisher<Integer>>() { + @Override + public Publisher<Integer> apply(Flowable<Integer> w) throws Throwable { + return w.take(1); + } + }) + .test(); + + assertTrue(pp.hasSubscribers()); + + pp.onNext(1); + + ts + .assertResult(1); + + assertFalse("Processor still has subscribers!", pp.hasSubscribers()); + } + + @Test + public void windowAbandonmentCancelsUpstreamExactTimeAndSize() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + + final AtomicReference<Flowable<Integer>> inner = new AtomicReference<>(); + + TestSubscriber<Flowable<Integer>> ts = pp.window(10, TimeUnit.MINUTES, 100) + .take(1) + .doOnNext(new Consumer<Flowable<Integer>>() { + @Override + public void accept(Flowable<Integer> v) throws Throwable { + inner.set(v); + } + }) + .test(); + + assertFalse("Processor still has subscribers!", pp.hasSubscribers()); + + ts + .assertValueCount(1) + .assertNoErrors() + .assertComplete(); + + inner.get().test().assertResult(); + } + + @Test + public void cancellingWindowCancelsUpstreamExactTimeSkip() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + + TestSubscriber<Integer> ts = pp.window(10, 15, TimeUnit.MINUTES) + .take(1) + .flatMap(new Function<Flowable<Integer>, Publisher<Integer>>() { + @Override + public Publisher<Integer> apply(Flowable<Integer> w) throws Throwable { + return w.take(1); + } + }) + .test(); + + assertTrue(pp.hasSubscribers()); + + pp.onNext(1); + + ts + .assertResult(1); + + assertFalse("Processor still has subscribers!", pp.hasSubscribers()); + } + + @Test + public void windowAbandonmentCancelsUpstreamExactTimeSkip() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + + final AtomicReference<Flowable<Integer>> inner = new AtomicReference<>(); + + TestSubscriber<Flowable<Integer>> ts = pp.window(10, 15, TimeUnit.MINUTES) + .take(1) + .doOnNext(new Consumer<Flowable<Integer>>() { + @Override + public void accept(Flowable<Integer> v) throws Throwable { + inner.set(v); + } + }) + .test(); + + assertFalse("Processor still has subscribers!", pp.hasSubscribers()); + + ts + .assertValueCount(1) + .assertNoErrors() + .assertComplete(); + + inner.get().test().assertResult(); + } + + @Test + public void badRequest() { + TestHelper.assertBadRequestReported(Flowable.never().window(1, TimeUnit.SECONDS)); + } + + @Test + public void timedBoundarySignalAndDisposeRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + TestScheduler scheduler = new TestScheduler(); + + PublishProcessor<Integer> pp = PublishProcessor.create(); + + TestSubscriber<Flowable<Integer>> ts = pp.window(1, TimeUnit.MINUTES, scheduler, 1) + .test(); + + TestHelper.race( + () -> pp.onNext(1), + () -> ts.cancel() + ); + } + } +} + diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableWithLatestFromTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableWithLatestFromTest.java new file mode 100644 index 0000000000..df325cd2c8 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableWithLatestFromTest.java @@ -0,0 +1,865 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +import java.util.*; + +import org.junit.Test; +import org.mockito.InOrder; +import org.reactivestreams.Subscriber; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.internal.subscriptions.BooleanSubscription; +import io.reactivex.rxjava3.internal.util.CrashingMappedIterable; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.processors.PublishProcessor; +import io.reactivex.rxjava3.subscribers.TestSubscriber; +import io.reactivex.rxjava3.testsupport.*; + +public class FlowableWithLatestFromTest extends RxJavaTest { + static final BiFunction<Integer, Integer, Integer> COMBINER = new BiFunction<Integer, Integer, Integer>() { + @Override + public Integer apply(Integer t1, Integer t2) { + return (t1 << 8) + t2; + } + }; + static final BiFunction<Integer, Integer, Integer> COMBINER_ERROR = new BiFunction<Integer, Integer, Integer>() { + @Override + public Integer apply(Integer t1, Integer t2) { + throw new TestException("Forced failure"); + } + }; + @Test + public void simple() { + PublishProcessor<Integer> source = PublishProcessor.create(); + PublishProcessor<Integer> other = PublishProcessor.create(); + + Subscriber<Integer> subscriber = TestHelper.mockSubscriber(); + InOrder inOrder = inOrder(subscriber); + + Flowable<Integer> result = source.withLatestFrom(other, COMBINER); + + result.subscribe(subscriber); + + source.onNext(1); + inOrder.verify(subscriber, never()).onNext(anyInt()); + + other.onNext(1); + inOrder.verify(subscriber, never()).onNext(anyInt()); + + source.onNext(2); + inOrder.verify(subscriber).onNext((2 << 8) + 1); + + other.onNext(2); + inOrder.verify(subscriber, never()).onNext(anyInt()); + + other.onComplete(); + inOrder.verify(subscriber, never()).onComplete(); + + source.onNext(3); + inOrder.verify(subscriber).onNext((3 << 8) + 2); + + source.onComplete(); + inOrder.verify(subscriber).onComplete(); + + verify(subscriber, never()).onError(any(Throwable.class)); + } + + @Test + public void emptySource() { + PublishProcessor<Integer> source = PublishProcessor.create(); + PublishProcessor<Integer> other = PublishProcessor.create(); + + Flowable<Integer> result = source.withLatestFrom(other, COMBINER); + + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(); + + result.subscribe(ts); + + assertTrue(source.hasSubscribers()); + assertTrue(other.hasSubscribers()); + + other.onNext(1); + + source.onComplete(); + + ts.assertNoErrors(); + ts.assertTerminated(); + ts.assertNoValues(); + + assertFalse(source.hasSubscribers()); + assertFalse(other.hasSubscribers()); + } + + @Test + public void emptyOther() { + PublishProcessor<Integer> source = PublishProcessor.create(); + PublishProcessor<Integer> other = PublishProcessor.create(); + + Flowable<Integer> result = source.withLatestFrom(other, COMBINER); + + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(); + + result.subscribe(ts); + + assertTrue(source.hasSubscribers()); + assertTrue(other.hasSubscribers()); + + source.onNext(1); + + source.onComplete(); + + ts.assertNoErrors(); + ts.assertTerminated(); + ts.assertNoValues(); + + assertFalse(source.hasSubscribers()); + assertFalse(other.hasSubscribers()); + } + + @Test + public void unsubscription() { + PublishProcessor<Integer> source = PublishProcessor.create(); + PublishProcessor<Integer> other = PublishProcessor.create(); + + Flowable<Integer> result = source.withLatestFrom(other, COMBINER); + + TestSubscriber<Integer> ts = new TestSubscriber<>(); + + result.subscribe(ts); + + assertTrue(source.hasSubscribers()); + assertTrue(other.hasSubscribers()); + + other.onNext(1); + source.onNext(1); + + ts.cancel(); + + ts.assertValue((1 << 8) + 1); + ts.assertNoErrors(); + ts.assertNotComplete(); + + assertFalse(source.hasSubscribers()); + assertFalse(other.hasSubscribers()); + } + + @Test + public void sourceThrows() { + PublishProcessor<Integer> source = PublishProcessor.create(); + PublishProcessor<Integer> other = PublishProcessor.create(); + + Flowable<Integer> result = source.withLatestFrom(other, COMBINER); + + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(); + + result.subscribe(ts); + + assertTrue(source.hasSubscribers()); + assertTrue(other.hasSubscribers()); + + other.onNext(1); + source.onNext(1); + + source.onError(new TestException()); + + ts.assertTerminated(); + ts.assertValue((1 << 8) + 1); + ts.assertError(TestException.class); + ts.assertNotComplete(); + + assertFalse(source.hasSubscribers()); + assertFalse(other.hasSubscribers()); + } + + @Test + public void otherThrows() { + PublishProcessor<Integer> source = PublishProcessor.create(); + PublishProcessor<Integer> other = PublishProcessor.create(); + + Flowable<Integer> result = source.withLatestFrom(other, COMBINER); + + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(); + + result.subscribe(ts); + + assertTrue(source.hasSubscribers()); + assertTrue(other.hasSubscribers()); + + other.onNext(1); + source.onNext(1); + + other.onError(new TestException()); + + ts.assertTerminated(); + ts.assertValue((1 << 8) + 1); + ts.assertNotComplete(); + ts.assertError(TestException.class); + + assertFalse(source.hasSubscribers()); + assertFalse(other.hasSubscribers()); + } + + @Test + public void functionThrows() { + PublishProcessor<Integer> source = PublishProcessor.create(); + PublishProcessor<Integer> other = PublishProcessor.create(); + + Flowable<Integer> result = source.withLatestFrom(other, COMBINER_ERROR); + + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(); + + result.subscribe(ts); + + assertTrue(source.hasSubscribers()); + assertTrue(other.hasSubscribers()); + + other.onNext(1); + source.onNext(1); + + ts.assertTerminated(); + ts.assertNotComplete(); + ts.assertNoValues(); + ts.assertError(TestException.class); + + assertFalse(source.hasSubscribers()); + assertFalse(other.hasSubscribers()); + } + + @Test + public void noDownstreamUnsubscribe() { + PublishProcessor<Integer> source = PublishProcessor.create(); + PublishProcessor<Integer> other = PublishProcessor.create(); + + Flowable<Integer> result = source.withLatestFrom(other, COMBINER); + + TestSubscriber<Integer> ts = new TestSubscriber<>(); + + result.subscribe(ts); + + source.onComplete(); + + assertFalse(ts.isCancelled()); + } + + @Test + public void backpressure() { + PublishProcessor<Integer> source = PublishProcessor.create(); + PublishProcessor<Integer> other = PublishProcessor.create(); + + Flowable<Integer> result = source.withLatestFrom(other, COMBINER); + + TestSubscriber<Integer> ts = new TestSubscriber<>(0L); + + result.subscribe(ts); + + assertTrue("Other has no observers!", other.hasSubscribers()); + + ts.request(1); + + source.onNext(1); + + assertTrue("Other has no observers!", other.hasSubscribers()); + + ts.assertNoValues(); + + other.onNext(1); + + source.onNext(2); + + ts.assertValue((2 << 8) + 1); + + ts.request(5); + source.onNext(3); + source.onNext(4); + source.onNext(5); + source.onNext(6); + source.onNext(7); + ts.assertValues( + (2 << 8) + 1, (3 << 8) + 1, (4 << 8) + 1, (5 << 8) + 1, + (6 << 8) + 1, (7 << 8) + 1 + ); + + ts.cancel(); + + assertFalse("Other has observers!", other.hasSubscribers()); + + ts.assertNoErrors(); + } + + static final Function<Object[], String> toArray = new Function<Object[], String>() { + @Override + public String apply(Object[] args) { + return Arrays.toString(args); + } + }; + + @Test + public void manySources() { + PublishProcessor<String> pp1 = PublishProcessor.create(); + PublishProcessor<String> pp2 = PublishProcessor.create(); + PublishProcessor<String> pp3 = PublishProcessor.create(); + PublishProcessor<String> main = PublishProcessor.create(); + + TestSubscriber<String> ts = new TestSubscriber<>(); + + main.withLatestFrom(new Flowable[] { pp1, pp2, pp3 }, toArray) + .subscribe(ts); + + main.onNext("1"); + ts.assertNoValues(); + pp1.onNext("a"); + ts.assertNoValues(); + pp2.onNext("A"); + ts.assertNoValues(); + pp3.onNext("="); + ts.assertNoValues(); + + main.onNext("2"); + ts.assertValues("[2, a, A, =]"); + + pp2.onNext("B"); + + ts.assertValues("[2, a, A, =]"); + + pp3.onComplete(); + ts.assertValues("[2, a, A, =]"); + + pp1.onNext("b"); + + main.onNext("3"); + + ts.assertValues("[2, a, A, =]", "[3, b, B, =]"); + + main.onComplete(); + ts.assertValues("[2, a, A, =]", "[3, b, B, =]"); + ts.assertNoErrors(); + ts.assertComplete(); + + assertFalse("ps1 has subscribers?", pp1.hasSubscribers()); + assertFalse("ps2 has subscribers?", pp2.hasSubscribers()); + assertFalse("ps3 has subscribers?", pp3.hasSubscribers()); + } + + @Test + public void manySourcesIterable() { + PublishProcessor<String> pp1 = PublishProcessor.create(); + PublishProcessor<String> pp2 = PublishProcessor.create(); + PublishProcessor<String> pp3 = PublishProcessor.create(); + PublishProcessor<String> main = PublishProcessor.create(); + + TestSubscriber<String> ts = new TestSubscriber<>(); + + main.withLatestFrom(Arrays.<Flowable<?>>asList(pp1, pp2, pp3), toArray) + .subscribe(ts); + + main.onNext("1"); + ts.assertNoValues(); + pp1.onNext("a"); + ts.assertNoValues(); + pp2.onNext("A"); + ts.assertNoValues(); + pp3.onNext("="); + ts.assertNoValues(); + + main.onNext("2"); + ts.assertValues("[2, a, A, =]"); + + pp2.onNext("B"); + + ts.assertValues("[2, a, A, =]"); + + pp3.onComplete(); + ts.assertValues("[2, a, A, =]"); + + pp1.onNext("b"); + + main.onNext("3"); + + ts.assertValues("[2, a, A, =]", "[3, b, B, =]"); + + main.onComplete(); + ts.assertValues("[2, a, A, =]", "[3, b, B, =]"); + ts.assertNoErrors(); + ts.assertComplete(); + + assertFalse("ps1 has subscribers?", pp1.hasSubscribers()); + assertFalse("ps2 has subscribers?", pp2.hasSubscribers()); + assertFalse("ps3 has subscribers?", pp3.hasSubscribers()); + } + + @Test + public void manySourcesIterableSweep() { + for (String val : new String[] { "1" /*, null*/ }) { + int n = 35; + for (int i = 0; i < n; i++) { + List<Flowable<?>> sources = new ArrayList<>(); + List<String> expected = new ArrayList<>(); + expected.add(val); + + for (int j = 0; j < i; j++) { + sources.add(Flowable.just(val)); + expected.add(String.valueOf(val)); + } + + TestSubscriber<String> ts = new TestSubscriber<>(); + + PublishProcessor<String> main = PublishProcessor.create(); + + main.withLatestFrom(sources, toArray).subscribe(ts); + + ts.assertNoValues(); + + main.onNext(val); + main.onComplete(); + + ts.assertValue(expected.toString()); + ts.assertNoErrors(); + ts.assertComplete(); + } + } + } + + @Test + public void backpressureNoSignal() { + PublishProcessor<String> pp1 = PublishProcessor.create(); + PublishProcessor<String> pp2 = PublishProcessor.create(); + + TestSubscriber<String> ts = new TestSubscriber<>(0); + + Flowable.range(1, 10).withLatestFrom(new Flowable<?>[] { pp1, pp2 }, toArray) + .subscribe(ts); + + ts.assertNoValues(); + + ts.request(1); + + ts.assertNoValues(); + ts.assertNoErrors(); + ts.assertComplete(); + + assertFalse("ps1 has subscribers?", pp1.hasSubscribers()); + assertFalse("ps2 has subscribers?", pp2.hasSubscribers()); + } + + @Test + public void backpressureWithSignal() { + PublishProcessor<String> pp1 = PublishProcessor.create(); + PublishProcessor<String> pp2 = PublishProcessor.create(); + + TestSubscriber<String> ts = new TestSubscriber<>(0); + + Flowable.range(1, 3).withLatestFrom(new Flowable<?>[] { pp1, pp2 }, toArray) + .subscribe(ts); + + ts.assertNoValues(); + + pp1.onNext("1"); + pp2.onNext("1"); + + ts.request(1); + + ts.assertValue("[1, 1, 1]"); + + ts.request(1); + + ts.assertValues("[1, 1, 1]", "[2, 1, 1]"); + + ts.request(1); + + ts.assertValues("[1, 1, 1]", "[2, 1, 1]", "[3, 1, 1]"); + ts.assertNoErrors(); + ts.assertComplete(); + + assertFalse("ps1 has subscribers?", pp1.hasSubscribers()); + assertFalse("ps2 has subscribers?", pp2.hasSubscribers()); + } + + @Test + public void withEmpty() { + TestSubscriber<String> ts = new TestSubscriber<>(0); + + Flowable.range(1, 3).withLatestFrom( + new Flowable<?>[] { Flowable.just(1), Flowable.empty() }, toArray) + .subscribe(ts); + + ts.assertNoValues(); + ts.assertNoErrors(); + ts.assertComplete(); + } + + @Test + public void withError() { + TestSubscriber<String> ts = new TestSubscriber<>(0); + + Flowable.range(1, 3).withLatestFrom( + new Flowable<?>[] { Flowable.just(1), Flowable.error(new TestException()) }, toArray) + .subscribe(ts); + + ts.assertNoValues(); + ts.assertError(TestException.class); + ts.assertNotComplete(); + } + + @Test + public void withMainError() { + TestSubscriber<String> ts = new TestSubscriber<>(0); + + Flowable.error(new TestException()).withLatestFrom( + new Flowable<?>[] { Flowable.just(1), Flowable.just(1) }, toArray) + .subscribe(ts); + + ts.assertNoValues(); + ts.assertError(TestException.class); + ts.assertNotComplete(); + } + + @Test + public void with2Others() { + Flowable<Integer> just = Flowable.just(1); + + TestSubscriber<List<Integer>> ts = new TestSubscriber<>(); + + just.withLatestFrom(just, just, new Function3<Integer, Integer, Integer, List<Integer>>() { + @Override + public List<Integer> apply(Integer a, Integer b, Integer c) { + return Arrays.asList(a, b, c); + } + }) + .subscribe(ts); + + ts.assertValue(Arrays.asList(1, 1, 1)); + ts.assertNoErrors(); + ts.assertComplete(); + } + + @Test + public void with3Others() { + Flowable<Integer> just = Flowable.just(1); + + TestSubscriber<List<Integer>> ts = new TestSubscriber<>(); + + just.withLatestFrom(just, just, just, new Function4<Integer, Integer, Integer, Integer, List<Integer>>() { + @Override + public List<Integer> apply(Integer a, Integer b, Integer c, Integer d) { + return Arrays.asList(a, b, c, d); + } + }) + .subscribe(ts); + + ts.assertValue(Arrays.asList(1, 1, 1, 1)); + ts.assertNoErrors(); + ts.assertComplete(); + } + + @Test + public void with4Others() { + Flowable<Integer> just = Flowable.just(1); + + TestSubscriber<List<Integer>> ts = new TestSubscriber<>(); + + just.withLatestFrom(just, just, just, just, new Function5<Integer, Integer, Integer, Integer, Integer, List<Integer>>() { + @Override + public List<Integer> apply(Integer a, Integer b, Integer c, Integer d, Integer e) { + return Arrays.asList(a, b, c, d, e); + } + }) + .subscribe(ts); + + ts.assertValue(Arrays.asList(1, 1, 1, 1, 1)); + ts.assertNoErrors(); + ts.assertComplete(); + } + + @Test + public void dispose() { + TestHelper.checkDisposed(Flowable.just(1).withLatestFrom(Flowable.just(2), new BiFunction<Integer, Integer, Object>() { + @Override + public Object apply(Integer a, Integer b) throws Exception { + return a; + } + })); + + TestHelper.checkDisposed(Flowable.just(1).withLatestFrom(Flowable.just(2), Flowable.just(3), new Function3<Integer, Integer, Integer, Object>() { + @Override + public Object apply(Integer a, Integer b, Integer c) throws Exception { + return a; + } + })); + } + + @Test + public void manyIteratorThrows() { + Flowable.just(1) + .withLatestFrom(new CrashingMappedIterable<>(1, 100, 100, new Function<Integer, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Integer v) throws Exception { + return Flowable.just(2); + } + }), new Function<Object[], Object>() { + @Override + public Object apply(Object[] a) throws Exception { + return a; + } + }) + .to(TestHelper.testConsumer()) + .assertFailureAndMessage(TestException.class, "iterator()"); + } + + @Test + public void manyCombinerThrows() { + Flowable.just(1).withLatestFrom(Flowable.just(2), Flowable.just(3), new Function3<Integer, Integer, Integer, Object>() { + @Override + public Object apply(Integer a, Integer b, Integer c) throws Exception { + throw new TestException(); + } + }) + .test() + .assertFailure(TestException.class); + } + + @Test + public void manyErrors() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + new Flowable<Integer>() { + @Override + protected void subscribeActual(Subscriber<? super Integer> subscriber) { + subscriber.onSubscribe(new BooleanSubscription()); + subscriber.onError(new TestException("First")); + subscriber.onNext(1); + subscriber.onError(new TestException("Second")); + subscriber.onComplete(); + } + }.withLatestFrom(Flowable.just(2), Flowable.just(3), new Function3<Integer, Integer, Integer, Object>() { + @Override + public Object apply(Integer a, Integer b, Integer c) throws Exception { + return a; + } + }) + .to(TestHelper.testConsumer()) + .assertFailureAndMessage(TestException.class, "First"); + + TestHelper.assertUndeliverable(errors, 0, TestException.class, "Second"); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void otherErrors() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + Flowable.just(1) + .withLatestFrom(new Flowable<Integer>() { + @Override + protected void subscribeActual(Subscriber<? super Integer> s) { + s.onSubscribe(new BooleanSubscription()); + s.onError(new TestException("First")); + s.onError(new TestException("Second")); + } + }, new BiFunction<Integer, Integer, Integer>() { + @Override + public Integer apply(Integer a, Integer b) throws Exception { + return a + b; + } + }) + .to(TestHelper.<Integer>testConsumer()) + .assertFailureAndMessage(TestException.class, "First"); + + TestHelper.assertUndeliverable(errors, 0, TestException.class, "Second"); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void combineToNull1() { + Flowable.just(1) + .withLatestFrom(Flowable.just(2), new BiFunction<Integer, Integer, Object>() { + @Override + public Object apply(Integer a, Integer b) throws Exception { + return null; + } + }) + .test() + .assertFailure(NullPointerException.class); + } + + @Test + public void combineToNull2() { + Flowable.just(1) + .withLatestFrom(Arrays.asList(Flowable.just(2), Flowable.just(3)), new Function<Object[], Object>() { + @Override + public Object apply(Object[] o) throws Exception { + return null; + } + }) + .test() + .assertFailure(NullPointerException.class); + } + + @Test + public void zeroOtherCombinerReturnsNull() { + Flowable.just(1) + .withLatestFrom(new Flowable[0], Functions.justFunction(null)) + .to(TestHelper.testConsumer()) + .assertFailureAndMessage(NullPointerException.class, "The combiner returned a null value"); + } + + @Test + public void singleRequestNotForgottenWhenNoData() { + PublishProcessor<Integer> source = PublishProcessor.create(); + PublishProcessor<Integer> other = PublishProcessor.create(); + + Flowable<Integer> result = source.withLatestFrom(other, COMBINER); + + TestSubscriber<Integer> ts = new TestSubscriber<>(0L); + + result.subscribe(ts); + + ts.request(1); + + source.onNext(1); + + ts.assertNoValues(); + + other.onNext(1); + + ts.assertNoValues(); + + source.onNext(2); + + ts.assertValue((2 << 8) + 1); + } + + @Test + public void coldSourceConsumedWithoutOther() { + Flowable.range(1, 10).withLatestFrom(Flowable.never(), + new BiFunction<Integer, Object, Object>() { + @Override + public Object apply(Integer a, Object b) throws Exception { + return a; + } + }) + .test(1) + .assertResult(); + } + + @Test + public void coldSourceConsumedWithoutManyOthers() { + Flowable.range(1, 10).withLatestFrom(Flowable.never(), Flowable.never(), Flowable.never(), + new Function4<Integer, Object, Object, Object, Object>() { + @Override + public Object apply(Integer a, Object b, Object c, Object d) throws Exception { + return a; + } + }) + .test(1) + .assertResult(); + } + + @Test + public void otherOnSubscribeRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final PublishProcessor<Integer> pp0 = PublishProcessor.create(); + final PublishProcessor<Integer> pp1 = PublishProcessor.create(); + final PublishProcessor<Integer> pp2 = PublishProcessor.create(); + final PublishProcessor<Integer> pp3 = PublishProcessor.create(); + + final Flowable<Object> source = pp0.withLatestFrom(pp1, pp2, pp3, new Function4<Object, Integer, Integer, Integer, Object>() { + @Override + public Object apply(Object a, Integer b, Integer c, Integer d) + throws Exception { + return a; + } + }); + + final TestSubscriber<Object> ts = new TestSubscriber<>(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + source.subscribe(ts); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + ts.cancel(); + } + }; + + TestHelper.race(r1, r2); + + ts.assertEmpty(); + + assertFalse(pp0.hasSubscribers()); + assertFalse(pp1.hasSubscribers()); + assertFalse(pp2.hasSubscribers()); + assertFalse(pp3.hasSubscribers()); + } + } + + @Test + public void otherCompleteRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final PublishProcessor<Integer> pp0 = PublishProcessor.create(); + final PublishProcessor<Integer> pp1 = PublishProcessor.create(); + final PublishProcessor<Integer> pp2 = PublishProcessor.create(); + final PublishProcessor<Integer> pp3 = PublishProcessor.create(); + + final Flowable<Object> source = pp0.withLatestFrom(pp1, pp2, pp3, new Function4<Object, Integer, Integer, Integer, Object>() { + @Override + public Object apply(Object a, Integer b, Integer c, Integer d) + throws Exception { + return a; + } + }); + + final TestSubscriber<Object> ts = new TestSubscriber<>(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + source.subscribe(ts); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + pp1.onComplete(); + } + }; + + TestHelper.race(r1, r2); + + ts.assertResult(); + + assertFalse(pp0.hasSubscribers()); + assertFalse(pp1.hasSubscribers()); + assertFalse(pp2.hasSubscribers()); + assertFalse(pp3.hasSubscribers()); + } + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableZipCompletionTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableZipCompletionTest.java new file mode 100644 index 0000000000..fe69ea6be5 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableZipCompletionTest.java @@ -0,0 +1,113 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import static org.mockito.Mockito.*; + +import org.junit.*; +import org.mockito.InOrder; +import org.reactivestreams.Subscriber; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.functions.BiFunction; +import io.reactivex.rxjava3.processors.PublishProcessor; +import io.reactivex.rxjava3.testsupport.TestHelper; + +/** + * Systematically tests that when zipping an infinite and a finite Observable, + * the resulting Observable is finite. + * + */ +public class FlowableZipCompletionTest extends RxJavaTest { + BiFunction<String, String, String> concat2Strings; + + PublishProcessor<String> s1; + PublishProcessor<String> s2; + Flowable<String> zipped; + + Subscriber<String> subscriber; + InOrder inOrder; + + @Before + public void setUp() { + concat2Strings = new BiFunction<String, String, String>() { + @Override + public String apply(String t1, String t2) { + return t1 + "-" + t2; + } + }; + + s1 = PublishProcessor.create(); + s2 = PublishProcessor.create(); + zipped = Flowable.zip(s1, s2, concat2Strings); + + subscriber = TestHelper.mockSubscriber(); + inOrder = inOrder(subscriber); + + zipped.subscribe(subscriber); + } + + @Test + public void firstCompletesThenSecondInfinite() { + s1.onNext("a"); + s1.onNext("b"); + s1.onComplete(); + s2.onNext("1"); + inOrder.verify(subscriber, times(1)).onNext("a-1"); + s2.onNext("2"); + inOrder.verify(subscriber, times(1)).onNext("b-2"); + inOrder.verify(subscriber, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void secondInfiniteThenFirstCompletes() { + s2.onNext("1"); + s2.onNext("2"); + s1.onNext("a"); + inOrder.verify(subscriber, times(1)).onNext("a-1"); + s1.onNext("b"); + inOrder.verify(subscriber, times(1)).onNext("b-2"); + s1.onComplete(); + inOrder.verify(subscriber, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void secondCompletesThenFirstInfinite() { + s2.onNext("1"); + s2.onNext("2"); + s2.onComplete(); + s1.onNext("a"); + inOrder.verify(subscriber, times(1)).onNext("a-1"); + s1.onNext("b"); + inOrder.verify(subscriber, times(1)).onNext("b-2"); + inOrder.verify(subscriber, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void firstInfiniteThenSecondCompletes() { + s1.onNext("a"); + s1.onNext("b"); + s2.onNext("1"); + inOrder.verify(subscriber, times(1)).onNext("a-1"); + s2.onNext("2"); + inOrder.verify(subscriber, times(1)).onNext("b-2"); + s2.onComplete(); + inOrder.verify(subscriber, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + } + +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableZipIterableTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableZipIterableTest.java new file mode 100644 index 0000000000..aee49596d9 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableZipIterableTest.java @@ -0,0 +1,433 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import java.util.*; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.*; +import org.mockito.InOrder; +import org.reactivestreams.Subscriber; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.subscriptions.BooleanSubscription; +import io.reactivex.rxjava3.internal.util.CrashingIterable; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.processors.PublishProcessor; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class FlowableZipIterableTest extends RxJavaTest { + BiFunction<String, String, String> concat2Strings; + PublishProcessor<String> s1; + PublishProcessor<String> s2; + Flowable<String> zipped; + + Subscriber<String> subscriber; + InOrder inOrder; + + @Before + public void setUp() { + concat2Strings = new BiFunction<String, String, String>() { + @Override + public String apply(String t1, String t2) { + return t1 + "-" + t2; + } + }; + + s1 = PublishProcessor.create(); + s2 = PublishProcessor.create(); + zipped = Flowable.zip(s1, s2, concat2Strings); + + subscriber = TestHelper.mockSubscriber(); + inOrder = inOrder(subscriber); + + zipped.subscribe(subscriber); + } + + BiFunction<Object, Object, String> zipr2 = new BiFunction<Object, Object, String>() { + + @Override + public String apply(Object t1, Object t2) { + return "" + t1 + t2; + } + + }; + Function3<Object, Object, Object, String> zipr3 = new Function3<Object, Object, Object, String>() { + + @Override + public String apply(Object t1, Object t2, Object t3) { + return "" + t1 + t2 + t3; + } + + }; + + @Test + public void zipIterableSameSize() { + PublishProcessor<String> r1 = PublishProcessor.create(); + /* define a Subscriber to receive aggregated events */ + Subscriber<String> subscriber = TestHelper.mockSubscriber(); + InOrder io = inOrder(subscriber); + + Iterable<String> r2 = Arrays.asList("1", "2", "3"); + + r1.zipWith(r2, zipr2).subscribe(subscriber); + + r1.onNext("one-"); + r1.onNext("two-"); + r1.onNext("three-"); + r1.onComplete(); + + io.verify(subscriber).onNext("one-1"); + io.verify(subscriber).onNext("two-2"); + io.verify(subscriber).onNext("three-3"); + io.verify(subscriber).onComplete(); + + verify(subscriber, never()).onError(any(Throwable.class)); + + } + + @Test + public void zipIterableEmptyFirstSize() { + PublishProcessor<String> r1 = PublishProcessor.create(); + /* define a Subscriber to receive aggregated events */ + Subscriber<String> subscriber = TestHelper.mockSubscriber(); + InOrder io = inOrder(subscriber); + + Iterable<String> r2 = Arrays.asList("1", "2", "3"); + + r1.zipWith(r2, zipr2).subscribe(subscriber); + + r1.onComplete(); + + io.verify(subscriber).onComplete(); + + verify(subscriber, never()).onNext(any(String.class)); + verify(subscriber, never()).onError(any(Throwable.class)); + + } + + @Test + public void zipIterableEmptySecond() { + PublishProcessor<String> r1 = PublishProcessor.create(); + /* define a Subscriber to receive aggregated events */ + Subscriber<String> subscriber = TestHelper.mockSubscriber(); + InOrder io = inOrder(subscriber); + + Iterable<String> r2 = Arrays.asList(); + + r1.zipWith(r2, zipr2).subscribe(subscriber); + + r1.onNext("one-"); + r1.onNext("two-"); + r1.onNext("three-"); + r1.onComplete(); + + io.verify(subscriber).onComplete(); + + verify(subscriber, never()).onNext(any(String.class)); + verify(subscriber, never()).onError(any(Throwable.class)); + } + + @Test + public void zipIterableFirstShorter() { + PublishProcessor<String> r1 = PublishProcessor.create(); + /* define a Subscriber to receive aggregated events */ + Subscriber<String> subscriber = TestHelper.mockSubscriber(); + InOrder io = inOrder(subscriber); + + Iterable<String> r2 = Arrays.asList("1", "2", "3"); + + r1.zipWith(r2, zipr2).subscribe(subscriber); + + r1.onNext("one-"); + r1.onNext("two-"); + r1.onComplete(); + + io.verify(subscriber).onNext("one-1"); + io.verify(subscriber).onNext("two-2"); + io.verify(subscriber).onComplete(); + + verify(subscriber, never()).onError(any(Throwable.class)); + + } + + @Test + public void zipIterableSecondShorter() { + PublishProcessor<String> r1 = PublishProcessor.create(); + /* define a Subscriber to receive aggregated events */ + Subscriber<String> subscriber = TestHelper.mockSubscriber(); + InOrder io = inOrder(subscriber); + + Iterable<String> r2 = Arrays.asList("1", "2"); + + r1.zipWith(r2, zipr2).subscribe(subscriber); + + r1.onNext("one-"); + r1.onNext("two-"); + r1.onNext("three-"); + r1.onComplete(); + + io.verify(subscriber).onNext("one-1"); + io.verify(subscriber).onNext("two-2"); + io.verify(subscriber).onComplete(); + + verify(subscriber, never()).onError(any(Throwable.class)); + + } + + @Test + public void zipIterableFirstThrows() { + PublishProcessor<String> r1 = PublishProcessor.create(); + /* define a Subscriber to receive aggregated events */ + Subscriber<String> subscriber = TestHelper.mockSubscriber(); + InOrder io = inOrder(subscriber); + + Iterable<String> r2 = Arrays.asList("1", "2", "3"); + + r1.zipWith(r2, zipr2).subscribe(subscriber); + + r1.onNext("one-"); + r1.onNext("two-"); + r1.onError(new TestException()); + + io.verify(subscriber).onNext("one-1"); + io.verify(subscriber).onNext("two-2"); + io.verify(subscriber).onError(any(TestException.class)); + + verify(subscriber, never()).onComplete(); + + } + + @Test + public void zipIterableIteratorThrows() { + PublishProcessor<String> r1 = PublishProcessor.create(); + /* define a Subscriber to receive aggregated events */ + Subscriber<String> subscriber = TestHelper.mockSubscriber(); + InOrder io = inOrder(subscriber); + + Iterable<String> r2 = new Iterable<String>() { + @Override + public Iterator<String> iterator() { + throw new TestException(); + } + }; + + r1.zipWith(r2, zipr2).subscribe(subscriber); + + r1.onNext("one-"); + r1.onNext("two-"); + r1.onError(new TestException()); + + io.verify(subscriber).onError(any(TestException.class)); + + verify(subscriber, never()).onComplete(); + verify(subscriber, never()).onNext(any(String.class)); + + } + + @Test + public void zipIterableHasNextThrows() { + PublishProcessor<String> r1 = PublishProcessor.create(); + /* define a Subscriber to receive aggregated events */ + Subscriber<String> subscriber = TestHelper.mockSubscriber(); + InOrder io = inOrder(subscriber); + + Iterable<String> r2 = new Iterable<String>() { + + @Override + public Iterator<String> iterator() { + return new Iterator<String>() { + int count; + + @Override + public boolean hasNext() { + if (count == 0) { + return true; + } + throw new TestException(); + } + + @Override + public String next() { + count++; + return "1"; + } + + @Override + public void remove() { + throw new UnsupportedOperationException("Not supported yet."); + } + + }; + } + + }; + + r1.zipWith(r2, zipr2).subscribe(subscriber); + + r1.onNext("one-"); + r1.onError(new TestException()); + + io.verify(subscriber).onNext("one-1"); + io.verify(subscriber).onError(any(TestException.class)); + + verify(subscriber, never()).onComplete(); + + } + + @Test + public void zipIterableNextThrows() { + PublishProcessor<String> r1 = PublishProcessor.create(); + /* define a Subscriber to receive aggregated events */ + Subscriber<String> subscriber = TestHelper.mockSubscriber(); + InOrder io = inOrder(subscriber); + + Iterable<String> r2 = new Iterable<String>() { + + @Override + public Iterator<String> iterator() { + return new Iterator<String>() { + @Override + public boolean hasNext() { + return true; + } + + @Override + public String next() { + throw new TestException(); + } + + @Override + public void remove() { + throw new UnsupportedOperationException("Not supported yet."); + } + + }; + } + + }; + + r1.zipWith(r2, zipr2).subscribe(subscriber); + + r1.onError(new TestException()); + + io.verify(subscriber).onError(any(TestException.class)); + + verify(subscriber, never()).onNext(any(String.class)); + verify(subscriber, never()).onComplete(); + + } + + Consumer<String> printer = new Consumer<String>() { + @Override + public void accept(String pv) { + System.out.println(pv); + } + }; + + static final class SquareStr implements Function<Integer, String> { + final AtomicInteger counter = new AtomicInteger(); + @Override + public String apply(Integer t1) { + counter.incrementAndGet(); + System.out.println("Omg I'm calculating so hard: " + t1 + "*" + t1 + "=" + (t1 * t1)); + return " " + (t1 * t1); + } + } + + @Test + public void take2() { + Flowable<Integer> f = Flowable.just(1, 2, 3, 4, 5); + Iterable<String> it = Arrays.asList("a", "b", "c", "d", "e"); + + SquareStr squareStr = new SquareStr(); + + f.map(squareStr).zipWith(it, concat2Strings).take(2).subscribe(printer); + + assertEquals(2, squareStr.counter.get()); + } + + @Test + public void dispose() { + TestHelper.checkDisposed(Flowable.just(1).zipWith(Arrays.asList(1), new BiFunction<Integer, Integer, Object>() { + @Override + public Object apply(Integer a, Integer b) throws Exception { + return a + b; + } + })); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeFlowable(new Function<Flowable<Integer>, Flowable<Object>>() { + @Override + public Flowable<Object> apply(Flowable<Integer> f) throws Exception { + return f.zipWith(Arrays.asList(1), new BiFunction<Integer, Integer, Object>() { + @Override + public Object apply(Integer a, Integer b) throws Exception { + return a + b; + } + }); + } + }); + } + + @Test + public void iteratorThrows() { + Flowable.just(1).zipWith(new CrashingIterable(100, 1, 100), new BiFunction<Integer, Integer, Object>() { + @Override + public Object apply(Integer a, Integer b) throws Exception { + return a + b; + } + }) + .to(TestHelper.<Object>testConsumer()) + .assertFailureAndMessage(TestException.class, "hasNext()"); + } + + @Test + public void badSource() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + new Flowable<Integer>() { + @Override + protected void subscribeActual(Subscriber<? super Integer> subscriber) { + subscriber.onSubscribe(new BooleanSubscription()); + subscriber.onNext(1); + subscriber.onComplete(); + subscriber.onNext(2); + subscriber.onError(new TestException()); + subscriber.onComplete(); + } + } + .zipWith(Arrays.asList(1), new BiFunction<Integer, Integer, Object>() { + @Override + public Object apply(Integer a, Integer b) throws Exception { + return a + b; + } + }) + .test() + .assertResult(2); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableZipTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableZipTest.java new file mode 100644 index 0000000000..47bbd491bd --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/FlowableZipTest.java @@ -0,0 +1,1943 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +import java.lang.reflect.*; +import java.util.*; +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.*; +import org.mockito.InOrder; +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.internal.subscriptions.BooleanSubscription; +import io.reactivex.rxjava3.operators.QueueSubscription; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.processors.PublishProcessor; +import io.reactivex.rxjava3.schedulers.Schedulers; +import io.reactivex.rxjava3.subscribers.*; +import io.reactivex.rxjava3.testsupport.*; + +public class FlowableZipTest extends RxJavaTest { + BiFunction<String, String, String> concat2Strings; + PublishProcessor<String> s1; + PublishProcessor<String> s2; + Flowable<String> zipped; + + Subscriber<String> subscriber; + InOrder inOrder; + + @Before + public void setUp() { + concat2Strings = new BiFunction<String, String, String>() { + @Override + public String apply(String t1, String t2) { + return t1 + "-" + t2; + } + }; + + s1 = PublishProcessor.create(); + s2 = PublishProcessor.create(); + zipped = Flowable.zip(s1, s2, concat2Strings); + + subscriber = TestHelper.mockSubscriber(); + inOrder = inOrder(subscriber); + + zipped.subscribe(subscriber); + } + + @SuppressWarnings("unchecked") + @Test + public void collectionSizeDifferentThanFunction() { + Function<Object[], String> zipr = Functions.toFunction(getConcatStringIntegerIntArrayZipr()); + //Function3<String, Integer, int[], String> + + /* define a Subscriber to receive aggregated events */ + Subscriber<String> subscriber = TestHelper.mockSubscriber(); + + @SuppressWarnings("rawtypes") + Collection ws = java.util.Collections.singleton(Flowable.just("one", "two")); + Flowable<String> w = Flowable.zip(ws, zipr); + w.subscribe(subscriber); + + verify(subscriber, times(1)).onError(any(Throwable.class)); + verify(subscriber, never()).onComplete(); + verify(subscriber, never()).onNext(any(String.class)); + } + + @Test + public void startpingDifferentLengthFlowableSequences1() { + Subscriber<String> w = TestHelper.mockSubscriber(); + + TestFlowable w1 = new TestFlowable(); + TestFlowable w2 = new TestFlowable(); + TestFlowable w3 = new TestFlowable(); + + Flowable<String> zipW = Flowable.zip( + Flowable.unsafeCreate(w1), Flowable.unsafeCreate(w2), + Flowable.unsafeCreate(w3), getConcat3StringsZipr()); + zipW.subscribe(w); + + /* simulate sending data */ + // once for w1 + w1.subscriber.onNext("1a"); + w1.subscriber.onComplete(); + // twice for w2 + w2.subscriber.onNext("2a"); + w2.subscriber.onNext("2b"); + w2.subscriber.onComplete(); + // 4 times for w3 + w3.subscriber.onNext("3a"); + w3.subscriber.onNext("3b"); + w3.subscriber.onNext("3c"); + w3.subscriber.onNext("3d"); + w3.subscriber.onComplete(); + + /* we should have been called 1 time on the Subscriber */ + InOrder io = inOrder(w); + io.verify(w).onNext("1a2a3a"); + + io.verify(w, times(1)).onComplete(); + } + + @Test + public void startpingDifferentLengthFlowableSequences2() { + Subscriber<String> w = TestHelper.mockSubscriber(); + + TestFlowable w1 = new TestFlowable(); + TestFlowable w2 = new TestFlowable(); + TestFlowable w3 = new TestFlowable(); + + Flowable<String> zipW = Flowable.zip(Flowable.unsafeCreate(w1), Flowable.unsafeCreate(w2), Flowable.unsafeCreate(w3), getConcat3StringsZipr()); + zipW.subscribe(w); + + /* simulate sending data */ + // 4 times for w1 + w1.subscriber.onNext("1a"); + w1.subscriber.onNext("1b"); + w1.subscriber.onNext("1c"); + w1.subscriber.onNext("1d"); + w1.subscriber.onComplete(); + // twice for w2 + w2.subscriber.onNext("2a"); + w2.subscriber.onNext("2b"); + w2.subscriber.onComplete(); + // 1 times for w3 + w3.subscriber.onNext("3a"); + w3.subscriber.onComplete(); + + /* we should have been called 1 time on the Subscriber */ + InOrder io = inOrder(w); + io.verify(w).onNext("1a2a3a"); + + io.verify(w, times(1)).onComplete(); + + } + + BiFunction<Object, Object, String> zipr2 = new BiFunction<Object, Object, String>() { + + @Override + public String apply(Object t1, Object t2) { + return "" + t1 + t2; + } + + }; + Function3<Object, Object, Object, String> zipr3 = new Function3<Object, Object, Object, String>() { + + @Override + public String apply(Object t1, Object t2, Object t3) { + return "" + t1 + t2 + t3; + } + + }; + + /** + * Testing internal private logic due to the complexity so I want to use TDD to test as a I build it rather than relying purely on the overall functionality expected by the public methods. + */ + @Test + public void aggregatorSimple() { + PublishProcessor<String> r1 = PublishProcessor.create(); + PublishProcessor<String> r2 = PublishProcessor.create(); + /* define a Subscriber to receive aggregated events */ + Subscriber<String> subscriber = TestHelper.mockSubscriber(); + + Flowable.zip(r1, r2, zipr2).subscribe(subscriber); + + /* simulate the Flowables pushing data into the aggregator */ + r1.onNext("hello"); + r2.onNext("world"); + + InOrder inOrder = inOrder(subscriber); + + verify(subscriber, never()).onError(any(Throwable.class)); + verify(subscriber, never()).onComplete(); + inOrder.verify(subscriber, times(1)).onNext("helloworld"); + + r1.onNext("hello "); + r2.onNext("again"); + + verify(subscriber, never()).onError(any(Throwable.class)); + verify(subscriber, never()).onComplete(); + inOrder.verify(subscriber, times(1)).onNext("hello again"); + + r1.onComplete(); + r2.onComplete(); + + inOrder.verify(subscriber, never()).onNext(anyString()); + verify(subscriber, times(1)).onComplete(); + } + + @Test + public void aggregatorDifferentSizedResultsWithOnComplete() { + /* create the aggregator which will execute the zip function when all Flowables provide values */ + /* define a Subscriber to receive aggregated events */ + PublishProcessor<String> r1 = PublishProcessor.create(); + PublishProcessor<String> r2 = PublishProcessor.create(); + /* define a Subscriber to receive aggregated events */ + Subscriber<String> subscriber = TestHelper.mockSubscriber(); + Flowable.zip(r1, r2, zipr2).subscribe(subscriber); + + /* simulate the Flowables pushing data into the aggregator */ + r1.onNext("hello"); + r2.onNext("world"); + r2.onComplete(); + + InOrder inOrder = inOrder(subscriber); + + inOrder.verify(subscriber, never()).onError(any(Throwable.class)); + inOrder.verify(subscriber, times(1)).onNext("helloworld"); + inOrder.verify(subscriber, times(1)).onComplete(); + + r1.onNext("hi"); + r1.onComplete(); + + inOrder.verify(subscriber, never()).onError(any(Throwable.class)); + inOrder.verify(subscriber, never()).onComplete(); + inOrder.verify(subscriber, never()).onNext(anyString()); + } + + @Test + public void aggregateMultipleTypes() { + PublishProcessor<String> r1 = PublishProcessor.create(); + PublishProcessor<Integer> r2 = PublishProcessor.create(); + /* define a Subscriber to receive aggregated events */ + Subscriber<String> subscriber = TestHelper.mockSubscriber(); + + Flowable.zip(r1, r2, zipr2).subscribe(subscriber); + + /* simulate the Flowables pushing data into the aggregator */ + r1.onNext("hello"); + r2.onNext(1); + r2.onComplete(); + + InOrder inOrder = inOrder(subscriber); + + inOrder.verify(subscriber, never()).onError(any(Throwable.class)); + inOrder.verify(subscriber, times(1)).onNext("hello1"); + inOrder.verify(subscriber, times(1)).onComplete(); + + r1.onNext("hi"); + r1.onComplete(); + + inOrder.verify(subscriber, never()).onError(any(Throwable.class)); + inOrder.verify(subscriber, never()).onComplete(); + inOrder.verify(subscriber, never()).onNext(anyString()); + } + + @Test + public void aggregate3Types() { + PublishProcessor<String> r1 = PublishProcessor.create(); + PublishProcessor<Integer> r2 = PublishProcessor.create(); + PublishProcessor<List<Integer>> r3 = PublishProcessor.create(); + /* define a Subscriber to receive aggregated events */ + Subscriber<String> subscriber = TestHelper.mockSubscriber(); + + Flowable.zip(r1, r2, r3, zipr3).subscribe(subscriber); + + /* simulate the Flowables pushing data into the aggregator */ + r1.onNext("hello"); + r2.onNext(2); + r3.onNext(Arrays.asList(5, 6, 7)); + + verify(subscriber, never()).onError(any(Throwable.class)); + verify(subscriber, never()).onComplete(); + verify(subscriber, times(1)).onNext("hello2[5, 6, 7]"); + } + + @Test + public void aggregatorsWithDifferentSizesAndTiming() { + PublishProcessor<String> r1 = PublishProcessor.create(); + PublishProcessor<String> r2 = PublishProcessor.create(); + /* define a Subscriber to receive aggregated events */ + Subscriber<String> subscriber = TestHelper.mockSubscriber(); + + Flowable.zip(r1, r2, zipr2).subscribe(subscriber); + + /* simulate the Flowables pushing data into the aggregator */ + r1.onNext("one"); + r1.onNext("two"); + r1.onNext("three"); + r2.onNext("A"); + + verify(subscriber, never()).onError(any(Throwable.class)); + verify(subscriber, never()).onComplete(); + verify(subscriber, times(1)).onNext("oneA"); + + r1.onNext("four"); + r1.onComplete(); + r2.onNext("B"); + verify(subscriber, times(1)).onNext("twoB"); + r2.onNext("C"); + verify(subscriber, times(1)).onNext("threeC"); + r2.onNext("D"); + verify(subscriber, times(1)).onNext("fourD"); + r2.onNext("E"); + verify(subscriber, never()).onNext("E"); + r2.onComplete(); + + verify(subscriber, never()).onError(any(Throwable.class)); + verify(subscriber, times(1)).onComplete(); + } + + @Test + public void aggregatorError() { + PublishProcessor<String> r1 = PublishProcessor.create(); + PublishProcessor<String> r2 = PublishProcessor.create(); + /* define a Subscriber to receive aggregated events */ + Subscriber<String> subscriber = TestHelper.mockSubscriber(); + + Flowable.zip(r1, r2, zipr2).subscribe(subscriber); + + /* simulate the Flowables pushing data into the aggregator */ + r1.onNext("hello"); + r2.onNext("world"); + + verify(subscriber, never()).onError(any(Throwable.class)); + verify(subscriber, never()).onComplete(); + verify(subscriber, times(1)).onNext("helloworld"); + + r1.onError(new RuntimeException("")); + r1.onNext("hello"); + r2.onNext("again"); + + verify(subscriber, times(1)).onError(any(Throwable.class)); + verify(subscriber, never()).onComplete(); + // we don't want to be called again after an error + verify(subscriber, times(0)).onNext("helloagain"); + } + + @Test + public void aggregatorUnsubscribe() { + PublishProcessor<String> r1 = PublishProcessor.create(); + PublishProcessor<String> r2 = PublishProcessor.create(); + /* define a Subscriber to receive aggregated events */ + Subscriber<String> subscriber = TestHelper.mockSubscriber(); + TestSubscriber<String> ts = new TestSubscriber<>(subscriber); + + Flowable.zip(r1, r2, zipr2).subscribe(ts); + + /* simulate the Flowables pushing data into the aggregator */ + r1.onNext("hello"); + r2.onNext("world"); + + verify(subscriber, never()).onError(any(Throwable.class)); + verify(subscriber, never()).onComplete(); + verify(subscriber, times(1)).onNext("helloworld"); + + ts.cancel(); + r1.onNext("hello"); + r2.onNext("again"); + + verify(subscriber, times(0)).onError(any(Throwable.class)); + verify(subscriber, never()).onComplete(); + // we don't want to be called again after an error + verify(subscriber, times(0)).onNext("helloagain"); + } + + @Test + public void aggregatorEarlyCompletion() { + PublishProcessor<String> r1 = PublishProcessor.create(); + PublishProcessor<String> r2 = PublishProcessor.create(); + /* define a Subscriber to receive aggregated events */ + Subscriber<String> subscriber = TestHelper.mockSubscriber(); + + Flowable.zip(r1, r2, zipr2).subscribe(subscriber); + + /* simulate the Flowables pushing data into the aggregator */ + r1.onNext("one"); + r1.onNext("two"); + r1.onComplete(); + r2.onNext("A"); + + InOrder inOrder = inOrder(subscriber); + + inOrder.verify(subscriber, never()).onError(any(Throwable.class)); + inOrder.verify(subscriber, never()).onComplete(); + inOrder.verify(subscriber, times(1)).onNext("oneA"); + + r2.onComplete(); + + inOrder.verify(subscriber, never()).onError(any(Throwable.class)); + inOrder.verify(subscriber, times(1)).onComplete(); + inOrder.verify(subscriber, never()).onNext(anyString()); + } + + @Test + public void start2Types() { + BiFunction<String, Integer, String> zipr = getConcatStringIntegerZipr(); + + /* define a Subscriber to receive aggregated events */ + Subscriber<String> subscriber = TestHelper.mockSubscriber(); + + Flowable<String> w = Flowable.zip(Flowable.just("one", "two"), Flowable.just(2, 3, 4), zipr); + w.subscribe(subscriber); + + verify(subscriber, never()).onError(any(Throwable.class)); + verify(subscriber, times(1)).onComplete(); + verify(subscriber, times(1)).onNext("one2"); + verify(subscriber, times(1)).onNext("two3"); + verify(subscriber, never()).onNext("4"); + } + + @Test + public void start3Types() { + Function3<String, Integer, int[], String> zipr = getConcatStringIntegerIntArrayZipr(); + + /* define a Subscriber to receive aggregated events */ + Subscriber<String> subscriber = TestHelper.mockSubscriber(); + + Flowable<String> w = Flowable.zip(Flowable.just("one", "two"), Flowable.just(2), Flowable.just(new int[] { 4, 5, 6 }), zipr); + w.subscribe(subscriber); + + verify(subscriber, never()).onError(any(Throwable.class)); + verify(subscriber, times(1)).onComplete(); + verify(subscriber, times(1)).onNext("one2[4, 5, 6]"); + verify(subscriber, never()).onNext("two"); + } + + @Test + public void onNextExceptionInvokesOnError() { + BiFunction<Integer, Integer, Integer> zipr = getDivideZipr(); + + Subscriber<Integer> subscriber = TestHelper.mockSubscriber(); + + Flowable<Integer> w = Flowable.zip(Flowable.just(10, 20, 30), Flowable.just(0, 1, 2), zipr); + w.subscribe(subscriber); + + verify(subscriber, times(1)).onError(any(Throwable.class)); + } + + @Test + public void onFirstCompletion() { + PublishProcessor<String> oA = PublishProcessor.create(); + PublishProcessor<String> oB = PublishProcessor.create(); + + Subscriber<String> obs = TestHelper.mockSubscriber(); + + Flowable<String> f = Flowable.zip(oA, oB, getConcat2Strings()); + f.subscribe(obs); + + InOrder io = inOrder(obs); + + oA.onNext("a1"); + io.verify(obs, never()).onNext(anyString()); + oB.onNext("b1"); + io.verify(obs, times(1)).onNext("a1-b1"); + oB.onNext("b2"); + io.verify(obs, never()).onNext(anyString()); + oA.onNext("a2"); + io.verify(obs, times(1)).onNext("a2-b2"); + + oA.onNext("a3"); + oA.onNext("a4"); + oA.onNext("a5"); + oA.onComplete(); + + // SHOULD ONCOMPLETE BE EMITTED HERE INSTEAD OF WAITING + // FOR B3, B4, B5 TO BE EMITTED? + + oB.onNext("b3"); + oB.onNext("b4"); + oB.onNext("b5"); + + io.verify(obs, times(1)).onNext("a3-b3"); + io.verify(obs, times(1)).onNext("a4-b4"); + io.verify(obs, times(1)).onNext("a5-b5"); + + // WE RECEIVE THE ONCOMPLETE HERE + io.verify(obs, times(1)).onComplete(); + + oB.onNext("b6"); + oB.onNext("b7"); + oB.onNext("b8"); + oB.onNext("b9"); + // never completes (infinite stream for example) + + // we should receive nothing else despite oB continuing after oA completed + io.verifyNoMoreInteractions(); + } + + @Test + public void onErrorTermination() { + PublishProcessor<String> oA = PublishProcessor.create(); + PublishProcessor<String> oB = PublishProcessor.create(); + + Subscriber<String> obs = TestHelper.mockSubscriber(); + + Flowable<String> f = Flowable.zip(oA, oB, getConcat2Strings()); + f.subscribe(obs); + + InOrder io = inOrder(obs); + + oA.onNext("a1"); + io.verify(obs, never()).onNext(anyString()); + oB.onNext("b1"); + io.verify(obs, times(1)).onNext("a1-b1"); + oB.onNext("b2"); + io.verify(obs, never()).onNext(anyString()); + oA.onNext("a2"); + io.verify(obs, times(1)).onNext("a2-b2"); + + oA.onNext("a3"); + oA.onNext("a4"); + oA.onNext("a5"); + oA.onError(new RuntimeException("forced failure")); + + // it should emit failure immediately + io.verify(obs, times(1)).onError(any(RuntimeException.class)); + + oB.onNext("b3"); + oB.onNext("b4"); + oB.onNext("b5"); + oB.onNext("b6"); + oB.onNext("b7"); + oB.onNext("b8"); + oB.onNext("b9"); + // never completes (infinite stream for example) + + // we should receive nothing else despite oB continuing after oA completed + io.verifyNoMoreInteractions(); + } + + private BiFunction<String, String, String> getConcat2Strings() { + return new BiFunction<String, String, String>() { + + @Override + public String apply(String t1, String t2) { + return t1 + "-" + t2; + } + }; + } + + private BiFunction<Integer, Integer, Integer> getDivideZipr() { + BiFunction<Integer, Integer, Integer> zipr = new BiFunction<Integer, Integer, Integer>() { + + @Override + public Integer apply(Integer i1, Integer i2) { + return i1 / i2; + } + + }; + return zipr; + } + + private Function3<String, String, String, String> getConcat3StringsZipr() { + Function3<String, String, String, String> zipr = new Function3<String, String, String, String>() { + + @Override + public String apply(String a1, String a2, String a3) { + if (a1 == null) { + a1 = ""; + } + if (a2 == null) { + a2 = ""; + } + if (a3 == null) { + a3 = ""; + } + return a1 + a2 + a3; + } + + }; + return zipr; + } + + private BiFunction<String, Integer, String> getConcatStringIntegerZipr() { + BiFunction<String, Integer, String> zipr = new BiFunction<String, Integer, String>() { + + @Override + public String apply(String s, Integer i) { + return getStringValue(s) + getStringValue(i); + } + + }; + return zipr; + } + + private Function3<String, Integer, int[], String> getConcatStringIntegerIntArrayZipr() { + Function3<String, Integer, int[], String> zipr = new Function3<String, Integer, int[], String>() { + + @Override + public String apply(String s, Integer i, int[] iArray) { + return getStringValue(s) + getStringValue(i) + getStringValue(iArray); + } + + }; + return zipr; + } + + private static String getStringValue(Object o) { + if (o == null) { + return ""; + } else { + if (o instanceof int[]) { + return Arrays.toString((int[]) o); + } else { + return String.valueOf(o); + } + } + } + + private static class TestFlowable implements Publisher<String> { + + Subscriber<? super String> subscriber; + + @Override + public void subscribe(Subscriber<? super String> subscriber) { + // just store the variable where it can be accessed so we can manually trigger it + this.subscriber = subscriber; + subscriber.onSubscribe(new BooleanSubscription()); + } + + } + + @Test + public void firstCompletesThenSecondInfinite() { + s1.onNext("a"); + s1.onNext("b"); + s1.onComplete(); + s2.onNext("1"); + inOrder.verify(subscriber, times(1)).onNext("a-1"); + s2.onNext("2"); + inOrder.verify(subscriber, times(1)).onNext("b-2"); + inOrder.verify(subscriber, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void secondInfiniteThenFirstCompletes() { + s2.onNext("1"); + s2.onNext("2"); + s1.onNext("a"); + inOrder.verify(subscriber, times(1)).onNext("a-1"); + s1.onNext("b"); + inOrder.verify(subscriber, times(1)).onNext("b-2"); + s1.onComplete(); + inOrder.verify(subscriber, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void secondCompletesThenFirstInfinite() { + s2.onNext("1"); + s2.onNext("2"); + s2.onComplete(); + s1.onNext("a"); + inOrder.verify(subscriber, times(1)).onNext("a-1"); + s1.onNext("b"); + inOrder.verify(subscriber, times(1)).onNext("b-2"); + inOrder.verify(subscriber, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void firstInfiniteThenSecondCompletes() { + s1.onNext("a"); + s1.onNext("b"); + s2.onNext("1"); + inOrder.verify(subscriber, times(1)).onNext("a-1"); + s2.onNext("2"); + inOrder.verify(subscriber, times(1)).onNext("b-2"); + s2.onComplete(); + inOrder.verify(subscriber, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void firstFails() { + s2.onNext("a"); + s1.onError(new RuntimeException("Forced failure")); + + inOrder.verify(subscriber, times(1)).onError(any(RuntimeException.class)); + + s2.onNext("b"); + s1.onNext("1"); + s1.onNext("2"); + + inOrder.verify(subscriber, never()).onComplete(); + inOrder.verify(subscriber, never()).onNext(any(String.class)); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void secondFails() { + s1.onNext("a"); + s1.onNext("b"); + s2.onError(new RuntimeException("Forced failure")); + + inOrder.verify(subscriber, times(1)).onError(any(RuntimeException.class)); + + s2.onNext("1"); + s2.onNext("2"); + + inOrder.verify(subscriber, never()).onComplete(); + inOrder.verify(subscriber, never()).onNext(any(String.class)); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void startWithOnCompletedTwice() { + // issue: https://groups.google.com/forum/#!topic/rxjava/79cWTv3TFp0 + // The problem is the original "zip" implementation does not wrap + // an internal subscriber with a SafeSubscriber. However, in the "zip", + // it may calls "onComplete" twice. That breaks the Rx contract. + + // This test tries to emulate this case. + // As "TestHelper.mockSubscriber()" will create an instance in the package "rx", + // we need to wrap "TestHelper.mockSubscriber()" with an subscriber instance + // which is in the package "rx.operators". + final Subscriber<Integer> subscriber = TestHelper.mockSubscriber(); + + Flowable.zip(Flowable.just(1), + Flowable.just(1), new BiFunction<Integer, Integer, Integer>() { + @Override + public Integer apply(Integer a, Integer b) { + return a + b; + } + }).subscribe(new DefaultSubscriber<Integer>() { + + @Override + public void onComplete() { + subscriber.onComplete(); + } + + @Override + public void onError(Throwable e) { + subscriber.onError(e); + } + + @Override + public void onNext(Integer args) { + subscriber.onNext(args); + } + + }); + + InOrder inOrder = inOrder(subscriber); + inOrder.verify(subscriber, times(1)).onNext(2); + inOrder.verify(subscriber, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void start() { + Flowable<String> os = OBSERVABLE_OF_5_INTEGERS + .zipWith(OBSERVABLE_OF_5_INTEGERS, new BiFunction<Integer, Integer, String>() { + + @Override + public String apply(Integer a, Integer b) { + return a + "-" + b; + } + }); + + final ArrayList<String> list = new ArrayList<>(); + os.subscribe(new Consumer<String>() { + + @Override + public void accept(String s) { + System.out.println(s); + list.add(s); + } + }); + + assertEquals(5, list.size()); + assertEquals("1-1", list.get(0)); + assertEquals("2-2", list.get(1)); + assertEquals("5-5", list.get(4)); + } + + @Test + public void startAsync() throws InterruptedException { + Flowable<String> os = ASYNC_OBSERVABLE_OF_INFINITE_INTEGERS(new CountDownLatch(1)).onBackpressureBuffer() + .zipWith(ASYNC_OBSERVABLE_OF_INFINITE_INTEGERS(new CountDownLatch(1)).onBackpressureBuffer(), new BiFunction<Integer, Integer, String>() { + + @Override + public String apply(Integer a, Integer b) { + return a + "-" + b; + } + }).take(5); + + TestSubscriber<String> ts = new TestSubscriber<>(); + os.subscribe(ts); + + ts.awaitDone(5, TimeUnit.SECONDS); + ts.assertNoErrors(); + + assertEquals(5, ts.values().size()); + assertEquals("1-1", ts.values().get(0)); + assertEquals("2-2", ts.values().get(1)); + assertEquals("5-5", ts.values().get(4)); + } + + @Test + public void startInfiniteAndFinite() throws InterruptedException { + final CountDownLatch latch = new CountDownLatch(1); + final CountDownLatch infiniteFlowable = new CountDownLatch(1); + Flowable<String> os = OBSERVABLE_OF_5_INTEGERS + .zipWith(ASYNC_OBSERVABLE_OF_INFINITE_INTEGERS(infiniteFlowable), new BiFunction<Integer, Integer, String>() { + + @Override + public String apply(Integer a, Integer b) { + return a + "-" + b; + } + }); + + final ArrayList<String> list = new ArrayList<>(); + os.subscribe(new DefaultSubscriber<String>() { + + @Override + public void onComplete() { + latch.countDown(); + } + + @Override + public void onError(Throwable e) { + e.printStackTrace(); + latch.countDown(); + } + + @Override + public void onNext(String s) { + System.out.println(s); + list.add(s); + } + }); + + latch.await(1000, TimeUnit.MILLISECONDS); + if (!infiniteFlowable.await(2000, TimeUnit.MILLISECONDS)) { + throw new RuntimeException("didn't unsubscribe"); + } + + assertEquals(5, list.size()); + assertEquals("1-1", list.get(0)); + assertEquals("2-2", list.get(1)); + assertEquals("5-5", list.get(4)); + } + + @SuppressWarnings("rawtypes") + static String kind(Notification notification) { + if (notification.isOnError()) { + return "OnError"; + } + if (notification.isOnNext()) { + return "OnNext"; + } + return "OnComplete"; + } + + @SuppressWarnings("rawtypes") + static String value(Notification notification) { + if (notification.isOnNext()) { + return String.valueOf(notification.getValue()); + } + return "null"; + } + + @Test + public void emitMaterializedNotifications() { + Flowable<Notification<Integer>> oi = Flowable.just(1, 2, 3).materialize(); + Flowable<Notification<String>> os = Flowable.just("a", "b", "c").materialize(); + Flowable<String> f = Flowable.zip(oi, os, new BiFunction<Notification<Integer>, Notification<String>, String>() { + + @Override + public String apply(Notification<Integer> t1, Notification<String> t2) { + return kind(t1) + "_" + value(t1) + "-" + kind(t2) + "_" + value(t2); + } + + }); + + final ArrayList<String> list = new ArrayList<>(); + f.subscribe(new Consumer<String>() { + + @Override + public void accept(String s) { + System.out.println(s); + list.add(s); + } + }); + + assertEquals(4, list.size()); + assertEquals("OnNext_1-OnNext_a", list.get(0)); + assertEquals("OnNext_2-OnNext_b", list.get(1)); + assertEquals("OnNext_3-OnNext_c", list.get(2)); + assertEquals("OnComplete_null-OnComplete_null", list.get(3)); + } + + @Test + public void startEmptyFlowables() { + + Flowable<String> f = Flowable.zip(Flowable.<Integer> empty(), Flowable.<String> empty(), new BiFunction<Integer, String, String>() { + + @Override + public String apply(Integer t1, String t2) { + return t1 + "-" + t2; + } + + }); + + final ArrayList<String> list = new ArrayList<>(); + f.subscribe(new Consumer<String>() { + + @Override + public void accept(String s) { + System.out.println(s); + list.add(s); + } + }); + + assertEquals(0, list.size()); + } + + @Test + public void startEmptyList() { + + final Object invoked = new Object(); + Collection<Flowable<Object>> observables = Collections.emptyList(); + + Flowable<Object> f = Flowable.zip(observables, new Function<Object[], Object>() { + @Override + public Object apply(final Object[] args) { + assertEquals("No argument should have been passed", 0, args.length); + return invoked; + } + }); + + TestSubscriber<Object> ts = new TestSubscriber<>(); + f.subscribe(ts); + ts.awaitDone(200, TimeUnit.MILLISECONDS); + ts.assertNoValues(); + } + + /** + * Expect NoSuchElementException instead of blocking forever as zip should emit onComplete and no onNext + * and last() expects at least a single response. + */ + @Test(expected = NoSuchElementException.class) + public void startEmptyListBlocking() { + + final Object invoked = new Object(); + Collection<Flowable<Object>> observables = Collections.emptyList(); + + Flowable<Object> f = Flowable.zip(observables, new Function<Object[], Object>() { + @Override + public Object apply(final Object[] args) { + assertEquals("No argument should have been passed", 0, args.length); + return invoked; + } + }); + + f.blockingLast(); + } + + @Test + public void backpressureSync() { + AtomicInteger generatedA = new AtomicInteger(); + AtomicInteger generatedB = new AtomicInteger(); + Flowable<Integer> f1 = createInfiniteFlowable(generatedA); + Flowable<Integer> f2 = createInfiniteFlowable(generatedB); + + TestSubscriber<String> ts = new TestSubscriber<>(); + Flowable.zip(f1, f2, new BiFunction<Integer, Integer, String>() { + + @Override + public String apply(Integer t1, Integer t2) { + return t1 + "-" + t2; + } + + }).take(Flowable.bufferSize() * 2).subscribe(ts); + + ts.awaitDone(5, TimeUnit.SECONDS); + ts.assertNoErrors(); + assertEquals(Flowable.bufferSize() * 2, ts.values().size()); + assertTrue(generatedA.get() < (Flowable.bufferSize() * 3)); + assertTrue(generatedB.get() < (Flowable.bufferSize() * 3)); + } + + @Test + public void backpressureAsync() { + AtomicInteger generatedA = new AtomicInteger(); + AtomicInteger generatedB = new AtomicInteger(); + Flowable<Integer> f1 = createInfiniteFlowable(generatedA).subscribeOn(Schedulers.computation()); + Flowable<Integer> f2 = createInfiniteFlowable(generatedB).subscribeOn(Schedulers.computation()); + + TestSubscriber<String> ts = new TestSubscriber<>(); + Flowable.zip(f1, f2, new BiFunction<Integer, Integer, String>() { + + @Override + public String apply(Integer t1, Integer t2) { + return t1 + "-" + t2; + } + + }).take(Flowable.bufferSize() * 2).subscribe(ts); + + ts.awaitDone(5, TimeUnit.SECONDS); + ts.assertNoErrors(); + assertEquals(Flowable.bufferSize() * 2, ts.values().size()); + assertTrue(generatedA.get() < (Flowable.bufferSize() * 3)); + assertTrue(generatedB.get() < (Flowable.bufferSize() * 3)); + } + + @Test + public void downstreamBackpressureRequestsWithFiniteSyncFlowables() { + AtomicInteger generatedA = new AtomicInteger(); + AtomicInteger generatedB = new AtomicInteger(); + Flowable<Integer> f1 = createInfiniteFlowable(generatedA).take(Flowable.bufferSize() * 2); + Flowable<Integer> f2 = createInfiniteFlowable(generatedB).take(Flowable.bufferSize() * 2); + + TestSubscriber<String> ts = new TestSubscriber<>(); + Flowable.zip(f1, f2, new BiFunction<Integer, Integer, String>() { + + @Override + public String apply(Integer t1, Integer t2) { + return t1 + "-" + t2; + } + + }).observeOn(Schedulers.computation()).take(Flowable.bufferSize() * 2).subscribe(ts); + + ts.awaitDone(5, TimeUnit.SECONDS); + ts.assertNoErrors(); + assertEquals(Flowable.bufferSize() * 2, ts.values().size()); + System.out.println("Generated => A: " + generatedA.get() + " B: " + generatedB.get()); + assertTrue(generatedA.get() < (Flowable.bufferSize() * 3)); + assertTrue(generatedB.get() < (Flowable.bufferSize() * 3)); + } + + @Test + public void downstreamBackpressureRequestsWithInfiniteAsyncFlowables() { + AtomicInteger generatedA = new AtomicInteger(); + AtomicInteger generatedB = new AtomicInteger(); + Flowable<Integer> f1 = createInfiniteFlowable(generatedA).subscribeOn(Schedulers.computation()); + Flowable<Integer> f2 = createInfiniteFlowable(generatedB).subscribeOn(Schedulers.computation()); + + TestSubscriber<String> ts = new TestSubscriber<>(); + Flowable.zip(f1, f2, new BiFunction<Integer, Integer, String>() { + + @Override + public String apply(Integer t1, Integer t2) { + return t1 + "-" + t2; + } + + }).observeOn(Schedulers.computation()).take(Flowable.bufferSize() * 2).subscribe(ts); + + ts.awaitDone(5, TimeUnit.SECONDS); + ts.assertNoErrors(); + assertEquals(Flowable.bufferSize() * 2, ts.values().size()); + System.out.println("Generated => A: " + generatedA.get() + " B: " + generatedB.get()); + assertTrue(generatedA.get() < (Flowable.bufferSize() * 4)); + assertTrue(generatedB.get() < (Flowable.bufferSize() * 4)); + } + + @Test + public void downstreamBackpressureRequestsWithInfiniteSyncFlowables() { + AtomicInteger generatedA = new AtomicInteger(); + AtomicInteger generatedB = new AtomicInteger(); + Flowable<Integer> f1 = createInfiniteFlowable(generatedA); + Flowable<Integer> f2 = createInfiniteFlowable(generatedB); + + TestSubscriber<String> ts = new TestSubscriber<>(); + Flowable.zip(f1, f2, new BiFunction<Integer, Integer, String>() { + + @Override + public String apply(Integer t1, Integer t2) { + return t1 + "-" + t2; + } + + }).observeOn(Schedulers.computation()).take(Flowable.bufferSize() * 2).subscribe(ts); + + ts.awaitDone(5, TimeUnit.SECONDS); + ts.assertNoErrors(); + assertEquals(Flowable.bufferSize() * 2, ts.values().size()); + System.out.println("Generated => A: " + generatedA.get() + " B: " + generatedB.get()); + assertTrue(generatedA.get() < (Flowable.bufferSize() * 4)); + assertTrue(generatedB.get() < (Flowable.bufferSize() * 4)); + } + + private Flowable<Integer> createInfiniteFlowable(final AtomicInteger generated) { + Flowable<Integer> flowable = Flowable.fromIterable(new Iterable<Integer>() { + @Override + public Iterator<Integer> iterator() { + return new Iterator<Integer>() { + + @Override + public void remove() { + } + + @Override + public Integer next() { + return generated.getAndIncrement(); + } + + @Override + public boolean hasNext() { + return true; + } + }; + } + }); + return flowable; + } + + Flowable<Integer> OBSERVABLE_OF_5_INTEGERS = OBSERVABLE_OF_5_INTEGERS(new AtomicInteger()); + + Flowable<Integer> OBSERVABLE_OF_5_INTEGERS(final AtomicInteger numEmitted) { + return Flowable.unsafeCreate(new Publisher<Integer>() { + + @Override + public void subscribe(final Subscriber<? super Integer> subscriber) { + BooleanSubscription bs = new BooleanSubscription(); + subscriber.onSubscribe(bs); + for (int i = 1; i <= 5; i++) { + if (bs.isCancelled()) { + break; + } + numEmitted.incrementAndGet(); + subscriber.onNext(i); + Thread.yield(); + } + subscriber.onComplete(); + } + + }); + } + + Flowable<Integer> ASYNC_OBSERVABLE_OF_INFINITE_INTEGERS(final CountDownLatch latch) { + return Flowable.unsafeCreate(new Publisher<Integer>() { + + @Override + public void subscribe(final Subscriber<? super Integer> subscriber) { + final BooleanSubscription bs = new BooleanSubscription(); + subscriber.onSubscribe(bs); + Thread t = new Thread(new Runnable() { + + @Override + public void run() { + System.out.println("-------> subscribe to infinite sequence"); + System.out.println("Starting thread: " + Thread.currentThread()); + int i = 1; + while (!bs.isCancelled()) { + subscriber.onNext(i++); + Thread.yield(); + } + subscriber.onComplete(); + latch.countDown(); + System.out.println("Ending thread: " + Thread.currentThread()); + } + }); + t.start(); + + } + + }); + } + + @Test + public void issue1812() { + // https://github.com/ReactiveX/RxJava/issues/1812 + Flowable<Integer> zip1 = Flowable.zip(Flowable.range(0, 1026), Flowable.range(0, 1026), + new BiFunction<Integer, Integer, Integer>() { + + @Override + public Integer apply(Integer i1, Integer i2) { + return i1 + i2; + } + }); + Flowable<Integer> zip2 = Flowable.zip(zip1, Flowable.range(0, 1026), + new BiFunction<Integer, Integer, Integer>() { + + @Override + public Integer apply(Integer i1, Integer i2) { + return i1 + i2; + } + }); + List<Integer> expected = new ArrayList<>(); + for (int i = 0; i < 1026; i++) { + expected.add(i * 3); + } + assertEquals(expected, zip2.toList().blockingGet()); + } + + @Test + public void unboundedDownstreamOverrequesting() { + Flowable<Integer> source = Flowable.range(1, 2).zipWith(Flowable.range(1, 2), new BiFunction<Integer, Integer, Integer>() { + @Override + public Integer apply(Integer t1, Integer t2) { + return t1 + 10 * t2; + } + }); + + TestSubscriberEx<Integer> ts = new TestSubscriberEx<Integer>() { + @Override + public void onNext(Integer t) { + super.onNext(t); + request(5); + } + }; + + source.subscribe(ts); + + ts.assertNoErrors(); + ts.assertTerminated(); + ts.assertValues(11, 22); + } + + @Test + public void zipRace() { + long startTime = System.currentTimeMillis(); + Flowable<Integer> src = Flowable.just(1).subscribeOn(Schedulers.computation()); + + // now try and generate a hang by zipping src with itself repeatedly. A + // time limit of 9 seconds ( 1 second less than the test timeout) is + // used so that this test will not timeout on slow machines. + int i = 0; + while (System.currentTimeMillis() - startTime < 9000 && i++ < 100000) { + int value = Flowable.zip(src, src, new BiFunction<Integer, Integer, Integer>() { + @Override + public Integer apply(Integer t1, Integer t2) { + return t1 + t2 * 10; + } + }).blockingSingle(0); + + Assert.assertEquals(11, value); + } + } + /** + * Request only a single value and don't wait for another request just + * to emit an onComplete. + */ + @Test + public void zipRequest1() { + Flowable<Integer> src = Flowable.just(1).subscribeOn(Schedulers.computation()); + TestSubscriber<Integer> ts = new TestSubscriber<>(1L); + + Flowable.zip(src, src, new BiFunction<Integer, Integer, Integer>() { + @Override + public Integer apply(Integer t1, Integer t2) { + return t1 + t2 * 10; + } + }).subscribe(ts); + + ts.awaitDone(1, TimeUnit.SECONDS); + ts.assertNoErrors(); + ts.assertValue(11); + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + @Test + public void zipNArguments() throws Exception { + Flowable source = Flowable.just(1); + + for (int i = 2; i < 10; i++) { + Class<?>[] types = new Class[i + 1]; + Arrays.fill(types, Publisher.class); + types[i] = i == 2 ? BiFunction.class : Class.forName("io.reactivex.rxjava3.functions.Function" + i); + + Method m = Flowable.class.getMethod("zip", types); + + Object[] params = new Object[i + 1]; + Arrays.fill(params, source); + params[i] = ArgsToString.INSTANCE; + + StringBuilder b = new StringBuilder(); + for (int j = 0; j < i; j++) { + b.append('1'); + } + + ((Flowable)m.invoke(null, params)).test().assertResult(b.toString()); + + for (int j = 0; j < params.length; j++) { + Object[] params0 = params.clone(); + params0[j] = null; + + try { + m.invoke(null, params0); + fail("Should have thrown @ " + m); + } catch (InvocationTargetException ex) { + assertTrue(ex.toString(), ex.getCause() instanceof NullPointerException); + + if (j < i) { + assertEquals("source" + (j + 1) + " is null", ex.getCause().getMessage()); + } else { + assertEquals("zipper is null", ex.getCause().getMessage()); + } + } + } + } + } + + /** + * Implements all Function types which return a String concatenating their inputs. + */ + @SuppressWarnings("rawtypes") + public enum ArgsToString implements Function, BiFunction, Function3, Function4, Function5, Function6, Function7, Function8, Function9 { + INSTANCE; + + @Override + public Object apply(Object t1, Object t2, Object t3, Object t4, Object t5, Object t6, Object t7, Object t8, + Object t9) throws Exception { + return "" + t1 + t2 + t3 + t4 + t5 + t6 + t7 + t8 + t9; + } + + @Override + public Object apply(Object t1, Object t2, Object t3, Object t4, Object t5, Object t6, Object t7, Object t8) + throws Exception { + return "" + t1 + t2 + t3 + t4 + t5 + t6 + t7 + t8; + } + + @Override + public Object apply(Object t1, Object t2, Object t3, Object t4, Object t5, Object t6, Object t7) + throws Exception { + return "" + t1 + t2 + t3 + t4 + t5 + t6 + t7; + } + + @Override + public Object apply(Object t1, Object t2, Object t3, Object t4, Object t5, Object t6) throws Exception { + return "" + t1 + t2 + t3 + t4 + t5 + t6; + } + + @Override + public Object apply(Object t1, Object t2, Object t3, Object t4, Object t5) throws Exception { + return "" + t1 + t2 + t3 + t4 + t5; + } + + @Override + public Object apply(Object t1, Object t2, Object t3, Object t4) throws Exception { + return "" + t1 + t2 + t3 + t4; + } + + @Override + public Object apply(Object t1, Object t2, Object t3) throws Exception { + return "" + t1 + t2 + t3; + } + + @Override + public Object apply(Object t1, Object t2) throws Exception { + return "" + t1 + t2; + } + + @Override + public Object apply(Object t1) throws Exception { + return "" + t1; + } + } + + @Test + public void zip2DelayError() { + Flowable<Integer> error1 = Flowable.error(new TestException("One")); + Flowable<Integer> source1 = Flowable.range(1, 3).concatWith(error1); + + Flowable<Integer> error2 = Flowable.error(new TestException("Two")); + Flowable<Integer> source2 = Flowable.range(1, 2).concatWith(error2); + + TestSubscriberEx<Object> ts = Flowable.zip(source1, source2, new BiFunction<Integer, Integer, Object>() { + @Override + public Object apply(Integer a, Integer b) throws Exception { + return "" + a + b; + } + }, true) + .to(TestHelper.<Object>testConsumer()) + .assertFailure(CompositeException.class, "11", "22"); + + List<Throwable> errors = TestHelper.compositeList(ts.errors().get(0)); + TestHelper.assertError(errors, 0, TestException.class, "One"); + TestHelper.assertError(errors, 1, TestException.class, "Two"); + assertEquals(2, errors.size()); + } + + @Test + public void zip2DelayErrorPrefetch() { + Flowable<Integer> error1 = Flowable.error(new TestException("One")); + Flowable<Integer> source1 = Flowable.range(1, 3).concatWith(error1); + + Flowable<Integer> error2 = Flowable.error(new TestException("Two")); + Flowable<Integer> source2 = Flowable.range(1, 2).concatWith(error2); + + TestSubscriberEx<Object> ts = Flowable.zip(source1, source2, new BiFunction<Integer, Integer, Object>() { + @Override + public Object apply(Integer a, Integer b) throws Exception { + return "" + a + b; + } + }, true, 1) + .to(TestHelper.<Object>testConsumer()) + .assertFailure(CompositeException.class, "11", "22"); + + List<Throwable> errors = TestHelper.compositeList(ts.errors().get(0)); + TestHelper.assertError(errors, 0, TestException.class, "One"); + TestHelper.assertError(errors, 1, TestException.class, "Two"); + assertEquals(2, errors.size()); + } + + @Test + public void zip2Prefetch() { + Flowable.zip(Flowable.range(1, 9), + Flowable.range(21, 9), + new BiFunction<Integer, Integer, Object>() { + @Override + public Object apply(Integer a, Integer b) throws Exception { + return "" + a + b; + } + }, false, 2 + ) + .takeLast(1) + .test() + .assertResult("929"); + } + + @Test + public void zipArrayEmpty() { + assertSame(Flowable.empty(), Flowable.zipArray(Functions.<Object[]>identity(), false, 16)); + } + + @Test + public void zip2() { + Flowable.zip(Flowable.just(1), + Flowable.just(2), + new BiFunction<Integer, Integer, Object>() { + @Override + public Object apply(Integer a, Integer b) throws Exception { + return "" + a + b; + } + } + ) + .test() + .assertResult("12"); + } + + @Test + public void zip3() { + Flowable.zip(Flowable.just(1), + Flowable.just(2), Flowable.just(3), + new Function3<Integer, Integer, Integer, Object>() { + @Override + public Object apply(Integer a, Integer b, Integer c) throws Exception { + return "" + a + b + c; + } + } + ) + .test() + .assertResult("123"); + } + + @Test + public void zip4() { + Flowable.zip(Flowable.just(1), + Flowable.just(2), Flowable.just(3), + Flowable.just(4), + new Function4<Integer, Integer, Integer, Integer, Object>() { + @Override + public Object apply(Integer a, Integer b, Integer c, Integer d) throws Exception { + return "" + a + b + c + d; + } + } + ) + .test() + .assertResult("1234"); + } + + @Test + public void zip5() { + Flowable.zip(Flowable.just(1), + Flowable.just(2), Flowable.just(3), + Flowable.just(4), Flowable.just(5), + new Function5<Integer, Integer, Integer, Integer, Integer, Object>() { + @Override + public Object apply(Integer a, Integer b, Integer c, Integer d, Integer e) throws Exception { + return "" + a + b + c + d + e; + } + } + ) + .test() + .assertResult("12345"); + } + + @Test + public void zip6() { + Flowable.zip(Flowable.just(1), + Flowable.just(2), Flowable.just(3), + Flowable.just(4), Flowable.just(5), + Flowable.just(6), + new Function6<Integer, Integer, Integer, Integer, Integer, Integer, Object>() { + @Override + public Object apply(Integer a, Integer b, Integer c, Integer d, Integer e, Integer f) throws Exception { + return "" + a + b + c + d + e + f; + } + } + ) + .test() + .assertResult("123456"); + } + + @Test + public void zip7() { + Flowable.zip(Flowable.just(1), + Flowable.just(2), Flowable.just(3), + Flowable.just(4), Flowable.just(5), + Flowable.just(6), Flowable.just(7), + new Function7<Integer, Integer, Integer, Integer, Integer, Integer, Integer, Object>() { + @Override + public Object apply(Integer a, Integer b, Integer c, Integer d, Integer e, Integer f, Integer g) + throws Exception { + return "" + a + b + c + d + e + f + g; + } + } + ) + .test() + .assertResult("1234567"); + } + + @Test + public void zip8() { + Flowable.zip(Flowable.just(1), + Flowable.just(2), Flowable.just(3), + Flowable.just(4), Flowable.just(5), + Flowable.just(6), Flowable.just(7), + Flowable.just(8), + new Function8<Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Object>() { + @Override + public Object apply(Integer a, Integer b, Integer c, Integer d, Integer e, Integer f, Integer g, + Integer h) throws Exception { + return "" + a + b + c + d + e + f + g + h; + } + } + ) + .test() + .assertResult("12345678"); + } + + @Test + public void zip9() { + Flowable.zip(Flowable.just(1), + Flowable.just(2), Flowable.just(3), + Flowable.just(4), Flowable.just(5), + Flowable.just(6), Flowable.just(7), + Flowable.just(8), Flowable.just(9), + new Function9<Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Object>() { + @Override + public Object apply(Integer a, Integer b, Integer c, Integer d, Integer e, Integer f, Integer g, + Integer h, Integer i) throws Exception { + return "" + a + b + c + d + e + f + g + h + i; + } + } + ) + .test() + .assertResult("123456789"); + } + + @Test + public void zipArrayMany() { + @SuppressWarnings("unchecked") + Flowable<Integer>[] arr = new Flowable[10]; + + Arrays.fill(arr, Flowable.just(1)); + + Flowable.zip(Arrays.asList(arr), new Function<Object[], Object>() { + @Override + public Object apply(Object[] a) throws Exception { + return Arrays.toString(a); + } + }) + .test() + .assertResult("[1, 1, 1, 1, 1, 1, 1, 1, 1, 1]"); + } + + @Test + public void dispose() { + TestHelper.checkDisposed(Flowable.zip(Flowable.just(1), Flowable.just(1), new BiFunction<Integer, Integer, Object>() { + @Override + public Object apply(Integer a, Integer b) throws Exception { + return a + b; + } + })); + } + + @Test + public void badRequest() { + TestHelper.assertBadRequestReported(Flowable.zip(Flowable.just(1), Flowable.just(1), new BiFunction<Integer, Integer, Object>() { + @Override + public Object apply(Integer a, Integer b) throws Exception { + return a + b; + } + })); + } + + @Test + public void multiError() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + PublishProcessor<Object> pp = PublishProcessor.create(); + + @SuppressWarnings("rawtypes") + final Subscriber[] sub = { null }; + TestSubscriberEx<Object> ts = Flowable.zip(pp, new Flowable<Object>() { + @Override + protected void subscribeActual(Subscriber<? super Object> s) { + sub[0] = s; + } + }, new BiFunction<Object, Object, Object>() { + @Override + public Object apply(Object a, Object b) throws Exception { + return a; + } + }) + .to(TestHelper.<Object>testConsumer()); + + pp.onError(new TestException("First")); + + ts + .assertFailureAndMessage(TestException.class, "First"); + + sub[0].onError(new TestException("Second")); + + TestHelper.assertUndeliverable(errors, 0, TestException.class, "Second"); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void singleErrorDelayed() { + PublishProcessor<Object> pp1 = PublishProcessor.create(); + PublishProcessor<Object> pp2 = PublishProcessor.create(); + + TestSubscriberEx<Object> ts = Flowable.zip(pp1, pp2, new BiFunction<Object, Object, Object>() { + @Override + public Object apply(Object a, Object b) throws Exception { + return a; + } + }, true) + .to(TestHelper.<Object>testConsumer()); + + pp1.onError(new TestException("First")); + pp2.onComplete(); + + ts + .assertFailureAndMessage(TestException.class, "First"); + } + + @Test + public void singleErrorDelayedBackpressured() { + PublishProcessor<Object> pp1 = PublishProcessor.create(); + PublishProcessor<Object> pp2 = PublishProcessor.create(); + + TestSubscriberEx<Object> ts = Flowable.zip(pp1, pp2, new BiFunction<Object, Object, Object>() { + @Override + public Object apply(Object a, Object b) throws Exception { + return a; + } + }) + .to(TestHelper.<Object>testSubscriber(0L)); + + pp1.onError(new TestException("First")); + pp2.onComplete(); + + ts + .assertFailureAndMessage(TestException.class, "First"); + } + + @Test + public void fusedInputThrows() { + Flowable.zip(Flowable.just(1).map(new Function<Integer, Integer>() { + @Override + public Integer apply(Integer v) throws Exception { + throw new TestException(); + } + }), Flowable.just(2), new BiFunction<Integer, Integer, Integer>() { + @Override + public Integer apply(Integer a, Integer b) throws Exception { + return a + b; + } + }) + .test() + .assertFailure(TestException.class); + } + + @Test + public void fusedInputThrowsDelayError() { + Flowable.zip(Flowable.just(1).map(new Function<Integer, Integer>() { + @Override + public Integer apply(Integer v) throws Exception { + throw new TestException(); + } + }), Flowable.just(2), new BiFunction<Integer, Integer, Integer>() { + @Override + public Integer apply(Integer a, Integer b) throws Exception { + return a + b; + } + }, true) + .test() + .assertFailure(TestException.class); + } + + @Test + public void fusedInputThrowsBackpressured() { + Flowable.zip(Flowable.just(1).map(new Function<Integer, Integer>() { + @Override + public Integer apply(Integer v) throws Exception { + throw new TestException(); + } + }), Flowable.just(2), new BiFunction<Integer, Integer, Integer>() { + @Override + public Integer apply(Integer a, Integer b) throws Exception { + return a + b; + } + }) + .test(0L) + .assertFailure(TestException.class); + } + + @Test + public void fusedInputThrowsDelayErrorBackpressured() { + Flowable.zip(Flowable.just(1).map(new Function<Integer, Integer>() { + @Override + public Integer apply(Integer v) throws Exception { + throw new TestException(); + } + }), Flowable.just(2), new BiFunction<Integer, Integer, Integer>() { + @Override + public Integer apply(Integer a, Integer b) throws Exception { + return a + b; + } + }, true) + .test(0L) + .assertFailure(TestException.class); + } + + @Test + public void noCrossBoundaryFusion() { + for (int i = 0; i < 500; i++) { + TestSubscriber<List<Object>> ts = Flowable.zip( + Flowable.just(1).observeOn(Schedulers.single()).map(new Function<Integer, Object>() { + @Override + public Object apply(Integer v) throws Exception { + return Thread.currentThread().getName().substring(0, 4); + } + }), + Flowable.just(1).observeOn(Schedulers.computation()).map(new Function<Integer, Object>() { + @Override + public Object apply(Integer v) throws Exception { + return Thread.currentThread().getName().substring(0, 4); + } + }), + new BiFunction<Object, Object, List<Object>>() { + @Override + public List<Object> apply(Object t1, Object t2) throws Exception { + return Arrays.asList(t1, t2); + } + } + ) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertValueCount(1); + + List<Object> list = ts.values().get(0); + + assertTrue(list.toString(), list.contains("RxSi")); + assertTrue(list.toString(), list.contains("RxCo")); + } + } + + static final class ThrowingQueueSubscription implements QueueSubscription<Integer>, Publisher<Integer> { + + @Override + public int requestFusion(int mode) { + return mode & SYNC; + } + + @Override + public boolean offer(Integer value) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean offer(Integer v1, Integer v2) { + throw new UnsupportedOperationException(); + } + + @Override + public Integer poll() throws Exception { + throw new TestException(); + } + + @Override + public boolean isEmpty() { + return false; + } + + @Override + public void clear() { + } + + @Override + public void request(long n) { + } + + @Override + public void cancel() { + } + + @Override + public void subscribe(Subscriber<? super Integer> s) { + s.onSubscribe(this); + } + } + + @Test + public void fusedInputThrows2() { + Flowable.zip(new ThrowingQueueSubscription(), Flowable.just(1), new BiFunction<Integer, Integer, Integer>() { + @Override + public Integer apply(Integer a, Integer b) throws Exception { + return a + b; + } + }) + .test() + .assertFailure(TestException.class); + } + + @Test + public void fusedInputThrows2Backpressured() { + Flowable.zip(new ThrowingQueueSubscription(), Flowable.just(1), new BiFunction<Integer, Integer, Integer>() { + @Override + public Integer apply(Integer a, Integer b) throws Exception { + return a + b; + } + }) + .test(0) + .assertFailure(TestException.class); + } + + @Test + public void cancelOnBackpressureBoundary() { + TestSubscriber<Integer> ts = new TestSubscriber<Integer>(1L) { + @Override + public void onNext(Integer t) { + super.onNext(t); + cancel(); + onComplete(); + } + }; + + Flowable.zip(Flowable.range(1, 2), Flowable.range(3, 2), new BiFunction<Integer, Integer, Integer>() { + @Override + public Integer apply(Integer a, Integer b) throws Exception { + return a + b; + } + }) + .subscribe(ts); + + ts.assertResult(4); + } + + @Test + public void firstErrorPreventsSecondSubscription() { + final AtomicInteger counter = new AtomicInteger(); + + List<Flowable<?>> flowableList = new ArrayList<>(); + flowableList.add(Flowable.create(new FlowableOnSubscribe<Object>() { + @Override + public void subscribe(FlowableEmitter<Object> e) + throws Exception { throw new TestException(); } + }, BackpressureStrategy.MISSING)); + flowableList.add(Flowable.create(new FlowableOnSubscribe<Object>() { + @Override + public void subscribe(FlowableEmitter<Object> e) + throws Exception { counter.getAndIncrement(); } + }, BackpressureStrategy.MISSING)); + + Flowable.zip(flowableList, + new Function<Object[], Object>() { + @Override + public Object apply(Object[] a) throws Exception { + return a; + } + }) + .test() + .assertFailure(TestException.class) + ; + + assertEquals(0, counter.get()); + } + + @Test + public void publishersInIterable() { + Publisher<Integer> source = new Publisher<Integer>() { + @Override + public void subscribe(Subscriber<? super Integer> subscriber) { + Flowable.just(1).subscribe(subscriber); + } + }; + + Flowable.zip(Arrays.asList(source, source), new Function<Object[], Integer>() { + @Override + public Integer apply(Object[] t) throws Throwable { + return 2; + } + }) + .test() + .assertResult(2); + } + + @Test + public void fusedInnerPollCrashDelayError() { + Flowable.zip( + Flowable.range(1, 5), + Flowable.just(1) + .<Integer>map(v -> { throw new TestException(); }) + .compose(TestHelper.flowableStripBoundary()), + (a, b) -> a + b, true + ) + .test() + .assertFailure(TestException.class); + } + + @Test + public void fusedInnerPollCrashRequestBoundaryDelayError() { + Flowable.zip( + Flowable.range(1, 5), + Flowable.just(1) + .<Integer>map(v -> { throw new TestException(); }) + .compose(TestHelper.flowableStripBoundary()), + (a, b) -> a + b, true + ) + .test(0L) + .assertFailure(TestException.class); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/NotificationLiteTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/NotificationLiteTest.java new file mode 100644 index 0000000000..2c4b42aa9c --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/flowable/NotificationLiteTest.java @@ -0,0 +1,124 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.flowable; + +import static org.junit.Assert.*; + +import io.reactivex.rxjava3.disposables.Disposable; +import org.junit.Test; + +import io.reactivex.rxjava3.core.RxJavaTest; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.internal.subscriptions.BooleanSubscription; +import io.reactivex.rxjava3.internal.util.NotificationLite; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class NotificationLiteTest extends RxJavaTest { + + @Test + public void complete() { + Object n = NotificationLite.next("Hello"); + Object c = NotificationLite.complete(); + + assertTrue(NotificationLite.isComplete(c)); + assertFalse(NotificationLite.isComplete(n)); + + assertEquals("Hello", NotificationLite.getValue(n)); + } + + @Test + public void valueKind() { + assertSame(1, NotificationLite.next(1)); + } + + @Test + public void soloEnum() { + TestHelper.checkEnum(NotificationLite.class); + } + + @Test + public void errorNotification() { + Object o = NotificationLite.error(new TestException()); + + assertEquals("NotificationLite.Error[io.reactivex.rxjava3.exceptions.TestException]", o.toString()); + + assertTrue(NotificationLite.isError(o)); + assertFalse(NotificationLite.isComplete(o)); + assertFalse(NotificationLite.isDisposable(o)); + assertFalse(NotificationLite.isSubscription(o)); + + assertTrue(NotificationLite.getError(o) instanceof TestException); + } + + @Test + public void completeNotification() { + Object o = NotificationLite.complete(); + Object o2 = NotificationLite.complete(); + + assertSame(o, o2); + + assertFalse(NotificationLite.isError(o)); + assertTrue(NotificationLite.isComplete(o)); + assertFalse(NotificationLite.isDisposable(o)); + assertFalse(NotificationLite.isSubscription(o)); + + assertEquals("NotificationLite.Complete", o.toString()); + + assertTrue(NotificationLite.isComplete(o)); + } + + @Test + public void disposableNotification() { + Object o = NotificationLite.disposable(Disposable.empty()); + + assertEquals("NotificationLite.Disposable[RunnableDisposable(disposed=false, EmptyRunnable)]", o.toString()); + + assertFalse(NotificationLite.isError(o)); + assertFalse(NotificationLite.isComplete(o)); + assertTrue(NotificationLite.isDisposable(o)); + assertFalse(NotificationLite.isSubscription(o)); + + assertNotNull(NotificationLite.getDisposable(o)); + } + + @Test + public void subscriptionNotification() { + Object o = NotificationLite.subscription(new BooleanSubscription()); + + assertEquals("NotificationLite.Subscription[BooleanSubscription(cancelled=false)]", o.toString()); + + assertFalse(NotificationLite.isError(o)); + assertFalse(NotificationLite.isComplete(o)); + assertFalse(NotificationLite.isDisposable(o)); + assertTrue(NotificationLite.isSubscription(o)); + + assertNotNull(NotificationLite.getSubscription(o)); + } + + // TODO this test is no longer relevant as nulls are not allowed and value maps to itself +// @Test +// public void testValueKind() { +// assertTrue(NotificationLite.isNull(NotificationLite.next(null))); +// assertFalse(NotificationLite.isNull(NotificationLite.next(1))); +// assertFalse(NotificationLite.isNull(NotificationLite.error(new TestException()))); +// assertFalse(NotificationLite.isNull(NotificationLite.completed())); +// assertFalse(NotificationLite.isNull(null)); +// +// assertTrue(NotificationLite.isNext(NotificationLite.next(null))); +// assertTrue(NotificationLite.isNext(NotificationLite.next(1))); +// assertFalse(NotificationLite.isNext(NotificationLite.completed())); +// assertFalse(NotificationLite.isNext(null)); +// assertFalse(NotificationLite.isNext(NotificationLite.error(new TestException()))); +// } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/AbstractMaybeWithUpstreamTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/AbstractMaybeWithUpstreamTest.java new file mode 100644 index 0000000000..dca854d3c1 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/AbstractMaybeWithUpstreamTest.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.maybe; + +import static org.junit.Assert.assertSame; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.internal.fuseable.HasUpstreamMaybeSource; + +public class AbstractMaybeWithUpstreamTest extends RxJavaTest { + + @SuppressWarnings("unchecked") + @Test + public void upstream() { + Maybe<Integer> source = Maybe.just(1); + + assertSame(source, ((HasUpstreamMaybeSource<Integer>)source.map(Functions.<Integer>identity())).source()); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeAmbTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeAmbTest.java new file mode 100644 index 0000000000..5475010163 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeAmbTest.java @@ -0,0 +1,263 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.maybe; + +import static org.junit.Assert.*; + +import java.util.*; +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicBoolean; + +import io.reactivex.rxjava3.disposables.Disposable; +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.processors.PublishProcessor; +import io.reactivex.rxjava3.schedulers.Schedulers; +import io.reactivex.rxjava3.subjects.*; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class MaybeAmbTest extends RxJavaTest { + + @Test + public void ambLots() { + List<Maybe<Integer>> ms = new ArrayList<>(); + + for (int i = 0; i < 32; i++) { + ms.add(Maybe.<Integer>never()); + } + + ms.add(Maybe.just(1)); + + Maybe.amb(ms) + .test() + .assertResult(1); + } + + @Test + public void ambFirstDone() { + Maybe.amb(Arrays.asList(Maybe.just(1), Maybe.just(2))) + .test() + .assertResult(1); + } + + @Test + public void dispose() { + PublishProcessor<Integer> pp1 = PublishProcessor.create(); + PublishProcessor<Integer> pp2 = PublishProcessor.create(); + + TestObserver<Integer> to = Maybe.amb(Arrays.asList(pp1.singleElement(), pp2.singleElement())) + .test(); + + assertTrue(pp1.hasSubscribers()); + assertTrue(pp2.hasSubscribers()); + + to.dispose(); + + assertFalse(pp1.hasSubscribers()); + assertFalse(pp2.hasSubscribers()); + } + + @Test + public void innerErrorRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + final PublishProcessor<Integer> pp0 = PublishProcessor.create(); + final PublishProcessor<Integer> pp1 = PublishProcessor.create(); + + final TestObserver<Integer> to = Maybe.amb(Arrays.asList(pp0.singleElement(), pp1.singleElement())) + .test(); + + final TestException ex = new TestException(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + pp0.onError(ex); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + pp1.onError(ex); + } + }; + + TestHelper.race(r1, r2); + + to.assertFailure(TestException.class); + + if (!errors.isEmpty()) { + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } + } finally { + RxJavaPlugins.reset(); + } + } + } + + @Test + public void disposeNoFurtherSignals() { + TestObserver<Integer> to = Maybe.ambArray(new Maybe<Integer>() { + @Override + protected void subscribeActual( + MaybeObserver<? super Integer> observer) { + observer.onSubscribe(Disposable.empty()); + observer.onSuccess(1); + observer.onSuccess(2); + observer.onComplete(); + } + }, Maybe.<Integer>never()) + .test(); + + to.dispose(); + + to.assertResult(1); + } + + @Test + public void noWinnerSuccessDispose() throws Exception { + for (int i = 0; i < TestHelper.RACE_LONG_LOOPS; i++) { + final AtomicBoolean interrupted = new AtomicBoolean(); + final CountDownLatch cdl = new CountDownLatch(1); + + Maybe.ambArray( + Maybe.just(1) + .subscribeOn(Schedulers.single()) + .observeOn(Schedulers.computation()), + Maybe.never() + ) + .subscribe(new Consumer<Object>() { + @Override + public void accept(Object v) throws Exception { + interrupted.set(Thread.currentThread().isInterrupted()); + cdl.countDown(); + } + }); + + assertTrue(cdl.await(500, TimeUnit.SECONDS)); + assertFalse("Interrupted!", interrupted.get()); + } + } + + @Test + public void noWinnerErrorDispose() throws Exception { + final TestException ex = new TestException(); + for (int i = 0; i < TestHelper.RACE_LONG_LOOPS; i++) { + final AtomicBoolean interrupted = new AtomicBoolean(); + final CountDownLatch cdl = new CountDownLatch(1); + + Maybe.ambArray( + Maybe.error(ex) + .subscribeOn(Schedulers.single()) + .observeOn(Schedulers.computation()), + Maybe.never() + ) + .subscribe(Functions.emptyConsumer(), new Consumer<Throwable>() { + @Override + public void accept(Throwable e) throws Exception { + interrupted.set(Thread.currentThread().isInterrupted()); + cdl.countDown(); + } + }); + + assertTrue(cdl.await(500, TimeUnit.SECONDS)); + assertFalse("Interrupted!", interrupted.get()); + } + } + + @Test + public void noWinnerCompleteDispose() throws Exception { + for (int i = 0; i < TestHelper.RACE_LONG_LOOPS; i++) { + final AtomicBoolean interrupted = new AtomicBoolean(); + final CountDownLatch cdl = new CountDownLatch(1); + + Maybe.ambArray( + Maybe.empty() + .subscribeOn(Schedulers.single()) + .observeOn(Schedulers.computation()), + Maybe.never() + ) + .subscribe(Functions.emptyConsumer(), Functions.emptyConsumer(), new Action() { + @Override + public void run() throws Exception { + interrupted.set(Thread.currentThread().isInterrupted()); + cdl.countDown(); + } + }); + + assertTrue(cdl.await(500, TimeUnit.SECONDS)); + assertFalse("Interrupted!", interrupted.get()); + } + } + + @Test + public void nullSourceSuccessRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + List<Throwable> errors = TestHelper.trackPluginErrors(); + + try { + + final Subject<Integer> ps = ReplaySubject.create(); + ps.onNext(1); + + final Maybe<Integer> source = Maybe.ambArray(ps.singleElement(), + Maybe.<Integer>never(), Maybe.<Integer>never(), null); + + Runnable r1 = new Runnable() { + @Override + public void run() { + source.test(); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + ps.onComplete(); + } + }; + + TestHelper.race(r1, r2); + + if (!errors.isEmpty()) { + TestHelper.assertError(errors, 0, NullPointerException.class); + } + } finally { + RxJavaPlugins.reset(); + } + } + } + + @Test + public void maybeSourcesInIterable() { + MaybeSource<Integer> source = new MaybeSource<Integer>() { + @Override + public void subscribe(MaybeObserver<? super Integer> observer) { + Maybe.just(1).subscribe(observer); + } + }; + + Maybe.amb(Arrays.asList(source, source)) + .test() + .assertResult(1); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeBlockingSubscribeTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeBlockingSubscribeTest.java new file mode 100644 index 0000000000..e11548cdc9 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeBlockingSubscribeTest.java @@ -0,0 +1,502 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.maybe; + +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import java.util.concurrent.TimeUnit; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.Maybe; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.schedulers.Schedulers; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class MaybeBlockingSubscribeTest { + + @Test + public void noArgSuccess() { + Maybe.just(1) + .blockingSubscribe(); + } + + @Test + public void noArgSuccessAsync() { + Maybe.just(1) + .delay(100, TimeUnit.MILLISECONDS) + .blockingSubscribe(); + } + + @Test + public void noArgEmpty() { + Maybe.empty() + .blockingSubscribe(); + } + + @Test + public void noArgEmptyAsync() { + Maybe.empty() + .delay(100, TimeUnit.MILLISECONDS) + .blockingSubscribe(); + } + + @Test + public void noArgError() throws Throwable { + TestHelper.withErrorTracking(errors -> { + Maybe.error(new TestException()) + .blockingSubscribe(); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + }); + } + + @Test + public void noArgErrorAsync() throws Throwable { + TestHelper.withErrorTracking(errors -> { + Maybe.error(new TestException()) + .delay(100, TimeUnit.MILLISECONDS, Schedulers.computation()) + .blockingSubscribe(); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + }); + } + + @Test + public void oneArgSuccess() throws Throwable { + @SuppressWarnings("unchecked") + Consumer<Integer> success = mock(Consumer.class); + + Maybe.just(1) + .blockingSubscribe(success); + + verify(success).accept(1); + } + + @Test + public void oneArgSuccessAsync() throws Throwable { + @SuppressWarnings("unchecked") + Consumer<Integer> success = mock(Consumer.class); + + Maybe.just(1) + .delay(50, TimeUnit.MILLISECONDS) + .blockingSubscribe(success); + + verify(success).accept(1); + } + + @Test + public void oneArgEmpty() throws Throwable { + @SuppressWarnings("unchecked") + Consumer<Integer> success = mock(Consumer.class); + + Maybe.<Integer>empty() + .blockingSubscribe(success); + + verify(success, never()).accept(any()); + } + + @Test + public void oneArgEmptyAsync() throws Throwable { + @SuppressWarnings("unchecked") + Consumer<Integer> success = mock(Consumer.class); + + Maybe.<Integer>empty() + .delay(50, TimeUnit.MILLISECONDS) + .blockingSubscribe(success); + + verify(success, never()).accept(any()); + } + + @Test + public void oneArgSuccessFails() throws Throwable { + TestHelper.withErrorTracking(errors -> { + @SuppressWarnings("unchecked") + Consumer<Integer> success = mock(Consumer.class); + doThrow(new TestException()).when(success).accept(any()); + + Maybe.just(1) + .blockingSubscribe(success); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + + verify(success).accept(1); + }); + } + + @Test + public void oneArgError() throws Throwable { + TestHelper.withErrorTracking(errors -> { + @SuppressWarnings("unchecked") + Consumer<Integer> success = mock(Consumer.class); + + Maybe.<Integer>error(new TestException()) + .blockingSubscribe(success); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + + verify(success, never()).accept(any()); + }); + } + + @Test + public void oneArgErrorAsync() throws Throwable { + TestHelper.withErrorTracking(errors -> { + @SuppressWarnings("unchecked") + Consumer<Integer> success = mock(Consumer.class); + + Maybe.<Integer>error(new TestException()) + .delay(50, TimeUnit.MILLISECONDS, Schedulers.computation()) + .blockingSubscribe(success); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + + verify(success, never()).accept(any()); + }); + } + + @Test + public void twoArgSuccess() throws Throwable { + @SuppressWarnings("unchecked") + Consumer<Integer> success = mock(Consumer.class); + @SuppressWarnings("unchecked") + Consumer<? super Throwable> consumer = mock(Consumer.class); + + Maybe.just(1) + .blockingSubscribe(success, consumer); + + verify(success).accept(1); + verify(consumer, never()).accept(any()); + } + + @Test + public void twoArgSuccessAsync() throws Throwable { + @SuppressWarnings("unchecked") + Consumer<Integer> success = mock(Consumer.class); + @SuppressWarnings("unchecked") + Consumer<? super Throwable> consumer = mock(Consumer.class); + + Maybe.just(1) + .delay(50, TimeUnit.MILLISECONDS) + .blockingSubscribe(success, consumer); + + verify(success).accept(any()); + verify(consumer, never()).accept(any()); + } + + @Test + public void twoArgEmpty() throws Throwable { + @SuppressWarnings("unchecked") + Consumer<Integer> success = mock(Consumer.class); + @SuppressWarnings("unchecked") + Consumer<? super Throwable> consumer = mock(Consumer.class); + + Maybe.<Integer>empty() + .blockingSubscribe(success, consumer); + + verify(success, never()).accept(any()); + verify(consumer, never()).accept(any()); + } + + @Test + public void twoArgEmptyAsync() throws Throwable { + @SuppressWarnings("unchecked") + Consumer<Integer> success = mock(Consumer.class); + @SuppressWarnings("unchecked") + Consumer<? super Throwable> consumer = mock(Consumer.class); + + Maybe.<Integer>empty() + .delay(50, TimeUnit.MILLISECONDS) + .blockingSubscribe(success, consumer); + + verify(success, never()).accept(any()); + verify(consumer, never()).accept(any()); + } + + @Test + public void twoArgSuccessFails() throws Throwable { + TestHelper.withErrorTracking(errors -> { + @SuppressWarnings("unchecked") + Consumer<Integer> success = mock(Consumer.class); + doThrow(new TestException()).when(success).accept(any()); + @SuppressWarnings("unchecked") + Consumer<? super Throwable> consumer = mock(Consumer.class); + + Maybe.just(1) + .blockingSubscribe(success, consumer); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + + verify(success).accept(any()); + verify(consumer, never()).accept(any()); + }); + } + + @Test + public void twoArgError() throws Throwable { + TestHelper.withErrorTracking(errors -> { + @SuppressWarnings("unchecked") + Consumer<Integer> success = mock(Consumer.class); + @SuppressWarnings("unchecked") + Consumer<? super Throwable> consumer = mock(Consumer.class); + + Maybe.<Integer>error(new TestException()) + .blockingSubscribe(success, consumer); + + assertTrue("" + errors, errors.isEmpty()); + + verify(success, never()).accept(any()); + verify(consumer).accept(any(TestException.class)); + }); + } + + @Test + public void twoArgErrorAsync() throws Throwable { + TestHelper.withErrorTracking(errors -> { + @SuppressWarnings("unchecked") + Consumer<Integer> success = mock(Consumer.class); + @SuppressWarnings("unchecked") + Consumer<? super Throwable> consumer = mock(Consumer.class); + + Maybe.<Integer>error(new TestException()) + .delay(50, TimeUnit.MILLISECONDS, Schedulers.computation()) + .blockingSubscribe(success, consumer); + + assertTrue("" + errors, errors.isEmpty()); + + verify(success, never()).accept(any()); + verify(consumer).accept(any(TestException.class)); + }); + } + + @Test + public void twoArgErrorFails() throws Throwable { + TestHelper.withErrorTracking(errors -> { + @SuppressWarnings("unchecked") + Consumer<Integer> success = mock(Consumer.class); + @SuppressWarnings("unchecked") + Consumer<? super Throwable> consumer = mock(Consumer.class); + doThrow(new TestException()).when(consumer).accept(any()); + + Maybe.<Integer>error(new TestException()) + .delay(50, TimeUnit.MILLISECONDS, Schedulers.computation()) + .blockingSubscribe(success, consumer); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + + verify(success, never()).accept(any()); + verify(consumer).accept(any(TestException.class)); + }); + } + + @Test + public void threeArgSuccess() throws Throwable { + @SuppressWarnings("unchecked") + Consumer<Integer> success = mock(Consumer.class); + @SuppressWarnings("unchecked") + Consumer<? super Throwable> consumer = mock(Consumer.class); + Action action = mock(Action.class); + + Maybe.just(1) + .blockingSubscribe(success, consumer, action); + + verify(success).accept(any()); + verify(consumer, never()).accept(any(Throwable.class)); + verify(action, never()).run(); + } + + @Test + public void threeArgEmpty() throws Throwable { + @SuppressWarnings("unchecked") + Consumer<Integer> success = mock(Consumer.class); + @SuppressWarnings("unchecked") + Consumer<? super Throwable> consumer = mock(Consumer.class); + Action action = mock(Action.class); + + Maybe.<Integer>empty() + .blockingSubscribe(success, consumer, action); + + verify(success, never()).accept(any()); + verify(consumer, never()).accept(any(Throwable.class)); + verify(action).run(); + } + + @Test + public void threeArgError() throws Throwable { + @SuppressWarnings("unchecked") + Consumer<Integer> success = mock(Consumer.class); + @SuppressWarnings("unchecked") + Consumer<? super Throwable> consumer = mock(Consumer.class); + Action action = mock(Action.class); + + Maybe.<Integer>error(new TestException()) + .blockingSubscribe(success, consumer, action); + + verify(success, never()).accept(any()); + verify(consumer).accept(any(TestException.class)); + verify(action, never()).run(); + } + + @Test + public void threeArgEmptyFails() throws Throwable { + TestHelper.withErrorTracking(errors -> { + @SuppressWarnings("unchecked") + Consumer<Integer> success = mock(Consumer.class); + @SuppressWarnings("unchecked") + Consumer<? super Throwable> consumer = mock(Consumer.class); + + Action action = mock(Action.class); + doThrow(new TestException()).when(action).run(); + + Maybe.<Integer>empty() + .delay(50, TimeUnit.MILLISECONDS, Schedulers.computation()) + .blockingSubscribe(success, consumer, action); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + + verify(success, never()).accept(any()); + verify(consumer, never()).accept(any()); + verify(action).run(); + }); + } + + @Test + public void threeArgInterrupted() throws Throwable { + TestHelper.withErrorTracking(errors -> { + Action onDispose = mock(Action.class); + + @SuppressWarnings("unchecked") + Consumer<Integer> success = mock(Consumer.class); + @SuppressWarnings("unchecked") + Consumer<? super Throwable> consumer = mock(Consumer.class); + Action action = mock(Action.class); + + Thread.currentThread().interrupt(); + + Maybe.<Integer>never() + .doOnDispose(onDispose) + .blockingSubscribe(success, consumer, action); + + assertTrue("" + errors, errors.isEmpty()); + + verify(onDispose).run(); + verify(success, never()).accept(any()); + verify(action, never()).run(); + verify(consumer).accept(any(InterruptedException.class)); + }); + } + + @Test + public void observerSuccess() { + TestObserver<Integer> to = new TestObserver<>(); + + Maybe.just(1) + .blockingSubscribe(to); + + to.assertResult(1); + } + + @Test + public void observerSuccessAsync() { + TestObserver<Integer> to = new TestObserver<>(); + + Maybe.just(1) + .delay(50, TimeUnit.MILLISECONDS, Schedulers.computation()) + .blockingSubscribe(to); + + to.assertResult(1); + } + + @Test + public void observerEmpty() { + TestObserver<Integer> to = new TestObserver<>(); + + Maybe.<Integer>empty() + .blockingSubscribe(to); + + to.assertResult(); + } + + @Test + public void observerEmptyAsync() { + TestObserver<Integer> to = new TestObserver<>(); + + Maybe.<Integer>empty() + .delay(50, TimeUnit.MILLISECONDS, Schedulers.computation()) + .blockingSubscribe(to); + + to.assertResult(); + } + + @Test + public void observerError() { + TestObserver<Object> to = new TestObserver<>(); + + Maybe.error(new TestException()) + .blockingSubscribe(to); + + to.assertFailure(TestException.class); + } + + @Test + public void observerErrorAsync() { + TestObserver<Object> to = new TestObserver<>(); + + Maybe.error(new TestException()) + .delay(50, TimeUnit.MILLISECONDS, Schedulers.computation()) + .blockingSubscribe(to); + + to.assertFailure(TestException.class); + } + + @Test + public void observerDispose() throws Throwable { + Action onDispose = mock(Action.class); + + TestObserver<Object> to = new TestObserver<>(); + to.dispose(); + + Maybe.never() + .doOnDispose(onDispose) + .blockingSubscribe(to); + + to.assertEmpty(); + + verify(onDispose).run(); + } + + @Test + public void ovserverInterrupted() throws Throwable { + TestHelper.withErrorTracking(errors -> { + Action onDispose = mock(Action.class); + + TestObserver<Object> to = new TestObserver<>(); + + Thread.currentThread().interrupt(); + + Maybe.never() + .doOnDispose(onDispose) + .blockingSubscribe(to); + + assertTrue("" + errors, errors.isEmpty()); + + verify(onDispose).run(); + to.assertFailure(InterruptedException.class); + }); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeCacheTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeCacheTest.java new file mode 100644 index 0000000000..80f61f5931 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeCacheTest.java @@ -0,0 +1,305 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.maybe; + +import static org.junit.Assert.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.processors.PublishProcessor; +import io.reactivex.rxjava3.subscribers.TestSubscriber; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class MaybeCacheTest extends RxJavaTest { + + @Test + public void offlineSuccess() { + Maybe<Integer> source = Maybe.just(1).cache(); + assertEquals(1, source.blockingGet().intValue()); + + source.test() + .assertResult(1); + } + + @Test + public void offlineError() { + Maybe<Integer> source = Maybe.<Integer>error(new TestException()).cache(); + + try { + source.blockingGet(); + fail("Should have thrown"); + } catch (TestException ex) { + // expected + } + + source.test() + .assertFailure(TestException.class); + } + + @Test + public void offlineComplete() { + Maybe<Integer> source = Maybe.<Integer>empty().cache(); + + assertNull(source.blockingGet()); + + source.test() + .assertResult(); + } + + @Test + public void onlineSuccess() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + + Maybe<Integer> source = pp.singleElement().cache(); + + assertFalse(pp.hasSubscribers()); + + assertNotNull(((MaybeCache<Integer>)source).source.get()); + + TestObserver<Integer> to = source.test(); + + assertNull(((MaybeCache<Integer>)source).source.get()); + + assertTrue(pp.hasSubscribers()); + + source.test(true).assertEmpty(); + + to.assertEmpty(); + + pp.onNext(1); + pp.onComplete(); + + to.assertResult(1); + + source.test().assertResult(1); + + source.test(true).assertEmpty(); + } + + @Test + public void onlineError() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + + Maybe<Integer> source = pp.singleElement().cache(); + + assertFalse(pp.hasSubscribers()); + + assertNotNull(((MaybeCache<Integer>)source).source.get()); + + TestObserver<Integer> to = source.test(); + + assertNull(((MaybeCache<Integer>)source).source.get()); + + assertTrue(pp.hasSubscribers()); + + source.test(true).assertEmpty(); + + to.assertEmpty(); + + pp.onError(new TestException()); + + to.assertFailure(TestException.class); + + source.test().assertFailure(TestException.class); + + source.test(true).assertEmpty(); + } + + @Test + public void onlineComplete() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + + Maybe<Integer> source = pp.singleElement().cache(); + + assertFalse(pp.hasSubscribers()); + + assertNotNull(((MaybeCache<Integer>)source).source.get()); + + TestObserver<Integer> to = source.test(); + + assertNull(((MaybeCache<Integer>)source).source.get()); + + assertTrue(pp.hasSubscribers()); + + source.test(true).assertEmpty(); + + to.assertEmpty(); + + pp.onComplete(); + + to.assertResult(); + + source.test().assertResult(); + + source.test(true).assertEmpty(); + } + + @Test + public void crossCancelOnSuccess() { + + final TestSubscriber<Integer> ts = new TestSubscriber<>(); + + PublishProcessor<Integer> pp = PublishProcessor.create(); + + Maybe<Integer> source = pp.singleElement().cache(); + + source.subscribe(new Consumer<Integer>() { + @Override + public void accept(Integer v) throws Exception { + ts.cancel(); + } + }); + + source.toFlowable().subscribe(ts); + + pp.onNext(1); + pp.onComplete(); + + ts.assertEmpty(); + } + + @Test + public void crossCancelOnError() { + + final TestSubscriber<Integer> ts = new TestSubscriber<>(); + + PublishProcessor<Integer> pp = PublishProcessor.create(); + + Maybe<Integer> source = pp.singleElement().cache(); + + source.subscribe(Functions.emptyConsumer(), new Consumer<Object>() { + @Override + public void accept(Object v) throws Exception { + ts.cancel(); + } + }); + + source.toFlowable().subscribe(ts); + + pp.onError(new TestException()); + + ts.assertEmpty(); + } + + @Test + public void crossCancelOnComplete() { + + final TestSubscriber<Integer> ts = new TestSubscriber<>(); + + PublishProcessor<Integer> pp = PublishProcessor.create(); + + Maybe<Integer> source = pp.singleElement().cache(); + + source.subscribe(Functions.emptyConsumer(), Functions.emptyConsumer(), new Action() { + @Override + public void run() throws Exception { + ts.cancel(); + } + }); + + source.toFlowable().subscribe(ts); + + pp.onComplete(); + + ts.assertEmpty(); + } + + @Test + public void addAddRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + PublishProcessor<Integer> pp = PublishProcessor.create(); + + final Maybe<Integer> source = pp.singleElement().cache(); + + Runnable r = new Runnable() { + @Override + public void run() { + source.test(); + } + }; + + TestHelper.race(r, r); + } + } + + @Test + public void removeRemoveRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + PublishProcessor<Integer> pp = PublishProcessor.create(); + + final Maybe<Integer> source = pp.singleElement().cache(); + + final TestObserver<Integer> to1 = source.test(); + final TestObserver<Integer> to2 = source.test(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + to1.dispose(); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + to2.dispose(); + } + }; + + TestHelper.race(r1, r2); + } + } + + @Test + public void doubleDispose() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + + final Maybe<Integer> source = pp.singleElement().cache(); + + final Disposable[] dout = { null }; + + source.subscribe(new MaybeObserver<Integer>() { + + @Override + public void onSubscribe(Disposable d) { + dout[0] = d; + } + + @Override + public void onSuccess(Integer value) { + + } + + @Override + public void onError(Throwable e) { + + } + + @Override + public void onComplete() { + + } + + }); + + dout[0].dispose(); + dout[0].dispose(); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeCallbackObserverTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeCallbackObserverTest.java new file mode 100644 index 0000000000..1758a1116b --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeCallbackObserverTest.java @@ -0,0 +1,143 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.maybe; + +import static org.junit.Assert.*; + +import java.util.List; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.RxJavaTest; +import io.reactivex.rxjava3.disposables.*; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class MaybeCallbackObserverTest extends RxJavaTest { + + @Test + public void dispose() { + MaybeCallbackObserver<Object> mo = new MaybeCallbackObserver<>(Functions.emptyConsumer(), Functions.emptyConsumer(), Functions.EMPTY_ACTION); + + Disposable d = Disposable.empty(); + + mo.onSubscribe(d); + + assertFalse(mo.isDisposed()); + + mo.dispose(); + + assertTrue(mo.isDisposed()); + + assertTrue(d.isDisposed()); + } + + @Test + public void onSuccessCrashes() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + MaybeCallbackObserver<Object> mo = new MaybeCallbackObserver<>( + new Consumer<Object>() { + @Override + public void accept(Object v) throws Exception { + throw new TestException(); + } + }, + Functions.emptyConsumer(), + Functions.EMPTY_ACTION); + + mo.onSubscribe(Disposable.empty()); + + mo.onSuccess(1); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void onErrorCrashes() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + MaybeCallbackObserver<Object> mo = new MaybeCallbackObserver<>( + Functions.emptyConsumer(), + new Consumer<Object>() { + @Override + public void accept(Object v) throws Exception { + throw new TestException("Inner"); + } + }, + Functions.EMPTY_ACTION); + + mo.onSubscribe(Disposable.empty()); + + mo.onError(new TestException("Outer")); + + TestHelper.assertError(errors, 0, CompositeException.class); + + List<Throwable> ce = TestHelper.compositeList(errors.get(0)); + + TestHelper.assertError(ce, 0, TestException.class, "Outer"); + TestHelper.assertError(ce, 1, TestException.class, "Inner"); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void onCompleteCrashes() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + MaybeCallbackObserver<Object> mo = new MaybeCallbackObserver<>( + Functions.emptyConsumer(), + Functions.emptyConsumer(), + new Action() { + @Override + public void run() throws Exception { + throw new TestException(); + } + }); + + mo.onSubscribe(Disposable.empty()); + + mo.onComplete(); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void onErrorMissingShouldReportNoCustomOnError() { + MaybeCallbackObserver<Integer> o = new MaybeCallbackObserver<>(Functions.<Integer>emptyConsumer(), + Functions.ON_ERROR_MISSING, + Functions.EMPTY_ACTION); + + assertFalse(o.hasCustomOnError()); + } + + @Test + public void customOnErrorShouldReportCustomOnError() { + MaybeCallbackObserver<Integer> o = new MaybeCallbackObserver<>(Functions.<Integer>emptyConsumer(), + Functions.<Throwable>emptyConsumer(), + Functions.EMPTY_ACTION); + + assertTrue(o.hasCustomOnError()); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeConcatArrayEagerDelayErrorTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeConcatArrayEagerDelayErrorTest.java new file mode 100644 index 0000000000..94e77bf55a --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeConcatArrayEagerDelayErrorTest.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.maybe; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.Maybe; +import io.reactivex.rxjava3.exceptions.TestException; + +public class MaybeConcatArrayEagerDelayErrorTest { + + @Test + public void normal() { + Maybe.concatArrayEagerDelayError( + Maybe.just(1), + Maybe.<Integer>error(new TestException()), + Maybe.empty(), + Maybe.just(2) + ) + .test() + .assertFailure(TestException.class, 1, 2); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeConcatArrayTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeConcatArrayTest.java new file mode 100644 index 0000000000..c2d6fcc0ae --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeConcatArrayTest.java @@ -0,0 +1,267 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.maybe; + +import static org.junit.Assert.assertEquals; + +import java.io.IOException; +import java.util.List; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.subjects.MaybeSubject; +import io.reactivex.rxjava3.subscribers.TestSubscriber; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class MaybeConcatArrayTest extends RxJavaTest { + + @Test + public void cancel() { + Maybe.concatArray(Maybe.just(1), Maybe.just(2)) + .take(1) + .test() + .assertResult(1); + } + + @Test + public void cancelDelayError() { + Maybe.concatArrayDelayError(Maybe.just(1), Maybe.just(2)) + .take(1) + .test() + .assertResult(1); + } + + @Test + public void backpressure() { + TestSubscriber<Integer> ts = Maybe.concatArray(Maybe.just(1), Maybe.just(2)) + .test(0L); + + ts.assertEmpty(); + + ts.request(1); + + ts.assertValue(1); + + ts.request(2); + + ts.assertResult(1, 2); + } + + @Test + public void backpressureDelayError() { + TestSubscriber<Integer> ts = Maybe.concatArrayDelayError(Maybe.just(1), Maybe.just(2)) + .test(0L); + + ts.assertEmpty(); + + ts.request(1); + + ts.assertValue(1); + + ts.request(2); + + ts.assertResult(1, 2); + } + + @Test + public void requestCancelRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final TestSubscriber<Integer> ts = Maybe.concatArray(Maybe.just(1), Maybe.just(2)) + .test(0L); + + Runnable r1 = new Runnable() { + @Override + public void run() { + ts.cancel(); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + ts.request(1); + } + }; + + TestHelper.race(r1, r2); + } + } + + @Test + public void requestCancelRaceDelayError() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final TestSubscriber<Integer> ts = Maybe.concatArrayDelayError(Maybe.just(1), Maybe.just(2)) + .test(0L); + + Runnable r1 = new Runnable() { + @Override + public void run() { + ts.cancel(); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + ts.request(1); + } + }; + + TestHelper.race(r1, r2); + } + } + + @Test + public void errorAfterTermination() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + final MaybeObserver<?>[] o = { null }; + Maybe.concatArrayDelayError(Maybe.just(1), + Maybe.error(new IOException()), + new Maybe<Integer>() { + @Override + protected void subscribeActual(MaybeObserver<? super Integer> observer) { + observer.onSubscribe(Disposable.empty()); + observer.onSuccess(2); + o[0] = observer; + } + }) + .test() + .assertFailure(IOException.class, 1, 2); + + o[0].onError(new TestException()); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void noSubsequentSubscription() { + final int[] calls = { 0 }; + + Maybe<Integer> source = Maybe.create(new MaybeOnSubscribe<Integer>() { + @Override + public void subscribe(MaybeEmitter<Integer> s) throws Exception { + calls[0]++; + s.onSuccess(1); + } + }); + + Maybe.concatArray(source, source).firstElement() + .test() + .assertResult(1); + + assertEquals(1, calls[0]); + } + + @Test + public void noSubsequentSubscriptionDelayError() { + final int[] calls = { 0 }; + + Maybe<Integer> source = Maybe.create(new MaybeOnSubscribe<Integer>() { + @Override + public void subscribe(MaybeEmitter<Integer> s) throws Exception { + calls[0]++; + s.onSuccess(1); + } + }); + + Maybe.concatArrayDelayError(source, source).firstElement() + .test() + .assertResult(1); + + assertEquals(1, calls[0]); + } + + @Test + public void badRequest() { + TestHelper.assertBadRequestReported(Maybe.concatArray(MaybeSubject.create(), MaybeSubject.create())); + } + + @Test + public void badRequestDelayError() { + TestHelper.assertBadRequestReported(Maybe.concatArrayDelayError(MaybeSubject.create(), MaybeSubject.create())); + } + + @Test + public void mixed() { + Maybe.concatArray( + Maybe.just(1), + Maybe.empty(), + Maybe.just(2), + Maybe.empty(), + Maybe.empty() + ) + .test() + .assertResult(1, 2); + } + + @Test + public void requestBeforeSuccess() { + MaybeSubject<Integer> ms = MaybeSubject.create(); + TestSubscriber<Integer> ts = Maybe.concatArray(ms, ms) + .test(); + + ts.assertEmpty(); + + ms.onSuccess(1); + + ts.assertResult(1, 1); + } + + @Test + public void requestBeforeComplete() { + MaybeSubject<Integer> ms = MaybeSubject.create(); + TestSubscriber<Integer> ts = Maybe.concatArray(ms, ms) + .test(); + + ts.assertEmpty(); + + ms.onComplete(); + + ts.assertResult(); + } + + @Test + public void requestBeforeSuccessDelayError() { + MaybeSubject<Integer> ms = MaybeSubject.create(); + TestSubscriber<Integer> ts = Maybe.concatArrayDelayError(ms, ms) + .test(); + + ts.assertEmpty(); + + ms.onSuccess(1); + + ts.assertResult(1, 1); + } + + @Test + public void requestBeforeCompleteDelayError() { + MaybeSubject<Integer> ms = MaybeSubject.create(); + TestSubscriber<Integer> ts = Maybe.concatArrayDelayError(ms, ms) + .test(); + + ts.assertEmpty(); + + ms.onComplete(); + + ts.assertResult(); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeConcatEagerTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeConcatEagerTest.java new file mode 100644 index 0000000000..4c53fcddf3 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeConcatEagerTest.java @@ -0,0 +1,152 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.maybe; + +import java.util.Arrays; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; + +public class MaybeConcatEagerTest { + + @Test + public void iterableNormal() { + Maybe.concatEager(Arrays.asList( + Maybe.just(1), + Maybe.empty(), + Maybe.just(2) + )) + .test() + .assertResult(1, 2); + } + + @Test + public void iterableNormalMaxConcurrency() { + Maybe.concatEager(Arrays.asList( + Maybe.just(1), + Maybe.empty(), + Maybe.just(2) + ), 1) + .test() + .assertResult(1, 2); + } + + @Test + public void iterableError() { + Maybe.concatEager(Arrays.asList( + Maybe.just(1), + Maybe.error(new TestException()), + Maybe.empty(), + Maybe.just(2) + )) + .test() + .assertFailure(TestException.class, 1); + } + + @Test + public void iterableErrorMaxConcurrency() { + Maybe.concatEager(Arrays.asList( + Maybe.just(1), + Maybe.error(new TestException()), + Maybe.empty(), + Maybe.just(2) + ), 1) + .test() + .assertFailure(TestException.class, 1); + } + + @Test + public void publisherNormal() { + Maybe.concatEager(Flowable.fromArray( + Maybe.just(1), + Maybe.empty(), + Maybe.just(2) + )) + .test() + .assertResult(1, 2); + } + + @Test + public void publisherNormalMaxConcurrency() { + Maybe.concatEager(Flowable.fromArray( + Maybe.just(1), + Maybe.empty(), + Maybe.just(2) + ), 1) + .test() + .assertResult(1, 2); + } + + @Test + public void publisherError() { + Maybe.concatEager(Flowable.fromArray( + Maybe.just(1), + Maybe.error(new TestException()), + Maybe.empty(), + Maybe.just(2) + )) + .test() + .assertFailure(TestException.class, 1); + } + + @Test + public void iterableDelayError() { + Maybe.concatEagerDelayError(Arrays.asList( + Maybe.just(1), + Maybe.error(new TestException()), + Maybe.empty(), + Maybe.just(2) + )) + .test() + .assertFailure(TestException.class, 1, 2); + } + + @Test + public void iterableDelayErrorMaxConcurrency() { + Maybe.concatEagerDelayError(Arrays.asList( + Maybe.just(1), + Maybe.error(new TestException()), + Maybe.empty(), + Maybe.just(2) + ), 1) + .test() + .assertFailure(TestException.class, 1, 2); + } + + @Test + public void publisherDelayError() { + Maybe.concatEagerDelayError(Flowable.fromArray( + Maybe.just(1), + Maybe.error(new TestException()), + Maybe.empty(), + Maybe.just(2) + )) + .test() + .assertFailure(TestException.class, 1, 2); + } + + @Test + public void publisherDelayErrorMaxConcurrency() { + Maybe.concatEagerDelayError(Flowable.fromArray( + Maybe.just(1), + Maybe.error(new TestException()), + Maybe.empty(), + Maybe.just(2) + ), 1) + .test() + .assertFailure(TestException.class, 1, 2); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeConcatIterableTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeConcatIterableTest.java new file mode 100644 index 0000000000..c8b9045263 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeConcatIterableTest.java @@ -0,0 +1,170 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.maybe; + +import static org.junit.Assert.assertEquals; + +import java.util.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.util.CrashingMappedIterable; +import io.reactivex.rxjava3.processors.PublishProcessor; +import io.reactivex.rxjava3.subjects.MaybeSubject; +import io.reactivex.rxjava3.subscribers.TestSubscriber; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class MaybeConcatIterableTest extends RxJavaTest { + + @Test + public void take() { + Maybe.concat(Arrays.asList(Maybe.just(1), Maybe.just(2), Maybe.just(3))) + .take(1) + .test() + .assertResult(1); + } + + @Test + public void iteratorThrows() { + Maybe.concat(new Iterable<MaybeSource<Object>>() { + @Override + public Iterator<MaybeSource<Object>> iterator() { + throw new TestException("iterator()"); + } + }) + .to(TestHelper.<Object>testConsumer()) + .assertFailureAndMessage(TestException.class, "iterator()"); + } + + @Test + public void error() { + Maybe.concat(Arrays.asList(Maybe.just(1), Maybe.<Integer>error(new TestException()), Maybe.just(3))) + .test() + .assertFailure(TestException.class, 1); + } + + @Test + public void successCancelRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + + final PublishProcessor<Integer> pp = PublishProcessor.create(); + + final TestSubscriber<Integer> ts = Maybe.concat(Arrays.asList(pp.singleElement())) + .test(); + + pp.onNext(1); + + Runnable r1 = new Runnable() { + @Override + public void run() { + ts.cancel(); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + pp.onComplete(); + } + }; + + TestHelper.race(r1, r2); + } + } + + @Test + public void hasNextThrows() { + Maybe.concat(new CrashingMappedIterable<>(100, 1, 100, new Function<Integer, Maybe<Integer>>() { + @Override + public Maybe<Integer> apply(Integer v) throws Exception { + return Maybe.just(1); + } + })) + .to(TestHelper.<Integer>testConsumer()) + .assertFailureAndMessage(TestException.class, "hasNext()"); + } + + @Test + public void nextThrows() { + Maybe.concat(new CrashingMappedIterable<>(100, 100, 1, new Function<Integer, Maybe<Integer>>() { + @Override + public Maybe<Integer> apply(Integer v) throws Exception { + return Maybe.just(1); + } + })) + .to(TestHelper.<Integer>testConsumer()) + .assertFailureAndMessage(TestException.class, "next()"); + } + + @Test + public void nextReturnsNull() { + Maybe.concat(new CrashingMappedIterable<>(100, 100, 100, new Function<Integer, Maybe<Integer>>() { + @Override + public Maybe<Integer> apply(Integer v) throws Exception { + return null; + } + })) + .test() + .assertFailure(NullPointerException.class); + } + + @Test + public void noSubsequentSubscription() { + final int[] calls = { 0 }; + + Maybe<Integer> source = Maybe.create(new MaybeOnSubscribe<Integer>() { + @Override + public void subscribe(MaybeEmitter<Integer> s) throws Exception { + calls[0]++; + s.onSuccess(1); + } + }); + + Maybe.concat(Arrays.asList(source, source)).firstElement() + .test() + .assertResult(1); + + assertEquals(1, calls[0]); + } + + @Test + public void noSubsequentSubscriptionDelayError() { + final int[] calls = { 0 }; + + Maybe<Integer> source = Maybe.create(new MaybeOnSubscribe<Integer>() { + @Override + public void subscribe(MaybeEmitter<Integer> s) throws Exception { + calls[0]++; + s.onSuccess(1); + } + }); + + Maybe.concatDelayError(Arrays.asList(source, source)).firstElement() + .test() + .assertResult(1); + + assertEquals(1, calls[0]); + } + + @Test + public void badRequest() { + TestHelper.assertBadRequestReported(Maybe.concat(Arrays.asList( + MaybeSubject.create(), + MaybeSubject.create() + ))); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeConcatMapCompletableTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeConcatMapCompletableTest.java new file mode 100644 index 0000000000..dedd03a02e --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeConcatMapCompletableTest.java @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.maybe; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class MaybeConcatMapCompletableTest extends RxJavaTest { + + @Test + public void dispose() { + TestHelper.checkDisposed(Maybe.just(1).concatMapCompletable(new Function<Integer, Completable>() { + @Override + public Completable apply(Integer v) throws Exception { + return Completable.complete(); + } + })); + } + + @Test + public void mapperThrows() { + Maybe.just(1) + .concatMapCompletable(new Function<Integer, Completable>() { + @Override + public Completable apply(Integer v) throws Exception { + throw new TestException(); + } + }) + .test() + .assertFailure(TestException.class); + } + + @Test + public void mapperReturnsNull() { + Maybe.just(1) + .concatMapCompletable(new Function<Integer, Completable>() { + @Override + public Completable apply(Integer v) throws Exception { + return null; + } + }) + .test() + .assertFailure(NullPointerException.class); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeConcatMapSingleTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeConcatMapSingleTest.java new file mode 100644 index 0000000000..93695b5293 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeConcatMapSingleTest.java @@ -0,0 +1,142 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.maybe; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class MaybeConcatMapSingleTest extends RxJavaTest { + @Test + public void flatMapSingleElementValue() { + Maybe.just(1).concatMapSingle(new Function<Integer, SingleSource<Integer>>() { + @Override public SingleSource<Integer> apply(final Integer integer) throws Exception { + if (integer == 1) { + return Single.just(2); + } + + return Single.just(1); + } + }) + .test() + .assertResult(2); + } + + @Test + public void flatMapSingleElementValueDifferentType() { + Maybe.just(1).concatMapSingle(new Function<Integer, SingleSource<String>>() { + @Override public SingleSource<String> apply(final Integer integer) throws Exception { + if (integer == 1) { + return Single.just("2"); + } + + return Single.just("1"); + } + }) + .test() + .assertResult("2"); + } + + @Test + public void flatMapSingleElementValueNull() { + Maybe.just(1).concatMapSingle(new Function<Integer, SingleSource<Integer>>() { + @Override public SingleSource<Integer> apply(final Integer integer) throws Exception { + return null; + } + }) + .to(TestHelper.<Integer>testConsumer()) + .assertNoValues() + .assertError(NullPointerException.class) + .assertErrorMessage("The mapper returned a null SingleSource"); + } + + @Test + public void flatMapSingleElementValueErrorThrown() { + Maybe.just(1).concatMapSingle(new Function<Integer, SingleSource<Integer>>() { + @Override public SingleSource<Integer> apply(final Integer integer) throws Exception { + throw new RuntimeException("something went terribly wrong!"); + } + }) + .to(TestHelper.<Integer>testConsumer()) + .assertNoValues() + .assertError(RuntimeException.class) + .assertErrorMessage("something went terribly wrong!"); + } + + @Test + public void flatMapSingleElementError() { + RuntimeException exception = new RuntimeException("test"); + + Maybe.error(exception).concatMapSingle(new Function<Object, SingleSource<Object>>() { + @Override public SingleSource<Object> apply(final Object integer) throws Exception { + return Single.just(new Object()); + } + }) + .test() + .assertError(exception); + } + + @Test + public void flatMapSingleElementEmpty() { + Maybe.<Integer>empty().concatMapSingle(new Function<Integer, SingleSource<Integer>>() { + @Override public SingleSource<Integer> apply(final Integer integer) throws Exception { + return Single.just(2); + } + }) + .test() + .assertNoValues() + .assertResult(); + } + + @Test + public void dispose() { + TestHelper.checkDisposed(Maybe.just(1).concatMapSingle(new Function<Integer, SingleSource<Integer>>() { + @Override + public SingleSource<Integer> apply(final Integer integer) throws Exception { + return Single.just(2); + } + })); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeMaybe(new Function<Maybe<Integer>, Maybe<Integer>>() { + @Override + public Maybe<Integer> apply(Maybe<Integer> m) throws Exception { + return m.concatMapSingle(new Function<Integer, SingleSource<Integer>>() { + @Override + public SingleSource<Integer> apply(final Integer integer) throws Exception { + return Single.just(2); + } + }); + } + }); + } + + @Test + public void singleErrors() { + Maybe.just(1) + .concatMapSingle(new Function<Integer, SingleSource<Integer>>() { + @Override + public SingleSource<Integer> apply(final Integer integer) throws Exception { + return Single.error(new TestException()); + } + }) + .test() + .assertFailure(TestException.class); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeConcatMapTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeConcatMapTest.java new file mode 100644 index 0000000000..04e5abfafd --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeConcatMapTest.java @@ -0,0 +1,101 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.maybe; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class MaybeConcatMapTest extends RxJavaTest { + + @Test + public void dispose() { + TestHelper.checkDisposed(Maybe.just(1).concatMap(new Function<Integer, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Integer v) throws Exception { + return Maybe.just(2); + } + })); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeMaybe(new Function<Maybe<Integer>, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Maybe<Integer> v) throws Exception { + return v.concatMap(new Function<Integer, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Integer v) throws Exception { + return Maybe.just(2); + } + }); + } + }); + } + + @Test + public void mainError() { + Maybe.<Integer>error(new TestException()) + .concatMap(new Function<Integer, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Integer v) throws Exception { + return Maybe.just(2); + } + }) + .test() + .assertFailure(TestException.class); + } + + @Test + public void mainEmpty() { + Maybe.<Integer>empty() + .concatMap(new Function<Integer, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Integer v) throws Exception { + return Maybe.just(2); + } + }) + .test() + .assertResult(); + } + + @Test + public void mapperThrows() { + Maybe.just(1) + .concatMap(new Function<Integer, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Integer v) throws Exception { + throw new TestException(); + } + }) + .test() + .assertFailure(TestException.class); + } + + @Test + public void mapperReturnsNull() { + Maybe.just(1) + .concatMap(new Function<Integer, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Integer v) throws Exception { + return null; + } + }) + .test() + .assertFailure(NullPointerException.class); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeConcatPublisherTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeConcatPublisherTest.java new file mode 100644 index 0000000000..ad334cac7b --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeConcatPublisherTest.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.maybe; + +import java.util.concurrent.Callable; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; + +public class MaybeConcatPublisherTest extends RxJavaTest { + + @Test + public void scalar() { + Maybe.concat(Flowable.just(Maybe.just(1))) + .test() + .assertResult(1); + } + + @Test + public void callable() { + Maybe.concat(Flowable.fromCallable(new Callable<Maybe<Integer>>() { + @Override + public Maybe<Integer> call() throws Exception { + return Maybe.just(1); + } + })) + .test() + .assertResult(1); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeContainsTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeContainsTest.java new file mode 100644 index 0000000000..ca019517d8 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeContainsTest.java @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.maybe; + +import static org.junit.Assert.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.fuseable.HasUpstreamMaybeSource; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.processors.PublishProcessor; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class MaybeContainsTest extends RxJavaTest { + + @Test + public void doesContain() { + Maybe.just(1).contains(1).test().assertResult(true); + } + + @Test + public void doesntContain() { + Maybe.just(1).contains(2).test().assertResult(false); + } + + @Test + public void empty() { + Maybe.empty().contains(2).test().assertResult(false); + } + + @Test + public void error() { + Maybe.error(new TestException()).contains(2).test().assertFailure(TestException.class); + } + + @Test + public void dispose() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + + TestObserver<Boolean> to = pp.singleElement().contains(1).test(); + + assertTrue(pp.hasSubscribers()); + + to.dispose(); + + assertFalse(pp.hasSubscribers()); + } + + @Test + public void isDisposed() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + + TestHelper.checkDisposed(pp.singleElement().contains(1)); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeMaybeToSingle(new Function<Maybe<Object>, SingleSource<Boolean>>() { + @Override + public SingleSource<Boolean> apply(Maybe<Object> f) throws Exception { + return f.contains(1); + } + }); + } + + @SuppressWarnings("unchecked") + @Test + public void hasSource() { + assertSame(Maybe.empty(), ((HasUpstreamMaybeSource<Object>)(Maybe.empty().contains(0))).source()); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeCountTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeCountTest.java new file mode 100644 index 0000000000..b223358ef2 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeCountTest.java @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.maybe; + +import static org.junit.Assert.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.fuseable.HasUpstreamMaybeSource; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.processors.PublishProcessor; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class MaybeCountTest extends RxJavaTest { + + @Test + public void one() { + Maybe.just(1).count().test().assertResult(1L); + } + + @Test + public void empty() { + Maybe.empty().count().test().assertResult(0L); + } + + @Test + public void error() { + Maybe.error(new TestException()).count().test().assertFailure(TestException.class); + } + + @Test + public void dispose() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + + TestObserver<Long> to = pp.singleElement().count().test(); + + assertTrue(pp.hasSubscribers()); + + to.dispose(); + + assertFalse(pp.hasSubscribers()); + } + + @Test + public void isDisposed() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + + TestHelper.checkDisposed(pp.singleElement().count()); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeMaybeToSingle(new Function<Maybe<Object>, SingleSource<Long>>() { + @Override + public SingleSource<Long> apply(Maybe<Object> f) throws Exception { + return f.count(); + } + }); + } + + @SuppressWarnings("unchecked") + @Test + public void hasSource() { + assertSame(Maybe.empty(), ((HasUpstreamMaybeSource<Object>)(Maybe.empty().count())).source()); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeCreateTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeCreateTest.java new file mode 100644 index 0000000000..6bd42bce44 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeCreateTest.java @@ -0,0 +1,349 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.maybe; + +import static org.junit.Assert.*; + +import java.io.IOException; +import java.util.List; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class MaybeCreateTest extends RxJavaTest { + + @Test + public void callbackThrows() { + Maybe.create(new MaybeOnSubscribe<Object>() { + @Override + public void subscribe(MaybeEmitter<Object> e) throws Exception { + throw new TestException(); + } + }) + .test() + .assertFailure(TestException.class); + } + + @Test + public void onSuccessNull() { + Maybe.create(new MaybeOnSubscribe<Object>() { + @Override + public void subscribe(MaybeEmitter<Object> e) throws Exception { + e.onSuccess(null); + } + }) + .test() + .assertFailure(NullPointerException.class); + } + + @Test + public void onErrorNull() { + Maybe.create(new MaybeOnSubscribe<Object>() { + @Override + public void subscribe(MaybeEmitter<Object> e) throws Exception { + e.onError(null); + } + }) + .test() + .assertFailure(NullPointerException.class); + } + + @Test + public void dispose() { + TestHelper.checkDisposed(Maybe.create(new MaybeOnSubscribe<Object>() { + @Override + public void subscribe(MaybeEmitter<Object> e) throws Exception { + e.onSuccess(1); + } + })); + } + + @Test + public void onSuccessThrows() { + Maybe.create(new MaybeOnSubscribe<Object>() { + @Override + public void subscribe(MaybeEmitter<Object> e) throws Exception { + Disposable d = Disposable.empty(); + e.setDisposable(d); + + try { + e.onSuccess(1); + fail("Should have thrown"); + } catch (TestException ex) { + // expected + } + + assertTrue(d.isDisposed()); + assertTrue(e.isDisposed()); + } + }).subscribe(new MaybeObserver<Object>() { + + @Override + public void onSubscribe(Disposable d) { + + } + + @Override + public void onSuccess(Object value) { + throw new TestException(); + } + + @Override + public void onError(Throwable e) { + + } + + @Override + public void onComplete() { + + } + }); + } + + @Test + public void onErrorThrows() { + Maybe.create(new MaybeOnSubscribe<Object>() { + @Override + public void subscribe(MaybeEmitter<Object> e) throws Exception { + Disposable d = Disposable.empty(); + e.setDisposable(d); + + try { + e.onError(new IOException()); + fail("Should have thrown"); + } catch (TestException ex) { + // expected + } + + assertTrue(d.isDisposed()); + assertTrue(e.isDisposed()); + } + }).subscribe(new MaybeObserver<Object>() { + + @Override + public void onSubscribe(Disposable d) { + + } + + @Override + public void onSuccess(Object value) { + + } + + @Override + public void onError(Throwable e) { + throw new TestException(); + } + + @Override + public void onComplete() { + + } + }); + } + + @Test + public void onCompleteThrows() { + Maybe.create(new MaybeOnSubscribe<Object>() { + @Override + public void subscribe(MaybeEmitter<Object> e) throws Exception { + Disposable d = Disposable.empty(); + e.setDisposable(d); + + try { + e.onComplete(); + fail("Should have thrown"); + } catch (TestException ex) { + // expected + } + + assertTrue(d.isDisposed()); + assertTrue(e.isDisposed()); + } + }).subscribe(new MaybeObserver<Object>() { + + @Override + public void onSubscribe(Disposable d) { + + } + + @Override + public void onSuccess(Object value) { + + } + + @Override + public void onError(Throwable e) { + + } + + @Override + public void onComplete() { + throw new TestException(); + } + }); + } + + @Test + public void onSuccessThrows2() { + Maybe.create(new MaybeOnSubscribe<Object>() { + @Override + public void subscribe(MaybeEmitter<Object> e) throws Exception { + try { + e.onSuccess(1); + fail("Should have thrown"); + } catch (TestException ex) { + // expected + } + + assertTrue(e.isDisposed()); + } + }).subscribe(new MaybeObserver<Object>() { + + @Override + public void onSubscribe(Disposable d) { + + } + + @Override + public void onSuccess(Object value) { + throw new TestException(); + } + + @Override + public void onError(Throwable e) { + + } + + @Override + public void onComplete() { + + } + }); + } + + @Test + public void onErrorThrows2() { + Maybe.create(new MaybeOnSubscribe<Object>() { + @Override + public void subscribe(MaybeEmitter<Object> e) throws Exception { + try { + e.onError(new IOException()); + fail("Should have thrown"); + } catch (TestException ex) { + // expected + } + + assertTrue(e.isDisposed()); + } + }).subscribe(new MaybeObserver<Object>() { + + @Override + public void onSubscribe(Disposable d) { + + } + + @Override + public void onSuccess(Object value) { + + } + + @Override + public void onError(Throwable e) { + throw new TestException(); + } + + @Override + public void onComplete() { + + } + }); + } + + @Test + public void onCompleteThrows2() { + Maybe.create(new MaybeOnSubscribe<Object>() { + @Override + public void subscribe(MaybeEmitter<Object> e) throws Exception { + try { + e.onComplete(); + fail("Should have thrown"); + } catch (TestException ex) { + // expected + } + + assertTrue(e.isDisposed()); + } + }).subscribe(new MaybeObserver<Object>() { + + @Override + public void onSubscribe(Disposable d) { + + } + + @Override + public void onSuccess(Object value) { + + } + + @Override + public void onError(Throwable e) { + + } + + @Override + public void onComplete() { + throw new TestException(); + } + }); + } + + @Test + public void tryOnError() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + final Boolean[] response = { null }; + Maybe.create(new MaybeOnSubscribe<Object>() { + @Override + public void subscribe(MaybeEmitter<Object> e) throws Exception { + e.onSuccess(1); + response[0] = e.tryOnError(new TestException()); + } + }) + .test() + .assertResult(1); + + assertFalse(response[0]); + + assertTrue(errors.toString(), errors.isEmpty()); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void emitterHasToString() { + Maybe.create(new MaybeOnSubscribe<Object>() { + @Override + public void subscribe(MaybeEmitter<Object> emitter) throws Exception { + assertTrue(emitter.toString().contains(MaybeCreate.Emitter.class.getSimpleName())); + } + }).test().assertEmpty(); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeDelayOtherTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeDelayOtherTest.java new file mode 100644 index 0000000000..f482834e29 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeDelayOtherTest.java @@ -0,0 +1,244 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.maybe; + +import static org.junit.Assert.*; + +import java.util.List; + +import org.junit.Test; +import org.reactivestreams.Subscriber; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.subscriptions.BooleanSubscription; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.processors.PublishProcessor; +import io.reactivex.rxjava3.testsupport.*; + +public class MaybeDelayOtherTest extends RxJavaTest { + + @Test + public void justWithOnNext() { + PublishProcessor<Object> pp = PublishProcessor.create(); + + TestObserver<Integer> to = Maybe.just(1) + .delay(pp).test(); + + to.assertEmpty(); + + assertTrue(pp.hasSubscribers()); + + pp.onNext(1); + + assertFalse(pp.hasSubscribers()); + + to.assertResult(1); + } + + @Test + public void justWithOnComplete() { + PublishProcessor<Object> pp = PublishProcessor.create(); + + TestObserver<Integer> to = Maybe.just(1) + .delay(pp).test(); + + to.assertEmpty(); + + assertTrue(pp.hasSubscribers()); + + pp.onComplete(); + + assertFalse(pp.hasSubscribers()); + + to.assertResult(1); + } + + @Test + public void justWithOnError() { + PublishProcessor<Object> pp = PublishProcessor.create(); + + TestObserverEx<Integer> to = Maybe.just(1) + .delay(pp).to(TestHelper.<Integer>testConsumer()); + + to.assertEmpty(); + + assertTrue(pp.hasSubscribers()); + + pp.onError(new TestException("Other")); + + assertFalse(pp.hasSubscribers()); + + to.assertFailureAndMessage(TestException.class, "Other"); + } + + @Test + public void emptyWithOnNext() { + PublishProcessor<Object> pp = PublishProcessor.create(); + + TestObserver<Integer> to = Maybe.<Integer>empty() + .delay(pp).test(); + + to.assertEmpty(); + + assertTrue(pp.hasSubscribers()); + + pp.onNext(1); + + assertFalse(pp.hasSubscribers()); + + to.assertResult(); + } + + @Test + public void emptyWithOnComplete() { + PublishProcessor<Object> pp = PublishProcessor.create(); + + TestObserver<Integer> to = Maybe.<Integer>empty() + .delay(pp).test(); + + to.assertEmpty(); + + assertTrue(pp.hasSubscribers()); + + pp.onComplete(); + + assertFalse(pp.hasSubscribers()); + + to.assertResult(); + } + + @Test + public void emptyWithOnError() { + PublishProcessor<Object> pp = PublishProcessor.create(); + + TestObserverEx<Integer> to = Maybe.<Integer>empty() + .delay(pp).to(TestHelper.<Integer>testConsumer()); + + to.assertEmpty(); + + assertTrue(pp.hasSubscribers()); + + pp.onError(new TestException("Other")); + + assertFalse(pp.hasSubscribers()); + + to.assertFailureAndMessage(TestException.class, "Other"); + } + + @Test + public void errorWithOnNext() { + PublishProcessor<Object> pp = PublishProcessor.create(); + + TestObserverEx<Integer> to = Maybe.<Integer>error(new TestException("Main")) + .delay(pp).to(TestHelper.<Integer>testConsumer()); + + to.assertEmpty(); + + assertTrue(pp.hasSubscribers()); + + pp.onNext(1); + + assertFalse(pp.hasSubscribers()); + + to.assertFailureAndMessage(TestException.class, "Main"); + } + + @Test + public void errorWithOnComplete() { + PublishProcessor<Object> pp = PublishProcessor.create(); + + TestObserverEx<Integer> to = Maybe.<Integer>error(new TestException("Main")) + .delay(pp).to(TestHelper.<Integer>testConsumer()); + + to.assertEmpty(); + + assertTrue(pp.hasSubscribers()); + + pp.onComplete(); + + assertFalse(pp.hasSubscribers()); + + to.assertFailureAndMessage(TestException.class, "Main"); + } + + @Test + public void errorWithOnError() { + PublishProcessor<Object> pp = PublishProcessor.create(); + + TestObserverEx<Integer> to = Maybe.<Integer>error(new TestException("Main")) + .delay(pp).to(TestHelper.<Integer>testConsumer()); + + to.assertEmpty(); + + assertTrue(pp.hasSubscribers()); + + pp.onError(new TestException("Other")); + + assertFalse(pp.hasSubscribers()); + + to.assertFailure(CompositeException.class); + + List<Throwable> list = TestHelper.compositeList(to.errors().get(0)); + assertEquals(2, list.size()); + + TestHelper.assertError(list, 0, TestException.class, "Main"); + TestHelper.assertError(list, 1, TestException.class, "Other"); + } + + @Test + public void withCompletableDispose() { + TestHelper.checkDisposed(Completable.complete().andThen(Maybe.just(1))); + } + + @Test + public void withCompletableDoubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeCompletableToMaybe(new Function<Completable, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Completable c) throws Exception { + return c.andThen(Maybe.just(1)); + } + }); + } + + @Test + public void withOtherPublisherDispose() { + TestHelper.checkDisposed(Maybe.just(1).delay(Flowable.just(1))); + } + + @Test + public void withOtherPublisherDoubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeMaybe(new Function<Maybe<Integer>, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Maybe<Integer> c) throws Exception { + return c.delay(Flowable.never()); + } + }); + } + + @Test + public void otherPublisherNextSlipsThrough() { + Maybe.just(1).delay(new Flowable<Integer>() { + @Override + protected void subscribeActual(Subscriber<? super Integer> s) { + s.onSubscribe(new BooleanSubscription()); + s.onNext(1); + s.onNext(2); + } + }) + .test() + .assertResult(1); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeDelaySubscriptionTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeDelaySubscriptionTest.java new file mode 100644 index 0000000000..dae5d6f68c --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeDelaySubscriptionTest.java @@ -0,0 +1,149 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.maybe; + +import static org.junit.Assert.*; + +import java.util.List; +import java.util.concurrent.TimeUnit; + +import org.junit.Test; +import org.reactivestreams.Subscriber; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.subscriptions.BooleanSubscription; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.processors.PublishProcessor; +import io.reactivex.rxjava3.schedulers.TestScheduler; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class MaybeDelaySubscriptionTest extends RxJavaTest { + + @Test + public void normal() { + PublishProcessor<Object> pp = PublishProcessor.create(); + + TestObserver<Integer> to = Maybe.just(1).delaySubscription(pp) + .test(); + + assertTrue(pp.hasSubscribers()); + + to.assertEmpty(); + + pp.onNext("one"); + + assertFalse(pp.hasSubscribers()); + + to.assertResult(1); + } + + @Test + public void timed() { + Maybe.just(1).delaySubscription(100, TimeUnit.MILLISECONDS) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(1); + } + + @Test + public void timedEmpty() { + Maybe.<Integer>empty().delaySubscription(100, TimeUnit.MILLISECONDS) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(); + } + + @Test + public void timedTestScheduler() { + TestScheduler scheduler = new TestScheduler(); + + TestObserver<Integer> to = Maybe.just(1) + .delaySubscription(100, TimeUnit.MILLISECONDS, scheduler) + .test(); + + to.assertEmpty(); + + scheduler.advanceTimeBy(99, TimeUnit.MILLISECONDS); + + to.assertEmpty(); + + scheduler.advanceTimeBy(1, TimeUnit.MILLISECONDS); + + to.assertResult(1); + } + + @Test + public void otherError() { + Maybe.just(1).delaySubscription(Flowable.error(new TestException())) + .test() + .assertFailure(TestException.class); + } + + @Test + public void mainError() { + Maybe.error(new TestException()) + .delaySubscription(Flowable.empty()) + .test() + .assertFailure(TestException.class); + } + + @Test + public void withPublisherDispose() { + TestHelper.checkDisposed(Maybe.just(1).delaySubscription(Flowable.never())); + } + + @Test + public void withPublisherDoubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeMaybe(new Function<Maybe<Object>, MaybeSource<Object>>() { + @Override + public MaybeSource<Object> apply(Maybe<Object> m) throws Exception { + return m.delaySubscription(Flowable.just(1)); + } + }); + } + + @Test + public void withPublisherCallAfterTerminalEvent() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + + try { + Flowable<Integer> f = new Flowable<Integer>() { + @Override + protected void subscribeActual(Subscriber<? super Integer> subscriber) { + subscriber.onSubscribe(new BooleanSubscription()); + subscriber.onNext(1); + subscriber.onError(new TestException()); + subscriber.onComplete(); + subscriber.onNext(2); + } + }; + + Maybe.just(1).delaySubscription(f) + .test() + .assertResult(1); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void doubleOnSubscribePublisher() { + TestHelper.checkDoubleOnSubscribeFlowableToMaybe(f -> Maybe.just(1).delaySubscription(f)); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeDelayTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeDelayTest.java new file mode 100644 index 0000000000..4485f58f5e --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeDelayTest.java @@ -0,0 +1,127 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.maybe; + +import static org.junit.Assert.*; + +import java.util.concurrent.TimeUnit; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.processors.PublishProcessor; +import io.reactivex.rxjava3.schedulers.TestScheduler; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class MaybeDelayTest extends RxJavaTest { + + @Test + public void success() { + Maybe.just(1).delay(100, TimeUnit.MILLISECONDS) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(1); + } + + @Test + public void error() { + Maybe.error(new TestException()).delay(100, TimeUnit.MILLISECONDS) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertFailure(TestException.class); + } + + @Test + public void complete() { + Maybe.empty().delay(100, TimeUnit.MILLISECONDS) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(); + } + + @Test + public void disposeDuringDelay() { + TestScheduler scheduler = new TestScheduler(); + + TestObserver<Integer> to = Maybe.just(1).delay(100, TimeUnit.MILLISECONDS, scheduler) + .test(); + + to.dispose(); + + scheduler.advanceTimeBy(1, TimeUnit.SECONDS); + + to.assertEmpty(); + } + + @Test + public void dispose() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + + TestObserver<Integer> to = pp.singleElement().delay(100, TimeUnit.MILLISECONDS).test(); + + assertTrue(pp.hasSubscribers()); + + to.dispose(); + + assertFalse(pp.hasSubscribers()); + } + + @Test + public void isDisposed() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + + TestHelper.checkDisposed(pp.singleElement().delay(100, TimeUnit.MILLISECONDS)); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeMaybe(new Function<Maybe<Object>, Maybe<Object>>() { + @Override + public Maybe<Object> apply(Maybe<Object> f) throws Exception { + return f.delay(100, TimeUnit.MILLISECONDS); + } + }); + } + + @Test + public void delayedErrorOnSuccess() { + final TestScheduler scheduler = new TestScheduler(); + final TestObserver<Integer> observer = Maybe.just(1) + .delay(5, TimeUnit.SECONDS, scheduler, true) + .test(); + + scheduler.advanceTimeTo(2, TimeUnit.SECONDS); + observer.assertNoValues(); + + scheduler.advanceTimeTo(5, TimeUnit.SECONDS); + observer.assertValue(1); + } + + @Test + public void delayedErrorOnError() { + final TestScheduler scheduler = new TestScheduler(); + final TestObserver<?> observer = Maybe.error(new TestException()) + .delay(5, TimeUnit.SECONDS, scheduler, true) + .test(); + + scheduler.advanceTimeTo(2, TimeUnit.SECONDS); + observer.assertNoErrors(); + + scheduler.advanceTimeTo(5, TimeUnit.SECONDS); + observer.assertError(TestException.class); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeDematerializeTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeDematerializeTest.java new file mode 100644 index 0000000000..4f78295f76 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeDematerializeTest.java @@ -0,0 +1,122 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.maybe; + +import static org.mockito.Mockito.*; +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.subjects.MaybeSubject; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class MaybeDematerializeTest extends RxJavaTest { + + @Test + public void success() { + Maybe.just(Notification.createOnNext(1)) + .dematerialize(Functions.<Notification<Integer>>identity()) + .test() + .assertResult(1); + } + + @Test + public void empty() { + Maybe.just(Notification.<Integer>createOnComplete()) + .dematerialize(Functions.<Notification<Integer>>identity()) + .test() + .assertResult(); + } + + @Test + public void emptySource() throws Throwable { + @SuppressWarnings("unchecked") + Function<Notification<Integer>, Notification<Integer>> function = mock(Function.class); + + Maybe.<Notification<Integer>>empty() + .dematerialize(function) + .test() + .assertResult(); + + verify(function, never()).apply(any()); + } + + @Test + public void error() { + Maybe.<Notification<Integer>>error(new TestException()) + .dematerialize(Functions.<Notification<Integer>>identity()) + .test() + .assertFailure(TestException.class); + } + + @Test + public void errorNotification() { + Maybe.just(Notification.<Integer>createOnError(new TestException())) + .dematerialize(Functions.<Notification<Integer>>identity()) + .test() + .assertFailure(TestException.class); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeMaybe(new Function<Maybe<Object>, MaybeSource<Object>>() { + @SuppressWarnings({ "unchecked", "rawtypes" }) + @Override + public MaybeSource<Object> apply(Maybe<Object> v) throws Exception { + return v.dematerialize((Function)Functions.identity()); + } + }); + } + + @Test + public void dispose() { + TestHelper.checkDisposed(MaybeSubject.<Notification<Integer>>create().dematerialize(Functions.<Notification<Integer>>identity())); + } + + @Test + public void selectorCrash() { + Maybe.just(Notification.createOnNext(1)) + .dematerialize(new Function<Notification<Integer>, Notification<Integer>>() { + @Override + public Notification<Integer> apply(Notification<Integer> v) throws Exception { + throw new TestException(); + } + }) + .test() + .assertFailure(TestException.class); + } + + @Test + public void selectorNull() { + Maybe.just(Notification.createOnNext(1)) + .dematerialize(Functions.justFunction((Notification<Integer>)null)) + .test() + .assertFailure(NullPointerException.class); + } + + @Test + public void selectorDifferentType() { + Maybe.just(Notification.createOnNext(1)) + .dematerialize(new Function<Notification<Integer>, Notification<String>>() { + @Override + public Notification<String> apply(Notification<Integer> v) throws Exception { + return Notification.createOnNext("Value-" + 1); + } + }) + .test() + .assertResult("Value-1"); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeDetachTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeDetachTest.java new file mode 100644 index 0000000000..6498e98371 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeDetachTest.java @@ -0,0 +1,168 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.maybe; + +import static org.junit.Assert.assertNull; + +import java.io.IOException; +import java.lang.ref.WeakReference; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.processors.PublishProcessor; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class MaybeDetachTest extends RxJavaTest { + + @Test + public void doubleSubscribe() { + + TestHelper.checkDoubleOnSubscribeMaybe(new Function<Maybe<Object>, MaybeSource<Object>>() { + @Override + public MaybeSource<Object> apply(Maybe<Object> m) throws Exception { + return m.onTerminateDetach(); + } + }); + } + + @Test + public void dispose() { + TestHelper.checkDisposed(PublishProcessor.create().singleElement().onTerminateDetach()); + } + + @Test + public void onError() { + Maybe.error(new TestException()) + .onTerminateDetach() + .test() + .assertFailure(TestException.class); + } + + @Test + public void onComplete() { + Maybe.empty() + .onTerminateDetach() + .test() + .assertResult(); + } + + @Test + public void cancelDetaches() throws Exception { + Disposable d = Disposable.empty(); + final WeakReference<Disposable> wr = new WeakReference<>(d); + + TestObserver<Object> to = new Maybe<Object>() { + @Override + protected void subscribeActual(MaybeObserver<? super Object> observer) { + observer.onSubscribe(wr.get()); + }; + } + .onTerminateDetach() + .test(); + + d = null; + + to.dispose(); + + System.gc(); + Thread.sleep(200); + + to.assertEmpty(); + + assertNull(wr.get()); + } + + @Test + public void completeDetaches() throws Exception { + Disposable d = Disposable.empty(); + final WeakReference<Disposable> wr = new WeakReference<>(d); + + TestObserver<Integer> to = new Maybe<Integer>() { + @Override + protected void subscribeActual(MaybeObserver<? super Integer> observer) { + observer.onSubscribe(wr.get()); + observer.onComplete(); + observer.onComplete(); + }; + } + .onTerminateDetach() + .test(); + + d = null; + + System.gc(); + Thread.sleep(200); + + to.assertResult(); + + assertNull(wr.get()); + } + + @Test + public void errorDetaches() throws Exception { + Disposable d = Disposable.empty(); + final WeakReference<Disposable> wr = new WeakReference<>(d); + + TestObserver<Integer> to = new Maybe<Integer>() { + @Override + protected void subscribeActual(MaybeObserver<? super Integer> observer) { + observer.onSubscribe(wr.get()); + observer.onError(new TestException()); + observer.onError(new IOException()); + }; + } + .onTerminateDetach() + .test(); + + d = null; + + System.gc(); + Thread.sleep(200); + + to.assertFailure(TestException.class); + + assertNull(wr.get()); + } + + @Test + public void successDetaches() throws Exception { + Disposable d = Disposable.empty(); + final WeakReference<Disposable> wr = new WeakReference<>(d); + + TestObserver<Integer> to = new Maybe<Integer>() { + @Override + protected void subscribeActual(MaybeObserver<? super Integer> observer) { + observer.onSubscribe(wr.get()); + observer.onSuccess(1); + observer.onSuccess(2); + }; + } + .onTerminateDetach() + .test(); + + d = null; + + System.gc(); + Thread.sleep(200); + + to.assertResult(1); + + assertNull(wr.get()); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeDoAfterSuccessTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeDoAfterSuccessTest.java new file mode 100644 index 0000000000..a961f0b11c --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeDoAfterSuccessTest.java @@ -0,0 +1,147 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.maybe; + +import static org.junit.Assert.*; + +import java.util.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.subjects.PublishSubject; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class MaybeDoAfterSuccessTest extends RxJavaTest { + + final List<Integer> values = new ArrayList<>(); + + final Consumer<Integer> afterSuccess = new Consumer<Integer>() { + @Override + public void accept(Integer e) throws Exception { + values.add(-e); + } + }; + + final TestObserver<Integer> to = new TestObserver<Integer>() { + @Override + public void onNext(Integer t) { + super.onNext(t); + MaybeDoAfterSuccessTest.this.values.add(t); + } + }; + + @Test + public void just() { + Maybe.just(1) + .doAfterSuccess(afterSuccess) + .subscribeWith(to) + .assertResult(1); + + assertEquals(Arrays.asList(1, -1), values); + } + + @Test + public void error() { + Maybe.<Integer>error(new TestException()) + .doAfterSuccess(afterSuccess) + .subscribeWith(to) + .assertFailure(TestException.class); + + assertTrue(values.isEmpty()); + } + + @Test + public void empty() { + Maybe.<Integer>empty() + .doAfterSuccess(afterSuccess) + .subscribeWith(to) + .assertResult(); + + assertTrue(values.isEmpty()); + } + + @Test + public void justConditional() { + Maybe.just(1) + .doAfterSuccess(afterSuccess) + .filter(Functions.alwaysTrue()) + .subscribeWith(to) + .assertResult(1); + + assertEquals(Arrays.asList(1, -1), values); + } + + @Test + public void errorConditional() { + Maybe.<Integer>error(new TestException()) + .doAfterSuccess(afterSuccess) + .filter(Functions.alwaysTrue()) + .subscribeWith(to) + .assertFailure(TestException.class); + + assertTrue(values.isEmpty()); + } + + @Test + public void emptyConditional() { + Maybe.<Integer>empty() + .doAfterSuccess(afterSuccess) + .filter(Functions.alwaysTrue()) + .subscribeWith(to) + .assertResult(); + + assertTrue(values.isEmpty()); + } + + @Test + public void consumerThrows() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + Maybe.just(1) + .doAfterSuccess(new Consumer<Integer>() { + @Override + public void accept(Integer e) throws Exception { + throw new TestException(); + } + }) + .test() + .assertResult(1); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void dispose() { + TestHelper.checkDisposed(PublishSubject.<Integer>create().singleElement().doAfterSuccess(afterSuccess)); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeMaybe(new Function<Maybe<Integer>, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Maybe<Integer> m) throws Exception { + return m.doAfterSuccess(afterSuccess); + } + }); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeDoFinallyTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeDoFinallyTest.java new file mode 100644 index 0000000000..f98c3cf810 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeDoFinallyTest.java @@ -0,0 +1,165 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.maybe; + +import static org.junit.Assert.assertEquals; + +import java.util.List; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.subjects.PublishSubject; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class MaybeDoFinallyTest extends RxJavaTest implements Action { + + int calls; + + @Override + public void run() throws Exception { + calls++; + } + + @Test + public void normalJust() { + Maybe.just(1) + .doFinally(this) + .test() + .assertResult(1); + + assertEquals(1, calls); + } + + @Test + public void normalEmpty() { + Maybe.empty() + .doFinally(this) + .test() + .assertResult(); + + assertEquals(1, calls); + } + + @Test + public void normalError() { + Maybe.error(new TestException()) + .doFinally(this) + .test() + .assertFailure(TestException.class); + + assertEquals(1, calls); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeMaybe(new Function<Maybe<Object>, Maybe<Object>>() { + @Override + public Maybe<Object> apply(Maybe<Object> f) throws Exception { + return f.doFinally(MaybeDoFinallyTest.this); + } + }); + TestHelper.checkDoubleOnSubscribeMaybe(new Function<Maybe<Object>, Maybe<Object>>() { + @Override + public Maybe<Object> apply(Maybe<Object> f) throws Exception { + return f.doFinally(MaybeDoFinallyTest.this).filter(Functions.alwaysTrue()); + } + }); + } + + @Test + public void normalJustConditional() { + Maybe.just(1) + .doFinally(this) + .filter(Functions.alwaysTrue()) + .test() + .assertResult(1); + + assertEquals(1, calls); + } + + @Test + public void normalEmptyConditional() { + Maybe.empty() + .doFinally(this) + .filter(Functions.alwaysTrue()) + .test() + .assertResult(); + + assertEquals(1, calls); + } + + @Test + public void normalErrorConditional() { + Maybe.error(new TestException()) + .doFinally(this) + .filter(Functions.alwaysTrue()) + .test() + .assertFailure(TestException.class); + + assertEquals(1, calls); + } + + @Test + public void actionThrows() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + Maybe.just(1) + .doFinally(new Action() { + @Override + public void run() throws Exception { + throw new TestException(); + } + }) + .test() + .assertResult(1) + .dispose(); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void actionThrowsConditional() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + Maybe.just(1) + .doFinally(new Action() { + @Override + public void run() throws Exception { + throw new TestException(); + } + }) + .filter(Functions.alwaysTrue()) + .test() + .assertResult(1) + .dispose(); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void disposed() { + TestHelper.checkDisposed(PublishSubject.create().singleElement().doFinally(this)); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeDoOnEventTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeDoOnEventTest.java new file mode 100644 index 0000000000..37614ec793 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeDoOnEventTest.java @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.maybe; + +import static org.junit.Assert.assertTrue; + +import java.util.List; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.subjects.PublishSubject; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class MaybeDoOnEventTest extends RxJavaTest { + + @Test + public void dispose() { + TestHelper.checkDisposed(PublishSubject.<Integer>create().singleElement().doOnEvent(new BiConsumer<Integer, Throwable>() { + @Override + public void accept(Integer v, Throwable e) throws Exception { + // irrelevant + } + })); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeMaybe(new Function<Maybe<Integer>, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Maybe<Integer> m) throws Exception { + return m.doOnEvent(new BiConsumer<Integer, Throwable>() { + @Override + public void accept(Integer v, Throwable e) throws Exception { + // irrelevant + } + }); + } + }); + } + + @Test + public void onSubscribeCrash() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + final Disposable bs = Disposable.empty(); + + new Maybe<Integer>() { + @Override + protected void subscribeActual(MaybeObserver<? super Integer> observer) { + observer.onSubscribe(bs); + observer.onError(new TestException("Second")); + observer.onComplete(); + observer.onSuccess(1); + } + } + .doOnSubscribe(new Consumer<Disposable>() { + @Override + public void accept(Disposable d) throws Exception { + throw new TestException("First"); + } + }) + .to(TestHelper.<Integer>testConsumer()) + .assertFailureAndMessage(TestException.class, "First"); + + assertTrue(bs.isDisposed()); + + TestHelper.assertUndeliverable(errors, 0, TestException.class, "Second"); + } finally { + RxJavaPlugins.reset(); + } + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeDoOnLifecycleTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeDoOnLifecycleTest.java new file mode 100644 index 0000000000..39d71105ca --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeDoOnLifecycleTest.java @@ -0,0 +1,168 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.maybe; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.subjects.MaybeSubject; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class MaybeDoOnLifecycleTest extends RxJavaTest { + + @Test + public void success() throws Throwable { + @SuppressWarnings("unchecked") + Consumer<? super Disposable> onSubscribe = mock(Consumer.class); + Action onDispose = mock(Action.class); + + Maybe.just(1) + .doOnLifecycle(onSubscribe, onDispose) + .test() + .assertResult(1); + + verify(onSubscribe).accept(any()); + verify(onDispose, never()).run(); + } + + @Test + public void empty() throws Throwable { + @SuppressWarnings("unchecked") + Consumer<? super Disposable> onSubscribe = mock(Consumer.class); + Action onDispose = mock(Action.class); + + Maybe.empty() + .doOnLifecycle(onSubscribe, onDispose) + .test() + .assertResult(); + + verify(onSubscribe).accept(any()); + verify(onDispose, never()).run(); + } + + @Test + public void error() throws Throwable { + @SuppressWarnings("unchecked") + Consumer<? super Disposable> onSubscribe = mock(Consumer.class); + Action onDispose = mock(Action.class); + + Maybe.error(new TestException()) + .doOnLifecycle(onSubscribe, onDispose) + .test() + .assertFailure(TestException.class); + + verify(onSubscribe).accept(any()); + verify(onDispose, never()).run(); + } + + @Test + public void onSubscribeCrash() throws Throwable { + TestHelper.withErrorTracking(errors -> { + @SuppressWarnings("unchecked") + Consumer<? super Disposable> onSubscribe = mock(Consumer.class); + Action onDispose = mock(Action.class); + + doThrow(new TestException("First")).when(onSubscribe).accept(any()); + + Disposable bs = Disposable.empty(); + + new Maybe<Integer>() { + @Override + protected void subscribeActual(MaybeObserver<? super Integer> observer) { + observer.onSubscribe(bs); + observer.onError(new TestException("Second")); + observer.onComplete(); + observer.onSuccess(1); + } + } + .doOnLifecycle(onSubscribe, onDispose) + .to(TestHelper.<Integer>testConsumer()) + .assertFailureAndMessage(TestException.class, "First"); + + assertTrue(bs.isDisposed()); + + TestHelper.assertUndeliverable(errors, 0, TestException.class, "Second"); + + verify(onSubscribe).accept(any()); + verify(onDispose, never()).run(); + }); + } + + @Test + public void onDisposeCrash() throws Throwable { + TestHelper.withErrorTracking(errors -> { + @SuppressWarnings("unchecked") + Consumer<? super Disposable> onSubscribe = mock(Consumer.class); + Action onDispose = mock(Action.class); + + doThrow(new TestException("First")).when(onDispose).run(); + + MaybeSubject<Integer> ms = MaybeSubject.create(); + + TestObserver<Integer> to = ms + .doOnLifecycle(onSubscribe, onDispose) + .test(); + + assertTrue(ms.hasObservers()); + + to.dispose(); + + assertFalse(ms.hasObservers()); + + TestHelper.assertUndeliverable(errors, 0, TestException.class, "First"); + + verify(onSubscribe).accept(any()); + verify(onDispose).run(); + }); + } + + @Test + public void dispose() throws Throwable { + @SuppressWarnings("unchecked") + Consumer<? super Disposable> onSubscribe = mock(Consumer.class); + Action onDispose = mock(Action.class); + + MaybeSubject<Integer> ms = MaybeSubject.create(); + + TestObserver<Integer> to = ms + .doOnLifecycle(onSubscribe, onDispose) + .test(); + + assertTrue(ms.hasObservers()); + + to.dispose(); + + assertFalse(ms.hasObservers()); + + verify(onSubscribe).accept(any()); + verify(onDispose).run(); + } + + @Test + public void isDisposed() { + TestHelper.checkDisposed(MaybeSubject.create().doOnLifecycle(d -> { }, () -> { })); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeMaybe(m -> m.doOnLifecycle(d -> { }, () -> { })); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeDoOnTerminateTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeDoOnTerminateTest.java new file mode 100644 index 0000000000..4471db0ee1 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeDoOnTerminateTest.java @@ -0,0 +1,116 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.maybe; + +import static org.junit.Assert.assertTrue; + +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.Action; +import io.reactivex.rxjava3.testsupport.*; + +public class MaybeDoOnTerminateTest extends RxJavaTest { + + @Test + public void doOnTerminateSuccess() { + final AtomicBoolean atomicBoolean = new AtomicBoolean(); + Maybe.just(1).doOnTerminate(new Action() { + @Override + public void run() { + atomicBoolean.set(true); + } + }) + .test() + .assertResult(1); + + assertTrue(atomicBoolean.get()); + } + + @Test + public void doOnTerminateError() { + final AtomicBoolean atomicBoolean = new AtomicBoolean(); + Maybe.error(new TestException()).doOnTerminate(new Action() { + @Override + public void run() { + atomicBoolean.set(true); + } + }) + .test() + .assertFailure(TestException.class); + + assertTrue(atomicBoolean.get()); + } + + @Test + public void doOnTerminateComplete() { + final AtomicBoolean atomicBoolean = new AtomicBoolean(); + Maybe.empty().doOnTerminate(new Action() { + @Override + public void run() { + atomicBoolean.set(true); + } + }) + .test() + .assertResult(); + + assertTrue(atomicBoolean.get()); + } + + @Test + public void doOnTerminateSuccessCrash() { + Maybe.just(1).doOnTerminate(new Action() { + @Override + public void run() { + throw new TestException(); + } + }) + .test() + .assertFailure(TestException.class); + } + + @Test + public void doOnTerminateErrorCrash() { + TestObserverEx<Object> to = Maybe.error(new TestException("Outer")) + .doOnTerminate(new Action() { + @Override + public void run() { + throw new TestException("Inner"); + } + }) + .to(TestHelper.<Object>testConsumer()) + .assertFailure(CompositeException.class); + + List<Throwable> errors = TestHelper.compositeList(to.errors().get(0)); + TestHelper.assertError(errors, 0, TestException.class, "Outer"); + TestHelper.assertError(errors, 1, TestException.class, "Inner"); + } + + @Test + public void doOnTerminateCompleteCrash() { + Maybe.empty() + .doOnTerminate(new Action() { + @Override + public void run() { + throw new TestException(); + } + }) + .test() + .assertFailure(TestException.class); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeEmptyTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeEmptyTest.java new file mode 100644 index 0000000000..a219718936 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeEmptyTest.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.maybe; + +import static org.junit.Assert.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.operators.ScalarSupplier; + +public class MaybeEmptyTest extends RxJavaTest { + + @Test + public void scalarSupplier() { + Maybe<Integer> m = Maybe.empty(); + + assertTrue(m.getClass().toString(), m instanceof ScalarSupplier); + + assertNull(((ScalarSupplier<?>)m).get()); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeEqualTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeEqualTest.java new file mode 100644 index 0000000000..e7b6ea4302 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeEqualTest.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.maybe; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.BiPredicate; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class MaybeEqualTest extends RxJavaTest { + + @Test + public void dispose() { + TestHelper.checkDisposed(Maybe.sequenceEqual(Maybe.just(1), Maybe.just(1))); + } + + @Test + public void predicateThrows() { + Maybe.sequenceEqual(Maybe.just(1), Maybe.just(2), new BiPredicate<Integer, Integer>() { + @Override + public boolean test(Integer a, Integer b) throws Exception { + throw new TestException(); + } + }) + .test() + .assertFailure(TestException.class); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeErrorTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeErrorTest.java new file mode 100644 index 0000000000..f67dc7884b --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeErrorTest.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.maybe; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.Supplier; + +public class MaybeErrorTest extends RxJavaTest { + + @Test + public void errorSupplierThrows() { + Maybe.error(new Supplier<Throwable>() { + @Override + public Throwable get() throws Exception { + throw new TestException(); + } + }) + .test() + .assertFailure(TestException.class); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeFilterSingleTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeFilterSingleTest.java new file mode 100644 index 0000000000..52682cc208 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeFilterSingleTest.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.maybe; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.subjects.PublishSubject; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class MaybeFilterSingleTest extends RxJavaTest { + + @Test + public void error() { + Single.error(new TestException()) + .filter(Functions.alwaysTrue()) + .test() + .assertFailure(TestException.class); + } + + @Test + public void dispose() { + TestHelper.checkDisposed(PublishSubject.create().singleOrError().filter(Functions.alwaysTrue())); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeSingleToMaybe(new Function<Single<Object>, MaybeSource<Object>>() { + @Override + public MaybeSource<Object> apply(Single<Object> v) throws Exception { + return v.filter(Functions.alwaysTrue()); + } + }); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeFlatMapBiSelectorTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeFlatMapBiSelectorTest.java new file mode 100644 index 0000000000..67227bb422 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeFlatMapBiSelectorTest.java @@ -0,0 +1,236 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.maybe; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.processors.PublishProcessor; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class MaybeFlatMapBiSelectorTest extends RxJavaTest { + + BiFunction<Integer, Integer, String> stringCombine() { + return new BiFunction<Integer, Integer, String>() { + @Override + public String apply(Integer a, Integer b) throws Exception { + return a + ":" + b; + } + }; + } + + @Test + public void normal() { + Maybe.just(1) + .flatMap(new Function<Integer, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Integer v) throws Exception { + return Maybe.just(2); + } + }, stringCombine()) + .test() + .assertResult("1:2"); + } + + @Test + public void normalWithEmpty() { + Maybe.just(1) + .flatMap(new Function<Integer, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Integer v) throws Exception { + return Maybe.empty(); + } + }, stringCombine()) + .test() + .assertResult(); + } + + @Test + public void emptyWithJust() { + final int[] call = { 0 }; + + Maybe.<Integer>empty() + .flatMap(new Function<Integer, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Integer v) throws Exception { + call[0]++; + return Maybe.just(1); + } + }, stringCombine()) + .test() + .assertResult(); + + assertEquals(0, call[0]); + } + + @Test + public void errorWithJust() { + final int[] call = { 0 }; + + Maybe.<Integer>error(new TestException()) + .flatMap(new Function<Integer, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Integer v) throws Exception { + call[0]++; + return Maybe.just(1); + } + }, stringCombine()) + .test() + .assertFailure(TestException.class); + + assertEquals(0, call[0]); + } + + @Test + public void justWithError() { + final int[] call = { 0 }; + + Maybe.just(1) + .flatMap(new Function<Integer, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Integer v) throws Exception { + call[0]++; + return Maybe.<Integer>error(new TestException()); + } + }, stringCombine()) + .test() + .assertFailure(TestException.class); + + assertEquals(1, call[0]); + } + + @Test + public void dispose() { + TestHelper.checkDisposed(PublishProcessor.create().singleElement() + .flatMap(new Function<Object, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Object v) throws Exception { + return Maybe.just(1); + } + }, new BiFunction<Object, Integer, Object>() { + @Override + public Object apply(Object a, Integer b) throws Exception { + return b; + } + })); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeMaybe(new Function<Maybe<Object>, MaybeSource<Object>>() { + @Override + public MaybeSource<Object> apply(Maybe<Object> v) throws Exception { + return v.flatMap(new Function<Object, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Object v) throws Exception { + return Maybe.just(1); + } + }, new BiFunction<Object, Integer, Object>() { + @Override + public Object apply(Object a, Integer b) throws Exception { + return b; + } + }); + } + }); + } + + @Test + public void mapperThrows() { + Maybe.just(1) + .flatMap(new Function<Integer, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Integer v) throws Exception { + throw new TestException(); + } + }, stringCombine()) + .test() + .assertFailure(TestException.class); + } + + @Test + public void mapperReturnsNull() { + Maybe.just(1) + .flatMap(new Function<Integer, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Integer v) throws Exception { + return null; + } + }, stringCombine()) + .test() + .assertFailure(NullPointerException.class); + } + + @Test + public void resultSelectorThrows() { + Maybe.just(1) + .flatMap(new Function<Integer, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Integer v) throws Exception { + return Maybe.just(2); + } + }, new BiFunction<Integer, Integer, Object>() { + @Override + public Object apply(Integer a, Integer b) throws Exception { + throw new TestException(); + } + }) + .test() + .assertFailure(TestException.class); + } + + @Test + public void resultSelectorReturnsNull() { + Maybe.just(1) + .flatMap(new Function<Integer, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Integer v) throws Exception { + return Maybe.just(2); + } + }, new BiFunction<Integer, Integer, Object>() { + @Override + public Object apply(Integer a, Integer b) throws Exception { + return null; + } + }) + .test() + .assertFailure(NullPointerException.class); + } + + @Test + public void mapperCancels() { + final TestObserver<Integer> to = new TestObserver<>(); + + Maybe.just(1) + .flatMap(new Function<Integer, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Integer v) throws Exception { + to.dispose(); + return Maybe.just(2); + } + }, new BiFunction<Integer, Integer, Integer>() { + @Override + public Integer apply(Integer a, Integer b) throws Exception { + throw new IllegalStateException(); + } + }) + .subscribeWith(to) + .assertEmpty(); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeFlatMapCompletableTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeFlatMapCompletableTest.java new file mode 100644 index 0000000000..053f1e3e40 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeFlatMapCompletableTest.java @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.maybe; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class MaybeFlatMapCompletableTest extends RxJavaTest { + + @Test + public void dispose() { + TestHelper.checkDisposed(Maybe.just(1).flatMapCompletable(new Function<Integer, Completable>() { + @Override + public Completable apply(Integer v) throws Exception { + return Completable.complete(); + } + })); + } + + @Test + public void mapperThrows() { + Maybe.just(1) + .flatMapCompletable(new Function<Integer, Completable>() { + @Override + public Completable apply(Integer v) throws Exception { + throw new TestException(); + } + }) + .test() + .assertFailure(TestException.class); + } + + @Test + public void mapperReturnsNull() { + Maybe.just(1) + .flatMapCompletable(new Function<Integer, Completable>() { + @Override + public Completable apply(Integer v) throws Exception { + return null; + } + }) + .test() + .assertFailure(NullPointerException.class); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeFlatMapIterableFlowableTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeFlatMapIterableFlowableTest.java new file mode 100644 index 0000000000..e4bcaa9f77 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeFlatMapIterableFlowableTest.java @@ -0,0 +1,609 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.maybe; + +import static org.junit.Assert.*; + +import java.util.*; +import java.util.concurrent.TimeUnit; + +import org.junit.Test; +import org.reactivestreams.Subscription; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.util.CrashingIterable; +import io.reactivex.rxjava3.operators.QueueFuseable; +import io.reactivex.rxjava3.operators.QueueSubscription; +import io.reactivex.rxjava3.schedulers.Schedulers; +import io.reactivex.rxjava3.subjects.*; +import io.reactivex.rxjava3.subscribers.TestSubscriber; +import io.reactivex.rxjava3.testsupport.*; + +public class MaybeFlatMapIterableFlowableTest extends RxJavaTest { + + @Test + public void normal() { + + Maybe.just(1).flattenAsFlowable(new Function<Integer, Iterable<Integer>>() { + @Override + public Iterable<Integer> apply(Integer v) throws Exception { + return Arrays.asList(v, v + 1); + } + }) + .test() + .assertResult(1, 2); + } + + @Test + public void emptyIterable() { + + Maybe.just(1).flattenAsFlowable(new Function<Integer, Iterable<Integer>>() { + @Override + public Iterable<Integer> apply(Integer v) throws Exception { + return Collections.<Integer>emptyList(); + } + }) + .test() + .assertResult(); + } + + @Test + public void error() { + + Maybe.<Integer>error(new TestException()).flattenAsFlowable(new Function<Integer, Iterable<Integer>>() { + @Override + public Iterable<Integer> apply(Integer v) throws Exception { + return Arrays.asList(v, v + 1); + } + }) + .test() + .assertFailure(TestException.class); + } + + @Test + public void empty() { + + Maybe.<Integer>empty().flattenAsFlowable(new Function<Integer, Iterable<Integer>>() { + @Override + public Iterable<Integer> apply(Integer v) throws Exception { + return Arrays.asList(v, v + 1); + } + }) + .test() + .assertResult(); + } + + @Test + public void backpressure() { + + TestSubscriber<Integer> ts = Maybe.just(1).flattenAsFlowable(new Function<Integer, Iterable<Integer>>() { + @Override + public Iterable<Integer> apply(Integer v) throws Exception { + return Arrays.asList(v, v + 1); + } + }) + .test(0); + + ts.assertEmpty(); + + ts.request(1); + + ts.assertValue(1); + + ts.request(1); + + ts.assertResult(1, 2); + } + + @Test + public void take() { + Maybe.just(1).flattenAsFlowable(new Function<Integer, Iterable<Integer>>() { + @Override + public Iterable<Integer> apply(Integer v) throws Exception { + return Arrays.asList(v, v + 1); + } + }) + .take(1) + .test() + .assertResult(1); + } + + @Test + public void take2() { + Maybe.just(1).flattenAsFlowable(new Function<Integer, Iterable<Integer>>() { + @Override + public Iterable<Integer> apply(Integer v) throws Exception { + return Arrays.asList(v, v + 1); + } + }) + .doOnSubscribe(s -> s.request(Long.MAX_VALUE)) + .take(1) + .test() + .assertResult(1); + } + + @Test + public void fused() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<Integer>().setInitialFusionMode(QueueFuseable.ANY); + + Maybe.just(1).flattenAsFlowable(new Function<Integer, Iterable<Integer>>() { + @Override + public Iterable<Integer> apply(Integer v) throws Exception { + return Arrays.asList(v, v + 1); + } + }) + .subscribe(ts); + + ts.assertFuseable() + .assertFusionMode(QueueFuseable.ASYNC) + .assertResult(1, 2); + ; + } + + @Test + public void fusedNoSync() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<Integer>().setInitialFusionMode(QueueFuseable.SYNC); + + Maybe.just(1).flattenAsFlowable(new Function<Integer, Iterable<Integer>>() { + @Override + public Iterable<Integer> apply(Integer v) throws Exception { + return Arrays.asList(v, v + 1); + } + }) + .subscribe(ts); + + ts.assertFuseable() + .assertFusionMode(QueueFuseable.NONE) + .assertResult(1, 2); + ; + } + + @Test + public void iteratorCrash() { + + Maybe.just(1).flattenAsFlowable(new Function<Integer, Iterable<Integer>>() { + @Override + public Iterable<Integer> apply(Integer v) throws Exception { + return new CrashingIterable(1, 100, 100); + } + }) + .to(TestHelper.<Integer>testConsumer()) + .assertFailureAndMessage(TestException.class, "iterator()"); + } + + @Test + public void hasNextCrash() { + + Maybe.just(1).flattenAsFlowable(new Function<Integer, Iterable<Integer>>() { + @Override + public Iterable<Integer> apply(Integer v) throws Exception { + return new CrashingIterable(100, 1, 100); + } + }) + .to(TestHelper.<Integer>testConsumer()) + .assertFailureAndMessage(TestException.class, "hasNext()"); + } + + @Test + public void nextCrash() { + + Maybe.just(1).flattenAsFlowable(new Function<Integer, Iterable<Integer>>() { + @Override + public Iterable<Integer> apply(Integer v) throws Exception { + return new CrashingIterable(100, 100, 1); + } + }) + .to(TestHelper.<Integer>testConsumer()) + .assertFailureAndMessage(TestException.class, "next()"); + } + + @Test + public void hasNextCrash2() { + + Maybe.just(1).flattenAsFlowable(new Function<Integer, Iterable<Integer>>() { + @Override + public Iterable<Integer> apply(Integer v) throws Exception { + return new CrashingIterable(100, 2, 100); + } + }) + .to(TestHelper.<Integer>testConsumer()) + .assertFailureAndMessage(TestException.class, "hasNext()", 0); + } + + @Test + public void async1() { + Maybe.just(1) + .flattenAsFlowable(new Function<Object, Iterable<Integer>>() { + @Override + public Iterable<Integer> apply(Object v) throws Exception { + Integer[] array = new Integer[1000 * 1000]; + Arrays.fill(array, 1); + return Arrays.asList(array); + } + }) + .hide() + .observeOn(Schedulers.single()) + .to(TestHelper.<Integer>testConsumer()) + .awaitDone(5, TimeUnit.SECONDS) + .assertSubscribed() + .assertValueCount(1000 * 1000) + .assertNoErrors() + .assertComplete(); + } + + @Test + public void async2() { + Maybe.just(1) + .flattenAsFlowable(new Function<Object, Iterable<Integer>>() { + @Override + public Iterable<Integer> apply(Object v) throws Exception { + Integer[] array = new Integer[1000 * 1000]; + Arrays.fill(array, 1); + return Arrays.asList(array); + } + }) + .observeOn(Schedulers.single()) + .to(TestHelper.<Integer>testConsumer()) + .awaitDone(5, TimeUnit.SECONDS) + .assertSubscribed() + .assertValueCount(1000 * 1000) + .assertNoErrors() + .assertComplete(); + } + + @Test + public void async3() { + Maybe.just(1) + .flattenAsFlowable(new Function<Object, Iterable<Integer>>() { + @Override + public Iterable<Integer> apply(Object v) throws Exception { + Integer[] array = new Integer[1000 * 1000]; + Arrays.fill(array, 1); + return Arrays.asList(array); + } + }) + .take(500 * 1000) + .observeOn(Schedulers.single()) + .to(TestHelper.<Integer>testConsumer()) + .awaitDone(5, TimeUnit.SECONDS) + .assertSubscribed() + .assertValueCount(500 * 1000) + .assertNoErrors() + .assertComplete(); + } + + @Test + public void async4() { + Maybe.just(1) + .flattenAsFlowable(new Function<Object, Iterable<Integer>>() { + @Override + public Iterable<Integer> apply(Object v) throws Exception { + Integer[] array = new Integer[1000 * 1000]; + Arrays.fill(array, 1); + return Arrays.asList(array); + } + }) + .observeOn(Schedulers.single()) + .take(500 * 1000) + .to(TestHelper.<Integer>testConsumer()) + .awaitDone(5, TimeUnit.SECONDS) + .assertSubscribed() + .assertValueCount(500 * 1000) + .assertNoErrors() + .assertComplete(); + } + + @Test + public void fusedEmptyCheck() { + Maybe.just(1) + .flattenAsFlowable(new Function<Object, Iterable<Integer>>() { + @Override + public Iterable<Integer> apply(Object v) throws Exception { + return Arrays.asList(1, 2, 3); + } + }).subscribe(new FlowableSubscriber<Integer>() { + QueueSubscription<Integer> qs; + @SuppressWarnings("unchecked") + @Override + public void onSubscribe(Subscription s) { + qs = (QueueSubscription<Integer>)s; + + assertEquals(QueueFuseable.ASYNC, qs.requestFusion(QueueFuseable.ANY)); + } + + @Override + public void onNext(Integer value) { + assertFalse(qs.isEmpty()); + + qs.clear(); + + assertTrue(qs.isEmpty()); + + qs.cancel(); + } + + @Override + public void onError(Throwable e) { + } + + @Override + public void onComplete() { + } + }); + } + + @Test + public void hasNextThrowsUnbounded() { + Maybe.just(1) + .flattenAsFlowable(new Function<Object, Iterable<Integer>>() { + @Override + public Iterable<Integer> apply(Object v) throws Exception { + return new CrashingIterable(100, 2, 100); + } + }) + .to(TestHelper.<Integer>testConsumer()) + .assertFailureAndMessage(TestException.class, "hasNext()", 0); + } + + @Test + public void nextThrowsUnbounded() { + Maybe.just(1) + .flattenAsFlowable(new Function<Object, Iterable<Integer>>() { + @Override + public Iterable<Integer> apply(Object v) throws Exception { + return new CrashingIterable(100, 100, 1); + } + }) + .to(TestHelper.<Integer>testConsumer()) + .assertFailureAndMessage(TestException.class, "next()"); + } + + @Test + public void hasNextThrows() { + Maybe.just(1) + .flattenAsFlowable(new Function<Object, Iterable<Integer>>() { + @Override + public Iterable<Integer> apply(Object v) throws Exception { + return new CrashingIterable(100, 2, 100); + } + }) + .to(TestHelper.<Integer>testSubscriber(2L)) + .assertFailureAndMessage(TestException.class, "hasNext()", 0); + } + + @Test + public void nextThrows() { + Maybe.just(1) + .flattenAsFlowable(new Function<Object, Iterable<Integer>>() { + @Override + public Iterable<Integer> apply(Object v) throws Exception { + return new CrashingIterable(100, 100, 1); + } + }) + .to(TestHelper.<Integer>testSubscriber(2L)) + .assertFailureAndMessage(TestException.class, "next()"); + } + + @Test + public void requestBefore() { + for (int i = 0; i < 500; i++) { + final PublishSubject<Integer> ps = PublishSubject.create(); + + ps.singleElement().flattenAsFlowable( + new Function<Integer, Iterable<Integer>>() { + @Override + public Iterable<Integer> apply(Integer v) throws Exception { + return Arrays.asList(1, 2, 3); + } + }) + .test(5L) + .assertEmpty(); + } + } + + @Test + public void requestCreateInnerRace() { + final Integer[] a = new Integer[1000]; + Arrays.fill(a, 1); + + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final PublishSubject<Integer> ps = PublishSubject.create(); + + ps.onNext(1); + + final TestSubscriber<Integer> ts = ps.singleElement().flattenAsFlowable( + new Function<Integer, Iterable<Integer>>() { + @Override + public Iterable<Integer> apply(Integer v) throws Exception { + return Arrays.asList(a); + } + }) + .test(0L); + + Runnable r1 = new Runnable() { + @Override + public void run() { + ps.onComplete(); + for (int i = 0; i < 500; i++) { + ts.request(1); + } + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + for (int i = 0; i < 500; i++) { + ts.request(1); + } + } + }; + + TestHelper.race(r1, r2); + } + } + + @Test + public void cancelCreateInnerRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final PublishSubject<Integer> ps = PublishSubject.create(); + + ps.onNext(1); + + final TestSubscriber<Integer> ts = ps.singleElement().flattenAsFlowable( + new Function<Integer, Iterable<Integer>>() { + @Override + public Iterable<Integer> apply(Integer v) throws Exception { + return Arrays.asList(1, 2, 3); + } + }) + .test(0L); + + Runnable r1 = new Runnable() { + @Override + public void run() { + ps.onComplete(); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + ts.cancel(); + } + }; + + TestHelper.race(r1, r2); + } + } + + @Test + public void slowPathCancelAfterHasNext() { + final Integer[] a = new Integer[1000]; + Arrays.fill(a, 1); + + final TestSubscriber<Integer> ts = new TestSubscriber<>(0L); + + Maybe.just(1) + .flattenAsFlowable(new Function<Integer, Iterable<Integer>>() { + @Override + public Iterable<Integer> apply(Integer v) throws Exception { + return new Iterable<Integer>() { + @Override + public Iterator<Integer> iterator() { + return new Iterator<Integer>() { + int count; + @Override + public boolean hasNext() { + if (count++ == 2) { + ts.cancel(); + } + return true; + } + + @Override + public Integer next() { + return 1; + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + }; + } + }; + } + }) + .subscribe(ts); + + ts.request(3); + ts.assertValues(1, 1).assertNoErrors().assertNotComplete(); + } + + @Test + public void fastPathCancelAfterHasNext() { + final Integer[] a = new Integer[1000]; + Arrays.fill(a, 1); + + final TestSubscriber<Integer> ts = new TestSubscriber<>(0L); + + Maybe.just(1) + .flattenAsFlowable(new Function<Integer, Iterable<Integer>>() { + @Override + public Iterable<Integer> apply(Integer v) throws Exception { + return new Iterable<Integer>() { + @Override + public Iterator<Integer> iterator() { + return new Iterator<Integer>() { + int count; + @Override + public boolean hasNext() { + if (count++ == 2) { + ts.cancel(); + } + return true; + } + + @Override + public Integer next() { + return 1; + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + }; + } + }; + } + }) + .subscribe(ts); + + ts.request(Long.MAX_VALUE); + ts.assertValues(1, 1).assertNoErrors().assertNotComplete(); + } + + @Test + public void badRequest() { + TestHelper.assertBadRequestReported(MaybeSubject.create().flattenAsFlowable(v -> Arrays.asList(v))); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeMaybeToFlowable(m -> m.flattenAsFlowable(v -> Arrays.asList(v))); + } + + @Test + public void onSuccessRequestRace() { + List<Object> list = Arrays.asList(1); + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + + MaybeSubject<Integer> ms = MaybeSubject.create(); + + TestSubscriber<Object> ts = ms.flattenAsFlowable(v -> list) + .test(0L); + + TestHelper.race( + () -> ms.onSuccess(1), + () -> ts.request(1) + ); + + ts.assertResult(1); + } + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeFlatMapIterableObservableTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeFlatMapIterableObservableTest.java new file mode 100644 index 0000000000..d06f626119 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeFlatMapIterableObservableTest.java @@ -0,0 +1,335 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.maybe; + +import static org.junit.Assert.*; + +import java.util.*; +import java.util.concurrent.TimeUnit; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.core.Observer; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.util.CrashingIterable; +import io.reactivex.rxjava3.operators.QueueDisposable; +import io.reactivex.rxjava3.operators.QueueFuseable; +import io.reactivex.rxjava3.schedulers.Schedulers; +import io.reactivex.rxjava3.testsupport.*; + +public class MaybeFlatMapIterableObservableTest extends RxJavaTest { + + @Test + public void normal() { + + Maybe.just(1).flattenAsObservable(new Function<Integer, Iterable<Integer>>() { + @Override + public Iterable<Integer> apply(Integer v) throws Exception { + return Arrays.asList(v, v + 1); + } + }) + .test() + .assertResult(1, 2); + } + + @Test + public void emptyIterable() { + + Maybe.just(1).flattenAsObservable(new Function<Integer, Iterable<Integer>>() { + @Override + public Iterable<Integer> apply(Integer v) throws Exception { + return Collections.<Integer>emptyList(); + } + }) + .test() + .assertResult(); + } + + @Test + public void error() { + + Maybe.<Integer>error(new TestException()).flattenAsObservable(new Function<Integer, Iterable<Integer>>() { + @Override + public Iterable<Integer> apply(Integer v) throws Exception { + return Arrays.asList(v, v + 1); + } + }) + .test() + .assertFailure(TestException.class); + } + + @Test + public void empty() { + + Maybe.<Integer>empty().flattenAsObservable(new Function<Integer, Iterable<Integer>>() { + @Override + public Iterable<Integer> apply(Integer v) throws Exception { + return Arrays.asList(v, v + 1); + } + }) + .test() + .assertResult(); + } + + @Test + public void take() { + Maybe.just(1).flattenAsObservable(new Function<Integer, Iterable<Integer>>() { + @Override + public Iterable<Integer> apply(Integer v) throws Exception { + return Arrays.asList(v, v + 1); + } + }) + .take(1) + .test() + .assertResult(1); + } + + @Test + public void fused() { + TestObserverEx<Integer> to = new TestObserverEx<>(QueueFuseable.ANY); + + Maybe.just(1).flattenAsObservable(new Function<Integer, Iterable<Integer>>() { + @Override + public Iterable<Integer> apply(Integer v) throws Exception { + return Arrays.asList(v, v + 1); + } + }) + .subscribe(to); + + to.assertFuseable() + .assertFusionMode(QueueFuseable.ASYNC) + .assertResult(1, 2); + ; + } + + @Test + public void fusedNoSync() { + TestObserverEx<Integer> to = new TestObserverEx<>(QueueFuseable.SYNC); + + Maybe.just(1).flattenAsObservable(new Function<Integer, Iterable<Integer>>() { + @Override + public Iterable<Integer> apply(Integer v) throws Exception { + return Arrays.asList(v, v + 1); + } + }) + .subscribe(to); + + to.assertFuseable() + .assertFusionMode(QueueFuseable.NONE) + .assertResult(1, 2) + ; + } + + @Test + public void iteratorCrash() { + + Maybe.just(1).flattenAsObservable(new Function<Integer, Iterable<Integer>>() { + @Override + public Iterable<Integer> apply(Integer v) throws Exception { + return new CrashingIterable(1, 100, 100); + } + }) + .to(TestHelper.<Integer>testConsumer()) + .assertFailureAndMessage(TestException.class, "iterator()"); + } + + @Test + public void hasNextCrash() { + + Maybe.just(1).flattenAsObservable(new Function<Integer, Iterable<Integer>>() { + @Override + public Iterable<Integer> apply(Integer v) throws Exception { + return new CrashingIterable(100, 1, 100); + } + }) + .to(TestHelper.<Integer>testConsumer()) + .assertFailureAndMessage(TestException.class, "hasNext()"); + } + + @Test + public void nextCrash() { + + Maybe.just(1).flattenAsObservable(new Function<Integer, Iterable<Integer>>() { + @Override + public Iterable<Integer> apply(Integer v) throws Exception { + return new CrashingIterable(100, 100, 1); + } + }) + .to(TestHelper.<Integer>testConsumer()) + .assertFailureAndMessage(TestException.class, "next()"); + } + + @Test + public void hasNextCrash2() { + + Maybe.just(1).flattenAsObservable(new Function<Integer, Iterable<Integer>>() { + @Override + public Iterable<Integer> apply(Integer v) throws Exception { + return new CrashingIterable(100, 2, 100); + } + }) + .to(TestHelper.<Integer>testConsumer()) + .assertFailureAndMessage(TestException.class, "hasNext()", 0); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeMaybeToObservable(new Function<Maybe<Object>, ObservableSource<Integer>>() { + @Override + public ObservableSource<Integer> apply(Maybe<Object> o) throws Exception { + return o.flattenAsObservable(new Function<Object, Iterable<Integer>>() { + @Override + public Iterable<Integer> apply(Object v) throws Exception { + return Collections.singleton(1); + } + }); + } + }); + } + + @Test + public void dispose() { + TestHelper.checkDisposed(Maybe.just(1).flattenAsObservable(new Function<Object, Iterable<Integer>>() { + @Override + public Iterable<Integer> apply(Object v) throws Exception { + return Collections.singleton(1); + } + })); + } + + @Test + public void async1() { + Maybe.just(1) + .flattenAsObservable(new Function<Object, Iterable<Integer>>() { + @Override + public Iterable<Integer> apply(Object v) throws Exception { + Integer[] array = new Integer[1000 * 1000]; + Arrays.fill(array, 1); + return Arrays.asList(array); + } + }) + .hide() + .observeOn(Schedulers.single()) + .to(TestHelper.<Integer>testConsumer()) + .awaitDone(5, TimeUnit.SECONDS) + .assertSubscribed() + .assertValueCount(1000 * 1000) + .assertNoErrors() + .assertComplete(); + } + + @Test + public void async2() { + Maybe.just(1) + .flattenAsObservable(new Function<Object, Iterable<Integer>>() { + @Override + public Iterable<Integer> apply(Object v) throws Exception { + Integer[] array = new Integer[1000 * 1000]; + Arrays.fill(array, 1); + return Arrays.asList(array); + } + }) + .observeOn(Schedulers.single()) + .to(TestHelper.<Integer>testConsumer()) + .awaitDone(5, TimeUnit.SECONDS) + .assertSubscribed() + .assertValueCount(1000 * 1000) + .assertNoErrors() + .assertComplete(); + } + + @Test + public void async3() { + Maybe.just(1) + .flattenAsObservable(new Function<Object, Iterable<Integer>>() { + @Override + public Iterable<Integer> apply(Object v) throws Exception { + Integer[] array = new Integer[1000 * 1000]; + Arrays.fill(array, 1); + return Arrays.asList(array); + } + }) + .take(500 * 1000) + .observeOn(Schedulers.single()) + .to(TestHelper.<Integer>testConsumer()) + .awaitDone(5, TimeUnit.SECONDS) + .assertSubscribed() + .assertValueCount(500 * 1000) + .assertNoErrors() + .assertComplete(); + } + + @Test + public void async4() { + Maybe.just(1) + .flattenAsObservable(new Function<Object, Iterable<Integer>>() { + @Override + public Iterable<Integer> apply(Object v) throws Exception { + Integer[] array = new Integer[1000 * 1000]; + Arrays.fill(array, 1); + return Arrays.asList(array); + } + }) + .observeOn(Schedulers.single()) + .take(500 * 1000) + .to(TestHelper.<Integer>testConsumer()) + .awaitDone(5, TimeUnit.SECONDS) + .assertSubscribed() + .assertValueCount(500 * 1000) + .assertNoErrors() + .assertComplete(); + } + + @Test + public void fusedEmptyCheck() { + Maybe.just(1) + .flattenAsObservable(new Function<Object, Iterable<Integer>>() { + @Override + public Iterable<Integer> apply(Object v) throws Exception { + return Arrays.asList(1, 2, 3); + } + }).subscribe(new Observer<Integer>() { + QueueDisposable<Integer> qd; + @SuppressWarnings("unchecked") + @Override + public void onSubscribe(Disposable d) { + qd = (QueueDisposable<Integer>)d; + + assertEquals(QueueFuseable.ASYNC, qd.requestFusion(QueueFuseable.ANY)); + } + + @Override + public void onNext(Integer value) { + assertFalse(qd.isEmpty()); + + qd.clear(); + + assertTrue(qd.isEmpty()); + + qd.dispose(); + } + + @Override + public void onError(Throwable e) { + } + + @Override + public void onComplete() { + } + }); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeFlatMapNotificationTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeFlatMapNotificationTest.java new file mode 100644 index 0000000000..1d4d4f0d5e --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeFlatMapNotificationTest.java @@ -0,0 +1,101 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.maybe; + +import java.util.List; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.testsupport.*; + +public class MaybeFlatMapNotificationTest extends RxJavaTest { + + @Test + public void dispose() { + TestHelper.checkDisposed(Maybe.just(1) + .flatMap(Functions.justFunction(Maybe.just(1)), + Functions.justFunction(Maybe.just(1)), Functions.justSupplier(Maybe.just(1)))); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeMaybe(new Function<Maybe<Integer>, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Maybe<Integer> m) throws Exception { + return m + .flatMap(Functions.justFunction(Maybe.just(1)), + Functions.justFunction(Maybe.just(1)), Functions.justSupplier(Maybe.just(1))); + } + }); + } + + @Test + public void onSuccessNull() { + Maybe.just(1) + .flatMap(Functions.justFunction((Maybe<Integer>)null), + Functions.justFunction(Maybe.just(1)), + Functions.justSupplier(Maybe.just(1))) + .test() + .assertFailure(NullPointerException.class); + } + + @Test + public void onErrorNull() { + TestObserverEx<Integer> to = Maybe.<Integer>error(new TestException()) + .flatMap(Functions.justFunction(Maybe.just(1)), + Functions.justFunction((Maybe<Integer>)null), + Functions.justSupplier(Maybe.just(1))) + .to(TestHelper.<Integer>testConsumer()) + .assertFailure(CompositeException.class); + + List<Throwable> ce = TestHelper.compositeList(to.errors().get(0)); + + TestHelper.assertError(ce, 0, TestException.class); + TestHelper.assertError(ce, 1, NullPointerException.class); + } + + @Test + public void onCompleteNull() { + Maybe.<Integer>empty() + .flatMap(Functions.justFunction(Maybe.just(1)), + Functions.justFunction(Maybe.just(1)), + Functions.justSupplier((Maybe<Integer>)null)) + .test() + .assertFailure(NullPointerException.class); + } + + @Test + public void onSuccessEmpty() { + Maybe.just(1) + .flatMap(Functions.justFunction(Maybe.<Integer>empty()), + Functions.justFunction(Maybe.just(1)), + Functions.justSupplier(Maybe.just(1))) + .test() + .assertResult(); + } + + @Test + public void onSuccessError() { + Maybe.just(1) + .flatMap(Functions.justFunction(Maybe.<Integer>error(new TestException())), + Functions.justFunction((Maybe<Integer>)null), + Functions.justSupplier(Maybe.just(1))) + .test() + .assertFailure(TestException.class); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeFlatMapSingleElementTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeFlatMapSingleElementTest.java new file mode 100644 index 0000000000..2f5cf42426 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeFlatMapSingleElementTest.java @@ -0,0 +1,142 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.maybe; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class MaybeFlatMapSingleElementTest extends RxJavaTest { + @Test + public void flatMapSingleValue() { + Maybe.just(1).flatMapSingle(new Function<Integer, SingleSource<Integer>>() { + @Override public SingleSource<Integer> apply(final Integer integer) throws Exception { + if (integer == 1) { + return Single.just(2); + } + + return Single.just(1); + } + }) + .test() + .assertResult(2); + } + + @Test + public void flatMapSingleValueDifferentType() { + Maybe.just(1).flatMapSingle(new Function<Integer, SingleSource<String>>() { + @Override public SingleSource<String> apply(final Integer integer) throws Exception { + if (integer == 1) { + return Single.just("2"); + } + + return Single.just("1"); + } + }) + .test() + .assertResult("2"); + } + + @Test + public void flatMapSingleValueNull() { + Maybe.just(1).flatMapSingle(new Function<Integer, SingleSource<Integer>>() { + @Override public SingleSource<Integer> apply(final Integer integer) throws Exception { + return null; + } + }) + .to(TestHelper.<Integer>testConsumer()) + .assertNoValues() + .assertError(NullPointerException.class) + .assertErrorMessage("The mapper returned a null SingleSource"); + } + + @Test + public void flatMapSingleValueErrorThrown() { + Maybe.just(1).flatMapSingle(new Function<Integer, SingleSource<Integer>>() { + @Override public SingleSource<Integer> apply(final Integer integer) throws Exception { + throw new RuntimeException("something went terribly wrong!"); + } + }) + .to(TestHelper.<Integer>testConsumer()) + .assertNoValues() + .assertError(RuntimeException.class) + .assertErrorMessage("something went terribly wrong!"); + } + + @Test + public void flatMapSingleError() { + RuntimeException exception = new RuntimeException("test"); + + Maybe.error(exception).flatMapSingle(new Function<Object, SingleSource<Object>>() { + @Override public SingleSource<Object> apply(final Object integer) throws Exception { + return Single.just(new Object()); + } + }) + .test() + .assertError(exception); + } + + @Test + public void flatMapSingleEmpty() { + Maybe.<Integer>empty().flatMapSingle(new Function<Integer, SingleSource<Integer>>() { + @Override public SingleSource<Integer> apply(final Integer integer) throws Exception { + return Single.just(2); + } + }) + .test() + .assertNoValues() + .assertResult(); + } + + @Test + public void dispose() { + TestHelper.checkDisposed(Maybe.just(1).flatMapSingle(new Function<Integer, SingleSource<Integer>>() { + @Override + public SingleSource<Integer> apply(final Integer integer) throws Exception { + return Single.just(2); + } + })); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeMaybe(new Function<Maybe<Integer>, Maybe<Integer>>() { + @Override + public Maybe<Integer> apply(Maybe<Integer> m) throws Exception { + return m.flatMapSingle(new Function<Integer, SingleSource<Integer>>() { + @Override + public SingleSource<Integer> apply(final Integer integer) throws Exception { + return Single.just(2); + } + }); + } + }); + } + + @Test + public void singleErrors() { + Maybe.just(1) + .flatMapSingle(new Function<Integer, SingleSource<Integer>>() { + @Override + public SingleSource<Integer> apply(final Integer integer) throws Exception { + return Single.error(new TestException()); + } + }) + .test() + .assertFailure(TestException.class); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeFlatMapSingleTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeFlatMapSingleTest.java new file mode 100644 index 0000000000..cdf7f36f1f --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeFlatMapSingleTest.java @@ -0,0 +1,151 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.maybe; + +import java.util.NoSuchElementException; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class MaybeFlatMapSingleTest extends RxJavaTest { + @Test + public void flatMapSingleValue() { + Maybe.just(1).flatMapSingle(new Function<Integer, SingleSource<Integer>>() { + @Override public SingleSource<Integer> apply(final Integer integer) throws Exception { + if (integer == 1) { + return Single.just(2); + } + + return Single.just(1); + } + }) + .toSingle() + .test() + .assertResult(2); + } + + @Test + public void flatMapSingleValueDifferentType() { + Maybe.just(1).flatMapSingle(new Function<Integer, SingleSource<String>>() { + @Override public SingleSource<String> apply(final Integer integer) throws Exception { + if (integer == 1) { + return Single.just("2"); + } + + return Single.just("1"); + } + }) + .toSingle() + .test() + .assertResult("2"); + } + + @Test + public void flatMapSingleValueNull() { + Maybe.just(1).flatMapSingle(new Function<Integer, SingleSource<Integer>>() { + @Override public SingleSource<Integer> apply(final Integer integer) throws Exception { + return null; + } + }) + .toSingle() + .to(TestHelper.<Integer>testConsumer()) + .assertNoValues() + .assertError(NullPointerException.class) + .assertErrorMessage("The mapper returned a null SingleSource"); + } + + @Test + public void flatMapSingleValueErrorThrown() { + Maybe.just(1).flatMapSingle(new Function<Integer, SingleSource<Integer>>() { + @Override public SingleSource<Integer> apply(final Integer integer) throws Exception { + throw new RuntimeException("something went terribly wrong!"); + } + }) + .toSingle() + .to(TestHelper.<Integer>testConsumer()) + .assertNoValues() + .assertError(RuntimeException.class) + .assertErrorMessage("something went terribly wrong!"); + } + + @Test + public void flatMapSingleError() { + RuntimeException exception = new RuntimeException("test"); + + Maybe.error(exception).flatMapSingle(new Function<Object, SingleSource<Object>>() { + @Override public SingleSource<Object> apply(final Object integer) throws Exception { + return Single.just(new Object()); + } + }) + .toSingle() + .test() + .assertError(exception); + } + + @Test + public void flatMapSingleEmpty() { + Maybe.<Integer>empty().flatMapSingle(new Function<Integer, SingleSource<Integer>>() { + @Override public SingleSource<Integer> apply(final Integer integer) throws Exception { + return Single.just(2); + } + }) + .toSingle() + .test() + .assertNoValues() + .assertError(NoSuchElementException.class); + } + + @Test + public void dispose() { + TestHelper.checkDisposed(Maybe.just(1).flatMapSingle(new Function<Integer, SingleSource<Integer>>() { + @Override + public SingleSource<Integer> apply(final Integer integer) throws Exception { + return Single.just(2); + } + }).toSingle()); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeMaybeToSingle(new Function<Maybe<Integer>, SingleSource<Integer>>() { + @Override + public SingleSource<Integer> apply(Maybe<Integer> m) throws Exception { + return m.flatMapSingle(new Function<Integer, SingleSource<Integer>>() { + @Override + public SingleSource<Integer> apply(final Integer integer) throws Exception { + return Single.just(2); + } + }).toSingle(); + } + }); + } + + @Test + public void singleErrors() { + Maybe.just(1) + .flatMapSingle(new Function<Integer, SingleSource<Integer>>() { + @Override + public SingleSource<Integer> apply(final Integer integer) throws Exception { + return Single.error(new TestException()); + } + }) + .toSingle() + .test() + .assertFailure(TestException.class); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeFlattenTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeFlattenTest.java new file mode 100644 index 0000000000..30c0cfdeb8 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeFlattenTest.java @@ -0,0 +1,101 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.maybe; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class MaybeFlattenTest extends RxJavaTest { + + @Test + public void dispose() { + TestHelper.checkDisposed(Maybe.just(1).flatMap(new Function<Integer, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Integer v) throws Exception { + return Maybe.just(2); + } + })); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeMaybe(new Function<Maybe<Integer>, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Maybe<Integer> v) throws Exception { + return v.flatMap(new Function<Integer, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Integer v) throws Exception { + return Maybe.just(2); + } + }); + } + }); + } + + @Test + public void mainError() { + Maybe.<Integer>error(new TestException()) + .flatMap(new Function<Integer, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Integer v) throws Exception { + return Maybe.just(2); + } + }) + .test() + .assertFailure(TestException.class); + } + + @Test + public void mainEmpty() { + Maybe.<Integer>empty() + .flatMap(new Function<Integer, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Integer v) throws Exception { + return Maybe.just(2); + } + }) + .test() + .assertResult(); + } + + @Test + public void mapperThrows() { + Maybe.just(1) + .flatMap(new Function<Integer, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Integer v) throws Exception { + throw new TestException(); + } + }) + .test() + .assertFailure(TestException.class); + } + + @Test + public void mapperReturnsNull() { + Maybe.just(1) + .flatMap(new Function<Integer, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Integer v) throws Exception { + return null; + } + }) + .test() + .assertFailure(NullPointerException.class); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeFromActionTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeFromActionTest.java new file mode 100644 index 0000000000..32f37c1cd7 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeFromActionTest.java @@ -0,0 +1,181 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.maybe; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +import java.util.List; +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.schedulers.Schedulers; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class MaybeFromActionTest extends RxJavaTest { + @Test + public void fromAction() { + final AtomicInteger atomicInteger = new AtomicInteger(); + + Maybe.fromAction(new Action() { + @Override + public void run() throws Exception { + atomicInteger.incrementAndGet(); + } + }) + .test() + .assertResult(); + + assertEquals(1, atomicInteger.get()); + } + + @Test + public void fromActionTwice() { + final AtomicInteger atomicInteger = new AtomicInteger(); + + Action run = new Action() { + @Override + public void run() throws Exception { + atomicInteger.incrementAndGet(); + } + }; + + Maybe.fromAction(run) + .test() + .assertResult(); + + assertEquals(1, atomicInteger.get()); + + Maybe.fromAction(run) + .test() + .assertResult(); + + assertEquals(2, atomicInteger.get()); + } + + @Test + public void fromActionInvokesLazy() { + final AtomicInteger atomicInteger = new AtomicInteger(); + + Maybe<Object> maybe = Maybe.fromAction(new Action() { + @Override + public void run() throws Exception { + atomicInteger.incrementAndGet(); + } + }); + + assertEquals(0, atomicInteger.get()); + + maybe + .test() + .assertResult(); + + assertEquals(1, atomicInteger.get()); + } + + @Test + public void fromActionThrows() { + Maybe.fromAction(new Action() { + @Override + public void run() throws Exception { + throw new UnsupportedOperationException(); + } + }) + .test() + .assertFailure(UnsupportedOperationException.class); + } + + @SuppressWarnings("unchecked") + @Test + public void callable() throws Throwable { + final int[] counter = { 0 }; + + Maybe<Void> m = Maybe.fromAction(new Action() { + @Override + public void run() throws Exception { + counter[0]++; + } + }); + + assertTrue(m.getClass().toString(), m instanceof Supplier); + + assertNull(((Supplier<Void>)m).get()); + + assertEquals(1, counter[0]); + } + + @Test + public void noErrorLoss() throws Exception { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + final CountDownLatch cdl1 = new CountDownLatch(1); + final CountDownLatch cdl2 = new CountDownLatch(1); + + TestObserver<Object> to = Maybe.fromAction(new Action() { + @Override + public void run() throws Exception { + cdl1.countDown(); + cdl2.await(5, TimeUnit.SECONDS); + } + }).subscribeOn(Schedulers.single()).test(); + + assertTrue(cdl1.await(5, TimeUnit.SECONDS)); + + to.dispose(); + + int timeout = 10; + + while (timeout-- > 0 && errors.isEmpty()) { + Thread.sleep(100); + } + + TestHelper.assertUndeliverable(errors, 0, InterruptedException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void disposedUpfront() throws Throwable { + Action run = mock(Action.class); + + Maybe.fromAction(run) + .test(true) + .assertEmpty(); + + verify(run, never()).run(); + } + + @Test + public void cancelWhileRunning() { + final TestObserver<Object> to = new TestObserver<>(); + + Maybe.fromAction(new Action() { + @Override + public void run() throws Exception { + to.dispose(); + } + }) + .subscribeWith(to) + .assertEmpty(); + + assertTrue(to.isDisposed()); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeFromCallableTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeFromCallableTest.java new file mode 100644 index 0000000000..dec0d8e33e --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeFromCallableTest.java @@ -0,0 +1,230 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.maybe; + +import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import java.util.List; +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.Test; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.functions.Supplier; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.schedulers.Schedulers; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class MaybeFromCallableTest extends RxJavaTest { + @Test + public void fromCallable() { + final AtomicInteger atomicInteger = new AtomicInteger(); + + Maybe.fromCallable(new Callable<Object>() { + @Override + public Object call() throws Exception { + atomicInteger.incrementAndGet(); + return null; + } + }) + .test() + .assertResult(); + + assertEquals(1, atomicInteger.get()); + } + + @Test + public void fromCallableTwice() { + final AtomicInteger atomicInteger = new AtomicInteger(); + + Callable<Object> callable = new Callable<Object>() { + @Override + public Object call() throws Exception { + atomicInteger.incrementAndGet(); + return null; + } + }; + + Maybe.fromCallable(callable) + .test() + .assertResult(); + + assertEquals(1, atomicInteger.get()); + + Maybe.fromCallable(callable) + .test() + .assertResult(); + + assertEquals(2, atomicInteger.get()); + } + + @Test + public void fromCallableInvokesLazy() { + final AtomicInteger atomicInteger = new AtomicInteger(); + + Maybe<Object> completable = Maybe.fromCallable(new Callable<Object>() { + @Override + public Object call() throws Exception { + atomicInteger.incrementAndGet(); + return null; + } + }); + + assertEquals(0, atomicInteger.get()); + + completable + .test() + .assertResult(); + + assertEquals(1, atomicInteger.get()); + } + + @Test + public void fromCallableThrows() { + Maybe.fromCallable(new Callable<Object>() { + @Override + public Object call() throws Exception { + throw new UnsupportedOperationException(); + } + }) + .test() + .assertFailure(UnsupportedOperationException.class); + } + + @SuppressWarnings("unchecked") + @Test + public void callable() throws Throwable { + final int[] counter = { 0 }; + + Maybe<Integer> m = Maybe.fromCallable(new Callable<Integer>() { + @Override + public Integer call() throws Exception { + counter[0]++; + return 0; + } + }); + + assertTrue(m.getClass().toString(), m instanceof Supplier); + + assertEquals(0, ((Supplier<Void>)m).get()); + + assertEquals(1, counter[0]); + } + + @Test + public void noErrorLoss() throws Exception { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + final CountDownLatch cdl1 = new CountDownLatch(1); + final CountDownLatch cdl2 = new CountDownLatch(1); + + TestObserver<Integer> to = Maybe.fromCallable(new Callable<Integer>() { + @Override + public Integer call() throws Exception { + cdl1.countDown(); + cdl2.await(5, TimeUnit.SECONDS); + return 1; + } + }).subscribeOn(Schedulers.single()).test(); + + assertTrue(cdl1.await(5, TimeUnit.SECONDS)); + + to.dispose(); + + int timeout = 10; + + while (timeout-- > 0 && errors.isEmpty()) { + Thread.sleep(100); + } + + TestHelper.assertUndeliverable(errors, 0, InterruptedException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @SuppressWarnings("unchecked") + @Test + public void shouldNotDeliverResultIfSubscriberUnsubscribedBeforeEmission() throws Exception { + Callable<String> func = mock(Callable.class); + + final CountDownLatch funcLatch = new CountDownLatch(1); + final CountDownLatch observerLatch = new CountDownLatch(1); + + when(func.call()).thenAnswer(new Answer<String>() { + @Override + public String answer(InvocationOnMock invocation) throws Throwable { + observerLatch.countDown(); + + try { + funcLatch.await(); + } catch (InterruptedException e) { + // It's okay, unsubscription causes Thread interruption + + // Restoring interruption status of the Thread + Thread.currentThread().interrupt(); + } + + return "should_not_be_delivered"; + } + }); + + Maybe<String> fromCallableObservable = Maybe.fromCallable(func); + + Observer<Object> observer = TestHelper.mockObserver(); + + TestObserver<String> outer = new TestObserver<>(observer); + + fromCallableObservable + .subscribeOn(Schedulers.computation()) + .subscribe(outer); + + // Wait until func will be invoked + observerLatch.await(); + + // Unsubscribing before emission + outer.dispose(); + + // Emitting result + funcLatch.countDown(); + + // func must be invoked + verify(func).call(); + + // Observer must not be notified at all + verify(observer).onSubscribe(any(Disposable.class)); + verifyNoMoreInteractions(observer); + } + + @Test + public void disposeUpfront() { + Maybe.fromCallable(() -> 1) + .test(true) + .assertEmpty(); + } + + @Test + public void success() { + Maybe.fromCallable(() -> 1) + .test() + .assertResult(1); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeFromCompletableTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeFromCompletableTest.java new file mode 100644 index 0000000000..40f00e371a --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeFromCompletableTest.java @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.maybe; + +import static org.junit.Assert.assertSame; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.fuseable.HasUpstreamCompletableSource; +import io.reactivex.rxjava3.processors.PublishProcessor; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class MaybeFromCompletableTest extends RxJavaTest { + @Test + public void fromCompletable() { + Maybe.fromCompletable(Completable.complete()) + .test() + .assertResult(); + } + + @Test + public void fromCompletableError() { + Maybe.fromCompletable(Completable.error(new UnsupportedOperationException())) + .test() + .assertFailure(UnsupportedOperationException.class); + } + + @Test + public void source() { + Completable c = Completable.complete(); + + assertSame(c, ((HasUpstreamCompletableSource)Maybe.fromCompletable(c)).source()); + } + + @Test + public void dispose() { + TestHelper.checkDisposed(Maybe.fromCompletable(PublishProcessor.create().ignoreElements())); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeCompletableToMaybe(new Function<Completable, MaybeSource<Object>>() { + @Override + public MaybeSource<Object> apply(Completable v) throws Exception { + return Maybe.fromCompletable(v); + } + }); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeFromFutureTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeFromFutureTest.java new file mode 100644 index 0000000000..fb88c60a72 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeFromFutureTest.java @@ -0,0 +1,122 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.maybe; + +import static org.junit.Assert.assertTrue; + +import java.util.concurrent.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.schedulers.Schedulers; + +public class MaybeFromFutureTest extends RxJavaTest { + + @Test + public void cancelImmediately() { + FutureTask<Integer> ft = new FutureTask<>(Functions.justCallable(1)); + + Maybe.fromFuture(ft).test(true) + .assertEmpty(); + } + + @Test + public void timeout() { + FutureTask<Integer> ft = new FutureTask<>(Functions.justCallable(1)); + + Maybe.fromFuture(ft, 1, TimeUnit.MILLISECONDS).test() + .awaitDone(5, TimeUnit.SECONDS) + .assertFailure(TimeoutException.class); + } + + @Test + public void timedWait() { + FutureTask<Integer> ft = new FutureTask<>(Functions.justCallable(1)); + ft.run(); + + Maybe.fromFuture(ft, 1, TimeUnit.MILLISECONDS).test() + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(1); + } + + @Test + public void interrupt() { + FutureTask<Integer> ft = new FutureTask<>(Functions.justCallable(1)); + + Thread.currentThread().interrupt(); + + Maybe.fromFuture(ft, 1, TimeUnit.MILLISECONDS).test() + .assertFailure(InterruptedException.class); + } + + @Test + public void cancelWhileRunning() { + final TestObserver<Object> to = new TestObserver<>(); + + FutureTask<Object> ft = new FutureTask<>(new Runnable() { + @Override + public void run() { + to.dispose(); + } + }, null); + + Schedulers.single().scheduleDirect(ft, 100, TimeUnit.MILLISECONDS); + + Maybe.fromFuture(ft) + .subscribeWith(to) + .assertEmpty(); + + assertTrue(to.isDisposed()); + } + + @Test + public void cancelAndCrashWhileRunning() { + final TestObserver<Object> to = new TestObserver<>(); + + FutureTask<Object> ft = new FutureTask<>(new Runnable() { + @Override + public void run() { + to.dispose(); + throw new TestException(); + } + }, null); + + Schedulers.single().scheduleDirect(ft, 100, TimeUnit.MILLISECONDS); + + Maybe.fromFuture(ft) + .subscribeWith(to) + .assertEmpty(); + + assertTrue(to.isDisposed()); + } + + @Test + public void futureNull() { + FutureTask<Object> ft = new FutureTask<>(new Runnable() { + @Override + public void run() { + } + }, null); + + Schedulers.single().scheduleDirect(ft, 100, TimeUnit.MILLISECONDS); + + Maybe.fromFuture(ft) + .test() + .assertResult(); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeFromObservableTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeFromObservableTest.java new file mode 100644 index 0000000000..311f33e4d4 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeFromObservableTest.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.maybe; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; + +public class MaybeFromObservableTest extends RxJavaTest { + + @Test + public void empty() { + Maybe.fromObservable(Observable.empty().hide()) + .test() + .assertResult(); + } + + @Test + public void just() { + Maybe.fromObservable(Observable.just(1).hide()) + .test() + .assertResult(1); + } + + @Test + public void range() { + Maybe.fromObservable(Observable.range(1, 5).hide()) + .test() + .assertResult(1); + } + + @Test + public void error() { + Maybe.fromObservable(Observable.error(new TestException()).hide()) + .test() + .assertFailure(TestException.class); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeFromPubisherTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeFromPubisherTest.java new file mode 100644 index 0000000000..c51c23bae4 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeFromPubisherTest.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.maybe; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; + +public class MaybeFromPubisherTest extends RxJavaTest { + + @Test + public void empty() { + Maybe.fromPublisher(Flowable.empty().hide()) + .test() + .assertResult(); + } + + @Test + public void just() { + Maybe.fromPublisher(Flowable.just(1).hide()) + .test() + .assertResult(1); + } + + @Test + public void range() { + Maybe.fromPublisher(Flowable.range(1, 5).hide()) + .test() + .assertResult(1); + } + + @Test + public void error() { + Maybe.fromPublisher(Flowable.error(new TestException()).hide()) + .test() + .assertFailure(TestException.class); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeFromRunnableTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeFromRunnableTest.java new file mode 100644 index 0000000000..5d1db4ad4d --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeFromRunnableTest.java @@ -0,0 +1,186 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.maybe; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +import java.util.List; +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.functions.Supplier; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.schedulers.Schedulers; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class MaybeFromRunnableTest extends RxJavaTest { + @Test + public void fromRunnable() { + final AtomicInteger atomicInteger = new AtomicInteger(); + + Maybe.fromRunnable(new Runnable() { + @Override + public void run() { + atomicInteger.incrementAndGet(); + } + }) + .test() + .assertResult(); + + assertEquals(1, atomicInteger.get()); + } + + @Test + public void fromRunnableTwice() { + final AtomicInteger atomicInteger = new AtomicInteger(); + + Runnable run = new Runnable() { + @Override + public void run() { + atomicInteger.incrementAndGet(); + } + }; + + Maybe.fromRunnable(run) + .test() + .assertResult(); + + assertEquals(1, atomicInteger.get()); + + Maybe.fromRunnable(run) + .test() + .assertResult(); + + assertEquals(2, atomicInteger.get()); + } + + @Test + public void fromRunnableInvokesLazy() { + final AtomicInteger atomicInteger = new AtomicInteger(); + + final Maybe<Object> maybe = Maybe.fromRunnable(new Runnable() { + @Override public void run() { + atomicInteger.incrementAndGet(); + } + }); + + assertEquals(0, atomicInteger.get()); + + maybe + .test() + .assertResult(); + + assertEquals(1, atomicInteger.get()); + } + + @Test + public void fromRunnableThrows() { + Maybe.fromRunnable(new Runnable() { + @Override + public void run() { + throw new UnsupportedOperationException(); + } + }) + .test() + .assertFailure(UnsupportedOperationException.class); + } + + @SuppressWarnings("unchecked") + @Test + public void callable() throws Throwable { + final int[] counter = { 0 }; + + Maybe<Void> m = Maybe.fromRunnable(new Runnable() { + @Override + public void run() { + counter[0]++; + } + }); + + assertTrue(m.getClass().toString(), m instanceof Supplier); + + assertNull(((Supplier<Void>)m).get()); + + assertEquals(1, counter[0]); + } + + @Test + public void noErrorLoss() throws Exception { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + final CountDownLatch cdl1 = new CountDownLatch(1); + final CountDownLatch cdl2 = new CountDownLatch(1); + + TestObserver<Object> to = Maybe.fromRunnable(new Runnable() { + @Override + public void run() { + cdl1.countDown(); + try { + cdl2.await(5, TimeUnit.SECONDS); + } catch (InterruptedException ex) { + throw new RuntimeException(ex); + } + } + }).subscribeOn(Schedulers.single()).test(); + + assertTrue(cdl1.await(5, TimeUnit.SECONDS)); + + to.dispose(); + + int timeout = 10; + + while (timeout-- > 0 && errors.isEmpty()) { + Thread.sleep(100); + } + + TestHelper.assertUndeliverable(errors, 0, RuntimeException.class); + + assertTrue(errors.get(0).toString(), errors.get(0).getCause().getCause() instanceof InterruptedException); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void disposedUpfront() { + Runnable run = mock(Runnable.class); + + Maybe.fromRunnable(run) + .test(true) + .assertEmpty(); + + verify(run, never()).run(); + } + + @Test + public void cancelWhileRunning() { + final TestObserver<Object> to = new TestObserver<>(); + + Maybe.fromRunnable(new Runnable() { + @Override + public void run() { + to.dispose(); + } + }) + .subscribeWith(to) + .assertEmpty(); + + assertTrue(to.isDisposed()); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeFromSingleTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeFromSingleTest.java new file mode 100644 index 0000000000..27c13626f6 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeFromSingleTest.java @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.maybe; + +import static org.junit.Assert.assertSame; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.fuseable.HasUpstreamSingleSource; +import io.reactivex.rxjava3.processors.PublishProcessor; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class MaybeFromSingleTest extends RxJavaTest { + @Test + public void fromSingle() { + Maybe.fromSingle(Single.just(1)) + .test() + .assertResult(1); + } + + @Test + public void fromSingleThrows() { + Maybe.fromSingle(Single.error(new UnsupportedOperationException())) + .test() + .assertFailure(UnsupportedOperationException.class); + } + + @Test + public void source() { + Single<Integer> c = Single.never(); + + assertSame(c, ((HasUpstreamSingleSource<?>)Maybe.fromSingle(c)).source()); + } + + @Test + public void dispose() { + TestHelper.checkDisposed(Maybe.fromSingle(PublishProcessor.create().singleOrError())); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeSingleToMaybe(new Function<Single<Object>, MaybeSource<Object>>() { + @Override + public MaybeSource<Object> apply(Single<Object> v) throws Exception { + return Maybe.fromSingle(v); + } + }); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeFromSupplierTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeFromSupplierTest.java new file mode 100644 index 0000000000..bcbe0783cc --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeFromSupplierTest.java @@ -0,0 +1,236 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.maybe; + +import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import java.util.List; +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.Test; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.functions.Supplier; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.schedulers.Schedulers; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class MaybeFromSupplierTest extends RxJavaTest { + + @Test + public void fromSupplier() { + final AtomicInteger atomicInteger = new AtomicInteger(); + + Maybe.fromSupplier(new Supplier<Object>() { + @Override + public Object get() throws Exception { + atomicInteger.incrementAndGet(); + return null; + } + }) + .test() + .assertResult(); + + assertEquals(1, atomicInteger.get()); + } + + @Test + public void fromSupplierTwice() { + final AtomicInteger atomicInteger = new AtomicInteger(); + + Supplier<Object> supplier = new Supplier<Object>() { + @Override + public Object get() throws Exception { + atomicInteger.incrementAndGet(); + return null; + } + }; + + Maybe.fromSupplier(supplier) + .test() + .assertResult(); + + assertEquals(1, atomicInteger.get()); + + Maybe.fromSupplier(supplier) + .test() + .assertResult(); + + assertEquals(2, atomicInteger.get()); + } + + @Test + public void fromSupplierInvokesLazy() { + final AtomicInteger atomicInteger = new AtomicInteger(); + + Maybe<Object> completable = Maybe.fromSupplier(new Supplier<Object>() { + @Override + public Object get() throws Exception { + atomicInteger.incrementAndGet(); + return null; + } + }); + + assertEquals(0, atomicInteger.get()); + + completable + .test() + .assertResult(); + + assertEquals(1, atomicInteger.get()); + } + + @Test + public void fromSupplierThrows() { + Maybe.fromSupplier(new Supplier<Object>() { + @Override + public Object get() throws Exception { + throw new UnsupportedOperationException(); + } + }) + .test() + .assertFailure(UnsupportedOperationException.class); + } + + @SuppressWarnings("unchecked") + @Test + public void supplier() throws Throwable { + final int[] counter = { 0 }; + + Maybe<Integer> m = Maybe.fromSupplier(new Supplier<Integer>() { + @Override + public Integer get() throws Exception { + counter[0]++; + return 0; + } + }); + + assertTrue(m.getClass().toString(), m instanceof Supplier); + + assertEquals(0, ((Supplier<Void>)m).get()); + + assertEquals(1, counter[0]); + } + + @Test + public void noErrorLoss() throws Exception { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + final CountDownLatch cdl1 = new CountDownLatch(1); + final CountDownLatch cdl2 = new CountDownLatch(1); + + TestObserver<Integer> to = Maybe.fromSupplier(new Supplier<Integer>() { + @Override + public Integer get() throws Exception { + cdl1.countDown(); + cdl2.await(5, TimeUnit.SECONDS); + return 1; + } + }).subscribeOn(Schedulers.single()).test(); + + assertTrue(cdl1.await(5, TimeUnit.SECONDS)); + + to.dispose(); + + int timeout = 10; + + while (timeout-- > 0 && errors.isEmpty()) { + Thread.sleep(100); + } + + TestHelper.assertUndeliverable(errors, 0, InterruptedException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @SuppressWarnings("unchecked") + @Test + public void shouldNotDeliverResultIfSubscriberUnsubscribedBeforeEmission() throws Throwable { + Supplier<String> func = mock(Supplier.class); + + final CountDownLatch funcLatch = new CountDownLatch(1); + final CountDownLatch observerLatch = new CountDownLatch(1); + + when(func.get()).thenAnswer(new Answer<String>() { + @Override + public String answer(InvocationOnMock invocation) throws Throwable { + observerLatch.countDown(); + + try { + funcLatch.await(); + } catch (InterruptedException e) { + // It's okay, unsubscription causes Thread interruption + + // Restoring interruption status of the Thread + Thread.currentThread().interrupt(); + } + + return "should_not_be_delivered"; + } + }); + + Maybe<String> fromSupplierObservable = Maybe.fromSupplier(func); + + Observer<Object> observer = TestHelper.mockObserver(); + + TestObserver<String> outer = new TestObserver<>(observer); + + fromSupplierObservable + .subscribeOn(Schedulers.computation()) + .subscribe(outer); + + // Wait until func will be invoked + observerLatch.await(); + + // Unsubscribing before emission + outer.dispose(); + + // Emitting result + funcLatch.countDown(); + + // func must be invoked + verify(func).get(); + + // Observer must not be notified at all + verify(observer).onSubscribe(any(Disposable.class)); + verifyNoMoreInteractions(observer); + } + + @Test + public void success() { + Maybe.fromSupplier(() -> 1) + .test() + .assertResult(1); + } + + @Test + public void disposeUpfront() throws Throwable { + @SuppressWarnings("unchecked") + Supplier<Integer> supplier = mock(Supplier.class); + + Maybe.fromSupplier(supplier) + .test(true) + .assertEmpty(); + + verify(supplier, never()).get(); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeHideTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeHideTest.java new file mode 100644 index 0000000000..11a1cbb145 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeHideTest.java @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.maybe; + +import static org.junit.Assert.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.operators.ScalarSupplier; +import io.reactivex.rxjava3.processors.PublishProcessor; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class MaybeHideTest extends RxJavaTest { + + @Test + public void normal() { + Maybe.just(1) + .hide() + .test() + .assertResult(1); + } + + @Test + public void empty() { + Maybe.empty() + .hide() + .test() + .assertResult(); + } + + @Test + public void error() { + Maybe.error(new TestException()) + .hide() + .test() + .assertFailure(TestException.class); + } + + @Test + public void hidden() { + assertTrue(Maybe.just(1) instanceof ScalarSupplier); + + assertFalse(Maybe.just(1).hide() instanceof ScalarSupplier); + } + + @Test + public void dispose() { + TestHelper.checkDisposedMaybe(new Function<Maybe<Object>, MaybeSource<Object>>() { + @Override + public MaybeSource<Object> apply(Maybe<Object> m) throws Exception { + return m.hide(); + } + }); + } + + @Test + public void isDisposed() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + + TestHelper.checkDisposed(pp.singleElement().hide()); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeMaybe(new Function<Maybe<Object>, Maybe<Object>>() { + @Override + public Maybe<Object> apply(Maybe<Object> f) throws Exception { + return f.hide(); + } + }); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeIgnoreElementTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeIgnoreElementTest.java new file mode 100644 index 0000000000..a7ab2f2024 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeIgnoreElementTest.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.maybe; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.processors.PublishProcessor; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class MaybeIgnoreElementTest extends RxJavaTest { + + @Test + public void dispose() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + + TestHelper.checkDisposed(pp.singleElement().ignoreElement().toMaybe()); + } + + @Test + public void dispose2() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + + TestHelper.checkDisposed(pp.singleElement().ignoreElement()); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeMaybe(new Function<Maybe<Object>, MaybeSource<Object>>() { + @Override + public MaybeSource<Object> apply(Maybe<Object> v) throws Exception { + return v.ignoreElement().toMaybe(); + } + }); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeIsEmptySingleTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeIsEmptySingleTest.java new file mode 100644 index 0000000000..23ead59639 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeIsEmptySingleTest.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.maybe; + +import static org.junit.Assert.assertSame; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.internal.fuseable.HasUpstreamMaybeSource; + +public class MaybeIsEmptySingleTest extends RxJavaTest { + + @Test + public void source() { + Maybe<Integer> m = Maybe.just(1); + + assertSame(m, (((HasUpstreamMaybeSource<?>)m.isEmpty()).source())); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeIsEmptyTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeIsEmptyTest.java new file mode 100644 index 0000000000..a3a4a893c9 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeIsEmptyTest.java @@ -0,0 +1,139 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.maybe; + +import static org.junit.Assert.assertTrue; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.processors.PublishProcessor; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class MaybeIsEmptyTest extends RxJavaTest { + + @Test + public void normal() { + Maybe.just(1) + .isEmpty() + .test() + .assertResult(false); + } + + @Test + public void empty() { + Maybe.empty() + .isEmpty() + .test() + .assertResult(true); + } + + @Test + public void error() { + Maybe.error(new TestException()) + .isEmpty() + .test() + .assertFailure(TestException.class); + } + + @Test + public void fusedBackToMaybe() { + assertTrue(Maybe.just(1) + .isEmpty() + .toMaybe() instanceof MaybeIsEmpty); + } + + @Test + public void normalToMaybe() { + Maybe.just(1) + .isEmpty() + .toMaybe() + .test() + .assertResult(false); + } + + @Test + public void emptyToMaybe() { + Maybe.empty() + .isEmpty() + .toMaybe() + .test() + .assertResult(true); + } + + @Test + public void errorToMaybe() { + Maybe.error(new TestException()) + .isEmpty() + .toMaybe() + .test() + .assertFailure(TestException.class); + } + + @Test + public void dispose() { + TestHelper.checkDisposedMaybeToSingle(new Function<Maybe<Object>, SingleSource<Boolean>>() { + @Override + public SingleSource<Boolean> apply(Maybe<Object> m) throws Exception { + return m.isEmpty(); + } + }); + } + + @Test + public void isDisposed() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + + TestHelper.checkDisposed(pp.singleElement().isEmpty()); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeMaybeToSingle(new Function<Maybe<Object>, Single<Boolean>>() { + @Override + public Single<Boolean> apply(Maybe<Object> f) throws Exception { + return f.isEmpty(); + } + }); + } + + @Test + public void disposeToMaybe() { + TestHelper.checkDisposedMaybe(new Function<Maybe<Object>, Maybe<Boolean>>() { + @Override + public Maybe<Boolean> apply(Maybe<Object> m) throws Exception { + return m.isEmpty().toMaybe(); + } + }); + } + + @Test + public void isDisposedToMaybe() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + + TestHelper.checkDisposed(pp.singleElement().isEmpty().toMaybe()); + } + + @Test + public void doubleOnSubscribeToMaybe() { + TestHelper.checkDoubleOnSubscribeMaybe(new Function<Maybe<Object>, Maybe<Boolean>>() { + @Override + public Maybe<Boolean> apply(Maybe<Object> f) throws Exception { + return f.isEmpty().toMaybe(); + } + }); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeJustTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeJustTest.java new file mode 100644 index 0000000000..a49ac11a41 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeJustTest.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.maybe; + +import static org.junit.Assert.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.operators.ScalarSupplier; + +public class MaybeJustTest extends RxJavaTest { + + @SuppressWarnings("unchecked") + @Test + public void scalarSupplier() { + Maybe<Integer> m = Maybe.just(1); + + assertTrue(m.getClass().toString(), m instanceof ScalarSupplier); + + assertEquals(1, ((ScalarSupplier<Integer>)m).get().intValue()); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeMapTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeMapTest.java new file mode 100644 index 0000000000..b58b78b26f --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeMapTest.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.maybe; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class MaybeMapTest extends RxJavaTest { + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeMaybe(new Function<Maybe<Object>, MaybeSource<Object>>() { + @Override + public MaybeSource<Object> apply(Maybe<Object> m) throws Exception { + return m.map(Functions.identity()); + } + }); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeMaterializeTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeMaterializeTest.java new file mode 100644 index 0000000000..0c7f570434 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeMaterializeTest.java @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.maybe; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.subjects.MaybeSubject; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class MaybeMaterializeTest extends RxJavaTest { + + @Test + public void success() { + Maybe.just(1) + .materialize() + .test() + .assertResult(Notification.createOnNext(1)); + } + + @Test + public void error() { + TestException ex = new TestException(); + Maybe.error(ex) + .materialize() + .test() + .assertResult(Notification.createOnError(ex)); + } + + @Test + public void empty() { + Maybe.empty() + .materialize() + .test() + .assertResult(Notification.createOnComplete()); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeMaybeToSingle(new Function<Maybe<Object>, SingleSource<Notification<Object>>>() { + @Override + public SingleSource<Notification<Object>> apply(Maybe<Object> v) throws Exception { + return v.materialize(); + } + }); + } + + @Test + public void dispose() { + TestHelper.checkDisposed(MaybeSubject.create().materialize()); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeMergeArrayTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeMergeArrayTest.java new file mode 100644 index 0000000000..bf4f7313dc --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeMergeArrayTest.java @@ -0,0 +1,275 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.maybe; + +import static org.junit.Assert.*; + +import java.util.*; + +import org.junit.Test; +import org.reactivestreams.Subscription; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.internal.operators.maybe.MaybeMergeArray.MergeMaybeObserver; +import io.reactivex.rxjava3.operators.QueueFuseable; +import io.reactivex.rxjava3.operators.QueueSubscription; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.subjects.*; +import io.reactivex.rxjava3.subscribers.TestSubscriber; +import io.reactivex.rxjava3.testsupport.*; + +public class MaybeMergeArrayTest extends RxJavaTest { + + @Test + public void normal() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<Integer>().setInitialFusionMode(QueueFuseable.SYNC); + + Maybe.mergeArray(Maybe.just(1), Maybe.just(2)) + .subscribe(ts); + ts + .assertFuseable() + .assertFusionMode(QueueFuseable.NONE) + .assertResult(1, 2); + } + + @Test + public void fusedPollMixed() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<Integer>().setInitialFusionMode(QueueFuseable.ANY); + + Maybe.mergeArray(Maybe.just(1), Maybe.<Integer>empty(), Maybe.just(2)) + .subscribe(ts); + ts + .assertFuseable() + .assertFusionMode(QueueFuseable.ASYNC) + .assertResult(1, 2); + } + + @SuppressWarnings("unchecked") + @Test + public void fusedEmptyCheck() { + Maybe.mergeArray(Maybe.just(1), Maybe.<Integer>empty(), Maybe.just(2)) + .subscribe(new FlowableSubscriber<Integer>() { + QueueSubscription<Integer> qs; + @Override + public void onSubscribe(Subscription s) { + qs = (QueueSubscription<Integer>)s; + + assertEquals(QueueFuseable.ASYNC, qs.requestFusion(QueueFuseable.ANY)); + } + + @Override + public void onNext(Integer value) { + assertFalse(qs.isEmpty()); + + qs.clear(); + + assertTrue(qs.isEmpty()); + + qs.cancel(); + } + + @Override + public void onError(Throwable e) { + } + + @Override + public void onComplete() { + } + }); + } + + @Test + public void cancel() { + TestSubscriber<Integer> ts = new TestSubscriber<>(0L); + + Maybe.mergeArray(Maybe.just(1), Maybe.<Integer>empty(), Maybe.just(2)) + .subscribe(ts); + + ts.cancel(); + ts.request(10); + + ts.assertEmpty(); + } + + @Test + public void firstErrors() { + TestSubscriber<Integer> ts = new TestSubscriber<>(0L); + + Maybe.mergeArray(Maybe.<Integer>error(new TestException()), Maybe.<Integer>empty(), Maybe.just(2)) + .subscribe(ts); + + ts.assertFailure(TestException.class); + } + + @Test + public void errorFused() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<Integer>().setInitialFusionMode(QueueFuseable.ANY); + + Maybe.mergeArray(Maybe.<Integer>error(new TestException()), Maybe.just(2)) + .subscribe(ts); + ts + .assertFuseable() + .assertFusionMode(QueueFuseable.ASYNC) + .assertFailure(TestException.class); + } + + @Test + public void errorRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + List<Throwable> errors = TestHelper.trackPluginErrors(); + + try { + final PublishSubject<Integer> ps1 = PublishSubject.create(); + final PublishSubject<Integer> ps2 = PublishSubject.create(); + + TestSubscriber<Integer> ts = Maybe.mergeArray(ps1.singleElement(), ps2.singleElement()) + .test(); + + final TestException ex = new TestException(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + ps1.onError(ex); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + ps2.onError(ex); + } + }; + + TestHelper.race(r1, r2); + + ts.assertFailure(Throwable.class); + + if (!errors.isEmpty()) { + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } + } finally { + RxJavaPlugins.reset(); + } + } + } + + @Test + public void mergeBadSource() { + Maybe.mergeArray(new Maybe<Integer>() { + @Override + protected void subscribeActual(MaybeObserver<? super Integer> observer) { + observer.onSubscribe(Disposable.empty()); + observer.onSuccess(1); + observer.onSuccess(2); + observer.onSuccess(3); + } + }, Maybe.never()) + .test() + .assertResult(1, 2); + } + + @SuppressWarnings("unchecked") + @Test + public void smallOffer2Throws() { + Maybe.mergeArray(Maybe.never(), Maybe.never()) + .subscribe(new FlowableSubscriber<Object>() { + + @SuppressWarnings("rawtypes") + @Override + public void onSubscribe(Subscription s) { + MergeMaybeObserver o = (MergeMaybeObserver)s; + + try { + o.queue.offer(1, 2); + fail("Should have thrown"); + } catch (UnsupportedOperationException ex) { + // expected + } + } + + @Override + public void onNext(Object t) { + } + + @Override + public void onError(Throwable t) { + } + + @Override + public void onComplete() { + } + }); + } + + @SuppressWarnings("unchecked") + @Test + public void largeOffer2Throws() { + Maybe<Integer>[] a = new Maybe[1024]; + Arrays.fill(a, Maybe.never()); + Maybe.mergeArray(a) + .subscribe(new FlowableSubscriber<Object>() { + + @SuppressWarnings("rawtypes") + @Override + public void onSubscribe(Subscription s) { + MergeMaybeObserver o = (MergeMaybeObserver)s; + + try { + o.queue.offer(1, 2); + fail("Should have thrown"); + } catch (UnsupportedOperationException ex) { + // expected + } + + o.queue.drop(); + } + + @Override + public void onNext(Object t) { + } + + @Override + public void onError(Throwable t) { + } + + @Override + public void onComplete() { + } + }); + } + + @Test + public void badRequest() { + TestHelper.assertBadRequestReported( + Maybe.mergeArray(MaybeSubject.create(), MaybeSubject.create()) + ); + } + + @Test + public void cancel2() { + TestHelper.checkDisposed(Maybe.mergeArray(MaybeSubject.create(), MaybeSubject.create())); + } + + @Test + public void take() { + Maybe.mergeArray(Maybe.just(1), Maybe.empty(), Maybe.just(2)) + .doOnSubscribe(s -> s.request(Long.MAX_VALUE)) + .take(1) + .test() + .assertResult(1); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeMergeTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeMergeTest.java new file mode 100644 index 0000000000..0bf38ccc0f --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeMergeTest.java @@ -0,0 +1,110 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.maybe; + +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.schedulers.Schedulers; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class MaybeMergeTest extends RxJavaTest { + + @Test + public void delayErrorWithMaxConcurrency() { + Maybe.mergeDelayError( + Flowable.just(Maybe.just(1), Maybe.just(2), Maybe.just(3)), 1) + .test() + .assertResult(1, 2, 3); + } + + @Test + public void delayErrorWithMaxConcurrencyError() { + Maybe.mergeDelayError( + Flowable.just(Maybe.just(1), Maybe.<Integer>error(new TestException()), Maybe.just(3)), 1) + .test() + .assertFailure(TestException.class, 1, 3); + } + + @Test + public void delayErrorWithMaxConcurrencyAsync() { + final AtomicInteger count = new AtomicInteger(); + @SuppressWarnings("unchecked") + Maybe<Integer>[] sources = new Maybe[3]; + for (int i = 0; i < 3; i++) { + final int j = i + 1; + sources[i] = Maybe.fromCallable(new Callable<Integer>() { + @Override + public Integer call() throws Exception { + return count.incrementAndGet() - j; + } + }) + .subscribeOn(Schedulers.io()); + } + + for (int i = 0; i < 1000; i++) { + count.set(0); + Maybe.mergeDelayError( + Flowable.fromArray(sources), 1) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(0, 0, 0); + } + } + + @Test + public void delayErrorWithMaxConcurrencyAsyncError() { + final AtomicInteger count = new AtomicInteger(); + @SuppressWarnings("unchecked") + Maybe<Integer>[] sources = new Maybe[3]; + for (int i = 0; i < 3; i++) { + final int j = i + 1; + sources[i] = Maybe.fromCallable(new Callable<Integer>() { + @Override + public Integer call() throws Exception { + return count.incrementAndGet() - j; + } + }) + .subscribeOn(Schedulers.io()); + } + sources[1] = Maybe.fromCallable(new Callable<Integer>() { + @Override + public Integer call() throws Exception { + throw new TestException("" + count.incrementAndGet()); + } + }) + .subscribeOn(Schedulers.io()); + + for (int i = 0; i < 1000; i++) { + count.set(0); + Maybe.mergeDelayError( + Flowable.fromArray(sources), 1) + .to(TestHelper.<Integer>testConsumer()) + .awaitDone(5, TimeUnit.SECONDS) + .assertFailureAndMessage(TestException.class, "2", 0, 0); + } + } + + @Test + public void scalar() { + Maybe.mergeDelayError( + Flowable.just(Maybe.just(1))) + .test() + .assertResult(1); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeMergeWithTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeMergeWithTest.java new file mode 100644 index 0000000000..dd4d0817a8 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeMergeWithTest.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.maybe; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; + +public class MaybeMergeWithTest extends RxJavaTest { + + @Test + public void normal() { + Maybe.just(1).mergeWith(Maybe.just(2)) + .test() + .assertResult(1, 2); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeOfTypeTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeOfTypeTest.java new file mode 100644 index 0000000000..5d58b9ff90 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeOfTypeTest.java @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.maybe; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.processors.PublishProcessor; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class MaybeOfTypeTest extends RxJavaTest { + + @Test + public void normal() { + Maybe.just(1).ofType(Integer.class) + .test() + .assertResult(1); + } + + @Test + public void normalDowncast() { + TestObserver<Number> to = Maybe.just(1) + .ofType(Number.class) + .test(); + // don't make this fluent, target type required! + to.assertResult((Number)1); + } + + @Test + public void notInstance() { + TestObserver<String> to = Maybe.just(1) + .ofType(String.class) + .test(); + // don't make this fluent, target type required! + to.assertResult(); + } + + @Test + public void error() { + TestObserver<Number> to = Maybe.<Integer>error(new TestException()) + .ofType(Number.class) + .test(); + // don't make this fluent, target type required! + to.assertFailure(TestException.class); + } + + @Test + public void errorNotInstance() { + TestObserver<String> to = Maybe.<Integer>error(new TestException()) + .ofType(String.class) + .test(); + // don't make this fluent, target type required! + to.assertFailure(TestException.class); + } + + @Test + public void dispose() { + TestHelper.checkDisposedMaybe(new Function<Maybe<Object>, Maybe<Object>>() { + @Override + public Maybe<Object> apply(Maybe<Object> m) throws Exception { + return m.ofType(Object.class); + } + }); + } + + @Test + public void isDisposed() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + + TestHelper.checkDisposed(pp.singleElement().ofType(Object.class)); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeMaybe(new Function<Maybe<Object>, Maybe<Object>>() { + @Override + public Maybe<Object> apply(Maybe<Object> f) throws Exception { + return f.ofType(Object.class); + } + }); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeOnErrorXTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeOnErrorXTest.java new file mode 100644 index 0000000000..2cbc2fe427 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeOnErrorXTest.java @@ -0,0 +1,197 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.maybe; + +import java.io.IOException; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.processors.PublishProcessor; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class MaybeOnErrorXTest extends RxJavaTest { + + @Test + public void onErrorReturnConst() { + Maybe.error(new TestException()) + .onErrorReturnItem(1) + .test() + .assertResult(1); + } + + @Test + public void onErrorReturn() { + Maybe.error(new TestException()) + .onErrorReturn(Functions.justFunction(1)) + .test() + .assertResult(1); + } + + @Test + public void onErrorComplete() { + Maybe.error(new TestException()) + .onErrorComplete() + .test() + .assertResult(); + } + + @Test + public void onErrorCompleteTrue() { + Maybe.error(new TestException()) + .onErrorComplete(Functions.alwaysTrue()) + .test() + .assertResult(); + } + + @Test + public void onErrorCompleteFalse() { + Maybe.error(new TestException()) + .onErrorComplete(Functions.alwaysFalse()) + .test() + .assertFailure(TestException.class); + } + + @Test + public void onErrorReturnFunctionThrows() { + TestHelper.assertCompositeExceptions(Maybe.error(new TestException()) + .onErrorReturn(new Function<Throwable, Object>() { + @Override + public Object apply(Throwable v) throws Exception { + throw new IOException(); + } + }) + .to(TestHelper.testConsumer()), TestException.class, IOException.class); + } + + @Test + public void onErrorCompletePredicateThrows() { + TestHelper.assertCompositeExceptions(Maybe.error(new TestException()) + .onErrorComplete(new Predicate<Throwable>() { + @Override + public boolean test(Throwable v) throws Exception { + throw new IOException(); + } + }) + .to(TestHelper.testConsumer()), TestException.class, IOException.class); + } + + @Test + public void onErrorResumeNext() { + Maybe.error(new TestException()) + .onErrorResumeNext(Functions.justFunction(Maybe.just(1))) + .test() + .assertResult(1); + } + + @Test + public void onErrorResumeNextFunctionThrows() { + TestHelper.assertCompositeExceptions(Maybe.error(new TestException()) + .onErrorResumeNext(new Function<Throwable, Maybe<Object>>() { + @Override + public Maybe<Object> apply(Throwable v) throws Exception { + throw new IOException(); + } + }) + .to(TestHelper.testConsumer()), TestException.class, IOException.class); + } + + @Test + public void onErrorReturnSuccess() { + Maybe.just(1) + .onErrorReturnItem(2) + .test() + .assertResult(1); + } + + @Test + public void onErrorReturnEmpty() { + Maybe.<Integer>empty() + .onErrorReturnItem(2) + .test() + .assertResult(); + } + + @Test + public void onErrorReturnDispose() { + TestHelper.checkDisposed(PublishProcessor.create().singleElement().onErrorReturnItem(1)); + } + + @Test + public void onErrorReturnDoubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeMaybe(new Function<Maybe<Object>, MaybeSource<Object>>() { + @Override + public MaybeSource<Object> apply(Maybe<Object> v) throws Exception { + return v.onErrorReturnItem(1); + } + }); + } + + @Test + public void onErrorCompleteSuccess() { + Maybe.just(1) + .onErrorComplete() + .test() + .assertResult(1); + } + + @Test + public void onErrorCompleteEmpty() { + Maybe.<Integer>empty() + .onErrorComplete() + .test() + .assertResult(); + } + + @Test + public void onErrorCompleteDispose() { + TestHelper.checkDisposed(PublishProcessor.create().singleElement().onErrorComplete()); + } + + @Test + public void onErrorCompleteDoubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeMaybe(new Function<Maybe<Object>, MaybeSource<Object>>() { + @Override + public MaybeSource<Object> apply(Maybe<Object> v) throws Exception { + return v.onErrorComplete(); + } + }); + } + + @Test + public void onErrorNextDispose() { + TestHelper.checkDisposed(PublishProcessor.create().singleElement().onErrorResumeWith(Maybe.just(1))); + } + + @Test + public void onErrorNextDoubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeMaybe(new Function<Maybe<Object>, MaybeSource<Object>>() { + @Override + public MaybeSource<Object> apply(Maybe<Object> v) throws Exception { + return v.onErrorResumeWith(Maybe.just(1)); + } + }); + } + + @Test + public void onErrorNextIsAlsoError() { + Maybe.error(new TestException("Main")) + .onErrorResumeWith(Maybe.error(new TestException("Secondary"))) + .to(TestHelper.testConsumer()) + .assertFailureAndMessage(TestException.class, "Secondary"); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybePeekTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybePeekTest.java new file mode 100644 index 0000000000..ade9039eca --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybePeekTest.java @@ -0,0 +1,148 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.maybe; + +import static org.junit.Assert.*; + +import java.util.List; + +import io.reactivex.rxjava3.disposables.Disposable; +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.processors.PublishProcessor; +import io.reactivex.rxjava3.testsupport.*; + +public class MaybePeekTest extends RxJavaTest { + + @Test + public void disposed() { + TestHelper.checkDisposed(PublishProcessor.create().singleElement().doOnSuccess(Functions.emptyConsumer())); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeMaybe(new Function<Maybe<Object>, MaybeSource<Object>>() { + @Override + public MaybeSource<Object> apply(Maybe<Object> m) throws Exception { + return m.doOnSuccess(Functions.emptyConsumer()); + } + }); + } + + @Test + public void doubleError() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + + final Throwable[] err = { null }; + + try { + TestObserverEx<Integer> to = new Maybe<Integer>() { + @Override + protected void subscribeActual(MaybeObserver<? super Integer> observer) { + observer.onSubscribe(Disposable.empty()); + observer.onError(new TestException("First")); + observer.onError(new TestException("Second")); + } + } + .doOnError(new Consumer<Throwable>() { + @Override + public void accept(Throwable e) throws Exception { + err[0] = e; + } + }) + .to(TestHelper.<Integer>testConsumer()); + + TestHelper.assertUndeliverable(errors, 0, TestException.class, "Second"); + + assertTrue("" + err, err[0] instanceof TestException); + assertEquals("First", err[0].getMessage()); + + to.assertFailureAndMessage(TestException.class, "First"); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void doubleComplete() { + final int[] compl = { 0 }; + + TestObserver<Integer> to = new Maybe<Integer>() { + @Override + protected void subscribeActual(MaybeObserver<? super Integer> observer) { + observer.onSubscribe(Disposable.empty()); + observer.onComplete(); + observer.onComplete(); + } + } + .doOnComplete(new Action() { + @Override + public void run() throws Exception { + compl[0]++; + } + }) + .test(); + + assertEquals(1, compl[0]); + + to.assertResult(); + } + + @Test + public void doOnErrorThrows() { + TestObserverEx<Object> to = Maybe.error(new TestException("Main")) + .doOnError(new Consumer<Object>() { + @Override + public void accept(Object t) throws Exception { + throw new TestException("Inner"); + } + }) + .to(TestHelper.<Object>testConsumer()); + + to.assertFailure(CompositeException.class); + + List<Throwable> errors = TestHelper.compositeList(to.errors().get(0)); + + TestHelper.assertError(errors, 0, TestException.class, "Main"); + TestHelper.assertError(errors, 1, TestException.class, "Inner"); + } + + @Test + public void afterTerminateThrows() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + + try { + + Maybe.just(1) + .doAfterTerminate(new Action() { + @Override + public void run() throws Exception { + throw new TestException(); + } + }) + .test() + .assertResult(1); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeSafeSubscribeTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeSafeSubscribeTest.java new file mode 100644 index 0000000000..85919751d4 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeSafeSubscribeTest.java @@ -0,0 +1,197 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.maybe; + +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import java.io.IOException; + +import org.junit.Test; +import org.mockito.InOrder; + +import io.reactivex.rxjava3.annotations.NonNull; +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class MaybeSafeSubscribeTest { + + @Test + public void normalSuccess() throws Throwable { + TestHelper.withErrorTracking(errors -> { + @SuppressWarnings("unchecked") + MaybeObserver<Integer> consumer = mock(MaybeObserver.class); + + Maybe.just(1) + .safeSubscribe(consumer); + + InOrder order = inOrder(consumer); + order.verify(consumer).onSubscribe(any(Disposable.class)); + order.verify(consumer).onSuccess(1); + order.verifyNoMoreInteractions(); + + assertTrue("" + errors, errors.isEmpty()); + }); + } + + @Test + public void normalError() throws Throwable { + TestHelper.withErrorTracking(errors -> { + @SuppressWarnings("unchecked") + MaybeObserver<Integer> consumer = mock(MaybeObserver.class); + + Maybe.<Integer>error(new TestException()) + .safeSubscribe(consumer); + + InOrder order = inOrder(consumer); + order.verify(consumer).onSubscribe(any(Disposable.class)); + order.verify(consumer).onError(any(TestException.class)); + order.verifyNoMoreInteractions(); + + assertTrue("" + errors, errors.isEmpty()); + }); + } + + @Test + public void normalEmpty() throws Throwable { + TestHelper.withErrorTracking(errors -> { + @SuppressWarnings("unchecked") + MaybeObserver<Integer> consumer = mock(MaybeObserver.class); + + Maybe.<Integer>empty() + .safeSubscribe(consumer); + + InOrder order = inOrder(consumer); + order.verify(consumer).onSubscribe(any(Disposable.class)); + order.verify(consumer).onComplete(); + order.verifyNoMoreInteractions(); + }); + } + + @Test + public void onSubscribeCrash() throws Throwable { + TestHelper.withErrorTracking(errors -> { + @SuppressWarnings("unchecked") + MaybeObserver<Integer> consumer = mock(MaybeObserver.class); + doThrow(new TestException()).when(consumer).onSubscribe(any()); + + Disposable d = Disposable.empty(); + + new Maybe<Integer>() { + @Override + protected void subscribeActual(@NonNull MaybeObserver<? super Integer> observer) { + observer.onSubscribe(d); + // none of the following should arrive at the consumer + observer.onSuccess(1); + observer.onError(new IOException()); + observer.onComplete(); + } + } + .safeSubscribe(consumer); + + InOrder order = inOrder(consumer); + order.verify(consumer).onSubscribe(any(Disposable.class)); + order.verifyNoMoreInteractions(); + + assertTrue(d.isDisposed()); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + TestHelper.assertUndeliverable(errors, 1, IOException.class); + }); + } + + @Test + public void onSuccessCrash() throws Throwable { + TestHelper.withErrorTracking(errors -> { + @SuppressWarnings("unchecked") + MaybeObserver<Integer> consumer = mock(MaybeObserver.class); + doThrow(new TestException()).when(consumer).onSuccess(any()); + + new Maybe<Integer>() { + @Override + protected void subscribeActual(@NonNull MaybeObserver<? super Integer> observer) { + observer.onSubscribe(Disposable.empty()); + observer.onSuccess(1); + } + } + .safeSubscribe(consumer); + + InOrder order = inOrder(consumer); + order.verify(consumer).onSubscribe(any(Disposable.class)); + order.verify(consumer).onSuccess(1); + order.verifyNoMoreInteractions(); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + }); + } + + @Test + public void onErrorCrash() throws Throwable { + TestHelper.withErrorTracking(errors -> { + @SuppressWarnings("unchecked") + MaybeObserver<Integer> consumer = mock(MaybeObserver.class); + doThrow(new TestException()).when(consumer).onError(any()); + + new Maybe<Integer>() { + @Override + protected void subscribeActual(@NonNull MaybeObserver<? super Integer> observer) { + observer.onSubscribe(Disposable.empty()); + // none of the following should arrive at the consumer + observer.onError(new IOException()); + } + } + .safeSubscribe(consumer); + + InOrder order = inOrder(consumer); + order.verify(consumer).onSubscribe(any(Disposable.class)); + order.verify(consumer).onError(any(IOException.class)); + order.verifyNoMoreInteractions(); + + TestHelper.assertError(errors, 0, CompositeException.class); + + CompositeException compositeException = (CompositeException)errors.get(0); + TestHelper.assertError(compositeException.getExceptions(), 0, IOException.class); + TestHelper.assertError(compositeException.getExceptions(), 1, TestException.class); + }); + } + + @Test + public void onCompleteCrash() throws Throwable { + TestHelper.withErrorTracking(errors -> { + @SuppressWarnings("unchecked") + MaybeObserver<Integer> consumer = mock(MaybeObserver.class); + doThrow(new TestException()).when(consumer).onComplete(); + + new Maybe<Integer>() { + @Override + protected void subscribeActual(@NonNull MaybeObserver<? super Integer> observer) { + observer.onSubscribe(Disposable.empty()); + // none of the following should arrive at the consumer + observer.onComplete(); + } + } + .safeSubscribe(consumer); + + InOrder order = inOrder(consumer); + order.verify(consumer).onSubscribe(any(Disposable.class)); + order.verify(consumer).onComplete(); + order.verifyNoMoreInteractions(); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + }); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeStartWithTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeStartWithTest.java new file mode 100644 index 0000000000..31b0314158 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeStartWithTest.java @@ -0,0 +1,197 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.maybe; + +import static org.mockito.Mockito.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; + +public class MaybeStartWithTest { + + @Test + public void justCompletableComplete() { + Maybe.just(1).startWith(Completable.complete()) + .test() + .assertResult(1); + } + + @Test + public void emptyCompletableComplete() { + Maybe.empty().startWith(Completable.complete()) + .test() + .assertResult(); + } + + @Test + public void runCompletableError() { + Runnable run = mock(Runnable.class); + + Maybe.fromRunnable(run).startWith(Completable.error(new TestException())) + .test() + .assertFailure(TestException.class); + + verify(run, never()).run(); + } + + @Test + public void justSingleJust() { + Maybe.just(1).startWith(Single.just(2)) + .test() + .assertResult(2, 1); + } + + @Test + public void emptySingleJust() { + Runnable run = mock(Runnable.class); + + Maybe.fromRunnable(run) + .startWith(Single.just(2)) + .test() + .assertResult(2); + + verify(run).run(); + } + + @Test + public void runSingleError() { + Runnable run = mock(Runnable.class); + + Maybe.fromRunnable(run).startWith(Single.error(new TestException())) + .test() + .assertFailure(TestException.class); + + verify(run, never()).run(); + } + + @Test + public void justMaybeJust() { + Maybe.just(1).startWith(Maybe.just(2)) + .test() + .assertResult(2, 1); + } + + @Test + public void emptyMaybeJust() { + Runnable run = mock(Runnable.class); + + Maybe.fromRunnable(run) + .startWith(Maybe.just(2)) + .test() + .assertResult(2); + + verify(run).run(); + } + + @Test + public void runMaybeError() { + Runnable run = mock(Runnable.class); + + Maybe.fromRunnable(run).startWith(Maybe.error(new TestException())) + .test() + .assertFailure(TestException.class); + + verify(run, never()).run(); + } + + @Test + public void justObservableJust() { + Maybe.just(1).startWith(Observable.just(2, 3, 4, 5)) + .test() + .assertResult(2, 3, 4, 5, 1); + } + + @Test + public void emptyObservableJust() { + Runnable run = mock(Runnable.class); + + Maybe.fromRunnable(run) + .startWith(Observable.just(2, 3, 4, 5)) + .test() + .assertResult(2, 3, 4, 5); + + verify(run).run(); + } + + @Test + public void emptyObservableEmpty() { + Runnable run = mock(Runnable.class); + Runnable run2 = mock(Runnable.class); + + Maybe.fromRunnable(run) + .startWith(Observable.fromRunnable(run2)) + .test() + .assertResult(); + + verify(run).run(); + verify(run2).run(); + } + + @Test + public void runObservableError() { + Runnable run = mock(Runnable.class); + + Maybe.fromRunnable(run).startWith(Observable.error(new TestException())) + .test() + .assertFailure(TestException.class); + + verify(run, never()).run(); + } + + @Test + public void justFlowableJust() { + Maybe.just(1).startWith(Flowable.just(2, 3, 4, 5)) + .test() + .assertResult(2, 3, 4, 5, 1); + } + + @Test + public void emptyFlowableJust() { + Runnable run = mock(Runnable.class); + + Maybe.fromRunnable(run) + .startWith(Flowable.just(2, 3, 4, 5)) + .test() + .assertResult(2, 3, 4, 5); + + verify(run).run(); + } + + @Test + public void emptyFlowableEmpty() { + Runnable run = mock(Runnable.class); + Runnable run2 = mock(Runnable.class); + + Maybe.fromRunnable(run) + .startWith(Flowable.fromRunnable(run2)) + .test() + .assertResult(); + + verify(run).run(); + verify(run2).run(); + } + + @Test + public void runFlowableError() { + Runnable run = mock(Runnable.class); + + Maybe.fromRunnable(run).startWith(Flowable.error(new TestException())) + .test() + .assertFailure(TestException.class); + + verify(run, never()).run(); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeSubscribeOnTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeSubscribeOnTest.java new file mode 100644 index 0000000000..3f5a29bc23 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeSubscribeOnTest.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.maybe; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.schedulers.Schedulers; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class MaybeSubscribeOnTest extends RxJavaTest { + + @Test + public void dispose() { + TestHelper.checkDisposed(Maybe.just(1).subscribeOn(Schedulers.single())); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeSwitchIfEmptySingleTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeSwitchIfEmptySingleTest.java new file mode 100644 index 0000000000..153b183287 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeSwitchIfEmptySingleTest.java @@ -0,0 +1,114 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.maybe; + +import static org.junit.Assert.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.fuseable.HasUpstreamMaybeSource; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.processors.PublishProcessor; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class MaybeSwitchIfEmptySingleTest extends RxJavaTest { + + @Test + public void nonEmpty() { + Maybe.just(1).switchIfEmpty(Single.just(2)).test().assertResult(1); + } + + @Test + public void empty() { + Maybe.<Integer>empty().switchIfEmpty(Single.just(2)).test().assertResult(2); + } + + @Test + public void error() { + Maybe.<Integer>error(new TestException()).switchIfEmpty(Single.just(2)) + .test().assertFailure(TestException.class); + } + + @Test + public void errorOther() { + Maybe.empty().switchIfEmpty(Single.<Integer>error(new TestException())) + .test().assertFailure(TestException.class); + } + + @Test + public void dispose() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + + TestObserver<Integer> to = pp.singleElement().switchIfEmpty(Single.just(2)).test(); + + assertTrue(pp.hasSubscribers()); + + to.dispose(); + + assertFalse(pp.hasSubscribers()); + } + + @Test + public void isDisposed() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + + TestHelper.checkDisposed(pp.singleElement().switchIfEmpty(Single.just(2))); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeMaybeToSingle(new Function<Maybe<Integer>, Single<Integer>>() { + @Override + public Single<Integer> apply(Maybe<Integer> f) throws Exception { + return f.switchIfEmpty(Single.just(2)); + } + }); + } + + @Test + public void emptyCancelRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final PublishProcessor<Integer> pp = PublishProcessor.create(); + + final TestObserver<Integer> to = pp.singleElement().switchIfEmpty(Single.just(2)).test(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + pp.onComplete(); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + to.dispose(); + } + }; + + TestHelper.race(r1, r2); + } + } + + @SuppressWarnings("rawtypes") + @Test + public void source() { + assertSame(Maybe.empty(), + ((HasUpstreamMaybeSource)(Maybe.<Integer>empty().switchIfEmpty(Single.just(1)))).source() + ); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeSwitchIfEmptyTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeSwitchIfEmptyTest.java new file mode 100644 index 0000000000..f15f5ae8be --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeSwitchIfEmptyTest.java @@ -0,0 +1,121 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.maybe; + +import static org.junit.Assert.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.processors.PublishProcessor; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class MaybeSwitchIfEmptyTest extends RxJavaTest { + + @Test + public void nonEmpty() { + Maybe.just(1).switchIfEmpty(Maybe.just(2)).test().assertResult(1); + } + + @Test + public void empty() { + Maybe.<Integer>empty().switchIfEmpty(Maybe.just(2)).test().assertResult(2); + } + + @Test + public void defaultIfEmptyNonEmpty() { + Maybe.just(1).defaultIfEmpty(2).test().assertResult(1); + } + + @Test + public void defaultIfEmptyEmpty() { + Maybe.<Integer>empty().defaultIfEmpty(2).test().assertResult(2); + } + + @Test + public void error() { + Maybe.<Integer>error(new TestException()).switchIfEmpty(Maybe.just(2)) + .test().assertFailure(TestException.class); + } + + @Test + public void errorOther() { + Maybe.empty().switchIfEmpty(Maybe.<Integer>error(new TestException())) + .test().assertFailure(TestException.class); + } + + @Test + public void emptyOtherToo() { + Maybe.empty().switchIfEmpty(Maybe.empty()) + .test().assertResult(); + } + + @Test + public void dispose() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + + TestObserver<Integer> to = pp.singleElement().switchIfEmpty(Maybe.just(2)).test(); + + assertTrue(pp.hasSubscribers()); + + to.dispose(); + + assertFalse(pp.hasSubscribers()); + } + + @Test + public void isDisposed() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + + TestHelper.checkDisposed(pp.singleElement().switchIfEmpty(Maybe.just(2))); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeMaybe(new Function<Maybe<Integer>, Maybe<Integer>>() { + @Override + public Maybe<Integer> apply(Maybe<Integer> f) throws Exception { + return f.switchIfEmpty(Maybe.just(2)); + } + }); + } + + @Test + public void emptyCancelRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final PublishProcessor<Integer> pp = PublishProcessor.create(); + + final TestObserver<Integer> to = pp.singleElement().switchIfEmpty(Maybe.just(2)).test(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + pp.onComplete(); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + to.dispose(); + } + }; + + TestHelper.race(r1, r2); + } + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeSwitchOnNextTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeSwitchOnNextTest.java new file mode 100644 index 0000000000..7a502faa28 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeSwitchOnNextTest.java @@ -0,0 +1,123 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.maybe; + +import static org.junit.Assert.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.processors.PublishProcessor; +import io.reactivex.rxjava3.subjects.MaybeSubject; +import io.reactivex.rxjava3.subscribers.TestSubscriber; + +public class MaybeSwitchOnNextTest extends RxJavaTest { + + @Test + public void normal() { + Maybe.switchOnNext( + Flowable.range(1, 10) + .map(v -> { + if (v % 2 == 0) { + return Maybe.just(v); + } + return Maybe.empty(); + }) + ) + .test() + .assertResult(2, 4, 6, 8, 10); + } + + @Test + public void normalDelayError() { + Maybe.switchOnNextDelayError( + Flowable.range(1, 10) + .map(v -> { + if (v % 2 == 0) { + return Maybe.just(v); + } + return Maybe.empty(); + }) + ) + .test() + .assertResult(2, 4, 6, 8, 10); + } + + @Test + public void noDelaySwitch() { + PublishProcessor<Maybe<Integer>> pp = PublishProcessor.create(); + + TestSubscriber<Integer> ts = Maybe.switchOnNext(pp).test(); + + assertTrue(pp.hasSubscribers()); + + ts.assertEmpty(); + + MaybeSubject<Integer> ms1 = MaybeSubject.create(); + MaybeSubject<Integer> ms2 = MaybeSubject.create(); + + pp.onNext(ms1); + + assertTrue(ms1.hasObservers()); + + pp.onNext(ms2); + + assertFalse(ms1.hasObservers()); + assertTrue(ms2.hasObservers()); + + pp.onComplete(); + + assertTrue(ms2.hasObservers()); + + ms2.onSuccess(1); + + ts.assertResult(1); + } + + @Test + public void delaySwitch() { + PublishProcessor<Maybe<Integer>> pp = PublishProcessor.create(); + + TestSubscriber<Integer> ts = Maybe.switchOnNextDelayError(pp).test(); + + assertTrue(pp.hasSubscribers()); + + ts.assertEmpty(); + + MaybeSubject<Integer> ms1 = MaybeSubject.create(); + MaybeSubject<Integer> ms2 = MaybeSubject.create(); + + pp.onNext(ms1); + + assertTrue(ms1.hasObservers()); + + pp.onNext(ms2); + + assertFalse(ms1.hasObservers()); + assertTrue(ms2.hasObservers()); + + assertTrue(ms2.hasObservers()); + + ms2.onError(new TestException()); + + assertTrue(pp.hasSubscribers()); + + ts.assertEmpty(); + + pp.onComplete(); + + ts.assertFailure(TestException.class); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeTakeUntilPublisherTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeTakeUntilPublisherTest.java new file mode 100644 index 0000000000..f5ce249514 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeTakeUntilPublisherTest.java @@ -0,0 +1,201 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.maybe; + +import static org.junit.Assert.*; + +import java.util.List; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.processors.PublishProcessor; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class MaybeTakeUntilPublisherTest extends RxJavaTest { + + @Test + public void disposed() { + TestHelper.checkDisposed(PublishProcessor.create().singleElement().takeUntil(Flowable.never())); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeMaybe(new Function<Maybe<Object>, MaybeSource<Object>>() { + @Override + public MaybeSource<Object> apply(Maybe<Object> m) throws Exception { + return m.takeUntil(Flowable.never()); + } + }); + } + + @Test + public void mainErrors() { + PublishProcessor<Integer> pp1 = PublishProcessor.create(); + PublishProcessor<Integer> pp2 = PublishProcessor.create(); + + TestObserver<Integer> to = pp1.singleElement().takeUntil(pp2).test(); + + assertTrue(pp1.hasSubscribers()); + assertTrue(pp2.hasSubscribers()); + + pp1.onError(new TestException()); + + assertFalse(pp1.hasSubscribers()); + assertFalse(pp2.hasSubscribers()); + + to.assertFailure(TestException.class); + } + + @Test + public void otherErrors() { + PublishProcessor<Integer> pp1 = PublishProcessor.create(); + PublishProcessor<Integer> pp2 = PublishProcessor.create(); + + TestObserver<Integer> to = pp1.singleElement().takeUntil(pp2).test(); + + assertTrue(pp1.hasSubscribers()); + assertTrue(pp2.hasSubscribers()); + + pp2.onError(new TestException()); + + assertFalse(pp1.hasSubscribers()); + assertFalse(pp2.hasSubscribers()); + + to.assertFailure(TestException.class); + } + + @Test + public void mainCompletes() { + PublishProcessor<Integer> pp1 = PublishProcessor.create(); + PublishProcessor<Integer> pp2 = PublishProcessor.create(); + + TestObserver<Integer> to = pp1.singleElement().takeUntil(pp2).test(); + + assertTrue(pp1.hasSubscribers()); + assertTrue(pp2.hasSubscribers()); + + pp1.onComplete(); + + assertFalse(pp1.hasSubscribers()); + assertFalse(pp2.hasSubscribers()); + + to.assertResult(); + } + + @Test + public void otherCompletes() { + PublishProcessor<Integer> pp1 = PublishProcessor.create(); + PublishProcessor<Integer> pp2 = PublishProcessor.create(); + + TestObserver<Integer> to = pp1.singleElement().takeUntil(pp2).test(); + + assertTrue(pp1.hasSubscribers()); + assertTrue(pp2.hasSubscribers()); + + pp2.onComplete(); + + assertFalse(pp1.hasSubscribers()); + assertFalse(pp2.hasSubscribers()); + + to.assertResult(); + } + + @Test + public void onErrorRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final PublishProcessor<Integer> pp1 = PublishProcessor.create(); + final PublishProcessor<Integer> pp2 = PublishProcessor.create(); + + TestObserver<Integer> to = pp1.singleElement().takeUntil(pp2).test(); + + final TestException ex1 = new TestException(); + final TestException ex2 = new TestException(); + + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + + Runnable r1 = new Runnable() { + @Override + public void run() { + pp1.onError(ex1); + } + }; + Runnable r2 = new Runnable() { + @Override + public void run() { + pp2.onError(ex2); + } + }; + + TestHelper.race(r1, r2); + + to.assertFailure(TestException.class); + + if (!errors.isEmpty()) { + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } + + } finally { + RxJavaPlugins.reset(); + } + + } + } + + @Test + public void onCompleteRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final PublishProcessor<Integer> pp1 = PublishProcessor.create(); + final PublishProcessor<Integer> pp2 = PublishProcessor.create(); + + TestObserver<Integer> to = pp1.singleElement().takeUntil(pp2).test(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + pp1.onComplete(); + } + }; + Runnable r2 = new Runnable() { + @Override + public void run() { + pp2.onComplete(); + } + }; + + TestHelper.race(r1, r2); + + to.assertResult(); + } + } + + @Test + public void otherSignalsAndCompletes() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + Maybe.just(1).takeUntil(Flowable.just(1).take(1)) + .test() + .assertResult(); + + assertTrue(errors.toString(), errors.isEmpty()); + } finally { + RxJavaPlugins.reset(); + } + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeTakeUntilTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeTakeUntilTest.java new file mode 100644 index 0000000000..20ce1cfa41 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeTakeUntilTest.java @@ -0,0 +1,468 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.maybe; + +import static org.junit.Assert.*; + +import java.util.List; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.processors.PublishProcessor; +import io.reactivex.rxjava3.subjects.MaybeSubject; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class MaybeTakeUntilTest extends RxJavaTest { + + @Test + public void normalPublisher() { + Maybe.just(1).takeUntil(Flowable.never()) + .test() + .assertResult(1); + } + + @Test + public void normalMaybe() { + Maybe.just(1).takeUntil(Maybe.never()) + .test() + .assertResult(1); + } + + @Test + public void untilFirstPublisher() { + Maybe.just(1).takeUntil(Flowable.just("one")) + .test() + .assertResult(); + } + + @Test + public void untilFirstMaybe() { + Maybe.just(1).takeUntil(Maybe.just("one")) + .test() + .assertResult(); + } + + @Test + public void disposed() { + TestHelper.checkDisposed(PublishProcessor.create().singleElement().takeUntil(Maybe.never())); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeMaybe(new Function<Maybe<Object>, MaybeSource<Object>>() { + @Override + public MaybeSource<Object> apply(Maybe<Object> m) throws Exception { + return m.takeUntil(Maybe.never()); + } + }); + } + + @Test + public void mainErrors() { + PublishProcessor<Integer> pp1 = PublishProcessor.create(); + PublishProcessor<Integer> pp2 = PublishProcessor.create(); + + TestObserver<Integer> to = pp1.singleElement().takeUntil(pp2.singleElement()).test(); + + assertTrue(pp1.hasSubscribers()); + assertTrue(pp2.hasSubscribers()); + + pp1.onError(new TestException()); + + assertFalse(pp1.hasSubscribers()); + assertFalse(pp2.hasSubscribers()); + + to.assertFailure(TestException.class); + } + + @Test + public void otherErrors() { + PublishProcessor<Integer> pp1 = PublishProcessor.create(); + PublishProcessor<Integer> pp2 = PublishProcessor.create(); + + TestObserver<Integer> to = pp1.singleElement().takeUntil(pp2.singleElement()).test(); + + assertTrue(pp1.hasSubscribers()); + assertTrue(pp2.hasSubscribers()); + + pp2.onError(new TestException()); + + assertFalse(pp1.hasSubscribers()); + assertFalse(pp2.hasSubscribers()); + + to.assertFailure(TestException.class); + } + + @Test + public void mainCompletes() { + PublishProcessor<Integer> pp1 = PublishProcessor.create(); + PublishProcessor<Integer> pp2 = PublishProcessor.create(); + + TestObserver<Integer> to = pp1.singleElement().takeUntil(pp2.singleElement()).test(); + + assertTrue(pp1.hasSubscribers()); + assertTrue(pp2.hasSubscribers()); + + pp1.onComplete(); + + assertFalse(pp1.hasSubscribers()); + assertFalse(pp2.hasSubscribers()); + + to.assertResult(); + } + + @Test + public void otherCompletes() { + PublishProcessor<Integer> pp1 = PublishProcessor.create(); + PublishProcessor<Integer> pp2 = PublishProcessor.create(); + + TestObserver<Integer> to = pp1.singleElement().takeUntil(pp2.singleElement()).test(); + + assertTrue(pp1.hasSubscribers()); + assertTrue(pp2.hasSubscribers()); + + pp2.onComplete(); + + assertFalse(pp1.hasSubscribers()); + assertFalse(pp2.hasSubscribers()); + + to.assertResult(); + } + + @Test + public void onErrorRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final PublishProcessor<Integer> pp1 = PublishProcessor.create(); + final PublishProcessor<Integer> pp2 = PublishProcessor.create(); + + TestObserver<Integer> to = pp1.singleElement().takeUntil(pp2.singleElement()).test(); + + final TestException ex1 = new TestException(); + final TestException ex2 = new TestException(); + + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + + Runnable r1 = new Runnable() { + @Override + public void run() { + pp1.onError(ex1); + } + }; + Runnable r2 = new Runnable() { + @Override + public void run() { + pp2.onError(ex2); + } + }; + + TestHelper.race(r1, r2); + + to.assertFailure(TestException.class); + + if (!errors.isEmpty()) { + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } + + } finally { + RxJavaPlugins.reset(); + } + + } + } + + @Test + public void onCompleteRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final PublishProcessor<Integer> pp1 = PublishProcessor.create(); + final PublishProcessor<Integer> pp2 = PublishProcessor.create(); + + TestObserver<Integer> to = pp1.singleElement().takeUntil(pp2.singleElement()).test(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + pp1.onComplete(); + } + }; + Runnable r2 = new Runnable() { + @Override + public void run() { + pp2.onComplete(); + } + }; + + TestHelper.race(r1, r2); + + to.assertResult(); + } + } + + @Test + public void untilMaybeMainSuccess() { + MaybeSubject<Integer> main = MaybeSubject.create(); + MaybeSubject<Integer> other = MaybeSubject.create(); + + TestObserver<Integer> to = main.takeUntil(other).test(); + + assertTrue("Main no observers?", main.hasObservers()); + assertTrue("Other no observers?", other.hasObservers()); + + main.onSuccess(1); + + assertFalse("Main has observers?", main.hasObservers()); + assertFalse("Other has observers?", other.hasObservers()); + + to.assertResult(1); + } + + @Test + public void untilMaybeMainComplete() { + MaybeSubject<Integer> main = MaybeSubject.create(); + MaybeSubject<Integer> other = MaybeSubject.create(); + + TestObserver<Integer> to = main.takeUntil(other).test(); + + assertTrue("Main no observers?", main.hasObservers()); + assertTrue("Other no observers?", other.hasObservers()); + + main.onComplete(); + + assertFalse("Main has observers?", main.hasObservers()); + assertFalse("Other has observers?", other.hasObservers()); + + to.assertResult(); + } + + @Test + public void untilMaybeMainError() { + MaybeSubject<Integer> main = MaybeSubject.create(); + MaybeSubject<Integer> other = MaybeSubject.create(); + + TestObserver<Integer> to = main.takeUntil(other).test(); + + assertTrue("Main no observers?", main.hasObservers()); + assertTrue("Other no observers?", other.hasObservers()); + + main.onError(new TestException()); + + assertFalse("Main has observers?", main.hasObservers()); + assertFalse("Other has observers?", other.hasObservers()); + + to.assertFailure(TestException.class); + } + + @Test + public void untilMaybeOtherSuccess() { + MaybeSubject<Integer> main = MaybeSubject.create(); + MaybeSubject<Integer> other = MaybeSubject.create(); + + TestObserver<Integer> to = main.takeUntil(other).test(); + + assertTrue("Main no observers?", main.hasObservers()); + assertTrue("Other no observers?", other.hasObservers()); + + other.onSuccess(1); + + assertFalse("Main has observers?", main.hasObservers()); + assertFalse("Other has observers?", other.hasObservers()); + + to.assertResult(); + } + + @Test + public void untilMaybeOtherComplete() { + MaybeSubject<Integer> main = MaybeSubject.create(); + MaybeSubject<Integer> other = MaybeSubject.create(); + + TestObserver<Integer> to = main.takeUntil(other).test(); + + assertTrue("Main no observers?", main.hasObservers()); + assertTrue("Other no observers?", other.hasObservers()); + + other.onComplete(); + + assertFalse("Main has observers?", main.hasObservers()); + assertFalse("Other has observers?", other.hasObservers()); + + to.assertResult(); + } + + @Test + public void untilMaybeOtherError() { + MaybeSubject<Integer> main = MaybeSubject.create(); + MaybeSubject<Integer> other = MaybeSubject.create(); + + TestObserver<Integer> to = main.takeUntil(other).test(); + + assertTrue("Main no observers?", main.hasObservers()); + assertTrue("Other no observers?", other.hasObservers()); + + other.onError(new TestException()); + + assertFalse("Main has observers?", main.hasObservers()); + assertFalse("Other has observers?", other.hasObservers()); + + to.assertFailure(TestException.class); + } + + @Test + public void untilMaybeDispose() { + MaybeSubject<Integer> main = MaybeSubject.create(); + MaybeSubject<Integer> other = MaybeSubject.create(); + + TestObserver<Integer> to = main.takeUntil(other).test(); + + assertTrue("Main no observers?", main.hasObservers()); + assertTrue("Other no observers?", other.hasObservers()); + + to.dispose(); + + assertFalse("Main has observers?", main.hasObservers()); + assertFalse("Other has observers?", other.hasObservers()); + + to.assertEmpty(); + } + + @Test + public void untilPublisherMainSuccess() { + MaybeSubject<Integer> main = MaybeSubject.create(); + PublishProcessor<Integer> other = PublishProcessor.create(); + + TestObserver<Integer> to = main.takeUntil(other).test(); + + assertTrue("Main no observers?", main.hasObservers()); + assertTrue("Other no observers?", other.hasSubscribers()); + + main.onSuccess(1); + + assertFalse("Main has observers?", main.hasObservers()); + assertFalse("Other has observers?", other.hasSubscribers()); + + to.assertResult(1); + } + + @Test + public void untilPublisherMainComplete() { + MaybeSubject<Integer> main = MaybeSubject.create(); + PublishProcessor<Integer> other = PublishProcessor.create(); + + TestObserver<Integer> to = main.takeUntil(other).test(); + + assertTrue("Main no observers?", main.hasObservers()); + assertTrue("Other no observers?", other.hasSubscribers()); + + main.onComplete(); + + assertFalse("Main has observers?", main.hasObservers()); + assertFalse("Other has observers?", other.hasSubscribers()); + + to.assertResult(); + } + + @Test + public void untilPublisherMainError() { + MaybeSubject<Integer> main = MaybeSubject.create(); + PublishProcessor<Integer> other = PublishProcessor.create(); + + TestObserver<Integer> to = main.takeUntil(other).test(); + + assertTrue("Main no observers?", main.hasObservers()); + assertTrue("Other no observers?", other.hasSubscribers()); + + main.onError(new TestException()); + + assertFalse("Main has observers?", main.hasObservers()); + assertFalse("Other has observers?", other.hasSubscribers()); + + to.assertFailure(TestException.class); + } + + @Test + public void untilPublisherOtherOnNext() { + MaybeSubject<Integer> main = MaybeSubject.create(); + PublishProcessor<Integer> other = PublishProcessor.create(); + + TestObserver<Integer> to = main.takeUntil(other).test(); + + assertTrue("Main no observers?", main.hasObservers()); + assertTrue("Other no observers?", other.hasSubscribers()); + + other.onNext(1); + + assertFalse("Main has observers?", main.hasObservers()); + assertFalse("Other has observers?", other.hasSubscribers()); + + to.assertResult(); + } + + @Test + public void untilPublisherOtherOnComplete() { + MaybeSubject<Integer> main = MaybeSubject.create(); + PublishProcessor<Integer> other = PublishProcessor.create(); + + TestObserver<Integer> to = main.takeUntil(other).test(); + + assertTrue("Main no observers?", main.hasObservers()); + assertTrue("Other no observers?", other.hasSubscribers()); + + other.onComplete(); + + assertFalse("Main has observers?", main.hasObservers()); + assertFalse("Other has observers?", other.hasSubscribers()); + + to.assertResult(); + } + + @Test + public void untilPublisherOtherError() { + MaybeSubject<Integer> main = MaybeSubject.create(); + PublishProcessor<Integer> other = PublishProcessor.create(); + + TestObserver<Integer> to = main.takeUntil(other).test(); + + assertTrue("Main no observers?", main.hasObservers()); + assertTrue("Other no observers?", other.hasSubscribers()); + + other.onError(new TestException()); + + assertFalse("Main has observers?", main.hasObservers()); + assertFalse("Other has observers?", other.hasSubscribers()); + + to.assertFailure(TestException.class); + } + + @Test + public void untilPublisherDispose() { + MaybeSubject<Integer> main = MaybeSubject.create(); + PublishProcessor<Integer> other = PublishProcessor.create(); + + TestObserver<Integer> to = main.takeUntil(other).test(); + + assertTrue("Main no observers?", main.hasObservers()); + assertTrue("Other no observers?", other.hasSubscribers()); + + to.dispose(); + + assertFalse("Main has observers?", main.hasObservers()); + assertFalse("Other has observers?", other.hasSubscribers()); + + to.assertEmpty(); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeTimeIntervalTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeTimeIntervalTest.java new file mode 100644 index 0000000000..bd50202ae4 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeTimeIntervalTest.java @@ -0,0 +1,111 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.maybe; + +import java.util.concurrent.TimeUnit; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.Maybe; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.schedulers.*; +import io.reactivex.rxjava3.subjects.MaybeSubject; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class MaybeTimeIntervalTest { + + @Test + public void just() { + Maybe.just(1) + .timeInterval() + .test() + .assertValueCount(1) + .assertNoErrors() + .assertComplete(); + } + + @Test + public void empty() { + Maybe.empty() + .timeInterval() + .test() + .assertResult(); + } + + @Test + public void error() { + Maybe.error(new TestException()) + .timeInterval() + .test() + .assertFailure(TestException.class); + } + + @Test + public void justSeconds() { + Maybe.just(1) + .timeInterval(TimeUnit.SECONDS) + .test() + .assertValueCount(1) + .assertNoErrors() + .assertComplete(); + } + + @Test + public void justScheduler() { + Maybe.just(1) + .timeInterval(Schedulers.single()) + .test() + .assertValueCount(1) + .assertNoErrors() + .assertComplete(); + } + + @Test + public void justSecondsScheduler() { + Maybe.just(1) + .timeInterval(TimeUnit.SECONDS, Schedulers.single()) + .test() + .assertValueCount(1) + .assertNoErrors() + .assertComplete(); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeMaybe(m -> m.timeInterval()); + } + + @Test + public void dispose() { + TestHelper.checkDisposed(MaybeSubject.create().timeInterval()); + } + + @Test + public void timeInfo() { + TestScheduler scheduler = new TestScheduler(); + + MaybeSubject<Integer> ms = MaybeSubject.create(); + + TestObserver<Timed<Integer>> to = ms + .timeInterval(scheduler) + .test(); + + scheduler.advanceTimeBy(1000, TimeUnit.MILLISECONDS); + + ms.onSuccess(1); + + to.assertResult(new Timed<>(1, 1000L, TimeUnit.MILLISECONDS)); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeTimeoutPublisherTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeTimeoutPublisherTest.java new file mode 100644 index 0000000000..efa2ce6ff6 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeTimeoutPublisherTest.java @@ -0,0 +1,261 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.maybe; + +import static org.junit.Assert.*; + +import java.util.concurrent.TimeoutException; + +import org.junit.Test; + +import io.reactivex.rxjava3.annotations.NonNull; +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.processors.PublishProcessor; +import io.reactivex.rxjava3.testsupport.*; + +public class MaybeTimeoutPublisherTest extends RxJavaTest { + + @Test + public void mainError() { + PublishProcessor<Integer> pp1 = PublishProcessor.create(); + PublishProcessor<Integer> pp2 = PublishProcessor.create(); + + TestObserver<Integer> to = pp1.singleElement().timeout(pp2).test(); + + assertTrue(pp1.hasSubscribers()); + assertTrue(pp2.hasSubscribers()); + + pp1.onError(new TestException()); + + assertFalse(pp1.hasSubscribers()); + assertFalse(pp2.hasSubscribers()); + + to.assertFailure(TestException.class); + } + + @Test + public void otherError() { + PublishProcessor<Integer> pp1 = PublishProcessor.create(); + PublishProcessor<Integer> pp2 = PublishProcessor.create(); + + TestObserver<Integer> to = pp1.singleElement().timeout(pp2).test(); + + assertTrue(pp1.hasSubscribers()); + assertTrue(pp2.hasSubscribers()); + + pp2.onError(new TestException()); + + assertFalse(pp1.hasSubscribers()); + assertFalse(pp2.hasSubscribers()); + + to.assertFailure(TestException.class); + } + + @Test + public void fallbackError() { + PublishProcessor<Integer> pp1 = PublishProcessor.create(); + PublishProcessor<Integer> pp2 = PublishProcessor.create(); + + TestObserver<Integer> to = pp1.singleElement().timeout(pp2, Maybe.<Integer>error(new TestException())).test(); + + assertTrue(pp1.hasSubscribers()); + assertTrue(pp2.hasSubscribers()); + + pp2.onNext(1); + pp2.onComplete(); + + assertFalse(pp1.hasSubscribers()); + assertFalse(pp2.hasSubscribers()); + + to.assertFailure(TestException.class); + } + + @Test + public void fallbackComplete() { + PublishProcessor<Integer> pp1 = PublishProcessor.create(); + PublishProcessor<Integer> pp2 = PublishProcessor.create(); + + TestObserver<Integer> to = pp1.singleElement().timeout(pp2, Maybe.<Integer>empty()).test(); + + assertTrue(pp1.hasSubscribers()); + assertTrue(pp2.hasSubscribers()); + + pp2.onNext(1); + pp2.onComplete(); + + assertFalse(pp1.hasSubscribers()); + assertFalse(pp2.hasSubscribers()); + + to.assertResult(); + } + + @Test + public void mainComplete() { + PublishProcessor<Integer> pp1 = PublishProcessor.create(); + PublishProcessor<Integer> pp2 = PublishProcessor.create(); + + TestObserver<Integer> to = pp1.singleElement().timeout(pp2).test(); + + assertTrue(pp1.hasSubscribers()); + assertTrue(pp2.hasSubscribers()); + + pp1.onComplete(); + + assertFalse(pp1.hasSubscribers()); + assertFalse(pp2.hasSubscribers()); + + to.assertResult(); + } + + @Test + public void otherComplete() { + PublishProcessor<Integer> pp1 = PublishProcessor.create(); + PublishProcessor<Integer> pp2 = PublishProcessor.create(); + + TestObserver<Integer> to = pp1.singleElement().timeout(pp2).test(); + + assertTrue(pp1.hasSubscribers()); + assertTrue(pp2.hasSubscribers()); + + pp2.onComplete(); + + assertFalse(pp1.hasSubscribers()); + assertFalse(pp2.hasSubscribers()); + + to.assertFailure(TimeoutException.class); + } + + @Test + public void dispose() { + PublishProcessor<Integer> pp1 = PublishProcessor.create(); + PublishProcessor<Integer> pp2 = PublishProcessor.create(); + + TestHelper.checkDisposed(pp1.singleElement().timeout(pp2)); + } + + @Test + public void dispose2() { + PublishProcessor<Integer> pp1 = PublishProcessor.create(); + PublishProcessor<Integer> pp2 = PublishProcessor.create(); + + TestHelper.checkDisposed(pp1.singleElement().timeout(pp2, Maybe.just(1))); + } + + @Test + public void onErrorRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + TestHelper.trackPluginErrors(); + try { + final PublishProcessor<Integer> pp1 = PublishProcessor.create(); + final PublishProcessor<Integer> pp2 = PublishProcessor.create(); + + TestObserver<Integer> to = pp1.singleElement().timeout(pp2).test(); + + final TestException ex = new TestException(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + pp1.onError(ex); + } + }; + Runnable r2 = new Runnable() { + @Override + public void run() { + pp2.onError(ex); + } + }; + + TestHelper.race(r1, r2); + + to.assertFailure(TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + } + + @Test + public void onCompleteRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final PublishProcessor<Integer> pp1 = PublishProcessor.create(); + final PublishProcessor<Integer> pp2 = PublishProcessor.create(); + + TestObserverEx<Integer> to = pp1.singleElement().timeout(pp2).to(TestHelper.<Integer>testConsumer()); + + Runnable r1 = new Runnable() { + @Override + public void run() { + pp1.onComplete(); + } + }; + Runnable r2 = new Runnable() { + @Override + public void run() { + pp2.onComplete(); + } + }; + + TestHelper.race(r1, r2); + + to.assertSubscribed().assertNoValues(); + + if (to.errors().size() != 0) { + to.assertError(TimeoutException.class).assertNotComplete(); + } else { + to.assertNoErrors().assertComplete(); + } + } + } + + @Test + public void badSourceOther() { + TestHelper.checkBadSourceFlowable(new Function<Flowable<Integer>, Object>() { + @Override + public Object apply(Flowable<Integer> f) throws Exception { + return Maybe.never().timeout(f, Maybe.just(1)); + } + }, false, null, 1, 1); + } + + @Test + public void mainSuccessAfterOtherSignal() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + + new Maybe<Integer>() { + @Override + protected void subscribeActual(@NonNull MaybeObserver<? super Integer> observer) { + observer.onSubscribe(Disposable.empty()); + pp.onNext(2); + observer.onSuccess(1); + } + } + .timeout(pp) + .test() + .assertFailure(TimeoutException.class); + } + + @Test + public void mainSuccess() { + Maybe.just(1) + .timeout(Flowable.never()) + .test() + .assertResult(1); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeTimeoutTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeTimeoutTest.java new file mode 100644 index 0000000000..b5b61a6ffc --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeTimeoutTest.java @@ -0,0 +1,364 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.maybe; + +import static org.junit.Assert.*; + +import java.util.concurrent.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.annotations.NonNull; +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.processors.PublishProcessor; +import io.reactivex.rxjava3.schedulers.Schedulers; +import io.reactivex.rxjava3.subjects.MaybeSubject; +import io.reactivex.rxjava3.testsupport.*; + +public class MaybeTimeoutTest extends RxJavaTest { + + @Test + public void normal() { + Maybe.just(1) + .timeout(1, TimeUnit.DAYS) + .test() + .assertResult(1); + } + + @Test + public void normalMaybe() { + Maybe.just(1) + .timeout(Maybe.timer(1, TimeUnit.DAYS)) + .test() + .assertResult(1); + } + + @Test + public void never() { + Maybe.never() + .timeout(1, TimeUnit.MILLISECONDS) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertFailure(TimeoutException.class); + } + + @Test + public void neverMaybe() { + Maybe.never() + .timeout(Maybe.timer(1, TimeUnit.MILLISECONDS)) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertFailure(TimeoutException.class); + } + + @Test + public void normalFallback() { + Maybe.just(1) + .timeout(1, TimeUnit.DAYS, Maybe.just(2)) + .test() + .assertResult(1); + } + + @Test + public void normalMaybeFallback() { + Maybe.just(1) + .timeout(Maybe.timer(1, TimeUnit.DAYS), Maybe.just(2)) + .test() + .assertResult(1); + } + + @Test + public void neverFallback() { + Maybe.never() + .timeout(1, TimeUnit.MILLISECONDS, Maybe.just(2)) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(2); + } + + @Test + public void neverMaybeFallback() { + Maybe.never() + .timeout(Maybe.timer(1, TimeUnit.MILLISECONDS), Maybe.just(2)) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(2); + } + + @Test + public void neverFallbackScheduler() { + Maybe.never() + .timeout(1, TimeUnit.MILLISECONDS, Schedulers.single(), Maybe.just(2)) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(2); + } + + @Test + public void neverScheduler() { + Maybe.never() + .timeout(1, TimeUnit.MILLISECONDS, Schedulers.single()) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertFailure(TimeoutException.class); + } + + @Test + public void normalFlowableFallback() { + Maybe.just(1) + .timeout(Flowable.timer(1, TimeUnit.DAYS), Maybe.just(2)) + .test() + .assertResult(1); + } + + @Test + public void neverFlowableFallback() { + Maybe.never() + .timeout(Flowable.timer(1, TimeUnit.MILLISECONDS), Maybe.just(2)) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(2); + } + + @Test + public void normalFlowable() { + Maybe.just(1) + .timeout(Flowable.timer(1, TimeUnit.DAYS)) + .test() + .assertResult(1); + } + + @Test + public void neverFlowable() { + Maybe.never() + .timeout(Flowable.timer(1, TimeUnit.MILLISECONDS)) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertFailure(TimeoutException.class); + } + + @Test + public void mainError() { + PublishProcessor<Integer> pp1 = PublishProcessor.create(); + PublishProcessor<Integer> pp2 = PublishProcessor.create(); + + TestObserver<Integer> to = pp1.singleElement().timeout(pp2.singleElement()).test(); + + assertTrue(pp1.hasSubscribers()); + assertTrue(pp2.hasSubscribers()); + + pp1.onError(new TestException()); + + assertFalse(pp1.hasSubscribers()); + assertFalse(pp2.hasSubscribers()); + + to.assertFailure(TestException.class); + } + + @Test + public void otherError() { + PublishProcessor<Integer> pp1 = PublishProcessor.create(); + PublishProcessor<Integer> pp2 = PublishProcessor.create(); + + TestObserver<Integer> to = pp1.singleElement().timeout(pp2.singleElement()).test(); + + assertTrue(pp1.hasSubscribers()); + assertTrue(pp2.hasSubscribers()); + + pp2.onError(new TestException()); + + assertFalse(pp1.hasSubscribers()); + assertFalse(pp2.hasSubscribers()); + + to.assertFailure(TestException.class); + } + + @Test + public void fallbackError() { + PublishProcessor<Integer> pp1 = PublishProcessor.create(); + PublishProcessor<Integer> pp2 = PublishProcessor.create(); + + TestObserver<Integer> to = pp1.singleElement().timeout(pp2.singleElement(), Maybe.<Integer>error(new TestException())).test(); + + assertTrue(pp1.hasSubscribers()); + assertTrue(pp2.hasSubscribers()); + + pp2.onNext(1); + pp2.onComplete(); + + assertFalse(pp1.hasSubscribers()); + assertFalse(pp2.hasSubscribers()); + + to.assertFailure(TestException.class); + } + + @Test + public void fallbackComplete() { + PublishProcessor<Integer> pp1 = PublishProcessor.create(); + PublishProcessor<Integer> pp2 = PublishProcessor.create(); + + TestObserver<Integer> to = pp1.singleElement().timeout(pp2.singleElement(), Maybe.<Integer>empty()).test(); + + assertTrue(pp1.hasSubscribers()); + assertTrue(pp2.hasSubscribers()); + + pp2.onNext(1); + pp2.onComplete(); + + assertFalse(pp1.hasSubscribers()); + assertFalse(pp2.hasSubscribers()); + + to.assertResult(); + } + + @Test + public void mainComplete() { + PublishProcessor<Integer> pp1 = PublishProcessor.create(); + PublishProcessor<Integer> pp2 = PublishProcessor.create(); + + TestObserver<Integer> to = pp1.singleElement().timeout(pp2.singleElement()).test(); + + assertTrue(pp1.hasSubscribers()); + assertTrue(pp2.hasSubscribers()); + + pp1.onComplete(); + + assertFalse(pp1.hasSubscribers()); + assertFalse(pp2.hasSubscribers()); + + to.assertResult(); + } + + @Test + public void otherComplete() { + PublishProcessor<Integer> pp1 = PublishProcessor.create(); + PublishProcessor<Integer> pp2 = PublishProcessor.create(); + + TestObserver<Integer> to = pp1.singleElement().timeout(pp2.singleElement()).test(); + + assertTrue(pp1.hasSubscribers()); + assertTrue(pp2.hasSubscribers()); + + pp2.onComplete(); + + assertFalse(pp1.hasSubscribers()); + assertFalse(pp2.hasSubscribers()); + + to.assertFailure(TimeoutException.class); + } + + @Test + public void dispose() { + PublishProcessor<Integer> pp1 = PublishProcessor.create(); + PublishProcessor<Integer> pp2 = PublishProcessor.create(); + + TestHelper.checkDisposed(pp1.singleElement().timeout(pp2.singleElement())); + } + + @Test + public void dispose2() { + PublishProcessor<Integer> pp1 = PublishProcessor.create(); + PublishProcessor<Integer> pp2 = PublishProcessor.create(); + + TestHelper.checkDisposed(pp1.singleElement().timeout(pp2.singleElement(), Maybe.just(1))); + } + + @Test + public void onErrorRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + TestHelper.trackPluginErrors(); + try { + final PublishProcessor<Integer> pp1 = PublishProcessor.create(); + final PublishProcessor<Integer> pp2 = PublishProcessor.create(); + + TestObserver<Integer> to = pp1.singleElement().timeout(pp2.singleElement()).test(); + + final TestException ex = new TestException(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + pp1.onError(ex); + } + }; + Runnable r2 = new Runnable() { + @Override + public void run() { + pp2.onError(ex); + } + }; + + TestHelper.race(r1, r2); + + to.assertFailure(TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + } + + @Test + public void onCompleteRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final PublishProcessor<Integer> pp1 = PublishProcessor.create(); + final PublishProcessor<Integer> pp2 = PublishProcessor.create(); + + TestObserverEx<Integer> to = pp1.singleElement().timeout(pp2.singleElement()).to(TestHelper.<Integer>testConsumer()); + + Runnable r1 = new Runnable() { + @Override + public void run() { + pp1.onComplete(); + } + }; + Runnable r2 = new Runnable() { + @Override + public void run() { + pp2.onComplete(); + } + }; + + TestHelper.race(r1, r2); + + to.assertSubscribed().assertNoValues(); + + if (to.errors().size() != 0) { + to.assertError(TimeoutException.class).assertNotComplete(); + } else { + to.assertNoErrors().assertComplete(); + } + } + } + + @Test + public void mainSuccessAfterOtherSignal() { + MaybeSubject<Integer> ms = MaybeSubject.create(); + + new Maybe<Integer>() { + @Override + protected void subscribeActual(@NonNull MaybeObserver<? super Integer> observer) { + observer.onSubscribe(Disposable.empty()); + ms.onSuccess(2); + observer.onSuccess(1); + } + } + .timeout(ms) + .test() + .assertFailure(TimeoutException.class); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeTimerTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeTimerTest.java new file mode 100644 index 0000000000..05ffdf0562 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeTimerTest.java @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.maybe; + +import static org.junit.Assert.assertTrue; + +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.schedulers.*; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class MaybeTimerTest extends RxJavaTest { + + @Test + public void dispose() { + TestHelper.checkDisposed(Maybe.timer(1, TimeUnit.SECONDS, new TestScheduler())); + } + + @Test + public void timerInterruptible() throws Exception { + ScheduledExecutorService exec = Executors.newSingleThreadScheduledExecutor(); + try { + for (Scheduler s : new Scheduler[] { Schedulers.single(), Schedulers.computation(), Schedulers.newThread(), Schedulers.io(), Schedulers.from(exec, true) }) { + final AtomicBoolean interrupted = new AtomicBoolean(); + TestObserver<Long> to = Maybe.timer(1, TimeUnit.MILLISECONDS, s) + .map(new Function<Long, Long>() { + @Override + public Long apply(Long v) throws Exception { + try { + Thread.sleep(3000); + } catch (InterruptedException ex) { + interrupted.set(true); + } + return v; + } + }) + .test(); + + Thread.sleep(500); + + to.dispose(); + + Thread.sleep(500); + + assertTrue(s.getClass().getSimpleName(), interrupted.get()); + } + } finally { + exec.shutdown(); + } + } + +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeTimestampTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeTimestampTest.java new file mode 100644 index 0000000000..935147a906 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeTimestampTest.java @@ -0,0 +1,111 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.maybe; + +import java.util.concurrent.TimeUnit; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.Maybe; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.schedulers.*; +import io.reactivex.rxjava3.subjects.MaybeSubject; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class MaybeTimestampTest { + + @Test + public void just() { + Maybe.just(1) + .timestamp() + .test() + .assertValueCount(1) + .assertNoErrors() + .assertComplete(); + } + + @Test + public void empty() { + Maybe.empty() + .timestamp() + .test() + .assertResult(); + } + + @Test + public void error() { + Maybe.error(new TestException()) + .timestamp() + .test() + .assertFailure(TestException.class); + } + + @Test + public void justSeconds() { + Maybe.just(1) + .timestamp(TimeUnit.SECONDS) + .test() + .assertValueCount(1) + .assertNoErrors() + .assertComplete(); + } + + @Test + public void justScheduler() { + Maybe.just(1) + .timestamp(Schedulers.single()) + .test() + .assertValueCount(1) + .assertNoErrors() + .assertComplete(); + } + + @Test + public void justSecondsScheduler() { + Maybe.just(1) + .timestamp(TimeUnit.SECONDS, Schedulers.single()) + .test() + .assertValueCount(1) + .assertNoErrors() + .assertComplete(); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeMaybe(m -> m.timestamp()); + } + + @Test + public void dispose() { + TestHelper.checkDisposed(MaybeSubject.create().timestamp()); + } + + @Test + public void timeInfo() { + TestScheduler scheduler = new TestScheduler(); + + MaybeSubject<Integer> ms = MaybeSubject.create(); + + TestObserver<Timed<Integer>> to = ms + .timestamp(scheduler) + .test(); + + scheduler.advanceTimeBy(1000, TimeUnit.MILLISECONDS); + + ms.onSuccess(1); + + to.assertResult(new Timed<>(1, 1000L, TimeUnit.MILLISECONDS)); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeToCompletableTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeToCompletableTest.java new file mode 100644 index 0000000000..4bae2cc98f --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeToCompletableTest.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.maybe; + +import static org.junit.Assert.assertSame; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.fuseable.HasUpstreamMaybeSource; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class MaybeToCompletableTest extends RxJavaTest { + + @SuppressWarnings("unchecked") + @Test + public void source() { + Maybe<Integer> source = Maybe.just(1); + + assertSame(source, ((HasUpstreamMaybeSource<Integer>)source.ignoreElement().toMaybe()).source()); + } + + @Test + public void dispose() { + TestHelper.checkDisposed(Maybe.never().ignoreElement().toMaybe()); + } + + @Test + public void successToComplete() { + Maybe.just(1) + .ignoreElement() + .test() + .assertResult(); + } + + @Test + public void doubleSubscribe() { + TestHelper.checkDoubleOnSubscribeMaybeToCompletable(new Function<Maybe<Object>, CompletableSource>() { + @Override + public CompletableSource apply(Maybe<Object> m) throws Exception { + return m.ignoreElement(); + } + }); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeToFlowableTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeToFlowableTest.java new file mode 100644 index 0000000000..66e94da8d0 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeToFlowableTest.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.maybe; + +import static org.junit.Assert.assertSame; + +import org.junit.Test; +import org.reactivestreams.Publisher; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.fuseable.HasUpstreamMaybeSource; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class MaybeToFlowableTest extends RxJavaTest { + + @Test + public void source() { + Maybe<Integer> m = Maybe.just(1); + + assertSame(m, (((HasUpstreamMaybeSource<?>)m.toFlowable()).source())); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeMaybeToFlowable(new Function<Maybe<Object>, Publisher<Object>>() { + @Override + public Publisher<Object> apply(Maybe<Object> m) throws Exception { + return m.toFlowable(); + } + }); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeToFutureTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeToFutureTest.java new file mode 100644 index 0000000000..b36a299cd4 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeToFutureTest.java @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.maybe; + +import static org.junit.Assert.*; + +import java.util.concurrent.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.schedulers.Schedulers; +import io.reactivex.rxjava3.subjects.MaybeSubject; + +public class MaybeToFutureTest extends RxJavaTest { + + @Test + public void success() throws Exception { + assertEquals((Integer)1, Maybe.just(1) + .subscribeOn(Schedulers.computation()) + .toFuture() + .get()); + } + + @Test + public void empty() throws Exception { + assertNull(Maybe.empty() + .subscribeOn(Schedulers.computation()) + .toFuture() + .get()); + } + + @Test + public void error() throws InterruptedException { + try { + Maybe.error(new TestException()) + .subscribeOn(Schedulers.computation()) + .toFuture() + .get(); + + fail("Should have thrown!"); + } catch (ExecutionException ex) { + assertTrue("" + ex.getCause(), ex.getCause() instanceof TestException); + } + } + + @Test + public void cancel() { + MaybeSubject<Integer> ms = MaybeSubject.create(); + + Future<Integer> f = ms.toFuture(); + + assertTrue(ms.hasObservers()); + + f.cancel(true); + + assertFalse(ms.hasObservers()); + } + + @Test + public void cancel2() { + MaybeSubject<Integer> ms = MaybeSubject.create(); + + Future<Integer> f = ms.toFuture(); + + assertTrue(ms.hasObservers()); + + f.cancel(false); + + assertFalse(ms.hasObservers()); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeToObservableTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeToObservableTest.java new file mode 100644 index 0000000000..6747203723 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeToObservableTest.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.maybe; + +import static org.junit.Assert.assertSame; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.fuseable.HasUpstreamMaybeSource; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class MaybeToObservableTest extends RxJavaTest { + + @Test + public void source() { + Maybe<Integer> m = Maybe.just(1); + + assertSame(m, (((HasUpstreamMaybeSource<?>)m.toObservable()).source())); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeMaybeToObservable(new Function<Maybe<Object>, ObservableSource<Object>>() { + @Override + public ObservableSource<Object> apply(Maybe<Object> m) throws Exception { + return m.toObservable(); + } + }); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeToSingleTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeToSingleTest.java new file mode 100644 index 0000000000..1ee84470f2 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeToSingleTest.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.maybe; + +import static org.junit.Assert.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.fuseable.HasUpstreamMaybeSource; +import io.reactivex.rxjava3.processors.PublishProcessor; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class MaybeToSingleTest extends RxJavaTest { + + @Test + public void source() { + Maybe<Integer> m = Maybe.just(1); + Single<Integer> s = m.toSingle(); + + assertTrue(s.getClass().toString(), s instanceof HasUpstreamMaybeSource); + + assertSame(m, (((HasUpstreamMaybeSource<?>)s).source())); + } + + @Test + public void dispose() { + TestHelper.checkDisposed(PublishProcessor.create().singleElement().toSingle()); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeMaybeToSingle(new Function<Maybe<Object>, SingleSource<Object>>() { + @Override + public SingleSource<Object> apply(Maybe<Object> m) throws Exception { + return m.toSingle(); + } + }); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeUnsubscribeOnTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeUnsubscribeOnTest.java new file mode 100644 index 0000000000..3159fb411e --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeUnsubscribeOnTest.java @@ -0,0 +1,144 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.maybe; + +import static org.junit.Assert.*; + +import java.util.concurrent.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.processors.PublishProcessor; +import io.reactivex.rxjava3.schedulers.Schedulers; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class MaybeUnsubscribeOnTest extends RxJavaTest { + + @Test + public void normal() throws Exception { + PublishProcessor<Integer> pp = PublishProcessor.create(); + + final String[] name = { null }; + + final CountDownLatch cdl = new CountDownLatch(1); + + pp.doOnCancel(new Action() { + @Override + public void run() throws Exception { + name[0] = Thread.currentThread().getName(); + cdl.countDown(); + } + }) + .singleElement() + .unsubscribeOn(Schedulers.single()) + .test(true) + ; + + assertTrue(cdl.await(5, TimeUnit.SECONDS)); + + int times = 10; + + while (times-- > 0 && pp.hasSubscribers()) { + Thread.sleep(100); + } + + assertFalse(pp.hasSubscribers()); + + assertNotEquals(Thread.currentThread().getName(), name[0]); + } + + @Test + public void just() { + Maybe.just(1) + .unsubscribeOn(Schedulers.single()) + .test() + .assertResult(1); + } + + @Test + public void error() { + Maybe.<Integer>error(new TestException()) + .unsubscribeOn(Schedulers.single()) + .test() + .assertFailure(TestException.class); + } + + @Test + public void empty() { + Maybe.empty() + .unsubscribeOn(Schedulers.single()) + .test() + .assertResult(); + } + + @Test + public void dispose() { + TestHelper.checkDisposed(Maybe.just(1) + .unsubscribeOn(Schedulers.single())); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeMaybe(new Function<Maybe<Object>, MaybeSource<Object>>() { + @Override + public MaybeSource<Object> apply(Maybe<Object> v) throws Exception { + return v.unsubscribeOn(Schedulers.single()); + } + }); + } + + @Test + public void disposeRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + PublishProcessor<Integer> pp = PublishProcessor.create(); + + final Disposable[] ds = { null }; + pp.singleElement().unsubscribeOn(Schedulers.computation()) + .subscribe(new MaybeObserver<Integer>() { + @Override + public void onSubscribe(Disposable d) { + ds[0] = d; + } + + @Override + public void onSuccess(Integer value) { + + } + + @Override + public void onError(Throwable e) { + + } + + @Override + public void onComplete() { + + } + }); + + Runnable r = new Runnable() { + @Override + public void run() { + ds[0].dispose(); + } + }; + + TestHelper.race(r, r); + } + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeUsingTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeUsingTest.java new file mode 100644 index 0000000000..0f083c93d2 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeUsingTest.java @@ -0,0 +1,632 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.maybe; + +import static org.junit.Assert.*; + +import java.util.List; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.*; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.subjects.PublishSubject; +import io.reactivex.rxjava3.testsupport.*; + +public class MaybeUsingTest extends RxJavaTest { + + @Test + public void resourceSupplierThrows() { + + Maybe.using(new Supplier<Object>() { + @Override + public Object get() throws Exception { + throw new TestException(); + } + }, new Function<Object, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Object v) throws Exception { + return Maybe.just(1); + } + }, new Consumer<Object>() { + @Override + public void accept(Object d) throws Exception { + + } + }) + .test() + .assertFailure(TestException.class); + } + + @Test + public void errorEager() { + + Maybe.using(new Supplier<Object>() { + @Override + public Object get() throws Exception { + return 1; + } + }, new Function<Object, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Object v) throws Exception { + return Maybe.error(new TestException()); + } + }, new Consumer<Object>() { + @Override + public void accept(Object d) throws Exception { + + } + }, true) + .test() + .assertFailure(TestException.class); + } + + @Test + public void emptyEager() { + + Maybe.using(new Supplier<Object>() { + @Override + public Object get() throws Exception { + return 1; + } + }, new Function<Object, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Object v) throws Exception { + return Maybe.empty(); + } + }, new Consumer<Object>() { + @Override + public void accept(Object d) throws Exception { + + } + }, true) + .test() + .assertResult(); + } + + @Test + public void errorNonEager() { + + Maybe.using(new Supplier<Object>() { + @Override + public Object get() throws Exception { + return 1; + } + }, new Function<Object, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Object v) throws Exception { + return Maybe.error(new TestException()); + } + }, new Consumer<Object>() { + @Override + public void accept(Object d) throws Exception { + + } + }, false) + .test() + .assertFailure(TestException.class); + } + + @Test + public void emptyNonEager() { + + Maybe.using(new Supplier<Object>() { + @Override + public Object get() throws Exception { + return 1; + } + }, new Function<Object, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Object v) throws Exception { + return Maybe.empty(); + } + }, new Consumer<Object>() { + @Override + public void accept(Object d) throws Exception { + + } + }, false) + .test() + .assertResult(); + } + + @Test + public void supplierCrashEager() { + + Maybe.using(new Supplier<Object>() { + @Override + public Object get() throws Exception { + return 1; + } + }, new Function<Object, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Object v) throws Exception { + throw new TestException(); + } + }, new Consumer<Object>() { + @Override + public void accept(Object d) throws Exception { + + } + }, true) + .test() + .assertFailure(TestException.class); + } + + @Test + public void supplierCrashNonEager() { + + Maybe.using(new Supplier<Object>() { + @Override + public Object get() throws Exception { + return 1; + } + }, new Function<Object, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Object v) throws Exception { + throw new TestException(); + } + }, new Consumer<Object>() { + @Override + public void accept(Object d) throws Exception { + + } + }, false) + .test() + .assertFailure(TestException.class); + } + + @Test + public void supplierAndDisposerCrashEager() { + TestObserverEx<Integer> to = Maybe.using(new Supplier<Object>() { + @Override + public Object get() throws Exception { + return 1; + } + }, new Function<Object, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Object v) throws Exception { + throw new TestException("Main"); + } + }, new Consumer<Object>() { + @Override + public void accept(Object d) throws Exception { + throw new TestException("Disposer"); + } + }, true) + .to(TestHelper.<Integer>testConsumer()) + .assertFailure(CompositeException.class); + + List<Throwable> list = TestHelper.compositeList(to.errors().get(0)); + + TestHelper.assertError(list, 0, TestException.class, "Main"); + TestHelper.assertError(list, 1, TestException.class, "Disposer"); + } + + @Test + public void supplierAndDisposerCrashNonEager() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + Maybe.using(new Supplier<Object>() { + @Override + public Object get() throws Exception { + return 1; + } + }, new Function<Object, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Object v) throws Exception { + throw new TestException("Main"); + } + }, new Consumer<Object>() { + @Override + public void accept(Object d) throws Exception { + throw new TestException("Disposer"); + } + }, false) + .to(TestHelper.<Integer>testConsumer()) + .assertFailureAndMessage(TestException.class, "Main"); + + TestHelper.assertUndeliverable(errors, 0, TestException.class, "Disposer"); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void dispose() { + final int[] call = {0 }; + + TestObserver<Integer> to = Maybe.using(new Supplier<Object>() { + @Override + public Object get() throws Exception { + return 1; + } + }, new Function<Object, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Object v) throws Exception { + return Maybe.never(); + } + }, new Consumer<Object>() { + @Override + public void accept(Object d) throws Exception { + call[0]++; + } + }, false) + .test(); + + to.dispose(); + + assertEquals(1, call[0]); + } + + @Test + public void disposeCrashes() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + TestObserver<Integer> to = Maybe.using(new Supplier<Object>() { + @Override + public Object get() throws Exception { + return 1; + } + }, new Function<Object, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Object v) throws Exception { + return Maybe.never(); + } + }, new Consumer<Object>() { + @Override + public void accept(Object d) throws Exception { + throw new TestException(); + } + }, false) + .test(); + + to.dispose(); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void isDisposed() { + TestHelper.checkDisposed(Maybe.using(new Supplier<Object>() { + @Override + public Object get() throws Exception { + return 1; + } + }, new Function<Object, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Object v) throws Exception { + return Maybe.never(); + } + }, new Consumer<Object>() { + @Override + public void accept(Object d) throws Exception { + + } + }, false)); + } + + @Test + public void justDisposerCrashes() { + Maybe.using(new Supplier<Object>() { + @Override + public Object get() throws Exception { + return 1; + } + }, new Function<Object, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Object v) throws Exception { + return Maybe.just(1); + } + }, new Consumer<Object>() { + @Override + public void accept(Object d) throws Exception { + throw new TestException("Disposer"); + } + }, true) + .test() + .assertFailure(TestException.class); + } + + @Test + public void emptyDisposerCrashes() { + Maybe.using(new Supplier<Object>() { + @Override + public Object get() throws Exception { + return 1; + } + }, new Function<Object, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Object v) throws Exception { + return Maybe.empty(); + } + }, new Consumer<Object>() { + @Override + public void accept(Object d) throws Exception { + throw new TestException("Disposer"); + } + }, true) + .test() + .assertFailure(TestException.class); + } + + @Test + public void errorDisposerCrash() { + TestObserverEx<Integer> to = Maybe.using(new Supplier<Object>() { + @Override + public Object get() throws Exception { + return 1; + } + }, new Function<Object, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Object v) throws Exception { + return Maybe.error(new TestException("Main")); + } + }, new Consumer<Object>() { + @Override + public void accept(Object d) throws Exception { + throw new TestException("Disposer"); + } + }, true) + .to(TestHelper.<Integer>testConsumer()) + .assertFailure(CompositeException.class); + + List<Throwable> list = TestHelper.compositeList(to.errors().get(0)); + + TestHelper.assertError(list, 0, TestException.class, "Main"); + TestHelper.assertError(list, 1, TestException.class, "Disposer"); + } + + @Test + public void doubleOnSubscribe() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + Maybe.using(new Supplier<Object>() { + @Override + public Object get() throws Exception { + return 1; + } + }, new Function<Object, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Object v) throws Exception { + return Maybe.wrap(new MaybeSource<Integer>() { + @Override + public void subscribe(MaybeObserver<? super Integer> observer) { + Disposable d1 = Disposable.empty(); + + observer.onSubscribe(d1); + + Disposable d2 = Disposable.empty(); + + observer.onSubscribe(d2); + + assertFalse(d1.isDisposed()); + + assertTrue(d2.isDisposed()); + } + }); + } + }, new Consumer<Object>() { + @Override + public void accept(Object d) throws Exception { + + } + }, false).test(); + TestHelper.assertError(errors, 0, IllegalStateException.class, "Disposable already set!"); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void successDisposeRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + + final PublishSubject<Integer> ps = PublishSubject.create(); + + final TestObserver<Integer> to = Maybe.using(new Supplier<Object>() { + @Override + public Object get() throws Exception { + return 1; + } + }, new Function<Object, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Object v) throws Exception { + return ps.lastElement(); + } + }, new Consumer<Object>() { + @Override + public void accept(Object d) throws Exception { + } + }, true) + .test(); + + ps.onNext(1); + + Runnable r1 = new Runnable() { + @Override + public void run() { + to.dispose(); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + ps.onComplete(); + } + }; + + TestHelper.race(r1, r2); + } + } + + @Test + @SuppressUndeliverable + public void errorDisposeRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + + final PublishSubject<Integer> ps = PublishSubject.create(); + + final TestObserver<Integer> to = Maybe.using(new Supplier<Object>() { + @Override + public Object get() throws Exception { + return 1; + } + }, new Function<Object, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Object v) throws Exception { + return ps.firstElement(); + } + }, new Consumer<Object>() { + @Override + public void accept(Object d) throws Exception { + } + }, true) + .test(); + + final TestException ex = new TestException(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + to.dispose(); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + ps.onError(ex); + } + }; + + TestHelper.race(r1, r2); + } + } + + @Test + public void emptyDisposeRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + + final PublishSubject<Integer> ps = PublishSubject.create(); + + final TestObserver<Integer> to = Maybe.using(new Supplier<Object>() { + @Override + public Object get() throws Exception { + return 1; + } + }, new Function<Object, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Object v) throws Exception { + return ps.firstElement(); + } + }, new Consumer<Object>() { + @Override + public void accept(Object d) throws Exception { + + } + }, true) + .test(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + to.dispose(); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + ps.onComplete(); + } + }; + + TestHelper.race(r1, r2); + } + } + + @Test + public void eagerDisposeResourceThenDisposeUpstream() { + final StringBuilder sb = new StringBuilder(); + + TestObserver<Integer> to = Maybe.using(Functions.justSupplier(1), + new Function<Integer, Maybe<Integer>>() { + @Override + public Maybe<Integer> apply(Integer t) throws Throwable { + return Maybe.<Integer>never() + .doOnDispose(new Action() { + @Override + public void run() throws Throwable { + sb.append("Dispose"); + } + }) + ; + } + }, new Consumer<Integer>() { + @Override + public void accept(Integer t) throws Throwable { + sb.append("Resource"); + } + }, true) + .test() + ; + to.assertEmpty(); + + to.dispose(); + + assertEquals("ResourceDispose", sb.toString()); + } + + @Test + public void nonEagerDisposeUpstreamThenDisposeResource() { + final StringBuilder sb = new StringBuilder(); + + TestObserver<Integer> to = Maybe.using(Functions.justSupplier(1), + new Function<Integer, Maybe<Integer>>() { + @Override + public Maybe<Integer> apply(Integer t) throws Throwable { + return Maybe.<Integer>never() + .doOnDispose(new Action() { + @Override + public void run() throws Throwable { + sb.append("Dispose"); + } + }) + ; + } + }, new Consumer<Integer>() { + @Override + public void accept(Integer t) throws Throwable { + sb.append("Resource"); + } + }, false) + .test() + ; + to.assertEmpty(); + + to.dispose(); + + assertEquals("DisposeResource", sb.toString()); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeZipArrayTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeZipArrayTest.java new file mode 100644 index 0000000000..2cbf8e4263 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeZipArrayTest.java @@ -0,0 +1,241 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.maybe; + +import static org.junit.Assert.*; + +import java.util.*; +import java.util.concurrent.atomic.AtomicReference; + +import org.junit.Test; + +import io.reactivex.rxjava3.annotations.NonNull; +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.processors.PublishProcessor; +import io.reactivex.rxjava3.subjects.MaybeSubject; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class MaybeZipArrayTest extends RxJavaTest { + + final BiFunction<Object, Object, Object> addString = new BiFunction<Object, Object, Object>() { + @Override + public Object apply(Object a, Object b) throws Exception { + return "" + a + b; + } + }; + + final Function3<Object, Object, Object, Object> addString3 = new Function3<Object, Object, Object, Object>() { + @Override + public Object apply(Object a, Object b, Object c) throws Exception { + return "" + a + b + c; + } + }; + + @Test + public void firstError() { + Maybe.zip(Maybe.error(new TestException()), Maybe.just(1), addString) + .test() + .assertFailure(TestException.class); + } + + @Test + public void secondError() { + Maybe.zip(Maybe.just(1), Maybe.<Integer>error(new TestException()), addString) + .test() + .assertFailure(TestException.class); + } + + @Test + public void dispose() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + + TestObserver<Object> to = Maybe.zip(pp.singleElement(), pp.singleElement(), addString) + .test(); + + assertTrue(pp.hasSubscribers()); + + to.dispose(); + + assertFalse(pp.hasSubscribers()); + } + + @Test + public void zipperThrows() { + Maybe.zip(Maybe.just(1), Maybe.just(2), new BiFunction<Integer, Integer, Object>() { + @Override + public Object apply(Integer a, Integer b) throws Exception { + throw new TestException(); + } + }) + .test() + .assertFailure(TestException.class); + } + + @Test + public void zipperReturnsNull() { + Maybe.zip(Maybe.just(1), Maybe.just(2), new BiFunction<Integer, Integer, Object>() { + @Override + public Object apply(Integer a, Integer b) throws Exception { + return null; + } + }) + .test() + .assertFailure(NullPointerException.class); + } + + @Test + public void middleError() { + PublishProcessor<Integer> pp0 = PublishProcessor.create(); + PublishProcessor<Integer> pp1 = PublishProcessor.create(); + + TestObserver<Object> to = Maybe.zip(pp0.singleElement(), pp1.singleElement(), pp0.singleElement(), addString3) + .test(); + + pp1.onError(new TestException()); + + assertFalse(pp0.hasSubscribers()); + + to.assertFailure(TestException.class); + } + + @Test + public void innerErrorRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + final PublishProcessor<Integer> pp0 = PublishProcessor.create(); + final PublishProcessor<Integer> pp1 = PublishProcessor.create(); + + final TestObserver<Object> to = Maybe.zip(pp0.singleElement(), pp1.singleElement(), addString) + .test(); + + final TestException ex = new TestException(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + pp0.onError(ex); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + pp1.onError(ex); + } + }; + + TestHelper.race(r1, r2); + + to.assertFailure(TestException.class); + + if (!errors.isEmpty()) { + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } + } finally { + RxJavaPlugins.reset(); + } + } + } + + @Test(expected = NullPointerException.class) + public void zipArrayOneIsNull() { + Maybe.zipArray(new Function<Object[], Object>() { + @Override + public Object apply(Object[] v) { + return 1; + } + }, Maybe.just(1), null) + .blockingGet(); + } + + @Test + public void singleSourceZipperReturnsNull() { + Maybe.zipArray(Functions.justFunction(null), Maybe.just(1)) + .to(TestHelper.<Object>testConsumer()) + .assertFailureAndMessage(NullPointerException.class, "The zipper returned a null value"); + } + + @Test + public void dispose2() { + TestHelper.checkDisposed(Maybe.zipArray(v -> v, MaybeSubject.create(), MaybeSubject.create())); + } + + @Test + public void bothComplete() { + AtomicReference<MaybeObserver<? super Integer>> ref1 = new AtomicReference<>(); + AtomicReference<MaybeObserver<? super Integer>> ref2 = new AtomicReference<>(); + + Maybe<Integer> m1 = new Maybe<Integer>() { + @Override + protected void subscribeActual(@NonNull MaybeObserver<? super Integer> observer) { + ref1.set(observer); + } + }; + Maybe<Integer> m2 = new Maybe<Integer>() { + @Override + protected void subscribeActual(@NonNull MaybeObserver<? super Integer> observer) { + ref2.set(observer); + } + }; + + TestObserver<Object[]> to = Maybe.zipArray(v -> v, m1, m2) + .test(); + + ref1.get().onSubscribe(Disposable.empty()); + ref2.get().onSubscribe(Disposable.empty()); + + ref1.get().onComplete(); + ref2.get().onComplete(); + + to.assertResult(); + } + + @Test + public void bothSucceed() { + Maybe.zipArray(v -> Arrays.asList(v), Maybe.just(1), Maybe.just(2)) + .test() + .assertResult(Arrays.asList(1, 2)); + } + + @Test + public void oneSourceOnly() { + Maybe.zipArray(v -> Arrays.asList(v), Maybe.just(1)) + .test() + .assertResult(Arrays.asList(1)); + } + + @Test + public void onSuccessAfterDispose() { + AtomicReference<MaybeObserver<? super Integer>> emitter = new AtomicReference<>(); + + TestObserver<List<Object>> to = Maybe.zipArray(Arrays::asList, + (MaybeSource<Integer>)o -> emitter.set(o), Maybe.<Integer>never()) + .test(); + + emitter.get().onSubscribe(Disposable.empty()); + + to.dispose(); + + emitter.get().onSuccess(1); + + to.assertEmpty(); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeZipIterableTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeZipIterableTest.java new file mode 100644 index 0000000000..2d60c22157 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/maybe/MaybeZipIterableTest.java @@ -0,0 +1,233 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.maybe; + +import static org.junit.Assert.*; + +import java.util.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.internal.util.CrashingMappedIterable; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.processors.PublishProcessor; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class MaybeZipIterableTest extends RxJavaTest { + + final Function<Object[], Object> addString = new Function<Object[], Object>() { + @Override + public Object apply(Object[] a) throws Exception { + return Arrays.toString(a); + } + }; + + @Test + public void firstError() { + Maybe.zip(Arrays.asList(Maybe.error(new TestException()), Maybe.just(1)), addString) + .test() + .assertFailure(TestException.class); + } + + @Test + public void secondError() { + Maybe.zip(Arrays.asList(Maybe.just(1), Maybe.<Integer>error(new TestException())), addString) + .test() + .assertFailure(TestException.class); + } + + @Test + public void dispose() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + + TestObserver<Object> to = Maybe.zip(Arrays.asList(pp.singleElement(), pp.singleElement()), addString) + .test(); + + assertTrue(pp.hasSubscribers()); + + to.dispose(); + + assertFalse(pp.hasSubscribers()); + } + + @Test + public void zipperThrows() { + Maybe.zip(Arrays.asList(Maybe.just(1), Maybe.just(2)), new Function<Object[], Object>() { + @Override + public Object apply(Object[] b) throws Exception { + throw new TestException(); + } + }) + .test() + .assertFailure(TestException.class); + } + + @Test + public void zipperReturnsNull() { + Maybe.zip(Arrays.asList(Maybe.just(1), Maybe.just(2)), new Function<Object[], Object>() { + @Override + public Object apply(Object[] a) throws Exception { + return null; + } + }) + .test() + .assertFailure(NullPointerException.class); + } + + @Test + public void middleError() { + PublishProcessor<Integer> pp0 = PublishProcessor.create(); + PublishProcessor<Integer> pp1 = PublishProcessor.create(); + + TestObserver<Object> to = Maybe.zip( + Arrays.asList(pp0.singleElement(), pp1.singleElement(), pp0.singleElement()), addString) + .test(); + + pp1.onError(new TestException()); + + assertFalse(pp0.hasSubscribers()); + + to.assertFailure(TestException.class); + } + + @Test + public void innerErrorRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + final PublishProcessor<Integer> pp0 = PublishProcessor.create(); + final PublishProcessor<Integer> pp1 = PublishProcessor.create(); + + final TestObserver<Object> to = Maybe.zip( + Arrays.asList(pp0.singleElement(), pp1.singleElement()), addString) + .test(); + + final TestException ex = new TestException(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + pp0.onError(ex); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + pp1.onError(ex); + } + }; + + TestHelper.race(r1, r2); + + to.assertFailure(TestException.class); + + if (!errors.isEmpty()) { + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } + } finally { + RxJavaPlugins.reset(); + } + } + } + + @Test + public void iteratorThrows() { + Maybe.zip(new CrashingMappedIterable<>(1, 100, 100, new Function<Integer, Maybe<Integer>>() { + @Override + public Maybe<Integer> apply(Integer v) throws Exception { + return Maybe.just(v); + } + }), addString) + .to(TestHelper.<Object>testConsumer()) + .assertFailureAndMessage(TestException.class, "iterator()"); + } + + @Test + public void hasNextThrows() { + Maybe.zip(new CrashingMappedIterable<>(100, 20, 100, new Function<Integer, Maybe<Integer>>() { + @Override + public Maybe<Integer> apply(Integer v) throws Exception { + return Maybe.just(v); + } + }), addString) + .to(TestHelper.<Object>testConsumer()) + .assertFailureAndMessage(TestException.class, "hasNext()"); + } + + @Test + public void nextThrows() { + Maybe.zip(new CrashingMappedIterable<>(100, 100, 5, new Function<Integer, Maybe<Integer>>() { + @Override + public Maybe<Integer> apply(Integer v) throws Exception { + return Maybe.just(v); + } + }), addString) + .to(TestHelper.<Object>testConsumer()) + .assertFailureAndMessage(TestException.class, "next()"); + } + + @Test(expected = NullPointerException.class) + public void zipIterableOneIsNull() { + Maybe.zip(Arrays.asList(null, Maybe.just(1)), new Function<Object[], Object>() { + @Override + public Object apply(Object[] v) { + return 1; + } + }) + .blockingGet(); + } + + @Test(expected = NullPointerException.class) + public void zipIterableTwoIsNull() { + Maybe.zip(Arrays.asList(Maybe.just(1), null), new Function<Object[], Object>() { + @Override + public Object apply(Object[] v) { + return 1; + } + }) + .blockingGet(); + } + + @Test + public void singleSourceZipperReturnsNull() { + Maybe.zipArray(Functions.justFunction(null), Maybe.just(1)) + .to(TestHelper.<Object>testConsumer()) + .assertFailureAndMessage(NullPointerException.class, "The zipper returned a null value"); + } + + @Test + public void maybeSourcesInIterable() { + MaybeSource<Integer> source = new MaybeSource<Integer>() { + @Override + public void subscribe(MaybeObserver<? super Integer> observer) { + Maybe.just(1).subscribe(observer); + } + }; + + Maybe.zip(Arrays.asList(source, source), new Function<Object[], Integer>() { + @Override + public Integer apply(Object[] t) throws Throwable { + return 2; + } + }) + .test() + .assertResult(2); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/mixed/CompletableAndThenObservableTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/mixed/CompletableAndThenObservableTest.java new file mode 100644 index 0000000000..a1e087bdf2 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/mixed/CompletableAndThenObservableTest.java @@ -0,0 +1,115 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.mixed; + +import static org.junit.Assert.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.subjects.*; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class CompletableAndThenObservableTest extends RxJavaTest { + + @Test + public void cancelMain() { + CompletableSubject cs = CompletableSubject.create(); + PublishSubject<Integer> ps = PublishSubject.create(); + + TestObserver<Integer> to = cs.andThen(ps) + .test(); + + assertTrue(cs.hasObservers()); + assertFalse(ps.hasObservers()); + + to.dispose(); + + assertFalse(cs.hasObservers()); + assertFalse(ps.hasObservers()); + } + + @Test + public void cancelOther() { + CompletableSubject cs = CompletableSubject.create(); + PublishSubject<Integer> ps = PublishSubject.create(); + + TestObserver<Integer> to = cs.andThen(ps) + .test(); + + assertTrue(cs.hasObservers()); + assertFalse(ps.hasObservers()); + + cs.onComplete(); + + assertFalse(cs.hasObservers()); + assertTrue(ps.hasObservers()); + + to.dispose(); + + assertFalse(cs.hasObservers()); + assertFalse(ps.hasObservers()); + } + + @Test + public void errorMain() { + CompletableSubject cs = CompletableSubject.create(); + PublishSubject<Integer> ps = PublishSubject.create(); + + TestObserver<Integer> to = cs.andThen(ps) + .test(); + + assertTrue(cs.hasObservers()); + assertFalse(ps.hasObservers()); + + cs.onError(new TestException()); + + assertFalse(cs.hasObservers()); + assertFalse(ps.hasObservers()); + + to.assertFailure(TestException.class); + } + + @Test + public void errorOther() { + CompletableSubject cs = CompletableSubject.create(); + PublishSubject<Integer> ps = PublishSubject.create(); + + TestObserver<Integer> to = cs.andThen(ps) + .test(); + + assertTrue(cs.hasObservers()); + assertFalse(ps.hasObservers()); + + cs.onComplete(); + + assertFalse(cs.hasObservers()); + assertTrue(ps.hasObservers()); + + ps.onError(new TestException()); + + assertFalse(cs.hasObservers()); + assertFalse(ps.hasObservers()); + + to.assertFailure(TestException.class); + } + + @Test + public void isDisposed() { + TestHelper.checkDisposed(Completable.never().andThen(Observable.never())); + } + +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/mixed/CompletableAndThenPublisherTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/mixed/CompletableAndThenPublisherTest.java new file mode 100644 index 0000000000..bd9c9aa2d9 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/mixed/CompletableAndThenPublisherTest.java @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.mixed; + +import static org.junit.Assert.*; + +import org.junit.Test; +import org.reactivestreams.Publisher; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.processors.PublishProcessor; +import io.reactivex.rxjava3.subjects.CompletableSubject; +import io.reactivex.rxjava3.subscribers.TestSubscriber; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class CompletableAndThenPublisherTest extends RxJavaTest { + + @Test + public void cancelMain() { + CompletableSubject cs = CompletableSubject.create(); + PublishProcessor<Integer> pp = PublishProcessor.create(); + + TestSubscriber<Integer> ts = cs.andThen(pp) + .test(); + + assertTrue(cs.hasObservers()); + assertFalse(pp.hasSubscribers()); + + ts.cancel(); + + assertFalse(cs.hasObservers()); + assertFalse(pp.hasSubscribers()); + } + + @Test + public void cancelOther() { + CompletableSubject cs = CompletableSubject.create(); + PublishProcessor<Integer> pp = PublishProcessor.create(); + + TestSubscriber<Integer> ts = cs.andThen(pp) + .test(); + + assertTrue(cs.hasObservers()); + assertFalse(pp.hasSubscribers()); + + cs.onComplete(); + + assertFalse(cs.hasObservers()); + assertTrue(pp.hasSubscribers()); + + ts.cancel(); + + assertFalse(cs.hasObservers()); + assertFalse(pp.hasSubscribers()); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeCompletableToFlowable(new Function<Completable, Publisher<Object>>() { + @Override + public Publisher<Object> apply(Completable m) throws Exception { + return m.andThen(Flowable.never()); + } + }); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/mixed/FlowableConcatMapCompletableTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/mixed/FlowableConcatMapCompletableTest.java new file mode 100644 index 0000000000..44acad42b1 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/mixed/FlowableConcatMapCompletableTest.java @@ -0,0 +1,492 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.mixed; + +import static org.junit.Assert.*; + +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.Test; +import org.reactivestreams.Subscriber; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.internal.subscriptions.BooleanSubscription; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.processors.*; +import io.reactivex.rxjava3.subjects.CompletableSubject; +import io.reactivex.rxjava3.testsupport.*; + +public class FlowableConcatMapCompletableTest extends RxJavaTest { + + @Test + public void simple() { + Flowable.range(1, 5) + .concatMapCompletable(Functions.justFunction(Completable.complete())) + .test() + .assertResult(); + } + + @Test + public void simple2() { + final AtomicInteger counter = new AtomicInteger(); + Flowable.range(1, 5) + .concatMapCompletable(Functions.justFunction(Completable.fromAction(new Action() { + @Override + public void run() throws Exception { + counter.incrementAndGet(); + } + }))) + .test() + .assertResult(); + + assertEquals(5, counter.get()); + } + + @Test + public void simpleLongPrefetch() { + Flowable.range(1, 1024) + .concatMapCompletable(Functions.justFunction(Completable.complete()), 32) + .test() + .assertResult(); + } + + @Test + public void simpleLongPrefetchHidden() { + Flowable.range(1, 1024).hide() + .concatMapCompletable(Functions.justFunction(Completable.complete()), 32) + .test() + .assertResult(); + } + + @Test + public void mainError() { + Flowable.<Integer>error(new TestException()) + .concatMapCompletable(Functions.justFunction(Completable.complete())) + .test() + .assertFailure(TestException.class); + } + + @Test + public void innerError() { + Flowable.just(1) + .concatMapCompletable(Functions.justFunction(Completable.error(new TestException()))) + .test() + .assertFailure(TestException.class); + } + + @Test + public void innerErrorDelayed() { + TestObserverEx<Void> to = Flowable.range(1, 5) + .concatMapCompletableDelayError( + new Function<Integer, CompletableSource>() { + @Override + public CompletableSource apply(Integer v) throws Exception { + return Completable.error(new TestException()); + } + } + ) + .to(TestHelper.<Void>testConsumer()) + .assertFailure(CompositeException.class) + ; + + assertEquals(5, ((CompositeException)to.errors().get(0)).getExceptions().size()); + } + + @Test + public void mapperCrash() { + Flowable.just(1) + .concatMapCompletable(new Function<Integer, CompletableSource>() { + @Override + public CompletableSource apply(Integer v) throws Exception { + throw new TestException(); + } + }) + .test() + .assertFailure(TestException.class); + } + + @Test + public void immediateError() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + CompletableSubject cs = CompletableSubject.create(); + + TestObserver<Void> to = pp.concatMapCompletable( + Functions.justFunction(cs)).test(); + + to.assertEmpty(); + + assertTrue(pp.hasSubscribers()); + assertFalse(cs.hasObservers()); + + pp.onNext(1); + + assertTrue(cs.hasObservers()); + + pp.onError(new TestException()); + + assertFalse(cs.hasObservers()); + + to.assertFailure(TestException.class); + } + + @Test + public void immediateError2() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + CompletableSubject cs = CompletableSubject.create(); + + TestObserver<Void> to = pp.concatMapCompletable( + Functions.justFunction(cs)).test(); + + to.assertEmpty(); + + assertTrue(pp.hasSubscribers()); + assertFalse(cs.hasObservers()); + + pp.onNext(1); + + assertTrue(cs.hasObservers()); + + cs.onError(new TestException()); + + assertFalse(pp.hasSubscribers()); + + to.assertFailure(TestException.class); + } + + @Test + public void boundaryError() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + CompletableSubject cs = CompletableSubject.create(); + + TestObserver<Void> to = pp.concatMapCompletableDelayError( + Functions.justFunction(cs), false).test(); + + to.assertEmpty(); + + assertTrue(pp.hasSubscribers()); + assertFalse(cs.hasObservers()); + + pp.onNext(1); + + assertTrue(cs.hasObservers()); + + pp.onError(new TestException()); + + assertTrue(cs.hasObservers()); + + to.assertEmpty(); + + cs.onComplete(); + + to.assertFailure(TestException.class); + } + + @Test + public void endError() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + final CompletableSubject cs = CompletableSubject.create(); + final CompletableSubject cs2 = CompletableSubject.create(); + + TestObserver<Void> to = pp.concatMapCompletableDelayError( + new Function<Integer, CompletableSource>() { + @Override + public CompletableSource apply(Integer v) throws Exception { + if (v == 1) { + return cs; + } + return cs2; + } + }, true, 32 + ) + .test(); + + to.assertEmpty(); + + assertTrue(pp.hasSubscribers()); + assertFalse(cs.hasObservers()); + + pp.onNext(1); + + assertTrue(cs.hasObservers()); + + cs.onError(new TestException()); + + assertTrue(pp.hasSubscribers()); + + pp.onNext(2); + + to.assertEmpty(); + + cs2.onComplete(); + + assertTrue(pp.hasSubscribers()); + + to.assertEmpty(); + + pp.onComplete(); + + to.assertFailure(TestException.class); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeFlowableToCompletable( + new Function<Flowable<Object>, Completable>() { + @Override + public Completable apply(Flowable<Object> f) + throws Exception { + return f.concatMapCompletable( + Functions.justFunction(Completable.complete())); + } + } + ); + } + + @Test + public void disposed() { + TestHelper.checkDisposed( + Flowable.never() + .concatMapCompletable( + Functions.justFunction(Completable.complete())) + ); + } + + @Test + public void queueOverflow() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + new Flowable<Integer>() { + @Override + protected void subscribeActual(Subscriber<? super Integer> s) { + s.onSubscribe(new BooleanSubscription()); + s.onNext(1); + s.onNext(2); + s.onNext(3); + s.onError(new TestException()); + } + } + .concatMapCompletable( + Functions.justFunction(Completable.never()), 1 + ) + .test() + .assertFailure(QueueOverflowException.class); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void immediateOuterInnerErrorRace() { + final TestException ex = new TestException(); + for (int i = 0; i < TestHelper.RACE_LONG_LOOPS; i++) { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + final PublishProcessor<Integer> pp = PublishProcessor.create(); + final CompletableSubject cs = CompletableSubject.create(); + + TestObserver<Void> to = pp.concatMapCompletable( + Functions.justFunction(cs) + ) + .test(); + + pp.onNext(1); + + Runnable r1 = new Runnable() { + @Override + public void run() { + pp.onError(ex); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + cs.onError(ex); + } + }; + + TestHelper.race(r1, r2); + + to.assertError(new Predicate<Throwable>() { + @Override + public boolean test(Throwable e) throws Exception { + return e instanceof TestException || e instanceof CompositeException; + } + }) + .assertNotComplete(); + + if (!errors.isEmpty()) { + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } + } finally { + RxJavaPlugins.reset(); + } + } + } + + @Test + public void disposeInDrainLoop() { + for (int i = 0; i < TestHelper.RACE_LONG_LOOPS; i++) { + final PublishProcessor<Integer> pp = PublishProcessor.create(); + final CompletableSubject cs = CompletableSubject.create(); + + final TestObserver<Void> to = pp.concatMapCompletable( + Functions.justFunction(cs) + ) + .test(); + + pp.onNext(1); + + Runnable r1 = new Runnable() { + @Override + public void run() { + pp.onNext(2); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + cs.onComplete(); + to.dispose(); + } + }; + + TestHelper.race(r1, r2); + + to.assertEmpty(); + } + } + + @Test + public void doneButNotEmpty() { + final PublishProcessor<Integer> pp = PublishProcessor.create(); + final CompletableSubject cs = CompletableSubject.create(); + + final TestObserver<Void> to = pp.concatMapCompletable( + Functions.justFunction(cs) + ) + .test(); + + pp.onNext(1); + pp.onNext(2); + pp.onComplete(); + + cs.onComplete(); + + to.assertResult(); + } + + @Test + public void undeliverableUponCancel() { + TestHelper.checkUndeliverableUponCancel(new FlowableConverter<Integer, Completable>() { + @Override + public Completable apply(Flowable<Integer> upstream) { + return upstream.concatMapCompletable(new Function<Integer, Completable>() { + @Override + public Completable apply(Integer v) throws Throwable { + return Completable.complete().hide(); + } + }); + } + }); + } + + @Test + public void undeliverableUponCancelDelayError() { + TestHelper.checkUndeliverableUponCancel(new FlowableConverter<Integer, Completable>() { + @Override + public Completable apply(Flowable<Integer> upstream) { + return upstream.concatMapCompletableDelayError(new Function<Integer, Completable>() { + @Override + public Completable apply(Integer v) throws Throwable { + return Completable.complete().hide(); + } + }, false, 2); + } + }); + } + + @Test + public void undeliverableUponCancelDelayErrorTillEnd() { + TestHelper.checkUndeliverableUponCancel(new FlowableConverter<Integer, Completable>() { + @Override + public Completable apply(Flowable<Integer> upstream) { + return upstream.concatMapCompletableDelayError(new Function<Integer, Completable>() { + @Override + public Completable apply(Integer v) throws Throwable { + return Completable.complete().hide(); + } + }, true, 2); + } + }); + } + + @Test + public void basicNonFused() { + Flowable.range(1, 5).hide() + .concatMapCompletable(v -> Completable.complete().hide()) + .test() + .assertResult(); + } + + @Test + public void basicSyncFused() { + Flowable.range(1, 5) + .concatMapCompletable(v -> Completable.complete().hide()) + .test() + .assertResult(); + } + + @Test + public void basicAsyncFused() { + UnicastProcessor<Integer> up = UnicastProcessor.create(); + TestHelper.emit(up, 1, 2, 3, 4, 5); + + up + .concatMapCompletable(v -> Completable.complete().hide()) + .test() + .assertResult(); + } + + @Test + public void basicFusionRejected() { + TestHelper.<Integer>rejectFlowableFusion() + .concatMapCompletable(v -> Completable.complete().hide()) + .test() + .assertEmpty(); + } + + @Test + public void fusedPollCrash() { + Flowable.range(1, 5) + .map(v -> { + if (v == 3) { + throw new TestException(); + } + return v; + }) + .compose(TestHelper.flowableStripBoundary()) + .concatMapCompletable(v -> Completable.complete().hide()) + .test() + .assertFailure(TestException.class); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/mixed/FlowableConcatMapMaybeTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/mixed/FlowableConcatMapMaybeTest.java new file mode 100644 index 0000000000..12b61a5e8c --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/mixed/FlowableConcatMapMaybeTest.java @@ -0,0 +1,521 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.mixed; + +import static org.junit.Assert.*; + +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +import org.junit.Test; +import org.reactivestreams.Subscriber; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.internal.operators.mixed.FlowableConcatMapMaybe.ConcatMapMaybeSubscriber; +import io.reactivex.rxjava3.internal.subscriptions.BooleanSubscription; +import io.reactivex.rxjava3.internal.util.ErrorMode; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.processors.*; +import io.reactivex.rxjava3.schedulers.Schedulers; +import io.reactivex.rxjava3.subjects.MaybeSubject; +import io.reactivex.rxjava3.subscribers.TestSubscriber; +import io.reactivex.rxjava3.testsupport.*; + +public class FlowableConcatMapMaybeTest extends RxJavaTest { + + @Test + public void simple() { + Flowable.range(1, 5) + .concatMapMaybe(new Function<Integer, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Integer v) + throws Exception { + return Maybe.just(v); + } + }) + .test() + .assertResult(1, 2, 3, 4, 5); + } + + @Test + public void simpleLongPrefetch() { + Flowable.range(1, 1024) + .concatMapMaybe(Maybe::just, 32) + .test() + .assertValueCount(1024) + .assertNoErrors() + .assertComplete(); + } + + @Test + public void simpleLongPrefetchHidden() { + Flowable.range(1, 1024).hide() + .concatMapMaybe(Maybe::just, 32) + .test() + .assertValueCount(1024) + .assertNoErrors() + .assertComplete(); + } + + @Test + public void backpressure() { + TestSubscriber<Integer> ts = Flowable.range(1, 1024) + .concatMapMaybe(new Function<Integer, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Integer v) + throws Exception { + return Maybe.just(v); + } + }, 32) + .test(0); + + for (int i = 1; i <= 1024; i++) { + ts.assertValueCount(i - 1) + .assertNoErrors() + .assertNotComplete() + .requestMore(1) + .assertValueCount(i) + .assertNoErrors(); + } + + ts.assertComplete(); + } + + @Test + public void empty() { + Flowable.range(1, 10) + .concatMapMaybe(new Function<Integer, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Integer v) + throws Exception { + return Maybe.empty(); + } + }) + .test() + .assertResult(); + } + + @Test + public void mixed() { + Flowable.range(1, 10) + .concatMapMaybe(new Function<Integer, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Integer v) + throws Exception { + if (v % 2 == 0) { + return Maybe.just(v); + } + return Maybe.empty(); + } + }) + .test() + .assertResult(2, 4, 6, 8, 10); + } + + @Test + public void mixedLong() { + TestSubscriberEx<Integer> ts = Flowable.range(1, 1024) + .concatMapMaybe(new Function<Integer, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Integer v) + throws Exception { + if (v % 2 == 0) { + return Maybe.just(v).subscribeOn(Schedulers.computation()); + } + return Maybe.<Integer>empty().subscribeOn(Schedulers.computation()); + } + }) + .to(TestHelper.<Integer>testConsumer()) + .awaitDone(5, TimeUnit.SECONDS) + .assertValueCount(512) + .assertNoErrors() + .assertComplete() + ; + + for (int i = 0; i < 512; i ++) { + ts.assertValueAt(i, (i + 1) * 2); + } + } + + @Test + public void mainError() { + Flowable.error(new TestException()) + .concatMapMaybe(Functions.justFunction(Maybe.just(1))) + .test() + .assertFailure(TestException.class); + } + + @Test + public void innerError() { + Flowable.just(1) + .concatMapMaybe(Functions.justFunction(Maybe.error(new TestException()))) + .test() + .assertFailure(TestException.class); + } + + @Test + public void mainBoundaryErrorInnerSuccess() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + MaybeSubject<Integer> ms = MaybeSubject.create(); + + TestSubscriber<Integer> ts = pp.concatMapMaybeDelayError(Functions.justFunction(ms), false).test(); + + ts.assertEmpty(); + + pp.onNext(1); + + assertTrue(ms.hasObservers()); + + pp.onError(new TestException()); + + assertTrue(ms.hasObservers()); + + ts.assertEmpty(); + + ms.onSuccess(1); + + ts.assertFailure(TestException.class, 1); + } + + @Test + public void mainBoundaryErrorInnerEmpty() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + MaybeSubject<Integer> ms = MaybeSubject.create(); + + TestSubscriber<Integer> ts = pp.concatMapMaybeDelayError(Functions.justFunction(ms), false).test(); + + ts.assertEmpty(); + + pp.onNext(1); + + assertTrue(ms.hasObservers()); + + pp.onError(new TestException()); + + assertTrue(ms.hasObservers()); + + ts.assertEmpty(); + + ms.onComplete(); + + ts.assertFailure(TestException.class); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeFlowable( + new Function<Flowable<Object>, Flowable<Object>>() { + @Override + public Flowable<Object> apply(Flowable<Object> f) + throws Exception { + return f.concatMapMaybeDelayError( + Functions.justFunction(Maybe.empty())); + } + } + ); + } + + @Test + public void queueOverflow() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + new Flowable<Integer>() { + @Override + protected void subscribeActual(Subscriber<? super Integer> s) { + s.onSubscribe(new BooleanSubscription()); + s.onNext(1); + s.onNext(2); + s.onNext(3); + s.onError(new TestException()); + } + } + .concatMapMaybe( + Functions.justFunction(Maybe.never()), 1 + ) + .test() + .assertFailure(QueueOverflowException.class); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void limit() { + Flowable.range(1, 5) + .concatMapMaybe(new Function<Integer, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Integer v) + throws Exception { + return Maybe.just(v); + } + }) + .take(3) + .test() + .assertResult(1, 2, 3); + } + + @Test + public void cancel() { + Flowable.range(1, 5) + .concatMapMaybe(new Function<Integer, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Integer v) + throws Exception { + return Maybe.just(v); + } + }) + .test(3) + .assertValues(1, 2, 3) + .assertNoErrors() + .assertNotComplete() + .cancel(); + } + + @Test + public void innerErrorAfterMainError() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + final PublishProcessor<Integer> pp = PublishProcessor.create(); + + final AtomicReference<MaybeObserver<? super Integer>> obs = new AtomicReference<>(); + + TestSubscriberEx<Integer> ts = pp.concatMapMaybe( + new Function<Integer, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Integer v) + throws Exception { + return new Maybe<Integer>() { + @Override + protected void subscribeActual( + MaybeObserver<? super Integer> observer) { + observer.onSubscribe(Disposable.empty()); + obs.set(observer); + } + }; + } + } + ).to(TestHelper.<Integer>testConsumer()); + + pp.onNext(1); + + pp.onError(new TestException("outer")); + obs.get().onError(new TestException("inner")); + + ts.assertFailureAndMessage(TestException.class, "outer"); + + TestHelper.assertUndeliverable(errors, 0, TestException.class, "inner"); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void delayAllErrors() { + TestSubscriberEx<Object> ts = Flowable.range(1, 5) + .concatMapMaybeDelayError(new Function<Integer, MaybeSource<? extends Object>>() { + @Override + public MaybeSource<? extends Object> apply(Integer v) + throws Exception { + return Maybe.error(new TestException()); + } + }) + .to(TestHelper.<Object>testConsumer()) + .assertFailure(CompositeException.class) + ; + + CompositeException ce = (CompositeException)ts.errors().get(0); + assertEquals(5, ce.getExceptions().size()); + } + + @Test + public void mapperCrash() { + final PublishProcessor<Integer> pp = PublishProcessor.create(); + + TestSubscriber<Object> ts = pp + .concatMapMaybe(new Function<Integer, MaybeSource<? extends Object>>() { + @Override + public MaybeSource<? extends Object> apply(Integer v) + throws Exception { + throw new TestException(); + } + }) + .test(); + + ts.assertEmpty(); + + assertTrue(pp.hasSubscribers()); + + pp.onNext(1); + + ts.assertFailure(TestException.class); + + assertFalse(pp.hasSubscribers()); + } + + @Test + public void cancelNoConcurrentClean() { + TestSubscriber<Integer> ts = new TestSubscriber<>(); + ConcatMapMaybeSubscriber<Integer, Integer> operator = + new ConcatMapMaybeSubscriber<>( + ts, Functions.justFunction(Maybe.<Integer>never()), 16, ErrorMode.IMMEDIATE); + + operator.onSubscribe(new BooleanSubscription()); + + operator.queue.offer(1); + + operator.getAndIncrement(); + + ts.cancel(); + + assertFalse(operator.queue.isEmpty()); + + operator.addAndGet(-2); + + operator.cancel(); + + assertTrue(operator.queue.isEmpty()); + } + + @Test + public void innerSuccessDisposeRace() { + for (int i = 0; i < TestHelper.RACE_LONG_LOOPS; i++) { + + final MaybeSubject<Integer> ms = MaybeSubject.create(); + + final TestSubscriber<Integer> ts = Flowable.just(1) + .hide() + .concatMapMaybe(Functions.justFunction(ms)) + .test(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + ms.onSuccess(1); + } + }; + Runnable r2 = new Runnable() { + @Override + public void run() { + ts.cancel(); + } + }; + + TestHelper.race(r1, r2); + + ts.assertNoErrors(); + } + } + + @Test + public void undeliverableUponCancel() { + TestHelper.checkUndeliverableUponCancel(new FlowableConverter<Integer, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Flowable<Integer> upstream) { + return upstream.concatMapMaybe(new Function<Integer, Maybe<Integer>>() { + @Override + public Maybe<Integer> apply(Integer v) throws Throwable { + return Maybe.just(v).hide(); + } + }); + } + }); + } + + @Test + public void undeliverableUponCancelDelayError() { + TestHelper.checkUndeliverableUponCancel(new FlowableConverter<Integer, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Flowable<Integer> upstream) { + return upstream.concatMapMaybeDelayError(new Function<Integer, Maybe<Integer>>() { + @Override + public Maybe<Integer> apply(Integer v) throws Throwable { + return Maybe.just(v).hide(); + } + }, false, 2); + } + }); + } + + @Test + public void undeliverableUponCancelDelayErrorTillEnd() { + TestHelper.checkUndeliverableUponCancel(new FlowableConverter<Integer, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Flowable<Integer> upstream) { + return upstream.concatMapMaybeDelayError(new Function<Integer, Maybe<Integer>>() { + @Override + public Maybe<Integer> apply(Integer v) throws Throwable { + return Maybe.just(v).hide(); + } + }, true, 2); + } + }); + } + + @Test + public void basicNonFused() { + Flowable.range(1, 5).hide() + .concatMapMaybe(v -> Maybe.just(v).hide()) + .test() + .assertResult(1, 2, 3, 4, 5); + } + + @Test + public void basicSyncFused() { + Flowable.range(1, 5) + .concatMapMaybe(v -> Maybe.just(v).hide()) + .test() + .assertResult(1, 2, 3, 4, 5); + } + + @Test + public void basicAsyncFused() { + UnicastProcessor<Integer> up = UnicastProcessor.create(); + TestHelper.emit(up, 1, 2, 3, 4, 5); + + up + .concatMapMaybe(v -> Maybe.just(v).hide()) + .test() + .assertResult(1, 2, 3, 4, 5); + } + + @Test + public void basicFusionRejected() { + TestHelper.<Integer>rejectFlowableFusion() + .concatMapMaybe(v -> Maybe.just(v).hide()) + .test() + .assertEmpty(); + } + + @Test + public void fusedPollCrash() { + Flowable.range(1, 5) + .map(v -> { + if (v == 3) { + throw new TestException(); + } + return v; + }) + .compose(TestHelper.flowableStripBoundary()) + .concatMapMaybe(v -> Maybe.just(v).hide()) + .test() + .assertFailure(TestException.class, 1, 2); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/mixed/FlowableConcatMapSingleTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/mixed/FlowableConcatMapSingleTest.java new file mode 100644 index 0000000000..60cb2f5de0 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/mixed/FlowableConcatMapSingleTest.java @@ -0,0 +1,439 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.mixed; + +import static org.junit.Assert.*; + +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; + +import io.reactivex.rxjava3.disposables.Disposable; +import org.junit.Test; +import org.reactivestreams.Subscriber; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.internal.operators.mixed.FlowableConcatMapSingle.ConcatMapSingleSubscriber; +import io.reactivex.rxjava3.internal.subscriptions.BooleanSubscription; +import io.reactivex.rxjava3.internal.util.ErrorMode; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.processors.*; +import io.reactivex.rxjava3.subjects.SingleSubject; +import io.reactivex.rxjava3.subscribers.TestSubscriber; +import io.reactivex.rxjava3.testsupport.*; + +public class FlowableConcatMapSingleTest extends RxJavaTest { + + @Test + public void simple() { + Flowable.range(1, 5) + .concatMapSingle(new Function<Integer, SingleSource<Integer>>() { + @Override + public SingleSource<Integer> apply(Integer v) + throws Exception { + return Single.just(v); + } + }) + .test() + .assertResult(1, 2, 3, 4, 5); + } + + @Test + public void simpleLongPrefetch() { + Flowable.range(1, 1024) + .concatMapSingle(Single::just, 32) + .test() + .assertValueCount(1024) + .assertNoErrors() + .assertComplete(); + } + + @Test + public void simpleLongPrefetchHidden() { + Flowable.range(1, 1024).hide() + .concatMapSingle(Single::just, 32) + .test() + .assertValueCount(1024) + .assertNoErrors() + .assertComplete(); + } + + @Test + public void backpressure() { + TestSubscriber<Integer> ts = Flowable.range(1, 1024) + .concatMapSingle(new Function<Integer, SingleSource<Integer>>() { + @Override + public SingleSource<Integer> apply(Integer v) + throws Exception { + return Single.just(v); + } + }, 32) + .test(0); + + for (int i = 1; i <= 1024; i++) { + ts.assertValueCount(i - 1) + .assertNoErrors() + .assertNotComplete() + .requestMore(1) + .assertValueCount(i) + .assertNoErrors(); + } + + ts.assertComplete(); + } + + @Test + public void mainError() { + Flowable.error(new TestException()) + .concatMapSingle(Functions.justFunction(Single.just(1))) + .test() + .assertFailure(TestException.class); + } + + @Test + public void innerError() { + Flowable.just(1) + .concatMapSingle(Functions.justFunction(Single.error(new TestException()))) + .test() + .assertFailure(TestException.class); + } + + @Test + public void mainBoundaryErrorInnerSuccess() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + SingleSubject<Integer> ms = SingleSubject.create(); + + TestSubscriber<Integer> ts = pp.concatMapSingleDelayError(Functions.justFunction(ms), false).test(); + + ts.assertEmpty(); + + pp.onNext(1); + + assertTrue(ms.hasObservers()); + + pp.onError(new TestException()); + + assertTrue(ms.hasObservers()); + + ts.assertEmpty(); + + ms.onSuccess(1); + + ts.assertFailure(TestException.class, 1); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeFlowable( + new Function<Flowable<Object>, Flowable<Object>>() { + @Override + public Flowable<Object> apply(Flowable<Object> f) + throws Exception { + return f.concatMapSingleDelayError( + Functions.justFunction(Single.just((Object)1))); + } + } + ); + } + + @Test + public void queueOverflow() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + new Flowable<Integer>() { + @Override + protected void subscribeActual(Subscriber<? super Integer> s) { + s.onSubscribe(new BooleanSubscription()); + s.onNext(1); + s.onNext(2); + s.onNext(3); + s.onError(new TestException()); + } + } + .concatMapSingle( + Functions.justFunction(Single.never()), 1 + ) + .test() + .assertFailure(QueueOverflowException.class); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void limit() { + Flowable.range(1, 5) + .concatMapSingle(new Function<Integer, SingleSource<Integer>>() { + @Override + public SingleSource<Integer> apply(Integer v) + throws Exception { + return Single.just(v); + } + }) + .take(3) + .test() + .assertResult(1, 2, 3); + } + + @Test + public void cancel() { + Flowable.range(1, 5) + .concatMapSingle(new Function<Integer, SingleSource<Integer>>() { + @Override + public SingleSource<Integer> apply(Integer v) + throws Exception { + return Single.just(v); + } + }) + .test(3) + .assertValues(1, 2, 3) + .assertNoErrors() + .assertNotComplete() + .cancel(); + } + + @Test + public void innerErrorAfterMainError() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + final PublishProcessor<Integer> pp = PublishProcessor.create(); + + final AtomicReference<SingleObserver<? super Integer>> obs = new AtomicReference<>(); + + TestSubscriberEx<Integer> ts = pp.concatMapSingle( + new Function<Integer, SingleSource<Integer>>() { + @Override + public SingleSource<Integer> apply(Integer v) + throws Exception { + return new Single<Integer>() { + @Override + protected void subscribeActual( + SingleObserver<? super Integer> observer) { + observer.onSubscribe(Disposable.empty()); + obs.set(observer); + } + }; + } + } + ).to(TestHelper.<Integer>testConsumer()); + + pp.onNext(1); + + pp.onError(new TestException("outer")); + obs.get().onError(new TestException("inner")); + + ts.assertFailureAndMessage(TestException.class, "outer"); + + TestHelper.assertUndeliverable(errors, 0, TestException.class, "inner"); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void delayAllErrors() { + TestSubscriberEx<Object> ts = Flowable.range(1, 5) + .concatMapSingleDelayError(new Function<Integer, SingleSource<? extends Object>>() { + @Override + public SingleSource<? extends Object> apply(Integer v) + throws Exception { + return Single.error(new TestException()); + } + }) + .to(TestHelper.<Object>testConsumer()) + .assertFailure(CompositeException.class) + ; + + CompositeException ce = (CompositeException)ts.errors().get(0); + assertEquals(5, ce.getExceptions().size()); + } + + @Test + public void mapperCrash() { + final PublishProcessor<Integer> pp = PublishProcessor.create(); + + TestSubscriber<Object> ts = pp + .concatMapSingle(new Function<Integer, SingleSource<? extends Object>>() { + @Override + public SingleSource<? extends Object> apply(Integer v) + throws Exception { + throw new TestException(); + } + }) + .test(); + + ts.assertEmpty(); + + assertTrue(pp.hasSubscribers()); + + pp.onNext(1); + + ts.assertFailure(TestException.class); + + assertFalse(pp.hasSubscribers()); + } + + @Test + public void cancelNoConcurrentClean() { + TestSubscriber<Integer> ts = new TestSubscriber<>(); + ConcatMapSingleSubscriber<Integer, Integer> operator = + new ConcatMapSingleSubscriber<>( + ts, Functions.justFunction(Single.<Integer>never()), 16, ErrorMode.IMMEDIATE); + + operator.onSubscribe(new BooleanSubscription()); + + operator.queue.offer(1); + + operator.getAndIncrement(); + + ts.cancel(); + + assertFalse(operator.queue.isEmpty()); + + operator.addAndGet(-2); + + operator.cancel(); + + assertTrue(operator.queue.isEmpty()); + } + + @Test + public void innerSuccessDisposeRace() { + for (int i = 0; i < TestHelper.RACE_LONG_LOOPS; i++) { + + final SingleSubject<Integer> ss = SingleSubject.create(); + + final TestSubscriber<Integer> ts = Flowable.just(1) + .hide() + .concatMapSingle(Functions.justFunction(ss)) + .test(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + ss.onSuccess(1); + } + }; + Runnable r2 = new Runnable() { + @Override + public void run() { + ts.cancel(); + } + }; + + TestHelper.race(r1, r2); + + ts.assertNoErrors(); + } + } + + @Test + public void undeliverableUponCancel() { + TestHelper.checkUndeliverableUponCancel(new FlowableConverter<Integer, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Flowable<Integer> upstream) { + return upstream.concatMapSingle(new Function<Integer, Single<Integer>>() { + @Override + public Single<Integer> apply(Integer v) throws Throwable { + return Single.just(v).hide(); + } + }); + } + }); + } + + @Test + public void undeliverableUponCancelDelayError() { + TestHelper.checkUndeliverableUponCancel(new FlowableConverter<Integer, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Flowable<Integer> upstream) { + return upstream.concatMapSingleDelayError(new Function<Integer, Single<Integer>>() { + @Override + public Single<Integer> apply(Integer v) throws Throwable { + return Single.just(v).hide(); + } + }, false, 2); + } + }); + } + + @Test + public void undeliverableUponCancelDelayErrorTillEnd() { + TestHelper.checkUndeliverableUponCancel(new FlowableConverter<Integer, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Flowable<Integer> upstream) { + return upstream.concatMapSingleDelayError(new Function<Integer, Single<Integer>>() { + @Override + public Single<Integer> apply(Integer v) throws Throwable { + return Single.just(v).hide(); + } + }, true, 2); + } + }); + } + + @Test + public void basicNonFused() { + Flowable.range(1, 5).hide() + .concatMapSingle(v -> Single.just(v).hide()) + .test() + .assertResult(1, 2, 3, 4, 5); + } + + @Test + public void basicSyncFused() { + Flowable.range(1, 5) + .concatMapSingle(v -> Single.just(v).hide()) + .test() + .assertResult(1, 2, 3, 4, 5); + } + + @Test + public void basicAsyncFused() { + UnicastProcessor<Integer> up = UnicastProcessor.create(); + TestHelper.emit(up, 1, 2, 3, 4, 5); + + up + .concatMapSingle(v -> Single.just(v).hide()) + .test() + .assertResult(1, 2, 3, 4, 5); + } + + @Test + public void basicFusionRejected() { + TestHelper.<Integer>rejectFlowableFusion() + .concatMapSingle(v -> Single.just(v).hide()) + .test() + .assertEmpty(); + } + + @Test + public void fusedPollCrash() { + Flowable.range(1, 5) + .map(v -> { + if (v == 3) { + throw new TestException(); + } + return v; + }) + .compose(TestHelper.flowableStripBoundary()) + .concatMapSingle(v -> Single.just(v).hide()) + .test() + .assertFailure(TestException.class, 1, 2); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/mixed/FlowableSwitchMapCompletableTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/mixed/FlowableSwitchMapCompletableTest.java new file mode 100644 index 0000000000..cf2065cc72 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/mixed/FlowableSwitchMapCompletableTest.java @@ -0,0 +1,420 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.mixed; + +import static org.junit.Assert.*; + +import java.util.List; + +import org.junit.Test; +import org.reactivestreams.Subscriber; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.internal.subscriptions.BooleanSubscription; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.processors.PublishProcessor; +import io.reactivex.rxjava3.subjects.CompletableSubject; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class FlowableSwitchMapCompletableTest extends RxJavaTest { + + @Test + public void normal() { + Flowable.range(1, 10) + .switchMapCompletable(new Function<Integer, CompletableSource>() { + @Override + public CompletableSource apply(Integer v) throws Exception { + return Completable.complete(); + } + }) + .test() + .assertResult(); + } + + @Test + public void mainError() { + Flowable.<Integer>error(new TestException()) + .switchMapCompletable(new Function<Integer, CompletableSource>() { + @Override + public CompletableSource apply(Integer v) throws Exception { + return Completable.complete(); + } + }) + .test() + .assertFailure(TestException.class); + } + + @Test + public void innerError() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + CompletableSubject cs = CompletableSubject.create(); + + TestObserver<Void> to = pp.switchMapCompletable(Functions.justFunction(cs)) + .test(); + + assertTrue(pp.hasSubscribers()); + assertFalse(cs.hasObservers()); + + pp.onNext(1); + + assertTrue(cs.hasObservers()); + + to.assertEmpty(); + + cs.onError(new TestException()); + + to.assertFailure(TestException.class); + + assertFalse(pp.hasSubscribers()); + assertFalse(cs.hasObservers()); + } + + @Test + public void switchOver() { + final CompletableSubject[] css = { + CompletableSubject.create(), + CompletableSubject.create() + }; + + PublishProcessor<Integer> pp = PublishProcessor.create(); + + TestObserver<Void> to = pp.switchMapCompletable(new Function<Integer, CompletableSource>() { + @Override + public CompletableSource apply(Integer v) throws Exception { + return css[v]; + } + }) + .test(); + + to.assertEmpty(); + + pp.onNext(0); + + assertTrue(css[0].hasObservers()); + + pp.onNext(1); + + assertFalse(css[0].hasObservers()); + assertTrue(css[1].hasObservers()); + + pp.onComplete(); + + to.assertEmpty(); + + assertTrue(css[1].hasObservers()); + + css[1].onComplete(); + + to.assertResult(); + } + + @Test + public void dispose() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + CompletableSubject cs = CompletableSubject.create(); + + TestObserver<Void> to = pp.switchMapCompletable(Functions.justFunction(cs)) + .test(); + + pp.onNext(1); + + assertTrue(pp.hasSubscribers()); + assertTrue(cs.hasObservers()); + + to.dispose(); + + assertFalse(pp.hasSubscribers()); + assertFalse(cs.hasObservers()); + } + + @Test + public void checkDisposed() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + CompletableSubject cs = CompletableSubject.create(); + + TestHelper.checkDisposed(pp.switchMapCompletable(Functions.justFunction(cs))); + } + + @Test + public void checkBadSource() { + TestHelper.checkDoubleOnSubscribeFlowableToCompletable(new Function<Flowable<Object>, Completable>() { + @Override + public Completable apply(Flowable<Object> f) throws Exception { + return f.switchMapCompletable(Functions.justFunction(Completable.never())); + } + }); + } + + @Test + public void mapperCrash() { + Flowable.range(1, 5).switchMapCompletable(new Function<Integer, CompletableSource>() { + @Override + public CompletableSource apply(Integer f) throws Exception { + throw new TestException(); + } + }) + .test() + .assertFailure(TestException.class); + } + + @Test + public void mapperCancels() { + final TestObserver<Void> to = new TestObserver<>(); + + Flowable.range(1, 5).switchMapCompletable(new Function<Integer, CompletableSource>() { + @Override + public CompletableSource apply(Integer f) throws Exception { + to.dispose(); + return Completable.complete(); + } + }) + .subscribe(to); + + to.assertEmpty(); + } + + @Test + public void onNextInnerCompleteRace() { + for (int i = 0; i < TestHelper.RACE_LONG_LOOPS; i++) { + final PublishProcessor<Integer> pp = PublishProcessor.create(); + final CompletableSubject cs = CompletableSubject.create(); + + TestObserver<Void> to = pp.switchMapCompletable(Functions.justFunction(cs)).test(); + + pp.onNext(1); + + Runnable r1 = new Runnable() { + @Override + public void run() { + pp.onNext(2); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + cs.onComplete(); + } + }; + + TestHelper.race(r1, r2); + + to.assertEmpty(); + } + } + + @Test + public void onNextInnerErrorRace() { + final TestException ex = new TestException(); + for (int i = 0; i < TestHelper.RACE_LONG_LOOPS; i++) { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + final PublishProcessor<Integer> pp = PublishProcessor.create(); + final CompletableSubject cs = CompletableSubject.create(); + + TestObserver<Void> to = pp.switchMapCompletable(Functions.justFunction(cs)).test(); + + pp.onNext(1); + + Runnable r1 = new Runnable() { + @Override + public void run() { + pp.onNext(2); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + cs.onError(ex); + } + }; + + TestHelper.race(r1, r2); + + to.assertError(new Predicate<Throwable>() { + @Override + public boolean test(Throwable e) throws Exception { + return e instanceof TestException || e instanceof CompositeException; + } + }); + + if (!errors.isEmpty()) { + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } + } finally { + RxJavaPlugins.reset(); + } + } + } + + @Test + public void onErrorInnerErrorRace() { + final TestException ex0 = new TestException(); + final TestException ex = new TestException(); + for (int i = 0; i < TestHelper.RACE_LONG_LOOPS; i++) { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + final PublishProcessor<Integer> pp = PublishProcessor.create(); + final CompletableSubject cs = CompletableSubject.create(); + + TestObserver<Void> to = pp.switchMapCompletable(Functions.justFunction(cs)).test(); + + pp.onNext(1); + + Runnable r1 = new Runnable() { + @Override + public void run() { + pp.onError(ex0); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + cs.onError(ex); + } + }; + + TestHelper.race(r1, r2); + + to.assertError(new Predicate<Throwable>() { + @Override + public boolean test(Throwable e) throws Exception { + return e instanceof TestException || e instanceof CompositeException; + } + }); + + if (!errors.isEmpty()) { + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } + } finally { + RxJavaPlugins.reset(); + } + } + } + + @Test + public void innerErrorThenMainError() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + new Flowable<Integer>() { + @Override + protected void subscribeActual(Subscriber<? super Integer> s) { + s.onSubscribe(new BooleanSubscription()); + s.onNext(1); + s.onError(new TestException("main")); + } + } + .switchMapCompletable(Functions.justFunction(Completable.error(new TestException("inner")))) + .to(TestHelper.testConsumer()) + .assertFailureAndMessage(TestException.class, "inner"); + + TestHelper.assertUndeliverable(errors, 0, TestException.class, "main"); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void innerErrorDelayed() { + final PublishProcessor<Integer> pp = PublishProcessor.create(); + final CompletableSubject cs = CompletableSubject.create(); + + TestObserver<Void> to = pp.switchMapCompletableDelayError(Functions.justFunction(cs)).test(); + + pp.onNext(1); + + cs.onError(new TestException()); + + to.assertEmpty(); + + assertTrue(pp.hasSubscribers()); + + pp.onComplete(); + + to.assertFailure(TestException.class); + } + + @Test + public void mainCompletesinnerErrorDelayed() { + final PublishProcessor<Integer> pp = PublishProcessor.create(); + final CompletableSubject cs = CompletableSubject.create(); + + TestObserver<Void> to = pp.switchMapCompletableDelayError(Functions.justFunction(cs)).test(); + + pp.onNext(1); + pp.onComplete(); + + to.assertEmpty(); + + cs.onError(new TestException()); + + to.assertFailure(TestException.class); + } + + @Test + public void mainErrorDelayed() { + final PublishProcessor<Integer> pp = PublishProcessor.create(); + final CompletableSubject cs = CompletableSubject.create(); + + TestObserver<Void> to = pp.switchMapCompletableDelayError(Functions.justFunction(cs)).test(); + + pp.onNext(1); + + pp.onError(new TestException()); + + to.assertEmpty(); + + assertTrue(cs.hasObservers()); + + cs.onComplete(); + + to.assertFailure(TestException.class); + } + + @Test + public void undeliverableUponCancel() { + TestHelper.checkUndeliverableUponCancel(new FlowableConverter<Integer, Completable>() { + @Override + public Completable apply(Flowable<Integer> upstream) { + return upstream.switchMapCompletable(new Function<Integer, Completable>() { + @Override + public Completable apply(Integer v) throws Throwable { + return Completable.complete().hide(); + } + }); + } + }); + } + + @Test + public void undeliverableUponCancelDelayError() { + TestHelper.checkUndeliverableUponCancel(new FlowableConverter<Integer, Completable>() { + @Override + public Completable apply(Flowable<Integer> upstream) { + return upstream.switchMapCompletableDelayError(new Function<Integer, Completable>() { + @Override + public Completable apply(Integer v) throws Throwable { + return Completable.complete().hide(); + } + }); + } + }); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/mixed/FlowableSwitchMapMaybeTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/mixed/FlowableSwitchMapMaybeTest.java new file mode 100644 index 0000000000..1ace07440f --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/mixed/FlowableSwitchMapMaybeTest.java @@ -0,0 +1,679 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.mixed; + +import static org.junit.Assert.*; + +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; + +import io.reactivex.rxjava3.disposables.Disposable; +import org.junit.Test; +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.internal.subscriptions.BooleanSubscription; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.processors.PublishProcessor; +import io.reactivex.rxjava3.subjects.MaybeSubject; +import io.reactivex.rxjava3.subscribers.TestSubscriber; +import io.reactivex.rxjava3.testsupport.*; + +public class FlowableSwitchMapMaybeTest extends RxJavaTest { + + @Test + public void simple() { + Flowable.range(1, 5) + .switchMapMaybe(new Function<Integer, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Integer v) + throws Exception { + return Maybe.just(v); + } + }) + .test() + .assertResult(1, 2, 3, 4, 5); + } + + @Test + public void simpleEmpty() { + Flowable.range(1, 5) + .switchMapMaybe(new Function<Integer, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Integer v) + throws Exception { + return Maybe.empty(); + } + }) + .test() + .assertResult(); + } + + @Test + public void simpleMixed() { + Flowable.range(1, 10) + .switchMapMaybe(new Function<Integer, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Integer v) + throws Exception { + if (v % 2 == 0) { + return Maybe.just(v); + } + return Maybe.empty(); + } + }) + .test() + .assertResult(2, 4, 6, 8, 10); + } + + @Test + public void backpressured() { + TestSubscriber<Integer> ts = Flowable.range(1, 1024) + .switchMapMaybe(new Function<Integer, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Integer v) + throws Exception { + if (v % 2 == 0) { + return Maybe.just(v); + } + return Maybe.empty(); + } + }) + .test(0L); + + // backpressure results items skipped + ts + .requestMore(1) + .assertResult(1024); + } + + @Test + public void mainError() { + Flowable.error(new TestException()) + .switchMapMaybe(Functions.justFunction(Maybe.never())) + .test() + .assertFailure(TestException.class); + } + + @Test + public void innerError() { + Flowable.just(1) + .switchMapMaybe(Functions.justFunction(Maybe.error(new TestException()))) + .test() + .assertFailure(TestException.class); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeFlowable(new Function<Flowable<Object>, Publisher<Object>>() { + @Override + public Publisher<Object> apply(Flowable<Object> f) + throws Exception { + return f + .switchMapMaybe(Functions.justFunction(Maybe.never())); + } + } + ); + } + + @Test + public void limit() { + Flowable.range(1, 5) + .switchMapMaybe(new Function<Integer, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Integer v) + throws Exception { + return Maybe.just(v); + } + }) + .take(3) + .test() + .assertResult(1, 2, 3); + } + + @Test + public void switchOver() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + + final MaybeSubject<Integer> ms1 = MaybeSubject.create(); + final MaybeSubject<Integer> ms2 = MaybeSubject.create(); + + TestSubscriber<Integer> ts = pp.switchMapMaybe(new Function<Integer, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Integer v) + throws Exception { + if (v == 1) { + return ms1; + } + return ms2; + } + }).test(); + + ts.assertEmpty(); + + pp.onNext(1); + + ts.assertEmpty(); + + assertTrue(ms1.hasObservers()); + + pp.onNext(2); + + assertFalse(ms1.hasObservers()); + assertTrue(ms2.hasObservers()); + + ms2.onError(new TestException()); + + assertFalse(pp.hasSubscribers()); + + ts.assertFailure(TestException.class); + } + + @Test + public void switchOverDelayError() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + + final MaybeSubject<Integer> ms1 = MaybeSubject.create(); + final MaybeSubject<Integer> ms2 = MaybeSubject.create(); + + TestSubscriber<Integer> ts = pp.switchMapMaybeDelayError(new Function<Integer, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Integer v) + throws Exception { + if (v == 1) { + return ms1; + } + return ms2; + } + }).test(); + + ts.assertEmpty(); + + pp.onNext(1); + + ts.assertEmpty(); + + assertTrue(ms1.hasObservers()); + + pp.onNext(2); + + assertFalse(ms1.hasObservers()); + assertTrue(ms2.hasObservers()); + + ms2.onError(new TestException()); + + ts.assertEmpty(); + + assertTrue(pp.hasSubscribers()); + + pp.onComplete(); + + ts.assertFailure(TestException.class); + } + + @Test + public void mainErrorInnerCompleteDelayError() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + + final MaybeSubject<Integer> ms = MaybeSubject.create(); + + TestSubscriber<Integer> ts = pp.switchMapMaybeDelayError(new Function<Integer, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Integer v) + throws Exception { + return ms; + } + }).test(); + + ts.assertEmpty(); + + pp.onNext(1); + + ts.assertEmpty(); + + assertTrue(ms.hasObservers()); + + pp.onError(new TestException()); + + assertTrue(ms.hasObservers()); + + ts.assertEmpty(); + + ms.onComplete(); + + ts.assertFailure(TestException.class); + } + + @Test + public void mainErrorInnerSuccessDelayError() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + + final MaybeSubject<Integer> ms = MaybeSubject.create(); + + TestSubscriber<Integer> ts = pp.switchMapMaybeDelayError(new Function<Integer, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Integer v) + throws Exception { + return ms; + } + }).test(); + + ts.assertEmpty(); + + pp.onNext(1); + + ts.assertEmpty(); + + assertTrue(ms.hasObservers()); + + pp.onError(new TestException()); + + assertTrue(ms.hasObservers()); + + ts.assertEmpty(); + + ms.onSuccess(1); + + ts.assertFailure(TestException.class, 1); + } + + @Test + public void mapperCrash() { + Flowable.just(1) + .switchMapMaybe(new Function<Integer, MaybeSource<? extends Object>>() { + @Override + public MaybeSource<? extends Object> apply(Integer v) + throws Exception { + throw new TestException(); + } + }) + .test() + .assertFailure(TestException.class); + } + + @Test + public void disposeBeforeSwitchInOnNext() { + final TestSubscriber<Integer> ts = new TestSubscriber<>(); + + Flowable.just(1) + .switchMapMaybe(new Function<Integer, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Integer v) + throws Exception { + ts.cancel(); + return Maybe.just(1); + } + }).subscribe(ts); + + ts.assertEmpty(); + } + + @Test + public void disposeOnNextAfterFirst() { + final TestSubscriber<Integer> ts = new TestSubscriber<>(); + + Flowable.just(1, 2) + .switchMapMaybe(new Function<Integer, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Integer v) + throws Exception { + if (v == 2) { + ts.cancel(); + } + return Maybe.just(1); + } + }).subscribe(ts); + + ts.assertValue(1) + .assertNoErrors() + .assertNotComplete(); + } + + @Test + public void cancel() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + + final MaybeSubject<Integer> ms = MaybeSubject.create(); + + TestSubscriber<Integer> ts = pp.switchMapMaybeDelayError(new Function<Integer, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Integer v) + throws Exception { + return ms; + } + }).test(); + + ts.assertEmpty(); + + pp.onNext(1); + + ts.assertEmpty(); + + assertTrue(pp.hasSubscribers()); + assertTrue(ms.hasObservers()); + + ts.cancel(); + + assertFalse(pp.hasSubscribers()); + assertFalse(ms.hasObservers()); + } + + @Test + public void mainErrorAfterTermination() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + new Flowable<Integer>() { + @Override + protected void subscribeActual(Subscriber<? super Integer> s) { + s.onSubscribe(new BooleanSubscription()); + s.onNext(1); + s.onError(new TestException("outer")); + } + } + .switchMapMaybe(new Function<Integer, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Integer v) + throws Exception { + return Maybe.error(new TestException("inner")); + } + }) + .to(TestHelper.<Integer>testConsumer()) + .assertFailureAndMessage(TestException.class, "inner"); + + TestHelper.assertUndeliverable(errors, 0, TestException.class, "outer"); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void innerErrorAfterTermination() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + final AtomicReference<MaybeObserver<? super Integer>> moRef = new AtomicReference<>(); + + TestSubscriberEx<Integer> ts = new Flowable<Integer>() { + @Override + protected void subscribeActual(Subscriber<? super Integer> s) { + s.onSubscribe(new BooleanSubscription()); + s.onNext(1); + s.onError(new TestException("outer")); + } + } + .switchMapMaybe(new Function<Integer, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Integer v) + throws Exception { + return new Maybe<Integer>() { + @Override + protected void subscribeActual( + MaybeObserver<? super Integer> observer) { + observer.onSubscribe(Disposable.empty()); + moRef.set(observer); + } + }; + } + }) + .to(TestHelper.<Integer>testConsumer()); + + ts.assertFailureAndMessage(TestException.class, "outer"); + + moRef.get().onError(new TestException("inner")); + moRef.get().onComplete(); + + TestHelper.assertUndeliverable(errors, 0, TestException.class, "inner"); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void nextCancelRace() { + for (int i = 0; i < TestHelper.RACE_LONG_LOOPS; i++) { + + final PublishProcessor<Integer> pp = PublishProcessor.create(); + + final MaybeSubject<Integer> ms = MaybeSubject.create(); + + final TestSubscriber<Integer> ts = pp.switchMapMaybeDelayError(new Function<Integer, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Integer v) + throws Exception { + return ms; + } + }).test(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + pp.onNext(1); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + ts.cancel(); + } + }; + + TestHelper.race(r1, r2); + + ts.assertNoErrors() + .assertNotComplete(); + } + } + + @Test + public void nextInnerErrorRace() { + final TestException ex = new TestException(); + + for (int i = 0; i < TestHelper.RACE_LONG_LOOPS; i++) { + + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + final PublishProcessor<Integer> pp = PublishProcessor.create(); + + final MaybeSubject<Integer> ms = MaybeSubject.create(); + + final TestSubscriberEx<Integer> ts = pp.switchMapMaybeDelayError(new Function<Integer, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Integer v) + throws Exception { + if (v == 1) { + return ms; + } + return Maybe.never(); + } + }).to(TestHelper.<Integer>testConsumer()); + + pp.onNext(1); + + Runnable r1 = new Runnable() { + @Override + public void run() { + pp.onNext(2); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + ms.onError(ex); + } + }; + + TestHelper.race(r1, r2); + + if (ts.errors().size() != 0) { + assertTrue(errors.isEmpty()); + ts.assertFailure(TestException.class); + } else if (!errors.isEmpty()) { + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } + } finally { + RxJavaPlugins.reset(); + } + } + } + + @Test + public void mainErrorInnerErrorRace() { + final TestException ex = new TestException(); + final TestException ex2 = new TestException(); + + for (int i = 0; i < TestHelper.RACE_LONG_LOOPS; i++) { + + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + final PublishProcessor<Integer> pp = PublishProcessor.create(); + + final MaybeSubject<Integer> ms = MaybeSubject.create(); + + final TestSubscriber<Integer> ts = pp.switchMapMaybeDelayError(new Function<Integer, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Integer v) + throws Exception { + if (v == 1) { + return ms; + } + return Maybe.never(); + } + }).test(); + + pp.onNext(1); + + Runnable r1 = new Runnable() { + @Override + public void run() { + pp.onError(ex); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + ms.onError(ex2); + } + }; + + TestHelper.race(r1, r2); + + ts.assertError(new Predicate<Throwable>() { + @Override + public boolean test(Throwable e) throws Exception { + return e instanceof TestException || e instanceof CompositeException; + } + }); + + if (!errors.isEmpty()) { + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } + } finally { + RxJavaPlugins.reset(); + } + } + } + + @Test + public void nextInnerSuccessRace() { + for (int i = 0; i < TestHelper.RACE_LONG_LOOPS; i++) { + + final PublishProcessor<Integer> pp = PublishProcessor.create(); + + final MaybeSubject<Integer> ms = MaybeSubject.create(); + + final TestSubscriber<Integer> ts = pp.switchMapMaybeDelayError(new Function<Integer, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Integer v) + throws Exception { + if (v == 1) { + return ms; + } + return Maybe.empty(); + } + }).test(); + + pp.onNext(1); + + Runnable r1 = new Runnable() { + @Override + public void run() { + pp.onNext(2); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + ms.onSuccess(3); + } + }; + + TestHelper.race(r1, r2); + + ts.assertNoErrors() + .assertNotComplete(); + } + } + + @Test + public void requestMoreOnNext() { + TestSubscriber<Integer> ts = new TestSubscriber<Integer>(1) { + @Override + public void onNext(Integer t) { + super.onNext(t); + requestMore(1); + } + }; + Flowable.range(1, 5) + .switchMapMaybe(Functions.justFunction(Maybe.just(1))) + .subscribe(ts); + + ts.assertResult(1, 1, 1, 1, 1); + } + + @Test + public void undeliverableUponCancel() { + TestHelper.checkUndeliverableUponCancel(new FlowableConverter<Integer, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Flowable<Integer> upstream) { + return upstream.switchMapMaybe(new Function<Integer, Maybe<Integer>>() { + @Override + public Maybe<Integer> apply(Integer v) throws Throwable { + return Maybe.just(v).hide(); + } + }); + } + }); + } + + @Test + public void undeliverableUponCancelDelayError() { + TestHelper.checkUndeliverableUponCancel(new FlowableConverter<Integer, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Flowable<Integer> upstream) { + return upstream.switchMapMaybeDelayError(new Function<Integer, Maybe<Integer>>() { + @Override + public Maybe<Integer> apply(Integer v) throws Throwable { + return Maybe.just(v).hide(); + } + }); + } + }); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/mixed/FlowableSwitchMapSingleTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/mixed/FlowableSwitchMapSingleTest.java new file mode 100644 index 0000000000..cd4d64a9eb --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/mixed/FlowableSwitchMapSingleTest.java @@ -0,0 +1,636 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.mixed; + +import static org.junit.Assert.*; + +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; + +import io.reactivex.rxjava3.disposables.Disposable; +import org.junit.Test; +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.internal.subscriptions.BooleanSubscription; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.processors.PublishProcessor; +import io.reactivex.rxjava3.subjects.SingleSubject; +import io.reactivex.rxjava3.subscribers.TestSubscriber; +import io.reactivex.rxjava3.testsupport.*; + +public class FlowableSwitchMapSingleTest extends RxJavaTest { + + @Test + public void simple() { + Flowable.range(1, 5) + .switchMapSingle(new Function<Integer, SingleSource<Integer>>() { + @Override + public SingleSource<Integer> apply(Integer v) + throws Exception { + return Single.just(v); + } + }) + .test() + .assertResult(1, 2, 3, 4, 5); + } + + @Test + public void mainError() { + Flowable.error(new TestException()) + .switchMapSingle(Functions.justFunction(Single.never())) + .test() + .assertFailure(TestException.class); + } + + @Test + public void innerError() { + Flowable.just(1) + .switchMapSingle(Functions.justFunction(Single.error(new TestException()))) + .test() + .assertFailure(TestException.class); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeFlowable(new Function<Flowable<Object>, Publisher<Object>>() { + @Override + public Publisher<Object> apply(Flowable<Object> f) + throws Exception { + return f + .switchMapSingle(Functions.justFunction(Single.never())); + } + } + ); + } + + @Test + public void limit() { + Flowable.range(1, 5) + .switchMapSingle(new Function<Integer, SingleSource<Integer>>() { + @Override + public SingleSource<Integer> apply(Integer v) + throws Exception { + return Single.just(v); + } + }) + .take(3) + .test() + .assertResult(1, 2, 3); + } + + @Test + public void switchOver() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + + final SingleSubject<Integer> ms1 = SingleSubject.create(); + final SingleSubject<Integer> ms2 = SingleSubject.create(); + + TestSubscriber<Integer> ts = pp.switchMapSingle(new Function<Integer, SingleSource<Integer>>() { + @Override + public SingleSource<Integer> apply(Integer v) + throws Exception { + if (v == 1) { + return ms1; + } + return ms2; + } + }).test(); + + ts.assertEmpty(); + + pp.onNext(1); + + ts.assertEmpty(); + + assertTrue(ms1.hasObservers()); + + pp.onNext(2); + + assertFalse(ms1.hasObservers()); + assertTrue(ms2.hasObservers()); + + ms2.onError(new TestException()); + + assertFalse(pp.hasSubscribers()); + + ts.assertFailure(TestException.class); + } + + @Test + public void switchOverDelayError() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + + final SingleSubject<Integer> ms1 = SingleSubject.create(); + final SingleSubject<Integer> ms2 = SingleSubject.create(); + + TestSubscriber<Integer> ts = pp.switchMapSingleDelayError(new Function<Integer, SingleSource<Integer>>() { + @Override + public SingleSource<Integer> apply(Integer v) + throws Exception { + if (v == 1) { + return ms1; + } + return ms2; + } + }).test(); + + ts.assertEmpty(); + + pp.onNext(1); + + ts.assertEmpty(); + + assertTrue(ms1.hasObservers()); + + pp.onNext(2); + + assertFalse(ms1.hasObservers()); + assertTrue(ms2.hasObservers()); + + ms2.onError(new TestException()); + + ts.assertEmpty(); + + assertTrue(pp.hasSubscribers()); + + pp.onComplete(); + + ts.assertFailure(TestException.class); + } + + @Test + public void mainErrorInnerCompleteDelayError() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + + final SingleSubject<Integer> ms = SingleSubject.create(); + + TestSubscriber<Integer> ts = pp.switchMapSingleDelayError(new Function<Integer, SingleSource<Integer>>() { + @Override + public SingleSource<Integer> apply(Integer v) + throws Exception { + return ms; + } + }).test(); + + ts.assertEmpty(); + + pp.onNext(1); + + ts.assertEmpty(); + + assertTrue(ms.hasObservers()); + + pp.onError(new TestException()); + + assertTrue(ms.hasObservers()); + + ts.assertEmpty(); + + ms.onSuccess(1); + + ts.assertFailure(TestException.class, 1); + } + + @Test + public void mainErrorInnerSuccessDelayError() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + + final SingleSubject<Integer> ms = SingleSubject.create(); + + TestSubscriber<Integer> ts = pp.switchMapSingleDelayError(new Function<Integer, SingleSource<Integer>>() { + @Override + public SingleSource<Integer> apply(Integer v) + throws Exception { + return ms; + } + }).test(); + + ts.assertEmpty(); + + pp.onNext(1); + + ts.assertEmpty(); + + assertTrue(ms.hasObservers()); + + pp.onError(new TestException()); + + assertTrue(ms.hasObservers()); + + ts.assertEmpty(); + + ms.onSuccess(1); + + ts.assertFailure(TestException.class, 1); + } + + @Test + public void mapperCrash() { + Flowable.just(1) + .switchMapSingle(new Function<Integer, SingleSource<? extends Object>>() { + @Override + public SingleSource<? extends Object> apply(Integer v) + throws Exception { + throw new TestException(); + } + }) + .test() + .assertFailure(TestException.class); + } + + @Test + public void disposeBeforeSwitchInOnNext() { + final TestSubscriber<Integer> ts = new TestSubscriber<>(); + + Flowable.just(1) + .switchMapSingle(new Function<Integer, SingleSource<Integer>>() { + @Override + public SingleSource<Integer> apply(Integer v) + throws Exception { + ts.cancel(); + return Single.just(1); + } + }).subscribe(ts); + + ts.assertEmpty(); + } + + @Test + public void disposeOnNextAfterFirst() { + final TestSubscriber<Integer> ts = new TestSubscriber<>(); + + Flowable.just(1, 2) + .switchMapSingle(new Function<Integer, SingleSource<Integer>>() { + @Override + public SingleSource<Integer> apply(Integer v) + throws Exception { + if (v == 2) { + ts.cancel(); + } + return Single.just(1); + } + }).subscribe(ts); + + ts.assertValue(1) + .assertNoErrors() + .assertNotComplete(); + } + + @Test + public void cancel() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + + final SingleSubject<Integer> ms = SingleSubject.create(); + + TestSubscriber<Integer> ts = pp.switchMapSingleDelayError(new Function<Integer, SingleSource<Integer>>() { + @Override + public SingleSource<Integer> apply(Integer v) + throws Exception { + return ms; + } + }).test(); + + ts.assertEmpty(); + + pp.onNext(1); + + ts.assertEmpty(); + + assertTrue(pp.hasSubscribers()); + assertTrue(ms.hasObservers()); + + ts.cancel(); + + assertFalse(pp.hasSubscribers()); + assertFalse(ms.hasObservers()); + } + + @Test + public void mainErrorAfterTermination() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + new Flowable<Integer>() { + @Override + protected void subscribeActual(Subscriber<? super Integer> s) { + s.onSubscribe(new BooleanSubscription()); + s.onNext(1); + s.onError(new TestException("outer")); + } + } + .switchMapSingle(new Function<Integer, SingleSource<Integer>>() { + @Override + public SingleSource<Integer> apply(Integer v) + throws Exception { + return Single.error(new TestException("inner")); + } + }) + .to(TestHelper.<Integer>testConsumer()) + .assertFailureAndMessage(TestException.class, "inner"); + + TestHelper.assertUndeliverable(errors, 0, TestException.class, "outer"); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void innerErrorAfterTermination() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + final AtomicReference<SingleObserver<? super Integer>> moRef = new AtomicReference<>(); + + TestSubscriberEx<Integer> ts = new Flowable<Integer>() { + @Override + protected void subscribeActual(Subscriber<? super Integer> s) { + s.onSubscribe(new BooleanSubscription()); + s.onNext(1); + s.onError(new TestException("outer")); + } + } + .switchMapSingle(new Function<Integer, SingleSource<Integer>>() { + @Override + public SingleSource<Integer> apply(Integer v) + throws Exception { + return new Single<Integer>() { + @Override + protected void subscribeActual( + SingleObserver<? super Integer> observer) { + observer.onSubscribe(Disposable.empty()); + moRef.set(observer); + } + }; + } + }) + .to(TestHelper.<Integer>testConsumer()); + + ts.assertFailureAndMessage(TestException.class, "outer"); + + moRef.get().onError(new TestException("inner")); + + TestHelper.assertUndeliverable(errors, 0, TestException.class, "inner"); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void nextCancelRace() { + for (int i = 0; i < TestHelper.RACE_LONG_LOOPS; i++) { + + final PublishProcessor<Integer> pp = PublishProcessor.create(); + + final SingleSubject<Integer> ms = SingleSubject.create(); + + final TestSubscriber<Integer> ts = pp.switchMapSingleDelayError(new Function<Integer, SingleSource<Integer>>() { + @Override + public SingleSource<Integer> apply(Integer v) + throws Exception { + return ms; + } + }).test(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + pp.onNext(1); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + ts.cancel(); + } + }; + + TestHelper.race(r1, r2); + + ts.assertNoErrors() + .assertNotComplete(); + } + } + + @Test + public void nextInnerErrorRace() { + final TestException ex = new TestException(); + + for (int i = 0; i < TestHelper.RACE_LONG_LOOPS; i++) { + + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + final PublishProcessor<Integer> pp = PublishProcessor.create(); + + final SingleSubject<Integer> ms = SingleSubject.create(); + + final TestSubscriberEx<Integer> ts = pp.switchMapSingleDelayError(new Function<Integer, SingleSource<Integer>>() { + @Override + public SingleSource<Integer> apply(Integer v) + throws Exception { + if (v == 1) { + return ms; + } + return Single.never(); + } + }).to(TestHelper.<Integer>testConsumer()); + + pp.onNext(1); + + Runnable r1 = new Runnable() { + @Override + public void run() { + pp.onNext(2); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + ms.onError(ex); + } + }; + + TestHelper.race(r1, r2); + + if (ts.errors().size() != 0) { + assertTrue(errors.isEmpty()); + ts.assertFailure(TestException.class); + } else if (!errors.isEmpty()) { + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } + } finally { + RxJavaPlugins.reset(); + } + } + } + + @Test + public void mainErrorInnerErrorRace() { + final TestException ex = new TestException(); + final TestException ex2 = new TestException(); + + for (int i = 0; i < TestHelper.RACE_LONG_LOOPS; i++) { + + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + final PublishProcessor<Integer> pp = PublishProcessor.create(); + + final SingleSubject<Integer> ms = SingleSubject.create(); + + final TestSubscriber<Integer> ts = pp.switchMapSingleDelayError(new Function<Integer, SingleSource<Integer>>() { + @Override + public SingleSource<Integer> apply(Integer v) + throws Exception { + if (v == 1) { + return ms; + } + return Single.never(); + } + }).test(); + + pp.onNext(1); + + Runnable r1 = new Runnable() { + @Override + public void run() { + pp.onError(ex); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + ms.onError(ex2); + } + }; + + TestHelper.race(r1, r2); + + ts.assertError(new Predicate<Throwable>() { + @Override + public boolean test(Throwable e) throws Exception { + return e instanceof TestException || e instanceof CompositeException; + } + }); + + if (!errors.isEmpty()) { + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } + } finally { + RxJavaPlugins.reset(); + } + } + } + + @Test + public void nextInnerSuccessRace() { + for (int i = 0; i < TestHelper.RACE_LONG_LOOPS; i++) { + + final PublishProcessor<Integer> pp = PublishProcessor.create(); + + final SingleSubject<Integer> ms = SingleSubject.create(); + + final TestSubscriber<Integer> ts = pp.switchMapSingleDelayError(new Function<Integer, SingleSource<Integer>>() { + @Override + public SingleSource<Integer> apply(Integer v) + throws Exception { + if (v == 1) { + return ms; + } + return Single.never(); + } + }).test(); + + pp.onNext(1); + + Runnable r1 = new Runnable() { + @Override + public void run() { + pp.onNext(2); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + ms.onSuccess(3); + } + }; + + TestHelper.race(r1, r2); + + ts.assertNoErrors() + .assertNotComplete(); + } + } + + @Test + public void requestMoreOnNext() { + TestSubscriber<Integer> ts = new TestSubscriber<Integer>(1) { + @Override + public void onNext(Integer t) { + super.onNext(t); + requestMore(1); + } + }; + Flowable.range(1, 5) + .switchMapSingle(Functions.justFunction(Single.just(1))) + .subscribe(ts); + + ts.assertResult(1, 1, 1, 1, 1); + } + + @Test + public void backpressured() { + Flowable.just(1) + .switchMapSingle(Functions.justFunction(Single.just(1))) + .test(0) + .assertEmpty() + .requestMore(1) + .assertResult(1); + } + + @Test + public void undeliverableUponCancel() { + TestHelper.checkUndeliverableUponCancel(new FlowableConverter<Integer, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Flowable<Integer> upstream) { + return upstream.switchMapSingle(new Function<Integer, Single<Integer>>() { + @Override + public Single<Integer> apply(Integer v) throws Throwable { + return Single.just(v).hide(); + } + }); + } + }); + } + + @Test + public void undeliverableUponCancelDelayError() { + TestHelper.checkUndeliverableUponCancel(new FlowableConverter<Integer, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Flowable<Integer> upstream) { + return upstream.switchMapSingleDelayError(new Function<Integer, Single<Integer>>() { + @Override + public Single<Integer> apply(Integer v) throws Throwable { + return Single.just(v).hide(); + } + }); + } + }); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/mixed/MaybeFlatMapObservableTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/mixed/MaybeFlatMapObservableTest.java new file mode 100644 index 0000000000..f91b5c6582 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/mixed/MaybeFlatMapObservableTest.java @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.mixed; + +import static org.junit.Assert.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.subjects.*; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class MaybeFlatMapObservableTest extends RxJavaTest { + + @Test + public void cancelMain() { + MaybeSubject<Integer> ms = MaybeSubject.create(); + PublishSubject<Integer> ps = PublishSubject.create(); + + TestObserver<Integer> to = ms.flatMapObservable(Functions.justFunction(ps)) + .test(); + + assertTrue(ms.hasObservers()); + assertFalse(ps.hasObservers()); + + to.dispose(); + + assertFalse(ms.hasObservers()); + assertFalse(ps.hasObservers()); + } + + @Test + public void cancelOther() { + MaybeSubject<Integer> ms = MaybeSubject.create(); + PublishSubject<Integer> ps = PublishSubject.create(); + + TestObserver<Integer> to = ms.flatMapObservable(Functions.justFunction(ps)) + .test(); + + assertTrue(ms.hasObservers()); + assertFalse(ps.hasObservers()); + + ms.onSuccess(1); + + assertFalse(ms.hasObservers()); + assertTrue(ps.hasObservers()); + + to.dispose(); + + assertFalse(ms.hasObservers()); + assertFalse(ps.hasObservers()); + } + + @Test + public void mapperCrash() { + Maybe.just(1).flatMapObservable(new Function<Integer, ObservableSource<? extends Object>>() { + @Override + public ObservableSource<? extends Object> apply(Integer v) throws Exception { + throw new TestException(); + } + }) + .test() + .assertFailure(TestException.class); + } + + @Test + public void isDisposed() { + TestHelper.checkDisposed(Maybe.never().flatMapObservable(Functions.justFunction(Observable.never()))); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/mixed/MaybeFlatMapPublisherTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/mixed/MaybeFlatMapPublisherTest.java new file mode 100644 index 0000000000..96948b05dc --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/mixed/MaybeFlatMapPublisherTest.java @@ -0,0 +1,92 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.mixed; + +import static org.junit.Assert.*; + +import org.junit.Test; +import org.reactivestreams.Publisher; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.processors.PublishProcessor; +import io.reactivex.rxjava3.subjects.MaybeSubject; +import io.reactivex.rxjava3.subscribers.TestSubscriber; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class MaybeFlatMapPublisherTest extends RxJavaTest { + + @Test + public void cancelMain() { + MaybeSubject<Integer> ms = MaybeSubject.create(); + PublishProcessor<Integer> pp = PublishProcessor.create(); + + TestSubscriber<Integer> ts = ms.flatMapPublisher(Functions.justFunction(pp)) + .test(); + + assertTrue(ms.hasObservers()); + assertFalse(pp.hasSubscribers()); + + ts.cancel(); + + assertFalse(ms.hasObservers()); + assertFalse(pp.hasSubscribers()); + } + + @Test + public void cancelOther() { + MaybeSubject<Integer> ms = MaybeSubject.create(); + PublishProcessor<Integer> pp = PublishProcessor.create(); + + TestSubscriber<Integer> ts = ms.flatMapPublisher(Functions.justFunction(pp)) + .test(); + + assertTrue(ms.hasObservers()); + assertFalse(pp.hasSubscribers()); + + ms.onSuccess(1); + + assertFalse(ms.hasObservers()); + assertTrue(pp.hasSubscribers()); + + ts.cancel(); + + assertFalse(ms.hasObservers()); + assertFalse(pp.hasSubscribers()); + } + + @Test + public void mapperCrash() { + Maybe.just(1).flatMapPublisher(new Function<Integer, Publisher<? extends Object>>() { + @Override + public Publisher<? extends Object> apply(Integer v) throws Exception { + throw new TestException(); + } + }) + .test() + .assertFailure(TestException.class); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeMaybeToFlowable(new Function<Maybe<Object>, Publisher<Object>>() { + @Override + public Publisher<Object> apply(Maybe<Object> m) throws Exception { + return m.flatMapPublisher(Functions.justFunction(Flowable.never())); + } + }); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/mixed/ObservableConcatMapCompletableTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/mixed/ObservableConcatMapCompletableTest.java new file mode 100644 index 0000000000..e69e3e0ee5 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/mixed/ObservableConcatMapCompletableTest.java @@ -0,0 +1,527 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.mixed; + +import static org.junit.Assert.*; + +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.internal.schedulers.ImmediateThinScheduler; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.subjects.*; +import io.reactivex.rxjava3.testsupport.*; + +public class ObservableConcatMapCompletableTest extends RxJavaTest { + + @Test + public void simple() { + Observable.range(1, 5) + .concatMapCompletable(Functions.justFunction(Completable.complete())) + .test() + .assertResult(); + } + + @Test + public void simple2() { + final AtomicInteger counter = new AtomicInteger(); + Observable.range(1, 5) + .concatMapCompletable(Functions.justFunction(Completable.fromAction(new Action() { + @Override + public void run() throws Exception { + counter.incrementAndGet(); + } + }))) + .test() + .assertResult(); + + assertEquals(5, counter.get()); + } + + @Test + public void simpleLongPrefetch() { + Observable.range(1, 1024) + .concatMapCompletable(Functions.justFunction(Completable.complete()), 32) + .test() + .assertResult(); + } + + @Test + public void mainError() { + Observable.<Integer>error(new TestException()) + .concatMapCompletable(Functions.justFunction(Completable.complete())) + .test() + .assertFailure(TestException.class); + } + + @Test + public void innerError() { + Observable.just(1) + .concatMapCompletable(Functions.justFunction(Completable.error(new TestException()))) + .test() + .assertFailure(TestException.class); + } + + @Test + public void innerErrorDelayed() { + TestObserverEx<Void> to = Observable.range(1, 5) + .concatMapCompletableDelayError( + new Function<Integer, CompletableSource>() { + @Override + public CompletableSource apply(Integer v) throws Exception { + return Completable.error(new TestException()); + } + } + ) + .to(TestHelper.<Void>testConsumer()) + .assertFailure(CompositeException.class) + ; + + assertEquals(5, ((CompositeException)to.errors().get(0)).getExceptions().size()); + } + + @Test + public void mapperCrash() { + Observable.just(1) + .concatMapCompletable(new Function<Integer, CompletableSource>() { + @Override + public CompletableSource apply(Integer v) throws Exception { + throw new TestException(); + } + }) + .test() + .assertFailure(TestException.class); + } + + @Test + public void mapperCrashHidden() { + Observable.just(1).hide() + .concatMapCompletable(new Function<Integer, CompletableSource>() { + @Override + public CompletableSource apply(Integer v) throws Exception { + throw new TestException(); + } + }) + .test() + .assertFailure(TestException.class); + } + + @Test + public void immediateError() { + PublishSubject<Integer> ps = PublishSubject.create(); + CompletableSubject cs = CompletableSubject.create(); + + TestObserver<Void> to = ps.concatMapCompletable( + Functions.justFunction(cs)).test(); + + to.assertEmpty(); + + assertTrue(ps.hasObservers()); + assertFalse(cs.hasObservers()); + + ps.onNext(1); + + assertTrue(cs.hasObservers()); + + ps.onError(new TestException()); + + assertFalse(cs.hasObservers()); + + to.assertFailure(TestException.class); + } + + @Test + public void immediateError2() { + PublishSubject<Integer> ps = PublishSubject.create(); + CompletableSubject cs = CompletableSubject.create(); + + TestObserver<Void> to = ps.concatMapCompletable( + Functions.justFunction(cs)).test(); + + to.assertEmpty(); + + assertTrue(ps.hasObservers()); + assertFalse(cs.hasObservers()); + + ps.onNext(1); + + assertTrue(cs.hasObservers()); + + cs.onError(new TestException()); + + assertFalse(ps.hasObservers()); + + to.assertFailure(TestException.class); + } + + @Test + public void boundaryError() { + PublishSubject<Integer> ps = PublishSubject.create(); + CompletableSubject cs = CompletableSubject.create(); + + TestObserver<Void> to = ps.concatMapCompletableDelayError( + Functions.justFunction(cs), false).test(); + + to.assertEmpty(); + + assertTrue(ps.hasObservers()); + assertFalse(cs.hasObservers()); + + ps.onNext(1); + + assertTrue(cs.hasObservers()); + + ps.onError(new TestException()); + + assertTrue(cs.hasObservers()); + + to.assertEmpty(); + + cs.onComplete(); + + to.assertFailure(TestException.class); + } + + @Test + public void endError() { + PublishSubject<Integer> ps = PublishSubject.create(); + final CompletableSubject cs = CompletableSubject.create(); + final CompletableSubject cs2 = CompletableSubject.create(); + + TestObserver<Void> to = ps.concatMapCompletableDelayError( + new Function<Integer, CompletableSource>() { + @Override + public CompletableSource apply(Integer v) throws Exception { + if (v == 1) { + return cs; + } + return cs2; + } + }, true, 32 + ) + .test(); + + to.assertEmpty(); + + assertTrue(ps.hasObservers()); + assertFalse(cs.hasObservers()); + + ps.onNext(1); + + assertTrue(cs.hasObservers()); + + cs.onError(new TestException()); + + assertTrue(ps.hasObservers()); + + ps.onNext(2); + + to.assertEmpty(); + + cs2.onComplete(); + + assertTrue(ps.hasObservers()); + + to.assertEmpty(); + + ps.onComplete(); + + to.assertFailure(TestException.class); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeObservableToCompletable( + new Function<Observable<Object>, Completable>() { + @Override + public Completable apply(Observable<Object> f) + throws Exception { + return f.concatMapCompletable( + Functions.justFunction(Completable.complete())); + } + } + ); + } + + @Test + public void disposed() { + TestHelper.checkDisposed( + Observable.never() + .concatMapCompletable( + Functions.justFunction(Completable.complete())) + ); + } + + @Test + public void immediateOuterInnerErrorRace() { + final TestException ex = new TestException(); + for (int i = 0; i < TestHelper.RACE_LONG_LOOPS; i++) { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + final PublishSubject<Integer> ps = PublishSubject.create(); + final CompletableSubject cs = CompletableSubject.create(); + + TestObserver<Void> to = ps.concatMapCompletable( + Functions.justFunction(cs) + ) + .test(); + + ps.onNext(1); + + Runnable r1 = new Runnable() { + @Override + public void run() { + ps.onError(ex); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + cs.onError(ex); + } + }; + + TestHelper.race(r1, r2); + + to.assertError(new Predicate<Throwable>() { + @Override + public boolean test(Throwable e) throws Exception { + return e instanceof TestException || e instanceof CompositeException; + } + }) + .assertNotComplete(); + + if (!errors.isEmpty()) { + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } + } finally { + RxJavaPlugins.reset(); + } + } + } + + @Test + public void disposeInDrainLoop() { + for (int i = 0; i < TestHelper.RACE_LONG_LOOPS; i++) { + final PublishSubject<Integer> ps = PublishSubject.create(); + final CompletableSubject cs = CompletableSubject.create(); + + final TestObserver<Void> to = ps.concatMapCompletable( + Functions.justFunction(cs) + ) + .test(); + + ps.onNext(1); + + Runnable r1 = new Runnable() { + @Override + public void run() { + ps.onNext(2); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + cs.onComplete(); + to.dispose(); + } + }; + + TestHelper.race(r1, r2); + + to.assertEmpty(); + } + } + + @Test + public void doneButNotEmpty() { + final PublishSubject<Integer> ps = PublishSubject.create(); + final CompletableSubject cs = CompletableSubject.create(); + + final TestObserver<Void> to = ps.concatMapCompletable( + Functions.justFunction(cs) + ) + .test(); + + ps.onNext(1); + ps.onNext(2); + ps.onComplete(); + + cs.onComplete(); + + to.assertResult(); + } + + @Test + public void asyncFused() { + final PublishSubject<Integer> ps = PublishSubject.create(); + final CompletableSubject cs = CompletableSubject.create(); + + final TestObserver<Void> to = ps.observeOn(ImmediateThinScheduler.INSTANCE) + .concatMapCompletable( + Functions.justFunction(cs) + ) + .test(); + + ps.onNext(1); + ps.onComplete(); + + cs.onComplete(); + + to.assertResult(); + } + + @Test + public void fusionRejected() { + final CompletableSubject cs = CompletableSubject.create(); + + TestHelper.rejectObservableFusion() + .concatMapCompletable( + Functions.justFunction(cs) + ) + .test() + .assertEmpty(); + } + + @Test + public void emptyScalarSource() { + final CompletableSubject cs = CompletableSubject.create(); + + Observable.empty() + .concatMapCompletable(Functions.justFunction(cs)) + .test() + .assertResult(); + } + + @Test + public void justScalarSource() { + final CompletableSubject cs = CompletableSubject.create(); + + TestObserver<Void> to = Observable.just(1) + .concatMapCompletable(Functions.justFunction(cs)) + .test(); + + to.assertEmpty(); + + assertTrue(cs.hasObservers()); + + cs.onComplete(); + + to.assertResult(); + } + + @Test + public void undeliverableUponCancel() { + TestHelper.checkUndeliverableUponCancel(new ObservableConverter<Integer, Completable>() { + @Override + public Completable apply(Observable<Integer> upstream) { + return upstream.concatMapCompletable(new Function<Integer, Completable>() { + @Override + public Completable apply(Integer v) throws Throwable { + return Completable.complete().hide(); + } + }); + } + }); + } + + @Test + public void undeliverableUponCancelDelayError() { + TestHelper.checkUndeliverableUponCancel(new ObservableConverter<Integer, Completable>() { + @Override + public Completable apply(Observable<Integer> upstream) { + return upstream.concatMapCompletableDelayError(new Function<Integer, Completable>() { + @Override + public Completable apply(Integer v) throws Throwable { + return Completable.complete().hide(); + } + }, false, 2); + } + }); + } + + @Test + public void undeliverableUponCancelDelayErrorTillEnd() { + TestHelper.checkUndeliverableUponCancel(new ObservableConverter<Integer, Completable>() { + @Override + public Completable apply(Observable<Integer> upstream) { + return upstream.concatMapCompletableDelayError(new Function<Integer, Completable>() { + @Override + public Completable apply(Integer v) throws Throwable { + return Completable.complete().hide(); + } + }, true, 2); + } + }); + } + + @Test + public void basicNonFused() { + Observable.range(1, 5).hide() + .concatMapCompletable(v -> Completable.complete().hide()) + .test() + .assertResult(); + } + + @Test + public void basicSyncFused() { + Observable.range(1, 5) + .concatMapCompletable(v -> Completable.complete().hide()) + .test() + .assertResult(); + } + + @Test + public void basicAsyncFused() { + UnicastSubject<Integer> us = UnicastSubject.create(); + TestHelper.emit(us, 1, 2, 3, 4, 5); + + us + .concatMapCompletable(v -> Completable.complete().hide()) + .test() + .assertResult(); + } + + @Test + public void basicFusionRejected() { + TestHelper.<Integer>rejectObservableFusion() + .concatMapCompletable(v -> Completable.complete().hide()) + .test() + .assertEmpty(); + } + + @Test + public void fusedPollCrash() { + Observable.range(1, 5) + .map(v -> { + if (v == 3) { + throw new TestException(); + } + return v; + }) + .compose(TestHelper.observableStripBoundary()) + .concatMapCompletable(v -> Completable.complete().hide()) + .test() + .assertFailure(TestException.class); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/mixed/ObservableConcatMapMaybeTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/mixed/ObservableConcatMapMaybeTest.java new file mode 100644 index 0000000000..0dcbd27408 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/mixed/ObservableConcatMapMaybeTest.java @@ -0,0 +1,538 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.mixed; + +import static org.junit.Assert.*; + +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +import io.reactivex.rxjava3.disposables.Disposable; +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.internal.operators.mixed.ObservableConcatMapMaybe.ConcatMapMaybeMainObserver; +import io.reactivex.rxjava3.internal.util.ErrorMode; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.schedulers.Schedulers; +import io.reactivex.rxjava3.subjects.*; +import io.reactivex.rxjava3.testsupport.*; + +public class ObservableConcatMapMaybeTest extends RxJavaTest { + + @Test + public void simple() { + Observable.range(1, 5) + .concatMapMaybe(new Function<Integer, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Integer v) + throws Exception { + return Maybe.just(v); + } + }) + .test() + .assertResult(1, 2, 3, 4, 5); + } + + @Test + public void simpleLong() { + Observable.range(1, 1024) + .concatMapMaybe(new Function<Integer, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Integer v) + throws Exception { + return Maybe.just(v); + } + }, 32) + .test() + .assertValueCount(1024) + .assertNoErrors() + .assertComplete(); + } + + @Test + public void empty() { + Observable.range(1, 10) + .concatMapMaybe(new Function<Integer, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Integer v) + throws Exception { + return Maybe.empty(); + } + }) + .test() + .assertResult(); + } + + @Test + public void mixed() { + Observable.range(1, 10) + .concatMapMaybe(new Function<Integer, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Integer v) + throws Exception { + if (v % 2 == 0) { + return Maybe.just(v); + } + return Maybe.empty(); + } + }) + .test() + .assertResult(2, 4, 6, 8, 10); + } + + @Test + public void mixedLong() { + TestObserverEx<Integer> to = Observable.range(1, 1024) + .concatMapMaybe(new Function<Integer, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Integer v) + throws Exception { + if (v % 2 == 0) { + return Maybe.just(v).subscribeOn(Schedulers.computation()); + } + return Maybe.<Integer>empty().subscribeOn(Schedulers.computation()); + } + }) + .to(TestHelper.<Integer>testConsumer()) + .awaitDone(5, TimeUnit.SECONDS) + .assertValueCount(512) + .assertNoErrors() + .assertComplete() + ; + + for (int i = 0; i < 512; i ++) { + to.assertValueAt(i, (i + 1) * 2); + } + } + + @Test + public void mainError() { + Observable.error(new TestException()) + .concatMapMaybe(Functions.justFunction(Maybe.just(1))) + .test() + .assertFailure(TestException.class); + } + + @Test + public void innerError() { + Observable.just(1) + .concatMapMaybe(Functions.justFunction(Maybe.error(new TestException()))) + .test() + .assertFailure(TestException.class); + } + + @Test + public void mainBoundaryErrorInnerSuccess() { + PublishSubject<Integer> ps = PublishSubject.create(); + MaybeSubject<Integer> ms = MaybeSubject.create(); + + TestObserver<Integer> to = ps.concatMapMaybeDelayError(Functions.justFunction(ms), false).test(); + + to.assertEmpty(); + + ps.onNext(1); + + assertTrue(ms.hasObservers()); + + ps.onError(new TestException()); + + assertTrue(ms.hasObservers()); + + to.assertEmpty(); + + ms.onSuccess(1); + + to.assertFailure(TestException.class, 1); + } + + @Test + public void mainBoundaryErrorInnerEmpty() { + PublishSubject<Integer> ps = PublishSubject.create(); + MaybeSubject<Integer> ms = MaybeSubject.create(); + + TestObserver<Integer> to = ps.concatMapMaybeDelayError(Functions.justFunction(ms), false).test(); + + to.assertEmpty(); + + ps.onNext(1); + + assertTrue(ms.hasObservers()); + + ps.onError(new TestException()); + + assertTrue(ms.hasObservers()); + + to.assertEmpty(); + + ms.onComplete(); + + to.assertFailure(TestException.class); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeObservable( + new Function<Observable<Object>, Observable<Object>>() { + @Override + public Observable<Object> apply(Observable<Object> f) + throws Exception { + return f.concatMapMaybeDelayError( + Functions.justFunction(Maybe.empty())); + } + } + ); + } + + @Test + public void take() { + Observable.range(1, 5) + .concatMapMaybe(new Function<Integer, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Integer v) + throws Exception { + return Maybe.just(v); + } + }) + .take(3) + .test() + .assertResult(1, 2, 3); + } + + @Test + public void cancel() { + Observable.range(1, 5).concatWith(Observable.<Integer>never()) + .concatMapMaybe(new Function<Integer, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Integer v) + throws Exception { + return Maybe.just(v); + } + }) + .test() + .assertValues(1, 2, 3, 4, 5) + .assertNoErrors() + .assertNotComplete() + .dispose(); + } + + @Test + public void mainErrorAfterInnerError() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + new Observable<Integer>() { + @Override + protected void subscribeActual(Observer<? super Integer> observer) { + observer.onSubscribe(Disposable.empty()); + observer.onNext(1); + observer.onError(new TestException("outer")); + } + } + .concatMapMaybe( + Functions.justFunction(Maybe.error(new TestException("inner"))), 1 + ) + .to(TestHelper.<Object>testConsumer()) + .assertFailureAndMessage(TestException.class, "inner"); + + TestHelper.assertUndeliverable(errors, 0, TestException.class, "outer"); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void innerErrorAfterMainError() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + final PublishSubject<Integer> ps = PublishSubject.create(); + + final AtomicReference<MaybeObserver<? super Integer>> obs = new AtomicReference<>(); + + TestObserverEx<Integer> to = ps.concatMapMaybe( + new Function<Integer, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Integer v) + throws Exception { + return new Maybe<Integer>() { + @Override + protected void subscribeActual( + MaybeObserver<? super Integer> observer) { + observer.onSubscribe(Disposable.empty()); + obs.set(observer); + } + }; + } + } + ).to(TestHelper.<Integer>testConsumer()); + + ps.onNext(1); + + ps.onError(new TestException("outer")); + obs.get().onError(new TestException("inner")); + + to.assertFailureAndMessage(TestException.class, "outer"); + + TestHelper.assertUndeliverable(errors, 0, TestException.class, "inner"); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void delayAllErrors() { + TestObserverEx<Object> to = Observable.range(1, 5) + .concatMapMaybeDelayError(new Function<Integer, MaybeSource<? extends Object>>() { + @Override + public MaybeSource<? extends Object> apply(Integer v) + throws Exception { + return Maybe.error(new TestException()); + } + }) + .to(TestHelper.<Object>testConsumer()) + .assertFailure(CompositeException.class) + ; + + CompositeException ce = (CompositeException)to.errors().get(0); + assertEquals(5, ce.getExceptions().size()); + } + + @Test + public void mapperCrash() { + final PublishSubject<Integer> ps = PublishSubject.create(); + + TestObserver<Object> to = ps + .concatMapMaybe(new Function<Integer, MaybeSource<? extends Object>>() { + @Override + public MaybeSource<? extends Object> apply(Integer v) + throws Exception { + throw new TestException(); + } + }) + .test(); + + to.assertEmpty(); + + assertTrue(ps.hasObservers()); + + ps.onNext(1); + + to.assertFailure(TestException.class); + + assertFalse(ps.hasObservers()); + } + + @Test + public void scalarMapperCrash() { + TestObserver<Object> to = Observable.just(1) + .concatMapMaybe(new Function<Integer, MaybeSource<? extends Object>>() { + @Override + public MaybeSource<? extends Object> apply(Integer v) + throws Exception { + throw new TestException(); + } + }) + .test(); + + to.assertFailure(TestException.class); + } + + @Test + public void disposed() { + TestHelper.checkDisposed(Observable.just(1).hide() + .concatMapMaybe(Functions.justFunction(Maybe.never())) + ); + } + + @Test + public void scalarEmptySource() { + MaybeSubject<Integer> ms = MaybeSubject.create(); + + Observable.empty() + .concatMapMaybe(Functions.justFunction(ms)) + .test() + .assertResult(); + + assertFalse(ms.hasObservers()); + } + + @Test + public void cancelNoConcurrentClean() { + TestObserver<Integer> to = new TestObserver<>(); + ConcatMapMaybeMainObserver<Integer, Integer> operator = + new ConcatMapMaybeMainObserver<>( + to, Functions.justFunction(Maybe.<Integer>never()), 16, ErrorMode.IMMEDIATE); + + operator.onSubscribe(Disposable.empty()); + + operator.queue.offer(1); + + operator.getAndIncrement(); + + to.dispose(); + + assertFalse(operator.queue.isEmpty()); + + operator.addAndGet(-2); + + operator.dispose(); + + assertTrue(operator.queue.isEmpty()); + } + + @Test + public void checkUnboundedInnerQueue() { + MaybeSubject<Integer> ms = MaybeSubject.create(); + + TestObserver<Integer> to = Observable + .fromArray(ms, Maybe.just(2), Maybe.just(3), Maybe.just(4)) + .concatMapMaybe(Functions.<Maybe<Integer>>identity(), 2) + .test(); + + to.assertEmpty(); + + ms.onSuccess(1); + + to.assertResult(1, 2, 3, 4); + } + + @Test + public void innerSuccessDisposeRace() { + for (int i = 0; i < TestHelper.RACE_LONG_LOOPS; i++) { + + final MaybeSubject<Integer> ms = MaybeSubject.create(); + + final TestObserver<Integer> to = Observable.just(1) + .hide() + .concatMapMaybe(Functions.justFunction(ms)) + .test(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + ms.onSuccess(1); + } + }; + Runnable r2 = new Runnable() { + @Override + public void run() { + to.dispose(); + } + }; + + TestHelper.race(r1, r2); + + to.assertNoErrors(); + } + } + + @Test + public void undeliverableUponCancel() { + TestHelper.checkUndeliverableUponCancel(new ObservableConverter<Integer, Observable<Integer>>() { + @Override + public Observable<Integer> apply(Observable<Integer> upstream) { + return upstream.concatMapMaybe(new Function<Integer, Maybe<Integer>>() { + @Override + public Maybe<Integer> apply(Integer v) throws Throwable { + return Maybe.just(v).hide(); + } + }); + } + }); + } + + @Test + public void undeliverableUponCancelDelayError() { + TestHelper.checkUndeliverableUponCancel(new ObservableConverter<Integer, Observable<Integer>>() { + @Override + public Observable<Integer> apply(Observable<Integer> upstream) { + return upstream.concatMapMaybeDelayError(new Function<Integer, Maybe<Integer>>() { + @Override + public Maybe<Integer> apply(Integer v) throws Throwable { + return Maybe.just(v).hide(); + } + }, false, 2); + } + }); + } + + @Test + public void undeliverableUponCancelDelayErrorTillEnd() { + TestHelper.checkUndeliverableUponCancel(new ObservableConverter<Integer, Observable<Integer>>() { + @Override + public Observable<Integer> apply(Observable<Integer> upstream) { + return upstream.concatMapMaybeDelayError(new Function<Integer, Maybe<Integer>>() { + @Override + public Maybe<Integer> apply(Integer v) throws Throwable { + return Maybe.just(v).hide(); + } + }, true, 2); + } + }); + } + + @Test + public void basicNonFused() { + Observable.range(1, 5).hide() + .concatMapMaybe(v -> Maybe.just(v).hide()) + .test() + .assertResult(1, 2, 3, 4, 5); + } + + @Test + public void basicSyncFused() { + Observable.range(1, 5) + .concatMapMaybe(v -> Maybe.just(v).hide()) + .test() + .assertResult(1, 2, 3, 4, 5); + } + + @Test + public void basicAsyncFused() { + UnicastSubject<Integer> us = UnicastSubject.create(); + TestHelper.emit(us, 1, 2, 3, 4, 5); + + us + .concatMapMaybe(v -> Maybe.just(v).hide()) + .test() + .assertResult(1, 2, 3, 4, 5); + } + + @Test + public void basicFusionRejected() { + TestHelper.<Integer>rejectObservableFusion() + .concatMapMaybe(v -> Maybe.just(v).hide()) + .test() + .assertEmpty(); + } + + @Test + public void fusedPollCrash() { + Observable.range(1, 5) + .map(v -> { + if (v == 3) { + throw new TestException(); + } + return v; + }) + .compose(TestHelper.observableStripBoundary()) + .concatMapMaybe(v -> Maybe.just(v).hide()) + .test() + .assertFailure(TestException.class, 1, 2); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/mixed/ObservableConcatMapSingleTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/mixed/ObservableConcatMapSingleTest.java new file mode 100644 index 0000000000..9f70b4addf --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/mixed/ObservableConcatMapSingleTest.java @@ -0,0 +1,478 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.mixed; + +import static org.junit.Assert.*; + +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; + +import io.reactivex.rxjava3.disposables.Disposable; +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.internal.operators.mixed.ObservableConcatMapSingle.ConcatMapSingleMainObserver; +import io.reactivex.rxjava3.internal.util.ErrorMode; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.subjects.*; +import io.reactivex.rxjava3.testsupport.*; + +public class ObservableConcatMapSingleTest extends RxJavaTest { + + @Test + public void simple() { + Observable.range(1, 5) + .concatMapSingle(new Function<Integer, SingleSource<Integer>>() { + @Override + public SingleSource<Integer> apply(Integer v) + throws Exception { + return Single.just(v); + } + }) + .test() + .assertResult(1, 2, 3, 4, 5); + } + + @Test + public void simpleLong() { + Observable.range(1, 1024) + .concatMapSingle(new Function<Integer, SingleSource<Integer>>() { + @Override + public SingleSource<Integer> apply(Integer v) + throws Exception { + return Single.just(v); + } + }, 32) + .test() + .assertValueCount(1024) + .assertNoErrors() + .assertComplete(); + } + + @Test + public void mainError() { + Observable.error(new TestException()) + .concatMapSingle(Functions.justFunction(Single.just(1))) + .test() + .assertFailure(TestException.class); + } + + @Test + public void innerError() { + Observable.just(1) + .concatMapSingle(Functions.justFunction(Single.error(new TestException()))) + .test() + .assertFailure(TestException.class); + } + + @Test + public void mainBoundaryErrorInnerSuccess() { + PublishSubject<Integer> ps = PublishSubject.create(); + SingleSubject<Integer> ms = SingleSubject.create(); + + TestObserver<Integer> to = ps.concatMapSingleDelayError(Functions.justFunction(ms), false).test(); + + to.assertEmpty(); + + ps.onNext(1); + + assertTrue(ms.hasObservers()); + + ps.onError(new TestException()); + + assertTrue(ms.hasObservers()); + + to.assertEmpty(); + + ms.onSuccess(1); + + to.assertFailure(TestException.class, 1); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeObservable( + new Function<Observable<Object>, Observable<Object>>() { + @Override + public Observable<Object> apply(Observable<Object> f) + throws Exception { + return f.concatMapSingleDelayError( + Functions.justFunction(Single.never())); + } + } + ); + } + + @Test + public void take() { + Observable.range(1, 5) + .concatMapSingle(new Function<Integer, SingleSource<Integer>>() { + @Override + public SingleSource<Integer> apply(Integer v) + throws Exception { + return Single.just(v); + } + }) + .take(3) + .test() + .assertResult(1, 2, 3); + } + + @Test + public void cancel() { + Observable.range(1, 5).concatWith(Observable.<Integer>never()) + .concatMapSingle(new Function<Integer, SingleSource<Integer>>() { + @Override + public SingleSource<Integer> apply(Integer v) + throws Exception { + return Single.just(v); + } + }) + .test() + .assertValues(1, 2, 3, 4, 5) + .assertNoErrors() + .assertNotComplete() + .dispose(); + } + + @Test + public void mainErrorAfterInnerError() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + new Observable<Integer>() { + @Override + protected void subscribeActual(Observer<? super Integer> observer) { + observer.onSubscribe(Disposable.empty()); + observer.onNext(1); + observer.onError(new TestException("outer")); + } + } + .concatMapSingle( + Functions.justFunction(Single.error(new TestException("inner"))), 1 + ) + .to(TestHelper.<Object>testConsumer()) + .assertFailureAndMessage(TestException.class, "inner"); + + TestHelper.assertUndeliverable(errors, 0, TestException.class, "outer"); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void innerErrorAfterMainError() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + final PublishSubject<Integer> ps = PublishSubject.create(); + + final AtomicReference<SingleObserver<? super Integer>> obs = new AtomicReference<>(); + + TestObserverEx<Integer> to = ps.concatMapSingle( + new Function<Integer, SingleSource<Integer>>() { + @Override + public SingleSource<Integer> apply(Integer v) + throws Exception { + return new Single<Integer>() { + @Override + protected void subscribeActual( + SingleObserver<? super Integer> observer) { + observer.onSubscribe(Disposable.empty()); + obs.set(observer); + } + }; + } + } + ).to(TestHelper.<Integer>testConsumer()); + + ps.onNext(1); + + ps.onError(new TestException("outer")); + obs.get().onError(new TestException("inner")); + + to.assertFailureAndMessage(TestException.class, "outer"); + + TestHelper.assertUndeliverable(errors, 0, TestException.class, "inner"); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void delayAllErrors() { + TestObserverEx<Object> to = Observable.range(1, 5) + .concatMapSingleDelayError(new Function<Integer, SingleSource<? extends Object>>() { + @Override + public SingleSource<? extends Object> apply(Integer v) + throws Exception { + return Single.error(new TestException()); + } + }) + .to(TestHelper.<Object>testConsumer()) + .assertFailure(CompositeException.class) + ; + + CompositeException ce = (CompositeException)to.errors().get(0); + assertEquals(5, ce.getExceptions().size()); + } + + @Test + public void mapperCrash() { + final PublishSubject<Integer> ps = PublishSubject.create(); + + TestObserver<Object> to = ps + .concatMapSingle(new Function<Integer, SingleSource<? extends Object>>() { + @Override + public SingleSource<? extends Object> apply(Integer v) + throws Exception { + throw new TestException(); + } + }) + .test(); + + to.assertEmpty(); + + assertTrue(ps.hasObservers()); + + ps.onNext(1); + + to.assertFailure(TestException.class); + + assertFalse(ps.hasObservers()); + } + + @Test + public void mapperCrashScalar() { + TestObserver<Object> to = Observable.just(1) + .concatMapSingle(new Function<Integer, SingleSource<? extends Object>>() { + @Override + public SingleSource<? extends Object> apply(Integer v) + throws Exception { + throw new TestException(); + } + }) + .test(); + + to.assertFailure(TestException.class); + } + + @Test + public void disposed() { + TestHelper.checkDisposed(Observable.just(1).hide() + .concatMapSingle(Functions.justFunction(Single.never())) + ); + } + + @Test + public void mainCompletesWhileInnerActive() { + PublishSubject<Integer> ps = PublishSubject.create(); + SingleSubject<Integer> ms = SingleSubject.create(); + + TestObserver<Integer> to = ps.concatMapSingleDelayError(Functions.justFunction(ms), false).test(); + + to.assertEmpty(); + + ps.onNext(1); + ps.onNext(2); + ps.onComplete(); + + assertTrue(ms.hasObservers()); + + to.assertEmpty(); + + ms.onSuccess(1); + + to.assertResult(1, 1); + } + + @Test + public void scalarEmptySource() { + SingleSubject<Integer> ss = SingleSubject.create(); + + Observable.empty() + .concatMapSingle(Functions.justFunction(ss)) + .test() + .assertResult(); + + assertFalse(ss.hasObservers()); + } + + @Test + public void cancelNoConcurrentClean() { + TestObserver<Integer> to = new TestObserver<>(); + ConcatMapSingleMainObserver<Integer, Integer> operator = + new ConcatMapSingleMainObserver<>( + to, Functions.justFunction(Single.<Integer>never()), 16, ErrorMode.IMMEDIATE); + + operator.onSubscribe(Disposable.empty()); + + operator.queue.offer(1); + + operator.getAndIncrement(); + + to.dispose(); + + assertFalse(operator.queue.isEmpty()); + + operator.addAndGet(-2); + + operator.dispose(); + + assertTrue(operator.queue.isEmpty()); + } + + @Test + public void checkUnboundedInnerQueue() { + SingleSubject<Integer> ss = SingleSubject.create(); + + TestObserver<Integer> to = Observable + .fromArray(ss, Single.just(2), Single.just(3), Single.just(4)) + .concatMapSingle(Functions.<Single<Integer>>identity(), 2) + .test(); + + to.assertEmpty(); + + ss.onSuccess(1); + + to.assertResult(1, 2, 3, 4); + } + + @Test + public void innerSuccessDisposeRace() { + for (int i = 0; i < TestHelper.RACE_LONG_LOOPS; i++) { + + final SingleSubject<Integer> ss = SingleSubject.create(); + + final TestObserver<Integer> to = Observable.just(1) + .hide() + .concatMapSingle(Functions.justFunction(ss)) + .test(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + ss.onSuccess(1); + } + }; + Runnable r2 = new Runnable() { + @Override + public void run() { + to.dispose(); + } + }; + + TestHelper.race(r1, r2); + + to.assertNoErrors(); + } + } + + @Test + public void undeliverableUponCancel() { + TestHelper.checkUndeliverableUponCancel(new ObservableConverter<Integer, Observable<Integer>>() { + @Override + public Observable<Integer> apply(Observable<Integer> upstream) { + return upstream.concatMapSingle(new Function<Integer, Single<Integer>>() { + @Override + public Single<Integer> apply(Integer v) throws Throwable { + return Single.just(v).hide(); + } + }); + } + }); + } + + @Test + public void undeliverableUponCancelDelayError() { + TestHelper.checkUndeliverableUponCancel(new ObservableConverter<Integer, Observable<Integer>>() { + @Override + public Observable<Integer> apply(Observable<Integer> upstream) { + return upstream.concatMapSingleDelayError(new Function<Integer, Single<Integer>>() { + @Override + public Single<Integer> apply(Integer v) throws Throwable { + return Single.just(v).hide(); + } + }, false, 2); + } + }); + } + + @Test + public void undeliverableUponCancelDelayErrorTillEnd() { + TestHelper.checkUndeliverableUponCancel(new ObservableConverter<Integer, Observable<Integer>>() { + @Override + public Observable<Integer> apply(Observable<Integer> upstream) { + return upstream.concatMapSingleDelayError(new Function<Integer, Single<Integer>>() { + @Override + public Single<Integer> apply(Integer v) throws Throwable { + return Single.just(v).hide(); + } + }, true, 2); + } + }); + } + + @Test + public void basicNonFused() { + Observable.range(1, 5).hide() + .concatMapSingle(v -> Single.just(v).hide()) + .test() + .assertResult(1, 2, 3, 4, 5); + } + + @Test + public void basicSyncFused() { + Observable.range(1, 5) + .concatMapSingle(v -> Single.just(v).hide()) + .test() + .assertResult(1, 2, 3, 4, 5); + } + + @Test + public void basicAsyncFused() { + UnicastSubject<Integer> us = UnicastSubject.create(); + TestHelper.emit(us, 1, 2, 3, 4, 5); + + us + .concatMapSingle(v -> Single.just(v).hide()) + .test() + .assertResult(1, 2, 3, 4, 5); + } + + @Test + public void basicFusionRejected() { + TestHelper.<Integer>rejectObservableFusion() + .concatMapSingle(v -> Single.just(v).hide()) + .test() + .assertEmpty(); + } + + @Test + public void fusedPollCrash() { + Observable.range(1, 5) + .map(v -> { + if (v == 3) { + throw new TestException(); + } + return v; + }) + .compose(TestHelper.observableStripBoundary()) + .concatMapSingle(v -> Single.just(v).hide()) + .test() + .assertFailure(TestException.class, 1, 2); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/mixed/ObservableSwitchMapCompletableTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/mixed/ObservableSwitchMapCompletableTest.java new file mode 100644 index 0000000000..0efeb6f676 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/mixed/ObservableSwitchMapCompletableTest.java @@ -0,0 +1,462 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.mixed; + +import static org.junit.Assert.*; + +import java.util.List; + +import io.reactivex.rxjava3.disposables.Disposable; +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.subjects.*; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class ObservableSwitchMapCompletableTest extends RxJavaTest { + + @Test + public void normal() { + Observable.range(1, 10) + .switchMapCompletable(new Function<Integer, CompletableSource>() { + @Override + public CompletableSource apply(Integer v) throws Exception { + return Completable.complete(); + } + }) + .test() + .assertResult(); + } + + @Test + public void mainError() { + Observable.<Integer>error(new TestException()) + .switchMapCompletable(new Function<Integer, CompletableSource>() { + @Override + public CompletableSource apply(Integer v) throws Exception { + return Completable.complete(); + } + }) + .test() + .assertFailure(TestException.class); + } + + @Test + public void innerError() { + PublishSubject<Integer> ps = PublishSubject.create(); + CompletableSubject cs = CompletableSubject.create(); + + TestObserver<Void> to = ps.switchMapCompletable(Functions.justFunction(cs)) + .test(); + + assertTrue(ps.hasObservers()); + assertFalse(cs.hasObservers()); + + ps.onNext(1); + + assertTrue(cs.hasObservers()); + + to.assertEmpty(); + + cs.onError(new TestException()); + + to.assertFailure(TestException.class); + + assertFalse(ps.hasObservers()); + assertFalse(cs.hasObservers()); + } + + @Test + public void switchOver() { + final CompletableSubject[] css = { + CompletableSubject.create(), + CompletableSubject.create() + }; + + PublishSubject<Integer> ps = PublishSubject.create(); + + TestObserver<Void> to = ps.switchMapCompletable(new Function<Integer, CompletableSource>() { + @Override + public CompletableSource apply(Integer v) throws Exception { + return css[v]; + } + }) + .test(); + + to.assertEmpty(); + + ps.onNext(0); + + assertTrue(css[0].hasObservers()); + + ps.onNext(1); + + assertFalse(css[0].hasObservers()); + assertTrue(css[1].hasObservers()); + + ps.onComplete(); + + to.assertEmpty(); + + assertTrue(css[1].hasObservers()); + + css[1].onComplete(); + + to.assertResult(); + } + + @Test + public void dispose() { + PublishSubject<Integer> ps = PublishSubject.create(); + CompletableSubject cs = CompletableSubject.create(); + + TestObserver<Void> to = ps.switchMapCompletable(Functions.justFunction(cs)) + .test(); + + ps.onNext(1); + + assertTrue(ps.hasObservers()); + assertTrue(cs.hasObservers()); + + to.dispose(); + + assertFalse(ps.hasObservers()); + assertFalse(cs.hasObservers()); + } + + @Test + public void checkDisposed() { + PublishSubject<Integer> ps = PublishSubject.create(); + CompletableSubject cs = CompletableSubject.create(); + + TestHelper.checkDisposed(ps.switchMapCompletable(Functions.justFunction(cs))); + } + + @Test + public void checkBadSource() { + TestHelper.checkDoubleOnSubscribeObservableToCompletable(new Function<Observable<Object>, Completable>() { + @Override + public Completable apply(Observable<Object> f) throws Exception { + return f.switchMapCompletable(Functions.justFunction(Completable.never())); + } + }); + } + + @Test + public void mapperCrash() { + Observable.range(1, 5).switchMapCompletable(new Function<Integer, CompletableSource>() { + @Override + public CompletableSource apply(Integer f) throws Exception { + throw new TestException(); + } + }) + .test() + .assertFailure(TestException.class); + } + + @Test + public void mapperCancels() { + final TestObserver<Void> to = new TestObserver<>(); + + Observable.range(1, 5).switchMapCompletable(new Function<Integer, CompletableSource>() { + @Override + public CompletableSource apply(Integer f) throws Exception { + to.dispose(); + return Completable.complete(); + } + }) + .subscribe(to); + + to.assertEmpty(); + } + + @Test + public void onNextInnerCompleteRace() { + for (int i = 0; i < TestHelper.RACE_LONG_LOOPS; i++) { + final PublishSubject<Integer> ps = PublishSubject.create(); + final CompletableSubject cs = CompletableSubject.create(); + + TestObserver<Void> to = ps.switchMapCompletable(Functions.justFunction(cs)).test(); + + ps.onNext(1); + + Runnable r1 = new Runnable() { + @Override + public void run() { + ps.onNext(2); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + cs.onComplete(); + } + }; + + TestHelper.race(r1, r2); + + to.assertEmpty(); + } + } + + @Test + public void onNextInnerErrorRace() { + final TestException ex = new TestException(); + for (int i = 0; i < TestHelper.RACE_LONG_LOOPS; i++) { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + final PublishSubject<Integer> ps = PublishSubject.create(); + final CompletableSubject cs = CompletableSubject.create(); + + TestObserver<Void> to = ps.switchMapCompletable(Functions.justFunction(cs)).test(); + + ps.onNext(1); + + Runnable r1 = new Runnable() { + @Override + public void run() { + ps.onNext(2); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + cs.onError(ex); + } + }; + + TestHelper.race(r1, r2); + + to.assertError(new Predicate<Throwable>() { + @Override + public boolean test(Throwable e) throws Exception { + return e instanceof TestException || e instanceof CompositeException; + } + }); + + if (!errors.isEmpty()) { + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } + } finally { + RxJavaPlugins.reset(); + } + } + } + + @Test + public void onErrorInnerErrorRace() { + final TestException ex0 = new TestException(); + final TestException ex = new TestException(); + for (int i = 0; i < TestHelper.RACE_LONG_LOOPS; i++) { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + final PublishSubject<Integer> ps = PublishSubject.create(); + final CompletableSubject cs = CompletableSubject.create(); + + TestObserver<Void> to = ps.switchMapCompletable(Functions.justFunction(cs)).test(); + + ps.onNext(1); + + Runnable r1 = new Runnable() { + @Override + public void run() { + ps.onError(ex0); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + cs.onError(ex); + } + }; + + TestHelper.race(r1, r2); + + to.assertError(new Predicate<Throwable>() { + @Override + public boolean test(Throwable e) throws Exception { + return e instanceof TestException || e instanceof CompositeException; + } + }); + + if (!errors.isEmpty()) { + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } + } finally { + RxJavaPlugins.reset(); + } + } + } + + @Test + public void innerErrorThenMainError() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + new Observable<Integer>() { + @Override + protected void subscribeActual(Observer<? super Integer> observer) { + observer.onSubscribe(Disposable.empty()); + observer.onNext(1); + observer.onError(new TestException("main")); + } + } + .switchMapCompletable(Functions.justFunction(Completable.error(new TestException("inner")))) + .to(TestHelper.testConsumer()) + .assertFailureAndMessage(TestException.class, "inner"); + + TestHelper.assertUndeliverable(errors, 0, TestException.class, "main"); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void innerErrorDelayed() { + final PublishSubject<Integer> ps = PublishSubject.create(); + final CompletableSubject cs = CompletableSubject.create(); + + TestObserver<Void> to = ps.switchMapCompletableDelayError(Functions.justFunction(cs)).test(); + + ps.onNext(1); + + cs.onError(new TestException()); + + to.assertEmpty(); + + assertTrue(ps.hasObservers()); + + ps.onComplete(); + + to.assertFailure(TestException.class); + } + + @Test + public void mainCompletesinnerErrorDelayed() { + final PublishSubject<Integer> ps = PublishSubject.create(); + final CompletableSubject cs = CompletableSubject.create(); + + TestObserver<Void> to = ps.switchMapCompletableDelayError(Functions.justFunction(cs)).test(); + + ps.onNext(1); + ps.onComplete(); + + to.assertEmpty(); + + cs.onError(new TestException()); + + to.assertFailure(TestException.class); + } + + @Test + public void mainErrorDelayed() { + final PublishSubject<Integer> ps = PublishSubject.create(); + final CompletableSubject cs = CompletableSubject.create(); + + TestObserver<Void> to = ps.switchMapCompletableDelayError(Functions.justFunction(cs)).test(); + + ps.onNext(1); + + ps.onError(new TestException()); + + to.assertEmpty(); + + assertTrue(cs.hasObservers()); + + cs.onComplete(); + + to.assertFailure(TestException.class); + } + + @Test + public void scalarMapperCrash() { + TestObserver<Void> to = Observable.just(1) + .switchMapCompletable(new Function<Integer, CompletableSource>() { + @Override + public CompletableSource apply(Integer v) + throws Exception { + throw new TestException(); + } + }) + .test(); + + to.assertFailure(TestException.class); + } + + @Test + public void scalarEmptySource() { + CompletableSubject cs = CompletableSubject.create(); + + Observable.empty() + .switchMapCompletable(Functions.justFunction(cs)) + .test() + .assertResult(); + + assertFalse(cs.hasObservers()); + } + + @Test + public void scalarSource() { + CompletableSubject cs = CompletableSubject.create(); + + TestObserver<Void> to = Observable.just(1) + .switchMapCompletable(Functions.justFunction(cs)) + .test(); + + assertTrue(cs.hasObservers()); + + to.assertEmpty(); + + cs.onComplete(); + + to.assertResult(); + } + + @Test + public void undeliverableUponCancel() { + TestHelper.checkUndeliverableUponCancel(new ObservableConverter<Integer, Completable>() { + @Override + public Completable apply(Observable<Integer> upstream) { + return upstream.switchMapCompletable(new Function<Integer, Completable>() { + @Override + public Completable apply(Integer v) throws Throwable { + return Completable.complete().hide(); + } + }); + } + }); + } + + @Test + public void undeliverableUponCancelDelayError() { + TestHelper.checkUndeliverableUponCancel(new ObservableConverter<Integer, Completable>() { + @Override + public Completable apply(Observable<Integer> upstream) { + return upstream.switchMapCompletableDelayError(new Function<Integer, Completable>() { + @Override + public Completable apply(Integer v) throws Throwable { + return Completable.complete().hide(); + } + }); + } + }); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/mixed/ObservableSwitchMapMaybeTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/mixed/ObservableSwitchMapMaybeTest.java new file mode 100644 index 0000000000..b2eabdd96f --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/mixed/ObservableSwitchMapMaybeTest.java @@ -0,0 +1,719 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.mixed; + +import static org.junit.Assert.*; + +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; + +import io.reactivex.rxjava3.disposables.Disposable; +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.subjects.*; +import io.reactivex.rxjava3.testsupport.*; + +public class ObservableSwitchMapMaybeTest extends RxJavaTest { + + @Test + public void simple() { + Observable.range(1, 5) + .switchMapMaybe(new Function<Integer, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Integer v) + throws Exception { + return Maybe.just(v); + } + }) + .test() + .assertResult(1, 2, 3, 4, 5); + } + + @Test + public void simpleEmpty() { + Observable.range(1, 5) + .switchMapMaybe(new Function<Integer, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Integer v) + throws Exception { + return Maybe.empty(); + } + }) + .test() + .assertResult(); + } + + @Test + public void simpleMixed() { + Observable.range(1, 10) + .switchMapMaybe(new Function<Integer, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Integer v) + throws Exception { + if (v % 2 == 0) { + return Maybe.just(v); + } + return Maybe.empty(); + } + }) + .test() + .assertResult(2, 4, 6, 8, 10); + } + + @Test + public void mainError() { + Observable.error(new TestException()) + .switchMapMaybe(Functions.justFunction(Maybe.never())) + .test() + .assertFailure(TestException.class); + } + + @Test + public void innerError() { + Observable.just(1) + .switchMapMaybe(Functions.justFunction(Maybe.error(new TestException()))) + .test() + .assertFailure(TestException.class); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeObservable(new Function<Observable<Object>, Observable<Object>>() { + @Override + public Observable<Object> apply(Observable<Object> f) + throws Exception { + return f + .switchMapMaybe(Functions.justFunction(Maybe.never())); + } + } + ); + } + + @Test + public void take() { + Observable.range(1, 5) + .switchMapMaybe(new Function<Integer, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Integer v) + throws Exception { + return Maybe.just(v); + } + }) + .take(3) + .test() + .assertResult(1, 2, 3); + } + + @Test + public void switchOver() { + PublishSubject<Integer> ps = PublishSubject.create(); + + final MaybeSubject<Integer> ms1 = MaybeSubject.create(); + final MaybeSubject<Integer> ms2 = MaybeSubject.create(); + + TestObserver<Integer> to = ps.switchMapMaybe(new Function<Integer, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Integer v) + throws Exception { + if (v == 1) { + return ms1; + } + return ms2; + } + }).test(); + + to.assertEmpty(); + + ps.onNext(1); + + to.assertEmpty(); + + assertTrue(ms1.hasObservers()); + + ps.onNext(2); + + assertFalse(ms1.hasObservers()); + assertTrue(ms2.hasObservers()); + + ms2.onError(new TestException()); + + assertFalse(ps.hasObservers()); + + to.assertFailure(TestException.class); + } + + @Test + public void switchOverDelayError() { + PublishSubject<Integer> ps = PublishSubject.create(); + + final MaybeSubject<Integer> ms1 = MaybeSubject.create(); + final MaybeSubject<Integer> ms2 = MaybeSubject.create(); + + TestObserver<Integer> to = ps.switchMapMaybeDelayError(new Function<Integer, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Integer v) + throws Exception { + if (v == 1) { + return ms1; + } + return ms2; + } + }).test(); + + to.assertEmpty(); + + ps.onNext(1); + + to.assertEmpty(); + + assertTrue(ms1.hasObservers()); + + ps.onNext(2); + + assertFalse(ms1.hasObservers()); + assertTrue(ms2.hasObservers()); + + ms2.onError(new TestException()); + + to.assertEmpty(); + + assertTrue(ps.hasObservers()); + + ps.onComplete(); + + to.assertFailure(TestException.class); + } + + @Test + public void mainErrorInnerCompleteDelayError() { + PublishSubject<Integer> ps = PublishSubject.create(); + + final MaybeSubject<Integer> ms = MaybeSubject.create(); + + TestObserver<Integer> to = ps.switchMapMaybeDelayError(new Function<Integer, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Integer v) + throws Exception { + return ms; + } + }).test(); + + to.assertEmpty(); + + ps.onNext(1); + + to.assertEmpty(); + + assertTrue(ms.hasObservers()); + + ps.onError(new TestException()); + + assertTrue(ms.hasObservers()); + + to.assertEmpty(); + + ms.onComplete(); + + to.assertFailure(TestException.class); + } + + @Test + public void mainErrorInnerSuccessDelayError() { + PublishSubject<Integer> ps = PublishSubject.create(); + + final MaybeSubject<Integer> ms = MaybeSubject.create(); + + TestObserver<Integer> to = ps.switchMapMaybeDelayError(new Function<Integer, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Integer v) + throws Exception { + return ms; + } + }).test(); + + to.assertEmpty(); + + ps.onNext(1); + + to.assertEmpty(); + + assertTrue(ms.hasObservers()); + + ps.onError(new TestException()); + + assertTrue(ms.hasObservers()); + + to.assertEmpty(); + + ms.onSuccess(1); + + to.assertFailure(TestException.class, 1); + } + + @Test + public void mapperCrash() { + Observable.just(1).hide() + .switchMapMaybe(new Function<Integer, MaybeSource<? extends Object>>() { + @Override + public MaybeSource<? extends Object> apply(Integer v) + throws Exception { + throw new TestException(); + } + }) + .test() + .assertFailure(TestException.class); + } + + @Test + public void disposeBeforeSwitchInOnNext() { + final TestObserver<Integer> to = new TestObserver<>(); + + Observable.just(1) + .switchMapMaybe(new Function<Integer, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Integer v) + throws Exception { + to.dispose(); + return Maybe.just(1); + } + }).subscribe(to); + + to.assertEmpty(); + } + + @Test + public void disposeOnNextAfterFirst() { + final TestObserver<Integer> to = new TestObserver<>(); + + Observable.just(1, 2) + .switchMapMaybe(new Function<Integer, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Integer v) + throws Exception { + if (v == 2) { + to.dispose(); + } + return Maybe.just(1); + } + }).subscribe(to); + + to.assertValue(1) + .assertNoErrors() + .assertNotComplete(); + } + + @Test + public void cancel() { + PublishSubject<Integer> ps = PublishSubject.create(); + + final MaybeSubject<Integer> ms = MaybeSubject.create(); + + TestObserver<Integer> to = ps.switchMapMaybeDelayError(new Function<Integer, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Integer v) + throws Exception { + return ms; + } + }).test(); + + to.assertEmpty(); + + ps.onNext(1); + + to.assertEmpty(); + + assertTrue(ps.hasObservers()); + assertTrue(ms.hasObservers()); + + to.dispose(); + + assertFalse(ps.hasObservers()); + assertFalse(ms.hasObservers()); + } + + @Test + public void mainErrorAfterTermination() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + new Observable<Integer>() { + @Override + protected void subscribeActual(Observer<? super Integer> observer) { + observer.onSubscribe(Disposable.empty()); + observer.onNext(1); + observer.onError(new TestException("outer")); + } + } + .switchMapMaybe(new Function<Integer, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Integer v) + throws Exception { + return Maybe.error(new TestException("inner")); + } + }) + .to(TestHelper.<Integer>testConsumer()) + .assertFailureAndMessage(TestException.class, "inner"); + + TestHelper.assertUndeliverable(errors, 0, TestException.class, "outer"); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void innerErrorAfterTermination() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + final AtomicReference<MaybeObserver<? super Integer>> moRef = new AtomicReference<>(); + + TestObserverEx<Integer> to = new Observable<Integer>() { + @Override + protected void subscribeActual(Observer<? super Integer> observer) { + observer.onSubscribe(Disposable.empty()); + observer.onNext(1); + observer.onError(new TestException("outer")); + } + } + .switchMapMaybe(new Function<Integer, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Integer v) + throws Exception { + return new Maybe<Integer>() { + @Override + protected void subscribeActual( + MaybeObserver<? super Integer> observer) { + observer.onSubscribe(Disposable.empty()); + moRef.set(observer); + } + }; + } + }) + .to(TestHelper.<Integer>testConsumer()); + + to.assertFailureAndMessage(TestException.class, "outer"); + + moRef.get().onError(new TestException("inner")); + moRef.get().onComplete(); + + TestHelper.assertUndeliverable(errors, 0, TestException.class, "inner"); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void nextCancelRace() { + for (int i = 0; i < TestHelper.RACE_LONG_LOOPS; i++) { + + final PublishSubject<Integer> ps = PublishSubject.create(); + + final MaybeSubject<Integer> ms = MaybeSubject.create(); + + final TestObserver<Integer> to = ps.switchMapMaybeDelayError(new Function<Integer, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Integer v) + throws Exception { + return ms; + } + }).test(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + ps.onNext(1); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + to.dispose(); + } + }; + + TestHelper.race(r1, r2); + + to.assertNoErrors() + .assertNotComplete(); + } + } + + @Test + public void nextInnerErrorRace() { + final TestException ex = new TestException(); + + for (int i = 0; i < TestHelper.RACE_LONG_LOOPS; i++) { + + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + final PublishSubject<Integer> ps = PublishSubject.create(); + + final MaybeSubject<Integer> ms = MaybeSubject.create(); + + final TestObserverEx<Integer> to = ps.switchMapMaybeDelayError(new Function<Integer, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Integer v) + throws Exception { + if (v == 1) { + return ms; + } + return Maybe.never(); + } + }).to(TestHelper.<Integer>testConsumer()); + + ps.onNext(1); + + Runnable r1 = new Runnable() { + @Override + public void run() { + ps.onNext(2); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + ms.onError(ex); + } + }; + + TestHelper.race(r1, r2); + + if (to.errors().size() != 0) { + assertTrue(errors.isEmpty()); + to.assertFailure(TestException.class); + } else if (!errors.isEmpty()) { + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } + } finally { + RxJavaPlugins.reset(); + } + } + } + + @Test + public void mainErrorInnerErrorRace() { + final TestException ex = new TestException(); + final TestException ex2 = new TestException(); + + for (int i = 0; i < TestHelper.RACE_LONG_LOOPS; i++) { + + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + final PublishSubject<Integer> ps = PublishSubject.create(); + + final MaybeSubject<Integer> ms = MaybeSubject.create(); + + final TestObserver<Integer> to = ps.switchMapMaybeDelayError(new Function<Integer, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Integer v) + throws Exception { + if (v == 1) { + return ms; + } + return Maybe.never(); + } + }).test(); + + ps.onNext(1); + + Runnable r1 = new Runnable() { + @Override + public void run() { + ps.onError(ex); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + ms.onError(ex2); + } + }; + + TestHelper.race(r1, r2); + + to.assertError(new Predicate<Throwable>() { + @Override + public boolean test(Throwable e) throws Exception { + return e instanceof TestException || e instanceof CompositeException; + } + }); + + if (!errors.isEmpty()) { + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } + } finally { + RxJavaPlugins.reset(); + } + } + } + + @Test + public void nextInnerSuccessRace() { + for (int i = 0; i < TestHelper.RACE_LONG_LOOPS; i++) { + + final PublishSubject<Integer> ps = PublishSubject.create(); + + final MaybeSubject<Integer> ms = MaybeSubject.create(); + + final TestObserver<Integer> to = ps.switchMapMaybeDelayError(new Function<Integer, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Integer v) + throws Exception { + if (v == 1) { + return ms; + } + return Maybe.empty(); + } + }).test(); + + ps.onNext(1); + + Runnable r1 = new Runnable() { + @Override + public void run() { + ps.onNext(2); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + ms.onSuccess(3); + } + }; + + TestHelper.race(r1, r2); + + to.assertNoErrors() + .assertNotComplete(); + } + } + + @Test + public void checkDisposed() { + PublishSubject<Integer> ps = PublishSubject.create(); + MaybeSubject<Integer> ms = MaybeSubject.create(); + + TestHelper.checkDisposed(ps.switchMapMaybe(Functions.justFunction(ms))); + } + + @Test + public void drainReentrant() { + final PublishSubject<Integer> ps = PublishSubject.create(); + + TestObserver<Integer> to = new TestObserver<Integer>() { + @Override + public void onNext(Integer t) { + super.onNext(t); + if (t == 1) { + ps.onNext(2); + } + } + }; + + ps.switchMapMaybe(new Function<Integer, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Integer v) + throws Exception { + return Maybe.just(v); + } + }).subscribe(to); + + ps.onNext(1); + ps.onComplete(); + + to.assertResult(1, 2); + } + + @Test + public void scalarMapperCrash() { + TestObserver<Integer> to = Observable.just(1) + .switchMapMaybe(new Function<Integer, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Integer v) + throws Exception { + throw new TestException(); + } + }) + .test(); + + to.assertFailure(TestException.class); + } + + @Test + public void scalarEmptySource() { + MaybeSubject<Integer> ms = MaybeSubject.create(); + + Observable.empty() + .switchMapMaybe(Functions.justFunction(ms)) + .test() + .assertResult(); + + assertFalse(ms.hasObservers()); + } + + @Test + public void scalarSource() { + MaybeSubject<Integer> ms = MaybeSubject.create(); + + TestObserver<Integer> to = Observable.just(1) + .switchMapMaybe(Functions.justFunction(ms)) + .test(); + + assertTrue(ms.hasObservers()); + + to.assertEmpty(); + + ms.onSuccess(2); + + to.assertResult(2); + } + + @Test + public void undeliverableUponCancel() { + TestHelper.checkUndeliverableUponCancel(new ObservableConverter<Integer, Observable<Integer>>() { + @Override + public Observable<Integer> apply(Observable<Integer> upstream) { + return upstream.switchMapMaybe(new Function<Integer, Maybe<Integer>>() { + @Override + public Maybe<Integer> apply(Integer v) throws Throwable { + return Maybe.just(v).hide(); + } + }); + } + }); + } + + @Test + public void undeliverableUponCancelDelayError() { + TestHelper.checkUndeliverableUponCancel(new ObservableConverter<Integer, Observable<Integer>>() { + @Override + public Observable<Integer> apply(Observable<Integer> upstream) { + return upstream.switchMapMaybeDelayError(new Function<Integer, Maybe<Integer>>() { + @Override + public Maybe<Integer> apply(Integer v) throws Throwable { + return Maybe.just(v).hide(); + } + }); + } + }); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/mixed/ObservableSwitchMapSingleTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/mixed/ObservableSwitchMapSingleTest.java new file mode 100644 index 0000000000..a98afae4de --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/mixed/ObservableSwitchMapSingleTest.java @@ -0,0 +1,687 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.mixed; + +import static org.junit.Assert.*; + +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; + +import io.reactivex.rxjava3.disposables.Disposable; +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.subjects.*; +import io.reactivex.rxjava3.testsupport.*; + +public class ObservableSwitchMapSingleTest extends RxJavaTest { + + @Test + public void simple() { + Observable.range(1, 5) + .switchMapSingle(new Function<Integer, SingleSource<Integer>>() { + @Override + public SingleSource<Integer> apply(Integer v) + throws Exception { + return Single.just(v); + } + }) + .test() + .assertResult(1, 2, 3, 4, 5); + } + + @Test + public void mainError() { + Observable.error(new TestException()) + .switchMapSingle(Functions.justFunction(Single.never())) + .test() + .assertFailure(TestException.class); + } + + @Test + public void innerError() { + Observable.just(1) + .switchMapSingle(Functions.justFunction(Single.error(new TestException()))) + .test() + .assertFailure(TestException.class); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeObservable(new Function<Observable<Object>, Observable<Object>>() { + @Override + public Observable<Object> apply(Observable<Object> f) + throws Exception { + return f + .switchMapSingle(Functions.justFunction(Single.never())); + } + } + ); + } + + @Test + public void take() { + Observable.range(1, 5) + .switchMapSingle(new Function<Integer, SingleSource<Integer>>() { + @Override + public SingleSource<Integer> apply(Integer v) + throws Exception { + return Single.just(v); + } + }) + .take(3) + .test() + .assertResult(1, 2, 3); + } + + @Test + public void switchOver() { + PublishSubject<Integer> ps = PublishSubject.create(); + + final SingleSubject<Integer> ms1 = SingleSubject.create(); + final SingleSubject<Integer> ms2 = SingleSubject.create(); + + TestObserver<Integer> to = ps.switchMapSingle(new Function<Integer, SingleSource<Integer>>() { + @Override + public SingleSource<Integer> apply(Integer v) + throws Exception { + if (v == 1) { + return ms1; + } + return ms2; + } + }).test(); + + to.assertEmpty(); + + ps.onNext(1); + + to.assertEmpty(); + + assertTrue(ms1.hasObservers()); + + ps.onNext(2); + + assertFalse(ms1.hasObservers()); + assertTrue(ms2.hasObservers()); + + ms2.onError(new TestException()); + + assertFalse(ps.hasObservers()); + + to.assertFailure(TestException.class); + } + + @Test + public void switchOverDelayError() { + PublishSubject<Integer> ps = PublishSubject.create(); + + final SingleSubject<Integer> ms1 = SingleSubject.create(); + final SingleSubject<Integer> ms2 = SingleSubject.create(); + + TestObserver<Integer> to = ps.switchMapSingleDelayError(new Function<Integer, SingleSource<Integer>>() { + @Override + public SingleSource<Integer> apply(Integer v) + throws Exception { + if (v == 1) { + return ms1; + } + return ms2; + } + }).test(); + + to.assertEmpty(); + + ps.onNext(1); + + to.assertEmpty(); + + assertTrue(ms1.hasObservers()); + + ps.onNext(2); + + assertFalse(ms1.hasObservers()); + assertTrue(ms2.hasObservers()); + + ms2.onError(new TestException()); + + to.assertEmpty(); + + assertTrue(ps.hasObservers()); + + ps.onComplete(); + + to.assertFailure(TestException.class); + } + + @Test + public void mainErrorInnerCompleteDelayError() { + PublishSubject<Integer> ps = PublishSubject.create(); + + final SingleSubject<Integer> ms = SingleSubject.create(); + + TestObserver<Integer> to = ps.switchMapSingleDelayError(new Function<Integer, SingleSource<Integer>>() { + @Override + public SingleSource<Integer> apply(Integer v) + throws Exception { + return ms; + } + }).test(); + + to.assertEmpty(); + + ps.onNext(1); + + to.assertEmpty(); + + assertTrue(ms.hasObservers()); + + ps.onError(new TestException()); + + assertTrue(ms.hasObservers()); + + to.assertEmpty(); + + ms.onSuccess(1); + + to.assertFailure(TestException.class, 1); + } + + @Test + public void mainErrorInnerSuccessDelayError() { + PublishSubject<Integer> ps = PublishSubject.create(); + + final SingleSubject<Integer> ms = SingleSubject.create(); + + TestObserver<Integer> to = ps.switchMapSingleDelayError(new Function<Integer, SingleSource<Integer>>() { + @Override + public SingleSource<Integer> apply(Integer v) + throws Exception { + return ms; + } + }).test(); + + to.assertEmpty(); + + ps.onNext(1); + + to.assertEmpty(); + + assertTrue(ms.hasObservers()); + + ps.onError(new TestException()); + + assertTrue(ms.hasObservers()); + + to.assertEmpty(); + + ms.onSuccess(1); + + to.assertFailure(TestException.class, 1); + } + + @Test + public void mapperCrash() { + Observable.just(1).hide() + .switchMapSingle(new Function<Integer, SingleSource<? extends Object>>() { + @Override + public SingleSource<? extends Object> apply(Integer v) + throws Exception { + throw new TestException(); + } + }) + .test() + .assertFailure(TestException.class); + } + + @Test + public void disposeBeforeSwitchInOnNext() { + final TestObserver<Integer> to = new TestObserver<>(); + + Observable.just(1).hide() + .switchMapSingle(new Function<Integer, SingleSource<Integer>>() { + @Override + public SingleSource<Integer> apply(Integer v) + throws Exception { + to.dispose(); + return Single.just(1); + } + }).subscribe(to); + + to.assertEmpty(); + } + + @Test + public void disposeOnNextAfterFirst() { + final TestObserver<Integer> to = new TestObserver<>(); + + Observable.just(1, 2) + .switchMapSingle(new Function<Integer, SingleSource<Integer>>() { + @Override + public SingleSource<Integer> apply(Integer v) + throws Exception { + if (v == 2) { + to.dispose(); + } + return Single.just(1); + } + }).subscribe(to); + + to.assertValue(1) + .assertNoErrors() + .assertNotComplete(); + } + + @Test + public void cancel() { + PublishSubject<Integer> ps = PublishSubject.create(); + + final SingleSubject<Integer> ms = SingleSubject.create(); + + TestObserver<Integer> to = ps.switchMapSingleDelayError(new Function<Integer, SingleSource<Integer>>() { + @Override + public SingleSource<Integer> apply(Integer v) + throws Exception { + return ms; + } + }).test(); + + to.assertEmpty(); + + ps.onNext(1); + + to.assertEmpty(); + + assertTrue(ps.hasObservers()); + assertTrue(ms.hasObservers()); + + to.dispose(); + + assertFalse(ps.hasObservers()); + assertFalse(ms.hasObservers()); + } + + @Test + public void mainErrorAfterTermination() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + new Observable<Integer>() { + @Override + protected void subscribeActual(Observer<? super Integer> observer) { + observer.onSubscribe(Disposable.empty()); + observer.onNext(1); + observer.onError(new TestException("outer")); + } + } + .switchMapSingle(new Function<Integer, SingleSource<Integer>>() { + @Override + public SingleSource<Integer> apply(Integer v) + throws Exception { + return Single.error(new TestException("inner")); + } + }) + .to(TestHelper.<Integer>testConsumer()) + .assertFailureAndMessage(TestException.class, "inner"); + + TestHelper.assertUndeliverable(errors, 0, TestException.class, "outer"); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void innerErrorAfterTermination() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + final AtomicReference<SingleObserver<? super Integer>> moRef = new AtomicReference<>(); + + TestObserverEx<Integer> to = new Observable<Integer>() { + @Override + protected void subscribeActual(Observer<? super Integer> observer) { + observer.onSubscribe(Disposable.empty()); + observer.onNext(1); + observer.onError(new TestException("outer")); + } + } + .switchMapSingle(new Function<Integer, SingleSource<Integer>>() { + @Override + public SingleSource<Integer> apply(Integer v) + throws Exception { + return new Single<Integer>() { + @Override + protected void subscribeActual( + SingleObserver<? super Integer> observer) { + observer.onSubscribe(Disposable.empty()); + moRef.set(observer); + } + }; + } + }) + .to(TestHelper.<Integer>testConsumer()); + + to.assertFailureAndMessage(TestException.class, "outer"); + + moRef.get().onError(new TestException("inner")); + + TestHelper.assertUndeliverable(errors, 0, TestException.class, "inner"); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void nextCancelRace() { + for (int i = 0; i < TestHelper.RACE_LONG_LOOPS; i++) { + + final PublishSubject<Integer> ps = PublishSubject.create(); + + final SingleSubject<Integer> ms = SingleSubject.create(); + + final TestObserver<Integer> to = ps.switchMapSingleDelayError(new Function<Integer, SingleSource<Integer>>() { + @Override + public SingleSource<Integer> apply(Integer v) + throws Exception { + return ms; + } + }).test(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + ps.onNext(1); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + to.dispose(); + } + }; + + TestHelper.race(r1, r2); + + to.assertNoErrors() + .assertNotComplete(); + } + } + + @Test + public void nextInnerErrorRace() { + final TestException ex = new TestException(); + + for (int i = 0; i < TestHelper.RACE_LONG_LOOPS; i++) { + + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + final PublishSubject<Integer> ps = PublishSubject.create(); + + final SingleSubject<Integer> ms = SingleSubject.create(); + + final TestObserverEx<Integer> to = ps.switchMapSingleDelayError(new Function<Integer, SingleSource<Integer>>() { + @Override + public SingleSource<Integer> apply(Integer v) + throws Exception { + if (v == 1) { + return ms; + } + return Single.never(); + } + }).to(TestHelper.<Integer>testConsumer()); + + ps.onNext(1); + + Runnable r1 = new Runnable() { + @Override + public void run() { + ps.onNext(2); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + ms.onError(ex); + } + }; + + TestHelper.race(r1, r2); + + if (to.errors().size() != 0) { + assertTrue(errors.isEmpty()); + to.assertFailure(TestException.class); + } else if (!errors.isEmpty()) { + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } + } finally { + RxJavaPlugins.reset(); + } + } + } + + @Test + public void mainErrorInnerErrorRace() { + final TestException ex = new TestException(); + final TestException ex2 = new TestException(); + + for (int i = 0; i < TestHelper.RACE_LONG_LOOPS; i++) { + + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + final PublishSubject<Integer> ps = PublishSubject.create(); + + final SingleSubject<Integer> ms = SingleSubject.create(); + + final TestObserver<Integer> to = ps.switchMapSingleDelayError(new Function<Integer, SingleSource<Integer>>() { + @Override + public SingleSource<Integer> apply(Integer v) + throws Exception { + if (v == 1) { + return ms; + } + return Single.never(); + } + }).test(); + + ps.onNext(1); + + Runnable r1 = new Runnable() { + @Override + public void run() { + ps.onError(ex); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + ms.onError(ex2); + } + }; + + TestHelper.race(r1, r2); + + to.assertError(new Predicate<Throwable>() { + @Override + public boolean test(Throwable e) throws Exception { + return e instanceof TestException || e instanceof CompositeException; + } + }); + + if (!errors.isEmpty()) { + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } + } finally { + RxJavaPlugins.reset(); + } + } + } + + @Test + public void nextInnerSuccessRace() { + for (int i = 0; i < TestHelper.RACE_LONG_LOOPS; i++) { + + final PublishSubject<Integer> ps = PublishSubject.create(); + + final SingleSubject<Integer> ms = SingleSubject.create(); + + final TestObserver<Integer> to = ps.switchMapSingleDelayError(new Function<Integer, SingleSource<Integer>>() { + @Override + public SingleSource<Integer> apply(Integer v) + throws Exception { + if (v == 1) { + return ms; + } + return Single.never(); + } + }).test(); + + ps.onNext(1); + + Runnable r1 = new Runnable() { + @Override + public void run() { + ps.onNext(2); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + ms.onSuccess(3); + } + }; + + TestHelper.race(r1, r2); + + to.assertNoErrors() + .assertNotComplete(); + } + } + + @Test + public void checkDisposed() { + PublishSubject<Integer> ps = PublishSubject.create(); + SingleSubject<Integer> ms = SingleSubject.create(); + + TestHelper.checkDisposed(ps.switchMapSingle(Functions.justFunction(ms))); + } + + @Test + public void drainReentrant() { + final PublishSubject<Integer> ps = PublishSubject.create(); + + TestObserver<Integer> to = new TestObserver<Integer>() { + @Override + public void onNext(Integer t) { + super.onNext(t); + if (t == 1) { + ps.onNext(2); + } + } + }; + + ps.switchMapSingle(new Function<Integer, SingleSource<Integer>>() { + @Override + public SingleSource<Integer> apply(Integer v) + throws Exception { + return Single.just(v); + } + }).subscribe(to); + + ps.onNext(1); + ps.onComplete(); + + to.assertResult(1, 2); + } + + @Test + public void scalarMapperCrash() { + TestObserver<Integer> to = Observable.just(1) + .switchMapSingle(new Function<Integer, SingleSource<Integer>>() { + @Override + public SingleSource<Integer> apply(Integer v) + throws Exception { + throw new TestException(); + } + }) + .test(); + + to.assertFailure(TestException.class); + } + + @Test + public void scalarEmptySource() { + SingleSubject<Integer> ss = SingleSubject.create(); + + Observable.empty() + .switchMapSingle(Functions.justFunction(ss)) + .test() + .assertResult(); + + assertFalse(ss.hasObservers()); + } + + @Test + public void scalarSource() { + SingleSubject<Integer> ss = SingleSubject.create(); + + TestObserver<Integer> to = Observable.just(1) + .switchMapSingle(Functions.justFunction(ss)) + .test(); + + assertTrue(ss.hasObservers()); + + to.assertEmpty(); + + ss.onSuccess(2); + + to.assertResult(2); + } + + @Test + public void undeliverableUponCancel() { + TestHelper.checkUndeliverableUponCancel(new ObservableConverter<Integer, Observable<Integer>>() { + @Override + public Observable<Integer> apply(Observable<Integer> upstream) { + return upstream.switchMapSingle(new Function<Integer, Single<Integer>>() { + @Override + public Single<Integer> apply(Integer v) throws Throwable { + return Single.just(v).hide(); + } + }); + } + }); + } + + @Test + public void undeliverableUponCancelDelayError() { + TestHelper.checkUndeliverableUponCancel(new ObservableConverter<Integer, Observable<Integer>>() { + @Override + public Observable<Integer> apply(Observable<Integer> upstream) { + return upstream.switchMapSingleDelayError(new Function<Integer, Single<Integer>>() { + @Override + public Single<Integer> apply(Integer v) throws Throwable { + return Single.just(v).hide(); + } + }); + } + }); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/mixed/ScalarXMapZHelperTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/mixed/ScalarXMapZHelperTest.java new file mode 100644 index 0000000000..2ac6eed7d3 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/mixed/ScalarXMapZHelperTest.java @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.mixed; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.RxJavaTest; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class ScalarXMapZHelperTest extends RxJavaTest { + + @Test + public void utilityClass() { + TestHelper.checkUtilityClass(ScalarXMapZHelper.class); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/mixed/SingleFlatMapObservableTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/mixed/SingleFlatMapObservableTest.java new file mode 100644 index 0000000000..a002620b6a --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/mixed/SingleFlatMapObservableTest.java @@ -0,0 +1,128 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.mixed; + +import static org.junit.Assert.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.subjects.*; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class SingleFlatMapObservableTest extends RxJavaTest { + + @Test + public void cancelMain() { + SingleSubject<Integer> ss = SingleSubject.create(); + PublishSubject<Integer> ps = PublishSubject.create(); + + TestObserver<Integer> to = ss.flatMapObservable(Functions.justFunction(ps)) + .test(); + + assertTrue(ss.hasObservers()); + assertFalse(ps.hasObservers()); + + to.dispose(); + + assertFalse(ss.hasObservers()); + assertFalse(ps.hasObservers()); + } + + @Test + public void cancelOther() { + SingleSubject<Integer> ss = SingleSubject.create(); + PublishSubject<Integer> ps = PublishSubject.create(); + + TestObserver<Integer> to = ss.flatMapObservable(Functions.justFunction(ps)) + .test(); + + assertTrue(ss.hasObservers()); + assertFalse(ps.hasObservers()); + + ss.onSuccess(1); + + assertFalse(ss.hasObservers()); + assertTrue(ps.hasObservers()); + + to.dispose(); + + assertFalse(ss.hasObservers()); + assertFalse(ps.hasObservers()); + } + + @Test + public void errorMain() { + SingleSubject<Integer> ss = SingleSubject.create(); + PublishSubject<Integer> ps = PublishSubject.create(); + + TestObserver<Integer> to = ss.flatMapObservable(Functions.justFunction(ps)) + .test(); + + assertTrue(ss.hasObservers()); + assertFalse(ps.hasObservers()); + + ss.onError(new TestException()); + + assertFalse(ss.hasObservers()); + assertFalse(ps.hasObservers()); + + to.assertFailure(TestException.class); + } + + @Test + public void errorOther() { + SingleSubject<Integer> ss = SingleSubject.create(); + PublishSubject<Integer> ps = PublishSubject.create(); + + TestObserver<Integer> to = ss.flatMapObservable(Functions.justFunction(ps)) + .test(); + + assertTrue(ss.hasObservers()); + assertFalse(ps.hasObservers()); + + ss.onSuccess(1); + + assertFalse(ss.hasObservers()); + assertTrue(ps.hasObservers()); + + ps.onError(new TestException()); + + assertFalse(ss.hasObservers()); + assertFalse(ps.hasObservers()); + + to.assertFailure(TestException.class); + } + + @Test + public void mapperCrash() { + Single.just(1).flatMapObservable(new Function<Integer, ObservableSource<? extends Object>>() { + @Override + public ObservableSource<? extends Object> apply(Integer v) throws Exception { + throw new TestException(); + } + }) + .test() + .assertFailure(TestException.class); + } + + @Test + public void isDisposed() { + TestHelper.checkDisposed(Single.never().flatMapObservable(Functions.justFunction(Observable.never()))); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/observable/AbstractObservableWithUpstreamTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/AbstractObservableWithUpstreamTest.java new file mode 100644 index 0000000000..5d49cd8b66 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/AbstractObservableWithUpstreamTest.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import static org.junit.Assert.assertSame; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.internal.fuseable.HasUpstreamObservableSource; + +public class AbstractObservableWithUpstreamTest extends RxJavaTest { + + @SuppressWarnings("unchecked") + @Test + public void source() { + Observable<Integer> o = Observable.just(1); + + assertSame(o, ((HasUpstreamObservableSource<Integer>)o.map(Functions.<Integer>identity())).source()); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/observable/BlockingObservableLatestTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/BlockingObservableLatestTest.java new file mode 100644 index 0000000000..feac576be7 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/BlockingObservableLatestTest.java @@ -0,0 +1,230 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import static org.junit.Assert.*; + +import java.util.*; +import java.util.concurrent.TimeUnit; + +import org.junit.*; + +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.core.Observer; +import io.reactivex.rxjava3.core.RxJavaTest; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.schedulers.TestScheduler; +import io.reactivex.rxjava3.subjects.PublishSubject; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class BlockingObservableLatestTest extends RxJavaTest { + @Test + public void simple() { + TestScheduler scheduler = new TestScheduler(); + + Observable<Long> source = Observable.interval(1, TimeUnit.SECONDS, scheduler).take(10); + + Iterable<Long> iter = source.blockingLatest(); + + Iterator<Long> it = iter.iterator(); + + // only 9 because take(10) will immediately call onComplete when receiving the 10th item + // which onComplete will overwrite the previous value + for (int i = 0; i < 9; i++) { + scheduler.advanceTimeBy(1, TimeUnit.SECONDS); + + Assert.assertTrue(it.hasNext()); + + Assert.assertEquals(Long.valueOf(i), it.next()); + } + + scheduler.advanceTimeBy(1, TimeUnit.SECONDS); + Assert.assertFalse(it.hasNext()); + } + + @Test + public void sameSourceMultipleIterators() { + TestScheduler scheduler = new TestScheduler(); + + Observable<Long> source = Observable.interval(1, TimeUnit.SECONDS, scheduler).take(10); + + Iterable<Long> iter = source.blockingLatest(); + + for (int j = 0; j < 3; j++) { + Iterator<Long> it = iter.iterator(); + + // only 9 because take(10) will immediately call onComplete when receiving the 10th item + // which onComplete will overwrite the previous value + for (int i = 0; i < 9; i++) { + scheduler.advanceTimeBy(1, TimeUnit.SECONDS); + + Assert.assertTrue(it.hasNext()); + + Assert.assertEquals(Long.valueOf(i), it.next()); + } + + scheduler.advanceTimeBy(1, TimeUnit.SECONDS); + Assert.assertFalse(it.hasNext()); + } + } + + @Test(expected = NoSuchElementException.class) + public void empty() { + Observable<Long> source = Observable.<Long> empty(); + + Iterable<Long> iter = source.blockingLatest(); + + Iterator<Long> it = iter.iterator(); + + Assert.assertFalse(it.hasNext()); + + it.next(); + } + + @Test(expected = NoSuchElementException.class) + public void simpleJustNext() { + TestScheduler scheduler = new TestScheduler(); + + Observable<Long> source = Observable.interval(1, TimeUnit.SECONDS, scheduler).take(10); + + Iterable<Long> iter = source.blockingLatest(); + + Iterator<Long> it = iter.iterator(); + + // only 9 because take(10) will immediately call onComplete when receiving the 10th item + // which onComplete will overwrite the previous value + for (int i = 0; i < 10; i++) { + scheduler.advanceTimeBy(1, TimeUnit.SECONDS); + + Assert.assertEquals(Long.valueOf(i), it.next()); + } + } + + @Test(expected = RuntimeException.class) + public void hasNextThrows() { + TestScheduler scheduler = new TestScheduler(); + + Observable<Long> source = Observable.<Long> error(new RuntimeException("Forced failure!")).subscribeOn(scheduler); + + Iterable<Long> iter = source.blockingLatest(); + + Iterator<Long> it = iter.iterator(); + + scheduler.advanceTimeBy(1, TimeUnit.SECONDS); + + it.hasNext(); + } + + @Test(expected = RuntimeException.class) + public void nextThrows() { + TestScheduler scheduler = new TestScheduler(); + + Observable<Long> source = Observable.<Long> error(new RuntimeException("Forced failure!")).subscribeOn(scheduler); + + Iterable<Long> iter = source.blockingLatest(); + Iterator<Long> it = iter.iterator(); + + scheduler.advanceTimeBy(1, TimeUnit.SECONDS); + + it.next(); + } + + @Test + public void fasterSource() { + PublishSubject<Integer> source = PublishSubject.create(); + Observable<Integer> blocker = source; + + Iterable<Integer> iter = blocker.blockingLatest(); + Iterator<Integer> it = iter.iterator(); + + source.onNext(1); + + Assert.assertEquals(Integer.valueOf(1), it.next()); + + source.onNext(2); + source.onNext(3); + + Assert.assertEquals(Integer.valueOf(3), it.next()); + + source.onNext(4); + source.onNext(5); + source.onNext(6); + + Assert.assertEquals(Integer.valueOf(6), it.next()); + + source.onNext(7); + source.onComplete(); + + Assert.assertFalse(it.hasNext()); + } + + @Test(expected = UnsupportedOperationException.class) + public void remove() { + Observable.never().blockingLatest().iterator().remove(); + } + + @Test(expected = NoSuchElementException.class) + public void empty2() { + Observable.empty().blockingLatest().iterator().next(); + } + + @Test(expected = TestException.class) + public void error() { + Observable.error(new TestException()).blockingLatest().iterator().next(); + } + + @Test + public void error2() { + Iterator<Object> it = Observable.error(new TestException()).blockingLatest().iterator(); + + for (int i = 0; i < 3; i++) { + try { + it.hasNext(); + fail("Should have thrown"); + } catch (TestException ex) { + // expected + } + } + } + + @Test + public void interrupted() { + Iterator<Object> it = Observable.never().blockingLatest().iterator(); + + Thread.currentThread().interrupt(); + + try { + it.hasNext(); + } catch (RuntimeException ex) { + assertTrue(ex.toString(), ex.getCause() instanceof InterruptedException); + } + Thread.interrupted(); + } + + @SuppressWarnings("unchecked") + @Test + public void onError() { + Iterator<Object> it = Observable.never().blockingLatest().iterator(); + + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + ((Observer<Object>)it).onError(new TestException()); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/observable/BlockingObservableMostRecentTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/BlockingObservableMostRecentTest.java new file mode 100644 index 0000000000..ccf611bbd4 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/BlockingObservableMostRecentTest.java @@ -0,0 +1,121 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import static org.junit.Assert.*; + +import java.util.*; +import java.util.concurrent.TimeUnit; + +import org.junit.*; + +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.core.RxJavaTest; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.schedulers.TestScheduler; +import io.reactivex.rxjava3.subjects.*; + +public class BlockingObservableMostRecentTest extends RxJavaTest { + + static <T> Iterable<T> mostRecent(Observable<T> source, T initialValue) { + return source.blockingMostRecent(initialValue); + } + + @Test + public void mostRecent() { + Subject<String> s = PublishSubject.create(); + + Iterator<String> it = mostRecent(s, "default").iterator(); + + assertTrue(it.hasNext()); + assertEquals("default", it.next()); + assertEquals("default", it.next()); + + s.onNext("one"); + assertTrue(it.hasNext()); + assertEquals("one", it.next()); + assertEquals("one", it.next()); + + s.onNext("two"); + assertTrue(it.hasNext()); + assertEquals("two", it.next()); + assertEquals("two", it.next()); + + s.onComplete(); + assertFalse(it.hasNext()); + + } + + @Test(expected = TestException.class) + public void mostRecentWithException() { + Subject<String> s = PublishSubject.create(); + + Iterator<String> it = mostRecent(s, "default").iterator(); + + assertTrue(it.hasNext()); + assertEquals("default", it.next()); + assertEquals("default", it.next()); + + s.onError(new TestException()); + assertTrue(it.hasNext()); + + it.next(); + } + + @Test + public void singleSourceManyIterators() { + TestScheduler scheduler = new TestScheduler(); + Observable<Long> source = Observable.interval(1, TimeUnit.SECONDS, scheduler).take(10); + + Iterable<Long> iter = source.blockingMostRecent(-1L); + + for (int j = 0; j < 3; j++) { + Iterator<Long> it = iter.iterator(); + + Assert.assertEquals(Long.valueOf(-1), it.next()); + + for (int i = 0; i < 9; i++) { + scheduler.advanceTimeBy(1, TimeUnit.SECONDS); + + Assert.assertTrue(it.hasNext()); + Assert.assertEquals(Long.valueOf(i), it.next()); + } + scheduler.advanceTimeBy(1, TimeUnit.SECONDS); + + Assert.assertFalse(it.hasNext()); + } + + } + + @Test + public void empty() { + Iterator<Integer> it = Observable.<Integer>empty() + .blockingMostRecent(1) + .iterator(); + + try { + it.next(); + fail("Should have thrown"); + } catch (NoSuchElementException ex) { + // expected + } + + try { + it.remove(); + fail("Should have thrown"); + } catch (UnsupportedOperationException ex) { + // expected + } + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/observable/BlockingObservableNextTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/BlockingObservableNextTest.java new file mode 100644 index 0000000000..8274e9f9e3 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/BlockingObservableNextTest.java @@ -0,0 +1,396 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import static org.junit.Assert.*; + +import java.util.*; +import java.util.concurrent.*; +import java.util.concurrent.atomic.*; + +import org.junit.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.core.Observer; +import io.reactivex.rxjava3.disposables.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.internal.operators.observable.BlockingObservableNext.NextObserver; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.schedulers.Schedulers; +import io.reactivex.rxjava3.subjects.*; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class BlockingObservableNextTest extends RxJavaTest { + + private void fireOnNextInNewThread(final Subject<String> o, final String value) { + new Thread() { + @Override + public void run() { + try { + Thread.sleep(500); + } catch (InterruptedException e) { + // ignore + } + o.onNext(value); + } + }.start(); + } + + private void fireOnErrorInNewThread(final Subject<String> o) { + new Thread() { + @Override + public void run() { + try { + Thread.sleep(500); + } catch (InterruptedException e) { + // ignore + } + o.onError(new TestException()); + } + }.start(); + } + + static <T> Iterable<T> next(ObservableSource<T> source) { + return new BlockingObservableNext<>(source); + } + + @Test + public void next() { + Subject<String> obs = PublishSubject.create(); + Iterator<String> it = next(obs).iterator(); + fireOnNextInNewThread(obs, "one"); + assertTrue(it.hasNext()); + assertEquals("one", it.next()); + + fireOnNextInNewThread(obs, "two"); + assertTrue(it.hasNext()); + assertEquals("two", it.next()); + + fireOnNextInNewThread(obs, "three"); + try { + assertEquals("three", it.next()); + } catch (NoSuchElementException e) { + fail("Calling next() without hasNext() should wait for next fire"); + } + + obs.onComplete(); + assertFalse(it.hasNext()); + try { + it.next(); + fail("At the end of an iterator should throw a NoSuchElementException"); + } catch (NoSuchElementException e) { + } + + // If the Observable is completed, hasNext always returns false and next always throw a NoSuchElementException. + assertFalse(it.hasNext()); + try { + it.next(); + fail("At the end of an iterator should throw a NoSuchElementException"); + } catch (NoSuchElementException e) { + } + } + + @Test + public void nextWithError() { + Subject<String> obs = PublishSubject.create(); + Iterator<String> it = next(obs).iterator(); + fireOnNextInNewThread(obs, "one"); + assertTrue(it.hasNext()); + assertEquals("one", it.next()); + + fireOnErrorInNewThread(obs); + try { + it.hasNext(); + fail("Expected an TestException"); + } catch (TestException e) { + } + + assertErrorAfterObservableFail(it); + } + + @Test + public void nextWithEmpty() { + Observable<String> obs = Observable.<String> empty().observeOn(Schedulers.newThread()); + Iterator<String> it = next(obs).iterator(); + + assertFalse(it.hasNext()); + try { + it.next(); + fail("At the end of an iterator should throw a NoSuchElementException"); + } catch (NoSuchElementException e) { + } + + // If the Observable is completed, hasNext always returns false and next always throw a NoSuchElementException. + assertFalse(it.hasNext()); + try { + it.next(); + fail("At the end of an iterator should throw a NoSuchElementException"); + } catch (NoSuchElementException e) { + } + } + + @Test + public void onError() throws Throwable { + Subject<String> obs = PublishSubject.create(); + Iterator<String> it = next(obs).iterator(); + + obs.onError(new TestException()); + try { + it.hasNext(); + fail("Expected an TestException"); + } catch (TestException e) { + // successful + } + + assertErrorAfterObservableFail(it); + } + + @Test + public void onErrorInNewThread() { + Subject<String> obs = PublishSubject.create(); + Iterator<String> it = next(obs).iterator(); + + fireOnErrorInNewThread(obs); + + try { + it.hasNext(); + fail("Expected an TestException"); + } catch (TestException e) { + // successful + } + + assertErrorAfterObservableFail(it); + } + + private void assertErrorAfterObservableFail(Iterator<String> it) { + // After the Observable fails, hasNext and next always throw the exception. + try { + it.hasNext(); + fail("hasNext should throw a TestException"); + } catch (TestException e) { + } + try { + it.next(); + fail("next should throw a TestException"); + } catch (TestException e) { + } + } + + @Test + public void nextWithOnlyUsingNextMethod() { + Subject<String> obs = PublishSubject.create(); + Iterator<String> it = next(obs).iterator(); + fireOnNextInNewThread(obs, "one"); + assertEquals("one", it.next()); + + fireOnNextInNewThread(obs, "two"); + assertEquals("two", it.next()); + + obs.onComplete(); + try { + it.next(); + fail("At the end of an iterator should throw a NoSuchElementException"); + } catch (NoSuchElementException e) { + } + } + + @Test + public void nextWithCallingHasNextMultipleTimes() { + Subject<String> obs = PublishSubject.create(); + Iterator<String> it = next(obs).iterator(); + fireOnNextInNewThread(obs, "one"); + assertTrue(it.hasNext()); + assertTrue(it.hasNext()); + assertTrue(it.hasNext()); + assertTrue(it.hasNext()); + assertEquals("one", it.next()); + + obs.onComplete(); + try { + it.next(); + fail("At the end of an iterator should throw a NoSuchElementException"); + } catch (NoSuchElementException e) { + } + } + + /** + * Confirm that no buffering or blocking of the Observable onNext calls occurs and it just grabs the next emitted value. + * <p> + * This results in output such as {@code => a: 1 b: 2 c: 89} + * + * @throws Throwable some method call is declared throws + */ + @Test + public void noBufferingOrBlockingOfSequence() throws Throwable { + int repeat = 0; + for (;;) { + final SerialDisposable task = new SerialDisposable(); + try { + final CountDownLatch finished = new CountDownLatch(1); + final int COUNT = 30; + final CountDownLatch timeHasPassed = new CountDownLatch(COUNT); + final AtomicBoolean running = new AtomicBoolean(true); + final AtomicInteger count = new AtomicInteger(0); + final Observable<Integer> obs = Observable.unsafeCreate(new ObservableSource<Integer>() { + + @Override + public void subscribe(final Observer<? super Integer> o) { + o.onSubscribe(Disposable.empty()); + task.replace(Schedulers.single().scheduleDirect(new Runnable() { + + @Override + public void run() { + try { + while (running.get() && !task.isDisposed()) { + o.onNext(count.incrementAndGet()); + timeHasPassed.countDown(); + } + o.onComplete(); + } catch (Throwable e) { + o.onError(e); + } finally { + finished.countDown(); + } + } + })); + } + + }); + + Iterator<Integer> it = next(obs).iterator(); + + assertTrue(it.hasNext()); + int a = it.next(); + assertTrue(it.hasNext()); + int b = it.next(); + // we should have a different value + assertTrue("a and b should be different", a != b); + + // wait for some time (if times out we are blocked somewhere so fail ... set very high for very slow, constrained machines) + timeHasPassed.await(8000, TimeUnit.MILLISECONDS); + + assertTrue(it.hasNext()); + int c = it.next(); + + assertTrue("c should not just be the next in sequence", c != (b + 1)); + assertTrue("expected that c [" + c + "] is higher than or equal to " + COUNT, c >= COUNT); + + assertTrue(it.hasNext()); + int d = it.next(); + assertTrue(d > c); + + // shut down the thread + running.set(false); + + finished.await(); + + assertFalse(it.hasNext()); + + System.out.println("a: " + a + " b: " + b + " c: " + c); + break; + } catch (AssertionError ex) { + if (++repeat == 3) { + throw ex; + } + Thread.sleep((int)(1000 * Math.pow(2, repeat - 1))); + } finally { + task.dispose(); + } + } + } + + @Test + public void singleSourceManyIterators() throws InterruptedException { + Observable<Long> o = Observable.interval(250, TimeUnit.MILLISECONDS); + PublishSubject<Integer> terminal = PublishSubject.create(); + Observable<Long> source = o.takeUntil(terminal); + + Iterable<Long> iter = source.blockingNext(); + + for (int j = 0; j < 3; j++) { + BlockingObservableNext.NextIterator<Long> it = (BlockingObservableNext.NextIterator<Long>)iter.iterator(); + + for (long i = 0; i < 10; i++) { + Assert.assertTrue(it.hasNext()); + Assert.assertEquals(j + "th iteration next", Long.valueOf(i), it.next()); + } + terminal.onNext(1); + } + } + + @Test + public void synchronousNext() { + assertEquals(1, BehaviorSubject.createDefault(1).take(1).blockingSingle().intValue()); + assertEquals(2, BehaviorSubject.createDefault(2).blockingIterable().iterator().next().intValue()); + assertEquals(3, BehaviorSubject.createDefault(3).blockingNext().iterator().next().intValue()); + } + + @Test + public void interrupt() { + Iterator<Object> it = Observable.never().blockingNext().iterator(); + + try { + Thread.currentThread().interrupt(); + it.next(); + } catch (RuntimeException ex) { + assertTrue(ex.toString(), ex.getCause() instanceof InterruptedException); + } + } + + @Test(expected = UnsupportedOperationException.class) + public void remove() { + Observable.never().blockingNext().iterator().remove(); + } + + @Test + public void nextObserverError() { + NextObserver<Integer> no = new NextObserver<>(); + + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + no.onError(new TestException()); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void nextObserverOnNext() throws Exception { + NextObserver<Integer> no = new NextObserver<>(); + + no.setWaiting(); + no.onNext(Notification.createOnNext(1)); + + no.setWaiting(); + no.onNext(Notification.createOnNext(1)); + + assertEquals(1, no.takeNext().getValue().intValue()); + } + + @Test + public void nextObserverOnCompleteOnNext() throws Exception { + NextObserver<Integer> no = new NextObserver<>(); + + no.setWaiting(); + no.onNext(Notification.<Integer>createOnComplete()); + + no.setWaiting(); + no.onNext(Notification.createOnNext(1)); + + assertTrue(no.takeNext().isOnComplete()); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/observable/BlockingObservableToFutureTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/BlockingObservableToFutureTest.java new file mode 100644 index 0000000000..dacf971596 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/BlockingObservableToFutureTest.java @@ -0,0 +1,110 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import static org.junit.Assert.*; + +import java.util.*; +import java.util.concurrent.*; + +import io.reactivex.rxjava3.disposables.Disposable; +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.core.Observer; +import io.reactivex.rxjava3.exceptions.TestException; + +public class BlockingObservableToFutureTest extends RxJavaTest { + @Test + public void toFuture() throws InterruptedException, ExecutionException { + Observable<String> obs = Observable.just("one"); + Future<String> f = obs.toFuture(); + assertEquals("one", f.get()); + } + + @Test + public void toFutureList() throws InterruptedException, ExecutionException { + Observable<String> obs = Observable.just("one", "two", "three"); + Future<List<String>> f = obs.toList().toFuture(); + assertEquals("one", f.get().get(0)); + assertEquals("two", f.get().get(1)); + assertEquals("three", f.get().get(2)); + } + + @Test(expected = IndexOutOfBoundsException.class) + public void exceptionWithMoreThanOneElement() throws Throwable { + Observable<String> obs = Observable.just("one", "two"); + Future<String> f = obs.toFuture(); + try { + // we expect an exception since there are more than 1 element + f.get(); + fail("Should have thrown!"); + } + catch (ExecutionException e) { + throw e.getCause(); + } + } + + @Test + public void toFutureWithException() { + Observable<String> obs = Observable.unsafeCreate(new ObservableSource<String>() { + + @Override + public void subscribe(Observer<? super String> observer) { + observer.onSubscribe(Disposable.empty()); + observer.onNext("one"); + observer.onError(new TestException()); + } + }); + + Future<String> f = obs.toFuture(); + try { + f.get(); + fail("expected exception"); + } catch (Throwable e) { + assertEquals(TestException.class, e.getCause().getClass()); + } + } + + @Test(expected = CancellationException.class) + public void getAfterCancel() throws Exception { + Observable<String> obs = Observable.never(); + Future<String> f = obs.toFuture(); + boolean cancelled = f.cancel(true); + assertTrue(cancelled); // because OperationNeverComplete never does + f.get(); // Future.get() docs require this to throw + } + + @Test(expected = CancellationException.class) + public void getWithTimeoutAfterCancel() throws Exception { + Observable<String> obs = Observable.never(); + Future<String> f = obs.toFuture(); + boolean cancelled = f.cancel(true); + assertTrue(cancelled); // because OperationNeverComplete never does + f.get(Long.MAX_VALUE, TimeUnit.NANOSECONDS); // Future.get() docs require this to throw + } + + @Test(expected = NoSuchElementException.class) + public void getWithEmptyFlowable() throws Throwable { + Observable<String> obs = Observable.empty(); + Future<String> f = obs.toFuture(); + try { + f.get(); + } + catch (ExecutionException e) { + throw e.getCause(); + } + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/observable/BlockingObservableToIteratorTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/BlockingObservableToIteratorTest.java new file mode 100644 index 0000000000..684eefbf61 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/BlockingObservableToIteratorTest.java @@ -0,0 +1,143 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import static org.junit.Assert.*; + +import java.util.*; +import java.util.concurrent.TimeUnit; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.core.Observer; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.internal.operators.observable.BlockingObservableIterable.BlockingObservableIterator; +import io.reactivex.rxjava3.schedulers.Schedulers; +import io.reactivex.rxjava3.subjects.PublishSubject; + +public class BlockingObservableToIteratorTest extends RxJavaTest { + + @Test + public void toIterator() { + Observable<String> obs = Observable.just("one", "two", "three"); + + Iterator<String> it = obs.blockingIterable().iterator(); + + assertTrue(it.hasNext()); + assertEquals("one", it.next()); + + assertTrue(it.hasNext()); + assertEquals("two", it.next()); + + assertTrue(it.hasNext()); + assertEquals("three", it.next()); + + assertFalse(it.hasNext()); + + } + + @Test(expected = TestException.class) + public void toIteratorWithException() { + Observable<String> obs = Observable.unsafeCreate(new ObservableSource<String>() { + + @Override + public void subscribe(Observer<? super String> observer) { + observer.onSubscribe(Disposable.empty()); + observer.onNext("one"); + observer.onError(new TestException()); + } + }); + + Iterator<String> it = obs.blockingIterable().iterator(); + + assertTrue(it.hasNext()); + assertEquals("one", it.next()); + + assertTrue(it.hasNext()); + it.next(); + } + + @Test + public void dispose() { + BlockingObservableIterator<Integer> it = new BlockingObservableIterator<>(128); + + assertFalse(it.isDisposed()); + + it.dispose(); + + assertTrue(it.isDisposed()); + } + + @Test + public void interruptWait() { + BlockingObservableIterator<Integer> it = new BlockingObservableIterator<>(128); + + try { + Thread.currentThread().interrupt(); + + it.hasNext(); + } catch (RuntimeException ex) { + assertTrue(ex.toString(), ex.getCause() instanceof InterruptedException); + } + } + + @Test(expected = NoSuchElementException.class) + public void emptyThrowsNoSuch() { + BlockingObservableIterator<Integer> it = new BlockingObservableIterator<>(128); + it.onComplete(); + it.next(); + } + + @Test(expected = UnsupportedOperationException.class) + public void remove() { + BlockingObservableIterator<Integer> it = new BlockingObservableIterator<>(128); + it.remove(); + } + + @Test(expected = NoSuchElementException.class) + public void disposedIteratorHasNextReturns() { + Iterator<Integer> it = PublishSubject.<Integer>create() + .blockingIterable().iterator(); + ((Disposable)it).dispose(); + assertFalse(it.hasNext()); + it.next(); + } + + @Test + public void asyncDisposeUnblocks() { + final Iterator<Integer> it = PublishSubject.<Integer>create() + .blockingIterable().iterator(); + + Schedulers.single().scheduleDirect(new Runnable() { + @Override + public void run() { + ((Disposable)it).dispose(); + } + }, 1, TimeUnit.SECONDS); + + assertFalse(it.hasNext()); + } + + @Test(expected = TestException.class) + public void errorAfterDispose() { + Iterator<Object> it = Observable.error(new TestException()).blockingIterable().iterator(); + + ((Disposable)it).dispose(); + + it.hasNext(); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/observable/Burst.java b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/Burst.java new file mode 100644 index 0000000000..46b5df42f3 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/Burst.java @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import java.util.*; + +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.core.Observer; +import io.reactivex.rxjava3.disposables.Disposable; + +/** + * Creates {@link Observable} of a number of items followed by either an error or + * completion. Subscription status is not checked before emitting an event. + * + * @param <T> the value type + */ +public final class Burst<T> extends Observable<T> { + + final List<T> items; + final Throwable error; + + Burst(Throwable error, List<T> items) { + this.error = error; + this.items = items; + } + + @Override + protected void subscribeActual(final Observer<? super T> observer) { + observer.onSubscribe(Disposable.empty()); + for (T item: items) { + observer.onNext(item); + } + if (error != null) { + observer.onError(error); + } else { + observer.onComplete(); + } + } + + public static <T> Builder<T> item(T item) { + return items(item); + } + + @SafeVarargs + public static <T> Builder<T> items(T... items) { + return new Builder<>(Arrays.asList(items)); + } + + public static final class Builder<T> { + + private final List<T> items; + private Throwable error; + + Builder(List<T> items) { + this.items = items; + } + + public Observable<T> error(Throwable e) { + this.error = e; + return create(); + } + + public Observable<T> create() { + return new Burst<>(error, items); + } + + } + +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableAllTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableAllTest.java new file mode 100644 index 0000000000..fba37cb9f9 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableAllTest.java @@ -0,0 +1,377 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import java.util.List; +import java.util.concurrent.TimeUnit; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.testsupport.*; + +public class ObservableAllTest extends RxJavaTest { + + @Test + public void allObservable() { + Observable<String> obs = Observable.just("one", "two", "six"); + + Observer <Boolean> observer = TestHelper.mockObserver(); + + obs.all(new Predicate<String>() { + @Override + public boolean test(String s) { + return s.length() == 3; + } + }).toObservable() + .subscribe(observer); + + verify(observer).onSubscribe((Disposable)any()); + verify(observer).onNext(true); + verify(observer).onComplete(); + verifyNoMoreInteractions(observer); + } + + @Test + public void notAllObservable() { + Observable<String> obs = Observable.just("one", "two", "three", "six"); + + Observer <Boolean> observer = TestHelper.mockObserver(); + + obs.all(new Predicate<String>() { + @Override + public boolean test(String s) { + return s.length() == 3; + } + }).toObservable() + .subscribe(observer); + + verify(observer).onSubscribe((Disposable)any()); + verify(observer).onNext(false); + verify(observer).onComplete(); + verifyNoMoreInteractions(observer); + } + + @Test + public void emptyObservable() { + Observable<String> obs = Observable.empty(); + + Observer <Boolean> observer = TestHelper.mockObserver(); + + obs.all(new Predicate<String>() { + @Override + public boolean test(String s) { + return s.length() == 3; + } + }).toObservable() + .subscribe(observer); + + verify(observer).onSubscribe((Disposable)any()); + verify(observer).onNext(true); + verify(observer).onComplete(); + verifyNoMoreInteractions(observer); + } + + @Test + public void errorObservable() { + Throwable error = new Throwable(); + Observable<String> obs = Observable.error(error); + + Observer <Boolean> observer = TestHelper.mockObserver(); + + obs.all(new Predicate<String>() { + @Override + public boolean test(String s) { + return s.length() == 3; + } + }).toObservable() + .subscribe(observer); + + verify(observer).onSubscribe((Disposable)any()); + verify(observer).onError(error); + verifyNoMoreInteractions(observer); + } + + @Test + public void followingFirstObservable() { + Observable<Integer> o = Observable.fromArray(1, 3, 5, 6); + Observable<Boolean> allOdd = o.all(new Predicate<Integer>() { + @Override + public boolean test(Integer i) { + return i % 2 == 1; + } + }).toObservable(); + + assertFalse(allOdd.blockingFirst()); + } + + @Test + public void issue1935NoUnsubscribeDownstreamObservable() { + Observable<Integer> source = Observable.just(1) + .all(new Predicate<Integer>() { + @Override + public boolean test(Integer t1) { + return false; + } + }).toObservable() + .flatMap(new Function<Boolean, Observable<Integer>>() { + @Override + public Observable<Integer> apply(Boolean t1) { + return Observable.just(2).delay(500, TimeUnit.MILLISECONDS); + } + }); + + assertEquals((Object)2, source.blockingFirst()); + } + + @Test + public void predicateThrowsExceptionAndValueInCauseMessageObservable() { + TestObserverEx<Boolean> to = new TestObserverEx<>(); + + final IllegalArgumentException ex = new IllegalArgumentException(); + + Observable.just("Boo!").all(new Predicate<String>() { + @Override + public boolean test(String v) { + throw ex; + } + }) + .subscribe(to); + + to.assertTerminated(); + to.assertNoValues(); + to.assertNotComplete(); + to.assertError(ex); + // FIXME need to decide about adding the value that probably caused the crash in some way +// assertTrue(ex.getCause().getMessage().contains("Boo!")); + } + + @Test + public void all() { + Observable<String> obs = Observable.just("one", "two", "six"); + + SingleObserver<Boolean> observer = TestHelper.mockSingleObserver(); + + obs.all(new Predicate<String>() { + @Override + public boolean test(String s) { + return s.length() == 3; + } + }) + .subscribe(observer); + + verify(observer).onSubscribe((Disposable)any()); + verify(observer).onSuccess(true); + verifyNoMoreInteractions(observer); + } + + @Test + public void notAll() { + Observable<String> obs = Observable.just("one", "two", "three", "six"); + + SingleObserver <Boolean> observer = TestHelper.mockSingleObserver(); + + obs.all(new Predicate<String>() { + @Override + public boolean test(String s) { + return s.length() == 3; + } + }) + .subscribe(observer); + + verify(observer).onSubscribe((Disposable)any()); + verify(observer).onSuccess(false); + verifyNoMoreInteractions(observer); + } + + @Test + public void empty() { + Observable<String> obs = Observable.empty(); + + SingleObserver <Boolean> observer = TestHelper.mockSingleObserver(); + + obs.all(new Predicate<String>() { + @Override + public boolean test(String s) { + return s.length() == 3; + } + }) + .subscribe(observer); + + verify(observer).onSubscribe((Disposable)any()); + verify(observer).onSuccess(true); + verifyNoMoreInteractions(observer); + } + + @Test + public void error() { + Throwable error = new Throwable(); + Observable<String> obs = Observable.error(error); + + SingleObserver <Boolean> observer = TestHelper.mockSingleObserver(); + + obs.all(new Predicate<String>() { + @Override + public boolean test(String s) { + return s.length() == 3; + } + }) + .subscribe(observer); + + verify(observer).onSubscribe((Disposable)any()); + verify(observer).onError(error); + verifyNoMoreInteractions(observer); + } + + @Test + public void followingFirst() { + Observable<Integer> o = Observable.fromArray(1, 3, 5, 6); + Single<Boolean> allOdd = o.all(new Predicate<Integer>() { + @Override + public boolean test(Integer i) { + return i % 2 == 1; + } + }); + + assertFalse(allOdd.blockingGet()); + } + + @Test + public void issue1935NoUnsubscribeDownstream() { + Observable<Integer> source = Observable.just(1) + .all(new Predicate<Integer>() { + @Override + public boolean test(Integer t1) { + return false; + } + }) + .flatMapObservable(new Function<Boolean, Observable<Integer>>() { + @Override + public Observable<Integer> apply(Boolean t1) { + return Observable.just(2).delay(500, TimeUnit.MILLISECONDS); + } + }); + + assertEquals((Object)2, source.blockingFirst()); + } + + @Test + public void predicateThrowsExceptionAndValueInCauseMessage() { + TestObserverEx<Boolean> to = new TestObserverEx<>(); + + final IllegalArgumentException ex = new IllegalArgumentException(); + + Observable.just("Boo!").all(new Predicate<String>() { + @Override + public boolean test(String v) { + throw ex; + } + }) + .subscribe(to); + + to.assertTerminated(); + to.assertNoValues(); + to.assertNotComplete(); + to.assertError(ex); + // FIXME need to decide about adding the value that probably caused the crash in some way +// assertTrue(ex.getCause().getMessage().contains("Boo!")); + } + + @Test + public void dispose() { + TestHelper.checkDisposed(Observable.just(1).all(Functions.alwaysTrue()).toObservable()); + + TestHelper.checkDisposed(Observable.just(1).all(Functions.alwaysTrue())); + } + + @Test + public void predicateThrowsObservable() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + new Observable<Integer>() { + @Override + protected void subscribeActual(Observer<? super Integer> observer) { + observer.onSubscribe(Disposable.empty()); + + observer.onNext(1); + observer.onNext(2); + observer.onError(new TestException()); + observer.onComplete(); + } + } + .all(new Predicate<Integer>() { + @Override + public boolean test(Integer v) throws Exception { + throw new TestException(); + } + }) + .toObservable() + .test() + .assertFailure(TestException.class); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void predicateThrows() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + new Observable<Integer>() { + @Override + protected void subscribeActual(Observer<? super Integer> observer) { + observer.onSubscribe(Disposable.empty()); + + observer.onNext(1); + observer.onNext(2); + observer.onError(new TestException()); + observer.onComplete(); + } + } + .all(new Predicate<Integer>() { + @Override + public boolean test(Integer v) throws Exception { + throw new TestException(); + } + }) + .test() + .assertFailure(TestException.class); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeObservableToSingle(o -> o.all(v -> true)); + } + + @Test + public void doubleOnSubscribeObservable() { + TestHelper.checkDoubleOnSubscribeObservable(o -> o.all(v -> true).toObservable()); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableAmbTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableAmbTest.java new file mode 100644 index 0000000000..0a349cd417 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableAmbTest.java @@ -0,0 +1,467 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +import java.io.IOException; +import java.util.*; +import java.util.concurrent.*; +import java.util.concurrent.atomic.*; + +import org.junit.*; +import org.mockito.InOrder; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.core.Observer; +import io.reactivex.rxjava3.disposables.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.schedulers.*; +import io.reactivex.rxjava3.subjects.PublishSubject; +import io.reactivex.rxjava3.testsupport.*; + +public class ObservableAmbTest extends RxJavaTest { + + private TestScheduler scheduler; + private Scheduler.Worker innerScheduler; + + @Before + public void setUp() { + scheduler = new TestScheduler(); + innerScheduler = scheduler.createWorker(); + } + + private Observable<String> createObservable(final String[] values, + final long interval, final Throwable e) { + return Observable.unsafeCreate(new ObservableSource<String>() { + + @Override + public void subscribe(final Observer<? super String> observer) { + CompositeDisposable parentSubscription = new CompositeDisposable(); + + observer.onSubscribe(parentSubscription); + + long delay = interval; + for (final String value : values) { + parentSubscription.add(innerScheduler.schedule(new Runnable() { + @Override + public void run() { + observer.onNext(value); + } + } + , delay, TimeUnit.MILLISECONDS)); + delay += interval; + } + parentSubscription.add(innerScheduler.schedule(new Runnable() { + @Override + public void run() { + if (e == null) { + observer.onComplete(); + } else { + observer.onError(e); + } + } + }, delay, TimeUnit.MILLISECONDS)); + } + }); + } + + @Test + public void amb() { + Observable<String> observable1 = createObservable(new String[] { + "1", "11", "111", "1111" }, 2000, null); + Observable<String> observable2 = createObservable(new String[] { + "2", "22", "222", "2222" }, 1000, null); + Observable<String> observable3 = createObservable(new String[] { + "3", "33", "333", "3333" }, 3000, null); + + Observable<String> o = Observable.ambArray(observable1, + observable2, observable3); + + Observer<String> observer = TestHelper.mockObserver(); + o.subscribe(observer); + + scheduler.advanceTimeBy(100000, TimeUnit.MILLISECONDS); + + InOrder inOrder = inOrder(observer); + inOrder.verify(observer, times(1)).onNext("2"); + inOrder.verify(observer, times(1)).onNext("22"); + inOrder.verify(observer, times(1)).onNext("222"); + inOrder.verify(observer, times(1)).onNext("2222"); + inOrder.verify(observer, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void amb2() { + IOException expectedException = new IOException( + "fake exception"); + Observable<String> observable1 = createObservable(new String[] {}, + 2000, new IOException("fake exception")); + Observable<String> observable2 = createObservable(new String[] { + "2", "22", "222", "2222" }, 1000, expectedException); + Observable<String> observable3 = createObservable(new String[] {}, + 3000, new IOException("fake exception")); + + Observable<String> o = Observable.ambArray(observable1, + observable2, observable3); + + Observer<String> observer = TestHelper.mockObserver(); + o.subscribe(observer); + + scheduler.advanceTimeBy(100000, TimeUnit.MILLISECONDS); + + InOrder inOrder = inOrder(observer); + inOrder.verify(observer, times(1)).onNext("2"); + inOrder.verify(observer, times(1)).onNext("22"); + inOrder.verify(observer, times(1)).onNext("222"); + inOrder.verify(observer, times(1)).onNext("2222"); + inOrder.verify(observer, times(1)).onError(expectedException); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void amb3() { + Observable<String> observable1 = createObservable(new String[] { + "1" }, 2000, null); + Observable<String> observable2 = createObservable(new String[] {}, + 1000, null); + Observable<String> observable3 = createObservable(new String[] { + "3" }, 3000, null); + + Observable<String> o = Observable.ambArray(observable1, + observable2, observable3); + + Observer<String> observer = TestHelper.mockObserver(); + o.subscribe(observer); + + scheduler.advanceTimeBy(100000, TimeUnit.MILLISECONDS); + InOrder inOrder = inOrder(observer); + inOrder.verify(observer, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void subscriptionOnlyHappensOnce() throws InterruptedException { + final AtomicLong count = new AtomicLong(); + Consumer<Disposable> incrementer = new Consumer<Disposable>() { + @Override + public void accept(Disposable d) { + count.incrementAndGet(); + } + }; + + //this aync stream should emit first + Observable<Integer> o1 = Observable.just(1).doOnSubscribe(incrementer) + .delay(100, TimeUnit.MILLISECONDS).subscribeOn(Schedulers.computation()); + //this stream emits second + Observable<Integer> o2 = Observable.just(1).doOnSubscribe(incrementer) + .delay(100, TimeUnit.MILLISECONDS).subscribeOn(Schedulers.computation()); + TestObserver<Integer> to = new TestObserver<>(); + Observable.ambArray(o1, o2).subscribe(to); + to.awaitDone(5, TimeUnit.SECONDS); + to.assertNoErrors(); + assertEquals(2, count.get()); + } + + @Test + public void synchronousSources() { + // under async subscription the second Observable would complete before + // the first but because this is a synchronous subscription to sources + // then second Observable does not get subscribed to before first + // subscription completes hence first Observable emits result through + // amb + int result = Observable.just(1).doOnNext(new Consumer<Integer>() { + @Override + public void accept(Integer t) { + try { + Thread.sleep(100); + } catch (InterruptedException e) { + // + } + } + }).ambWith(Observable.just(2)).blockingSingle(); + assertEquals(1, result); + } + + @Test + public void ambCancelsOthers() { + PublishSubject<Integer> source1 = PublishSubject.create(); + PublishSubject<Integer> source2 = PublishSubject.create(); + PublishSubject<Integer> source3 = PublishSubject.create(); + + TestObserver<Integer> to = new TestObserver<>(); + + Observable.ambArray(source1, source2, source3).subscribe(to); + + assertTrue("Source 1 doesn't have subscribers!", source1.hasObservers()); + assertTrue("Source 2 doesn't have subscribers!", source2.hasObservers()); + assertTrue("Source 3 doesn't have subscribers!", source3.hasObservers()); + + source1.onNext(1); + + assertTrue("Source 1 doesn't have subscribers!", source1.hasObservers()); + assertFalse("Source 2 still has subscribers!", source2.hasObservers()); + assertFalse("Source 2 still has subscribers!", source3.hasObservers()); + + } + + @Test + public void ambArrayEmpty() { + assertSame(Observable.empty(), Observable.ambArray()); + } + + @Test + public void ambArraySingleElement() { + assertSame(Observable.never(), Observable.ambArray(Observable.never())); + } + + @Test + public void manySources() { + Observable<?>[] a = new Observable[32]; + Arrays.fill(a, Observable.never()); + a[31] = Observable.just(1); + + Observable.amb(Arrays.asList(a)) + .test() + .assertResult(1); + } + + @Test + public void emptyIterable() { + Observable.amb(Collections.<Observable<Integer>>emptyList()) + .test() + .assertResult(); + } + + @Test + public void singleIterable() { + Observable.amb(Collections.singletonList(Observable.just(1))) + .test() + .assertResult(1); + } + + @Test + public void disposed() { + TestHelper.checkDisposed(Observable.ambArray(Observable.never(), Observable.never())); + } + + @Test + public void onNextRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final PublishSubject<Integer> ps1 = PublishSubject.create(); + final PublishSubject<Integer> ps2 = PublishSubject.create(); + + TestObserverEx<Integer> to = Observable.ambArray(ps1, ps2).to(TestHelper.<Integer>testConsumer()); + + Runnable r1 = new Runnable() { + @Override + public void run() { + ps1.onNext(1); + } + }; + Runnable r2 = new Runnable() { + @Override + public void run() { + ps2.onNext(1); + } + }; + + TestHelper.race(r1, r2); + + to.assertSubscribed() + .assertNoErrors() + .assertNotComplete() + .assertValueCount(1); + } + } + + @Test + public void onCompleteRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final PublishSubject<Integer> ps1 = PublishSubject.create(); + final PublishSubject<Integer> ps2 = PublishSubject.create(); + + TestObserver<Integer> to = Observable.ambArray(ps1, ps2).test(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + ps1.onComplete(); + } + }; + Runnable r2 = new Runnable() { + @Override + public void run() { + ps2.onComplete(); + } + }; + + TestHelper.race(r1, r2); + + to.assertResult(); + } + } + + @Test + public void onErrorRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final PublishSubject<Integer> ps1 = PublishSubject.create(); + final PublishSubject<Integer> ps2 = PublishSubject.create(); + + TestObserver<Integer> to = Observable.ambArray(ps1, ps2).test(); + + final Throwable ex = new TestException(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + ps1.onError(ex); + } + }; + Runnable r2 = new Runnable() { + @Override + public void run() { + ps2.onError(ex); + } + }; + + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + TestHelper.race(r1, r2); + } finally { + RxJavaPlugins.reset(); + } + + to.assertFailure(TestException.class); + if (!errors.isEmpty()) { + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } + } + } + + @Test + public void ambWithOrder() { + Observable<Integer> error = Observable.error(new RuntimeException()); + Observable.just(1).ambWith(error).test().assertValue(1).assertComplete(); + } + + @Test + public void ambIterableOrder() { + Observable<Integer> error = Observable.error(new RuntimeException()); + Observable.amb(Arrays.asList(Observable.just(1), error)).test().assertValue(1).assertComplete(); + } + + @Test + public void ambArrayOrder() { + Observable<Integer> error = Observable.error(new RuntimeException()); + Observable.ambArray(Observable.just(1), error).test().assertValue(1).assertComplete(); + } + + @Test + public void noWinnerSuccessDispose() throws Exception { + for (int i = 0; i < TestHelper.RACE_LONG_LOOPS; i++) { + final AtomicBoolean interrupted = new AtomicBoolean(); + final CountDownLatch cdl = new CountDownLatch(1); + + Observable.ambArray( + Observable.just(1) + .subscribeOn(Schedulers.single()) + .observeOn(Schedulers.computation()), + Observable.never() + ) + .subscribe(new Consumer<Object>() { + @Override + public void accept(Object v) throws Exception { + interrupted.set(Thread.currentThread().isInterrupted()); + cdl.countDown(); + } + }); + + assertTrue(cdl.await(500, TimeUnit.SECONDS)); + assertFalse("Interrupted!", interrupted.get()); + } + } + + @Test + public void noWinnerErrorDispose() throws Exception { + final TestException ex = new TestException(); + for (int i = 0; i < TestHelper.RACE_LONG_LOOPS; i++) { + final AtomicBoolean interrupted = new AtomicBoolean(); + final CountDownLatch cdl = new CountDownLatch(1); + + Observable.ambArray( + Observable.error(ex) + .subscribeOn(Schedulers.single()) + .observeOn(Schedulers.computation()), + Observable.never() + ) + .subscribe(Functions.emptyConsumer(), new Consumer<Throwable>() { + @Override + public void accept(Throwable e) throws Exception { + interrupted.set(Thread.currentThread().isInterrupted()); + cdl.countDown(); + } + }); + + assertTrue(cdl.await(500, TimeUnit.SECONDS)); + assertFalse("Interrupted!", interrupted.get()); + } + } + + @Test + public void noWinnerCompleteDispose() throws Exception { + for (int i = 0; i < TestHelper.RACE_LONG_LOOPS; i++) { + final AtomicBoolean interrupted = new AtomicBoolean(); + final CountDownLatch cdl = new CountDownLatch(1); + + Observable.ambArray( + Observable.empty() + .subscribeOn(Schedulers.single()) + .observeOn(Schedulers.computation()), + Observable.never() + ) + .subscribe(Functions.emptyConsumer(), Functions.emptyConsumer(), new Action() { + @Override + public void run() throws Exception { + interrupted.set(Thread.currentThread().isInterrupted()); + cdl.countDown(); + } + }); + + assertTrue(cdl.await(500, TimeUnit.SECONDS)); + assertFalse("Interrupted!", interrupted.get()); + } + } + + @Test + public void observableSourcesInIterable() { + ObservableSource<Integer> source = new ObservableSource<Integer>() { + @Override + public void subscribe(Observer<? super Integer> observer) { + Observable.just(1).subscribe(observer); + } + }; + + Observable.amb(Arrays.asList(source, source)) + .test() + .assertResult(1); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableAnyTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableAnyTest.java new file mode 100644 index 0000000000..cb85603fa1 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableAnyTest.java @@ -0,0 +1,568 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import java.io.IOException; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import io.reactivex.rxjava3.disposables.Disposable; +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.testsupport.*; + +public class ObservableAnyTest extends RxJavaTest { + + @Test + public void anyWithTwoItemsObservable() { + Observable<Integer> w = Observable.just(1, 2); + Observable<Boolean> observable = w.any(new Predicate<Integer>() { + @Override + public boolean test(Integer v) { + return true; + } + }).toObservable(); + + Observer<Boolean> observer = TestHelper.mockObserver(); + + observable.subscribe(observer); + + verify(observer, never()).onNext(false); + verify(observer, times(1)).onNext(true); + verify(observer, never()).onError(any(Throwable.class)); + verify(observer, times(1)).onComplete(); + } + + @Test + public void isEmptyWithTwoItemsObservable() { + Observable<Integer> w = Observable.just(1, 2); + Observable<Boolean> observable = w.isEmpty().toObservable(); + + Observer<Boolean> observer = TestHelper.mockObserver(); + + observable.subscribe(observer); + + verify(observer, never()).onNext(true); + verify(observer, times(1)).onNext(false); + verify(observer, never()).onError(any(Throwable.class)); + verify(observer, times(1)).onComplete(); + } + + @Test + public void anyWithOneItemObservable() { + Observable<Integer> w = Observable.just(1); + Observable<Boolean> observable = w.any(new Predicate<Integer>() { + @Override + public boolean test(Integer v) { + return true; + } + }).toObservable(); + + Observer<Boolean> observer = TestHelper.mockObserver(); + + observable.subscribe(observer); + + verify(observer, never()).onNext(false); + verify(observer, times(1)).onNext(true); + verify(observer, never()).onError(any(Throwable.class)); + verify(observer, times(1)).onComplete(); + } + + @Test + public void isEmptyWithOneItemObservable() { + Observable<Integer> w = Observable.just(1); + Observable<Boolean> observable = w.isEmpty().toObservable(); + + Observer<Boolean> observer = TestHelper.mockObserver(); + + observable.subscribe(observer); + + verify(observer, never()).onNext(true); + verify(observer, times(1)).onNext(false); + verify(observer, never()).onError(any(Throwable.class)); + verify(observer, times(1)).onComplete(); + } + + @Test + public void anyWithEmptyObservable() { + Observable<Integer> w = Observable.empty(); + Observable<Boolean> observable = w.any(new Predicate<Integer>() { + @Override + public boolean test(Integer v) { + return true; + } + }).toObservable(); + + Observer<Boolean> observer = TestHelper.mockObserver(); + + observable.subscribe(observer); + + verify(observer, times(1)).onNext(false); + verify(observer, never()).onNext(true); + verify(observer, never()).onError(any(Throwable.class)); + verify(observer, times(1)).onComplete(); + } + + @Test + public void isEmptyWithEmptyObservable() { + Observable<Integer> w = Observable.empty(); + Observable<Boolean> observable = w.isEmpty().toObservable(); + + Observer<Boolean> observer = TestHelper.mockObserver(); + + observable.subscribe(observer); + + verify(observer, times(1)).onNext(true); + verify(observer, never()).onNext(false); + verify(observer, never()).onError(any(Throwable.class)); + verify(observer, times(1)).onComplete(); + } + + @Test + public void anyWithPredicate1Observable() { + Observable<Integer> w = Observable.just(1, 2, 3); + Observable<Boolean> observable = w.any(new Predicate<Integer>() { + @Override + public boolean test(Integer t1) { + return t1 < 2; + } + }).toObservable(); + + Observer<Boolean> observer = TestHelper.mockObserver(); + + observable.subscribe(observer); + + verify(observer, never()).onNext(false); + verify(observer, times(1)).onNext(true); + verify(observer, never()).onError(any(Throwable.class)); + verify(observer, times(1)).onComplete(); + } + + @Test + public void exists1Observable() { + Observable<Integer> w = Observable.just(1, 2, 3); + Observable<Boolean> observable = w.any(new Predicate<Integer>() { + @Override + public boolean test(Integer t1) { + return t1 < 2; + } + }).toObservable(); + + Observer<Boolean> observer = TestHelper.mockObserver(); + + observable.subscribe(observer); + + verify(observer, never()).onNext(false); + verify(observer, times(1)).onNext(true); + verify(observer, never()).onError(any(Throwable.class)); + verify(observer, times(1)).onComplete(); + } + + @Test + public void anyWithPredicate2Observable() { + Observable<Integer> w = Observable.just(1, 2, 3); + Observable<Boolean> observable = w.any(new Predicate<Integer>() { + @Override + public boolean test(Integer t1) { + return t1 < 1; + } + }).toObservable(); + + Observer<Boolean> observer = TestHelper.mockObserver(); + + observable.subscribe(observer); + + verify(observer, times(1)).onNext(false); + verify(observer, never()).onNext(true); + verify(observer, never()).onError(any(Throwable.class)); + verify(observer, times(1)).onComplete(); + } + + @Test + public void anyWithEmptyAndPredicateObservable() { + // If the source is empty, always output false. + Observable<Integer> w = Observable.empty(); + Observable<Boolean> observable = w.any(new Predicate<Integer>() { + @Override + public boolean test(Integer t) { + return true; + } + }).toObservable(); + + Observer<Boolean> observer = TestHelper.mockObserver(); + + observable.subscribe(observer); + + verify(observer, times(1)).onNext(false); + verify(observer, never()).onNext(true); + verify(observer, never()).onError(any(Throwable.class)); + verify(observer, times(1)).onComplete(); + } + + @Test + public void withFollowingFirstObservable() { + Observable<Integer> o = Observable.fromArray(1, 3, 5, 6); + Observable<Boolean> anyEven = o.any(new Predicate<Integer>() { + @Override + public boolean test(Integer i) { + return i % 2 == 0; + } + }).toObservable(); + + assertTrue(anyEven.blockingFirst()); + } + + @Test + public void issue1935NoUnsubscribeDownstreamObservable() { + Observable<Integer> source = Observable.just(1).isEmpty().toObservable() + .flatMap(new Function<Boolean, Observable<Integer>>() { + @Override + public Observable<Integer> apply(Boolean t1) { + return Observable.just(2).delay(500, TimeUnit.MILLISECONDS); + } + }); + + assertEquals((Object)2, source.blockingFirst()); + } + + @Test + public void predicateThrowsExceptionAndValueInCauseMessageObservable() { + TestObserverEx<Boolean> to = new TestObserverEx<>(); + final IllegalArgumentException ex = new IllegalArgumentException(); + + Observable.just("Boo!").any(new Predicate<String>() { + @Override + public boolean test(String v) { + throw ex; + } + }).subscribe(to); + + to.assertTerminated(); + to.assertNoValues(); + to.assertNotComplete(); + to.assertError(ex); + // FIXME value as last cause? +// assertTrue(ex.getCause().getMessage().contains("Boo!")); + } + + @Test + public void anyWithTwoItems() { + Observable<Integer> w = Observable.just(1, 2); + Single<Boolean> single = w.any(new Predicate<Integer>() { + @Override + public boolean test(Integer v) { + return true; + } + }); + + SingleObserver<Boolean> observer = TestHelper.mockSingleObserver(); + + single.subscribe(observer); + + verify(observer, never()).onSuccess(false); + verify(observer, times(1)).onSuccess(true); + verify(observer, never()).onError(any(Throwable.class)); + } + + @Test + public void isEmptyWithTwoItems() { + Observable<Integer> w = Observable.just(1, 2); + Single<Boolean> single = w.isEmpty(); + + SingleObserver<Boolean> observer = TestHelper.mockSingleObserver(); + + single.subscribe(observer); + + verify(observer, never()).onSuccess(true); + verify(observer, times(1)).onSuccess(false); + verify(observer, never()).onError(any(Throwable.class)); + } + + @Test + public void anyWithOneItem() { + Observable<Integer> w = Observable.just(1); + Single<Boolean> single = w.any(new Predicate<Integer>() { + @Override + public boolean test(Integer v) { + return true; + } + }); + + SingleObserver<Boolean> observer = TestHelper.mockSingleObserver(); + + single.subscribe(observer); + + verify(observer, never()).onSuccess(false); + verify(observer, times(1)).onSuccess(true); + verify(observer, never()).onError(any(Throwable.class)); + } + + @Test + public void isEmptyWithOneItem() { + Observable<Integer> w = Observable.just(1); + Single<Boolean> single = w.isEmpty(); + + SingleObserver<Boolean> observer = TestHelper.mockSingleObserver(); + + single.subscribe(observer); + + verify(observer, never()).onSuccess(true); + verify(observer, times(1)).onSuccess(false); + verify(observer, never()).onError(any(Throwable.class)); + } + + @Test + public void anyWithEmpty() { + Observable<Integer> w = Observable.empty(); + Single<Boolean> single = w.any(new Predicate<Integer>() { + @Override + public boolean test(Integer v) { + return true; + } + }); + + SingleObserver<Boolean> observer = TestHelper.mockSingleObserver(); + + single.subscribe(observer); + + verify(observer, times(1)).onSuccess(false); + verify(observer, never()).onSuccess(true); + verify(observer, never()).onError(any(Throwable.class)); + } + + @Test + public void isEmptyWithEmpty() { + Observable<Integer> w = Observable.empty(); + Single<Boolean> single = w.isEmpty(); + + SingleObserver<Boolean> observer = TestHelper.mockSingleObserver(); + + single.subscribe(observer); + + verify(observer, times(1)).onSuccess(true); + verify(observer, never()).onSuccess(false); + verify(observer, never()).onError(any(Throwable.class)); + } + + @Test + public void anyWithPredicate1() { + Observable<Integer> w = Observable.just(1, 2, 3); + Single<Boolean> single = w.any(new Predicate<Integer>() { + @Override + public boolean test(Integer t1) { + return t1 < 2; + } + }); + + SingleObserver<Boolean> observer = TestHelper.mockSingleObserver(); + + single.subscribe(observer); + + verify(observer, never()).onSuccess(false); + verify(observer, times(1)).onSuccess(true); + verify(observer, never()).onError(any(Throwable.class)); + } + + @Test + public void exists1() { + Observable<Integer> w = Observable.just(1, 2, 3); + Single<Boolean> single = w.any(new Predicate<Integer>() { + @Override + public boolean test(Integer t1) { + return t1 < 2; + } + }); + + SingleObserver<Boolean> observer = TestHelper.mockSingleObserver(); + + single.subscribe(observer); + + verify(observer, never()).onSuccess(false); + verify(observer, times(1)).onSuccess(true); + verify(observer, never()).onError(any(Throwable.class)); + } + + @Test + public void anyWithPredicate2() { + Observable<Integer> w = Observable.just(1, 2, 3); + Single<Boolean> single = w.any(new Predicate<Integer>() { + @Override + public boolean test(Integer t1) { + return t1 < 1; + } + }); + + SingleObserver<Boolean> observer = TestHelper.mockSingleObserver(); + + single.subscribe(observer); + + verify(observer, times(1)).onSuccess(false); + verify(observer, never()).onSuccess(true); + verify(observer, never()).onError(any(Throwable.class)); + } + + @Test + public void anyWithEmptyAndPredicate() { + // If the source is empty, always output false. + Observable<Integer> w = Observable.empty(); + Single<Boolean> single = w.any(new Predicate<Integer>() { + @Override + public boolean test(Integer t) { + return true; + } + }); + + SingleObserver<Boolean> observer = TestHelper.mockSingleObserver(); + + single.subscribe(observer); + + verify(observer, times(1)).onSuccess(false); + verify(observer, never()).onSuccess(true); + verify(observer, never()).onError(any(Throwable.class)); + } + + @Test + public void withFollowingFirst() { + Observable<Integer> o = Observable.fromArray(1, 3, 5, 6); + Single<Boolean> anyEven = o.any(new Predicate<Integer>() { + @Override + public boolean test(Integer i) { + return i % 2 == 0; + } + }); + + assertTrue(anyEven.blockingGet()); + } + + @Test + public void issue1935NoUnsubscribeDownstream() { + Observable<Integer> source = Observable.just(1).isEmpty() + .flatMapObservable(new Function<Boolean, Observable<Integer>>() { + @Override + public Observable<Integer> apply(Boolean t1) { + return Observable.just(2).delay(500, TimeUnit.MILLISECONDS); + } + }); + + assertEquals((Object)2, source.blockingFirst()); + } + + @Test + public void predicateThrowsExceptionAndValueInCauseMessage() { + TestObserverEx<Boolean> to = new TestObserverEx<>(); + final IllegalArgumentException ex = new IllegalArgumentException(); + + Observable.just("Boo!").any(new Predicate<String>() { + @Override + public boolean test(String v) { + throw ex; + } + }).subscribe(to); + + to.assertTerminated(); + to.assertNoValues(); + to.assertNotComplete(); + to.assertError(ex); + // FIXME value as last cause? +// assertTrue(ex.getCause().getMessage().contains("Boo!")); + } + + @Test + public void dispose() { + TestHelper.checkDisposed(Observable.just(1).any(Functions.alwaysTrue()).toObservable()); + + TestHelper.checkDisposed(Observable.just(1).any(Functions.alwaysTrue())); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeObservable(new Function<Observable<Object>, ObservableSource<Boolean>>() { + @Override + public ObservableSource<Boolean> apply(Observable<Object> o) throws Exception { + return o.any(Functions.alwaysTrue()).toObservable(); + } + }); + TestHelper.checkDoubleOnSubscribeObservableToSingle(new Function<Observable<Object>, SingleSource<Boolean>>() { + @Override + public SingleSource<Boolean> apply(Observable<Object> o) throws Exception { + return o.any(Functions.alwaysTrue()); + } + }); + } + + @Test + public void predicateThrowsSuppressOthers() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + new Observable<Integer>() { + @Override + protected void subscribeActual(Observer<? super Integer> observer) { + observer.onSubscribe(Disposable.empty()); + + observer.onNext(1); + observer.onNext(2); + observer.onError(new IOException()); + observer.onComplete(); + } + } + .any(new Predicate<Integer>() { + @Override + public boolean test(Integer v) throws Exception { + throw new TestException(); + } + }) + .toObservable() + .test() + .assertFailure(TestException.class); + + TestHelper.assertUndeliverable(errors, 0, IOException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void badSourceSingle() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + new Observable<Integer>() { + @Override + protected void subscribeActual(Observer<? super Integer> observer) { + observer.onSubscribe(Disposable.empty()); + observer.onError(new TestException("First")); + + observer.onNext(1); + observer.onError(new TestException("Second")); + observer.onComplete(); + } + } + .any(Functions.alwaysTrue()) + .to(TestHelper.<Boolean>testConsumer()) + .assertFailureAndMessage(TestException.class, "First"); + + TestHelper.assertUndeliverable(errors, 0, TestException.class, "Second"); + } finally { + RxJavaPlugins.reset(); + } + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableAutoConnectTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableAutoConnectTest.java new file mode 100644 index 0000000000..16128ff195 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableAutoConnectTest.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import static org.junit.Assert.assertTrue; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.RxJavaTest; +import io.reactivex.rxjava3.subjects.PublishSubject; + +public class ObservableAutoConnectTest extends RxJavaTest { + + @Test + public void autoConnectImmediately() { + PublishSubject<Integer> ps = PublishSubject.create(); + + ps.publish().autoConnect(0); + + assertTrue(ps.hasObservers()); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableBlockingTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableBlockingTest.java new file mode 100644 index 0000000000..05fbb5a94c --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableBlockingTest.java @@ -0,0 +1,322 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import static org.junit.Assert.*; + +import java.util.*; +import java.util.concurrent.TimeUnit; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.core.Observer; +import io.reactivex.rxjava3.core.RxJavaTest; +import io.reactivex.rxjava3.disposables.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.internal.observers.BlockingFirstObserver; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.schedulers.Schedulers; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class ObservableBlockingTest extends RxJavaTest { + + @Test + public void blockingFirst() { + assertEquals(1, Observable.range(1, 10) + .subscribeOn(Schedulers.computation()).blockingFirst().intValue()); + } + + @Test + public void blockingFirstDefault() { + assertEquals(1, Observable.<Integer>empty() + .subscribeOn(Schedulers.computation()).blockingFirst(1).intValue()); + } + + @Test + public void blockingSubscribeConsumer() { + final List<Integer> list = new ArrayList<>(); + + Observable.range(1, 5) + .subscribeOn(Schedulers.computation()) + .blockingSubscribe(new Consumer<Integer>() { + @Override + public void accept(Integer v) throws Exception { + list.add(v); + } + }); + + assertEquals(Arrays.asList(1, 2, 3, 4, 5), list); + } + + @Test + public void blockingSubscribeConsumerConsumer() { + final List<Object> list = new ArrayList<>(); + + Observable.range(1, 5) + .subscribeOn(Schedulers.computation()) + .blockingSubscribe(new Consumer<Integer>() { + @Override + public void accept(Integer v) throws Exception { + list.add(v); + } + }, Functions.emptyConsumer()); + + assertEquals(Arrays.asList(1, 2, 3, 4, 5), list); + } + + @Test + public void blockingSubscribeConsumerConsumerError() { + final List<Object> list = new ArrayList<>(); + + TestException ex = new TestException(); + + Consumer<Object> cons = new Consumer<Object>() { + @Override + public void accept(Object v) throws Exception { + list.add(v); + } + }; + + Observable.range(1, 5).concatWith(Observable.<Integer>error(ex)) + .subscribeOn(Schedulers.computation()) + .blockingSubscribe(cons, cons); + + assertEquals(Arrays.asList(1, 2, 3, 4, 5, ex), list); + } + + @Test + public void blockingSubscribeConsumerConsumerAction() { + final List<Object> list = new ArrayList<>(); + + Consumer<Object> cons = new Consumer<Object>() { + @Override + public void accept(Object v) throws Exception { + list.add(v); + } + }; + + Observable.range(1, 5) + .subscribeOn(Schedulers.computation()) + .blockingSubscribe(cons, cons, new Action() { + @Override + public void run() throws Exception { + list.add(100); + } + }); + + assertEquals(Arrays.asList(1, 2, 3, 4, 5, 100), list); + } + + @Test + public void blockingSubscribeObserver() { + final List<Object> list = new ArrayList<>(); + + Observable.range(1, 5) + .subscribeOn(Schedulers.computation()) + .blockingSubscribe(new Observer<Object>() { + + @Override + public void onSubscribe(Disposable d) { + + } + + @Override + public void onNext(Object value) { + list.add(value); + } + + @Override + public void onError(Throwable e) { + list.add(e); + } + + @Override + public void onComplete() { + list.add(100); + } + + }); + + assertEquals(Arrays.asList(1, 2, 3, 4, 5, 100), list); + } + + @Test + public void blockingSubscribeObserverError() { + final List<Object> list = new ArrayList<>(); + + final TestException ex = new TestException(); + + Observable.range(1, 5).concatWith(Observable.<Integer>error(ex)) + .subscribeOn(Schedulers.computation()) + .blockingSubscribe(new Observer<Object>() { + + @Override + public void onSubscribe(Disposable d) { + + } + + @Override + public void onNext(Object value) { + list.add(value); + } + + @Override + public void onError(Throwable e) { + list.add(e); + } + + @Override + public void onComplete() { + list.add(100); + } + + }); + + assertEquals(Arrays.asList(1, 2, 3, 4, 5, ex), list); + } + + @Test(expected = TestException.class) + public void blockingForEachThrows() { + Observable.just(1) + .blockingForEach(new Consumer<Integer>() { + @Override + public void accept(Integer e) throws Exception { + throw new TestException(); + } + }); + } + + @Test(expected = NoSuchElementException.class) + public void blockingFirstEmpty() { + Observable.empty().blockingFirst(); + } + + @Test(expected = NoSuchElementException.class) + public void blockingLastEmpty() { + Observable.empty().blockingLast(); + } + + @Test + public void blockingFirstNormal() { + assertEquals(1, Observable.just(1, 2).blockingFirst(3).intValue()); + } + + @Test + public void blockingLastNormal() { + assertEquals(2, Observable.just(1, 2).blockingLast(3).intValue()); + } + + @Test(expected = NoSuchElementException.class) + public void blockingSingleEmpty() { + Observable.empty().blockingSingle(); + } + + @Test + public void utilityClass() { + TestHelper.checkUtilityClass(ObservableBlockingSubscribe.class); + } + + @Test + public void disposeUpFront() { + TestObserver<Object> to = new TestObserver<>(); + to.dispose(); + Observable.just(1).blockingSubscribe(to); + + to.assertEmpty(); + } + + @SuppressWarnings("rawtypes") + @Test + public void delayed() throws Exception { + final TestObserver<Object> to = new TestObserver<>(); + final Observer[] s = { null }; + + Schedulers.single().scheduleDirect(new Runnable() { + @SuppressWarnings("unchecked") + @Override + public void run() { + to.dispose(); + s[0].onNext(1); + } + }, 200, TimeUnit.MILLISECONDS); + + new Observable<Integer>() { + @Override + protected void subscribeActual(Observer<? super Integer> observer) { + observer.onSubscribe(Disposable.empty()); + s[0] = observer; + } + }.blockingSubscribe(to); + + while (!to.isDisposed()) { + Thread.sleep(100); + } + + to.assertEmpty(); + } + + @Test + public void interrupt() { + TestObserver<Object> to = new TestObserver<>(); + Thread.currentThread().interrupt(); + Observable.never().blockingSubscribe(to); + } + + @Test + public void onCompleteDelayed() { + TestObserver<Object> to = new TestObserver<>(); + + Observable.empty().delay(100, TimeUnit.MILLISECONDS) + .blockingSubscribe(to); + + to.assertResult(); + } + + @Test + public void blockingCancelUpfront() { + BlockingFirstObserver<Integer> o = new BlockingFirstObserver<>(); + + assertFalse(o.isDisposed()); + o.dispose(); + assertTrue(o.isDisposed()); + + Disposable d = Disposable.empty(); + + o.onSubscribe(d); + + assertTrue(d.isDisposed()); + + Thread.currentThread().interrupt(); + try { + o.blockingGet(); + fail("Should have thrown"); + } catch (RuntimeException ex) { + assertTrue(ex.toString(), ex.getCause() instanceof InterruptedException); + } + + Thread.interrupted(); + + o.onError(new TestException()); + + try { + o.blockingGet(); + fail("Should have thrown"); + } catch (TestException ex) { + // expected + } + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableBufferTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableBufferTest.java new file mode 100644 index 0000000000..8264a66494 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableBufferTest.java @@ -0,0 +1,1818 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import java.io.IOException; +import java.util.*; +import java.util.concurrent.*; +import java.util.concurrent.atomic.*; + +import org.junit.*; +import org.mockito.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.core.Observer; +import io.reactivex.rxjava3.disposables.*; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.internal.operators.observable.ObservableBuffer.BufferExactObserver; +import io.reactivex.rxjava3.internal.operators.observable.ObservableBufferTimed.*; +import io.reactivex.rxjava3.observers.*; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.schedulers.*; +import io.reactivex.rxjava3.subjects.*; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class ObservableBufferTest extends RxJavaTest { + + private Observer<List<String>> observer; + private TestScheduler scheduler; + private Scheduler.Worker innerScheduler; + + @Before + public void before() { + observer = TestHelper.mockObserver(); + scheduler = new TestScheduler(); + innerScheduler = scheduler.createWorker(); + } + + @Test + public void complete() { + Observable<String> source = Observable.empty(); + + Observable<List<String>> buffered = source.buffer(3, 3); + buffered.subscribe(observer); + + Mockito.verify(observer, Mockito.never()).onNext(Mockito.<String>anyList()); + Mockito.verify(observer, Mockito.never()).onError(Mockito.any(Throwable.class)); + Mockito.verify(observer, Mockito.times(1)).onComplete(); + } + + @Test + public void skipAndCountOverlappingBuffers() { + Observable<String> source = Observable.unsafeCreate(new ObservableSource<String>() { + @Override + public void subscribe(Observer<? super String> observer) { + observer.onSubscribe(Disposable.empty()); + observer.onNext("one"); + observer.onNext("two"); + observer.onNext("three"); + observer.onNext("four"); + observer.onNext("five"); + } + }); + + Observable<List<String>> buffered = source.buffer(3, 1); + buffered.subscribe(observer); + + InOrder inOrder = Mockito.inOrder(observer); + inOrder.verify(observer, Mockito.times(1)).onNext(list("one", "two", "three")); + inOrder.verify(observer, Mockito.times(1)).onNext(list("two", "three", "four")); + inOrder.verify(observer, Mockito.times(1)).onNext(list("three", "four", "five")); + inOrder.verify(observer, Mockito.never()).onNext(Mockito.<String>anyList()); + inOrder.verify(observer, Mockito.never()).onError(Mockito.any(Throwable.class)); + inOrder.verify(observer, Mockito.never()).onComplete(); + } + + @Test + public void skipAndCountGaplessBuffers() { + Observable<String> source = Observable.just("one", "two", "three", "four", "five"); + + Observable<List<String>> buffered = source.buffer(3, 3); + buffered.subscribe(observer); + + InOrder inOrder = Mockito.inOrder(observer); + inOrder.verify(observer, Mockito.times(1)).onNext(list("one", "two", "three")); + inOrder.verify(observer, Mockito.times(1)).onNext(list("four", "five")); + inOrder.verify(observer, Mockito.never()).onNext(Mockito.<String>anyList()); + inOrder.verify(observer, Mockito.never()).onError(Mockito.any(Throwable.class)); + inOrder.verify(observer, Mockito.times(1)).onComplete(); + } + + @Test + public void skipAndCountBuffersWithGaps() { + Observable<String> source = Observable.just("one", "two", "three", "four", "five"); + + Observable<List<String>> buffered = source.buffer(2, 3); + buffered.subscribe(observer); + + InOrder inOrder = Mockito.inOrder(observer); + inOrder.verify(observer, Mockito.times(1)).onNext(list("one", "two")); + inOrder.verify(observer, Mockito.times(1)).onNext(list("four", "five")); + inOrder.verify(observer, Mockito.never()).onNext(Mockito.<String>anyList()); + inOrder.verify(observer, Mockito.never()).onError(Mockito.any(Throwable.class)); + inOrder.verify(observer, Mockito.times(1)).onComplete(); + } + + @Test + public void timedAndCount() { + Observable<String> source = Observable.unsafeCreate(new ObservableSource<String>() { + @Override + public void subscribe(Observer<? super String> observer) { + observer.onSubscribe(Disposable.empty()); + push(observer, "one", 10); + push(observer, "two", 90); + push(observer, "three", 110); + push(observer, "four", 190); + push(observer, "five", 210); + complete(observer, 250); + } + }); + + Observable<List<String>> buffered = source.buffer(100, TimeUnit.MILLISECONDS, scheduler, 2); + buffered.subscribe(observer); + + InOrder inOrder = Mockito.inOrder(observer); + scheduler.advanceTimeTo(100, TimeUnit.MILLISECONDS); + inOrder.verify(observer, Mockito.times(1)).onNext(list("one", "two")); + + scheduler.advanceTimeTo(200, TimeUnit.MILLISECONDS); + inOrder.verify(observer, Mockito.times(1)).onNext(list("three", "four")); + + scheduler.advanceTimeTo(300, TimeUnit.MILLISECONDS); + inOrder.verify(observer, Mockito.times(1)).onNext(list("five")); + inOrder.verify(observer, Mockito.never()).onNext(Mockito.<String>anyList()); + inOrder.verify(observer, Mockito.never()).onError(Mockito.any(Throwable.class)); + inOrder.verify(observer, Mockito.times(1)).onComplete(); + } + + @Test + public void timed() { + Observable<String> source = Observable.unsafeCreate(new ObservableSource<String>() { + @Override + public void subscribe(Observer<? super String> observer) { + observer.onSubscribe(Disposable.empty()); + push(observer, "one", 97); + push(observer, "two", 98); + /** + * Changed from 100. Because scheduling the cut to 100ms happens before this + * Observable even runs due how lift works, pushing at 100ms would execute after the + * buffer cut. + */ + push(observer, "three", 99); + push(observer, "four", 101); + push(observer, "five", 102); + complete(observer, 150); + } + }); + + Observable<List<String>> buffered = source.buffer(100, TimeUnit.MILLISECONDS, scheduler); + buffered.subscribe(observer); + + InOrder inOrder = Mockito.inOrder(observer); + scheduler.advanceTimeTo(101, TimeUnit.MILLISECONDS); + inOrder.verify(observer, Mockito.times(1)).onNext(list("one", "two", "three")); + + scheduler.advanceTimeTo(201, TimeUnit.MILLISECONDS); + inOrder.verify(observer, Mockito.times(1)).onNext(list("four", "five")); + inOrder.verify(observer, Mockito.never()).onNext(Mockito.<String>anyList()); + inOrder.verify(observer, Mockito.never()).onError(Mockito.any(Throwable.class)); + inOrder.verify(observer, Mockito.times(1)).onComplete(); + } + + @Test + public void observableBasedOpenerAndCloser() { + Observable<String> source = Observable.unsafeCreate(new ObservableSource<String>() { + @Override + public void subscribe(Observer<? super String> observer) { + observer.onSubscribe(Disposable.empty()); + push(observer, "one", 10); + push(observer, "two", 60); + push(observer, "three", 110); + push(observer, "four", 160); + push(observer, "five", 210); + complete(observer, 500); + } + }); + + Observable<Object> openings = Observable.unsafeCreate(new ObservableSource<Object>() { + @Override + public void subscribe(Observer<Object> observer) { + observer.onSubscribe(Disposable.empty()); + push(observer, new Object(), 50); + push(observer, new Object(), 200); + complete(observer, 250); + } + }); + + Function<Object, Observable<Object>> closer = new Function<Object, Observable<Object>>() { + @Override + public Observable<Object> apply(Object opening) { + return Observable.unsafeCreate(new ObservableSource<Object>() { + @Override + public void subscribe(Observer<? super Object> observer) { + observer.onSubscribe(Disposable.empty()); + push(observer, new Object(), 100); + complete(observer, 101); + } + }); + } + }; + + Observable<List<String>> buffered = source.buffer(openings, closer); + buffered.subscribe(observer); + + InOrder inOrder = Mockito.inOrder(observer); + scheduler.advanceTimeTo(500, TimeUnit.MILLISECONDS); + inOrder.verify(observer, Mockito.times(1)).onNext(list("two", "three")); + inOrder.verify(observer, Mockito.times(1)).onNext(list("five")); + inOrder.verify(observer, Mockito.never()).onNext(Mockito.<String>anyList()); + inOrder.verify(observer, Mockito.never()).onError(Mockito.any(Throwable.class)); + inOrder.verify(observer, Mockito.times(1)).onComplete(); + } + + @Test + public void longTimeAction() throws InterruptedException { + final CountDownLatch latch = new CountDownLatch(1); + LongTimeAction action = new LongTimeAction(latch); + Observable.just(1).buffer(10, TimeUnit.MILLISECONDS, 10) + .subscribe(action); + latch.await(); + assertFalse(action.fail); + } + + private static class LongTimeAction implements Consumer<List<Integer>> { + + CountDownLatch latch; + boolean fail; + + LongTimeAction(CountDownLatch latch) { + this.latch = latch; + } + + @Override + public void accept(List<Integer> t1) { + try { + if (fail) { + return; + } + Thread.sleep(200); + } catch (InterruptedException e) { + fail = true; + } finally { + latch.countDown(); + } + } + } + + private List<String> list(String... args) { + List<String> list = new ArrayList<>(); + for (String arg : args) { + list.add(arg); + } + return list; + } + + private <T> void push(final Observer<T> observer, final T value, int delay) { + innerScheduler.schedule(new Runnable() { + @Override + public void run() { + observer.onNext(value); + } + }, delay, TimeUnit.MILLISECONDS); + } + + private void complete(final Observer<?> observer, int delay) { + innerScheduler.schedule(new Runnable() { + @Override + public void run() { + observer.onComplete(); + } + }, delay, TimeUnit.MILLISECONDS); + } + + @Test + public void bufferStopsWhenUnsubscribed1() { + Observable<Integer> source = Observable.never(); + + Observer<List<Integer>> o = TestHelper.mockObserver(); + TestObserver<List<Integer>> to = new TestObserver<>(o); + + source.buffer(100, 200, TimeUnit.MILLISECONDS, scheduler) + .doOnNext(new Consumer<List<Integer>>() { + @Override + public void accept(List<Integer> pv) { + System.out.println(pv); + } + }) + .subscribe(to); + + InOrder inOrder = Mockito.inOrder(o); + + scheduler.advanceTimeBy(1001, TimeUnit.MILLISECONDS); + + inOrder.verify(o, times(5)).onNext(Arrays.<Integer> asList()); + + to.dispose(); + + scheduler.advanceTimeBy(999, TimeUnit.MILLISECONDS); + + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void bufferWithBONormal1() { + PublishSubject<Integer> source = PublishSubject.create(); + PublishSubject<Integer> boundary = PublishSubject.create(); + + Observer<Object> o = TestHelper.mockObserver(); + InOrder inOrder = Mockito.inOrder(o); + + source.buffer(boundary).subscribe(o); + + source.onNext(1); + source.onNext(2); + source.onNext(3); + + boundary.onNext(1); + + inOrder.verify(o, times(1)).onNext(Arrays.asList(1, 2, 3)); + + source.onNext(4); + source.onNext(5); + + boundary.onNext(2); + + inOrder.verify(o, times(1)).onNext(Arrays.asList(4, 5)); + + source.onNext(6); + boundary.onComplete(); + + inOrder.verify(o, times(1)).onNext(Arrays.asList(6)); + + inOrder.verify(o).onComplete(); + + verify(o, never()).onError(any(Throwable.class)); + } + + @Test + public void bufferWithBOEmptyLastViaBoundary() { + PublishSubject<Integer> source = PublishSubject.create(); + PublishSubject<Integer> boundary = PublishSubject.create(); + + Observer<Object> o = TestHelper.mockObserver(); + InOrder inOrder = Mockito.inOrder(o); + + source.buffer(boundary).subscribe(o); + + boundary.onComplete(); + + inOrder.verify(o, times(1)).onNext(Arrays.asList()); + + inOrder.verify(o).onComplete(); + + verify(o, never()).onError(any(Throwable.class)); + } + + @Test + public void bufferWithBOEmptyLastViaSource() { + PublishSubject<Integer> source = PublishSubject.create(); + PublishSubject<Integer> boundary = PublishSubject.create(); + + Observer<Object> o = TestHelper.mockObserver(); + InOrder inOrder = Mockito.inOrder(o); + + source.buffer(boundary).subscribe(o); + + source.onComplete(); + + inOrder.verify(o, times(1)).onNext(Arrays.asList()); + + inOrder.verify(o).onComplete(); + + verify(o, never()).onError(any(Throwable.class)); + } + + @Test + public void bufferWithBOEmptyLastViaBoth() { + PublishSubject<Integer> source = PublishSubject.create(); + PublishSubject<Integer> boundary = PublishSubject.create(); + + Observer<Object> o = TestHelper.mockObserver(); + InOrder inOrder = Mockito.inOrder(o); + + source.buffer(boundary).subscribe(o); + + source.onComplete(); + boundary.onComplete(); + + inOrder.verify(o, times(1)).onNext(Arrays.asList()); + + inOrder.verify(o).onComplete(); + + verify(o, never()).onError(any(Throwable.class)); + } + + @Test + public void bufferWithBOSourceThrows() { + PublishSubject<Integer> source = PublishSubject.create(); + PublishSubject<Integer> boundary = PublishSubject.create(); + + Observer<Object> o = TestHelper.mockObserver(); + + source.buffer(boundary).subscribe(o); + source.onNext(1); + source.onError(new TestException()); + + verify(o).onError(any(TestException.class)); + verify(o, never()).onComplete(); + verify(o, never()).onNext(any()); + } + + @Test + public void bufferWithBOBoundaryThrows() { + PublishSubject<Integer> source = PublishSubject.create(); + PublishSubject<Integer> boundary = PublishSubject.create(); + + Observer<Object> o = TestHelper.mockObserver(); + + source.buffer(boundary).subscribe(o); + + source.onNext(1); + boundary.onError(new TestException()); + + verify(o).onError(any(TestException.class)); + verify(o, never()).onComplete(); + verify(o, never()).onNext(any()); + } + + @Test + public void bufferWithSizeTake1() { + Observable<Integer> source = Observable.just(1).repeat(); + + Observable<List<Integer>> result = source.buffer(2).take(1); + + Observer<Object> o = TestHelper.mockObserver(); + + result.subscribe(o); + + verify(o).onNext(Arrays.asList(1, 1)); + verify(o).onComplete(); + verify(o, never()).onError(any(Throwable.class)); + } + + @Test + public void bufferWithSizeSkipTake1() { + Observable<Integer> source = Observable.just(1).repeat(); + + Observable<List<Integer>> result = source.buffer(2, 3).take(1); + + Observer<Object> o = TestHelper.mockObserver(); + + result.subscribe(o); + + verify(o).onNext(Arrays.asList(1, 1)); + verify(o).onComplete(); + verify(o, never()).onError(any(Throwable.class)); + } + + @Test + public void bufferWithTimeTake1() { + Observable<Long> source = Observable.interval(40, 40, TimeUnit.MILLISECONDS, scheduler); + + Observable<List<Long>> result = source.buffer(100, TimeUnit.MILLISECONDS, scheduler).take(1); + + Observer<Object> o = TestHelper.mockObserver(); + + result.subscribe(o); + + scheduler.advanceTimeBy(5, TimeUnit.SECONDS); + + verify(o).onNext(Arrays.asList(0L, 1L)); + verify(o).onComplete(); + verify(o, never()).onError(any(Throwable.class)); + } + + @Test + public void bufferWithTimeSkipTake2() { + Observable<Long> source = Observable.interval(40, 40, TimeUnit.MILLISECONDS, scheduler); + + Observable<List<Long>> result = source.buffer(100, 60, TimeUnit.MILLISECONDS, scheduler).take(2); + + Observer<Object> o = TestHelper.mockObserver(); + InOrder inOrder = inOrder(o); + + result.subscribe(o); + + scheduler.advanceTimeBy(5, TimeUnit.SECONDS); + + inOrder.verify(o).onNext(Arrays.asList(0L, 1L)); + inOrder.verify(o).onNext(Arrays.asList(1L, 2L)); + inOrder.verify(o).onComplete(); + verify(o, never()).onError(any(Throwable.class)); + } + + @Test + public void bufferWithBoundaryTake2() { + Observable<Long> boundary = Observable.interval(60, 60, TimeUnit.MILLISECONDS, scheduler); + Observable<Long> source = Observable.interval(40, 40, TimeUnit.MILLISECONDS, scheduler); + + Observable<List<Long>> result = source.buffer(boundary).take(2); + + Observer<Object> o = TestHelper.mockObserver(); + InOrder inOrder = inOrder(o); + + result.subscribe(o); + + scheduler.advanceTimeBy(5, TimeUnit.SECONDS); + + inOrder.verify(o).onNext(Arrays.asList(0L)); + inOrder.verify(o).onNext(Arrays.asList(1L)); + inOrder.verify(o).onComplete(); + verify(o, never()).onError(any(Throwable.class)); + + } + + @Test + public void bufferWithStartEndBoundaryTake2() { + Observable<Long> start = Observable.interval(61, 61, TimeUnit.MILLISECONDS, scheduler); + Function<Long, Observable<Long>> end = new Function<Long, Observable<Long>>() { + @Override + public Observable<Long> apply(Long t1) { + return Observable.interval(100, 100, TimeUnit.MILLISECONDS, scheduler); + } + }; + + Observable<Long> source = Observable.interval(40, 40, TimeUnit.MILLISECONDS, scheduler); + + Observable<List<Long>> result = source.buffer(start, end).take(2); + + Observer<Object> o = TestHelper.mockObserver(); + InOrder inOrder = inOrder(o); + + result + .doOnNext(new Consumer<List<Long>>() { + @Override + public void accept(List<Long> pv) { + System.out.println(pv); + } + }) + .subscribe(o); + + scheduler.advanceTimeBy(5, TimeUnit.SECONDS); + + inOrder.verify(o).onNext(Arrays.asList(1L, 2L, 3L)); + inOrder.verify(o).onNext(Arrays.asList(3L, 4L)); + inOrder.verify(o).onComplete(); + verify(o, never()).onError(any(Throwable.class)); + } + + @Test + public void bufferWithSizeThrows() { + PublishSubject<Integer> source = PublishSubject.create(); + + Observable<List<Integer>> result = source.buffer(2); + + Observer<Object> o = TestHelper.mockObserver(); + + InOrder inOrder = inOrder(o); + + result.subscribe(o); + + source.onNext(1); + source.onNext(2); + source.onNext(3); + source.onError(new TestException()); + + inOrder.verify(o).onNext(Arrays.asList(1, 2)); + inOrder.verify(o).onError(any(TestException.class)); + inOrder.verifyNoMoreInteractions(); + verify(o, never()).onNext(Arrays.asList(3)); + verify(o, never()).onComplete(); + + } + + @Test + public void bufferWithTimeThrows() { + PublishSubject<Integer> source = PublishSubject.create(); + + Observable<List<Integer>> result = source.buffer(100, TimeUnit.MILLISECONDS, scheduler); + + Observer<Object> o = TestHelper.mockObserver(); + InOrder inOrder = inOrder(o); + + result.subscribe(o); + + source.onNext(1); + source.onNext(2); + scheduler.advanceTimeBy(100, TimeUnit.MILLISECONDS); + source.onNext(3); + source.onError(new TestException()); + scheduler.advanceTimeBy(100, TimeUnit.MILLISECONDS); + + inOrder.verify(o).onNext(Arrays.asList(1, 2)); + inOrder.verify(o).onError(any(TestException.class)); + inOrder.verifyNoMoreInteractions(); + verify(o, never()).onNext(Arrays.asList(3)); + verify(o, never()).onComplete(); + + } + + @Test + public void bufferWithTimeAndSize() { + Observable<Long> source = Observable.interval(30, 30, TimeUnit.MILLISECONDS, scheduler); + + Observable<List<Long>> result = source.buffer(100, TimeUnit.MILLISECONDS, scheduler, 2).take(3); + + Observer<Object> o = TestHelper.mockObserver(); + InOrder inOrder = inOrder(o); + + result.subscribe(o); + + scheduler.advanceTimeBy(5, TimeUnit.SECONDS); + + inOrder.verify(o).onNext(Arrays.asList(0L, 1L)); + inOrder.verify(o).onNext(Arrays.asList(2L)); + inOrder.verify(o).onComplete(); + verify(o, never()).onError(any(Throwable.class)); + } + + @Test + public void bufferWithStartEndStartThrows() { + PublishSubject<Integer> start = PublishSubject.create(); + + Function<Integer, Observable<Integer>> end = new Function<Integer, Observable<Integer>>() { + @Override + public Observable<Integer> apply(Integer t1) { + return Observable.never(); + } + }; + + PublishSubject<Integer> source = PublishSubject.create(); + + Observable<List<Integer>> result = source.buffer(start, end); + + Observer<Object> o = TestHelper.mockObserver(); + + result.subscribe(o); + + start.onNext(1); + source.onNext(1); + source.onNext(2); + start.onError(new TestException()); + + verify(o, never()).onNext(any()); + verify(o, never()).onComplete(); + verify(o).onError(any(TestException.class)); + } + + @Test + public void bufferWithStartEndEndFunctionThrows() { + PublishSubject<Integer> start = PublishSubject.create(); + + Function<Integer, Observable<Integer>> end = new Function<Integer, Observable<Integer>>() { + @Override + public Observable<Integer> apply(Integer t1) { + throw new TestException(); + } + }; + + PublishSubject<Integer> source = PublishSubject.create(); + + Observable<List<Integer>> result = source.buffer(start, end); + + Observer<Object> o = TestHelper.mockObserver(); + + result.subscribe(o); + + start.onNext(1); + source.onNext(1); + source.onNext(2); + + verify(o, never()).onNext(any()); + verify(o, never()).onComplete(); + verify(o).onError(any(TestException.class)); + } + + @Test + public void bufferWithStartEndEndThrows() { + PublishSubject<Integer> start = PublishSubject.create(); + + Function<Integer, Observable<Integer>> end = new Function<Integer, Observable<Integer>>() { + @Override + public Observable<Integer> apply(Integer t1) { + return Observable.error(new TestException()); + } + }; + + PublishSubject<Integer> source = PublishSubject.create(); + + Observable<List<Integer>> result = source.buffer(start, end); + + Observer<Object> o = TestHelper.mockObserver(); + + result.subscribe(o); + + start.onNext(1); + source.onNext(1); + source.onNext(2); + + verify(o, never()).onNext(any()); + verify(o, never()).onComplete(); + verify(o).onError(any(TestException.class)); + } + + @Test + public void bufferWithTimeDoesntUnsubscribeDownstream() throws InterruptedException { + final Observer<Object> o = TestHelper.mockObserver(); + + final CountDownLatch cdl = new CountDownLatch(1); + DisposableObserver<Object> observer = new DisposableObserver<Object>() { + @Override + public void onNext(Object t) { + o.onNext(t); + } + + @Override + public void onError(Throwable e) { + o.onError(e); + cdl.countDown(); + } + + @Override + public void onComplete() { + o.onComplete(); + cdl.countDown(); + } + }; + + Observable.range(1, 1).delay(1, TimeUnit.SECONDS).buffer(2, TimeUnit.SECONDS).subscribe(observer); + + cdl.await(); + + verify(o).onNext(Arrays.asList(1)); + verify(o).onComplete(); + verify(o, never()).onError(any(Throwable.class)); + + assertFalse(observer.isDisposed()); + } + + @Test + public void bufferTimeSkipDefault() { + Observable.range(1, 5).buffer(1, 1, TimeUnit.MINUTES) + .test() + .assertResult(Arrays.asList(1, 2, 3, 4, 5)); + } + + @Test + public void bufferBoundaryHint() { + Observable.range(1, 5).buffer(Observable.timer(1, TimeUnit.MINUTES), 2) + .test() + .assertResult(Arrays.asList(1, 2, 3, 4, 5)); + } + + static HashSet<Integer> set(Integer... values) { + return new HashSet<>(Arrays.asList(values)); + } + + @Test + public void bufferIntoCustomCollection() { + Observable.just(1, 1, 2, 2, 3, 3, 4, 4) + .buffer(3, new Supplier<Collection<Integer>>() { + @Override + public Collection<Integer> get() throws Exception { + return new HashSet<>(); + } + }) + .test() + .assertResult(set(1, 2), set(2, 3), set(4)); + } + + @Test + public void bufferSkipIntoCustomCollection() { + Observable.just(1, 1, 2, 2, 3, 3, 4, 4) + .buffer(3, 3, new Supplier<Collection<Integer>>() { + @Override + public Collection<Integer> get() throws Exception { + return new HashSet<>(); + } + }) + .test() + .assertResult(set(1, 2), set(2, 3), set(4)); + } + + @Test + public void supplierThrows() { + Observable.just(1) + .buffer(1, TimeUnit.SECONDS, Schedulers.single(), Integer.MAX_VALUE, new Supplier<Collection<Integer>>() { + @Override + public Collection<Integer> get() throws Exception { + throw new TestException(); + } + }, false) + .test() + .assertFailure(TestException.class); + } + + @Test + public void supplierThrows2() { + Observable.just(1) + .buffer(1, TimeUnit.SECONDS, Schedulers.single(), 10, new Supplier<Collection<Integer>>() { + @Override + public Collection<Integer> get() throws Exception { + throw new TestException(); + } + }, false) + .test() + .assertFailure(TestException.class); + } + + @Test + public void supplierThrows3() { + Observable.just(1) + .buffer(2, 1, TimeUnit.SECONDS, Schedulers.single(), new Supplier<Collection<Integer>>() { + @Override + public Collection<Integer> get() throws Exception { + throw new TestException(); + } + }) + .test() + .assertFailure(TestException.class); + } + + @Test + public void supplierThrows4() { + Observable.<Integer>never() + .buffer(1, TimeUnit.MILLISECONDS, Schedulers.single(), Integer.MAX_VALUE, new Supplier<Collection<Integer>>() { + int count; + @Override + public Collection<Integer> get() throws Exception { + if (count++ == 1) { + throw new TestException(); + } else { + return new ArrayList<>(); + } + } + }, false) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertFailure(TestException.class); + } + + @Test + public void supplierThrows5() { + Observable.<Integer>never() + .buffer(1, TimeUnit.MILLISECONDS, Schedulers.single(), 10, new Supplier<Collection<Integer>>() { + int count; + @Override + public Collection<Integer> get() throws Exception { + if (count++ == 1) { + throw new TestException(); + } else { + return new ArrayList<>(); + } + } + }, false) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertFailure(TestException.class); + } + + @Test + public void supplierThrows6() { + Observable.<Integer>never() + .buffer(2, 1, TimeUnit.MILLISECONDS, Schedulers.single(), new Supplier<Collection<Integer>>() { + int count; + @Override + public Collection<Integer> get() throws Exception { + if (count++ == 1) { + throw new TestException(); + } else { + return new ArrayList<>(); + } + } + }) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertFailure(TestException.class); + } + + @Test + public void supplierReturnsNull() { + Observable.<Integer>never() + .buffer(1, TimeUnit.MILLISECONDS, Schedulers.single(), Integer.MAX_VALUE, new Supplier<Collection<Integer>>() { + int count; + @Override + public Collection<Integer> get() throws Exception { + if (count++ == 1) { + return null; + } else { + return new ArrayList<>(); + } + } + }, false) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertFailure(NullPointerException.class); + } + + @Test + public void supplierReturnsNull2() { + Observable.<Integer>never() + .buffer(1, TimeUnit.MILLISECONDS, Schedulers.single(), 10, new Supplier<Collection<Integer>>() { + int count; + @Override + public Collection<Integer> get() throws Exception { + if (count++ == 1) { + return null; + } else { + return new ArrayList<>(); + } + } + }, false) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertFailure(NullPointerException.class); + } + + @Test + public void supplierReturnsNull3() { + Observable.<Integer>never() + .buffer(2, 1, TimeUnit.MILLISECONDS, Schedulers.single(), new Supplier<Collection<Integer>>() { + int count; + @Override + public Collection<Integer> get() throws Exception { + if (count++ == 1) { + return null; + } else { + return new ArrayList<>(); + } + } + }) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertFailure(NullPointerException.class); + } + + @Test + public void dispose() { + TestHelper.checkDisposed(Observable.range(1, 5).buffer(1, TimeUnit.DAYS, Schedulers.single())); + + TestHelper.checkDisposed(Observable.range(1, 5).buffer(2, 1, TimeUnit.DAYS, Schedulers.single())); + + TestHelper.checkDisposed(Observable.range(1, 5).buffer(1, 2, TimeUnit.DAYS, Schedulers.single())); + + TestHelper.checkDisposed(Observable.range(1, 5) + .buffer(1, TimeUnit.DAYS, Schedulers.single(), 2, Functions.<Integer>createArrayList(16), true)); + + TestHelper.checkDisposed(Observable.range(1, 5).buffer(1)); + + TestHelper.checkDisposed(Observable.range(1, 5).buffer(2, 1)); + + TestHelper.checkDisposed(Observable.range(1, 5).buffer(1, 2)); + + TestHelper.checkDisposed(PublishSubject.create().buffer(Observable.never())); + + TestHelper.checkDisposed(PublishSubject.create().buffer(Observable.never(), Functions.justFunction(Observable.never()))); + } + + @Test + public void restartTimer() { + Observable.range(1, 5) + .buffer(1, TimeUnit.DAYS, Schedulers.single(), 2, Functions.<Integer>createArrayList(16), true) + .test() + .assertResult(Arrays.asList(1, 2), Arrays.asList(3, 4), Arrays.asList(5)); + } + + @Test + public void bufferSupplierCrash2() { + Observable.range(1, 2) + .buffer(1, new Supplier<List<Integer>>() { + int calls; + @Override + public List<Integer> get() throws Exception { + if (++calls == 2) { + throw new TestException(); + } + return new ArrayList<>(); + } + }) + .test() + .assertFailure(TestException.class, Arrays.asList(1)); + } + + @Test + public void bufferSkipSupplierCrash2() { + Observable.range(1, 2) + .buffer(2, 1, new Supplier<List<Integer>>() { + int calls; + @Override + public List<Integer> get() throws Exception { + if (++calls == 2) { + throw new TestException(); + } + return new ArrayList<>(); + } + }) + .test() + .assertFailure(TestException.class); + } + + @Test + public void bufferSkipError() { + Observable.<Integer>error(new TestException()) + .buffer(2, 1) + .test() + .assertFailure(TestException.class); + } + + @Test + public void bufferSkipOverlap() { + Observable.range(1, 5) + .buffer(5, 1) + .test() + .assertResult( + Arrays.asList(1, 2, 3, 4, 5), + Arrays.asList(2, 3, 4, 5), + Arrays.asList(3, 4, 5), + Arrays.asList(4, 5), + Arrays.asList(5) + ); + } + + @Test + public void bufferTimedExactError() { + Observable.error(new TestException()) + .buffer(1, TimeUnit.DAYS) + .test() + .assertFailure(TestException.class); + } + + @Test + public void bufferTimedSkipError() { + Observable.error(new TestException()) + .buffer(1, 2, TimeUnit.DAYS) + .test() + .assertFailure(TestException.class); + } + + @Test + public void bufferTimedOverlapError() { + Observable.error(new TestException()) + .buffer(2, 1, TimeUnit.DAYS) + .test() + .assertFailure(TestException.class); + } + + @Test + public void bufferTimedExactEmpty() { + Observable.empty() + .buffer(1, TimeUnit.DAYS) + .test() + .assertResult(Collections.emptyList()); + } + + @Test + public void bufferTimedSkipEmpty() { + Observable.empty() + .buffer(1, 2, TimeUnit.DAYS) + .test() + .assertResult(Collections.emptyList()); + } + + @Test + public void bufferTimedOverlapEmpty() { + Observable.empty() + .buffer(2, 1, TimeUnit.DAYS) + .test() + .assertResult(Collections.emptyList()); + } + + @Test + public void bufferTimedExactSupplierCrash() { + TestScheduler scheduler = new TestScheduler(); + + PublishSubject<Integer> ps = PublishSubject.create(); + + TestObserver<List<Integer>> to = ps + .buffer(1, TimeUnit.MILLISECONDS, scheduler, 1, new Supplier<List<Integer>>() { + int calls; + @Override + public List<Integer> get() throws Exception { + if (++calls == 2) { + throw new TestException(); + } + return new ArrayList<>(); + } + }, true) + .test(); + + ps.onNext(1); + + scheduler.advanceTimeBy(1, TimeUnit.MILLISECONDS); + + ps.onNext(2); + + to + .assertFailure(TestException.class, Arrays.asList(1)); + } + + @Test + public void bufferTimedExactBoundedError() { + TestScheduler scheduler = new TestScheduler(); + + PublishSubject<Integer> ps = PublishSubject.create(); + + TestObserver<List<Integer>> to = ps + .buffer(1, TimeUnit.MILLISECONDS, scheduler, 1, Functions.<Integer>createArrayList(16), true) + .test(); + + ps.onError(new TestException()); + + to + .assertFailure(TestException.class); + } + + @Test + public void withTimeAndSizeCapacityRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final TestScheduler scheduler = new TestScheduler(); + + final PublishSubject<Object> ps = PublishSubject.create(); + + TestObserver<List<Object>> to = ps.buffer(1, TimeUnit.SECONDS, scheduler, 5).test(); + + ps.onNext(1); + ps.onNext(2); + ps.onNext(3); + ps.onNext(4); + + Runnable r1 = new Runnable() { + @Override + public void run() { + ps.onNext(5); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + scheduler.advanceTimeBy(1, TimeUnit.SECONDS); + } + }; + + TestHelper.race(r1, r2); + + ps.onComplete(); + + int items = 0; + for (List<Object> o : to.values()) { + items += o.size(); + } + + assertEquals("Round: " + i, 5, items); + } + } + + @Test + public void noCompletionCancelExact() { + final AtomicInteger counter = new AtomicInteger(); + + Observable.<Integer>empty() + .doOnDispose(new Action() { + @Override + public void run() throws Exception { + counter.getAndIncrement(); + } + }) + .buffer(5, TimeUnit.SECONDS) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(Collections.<Integer>emptyList()); + + assertEquals(0, counter.get()); + } + + @Test + public void noCompletionCancelSkip() { + final AtomicInteger counter = new AtomicInteger(); + + Observable.<Integer>empty() + .doOnDispose(new Action() { + @Override + public void run() throws Exception { + counter.getAndIncrement(); + } + }) + .buffer(5, 10, TimeUnit.SECONDS) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(Collections.<Integer>emptyList()); + + assertEquals(0, counter.get()); + } + + @Test + public void noCompletionCancelOverlap() { + final AtomicInteger counter = new AtomicInteger(); + + Observable.<Integer>empty() + .doOnDispose(new Action() { + @Override + public void run() throws Exception { + counter.getAndIncrement(); + } + }) + .buffer(10, 5, TimeUnit.SECONDS) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(Collections.<Integer>emptyList()); + + assertEquals(0, counter.get()); + } + + @Test + public void boundaryOpenCloseDisposedOnComplete() { + PublishSubject<Integer> source = PublishSubject.create(); + + PublishSubject<Integer> openIndicator = PublishSubject.create(); + + PublishSubject<Integer> closeIndicator = PublishSubject.create(); + + TestObserver<List<Integer>> to = source + .buffer(openIndicator, Functions.justFunction(closeIndicator)) + .test(); + + assertTrue(source.hasObservers()); + assertTrue(openIndicator.hasObservers()); + assertFalse(closeIndicator.hasObservers()); + + openIndicator.onNext(1); + + assertTrue(openIndicator.hasObservers()); + assertTrue(closeIndicator.hasObservers()); + + source.onComplete(); + + to.assertResult(Collections.<Integer>emptyList()); + + assertFalse(openIndicator.hasObservers()); + assertFalse(closeIndicator.hasObservers()); + } + + @Test + public void bufferedCanCompleteIfOpenNeverCompletesDropping() { + Observable.range(1, 50) + .zipWith(Observable.interval(5, TimeUnit.MILLISECONDS), + new BiFunction<Integer, Long, Integer>() { + @Override + public Integer apply(Integer integer, Long aLong) { + return integer; + } + }) + .buffer(Observable.interval(0, 200, TimeUnit.MILLISECONDS), + new Function<Long, Observable<?>>() { + @Override + public Observable<?> apply(Long a) { + return Observable.just(a).delay(100, TimeUnit.MILLISECONDS); + } + }) + .to(TestHelper.<List<Integer>>testConsumer()) + .assertSubscribed() + .awaitDone(3, TimeUnit.SECONDS) + .assertComplete(); + } + + @Test + public void bufferedCanCompleteIfOpenNeverCompletesOverlapping() { + Observable.range(1, 50) + .zipWith(Observable.interval(5, TimeUnit.MILLISECONDS), + new BiFunction<Integer, Long, Integer>() { + @Override + public Integer apply(Integer integer, Long aLong) { + return integer; + } + }) + .buffer(Observable.interval(0, 100, TimeUnit.MILLISECONDS), + new Function<Long, Observable<?>>() { + @Override + public Observable<?> apply(Long a) { + return Observable.just(a).delay(200, TimeUnit.MILLISECONDS); + } + }) + .to(TestHelper.<List<Integer>>testConsumer()) + .assertSubscribed() + .awaitDone(3, TimeUnit.SECONDS) + .assertComplete(); + } + + @Test + public void openClosemainError() { + Observable.error(new TestException()) + .buffer(Observable.never(), Functions.justFunction(Observable.never())) + .test() + .assertFailure(TestException.class); + } + + @Test + public void openClosebadSource() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + new Observable<Object>() { + @Override + protected void subscribeActual(Observer<? super Object> observer) { + Disposable bs1 = Disposable.empty(); + Disposable bs2 = Disposable.empty(); + + observer.onSubscribe(bs1); + + assertFalse(bs1.isDisposed()); + assertFalse(bs2.isDisposed()); + + observer.onSubscribe(bs2); + + assertFalse(bs1.isDisposed()); + assertTrue(bs2.isDisposed()); + + observer.onError(new IOException()); + observer.onComplete(); + observer.onNext(1); + observer.onError(new TestException()); + } + } + .buffer(Observable.never(), Functions.justFunction(Observable.never())) + .test() + .assertFailure(IOException.class); + + TestHelper.assertError(errors, 0, ProtocolViolationException.class); + TestHelper.assertUndeliverable(errors, 1, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void openCloseOpenCompletes() { + PublishSubject<Integer> source = PublishSubject.create(); + + PublishSubject<Integer> openIndicator = PublishSubject.create(); + + PublishSubject<Integer> closeIndicator = PublishSubject.create(); + + TestObserver<List<Integer>> to = source + .buffer(openIndicator, Functions.justFunction(closeIndicator)) + .test(); + + openIndicator.onNext(1); + + assertTrue(closeIndicator.hasObservers()); + + openIndicator.onComplete(); + + assertTrue(source.hasObservers()); + assertTrue(closeIndicator.hasObservers()); + + closeIndicator.onComplete(); + + assertFalse(source.hasObservers()); + + to.assertResult(Collections.<Integer>emptyList()); + } + + @Test + public void openCloseOpenCompletesNoBuffers() { + PublishSubject<Integer> source = PublishSubject.create(); + + PublishSubject<Integer> openIndicator = PublishSubject.create(); + + PublishSubject<Integer> closeIndicator = PublishSubject.create(); + + TestObserver<List<Integer>> to = source + .buffer(openIndicator, Functions.justFunction(closeIndicator)) + .test(); + + openIndicator.onNext(1); + + assertTrue(closeIndicator.hasObservers()); + + closeIndicator.onComplete(); + + assertTrue(source.hasObservers()); + assertTrue(openIndicator.hasObservers()); + + openIndicator.onComplete(); + + assertFalse(source.hasObservers()); + + to.assertResult(Collections.<Integer>emptyList()); + } + + @Test + public void openCloseTake() { + PublishSubject<Integer> source = PublishSubject.create(); + + PublishSubject<Integer> openIndicator = PublishSubject.create(); + + PublishSubject<Integer> closeIndicator = PublishSubject.create(); + + TestObserver<List<Integer>> to = source + .buffer(openIndicator, Functions.justFunction(closeIndicator)) + .take(1) + .test(); + + openIndicator.onNext(1); + closeIndicator.onComplete(); + + assertFalse(source.hasObservers()); + assertFalse(openIndicator.hasObservers()); + assertFalse(closeIndicator.hasObservers()); + + to.assertResult(Collections.<Integer>emptyList()); + } + + @Test + public void openCloseBadOpen() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + Observable.never() + .buffer(new Observable<Object>() { + @Override + protected void subscribeActual(Observer<? super Object> observer) { + + assertFalse(((Disposable)observer).isDisposed()); + + Disposable bs1 = Disposable.empty(); + Disposable bs2 = Disposable.empty(); + + observer.onSubscribe(bs1); + + assertFalse(bs1.isDisposed()); + assertFalse(bs2.isDisposed()); + + observer.onSubscribe(bs2); + + assertFalse(bs1.isDisposed()); + assertTrue(bs2.isDisposed()); + + observer.onError(new IOException()); + + assertTrue(((Disposable)observer).isDisposed()); + + observer.onComplete(); + observer.onNext(1); + observer.onError(new TestException()); + } + }, Functions.justFunction(Observable.never())) + .test() + .assertFailure(IOException.class); + + TestHelper.assertError(errors, 0, ProtocolViolationException.class); + TestHelper.assertUndeliverable(errors, 1, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void openCloseBadClose() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + Observable.never() + .buffer(Observable.just(1).concatWith(Observable.<Integer>never()), + Functions.justFunction(new Observable<Object>() { + @Override + protected void subscribeActual(Observer<? super Object> observer) { + + assertFalse(((Disposable)observer).isDisposed()); + + Disposable bs1 = Disposable.empty(); + Disposable bs2 = Disposable.empty(); + + observer.onSubscribe(bs1); + + assertFalse(bs1.isDisposed()); + assertFalse(bs2.isDisposed()); + + observer.onSubscribe(bs2); + + assertFalse(bs1.isDisposed()); + assertTrue(bs2.isDisposed()); + + observer.onError(new IOException()); + + assertTrue(((Disposable)observer).isDisposed()); + + observer.onComplete(); + observer.onNext(1); + observer.onError(new TestException()); + } + })) + .test() + .assertFailure(IOException.class); + + TestHelper.assertError(errors, 0, ProtocolViolationException.class); + TestHelper.assertUndeliverable(errors, 1, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void bufferExactBoundaryDoubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeObservable( + new Function<Observable<Object>, ObservableSource<List<Object>>>() { + @Override + public ObservableSource<List<Object>> apply(Observable<Object> f) + throws Exception { + return f.buffer(Observable.never()); + } + } + ); + } + + @Test + public void bufferExactBoundarySecondBufferCrash() { + PublishSubject<Integer> ps = PublishSubject.create(); + PublishSubject<Integer> b = PublishSubject.create(); + + TestObserver<List<Integer>> to = ps.buffer(b, new Supplier<List<Integer>>() { + int calls; + @Override + public List<Integer> get() throws Exception { + if (++calls == 2) { + throw new TestException(); + } + return new ArrayList<>(); + } + }).test(); + + b.onNext(1); + + to.assertFailure(TestException.class); + } + + @Test + public void bufferExactBoundaryBadSource() { + Observable<Integer> ps = new Observable<Integer>() { + @Override + protected void subscribeActual(Observer<? super Integer> observer) { + observer.onSubscribe(Disposable.empty()); + observer.onComplete(); + observer.onNext(1); + observer.onComplete(); + } + }; + + final AtomicReference<Observer<? super Integer>> ref = new AtomicReference<>(); + Observable<Integer> b = new Observable<Integer>() { + @Override + protected void subscribeActual(Observer<? super Integer> observer) { + observer.onSubscribe(Disposable.empty()); + ref.set(observer); + } + }; + + TestObserver<List<Integer>> to = ps.buffer(b).test(); + + ref.get().onNext(1); + + to.assertResult(Collections.<Integer>emptyList()); + } + + @Test + public void timedDoubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeObservable(new Function<Observable<Object>, Observable<List<Object>>>() { + @Override + public Observable<List<Object>> apply(Observable<Object> f) + throws Exception { + return f.buffer(1, TimeUnit.SECONDS); + } + }); + } + + @Test + public void timedCancelledUpfront() { + TestScheduler sch = new TestScheduler(); + + TestObserver<List<Object>> to = Observable.never() + .buffer(1, TimeUnit.MILLISECONDS, sch) + .test(true); + + sch.advanceTimeBy(1, TimeUnit.MILLISECONDS); + + to.assertEmpty(); + } + + @Test + public void timedInternalState() { + TestScheduler sch = new TestScheduler(); + + TestObserver<List<Integer>> to = new TestObserver<>(); + + BufferExactUnboundedObserver<Integer, List<Integer>> sub = new BufferExactUnboundedObserver<>( + to, Functions.justSupplier((List<Integer>) new ArrayList<Integer>()), 1, TimeUnit.SECONDS, sch); + + sub.onSubscribe(Disposable.empty()); + + assertFalse(sub.isDisposed()); + + sub.onError(new TestException()); + sub.onNext(1); + sub.onComplete(); + + sub.run(); + + sub.dispose(); + + assertTrue(sub.isDisposed()); + + sub.buffer = new ArrayList<>(); + sub.enter(); + sub.onComplete(); + } + + @Test + public void timedSkipDoubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeObservable(new Function<Observable<Object>, Observable<List<Object>>>() { + @Override + public Observable<List<Object>> apply(Observable<Object> f) + throws Exception { + return f.buffer(2, 1, TimeUnit.SECONDS); + } + }); + } + + @Test + public void timedSizedDoubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeObservable(new Function<Observable<Object>, Observable<List<Object>>>() { + @Override + public Observable<List<Object>> apply(Observable<Object> f) + throws Exception { + return f.buffer(2, TimeUnit.SECONDS, 10); + } + }); + } + + @Test + public void timedSkipInternalState() { + TestScheduler sch = new TestScheduler(); + + TestObserver<List<Integer>> to = new TestObserver<>(); + + BufferSkipBoundedObserver<Integer, List<Integer>> sub = new BufferSkipBoundedObserver<>( + to, Functions.justSupplier((List<Integer>) new ArrayList<Integer>()), 1, 1, TimeUnit.SECONDS, sch.createWorker()); + + sub.onSubscribe(Disposable.empty()); + + sub.enter(); + sub.onComplete(); + + sub.dispose(); + + sub.run(); + } + + @Test + public void timedSkipCancelWhenSecondBuffer() { + TestScheduler sch = new TestScheduler(); + + final TestObserver<List<Integer>> to = new TestObserver<>(); + + BufferSkipBoundedObserver<Integer, List<Integer>> sub = new BufferSkipBoundedObserver<>( + to, new Supplier<List<Integer>>() { + int calls; + + @Override + public List<Integer> get() throws Exception { + if (++calls == 2) { + to.dispose(); + } + return new ArrayList<>(); + } + }, 1, 1, TimeUnit.SECONDS, sch.createWorker()); + + sub.onSubscribe(Disposable.empty()); + + sub.run(); + + assertTrue(to.isDisposed()); + } + + @Test + public void timedSizeBufferAlreadyCleared() { + TestScheduler sch = new TestScheduler(); + + TestObserver<List<Integer>> to = new TestObserver<>(); + + BufferExactBoundedObserver<Integer, List<Integer>> sub = + new BufferExactBoundedObserver<>( + to, Functions.justSupplier((List<Integer>) new ArrayList<Integer>()), + 1, TimeUnit.SECONDS, 1, false, sch.createWorker()) + ; + + Disposable bs = Disposable.empty(); + + sub.onSubscribe(bs); + + sub.producerIndex++; + + sub.run(); + + assertFalse(sub.isDisposed()); + + sub.enter(); + sub.onComplete(); + + sub.dispose(); + + assertTrue(sub.isDisposed()); + + sub.run(); + + sub.onNext(1); + } + + @Test + public void bufferExactDoubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeObservable(new Function<Observable<Object>, ObservableSource<List<Object>>>() { + @Override + public ObservableSource<List<Object>> apply(Observable<Object> o) + throws Exception { + return o.buffer(1); + } + }); + } + + @Test + public void bufferExactState() { + TestObserver<List<Integer>> to = new TestObserver<>(); + + BufferExactObserver<Integer, List<Integer>> sub = new BufferExactObserver<>( + to, 1, Functions.justSupplier((List<Integer>) new ArrayList<Integer>()) + ); + + sub.onComplete(); + sub.onNext(1); + sub.onComplete(); + } + + @Test + public void bufferSkipDoubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeObservable(new Function<Observable<Object>, ObservableSource<List<Object>>>() { + @Override + public ObservableSource<List<Object>> apply(Observable<Object> o) + throws Exception { + return o.buffer(1, 2); + } + }); + } + + @Test + public void bufferExactFailingSupplier() { + Observable.empty() + .buffer(1, TimeUnit.SECONDS, Schedulers.computation(), 10, new Supplier<List<Object>>() { + @Override + public List<Object> get() throws Exception { + throw new TestException(); + } + }, false) + .test() + .awaitDone(1, TimeUnit.SECONDS) + .assertFailure(TestException.class) + ; + } + + @Test + public void timedUnboundedCancelUpfront() { + Observable.never() + .buffer(1, TimeUnit.SECONDS) + .test(true) + .assertEmpty(); + } + + @Test + public void boundaryCloseCompleteRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + BehaviorSubject<Integer> bs = BehaviorSubject.createDefault(1); + PublishSubject<Integer> ps = PublishSubject.create(); + + TestObserver<List<Integer>> to = bs + .buffer(BehaviorSubject.createDefault(0), v -> ps) + .test(); + + TestHelper.race( + () -> bs.onComplete(), + () -> ps.onComplete() + ); + + to.assertResult(Arrays.asList(1)); + } + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableBufferUntilSubscriberTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableBufferUntilSubscriberTest.java new file mode 100644 index 0000000000..eb67fee441 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableBufferUntilSubscriberTest.java @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import java.util.List; +import java.util.concurrent.*; +import java.util.concurrent.atomic.*; + +import org.junit.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.schedulers.Schedulers; +import io.reactivex.rxjava3.subjects.PublishSubject; + +public class ObservableBufferUntilSubscriberTest extends RxJavaTest { + + @Test + public void issue1677() throws InterruptedException { + final AtomicLong counter = new AtomicLong(); + final Integer[] numbers = new Integer[5000]; + for (int i = 0; i < numbers.length; i++) { + numbers[i] = i + 1; + } + final int NITERS = 250; + final CountDownLatch latch = new CountDownLatch(NITERS); + for (int iters = 0; iters < NITERS; iters++) { + final CountDownLatch innerLatch = new CountDownLatch(1); + final PublishSubject<Void> s = PublishSubject.create(); + final AtomicBoolean completed = new AtomicBoolean(); + Observable.fromArray(numbers) + .takeUntil(s) + .window(50) + .flatMap(new Function<Observable<Integer>, Observable<Object>>() { + @Override + public Observable<Object> apply(Observable<Integer> integerObservable) { + return integerObservable + .subscribeOn(Schedulers.computation()) + .map(new Function<Integer, Object>() { + @Override + public Object apply(Integer integer) { + if (integer >= 5 && completed.compareAndSet(false, true)) { + s.onComplete(); + } + // do some work + Math.pow(Math.random(), Math.random()); + return integer * 2; + } + }); + } + }) + .toList() + .doOnSuccess(new Consumer<List<Object>>() { + @Override + public void accept(List<Object> integers) { + counter.incrementAndGet(); + latch.countDown(); + innerLatch.countDown(); + } + }) + .subscribe(); + if (!innerLatch.await(30, TimeUnit.SECONDS)) { + Assert.fail("Failed inner latch wait, iteration " + iters); + } + } + if (!latch.await(30, TimeUnit.SECONDS)) { + Assert.fail("Incomplete! Went through " + latch.getCount() + " iterations"); + } else { + Assert.assertEquals(NITERS, counter.get()); + } + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableCacheTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableCacheTest.java new file mode 100644 index 0000000000..7f47ec95d8 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableCacheTest.java @@ -0,0 +1,358 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +import java.util.*; +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.core.Observer; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.schedulers.Schedulers; +import io.reactivex.rxjava3.subjects.PublishSubject; +import io.reactivex.rxjava3.testsupport.*; + +public class ObservableCacheTest extends RxJavaTest { + @Test + public void coldReplayNoBackpressure() { + ObservableCache<Integer> source = new ObservableCache<>(Observable.range(0, 1000), 16); + + assertFalse("Source is connected!", source.isConnected()); + + TestObserverEx<Integer> to = new TestObserverEx<>(); + + source.subscribe(to); + + assertTrue("Source is not connected!", source.isConnected()); + assertFalse("Subscribers retained!", source.hasObservers()); + + to.assertNoErrors(); + to.assertTerminated(); + List<Integer> onNextEvents = to.values(); + assertEquals(1000, onNextEvents.size()); + + for (int i = 0; i < 1000; i++) { + assertEquals((Integer)i, onNextEvents.get(i)); + } + } + + @Test + public void cache() throws InterruptedException { + final AtomicInteger counter = new AtomicInteger(); + Observable<String> o = Observable.unsafeCreate(new ObservableSource<String>() { + + @Override + public void subscribe(final Observer<? super String> observer) { + observer.onSubscribe(Disposable.empty()); + new Thread(new Runnable() { + + @Override + public void run() { + counter.incrementAndGet(); + System.out.println("published Observable being executed"); + observer.onNext("one"); + observer.onComplete(); + } + }).start(); + } + }).cache(); + + // we then expect the following 2 subscriptions to get that same value + final CountDownLatch latch = new CountDownLatch(2); + + // subscribe once + o.subscribe(new Consumer<String>() { + @Override + public void accept(String v) { + assertEquals("one", v); + System.out.println("v: " + v); + latch.countDown(); + } + }); + + // subscribe again + o.subscribe(new Consumer<String>() { + @Override + public void accept(String v) { + assertEquals("one", v); + System.out.println("v: " + v); + latch.countDown(); + } + }); + + if (!latch.await(1000, TimeUnit.MILLISECONDS)) { + fail("subscriptions did not receive values"); + } + assertEquals(1, counter.get()); + } + + @Test + public void unsubscribeSource() throws Throwable { + Action unsubscribe = mock(Action.class); + Observable<Integer> o = Observable.just(1).doOnDispose(unsubscribe).cache(); + o.subscribe(); + o.subscribe(); + o.subscribe(); + verify(unsubscribe, never()).run(); + } + + @Test + public void take() { + TestObserver<Integer> to = new TestObserver<>(); + + ObservableCache<Integer> cached = new ObservableCache<>(Observable.range(1, 1000), 16); + cached.take(10).subscribe(to); + + to.assertNoErrors(); + to.assertComplete(); + to.assertValues(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); +// ts.assertUnsubscribed(); // FIXME no longer valid + assertFalse(cached.hasObservers()); + } + + @Test + public void async() { + Observable<Integer> source = Observable.range(1, 10000); + for (int i = 0; i < 100; i++) { + TestObserver<Integer> to1 = new TestObserver<>(); + + ObservableCache<Integer> cached = new ObservableCache<>(source, 16); + + cached.observeOn(Schedulers.computation()).subscribe(to1); + + to1.awaitDone(2, TimeUnit.SECONDS); + to1.assertNoErrors(); + to1.assertComplete(); + assertEquals(10000, to1.values().size()); + + TestObserver<Integer> to2 = new TestObserver<>(); + cached.observeOn(Schedulers.computation()).subscribe(to2); + + to2.awaitDone(2, TimeUnit.SECONDS); + to2.assertNoErrors(); + to2.assertComplete(); + assertEquals(10000, to2.values().size()); + } + } + + @Test + public void asyncComeAndGo() { + Observable<Long> source = Observable.interval(1, 1, TimeUnit.MILLISECONDS) + .take(1000) + .subscribeOn(Schedulers.io()); + ObservableCache<Long> cached = new ObservableCache<>(source, 16); + + Observable<Long> output = cached.observeOn(Schedulers.computation()); + + List<TestObserver<Long>> list = new ArrayList<>(100); + for (int i = 0; i < 100; i++) { + TestObserver<Long> to = new TestObserver<>(); + list.add(to); + output.skip(i * 10).take(10).subscribe(to); + } + + List<Long> expected = new ArrayList<>(); + for (int i = 0; i < 10; i++) { + expected.add((long)(i - 10)); + } + int j = 0; + for (TestObserver<Long> to : list) { + to.awaitDone(3, TimeUnit.SECONDS); + to.assertNoErrors(); + to.assertComplete(); + + for (int i = j * 10; i < j * 10 + 10; i++) { + expected.set(i - j * 10, (long)i); + } + + to.assertValueSequence(expected); + + j++; + } + } + + @Test + public void noMissingBackpressureException() { + final int m = 4 * 1000 * 1000; + Observable<Integer> firehose = Observable.unsafeCreate(new ObservableSource<Integer>() { + @Override + public void subscribe(Observer<? super Integer> t) { + t.onSubscribe(Disposable.empty()); + for (int i = 0; i < m; i++) { + t.onNext(i); + } + t.onComplete(); + } + }); + + TestObserver<Integer> to = new TestObserver<>(); + firehose.cache().observeOn(Schedulers.computation()).takeLast(100).subscribe(to); + + to.awaitDone(3, TimeUnit.SECONDS); + to.assertNoErrors(); + to.assertComplete(); + + assertEquals(100, to.values().size()); + } + + @Test + public void valuesAndThenError() { + Observable<Integer> source = Observable.range(1, 10) + .concatWith(Observable.<Integer>error(new TestException())) + .cache(); + + TestObserver<Integer> to = new TestObserver<>(); + source.subscribe(to); + + to.assertValues(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + to.assertNotComplete(); + to.assertError(TestException.class); + + TestObserver<Integer> to2 = new TestObserver<>(); + source.subscribe(to2); + + to2.assertValues(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + to2.assertNotComplete(); + to2.assertError(TestException.class); + } + + @Test + public void observers() { + PublishSubject<Integer> ps = PublishSubject.create(); + ObservableCache<Integer> cache = (ObservableCache<Integer>)Observable.range(1, 5).concatWith(ps).cache(); + + assertFalse(cache.hasObservers()); + + assertEquals(0, cache.cachedEventCount()); + + TestObserver<Integer> to = cache.test(); + + assertTrue(cache.hasObservers()); + + assertEquals(5, cache.cachedEventCount()); + + ps.onComplete(); + + to.assertResult(1, 2, 3, 4, 5); + } + + @Test + public void disposeOnArrival() { + Observable.range(1, 5).cache() + .test(true) + .assertEmpty(); + } + + @Test + public void disposeOnArrival2() { + Observable<Integer> o = PublishSubject.<Integer>create().cache(); + + o.test(); + + o.test(true) + .assertEmpty(); + } + + @Test + public void dispose() { + TestHelper.checkDisposed(Observable.range(1, 5).cache()); + } + + @Test + public void take2() { + Observable<Integer> cache = Observable.range(1, 5).cache(); + + cache.take(2).test().assertResult(1, 2); + cache.take(3).test().assertResult(1, 2, 3); + } + + @Test + public void subscribeEmitRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final PublishSubject<Integer> ps = PublishSubject.<Integer>create(); + + final Observable<Integer> cache = ps.cache(); + + cache.test(); + + final TestObserverEx<Integer> to = new TestObserverEx<>(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + cache.subscribe(to); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + for (int j = 0; j < 500; j++) { + ps.onNext(j); + } + ps.onComplete(); + } + }; + + TestHelper.race(r1, r2); + + to + .awaitDone(5, TimeUnit.SECONDS) + .assertSubscribed().assertValueCount(500).assertComplete().assertNoErrors(); + } + } + + @Test + public void cancelledUpFront() { + final AtomicInteger call = new AtomicInteger(); + Observable<Object> f = Observable.fromCallable(new Callable<Object>() { + @Override + public Object call() throws Exception { + return call.incrementAndGet(); + } + }).concatWith(Observable.never()) + .cache(); + + f.test().assertValuesOnly(1); + + f.test(true) + .assertEmpty(); + + assertEquals(1, call.get()); + } + + @Test + public void addRemoveRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + Observable<Object> o = Observable.never().cache(); + + TestObserver<Object> to = o.test(); + + TestHelper.race( + () -> to.dispose(), + () -> o.test() + ); + } + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableCastTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableCastTest.java new file mode 100644 index 0000000000..83fdb4b1e5 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableCastTest.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class ObservableCastTest extends RxJavaTest { + + @Test + public void cast() { + Observable<?> source = Observable.just(1, 2); + Observable<Integer> observable = source.cast(Integer.class); + + Observer<Integer> observer = TestHelper.mockObserver(); + + observable.subscribe(observer); + + verify(observer, times(1)).onNext(1); + verify(observer, times(1)).onNext(1); + verify(observer, never()).onError( + any(Throwable.class)); + verify(observer, times(1)).onComplete(); + } + + @Test + public void castWithWrongType() { + Observable<?> source = Observable.just(1, 2); + Observable<Boolean> observable = source.cast(Boolean.class); + + Observer<Boolean> observer = TestHelper.mockObserver(); + + observable.subscribe(observer); + + verify(observer, times(1)).onError( + any(ClassCastException.class)); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableCollectTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableCollectTest.java new file mode 100644 index 0000000000..92867c2d79 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableCollectTest.java @@ -0,0 +1,364 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import static io.reactivex.rxjava3.internal.util.TestingHelper.*; +import static org.junit.Assert.*; + +import java.util.*; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public final class ObservableCollectTest extends RxJavaTest { + + @Test + public void collectToListObservable() { + Observable<List<Integer>> o = Observable.just(1, 2, 3) + .collect(new Supplier<List<Integer>>() { + @Override + public List<Integer> get() { + return new ArrayList<>(); + } + }, new BiConsumer<List<Integer>, Integer>() { + @Override + public void accept(List<Integer> list, Integer v) { + list.add(v); + } + }).toObservable(); + + List<Integer> list = o.blockingLast(); + + assertEquals(3, list.size()); + assertEquals(1, list.get(0).intValue()); + assertEquals(2, list.get(1).intValue()); + assertEquals(3, list.get(2).intValue()); + + // test multiple subscribe + List<Integer> list2 = o.blockingLast(); + + assertEquals(3, list2.size()); + assertEquals(1, list2.get(0).intValue()); + assertEquals(2, list2.get(1).intValue()); + assertEquals(3, list2.get(2).intValue()); + } + + @Test + public void collectToStringObservable() { + String value = Observable.just(1, 2, 3).collect(new Supplier<StringBuilder>() { + @Override + public StringBuilder get() { + return new StringBuilder(); + } + }, + new BiConsumer<StringBuilder, Integer>() { + @Override + public void accept(StringBuilder sb, Integer v) { + if (sb.length() > 0) { + sb.append("-"); + } + sb.append(v); + } + }).toObservable().blockingLast().toString(); + + assertEquals("1-2-3", value); + } + + @Test + public void collectorFailureDoesNotResultInTwoErrorEmissionsObservable() { + try { + final List<Throwable> list = new CopyOnWriteArrayList<>(); + RxJavaPlugins.setErrorHandler(addToList(list)); + final RuntimeException e1 = new RuntimeException(); + final RuntimeException e2 = new RuntimeException(); + + Burst.items(1).error(e2) // + .collect(supplierListCreator(), biConsumerThrows(e1)) // + .toObservable() + .test() // + .assertError(e1) // + .assertNotComplete(); + + assertEquals(1, list.size()); + assertEquals(e2, list.get(0).getCause()); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void collectorFailureDoesNotResultInErrorAndCompletedEmissionsObservable() { + final RuntimeException e = new RuntimeException(); + Burst.item(1).create() // + .collect(supplierListCreator(), biConsumerThrows(e)) // + .toObservable() + .test() // + .assertError(e) // + .assertNotComplete(); + } + + @Test + public void collectorFailureDoesNotResultInErrorAndOnNextEmissionsObservable() { + final RuntimeException e = new RuntimeException(); + final AtomicBoolean added = new AtomicBoolean(); + BiConsumer<Object, Integer> throwOnFirstOnly = new BiConsumer<Object, Integer>() { + + boolean once = true; + + @Override + public void accept(Object o, Integer t) { + if (once) { + once = false; + throw e; + } else { + added.set(true); + } + } + }; + Burst.items(1, 2).create() // + .collect(supplierListCreator(), throwOnFirstOnly)// + .test() // + .assertError(e) // + .assertNoValues() // + .assertNotComplete(); + assertFalse(added.get()); + } + + @Test + public void collectIntoObservable() { + Observable.just(1, 1, 1, 1, 2) + .collectInto(new HashSet<>(), new BiConsumer<HashSet<Integer>, Integer>() { + @Override + public void accept(HashSet<Integer> s, Integer v) throws Exception { + s.add(v); + } + }).toObservable() + .test() + .assertResult(new HashSet<>(Arrays.asList(1, 2))); + } + + @Test + public void collectToList() { + Single<List<Integer>> o = Observable.just(1, 2, 3) + .collect(new Supplier<List<Integer>>() { + @Override + public List<Integer> get() { + return new ArrayList<>(); + } + }, new BiConsumer<List<Integer>, Integer>() { + @Override + public void accept(List<Integer> list, Integer v) { + list.add(v); + } + }); + + List<Integer> list = o.blockingGet(); + + assertEquals(3, list.size()); + assertEquals(1, list.get(0).intValue()); + assertEquals(2, list.get(1).intValue()); + assertEquals(3, list.get(2).intValue()); + + // test multiple subscribe + List<Integer> list2 = o.blockingGet(); + + assertEquals(3, list2.size()); + assertEquals(1, list2.get(0).intValue()); + assertEquals(2, list2.get(1).intValue()); + assertEquals(3, list2.get(2).intValue()); + } + + @Test + public void collectToString() { + String value = Observable.just(1, 2, 3).collect(new Supplier<StringBuilder>() { + @Override + public StringBuilder get() { + return new StringBuilder(); + } + }, + new BiConsumer<StringBuilder, Integer>() { + @Override + public void accept(StringBuilder sb, Integer v) { + if (sb.length() > 0) { + sb.append("-"); + } + sb.append(v); + } + }).blockingGet().toString(); + + assertEquals("1-2-3", value); + } + + @Test + public void collectorFailureDoesNotResultInTwoErrorEmissions() { + try { + final List<Throwable> list = new CopyOnWriteArrayList<>(); + RxJavaPlugins.setErrorHandler(addToList(list)); + final RuntimeException e1 = new RuntimeException(); + final RuntimeException e2 = new RuntimeException(); + + Burst.items(1).error(e2) // + .collect(supplierListCreator(), biConsumerThrows(e1)) // + .test() // + .assertError(e1) // + .assertNotComplete(); + + assertEquals(1, list.size()); + assertEquals(e2, list.get(0).getCause()); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void collectorFailureDoesNotResultInErrorAndCompletedEmissions() { + final RuntimeException e = new RuntimeException(); + Burst.item(1).create() // + .collect(supplierListCreator(), biConsumerThrows(e)) // + .test() // + .assertError(e) // + .assertNotComplete(); + } + + @Test + public void collectorFailureDoesNotResultInErrorAndOnNextEmissions() { + final RuntimeException e = new RuntimeException(); + final AtomicBoolean added = new AtomicBoolean(); + BiConsumer<Object, Integer> throwOnFirstOnly = new BiConsumer<Object, Integer>() { + + boolean once = true; + + @Override + public void accept(Object o, Integer t) { + if (once) { + once = false; + throw e; + } else { + added.set(true); + } + } + }; + Burst.items(1, 2).create() // + .collect(supplierListCreator(), throwOnFirstOnly)// + .test() // + .assertError(e) // + .assertNoValues() // + .assertNotComplete(); + assertFalse(added.get()); + } + + @Test + public void collectInto() { + Observable.just(1, 1, 1, 1, 2) + .collectInto(new HashSet<>(), new BiConsumer<HashSet<Integer>, Integer>() { + @Override + public void accept(HashSet<Integer> s, Integer v) throws Exception { + s.add(v); + } + }) + .test() + .assertResult(new HashSet<>(Arrays.asList(1, 2))); + } + + @Test + public void dispose() { + TestHelper.checkDisposed(Observable.range(1, 3).collect(new Supplier<List<Integer>>() { + @Override + public List<Integer> get() throws Exception { + return new ArrayList<>(); + } + }, new BiConsumer<List<Integer>, Integer>() { + @Override + public void accept(List<Integer> a, Integer b) throws Exception { + a.add(b); + } + })); + + TestHelper.checkDisposed(Observable.range(1, 3).collect(new Supplier<List<Integer>>() { + @Override + public List<Integer> get() throws Exception { + return new ArrayList<>(); + } + }, new BiConsumer<List<Integer>, Integer>() { + @Override + public void accept(List<Integer> a, Integer b) throws Exception { + a.add(b); + } + }).toObservable()); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeObservableToSingle(new Function<Observable<Integer>, SingleSource<List<Integer>>>() { + @Override + public SingleSource<List<Integer>> apply(Observable<Integer> o) throws Exception { + return o.collect(new Supplier<List<Integer>>() { + @Override + public List<Integer> get() throws Exception { + return new ArrayList<>(); + } + }, new BiConsumer<List<Integer>, Integer>() { + @Override + public void accept(List<Integer> a, Integer b) throws Exception { + a.add(b); + } + }); + } + }); + + TestHelper.checkDoubleOnSubscribeObservable(new Function<Observable<Integer>, ObservableSource<List<Integer>>>() { + @Override + public ObservableSource<List<Integer>> apply(Observable<Integer> o) throws Exception { + return o.collect(new Supplier<List<Integer>>() { + @Override + public List<Integer> get() throws Exception { + return new ArrayList<>(); + } + }, new BiConsumer<List<Integer>, Integer>() { + @Override + public void accept(List<Integer> a, Integer b) throws Exception { + a.add(b); + } + }).toObservable(); + } + }); + } + + @Test + public void badSource() { + TestHelper.checkBadSourceObservable(new Function<Observable<Integer>, Object>() { + @Override + public Object apply(Observable<Integer> o) throws Exception { + return o.collect(new Supplier<List<Integer>>() { + @Override + public List<Integer> get() throws Exception { + return new ArrayList<>(); + } + }, new BiConsumer<List<Integer>, Integer>() { + @Override + public void accept(List<Integer> a, Integer b) throws Exception { + a.add(b); + } + }).toObservable(); + } + }, false, 1, 2, Arrays.asList(1)); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableCombineLatestTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableCombineLatestTest.java new file mode 100644 index 0000000000..2b966ec29c --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableCombineLatestTest.java @@ -0,0 +1,1301 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +import java.util.*; +import java.util.concurrent.*; +import java.util.concurrent.atomic.*; + +import org.junit.Test; +import org.mockito.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.core.Observer; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.observers.*; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.schedulers.*; +import io.reactivex.rxjava3.subjects.PublishSubject; +import io.reactivex.rxjava3.testsupport.*; + +public class ObservableCombineLatestTest extends RxJavaTest { + + @Test + public void combineLatestWithFunctionThatThrowsAnException() { + Observer<String> w = TestHelper.mockObserver(); + + PublishSubject<String> w1 = PublishSubject.create(); + PublishSubject<String> w2 = PublishSubject.create(); + + Observable<String> combined = Observable.combineLatest(w1, w2, new BiFunction<String, String, String>() { + @Override + public String apply(String v1, String v2) { + throw new RuntimeException("I don't work."); + } + }); + combined.subscribe(w); + + w1.onNext("first value of w1"); + w2.onNext("first value of w2"); + + verify(w, never()).onNext(anyString()); + verify(w, never()).onComplete(); + verify(w, times(1)).onError(Mockito.<RuntimeException> any()); + } + + @Test + public void combineLatestDifferentLengthObservableSequences1() { + Observer<String> w = TestHelper.mockObserver(); + + PublishSubject<String> w1 = PublishSubject.create(); + PublishSubject<String> w2 = PublishSubject.create(); + PublishSubject<String> w3 = PublishSubject.create(); + + Observable<String> combineLatestW = Observable.combineLatest(w1, w2, w3, + getConcat3StringsCombineLatestFunction()); + combineLatestW.subscribe(w); + + /* simulate sending data */ + // once for w1 + w1.onNext("1a"); + w2.onNext("2a"); + w3.onNext("3a"); + w1.onComplete(); + // twice for w2 + w2.onNext("2b"); + w2.onComplete(); + // 4 times for w3 + w3.onNext("3b"); + w3.onNext("3c"); + w3.onNext("3d"); + w3.onComplete(); + + /* we should have been called 4 times on the Observer */ + InOrder inOrder = inOrder(w); + inOrder.verify(w).onNext("1a2a3a"); + inOrder.verify(w).onNext("1a2b3a"); + inOrder.verify(w).onNext("1a2b3b"); + inOrder.verify(w).onNext("1a2b3c"); + inOrder.verify(w).onNext("1a2b3d"); + inOrder.verify(w, never()).onNext(anyString()); + inOrder.verify(w, times(1)).onComplete(); + } + + @Test + public void combineLatestDifferentLengthObservableSequences2() { + Observer<String> w = TestHelper.mockObserver(); + + PublishSubject<String> w1 = PublishSubject.create(); + PublishSubject<String> w2 = PublishSubject.create(); + PublishSubject<String> w3 = PublishSubject.create(); + + Observable<String> combineLatestW = Observable.combineLatest(w1, w2, w3, getConcat3StringsCombineLatestFunction()); + combineLatestW.subscribe(w); + + /* simulate sending data */ + // 4 times for w1 + w1.onNext("1a"); + w1.onNext("1b"); + w1.onNext("1c"); + w1.onNext("1d"); + w1.onComplete(); + // twice for w2 + w2.onNext("2a"); + w2.onNext("2b"); + w2.onComplete(); + // 1 times for w3 + w3.onNext("3a"); + w3.onComplete(); + + /* we should have been called 1 time only on the Observer since we only combine the "latest" we don't go back and loop through others once completed */ + InOrder inOrder = inOrder(w); + inOrder.verify(w, times(1)).onNext("1d2b3a"); + inOrder.verify(w, never()).onNext(anyString()); + + inOrder.verify(w, times(1)).onComplete(); + + } + + @Test + public void combineLatestWithInterleavingSequences() { + Observer<String> w = TestHelper.mockObserver(); + + PublishSubject<String> w1 = PublishSubject.create(); + PublishSubject<String> w2 = PublishSubject.create(); + PublishSubject<String> w3 = PublishSubject.create(); + + Observable<String> combineLatestW = Observable.combineLatest(w1, w2, w3, getConcat3StringsCombineLatestFunction()); + combineLatestW.subscribe(w); + + /* simulate sending data */ + w1.onNext("1a"); + w2.onNext("2a"); + w2.onNext("2b"); + w3.onNext("3a"); + + w1.onNext("1b"); + w2.onNext("2c"); + w2.onNext("2d"); + w3.onNext("3b"); + + w1.onComplete(); + w2.onComplete(); + w3.onComplete(); + + /* we should have been called 5 times on the Observer */ + InOrder inOrder = inOrder(w); + inOrder.verify(w).onNext("1a2b3a"); + inOrder.verify(w).onNext("1b2b3a"); + inOrder.verify(w).onNext("1b2c3a"); + inOrder.verify(w).onNext("1b2d3a"); + inOrder.verify(w).onNext("1b2d3b"); + + inOrder.verify(w, never()).onNext(anyString()); + inOrder.verify(w, times(1)).onComplete(); + } + + @Test + public void combineLatest2Types() { + BiFunction<String, Integer, String> combineLatestFunction = getConcatStringIntegerCombineLatestFunction(); + + /* define an Observer to receive aggregated events */ + Observer<String> observer = TestHelper.mockObserver(); + + Observable<String> w = Observable.combineLatest(Observable.just("one", "two"), Observable.just(2, 3, 4), combineLatestFunction); + w.subscribe(observer); + + verify(observer, never()).onError(any(Throwable.class)); + verify(observer, times(1)).onComplete(); + verify(observer, times(1)).onNext("two2"); + verify(observer, times(1)).onNext("two3"); + verify(observer, times(1)).onNext("two4"); + } + + @Test + public void combineLatest3TypesA() { + Function3<String, Integer, int[], String> combineLatestFunction = getConcatStringIntegerIntArrayCombineLatestFunction(); + + /* define an Observer to receive aggregated events */ + Observer<String> observer = TestHelper.mockObserver(); + + Observable<String> w = Observable.combineLatest(Observable.just("one", "two"), Observable.just(2), Observable.just(new int[] { 4, 5, 6 }), combineLatestFunction); + w.subscribe(observer); + + verify(observer, never()).onError(any(Throwable.class)); + verify(observer, times(1)).onComplete(); + verify(observer, times(1)).onNext("two2[4, 5, 6]"); + } + + @Test + public void combineLatest3TypesB() { + Function3<String, Integer, int[], String> combineLatestFunction = getConcatStringIntegerIntArrayCombineLatestFunction(); + + /* define an Observer to receive aggregated events */ + Observer<String> observer = TestHelper.mockObserver(); + + Observable<String> w = Observable.combineLatest(Observable.just("one"), Observable.just(2), Observable.just(new int[] { 4, 5, 6 }, new int[] { 7, 8 }), combineLatestFunction); + w.subscribe(observer); + + verify(observer, never()).onError(any(Throwable.class)); + verify(observer, times(1)).onComplete(); + verify(observer, times(1)).onNext("one2[4, 5, 6]"); + verify(observer, times(1)).onNext("one2[7, 8]"); + } + + private Function3<String, String, String, String> getConcat3StringsCombineLatestFunction() { + Function3<String, String, String, String> combineLatestFunction = new Function3<String, String, String, String>() { + @Override + public String apply(String a1, String a2, String a3) { + if (a1 == null) { + a1 = ""; + } + if (a2 == null) { + a2 = ""; + } + if (a3 == null) { + a3 = ""; + } + return a1 + a2 + a3; + } + }; + return combineLatestFunction; + } + + private BiFunction<String, Integer, String> getConcatStringIntegerCombineLatestFunction() { + BiFunction<String, Integer, String> combineLatestFunction = new BiFunction<String, Integer, String>() { + @Override + public String apply(String s, Integer i) { + return getStringValue(s) + getStringValue(i); + } + }; + return combineLatestFunction; + } + + private Function3<String, Integer, int[], String> getConcatStringIntegerIntArrayCombineLatestFunction() { + return new Function3<String, Integer, int[], String>() { + @Override + public String apply(String s, Integer i, int[] iArray) { + return getStringValue(s) + getStringValue(i) + getStringValue(iArray); + } + }; + } + + private static String getStringValue(Object o) { + if (o == null) { + return ""; + } else { + if (o instanceof int[]) { + return Arrays.toString((int[]) o); + } else { + return String.valueOf(o); + } + } + } + + BiFunction<Integer, Integer, Integer> or = new BiFunction<Integer, Integer, Integer>() { + @Override + public Integer apply(Integer t1, Integer t2) { + return t1 | t2; + } + }; + + @Test + public void combineSimple() { + PublishSubject<Integer> a = PublishSubject.create(); + PublishSubject<Integer> b = PublishSubject.create(); + + Observable<Integer> source = Observable.combineLatest(a, b, or); + + Observer<Object> observer = TestHelper.mockObserver(); + InOrder inOrder = inOrder(observer); + + source.subscribe(observer); + + a.onNext(1); + + inOrder.verify(observer, never()).onNext(any()); + + a.onNext(2); + + inOrder.verify(observer, never()).onNext(any()); + + b.onNext(0x10); + + inOrder.verify(observer, times(1)).onNext(0x12); + + b.onNext(0x20); + inOrder.verify(observer, times(1)).onNext(0x22); + + b.onComplete(); + + inOrder.verify(observer, never()).onComplete(); + + a.onComplete(); + + inOrder.verify(observer, times(1)).onComplete(); + + a.onNext(3); + b.onNext(0x30); + a.onComplete(); + b.onComplete(); + + inOrder.verifyNoMoreInteractions(); + verify(observer, never()).onError(any(Throwable.class)); + } + + @Test + public void combineMultipleObservers() { + PublishSubject<Integer> a = PublishSubject.create(); + PublishSubject<Integer> b = PublishSubject.create(); + + Observable<Integer> source = Observable.combineLatest(a, b, or); + + Observer<Object> observer1 = TestHelper.mockObserver(); + + Observer<Object> observer2 = TestHelper.mockObserver(); + + InOrder inOrder1 = inOrder(observer1); + InOrder inOrder2 = inOrder(observer2); + + source.subscribe(observer1); + source.subscribe(observer2); + + a.onNext(1); + + inOrder1.verify(observer1, never()).onNext(any()); + inOrder2.verify(observer2, never()).onNext(any()); + + a.onNext(2); + + inOrder1.verify(observer1, never()).onNext(any()); + inOrder2.verify(observer2, never()).onNext(any()); + + b.onNext(0x10); + + inOrder1.verify(observer1, times(1)).onNext(0x12); + inOrder2.verify(observer2, times(1)).onNext(0x12); + + b.onNext(0x20); + inOrder1.verify(observer1, times(1)).onNext(0x22); + inOrder2.verify(observer2, times(1)).onNext(0x22); + + b.onComplete(); + + inOrder1.verify(observer1, never()).onComplete(); + inOrder2.verify(observer2, never()).onComplete(); + + a.onComplete(); + + inOrder1.verify(observer1, times(1)).onComplete(); + inOrder2.verify(observer2, times(1)).onComplete(); + + a.onNext(3); + b.onNext(0x30); + a.onComplete(); + b.onComplete(); + + inOrder1.verifyNoMoreInteractions(); + inOrder2.verifyNoMoreInteractions(); + verify(observer1, never()).onError(any(Throwable.class)); + verify(observer2, never()).onError(any(Throwable.class)); + } + + @Test + public void firstNeverProduces() { + PublishSubject<Integer> a = PublishSubject.create(); + PublishSubject<Integer> b = PublishSubject.create(); + + Observable<Integer> source = Observable.combineLatest(a, b, or); + + Observer<Object> observer = TestHelper.mockObserver(); + InOrder inOrder = inOrder(observer); + + source.subscribe(observer); + + b.onNext(0x10); + b.onNext(0x20); + + a.onComplete(); + + inOrder.verify(observer, times(1)).onComplete(); + verify(observer, never()).onNext(any()); + verify(observer, never()).onError(any(Throwable.class)); + } + + @Test + public void secondNeverProduces() { + PublishSubject<Integer> a = PublishSubject.create(); + PublishSubject<Integer> b = PublishSubject.create(); + + Observable<Integer> source = Observable.combineLatest(a, b, or); + + Observer<Object> observer = TestHelper.mockObserver(); + InOrder inOrder = inOrder(observer); + + source.subscribe(observer); + + a.onNext(0x1); + a.onNext(0x2); + + b.onComplete(); + a.onComplete(); + + inOrder.verify(observer, times(1)).onComplete(); + verify(observer, never()).onNext(any()); + verify(observer, never()).onError(any(Throwable.class)); + } + + @Test + public void oneToNSources() { + int n = 30; + Function<Object[], List<Object>> func = new Function<Object[], List<Object>>() { + + @Override + public List<Object> apply(Object[] args) { + return Arrays.asList(args); + } + }; + for (int i = 1; i <= n; i++) { + System.out.println("test1ToNSources: " + i + " sources"); + List<Observable<Integer>> sources = new ArrayList<>(); + List<Object> values = new ArrayList<>(); + for (int j = 0; j < i; j++) { + sources.add(Observable.just(j)); + values.add(j); + } + + Observable<List<Object>> result = Observable.combineLatest(sources, func); + + Observer<List<Object>> o = TestHelper.mockObserver(); + + result.subscribe(o); + + verify(o).onNext(values); + verify(o).onComplete(); + verify(o, never()).onError(any(Throwable.class)); + } + } + + @Test + public void oneToNSourcesScheduled() throws InterruptedException { + int n = 10; + Function<Object[], List<Object>> func = new Function<Object[], List<Object>>() { + + @Override + public List<Object> apply(Object[] args) { + return Arrays.asList(args); + } + }; + for (int i = 1; i <= n; i++) { + System.out.println("test1ToNSourcesScheduled: " + i + " sources"); + List<Observable<Integer>> sources = new ArrayList<>(); + List<Object> values = new ArrayList<>(); + for (int j = 0; j < i; j++) { + sources.add(Observable.just(j).subscribeOn(Schedulers.io())); + values.add(j); + } + + Observable<List<Object>> result = Observable.combineLatest(sources, func); + + final Observer<List<Object>> o = TestHelper.mockObserver(); + + final CountDownLatch cdl = new CountDownLatch(1); + + Observer<List<Object>> observer = new DefaultObserver<List<Object>>() { + + @Override + public void onNext(List<Object> t) { + o.onNext(t); + } + + @Override + public void onError(Throwable e) { + o.onError(e); + cdl.countDown(); + } + + @Override + public void onComplete() { + o.onComplete(); + cdl.countDown(); + } + }; + + result.subscribe(observer); + + cdl.await(); + + verify(o).onNext(values); + verify(o).onComplete(); + verify(o, never()).onError(any(Throwable.class)); + } + } + + @Test + public void twoSourcesOverload() { + Observable<Integer> s1 = Observable.just(1); + Observable<Integer> s2 = Observable.just(2); + + Observable<List<Integer>> result = Observable.combineLatest(s1, s2, + new BiFunction<Integer, Integer, List<Integer>>() { + @Override + public List<Integer> apply(Integer t1, Integer t2) { + return Arrays.asList(t1, t2); + } + }); + + Observer<Object> o = TestHelper.mockObserver(); + + result.subscribe(o); + + verify(o).onNext(Arrays.asList(1, 2)); + verify(o).onComplete(); + verify(o, never()).onError(any(Throwable.class)); + } + + @Test + public void threeSourcesOverload() { + Observable<Integer> s1 = Observable.just(1); + Observable<Integer> s2 = Observable.just(2); + Observable<Integer> s3 = Observable.just(3); + + Observable<List<Integer>> result = Observable.combineLatest(s1, s2, s3, + new Function3<Integer, Integer, Integer, List<Integer>>() { + @Override + public List<Integer> apply(Integer t1, Integer t2, Integer t3) { + return Arrays.asList(t1, t2, t3); + } + }); + + Observer<Object> o = TestHelper.mockObserver(); + + result.subscribe(o); + + verify(o).onNext(Arrays.asList(1, 2, 3)); + verify(o).onComplete(); + verify(o, never()).onError(any(Throwable.class)); + } + + @Test + public void fourSourcesOverload() { + Observable<Integer> s1 = Observable.just(1); + Observable<Integer> s2 = Observable.just(2); + Observable<Integer> s3 = Observable.just(3); + Observable<Integer> s4 = Observable.just(4); + + Observable<List<Integer>> result = Observable.combineLatest(s1, s2, s3, s4, + new Function4<Integer, Integer, Integer, Integer, List<Integer>>() { + @Override + public List<Integer> apply(Integer t1, Integer t2, Integer t3, Integer t4) { + return Arrays.asList(t1, t2, t3, t4); + } + }); + + Observer<Object> o = TestHelper.mockObserver(); + + result.subscribe(o); + + verify(o).onNext(Arrays.asList(1, 2, 3, 4)); + verify(o).onComplete(); + verify(o, never()).onError(any(Throwable.class)); + } + + @Test + public void fiveSourcesOverload() { + Observable<Integer> s1 = Observable.just(1); + Observable<Integer> s2 = Observable.just(2); + Observable<Integer> s3 = Observable.just(3); + Observable<Integer> s4 = Observable.just(4); + Observable<Integer> s5 = Observable.just(5); + + Observable<List<Integer>> result = Observable.combineLatest(s1, s2, s3, s4, s5, + new Function5<Integer, Integer, Integer, Integer, Integer, List<Integer>>() { + @Override + public List<Integer> apply(Integer t1, Integer t2, Integer t3, Integer t4, Integer t5) { + return Arrays.asList(t1, t2, t3, t4, t5); + } + }); + + Observer<Object> o = TestHelper.mockObserver(); + + result.subscribe(o); + + verify(o).onNext(Arrays.asList(1, 2, 3, 4, 5)); + verify(o).onComplete(); + verify(o, never()).onError(any(Throwable.class)); + } + + @Test + public void sixSourcesOverload() { + Observable<Integer> s1 = Observable.just(1); + Observable<Integer> s2 = Observable.just(2); + Observable<Integer> s3 = Observable.just(3); + Observable<Integer> s4 = Observable.just(4); + Observable<Integer> s5 = Observable.just(5); + Observable<Integer> s6 = Observable.just(6); + + Observable<List<Integer>> result = Observable.combineLatest(s1, s2, s3, s4, s5, s6, + new Function6<Integer, Integer, Integer, Integer, Integer, Integer, List<Integer>>() { + @Override + public List<Integer> apply(Integer t1, Integer t2, Integer t3, Integer t4, Integer t5, Integer t6) { + return Arrays.asList(t1, t2, t3, t4, t5, t6); + } + }); + + Observer<Object> o = TestHelper.mockObserver(); + + result.subscribe(o); + + verify(o).onNext(Arrays.asList(1, 2, 3, 4, 5, 6)); + verify(o).onComplete(); + verify(o, never()).onError(any(Throwable.class)); + } + + @Test + public void sevenSourcesOverload() { + Observable<Integer> s1 = Observable.just(1); + Observable<Integer> s2 = Observable.just(2); + Observable<Integer> s3 = Observable.just(3); + Observable<Integer> s4 = Observable.just(4); + Observable<Integer> s5 = Observable.just(5); + Observable<Integer> s6 = Observable.just(6); + Observable<Integer> s7 = Observable.just(7); + + Observable<List<Integer>> result = Observable.combineLatest(s1, s2, s3, s4, s5, s6, s7, + new Function7<Integer, Integer, Integer, Integer, Integer, Integer, Integer, List<Integer>>() { + @Override + public List<Integer> apply(Integer t1, Integer t2, Integer t3, Integer t4, Integer t5, Integer t6, Integer t7) { + return Arrays.asList(t1, t2, t3, t4, t5, t6, t7); + } + }); + + Observer<Object> o = TestHelper.mockObserver(); + + result.subscribe(o); + + verify(o).onNext(Arrays.asList(1, 2, 3, 4, 5, 6, 7)); + verify(o).onComplete(); + verify(o, never()).onError(any(Throwable.class)); + } + + @Test + public void eightSourcesOverload() { + Observable<Integer> s1 = Observable.just(1); + Observable<Integer> s2 = Observable.just(2); + Observable<Integer> s3 = Observable.just(3); + Observable<Integer> s4 = Observable.just(4); + Observable<Integer> s5 = Observable.just(5); + Observable<Integer> s6 = Observable.just(6); + Observable<Integer> s7 = Observable.just(7); + Observable<Integer> s8 = Observable.just(8); + + Observable<List<Integer>> result = Observable.combineLatest(s1, s2, s3, s4, s5, s6, s7, s8, + new Function8<Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, List<Integer>>() { + @Override + public List<Integer> apply(Integer t1, Integer t2, Integer t3, Integer t4, Integer t5, Integer t6, Integer t7, Integer t8) { + return Arrays.asList(t1, t2, t3, t4, t5, t6, t7, t8); + } + }); + + Observer<Object> o = TestHelper.mockObserver(); + + result.subscribe(o); + + verify(o).onNext(Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8)); + verify(o).onComplete(); + verify(o, never()).onError(any(Throwable.class)); + } + + @Test + public void nineSourcesOverload() { + Observable<Integer> s1 = Observable.just(1); + Observable<Integer> s2 = Observable.just(2); + Observable<Integer> s3 = Observable.just(3); + Observable<Integer> s4 = Observable.just(4); + Observable<Integer> s5 = Observable.just(5); + Observable<Integer> s6 = Observable.just(6); + Observable<Integer> s7 = Observable.just(7); + Observable<Integer> s8 = Observable.just(8); + Observable<Integer> s9 = Observable.just(9); + + Observable<List<Integer>> result = Observable.combineLatest(s1, s2, s3, s4, s5, s6, s7, s8, s9, + new Function9<Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, List<Integer>>() { + @Override + public List<Integer> apply(Integer t1, Integer t2, Integer t3, Integer t4, Integer t5, Integer t6, Integer t7, Integer t8, Integer t9) { + return Arrays.asList(t1, t2, t3, t4, t5, t6, t7, t8, t9); + } + }); + + Observer<Object> o = TestHelper.mockObserver(); + + result.subscribe(o); + + verify(o).onNext(Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9)); + verify(o).onComplete(); + verify(o, never()).onError(any(Throwable.class)); + } + + @Test + public void zeroSources() { + Observable<Object> result = Observable.combineLatest( + Collections.<Observable<Object>> emptyList(), new Function<Object[], Object>() { + + @Override + public Object apply(Object[] args) { + return args; + } + + }); + + Observer<Object> o = TestHelper.mockObserver(); + + result.subscribe(o); + + verify(o).onComplete(); + verify(o, never()).onNext(any()); + verify(o, never()).onError(any(Throwable.class)); + + } + + @Test + public void withCombineLatestIssue1717() throws InterruptedException { + final CountDownLatch latch = new CountDownLatch(1); + final AtomicInteger count = new AtomicInteger(); + final int SIZE = 2000; + Observable<Long> timer = Observable.interval(0, 1, TimeUnit.MILLISECONDS) + .observeOn(Schedulers.newThread()) + .doOnEach(new Consumer<Notification<Long>>() { + @Override + public void accept(Notification<Long> n) { + // System.out.println(n); + if (count.incrementAndGet() >= SIZE) { + latch.countDown(); + } + } + }).take(SIZE); + + TestObserver<Long> to = new TestObserver<>(); + + Observable.combineLatest(timer, Observable.<Integer> never(), new BiFunction<Long, Integer, Long>() { + @Override + public Long apply(Long t1, Integer t2) { + return t1; + } + }).subscribe(to); + + if (!latch.await(SIZE + 1000, TimeUnit.MILLISECONDS)) { + fail("timed out"); + } + + assertEquals(SIZE, count.get()); + } + + @SuppressWarnings("unchecked") + @Test + public void combineLatestArrayOfSources() { + + Observable.combineLatestArray(new ObservableSource[] { + Observable.just(1), Observable.just(2) + }, new Function<Object[], Object>() { + @Override + public Object apply(Object[] a) throws Exception { + return Arrays.toString(a); + } + }) + .test() + .assertResult("[1, 2]"); + } + + @Test + @SuppressWarnings("unchecked") + public void combineLatestDelayErrorArrayOfSources() { + + Observable.combineLatestArrayDelayError(new ObservableSource[] { + Observable.just(1), Observable.just(2) + }, new Function<Object[], Object>() { + @Override + public Object apply(Object[] a) throws Exception { + return Arrays.toString(a); + } + }) + .test() + .assertResult("[1, 2]"); + } + + @Test + @SuppressWarnings("unchecked") + public void combineLatestDelayErrorArrayOfSourcesWithError() { + + Observable.combineLatestArrayDelayError(new ObservableSource[] { + Observable.just(1), Observable.just(2).concatWith(Observable.<Integer>error(new TestException())) + }, new Function<Object[], Object>() { + @Override + public Object apply(Object[] a) throws Exception { + return Arrays.toString(a); + } + }) + .test() + .assertFailure(TestException.class, "[1, 2]"); + } + + @Test + public void combineLatestDelayErrorIterableOfSources() { + + Observable.combineLatestDelayError(Arrays.asList( + Observable.just(1), Observable.just(2) + ), new Function<Object[], Object>() { + @Override + public Object apply(Object[] a) throws Exception { + return Arrays.toString(a); + } + }) + .test() + .assertResult("[1, 2]"); + } + + @Test + public void combineLatestDelayErrorIterableOfSourcesWithError() { + + Observable.combineLatestDelayError(Arrays.asList( + Observable.just(1), Observable.just(2).concatWith(Observable.<Integer>error(new TestException())) + ), new Function<Object[], Object>() { + @Override + public Object apply(Object[] a) throws Exception { + return Arrays.toString(a); + } + }) + .test() + .assertFailure(TestException.class, "[1, 2]"); + } + + @SuppressWarnings("unchecked") + @Test + public void combineLatestArrayEmpty() { + assertSame(Observable.empty(), Observable.combineLatestArray(new ObservableSource[0], Functions.<Object[]>identity(), 16)); + } + + @SuppressWarnings("unchecked") + @Test + public void combineLatestDelayErrorEmpty() { + assertSame(Observable.empty(), Observable.combineLatestArrayDelayError(new ObservableSource[0], Functions.<Object[]>identity(), 16)); + } + + @Test + public void disposed() { + TestHelper.checkDisposed(Observable.combineLatest(Observable.never(), Observable.never(), new BiFunction<Object, Object, Object>() { + @Override + public Object apply(Object a, Object b) throws Exception { + return a; + } + })); + } + + @Test + public void cancelWhileSubscribing() { + final TestObserver<Object> to = new TestObserver<>(); + + Observable.combineLatest( + Observable.just(1) + .doOnNext(new Consumer<Integer>() { + @Override + public void accept(Integer v) throws Exception { + to.dispose(); + } + }), + Observable.never(), + new BiFunction<Object, Object, Object>() { + @Override + public Object apply(Object a, Object b) throws Exception { + return a; + } + }) + .subscribe(to); + } + + @Test + public void combineAsync() { + Observable<Integer> source = Observable.range(1, 1000).subscribeOn(Schedulers.computation()); + + Observable.combineLatest(source, source, new BiFunction<Object, Object, Object>() { + @Override + public Object apply(Object a, Object b) throws Exception { + return a; + } + }) + .take(500) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertNoErrors() + .assertComplete(); + } + + @Test + public void error() { + Observable.combineLatest(Observable.never(), Observable.error(new TestException()), new BiFunction<Object, Object, Object>() { + @Override + public Object apply(Object a, Object b) throws Exception { + return a; + } + }) + .test() + .assertFailure(TestException.class); + } + + @SuppressWarnings("unchecked") + @Test + public void errorDelayed() { + Observable.combineLatestArrayDelayError( + new ObservableSource[] { Observable.error(new TestException()), Observable.just(1) }, + new Function<Object[], Object>() { + @Override + public Object apply(Object[] a) throws Exception { + return a; + } + }, + 128 + ) + .test() + .assertFailure(TestException.class); + } + + @SuppressWarnings("unchecked") + @Test + public void errorDelayed2() { + Observable.combineLatestArrayDelayError( + new ObservableSource[] { Observable.error(new TestException()).startWithItem(1), Observable.empty() }, + new Function<Object[], Object>() { + @Override + public Object apply(Object[] a) throws Exception { + return a; + } + }, + 128 + ) + .test() + .assertFailure(TestException.class); + } + + @Test + public void onErrorRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + final PublishSubject<Integer> ps1 = PublishSubject.create(); + final PublishSubject<Integer> ps2 = PublishSubject.create(); + + TestObserverEx<Integer> to = Observable.combineLatest(ps1, ps2, new BiFunction<Integer, Integer, Integer>() { + @Override + public Integer apply(Integer a, Integer b) throws Exception { + return a; + } + }).to(TestHelper.<Integer>testConsumer()); + + final TestException ex1 = new TestException(); + final TestException ex2 = new TestException(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + ps1.onError(ex1); + } + }; + Runnable r2 = new Runnable() { + @Override + public void run() { + ps2.onError(ex2); + } + }; + + TestHelper.race(r1, r2); + + if (to.errors().size() != 0) { + if (to.errors().get(0) instanceof CompositeException) { + to.assertSubscribed() + .assertNotComplete() + .assertNoValues(); + + for (Throwable e : TestHelper.errorList(to)) { + assertTrue(e.toString(), e instanceof TestException); + } + + } else { + to.assertFailure(TestException.class); + } + } + + for (Throwable e : errors) { + assertTrue(e.toString(), e.getCause() instanceof TestException); + } + } finally { + RxJavaPlugins.reset(); + } + } + } + + @Test + public void dontSubscribeIfDone() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + final int[] count = { 0 }; + + Observable.combineLatest(Observable.empty(), + Observable.error(new TestException()) + .doOnSubscribe(new Consumer<Disposable>() { + @Override + public void accept(Disposable d) throws Exception { + count[0]++; + } + }), + new BiFunction<Object, Object, Object>() { + @Override + public Object apply(Object a, Object b) throws Exception { + return 0; + } + }) + .test() + .assertResult(); + + assertEquals(0, count[0]); + + assertTrue(errors.toString(), errors.isEmpty()); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void dontSubscribeIfDone2() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + final int[] count = { 0 }; + + Observable.combineLatestDelayError( + Arrays.asList(Observable.empty(), + Observable.error(new TestException()) + .doOnSubscribe(new Consumer<Disposable>() { + @Override + public void accept(Disposable d) throws Exception { + count[0]++; + } + }) + ), + new Function<Object[], Object>() { + @Override + public Object apply(Object[] a) throws Exception { + return 0; + } + }) + .test() + .assertResult(); + + assertEquals(0, count[0]); + + assertTrue(errors.toString(), errors.isEmpty()); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void combine2Observable2Errors() throws Exception { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + TestObserver<Object> testObserver = TestObserver.create(); + + TestScheduler testScheduler = new TestScheduler(); + + Observable<Integer> emptyObservable = Observable.timer(10, TimeUnit.MILLISECONDS, testScheduler) + .flatMap(new Function<Long, ObservableSource<Integer>>() { + @Override + public ObservableSource<Integer> apply(Long aLong) throws Exception { + return Observable.error(new Exception()); + } + }); + Observable<Object> errorObservable = Observable.timer(100, TimeUnit.MILLISECONDS, testScheduler).map(new Function<Long, Object>() { + @Override + public Object apply(Long aLong) throws Exception { + throw new Exception(); + } + }); + + Observable.combineLatestDelayError( + Arrays.asList( + emptyObservable + .doOnEach(new Consumer<Notification<Integer>>() { + @Override + public void accept(Notification<Integer> integerNotification) throws Exception { + System.out.println("emptyObservable: " + integerNotification); + } + }) + .doFinally(new Action() { + @Override + public void run() throws Exception { + System.out.println("emptyObservable: doFinally"); + } + }), + errorObservable + .doOnEach(new Consumer<Notification<Object>>() { + @Override + public void accept(Notification<Object> integerNotification) throws Exception { + System.out.println("errorObservable: " + integerNotification); + } + }) + .doFinally(new Action() { + @Override + public void run() throws Exception { + System.out.println("errorObservable: doFinally"); + } + })), + new Function<Object[], Object>() { + @Override + public Object apply(Object[] objects) throws Exception { + return 0; + } + } + ) + .doOnEach(new Consumer<Notification<Object>>() { + @Override + public void accept(Notification<Object> integerNotification) throws Exception { + System.out.println("combineLatestDelayError: " + integerNotification); + } + }) + .doFinally(new Action() { + @Override + public void run() throws Exception { + System.out.println("combineLatestDelayError: doFinally"); + } + }) + .subscribe(testObserver); + + testScheduler.advanceTimeBy(100, TimeUnit.MILLISECONDS); + + testObserver.awaitDone(5, TimeUnit.SECONDS); + + assertTrue(errors.toString(), errors.isEmpty()); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void eagerDispose() { + final PublishSubject<Integer> ps1 = PublishSubject.create(); + final PublishSubject<Integer> ps2 = PublishSubject.create(); + + TestObserver<Integer> to = new TestObserver<Integer>() { + @Override + public void onNext(Integer t) { + super.onNext(t); + dispose(); + if (ps1.hasObservers()) { + onError(new IllegalStateException("ps1 not disposed")); + } else + if (ps2.hasObservers()) { + onError(new IllegalStateException("ps2 not disposed")); + } else { + onComplete(); + } + } + }; + + Observable.combineLatest(ps1, ps2, new BiFunction<Integer, Integer, Integer>() { + @Override + public Integer apply(Integer t1, Integer t2) throws Exception { + return t1 + t2; + } + }) + .subscribe(to); + + ps1.onNext(1); + ps2.onNext(2); + to.assertResult(3); + } + + @Test + public void syncFirstErrorsAfterItemDelayError() { + Observable.combineLatestDelayError(Arrays.asList( + Observable.just(21).concatWith(Observable.<Integer>error(new TestException())), + Observable.just(21).delay(100, TimeUnit.MILLISECONDS) + ), + new Function<Object[], Object>() { + @Override + public Object apply(Object[] a) throws Exception { + return (Integer)a[0] + (Integer)a[1]; + } + } + ) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertFailure(TestException.class, 42); + } + + @Test + public void observableSourcesInIterable() { + ObservableSource<Integer> source = new ObservableSource<Integer>() { + @Override + public void subscribe(Observer<? super Integer> observer) { + Observable.just(1).subscribe(observer); + } + }; + + Observable.combineLatest(Arrays.asList(source, source), new Function<Object[], Integer>() { + @Override + public Integer apply(Object[] t) throws Throwable { + return 2; + } + }) + .test() + .assertResult(2); + } + + @Test + public void onCompleteDisposeRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + + TestObserver<Integer> to = new TestObserver<>(); + PublishSubject<Integer> ps = PublishSubject.create(); + + Observable.combineLatest(ps, Observable.never(), (a, b) -> a) + .subscribe(to); + + TestHelper.race(() -> ps.onComplete(), () -> to.dispose()); + } + } + + @Test + public void onErrorDisposeDelayErrorRace() throws Throwable { + TestHelper.withErrorTracking(errors -> { + TestException ex = new TestException(); + + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + + TestObserverEx<Object[]> to = new TestObserverEx<>(); + AtomicReference<Observer<? super Object>> ref = new AtomicReference<>(); + Observable<Object> o = new Observable<Object>() { + @Override + public void subscribeActual(Observer<? super Object> observer) { + ref.set(observer); + } + }; + + Observable.combineLatestDelayError(Arrays.asList(o, Observable.never()), (a) -> a) + .subscribe(to); + + ref.get().onSubscribe(Disposable.empty()); + + TestHelper.race(() -> ref.get().onError(ex), () -> to.dispose()); + + if (to.errors().isEmpty()) { + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } + } + }); + } + + @Test + public void doneButNotEmpty() { + PublishSubject<Integer> ps1 = PublishSubject.create(); + PublishSubject<Integer> ps2 = PublishSubject.create(); + + TestObserver<Integer> to = Observable.combineLatest(ps1, ps2, (a, b) -> a + b) + .doOnNext(v -> { + if (v == 2) { + ps2.onNext(3); + ps2.onComplete(); + ps1.onComplete(); + } + }) + .test(); + + ps1.onNext(1); + ps2.onNext(1); + + to.assertResult(2, 4); + } + + @Test + public void iterableNullPublisher() { + Observable.combineLatest(Arrays.asList(Observable.never(), null), (a) -> a) + .test() + .assertFailure(NullPointerException.class); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableConcatMapCompletableTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableConcatMapCompletableTest.java new file mode 100644 index 0000000000..931fcd403f --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableConcatMapCompletableTest.java @@ -0,0 +1,252 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import static org.junit.Assert.assertTrue; + +import java.util.List; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.subjects.*; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class ObservableConcatMapCompletableTest extends RxJavaTest { + + @Test + public void asyncFused() throws Exception { + UnicastSubject<Integer> us = UnicastSubject.create(); + + TestObserver<Void> to = us.concatMapCompletable(completableComplete(), 2).test(); + + us.onNext(1); + us.onComplete(); + + to.assertComplete(); + to.assertValueCount(0); + } + + @Test + public void notFused() throws Exception { + UnicastSubject<Integer> us = UnicastSubject.create(); + TestObserver<Void> to = us.hide().concatMapCompletable(completableComplete(), 2).test(); + + us.onNext(1); + us.onNext(2); + us.onComplete(); + + to.assertComplete(); + to.assertValueCount(0); + to.assertNoErrors(); + } + + @Test + public void dispose() { + TestHelper.checkDisposed(Observable.<Integer>just(1).hide() + .concatMapCompletable(completableError())); + } + + @Test + public void mainError() { + Observable.<Integer>error(new TestException()) + .concatMapCompletable(completableComplete()) + .test() + .assertFailure(TestException.class); + } + + @Test + public void innerError() { + Observable.<Integer>just(1).hide() + .concatMapCompletable(completableError()) + .test() + .assertFailure(TestException.class); + } + + @Test + public void badSource() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + new Observable<Integer>() { + @Override + protected void subscribeActual(Observer<? super Integer> observer) { + observer.onSubscribe(Disposable.empty()); + + observer.onNext(1); + observer.onComplete(); + observer.onNext(2); + observer.onError(new TestException()); + observer.onComplete(); + } + } + .concatMapCompletable(completableComplete()) + .test() + .assertComplete(); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void onErrorRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + final PublishSubject<Integer> ps1 = PublishSubject.create(); + final PublishSubject<Integer> ps2 = PublishSubject.create(); + + TestObserver<Void> to = ps1.concatMapCompletable(new Function<Integer, CompletableSource>() { + @Override + public CompletableSource apply(Integer v) throws Exception { + return Completable.fromObservable(ps2); + } + }).test(); + + final TestException ex1 = new TestException(); + final TestException ex2 = new TestException(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + ps1.onError(ex1); + } + }; + Runnable r2 = new Runnable() { + @Override + public void run() { + ps2.onError(ex2); + } + }; + + TestHelper.race(r1, r2); + + to.assertFailure(TestException.class); + + if (!errors.isEmpty()) { + TestHelper.assertError(errors, 0, TestException.class); + } + } finally { + RxJavaPlugins.reset(); + } + } + } + + @Test + public void mapperThrows() { + Observable.just(1).hide() + .concatMapCompletable(completableThrows()) + .test() + .assertFailure(TestException.class); + } + + @Test + public void fusedPollThrows() { + Observable.just(1) + .map(new Function<Integer, Integer>() { + @Override + public Integer apply(Integer v) throws Exception { + throw new TestException(); + } + }) + .concatMapCompletable(completableComplete()) + .test() + .assertFailure(TestException.class); + } + + @Test + public void concatReportsDisposedOnComplete() { + final Disposable[] disposable = { null }; + + Observable.just(1) + .hide() + .concatMapCompletable(completableComplete()) + .subscribe(new CompletableObserver() { + + @Override + public void onSubscribe(Disposable d) { + disposable[0] = d; + } + + @Override + public void onError(Throwable e) { + } + + @Override + public void onComplete() { + } + }); + + assertTrue(disposable[0].isDisposed()); + } + + @Test + public void concatReportsDisposedOnError() { + final Disposable[] disposable = { null }; + + Observable.just(1) + .hide() + .concatMapCompletable(completableError()) + .subscribe(new CompletableObserver() { + + @Override + public void onSubscribe(Disposable d) { + disposable[0] = d; + } + + @Override + public void onError(Throwable e) { + } + + @Override + public void onComplete() { + } + }); + + assertTrue(disposable[0].isDisposed()); + } + + private Function<Integer, CompletableSource> completableComplete() { + return new Function<Integer, CompletableSource>() { + @Override + public CompletableSource apply(Integer v) throws Exception { + return Completable.complete(); + } + }; + } + + private Function<Integer, CompletableSource> completableError() { + return new Function<Integer, CompletableSource>() { + @Override + public CompletableSource apply(Integer v) throws Exception { + return Completable.error(new TestException()); + } + }; + } + + private Function<Integer, CompletableSource> completableThrows() { + return new Function<Integer, CompletableSource>() { + @Override + public CompletableSource apply(Integer v) throws Exception { + throw new TestException(); + } + }; + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableConcatMapEagerTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableConcatMapEagerTest.java new file mode 100644 index 0000000000..1e75ff9aef --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableConcatMapEagerTest.java @@ -0,0 +1,1090 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import static org.junit.Assert.*; + +import java.lang.reflect.Method; +import java.util.*; +import java.util.concurrent.*; +import java.util.concurrent.atomic.*; + +import org.junit.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.schedulers.Schedulers; +import io.reactivex.rxjava3.subjects.*; +import io.reactivex.rxjava3.testsupport.*; + +public class ObservableConcatMapEagerTest extends RxJavaTest { + + @Test + public void normal() { + Observable.range(1, 5) + .concatMapEager(new Function<Integer, ObservableSource<Integer>>() { + @Override + public ObservableSource<Integer> apply(Integer t) { + return Observable.range(t, 2); + } + }) + .test() + .assertResult(1, 2, 2, 3, 3, 4, 4, 5, 5, 6); + } + + @Test + public void normalDelayBoundary() { + Observable.range(1, 5) + .concatMapEagerDelayError(new Function<Integer, ObservableSource<Integer>>() { + @Override + public ObservableSource<Integer> apply(Integer t) { + return Observable.range(t, 2); + } + }, false) + .test() + .assertResult(1, 2, 2, 3, 3, 4, 4, 5, 5, 6); + } + + @Test + public void normalDelayEnd() { + Observable.range(1, 5) + .concatMapEagerDelayError(new Function<Integer, ObservableSource<Integer>>() { + @Override + public ObservableSource<Integer> apply(Integer t) { + return Observable.range(t, 2); + } + }, true) + .test() + .assertResult(1, 2, 2, 3, 3, 4, 4, 5, 5, 6); + } + + @Test + public void mainErrorsDelayBoundary() { + PublishSubject<Integer> main = PublishSubject.create(); + final PublishSubject<Integer> inner = PublishSubject.create(); + + TestObserverEx<Integer> to = main.concatMapEagerDelayError( + new Function<Integer, ObservableSource<Integer>>() { + @Override + public ObservableSource<Integer> apply(Integer t) { + return inner; + } + }, false).to(TestHelper.<Integer>testConsumer()); + + main.onNext(1); + + inner.onNext(2); + + to.assertValue(2); + + main.onError(new TestException("Forced failure")); + + to.assertNoErrors(); + + inner.onNext(3); + inner.onComplete(); + + to.assertFailureAndMessage(TestException.class, "Forced failure", 2, 3); + } + + @Test + public void mainErrorsDelayEnd() { + PublishSubject<Integer> main = PublishSubject.create(); + final PublishSubject<Integer> inner = PublishSubject.create(); + + TestObserverEx<Integer> to = main.concatMapEagerDelayError( + new Function<Integer, ObservableSource<Integer>>() { + @Override + public ObservableSource<Integer> apply(Integer t) { + return inner; + } + }, true).to(TestHelper.<Integer>testConsumer()); + + main.onNext(1); + main.onNext(2); + + inner.onNext(2); + + to.assertValue(2); + + main.onError(new TestException("Forced failure")); + + to.assertNoErrors(); + + inner.onNext(3); + inner.onComplete(); + + to.assertFailureAndMessage(TestException.class, "Forced failure", 2, 3, 2, 3); + } + + @Test + public void mainErrorsImmediate() { + PublishSubject<Integer> main = PublishSubject.create(); + final PublishSubject<Integer> inner = PublishSubject.create(); + + TestObserverEx<Integer> to = main.concatMapEager( + new Function<Integer, ObservableSource<Integer>>() { + @Override + public ObservableSource<Integer> apply(Integer t) { + return inner; + } + }).to(TestHelper.<Integer>testConsumer()); + + main.onNext(1); + main.onNext(2); + + inner.onNext(2); + + to.assertValue(2); + + main.onError(new TestException("Forced failure")); + + assertFalse("inner has subscribers?", inner.hasObservers()); + + inner.onNext(3); + inner.onComplete(); + + to.assertFailureAndMessage(TestException.class, "Forced failure", 2); + } + + @Test + public void longEager() { + + Observable.range(1, 2 * Observable.bufferSize()) + .concatMapEager(new Function<Integer, ObservableSource<Integer>>() { + @Override + public ObservableSource<Integer> apply(Integer v) { + return Observable.just(1); + } + }) + .test() + .assertValueCount(2 * Observable.bufferSize()) + .assertNoErrors() + .assertComplete(); + } + + TestObserver<Object> to; + + Function<Integer, Observable<Integer>> toJust = new Function<Integer, Observable<Integer>>() { + @Override + public Observable<Integer> apply(Integer t) { + return Observable.just(t); + } + }; + + Function<Integer, Observable<Integer>> toRange = new Function<Integer, Observable<Integer>>() { + @Override + public Observable<Integer> apply(Integer t) { + return Observable.range(t, 2); + } + }; + + @Before + public void before() { + to = new TestObserver<>(); + } + + @Test + public void simple() { + Observable.range(1, 100).concatMapEager(toJust).subscribe(to); + + to.assertNoErrors(); + to.assertValueCount(100); + to.assertComplete(); + } + + @Test + public void simple2() { + Observable.range(1, 100).concatMapEager(toRange).subscribe(to); + + to.assertNoErrors(); + to.assertValueCount(200); + to.assertComplete(); + } + + @Test + public void eagerness2() { + final AtomicInteger count = new AtomicInteger(); + Observable<Integer> source = Observable.just(1).doOnNext(new Consumer<Integer>() { + @Override + public void accept(Integer t) { + count.getAndIncrement(); + } + }).hide(); + + Observable.concatArrayEager(source, source).subscribe(to); + + Assert.assertEquals(2, count.get()); + + to.assertValueCount(count.get()); + to.assertNoErrors(); + to.assertComplete(); + } + + @Test + public void eagerness3() { + final AtomicInteger count = new AtomicInteger(); + Observable<Integer> source = Observable.just(1).doOnNext(new Consumer<Integer>() { + @Override + public void accept(Integer t) { + count.getAndIncrement(); + } + }).hide(); + + Observable.concatArrayEager(source, source, source).subscribe(to); + + Assert.assertEquals(3, count.get()); + + to.assertValueCount(count.get()); + to.assertNoErrors(); + to.assertComplete(); + } + + @Test + public void eagerness4() { + final AtomicInteger count = new AtomicInteger(); + Observable<Integer> source = Observable.just(1).doOnNext(new Consumer<Integer>() { + @Override + public void accept(Integer t) { + count.getAndIncrement(); + } + }).hide(); + + Observable.concatArrayEager(source, source, source, source).subscribe(to); + + Assert.assertEquals(4, count.get()); + + to.assertValueCount(count.get()); + to.assertNoErrors(); + to.assertComplete(); + } + + @Test + public void eagerness5() { + final AtomicInteger count = new AtomicInteger(); + Observable<Integer> source = Observable.just(1).doOnNext(new Consumer<Integer>() { + @Override + public void accept(Integer t) { + count.getAndIncrement(); + } + }).hide(); + + Observable.concatArrayEager(source, source, source, source, source).subscribe(to); + + Assert.assertEquals(5, count.get()); + + to.assertValueCount(count.get()); + to.assertNoErrors(); + to.assertComplete(); + } + + @Test + public void eagerness6() { + final AtomicInteger count = new AtomicInteger(); + Observable<Integer> source = Observable.just(1).doOnNext(new Consumer<Integer>() { + @Override + public void accept(Integer t) { + count.getAndIncrement(); + } + }).hide(); + + Observable.concatArrayEager(source, source, source, source, source, source).subscribe(to); + + Assert.assertEquals(6, count.get()); + + to.assertValueCount(count.get()); + to.assertNoErrors(); + to.assertComplete(); + } + + @Test + public void eagerness7() { + final AtomicInteger count = new AtomicInteger(); + Observable<Integer> source = Observable.just(1).doOnNext(new Consumer<Integer>() { + @Override + public void accept(Integer t) { + count.getAndIncrement(); + } + }).hide(); + + Observable.concatArrayEager(source, source, source, source, source, source, source).subscribe(to); + + Assert.assertEquals(7, count.get()); + + to.assertValueCount(count.get()); + to.assertNoErrors(); + to.assertComplete(); + } + + @Test + public void eagerness8() { + final AtomicInteger count = new AtomicInteger(); + Observable<Integer> source = Observable.just(1).doOnNext(new Consumer<Integer>() { + @Override + public void accept(Integer t) { + count.getAndIncrement(); + } + }).hide(); + + Observable.concatArrayEager(source, source, source, source, source, source, source, source).subscribe(to); + + Assert.assertEquals(8, count.get()); + + to.assertValueCount(count.get()); + to.assertNoErrors(); + to.assertComplete(); + } + + @Test + public void eagerness9() { + final AtomicInteger count = new AtomicInteger(); + Observable<Integer> source = Observable.just(1).doOnNext(new Consumer<Integer>() { + @Override + public void accept(Integer t) { + count.getAndIncrement(); + } + }).hide(); + + Observable.concatArrayEager(source, source, source, source, source, source, source, source, source).subscribe(to); + + Assert.assertEquals(9, count.get()); + + to.assertValueCount(count.get()); + to.assertNoErrors(); + to.assertComplete(); + } + + @Test + public void mainError() { + Observable.<Integer>error(new TestException()).concatMapEager(toJust).subscribe(to); + + to.assertNoValues(); + to.assertError(TestException.class); + to.assertNotComplete(); + } + + @Test + public void innerError() { + // TODO verify: concatMapEager subscribes first then consumes the sources is okay + + PublishSubject<Integer> ps = PublishSubject.create(); + + Observable.concatArrayEager(Observable.just(1), ps) + .subscribe(to); + + ps.onError(new TestException()); + + to.assertValue(1); + to.assertError(TestException.class); + to.assertNotComplete(); + } + + @Test + public void innerEmpty() { + Observable.concatArrayEager(Observable.empty(), Observable.empty()).subscribe(to); + + to.assertNoValues(); + to.assertNoErrors(); + to.assertComplete(); + } + + @Test + public void mapperThrows() { + Observable.just(1).concatMapEager(new Function<Integer, Observable<Integer>>() { + @Override + public Observable<Integer> apply(Integer t) { + throw new TestException(); + } + }).subscribe(to); + + to.assertNoValues(); + to.assertNotComplete(); + to.assertError(TestException.class); + } + + @Test(expected = IllegalArgumentException.class) + public void invalidMaxConcurrent() { + Observable.just(1).concatMapEager(toJust, 0, Observable.bufferSize()); + } + + @Test(expected = IllegalArgumentException.class) + public void invalidCapacityHint() { + Observable.just(1).concatMapEager(toJust, Observable.bufferSize(), 0); + } + + @Test + public void asynchronousRun() { + Observable.range(1, 2).concatMapEager(new Function<Integer, Observable<Integer>>() { + @Override + public Observable<Integer> apply(Integer t) { + return Observable.range(1, 1000).subscribeOn(Schedulers.computation()); + } + }).observeOn(Schedulers.newThread()).subscribe(to); + + to.awaitDone(5, TimeUnit.SECONDS); + to.assertNoErrors(); + to.assertValueCount(2000); + } + + @Test + public void reentrantWork() { + final PublishSubject<Integer> subject = PublishSubject.create(); + + final AtomicBoolean once = new AtomicBoolean(); + + subject.concatMapEager(new Function<Integer, Observable<Integer>>() { + @Override + public Observable<Integer> apply(Integer t) { + return Observable.just(t); + } + }) + .doOnNext(new Consumer<Integer>() { + @Override + public void accept(Integer t) { + if (once.compareAndSet(false, true)) { + subject.onNext(2); + } + } + }) + .subscribe(to); + + subject.onNext(1); + + to.assertNoErrors(); + to.assertNotComplete(); + to.assertValues(1, 2); + } + + @SuppressWarnings("unchecked") + @Test + public void concatArrayEager() throws Exception { + for (int i = 2; i < 10; i++) { + Observable<Integer>[] obs = new Observable[i]; + Arrays.fill(obs, Observable.just(1)); + + Integer[] expected = new Integer[i]; + Arrays.fill(expected, 1); + + Method m = Observable.class.getMethod("concatArrayEager", ObservableSource[].class); + + TestObserver<Integer> to = TestObserver.create(); + + ((Observable<Integer>)m.invoke(null, new Object[]{obs})).subscribe(to); + + to.assertValues(expected); + to.assertNoErrors(); + to.assertComplete(); + } + } + + @Test + public void capacityHint() { + Observable<Integer> source = Observable.just(1); + TestObserver<Integer> to = TestObserver.create(); + + Observable.concatEager(Arrays.asList(source, source, source), 1, 1).subscribe(to); + + to.assertValues(1, 1, 1); + to.assertNoErrors(); + to.assertComplete(); + } + + @Test + public void Observable() { + Observable<Integer> source = Observable.just(1); + TestObserver<Integer> to = TestObserver.create(); + + Observable.concatEager(Observable.just(source, source, source)).subscribe(to); + + to.assertValues(1, 1, 1); + to.assertNoErrors(); + to.assertComplete(); + } + + @Test + public void ObservableCapacityHint() { + Observable<Integer> source = Observable.just(1); + TestObserver<Integer> to = TestObserver.create(); + + Observable.concatEager(Observable.just(source, source, source), 1, 1).subscribe(to); + + to.assertValues(1, 1, 1); + to.assertNoErrors(); + to.assertComplete(); + } + + @Test + public void badCapacityHint() throws Exception { + Observable<Integer> source = Observable.just(1); + try { + Observable.concatEager(Arrays.asList(source, source, source), 1, -99); + } catch (IllegalArgumentException ex) { + assertEquals("bufferSize > 0 required but it was -99", ex.getMessage()); + } + + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + @Test + public void mappingBadCapacityHint() throws Exception { + Observable<Integer> source = Observable.just(1); + try { + Observable.just(source, source, source).concatMapEager((Function)Functions.identity(), 10, -99); + } catch (IllegalArgumentException ex) { + assertEquals("bufferSize > 0 required but it was -99", ex.getMessage()); + } + + } + + @Test + public void concatEagerIterable() { + Observable.concatEager(Arrays.asList(Observable.just(1), Observable.just(2))) + .test() + .assertResult(1, 2); + } + + @Test + public void dispose() { + TestHelper.checkDisposed(Observable.just(1).hide().concatMapEager(new Function<Integer, ObservableSource<Integer>>() { + @Override + public ObservableSource<Integer> apply(Integer v) throws Exception { + return Observable.range(1, 2); + } + })); + } + + @Test + public void empty() { + Observable.<Integer>empty().hide().concatMapEager(new Function<Integer, ObservableSource<Integer>>() { + @Override + public ObservableSource<Integer> apply(Integer v) throws Exception { + return Observable.range(1, 2); + } + }) + .test() + .assertResult(); + } + + @Test + public void innerError2() { + Observable.<Integer>just(1).hide().concatMapEager(new Function<Integer, ObservableSource<Integer>>() { + @Override + public ObservableSource<Integer> apply(Integer v) throws Exception { + return Observable.error(new TestException()); + } + }) + .test() + .assertFailure(TestException.class); + } + + @Test + public void innerErrorMaxConcurrency() { + Observable.<Integer>just(1).hide().concatMapEager(new Function<Integer, ObservableSource<Integer>>() { + @Override + public ObservableSource<Integer> apply(Integer v) throws Exception { + return Observable.error(new TestException()); + } + }, 1, 128) + .test() + .assertFailure(TestException.class); + } + + @Test + public void innerCallableThrows() { + Observable.<Integer>just(1).hide().concatMapEager(new Function<Integer, ObservableSource<Integer>>() { + @Override + public ObservableSource<Integer> apply(Integer v) throws Exception { + return Observable.fromCallable(new Callable<Integer>() { + @Override + public Integer call() throws Exception { + throw new TestException(); + } + }); + } + }) + .test() + .assertFailure(TestException.class); + } + + @Test + public void innerOuterRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + final PublishSubject<Integer> ps1 = PublishSubject.create(); + final PublishSubject<Integer> ps2 = PublishSubject.create(); + + TestObserverEx<Integer> to = ps1.concatMapEager(new Function<Integer, ObservableSource<Integer>>() { + @Override + public ObservableSource<Integer> apply(Integer v) throws Exception { + return ps2; + } + }).to(TestHelper.<Integer>testConsumer()); + + final TestException ex1 = new TestException(); + final TestException ex2 = new TestException(); + + ps1.onNext(1); + + Runnable r1 = new Runnable() { + @Override + public void run() { + ps1.onError(ex1); + } + }; + Runnable r2 = new Runnable() { + @Override + public void run() { + ps2.onError(ex2); + } + }; + + TestHelper.race(r1, r2); + + to.assertSubscribed().assertNoValues().assertNotComplete(); + + Throwable ex = to.errors().get(0); + + if (ex instanceof CompositeException) { + List<Throwable> es = TestHelper.errorList(to); + TestHelper.assertError(es, 0, TestException.class); + TestHelper.assertError(es, 1, TestException.class); + } else { + to.assertError(TestException.class); + if (!errors.isEmpty()) { + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } + } + } finally { + RxJavaPlugins.reset(); + } + } + } + + @Test + public void nextCancelRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final PublishSubject<Integer> ps1 = PublishSubject.create(); + + final TestObserver<Integer> to = ps1.concatMapEager(new Function<Integer, ObservableSource<Integer>>() { + @Override + public ObservableSource<Integer> apply(Integer v) throws Exception { + return Observable.never(); + } + }).test(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + ps1.onNext(1); + } + }; + Runnable r2 = new Runnable() { + @Override + public void run() { + to.dispose(); + } + }; + + TestHelper.race(r1, r2); + + to.assertEmpty(); + } + } + + @Test + public void mapperCancels() { + final TestObserver<Integer> to = new TestObserver<>(); + + Observable.just(1).hide() + .concatMapEager(new Function<Integer, ObservableSource<Integer>>() { + @Override + public ObservableSource<Integer> apply(Integer v) throws Exception { + to.dispose(); + return Observable.never(); + } + }, 1, 128) + .subscribe(to); + + to.assertEmpty(); + } + + @Test + public void innerErrorFused() { + Observable.<Integer>just(1).hide().concatMapEager(new Function<Integer, ObservableSource<Integer>>() { + @Override + public ObservableSource<Integer> apply(Integer v) throws Exception { + return Observable.range(1, 2).map(new Function<Integer, Integer>() { + @Override + public Integer apply(Integer v) throws Exception { + throw new TestException(); + } + }); + } + }) + .test() + .assertFailure(TestException.class); + } + + @Test + public void innerErrorAfterPoll() { + final UnicastSubject<Integer> us = UnicastSubject.create(); + us.onNext(1); + + TestObserver<Integer> to = new TestObserver<Integer>() { + @Override + public void onNext(Integer t) { + super.onNext(t); + us.onError(new TestException()); + } + }; + + Observable.<Integer>just(1).hide() + .concatMapEager(new Function<Integer, ObservableSource<Integer>>() { + @Override + public ObservableSource<Integer> apply(Integer v) throws Exception { + return us; + } + }, 1, 128) + .subscribe(to); + + to + .assertFailure(TestException.class, 1); + } + + @Test + public void fuseAndTake() { + UnicastSubject<Integer> us = UnicastSubject.create(); + + us.onNext(1); + us.onComplete(); + + us.concatMapEager(new Function<Integer, ObservableSource<Integer>>() { + @Override + public ObservableSource<Integer> apply(Integer v) throws Exception { + return Observable.just(1); + } + }) + .take(1) + .test() + .assertResult(1); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeObservable(new Function<Observable<Object>, ObservableSource<Object>>() { + @Override + public ObservableSource<Object> apply(Observable<Object> o) throws Exception { + return o.concatMapEager(new Function<Object, ObservableSource<Object>>() { + @Override + public ObservableSource<Object> apply(Object v) throws Exception { + return Observable.just(v); + } + }); + } + }); + } + + @Test + public void oneDelayed() { + Observable.just(1, 2, 3, 4, 5) + .concatMapEager(new Function<Integer, ObservableSource<Integer>>() { + @Override + public ObservableSource<Integer> apply(Integer i) throws Exception { + return i == 3 ? Observable.just(i) : Observable + .just(i) + .delay(1, TimeUnit.MILLISECONDS, Schedulers.io()); + } + }) + .observeOn(Schedulers.io()) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(1, 2, 3, 4, 5) + ; + } + + @Test + @SuppressWarnings("unchecked") + public void maxConcurrencyOf2() { + List<Integer>[] list = new ArrayList[100]; + for (int i = 0; i < 100; i++) { + List<Integer> lst = new ArrayList<>(); + list[i] = lst; + for (int k = 1; k <= 10; k++) { + lst.add((i) * 10 + k); + } + } + + Observable.range(1, 1000) + .buffer(10) + .concatMapEager(new Function<List<Integer>, ObservableSource<List<Integer>>>() { + @Override + public ObservableSource<List<Integer>> apply(List<Integer> v) + throws Exception { + return Observable.just(v) + .subscribeOn(Schedulers.io()) + .doOnNext(new Consumer<List<Integer>>() { + @Override + public void accept(List<Integer> v) + throws Exception { + Thread.sleep(new Random().nextInt(20)); + } + }); + } + } + , 2, 3) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(list); + } + + @Test + public void arrayDelayErrorDefault() { + PublishSubject<Integer> ps1 = PublishSubject.create(); + PublishSubject<Integer> ps2 = PublishSubject.create(); + PublishSubject<Integer> ps3 = PublishSubject.create(); + + TestObserver<Integer> to = Observable.concatArrayEagerDelayError(ps1, ps2, ps3) + .test(); + + to.assertEmpty(); + + assertTrue(ps1.hasObservers()); + assertTrue(ps2.hasObservers()); + assertTrue(ps3.hasObservers()); + + ps2.onNext(2); + ps2.onComplete(); + + to.assertEmpty(); + + ps1.onNext(1); + + to.assertValuesOnly(1); + + ps1.onComplete(); + + to.assertValuesOnly(1, 2); + + ps3.onComplete(); + + to.assertResult(1, 2); + } + + @Test + public void arrayDelayErrorMaxConcurrency() { + PublishSubject<Integer> ps1 = PublishSubject.create(); + PublishSubject<Integer> ps2 = PublishSubject.create(); + PublishSubject<Integer> ps3 = PublishSubject.create(); + + TestObserver<Integer> to = Observable.concatArrayEagerDelayError(2, 2, ps1, ps2, ps3) + .test(); + + to.assertEmpty(); + + assertTrue(ps1.hasObservers()); + assertTrue(ps2.hasObservers()); + assertFalse(ps3.hasObservers()); + + ps2.onNext(2); + ps2.onComplete(); + + to.assertEmpty(); + + ps1.onNext(1); + + to.assertValuesOnly(1); + + ps1.onComplete(); + + assertTrue(ps3.hasObservers()); + + to.assertValuesOnly(1, 2); + + ps3.onComplete(); + + to.assertResult(1, 2); + } + + @Test + public void arrayDelayErrorMaxConcurrencyErrorDelayed() { + PublishSubject<Integer> ps1 = PublishSubject.create(); + PublishSubject<Integer> ps2 = PublishSubject.create(); + PublishSubject<Integer> ps3 = PublishSubject.create(); + + TestObserver<Integer> to = Observable.concatArrayEagerDelayError(2, 2, ps1, ps2, ps3) + .test(); + + to.assertEmpty(); + + assertTrue(ps1.hasObservers()); + assertTrue(ps2.hasObservers()); + assertFalse(ps3.hasObservers()); + + ps2.onNext(2); + ps2.onError(new TestException()); + + to.assertEmpty(); + + ps1.onNext(1); + + to.assertValuesOnly(1); + + ps1.onComplete(); + + assertTrue(ps3.hasObservers()); + + to.assertValuesOnly(1, 2); + + ps3.onComplete(); + + to.assertFailure(TestException.class, 1, 2); + } + + @Test + public void cancelActive() { + PublishSubject<Integer> ps1 = PublishSubject.create(); + PublishSubject<Integer> ps2 = PublishSubject.create(); + + TestObserver<Integer> to = Observable + .concatEager(Observable.just(ps1, ps2)) + .test(); + + assertTrue(ps1.hasObservers()); + assertTrue(ps2.hasObservers()); + + to.dispose(); + + assertFalse(ps1.hasObservers()); + assertFalse(ps2.hasObservers()); + } + + @Test + public void cancelNoInnerYet() { + PublishSubject<Observable<Integer>> ps1 = PublishSubject.create(); + + TestObserver<Integer> to = Observable + .concatEager(ps1) + .test(); + + assertTrue(ps1.hasObservers()); + + to.dispose(); + + assertFalse(ps1.hasObservers()); + } + + @Test + public void undeliverableUponCancel() { + TestHelper.checkUndeliverableUponCancel(new ObservableConverter<Integer, Observable<Integer>>() { + @Override + public Observable<Integer> apply(Observable<Integer> upstream) { + return upstream.concatMapEager(new Function<Integer, Observable<Integer>>() { + @Override + public Observable<Integer> apply(Integer v) throws Throwable { + return Observable.just(v).hide(); + } + }); + } + }); + } + + @Test + public void undeliverableUponCancelDelayError() { + TestHelper.checkUndeliverableUponCancel(new ObservableConverter<Integer, Observable<Integer>>() { + @Override + public Observable<Integer> apply(Observable<Integer> upstream) { + return upstream.concatMapEagerDelayError(new Function<Integer, Observable<Integer>>() { + @Override + public Observable<Integer> apply(Integer v) throws Throwable { + return Observable.just(v).hide(); + } + }, false); + } + }); + } + + @Test + public void undeliverableUponCancelDelayErrorTillEnd() { + TestHelper.checkUndeliverableUponCancel(new ObservableConverter<Integer, Observable<Integer>>() { + @Override + public Observable<Integer> apply(Observable<Integer> upstream) { + return upstream.concatMapEagerDelayError(new Function<Integer, Observable<Integer>>() { + @Override + public Observable<Integer> apply(Integer v) throws Throwable { + return Observable.just(v).hide(); + } + }, true); + } + }); + } + + @Test + public void iterableDelayError() { + Observable.concatEagerDelayError(Arrays.asList( + Observable.range(1, 2), + Observable.error(new TestException()), + Observable.range(3, 3) + )) + .test() + .assertFailure(TestException.class, 1, 2, 3, 4, 5); + } + + @Test + public void iterableDelayErrorMaxConcurrency() { + Observable.concatEagerDelayError(Arrays.asList( + Observable.range(1, 2), + Observable.error(new TestException()), + Observable.range(3, 3) + ), 1, 1) + .test() + .assertFailure(TestException.class, 1, 2, 3, 4, 5); + } + + @Test + public void observerDelayError() { + Observable.concatEagerDelayError(Observable.fromArray( + Observable.range(1, 2), + Observable.error(new TestException()), + Observable.range(3, 3) + )) + .test() + .assertFailure(TestException.class, 1, 2, 3, 4, 5); + } + + @Test + public void observerDelayErrorMaxConcurrency() { + Observable.concatEagerDelayError(Observable.fromArray( + Observable.range(1, 2), + Observable.error(new TestException()), + Observable.range(3, 3) + ), 1, 1) + .test() + .assertFailure(TestException.class, 1, 2, 3, 4, 5); + } + + @Test + public void innerFusionRejected() { + Observable.just(1) + .hide() + .concatMapEager(v -> TestHelper.rejectObservableFusion()) + .test() + .assertEmpty(); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableConcatMapSchedulerTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableConcatMapSchedulerTest.java new file mode 100644 index 0000000000..f2a6ede314 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableConcatMapSchedulerTest.java @@ -0,0 +1,1124 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import static org.junit.Assert.*; + +import java.lang.reflect.Method; +import java.util.*; +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.Test; + +import io.reactivex.rxjava3.annotations.NonNull; +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.core.Observer; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.disposables.EmptyDisposable; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.internal.schedulers.ImmediateThinScheduler; +import io.reactivex.rxjava3.observers.*; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.schedulers.Schedulers; +import io.reactivex.rxjava3.subjects.*; +import io.reactivex.rxjava3.testsupport.*; + +public class ObservableConcatMapSchedulerTest { + + @Test + public void boundaryFusion() { + Observable.range(1, 10000) + .observeOn(Schedulers.single()) + .map(new Function<Integer, String>() { + @Override + public String apply(Integer t) throws Exception { + String name = Thread.currentThread().getName(); + if (name.contains("RxSingleScheduler")) { + return "RxSingleScheduler"; + } + return name; + } + }) + .concatMap(new Function<String, ObservableSource<? extends Object>>() { + @Override + public ObservableSource<? extends Object> apply(String v) + throws Exception { + return Observable.just(v); + } + }, 2, ImmediateThinScheduler.INSTANCE) + .observeOn(Schedulers.computation()) + .distinct() + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertResult("RxSingleScheduler"); + } + + @Test + public void boundaryFusionDelayError() { + Observable.range(1, 10000) + .observeOn(Schedulers.single()) + .map(new Function<Integer, String>() { + @Override + public String apply(Integer t) throws Exception { + String name = Thread.currentThread().getName(); + if (name.contains("RxSingleScheduler")) { + return "RxSingleScheduler"; + } + return name; + } + }) + .concatMapDelayError(new Function<String, ObservableSource<? extends Object>>() { + @Override + public ObservableSource<? extends Object> apply(String v) + throws Exception { + return Observable.just(v); + } + }, true, 2, ImmediateThinScheduler.INSTANCE) + .observeOn(Schedulers.computation()) + .distinct() + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertResult("RxSingleScheduler"); + } + + @Test + public void pollThrows() { + Observable.just(1) + .map(new Function<Integer, Integer>() { + @Override + public Integer apply(Integer v) throws Exception { + throw new TestException(); + } + }) + .compose(TestHelper.<Integer>observableStripBoundary()) + .concatMap(new Function<Integer, ObservableSource<Integer>>() { + @Override + public ObservableSource<Integer> apply(Integer v) + throws Exception { + return Observable.just(v); + } + }, 2, ImmediateThinScheduler.INSTANCE) + .test() + .assertFailure(TestException.class); + } + + @Test + public void pollThrowsDelayError() { + Observable.just(1) + .map(new Function<Integer, Integer>() { + @Override + public Integer apply(Integer v) throws Exception { + throw new TestException(); + } + }) + .compose(TestHelper.<Integer>observableStripBoundary()) + .concatMapDelayError(new Function<Integer, ObservableSource<Integer>>() { + @Override + public ObservableSource<Integer> apply(Integer v) + throws Exception { + return Observable.just(v); + } + }, true, 2, ImmediateThinScheduler.INSTANCE) + .test() + .assertFailure(TestException.class); + } + + @Test + public void noCancelPrevious() { + final AtomicInteger counter = new AtomicInteger(); + + Observable.range(1, 5) + .concatMap(new Function<Integer, Observable<Integer>>() { + @Override + public Observable<Integer> apply(Integer v) throws Exception { + return Observable.just(v).doOnDispose(new Action() { + @Override + public void run() throws Exception { + counter.getAndIncrement(); + } + }); + } + }, 2, ImmediateThinScheduler.INSTANCE) + .test() + .assertResult(1, 2, 3, 4, 5); + + assertEquals(0, counter.get()); + } + + @Test + public void delayErrorCallableTillTheEnd() { + Observable.just(1, 2, 3, 101, 102, 23, 890, 120, 32) + .concatMapDelayError(new Function<Integer, Observable<Integer>>() { + @Override public Observable<Integer> apply(final Integer integer) throws Exception { + return Observable.fromCallable(new Callable<Integer>() { + @Override public Integer call() throws Exception { + if (integer >= 100) { + throw new NullPointerException("test null exp"); + } + return integer; + } + }); + } + }, true, 2, ImmediateThinScheduler.INSTANCE) + .test() + .assertFailure(CompositeException.class, 1, 2, 3, 23, 32); + } + + @Test + public void delayErrorCallableEager() { + Observable.just(1, 2, 3, 101, 102, 23, 890, 120, 32) + .concatMapDelayError(new Function<Integer, Observable<Integer>>() { + @Override public Observable<Integer> apply(final Integer integer) throws Exception { + return Observable.fromCallable(new Callable<Integer>() { + @Override public Integer call() throws Exception { + if (integer >= 100) { + throw new NullPointerException("test null exp"); + } + return integer; + } + }); + } + }, false, 2, ImmediateThinScheduler.INSTANCE) + .test() + .assertFailure(NullPointerException.class, 1, 2, 3); + } + + @Test + public void mapperScheduled() { + TestObserver<String> to = Observable.just(1) + .concatMap(new Function<Integer, Observable<String>>() { + @Override + public Observable<String> apply(Integer t) throws Throwable { + return Observable.just(Thread.currentThread().getName()); + } + }, 2, Schedulers.single()) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertValueCount(1) + .assertNoErrors() + .assertComplete(); + + assertTrue(to.values().toString(), to.values().get(0).startsWith("RxSingleScheduler-")); + } + + @Test + public void mapperScheduledHidden() { + TestObserver<String> to = Observable.just(1) + .concatMap(new Function<Integer, Observable<String>>() { + @Override + public Observable<String> apply(Integer t) throws Throwable { + return Observable.just(Thread.currentThread().getName()).hide(); + } + }, 2, Schedulers.single()) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertValueCount(1) + .assertNoErrors() + .assertComplete(); + + assertTrue(to.values().toString(), to.values().get(0).startsWith("RxSingleScheduler-")); + } + + @Test + public void mapperDelayErrorScheduled() { + TestObserver<String> to = Observable.just(1) + .concatMapDelayError(new Function<Integer, Observable<String>>() { + @Override + public Observable<String> apply(Integer t) throws Throwable { + return Observable.just(Thread.currentThread().getName()); + } + }, false, 2, Schedulers.single()) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertValueCount(1) + .assertNoErrors() + .assertComplete(); + + assertTrue(to.values().toString(), to.values().get(0).startsWith("RxSingleScheduler-")); + } + + @Test + public void mapperDelayErrorScheduledHidden() { + TestObserver<String> to = Observable.just(1) + .concatMapDelayError(new Function<Integer, Observable<String>>() { + @Override + public Observable<String> apply(Integer t) throws Throwable { + return Observable.just(Thread.currentThread().getName()).hide(); + } + }, false, 2, Schedulers.single()) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertValueCount(1) + .assertNoErrors() + .assertComplete(); + + assertTrue(to.values().toString(), to.values().get(0).startsWith("RxSingleScheduler-")); + } + + @Test + public void mapperDelayError2Scheduled() { + TestObserver<String> to = Observable.just(1) + .concatMapDelayError(new Function<Integer, Observable<String>>() { + @Override + public Observable<String> apply(Integer t) throws Throwable { + return Observable.just(Thread.currentThread().getName()); + } + }, true, 2, Schedulers.single()) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertValueCount(1) + .assertNoErrors() + .assertComplete(); + + assertTrue(to.values().toString(), to.values().get(0).startsWith("RxSingleScheduler-")); + } + + @Test + public void mapperDelayError2ScheduledHidden() { + TestObserver<String> to = Observable.just(1) + .concatMapDelayError(new Function<Integer, Observable<String>>() { + @Override + public Observable<String> apply(Integer t) throws Throwable { + return Observable.just(Thread.currentThread().getName()).hide(); + } + }, true, 2, Schedulers.single()) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertValueCount(1) + .assertNoErrors() + .assertComplete(); + + assertTrue(to.values().toString(), to.values().get(0).startsWith("RxSingleScheduler-")); + } + + @Test + public void issue2890NoStackoverflow() throws InterruptedException, TimeoutException { + final ExecutorService executor = Executors.newFixedThreadPool(2); + final Scheduler sch = Schedulers.from(executor); + + Function<Integer, Observable<Integer>> func = new Function<Integer, Observable<Integer>>() { + @Override + public Observable<Integer> apply(Integer t) { + Observable<Integer> flowable = Observable.just(t) + .subscribeOn(sch) + ; + Subject<Integer> processor = UnicastSubject.create(); + flowable.subscribe(processor); + return processor; + } + }; + + int n = 5000; + final AtomicInteger counter = new AtomicInteger(); + + Observable.range(1, n).concatMap(func, 2, ImmediateThinScheduler.INSTANCE).subscribe(new DefaultObserver<Integer>() { + @Override + public void onNext(Integer t) { + // Consume after sleep for 1 ms + try { + Thread.sleep(1); + } catch (InterruptedException e) { + // ignored + } + if (counter.getAndIncrement() % 100 == 0) { + System.out.print("testIssue2890NoStackoverflow -> "); + System.out.println(counter.get()); + }; + } + + @Override + public void onComplete() { + executor.shutdown(); + } + + @Override + public void onError(Throwable e) { + executor.shutdown(); + } + }); + + long awaitTerminationTimeout = 100_000; + if (!executor.awaitTermination(awaitTerminationTimeout, TimeUnit.MILLISECONDS)) { + throw new TimeoutException("Completed " + counter.get() + "/" + n + " before timed out after " + + awaitTerminationTimeout + " milliseconds."); + } + + assertEquals(n, counter.get()); + } + + @Test + public void concatMapRangeAsyncLoopIssue2876() { + final long durationSeconds = 2; + final long startTime = System.currentTimeMillis(); + for (int i = 0;; i++) { + //only run this for a max of ten seconds + if (System.currentTimeMillis() - startTime > TimeUnit.SECONDS.toMillis(durationSeconds)) { + return; + } + if (i % 1000 == 0) { + System.out.println("concatMapRangeAsyncLoop > " + i); + } + TestObserverEx<Integer> to = new TestObserverEx<>(); + Observable.range(0, 1000) + .concatMap(new Function<Integer, Observable<Integer>>() { + @Override + public Observable<Integer> apply(Integer t) { + return Observable.fromIterable(Arrays.asList(t)); + } + }, 2, ImmediateThinScheduler.INSTANCE) + .observeOn(Schedulers.computation()).subscribe(to); + + to.awaitDone(2500, TimeUnit.MILLISECONDS); + to.assertTerminated(); + to.assertNoErrors(); + assertEquals(1000, to.values().size()); + assertEquals((Integer)999, to.values().get(999)); + } + } + + @SuppressWarnings("unchecked") + @Test + public void concatArray() throws Exception { + for (int i = 2; i < 10; i++) { + Observable<Integer>[] obs = new Observable[i]; + Arrays.fill(obs, Observable.just(1)); + + Integer[] expected = new Integer[i]; + Arrays.fill(expected, 1); + + Method m = Observable.class.getMethod("concatArray", ObservableSource[].class); + + TestObserver<Integer> to = TestObserver.create(); + + ((Observable<Integer>)m.invoke(null, new Object[]{obs})).subscribe(to); + + to.assertValues(expected); + to.assertNoErrors(); + to.assertComplete(); + } + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + @Test + public void concatMapJustJust() { + TestObserver<Integer> to = TestObserver.create(); + + Observable.just(Observable.just(1)).concatMap((Function)Functions.identity(), 2, ImmediateThinScheduler.INSTANCE).subscribe(to); + + to.assertValue(1); + to.assertNoErrors(); + to.assertComplete(); + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + @Test + public void concatMapJustRange() { + TestObserver<Integer> to = TestObserver.create(); + + Observable.just(Observable.range(1, 5)).concatMap((Function)Functions.identity(), 2, ImmediateThinScheduler.INSTANCE).subscribe(to); + + to.assertValues(1, 2, 3, 4, 5); + to.assertNoErrors(); + to.assertComplete(); + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + @Test + public void concatMapDelayErrorJustJust() { + TestObserver<Integer> to = TestObserver.create(); + + Observable.just(Observable.just(1)).concatMapDelayError((Function)Functions.identity(), true, 2, ImmediateThinScheduler.INSTANCE).subscribe(to); + + to.assertValue(1); + to.assertNoErrors(); + to.assertComplete(); + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + @Test + public void concatMapDelayErrorJustRange() { + TestObserver<Integer> to = TestObserver.create(); + + Observable.just(Observable.range(1, 5)).concatMapDelayError((Function)Functions.identity(), true, 2, ImmediateThinScheduler.INSTANCE).subscribe(to); + + to.assertValues(1, 2, 3, 4, 5); + to.assertNoErrors(); + to.assertComplete(); + } + + @SuppressWarnings("unchecked") + @Test + public void startWithArray() throws Exception { + for (int i = 2; i < 10; i++) { + Object[] obs = new Object[i]; + Arrays.fill(obs, 1); + + Integer[] expected = new Integer[i]; + Arrays.fill(expected, 1); + + Method m = Observable.class.getMethod("startWithArray", Object[].class); + + TestObserver<Integer> to = TestObserver.create(); + + ((Observable<Integer>)m.invoke(Observable.empty(), new Object[]{obs})).subscribe(to); + + to.assertValues(expected); + to.assertNoErrors(); + to.assertComplete(); + } + } + + static final class InfiniteIterator implements Iterator<Integer>, Iterable<Integer> { + + int count; + + @Override + public boolean hasNext() { + return true; + } + + @Override + public Integer next() { + return count++; + } + + @Override + public void remove() { + } + + @Override + public Iterator<Integer> iterator() { + return this; + } + } + + @Test + public void concatMapDelayError() { + Observable.just(Observable.just(1), Observable.just(2)) + .concatMapDelayError(Functions.<Observable<Integer>>identity(), true, 2, ImmediateThinScheduler.INSTANCE) + .test() + .assertResult(1, 2); + } + + @Test + public void concatMapDelayErrorJustSource() { + Observable.just(0) + .concatMapDelayError(new Function<Object, Observable<Integer>>() { + @Override + public Observable<Integer> apply(Object v) throws Exception { + return Observable.just(1); + } + }, true, 16, ImmediateThinScheduler.INSTANCE) + .test() + .assertResult(1); + + } + + @Test + public void concatMapJustSource() { + Observable.just(0).hide() + .concatMap(new Function<Object, Observable<Integer>>() { + @Override + public Observable<Integer> apply(Object v) throws Exception { + return Observable.just(1); + } + }, 16, ImmediateThinScheduler.INSTANCE) + .test() + .assertResult(1); + } + + @Test + public void concatMapJustSourceDelayError() { + Observable.just(0).hide() + .concatMapDelayError(new Function<Object, Observable<Integer>>() { + @Override + public Observable<Integer> apply(Object v) throws Exception { + return Observable.just(1); + } + }, false, 16, ImmediateThinScheduler.INSTANCE) + .test() + .assertResult(1); + } + + @Test + public void concatMapEmpty() { + Observable.just(1).hide() + .concatMap(Functions.justFunction(Observable.empty()), 2, ImmediateThinScheduler.INSTANCE) + .test() + .assertResult(); + } + + @Test + public void concatMapEmptyDelayError() { + Observable.just(1).hide() + .concatMapDelayError(Functions.justFunction(Observable.empty()), true, 2, ImmediateThinScheduler.INSTANCE) + .test() + .assertResult(); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeObservable(new Function<Observable<Object>, ObservableSource<Integer>>() { + @Override + public ObservableSource<Integer> apply(Observable<Object> f) throws Exception { + return f.concatMap(Functions.justFunction(Observable.just(2)), 2, ImmediateThinScheduler.INSTANCE); + } + }); + TestHelper.checkDoubleOnSubscribeObservable(new Function<Observable<Object>, ObservableSource<Integer>>() { + @Override + public ObservableSource<Integer> apply(Observable<Object> f) throws Exception { + return f.concatMapDelayError(Functions.justFunction(Observable.just(2)), true, 2, ImmediateThinScheduler.INSTANCE); + } + }); + } + + @Test + public void immediateInnerNextOuterError() { + final PublishSubject<Integer> ps = PublishSubject.create(); + + final TestObserverEx<Integer> to = new TestObserverEx<Integer>() { + @Override + public void onNext(Integer t) { + super.onNext(t); + if (t == 1) { + ps.onError(new TestException("First")); + } + } + }; + + ps.concatMap(Functions.justFunction(Observable.just(1)), 2, ImmediateThinScheduler.INSTANCE) + .subscribe(to); + + ps.onNext(1); + + assertFalse(ps.hasObservers()); + + to.assertFailureAndMessage(TestException.class, "First", 1); + } + + @Test + public void immediateInnerNextOuterError2() { + final PublishSubject<Integer> ps = PublishSubject.create(); + + final TestObserverEx<Integer> to = new TestObserverEx<Integer>() { + @Override + public void onNext(Integer t) { + super.onNext(t); + if (t == 1) { + ps.onError(new TestException("First")); + } + } + }; + + ps.concatMap(Functions.justFunction(Observable.just(1).hide()), 2, ImmediateThinScheduler.INSTANCE) + .subscribe(to); + + ps.onNext(1); + + assertFalse(ps.hasObservers()); + + to.assertFailureAndMessage(TestException.class, "First", 1); + } + + @Test + public void concatMapInnerError() { + Observable.just(1).hide() + .concatMap(Functions.justFunction(Observable.error(new TestException())), 2, ImmediateThinScheduler.INSTANCE) + .test() + .assertFailure(TestException.class); + } + + @Test + public void concatMapInnerErrorDelayError() { + Observable.just(1).hide() + .concatMapDelayError(Functions.justFunction(Observable.error(new TestException())), true, 2, ImmediateThinScheduler.INSTANCE) + .test() + .assertFailure(TestException.class); + } + + @Test + public void badSource() { + TestHelper.checkBadSourceObservable(new Function<Observable<Integer>, Object>() { + @Override + public Object apply(Observable<Integer> f) throws Exception { + return f.concatMap(Functions.justFunction(Observable.just(1).hide()), 2, ImmediateThinScheduler.INSTANCE); + } + }, true, 1, 1, 1); + } + + @Test + public void badInnerSource() { + @SuppressWarnings("rawtypes") + final Observer[] ts0 = { null }; + TestObserverEx<Integer> to = Observable.just(1).hide().concatMap(Functions.justFunction(new Observable<Integer>() { + @Override + protected void subscribeActual(Observer<? super Integer> o) { + ts0[0] = o; + o.onSubscribe(Disposable.empty()); + o.onError(new TestException("First")); + } + }), 2, ImmediateThinScheduler.INSTANCE) + .to(TestHelper.<Integer>testConsumer()); + + to.assertFailureAndMessage(TestException.class, "First"); + + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + ts0[0].onError(new TestException("Second")); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void badInnerSourceDelayError() { + @SuppressWarnings("rawtypes") + final Observer[] ts0 = { null }; + TestObserverEx<Integer> to = Observable.just(1).hide().concatMapDelayError(Functions.justFunction(new Observable<Integer>() { + @Override + protected void subscribeActual(Observer<? super Integer> o) { + ts0[0] = o; + o.onSubscribe(Disposable.empty()); + o.onError(new TestException("First")); + } + }), true, 2, ImmediateThinScheduler.INSTANCE) + .to(TestHelper.<Integer>testConsumer()); + + to.assertFailureAndMessage(TestException.class, "First"); + + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + ts0[0].onError(new TestException("Second")); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void badSourceDelayError() { + TestHelper.checkBadSourceObservable(new Function<Observable<Integer>, Object>() { + @Override + public Object apply(Observable<Integer> f) throws Exception { + return f.concatMapDelayError(Functions.justFunction(Observable.just(1).hide()), true, 2, ImmediateThinScheduler.INSTANCE); + } + }, true, 1, 1, 1); + } + + @Test + public void fusedCrash() { + Observable.range(1, 2) + .map(new Function<Integer, Object>() { + @Override + public Object apply(Integer v) throws Exception { throw new TestException(); } + }) + .concatMap(Functions.justFunction(Observable.just(1)), 2, ImmediateThinScheduler.INSTANCE) + .test() + .assertFailure(TestException.class); + } + + @Test + public void fusedCrashDelayError() { + Observable.range(1, 2) + .map(new Function<Integer, Object>() { + @Override + public Object apply(Integer v) throws Exception { throw new TestException(); } + }) + .concatMapDelayError(Functions.justFunction(Observable.just(1)), true, 2, ImmediateThinScheduler.INSTANCE) + .test() + .assertFailure(TestException.class); + } + + @Test + public void callableCrash() { + Observable.just(1).hide() + .concatMap(Functions.justFunction(Observable.fromCallable(new Callable<Object>() { + @Override + public Object call() throws Exception { + throw new TestException(); + } + })), 2, ImmediateThinScheduler.INSTANCE) + .test() + .assertFailure(TestException.class); + } + + @Test + public void callableCrashDelayError() { + Observable.just(1).hide() + .concatMapDelayError(Functions.justFunction(Observable.fromCallable(new Callable<Object>() { + @Override + public Object call() throws Exception { + throw new TestException(); + } + })), true, 2, ImmediateThinScheduler.INSTANCE) + .test() + .assertFailure(TestException.class); + } + + @Test + public void dispose() { + TestHelper.checkDisposed(Observable.range(1, 2) + .concatMap(Functions.justFunction(Observable.just(1)), 2, ImmediateThinScheduler.INSTANCE)); + + TestHelper.checkDisposed(Observable.range(1, 2) + .concatMapDelayError(Functions.justFunction(Observable.just(1)), true, 2, ImmediateThinScheduler.INSTANCE)); + } + + @Test + public void notVeryEnd() { + Observable.range(1, 2) + .concatMapDelayError(Functions.justFunction(Observable.error(new TestException())), false, 16, ImmediateThinScheduler.INSTANCE) + .test() + .assertFailure(TestException.class); + } + + @Test + public void error() { + Observable.error(new TestException()) + .concatMapDelayError(Functions.justFunction(Observable.just(2)), false, 16, ImmediateThinScheduler.INSTANCE) + .test() + .assertFailure(TestException.class); + } + + @Test + public void mapperThrows() { + Observable.range(1, 2) + .concatMap(new Function<Integer, ObservableSource<Object>>() { + @Override + public ObservableSource<Object> apply(Integer v) throws Exception { + throw new TestException(); + } + }, 2, ImmediateThinScheduler.INSTANCE) + .test() + .assertFailure(TestException.class); + } + + @Test + public void mainErrors() { + PublishSubject<Integer> source = PublishSubject.create(); + + TestObserver<Integer> to = TestObserver.create(); + + source.concatMapDelayError(new Function<Integer, Observable<Integer>>() { + @Override + public Observable<Integer> apply(Integer v) { + return Observable.range(v, 2); + } + }, true, 2, ImmediateThinScheduler.INSTANCE).subscribe(to); + + source.onNext(1); + source.onNext(2); + source.onError(new TestException()); + + to.assertValues(1, 2, 2, 3); + to.assertError(TestException.class); + to.assertNotComplete(); + } + + @Test + public void innerErrors() { + final Observable<Integer> inner = Observable.range(1, 2) + .concatWith(Observable.<Integer>error(new TestException())); + + TestObserver<Integer> to = TestObserver.create(); + + Observable.range(1, 3).concatMapDelayError(new Function<Integer, Observable<Integer>>() { + @Override + public Observable<Integer> apply(Integer v) { + return inner; + } + }, true, 2, ImmediateThinScheduler.INSTANCE).subscribe(to); + + to.assertValues(1, 2, 1, 2, 1, 2); + to.assertError(CompositeException.class); + to.assertNotComplete(); + } + + @Test + public void singleInnerErrors() { + final Observable<Integer> inner = Observable.range(1, 2).concatWith(Observable.<Integer>error(new TestException())); + + TestObserver<Integer> to = TestObserver.create(); + + Observable.just(1) + .hide() // prevent scalar optimization + .concatMapDelayError(new Function<Integer, Observable<Integer>>() { + @Override + public Observable<Integer> apply(Integer v) { + return inner; + } + }, true, 2, ImmediateThinScheduler.INSTANCE).subscribe(to); + + to.assertValues(1, 2); + to.assertError(TestException.class); + to.assertNotComplete(); + } + + @Test + public void innerNull() { + TestObserver<Integer> to = TestObserver.create(); + + Observable.just(1) + .hide() // prevent scalar optimization + .concatMapDelayError(new Function<Integer, Observable<Integer>>() { + @Override + public Observable<Integer> apply(Integer v) { + return null; + } + }, true, 2, ImmediateThinScheduler.INSTANCE).subscribe(to); + + to.assertNoValues(); + to.assertError(NullPointerException.class); + to.assertNotComplete(); + } + + @Test + public void innerThrows() { + TestObserver<Integer> to = TestObserver.create(); + + Observable.just(1) + .hide() // prevent scalar optimization + .concatMapDelayError(new Function<Integer, Observable<Integer>>() { + @Override + public Observable<Integer> apply(Integer v) { + throw new TestException(); + } + }, true, 2, ImmediateThinScheduler.INSTANCE).subscribe(to); + + to.assertNoValues(); + to.assertError(TestException.class); + to.assertNotComplete(); + } + + @Test + public void innerWithEmpty() { + TestObserver<Integer> to = TestObserver.create(); + + Observable.range(1, 3) + .concatMapDelayError(new Function<Integer, Observable<Integer>>() { + @Override + public Observable<Integer> apply(Integer v) { + return v == 2 ? Observable.<Integer>empty() : Observable.range(1, 2); + } + }, true, 2, ImmediateThinScheduler.INSTANCE).subscribe(to); + + to.assertValues(1, 2, 1, 2); + to.assertNoErrors(); + to.assertComplete(); + } + + @Test + public void innerWithScalar() { + TestObserver<Integer> to = TestObserver.create(); + + Observable.range(1, 3) + .concatMapDelayError(new Function<Integer, Observable<Integer>>() { + @Override + public Observable<Integer> apply(Integer v) { + return v == 2 ? Observable.just(3) : Observable.range(1, 2); + } + }, true, 2, ImmediateThinScheduler.INSTANCE).subscribe(to); + + to.assertValues(1, 2, 3, 1, 2); + to.assertNoErrors(); + to.assertComplete(); + } + + @Test + public void mapperScheduledLong() { + TestObserver<String> to = Observable.range(1, 1000) + .hide() + .observeOn(Schedulers.computation()) + .concatMap(new Function<Integer, Observable<String>>() { + @Override + public Observable<String> apply(Integer t) throws Throwable { + return Observable.just(Thread.currentThread().getName()) + .repeat(1000) + .observeOn(Schedulers.io()); + } + }, 2, Schedulers.single()) + .distinct() + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertValueCount(1) + .assertNoErrors() + .assertComplete(); + + assertTrue(to.values().toString(), to.values().get(0).startsWith("RxSingleScheduler-")); + } + + @Test + public void mapperDelayErrorScheduledLong() { + TestObserver<String> to = Observable.range(1, 1000) + .hide() + .observeOn(Schedulers.computation()) + .concatMapDelayError(new Function<Integer, Observable<String>>() { + @Override + public Observable<String> apply(Integer t) throws Throwable { + return Observable.just(Thread.currentThread().getName()) + .repeat(1000) + .observeOn(Schedulers.io()); + } + }, false, 2, Schedulers.single()) + .distinct() + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertValueCount(1) + .assertNoErrors() + .assertComplete(); + + assertTrue(to.values().toString(), to.values().get(0).startsWith("RxSingleScheduler-")); + } + + @Test + public void mapperDelayError2ScheduledLong() { + TestObserver<String> to = Observable.range(1, 1000) + .hide() + .observeOn(Schedulers.computation()) + .concatMapDelayError(new Function<Integer, Observable<String>>() { + @Override + public Observable<String> apply(Integer t) throws Throwable { + return Observable.just(Thread.currentThread().getName()) + .repeat(1000) + .observeOn(Schedulers.io()); + } + }, true, 2, Schedulers.single()) + .distinct() + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertValueCount(1) + .assertNoErrors() + .assertComplete(); + + assertTrue(to.values().toString(), to.values().get(0).startsWith("RxSingleScheduler-")); + } + + @Test + public void undeliverableUponCancel() { + TestHelper.checkUndeliverableUponCancel(new ObservableConverter<Integer, Observable<Integer>>() { + @Override + public Observable<Integer> apply(Observable<Integer> upstream) { + return upstream.concatMap(new Function<Integer, Observable<Integer>>() { + @Override + public Observable<Integer> apply(Integer v) throws Throwable { + return Observable.just(v).hide(); + } + }, 2, ImmediateThinScheduler.INSTANCE); + } + }); + } + + @Test + public void undeliverableUponCancelDelayError() { + TestHelper.checkUndeliverableUponCancel(new ObservableConverter<Integer, Observable<Integer>>() { + @Override + public Observable<Integer> apply(Observable<Integer> upstream) { + return upstream.concatMapDelayError(new Function<Integer, Observable<Integer>>() { + @Override + public Observable<Integer> apply(Integer v) throws Throwable { + return Observable.just(v).hide(); + } + }, false, 2, ImmediateThinScheduler.INSTANCE); + } + }); + } + + @Test + public void undeliverableUponCancelDelayErrorTillEnd() { + TestHelper.checkUndeliverableUponCancel(new ObservableConverter<Integer, Observable<Integer>>() { + @Override + public Observable<Integer> apply(Observable<Integer> upstream) { + return upstream.concatMapDelayError(new Function<Integer, Observable<Integer>>() { + @Override + public Observable<Integer> apply(Integer v) throws Throwable { + return Observable.just(v).hide(); + } + }, true, 2, ImmediateThinScheduler.INSTANCE); + } + }); + } + + @Test + public void fusionRejected() { + TestObserverEx<Object> to = new TestObserverEx<>(); + + TestHelper.rejectObservableFusion() + .concatMap(v -> Observable.never(), 2, ImmediateThinScheduler.INSTANCE) + .subscribe(to); + } + + @Test + public void fusionRejectedDelayErrorr() { + TestObserverEx<Object> to = new TestObserverEx<>(); + + TestHelper.rejectObservableFusion() + .concatMapDelayError(v -> Observable.never(), true, 2, ImmediateThinScheduler.INSTANCE) + .subscribe(to); + } + + @Test + public void scalarInnerJustDisposeDelayError() { + TestObserver<Integer> to = new TestObserver<>(); + + Observable.just(1) + .hide() + .concatMapDelayError(v -> Observable.fromCallable(() -> { + to.dispose(); + return 1; + }), true, 2, ImmediateThinScheduler.INSTANCE) + .subscribe(to); + + to.assertEmpty(); + } + + static final class EmptyDisposingObservable extends Observable<Object> + implements Supplier<Object> { + final TestObserver<Object> to; + EmptyDisposingObservable(TestObserver<Object> to) { + this.to = to; + } + + @Override + protected void subscribeActual(@NonNull Observer<? super @NonNull Object> observer) { + EmptyDisposable.complete(observer); + } + + @Override + public @NonNull Object get() throws Throwable { + to.dispose(); + return null; + } + } + + @Test + public void scalarInnerEmptyDisposeDelayError() { + TestObserver<Object> to = new TestObserver<>(); + + Observable.just(1) + .hide() + .concatMapDelayError(v -> new EmptyDisposingObservable(to), + true, 2, ImmediateThinScheduler.INSTANCE + ) + .subscribe(to); + + to.assertEmpty(); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableConcatMapTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableConcatMapTest.java new file mode 100644 index 0000000000..8fb29f2eb4 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableConcatMapTest.java @@ -0,0 +1,673 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import static org.junit.Assert.*; + +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.internal.operators.observable.ObservableConcatMapSchedulerTest.EmptyDisposingObservable; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.subjects.*; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class ObservableConcatMapTest extends RxJavaTest { + + @Test + public void asyncFused() { + UnicastSubject<Integer> us = UnicastSubject.create(); + + TestObserver<Integer> to = us.concatMap(new Function<Integer, ObservableSource<Integer>>() { + @Override + public ObservableSource<Integer> apply(Integer v) throws Exception { + return Observable.range(v, 2); + } + }) + .test(); + + us.onNext(1); + us.onComplete(); + + to.assertResult(1, 2); + } + + @Test + public void dispose() { + TestHelper.checkDisposed(Observable.<Integer>just(1).hide() + .concatMap(new Function<Integer, ObservableSource<Integer>>() { + @Override + public ObservableSource<Integer> apply(Integer v) throws Exception { + return Observable.error(new TestException()); + } + })); + } + + @Test + public void dispose2() { + TestHelper.checkDisposed(Observable.<Integer>just(1).hide() + .concatMapDelayError(new Function<Integer, ObservableSource<Integer>>() { + @Override + public ObservableSource<Integer> apply(Integer v) throws Exception { + return Observable.error(new TestException()); + } + })); + } + + @Test + public void mainError() { + Observable.<Integer>error(new TestException()) + .concatMap(new Function<Integer, ObservableSource<Integer>>() { + @Override + public ObservableSource<Integer> apply(Integer v) throws Exception { + return Observable.range(v, 2); + } + }) + .test() + .assertFailure(TestException.class); + } + + @Test + public void innerError() { + Observable.<Integer>just(1).hide() + .concatMap(new Function<Integer, ObservableSource<Integer>>() { + @Override + public ObservableSource<Integer> apply(Integer v) throws Exception { + return Observable.error(new TestException()); + } + }) + .test() + .assertFailure(TestException.class); + } + + @Test + public void mainErrorDelayed() { + Observable.<Integer>error(new TestException()) + .concatMapDelayError(new Function<Integer, ObservableSource<Integer>>() { + @Override + public ObservableSource<Integer> apply(Integer v) throws Exception { + return Observable.range(v, 2); + } + }) + .test() + .assertFailure(TestException.class); + } + + @Test + public void innerErrorDelayError() { + Observable.<Integer>just(1).hide() + .concatMapDelayError(new Function<Integer, ObservableSource<Integer>>() { + @Override + public ObservableSource<Integer> apply(Integer v) throws Exception { + return Observable.error(new TestException()); + } + }) + .test() + .assertFailure(TestException.class); + } + + @Test + public void innerErrorDelayError2() { + Observable.<Integer>just(1).hide() + .concatMapDelayError(new Function<Integer, ObservableSource<Integer>>() { + @Override + public ObservableSource<Integer> apply(Integer v) throws Exception { + return Observable.fromCallable(new Callable<Integer>() { + @Override + public Integer call() throws Exception { + throw new TestException(); + } + }); + } + }) + .test() + .assertFailure(TestException.class); + } + + @Test + public void badSource() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + new Observable<Integer>() { + @Override + protected void subscribeActual(Observer<? super Integer> observer) { + observer.onSubscribe(Disposable.empty()); + + observer.onNext(1); + observer.onComplete(); + observer.onNext(2); + observer.onError(new TestException()); + observer.onComplete(); + } + } + .concatMap(new Function<Integer, ObservableSource<Integer>>() { + @Override + public ObservableSource<Integer> apply(Integer v) throws Exception { + return Observable.range(v, 2); + } + }) + .test() + .assertResult(1, 2); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void badSourceDelayError() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + new Observable<Integer>() { + @Override + protected void subscribeActual(Observer<? super Integer> observer) { + observer.onSubscribe(Disposable.empty()); + + observer.onNext(1); + observer.onComplete(); + observer.onNext(2); + observer.onError(new TestException()); + observer.onComplete(); + } + } + .concatMapDelayError(new Function<Integer, ObservableSource<Integer>>() { + @Override + public ObservableSource<Integer> apply(Integer v) throws Exception { + return Observable.range(v, 2); + } + }) + .test() + .assertResult(1, 2); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void normalDelayErrors() { + Observable.just(1).hide() + .concatMapDelayError(new Function<Integer, ObservableSource<Integer>>() { + @Override + public ObservableSource<Integer> apply(Integer v) throws Exception { + return Observable.range(v, 2); + } + }) + .test() + .assertResult(1, 2); + } + + @Test + public void normalDelayErrorsTillTheEnd() { + Observable.just(1).hide() + .concatMapDelayError(new Function<Integer, ObservableSource<Integer>>() { + @Override + public ObservableSource<Integer> apply(Integer v) throws Exception { + return Observable.range(v, 2); + } + }, true, 16) + .test() + .assertResult(1, 2); + } + + @Test + public void onErrorRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + final PublishSubject<Integer> ps1 = PublishSubject.create(); + final PublishSubject<Integer> ps2 = PublishSubject.create(); + + TestObserver<Integer> to = ps1.concatMap(new Function<Integer, ObservableSource<Integer>>() { + @Override + public ObservableSource<Integer> apply(Integer v) throws Exception { + return ps2; + } + }).test(); + + final TestException ex1 = new TestException(); + final TestException ex2 = new TestException(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + ps1.onError(ex1); + } + }; + Runnable r2 = new Runnable() { + @Override + public void run() { + ps2.onError(ex2); + } + }; + + TestHelper.race(r1, r2); + + to.assertFailure(TestException.class); + + if (!errors.isEmpty()) { + TestHelper.assertError(errors, 0, TestException.class); + } + } finally { + RxJavaPlugins.reset(); + } + } + } + + @Test + public void mapperThrows() { + Observable.just(1).hide() + .concatMap(new Function<Integer, ObservableSource<Integer>>() { + @Override + public ObservableSource<Integer> apply(Integer v) throws Exception { + throw new TestException(); + } + }) + .test() + .assertFailure(TestException.class); + } + + @Test + public void fusedPollThrows() { + Observable.just(1) + .map(new Function<Integer, Integer>() { + @Override + public Integer apply(Integer v) throws Exception { + throw new TestException(); + } + }) + .concatMap(new Function<Integer, ObservableSource<Integer>>() { + @Override + public ObservableSource<Integer> apply(Integer v) throws Exception { + return Observable.range(v, 2); + } + }) + .test() + .assertFailure(TestException.class); + } + + @Test + public void fusedPollThrowsDelayError() { + Observable.just(1) + .map(new Function<Integer, Integer>() { + @Override + public Integer apply(Integer v) throws Exception { + throw new TestException(); + } + }) + .concatMapDelayError(new Function<Integer, ObservableSource<Integer>>() { + @Override + public ObservableSource<Integer> apply(Integer v) throws Exception { + return Observable.range(v, 2); + } + }) + .test() + .assertFailure(TestException.class); + } + + @Test + public void mapperThrowsDelayError() { + Observable.just(1).hide() + .concatMapDelayError(new Function<Integer, ObservableSource<Integer>>() { + @Override + public ObservableSource<Integer> apply(Integer v) throws Exception { + throw new TestException(); + } + }) + .test() + .assertFailure(TestException.class); + } + + @Test + public void badInnerDelayError() { + @SuppressWarnings("rawtypes") + final Observer[] o = { null }; + + List<Throwable> errors = TestHelper.trackPluginErrors(); + + try { + Observable.just(1).hide() + .concatMapDelayError(new Function<Integer, ObservableSource<Integer>>() { + @Override + public ObservableSource<Integer> apply(Integer v) throws Exception { + return new Observable<Integer>() { + @Override + protected void subscribeActual(Observer<? super Integer> observer) { + o[0] = observer; + observer.onSubscribe(Disposable.empty()); + observer.onComplete(); + } + }; + } + }) + .test() + .assertResult(); + + o[0].onError(new TestException()); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void concatReportsDisposedOnComplete() { + final Disposable[] disposable = { null }; + + Observable.fromArray(Observable.just(1), Observable.just(2)) + .hide() + .concatMap(Functions.<Observable<Integer>>identity()) + .subscribe(new Observer<Integer>() { + + @Override + public void onSubscribe(Disposable d) { + disposable[0] = d; + } + + @Override + public void onNext(Integer t) { + } + + @Override + public void onError(Throwable e) { + } + + @Override + public void onComplete() { + } + }); + + assertTrue(disposable[0].isDisposed()); + } + + @Test + public void concatReportsDisposedOnError() { + final Disposable[] disposable = { null }; + + Observable.fromArray(Observable.just(1), Observable.<Integer>error(new TestException())) + .hide() + .concatMap(Functions.<Observable<Integer>>identity()) + .subscribe(new Observer<Integer>() { + + @Override + public void onSubscribe(Disposable d) { + disposable[0] = d; + } + + @Override + public void onNext(Integer t) { + } + + @Override + public void onError(Throwable e) { + } + + @Override + public void onComplete() { + } + }); + + assertTrue(disposable[0].isDisposed()); + } + + @Test + public void reentrantNoOverflow() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + final PublishSubject<Integer> ps = PublishSubject.create(); + + TestObserver<Integer> to = ps.concatMap(new Function<Integer, Observable<Integer>>() { + @Override + public Observable<Integer> apply(Integer v) + throws Exception { + return Observable.just(v + 1); + } + }, 1) + .subscribeWith(new TestObserver<Integer>() { + @Override + public void onNext(Integer t) { + super.onNext(t); + if (t == 1) { + for (int i = 1; i < 10; i++) { + ps.onNext(i); + } + ps.onComplete(); + } + } + }); + + ps.onNext(0); + + if (!errors.isEmpty()) { + to.onError(new CompositeException(errors)); + } + + to.assertResult(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void reentrantNoOverflowHidden() { + final PublishSubject<Integer> ps = PublishSubject.create(); + + TestObserver<Integer> to = ps.concatMap(new Function<Integer, Observable<Integer>>() { + @Override + public Observable<Integer> apply(Integer v) + throws Exception { + return Observable.just(v + 1).hide(); + } + }, 1) + .subscribeWith(new TestObserver<Integer>() { + @Override + public void onNext(Integer t) { + super.onNext(t); + if (t == 1) { + for (int i = 1; i < 10; i++) { + ps.onNext(i); + } + ps.onComplete(); + } + } + }); + + ps.onNext(0); + + to.assertResult(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + } + + @Test + public void noCancelPrevious() { + final AtomicInteger counter = new AtomicInteger(); + + Observable.range(1, 5) + .concatMap(new Function<Integer, ObservableSource<Integer>>() { + @Override + public ObservableSource<Integer> apply(Integer v) throws Exception { + return Observable.just(v).doOnDispose(new Action() { + @Override + public void run() throws Exception { + counter.getAndIncrement(); + } + }); + } + }) + .test() + .assertResult(1, 2, 3, 4, 5); + + assertEquals(0, counter.get()); + } + + @Test + public void undeliverableUponCancel() { + TestHelper.checkUndeliverableUponCancel(new ObservableConverter<Integer, Observable<Integer>>() { + @Override + public Observable<Integer> apply(Observable<Integer> upstream) { + return upstream.concatMap(new Function<Integer, Observable<Integer>>() { + @Override + public Observable<Integer> apply(Integer v) throws Throwable { + return Observable.just(v).hide(); + } + }); + } + }); + } + + @Test + public void undeliverableUponCancelDelayError() { + TestHelper.checkUndeliverableUponCancel(new ObservableConverter<Integer, Observable<Integer>>() { + @Override + public Observable<Integer> apply(Observable<Integer> upstream) { + return upstream.concatMapDelayError(new Function<Integer, Observable<Integer>>() { + @Override + public Observable<Integer> apply(Integer v) throws Throwable { + return Observable.just(v).hide(); + } + }, false, 2); + } + }); + } + + @Test + public void undeliverableUponCancelDelayErrorTillEnd() { + TestHelper.checkUndeliverableUponCancel(new ObservableConverter<Integer, Observable<Integer>>() { + @Override + public Observable<Integer> apply(Observable<Integer> upstream) { + return upstream.concatMapDelayError(new Function<Integer, Observable<Integer>>() { + @Override + public Observable<Integer> apply(Integer v) throws Throwable { + return Observable.just(v).hide(); + } + }, true, 2); + } + }); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeObservable(o -> o.concatMap(v -> Observable.never())); + } + + @Test + public void doubleOnSubscribeDelayError() { + TestHelper.checkDoubleOnSubscribeObservable(o -> o.concatMapDelayError(v -> Observable.never())); + } + + @Test + public void scalarXMap() { + Observable.fromCallable(() -> 1) + .concatMap(v -> Observable.just(2).hide()) + .test() + .assertResult(2); + } + + @Test + public void rejectedFusion() { + TestHelper.rejectObservableFusion() + .concatMap(v -> Observable.never()) + .test(); + } + + @Test + public void rejectedFusionDelayError() { + TestHelper.rejectObservableFusion() + .concatMapDelayError(v -> Observable.never()) + .test(); + } + + @Test + public void asyncFusedDelayError() { + UnicastSubject<Integer> uc = UnicastSubject.create(); + + TestObserver<Integer> to = uc.concatMapDelayError(v -> Observable.just(v).hide()) + .test(); + + uc.onNext(1); + uc.onComplete(); + + to.assertResult(1); + } + + @Test + public void scalarInnerJustDelayError() { + Observable.just(1) + .hide() + .concatMapDelayError(v -> Observable.just(v)) + .test() + .assertResult(1); + } + + @Test + public void scalarInnerEmptyDelayError() { + Observable.just(1) + .hide() + .concatMapDelayError(v -> Observable.empty()) + .test() + .assertResult(); + } + + @Test + public void scalarInnerJustDisposeDelayError() { + TestObserver<Integer> to = new TestObserver<>(); + + Observable.just(1) + .hide() + .concatMapDelayError(v -> Observable.fromCallable(() -> { + to.dispose(); + return 1; + })) + .subscribe(to); + + to.assertEmpty(); + } + + @Test + public void scalarInnerEmptyDisposeDelayError() { + TestObserver<Object> to = new TestObserver<>(); + + Observable.just(1) + .hide() + .concatMapDelayError(v -> new EmptyDisposingObservable(to)) + .subscribe(to); + + to.assertEmpty(); + } + + @Test + public void delayErrorInnerActive() { + PublishSubject<Integer> ps = PublishSubject.create(); + + TestObserver<Integer> to = Observable.range(1, 5) + .hide() + .concatMapDelayError(v -> ps) + .test(); + + ps.onComplete(); + + to.assertResult(); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableConcatTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableConcatTest.java new file mode 100644 index 0000000000..d14f803ee9 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableConcatTest.java @@ -0,0 +1,1188 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +import java.util.*; +import java.util.concurrent.*; +import java.util.concurrent.atomic.*; + +import org.junit.Test; +import org.mockito.InOrder; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.core.Observer; +import io.reactivex.rxjava3.disposables.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.observers.*; +import io.reactivex.rxjava3.schedulers.*; +import io.reactivex.rxjava3.subjects.*; +import io.reactivex.rxjava3.testsupport.*; + +public class ObservableConcatTest extends RxJavaTest { + + @Test + public void concat() { + Observer<String> observer = TestHelper.mockObserver(); + + final String[] o = { "1", "3", "5", "7" }; + final String[] e = { "2", "4", "6" }; + + final Observable<String> odds = Observable.fromArray(o); + final Observable<String> even = Observable.fromArray(e); + + Observable<String> concat = Observable.concat(odds, even); + concat.subscribe(observer); + + verify(observer, times(7)).onNext(anyString()); + } + + @Test + public void concatWithList() { + Observer<String> observer = TestHelper.mockObserver(); + + final String[] o = { "1", "3", "5", "7" }; + final String[] e = { "2", "4", "6" }; + + final Observable<String> odds = Observable.fromArray(o); + final Observable<String> even = Observable.fromArray(e); + final List<Observable<String>> list = new ArrayList<>(); + list.add(odds); + list.add(even); + Observable<String> concat = Observable.concat(Observable.fromIterable(list)); + concat.subscribe(observer); + + verify(observer, times(7)).onNext(anyString()); + } + + @Test + public void concatObservableOfObservables() { + Observer<String> observer = TestHelper.mockObserver(); + + final String[] o = { "1", "3", "5", "7" }; + final String[] e = { "2", "4", "6" }; + + final Observable<String> odds = Observable.fromArray(o); + final Observable<String> even = Observable.fromArray(e); + + Observable<Observable<String>> observableOfObservables = Observable.unsafeCreate(new ObservableSource<Observable<String>>() { + + @Override + public void subscribe(Observer<? super Observable<String>> observer) { + observer.onSubscribe(Disposable.empty()); + // simulate what would happen in an Observable + observer.onNext(odds); + observer.onNext(even); + observer.onComplete(); + } + + }); + Observable<String> concat = Observable.concat(observableOfObservables); + + concat.subscribe(observer); + + verify(observer, times(7)).onNext(anyString()); + } + + /** + * Simple concat of 2 asynchronous observables ensuring it emits in correct order. + */ + @Test + public void simpleAsyncConcat() { + Observer<String> observer = TestHelper.mockObserver(); + + TestObservable<String> o1 = new TestObservable<>("one", "two", "three"); + TestObservable<String> o2 = new TestObservable<>("four", "five", "six"); + + Observable.concat(Observable.unsafeCreate(o1), Observable.unsafeCreate(o2)).subscribe(observer); + + try { + // wait for async observables to complete + o1.t.join(); + o2.t.join(); + } catch (Throwable e) { + throw new RuntimeException("failed waiting on threads"); + } + + InOrder inOrder = inOrder(observer); + inOrder.verify(observer, times(1)).onNext("one"); + inOrder.verify(observer, times(1)).onNext("two"); + inOrder.verify(observer, times(1)).onNext("three"); + inOrder.verify(observer, times(1)).onNext("four"); + inOrder.verify(observer, times(1)).onNext("five"); + inOrder.verify(observer, times(1)).onNext("six"); + } + + @Test + public void nestedAsyncConcatLoop() throws Throwable { + for (int i = 0; i < 500; i++) { + if (i % 10 == 0) { + System.out.println("testNestedAsyncConcat >> " + i); + } + nestedAsyncConcat(); + } + } + + /** + * Test an async Observable that emits more async Observables. + * @throws InterruptedException if the test is interrupted + */ + @Test + public void nestedAsyncConcat() throws InterruptedException { + Observer<String> observer = TestHelper.mockObserver(); + + final TestObservable<String> o1 = new TestObservable<>("one", "two", "three"); + final TestObservable<String> o2 = new TestObservable<>("four", "five", "six"); + final TestObservable<String> o3 = new TestObservable<>("seven", "eight", "nine"); + final CountDownLatch allowThird = new CountDownLatch(1); + + final AtomicReference<Thread> parent = new AtomicReference<>(); + final CountDownLatch parentHasStarted = new CountDownLatch(1); + final CountDownLatch parentHasFinished = new CountDownLatch(1); + + Observable<Observable<String>> observableOfObservables = Observable.unsafeCreate(new ObservableSource<Observable<String>>() { + + @Override + public void subscribe(final Observer<? super Observable<String>> observer) { + final Disposable d = Disposable.empty(); + observer.onSubscribe(d); + parent.set(new Thread(new Runnable() { + + @Override + public void run() { + try { + // emit first + if (!d.isDisposed()) { + System.out.println("Emit o1"); + observer.onNext(Observable.unsafeCreate(o1)); + } + // emit second + if (!d.isDisposed()) { + System.out.println("Emit o2"); + observer.onNext(Observable.unsafeCreate(o2)); + } + + // wait until sometime later and emit third + try { + allowThird.await(); + } catch (InterruptedException e) { + observer.onError(e); + } + if (!d.isDisposed()) { + System.out.println("Emit o3"); + observer.onNext(Observable.unsafeCreate(o3)); + } + + } catch (Throwable e) { + observer.onError(e); + } finally { + System.out.println("Done parent Observable"); + observer.onComplete(); + parentHasFinished.countDown(); + } + } + })); + parent.get().start(); + parentHasStarted.countDown(); + } + }); + + Observable.concat(observableOfObservables).subscribe(observer); + + // wait for parent to start + parentHasStarted.await(); + + try { + // wait for first 2 async observables to complete + System.out.println("Thread1 is starting ... waiting for it to complete ..."); + o1.waitForThreadDone(); + System.out.println("Thread2 is starting ... waiting for it to complete ..."); + o2.waitForThreadDone(); + } catch (Throwable e) { + throw new RuntimeException("failed waiting on threads", e); + } + + InOrder inOrder = inOrder(observer); + inOrder.verify(observer, times(1)).onNext("one"); + inOrder.verify(observer, times(1)).onNext("two"); + inOrder.verify(observer, times(1)).onNext("three"); + inOrder.verify(observer, times(1)).onNext("four"); + inOrder.verify(observer, times(1)).onNext("five"); + inOrder.verify(observer, times(1)).onNext("six"); + // we shouldn't have the following 3 yet + inOrder.verify(observer, never()).onNext("seven"); + inOrder.verify(observer, never()).onNext("eight"); + inOrder.verify(observer, never()).onNext("nine"); + // we should not be completed yet + verify(observer, never()).onComplete(); + verify(observer, never()).onError(any(Throwable.class)); + + // now allow the third + allowThird.countDown(); + + try { + // wait for 3rd to complete + o3.waitForThreadDone(); + } catch (Throwable e) { + throw new RuntimeException("failed waiting on threads", e); + } + + try { + // wait for the parent to complete + if (!parentHasFinished.await(5, TimeUnit.SECONDS)) { + fail("Parent didn't finish within the time limit"); + } + } catch (Throwable e) { + throw new RuntimeException("failed waiting on threads", e); + } + + inOrder.verify(observer, times(1)).onNext("seven"); + inOrder.verify(observer, times(1)).onNext("eight"); + inOrder.verify(observer, times(1)).onNext("nine"); + + verify(observer, never()).onError(any(Throwable.class)); + inOrder.verify(observer, times(1)).onComplete(); + } + + @Test + public void blockedObservableOfObservables() { + Observer<String> observer = TestHelper.mockObserver(); + + final String[] o = { "1", "3", "5", "7" }; + final String[] e = { "2", "4", "6" }; + final Observable<String> odds = Observable.fromArray(o); + final Observable<String> even = Observable.fromArray(e); + final CountDownLatch callOnce = new CountDownLatch(1); + final CountDownLatch okToContinue = new CountDownLatch(1); + + TestObservable<Observable<String>> observableOfObservables = new TestObservable<>(callOnce, okToContinue, odds, even); + Observable<String> concatF = Observable.concat(Observable.unsafeCreate(observableOfObservables)); + concatF.subscribe(observer); + try { + //Block main thread to allow observables to serve up o1. + callOnce.await(); + } catch (Throwable ex) { + ex.printStackTrace(); + fail(ex.getMessage()); + } + // The concated Observable should have served up all of the odds. + verify(observer, times(1)).onNext("1"); + verify(observer, times(1)).onNext("3"); + verify(observer, times(1)).onNext("5"); + verify(observer, times(1)).onNext("7"); + + try { + // unblock observables so it can serve up o2 and complete + okToContinue.countDown(); + observableOfObservables.t.join(); + } catch (Throwable ex) { + ex.printStackTrace(); + fail(ex.getMessage()); + } + // The concatenated Observable should now have served up all the evens. + verify(observer, times(1)).onNext("2"); + verify(observer, times(1)).onNext("4"); + verify(observer, times(1)).onNext("6"); + } + + @Test + public void concatConcurrentWithInfinity() { + final TestObservable<String> w1 = new TestObservable<>("one", "two", "three"); + //This Observable will send "hello" MAX_VALUE time. + final TestObservable<String> w2 = new TestObservable<>("hello", Integer.MAX_VALUE); + + Observer<String> observer = TestHelper.mockObserver(); + + TestObservable<Observable<String>> observableOfObservables = new TestObservable<>(Observable.unsafeCreate(w1), Observable.unsafeCreate(w2)); + Observable<String> concatF = Observable.concat(Observable.unsafeCreate(observableOfObservables)); + + concatF.take(50).subscribe(observer); + + //Wait for the thread to start up. + try { + w1.waitForThreadDone(); + w2.waitForThreadDone(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + + InOrder inOrder = inOrder(observer); + inOrder.verify(observer, times(1)).onNext("one"); + inOrder.verify(observer, times(1)).onNext("two"); + inOrder.verify(observer, times(1)).onNext("three"); + inOrder.verify(observer, times(47)).onNext("hello"); + verify(observer, times(1)).onComplete(); + verify(observer, never()).onError(any(Throwable.class)); + } + + @Test + public void concatNonBlockingObservables() { + + final CountDownLatch okToContinueW1 = new CountDownLatch(1); + final CountDownLatch okToContinueW2 = new CountDownLatch(1); + + final TestObservable<String> w1 = new TestObservable<>(null, okToContinueW1, "one", "two", "three"); + final TestObservable<String> w2 = new TestObservable<>(null, okToContinueW2, "four", "five", "six"); + + Observer<String> observer = TestHelper.mockObserver(); + + Observable<Observable<String>> observableOfObservables = Observable.unsafeCreate(new ObservableSource<Observable<String>>() { + + @Override + public void subscribe(Observer<? super Observable<String>> observer) { + observer.onSubscribe(Disposable.empty()); + // simulate what would happen in an Observable + observer.onNext(Observable.unsafeCreate(w1)); + observer.onNext(Observable.unsafeCreate(w2)); + observer.onComplete(); + } + + }); + Observable<String> concat = Observable.concat(observableOfObservables); + concat.subscribe(observer); + + verify(observer, times(0)).onComplete(); + + try { + // release both threads + okToContinueW1.countDown(); + okToContinueW2.countDown(); + // wait for both to finish + w1.t.join(); + w2.t.join(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + + InOrder inOrder = inOrder(observer); + inOrder.verify(observer, times(1)).onNext("one"); + inOrder.verify(observer, times(1)).onNext("two"); + inOrder.verify(observer, times(1)).onNext("three"); + inOrder.verify(observer, times(1)).onNext("four"); + inOrder.verify(observer, times(1)).onNext("five"); + inOrder.verify(observer, times(1)).onNext("six"); + verify(observer, times(1)).onComplete(); + + } + + /** + * Test unsubscribing the concatenated Observable in a single thread. + */ + @Test + public void concatUnsubscribe() { + final CountDownLatch callOnce = new CountDownLatch(1); + final CountDownLatch okToContinue = new CountDownLatch(1); + final TestObservable<String> w1 = new TestObservable<>("one", "two", "three"); + final TestObservable<String> w2 = new TestObservable<>(callOnce, okToContinue, "four", "five", "six"); + + Observer<String> observer = TestHelper.mockObserver(); + TestObserver<String> to = new TestObserver<>(observer); + + final Observable<String> concat = Observable.concat(Observable.unsafeCreate(w1), Observable.unsafeCreate(w2)); + + try { + // Subscribe + concat.subscribe(to); + //Block main thread to allow Observable "w1" to complete and Observable "w2" to call onNext once. + callOnce.await(); + // Unsubcribe + to.dispose(); + //Unblock the Observable to continue. + okToContinue.countDown(); + w1.t.join(); + w2.t.join(); + } catch (Throwable e) { + e.printStackTrace(); + fail(e.getMessage()); + } + + InOrder inOrder = inOrder(observer); + inOrder.verify(observer, times(1)).onNext("one"); + inOrder.verify(observer, times(1)).onNext("two"); + inOrder.verify(observer, times(1)).onNext("three"); + inOrder.verify(observer, times(1)).onNext("four"); + inOrder.verify(observer, never()).onNext("five"); + inOrder.verify(observer, never()).onNext("six"); + inOrder.verify(observer, never()).onComplete(); + + } + + /** + * All observables will be running in different threads so subscribe() is unblocked. CountDownLatch is only used in order to call unsubscribe() in a predictable manner. + */ + @Test + public void concatUnsubscribeConcurrent() { + final CountDownLatch callOnce = new CountDownLatch(1); + final CountDownLatch okToContinue = new CountDownLatch(1); + final TestObservable<String> w1 = new TestObservable<>("one", "two", "three"); + final TestObservable<String> w2 = new TestObservable<>(callOnce, okToContinue, "four", "five", "six"); + + Observer<String> observer = TestHelper.mockObserver(); + TestObserver<String> to = new TestObserver<>(observer); + + TestObservable<Observable<String>> observableOfObservables = new TestObservable<>(Observable.unsafeCreate(w1), Observable.unsafeCreate(w2)); + Observable<String> concatF = Observable.concat(Observable.unsafeCreate(observableOfObservables)); + + concatF.subscribe(to); + + try { + //Block main thread to allow Observable "w1" to complete and Observable "w2" to call onNext exactly once. + callOnce.await(); + //"four" from w2 has been processed by onNext() + to.dispose(); + //"five" and "six" will NOT be processed by onNext() + //Unblock the Observable to continue. + okToContinue.countDown(); + w1.t.join(); + w2.t.join(); + } catch (Throwable e) { + e.printStackTrace(); + fail(e.getMessage()); + } + + InOrder inOrder = inOrder(observer); + inOrder.verify(observer, times(1)).onNext("one"); + inOrder.verify(observer, times(1)).onNext("two"); + inOrder.verify(observer, times(1)).onNext("three"); + inOrder.verify(observer, times(1)).onNext("four"); + inOrder.verify(observer, never()).onNext("five"); + inOrder.verify(observer, never()).onNext("six"); + verify(observer, never()).onComplete(); + verify(observer, never()).onError(any(Throwable.class)); + } + + static class TestObservable<T> implements ObservableSource<T> { + + private final Disposable upstream = new Disposable() { + @Override + public void dispose() { + subscribed = false; + } + + @Override + public boolean isDisposed() { + return subscribed; + } + }; + private final List<T> values; + private Thread t; + private int count; + private volatile boolean subscribed = true; + private final CountDownLatch once; + private final CountDownLatch okToContinue; + private final CountDownLatch threadHasStarted = new CountDownLatch(1); + private final T seed; + private final int size; + + @SafeVarargs + TestObservable(T... values) { + this(null, null, values); + } + + @SafeVarargs + TestObservable(CountDownLatch once, CountDownLatch okToContinue, T... values) { + this.values = Arrays.asList(values); + this.size = this.values.size(); + this.once = once; + this.okToContinue = okToContinue; + this.seed = null; + } + + TestObservable(T seed, int size) { + values = null; + once = null; + okToContinue = null; + this.seed = seed; + this.size = size; + } + + @Override + public void subscribe(final Observer<? super T> observer) { + observer.onSubscribe(upstream); + t = new Thread(new Runnable() { + + @Override + public void run() { + try { + while (count < size && subscribed) { + if (null != values) { + observer.onNext(values.get(count)); + } else { + observer.onNext(seed); + } + count++; + //Unblock the main thread to call unsubscribe. + if (null != once) { + once.countDown(); + } + //Block until the main thread has called unsubscribe. + if (null != okToContinue) { + okToContinue.await(5, TimeUnit.SECONDS); + } + } + if (subscribed) { + observer.onComplete(); + } + } catch (InterruptedException e) { + e.printStackTrace(); + fail(e.getMessage()); + } + } + + }); + t.start(); + threadHasStarted.countDown(); + } + + void waitForThreadDone() throws InterruptedException { + threadHasStarted.await(); + t.join(); + } + } + + @Test + public void multipleObservers() { + Observer<Object> o1 = TestHelper.mockObserver(); + Observer<Object> o2 = TestHelper.mockObserver(); + + TestScheduler s = new TestScheduler(); + + Observable<Long> timer = Observable.interval(500, TimeUnit.MILLISECONDS, s).take(2); + Observable<Long> o = Observable.concat(timer, timer); + + o.subscribe(o1); + o.subscribe(o2); + + InOrder inOrder1 = inOrder(o1); + InOrder inOrder2 = inOrder(o2); + + s.advanceTimeBy(500, TimeUnit.MILLISECONDS); + + inOrder1.verify(o1, times(1)).onNext(0L); + inOrder2.verify(o2, times(1)).onNext(0L); + + s.advanceTimeBy(500, TimeUnit.MILLISECONDS); + + inOrder1.verify(o1, times(1)).onNext(1L); + inOrder2.verify(o2, times(1)).onNext(1L); + + s.advanceTimeBy(500, TimeUnit.MILLISECONDS); + + inOrder1.verify(o1, times(1)).onNext(0L); + inOrder2.verify(o2, times(1)).onNext(0L); + + s.advanceTimeBy(500, TimeUnit.MILLISECONDS); + + inOrder1.verify(o1, times(1)).onNext(1L); + inOrder2.verify(o2, times(1)).onNext(1L); + + inOrder1.verify(o1, times(1)).onComplete(); + inOrder2.verify(o2, times(1)).onComplete(); + + verify(o1, never()).onError(any(Throwable.class)); + verify(o2, never()).onError(any(Throwable.class)); + } + + @Test + public void concatVeryLongObservableOfObservables() { + final int n = 10000; + Observable<Observable<Integer>> source = Observable.range(0, n).map(new Function<Integer, Observable<Integer>>() { + @Override + public Observable<Integer> apply(Integer v) { + return Observable.just(v); + } + }); + + Single<List<Integer>> result = Observable.concat(source).toList(); + + SingleObserver<List<Integer>> o = TestHelper.mockSingleObserver(); + InOrder inOrder = inOrder(o); + + result.subscribe(o); + + List<Integer> list = new ArrayList<>(n); + for (int i = 0; i < n; i++) { + list.add(i); + } + inOrder.verify(o).onSuccess(list); + verify(o, never()).onError(any(Throwable.class)); + } + + @Test + public void concatVeryLongObservableOfObservablesTakeHalf() { + final int n = 10000; + Observable<Observable<Integer>> source = Observable.range(0, n).map(new Function<Integer, Observable<Integer>>() { + @Override + public Observable<Integer> apply(Integer v) { + return Observable.just(v); + } + }); + + Single<List<Integer>> result = Observable.concat(source).take(n / 2).toList(); + + SingleObserver<List<Integer>> o = TestHelper.mockSingleObserver(); + InOrder inOrder = inOrder(o); + + result.subscribe(o); + + List<Integer> list = new ArrayList<>(n); + for (int i = 0; i < n / 2; i++) { + list.add(i); + } + inOrder.verify(o).onSuccess(list); + verify(o, never()).onError(any(Throwable.class)); + } + + @Test + public void concatOuterBackpressure() { + assertEquals(1, + (int) Observable.<Integer> empty() + .concatWith(Observable.just(1)) + .take(1) + .blockingSingle()); + } + + // https://github.com/ReactiveX/RxJava/issues/1818 + @Test + public void concatWithNonCompliantSourceDoubleOnComplete() { + Observable<String> o = Observable.unsafeCreate(new ObservableSource<String>() { + + @Override + public void subscribe(Observer<? super String> observer) { + observer.onSubscribe(Disposable.empty()); + observer.onNext("hello"); + observer.onComplete(); + observer.onComplete(); + } + + }); + + TestObserverEx<String> to = new TestObserverEx<>(); + Observable.concat(o, o).subscribe(to); + to.awaitDone(500, TimeUnit.MILLISECONDS); + to.assertTerminated(); + to.assertNoErrors(); + to.assertValues("hello", "hello"); + } + + @Test + public void issue2890NoStackoverflow() throws InterruptedException, TimeoutException { + final ExecutorService executor = Executors.newFixedThreadPool(2); + final Scheduler sch = Schedulers.from(executor); + + Function<Integer, Observable<Integer>> func = new Function<Integer, Observable<Integer>>() { + @Override + public Observable<Integer> apply(Integer t) { + Observable<Integer> o = Observable.just(t) + .subscribeOn(sch) + ; + Subject<Integer> subject = UnicastSubject.create(); + o.subscribe(subject); + return subject; + } + }; + + int n = 5000; + final AtomicInteger counter = new AtomicInteger(); + + Observable.range(1, n) + .concatMap(func).subscribe(new DefaultObserver<Integer>() { + @Override + public void onNext(Integer t) { + // Consume after sleep for 1 ms + try { + Thread.sleep(1); + } catch (InterruptedException e) { + // ignored + } + if (counter.getAndIncrement() % 100 == 0) { + System.out.println("testIssue2890NoStackoverflow -> " + counter.get()); + }; + } + + @Override + public void onComplete() { + executor.shutdown(); + } + + @Override + public void onError(Throwable e) { + executor.shutdown(); + } + }); + + long awaitTerminationTimeout = 100_000; + if (!executor.awaitTermination(awaitTerminationTimeout, TimeUnit.MILLISECONDS)) { + throw new TimeoutException("Completed " + counter.get() + "/" + n + " before timed out after " + + awaitTerminationTimeout + " milliseconds."); + } + + assertEquals(n, counter.get()); + } + + @Test + public void concatMapRangeAsyncLoopIssue2876() { + final long durationSeconds = 2; + final long startTime = System.currentTimeMillis(); + for (int i = 0;; i++) { + //only run this for a max of ten seconds + if (System.currentTimeMillis() - startTime > TimeUnit.SECONDS.toMillis(durationSeconds)) { + return; + } + if (i % 1000 == 0) { + System.out.println("concatMapRangeAsyncLoop > " + i); + } + TestObserverEx<Integer> to = new TestObserverEx<>(); + + Observable.range(0, 1000) + .concatMap(new Function<Integer, Observable<Integer>>() { + @Override + public Observable<Integer> apply(Integer t) { + return Observable.fromIterable(Arrays.asList(t)); + } + }) + .observeOn(Schedulers.computation()) + .subscribe(to); + + to.awaitDone(2500, TimeUnit.MILLISECONDS); + to.assertTerminated(); + to.assertNoErrors(); + assertEquals(1000, to.values().size()); + assertEquals((Integer)999, to.values().get(999)); + } + } + + @Test + public void concat3() { + Observable.concat(Observable.just(1), Observable.just(2), Observable.just(3)) + .test() + .assertResult(1, 2, 3); + } + + @Test + public void concat4() { + Observable.concat(Observable.just(1), Observable.just(2), + Observable.just(3), Observable.just(4)) + .test() + .assertResult(1, 2, 3, 4); + } + + @Test + public void concatArrayDelayError() { + Observable.concatArrayDelayError(Observable.just(1), Observable.just(2), + Observable.just(3), Observable.just(4)) + .test() + .assertResult(1, 2, 3, 4); + } + + @Test + public void concatArrayDelayErrorWithError() { + Observable.concatArrayDelayError(Observable.just(1), Observable.just(2), + Observable.just(3).concatWith(Observable.<Integer>error(new TestException())), + Observable.just(4)) + .test() + .assertFailure(TestException.class, 1, 2, 3, 4); + } + + @Test + public void concatIterableDelayError() { + Observable.concatDelayError( + Arrays.asList(Observable.just(1), Observable.just(2), + Observable.just(3), Observable.just(4))) + .test() + .assertResult(1, 2, 3, 4); + } + + @Test + public void concatIterableDelayErrorWithError() { + Observable.concatDelayError( + Arrays.asList(Observable.just(1), Observable.just(2), + Observable.just(3).concatWith(Observable.<Integer>error(new TestException())), + Observable.just(4))) + .test() + .assertFailure(TestException.class, 1, 2, 3, 4); + } + + @Test + public void concatObservableDelayError() { + Observable.concatDelayError( + Observable.just(Observable.just(1), Observable.just(2), + Observable.just(3), Observable.just(4))) + .test() + .assertResult(1, 2, 3, 4); + } + + @Test + public void concatObservableDelayErrorWithError() { + Observable.concatDelayError( + Observable.just(Observable.just(1), Observable.just(2), + Observable.just(3).concatWith(Observable.<Integer>error(new TestException())), + Observable.just(4))) + .test() + .assertFailure(TestException.class, 1, 2, 3, 4); + } + + @Test + public void concatObservableDelayErrorBoundary() { + Observable.concatDelayError( + Observable.just(Observable.just(1), Observable.just(2), + Observable.just(3).concatWith(Observable.<Integer>error(new TestException())), + Observable.just(4)), 2, false) + .test() + .assertFailure(TestException.class, 1, 2, 3); + } + + @Test + public void concatObservableDelayErrorTillEnd() { + Observable.concatDelayError( + Observable.just(Observable.just(1), Observable.just(2), + Observable.just(3).concatWith(Observable.<Integer>error(new TestException())), + Observable.just(4)), 2, true) + .test() + .assertFailure(TestException.class, 1, 2, 3, 4); + } + + @Test + public void concatMapDelayError() { + Observable.just(Observable.just(1), Observable.just(2)) + .concatMapDelayError(Functions.<Observable<Integer>>identity()) + .test() + .assertResult(1, 2); + } + + @Test + public void concatMapDelayErrorWithError() { + Observable.just(Observable.just(1).concatWith(Observable.<Integer>error(new TestException())), Observable.just(2)) + .concatMapDelayError(Functions.<Observable<Integer>>identity()) + .test() + .assertFailure(TestException.class, 1, 2); + } + + @Test + public void concatMapIterableBufferSize() { + + Observable.just(1, 2).concatMapIterable(new Function<Integer, Iterable<Integer>>() { + @Override + public Iterable<Integer> apply(Integer v) throws Exception { + return Arrays.asList(1, 2, 3, 4, 5); + } + }) + .test() + .assertResult(1, 2, 3, 4, 5, 1, 2, 3, 4, 5); + } + + @Test + public void emptyArray() { + assertSame(Observable.empty(), Observable.concatArrayDelayError()); + } + + @Test + public void singleElementArray() { + assertSame(Observable.never(), Observable.concatArrayDelayError(Observable.never())); + } + + @Test + public void concatMapDelayErrorEmptySource() { + assertSame(Observable.empty(), Observable.<Object>empty() + .concatMapDelayError(new Function<Object, ObservableSource<Integer>>() { + @Override + public ObservableSource<Integer> apply(Object v) throws Exception { + return Observable.just(1); + } + }, true, 16)); + } + + @Test + public void concatMapDelayErrorJustSource() { + Observable.just(0) + .concatMapDelayError(new Function<Object, ObservableSource<Integer>>() { + @Override + public ObservableSource<Integer> apply(Object v) throws Exception { + return Observable.just(1); + } + }, true, 16) + .test() + .assertResult(1); + + } + + @Test + public void concatArrayEmpty() { + assertSame(Observable.empty(), Observable.concatArray()); + } + + @Test + public void concatArraySingleElement() { + assertSame(Observable.never(), Observable.concatArray(Observable.never())); + } + + @Test + public void concatMapErrorEmptySource() { + assertSame(Observable.empty(), Observable.<Object>empty() + .concatMap(new Function<Object, ObservableSource<Integer>>() { + @Override + public ObservableSource<Integer> apply(Object v) throws Exception { + return Observable.just(1); + } + }, 16)); + } + + @Test + public void concatMapJustSource() { + Observable.just(0) + .concatMap(new Function<Object, ObservableSource<Integer>>() { + @Override + public ObservableSource<Integer> apply(Object v) throws Exception { + return Observable.just(1); + } + }, 16) + .test() + .assertResult(1); + + } + + @Test + public void noSubsequentSubscription() { + final int[] calls = { 0 }; + + Observable<Integer> source = Observable.create(new ObservableOnSubscribe<Integer>() { + @Override + public void subscribe(ObservableEmitter<Integer> s) throws Exception { + calls[0]++; + s.onNext(1); + s.onComplete(); + } + }); + + Observable.concatArray(source, source).firstElement() + .test() + .assertResult(1); + + assertEquals(1, calls[0]); + } + + @Test + public void noSubsequentSubscriptionDelayError() { + final int[] calls = { 0 }; + + Observable<Integer> source = Observable.create(new ObservableOnSubscribe<Integer>() { + @Override + public void subscribe(ObservableEmitter<Integer> s) throws Exception { + calls[0]++; + s.onNext(1); + s.onComplete(); + } + }); + + Observable.concatArrayDelayError(source, source).firstElement() + .test() + .assertResult(1); + + assertEquals(1, calls[0]); + } + + @Test + public void noSubsequentSubscriptionIterable() { + final int[] calls = { 0 }; + + Observable<Integer> source = Observable.create(new ObservableOnSubscribe<Integer>() { + @Override + public void subscribe(ObservableEmitter<Integer> s) throws Exception { + calls[0]++; + s.onNext(1); + s.onComplete(); + } + }); + + Observable.concat(Arrays.asList(source, source)).firstElement() + .test() + .assertResult(1); + + assertEquals(1, calls[0]); + } + + @Test + public void noSubsequentSubscriptionDelayErrorIterable() { + final int[] calls = { 0 }; + + Observable<Integer> source = Observable.create(new ObservableOnSubscribe<Integer>() { + @Override + public void subscribe(ObservableEmitter<Integer> s) throws Exception { + calls[0]++; + s.onNext(1); + s.onComplete(); + } + }); + + Observable.concatDelayError(Arrays.asList(source, source)).firstElement() + .test() + .assertResult(1); + + assertEquals(1, calls[0]); + } + + @Test + public void concatReportsDisposedOnComplete() { + final Disposable[] disposable = { null }; + + Observable.concat(Observable.just(1), Observable.just(2)) + .subscribe(new Observer<Integer>() { + + @Override + public void onSubscribe(Disposable d) { + disposable[0] = d; + } + + @Override + public void onNext(Integer t) { + } + + @Override + public void onError(Throwable e) { + } + + @Override + public void onComplete() { + } + }); + + assertTrue(disposable[0].isDisposed()); + } + + @Test + public void concatReportsDisposedOnCompleteDelayError() { + final Disposable[] disposable = { null }; + + Observable.concatArrayDelayError(Observable.just(1), Observable.just(2)) + .subscribe(new Observer<Integer>() { + + @Override + public void onSubscribe(Disposable d) { + disposable[0] = d; + } + + @Override + public void onNext(Integer t) { + } + + @Override + public void onError(Throwable e) { + } + + @Override + public void onComplete() { + } + }); + + assertTrue(disposable[0].isDisposed()); + } + + @Test + public void concatReportsDisposedOnError() { + final Disposable[] disposable = { null }; + + Observable.concat(Observable.just(1), Observable.<Integer>error(new TestException())) + .subscribe(new Observer<Integer>() { + + @Override + public void onSubscribe(Disposable d) { + disposable[0] = d; + } + + @Override + public void onNext(Integer t) { + } + + @Override + public void onError(Throwable e) { + } + + @Override + public void onComplete() { + } + }); + + assertTrue(disposable[0].isDisposed()); + } + + @Test + public void concatReportsDisposedOnErrorDelayError() { + final Disposable[] disposable = { null }; + + Observable.concatArrayDelayError(Observable.just(1), Observable.<Integer>error(new TestException())) + .subscribe(new Observer<Integer>() { + + @Override + public void onSubscribe(Disposable d) { + disposable[0] = d; + } + + @Override + public void onNext(Integer t) { + } + + @Override + public void onError(Throwable e) { + } + + @Override + public void onComplete() { + } + }); + + assertTrue(disposable[0].isDisposed()); + } + + @Test + public void noCancelPreviousArray() { + final AtomicInteger counter = new AtomicInteger(); + + Observable<Integer> source = Observable.just(1).doOnDispose(new Action() { + @Override + public void run() throws Exception { + counter.getAndIncrement(); + } + }); + + Observable.concatArray(source, source, source, source, source) + .test() + .assertResult(1, 1, 1, 1, 1); + + assertEquals(0, counter.get()); + } + + @Test + public void noCancelPreviousIterable() { + final AtomicInteger counter = new AtomicInteger(); + + Observable<Integer> source = Observable.just(1).doOnDispose(new Action() { + @Override + public void run() throws Exception { + counter.getAndIncrement(); + } + }); + + Observable.concat(Arrays.asList(source, source, source, source, source)) + .test() + .assertResult(1, 1, 1, 1, 1); + + assertEquals(0, counter.get()); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableConcatWithCompletableTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableConcatWithCompletableTest.java new file mode 100644 index 0000000000..5e0d878a34 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableConcatWithCompletableTest.java @@ -0,0 +1,145 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import static org.junit.Assert.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.Action; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.subjects.CompletableSubject; + +public class ObservableConcatWithCompletableTest extends RxJavaTest { + + @Test + public void normal() { + final TestObserver<Integer> to = new TestObserver<>(); + + Observable.range(1, 5) + .concatWith(Completable.fromAction(new Action() { + @Override + public void run() throws Exception { + to.onNext(100); + } + })) + .subscribe(to); + + to.assertResult(1, 2, 3, 4, 5, 100); + } + + @Test + public void mainError() { + final TestObserver<Integer> to = new TestObserver<>(); + + Observable.<Integer>error(new TestException()) + .concatWith(Completable.fromAction(new Action() { + @Override + public void run() throws Exception { + to.onNext(100); + } + })) + .subscribe(to); + + to.assertFailure(TestException.class); + } + + @Test + public void otherError() { + final TestObserver<Integer> to = new TestObserver<>(); + + Observable.range(1, 5) + .concatWith(Completable.error(new TestException())) + .subscribe(to); + + to.assertFailure(TestException.class, 1, 2, 3, 4, 5); + } + + @Test + public void takeMain() { + final TestObserver<Integer> to = new TestObserver<>(); + + Observable.range(1, 5) + .concatWith(Completable.fromAction(new Action() { + @Override + public void run() throws Exception { + to.onNext(100); + } + })) + .take(3) + .subscribe(to); + + to.assertResult(1, 2, 3); + } + + @Test + public void cancelOther() { + CompletableSubject other = CompletableSubject.create(); + + TestObserver<Object> to = Observable.empty() + .concatWith(other) + .test(); + + assertTrue(other.hasObservers()); + + to.dispose(); + + assertFalse(other.hasObservers()); + } + + @Test + public void badSource() { + new Observable<Integer>() { + @Override + protected void subscribeActual(Observer<? super Integer> observer) { + Disposable bs1 = Disposable.empty(); + observer.onSubscribe(bs1); + + Disposable bs2 = Disposable.empty(); + observer.onSubscribe(bs2); + + assertFalse(bs1.isDisposed()); + assertTrue(bs2.isDisposed()); + + observer.onComplete(); + } + }.concatWith(Completable.complete()) + .test() + .assertResult(); + } + + @Test + public void consumerDisposed() { + new Observable<Integer>() { + @Override + protected void subscribeActual(Observer<? super Integer> observer) { + Disposable bs1 = Disposable.empty(); + observer.onSubscribe(bs1); + + assertFalse(((Disposable)observer).isDisposed()); + + observer.onNext(1); + + assertTrue(((Disposable)observer).isDisposed()); + assertTrue(bs1.isDisposed()); + } + }.concatWith(Completable.complete()) + .take(1) + .test() + .assertResult(1); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableConcatWithMaybeTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableConcatWithMaybeTest.java new file mode 100644 index 0000000000..6c70e75a9d --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableConcatWithMaybeTest.java @@ -0,0 +1,178 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import static org.junit.Assert.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.Action; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.subjects.MaybeSubject; + +public class ObservableConcatWithMaybeTest extends RxJavaTest { + + @Test + public void normalEmpty() { + final TestObserver<Integer> to = new TestObserver<>(); + + Observable.range(1, 5) + .concatWith(Maybe.<Integer>fromAction(new Action() { + @Override + public void run() throws Exception { + to.onNext(100); + } + })) + .subscribe(to); + + to.assertResult(1, 2, 3, 4, 5, 100); + } + + @Test + public void normalNonEmpty() { + final TestObserver<Integer> to = new TestObserver<>(); + + Observable.range(1, 5) + .concatWith(Maybe.just(100)) + .subscribe(to); + + to.assertResult(1, 2, 3, 4, 5, 100); + } + + @Test + public void mainError() { + final TestObserver<Integer> to = new TestObserver<>(); + + Observable.<Integer>error(new TestException()) + .concatWith(Maybe.<Integer>fromAction(new Action() { + @Override + public void run() throws Exception { + to.onNext(100); + } + })) + .subscribe(to); + + to.assertFailure(TestException.class); + } + + @Test + public void otherError() { + final TestObserver<Integer> to = new TestObserver<>(); + + Observable.range(1, 5) + .concatWith(Maybe.<Integer>error(new TestException())) + .subscribe(to); + + to.assertFailure(TestException.class, 1, 2, 3, 4, 5); + } + + @Test + public void takeMain() { + final TestObserver<Integer> to = new TestObserver<>(); + + Observable.range(1, 5) + .concatWith(Maybe.<Integer>fromAction(new Action() { + @Override + public void run() throws Exception { + to.onNext(100); + } + })) + .take(3) + .subscribe(to); + + to.assertResult(1, 2, 3); + } + + @Test + public void cancelOther() { + MaybeSubject<Object> other = MaybeSubject.create(); + + TestObserver<Object> to = Observable.empty() + .concatWith(other) + .test(); + + assertTrue(other.hasObservers()); + + to.dispose(); + + assertFalse(other.hasObservers()); + } + + @Test + public void consumerDisposed() { + new Observable<Integer>() { + @Override + protected void subscribeActual(Observer<? super Integer> observer) { + Disposable bs1 = Disposable.empty(); + observer.onSubscribe(bs1); + + assertFalse(((Disposable)observer).isDisposed()); + + observer.onNext(1); + + assertTrue(((Disposable)observer).isDisposed()); + assertTrue(bs1.isDisposed()); + } + }.concatWith(Maybe.just(100)) + .take(1) + .test() + .assertResult(1); + } + + @Test + public void badSource() { + new Observable<Integer>() { + @Override + protected void subscribeActual(Observer<? super Integer> observer) { + Disposable bs1 = Disposable.empty(); + observer.onSubscribe(bs1); + + Disposable bs2 = Disposable.empty(); + observer.onSubscribe(bs2); + + assertFalse(bs1.isDisposed()); + assertTrue(bs2.isDisposed()); + + observer.onComplete(); + } + }.concatWith(Maybe.<Integer>empty()) + .test() + .assertResult(); + } + + @Test + public void badSource2() { + Flowable.empty().concatWith(new Maybe<Integer>() { + @Override + protected void subscribeActual(MaybeObserver<? super Integer> observer) { + Disposable bs1 = Disposable.empty(); + observer.onSubscribe(bs1); + + Disposable bs2 = Disposable.empty(); + observer.onSubscribe(bs2); + + assertFalse(bs1.isDisposed()); + assertTrue(bs2.isDisposed()); + + observer.onComplete(); + } + }) + .test() + .assertResult(); + } + +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableConcatWithSingleTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableConcatWithSingleTest.java new file mode 100644 index 0000000000..8ac2519c3c --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableConcatWithSingleTest.java @@ -0,0 +1,151 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import static org.junit.Assert.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.subjects.SingleSubject; + +public class ObservableConcatWithSingleTest extends RxJavaTest { + + @Test + public void normal() { + final TestObserver<Integer> to = new TestObserver<>(); + + Observable.range(1, 5) + .concatWith(Single.just(100)) + .subscribe(to); + + to.assertResult(1, 2, 3, 4, 5, 100); + } + + @Test + public void mainError() { + final TestObserver<Integer> to = new TestObserver<>(); + + Observable.<Integer>error(new TestException()) + .concatWith(Single.just(100)) + .subscribe(to); + + to.assertFailure(TestException.class); + } + + @Test + public void otherError() { + final TestObserver<Integer> to = new TestObserver<>(); + + Observable.range(1, 5) + .concatWith(Single.<Integer>error(new TestException())) + .subscribe(to); + + to.assertFailure(TestException.class, 1, 2, 3, 4, 5); + } + + @Test + public void takeMain() { + final TestObserver<Integer> to = new TestObserver<>(); + + Observable.range(1, 5) + .concatWith(Single.just(100)) + .take(3) + .subscribe(to); + + to.assertResult(1, 2, 3); + } + + @Test + public void cancelOther() { + SingleSubject<Object> other = SingleSubject.create(); + + TestObserver<Object> to = Observable.empty() + .concatWith(other) + .test(); + + assertTrue(other.hasObservers()); + + to.dispose(); + + assertFalse(other.hasObservers()); + } + + @Test + public void consumerDisposed() { + new Observable<Integer>() { + @Override + protected void subscribeActual(Observer<? super Integer> observer) { + Disposable bs1 = Disposable.empty(); + observer.onSubscribe(bs1); + + assertFalse(((Disposable)observer).isDisposed()); + + observer.onNext(1); + + assertTrue(((Disposable)observer).isDisposed()); + assertTrue(bs1.isDisposed()); + } + }.concatWith(Single.just(100)) + .take(1) + .test() + .assertResult(1); + } + + @Test + public void badSource() { + new Observable<Integer>() { + @Override + protected void subscribeActual(Observer<? super Integer> observer) { + Disposable bs1 = Disposable.empty(); + observer.onSubscribe(bs1); + + Disposable bs2 = Disposable.empty(); + observer.onSubscribe(bs2); + + assertFalse(bs1.isDisposed()); + assertTrue(bs2.isDisposed()); + + observer.onComplete(); + } + }.concatWith(Single.<Integer>just(100)) + .test() + .assertResult(100); + } + + @Test + public void badSource2() { + Flowable.empty().concatWith(new Single<Integer>() { + @Override + protected void subscribeActual(SingleObserver<? super Integer> observer) { + Disposable bs1 = Disposable.empty(); + observer.onSubscribe(bs1); + + Disposable bs2 = Disposable.empty(); + observer.onSubscribe(bs2); + + assertFalse(bs1.isDisposed()); + assertTrue(bs2.isDisposed()); + + observer.onSuccess(100); + } + }) + .test() + .assertResult(100); + } + +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableCountTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableCountTest.java new file mode 100644 index 0000000000..1390502816 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableCountTest.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class ObservableCountTest extends RxJavaTest { + + @Test + public void dispose() { + TestHelper.checkDisposed(Observable.just(1).count()); + + TestHelper.checkDisposed(Observable.just(1).count().toObservable()); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeObservable(new Function<Observable<Object>, ObservableSource<Long>>() { + @Override + public ObservableSource<Long> apply(Observable<Object> o) throws Exception { + return o.count().toObservable(); + } + }); + + TestHelper.checkDoubleOnSubscribeObservableToSingle(new Function<Observable<Object>, SingleSource<Long>>() { + @Override + public SingleSource<Long> apply(Observable<Object> o) throws Exception { + return o.count(); + } + }); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableCreateTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableCreateTest.java new file mode 100644 index 0000000000..0b96fae6aa --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableCreateTest.java @@ -0,0 +1,759 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import static org.junit.Assert.*; + +import java.io.IOException; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicReference; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.Cancellable; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.testsupport.*; + +public class ObservableCreateTest extends RxJavaTest { + + @Test + @SuppressUndeliverable + public void basic() { + final Disposable d = Disposable.empty(); + + Observable.<Integer>create(new ObservableOnSubscribe<Integer>() { + @Override + public void subscribe(ObservableEmitter<Integer> e) throws Exception { + e.setDisposable(d); + + e.onNext(1); + e.onNext(2); + e.onNext(3); + e.onComplete(); + e.onError(new TestException()); + e.onNext(4); + e.onError(new TestException()); + e.onComplete(); + } + }) + .test() + .assertResult(1, 2, 3); + + assertTrue(d.isDisposed()); + } + + @Test + @SuppressUndeliverable + public void basicWithCancellable() { + final Disposable d1 = Disposable.empty(); + final Disposable d2 = Disposable.empty(); + + Observable.<Integer>create(new ObservableOnSubscribe<Integer>() { + @Override + public void subscribe(ObservableEmitter<Integer> e) throws Exception { + e.setDisposable(d1); + e.setCancellable(new Cancellable() { + @Override + public void cancel() throws Exception { + d2.dispose(); + } + }); + + e.onNext(1); + e.onNext(2); + e.onNext(3); + e.onComplete(); + e.onError(new TestException()); + e.onNext(4); + e.onError(new TestException()); + e.onComplete(); + } + }) + .test() + .assertResult(1, 2, 3); + + assertTrue(d1.isDisposed()); + assertTrue(d2.isDisposed()); + } + + @Test + @SuppressUndeliverable + public void basicWithError() { + final Disposable d = Disposable.empty(); + + Observable.<Integer>create(new ObservableOnSubscribe<Integer>() { + @Override + public void subscribe(ObservableEmitter<Integer> e) throws Exception { + e.setDisposable(d); + + e.onNext(1); + e.onNext(2); + e.onNext(3); + e.onError(new TestException()); + e.onComplete(); + e.onNext(4); + e.onError(new TestException()); + } + }) + .test() + .assertFailure(TestException.class, 1, 2, 3); + + assertTrue(d.isDisposed()); + } + + @Test + @SuppressUndeliverable + public void basicSerialized() { + final Disposable d = Disposable.empty(); + + Observable.<Integer>create(new ObservableOnSubscribe<Integer>() { + @Override + public void subscribe(ObservableEmitter<Integer> e) throws Exception { + e = e.serialize(); + + e.setDisposable(d); + + e.onNext(1); + e.onNext(2); + e.onNext(3); + e.onComplete(); + e.onError(new TestException()); + e.onNext(4); + e.onError(new TestException()); + e.onComplete(); + } + }) + .test() + .assertResult(1, 2, 3); + + assertTrue(d.isDisposed()); + } + + @Test + @SuppressUndeliverable + public void basicWithErrorSerialized() { + final Disposable d = Disposable.empty(); + + Observable.<Integer>create(new ObservableOnSubscribe<Integer>() { + @Override + public void subscribe(ObservableEmitter<Integer> e) throws Exception { + e = e.serialize(); + + e.setDisposable(d); + + e.onNext(1); + e.onNext(2); + e.onNext(3); + e.onError(new TestException()); + e.onComplete(); + e.onNext(4); + e.onError(new TestException()); + } + }) + .test() + .assertFailure(TestException.class, 1, 2, 3); + + assertTrue(d.isDisposed()); + } + + @Test + public void wrap() { + Observable.wrap(new ObservableSource<Integer>() { + @Override + public void subscribe(Observer<? super Integer> observer) { + observer.onSubscribe(Disposable.empty()); + observer.onNext(1); + observer.onNext(2); + observer.onNext(3); + observer.onNext(4); + observer.onNext(5); + observer.onComplete(); + } + }) + .test() + .assertResult(1, 2, 3, 4, 5); + } + + @Test + public void unsafe() { + Observable.unsafeCreate(new ObservableSource<Integer>() { + @Override + public void subscribe(Observer<? super Integer> observer) { + observer.onSubscribe(Disposable.empty()); + observer.onNext(1); + observer.onNext(2); + observer.onNext(3); + observer.onNext(4); + observer.onNext(5); + observer.onComplete(); + } + }) + .test() + .assertResult(1, 2, 3, 4, 5); + } + + @Test(expected = IllegalArgumentException.class) + public void unsafeWithObservable() { + Observable.unsafeCreate(Observable.just(1)); + } + + @Test + @SuppressUndeliverable + public void createNullValue() { + final Throwable[] error = { null }; + + Observable.create(new ObservableOnSubscribe<Integer>() { + @Override + public void subscribe(ObservableEmitter<Integer> e) throws Exception { + try { + e.onNext(null); + e.onNext(1); + e.onError(new TestException()); + e.onComplete(); + } catch (Throwable ex) { + error[0] = ex; + } + } + }) + .test() + .assertFailure(NullPointerException.class); + + assertNull(error[0]); + } + + @Test + @SuppressUndeliverable + public void createNullValueSerialized() { + final Throwable[] error = { null }; + + Observable.create(new ObservableOnSubscribe<Integer>() { + @Override + public void subscribe(ObservableEmitter<Integer> e) throws Exception { + e = e.serialize(); + try { + e.onNext(null); + e.onNext(1); + e.onError(new TestException()); + e.onComplete(); + } catch (Throwable ex) { + error[0] = ex; + } + } + }) + .test() + .assertFailure(NullPointerException.class); + + assertNull(error[0]); + } + + @Test + public void callbackThrows() { + Observable.create(new ObservableOnSubscribe<Object>() { + @Override + public void subscribe(ObservableEmitter<Object> e) throws Exception { + throw new TestException(); + } + }) + .test() + .assertFailure(TestException.class); + } + + @Test + public void nullValue() { + Observable.create(new ObservableOnSubscribe<Object>() { + @Override + public void subscribe(ObservableEmitter<Object> e) throws Exception { + e.onNext(null); + } + }) + .test() + .assertFailure(NullPointerException.class); + } + + @Test + public void nullThrowable() { + Observable.create(new ObservableOnSubscribe<Object>() { + @Override + public void subscribe(ObservableEmitter<Object> e) throws Exception { + e.onError(null); + } + }) + .test() + .assertFailure(NullPointerException.class); + } + + @Test + public void nullValueSync() { + Observable.create(new ObservableOnSubscribe<Object>() { + @Override + public void subscribe(ObservableEmitter<Object> e) throws Exception { + e.serialize().onNext(null); + } + }) + .test() + .assertFailure(NullPointerException.class); + } + + @Test + public void nullThrowableSync() { + Observable.create(new ObservableOnSubscribe<Object>() { + @Override + public void subscribe(ObservableEmitter<Object> e) throws Exception { + e.serialize().onError(null); + } + }) + .test() + .assertFailure(NullPointerException.class); + } + + @Test + public void onErrorCrash() { + Observable.create(new ObservableOnSubscribe<Object>() { + @Override + public void subscribe(ObservableEmitter<Object> e) throws Exception { + Disposable d = Disposable.empty(); + e.setDisposable(d); + try { + e.onError(new IOException()); + fail("Should have thrown"); + } catch (TestException ex) { + // expected + } + assertTrue(d.isDisposed()); + } + }) + .subscribe(new Observer<Object>() { + @Override + public void onSubscribe(Disposable d) { + } + + @Override + public void onNext(Object value) { + } + + @Override + public void onError(Throwable e) { + throw new TestException(); + } + + @Override + public void onComplete() { + } + }); + } + + @Test + public void onCompleteCrash() { + Observable.create(new ObservableOnSubscribe<Object>() { + @Override + public void subscribe(ObservableEmitter<Object> e) throws Exception { + Disposable d = Disposable.empty(); + e.setDisposable(d); + try { + e.onComplete(); + fail("Should have thrown"); + } catch (TestException ex) { + // expected + } + assertTrue(d.isDisposed()); + } + }) + .subscribe(new Observer<Object>() { + @Override + public void onSubscribe(Disposable d) { + } + + @Override + public void onNext(Object value) { + } + + @Override + public void onError(Throwable e) { + } + + @Override + public void onComplete() { + throw new TestException(); + } + }); + } + + @Test + public void serialized() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + Observable.create(new ObservableOnSubscribe<Object>() { + @Override + public void subscribe(ObservableEmitter<Object> e) throws Exception { + ObservableEmitter<Object> f = e.serialize(); + + assertSame(f, f.serialize()); + + assertFalse(f.isDisposed()); + + final int[] calls = { 0 }; + + f.setCancellable(new Cancellable() { + @Override + public void cancel() throws Exception { + calls[0]++; + } + }); + + e.onComplete(); + + assertTrue(f.isDisposed()); + + assertEquals(1, calls[0]); + } + }) + .test() + .assertResult(); + + assertTrue(errors.toString(), errors.isEmpty()); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void serializedConcurrentOnNext() { + Observable.create(new ObservableOnSubscribe<Object>() { + @Override + public void subscribe(ObservableEmitter<Object> e) throws Exception { + final ObservableEmitter<Object> f = e.serialize(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + f.onNext(1); + } + } + }; + + TestHelper.race(r1, r1); + } + }) + .take(TestHelper.RACE_DEFAULT_LOOPS) + .to(TestHelper.<Object>testConsumer()) + .assertSubscribed() + .assertValueCount(TestHelper.RACE_DEFAULT_LOOPS) + .assertComplete() + .assertNoErrors(); + } + + @Test + public void serializedConcurrentOnNextOnError() { + Observable.create(new ObservableOnSubscribe<Object>() { + @Override + public void subscribe(ObservableEmitter<Object> e) throws Exception { + final ObservableEmitter<Object> f = e.serialize(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + for (int i = 0; i < 1000; i++) { + f.onNext(1); + } + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + for (int i = 0; i < 100; i++) { + f.onNext(1); + } + f.onError(new TestException()); + } + }; + + TestHelper.race(r1, r2); + } + }) + .to(TestHelper.<Object>testConsumer()) + .assertSubscribed() + .assertNotComplete() + .assertError(TestException.class); + } + + @Test + public void serializedConcurrentOnNextOnComplete() { + TestObserverEx<Object> to = Observable.create(new ObservableOnSubscribe<Object>() { + @Override + public void subscribe(ObservableEmitter<Object> e) throws Exception { + final ObservableEmitter<Object> f = e.serialize(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + for (int i = 0; i < 1000; i++) { + f.onNext(1); + } + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + for (int i = 0; i < 100; i++) { + f.onNext(1); + } + f.onComplete(); + } + }; + + TestHelper.race(r1, r2); + } + }) + .to(TestHelper.<Object>testConsumer()) + .assertSubscribed() + .assertComplete() + .assertNoErrors(); + + int c = to.values().size(); + assertTrue("" + c, c >= 100); + } + + @Test + public void onErrorRace() { + Observable<Object> source = Observable.create(new ObservableOnSubscribe<Object>() { + @Override + public void subscribe(ObservableEmitter<Object> e) throws Exception { + final ObservableEmitter<Object> f = e.serialize(); + + final TestException ex = new TestException(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + f.onError(null); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + f.onError(ex); + } + }; + + TestHelper.race(r1, r2); + } + }); + + List<Throwable> errors = TestHelper.trackPluginErrors(); + + try { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + source + .test() + .assertFailure(Throwable.class); + } + } finally { + RxJavaPlugins.reset(); + } + assertFalse(errors.isEmpty()); + } + + @Test + public void onCompleteRace() { + Observable<Object> source = Observable.create(new ObservableOnSubscribe<Object>() { + @Override + public void subscribe(ObservableEmitter<Object> e) throws Exception { + final ObservableEmitter<Object> f = e.serialize(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + f.onComplete(); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + f.onComplete(); + } + }; + + TestHelper.race(r1, r2); + } + }); + + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + source + .test() + .assertResult(); + } + } + + @Test + public void tryOnError() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + final Boolean[] response = { null }; + Observable.create(new ObservableOnSubscribe<Object>() { + @Override + public void subscribe(ObservableEmitter<Object> e) throws Exception { + e.onNext(1); + response[0] = e.tryOnError(new TestException()); + } + }) + .take(1) + .test() + .assertResult(1); + + assertFalse(response[0]); + + assertTrue(errors.toString(), errors.isEmpty()); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void tryOnErrorSerialized() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + final Boolean[] response = { null }; + Observable.create(new ObservableOnSubscribe<Object>() { + @Override + public void subscribe(ObservableEmitter<Object> e) throws Exception { + e = e.serialize(); + e.onNext(1); + response[0] = e.tryOnError(new TestException()); + } + }) + .take(1) + .test() + .assertResult(1); + + assertFalse(response[0]); + + assertTrue(errors.toString(), errors.isEmpty()); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void emitterHasToString() { + Observable.create(new ObservableOnSubscribe<Object>() { + @Override + public void subscribe(ObservableEmitter<Object> emitter) throws Exception { + assertTrue(emitter.toString().contains(ObservableCreate.CreateEmitter.class.getSimpleName())); + assertTrue(emitter.serialize().toString().contains(ObservableCreate.CreateEmitter.class.getSimpleName())); + } + }).test().assertEmpty(); + } + + @Test + public void emptySerialized() { + Observable.create(emitter -> emitter.serialize().onComplete()) + .test() + .assertResult(); + } + + @Test + public void serializedDisposedBeforeOnNext() { + TestObserver<Object> to = new TestObserver<>(); + + Observable.create(emitter -> { + to.dispose(); + emitter.serialize().onNext(1); + }) + .subscribe(to); + + to.assertEmpty(); + } + + @Test + public void serializedOnNextAfterComplete() { + TestObserver<Object> to = new TestObserver<>(); + + Observable.create(emitter -> { + emitter = emitter.serialize(); + + emitter.onComplete(); + emitter.onNext(1); + }) + .subscribe(to); + + to.assertResult(); + } + + @Test + public void serializedEnqueueAndDrainRace() throws Throwable { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + TestObserver<Integer> to = new TestObserver<>(); + AtomicReference<ObservableEmitter<Integer>> ref = new AtomicReference<>(); + + CountDownLatch cdl = new CountDownLatch(1); + + Observable.<Integer>create(emitter -> { + emitter = emitter.serialize(); + ref.set(emitter); + emitter.onNext(1); + }) + .doOnNext(v -> { + if (v == 1) { + TestHelper.raceOther(() -> { + ref.get().onNext(2); + }, cdl); + ref.get().onNext(3); + } + }) + .subscribe(to); + + cdl.await(); + + to.assertValueCount(3); + } + } + + @Test + public void serializedDrainDoneButNotEmpty() throws Throwable { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + TestObserver<Integer> to = new TestObserver<>(); + AtomicReference<ObservableEmitter<Integer>> ref = new AtomicReference<>(); + + CountDownLatch cdl = new CountDownLatch(1); + + Observable.<Integer>create(emitter -> { + emitter = emitter.serialize(); + ref.set(emitter); + emitter.onNext(1); + }) + .doOnNext(v -> { + if (v == 1) { + TestHelper.raceOther(() -> { + ref.get().onNext(2); + ref.get().onComplete(); + }, cdl); + ref.get().onNext(3); + } + }) + .subscribe(to); + + cdl.await(); + } + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableDebounceTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableDebounceTest.java new file mode 100644 index 0000000000..fc4e7d8478 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableDebounceTest.java @@ -0,0 +1,596 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +import io.reactivex.rxjava3.functions.Action; +import org.junit.*; +import org.mockito.InOrder; +import org.reactivestreams.Publisher; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.internal.operators.observable.ObservableDebounceTimed.*; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.schedulers.TestScheduler; +import io.reactivex.rxjava3.subjects.*; +import io.reactivex.rxjava3.testsupport.*; + +public class ObservableDebounceTest extends RxJavaTest { + + private TestScheduler scheduler; + private Observer<String> observer; + private Scheduler.Worker innerScheduler; + + @Before + public void before() { + scheduler = new TestScheduler(); + observer = TestHelper.mockObserver(); + innerScheduler = scheduler.createWorker(); + } + + @Test + public void debounceWithOnDroppedCallbackWithEx() throws Throwable { + Observable<String> source = Observable.unsafeCreate(new ObservableSource<String>() { + @Override + public void subscribe(Observer<? super String> observer) { + observer.onSubscribe(Disposable.empty()); + publishNext(observer, 100, "one"); // Should be skipped since "two" will arrive before the timeout expires. + publishNext(observer, 400, "two"); // Should be published since "three" will arrive after the timeout expires. + publishNext(observer, 900, "three"); // Should be skipped since onComplete will arrive before the timeout expires. + publishNext(observer, 999, "four"); // Should be skipped since onComplete will arrive before the timeout expires. + publishCompleted(observer, 1000); // Should be published as soon as the timeout expires. + } + }); + + Action whenDisposed = mock(Action.class); + Observable<String> sampled = source + .doOnDispose(whenDisposed) + .debounce(400, TimeUnit.MILLISECONDS, scheduler, e -> { + if ("three".equals(e)) { + throw new TestException("forced"); + } + }); + sampled.subscribe(observer); + + scheduler.advanceTimeTo(0, TimeUnit.MILLISECONDS); + InOrder inOrder = inOrder(observer); + // must go to 800 since it must be 400 after when two is sent, which is at 400 + scheduler.advanceTimeTo(800, TimeUnit.MILLISECONDS); + inOrder.verify(observer, times(1)).onNext("two"); + scheduler.advanceTimeTo(1000, TimeUnit.MILLISECONDS); + inOrder.verify(observer, times(1)).onError(any(TestException.class)); + inOrder.verify(observer, never()).onNext("three"); + inOrder.verify(observer, never()).onNext("four"); + inOrder.verify(observer, never()).onComplete(); + inOrder.verifyNoMoreInteractions(); + verify(whenDisposed).run(); + } + + @Test + public void debounceWithOnDroppedCallback() { + Observable<String> source = Observable.unsafeCreate(new ObservableSource<String>() { + @Override + public void subscribe(Observer<? super String> observer) { + observer.onSubscribe(Disposable.empty()); + publishNext(observer, 100, "one"); // Should be skipped since "two" will arrive before the timeout expires. + publishNext(observer, 400, "two"); // Should be published since "three" will arrive after the timeout expires. + publishNext(observer, 900, "three"); // Should be skipped since onComplete will arrive before the timeout expires. + publishNext(observer, 999, "four"); // Should be skipped since onComplete will arrive before the timeout expires. + publishCompleted(observer, 1000); // Should be published as soon as the timeout expires. + } + }); + + Observer<Object> drops = TestHelper.mockObserver(); + InOrder inOrderDrops = inOrder(drops); + Observable<String> sampled = source.debounce(400, TimeUnit.MILLISECONDS, scheduler, drops::onNext); + sampled.subscribe(observer); + + scheduler.advanceTimeTo(0, TimeUnit.MILLISECONDS); + InOrder inOrder = inOrder(observer); + // must go to 800 since it must be 400 after when two is sent, which is at 400 + scheduler.advanceTimeTo(800, TimeUnit.MILLISECONDS); + inOrderDrops.verify(drops, times(1)).onNext("one"); + inOrder.verify(observer, times(1)).onNext("two"); + scheduler.advanceTimeTo(1000, TimeUnit.MILLISECONDS); + inOrderDrops.verify(drops, times(1)).onNext("three"); + inOrder.verify(observer, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + inOrderDrops.verifyNoMoreInteractions(); + } + + @Test + public void debounceWithCompleted() { + Observable<String> source = Observable.unsafeCreate(new ObservableSource<String>() { + @Override + public void subscribe(Observer<? super String> observer) { + observer.onSubscribe(Disposable.empty()); + publishNext(observer, 100, "one"); // Should be skipped since "two" will arrive before the timeout expires. + publishNext(observer, 400, "two"); // Should be published since "three" will arrive after the timeout expires. + publishNext(observer, 900, "three"); // Should be skipped since onComplete will arrive before the timeout expires. + publishCompleted(observer, 1000); // Should be published as soon as the timeout expires. + } + }); + + Observable<String> sampled = source.debounce(400, TimeUnit.MILLISECONDS, scheduler); + sampled.subscribe(observer); + + scheduler.advanceTimeTo(0, TimeUnit.MILLISECONDS); + InOrder inOrder = inOrder(observer); + // must go to 800 since it must be 400 after when two is sent, which is at 400 + scheduler.advanceTimeTo(800, TimeUnit.MILLISECONDS); + inOrder.verify(observer, times(1)).onNext("two"); + scheduler.advanceTimeTo(1000, TimeUnit.MILLISECONDS); + inOrder.verify(observer, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void debounceNeverEmits() { + Observable<String> source = Observable.unsafeCreate(new ObservableSource<String>() { + @Override + public void subscribe(Observer<? super String> observer) { + observer.onSubscribe(Disposable.empty()); + // all should be skipped since they are happening faster than the 200ms timeout + publishNext(observer, 100, "a"); // Should be skipped + publishNext(observer, 200, "b"); // Should be skipped + publishNext(observer, 300, "c"); // Should be skipped + publishNext(observer, 400, "d"); // Should be skipped + publishNext(observer, 500, "e"); // Should be skipped + publishNext(observer, 600, "f"); // Should be skipped + publishNext(observer, 700, "g"); // Should be skipped + publishNext(observer, 800, "h"); // Should be skipped + publishCompleted(observer, 900); // Should be published as soon as the timeout expires. + } + }); + + Observable<String> sampled = source.debounce(200, TimeUnit.MILLISECONDS, scheduler); + sampled.subscribe(observer); + + scheduler.advanceTimeTo(0, TimeUnit.MILLISECONDS); + InOrder inOrder = inOrder(observer); + inOrder.verify(observer, times(0)).onNext(anyString()); + scheduler.advanceTimeTo(1000, TimeUnit.MILLISECONDS); + inOrder.verify(observer, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void debounceWithError() { + Observable<String> source = Observable.unsafeCreate(new ObservableSource<String>() { + @Override + public void subscribe(Observer<? super String> observer) { + observer.onSubscribe(Disposable.empty()); + Exception error = new TestException(); + publishNext(observer, 100, "one"); // Should be published since "two" will arrive after the timeout expires. + publishNext(observer, 600, "two"); // Should be skipped since onError will arrive before the timeout expires. + publishError(observer, 700, error); // Should be published as soon as the timeout expires. + } + }); + + Observable<String> sampled = source.debounce(400, TimeUnit.MILLISECONDS, scheduler); + sampled.subscribe(observer); + + scheduler.advanceTimeTo(0, TimeUnit.MILLISECONDS); + InOrder inOrder = inOrder(observer); + // 100 + 400 means it triggers at 500 + scheduler.advanceTimeTo(500, TimeUnit.MILLISECONDS); + inOrder.verify(observer).onNext("one"); + scheduler.advanceTimeTo(701, TimeUnit.MILLISECONDS); + inOrder.verify(observer).onError(any(TestException.class)); + inOrder.verifyNoMoreInteractions(); + } + + private <T> void publishCompleted(final Observer<T> observer, long delay) { + innerScheduler.schedule(new Runnable() { + @Override + public void run() { + observer.onComplete(); + } + }, delay, TimeUnit.MILLISECONDS); + } + + private <T> void publishError(final Observer<T> observer, long delay, final Exception error) { + innerScheduler.schedule(new Runnable() { + @Override + public void run() { + observer.onError(error); + } + }, delay, TimeUnit.MILLISECONDS); + } + + private <T> void publishNext(final Observer<T> observer, final long delay, final T value) { + innerScheduler.schedule(new Runnable() { + @Override + public void run() { + observer.onNext(value); + } + }, delay, TimeUnit.MILLISECONDS); + } + + @Test + public void debounceSelectorNormal1() { + PublishSubject<Integer> source = PublishSubject.create(); + final PublishSubject<Integer> debouncer = PublishSubject.create(); + Function<Integer, Observable<Integer>> debounceSel = new Function<Integer, Observable<Integer>>() { + + @Override + public Observable<Integer> apply(Integer t1) { + return debouncer; + } + }; + + Observer<Object> o = TestHelper.mockObserver(); + InOrder inOrder = inOrder(o); + + source.debounce(debounceSel).subscribe(o); + + source.onNext(1); + debouncer.onNext(1); + + source.onNext(2); + source.onNext(3); + source.onNext(4); + + debouncer.onNext(2); + + source.onNext(5); + source.onComplete(); + + inOrder.verify(o).onNext(1); + inOrder.verify(o).onNext(4); + inOrder.verify(o).onNext(5); + inOrder.verify(o).onComplete(); + + verify(o, never()).onError(any(Throwable.class)); + } + + @Test + public void debounceSelectorFuncThrows() { + PublishSubject<Integer> source = PublishSubject.create(); + Function<Integer, Observable<Integer>> debounceSel = new Function<Integer, Observable<Integer>>() { + + @Override + public Observable<Integer> apply(Integer t1) { + throw new TestException(); + } + }; + + Observer<Object> o = TestHelper.mockObserver(); + + source.debounce(debounceSel).subscribe(o); + + source.onNext(1); + + verify(o, never()).onNext(any()); + verify(o, never()).onComplete(); + verify(o).onError(any(TestException.class)); + } + + @Test + public void debounceSelectorObservableThrows() { + PublishSubject<Integer> source = PublishSubject.create(); + Function<Integer, Observable<Integer>> debounceSel = new Function<Integer, Observable<Integer>>() { + + @Override + public Observable<Integer> apply(Integer t1) { + return Observable.error(new TestException()); + } + }; + + Observer<Object> o = TestHelper.mockObserver(); + + source.debounce(debounceSel).subscribe(o); + + source.onNext(1); + + verify(o, never()).onNext(any()); + verify(o, never()).onComplete(); + verify(o).onError(any(TestException.class)); + } + + @Test + public void debounceTimedLastIsNotLost() { + PublishSubject<Integer> source = PublishSubject.create(); + + Observer<Object> o = TestHelper.mockObserver(); + + source.debounce(100, TimeUnit.MILLISECONDS, scheduler).subscribe(o); + + source.onNext(1); + source.onComplete(); + + scheduler.advanceTimeBy(1, TimeUnit.SECONDS); + + verify(o).onNext(1); + verify(o).onComplete(); + verify(o, never()).onError(any(Throwable.class)); + } + + @Test + public void debounceSelectorLastIsNotLost() { + PublishSubject<Integer> source = PublishSubject.create(); + final PublishSubject<Integer> debouncer = PublishSubject.create(); + + Function<Integer, Observable<Integer>> debounceSel = new Function<Integer, Observable<Integer>>() { + + @Override + public Observable<Integer> apply(Integer t1) { + return debouncer; + } + }; + + Observer<Object> o = TestHelper.mockObserver(); + + source.debounce(debounceSel).subscribe(o); + + source.onNext(1); + source.onComplete(); + + debouncer.onComplete(); + + verify(o).onNext(1); + verify(o).onComplete(); + verify(o, never()).onError(any(Throwable.class)); + } + + @Test + public void debounceWithTimeBackpressure() throws InterruptedException { + TestScheduler scheduler = new TestScheduler(); + TestObserverEx<Integer> observer = new TestObserverEx<>(); + + Observable.merge( + Observable.just(1), + Observable.just(2).delay(10, TimeUnit.MILLISECONDS, scheduler) + ).debounce(20, TimeUnit.MILLISECONDS, scheduler).take(1).subscribe(observer); + + scheduler.advanceTimeBy(30, TimeUnit.MILLISECONDS); + + observer.assertValue(2); + observer.assertTerminated(); + observer.assertNoErrors(); + } + + @Test + public void debounceDefault() throws Exception { + + Observable.just(1).debounce(1, TimeUnit.SECONDS) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(1); + } + + @Test + public void dispose() { + TestHelper.checkDisposed(PublishSubject.create().debounce(1, TimeUnit.SECONDS, new TestScheduler())); + + TestHelper.checkDisposed(PublishSubject.create().debounce(Functions.justFunction(Observable.never()))); + + Disposable d = new ObservableDebounceTimed.DebounceEmitter<>(1, 1, null); + assertFalse(d.isDisposed()); + + d.dispose(); + + assertTrue(d.isDisposed()); + } + + @Test + public void badSource() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + new Observable<Integer>() { + @Override + protected void subscribeActual(Observer<? super Integer> observer) { + observer.onSubscribe(Disposable.empty()); + observer.onComplete(); + observer.onNext(1); + observer.onError(new TestException()); + observer.onComplete(); + } + } + .debounce(1, TimeUnit.SECONDS, new TestScheduler()) + .test() + .assertResult(); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void badSourceSelector() { + TestHelper.checkBadSourceObservable(new Function<Observable<Integer>, Object>() { + @Override + public Object apply(Observable<Integer> o) throws Exception { + return o.debounce(new Function<Integer, ObservableSource<Long>>() { + @Override + public ObservableSource<Long> apply(Integer v) throws Exception { + return Observable.timer(1, TimeUnit.SECONDS); + } + }); + } + }, false, 1, 1, 1); + + TestHelper.checkBadSourceObservable(new Function<Observable<Integer>, Object>() { + @Override + public Object apply(final Observable<Integer> o) throws Exception { + return Observable.just(1).debounce(new Function<Integer, ObservableSource<Integer>>() { + @Override + public ObservableSource<Integer> apply(Integer v) throws Exception { + return o; + } + }); + } + }, false, 1, 1, 1); + } + + @Test + public void debounceWithEmpty() { + Observable.just(1).debounce(Functions.justFunction(Observable.empty())) + .test() + .assertResult(1); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeObservable(new Function<Observable<Object>, Observable<Object>>() { + @Override + public Observable<Object> apply(Observable<Object> o) throws Exception { + return o.debounce(Functions.justFunction(Observable.never())); + } + }); + } + + @Test + public void disposeInOnNext() { + final TestObserver<Integer> to = new TestObserver<>(); + + BehaviorSubject.createDefault(1) + .debounce(new Function<Integer, ObservableSource<Object>>() { + @Override + public ObservableSource<Object> apply(Integer o) throws Exception { + to.dispose(); + return Observable.never(); + } + }) + .subscribeWith(to) + .assertEmpty(); + + assertTrue(to.isDisposed()); + } + + @Test + public void disposedInOnComplete() { + final TestObserver<Integer> to = new TestObserver<>(); + + new Observable<Integer>() { + @Override + protected void subscribeActual(Observer<? super Integer> observer) { + observer.onSubscribe(Disposable.empty()); + to.dispose(); + observer.onComplete(); + } + } + .debounce(Functions.justFunction(Observable.never())) + .subscribeWith(to) + .assertEmpty(); + } + + @Test + public void emitLate() { + final AtomicReference<Observer<? super Integer>> ref = new AtomicReference<>(); + + TestObserver<Integer> to = Observable.range(1, 2) + .debounce(new Function<Integer, ObservableSource<Integer>>() { + @Override + public ObservableSource<Integer> apply(Integer o) throws Exception { + if (o != 1) { + return Observable.never(); + } + return new Observable<Integer>() { + @Override + protected void subscribeActual(Observer<? super Integer> observer) { + observer.onSubscribe(Disposable.empty()); + ref.set(observer); + } + }; + } + }) + .test(); + + ref.get().onNext(1); + + to + .assertResult(2); + } + + @Test + public void timedDoubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeFlowable(new Function<Flowable<Object>, Publisher<Object>>() { + @Override + public Publisher<Object> apply(Flowable<Object> f) + throws Exception { + return f.debounce(1, TimeUnit.SECONDS); + } + }); + } + + @Test + public void timedDisposedIgnoredBySource() { + final TestObserver<Integer> to = new TestObserver<>(); + + new Observable<Integer>() { + @Override + protected void subscribeActual( + Observer<? super Integer> observer) { + observer.onSubscribe(Disposable.empty()); + to.dispose(); + observer.onNext(1); + observer.onComplete(); + } + } + .debounce(1, TimeUnit.SECONDS) + .subscribe(to); + } + + @Test + public void timedLateEmit() { + TestObserver<Integer> to = new TestObserver<>(); + DebounceTimedObserver<Integer> sub = new DebounceTimedObserver<>( + to, 1, TimeUnit.SECONDS, new TestScheduler().createWorker(), null); + + sub.onSubscribe(Disposable.empty()); + + DebounceEmitter<Integer> de = new DebounceEmitter<>(1, 50, sub); + de.run(); + de.run(); + + to.assertEmpty(); + } + + @Test + public void timedError() { + Observable.error(new TestException()) + .debounce(1, TimeUnit.SECONDS) + .test() + .assertFailure(TestException.class); + } + + @Test + public void debounceOnEmpty() { + Observable.empty().debounce(new Function<Object, ObservableSource<Object>>() { + @Override + public ObservableSource<Object> apply(Object o) { + return Observable.just(new Object()); + } + }).subscribe(); + } + + @Test + public void doubleOnSubscribeTime() { + TestHelper.checkDoubleOnSubscribeObservable(o -> o.debounce(1, TimeUnit.SECONDS)); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableDefaultIfEmptyTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableDefaultIfEmptyTest.java new file mode 100644 index 0000000000..7a1d66e908 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableDefaultIfEmptyTest.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class ObservableDefaultIfEmptyTest extends RxJavaTest { + + @Test + public void defaultIfEmpty() { + Observable<Integer> source = Observable.just(1, 2, 3); + Observable<Integer> observable = source.defaultIfEmpty(10); + + Observer<Integer> observer = TestHelper.mockObserver(); + + observable.subscribe(observer); + + verify(observer, never()).onNext(10); + verify(observer).onNext(1); + verify(observer).onNext(2); + verify(observer).onNext(3); + verify(observer).onComplete(); + verify(observer, never()).onError(any(Throwable.class)); + } + + @Test + public void defaultIfEmptyWithEmpty() { + Observable<Integer> source = Observable.empty(); + Observable<Integer> observable = source.defaultIfEmpty(10); + + Observer<Integer> observer = TestHelper.mockObserver(); + + observable.subscribe(observer); + + verify(observer).onNext(10); + verify(observer).onComplete(); + verify(observer, never()).onError(any(Throwable.class)); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableDeferTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableDeferTest.java new file mode 100644 index 0000000000..cb919f02ff --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableDeferTest.java @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.Supplier; +import io.reactivex.rxjava3.testsupport.TestHelper; + +@SuppressWarnings("unchecked") +public class ObservableDeferTest extends RxJavaTest { + + @Test + public void defer() throws Throwable { + + Supplier<Observable<String>> factory = mock(Supplier.class); + + Observable<String> firstObservable = Observable.just("one", "two"); + Observable<String> secondObservable = Observable.just("three", "four"); + when(factory.get()).thenReturn(firstObservable, secondObservable); + + Observable<String> deferred = Observable.defer(factory); + + verifyNoInteractions(factory); + + Observer<String> firstObserver = TestHelper.mockObserver(); + deferred.subscribe(firstObserver); + + verify(factory, times(1)).get(); + verify(firstObserver, times(1)).onNext("one"); + verify(firstObserver, times(1)).onNext("two"); + verify(firstObserver, times(0)).onNext("three"); + verify(firstObserver, times(0)).onNext("four"); + verify(firstObserver, times(1)).onComplete(); + + Observer<String> secondObserver = TestHelper.mockObserver(); + deferred.subscribe(secondObserver); + + verify(factory, times(2)).get(); + verify(secondObserver, times(0)).onNext("one"); + verify(secondObserver, times(0)).onNext("two"); + verify(secondObserver, times(1)).onNext("three"); + verify(secondObserver, times(1)).onNext("four"); + verify(secondObserver, times(1)).onComplete(); + + } + + @Test + public void deferFunctionThrows() throws Throwable { + Supplier<Observable<String>> factory = mock(Supplier.class); + + when(factory.get()).thenThrow(new TestException()); + + Observable<String> result = Observable.defer(factory); + + Observer<String> o = TestHelper.mockObserver(); + + result.subscribe(o); + + verify(o).onError(any(TestException.class)); + verify(o, never()).onNext(any(String.class)); + verify(o, never()).onComplete(); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableDelaySubscriptionOtherTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableDelaySubscriptionOtherTest.java new file mode 100644 index 0000000000..02d988c2e8 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableDelaySubscriptionOtherTest.java @@ -0,0 +1,233 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.schedulers.Schedulers; +import io.reactivex.rxjava3.subjects.PublishSubject; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class ObservableDelaySubscriptionOtherTest extends RxJavaTest { + @Test + public void noPrematureSubscription() { + PublishSubject<Object> other = PublishSubject.create(); + + TestObserver<Integer> to = new TestObserver<>(); + + final AtomicInteger subscribed = new AtomicInteger(); + + Observable.just(1) + .doOnSubscribe(new Consumer<Disposable>() { + @Override + public void accept(Disposable d) { + subscribed.getAndIncrement(); + } + }) + .delaySubscription(other) + .subscribe(to); + + to.assertNotComplete(); + to.assertNoErrors(); + to.assertNoValues(); + + Assert.assertEquals("Premature subscription", 0, subscribed.get()); + + other.onNext(1); + + Assert.assertEquals("No subscription", 1, subscribed.get()); + + to.assertValue(1); + to.assertNoErrors(); + to.assertComplete(); + } + + @Test + public void noMultipleSubscriptions() { + PublishSubject<Object> other = PublishSubject.create(); + + TestObserver<Integer> to = new TestObserver<>(); + + final AtomicInteger subscribed = new AtomicInteger(); + + Observable.just(1) + .doOnSubscribe(new Consumer<Disposable>() { + @Override + public void accept(Disposable d) { + subscribed.getAndIncrement(); + } + }) + .delaySubscription(other) + .subscribe(to); + + to.assertNotComplete(); + to.assertNoErrors(); + to.assertNoValues(); + + Assert.assertEquals("Premature subscription", 0, subscribed.get()); + + other.onNext(1); + other.onNext(2); + + Assert.assertEquals("No subscription", 1, subscribed.get()); + + to.assertValue(1); + to.assertNoErrors(); + to.assertComplete(); + } + + @Test + public void completeTriggersSubscription() { + PublishSubject<Object> other = PublishSubject.create(); + + TestObserver<Integer> to = new TestObserver<>(); + + final AtomicInteger subscribed = new AtomicInteger(); + + Observable.just(1) + .doOnSubscribe(new Consumer<Disposable>() { + @Override + public void accept(Disposable d) { + subscribed.getAndIncrement(); + } + }) + .delaySubscription(other) + .subscribe(to); + + to.assertNotComplete(); + to.assertNoErrors(); + to.assertNoValues(); + + Assert.assertEquals("Premature subscription", 0, subscribed.get()); + + other.onComplete(); + + Assert.assertEquals("No subscription", 1, subscribed.get()); + + to.assertValue(1); + to.assertNoErrors(); + to.assertComplete(); + } + + @Test + public void noPrematureSubscriptionToError() { + PublishSubject<Object> other = PublishSubject.create(); + + TestObserver<Integer> to = new TestObserver<>(); + + final AtomicInteger subscribed = new AtomicInteger(); + + Observable.<Integer>error(new TestException()) + .doOnSubscribe(new Consumer<Disposable>() { + @Override + public void accept(Disposable d) { + subscribed.getAndIncrement(); + } + }) + .delaySubscription(other) + .subscribe(to); + + to.assertNotComplete(); + to.assertNoErrors(); + to.assertNoValues(); + + Assert.assertEquals("Premature subscription", 0, subscribed.get()); + + other.onComplete(); + + Assert.assertEquals("No subscription", 1, subscribed.get()); + + to.assertNoValues(); + to.assertNotComplete(); + to.assertError(TestException.class); + } + + @Test + public void noSubscriptionIfOtherErrors() { + PublishSubject<Object> other = PublishSubject.create(); + + TestObserver<Integer> to = new TestObserver<>(); + + final AtomicInteger subscribed = new AtomicInteger(); + + Observable.<Integer>error(new TestException()) + .doOnSubscribe(new Consumer<Disposable>() { + @Override + public void accept(Disposable d) { + subscribed.getAndIncrement(); + } + }) + .delaySubscription(other) + .subscribe(to); + + to.assertNotComplete(); + to.assertNoErrors(); + to.assertNoValues(); + + Assert.assertEquals("Premature subscription", 0, subscribed.get()); + + other.onError(new TestException()); + + Assert.assertEquals("Premature subscription", 0, subscribed.get()); + + to.assertNoValues(); + to.assertNotComplete(); + to.assertError(TestException.class); + } + + @Test + public void badSourceOther() { + TestHelper.checkBadSourceObservable(new Function<Observable<Integer>, Object>() { + @Override + public Object apply(Observable<Integer> o) throws Exception { + return Observable.just(1).delaySubscription(o); + } + }, false, 1, 1, 1); + } + + @Test + public void afterDelayNoInterrupt() { + ScheduledExecutorService exec = Executors.newSingleThreadScheduledExecutor(); + try { + for (Scheduler s : new Scheduler[] { Schedulers.single(), Schedulers.computation(), Schedulers.newThread(), Schedulers.io(), Schedulers.from(exec) }) { + final TestObserver<Boolean> observer = TestObserver.create(); + observer.withTag(s.getClass().getSimpleName()); + + Observable.<Boolean>create(new ObservableOnSubscribe<Boolean>() { + @Override + public void subscribe(ObservableEmitter<Boolean> emitter) throws Exception { + emitter.onNext(Thread.interrupted()); + emitter.onComplete(); + } + }) + .delaySubscription(100, TimeUnit.MILLISECONDS, s) + .subscribe(observer); + + observer.awaitDone(5, TimeUnit.SECONDS); + observer.assertValue(false); + } + } finally { + exec.shutdown(); + } + } + +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableDelayTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableDelayTest.java new file mode 100644 index 0000000000..ea8d51d996 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableDelayTest.java @@ -0,0 +1,981 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +import java.util.*; +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicReference; + +import org.junit.*; +import org.mockito.InOrder; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.core.Observer; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.observers.*; +import io.reactivex.rxjava3.schedulers.*; +import io.reactivex.rxjava3.subjects.PublishSubject; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class ObservableDelayTest extends RxJavaTest { + private Observer<Long> observer; + private Observer<Long> observer2; + + private TestScheduler scheduler; + + @Before + public void before() { + observer = TestHelper.mockObserver(); + observer2 = TestHelper.mockObserver(); + + scheduler = new TestScheduler(); + } + + @Test + public void delay() { + Observable<Long> source = Observable.interval(1L, TimeUnit.SECONDS, scheduler).take(3); + Observable<Long> delayed = source.delay(500L, TimeUnit.MILLISECONDS, scheduler); + delayed.subscribe(observer); + + InOrder inOrder = inOrder(observer); + scheduler.advanceTimeTo(1499L, TimeUnit.MILLISECONDS); + verify(observer, never()).onNext(anyLong()); + verify(observer, never()).onComplete(); + verify(observer, never()).onError(any(Throwable.class)); + + scheduler.advanceTimeTo(1500L, TimeUnit.MILLISECONDS); + inOrder.verify(observer, times(1)).onNext(0L); + inOrder.verify(observer, never()).onNext(anyLong()); + verify(observer, never()).onComplete(); + verify(observer, never()).onError(any(Throwable.class)); + + scheduler.advanceTimeTo(2400L, TimeUnit.MILLISECONDS); + inOrder.verify(observer, never()).onNext(anyLong()); + verify(observer, never()).onComplete(); + verify(observer, never()).onError(any(Throwable.class)); + + scheduler.advanceTimeTo(2500L, TimeUnit.MILLISECONDS); + inOrder.verify(observer, times(1)).onNext(1L); + inOrder.verify(observer, never()).onNext(anyLong()); + verify(observer, never()).onComplete(); + verify(observer, never()).onError(any(Throwable.class)); + + scheduler.advanceTimeTo(3400L, TimeUnit.MILLISECONDS); + inOrder.verify(observer, never()).onNext(anyLong()); + verify(observer, never()).onComplete(); + verify(observer, never()).onError(any(Throwable.class)); + + scheduler.advanceTimeTo(3500L, TimeUnit.MILLISECONDS); + inOrder.verify(observer, times(1)).onNext(2L); + verify(observer, times(1)).onComplete(); + verify(observer, never()).onError(any(Throwable.class)); + } + + @Test + public void longDelay() { + Observable<Long> source = Observable.interval(1L, TimeUnit.SECONDS, scheduler).take(3); + Observable<Long> delayed = source.delay(5L, TimeUnit.SECONDS, scheduler); + delayed.subscribe(observer); + + InOrder inOrder = inOrder(observer); + + scheduler.advanceTimeTo(5999L, TimeUnit.MILLISECONDS); + verify(observer, never()).onNext(anyLong()); + verify(observer, never()).onComplete(); + verify(observer, never()).onError(any(Throwable.class)); + + scheduler.advanceTimeTo(6000L, TimeUnit.MILLISECONDS); + inOrder.verify(observer, times(1)).onNext(0L); + scheduler.advanceTimeTo(6999L, TimeUnit.MILLISECONDS); + inOrder.verify(observer, never()).onNext(anyLong()); + scheduler.advanceTimeTo(7000L, TimeUnit.MILLISECONDS); + inOrder.verify(observer, times(1)).onNext(1L); + scheduler.advanceTimeTo(7999L, TimeUnit.MILLISECONDS); + inOrder.verify(observer, never()).onNext(anyLong()); + scheduler.advanceTimeTo(8000L, TimeUnit.MILLISECONDS); + inOrder.verify(observer, times(1)).onNext(2L); + inOrder.verify(observer, times(1)).onComplete(); + inOrder.verify(observer, never()).onNext(anyLong()); + inOrder.verify(observer, never()).onComplete(); + verify(observer, never()).onError(any(Throwable.class)); + } + + @Test + public void delayWithError() { + Observable<Long> source = Observable.interval(1L, TimeUnit.SECONDS, scheduler) + .map(new Function<Long, Long>() { + @Override + public Long apply(Long value) { + if (value == 1L) { + throw new RuntimeException("error!"); + } + return value; + } + }); + Observable<Long> delayed = source.delay(1L, TimeUnit.SECONDS, scheduler); + delayed.subscribe(observer); + + InOrder inOrder = inOrder(observer); + + scheduler.advanceTimeTo(1999L, TimeUnit.MILLISECONDS); + verify(observer, never()).onNext(anyLong()); + verify(observer, never()).onComplete(); + verify(observer, never()).onError(any(Throwable.class)); + + scheduler.advanceTimeTo(2000L, TimeUnit.MILLISECONDS); + inOrder.verify(observer, times(1)).onError(any(Throwable.class)); + inOrder.verify(observer, never()).onNext(anyLong()); + verify(observer, never()).onComplete(); + + scheduler.advanceTimeTo(5000L, TimeUnit.MILLISECONDS); + inOrder.verify(observer, never()).onNext(anyLong()); + inOrder.verify(observer, never()).onError(any(Throwable.class)); + verify(observer, never()).onComplete(); + } + + @Test + public void delayWithMultipleSubscriptions() { + Observable<Long> source = Observable.interval(1L, TimeUnit.SECONDS, scheduler).take(3); + Observable<Long> delayed = source.delay(500L, TimeUnit.MILLISECONDS, scheduler); + delayed.subscribe(observer); + delayed.subscribe(observer2); + + InOrder inOrder = inOrder(observer); + InOrder inOrder2 = inOrder(observer2); + + scheduler.advanceTimeTo(1499L, TimeUnit.MILLISECONDS); + verify(observer, never()).onNext(anyLong()); + verify(observer2, never()).onNext(anyLong()); + + scheduler.advanceTimeTo(1500L, TimeUnit.MILLISECONDS); + inOrder.verify(observer, times(1)).onNext(0L); + inOrder2.verify(observer2, times(1)).onNext(0L); + + scheduler.advanceTimeTo(2499L, TimeUnit.MILLISECONDS); + inOrder.verify(observer, never()).onNext(anyLong()); + inOrder2.verify(observer2, never()).onNext(anyLong()); + + scheduler.advanceTimeTo(2500L, TimeUnit.MILLISECONDS); + inOrder.verify(observer, times(1)).onNext(1L); + inOrder2.verify(observer2, times(1)).onNext(1L); + + verify(observer, never()).onComplete(); + verify(observer2, never()).onComplete(); + + scheduler.advanceTimeTo(3500L, TimeUnit.MILLISECONDS); + inOrder.verify(observer, times(1)).onNext(2L); + inOrder2.verify(observer2, times(1)).onNext(2L); + inOrder.verify(observer, never()).onNext(anyLong()); + inOrder2.verify(observer2, never()).onNext(anyLong()); + inOrder.verify(observer, times(1)).onComplete(); + inOrder2.verify(observer2, times(1)).onComplete(); + + verify(observer, never()).onError(any(Throwable.class)); + verify(observer2, never()).onError(any(Throwable.class)); + } + + @Test + public void delaySubscription() { + Observable<Integer> result = Observable.just(1, 2, 3).delaySubscription(100, TimeUnit.MILLISECONDS, scheduler); + + Observer<Object> o = TestHelper.mockObserver(); + InOrder inOrder = inOrder(o); + + result.subscribe(o); + + inOrder.verify(o, never()).onNext(any()); + inOrder.verify(o, never()).onComplete(); + + scheduler.advanceTimeBy(100, TimeUnit.MILLISECONDS); + + inOrder.verify(o, times(1)).onNext(1); + inOrder.verify(o, times(1)).onNext(2); + inOrder.verify(o, times(1)).onNext(3); + inOrder.verify(o, times(1)).onComplete(); + + verify(o, never()).onError(any(Throwable.class)); + } + + @Test + public void delaySubscriptionDisposeBeforeTime() { + Observable<Integer> result = Observable.just(1, 2, 3).delaySubscription(100, TimeUnit.MILLISECONDS, scheduler); + + Observer<Object> o = TestHelper.mockObserver(); + TestObserver<Object> to = new TestObserver<>(o); + + result.subscribe(to); + to.dispose(); + scheduler.advanceTimeBy(100, TimeUnit.MILLISECONDS); + + verify(o, never()).onNext(any()); + verify(o, never()).onComplete(); + verify(o, never()).onError(any(Throwable.class)); + } + + @Test + public void delayWithObservableNormal1() { + PublishSubject<Integer> source = PublishSubject.create(); + final List<PublishSubject<Integer>> delays = new ArrayList<>(); + final int n = 10; + for (int i = 0; i < n; i++) { + PublishSubject<Integer> delay = PublishSubject.create(); + delays.add(delay); + } + + Function<Integer, Observable<Integer>> delayFunc = new Function<Integer, Observable<Integer>>() { + @Override + public Observable<Integer> apply(Integer t1) { + return delays.get(t1); + } + }; + + Observer<Object> o = TestHelper.mockObserver(); + InOrder inOrder = inOrder(o); + + source.delay(delayFunc).subscribe(o); + + for (int i = 0; i < n; i++) { + source.onNext(i); + delays.get(i).onNext(i); + inOrder.verify(o).onNext(i); + } + source.onComplete(); + + inOrder.verify(o).onComplete(); + inOrder.verifyNoMoreInteractions(); + + verify(o, never()).onError(any(Throwable.class)); + } + + @Test + public void delayWithObservableSingleSend1() { + PublishSubject<Integer> source = PublishSubject.create(); + final PublishSubject<Integer> delay = PublishSubject.create(); + + Function<Integer, Observable<Integer>> delayFunc = new Function<Integer, Observable<Integer>>() { + + @Override + public Observable<Integer> apply(Integer t1) { + return delay; + } + }; + Observer<Object> o = TestHelper.mockObserver(); + InOrder inOrder = inOrder(o); + + source.delay(delayFunc).subscribe(o); + + source.onNext(1); + delay.onNext(1); + delay.onNext(2); + + inOrder.verify(o).onNext(1); + inOrder.verifyNoMoreInteractions(); + verify(o, never()).onError(any(Throwable.class)); + } + + @Test + public void delayWithObservableSourceThrows() { + PublishSubject<Integer> source = PublishSubject.create(); + final PublishSubject<Integer> delay = PublishSubject.create(); + + Function<Integer, Observable<Integer>> delayFunc = new Function<Integer, Observable<Integer>>() { + + @Override + public Observable<Integer> apply(Integer t1) { + return delay; + } + }; + Observer<Object> o = TestHelper.mockObserver(); + InOrder inOrder = inOrder(o); + + source.delay(delayFunc).subscribe(o); + source.onNext(1); + source.onError(new TestException()); + delay.onNext(1); + + inOrder.verify(o).onError(any(TestException.class)); + inOrder.verifyNoMoreInteractions(); + verify(o, never()).onNext(any()); + verify(o, never()).onComplete(); + } + + @Test + public void delayWithObservableDelayFunctionThrows() { + PublishSubject<Integer> source = PublishSubject.create(); + + Function<Integer, Observable<Integer>> delayFunc = new Function<Integer, Observable<Integer>>() { + + @Override + public Observable<Integer> apply(Integer t1) { + throw new TestException(); + } + }; + Observer<Object> o = TestHelper.mockObserver(); + InOrder inOrder = inOrder(o); + + source.delay(delayFunc).subscribe(o); + source.onNext(1); + + inOrder.verify(o).onError(any(TestException.class)); + inOrder.verifyNoMoreInteractions(); + verify(o, never()).onNext(any()); + verify(o, never()).onComplete(); + } + + @Test + public void delayWithObservableDelayThrows() { + PublishSubject<Integer> source = PublishSubject.create(); + final PublishSubject<Integer> delay = PublishSubject.create(); + + Function<Integer, Observable<Integer>> delayFunc = new Function<Integer, Observable<Integer>>() { + + @Override + public Observable<Integer> apply(Integer t1) { + return delay; + } + }; + Observer<Object> o = TestHelper.mockObserver(); + InOrder inOrder = inOrder(o); + + source.delay(delayFunc).subscribe(o); + source.onNext(1); + delay.onError(new TestException()); + + inOrder.verify(o).onError(any(TestException.class)); + inOrder.verifyNoMoreInteractions(); + verify(o, never()).onNext(any()); + verify(o, never()).onComplete(); + } + + @Test + public void delayWithObservableSubscriptionNormal() { + PublishSubject<Integer> source = PublishSubject.create(); + final PublishSubject<Integer> delay = PublishSubject.create(); + Supplier<Observable<Integer>> subFunc = new Supplier<Observable<Integer>>() { + @Override + public Observable<Integer> get() { + return delay; + } + }; + Function<Integer, Observable<Integer>> delayFunc = new Function<Integer, Observable<Integer>>() { + + @Override + public Observable<Integer> apply(Integer t1) { + return delay; + } + }; + + Observer<Object> o = TestHelper.mockObserver(); + InOrder inOrder = inOrder(o); + + source.delay(Observable.defer(subFunc), delayFunc).subscribe(o); + + source.onNext(1); + delay.onNext(1); + + source.onNext(2); + delay.onNext(2); + + inOrder.verify(o).onNext(2); + inOrder.verifyNoMoreInteractions(); + verify(o, never()).onError(any(Throwable.class)); + verify(o, never()).onComplete(); + } + + @Test + public void delayWithObservableSubscriptionFunctionThrows() { + PublishSubject<Integer> source = PublishSubject.create(); + final PublishSubject<Integer> delay = PublishSubject.create(); + Supplier<Observable<Integer>> subFunc = new Supplier<Observable<Integer>>() { + @Override + public Observable<Integer> get() { + throw new TestException(); + } + }; + Function<Integer, Observable<Integer>> delayFunc = new Function<Integer, Observable<Integer>>() { + + @Override + public Observable<Integer> apply(Integer t1) { + return delay; + } + }; + + Observer<Object> o = TestHelper.mockObserver(); + InOrder inOrder = inOrder(o); + + source.delay(Observable.defer(subFunc), delayFunc).subscribe(o); + + source.onNext(1); + delay.onNext(1); + + source.onNext(2); + + inOrder.verify(o).onError(any(TestException.class)); + inOrder.verifyNoMoreInteractions(); + verify(o, never()).onNext(any()); + verify(o, never()).onComplete(); + } + + @Test + public void delayWithObservableSubscriptionThrows() { + PublishSubject<Integer> source = PublishSubject.create(); + final PublishSubject<Integer> delay = PublishSubject.create(); + Supplier<Observable<Integer>> subFunc = new Supplier<Observable<Integer>>() { + @Override + public Observable<Integer> get() { + return delay; + } + }; + Function<Integer, Observable<Integer>> delayFunc = new Function<Integer, Observable<Integer>>() { + + @Override + public Observable<Integer> apply(Integer t1) { + return delay; + } + }; + + Observer<Object> o = TestHelper.mockObserver(); + InOrder inOrder = inOrder(o); + + source.delay(Observable.defer(subFunc), delayFunc).subscribe(o); + + source.onNext(1); + delay.onError(new TestException()); + + source.onNext(2); + + inOrder.verify(o).onError(any(TestException.class)); + inOrder.verifyNoMoreInteractions(); + verify(o, never()).onNext(any()); + verify(o, never()).onComplete(); + } + + @Test + public void delayWithObservableEmptyDelayer() { + PublishSubject<Integer> source = PublishSubject.create(); + + Function<Integer, Observable<Integer>> delayFunc = new Function<Integer, Observable<Integer>>() { + + @Override + public Observable<Integer> apply(Integer t1) { + return Observable.empty(); + } + }; + Observer<Object> o = TestHelper.mockObserver(); + InOrder inOrder = inOrder(o); + + source.delay(delayFunc).subscribe(o); + + source.onNext(1); + source.onComplete(); + + inOrder.verify(o).onNext(1); + inOrder.verify(o).onComplete(); + inOrder.verifyNoMoreInteractions(); + verify(o, never()).onError(any(Throwable.class)); + } + + @Test + public void delayWithObservableSubscriptionRunCompletion() { + PublishSubject<Integer> source = PublishSubject.create(); + final PublishSubject<Integer> sdelay = PublishSubject.create(); + final PublishSubject<Integer> delay = PublishSubject.create(); + Supplier<Observable<Integer>> subFunc = new Supplier<Observable<Integer>>() { + @Override + public Observable<Integer> get() { + return sdelay; + } + }; + Function<Integer, Observable<Integer>> delayFunc = new Function<Integer, Observable<Integer>>() { + + @Override + public Observable<Integer> apply(Integer t1) { + return delay; + } + }; + + Observer<Object> o = TestHelper.mockObserver(); + InOrder inOrder = inOrder(o); + + source.delay(Observable.defer(subFunc), delayFunc).subscribe(o); + + source.onNext(1); + sdelay.onComplete(); + + source.onNext(2); + delay.onNext(2); + + inOrder.verify(o).onNext(2); + inOrder.verifyNoMoreInteractions(); + verify(o, never()).onError(any(Throwable.class)); + verify(o, never()).onComplete(); + } + + @Test + public void delayWithObservableAsTimed() { + Observable<Long> source = Observable.interval(1L, TimeUnit.SECONDS, scheduler).take(3); + + final Observable<Long> delayer = Observable.timer(500L, TimeUnit.MILLISECONDS, scheduler); + + Function<Long, Observable<Long>> delayFunc = new Function<Long, Observable<Long>>() { + @Override + public Observable<Long> apply(Long t1) { + return delayer; + } + }; + + Observable<Long> delayed = source.delay(delayFunc); + delayed.subscribe(observer); + + InOrder inOrder = inOrder(observer); + scheduler.advanceTimeTo(1499L, TimeUnit.MILLISECONDS); + verify(observer, never()).onNext(anyLong()); + verify(observer, never()).onComplete(); + verify(observer, never()).onError(any(Throwable.class)); + + scheduler.advanceTimeTo(1500L, TimeUnit.MILLISECONDS); + inOrder.verify(observer, times(1)).onNext(0L); + inOrder.verify(observer, never()).onNext(anyLong()); + verify(observer, never()).onComplete(); + verify(observer, never()).onError(any(Throwable.class)); + + scheduler.advanceTimeTo(2400L, TimeUnit.MILLISECONDS); + inOrder.verify(observer, never()).onNext(anyLong()); + verify(observer, never()).onComplete(); + verify(observer, never()).onError(any(Throwable.class)); + + scheduler.advanceTimeTo(2500L, TimeUnit.MILLISECONDS); + inOrder.verify(observer, times(1)).onNext(1L); + inOrder.verify(observer, never()).onNext(anyLong()); + verify(observer, never()).onComplete(); + verify(observer, never()).onError(any(Throwable.class)); + + scheduler.advanceTimeTo(3400L, TimeUnit.MILLISECONDS); + inOrder.verify(observer, never()).onNext(anyLong()); + verify(observer, never()).onComplete(); + verify(observer, never()).onError(any(Throwable.class)); + + scheduler.advanceTimeTo(3500L, TimeUnit.MILLISECONDS); + inOrder.verify(observer, times(1)).onNext(2L); + verify(observer, times(1)).onComplete(); + verify(observer, never()).onError(any(Throwable.class)); + } + + @Test + public void delayWithObservableReorder() { + int n = 3; + + PublishSubject<Integer> source = PublishSubject.create(); + final List<PublishSubject<Integer>> subjects = new ArrayList<>(); + for (int i = 0; i < n; i++) { + subjects.add(PublishSubject.<Integer> create()); + } + + Observable<Integer> result = source.delay(new Function<Integer, Observable<Integer>>() { + + @Override + public Observable<Integer> apply(Integer t1) { + return subjects.get(t1); + } + }); + + Observer<Object> o = TestHelper.mockObserver(); + InOrder inOrder = inOrder(o); + + result.subscribe(o); + + for (int i = 0; i < n; i++) { + source.onNext(i); + } + source.onComplete(); + + inOrder.verify(o, never()).onNext(anyInt()); + inOrder.verify(o, never()).onComplete(); + + for (int i = n - 1; i >= 0; i--) { + subjects.get(i).onComplete(); + inOrder.verify(o).onNext(i); + } + + inOrder.verify(o).onComplete(); + + verify(o, never()).onError(any(Throwable.class)); + } + + @Test + public void delayEmitsEverything() { + Observable<Integer> source = Observable.range(1, 5); + Observable<Integer> delayed = source.delay(500L, TimeUnit.MILLISECONDS, scheduler); + delayed = delayed.doOnEach(new Consumer<Notification<Integer>>() { + + @Override + public void accept(Notification<Integer> t1) { + System.out.println(t1); + } + + }); + TestObserver<Integer> observer = new TestObserver<>(); + delayed.subscribe(observer); + // all will be delivered after 500ms since range does not delay between them + scheduler.advanceTimeBy(500L, TimeUnit.MILLISECONDS); + observer.assertValues(1, 2, 3, 4, 5); + } + + @Test + public void backpressureWithTimedDelay() { + TestObserver<Integer> to = new TestObserver<>(); + Observable.range(1, Flowable.bufferSize() * 2) + .delay(100, TimeUnit.MILLISECONDS) + .observeOn(Schedulers.computation()) + .map(new Function<Integer, Integer>() { + + int c; + + @Override + public Integer apply(Integer t) { + if (c++ <= 0) { + try { + Thread.sleep(500); + } catch (InterruptedException e) { + } + } + return t; + } + + }).subscribe(to); + + to.awaitDone(5, TimeUnit.SECONDS); + to.assertNoErrors(); + assertEquals(Flowable.bufferSize() * 2, to.values().size()); + } + + @Test + public void backpressureWithSubscriptionTimedDelay() { + TestObserver<Integer> to = new TestObserver<>(); + Observable.range(1, Flowable.bufferSize() * 2) + .delaySubscription(100, TimeUnit.MILLISECONDS) + .delay(100, TimeUnit.MILLISECONDS) + .observeOn(Schedulers.computation()) + .map(new Function<Integer, Integer>() { + + int c; + + @Override + public Integer apply(Integer t) { + if (c++ <= 0) { + try { + Thread.sleep(500); + } catch (InterruptedException e) { + } + } + return t; + } + + }).subscribe(to); + + to.awaitDone(5, TimeUnit.SECONDS); + to.assertNoErrors(); + assertEquals(Flowable.bufferSize() * 2, to.values().size()); + } + + @Test + public void backpressureWithSelectorDelay() { + TestObserver<Integer> to = new TestObserver<>(); + Observable.range(1, Flowable.bufferSize() * 2) + .delay(new Function<Integer, Observable<Long>>() { + + @Override + public Observable<Long> apply(Integer i) { + return Observable.timer(100, TimeUnit.MILLISECONDS); + } + + }) + .observeOn(Schedulers.computation()) + .map(new Function<Integer, Integer>() { + + int c; + + @Override + public Integer apply(Integer t) { + if (c++ <= 0) { + try { + Thread.sleep(500); + } catch (InterruptedException e) { + } + } + return t; + } + + }).subscribe(to); + + to.awaitDone(5, TimeUnit.SECONDS); + to.assertNoErrors(); + assertEquals(Flowable.bufferSize() * 2, to.values().size()); + } + + @Test + public void backpressureWithSelectorDelayAndSubscriptionDelay() { + TestObserver<Integer> to = new TestObserver<>(); + Observable.range(1, Flowable.bufferSize() * 2) + .delay(Observable.timer(500, TimeUnit.MILLISECONDS) + , new Function<Integer, Observable<Long>>() { + + @Override + public Observable<Long> apply(Integer i) { + return Observable.timer(100, TimeUnit.MILLISECONDS); + } + + }) + .observeOn(Schedulers.computation()) + .map(new Function<Integer, Integer>() { + + int c; + + @Override + public Integer apply(Integer t) { + if (c++ <= 0) { + try { + Thread.sleep(500); + } catch (InterruptedException e) { + } + } + return t; + } + + }).subscribe(to); + + to.awaitDone(5, TimeUnit.SECONDS); + to.assertNoErrors(); + assertEquals(Flowable.bufferSize() * 2, to.values().size()); + } + + @Test + public void errorRunsBeforeOnNext() { + TestScheduler test = new TestScheduler(); + + PublishSubject<Integer> ps = PublishSubject.create(); + + TestObserver<Integer> to = new TestObserver<>(); + + ps.delay(1, TimeUnit.SECONDS, test).subscribe(to); + + ps.onNext(1); + + test.advanceTimeBy(500, TimeUnit.MILLISECONDS); + + ps.onError(new TestException()); + + test.advanceTimeBy(1, TimeUnit.SECONDS); + + to.assertNoValues(); + to.assertError(TestException.class); + to.assertNotComplete(); + } + + @Test + public void delaySupplierSimple() { + final PublishSubject<Integer> ps = PublishSubject.create(); + + Observable<Integer> source = Observable.range(1, 5); + + TestObserver<Integer> to = new TestObserver<>(); + + source.delaySubscription(ps).subscribe(to); + + to.assertNoValues(); + to.assertNoErrors(); + to.assertNotComplete(); + + ps.onNext(1); + + to.assertValues(1, 2, 3, 4, 5); + to.assertComplete(); + to.assertNoErrors(); + } + + @Test + public void delaySupplierCompletes() { + final PublishSubject<Integer> ps = PublishSubject.create(); + + Observable<Integer> source = Observable.range(1, 5); + + TestObserver<Integer> to = new TestObserver<>(); + + source.delaySubscription(ps).subscribe(to); + + to.assertNoValues(); + to.assertNoErrors(); + to.assertNotComplete(); + + // FIXME should this complete the source instead of consuming it? + ps.onComplete(); + + to.assertValues(1, 2, 3, 4, 5); + to.assertComplete(); + to.assertNoErrors(); + } + + @Test + public void delaySupplierErrors() { + final PublishSubject<Integer> ps = PublishSubject.create(); + + Observable<Integer> source = Observable.range(1, 5); + + TestObserver<Integer> to = new TestObserver<>(); + + source.delaySubscription(ps).subscribe(to); + + to.assertNoValues(); + to.assertNoErrors(); + to.assertNotComplete(); + + ps.onError(new TestException()); + + to.assertNoValues(); + to.assertNotComplete(); + to.assertError(TestException.class); + } + + @Test + public void delayWithTimeDelayError() throws Exception { + Observable.just(1).concatWith(Observable.<Integer>error(new TestException())) + .delay(100, TimeUnit.MILLISECONDS, true) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertFailure(TestException.class, 1); + } + + @Test + public void onErrorCalledOnScheduler() throws Exception { + final CountDownLatch latch = new CountDownLatch(1); + final AtomicReference<Thread> thread = new AtomicReference<>(); + + Observable.<String>error(new Exception()) + .delay(0, TimeUnit.MILLISECONDS, Schedulers.newThread()) + .doOnError(new Consumer<Throwable>() { + @Override + public void accept(Throwable throwable) throws Exception { + thread.set(Thread.currentThread()); + latch.countDown(); + } + }) + .onErrorResumeWith(Observable.<String>empty()) + .subscribe(); + + latch.await(); + + assertNotEquals(Thread.currentThread(), thread.get()); + } + + @Test + public void dispose() { + TestHelper.checkDisposed(PublishSubject.create().delay(1, TimeUnit.SECONDS)); + + TestHelper.checkDisposed(PublishSubject.create().delay(Functions.justFunction(Observable.never()))); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeObservable(new Function<Observable<Object>, ObservableSource<Object>>() { + @Override + public ObservableSource<Object> apply(Observable<Object> o) throws Exception { + return o.delay(1, TimeUnit.SECONDS); + } + }); + + TestHelper.checkDoubleOnSubscribeObservable(new Function<Observable<Object>, ObservableSource<Object>>() { + @Override + public ObservableSource<Object> apply(Observable<Object> o) throws Exception { + return o.delay(Functions.justFunction(Observable.never())); + } + }); + } + + @Test + public void onCompleteFinal() { + TestScheduler scheduler = new TestScheduler(); + + Observable.empty() + .delay(1, TimeUnit.MILLISECONDS, scheduler) + .subscribe(new DisposableObserver<Object>() { + @Override + public void onNext(Object value) { + } + + @Override + public void onError(Throwable e) { + } + + @Override + public void onComplete() { + throw new TestException(); + } + }); + + try { + scheduler.advanceTimeBy(1, TimeUnit.SECONDS); + fail("Should have thrown"); + } catch (TestException ex) { + // expected + } + } + + @Test + public void onErrorFinal() { + TestScheduler scheduler = new TestScheduler(); + + Observable.error(new TestException()) + .delay(1, TimeUnit.MILLISECONDS, scheduler) + .subscribe(new DisposableObserver<Object>() { + @Override + public void onNext(Object value) { + } + + @Override + public void onError(Throwable e) { + throw new TestException(); + } + + @Override + public void onComplete() { + } + }); + + try { + scheduler.advanceTimeBy(1, TimeUnit.SECONDS); + fail("Should have thrown"); + } catch (TestException ex) { + // expected + } + } + + @Test + public void itemDelayReturnsNull() { + Observable.just(1).delay(new Function<Integer, Observable<Object>>() { + @Override + public Observable<Object> apply(Integer t) throws Exception { + return null; + } + }) + .to(TestHelper.<Integer>testConsumer()) + .assertFailureAndMessage(NullPointerException.class, "The itemDelay returned a null ObservableSource"); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableDematerializeTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableDematerializeTest.java new file mode 100644 index 0000000000..e28b71a629 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableDematerializeTest.java @@ -0,0 +1,238 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import java.util.List; + +import io.reactivex.rxjava3.disposables.Disposable; +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.testsupport.*; + +public class ObservableDematerializeTest extends RxJavaTest { + + @Test + public void simpleSelector() { + Observable<Notification<Integer>> notifications = Observable.just(1, 2).materialize(); + Observable<Integer> dematerialize = notifications.dematerialize(Functions.<Notification<Integer>>identity()); + + Observer<Integer> observer = TestHelper.mockObserver(); + + dematerialize.subscribe(observer); + + verify(observer, times(1)).onNext(1); + verify(observer, times(1)).onNext(2); + verify(observer, times(1)).onComplete(); + verify(observer, never()).onError(any(Throwable.class)); + } + + @Test + public void selectorCrash() { + Observable.just(1, 2) + .materialize() + .dematerialize(new Function<Notification<Integer>, Notification<Object>>() { + @Override + public Notification<Object> apply(Notification<Integer> v) throws Exception { + throw new TestException(); + } + }) + .test() + .assertFailure(TestException.class); + } + + @Test + public void selectorNull() { + Observable.just(1, 2) + .materialize() + .dematerialize(new Function<Notification<Integer>, Notification<Object>>() { + @Override + public Notification<Object> apply(Notification<Integer> v) throws Exception { + return null; + } + }) + .test() + .assertFailure(NullPointerException.class); + } + + @Test + public void dematerialize1() { + Observable<Notification<Integer>> notifications = Observable.just(1, 2).materialize(); + Observable<Integer> dematerialize = notifications.dematerialize(Functions.<Notification<Integer>>identity()); + + Observer<Integer> observer = TestHelper.mockObserver(); + + dematerialize.subscribe(observer); + + verify(observer, times(1)).onNext(1); + verify(observer, times(1)).onNext(2); + verify(observer, times(1)).onComplete(); + verify(observer, never()).onError(any(Throwable.class)); + } + + @Test + public void dematerialize2() { + Throwable exception = new Throwable("test"); + Observable<Integer> o = Observable.error(exception); + Observable<Integer> dematerialize = o.materialize().dematerialize(Functions.<Notification<Integer>>identity()); + + Observer<Integer> observer = TestHelper.mockObserver(); + + dematerialize.subscribe(observer); + + verify(observer, times(1)).onError(exception); + verify(observer, times(0)).onComplete(); + verify(observer, times(0)).onNext(any(Integer.class)); + } + + @Test + public void dematerialize3() { + Exception exception = new Exception("test"); + Observable<Integer> o = Observable.error(exception); + Observable<Integer> dematerialize = o.materialize().dematerialize(Functions.<Notification<Integer>>identity()); + + Observer<Integer> observer = TestHelper.mockObserver(); + + dematerialize.subscribe(observer); + + verify(observer, times(1)).onError(exception); + verify(observer, times(0)).onComplete(); + verify(observer, times(0)).onNext(any(Integer.class)); + } + + @Test + public void errorPassThru() { + Exception exception = new Exception("test"); + Observable<Notification<Integer>> o = Observable.error(exception); + Observable<Integer> dematerialize = o.dematerialize(Functions.<Notification<Integer>>identity()); + + Observer<Integer> observer = TestHelper.mockObserver(); + + dematerialize.subscribe(observer); + + verify(observer, times(1)).onError(exception); + verify(observer, times(0)).onComplete(); + verify(observer, times(0)).onNext(any(Integer.class)); + } + + @Test + public void completePassThru() { + Observable<Notification<Integer>> o = Observable.empty(); + Observable<Integer> dematerialize = o.dematerialize(Functions.<Notification<Integer>>identity()); + + Observer<Integer> observer = TestHelper.mockObserver(); + + TestObserverEx<Integer> to = new TestObserverEx<>(observer); + dematerialize.subscribe(to); + + System.out.println(to.errors()); + + verify(observer, never()).onError(any(Throwable.class)); + verify(observer, times(1)).onComplete(); + verify(observer, times(0)).onNext(any(Integer.class)); + } + + @Test + public void honorsContractWhenCompleted() { + Observable<Integer> source = Observable.just(1); + + Observable<Integer> result = source.materialize().dematerialize(Functions.<Notification<Integer>>identity()); + + Observer<Integer> o = TestHelper.mockObserver(); + + result.subscribe(o); + + verify(o).onNext(1); + verify(o).onComplete(); + verify(o, never()).onError(any(Throwable.class)); + } + + @Test + public void honorsContractWhenThrows() { + Observable<Integer> source = Observable.error(new TestException()); + + Observable<Integer> result = source.materialize().dematerialize(Functions.<Notification<Integer>>identity()); + + Observer<Integer> o = TestHelper.mockObserver(); + + result.subscribe(o); + + verify(o, never()).onNext(any(Integer.class)); + verify(o, never()).onComplete(); + verify(o).onError(any(TestException.class)); + } + + @Test + public void dispose() { + TestHelper.checkDisposed(Observable.just(Notification.<Integer>createOnComplete()).dematerialize(Functions.<Notification<Integer>>identity())); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeObservable(new Function<Observable<Notification<Object>>, ObservableSource<Object>>() { + @Override + public ObservableSource<Object> apply(Observable<Notification<Object>> o) throws Exception { + return o.dematerialize(Functions.<Notification<Object>>identity()); + } + }); + } + + @Test + public void eventsAfterDematerializedTerminal() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + new Observable<Notification<Object>>() { + @Override + protected void subscribeActual(Observer<? super Notification<Object>> observer) { + observer.onSubscribe(Disposable.empty()); + observer.onNext(Notification.createOnComplete()); + observer.onNext(Notification.<Object>createOnNext(1)); + observer.onNext(Notification.createOnError(new TestException("First"))); + observer.onError(new TestException("Second")); + } + } + .dematerialize(Functions.<Notification<Object>>identity()) + .test() + .assertResult(); + + TestHelper.assertUndeliverable(errors, 0, TestException.class, "First"); + TestHelper.assertUndeliverable(errors, 1, TestException.class, "Second"); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + @SuppressWarnings("unchecked") + public void nonNotificationInstanceAfterDispose() { + new Observable<Object>() { + @Override + protected void subscribeActual(Observer<? super Object> observer) { + observer.onSubscribe(Disposable.empty()); + observer.onNext(Notification.createOnComplete()); + observer.onNext(1); + } + } + .dematerialize(v -> (Notification<Object>)v) + .test() + .assertResult(); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableDetachTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableDetachTest.java new file mode 100644 index 0000000000..5e4e3d5fce --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableDetachTest.java @@ -0,0 +1,118 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import java.lang.ref.WeakReference; + +import org.junit.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class ObservableDetachTest extends RxJavaTest { + + Object o; + + @Test + public void just() throws Exception { + o = new Object(); + + WeakReference<Object> wr = new WeakReference<>(o); + + TestObserver<Object> to = new TestObserver<>(); + + Observable.just(o).count().toObservable().onTerminateDetach().subscribe(to); + + to.assertValue(1L); + to.assertComplete(); + to.assertNoErrors(); + + o = null; + + System.gc(); + Thread.sleep(200); + + Assert.assertNull("Object retained!", wr.get()); + + } + + @Test + public void error() { + TestObserver<Object> to = new TestObserver<>(); + + Observable.error(new TestException()).onTerminateDetach().subscribe(to); + + to.assertNoValues(); + to.assertError(TestException.class); + to.assertNotComplete(); + } + + @Test + public void empty() { + TestObserver<Object> to = new TestObserver<>(); + + Observable.empty().onTerminateDetach().subscribe(to); + + to.assertNoValues(); + to.assertNoErrors(); + to.assertComplete(); + } + + @Test + public void range() { + TestObserver<Object> to = new TestObserver<>(); + + Observable.range(1, 1000).onTerminateDetach().subscribe(to); + + to.assertValueCount(1000); + to.assertNoErrors(); + to.assertComplete(); + } + + @Test + public void justUnsubscribed() throws Exception { + o = new Object(); + + WeakReference<Object> wr = new WeakReference<>(o); + + TestObserver<Long> to = Observable.just(o).count().toObservable().onTerminateDetach().test(); + + o = null; + to.dispose(); + + System.gc(); + Thread.sleep(200); + + Assert.assertNull("Object retained!", wr.get()); + + } + + @Test + public void dispose() { + TestHelper.checkDisposed(Observable.never().onTerminateDetach()); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeObservable(new Function<Observable<Object>, ObservableSource<Object>>() { + @Override + public ObservableSource<Object> apply(Observable<Object> o) throws Exception { + return o.onTerminateDetach(); + } + }); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableDistinctTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableDistinctTest.java new file mode 100644 index 0000000000..178f700db5 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableDistinctTest.java @@ -0,0 +1,227 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +import java.util.*; + +import org.junit.*; +import org.mockito.InOrder; + +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.core.Observer; +import io.reactivex.rxjava3.core.RxJavaTest; +import io.reactivex.rxjava3.disposables.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.internal.util.ExceptionHelper; +import io.reactivex.rxjava3.operators.QueueDisposable; +import io.reactivex.rxjava3.operators.QueueFuseable; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.subjects.UnicastSubject; +import io.reactivex.rxjava3.testsupport.*; + +public class ObservableDistinctTest extends RxJavaTest { + + Observer<String> w; + + // nulls lead to exceptions + final Function<String, String> TO_UPPER_WITH_EXCEPTION = new Function<String, String>() { + @Override + public String apply(String s) { + if (s.equals("x")) { + return "XX"; + } + return s.toUpperCase(); + } + }; + + @Before + public void before() { + w = TestHelper.mockObserver(); + } + + @Test + public void distinctOfNone() { + Observable<String> src = Observable.empty(); + src.distinct().subscribe(w); + + verify(w, never()).onNext(anyString()); + verify(w, never()).onError(any(Throwable.class)); + verify(w, times(1)).onComplete(); + } + + @Test + public void distinctOfNoneWithKeySelector() { + Observable<String> src = Observable.empty(); + src.distinct(TO_UPPER_WITH_EXCEPTION).subscribe(w); + + verify(w, never()).onNext(anyString()); + verify(w, never()).onError(any(Throwable.class)); + verify(w, times(1)).onComplete(); + } + + @Test + public void distinctOfNormalSource() { + Observable<String> src = Observable.just("a", "b", "c", "c", "c", "b", "b", "a", "e"); + src.distinct().subscribe(w); + + InOrder inOrder = inOrder(w); + inOrder.verify(w, times(1)).onNext("a"); + inOrder.verify(w, times(1)).onNext("b"); + inOrder.verify(w, times(1)).onNext("c"); + inOrder.verify(w, times(1)).onNext("e"); + inOrder.verify(w, times(1)).onComplete(); + inOrder.verify(w, never()).onNext(anyString()); + verify(w, never()).onError(any(Throwable.class)); + } + + @Test + public void distinctOfNormalSourceWithKeySelector() { + Observable<String> src = Observable.just("a", "B", "c", "C", "c", "B", "b", "a", "E"); + src.distinct(TO_UPPER_WITH_EXCEPTION).subscribe(w); + + InOrder inOrder = inOrder(w); + inOrder.verify(w, times(1)).onNext("a"); + inOrder.verify(w, times(1)).onNext("B"); + inOrder.verify(w, times(1)).onNext("c"); + inOrder.verify(w, times(1)).onNext("E"); + inOrder.verify(w, times(1)).onComplete(); + inOrder.verify(w, never()).onNext(anyString()); + verify(w, never()).onError(any(Throwable.class)); + } + + @Test + public void error() { + Observable.error(new TestException()) + .distinct() + .test() + .assertFailure(TestException.class); + } + + @Test + public void fusedSync() { + TestObserverEx<Integer> to = new TestObserverEx<>(QueueFuseable.ANY); + + Observable.just(1, 1, 2, 1, 3, 2, 4, 5, 4) + .distinct() + .subscribe(to); + + to.assertFusionMode(QueueFuseable.SYNC) + .assertResult(1, 2, 3, 4, 5); + } + + @Test + public void fusedAsync() { + TestObserverEx<Integer> to = new TestObserverEx<>(QueueFuseable.ANY); + + UnicastSubject<Integer> us = UnicastSubject.create(); + + us + .distinct() + .subscribe(to); + + TestHelper.emit(us, 1, 1, 2, 1, 3, 2, 4, 5, 4); + + to.assertFusionMode(QueueFuseable.ASYNC) + .assertResult(1, 2, 3, 4, 5); + } + + @Test + public void fusedClear() { + Observable.just(1, 1, 2, 1, 3, 2, 4, 5, 4) + .distinct() + .subscribe(new Observer<Integer>() { + @Override + public void onSubscribe(Disposable d) { + QueueDisposable<?> qd = (QueueDisposable<?>)d; + + assertFalse(qd.isEmpty()); + + qd.clear(); + + assertTrue(qd.isEmpty()); + } + + @Override + public void onNext(Integer value) { + } + + @Override + public void onError(Throwable e) { + } + + @Override + public void onComplete() { + } + }); + } + + @Test + public void collectionSupplierThrows() { + Observable.just(1) + .distinct(Functions.identity(), new Supplier<Collection<Object>>() { + @Override + public Collection<Object> get() throws Exception { + throw new TestException(); + } + }) + .test() + .assertFailure(TestException.class); + } + + @Test + public void collectionSupplierIsNull() { + Observable.just(1) + .distinct(Functions.identity(), new Supplier<Collection<Object>>() { + @Override + public Collection<Object> get() throws Exception { + return null; + } + }) + .to(TestHelper.<Integer>testConsumer()) + .assertFailure(NullPointerException.class) + .assertErrorMessage(ExceptionHelper.nullWarning("The collectionSupplier returned a null Collection.")); + } + + @Test + public void badSource() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + new Observable<Integer>() { + @Override + protected void subscribeActual(Observer<? super Integer> observer) { + observer.onSubscribe(Disposable.empty()); + + observer.onNext(1); + observer.onComplete(); + observer.onNext(2); + observer.onError(new TestException()); + observer.onComplete(); + } + } + .distinct() + .test() + .assertResult(1); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableDistinctUntilChangedTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableDistinctUntilChangedTest.java new file mode 100644 index 0000000000..7f58ab2f89 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableDistinctUntilChangedTest.java @@ -0,0 +1,247 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +import java.io.IOException; +import java.util.List; + +import io.reactivex.rxjava3.disposables.Disposable; +import org.junit.*; +import org.mockito.InOrder; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.operators.QueueFuseable; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.subjects.*; +import io.reactivex.rxjava3.testsupport.*; + +public class ObservableDistinctUntilChangedTest extends RxJavaTest { + + Observer<String> w; + Observer<String> w2; + + // nulls lead to exceptions + final Function<String, String> TO_UPPER_WITH_EXCEPTION = new Function<String, String>() { + @Override + public String apply(String s) { + if (s.equals("x")) { + return "xx"; + } + return s.toUpperCase(); + } + }; + + @Before + public void before() { + w = TestHelper.mockObserver(); + w2 = TestHelper.mockObserver(); + } + + @Test + public void distinctUntilChangedOfNone() { + Observable<String> src = Observable.empty(); + src.distinctUntilChanged().subscribe(w); + + verify(w, never()).onNext(anyString()); + verify(w, never()).onError(any(Throwable.class)); + verify(w, times(1)).onComplete(); + } + + @Test + public void distinctUntilChangedOfNoneWithKeySelector() { + Observable<String> src = Observable.empty(); + src.distinctUntilChanged(TO_UPPER_WITH_EXCEPTION).subscribe(w); + + verify(w, never()).onNext(anyString()); + verify(w, never()).onError(any(Throwable.class)); + verify(w, times(1)).onComplete(); + } + + @Test + public void distinctUntilChangedOfNormalSource() { + Observable<String> src = Observable.just("a", "b", "c", "c", "c", "b", "b", "a", "e"); + src.distinctUntilChanged().subscribe(w); + + InOrder inOrder = inOrder(w); + inOrder.verify(w, times(1)).onNext("a"); + inOrder.verify(w, times(1)).onNext("b"); + inOrder.verify(w, times(1)).onNext("c"); + inOrder.verify(w, times(1)).onNext("b"); + inOrder.verify(w, times(1)).onNext("a"); + inOrder.verify(w, times(1)).onNext("e"); + inOrder.verify(w, times(1)).onComplete(); + inOrder.verify(w, never()).onNext(anyString()); + verify(w, never()).onError(any(Throwable.class)); + } + + @Test + public void distinctUntilChangedOfNormalSourceWithKeySelector() { + Observable<String> src = Observable.just("a", "b", "c", "C", "c", "B", "b", "a", "e"); + src.distinctUntilChanged(TO_UPPER_WITH_EXCEPTION).subscribe(w); + + InOrder inOrder = inOrder(w); + inOrder.verify(w, times(1)).onNext("a"); + inOrder.verify(w, times(1)).onNext("b"); + inOrder.verify(w, times(1)).onNext("c"); + inOrder.verify(w, times(1)).onNext("B"); + inOrder.verify(w, times(1)).onNext("a"); + inOrder.verify(w, times(1)).onNext("e"); + inOrder.verify(w, times(1)).onComplete(); + inOrder.verify(w, never()).onNext(anyString()); + verify(w, never()).onError(any(Throwable.class)); + } + + @Test + public void customComparator() { + Observable<String> source = Observable.just("a", "b", "B", "A", "a", "C"); + + TestObserver<String> to = TestObserver.create(); + + source.distinctUntilChanged(new BiPredicate<String, String>() { + @Override + public boolean test(String a, String b) { + return a.compareToIgnoreCase(b) == 0; + } + }) + .subscribe(to); + + to.assertValues("a", "b", "A", "C"); + to.assertNoErrors(); + to.assertComplete(); + } + + @Test + public void customComparatorThrows() { + Observable<String> source = Observable.just("a", "b", "B", "A", "a", "C"); + + TestObserver<String> to = TestObserver.create(); + + source.distinctUntilChanged(new BiPredicate<String, String>() { + @Override + public boolean test(String a, String b) { + throw new TestException(); + } + }) + .subscribe(to); + + to.assertValue("a"); + to.assertNotComplete(); + to.assertError(TestException.class); + } + + @Test + public void fused() { + TestObserverEx<Integer> to = new TestObserverEx<>(QueueFuseable.ANY); + + Observable.just(1, 2, 2, 3, 3, 4, 5) + .distinctUntilChanged(new BiPredicate<Integer, Integer>() { + @Override + public boolean test(Integer a, Integer b) throws Exception { + return a.equals(b); + } + }) + .subscribe(to); + + to.assertFuseable() + .assertFusionMode(QueueFuseable.SYNC) + .assertResult(1, 2, 3, 4, 5) + ; + } + + @Test + public void fusedAsync() { + TestObserverEx<Integer> to = new TestObserverEx<>(QueueFuseable.ANY); + + UnicastSubject<Integer> us = UnicastSubject.create(); + + us + .distinctUntilChanged(new BiPredicate<Integer, Integer>() { + @Override + public boolean test(Integer a, Integer b) throws Exception { + return a.equals(b); + } + }) + .subscribe(to); + + TestHelper.emit(us, 1, 2, 2, 3, 3, 4, 5); + + to.assertFuseable() + .assertFusionMode(QueueFuseable.ASYNC) + .assertResult(1, 2, 3, 4, 5) + ; + } + + @Test + public void ignoreCancel() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + + try { + Observable.wrap(new ObservableSource<Integer>() { + @Override + public void subscribe(Observer<? super Integer> observer) { + observer.onSubscribe(Disposable.empty()); + observer.onNext(1); + observer.onNext(2); + observer.onNext(3); + observer.onError(new IOException()); + observer.onComplete(); + } + }) + .distinctUntilChanged(new BiPredicate<Integer, Integer>() { + @Override + public boolean test(Integer a, Integer b) throws Exception { + throw new TestException(); + } + }) + .test() + .assertFailure(TestException.class, 1); + + TestHelper.assertUndeliverable(errors, 0, IOException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + class Mutable { + int value; + } + + @Test + public void mutableWithSelector() { + Mutable m = new Mutable(); + + PublishSubject<Mutable> ps = PublishSubject.create(); + + TestObserver<Mutable> to = ps.distinctUntilChanged(new Function<Mutable, Object>() { + @Override + public Object apply(Mutable m) throws Exception { + return m.value; + } + }) + .test(); + + ps.onNext(m); + m.value = 1; + ps.onNext(m); + ps.onComplete(); + + to.assertResult(m, m); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableDoAfterNextTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableDoAfterNextTest.java new file mode 100644 index 0000000000..8338d2c9ec --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableDoAfterNextTest.java @@ -0,0 +1,281 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import static org.junit.Assert.*; + +import java.util.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.core.RxJavaTest; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.Consumer; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.operators.QueueFuseable; +import io.reactivex.rxjava3.subjects.UnicastSubject; +import io.reactivex.rxjava3.testsupport.*; + +public class ObservableDoAfterNextTest extends RxJavaTest { + + final List<Integer> values = new ArrayList<>(); + + final Consumer<Integer> afterNext = new Consumer<Integer>() { + @Override + public void accept(Integer e) throws Exception { + values.add(-e); + } + }; + + final TestObserver<Integer> to = new TestObserver<Integer>() { + @Override + public void onNext(Integer t) { + super.onNext(t); + ObservableDoAfterNextTest.this.values.add(t); + } + }; + + @Test + public void just() { + Observable.just(1) + .doAfterNext(afterNext) + .subscribeWith(to) + .assertResult(1); + + assertEquals(Arrays.asList(1, -1), values); + } + + @Test + public void justHidden() { + Observable.just(1) + .hide() + .doAfterNext(afterNext) + .subscribeWith(to) + .assertResult(1); + + assertEquals(Arrays.asList(1, -1), values); + } + + @Test + public void range() { + Observable.range(1, 5) + .doAfterNext(afterNext) + .subscribeWith(to) + .assertResult(1, 2, 3, 4, 5); + + assertEquals(Arrays.asList(1, -1, 2, -2, 3, -3, 4, -4, 5, -5), values); + } + + @Test + public void error() { + Observable.<Integer>error(new TestException()) + .doAfterNext(afterNext) + .subscribeWith(to) + .assertFailure(TestException.class); + + assertTrue(values.isEmpty()); + } + + @Test + public void empty() { + Observable.<Integer>empty() + .doAfterNext(afterNext) + .subscribeWith(to) + .assertResult(); + + assertTrue(values.isEmpty()); + } + + @Test + public void syncFused() { + TestObserverEx<Integer> to0 = new TestObserverEx<>(QueueFuseable.SYNC); + + Observable.range(1, 5) + .doAfterNext(afterNext) + .subscribe(to0); + + to0.assertFusionMode(QueueFuseable.SYNC) + .assertResult(1, 2, 3, 4, 5); + + assertEquals(Arrays.asList(-1, -2, -3, -4, -5), values); + } + + @Test + public void asyncFusedRejected() { + TestObserverEx<Integer> to0 = new TestObserverEx<>(QueueFuseable.ASYNC); + + Observable.range(1, 5) + .doAfterNext(afterNext) + .subscribe(to0); + + to0.assertFusionMode(QueueFuseable.NONE) + .assertResult(1, 2, 3, 4, 5); + + assertEquals(Arrays.asList(-1, -2, -3, -4, -5), values); + } + + @Test + public void asyncFused() { + TestObserverEx<Integer> to0 = new TestObserverEx<>(QueueFuseable.ASYNC); + + UnicastSubject<Integer> us = UnicastSubject.create(); + + TestHelper.emit(us, 1, 2, 3, 4, 5); + + us + .doAfterNext(afterNext) + .subscribe(to0); + + to0.assertFusionMode(QueueFuseable.ASYNC) + .assertResult(1, 2, 3, 4, 5); + + assertEquals(Arrays.asList(-1, -2, -3, -4, -5), values); + } + + @Test + public void justConditional() { + Observable.just(1) + .doAfterNext(afterNext) + .filter(Functions.alwaysTrue()) + .subscribeWith(to) + .assertResult(1); + + assertEquals(Arrays.asList(1, -1), values); + } + + @Test + public void rangeConditional() { + Observable.range(1, 5) + .doAfterNext(afterNext) + .filter(Functions.alwaysTrue()) + .subscribeWith(to) + .assertResult(1, 2, 3, 4, 5); + + assertEquals(Arrays.asList(1, -1, 2, -2, 3, -3, 4, -4, 5, -5), values); + } + + @Test + public void errorConditional() { + Observable.<Integer>error(new TestException()) + .doAfterNext(afterNext) + .filter(Functions.alwaysTrue()) + .subscribeWith(to) + .assertFailure(TestException.class); + + assertTrue(values.isEmpty()); + } + + @Test + public void emptyConditional() { + Observable.<Integer>empty() + .doAfterNext(afterNext) + .filter(Functions.alwaysTrue()) + .subscribeWith(to) + .assertResult(); + + assertTrue(values.isEmpty()); + } + + @Test + public void syncFusedConditional() { + TestObserverEx<Integer> to0 = new TestObserverEx<>(QueueFuseable.SYNC); + + Observable.range(1, 5) + .doAfterNext(afterNext) + .filter(Functions.alwaysTrue()) + .subscribe(to0); + + to0.assertFusionMode(QueueFuseable.SYNC) + .assertResult(1, 2, 3, 4, 5); + + assertEquals(Arrays.asList(-1, -2, -3, -4, -5), values); + } + + @Test + public void asyncFusedRejectedConditional() { + TestObserverEx<Integer> to0 = new TestObserverEx<>(QueueFuseable.ASYNC); + + Observable.range(1, 5) + .doAfterNext(afterNext) + .filter(Functions.alwaysTrue()) + .subscribe(to0); + + to0.assertFusionMode(QueueFuseable.NONE) + .assertResult(1, 2, 3, 4, 5); + + assertEquals(Arrays.asList(-1, -2, -3, -4, -5), values); + } + + @Test + public void asyncFusedConditional() { + TestObserverEx<Integer> to0 = new TestObserverEx<>(QueueFuseable.ASYNC); + + UnicastSubject<Integer> us = UnicastSubject.create(); + + TestHelper.emit(us, 1, 2, 3, 4, 5); + + us + .doAfterNext(afterNext) + .filter(Functions.alwaysTrue()) + .subscribe(to0); + + to0.assertFusionMode(QueueFuseable.ASYNC) + .assertResult(1, 2, 3, 4, 5); + + assertEquals(Arrays.asList(-1, -2, -3, -4, -5), values); + } + + @Test + public void consumerThrows() { + Observable.just(1, 2) + .doAfterNext(new Consumer<Integer>() { + @Override + public void accept(Integer e) throws Exception { + throw new TestException(); + } + }) + .test() + .assertFailure(TestException.class, 1); + } + + @Test + public void consumerThrowsConditional() { + Observable.just(1, 2) + .doAfterNext(new Consumer<Integer>() { + @Override + public void accept(Integer e) throws Exception { + throw new TestException(); + } + }) + .filter(Functions.alwaysTrue()) + .test() + .assertFailure(TestException.class, 1); + } + + @Test + public void consumerThrowsConditional2() { + Observable.just(1, 2).hide() + .doAfterNext(new Consumer<Integer>() { + @Override + public void accept(Integer e) throws Exception { + throw new TestException(); + } + }) + .filter(Functions.alwaysTrue()) + .test() + .assertFailure(TestException.class, 1); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableDoFinallyTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableDoFinallyTest.java new file mode 100644 index 0000000000..3753b41127 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableDoFinallyTest.java @@ -0,0 +1,535 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import static org.junit.Assert.*; + +import java.util.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.core.Observer; +import io.reactivex.rxjava3.core.RxJavaTest; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.operators.QueueDisposable; +import io.reactivex.rxjava3.operators.QueueFuseable; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.subjects.UnicastSubject; +import io.reactivex.rxjava3.testsupport.*; + +public class ObservableDoFinallyTest extends RxJavaTest implements Action { + + int calls; + + @Override + public void run() throws Exception { + calls++; + } + + @Test + public void normalJust() { + Observable.just(1) + .doFinally(this) + .test() + .assertResult(1); + + assertEquals(1, calls); + } + + @Test + public void normalEmpty() { + Observable.empty() + .doFinally(this) + .test() + .assertResult(); + + assertEquals(1, calls); + } + + @Test + public void normalError() { + Observable.error(new TestException()) + .doFinally(this) + .test() + .assertFailure(TestException.class); + + assertEquals(1, calls); + } + + @Test + public void normalTake() { + Observable.range(1, 10) + .doFinally(this) + .take(5) + .test() + .assertResult(1, 2, 3, 4, 5); + + assertEquals(1, calls); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeObservable(new Function<Observable<Object>, Observable<Object>>() { + @Override + public Observable<Object> apply(Observable<Object> f) throws Exception { + return f.doFinally(ObservableDoFinallyTest.this); + } + }); + TestHelper.checkDoubleOnSubscribeObservable(new Function<Observable<Object>, Observable<Object>>() { + @Override + public Observable<Object> apply(Observable<Object> f) throws Exception { + return f.doFinally(ObservableDoFinallyTest.this).filter(Functions.alwaysTrue()); + } + }); + } + + @Test + public void syncFused() { + TestObserverEx<Integer> to = new TestObserverEx<>(QueueFuseable.SYNC); + + Observable.range(1, 5) + .doFinally(this) + .subscribe(to); + + to.assertFusionMode(QueueFuseable.SYNC) + .assertResult(1, 2, 3, 4, 5); + + assertEquals(1, calls); + } + + @Test + public void syncFusedBoundary() { + TestObserverEx<Integer> to = new TestObserverEx<>(QueueFuseable.SYNC | QueueFuseable.BOUNDARY); + + Observable.range(1, 5) + .doFinally(this) + .subscribe(to); + + to.assertFusionMode(QueueFuseable.NONE) + .assertResult(1, 2, 3, 4, 5); + + assertEquals(1, calls); + } + + @Test + public void asyncFused() { + TestObserverEx<Integer> to = new TestObserverEx<>(QueueFuseable.ASYNC); + + UnicastSubject<Integer> us = UnicastSubject.create(); + TestHelper.emit(us, 1, 2, 3, 4, 5); + + us + .doFinally(this) + .subscribe(to); + + to.assertFusionMode(QueueFuseable.ASYNC) + .assertResult(1, 2, 3, 4, 5); + + assertEquals(1, calls); + } + + @Test + public void asyncFusedBoundary() { + TestObserverEx<Integer> to = new TestObserverEx<>(QueueFuseable.ASYNC | QueueFuseable.BOUNDARY); + + UnicastSubject<Integer> us = UnicastSubject.create(); + TestHelper.emit(us, 1, 2, 3, 4, 5); + + us + .doFinally(this) + .subscribe(to); + + to.assertFusionMode(QueueFuseable.NONE) + .assertResult(1, 2, 3, 4, 5); + + assertEquals(1, calls); + } + + @Test + public void normalJustConditional() { + Observable.just(1) + .doFinally(this) + .filter(Functions.alwaysTrue()) + .test() + .assertResult(1); + + assertEquals(1, calls); + } + + @Test + public void normalEmptyConditional() { + Observable.empty() + .doFinally(this) + .filter(Functions.alwaysTrue()) + .test() + .assertResult(); + + assertEquals(1, calls); + } + + @Test + public void normalErrorConditional() { + Observable.error(new TestException()) + .doFinally(this) + .filter(Functions.alwaysTrue()) + .test() + .assertFailure(TestException.class); + + assertEquals(1, calls); + } + + @Test + public void normalTakeConditional() { + Observable.range(1, 10) + .doFinally(this) + .filter(Functions.alwaysTrue()) + .take(5) + .test() + .assertResult(1, 2, 3, 4, 5); + + assertEquals(1, calls); + } + + @Test + public void syncFusedConditional() { + TestObserverEx<Integer> to = new TestObserverEx<>(QueueFuseable.SYNC); + + Observable.range(1, 5) + .doFinally(this) + .filter(Functions.alwaysTrue()) + .subscribe(to); + + to.assertFusionMode(QueueFuseable.SYNC) + .assertResult(1, 2, 3, 4, 5); + + assertEquals(1, calls); + } + + @Test + public void nonFused() { + TestObserverEx<Integer> to = new TestObserverEx<>(QueueFuseable.SYNC); + + Observable.range(1, 5).hide() + .doFinally(this) + .subscribe(to); + + to.assertFusionMode(QueueFuseable.NONE) + .assertResult(1, 2, 3, 4, 5); + + assertEquals(1, calls); + } + + @Test + public void nonFusedConditional() { + TestObserverEx<Integer> to = new TestObserverEx<>(QueueFuseable.SYNC); + + Observable.range(1, 5).hide() + .doFinally(this) + .filter(Functions.alwaysTrue()) + .subscribe(to); + + to.assertFusionMode(QueueFuseable.NONE) + .assertResult(1, 2, 3, 4, 5); + + assertEquals(1, calls); + } + + @Test + public void syncFusedBoundaryConditional() { + TestObserverEx<Integer> to = new TestObserverEx<>(QueueFuseable.SYNC | QueueFuseable.BOUNDARY); + + Observable.range(1, 5) + .doFinally(this) + .filter(Functions.alwaysTrue()) + .subscribe(to); + + to.assertFusionMode(QueueFuseable.NONE) + .assertResult(1, 2, 3, 4, 5); + + assertEquals(1, calls); + } + + @Test + public void asyncFusedConditional() { + TestObserverEx<Integer> to = new TestObserverEx<>(QueueFuseable.ASYNC); + + UnicastSubject<Integer> us = UnicastSubject.create(); + TestHelper.emit(us, 1, 2, 3, 4, 5); + + us + .doFinally(this) + .filter(Functions.alwaysTrue()) + .subscribe(to); + + to.assertFusionMode(QueueFuseable.ASYNC) + .assertResult(1, 2, 3, 4, 5); + + assertEquals(1, calls); + } + + @Test + public void asyncFusedBoundaryConditional() { + TestObserverEx<Integer> to = new TestObserverEx<>(QueueFuseable.ASYNC | QueueFuseable.BOUNDARY); + + UnicastSubject<Integer> us = UnicastSubject.create(); + TestHelper.emit(us, 1, 2, 3, 4, 5); + + us + .doFinally(this) + .filter(Functions.alwaysTrue()) + .subscribe(to); + + to.assertFusionMode(QueueFuseable.NONE) + .assertResult(1, 2, 3, 4, 5); + + assertEquals(1, calls); + } + + @Test + public void actionThrows() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + Observable.just(1) + .doFinally(new Action() { + @Override + public void run() throws Exception { + throw new TestException(); + } + }) + .test() + .assertResult(1) + .dispose(); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void actionThrowsConditional() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + Observable.just(1) + .doFinally(new Action() { + @Override + public void run() throws Exception { + throw new TestException(); + } + }) + .filter(Functions.alwaysTrue()) + .test() + .assertResult(1) + .dispose(); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void clearIsEmpty() { + Observable.range(1, 5) + .doFinally(this) + .subscribe(new Observer<Integer>() { + + @Override + public void onSubscribe(Disposable d) { + @SuppressWarnings("unchecked") + QueueDisposable<Integer> qd = (QueueDisposable<Integer>)d; + + qd.requestFusion(QueueFuseable.ANY); + + assertFalse(qd.isEmpty()); + + try { + assertEquals(1, qd.poll().intValue()); + } catch (Throwable ex) { + throw new RuntimeException(ex); + } + + assertFalse(qd.isEmpty()); + + qd.clear(); + + assertTrue(qd.isEmpty()); + + qd.dispose(); + } + + @Override + public void onNext(Integer t) { + } + + @Override + public void onError(Throwable t) { + } + + @Override + public void onComplete() { + } + }); + + assertEquals(1, calls); + } + + @Test + public void clearIsEmptyConditional() { + Observable.range(1, 5) + .doFinally(this) + .filter(Functions.alwaysTrue()) + .subscribe(new Observer<Integer>() { + + @Override + public void onSubscribe(Disposable d) { + @SuppressWarnings("unchecked") + QueueDisposable<Integer> qd = (QueueDisposable<Integer>)d; + + qd.requestFusion(QueueFuseable.ANY); + + assertFalse(qd.isEmpty()); + + assertFalse(qd.isDisposed()); + + try { + assertEquals(1, qd.poll().intValue()); + } catch (Throwable ex) { + throw new RuntimeException(ex); + } + + assertFalse(qd.isEmpty()); + + qd.clear(); + + assertTrue(qd.isEmpty()); + + qd.dispose(); + + assertTrue(qd.isDisposed()); + } + + @Override + public void onNext(Integer t) { + } + + @Override + public void onError(Throwable t) { + } + + @Override + public void onComplete() { + } + }); + + assertEquals(1, calls); + } + + @Test + public void eventOrdering() { + final List<String> list = new ArrayList<>(); + + Observable.error(new TestException()) + .doOnDispose(new Action() { + @Override + public void run() throws Exception { + list.add("dispose"); + } + }) + .doFinally(new Action() { + @Override + public void run() throws Exception { + list.add("finally"); + } + }) + .subscribe( + new Consumer<Object>() { + @Override + public void accept(Object v) throws Exception { + list.add("onNext"); + } + }, + new Consumer<Throwable>() { + @Override + public void accept(Throwable e) throws Exception { + list.add("onError"); + } + }, + new Action() { + @Override + public void run() throws Exception { + list.add("onComplete"); + } + }); + + assertEquals(Arrays.asList("onError", "finally"), list); + } + + @Test + public void eventOrdering2() { + final List<String> list = new ArrayList<>(); + + Observable.just(1) + .doOnDispose(new Action() { + @Override + public void run() throws Exception { + list.add("dispose"); + } + }) + .doFinally(new Action() { + @Override + public void run() throws Exception { + list.add("finally"); + } + }) + .subscribe( + new Consumer<Object>() { + @Override + public void accept(Object v) throws Exception { + list.add("onNext"); + } + }, + new Consumer<Throwable>() { + @Override + public void accept(Throwable e) throws Exception { + list.add("onError"); + } + }, + new Action() { + @Override + public void run() throws Exception { + list.add("onComplete"); + } + }); + + assertEquals(Arrays.asList("onNext", "onComplete", "finally"), list); + } + + @Test + public void fusionRejected() { + TestObserverEx<Object> to = new TestObserverEx<>(); + to.setInitialFusionMode(QueueFuseable.ANY); + + TestHelper.rejectObservableFusion() + .doFinally(() -> { }) + .subscribeWith(to); + + to.assertFuseable() + .assertFusionMode(QueueFuseable.NONE); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableDoOnEachTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableDoOnEachTest.java new file mode 100644 index 0000000000..1f71d2b0a3 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableDoOnEachTest.java @@ -0,0 +1,710 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import java.io.IOException; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + +import io.reactivex.rxjava3.disposables.Disposable; +import org.junit.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.operators.QueueFuseable; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.subjects.UnicastSubject; +import io.reactivex.rxjava3.testsupport.*; + +public class ObservableDoOnEachTest extends RxJavaTest { + + Observer<String> subscribedObserver; + Observer<String> sideEffectObserver; + + @Before + public void before() { + subscribedObserver = TestHelper.mockObserver(); + sideEffectObserver = TestHelper.mockObserver(); + } + + @Test + public void doOnEach() { + Observable<String> base = Observable.just("a", "b", "c"); + Observable<String> doOnEach = base.doOnEach(sideEffectObserver); + + doOnEach.subscribe(subscribedObserver); + + // ensure the leaf Observer is still getting called + verify(subscribedObserver, never()).onError(any(Throwable.class)); + verify(subscribedObserver, times(1)).onNext("a"); + verify(subscribedObserver, times(1)).onNext("b"); + verify(subscribedObserver, times(1)).onNext("c"); + verify(subscribedObserver, times(1)).onComplete(); + + // ensure our injected Observer is getting called + verify(sideEffectObserver, never()).onError(any(Throwable.class)); + verify(sideEffectObserver, times(1)).onNext("a"); + verify(sideEffectObserver, times(1)).onNext("b"); + verify(sideEffectObserver, times(1)).onNext("c"); + verify(sideEffectObserver, times(1)).onComplete(); + } + + @Test + public void doOnEachWithError() { + Observable<String> base = Observable.just("one", "fail", "two", "three", "fail"); + Observable<String> errs = base.map(new Function<String, String>() { + @Override + public String apply(String s) { + if ("fail".equals(s)) { + throw new RuntimeException("Forced Failure"); + } + return s; + } + }); + + Observable<String> doOnEach = errs.doOnEach(sideEffectObserver); + + doOnEach.subscribe(subscribedObserver); + verify(subscribedObserver, times(1)).onNext("one"); + verify(subscribedObserver, never()).onNext("two"); + verify(subscribedObserver, never()).onNext("three"); + verify(subscribedObserver, never()).onComplete(); + verify(subscribedObserver, times(1)).onError(any(Throwable.class)); + + verify(sideEffectObserver, times(1)).onNext("one"); + verify(sideEffectObserver, never()).onNext("two"); + verify(sideEffectObserver, never()).onNext("three"); + verify(sideEffectObserver, never()).onComplete(); + verify(sideEffectObserver, times(1)).onError(any(Throwable.class)); + } + + @Test + public void doOnEachWithErrorInCallback() { + Observable<String> base = Observable.just("one", "two", "fail", "three"); + Observable<String> doOnEach = base.doOnNext(new Consumer<String>() { + @Override + public void accept(String s) { + if ("fail".equals(s)) { + throw new RuntimeException("Forced Failure"); + } + } + }); + + doOnEach.subscribe(subscribedObserver); + verify(subscribedObserver, times(1)).onNext("one"); + verify(subscribedObserver, times(1)).onNext("two"); + verify(subscribedObserver, never()).onNext("three"); + verify(subscribedObserver, never()).onComplete(); + verify(subscribedObserver, times(1)).onError(any(Throwable.class)); + + } + + @Test + public void issue1451Case1() { + // https://github.com/Netflix/RxJava/issues/1451 + final int expectedCount = 3; + final AtomicInteger count = new AtomicInteger(); + for (int i = 0; i < expectedCount; i++) { + Observable + .just(Boolean.TRUE, Boolean.FALSE) + .takeWhile(new Predicate<Boolean>() { + @Override + public boolean test(Boolean value) { + return value; + } + }) + .toList() + .doOnSuccess(new Consumer<List<Boolean>>() { + @Override + public void accept(List<Boolean> booleans) { + count.incrementAndGet(); + } + }) + .subscribe(); + } + assertEquals(expectedCount, count.get()); + } + + @Test + public void issue1451Case2() { + // https://github.com/Netflix/RxJava/issues/1451 + final int expectedCount = 3; + final AtomicInteger count = new AtomicInteger(); + for (int i = 0; i < expectedCount; i++) { + Observable + .just(Boolean.TRUE, Boolean.FALSE, Boolean.FALSE) + .takeWhile(new Predicate<Boolean>() { + @Override + public boolean test(Boolean value) { + return value; + } + }) + .toList() + .doOnSuccess(new Consumer<List<Boolean>>() { + @Override + public void accept(List<Boolean> booleans) { + count.incrementAndGet(); + } + }) + .subscribe(); + } + assertEquals(expectedCount, count.get()); + } + + // FIXME crashing ObservableSource can't propagate to an Observer +// @Test +// public void testFatalError() { +// try { +// Observable.just(1, 2, 3) +// .flatMap(new Function<Integer, Observable<?>>() { +// @Override +// public Observable<?> apply(Integer integer) { +// return Observable.create(new ObservableSource<Object>() { +// @Override +// public void accept(Observer<Object> o) { +// throw new NullPointerException("Test NPE"); +// } +// }); +// } +// }) +// .doOnNext(new Consumer<Object>() { +// @Override +// public void accept(Object o) { +// System.out.println("Won't come here"); +// } +// }) +// .subscribe(); +// fail("should have thrown an exception"); +// } catch (OnErrorNotImplementedException e) { +// assertTrue(e.getCause() instanceof NullPointerException); +// assertEquals(e.getCause().getMessage(), "Test NPE"); +// System.out.println("Received exception: " + e); +// } +// } + + @Test + public void onErrorThrows() { + TestObserverEx<Object> to = new TestObserverEx<>(); + + Observable.error(new TestException()) + .doOnError(new Consumer<Throwable>() { + @Override + public void accept(Throwable e) { + throw new TestException(); + } + }).subscribe(to); + + to.assertNoValues(); + to.assertNotComplete(); + to.assertError(CompositeException.class); + + CompositeException ex = (CompositeException)to.errors().get(0); + + List<Throwable> exceptions = ex.getExceptions(); + assertEquals(2, exceptions.size()); + Assert.assertTrue(exceptions.get(0) instanceof TestException); + Assert.assertTrue(exceptions.get(1) instanceof TestException); + } + + @Test + public void ignoreCancel() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + + try { + Observable.wrap(new ObservableSource<Object>() { + @Override + public void subscribe(Observer<? super Object> observer) { + observer.onSubscribe(Disposable.empty()); + observer.onNext(1); + observer.onNext(2); + observer.onError(new IOException()); + observer.onComplete(); + } + }) + .doOnNext(new Consumer<Object>() { + @Override + public void accept(Object e) throws Exception { + throw new TestException(); + } + }) + .test() + .assertFailure(TestException.class); + + TestHelper.assertUndeliverable(errors, 0, IOException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void onErrorAfterCrash() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + + try { + Observable.wrap(new ObservableSource<Object>() { + @Override + public void subscribe(Observer<? super Object> observer) { + observer.onSubscribe(Disposable.empty()); + observer.onError(new TestException()); + } + }) + .doAfterTerminate(new Action() { + @Override + public void run() throws Exception { + throw new IOException(); + } + }) + .test() + .assertFailure(TestException.class); + + TestHelper.assertUndeliverable(errors, 0, IOException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void onCompleteAfterCrash() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + + try { + Observable.wrap(new ObservableSource<Object>() { + @Override + public void subscribe(Observer<? super Object> observer) { + observer.onSubscribe(Disposable.empty()); + observer.onComplete(); + } + }) + .doAfterTerminate(new Action() { + @Override + public void run() throws Exception { + throw new IOException(); + } + }) + .test() + .assertResult(); + + TestHelper.assertUndeliverable(errors, 0, IOException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void onCompleteCrash() { + Observable.wrap(new ObservableSource<Object>() { + @Override + public void subscribe(Observer<? super Object> observer) { + observer.onSubscribe(Disposable.empty()); + observer.onComplete(); + } + }) + .doOnComplete(new Action() { + @Override + public void run() throws Exception { + throw new IOException(); + } + }) + .test() + .assertFailure(IOException.class); + } + + @Test + public void ignoreCancelConditional() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + + try { + Observable.wrap(new ObservableSource<Object>() { + @Override + public void subscribe(Observer<? super Object> observer) { + observer.onSubscribe(Disposable.empty()); + observer.onNext(1); + observer.onNext(2); + observer.onError(new IOException()); + observer.onComplete(); + } + }) + .doOnNext(new Consumer<Object>() { + @Override + public void accept(Object e) throws Exception { + throw new TestException(); + } + }) + .filter(Functions.alwaysTrue()) + .test() + .assertFailure(TestException.class); + + TestHelper.assertUndeliverable(errors, 0, IOException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void onErrorAfterCrashConditional() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + + try { + Observable.wrap(new ObservableSource<Object>() { + @Override + public void subscribe(Observer<? super Object> observer) { + observer.onSubscribe(Disposable.empty()); + observer.onError(new TestException()); + } + }) + .doAfterTerminate(new Action() { + @Override + public void run() throws Exception { + throw new IOException(); + } + }) + .filter(Functions.alwaysTrue()) + .test() + .assertFailure(TestException.class); + + TestHelper.assertUndeliverable(errors, 0, IOException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void onCompleteAfter() { + final int[] call = { 0 }; + Observable.just(1) + .doAfterTerminate(new Action() { + @Override + public void run() throws Exception { + call[0]++; + } + }) + .test() + .assertResult(1); + + assertEquals(1, call[0]); + } + + @Test + public void onCompleteAfterCrashConditional() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + + try { + Observable.wrap(new ObservableSource<Object>() { + @Override + public void subscribe(Observer<? super Object> observer) { + observer.onSubscribe(Disposable.empty()); + observer.onComplete(); + } + }) + .doAfterTerminate(new Action() { + @Override + public void run() throws Exception { + throw new IOException(); + } + }) + .filter(Functions.alwaysTrue()) + .test() + .assertResult(); + + TestHelper.assertUndeliverable(errors, 0, IOException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void onCompleteCrashConditional() { + Observable.wrap(new ObservableSource<Object>() { + @Override + public void subscribe(Observer<? super Object> observer) { + observer.onSubscribe(Disposable.empty()); + observer.onComplete(); + } + }) + .doOnComplete(new Action() { + @Override + public void run() throws Exception { + throw new IOException(); + } + }) + .filter(Functions.alwaysTrue()) + .test() + .assertFailure(IOException.class); + } + + @Test + public void onErrorOnErrorCrashConditional() { + TestObserverEx<Object> to = Observable.error(new TestException("Outer")) + .doOnError(new Consumer<Throwable>() { + @Override + public void accept(Throwable e) throws Exception { + throw new TestException("Inner"); + } + }) + .filter(Functions.alwaysTrue()) + .to(TestHelper.testConsumer()) + .assertFailure(CompositeException.class); + + List<Throwable> errors = TestHelper.compositeList(to.errors().get(0)); + + TestHelper.assertError(errors, 0, TestException.class, "Outer"); + TestHelper.assertError(errors, 1, TestException.class, "Inner"); + } + + @Test + @Ignore("Fusion not supported yet") // TODO decide/implement fusion + public void fused() { + TestObserverEx<Integer> to = new TestObserverEx<>(QueueFuseable.ANY); + + final int[] call = { 0, 0 }; + + Observable.range(1, 5) + .doOnNext(new Consumer<Integer>() { + @Override + public void accept(Integer v) throws Exception { + call[0]++; + } + }) + .doOnComplete(new Action() { + @Override + public void run() throws Exception { + call[1]++; + } + }) + .subscribe(to); + + to.assertFuseable() + .assertFusionMode(QueueFuseable.SYNC) + .assertResult(1, 2, 3, 4, 5); + + assertEquals(5, call[0]); + assertEquals(1, call[1]); + } + + @Test + @Ignore("Fusion not supported yet") // TODO decide/implement fusion + public void fusedOnErrorCrash() { + TestObserverEx<Integer> to = new TestObserverEx<>(QueueFuseable.ANY); + + final int[] call = { 0 }; + + Observable.range(1, 5) + .doOnNext(new Consumer<Integer>() { + @Override + public void accept(Integer v) throws Exception { + throw new TestException(); + } + }) + .doOnComplete(new Action() { + @Override + public void run() throws Exception { + call[0]++; + } + }) + .subscribe(to); + + to.assertFuseable() + .assertFusionMode(QueueFuseable.SYNC) + .assertFailure(TestException.class); + + assertEquals(0, call[0]); + } + + @Test + @Ignore("Fusion not supported yet") // TODO decide/implement fusion + public void fusedConditional() { + TestObserverEx<Integer> to = new TestObserverEx<>(QueueFuseable.ANY); + + final int[] call = { 0, 0 }; + + Observable.range(1, 5) + .doOnNext(new Consumer<Integer>() { + @Override + public void accept(Integer v) throws Exception { + call[0]++; + } + }) + .doOnComplete(new Action() { + @Override + public void run() throws Exception { + call[1]++; + } + }) + .filter(Functions.alwaysTrue()) + .subscribe(to); + + to.assertFuseable() + .assertFusionMode(QueueFuseable.SYNC) + .assertResult(1, 2, 3, 4, 5); + + assertEquals(5, call[0]); + assertEquals(1, call[1]); + } + + @Test + @Ignore("Fusion not supported yet") // TODO decide/implement fusion + public void fusedOnErrorCrashConditional() { + TestObserverEx<Integer> to = new TestObserverEx<>(QueueFuseable.ANY); + + final int[] call = { 0 }; + + Observable.range(1, 5) + .doOnNext(new Consumer<Integer>() { + @Override + public void accept(Integer v) throws Exception { + throw new TestException(); + } + }) + .doOnComplete(new Action() { + @Override + public void run() throws Exception { + call[0]++; + } + }) + .filter(Functions.alwaysTrue()) + .subscribe(to); + + to.assertFuseable() + .assertFusionMode(QueueFuseable.SYNC) + .assertFailure(TestException.class); + + assertEquals(0, call[0]); + } + + @Test + @Ignore("Fusion not supported yet") // TODO decide/implement fusion + public void fusedAsync() { + TestObserverEx<Integer> to = new TestObserverEx<>(QueueFuseable.ANY); + + final int[] call = { 0, 0 }; + + UnicastSubject<Integer> us = UnicastSubject.create(); + + us + .doOnNext(new Consumer<Integer>() { + @Override + public void accept(Integer v) throws Exception { + call[0]++; + } + }) + .doOnComplete(new Action() { + @Override + public void run() throws Exception { + call[1]++; + } + }) + .subscribe(to); + + TestHelper.emit(us, 1, 2, 3, 4, 5); + + to.assertFuseable() + .assertFusionMode(QueueFuseable.ASYNC) + .assertResult(1, 2, 3, 4, 5); + + assertEquals(5, call[0]); + assertEquals(1, call[1]); + } + + @Test + @Ignore("Fusion not supported yet") // TODO decide/implement fusion + public void fusedAsyncConditional() { + TestObserverEx<Integer> to = new TestObserverEx<>(QueueFuseable.ANY); + + final int[] call = { 0, 0 }; + + UnicastSubject<Integer> us = UnicastSubject.create(); + + us + .doOnNext(new Consumer<Integer>() { + @Override + public void accept(Integer v) throws Exception { + call[0]++; + } + }) + .doOnComplete(new Action() { + @Override + public void run() throws Exception { + call[1]++; + } + }) + .filter(Functions.alwaysTrue()) + .subscribe(to); + + TestHelper.emit(us, 1, 2, 3, 4, 5); + + to.assertFuseable() + .assertFusionMode(QueueFuseable.ASYNC) + .assertResult(1, 2, 3, 4, 5); + + assertEquals(5, call[0]); + assertEquals(1, call[1]); + } + + @Test + @Ignore("Fusion not supported yet") // TODO decide/implement fusion + public void fusedAsyncConditional2() { + TestObserverEx<Integer> to = new TestObserverEx<>(QueueFuseable.ANY); + + final int[] call = { 0, 0 }; + + UnicastSubject<Integer> us = UnicastSubject.create(); + + us.hide() + .doOnNext(new Consumer<Integer>() { + @Override + public void accept(Integer v) throws Exception { + call[0]++; + } + }) + .doOnComplete(new Action() { + @Override + public void run() throws Exception { + call[1]++; + } + }) + .filter(Functions.alwaysTrue()) + .subscribe(to); + + TestHelper.emit(us, 1, 2, 3, 4, 5); + + to.assertFuseable() + .assertFusionMode(QueueFuseable.NONE) + .assertResult(1, 2, 3, 4, 5); + + assertEquals(5, call[0]); + assertEquals(1, call[1]); + } + + @Test + public void dispose() { + TestHelper.checkDisposed(Observable.just(1).doOnEach(new TestObserver<>())); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeObservable(new Function<Observable<Object>, ObservableSource<Object>>() { + @Override + public ObservableSource<Object> apply(Observable<Object> o) throws Exception { + return o.doOnEach(new TestObserver<>()); + } + }); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableDoOnSubscribeTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableDoOnSubscribeTest.java new file mode 100644 index 0000000000..f001d28ac0 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableDoOnSubscribeTest.java @@ -0,0 +1,140 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import static org.junit.Assert.*; + +import java.util.List; +import java.util.concurrent.atomic.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.Consumer; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class ObservableDoOnSubscribeTest extends RxJavaTest { + + @Test + public void doOnSubscribe() throws Exception { + final AtomicInteger count = new AtomicInteger(); + Observable<Integer> o = Observable.just(1).doOnSubscribe(new Consumer<Disposable>() { + @Override + public void accept(Disposable d) { + count.incrementAndGet(); + } + }); + + o.subscribe(); + o.subscribe(); + o.subscribe(); + assertEquals(3, count.get()); + } + + @Test + public void doOnSubscribe2() throws Exception { + final AtomicInteger count = new AtomicInteger(); + Observable<Integer> o = Observable.just(1).doOnSubscribe(new Consumer<Disposable>() { + @Override + public void accept(Disposable d) { + count.incrementAndGet(); + } + }).take(1).doOnSubscribe(new Consumer<Disposable>() { + @Override + public void accept(Disposable d) { + count.incrementAndGet(); + } + }); + + o.subscribe(); + assertEquals(2, count.get()); + } + + @Test + public void doOnUnSubscribeWorksWithRefCount() throws Exception { + final AtomicInteger onSubscribed = new AtomicInteger(); + final AtomicInteger countBefore = new AtomicInteger(); + final AtomicInteger countAfter = new AtomicInteger(); + final AtomicReference<Observer<? super Integer>> sref = new AtomicReference<>(); + Observable<Integer> o = Observable.unsafeCreate(new ObservableSource<Integer>() { + + @Override + public void subscribe(Observer<? super Integer> observer) { + observer.onSubscribe(Disposable.empty()); + onSubscribed.incrementAndGet(); + sref.set(observer); + } + + }).doOnSubscribe(new Consumer<Disposable>() { + @Override + public void accept(Disposable d) { + countBefore.incrementAndGet(); + } + }).publish().refCount() + .doOnSubscribe(new Consumer<Disposable>() { + @Override + public void accept(Disposable d) { + countAfter.incrementAndGet(); + } + }); + + o.subscribe(); + o.subscribe(); + o.subscribe(); + assertEquals(1, countBefore.get()); + assertEquals(1, onSubscribed.get()); + assertEquals(3, countAfter.get()); + sref.get().onComplete(); + o.subscribe(); + o.subscribe(); + o.subscribe(); + assertEquals(2, countBefore.get()); + assertEquals(2, onSubscribed.get()); + assertEquals(6, countAfter.get()); + } + + @Test + public void onSubscribeCrash() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + final Disposable bs = Disposable.empty(); + + new Observable<Integer>() { + @Override + protected void subscribeActual(Observer<? super Integer> observer) { + observer.onSubscribe(bs); + observer.onError(new TestException("Second")); + observer.onComplete(); + } + } + .doOnSubscribe(new Consumer<Disposable>() { + @Override + public void accept(Disposable d) throws Exception { + throw new TestException("First"); + } + }) + .to(TestHelper.<Integer>testConsumer()) + .assertFailureAndMessage(TestException.class, "First"); + + assertTrue(bs.isDisposed()); + + TestHelper.assertUndeliverable(errors, 0, TestException.class, "Second"); + } finally { + RxJavaPlugins.reset(); + } + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableDoOnUnsubscribeTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableDoOnUnsubscribeTest.java new file mode 100644 index 0000000000..5ac217725b --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableDoOnUnsubscribeTest.java @@ -0,0 +1,177 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import static org.junit.Assert.assertEquals; + +import java.util.*; +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.core.RxJavaTest; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.subjects.BehaviorSubject; + +public class ObservableDoOnUnsubscribeTest extends RxJavaTest { + + @Test + public void doOnUnsubscribe() throws Exception { + int subCount = 3; + final CountDownLatch upperLatch = new CountDownLatch(subCount); + final CountDownLatch lowerLatch = new CountDownLatch(subCount); + final CountDownLatch onNextLatch = new CountDownLatch(subCount); + + final AtomicInteger upperCount = new AtomicInteger(); + final AtomicInteger lowerCount = new AtomicInteger(); + Observable<Long> longs = Observable + // The stream needs to be infinite to ensure the stream does not terminate + // before it is unsubscribed + .interval(50, TimeUnit.MILLISECONDS) + .doOnDispose(new Action() { + @Override + public void run() { + // Test that upper stream will be notified for un-subscription + // from a child Observer + upperLatch.countDown(); + upperCount.incrementAndGet(); + } + }) + .doOnNext(new Consumer<Long>() { + @Override + public void accept(Long aLong) { + // Ensure there is at least some onNext events before un-subscription happens + onNextLatch.countDown(); + } + }) + .doOnDispose(new Action() { + @Override + public void run() { + // Test that lower stream will be notified for a direct un-subscription + lowerLatch.countDown(); + lowerCount.incrementAndGet(); + } + }); + + List<Disposable> subscriptions = new ArrayList<>(); + List<TestObserver<Long>> subscribers = new ArrayList<>(); + + for (int i = 0; i < subCount; ++i) { + TestObserver<Long> observer = new TestObserver<>(); + subscriptions.add(observer); + longs.subscribe(observer); + subscribers.add(observer); + } + + onNextLatch.await(); + for (int i = 0; i < subCount; ++i) { + subscriptions.get(i).dispose(); + // Test that unsubscribe() method is not affected in any way + // FIXME no longer valid +// subscribers.get(i).assertUnsubscribed(); + } + + upperLatch.await(); + lowerLatch.await(); + assertEquals(String.format("There should exactly %d un-subscription events for upper stream", subCount), subCount, upperCount.get()); + assertEquals(String.format("There should exactly %d un-subscription events for lower stream", subCount), subCount, lowerCount.get()); + } + + @Test + public void doOnUnSubscribeWorksWithRefCount() throws Exception { + int subCount = 3; + final CountDownLatch upperLatch = new CountDownLatch(1); + final CountDownLatch lowerLatch = new CountDownLatch(1); + final CountDownLatch onNextLatch = new CountDownLatch(subCount); + + final AtomicInteger upperCount = new AtomicInteger(); + final AtomicInteger lowerCount = new AtomicInteger(); + Observable<Long> longs = Observable + // The stream needs to be infinite to ensure the stream does not terminate + // before it is unsubscribed + .interval(50, TimeUnit.MILLISECONDS) + .doOnDispose(new Action() { + @Override + public void run() { + // Test that upper stream will be notified for un-subscription + upperLatch.countDown(); + upperCount.incrementAndGet(); + } + }) + .doOnNext(new Consumer<Long>() { + @Override + public void accept(Long aLong) { + // Ensure there is at least some onNext events before un-subscription happens + onNextLatch.countDown(); + } + }) + .doOnDispose(new Action() { + @Override + public void run() { + // Test that lower stream will be notified for un-subscription + lowerLatch.countDown(); + lowerCount.incrementAndGet(); + } + }) + .publish() + .refCount(); + + List<Disposable> subscriptions = new ArrayList<>(); + List<TestObserver<Long>> subscribers = new ArrayList<>(); + + for (int i = 0; i < subCount; ++i) { + TestObserver<Long> observer = new TestObserver<>(); + longs.subscribe(observer); + subscriptions.add(observer); + subscribers.add(observer); + } + + onNextLatch.await(); + for (int i = 0; i < subCount; ++i) { + subscriptions.get(i).dispose(); + // Test that unsubscribe() method is not affected in any way + // FIXME no longer valid +// subscribers.get(i).assertUnsubscribed(); + } + + upperLatch.await(); + lowerLatch.await(); + assertEquals("There should exactly 1 un-subscription events for upper stream", 1, upperCount.get()); + assertEquals("There should exactly 1 un-subscription events for lower stream", 1, lowerCount.get()); + } + + @Test + public void noReentrantDispose() { + + final AtomicInteger disposeCalled = new AtomicInteger(); + + final BehaviorSubject<Integer> s = BehaviorSubject.create(); + s.doOnDispose(new Action() { + @Override + public void run() throws Exception { + disposeCalled.incrementAndGet(); + s.onNext(2); + } + }) + .firstOrError() + .subscribe() + .dispose(); + + assertEquals(1, disposeCalled.get()); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableElementAtTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableElementAtTest.java new file mode 100644 index 0000000000..8ce5cb047c --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableElementAtTest.java @@ -0,0 +1,304 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import static org.junit.Assert.*; + +import java.util.*; + +import io.reactivex.rxjava3.disposables.Disposable; +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.core.Observer; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.subjects.PublishSubject; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class ObservableElementAtTest extends RxJavaTest { + + @Test + public void elementAtObservable() { + assertEquals(2, Observable.fromArray(1, 2).elementAt(1).toObservable().blockingSingle() + .intValue()); + } + + @Test + public void elementAtWithIndexOutOfBoundsObservable() { + assertEquals(-99, Observable.fromArray(1, 2).elementAt(2).toObservable().blockingSingle(-99).intValue()); + } + + @Test + public void elementAtOrDefaultObservable() { + assertEquals(2, Observable.fromArray(1, 2).elementAt(1, 0).toObservable().blockingSingle().intValue()); + } + + @Test + public void elementAtOrDefaultWithIndexOutOfBoundsObservable() { + assertEquals(0, Observable.fromArray(1, 2).elementAt(2, 0).toObservable().blockingSingle().intValue()); + } + + @Test + public void elementAt() { + assertEquals(2, Observable.fromArray(1, 2).elementAt(1).blockingGet() + .intValue()); + } + + @Test(expected = IndexOutOfBoundsException.class) + public void elementAtWithMinusIndex() { + Observable.fromArray(1, 2).elementAt(-1); + } + + @Test + public void elementAtWithIndexOutOfBounds() { + assertNull(Observable.fromArray(1, 2).elementAt(2).blockingGet()); + } + + @Test + public void elementAtOrDefault() { + assertEquals(2, Observable.fromArray(1, 2).elementAt(1, 0).blockingGet().intValue()); + } + + @Test + public void elementAtOrDefaultWithIndexOutOfBounds() { + assertEquals(0, Observable.fromArray(1, 2).elementAt(2, 0).blockingGet().intValue()); + } + + @Test(expected = IndexOutOfBoundsException.class) + public void elementAtOrDefaultWithMinusIndex() { + Observable.fromArray(1, 2).elementAt(-1, 0); + } + + @Test(expected = IndexOutOfBoundsException.class) + public void elementAtOrErrorNegativeIndex() { + Observable.empty() + .elementAtOrError(-1); + } + + @Test + public void elementAtOrErrorNoElement() { + Observable.empty() + .elementAtOrError(0) + .test() + .assertNoValues() + .assertError(NoSuchElementException.class); + } + + @Test + public void elementAtOrErrorOneElement() { + Observable.just(1) + .elementAtOrError(0) + .test() + .assertNoErrors() + .assertValue(1); + } + + @Test + public void elementAtOrErrorMultipleElements() { + Observable.just(1, 2, 3) + .elementAtOrError(1) + .test() + .assertNoErrors() + .assertValue(2); + } + + @Test + public void elementAtOrErrorInvalidIndex() { + Observable.just(1, 2, 3) + .elementAtOrError(3) + .test() + .assertNoValues() + .assertError(NoSuchElementException.class); + } + + @Test + public void elementAtOrErrorError() { + Observable.error(new RuntimeException("error")) + .elementAtOrError(0) + .to(TestHelper.testConsumer()) + .assertNoValues() + .assertErrorMessage("error") + .assertError(RuntimeException.class); + } + + @Test + public void elementAtIndex0OnEmptySource() { + Observable.empty() + .elementAt(0) + .test() + .assertResult(); + } + + @Test + public void elementAtIndex0WithDefaultOnEmptySource() { + Observable.empty() + .elementAt(0, 5) + .test() + .assertResult(5); + } + + @Test + public void elementAtIndex1OnEmptySource() { + Observable.empty() + .elementAt(1) + .test() + .assertResult(); + } + + @Test + public void elementAtIndex1WithDefaultOnEmptySource() { + Observable.empty() + .elementAt(1, 10) + .test() + .assertResult(10); + } + + @Test + public void elementAtOrErrorIndex1OnEmptySource() { + Observable.empty() + .elementAtOrError(1) + .test() + .assertFailure(NoSuchElementException.class); + } + + @Test + public void dispose() { + TestHelper.checkDisposed(PublishSubject.create().elementAt(0).toObservable()); + TestHelper.checkDisposed(PublishSubject.create().elementAt(0)); + + TestHelper.checkDisposed(PublishSubject.create().elementAt(0, 1).toObservable()); + TestHelper.checkDisposed(PublishSubject.create().elementAt(0, 1)); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeObservable(new Function<Observable<Object>, ObservableSource<Object>>() { + @Override + public ObservableSource<Object> apply(Observable<Object> o) throws Exception { + return o.elementAt(0).toObservable(); + } + }); + + TestHelper.checkDoubleOnSubscribeObservableToMaybe(new Function<Observable<Object>, MaybeSource<Object>>() { + @Override + public MaybeSource<Object> apply(Observable<Object> o) throws Exception { + return o.elementAt(0); + } + }); + + TestHelper.checkDoubleOnSubscribeObservableToSingle(new Function<Observable<Object>, SingleSource<Object>>() { + @Override + public SingleSource<Object> apply(Observable<Object> o) throws Exception { + return o.elementAt(0, 1); + } + }); + } + + @Test + public void elementAtIndex1WithDefaultOnEmptySourceObservable() { + Observable.empty() + .elementAt(1, 10) + .toObservable() + .test() + .assertResult(10); + } + + @Test + public void errorObservable() { + Observable.error(new TestException()) + .elementAt(1, 10) + .toObservable() + .test() + .assertFailure(TestException.class); + } + + @Test + public void badSourceObservable() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + new Observable<Integer>() { + @Override + protected void subscribeActual(Observer<? super Integer> observer) { + observer.onSubscribe(Disposable.empty()); + + observer.onNext(1); + observer.onNext(2); + observer.onError(new TestException()); + observer.onComplete(); + } + } + .elementAt(0) + .toObservable() + .test() + .assertResult(1); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void badSource() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + new Observable<Integer>() { + @Override + protected void subscribeActual(Observer<? super Integer> observer) { + observer.onSubscribe(Disposable.empty()); + + observer.onNext(1); + observer.onNext(2); + observer.onError(new TestException()); + observer.onComplete(); + } + } + .elementAt(0) + .test() + .assertResult(1); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void badSource2() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + new Observable<Integer>() { + @Override + protected void subscribeActual(Observer<? super Integer> observer) { + observer.onSubscribe(Disposable.empty()); + + observer.onNext(1); + observer.onNext(2); + observer.onError(new TestException()); + observer.onComplete(); + } + } + .elementAt(0, 1) + .test() + .assertResult(1); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableFilterTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableFilterTest.java new file mode 100644 index 0000000000..71585b1c33 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableFilterTest.java @@ -0,0 +1,136 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import org.junit.Test; +import org.mockito.Mockito; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.operators.QueueFuseable; +import io.reactivex.rxjava3.subjects.UnicastSubject; +import io.reactivex.rxjava3.testsupport.*; + +public class ObservableFilterTest extends RxJavaTest { + + @Test + public void filter() { + Observable<String> w = Observable.just("one", "two", "three"); + Observable<String> observable = w.filter(new Predicate<String>() { + + @Override + public boolean test(String t1) { + return t1.equals("two"); + } + }); + + Observer<String> observer = TestHelper.mockObserver(); + + observable.subscribe(observer); + + verify(observer, Mockito.never()).onNext("one"); + verify(observer, times(1)).onNext("two"); + verify(observer, Mockito.never()).onNext("three"); + verify(observer, Mockito.never()).onError(any(Throwable.class)); + verify(observer, times(1)).onComplete(); + } + + @Test + public void dispose() { + TestHelper.checkDisposed(Observable.range(1, 5).filter(Functions.alwaysTrue())); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeObservable(new Function<Observable<Object>, ObservableSource<Object>>() { + @Override + public ObservableSource<Object> apply(Observable<Object> o) throws Exception { + return o.filter(Functions.alwaysTrue()); + } + }); + } + + @Test + public void fusedSync() { + TestObserverEx<Integer> to = new TestObserverEx<>(QueueFuseable.ANY); + + Observable.range(1, 5) + .filter(new Predicate<Integer>() { + @Override + public boolean test(Integer v) throws Exception { + return v % 2 == 0; + } + }) + .subscribe(to); + + to.assertFusionMode(QueueFuseable.SYNC) + .assertResult(2, 4); + } + + @Test + public void fusedAsync() { + TestObserverEx<Integer> to = new TestObserverEx<>(QueueFuseable.ANY); + + UnicastSubject<Integer> us = UnicastSubject.create(); + + us + .filter(new Predicate<Integer>() { + @Override + public boolean test(Integer v) throws Exception { + return v % 2 == 0; + } + }) + .subscribe(to); + + TestHelper.emit(us, 1, 2, 3, 4, 5); + + to.assertFusionMode(QueueFuseable.ASYNC) + .assertResult(2, 4); + } + + @Test + public void fusedReject() { + TestObserverEx<Integer> to = new TestObserverEx<>(QueueFuseable.ANY | QueueFuseable.BOUNDARY); + + Observable.range(1, 5) + .filter(new Predicate<Integer>() { + @Override + public boolean test(Integer v) throws Exception { + return v % 2 == 0; + } + }) + .subscribe(to); + + to.assertFusionMode(QueueFuseable.NONE) + .assertResult(2, 4); + } + + @Test + public void filterThrows() { + Observable.range(1, 5) + .filter(new Predicate<Integer>() { + @Override + public boolean test(Integer v) throws Exception { + throw new TestException(); + } + }) + .test() + .assertFailure(TestException.class); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableFinallyTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableFinallyTest.java new file mode 100644 index 0000000000..9f20203ed2 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableFinallyTest.java @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import static org.mockito.Mockito.*; + +import org.junit.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.functions.Action; +import io.reactivex.rxjava3.internal.util.ExceptionHelper; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class ObservableFinallyTest extends RxJavaTest { + + private Action aAction0; + private Observer<String> observer; + + // mocking has to be unchecked, unfortunately + @Before + public void before() { + aAction0 = mock(Action.class); + observer = TestHelper.mockObserver(); + } + + private void checkActionCalled(Observable<String> input) { + input.doAfterTerminate(aAction0).subscribe(observer); + try { + verify(aAction0, times(1)).run(); + } catch (Throwable e) { + throw ExceptionHelper.wrapOrThrow(e); + } + } + + @Test + public void finallyCalledOnComplete() { + checkActionCalled(Observable.fromArray("1", "2", "3")); + } + + @Test + public void finallyCalledOnError() { + checkActionCalled(Observable.<String> error(new RuntimeException("expected"))); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableFirstTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableFirstTest.java new file mode 100644 index 0000000000..9129f1d0cd --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableFirstTest.java @@ -0,0 +1,587 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +import java.util.NoSuchElementException; + +import org.junit.*; +import org.mockito.InOrder; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.functions.Predicate; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class ObservableFirstTest extends RxJavaTest { + + Observer<String> w; + SingleObserver<Object> wo; + MaybeObserver<Object> wm; + + private static final Predicate<String> IS_D = new Predicate<String>() { + @Override + public boolean test(String value) { + return "d".equals(value); + } + }; + + @Before + public void before() { + w = TestHelper.mockObserver(); + wo = TestHelper.mockSingleObserver(); + wm = TestHelper.mockMaybeObserver(); + } + + @Test + public void firstOrElseOfNoneObservable() { + Observable<String> src = Observable.empty(); + src.first("default").toObservable().subscribe(w); + + verify(w, times(1)).onNext(anyString()); + verify(w, times(1)).onNext("default"); + verify(w, never()).onError(any(Throwable.class)); + verify(w, times(1)).onComplete(); + } + + @Test + public void firstOrElseOfSomeObservable() { + Observable<String> src = Observable.just("a", "b", "c"); + src.first("default").toObservable().subscribe(w); + + verify(w, times(1)).onNext(anyString()); + verify(w, times(1)).onNext("a"); + verify(w, never()).onError(any(Throwable.class)); + verify(w, times(1)).onComplete(); + } + + @Test + public void firstOrElseWithPredicateOfNoneMatchingThePredicateObservable() { + Observable<String> src = Observable.just("a", "b", "c"); + src.filter(IS_D).first("default").toObservable().subscribe(w); + + verify(w, times(1)).onNext(anyString()); + verify(w, times(1)).onNext("default"); + verify(w, never()).onError(any(Throwable.class)); + verify(w, times(1)).onComplete(); + } + + @Test + public void firstOrElseWithPredicateOfSomeObservable() { + Observable<String> src = Observable.just("a", "b", "c", "d", "e", "f"); + src.filter(IS_D).first("default").toObservable().subscribe(w); + + verify(w, times(1)).onNext(anyString()); + verify(w, times(1)).onNext("d"); + verify(w, never()).onError(any(Throwable.class)); + verify(w, times(1)).onComplete(); + } + + @Test + public void firstObservable() { + Observable<Integer> o = Observable.just(1, 2, 3).firstElement().toObservable(); + + Observer<Integer> observer = TestHelper.mockObserver(); + o.subscribe(observer); + + InOrder inOrder = inOrder(observer); + inOrder.verify(observer, times(1)).onNext(1); + inOrder.verify(observer, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void firstWithOneElementObservable() { + Observable<Integer> o = Observable.just(1).firstElement().toObservable(); + + Observer<Integer> observer = TestHelper.mockObserver(); + o.subscribe(observer); + + InOrder inOrder = inOrder(observer); + inOrder.verify(observer, times(1)).onNext(1); + inOrder.verify(observer, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void firstWithEmptyObservable() { + Observable<Integer> o = Observable.<Integer> empty().firstElement().toObservable(); + + Observer<Integer> observer = TestHelper.mockObserver(); + o.subscribe(observer); + + InOrder inOrder = inOrder(observer); + inOrder.verify(observer).onComplete(); + inOrder.verify(observer, never()).onError(any(Throwable.class)); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void firstWithPredicateObservable() { + Observable<Integer> o = Observable.just(1, 2, 3, 4, 5, 6) + .filter(new Predicate<Integer>() { + @Override + public boolean test(Integer t1) { + return t1 % 2 == 0; + } + }) + .firstElement().toObservable(); + + Observer<Integer> observer = TestHelper.mockObserver(); + o.subscribe(observer); + + InOrder inOrder = inOrder(observer); + inOrder.verify(observer, times(1)).onNext(2); + inOrder.verify(observer, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void firstWithPredicateAndOneElementObservable() { + Observable<Integer> o = Observable.just(1, 2) + .filter(new Predicate<Integer>() { + @Override + public boolean test(Integer t1) { + return t1 % 2 == 0; + } + }) + .firstElement().toObservable(); + + Observer<Integer> observer = TestHelper.mockObserver(); + o.subscribe(observer); + + InOrder inOrder = inOrder(observer); + inOrder.verify(observer, times(1)).onNext(2); + inOrder.verify(observer, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void firstWithPredicateAndEmptyObservable() { + Observable<Integer> o = Observable.just(1) + .filter(new Predicate<Integer>() { + @Override + public boolean test(Integer t1) { + return t1 % 2 == 0; + } + }) + .firstElement().toObservable(); + + Observer<Integer> observer = TestHelper.mockObserver(); + o.subscribe(observer); + + InOrder inOrder = inOrder(observer); + inOrder.verify(observer).onComplete(); + inOrder.verify(observer, never()).onError(any(Throwable.class)); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void firstOrDefaultObservable() { + Observable<Integer> o = Observable.just(1, 2, 3) + .first(4).toObservable(); + + Observer<Integer> observer = TestHelper.mockObserver(); + o.subscribe(observer); + + InOrder inOrder = inOrder(observer); + inOrder.verify(observer, times(1)).onNext(1); + inOrder.verify(observer, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void firstOrDefaultWithOneElementObservable() { + Observable<Integer> o = Observable.just(1).first(2).toObservable(); + + Observer<Integer> observer = TestHelper.mockObserver(); + o.subscribe(observer); + + InOrder inOrder = inOrder(observer); + inOrder.verify(observer, times(1)).onNext(1); + inOrder.verify(observer, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void firstOrDefaultWithEmptyObservable() { + Observable<Integer> o = Observable.<Integer> empty() + .first(1).toObservable(); + + Observer<Integer> observer = TestHelper.mockObserver(); + o.subscribe(observer); + + InOrder inOrder = inOrder(observer); + inOrder.verify(observer, times(1)).onNext(1); + inOrder.verify(observer, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void firstOrDefaultWithPredicateObservable() { + Observable<Integer> o = Observable.just(1, 2, 3, 4, 5, 6) + .filter(new Predicate<Integer>() { + @Override + public boolean test(Integer t1) { + return t1 % 2 == 0; + } + }) + .first(8).toObservable(); + + Observer<Integer> observer = TestHelper.mockObserver(); + o.subscribe(observer); + + InOrder inOrder = inOrder(observer); + inOrder.verify(observer, times(1)).onNext(2); + inOrder.verify(observer, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void firstOrDefaultWithPredicateAndOneElementObservable() { + Observable<Integer> o = Observable.just(1, 2) + .filter(new Predicate<Integer>() { + @Override + public boolean test(Integer t1) { + return t1 % 2 == 0; + } + }) + .first(4).toObservable(); + + Observer<Integer> observer = TestHelper.mockObserver(); + o.subscribe(observer); + + InOrder inOrder = inOrder(observer); + inOrder.verify(observer, times(1)).onNext(2); + inOrder.verify(observer, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void firstOrDefaultWithPredicateAndEmptyObservable() { + Observable<Integer> o = Observable.just(1) + .filter(new Predicate<Integer>() { + @Override + public boolean test(Integer t1) { + return t1 % 2 == 0; + } + }) + .first(2).toObservable(); + + Observer<Integer> observer = TestHelper.mockObserver(); + o.subscribe(observer); + + InOrder inOrder = inOrder(observer); + inOrder.verify(observer, times(1)).onNext(2); + inOrder.verify(observer, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void firstOrElseOfNone() { + Observable<String> src = Observable.empty(); + src.first("default").subscribe(wo); + + verify(wo, times(1)).onSuccess(anyString()); + verify(wo, times(1)).onSuccess("default"); + verify(wo, never()).onError(any(Throwable.class)); + } + + @Test + public void firstOrElseOfSome() { + Observable<String> src = Observable.just("a", "b", "c"); + src.first("default").subscribe(wo); + + verify(wo, times(1)).onSuccess(anyString()); + verify(wo, times(1)).onSuccess("a"); + verify(wo, never()).onError(any(Throwable.class)); + } + + @Test + public void firstOrElseWithPredicateOfNoneMatchingThePredicate() { + Observable<String> src = Observable.just("a", "b", "c"); + src.filter(IS_D).first("default").subscribe(wo); + + verify(wo, times(1)).onSuccess(anyString()); + verify(wo, times(1)).onSuccess("default"); + verify(wo, never()).onError(any(Throwable.class)); + } + + @Test + public void firstOrElseWithPredicateOfSome() { + Observable<String> src = Observable.just("a", "b", "c", "d", "e", "f"); + src.filter(IS_D).first("default").subscribe(wo); + + verify(wo, times(1)).onSuccess(anyString()); + verify(wo, times(1)).onSuccess("d"); + verify(wo, never()).onError(any(Throwable.class)); + } + + @Test + public void first() { + Maybe<Integer> o = Observable.just(1, 2, 3).firstElement(); + + o.subscribe(wm); + + InOrder inOrder = inOrder(wm); + inOrder.verify(wm, times(1)).onSuccess(1); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void firstWithOneElement() { + Maybe<Integer> o = Observable.just(1).firstElement(); + + o.subscribe(wm); + + InOrder inOrder = inOrder(wm); + inOrder.verify(wm, times(1)).onSuccess(1); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void firstWithEmpty() { + Maybe<Integer> o = Observable.<Integer> empty().firstElement(); + + o.subscribe(wm); + + InOrder inOrder = inOrder(wm); + inOrder.verify(wm, times(1)).onComplete(); + inOrder.verify(wm, never()).onError(any(Throwable.class)); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void firstWithPredicate() { + Maybe<Integer> o = Observable.just(1, 2, 3, 4, 5, 6) + .filter(new Predicate<Integer>() { + @Override + public boolean test(Integer t1) { + return t1 % 2 == 0; + } + }) + .firstElement(); + + o.subscribe(wm); + + InOrder inOrder = inOrder(wm); + inOrder.verify(wm, times(1)).onSuccess(2); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void firstWithPredicateAndOneElement() { + Maybe<Integer> o = Observable.just(1, 2) + .filter(new Predicate<Integer>() { + @Override + public boolean test(Integer t1) { + return t1 % 2 == 0; + } + }) + .firstElement(); + + o.subscribe(wm); + + InOrder inOrder = inOrder(wm); + inOrder.verify(wm, times(1)).onSuccess(2); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void firstWithPredicateAndEmpty() { + Maybe<Integer> o = Observable.just(1) + .filter(new Predicate<Integer>() { + @Override + public boolean test(Integer t1) { + return t1 % 2 == 0; + } + }) + .firstElement(); + + o.subscribe(wm); + + InOrder inOrder = inOrder(wm); + inOrder.verify(wm, times(1)).onComplete(); + inOrder.verify(wm, never()).onError(any(Throwable.class)); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void firstOrDefault() { + Single<Integer> o = Observable.just(1, 2, 3) + .first(4); + + o.subscribe(wo); + + InOrder inOrder = inOrder(wo); + inOrder.verify(wo, times(1)).onSuccess(1); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void firstOrDefaultWithOneElement() { + Single<Integer> o = Observable.just(1).first(2); + + o.subscribe(wo); + + InOrder inOrder = inOrder(wo); + inOrder.verify(wo, times(1)).onSuccess(1); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void firstOrDefaultWithEmpty() { + Single<Integer> o = Observable.<Integer> empty() + .first(1); + + o.subscribe(wo); + + InOrder inOrder = inOrder(wo); + inOrder.verify(wo, times(1)).onSuccess(1); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void firstOrDefaultWithPredicate() { + Single<Integer> o = Observable.just(1, 2, 3, 4, 5, 6) + .filter(new Predicate<Integer>() { + @Override + public boolean test(Integer t1) { + return t1 % 2 == 0; + } + }) + .first(8); + + o.subscribe(wo); + + InOrder inOrder = inOrder(wo); + inOrder.verify(wo, times(1)).onSuccess(2); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void firstOrDefaultWithPredicateAndOneElement() { + Single<Integer> o = Observable.just(1, 2) + .filter(new Predicate<Integer>() { + @Override + public boolean test(Integer t1) { + return t1 % 2 == 0; + } + }) + .first(4); + + o.subscribe(wo); + + InOrder inOrder = inOrder(wo); + inOrder.verify(wo, times(1)).onSuccess(2); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void firstOrDefaultWithPredicateAndEmpty() { + Single<Integer> o = Observable.just(1) + .filter(new Predicate<Integer>() { + @Override + public boolean test(Integer t1) { + return t1 % 2 == 0; + } + }) + .first(2); + + o.subscribe(wo); + + InOrder inOrder = inOrder(wo); + inOrder.verify(wo, times(1)).onSuccess(2); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void firstOrErrorNoElement() { + Observable.empty() + .firstOrError() + .test() + .assertNoValues() + .assertError(NoSuchElementException.class); + } + + @Test + public void firstOrErrorOneElement() { + Observable.just(1) + .firstOrError() + .test() + .assertNoErrors() + .assertValue(1); + } + + @Test + public void firstOrErrorMultipleElements() { + Observable.just(1, 2, 3) + .firstOrError() + .test() + .assertNoErrors() + .assertValue(1); + } + + @Test + public void firstOrErrorError() { + Observable.error(new RuntimeException("error")) + .firstOrError() + .to(TestHelper.testConsumer()) + .assertNoValues() + .assertErrorMessage("error") + .assertError(RuntimeException.class); + } + + @Test + public void firstOrErrorNoElementObservable() { + Observable.empty() + .firstOrError() + .toObservable() + .test() + .assertNoValues() + .assertError(NoSuchElementException.class); + } + + @Test + public void firstOrErrorOneElementObservable() { + Observable.just(1) + .firstOrError() + .toObservable() + .test() + .assertNoErrors() + .assertValue(1); + } + + @Test + public void firstOrErrorMultipleElementsObservable() { + Observable.just(1, 2, 3) + .firstOrError() + .toObservable() + .test() + .assertNoErrors() + .assertValue(1); + } + + @Test + public void firstOrErrorErrorObservable() { + Observable.error(new RuntimeException("error")) + .firstOrError() + .toObservable() + .to(TestHelper.testConsumer()) + .assertNoValues() + .assertErrorMessage("error") + .assertError(RuntimeException.class); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableFlatMapCompletableTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableFlatMapCompletableTest.java new file mode 100644 index 0000000000..facfb084d1 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableFlatMapCompletableTest.java @@ -0,0 +1,565 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import static org.junit.Assert.*; + +import java.util.List; +import java.util.concurrent.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.*; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.operators.QueueDisposable; +import io.reactivex.rxjava3.operators.QueueFuseable; +import io.reactivex.rxjava3.schedulers.Schedulers; +import io.reactivex.rxjava3.subjects.PublishSubject; +import io.reactivex.rxjava3.testsupport.*; + +public class ObservableFlatMapCompletableTest extends RxJavaTest { + + @Test + public void normalObservable() { + Observable.range(1, 10) + .flatMapCompletable(new Function<Integer, CompletableSource>() { + @Override + public CompletableSource apply(Integer v) throws Exception { + return Completable.complete(); + } + }).toObservable() + .test() + .assertResult(); + } + + @Test + public void mapperThrowsObservable() { + PublishSubject<Integer> ps = PublishSubject.create(); + + TestObserver<Integer> to = ps + .flatMapCompletable(new Function<Integer, CompletableSource>() { + @Override + public CompletableSource apply(Integer v) throws Exception { + throw new TestException(); + } + }).<Integer>toObservable() + .test(); + + assertTrue(ps.hasObservers()); + + ps.onNext(1); + + to.assertFailure(TestException.class); + + assertFalse(ps.hasObservers()); + } + + @Test + public void mapperReturnsNullObservable() { + PublishSubject<Integer> ps = PublishSubject.create(); + + TestObserver<Integer> to = ps + .flatMapCompletable(new Function<Integer, CompletableSource>() { + @Override + public CompletableSource apply(Integer v) throws Exception { + return null; + } + }).<Integer>toObservable() + .test(); + + assertTrue(ps.hasObservers()); + + ps.onNext(1); + + to.assertFailure(NullPointerException.class); + + assertFalse(ps.hasObservers()); + } + + @Test + public void normalDelayErrorObservable() { + Observable.range(1, 10) + .flatMapCompletable(new Function<Integer, CompletableSource>() { + @Override + public CompletableSource apply(Integer v) throws Exception { + return Completable.complete(); + } + }, true).toObservable() + .test() + .assertResult(); + } + + @Test + public void normalAsyncObservable() { + Observable.range(1, 1000) + .flatMapCompletable(new Function<Integer, CompletableSource>() { + @Override + public CompletableSource apply(Integer v) throws Exception { + return Observable.range(1, 100).subscribeOn(Schedulers.computation()).ignoreElements(); + } + }).toObservable() + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(); + } + + @Test + public void normalDelayErrorAllObservable() { + TestObserverEx<Integer> to = Observable.range(1, 10).concatWith(Observable.<Integer>error(new TestException())) + .flatMapCompletable(new Function<Integer, CompletableSource>() { + @Override + public CompletableSource apply(Integer v) throws Exception { + return Completable.error(new TestException()); + } + }, true).<Integer>toObservable() + .to(TestHelper.<Integer>testConsumer()) + .assertFailure(CompositeException.class); + + List<Throwable> errors = TestHelper.compositeList(to.errors().get(0)); + + for (int i = 0; i < 11; i++) { + TestHelper.assertError(errors, i, TestException.class); + } + } + + @Test + public void normalDelayInnerErrorAllObservable() { + TestObserverEx<Integer> to = Observable.range(1, 10) + .flatMapCompletable(new Function<Integer, CompletableSource>() { + @Override + public CompletableSource apply(Integer v) throws Exception { + return Completable.error(new TestException()); + } + }, true).<Integer>toObservable() + .to(TestHelper.<Integer>testConsumer()) + .assertFailure(CompositeException.class); + + List<Throwable> errors = TestHelper.compositeList(to.errors().get(0)); + + for (int i = 0; i < 10; i++) { + TestHelper.assertError(errors, i, TestException.class); + } + } + + @Test + public void normalNonDelayErrorOuterObservable() { + Observable.range(1, 10).concatWith(Observable.<Integer>error(new TestException())) + .flatMapCompletable(new Function<Integer, CompletableSource>() { + @Override + public CompletableSource apply(Integer v) throws Exception { + return Completable.complete(); + } + }, false).toObservable() + .test() + .assertFailure(TestException.class); + } + + @Test + public void fusedObservable() { + TestObserverEx<Integer> to = new TestObserverEx<>(QueueFuseable.ANY); + + Observable.range(1, 10) + .flatMapCompletable(new Function<Integer, CompletableSource>() { + @Override + public CompletableSource apply(Integer v) throws Exception { + return Completable.complete(); + } + }).<Integer>toObservable() + .subscribe(to); + + to + .assertFuseable() + .assertFusionMode(QueueFuseable.ASYNC) + .assertResult(); + } + + @Test + public void disposedObservable() { + TestHelper.checkDisposed(Observable.range(1, 10) + .flatMapCompletable(new Function<Integer, CompletableSource>() { + @Override + public CompletableSource apply(Integer v) throws Exception { + return Completable.complete(); + } + }).toObservable()); + } + + @Test + public void normal() { + Observable.range(1, 10) + .flatMapCompletable(new Function<Integer, CompletableSource>() { + @Override + public CompletableSource apply(Integer v) throws Exception { + return Completable.complete(); + } + }) + .test() + .assertResult(); + } + + @Test + public void mapperThrows() { + PublishSubject<Integer> ps = PublishSubject.create(); + + TestObserver<Void> to = ps + .flatMapCompletable(new Function<Integer, CompletableSource>() { + @Override + public CompletableSource apply(Integer v) throws Exception { + throw new TestException(); + } + }) + .test(); + + assertTrue(ps.hasObservers()); + + ps.onNext(1); + + to.assertFailure(TestException.class); + + assertFalse(ps.hasObservers()); + } + + @Test + public void mapperReturnsNull() { + PublishSubject<Integer> ps = PublishSubject.create(); + + TestObserver<Void> to = ps + .flatMapCompletable(new Function<Integer, CompletableSource>() { + @Override + public CompletableSource apply(Integer v) throws Exception { + return null; + } + }) + .test(); + + assertTrue(ps.hasObservers()); + + ps.onNext(1); + + to.assertFailure(NullPointerException.class); + + assertFalse(ps.hasObservers()); + } + + @Test + public void normalDelayError() { + Observable.range(1, 10) + .flatMapCompletable(new Function<Integer, CompletableSource>() { + @Override + public CompletableSource apply(Integer v) throws Exception { + return Completable.complete(); + } + }, true) + .test() + .assertResult(); + } + + @Test + public void normalAsync() { + Observable.range(1, 1000) + .flatMapCompletable(new Function<Integer, CompletableSource>() { + @Override + public CompletableSource apply(Integer v) throws Exception { + return Observable.range(1, 100).subscribeOn(Schedulers.computation()).ignoreElements(); + } + }) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(); + } + + @Test + public void normalDelayErrorAll() { + TestObserverEx<Void> to = Observable.range(1, 10).concatWith(Observable.<Integer>error(new TestException())) + .flatMapCompletable(new Function<Integer, CompletableSource>() { + @Override + public CompletableSource apply(Integer v) throws Exception { + return Completable.error(new TestException()); + } + }, true) + .to(TestHelper.<Void>testConsumer()) + .assertFailure(CompositeException.class); + + List<Throwable> errors = TestHelper.compositeList(to.errors().get(0)); + + for (int i = 0; i < 11; i++) { + TestHelper.assertError(errors, i, TestException.class); + } + } + + @Test + public void normalDelayInnerErrorAll() { + TestObserverEx<Void> to = Observable.range(1, 10) + .flatMapCompletable(new Function<Integer, CompletableSource>() { + @Override + public CompletableSource apply(Integer v) throws Exception { + return Completable.error(new TestException()); + } + }, true) + .to(TestHelper.<Void>testConsumer()) + .assertFailure(CompositeException.class); + + List<Throwable> errors = TestHelper.compositeList(to.errors().get(0)); + + for (int i = 0; i < 10; i++) { + TestHelper.assertError(errors, i, TestException.class); + } + } + + @Test + public void normalNonDelayErrorOuter() { + Observable.range(1, 10).concatWith(Observable.<Integer>error(new TestException())) + .flatMapCompletable(new Function<Integer, CompletableSource>() { + @Override + public CompletableSource apply(Integer v) throws Exception { + return Completable.complete(); + } + }, false) + .test() + .assertFailure(TestException.class); + } + + @Test + public void fused() { + TestObserverEx<Integer> to = new TestObserverEx<>(QueueFuseable.ANY); + + Observable.range(1, 10) + .flatMapCompletable(new Function<Integer, CompletableSource>() { + @Override + public CompletableSource apply(Integer v) throws Exception { + return Completable.complete(); + } + }) + .<Integer>toObservable() + .subscribe(to); + + to + .assertFuseable() + .assertFusionMode(QueueFuseable.ASYNC) + .assertResult(); + } + + @Test + public void disposed() { + TestHelper.checkDisposed(Observable.range(1, 10) + .flatMapCompletable(new Function<Integer, CompletableSource>() { + @Override + public CompletableSource apply(Integer v) throws Exception { + return Completable.complete(); + } + })); + } + + @Test + public void innerObserver() { + Observable.range(1, 3) + .flatMapCompletable(new Function<Integer, CompletableSource>() { + @Override + public CompletableSource apply(Integer v) throws Exception { + return new Completable() { + @Override + protected void subscribeActual(CompletableObserver observer) { + observer.onSubscribe(Disposable.empty()); + + assertFalse(((Disposable)observer).isDisposed()); + + ((Disposable)observer).dispose(); + + assertTrue(((Disposable)observer).isDisposed()); + } + }; + } + }) + .test(); + } + + @Test + public void badSource() { + TestHelper.checkBadSourceObservable(new Function<Observable<Integer>, Object>() { + @Override + public Object apply(Observable<Integer> o) throws Exception { + return o.flatMapCompletable(new Function<Integer, CompletableSource>() { + @Override + public CompletableSource apply(Integer v) throws Exception { + return Completable.complete(); + } + }); + } + }, false, 1, null); + } + + @Test + public void fusedInternalsObservable() { + Observable.range(1, 10) + .flatMapCompletable(new Function<Integer, CompletableSource>() { + @Override + public CompletableSource apply(Integer v) throws Exception { + return Completable.complete(); + } + }) + .toObservable() + .subscribe(new Observer<Object>() { + @Override + public void onSubscribe(Disposable d) { + QueueDisposable<?> qd = (QueueDisposable<?>)d; + try { + assertNull(qd.poll()); + } catch (Throwable ex) { + throw new RuntimeException(ex); + } + assertTrue(qd.isEmpty()); + qd.clear(); + } + + @Override + public void onNext(Object t) { + } + + @Override + public void onError(Throwable t) { + } + + @Override + public void onComplete() { + } + }); + } + + @Test + public void innerObserverObservable() { + Observable.range(1, 3) + .flatMapCompletable(new Function<Integer, CompletableSource>() { + @Override + public CompletableSource apply(Integer v) throws Exception { + return new Completable() { + @Override + protected void subscribeActual(CompletableObserver observer) { + observer.onSubscribe(Disposable.empty()); + + assertFalse(((Disposable)observer).isDisposed()); + + ((Disposable)observer).dispose(); + + assertTrue(((Disposable)observer).isDisposed()); + } + }; + } + }) + .toObservable() + .test(); + } + + @Test + public void badSourceObservable() { + TestHelper.checkBadSourceObservable(new Function<Observable<Integer>, Object>() { + @Override + public Object apply(Observable<Integer> o) throws Exception { + return o.flatMapCompletable(new Function<Integer, CompletableSource>() { + @Override + public CompletableSource apply(Integer v) throws Exception { + return Completable.complete(); + } + }).toObservable(); + } + }, false, 1, null); + } + + @Test + public void undeliverableUponCancel() { + TestHelper.checkUndeliverableUponCancel(new ObservableConverter<Integer, Completable>() { + @Override + public Completable apply(Observable<Integer> upstream) { + return upstream.flatMapCompletable(new Function<Integer, Completable>() { + @Override + public Completable apply(Integer v) throws Throwable { + return Completable.complete().hide(); + } + }); + } + }); + } + + @Test + public void undeliverableUponCancelDelayError() { + TestHelper.checkUndeliverableUponCancel(new ObservableConverter<Integer, Completable>() { + @Override + public Completable apply(Observable<Integer> upstream) { + return upstream.flatMapCompletable(new Function<Integer, Completable>() { + @Override + public Completable apply(Integer v) throws Throwable { + return Completable.complete().hide(); + } + }, true); + } + }); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeObservable(o -> o.flatMapCompletable(v -> Completable.never()).toObservable()); + } + + @Test + public void doubleOnSubscribeCompletable() { + TestHelper.checkDoubleOnSubscribeObservableToCompletable(o -> o.flatMapCompletable(v -> Completable.never())); + } + + @Test + public void cancelWhileMapping() throws Throwable { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + PublishSubject<Integer> ps1 = PublishSubject.create(); + + TestObserver<Object> to = new TestObserver<>(); + CountDownLatch cdl = new CountDownLatch(1); + + ps1.flatMapCompletable(v -> { + TestHelper.raceOther(() -> { + to.dispose(); + }, cdl); + return Completable.complete(); + }) + .toObservable() + .subscribe(to); + + ps1.onNext(1); + + cdl.await(); + } + } + + @Test + public void cancelWhileMappingCompletable() throws Throwable { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + PublishSubject<Integer> ps1 = PublishSubject.create(); + + TestObserver<Void> to = new TestObserver<>(); + CountDownLatch cdl = new CountDownLatch(1); + + ps1.flatMapCompletable(v -> { + TestHelper.raceOther(() -> { + to.dispose(); + }, cdl); + return Completable.complete(); + }) + .subscribe(to); + + ps1.onNext(1); + + cdl.await(); + } + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableFlatMapMaybeTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableFlatMapMaybeTest.java new file mode 100644 index 0000000000..0c0d29c8e7 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableFlatMapMaybeTest.java @@ -0,0 +1,546 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import static org.junit.Assert.*; + +import java.util.List; +import java.util.concurrent.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.schedulers.Schedulers; +import io.reactivex.rxjava3.subjects.*; +import io.reactivex.rxjava3.testsupport.*; + +public class ObservableFlatMapMaybeTest extends RxJavaTest { + + @Test + public void normal() { + Observable.range(1, 10) + .flatMapMaybe(new Function<Integer, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Integer v) throws Exception { + return Maybe.just(v); + } + }) + .test() + .assertResult(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + } + + @Test + public void normalEmpty() { + Observable.range(1, 10) + .flatMapMaybe(new Function<Integer, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Integer v) throws Exception { + return Maybe.empty(); + } + }) + .test() + .assertResult(); + } + + @Test + public void normalDelayError() { + Observable.range(1, 10) + .flatMapMaybe(new Function<Integer, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Integer v) throws Exception { + return Maybe.just(v); + } + }, true) + .test() + .assertResult(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + } + + @Test + public void normalAsync() { + TestObserverEx<Integer> to = Observable.range(1, 10) + .flatMapMaybe(new Function<Integer, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Integer v) throws Exception { + return Maybe.just(v).subscribeOn(Schedulers.computation()); + } + }) + .to(TestHelper.<Integer>testConsumer()) + .awaitDone(5, TimeUnit.SECONDS) + .assertSubscribed() + .assertNoErrors() + .assertComplete(); + + TestHelper.assertValueSet(to, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + } + + @Test + public void mapperThrowsObservable() { + PublishSubject<Integer> ps = PublishSubject.create(); + + TestObserver<Integer> to = ps + .flatMapMaybe(new Function<Integer, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Integer v) throws Exception { + throw new TestException(); + } + }) + .test(); + + assertTrue(ps.hasObservers()); + + ps.onNext(1); + + to.assertFailure(TestException.class); + + assertFalse(ps.hasObservers()); + } + + @Test + public void mapperReturnsNullObservable() { + PublishSubject<Integer> ps = PublishSubject.create(); + + TestObserver<Integer> to = ps + .flatMapMaybe(new Function<Integer, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Integer v) throws Exception { + return null; + } + }) + .test(); + + assertTrue(ps.hasObservers()); + + ps.onNext(1); + + to.assertFailure(NullPointerException.class); + + assertFalse(ps.hasObservers()); + } + + @Test + public void normalDelayErrorAll() { + TestObserverEx<Integer> to = Observable.range(1, 10) + .concatWith(Observable.<Integer>error(new TestException())) + .flatMapMaybe(new Function<Integer, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Integer v) throws Exception { + return Maybe.error(new TestException()); + } + }, true) + .to(TestHelper.<Integer>testConsumer()) + .assertFailure(CompositeException.class); + + List<Throwable> errors = TestHelper.compositeList(to.errors().get(0)); + + for (int i = 0; i < 11; i++) { + TestHelper.assertError(errors, i, TestException.class); + } + } + + @Test + public void takeAsync() { + TestObserverEx<Integer> to = Observable.range(1, 10) + .flatMapMaybe(new Function<Integer, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Integer v) throws Exception { + return Maybe.just(v).subscribeOn(Schedulers.computation()); + } + }) + .take(2) + .to(TestHelper.<Integer>testConsumer()) + .awaitDone(5, TimeUnit.SECONDS) + .assertSubscribed() + .assertValueCount(2) + .assertNoErrors() + .assertComplete(); + + TestHelper.assertValueSet(to, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + } + + @Test + public void take() { + Observable.range(1, 10) + .flatMapMaybe(new Function<Integer, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Integer v) throws Exception { + return Maybe.just(v); + } + }) + .take(2) + .test() + .assertResult(1, 2); + } + + @Test + public void middleError() { + Observable.fromArray(new String[]{"1", "a", "2"}).flatMapMaybe(new Function<String, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(final String s) throws NumberFormatException { + //return Single.just(Integer.valueOf(s)); //This works + return Maybe.fromCallable(new Callable<Integer>() { + @Override + public Integer call() throws NumberFormatException { + return Integer.valueOf(s); + } + }); + } + }) + .test() + .assertFailure(NumberFormatException.class, 1); + } + + @Test + public void asyncFlatten() { + Observable.range(1, 1000) + .flatMapMaybe(new Function<Integer, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Integer v) throws Exception { + return Maybe.just(1).subscribeOn(Schedulers.computation()); + } + }) + .take(500) + .to(TestHelper.<Integer>testConsumer()) + .awaitDone(5, TimeUnit.SECONDS) + .assertSubscribed() + .assertValueCount(500) + .assertNoErrors() + .assertComplete(); + } + + @Test + public void asyncFlattenNone() { + Observable.range(1, 1000) + .flatMapMaybe(new Function<Integer, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Integer v) throws Exception { + return Maybe.<Integer>empty().subscribeOn(Schedulers.computation()); + } + }) + .take(500) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(); + } + + @Test + public void successError() { + final PublishSubject<Integer> ps = PublishSubject.create(); + + TestObserver<Integer> to = Observable.range(1, 2) + .flatMapMaybe(new Function<Integer, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Integer v) throws Exception { + if (v == 2) { + return ps.singleElement(); + } + return Maybe.error(new TestException()); + } + }, true) + .test(); + + ps.onNext(1); + ps.onComplete(); + + to + .assertFailure(TestException.class, 1); + } + + @Test + public void completeError() { + final PublishSubject<Integer> ps = PublishSubject.create(); + + TestObserver<Integer> to = Observable.range(1, 2) + .flatMapMaybe(new Function<Integer, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Integer v) throws Exception { + if (v == 2) { + return ps.singleElement(); + } + return Maybe.error(new TestException()); + } + }, true) + .test(); + + ps.onComplete(); + + to + .assertFailure(TestException.class); + } + + @Test + public void disposed() { + TestHelper.checkDisposed(PublishSubject.<Integer>create().flatMapMaybe(new Function<Integer, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Integer v) throws Exception { + return Maybe.<Integer>empty(); + } + })); + } + + @Test + public void innerSuccessCompletesAfterMain() { + PublishSubject<Integer> ps = PublishSubject.create(); + + TestObserver<Integer> to = Observable.just(1).flatMapMaybe(Functions.justFunction(ps.singleElement())) + .test(); + + ps.onNext(2); + ps.onComplete(); + + to + .assertResult(2); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeObservable(new Function<Observable<Object>, ObservableSource<Integer>>() { + @Override + public ObservableSource<Integer> apply(Observable<Object> f) throws Exception { + return f.flatMapMaybe(Functions.justFunction(Maybe.just(2))); + } + }); + } + + @Test + public void badSource() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + new Observable<Integer>() { + @Override + protected void subscribeActual(Observer<? super Integer> observer) { + observer.onSubscribe(Disposable.empty()); + observer.onError(new TestException("First")); + observer.onError(new TestException("Second")); + } + } + .flatMapMaybe(Functions.justFunction(Maybe.just(2))) + .to(TestHelper.<Integer>testConsumer()) + .assertFailureAndMessage(TestException.class, "First"); + + TestHelper.assertUndeliverable(errors, 0, TestException.class, "Second"); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void badInnerSource() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + Observable.just(1) + .flatMapMaybe(Functions.justFunction(new Maybe<Integer>() { + @Override + protected void subscribeActual(MaybeObserver<? super Integer> observer) { + observer.onSubscribe(Disposable.empty()); + observer.onError(new TestException("First")); + observer.onError(new TestException("Second")); + } + })) + .to(TestHelper.<Integer>testConsumer()) + .assertFailureAndMessage(TestException.class, "First"); + + TestHelper.assertUndeliverable(errors, 0, TestException.class, "Second"); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void emissionQueueTrigger() { + final PublishSubject<Integer> ps1 = PublishSubject.create(); + final PublishSubject<Integer> ps2 = PublishSubject.create(); + + TestObserver<Integer> to = new TestObserver<Integer>() { + @Override + public void onNext(Integer t) { + super.onNext(t); + if (t == 1) { + ps2.onNext(2); + ps2.onComplete(); + } + } + }; + + Observable.just(ps1, ps2) + .flatMapMaybe(new Function<PublishSubject<Integer>, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(PublishSubject<Integer> v) throws Exception { + return v.singleElement(); + } + }) + .subscribe(to); + + ps1.onNext(1); + ps1.onComplete(); + + to.assertResult(1, 2); + } + + @Test + public void emissionQueueTrigger2() { + final PublishSubject<Integer> ps1 = PublishSubject.create(); + final PublishSubject<Integer> ps2 = PublishSubject.create(); + final PublishSubject<Integer> ps3 = PublishSubject.create(); + + TestObserver<Integer> to = new TestObserver<Integer>() { + @Override + public void onNext(Integer t) { + super.onNext(t); + if (t == 1) { + ps2.onNext(2); + ps2.onComplete(); + } + } + }; + + Observable.just(ps1, ps2, ps3) + .flatMapMaybe(new Function<PublishSubject<Integer>, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(PublishSubject<Integer> v) throws Exception { + return v.singleElement(); + } + }) + .subscribe(to); + + ps1.onNext(1); + ps1.onComplete(); + + ps3.onComplete(); + + to.assertResult(1, 2); + } + + @Test + public void disposeInner() { + final TestObserver<Object> to = new TestObserver<>(); + + Observable.just(1).flatMapMaybe(new Function<Integer, MaybeSource<Object>>() { + @Override + public MaybeSource<Object> apply(Integer v) throws Exception { + return new Maybe<Object>() { + @Override + protected void subscribeActual(MaybeObserver<? super Object> observer) { + observer.onSubscribe(Disposable.empty()); + + assertFalse(((Disposable)observer).isDisposed()); + + to.dispose(); + + assertTrue(((Disposable)observer).isDisposed()); + } + }; + } + }) + .subscribe(to); + + to + .assertEmpty(); + } + + @Test + public void undeliverableUponCancel() { + TestHelper.checkUndeliverableUponCancel(new ObservableConverter<Integer, Observable<Integer>>() { + @Override + public Observable<Integer> apply(Observable<Integer> upstream) { + return upstream.flatMapMaybe(new Function<Integer, Maybe<Integer>>() { + @Override + public Maybe<Integer> apply(Integer v) throws Throwable { + return Maybe.just(v).hide(); + } + }); + } + }); + } + + @Test + public void undeliverableUponCancelDelayError() { + TestHelper.checkUndeliverableUponCancel(new ObservableConverter<Integer, Observable<Integer>>() { + @Override + public Observable<Integer> apply(Observable<Integer> upstream) { + return upstream.flatMapMaybe(new Function<Integer, Maybe<Integer>>() { + @Override + public Maybe<Integer> apply(Integer v) throws Throwable { + return Maybe.just(v).hide(); + } + }, true); + } + }); + } + + @Test + public void cancelWhileMapping() throws Throwable { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + PublishSubject<Integer> ps1 = PublishSubject.create(); + + TestObserver<Integer> to = new TestObserver<>(); + CountDownLatch cdl = new CountDownLatch(1); + + ps1.flatMapMaybe(v -> { + TestHelper.raceOther(() -> { + to.dispose(); + }, cdl); + return Maybe.just(1); + }) + .subscribe(to); + + ps1.onNext(1); + + cdl.await(); + } + } + + @Test + public void successCompleteRace() { + for (int i = 0; i < TestHelper.RACE_LONG_LOOPS; i++) { + MaybeSubject<Integer> ms1 = MaybeSubject.create(); + MaybeSubject<Integer> ms2 = MaybeSubject.create(); + + TestObserver<Integer> to = Observable.just(1, 2) + .flatMapMaybe(v -> v == 1 ? ms1 : ms2) + .test(); + + TestHelper.race( + () -> ms1.onComplete(), + () -> ms2.onSuccess(1) + ); + + to.assertResult(1); + } + } + + @Test + public void successCompleteRace2() { + for (int i = 0; i < TestHelper.RACE_LONG_LOOPS; i++) { + MaybeSubject<Integer> ms1 = MaybeSubject.create(); + MaybeSubject<Integer> ms2 = MaybeSubject.create(); + + TestObserver<Integer> to = Observable.just(1, 2) + .flatMapMaybe(v -> v == 1 ? ms1 : ms2) + .test(); + + TestHelper.race( + () -> ms2.onSuccess(1), + () -> ms1.onComplete() + ); + + to.assertResult(1); + } + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableFlatMapSingleTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableFlatMapSingleTest.java new file mode 100644 index 0000000000..1fa219111b --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableFlatMapSingleTest.java @@ -0,0 +1,466 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import static org.junit.Assert.*; + +import java.util.List; +import java.util.concurrent.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.schedulers.Schedulers; +import io.reactivex.rxjava3.subjects.*; +import io.reactivex.rxjava3.testsupport.*; + +public class ObservableFlatMapSingleTest extends RxJavaTest { + + @Test + public void normal() { + Observable.range(1, 10) + .flatMapSingle(new Function<Integer, SingleSource<Integer>>() { + @Override + public SingleSource<Integer> apply(Integer v) throws Exception { + return Single.just(v); + } + }) + .test() + .assertResult(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + } + + @Test + public void normalDelayError() { + Observable.range(1, 10) + .flatMapSingle(new Function<Integer, SingleSource<Integer>>() { + @Override + public SingleSource<Integer> apply(Integer v) throws Exception { + return Single.just(v); + } + }, true) + .test() + .assertResult(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + } + + @Test + public void normalAsync() { + TestObserverEx<Integer> to = Observable.range(1, 10) + .flatMapSingle(new Function<Integer, SingleSource<Integer>>() { + @Override + public SingleSource<Integer> apply(Integer v) throws Exception { + return Single.just(v).subscribeOn(Schedulers.computation()); + } + }) + .to(TestHelper.<Integer>testConsumer()) + .awaitDone(5, TimeUnit.SECONDS) + .assertSubscribed() + .assertNoErrors() + .assertComplete(); + + TestHelper.assertValueSet(to, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + } + + @Test + public void mapperThrowsObservable() { + PublishSubject<Integer> ps = PublishSubject.create(); + + TestObserver<Integer> to = ps + .flatMapSingle(new Function<Integer, SingleSource<Integer>>() { + @Override + public SingleSource<Integer> apply(Integer v) throws Exception { + throw new TestException(); + } + }) + .test(); + + assertTrue(ps.hasObservers()); + + ps.onNext(1); + + to.assertFailure(TestException.class); + + assertFalse(ps.hasObservers()); + } + + @Test + public void mapperReturnsNullObservable() { + PublishSubject<Integer> ps = PublishSubject.create(); + + TestObserver<Integer> to = ps + .flatMapSingle(new Function<Integer, SingleSource<Integer>>() { + @Override + public SingleSource<Integer> apply(Integer v) throws Exception { + return null; + } + }) + .test(); + + assertTrue(ps.hasObservers()); + + ps.onNext(1); + + to.assertFailure(NullPointerException.class); + + assertFalse(ps.hasObservers()); + } + + @Test + public void normalDelayErrorAll() { + TestObserverEx<Integer> to = Observable.range(1, 10).concatWith(Observable.<Integer>error(new TestException())) + .flatMapSingle(new Function<Integer, SingleSource<Integer>>() { + @Override + public SingleSource<Integer> apply(Integer v) throws Exception { + return Single.error(new TestException()); + } + }, true) + .to(TestHelper.<Integer>testConsumer()) + .assertFailure(CompositeException.class); + + List<Throwable> errors = TestHelper.compositeList(to.errors().get(0)); + + for (int i = 0; i < 11; i++) { + TestHelper.assertError(errors, i, TestException.class); + } + } + + @Test + public void takeAsync() { + TestObserverEx<Integer> to = Observable.range(1, 10) + .flatMapSingle(new Function<Integer, SingleSource<Integer>>() { + @Override + public SingleSource<Integer> apply(Integer v) throws Exception { + return Single.just(v).subscribeOn(Schedulers.computation()); + } + }) + .take(2) + .to(TestHelper.<Integer>testConsumer()) + .awaitDone(5, TimeUnit.SECONDS) + .assertSubscribed() + .assertValueCount(2) + .assertNoErrors() + .assertComplete(); + + TestHelper.assertValueSet(to, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + } + + @Test + public void take() { + Observable.range(1, 10) + .flatMapSingle(new Function<Integer, SingleSource<Integer>>() { + @Override + public SingleSource<Integer> apply(Integer v) throws Exception { + return Single.just(v); + } + }) + .take(2) + .test() + .assertResult(1, 2); + } + + @Test + public void middleError() { + Observable.fromArray(new String[]{"1", "a", "2"}).flatMapSingle(new Function<String, SingleSource<Integer>>() { + @Override + public SingleSource<Integer> apply(final String s) throws NumberFormatException { + //return Single.just(Integer.valueOf(s)); //This works + return Single.fromCallable(new Callable<Integer>() { + @Override + public Integer call() throws NumberFormatException { + return Integer.valueOf(s); + } + }); + } + }) + .test() + .assertFailure(NumberFormatException.class, 1); + } + + @Test + public void asyncFlatten() { + Observable.range(1, 1000) + .flatMapSingle(new Function<Integer, SingleSource<Integer>>() { + @Override + public SingleSource<Integer> apply(Integer v) throws Exception { + return Single.just(1).subscribeOn(Schedulers.computation()); + } + }) + .take(500) + .to(TestHelper.<Integer>testConsumer()) + .awaitDone(5, TimeUnit.SECONDS) + .assertSubscribed() + .assertValueCount(500) + .assertNoErrors() + .assertComplete(); + } + + @Test + public void successError() { + final PublishSubject<Integer> ps = PublishSubject.create(); + + TestObserver<Integer> to = Observable.range(1, 2) + .flatMapSingle(new Function<Integer, SingleSource<Integer>>() { + @Override + public SingleSource<Integer> apply(Integer v) throws Exception { + if (v == 2) { + return ps.singleOrError(); + } + return Single.error(new TestException()); + } + }, true) + .test(); + + ps.onNext(1); + ps.onComplete(); + + to + .assertFailure(TestException.class, 1); + } + + @Test + public void disposed() { + TestHelper.checkDisposed(PublishSubject.<Integer>create().flatMapSingle(new Function<Integer, SingleSource<Integer>>() { + @Override + public SingleSource<Integer> apply(Integer v) throws Exception { + return Single.<Integer>just(1); + } + })); + } + + @Test + public void innerSuccessCompletesAfterMain() { + PublishSubject<Integer> ps = PublishSubject.create(); + + TestObserver<Integer> to = Observable.just(1).flatMapSingle(Functions.justFunction(ps.singleOrError())) + .test(); + + ps.onNext(2); + ps.onComplete(); + + to + .assertResult(2); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeObservable(new Function<Observable<Object>, ObservableSource<Integer>>() { + @Override + public ObservableSource<Integer> apply(Observable<Object> f) throws Exception { + return f.flatMapSingle(Functions.justFunction(Single.just(2))); + } + }); + } + + @Test + public void badSource() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + new Observable<Integer>() { + @Override + protected void subscribeActual(Observer<? super Integer> observer) { + observer.onSubscribe(Disposable.empty()); + observer.onError(new TestException("First")); + observer.onError(new TestException("Second")); + } + } + .flatMapSingle(Functions.justFunction(Single.just(2))) + .to(TestHelper.<Integer>testConsumer()) + .assertFailureAndMessage(TestException.class, "First"); + + TestHelper.assertUndeliverable(errors, 0, TestException.class, "Second"); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void badInnerSource() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + Observable.just(1) + .flatMapSingle(Functions.justFunction(new Single<Integer>() { + @Override + protected void subscribeActual(SingleObserver<? super Integer> observer) { + observer.onSubscribe(Disposable.empty()); + observer.onError(new TestException("First")); + observer.onError(new TestException("Second")); + } + })) + .to(TestHelper.<Integer>testConsumer()) + .assertFailureAndMessage(TestException.class, "First"); + + TestHelper.assertUndeliverable(errors, 0, TestException.class, "Second"); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void emissionQueueTrigger() { + final PublishSubject<Integer> ps1 = PublishSubject.create(); + final PublishSubject<Integer> ps2 = PublishSubject.create(); + + TestObserver<Integer> to = new TestObserver<Integer>() { + @Override + public void onNext(Integer t) { + super.onNext(t); + if (t == 1) { + ps2.onNext(2); + ps2.onComplete(); + } + } + }; + + Observable.just(ps1, ps2) + .flatMapSingle(new Function<PublishSubject<Integer>, SingleSource<Integer>>() { + @Override + public SingleSource<Integer> apply(PublishSubject<Integer> v) throws Exception { + return v.singleOrError(); + } + }) + .subscribe(to); + + ps1.onNext(1); + ps1.onComplete(); + + to.assertResult(1, 2); + } + + @Test + public void disposeInner() { + final TestObserver<Object> to = new TestObserver<>(); + + Observable.just(1).flatMapSingle(new Function<Integer, SingleSource<Object>>() { + @Override + public SingleSource<Object> apply(Integer v) throws Exception { + return new Single<Object>() { + @Override + protected void subscribeActual(SingleObserver<? super Object> observer) { + observer.onSubscribe(Disposable.empty()); + + assertFalse(((Disposable)observer).isDisposed()); + + to.dispose(); + + assertTrue(((Disposable)observer).isDisposed()); + } + }; + } + }) + .subscribe(to); + + to + .assertEmpty(); + } + + @Test + public void undeliverableUponCancel() { + TestHelper.checkUndeliverableUponCancel(new ObservableConverter<Integer, Observable<Integer>>() { + @Override + public Observable<Integer> apply(Observable<Integer> upstream) { + return upstream.flatMapSingle(new Function<Integer, Single<Integer>>() { + @Override + public Single<Integer> apply(Integer v) throws Throwable { + return Single.just(v).hide(); + } + }); + } + }); + } + + @Test + public void undeliverableUponCancelDelayError() { + TestHelper.checkUndeliverableUponCancel(new ObservableConverter<Integer, Observable<Integer>>() { + @Override + public Observable<Integer> apply(Observable<Integer> upstream) { + return upstream.flatMapSingle(new Function<Integer, Single<Integer>>() { + @Override + public Single<Integer> apply(Integer v) throws Throwable { + return Single.just(v).hide(); + } + }, true); + } + }); + } + + @Test + public void innerErrorOuterCompleteRace() { + TestException ex = new TestException(); + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + PublishSubject<Integer> ps1 = PublishSubject.create(); + SingleSubject<Integer> ps2 = SingleSubject.create(); + + TestObserver<Integer> to = ps1.flatMapSingle(v -> ps2) + .test(); + + ps1.onNext(1); + + TestHelper.race( + () -> ps1.onComplete(), + () -> ps2.onError(ex) + ); + + to.assertFailure(TestException.class); + } + } + + @Test + public void cancelWhileMapping() throws Throwable { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + PublishSubject<Integer> ps1 = PublishSubject.create(); + + TestObserver<Integer> to = new TestObserver<>(); + CountDownLatch cdl = new CountDownLatch(1); + + ps1.flatMapSingle(v -> { + TestHelper.raceOther(() -> { + to.dispose(); + }, cdl); + return Single.just(1); + }) + .subscribe(to); + + ps1.onNext(1); + + cdl.await(); + } + } + + @Test + public void onNextDrainCancel() { + SingleSubject<Integer> ss1 = SingleSubject.create(); + SingleSubject<Integer> ss2 = SingleSubject.create(); + + TestObserver<Integer> to = new TestObserver<>(); + + Observable.just(1, 2) + .flatMapSingle(v -> v == 1 ? ss1 : ss2) + .doOnNext(v -> { + if (v == 1) { + ss2.onSuccess(2); + to.dispose(); + } + }) + .subscribe(to); + + ss1.onSuccess(1); + + to.assertValuesOnly(1); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableFlatMapTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableFlatMapTest.java new file mode 100644 index 0000000000..4622888628 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableFlatMapTest.java @@ -0,0 +1,1267 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import java.io.IOException; +import java.util.*; +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.*; + +import io.reactivex.rxjava3.annotations.NonNull; +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.core.Observer; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.schedulers.Schedulers; +import io.reactivex.rxjava3.subjects.*; +import io.reactivex.rxjava3.testsupport.*; + +public class ObservableFlatMapTest extends RxJavaTest { + @Test + public void normal() { + Observer<Object> o = TestHelper.mockObserver(); + + final List<Integer> list = Arrays.asList(1, 2, 3); + + Function<Integer, List<Integer>> func = new Function<Integer, List<Integer>>() { + @Override + public List<Integer> apply(Integer t1) { + return list; + } + }; + BiFunction<Integer, Integer, Integer> resFunc = new BiFunction<Integer, Integer, Integer>() { + + @Override + public Integer apply(Integer t1, Integer t2) { + return t1 | t2; + } + }; + + List<Integer> source = Arrays.asList(16, 32, 64); + + Observable.fromIterable(source).flatMapIterable(func, resFunc).subscribe(o); + + for (Integer s : source) { + for (Integer v : list) { + verify(o).onNext(s | v); + } + } + verify(o).onComplete(); + verify(o, never()).onError(any(Throwable.class)); + } + + @Test + public void collectionFunctionThrows() { + Observer<Object> o = TestHelper.mockObserver(); + + Function<Integer, List<Integer>> func = new Function<Integer, List<Integer>>() { + @Override + public List<Integer> apply(Integer t1) { + throw new TestException(); + } + }; + BiFunction<Integer, Integer, Integer> resFunc = new BiFunction<Integer, Integer, Integer>() { + + @Override + public Integer apply(Integer t1, Integer t2) { + return t1 | t2; + } + }; + + List<Integer> source = Arrays.asList(16, 32, 64); + + Observable.fromIterable(source).flatMapIterable(func, resFunc).subscribe(o); + + verify(o, never()).onComplete(); + verify(o, never()).onNext(any()); + verify(o).onError(any(TestException.class)); + } + + @Test + public void resultFunctionThrows() { + Observer<Object> o = TestHelper.mockObserver(); + + final List<Integer> list = Arrays.asList(1, 2, 3); + + Function<Integer, List<Integer>> func = new Function<Integer, List<Integer>>() { + @Override + public List<Integer> apply(Integer t1) { + return list; + } + }; + BiFunction<Integer, Integer, Integer> resFunc = new BiFunction<Integer, Integer, Integer>() { + + @Override + public Integer apply(Integer t1, Integer t2) { + throw new TestException(); + } + }; + + List<Integer> source = Arrays.asList(16, 32, 64); + + Observable.fromIterable(source).flatMapIterable(func, resFunc).subscribe(o); + + verify(o, never()).onComplete(); + verify(o, never()).onNext(any()); + verify(o).onError(any(TestException.class)); + } + + @Test + public void mergeError() { + Observer<Object> o = TestHelper.mockObserver(); + + Function<Integer, Observable<Integer>> func = new Function<Integer, Observable<Integer>>() { + @Override + public Observable<Integer> apply(Integer t1) { + return Observable.error(new TestException()); + } + }; + BiFunction<Integer, Integer, Integer> resFunc = new BiFunction<Integer, Integer, Integer>() { + + @Override + public Integer apply(Integer t1, Integer t2) { + return t1 | t2; + } + }; + + List<Integer> source = Arrays.asList(16, 32, 64); + + Observable.fromIterable(source).flatMap(func, resFunc).subscribe(o); + + verify(o, never()).onComplete(); + verify(o, never()).onNext(any()); + verify(o).onError(any(TestException.class)); + } + + <T, R> Function<T, R> just(final R value) { + return new Function<T, R>() { + + @Override + public R apply(T t1) { + return value; + } + }; + } + + <R> Supplier<R> just0(final R value) { + return new Supplier<R>() { + + @Override + public R get() { + return value; + } + }; + } + + @Test + public void flatMapTransformsNormal() { + Observable<Integer> onNext = Observable.fromIterable(Arrays.asList(1, 2, 3)); + Observable<Integer> onComplete = Observable.fromIterable(Arrays.asList(4)); + Observable<Integer> onError = Observable.fromIterable(Arrays.asList(5)); + + Observable<Integer> source = Observable.fromIterable(Arrays.asList(10, 20, 30)); + + Observer<Object> o = TestHelper.mockObserver(); + + source.flatMap(just(onNext), just(onError), just0(onComplete)).subscribe(o); + + verify(o, times(3)).onNext(1); + verify(o, times(3)).onNext(2); + verify(o, times(3)).onNext(3); + verify(o).onNext(4); + verify(o).onComplete(); + + verify(o, never()).onNext(5); + verify(o, never()).onError(any(Throwable.class)); + } + + @Test + public void flatMapTransformsException() { + Observable<Integer> onNext = Observable.fromIterable(Arrays.asList(1, 2, 3)); + Observable<Integer> onComplete = Observable.fromIterable(Arrays.asList(4)); + Observable<Integer> onError = Observable.fromIterable(Arrays.asList(5)); + + Observable<Integer> source = Observable.concat( + Observable.fromIterable(Arrays.asList(10, 20, 30)), + Observable.<Integer> error(new RuntimeException("Forced failure!")) + ); + + Observer<Object> o = TestHelper.mockObserver(); + + source.flatMap(just(onNext), just(onError), just0(onComplete)).subscribe(o); + + verify(o, times(3)).onNext(1); + verify(o, times(3)).onNext(2); + verify(o, times(3)).onNext(3); + verify(o).onNext(5); + verify(o).onComplete(); + verify(o, never()).onNext(4); + + verify(o, never()).onError(any(Throwable.class)); + } + + <R> Supplier<R> funcThrow0(R r) { + return new Supplier<R>() { + @Override + public R get() { + throw new TestException(); + } + }; + } + + <T, R> Function<T, R> funcThrow(T t, R r) { + return new Function<T, R>() { + @Override + public R apply(T t) { + throw new TestException(); + } + }; + } + + @Test + public void flatMapTransformsOnNextFuncThrows() { + Observable<Integer> onComplete = Observable.fromIterable(Arrays.asList(4)); + Observable<Integer> onError = Observable.fromIterable(Arrays.asList(5)); + + Observable<Integer> source = Observable.fromIterable(Arrays.asList(10, 20, 30)); + + Observer<Object> o = TestHelper.mockObserver(); + + source.flatMap(funcThrow(1, onError), just(onError), just0(onComplete)).subscribe(o); + + verify(o).onError(any(TestException.class)); + verify(o, never()).onNext(any()); + verify(o, never()).onComplete(); + } + + @Test + public void flatMapTransformsOnErrorFuncThrows() { + Observable<Integer> onNext = Observable.fromIterable(Arrays.asList(1, 2, 3)); + Observable<Integer> onComplete = Observable.fromIterable(Arrays.asList(4)); + Observable<Integer> onError = Observable.fromIterable(Arrays.asList(5)); + + Observable<Integer> source = Observable.error(new TestException()); + + Observer<Object> o = TestHelper.mockObserver(); + + source.flatMap(just(onNext), funcThrow((Throwable) null, onError), just0(onComplete)).subscribe(o); + + verify(o).onError(any(CompositeException.class)); + verify(o, never()).onNext(any()); + verify(o, never()).onComplete(); + } + + @Test + public void flatMapTransformsOnCompletedFuncThrows() { + Observable<Integer> onNext = Observable.fromIterable(Arrays.asList(1, 2, 3)); + Observable<Integer> onComplete = Observable.fromIterable(Arrays.asList(4)); + Observable<Integer> onError = Observable.fromIterable(Arrays.asList(5)); + + Observable<Integer> source = Observable.fromIterable(Arrays.<Integer> asList()); + + Observer<Object> o = TestHelper.mockObserver(); + + source.flatMap(just(onNext), just(onError), funcThrow0(onComplete)).subscribe(o); + + verify(o).onError(any(TestException.class)); + verify(o, never()).onNext(any()); + verify(o, never()).onComplete(); + } + + @Test + public void flatMapTransformsMergeException() { + Observable<Integer> onNext = Observable.error(new TestException()); + Observable<Integer> onComplete = Observable.fromIterable(Arrays.asList(4)); + Observable<Integer> onError = Observable.fromIterable(Arrays.asList(5)); + + Observable<Integer> source = Observable.fromIterable(Arrays.asList(10, 20, 30)); + + Observer<Object> o = TestHelper.mockObserver(); + + source.flatMap(just(onNext), just(onError), funcThrow0(onComplete)).subscribe(o); + + verify(o).onError(any(TestException.class)); + verify(o, never()).onNext(any()); + verify(o, never()).onComplete(); + } + + private static <T> Observable<T> composer(Observable<T> source, final AtomicInteger subscriptionCount, final int m) { + return source.doOnSubscribe(new Consumer<Disposable>() { + @Override + public void accept(Disposable d) { + int n = subscriptionCount.getAndIncrement(); + if (n >= m) { + Assert.fail("Too many subscriptions! " + (n + 1)); + } + } + }).doOnComplete(new Action() { + @Override + public void run() { + int n = subscriptionCount.decrementAndGet(); + if (n < 0) { + Assert.fail("Too many unsubscriptions! " + (n - 1)); + } + } + }); + } + + @Test + public void flatMapMaxConcurrent() { + final int m = 4; + final AtomicInteger subscriptionCount = new AtomicInteger(); + Observable<Integer> source = Observable.range(1, 10) + .flatMap(new Function<Integer, Observable<Integer>>() { + @Override + public Observable<Integer> apply(Integer t1) { + return composer(Observable.range(t1 * 10, 2), subscriptionCount, m) + .subscribeOn(Schedulers.computation()); + } + }, m); + + TestObserver<Integer> to = new TestObserver<>(); + + source.subscribe(to); + + to.awaitDone(5, TimeUnit.SECONDS); + to.assertNoErrors(); + Set<Integer> expected = new HashSet<>(Arrays.asList( + 10, 11, 20, 21, 30, 31, 40, 41, 50, 51, 60, 61, 70, 71, 80, 81, 90, 91, 100, 101 + )); + Assert.assertEquals(expected.size(), to.values().size()); + Assert.assertTrue(expected.containsAll(to.values())); + } + + @Test + public void flatMapSelectorMaxConcurrent() { + final int m = 4; + final AtomicInteger subscriptionCount = new AtomicInteger(); + Observable<Integer> source = Observable.range(1, 10) + .flatMap(new Function<Integer, Observable<Integer>>() { + @Override + public Observable<Integer> apply(Integer t1) { + return composer(Observable.range(t1 * 10, 2), subscriptionCount, m) + .subscribeOn(Schedulers.computation()); + } + }, new BiFunction<Integer, Integer, Integer>() { + @Override + public Integer apply(Integer t1, Integer t2) { + return t1 * 1000 + t2; + } + }, m); + + TestObserver<Integer> to = new TestObserver<>(); + + source.subscribe(to); + + to.awaitDone(5, TimeUnit.SECONDS); + to.assertNoErrors(); + Set<Integer> expected = new HashSet<>(Arrays.asList( + 1010, 1011, 2020, 2021, 3030, 3031, 4040, 4041, 5050, 5051, + 6060, 6061, 7070, 7071, 8080, 8081, 9090, 9091, 10100, 10101 + )); + Assert.assertEquals(expected.size(), to.values().size()); + System.out.println("--> testFlatMapSelectorMaxConcurrent: " + to.values()); + Assert.assertTrue(expected.containsAll(to.values())); + } + + @Test + public void flatMapTransformsMaxConcurrentNormalLoop() { + for (int i = 0; i < 1000; i++) { + if (i % 100 == 0) { + System.out.println("testFlatMapTransformsMaxConcurrentNormalLoop => " + i); + } + flatMapTransformsMaxConcurrentNormal(); + } + } + + @Test + public void flatMapTransformsMaxConcurrentNormal() { + final int m = 2; + final AtomicInteger subscriptionCount = new AtomicInteger(); + Observable<Integer> onNext = + composer( + Observable.fromIterable(Arrays.asList(1, 2, 3)) + .observeOn(Schedulers.computation()) + , + subscriptionCount, m) + .subscribeOn(Schedulers.computation()) + ; + + Observable<Integer> onComplete = composer(Observable.fromIterable(Arrays.asList(4)), subscriptionCount, m) + .subscribeOn(Schedulers.computation()); + + Observable<Integer> onError = Observable.fromIterable(Arrays.asList(5)); + + Observable<Integer> source = Observable.fromIterable(Arrays.asList(10, 20, 30)); + + Observer<Object> o = TestHelper.mockObserver(); + TestObserverEx<Object> to = new TestObserverEx<>(o); + + Function<Throwable, Observable<Integer>> just = just(onError); + source.flatMap(just(onNext), just, just0(onComplete), m).subscribe(to); + + to.awaitDone(1, TimeUnit.SECONDS); + to.assertNoErrors(); + to.assertTerminated(); + + verify(o, times(3)).onNext(1); + verify(o, times(3)).onNext(2); + verify(o, times(3)).onNext(3); + verify(o).onNext(4); + verify(o).onComplete(); + + verify(o, never()).onNext(5); + verify(o, never()).onError(any(Throwable.class)); + } + + @Test + public void flatMapRangeMixedAsyncLoop() { + for (int i = 0; i < 2000; i++) { + if (i % 10 == 0) { + System.out.println("flatMapRangeAsyncLoop > " + i); + } + TestObserverEx<Integer> to = new TestObserverEx<>(); + Observable.range(0, 1000) + .flatMap(new Function<Integer, Observable<Integer>>() { + final Random rnd = new Random(); + @Override + public Observable<Integer> apply(Integer t) { + Observable<Integer> r = Observable.just(t); + if (rnd.nextBoolean()) { + r = r.hide(); + } + return r; + } + }) + .observeOn(Schedulers.computation()) + .subscribe(to); + + to.awaitDone(2500, TimeUnit.MILLISECONDS); + if (to.completions() == 0) { + System.out.println(to.values().size()); + } + to.assertTerminated(); + to.assertNoErrors(); + List<Integer> list = to.values(); + if (list.size() < 1000) { + Set<Integer> set = new HashSet<>(list); + for (int j = 0; j < 1000; j++) { + if (!set.contains(j)) { + System.out.println(j + " missing"); + } + } + } + assertEquals(1000, list.size()); + } + } + + @Test + public void flatMapIntPassthruAsync() { + for (int i = 0; i < 1000; i++) { + TestObserver<Integer> to = new TestObserver<>(); + + Observable.range(1, 1000).flatMap(new Function<Integer, Observable<Integer>>() { + @Override + public Observable<Integer> apply(Integer t) { + return Observable.just(1).subscribeOn(Schedulers.computation()); + } + }).subscribe(to); + + to.awaitDone(5, TimeUnit.SECONDS); + to.assertNoErrors(); + to.assertComplete(); + to.assertValueCount(1000); + } + } + + @Test + public void flatMapTwoNestedSync() { + for (final int n : new int[] { 1, 1000, 1000000 }) { + TestObserver<Integer> to = new TestObserver<>(); + + Observable.just(1, 2).flatMap(new Function<Integer, Observable<Integer>>() { + @Override + public Observable<Integer> apply(Integer t) { + return Observable.range(1, n); + } + }).subscribe(to); + + System.out.println("flatMapTwoNestedSync >> @ " + n); + to.assertNoErrors(); + to.assertComplete(); + to.assertValueCount(n * 2); + } + } + + @Test + public void flatMapBiMapper() { + Observable.just(1) + .flatMap(new Function<Integer, ObservableSource<Integer>>() { + @Override + public ObservableSource<Integer> apply(Integer v) throws Exception { + return Observable.just(v * 10); + } + }, new BiFunction<Integer, Integer, Integer>() { + @Override + public Integer apply(Integer a, Integer b) throws Exception { + return a + b; + } + }, true) + .test() + .assertResult(11); + } + + @Test + public void flatMapBiMapperWithError() { + Observable.just(1) + .flatMap(new Function<Integer, ObservableSource<Integer>>() { + @Override + public ObservableSource<Integer> apply(Integer v) throws Exception { + return Observable.just(v * 10).concatWith(Observable.<Integer>error(new TestException())); + } + }, new BiFunction<Integer, Integer, Integer>() { + @Override + public Integer apply(Integer a, Integer b) throws Exception { + return a + b; + } + }, true) + .test() + .assertFailure(TestException.class, 11); + } + + @Test + public void flatMapBiMapperMaxConcurrency() { + Observable.just(1, 2) + .flatMap(new Function<Integer, ObservableSource<Integer>>() { + @Override + public ObservableSource<Integer> apply(Integer v) throws Exception { + return Observable.just(v * 10); + } + }, new BiFunction<Integer, Integer, Integer>() { + @Override + public Integer apply(Integer a, Integer b) throws Exception { + return a + b; + } + }, true, 1) + .test() + .assertResult(11, 22); + } + + @Test + public void flatMapEmpty() { + assertSame(Observable.empty(), Observable.empty().flatMap(new Function<Object, ObservableSource<Object>>() { + @Override + public ObservableSource<Object> apply(Object v) throws Exception { + return Observable.just(v); + } + })); + } + + @Test + public void mergeScalar() { + Observable.merge(Observable.just(Observable.just(1))) + .test() + .assertResult(1); + } + + @Test + public void mergeScalar2() { + Observable.merge(Observable.just(Observable.just(1)).hide()) + .test() + .assertResult(1); + } + + @Test + public void mergeScalarEmpty() { + Observable.merge(Observable.just(Observable.empty()).hide()) + .test() + .assertResult(); + } + + @Test + public void mergeScalarError() { + Observable.merge(Observable.just(Observable.fromCallable(new Callable<Object>() { + @Override + public Object call() throws Exception { + throw new TestException(); + } + })).hide()) + .test() + .assertFailure(TestException.class); + } + + @Test + public void scalarReentrant() { + final PublishSubject<Observable<Integer>> ps = PublishSubject.create(); + + TestObserver<Integer> to = new TestObserver<Integer>() { + @Override + public void onNext(Integer t) { + super.onNext(t); + if (t == 1) { + ps.onNext(Observable.just(2)); + } + } + }; + + Observable.merge(ps) + .subscribe(to); + + ps.onNext(Observable.just(1)); + ps.onComplete(); + + to.assertResult(1, 2); + } + + @Test + public void scalarReentrant2() { + final PublishSubject<Observable<Integer>> ps = PublishSubject.create(); + + TestObserver<Integer> to = new TestObserver<Integer>() { + @Override + public void onNext(Integer t) { + super.onNext(t); + if (t == 1) { + ps.onNext(Observable.just(2)); + } + } + }; + + Observable.merge(ps, 2) + .subscribe(to); + + ps.onNext(Observable.just(1)); + ps.onComplete(); + + to.assertResult(1, 2); + } + + @Test + public void innerCompleteCancelRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final PublishSubject<Integer> ps = PublishSubject.create(); + + final TestObserver<Integer> to = Observable.merge(Observable.just(ps)).test(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + ps.onComplete(); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + to.dispose(); + } + }; + + TestHelper.race(r1, r2); + } + } + + @Test + public void fusedInnerThrows() { + Observable.just(1).hide() + .flatMap(new Function<Integer, ObservableSource<Object>>() { + @Override + public ObservableSource<Object> apply(Integer v) throws Exception { + return Observable.range(1, 2).map(new Function<Integer, Object>() { + @Override + public Object apply(Integer w) throws Exception { + throw new TestException(); + } + }); + } + }) + .test() + .assertFailure(TestException.class); + } + + @Test + public void fusedInnerThrows2() { + TestObserverEx<Integer> to = Observable.range(1, 2).hide() + .flatMap(new Function<Integer, ObservableSource<Integer>>() { + @Override + public ObservableSource<Integer> apply(Integer v) throws Exception { + return Observable.range(1, 2).map(new Function<Integer, Integer>() { + @Override + public Integer apply(Integer w) throws Exception { + throw new TestException(); + } + }); + } + }, true) + .to(TestHelper.<Integer>testConsumer()) + .assertFailure(CompositeException.class); + + List<Throwable> errors = TestHelper.errorList(to); + + TestHelper.assertError(errors, 0, TestException.class); + + TestHelper.assertError(errors, 1, TestException.class); + } + + @Test + public void noCrossBoundaryFusion() { + for (int i = 0; i < 500; i++) { + TestObserver<Object> to = Observable.merge( + Observable.just(1).observeOn(Schedulers.single()).map(new Function<Integer, Object>() { + @Override + public Object apply(Integer v) throws Exception { + return Thread.currentThread().getName().substring(0, 4); + } + }), + Observable.just(1).observeOn(Schedulers.computation()).map(new Function<Integer, Object>() { + @Override + public Object apply(Integer v) throws Exception { + return Thread.currentThread().getName().substring(0, 4); + } + }) + ) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertValueCount(2); + + List<Object> list = to.values(); + + assertTrue(list.toString(), list.contains("RxSi")); + assertTrue(list.toString(), list.contains("RxCo")); + } + } + + @Test + public void cancelScalarDrainRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + + final PublishSubject<Observable<Integer>> ps = PublishSubject.create(); + + final TestObserver<Integer> to = ps.flatMap(Functions.<Observable<Integer>>identity()).test(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + to.dispose(); + } + }; + Runnable r2 = new Runnable() { + @Override + public void run() { + ps.onComplete(); + } + }; + + TestHelper.race(r1, r2); + + assertTrue(errors.toString(), errors.isEmpty()); + } finally { + RxJavaPlugins.reset(); + } + } + } + + @Test + public void cancelDrainRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + for (int j = 1; j < 50; j += 5) { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + + final PublishSubject<Observable<Integer>> ps = PublishSubject.create(); + + final TestObserver<Integer> to = ps.flatMap(Functions.<Observable<Integer>>identity()).test(); + + final PublishSubject<Integer> just = PublishSubject.create(); + final PublishSubject<Integer> just2 = PublishSubject.create(); + ps.onNext(just); + ps.onNext(just2); + + Runnable r1 = new Runnable() { + @Override + public void run() { + just2.onNext(1); + to.dispose(); + } + }; + Runnable r2 = new Runnable() { + @Override + public void run() { + just.onNext(1); + } + }; + + TestHelper.race(r1, r2); + + assertTrue(errors.toString(), errors.isEmpty()); + } finally { + RxJavaPlugins.reset(); + } + } + } + } + + @Test + public void iterableMapperFunctionReturnsNull() { + Observable.just(1) + .flatMapIterable(new Function<Integer, Iterable<Object>>() { + @Override + public Iterable<Object> apply(Integer v) throws Exception { + return null; + } + }, new BiFunction<Integer, Object, Object>() { + @Override + public Object apply(Integer v, Object w) throws Exception { + return v; + } + }) + .to(TestHelper.<Object>testConsumer()) + .assertFailureAndMessage(NullPointerException.class, "The mapper returned a null Iterable"); + } + + @Test + public void combinerMapperFunctionReturnsNull() { + Observable.just(1) + .flatMap(new Function<Integer, Observable<Object>>() { + @Override + public Observable<Object> apply(Integer v) throws Exception { + return null; + } + }, new BiFunction<Integer, Object, Object>() { + @Override + public Object apply(Integer v, Object w) throws Exception { + return v; + } + }) + .to(TestHelper.<Object>testConsumer()) + .assertFailureAndMessage(NullPointerException.class, "The mapper returned a null ObservableSource"); + } + + @Test + public void failingFusedInnerCancelsSource() { + final AtomicInteger counter = new AtomicInteger(); + Observable.range(1, 5) + .doOnNext(new Consumer<Integer>() { + @Override + public void accept(Integer v) throws Exception { + counter.getAndIncrement(); + } + }) + .flatMap(new Function<Integer, Observable<Integer>>() { + @Override + public Observable<Integer> apply(Integer v) + throws Exception { + return Observable.<Integer>fromIterable(new Iterable<Integer>() { + @Override + public Iterator<Integer> iterator() { + return new Iterator<Integer>() { + @Override + public boolean hasNext() { + return true; + } + + @Override + public Integer next() { + throw new TestException(); + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + }; + } + }); + } + }) + .test() + .assertFailure(TestException.class); + + assertEquals(1, counter.get()); + } + + @Test + public void scalarQueueNoOverflow() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + final PublishSubject<Integer> ps = PublishSubject.create(); + + TestObserver<Integer> to = ps.flatMap(new Function<Integer, Observable<Integer>>() { + @Override + public Observable<Integer> apply(Integer v) + throws Exception { + return Observable.just(v + 1); + } + }, 1) + .subscribeWith(new TestObserver<Integer>() { + @Override + public void onNext(Integer t) { + super.onNext(t); + if (t == 1) { + for (int i = 1; i < 10; i++) { + ps.onNext(i); + } + ps.onComplete(); + } + } + }); + + ps.onNext(0); + + if (!errors.isEmpty()) { + to.onError(new CompositeException(errors)); + } + + to.assertResult(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void scalarQueueNoOverflowHidden() { + final PublishSubject<Integer> ps = PublishSubject.create(); + + TestObserver<Integer> to = ps.flatMap(new Function<Integer, Observable<Integer>>() { + @Override + public Observable<Integer> apply(Integer v) + throws Exception { + return Observable.just(v + 1).hide(); + } + }, 1) + .subscribeWith(new TestObserver<Integer>() { + @Override + public void onNext(Integer t) { + super.onNext(t); + if (t == 1) { + for (int i = 1; i < 10; i++) { + ps.onNext(i); + } + ps.onComplete(); + } + } + }); + + ps.onNext(0); + + to.assertResult(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + } + + @Test + public void fusedSourceCrashResumeWithNextSource() { + final UnicastSubject<Integer> fusedSource = UnicastSubject.create(); + TestObserver<Integer> to = new TestObserver<>(); + + ObservableFlatMap.MergeObserver<Integer, Integer> merger = + new ObservableFlatMap.MergeObserver<>(to, new Function<Integer, Observable<Integer>>() { + @Override + public Observable<Integer> apply(Integer t) + throws Exception { + if (t == 0) { + return fusedSource + .map(new Function<Integer, Integer>() { + @Override + public Integer apply(Integer v) + throws Exception { + throw new TestException(); + } + }) + .compose(TestHelper.<Integer>observableStripBoundary()); + } + return Observable.range(10 * t, 5); + } + }, true, Integer.MAX_VALUE, 128); + + merger.onSubscribe(Disposable.empty()); + merger.getAndIncrement(); + + merger.onNext(0); + merger.onNext(1); + merger.onNext(2); + + assertTrue(fusedSource.hasObservers()); + + fusedSource.onNext(-1); + + merger.drainLoop(); + + to.assertValuesOnly(10, 11, 12, 13, 14, 20, 21, 22, 23, 24); + } + + @Test + public void maxConcurrencySustained() { + final PublishSubject<Integer> ps1 = PublishSubject.create(); + final PublishSubject<Integer> ps2 = PublishSubject.create(); + PublishSubject<Integer> ps3 = PublishSubject.create(); + PublishSubject<Integer> ps4 = PublishSubject.create(); + + TestObserver<Integer> to = Observable.just(ps1, ps2, ps3, ps4) + .flatMap(new Function<PublishSubject<Integer>, ObservableSource<Integer>>() { + @Override + public ObservableSource<Integer> apply(PublishSubject<Integer> v) throws Exception { + return v; + } + }, 2) + .doOnNext(new Consumer<Integer>() { + @Override + public void accept(Integer v) throws Exception { + if (v == 1) { + // this will make sure the drain loop detects two completed + // inner sources and replaces them with fresh ones + ps1.onComplete(); + ps2.onComplete(); + } + } + }) + .test(); + + ps1.onNext(1); + + assertFalse(ps1.hasObservers()); + assertFalse(ps2.hasObservers()); + assertTrue(ps3.hasObservers()); + assertTrue(ps4.hasObservers()); + + to.dispose(); + + assertFalse(ps3.hasObservers()); + assertFalse(ps4.hasObservers()); + } + + @Test + public void undeliverableUponCancel() { + TestHelper.checkUndeliverableUponCancel(new ObservableConverter<Integer, Observable<Integer>>() { + @Override + public Observable<Integer> apply(Observable<Integer> upstream) { + return upstream.flatMap(new Function<Integer, Observable<Integer>>() { + @Override + public Observable<Integer> apply(Integer v) throws Throwable { + return Observable.just(v).hide(); + } + }); + } + }); + } + + @Test + public void undeliverableUponCancelDelayError() { + TestHelper.checkUndeliverableUponCancel(new ObservableConverter<Integer, Observable<Integer>>() { + @Override + public Observable<Integer> apply(Observable<Integer> upstream) { + return upstream.flatMap(new Function<Integer, Observable<Integer>>() { + @Override + public Observable<Integer> apply(Integer v) throws Throwable { + return Observable.just(v).hide(); + } + }, true); + } + }); + } + + @Test + public void mainErrorsInnerCancelled() { + PublishSubject<Integer> ps1 = PublishSubject.create(); + PublishSubject<Integer> ps2 = PublishSubject.create(); + + ps1 + .flatMap(v -> ps2) + .test(); + + ps1.onNext(1); + assertTrue("No subscribers?", ps2.hasObservers()); + + ps1.onError(new TestException()); + + assertFalse("Has subscribers?", ps2.hasObservers()); + } + + @Test + public void innerErrorsMainCancelled() { + PublishSubject<Integer> ps1 = PublishSubject.create(); + PublishSubject<Integer> ps2 = PublishSubject.create(); + + ps1 + .flatMap(v -> ps2) + .test(); + + ps1.onNext(1); + assertTrue("No subscribers?", ps2.hasObservers()); + + ps2.onError(new TestException()); + + assertFalse("Has subscribers?", ps1.hasObservers()); + } + + @Test + public void signalsAfterMapperCrash() throws Throwable { + TestHelper.withErrorTracking(errors -> { + new Observable<Integer>() { + @Override + protected void subscribeActual(@NonNull Observer<? super @NonNull Integer> observer) { + observer.onSubscribe(Disposable.empty()); + observer.onNext(1); + observer.onNext(2); + observer.onComplete(); + observer.onError(new IOException()); + } + } + .flatMap(v -> { + throw new TestException(); + }) + .test() + .assertFailure(TestException.class); + + TestHelper.assertUndeliverable(errors, 0, IOException.class); + }); + } + + @Test + public void scalarQueueTerminate() { + PublishSubject<Integer> ps = PublishSubject.create(); + TestObserver<Integer> to = new TestObserver<>(); + + ps + .flatMap(v -> Observable.just(v)) + .doOnNext(v -> { + if (v == 1) { + ps.onNext(2); + ps.onNext(3); + } + }) + .take(2) + .subscribe(to); + + ps.onNext(1); + + to.assertResult(1, 2); + } + + @Test + public void scalarQueueCompleteMain() throws Exception { + PublishSubject<Integer> ps = PublishSubject.create(); + TestObserver<Integer> to = new TestObserver<>(); + CountDownLatch cdl = new CountDownLatch(1); + ps + .flatMap(v -> Observable.just(v)) + .doOnNext(v -> { + if (v == 1) { + ps.onNext(2); + TestHelper.raceOther(() -> ps.onComplete(), cdl); + } + }) + .subscribe(to); + + ps.onNext(1); + + cdl.await(); + to.assertResult(1, 2); + } + + @Test + public void fusedInnerCrash() { + UnicastSubject<Integer> us = UnicastSubject.create(); + PublishSubject<Integer> ps = PublishSubject.create(); + + TestObserver<Integer> to = Observable.just( + ps, + us.map(v -> { + if (v == 10) { + throw new TestException(); + } + return v; + }) + .compose(TestHelper.observableStripBoundary()) + ) + .flatMap(v -> v, true) + .doOnNext(v -> { + if (v == 1) { + ps.onNext(2); + us.onNext(10); + } + }) + .test(); + + ps.onNext(1); + ps.onComplete(); + + to.assertFailure(TestException.class, 1, 2); + } + + @Test + public void fusedInnerCrash2() { + UnicastSubject<Integer> us = UnicastSubject.create(); + PublishSubject<Integer> ps = PublishSubject.create(); + + TestObserver<Integer> to = Observable.just( + us.map(v -> { + if (v == 10) { + throw new TestException(); + } + return v; + }) + .compose(TestHelper.observableStripBoundary()) + , ps + ) + .flatMap(v -> v, true) + .doOnNext(v -> { + if (v == 1) { + ps.onNext(2); + us.onNext(10); + } + }) + .test(); + + ps.onNext(1); + ps.onComplete(); + + to.assertFailure(TestException.class, 1, 2); + } + + @Test(timeout = 5000) + public void mixedScalarAsync() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + Observable + .range(0, 20) + .flatMap( + integer -> { + if (integer % 5 != 0) { + return Observable + .just(integer); + } + + return Observable + .just(-integer) + .observeOn(Schedulers.computation()); + }, + false, + 1 + ) + .ignoreElements() + .blockingAwait(); + } + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableFlattenIterableTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableFlattenIterableTest.java new file mode 100644 index 0000000000..15d6aae37b --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableFlattenIterableTest.java @@ -0,0 +1,104 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import static org.junit.Assert.assertEquals; + +import java.util.*; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.core.RxJavaTest; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.subjects.PublishSubject; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class ObservableFlattenIterableTest extends RxJavaTest { + + @Test + public void dispose() { + TestHelper.checkDisposed(PublishSubject.create().flatMapIterable(new Function<Object, Iterable<Integer>>() { + @Override + public Iterable<Integer> apply(Object v) throws Exception { + return Arrays.asList(10, 20); + } + })); + } + + @Test + public void badSource() { + TestHelper.checkBadSourceObservable(new Function<Observable<Integer>, Object>() { + @Override + public Object apply(Observable<Integer> o) throws Exception { + return o.flatMapIterable(new Function<Object, Iterable<Integer>>() { + @Override + public Iterable<Integer> apply(Object v) throws Exception { + return Arrays.asList(10, 20); + } + }); + } + }, false, 1, 1, 10, 20); + } + + @Test + public void failingInnerCancelsSource() { + final AtomicInteger counter = new AtomicInteger(); + Observable.range(1, 5) + .doOnNext(new Consumer<Integer>() { + @Override + public void accept(Integer v) throws Exception { + counter.getAndIncrement(); + } + }) + .flatMapIterable(new Function<Integer, Iterable<Integer>>() { + @Override + public Iterable<Integer> apply(Integer v) + throws Exception { + return new Iterable<Integer>() { + @Override + public Iterator<Integer> iterator() { + return new Iterator<Integer>() { + @Override + public boolean hasNext() { + return true; + } + + @Override + public Integer next() { + throw new TestException(); + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + }; + } + }; + } + }) + .test() + .assertFailure(TestException.class); + + assertEquals(1, counter.get()); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeObservable(o -> o.flatMapIterable(v -> Collections.singletonList(v))); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableForEachTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableForEachTest.java new file mode 100644 index 0000000000..ac8312a893 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableForEachTest.java @@ -0,0 +1,164 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import static org.junit.Assert.*; + +import java.util.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.core.RxJavaTest; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.subjects.PublishSubject; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class ObservableForEachTest extends RxJavaTest { + + @Test + public void forEachWile() { + final List<Object> list = new ArrayList<>(); + + Observable.range(1, 5) + .doOnNext(new Consumer<Integer>() { + @Override + public void accept(Integer v) throws Exception { + list.add(v); + } + }) + .forEachWhile(new Predicate<Integer>() { + @Override + public boolean test(Integer v) throws Exception { + return v < 3; + } + }); + + assertEquals(Arrays.asList(1, 2, 3), list); + } + + @Test + public void forEachWileWithError() { + final List<Object> list = new ArrayList<>(); + + Observable.range(1, 5).concatWith(Observable.<Integer>error(new TestException())) + .doOnNext(new Consumer<Integer>() { + @Override + public void accept(Integer v) throws Exception { + list.add(v); + } + }) + .forEachWhile(new Predicate<Integer>() { + @Override + public boolean test(Integer v) throws Exception { + return true; + } + }, new Consumer<Throwable>() { + @Override + public void accept(Throwable e) throws Exception { + list.add(100); + } + }); + + assertEquals(Arrays.asList(1, 2, 3, 4, 5, 100), list); + } + + @Test + public void badSource() { + TestHelper.checkBadSourceObservable(new Function<Observable<Integer>, Object>() { + @Override + public Object apply(Observable<Integer> f) throws Exception { + return f.forEachWhile(Functions.alwaysTrue()); + } + }, false, 1, 1, (Object[])null); + } + + @Test + public void dispose() { + PublishSubject<Integer> ps = PublishSubject.create(); + + Disposable d = ps.forEachWhile(Functions.alwaysTrue()); + + assertFalse(d.isDisposed()); + + d.dispose(); + + assertTrue(d.isDisposed()); + } + + @Test + public void whilePredicateThrows() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + Observable.just(1).forEachWhile(new Predicate<Integer>() { + @Override + public boolean test(Integer v) throws Exception { + throw new TestException(); + } + }); + + TestHelper.assertError(errors, 0, OnErrorNotImplementedException.class); + Throwable c = errors.get(0).getCause(); + assertTrue("" + c, c instanceof TestException); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void whileErrorThrows() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + Observable.<Integer>error(new TestException("Outer")) + .forEachWhile(Functions.alwaysTrue(), new Consumer<Throwable>() { + @Override + public void accept(Throwable v) throws Exception { + throw new TestException("Inner"); + } + }); + + TestHelper.assertError(errors, 0, CompositeException.class); + + List<Throwable> ce = TestHelper.compositeList(errors.get(0)); + + TestHelper.assertError(ce, 0, TestException.class, "Outer"); + TestHelper.assertError(ce, 1, TestException.class, "Inner"); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void whileCompleteThrows() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + Observable.just(1).forEachWhile(Functions.alwaysTrue(), Functions.emptyConsumer(), + new Action() { + @Override + public void run() throws Exception { + throw new TestException(); + } + }); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableFromActionTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableFromActionTest.java new file mode 100644 index 0000000000..eb20166507 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableFromActionTest.java @@ -0,0 +1,214 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +import java.util.List; +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.operators.QueueFuseable; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.schedulers.Schedulers; +import io.reactivex.rxjava3.testsupport.*; + +public class ObservableFromActionTest extends RxJavaTest { + @Test + public void fromAction() { + final AtomicInteger atomicInteger = new AtomicInteger(); + + Observable.fromAction(new Action() { + @Override + public void run() throws Exception { + atomicInteger.incrementAndGet(); + } + }) + .test() + .assertResult(); + + assertEquals(1, atomicInteger.get()); + } + + @Test + public void fromActionTwice() { + final AtomicInteger atomicInteger = new AtomicInteger(); + + Action run = new Action() { + @Override + public void run() throws Exception { + atomicInteger.incrementAndGet(); + } + }; + + Observable.fromAction(run) + .test() + .assertResult(); + + assertEquals(1, atomicInteger.get()); + + Observable.fromAction(run) + .test() + .assertResult(); + + assertEquals(2, atomicInteger.get()); + } + + @Test + public void fromActionInvokesLazy() { + final AtomicInteger atomicInteger = new AtomicInteger(); + + Observable<Object> source = Observable.fromAction(new Action() { + @Override + public void run() throws Exception { + atomicInteger.incrementAndGet(); + } + }); + + assertEquals(0, atomicInteger.get()); + + source + .test() + .assertResult(); + + assertEquals(1, atomicInteger.get()); + } + + @Test + public void fromActionThrows() { + Observable.fromAction(new Action() { + @Override + public void run() throws Exception { + throw new UnsupportedOperationException(); + } + }) + .test() + .assertFailure(UnsupportedOperationException.class); + } + + @SuppressWarnings("unchecked") + @Test + public void callable() throws Throwable { + final int[] counter = { 0 }; + + Observable<Void> m = Observable.fromAction(new Action() { + @Override + public void run() throws Exception { + counter[0]++; + } + }); + + assertTrue(m.getClass().toString(), m instanceof Supplier); + + assertNull(((Supplier<Void>)m).get()); + + assertEquals(1, counter[0]); + } + + @Test + public void noErrorLoss() throws Exception { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + final CountDownLatch cdl1 = new CountDownLatch(1); + final CountDownLatch cdl2 = new CountDownLatch(1); + + TestObserver<Object> to = Observable.fromAction(new Action() { + @Override + public void run() throws Exception { + cdl1.countDown(); + cdl2.await(5, TimeUnit.SECONDS); + } + }).subscribeOn(Schedulers.single()).test(); + + assertTrue(cdl1.await(5, TimeUnit.SECONDS)); + + to.dispose(); + + int timeout = 10; + + while (timeout-- > 0 && errors.isEmpty()) { + Thread.sleep(100); + } + + TestHelper.assertUndeliverable(errors, 0, InterruptedException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void disposedUpfront() throws Throwable { + Action run = mock(Action.class); + + Observable.fromAction(run) + .test(true) + .assertEmpty(); + + verify(run, never()).run(); + } + + @Test + public void cancelWhileRunning() { + final TestObserver<Object> to = new TestObserver<>(); + + Observable.fromAction(new Action() { + @Override + public void run() throws Exception { + to.dispose(); + } + }) + .subscribeWith(to) + .assertEmpty(); + + assertTrue(to.isDisposed()); + } + + @Test + public void asyncFused() throws Throwable { + TestObserverEx<Object> to = new TestObserverEx<>(); + to.setInitialFusionMode(QueueFuseable.ASYNC); + + Action action = mock(Action.class); + + Observable.fromAction(action) + .subscribe(to); + + to.assertFusionMode(QueueFuseable.ASYNC) + .assertResult(); + + verify(action).run(); + } + + @Test + public void syncFusedRejected() throws Throwable { + TestObserverEx<Object> to = new TestObserverEx<>(); + to.setInitialFusionMode(QueueFuseable.SYNC); + + Action action = mock(Action.class); + + Observable.fromAction(action) + .subscribe(to); + + to.assertFusionMode(QueueFuseable.NONE) + .assertResult(); + + verify(action).run(); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableFromCallableTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableFromCallableTest.java new file mode 100644 index 0000000000..c701f60c47 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableFromCallableTest.java @@ -0,0 +1,311 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import java.util.List; +import java.util.concurrent.*; + +import org.junit.Test; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.schedulers.Schedulers; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class ObservableFromCallableTest extends RxJavaTest { + + @SuppressWarnings("unchecked") + @Test + public void shouldNotInvokeFuncUntilSubscription() throws Exception { + Callable<Object> func = mock(Callable.class); + + when(func.call()).thenReturn(new Object()); + + Observable<Object> fromCallableObservable = Observable.fromCallable(func); + + verifyNoInteractions(func); + + fromCallableObservable.subscribe(); + + verify(func).call(); + } + + @SuppressWarnings("unchecked") + @Test + public void shouldCallOnNextAndOnCompleted() throws Exception { + Callable<String> func = mock(Callable.class); + + when(func.call()).thenReturn("test_value"); + + Observable<String> fromCallableObservable = Observable.fromCallable(func); + + Observer<Object> observer = TestHelper.mockObserver(); + + fromCallableObservable.subscribe(observer); + + verify(observer).onNext("test_value"); + verify(observer).onComplete(); + verify(observer, never()).onError(any(Throwable.class)); + } + + @SuppressWarnings("unchecked") + @Test + public void shouldCallOnError() throws Exception { + Callable<Object> func = mock(Callable.class); + + Throwable throwable = new IllegalStateException("Test exception"); + when(func.call()).thenThrow(throwable); + + Observable<Object> fromCallableObservable = Observable.fromCallable(func); + + Observer<Object> observer = TestHelper.mockObserver(); + + fromCallableObservable.subscribe(observer); + + verify(observer, never()).onNext(any()); + verify(observer, never()).onComplete(); + verify(observer).onError(throwable); + } + + @SuppressWarnings("unchecked") + @Test + public void shouldNotDeliverResultIfSubscriberUnsubscribedBeforeEmission() throws Exception { + Callable<String> func = mock(Callable.class); + + final CountDownLatch funcLatch = new CountDownLatch(1); + final CountDownLatch observerLatch = new CountDownLatch(1); + + when(func.call()).thenAnswer(new Answer<String>() { + @Override + public String answer(InvocationOnMock invocation) throws Throwable { + observerLatch.countDown(); + + try { + funcLatch.await(); + } catch (InterruptedException e) { + // It's okay, unsubscription causes Thread interruption + + // Restoring interruption status of the Thread + Thread.currentThread().interrupt(); + } + + return "should_not_be_delivered"; + } + }); + + Observable<String> fromCallableObservable = Observable.fromCallable(func); + + Observer<Object> observer = TestHelper.mockObserver(); + + TestObserver<String> outer = new TestObserver<>(observer); + + fromCallableObservable + .subscribeOn(Schedulers.computation()) + .subscribe(outer); + + // Wait until func will be invoked + observerLatch.await(); + + // Unsubscribing before emission + outer.dispose(); + + // Emitting result + funcLatch.countDown(); + + // func must be invoked + verify(func).call(); + + // Observer must not be notified at all + verify(observer).onSubscribe(any(Disposable.class)); + verifyNoMoreInteractions(observer); + } + + @Test + public void shouldAllowToThrowCheckedException() { + final Exception checkedException = new Exception("test exception"); + + Observable<Object> fromCallableObservable = Observable.fromCallable(new Callable<Object>() { + @Override + public Object call() throws Exception { + throw checkedException; + } + }); + + Observer<Object> observer = TestHelper.mockObserver(); + + fromCallableObservable.subscribe(observer); + + verify(observer).onSubscribe(any(Disposable.class)); + verify(observer).onError(checkedException); + verifyNoMoreInteractions(observer); + } + + @Test + public void fusedFlatMapExecution() { + final int[] calls = { 0 }; + + Observable.just(1).flatMap(new Function<Integer, ObservableSource<? extends Object>>() { + @Override + public ObservableSource<? extends Object> apply(Integer v) + throws Exception { + return Observable.fromCallable(new Callable<Object>() { + @Override + public Object call() throws Exception { + return ++calls[0]; + } + }); + } + }) + .test() + .assertResult(1); + + assertEquals(1, calls[0]); + } + + @Test + public void fusedFlatMapExecutionHidden() { + final int[] calls = { 0 }; + + Observable.just(1).hide().flatMap(new Function<Integer, ObservableSource<? extends Object>>() { + @Override + public ObservableSource<? extends Object> apply(Integer v) + throws Exception { + return Observable.fromCallable(new Callable<Object>() { + @Override + public Object call() throws Exception { + return ++calls[0]; + } + }); + } + }) + .test() + .assertResult(1); + + assertEquals(1, calls[0]); + } + + @Test + public void fusedFlatMapNull() { + Observable.just(1).flatMap(new Function<Integer, ObservableSource<? extends Object>>() { + @Override + public ObservableSource<? extends Object> apply(Integer v) + throws Exception { + return Observable.fromCallable(new Callable<Object>() { + @Override + public Object call() throws Exception { + return null; + } + }); + } + }) + .test() + .assertFailure(NullPointerException.class); + } + + @Test + public void fusedFlatMapNullHidden() { + Observable.just(1).hide().flatMap(new Function<Integer, ObservableSource<? extends Object>>() { + @Override + public ObservableSource<? extends Object> apply(Integer v) + throws Exception { + return Observable.fromCallable(new Callable<Object>() { + @Override + public Object call() throws Exception { + return null; + } + }); + } + }) + .test() + .assertFailure(NullPointerException.class); + } + + @Test + public void disposedOnArrival() { + final int[] count = { 0 }; + Observable.fromCallable(new Callable<Object>() { + @Override + public Object call() throws Exception { + count[0]++; + return 1; + } + }) + .test(true) + .assertEmpty(); + + assertEquals(0, count[0]); + } + + @Test + public void disposedOnCall() { + final TestObserver<Integer> to = new TestObserver<>(); + + Observable.fromCallable(new Callable<Integer>() { + @Override + public Integer call() throws Exception { + to.dispose(); + return 1; + } + }) + .subscribe(to); + + to.assertEmpty(); + } + + @Test + public void disposedOnCallThrows() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + final TestObserver<Integer> to = new TestObserver<>(); + + Observable.fromCallable(new Callable<Integer>() { + @Override + public Integer call() throws Exception { + to.dispose(); + throw new TestException(); + } + }) + .subscribe(to); + + to.assertEmpty(); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void take() { + Observable.fromCallable(new Callable<Object>() { + @Override + public Object call() throws Exception { + return 1; + } + }) + .take(1) + .test() + .assertResult(1); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableFromCompletableTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableFromCompletableTest.java new file mode 100644 index 0000000000..6cfd2eb0ad --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableFromCompletableTest.java @@ -0,0 +1,208 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +import java.util.List; +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.fuseable.*; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.operators.QueueFuseable; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.schedulers.Schedulers; +import io.reactivex.rxjava3.testsupport.*; + +public class ObservableFromCompletableTest extends RxJavaTest { + @Test + public void fromCompletable() { + final AtomicInteger atomicInteger = new AtomicInteger(); + + Observable.fromCompletable(Completable.fromAction(new Action() { + @Override + public void run() throws Exception { + atomicInteger.incrementAndGet(); + } + })) + .test() + .assertResult(); + + assertEquals(1, atomicInteger.get()); + } + + @Test + public void fromCompletableTwice() { + final AtomicInteger atomicInteger = new AtomicInteger(); + + Action run = new Action() { + @Override + public void run() throws Exception { + atomicInteger.incrementAndGet(); + } + }; + + Observable.fromCompletable(Completable.fromAction(run)) + .test() + .assertResult(); + + assertEquals(1, atomicInteger.get()); + + Observable.fromCompletable(Completable.fromAction(run)) + .test() + .assertResult(); + + assertEquals(2, atomicInteger.get()); + } + + @Test + public void fromCompletableInvokesLazy() { + final AtomicInteger atomicInteger = new AtomicInteger(); + + Observable<Object> source = Observable.fromCompletable(Completable.fromAction(new Action() { + @Override + public void run() throws Exception { + atomicInteger.incrementAndGet(); + } + })); + + assertEquals(0, atomicInteger.get()); + + source + .test() + .assertResult(); + + assertEquals(1, atomicInteger.get()); + } + + @Test + public void fromCompletableThrows() { + Observable.fromCompletable(Completable.fromAction(new Action() { + @Override + public void run() throws Exception { + throw new UnsupportedOperationException(); + } + })) + .test() + .assertFailure(UnsupportedOperationException.class); + } + + @Test + public void noErrorLoss() throws Exception { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + final CountDownLatch cdl1 = new CountDownLatch(1); + final CountDownLatch cdl2 = new CountDownLatch(1); + + TestObserver<Object> to = Observable.fromCompletable(Completable.fromAction(new Action() { + @Override + public void run() throws Exception { + cdl1.countDown(); + cdl2.await(5, TimeUnit.SECONDS); + } + })).subscribeOn(Schedulers.single()).test(); + + assertTrue(cdl1.await(5, TimeUnit.SECONDS)); + + to.dispose(); + + int timeout = 10; + + while (timeout-- > 0 && errors.isEmpty()) { + Thread.sleep(100); + } + + TestHelper.assertUndeliverable(errors, 0, InterruptedException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void disposedUpfront() throws Throwable { + Action run = mock(Action.class); + + Observable.fromCompletable(Completable.fromAction(run)) + .test(true) + .assertEmpty(); + + verify(run, never()).run(); + } + + @Test + public void cancelWhileRunning() { + final TestObserver<Object> to = new TestObserver<>(); + + Observable.fromCompletable(Completable.fromAction(new Action() { + @Override + public void run() throws Exception { + to.dispose(); + } + })) + .subscribeWith(to) + .assertEmpty(); + + assertTrue(to.isDisposed()); + } + + @Test + public void asyncFused() throws Throwable { + TestObserverEx<Object> to = new TestObserverEx<>(); + to.setInitialFusionMode(QueueFuseable.ASYNC); + + Action action = mock(Action.class); + + Observable.fromCompletable(Completable.fromAction(action)) + .subscribe(to); + + to.assertFusionMode(QueueFuseable.ASYNC) + .assertResult(); + + verify(action).run(); + } + + @Test + public void syncFusedRejected() throws Throwable { + TestObserverEx<Object> to = new TestObserverEx<>(); + to.setInitialFusionMode(QueueFuseable.SYNC); + + Action action = mock(Action.class); + + Observable.fromCompletable(Completable.fromAction(action)) + .subscribe(to); + + to.assertFusionMode(QueueFuseable.NONE) + .assertResult(); + + verify(action).run(); + } + + @Test + public void disposed() { + TestHelper.checkDisposed(Observable.fromCompletable(Completable.never())); + } + + @Test + public void upstream() { + Observable<?> o = Observable.fromCompletable(Completable.never()); + assertTrue(o instanceof HasUpstreamCompletableSource); + assertSame(Completable.never(), ((HasUpstreamCompletableSource)o).source()); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableFromIterableTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableFromIterableTest.java new file mode 100644 index 0000000000..b816baf4b0 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableFromIterableTest.java @@ -0,0 +1,373 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import java.util.*; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.junit.Test; +import org.mockito.Mockito; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.core.Observer; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.util.CrashingIterable; +import io.reactivex.rxjava3.observers.*; +import io.reactivex.rxjava3.operators.QueueDisposable; +import io.reactivex.rxjava3.operators.QueueFuseable; +import io.reactivex.rxjava3.testsupport.*; + +public class ObservableFromIterableTest extends RxJavaTest { + + @Test + public void listIterable() { + Observable<String> o = Observable.fromIterable(Arrays.<String> asList("one", "two", "three")); + + Observer<String> observer = TestHelper.mockObserver(); + + o.subscribe(observer); + + verify(observer, times(1)).onNext("one"); + verify(observer, times(1)).onNext("two"); + verify(observer, times(1)).onNext("three"); + verify(observer, Mockito.never()).onError(any(Throwable.class)); + verify(observer, times(1)).onComplete(); + } + + /** + * This tests the path that can not optimize based on size so must use setProducer. + */ + @Test + public void rawIterable() { + Iterable<String> it = new Iterable<String>() { + + @Override + public Iterator<String> iterator() { + return new Iterator<String>() { + + int i; + + @Override + public boolean hasNext() { + return i < 3; + } + + @Override + public String next() { + return String.valueOf(++i); + } + + @Override + public void remove() { + } + + }; + } + + }; + Observable<String> o = Observable.fromIterable(it); + + Observer<String> observer = TestHelper.mockObserver(); + + o.subscribe(observer); + + verify(observer, times(1)).onNext("1"); + verify(observer, times(1)).onNext("2"); + verify(observer, times(1)).onNext("3"); + verify(observer, Mockito.never()).onError(any(Throwable.class)); + verify(observer, times(1)).onComplete(); + } + + @Test + public void observableFromIterable() { + Observable<String> o = Observable.fromIterable(Arrays.<String> asList("one", "two", "three")); + + Observer<String> observer = TestHelper.mockObserver(); + + o.subscribe(observer); + + verify(observer, times(1)).onNext("one"); + verify(observer, times(1)).onNext("two"); + verify(observer, times(1)).onNext("three"); + verify(observer, Mockito.never()).onError(any(Throwable.class)); + verify(observer, times(1)).onComplete(); + } + + @Test + public void noBackpressure() { + Observable<Integer> o = Observable.fromIterable(Arrays.asList(1, 2, 3, 4, 5)); + + TestObserverEx<Integer> to = new TestObserverEx<>(); + + o.subscribe(to); + + to.assertValues(1, 2, 3, 4, 5); + to.assertTerminated(); + } + + @Test + public void subscribeMultipleTimes() { + Observable<Integer> o = Observable.fromIterable(Arrays.asList(1, 2, 3)); + + for (int i = 0; i < 10; i++) { + TestObserver<Integer> to = new TestObserver<>(); + + o.subscribe(to); + + to.assertValues(1, 2, 3); + to.assertNoErrors(); + to.assertComplete(); + } + } + + @Test + public void doesNotCallIteratorHasNextMoreThanRequiredWithBackpressure() { + final AtomicBoolean called = new AtomicBoolean(false); + Iterable<Integer> iterable = new Iterable<Integer>() { + + @Override + public Iterator<Integer> iterator() { + return new Iterator<Integer>() { + + int count = 1; + + @Override + public void remove() { + // ignore + } + + @Override + public boolean hasNext() { + if (count > 1) { + called.set(true); + return false; + } + return true; + } + + @Override + public Integer next() { + return count++; + } + + }; + } + }; + Observable.fromIterable(iterable).take(1).subscribe(); + assertFalse(called.get()); + } + + @Test + public void doesNotCallIteratorHasNextMoreThanRequiredFastPath() { + final AtomicBoolean called = new AtomicBoolean(false); + Iterable<Integer> iterable = new Iterable<Integer>() { + + @Override + public Iterator<Integer> iterator() { + return new Iterator<Integer>() { + + @Override + public void remove() { + // ignore + } + + int count = 1; + + @Override + public boolean hasNext() { + if (count > 1) { + called.set(true); + return false; + } + return true; + } + + @Override + public Integer next() { + return count++; + } + + }; + } + }; + Observable.fromIterable(iterable).subscribe(new DefaultObserver<Integer>() { + + @Override + public void onComplete() { + + } + + @Override + public void onError(Throwable e) { + + } + + @Override + public void onNext(Integer t) { + // unsubscribe on first emission + cancel(); + } + }); + assertFalse(called.get()); + } + + @Test + public void fusionWithConcatMap() { + TestObserver<Integer> to = new TestObserver<>(); + + Observable.fromIterable(Arrays.asList(1, 2, 3, 4)).concatMap( + new Function<Integer, ObservableSource<Integer>>() { + @Override + public ObservableSource<Integer> apply(Integer v) { + return Observable.range(v, 2); + } + }).subscribe(to); + + to.assertValues(1, 2, 2, 3, 3, 4, 4, 5); + to.assertNoErrors(); + to.assertComplete(); + } + + @Test + public void iteratorThrows() { + Observable.fromIterable(new CrashingIterable(1, 100, 100)) + .to(TestHelper.<Integer>testConsumer()) + .assertFailureAndMessage(TestException.class, "iterator()"); + } + + @Test + public void hasNext2Throws() { + Observable.fromIterable(new CrashingIterable(100, 2, 100)) + .to(TestHelper.<Integer>testConsumer()) + .assertFailureAndMessage(TestException.class, "hasNext()", 0); + } + + @Test + public void hasNextCancels() { + final TestObserver<Integer> to = new TestObserver<>(); + + Observable.fromIterable(new Iterable<Integer>() { + @Override + public Iterator<Integer> iterator() { + return new Iterator<Integer>() { + int count; + + @Override + public boolean hasNext() { + if (++count == 2) { + to.dispose(); + } + return true; + } + + @Override + public Integer next() { + return 1; + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + }; + } + }) + .subscribe(to); + + to.assertValue(1) + .assertNoErrors() + .assertNotComplete(); + } + + @Test + public void fusionRejected() { + TestObserverEx<Integer> to = new TestObserverEx<>(QueueFuseable.ASYNC); + + Observable.fromIterable(Arrays.asList(1, 2, 3)) + .subscribe(to); + + to.assertFusionMode(QueueFuseable.NONE) + .assertResult(1, 2, 3); + } + + @Test + public void fusionClear() { + Observable.fromIterable(Arrays.asList(1, 2, 3)) + .subscribe(new Observer<Integer>() { + @Override + public void onSubscribe(Disposable d) { + @SuppressWarnings("unchecked") + QueueDisposable<Integer> qd = (QueueDisposable<Integer>)d; + + qd.requestFusion(QueueFuseable.ANY); + + try { + assertEquals(1, qd.poll().intValue()); + } catch (Throwable ex) { + fail(ex.toString()); + } + + qd.clear(); + try { + assertNull(qd.poll()); + } catch (Throwable ex) { + fail(ex.toString()); + } + } + + @Override + public void onNext(Integer value) { + } + + @Override + public void onError(Throwable e) { + } + + @Override + public void onComplete() { + } + }); + } + + @Test + public void disposeAfterHasNext() { + TestObserver<Integer> to = new TestObserver<>(); + + Observable.fromIterable(() -> new Iterator<Integer>() { + int count; + @Override + public boolean hasNext() { + if (count++ == 2) { + to.dispose(); + return false; + } + return true; + } + + @Override + public Integer next() { + return 1; + } + }) + .subscribeWith(to) + .assertValuesOnly(1, 1); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableFromMaybeTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableFromMaybeTest.java new file mode 100644 index 0000000000..e0175b4a59 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableFromMaybeTest.java @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import static org.junit.Assert.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.operators.QueueFuseable; +import io.reactivex.rxjava3.subjects.MaybeSubject; +import io.reactivex.rxjava3.testsupport.TestObserverEx; + +public class ObservableFromMaybeTest extends RxJavaTest { + + @Test + public void success() { + Observable.fromMaybe(Maybe.just(1).hide()) + .test() + .assertResult(1); + } + + @Test + public void empty() { + Observable.fromMaybe(Maybe.empty().hide()) + .test() + .assertResult(); + } + + @Test + public void error() { + Observable.fromMaybe(Maybe.error(new TestException()).hide()) + .test() + .assertFailure(TestException.class); + } + + @Test + public void cancelComposes() { + MaybeSubject<Integer> ms = MaybeSubject.create(); + + TestObserver<Integer> to = Observable.fromMaybe(ms) + .test(); + + to.assertEmpty(); + + assertTrue(ms.hasObservers()); + + to.dispose(); + + assertFalse(ms.hasObservers()); + } + + @Test + public void asyncFusion() { + TestObserverEx<Integer> to = new TestObserverEx<>(); + to.setInitialFusionMode(QueueFuseable.ASYNC); + + Observable.fromMaybe(Maybe.just(1)) + .subscribe(to); + + to + .assertFuseable() + .assertFusionMode(QueueFuseable.ASYNC) + .assertResult(1); + } + + @Test + public void syncFusionRejected() { + TestObserverEx<Integer> to = new TestObserverEx<>(); + to.setInitialFusionMode(QueueFuseable.SYNC); + + Observable.fromMaybe(Maybe.just(1)) + .subscribe(to); + + to + .assertFuseable() + .assertFusionMode(QueueFuseable.NONE) + .assertResult(1); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableFromRunnableTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableFromRunnableTest.java new file mode 100644 index 0000000000..b566f66775 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableFromRunnableTest.java @@ -0,0 +1,220 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +import java.util.List; +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.Supplier; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.operators.QueueFuseable; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.schedulers.Schedulers; +import io.reactivex.rxjava3.testsupport.*; + +public class ObservableFromRunnableTest extends RxJavaTest { + @Test + public void fromRunnable() { + final AtomicInteger atomicInteger = new AtomicInteger(); + + Observable.fromRunnable(new Runnable() { + @Override + public void run() { + atomicInteger.incrementAndGet(); + } + }) + .test() + .assertResult(); + + assertEquals(1, atomicInteger.get()); + } + + @Test + public void fromRunnableTwice() { + final AtomicInteger atomicInteger = new AtomicInteger(); + + Runnable run = new Runnable() { + @Override + public void run() { + atomicInteger.incrementAndGet(); + } + }; + + Observable.fromRunnable(run) + .test() + .assertResult(); + + assertEquals(1, atomicInteger.get()); + + Observable.fromRunnable(run) + .test() + .assertResult(); + + assertEquals(2, atomicInteger.get()); + } + + @Test + public void fromRunnableInvokesLazy() { + final AtomicInteger atomicInteger = new AtomicInteger(); + + Observable<Object> source = Observable.fromRunnable(new Runnable() { + @Override + public void run() { + atomicInteger.incrementAndGet(); + } + }); + + assertEquals(0, atomicInteger.get()); + + source + .test() + .assertResult(); + + assertEquals(1, atomicInteger.get()); + } + + @Test + public void fromRunnableThrows() { + Observable.fromRunnable(new Runnable() { + @Override + public void run() { + throw new UnsupportedOperationException(); + } + }) + .test() + .assertFailure(UnsupportedOperationException.class); + } + + @SuppressWarnings("unchecked") + @Test + public void callable() throws Throwable { + final int[] counter = { 0 }; + + Observable<Void> m = Observable.fromRunnable(new Runnable() { + @Override + public void run() { + counter[0]++; + } + }); + + assertTrue(m.getClass().toString(), m instanceof Supplier); + + assertNull(((Supplier<Void>)m).get()); + + assertEquals(1, counter[0]); + } + + @Test + public void noErrorLoss() throws Exception { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + final CountDownLatch cdl1 = new CountDownLatch(1); + final CountDownLatch cdl2 = new CountDownLatch(1); + + TestObserver<Object> to = Observable.fromRunnable(new Runnable() { + @Override + public void run() { + cdl1.countDown(); + try { + cdl2.await(5, TimeUnit.SECONDS); + } catch (InterruptedException e) { + e.printStackTrace(); + throw new TestException(e); + } + } + }).subscribeOn(Schedulers.single()).test(); + + assertTrue(cdl1.await(5, TimeUnit.SECONDS)); + + to.dispose(); + + int timeout = 10; + + while (timeout-- > 0 && errors.isEmpty()) { + Thread.sleep(100); + } + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void disposedUpfront() throws Throwable { + Runnable run = mock(Runnable.class); + + Observable.fromRunnable(run) + .test(true) + .assertEmpty(); + + verify(run, never()).run(); + } + + @Test + public void cancelWhileRunning() { + final TestObserver<Object> to = new TestObserver<>(); + + Observable.fromRunnable(new Runnable() { + @Override + public void run() { + to.dispose(); + } + }) + .subscribeWith(to) + .assertEmpty(); + + assertTrue(to.isDisposed()); + } + + @Test + public void asyncFused() throws Throwable { + TestObserverEx<Object> to = new TestObserverEx<>(); + to.setInitialFusionMode(QueueFuseable.ASYNC); + + Runnable action = mock(Runnable.class); + + Observable.fromRunnable(action) + .subscribe(to); + + to.assertFusionMode(QueueFuseable.ASYNC) + .assertResult(); + + verify(action).run(); + } + + @Test + public void syncFusedRejected() throws Throwable { + TestObserverEx<Object> to = new TestObserverEx<>(); + to.setInitialFusionMode(QueueFuseable.SYNC); + + Runnable action = mock(Runnable.class); + + Observable.fromRunnable(action) + .subscribe(to); + + to.assertFusionMode(QueueFuseable.NONE) + .assertResult(); + + verify(action).run(); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableFromSingleTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableFromSingleTest.java new file mode 100644 index 0000000000..aaabb513b3 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableFromSingleTest.java @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import static org.junit.Assert.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.operators.QueueFuseable; +import io.reactivex.rxjava3.subjects.SingleSubject; +import io.reactivex.rxjava3.testsupport.TestObserverEx; + +public class ObservableFromSingleTest extends RxJavaTest { + + @Test + public void success() { + Observable.fromSingle(Single.just(1).hide()) + .test() + .assertResult(1); + } + + @Test + public void error() { + Observable.fromSingle(Single.error(new TestException()).hide()) + .test() + .assertFailure(TestException.class); + } + + @Test + public void cancelComposes() { + SingleSubject<Integer> ms = SingleSubject.create(); + + TestObserver<Integer> to = Observable.fromSingle(ms) + .test(); + + to.assertEmpty(); + + assertTrue(ms.hasObservers()); + + to.dispose(); + + assertFalse(ms.hasObservers()); + } + + @Test + public void asyncFusion() { + TestObserverEx<Integer> to = new TestObserverEx<>(); + to.setInitialFusionMode(QueueFuseable.ASYNC); + + Observable.fromSingle(Single.just(1)) + .subscribe(to); + + to + .assertFuseable() + .assertFusionMode(QueueFuseable.ASYNC) + .assertResult(1); + } + + @Test + public void syncFusionRejected() { + TestObserverEx<Integer> to = new TestObserverEx<>(); + to.setInitialFusionMode(QueueFuseable.SYNC); + + Observable.fromSingle(Single.just(1)) + .subscribe(to); + + to + .assertFuseable() + .assertFusionMode(QueueFuseable.NONE) + .assertResult(1); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableFromSupplierTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableFromSupplierTest.java new file mode 100644 index 0000000000..56e31ddfb7 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableFromSupplierTest.java @@ -0,0 +1,311 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import java.util.List; +import java.util.concurrent.CountDownLatch; + +import org.junit.Test; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.schedulers.Schedulers; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class ObservableFromSupplierTest extends RxJavaTest { + + @SuppressWarnings("unchecked") + @Test + public void shouldNotInvokeFuncUntilSubscription() throws Throwable { + Supplier<Object> func = mock(Supplier.class); + + when(func.get()).thenReturn(new Object()); + + Observable<Object> fromSupplierObservable = Observable.fromSupplier(func); + + verifyNoInteractions(func); + + fromSupplierObservable.subscribe(); + + verify(func).get(); + } + + @SuppressWarnings("unchecked") + @Test + public void shouldCallOnNextAndOnCompleted() throws Throwable { + Supplier<String> func = mock(Supplier.class); + + when(func.get()).thenReturn("test_value"); + + Observable<String> fromSupplierObservable = Observable.fromSupplier(func); + + Observer<Object> observer = TestHelper.mockObserver(); + + fromSupplierObservable.subscribe(observer); + + verify(observer).onNext("test_value"); + verify(observer).onComplete(); + verify(observer, never()).onError(any(Throwable.class)); + } + + @SuppressWarnings("unchecked") + @Test + public void shouldCallOnError() throws Throwable { + Supplier<Object> func = mock(Supplier.class); + + Throwable throwable = new IllegalStateException("Test exception"); + when(func.get()).thenThrow(throwable); + + Observable<Object> fromSupplierObservable = Observable.fromSupplier(func); + + Observer<Object> observer = TestHelper.mockObserver(); + + fromSupplierObservable.subscribe(observer); + + verify(observer, never()).onNext(any()); + verify(observer, never()).onComplete(); + verify(observer).onError(throwable); + } + + @SuppressWarnings("unchecked") + @Test + public void shouldNotDeliverResultIfSubscriberUnsubscribedBeforeEmission() throws Throwable { + Supplier<String> func = mock(Supplier.class); + + final CountDownLatch funcLatch = new CountDownLatch(1); + final CountDownLatch observerLatch = new CountDownLatch(1); + + when(func.get()).thenAnswer(new Answer<String>() { + @Override + public String answer(InvocationOnMock invocation) throws Throwable { + observerLatch.countDown(); + + try { + funcLatch.await(); + } catch (InterruptedException e) { + // It's okay, unsubscription causes Thread interruption + + // Restoring interruption status of the Thread + Thread.currentThread().interrupt(); + } + + return "should_not_be_delivered"; + } + }); + + Observable<String> fromSupplierObservable = Observable.fromSupplier(func); + + Observer<Object> observer = TestHelper.mockObserver(); + + TestObserver<String> outer = new TestObserver<>(observer); + + fromSupplierObservable + .subscribeOn(Schedulers.computation()) + .subscribe(outer); + + // Wait until func will be invoked + observerLatch.await(); + + // Unsubscribing before emission + outer.dispose(); + + // Emitting result + funcLatch.countDown(); + + // func must be invoked + verify(func).get(); + + // Observer must not be notified at all + verify(observer).onSubscribe(any(Disposable.class)); + verifyNoMoreInteractions(observer); + } + + @Test + public void shouldAllowToThrowCheckedException() { + final Exception checkedException = new Exception("test exception"); + + Observable<Object> fromSupplierObservable = Observable.fromSupplier(new Supplier<Object>() { + @Override + public Object get() throws Exception { + throw checkedException; + } + }); + + Observer<Object> observer = TestHelper.mockObserver(); + + fromSupplierObservable.subscribe(observer); + + verify(observer).onSubscribe(any(Disposable.class)); + verify(observer).onError(checkedException); + verifyNoMoreInteractions(observer); + } + + @Test + public void fusedFlatMapExecution() { + final int[] calls = { 0 }; + + Observable.just(1).flatMap(new Function<Integer, ObservableSource<? extends Object>>() { + @Override + public ObservableSource<? extends Object> apply(Integer v) + throws Exception { + return Observable.fromSupplier(new Supplier<Object>() { + @Override + public Object get() throws Exception { + return ++calls[0]; + } + }); + } + }) + .test() + .assertResult(1); + + assertEquals(1, calls[0]); + } + + @Test + public void fusedFlatMapExecutionHidden() { + final int[] calls = { 0 }; + + Observable.just(1).hide().flatMap(new Function<Integer, ObservableSource<? extends Object>>() { + @Override + public ObservableSource<? extends Object> apply(Integer v) + throws Exception { + return Observable.fromSupplier(new Supplier<Object>() { + @Override + public Object get() throws Exception { + return ++calls[0]; + } + }); + } + }) + .test() + .assertResult(1); + + assertEquals(1, calls[0]); + } + + @Test + public void fusedFlatMapNull() { + Observable.just(1).flatMap(new Function<Integer, ObservableSource<? extends Object>>() { + @Override + public ObservableSource<? extends Object> apply(Integer v) + throws Exception { + return Observable.fromSupplier(new Supplier<Object>() { + @Override + public Object get() throws Exception { + return null; + } + }); + } + }) + .test() + .assertFailure(NullPointerException.class); + } + + @Test + public void fusedFlatMapNullHidden() { + Observable.just(1).hide().flatMap(new Function<Integer, ObservableSource<? extends Object>>() { + @Override + public ObservableSource<? extends Object> apply(Integer v) + throws Exception { + return Observable.fromSupplier(new Supplier<Object>() { + @Override + public Object get() throws Exception { + return null; + } + }); + } + }) + .test() + .assertFailure(NullPointerException.class); + } + + @Test + public void disposedOnArrival() { + final int[] count = { 0 }; + Observable.fromSupplier(new Supplier<Object>() { + @Override + public Object get() throws Exception { + count[0]++; + return 1; + } + }) + .test(true) + .assertEmpty(); + + assertEquals(0, count[0]); + } + + @Test + public void disposedOnCall() { + final TestObserver<Integer> to = new TestObserver<>(); + + Observable.fromSupplier(new Supplier<Integer>() { + @Override + public Integer get() throws Exception { + to.dispose(); + return 1; + } + }) + .subscribe(to); + + to.assertEmpty(); + } + + @Test + public void disposedOnCallThrows() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + final TestObserver<Integer> to = new TestObserver<>(); + + Observable.fromSupplier(new Supplier<Integer>() { + @Override + public Integer get() throws Exception { + to.dispose(); + throw new TestException(); + } + }) + .subscribe(to); + + to.assertEmpty(); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void take() { + Observable.fromSupplier(new Supplier<Object>() { + @Override + public Object get() throws Exception { + return 1; + } + }) + .take(1) + .test() + .assertResult(1); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableFromTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableFromTest.java new file mode 100644 index 0000000000..639d3c4d14 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableFromTest.java @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import static org.junit.Assert.*; + +import java.util.concurrent.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.operators.QueueFuseable; +import io.reactivex.rxjava3.operators.ScalarSupplier; +import io.reactivex.rxjava3.schedulers.Schedulers; +import io.reactivex.rxjava3.testsupport.*; + +public class ObservableFromTest extends RxJavaTest { + + @Test + public void fromFutureTimeout() throws Exception { + Observable.fromFuture(Observable.never() + .toFuture(), 100, TimeUnit.MILLISECONDS) + .subscribeOn(Schedulers.io()) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertFailure(TimeoutException.class); + } + + @Test + public void fromPublisher() { + Observable.fromPublisher(Flowable.just(1)) + .test() + .assertResult(1); + } + + @Test + public void just10() { + Observable.just(1, 2, 3, 4, 5, 6, 7, 8, 9, 10) + .test() + .assertResult(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + } + + @Test + public void fromArrayEmpty() { + assertSame(Observable.empty(), Observable.fromArray()); + } + + @Test + public void fromArraySingle() { + assertTrue(Observable.fromArray(1) instanceof ScalarSupplier); + } + + @Test + public void fromPublisherDispose() { + TestHelper.checkDisposed(Flowable.just(1).toObservable()); + } + + @Test + public void fromPublisherDoubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeFlowableToObservable(new Function<Flowable<Object>, ObservableSource<Object>>() { + @Override + public ObservableSource<Object> apply(Flowable<Object> f) throws Exception { + return f.toObservable(); + } + }); + } + + @Test + public void fusionRejected() { + TestObserverEx<Integer> to = new TestObserverEx<>(QueueFuseable.ASYNC); + + Observable.fromArray(1, 2, 3) + .subscribe(to); + + to.assertFusionMode(QueueFuseable.NONE) + .assertResult(1, 2, 3); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableGenerateTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableGenerateTest.java new file mode 100644 index 0000000000..90a3d60eaf --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableGenerateTest.java @@ -0,0 +1,210 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import static org.junit.Assert.assertEquals; + +import java.util.List; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class ObservableGenerateTest extends RxJavaTest { + + @Test + public void statefulBiconsumer() { + Observable.generate(new Supplier<Object>() { + @Override + public Object get() throws Exception { + return 10; + } + }, new BiConsumer<Object, Emitter<Object>>() { + @Override + public void accept(Object s, Emitter<Object> e) throws Exception { + e.onNext(s); + } + }, new Consumer<Object>() { + @Override + public void accept(Object d) throws Exception { + + } + }) + .take(5) + .test() + .assertResult(10, 10, 10, 10, 10); + } + + @Test + public void stateSupplierThrows() { + Observable.generate(new Supplier<Object>() { + @Override + public Object get() throws Exception { + throw new TestException(); + } + }, new BiConsumer<Object, Emitter<Object>>() { + @Override + public void accept(Object s, Emitter<Object> e) throws Exception { + e.onNext(s); + } + }, Functions.emptyConsumer()) + .test() + .assertFailure(TestException.class); + } + + @Test + public void generatorThrows() { + Observable.generate(new Supplier<Object>() { + @Override + public Object get() throws Exception { + return 1; + } + }, new BiConsumer<Object, Emitter<Object>>() { + @Override + public void accept(Object s, Emitter<Object> e) throws Exception { + throw new TestException(); + } + }, Functions.emptyConsumer()) + .test() + .assertFailure(TestException.class); + } + + @Test + public void disposerThrows() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + Observable.generate(new Supplier<Object>() { + @Override + public Object get() throws Exception { + return 1; + } + }, new BiConsumer<Object, Emitter<Object>>() { + @Override + public void accept(Object s, Emitter<Object> e) throws Exception { + e.onComplete(); + } + }, new Consumer<Object>() { + @Override + public void accept(Object d) throws Exception { + throw new TestException(); + } + }) + .test() + .assertResult(); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void dispose() { + TestHelper.checkDisposed(Observable.generate(new Supplier<Object>() { + @Override + public Object get() throws Exception { + return 1; + } + }, new BiConsumer<Object, Emitter<Object>>() { + @Override + public void accept(Object s, Emitter<Object> e) throws Exception { + e.onComplete(); + } + }, Functions.emptyConsumer())); + } + + @Test + public void nullError() { + final int[] call = { 0 }; + Observable.generate(Functions.justSupplier(1), + new BiConsumer<Integer, Emitter<Object>>() { + @Override + public void accept(Integer s, Emitter<Object> e) throws Exception { + try { + e.onError(null); + } catch (NullPointerException ex) { + call[0]++; + } + } + }, Functions.emptyConsumer()) + .test() + .assertFailure(NullPointerException.class); + + assertEquals(0, call[0]); + } + + @Test + public void multipleOnNext() { + Observable.generate(new Consumer<Emitter<Object>>() { + @Override + public void accept(Emitter<Object> e) throws Exception { + e.onNext(1); + e.onNext(2); + } + }) + .test() + .assertFailure(IllegalStateException.class, 1); + } + + @Test + public void multipleOnError() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + Observable.generate(new Consumer<Emitter<Object>>() { + @Override + public void accept(Emitter<Object> e) throws Exception { + e.onError(new TestException("First")); + e.onError(new TestException("Second")); + } + }) + .test() + .assertFailure(TestException.class); + + TestHelper.assertUndeliverable(errors, 0, TestException.class, "Second"); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void multipleOnComplete() { + Observable.generate(new Consumer<Emitter<Object>>() { + @Override + public void accept(Emitter<Object> e) throws Exception { + e.onComplete(); + e.onComplete(); + } + }) + .test() + .assertResult(); + } + + @Test + public void onNextAfterOnComplete() { + Observable.generate(new Consumer<Emitter<Object>>() { + @Override + public void accept(Emitter<Object> e) throws Exception { + e.onComplete(); + e.onNext(1); + } + }) + .test() + .assertResult(); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableGroupByTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableGroupByTest.java new file mode 100644 index 0000000000..32c1abc2e2 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableGroupByTest.java @@ -0,0 +1,1744 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +import java.util.*; +import java.util.concurrent.*; +import java.util.concurrent.atomic.*; + +import org.junit.Test; +import org.mockito.Mockito; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.core.Observer; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.observables.GroupedObservable; +import io.reactivex.rxjava3.observers.*; +import io.reactivex.rxjava3.schedulers.Schedulers; +import io.reactivex.rxjava3.subjects.*; +import io.reactivex.rxjava3.testsupport.*; + +public class ObservableGroupByTest extends RxJavaTest { + + final Function<String, Integer> length = new Function<String, Integer>() { + @Override + public Integer apply(String s) { + return s.length(); + } + }; + + @Test + public void groupBy() { + Observable<String> source = Observable.just("one", "two", "three", "four", "five", "six"); + Observable<GroupedObservable<Integer, String>> grouped = source.groupBy(length); + + Map<Integer, Collection<String>> map = toMap(grouped); + + assertEquals(3, map.size()); + assertArrayEquals(Arrays.asList("one", "two", "six").toArray(), map.get(3).toArray()); + assertArrayEquals(Arrays.asList("four", "five").toArray(), map.get(4).toArray()); + assertArrayEquals(Arrays.asList("three").toArray(), map.get(5).toArray()); + } + + @Test + public void groupByWithElementSelector() { + Observable<String> source = Observable.just("one", "two", "three", "four", "five", "six"); + Observable<GroupedObservable<Integer, Integer>> grouped = source.groupBy(length, length); + + Map<Integer, Collection<Integer>> map = toMap(grouped); + + assertEquals(3, map.size()); + assertArrayEquals(Arrays.asList(3, 3, 3).toArray(), map.get(3).toArray()); + assertArrayEquals(Arrays.asList(4, 4).toArray(), map.get(4).toArray()); + assertArrayEquals(Arrays.asList(5).toArray(), map.get(5).toArray()); + } + + @Test + public void groupByWithElementSelector2() { + Observable<String> source = Observable.just("one", "two", "three", "four", "five", "six"); + Observable<GroupedObservable<Integer, Integer>> grouped = source.groupBy(length, length); + + Map<Integer, Collection<Integer>> map = toMap(grouped); + + assertEquals(3, map.size()); + assertArrayEquals(Arrays.asList(3, 3, 3).toArray(), map.get(3).toArray()); + assertArrayEquals(Arrays.asList(4, 4).toArray(), map.get(4).toArray()); + assertArrayEquals(Arrays.asList(5).toArray(), map.get(5).toArray()); + } + + @Test + public void empty() { + Observable<String> source = Observable.empty(); + Observable<GroupedObservable<Integer, String>> grouped = source.groupBy(length); + + Map<Integer, Collection<String>> map = toMap(grouped); + + assertTrue(map.isEmpty()); + } + + @Test + @SuppressUndeliverable + public void error() { + Observable<String> sourceStrings = Observable.just("one", "two", "three", "four", "five", "six"); + Observable<String> errorSource = Observable.error(new RuntimeException("forced failure")); + Observable<String> source = Observable.concat(sourceStrings, errorSource); + + Observable<GroupedObservable<Integer, String>> grouped = source.groupBy(length); + + final AtomicInteger groupCounter = new AtomicInteger(); + final AtomicInteger eventCounter = new AtomicInteger(); + final AtomicReference<Throwable> error = new AtomicReference<>(); + + grouped.flatMap(new Function<GroupedObservable<Integer, String>, Observable<String>>() { + + @Override + public Observable<String> apply(final GroupedObservable<Integer, String> o) { + groupCounter.incrementAndGet(); + return o.map(new Function<String, String>() { + + @Override + public String apply(String v) { + return "Event => key: " + o.getKey() + " value: " + v; + } + }); + } + }).subscribe(new DefaultObserver<String>() { + + @Override + public void onComplete() { + + } + + @Override + public void onError(Throwable e) { + e.printStackTrace(); + error.set(e); + } + + @Override + public void onNext(String v) { + eventCounter.incrementAndGet(); + System.out.println(v); + + } + }); + + assertEquals(3, groupCounter.get()); + assertEquals(6, eventCounter.get()); + assertNotNull(error.get()); + } + + private static <K, V> Map<K, Collection<V>> toMap(Observable<GroupedObservable<K, V>> observable) { + + final ConcurrentHashMap<K, Collection<V>> result = new ConcurrentHashMap<>(); + + observable.doOnNext(new Consumer<GroupedObservable<K, V>>() { + + @Override + public void accept(final GroupedObservable<K, V> o) { + result.put(o.getKey(), new ConcurrentLinkedQueue<>()); + o.subscribe(new Consumer<V>() { + + @Override + public void accept(V v) { + result.get(o.getKey()).add(v); + } + + }); + } + }).blockingSubscribe(); + + return result; + } + + /** + * Assert that only a single subscription to a stream occurs and that all events are received. + * + * @throws Throwable some method may throw + */ + @Test + public void groupedEventStream() throws Throwable { + + final AtomicInteger eventCounter = new AtomicInteger(); + final AtomicInteger subscribeCounter = new AtomicInteger(); + final AtomicInteger groupCounter = new AtomicInteger(); + final CountDownLatch latch = new CountDownLatch(1); + final int count = 100; + final int groupCount = 2; + + Observable<Event> es = Observable.unsafeCreate(new ObservableSource<Event>() { + + @Override + public void subscribe(final Observer<? super Event> observer) { + observer.onSubscribe(Disposable.empty()); + System.out.println("*** Subscribing to EventStream ***"); + subscribeCounter.incrementAndGet(); + new Thread(new Runnable() { + + @Override + public void run() { + for (int i = 0; i < count; i++) { + Event e = new Event(); + e.source = i % groupCount; + e.message = "Event-" + i; + observer.onNext(e); + } + observer.onComplete(); + } + + }).start(); + } + + }); + + es.groupBy(new Function<Event, Integer>() { + + @Override + public Integer apply(Event e) { + return e.source; + } + }).flatMap(new Function<GroupedObservable<Integer, Event>, Observable<String>>() { + + @Override + public Observable<String> apply(GroupedObservable<Integer, Event> eventGroupedObservable) { + System.out.println("GroupedObservable Key: " + eventGroupedObservable.getKey()); + groupCounter.incrementAndGet(); + + return eventGroupedObservable.map(new Function<Event, String>() { + + @Override + public String apply(Event event) { + return "Source: " + event.source + " Message: " + event.message; + } + }); + + } + }).subscribe(new DefaultObserver<String>() { + + @Override + public void onComplete() { + latch.countDown(); + } + + @Override + public void onError(Throwable e) { + e.printStackTrace(); + latch.countDown(); + } + + @Override + public void onNext(String outputMessage) { + System.out.println(outputMessage); + eventCounter.incrementAndGet(); + } + }); + + latch.await(5000, TimeUnit.MILLISECONDS); + assertEquals(1, subscribeCounter.get()); + assertEquals(groupCount, groupCounter.get()); + assertEquals(count, eventCounter.get()); + + } + + /* + * We will only take 1 group with 20 events from it and then unsubscribe. + */ + @Test + public void unsubscribeOnNestedTakeAndSyncInfiniteStream() throws InterruptedException { + final AtomicInteger subscribeCounter = new AtomicInteger(); + final AtomicInteger sentEventCounter = new AtomicInteger(); + doTestUnsubscribeOnNestedTakeAndAsyncInfiniteStream(SYNC_INFINITE_OBSERVABLE_OF_EVENT(2, subscribeCounter, sentEventCounter), subscribeCounter); + Thread.sleep(500); + assertEquals(39, sentEventCounter.get()); + } + + /* + * We will only take 1 group with 20 events from it and then unsubscribe. + */ + @Test + public void unsubscribeOnNestedTakeAndAsyncInfiniteStream() throws InterruptedException { + final AtomicInteger subscribeCounter = new AtomicInteger(); + final AtomicInteger sentEventCounter = new AtomicInteger(); + doTestUnsubscribeOnNestedTakeAndAsyncInfiniteStream(ASYNC_INFINITE_OBSERVABLE_OF_EVENT(2, subscribeCounter, sentEventCounter), subscribeCounter); + Thread.sleep(500); + assertEquals(39, sentEventCounter.get()); + } + + private void doTestUnsubscribeOnNestedTakeAndAsyncInfiniteStream(Observable<Event> es, AtomicInteger subscribeCounter) throws InterruptedException { + final AtomicInteger eventCounter = new AtomicInteger(); + final AtomicInteger groupCounter = new AtomicInteger(); + final CountDownLatch latch = new CountDownLatch(1); + + es.groupBy(new Function<Event, Integer>() { + + @Override + public Integer apply(Event e) { + return e.source; + } + }) + .take(1) // we want only the first group + .flatMap(new Function<GroupedObservable<Integer, Event>, Observable<String>>() { + + @Override + public Observable<String> apply(GroupedObservable<Integer, Event> eventGroupedObservable) { + System.out.println("testUnsubscribe => GroupedObservable Key: " + eventGroupedObservable.getKey()); + groupCounter.incrementAndGet(); + + return eventGroupedObservable + .take(20) // limit to only 20 events on this group + .map(new Function<Event, String>() { + + @Override + public String apply(Event event) { + return "testUnsubscribe => Source: " + event.source + " Message: " + event.message; + } + }); + + } + }).subscribe(new DefaultObserver<String>() { + + @Override + public void onComplete() { + latch.countDown(); + } + + @Override + public void onError(Throwable e) { + e.printStackTrace(); + latch.countDown(); + } + + @Override + public void onNext(String outputMessage) { + System.out.println(outputMessage); + eventCounter.incrementAndGet(); + } + }); + + if (!latch.await(2000, TimeUnit.MILLISECONDS)) { + fail("timed out so likely did not unsubscribe correctly"); + } + assertEquals(1, subscribeCounter.get()); + assertEquals(1, groupCounter.get()); + assertEquals(20, eventCounter.get()); + // sentEvents will go until 'eventCounter' hits 20 and then unsubscribes + // which means it will also send (but ignore) the 19/20 events for the other group + // It will not however send all 100 events. + } + + @Test + public void unsubscribeViaTakeOnGroupThenMergeAndTake() { + final AtomicInteger subscribeCounter = new AtomicInteger(); + final AtomicInteger sentEventCounter = new AtomicInteger(); + final AtomicInteger eventCounter = new AtomicInteger(); + + SYNC_INFINITE_OBSERVABLE_OF_EVENT(4, subscribeCounter, sentEventCounter) + .groupBy(new Function<Event, Integer>() { + + @Override + public Integer apply(Event e) { + return e.source; + } + }) + // take 2 of the 4 groups + .take(2) + .flatMap(new Function<GroupedObservable<Integer, Event>, Observable<String>>() { + + @Override + public Observable<String> apply(GroupedObservable<Integer, Event> eventGroupedObservable) { + return eventGroupedObservable + .map(new Function<Event, String>() { + + @Override + public String apply(Event event) { + return "testUnsubscribe => Source: " + event.source + " Message: " + event.message; + } + }); + + } + }) + .take(30).subscribe(new Consumer<String>() { + + @Override + public void accept(String s) { + eventCounter.incrementAndGet(); + System.out.println("=> " + s); + } + + }); + + assertEquals(30, eventCounter.get()); + // we should send 28 additional events that are filtered out as they are in the groups we skip + assertEquals(58, sentEventCounter.get()); + } + + @Test + public void unsubscribeViaTakeOnGroupThenTakeOnInner() { + final AtomicInteger subscribeCounter = new AtomicInteger(); + final AtomicInteger sentEventCounter = new AtomicInteger(); + final AtomicInteger eventCounter = new AtomicInteger(); + + SYNC_INFINITE_OBSERVABLE_OF_EVENT(4, subscribeCounter, sentEventCounter) + .groupBy(new Function<Event, Integer>() { + + @Override + public Integer apply(Event e) { + return e.source; + } + }) + // take 2 of the 4 groups + .take(2) + .flatMap(new Function<GroupedObservable<Integer, Event>, Observable<String>>() { + + @Override + public Observable<String> apply(GroupedObservable<Integer, Event> eventGroupedObservable) { + int numToTake = 0; + if (eventGroupedObservable.getKey() == 1) { + numToTake = 10; + } else if (eventGroupedObservable.getKey() == 2) { + numToTake = 5; + } + return eventGroupedObservable + .take(numToTake) + .map(new Function<Event, String>() { + + @Override + public String apply(Event event) { + return "testUnsubscribe => Source: " + event.source + " Message: " + event.message; + } + }); + + } + }) + .subscribe(new Consumer<String>() { + + @Override + public void accept(String s) { + eventCounter.incrementAndGet(); + System.out.println("=> " + s); + } + + }); + + assertEquals(15, eventCounter.get()); + // we should send 22 additional events that are filtered out as they are skipped while taking the 15 we want + assertEquals(37, sentEventCounter.get()); + } + + @Test + public void staggeredCompletion() throws InterruptedException { + final AtomicInteger eventCounter = new AtomicInteger(); + final CountDownLatch latch = new CountDownLatch(1); + Observable.range(0, 100) + .groupBy(new Function<Integer, Integer>() { + + @Override + public Integer apply(Integer i) { + return i % 2; + } + }) + .flatMap(new Function<GroupedObservable<Integer, Integer>, Observable<Integer>>() { + + @Override + public Observable<Integer> apply(GroupedObservable<Integer, Integer> group) { + if (group.getKey() == 0) { + return group.delay(100, TimeUnit.MILLISECONDS).map(new Function<Integer, Integer>() { + @Override + public Integer apply(Integer t) { + return t * 10; + } + + }); + } else { + return group; + } + } + }) + .subscribe(new DefaultObserver<Integer>() { + + @Override + public void onComplete() { + System.out.println("=> onComplete"); + latch.countDown(); + } + + @Override + public void onError(Throwable e) { + e.printStackTrace(); + latch.countDown(); + } + + @Override + public void onNext(Integer s) { + eventCounter.incrementAndGet(); + System.out.println("=> " + s); + } + }); + + if (!latch.await(3000, TimeUnit.MILLISECONDS)) { + fail("timed out"); + } + + assertEquals(100, eventCounter.get()); + } + + @Test + public void completionIfInnerNotSubscribed() throws InterruptedException { + final CountDownLatch latch = new CountDownLatch(1); + final AtomicInteger eventCounter = new AtomicInteger(); + Observable.range(0, 100) + .groupBy(new Function<Integer, Integer>() { + + @Override + public Integer apply(Integer i) { + return i % 2; + } + }) + .subscribe(new DefaultObserver<GroupedObservable<Integer, Integer>>() { + + @Override + public void onComplete() { + latch.countDown(); + } + + @Override + public void onError(Throwable e) { + e.printStackTrace(); + latch.countDown(); + } + + @Override + public void onNext(GroupedObservable<Integer, Integer> s) { + eventCounter.incrementAndGet(); + System.out.println("=> " + s); + } + }); + if (!latch.await(500, TimeUnit.MILLISECONDS)) { + fail("timed out - never got completion"); + } + // Behavior change: groups not subscribed immediately will be automatically abandoned + // so this leads to group recreation + assertEquals(100, eventCounter.get()); + } + + @Test + public void ignoringGroups() { + final AtomicInteger subscribeCounter = new AtomicInteger(); + final AtomicInteger sentEventCounter = new AtomicInteger(); + final AtomicInteger eventCounter = new AtomicInteger(); + + SYNC_INFINITE_OBSERVABLE_OF_EVENT(4, subscribeCounter, sentEventCounter) + .groupBy(new Function<Event, Integer>() { + + @Override + public Integer apply(Event e) { + return e.source; + } + }) + .flatMap(new Function<GroupedObservable<Integer, Event>, Observable<String>>() { + + @Override + public Observable<String> apply(GroupedObservable<Integer, Event> eventGroupedObservable) { + Observable<Event> eventStream = eventGroupedObservable; + if (eventGroupedObservable.getKey() >= 2) { + // filter these + eventStream = eventGroupedObservable.filter(new Predicate<Event>() { + @Override + public boolean test(Event t1) { + return false; + } + }); + } + + return eventStream + .map(new Function<Event, String>() { + + @Override + public String apply(Event event) { + return "testUnsubscribe => Source: " + event.source + " Message: " + event.message; + } + }); + + } + }) + .take(30).subscribe(new Consumer<String>() { + + @Override + public void accept(String s) { + eventCounter.incrementAndGet(); + System.out.println("=> " + s); + } + + }); + + assertEquals(30, eventCounter.get()); + // we should send 30 additional events that are filtered out as they are in the groups we skip + assertEquals(60, sentEventCounter.get()); + } + + @Test + public void firstGroupsCompleteAndParentSlowToThenEmitFinalGroupsAndThenComplete() throws InterruptedException { + final CountDownLatch first = new CountDownLatch(2); // there are two groups to first complete + final ArrayList<String> results = new ArrayList<>(); + Observable.unsafeCreate(new ObservableSource<Integer>() { + + @Override + public void subscribe(Observer<? super Integer> sub) { + sub.onSubscribe(Disposable.empty()); + sub.onNext(1); + sub.onNext(2); + sub.onNext(1); + sub.onNext(2); + try { + first.await(); + } catch (InterruptedException e) { + sub.onError(e); + return; + } + sub.onNext(3); + sub.onNext(3); + sub.onComplete(); + } + + }).groupBy(new Function<Integer, Integer>() { + + @Override + public Integer apply(Integer t) { + return t; + } + + }).flatMap(new Function<GroupedObservable<Integer, Integer>, Observable<String>>() { + + @Override + public Observable<String> apply(final GroupedObservable<Integer, Integer> group) { + if (group.getKey() < 3) { + return group.map(new Function<Integer, String>() { + + @Override + public String apply(Integer t1) { + return "first groups: " + t1; + } + + }) + // must take(2) so an onComplete + unsubscribe happens on these first 2 groups + .take(2).doOnComplete(new Action() { + + @Override + public void run() { + first.countDown(); + } + + }); + } else { + return group.map(new Function<Integer, String>() { + + @Override + public String apply(Integer t1) { + return "last group: " + t1; + } + + }); + } + } + + }).blockingForEach(new Consumer<String>() { + + @Override + public void accept(String s) { + results.add(s); + } + + }); + + System.out.println("Results: " + results); + assertEquals(6, results.size()); + } + + @Test + public void firstGroupsCompleteAndParentSlowToThenEmitFinalGroupsWhichThenSubscribesOnAndDelaysAndThenCompletes() throws InterruptedException { + System.err.println("----------------------------------------------------------------------------------------------"); + final CountDownLatch first = new CountDownLatch(2); // there are two groups to first complete + final ArrayList<String> results = new ArrayList<>(); + Observable.unsafeCreate(new ObservableSource<Integer>() { + + @Override + public void subscribe(Observer<? super Integer> sub) { + sub.onSubscribe(Disposable.empty()); + sub.onNext(1); + sub.onNext(2); + sub.onNext(1); + sub.onNext(2); + try { + first.await(); + } catch (InterruptedException e) { + sub.onError(e); + return; + } + sub.onNext(3); + sub.onNext(3); + sub.onComplete(); + } + + }).groupBy(new Function<Integer, Integer>() { + + @Override + public Integer apply(Integer t) { + return t; + } + + }).flatMap(new Function<GroupedObservable<Integer, Integer>, Observable<String>>() { + + @Override + public Observable<String> apply(final GroupedObservable<Integer, Integer> group) { + if (group.getKey() < 3) { + return group.map(new Function<Integer, String>() { + + @Override + public String apply(Integer t1) { + return "first groups: " + t1; + } + + }) + // must take(2) so an onComplete + unsubscribe happens on these first 2 groups + .take(2).doOnComplete(new Action() { + + @Override + public void run() { + first.countDown(); + } + + }); + } else { + return group.subscribeOn(Schedulers.newThread()).delay(400, TimeUnit.MILLISECONDS).map(new Function<Integer, String>() { + + @Override + public String apply(Integer t1) { + return "last group: " + t1; + } + + }).doOnEach(new Consumer<Notification<String>>() { + + @Override + public void accept(Notification<String> t1) { + System.err.println("subscribeOn notification => " + t1); + } + + }); + } + } + + }).doOnEach(new Consumer<Notification<String>>() { + + @Override + public void accept(Notification<String> t1) { + System.err.println("outer notification => " + t1); + } + + }).blockingForEach(new Consumer<String>() { + + @Override + public void accept(String s) { + results.add(s); + } + + }); + + System.out.println("Results: " + results); + assertEquals(6, results.size()); + } + + @Test + public void firstGroupsCompleteAndParentSlowToThenEmitFinalGroupsWhichThenObservesOnAndDelaysAndThenCompletes() throws InterruptedException { + final CountDownLatch first = new CountDownLatch(2); // there are two groups to first complete + final ArrayList<String> results = new ArrayList<>(); + Observable.unsafeCreate(new ObservableSource<Integer>() { + + @Override + public void subscribe(Observer<? super Integer> sub) { + sub.onSubscribe(Disposable.empty()); + sub.onNext(1); + sub.onNext(2); + sub.onNext(1); + sub.onNext(2); + try { + first.await(); + } catch (InterruptedException e) { + sub.onError(e); + return; + } + sub.onNext(3); + sub.onNext(3); + sub.onComplete(); + } + + }).groupBy(new Function<Integer, Integer>() { + + @Override + public Integer apply(Integer t) { + return t; + } + + }).flatMap(new Function<GroupedObservable<Integer, Integer>, Observable<String>>() { + + @Override + public Observable<String> apply(final GroupedObservable<Integer, Integer> group) { + if (group.getKey() < 3) { + return group.map(new Function<Integer, String>() { + + @Override + public String apply(Integer t1) { + return "first groups: " + t1; + } + + }) + // must take(2) so an onComplete + unsubscribe happens on these first 2 groups + .take(2).doOnComplete(new Action() { + + @Override + public void run() { + first.countDown(); + } + + }); + } else { + return group.observeOn(Schedulers.newThread()).delay(400, TimeUnit.MILLISECONDS).map(new Function<Integer, String>() { + + @Override + public String apply(Integer t1) { + return "last group: " + t1; + } + + }); + } + } + + }).blockingForEach(new Consumer<String>() { + + @Override + public void accept(String s) { + results.add(s); + } + + }); + + System.out.println("Results: " + results); + assertEquals(6, results.size()); + } + + @Test + public void groupsWithNestedSubscribeOn() throws InterruptedException { + final ArrayList<String> results = new ArrayList<>(); + Observable.unsafeCreate(new ObservableSource<Integer>() { + + @Override + public void subscribe(Observer<? super Integer> sub) { + sub.onSubscribe(Disposable.empty()); + sub.onNext(1); + sub.onNext(2); + sub.onNext(1); + sub.onNext(2); + sub.onComplete(); + } + + }).groupBy(new Function<Integer, Integer>() { + + @Override + public Integer apply(Integer t) { + return t; + } + + }).flatMap(new Function<GroupedObservable<Integer, Integer>, Observable<String>>() { + + @Override + public Observable<String> apply(final GroupedObservable<Integer, Integer> group) { + return group.subscribeOn(Schedulers.newThread()).map(new Function<Integer, String>() { + + @Override + public String apply(Integer t1) { + System.out.println("Received: " + t1 + " on group : " + group.getKey()); + return "first groups: " + t1; + } + + }); + } + + }).doOnEach(new Consumer<Notification<String>>() { + + @Override + public void accept(Notification<String> t1) { + System.out.println("notification => " + t1); + } + + }).blockingForEach(new Consumer<String>() { + + @Override + public void accept(String s) { + results.add(s); + } + + }); + + System.out.println("Results: " + results); + assertEquals(4, results.size()); + } + + @Test + public void groupsWithNestedObserveOn() throws InterruptedException { + final ArrayList<String> results = new ArrayList<>(); + Observable.unsafeCreate(new ObservableSource<Integer>() { + + @Override + public void subscribe(Observer<? super Integer> sub) { + sub.onSubscribe(Disposable.empty()); + sub.onNext(1); + sub.onNext(2); + sub.onNext(1); + sub.onNext(2); + sub.onComplete(); + } + + }).groupBy(new Function<Integer, Integer>() { + + @Override + public Integer apply(Integer t) { + return t; + } + + }).flatMap(new Function<GroupedObservable<Integer, Integer>, Observable<String>>() { + + @Override + public Observable<String> apply(final GroupedObservable<Integer, Integer> group) { + return group.observeOn(Schedulers.newThread()).delay(400, TimeUnit.MILLISECONDS).map(new Function<Integer, String>() { + + @Override + public String apply(Integer t1) { + return "first groups: " + t1; + } + + }); + } + + }).blockingForEach(new Consumer<String>() { + + @Override + public void accept(String s) { + results.add(s); + } + + }); + + System.out.println("Results: " + results); + assertEquals(4, results.size()); + } + + private static class Event { + int source; + String message; + + @Override + public String toString() { + return "Event => source: " + source + " message: " + message; + } + } + + Observable<Event> ASYNC_INFINITE_OBSERVABLE_OF_EVENT(final int numGroups, final AtomicInteger subscribeCounter, final AtomicInteger sentEventCounter) { + return SYNC_INFINITE_OBSERVABLE_OF_EVENT(numGroups, subscribeCounter, sentEventCounter).subscribeOn(Schedulers.newThread()); + }; + + Observable<Event> SYNC_INFINITE_OBSERVABLE_OF_EVENT(final int numGroups, final AtomicInteger subscribeCounter, final AtomicInteger sentEventCounter) { + return Observable.unsafeCreate(new ObservableSource<Event>() { + + @Override + public void subscribe(final Observer<? super Event> op) { + Disposable d = Disposable.empty(); + op.onSubscribe(d); + subscribeCounter.incrementAndGet(); + int i = 0; + while (!d.isDisposed()) { + i++; + Event e = new Event(); + e.source = i % numGroups; + e.message = "Event-" + i; + op.onNext(e); + sentEventCounter.incrementAndGet(); + } + op.onComplete(); + } + + }); + }; + + @Test + public void groupByOnAsynchronousSourceAcceptsMultipleSubscriptions() throws InterruptedException { + + // choose an asynchronous source + Observable<Long> source = Observable.interval(10, TimeUnit.MILLISECONDS).take(1); + + // apply groupBy to the source + Observable<GroupedObservable<Boolean, Long>> stream = source.groupBy(IS_EVEN); + + // create two observers + Observer<GroupedObservable<Boolean, Long>> o1 = TestHelper.mockObserver(); + Observer<GroupedObservable<Boolean, Long>> o2 = TestHelper.mockObserver(); + + // subscribe with the observers + stream.subscribe(o1); + stream.subscribe(o2); + + // check that subscriptions were successful + verify(o1, never()).onError(Mockito.<Throwable> any()); + verify(o2, never()).onError(Mockito.<Throwable> any()); + } + + private static Function<Long, Boolean> IS_EVEN = new Function<Long, Boolean>() { + + @Override + public Boolean apply(Long n) { + return n % 2 == 0; + } + }; + + private static Function<Integer, Boolean> IS_EVEN2 = new Function<Integer, Boolean>() { + + @Override + public Boolean apply(Integer n) { + return n % 2 == 0; + } + }; + + @Test + public void groupByBackpressure() throws InterruptedException { + + TestObserver<String> to = new TestObserver<>(); + + Observable.range(1, 4000) + .groupBy(IS_EVEN2) + .flatMap(new Function<GroupedObservable<Boolean, Integer>, Observable<String>>() { + + @Override + public Observable<String> apply(final GroupedObservable<Boolean, Integer> g) { + return g.observeOn(Schedulers.computation()).map(new Function<Integer, String>() { + + @Override + public String apply(Integer l) { + if (g.getKey()) { + try { + Thread.sleep(1); + } catch (InterruptedException e) { + } + return l + " is even."; + } else { + return l + " is odd."; + } + } + + }); + } + + }).subscribe(to); + to.awaitDone(5, TimeUnit.SECONDS); + to.assertNoErrors(); + } + + <T, R> Function<T, R> just(final R value) { + return new Function<T, R>() { + @Override + public R apply(T t1) { + return value; + } + }; + } + + <T> Function<Integer, T> fail(T dummy) { + return new Function<Integer, T>() { + @Override + public T apply(Integer t1) { + throw new RuntimeException("Forced failure"); + } + }; + } + + <T, R> Function<T, R> fail2(R dummy2) { + return new Function<T, R>() { + @Override + public R apply(T t1) { + throw new RuntimeException("Forced failure"); + } + }; + } + + Function<Integer, Integer> dbl = new Function<Integer, Integer>() { + @Override + public Integer apply(Integer t1) { + return t1 * 2; + } + }; + Function<Integer, Integer> identity = new Function<Integer, Integer>() { + @Override + public Integer apply(Integer v) { + return v; + } + }; + + @Test + public void normalBehavior() { + Observable<String> source = Observable.fromIterable(Arrays.asList( + " foo", + " FoO ", + "baR ", + "foO ", + " Baz ", + " qux ", + " bar", + " BAR ", + "FOO ", + "baz ", + " bAZ ", + " fOo " + )); + + /* + * foo FoO foO FOO fOo + * baR bar BAR + * Baz baz bAZ + * qux + * + */ + Function<String, String> keysel = new Function<String, String>() { + @Override + public String apply(String t1) { + return t1.trim().toLowerCase(); + } + }; + Function<String, String> valuesel = new Function<String, String>() { + @Override + public String apply(String t1) { + return t1 + t1; + } + }; + + Observable<String> m = source.groupBy(keysel, valuesel) + .flatMap(new Function<GroupedObservable<String, String>, Observable<String>>() { + @Override + public Observable<String> apply(final GroupedObservable<String, String> g) { + System.out.println("-----------> NEXT: " + g.getKey()); + return g.take(2).map(new Function<String, String>() { + + int count; + + @Override + public String apply(String v) { + System.out.println(v); + return g.getKey() + "-" + count++; + } + + }); + } + }); + + TestObserver<String> to = new TestObserver<>(); + m.subscribe(to); + to.awaitDone(5, TimeUnit.SECONDS); + System.out.println("ts .get " + to.values()); + to.assertNoErrors(); + assertEquals(to.values(), + Arrays.asList("foo-0", "foo-1", "bar-0", "foo-0", "baz-0", "qux-0", "bar-1", "bar-0", "foo-1", "baz-1", "baz-0", "foo-0")); + + } + + @Test + public void keySelectorThrows() { + Observable<Integer> source = Observable.just(0, 1, 2, 3, 4, 5, 6); + + Observable<Integer> m = source.groupBy(fail(0), dbl).flatMap(FLATTEN_INTEGER); + + TestObserverEx<Integer> to = new TestObserverEx<>(); + m.subscribe(to); + to.awaitDone(5, TimeUnit.SECONDS); + assertEquals(1, to.errors().size()); + to.assertNoValues(); + } + + @Test + @SuppressUndeliverable + public void valueSelectorThrows() { + Observable<Integer> source = Observable.just(0, 1, 2, 3, 4, 5, 6); + + Observable<Integer> m = source.groupBy(identity, fail(0)).flatMap(FLATTEN_INTEGER); + TestObserverEx<Integer> to = new TestObserverEx<>(); + m.subscribe(to); + to.awaitDone(5, TimeUnit.SECONDS); + assertEquals(1, to.errors().size()); + to.assertNoValues(); + + } + + @Test + public void innerEscapeCompleted() { + Observable<Integer> source = Observable.just(0); + + Observable<Integer> m = source.groupBy(identity, dbl).flatMap(FLATTEN_INTEGER); + + TestObserver<Object> to = new TestObserver<>(); + m.subscribe(to); + to.awaitDone(5, TimeUnit.SECONDS); + to.assertNoErrors(); + System.out.println(to.values()); + } + + /** + * Assert we get an IllegalStateException if trying to subscribe to an inner GroupedObservable more than once. + */ + @Test + public void exceptionIfSubscribeToChildMoreThanOnce() { + Observable<Integer> source = Observable.just(0); + + final AtomicReference<GroupedObservable<Integer, Integer>> inner = new AtomicReference<>(); + + Observable<GroupedObservable<Integer, Integer>> m = source.groupBy(identity, dbl); + + m.subscribe(new Consumer<GroupedObservable<Integer, Integer>>() { + @Override + public void accept(GroupedObservable<Integer, Integer> t1) { + inner.set(t1); + } + }); + + inner.get().subscribe(); + + Observer<Integer> o2 = TestHelper.mockObserver(); + + inner.get().subscribe(o2); + + verify(o2, never()).onComplete(); + verify(o2, never()).onNext(anyInt()); + verify(o2).onError(any(IllegalStateException.class)); + } + + @Test + @SuppressUndeliverable + public void error2() { + Observable<Integer> source = Observable.concat(Observable.just(0), + Observable.<Integer> error(new TestException("Forced failure"))); + + Observable<Integer> m = source.groupBy(identity, dbl).flatMap(FLATTEN_INTEGER); + + TestObserverEx<Object> to = new TestObserverEx<>(); + m.subscribe(to); + to.awaitDone(5, TimeUnit.SECONDS); + assertEquals(1, to.errors().size()); + to.assertValueCount(1); + } + + @Test + public void groupByBackpressure3() throws InterruptedException { + TestObserver<String> to = new TestObserver<>(); + + Observable.range(1, 4000).groupBy(IS_EVEN2).flatMap(new Function<GroupedObservable<Boolean, Integer>, Observable<String>>() { + + @Override + public Observable<String> apply(final GroupedObservable<Boolean, Integer> g) { + return g.doOnComplete(new Action() { + + @Override + public void run() { + System.out.println("//////////////////// COMPLETED-A"); + } + + }).observeOn(Schedulers.computation()).map(new Function<Integer, String>() { + + int c; + + @Override + public String apply(Integer l) { + if (g.getKey()) { + if (c++ < 400) { + try { + Thread.sleep(1); + } catch (InterruptedException e) { + } + } + return l + " is even."; + } else { + return l + " is odd."; + } + } + + }).doOnComplete(new Action() { + + @Override + public void run() { + System.out.println("//////////////////// COMPLETED-B"); + } + + }); + } + + }).doOnEach(new Consumer<Notification<String>>() { + + @Override + public void accept(Notification<String> t1) { + System.out.println("NEXT: " + t1); + } + + }).subscribe(to); + to.awaitDone(5, TimeUnit.SECONDS); + to.assertNoErrors(); + } + + @Test + public void groupByBackpressure2() throws InterruptedException { + + TestObserver<String> to = new TestObserver<>(); + + Observable.range(1, 4000).groupBy(IS_EVEN2).flatMap(new Function<GroupedObservable<Boolean, Integer>, Observable<String>>() { + + @Override + public Observable<String> apply(final GroupedObservable<Boolean, Integer> g) { + return g.take(2).observeOn(Schedulers.computation()).map(new Function<Integer, String>() { + + @Override + public String apply(Integer l) { + if (g.getKey()) { + try { + Thread.sleep(1); + } catch (InterruptedException e) { + } + return l + " is even."; + } else { + return l + " is odd."; + } + } + + }); + } + + }).subscribe(to); + to.awaitDone(5, TimeUnit.SECONDS); + to.assertNoErrors(); + } + + static Function<GroupedObservable<Integer, Integer>, Observable<Integer>> FLATTEN_INTEGER = new Function<GroupedObservable<Integer, Integer>, Observable<Integer>>() { + + @Override + public Observable<Integer> apply(GroupedObservable<Integer, Integer> t) { + return t; + } + + }; + + @Test + public void groupByWithNullKey() { + final String[] key = new String[]{"uninitialized"}; + final List<String> values = new ArrayList<>(); + Observable.just("a", "b", "c").groupBy(new Function<String, String>() { + + @Override + public String apply(String value) { + return null; + } + }).subscribe(new Consumer<GroupedObservable<String, String>>() { + + @Override + public void accept(GroupedObservable<String, String> groupedObservable) { + key[0] = groupedObservable.getKey(); + groupedObservable.subscribe(new Consumer<String>() { + + @Override + public void accept(String s) { + values.add(s); + } + }); + } + }); + assertNull(key[0]); + assertEquals(Arrays.asList("a", "b", "c"), values); + } + + @Test + public void groupByUnsubscribe() { + final Disposable upstream = mock(Disposable.class); + Observable<Integer> o = Observable.unsafeCreate( + new ObservableSource<Integer>() { + @Override + public void subscribe(Observer<? super Integer> observer) { + observer.onSubscribe(upstream); + } + } + ); + TestObserver<Object> to = new TestObserver<>(); + + o.groupBy(new Function<Integer, Integer>() { + + @Override + public Integer apply(Integer integer) { + return null; + } + }).subscribe(to); + + to.dispose(); + + verify(upstream).dispose(); + } + + @Test + public void groupByShouldPropagateError() { + final Throwable e = new RuntimeException("Oops"); + final TestObserverEx<Integer> inner1 = new TestObserverEx<>(); + final TestObserverEx<Integer> inner2 = new TestObserverEx<>(); + + final TestObserverEx<GroupedObservable<Integer, Integer>> outer + = new TestObserverEx<>(new DefaultObserver<GroupedObservable<Integer, Integer>>() { + + @Override + public void onComplete() { + } + + @Override + public void onError(Throwable e) { + } + + @Override + public void onNext(GroupedObservable<Integer, Integer> o) { + if (o.getKey() == 0) { + o.subscribe(inner1); + } else { + o.subscribe(inner2); + } + } + }); + Observable.unsafeCreate( + new ObservableSource<Integer>() { + @Override + public void subscribe(Observer<? super Integer> observer) { + observer.onSubscribe(Disposable.empty()); + observer.onNext(0); + observer.onNext(1); + observer.onError(e); + } + } + ).groupBy(new Function<Integer, Integer>() { + + @Override + public Integer apply(Integer i) { + return i % 2; + } + }).subscribe(outer); + assertEquals(Arrays.asList(e), outer.errors()); + assertEquals(Arrays.asList(e), inner1.errors()); + assertEquals(Arrays.asList(e), inner2.errors()); + } + + @Test + @SuppressUndeliverable + public void keySelectorAndDelayError() { + Observable.just(1).concatWith(Observable.<Integer>error(new TestException())) + .groupBy(Functions.<Integer>identity(), true) + .flatMap(new Function<GroupedObservable<Integer, Integer>, ObservableSource<Integer>>() { + @Override + public ObservableSource<Integer> apply(GroupedObservable<Integer, Integer> g) throws Exception { + return g; + } + }) + .test() + .assertFailure(TestException.class, 1); + } + + @Test + @SuppressUndeliverable + public void keyAndValueSelectorAndDelayError() { + Observable.just(1).concatWith(Observable.<Integer>error(new TestException())) + .groupBy(Functions.<Integer>identity(), Functions.<Integer>identity(), true) + .flatMap(new Function<GroupedObservable<Integer, Integer>, ObservableSource<Integer>>() { + @Override + public ObservableSource<Integer> apply(GroupedObservable<Integer, Integer> g) throws Exception { + return g; + } + }) + .test() + .assertFailure(TestException.class, 1); + } + + @Test + public void dispose() { + TestHelper.checkDisposed(Observable.just(1).groupBy(Functions.justFunction(1))); + + Observable.just(1) + .groupBy(Functions.justFunction(1)) + .doOnNext(new Consumer<GroupedObservable<Integer, Integer>>() { + @Override + public void accept(GroupedObservable<Integer, Integer> g) throws Exception { + TestHelper.checkDisposed(g); + } + }) + .test(); + } + + @Test + public void reentrantComplete() { + final PublishSubject<Integer> ps = PublishSubject.create(); + + TestObserver<Integer> to = new TestObserver<Integer>() { + @Override + public void onNext(Integer t) { + super.onNext(t); + if (t == 1) { + ps.onComplete(); + } + } + }; + + Observable.merge(ps.groupBy(Functions.justFunction(1))) + .subscribe(to); + + ps.onNext(1); + + to.assertResult(1); + } + + @Test + public void reentrantCompleteCancel() { + final PublishSubject<Integer> ps = PublishSubject.create(); + + TestObserverEx<Integer> to = new TestObserverEx<Integer>() { + @Override + public void onNext(Integer t) { + super.onNext(t); + if (t == 1) { + ps.onComplete(); + dispose(); + } + } + }; + + Observable.merge(ps.groupBy(Functions.justFunction(1))) + .subscribe(to); + + ps.onNext(1); + + to.assertSubscribed().assertValue(1).assertNoErrors().assertNotComplete(); + } + + @Test + public void delayErrorSimpleComplete() { + Observable.just(1) + .groupBy(Functions.justFunction(1), true) + .flatMap(Functions.<Observable<Integer>>identity()) + .test() + .assertResult(1); + } + + @Test + public void cancelOverFlatmapRace() { + for (int i = 0; i < TestHelper.RACE_LONG_LOOPS; i++) { + + final TestObserver<Integer> to = new TestObserver<>(); + + final PublishSubject<Integer> ps = PublishSubject.create(); + + ps.groupBy(new Function<Integer, Integer>() { + @Override + public Integer apply(Integer v) throws Throwable { + return v % 10; + } + }) + .flatMap(new Function<GroupedObservable<Integer, Integer>, ObservableSource<Integer>>() { + @Override + public ObservableSource<Integer> apply(GroupedObservable<Integer, Integer> v) + throws Throwable { + return v; + } + }) + .subscribe(to); + + Runnable r1 = new Runnable() { + @Override + public void run() { + for (int j = 0; j < 1000; j++) { + ps.onNext(j); + } + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + to.dispose(); + } + }; + + TestHelper.race(r1, r2); + + assertFalse("Round " + i, ps.hasObservers()); + } + } + + @Test + public void abandonedGroupsNoDataloss() { + final List<GroupedObservable<Integer, Integer>> groups = new ArrayList<>(); + + Observable.range(1, 1000) + .groupBy(new Function<Integer, Integer>() { + @Override + public Integer apply(Integer v) throws Throwable { + return v % 10; + } + }) + .doOnNext(new Consumer<GroupedObservable<Integer, Integer>>() { + @Override + public void accept(GroupedObservable<Integer, Integer> v) throws Throwable { + groups.add(v); + } + }) + .test() + .assertValueCount(1000) + .assertComplete() + .assertNoErrors(); + + Observable.concat(groups) + .test() + .assertValueCount(1000) + .assertNoErrors() + .assertComplete(); + } + + @Test + public void newGroupValueSelectorFails() { + TestObserver<Object> to1 = new TestObserver<>(); + final TestObserver<Object> to2 = new TestObserver<>(); + + Observable.just(1) + .groupBy(Functions.<Integer>identity(), new Function<Integer, Object>() { + @Override + public Object apply(Integer v) throws Throwable { + throw new TestException(); + } + }) + .doOnNext(new Consumer<GroupedObservable<Integer, Object>>() { + @Override + public void accept(GroupedObservable<Integer, Object> g) throws Throwable { + g.subscribe(to2); + } + }) + .subscribe(to1); + + to1.assertValueCount(1) + .assertError(TestException.class) + .assertNotComplete(); + + to2.assertFailure(TestException.class); + } + + @Test + public void existingGroupValueSelectorFails() { + TestObserver<Object> to1 = new TestObserver<>(); + final TestObserver<Object> to2 = new TestObserver<>(); + + Observable.just(1, 2) + .groupBy(Functions.justFunction(1), new Function<Integer, Object>() { + @Override + public Object apply(Integer v) throws Throwable { + if (v == 2) { + throw new TestException(); + } + return v; + } + }) + .doOnNext(new Consumer<GroupedObservable<Integer, Object>>() { + @Override + public void accept(GroupedObservable<Integer, Object> g) throws Throwable { + g.subscribe(to2); + } + }) + .subscribe(to1); + + to1.assertValueCount(1) + .assertError(TestException.class) + .assertNotComplete(); + + to2.assertFailure(TestException.class, 1); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeObservable(o -> o.groupBy(v -> v)); + } + + @Test + public void nullKeyDisposeGroup() { + Observable.just(1) + .groupBy(v -> null) + .flatMap(v -> v.take(1)) + .test() + .assertResult(1); + } + + @Test + public void groupSubscribeOnNextRace() throws Throwable { + for (int i = 0; i < TestHelper.RACE_LONG_LOOPS; i++) { + BehaviorSubject<Integer> bs = BehaviorSubject.createDefault(1); + CountDownLatch cdl = new CountDownLatch(1); + + bs.groupBy(v -> 1) + .doOnNext(g -> { + TestHelper.raceOther(() -> { + g.test(); + }, cdl); + }) + .test(); + + cdl.await(); + } + } + + @Test + public void abandonedGroupDispose() { + AtomicReference<Observable<Integer>> ref = new AtomicReference<>(); + + Observable.just(1) + .groupBy(v -> 1) + .doOnNext(ref::set) + .test(); + + ref.get().take(1).test().assertResult(1); + } + + @Test + public void delayErrorCompleteMoreWorkInGroup() { + PublishSubject<Integer> ps = PublishSubject.create(); + + TestObserver<Integer> to = ps.groupBy(v -> 1, true) + .flatMap(g -> g.doOnNext(v -> { + if (v == 1) { + ps.onNext(2); + ps.onComplete(); + } + }) + ) + .test() + ; + + ps.onNext(1); + + to.assertResult(1, 2); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableGroupJoinTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableGroupJoinTest.java new file mode 100644 index 0000000000..7959f90a05 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableGroupJoinTest.java @@ -0,0 +1,766 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import java.util.*; + +import org.junit.*; +import org.mockito.MockitoAnnotations; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.core.Observer; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.internal.operators.observable.ObservableGroupJoin.*; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.subjects.PublishSubject; +import io.reactivex.rxjava3.testsupport.*; + +public class ObservableGroupJoinTest extends RxJavaTest { + + Observer<Object> observer = TestHelper.mockObserver(); + + BiFunction<Integer, Integer, Integer> add = new BiFunction<Integer, Integer, Integer>() { + @Override + public Integer apply(Integer t1, Integer t2) { + return t1 + t2; + } + }; + + <T> Function<Integer, Observable<T>> just(final Observable<T> observable) { + return new Function<Integer, Observable<T>>() { + @Override + public Observable<T> apply(Integer t1) { + return observable; + } + }; + } + + <T, R> Function<T, Observable<R>> just2(final Observable<R> observable) { + return new Function<T, Observable<R>>() { + @Override + public Observable<R> apply(T t1) { + return observable; + } + }; + } + + BiFunction<Integer, Observable<Integer>, Observable<Integer>> add2 = new BiFunction<Integer, Observable<Integer>, Observable<Integer>>() { + @Override + public Observable<Integer> apply(final Integer leftValue, Observable<Integer> rightValues) { + return rightValues.map(new Function<Integer, Integer>() { + @Override + public Integer apply(Integer rightValue) throws Throwable { + return add.apply(leftValue, rightValue); + } + }); + } + + }; + + @Before + public void before() { + MockitoAnnotations.openMocks(this); + } + + @Test + public void behaveAsJoin() { + PublishSubject<Integer> source1 = PublishSubject.create(); + PublishSubject<Integer> source2 = PublishSubject.create(); + + Observable<Integer> m = Observable.merge(source1.groupJoin(source2, + just(Observable.never()), + just(Observable.never()), add2)); + + m.subscribe(observer); + + source1.onNext(1); + source1.onNext(2); + source1.onNext(4); + + source2.onNext(16); + source2.onNext(32); + source2.onNext(64); + + source1.onComplete(); + source2.onComplete(); + + verify(observer, times(1)).onNext(17); + verify(observer, times(1)).onNext(18); + verify(observer, times(1)).onNext(20); + verify(observer, times(1)).onNext(33); + verify(observer, times(1)).onNext(34); + verify(observer, times(1)).onNext(36); + verify(observer, times(1)).onNext(65); + verify(observer, times(1)).onNext(66); + verify(observer, times(1)).onNext(68); + + verify(observer, times(1)).onComplete(); //Never emitted? + verify(observer, never()).onError(any(Throwable.class)); + } + + class Person { + final int id; + final String name; + + Person(int id, String name) { + this.id = id; + this.name = name; + } + } + + class PersonFruit { + final int personId; + final String fruit; + + PersonFruit(int personId, String fruit) { + this.personId = personId; + this.fruit = fruit; + } + } + + class PPF { + final Person person; + final Observable<PersonFruit> fruits; + + PPF(Person person, Observable<PersonFruit> fruits) { + this.person = person; + this.fruits = fruits; + } + } + + @Test + public void normal1() { + Observable<Person> source1 = Observable.fromIterable(Arrays.asList( + new Person(1, "Joe"), + new Person(2, "Mike"), + new Person(3, "Charlie") + )); + + Observable<PersonFruit> source2 = Observable.fromIterable(Arrays.asList( + new PersonFruit(1, "Strawberry"), + new PersonFruit(1, "Apple"), + new PersonFruit(3, "Peach") + )); + + Observable<PPF> q = source1.groupJoin( + source2, + just2(Observable.<Object> never()), + just2(Observable.<Object> never()), + new BiFunction<Person, Observable<PersonFruit>, PPF>() { + @Override + public PPF apply(Person t1, Observable<PersonFruit> t2) { + return new PPF(t1, t2); + } + }); + + q.subscribe( + new Observer<PPF>() { + @Override + public void onNext(final PPF ppf) { + ppf.fruits.filter(new Predicate<PersonFruit>() { + @Override + public boolean test(PersonFruit t1) { + return ppf.person.id == t1.personId; + } + }).subscribe(new Consumer<PersonFruit>() { + @Override + public void accept(PersonFruit t1) { + observer.onNext(Arrays.asList(ppf.person.name, t1.fruit)); + } + }); + } + + @Override + public void onError(Throwable e) { + observer.onError(e); + } + + @Override + public void onComplete() { + observer.onComplete(); + } + + @Override + public void onSubscribe(Disposable d) { + } + + } + ); + + verify(observer, times(1)).onNext(Arrays.asList("Joe", "Strawberry")); + verify(observer, times(1)).onNext(Arrays.asList("Joe", "Apple")); + verify(observer, times(1)).onNext(Arrays.asList("Charlie", "Peach")); + + verify(observer, times(1)).onComplete(); + verify(observer, never()).onError(any(Throwable.class)); + } + + @Test + public void leftThrows() { + PublishSubject<Integer> source1 = PublishSubject.create(); + PublishSubject<Integer> source2 = PublishSubject.create(); + + Observable<Observable<Integer>> m = source1.groupJoin(source2, + just(Observable.never()), + just(Observable.never()), add2); + + m.subscribe(observer); + + source2.onNext(1); + source1.onError(new RuntimeException("Forced failure")); + + verify(observer, times(1)).onError(any(Throwable.class)); + verify(observer, never()).onComplete(); + verify(observer, never()).onNext(any()); + } + + @Test + public void rightThrows() { + PublishSubject<Integer> source1 = PublishSubject.create(); + PublishSubject<Integer> source2 = PublishSubject.create(); + + Observable<Observable<Integer>> m = source1.groupJoin(source2, + just(Observable.never()), + just(Observable.never()), add2); + + m.subscribe(observer); + + source1.onNext(1); + source2.onError(new RuntimeException("Forced failure")); + + verify(observer, times(1)).onNext(any(Observable.class)); + verify(observer, times(1)).onError(any(Throwable.class)); + verify(observer, never()).onComplete(); + } + + @Test + public void leftDurationThrows() { + PublishSubject<Integer> source1 = PublishSubject.create(); + PublishSubject<Integer> source2 = PublishSubject.create(); + + Observable<Integer> duration1 = Observable.<Integer> error(new RuntimeException("Forced failure")); + + Observable<Observable<Integer>> m = source1.groupJoin(source2, + just(duration1), + just(Observable.never()), add2); + m.subscribe(observer); + + source1.onNext(1); + + verify(observer, times(1)).onError(any(Throwable.class)); + verify(observer, never()).onComplete(); + verify(observer, never()).onNext(any()); + } + + @Test + public void rightDurationThrows() { + PublishSubject<Integer> source1 = PublishSubject.create(); + PublishSubject<Integer> source2 = PublishSubject.create(); + + Observable<Integer> duration1 = Observable.<Integer> error(new RuntimeException("Forced failure")); + + Observable<Observable<Integer>> m = source1.groupJoin(source2, + just(Observable.never()), + just(duration1), add2); + m.subscribe(observer); + + source2.onNext(1); + + verify(observer, times(1)).onError(any(Throwable.class)); + verify(observer, never()).onComplete(); + verify(observer, never()).onNext(any()); + } + + @Test + public void leftDurationSelectorThrows() { + PublishSubject<Integer> source1 = PublishSubject.create(); + PublishSubject<Integer> source2 = PublishSubject.create(); + + Function<Integer, Observable<Integer>> fail = new Function<Integer, Observable<Integer>>() { + @Override + public Observable<Integer> apply(Integer t1) { + throw new RuntimeException("Forced failure"); + } + }; + + Observable<Observable<Integer>> m = source1.groupJoin(source2, + fail, + just(Observable.never()), add2); + m.subscribe(observer); + + source1.onNext(1); + + verify(observer, times(1)).onError(any(Throwable.class)); + verify(observer, never()).onComplete(); + verify(observer, never()).onNext(any()); + } + + @Test + public void rightDurationSelectorThrows() { + PublishSubject<Integer> source1 = PublishSubject.create(); + PublishSubject<Integer> source2 = PublishSubject.create(); + + Function<Integer, Observable<Integer>> fail = new Function<Integer, Observable<Integer>>() { + @Override + public Observable<Integer> apply(Integer t1) { + throw new RuntimeException("Forced failure"); + } + }; + + Observable<Observable<Integer>> m = source1.groupJoin(source2, + just(Observable.never()), + fail, add2); + m.subscribe(observer); + + source2.onNext(1); + + verify(observer, times(1)).onError(any(Throwable.class)); + verify(observer, never()).onComplete(); + verify(observer, never()).onNext(any()); + } + + @Test + public void resultSelectorThrows() { + PublishSubject<Integer> source1 = PublishSubject.create(); + PublishSubject<Integer> source2 = PublishSubject.create(); + + BiFunction<Integer, Observable<Integer>, Integer> fail = new BiFunction<Integer, Observable<Integer>, Integer>() { + @Override + public Integer apply(Integer t1, Observable<Integer> t2) { + throw new RuntimeException("Forced failure"); + } + }; + + Observable<Integer> m = source1.groupJoin(source2, + just(Observable.never()), + just(Observable.never()), fail); + m.subscribe(observer); + + source1.onNext(1); + source2.onNext(2); + + verify(observer, times(1)).onError(any(Throwable.class)); + verify(observer, never()).onComplete(); + verify(observer, never()).onNext(any()); + } + + @Test + public void dispose() { + TestHelper.checkDisposed(Observable.just(1).groupJoin( + Observable.just(2), + new Function<Integer, ObservableSource<Object>>() { + @Override + public ObservableSource<Object> apply(Integer left) throws Exception { + return Observable.never(); + } + }, + new Function<Integer, ObservableSource<Object>>() { + @Override + public ObservableSource<Object> apply(Integer right) throws Exception { + return Observable.never(); + } + }, + new BiFunction<Integer, Observable<Integer>, Object>() { + @Override + public Object apply(Integer r, Observable<Integer> l) throws Exception { + return l; + } + } + )); + } + + @Test + public void innerCompleteLeft() { + Observable.just(1) + .groupJoin( + Observable.just(2), + new Function<Integer, ObservableSource<Object>>() { + @Override + public ObservableSource<Object> apply(Integer left) throws Exception { + return Observable.empty(); + } + }, + new Function<Integer, ObservableSource<Object>>() { + @Override + public ObservableSource<Object> apply(Integer right) throws Exception { + return Observable.never(); + } + }, + new BiFunction<Integer, Observable<Integer>, Observable<Integer>>() { + @Override + public Observable<Integer> apply(Integer r, Observable<Integer> l) throws Exception { + return l; + } + } + ) + .flatMap(Functions.<Observable<Integer>>identity()) + .test() + .assertResult(); + } + + @Test + public void innerErrorLeft() { + Observable.just(1) + .groupJoin( + Observable.just(2), + new Function<Integer, ObservableSource<Object>>() { + @Override + public ObservableSource<Object> apply(Integer left) throws Exception { + return Observable.error(new TestException()); + } + }, + new Function<Integer, ObservableSource<Object>>() { + @Override + public ObservableSource<Object> apply(Integer right) throws Exception { + return Observable.never(); + } + }, + new BiFunction<Integer, Observable<Integer>, Observable<Integer>>() { + @Override + public Observable<Integer> apply(Integer r, Observable<Integer> l) throws Exception { + return l; + } + } + ) + .flatMap(Functions.<Observable<Integer>>identity()) + .test() + .assertFailure(TestException.class); + } + + @Test + public void innerCompleteRight() { + Observable.just(1) + .groupJoin( + Observable.just(2), + new Function<Integer, ObservableSource<Object>>() { + @Override + public ObservableSource<Object> apply(Integer left) throws Exception { + return Observable.never(); + } + }, + new Function<Integer, ObservableSource<Object>>() { + @Override + public ObservableSource<Object> apply(Integer right) throws Exception { + return Observable.empty(); + } + }, + new BiFunction<Integer, Observable<Integer>, Observable<Integer>>() { + @Override + public Observable<Integer> apply(Integer r, Observable<Integer> l) throws Exception { + return l; + } + } + ) + .flatMap(Functions.<Observable<Integer>>identity()) + .test() + .assertResult(2); + } + + @Test + @SuppressUndeliverable + public void innerErrorRight() { + Observable.just(1) + .groupJoin( + Observable.just(2), + new Function<Integer, ObservableSource<Object>>() { + @Override + public ObservableSource<Object> apply(Integer left) throws Exception { + return Observable.never(); + } + }, + new Function<Integer, ObservableSource<Object>>() { + @Override + public ObservableSource<Object> apply(Integer right) throws Exception { + return Observable.error(new TestException()); + } + }, + new BiFunction<Integer, Observable<Integer>, Observable<Integer>>() { + @Override + public Observable<Integer> apply(Integer r, Observable<Integer> l) throws Exception { + return l; + } + } + ) + .flatMap(Functions.<Observable<Integer>>identity()) + .test() + .assertFailure(TestException.class); + } + + @Test + public void innerErrorRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final PublishSubject<Object> ps1 = PublishSubject.create(); + final PublishSubject<Object> ps2 = PublishSubject.create(); + + List<Throwable> errors = TestHelper.trackPluginErrors(); + + try { + TestObserverEx<Observable<Integer>> to = Observable.just(1) + .groupJoin( + Observable.just(2).concatWith(Observable.<Integer>never()), + new Function<Integer, ObservableSource<Object>>() { + @Override + public ObservableSource<Object> apply(Integer left) throws Exception { + return ps1; + } + }, + new Function<Integer, ObservableSource<Object>>() { + @Override + public ObservableSource<Object> apply(Integer right) throws Exception { + return ps2; + } + }, + new BiFunction<Integer, Observable<Integer>, Observable<Integer>>() { + @Override + public Observable<Integer> apply(Integer r, Observable<Integer> l) throws Exception { + return l; + } + } + ) + .to(TestHelper.<Observable<Integer>>testConsumer()); + + final TestException ex1 = new TestException(); + final TestException ex2 = new TestException(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + ps1.onError(ex1); + } + }; + Runnable r2 = new Runnable() { + @Override + public void run() { + ps2.onError(ex2); + } + }; + + TestHelper.race(r1, r2); + + to.assertError(Throwable.class).assertSubscribed().assertNotComplete().assertValueCount(1); + + Throwable exc = to.errors().get(0); + + if (exc instanceof CompositeException) { + List<Throwable> es = TestHelper.compositeList(exc); + TestHelper.assertError(es, 0, TestException.class); + TestHelper.assertError(es, 1, TestException.class); + } else { + to.assertError(TestException.class); + } + + if (!errors.isEmpty()) { + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } + } finally { + RxJavaPlugins.reset(); + } + } + } + + @Test + public void outerErrorRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final PublishSubject<Object> ps1 = PublishSubject.create(); + final PublishSubject<Object> ps2 = PublishSubject.create(); + + List<Throwable> errors = TestHelper.trackPluginErrors(); + + try { + TestObserverEx<Object> to = ps1 + .groupJoin( + ps2, + new Function<Object, ObservableSource<Object>>() { + @Override + public ObservableSource<Object> apply(Object left) throws Exception { + return Observable.never(); + } + }, + new Function<Object, ObservableSource<Object>>() { + @Override + public ObservableSource<Object> apply(Object right) throws Exception { + return Observable.never(); + } + }, + new BiFunction<Object, Observable<Object>, Observable<Object>>() { + @Override + public Observable<Object> apply(Object r, Observable<Object> l) throws Exception { + return l; + } + } + ) + .flatMap(Functions.<Observable<Object>>identity()) + .to(TestHelper.<Object>testConsumer()); + + final TestException ex1 = new TestException(); + final TestException ex2 = new TestException(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + ps1.onError(ex1); + } + }; + Runnable r2 = new Runnable() { + @Override + public void run() { + ps2.onError(ex2); + } + }; + + TestHelper.race(r1, r2); + + to.assertError(Throwable.class).assertSubscribed().assertNotComplete().assertNoValues(); + + Throwable exc = to.errors().get(0); + + if (exc instanceof CompositeException) { + List<Throwable> es = TestHelper.compositeList(exc); + TestHelper.assertError(es, 0, TestException.class); + TestHelper.assertError(es, 1, TestException.class); + } else { + to.assertError(TestException.class); + } + + if (!errors.isEmpty()) { + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } + } finally { + RxJavaPlugins.reset(); + } + } + } + + @Test + public void rightEmission() { + final PublishSubject<Object> ps1 = PublishSubject.create(); + final PublishSubject<Object> ps2 = PublishSubject.create(); + + TestObserver<Object> to = ps1 + .groupJoin( + ps2, + new Function<Object, ObservableSource<Object>>() { + @Override + public ObservableSource<Object> apply(Object left) throws Exception { + return Observable.never(); + } + }, + new Function<Object, ObservableSource<Object>>() { + @Override + public ObservableSource<Object> apply(Object right) throws Exception { + return Observable.never(); + } + }, + new BiFunction<Object, Observable<Object>, Observable<Object>>() { + @Override + public Observable<Object> apply(Object r, Observable<Object> l) throws Exception { + return l; + } + } + ) + .flatMap(Functions.<Observable<Object>>identity()) + .test(); + + ps2.onNext(2); + + ps1.onNext(1); + ps1.onComplete(); + + ps2.onComplete(); + + to.assertResult(2); + } + + @Test + public void leftRightState() { + JoinSupport js = mock(JoinSupport.class); + + LeftRightObserver o = new LeftRightObserver(js, false); + + assertFalse(o.isDisposed()); + + o.onNext(1); + o.onNext(2); + + o.dispose(); + + assertTrue(o.isDisposed()); + + verify(js).innerValue(false, 1); + verify(js).innerValue(false, 2); + } + + @Test + public void leftRightEndState() { + JoinSupport js = mock(JoinSupport.class); + + LeftRightEndObserver o = new LeftRightEndObserver(js, false, 0); + + assertFalse(o.isDisposed()); + + o.onNext(1); + o.onNext(2); + + assertTrue(o.isDisposed()); + + verify(js).innerClose(false, o); + } + + @Test + public void disposeAfterOnNext() { + PublishSubject<Integer> ps1 = PublishSubject.create(); + PublishSubject<Integer> ps2 = PublishSubject.create(); + + TestObserver<Integer> to = new TestObserver<>(); + + ps1.groupJoin(ps2, v -> Observable.never(), v -> Observable.never(), (a, b) -> a) + .doOnNext(v -> { + to.dispose(); + }) + .subscribe(to); + + ps2.onNext(1); + ps1.onNext(1); + } + + @Test + public void completeWithMoreWork() { + PublishSubject<Integer> ps1 = PublishSubject.create(); + PublishSubject<Integer> ps2 = PublishSubject.create(); + + TestObserver<Integer> to = new TestObserver<>(); + + ps1.groupJoin(ps2, v -> Observable.never(), v -> Observable.never(), (a, b) -> a) + .doOnNext(v -> { + if (v == 1) { + ps2.onNext(2); + ps1.onComplete(); + ps2.onComplete(); + } + }) + .subscribe(to); + + ps2.onNext(1); + ps1.onNext(1); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableHideTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableHideTest.java new file mode 100644 index 0000000000..0b624621ec --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableHideTest.java @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import static org.junit.Assert.assertFalse; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.subjects.PublishSubject; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class ObservableHideTest extends RxJavaTest { + @Test + public void hiding() { + PublishSubject<Integer> src = PublishSubject.create(); + + Observable<Integer> dst = src.hide(); + + assertFalse(dst instanceof PublishSubject); + + Observer<Object> o = TestHelper.mockObserver(); + + dst.subscribe(o); + + src.onNext(1); + src.onComplete(); + + verify(o).onNext(1); + verify(o).onComplete(); + verify(o, never()).onError(any(Throwable.class)); + } + + @Test + public void hidingError() { + PublishSubject<Integer> src = PublishSubject.create(); + + Observable<Integer> dst = src.hide(); + + assertFalse(dst instanceof PublishSubject); + + Observer<Object> o = TestHelper.mockObserver(); + + dst.subscribe(o); + + src.onError(new TestException()); + + verify(o, never()).onNext(any()); + verify(o, never()).onComplete(); + verify(o).onError(any(TestException.class)); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeObservable(new Function<Observable<Object>, ObservableSource<Object>>() { + @Override + public ObservableSource<Object> apply(Observable<Object> o) + throws Exception { + return o.hide(); + } + }); + } + + @Test + public void disposed() { + TestHelper.checkDisposed(PublishSubject.create().hide()); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableIgnoreElementsTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableIgnoreElementsTest.java new file mode 100644 index 0000000000..c7597c3a9f --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableIgnoreElementsTest.java @@ -0,0 +1,178 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import static org.junit.Assert.*; + +import java.util.concurrent.atomic.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.subjects.PublishSubject; +import io.reactivex.rxjava3.testsupport.*; + +public class ObservableIgnoreElementsTest extends RxJavaTest { + + @Test + public void withEmptyObservable() { + assertTrue(Observable.empty().ignoreElements().toObservable().isEmpty().blockingGet()); + } + + @Test + public void withNonEmptyObservable() { + assertTrue(Observable.just(1, 2, 3).ignoreElements().toObservable().isEmpty().blockingGet()); + } + + @Test + public void upstreamIsProcessedButIgnoredObservable() { + final int num = 10; + final AtomicInteger upstreamCount = new AtomicInteger(); + long count = Observable.range(1, num) + .doOnNext(new Consumer<Integer>() { + @Override + public void accept(Integer t) { + upstreamCount.incrementAndGet(); + } + }) + .ignoreElements() + .toObservable() + .count().blockingGet(); + assertEquals(num, upstreamCount.get()); + assertEquals(0, count); + } + + @Test + public void completedOkObservable() { + TestObserverEx<Object> to = new TestObserverEx<>(); + Observable.range(1, 10).ignoreElements().toObservable().subscribe(to); + to.assertNoErrors(); + to.assertNoValues(); + to.assertTerminated(); + } + + @Test + public void errorReceivedObservable() { + TestObserverEx<Object> to = new TestObserverEx<>(); + TestException ex = new TestException("boo"); + Observable.error(ex).ignoreElements().toObservable().subscribe(to); + to.assertNoValues(); + to.assertTerminated(); + to.assertError(TestException.class); + to.assertErrorMessage("boo"); + } + + @Test + public void unsubscribesFromUpstreamObservable() { + final AtomicBoolean unsub = new AtomicBoolean(); + Observable.range(1, 10).concatWith(Observable.<Integer>never()) + .doOnDispose(new Action() { + @Override + public void run() { + unsub.set(true); + }}) + .ignoreElements() + .toObservable() + .subscribe() + .dispose(); + assertTrue(unsub.get()); + } + + @Test + public void withEmpty() { + Observable.empty().ignoreElements().blockingAwait(); + } + + @Test + public void withNonEmpty() { + Observable.just(1, 2, 3).ignoreElements().blockingAwait(); + } + + @Test + public void upstreamIsProcessedButIgnored() { + final int num = 10; + final AtomicInteger upstreamCount = new AtomicInteger(); + Observable.range(1, num) + .doOnNext(new Consumer<Integer>() { + @Override + public void accept(Integer t) { + upstreamCount.incrementAndGet(); + } + }) + .ignoreElements() + .blockingAwait(); + assertEquals(num, upstreamCount.get()); + } + + @Test + public void completedOk() { + TestObserverEx<Object> to = new TestObserverEx<>(); + Observable.range(1, 10).ignoreElements().subscribe(to); + to.assertNoErrors(); + to.assertNoValues(); + to.assertTerminated(); + } + + @Test + public void errorReceived() { + TestObserverEx<Object> to = new TestObserverEx<>(); + TestException ex = new TestException("boo"); + Observable.error(ex).ignoreElements().subscribe(to); + to.assertNoValues(); + to.assertTerminated(); + to.assertError(TestException.class); + to.assertErrorMessage("boo"); + } + + @Test + public void unsubscribesFromUpstream() { + final AtomicBoolean unsub = new AtomicBoolean(); + Observable.range(1, 10).concatWith(Observable.<Integer>never()) + .doOnDispose(new Action() { + @Override + public void run() { + unsub.set(true); + }}) + .ignoreElements() + .subscribe() + .dispose(); + assertTrue(unsub.get()); + } + + @Test + public void dispose() { + + PublishSubject<Integer> ps = PublishSubject.create(); + + TestObserver<Integer> to = ps.ignoreElements().<Integer>toObservable().test(); + + assertTrue(ps.hasObservers()); + + to.dispose(); + + assertFalse(ps.hasObservers()); + + TestHelper.checkDisposed(ps.ignoreElements().<Integer>toObservable()); + } + + @Test + public void checkDispose() { + TestHelper.checkDisposed(Observable.just(1).ignoreElements()); + + TestHelper.checkDisposed(Observable.just(1).ignoreElements().toObservable()); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableInternalHelperTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableInternalHelperTest.java new file mode 100644 index 0000000000..87fc866f74 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableInternalHelperTest.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import static org.junit.Assert.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.RxJavaTest; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class ObservableInternalHelperTest extends RxJavaTest { + + @Test + public void utilityClass() { + TestHelper.checkUtilityClass(ObservableInternalHelper.class); + } + + @Test + public void enums() { + assertNotNull(ObservableInternalHelper.MapToInt.values()[0]); + assertNotNull(ObservableInternalHelper.MapToInt.valueOf("INSTANCE")); + + } + + @Test + public void mapToInt() throws Exception { + assertEquals(0, ObservableInternalHelper.MapToInt.INSTANCE.apply(null)); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableIntervalRangeTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableIntervalRangeTest.java new file mode 100644 index 0000000000..637d9164be --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableIntervalRangeTest.java @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import static org.junit.Assert.*; + +import java.util.concurrent.TimeUnit; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.schedulers.Schedulers; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class ObservableIntervalRangeTest extends RxJavaTest { + + @Test + public void simple() throws Exception { + Observable.intervalRange(5, 5, 50, 50, TimeUnit.MILLISECONDS) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(5L, 6L, 7L, 8L, 9L); + } + + @Test + public void customScheduler() { + Observable.intervalRange(1, 5, 1, 1, TimeUnit.MILLISECONDS, Schedulers.single()) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(1L, 2L, 3L, 4L, 5L); + } + + @Test + public void countZero() { + Observable.intervalRange(1, 0, 1, 1, TimeUnit.MILLISECONDS) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(); + } + + @Test + public void countNegative() { + try { + Observable.intervalRange(1, -1, 1, 1, TimeUnit.MILLISECONDS); + fail("Should have thrown!"); + } catch (IllegalArgumentException ex) { + assertEquals("count >= 0 required but it was -1", ex.getMessage()); + } + } + + @Test + public void longOverflow() { + Observable.intervalRange(Long.MAX_VALUE - 1, 2, 1, 1, TimeUnit.MILLISECONDS); + + Observable.intervalRange(Long.MIN_VALUE, Long.MAX_VALUE, 1, 1, TimeUnit.MILLISECONDS); + + try { + Observable.intervalRange(Long.MAX_VALUE - 1, 3, 1, 1, TimeUnit.MILLISECONDS); + fail("Should have thrown!"); + } catch (IllegalArgumentException ex) { + assertEquals("Overflow! start + count is bigger than Long.MAX_VALUE", ex.getMessage()); + } + } + + @Test + public void dispose() { + TestHelper.checkDisposed(Observable.intervalRange(1, 2, 1, 1, TimeUnit.MILLISECONDS)); + } + + @Test + public void cancel() { + Observable.intervalRange(0, 20, 1, 1, TimeUnit.MILLISECONDS, Schedulers.trampoline()) + .take(10) + .test() + .assertResult(0L, 1L, 2L, 3L, 4L, 5L, 6L, 7L, 8L, 9L); + } + + @Test + public void takeSameAsRange() { + Observable.intervalRange(0, 2, 1, 1, TimeUnit.MILLISECONDS, Schedulers.trampoline()) + .take(2) + .test() + .assertResult(0L, 1L); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableIntervalTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableIntervalTest.java new file mode 100644 index 0000000000..e8126962de --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableIntervalTest.java @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import java.util.concurrent.TimeUnit; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.internal.operators.observable.ObservableInterval.IntervalObserver; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.schedulers.*; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class ObservableIntervalTest extends RxJavaTest { + + @Test + public void dispose() { + TestHelper.checkDisposed(Observable.interval(1, TimeUnit.MILLISECONDS, new TestScheduler())); + } + + @Test + public void cancel() { + Observable.interval(1, TimeUnit.MILLISECONDS, Schedulers.trampoline()) + .take(10) + .test() + .assertResult(0L, 1L, 2L, 3L, 4L, 5L, 6L, 7L, 8L, 9L); + } + + @Test + public void cancelledOnRun() { + TestObserver<Long> to = new TestObserver<>(); + IntervalObserver is = new IntervalObserver(to); + to.onSubscribe(is); + + is.dispose(); + + is.run(); + + to.assertEmpty(); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableJoinTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableJoinTest.java new file mode 100644 index 0000000000..c6f439cdcb --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableJoinTest.java @@ -0,0 +1,470 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import java.util.List; + +import io.reactivex.rxjava3.disposables.Disposable; +import org.junit.*; +import org.mockito.MockitoAnnotations; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.subjects.PublishSubject; +import io.reactivex.rxjava3.testsupport.*; + +public class ObservableJoinTest extends RxJavaTest { + Observer<Object> observer = TestHelper.mockObserver(); + + BiFunction<Integer, Integer, Integer> add = new BiFunction<Integer, Integer, Integer>() { + @Override + public Integer apply(Integer t1, Integer t2) { + return t1 + t2; + } + }; + + <T> Function<Integer, Observable<T>> just(final Observable<T> observable) { + return new Function<Integer, Observable<T>>() { + @Override + public Observable<T> apply(Integer t1) { + return observable; + } + }; + } + + @Before + public void before() { + MockitoAnnotations.openMocks(this); + } + + @Test + public void normal1() { + PublishSubject<Integer> source1 = PublishSubject.create(); + PublishSubject<Integer> source2 = PublishSubject.create(); + + Observable<Integer> m = source1.join(source2, + just(Observable.never()), + just(Observable.never()), add); + + m.subscribe(observer); + + source1.onNext(1); + source1.onNext(2); + source1.onNext(4); + + source2.onNext(16); + source2.onNext(32); + source2.onNext(64); + + source1.onComplete(); + source2.onComplete(); + + verify(observer, times(1)).onNext(17); + verify(observer, times(1)).onNext(18); + verify(observer, times(1)).onNext(20); + verify(observer, times(1)).onNext(33); + verify(observer, times(1)).onNext(34); + verify(observer, times(1)).onNext(36); + verify(observer, times(1)).onNext(65); + verify(observer, times(1)).onNext(66); + verify(observer, times(1)).onNext(68); + + verify(observer, times(1)).onComplete(); + verify(observer, never()).onError(any(Throwable.class)); + } + + @Test + public void normal1WithDuration() { + PublishSubject<Integer> source1 = PublishSubject.create(); + PublishSubject<Integer> source2 = PublishSubject.create(); + + PublishSubject<Integer> duration1 = PublishSubject.create(); + + Observable<Integer> m = source1.join(source2, + just(duration1), + just(Observable.never()), add); + m.subscribe(observer); + + source1.onNext(1); + source1.onNext(2); + source2.onNext(16); + + duration1.onNext(1); + + source1.onNext(4); + source1.onNext(8); + + source1.onComplete(); + source2.onComplete(); + + verify(observer, times(1)).onNext(17); + verify(observer, times(1)).onNext(18); + verify(observer, times(1)).onNext(20); + verify(observer, times(1)).onNext(24); + + verify(observer, times(1)).onComplete(); + verify(observer, never()).onError(any(Throwable.class)); + + } + + @Test + public void normal2() { + PublishSubject<Integer> source1 = PublishSubject.create(); + PublishSubject<Integer> source2 = PublishSubject.create(); + + Observable<Integer> m = source1.join(source2, + just(Observable.never()), + just(Observable.never()), add); + + m.subscribe(observer); + + source1.onNext(1); + source1.onNext(2); + source1.onComplete(); + + source2.onNext(16); + source2.onNext(32); + source2.onNext(64); + + source2.onComplete(); + + verify(observer, times(1)).onNext(17); + verify(observer, times(1)).onNext(18); + verify(observer, times(1)).onNext(33); + verify(observer, times(1)).onNext(34); + verify(observer, times(1)).onNext(65); + verify(observer, times(1)).onNext(66); + + verify(observer, times(1)).onComplete(); + verify(observer, never()).onError(any(Throwable.class)); + } + + @Test + public void leftThrows() { + PublishSubject<Integer> source1 = PublishSubject.create(); + PublishSubject<Integer> source2 = PublishSubject.create(); + + Observable<Integer> m = source1.join(source2, + just(Observable.never()), + just(Observable.never()), add); + + m.subscribe(observer); + + source2.onNext(1); + source1.onError(new RuntimeException("Forced failure")); + + verify(observer, times(1)).onError(any(Throwable.class)); + verify(observer, never()).onComplete(); + verify(observer, never()).onNext(any()); + } + + @Test + public void rightThrows() { + PublishSubject<Integer> source1 = PublishSubject.create(); + PublishSubject<Integer> source2 = PublishSubject.create(); + + Observable<Integer> m = source1.join(source2, + just(Observable.never()), + just(Observable.never()), add); + + m.subscribe(observer); + + source1.onNext(1); + source2.onError(new RuntimeException("Forced failure")); + + verify(observer, times(1)).onError(any(Throwable.class)); + verify(observer, never()).onComplete(); + verify(observer, never()).onNext(any()); + } + + @Test + public void leftDurationThrows() { + PublishSubject<Integer> source1 = PublishSubject.create(); + PublishSubject<Integer> source2 = PublishSubject.create(); + + Observable<Integer> duration1 = Observable.<Integer> error(new RuntimeException("Forced failure")); + + Observable<Integer> m = source1.join(source2, + just(duration1), + just(Observable.never()), add); + m.subscribe(observer); + + source1.onNext(1); + + verify(observer, times(1)).onError(any(Throwable.class)); + verify(observer, never()).onComplete(); + verify(observer, never()).onNext(any()); + } + + @Test + public void rightDurationThrows() { + PublishSubject<Integer> source1 = PublishSubject.create(); + PublishSubject<Integer> source2 = PublishSubject.create(); + + Observable<Integer> duration1 = Observable.<Integer> error(new RuntimeException("Forced failure")); + + Observable<Integer> m = source1.join(source2, + just(Observable.never()), + just(duration1), add); + m.subscribe(observer); + + source2.onNext(1); + + verify(observer, times(1)).onError(any(Throwable.class)); + verify(observer, never()).onComplete(); + verify(observer, never()).onNext(any()); + } + + @Test + public void leftDurationSelectorThrows() { + PublishSubject<Integer> source1 = PublishSubject.create(); + PublishSubject<Integer> source2 = PublishSubject.create(); + + Function<Integer, Observable<Integer>> fail = new Function<Integer, Observable<Integer>>() { + @Override + public Observable<Integer> apply(Integer t1) { + throw new RuntimeException("Forced failure"); + } + }; + + Observable<Integer> m = source1.join(source2, + fail, + just(Observable.never()), add); + m.subscribe(observer); + + source1.onNext(1); + + verify(observer, times(1)).onError(any(Throwable.class)); + verify(observer, never()).onComplete(); + verify(observer, never()).onNext(any()); + } + + @Test + public void rightDurationSelectorThrows() { + PublishSubject<Integer> source1 = PublishSubject.create(); + PublishSubject<Integer> source2 = PublishSubject.create(); + + Function<Integer, Observable<Integer>> fail = new Function<Integer, Observable<Integer>>() { + @Override + public Observable<Integer> apply(Integer t1) { + throw new RuntimeException("Forced failure"); + } + }; + + Observable<Integer> m = source1.join(source2, + just(Observable.never()), + fail, add); + m.subscribe(observer); + + source2.onNext(1); + + verify(observer, times(1)).onError(any(Throwable.class)); + verify(observer, never()).onComplete(); + verify(observer, never()).onNext(any()); + } + + @Test + public void resultSelectorThrows() { + PublishSubject<Integer> source1 = PublishSubject.create(); + PublishSubject<Integer> source2 = PublishSubject.create(); + + BiFunction<Integer, Integer, Integer> fail = new BiFunction<Integer, Integer, Integer>() { + @Override + public Integer apply(Integer t1, Integer t2) { + throw new RuntimeException("Forced failure"); + } + }; + + Observable<Integer> m = source1.join(source2, + just(Observable.never()), + just(Observable.never()), fail); + m.subscribe(observer); + + source1.onNext(1); + source2.onNext(2); + + verify(observer, times(1)).onError(any(Throwable.class)); + verify(observer, never()).onComplete(); + verify(observer, never()).onNext(any()); + } + + @Test + public void dispose() { + TestHelper.checkDisposed(PublishSubject.<Integer>create().join(Observable.just(1), + Functions.justFunction(Observable.never()), + Functions.justFunction(Observable.never()), new BiFunction<Integer, Integer, Integer>() { + @Override + public Integer apply(Integer a, Integer b) throws Exception { + return a + b; + } + })); + } + + @Test + public void take() { + Observable.just(1).join( + Observable.just(2), + Functions.justFunction(Observable.never()), + Functions.justFunction(Observable.never()), + new BiFunction<Integer, Integer, Integer>() { + @Override + public Integer apply(Integer a, Integer b) throws Exception { + return a + b; + } + }) + .take(1) + .test() + .assertResult(3); + } + + @Test + public void rightClose() { + PublishSubject<Integer> ps = PublishSubject.create(); + + TestObserver<Integer> to = ps.join(Observable.just(2), + Functions.justFunction(Observable.never()), + Functions.justFunction(Observable.empty()), + new BiFunction<Integer, Integer, Integer>() { + @Override + public Integer apply(Integer a, Integer b) throws Exception { + return a + b; + } + }) + .test() + .assertEmpty(); + + ps.onNext(1); + + to.assertEmpty(); + } + + @Test + public void resultSelectorThrows2() { + PublishSubject<Integer> ps = PublishSubject.create(); + + TestObserver<Integer> to = ps.join( + Observable.just(2), + Functions.justFunction(Observable.never()), + Functions.justFunction(Observable.never()), + new BiFunction<Integer, Integer, Integer>() { + @Override + public Integer apply(Integer a, Integer b) throws Exception { + throw new TestException(); + } + }) + .test(); + + ps.onNext(1); + ps.onComplete(); + + to.assertFailure(TestException.class); + } + + @Test + public void badOuterSource() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + new Observable<Integer>() { + @Override + protected void subscribeActual(Observer<? super Integer> observer) { + observer.onSubscribe(Disposable.empty()); + observer.onError(new TestException("First")); + observer.onError(new TestException("Second")); + } + } + .join(Observable.just(2), + Functions.justFunction(Observable.never()), + Functions.justFunction(Observable.never()), + new BiFunction<Integer, Integer, Integer>() { + @Override + public Integer apply(Integer a, Integer b) throws Exception { + return a + b; + } + }) + .to(TestHelper.<Integer>testConsumer()) + .assertFailureAndMessage(TestException.class, "First"); + + TestHelper.assertUndeliverable(errors, 0, TestException.class, "Second"); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void badEndSource() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + @SuppressWarnings("rawtypes") + final Observer[] o = { null }; + + TestObserverEx<Integer> to = Observable.just(1) + .join(Observable.just(2), + Functions.justFunction(Observable.never()), + Functions.justFunction(new Observable<Integer>() { + @Override + protected void subscribeActual(Observer<? super Integer> observer) { + o[0] = observer; + observer.onSubscribe(Disposable.empty()); + observer.onError(new TestException("First")); + } + }), + new BiFunction<Integer, Integer, Integer>() { + @Override + public Integer apply(Integer a, Integer b) throws Exception { + return a + b; + } + }) + .to(TestHelper.<Integer>testConsumer()); + + o[0].onError(new TestException("Second")); + + to + .assertFailureAndMessage(TestException.class, "First"); + + TestHelper.assertUndeliverable(errors, 0, TestException.class, "Second"); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void bothTerminateWithWorkRemaining() { + PublishSubject<Integer> ps1 = PublishSubject.create(); + PublishSubject<Integer> ps2 = PublishSubject.create(); + + TestObserver<Integer> to = ps1.join( + ps2, + v -> Observable.never(), + v -> Observable.never(), + (a, b) -> a + b) + .doOnNext(v -> { + ps1.onComplete(); + ps2.onNext(2); + ps2.onComplete(); + }) + .test(); + + ps1.onNext(0); + ps2.onNext(1); + + to.assertComplete(); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableLastTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableLastTest.java new file mode 100644 index 0000000000..cabed665f4 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableLastTest.java @@ -0,0 +1,380 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import java.util.NoSuchElementException; + +import org.junit.Test; +import org.mockito.InOrder; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class ObservableLastTest extends RxJavaTest { + + @Test + public void lastWithElements() { + Maybe<Integer> last = Observable.just(1, 2, 3).lastElement(); + assertEquals(3, last.blockingGet().intValue()); + } + + @Test + public void lastWithNoElements() { + Maybe<?> last = Observable.empty().lastElement(); + assertNull(last.blockingGet()); + } + + @Test + public void lastMultiSubscribe() { + Maybe<Integer> last = Observable.just(1, 2, 3).lastElement(); + assertEquals(3, last.blockingGet().intValue()); + assertEquals(3, last.blockingGet().intValue()); + } + + @Test + public void lastViaObservable() { + Observable.just(1, 2, 3).lastElement(); + } + + @Test + public void last() { + Maybe<Integer> o = Observable.just(1, 2, 3).lastElement(); + + MaybeObserver<Integer> observer = TestHelper.mockMaybeObserver(); + o.subscribe(observer); + + InOrder inOrder = inOrder(observer); + inOrder.verify(observer, times(1)).onSuccess(3); +// inOrder.verify(observer, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void lastWithOneElement() { + Maybe<Integer> o = Observable.just(1).lastElement(); + + MaybeObserver<Integer> observer = TestHelper.mockMaybeObserver(); + o.subscribe(observer); + + InOrder inOrder = inOrder(observer); + inOrder.verify(observer, times(1)).onSuccess(1); +// inOrder.verify(observer, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void lastWithEmpty() { + Maybe<Integer> o = Observable.<Integer> empty().lastElement(); + + MaybeObserver<Integer> observer = TestHelper.mockMaybeObserver(); + o.subscribe(observer); + + InOrder inOrder = inOrder(observer); + inOrder.verify(observer).onComplete(); + inOrder.verify(observer, never()).onError(any(Throwable.class)); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void lastWithPredicate() { + Maybe<Integer> o = Observable.just(1, 2, 3, 4, 5, 6) + .filter(new Predicate<Integer>() { + + @Override + public boolean test(Integer t1) { + return t1 % 2 == 0; + } + }) + .lastElement(); + + MaybeObserver<Integer> observer = TestHelper.mockMaybeObserver(); + o.subscribe(observer); + + InOrder inOrder = inOrder(observer); + inOrder.verify(observer, times(1)).onSuccess(6); +// inOrder.verify(observer, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void lastWithPredicateAndOneElement() { + Maybe<Integer> o = Observable.just(1, 2) + .filter( + new Predicate<Integer>() { + + @Override + public boolean test(Integer t1) { + return t1 % 2 == 0; + } + }) + .lastElement(); + + MaybeObserver<Integer> observer = TestHelper.mockMaybeObserver(); + o.subscribe(observer); + + InOrder inOrder = inOrder(observer); + inOrder.verify(observer, times(1)).onSuccess(2); +// inOrder.verify(observer, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void lastWithPredicateAndEmpty() { + Maybe<Integer> o = Observable.just(1) + .filter( + new Predicate<Integer>() { + + @Override + public boolean test(Integer t1) { + return t1 % 2 == 0; + } + }).lastElement(); + + MaybeObserver<Integer> observer = TestHelper.mockMaybeObserver(); + o.subscribe(observer); + + InOrder inOrder = inOrder(observer); + inOrder.verify(observer).onComplete(); + inOrder.verify(observer, never()).onError(any(Throwable.class)); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void lastOrDefault() { + Single<Integer> o = Observable.just(1, 2, 3) + .last(4); + + SingleObserver<Integer> observer = TestHelper.mockSingleObserver(); + o.subscribe(observer); + + InOrder inOrder = inOrder(observer); + inOrder.verify(observer, times(1)).onSuccess(3); +// inOrder.verify(observer, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void lastOrDefaultWithOneElement() { + Single<Integer> o = Observable.just(1).last(2); + + SingleObserver<Integer> observer = TestHelper.mockSingleObserver(); + o.subscribe(observer); + + InOrder inOrder = inOrder(observer); + inOrder.verify(observer, times(1)).onSuccess(1); +// inOrder.verify(observer, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void lastOrDefaultWithEmpty() { + Single<Integer> o = Observable.<Integer> empty() + .last(1); + + SingleObserver<Integer> observer = TestHelper.mockSingleObserver(); + o.subscribe(observer); + + InOrder inOrder = inOrder(observer); + inOrder.verify(observer, times(1)).onSuccess(1); +// inOrder.verify(observer, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void lastOrDefaultWithPredicate() { + Single<Integer> o = Observable.just(1, 2, 3, 4, 5, 6) + .filter(new Predicate<Integer>() { + + @Override + public boolean test(Integer t1) { + return t1 % 2 == 0; + } + }) + .last(8); + + SingleObserver<Integer> observer = TestHelper.mockSingleObserver(); + o.subscribe(observer); + + InOrder inOrder = inOrder(observer); + inOrder.verify(observer, times(1)).onSuccess(6); +// inOrder.verify(observer, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void lastOrDefaultWithPredicateAndOneElement() { + Single<Integer> o = Observable.just(1, 2) + .filter(new Predicate<Integer>() { + + @Override + public boolean test(Integer t1) { + return t1 % 2 == 0; + } + }) + .last(4); + + SingleObserver<Integer> observer = TestHelper.mockSingleObserver(); + o.subscribe(observer); + + InOrder inOrder = inOrder(observer); + inOrder.verify(observer, times(1)).onSuccess(2); +// inOrder.verify(observer, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void lastOrDefaultWithPredicateAndEmpty() { + Single<Integer> o = Observable.just(1) + .filter( + new Predicate<Integer>() { + + @Override + public boolean test(Integer t1) { + return t1 % 2 == 0; + } + }) + .last(2); + + SingleObserver<Integer> observer = TestHelper.mockSingleObserver(); + o.subscribe(observer); + + InOrder inOrder = inOrder(observer); + inOrder.verify(observer, times(1)).onSuccess(2); +// inOrder.verify(observer, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void lastOrErrorNoElement() { + Observable.empty() + .lastOrError() + .test() + .assertNoValues() + .assertError(NoSuchElementException.class); + } + + @Test + public void lastOrErrorOneElement() { + Observable.just(1) + .lastOrError() + .test() + .assertNoErrors() + .assertValue(1); + } + + @Test + public void lastOrErrorMultipleElements() { + Observable.just(1, 2, 3) + .lastOrError() + .test() + .assertNoErrors() + .assertValue(3); + } + + @Test + public void lastOrErrorError() { + Observable.error(new RuntimeException("error")) + .lastOrError() + .to(TestHelper.testConsumer()) + .assertNoValues() + .assertErrorMessage("error") + .assertError(RuntimeException.class); + } + + @Test + public void dispose() { + TestHelper.checkDisposed(Observable.never().lastElement().toObservable()); + TestHelper.checkDisposed(Observable.never().lastElement()); + + TestHelper.checkDisposed(Observable.just(1).lastOrError().toObservable()); + TestHelper.checkDisposed(Observable.just(1).lastOrError()); + + TestHelper.checkDisposed(Observable.just(1).last(2).toObservable()); + TestHelper.checkDisposed(Observable.just(1).last(2)); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeObservableToMaybe(new Function<Observable<Object>, MaybeSource<Object>>() { + @Override + public MaybeSource<Object> apply(Observable<Object> o) throws Exception { + return o.lastElement(); + } + }); + TestHelper.checkDoubleOnSubscribeObservable(new Function<Observable<Object>, ObservableSource<Object>>() { + @Override + public ObservableSource<Object> apply(Observable<Object> o) throws Exception { + return o.lastElement().toObservable(); + } + }); + + TestHelper.checkDoubleOnSubscribeObservableToSingle(new Function<Observable<Object>, SingleSource<Object>>() { + @Override + public SingleSource<Object> apply(Observable<Object> o) throws Exception { + return o.lastOrError(); + } + }); + TestHelper.checkDoubleOnSubscribeObservable(new Function<Observable<Object>, ObservableSource<Object>>() { + @Override + public ObservableSource<Object> apply(Observable<Object> o) throws Exception { + return o.lastOrError().toObservable(); + } + }); + + TestHelper.checkDoubleOnSubscribeObservableToSingle(new Function<Observable<Object>, SingleSource<Object>>() { + @Override + public SingleSource<Object> apply(Observable<Object> o) throws Exception { + return o.last(2); + } + }); + TestHelper.checkDoubleOnSubscribeObservable(new Function<Observable<Object>, ObservableSource<Object>>() { + @Override + public ObservableSource<Object> apply(Observable<Object> o) throws Exception { + return o.last(2).toObservable(); + } + }); + } + + @Test + public void error() { + Observable.error(new TestException()) + .lastElement() + .test() + .assertFailure(TestException.class); + } + + @Test + public void errorLastOrErrorObservable() { + Observable.error(new TestException()) + .lastOrError() + .toObservable() + .test() + .assertFailure(TestException.class); + } + + @Test + public void emptyLastOrErrorObservable() { + Observable.empty() + .lastOrError() + .toObservable() + .test() + .assertFailure(NoSuchElementException.class); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableLiftTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableLiftTest.java new file mode 100644 index 0000000000..b761a6ad02 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableLiftTest.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import static org.junit.Assert.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.testsupport.SuppressUndeliverable; + +public class ObservableLiftTest extends RxJavaTest { + + @Test + @SuppressUndeliverable + public void callbackCrash() { + try { + Observable.just(1) + .lift(new ObservableOperator<Object, Integer>() { + @Override + public Observer<? super Integer> apply(Observer<? super Object> o) throws Exception { + throw new TestException(); + } + }) + .test(); + fail("Should have thrown"); + } catch (NullPointerException ex) { + assertTrue(ex.toString(), ex.getCause() instanceof TestException); + } + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableMapNotificationTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableMapNotificationTest.java new file mode 100644 index 0000000000..0672f528c9 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableMapNotificationTest.java @@ -0,0 +1,106 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import io.reactivex.rxjava3.disposables.Disposable; +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.internal.operators.observable.ObservableMapNotification.MapNotificationObserver; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.testsupport.*; + +public class ObservableMapNotificationTest extends RxJavaTest { + @Test + public void just() { + TestObserver<Object> to = new TestObserver<>(); + Observable.just(1) + .flatMap( + new Function<Integer, Observable<Object>>() { + @Override + public Observable<Object> apply(Integer item) { + return Observable.just((Object)(item + 1)); + } + }, + new Function<Throwable, Observable<Object>>() { + @Override + public Observable<Object> apply(Throwable e) { + return Observable.error(e); + } + }, + new Supplier<Observable<Object>>() { + @Override + public Observable<Object> get() { + return Observable.never(); + } + } + ).subscribe(to); + + to.assertNoErrors(); + to.assertNotComplete(); + to.assertValue(2); + } + + @Test + public void dispose() { + TestHelper.checkDisposed(new Observable<Integer>() { + @SuppressWarnings({ "rawtypes", "unchecked" }) + @Override + protected void subscribeActual(Observer<? super Integer> observer) { + MapNotificationObserver mn = new MapNotificationObserver( + observer, + Functions.justFunction(Observable.just(1)), + Functions.justFunction(Observable.just(2)), + Functions.justSupplier(Observable.just(3)) + ); + mn.onSubscribe(Disposable.empty()); + } + }); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeObservable(new Function<Observable<Object>, ObservableSource<Integer>>() { + @Override + public ObservableSource<Integer> apply(Observable<Object> o) throws Exception { + return o.flatMap( + Functions.justFunction(Observable.just(1)), + Functions.justFunction(Observable.just(2)), + Functions.justSupplier(Observable.just(3)) + ); + } + }); + } + + @Test + public void onErrorCrash() { + TestObserverEx<Integer> to = Observable.<Integer>error(new TestException("Outer")) + .flatMap(Functions.justFunction(Observable.just(1)), + new Function<Throwable, Observable<Integer>>() { + @Override + public Observable<Integer> apply(Throwable t) throws Exception { + throw new TestException("Inner"); + } + }, + Functions.justSupplier(Observable.just(3))) + .to(TestHelper.<Integer>testConsumer()) + .assertFailure(CompositeException.class); + + TestHelper.assertError(to, 0, TestException.class, "Outer"); + TestHelper.assertError(to, 1, TestException.class, "Inner"); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableMapTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableMapTest.java new file mode 100644 index 0000000000..4919a74f0d --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableMapTest.java @@ -0,0 +1,400 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import static org.junit.Assert.assertNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import java.util.*; + +import org.junit.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.core.Observer; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.operators.QueueFuseable; +import io.reactivex.rxjava3.schedulers.Schedulers; +import io.reactivex.rxjava3.subjects.UnicastSubject; +import io.reactivex.rxjava3.testsupport.*; + +public class ObservableMapTest extends RxJavaTest { + + Observer<String> stringObserver; + Observer<String> stringObserver2; + + static final BiFunction<String, Integer, String> APPEND_INDEX = new BiFunction<String, Integer, String>() { + @Override + public String apply(String value, Integer index) { + return value + index; + } + }; + + @Before + public void before() { + stringObserver = TestHelper.mockObserver(); + stringObserver2 = TestHelper.mockObserver(); + } + + @Test + public void map() { + Map<String, String> m1 = getMap("One"); + Map<String, String> m2 = getMap("Two"); + Observable<Map<String, String>> o = Observable.just(m1, m2); + + Observable<String> m = o.map(new Function<Map<String, String>, String>() { + @Override + public String apply(Map<String, String> map) { + return map.get("firstName"); + } + }); + + m.subscribe(stringObserver); + + verify(stringObserver, never()).onError(any(Throwable.class)); + verify(stringObserver, times(1)).onNext("OneFirst"); + verify(stringObserver, times(1)).onNext("TwoFirst"); + verify(stringObserver, times(1)).onComplete(); + } + + @Test + public void mapMany() { + /* simulate a top-level async call which returns IDs */ + Observable<Integer> ids = Observable.just(1, 2); + + /* now simulate the behavior to take those IDs and perform nested async calls based on them */ + Observable<String> m = ids.flatMap(new Function<Integer, Observable<String>>() { + + @Override + public Observable<String> apply(Integer id) { + /* simulate making a nested async call which creates another Observable */ + Observable<Map<String, String>> subObservable = null; + if (id == 1) { + Map<String, String> m1 = getMap("One"); + Map<String, String> m2 = getMap("Two"); + subObservable = Observable.just(m1, m2); + } else { + Map<String, String> m3 = getMap("Three"); + Map<String, String> m4 = getMap("Four"); + subObservable = Observable.just(m3, m4); + } + + /* simulate kicking off the async call and performing a select on it to transform the data */ + return subObservable.map(new Function<Map<String, String>, String>() { + @Override + public String apply(Map<String, String> map) { + return map.get("firstName"); + } + }); + } + + }); + m.subscribe(stringObserver); + + verify(stringObserver, never()).onError(any(Throwable.class)); + verify(stringObserver, times(1)).onNext("OneFirst"); + verify(stringObserver, times(1)).onNext("TwoFirst"); + verify(stringObserver, times(1)).onNext("ThreeFirst"); + verify(stringObserver, times(1)).onNext("FourFirst"); + verify(stringObserver, times(1)).onComplete(); + } + + @Test + public void mapMany2() { + Map<String, String> m1 = getMap("One"); + Map<String, String> m2 = getMap("Two"); + Observable<Map<String, String>> observable1 = Observable.just(m1, m2); + + Map<String, String> m3 = getMap("Three"); + Map<String, String> m4 = getMap("Four"); + Observable<Map<String, String>> observable2 = Observable.just(m3, m4); + + Observable<Observable<Map<String, String>>> o = Observable.just(observable1, observable2); + + Observable<String> m = o.flatMap(new Function<Observable<Map<String, String>>, Observable<String>>() { + + @Override + public Observable<String> apply(Observable<Map<String, String>> o) { + return o.map(new Function<Map<String, String>, String>() { + + @Override + public String apply(Map<String, String> map) { + return map.get("firstName"); + } + }); + } + + }); + m.subscribe(stringObserver); + + verify(stringObserver, never()).onError(any(Throwable.class)); + verify(stringObserver, times(1)).onNext("OneFirst"); + verify(stringObserver, times(1)).onNext("TwoFirst"); + verify(stringObserver, times(1)).onNext("ThreeFirst"); + verify(stringObserver, times(1)).onNext("FourFirst"); + verify(stringObserver, times(1)).onComplete(); + + } + + @Test + public void mapWithError() { + Observable<String> w = Observable.just("one", "fail", "two", "three", "fail"); + Observable<String> m = w.map(new Function<String, String>() { + @Override + public String apply(String s) { + if ("fail".equals(s)) { + throw new RuntimeException("Forced Failure"); + } + return s; + } + }).doOnError(new Consumer<Throwable>() { + + @Override + public void accept(Throwable t1) { + t1.printStackTrace(); + } + + }); + + m.subscribe(stringObserver); + verify(stringObserver, times(1)).onNext("one"); + verify(stringObserver, never()).onNext("two"); + verify(stringObserver, never()).onNext("three"); + verify(stringObserver, never()).onComplete(); + verify(stringObserver, times(1)).onError(any(Throwable.class)); + } + + @Test(expected = IllegalArgumentException.class) + public void mapWithIssue417() { + Observable.just(1).observeOn(Schedulers.computation()) + .map(new Function<Integer, Integer>() { + @Override + public Integer apply(Integer arg0) { + throw new IllegalArgumentException("any error"); + } + }).blockingSingle(); + } + + @Test(expected = IllegalArgumentException.class) + public void mapWithErrorInFuncAndThreadPoolScheduler() throws InterruptedException { + // The error will throw in one of threads in the thread pool. + // If map does not handle it, the error will disappear. + // so map needs to handle the error by itself. + Observable<String> m = Observable.just("one") + .observeOn(Schedulers.computation()) + .map(new Function<String, String>() { + @Override + public String apply(String arg0) { + throw new IllegalArgumentException("any error"); + } + }); + + // block for response, expecting exception thrown + m.blockingLast(); + } + + /** + * While mapping over range(1,0).last() we expect NoSuchElementException since the sequence is empty. + */ + @Test + public void errorPassesThruMap() { + assertNull(Observable.range(1, 0).lastElement().map(new Function<Integer, Integer>() { + + @Override + public Integer apply(Integer i) { + return i; + } + + }).blockingGet()); + } + + /** + * We expect IllegalStateException to pass thru map. + */ + @Test(expected = IllegalStateException.class) + public void errorPassesThruMap2() { + Observable.error(new IllegalStateException()).map(new Function<Object, Object>() { + + @Override + public Object apply(Object i) { + return i; + } + + }).blockingSingle(); + } + + /** + * We expect an ArithmeticException exception here because last() emits a single value + * but then we divide by 0. + */ + @Test(expected = ArithmeticException.class) + public void mapWithErrorInFunc() { + Observable.range(1, 1).lastElement().map(new Function<Integer, Integer>() { + + @Override + public Integer apply(Integer i) { + return i / 0; + } + + }).blockingGet(); + } + + // FIXME RS subscribers can't throw +// @Test(expected = OnErrorNotImplementedException.class) +// public void verifyExceptionIsThrownIfThereIsNoExceptionHandler() { +// +// ObservableSource<Object> creator = new ObservableSource<Object>() { +// +// @Override +// public void subscribeActual(Observer<? super Object> observer) { +// observer.onSubscribe(EmptyDisposable.INSTANCE); +// observer.onNext("a"); +// observer.onNext("b"); +// observer.onNext("c"); +// observer.onComplete(); +// } +// }; +// +// Function<Object, Observable<Object>> manyMapper = new Function<Object, Observable<Object>>() { +// +// @Override +// public Observable<Object> apply(Object object) { +// return Observable.just(object); +// } +// }; +// +// Function<Object, Object> mapper = new Function<Object, Object>() { +// private int count = 0; +// +// @Override +// public Object apply(Object object) { +// ++count; +// if (count > 2) { +// throw new RuntimeException(); +// } +// return object; +// } +// }; +// +// Consumer<Object> onNext = new Consumer<Object>() { +// +// @Override +// public void accept(Object object) { +// System.out.println(object.toString()); +// } +// }; +// +// try { +// Observable.unsafeCreate(creator).flatMap(manyMapper).map(mapper).subscribe(onNext); +// } catch (RuntimeException e) { +// e.printStackTrace(); +// throw e; +// } +// } + + private static Map<String, String> getMap(String prefix) { + Map<String, String> m = new HashMap<>(); + m.put("firstName", prefix + "First"); + m.put("lastName", prefix + "Last"); + return m; + } + + // FIXME RS subscribers can't throw +// @Test(expected = OnErrorNotImplementedException.class) +// public void testShouldNotSwallowOnErrorNotImplementedException() { +// Observable.just("a", "b").flatMap(new Function<String, Observable<String>>() { +// @Override +// public Observable<String> apply(String s) { +// return Observable.just(s + "1", s + "2"); +// } +// }).flatMap(new Function<String, Observable<String>>() { +// @Override +// public Observable<String> apply(String s) { +// return Observable.error(new Exception("test")); +// } +// }).forEach(new Consumer<String>() { +// @Override +// public void accept(String s) { +// System.out.println(s); +// } +// }); +// } + + @Test + public void dispose() { + TestHelper.checkDisposed(Observable.range(1, 5).map(Functions.identity())); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeObservable(new Function<Observable<Object>, ObservableSource<Object>>() { + @Override + public ObservableSource<Object> apply(Observable<Object> o) throws Exception { + return o.map(Functions.identity()); + } + }); + } + + @Test + public void fusedSync() { + TestObserverEx<Integer> to = new TestObserverEx<>(QueueFuseable.ANY); + + Observable.range(1, 5) + .map(Functions.<Integer>identity()) + .subscribe(to); + + to.assertFusionMode(QueueFuseable.SYNC) + .assertResult(1, 2, 3, 4, 5); + } + + @Test + public void fusedAsync() { + TestObserverEx<Integer> to = new TestObserverEx<>(QueueFuseable.ANY); + + UnicastSubject<Integer> us = UnicastSubject.create(); + + us + .map(Functions.<Integer>identity()) + .subscribe(to); + + TestHelper.emit(us, 1, 2, 3, 4, 5); + + to.assertFusionMode(QueueFuseable.ASYNC) + .assertResult(1, 2, 3, 4, 5); + } + + @Test + public void fusedReject() { + TestObserverEx<Integer> to = new TestObserverEx<>(QueueFuseable.ANY | QueueFuseable.BOUNDARY); + + Observable.range(1, 5) + .map(Functions.<Integer>identity()) + .subscribe(to); + + to.assertFusionMode(QueueFuseable.NONE) + .assertResult(1, 2, 3, 4, 5); + } + + @Test + public void badSource() { + TestHelper.checkBadSourceObservable(new Function<Observable<Object>, Object>() { + @Override + public Object apply(Observable<Object> o) throws Exception { + return o.map(Functions.identity()); + } + }, false, 1, 1, 1); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableMaterializeTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableMaterializeTest.java new file mode 100644 index 0000000000..02b16ad6ce --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableMaterializeTest.java @@ -0,0 +1,195 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import static org.junit.Assert.*; + +import java.util.*; +import java.util.concurrent.ExecutionException; + +import io.reactivex.rxjava3.disposables.Disposable; +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.core.Observer; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.observers.DefaultObserver; +import io.reactivex.rxjava3.testsupport.*; + +public class ObservableMaterializeTest extends RxJavaTest { + + @Test + public void materialize1() { + // null will cause onError to be triggered before "three" can be + // returned + final TestAsyncErrorObservable o1 = new TestAsyncErrorObservable("one", "two", null, + "three"); + + TestLocalObserver observer = new TestLocalObserver(); + Observable<Notification<String>> m = Observable.unsafeCreate(o1).materialize(); + m.subscribe(observer); + + try { + o1.t.join(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + + assertFalse(observer.onError); + assertTrue(observer.onComplete); + assertEquals(3, observer.notifications.size()); + + assertTrue(observer.notifications.get(0).isOnNext()); + assertEquals("one", observer.notifications.get(0).getValue()); + + assertTrue(observer.notifications.get(1).isOnNext()); + assertEquals("two", observer.notifications.get(1).getValue()); + + assertTrue(observer.notifications.get(2).isOnError()); + assertEquals(NullPointerException.class, observer.notifications.get(2).getError().getClass()); + } + + @Test + public void materialize2() { + final TestAsyncErrorObservable o1 = new TestAsyncErrorObservable("one", "two", "three"); + + TestLocalObserver observer = new TestLocalObserver(); + Observable<Notification<String>> m = Observable.unsafeCreate(o1).materialize(); + m.subscribe(observer); + + try { + o1.t.join(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + + assertFalse(observer.onError); + assertTrue(observer.onComplete); + assertEquals(4, observer.notifications.size()); + assertTrue(observer.notifications.get(0).isOnNext()); + assertEquals("one", observer.notifications.get(0).getValue()); + + assertTrue(observer.notifications.get(1).isOnNext()); + assertEquals("two", observer.notifications.get(1).getValue()); + + assertTrue(observer.notifications.get(2).isOnNext()); + assertEquals("three", observer.notifications.get(2).getValue()); + + assertTrue(observer.notifications.get(3).isOnComplete()); + } + + @Test + public void multipleSubscribes() throws InterruptedException, ExecutionException { + final TestAsyncErrorObservable o = new TestAsyncErrorObservable("one", "two", null, "three"); + + Observable<Notification<String>> m = Observable.unsafeCreate(o).materialize(); + + assertEquals(3, m.toList().toFuture().get().size()); + assertEquals(3, m.toList().toFuture().get().size()); + } + + @Test + public void withCompletionCausingError() { + TestObserverEx<Notification<Integer>> to = new TestObserverEx<>(); + final RuntimeException ex = new RuntimeException("boo"); + Observable.<Integer>empty().materialize().doOnNext(new Consumer<Object>() { + @Override + public void accept(Object t) { + throw ex; + } + }).subscribe(to); + to.assertError(ex); + to.assertNoValues(); + to.assertTerminated(); + } + + private static class TestLocalObserver extends DefaultObserver<Notification<String>> { + + boolean onComplete; + boolean onError; + List<Notification<String>> notifications = new Vector<>(); + + @Override + public void onComplete() { + this.onComplete = true; + } + + @Override + public void onError(Throwable e) { + this.onError = true; + } + + @Override + public void onNext(Notification<String> value) { + this.notifications.add(value); + } + + } + + private static class TestAsyncErrorObservable implements ObservableSource<String> { + + String[] valuesToReturn; + + TestAsyncErrorObservable(String... values) { + valuesToReturn = values; + } + + volatile Thread t; + + @Override + public void subscribe(final Observer<? super String> observer) { + observer.onSubscribe(Disposable.empty()); + t = new Thread(new Runnable() { + + @Override + public void run() { + for (String s : valuesToReturn) { + if (s == null) { + System.out.println("throwing exception"); + try { + Thread.sleep(100); + } catch (Throwable e) { + + } + observer.onError(new NullPointerException()); + return; + } else { + observer.onNext(s); + } + } + System.out.println("subscription complete"); + observer.onComplete(); + } + + }); + t.start(); + } + } + + @Test + public void dispose() { + TestHelper.checkDisposed(Observable.just(1).materialize()); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeObservable(new Function<Observable<Object>, ObservableSource<Notification<Object>>>() { + @Override + public ObservableSource<Notification<Object>> apply(Observable<Object> o) throws Exception { + return o.materialize(); + } + }); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableMergeDelayErrorTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableMergeDelayErrorTest.java new file mode 100644 index 0000000000..da92bc0338 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableMergeDelayErrorTest.java @@ -0,0 +1,601 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import java.util.*; +import java.util.concurrent.*; + +import io.reactivex.rxjava3.disposables.Disposable; +import org.junit.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.core.Observer; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.observers.DefaultObserver; +import io.reactivex.rxjava3.testsupport.*; + +public class ObservableMergeDelayErrorTest extends RxJavaTest { + + Observer<String> stringObserver; + + @Before + public void before() { + stringObserver = TestHelper.mockObserver(); + } + + @Test + public void errorDelayed1() { + final Observable<String> o1 = Observable.unsafeCreate(new TestErrorObservable("four", null, "six")); // we expect to lose "six" from the source (and it should never be sent by the source since onError was called + final Observable<String> o2 = Observable.unsafeCreate(new TestErrorObservable("one", "two", "three")); + + Observable<String> m = Observable.mergeDelayError(o1, o2); + m.subscribe(stringObserver); + + verify(stringObserver, times(1)).onError(any(NullPointerException.class)); + verify(stringObserver, never()).onComplete(); + verify(stringObserver, times(1)).onNext("one"); + verify(stringObserver, times(1)).onNext("two"); + verify(stringObserver, times(1)).onNext("three"); + verify(stringObserver, times(1)).onNext("four"); + verify(stringObserver, times(0)).onNext("five"); + // despite not expecting it ... we don't do anything to prevent it if the source Observable keeps sending after onError + // inner Observable errors are considered terminal for that source +// verify(stringObserver, times(1)).onNext("six"); + // inner Observable errors are considered terminal for that source + } + + @Test + public void errorDelayed2() { + final Observable<String> o1 = Observable.unsafeCreate(new TestErrorObservable("one", "two", "three")); + final Observable<String> o2 = Observable.unsafeCreate(new TestErrorObservable("four", null, "six")); // we expect to lose "six" from the source (and it should never be sent by the source since onError was called + final Observable<String> o3 = Observable.unsafeCreate(new TestErrorObservable("seven", "eight", null)); + final Observable<String> o4 = Observable.unsafeCreate(new TestErrorObservable("nine")); + + Observable<String> m = Observable.mergeDelayError(o1, o2, o3, o4); + m.subscribe(stringObserver); + + verify(stringObserver, times(1)).onError(any(CompositeException.class)); + verify(stringObserver, never()).onComplete(); + verify(stringObserver, times(1)).onNext("one"); + verify(stringObserver, times(1)).onNext("two"); + verify(stringObserver, times(1)).onNext("three"); + verify(stringObserver, times(1)).onNext("four"); + verify(stringObserver, times(0)).onNext("five"); + // despite not expecting it ... we don't do anything to prevent it if the source Observable keeps sending after onError + // inner Observable errors are considered terminal for that source +// verify(stringObserver, times(1)).onNext("six"); + verify(stringObserver, times(1)).onNext("seven"); + verify(stringObserver, times(1)).onNext("eight"); + verify(stringObserver, times(1)).onNext("nine"); + } + + @Test + public void errorDelayed3() { + final Observable<String> o1 = Observable.unsafeCreate(new TestErrorObservable("one", "two", "three")); + final Observable<String> o2 = Observable.unsafeCreate(new TestErrorObservable("four", "five", "six")); + final Observable<String> o3 = Observable.unsafeCreate(new TestErrorObservable("seven", "eight", null)); + final Observable<String> o4 = Observable.unsafeCreate(new TestErrorObservable("nine")); + + Observable<String> m = Observable.mergeDelayError(o1, o2, o3, o4); + m.subscribe(stringObserver); + + verify(stringObserver, times(1)).onError(any(NullPointerException.class)); + verify(stringObserver, never()).onComplete(); + verify(stringObserver, times(1)).onNext("one"); + verify(stringObserver, times(1)).onNext("two"); + verify(stringObserver, times(1)).onNext("three"); + verify(stringObserver, times(1)).onNext("four"); + verify(stringObserver, times(1)).onNext("five"); + verify(stringObserver, times(1)).onNext("six"); + verify(stringObserver, times(1)).onNext("seven"); + verify(stringObserver, times(1)).onNext("eight"); + verify(stringObserver, times(1)).onNext("nine"); + } + + @Test + public void errorDelayed4() { + final Observable<String> o1 = Observable.unsafeCreate(new TestErrorObservable("one", "two", "three")); + final Observable<String> o2 = Observable.unsafeCreate(new TestErrorObservable("four", "five", "six")); + final Observable<String> o3 = Observable.unsafeCreate(new TestErrorObservable("seven", "eight")); + final Observable<String> o4 = Observable.unsafeCreate(new TestErrorObservable("nine", null)); + + Observable<String> m = Observable.mergeDelayError(o1, o2, o3, o4); + m.subscribe(stringObserver); + + verify(stringObserver, times(1)).onError(any(NullPointerException.class)); + verify(stringObserver, never()).onComplete(); + verify(stringObserver, times(1)).onNext("one"); + verify(stringObserver, times(1)).onNext("two"); + verify(stringObserver, times(1)).onNext("three"); + verify(stringObserver, times(1)).onNext("four"); + verify(stringObserver, times(1)).onNext("five"); + verify(stringObserver, times(1)).onNext("six"); + verify(stringObserver, times(1)).onNext("seven"); + verify(stringObserver, times(1)).onNext("eight"); + verify(stringObserver, times(1)).onNext("nine"); + } + + @Test + public void errorDelayed4WithThreading() { + final TestAsyncErrorObservable o1 = new TestAsyncErrorObservable("one", "two", "three"); + final TestAsyncErrorObservable o2 = new TestAsyncErrorObservable("four", "five", "six"); + final TestAsyncErrorObservable o3 = new TestAsyncErrorObservable("seven", "eight"); + // throw the error at the very end so no onComplete will be called after it + final TestAsyncErrorObservable o4 = new TestAsyncErrorObservable("nine", null); + + Observable<String> m = Observable.mergeDelayError(Observable.unsafeCreate(o1), Observable.unsafeCreate(o2), Observable.unsafeCreate(o3), Observable.unsafeCreate(o4)); + m.subscribe(stringObserver); + + try { + o1.t.join(); + o2.t.join(); + o3.t.join(); + o4.t.join(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + + verify(stringObserver, times(1)).onNext("one"); + verify(stringObserver, times(1)).onNext("two"); + verify(stringObserver, times(1)).onNext("three"); + verify(stringObserver, times(1)).onNext("four"); + verify(stringObserver, times(1)).onNext("five"); + verify(stringObserver, times(1)).onNext("six"); + verify(stringObserver, times(1)).onNext("seven"); + verify(stringObserver, times(1)).onNext("eight"); + verify(stringObserver, times(1)).onNext("nine"); + verify(stringObserver, times(1)).onError(any(NullPointerException.class)); + verify(stringObserver, never()).onComplete(); + } + + @Test + public void compositeErrorDelayed1() { + final Observable<String> o1 = Observable.unsafeCreate(new TestErrorObservable("four", null, "six")); // we expect to lose "six" from the source (and it should never be sent by the source since onError was called + final Observable<String> o2 = Observable.unsafeCreate(new TestErrorObservable("one", "two", null)); + + Observable<String> m = Observable.mergeDelayError(o1, o2); + m.subscribe(stringObserver); + + verify(stringObserver, times(1)).onError(any(Throwable.class)); + verify(stringObserver, never()).onComplete(); + verify(stringObserver, times(1)).onNext("one"); + verify(stringObserver, times(1)).onNext("two"); + verify(stringObserver, times(0)).onNext("three"); + verify(stringObserver, times(1)).onNext("four"); + verify(stringObserver, times(0)).onNext("five"); + // despite not expecting it ... we don't do anything to prevent it if the source Observable keeps sending after onError + // inner Observable errors are considered terminal for that source +// verify(stringObserver, times(1)).onNext("six"); + } + + @Test + public void compositeErrorDelayed2() { + final Observable<String> o1 = Observable.unsafeCreate(new TestErrorObservable("four", null, "six")); // we expect to lose "six" from the source (and it should never be sent by the source since onError was called + final Observable<String> o2 = Observable.unsafeCreate(new TestErrorObservable("one", "two", null)); + + Observable<String> m = Observable.mergeDelayError(o1, o2); + CaptureObserver w = new CaptureObserver(); + m.subscribe(w); + + assertNotNull(w.e); + + assertEquals(2, ((CompositeException)w.e).size()); + +// if (w.e instanceof CompositeException) { +// assertEquals(2, ((CompositeException) w.e).getExceptions().size()); +// w.e.printStackTrace(); +// } else { +// fail("Expecting CompositeException"); +// } + + } + + /** + * The unit tests below are from OperationMerge and should ensure the normal merge functionality is correct. + */ + + @Test + public void mergeObservableOfObservables() { + final Observable<String> o1 = Observable.unsafeCreate(new TestSynchronousObservable()); + final Observable<String> o2 = Observable.unsafeCreate(new TestSynchronousObservable()); + + Observable<Observable<String>> observableOfObservables = Observable.unsafeCreate(new ObservableSource<Observable<String>>() { + + @Override + public void subscribe(Observer<? super Observable<String>> observer) { + observer.onSubscribe(Disposable.empty()); + // simulate what would happen in an Observable + observer.onNext(o1); + observer.onNext(o2); + observer.onComplete(); + } + + }); + Observable<String> m = Observable.mergeDelayError(observableOfObservables); + m.subscribe(stringObserver); + + verify(stringObserver, never()).onError(any(Throwable.class)); + verify(stringObserver, times(1)).onComplete(); + verify(stringObserver, times(2)).onNext("hello"); + } + + @Test + public void mergeArray() { + final Observable<String> o1 = Observable.unsafeCreate(new TestSynchronousObservable()); + final Observable<String> o2 = Observable.unsafeCreate(new TestSynchronousObservable()); + + Observable<String> m = Observable.mergeDelayError(o1, o2); + m.subscribe(stringObserver); + + verify(stringObserver, never()).onError(any(Throwable.class)); + verify(stringObserver, times(2)).onNext("hello"); + verify(stringObserver, times(1)).onComplete(); + } + + @Test + public void mergeList() { + final Observable<String> o1 = Observable.unsafeCreate(new TestSynchronousObservable()); + final Observable<String> o2 = Observable.unsafeCreate(new TestSynchronousObservable()); + List<Observable<String>> listOfObservables = new ArrayList<>(); + listOfObservables.add(o1); + listOfObservables.add(o2); + + Observable<String> m = Observable.mergeDelayError(Observable.fromIterable(listOfObservables)); + m.subscribe(stringObserver); + + verify(stringObserver, never()).onError(any(Throwable.class)); + verify(stringObserver, times(1)).onComplete(); + verify(stringObserver, times(2)).onNext("hello"); + } + + @Test + public void mergeArrayWithThreading() { + final TestASynchronousObservable o1 = new TestASynchronousObservable(); + final TestASynchronousObservable o2 = new TestASynchronousObservable(); + + Observable<String> m = Observable.mergeDelayError(Observable.unsafeCreate(o1), Observable.unsafeCreate(o2)); + m.subscribe(stringObserver); + + try { + o1.t.join(); + o2.t.join(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + + verify(stringObserver, never()).onError(any(Throwable.class)); + verify(stringObserver, times(2)).onNext("hello"); + verify(stringObserver, times(1)).onComplete(); + } + + @Test + public void synchronousError() { + final Observable<Observable<String>> o1 = Observable.error(new RuntimeException("unit test")); + + final CountDownLatch latch = new CountDownLatch(1); + Observable.mergeDelayError(o1).subscribe(new DefaultObserver<String>() { + @Override + public void onComplete() { + fail("Expected onError path"); + } + + @Override + public void onError(Throwable e) { + latch.countDown(); + } + + @Override + public void onNext(String s) { + fail("Expected onError path"); + } + }); + + try { + latch.await(); + } catch (InterruptedException ex) { + fail("interrupted"); + } + } + + private static class TestSynchronousObservable implements ObservableSource<String> { + + @Override + public void subscribe(Observer<? super String> observer) { + observer.onSubscribe(Disposable.empty()); + observer.onNext("hello"); + observer.onComplete(); + } + } + + private static class TestASynchronousObservable implements ObservableSource<String> { + Thread t; + + @Override + public void subscribe(final Observer<? super String> observer) { + observer.onSubscribe(Disposable.empty()); + t = new Thread(new Runnable() { + + @Override + public void run() { + observer.onNext("hello"); + observer.onComplete(); + } + + }); + t.start(); + } + } + + private static class TestErrorObservable implements ObservableSource<String> { + + String[] valuesToReturn; + + TestErrorObservable(String... values) { + valuesToReturn = values; + } + + @Override + public void subscribe(Observer<? super String> observer) { + observer.onSubscribe(Disposable.empty()); + boolean errorThrown = false; + for (String s : valuesToReturn) { + if (s == null) { + System.out.println("throwing exception"); + observer.onError(new NullPointerException()); + errorThrown = true; + // purposefully not returning here so it will continue calling onNext + // so that we also test that we handle bad sequences like this + } else { + observer.onNext(s); + } + } + if (!errorThrown) { + observer.onComplete(); + } + } + } + + private static class TestAsyncErrorObservable implements ObservableSource<String> { + + String[] valuesToReturn; + + TestAsyncErrorObservable(String... values) { + valuesToReturn = values; + } + + Thread t; + + @Override + public void subscribe(final Observer<? super String> observer) { + observer.onSubscribe(Disposable.empty()); + t = new Thread(new Runnable() { + + @Override + public void run() { + for (String s : valuesToReturn) { + if (s == null) { + System.out.println("throwing exception"); + try { + Thread.sleep(100); + } catch (Throwable e) { + + } + observer.onError(new NullPointerException()); + return; + } else { + observer.onNext(s); + } + } + System.out.println("subscription complete"); + observer.onComplete(); + } + + }); + t.start(); + } + } + + private static class CaptureObserver extends DefaultObserver<String> { + volatile Throwable e; + + @Override + public void onComplete() { + + } + + @Override + public void onError(Throwable e) { + this.e = e; + } + + @Override + public void onNext(String args) { + + } + + } + + @Test + public void errorInParentObservable() { + TestObserverEx<Integer> to = new TestObserverEx<>(); + Observable.mergeDelayError( + Observable.just(Observable.just(1), Observable.just(2)) + .startWithItem(Observable.<Integer> error(new RuntimeException())) + ).subscribe(to); + to.awaitDone(5, TimeUnit.SECONDS); + to.assertTerminated(); + to.assertValues(1, 2); + assertEquals(1, to.errors().size()); + + } + + @Test + public void errorInParentObservableDelayed() throws Exception { + for (int i = 0; i < 50; i++) { + final TestASynchronous1sDelayedObservable o1 = new TestASynchronous1sDelayedObservable(); + final TestASynchronous1sDelayedObservable o2 = new TestASynchronous1sDelayedObservable(); + Observable<Observable<String>> parentObservable = Observable.unsafeCreate(new ObservableSource<Observable<String>>() { + @Override + public void subscribe(Observer<? super Observable<String>> op) { + op.onSubscribe(Disposable.empty()); + op.onNext(Observable.unsafeCreate(o1)); + op.onNext(Observable.unsafeCreate(o2)); + op.onError(new NullPointerException("throwing exception in parent")); + } + }); + + Observer<String> stringObserver = TestHelper.mockObserver(); + + TestObserverEx<String> to = new TestObserverEx<>(stringObserver); + Observable<String> m = Observable.mergeDelayError(parentObservable); + m.subscribe(to); + System.out.println("testErrorInParentObservableDelayed | " + i); + to.awaitDone(2000, TimeUnit.MILLISECONDS); + to.assertTerminated(); + + verify(stringObserver, times(2)).onNext("hello"); + verify(stringObserver, times(1)).onError(any(NullPointerException.class)); + verify(stringObserver, never()).onComplete(); + } + } + + private static class TestASynchronous1sDelayedObservable implements ObservableSource<String> { + Thread t; + + @Override + public void subscribe(final Observer<? super String> observer) { + observer.onSubscribe(Disposable.empty()); + t = new Thread(new Runnable() { + + @Override + public void run() { + try { + Thread.sleep(100); + } catch (InterruptedException e) { + observer.onError(e); + } + observer.onNext("hello"); + observer.onComplete(); + } + + }); + t.start(); + } + } + + @Test + public void mergeIterableDelayError() { + Observable.mergeDelayError(Arrays.asList(Observable.just(1), Observable.just(2))) + .test() + .assertResult(1, 2); + } + + @Test + public void mergeArrayDelayError() { + Observable.mergeArrayDelayError(Observable.just(1), Observable.just(2)) + .test() + .assertResult(1, 2); + } + + @Test + public void mergeIterableDelayErrorWithError() { + Observable.mergeDelayError( + Arrays.asList(Observable.just(1).concatWith(Observable.<Integer>error(new TestException())), + Observable.just(2))) + .test() + .assertFailure(TestException.class, 1, 2); + } + + @Test + public void mergeDelayError() { + Observable.mergeDelayError( + Observable.just(Observable.just(1), + Observable.just(2))) + .test() + .assertResult(1, 2); + } + + @Test + public void mergeDelayErrorWithError() { + Observable.mergeDelayError( + Observable.just(Observable.just(1).concatWith(Observable.<Integer>error(new TestException())), + Observable.just(2))) + .test() + .assertFailure(TestException.class, 1, 2); + } + + @Test + public void mergeDelayErrorMaxConcurrency() { + Observable.mergeDelayError( + Observable.just(Observable.just(1), + Observable.just(2)), 1) + .test() + .assertResult(1, 2); + } + + @Test + public void mergeDelayErrorWithErrorMaxConcurrency() { + Observable.mergeDelayError( + Observable.just(Observable.just(1).concatWith(Observable.<Integer>error(new TestException())), + Observable.just(2)), 1) + .test() + .assertFailure(TestException.class, 1, 2); + } + + @Test + public void mergeIterableDelayErrorMaxConcurrency() { + Observable.mergeDelayError( + Arrays.asList(Observable.just(1), + Observable.just(2)), 1) + .test() + .assertResult(1, 2); + } + + @Test + public void mergeIterableDelayErrorWithErrorMaxConcurrency() { + Observable.mergeDelayError( + Arrays.asList(Observable.just(1).concatWith(Observable.<Integer>error(new TestException())), + Observable.just(2)), 1) + .test() + .assertFailure(TestException.class, 1, 2); + } + + @Test + public void mergeDelayError3() { + Observable.mergeDelayError( + Observable.just(1), + Observable.just(2), + Observable.just(3) + ) + .test() + .assertResult(1, 2, 3); + } + + @Test + public void mergeDelayError3WithError() { + Observable.mergeDelayError( + Observable.just(1), + Observable.just(2).concatWith(Observable.<Integer>error(new TestException())), + Observable.just(3) + ) + .test() + .assertFailure(TestException.class, 1, 2, 3); + } + +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableMergeMaxConcurrentTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableMergeMaxConcurrentTest.java new file mode 100644 index 0000000000..e1388f39ad --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableMergeMaxConcurrentTest.java @@ -0,0 +1,278 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import static org.junit.Assert.*; + +import java.util.*; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import io.reactivex.rxjava3.disposables.Disposable; +import org.junit.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.core.Observer; +import io.reactivex.rxjava3.internal.schedulers.IoScheduler; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.schedulers.Schedulers; +import io.reactivex.rxjava3.testsupport.*; + +public class ObservableMergeMaxConcurrentTest extends RxJavaTest { + + Observer<String> stringObserver; + + @Before + public void before() { + stringObserver = TestHelper.mockObserver(); + } + + @Test + public void whenMaxConcurrentIsOne() { + for (int i = 0; i < 100; i++) { + List<Observable<String>> os = new ArrayList<>(); + os.add(Observable.just("one", "two", "three", "four", "five").subscribeOn(Schedulers.newThread())); + os.add(Observable.just("one", "two", "three", "four", "five").subscribeOn(Schedulers.newThread())); + os.add(Observable.just("one", "two", "three", "four", "five").subscribeOn(Schedulers.newThread())); + + List<String> expected = Arrays.asList("one", "two", "three", "four", "five", "one", "two", "three", "four", "five", "one", "two", "three", "four", "five"); + Iterator<String> iter = Observable.merge(os, 1).blockingIterable().iterator(); + List<String> actual = new ArrayList<>(); + while (iter.hasNext()) { + actual.add(iter.next()); + } + assertEquals(expected, actual); + } + } + + @Test + public void maxConcurrent() { + for (int times = 0; times < 100; times++) { + int observableCount = 100; + // Test maxConcurrent from 2 to 12 + int maxConcurrent = 2 + (times % 10); + AtomicInteger subscriptionCount = new AtomicInteger(0); + + List<Observable<String>> os = new ArrayList<>(); + List<SubscriptionCheckObservable> scos = new ArrayList<>(); + for (int i = 0; i < observableCount; i++) { + SubscriptionCheckObservable sco = new SubscriptionCheckObservable(subscriptionCount, maxConcurrent); + scos.add(sco); + os.add(Observable.unsafeCreate(sco)); + } + + Iterator<String> iter = Observable.merge(os, maxConcurrent).blockingIterable().iterator(); + List<String> actual = new ArrayList<>(); + while (iter.hasNext()) { + actual.add(iter.next()); + } + // System.out.println("actual: " + actual); + assertEquals(5 * observableCount, actual.size()); + for (SubscriptionCheckObservable sco : scos) { + assertFalse(sco.failed); + } + } + } + + private static class SubscriptionCheckObservable implements ObservableSource<String> { + + private final AtomicInteger subscriptionCount; + private final int maxConcurrent; + volatile boolean failed; + + SubscriptionCheckObservable(AtomicInteger subscriptionCount, int maxConcurrent) { + this.subscriptionCount = subscriptionCount; + this.maxConcurrent = maxConcurrent; + } + + @Override + public void subscribe(final Observer<? super String> t1) { + t1.onSubscribe(Disposable.empty()); + new Thread(new Runnable() { + + @Override + public void run() { + if (subscriptionCount.incrementAndGet() > maxConcurrent) { + failed = true; + } + t1.onNext("one"); + t1.onNext("two"); + t1.onNext("three"); + t1.onNext("four"); + t1.onNext("five"); + // We could not decrement subscriptionCount in the unsubscribe method + // as "unsubscribe" is not guaranteed to be called before the next "subscribe". + subscriptionCount.decrementAndGet(); + t1.onComplete(); + } + + }).start(); + } + + } + + @Test + public void mergeALotOfSourcesOneByOneSynchronously() { + int n = 10000; + List<Observable<Integer>> sourceList = new ArrayList<>(n); + for (int i = 0; i < n; i++) { + sourceList.add(Observable.just(i)); + } + Iterator<Integer> it = Observable.merge(Observable.fromIterable(sourceList), 1).blockingIterable().iterator(); + int j = 0; + while (it.hasNext()) { + assertEquals((Integer)j, it.next()); + j++; + } + assertEquals(j, n); + } + + @Test + public void mergeALotOfSourcesOneByOneSynchronouslyTakeHalf() { + int n = 10000; + List<Observable<Integer>> sourceList = new ArrayList<>(n); + for (int i = 0; i < n; i++) { + sourceList.add(Observable.just(i)); + } + Iterator<Integer> it = Observable.merge(Observable.fromIterable(sourceList), 1).take(n / 2).blockingIterable().iterator(); + int j = 0; + while (it.hasNext()) { + assertEquals((Integer)j, it.next()); + j++; + } + assertEquals(j, n / 2); + } + + @Test + public void simple() { + for (int i = 1; i < 100; i++) { + TestObserverEx<Integer> to = new TestObserverEx<>(); + List<Observable<Integer>> sourceList = new ArrayList<>(i); + List<Integer> result = new ArrayList<>(i); + for (int j = 1; j <= i; j++) { + sourceList.add(Observable.just(j)); + result.add(j); + } + + Observable.merge(sourceList, i).subscribe(to); + + to.assertNoErrors(); + to.assertTerminated(); + to.assertValueSequence(result); + } + } + + @Test + public void simpleOneLess() { + for (int i = 2; i < 100; i++) { + TestObserverEx<Integer> to = new TestObserverEx<>(); + List<Observable<Integer>> sourceList = new ArrayList<>(i); + List<Integer> result = new ArrayList<>(i); + for (int j = 1; j <= i; j++) { + sourceList.add(Observable.just(j)); + result.add(j); + } + + Observable.merge(sourceList, i - 1).subscribe(to); + + to.assertNoErrors(); + to.assertTerminated(); + to.assertValueSequence(result); + } + } + + @Test + public void simpleAsyncLoop() { + IoScheduler ios = (IoScheduler)Schedulers.io(); + int c = ios.size(); + for (int i = 0; i < 200; i++) { + simpleAsync(); + int c1 = ios.size(); + if (c + 60 < c1) { + throw new AssertionError("Worker leak: " + c + " - " + c1); + } + } + } + + @Test + public void simpleAsync() { + for (int i = 1; i < 50; i++) { + TestObserver<Integer> to = new TestObserver<>(); + List<Observable<Integer>> sourceList = new ArrayList<>(i); + Set<Integer> expected = new HashSet<>(i); + for (int j = 1; j <= i; j++) { + sourceList.add(Observable.just(j).subscribeOn(Schedulers.io())); + expected.add(j); + } + + Observable.merge(sourceList, i).subscribe(to); + + to.awaitDone(1, TimeUnit.SECONDS); + to.assertNoErrors(); + Set<Integer> actual = new HashSet<>(to.values()); + + assertEquals(expected, actual); + } + } + + @Test + public void simpleOneLessAsyncLoop() { + for (int i = 0; i < 200; i++) { + simpleOneLessAsync(); + } + } + + @Test + public void simpleOneLessAsync() { + long t = System.currentTimeMillis(); + for (int i = 2; i < 50; i++) { + if (System.currentTimeMillis() - t > TimeUnit.SECONDS.toMillis(9)) { + break; + } + TestObserver<Integer> to = new TestObserver<>(); + List<Observable<Integer>> sourceList = new ArrayList<>(i); + Set<Integer> expected = new HashSet<>(i); + for (int j = 1; j <= i; j++) { + sourceList.add(Observable.just(j).subscribeOn(Schedulers.io())); + expected.add(j); + } + + Observable.merge(sourceList, i - 1).subscribe(to); + + to.awaitDone(1, TimeUnit.SECONDS); + to.assertNoErrors(); + Set<Integer> actual = new HashSet<>(to.values()); + + assertEquals(expected, actual); + } + } + + @Test + public void take() throws Exception { + List<Observable<Integer>> sourceList = new ArrayList<>(3); + + sourceList.add(Observable.range(0, 100000).subscribeOn(Schedulers.io())); + sourceList.add(Observable.range(0, 100000).subscribeOn(Schedulers.io())); + sourceList.add(Observable.range(0, 100000).subscribeOn(Schedulers.io())); + + TestObserver<Integer> to = new TestObserver<>(); + + Observable.merge(sourceList, 2).take(5).subscribe(to); + + to.awaitDone(5, TimeUnit.SECONDS); + to.assertNoErrors(); + to.assertValueCount(5); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableMergeTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableMergeTest.java new file mode 100644 index 0000000000..02ddf09fd0 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableMergeTest.java @@ -0,0 +1,1091 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import java.util.*; +import java.util.concurrent.*; +import java.util.concurrent.atomic.*; + +import org.junit.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.core.Observer; +import io.reactivex.rxjava3.core.Scheduler.Worker; +import io.reactivex.rxjava3.disposables.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.observers.*; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.schedulers.*; +import io.reactivex.rxjava3.testsupport.*; + +public class ObservableMergeTest extends RxJavaTest { + + Observer<String> stringObserver; + + int count; + + @Before + public void before() { + stringObserver = TestHelper.mockObserver(); + + for (Thread t : Thread.getAllStackTraces().keySet()) { + if (t.getName().startsWith("RxNewThread")) { + count++; + } + } + } + + @After + public void after() { + try { + Thread.sleep(100); + } catch (InterruptedException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + for (Thread t : Thread.getAllStackTraces().keySet()) { + if (t.getName().startsWith("RxNewThread")) { + --count; + } + } + if (count != 0) { + throw new IllegalStateException("NewThread leak!"); + } + } + + @Test + public void mergeObservableOfObservables() { + final Observable<String> o1 = Observable.unsafeCreate(new TestSynchronousObservable()); + final Observable<String> o2 = Observable.unsafeCreate(new TestSynchronousObservable()); + + Observable<Observable<String>> observableOfObservables = Observable.unsafeCreate(new ObservableSource<Observable<String>>() { + + @Override + public void subscribe(Observer<? super Observable<String>> observer) { + observer.onSubscribe(Disposable.empty()); + // simulate what would happen in an Observable + observer.onNext(o1); + observer.onNext(o2); + observer.onComplete(); + } + + }); + Observable<String> m = Observable.merge(observableOfObservables); + m.subscribe(stringObserver); + + verify(stringObserver, never()).onError(any(Throwable.class)); + verify(stringObserver, times(1)).onComplete(); + verify(stringObserver, times(2)).onNext("hello"); + } + + @Test + public void mergeArray() { + final Observable<String> o1 = Observable.unsafeCreate(new TestSynchronousObservable()); + final Observable<String> o2 = Observable.unsafeCreate(new TestSynchronousObservable()); + + Observable<String> m = Observable.merge(o1, o2); + m.subscribe(stringObserver); + + verify(stringObserver, never()).onError(any(Throwable.class)); + verify(stringObserver, times(2)).onNext("hello"); + verify(stringObserver, times(1)).onComplete(); + } + + @Test + public void mergeList() { + final Observable<String> o1 = Observable.unsafeCreate(new TestSynchronousObservable()); + final Observable<String> o2 = Observable.unsafeCreate(new TestSynchronousObservable()); + List<Observable<String>> listOfObservables = new ArrayList<>(); + listOfObservables.add(o1); + listOfObservables.add(o2); + + Observable<String> m = Observable.merge(listOfObservables); + m.subscribe(stringObserver); + + verify(stringObserver, never()).onError(any(Throwable.class)); + verify(stringObserver, times(1)).onComplete(); + verify(stringObserver, times(2)).onNext("hello"); + } + + @Test + public void unSubscribeObservableOfObservables() throws InterruptedException { + + final AtomicBoolean unsubscribed = new AtomicBoolean(); + final CountDownLatch latch = new CountDownLatch(1); + + Observable<Observable<Long>> source = Observable.unsafeCreate(new ObservableSource<Observable<Long>>() { + + @Override + public void subscribe(final Observer<? super Observable<Long>> observer) { + // verbose on purpose so I can track the inside of it + final Disposable upstream = Disposable.fromRunnable(new Runnable() { + @Override + public void run() { + System.out.println("*** unsubscribed"); + unsubscribed.set(true); + } + }); + observer.onSubscribe(upstream); + + new Thread(new Runnable() { + + @Override + public void run() { + + while (!unsubscribed.get()) { + observer.onNext(Observable.just(1L, 2L)); + } + System.out.println("Done looping after unsubscribe: " + unsubscribed.get()); + observer.onComplete(); + + // mark that the thread is finished + latch.countDown(); + } + }).start(); + } + + }); + + final AtomicInteger count = new AtomicInteger(); + Observable.merge(source).take(6).blockingForEach(new Consumer<Long>() { + + @Override + public void accept(Long v) { + System.out.println("Value: " + v); + int c = count.incrementAndGet(); + if (c > 6) { + fail("Should be only 6"); + } + + } + }); + + latch.await(1000, TimeUnit.MILLISECONDS); + + System.out.println("unsubscribed: " + unsubscribed.get()); + + assertTrue(unsubscribed.get()); + + } + + @Test + public void mergeArrayWithThreading() { + final TestASynchronousObservable o1 = new TestASynchronousObservable(); + final TestASynchronousObservable o2 = new TestASynchronousObservable(); + + Observable<String> m = Observable.merge(Observable.unsafeCreate(o1), Observable.unsafeCreate(o2)); + TestObserver<String> to = new TestObserver<>(stringObserver); + m.subscribe(to); + + to.awaitDone(5, TimeUnit.SECONDS); + to.assertNoErrors(); + + verify(stringObserver, never()).onError(any(Throwable.class)); + verify(stringObserver, times(2)).onNext("hello"); + verify(stringObserver, times(1)).onComplete(); + } + + @Test + public void synchronizationOfMultipleSequencesLoop() throws Throwable { + for (int i = 0; i < 100; i++) { + System.out.println("testSynchronizationOfMultipleSequencesLoop > " + i); + synchronizationOfMultipleSequences(); + } + } + + @Test + public void synchronizationOfMultipleSequences() throws Throwable { + final TestASynchronousObservable o1 = new TestASynchronousObservable(); + final TestASynchronousObservable o2 = new TestASynchronousObservable(); + + // use this latch to cause onNext to wait until we're ready to let it go + final CountDownLatch endLatch = new CountDownLatch(1); + + final AtomicInteger concurrentCounter = new AtomicInteger(); + final AtomicInteger totalCounter = new AtomicInteger(); + + Observable<String> m = Observable.merge(Observable.unsafeCreate(o1), Observable.unsafeCreate(o2)); + m.subscribe(new DefaultObserver<String>() { + + @Override + public void onComplete() { + + } + + @Override + public void onError(Throwable e) { + throw new RuntimeException("failed", e); + } + + @Override + public void onNext(String v) { + totalCounter.incrementAndGet(); + concurrentCounter.incrementAndGet(); + try { + // avoid deadlocking the main thread + if (Thread.currentThread().getName().equals("TestASynchronousObservable")) { + // wait here until we're done asserting + endLatch.await(); + } + } catch (InterruptedException e) { + e.printStackTrace(); + throw new RuntimeException("failed", e); + } finally { + concurrentCounter.decrementAndGet(); + } + } + + }); + + // wait for both observables to send (one should be blocked) + o1.onNextBeingSent.await(); + o2.onNextBeingSent.await(); + + // I can't think of a way to know for sure that both threads have or are trying to send onNext + // since I can't use a CountDownLatch for "after" onNext since I want to catch during it + // but I can't know for sure onNext is invoked + // so I'm unfortunately reverting to using a Thread.sleep to allow the process scheduler time + // to make sure after o1.onNextBeingSent and o2.onNextBeingSent are hit that the following + // onNext is invoked. + + int timeout = 20; + + while (timeout-- > 0 && concurrentCounter.get() != 1) { + Thread.sleep(100); + } + + try { // in try/finally so threads are released via latch countDown even if assertion fails + assertEquals(1, concurrentCounter.get()); + } finally { + // release so it can finish + endLatch.countDown(); + } + + try { + o1.t.join(); + o2.t.join(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + + assertEquals(2, totalCounter.get()); + assertEquals(0, concurrentCounter.get()); + } + + /** + * Unit test from OperationMergeDelayError backported here to show how these use cases work with normal merge. + */ + @Test + public void error1() { + // we are using synchronous execution to test this exactly rather than non-deterministic concurrent behavior + final Observable<String> o1 = Observable.unsafeCreate(new TestErrorObservable("four", null, "six")); // we expect to lose "six" + final Observable<String> o2 = Observable.unsafeCreate(new TestErrorObservable("one", "two", "three")); // we expect to lose all of these since o1 is done first and fails + + Observable<String> m = Observable.merge(o1, o2); + m.subscribe(stringObserver); + + verify(stringObserver, times(1)).onError(any(NullPointerException.class)); + verify(stringObserver, never()).onComplete(); + verify(stringObserver, times(0)).onNext("one"); + verify(stringObserver, times(0)).onNext("two"); + verify(stringObserver, times(0)).onNext("three"); + verify(stringObserver, times(1)).onNext("four"); + verify(stringObserver, times(0)).onNext("five"); + verify(stringObserver, times(0)).onNext("six"); + } + + /** + * Unit test from OperationMergeDelayError backported here to show how these use cases work with normal merge. + */ + @Test + public void error2() { + // we are using synchronous execution to test this exactly rather than non-deterministic concurrent behavior + final Observable<String> o1 = Observable.unsafeCreate(new TestErrorObservable("one", "two", "three")); + final Observable<String> o2 = Observable.unsafeCreate(new TestErrorObservable("four", null, "six")); // we expect to lose "six" + final Observable<String> o3 = Observable.unsafeCreate(new TestErrorObservable("seven", "eight", null)); // we expect to lose all of these since o2 is done first and fails + final Observable<String> o4 = Observable.unsafeCreate(new TestErrorObservable("nine")); // we expect to lose all of these since o2 is done first and fails + + Observable<String> m = Observable.merge(o1, o2, o3, o4); + m.subscribe(stringObserver); + + verify(stringObserver, times(1)).onError(any(NullPointerException.class)); + verify(stringObserver, never()).onComplete(); + verify(stringObserver, times(1)).onNext("one"); + verify(stringObserver, times(1)).onNext("two"); + verify(stringObserver, times(1)).onNext("three"); + verify(stringObserver, times(1)).onNext("four"); + verify(stringObserver, times(0)).onNext("five"); + verify(stringObserver, times(0)).onNext("six"); + verify(stringObserver, times(0)).onNext("seven"); + verify(stringObserver, times(0)).onNext("eight"); + verify(stringObserver, times(0)).onNext("nine"); + } + + private static class TestSynchronousObservable implements ObservableSource<String> { + + @Override + public void subscribe(Observer<? super String> observer) { + observer.onSubscribe(Disposable.empty()); + observer.onNext("hello"); + observer.onComplete(); + } + } + + private static class TestASynchronousObservable implements ObservableSource<String> { + Thread t; + final CountDownLatch onNextBeingSent = new CountDownLatch(1); + + @Override + public void subscribe(final Observer<? super String> observer) { + observer.onSubscribe(Disposable.empty()); + t = new Thread(new Runnable() { + + @Override + public void run() { + onNextBeingSent.countDown(); + try { + observer.onNext("hello"); + // I can't use a countDownLatch to prove we are actually sending 'onNext' + // since it will block if synchronized and I'll deadlock + observer.onComplete(); + } catch (Exception e) { + observer.onError(e); + } + } + + }, "TestASynchronousObservable"); + t.start(); + } + } + + private static class TestErrorObservable implements ObservableSource<String> { + + String[] valuesToReturn; + + TestErrorObservable(String... values) { + valuesToReturn = values; + } + + @Override + public void subscribe(Observer<? super String> observer) { + observer.onSubscribe(Disposable.empty()); + for (String s : valuesToReturn) { + if (s == null) { + System.out.println("throwing exception"); + observer.onError(new NullPointerException()); + } else { + observer.onNext(s); + } + } + observer.onComplete(); + } + } + + @Test + public void unsubscribeAsObservablesComplete() { + TestScheduler scheduler1 = new TestScheduler(); + AtomicBoolean os1 = new AtomicBoolean(false); + Observable<Long> o1 = createObservableOf5IntervalsOf1SecondIncrementsWithSubscriptionHook(scheduler1, os1); + + TestScheduler scheduler2 = new TestScheduler(); + AtomicBoolean os2 = new AtomicBoolean(false); + Observable<Long> o2 = createObservableOf5IntervalsOf1SecondIncrementsWithSubscriptionHook(scheduler2, os2); + + TestObserverEx<Long> to = new TestObserverEx<>(); + Observable.merge(o1, o2).subscribe(to); + + // we haven't incremented time so nothing should be received yet + to.assertNoValues(); + + scheduler1.advanceTimeBy(3, TimeUnit.SECONDS); + scheduler2.advanceTimeBy(2, TimeUnit.SECONDS); + + to.assertValues(0L, 1L, 2L, 0L, 1L); + // not unsubscribed yet + assertFalse(os1.get()); + assertFalse(os2.get()); + + // advance to the end at which point it should complete + scheduler1.advanceTimeBy(3, TimeUnit.SECONDS); + + to.assertValues(0L, 1L, 2L, 0L, 1L, 3L, 4L); + assertTrue(os1.get()); + assertFalse(os2.get()); + + // both should be completed now + scheduler2.advanceTimeBy(3, TimeUnit.SECONDS); + + to.assertValues(0L, 1L, 2L, 0L, 1L, 3L, 4L, 2L, 3L, 4L); + assertTrue(os1.get()); + assertTrue(os2.get()); + + to.assertTerminated(); + } + + @Test + public void earlyUnsubscribe() { + for (int i = 0; i < 10; i++) { + TestScheduler scheduler1 = new TestScheduler(); + AtomicBoolean os1 = new AtomicBoolean(false); + Observable<Long> o1 = createObservableOf5IntervalsOf1SecondIncrementsWithSubscriptionHook(scheduler1, os1); + + TestScheduler scheduler2 = new TestScheduler(); + AtomicBoolean os2 = new AtomicBoolean(false); + Observable<Long> o2 = createObservableOf5IntervalsOf1SecondIncrementsWithSubscriptionHook(scheduler2, os2); + + TestObserver<Long> to = new TestObserver<>(); + Observable.merge(o1, o2).subscribe(to); + + // we haven't incremented time so nothing should be received yet + to.assertNoValues(); + + scheduler1.advanceTimeBy(3, TimeUnit.SECONDS); + scheduler2.advanceTimeBy(2, TimeUnit.SECONDS); + + to.assertValues(0L, 1L, 2L, 0L, 1L); + // not unsubscribed yet + assertFalse(os1.get()); + assertFalse(os2.get()); + + // early unsubscribe + to.dispose(); + + assertTrue(os1.get()); + assertTrue(os2.get()); + + to.assertValues(0L, 1L, 2L, 0L, 1L); + // FIXME not happening anymore +// ts.assertUnsubscribed(); + } + } + + private Observable<Long> createObservableOf5IntervalsOf1SecondIncrementsWithSubscriptionHook(final Scheduler scheduler, final AtomicBoolean unsubscribed) { + return Observable.unsafeCreate(new ObservableSource<Long>() { + + @Override + public void subscribe(final Observer<? super Long> child) { + Observable.interval(1, TimeUnit.SECONDS, scheduler) + .take(5) + .subscribe(new Observer<Long>() { + @Override + public void onSubscribe(final Disposable d) { + child.onSubscribe(Disposable.fromRunnable(new Runnable() { + @Override + public void run() { + unsubscribed.set(true); + d.dispose(); + } + })); + } + + @Override + public void onNext(Long t) { + child.onNext(t); + } + + @Override + public void onError(Throwable t) { + unsubscribed.set(true); + child.onError(t); + } + + @Override + public void onComplete() { + unsubscribed.set(true); + child.onComplete(); + } + + }); + } + }); + } + + @Test + public void concurrency() { + Observable<Integer> o = Observable.range(1, 10000).subscribeOn(Schedulers.newThread()); + + for (int i = 0; i < 10; i++) { + Observable<Integer> merge = Observable.merge(o, o, o); + TestObserverEx<Integer> to = new TestObserverEx<>(); + merge.subscribe(to); + + to.awaitDone(3, TimeUnit.SECONDS); + to.assertTerminated(); + to.assertNoErrors(); + to.assertComplete(); + List<Integer> onNextEvents = to.values(); + assertEquals(30000, onNextEvents.size()); + // System.out.println("onNext: " + onNextEvents.size() + " onComplete: " + ts.getOnCompletedEvents().size()); + } + } + + @Test + public void concurrencyWithSleeping() { + + Observable<Integer> o = Observable.unsafeCreate(new ObservableSource<Integer>() { + + @Override + public void subscribe(final Observer<? super Integer> observer) { + Worker inner = Schedulers.newThread().createWorker(); + final CompositeDisposable as = new CompositeDisposable(); + as.add(Disposable.empty()); + as.add(inner); + + observer.onSubscribe(as); + + inner.schedule(new Runnable() { + + @Override + public void run() { + try { + for (int i = 0; i < 100; i++) { + observer.onNext(1); + try { + Thread.sleep(1); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + } catch (Exception e) { + observer.onError(e); + } + as.dispose(); + observer.onComplete(); + } + + }); + } + }); + + for (int i = 0; i < 10; i++) { + Observable<Integer> merge = Observable.merge(o, o, o); + TestObserver<Integer> to = new TestObserver<>(); + merge.subscribe(to); + + to.awaitDone(5, TimeUnit.SECONDS); + to.assertComplete(); + List<Integer> onNextEvents = to.values(); + assertEquals(300, onNextEvents.size()); + // System.out.println("onNext: " + onNextEvents.size() + " onComplete: " + ts.getOnCompletedEvents().size()); + } + } + + @Test + public void concurrencyWithBrokenOnCompleteContract() { + Observable<Integer> o = Observable.unsafeCreate(new ObservableSource<Integer>() { + + @Override + public void subscribe(final Observer<? super Integer> observer) { + Worker inner = Schedulers.newThread().createWorker(); + final CompositeDisposable as = new CompositeDisposable(); + as.add(Disposable.empty()); + as.add(inner); + + observer.onSubscribe(as); + + inner.schedule(new Runnable() { + + @Override + public void run() { + try { + for (int i = 0; i < 10000; i++) { + observer.onNext(i); + } + } catch (Exception e) { + observer.onError(e); + } + as.dispose(); + observer.onComplete(); + observer.onComplete(); + observer.onComplete(); + } + + }); + } + }); + + for (int i = 0; i < 10; i++) { + Observable<Integer> merge = Observable.merge(o, o, o); + TestObserver<Integer> to = new TestObserver<>(); + merge.subscribe(to); + + to.awaitDone(5, TimeUnit.SECONDS); + to.assertNoErrors(); + to.assertComplete(); + List<Integer> onNextEvents = to.values(); + assertEquals(30000, onNextEvents.size()); + // System.out.println("onNext: " + onNextEvents.size() + " onComplete: " + ts.getOnCompletedEvents().size()); + } + } + + @Test + public void backpressureUpstream() throws InterruptedException { + final AtomicInteger generated1 = new AtomicInteger(); + Observable<Integer> o1 = createInfiniteObservable(generated1).subscribeOn(Schedulers.computation()); + final AtomicInteger generated2 = new AtomicInteger(); + Observable<Integer> o2 = createInfiniteObservable(generated2).subscribeOn(Schedulers.computation()); + + TestObserverEx<Integer> testObserver = new TestObserverEx<Integer>() { + @Override + public void onNext(Integer t) { + System.err.println("TestObserver received => " + t + " on thread " + Thread.currentThread()); + super.onNext(t); + } + }; + + Observable.merge(o1.take(Flowable.bufferSize() * 2), o2.take(Flowable.bufferSize() * 2)).subscribe(testObserver); + testObserver.awaitDone(5, TimeUnit.SECONDS); + if (testObserver.errors().size() > 0) { + testObserver.errors().get(0).printStackTrace(); + } + testObserver.assertNoErrors(); + System.err.println(testObserver.values()); + assertEquals(Flowable.bufferSize() * 4, testObserver.values().size()); + // it should be between the take num and requested batch size across the async boundary + System.out.println("Generated 1: " + generated1.get()); + System.out.println("Generated 2: " + generated2.get()); + assertTrue(generated1.get() >= Flowable.bufferSize() * 2 + && generated1.get() <= Flowable.bufferSize() * 4); + } + + @Test + public void backpressureUpstream2InLoop() throws InterruptedException { + for (int i = 0; i < 1000; i++) { + System.err.flush(); + System.out.println("---"); + System.out.flush(); + backpressureUpstream2(); + } + } + + @Test + public void backpressureUpstream2() throws InterruptedException { + final AtomicInteger generated1 = new AtomicInteger(); + Observable<Integer> o1 = createInfiniteObservable(generated1).subscribeOn(Schedulers.computation()); + + TestObserverEx<Integer> testObserver = new TestObserverEx<Integer>() { + @Override + public void onNext(Integer t) { + super.onNext(t); + } + }; + + Observable.merge(o1.take(Flowable.bufferSize() * 2), Observable.just(-99)).subscribe(testObserver); + testObserver.awaitDone(5, TimeUnit.SECONDS); + + List<Integer> onNextEvents = testObserver.values(); + + System.out.println("Generated 1: " + generated1.get() + " / received: " + onNextEvents.size()); + System.out.println(onNextEvents); + + if (testObserver.errors().size() > 0) { + testObserver.errors().get(0).printStackTrace(); + } + testObserver.assertNoErrors(); + assertEquals(Flowable.bufferSize() * 2 + 1, onNextEvents.size()); + // it should be between the take num and requested batch size across the async boundary + assertTrue(generated1.get() >= Flowable.bufferSize() * 2 && generated1.get() <= Flowable.bufferSize() * 3); + } + + /** + * This is the same as the upstreams ones, but now adds the downstream as well by using observeOn. + * + * This requires merge to also obey the Product.request values coming from it's child Observer. + * @throws InterruptedException if the test is interrupted + */ + @Test + public void backpressureDownstreamWithConcurrentStreams() throws InterruptedException { + final AtomicInteger generated1 = new AtomicInteger(); + Observable<Integer> o1 = createInfiniteObservable(generated1).subscribeOn(Schedulers.computation()); + final AtomicInteger generated2 = new AtomicInteger(); + Observable<Integer> o2 = createInfiniteObservable(generated2).subscribeOn(Schedulers.computation()); + + TestObserverEx<Integer> to = new TestObserverEx<Integer>() { + @Override + public void onNext(Integer t) { + if (t < 100) { + try { + // force a slow consumer + Thread.sleep(1); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + // System.err.println("TestObserver received => " + t + " on thread " + Thread.currentThread()); + super.onNext(t); + } + }; + + Observable.merge(o1.take(Flowable.bufferSize() * 2), o2.take(Flowable.bufferSize() * 2)).observeOn(Schedulers.computation()).subscribe(to); + to.awaitDone(5, TimeUnit.SECONDS); + if (to.errors().size() > 0) { + to.errors().get(0).printStackTrace(); + } + to.assertNoErrors(); + System.err.println(to.values()); + assertEquals(Flowable.bufferSize() * 4, to.values().size()); + // it should be between the take num and requested batch size across the async boundary + System.out.println("Generated 1: " + generated1.get()); + System.out.println("Generated 2: " + generated2.get()); + assertTrue(generated1.get() >= Flowable.bufferSize() * 2 && generated1.get() <= Flowable.bufferSize() * 4); + } + + /** + * Currently there is no solution to this ... we can't exert backpressure on the outer Observable if we + * can't know if the ones we've received so far are going to emit or not, otherwise we could starve the system. + * + * For example, 10,000 Observables are being merged (bad use case to begin with, but ...) and it's only one of them + * that will ever emit. If backpressure only allowed the first 1,000 to be sent, we would hang and never receive an event. + * + * Thus, we must allow all Observables to be sent. The ScalarSynchronousObservable use case is an exception to this since + * we can grab the value synchronously. + * + * @throws InterruptedException if the await is interrupted + */ + @Test + public void backpressureBothUpstreamAndDownstreamWithRegularObservables() throws InterruptedException { + final AtomicInteger generated1 = new AtomicInteger(); + Observable<Observable<Integer>> o1 = createInfiniteObservable(generated1).map(new Function<Integer, Observable<Integer>>() { + + @Override + public Observable<Integer> apply(Integer t1) { + return Observable.just(1, 2, 3); + } + + }); + + TestObserverEx<Integer> to = new TestObserverEx<Integer>() { + int i; + + @Override + public void onNext(Integer t) { + if (i++ < 400) { + try { + // force a slow consumer + Thread.sleep(1); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + // System.err.println("TestObserver received => " + t + " on thread " + Thread.currentThread()); + super.onNext(t); + } + }; + + Observable.merge(o1).observeOn(Schedulers.computation()).take(Flowable.bufferSize() * 2).subscribe(to); + to.awaitDone(5, TimeUnit.SECONDS); + if (to.errors().size() > 0) { + to.errors().get(0).printStackTrace(); + } + to.assertNoErrors(); + System.out.println("Generated 1: " + generated1.get()); + System.err.println(to.values()); + System.out.println("done1 testBackpressureBothUpstreamAndDownstreamWithRegularObservables "); + assertEquals(Flowable.bufferSize() * 2, to.values().size()); + System.out.println("done2 testBackpressureBothUpstreamAndDownstreamWithRegularObservables "); + // we can't restrict this ... see comment above + // assertTrue(generated1.get() >= Observable.bufferSize() && generated1.get() <= Observable.bufferSize() * 4); + } + + @Test + public void merge1AsyncStreamOf1() { + TestObserver<Integer> to = new TestObserver<>(); + mergeNAsyncStreamsOfN(1, 1).subscribe(to); + to.awaitDone(5, TimeUnit.SECONDS); + to.assertNoErrors(); + assertEquals(1, to.values().size()); + } + + @Test + public void merge1AsyncStreamOf1000() { + TestObserver<Integer> to = new TestObserver<>(); + mergeNAsyncStreamsOfN(1, 1000).subscribe(to); + to.awaitDone(5, TimeUnit.SECONDS); + to.assertNoErrors(); + assertEquals(1000, to.values().size()); + } + + @Test + public void merge10AsyncStreamOf1000() { + TestObserver<Integer> to = new TestObserver<>(); + mergeNAsyncStreamsOfN(10, 1000).subscribe(to); + to.awaitDone(5, TimeUnit.SECONDS); + to.assertNoErrors(); + assertEquals(10000, to.values().size()); + } + + @Test + public void merge1000AsyncStreamOf1000() { + TestObserver<Integer> to = new TestObserver<>(); + mergeNAsyncStreamsOfN(1000, 1000).subscribe(to); + to.awaitDone(5, TimeUnit.SECONDS); + to.assertNoErrors(); + assertEquals(1000000, to.values().size()); + } + + @Test + public void merge2000AsyncStreamOf100() { + TestObserver<Integer> to = new TestObserver<>(); + mergeNAsyncStreamsOfN(2000, 100).subscribe(to); + to.awaitDone(5, TimeUnit.SECONDS); + to.assertNoErrors(); + assertEquals(200000, to.values().size()); + } + + @Test + public void merge100AsyncStreamOf1() { + TestObserver<Integer> to = new TestObserver<>(); + mergeNAsyncStreamsOfN(100, 1).subscribe(to); + to.awaitDone(5, TimeUnit.SECONDS); + to.assertNoErrors(); + assertEquals(100, to.values().size()); + } + + private Observable<Integer> mergeNAsyncStreamsOfN(final int outerSize, final int innerSize) { + Observable<Observable<Integer>> os = Observable.range(1, outerSize) + .map(new Function<Integer, Observable<Integer>>() { + + @Override + public Observable<Integer> apply(Integer i) { + return Observable.range(1, innerSize).subscribeOn(Schedulers.computation()); + } + + }); + return Observable.merge(os); + } + + @Test + public void merge1SyncStreamOf1() { + TestObserver<Integer> to = new TestObserver<>(); + mergeNSyncStreamsOfN(1, 1).subscribe(to); + to.awaitDone(5, TimeUnit.SECONDS); + to.assertNoErrors(); + assertEquals(1, to.values().size()); + } + + @Test + public void merge1SyncStreamOf1000000() { + TestObserver<Integer> to = new TestObserver<>(); + mergeNSyncStreamsOfN(1, 1000000).subscribe(to); + to.awaitDone(5, TimeUnit.SECONDS); + to.assertNoErrors(); + assertEquals(1000000, to.values().size()); + } + + @Test + public void merge1000SyncStreamOf1000() { + TestObserver<Integer> to = new TestObserver<>(); + mergeNSyncStreamsOfN(1000, 1000).subscribe(to); + to.awaitDone(5, TimeUnit.SECONDS); + to.assertNoErrors(); + assertEquals(1000000, to.values().size()); + } + + @Test + public void merge10000SyncStreamOf10() { + TestObserver<Integer> to = new TestObserver<>(); + mergeNSyncStreamsOfN(10000, 10).subscribe(to); + to.awaitDone(5, TimeUnit.SECONDS); + to.assertNoErrors(); + assertEquals(100000, to.values().size()); + } + + @Test + public void merge1000000SyncStreamOf1() { + TestObserver<Integer> to = new TestObserver<>(); + mergeNSyncStreamsOfN(1000000, 1).subscribe(to); + to.awaitDone(5, TimeUnit.SECONDS); + to.assertNoErrors(); + assertEquals(1000000, to.values().size()); + } + + private Observable<Integer> mergeNSyncStreamsOfN(final int outerSize, final int innerSize) { + Observable<Observable<Integer>> os = Observable.range(1, outerSize) + .map(new Function<Integer, Observable<Integer>>() { + + @Override + public Observable<Integer> apply(Integer i) { + return Observable.range(1, innerSize); + } + + }); + return Observable.merge(os); + } + + private Observable<Integer> createInfiniteObservable(final AtomicInteger generated) { + Observable<Integer> o = Observable.fromIterable(new Iterable<Integer>() { + @Override + public Iterator<Integer> iterator() { + return new Iterator<Integer>() { + + @Override + public void remove() { + } + + @Override + public Integer next() { + return generated.getAndIncrement(); + } + + @Override + public boolean hasNext() { + return true; + } + }; + } + }); + return o; + } + + @Test + public void mergeManyAsyncSingle() { + TestObserver<Integer> to = new TestObserver<>(); + Observable<Observable<Integer>> os = Observable.range(1, 10000) + .map(new Function<Integer, Observable<Integer>>() { + + @Override + public Observable<Integer> apply(final Integer i) { + return Observable.unsafeCreate(new ObservableSource<Integer>() { + + @Override + public void subscribe(Observer<? super Integer> observer) { + observer.onSubscribe(Disposable.empty()); + if (i < 500) { + try { + Thread.sleep(1); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + observer.onNext(i); + observer.onComplete(); + } + + }).subscribeOn(Schedulers.computation()).cache(); + } + + }); + Observable.merge(os).subscribe(to); + to.awaitDone(5, TimeUnit.SECONDS); + to.assertNoErrors(); + assertEquals(10000, to.values().size()); + } + + Function<Integer, Observable<Integer>> toScalar = new Function<Integer, Observable<Integer>>() { + @Override + public Observable<Integer> apply(Integer v) { + return Observable.just(v); + } + }; + + Function<Integer, Observable<Integer>> toHiddenScalar = new Function<Integer, Observable<Integer>>() { + @Override + public Observable<Integer> apply(Integer t) { + return Observable.just(t).hide(); + } + }; + ; + + void runMerge(Function<Integer, Observable<Integer>> func, TestObserverEx<Integer> to) { + List<Integer> list = new ArrayList<>(); + for (int i = 0; i < 1000; i++) { + list.add(i); + } + Observable<Integer> source = Observable.fromIterable(list); + source.flatMap(func).subscribe(to); + + if (to.values().size() != 1000) { + System.out.println(to.values()); + } + + to.assertTerminated(); + to.assertNoErrors(); + to.assertValueSequence(list); + } + + @Test + public void fastMergeFullScalar() { + runMerge(toScalar, new TestObserverEx<>()); + } + + @Test + public void fastMergeHiddenScalar() { + runMerge(toHiddenScalar, new TestObserverEx<>()); + } + + @Test + public void slowMergeFullScalar() { + for (final int req : new int[] { 16, 32, 64, 128, 256 }) { + TestObserverEx<Integer> to = new TestObserverEx<Integer>() { + int remaining = req; + + @Override + public void onNext(Integer t) { + super.onNext(t); + if (--remaining == 0) { + remaining = req; + } + } + }; + runMerge(toScalar, to); + } + } + + @Test + public void slowMergeHiddenScalar() { + for (final int req : new int[] { 16, 32, 64, 128, 256 }) { + TestObserverEx<Integer> to = new TestObserverEx<Integer>() { + int remaining = req; + @Override + public void onNext(Integer t) { + super.onNext(t); + if (--remaining == 0) { + remaining = req; + } + } + }; + runMerge(toHiddenScalar, to); + } + } + + @Test + public void mergeArray2() { + Observable.mergeArray(Observable.just(1), Observable.just(2)) + .test() + .assertResult(1, 2); + } + + @Test + public void mergeErrors() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + Observable<Integer> source1 = Observable.error(new TestException("First")); + Observable<Integer> source2 = Observable.error(new TestException("Second")); + + Observable.merge(source1, source2) + .to(TestHelper.<Integer>testConsumer()) + .assertFailureAndMessage(TestException.class, "First"); + + assertTrue(errors.toString(), errors.isEmpty()); + } finally { + RxJavaPlugins.reset(); + } + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableMergeWithCompletableTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableMergeWithCompletableTest.java new file mode 100644 index 0000000000..4b9ee5a9d2 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableMergeWithCompletableTest.java @@ -0,0 +1,185 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import static org.junit.Assert.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.Action; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.subjects.*; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class ObservableMergeWithCompletableTest extends RxJavaTest { + + @Test + public void normal() { + final TestObserver<Integer> to = new TestObserver<>(); + + Observable.range(1, 5).mergeWith( + Completable.fromAction(new Action() { + @Override + public void run() throws Exception { + to.onNext(100); + } + }) + ) + .subscribe(to); + + to.assertResult(1, 2, 3, 4, 5, 100); + } + + @Test + public void take() { + final TestObserver<Integer> to = new TestObserver<>(); + + Observable.range(1, 5).mergeWith( + Completable.complete() + ) + .take(3) + .subscribe(to); + + to.assertResult(1, 2, 3); + } + + @Test + public void cancel() { + final PublishSubject<Integer> ps = PublishSubject.create(); + final CompletableSubject cs = CompletableSubject.create(); + + TestObserver<Integer> to = ps.mergeWith(cs).test(); + + assertTrue(ps.hasObservers()); + assertTrue(cs.hasObservers()); + + to.dispose(); + + assertFalse(ps.hasObservers()); + assertFalse(cs.hasObservers()); + } + + @Test + public void mainError() { + Observable.error(new TestException()) + .mergeWith(Completable.complete()) + .test() + .assertFailure(TestException.class); + } + + @Test + public void otherError() { + Observable.never() + .mergeWith(Completable.error(new TestException())) + .test() + .assertFailure(TestException.class); + } + + @Test + public void completeRace() { + for (int i = 0; i < 1000; i++) { + final PublishSubject<Integer> ps = PublishSubject.create(); + final CompletableSubject cs = CompletableSubject.create(); + + TestObserver<Integer> to = ps.mergeWith(cs).test(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + ps.onNext(1); + ps.onComplete(); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + cs.onComplete(); + } + }; + + TestHelper.race(r1, r2); + + to.assertResult(1); + } + } + + @Test + public void isDisposed() { + new Observable<Integer>() { + @Override + protected void subscribeActual(Observer<? super Integer> observer) { + observer.onSubscribe(Disposable.empty()); + + assertFalse(((Disposable)observer).isDisposed()); + + observer.onNext(1); + + assertTrue(((Disposable)observer).isDisposed()); + } + }.mergeWith(Completable.complete()) + .take(1) + .test() + .assertResult(1); + } + + @Test + public void cancelOtherOnMainError() { + PublishSubject<Integer> ps = PublishSubject.create(); + CompletableSubject cs = CompletableSubject.create(); + + TestObserver<Integer> to = ps.mergeWith(cs).test(); + + assertTrue(ps.hasObservers()); + assertTrue(cs.hasObservers()); + + ps.onError(new TestException()); + + to.assertFailure(TestException.class); + + assertFalse("main has observers!", ps.hasObservers()); + assertFalse("other has observers", cs.hasObservers()); + } + + @Test + public void cancelMainOnOtherError() { + PublishSubject<Integer> ps = PublishSubject.create(); + CompletableSubject cs = CompletableSubject.create(); + + TestObserver<Integer> to = ps.mergeWith(cs).test(); + + assertTrue(ps.hasObservers()); + assertTrue(cs.hasObservers()); + + cs.onError(new TestException()); + + to.assertFailure(TestException.class); + + assertFalse("main has observers!", ps.hasObservers()); + assertFalse("other has observers", cs.hasObservers()); + } + + @Test + public void undeliverableUponCancel() { + TestHelper.checkUndeliverableUponCancel(new ObservableConverter<Integer, Observable<Integer>>() { + @Override + public Observable<Integer> apply(Observable<Integer> upstream) { + return upstream.mergeWith(Completable.complete().hide()); + } + }); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableMergeWithMaybeTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableMergeWithMaybeTest.java new file mode 100644 index 0000000000..3140c5c118 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableMergeWithMaybeTest.java @@ -0,0 +1,321 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import static org.junit.Assert.*; + +import java.io.IOException; +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.subjects.*; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class ObservableMergeWithMaybeTest extends RxJavaTest { + + @Test + public void normal() { + Observable.range(1, 5) + .mergeWith(Maybe.just(100)) + .test() + .assertResult(1, 2, 3, 4, 5, 100); + } + + @Test + public void emptyOther() { + Observable.range(1, 5) + .mergeWith(Maybe.<Integer>empty()) + .test() + .assertResult(1, 2, 3, 4, 5); + } + + @Test + public void normalLong() { + Observable.range(1, 512) + .mergeWith(Maybe.just(100)) + .test() + .assertValueCount(513) + .assertComplete(); + } + + @Test + public void take() { + Observable.range(1, 5) + .mergeWith(Maybe.just(100)) + .take(3) + .test() + .assertResult(1, 2, 3); + } + + @Test + public void cancel() { + final PublishSubject<Integer> ps = PublishSubject.create(); + final MaybeSubject<Integer> cs = MaybeSubject.create(); + + TestObserver<Integer> to = ps.mergeWith(cs).test(); + + assertTrue(ps.hasObservers()); + assertTrue(cs.hasObservers()); + + to.dispose(); + + assertFalse(ps.hasObservers()); + assertFalse(cs.hasObservers()); + } + + @Test + public void mainError() { + Observable.error(new TestException()) + .mergeWith(Maybe.just(100)) + .test() + .assertFailure(TestException.class); + } + + @Test + public void otherError() { + Observable.never() + .mergeWith(Maybe.error(new TestException())) + .test() + .assertFailure(TestException.class); + } + + @Test + public void completeRace() { + for (int i = 0; i < 10000; i++) { + final PublishSubject<Integer> ps = PublishSubject.create(); + final MaybeSubject<Integer> cs = MaybeSubject.create(); + + TestObserver<Integer> to = ps.mergeWith(cs).test(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + ps.onNext(1); + ps.onComplete(); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + cs.onSuccess(1); + } + }; + + TestHelper.race(r1, r2); + + to.assertResult(1, 1); + } + } + + @Test + public void onNextSlowPath() { + final PublishSubject<Integer> ps = PublishSubject.create(); + final MaybeSubject<Integer> cs = MaybeSubject.create(); + + TestObserver<Integer> to = ps.mergeWith(cs).subscribeWith(new TestObserver<Integer>() { + @Override + public void onNext(Integer t) { + super.onNext(t); + if (t == 1) { + ps.onNext(2); + } + } + }); + + ps.onNext(1); + cs.onSuccess(3); + + ps.onNext(4); + ps.onComplete(); + + to.assertResult(1, 2, 3, 4); + } + + @Test + public void onSuccessSlowPath() { + final PublishSubject<Integer> ps = PublishSubject.create(); + final MaybeSubject<Integer> cs = MaybeSubject.create(); + + TestObserver<Integer> to = ps.mergeWith(cs).subscribeWith(new TestObserver<Integer>() { + @Override + public void onNext(Integer t) { + super.onNext(t); + if (t == 1) { + cs.onSuccess(2); + } + } + }); + + ps.onNext(1); + + ps.onNext(3); + ps.onComplete(); + + to.assertResult(1, 2, 3); + } + + @Test + public void onErrorMainOverflow() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + final AtomicReference<Observer<?>> observerRef = new AtomicReference<>(); + TestObserver<Integer> to = new Observable<Integer>() { + @Override + protected void subscribeActual(Observer<? super Integer> observer) { + observer.onSubscribe(Disposable.empty()); + observerRef.set(observer); + } + } + .mergeWith(Maybe.<Integer>error(new IOException())) + .test(); + + observerRef.get().onError(new TestException()); + + to.assertFailure(IOException.class) + ; + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void onErrorOtherOverflow() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + Observable.error(new IOException()) + .mergeWith(Maybe.error(new TestException())) + .test() + .assertFailure(IOException.class) + ; + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void doubleOnSubscribeMain() { + TestHelper.checkDoubleOnSubscribeObservable( + new Function<Observable<Object>, Observable<Object>>() { + @Override + public Observable<Object> apply(Observable<Object> f) + throws Exception { + return f.mergeWith(Maybe.just(1)); + } + } + ); + } + + @Test + public void isDisposed() { + new Observable<Integer>() { + @Override + protected void subscribeActual(Observer<? super Integer> observer) { + observer.onSubscribe(Disposable.empty()); + + assertFalse(((Disposable)observer).isDisposed()); + + observer.onNext(1); + + assertTrue(((Disposable)observer).isDisposed()); + } + }.mergeWith(Maybe.<Integer>empty()) + .take(1) + .test() + .assertResult(1); + } + + @Test + public void onNextSlowPathCreateQueue() { + final PublishSubject<Integer> ps = PublishSubject.create(); + final MaybeSubject<Integer> cs = MaybeSubject.create(); + + TestObserver<Integer> to = ps.mergeWith(cs).subscribeWith(new TestObserver<Integer>() { + @Override + public void onNext(Integer t) { + super.onNext(t); + if (t == 1) { + ps.onNext(2); + ps.onNext(3); + } + } + }); + + cs.onSuccess(0); + ps.onNext(1); + + ps.onNext(4); + ps.onComplete(); + + to.assertResult(0, 1, 2, 3, 4); + } + + @Test + public void cancelOtherOnMainError() { + PublishSubject<Integer> ps = PublishSubject.create(); + MaybeSubject<Integer> ms = MaybeSubject.create(); + + TestObserver<Integer> to = ps.mergeWith(ms).test(); + + assertTrue(ps.hasObservers()); + assertTrue(ms.hasObservers()); + + ps.onError(new TestException()); + + to.assertFailure(TestException.class); + + assertFalse("main has observers!", ps.hasObservers()); + assertFalse("other has observers", ms.hasObservers()); + } + + @Test + public void cancelMainOnOtherError() { + PublishSubject<Integer> ps = PublishSubject.create(); + MaybeSubject<Integer> ms = MaybeSubject.create(); + + TestObserver<Integer> to = ps.mergeWith(ms).test(); + + assertTrue(ps.hasObservers()); + assertTrue(ms.hasObservers()); + + ms.onError(new TestException()); + + to.assertFailure(TestException.class); + + assertFalse("main has observers!", ps.hasObservers()); + assertFalse("other has observers", ms.hasObservers()); + } + + @Test + public void undeliverableUponCancel() { + TestHelper.checkUndeliverableUponCancel(new ObservableConverter<Integer, Observable<Integer>>() { + @Override + public Observable<Integer> apply(Observable<Integer> upstream) { + return upstream.mergeWith(Maybe.just(1).hide()); + } + }); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableMergeWithSingleTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableMergeWithSingleTest.java new file mode 100644 index 0000000000..9d6171ba06 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableMergeWithSingleTest.java @@ -0,0 +1,313 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import static org.junit.Assert.*; + +import java.io.IOException; +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.subjects.*; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class ObservableMergeWithSingleTest extends RxJavaTest { + + @Test + public void normal() { + Observable.range(1, 5) + .mergeWith(Single.just(100)) + .test() + .assertResult(1, 2, 3, 4, 5, 100); + } + + @Test + public void normalLong() { + Observable.range(1, 512) + .mergeWith(Single.just(100)) + .test() + .assertValueCount(513) + .assertComplete(); + } + + @Test + public void take() { + Observable.range(1, 5) + .mergeWith(Single.just(100)) + .take(3) + .test() + .assertResult(1, 2, 3); + } + + @Test + public void cancel() { + final PublishSubject<Integer> ps = PublishSubject.create(); + final SingleSubject<Integer> cs = SingleSubject.create(); + + TestObserver<Integer> to = ps.mergeWith(cs).test(); + + assertTrue(ps.hasObservers()); + assertTrue(cs.hasObservers()); + + to.dispose(); + + assertFalse(ps.hasObservers()); + assertFalse(cs.hasObservers()); + } + + @Test + public void mainError() { + Observable.error(new TestException()) + .mergeWith(Single.just(100)) + .test() + .assertFailure(TestException.class); + } + + @Test + public void otherError() { + Observable.never() + .mergeWith(Single.error(new TestException())) + .test() + .assertFailure(TestException.class); + } + + @Test + public void completeRace() { + for (int i = 0; i < 10000; i++) { + final PublishSubject<Integer> ps = PublishSubject.create(); + final SingleSubject<Integer> cs = SingleSubject.create(); + + TestObserver<Integer> to = ps.mergeWith(cs).test(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + ps.onNext(1); + ps.onComplete(); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + cs.onSuccess(1); + } + }; + + TestHelper.race(r1, r2); + + to.assertResult(1, 1); + } + } + + @Test + public void onNextSlowPath() { + final PublishSubject<Integer> ps = PublishSubject.create(); + final SingleSubject<Integer> cs = SingleSubject.create(); + + TestObserver<Integer> to = ps.mergeWith(cs).subscribeWith(new TestObserver<Integer>() { + @Override + public void onNext(Integer t) { + super.onNext(t); + if (t == 1) { + ps.onNext(2); + } + } + }); + + ps.onNext(1); + cs.onSuccess(3); + + ps.onNext(4); + ps.onComplete(); + + to.assertResult(1, 2, 3, 4); + } + + @Test + public void onSuccessSlowPath() { + final PublishSubject<Integer> ps = PublishSubject.create(); + final SingleSubject<Integer> cs = SingleSubject.create(); + + TestObserver<Integer> to = ps.mergeWith(cs).subscribeWith(new TestObserver<Integer>() { + @Override + public void onNext(Integer t) { + super.onNext(t); + if (t == 1) { + cs.onSuccess(2); + } + } + }); + + ps.onNext(1); + + ps.onNext(3); + ps.onComplete(); + + to.assertResult(1, 2, 3); + } + + @Test + public void onErrorMainOverflow() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + final AtomicReference<Observer<?>> observerRef = new AtomicReference<>(); + TestObserver<Integer> to = new Observable<Integer>() { + @Override + protected void subscribeActual(Observer<? super Integer> observer) { + observer.onSubscribe(Disposable.empty()); + observerRef.set(observer); + } + } + .mergeWith(Single.<Integer>error(new IOException())) + .test(); + + observerRef.get().onError(new TestException()); + + to.assertFailure(IOException.class) + ; + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void onErrorOtherOverflow() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + Observable.error(new IOException()) + .mergeWith(Single.error(new TestException())) + .test() + .assertFailure(IOException.class) + ; + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void doubleOnSubscribeMain() { + TestHelper.checkDoubleOnSubscribeObservable( + new Function<Observable<Object>, Observable<Object>>() { + @Override + public Observable<Object> apply(Observable<Object> f) + throws Exception { + return f.mergeWith(Single.just(1)); + } + } + ); + } + + @Test + public void isDisposed() { + new Observable<Integer>() { + @Override + protected void subscribeActual(Observer<? super Integer> observer) { + observer.onSubscribe(Disposable.empty()); + + assertFalse(((Disposable)observer).isDisposed()); + + observer.onNext(1); + + assertTrue(((Disposable)observer).isDisposed()); + } + }.mergeWith(Single.<Integer>just(1)) + .take(1) + .test() + .assertResult(1); + } + + @Test + public void onNextSlowPathCreateQueue() { + final PublishSubject<Integer> ps = PublishSubject.create(); + final SingleSubject<Integer> cs = SingleSubject.create(); + + TestObserver<Integer> to = ps.mergeWith(cs).subscribeWith(new TestObserver<Integer>() { + @Override + public void onNext(Integer t) { + super.onNext(t); + if (t == 1) { + ps.onNext(2); + ps.onNext(3); + } + } + }); + + cs.onSuccess(0); + ps.onNext(1); + + ps.onNext(4); + ps.onComplete(); + + to.assertResult(0, 1, 2, 3, 4); + } + + @Test + public void cancelOtherOnMainError() { + PublishSubject<Integer> ps = PublishSubject.create(); + SingleSubject<Integer> ss = SingleSubject.create(); + + TestObserver<Integer> to = ps.mergeWith(ss).test(); + + assertTrue(ps.hasObservers()); + assertTrue(ss.hasObservers()); + + ps.onError(new TestException()); + + to.assertFailure(TestException.class); + + assertFalse("main has observers!", ps.hasObservers()); + assertFalse("other has observers", ss.hasObservers()); + } + + @Test + public void cancelMainOnOtherError() { + PublishSubject<Integer> ps = PublishSubject.create(); + SingleSubject<Integer> ss = SingleSubject.create(); + + TestObserver<Integer> to = ps.mergeWith(ss).test(); + + assertTrue(ps.hasObservers()); + assertTrue(ss.hasObservers()); + + ss.onError(new TestException()); + + to.assertFailure(TestException.class); + + assertFalse("main has observers!", ps.hasObservers()); + assertFalse("other has observers", ss.hasObservers()); + } + + @Test + public void undeliverableUponCancel() { + TestHelper.checkUndeliverableUponCancel(new ObservableConverter<Integer, Observable<Integer>>() { + @Override + public Observable<Integer> apply(Observable<Integer> upstream) { + return upstream.mergeWith(Single.just(1).hide()); + } + }); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableObserveOnTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableObserveOnTest.java new file mode 100644 index 0000000000..5f41470db0 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableObserveOnTest.java @@ -0,0 +1,852 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +import java.util.*; +import java.util.concurrent.*; +import java.util.concurrent.atomic.*; + +import org.junit.Test; +import org.mockito.InOrder; + +import io.reactivex.rxjava3.annotations.Nullable; +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.core.Observer; +import io.reactivex.rxjava3.disposables.*; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.operators.flowable.FlowableObserveOnTest.DisposeTrackingScheduler; +import io.reactivex.rxjava3.internal.operators.observable.ObservableObserveOn.ObserveOnObserver; +import io.reactivex.rxjava3.internal.schedulers.ImmediateThinScheduler; +import io.reactivex.rxjava3.observers.*; +import io.reactivex.rxjava3.operators.QueueDisposable; +import io.reactivex.rxjava3.operators.QueueFuseable; +import io.reactivex.rxjava3.operators.SimpleQueue; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.schedulers.*; +import io.reactivex.rxjava3.subjects.*; +import io.reactivex.rxjava3.testsupport.*; + +public class ObservableObserveOnTest extends RxJavaTest { + + /** + * This is testing a no-op path since it uses Schedulers.immediate() which will not do scheduling. + */ + @Test + public void observeOn() { + Observer<Integer> observer = TestHelper.mockObserver(); + Observable.just(1, 2, 3).observeOn(ImmediateThinScheduler.INSTANCE).subscribe(observer); + + verify(observer, times(1)).onNext(1); + verify(observer, times(1)).onNext(2); + verify(observer, times(1)).onNext(3); + verify(observer, times(1)).onComplete(); + } + + @Test + public void ordering() throws InterruptedException { +// Observable<String> obs = Observable.just("one", null, "two", "three", "four"); + // FIXME null values not allowed + Observable<String> obs = Observable.just("one", "null", "two", "three", "four"); + + Observer<String> observer = TestHelper.mockObserver(); + + InOrder inOrder = inOrder(observer); + TestObserverEx<String> to = new TestObserverEx<>(observer); + + obs.observeOn(Schedulers.computation()).subscribe(to); + + to.awaitDone(1000, TimeUnit.MILLISECONDS); + if (to.errors().size() > 0) { + for (Throwable t : to.errors()) { + t.printStackTrace(); + } + fail("failed with exception"); + } + + inOrder.verify(observer, times(1)).onNext("one"); + inOrder.verify(observer, times(1)).onNext("null"); + inOrder.verify(observer, times(1)).onNext("two"); + inOrder.verify(observer, times(1)).onNext("three"); + inOrder.verify(observer, times(1)).onNext("four"); + inOrder.verify(observer, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void threadName() throws InterruptedException { + System.out.println("Main Thread: " + Thread.currentThread().getName()); + // FIXME null values not allowed +// Observable<String> obs = Observable.just("one", null, "two", "three", "four"); + Observable<String> obs = Observable.just("one", "null", "two", "three", "four"); + + Observer<String> observer = TestHelper.mockObserver(); + final String parentThreadName = Thread.currentThread().getName(); + + final CountDownLatch completedLatch = new CountDownLatch(1); + + // assert subscribe is on main thread + obs = obs.doOnNext(new Consumer<String>() { + + @Override + public void accept(String s) { + String threadName = Thread.currentThread().getName(); + System.out.println("Source ThreadName: " + threadName + " Expected => " + parentThreadName); + assertEquals(parentThreadName, threadName); + } + + }); + + // assert observe is on new thread + obs.observeOn(Schedulers.newThread()).doOnNext(new Consumer<String>() { + + @Override + public void accept(String t1) { + String threadName = Thread.currentThread().getName(); + boolean correctThreadName = threadName.startsWith("RxNewThreadScheduler"); + System.out.println("ObserveOn ThreadName: " + threadName + " Correct => " + correctThreadName); + assertTrue(correctThreadName); + } + + }).doAfterTerminate(new Action() { + + @Override + public void run() { + completedLatch.countDown(); + + } + }).subscribe(observer); + + if (!completedLatch.await(1000, TimeUnit.MILLISECONDS)) { + fail("timed out waiting"); + } + + verify(observer, never()).onError(any(Throwable.class)); + verify(observer, times(5)).onNext(any(String.class)); + verify(observer, times(1)).onComplete(); + } + + @Test + public void observeOnTheSameSchedulerTwice() { + Scheduler scheduler = ImmediateThinScheduler.INSTANCE; + + Observable<Integer> o = Observable.just(1, 2, 3); + Observable<Integer> o2 = o.observeOn(scheduler); + + Observer<Object> observer1 = TestHelper.mockObserver(); + Observer<Object> observer2 = TestHelper.mockObserver(); + + InOrder inOrder1 = inOrder(observer1); + InOrder inOrder2 = inOrder(observer2); + + o2.subscribe(observer1); + o2.subscribe(observer2); + + inOrder1.verify(observer1, times(1)).onNext(1); + inOrder1.verify(observer1, times(1)).onNext(2); + inOrder1.verify(observer1, times(1)).onNext(3); + inOrder1.verify(observer1, times(1)).onComplete(); + verify(observer1, never()).onError(any(Throwable.class)); + inOrder1.verifyNoMoreInteractions(); + + inOrder2.verify(observer2, times(1)).onNext(1); + inOrder2.verify(observer2, times(1)).onNext(2); + inOrder2.verify(observer2, times(1)).onNext(3); + inOrder2.verify(observer2, times(1)).onComplete(); + verify(observer2, never()).onError(any(Throwable.class)); + inOrder2.verifyNoMoreInteractions(); + } + + @Test + public void observeSameOnMultipleSchedulers() { + TestScheduler scheduler1 = new TestScheduler(); + TestScheduler scheduler2 = new TestScheduler(); + + Observable<Integer> o = Observable.just(1, 2, 3); + Observable<Integer> o1 = o.observeOn(scheduler1); + Observable<Integer> o2 = o.observeOn(scheduler2); + + Observer<Object> observer1 = TestHelper.mockObserver(); + Observer<Object> observer2 = TestHelper.mockObserver(); + + InOrder inOrder1 = inOrder(observer1); + InOrder inOrder2 = inOrder(observer2); + + o1.subscribe(observer1); + o2.subscribe(observer2); + + scheduler1.advanceTimeBy(1, TimeUnit.SECONDS); + scheduler2.advanceTimeBy(1, TimeUnit.SECONDS); + + inOrder1.verify(observer1, times(1)).onNext(1); + inOrder1.verify(observer1, times(1)).onNext(2); + inOrder1.verify(observer1, times(1)).onNext(3); + inOrder1.verify(observer1, times(1)).onComplete(); + verify(observer1, never()).onError(any(Throwable.class)); + inOrder1.verifyNoMoreInteractions(); + + inOrder2.verify(observer2, times(1)).onNext(1); + inOrder2.verify(observer2, times(1)).onNext(2); + inOrder2.verify(observer2, times(1)).onNext(3); + inOrder2.verify(observer2, times(1)).onComplete(); + verify(observer2, never()).onError(any(Throwable.class)); + inOrder2.verifyNoMoreInteractions(); + } + + /** + * Confirm that running on a NewThreadScheduler uses the same thread for the entire stream. + */ + @Test + public void observeOnWithNewThreadScheduler() { + final AtomicInteger count = new AtomicInteger(); + final int _multiple = 99; + + Observable.range(1, 100000).map(new Function<Integer, Integer>() { + + @Override + public Integer apply(Integer t1) { + return t1 * _multiple; + } + + }).observeOn(Schedulers.newThread()) + .blockingForEach(new Consumer<Integer>() { + + @Override + public void accept(Integer t1) { + assertEquals(count.incrementAndGet() * _multiple, t1.intValue()); + // FIXME toBlocking methods run on the current thread + String name = Thread.currentThread().getName(); + assertFalse("Wrong thread name: " + name, name.startsWith("Rx")); + } + + }); + + } + + /** + * Confirm that running on a ThreadPoolScheduler allows multiple threads but is still ordered. + */ + @Test + public void observeOnWithThreadPoolScheduler() { + final AtomicInteger count = new AtomicInteger(); + final int _multiple = 99; + + Observable.range(1, 100000).map(new Function<Integer, Integer>() { + + @Override + public Integer apply(Integer t1) { + return t1 * _multiple; + } + + }).observeOn(Schedulers.computation()) + .blockingForEach(new Consumer<Integer>() { + + @Override + public void accept(Integer t1) { + assertEquals(count.incrementAndGet() * _multiple, t1.intValue()); + // FIXME toBlocking methods run on the caller's thread + String name = Thread.currentThread().getName(); + assertFalse("Wrong thread name: " + name, name.startsWith("Rx")); + } + + }); + } + + /** + * Attempts to confirm that when pauses exist between events, the ScheduledObserver + * does not lose or reorder any events since the scheduler will not block, but will + * be re-scheduled when it receives new events after each pause. + * + * + * This is non-deterministic in proving success, but if it ever fails (non-deterministically) + * it is a sign of potential issues as thread-races and scheduling should not affect output. + */ + @Test + public void observeOnOrderingConcurrency() { + final AtomicInteger count = new AtomicInteger(); + final int _multiple = 99; + + Observable.range(1, 10000).map(new Function<Integer, Integer>() { + + @Override + public Integer apply(Integer t1) { + if (randomIntFrom0to100() > 98) { + try { + Thread.sleep(2); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + return t1 * _multiple; + } + + }).observeOn(Schedulers.computation()) + .blockingForEach(new Consumer<Integer>() { + + @Override + public void accept(Integer t1) { + assertEquals(count.incrementAndGet() * _multiple, t1.intValue()); +// assertTrue(name.startsWith("RxComputationThreadPool")); + // FIXME toBlocking now runs its methods on the caller thread + String name = Thread.currentThread().getName(); + assertFalse("Wrong thread name: " + name, name.startsWith("Rx")); + } + + }); + } + + @Test + public void nonBlockingOuterWhileBlockingOnNext() throws InterruptedException { + + final CountDownLatch completedLatch = new CountDownLatch(1); + final CountDownLatch nextLatch = new CountDownLatch(1); + final AtomicLong completeTime = new AtomicLong(); + // use subscribeOn to make async, observeOn to move + Observable.range(1, 2).subscribeOn(Schedulers.newThread()).observeOn(Schedulers.newThread()).subscribe(new DefaultObserver<Integer>() { + + @Override + public void onComplete() { + System.out.println("onComplete"); + completeTime.set(System.nanoTime()); + completedLatch.countDown(); + } + + @Override + public void onError(Throwable e) { + + } + + @Override + public void onNext(Integer t) { + // don't let this thing finish yet + try { + if (!nextLatch.await(1000, TimeUnit.MILLISECONDS)) { + throw new RuntimeException("it shouldn't have timed out"); + } + } catch (InterruptedException e) { + throw new RuntimeException("it shouldn't have failed"); + } + } + + }); + + long afterSubscribeTime = System.nanoTime(); + System.out.println("After subscribe: " + completedLatch.getCount()); + assertEquals(1, completedLatch.getCount()); + nextLatch.countDown(); + completedLatch.await(1000, TimeUnit.MILLISECONDS); + assertTrue(completeTime.get() > afterSubscribeTime); + System.out.println("onComplete nanos after subscribe: " + (completeTime.get() - afterSubscribeTime)); + } + + private static int randomIntFrom0to100() { + // XORShift instead of Math.random http://javamex.com/tutorials/random_numbers/xorshift.shtml + long x = System.nanoTime(); + x ^= (x << 21); + x ^= (x >>> 35); + x ^= (x << 4); + return Math.abs((int) x % 100); + } + + @Test + public void delayedErrorDeliveryWhenSafeSubscriberUnsubscribes() { + TestScheduler testScheduler = new TestScheduler(); + + Observable<Integer> source = Observable.concat(Observable.<Integer> error(new TestException()), Observable.just(1)); + + Observer<Integer> o = TestHelper.mockObserver(); + InOrder inOrder = inOrder(o); + + source.observeOn(testScheduler).subscribe(o); + + inOrder.verify(o, never()).onError(any(TestException.class)); + + testScheduler.advanceTimeBy(1, TimeUnit.SECONDS); + + inOrder.verify(o).onError(any(TestException.class)); + inOrder.verify(o, never()).onNext(anyInt()); + inOrder.verify(o, never()).onComplete(); + } + + @Test + public void afterUnsubscribeCalledThenObserverOnNextNeverCalled() { + final TestScheduler testScheduler = new TestScheduler(); + + final Observer<Integer> observer = TestHelper.mockObserver(); + TestObserver<Integer> to = new TestObserver<>(observer); + + Observable.just(1, 2, 3) + .observeOn(testScheduler) + .subscribe(to); + + to.dispose(); + testScheduler.advanceTimeBy(1, TimeUnit.SECONDS); + + final InOrder inOrder = inOrder(observer); + + inOrder.verify(observer, never()).onNext(anyInt()); + inOrder.verify(observer, never()).onError(any(Exception.class)); + inOrder.verify(observer, never()).onComplete(); + } + + @Test + public void backpressureWithTakeBefore() { + final AtomicInteger generated = new AtomicInteger(); + Observable<Integer> o = Observable.fromIterable(new Iterable<Integer>() { + @Override + public Iterator<Integer> iterator() { + return new Iterator<Integer>() { + + @Override + public void remove() { + } + + @Override + public Integer next() { + return generated.getAndIncrement(); + } + + @Override + public boolean hasNext() { + return true; + } + }; + } + }); + + TestObserver<Integer> to = new TestObserver<>(); + o + .take(7) + .observeOn(Schedulers.newThread()) + .subscribe(to); + + to.awaitDone(5, TimeUnit.SECONDS); + to.assertValues(0, 1, 2, 3, 4, 5, 6); + assertEquals(7, generated.get()); + } + + @Test + public void asyncChild() { + TestObserver<Integer> to = new TestObserver<>(); + Observable.range(0, 100000).observeOn(Schedulers.newThread()).observeOn(Schedulers.newThread()).subscribe(to); + to.awaitDone(5, TimeUnit.SECONDS); + to.assertNoErrors(); + } + + @Test + public void delayError() { + Observable.range(1, 5).concatWith(Observable.<Integer>error(new TestException())) + .observeOn(Schedulers.computation(), true) + .doOnNext(new Consumer<Integer>() { + @Override + public void accept(Integer v) throws Exception { + if (v == 1) { + Thread.sleep(100); + } + } + }) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertFailure(TestException.class, 1, 2, 3, 4, 5); + } + + @Test + public void trampolineScheduler() { + Observable.just(1) + .observeOn(Schedulers.trampoline()) + .test() + .assertResult(1); + } + + @Test + public void dispose() { + TestHelper.checkDisposed(PublishSubject.create().observeOn(new TestScheduler())); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeObservable(new Function<Observable<Object>, ObservableSource<Object>>() { + @Override + public ObservableSource<Object> apply(Observable<Object> o) throws Exception { + return o.observeOn(new TestScheduler()); + } + }); + } + + @Test + public void badSource() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + TestScheduler scheduler = new TestScheduler(); + TestObserver<Integer> to = new Observable<Integer>() { + @Override + protected void subscribeActual(Observer<? super Integer> observer) { + observer.onSubscribe(Disposable.empty()); + observer.onComplete(); + observer.onNext(1); + observer.onError(new TestException()); + observer.onComplete(); + } + } + .observeOn(scheduler) + .test(); + + scheduler.triggerActions(); + + to.assertResult(); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void inputSyncFused() { + Observable.range(1, 5) + .observeOn(Schedulers.single()) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(1, 2, 3, 4, 5); + } + + @Test + public void inputAsyncFused() { + UnicastSubject<Integer> us = UnicastSubject.create(); + + TestObserver<Integer> to = us.observeOn(Schedulers.single()).test(); + + TestHelper.emit(us, 1, 2, 3, 4, 5); + + to + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(1, 2, 3, 4, 5); + } + + @Test + public void inputAsyncFusedError() { + UnicastSubject<Integer> us = UnicastSubject.create(); + + TestObserver<Integer> to = us.observeOn(Schedulers.single()).test(); + + us.onError(new TestException()); + + to + .awaitDone(5, TimeUnit.SECONDS) + .assertFailure(TestException.class); + } + + @Test + public void inputAsyncFusedErrorDelayed() { + UnicastSubject<Integer> us = UnicastSubject.create(); + + TestObserver<Integer> to = us.observeOn(Schedulers.single(), true).test(); + + us.onError(new TestException()); + + to + .awaitDone(5, TimeUnit.SECONDS) + .assertFailure(TestException.class); + } + + @Test + public void outputFused() { + TestObserverEx<Integer> to = new TestObserverEx<>(QueueFuseable.ANY); + + Observable.range(1, 5).hide() + .observeOn(Schedulers.single()) + .subscribe(to); + + to.assertFusionMode(QueueFuseable.ASYNC) + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(1, 2, 3, 4, 5); + } + + @Test + public void outputFusedReject() { + TestObserverEx<Integer> to = new TestObserverEx<>(QueueFuseable.SYNC); + + Observable.range(1, 5).hide() + .observeOn(Schedulers.single()) + .subscribe(to); + + to.assertFusionMode(QueueFuseable.NONE) + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(1, 2, 3, 4, 5); + } + + @Test + public void inputOutputAsyncFusedError() { + TestObserverEx<Integer> to = new TestObserverEx<>(QueueFuseable.ANY); + + UnicastSubject<Integer> us = UnicastSubject.create(); + + us.observeOn(Schedulers.single()) + .subscribe(to); + + us.onError(new TestException()); + + to + .awaitDone(5, TimeUnit.SECONDS) + .assertFailure(TestException.class); + + to.assertFusionMode(QueueFuseable.ASYNC) + .awaitDone(5, TimeUnit.SECONDS) + .assertFailure(TestException.class); + } + + @Test + public void inputOutputAsyncFusedErrorDelayed() { + TestObserverEx<Integer> to = new TestObserverEx<>(QueueFuseable.ANY); + + UnicastSubject<Integer> us = UnicastSubject.create(); + + us.observeOn(Schedulers.single(), true) + .subscribe(to); + + us.onError(new TestException()); + + to + .awaitDone(5, TimeUnit.SECONDS) + .assertFailure(TestException.class); + + to.assertFusionMode(QueueFuseable.ASYNC) + .awaitDone(5, TimeUnit.SECONDS) + .assertFailure(TestException.class); + } + + @Test + public void outputFusedCancelReentrant() throws Exception { + final UnicastSubject<Integer> us = UnicastSubject.create(); + + final CountDownLatch cdl = new CountDownLatch(1); + + us.observeOn(Schedulers.single()) + .subscribe(new Observer<Integer>() { + Disposable upstream; + int count; + @Override + public void onSubscribe(Disposable d) { + this.upstream = d; + ((QueueDisposable<?>)d).requestFusion(QueueFuseable.ANY); + } + + @Override + public void onNext(Integer value) { + if (++count == 1) { + us.onNext(2); + upstream.dispose(); + cdl.countDown(); + } + } + + @Override + public void onError(Throwable e) { + + } + + @Override + public void onComplete() { + + } + }); + + us.onNext(1); + + cdl.await(); + } + + @Test + public void nonFusedPollThrows() { + new Observable<Integer>() { + @Override + protected void subscribeActual(Observer<? super Integer> observer) { + observer.onSubscribe(Disposable.empty()); + + @SuppressWarnings("unchecked") + ObserveOnObserver<Integer> oo = (ObserveOnObserver<Integer>)observer; + + oo.queue = new SimpleQueue<Integer>() { + + @Override + public boolean offer(Integer value) { + return false; + } + + @Override + public boolean offer(Integer v1, Integer v2) { + return false; + } + + @Nullable + @Override + public Integer poll() throws Exception { + throw new TestException(); + } + + @Override + public boolean isEmpty() { + return false; + } + + @Override + public void clear() { + } + }; + + oo.clear(); + + oo.schedule(); + } + } + .observeOn(Schedulers.single()) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertFailure(TestException.class); + } + + @Test + public void outputFusedOneSignal() { + final BehaviorSubject<Integer> bs = BehaviorSubject.createDefault(1); + + bs.observeOn(ImmediateThinScheduler.INSTANCE) + .concatMap(new Function<Integer, ObservableSource<Integer>>() { + @Override + public ObservableSource<Integer> apply(Integer v) + throws Exception { + return Observable.just(v + 1); + } + }) + .subscribeWith(new TestObserver<Integer>() { + @Override + public void onNext(Integer t) { + super.onNext(t); + if (t == 2) { + bs.onNext(2); + } + } + }) + .assertValuesOnly(2, 3); + } + + @Test + public void workerNotDisposedPrematurelyNormalInNormalOut() { + DisposeTrackingScheduler s = new DisposeTrackingScheduler(); + + Observable.concat( + Observable.just(1).hide().observeOn(s), + Observable.just(2) + ) + .test() + .assertResult(1, 2); + + assertEquals(1, s.disposedCount.get()); + } + + @Test + public void workerNotDisposedPrematurelySyncInNormalOut() { + DisposeTrackingScheduler s = new DisposeTrackingScheduler(); + + Observable.concat( + Observable.just(1).observeOn(s), + Observable.just(2) + ) + .test() + .assertResult(1, 2); + + assertEquals(1, s.disposedCount.get()); + } + + @Test + public void workerNotDisposedPrematurelyAsyncInNormalOut() { + DisposeTrackingScheduler s = new DisposeTrackingScheduler(); + + UnicastSubject<Integer> us = UnicastSubject.create(); + us.onNext(1); + us.onComplete(); + + Observable.concat( + us.observeOn(s), + Observable.just(2) + ) + .test() + .assertResult(1, 2); + + assertEquals(1, s.disposedCount.get()); + } + + static final class TestObserverFusedCanceling + extends TestObserverEx<Integer> { + + TestObserverFusedCanceling() { + super(); + initialFusionMode = QueueFuseable.ANY; + } + + @Override + public void onComplete() { + dispose(); + super.onComplete(); + } + } + + @Test + public void workerNotDisposedPrematurelyNormalInAsyncOut() { + DisposeTrackingScheduler s = new DisposeTrackingScheduler(); + + TestObserverEx<Integer> to = new TestObserverFusedCanceling(); + + Observable.just(1).hide().observeOn(s).subscribe(to); + + assertEquals(1, s.disposedCount.get()); + } + + @Test + public void fusedNoConcurrentCleanDueToCancel() { + for (int j = 0; j < TestHelper.RACE_LONG_LOOPS; j++) { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + final UnicastSubject<Integer> us = UnicastSubject.create(); + + TestObserver<Integer> to = us.hide() + .observeOn(Schedulers.io()) + .observeOn(Schedulers.single()) + .unsubscribeOn(Schedulers.computation()) + .firstOrError() + .test(); + + for (int i = 0; us.hasObservers() && i < 10000; i++) { + us.onNext(i); + } + + to + .awaitDone(5, TimeUnit.SECONDS) + ; + + if (!errors.isEmpty()) { + throw new CompositeException(errors); + } + + to.assertResult(0); + } finally { + RxJavaPlugins.reset(); + } + } + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableOnErrorCompleteTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableOnErrorCompleteTest.java new file mode 100644 index 0000000000..a4671dd535 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableOnErrorCompleteTest.java @@ -0,0 +1,134 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import static org.junit.Assert.*; + +import java.io.IOException; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.subjects.PublishSubject; +import io.reactivex.rxjava3.testsupport.*; + +public class ObservableOnErrorCompleteTest { + + @Test + public void normal() { + Observable.range(1, 10) + .onErrorComplete() + .test() + .assertResult(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + } + + @Test + public void empty() { + Observable.empty() + .onErrorComplete() + .test() + .assertResult(); + } + + @Test + public void error() throws Throwable { + TestHelper.withErrorTracking(errors -> { + Observable.error(new TestException()) + .onErrorComplete() + .test() + .assertResult(); + + assertTrue("" + errors, errors.isEmpty()); + }); + } + + @Test + public void errorMatches() throws Throwable { + TestHelper.withErrorTracking(errors -> { + Observable.error(new TestException()) + .onErrorComplete(error -> error instanceof TestException) + .test() + .assertResult(); + + assertTrue("" + errors, errors.isEmpty()); + }); + } + + @Test + public void errorNotMatches() throws Throwable { + TestHelper.withErrorTracking(errors -> { + Observable.error(new IOException()) + .onErrorComplete(error -> error instanceof TestException) + .test() + .assertFailure(IOException.class); + + assertTrue("" + errors, errors.isEmpty()); + }); + } + + @Test + public void errorPredicateCrash() throws Throwable { + TestHelper.withErrorTracking(errors -> { + TestObserverEx<Object> to = Observable.error(new IOException()) + .onErrorComplete(error -> { throw new TestException(); }) + .subscribeWith(new TestObserverEx<>()) + .assertFailure(CompositeException.class); + + TestHelper.assertError(to, 0, IOException.class); + TestHelper.assertError(to, 1, TestException.class); + + assertTrue("" + errors, errors.isEmpty()); + }); + } + + @Test + public void itemsThenError() throws Throwable { + TestHelper.withErrorTracking(errors -> { + Observable.range(1, 5) + .map(v -> 4 / (3 - v)) + .onErrorComplete() + .test() + .assertResult(2, 4); + + assertTrue("" + errors, errors.isEmpty()); + }); + } + + @Test + public void dispose() { + PublishSubject<Integer> ps = PublishSubject.create(); + + TestObserver<Integer> to = ps + .onErrorComplete() + .test(); + + assertTrue("No subscribers?!", ps.hasObservers()); + + to.dispose(); + + assertFalse("Still subscribers?!", ps.hasObservers()); + } + + @Test + public void onSubscribe() { + TestHelper.checkDoubleOnSubscribeObservable(f -> f.onErrorComplete()); + } + + @Test + public void isDisposed() { + TestHelper.checkDisposed(PublishSubject.create().onErrorComplete()); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableOnErrorResumeNextTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableOnErrorResumeNextTest.java new file mode 100644 index 0000000000..ed3e8dc571 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableOnErrorResumeNextTest.java @@ -0,0 +1,272 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import java.io.IOException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +import io.reactivex.rxjava3.disposables.Disposable; +import org.junit.Test; +import org.mockito.Mockito; +import org.reactivestreams.Subscription; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.schedulers.Schedulers; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class ObservableOnErrorResumeNextTest extends RxJavaTest { + + @Test + public void resumeNextWithSynchronousExecution() { + final AtomicReference<Throwable> receivedException = new AtomicReference<>(); + Observable<String> w = Observable.unsafeCreate(new ObservableSource<String>() { + + @Override + public void subscribe(Observer<? super String> observer) { + observer.onSubscribe(Disposable.empty()); + observer.onNext("one"); + observer.onError(new Throwable("injected failure")); + observer.onNext("two"); + observer.onNext("three"); + } + }); + + Function<Throwable, Observable<String>> resume = new Function<Throwable, Observable<String>>() { + + @Override + public Observable<String> apply(Throwable t1) { + receivedException.set(t1); + return Observable.just("twoResume", "threeResume"); + } + + }; + Observable<String> observable = w.onErrorResumeNext(resume); + + Observer<String> observer = TestHelper.mockObserver(); + + observable.subscribe(observer); + + verify(observer, Mockito.never()).onError(any(Throwable.class)); + verify(observer, times(1)).onComplete(); + verify(observer, times(1)).onNext("one"); + verify(observer, Mockito.never()).onNext("two"); + verify(observer, Mockito.never()).onNext("three"); + verify(observer, times(1)).onNext("twoResume"); + verify(observer, times(1)).onNext("threeResume"); + assertNotNull(receivedException.get()); + } + + @Test + public void resumeNextWithAsyncExecution() { + final AtomicReference<Throwable> receivedException = new AtomicReference<>(); + Subscription s = mock(Subscription.class); + TestObservable w = new TestObservable(s, "one"); + Function<Throwable, Observable<String>> resume = new Function<Throwable, Observable<String>>() { + + @Override + public Observable<String> apply(Throwable t1) { + receivedException.set(t1); + return Observable.just("twoResume", "threeResume"); + } + + }; + Observable<String> o = Observable.unsafeCreate(w).onErrorResumeNext(resume); + + Observer<String> observer = TestHelper.mockObserver(); + + o.subscribe(observer); + + try { + w.t.join(); + } catch (InterruptedException e) { + fail(e.getMessage()); + } + + verify(observer, Mockito.never()).onError(any(Throwable.class)); + verify(observer, times(1)).onComplete(); + verify(observer, times(1)).onNext("one"); + verify(observer, Mockito.never()).onNext("two"); + verify(observer, Mockito.never()).onNext("three"); + verify(observer, times(1)).onNext("twoResume"); + verify(observer, times(1)).onNext("threeResume"); + assertNotNull(receivedException.get()); + } + + /** + * Test that when a function throws an exception this is propagated through onError. + */ + @Test + public void functionThrowsError() { + Subscription s = mock(Subscription.class); + TestObservable w = new TestObservable(s, "one"); + Function<Throwable, Observable<String>> resume = new Function<Throwable, Observable<String>>() { + + @Override + public Observable<String> apply(Throwable t1) { + throw new RuntimeException("exception from function"); + } + + }; + Observable<String> o = Observable.unsafeCreate(w).onErrorResumeNext(resume); + + Observer<String> observer = TestHelper.mockObserver(); + o.subscribe(observer); + + try { + w.t.join(); + } catch (InterruptedException e) { + fail(e.getMessage()); + } + + // we should get the "one" value before the error + verify(observer, times(1)).onNext("one"); + + // we should have received an onError call on the Observer since the resume function threw an exception + verify(observer, times(1)).onError(any(Throwable.class)); + verify(observer, times(0)).onComplete(); + } + + @Test + public void mapResumeAsyncNext() { + // Trigger multiple failures + Observable<String> w = Observable.just("one", "fail", "two", "three", "fail"); + + // Introduce map function that fails intermittently (Map does not prevent this when the Observer is a + // rx.operator incl onErrorResumeNextViaObservable) + w = w.map(new Function<String, String>() { + @Override + public String apply(String s) { + if ("fail".equals(s)) { + throw new RuntimeException("Forced Failure"); + } + System.out.println("BadMapper:" + s); + return s; + } + }); + + Observable<String> o = w.onErrorResumeNext(new Function<Throwable, Observable<String>>() { + + @Override + public Observable<String> apply(Throwable t1) { + return Observable.just("twoResume", "threeResume").subscribeOn(Schedulers.computation()); + } + + }); + + Observer<String> observer = TestHelper.mockObserver(); + + TestObserver<String> to = new TestObserver<>(observer); + o.subscribe(to); + to.awaitDone(5, TimeUnit.SECONDS); + + verify(observer, Mockito.never()).onError(any(Throwable.class)); + verify(observer, times(1)).onComplete(); + verify(observer, times(1)).onNext("one"); + verify(observer, Mockito.never()).onNext("two"); + verify(observer, Mockito.never()).onNext("three"); + verify(observer, times(1)).onNext("twoResume"); + verify(observer, times(1)).onNext("threeResume"); + } + + static class TestObservable implements ObservableSource<String> { + + final String[] values; + Thread t; + + TestObservable(Subscription s, String... values) { + this.values = values; + } + + @Override + public void subscribe(final Observer<? super String> observer) { + System.out.println("TestObservable subscribed to ..."); + observer.onSubscribe(Disposable.empty()); + t = new Thread(new Runnable() { + + @Override + public void run() { + try { + System.out.println("running TestObservable thread"); + for (String s : values) { + System.out.println("TestObservable onNext: " + s); + observer.onNext(s); + } + throw new RuntimeException("Forced Failure"); + } catch (Throwable e) { + observer.onError(e); + } + } + + }); + System.out.println("starting TestObservable thread"); + t.start(); + System.out.println("done starting TestObservable thread"); + } + + } + + @Test + public void backpressure() { + TestObserver<Integer> to = new TestObserver<>(); + Observable.range(0, 100000) + .onErrorResumeNext(new Function<Throwable, Observable<Integer>>() { + + @Override + public Observable<Integer> apply(Throwable t1) { + return Observable.just(1); + } + + }) + .observeOn(Schedulers.computation()) + .map(new Function<Integer, Integer>() { + int c; + + @Override + public Integer apply(Integer t1) { + if (c++ <= 1) { + // slow + try { + Thread.sleep(500); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + return t1; + } + + }) + .subscribe(to); + to.awaitDone(5, TimeUnit.SECONDS); + to.assertNoErrors(); + } + + @Test + public void badOtherSource() { + TestHelper.checkBadSourceObservable(new Function<Observable<Integer>, Object>() { + @Override + public Object apply(Observable<Integer> o) throws Exception { + return Observable.error(new IOException()) + .onErrorResumeNext(Functions.justFunction(o)); + } + }, false, 1, 1, 1); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableOnErrorResumeWithTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableOnErrorResumeWithTest.java new file mode 100644 index 0000000000..7b659c9444 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableOnErrorResumeWithTest.java @@ -0,0 +1,174 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import static org.junit.Assert.fail; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import java.util.concurrent.TimeUnit; + +import org.junit.Test; +import org.mockito.Mockito; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.schedulers.Schedulers; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class ObservableOnErrorResumeWithTest extends RxJavaTest { + + @Test + public void resumeNext() { + Disposable upstream = mock(Disposable.class); + // Trigger failure on second element + TestObservable f = new TestObservable(upstream, "one", "fail", "two", "three"); + Observable<String> w = Observable.unsafeCreate(f); + Observable<String> resume = Observable.just("twoResume", "threeResume"); + Observable<String> observable = w.onErrorResumeWith(resume); + + Observer<String> observer = TestHelper.mockObserver(); + observable.subscribe(observer); + + try { + f.t.join(); + } catch (InterruptedException e) { + fail(e.getMessage()); + } + + verify(observer, Mockito.never()).onError(any(Throwable.class)); + verify(observer, times(1)).onComplete(); + verify(observer, times(1)).onNext("one"); + verify(observer, Mockito.never()).onNext("two"); + verify(observer, Mockito.never()).onNext("three"); + verify(observer, times(1)).onNext("twoResume"); + verify(observer, times(1)).onNext("threeResume"); + } + + @Test + public void mapResumeAsyncNext() { + Disposable sr = mock(Disposable.class); + // Trigger multiple failures + Observable<String> w = Observable.just("one", "fail", "two", "three", "fail"); + // Resume Observable is async + TestObservable f = new TestObservable(sr, "twoResume", "threeResume"); + Observable<String> resume = Observable.unsafeCreate(f); + + // Introduce map function that fails intermittently (Map does not prevent this when the Observer is a + // rx.operator incl onErrorResumeNextViaObservable) + w = w.map(new Function<String, String>() { + @Override + public String apply(String s) { + if ("fail".equals(s)) { + throw new RuntimeException("Forced Failure"); + } + System.out.println("BadMapper:" + s); + return s; + } + }); + + Observable<String> observable = w.onErrorResumeWith(resume); + + Observer<String> observer = TestHelper.mockObserver(); + + observable.subscribe(observer); + + try { + f.t.join(); + } catch (InterruptedException e) { + fail(e.getMessage()); + } + + verify(observer, Mockito.never()).onError(any(Throwable.class)); + verify(observer, times(1)).onComplete(); + verify(observer, times(1)).onNext("one"); + verify(observer, Mockito.never()).onNext("two"); + verify(observer, Mockito.never()).onNext("three"); + verify(observer, times(1)).onNext("twoResume"); + verify(observer, times(1)).onNext("threeResume"); + } + + static class TestObservable implements ObservableSource<String> { + + final Disposable upstream; + final String[] values; + Thread t; + + TestObservable(Disposable upstream, String... values) { + this.upstream = upstream; + this.values = values; + } + + @Override + public void subscribe(final Observer<? super String> observer) { + System.out.println("TestObservable subscribed to ..."); + observer.onSubscribe(upstream); + t = new Thread(new Runnable() { + + @Override + public void run() { + try { + System.out.println("running TestObservable thread"); + for (String s : values) { + if ("fail".equals(s)) { + throw new RuntimeException("Forced Failure"); + } + System.out.println("TestObservable onNext: " + s); + observer.onNext(s); + } + System.out.println("TestObservable onComplete"); + observer.onComplete(); + } catch (Throwable e) { + System.out.println("TestObservable onError: " + e); + observer.onError(e); + } + } + + }); + System.out.println("starting TestObservable thread"); + t.start(); + System.out.println("done starting TestObservable thread"); + } + } + + @Test + public void backpressure() { + TestObserver<Integer> to = new TestObserver<>(); + Observable.range(0, 100000) + .onErrorResumeWith(Observable.just(1)) + .observeOn(Schedulers.computation()) + .map(new Function<Integer, Integer>() { + int c; + + @Override + public Integer apply(Integer t1) { + if (c++ <= 1) { + // slow + try { + Thread.sleep(500); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + return t1; + } + + }) + .subscribe(to); + to.awaitDone(5, TimeUnit.SECONDS); + to.assertNoErrors(); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableOnErrorReturnTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableOnErrorReturnTest.java new file mode 100644 index 0000000000..da7acf2b47 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableOnErrorReturnTest.java @@ -0,0 +1,238 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +import io.reactivex.rxjava3.disposables.Disposable; +import org.junit.Test; +import org.mockito.Mockito; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.schedulers.Schedulers; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class ObservableOnErrorReturnTest extends RxJavaTest { + + @Test + public void resumeNext() { + TestObservable f = new TestObservable("one"); + Observable<String> w = Observable.unsafeCreate(f); + final AtomicReference<Throwable> capturedException = new AtomicReference<>(); + + Observable<String> observable = w.onErrorReturn(new Function<Throwable, String>() { + + @Override + public String apply(Throwable e) { + capturedException.set(e); + return "failure"; + } + + }); + + Observer<String> observer = TestHelper.mockObserver(); + observable.subscribe(observer); + + try { + f.t.join(); + } catch (InterruptedException e) { + fail(e.getMessage()); + } + + verify(observer, Mockito.never()).onError(any(Throwable.class)); + verify(observer, times(1)).onComplete(); + verify(observer, times(1)).onNext("one"); + verify(observer, times(1)).onNext("failure"); + assertNotNull(capturedException.get()); + } + + /** + * Test that when a function throws an exception this is propagated through onError. + */ + @Test + public void functionThrowsError() { + TestObservable f = new TestObservable("one"); + Observable<String> w = Observable.unsafeCreate(f); + final AtomicReference<Throwable> capturedException = new AtomicReference<>(); + + Observable<String> observable = w.onErrorReturn(new Function<Throwable, String>() { + + @Override + public String apply(Throwable e) { + capturedException.set(e); + throw new RuntimeException("exception from function"); + } + + }); + + Observer<String> observer = TestHelper.mockObserver(); + observable.subscribe(observer); + + try { + f.t.join(); + } catch (InterruptedException e) { + fail(e.getMessage()); + } + + // we should get the "one" value before the error + verify(observer, times(1)).onNext("one"); + + // we should have received an onError call on the Observer since the resume function threw an exception + verify(observer, times(1)).onError(any(Throwable.class)); + verify(observer, times(0)).onComplete(); + assertNotNull(capturedException.get()); + } + + @Test + public void mapResumeAsyncNext() { + // Trigger multiple failures + Observable<String> w = Observable.just("one", "fail", "two", "three", "fail"); + + // Introduce map function that fails intermittently (Map does not prevent this when the Observer is a + // rx.operator incl onErrorResumeNextViaObservable) + w = w.map(new Function<String, String>() { + @Override + public String apply(String s) { + if ("fail".equals(s)) { + throw new RuntimeException("Forced Failure"); + } + System.out.println("BadMapper:" + s); + return s; + } + }); + + Observable<String> observable = w.onErrorReturn(new Function<Throwable, String>() { + + @Override + public String apply(Throwable t1) { + return "resume"; + } + + }); + + Observer<String> observer = TestHelper.mockObserver(); + TestObserver<String> to = new TestObserver<>(observer); + observable.subscribe(to); + to.awaitDone(5, TimeUnit.SECONDS); + + verify(observer, Mockito.never()).onError(any(Throwable.class)); + verify(observer, times(1)).onComplete(); + verify(observer, times(1)).onNext("one"); + verify(observer, Mockito.never()).onNext("two"); + verify(observer, Mockito.never()).onNext("three"); + verify(observer, times(1)).onNext("resume"); + } + + @Test + public void backpressure() { + TestObserver<Integer> to = new TestObserver<>(); + Observable.range(0, 100000) + .onErrorReturn(new Function<Throwable, Integer>() { + + @Override + public Integer apply(Throwable t1) { + return 1; + } + + }) + .observeOn(Schedulers.computation()) + .map(new Function<Integer, Integer>() { + int c; + + @Override + public Integer apply(Integer t1) { + if (c++ <= 1) { + // slow + try { + Thread.sleep(500); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + return t1; + } + + }) + .subscribe(to); + to.awaitDone(5, TimeUnit.SECONDS); + to.assertNoErrors(); + } + + private static class TestObservable implements ObservableSource<String> { + + final String[] values; + Thread t; + + TestObservable(String... values) { + this.values = values; + } + + @Override + public void subscribe(final Observer<? super String> observer) { + observer.onSubscribe(Disposable.empty()); + System.out.println("TestObservable subscribed to ..."); + t = new Thread(new Runnable() { + + @Override + public void run() { + try { + System.out.println("running TestObservable thread"); + for (String s : values) { + System.out.println("TestObservable onNext: " + s); + observer.onNext(s); + } + throw new RuntimeException("Forced Failure"); + } catch (Throwable e) { + observer.onError(e); + } + } + + }); + System.out.println("starting TestObservable thread"); + t.start(); + System.out.println("done starting TestObservable thread"); + } + } + + @Test + public void returnItem() { + Observable.error(new TestException()) + .onErrorReturnItem(1) + .test() + .assertResult(1); + } + + @Test + public void dispose() { + TestHelper.checkDisposed(Observable.just(1).onErrorReturnItem(1)); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeObservable(new Function<Observable<Object>, ObservableSource<Object>>() { + @Override + public ObservableSource<Object> apply(Observable<Object> f) throws Exception { + return f.onErrorReturnItem(1); + } + }); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservablePublishTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservablePublishTest.java new file mode 100644 index 0000000000..edec6a517d --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservablePublishTest.java @@ -0,0 +1,896 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import static org.junit.Assert.*; + +import java.util.*; +import java.util.concurrent.*; +import java.util.concurrent.atomic.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.core.Observer; +import io.reactivex.rxjava3.disposables.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.internal.fuseable.HasUpstreamObservableSource; +import io.reactivex.rxjava3.observables.ConnectableObservable; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.schedulers.*; +import io.reactivex.rxjava3.subjects.PublishSubject; +import io.reactivex.rxjava3.testsupport.*; + +public class ObservablePublishTest extends RxJavaTest { + + @Test + public void publish() throws InterruptedException { + final AtomicInteger counter = new AtomicInteger(); + ConnectableObservable<String> o = Observable.unsafeCreate(new ObservableSource<String>() { + + @Override + public void subscribe(final Observer<? super String> observer) { + observer.onSubscribe(Disposable.empty()); + new Thread(new Runnable() { + + @Override + public void run() { + counter.incrementAndGet(); + observer.onNext("one"); + observer.onComplete(); + } + }).start(); + } + }).publish(); + + final CountDownLatch latch = new CountDownLatch(2); + + // subscribe once + o.subscribe(new Consumer<String>() { + + @Override + public void accept(String v) { + assertEquals("one", v); + latch.countDown(); + } + }); + + // subscribe again + o.subscribe(new Consumer<String>() { + + @Override + public void accept(String v) { + assertEquals("one", v); + latch.countDown(); + } + }); + + Disposable connection = o.connect(); + try { + if (!latch.await(1000, TimeUnit.MILLISECONDS)) { + fail("subscriptions did not receive values"); + } + assertEquals(1, counter.get()); + } finally { + connection.dispose(); + } + } + + @Test + public void backpressureFastSlow() { + ConnectableObservable<Integer> is = Observable.range(1, Flowable.bufferSize() * 2).publish(); + Observable<Integer> fast = is.observeOn(Schedulers.computation()) + .doOnComplete(new Action() { + @Override + public void run() { + System.out.println("^^^^^^^^^^^^^ completed FAST"); + } + }); + + Observable<Integer> slow = is.observeOn(Schedulers.computation()).map(new Function<Integer, Integer>() { + int c; + + @Override + public Integer apply(Integer i) { + if (c == 0) { + try { + Thread.sleep(500); + } catch (InterruptedException e) { + } + } + c++; + return i; + } + + }).doOnComplete(new Action() { + + @Override + public void run() { + System.out.println("^^^^^^^^^^^^^ completed SLOW"); + } + + }); + + TestObserver<Integer> to = new TestObserver<>(); + Observable.merge(fast, slow).subscribe(to); + is.connect(); + to.awaitDone(5, TimeUnit.SECONDS); + to.assertNoErrors(); + assertEquals(Flowable.bufferSize() * 4, to.values().size()); + } + + // use case from https://github.com/ReactiveX/RxJava/issues/1732 + @Test + public void takeUntilWithPublishedStreamUsingSelector() { + final AtomicInteger emitted = new AtomicInteger(); + Observable<Integer> xs = Observable.range(0, Flowable.bufferSize() * 2).doOnNext(new Consumer<Integer>() { + + @Override + public void accept(Integer t1) { + emitted.incrementAndGet(); + } + + }); + TestObserver<Integer> to = new TestObserver<>(); + xs.publish(new Function<Observable<Integer>, Observable<Integer>>() { + + @Override + public Observable<Integer> apply(Observable<Integer> xs) { + return xs.takeUntil(xs.skipWhile(new Predicate<Integer>() { + + @Override + public boolean test(Integer i) { + return i <= 3; + } + + })); + } + + }).subscribe(to); + to.awaitDone(5, TimeUnit.SECONDS); + to.assertNoErrors(); + to.assertValues(0, 1, 2, 3); + assertEquals(5, emitted.get()); + System.out.println(to.values()); + } + + // use case from https://github.com/ReactiveX/RxJava/issues/1732 + @Test + public void takeUntilWithPublishedStream() { + Observable<Integer> xs = Observable.range(0, Flowable.bufferSize() * 2); + TestObserver<Integer> to = new TestObserver<>(); + ConnectableObservable<Integer> xsp = xs.publish(); + xsp.takeUntil(xsp.skipWhile(new Predicate<Integer>() { + + @Override + public boolean test(Integer i) { + return i <= 3; + } + + })).subscribe(to); + xsp.connect(); + System.out.println(to.values()); + } + + @Test + public void backpressureTwoConsumers() { + final AtomicInteger sourceEmission = new AtomicInteger(); + final AtomicBoolean sourceUnsubscribed = new AtomicBoolean(); + final Observable<Integer> source = Observable.range(1, 100) + .doOnNext(new Consumer<Integer>() { + @Override + public void accept(Integer t1) { + sourceEmission.incrementAndGet(); + } + }) + .doOnDispose(new Action() { + @Override + public void run() { + sourceUnsubscribed.set(true); + } + }).share(); + ; + + final AtomicBoolean child1Unsubscribed = new AtomicBoolean(); + final AtomicBoolean child2Unsubscribed = new AtomicBoolean(); + + final TestObserver<Integer> to2 = new TestObserver<>(); + + final TestObserver<Integer> to1 = new TestObserver<Integer>() { + @Override + public void onNext(Integer t) { + if (values().size() == 2) { + source.doOnDispose(new Action() { + @Override + public void run() { + child2Unsubscribed.set(true); + } + }).take(5).subscribe(to2); + } + super.onNext(t); + } + }; + + source.doOnDispose(new Action() { + @Override + public void run() { + child1Unsubscribed.set(true); + } + }).take(5) + .subscribe(to1); + + to1.awaitDone(5, TimeUnit.SECONDS); + to2.awaitDone(5, TimeUnit.SECONDS); + + to1.assertNoErrors(); + to2.assertNoErrors(); + + assertTrue(sourceUnsubscribed.get()); + assertTrue(child1Unsubscribed.get()); + assertTrue(child2Unsubscribed.get()); + + to1.assertValues(1, 2, 3, 4, 5); + to2.assertValues(4, 5, 6, 7, 8); + + assertEquals(8, sourceEmission.get()); + } + + @Test + public void connectWithNoSubscriber() { + TestScheduler scheduler = new TestScheduler(); + ConnectableObservable<Long> co = Observable.interval(10, 10, TimeUnit.MILLISECONDS, scheduler).take(3).publish(); + co.connect(); + // Emit 0 + scheduler.advanceTimeBy(15, TimeUnit.MILLISECONDS); + TestObserverEx<Long> to = new TestObserverEx<>(); + co.subscribe(to); + // Emit 1 and 2 + scheduler.advanceTimeBy(50, TimeUnit.MILLISECONDS); + to.assertValues(1L, 2L); + to.assertNoErrors(); + to.assertTerminated(); + } + + @Test + public void subscribeAfterDisconnectThenConnect() { + ConnectableObservable<Integer> source = Observable.just(1).publish(); + + TestObserverEx<Integer> to1 = new TestObserverEx<>(); + + source.subscribe(to1); + + Disposable connection = source.connect(); + + to1.assertValue(1); + to1.assertNoErrors(); + to1.assertTerminated(); + + source.reset(); + + TestObserverEx<Integer> to2 = new TestObserverEx<>(); + + source.subscribe(to2); + + Disposable connection2 = source.connect(); + + to2.assertValue(1); + to2.assertNoErrors(); + to2.assertTerminated(); + + System.out.println(connection); + System.out.println(connection2); + } + + @Test + public void noSubscriberRetentionOnCompleted() { + ObservablePublish<Integer> source = (ObservablePublish<Integer>)Observable.just(1).publish(); + + TestObserverEx<Integer> to1 = new TestObserverEx<>(); + + source.subscribe(to1); + + to1.assertNoValues(); + to1.assertNoErrors(); + to1.assertNotComplete(); + + source.connect(); + + to1.assertValue(1); + to1.assertNoErrors(); + to1.assertTerminated(); + + assertEquals(0, source.current.get().get().length); + } + + @Test + public void nonNullConnection() { + ConnectableObservable<Object> source = Observable.never().publish(); + + assertNotNull(source.connect()); + assertNotNull(source.connect()); + } + + @Test + public void noDisconnectSomeoneElse() { + ConnectableObservable<Object> source = Observable.never().publish(); + + Disposable connection1 = source.connect(); + Disposable connection2 = source.connect(); + + connection1.dispose(); + + Disposable connection3 = source.connect(); + + connection2.dispose(); + + assertTrue(checkPublishDisposed(connection1)); + assertTrue(checkPublishDisposed(connection2)); + assertFalse(checkPublishDisposed(connection3)); + } + + @SuppressWarnings("unchecked") + static boolean checkPublishDisposed(Disposable d) { + return ((ObservablePublish.PublishConnection<Object>)d).isDisposed(); + } + + @Test + public void connectIsIdempotent() { + final AtomicInteger calls = new AtomicInteger(); + Observable<Integer> source = Observable.unsafeCreate(new ObservableSource<Integer>() { + @Override + public void subscribe(Observer<? super Integer> t) { + t.onSubscribe(Disposable.empty()); + calls.getAndIncrement(); + } + }); + + ConnectableObservable<Integer> conn = source.publish(); + + assertEquals(0, calls.get()); + + conn.connect(); + conn.connect(); + + assertEquals(1, calls.get()); + + conn.connect().dispose(); + + conn.connect(); + conn.connect(); + + assertEquals(2, calls.get()); + } + + @Test + public void observeOn() { + ConnectableObservable<Integer> co = Observable.range(0, 1000).publish(); + Observable<Integer> obs = co.observeOn(Schedulers.computation()); + for (int i = 0; i < 1000; i++) { + for (int j = 1; j < 6; j++) { + List<TestObserverEx<Integer>> tos = new ArrayList<>(); + for (int k = 1; k < j; k++) { + TestObserverEx<Integer> to = new TestObserverEx<>(); + tos.add(to); + obs.subscribe(to); + } + + Disposable connection = co.connect(); + + for (TestObserverEx<Integer> to : tos) { + to.awaitDone(2, TimeUnit.SECONDS); + to.assertTerminated(); + to.assertNoErrors(); + assertEquals(1000, to.values().size()); + } + connection.dispose(); + } + } + } + + @Test + public void preNextConnect() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + + final ConnectableObservable<Integer> co = Observable.<Integer>empty().publish(); + + co.connect(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + co.test(); + } + }; + + TestHelper.race(r1, r1); + } + } + + @Test + public void connectRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + + final ConnectableObservable<Integer> co = Observable.<Integer>empty().publish(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + co.connect(); + } + }; + + TestHelper.race(r1, r1); + } + } + + @Test + public void selectorCrash() { + Observable.just(1).publish(new Function<Observable<Integer>, ObservableSource<Object>>() { + @Override + public ObservableSource<Object> apply(Observable<Integer> v) throws Exception { + throw new TestException(); + } + }) + .test() + .assertFailure(TestException.class); + } + + @Test + public void source() { + Observable<Integer> o = Observable.never(); + + assertSame(o, (((HasUpstreamObservableSource<?>)o.publish()).source())); + } + + @Test + public void connectThrows() { + ConnectableObservable<Integer> co = Observable.<Integer>empty().publish(); + try { + co.connect(new Consumer<Disposable>() { + @Override + public void accept(Disposable d) throws Exception { + throw new TestException(); + } + }); + } catch (TestException ex) { + // expected + } + } + + @Test + public void addRemoveRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + + final ConnectableObservable<Integer> co = Observable.<Integer>empty().publish(); + + final TestObserver<Integer> to = co.test(); + + final TestObserver<Integer> to2 = new TestObserver<>(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + co.subscribe(to2); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + to.dispose(); + } + }; + + TestHelper.race(r1, r2); + } + } + + @Test + public void disposeOnArrival() { + ConnectableObservable<Integer> co = Observable.<Integer>empty().publish(); + + co.test(true).assertEmpty(); + } + + @Test + public void dispose() { + TestHelper.checkDisposed(Observable.never().publish()); + + TestHelper.checkDisposed(Observable.never().publish(Functions.<Observable<Object>>identity())); + } + + @Test + public void empty() { + ConnectableObservable<Integer> co = Observable.<Integer>empty().publish(); + + co.connect(); + } + + @Test + public void take() { + ConnectableObservable<Integer> co = Observable.range(1, 2).publish(); + + TestObserver<Integer> to = co.take(1).test(); + + co.connect(); + + to.assertResult(1); + } + + @Test + public void just() { + final PublishSubject<Integer> ps = PublishSubject.create(); + + ConnectableObservable<Integer> co = ps.publish(); + + TestObserver<Integer> to = new TestObserver<Integer>() { + @Override + public void onNext(Integer t) { + super.onNext(t); + ps.onComplete(); + } + }; + + co.subscribe(to); + co.connect(); + + ps.onNext(1); + + to.assertResult(1); + } + + @Test + public void nextCancelRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + + final PublishSubject<Integer> ps = PublishSubject.create(); + + final ConnectableObservable<Integer> co = ps.publish(); + + final TestObserver<Integer> to = co.test(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + ps.onNext(1); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + to.dispose(); + } + }; + + TestHelper.race(r1, r2); + } + } + + @Test + public void badSource() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + new Observable<Integer>() { + @Override + protected void subscribeActual(Observer<? super Integer> observer) { + observer.onSubscribe(Disposable.empty()); + observer.onNext(1); + observer.onComplete(); + observer.onNext(2); + observer.onError(new TestException()); + observer.onComplete(); + } + } + .publish() + .autoConnect() + .test() + .assertResult(1); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void noErrorLoss() { + ConnectableObservable<Object> co = Observable.error(new TestException()).publish(); + + co.connect(); + + // 3.x: terminal events remain observable until reset + co.test() + .assertFailure(TestException.class); + } + + @Test + public void subscribeDisconnectRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + + final PublishSubject<Integer> ps = PublishSubject.create(); + + final ConnectableObservable<Integer> co = ps.publish(); + + final Disposable d = co.connect(); + final TestObserver<Integer> to = new TestObserver<>(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + d.dispose(); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + co.subscribe(to); + } + }; + + TestHelper.race(r1, r2); + } + } + + @Test + public void selectorDisconnectsIndependentSource() { + PublishSubject<Integer> ps = PublishSubject.create(); + + ps.publish(new Function<Observable<Integer>, ObservableSource<Integer>>() { + @Override + public ObservableSource<Integer> apply(Observable<Integer> v) throws Exception { + return Observable.range(1, 2); + } + }) + .test() + .assertResult(1, 2); + + assertFalse(ps.hasObservers()); + } + + @Test + public void selectorLatecommer() { + Observable.range(1, 5) + .publish(new Function<Observable<Integer>, ObservableSource<Integer>>() { + @Override + public ObservableSource<Integer> apply(Observable<Integer> v) throws Exception { + return v.concatWith(v); + } + }) + .test() + .assertResult(1, 2, 3, 4, 5); + } + + @Test + public void mainError() { + Observable.error(new TestException()) + .publish(Functions.<Observable<Object>>identity()) + .test() + .assertFailure(TestException.class); + } + + @Test + public void selectorInnerError() { + PublishSubject<Integer> ps = PublishSubject.create(); + + ps.publish(new Function<Observable<Integer>, ObservableSource<Integer>>() { + @Override + public ObservableSource<Integer> apply(Observable<Integer> v) throws Exception { + return Observable.error(new TestException()); + } + }) + .test() + .assertFailure(TestException.class); + + assertFalse(ps.hasObservers()); + } + + @Test + public void delayedUpstreamOnSubscribe() { + final Observer<?>[] sub = { null }; + + new Observable<Integer>() { + @Override + protected void subscribeActual(Observer<? super Integer> observer) { + sub[0] = observer; + } + } + .publish() + .connect() + .dispose(); + + Disposable bs = Disposable.empty(); + + sub[0].onSubscribe(bs); + + assertTrue(bs.isDisposed()); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeObservable(new Function<Observable<Object>, ObservableSource<Object>>() { + @Override + public ObservableSource<Object> apply(final Observable<Object> o) + throws Exception { + return Observable.<Integer>never().publish(new Function<Observable<Integer>, ObservableSource<Object>>() { + @Override + public ObservableSource<Object> apply(Observable<Integer> v) + throws Exception { + return o; + } + }); + } + } + ); + } + + @Test + public void disposedUpfront() { + ConnectableObservable<Integer> co = Observable.just(1) + .concatWith(Observable.<Integer>never()) + .publish(); + + TestObserver<Integer> to1 = co.test(); + + TestObserver<Integer> to2 = co.test(true); + + co.connect(); + + to1.assertValuesOnly(1); + + to2.assertEmpty(); + + ((ObservablePublish<Integer>)co).current.get().remove(null); + } + + @Test + public void altConnectCrash() { + try { + new ObservablePublish<>(Observable.<Integer>empty()) + .connect(new Consumer<Disposable>() { + @Override + public void accept(Disposable t) throws Exception { + throw new TestException(); + } + }); + fail("Should have thrown"); + } catch (TestException expected) { + // expected + } + } + + @Test + public void altConnectRace() { + for (int i = 0; i < TestHelper.RACE_LONG_LOOPS; i++) { + final ConnectableObservable<Integer> co = + new ObservablePublish<>(Observable.<Integer>never()); + + Runnable r = new Runnable() { + @Override + public void run() { + co.connect(); + } + }; + + TestHelper.race(r, r); + } + } + + @Test + public void onCompleteAvailableUntilReset() { + ConnectableObservable<Integer> co = Observable.just(1).publish(); + + TestObserver<Integer> to = co.test(); + to.assertEmpty(); + + co.connect(); + + to.assertResult(1); + + co.test().assertResult(); + + co.reset(); + + to = co.test(); + to.assertEmpty(); + + co.connect(); + + to.assertResult(1); + } + + @Test + public void onErrorAvailableUntilReset() { + ConnectableObservable<Integer> co = Observable.just(1) + .concatWith(Observable.<Integer>error(new TestException())) + .publish(); + + TestObserver<Integer> to = co.test(); + to.assertEmpty(); + + co.connect(); + + to.assertFailure(TestException.class, 1); + + co.test().assertFailure(TestException.class); + + co.reset(); + + to = co.test(); + to.assertEmpty(); + + co.connect(); + + to.assertFailure(TestException.class, 1); + } + + @Test + public void disposeResets() { + PublishSubject<Integer> ps = PublishSubject.create(); + + ConnectableObservable<Integer> co = ps.publish(); + + assertFalse(ps.hasObservers()); + + Disposable d = co.connect(); + + assertTrue(ps.hasObservers()); + + d.dispose(); + + assertFalse(ps.hasObservers()); + + TestObserver<Integer> to = co.test(); + + co.connect(); + + assertTrue(ps.hasObservers()); + + ps.onNext(1); + + to.assertValuesOnly(1); + } + + @Test + public void disposeNoNeedForReset() { + PublishSubject<Integer> ps = PublishSubject.create(); + + ConnectableObservable<Integer> co = ps.publish(); + + TestObserver<Integer> to = co.test(); + + Disposable d = co.connect(); + + ps.onNext(1); + + d.dispose(); + + to = co.test(); + + to.assertEmpty(); + + co.connect(); + + to.assertEmpty(); + + ps.onNext(2); + + to.assertValuesOnly(2); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableRangeLongTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableRangeLongTest.java new file mode 100644 index 0000000000..c19219cdea --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableRangeLongTest.java @@ -0,0 +1,201 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import java.util.ArrayList; +import java.util.concurrent.atomic.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.functions.Consumer; +import io.reactivex.rxjava3.observers.*; +import io.reactivex.rxjava3.operators.QueueFuseable; +import io.reactivex.rxjava3.testsupport.*; + +public class ObservableRangeLongTest extends RxJavaTest { + @Test + public void rangeStartAt2Count3() { + Observer<Long> observer = TestHelper.mockObserver(); + + Observable.rangeLong(2, 3).subscribe(observer); + + verify(observer, times(1)).onNext(2L); + verify(observer, times(1)).onNext(3L); + verify(observer, times(1)).onNext(4L); + verify(observer, never()).onNext(5L); + verify(observer, never()).onError(any(Throwable.class)); + verify(observer, times(1)).onComplete(); + } + + @Test + public void rangeUnsubscribe() { + Observer<Long> observer = TestHelper.mockObserver(); + + final AtomicInteger count = new AtomicInteger(); + + Observable.rangeLong(1, 1000).doOnNext(new Consumer<Long>() { + @Override + public void accept(Long t1) { + count.incrementAndGet(); + } + }) + .take(3).subscribe(observer); + + verify(observer, times(1)).onNext(1L); + verify(observer, times(1)).onNext(2L); + verify(observer, times(1)).onNext(3L); + verify(observer, never()).onNext(4L); + verify(observer, never()).onError(any(Throwable.class)); + verify(observer, times(1)).onComplete(); + assertEquals(3, count.get()); + } + + @Test + public void rangeWithZero() { + Observable.rangeLong(1L, 0L); + } + + @Test + public void rangeWithOverflow2() { + Observable.rangeLong(Long.MAX_VALUE, 0L); + } + + @Test + public void rangeWithOverflow3() { + Observable.rangeLong(1L, Long.MAX_VALUE); + } + + @Test(expected = IllegalArgumentException.class) + public void rangeWithOverflow4() { + Observable.rangeLong(2L, Long.MAX_VALUE); + } + + @Test + public void rangeWithOverflow5() { + assertFalse(Observable.rangeLong(Long.MIN_VALUE, 0).blockingIterable().iterator().hasNext()); + } + + @Test + public void noBackpressure() { + ArrayList<Long> list = new ArrayList<>(Flowable.bufferSize() * 2); + for (long i = 1; i <= Flowable.bufferSize() * 2 + 1; i++) { + list.add(i); + } + + Observable<Long> o = Observable.rangeLong(1, list.size()); + + TestObserverEx<Long> to = new TestObserverEx<>(); + + o.subscribe(to); + + to.assertValueSequence(list); + to.assertTerminated(); + } + + @Test + public void emptyRangeSendsOnCompleteEagerlyWithRequestZero() { + final AtomicBoolean completed = new AtomicBoolean(false); + Observable.rangeLong(1L, 0L).subscribe(new DefaultObserver<Long>() { + + @Override + public void onStart() { +// request(0); + } + + @Override + public void onComplete() { + completed.set(true); + } + + @Override + public void onError(Throwable e) { + + } + + @Override + public void onNext(Long t) { + + }}); + assertTrue(completed.get()); + } + + @Test + public void nearMaxValueWithoutBackpressure() { + TestObserver<Long> to = new TestObserver<>(); + Observable.rangeLong(Long.MAX_VALUE - 1L, 2L).subscribe(to); + + to.assertComplete(); + to.assertNoErrors(); + to.assertValues(Long.MAX_VALUE - 1, Long.MAX_VALUE); + } + + @Test + public void negativeCount() { + try { + Observable.rangeLong(1L, -1L); + fail("Should have thrown IllegalArgumentException"); + } catch (IllegalArgumentException ex) { + assertEquals("count >= 0 required but it was -1", ex.getMessage()); + } + } + + @Test + public void countOne() { + Observable.rangeLong(5495454L, 1L) + .test() + .assertResult(5495454L); + } + + @Test + public void noOverflow() { + Observable.rangeLong(Long.MAX_VALUE - 1, 2); + Observable.rangeLong(Long.MIN_VALUE, 2); + Observable.rangeLong(Long.MIN_VALUE, Long.MAX_VALUE); + } + + @Test + public void fused() { + TestObserverEx<Long> to = new TestObserverEx<>(QueueFuseable.ANY); + + Observable.rangeLong(1, 2).subscribe(to); + + to.assertFusionMode(QueueFuseable.SYNC) + .assertResult(1L, 2L); + } + + @Test + public void fusedReject() { + TestObserverEx<Long> to = new TestObserverEx<>(QueueFuseable.ASYNC); + + Observable.rangeLong(1, 2).subscribe(to); + + to.assertFusionMode(QueueFuseable.NONE) + .assertResult(1L, 2L); + } + + @Test + public void disposed() { + TestHelper.checkDisposed(Observable.rangeLong(1, 2)); + } + + @Test + public void fusedClearIsEmpty() { + TestHelper.checkFusedIsEmptyClear(Observable.rangeLong(1, 2)); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableRangeTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableRangeTest.java new file mode 100644 index 0000000000..0f461a7154 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableRangeTest.java @@ -0,0 +1,169 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import java.util.ArrayList; +import java.util.concurrent.atomic.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.functions.Consumer; +import io.reactivex.rxjava3.observers.*; +import io.reactivex.rxjava3.operators.QueueFuseable; +import io.reactivex.rxjava3.testsupport.*; + +public class ObservableRangeTest extends RxJavaTest { + + @Test + public void rangeStartAt2Count3() { + Observer<Integer> observer = TestHelper.mockObserver(); + + Observable.range(2, 3).subscribe(observer); + + verify(observer, times(1)).onNext(2); + verify(observer, times(1)).onNext(3); + verify(observer, times(1)).onNext(4); + verify(observer, never()).onNext(5); + verify(observer, never()).onError(any(Throwable.class)); + verify(observer, times(1)).onComplete(); + } + + @Test + public void rangeUnsubscribe() { + Observer<Integer> observer = TestHelper.mockObserver(); + + final AtomicInteger count = new AtomicInteger(); + + Observable.range(1, 1000).doOnNext(new Consumer<Integer>() { + @Override + public void accept(Integer t1) { + count.incrementAndGet(); + } + }) + .take(3).subscribe(observer); + + verify(observer, times(1)).onNext(1); + verify(observer, times(1)).onNext(2); + verify(observer, times(1)).onNext(3); + verify(observer, never()).onNext(4); + verify(observer, never()).onError(any(Throwable.class)); + verify(observer, times(1)).onComplete(); + assertEquals(3, count.get()); + } + + @Test + public void rangeWithZero() { + Observable.range(1, 0); + } + + @Test + public void rangeWithOverflow2() { + Observable.range(Integer.MAX_VALUE, 0); + } + + @Test + public void rangeWithOverflow3() { + Observable.range(1, Integer.MAX_VALUE); + } + + @Test(expected = IllegalArgumentException.class) + public void rangeWithOverflow4() { + Observable.range(2, Integer.MAX_VALUE); + } + + @Test + public void rangeWithOverflow5() { + assertFalse(Observable.range(Integer.MIN_VALUE, 0).blockingIterable().iterator().hasNext()); + } + + @Test + public void noBackpressure() { + ArrayList<Integer> list = new ArrayList<>(Flowable.bufferSize() * 2); + for (int i = 1; i <= Flowable.bufferSize() * 2 + 1; i++) { + list.add(i); + } + + Observable<Integer> o = Observable.range(1, list.size()); + + TestObserverEx<Integer> to = new TestObserverEx<>(); + + o.subscribe(to); + + to.assertValueSequence(list); + to.assertTerminated(); + } + + @Test + public void emptyRangeSendsOnCompleteEagerlyWithRequestZero() { + final AtomicBoolean completed = new AtomicBoolean(false); + Observable.range(1, 0).subscribe(new DefaultObserver<Integer>() { + + @Override + public void onStart() { +// request(0); + } + + @Override + public void onComplete() { + completed.set(true); + } + + @Override + public void onError(Throwable e) { + + } + + @Override + public void onNext(Integer t) { + + }}); + assertTrue(completed.get()); + } + + @Test + public void nearMaxValueWithoutBackpressure() { + TestObserver<Integer> to = new TestObserver<>(); + Observable.range(Integer.MAX_VALUE - 1, 2).subscribe(to); + + to.assertComplete(); + to.assertNoErrors(); + to.assertValues(Integer.MAX_VALUE - 1, Integer.MAX_VALUE); + } + + @Test + public void negativeCount() { + try { + Observable.range(1, -1); + fail("Should have thrown IllegalArgumentException"); + } catch (IllegalArgumentException ex) { + assertEquals("count >= 0 required but it was -1", ex.getMessage()); + } + } + + @Test + public void requestWrongFusion() { + TestObserverEx<Integer> to = new TestObserverEx<>(QueueFuseable.ASYNC); + + Observable.range(1, 5) + .subscribe(to); + + to.assertFusionMode(QueueFuseable.NONE) + .assertResult(1, 2, 3, 4, 5); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableRedoTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableRedoTest.java new file mode 100644 index 0000000000..adb2caf4a3 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableRedoTest.java @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.observers.TestObserver; + +public class ObservableRedoTest extends RxJavaTest { + + @Test + public void redoCancel() { + final TestObserver<Integer> to = new TestObserver<>(); + + Observable.just(1) + .repeatWhen(new Function<Observable<Object>, ObservableSource<Object>>() { + @Override + public ObservableSource<Object> apply(Observable<Object> o) throws Exception { + return o.map(new Function<Object, Object>() { + int count; + @Override + public Object apply(Object v) throws Exception { + if (++count == 1) { + to.dispose(); + } + return v; + } + }); + } + }) + .subscribe(to); + } + + @Test + public void managerThrows() { + Observable.just(1) + .retryWhen(new Function<Observable<Throwable>, ObservableSource<Object>>() { + @Override + public ObservableSource<Object> apply(Observable<Throwable> v) throws Exception { + throw new TestException(); + } + }) + .test() + .assertFailure(TestException.class); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableReduceTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableReduceTest.java new file mode 100644 index 0000000000..0feab233d3 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableReduceTest.java @@ -0,0 +1,369 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import java.util.List; + +import io.reactivex.rxjava3.disposables.Disposable; +import org.junit.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.subjects.PublishSubject; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class ObservableReduceTest extends RxJavaTest { + Observer<Object> observer; + SingleObserver<Object> singleObserver; + + @Before + public void before() { + observer = TestHelper.mockObserver(); + singleObserver = TestHelper.mockSingleObserver(); + } + + BiFunction<Integer, Integer, Integer> sum = new BiFunction<Integer, Integer, Integer>() { + @Override + public Integer apply(Integer t1, Integer t2) { + return t1 + t2; + } + }; + + @Test + public void aggregateAsIntSumObservable() { + + Observable<Integer> result = Observable.just(1, 2, 3, 4, 5).reduce(0, sum) + .map(new Function<Integer, Integer>() { + @Override + public Integer apply(Integer v) { + return v; + } + }).toObservable(); + + result.subscribe(observer); + + verify(observer).onNext(1 + 2 + 3 + 4 + 5); + verify(observer).onComplete(); + verify(observer, never()).onError(any(Throwable.class)); + } + + @Test + public void aggregateAsIntSumSourceThrowsObservable() { + Observable<Integer> result = Observable.concat(Observable.just(1, 2, 3, 4, 5), + Observable.<Integer> error(new TestException())) + .reduce(0, sum).map(new Function<Integer, Integer>() { + @Override + public Integer apply(Integer v) { + return v; + } + }).toObservable(); + + result.subscribe(observer); + + verify(observer, never()).onNext(any()); + verify(observer, never()).onComplete(); + verify(observer, times(1)).onError(any(TestException.class)); + } + + @Test + public void aggregateAsIntSumAccumulatorThrowsObservable() { + BiFunction<Integer, Integer, Integer> sumErr = new BiFunction<Integer, Integer, Integer>() { + @Override + public Integer apply(Integer t1, Integer t2) { + throw new TestException(); + } + }; + + Observable<Integer> result = Observable.just(1, 2, 3, 4, 5) + .reduce(0, sumErr).map(new Function<Integer, Integer>() { + @Override + public Integer apply(Integer v) { + return v; + } + }).toObservable(); + + result.subscribe(observer); + + verify(observer, never()).onNext(any()); + verify(observer, never()).onComplete(); + verify(observer, times(1)).onError(any(TestException.class)); + } + + @Test + public void aggregateAsIntSumResultSelectorThrowsObservable() { + + Function<Integer, Integer> error = new Function<Integer, Integer>() { + + @Override + public Integer apply(Integer t1) { + throw new TestException(); + } + }; + + Observable<Integer> result = Observable.just(1, 2, 3, 4, 5) + .reduce(0, sum).toObservable().map(error); + + result.subscribe(observer); + + verify(observer, never()).onNext(any()); + verify(observer, never()).onComplete(); + verify(observer, times(1)).onError(any(TestException.class)); + } + + @Test + public void backpressureWithNoInitialValueObservable() throws InterruptedException { + Observable<Integer> source = Observable.just(1, 2, 3, 4, 5, 6); + Observable<Integer> reduced = source.reduce(sum).toObservable(); + + Integer r = reduced.blockingFirst(); + assertEquals(21, r.intValue()); + } + + @Test + public void backpressureWithInitialValueObservable() throws InterruptedException { + Observable<Integer> source = Observable.just(1, 2, 3, 4, 5, 6); + Observable<Integer> reduced = source.reduce(0, sum).toObservable(); + + Integer r = reduced.blockingFirst(); + assertEquals(21, r.intValue()); + } + + @Test + public void aggregateAsIntSum() { + + Single<Integer> result = Observable.just(1, 2, 3, 4, 5).reduce(0, sum) + .map(new Function<Integer, Integer>() { + @Override + public Integer apply(Integer v) { + return v; + } + }); + + result.subscribe(singleObserver); + + verify(singleObserver).onSuccess(1 + 2 + 3 + 4 + 5); + verify(singleObserver, never()).onError(any(Throwable.class)); + } + + @Test + public void aggregateAsIntSumSourceThrows() { + Single<Integer> result = Observable.concat(Observable.just(1, 2, 3, 4, 5), + Observable.<Integer> error(new TestException())) + .reduce(0, sum).map(new Function<Integer, Integer>() { + @Override + public Integer apply(Integer v) { + return v; + } + }); + + result.subscribe(singleObserver); + + verify(singleObserver, never()).onSuccess(any()); + verify(singleObserver, times(1)).onError(any(TestException.class)); + } + + @Test + public void aggregateAsIntSumAccumulatorThrows() { + BiFunction<Integer, Integer, Integer> sumErr = new BiFunction<Integer, Integer, Integer>() { + @Override + public Integer apply(Integer t1, Integer t2) { + throw new TestException(); + } + }; + + Single<Integer> result = Observable.just(1, 2, 3, 4, 5) + .reduce(0, sumErr).map(new Function<Integer, Integer>() { + @Override + public Integer apply(Integer v) { + return v; + } + }); + + result.subscribe(singleObserver); + + verify(singleObserver, never()).onSuccess(any()); + verify(singleObserver, times(1)).onError(any(TestException.class)); + } + + @Test + public void aggregateAsIntSumResultSelectorThrows() { + + Function<Integer, Integer> error = new Function<Integer, Integer>() { + + @Override + public Integer apply(Integer t1) { + throw new TestException(); + } + }; + + Single<Integer> result = Observable.just(1, 2, 3, 4, 5) + .reduce(0, sum).map(error); + + result.subscribe(singleObserver); + + verify(singleObserver, never()).onSuccess(any()); + verify(singleObserver, times(1)).onError(any(TestException.class)); + } + + @Test + public void backpressureWithNoInitialValue() throws InterruptedException { + Observable<Integer> source = Observable.just(1, 2, 3, 4, 5, 6); + Maybe<Integer> reduced = source.reduce(sum); + + Integer r = reduced.blockingGet(); + assertEquals(21, r.intValue()); + } + + @Test + public void backpressureWithInitialValue() throws InterruptedException { + Observable<Integer> source = Observable.just(1, 2, 3, 4, 5, 6); + Single<Integer> reduced = source.reduce(0, sum); + + Integer r = reduced.blockingGet(); + assertEquals(21, r.intValue()); + } + + @Test + public void reduceWithSingle() { + Observable.range(1, 5) + .reduceWith(new Supplier<Integer>() { + @Override + public Integer get() throws Exception { + return 0; + } + }, new BiFunction<Integer, Integer, Integer>() { + @Override + public Integer apply(Integer a, Integer b) throws Exception { + return a + b; + } + }) + .test() + .assertResult(15); + } + + @Test + public void reduceMaybeDoubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeObservableToMaybe(new Function<Observable<Object>, MaybeSource<Object>>() { + @Override + public MaybeSource<Object> apply(Observable<Object> o) + throws Exception { + return o.reduce(new BiFunction<Object, Object, Object>() { + @Override + public Object apply(Object a, Object b) throws Exception { + return a; + } + }); + } + }); + } + + @Test + public void reduceMaybeCheckDisposed() { + TestHelper.checkDisposed(Observable.just(new Object()).reduce(new BiFunction<Object, Object, Object>() { + @Override + public Object apply(Object a, Object b) throws Exception { + return a; + } + })); + } + + @Test + public void reduceMaybeBadSource() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + new Observable<Object>() { + @Override + protected void subscribeActual(Observer<? super Object> observer) { + observer.onSubscribe(Disposable.empty()); + observer.onComplete(); + observer.onNext(1); + observer.onError(new TestException()); + observer.onComplete(); + } + }.reduce(new BiFunction<Object, Object, Object>() { + @Override + public Object apply(Object a, Object b) throws Exception { + return a; + } + }) + .test() + .assertResult(); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void seedDoubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeObservableToSingle(new Function<Observable<Integer>, SingleSource<Integer>>() { + @Override + public SingleSource<Integer> apply(Observable<Integer> o) + throws Exception { + return o.reduce(0, new BiFunction<Integer, Integer, Integer>() { + @Override + public Integer apply(Integer a, Integer b) throws Exception { + return a; + } + }); + } + }); + } + + @Test + public void seedDisposed() { + TestHelper.checkDisposed(PublishSubject.<Integer>create().reduce(0, new BiFunction<Integer, Integer, Integer>() { + @Override + public Integer apply(Integer a, Integer b) throws Exception { + return a; + } + })); + } + + @Test + public void seedBadSource() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + new Observable<Integer>() { + @Override + protected void subscribeActual(Observer<? super Integer> observer) { + observer.onSubscribe(Disposable.empty()); + observer.onComplete(); + observer.onNext(1); + observer.onError(new TestException()); + observer.onComplete(); + } + } + .reduce(0, new BiFunction<Integer, Integer, Integer>() { + @Override + public Integer apply(Integer a, Integer b) throws Exception { + return a; + } + }) + .test() + .assertResult(0); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableRefCountTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableRefCountTest.java new file mode 100644 index 0000000000..638f694a88 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableRefCountTest.java @@ -0,0 +1,1434 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import java.io.IOException; +import java.lang.management.ManagementFactory; +import java.util.*; +import java.util.concurrent.*; +import java.util.concurrent.atomic.*; + +import org.junit.Test; +import org.mockito.InOrder; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.core.Observer; +import io.reactivex.rxjava3.disposables.*; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.internal.operators.observable.ObservableRefCount.RefConnection; +import io.reactivex.rxjava3.internal.util.ExceptionHelper; +import io.reactivex.rxjava3.observables.ConnectableObservable; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.schedulers.*; +import io.reactivex.rxjava3.subjects.*; +import io.reactivex.rxjava3.testsupport.*; + +public class ObservableRefCountTest extends RxJavaTest { + + @Test + public void refCountAsync() throws InterruptedException { + // Flaky + for (int i = 0; i < 10; i++) { + try { + refCountAsyncActual(); + return; + } catch (AssertionError ex) { + if (i == 9) { + throw ex; + } + Thread.sleep((int)(200 * (Math.random() * 10 + 1))); + } + } + } + + /** + * Tries to coordinate async counting but it is flaky due to the low 10s of milliseconds. + */ + void refCountAsyncActual() { + final AtomicInteger subscribeCount = new AtomicInteger(); + final AtomicInteger nextCount = new AtomicInteger(); + Observable<Long> r = Observable.interval(0, 25, TimeUnit.MILLISECONDS) + .doOnSubscribe(new Consumer<Disposable>() { + @Override + public void accept(Disposable d) { + subscribeCount.incrementAndGet(); + } + }) + .doOnNext(new Consumer<Long>() { + @Override + public void accept(Long l) { + nextCount.incrementAndGet(); + } + }) + .publish().refCount(); + + final AtomicInteger receivedCount = new AtomicInteger(); + Disposable d1 = r.subscribe(new Consumer<Long>() { + @Override + public void accept(Long l) { + receivedCount.incrementAndGet(); + } + }); + + Disposable d2 = r.subscribe(); + + // give time to emit + try { + Thread.sleep(260); + } catch (InterruptedException e) { + } + + // now unsubscribe + d2.dispose(); // unsubscribe s2 first as we're counting in 1 and there can be a race between unsubscribe and one Observer getting a value but not the other + d1.dispose(); + + System.out.println("onNext: " + nextCount.get()); + + // should emit once for both subscribers + assertEquals(nextCount.get(), receivedCount.get()); + // only 1 subscribe + assertEquals(1, subscribeCount.get()); + } + + @Test + public void refCountSynchronous() { + final AtomicInteger subscribeCount = new AtomicInteger(); + final AtomicInteger nextCount = new AtomicInteger(); + Observable<Integer> r = Observable.just(1, 2, 3, 4, 5, 6, 7, 8, 9) + .doOnSubscribe(new Consumer<Disposable>() { + @Override + public void accept(Disposable d) { + subscribeCount.incrementAndGet(); + } + }) + .doOnNext(new Consumer<Integer>() { + @Override + public void accept(Integer l) { + nextCount.incrementAndGet(); + } + }) + .publish().refCount(); + + final AtomicInteger receivedCount = new AtomicInteger(); + Disposable d1 = r.subscribe(new Consumer<Integer>() { + @Override + public void accept(Integer l) { + receivedCount.incrementAndGet(); + } + }); + + Disposable d2 = r.subscribe(); + + // give time to emit + try { + Thread.sleep(50); + } catch (InterruptedException e) { + } + + // now unsubscribe + d2.dispose(); // unsubscribe s2 first as we're counting in 1 and there can be a race between unsubscribe and one Observer getting a value but not the other + d1.dispose(); + + System.out.println("onNext Count: " + nextCount.get()); + + // it will emit twice because it is synchronous + assertEquals(nextCount.get(), receivedCount.get() * 2); + // it will subscribe twice because it is synchronous + assertEquals(2, subscribeCount.get()); + } + + @Test + public void refCountSynchronousTake() { + final AtomicInteger nextCount = new AtomicInteger(); + Observable<Integer> r = Observable.just(1, 2, 3, 4, 5, 6, 7, 8, 9) + .doOnNext(new Consumer<Integer>() { + @Override + public void accept(Integer l) { + System.out.println("onNext --------> " + l); + nextCount.incrementAndGet(); + } + }) + .take(4) + .publish().refCount(); + + final AtomicInteger receivedCount = new AtomicInteger(); + r.subscribe(new Consumer<Integer>() { + @Override + public void accept(Integer l) { + receivedCount.incrementAndGet(); + } + }); + + System.out.println("onNext: " + nextCount.get()); + + assertEquals(4, receivedCount.get()); + assertEquals(4, receivedCount.get()); + } + + @Test + public void repeat() { + final AtomicInteger subscribeCount = new AtomicInteger(); + final AtomicInteger unsubscribeCount = new AtomicInteger(); + Observable<Long> r = Observable.interval(0, 1, TimeUnit.MILLISECONDS) + .doOnSubscribe(new Consumer<Disposable>() { + @Override + public void accept(Disposable d) { + System.out.println("******************************* Subscribe received"); + // when we are subscribed + subscribeCount.incrementAndGet(); + } + }) + .doOnDispose(new Action() { + @Override + public void run() { + System.out.println("******************************* Unsubscribe received"); + // when we are unsubscribed + unsubscribeCount.incrementAndGet(); + } + }) + .publish().refCount(); + + for (int i = 0; i < 10; i++) { + TestObserver<Long> to1 = new TestObserver<>(); + TestObserver<Long> to2 = new TestObserver<>(); + r.subscribe(to1); + r.subscribe(to2); + try { + Thread.sleep(50); + } catch (InterruptedException e) { + } + to1.dispose(); + to2.dispose(); + to1.assertNoErrors(); + to2.assertNoErrors(); + assertTrue(to1.values().size() > 0); + assertTrue(to2.values().size() > 0); + } + + assertEquals(10, subscribeCount.get()); + assertEquals(10, unsubscribeCount.get()); + } + + @Test + public void connectUnsubscribe() throws InterruptedException { + final CountDownLatch unsubscribeLatch = new CountDownLatch(1); + final CountDownLatch subscribeLatch = new CountDownLatch(1); + + Observable<Long> o = synchronousInterval() + .doOnSubscribe(new Consumer<Disposable>() { + @Override + public void accept(Disposable d) { + System.out.println("******************************* Subscribe received"); + // when we are subscribed + subscribeLatch.countDown(); + } + }) + .doOnDispose(new Action() { + @Override + public void run() { + System.out.println("******************************* Unsubscribe received"); + // when we are unsubscribed + unsubscribeLatch.countDown(); + } + }); + + TestObserverEx<Long> observer = new TestObserverEx<>(); + o.publish().refCount().subscribeOn(Schedulers.newThread()).subscribe(observer); + System.out.println("send unsubscribe"); + // wait until connected + subscribeLatch.await(); + // now unsubscribe + observer.dispose(); + System.out.println("DONE sending unsubscribe ... now waiting"); + if (!unsubscribeLatch.await(3000, TimeUnit.MILLISECONDS)) { + System.out.println("Errors: " + observer.errors()); + if (observer.errors().size() > 0) { + observer.errors().get(0).printStackTrace(); + } + fail("timed out waiting for unsubscribe"); + } + observer.assertNoErrors(); + } + + @Test + public void connectUnsubscribeRaceConditionLoop() throws InterruptedException { + for (int i = 0; i < 100; i++) { + connectUnsubscribeRaceCondition(); + } + } + + @Test + public void connectUnsubscribeRaceCondition() throws InterruptedException { + final AtomicInteger subUnsubCount = new AtomicInteger(); + Observable<Long> o = synchronousInterval() + .doOnDispose(new Action() { + @Override + public void run() { + System.out.println("******************************* Unsubscribe received"); + // when we are unsubscribed + subUnsubCount.decrementAndGet(); + } + }) + .doOnSubscribe(new Consumer<Disposable>() { + @Override + public void accept(Disposable d) { + System.out.println("******************************* SUBSCRIBE received"); + subUnsubCount.incrementAndGet(); + } + }); + + TestObserverEx<Long> observer = new TestObserverEx<>(); + + o.publish().refCount().subscribeOn(Schedulers.computation()).subscribe(observer); + System.out.println("send unsubscribe"); + // now immediately unsubscribe while subscribeOn is racing to subscribe + observer.dispose(); + + // this generally will mean it won't even subscribe as it is already unsubscribed by the time connect() gets scheduled + // give time to the counter to update + Thread.sleep(10); + + // make sure we wait a bit in case the counter is still nonzero + int counter = 200; + while (subUnsubCount.get() != 0 && counter-- != 0) { + Thread.sleep(10); + } + // either we subscribed and then unsubscribed, or we didn't ever even subscribe + assertEquals(0, subUnsubCount.get()); + + System.out.println("DONE sending unsubscribe ... now waiting"); + System.out.println("Errors: " + observer.errors()); + if (observer.errors().size() > 0) { + observer.errors().get(0).printStackTrace(); + } + observer.assertNoErrors(); + } + + private Observable<Long> synchronousInterval() { + return Observable.unsafeCreate(new ObservableSource<Long>() { + @Override + public void subscribe(Observer<? super Long> observer) { + final AtomicBoolean cancel = new AtomicBoolean(); + observer.onSubscribe(Disposable.fromRunnable(new Runnable() { + @Override + public void run() { + cancel.set(true); + } + })); + for (;;) { + if (cancel.get()) { + break; + } + try { + Thread.sleep(100); + } catch (InterruptedException e) { + } + observer.onNext(1L); + } + } + }); + } + + @Test + public void onlyFirstShouldSubscribeAndLastUnsubscribe() { + final AtomicInteger subscriptionCount = new AtomicInteger(); + final AtomicInteger unsubscriptionCount = new AtomicInteger(); + Observable<Integer> o = Observable.unsafeCreate(new ObservableSource<Integer>() { + @Override + public void subscribe(Observer<? super Integer> observer) { + subscriptionCount.incrementAndGet(); + observer.onSubscribe(Disposable.fromRunnable(new Runnable() { + @Override + public void run() { + unsubscriptionCount.incrementAndGet(); + } + })); + } + }); + Observable<Integer> refCounted = o.publish().refCount(); + + Disposable first = refCounted.subscribe(); + assertEquals(1, subscriptionCount.get()); + + Disposable second = refCounted.subscribe(); + assertEquals(1, subscriptionCount.get()); + + first.dispose(); + assertEquals(0, unsubscriptionCount.get()); + + second.dispose(); + assertEquals(1, unsubscriptionCount.get()); + } + + @Test + public void refCount() { + TestScheduler s = new TestScheduler(); + Observable<Long> interval = Observable.interval(100, TimeUnit.MILLISECONDS, s).publish().refCount(); + + // subscribe list1 + final List<Long> list1 = new ArrayList<>(); + Disposable d1 = interval.subscribe(new Consumer<Long>() { + @Override + public void accept(Long t1) { + list1.add(t1); + } + }); + + s.advanceTimeBy(200, TimeUnit.MILLISECONDS); + + assertEquals(2, list1.size()); + assertEquals(0L, list1.get(0).longValue()); + assertEquals(1L, list1.get(1).longValue()); + + // subscribe list2 + final List<Long> list2 = new ArrayList<>(); + Disposable d2 = interval.subscribe(new Consumer<Long>() { + @Override + public void accept(Long t1) { + list2.add(t1); + } + }); + + s.advanceTimeBy(300, TimeUnit.MILLISECONDS); + + // list 1 should have 5 items + assertEquals(5, list1.size()); + assertEquals(2L, list1.get(2).longValue()); + assertEquals(3L, list1.get(3).longValue()); + assertEquals(4L, list1.get(4).longValue()); + + // list 2 should only have 3 items + assertEquals(3, list2.size()); + assertEquals(2L, list2.get(0).longValue()); + assertEquals(3L, list2.get(1).longValue()); + assertEquals(4L, list2.get(2).longValue()); + + // unsubscribe list1 + d1.dispose(); + + // advance further + s.advanceTimeBy(300, TimeUnit.MILLISECONDS); + + // list 1 should still have 5 items + assertEquals(5, list1.size()); + + // list 2 should have 6 items + assertEquals(6, list2.size()); + assertEquals(5L, list2.get(3).longValue()); + assertEquals(6L, list2.get(4).longValue()); + assertEquals(7L, list2.get(5).longValue()); + + // unsubscribe list2 + d2.dispose(); + + // advance further + s.advanceTimeBy(1000, TimeUnit.MILLISECONDS); + + // subscribing a new one should start over because the source should have been unsubscribed + // subscribe list3 + final List<Long> list3 = new ArrayList<>(); + interval.subscribe(new Consumer<Long>() { + @Override + public void accept(Long t1) { + list3.add(t1); + } + }); + + s.advanceTimeBy(200, TimeUnit.MILLISECONDS); + + assertEquals(2, list3.size()); + assertEquals(0L, list3.get(0).longValue()); + assertEquals(1L, list3.get(1).longValue()); + + } + + @Test + public void alreadyUnsubscribedClient() { + Observer<Integer> done = DisposingObserver.INSTANCE; + + Observer<Integer> o = TestHelper.mockObserver(); + + Observable<Integer> result = Observable.just(1).publish().refCount(); + + result.subscribe(done); + + result.subscribe(o); + + verify(o).onNext(1); + verify(o).onComplete(); + verify(o, never()).onError(any(Throwable.class)); + } + + @Test + public void alreadyUnsubscribedInterleavesWithClient() { + ReplaySubject<Integer> source = ReplaySubject.create(); + + Observer<Integer> done = DisposingObserver.INSTANCE; + + Observer<Integer> o = TestHelper.mockObserver(); + InOrder inOrder = inOrder(o); + + Observable<Integer> result = source.publish().refCount(); + + result.subscribe(o); + + source.onNext(1); + + result.subscribe(done); + + source.onNext(2); + source.onComplete(); + + inOrder.verify(o).onNext(1); + inOrder.verify(o).onNext(2); + inOrder.verify(o).onComplete(); + verify(o, never()).onError(any(Throwable.class)); + } + + @Test + public void connectDisconnectConnectAndSubjectState() { + Observable<Integer> o1 = Observable.just(10); + Observable<Integer> o2 = Observable.just(20); + Observable<Integer> combined = Observable.combineLatest(o1, o2, new BiFunction<Integer, Integer, Integer>() { + @Override + public Integer apply(Integer t1, Integer t2) { + return t1 + t2; + } + }) + .publish().refCount(); + + TestObserverEx<Integer> to1 = new TestObserverEx<>(); + TestObserverEx<Integer> to2 = new TestObserverEx<>(); + + combined.subscribe(to1); + combined.subscribe(to2); + + to1.assertTerminated(); + to1.assertNoErrors(); + to1.assertValue(30); + + to2.assertTerminated(); + to2.assertNoErrors(); + to2.assertValue(30); + } + + @Test + public void upstreamErrorAllowsRetry() throws InterruptedException { + final AtomicInteger intervalSubscribed = new AtomicInteger(); + Observable<String> interval = + Observable.interval(200, TimeUnit.MILLISECONDS) + .doOnSubscribe(new Consumer<Disposable>() { + @Override + public void accept(Disposable d) { + System.out.println("Subscribing to interval " + intervalSubscribed.incrementAndGet()); + } + } + ) + .flatMap(new Function<Long, Observable<String>>() { + @Override + public Observable<String> apply(Long t1) { + return Observable.defer(new Supplier<Observable<String>>() { + @Override + public Observable<String> get() { + return Observable.<String>error(new Exception("Some exception")); + } + }); + } + }) + .onErrorResumeNext(new Function<Throwable, Observable<String>>() { + @Override + public Observable<String> apply(Throwable t1) { + return Observable.<String>error(t1); + } + }) + .publish() + .refCount(); + + interval + .doOnError(new Consumer<Throwable>() { + @Override + public void accept(Throwable t1) { + System.out.println("Observer 1 onError: " + t1); + } + }) + .retry(5) + .subscribe(new Consumer<String>() { + @Override + public void accept(String t1) { + System.out.println("Observer 1: " + t1); + } + }); + Thread.sleep(100); + interval + .doOnError(new Consumer<Throwable>() { + @Override + public void accept(Throwable t1) { + System.out.println("Observer 2 onError: " + t1); + } + }) + .retry(5) + .subscribe(new Consumer<String>() { + @Override + public void accept(String t1) { + System.out.println("Observer 2: " + t1); + } + }); + + Thread.sleep(1300); + + System.out.println(intervalSubscribed.get()); + assertEquals(6, intervalSubscribed.get()); + } + + private enum DisposingObserver implements Observer<Integer> { + INSTANCE; + + @Override + public void onSubscribe(Disposable d) { + d.dispose(); + } + + @Override + public void onNext(Integer t) { + } + + @Override + public void onError(Throwable t) { + } + + @Override + public void onComplete() { + } + } + + @Test + public void disposed() { + TestHelper.checkDisposed(Observable.just(1).publish().refCount()); + } + + @Test + public void noOpConnect() { + final int[] calls = { 0 }; + Observable<Integer> o = new ConnectableObservable<Integer>() { + @Override + public void connect(Consumer<? super Disposable> connection) { + calls[0]++; + } + + @Override + public void reset() { + // nothing to do in this test + } + + @Override + protected void subscribeActual(Observer<? super Integer> observer) { + observer.onSubscribe(Disposable.disposed()); + } + }.refCount(); + + o.test(); + o.test(); + + assertEquals(1, calls[0]); + } + Observable<Object> source; + + @Test + public void replayNoLeak() throws Exception { + System.gc(); + Thread.sleep(100); + + long start = ManagementFactory.getMemoryMXBean().getHeapMemoryUsage().getUsed(); + + source = Observable.fromCallable(new Callable<Object>() { + @Override + public Object call() throws Exception { + return new byte[100 * 1000 * 1000]; + } + }) + .replay(1) + .refCount(); + + source.subscribe(); + + long after = TestHelper.awaitGC(GC_SLEEP_TIME, 20, start + 20 * 1000 * 1000); + + source = null; + assertTrue(String.format("%,3d -> %,3d%n", start, after), start + 20 * 1000 * 1000 > after); + } + + @Test + public void replayNoLeak2() throws Exception { + System.gc(); + Thread.sleep(100); + + long start = ManagementFactory.getMemoryMXBean().getHeapMemoryUsage().getUsed(); + + source = Observable.fromCallable(new Callable<Object>() { + @Override + public Object call() throws Exception { + return new byte[100 * 1000 * 1000]; + } + }).concatWith(Observable.never()) + .replay(1) + .refCount(); + + Disposable d1 = source.subscribe(); + Disposable d2 = source.subscribe(); + + d1.dispose(); + d2.dispose(); + + d1 = null; + d2 = null; + + long after = TestHelper.awaitGC(GC_SLEEP_TIME, 20, start + 20 * 1000 * 1000); + + source = null; + assertTrue(String.format("%,3d -> %,3d%n", start, after), start + 20 * 1000 * 1000 > after); + } + + static final class ExceptionData extends Exception { + private static final long serialVersionUID = -6763898015338136119L; + + public final Object data; + + ExceptionData(Object data) { + this.data = data; + } + } + + static final int GC_SLEEP_TIME = 250; + + @Test + public void publishNoLeak() throws Exception { + System.gc(); + Thread.sleep(GC_SLEEP_TIME); + + source = Observable.fromCallable(new Callable<Object>() { + @Override + public Object call() throws Exception { + throw new ExceptionData(new byte[100 * 1000 * 1000]); + } + }) + .publish() + .refCount(); + + long start = ManagementFactory.getMemoryMXBean().getHeapMemoryUsage().getUsed(); + + source.subscribe(Functions.emptyConsumer(), Functions.emptyConsumer()); + + long after = TestHelper.awaitGC(GC_SLEEP_TIME, 20, start + 20 * 1000 * 1000); + + source = null; + assertTrue(String.format("%,3d -> %,3d%n", start, after), start + 20 * 1000 * 1000 > after); + } + + @Test + public void publishNoLeak2() throws Exception { + System.gc(); + Thread.sleep(GC_SLEEP_TIME); + + long start = ManagementFactory.getMemoryMXBean().getHeapMemoryUsage().getUsed(); + + source = Observable.fromCallable(new Callable<Object>() { + @Override + public Object call() throws Exception { + return new byte[100 * 1000 * 1000]; + } + }).concatWith(Observable.never()) + .publish() + .refCount(); + + Disposable d1 = source.test(); + Disposable d2 = source.test(); + + d1.dispose(); + d2.dispose(); + + d1 = null; + d2 = null; + + long after = TestHelper.awaitGC(GC_SLEEP_TIME, 20, start + 20 * 1000 * 1000); + + source = null; + assertTrue(String.format("%,3d -> %,3d%n", start, after), start + 20 * 1000 * 1000 > after); + } + + @Test + public void replayIsUnsubscribed() { + ConnectableObservable<Integer> co = Observable.just(1).concatWith(Observable.<Integer>never()) + .replay(); + + if (co instanceof Disposable) { + assertTrue(((Disposable)co).isDisposed()); + + Disposable connection = co.connect(); + + assertFalse(((Disposable)co).isDisposed()); + + connection.dispose(); + + assertTrue(((Disposable)co).isDisposed()); + } + } + + static final class BadObservableSubscribe extends ConnectableObservable<Object> { + + @Override + public void connect(Consumer<? super Disposable> connection) { + try { + connection.accept(Disposable.empty()); + } catch (Throwable ex) { + throw ExceptionHelper.wrapOrThrow(ex); + } + } + + @Override + public void reset() { + // nothing to do in this test + } + + @Override + protected void subscribeActual(Observer<? super Object> observer) { + throw new TestException("subscribeActual"); + } + } + + static final class BadObservableDispose extends ConnectableObservable<Object> { + + @Override + public void reset() { + throw new TestException("dispose"); + } + + @Override + public void connect(Consumer<? super Disposable> connection) { + try { + connection.accept(Disposable.empty()); + } catch (Throwable ex) { + throw ExceptionHelper.wrapOrThrow(ex); + } + } + + @Override + protected void subscribeActual(Observer<? super Object> observer) { + observer.onSubscribe(Disposable.empty()); + } + } + + static final class BadObservableConnect extends ConnectableObservable<Object> { + + @Override + public void connect(Consumer<? super Disposable> connection) { + throw new TestException("connect"); + } + + @Override + public void reset() { + // nothing to do in this test + } + + @Override + protected void subscribeActual(Observer<? super Object> observer) { + observer.onSubscribe(Disposable.empty()); + } + } + + @Test + @SuppressUndeliverable + public void badSourceSubscribe() { + BadObservableSubscribe bo = new BadObservableSubscribe(); + + try { + bo.refCount() + .test(); + fail("Should have thrown"); + } catch (NullPointerException ex) { + assertTrue(ex.getCause() instanceof TestException); + } + } + + @Test + public void badSourceDispose() { + BadObservableDispose bo = new BadObservableDispose(); + + try { + bo.refCount() + .test() + .dispose(); + fail("Should have thrown"); + } catch (TestException expected) { + } + } + + @Test + @SuppressUndeliverable + public void badSourceConnect() { + BadObservableConnect bo = new BadObservableConnect(); + + try { + bo.refCount() + .test(); + fail("Should have thrown"); + } catch (NullPointerException ex) { + assertTrue(ex.getCause() instanceof TestException); + } + } + + static final class BadObservableSubscribe2 extends ConnectableObservable<Object> { + + int count; + + @Override + public void connect(Consumer<? super Disposable> connection) { + try { + connection.accept(Disposable.empty()); + } catch (Throwable ex) { + throw ExceptionHelper.wrapOrThrow(ex); + } + } + + @Override + public void reset() { + // nothing to do in this test + } + + @Override + protected void subscribeActual(Observer<? super Object> observer) { + if (++count == 1) { + observer.onSubscribe(Disposable.empty()); + } else { + throw new TestException("subscribeActual"); + } + } + } + + @Test + @SuppressUndeliverable + public void badSourceSubscribe2() { + BadObservableSubscribe2 bo = new BadObservableSubscribe2(); + + Observable<Object> o = bo.refCount(); + o.test(); + try { + o.test(); + fail("Should have thrown"); + } catch (NullPointerException ex) { + assertTrue(ex.getCause() instanceof TestException); + } + } + + static final class BadObservableConnect2 extends ConnectableObservable<Object> { + + @Override + public void connect(Consumer<? super Disposable> connection) { + try { + connection.accept(Disposable.empty()); + } catch (Throwable ex) { + throw ExceptionHelper.wrapOrThrow(ex); + } + } + + @Override + public void reset() { + throw new TestException("dispose"); + } + + @Override + protected void subscribeActual(Observer<? super Object> observer) { + observer.onSubscribe(Disposable.empty()); + observer.onComplete(); + } + } + + @Test + @SuppressUndeliverable + public void badSourceCompleteDisconnect() { + BadObservableConnect2 bo = new BadObservableConnect2(); + + try { + bo.refCount() + .test(); + fail("Should have thrown"); + } catch (NullPointerException ex) { + assertTrue(ex.getCause() instanceof TestException); + } + } + + @Test + public void blockingSourceAsnycCancel() throws Exception { + BehaviorSubject<Integer> bs = BehaviorSubject.createDefault(1); + + Observable<Integer> o = bs + .replay(1) + .refCount(); + + o.subscribe(); + + final AtomicBoolean interrupted = new AtomicBoolean(); + + o.switchMap(new Function<Integer, ObservableSource<? extends Object>>() { + @Override + public ObservableSource<? extends Object> apply(Integer v) throws Exception { + return Observable.create(new ObservableOnSubscribe<Object>() { + @Override + public void subscribe(ObservableEmitter<Object> emitter) throws Exception { + while (!emitter.isDisposed()) { + Thread.sleep(100); + } + interrupted.set(true); + } + }); + } + }) + .take(500, TimeUnit.MILLISECONDS) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(); + + assertTrue(interrupted.get()); + } + + @Test + public void byCount() { + final int[] subscriptions = { 0 }; + + Observable<Integer> source = Observable.range(1, 5) + .doOnSubscribe(new Consumer<Disposable>() { + @Override + public void accept(Disposable d) throws Exception { + subscriptions[0]++; + } + }) + .publish() + .refCount(2); + + for (int i = 0; i < 3; i++) { + TestObserver<Integer> to1 = source.test(); + + to1.withTag("to1 " + i); + to1.assertEmpty(); + + TestObserver<Integer> to2 = source.test(); + + to2.withTag("to2 " + i); + + to1.assertResult(1, 2, 3, 4, 5); + to2.assertResult(1, 2, 3, 4, 5); + } + + assertEquals(3, subscriptions[0]); + } + + @Test + public void resubscribeBeforeTimeout() throws Exception { + final int[] subscriptions = { 0 }; + + PublishSubject<Integer> ps = PublishSubject.create(); + + Observable<Integer> source = ps + .doOnSubscribe(new Consumer<Disposable>() { + @Override + public void accept(Disposable d) throws Exception { + subscriptions[0]++; + } + }) + .publish() + .refCount(500, TimeUnit.MILLISECONDS); + + TestObserver<Integer> to1 = source.test(); + + assertEquals(1, subscriptions[0]); + + to1.dispose(); + + Thread.sleep(100); + + to1 = source.test(); + + assertEquals(1, subscriptions[0]); + + Thread.sleep(500); + + assertEquals(1, subscriptions[0]); + + ps.onNext(1); + ps.onNext(2); + ps.onNext(3); + ps.onNext(4); + ps.onNext(5); + ps.onComplete(); + + to1 + .assertResult(1, 2, 3, 4, 5); + } + + @Test + public void letitTimeout() throws Exception { + final int[] subscriptions = { 0 }; + + PublishSubject<Integer> ps = PublishSubject.create(); + + Observable<Integer> source = ps + .doOnSubscribe(new Consumer<Disposable>() { + @Override + public void accept(Disposable d) throws Exception { + subscriptions[0]++; + } + }) + .publish() + .refCount(1, 100, TimeUnit.MILLISECONDS); + + TestObserver<Integer> to1 = source.test(); + + assertEquals(1, subscriptions[0]); + + to1.dispose(); + + assertTrue(ps.hasObservers()); + + Thread.sleep(200); + + assertFalse(ps.hasObservers()); + } + + @Test + public void error() { + Observable.<Integer>error(new IOException()) + .publish() + .refCount(500, TimeUnit.MILLISECONDS) + .test() + .assertFailure(IOException.class); + } + + @Test + public void comeAndGo() { + PublishSubject<Integer> ps = PublishSubject.create(); + + Observable<Integer> source = ps + .publish() + .refCount(1); + + TestObserver<Integer> to1 = source.test(); + + assertTrue(ps.hasObservers()); + + for (int i = 0; i < 3; i++) { + TestObserver<Integer> to2 = source.test(); + to1.dispose(); + to1 = to2; + } + + to1.dispose(); + + assertFalse(ps.hasObservers()); + } + + @Test + public void unsubscribeSubscribeRace() { + for (int i = 0; i < 1000; i++) { + + final Observable<Integer> source = Observable.range(1, 5) + .replay() + .refCount(1) + ; + + final TestObserver<Integer> to1 = source.test(); + + final TestObserver<Integer> to2 = new TestObserver<>(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + to1.dispose(); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + source.subscribe(to2); + } + }; + + TestHelper.race(r1, r2, Schedulers.single()); + + to2 + .withTag("Round: " + i) + .assertResult(1, 2, 3, 4, 5); + } + } + + static final class BadObservableDoubleOnX extends ConnectableObservable<Object> + implements Disposable { + + @Override + public void connect(Consumer<? super Disposable> connection) { + try { + connection.accept(Disposable.empty()); + } catch (Throwable ex) { + throw ExceptionHelper.wrapOrThrow(ex); + } + } + + @Override + public void reset() { + // nothing to do in this test + } + + @Override + protected void subscribeActual(Observer<? super Object> observer) { + observer.onSubscribe(Disposable.empty()); + observer.onSubscribe(Disposable.empty()); + observer.onComplete(); + observer.onComplete(); + observer.onError(new TestException()); + } + + @Override + public void dispose() { + } + + @Override + public boolean isDisposed() { + return false; + } + } + + @Test + public void doubleOnX() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + new BadObservableDoubleOnX() + .refCount() + .test() + .assertResult(); + + TestHelper.assertError(errors, 0, ProtocolViolationException.class); + TestHelper.assertUndeliverable(errors, 1, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void doubleOnXCount() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + new BadObservableDoubleOnX() + .refCount(1) + .test() + .assertResult(); + + TestHelper.assertError(errors, 0, ProtocolViolationException.class); + TestHelper.assertUndeliverable(errors, 1, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void doubleOnXTime() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + new BadObservableDoubleOnX() + .refCount(5, TimeUnit.SECONDS, Schedulers.single()) + .test() + .assertResult(); + + TestHelper.assertError(errors, 0, ProtocolViolationException.class); + TestHelper.assertUndeliverable(errors, 1, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void cancelTerminateStateExclusion() { + ObservableRefCount<Object> o = (ObservableRefCount<Object>)PublishSubject.create() + .publish() + .refCount(); + + o.cancel(new RefConnection(o)); + + RefConnection rc = new RefConnection(o); + o.connection = null; + rc.subscriberCount = 0; + o.timeout(rc); + + rc.subscriberCount = 1; + o.timeout(rc); + + o.connection = rc; + o.timeout(rc); + + rc.subscriberCount = 0; + o.timeout(rc); + + // ------------------- + + rc.subscriberCount = 2; + rc.connected = false; + o.connection = rc; + o.cancel(rc); + + rc.subscriberCount = 1; + rc.connected = false; + o.connection = rc; + o.cancel(rc); + + rc.subscriberCount = 2; + rc.connected = true; + o.connection = rc; + o.cancel(rc); + + rc.subscriberCount = 1; + rc.connected = true; + o.connection = rc; + rc.lazySet(null); + o.cancel(rc); + + o.connection = rc; + o.cancel(new RefConnection(o)); + } + + @Test + public void replayRefCountShallBeThreadSafe() { + for (int i = 0; i < TestHelper.RACE_LONG_LOOPS; i++) { + Observable<Integer> observable = Observable.just(1).replay(1).refCount(); + + TestObserver<Integer> observer1 = observable + .subscribeOn(Schedulers.io()) + .test(); + + TestObserver<Integer> observer2 = observable + .subscribeOn(Schedulers.io()) + .test(); + + observer1 + .withTag("" + i) + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(1); + + observer2 + .withTag("" + i) + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(1); + } + } + + static final class TestConnectableObservable<T> extends ConnectableObservable<T> { + + volatile boolean reset; + + @Override + public void reset() { + reset = true; + } + + @Override + public void connect(Consumer<? super Disposable> connection) { + // not relevant + } + + @Override + protected void subscribeActual(Observer<? super T> observer) { + // not relevant + } + } + + @Test + public void timeoutResetsSource() { + TestConnectableObservable<Object> tco = new TestConnectableObservable<>(); + ObservableRefCount<Object> o = (ObservableRefCount<Object>)tco.refCount(); + + RefConnection rc = new RefConnection(o); + rc.set(Disposable.empty()); + o.connection = rc; + + o.timeout(rc); + + assertTrue(tco.reset); + } + + @Test + public void disconnectBeforeConnect() { + BehaviorSubject<Integer> subject = BehaviorSubject.create(); + + Observable<Integer> observable = subject + .replay(1) + .refCount(); + + observable.takeUntil(Observable.just(1)).test(); + + subject.onNext(2); + + observable.take(1).test().assertResult(2); + } + + @Test + public void publishRefCountShallBeThreadSafe() { + for (int i = 0; i < TestHelper.RACE_LONG_LOOPS; i++) { + Observable<Integer> observable = Observable.just(1).publish().refCount(); + + TestObserver<Integer> observer1 = observable + .subscribeOn(Schedulers.io()) + .test(); + + TestObserver<Integer> observer2 = observable + .subscribeOn(Schedulers.io()) + .test(); + + observer1 + .withTag("observer1 " + i) + .awaitDone(5, TimeUnit.SECONDS) + .assertNoErrors() + .assertComplete(); + + observer2 + .withTag("observer2 " + i) + .awaitDone(5, TimeUnit.SECONDS) + .assertNoErrors() + .assertComplete(); + } + } + + @Test + public void upstreamTerminationTriggersAnotherCancel() throws Exception { + ReplaySubject<Integer> rs = ReplaySubject.create(); + rs.onNext(1); + rs.onComplete(); + + Observable<Integer> shared = rs.share(); + + shared + .buffer(shared.debounce(5, TimeUnit.SECONDS)) + .test() + .assertValueCount(2); + + shared + .buffer(shared.debounce(5, TimeUnit.SECONDS)) + .test() + .assertValueCount(2); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableRepeatTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableRepeatTest.java new file mode 100644 index 0000000000..8f3287e7e0 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableRepeatTest.java @@ -0,0 +1,454 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import java.util.*; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.core.Observer; +import io.reactivex.rxjava3.disposables.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.schedulers.Schedulers; +import io.reactivex.rxjava3.subjects.PublishSubject; +import io.reactivex.rxjava3.testsupport.*; + +public class ObservableRepeatTest extends RxJavaTest { + + @Test + public void repetition() { + int num = 10; + final AtomicInteger count = new AtomicInteger(); + int value = Observable.unsafeCreate(new ObservableSource<Integer>() { + + @Override + public void subscribe(final Observer<? super Integer> o) { + o.onNext(count.incrementAndGet()); + o.onComplete(); + } + }).repeat().subscribeOn(Schedulers.computation()) + .take(num).blockingLast(); + + assertEquals(num, value); + } + + @Test + public void repeatTake() { + Observable<Integer> xs = Observable.just(1, 2); + Object[] ys = xs.repeat().subscribeOn(Schedulers.newThread()).take(4).toList().blockingGet().toArray(); + assertArrayEquals(new Object[] { 1, 2, 1, 2 }, ys); + } + + @Test + public void noStackOverFlow() { + Observable.just(1).repeat().subscribeOn(Schedulers.newThread()).take(100000).blockingLast(); + } + + @Test + public void repeatTakeWithSubscribeOn() throws InterruptedException { + + final AtomicInteger counter = new AtomicInteger(); + Observable<Integer> oi = Observable.unsafeCreate(new ObservableSource<Integer>() { + + @Override + public void subscribe(Observer<? super Integer> sub) { + sub.onSubscribe(Disposable.empty()); + counter.incrementAndGet(); + sub.onNext(1); + sub.onNext(2); + sub.onComplete(); + } + }).subscribeOn(Schedulers.newThread()); + + Object[] ys = oi.repeat().subscribeOn(Schedulers.newThread()).map(new Function<Integer, Integer>() { + + @Override + public Integer apply(Integer t1) { + try { + Thread.sleep(50); + } catch (InterruptedException e) { + e.printStackTrace(); + } + return t1; + } + + }).take(4).toList().blockingGet().toArray(); + + assertEquals(2, counter.get()); + assertArrayEquals(new Object[] { 1, 2, 1, 2 }, ys); + } + + @Test + public void repeatAndTake() { + Observer<Object> o = TestHelper.mockObserver(); + + Observable.just(1).repeat().take(10).subscribe(o); + + verify(o, times(10)).onNext(1); + verify(o).onComplete(); + verify(o, never()).onError(any(Throwable.class)); + } + + @Test + public void repeatLimited() { + Observer<Object> o = TestHelper.mockObserver(); + + Observable.just(1).repeat(10).subscribe(o); + + verify(o, times(10)).onNext(1); + verify(o).onComplete(); + verify(o, never()).onError(any(Throwable.class)); + } + + @Test + public void repeatError() { + Observer<Object> o = TestHelper.mockObserver(); + + Observable.error(new TestException()).repeat(10).subscribe(o); + + verify(o).onError(any(TestException.class)); + verify(o, never()).onNext(any()); + verify(o, never()).onComplete(); + + } + + @Test + public void repeatZero() { + Observer<Object> o = TestHelper.mockObserver(); + + Observable.just(1).repeat(0).subscribe(o); + + verify(o).onComplete(); + verify(o, never()).onNext(any()); + verify(o, never()).onError(any(Throwable.class)); + } + + @Test + public void repeatOne() { + Observer<Object> o = TestHelper.mockObserver(); + + Observable.just(1).repeat(1).subscribe(o); + + verify(o).onComplete(); + verify(o, times(1)).onNext(any()); + verify(o, never()).onError(any(Throwable.class)); + } + + /** Issue #2587. */ + @Test + public void repeatAndDistinctUnbounded() { + Observable<Integer> src = Observable.fromIterable(Arrays.asList(1, 2, 3, 4, 5)) + .take(3) + .repeat(3) + .distinct(); + + TestObserverEx<Integer> to = new TestObserverEx<>(); + + src.subscribe(to); + + to.assertNoErrors(); + to.assertTerminated(); + to.assertValues(1, 2, 3); + } + + /** Issue #2844: wrong target of request. */ + @Test + public void repeatRetarget() { + final List<Integer> concatBase = new ArrayList<>(); + TestObserver<Integer> to = new TestObserver<>(); + Observable.just(1, 2) + .repeat(5) + .concatMap(new Function<Integer, Observable<Integer>>() { + @Override + public Observable<Integer> apply(Integer x) { + System.out.println("testRepeatRetarget -> " + x); + concatBase.add(x); + return Observable.<Integer>empty() + .delay(200, TimeUnit.MILLISECONDS); + } + }) + .subscribe(to); + + to.awaitDone(5, TimeUnit.SECONDS); + to.assertNoErrors(); + to.assertNoValues(); + + assertEquals(Arrays.asList(1, 2, 1, 2, 1, 2, 1, 2, 1, 2), concatBase); + } + + @Test + public void repeatUntil() { + Observable.just(1) + .repeatUntil(new BooleanSupplier() { + @Override + public boolean getAsBoolean() throws Exception { + return false; + } + }) + .take(5) + .test() + .assertResult(1, 1, 1, 1, 1); + } + + @Test + public void repeatLongPredicateInvalid() { + try { + Observable.just(1).repeat(-99); + fail("Should have thrown"); + } catch (IllegalArgumentException ex) { + assertEquals("times >= 0 required but it was -99", ex.getMessage()); + } + } + + @Test + public void repeatUntilError() { + Observable.error(new TestException()) + .repeatUntil(new BooleanSupplier() { + @Override + public boolean getAsBoolean() throws Exception { + return true; + } + }) + .test() + .assertFailure(TestException.class); + } + + @Test + public void repeatUntilFalse() { + Observable.just(1) + .repeatUntil(new BooleanSupplier() { + @Override + public boolean getAsBoolean() throws Exception { + return true; + } + }) + .test() + .assertResult(1); + } + + @Test + public void repeatUntilSupplierCrash() { + Observable.just(1) + .repeatUntil(new BooleanSupplier() { + @Override + public boolean getAsBoolean() throws Exception { + throw new TestException(); + } + }) + .test() + .assertFailure(TestException.class, 1); + } + + @Test + public void shouldDisposeInnerObservable() { + final PublishSubject<Object> subject = PublishSubject.create(); + final Disposable disposable = Observable.just("Leak") + .repeatWhen(new Function<Observable<Object>, ObservableSource<Object>>() { + @Override + public ObservableSource<Object> apply(Observable<Object> completions) throws Exception { + return completions.switchMap(new Function<Object, ObservableSource<Object>>() { + @Override + public ObservableSource<Object> apply(Object ignore) throws Exception { + return subject; + } + }); + } + }) + .subscribe(); + + assertTrue(subject.hasObservers()); + disposable.dispose(); + assertFalse(subject.hasObservers()); + } + + @Test + public void repeatWhen() { + Observable.error(new TestException()) + .repeatWhen(new Function<Observable<Object>, ObservableSource<Object>>() { + @Override + public ObservableSource<Object> apply(Observable<Object> v) throws Exception { + return v.delay(10, TimeUnit.SECONDS); + } + }) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertFailure(TestException.class); + } + + @Test + public void whenTake() { + Observable.range(1, 3).repeatWhen(new Function<Observable<Object>, ObservableSource<Object>>() { + @Override + public ObservableSource<Object> apply(Observable<Object> handler) throws Exception { + return handler.take(2); + } + }) + .test() + .assertResult(1, 2, 3, 1, 2, 3); + } + + @Test + public void handlerError() { + Observable.range(1, 3) + .repeatWhen(new Function<Observable<Object>, ObservableSource<Object>>() { + @Override + public ObservableSource<Object> apply(Observable<Object> v) throws Exception { + return v.map(new Function<Object, Object>() { + @Override + public Object apply(Object w) throws Exception { + throw new TestException(); + } + }); + } + }) + .test() + .assertFailure(TestException.class, 1, 2, 3); + } + + @Test + public void noCancelPreviousRepeat() { + final AtomicInteger counter = new AtomicInteger(); + + Observable<Integer> source = Observable.just(1).doOnDispose(new Action() { + @Override + public void run() throws Exception { + counter.getAndIncrement(); + } + }); + + source.repeat(5) + .test() + .assertResult(1, 1, 1, 1, 1); + + assertEquals(0, counter.get()); + } + + @Test + public void noCancelPreviousRepeatUntil() { + final AtomicInteger counter = new AtomicInteger(); + + Observable<Integer> source = Observable.just(1).doOnDispose(new Action() { + @Override + public void run() throws Exception { + counter.getAndIncrement(); + } + }); + + final AtomicInteger times = new AtomicInteger(); + + source.repeatUntil(new BooleanSupplier() { + @Override + public boolean getAsBoolean() throws Exception { + return times.getAndIncrement() == 4; + } + }) + .test() + .assertResult(1, 1, 1, 1, 1); + + assertEquals(0, counter.get()); + } + + @Test + public void noCancelPreviousRepeatWhen() { + final AtomicInteger counter = new AtomicInteger(); + + Observable<Integer> source = Observable.just(1).doOnDispose(new Action() { + @Override + public void run() throws Exception { + counter.getAndIncrement(); + } + }); + + final AtomicInteger times = new AtomicInteger(); + + source.repeatWhen(new Function<Observable<Object>, ObservableSource<?>>() { + @Override + public ObservableSource<?> apply(Observable<Object> e) throws Exception { + return e.takeWhile(new Predicate<Object>() { + @Override + public boolean test(Object v) throws Exception { + return times.getAndIncrement() < 4; + } + }); + } + }) + .test() + .assertResult(1, 1, 1, 1, 1); + + assertEquals(0, counter.get()); + } + + @Test + public void repeatFloodNoSubscriptionError() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + + try { + final PublishSubject<Integer> source = PublishSubject.create(); + final PublishSubject<Integer> signaller = PublishSubject.create(); + + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + + TestObserver<Integer> to = source.take(1) + .repeatWhen(new Function<Observable<Object>, ObservableSource<Integer>>() { + @Override + public ObservableSource<Integer> apply(Observable<Object> v) + throws Exception { + return signaller; + } + }).test(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + source.onNext(1); + } + } + }; + Runnable r2 = new Runnable() { + @Override + public void run() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + signaller.onNext(1); + } + } + }; + + TestHelper.race(r1, r2); + + to.dispose(); + } + + if (!errors.isEmpty()) { + for (Throwable e : errors) { + e.printStackTrace(); + } + fail(errors + ""); + } + } finally { + RxJavaPlugins.reset(); + } + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableReplayEagerTruncateTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableReplayEagerTruncateTest.java new file mode 100644 index 0000000000..aa1bcb4359 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableReplayEagerTruncateTest.java @@ -0,0 +1,2060 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +import java.lang.management.*; +import java.util.*; +import java.util.concurrent.*; +import java.util.concurrent.atomic.*; + +import org.junit.*; +import org.mockito.InOrder; + +import io.reactivex.rxjava3.annotations.NonNull; +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.core.Observer; +import io.reactivex.rxjava3.core.Scheduler.Worker; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.internal.fuseable.HasUpstreamObservableSource; +import io.reactivex.rxjava3.internal.operators.observable.ObservableReplay.*; +import io.reactivex.rxjava3.observables.ConnectableObservable; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.schedulers.*; +import io.reactivex.rxjava3.subjects.PublishSubject; +import io.reactivex.rxjava3.testsupport.*; + +public class ObservableReplayEagerTruncateTest extends RxJavaTest { + @Test + public void bufferedReplay() { + PublishSubject<Integer> source = PublishSubject.create(); + + ConnectableObservable<Integer> co = source.replay(3, true); + co.connect(); + + { + Observer<Object> observer1 = TestHelper.mockObserver(); + InOrder inOrder = inOrder(observer1); + + co.subscribe(observer1); + + source.onNext(1); + source.onNext(2); + source.onNext(3); + + inOrder.verify(observer1, times(1)).onNext(1); + inOrder.verify(observer1, times(1)).onNext(2); + inOrder.verify(observer1, times(1)).onNext(3); + + source.onNext(4); + source.onComplete(); + inOrder.verify(observer1, times(1)).onNext(4); + inOrder.verify(observer1, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + verify(observer1, never()).onError(any(Throwable.class)); + + } + + { + Observer<Object> observer1 = TestHelper.mockObserver(); + InOrder inOrder = inOrder(observer1); + + co.subscribe(observer1); + + inOrder.verify(observer1, times(1)).onNext(2); + inOrder.verify(observer1, times(1)).onNext(3); + inOrder.verify(observer1, times(1)).onNext(4); + inOrder.verify(observer1, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + verify(observer1, never()).onError(any(Throwable.class)); + } + } + + @Test + public void bufferedWindowReplay() { + PublishSubject<Integer> source = PublishSubject.create(); + TestScheduler scheduler = new TestScheduler(); + ConnectableObservable<Integer> co = source.replay(3, 100, TimeUnit.MILLISECONDS, scheduler, true); + co.connect(); + + { + Observer<Object> observer1 = TestHelper.mockObserver(); + InOrder inOrder = inOrder(observer1); + + co.subscribe(observer1); + + source.onNext(1); + scheduler.advanceTimeBy(10, TimeUnit.MILLISECONDS); + source.onNext(2); + scheduler.advanceTimeBy(10, TimeUnit.MILLISECONDS); + source.onNext(3); + scheduler.advanceTimeBy(10, TimeUnit.MILLISECONDS); + + inOrder.verify(observer1, times(1)).onNext(1); + inOrder.verify(observer1, times(1)).onNext(2); + inOrder.verify(observer1, times(1)).onNext(3); + + source.onNext(4); + source.onNext(5); + scheduler.advanceTimeBy(90, TimeUnit.MILLISECONDS); + + inOrder.verify(observer1, times(1)).onNext(4); + + inOrder.verify(observer1, times(1)).onNext(5); + + inOrder.verifyNoMoreInteractions(); + verify(observer1, never()).onError(any(Throwable.class)); + + } + + { + Observer<Object> observer1 = TestHelper.mockObserver(); + InOrder inOrder = inOrder(observer1); + + co.subscribe(observer1); + + inOrder.verify(observer1, times(1)).onNext(4); + inOrder.verify(observer1, times(1)).onNext(5); + inOrder.verifyNoMoreInteractions(); + verify(observer1, never()).onError(any(Throwable.class)); + } + } + + @Test + public void windowedReplay() { + TestScheduler scheduler = new TestScheduler(); + + PublishSubject<Integer> source = PublishSubject.create(); + + ConnectableObservable<Integer> co = source.replay(100, TimeUnit.MILLISECONDS, scheduler, true); + co.connect(); + + { + Observer<Object> observer1 = TestHelper.mockObserver(); + InOrder inOrder = inOrder(observer1); + + co.subscribe(observer1); + + source.onNext(1); + scheduler.advanceTimeBy(60, TimeUnit.MILLISECONDS); + source.onNext(2); + scheduler.advanceTimeBy(60, TimeUnit.MILLISECONDS); + source.onNext(3); + scheduler.advanceTimeBy(60, TimeUnit.MILLISECONDS); + source.onComplete(); + scheduler.advanceTimeBy(60, TimeUnit.MILLISECONDS); + + inOrder.verify(observer1, times(1)).onNext(1); + inOrder.verify(observer1, times(1)).onNext(2); + inOrder.verify(observer1, times(1)).onNext(3); + + inOrder.verify(observer1, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + verify(observer1, never()).onError(any(Throwable.class)); + + } + { + Observer<Object> observer1 = TestHelper.mockObserver(); + InOrder inOrder = inOrder(observer1); + + co.subscribe(observer1); + inOrder.verify(observer1, never()).onNext(3); + + inOrder.verify(observer1, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + verify(observer1, never()).onError(any(Throwable.class)); + } + } + + @Test + public void replaySelector() { + final Function<Integer, Integer> dbl = new Function<Integer, Integer>() { + + @Override + public Integer apply(Integer t1) { + return t1 * 2; + } + + }; + + Function<Observable<Integer>, Observable<Integer>> selector = new Function<Observable<Integer>, Observable<Integer>>() { + + @Override + public Observable<Integer> apply(Observable<Integer> t1) { + return t1.map(dbl); + } + + }; + + PublishSubject<Integer> source = PublishSubject.create(); + + Observable<Integer> co = source.replay(selector); + + { + Observer<Object> observer1 = TestHelper.mockObserver(); + InOrder inOrder = inOrder(observer1); + + co.subscribe(observer1); + + source.onNext(1); + source.onNext(2); + source.onNext(3); + + inOrder.verify(observer1, times(1)).onNext(2); + inOrder.verify(observer1, times(1)).onNext(4); + inOrder.verify(observer1, times(1)).onNext(6); + + source.onNext(4); + source.onComplete(); + inOrder.verify(observer1, times(1)).onNext(8); + inOrder.verify(observer1, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + verify(observer1, never()).onError(any(Throwable.class)); + + } + + { + Observer<Object> observer1 = TestHelper.mockObserver(); + InOrder inOrder = inOrder(observer1); + + co.subscribe(observer1); + + inOrder.verify(observer1, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + verify(observer1, never()).onError(any(Throwable.class)); + + } + + } + + @Test + public void bufferedReplaySelector() { + + final Function<Integer, Integer> dbl = new Function<Integer, Integer>() { + + @Override + public Integer apply(Integer t1) { + return t1 * 2; + } + + }; + + Function<Observable<Integer>, Observable<Integer>> selector = new Function<Observable<Integer>, Observable<Integer>>() { + + @Override + public Observable<Integer> apply(Observable<Integer> t1) { + return t1.map(dbl); + } + + }; + + PublishSubject<Integer> source = PublishSubject.create(); + + Observable<Integer> co = source.replay(selector, 3); + + { + Observer<Object> observer1 = TestHelper.mockObserver(); + InOrder inOrder = inOrder(observer1); + + co.subscribe(observer1); + + source.onNext(1); + source.onNext(2); + source.onNext(3); + + inOrder.verify(observer1, times(1)).onNext(2); + inOrder.verify(observer1, times(1)).onNext(4); + inOrder.verify(observer1, times(1)).onNext(6); + + source.onNext(4); + source.onComplete(); + inOrder.verify(observer1, times(1)).onNext(8); + inOrder.verify(observer1, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + verify(observer1, never()).onError(any(Throwable.class)); + + } + + { + Observer<Object> observer1 = TestHelper.mockObserver(); + InOrder inOrder = inOrder(observer1); + + co.subscribe(observer1); + + inOrder.verify(observer1, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + verify(observer1, never()).onError(any(Throwable.class)); + } + } + + @Test + public void windowedReplaySelector() { + + final Function<Integer, Integer> dbl = new Function<Integer, Integer>() { + + @Override + public Integer apply(Integer t1) { + return t1 * 2; + } + + }; + + Function<Observable<Integer>, Observable<Integer>> selector = new Function<Observable<Integer>, Observable<Integer>>() { + + @Override + public Observable<Integer> apply(Observable<Integer> t1) { + return t1.map(dbl); + } + + }; + + TestScheduler scheduler = new TestScheduler(); + + PublishSubject<Integer> source = PublishSubject.create(); + + Observable<Integer> co = source.replay(selector, 100, TimeUnit.MILLISECONDS, scheduler); + + { + Observer<Object> observer1 = TestHelper.mockObserver(); + InOrder inOrder = inOrder(observer1); + + co.subscribe(observer1); + + source.onNext(1); + scheduler.advanceTimeBy(60, TimeUnit.MILLISECONDS); + source.onNext(2); + scheduler.advanceTimeBy(60, TimeUnit.MILLISECONDS); + source.onNext(3); + scheduler.advanceTimeBy(60, TimeUnit.MILLISECONDS); + source.onComplete(); + scheduler.advanceTimeBy(60, TimeUnit.MILLISECONDS); + + inOrder.verify(observer1, times(1)).onNext(2); + inOrder.verify(observer1, times(1)).onNext(4); + inOrder.verify(observer1, times(1)).onNext(6); + + inOrder.verify(observer1, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + verify(observer1, never()).onError(any(Throwable.class)); + + } + { + Observer<Object> observer1 = TestHelper.mockObserver(); + InOrder inOrder = inOrder(observer1); + + co.subscribe(observer1); + + inOrder.verify(observer1, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + verify(observer1, never()).onError(any(Throwable.class)); + } + } + + @Test + public void bufferedReplayError() { + PublishSubject<Integer> source = PublishSubject.create(); + + ConnectableObservable<Integer> co = source.replay(3, true); + co.connect(); + + { + Observer<Object> observer1 = TestHelper.mockObserver(); + InOrder inOrder = inOrder(observer1); + + co.subscribe(observer1); + + source.onNext(1); + source.onNext(2); + source.onNext(3); + + inOrder.verify(observer1, times(1)).onNext(1); + inOrder.verify(observer1, times(1)).onNext(2); + inOrder.verify(observer1, times(1)).onNext(3); + + source.onNext(4); + source.onError(new RuntimeException("Forced failure")); + + inOrder.verify(observer1, times(1)).onNext(4); + inOrder.verify(observer1, times(1)).onError(any(RuntimeException.class)); + inOrder.verifyNoMoreInteractions(); + verify(observer1, never()).onComplete(); + + } + + { + Observer<Object> observer1 = TestHelper.mockObserver(); + InOrder inOrder = inOrder(observer1); + + co.subscribe(observer1); + + inOrder.verify(observer1, times(1)).onNext(2); + inOrder.verify(observer1, times(1)).onNext(3); + inOrder.verify(observer1, times(1)).onNext(4); + inOrder.verify(observer1, times(1)).onError(any(RuntimeException.class)); + inOrder.verifyNoMoreInteractions(); + verify(observer1, never()).onComplete(); + } + } + + @Test + public void windowedReplayError() { + TestScheduler scheduler = new TestScheduler(); + + PublishSubject<Integer> source = PublishSubject.create(); + + ConnectableObservable<Integer> co = source.replay(100, TimeUnit.MILLISECONDS, scheduler, true); + co.connect(); + + { + Observer<Object> observer1 = TestHelper.mockObserver(); + InOrder inOrder = inOrder(observer1); + + co.subscribe(observer1); + + source.onNext(1); + scheduler.advanceTimeBy(60, TimeUnit.MILLISECONDS); + source.onNext(2); + scheduler.advanceTimeBy(60, TimeUnit.MILLISECONDS); + source.onNext(3); + scheduler.advanceTimeBy(60, TimeUnit.MILLISECONDS); + source.onError(new RuntimeException("Forced failure")); + scheduler.advanceTimeBy(60, TimeUnit.MILLISECONDS); + + inOrder.verify(observer1, times(1)).onNext(1); + inOrder.verify(observer1, times(1)).onNext(2); + inOrder.verify(observer1, times(1)).onNext(3); + + inOrder.verify(observer1, times(1)).onError(any(RuntimeException.class)); + inOrder.verifyNoMoreInteractions(); + verify(observer1, never()).onComplete(); + + } + { + Observer<Object> observer1 = TestHelper.mockObserver(); + InOrder inOrder = inOrder(observer1); + + co.subscribe(observer1); + inOrder.verify(observer1, never()).onNext(3); + + inOrder.verify(observer1, times(1)).onError(any(RuntimeException.class)); + inOrder.verifyNoMoreInteractions(); + verify(observer1, never()).onComplete(); + } + } + + @Test + public void synchronousDisconnect() { + final AtomicInteger effectCounter = new AtomicInteger(); + Observable<Integer> source = Observable.just(1, 2, 3, 4) + .doOnNext(new Consumer<Integer>() { + @Override + public void accept(Integer v) { + effectCounter.incrementAndGet(); + System.out.println("Sideeffect #" + v); + } + }); + + Observable<Integer> result = source.replay( + new Function<Observable<Integer>, Observable<Integer>>() { + @Override + public Observable<Integer> apply(Observable<Integer> o) { + return o.take(2); + } + }); + + for (int i = 1; i < 3; i++) { + effectCounter.set(0); + System.out.printf("- %d -%n", i); + result.subscribe(new Consumer<Integer>() { + + @Override + public void accept(Integer t1) { + System.out.println(t1); + } + + }, new Consumer<Throwable>() { + + @Override + public void accept(Throwable t1) { + t1.printStackTrace(); + } + }, + new Action() { + @Override + public void run() { + System.out.println("Done"); + } + }); + assertEquals(2, effectCounter.get()); + } + } + + /* + * test the basic expectation of OperatorMulticast via replay + */ + @SuppressWarnings("unchecked") + @Test + public void issue2191_UnsubscribeSource() throws Throwable { + // setup mocks + Consumer<Integer> sourceNext = mock(Consumer.class); + Action sourceCompleted = mock(Action.class); + Action sourceUnsubscribed = mock(Action.class); + Observer<Integer> spiedSubscriberBeforeConnect = TestHelper.mockObserver(); + Observer<Integer> spiedSubscriberAfterConnect = TestHelper.mockObserver(); + + // Observable under test + Observable<Integer> source = Observable.just(1, 2); + + ConnectableObservable<Integer> replay = source + .doOnNext(sourceNext) + .doOnDispose(sourceUnsubscribed) + .doOnComplete(sourceCompleted) + .replay(); + + replay.subscribe(spiedSubscriberBeforeConnect); + replay.subscribe(spiedSubscriberBeforeConnect); + replay.connect(); + replay.subscribe(spiedSubscriberAfterConnect); + replay.subscribe(spiedSubscriberAfterConnect); + + verify(spiedSubscriberBeforeConnect, times(2)).onSubscribe((Disposable)any()); + verify(spiedSubscriberAfterConnect, times(2)).onSubscribe((Disposable)any()); + + // verify interactions + verify(sourceNext, times(1)).accept(1); + verify(sourceNext, times(1)).accept(2); + verify(sourceCompleted, times(1)).run(); + verifyObserverMock(spiedSubscriberBeforeConnect, 2, 4); + verifyObserverMock(spiedSubscriberAfterConnect, 2, 4); + +// verify(sourceUnsubscribed, times(1)).run(); + + verifyNoMoreInteractions(sourceNext); + verifyNoMoreInteractions(sourceCompleted); + verifyNoMoreInteractions(sourceUnsubscribed); + verifyNoMoreInteractions(spiedSubscriberBeforeConnect); + verifyNoMoreInteractions(spiedSubscriberAfterConnect); + + } + + /** + * Specifically test interaction with a Scheduler with subscribeOn. + * + * @throws Throwable functional interfaces are declared with throws Exception + */ + @SuppressWarnings("unchecked") + @Test + public void issue2191_SchedulerUnsubscribe() throws Throwable { + // setup mocks + Consumer<Integer> sourceNext = mock(Consumer.class); + Action sourceCompleted = mock(Action.class); + Action sourceUnsubscribed = mock(Action.class); + final TestScheduler mockScheduler = new TestScheduler(); + + Observer<Integer> mockObserverBeforeConnect = TestHelper.mockObserver(); + Observer<Integer> mockObserverAfterConnect = TestHelper.mockObserver(); + + // Observable under test + ConnectableObservable<Integer> replay = Observable.just(1, 2, 3) + .doOnNext(sourceNext) + .doOnDispose(sourceUnsubscribed) + .doOnComplete(sourceCompleted) + .subscribeOn(mockScheduler).replay(); + + replay.subscribe(mockObserverBeforeConnect); + replay.connect(); + replay.subscribe(mockObserverAfterConnect); + + verify(mockObserverBeforeConnect).onSubscribe((Disposable)any()); + verify(mockObserverAfterConnect).onSubscribe((Disposable)any()); + + mockScheduler.advanceTimeBy(1, TimeUnit.SECONDS); + + // verify interactions + verify(sourceNext, times(1)).accept(1); + verify(sourceNext, times(1)).accept(2); + verify(sourceNext, times(1)).accept(3); + verify(sourceCompleted, times(1)).run(); + verifyObserverMock(mockObserverBeforeConnect, 1, 3); + verifyObserverMock(mockObserverAfterConnect, 1, 3); + + // FIXME not supported +// verify(spiedWorker, times(1)).isUnsubscribed(); + // FIXME publish calls cancel too +// verify(sourceUnsubscribed, times(1)).run(); + + verifyNoMoreInteractions(sourceNext); + verifyNoMoreInteractions(sourceCompleted); + verifyNoMoreInteractions(sourceUnsubscribed); + verifyNoMoreInteractions(mockObserverBeforeConnect); + verifyNoMoreInteractions(mockObserverAfterConnect); + } + + /** + * Specifically test interaction with a Scheduler with subscribeOn. + * + * @throws Throwable functional interfaces are declared with throws Exception + */ + @SuppressWarnings("unchecked") + @Test + public void issue2191_SchedulerUnsubscribeOnError() throws Throwable { + // setup mocks + Consumer<Integer> sourceNext = mock(Consumer.class); + Action sourceCompleted = mock(Action.class); + Consumer<Throwable> sourceError = mock(Consumer.class); + Action sourceUnsubscribed = mock(Action.class); + final TestScheduler mockScheduler = new TestScheduler(); + Observer<Integer> mockObserverBeforeConnect = TestHelper.mockObserver(); + Observer<Integer> mockObserverAfterConnect = TestHelper.mockObserver(); + + // Observable under test + Function<Integer, Integer> mockFunc = mock(Function.class); + IllegalArgumentException illegalArgumentException = new IllegalArgumentException(); + when(mockFunc.apply(1)).thenReturn(1); + when(mockFunc.apply(2)).thenThrow(illegalArgumentException); + ConnectableObservable<Integer> replay = Observable.just(1, 2, 3).map(mockFunc) + .doOnNext(sourceNext) + .doOnDispose(sourceUnsubscribed) + .doOnComplete(sourceCompleted) + .doOnError(sourceError) + .subscribeOn(mockScheduler).replay(); + + replay.subscribe(mockObserverBeforeConnect); + replay.connect(); + replay.subscribe(mockObserverAfterConnect); + + verify(mockObserverBeforeConnect).onSubscribe((Disposable)any()); + verify(mockObserverAfterConnect).onSubscribe((Disposable)any()); + + mockScheduler.advanceTimeBy(1, TimeUnit.SECONDS); + // verify interactions + verify(sourceNext, times(1)).accept(1); + verify(sourceError, times(1)).accept(illegalArgumentException); + verifyObserver(mockObserverBeforeConnect, 1, 1, illegalArgumentException); + verifyObserver(mockObserverAfterConnect, 1, 1, illegalArgumentException); + + // FIXME no longer supported +// verify(spiedWorker, times(1)).isUnsubscribed(); + // FIXME publish also calls cancel +// verify(sourceUnsubscribed, times(1)).run(); + + verifyNoMoreInteractions(sourceNext); + verifyNoMoreInteractions(sourceCompleted); + verifyNoMoreInteractions(sourceError); + verifyNoMoreInteractions(sourceUnsubscribed); + verifyNoMoreInteractions(mockObserverBeforeConnect); + verifyNoMoreInteractions(mockObserverAfterConnect); + } + + private static void verifyObserverMock(Observer<Integer> mock, int numSubscriptions, int numItemsExpected) { + verify(mock, times(numItemsExpected)).onNext((Integer) notNull()); + verify(mock, times(numSubscriptions)).onComplete(); + verifyNoMoreInteractions(mock); + } + + private static void verifyObserver(Observer<Integer> mock, int numSubscriptions, int numItemsExpected, Throwable error) { + verify(mock, times(numItemsExpected)).onNext((Integer) notNull()); + verify(mock, times(numSubscriptions)).onError(error); + verifyNoMoreInteractions(mock); + } + + public static Worker workerSpy(final Disposable mockDisposable) { + return spy(new InprocessWorker(mockDisposable)); + } + + static class InprocessWorker extends Worker { + private final Disposable mockDisposable; + public boolean unsubscribed; + + InprocessWorker(Disposable mockDisposable) { + this.mockDisposable = mockDisposable; + } + + @NonNull + @Override + public Disposable schedule(@NonNull Runnable action) { + action.run(); + return mockDisposable; // this subscription is returned but discarded + } + + @NonNull + @Override + public Disposable schedule(@NonNull Runnable action, long delayTime, @NonNull TimeUnit unit) { + action.run(); + return mockDisposable; + } + + @Override + public void dispose() { + unsubscribed = true; + } + + @Override + public boolean isDisposed() { + return unsubscribed; + } + } + + @Test + public void boundedReplayBuffer() { + BoundedReplayBuffer<Integer> buf = new BoundedReplayBuffer<Integer>(false) { + private static final long serialVersionUID = -5182053207244406872L; + + @Override + void truncate() { + } + }; + buf.addLast(new Node(1)); + buf.addLast(new Node(2)); + buf.addLast(new Node(3)); + buf.addLast(new Node(4)); + buf.addLast(new Node(5)); + + List<Integer> values = new ArrayList<>(); + buf.collect(values); + + Assert.assertEquals(Arrays.asList(1, 2, 3, 4, 5), values); + + buf.removeSome(2); + buf.removeFirst(); + buf.removeSome(2); + + values.clear(); + buf.collect(values); + Assert.assertTrue(values.isEmpty()); + + buf.addLast(new Node(5)); + buf.addLast(new Node(6)); + buf.collect(values); + + Assert.assertEquals(Arrays.asList(5, 6), values); + + } + + @Test + public void timedAndSizedTruncation() { + TestScheduler test = new TestScheduler(); + SizeAndTimeBoundReplayBuffer<Integer> buf = new SizeAndTimeBoundReplayBuffer<>(2, 2000, TimeUnit.MILLISECONDS, test, false); + List<Integer> values = new ArrayList<>(); + + buf.next(1); + test.advanceTimeBy(1, TimeUnit.SECONDS); + buf.next(2); + test.advanceTimeBy(1, TimeUnit.SECONDS); + buf.collect(values); + Assert.assertEquals(Arrays.asList(2), values); + + buf.next(3); + buf.next(4); + values.clear(); + buf.collect(values); + Assert.assertEquals(Arrays.asList(3, 4), values); + + test.advanceTimeBy(2, TimeUnit.SECONDS); + buf.next(5); + + values.clear(); + buf.collect(values); + Assert.assertEquals(Arrays.asList(5), values); + Assert.assertFalse(buf.hasCompleted()); + + test.advanceTimeBy(2, TimeUnit.SECONDS); + buf.complete(); + + values.clear(); + buf.collect(values); + Assert.assertTrue(values.isEmpty()); + + Assert.assertEquals(1, buf.size); + Assert.assertTrue(buf.hasCompleted()); + Assert.assertFalse(buf.hasError()); + } + + @Test + public void timedAndSizedTruncationError() { + TestScheduler test = new TestScheduler(); + SizeAndTimeBoundReplayBuffer<Integer> buf = new SizeAndTimeBoundReplayBuffer<>(2, 2000, TimeUnit.MILLISECONDS, test, false); + + Assert.assertFalse(buf.hasCompleted()); + Assert.assertFalse(buf.hasError()); + + List<Integer> values = new ArrayList<>(); + + buf.next(1); + test.advanceTimeBy(1, TimeUnit.SECONDS); + buf.next(2); + test.advanceTimeBy(1, TimeUnit.SECONDS); + buf.collect(values); + Assert.assertEquals(Arrays.asList(2), values); + + buf.next(3); + buf.next(4); + values.clear(); + buf.collect(values); + Assert.assertEquals(Arrays.asList(3, 4), values); + + test.advanceTimeBy(2, TimeUnit.SECONDS); + buf.next(5); + + values.clear(); + buf.collect(values); + Assert.assertEquals(Arrays.asList(5), values); + Assert.assertFalse(buf.hasCompleted()); + Assert.assertFalse(buf.hasError()); + + test.advanceTimeBy(2, TimeUnit.SECONDS); + buf.error(new TestException()); + + values.clear(); + buf.collect(values); + Assert.assertTrue(values.isEmpty()); + + Assert.assertEquals(1, buf.size); + Assert.assertFalse(buf.hasCompleted()); + Assert.assertTrue(buf.hasError()); + } + + @Test + public void sizedTruncation() { + SizeBoundReplayBuffer<Integer> buf = new SizeBoundReplayBuffer<>(2, false); + List<Integer> values = new ArrayList<>(); + + buf.next(1); + buf.next(2); + buf.collect(values); + Assert.assertEquals(Arrays.asList(1, 2), values); + + buf.next(3); + buf.next(4); + values.clear(); + buf.collect(values); + Assert.assertEquals(Arrays.asList(3, 4), values); + + buf.next(5); + + values.clear(); + buf.collect(values); + Assert.assertEquals(Arrays.asList(4, 5), values); + Assert.assertFalse(buf.hasCompleted()); + + buf.complete(); + + values.clear(); + buf.collect(values); + Assert.assertEquals(Arrays.asList(4, 5), values); + + Assert.assertEquals(3, buf.size); + Assert.assertTrue(buf.hasCompleted()); + Assert.assertFalse(buf.hasError()); + } + + @Test + public void coldReplayNoBackpressure() { + Observable<Integer> source = Observable.range(0, 1000).replay().autoConnect(); + + TestObserverEx<Integer> to = new TestObserverEx<>(); + + source.subscribe(to); + + to.assertNoErrors(); + to.assertTerminated(); + List<Integer> onNextEvents = to.values(); + assertEquals(1000, onNextEvents.size()); + + for (int i = 0; i < 1000; i++) { + assertEquals((Integer)i, onNextEvents.get(i)); + } + } + + @Test + public void cache() throws InterruptedException { + final AtomicInteger counter = new AtomicInteger(); + Observable<String> o = Observable.unsafeCreate(new ObservableSource<String>() { + + @Override + public void subscribe(final Observer<? super String> observer) { + observer.onSubscribe(Disposable.empty()); + new Thread(new Runnable() { + + @Override + public void run() { + counter.incrementAndGet(); + System.out.println("published Observable being executed"); + observer.onNext("one"); + observer.onComplete(); + } + }).start(); + } + }).replay().autoConnect(); + + // we then expect the following 2 subscriptions to get that same value + final CountDownLatch latch = new CountDownLatch(2); + + // subscribe once + o.subscribe(new Consumer<String>() { + + @Override + public void accept(String v) { + assertEquals("one", v); + System.out.println("v: " + v); + latch.countDown(); + } + }); + + // subscribe again + o.subscribe(new Consumer<String>() { + + @Override + public void accept(String v) { + assertEquals("one", v); + System.out.println("v: " + v); + latch.countDown(); + } + }); + + if (!latch.await(1000, TimeUnit.MILLISECONDS)) { + fail("subscriptions did not receive values"); + } + assertEquals(1, counter.get()); + } + + @Test + public void unsubscribeSource() throws Throwable { + Action unsubscribe = mock(Action.class); + Observable<Integer> o = Observable.just(1).doOnDispose(unsubscribe).replay().autoConnect(); + o.subscribe(); + o.subscribe(); + o.subscribe(); + verify(unsubscribe, never()).run(); + } + + @Test + public void take() { + TestObserverEx<Integer> to = new TestObserverEx<>(); + + Observable<Integer> cached = Observable.range(1, 100).replay().autoConnect(); + cached.take(10).subscribe(to); + + to.assertNoErrors(); + to.assertTerminated(); + to.assertValues(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + // FIXME no longer assertable +// ts.assertUnsubscribed(); + } + + @Test + public void async() { + Observable<Integer> source = Observable.range(1, 10000); + for (int i = 0; i < 100; i++) { + TestObserverEx<Integer> to1 = new TestObserverEx<>(); + + Observable<Integer> cached = source.replay().autoConnect(); + + cached.observeOn(Schedulers.computation()).subscribe(to1); + + to1.awaitDone(2, TimeUnit.SECONDS); + to1.assertNoErrors(); + to1.assertTerminated(); + assertEquals(10000, to1.values().size()); + + TestObserverEx<Integer> to2 = new TestObserverEx<>(); + cached.observeOn(Schedulers.computation()).subscribe(to2); + + to2.awaitDone(2, TimeUnit.SECONDS); + to2.assertNoErrors(); + to2.assertTerminated(); + assertEquals(10000, to2.values().size()); + } + } + + @Test + public void asyncComeAndGo() { + Observable<Long> source = Observable.interval(1, 1, TimeUnit.MILLISECONDS) + .take(1000) + .subscribeOn(Schedulers.io()); + Observable<Long> cached = source.replay().autoConnect(); + + Observable<Long> output = cached.observeOn(Schedulers.computation()); + + List<TestObserverEx<Long>> list = new ArrayList<>(100); + for (int i = 0; i < 100; i++) { + TestObserverEx<Long> to = new TestObserverEx<>(); + list.add(to); + output.skip(i * 10).take(10).subscribe(to); + } + + List<Long> expected = new ArrayList<>(); + for (int i = 0; i < 10; i++) { + expected.add((long)(i - 10)); + } + int j = 0; + for (TestObserverEx<Long> to : list) { + to.awaitDone(3, TimeUnit.SECONDS); + to.assertNoErrors(); + to.assertTerminated(); + + for (int i = j * 10; i < j * 10 + 10; i++) { + expected.set(i - j * 10, (long)i); + } + + to.assertValueSequence(expected); + + j++; + } + } + + @Test + public void noMissingBackpressureException() { + final int m = 4 * 1000 * 1000; + Observable<Integer> firehose = Observable.unsafeCreate(new ObservableSource<Integer>() { + @Override + public void subscribe(Observer<? super Integer> t) { + t.onSubscribe(Disposable.empty()); + for (int i = 0; i < m; i++) { + t.onNext(i); + } + t.onComplete(); + } + }); + + TestObserverEx<Integer> to = new TestObserverEx<>(); + firehose.replay().autoConnect().observeOn(Schedulers.computation()).takeLast(100).subscribe(to); + + to.awaitDone(3, TimeUnit.SECONDS); + to.assertNoErrors(); + to.assertTerminated(); + + assertEquals(100, to.values().size()); + } + + @Test + public void valuesAndThenError() { + Observable<Integer> source = Observable.range(1, 10) + .concatWith(Observable.<Integer>error(new TestException())) + .replay().autoConnect(); + + TestObserverEx<Integer> to = new TestObserverEx<>(); + source.subscribe(to); + + to.assertValues(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + to.assertNotComplete(); + Assert.assertEquals(1, to.errors().size()); + + TestObserverEx<Integer> to2 = new TestObserverEx<>(); + source.subscribe(to2); + + to2.assertValues(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + to2.assertNotComplete(); + Assert.assertEquals(1, to2.errors().size()); + } + + @Test + public void replayTime() { + Observable.just(1).replay(1, TimeUnit.MINUTES, Schedulers.computation(), true) + .autoConnect() + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(1); + } + + @Test + public void replaySizeAndTime() { + Observable.just(1).replay(1, 1, TimeUnit.MILLISECONDS, Schedulers.computation(), true) + .autoConnect() + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(1); + } + + @Test + public void replaySelectorTime() { + Observable.just(1).replay(Functions.<Observable<Integer>>identity(), 1, TimeUnit.MINUTES) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(1); + } + + @Test + public void replayMaxInt() { + Observable.range(1, 2) + .replay(Integer.MAX_VALUE, true) + .autoConnect() + .test() + .assertResult(1, 2); + } + + @Test + public void source() { + Observable<Integer> source = Observable.range(1, 3); + + assertSame(source, (((HasUpstreamObservableSource<?>)source.replay())).source()); + } + + @Test + public void connectRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final ConnectableObservable<Integer> co = Observable.range(1, 3).replay(); + + Runnable r = new Runnable() { + @Override + public void run() { + co.connect(); + } + }; + + TestHelper.race(r, r); + } + } + + @Test + public void subscribeRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final ConnectableObservable<Integer> co = Observable.range(1, 3).replay(); + + final TestObserver<Integer> to1 = new TestObserver<>(); + final TestObserver<Integer> to2 = new TestObserver<>(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + co.subscribe(to1); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + co.subscribe(to2); + } + }; + + TestHelper.race(r1, r2); + } + } + + @Test + public void addRemoveRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final ConnectableObservable<Integer> co = Observable.range(1, 3).replay(); + + final TestObserver<Integer> to1 = new TestObserver<>(); + final TestObserver<Integer> to2 = new TestObserver<>(); + + co.subscribe(to1); + + Runnable r1 = new Runnable() { + @Override + public void run() { + to1.dispose(); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + co.subscribe(to2); + } + }; + + TestHelper.race(r1, r2); + } + } + + @Test + public void cancelOnArrival() { + Observable.range(1, 2) + .replay(Integer.MAX_VALUE, true) + .autoConnect() + .test(true) + .assertEmpty(); + } + + @Test + public void cancelOnArrival2() { + ConnectableObservable<Integer> co = PublishSubject.<Integer>create() + .replay(Integer.MAX_VALUE, true); + + co.test(); + + co + .autoConnect() + .test(true) + .assertEmpty(); + } + + @Test + public void connectConsumerThrows() { + ConnectableObservable<Integer> co = Observable.range(1, 2) + .replay(); + + try { + co.connect(new Consumer<Disposable>() { + @Override + public void accept(Disposable t) throws Exception { + throw new TestException(); + } + }); + fail("Should have thrown"); + } catch (TestException ex) { + // expected + } + + co.test().assertEmpty().dispose(); + + co.connect(); + + co.test().assertResult(1, 2); + } + + @Test + public void badSource() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + new Observable<Integer>() { + @Override + protected void subscribeActual(Observer<? super Integer> observer) { + observer.onSubscribe(Disposable.empty()); + observer.onError(new TestException("First")); + observer.onNext(1); + observer.onError(new TestException("Second")); + observer.onComplete(); + } + }.replay() + .autoConnect() + .to(TestHelper.<Integer>testConsumer()) + .assertFailureAndMessage(TestException.class, "First"); + + TestHelper.assertUndeliverable(errors, 0, TestException.class, "Second"); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void subscribeOnNextRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final PublishSubject<Integer> ps = PublishSubject.create(); + + final ConnectableObservable<Integer> co = ps.replay(); + + final TestObserver<Integer> to1 = new TestObserver<>(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + co.subscribe(to1); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + for (int j = 0; j < 1000; j++) { + ps.onNext(j); + } + } + }; + + TestHelper.race(r1, r2); + } + } + + @Test + public void unsubscribeOnNextRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final PublishSubject<Integer> ps = PublishSubject.create(); + + final ConnectableObservable<Integer> co = ps.replay(); + + final TestObserver<Integer> to1 = new TestObserver<>(); + + co.subscribe(to1); + + Runnable r1 = new Runnable() { + @Override + public void run() { + to1.dispose(); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + for (int j = 0; j < 1000; j++) { + ps.onNext(j); + } + } + }; + + TestHelper.race(r1, r2); + } + } + + @Test + public void unsubscribeReplayRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final ConnectableObservable<Integer> co = Observable.range(1, 1000).replay(); + + final TestObserver<Integer> to1 = new TestObserver<>(); + + co.connect(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + co.subscribe(to1); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + to1.dispose(); + } + }; + + TestHelper.race(r1, r2); + } + } + + @Test + public void reentrantOnNext() { + final PublishSubject<Integer> ps = PublishSubject.create(); + + TestObserver<Integer> to = new TestObserver<Integer>() { + @Override + public void onNext(Integer t) { + if (t == 1) { + ps.onNext(2); + ps.onComplete(); + } + super.onNext(t); + } + }; + + ps.replay().autoConnect().subscribe(to); + + ps.onNext(1); + + to.assertResult(1, 2); + } + + @Test + public void reentrantOnNextBound() { + final PublishSubject<Integer> ps = PublishSubject.create(); + + TestObserver<Integer> to = new TestObserver<Integer>() { + @Override + public void onNext(Integer t) { + if (t == 1) { + ps.onNext(2); + ps.onComplete(); + } + super.onNext(t); + } + }; + + ps.replay(10, true).autoConnect().subscribe(to); + + ps.onNext(1); + + to.assertResult(1, 2); + } + + @Test + public void reentrantOnNextCancel() { + final PublishSubject<Integer> ps = PublishSubject.create(); + + TestObserver<Integer> to = new TestObserver<Integer>() { + @Override + public void onNext(Integer t) { + if (t == 1) { + ps.onNext(2); + dispose(); + } + super.onNext(t); + } + }; + + ps.replay().autoConnect().subscribe(to); + + ps.onNext(1); + + to.assertValues(1); + } + + @Test + public void reentrantOnNextCancelBounded() { + final PublishSubject<Integer> ps = PublishSubject.create(); + + TestObserver<Integer> to = new TestObserver<Integer>() { + @Override + public void onNext(Integer t) { + if (t == 1) { + ps.onNext(2); + dispose(); + } + super.onNext(t); + } + }; + + ps.replay(10, true).autoConnect().subscribe(to); + + ps.onNext(1); + + to.assertValues(1); + } + + @Test + public void delayedUpstreamOnSubscribe() { + final Observer<?>[] sub = { null }; + + new Observable<Integer>() { + @Override + protected void subscribeActual(Observer<? super Integer> observer) { + sub[0] = observer; + } + } + .replay() + .connect() + .dispose(); + + Disposable bs = Disposable.empty(); + + sub[0].onSubscribe(bs); + + assertTrue(bs.isDisposed()); + } + + @Test + public void timedNoOutdatedData() { + TestScheduler scheduler = new TestScheduler(); + + Observable<Integer> source = Observable.just(1) + .replay(2, TimeUnit.SECONDS, scheduler, true) + .autoConnect(); + + source.test().assertResult(1); + + source.test().assertResult(1); + + scheduler.advanceTimeBy(3, TimeUnit.SECONDS); + + source.test().assertResult(); + } + + @Test + public void replaySelectorReturnsNull() { + Observable.just(1) + .replay(new Function<Observable<Integer>, Observable<Object>>() { + @Override + public Observable<Object> apply(Observable<Integer> v) throws Exception { + return null; + } + }) + .to(TestHelper.<Object>testConsumer()) + .assertFailureAndMessage(NullPointerException.class, "The selector returned a null ObservableSource"); + } + + @Test + public void replaySelectorConnectableReturnsNull() { + ObservableReplay.multicastSelector(Functions.justSupplier((ConnectableObservable<Integer>)null), Functions.justFunction(Observable.just(1))) + .to(TestHelper.<Integer>testConsumer()) + .assertFailureAndMessage(NullPointerException.class, "The connectableFactory returned a null ConnectableObservable"); + } + + @Test + public void noHeadRetentionCompleteSize() { + PublishSubject<Integer> source = PublishSubject.create(); + + ObservableReplay<Integer> co = (ObservableReplay<Integer>)source + .replay(1, true); + + co.connect(); + + BoundedReplayBuffer<Integer> buf = (BoundedReplayBuffer<Integer>)(co.current.get().buffer); + + source.onNext(1); + source.onNext(2); + source.onComplete(); + + assertNull(buf.get().value); + + Object o = buf.get(); + + buf.trimHead(); + + assertSame(o, buf.get()); + } + + @Test + public void noHeadRetentionErrorSize() { + PublishSubject<Integer> source = PublishSubject.create(); + + ObservableReplay<Integer> co = (ObservableReplay<Integer>)source + .replay(1, true); + + co.connect(); + + BoundedReplayBuffer<Integer> buf = (BoundedReplayBuffer<Integer>)(co.current.get().buffer); + + source.onNext(1); + source.onNext(2); + source.onError(new TestException()); + + assertNull(buf.get().value); + + Object o = buf.get(); + + buf.trimHead(); + + assertSame(o, buf.get()); + } + + @Test + public void noHeadRetentionSize() { + PublishSubject<Integer> source = PublishSubject.create(); + + ObservableReplay<Integer> co = (ObservableReplay<Integer>)source + .replay(1, true); + + co.connect(); + + BoundedReplayBuffer<Integer> buf = (BoundedReplayBuffer<Integer>)(co.current.get().buffer); + + source.onNext(1); + source.onNext(2); + + assertNull(buf.get().value); + + buf.trimHead(); + + assertNull(buf.get().value); + + Object o = buf.get(); + + buf.trimHead(); + + assertSame(o, buf.get()); + } + + @Test + public void noHeadRetentionCompleteTime() { + PublishSubject<Integer> source = PublishSubject.create(); + + ObservableReplay<Integer> co = (ObservableReplay<Integer>)source + .replay(1, TimeUnit.MINUTES, Schedulers.computation(), true); + + co.connect(); + + BoundedReplayBuffer<Integer> buf = (BoundedReplayBuffer<Integer>)(co.current.get().buffer); + + source.onNext(1); + source.onNext(2); + source.onComplete(); + + assertNull(buf.get().value); + + Object o = buf.get(); + + buf.trimHead(); + + assertSame(o, buf.get()); + } + + @Test + public void noHeadRetentionErrorTime() { + PublishSubject<Integer> source = PublishSubject.create(); + + ObservableReplay<Integer> co = (ObservableReplay<Integer>)source + .replay(1, TimeUnit.MINUTES, Schedulers.computation(), true); + + co.connect(); + + BoundedReplayBuffer<Integer> buf = (BoundedReplayBuffer<Integer>)(co.current.get().buffer); + + source.onNext(1); + source.onNext(2); + source.onError(new TestException()); + + assertNull(buf.get().value); + + Object o = buf.get(); + + buf.trimHead(); + + assertSame(o, buf.get()); + } + + @Test + public void noHeadRetentionTime() { + TestScheduler sch = new TestScheduler(); + + PublishSubject<Integer> source = PublishSubject.create(); + + ObservableReplay<Integer> co = (ObservableReplay<Integer>)source + .replay(1, TimeUnit.MILLISECONDS, sch, true); + + co.connect(); + + BoundedReplayBuffer<Integer> buf = (BoundedReplayBuffer<Integer>)(co.current.get().buffer); + + source.onNext(1); + + sch.advanceTimeBy(2, TimeUnit.MILLISECONDS); + + source.onNext(2); + + assertNull(buf.get().value); + + buf.trimHead(); + + assertNull(buf.get().value); + + Object o = buf.get(); + + buf.trimHead(); + + assertSame(o, buf.get()); + } + + @Test + public void noBoundedRetentionViaThreadLocal() throws Exception { + Observable<byte[]> source = Observable.range(1, 200) + .map(new Function<Integer, byte[]>() { + @Override + public byte[] apply(Integer v) throws Exception { + return new byte[1024 * 1024]; + } + }) + .replay(new Function<Observable<byte[]>, Observable<byte[]>>() { + @Override + public Observable<byte[]> apply(final Observable<byte[]> o) throws Exception { + return o.take(1) + .concatMap(new Function<byte[], Observable<byte[]>>() { + @Override + public Observable<byte[]> apply(byte[] v) throws Exception { + return o; + } + }); + } + }, 1) + .takeLast(1) + ; + + System.out.println("Bounded Replay Leak check: Wait before GC"); + Thread.sleep(1000); + + System.out.println("Bounded Replay Leak check: GC"); + System.gc(); + + Thread.sleep(500); + + final MemoryMXBean memoryMXBean = ManagementFactory.getMemoryMXBean(); + MemoryUsage memHeap = memoryMXBean.getHeapMemoryUsage(); + long initial = memHeap.getUsed(); + + System.out.printf("Bounded Replay Leak check: Starting: %.3f MB%n", initial / 1024.0 / 1024.0); + + final AtomicLong after = new AtomicLong(); + + source.subscribe(new Consumer<byte[]>() { + @Override + public void accept(byte[] v) throws Exception { + System.out.println("Bounded Replay Leak check: Wait before GC 2"); + Thread.sleep(1000); + + System.out.println("Bounded Replay Leak check: GC 2"); + System.gc(); + + Thread.sleep(500); + + after.set(memoryMXBean.getHeapMemoryUsage().getUsed()); + } + }); + + System.out.printf("Bounded Replay Leak check: After: %.3f MB%n", after.get() / 1024.0 / 1024.0); + + if (initial + 100 * 1024 * 1024 < after.get()) { + Assert.fail("Bounded Replay Leak check: Memory leak detected: " + (initial / 1024.0 / 1024.0) + + " -> " + after.get() / 1024.0 / 1024.0); + } + } + + @Test + public void sizeBoundEagerTruncate() throws Exception { + + PublishSubject<int[]> ps = PublishSubject.create(); + + ConnectableObservable<int[]> co = ps.replay(1, true); + + TestObserver<int[]> to = co.test(); + + co.connect(); + + Thread.sleep(200); + System.gc(); + Thread.sleep(200); + + final MemoryMXBean memoryMXBean = ManagementFactory.getMemoryMXBean(); + long initial = memoryMXBean.getHeapMemoryUsage().getUsed(); + + System.out.printf("Bounded Replay Leak check: Starting: %.3f MB%n", initial / 1024.0 / 1024.0); + + ps.onNext(new int[100 * 1024 * 1024]); + + to.assertValueCount(1); + to.values().clear(); + + ps.onNext(new int[0]); + + Thread.sleep(200); + System.gc(); + Thread.sleep(200); + + long after = memoryMXBean.getHeapMemoryUsage().getUsed(); + + to.dispose(); + + System.out.printf("Bounded Replay Leak check: After: %.3f MB%n", after / 1024.0 / 1024.0); + + if (initial + 100 * 1024 * 1024 < after) { + Assert.fail("Bounded Replay Leak check: Memory leak detected: " + (initial / 1024.0 / 1024.0) + + " -> " + after / 1024.0 / 1024.0); + } + } + + @Test + public void timeBoundEagerTruncate() throws Exception { + + PublishSubject<int[]> ps = PublishSubject.create(); + + TestScheduler scheduler = new TestScheduler(); + + ConnectableObservable<int[]> co = ps.replay(1, TimeUnit.SECONDS, scheduler, true); + + TestObserver<int[]> to = co.test(); + + co.connect(); + + Thread.sleep(200); + System.gc(); + Thread.sleep(200); + + final MemoryMXBean memoryMXBean = ManagementFactory.getMemoryMXBean(); + long initial = memoryMXBean.getHeapMemoryUsage().getUsed(); + + System.out.printf("Bounded Replay Leak check: Starting: %.3f MB%n", initial / 1024.0 / 1024.0); + + ps.onNext(new int[100 * 1024 * 1024]); + + to.assertValueCount(1); + to.values().clear(); + + scheduler.advanceTimeBy(2, TimeUnit.SECONDS); + + ps.onNext(new int[0]); + + Thread.sleep(200); + System.gc(); + Thread.sleep(200); + + long after = memoryMXBean.getHeapMemoryUsage().getUsed(); + + to.dispose(); + + System.out.printf("Bounded Replay Leak check: After: %.3f MB%n", after / 1024.0 / 1024.0); + + if (initial + 100 * 1024 * 1024 < after) { + Assert.fail("Bounded Replay Leak check: Memory leak detected: " + (initial / 1024.0 / 1024.0) + + " -> " + after / 1024.0 / 1024.0); + } + } + + @Test + public void timeAndSizeBoundEagerTruncate() throws Exception { + + PublishSubject<int[]> ps = PublishSubject.create(); + + TestScheduler scheduler = new TestScheduler(); + + ConnectableObservable<int[]> co = ps.replay(1, 5, TimeUnit.SECONDS, scheduler, true); + + TestObserver<int[]> to = co.test(); + + co.connect(); + + Thread.sleep(200); + System.gc(); + Thread.sleep(200); + + final MemoryMXBean memoryMXBean = ManagementFactory.getMemoryMXBean(); + long initial = memoryMXBean.getHeapMemoryUsage().getUsed(); + + System.out.printf("Bounded Replay Leak check: Starting: %.3f MB%n", initial / 1024.0 / 1024.0); + + ps.onNext(new int[100 * 1024 * 1024]); + + to.assertValueCount(1); + to.values().clear(); + + scheduler.advanceTimeBy(2, TimeUnit.SECONDS); + + ps.onNext(new int[0]); + + Thread.sleep(200); + System.gc(); + Thread.sleep(200); + + long after = memoryMXBean.getHeapMemoryUsage().getUsed(); + + to.dispose(); + + System.out.printf("Bounded Replay Leak check: After: %.3f MB%n", after / 1024.0 / 1024.0); + + if (initial + 100 * 1024 * 1024 < after) { + Assert.fail("Bounded Replay Leak check: Memory leak detected: " + (initial / 1024.0 / 1024.0) + + " -> " + after / 1024.0 / 1024.0); + } + } + + @Test + public void sizeBoundSelectorEagerTruncate() throws Exception { + + PublishSubject<int[]> ps = PublishSubject.create(); + + Observable<int[]> co = ps.replay(Functions.<Observable<int[]>>identity(), 1, true); + + TestObserver<int[]> to = co.test(); + + Thread.sleep(200); + System.gc(); + Thread.sleep(200); + + final MemoryMXBean memoryMXBean = ManagementFactory.getMemoryMXBean(); + long initial = memoryMXBean.getHeapMemoryUsage().getUsed(); + + System.out.printf("Bounded Replay Leak check: Starting: %.3f MB%n", initial / 1024.0 / 1024.0); + + ps.onNext(new int[100 * 1024 * 1024]); + + to.assertValueCount(1); + to.values().clear(); + + ps.onNext(new int[0]); + + Thread.sleep(200); + System.gc(); + Thread.sleep(200); + + long after = memoryMXBean.getHeapMemoryUsage().getUsed(); + + to.dispose(); + + System.out.printf("Bounded Replay Leak check: After: %.3f MB%n", after / 1024.0 / 1024.0); + + if (initial + 100 * 1024 * 1024 < after) { + Assert.fail("Bounded Replay Leak check: Memory leak detected: " + (initial / 1024.0 / 1024.0) + + " -> " + after / 1024.0 / 1024.0); + } + } + + @Test + public void timeBoundSelectorEagerTruncate() throws Exception { + + PublishSubject<int[]> ps = PublishSubject.create(); + + TestScheduler scheduler = new TestScheduler(); + + Observable<int[]> co = ps.replay(Functions.<Observable<int[]>>identity(), 1, TimeUnit.SECONDS, scheduler, true); + + TestObserver<int[]> to = co.test(); + + Thread.sleep(200); + System.gc(); + Thread.sleep(200); + + final MemoryMXBean memoryMXBean = ManagementFactory.getMemoryMXBean(); + long initial = memoryMXBean.getHeapMemoryUsage().getUsed(); + + System.out.printf("Bounded Replay Leak check: Starting: %.3f MB%n", initial / 1024.0 / 1024.0); + + ps.onNext(new int[100 * 1024 * 1024]); + + to.assertValueCount(1); + to.values().clear(); + + scheduler.advanceTimeBy(2, TimeUnit.SECONDS); + + ps.onNext(new int[0]); + + Thread.sleep(200); + System.gc(); + Thread.sleep(200); + + long after = memoryMXBean.getHeapMemoryUsage().getUsed(); + + to.dispose(); + + System.out.printf("Bounded Replay Leak check: After: %.3f MB%n", after / 1024.0 / 1024.0); + + if (initial + 100 * 1024 * 1024 < after) { + Assert.fail("Bounded Replay Leak check: Memory leak detected: " + (initial / 1024.0 / 1024.0) + + " -> " + after / 1024.0 / 1024.0); + } + } + + @Test + public void timeAndSizeSelectorBoundEagerTruncate() throws Exception { + + PublishSubject<int[]> ps = PublishSubject.create(); + + TestScheduler scheduler = new TestScheduler(); + + Observable<int[]> co = ps.replay(Functions.<Observable<int[]>>identity(), 1, 5, TimeUnit.SECONDS, scheduler, true); + + TestObserver<int[]> to = co.test(); + + Thread.sleep(200); + System.gc(); + Thread.sleep(200); + + final MemoryMXBean memoryMXBean = ManagementFactory.getMemoryMXBean(); + long initial = memoryMXBean.getHeapMemoryUsage().getUsed(); + + System.out.printf("Bounded Replay Leak check: Starting: %.3f MB%n", initial / 1024.0 / 1024.0); + + ps.onNext(new int[100 * 1024 * 1024]); + + to.assertValueCount(1); + to.values().clear(); + + scheduler.advanceTimeBy(2, TimeUnit.SECONDS); + + ps.onNext(new int[0]); + + Thread.sleep(200); + System.gc(); + Thread.sleep(200); + + long after = memoryMXBean.getHeapMemoryUsage().getUsed(); + + to.dispose(); + + System.out.printf("Bounded Replay Leak check: After: %.3f MB%n", after / 1024.0 / 1024.0); + + if (initial + 100 * 1024 * 1024 < after) { + Assert.fail("Bounded Replay Leak check: Memory leak detected: " + (initial / 1024.0 / 1024.0) + + " -> " + after / 1024.0 / 1024.0); + } + } + + @Test + public void timeAndSizeNoTerminalTruncationOnTimechange() { + Observable.just(1).replay(1, 1, TimeUnit.SECONDS, new TimesteppingScheduler(), true) + .autoConnect() + .test() + .assertComplete() + .assertNoErrors(); + } + + @Test + public void disposeNoNeedForResetSizeBound() { + PublishSubject<Integer> ps = PublishSubject.create(); + + ConnectableObservable<Integer> co = ps.replay(10, true); + + TestObserver<Integer> to = co.test(); + + Disposable d = co.connect(); + + ps.onNext(1); + + d.dispose(); + + to = co.test(); + + to.assertEmpty(); + + co.connect(); + + to.assertEmpty(); + + ps.onNext(2); + + to.assertValuesOnly(2); + } + + @Test + public void disposeNoNeedForResetTimeBound() { + PublishSubject<Integer> ps = PublishSubject.create(); + + ConnectableObservable<Integer> co = ps.replay(10, TimeUnit.MINUTES, Schedulers.single(), true); + + TestObserver<Integer> to = co.test(); + + Disposable d = co.connect(); + + ps.onNext(1); + + d.dispose(); + + to = co.test(); + + to.assertEmpty(); + + co.connect(); + + to.assertEmpty(); + + ps.onNext(2); + + to.assertValuesOnly(2); + } + + @Test + public void disposeNoNeedForResetTimeAndSIzeBound() { + PublishSubject<Integer> ps = PublishSubject.create(); + + ConnectableObservable<Integer> co = ps.replay(10, 10, TimeUnit.MINUTES, Schedulers.single(), true); + + TestObserver<Integer> to = co.test(); + + Disposable d = co.connect(); + + ps.onNext(1); + + d.dispose(); + + to = co.test(); + + to.assertEmpty(); + + co.connect(); + + to.assertEmpty(); + + ps.onNext(2); + + to.assertValuesOnly(2); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableReplayTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableReplayTest.java new file mode 100644 index 0000000000..8b4ef97d02 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableReplayTest.java @@ -0,0 +1,1754 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +import java.lang.management.*; +import java.util.*; +import java.util.concurrent.*; +import java.util.concurrent.atomic.*; + +import org.junit.*; +import org.mockito.InOrder; + +import io.reactivex.rxjava3.annotations.NonNull; +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.core.Observer; +import io.reactivex.rxjava3.core.Scheduler.Worker; +import io.reactivex.rxjava3.disposables.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.internal.fuseable.HasUpstreamObservableSource; +import io.reactivex.rxjava3.internal.operators.observable.ObservableReplay.*; +import io.reactivex.rxjava3.observables.ConnectableObservable; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.schedulers.*; +import io.reactivex.rxjava3.subjects.PublishSubject; +import io.reactivex.rxjava3.testsupport.*; + +public class ObservableReplayTest extends RxJavaTest { + @Test + public void bufferedReplay() { + PublishSubject<Integer> source = PublishSubject.create(); + + ConnectableObservable<Integer> co = source.replay(3); + co.connect(); + + { + Observer<Object> observer1 = TestHelper.mockObserver(); + InOrder inOrder = inOrder(observer1); + + co.subscribe(observer1); + + source.onNext(1); + source.onNext(2); + source.onNext(3); + + inOrder.verify(observer1, times(1)).onNext(1); + inOrder.verify(observer1, times(1)).onNext(2); + inOrder.verify(observer1, times(1)).onNext(3); + + source.onNext(4); + source.onComplete(); + inOrder.verify(observer1, times(1)).onNext(4); + inOrder.verify(observer1, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + verify(observer1, never()).onError(any(Throwable.class)); + + } + + { + Observer<Object> observer1 = TestHelper.mockObserver(); + InOrder inOrder = inOrder(observer1); + + co.subscribe(observer1); + + inOrder.verify(observer1, times(1)).onNext(2); + inOrder.verify(observer1, times(1)).onNext(3); + inOrder.verify(observer1, times(1)).onNext(4); + inOrder.verify(observer1, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + verify(observer1, never()).onError(any(Throwable.class)); + } + } + + @Test + public void bufferedWindowReplay() { + PublishSubject<Integer> source = PublishSubject.create(); + TestScheduler scheduler = new TestScheduler(); + ConnectableObservable<Integer> co = source.replay(3, 100, TimeUnit.MILLISECONDS, scheduler); + co.connect(); + + { + Observer<Object> observer1 = TestHelper.mockObserver(); + InOrder inOrder = inOrder(observer1); + + co.subscribe(observer1); + + source.onNext(1); + scheduler.advanceTimeBy(10, TimeUnit.MILLISECONDS); + source.onNext(2); + scheduler.advanceTimeBy(10, TimeUnit.MILLISECONDS); + source.onNext(3); + scheduler.advanceTimeBy(10, TimeUnit.MILLISECONDS); + + inOrder.verify(observer1, times(1)).onNext(1); + inOrder.verify(observer1, times(1)).onNext(2); + inOrder.verify(observer1, times(1)).onNext(3); + + source.onNext(4); + source.onNext(5); + scheduler.advanceTimeBy(90, TimeUnit.MILLISECONDS); + + inOrder.verify(observer1, times(1)).onNext(4); + + inOrder.verify(observer1, times(1)).onNext(5); + + inOrder.verifyNoMoreInteractions(); + verify(observer1, never()).onError(any(Throwable.class)); + + } + + { + Observer<Object> observer1 = TestHelper.mockObserver(); + InOrder inOrder = inOrder(observer1); + + co.subscribe(observer1); + + inOrder.verify(observer1, times(1)).onNext(4); + inOrder.verify(observer1, times(1)).onNext(5); + inOrder.verifyNoMoreInteractions(); + verify(observer1, never()).onError(any(Throwable.class)); + } + } + + @Test + public void windowedReplay() { + TestScheduler scheduler = new TestScheduler(); + + PublishSubject<Integer> source = PublishSubject.create(); + + ConnectableObservable<Integer> co = source.replay(100, TimeUnit.MILLISECONDS, scheduler); + co.connect(); + + { + Observer<Object> observer1 = TestHelper.mockObserver(); + InOrder inOrder = inOrder(observer1); + + co.subscribe(observer1); + + source.onNext(1); + scheduler.advanceTimeBy(60, TimeUnit.MILLISECONDS); + source.onNext(2); + scheduler.advanceTimeBy(60, TimeUnit.MILLISECONDS); + source.onNext(3); + scheduler.advanceTimeBy(60, TimeUnit.MILLISECONDS); + source.onComplete(); + scheduler.advanceTimeBy(60, TimeUnit.MILLISECONDS); + + inOrder.verify(observer1, times(1)).onNext(1); + inOrder.verify(observer1, times(1)).onNext(2); + inOrder.verify(observer1, times(1)).onNext(3); + + inOrder.verify(observer1, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + verify(observer1, never()).onError(any(Throwable.class)); + + } + { + Observer<Object> observer1 = TestHelper.mockObserver(); + InOrder inOrder = inOrder(observer1); + + co.subscribe(observer1); + inOrder.verify(observer1, never()).onNext(3); + + inOrder.verify(observer1, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + verify(observer1, never()).onError(any(Throwable.class)); + } + } + + @Test + public void replaySelector() { + final Function<Integer, Integer> dbl = new Function<Integer, Integer>() { + + @Override + public Integer apply(Integer t1) { + return t1 * 2; + } + + }; + + Function<Observable<Integer>, Observable<Integer>> selector = new Function<Observable<Integer>, Observable<Integer>>() { + + @Override + public Observable<Integer> apply(Observable<Integer> t1) { + return t1.map(dbl); + } + + }; + + PublishSubject<Integer> source = PublishSubject.create(); + + Observable<Integer> co = source.replay(selector); + + { + Observer<Object> observer1 = TestHelper.mockObserver(); + InOrder inOrder = inOrder(observer1); + + co.subscribe(observer1); + + source.onNext(1); + source.onNext(2); + source.onNext(3); + + inOrder.verify(observer1, times(1)).onNext(2); + inOrder.verify(observer1, times(1)).onNext(4); + inOrder.verify(observer1, times(1)).onNext(6); + + source.onNext(4); + source.onComplete(); + inOrder.verify(observer1, times(1)).onNext(8); + inOrder.verify(observer1, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + verify(observer1, never()).onError(any(Throwable.class)); + + } + + { + Observer<Object> observer1 = TestHelper.mockObserver(); + InOrder inOrder = inOrder(observer1); + + co.subscribe(observer1); + + inOrder.verify(observer1, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + verify(observer1, never()).onError(any(Throwable.class)); + + } + + } + + @Test + public void bufferedReplaySelector() { + + final Function<Integer, Integer> dbl = new Function<Integer, Integer>() { + + @Override + public Integer apply(Integer t1) { + return t1 * 2; + } + + }; + + Function<Observable<Integer>, Observable<Integer>> selector = new Function<Observable<Integer>, Observable<Integer>>() { + + @Override + public Observable<Integer> apply(Observable<Integer> t1) { + return t1.map(dbl); + } + + }; + + PublishSubject<Integer> source = PublishSubject.create(); + + Observable<Integer> co = source.replay(selector, 3); + + { + Observer<Object> observer1 = TestHelper.mockObserver(); + InOrder inOrder = inOrder(observer1); + + co.subscribe(observer1); + + source.onNext(1); + source.onNext(2); + source.onNext(3); + + inOrder.verify(observer1, times(1)).onNext(2); + inOrder.verify(observer1, times(1)).onNext(4); + inOrder.verify(observer1, times(1)).onNext(6); + + source.onNext(4); + source.onComplete(); + inOrder.verify(observer1, times(1)).onNext(8); + inOrder.verify(observer1, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + verify(observer1, never()).onError(any(Throwable.class)); + + } + + { + Observer<Object> observer1 = TestHelper.mockObserver(); + InOrder inOrder = inOrder(observer1); + + co.subscribe(observer1); + + inOrder.verify(observer1, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + verify(observer1, never()).onError(any(Throwable.class)); + } + } + + @Test + public void windowedReplaySelector() { + + final Function<Integer, Integer> dbl = new Function<Integer, Integer>() { + + @Override + public Integer apply(Integer t1) { + return t1 * 2; + } + + }; + + Function<Observable<Integer>, Observable<Integer>> selector = new Function<Observable<Integer>, Observable<Integer>>() { + + @Override + public Observable<Integer> apply(Observable<Integer> t1) { + return t1.map(dbl); + } + + }; + + TestScheduler scheduler = new TestScheduler(); + + PublishSubject<Integer> source = PublishSubject.create(); + + Observable<Integer> co = source.replay(selector, 100, TimeUnit.MILLISECONDS, scheduler); + + { + Observer<Object> observer1 = TestHelper.mockObserver(); + InOrder inOrder = inOrder(observer1); + + co.subscribe(observer1); + + source.onNext(1); + scheduler.advanceTimeBy(60, TimeUnit.MILLISECONDS); + source.onNext(2); + scheduler.advanceTimeBy(60, TimeUnit.MILLISECONDS); + source.onNext(3); + scheduler.advanceTimeBy(60, TimeUnit.MILLISECONDS); + source.onComplete(); + scheduler.advanceTimeBy(60, TimeUnit.MILLISECONDS); + + inOrder.verify(observer1, times(1)).onNext(2); + inOrder.verify(observer1, times(1)).onNext(4); + inOrder.verify(observer1, times(1)).onNext(6); + + inOrder.verify(observer1, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + verify(observer1, never()).onError(any(Throwable.class)); + + } + { + Observer<Object> observer1 = TestHelper.mockObserver(); + InOrder inOrder = inOrder(observer1); + + co.subscribe(observer1); + + inOrder.verify(observer1, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + verify(observer1, never()).onError(any(Throwable.class)); + } + } + + @Test + public void bufferedReplayError() { + PublishSubject<Integer> source = PublishSubject.create(); + + ConnectableObservable<Integer> co = source.replay(3); + co.connect(); + + { + Observer<Object> observer1 = TestHelper.mockObserver(); + InOrder inOrder = inOrder(observer1); + + co.subscribe(observer1); + + source.onNext(1); + source.onNext(2); + source.onNext(3); + + inOrder.verify(observer1, times(1)).onNext(1); + inOrder.verify(observer1, times(1)).onNext(2); + inOrder.verify(observer1, times(1)).onNext(3); + + source.onNext(4); + source.onError(new RuntimeException("Forced failure")); + + inOrder.verify(observer1, times(1)).onNext(4); + inOrder.verify(observer1, times(1)).onError(any(RuntimeException.class)); + inOrder.verifyNoMoreInteractions(); + verify(observer1, never()).onComplete(); + + } + + { + Observer<Object> observer1 = TestHelper.mockObserver(); + InOrder inOrder = inOrder(observer1); + + co.subscribe(observer1); + + inOrder.verify(observer1, times(1)).onNext(2); + inOrder.verify(observer1, times(1)).onNext(3); + inOrder.verify(observer1, times(1)).onNext(4); + inOrder.verify(observer1, times(1)).onError(any(RuntimeException.class)); + inOrder.verifyNoMoreInteractions(); + verify(observer1, never()).onComplete(); + } + } + + @Test + public void windowedReplayError() { + TestScheduler scheduler = new TestScheduler(); + + PublishSubject<Integer> source = PublishSubject.create(); + + ConnectableObservable<Integer> co = source.replay(100, TimeUnit.MILLISECONDS, scheduler); + co.connect(); + + { + Observer<Object> observer1 = TestHelper.mockObserver(); + InOrder inOrder = inOrder(observer1); + + co.subscribe(observer1); + + source.onNext(1); + scheduler.advanceTimeBy(60, TimeUnit.MILLISECONDS); + source.onNext(2); + scheduler.advanceTimeBy(60, TimeUnit.MILLISECONDS); + source.onNext(3); + scheduler.advanceTimeBy(60, TimeUnit.MILLISECONDS); + source.onError(new RuntimeException("Forced failure")); + scheduler.advanceTimeBy(60, TimeUnit.MILLISECONDS); + + inOrder.verify(observer1, times(1)).onNext(1); + inOrder.verify(observer1, times(1)).onNext(2); + inOrder.verify(observer1, times(1)).onNext(3); + + inOrder.verify(observer1, times(1)).onError(any(RuntimeException.class)); + inOrder.verifyNoMoreInteractions(); + verify(observer1, never()).onComplete(); + + } + { + Observer<Object> observer1 = TestHelper.mockObserver(); + InOrder inOrder = inOrder(observer1); + + co.subscribe(observer1); + inOrder.verify(observer1, never()).onNext(3); + + inOrder.verify(observer1, times(1)).onError(any(RuntimeException.class)); + inOrder.verifyNoMoreInteractions(); + verify(observer1, never()).onComplete(); + } + } + + @Test + public void synchronousDisconnect() { + final AtomicInteger effectCounter = new AtomicInteger(); + Observable<Integer> source = Observable.just(1, 2, 3, 4) + .doOnNext(new Consumer<Integer>() { + @Override + public void accept(Integer v) { + effectCounter.incrementAndGet(); + System.out.println("Sideeffect #" + v); + } + }); + + Observable<Integer> result = source.replay( + new Function<Observable<Integer>, Observable<Integer>>() { + @Override + public Observable<Integer> apply(Observable<Integer> o) { + return o.take(2); + } + }); + + for (int i = 1; i < 3; i++) { + effectCounter.set(0); + System.out.printf("- %d -%n", i); + result.subscribe(new Consumer<Integer>() { + + @Override + public void accept(Integer t1) { + System.out.println(t1); + } + + }, new Consumer<Throwable>() { + + @Override + public void accept(Throwable t1) { + t1.printStackTrace(); + } + }, + new Action() { + @Override + public void run() { + System.out.println("Done"); + } + }); + assertEquals(2, effectCounter.get()); + } + } + + /* + * test the basic expectation of OperatorMulticast via replay + */ + @SuppressWarnings("unchecked") + @Test + public void issue2191_UnsubscribeSource() throws Throwable { + // setup mocks + Consumer<Integer> sourceNext = mock(Consumer.class); + Action sourceCompleted = mock(Action.class); + Action sourceUnsubscribed = mock(Action.class); + Observer<Integer> spiedSubscriberBeforeConnect = TestHelper.mockObserver(); + Observer<Integer> spiedSubscriberAfterConnect = TestHelper.mockObserver(); + + // Observable under test + Observable<Integer> source = Observable.just(1, 2); + + ConnectableObservable<Integer> replay = source + .doOnNext(sourceNext) + .doOnDispose(sourceUnsubscribed) + .doOnComplete(sourceCompleted) + .replay(); + + replay.subscribe(spiedSubscriberBeforeConnect); + replay.subscribe(spiedSubscriberBeforeConnect); + replay.connect(); + replay.subscribe(spiedSubscriberAfterConnect); + replay.subscribe(spiedSubscriberAfterConnect); + + verify(spiedSubscriberBeforeConnect, times(2)).onSubscribe((Disposable)any()); + verify(spiedSubscriberAfterConnect, times(2)).onSubscribe((Disposable)any()); + + // verify interactions + verify(sourceNext, times(1)).accept(1); + verify(sourceNext, times(1)).accept(2); + verify(sourceCompleted, times(1)).run(); + verifyObserverMock(spiedSubscriberBeforeConnect, 2, 4); + verifyObserverMock(spiedSubscriberAfterConnect, 2, 4); + +// verify(sourceUnsubscribed, times(1)).run(); + + verifyNoMoreInteractions(sourceNext); + verifyNoMoreInteractions(sourceCompleted); + verifyNoMoreInteractions(sourceUnsubscribed); + verifyNoMoreInteractions(spiedSubscriberBeforeConnect); + verifyNoMoreInteractions(spiedSubscriberAfterConnect); + + } + + /** + * Specifically test interaction with a Scheduler with subscribeOn. + * + * @throws Throwable functional interfaces are declared with throws Exception + */ + @SuppressWarnings("unchecked") + @Test + public void issue2191_SchedulerUnsubscribe() throws Throwable { + // setup mocks + Consumer<Integer> sourceNext = mock(Consumer.class); + Action sourceCompleted = mock(Action.class); + Action sourceUnsubscribed = mock(Action.class); + final TestScheduler mockScheduler = new TestScheduler(); + + Observer<Integer> mockObserverBeforeConnect = TestHelper.mockObserver(); + Observer<Integer> mockObserverAfterConnect = TestHelper.mockObserver(); + + // Observable under test + ConnectableObservable<Integer> replay = Observable.just(1, 2, 3) + .doOnNext(sourceNext) + .doOnDispose(sourceUnsubscribed) + .doOnComplete(sourceCompleted) + .subscribeOn(mockScheduler).replay(); + + replay.subscribe(mockObserverBeforeConnect); + replay.connect(); + replay.subscribe(mockObserverAfterConnect); + + verify(mockObserverBeforeConnect).onSubscribe((Disposable)any()); + verify(mockObserverAfterConnect).onSubscribe((Disposable)any()); + + mockScheduler.advanceTimeBy(1, TimeUnit.SECONDS); + + // verify interactions + verify(sourceNext, times(1)).accept(1); + verify(sourceNext, times(1)).accept(2); + verify(sourceNext, times(1)).accept(3); + verify(sourceCompleted, times(1)).run(); + verifyObserverMock(mockObserverBeforeConnect, 1, 3); + verifyObserverMock(mockObserverAfterConnect, 1, 3); + + // FIXME not supported +// verify(spiedWorker, times(1)).isUnsubscribed(); + // FIXME publish calls cancel too +// verify(sourceUnsubscribed, times(1)).run(); + + verifyNoMoreInteractions(sourceNext); + verifyNoMoreInteractions(sourceCompleted); + verifyNoMoreInteractions(sourceUnsubscribed); + verifyNoMoreInteractions(mockObserverBeforeConnect); + verifyNoMoreInteractions(mockObserverAfterConnect); + } + + /** + * Specifically test interaction with a Scheduler with subscribeOn. + * + * @throws Throwable functional interfaces are declared with throws Exception + */ + @SuppressWarnings("unchecked") + @Test + public void issue2191_SchedulerUnsubscribeOnError() throws Throwable { + // setup mocks + Consumer<Integer> sourceNext = mock(Consumer.class); + Action sourceCompleted = mock(Action.class); + Consumer<Throwable> sourceError = mock(Consumer.class); + Action sourceUnsubscribed = mock(Action.class); + final TestScheduler mockScheduler = new TestScheduler(); + Observer<Integer> mockObserverBeforeConnect = TestHelper.mockObserver(); + Observer<Integer> mockObserverAfterConnect = TestHelper.mockObserver(); + + // Observable under test + Function<Integer, Integer> mockFunc = mock(Function.class); + IllegalArgumentException illegalArgumentException = new IllegalArgumentException(); + when(mockFunc.apply(1)).thenReturn(1); + when(mockFunc.apply(2)).thenThrow(illegalArgumentException); + ConnectableObservable<Integer> replay = Observable.just(1, 2, 3).map(mockFunc) + .doOnNext(sourceNext) + .doOnDispose(sourceUnsubscribed) + .doOnComplete(sourceCompleted) + .doOnError(sourceError) + .subscribeOn(mockScheduler).replay(); + + replay.subscribe(mockObserverBeforeConnect); + replay.connect(); + replay.subscribe(mockObserverAfterConnect); + + verify(mockObserverBeforeConnect).onSubscribe((Disposable)any()); + verify(mockObserverAfterConnect).onSubscribe((Disposable)any()); + + mockScheduler.advanceTimeBy(1, TimeUnit.SECONDS); + // verify interactions + verify(sourceNext, times(1)).accept(1); + verify(sourceError, times(1)).accept(illegalArgumentException); + verifyObserver(mockObserverBeforeConnect, 1, 1, illegalArgumentException); + verifyObserver(mockObserverAfterConnect, 1, 1, illegalArgumentException); + + // FIXME no longer supported +// verify(spiedWorker, times(1)).isUnsubscribed(); + // FIXME publish also calls cancel +// verify(sourceUnsubscribed, times(1)).run(); + + verifyNoMoreInteractions(sourceNext); + verifyNoMoreInteractions(sourceCompleted); + verifyNoMoreInteractions(sourceError); + verifyNoMoreInteractions(sourceUnsubscribed); + verifyNoMoreInteractions(mockObserverBeforeConnect); + verifyNoMoreInteractions(mockObserverAfterConnect); + } + + private static void verifyObserverMock(Observer<Integer> mock, int numSubscriptions, int numItemsExpected) { + verify(mock, times(numItemsExpected)).onNext((Integer) notNull()); + verify(mock, times(numSubscriptions)).onComplete(); + verifyNoMoreInteractions(mock); + } + + private static void verifyObserver(Observer<Integer> mock, int numSubscriptions, int numItemsExpected, Throwable error) { + verify(mock, times(numItemsExpected)).onNext((Integer) notNull()); + verify(mock, times(numSubscriptions)).onError(error); + verifyNoMoreInteractions(mock); + } + + public static Worker workerSpy(final Disposable mockDisposable) { + return spy(new InprocessWorker(mockDisposable)); + } + + static class InprocessWorker extends Worker { + private final Disposable mockDisposable; + public boolean unsubscribed; + + InprocessWorker(Disposable mockDisposable) { + this.mockDisposable = mockDisposable; + } + + @NonNull + @Override + public Disposable schedule(@NonNull Runnable action) { + action.run(); + return mockDisposable; // this subscription is returned but discarded + } + + @NonNull + @Override + public Disposable schedule(@NonNull Runnable action, long delayTime, @NonNull TimeUnit unit) { + action.run(); + return mockDisposable; + } + + @Override + public void dispose() { + unsubscribed = true; + } + + @Override + public boolean isDisposed() { + return unsubscribed; + } + } + + @Test + public void boundedReplayBuffer() { + BoundedReplayBuffer<Integer> buf = new BoundedReplayBuffer<Integer>(false) { + private static final long serialVersionUID = -5182053207244406872L; + + @Override + void truncate() { + } + }; + buf.addLast(new Node(1)); + buf.addLast(new Node(2)); + buf.addLast(new Node(3)); + buf.addLast(new Node(4)); + buf.addLast(new Node(5)); + + List<Integer> values = new ArrayList<>(); + buf.collect(values); + + Assert.assertEquals(Arrays.asList(1, 2, 3, 4, 5), values); + + buf.removeSome(2); + buf.removeFirst(); + buf.removeSome(2); + + values.clear(); + buf.collect(values); + Assert.assertTrue(values.isEmpty()); + + buf.addLast(new Node(5)); + buf.addLast(new Node(6)); + buf.collect(values); + + Assert.assertEquals(Arrays.asList(5, 6), values); + + } + + @Test + public void timedAndSizedTruncation() { + TestScheduler test = new TestScheduler(); + SizeAndTimeBoundReplayBuffer<Integer> buf = new SizeAndTimeBoundReplayBuffer<>(2, 2000, TimeUnit.MILLISECONDS, test, false); + List<Integer> values = new ArrayList<>(); + + buf.next(1); + test.advanceTimeBy(1, TimeUnit.SECONDS); + buf.next(2); + test.advanceTimeBy(1, TimeUnit.SECONDS); + buf.collect(values); + Assert.assertEquals(Arrays.asList(2), values); + + buf.next(3); + buf.next(4); + values.clear(); + buf.collect(values); + Assert.assertEquals(Arrays.asList(3, 4), values); + + test.advanceTimeBy(2, TimeUnit.SECONDS); + buf.next(5); + + values.clear(); + buf.collect(values); + Assert.assertEquals(Arrays.asList(5), values); + Assert.assertFalse(buf.hasCompleted()); + + test.advanceTimeBy(2, TimeUnit.SECONDS); + buf.complete(); + + values.clear(); + buf.collect(values); + Assert.assertTrue(values.isEmpty()); + + Assert.assertEquals(1, buf.size); + Assert.assertTrue(buf.hasCompleted()); + Assert.assertFalse(buf.hasError()); + } + + @Test + public void timedAndSizedTruncationError() { + TestScheduler test = new TestScheduler(); + SizeAndTimeBoundReplayBuffer<Integer> buf = new SizeAndTimeBoundReplayBuffer<>(2, 2000, TimeUnit.MILLISECONDS, test, false); + + Assert.assertFalse(buf.hasCompleted()); + Assert.assertFalse(buf.hasError()); + + List<Integer> values = new ArrayList<>(); + + buf.next(1); + test.advanceTimeBy(1, TimeUnit.SECONDS); + buf.next(2); + test.advanceTimeBy(1, TimeUnit.SECONDS); + buf.collect(values); + Assert.assertEquals(Arrays.asList(2), values); + + buf.next(3); + buf.next(4); + values.clear(); + buf.collect(values); + Assert.assertEquals(Arrays.asList(3, 4), values); + + test.advanceTimeBy(2, TimeUnit.SECONDS); + buf.next(5); + + values.clear(); + buf.collect(values); + Assert.assertEquals(Arrays.asList(5), values); + Assert.assertFalse(buf.hasCompleted()); + Assert.assertFalse(buf.hasError()); + + test.advanceTimeBy(2, TimeUnit.SECONDS); + buf.error(new TestException()); + + values.clear(); + buf.collect(values); + Assert.assertTrue(values.isEmpty()); + + Assert.assertEquals(1, buf.size); + Assert.assertFalse(buf.hasCompleted()); + Assert.assertTrue(buf.hasError()); + } + + @Test + public void sizedTruncation() { + SizeBoundReplayBuffer<Integer> buf = new SizeBoundReplayBuffer<>(2, false); + List<Integer> values = new ArrayList<>(); + + buf.next(1); + buf.next(2); + buf.collect(values); + Assert.assertEquals(Arrays.asList(1, 2), values); + + buf.next(3); + buf.next(4); + values.clear(); + buf.collect(values); + Assert.assertEquals(Arrays.asList(3, 4), values); + + buf.next(5); + + values.clear(); + buf.collect(values); + Assert.assertEquals(Arrays.asList(4, 5), values); + Assert.assertFalse(buf.hasCompleted()); + + buf.complete(); + + values.clear(); + buf.collect(values); + Assert.assertEquals(Arrays.asList(4, 5), values); + + Assert.assertEquals(3, buf.size); + Assert.assertTrue(buf.hasCompleted()); + Assert.assertFalse(buf.hasError()); + } + + @Test + public void coldReplayNoBackpressure() { + Observable<Integer> source = Observable.range(0, 1000).replay().autoConnect(); + + TestObserverEx<Integer> to = new TestObserverEx<>(); + + source.subscribe(to); + + to.assertNoErrors(); + to.assertTerminated(); + List<Integer> onNextEvents = to.values(); + assertEquals(1000, onNextEvents.size()); + + for (int i = 0; i < 1000; i++) { + assertEquals((Integer)i, onNextEvents.get(i)); + } + } + + @Test + public void cache() throws InterruptedException { + final AtomicInteger counter = new AtomicInteger(); + Observable<String> o = Observable.unsafeCreate(new ObservableSource<String>() { + + @Override + public void subscribe(final Observer<? super String> observer) { + observer.onSubscribe(Disposable.empty()); + new Thread(new Runnable() { + + @Override + public void run() { + counter.incrementAndGet(); + System.out.println("published Observable being executed"); + observer.onNext("one"); + observer.onComplete(); + } + }).start(); + } + }).replay().autoConnect(); + + // we then expect the following 2 subscriptions to get that same value + final CountDownLatch latch = new CountDownLatch(2); + + // subscribe once + o.subscribe(new Consumer<String>() { + + @Override + public void accept(String v) { + assertEquals("one", v); + System.out.println("v: " + v); + latch.countDown(); + } + }); + + // subscribe again + o.subscribe(new Consumer<String>() { + + @Override + public void accept(String v) { + assertEquals("one", v); + System.out.println("v: " + v); + latch.countDown(); + } + }); + + if (!latch.await(1000, TimeUnit.MILLISECONDS)) { + fail("subscriptions did not receive values"); + } + assertEquals(1, counter.get()); + } + + @Test + public void unsubscribeSource() throws Throwable { + Action unsubscribe = mock(Action.class); + Observable<Integer> o = Observable.just(1).doOnDispose(unsubscribe).replay().autoConnect(); + o.subscribe(); + o.subscribe(); + o.subscribe(); + verify(unsubscribe, never()).run(); + } + + @Test + public void take() { + TestObserverEx<Integer> to = new TestObserverEx<>(); + + Observable<Integer> cached = Observable.range(1, 100).replay().autoConnect(); + cached.take(10).subscribe(to); + + to.assertNoErrors(); + to.assertTerminated(); + to.assertValues(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + // FIXME no longer assertable +// ts.assertUnsubscribed(); + } + + @Test + public void async() { + Observable<Integer> source = Observable.range(1, 10000); + for (int i = 0; i < 100; i++) { + TestObserverEx<Integer> to1 = new TestObserverEx<>(); + + Observable<Integer> cached = source.replay().autoConnect(); + + cached.observeOn(Schedulers.computation()).subscribe(to1); + + to1.awaitDone(2, TimeUnit.SECONDS); + to1.assertNoErrors(); + to1.assertTerminated(); + assertEquals(10000, to1.values().size()); + + TestObserverEx<Integer> to2 = new TestObserverEx<>(); + cached.observeOn(Schedulers.computation()).subscribe(to2); + + to2.awaitDone(2, TimeUnit.SECONDS); + to2.assertNoErrors(); + to2.assertTerminated(); + assertEquals(10000, to2.values().size()); + } + } + + @Test + public void asyncComeAndGo() { + Observable<Long> source = Observable.interval(1, 1, TimeUnit.MILLISECONDS) + .take(1000) + .subscribeOn(Schedulers.io()); + Observable<Long> cached = source.replay().autoConnect(); + + Observable<Long> output = cached.observeOn(Schedulers.computation()); + + List<TestObserverEx<Long>> list = new ArrayList<>(100); + for (int i = 0; i < 100; i++) { + TestObserverEx<Long> to = new TestObserverEx<>(); + list.add(to); + output.skip(i * 10).take(10).subscribe(to); + } + + List<Long> expected = new ArrayList<>(); + for (int i = 0; i < 10; i++) { + expected.add((long)(i - 10)); + } + int j = 0; + for (TestObserverEx<Long> to : list) { + to.awaitDone(3, TimeUnit.SECONDS); + to.assertNoErrors(); + to.assertTerminated(); + + for (int i = j * 10; i < j * 10 + 10; i++) { + expected.set(i - j * 10, (long)i); + } + + to.assertValueSequence(expected); + + j++; + } + } + + @Test + public void noMissingBackpressureException() { + final int m = 4 * 1000 * 1000; + Observable<Integer> firehose = Observable.unsafeCreate(new ObservableSource<Integer>() { + @Override + public void subscribe(Observer<? super Integer> t) { + t.onSubscribe(Disposable.empty()); + for (int i = 0; i < m; i++) { + t.onNext(i); + } + t.onComplete(); + } + }); + + TestObserverEx<Integer> to = new TestObserverEx<>(); + firehose.replay().autoConnect().observeOn(Schedulers.computation()).takeLast(100).subscribe(to); + + to.awaitDone(3, TimeUnit.SECONDS); + to.assertNoErrors(); + to.assertTerminated(); + + assertEquals(100, to.values().size()); + } + + @Test + public void valuesAndThenError() { + Observable<Integer> source = Observable.range(1, 10) + .concatWith(Observable.<Integer>error(new TestException())) + .replay().autoConnect(); + + TestObserverEx<Integer> to = new TestObserverEx<>(); + source.subscribe(to); + + to.assertValues(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + to.assertNotComplete(); + Assert.assertEquals(1, to.errors().size()); + + TestObserverEx<Integer> to2 = new TestObserverEx<>(); + source.subscribe(to2); + + to2.assertValues(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + to2.assertNotComplete(); + Assert.assertEquals(1, to2.errors().size()); + } + + @Test + public void replayTime() { + Observable.just(1).replay(1, TimeUnit.MINUTES) + .autoConnect() + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(1); + } + + @Test + public void replaySizeAndTime() { + Observable.just(1).replay(1, 1, TimeUnit.MILLISECONDS) + .autoConnect() + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(1); + } + + @Test + public void replaySelectorTime() { + Observable.just(1).replay(Functions.<Observable<Integer>>identity(), 1, TimeUnit.MINUTES) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(1); + } + + @Test + public void replayMaxInt() { + Observable.range(1, 2) + .replay(Integer.MAX_VALUE) + .autoConnect() + .test() + .assertResult(1, 2); + } + + @Test + public void source() { + Observable<Integer> source = Observable.range(1, 3); + + assertSame(source, (((HasUpstreamObservableSource<?>)source.replay())).source()); + } + + @Test + public void connectRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final ConnectableObservable<Integer> co = Observable.range(1, 3).replay(); + + Runnable r = new Runnable() { + @Override + public void run() { + co.connect(); + } + }; + + TestHelper.race(r, r); + } + } + + @Test + public void subscribeRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final ConnectableObservable<Integer> co = Observable.range(1, 3).replay(); + + final TestObserver<Integer> to1 = new TestObserver<>(); + final TestObserver<Integer> to2 = new TestObserver<>(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + co.subscribe(to1); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + co.subscribe(to2); + } + }; + + TestHelper.race(r1, r2); + } + } + + @Test + public void addRemoveRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final ConnectableObservable<Integer> co = Observable.range(1, 3).replay(); + + final TestObserver<Integer> to1 = new TestObserver<>(); + final TestObserver<Integer> to2 = new TestObserver<>(); + + co.subscribe(to1); + + Runnable r1 = new Runnable() { + @Override + public void run() { + to1.dispose(); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + co.subscribe(to2); + } + }; + + TestHelper.race(r1, r2); + } + } + + @Test + public void cancelOnArrival() { + Observable.range(1, 2) + .replay(Integer.MAX_VALUE) + .autoConnect() + .test(true) + .assertEmpty(); + } + + @Test + public void cancelOnArrival2() { + ConnectableObservable<Integer> co = PublishSubject.<Integer>create() + .replay(Integer.MAX_VALUE); + + co.test(); + + co + .autoConnect() + .test(true) + .assertEmpty(); + } + + @Test + public void connectConsumerThrows() { + ConnectableObservable<Integer> co = Observable.range(1, 2) + .replay(); + + try { + co.connect(new Consumer<Disposable>() { + @Override + public void accept(Disposable t) throws Exception { + throw new TestException(); + } + }); + fail("Should have thrown"); + } catch (TestException ex) { + // expected + } + + co.test().assertEmpty().dispose(); + + co.connect(); + + co.test().assertResult(1, 2); + } + + @Test + public void badSource() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + new Observable<Integer>() { + @Override + protected void subscribeActual(Observer<? super Integer> observer) { + observer.onSubscribe(Disposable.empty()); + observer.onError(new TestException("First")); + observer.onNext(1); + observer.onError(new TestException("Second")); + observer.onComplete(); + } + }.replay() + .autoConnect() + .to(TestHelper.<Integer>testConsumer()) + .assertFailureAndMessage(TestException.class, "First"); + + TestHelper.assertUndeliverable(errors, 0, TestException.class, "Second"); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void subscribeOnNextRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final PublishSubject<Integer> ps = PublishSubject.create(); + + final ConnectableObservable<Integer> co = ps.replay(); + + final TestObserver<Integer> to1 = new TestObserver<>(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + co.subscribe(to1); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + for (int j = 0; j < 1000; j++) { + ps.onNext(j); + } + } + }; + + TestHelper.race(r1, r2); + } + } + + @Test + public void unsubscribeOnNextRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final PublishSubject<Integer> ps = PublishSubject.create(); + + final ConnectableObservable<Integer> co = ps.replay(); + + final TestObserver<Integer> to1 = new TestObserver<>(); + + co.subscribe(to1); + + Runnable r1 = new Runnable() { + @Override + public void run() { + to1.dispose(); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + for (int j = 0; j < 1000; j++) { + ps.onNext(j); + } + } + }; + + TestHelper.race(r1, r2); + } + } + + @Test + public void unsubscribeReplayRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final ConnectableObservable<Integer> co = Observable.range(1, 1000).replay(); + + final TestObserver<Integer> to1 = new TestObserver<>(); + + co.connect(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + co.subscribe(to1); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + to1.dispose(); + } + }; + + TestHelper.race(r1, r2); + } + } + + @Test + public void reentrantOnNext() { + final PublishSubject<Integer> ps = PublishSubject.create(); + + TestObserver<Integer> to = new TestObserver<Integer>() { + @Override + public void onNext(Integer t) { + if (t == 1) { + ps.onNext(2); + ps.onComplete(); + } + super.onNext(t); + } + }; + + ps.replay().autoConnect().subscribe(to); + + ps.onNext(1); + + to.assertResult(1, 2); + } + + @Test + public void reentrantOnNextBound() { + final PublishSubject<Integer> ps = PublishSubject.create(); + + TestObserver<Integer> to = new TestObserver<Integer>() { + @Override + public void onNext(Integer t) { + if (t == 1) { + ps.onNext(2); + ps.onComplete(); + } + super.onNext(t); + } + }; + + ps.replay(10).autoConnect().subscribe(to); + + ps.onNext(1); + + to.assertResult(1, 2); + } + + @Test + public void reentrantOnNextCancel() { + final PublishSubject<Integer> ps = PublishSubject.create(); + + TestObserver<Integer> to = new TestObserver<Integer>() { + @Override + public void onNext(Integer t) { + if (t == 1) { + ps.onNext(2); + dispose(); + } + super.onNext(t); + } + }; + + ps.replay().autoConnect().subscribe(to); + + ps.onNext(1); + + to.assertValues(1); + } + + @Test + public void reentrantOnNextCancelBounded() { + final PublishSubject<Integer> ps = PublishSubject.create(); + + TestObserver<Integer> to = new TestObserver<Integer>() { + @Override + public void onNext(Integer t) { + if (t == 1) { + ps.onNext(2); + dispose(); + } + super.onNext(t); + } + }; + + ps.replay(10).autoConnect().subscribe(to); + + ps.onNext(1); + + to.assertValues(1); + } + + @Test + public void delayedUpstreamOnSubscribe() { + final Observer<?>[] sub = { null }; + + new Observable<Integer>() { + @Override + protected void subscribeActual(Observer<? super Integer> observer) { + sub[0] = observer; + } + } + .replay() + .connect() + .dispose(); + + Disposable bs = Disposable.empty(); + + sub[0].onSubscribe(bs); + + assertTrue(bs.isDisposed()); + } + + @Test + public void timedNoOutdatedData() { + TestScheduler scheduler = new TestScheduler(); + + Observable<Integer> source = Observable.just(1) + .replay(2, TimeUnit.SECONDS, scheduler) + .autoConnect(); + + source.test().assertResult(1); + + source.test().assertResult(1); + + scheduler.advanceTimeBy(3, TimeUnit.SECONDS); + + source.test().assertResult(); + } + + @Test + public void replaySelectorReturnsNull() { + Observable.just(1) + .replay(new Function<Observable<Integer>, Observable<Object>>() { + @Override + public Observable<Object> apply(Observable<Integer> v) throws Exception { + return null; + } + }) + .to(TestHelper.<Object>testConsumer()) + .assertFailureAndMessage(NullPointerException.class, "The selector returned a null ObservableSource"); + } + + @Test + public void replaySelectorConnectableReturnsNull() { + ObservableReplay.multicastSelector(Functions.justSupplier((ConnectableObservable<Integer>)null), Functions.justFunction(Observable.just(1))) + .to(TestHelper.<Integer>testConsumer()) + .assertFailureAndMessage(NullPointerException.class, "The connectableFactory returned a null ConnectableObservable"); + } + + @Test + public void noHeadRetentionCompleteSize() { + PublishSubject<Integer> source = PublishSubject.create(); + + ObservableReplay<Integer> co = (ObservableReplay<Integer>)source + .replay(1); + + co.connect(); + + BoundedReplayBuffer<Integer> buf = (BoundedReplayBuffer<Integer>)(co.current.get().buffer); + + source.onNext(1); + source.onNext(2); + source.onComplete(); + + assertNull(buf.get().value); + + Object o = buf.get(); + + buf.trimHead(); + + assertSame(o, buf.get()); + } + + @Test + public void noHeadRetentionErrorSize() { + PublishSubject<Integer> source = PublishSubject.create(); + + ObservableReplay<Integer> co = (ObservableReplay<Integer>)source + .replay(1); + + co.connect(); + + BoundedReplayBuffer<Integer> buf = (BoundedReplayBuffer<Integer>)(co.current.get().buffer); + + source.onNext(1); + source.onNext(2); + source.onError(new TestException()); + + assertNull(buf.get().value); + + Object o = buf.get(); + + buf.trimHead(); + + assertSame(o, buf.get()); + } + + @Test + public void noHeadRetentionSize() { + PublishSubject<Integer> source = PublishSubject.create(); + + ObservableReplay<Integer> co = (ObservableReplay<Integer>)source + .replay(1); + + co.connect(); + + BoundedReplayBuffer<Integer> buf = (BoundedReplayBuffer<Integer>)(co.current.get().buffer); + + source.onNext(1); + source.onNext(2); + + assertNotNull(buf.get().value); + + buf.trimHead(); + + assertNull(buf.get().value); + + Object o = buf.get(); + + buf.trimHead(); + + assertSame(o, buf.get()); + } + + @Test + public void noHeadRetentionCompleteTime() { + PublishSubject<Integer> source = PublishSubject.create(); + + ObservableReplay<Integer> co = (ObservableReplay<Integer>)source + .replay(1, TimeUnit.MINUTES, Schedulers.computation()); + + co.connect(); + + BoundedReplayBuffer<Integer> buf = (BoundedReplayBuffer<Integer>)(co.current.get().buffer); + + source.onNext(1); + source.onNext(2); + source.onComplete(); + + assertNull(buf.get().value); + + Object o = buf.get(); + + buf.trimHead(); + + assertSame(o, buf.get()); + } + + @Test + public void noHeadRetentionErrorTime() { + PublishSubject<Integer> source = PublishSubject.create(); + + ObservableReplay<Integer> co = (ObservableReplay<Integer>)source + .replay(1, TimeUnit.MINUTES, Schedulers.computation()); + + co.connect(); + + BoundedReplayBuffer<Integer> buf = (BoundedReplayBuffer<Integer>)(co.current.get().buffer); + + source.onNext(1); + source.onNext(2); + source.onError(new TestException()); + + assertNull(buf.get().value); + + Object o = buf.get(); + + buf.trimHead(); + + assertSame(o, buf.get()); + } + + @Test + public void noHeadRetentionTime() { + TestScheduler sch = new TestScheduler(); + + PublishSubject<Integer> source = PublishSubject.create(); + + ObservableReplay<Integer> co = (ObservableReplay<Integer>)source + .replay(1, TimeUnit.MILLISECONDS, sch); + + co.connect(); + + BoundedReplayBuffer<Integer> buf = (BoundedReplayBuffer<Integer>)(co.current.get().buffer); + + source.onNext(1); + + sch.advanceTimeBy(2, TimeUnit.MILLISECONDS); + + source.onNext(2); + + assertNotNull(buf.get().value); + + buf.trimHead(); + + assertNull(buf.get().value); + + Object o = buf.get(); + + buf.trimHead(); + + assertSame(o, buf.get()); + } + + @Test + public void noBoundedRetentionViaThreadLocal() throws Exception { + Observable<byte[]> source = Observable.range(1, 200) + .map(new Function<Integer, byte[]>() { + @Override + public byte[] apply(Integer v) throws Exception { + return new byte[1024 * 1024]; + } + }) + .replay(new Function<Observable<byte[]>, Observable<byte[]>>() { + @Override + public Observable<byte[]> apply(final Observable<byte[]> o) throws Exception { + return o.take(1) + .concatMap(new Function<byte[], Observable<byte[]>>() { + @Override + public Observable<byte[]> apply(byte[] v) throws Exception { + return o; + } + }); + } + }, 1) + .takeLast(1) + ; + + System.out.println("Bounded Replay Leak check: Wait before GC"); + Thread.sleep(1000); + + System.out.println("Bounded Replay Leak check: GC"); + System.gc(); + + Thread.sleep(500); + + final MemoryMXBean memoryMXBean = ManagementFactory.getMemoryMXBean(); + MemoryUsage memHeap = memoryMXBean.getHeapMemoryUsage(); + long initial = memHeap.getUsed(); + + System.out.printf("Bounded Replay Leak check: Starting: %.3f MB%n", initial / 1024.0 / 1024.0); + + final AtomicLong after = new AtomicLong(); + + source.subscribe(new Consumer<byte[]>() { + @Override + public void accept(byte[] v) throws Exception { + System.out.println("Bounded Replay Leak check: Wait before GC 2"); + Thread.sleep(1000); + + System.out.println("Bounded Replay Leak check: GC 2"); + System.gc(); + + Thread.sleep(500); + + after.set(memoryMXBean.getHeapMemoryUsage().getUsed()); + } + }); + + System.out.printf("Bounded Replay Leak check: After: %.3f MB%n", after.get() / 1024.0 / 1024.0); + + if (initial + 100 * 1024 * 1024 < after.get()) { + Assert.fail("Bounded Replay Leak check: Memory leak detected: " + (initial / 1024.0 / 1024.0) + + " -> " + after.get() / 1024.0 / 1024.0); + } + } + + @Test(expected = TestException.class) + public void connectDisposeCrash() { + ConnectableObservable<Object> co = Observable.never().replay(); + + co.connect(); + + co.connect(d -> { throw new TestException(); }); + } + + @Test + public void resetWhileNotConnectedIsNoOp() { + ConnectableObservable<Object> co = Observable.never().replay(); + + co.reset(); + } + + @Test + public void resetWhileActiveIsNoOp() { + ConnectableObservable<Object> co = Observable.never().replay(); + + co.connect(); + + co.reset(); + } + + @Test + public void disposeNoNeedForReset() { + PublishSubject<Integer> ps = PublishSubject.create(); + + ConnectableObservable<Integer> co = ps.replay(); + + TestObserver<Integer> to = co.test(); + + Disposable d = co.connect(); + + ps.onNext(1); + + d.dispose(); + + to = co.test(); + + to.assertEmpty(); + + co.connect(); + + to.assertEmpty(); + + ps.onNext(2); + + to.assertValuesOnly(2); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableResourceWrapperTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableResourceWrapperTest.java new file mode 100644 index 0000000000..42d557e091 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableResourceWrapperTest.java @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import static org.junit.Assert.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.RxJavaTest; +import io.reactivex.rxjava3.disposables.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class ObservableResourceWrapperTest extends RxJavaTest { + + @Test + public void disposed() { + TestObserver<Object> to = new TestObserver<>(); + ObserverResourceWrapper<Object> orw = new ObserverResourceWrapper<>(to); + + Disposable d = Disposable.empty(); + + orw.onSubscribe(d); + + assertFalse(orw.isDisposed()); + + orw.dispose(); + + assertTrue(orw.isDisposed()); + } + + @Test + public void doubleOnSubscribe() { + TestObserver<Object> to = new TestObserver<>(); + ObserverResourceWrapper<Object> orw = new ObserverResourceWrapper<>(to); + + TestHelper.doubleOnSubscribe(orw); + } + + @Test + public void onErrorDisposes() { + TestObserver<Object> to = new TestObserver<>(); + ObserverResourceWrapper<Object> orw = new ObserverResourceWrapper<>(to); + + Disposable d = Disposable.empty(); + Disposable d1 = Disposable.empty(); + + orw.setResource(d1); + + orw.onSubscribe(d); + + orw.onError(new TestException()); + + assertTrue(d1.isDisposed()); + + to.assertFailure(TestException.class); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableRetryTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableRetryTest.java new file mode 100644 index 0000000000..13f19fb547 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableRetryTest.java @@ -0,0 +1,1196 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +import java.util.*; +import java.util.concurrent.*; +import java.util.concurrent.atomic.*; + +import org.junit.Test; +import org.mockito.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.core.Observer; +import io.reactivex.rxjava3.disposables.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.internal.subscriptions.BooleanSubscription; +import io.reactivex.rxjava3.observables.GroupedObservable; +import io.reactivex.rxjava3.observers.*; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.schedulers.Schedulers; +import io.reactivex.rxjava3.subjects.PublishSubject; +import io.reactivex.rxjava3.testsupport.*; + +public class ObservableRetryTest extends RxJavaTest { + + @Test + public void iterativeBackoff() { + Observer<String> consumer = TestHelper.mockObserver(); + + Observable<String> producer = Observable.unsafeCreate(new ObservableSource<String>() { + + private AtomicInteger count = new AtomicInteger(4); + long last = System.currentTimeMillis(); + + @Override + public void subscribe(Observer<? super String> t1) { + t1.onSubscribe(Disposable.empty()); + System.out.println(count.get() + " @ " + String.valueOf(last - System.currentTimeMillis())); + last = System.currentTimeMillis(); + if (count.getAndDecrement() == 0) { + t1.onNext("hello"); + t1.onComplete(); + } else { + t1.onError(new RuntimeException()); + } + } + + }); + TestObserver<String> to = new TestObserver<>(consumer); + producer.retryWhen(new Function<Observable<? extends Throwable>, Observable<Object>>() { + + @Override + public Observable<Object> apply(Observable<? extends Throwable> attempts) { + // Worker w = Schedulers.computation().createWorker(); + return attempts + .map(new Function<Throwable, Tuple>() { + @Override + public Tuple apply(Throwable n) { + return new Tuple(1L, n); + }}) + .scan(new BiFunction<Tuple, Tuple, Tuple>() { + @Override + public Tuple apply(Tuple t, Tuple n) { + return new Tuple(t.count + n.count, n.n); + }}) + .flatMap(new Function<Tuple, Observable<Long>>() { + @Override + public Observable<Long> apply(Tuple t) { + System.out.println("Retry # " + t.count); + return t.count > 20 ? + Observable.<Long>error(t.n) : + Observable.timer(t.count * 1L, TimeUnit.MILLISECONDS); + }}).cast(Object.class); + } + }).subscribe(to); + to.awaitDone(5, TimeUnit.SECONDS); + to.assertNoErrors(); + + InOrder inOrder = inOrder(consumer); + inOrder.verify(consumer, never()).onError(any(Throwable.class)); + inOrder.verify(consumer, times(1)).onNext("hello"); + inOrder.verify(consumer, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + + } + + public static class Tuple { + Long count; + Throwable n; + + Tuple(Long c, Throwable n) { + count = c; + this.n = n; + } + } + + @Test + public void retryIndefinitely() { + Observer<String> observer = TestHelper.mockObserver(); + int numRetries = 20; + Observable<String> origin = Observable.unsafeCreate(new FuncWithErrors(numRetries)); + origin.retry().subscribe(new TestObserver<>(observer)); + + InOrder inOrder = inOrder(observer); + // should show 3 attempts + inOrder.verify(observer, times(numRetries + 1)).onNext("beginningEveryTime"); + // should have no errors + inOrder.verify(observer, never()).onError(any(Throwable.class)); + // should have a single success + inOrder.verify(observer, times(1)).onNext("onSuccessOnly"); + // should have a single successful onComplete + inOrder.verify(observer, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void schedulingNotificationHandler() { + Observer<String> observer = TestHelper.mockObserver(); + int numRetries = 2; + Observable<String> origin = Observable.unsafeCreate(new FuncWithErrors(numRetries)); + TestObserver<String> to = new TestObserver<>(observer); + origin.retryWhen(new Function<Observable<? extends Throwable>, Observable<Object>>() { + @Override + public Observable<Object> apply(Observable<? extends Throwable> t1) { + return t1 + .observeOn(Schedulers.computation()) + .map(new Function<Throwable, Object>() { + @Override + public Object apply(Throwable t1) { + return 1; + } + }).startWithItem(1); + } + }) + .doOnError(new Consumer<Throwable>() { + @Override + public void accept(Throwable e) { + e.printStackTrace(); + } + }) + .subscribe(to); + + to.awaitDone(5, TimeUnit.SECONDS); + InOrder inOrder = inOrder(observer); + // should show 3 attempts + inOrder.verify(observer, times(1 + numRetries)).onNext("beginningEveryTime"); + // should have no errors + inOrder.verify(observer, never()).onError(any(Throwable.class)); + // should have a single success + inOrder.verify(observer, times(1)).onNext("onSuccessOnly"); + // should have a single successful onComplete + inOrder.verify(observer, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void onNextFromNotificationHandler() { + Observer<String> observer = TestHelper.mockObserver(); + int numRetries = 2; + Observable<String> origin = Observable.unsafeCreate(new FuncWithErrors(numRetries)); + origin.retryWhen(new Function<Observable<? extends Throwable>, Observable<Object>>() { + @Override + public Observable<Object> apply(Observable<? extends Throwable> t1) { + return t1.map(new Function<Throwable, Integer>() { + + @Override + public Integer apply(Throwable t1) { + return 0; + } + }).startWithItem(0).cast(Object.class); + } + }).subscribe(observer); + + InOrder inOrder = inOrder(observer); + // should show 3 attempts + inOrder.verify(observer, times(numRetries + 1)).onNext("beginningEveryTime"); + // should have no errors + inOrder.verify(observer, never()).onError(any(Throwable.class)); + // should have a single success + inOrder.verify(observer, times(1)).onNext("onSuccessOnly"); + // should have a single successful onComplete + inOrder.verify(observer, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void onCompletedFromNotificationHandler() { + Observer<String> observer = TestHelper.mockObserver(); + Observable<String> origin = Observable.unsafeCreate(new FuncWithErrors(1)); + TestObserver<String> to = new TestObserver<>(observer); + origin.retryWhen(new Function<Observable<? extends Throwable>, Observable<?>>() { + @Override + public Observable<?> apply(Observable<? extends Throwable> t1) { + return Observable.empty(); + } + }).subscribe(to); + + InOrder inOrder = inOrder(observer); + inOrder.verify(observer).onSubscribe((Disposable)notNull()); + inOrder.verify(observer, never()).onNext("beginningEveryTime"); + inOrder.verify(observer, never()).onNext("onSuccessOnly"); + inOrder.verify(observer, times(1)).onComplete(); + inOrder.verify(observer, never()).onError(any(Exception.class)); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void onErrorFromNotificationHandler() { + Observer<String> observer = TestHelper.mockObserver(); + Observable<String> origin = Observable.unsafeCreate(new FuncWithErrors(2)); + origin.retryWhen(new Function<Observable<? extends Throwable>, Observable<?>>() { + @Override + public Observable<?> apply(Observable<? extends Throwable> t1) { + return Observable.error(new RuntimeException()); + } + }).subscribe(observer); + + InOrder inOrder = inOrder(observer); + inOrder.verify(observer).onSubscribe((Disposable)notNull()); + inOrder.verify(observer, never()).onNext("beginningEveryTime"); + inOrder.verify(observer, never()).onNext("onSuccessOnly"); + inOrder.verify(observer, never()).onComplete(); + inOrder.verify(observer, times(1)).onError(any(RuntimeException.class)); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void singleSubscriptionOnFirst() throws Exception { + final AtomicInteger inc = new AtomicInteger(0); + ObservableSource<Integer> onSubscribe = new ObservableSource<Integer>() { + @Override + public void subscribe(Observer<? super Integer> observer) { + observer.onSubscribe(Disposable.empty()); + final int emit = inc.incrementAndGet(); + observer.onNext(emit); + observer.onComplete(); + } + }; + + int first = Observable.unsafeCreate(onSubscribe) + .retryWhen(new Function<Observable<? extends Throwable>, Observable<?>>() { + @Override + public Observable<?> apply(Observable<? extends Throwable> attempt) { + return attempt.zipWith(Observable.just(1), new BiFunction<Throwable, Integer, Void>() { + @Override + public Void apply(Throwable o, Integer integer) { + return null; + } + }); + } + }) + .blockingFirst(); + + assertEquals("Observer did not receive the expected output", 1, first); + assertEquals("Subscribe was not called once", 1, inc.get()); + } + + @Test + public void originFails() { + Observer<String> observer = TestHelper.mockObserver(); + Observable<String> origin = Observable.unsafeCreate(new FuncWithErrors(1)); + origin.subscribe(observer); + + InOrder inOrder = inOrder(observer); + inOrder.verify(observer, times(1)).onNext("beginningEveryTime"); + inOrder.verify(observer, times(1)).onError(any(RuntimeException.class)); + inOrder.verify(observer, never()).onNext("onSuccessOnly"); + inOrder.verify(observer, never()).onComplete(); + } + + @Test + public void retryFail() { + int numRetries = 1; + int numFailures = 2; + Observer<String> observer = TestHelper.mockObserver(); + Observable<String> origin = Observable.unsafeCreate(new FuncWithErrors(numFailures)); + origin.retry(numRetries).subscribe(observer); + + InOrder inOrder = inOrder(observer); + // should show 2 attempts (first time fail, second time (1st retry) fail) + inOrder.verify(observer, times(1 + numRetries)).onNext("beginningEveryTime"); + // should only retry once, fail again and emit onError + inOrder.verify(observer, times(1)).onError(any(RuntimeException.class)); + // no success + inOrder.verify(observer, never()).onNext("onSuccessOnly"); + inOrder.verify(observer, never()).onComplete(); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void retrySuccess() { + int numFailures = 1; + Observer<String> observer = TestHelper.mockObserver(); + Observable<String> origin = Observable.unsafeCreate(new FuncWithErrors(numFailures)); + origin.retry(3).subscribe(observer); + + InOrder inOrder = inOrder(observer); + // should show 3 attempts + inOrder.verify(observer, times(1 + numFailures)).onNext("beginningEveryTime"); + // should have no errors + inOrder.verify(observer, never()).onError(any(Throwable.class)); + // should have a single success + inOrder.verify(observer, times(1)).onNext("onSuccessOnly"); + // should have a single successful onComplete + inOrder.verify(observer, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void infiniteRetry() { + int numFailures = 20; + Observer<String> observer = TestHelper.mockObserver(); + Observable<String> origin = Observable.unsafeCreate(new FuncWithErrors(numFailures)); + origin.retry().subscribe(observer); + + InOrder inOrder = inOrder(observer); + // should show 3 attempts + inOrder.verify(observer, times(1 + numFailures)).onNext("beginningEveryTime"); + // should have no errors + inOrder.verify(observer, never()).onError(any(Throwable.class)); + // should have a single success + inOrder.verify(observer, times(1)).onNext("onSuccessOnly"); + // should have a single successful onComplete + inOrder.verify(observer, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + } + + /* + * Checks in a simple and synchronous way that retry resubscribes + * after error. This test fails against 0.16.1-0.17.4, hangs on 0.17.5 and + * passes in 0.17.6 thanks to fix for issue #1027. + */ + @SuppressWarnings("unchecked") + @Test + public void retrySubscribesAgainAfterError() throws Throwable { + + // record emitted values with this action + Consumer<Integer> record = mock(Consumer.class); + InOrder inOrder = inOrder(record); + + // always throw an exception with this action + Consumer<Integer> throwException = mock(Consumer.class); + doThrow(new RuntimeException()).when(throwException).accept(Mockito.anyInt()); + + // create a retrying Observable based on a PublishSubject + PublishSubject<Integer> subject = PublishSubject.create(); + subject + // record item + .doOnNext(record) + // throw a RuntimeException + .doOnNext(throwException) + // retry on error + .retry() + // subscribe and ignore + .subscribe(); + + inOrder.verifyNoMoreInteractions(); + + subject.onNext(1); + inOrder.verify(record).accept(1); + + subject.onNext(2); + inOrder.verify(record).accept(2); + + subject.onNext(3); + inOrder.verify(record).accept(3); + + inOrder.verifyNoMoreInteractions(); + } + + public static class FuncWithErrors implements ObservableSource<String> { + + private final int numFailures; + private final AtomicInteger count = new AtomicInteger(0); + + FuncWithErrors(int count) { + this.numFailures = count; + } + + @Override + public void subscribe(final Observer<? super String> o) { + o.onSubscribe(Disposable.empty()); + o.onNext("beginningEveryTime"); + int i = count.getAndIncrement(); + if (i < numFailures) { + o.onError(new RuntimeException("forced failure: " + (i + 1))); + } else { + o.onNext("onSuccessOnly"); + o.onComplete(); + } + } + } + + @Test + public void unsubscribeFromRetry() { + PublishSubject<Integer> subject = PublishSubject.create(); + final AtomicInteger count = new AtomicInteger(0); + Disposable sub = subject.retry().subscribe(new Consumer<Integer>() { + @Override + public void accept(Integer n) { + count.incrementAndGet(); + } + }); + subject.onNext(1); + sub.dispose(); + subject.onNext(2); + assertEquals(1, count.get()); + } + + @Test + public void retryAllowsSubscriptionAfterAllSubscriptionsUnsubscribed() throws InterruptedException { + final AtomicInteger subsCount = new AtomicInteger(0); + ObservableSource<String> onSubscribe = new ObservableSource<String>() { + @Override + public void subscribe(Observer<? super String> observer) { + subsCount.incrementAndGet(); + observer.onSubscribe(Disposable.fromRunnable(new Runnable() { + @Override + public void run() { + subsCount.decrementAndGet(); + } + })); + + } + }; + Observable<String> stream = Observable.unsafeCreate(onSubscribe); + Observable<String> streamWithRetry = stream.retry(); + Disposable sub = streamWithRetry.subscribe(); + assertEquals(1, subsCount.get()); + sub.dispose(); + assertEquals(0, subsCount.get()); + streamWithRetry.subscribe(); + assertEquals(1, subsCount.get()); + } + + @Test + public void sourceObservableCallsUnsubscribe() throws InterruptedException { + final AtomicInteger subsCount = new AtomicInteger(0); + + final TestObserver<String> to = new TestObserver<>(); + + ObservableSource<String> onSubscribe = new ObservableSource<String>() { + @Override + public void subscribe(Observer<? super String> observer) { + BooleanSubscription bs = new BooleanSubscription(); + // if isUnsubscribed is true that means we have a bug such as + // https://github.com/ReactiveX/RxJava/issues/1024 + if (!bs.isCancelled()) { + subsCount.incrementAndGet(); + observer.onError(new RuntimeException("failed")); + // it unsubscribes the child directly + // this simulates various error/completion scenarios that could occur + // or just a source that proactively triggers cleanup + // FIXME can't unsubscribe child +// s.unsubscribe(); + bs.cancel(); + } else { + observer.onError(new RuntimeException()); + } + } + }; + + Observable.unsafeCreate(onSubscribe).retry(3).subscribe(to); + assertEquals(4, subsCount.get()); // 1 + 3 retries + } + + @Test + public void sourceObservableRetry1() throws InterruptedException { + final AtomicInteger subsCount = new AtomicInteger(0); + + final TestObserver<String> to = new TestObserver<>(); + + ObservableSource<String> onSubscribe = new ObservableSource<String>() { + @Override + public void subscribe(Observer<? super String> observer) { + observer.onSubscribe(Disposable.empty()); + subsCount.incrementAndGet(); + observer.onError(new RuntimeException("failed")); + } + }; + + Observable.unsafeCreate(onSubscribe).retry(1).subscribe(to); + assertEquals(2, subsCount.get()); + } + + @Test + public void sourceObservableRetry0() throws InterruptedException { + final AtomicInteger subsCount = new AtomicInteger(0); + + final TestObserver<String> to = new TestObserver<>(); + + ObservableSource<String> onSubscribe = new ObservableSource<String>() { + @Override + public void subscribe(Observer<? super String> observer) { + observer.onSubscribe(Disposable.empty()); + subsCount.incrementAndGet(); + observer.onError(new RuntimeException("failed")); + } + }; + + Observable.unsafeCreate(onSubscribe).retry(0).subscribe(to); + assertEquals(1, subsCount.get()); + } + + static final class SlowObservable implements ObservableSource<Long> { + + final AtomicInteger efforts = new AtomicInteger(0); + final AtomicInteger active = new AtomicInteger(0), maxActive = new AtomicInteger(0); + final AtomicInteger nextBeforeFailure; + + final String context; + + private final int emitDelay; + + SlowObservable(int emitDelay, int countNext, String context) { + this.emitDelay = emitDelay; + this.nextBeforeFailure = new AtomicInteger(countNext); + this.context = context; + } + + @Override + public void subscribe(final Observer<? super Long> observer) { + final AtomicBoolean terminate = new AtomicBoolean(false); + observer.onSubscribe(Disposable.fromRunnable(new Runnable() { + @Override + public void run() { + terminate.set(true); + active.decrementAndGet(); + } + })); + efforts.getAndIncrement(); + active.getAndIncrement(); + maxActive.set(Math.max(active.get(), maxActive.get())); + final Thread thread = new Thread(context) { + @Override + public void run() { + long nr = 0; + try { + while (!terminate.get()) { + Thread.sleep(emitDelay); + if (nextBeforeFailure.getAndDecrement() > 0) { + observer.onNext(nr++); + } else { + active.decrementAndGet(); + observer.onError(new RuntimeException("expected-failed")); + break; + } + } + } catch (InterruptedException t) { + } + } + }; + thread.start(); + } + } + + /** Observer for listener on separate thread. */ + static final class AsyncObserver<T> extends DefaultObserver<T> { + + protected CountDownLatch latch = new CountDownLatch(1); + + protected Observer<T> target; + + /** + * Wrap existing Observer. + * @param target the target nbp subscriber + */ + AsyncObserver(Observer<T> target) { + this.target = target; + } + + /** Wait. */ + public void await() { + try { + latch.await(); + } catch (InterruptedException e) { + fail("Test interrupted"); + } + } + + // Observer implementation + + @Override + public void onComplete() { + target.onComplete(); + latch.countDown(); + } + + @Override + public void onError(Throwable t) { + target.onError(t); + latch.countDown(); + } + + @Override + public void onNext(T v) { + target.onNext(v); + } + } + + @Test + public void unsubscribeAfterError() { + + Observer<Long> observer = TestHelper.mockObserver(); + + // Observable that always fails after 100ms + SlowObservable so = new SlowObservable(100, 0, "testUnsubscribeAfterError"); + Observable<Long> o = Observable.unsafeCreate(so).retry(5); + + AsyncObserver<Long> async = new AsyncObserver<>(observer); + + o.subscribe(async); + + async.await(); + + InOrder inOrder = inOrder(observer); + // Should fail once + inOrder.verify(observer, times(1)).onError(any(Throwable.class)); + inOrder.verify(observer, never()).onComplete(); + + assertEquals("Start 6 threads, retry 5 then fail on 6", 6, so.efforts.get()); + assertEquals("Only 1 active subscription", 1, so.maxActive.get()); + } + + @Test + public void timeoutWithRetry() { + + Observer<Long> observer = TestHelper.mockObserver(); + + // Observable that sends every 100ms (timeout fails instead) + SlowObservable so = new SlowObservable(100, 10, "testTimeoutWithRetry"); + Observable<Long> o = Observable.unsafeCreate(so).timeout(80, TimeUnit.MILLISECONDS).retry(5); + + AsyncObserver<Long> async = new AsyncObserver<>(observer); + + o.subscribe(async); + + async.await(); + + InOrder inOrder = inOrder(observer); + // Should fail once + inOrder.verify(observer, times(1)).onError(any(Throwable.class)); + inOrder.verify(observer, never()).onComplete(); + + assertEquals("Start 6 threads, retry 5 then fail on 6", 6, so.efforts.get()); + } + + @Test + public void retryWithBackpressure() throws InterruptedException { + final int NUM_LOOPS = 1; + for (int j = 0; j < NUM_LOOPS; j++) { + final int NUM_RETRIES = Flowable.bufferSize() * 2; + for (int i = 0; i < 400; i++) { + Observer<String> observer = TestHelper.mockObserver(); + Observable<String> origin = Observable.unsafeCreate(new FuncWithErrors(NUM_RETRIES)); + TestObserver<String> to = new TestObserver<>(observer); + origin.retry().observeOn(Schedulers.computation()).subscribe(to); + to.awaitDone(5, TimeUnit.SECONDS); + + InOrder inOrder = inOrder(observer); + // should have no errors + verify(observer, never()).onError(any(Throwable.class)); + // should show NUM_RETRIES attempts + inOrder.verify(observer, times(NUM_RETRIES + 1)).onNext("beginningEveryTime"); + // should have a single success + inOrder.verify(observer, times(1)).onNext("onSuccessOnly"); + // should have a single successful onComplete + inOrder.verify(observer, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + } + } + } + + @Test + public void retryWithBackpressureParallel() throws InterruptedException { + final int NUM_LOOPS = 1; + final int NUM_RETRIES = Flowable.bufferSize() * 2; + int ncpu = Runtime.getRuntime().availableProcessors(); + ExecutorService exec = Executors.newFixedThreadPool(Math.max(ncpu / 2, 2)); + try { + for (int r = 0; r < NUM_LOOPS; r++) { + if (r % 10 == 0) { + System.out.println("testRetryWithBackpressureParallelLoop -> " + r); + } + + final AtomicInteger timeouts = new AtomicInteger(); + final Map<Integer, List<String>> data = new ConcurrentHashMap<>(); + + int m = 5000; + final CountDownLatch cdl = new CountDownLatch(m); + for (int i = 0; i < m; i++) { + final int j = i; + exec.execute(new Runnable() { + @Override + public void run() { + final AtomicInteger nexts = new AtomicInteger(); + try { + Observable<String> origin = Observable.unsafeCreate(new FuncWithErrors(NUM_RETRIES)); + TestObserverEx<String> to = new TestObserverEx<>(); + origin.retry() + .observeOn(Schedulers.computation()).subscribe(to); + to.awaitDone(2500, TimeUnit.MILLISECONDS); + List<String> onNextEvents = new ArrayList<>(to.values()); + if (onNextEvents.size() != NUM_RETRIES + 2) { + for (Throwable t : to.errors()) { + onNextEvents.add(t.toString()); + } + for (long err = to.completions(); err != 0; err--) { + onNextEvents.add("onComplete"); + } + data.put(j, onNextEvents); + } + } catch (Throwable t) { + timeouts.incrementAndGet(); + System.out.println(j + " | " + cdl.getCount() + " !!! " + nexts.get()); + } + cdl.countDown(); + } + }); + } + cdl.await(); + assertEquals(0, timeouts.get()); + if (data.size() > 0) { + fail("Data content mismatch: " + allSequenceFrequency(data)); + } + } + } finally { + exec.shutdown(); + } + } + static <T> StringBuilder allSequenceFrequency(Map<Integer, List<T>> its) { + StringBuilder b = new StringBuilder(); + for (Map.Entry<Integer, List<T>> e : its.entrySet()) { + if (b.length() > 0) { + b.append(", "); + } + b.append(e.getKey()).append("={"); + b.append(sequenceFrequency(e.getValue())); + b.append("}"); + } + return b; + } + static <T> StringBuilder sequenceFrequency(Iterable<T> it) { + StringBuilder sb = new StringBuilder(); + + Object prev = null; + int cnt = 0; + + for (Object curr : it) { + if (sb.length() > 0) { + if (!curr.equals(prev)) { + if (cnt > 1) { + sb.append(" x ").append(cnt); + cnt = 1; + } + sb.append(", "); + sb.append(curr); + } else { + cnt++; + } + } else { + sb.append(curr); + cnt++; + } + prev = curr; + } + if (cnt > 1) { + sb.append(" x ").append(cnt); + } + + return sb; + } + + @Test + public void issue1900() throws InterruptedException { + Observer<String> observer = TestHelper.mockObserver(); + final int NUM_MSG = 1034; + final AtomicInteger count = new AtomicInteger(); + + Observable<String> origin = Observable.range(0, NUM_MSG) + .map(new Function<Integer, String>() { + @Override + public String apply(Integer t1) { + return "msg: " + count.incrementAndGet(); + } + }); + + origin.retry() + .groupBy(new Function<String, String>() { + @Override + public String apply(String t1) { + return t1; + } + }) + .flatMap(new Function<GroupedObservable<String, String>, Observable<String>>() { + @Override + public Observable<String> apply(GroupedObservable<String, String> t1) { + return t1.take(1); + } + }) + .subscribe(new TestObserver<>(observer)); + + InOrder inOrder = inOrder(observer); + // should show 3 attempts + inOrder.verify(observer, times(NUM_MSG)).onNext(any(java.lang.String.class)); + // // should have no errors + inOrder.verify(observer, never()).onError(any(Throwable.class)); + // should have a single success + //inOrder.verify(observer, times(1)).onNext("onSuccessOnly"); + // should have a single successful onComplete + inOrder.verify(observer, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void issue1900SourceNotSupportingBackpressure() { + Observer<String> observer = TestHelper.mockObserver(); + final int NUM_MSG = 1034; + final AtomicInteger count = new AtomicInteger(); + + Observable<String> origin = Observable.unsafeCreate(new ObservableSource<String>() { + + @Override + public void subscribe(Observer<? super String> o) { + o.onSubscribe(Disposable.empty()); + for (int i = 0; i < NUM_MSG; i++) { + o.onNext("msg:" + count.incrementAndGet()); + } + o.onComplete(); + } + }); + + origin.retry() + .groupBy(new Function<String, String>() { + @Override + public String apply(String t1) { + return t1; + } + }) + .flatMap(new Function<GroupedObservable<String, String>, Observable<String>>() { + @Override + public Observable<String> apply(GroupedObservable<String, String> t1) { + return t1.take(1); + } + }) + .subscribe(new TestObserver<>(observer)); + + InOrder inOrder = inOrder(observer); + // should show 3 attempts + inOrder.verify(observer, times(NUM_MSG)).onNext(any(java.lang.String.class)); + // // should have no errors + inOrder.verify(observer, never()).onError(any(Throwable.class)); + // should have a single success + //inOrder.verify(observer, times(1)).onNext("onSuccessOnly"); + // should have a single successful onComplete + inOrder.verify(observer, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void retryPredicate() { + Observable.just(1).concatWith(Observable.<Integer>error(new TestException())) + .retry(new Predicate<Throwable>() { + @Override + public boolean test(Throwable v) throws Exception { + return true; + } + }) + .take(5) + .test() + .assertResult(1, 1, 1, 1, 1); + } + + @Test + public void retryUntil() { + Observable.just(1).concatWith(Observable.<Integer>error(new TestException())) + .retryUntil(new BooleanSupplier() { + @Override + public boolean getAsBoolean() throws Exception { + return false; + } + }) + .take(5) + .test() + .assertResult(1, 1, 1, 1, 1); + } + + @Test + public void retryLongPredicateInvalid() { + try { + Observable.just(1).retry(-99, new Predicate<Throwable>() { + @Override + public boolean test(Throwable e) throws Exception { + return true; + } + }); + fail("Should have thrown"); + } catch (IllegalArgumentException ex) { + assertEquals("times >= 0 required but it was -99", ex.getMessage()); + } + } + + @Test + public void shouldDisposeInnerObservable() { + final PublishSubject<Object> subject = PublishSubject.create(); + final Disposable disposable = Observable.error(new RuntimeException("Leak")) + .retryWhen(new Function<Observable<Throwable>, ObservableSource<Object>>() { + @Override + public ObservableSource<Object> apply(Observable<Throwable> errors) throws Exception { + return errors.switchMap(new Function<Throwable, ObservableSource<Object>>() { + @Override + public ObservableSource<Object> apply(Throwable ignore) throws Exception { + return subject; + } + }); + } + }) + .subscribe(); + + assertTrue(subject.hasObservers()); + disposable.dispose(); + assertFalse(subject.hasObservers()); + } + + @Test + public void noCancelPreviousRetry() { + final AtomicInteger counter = new AtomicInteger(); + + final AtomicInteger times = new AtomicInteger(); + + Observable<Integer> source = Observable.defer(new Supplier<ObservableSource<Integer>>() { + @Override + public ObservableSource<Integer> get() throws Exception { + if (times.getAndIncrement() < 4) { + return Observable.error(new TestException()); + } + return Observable.just(1); + } + }) + .doOnDispose(new Action() { + @Override + public void run() throws Exception { + counter.getAndIncrement(); + } + }); + + source.retry(5) + .test() + .assertResult(1); + + assertEquals(0, counter.get()); + } + + @Test + public void noCancelPreviousRetryWhile() { + final AtomicInteger counter = new AtomicInteger(); + + final AtomicInteger times = new AtomicInteger(); + + Observable<Integer> source = Observable.defer(new Supplier<ObservableSource<Integer>>() { + @Override + public ObservableSource<Integer> get() throws Exception { + if (times.getAndIncrement() < 4) { + return Observable.error(new TestException()); + } + return Observable.just(1); + } + }) + .doOnDispose(new Action() { + @Override + public void run() throws Exception { + counter.getAndIncrement(); + } + }); + + source.retry(5, Functions.alwaysTrue()) + .test() + .assertResult(1); + + assertEquals(0, counter.get()); + } + + @Test + public void noCancelPreviousRetryWhile2() { + final AtomicInteger counter = new AtomicInteger(); + + final AtomicInteger times = new AtomicInteger(); + + Observable<Integer> source = Observable.defer(new Supplier<ObservableSource<Integer>>() { + @Override + public ObservableSource<Integer> get() throws Exception { + if (times.getAndIncrement() < 4) { + return Observable.error(new TestException()); + } + return Observable.just(1); + } + }) + .doOnDispose(new Action() { + @Override + public void run() throws Exception { + counter.getAndIncrement(); + } + }); + + source.retry(new BiPredicate<Integer, Throwable>() { + @Override + public boolean test(Integer a, Throwable b) throws Exception { + return a < 5; + } + }) + .test() + .assertResult(1); + + assertEquals(0, counter.get()); + } + + @Test + public void noCancelPreviousRetryUntil() { + final AtomicInteger counter = new AtomicInteger(); + + final AtomicInteger times = new AtomicInteger(); + + Observable<Integer> source = Observable.defer(new Supplier<ObservableSource<Integer>>() { + @Override + public ObservableSource<Integer> get() throws Exception { + if (times.getAndIncrement() < 4) { + return Observable.error(new TestException()); + } + return Observable.just(1); + } + }) + .doOnDispose(new Action() { + @Override + public void run() throws Exception { + counter.getAndIncrement(); + } + }); + + source.retryUntil(new BooleanSupplier() { + @Override + public boolean getAsBoolean() throws Exception { + return false; + } + }) + .test() + .assertResult(1); + + assertEquals(0, counter.get()); + } + + @Test + public void noCancelPreviousRepeatWhen() { + final AtomicInteger counter = new AtomicInteger(); + + final AtomicInteger times = new AtomicInteger(); + + Observable<Integer> source = Observable.defer(new Supplier<ObservableSource<Integer>>() { + @Override + public ObservableSource<Integer> get() throws Exception { + if (times.get() < 4) { + return Observable.error(new TestException()); + } + return Observable.just(1); + } + }).doOnDispose(new Action() { + @Override + public void run() throws Exception { + counter.getAndIncrement(); + } + }); + + source.retryWhen(new Function<Observable<Throwable>, ObservableSource<?>>() { + @Override + public ObservableSource<?> apply(Observable<Throwable> e) throws Exception { + return e.takeWhile(new Predicate<Object>() { + @Override + public boolean test(Object v) throws Exception { + return times.getAndIncrement() < 4; + } + }); + } + }) + .test() + .assertResult(1); + + assertEquals(0, counter.get()); + } + + @Test + public void noCancelPreviousRepeatWhen2() { + final AtomicInteger counter = new AtomicInteger(); + + final AtomicInteger times = new AtomicInteger(); + + Observable<Integer> source = Observable.<Integer>error(new TestException()).doOnDispose(new Action() { + @Override + public void run() throws Exception { + counter.getAndIncrement(); + } + }); + + source.retryWhen(new Function<Observable<Throwable>, ObservableSource<?>>() { + @Override + public ObservableSource<?> apply(Observable<Throwable> e) throws Exception { + return e.takeWhile(new Predicate<Object>() { + @Override + public boolean test(Object v) throws Exception { + return times.getAndIncrement() < 4; + } + }); + } + }) + .test() + .assertResult(); + + assertEquals(0, counter.get()); + } + + @Test + public void repeatFloodNoSubscriptionError() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + + final TestException error = new TestException(); + + try { + final PublishSubject<Integer> source = PublishSubject.create(); + final PublishSubject<Integer> signaller = PublishSubject.create(); + + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + + TestObserver<Integer> to = source.take(1) + .map(new Function<Integer, Integer>() { + @Override + public Integer apply(Integer v) throws Exception { + throw error; + } + }) + .retryWhen(new Function<Observable<Throwable>, ObservableSource<Integer>>() { + @Override + public ObservableSource<Integer> apply(Observable<Throwable> v) + throws Exception { + return signaller; + } + }).test(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + source.onNext(1); + } + } + }; + Runnable r2 = new Runnable() { + @Override + public void run() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + signaller.onNext(1); + } + } + }; + + TestHelper.race(r1, r2); + + to.dispose(); + } + + if (!errors.isEmpty()) { + for (Throwable e : errors) { + e.printStackTrace(); + } + fail(errors + ""); + } + } finally { + RxJavaPlugins.reset(); + } + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableRetryWithPredicateTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableRetryWithPredicateTest.java new file mode 100644 index 0000000000..bce6e9f659 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableRetryWithPredicateTest.java @@ -0,0 +1,476 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import java.io.IOException; +import java.util.*; +import java.util.concurrent.*; +import java.util.concurrent.atomic.*; + +import org.junit.Test; +import org.mockito.InOrder; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.core.Observer; +import io.reactivex.rxjava3.disposables.*; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.subjects.PublishSubject; +import io.reactivex.rxjava3.testsupport.*; + +public class ObservableRetryWithPredicateTest extends RxJavaTest { + + BiPredicate<Integer, Throwable> retryTwice = new BiPredicate<Integer, Throwable>() { + @Override + public boolean test(Integer t1, Throwable t2) { + return t1 <= 2; + } + }; + BiPredicate<Integer, Throwable> retry5 = new BiPredicate<Integer, Throwable>() { + @Override + public boolean test(Integer t1, Throwable t2) { + return t1 <= 5; + } + }; + BiPredicate<Integer, Throwable> retryOnTestException = new BiPredicate<Integer, Throwable>() { + @Override + public boolean test(Integer t1, Throwable t2) { + return t2 instanceof IOException; + } + }; + @Test + public void withNothingToRetry() { + Observable<Integer> source = Observable.range(0, 3); + + Observer<Integer> o = TestHelper.mockObserver(); + InOrder inOrder = inOrder(o); + + source.retry(retryTwice).subscribe(o); + + inOrder.verify(o).onNext(0); + inOrder.verify(o).onNext(1); + inOrder.verify(o).onNext(2); + inOrder.verify(o).onComplete(); + verify(o, never()).onError(any(Throwable.class)); + } + + @Test + public void retryTwice() { + Observable<Integer> source = Observable.unsafeCreate(new ObservableSource<Integer>() { + int count; + @Override + public void subscribe(Observer<? super Integer> t1) { + t1.onSubscribe(Disposable.empty()); + count++; + t1.onNext(0); + t1.onNext(1); + if (count == 1) { + t1.onError(new TestException()); + return; + } + t1.onNext(2); + t1.onNext(3); + t1.onComplete(); + } + }); + + Observer<Integer> o = TestHelper.mockObserver(); + InOrder inOrder = inOrder(o); + + source.retry(retryTwice).subscribe(o); + + inOrder.verify(o).onNext(0); + inOrder.verify(o).onNext(1); + inOrder.verify(o).onNext(0); + inOrder.verify(o).onNext(1); + inOrder.verify(o).onNext(2); + inOrder.verify(o).onNext(3); + inOrder.verify(o).onComplete(); + verify(o, never()).onError(any(Throwable.class)); + + } + + @Test + public void retryTwiceAndGiveUp() { + Observable<Integer> source = Observable.unsafeCreate(new ObservableSource<Integer>() { + @Override + public void subscribe(Observer<? super Integer> t1) { + t1.onSubscribe(Disposable.empty()); + t1.onNext(0); + t1.onNext(1); + t1.onError(new TestException()); + } + }); + + Observer<Integer> o = TestHelper.mockObserver(); + InOrder inOrder = inOrder(o); + + source.retry(retryTwice).subscribe(o); + + inOrder.verify(o).onNext(0); + inOrder.verify(o).onNext(1); + inOrder.verify(o).onNext(0); + inOrder.verify(o).onNext(1); + inOrder.verify(o).onNext(0); + inOrder.verify(o).onNext(1); + inOrder.verify(o).onError(any(TestException.class)); + verify(o, never()).onComplete(); + + } + + @Test + public void retryOnSpecificException() { + Observable<Integer> source = Observable.unsafeCreate(new ObservableSource<Integer>() { + int count; + @Override + public void subscribe(Observer<? super Integer> t1) { + t1.onSubscribe(Disposable.empty()); + count++; + t1.onNext(0); + t1.onNext(1); + if (count == 1) { + t1.onError(new IOException()); + return; + } + t1.onNext(2); + t1.onNext(3); + t1.onComplete(); + } + }); + + Observer<Integer> o = TestHelper.mockObserver(); + InOrder inOrder = inOrder(o); + + source.retry(retryOnTestException).subscribe(o); + + inOrder.verify(o).onNext(0); + inOrder.verify(o).onNext(1); + inOrder.verify(o).onNext(0); + inOrder.verify(o).onNext(1); + inOrder.verify(o).onNext(2); + inOrder.verify(o).onNext(3); + inOrder.verify(o).onComplete(); + verify(o, never()).onError(any(Throwable.class)); + } + + @Test + public void retryOnSpecificExceptionAndNotOther() { + final IOException ioe = new IOException(); + final TestException te = new TestException(); + Observable<Integer> source = Observable.unsafeCreate(new ObservableSource<Integer>() { + int count; + @Override + public void subscribe(Observer<? super Integer> t1) { + t1.onSubscribe(Disposable.empty()); + count++; + t1.onNext(0); + t1.onNext(1); + if (count == 1) { + t1.onError(ioe); + return; + } + t1.onNext(2); + t1.onNext(3); + t1.onError(te); + } + }); + + Observer<Integer> o = TestHelper.mockObserver(); + InOrder inOrder = inOrder(o); + + source.retry(retryOnTestException).subscribe(o); + + inOrder.verify(o).onNext(0); + inOrder.verify(o).onNext(1); + inOrder.verify(o).onNext(0); + inOrder.verify(o).onNext(1); + inOrder.verify(o).onNext(2); + inOrder.verify(o).onNext(3); + inOrder.verify(o).onError(te); + verify(o, never()).onError(ioe); + verify(o, never()).onComplete(); + } + + @Test + public void unsubscribeFromRetry() { + PublishSubject<Integer> subject = PublishSubject.create(); + final AtomicInteger count = new AtomicInteger(0); + Disposable sub = subject.retry(retryTwice).subscribe(new Consumer<Integer>() { + @Override + public void accept(Integer n) { + count.incrementAndGet(); + } + }); + subject.onNext(1); + sub.dispose(); + subject.onNext(2); + assertEquals(1, count.get()); + } + + @Test + public void unsubscribeAfterError() { + + Observer<Long> observer = TestHelper.mockObserver(); + + // Observable that always fails after 100ms + ObservableRetryTest.SlowObservable so = new ObservableRetryTest.SlowObservable(100, 0, "testUnsubscribeAfterError"); + Observable<Long> o = Observable + .unsafeCreate(so) + .retry(retry5); + + ObservableRetryTest.AsyncObserver<Long> async = new ObservableRetryTest.AsyncObserver<>(observer); + + o.subscribe(async); + + async.await(); + + InOrder inOrder = inOrder(observer); + // Should fail once + inOrder.verify(observer, times(1)).onError(any(Throwable.class)); + inOrder.verify(observer, never()).onComplete(); + + assertEquals("Start 6 threads, retry 5 then fail on 6", 6, so.efforts.get()); + assertEquals("Only 1 active subscription", 1, so.maxActive.get()); + } + + @Test + public void timeoutWithRetry() { + + Observer<Long> observer = TestHelper.mockObserver(); + + // Observable that sends every 100ms (timeout fails instead) + ObservableRetryTest.SlowObservable so = new ObservableRetryTest.SlowObservable(100, 10, "testTimeoutWithRetry"); + Observable<Long> o = Observable + .unsafeCreate(so) + .timeout(80, TimeUnit.MILLISECONDS) + .retry(retry5); + + ObservableRetryTest.AsyncObserver<Long> async = new ObservableRetryTest.AsyncObserver<>(observer); + + o.subscribe(async); + + async.await(); + + InOrder inOrder = inOrder(observer); + // Should fail once + inOrder.verify(observer, times(1)).onError(any(Throwable.class)); + inOrder.verify(observer, never()).onComplete(); + + assertEquals("Start 6 threads, retry 5 then fail on 6", 6, so.efforts.get()); + } + + @Test + public void issue2826() { + TestObserverEx<Integer> to = new TestObserverEx<>(); + final RuntimeException e = new RuntimeException("You shall not pass"); + final AtomicInteger c = new AtomicInteger(); + Observable.just(1).map(new Function<Integer, Integer>() { + @Override + public Integer apply(Integer t1) { + c.incrementAndGet(); + throw e; + } + }).retry(retry5).subscribe(to); + + to.assertTerminated(); + assertEquals(6, c.get()); + assertEquals(Collections.singletonList(e), to.errors()); + } + + @Test + public void justAndRetry() throws Exception { + final AtomicBoolean throwException = new AtomicBoolean(true); + int value = Observable.just(1).map(new Function<Integer, Integer>() { + @Override + public Integer apply(Integer t1) { + if (throwException.compareAndSet(true, false)) { + throw new TestException(); + } + return t1; + } + }).retry(1).blockingSingle(); + + assertEquals(1, value); + } + + @Test + public void issue3008RetryWithPredicate() { + final List<Long> list = new CopyOnWriteArrayList<>(); + final AtomicBoolean isFirst = new AtomicBoolean(true); + Observable.<Long> just(1L, 2L, 3L).map(new Function<Long, Long>() { + @Override + public Long apply(Long x) { + System.out.println("map " + x); + if (x == 2 && isFirst.getAndSet(false)) { + throw new RuntimeException("retryable error"); + } + return x; + }}) + .retry(new BiPredicate<Integer, Throwable>() { + @Override + public boolean test(Integer t1, Throwable t2) { + return true; + }}) + .forEach(new Consumer<Long>() { + + @Override + public void accept(Long t) { + System.out.println(t); + list.add(t); + }}); + assertEquals(Arrays.asList(1L, 1L, 2L, 3L), list); + } + + @Test + public void issue3008RetryInfinite() { + final List<Long> list = new CopyOnWriteArrayList<>(); + final AtomicBoolean isFirst = new AtomicBoolean(true); + Observable.<Long> just(1L, 2L, 3L).map(new Function<Long, Long>() { + @Override + public Long apply(Long x) { + System.out.println("map " + x); + if (x == 2 && isFirst.getAndSet(false)) { + throw new RuntimeException("retryable error"); + } + return x; + }}) + .retry() + .forEach(new Consumer<Long>() { + + @Override + public void accept(Long t) { + System.out.println(t); + list.add(t); + }}); + assertEquals(Arrays.asList(1L, 1L, 2L, 3L), list); + } + + @Test + public void predicateThrows() { + + TestObserverEx<Object> to = Observable.error(new TestException("Outer")) + .retry(new Predicate<Throwable>() { + @Override + public boolean test(Throwable e) throws Exception { + throw new TestException("Inner"); + } + }) + .to(TestHelper.testConsumer()) + .assertFailure(CompositeException.class); + + List<Throwable> errors = TestHelper.compositeList(to.errors().get(0)); + + TestHelper.assertError(errors, 0, TestException.class, "Outer"); + TestHelper.assertError(errors, 1, TestException.class, "Inner"); + } + + @Test + public void dontRetry() { + Observable.error(new TestException("Outer")) + .retry(Functions.alwaysFalse()) + .to(TestHelper.testConsumer()) + .assertFailureAndMessage(TestException.class, "Outer"); + } + + @Test + @SuppressUndeliverable + public void retryDisposeRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final PublishSubject<Integer> ps = PublishSubject.create(); + + final TestObserver<Integer> to = ps.retry(Functions.alwaysTrue()).test(); + + final TestException ex = new TestException(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + ps.onError(ex); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + to.dispose(); + } + }; + + TestHelper.race(r1, r2); + + to.assertEmpty(); + } + } + + @Test + public void bipredicateThrows() { + + TestObserverEx<Object> to = Observable.error(new TestException("Outer")) + .retry(new BiPredicate<Integer, Throwable>() { + @Override + public boolean test(Integer n, Throwable e) throws Exception { + throw new TestException("Inner"); + } + }) + .to(TestHelper.testConsumer()) + .assertFailure(CompositeException.class); + + List<Throwable> errors = TestHelper.compositeList(to.errors().get(0)); + + TestHelper.assertError(errors, 0, TestException.class, "Outer"); + TestHelper.assertError(errors, 1, TestException.class, "Inner"); + } + + @Test + @SuppressUndeliverable + public void retryBiPredicateDisposeRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final PublishSubject<Integer> ps = PublishSubject.create(); + + final TestObserver<Integer> to = ps.retry(new BiPredicate<Object, Object>() { + @Override + public boolean test(Object t1, Object t2) throws Exception { + return true; + } + }).test(); + + final TestException ex = new TestException(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + ps.onError(ex); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + to.dispose(); + } + }; + + TestHelper.race(r1, r2); + + to.assertEmpty(); + } + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableSampleTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableSampleTest.java new file mode 100644 index 0000000000..7da1bfcbda --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableSampleTest.java @@ -0,0 +1,446 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import java.util.concurrent.TimeUnit; + +import org.junit.*; +import org.mockito.InOrder; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.schedulers.*; +import io.reactivex.rxjava3.subjects.PublishSubject; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class ObservableSampleTest extends RxJavaTest { + private TestScheduler scheduler; + private Scheduler.Worker innerScheduler; + private Observer<Long> observer; + private Observer<Object> observer2; + + @Before + // due to mocking + public void before() { + scheduler = new TestScheduler(); + innerScheduler = scheduler.createWorker(); + observer = TestHelper.mockObserver(); + observer2 = TestHelper.mockObserver(); + } + + @Test + public void sample() { + Observable<Long> source = Observable.unsafeCreate(new ObservableSource<Long>() { + @Override + public void subscribe(final Observer<? super Long> observer1) { + observer1.onSubscribe(Disposable.empty()); + innerScheduler.schedule(new Runnable() { + @Override + public void run() { + observer1.onNext(1L); + } + }, 1, TimeUnit.SECONDS); + innerScheduler.schedule(new Runnable() { + @Override + public void run() { + observer1.onNext(2L); + } + }, 2, TimeUnit.SECONDS); + innerScheduler.schedule(new Runnable() { + @Override + public void run() { + observer1.onComplete(); + } + }, 3, TimeUnit.SECONDS); + } + }); + + Observable<Long> sampled = source.sample(400L, TimeUnit.MILLISECONDS, scheduler); + sampled.subscribe(observer); + + InOrder inOrder = inOrder(observer); + + scheduler.advanceTimeTo(800L, TimeUnit.MILLISECONDS); + verify(observer, never()).onNext(any(Long.class)); + verify(observer, never()).onComplete(); + verify(observer, never()).onError(any(Throwable.class)); + + scheduler.advanceTimeTo(1200L, TimeUnit.MILLISECONDS); + inOrder.verify(observer, times(1)).onNext(1L); + verify(observer, never()).onNext(2L); + verify(observer, never()).onComplete(); + verify(observer, never()).onError(any(Throwable.class)); + + scheduler.advanceTimeTo(1600L, TimeUnit.MILLISECONDS); + inOrder.verify(observer, never()).onNext(1L); + verify(observer, never()).onNext(2L); + verify(observer, never()).onComplete(); + verify(observer, never()).onError(any(Throwable.class)); + + scheduler.advanceTimeTo(2000L, TimeUnit.MILLISECONDS); + inOrder.verify(observer, never()).onNext(1L); + inOrder.verify(observer, times(1)).onNext(2L); + verify(observer, never()).onComplete(); + verify(observer, never()).onError(any(Throwable.class)); + + scheduler.advanceTimeTo(3000L, TimeUnit.MILLISECONDS); + inOrder.verify(observer, never()).onNext(1L); + inOrder.verify(observer, never()).onNext(2L); + verify(observer, times(1)).onComplete(); + verify(observer, never()).onError(any(Throwable.class)); + } + + @Test + public void sampleWithSamplerNormal() { + PublishSubject<Integer> source = PublishSubject.create(); + PublishSubject<Integer> sampler = PublishSubject.create(); + + Observable<Integer> m = source.sample(sampler); + m.subscribe(observer2); + + source.onNext(1); + source.onNext(2); + sampler.onNext(1); + source.onNext(3); + source.onNext(4); + sampler.onNext(2); + source.onComplete(); + sampler.onNext(3); + + InOrder inOrder = inOrder(observer2); + inOrder.verify(observer2, never()).onNext(1); + inOrder.verify(observer2, times(1)).onNext(2); + inOrder.verify(observer2, never()).onNext(3); + inOrder.verify(observer2, times(1)).onNext(4); + inOrder.verify(observer2, times(1)).onComplete(); + verify(observer, never()).onError(any(Throwable.class)); + } + + @Test + public void sampleWithSamplerNoDuplicates() { + PublishSubject<Integer> source = PublishSubject.create(); + PublishSubject<Integer> sampler = PublishSubject.create(); + + Observable<Integer> m = source.sample(sampler); + m.subscribe(observer2); + + source.onNext(1); + source.onNext(2); + sampler.onNext(1); + sampler.onNext(1); + + source.onNext(3); + source.onNext(4); + sampler.onNext(2); + sampler.onNext(2); + + source.onComplete(); + sampler.onNext(3); + + InOrder inOrder = inOrder(observer2); + inOrder.verify(observer2, never()).onNext(1); + inOrder.verify(observer2, times(1)).onNext(2); + inOrder.verify(observer2, never()).onNext(3); + inOrder.verify(observer2, times(1)).onNext(4); + inOrder.verify(observer2, times(1)).onComplete(); + verify(observer, never()).onError(any(Throwable.class)); + } + + @Test + public void sampleWithSamplerTerminatingEarly() { + PublishSubject<Integer> source = PublishSubject.create(); + PublishSubject<Integer> sampler = PublishSubject.create(); + + Observable<Integer> m = source.sample(sampler); + m.subscribe(observer2); + + source.onNext(1); + source.onNext(2); + sampler.onNext(1); + sampler.onComplete(); + + source.onNext(3); + source.onNext(4); + + InOrder inOrder = inOrder(observer2); + inOrder.verify(observer2, never()).onNext(1); + inOrder.verify(observer2, times(1)).onNext(2); + inOrder.verify(observer2, times(1)).onComplete(); + inOrder.verify(observer2, never()).onNext(any()); + verify(observer, never()).onError(any(Throwable.class)); + } + + @Test + public void sampleWithSamplerEmitAndTerminate() { + PublishSubject<Integer> source = PublishSubject.create(); + PublishSubject<Integer> sampler = PublishSubject.create(); + + Observable<Integer> m = source.sample(sampler); + m.subscribe(observer2); + + source.onNext(1); + source.onNext(2); + sampler.onNext(1); + source.onNext(3); + source.onComplete(); + sampler.onNext(2); + sampler.onComplete(); + + InOrder inOrder = inOrder(observer2); + inOrder.verify(observer2, never()).onNext(1); + inOrder.verify(observer2, times(1)).onNext(2); + inOrder.verify(observer2, never()).onNext(3); + inOrder.verify(observer2, times(1)).onComplete(); + inOrder.verify(observer2, never()).onNext(any()); + verify(observer, never()).onError(any(Throwable.class)); + } + + @Test + public void sampleWithSamplerEmptySource() { + PublishSubject<Integer> source = PublishSubject.create(); + PublishSubject<Integer> sampler = PublishSubject.create(); + + Observable<Integer> m = source.sample(sampler); + m.subscribe(observer2); + + source.onComplete(); + sampler.onNext(1); + + InOrder inOrder = inOrder(observer2); + inOrder.verify(observer2, times(1)).onComplete(); + verify(observer2, never()).onNext(any()); + verify(observer, never()).onError(any(Throwable.class)); + } + + @Test + public void sampleWithSamplerSourceThrows() { + PublishSubject<Integer> source = PublishSubject.create(); + PublishSubject<Integer> sampler = PublishSubject.create(); + + Observable<Integer> m = source.sample(sampler); + m.subscribe(observer2); + + source.onNext(1); + source.onError(new RuntimeException("Forced failure!")); + sampler.onNext(1); + + InOrder inOrder = inOrder(observer2); + inOrder.verify(observer2, times(1)).onError(any(Throwable.class)); + verify(observer2, never()).onNext(any()); + verify(observer, never()).onComplete(); + } + + @Test + public void sampleWithSamplerThrows() { + PublishSubject<Integer> source = PublishSubject.create(); + PublishSubject<Integer> sampler = PublishSubject.create(); + + Observable<Integer> m = source.sample(sampler); + m.subscribe(observer2); + + source.onNext(1); + sampler.onNext(1); + sampler.onError(new RuntimeException("Forced failure!")); + + InOrder inOrder = inOrder(observer2); + inOrder.verify(observer2, times(1)).onNext(1); + inOrder.verify(observer2, times(1)).onError(any(RuntimeException.class)); + verify(observer, never()).onComplete(); + } + + @Test + public void sampleUnsubscribe() { + final Disposable upstream = mock(Disposable.class); + Observable<Integer> o = Observable.unsafeCreate( + new ObservableSource<Integer>() { + @Override + public void subscribe(Observer<? super Integer> observer) { + observer.onSubscribe(upstream); + } + } + ); + o.throttleLast(1, TimeUnit.MILLISECONDS).subscribe().dispose(); + verify(upstream).dispose(); + } + + @Test + public void dispose() { + TestHelper.checkDisposed(PublishSubject.create().sample(1, TimeUnit.SECONDS, new TestScheduler())); + + TestHelper.checkDisposed(PublishSubject.create().sample(Observable.never())); + } + + @Test + public void error() { + Observable.error(new TestException()) + .sample(1, TimeUnit.SECONDS) + .test() + .assertFailure(TestException.class); + } + + @Test + public void emitLastTimed() { + Observable.just(1) + .sample(1, TimeUnit.DAYS, true) + .test() + .assertResult(1); + } + + @Test + public void emitLastTimedEmpty() { + Observable.empty() + .sample(1, TimeUnit.DAYS, true) + .test() + .assertResult(); + } + + @Test + public void emitLastTimedCustomScheduler() { + Observable.just(1) + .sample(1, TimeUnit.DAYS, Schedulers.single(), true) + .test() + .assertResult(1); + } + + @Test + public void emitLastTimedRunCompleteRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final TestScheduler scheduler = new TestScheduler(); + + final PublishSubject<Integer> ps = PublishSubject.create(); + + TestObserver<Integer> to = ps.sample(1, TimeUnit.SECONDS, scheduler, true) + .test(); + + ps.onNext(1); + + Runnable r1 = new Runnable() { + @Override + public void run() { + ps.onComplete(); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + scheduler.advanceTimeBy(1, TimeUnit.SECONDS); + } + }; + + TestHelper.race(r1, r2); + + to.assertResult(1); + } + } + + @Test + public void emitLastOther() { + Observable.just(1) + .sample(Observable.timer(1, TimeUnit.DAYS), true) + .test() + .assertResult(1); + } + + @Test + public void emitLastOtherEmpty() { + Observable.empty() + .sample(Observable.timer(1, TimeUnit.DAYS), true) + .test() + .assertResult(); + } + + @Test + public void emitLastOtherRunCompleteRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final PublishSubject<Integer> ps = PublishSubject.create(); + final PublishSubject<Integer> sampler = PublishSubject.create(); + + TestObserver<Integer> to = ps.sample(sampler, true) + .test(); + + ps.onNext(1); + + Runnable r1 = new Runnable() { + @Override + public void run() { + ps.onComplete(); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + sampler.onNext(1); + } + }; + + TestHelper.race(r1, r2); + + to.assertResult(1); + } + } + + @Test + public void emitLastOtherCompleteCompleteRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final PublishSubject<Integer> ps = PublishSubject.create(); + final PublishSubject<Integer> sampler = PublishSubject.create(); + + TestObserver<Integer> to = ps.sample(sampler, true).test(); + + ps.onNext(1); + + Runnable r1 = new Runnable() { + @Override + public void run() { + ps.onComplete(); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + sampler.onComplete(); + } + }; + + TestHelper.race(r1, r2); + + to.assertResult(1); + } + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeObservable(new Function<Observable<Object>, Observable<Object>>() { + @Override + public Observable<Object> apply(Observable<Object> o) + throws Exception { + return o.sample(1, TimeUnit.SECONDS); + } + }); + } + + @Test + public void doubleOnSubscribeObservable() { + TestHelper.checkDoubleOnSubscribeObservable(o -> o.sample(Observable.never())); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableScalarXMapTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableScalarXMapTest.java new file mode 100644 index 0000000000..c27b2e17c0 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableScalarXMapTest.java @@ -0,0 +1,247 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import static org.junit.Assert.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.disposables.EmptyDisposable; +import io.reactivex.rxjava3.internal.operators.observable.ObservableScalarXMap.ScalarDisposable; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.operators.QueueFuseable; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class ObservableScalarXMapTest extends RxJavaTest { + + @Test + public void utilityClass() { + TestHelper.checkUtilityClass(ObservableScalarXMap.class); + } + + static final class CallablePublisher implements ObservableSource<Integer>, Supplier<Integer> { + @Override + public void subscribe(Observer<? super Integer> observer) { + EmptyDisposable.error(new TestException(), observer); + } + + @Override + public Integer get() throws Exception { + throw new TestException(); + } + } + + static final class EmptyCallablePublisher implements ObservableSource<Integer>, Supplier<Integer> { + @Override + public void subscribe(Observer<? super Integer> observer) { + EmptyDisposable.complete(observer); + } + + @Override + public Integer get() throws Exception { + return null; + } + } + + static final class OneCallablePublisher implements ObservableSource<Integer>, Supplier<Integer> { + @Override + public void subscribe(Observer<? super Integer> observer) { + ScalarDisposable<Integer> sd = new ScalarDisposable<>(observer, 1); + observer.onSubscribe(sd); + sd.run(); + } + + @Override + public Integer get() throws Exception { + return 1; + } + } + + @Test + public void tryScalarXMap() { + TestObserver<Integer> to = new TestObserver<>(); + assertTrue(ObservableScalarXMap.tryScalarXMapSubscribe(new CallablePublisher(), to, new Function<Integer, ObservableSource<Integer>>() { + @Override + public ObservableSource<Integer> apply(Integer f) throws Exception { + return Observable.just(1); + } + })); + + to.assertFailure(TestException.class); + } + + @Test + public void emptyXMap() { + TestObserver<Integer> to = new TestObserver<>(); + + assertTrue(ObservableScalarXMap.tryScalarXMapSubscribe(new EmptyCallablePublisher(), to, new Function<Integer, ObservableSource<Integer>>() { + @Override + public ObservableSource<Integer> apply(Integer f) throws Exception { + return Observable.just(1); + } + })); + + to.assertResult(); + } + + @Test + public void mapperCrashes() { + TestObserver<Integer> to = new TestObserver<>(); + + assertTrue(ObservableScalarXMap.tryScalarXMapSubscribe(new OneCallablePublisher(), to, new Function<Integer, ObservableSource<Integer>>() { + @Override + public ObservableSource<Integer> apply(Integer f) throws Exception { + throw new TestException(); + } + })); + + to.assertFailure(TestException.class); + } + + @Test + public void mapperToJust() { + TestObserver<Integer> to = new TestObserver<>(); + + assertTrue(ObservableScalarXMap.tryScalarXMapSubscribe(new OneCallablePublisher(), to, new Function<Integer, ObservableSource<Integer>>() { + @Override + public ObservableSource<Integer> apply(Integer f) throws Exception { + return Observable.just(1); + } + })); + + to.assertResult(1); + } + + @Test + public void mapperToEmpty() { + TestObserver<Integer> to = new TestObserver<>(); + + assertTrue(ObservableScalarXMap.tryScalarXMapSubscribe(new OneCallablePublisher(), to, new Function<Integer, ObservableSource<Integer>>() { + @Override + public ObservableSource<Integer> apply(Integer f) throws Exception { + return Observable.empty(); + } + })); + + to.assertResult(); + } + + @Test + public void mapperToCrashingCallable() { + TestObserver<Integer> to = new TestObserver<>(); + + assertTrue(ObservableScalarXMap.tryScalarXMapSubscribe(new OneCallablePublisher(), to, new Function<Integer, ObservableSource<Integer>>() { + @Override + public ObservableSource<Integer> apply(Integer f) throws Exception { + return new CallablePublisher(); + } + })); + + to.assertFailure(TestException.class); + } + + @Test + public void scalarMapToEmpty() { + ObservableScalarXMap.scalarXMap(1, new Function<Integer, ObservableSource<Integer>>() { + @Override + public ObservableSource<Integer> apply(Integer v) throws Exception { + return Observable.empty(); + } + }) + .test() + .assertResult(); + } + + @Test + public void scalarMapToCrashingCallable() { + ObservableScalarXMap.scalarXMap(1, new Function<Integer, ObservableSource<Integer>>() { + @Override + public ObservableSource<Integer> apply(Integer v) throws Exception { + return new CallablePublisher(); + } + }) + .test() + .assertFailure(TestException.class); + } + + @Test + public void scalarDisposableStateCheck() { + TestObserver<Integer> to = new TestObserver<>(); + ScalarDisposable<Integer> sd = new ScalarDisposable<>(to, 1); + to.onSubscribe(sd); + + assertFalse(sd.isDisposed()); + + assertTrue(sd.isEmpty()); + + sd.run(); + + assertTrue(sd.isDisposed()); + + assertTrue(sd.isEmpty()); + + to.assertResult(1); + + try { + sd.offer(1); + fail("Should have thrown"); + } catch (UnsupportedOperationException ex) { + // expected + } + + try { + sd.offer(1, 2); + fail("Should have thrown"); + } catch (UnsupportedOperationException ex) { + // expected + } + } + + @Test + public void scalarDisposableRunDisposeRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + TestObserver<Integer> to = new TestObserver<>(); + final ScalarDisposable<Integer> sd = new ScalarDisposable<>(to, 1); + to.onSubscribe(sd); + + Runnable r1 = new Runnable() { + @Override + public void run() { + sd.run(); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + sd.dispose(); + } + }; + + TestHelper.race(r1, r2); + } + } + + @Test + public void scalarDisposbleWrongFusion() { + TestObserver<Integer> to = new TestObserver<>(); + final ScalarDisposable<Integer> sd = new ScalarDisposable<>(to, 1); + to.onSubscribe(sd); + + assertEquals(QueueFuseable.NONE, sd.requestFusion(QueueFuseable.ASYNC)); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableScanTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableScanTest.java new file mode 100644 index 0000000000..9a1765ceb3 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableScanTest.java @@ -0,0 +1,389 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +import java.util.*; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.core.Observer; +import io.reactivex.rxjava3.disposables.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.observers.*; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.subjects.PublishSubject; +import io.reactivex.rxjava3.testsupport.*; + +public class ObservableScanTest extends RxJavaTest { + + @Test + public void scanIntegersWithInitialValue() { + Observer<String> observer = TestHelper.mockObserver(); + + Observable<Integer> o = Observable.just(1, 2, 3); + + Observable<String> m = o.scan("", new BiFunction<String, Integer, String>() { + + @Override + public String apply(String s, Integer n) { + return s + n.toString(); + } + + }); + m.subscribe(observer); + + verify(observer, never()).onError(any(Throwable.class)); + verify(observer, times(1)).onNext(""); + verify(observer, times(1)).onNext("1"); + verify(observer, times(1)).onNext("12"); + verify(observer, times(1)).onNext("123"); + verify(observer, times(4)).onNext(anyString()); + verify(observer, times(1)).onComplete(); + verify(observer, never()).onError(any(Throwable.class)); + } + + @Test + public void scanIntegersWithoutInitialValue() { + Observer<Integer> observer = TestHelper.mockObserver(); + + Observable<Integer> o = Observable.just(1, 2, 3); + + Observable<Integer> m = o.scan(new BiFunction<Integer, Integer, Integer>() { + + @Override + public Integer apply(Integer t1, Integer t2) { + return t1 + t2; + } + + }); + m.subscribe(observer); + + verify(observer, never()).onError(any(Throwable.class)); + verify(observer, never()).onNext(0); + verify(observer, times(1)).onNext(1); + verify(observer, times(1)).onNext(3); + verify(observer, times(1)).onNext(6); + verify(observer, times(3)).onNext(anyInt()); + verify(observer, times(1)).onComplete(); + verify(observer, never()).onError(any(Throwable.class)); + } + + @Test + public void scanIntegersWithoutInitialValueAndOnlyOneValue() { + Observer<Integer> observer = TestHelper.mockObserver(); + + Observable<Integer> o = Observable.just(1); + + Observable<Integer> m = o.scan(new BiFunction<Integer, Integer, Integer>() { + + @Override + public Integer apply(Integer t1, Integer t2) { + return t1 + t2; + } + + }); + m.subscribe(observer); + + verify(observer, never()).onError(any(Throwable.class)); + verify(observer, never()).onNext(0); + verify(observer, times(1)).onNext(1); + verify(observer, times(1)).onNext(anyInt()); + verify(observer, times(1)).onComplete(); + verify(observer, never()).onError(any(Throwable.class)); + } + + @Test + public void shouldNotEmitUntilAfterSubscription() { + TestObserver<Integer> to = new TestObserver<>(); + Observable.range(1, 100).scan(0, new BiFunction<Integer, Integer, Integer>() { + + @Override + public Integer apply(Integer t1, Integer t2) { + return t1 + t2; + } + + }).filter(new Predicate<Integer>() { + + @Override + public boolean test(Integer t1) { + // this will cause request(1) when 0 is emitted + return t1 > 0; + } + + }).subscribe(to); + + assertEquals(100, to.values().size()); + } + + @Test + public void noBackpressureWithInitialValue() { + final AtomicInteger count = new AtomicInteger(); + Observable.range(1, 100) + .scan(0, new BiFunction<Integer, Integer, Integer>() { + + @Override + public Integer apply(Integer t1, Integer t2) { + return t1 + t2; + } + + }) + .subscribe(new DefaultObserver<Integer>() { + + @Override + public void onComplete() { + + } + + @Override + public void onError(Throwable e) { + fail(e.getMessage()); + e.printStackTrace(); + } + + @Override + public void onNext(Integer t) { + count.incrementAndGet(); + } + + }); + + // we only expect to receive 101 as we'll receive all 100 + the initial value + assertEquals(101, count.get()); + } + + /** + * This uses the public API collect which uses scan under the covers. + */ + @Test + public void seedFactory() { + Observable<List<Integer>> o = Observable.range(1, 10) + .collect(new Supplier<List<Integer>>() { + + @Override + public List<Integer> get() { + return new ArrayList<>(); + } + + }, new BiConsumer<List<Integer>, Integer>() { + + @Override + public void accept(List<Integer> list, Integer t2) { + list.add(t2); + } + + }).toObservable().takeLast(1); + + assertEquals(Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10), o.blockingSingle()); + assertEquals(Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10), o.blockingSingle()); + } + + @Test + public void scanWithRequestOne() { + Observable<Integer> o = Observable.just(1, 2).scan(0, new BiFunction<Integer, Integer, Integer>() { + + @Override + public Integer apply(Integer t1, Integer t2) { + return t1 + t2; + } + + }).take(1); + + TestObserverEx<Integer> observer = new TestObserverEx<>(); + + o.subscribe(observer); + observer.assertValue(0); + observer.assertTerminated(); + observer.assertNoErrors(); + } + + @Test + public void initialValueEmittedNoProducer() { + PublishSubject<Integer> source = PublishSubject.create(); + + TestObserver<Integer> to = new TestObserver<>(); + + source.scan(0, new BiFunction<Integer, Integer, Integer>() { + @Override + public Integer apply(Integer t1, Integer t2) { + return t1 + t2; + } + }).subscribe(to); + + to.assertNoErrors(); + to.assertNotComplete(); + to.assertValue(0); + } + + @Test + public void dispose() { + TestHelper.checkDisposed(PublishSubject.create().scan(new BiFunction<Object, Object, Object>() { + @Override + public Object apply(Object a, Object b) throws Exception { + return a; + } + })); + + TestHelper.checkDisposed(PublishSubject.<Integer>create().scan(0, new BiFunction<Integer, Integer, Integer>() { + @Override + public Integer apply(Integer a, Integer b) throws Exception { + return a + b; + } + })); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeObservable(new Function<Observable<Object>, ObservableSource<Object>>() { + @Override + public ObservableSource<Object> apply(Observable<Object> o) throws Exception { + return o.scan(new BiFunction<Object, Object, Object>() { + @Override + public Object apply(Object a, Object b) throws Exception { + return a; + } + }); + } + }); + + TestHelper.checkDoubleOnSubscribeObservable(new Function<Observable<Object>, ObservableSource<Object>>() { + @Override + public ObservableSource<Object> apply(Observable<Object> o) throws Exception { + return o.scan(0, new BiFunction<Object, Object, Object>() { + @Override + public Object apply(Object a, Object b) throws Exception { + return a; + } + }); + } + }); + } + + @Test + public void error() { + Observable.error(new TestException()) + .scan(new BiFunction<Object, Object, Object>() { + @Override + public Object apply(Object a, Object b) throws Exception { + return a; + } + }) + .test() + .assertFailure(TestException.class); + } + + @Test + public void badSource() { + TestHelper.checkBadSourceObservable(new Function<Observable<Object>, Object>() { + @Override + public Object apply(Observable<Object> o) throws Exception { + return o.scan(0, new BiFunction<Object, Object, Object>() { + @Override + public Object apply(Object a, Object b) throws Exception { + return a; + } + }); + } + }, false, 1, 1, 0, 0); + } + + @Test + public void scanFunctionThrowsAndUpstreamErrorsDoesNotResultInTwoTerminalEvents() { + final RuntimeException err = new RuntimeException(); + final RuntimeException err2 = new RuntimeException(); + final List<Throwable> list = new CopyOnWriteArrayList<>(); + final Consumer<Throwable> errorConsumer = new Consumer<Throwable>() { + @Override + public void accept(Throwable t) throws Exception { + list.add(t); + }}; + try { + RxJavaPlugins.setErrorHandler(errorConsumer); + Observable.unsafeCreate(new ObservableSource<Integer>() { + @Override + public void subscribe(Observer<? super Integer> o) { + Disposable d = Disposable.empty(); + o.onSubscribe(d); + o.onNext(1); + o.onNext(2); + o.onError(err2); + }}) + .scan(new BiFunction<Integer, Integer, Integer>() { + @Override + public Integer apply(Integer t1, Integer t2) throws Exception { + throw err; + }}) + .test() + .assertError(err) + .assertValue(1); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void scanFunctionThrowsAndUpstreamCompletesDoesNotResultInTwoTerminalEvents() { + final RuntimeException err = new RuntimeException(); + Observable.unsafeCreate(new ObservableSource<Integer>() { + @Override + public void subscribe(Observer<? super Integer> o) { + Disposable d = Disposable.empty(); + o.onSubscribe(d); + o.onNext(1); + o.onNext(2); + o.onComplete(); + }}) + .scan(new BiFunction<Integer, Integer, Integer>() { + @Override + public Integer apply(Integer t1, Integer t2) throws Exception { + throw err; + }}) + .test() + .assertError(err) + .assertValue(1); + } + + @Test + public void scanFunctionThrowsAndUpstreamEmitsOnNextResultsInScanFunctionBeingCalledOnlyOnce() { + final RuntimeException err = new RuntimeException(); + final AtomicInteger count = new AtomicInteger(); + Observable.unsafeCreate(new ObservableSource<Integer>() { + @Override + public void subscribe(Observer<? super Integer> o) { + Disposable d = Disposable.empty(); + o.onSubscribe(d); + o.onNext(1); + o.onNext(2); + o.onNext(3); + }}) + .scan(new BiFunction<Integer, Integer, Integer>() { + @Override + public Integer apply(Integer t1, Integer t2) throws Exception { + count.incrementAndGet(); + throw err; + }}) + .test() + .assertError(err) + .assertValue(1); + assertEquals(1, count.get()); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableSequenceEqualTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableSequenceEqualTest.java new file mode 100644 index 0000000000..645fafbb62 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableSequenceEqualTest.java @@ -0,0 +1,412 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import static org.mockito.ArgumentMatchers.isA; +import static org.mockito.Mockito.*; + +import org.junit.Test; +import org.mockito.InOrder; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.BiPredicate; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.subjects.PublishSubject; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class ObservableSequenceEqualTest extends RxJavaTest { + + @Test + public void observable1() { + Observable<Boolean> o = Observable.sequenceEqual( + Observable.just("one", "two", "three"), + Observable.just("one", "two", "three")).toObservable(); + verifyResult(o, true); + } + + @Test + public void observable2() { + Observable<Boolean> o = Observable.sequenceEqual( + Observable.just("one", "two", "three"), + Observable.just("one", "two", "three", "four")).toObservable(); + verifyResult(o, false); + } + + @Test + public void observable3() { + Observable<Boolean> o = Observable.sequenceEqual( + Observable.just("one", "two", "three", "four"), + Observable.just("one", "two", "three")).toObservable(); + verifyResult(o, false); + } + + @Test + public void withError1Observable() { + Observable<Boolean> o = Observable.sequenceEqual( + Observable.concat(Observable.just("one"), + Observable.<String> error(new TestException())), + Observable.just("one", "two", "three")).toObservable(); + verifyError(o); + } + + @Test + public void withError2Observable() { + Observable<Boolean> o = Observable.sequenceEqual( + Observable.just("one", "two", "three"), + Observable.concat(Observable.just("one"), + Observable.<String> error(new TestException()))).toObservable(); + verifyError(o); + } + + @Test + public void withError3Observable() { + Observable<Boolean> o = Observable.sequenceEqual( + Observable.concat(Observable.just("one"), + Observable.<String> error(new TestException())), + Observable.concat(Observable.just("one"), + Observable.<String> error(new TestException()))).toObservable(); + verifyError(o); + } + + @Test + public void withEmpty1Observable() { + Observable<Boolean> o = Observable.sequenceEqual( + Observable.<String> empty(), + Observable.just("one", "two", "three")).toObservable(); + verifyResult(o, false); + } + + @Test + public void withEmpty2Observable() { + Observable<Boolean> o = Observable.sequenceEqual( + Observable.just("one", "two", "three"), + Observable.<String> empty()).toObservable(); + verifyResult(o, false); + } + + @Test + public void withEmpty3Observable() { + Observable<Boolean> o = Observable.sequenceEqual( + Observable.<String> empty(), Observable.<String> empty()).toObservable(); + verifyResult(o, true); + } + + @Test + public void withEqualityErrorObservable() { + Observable<Boolean> o = Observable.sequenceEqual( + Observable.just("one"), Observable.just("one"), + new BiPredicate<String, String>() { + @Override + public boolean test(String t1, String t2) { + throw new TestException(); + } + }).toObservable(); + verifyError(o); + } + + private void verifyResult(Single<Boolean> o, boolean result) { + SingleObserver<Boolean> observer = TestHelper.mockSingleObserver(); + + o.subscribe(observer); + + InOrder inOrder = inOrder(observer); + inOrder.verify(observer, times(1)).onSuccess(result); + inOrder.verifyNoMoreInteractions(); + } + + private void verifyError(Observable<Boolean> observable) { + Observer<Boolean> observer = TestHelper.mockObserver(); + observable.subscribe(observer); + + InOrder inOrder = inOrder(observer); + inOrder.verify(observer, times(1)).onError(isA(TestException.class)); + inOrder.verifyNoMoreInteractions(); + } + + private void verifyError(Single<Boolean> single) { + SingleObserver<Boolean> observer = TestHelper.mockSingleObserver(); + single.subscribe(observer); + + InOrder inOrder = inOrder(observer); + inOrder.verify(observer, times(1)).onError(isA(TestException.class)); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void prefetchObservable() { + Observable.sequenceEqual(Observable.range(1, 20), Observable.range(1, 20), 2) + .toObservable() + .test() + .assertResult(true); + } + + @Test + public void disposedObservable() { + TestHelper.checkDisposed(Observable.sequenceEqual(Observable.just(1), Observable.just(2)).toObservable()); + } + + @Test + public void one() { + Single<Boolean> o = Observable.sequenceEqual( + Observable.just("one", "two", "three"), + Observable.just("one", "two", "three")); + verifyResult(o, true); + } + + @Test + public void two() { + Single<Boolean> o = Observable.sequenceEqual( + Observable.just("one", "two", "three"), + Observable.just("one", "two", "three", "four")); + verifyResult(o, false); + } + + @Test + public void three() { + Single<Boolean> o = Observable.sequenceEqual( + Observable.just("one", "two", "three", "four"), + Observable.just("one", "two", "three")); + verifyResult(o, false); + } + + @Test + public void withError1() { + Single<Boolean> o = Observable.sequenceEqual( + Observable.concat(Observable.just("one"), + Observable.<String> error(new TestException())), + Observable.just("one", "two", "three")); + verifyError(o); + } + + @Test + public void withError2() { + Single<Boolean> o = Observable.sequenceEqual( + Observable.just("one", "two", "three"), + Observable.concat(Observable.just("one"), + Observable.<String> error(new TestException()))); + verifyError(o); + } + + @Test + public void withError3() { + Single<Boolean> o = Observable.sequenceEqual( + Observable.concat(Observable.just("one"), + Observable.<String> error(new TestException())), + Observable.concat(Observable.just("one"), + Observable.<String> error(new TestException()))); + verifyError(o); + } + + @Test + public void withEmpty1() { + Single<Boolean> o = Observable.sequenceEqual( + Observable.<String> empty(), + Observable.just("one", "two", "three")); + verifyResult(o, false); + } + + @Test + public void withEmpty2() { + Single<Boolean> o = Observable.sequenceEqual( + Observable.just("one", "two", "three"), + Observable.<String> empty()); + verifyResult(o, false); + } + + @Test + public void withEmpty3() { + Single<Boolean> o = Observable.sequenceEqual( + Observable.<String> empty(), Observable.<String> empty()); + verifyResult(o, true); + } + + @Test + public void withEqualityError() { + Single<Boolean> o = Observable.sequenceEqual( + Observable.just("one"), Observable.just("one"), + new BiPredicate<String, String>() { + @Override + public boolean test(String t1, String t2) { + throw new TestException(); + } + }); + verifyError(o); + } + + private void verifyResult(Observable<Boolean> o, boolean result) { + Observer<Boolean> observer = TestHelper.mockObserver(); + + o.subscribe(observer); + + InOrder inOrder = inOrder(observer); + inOrder.verify(observer, times(1)).onNext(result); + inOrder.verify(observer).onComplete(); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void prefetch() { + Observable.sequenceEqual(Observable.range(1, 20), Observable.range(1, 20), 2) + .test() + .assertResult(true); + } + + @Test + public void disposed() { + TestHelper.checkDisposed(Observable.sequenceEqual(Observable.just(1), Observable.just(2))); + } + + @Test + public void simpleInequal() { + Observable.sequenceEqual(Observable.just(1), Observable.just(2)) + .test() + .assertResult(false); + } + + @Test + public void simpleInequalObservable() { + Observable.sequenceEqual(Observable.just(1), Observable.just(2)) + .toObservable() + .test() + .assertResult(false); + } + + @Test + public void onNextCancelRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final PublishSubject<Integer> ps = PublishSubject.create(); + + final TestObserver<Boolean> to = Observable.sequenceEqual(Observable.never(), ps).test(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + to.dispose(); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + ps.onNext(1); + } + }; + + TestHelper.race(r1, r2); + + to.assertEmpty(); + } + } + + @Test + public void onNextCancelRaceObservable() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final PublishSubject<Integer> ps = PublishSubject.create(); + + final TestObserver<Boolean> to = Observable.sequenceEqual(Observable.never(), ps).toObservable().test(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + to.dispose(); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + ps.onNext(1); + } + }; + + TestHelper.race(r1, r2); + + to.assertEmpty(); + } + } + + @Test + public void firstCompletesBeforeSecond() { + Observable.sequenceEqual(Observable.just(1), Observable.empty()) + .test() + .assertResult(false); + } + + @Test + public void secondCompletesBeforeFirst() { + Observable.sequenceEqual(Observable.empty(), Observable.just(1)) + .test() + .assertResult(false); + } + + @Test + public void bothEmpty() { + Observable.sequenceEqual(Observable.empty(), Observable.empty()) + .test() + .assertResult(true); + } + + @Test + public void bothJust() { + Observable.sequenceEqual(Observable.just(1), Observable.just(1)) + .test() + .assertResult(true); + } + + @Test + public void bothCompleteWhileComparing() { + PublishSubject<Integer> ps1 = PublishSubject.create(); + PublishSubject<Integer> ps2 = PublishSubject.create(); + + TestObserver<Boolean> to = Observable.sequenceEqual(ps1, ps2, (a, b) -> { + ps1.onNext(1); + ps1.onComplete(); + + ps2.onNext(1); + ps2.onComplete(); + return a.equals(b); + }) + .test() + ; + + ps1.onNext(0); + ps2.onNext(0); + + to.assertResult(true); + } + + @Test + public void bothCompleteWhileComparingAsObservable() { + PublishSubject<Integer> ps1 = PublishSubject.create(); + PublishSubject<Integer> ps2 = PublishSubject.create(); + + TestObserver<Boolean> to = Observable.sequenceEqual(ps1, ps2, (a, b) -> { + ps1.onNext(1); + ps1.onComplete(); + + ps2.onNext(1); + ps2.onComplete(); + return a.equals(b); + }) + .toObservable() + .test() + ; + + ps1.onNext(0); + ps2.onNext(0); + + to.assertResult(true); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableSerializeTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableSerializeTest.java new file mode 100644 index 0000000000..9aa31211b5 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableSerializeTest.java @@ -0,0 +1,435 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicInteger; + +import io.reactivex.rxjava3.disposables.Disposable; +import org.junit.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.observers.DefaultObserver; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class ObservableSerializeTest extends RxJavaTest { + + Observer<String> observer; + + @Before + public void before() { + observer = TestHelper.mockObserver(); + } + + @Test + public void singleThreadedBasic() { + TestSingleThreadedObservable onSubscribe = new TestSingleThreadedObservable("one", "two", "three"); + Observable<String> w = Observable.unsafeCreate(onSubscribe); + + w.serialize().subscribe(observer); + onSubscribe.waitToFinish(); + + verify(observer, times(1)).onNext("one"); + verify(observer, times(1)).onNext("two"); + verify(observer, times(1)).onNext("three"); + verify(observer, never()).onError(any(Throwable.class)); + verify(observer, times(1)).onComplete(); + // non-deterministic because unsubscribe happens after 'waitToFinish' releases + // so commenting out for now as this is not a critical thing to test here + // verify(s, times(1)).unsubscribe(); + } + + @Test + public void multiThreadedBasic() { + TestMultiThreadedObservable onSubscribe = new TestMultiThreadedObservable("one", "two", "three"); + Observable<String> w = Observable.unsafeCreate(onSubscribe); + + BusyObserver busyobserver = new BusyObserver(); + + w.serialize().subscribe(busyobserver); + onSubscribe.waitToFinish(); + + assertEquals(3, busyobserver.onNextCount.get()); + assertFalse(busyobserver.onError); + assertTrue(busyobserver.onComplete); + // non-deterministic because unsubscribe happens after 'waitToFinish' releases + // so commenting out for now as this is not a critical thing to test here + // verify(s, times(1)).unsubscribe(); + + // we can have concurrency ... + assertTrue(onSubscribe.maxConcurrentThreads.get() > 1); + // ... but the onNext execution should be single threaded + assertEquals(1, busyobserver.maxConcurrentThreads.get()); + } + + @Test + public void multiThreadedWithNPEFlaky() throws InterruptedException { + int max = 9; + for (int i = 0; i <= max; i++) { + try { + multiThreadedWithNPE(); + return; + } catch (AssertionError ex) { + if (i == max) { + throw ex; + } + } + Thread.sleep((long)(1000 * Math.random() + 100)); + } + } + + void multiThreadedWithNPE() { + TestMultiThreadedObservable onSubscribe = new TestMultiThreadedObservable("one", "two", "three", null); + Observable<String> w = Observable.unsafeCreate(onSubscribe); + + BusyObserver busyobserver = new BusyObserver(); + + w.serialize().subscribe(busyobserver); + onSubscribe.waitToFinish(); + + System.out.println("maxConcurrentThreads: " + onSubscribe.maxConcurrentThreads.get()); + + // we can't know how many onNext calls will occur since they each run on a separate thread + // that depends on thread scheduling so 0, 1, 2 and 3 are all valid options + // assertEquals(3, busyobserver.onNextCount.get()); + assertTrue(busyobserver.onNextCount.get() < 4); + assertTrue(busyobserver.onError); + // no onComplete because onError was invoked + assertFalse(busyobserver.onComplete); + // non-deterministic because unsubscribe happens after 'waitToFinish' releases + // so commenting out for now as this is not a critical thing to test here + //verify(s, times(1)).unsubscribe(); + + // we can have concurrency ... + assertTrue(onSubscribe.maxConcurrentThreads.get() > 1); + // ... but the onNext execution should be single threaded + assertEquals(1, busyobserver.maxConcurrentThreads.get()); + } + + @Test + public void multiThreadedWithNPEinMiddleFlaky() throws InterruptedException { + int max = 9; + for (int i = 0; i <= max; i++) { + try { + multiThreadedWithNPEinMiddle(); + return; + } catch (AssertionError ex) { + if (i == max) { + throw ex; + } + } + Thread.sleep((long)(1000 * Math.random() + 100)); + } + } + + void multiThreadedWithNPEinMiddle() { + boolean lessThan9 = false; + for (int i = 0; i < 3; i++) { + TestMultiThreadedObservable onSubscribe = new TestMultiThreadedObservable("one", "two", "three", null, "four", "five", "six", "seven", "eight", "nine"); + Observable<String> w = Observable.unsafeCreate(onSubscribe); + + BusyObserver busyobserver = new BusyObserver(); + + w.serialize().subscribe(busyobserver); + onSubscribe.waitToFinish(); + + System.out.println("maxConcurrentThreads: " + onSubscribe.maxConcurrentThreads.get()); + // this should not always be the full number of items since the error should (very often) + // stop it before it completes all 9 + System.out.println("onNext count: " + busyobserver.onNextCount.get()); + if (busyobserver.onNextCount.get() < 9) { + lessThan9 = true; + } + assertTrue(busyobserver.onError); + // no onComplete because onError was invoked + assertFalse(busyobserver.onComplete); + // non-deterministic because unsubscribe happens after 'waitToFinish' releases + // so commenting out for now as this is not a critical thing to test here + // verify(s, times(1)).unsubscribe(); + + // we can have concurrency ... + int n = onSubscribe.maxConcurrentThreads.get(); + assertTrue("" + n, n > 1); + // ... but the onNext execution should be single threaded + assertEquals(1, busyobserver.maxConcurrentThreads.get()); + } + assertTrue(lessThan9); + } + + /** + * A thread that will pass data to onNext. + */ + public static class OnNextThread implements Runnable { + + private final DefaultObserver<String> observer; + private final int numStringsToSend; + + OnNextThread(DefaultObserver<String> observer, int numStringsToSend) { + this.observer = observer; + this.numStringsToSend = numStringsToSend; + } + + @Override + public void run() { + for (int i = 0; i < numStringsToSend; i++) { + observer.onNext("aString"); + } + } + } + + /** + * A thread that will call onError or onNext. + */ + public static class CompletionThread implements Runnable { + + private final DefaultObserver<String> observer; + private final TestConcurrencyobserverEvent event; + private final Future<?>[] waitOnThese; + + CompletionThread(DefaultObserver<String> observer, TestConcurrencyobserverEvent event, Future<?>... waitOnThese) { + this.observer = observer; + this.event = event; + this.waitOnThese = waitOnThese; + } + + @Override + public void run() { + /* if we have 'waitOnThese' futures, we'll wait on them before proceeding */ + if (waitOnThese != null) { + for (Future<?> f : waitOnThese) { + try { + f.get(); + } catch (Throwable e) { + System.err.println("Error while waiting on future in CompletionThread"); + } + } + } + + /* send the event */ + if (event == TestConcurrencyobserverEvent.onError) { + observer.onError(new RuntimeException("mocked exception")); + } else if (event == TestConcurrencyobserverEvent.onComplete) { + observer.onComplete(); + + } else { + throw new IllegalArgumentException("Expecting either onError or onComplete"); + } + } + } + + enum TestConcurrencyobserverEvent { + onComplete, onError, onNext + } + + /** + * This spawns a single thread for the subscribe execution. + */ + static class TestSingleThreadedObservable implements ObservableSource<String> { + + final String[] values; + private Thread t; + + TestSingleThreadedObservable(final String... values) { + this.values = values; + + } + + @Override + public void subscribe(final Observer<? super String> observer) { + observer.onSubscribe(Disposable.empty()); + System.out.println("TestSingleThreadedObservable subscribed to ..."); + t = new Thread(new Runnable() { + + @Override + public void run() { + try { + System.out.println("running TestSingleThreadedObservable thread"); + for (String s : values) { + System.out.println("TestSingleThreadedObservable onNext: " + s); + observer.onNext(s); + } + observer.onComplete(); + } catch (Throwable e) { + throw new RuntimeException(e); + } + } + + }); + System.out.println("starting TestSingleThreadedObservable thread"); + t.start(); + System.out.println("done starting TestSingleThreadedObservable thread"); + } + + public void waitToFinish() { + try { + t.join(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + + } + + /** + * This spawns a thread for the subscription, then a separate thread for each onNext call. + */ + private static class TestMultiThreadedObservable implements ObservableSource<String> { + final String[] values; + Thread t; + AtomicInteger threadsRunning = new AtomicInteger(); + AtomicInteger maxConcurrentThreads = new AtomicInteger(); + ExecutorService threadPool; + + TestMultiThreadedObservable(String... values) { + this.values = values; + this.threadPool = Executors.newCachedThreadPool(); + } + + @Override + public void subscribe(final Observer<? super String> observer) { + observer.onSubscribe(Disposable.empty()); + System.out.println("TestMultiThreadedObservable subscribed to ..."); + final NullPointerException npe = new NullPointerException(); + t = new Thread(new Runnable() { + + @Override + public void run() { + try { + System.out.println("running TestMultiThreadedObservable thread"); + for (final String s : values) { + threadPool.execute(new Runnable() { + + @Override + public void run() { + threadsRunning.incrementAndGet(); + try { + // perform onNext call + if (s == null) { + System.out.println("TestMultiThreadedObservable onNext: null"); + // force an error + throw npe; + } else { + System.out.println("TestMultiThreadedObservable onNext: " + s); + } + observer.onNext(s); + // capture 'maxThreads' + int concurrentThreads = threadsRunning.get(); + int maxThreads = maxConcurrentThreads.get(); + if (concurrentThreads > maxThreads) { + maxConcurrentThreads.compareAndSet(maxThreads, concurrentThreads); + } + } catch (Throwable e) { + observer.onError(e); + } finally { + threadsRunning.decrementAndGet(); + } + } + }); + } + // we are done spawning threads + threadPool.shutdown(); + } catch (Throwable e) { + throw new RuntimeException(e); + } + + // wait until all threads are done, then mark it as COMPLETED + try { + // wait for all the threads to finish + threadPool.awaitTermination(2, TimeUnit.SECONDS); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + observer.onComplete(); + } + }); + System.out.println("starting TestMultiThreadedObservable thread"); + t.start(); + System.out.println("done starting TestMultiThreadedObservable thread"); + } + + public void waitToFinish() { + try { + t.join(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + } + + private static class BusyObserver extends DefaultObserver<String> { + volatile boolean onComplete; + volatile boolean onError; + AtomicInteger onNextCount = new AtomicInteger(); + AtomicInteger threadsRunning = new AtomicInteger(); + AtomicInteger maxConcurrentThreads = new AtomicInteger(); + + @Override + public void onComplete() { + threadsRunning.incrementAndGet(); + + System.out.println(">>> Busyobserver received onComplete"); + onComplete = true; + + int concurrentThreads = threadsRunning.get(); + int maxThreads = maxConcurrentThreads.get(); + if (concurrentThreads > maxThreads) { + maxConcurrentThreads.compareAndSet(maxThreads, concurrentThreads); + } + threadsRunning.decrementAndGet(); + } + + @Override + public void onError(Throwable e) { + threadsRunning.incrementAndGet(); + + System.out.println(">>> Busyobserver received onError: " + e.getMessage()); + onError = true; + + int concurrentThreads = threadsRunning.get(); + int maxThreads = maxConcurrentThreads.get(); + if (concurrentThreads > maxThreads) { + maxConcurrentThreads.compareAndSet(maxThreads, concurrentThreads); + } + threadsRunning.decrementAndGet(); + } + + @Override + public void onNext(String args) { + threadsRunning.incrementAndGet(); + try { + onNextCount.incrementAndGet(); + System.out.println(">>> Busyobserver received onNext: " + args); + try { + // simulate doing something computational + Thread.sleep(200); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } finally { + // capture 'maxThreads' + int concurrentThreads = threadsRunning.get(); + int maxThreads = maxConcurrentThreads.get(); + if (concurrentThreads > maxThreads) { + maxConcurrentThreads.compareAndSet(maxThreads, concurrentThreads); + } + threadsRunning.decrementAndGet(); + } + } + + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableSingleTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableSingleTest.java new file mode 100644 index 0000000000..c511f28baf --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableSingleTest.java @@ -0,0 +1,569 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +import java.util.NoSuchElementException; +import java.util.concurrent.atomic.AtomicReference; + +import org.junit.Test; +import org.mockito.InOrder; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class ObservableSingleTest extends RxJavaTest { + + @Test + public void singleObservable() { + Observable<Integer> o = Observable.just(1).singleElement().toObservable(); + + Observer<Integer> observer = TestHelper.mockObserver(); + o.subscribe(observer); + + InOrder inOrder = inOrder(observer); + inOrder.verify(observer, times(1)).onNext(1); + inOrder.verify(observer, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void singleWithTooManyElementsObservable() { + Observable<Integer> o = Observable.just(1, 2).singleElement().toObservable(); + + Observer<Integer> observer = TestHelper.mockObserver(); + o.subscribe(observer); + + InOrder inOrder = inOrder(observer); + inOrder.verify(observer, times(1)).onError( + isA(IllegalArgumentException.class)); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void singleWithEmptyObservable() { + Observable<Integer> o = Observable.<Integer> empty().singleElement().toObservable(); + + Observer<Integer> observer = TestHelper.mockObserver(); + o.subscribe(observer); + + InOrder inOrder = inOrder(observer); + inOrder.verify(observer).onComplete(); + inOrder.verify(observer, never()).onError(any(Throwable.class)); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void singleWithPredicateObservable() { + Observable<Integer> o = Observable.just(1, 2) + .filter( + new Predicate<Integer>() { + + @Override + public boolean test(Integer t1) { + return t1 % 2 == 0; + } + }) + .singleElement().toObservable(); + + Observer<Integer> observer = TestHelper.mockObserver(); + o.subscribe(observer); + + InOrder inOrder = inOrder(observer); + inOrder.verify(observer, times(1)).onNext(2); + inOrder.verify(observer, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void singleWithPredicateAndTooManyElementsObservable() { + Observable<Integer> o = Observable.just(1, 2, 3, 4) + .filter( + new Predicate<Integer>() { + + @Override + public boolean test(Integer t1) { + return t1 % 2 == 0; + } + }) + .singleElement().toObservable(); + + Observer<Integer> observer = TestHelper.mockObserver(); + o.subscribe(observer); + + InOrder inOrder = inOrder(observer); + inOrder.verify(observer, times(1)).onError( + isA(IllegalArgumentException.class)); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void singleWithPredicateAndEmptyObservable() { + Observable<Integer> o = Observable.just(1) + .filter( + new Predicate<Integer>() { + + @Override + public boolean test(Integer t1) { + return t1 % 2 == 0; + } + }) + .singleElement().toObservable(); + Observer<Integer> observer = TestHelper.mockObserver(); + o.subscribe(observer); + + InOrder inOrder = inOrder(observer); + inOrder.verify(observer).onComplete(); + inOrder.verify(observer, never()).onError(any(Throwable.class)); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void singleOrDefaultObservable() { + Observable<Integer> o = Observable.just(1).single(2).toObservable(); + + Observer<Integer> observer = TestHelper.mockObserver(); + o.subscribe(observer); + + InOrder inOrder = inOrder(observer); + inOrder.verify(observer, times(1)).onNext(1); + inOrder.verify(observer, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void singleOrDefaultWithTooManyElementsObservable() { + Observable<Integer> o = Observable.just(1, 2).single(3).toObservable(); + + Observer<Integer> observer = TestHelper.mockObserver(); + o.subscribe(observer); + + InOrder inOrder = inOrder(observer); + inOrder.verify(observer, times(1)).onError( + isA(IllegalArgumentException.class)); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void singleOrDefaultWithEmptyObservable() { + Observable<Integer> o = Observable.<Integer> empty() + .single(1).toObservable(); + + Observer<Integer> observer = TestHelper.mockObserver(); + o.subscribe(observer); + + InOrder inOrder = inOrder(observer); + inOrder.verify(observer, times(1)).onNext(1); + inOrder.verify(observer, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void singleOrDefaultWithPredicateObservable() { + Observable<Integer> o = Observable.just(1, 2) + .filter(new Predicate<Integer>() { + @Override + public boolean test(Integer t1) { + return t1 % 2 == 0; + } + }) + .single(4).toObservable(); + + Observer<Integer> observer = TestHelper.mockObserver(); + o.subscribe(observer); + + InOrder inOrder = inOrder(observer); + inOrder.verify(observer, times(1)).onNext(2); + inOrder.verify(observer, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void singleOrDefaultWithPredicateAndTooManyElementsObservable() { + Observable<Integer> o = Observable.just(1, 2, 3, 4) + .filter(new Predicate<Integer>() { + @Override + public boolean test(Integer t1) { + return t1 % 2 == 0; + } + }) + .single(6).toObservable(); + + Observer<Integer> observer = TestHelper.mockObserver(); + o.subscribe(observer); + + InOrder inOrder = inOrder(observer); + inOrder.verify(observer, times(1)).onError( + isA(IllegalArgumentException.class)); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void singleOrDefaultWithPredicateAndEmptyObservable() { + Observable<Integer> o = Observable.just(1) + .filter(new Predicate<Integer>() { + @Override + public boolean test(Integer t1) { + return t1 % 2 == 0; + } + }) + .single(2).toObservable(); + + Observer<Integer> observer = TestHelper.mockObserver(); + o.subscribe(observer); + + InOrder inOrder = inOrder(observer); + inOrder.verify(observer, times(1)).onNext(2); + inOrder.verify(observer, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void issue1527Observable() throws InterruptedException { + //https://github.com/ReactiveX/RxJava/pull/1527 + Observable<Integer> source = Observable.just(1, 2, 3, 4, 5, 6); + Observable<Integer> reduced = source.reduce(new BiFunction<Integer, Integer, Integer>() { + @Override + public Integer apply(Integer i1, Integer i2) { + return i1 + i2; + } + }).toObservable(); + + Integer r = reduced.blockingFirst(); + assertEquals(21, r.intValue()); + } + + @Test + public void single() { + Maybe<Integer> o = Observable.just(1).singleElement(); + + MaybeObserver<Integer> observer = TestHelper.mockMaybeObserver(); + o.subscribe(observer); + + InOrder inOrder = inOrder(observer); + inOrder.verify(observer, times(1)).onSuccess(1); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void singleWithTooManyElements() { + Maybe<Integer> o = Observable.just(1, 2).singleElement(); + + MaybeObserver<Integer> observer = TestHelper.mockMaybeObserver(); + o.subscribe(observer); + + InOrder inOrder = inOrder(observer); + inOrder.verify(observer, times(1)).onError( + isA(IllegalArgumentException.class)); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void singleWithEmpty() { + Maybe<Integer> o = Observable.<Integer> empty().singleElement(); + + MaybeObserver<Integer> observer = TestHelper.mockMaybeObserver(); + o.subscribe(observer); + + InOrder inOrder = inOrder(observer); + inOrder.verify(observer).onComplete(); + inOrder.verify(observer, never()).onError(any(Throwable.class)); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void singleWithPredicate() { + Maybe<Integer> o = Observable.just(1, 2) + .filter( + new Predicate<Integer>() { + + @Override + public boolean test(Integer t1) { + return t1 % 2 == 0; + } + }) + .singleElement(); + + MaybeObserver<Integer> observer = TestHelper.mockMaybeObserver(); + o.subscribe(observer); + + InOrder inOrder = inOrder(observer); + inOrder.verify(observer, times(1)).onSuccess(2); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void singleWithPredicateAndTooManyElements() { + Maybe<Integer> o = Observable.just(1, 2, 3, 4) + .filter( + new Predicate<Integer>() { + + @Override + public boolean test(Integer t1) { + return t1 % 2 == 0; + } + }) + .singleElement(); + + MaybeObserver<Integer> observer = TestHelper.mockMaybeObserver(); + o.subscribe(observer); + + InOrder inOrder = inOrder(observer); + inOrder.verify(observer, times(1)).onError( + isA(IllegalArgumentException.class)); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void singleWithPredicateAndEmpty() { + Maybe<Integer> o = Observable.just(1) + .filter( + new Predicate<Integer>() { + + @Override + public boolean test(Integer t1) { + return t1 % 2 == 0; + } + }) + .singleElement(); + MaybeObserver<Integer> observer = TestHelper.mockMaybeObserver(); + o.subscribe(observer); + + InOrder inOrder = inOrder(observer); + inOrder.verify(observer).onComplete(); + inOrder.verify(observer, never()).onError(any(Throwable.class)); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void singleOrDefault() { + Single<Integer> o = Observable.just(1).single(2); + + SingleObserver<Integer> observer = TestHelper.mockSingleObserver(); + o.subscribe(observer); + + InOrder inOrder = inOrder(observer); + inOrder.verify(observer, times(1)).onSuccess(1); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void singleOrDefaultWithTooManyElements() { + Single<Integer> o = Observable.just(1, 2).single(3); + + SingleObserver<Integer> observer = TestHelper.mockSingleObserver(); + o.subscribe(observer); + + InOrder inOrder = inOrder(observer); + inOrder.verify(observer, times(1)).onError( + isA(IllegalArgumentException.class)); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void singleOrDefaultWithEmpty() { + Single<Integer> o = Observable.<Integer> empty() + .single(1); + + SingleObserver<Integer> observer = TestHelper.mockSingleObserver(); + o.subscribe(observer); + + InOrder inOrder = inOrder(observer); + inOrder.verify(observer, times(1)).onSuccess(1); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void singleOrDefaultWithPredicate() { + Single<Integer> o = Observable.just(1, 2) + .filter(new Predicate<Integer>() { + @Override + public boolean test(Integer t1) { + return t1 % 2 == 0; + } + }) + .single(4); + + SingleObserver<Integer> observer = TestHelper.mockSingleObserver(); + o.subscribe(observer); + + InOrder inOrder = inOrder(observer); + inOrder.verify(observer, times(1)).onSuccess(2); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void singleOrDefaultWithPredicateAndTooManyElements() { + Single<Integer> o = Observable.just(1, 2, 3, 4) + .filter(new Predicate<Integer>() { + @Override + public boolean test(Integer t1) { + return t1 % 2 == 0; + } + }) + .single(6); + + SingleObserver<Integer> observer = TestHelper.mockSingleObserver(); + o.subscribe(observer); + + InOrder inOrder = inOrder(observer); + inOrder.verify(observer, times(1)).onError( + isA(IllegalArgumentException.class)); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void singleOrDefaultWithPredicateAndEmpty() { + Single<Integer> o = Observable.just(1) + .filter(new Predicate<Integer>() { + @Override + public boolean test(Integer t1) { + return t1 % 2 == 0; + } + }) + .single(2); + + SingleObserver<Integer> observer = TestHelper.mockSingleObserver(); + o.subscribe(observer); + + InOrder inOrder = inOrder(observer); + inOrder.verify(observer, times(1)).onSuccess(2); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void issue1527() throws InterruptedException { + //https://github.com/ReactiveX/RxJava/pull/1527 + Observable<Integer> source = Observable.just(1, 2, 3, 4, 5, 6); + Maybe<Integer> reduced = source.reduce(new BiFunction<Integer, Integer, Integer>() { + @Override + public Integer apply(Integer i1, Integer i2) { + return i1 + i2; + } + }); + + Integer r = reduced.blockingGet(); + assertEquals(21, r.intValue()); + } + + @Test + public void singleElementOperatorDoNotSwallowExceptionWhenDone() { + final Throwable exception = new RuntimeException("some error"); + final AtomicReference<Throwable> error = new AtomicReference<>(); + + try { + RxJavaPlugins.setErrorHandler(new Consumer<Throwable>() { + @Override public void accept(final Throwable throwable) throws Exception { + error.set(throwable); + } + }); + + Observable.unsafeCreate(new ObservableSource<Integer>() { + @Override public void subscribe(final Observer<? super Integer> observer) { + observer.onComplete(); + observer.onError(exception); + } + }).singleElement().test().assertComplete(); + + assertSame(exception, error.get().getCause()); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void singleOrErrorNoElement() { + Observable.empty() + .singleOrError() + .test() + .assertNoValues() + .assertError(NoSuchElementException.class); + } + + @Test + public void singleOrErrorOneElement() { + Observable.just(1) + .singleOrError() + .test() + .assertNoErrors() + .assertValue(1); + } + + @Test + public void singleOrErrorMultipleElements() { + Observable.just(1, 2, 3) + .singleOrError() + .test() + .assertNoValues() + .assertError(IllegalArgumentException.class); + } + + @Test + public void singleOrErrorError() { + Observable.error(new RuntimeException("error")) + .singleOrError() + .to(TestHelper.testConsumer()) + .assertNoValues() + .assertErrorMessage("error") + .assertError(RuntimeException.class); + } + + @Test + public void badSource() { + TestHelper.checkBadSourceObservable(new Function<Observable<Object>, Object>() { + @Override + public Object apply(Observable<Object> o) throws Exception { + return o.singleOrError(); + } + }, false, 1, 1, 1); + + TestHelper.checkBadSourceObservable(new Function<Observable<Object>, Object>() { + @Override + public Object apply(Observable<Object> o) throws Exception { + return o.singleElement(); + } + }, false, 1, 1, 1); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeObservableToSingle(new Function<Observable<Object>, SingleSource<Object>>() { + @Override + public SingleSource<Object> apply(Observable<Object> o) throws Exception { + return o.singleOrError(); + } + }); + + TestHelper.checkDoubleOnSubscribeObservableToMaybe(new Function<Observable<Object>, MaybeSource<Object>>() { + @Override + public MaybeSource<Object> apply(Observable<Object> o) throws Exception { + return o.singleElement(); + } + }); + } + + @Test + public void singleOrError() { + Observable.empty() + .singleOrError() + .toObservable() + .test() + .assertFailure(NoSuchElementException.class); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableSkipLastTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableSkipLastTest.java new file mode 100644 index 0000000000..1a10af31d2 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableSkipLastTest.java @@ -0,0 +1,123 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import java.util.Arrays; +import java.util.concurrent.TimeUnit; + +import org.junit.Test; +import org.mockito.InOrder; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.schedulers.Schedulers; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class ObservableSkipLastTest extends RxJavaTest { + + @Test + public void skipLastEmpty() { + Observable<String> o = Observable.<String> empty().skipLast(2); + + Observer<String> observer = TestHelper.mockObserver(); + o.subscribe(observer); + verify(observer, never()).onNext(any(String.class)); + verify(observer, never()).onError(any(Throwable.class)); + verify(observer, times(1)).onComplete(); + } + + @Test + public void skipLast1() { + Observable<String> o = Observable.fromIterable(Arrays.asList("one", "two", "three")).skipLast(2); + + Observer<String> observer = TestHelper.mockObserver(); + InOrder inOrder = inOrder(observer); + o.subscribe(observer); + inOrder.verify(observer, never()).onNext("two"); + inOrder.verify(observer, never()).onNext("three"); + verify(observer, times(1)).onNext("one"); + verify(observer, never()).onError(any(Throwable.class)); + verify(observer, times(1)).onComplete(); + } + + @Test + public void skipLast2() { + Observable<String> o = Observable.fromIterable(Arrays.asList("one", "two")).skipLast(2); + + Observer<String> observer = TestHelper.mockObserver(); + o.subscribe(observer); + verify(observer, never()).onNext(any(String.class)); + verify(observer, never()).onError(any(Throwable.class)); + verify(observer, times(1)).onComplete(); + } + + @Test + public void skipLastWithZeroCount() { + Observable<String> w = Observable.just("one", "two"); + Observable<String> observable = w.skipLast(0); + + Observer<String> observer = TestHelper.mockObserver(); + observable.subscribe(observer); + verify(observer, times(1)).onNext("one"); + verify(observer, times(1)).onNext("two"); + verify(observer, never()).onError(any(Throwable.class)); + verify(observer, times(1)).onComplete(); + } + + @Test + public void skipLastWithBackpressure() { + Observable<Integer> o = Observable.range(0, Flowable.bufferSize() * 2).skipLast(Flowable.bufferSize() + 10); + TestObserver<Integer> to = new TestObserver<>(); + o.observeOn(Schedulers.computation()).subscribe(to); + to.awaitDone(5, TimeUnit.SECONDS); + to.assertNoErrors(); + assertEquals((Flowable.bufferSize()) - 10, to.values().size()); + + } + + @Test(expected = IllegalArgumentException.class) + public void skipLastWithNegativeCount() { + Observable.just("one").skipLast(-1); + } + + @Test + public void dispose() { + TestHelper.checkDisposed(Observable.just(1).skipLast(1)); + } + + @Test + public void error() { + Observable.error(new TestException()) + .skipLast(1) + .test() + .assertFailure(TestException.class); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeObservable(new Function<Observable<Object>, Observable<Object>>() { + @Override + public Observable<Object> apply(Observable<Object> o) + throws Exception { + return o.skipLast(1); + } + }); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableSkipLastTimedTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableSkipLastTimedTest.java new file mode 100644 index 0000000000..a4171ea2df --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableSkipLastTimedTest.java @@ -0,0 +1,438 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import java.util.concurrent.TimeUnit; + +import org.junit.Test; +import org.mockito.InOrder; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.schedulers.*; +import io.reactivex.rxjava3.subjects.PublishSubject; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class ObservableSkipLastTimedTest extends RxJavaTest { + + @Test + public void skipLastTimed() { + TestScheduler scheduler = new TestScheduler(); + + PublishSubject<Integer> source = PublishSubject.create(); + + // FIXME the timeunit now matters due to rounding + Observable<Integer> result = source.skipLast(1000, TimeUnit.MILLISECONDS, scheduler); + + Observer<Object> o = TestHelper.mockObserver(); + + result.subscribe(o); + + source.onNext(1); + source.onNext(2); + source.onNext(3); + + scheduler.advanceTimeBy(500, TimeUnit.MILLISECONDS); + + source.onNext(4); + source.onNext(5); + source.onNext(6); + + scheduler.advanceTimeBy(950, TimeUnit.MILLISECONDS); + source.onComplete(); + + InOrder inOrder = inOrder(o); + inOrder.verify(o).onNext(1); + inOrder.verify(o).onNext(2); + inOrder.verify(o).onNext(3); + inOrder.verify(o, never()).onNext(4); + inOrder.verify(o, never()).onNext(5); + inOrder.verify(o, never()).onNext(6); + inOrder.verify(o).onComplete(); + inOrder.verifyNoMoreInteractions(); + + verify(o, never()).onError(any(Throwable.class)); + } + + @Test + public void skipLastTimedErrorBeforeTime() { + TestScheduler scheduler = new TestScheduler(); + + PublishSubject<Integer> source = PublishSubject.create(); + + Observable<Integer> result = source.skipLast(1, TimeUnit.SECONDS, scheduler); + + Observer<Object> o = TestHelper.mockObserver(); + + result.subscribe(o); + + source.onNext(1); + source.onNext(2); + source.onNext(3); + source.onError(new TestException()); + + scheduler.advanceTimeBy(1050, TimeUnit.MILLISECONDS); + + verify(o).onError(any(TestException.class)); + + verify(o, never()).onComplete(); + verify(o, never()).onNext(any()); + } + + @Test + public void skipLastTimedCompleteBeforeTime() { + TestScheduler scheduler = new TestScheduler(); + + PublishSubject<Integer> source = PublishSubject.create(); + + Observable<Integer> result = source.skipLast(1, TimeUnit.SECONDS, scheduler); + + Observer<Object> o = TestHelper.mockObserver(); + + result.subscribe(o); + + source.onNext(1); + source.onNext(2); + source.onNext(3); + + scheduler.advanceTimeBy(500, TimeUnit.MILLISECONDS); + + source.onComplete(); + + InOrder inOrder = inOrder(o); + inOrder.verify(o).onComplete(); + inOrder.verifyNoMoreInteractions(); + + verify(o, never()).onNext(any()); + verify(o, never()).onError(any(Throwable.class)); + } + + @Test + public void skipLastTimedWhenAllElementsAreValid() { + TestScheduler scheduler = new TestScheduler(); + + PublishSubject<Integer> source = PublishSubject.create(); + + Observable<Integer> result = source.skipLast(1, TimeUnit.MILLISECONDS, scheduler); + + Observer<Object> o = TestHelper.mockObserver(); + + result.subscribe(o); + + source.onNext(1); + source.onNext(2); + source.onNext(3); + + scheduler.advanceTimeBy(500, TimeUnit.MILLISECONDS); + + source.onComplete(); + + InOrder inOrder = inOrder(o); + inOrder.verify(o).onNext(1); + inOrder.verify(o).onNext(2); + inOrder.verify(o).onNext(3); + inOrder.verify(o).onComplete(); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void skipLastTimedDefaultScheduler() { + Observable.just(1).concatWith(Observable.just(2).delay(500, TimeUnit.MILLISECONDS)) + .skipLast(300, TimeUnit.MILLISECONDS) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(1); + } + + @Test + public void skipLastTimedDefaultSchedulerDelayError() { + Observable.just(1).concatWith(Observable.just(2).delay(500, TimeUnit.MILLISECONDS)) + .skipLast(300, TimeUnit.MILLISECONDS, true) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(1); + } + + @Test + public void skipLastTimedCustomSchedulerDelayError() { + Observable.just(1).concatWith(Observable.just(2).delay(500, TimeUnit.MILLISECONDS)) + .skipLast(300, TimeUnit.MILLISECONDS, Schedulers.io(), true) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(1); + } + + @Test + public void dispose() { + TestHelper.checkDisposed(PublishSubject.create().skipLast(1, TimeUnit.DAYS)); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeObservable(new Function<Observable<Object>, ObservableSource<Object>>() { + @Override + public ObservableSource<Object> apply(Observable<Object> o) throws Exception { + return o.skipLast(1, TimeUnit.DAYS); + } + }); + } + + @Test + public void onCompleteDisposeRace() { + TestScheduler scheduler = new TestScheduler(); + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final PublishSubject<Integer> ps = PublishSubject.create(); + + final TestObserver<Integer> to = ps.skipLast(1, TimeUnit.DAYS, scheduler).test(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + ps.onComplete(); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + to.dispose(); + } + }; + + TestHelper.race(r1, r2); + } + } + + @Test + public void onCompleteDisposeDelayErrorRace() { + TestScheduler scheduler = new TestScheduler(); + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final PublishSubject<Integer> ps = PublishSubject.create(); + + final TestObserver<Integer> to = ps.skipLast(1, TimeUnit.DAYS, scheduler, true).test(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + ps.onComplete(); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + to.dispose(); + } + }; + + TestHelper.race(r1, r2); + } + } + + @Test + public void errorDelayed() { + Observable.error(new TestException()) + .skipLast(1, TimeUnit.DAYS, new TestScheduler(), true) + .test() + .assertFailure(TestException.class); + } + + @Test + public void take() { + Observable.just(1) + .skipLast(0, TimeUnit.SECONDS) + .take(1) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(1); + } + + @Test + public void onNextDisposeRace() { + TestScheduler scheduler = new TestScheduler(); + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final PublishSubject<Integer> ps = PublishSubject.create(); + + final TestObserver<Integer> to = ps.skipLast(1, TimeUnit.DAYS, scheduler).test(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + ps.onNext(1); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + to.dispose(); + } + }; + + TestHelper.race(r1, r2); + } + } + + @Test + public void onNextOnCompleteDisposeDelayErrorRace() { + TestScheduler scheduler = new TestScheduler(); + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final PublishSubject<Integer> ps = PublishSubject.create(); + + final TestObserver<Integer> to = ps.skipLast(1, TimeUnit.DAYS, scheduler, true).test(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + ps.onNext(1); + ps.onComplete(); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + to.dispose(); + } + }; + + TestHelper.race(r1, r2); + } + } + + @Test + public void skipLastTimedDelayError() { + TestScheduler scheduler = new TestScheduler(); + + PublishSubject<Integer> source = PublishSubject.create(); + + // FIXME the timeunit now matters due to rounding + Observable<Integer> result = source.skipLast(1000, TimeUnit.MILLISECONDS, scheduler, true); + + Observer<Object> o = TestHelper.mockObserver(); + + result.subscribe(o); + + source.onNext(1); + source.onNext(2); + source.onNext(3); + + scheduler.advanceTimeBy(500, TimeUnit.MILLISECONDS); + + source.onNext(4); + source.onNext(5); + source.onNext(6); + + scheduler.advanceTimeBy(950, TimeUnit.MILLISECONDS); + source.onComplete(); + + InOrder inOrder = inOrder(o); + inOrder.verify(o).onNext(1); + inOrder.verify(o).onNext(2); + inOrder.verify(o).onNext(3); + inOrder.verify(o, never()).onNext(4); + inOrder.verify(o, never()).onNext(5); + inOrder.verify(o, never()).onNext(6); + inOrder.verify(o).onComplete(); + inOrder.verifyNoMoreInteractions(); + + verify(o, never()).onError(any(Throwable.class)); + } + + @Test + public void skipLastTimedErrorBeforeTimeDelayError() { + TestScheduler scheduler = new TestScheduler(); + + PublishSubject<Integer> source = PublishSubject.create(); + + Observable<Integer> result = source.skipLast(1, TimeUnit.SECONDS, scheduler, true); + + Observer<Object> o = TestHelper.mockObserver(); + + result.subscribe(o); + + source.onNext(1); + source.onNext(2); + source.onNext(3); + source.onError(new TestException()); + + scheduler.advanceTimeBy(1050, TimeUnit.MILLISECONDS); + + verify(o).onError(any(TestException.class)); + + verify(o, never()).onComplete(); + verify(o, never()).onNext(any()); + } + + @Test + public void skipLastTimedCompleteBeforeTimeDelayError() { + TestScheduler scheduler = new TestScheduler(); + + PublishSubject<Integer> source = PublishSubject.create(); + + Observable<Integer> result = source.skipLast(1, TimeUnit.SECONDS, scheduler, true); + + Observer<Object> o = TestHelper.mockObserver(); + + result.subscribe(o); + + source.onNext(1); + source.onNext(2); + source.onNext(3); + + scheduler.advanceTimeBy(500, TimeUnit.MILLISECONDS); + + source.onComplete(); + + InOrder inOrder = inOrder(o); + inOrder.verify(o).onComplete(); + inOrder.verifyNoMoreInteractions(); + + verify(o, never()).onNext(any()); + verify(o, never()).onError(any(Throwable.class)); + } + + @Test + public void skipLastTimedWhenAllElementsAreValidDelayError() { + TestScheduler scheduler = new TestScheduler(); + + PublishSubject<Integer> source = PublishSubject.create(); + + Observable<Integer> result = source.skipLast(1, TimeUnit.MILLISECONDS, scheduler, true); + + Observer<Object> o = TestHelper.mockObserver(); + + result.subscribe(o); + + source.onNext(1); + source.onNext(2); + source.onNext(3); + + scheduler.advanceTimeBy(500, TimeUnit.MILLISECONDS); + + source.onComplete(); + + InOrder inOrder = inOrder(o); + inOrder.verify(o).onNext(1); + inOrder.verify(o).onNext(2); + inOrder.verify(o).onNext(3); + inOrder.verify(o).onComplete(); + inOrder.verifyNoMoreInteractions(); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableSkipTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableSkipTest.java new file mode 100644 index 0000000000..c1fe2bf2bc --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableSkipTest.java @@ -0,0 +1,166 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import java.util.Arrays; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.testsupport.*; + +public class ObservableSkipTest extends RxJavaTest { + + @Test(expected = IllegalArgumentException.class) + public void skipNegativeElements() { + + Observable<String> skip = Observable.just("one", "two", "three").skip(-99); + + Observer<String> observer = TestHelper.mockObserver(); + skip.subscribe(observer); + verify(observer, times(1)).onNext("one"); + verify(observer, times(1)).onNext("two"); + verify(observer, times(1)).onNext("three"); + verify(observer, never()).onError(any(Throwable.class)); + verify(observer, times(1)).onComplete(); + } + + @Test + public void skipZeroElements() { + + Observable<String> skip = Observable.just("one", "two", "three").skip(0); + + Observer<String> observer = TestHelper.mockObserver(); + skip.subscribe(observer); + verify(observer, times(1)).onNext("one"); + verify(observer, times(1)).onNext("two"); + verify(observer, times(1)).onNext("three"); + verify(observer, never()).onError(any(Throwable.class)); + verify(observer, times(1)).onComplete(); + } + + @Test + public void skipOneElement() { + + Observable<String> skip = Observable.just("one", "two", "three").skip(1); + + Observer<String> observer = TestHelper.mockObserver(); + skip.subscribe(observer); + verify(observer, never()).onNext("one"); + verify(observer, times(1)).onNext("two"); + verify(observer, times(1)).onNext("three"); + verify(observer, never()).onError(any(Throwable.class)); + verify(observer, times(1)).onComplete(); + } + + @Test + public void skipTwoElements() { + + Observable<String> skip = Observable.just("one", "two", "three").skip(2); + + Observer<String> observer = TestHelper.mockObserver(); + skip.subscribe(observer); + verify(observer, never()).onNext("one"); + verify(observer, never()).onNext("two"); + verify(observer, times(1)).onNext("three"); + verify(observer, never()).onError(any(Throwable.class)); + verify(observer, times(1)).onComplete(); + } + + @Test + public void skipEmptyStream() { + + Observable<String> w = Observable.empty(); + Observable<String> skip = w.skip(1); + + Observer<String> observer = TestHelper.mockObserver(); + skip.subscribe(observer); + verify(observer, never()).onNext(any(String.class)); + verify(observer, never()).onError(any(Throwable.class)); + verify(observer, times(1)).onComplete(); + } + + @Test + public void skipMultipleObservers() { + + Observable<String> skip = Observable.just("one", "two", "three") + .skip(2); + + Observer<String> observer1 = TestHelper.mockObserver(); + skip.subscribe(observer1); + + Observer<String> observer2 = TestHelper.mockObserver(); + skip.subscribe(observer2); + + verify(observer1, times(1)).onNext(any(String.class)); + verify(observer1, never()).onError(any(Throwable.class)); + verify(observer1, times(1)).onComplete(); + + verify(observer2, times(1)).onNext(any(String.class)); + verify(observer2, never()).onError(any(Throwable.class)); + verify(observer2, times(1)).onComplete(); + } + + @Test + public void skipError() { + + Exception e = new Exception(); + + Observable<String> ok = Observable.just("one"); + Observable<String> error = Observable.error(e); + + Observable<String> skip = Observable.concat(ok, error).skip(100); + + Observer<String> observer = TestHelper.mockObserver(); + skip.subscribe(observer); + + verify(observer, never()).onNext(any(String.class)); + verify(observer, times(1)).onError(e); + verify(observer, never()).onComplete(); + + } + + @Test + public void requestOverflowDoesNotOccur() { + TestObserverEx<Integer> to = new TestObserverEx<>(); + + Observable.range(1, 10).skip(5).subscribe(to); + + to.assertTerminated(); + to.assertComplete(); + to.assertNoErrors(); + assertEquals(Arrays.asList(6, 7, 8, 9, 10), to.values()); + } + + @Test + public void dispose() { + TestHelper.checkDisposed(Observable.just(1).skip(2)); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeObservable(new Function<Observable<Object>, Observable<Object>>() { + @Override + public Observable<Object> apply(Observable<Object> o) + throws Exception { + return o.skip(1); + } + }); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableSkipTimedTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableSkipTimedTest.java new file mode 100644 index 0000000000..83edc3201b --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableSkipTimedTest.java @@ -0,0 +1,168 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import java.util.concurrent.TimeUnit; + +import org.junit.Test; +import org.mockito.InOrder; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.schedulers.TestScheduler; +import io.reactivex.rxjava3.subjects.PublishSubject; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class ObservableSkipTimedTest extends RxJavaTest { + + @Test + public void skipTimed() { + TestScheduler scheduler = new TestScheduler(); + + PublishSubject<Integer> source = PublishSubject.create(); + + Observable<Integer> result = source.skip(1, TimeUnit.SECONDS, scheduler); + + Observer<Object> o = TestHelper.mockObserver(); + + result.subscribe(o); + + source.onNext(1); + source.onNext(2); + source.onNext(3); + + scheduler.advanceTimeBy(1, TimeUnit.SECONDS); + + source.onNext(4); + source.onNext(5); + source.onNext(6); + + source.onComplete(); + + InOrder inOrder = inOrder(o); + + inOrder.verify(o, never()).onNext(1); + inOrder.verify(o, never()).onNext(2); + inOrder.verify(o, never()).onNext(3); + inOrder.verify(o).onNext(4); + inOrder.verify(o).onNext(5); + inOrder.verify(o).onNext(6); + inOrder.verify(o).onComplete(); + inOrder.verifyNoMoreInteractions(); + verify(o, never()).onError(any(Throwable.class)); + } + + @Test + public void skipTimedFinishBeforeTime() { + TestScheduler scheduler = new TestScheduler(); + + PublishSubject<Integer> source = PublishSubject.create(); + + Observable<Integer> result = source.skip(1, TimeUnit.SECONDS, scheduler); + + Observer<Object> o = TestHelper.mockObserver(); + + result.subscribe(o); + + source.onNext(1); + source.onNext(2); + source.onNext(3); + source.onComplete(); + + scheduler.advanceTimeBy(1, TimeUnit.SECONDS); + + InOrder inOrder = inOrder(o); + + inOrder.verify(o).onComplete(); + inOrder.verifyNoMoreInteractions(); + verify(o, never()).onNext(any()); + verify(o, never()).onError(any(Throwable.class)); + } + + @Test + public void skipTimedErrorBeforeTime() { + TestScheduler scheduler = new TestScheduler(); + + PublishSubject<Integer> source = PublishSubject.create(); + + Observable<Integer> result = source.skip(1, TimeUnit.SECONDS, scheduler); + + Observer<Object> o = TestHelper.mockObserver(); + + result.subscribe(o); + + source.onNext(1); + source.onNext(2); + source.onNext(3); + source.onError(new TestException()); + + scheduler.advanceTimeBy(1, TimeUnit.SECONDS); + + InOrder inOrder = inOrder(o); + + inOrder.verify(o).onError(any(TestException.class)); + inOrder.verifyNoMoreInteractions(); + verify(o, never()).onNext(any()); + verify(o, never()).onComplete(); + } + + @Test + public void skipTimedErrorAfterTime() { + TestScheduler scheduler = new TestScheduler(); + + PublishSubject<Integer> source = PublishSubject.create(); + + Observable<Integer> result = source.skip(1, TimeUnit.SECONDS, scheduler); + + Observer<Object> o = TestHelper.mockObserver(); + + result.subscribe(o); + + source.onNext(1); + source.onNext(2); + source.onNext(3); + + scheduler.advanceTimeBy(1, TimeUnit.SECONDS); + + source.onNext(4); + source.onNext(5); + source.onNext(6); + + source.onError(new TestException()); + + InOrder inOrder = inOrder(o); + + inOrder.verify(o, never()).onNext(1); + inOrder.verify(o, never()).onNext(2); + inOrder.verify(o, never()).onNext(3); + inOrder.verify(o).onNext(4); + inOrder.verify(o).onNext(5); + inOrder.verify(o).onNext(6); + inOrder.verify(o).onError(any(TestException.class)); + inOrder.verifyNoMoreInteractions(); + verify(o, never()).onComplete(); + + } + + @Test + public void skipTimedDefaultScheduler() { + Observable.just(1).skip(1, TimeUnit.MINUTES) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableSkipUntilTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableSkipUntilTest.java new file mode 100644 index 0000000000..bb2c3ef127 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableSkipUntilTest.java @@ -0,0 +1,179 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import org.junit.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.subjects.PublishSubject; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class ObservableSkipUntilTest extends RxJavaTest { + Observer<Object> observer; + + @Before + public void before() { + observer = TestHelper.mockObserver(); + } + + @Test + public void normal1() { + PublishSubject<Integer> source = PublishSubject.create(); + PublishSubject<Integer> other = PublishSubject.create(); + + Observable<Integer> m = source.skipUntil(other); + m.subscribe(observer); + + source.onNext(0); + source.onNext(1); + + other.onNext(100); + + source.onNext(2); + source.onNext(3); + source.onNext(4); + source.onComplete(); + + verify(observer, never()).onError(any(Throwable.class)); + verify(observer, times(1)).onNext(2); + verify(observer, times(1)).onNext(3); + verify(observer, times(1)).onNext(4); + verify(observer, times(1)).onComplete(); + } + + @Test + public void otherNeverFires() { + PublishSubject<Integer> source = PublishSubject.create(); + + Observable<Integer> m = source.skipUntil(Observable.never()); + + m.subscribe(observer); + + source.onNext(0); + source.onNext(1); + source.onNext(2); + source.onNext(3); + source.onNext(4); + source.onComplete(); + + verify(observer, never()).onError(any(Throwable.class)); + verify(observer, never()).onNext(any()); + verify(observer, times(1)).onComplete(); + } + + @Test + public void otherEmpty() { + PublishSubject<Integer> source = PublishSubject.create(); + + Observable<Integer> m = source.skipUntil(Observable.empty()); + + m.subscribe(observer); + + verify(observer, never()).onError(any(Throwable.class)); + verify(observer, never()).onNext(any()); + verify(observer, never()).onComplete(); + } + + @Test + public void otherFiresAndCompletes() { + PublishSubject<Integer> source = PublishSubject.create(); + PublishSubject<Integer> other = PublishSubject.create(); + + Observable<Integer> m = source.skipUntil(other); + m.subscribe(observer); + + source.onNext(0); + source.onNext(1); + + other.onNext(100); + other.onComplete(); + + source.onNext(2); + source.onNext(3); + source.onNext(4); + source.onComplete(); + + verify(observer, never()).onError(any(Throwable.class)); + verify(observer, times(1)).onNext(2); + verify(observer, times(1)).onNext(3); + verify(observer, times(1)).onNext(4); + verify(observer, times(1)).onComplete(); + } + + @Test + public void sourceThrows() { + PublishSubject<Integer> source = PublishSubject.create(); + PublishSubject<Integer> other = PublishSubject.create(); + + Observable<Integer> m = source.skipUntil(other); + m.subscribe(observer); + + source.onNext(0); + source.onNext(1); + + other.onNext(100); + other.onComplete(); + + source.onNext(2); + source.onError(new RuntimeException("Forced failure")); + + verify(observer, times(1)).onNext(2); + verify(observer, times(1)).onError(any(Throwable.class)); + verify(observer, never()).onComplete(); + } + + @Test + public void otherThrowsImmediately() { + PublishSubject<Integer> source = PublishSubject.create(); + PublishSubject<Integer> other = PublishSubject.create(); + + Observable<Integer> m = source.skipUntil(other); + m.subscribe(observer); + + source.onNext(0); + source.onNext(1); + + other.onError(new RuntimeException("Forced failure")); + + verify(observer, never()).onNext(any()); + verify(observer, times(1)).onError(any(Throwable.class)); + verify(observer, never()).onComplete(); + } + + @Test + public void dispose() { + TestHelper.checkDisposed(PublishSubject.create().skipUntil(PublishSubject.create())); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeObservable(new Function<Observable<Object>, ObservableSource<Object>>() { + @Override + public ObservableSource<Object> apply(Observable<Object> o) throws Exception { + return o.skipUntil(Observable.never()); + } + }); + + TestHelper.checkDoubleOnSubscribeObservable(new Function<Observable<Object>, ObservableSource<Object>>() { + @Override + public ObservableSource<Object> apply(Observable<Object> o) throws Exception { + return Observable.never().skipUntil(o); + } + }); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableSkipWhileTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableSkipWhileTest.java new file mode 100644 index 0000000000..b6a0749088 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableSkipWhileTest.java @@ -0,0 +1,159 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +import org.junit.Test; +import org.mockito.InOrder; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.subjects.PublishSubject; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class ObservableSkipWhileTest extends RxJavaTest { + + Observer<Integer> w = TestHelper.mockObserver(); + + private static final Predicate<Integer> LESS_THAN_FIVE = new Predicate<Integer>() { + @Override + public boolean test(Integer v) { + if (v == 42) { + throw new RuntimeException("that's not the answer to everything!"); + } + return v < 5; + } + }; + + private static final Predicate<Integer> INDEX_LESS_THAN_THREE = new Predicate<Integer>() { + int index; + @Override + public boolean test(Integer value) { + return index++ < 3; + } + }; + + @Test + public void skipWithIndex() { + Observable<Integer> src = Observable.just(1, 2, 3, 4, 5); + src.skipWhile(INDEX_LESS_THAN_THREE).subscribe(w); + + InOrder inOrder = inOrder(w); + inOrder.verify(w, times(1)).onNext(4); + inOrder.verify(w, times(1)).onNext(5); + inOrder.verify(w, times(1)).onComplete(); + inOrder.verify(w, never()).onError(any(Throwable.class)); + } + + @Test + public void skipEmpty() { + Observable<Integer> src = Observable.empty(); + src.skipWhile(LESS_THAN_FIVE).subscribe(w); + verify(w, never()).onNext(anyInt()); + verify(w, never()).onError(any(Throwable.class)); + verify(w, times(1)).onComplete(); + } + + @Test + public void skipEverything() { + Observable<Integer> src = Observable.just(1, 2, 3, 4, 3, 2, 1); + src.skipWhile(LESS_THAN_FIVE).subscribe(w); + verify(w, never()).onNext(anyInt()); + verify(w, never()).onError(any(Throwable.class)); + verify(w, times(1)).onComplete(); + } + + @Test + public void skipNothing() { + Observable<Integer> src = Observable.just(5, 3, 1); + src.skipWhile(LESS_THAN_FIVE).subscribe(w); + + InOrder inOrder = inOrder(w); + inOrder.verify(w, times(1)).onNext(5); + inOrder.verify(w, times(1)).onNext(3); + inOrder.verify(w, times(1)).onNext(1); + inOrder.verify(w, times(1)).onComplete(); + inOrder.verify(w, never()).onError(any(Throwable.class)); + } + + @Test + public void skipSome() { + Observable<Integer> src = Observable.just(1, 2, 3, 4, 5, 3, 1, 5); + src.skipWhile(LESS_THAN_FIVE).subscribe(w); + + InOrder inOrder = inOrder(w); + inOrder.verify(w, times(1)).onNext(5); + inOrder.verify(w, times(1)).onNext(3); + inOrder.verify(w, times(1)).onNext(1); + inOrder.verify(w, times(1)).onNext(5); + inOrder.verify(w, times(1)).onComplete(); + inOrder.verify(w, never()).onError(any(Throwable.class)); + } + + @Test + public void skipError() { + Observable<Integer> src = Observable.just(1, 2, 42, 5, 3, 1); + src.skipWhile(LESS_THAN_FIVE).subscribe(w); + + InOrder inOrder = inOrder(w); + inOrder.verify(w, never()).onNext(anyInt()); + inOrder.verify(w, never()).onComplete(); + inOrder.verify(w, times(1)).onError(any(RuntimeException.class)); + } + + @Test + public void skipManySubscribers() { + Observable<Integer> src = Observable.range(1, 10).skipWhile(LESS_THAN_FIVE); + int n = 5; + for (int i = 0; i < n; i++) { + Observer<Object> o = TestHelper.mockObserver(); + InOrder inOrder = inOrder(o); + + src.subscribe(o); + + for (int j = 5; j < 10; j++) { + inOrder.verify(o).onNext(j); + } + inOrder.verify(o).onComplete(); + verify(o, never()).onError(any(Throwable.class)); + } + } + + @Test + public void dispose() { + TestHelper.checkDisposed(PublishSubject.create().skipWhile(Functions.alwaysFalse())); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeObservable(new Function<Observable<Object>, ObservableSource<Object>>() { + @Override + public ObservableSource<Object> apply(Observable<Object> o) throws Exception { + return o.skipWhile(Functions.alwaysFalse()); + } + }); + } + + @Test + public void error() { + Observable.error(new TestException()) + .skipWhile(Functions.alwaysFalse()) + .test() + .assertFailure(TestException.class); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableStartWithTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableStartWithTest.java new file mode 100644 index 0000000000..267f5be42a --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableStartWithTest.java @@ -0,0 +1,153 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import static org.mockito.Mockito.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; + +public class ObservableStartWithTest { + + @Test + public void justCompletableComplete() { + Observable.just(1).startWith(Completable.complete()) + .test() + .assertResult(1); + } + + @Test + public void emptyCompletableComplete() { + Observable.empty().startWith(Completable.complete()) + .test() + .assertResult(); + } + + @Test + public void runCompletableError() { + Runnable run = mock(Runnable.class); + + Observable.fromRunnable(run).startWith(Completable.error(new TestException())) + .test() + .assertFailure(TestException.class); + + verify(run, never()).run(); + } + + @Test + public void justSingleJust() { + Observable.just(1).startWith(Single.just(2)) + .test() + .assertResult(2, 1); + } + + @Test + public void emptySingleJust() { + Runnable run = mock(Runnable.class); + + Observable.fromRunnable(run) + .startWith(Single.just(2)) + .test() + .assertResult(2); + + verify(run).run(); + } + + @Test + public void runSingleError() { + Runnable run = mock(Runnable.class); + + Observable.fromRunnable(run).startWith(Single.error(new TestException())) + .test() + .assertFailure(TestException.class); + + verify(run, never()).run(); + } + + @Test + public void justMaybeJust() { + Observable.just(1).startWith(Maybe.just(2)) + .test() + .assertResult(2, 1); + } + + @Test + public void emptyMaybeJust() { + Runnable run = mock(Runnable.class); + + Observable.fromRunnable(run) + .startWith(Maybe.just(2)) + .test() + .assertResult(2); + + verify(run).run(); + } + + @Test + public void runMaybeError() { + Runnable run = mock(Runnable.class); + + Observable.fromRunnable(run).startWith(Maybe.error(new TestException())) + .test() + .assertFailure(TestException.class); + + verify(run, never()).run(); + } + + @Test + public void justObservableJust() { + Observable.just(1).startWith(Observable.just(2, 3, 4, 5)) + .test() + .assertResult(2, 3, 4, 5, 1); + } + + @Test + public void emptyObservableJust() { + Runnable run = mock(Runnable.class); + + Observable.fromRunnable(run) + .startWith(Observable.just(2, 3, 4, 5)) + .test() + .assertResult(2, 3, 4, 5); + + verify(run).run(); + } + + @Test + public void emptyObservableEmpty() { + Runnable run = mock(Runnable.class); + Runnable run2 = mock(Runnable.class); + + Observable.fromRunnable(run) + .startWith(Observable.fromRunnable(run2)) + .test() + .assertResult(); + + verify(run).run(); + verify(run2).run(); + } + + @Test + public void runObservableError() { + Runnable run = mock(Runnable.class); + + Observable.fromRunnable(run).startWith(Observable.error(new TestException())) + .test() + .assertFailure(TestException.class); + + verify(run, never()).run(); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableSubscribeOnTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableSubscribeOnTest.java new file mode 100644 index 0000000000..ab3890dcc6 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableSubscribeOnTest.java @@ -0,0 +1,196 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import static org.junit.Assert.assertEquals; + +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.Test; + +import io.reactivex.rxjava3.annotations.NonNull; +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.*; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.schedulers.*; +import io.reactivex.rxjava3.testsupport.*; + +public class ObservableSubscribeOnTest extends RxJavaTest { + + @Test + public void issue813() throws InterruptedException { + // https://github.com/ReactiveX/RxJava/issues/813 + final CountDownLatch scheduled = new CountDownLatch(1); + final CountDownLatch latch = new CountDownLatch(1); + final CountDownLatch doneLatch = new CountDownLatch(1); + + TestObserver<Integer> to = new TestObserver<>(); + + Observable + .unsafeCreate(new ObservableSource<Integer>() { + @Override + public void subscribe( + final Observer<? super Integer> observer) { + observer.onSubscribe(Disposable.empty()); + scheduled.countDown(); + try { + try { + latch.await(); + } catch (InterruptedException e) { + // this means we were unsubscribed (Scheduler shut down and interrupts) + // ... but we'll pretend we are like many Observables that ignore interrupts + } + + observer.onComplete(); + } catch (Throwable e) { + observer.onError(e); + } finally { + doneLatch.countDown(); + } + } + }).subscribeOn(Schedulers.computation()).subscribe(to); + + // wait for scheduling + scheduled.await(); + // trigger unsubscribe + to.dispose(); + latch.countDown(); + doneLatch.await(); + to.assertNoErrors(); + to.assertComplete(); + } + + @Test + public void onError() { + TestObserverEx<String> to = new TestObserverEx<>(); + Observable.unsafeCreate(new ObservableSource<String>() { + + @Override + public void subscribe(Observer<? super String> observer) { + observer.onSubscribe(Disposable.empty()); + observer.onError(new RuntimeException("fail")); + } + + }).subscribeOn(Schedulers.computation()).subscribe(to); + to.awaitDone(1000, TimeUnit.MILLISECONDS); + to.assertTerminated(); + } + + public static class SlowScheduler extends Scheduler { + final Scheduler actual; + final long delay; + final TimeUnit unit; + + public SlowScheduler() { + this(Schedulers.computation(), 2, TimeUnit.SECONDS); + } + + public SlowScheduler(Scheduler actual, long delay, TimeUnit unit) { + this.actual = actual; + this.delay = delay; + this.unit = unit; + } + + @NonNull + @Override + public Worker createWorker() { + return new SlowInner(actual.createWorker()); + } + + private final class SlowInner extends Worker { + + private final Scheduler.Worker actualInner; + + private SlowInner(Worker actual) { + this.actualInner = actual; + } + + @Override + public void dispose() { + actualInner.dispose(); + } + + @Override + public boolean isDisposed() { + return actualInner.isDisposed(); + } + + @NonNull + @Override + public Disposable schedule(@NonNull final Runnable action) { + return actualInner.schedule(action, delay, unit); + } + + @NonNull + @Override + public Disposable schedule(@NonNull final Runnable action, final long delayTime, @NonNull final TimeUnit delayUnit) { + TimeUnit common = delayUnit.compareTo(unit) < 0 ? delayUnit : unit; + long t = common.convert(delayTime, delayUnit) + common.convert(delay, unit); + return actualInner.schedule(action, t, common); + } + + } + + } + + @Test + public void unsubscribeInfiniteStream() throws InterruptedException { + TestObserver<Integer> to = new TestObserver<>(); + final AtomicInteger count = new AtomicInteger(); + Observable.unsafeCreate(new ObservableSource<Integer>() { + + @Override + public void subscribe(Observer<? super Integer> sub) { + Disposable d = Disposable.empty(); + sub.onSubscribe(d); + for (int i = 1; !d.isDisposed(); i++) { + count.incrementAndGet(); + sub.onNext(i); + } + } + + }).subscribeOn(Schedulers.newThread()).take(10).subscribe(to); + + to.awaitDone(1000, TimeUnit.MILLISECONDS); + to.dispose(); + Thread.sleep(200); // give time for the loop to continue + to.assertValues(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + assertEquals(10, count.get()); + } + + @Test + public void cancelBeforeActualSubscribe() { + TestScheduler test = new TestScheduler(); + + TestObserverEx<Integer> to = new TestObserverEx<>(); + + Observable.just(1).hide() + .subscribeOn(test).subscribe(to); + + to.dispose(); + + test.advanceTimeBy(1, TimeUnit.SECONDS); + + to + .assertSubscribed() + .assertNoValues() + .assertNotTerminated(); + } + + @Test + public void dispose() { + TestHelper.checkDisposed(Observable.just(1).subscribeOn(Schedulers.single())); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableSwitchIfEmptyTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableSwitchIfEmptyTest.java new file mode 100644 index 0000000000..5dd3703c67 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableSwitchIfEmptyTest.java @@ -0,0 +1,110 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import static org.junit.Assert.*; + +import java.util.Arrays; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.*; +import io.reactivex.rxjava3.functions.Consumer; +import io.reactivex.rxjava3.observers.DefaultObserver; + +public class ObservableSwitchIfEmptyTest extends RxJavaTest { + + @Test + public void switchWhenNotEmpty() throws Exception { + final AtomicBoolean subscribed = new AtomicBoolean(false); + final Observable<Integer> o = Observable.just(4) + .switchIfEmpty(Observable.just(2) + .doOnSubscribe(new Consumer<Disposable>() { + @Override + public void accept(Disposable d) { + subscribed.set(true); + } + })); + + assertEquals(4, o.blockingSingle().intValue()); + assertFalse(subscribed.get()); + } + + @Test + public void switchWhenEmpty() throws Exception { + final Observable<Integer> o = Observable.<Integer>empty() + .switchIfEmpty(Observable.fromIterable(Arrays.asList(42))); + + assertEquals(42, o.blockingSingle().intValue()); + } + + @Test + public void switchTriggerUnsubscribe() throws Exception { + + final Disposable d = Disposable.empty(); + + Observable<Long> withProducer = Observable.unsafeCreate(new ObservableSource<Long>() { + @Override + public void subscribe(final Observer<? super Long> observer) { + observer.onSubscribe(d); + observer.onNext(42L); + } + }); + + Observable.<Long>empty() + .switchIfEmpty(withProducer) + .lift(new ObservableOperator<Long, Long>() { + @Override + public Observer<? super Long> apply(final Observer<? super Long> child) { + return new DefaultObserver<Long>() { + @Override + public void onComplete() { + + } + + @Override + public void onError(Throwable e) { + + } + + @Override + public void onNext(Long aLong) { + cancel(); + } + + }; + } + }).subscribe(); + + assertTrue(d.isDisposed()); + // FIXME no longer assertable +// assertTrue(sub.isUnsubscribed()); + } + + @Test + public void switchShouldTriggerUnsubscribe() { + final Disposable d = Disposable.empty(); + + Observable.unsafeCreate(new ObservableSource<Long>() { + @Override + public void subscribe(final Observer<? super Long> observer) { + observer.onSubscribe(d); + observer.onComplete(); + } + }).switchIfEmpty(Observable.<Long>never()).subscribe(); + assertTrue(d.isDisposed()); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableSwitchTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableSwitchTest.java new file mode 100644 index 0000000000..72e6c93bb8 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableSwitchTest.java @@ -0,0 +1,1456 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +import java.util.*; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.*; + +import org.junit.*; +import org.mockito.InOrder; + +import io.reactivex.rxjava3.annotations.NonNull; +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.core.Observer; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.internal.schedulers.ImmediateThinScheduler; +import io.reactivex.rxjava3.internal.util.ExceptionHelper; +import io.reactivex.rxjava3.observers.*; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.schedulers.*; +import io.reactivex.rxjava3.subjects.PublishSubject; +import io.reactivex.rxjava3.testsupport.*; + +public class ObservableSwitchTest extends RxJavaTest { + + private TestScheduler scheduler; + private Scheduler.Worker innerScheduler; + private Observer<String> observer; + + @Before + public void before() { + scheduler = new TestScheduler(); + innerScheduler = scheduler.createWorker(); + observer = TestHelper.mockObserver(); + } + + @Test + public void switchWhenOuterCompleteBeforeInner() { + Observable<Observable<String>> source = Observable.unsafeCreate(new ObservableSource<Observable<String>>() { + @Override + public void subscribe(Observer<? super Observable<String>> outerObserver) { + outerObserver.onSubscribe(Disposable.empty()); + publishNext(outerObserver, 50, Observable.unsafeCreate(new ObservableSource<String>() { + @Override + public void subscribe(Observer<? super String> innerObserver) { + innerObserver.onSubscribe(Disposable.empty()); + publishNext(innerObserver, 70, "one"); + publishNext(innerObserver, 100, "two"); + publishCompleted(innerObserver, 200); + } + })); + publishCompleted(outerObserver, 60); + } + }); + + Observable<String> sampled = Observable.switchOnNext(source); + sampled.subscribe(observer); + + InOrder inOrder = inOrder(observer); + + scheduler.advanceTimeTo(350, TimeUnit.MILLISECONDS); + inOrder.verify(observer, times(2)).onNext(anyString()); + inOrder.verify(observer, times(1)).onComplete(); + } + + @Test + public void switchWhenInnerCompleteBeforeOuter() { + Observable<Observable<String>> source = Observable.unsafeCreate(new ObservableSource<Observable<String>>() { + @Override + public void subscribe(Observer<? super Observable<String>> outerObserver) { + outerObserver.onSubscribe(Disposable.empty()); + publishNext(outerObserver, 10, Observable.unsafeCreate(new ObservableSource<String>() { + @Override + public void subscribe(Observer<? super String> innerObserver) { + innerObserver.onSubscribe(Disposable.empty()); + publishNext(innerObserver, 0, "one"); + publishNext(innerObserver, 10, "two"); + publishCompleted(innerObserver, 20); + } + })); + + publishNext(outerObserver, 100, Observable.unsafeCreate(new ObservableSource<String>() { + @Override + public void subscribe(Observer<? super String> innerObserver) { + innerObserver.onSubscribe(Disposable.empty()); + publishNext(innerObserver, 0, "three"); + publishNext(innerObserver, 10, "four"); + publishCompleted(innerObserver, 20); + } + })); + publishCompleted(outerObserver, 200); + } + }); + + Observable<String> sampled = Observable.switchOnNext(source); + sampled.subscribe(observer); + + InOrder inOrder = inOrder(observer); + + scheduler.advanceTimeTo(150, TimeUnit.MILLISECONDS); + inOrder.verify(observer, never()).onComplete(); + inOrder.verify(observer, times(1)).onNext("one"); + inOrder.verify(observer, times(1)).onNext("two"); + inOrder.verify(observer, times(1)).onNext("three"); + inOrder.verify(observer, times(1)).onNext("four"); + + scheduler.advanceTimeTo(250, TimeUnit.MILLISECONDS); + inOrder.verify(observer, never()).onNext(anyString()); + inOrder.verify(observer, times(1)).onComplete(); + } + + @Test + public void switchWithComplete() { + Observable<Observable<String>> source = Observable.unsafeCreate(new ObservableSource<Observable<String>>() { + @Override + public void subscribe(Observer<? super Observable<String>> outerObserver) { + outerObserver.onSubscribe(Disposable.empty()); + publishNext(outerObserver, 50, Observable.unsafeCreate(new ObservableSource<String>() { + @Override + public void subscribe(final Observer<? super String> innerObserver) { + innerObserver.onSubscribe(Disposable.empty()); + publishNext(innerObserver, 60, "one"); + publishNext(innerObserver, 100, "two"); + } + })); + + publishNext(outerObserver, 200, Observable.unsafeCreate(new ObservableSource<String>() { + @Override + public void subscribe(final Observer<? super String> innerObserver) { + innerObserver.onSubscribe(Disposable.empty()); + publishNext(innerObserver, 0, "three"); + publishNext(innerObserver, 100, "four"); + } + })); + + publishCompleted(outerObserver, 250); + } + }); + + Observable<String> sampled = Observable.switchOnNext(source); + sampled.subscribe(observer); + + InOrder inOrder = inOrder(observer); + + scheduler.advanceTimeTo(90, TimeUnit.MILLISECONDS); + inOrder.verify(observer, never()).onNext(anyString()); + verify(observer, never()).onComplete(); + verify(observer, never()).onError(any(Throwable.class)); + + scheduler.advanceTimeTo(125, TimeUnit.MILLISECONDS); + inOrder.verify(observer, times(1)).onNext("one"); + verify(observer, never()).onComplete(); + verify(observer, never()).onError(any(Throwable.class)); + + scheduler.advanceTimeTo(175, TimeUnit.MILLISECONDS); + inOrder.verify(observer, times(1)).onNext("two"); + verify(observer, never()).onComplete(); + verify(observer, never()).onError(any(Throwable.class)); + + scheduler.advanceTimeTo(225, TimeUnit.MILLISECONDS); + inOrder.verify(observer, times(1)).onNext("three"); + verify(observer, never()).onComplete(); + verify(observer, never()).onError(any(Throwable.class)); + + scheduler.advanceTimeTo(350, TimeUnit.MILLISECONDS); + inOrder.verify(observer, times(1)).onNext("four"); + verify(observer, never()).onComplete(); + verify(observer, never()).onError(any(Throwable.class)); + } + + @Test + public void switchWithError() { + Observable<Observable<String>> source = Observable.unsafeCreate(new ObservableSource<Observable<String>>() { + @Override + public void subscribe(Observer<? super Observable<String>> outerObserver) { + outerObserver.onSubscribe(Disposable.empty()); + publishNext(outerObserver, 50, Observable.unsafeCreate(new ObservableSource<String>() { + @Override + public void subscribe(final Observer<? super String> innerObserver) { + innerObserver.onSubscribe(Disposable.empty()); + publishNext(innerObserver, 50, "one"); + publishNext(innerObserver, 100, "two"); + } + })); + + publishNext(outerObserver, 200, Observable.unsafeCreate(new ObservableSource<String>() { + @Override + public void subscribe(Observer<? super String> innerObserver) { + innerObserver.onSubscribe(Disposable.empty()); + publishNext(innerObserver, 0, "three"); + publishNext(innerObserver, 100, "four"); + } + })); + + publishError(outerObserver, 250, new TestException()); + } + }); + + Observable<String> sampled = Observable.switchOnNext(source); + sampled.subscribe(observer); + + InOrder inOrder = inOrder(observer); + + scheduler.advanceTimeTo(90, TimeUnit.MILLISECONDS); + inOrder.verify(observer, never()).onNext(anyString()); + verify(observer, never()).onComplete(); + verify(observer, never()).onError(any(Throwable.class)); + + scheduler.advanceTimeTo(125, TimeUnit.MILLISECONDS); + inOrder.verify(observer, times(1)).onNext("one"); + verify(observer, never()).onComplete(); + verify(observer, never()).onError(any(Throwable.class)); + + scheduler.advanceTimeTo(175, TimeUnit.MILLISECONDS); + inOrder.verify(observer, times(1)).onNext("two"); + verify(observer, never()).onComplete(); + verify(observer, never()).onError(any(Throwable.class)); + + scheduler.advanceTimeTo(225, TimeUnit.MILLISECONDS); + inOrder.verify(observer, times(1)).onNext("three"); + verify(observer, never()).onComplete(); + verify(observer, never()).onError(any(Throwable.class)); + + scheduler.advanceTimeTo(350, TimeUnit.MILLISECONDS); + inOrder.verify(observer, never()).onNext(anyString()); + verify(observer, never()).onComplete(); + verify(observer, times(1)).onError(any(TestException.class)); + } + + @Test + public void switchWithSubsequenceComplete() { + Observable<Observable<String>> source = Observable.unsafeCreate(new ObservableSource<Observable<String>>() { + @Override + public void subscribe(Observer<? super Observable<String>> outerObserver) { + outerObserver.onSubscribe(Disposable.empty()); + publishNext(outerObserver, 50, Observable.unsafeCreate(new ObservableSource<String>() { + @Override + public void subscribe(Observer<? super String> innerObserver) { + innerObserver.onSubscribe(Disposable.empty()); + publishNext(innerObserver, 50, "one"); + publishNext(innerObserver, 100, "two"); + } + })); + + publishNext(outerObserver, 130, Observable.unsafeCreate(new ObservableSource<String>() { + @Override + public void subscribe(Observer<? super String> innerObserver) { + innerObserver.onSubscribe(Disposable.empty()); + publishCompleted(innerObserver, 0); + } + })); + + publishNext(outerObserver, 150, Observable.unsafeCreate(new ObservableSource<String>() { + @Override + public void subscribe(Observer<? super String> innerObserver) { + innerObserver.onSubscribe(Disposable.empty()); + publishNext(innerObserver, 50, "three"); + } + })); + } + }); + + Observable<String> sampled = Observable.switchOnNext(source); + sampled.subscribe(observer); + + InOrder inOrder = inOrder(observer); + + scheduler.advanceTimeTo(90, TimeUnit.MILLISECONDS); + inOrder.verify(observer, never()).onNext(anyString()); + verify(observer, never()).onComplete(); + verify(observer, never()).onError(any(Throwable.class)); + + scheduler.advanceTimeTo(125, TimeUnit.MILLISECONDS); + inOrder.verify(observer, times(1)).onNext("one"); + verify(observer, never()).onComplete(); + verify(observer, never()).onError(any(Throwable.class)); + + scheduler.advanceTimeTo(250, TimeUnit.MILLISECONDS); + inOrder.verify(observer, times(1)).onNext("three"); + verify(observer, never()).onComplete(); + verify(observer, never()).onError(any(Throwable.class)); + } + + @Test + public void switchWithSubsequenceError() { + Observable<Observable<String>> source = Observable.unsafeCreate(new ObservableSource<Observable<String>>() { + @Override + public void subscribe(Observer<? super Observable<String>> observer) { + observer.onSubscribe(Disposable.empty()); + publishNext(observer, 50, Observable.unsafeCreate(new ObservableSource<String>() { + @Override + public void subscribe(Observer<? super String> observer) { + observer.onSubscribe(Disposable.empty()); + publishNext(observer, 50, "one"); + publishNext(observer, 100, "two"); + } + })); + + publishNext(observer, 130, Observable.unsafeCreate(new ObservableSource<String>() { + @Override + public void subscribe(Observer<? super String> observer) { + observer.onSubscribe(Disposable.empty()); + publishError(observer, 0, new TestException()); + } + })); + + publishNext(observer, 150, Observable.unsafeCreate(new ObservableSource<String>() { + @Override + public void subscribe(Observer<? super String> observer) { + observer.onSubscribe(Disposable.empty()); + publishNext(observer, 50, "three"); + } + })); + + } + }); + + Observable<String> sampled = Observable.switchOnNext(source); + sampled.subscribe(observer); + + InOrder inOrder = inOrder(observer); + + scheduler.advanceTimeTo(90, TimeUnit.MILLISECONDS); + inOrder.verify(observer, never()).onNext(anyString()); + verify(observer, never()).onComplete(); + verify(observer, never()).onError(any(Throwable.class)); + + scheduler.advanceTimeTo(125, TimeUnit.MILLISECONDS); + inOrder.verify(observer, times(1)).onNext("one"); + verify(observer, never()).onComplete(); + verify(observer, never()).onError(any(Throwable.class)); + + scheduler.advanceTimeTo(250, TimeUnit.MILLISECONDS); + inOrder.verify(observer, never()).onNext("three"); + verify(observer, never()).onComplete(); + verify(observer, times(1)).onError(any(TestException.class)); + } + + private <T> void publishCompleted(final Observer<T> observer, long delay) { + innerScheduler.schedule(new Runnable() { + @Override + public void run() { + observer.onComplete(); + } + }, delay, TimeUnit.MILLISECONDS); + } + + private <T> void publishError(final Observer<T> observer, long delay, final Throwable error) { + innerScheduler.schedule(new Runnable() { + @Override + public void run() { + observer.onError(error); + } + }, delay, TimeUnit.MILLISECONDS); + } + + private <T> void publishNext(final Observer<T> observer, long delay, final T value) { + innerScheduler.schedule(new Runnable() { + @Override + public void run() { + observer.onNext(value); + } + }, delay, TimeUnit.MILLISECONDS); + } + + @Test + public void switchIssue737() { + // https://github.com/ReactiveX/RxJava/issues/737 + Observable<Observable<String>> source = Observable.unsafeCreate(new ObservableSource<Observable<String>>() { + @Override + public void subscribe(Observer<? super Observable<String>> outerObserver) { + outerObserver.onSubscribe(Disposable.empty()); + publishNext(outerObserver, 0, Observable.unsafeCreate(new ObservableSource<String>() { + @Override + public void subscribe(Observer<? super String> innerObserver) { + innerObserver.onSubscribe(Disposable.empty()); + publishNext(innerObserver, 10, "1-one"); + publishNext(innerObserver, 20, "1-two"); + // The following events will be ignored + publishNext(innerObserver, 30, "1-three"); + publishCompleted(innerObserver, 40); + } + })); + publishNext(outerObserver, 25, Observable.unsafeCreate(new ObservableSource<String>() { + @Override + public void subscribe(Observer<? super String> innerObserver) { + innerObserver.onSubscribe(Disposable.empty()); + publishNext(innerObserver, 10, "2-one"); + publishNext(innerObserver, 20, "2-two"); + publishNext(innerObserver, 30, "2-three"); + publishCompleted(innerObserver, 40); + } + })); + publishCompleted(outerObserver, 30); + } + }); + + Observable<String> sampled = Observable.switchOnNext(source); + sampled.subscribe(observer); + + scheduler.advanceTimeTo(1000, TimeUnit.MILLISECONDS); + + InOrder inOrder = inOrder(observer); + inOrder.verify(observer, times(1)).onNext("1-one"); + inOrder.verify(observer, times(1)).onNext("1-two"); + inOrder.verify(observer, times(1)).onNext("2-one"); + inOrder.verify(observer, times(1)).onNext("2-two"); + inOrder.verify(observer, times(1)).onNext("2-three"); + inOrder.verify(observer, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void unsubscribe() { + final AtomicBoolean isUnsubscribed = new AtomicBoolean(); + Observable.switchOnNext( + Observable.unsafeCreate(new ObservableSource<Observable<Integer>>() { + @Override + public void subscribe(final Observer<? super Observable<Integer>> observer) { + Disposable bs = Disposable.empty(); + observer.onSubscribe(bs); + observer.onNext(Observable.just(1)); + isUnsubscribed.set(bs.isDisposed()); + } + }) + ).take(1).subscribe(); + assertTrue("Switch doesn't propagate 'unsubscribe'", isUnsubscribed.get()); + } + /** The upstream producer hijacked the switch producer stopping the requests aimed at the inner observables. */ + @Test + public void issue2654() { + Observable<String> oneItem = Observable.just("Hello").mergeWith(Observable.<String>never()); + + Observable<String> src = oneItem.switchMap(new Function<String, Observable<String>>() { + @Override + public Observable<String> apply(final String s) { + return Observable.just(s) + .mergeWith(Observable.interval(10, TimeUnit.MILLISECONDS) + .map(new Function<Long, String>() { + @Override + public String apply(Long i) { + return s + " " + i; + } + })).take(250); + } + }) + .share() + ; + + TestObserverEx<String> to = new TestObserverEx<String>() { + @Override + public void onNext(String t) { + super.onNext(t); + if (values().size() == 250) { + onComplete(); + dispose(); + } + } + }; + src.subscribe(to); + + to.awaitDone(10, TimeUnit.SECONDS); + + System.out.println("> testIssue2654: " + to.values().size()); + + to.assertTerminated(); + to.assertNoErrors(); + + Assert.assertEquals(250, to.values().size()); + } + + @Test + public void delayErrors() { + PublishSubject<ObservableSource<Integer>> source = PublishSubject.create(); + + TestObserverEx<Integer> to = source.switchMapDelayError(Functions.<ObservableSource<Integer>>identity()) + .to(TestHelper.<Integer>testConsumer()); + + to.assertNoValues() + .assertNoErrors() + .assertNotComplete(); + + source.onNext(Observable.just(1)); + + source.onNext(Observable.<Integer>error(new TestException("Forced failure 1"))); + + source.onNext(Observable.just(2, 3, 4)); + + source.onNext(Observable.<Integer>error(new TestException("Forced failure 2"))); + + source.onNext(Observable.just(5)); + + source.onError(new TestException("Forced failure 3")); + + to.assertValues(1, 2, 3, 4, 5) + .assertNotComplete() + .assertError(CompositeException.class); + + List<Throwable> errors = ExceptionHelper.flatten(to.errors().get(0)); + + TestHelper.assertError(errors, 0, TestException.class, "Forced failure 1"); + TestHelper.assertError(errors, 1, TestException.class, "Forced failure 2"); + TestHelper.assertError(errors, 2, TestException.class, "Forced failure 3"); + } + + @Test + public void switchOnNextDelayError() { + PublishSubject<Observable<Integer>> ps = PublishSubject.create(); + + TestObserver<Integer> to = Observable.switchOnNextDelayError(ps).test(); + + ps.onNext(Observable.just(1)); + ps.onNext(Observable.range(2, 4)); + ps.onComplete(); + + to.assertResult(1, 2, 3, 4, 5); + } + + @Test + public void switchOnNextDelayErrorWithError() { + PublishSubject<Observable<Integer>> ps = PublishSubject.create(); + + TestObserver<Integer> to = Observable.switchOnNextDelayError(ps).test(); + + ps.onNext(Observable.just(1)); + ps.onNext(Observable.<Integer>error(new TestException())); + ps.onNext(Observable.range(2, 4)); + ps.onComplete(); + + to.assertFailure(TestException.class, 1, 2, 3, 4, 5); + } + + @Test + public void switchOnNextDelayErrorBufferSize() { + PublishSubject<Observable<Integer>> ps = PublishSubject.create(); + + TestObserver<Integer> to = Observable.switchOnNextDelayError(ps, 2).test(); + + ps.onNext(Observable.just(1)); + ps.onNext(Observable.range(2, 4)); + ps.onComplete(); + + to.assertResult(1, 2, 3, 4, 5); + } + + @Test + public void switchMapDelayErrorEmptySource() { + assertSame(Observable.empty(), Observable.<Object>empty() + .switchMapDelayError(new Function<Object, ObservableSource<Integer>>() { + @Override + public ObservableSource<Integer> apply(Object v) throws Exception { + return Observable.just(1); + } + }, 16)); + } + + @Test + public void switchMapDelayErrorJustSource() { + Observable.just(0) + .switchMapDelayError(new Function<Object, ObservableSource<Integer>>() { + @Override + public ObservableSource<Integer> apply(Object v) throws Exception { + return Observable.just(1); + } + }, 16) + .test() + .assertResult(1); + } + + @Test + public void switchMapErrorEmptySource() { + assertSame(Observable.empty(), Observable.<Object>empty() + .switchMap(new Function<Object, ObservableSource<Integer>>() { + @Override + public ObservableSource<Integer> apply(Object v) throws Exception { + return Observable.just(1); + } + }, 16)); + } + + @Test + public void switchMapJustSource() { + Observable.just(0) + .switchMap(new Function<Object, ObservableSource<Integer>>() { + @Override + public ObservableSource<Integer> apply(Object v) throws Exception { + return Observable.just(1); + } + }, 16) + .test() + .assertResult(1); + + } + + @Test + public void switchMapInnerCancelled() { + PublishSubject<Integer> ps = PublishSubject.create(); + + TestObserver<Integer> to = Observable.just(1) + .switchMap(Functions.justFunction(ps)) + .test(); + + assertTrue(ps.hasObservers()); + + to.dispose(); + + assertFalse(ps.hasObservers()); + } + + @Test + public void switchMapSingleJustSource() { + Observable.just(0) + .switchMapSingle(new Function<Object, SingleSource<Integer>>() { + @Override + public SingleSource<Integer> apply(Object v) throws Exception { + return Single.just(1); + } + }) + .test() + .assertResult(1); + } + + @Test + public void switchMapSingleMapperReturnsNull() { + Observable.just(0) + .switchMapSingle(new Function<Object, SingleSource<Integer>>() { + @Override + public SingleSource<Integer> apply(Object v) throws Exception { + return null; + } + }) + .test() + .assertError(NullPointerException.class); + } + + @Test + public void switchMapSingleFunctionDoesntReturnSingle() { + Observable.just(0) + .switchMapSingle(new Function<Object, SingleSource<Integer>>() { + @Override + public SingleSource<Integer> apply(Object v) throws Exception { + return new SingleSource<Integer>() { + @Override + public void subscribe(SingleObserver<? super Integer> observer) { + observer.onSubscribe(Disposable.empty()); + observer.onSuccess(1); + } + }; + } + }) + .test() + .assertResult(1); + } + + @Test + public void switchMapSingleDelayErrorJustSource() { + final AtomicBoolean completed = new AtomicBoolean(); + Observable.just(0, 1) + .switchMapSingleDelayError(new Function<Integer, SingleSource<Integer>>() { + @Override + public SingleSource<Integer> apply(Integer v) throws Exception { + if (v == 0) { + return Single.error(new RuntimeException()); + } else { + return Single.just(1).doOnSuccess(new Consumer<Integer>() { + + @Override + public void accept(Integer n) throws Exception { + completed.set(true); + }}); + } + } + }) + .test() + .assertValue(1) + .assertError(RuntimeException.class); + assertTrue(completed.get()); + } + + @Test + public void scalarMap() { + Observable.switchOnNext(Observable.just(Observable.just(1))) + .test() + .assertResult(1); + } + + @Test + public void scalarMapDelayError() { + Observable.switchOnNextDelayError(Observable.just(Observable.just(1))) + .test() + .assertResult(1); + } + + @Test + public void dispose() { + TestHelper.checkDisposed(Observable.switchOnNext( + Observable.just(Observable.just(1)).hide())); + } + + @Test + public void nextSourceErrorRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + + final PublishSubject<Integer> ps1 = PublishSubject.create(); + final PublishSubject<Integer> ps2 = PublishSubject.create(); + + ps1.switchMap(new Function<Integer, ObservableSource<Integer>>() { + @Override + public ObservableSource<Integer> apply(Integer v) throws Exception { + if (v == 1) { + return ps2; + } + return Observable.never(); + } + }) + .test(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + ps1.onNext(2); + } + }; + + final TestException ex = new TestException(); + + Runnable r2 = new Runnable() { + @Override + public void run() { + ps2.onError(ex); + } + }; + + TestHelper.race(r1, r2); + + for (Throwable e : errors) { + assertTrue(e.toString(), e instanceof TestException); + } + } finally { + RxJavaPlugins.reset(); + } + } + } + + @Test + public void outerInnerErrorRace() { + for (int i = 0; i < TestHelper.RACE_LONG_LOOPS; i++) { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + + final PublishSubject<Integer> ps1 = PublishSubject.create(); + final PublishSubject<Integer> ps2 = PublishSubject.create(); + + ps1.switchMap(new Function<Integer, ObservableSource<Integer>>() { + @Override + public ObservableSource<Integer> apply(Integer v) throws Exception { + if (v == 1) { + return ps2; + } + return Observable.never(); + } + }) + .test(); + + ps1.onNext(1); + + final TestException ex1 = new TestException(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + ps1.onError(ex1); + } + }; + + final TestException ex2 = new TestException(); + + Runnable r2 = new Runnable() { + @Override + public void run() { + ps2.onError(ex2); + } + }; + + TestHelper.race(r1, r2); + + for (Throwable e : errors) { + assertTrue(e.getCause().toString(), e.getCause() instanceof TestException); + } + } finally { + RxJavaPlugins.reset(); + } + } + } + + @Test + public void nextCancelRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final PublishSubject<Integer> ps1 = PublishSubject.create(); + + final TestObserver<Integer> to = ps1.switchMap(new Function<Integer, ObservableSource<Integer>>() { + @Override + public ObservableSource<Integer> apply(Integer v) throws Exception { + return Observable.never(); + } + }) + .test(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + ps1.onNext(2); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + to.dispose(); + } + }; + + TestHelper.race(r1, r2); + } + } + + @Test + public void mapperThrows() { + Observable.just(1).hide() + .switchMap(new Function<Integer, ObservableSource<Object>>() { + @Override + public ObservableSource<Object> apply(Integer v) throws Exception { + throw new TestException(); + } + }) + .test() + .assertFailure(TestException.class); + } + + @Test + public void badMainSource() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + new Observable<Integer>() { + @Override + protected void subscribeActual(Observer<? super Integer> observer) { + observer.onSubscribe(Disposable.empty()); + observer.onComplete(); + observer.onError(new TestException()); + observer.onComplete(); + } + } + .switchMap(Functions.justFunction(Observable.never())) + .test() + .assertResult(); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void emptyInner() { + Observable.range(1, 5) + .switchMap(Functions.justFunction(Observable.empty())) + .test() + .assertResult(); + } + + @Test + public void justInner() { + Observable.range(1, 5) + .switchMap(Functions.justFunction(Observable.just(1))) + .test() + .assertResult(1, 1, 1, 1, 1); + } + + @Test + public void badInnerSource() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + Observable.just(1).hide() + .switchMap(Functions.justFunction(new Observable<Integer>() { + @Override + protected void subscribeActual(Observer<? super Integer> observer) { + observer.onSubscribe(Disposable.empty()); + observer.onError(new TestException()); + observer.onComplete(); + observer.onError(new TestException()); + observer.onComplete(); + } + })) + .test() + .assertFailure(TestException.class); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void innerCompletesReentrant() { + final PublishSubject<Integer> ps = PublishSubject.create(); + + TestObserver<Integer> to = new TestObserver<Integer>() { + @Override + public void onNext(Integer t) { + super.onNext(t); + ps.onComplete(); + } + }; + + Observable.just(1).hide() + .switchMap(Functions.justFunction(ps)) + .subscribe(to); + + ps.onNext(1); + + to.assertResult(1); + } + + @Test + public void innerErrorsReentrant() { + final PublishSubject<Integer> ps = PublishSubject.create(); + + TestObserver<Integer> to = new TestObserver<Integer>() { + @Override + public void onNext(Integer t) { + super.onNext(t); + ps.onError(new TestException()); + } + }; + + Observable.just(1).hide() + .switchMap(Functions.justFunction(ps)) + .subscribe(to); + + ps.onNext(1); + + to.assertFailure(TestException.class, 1); + } + + @Test + public void innerDisposedOnMainError() { + final PublishSubject<Integer> main = PublishSubject.create(); + final PublishSubject<Integer> inner = PublishSubject.create(); + + TestObserver<Integer> to = main.switchMap(Functions.justFunction(inner)) + .test(); + + assertTrue(main.hasObservers()); + + main.onNext(1); + + assertTrue(inner.hasObservers()); + + main.onError(new TestException()); + + assertFalse(inner.hasObservers()); + + to.assertFailure(TestException.class); + } + + @Test + public void outerInnerErrorRaceIgnoreDispose() { + for (int i = 0; i < TestHelper.RACE_LONG_LOOPS; i++) { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + + final AtomicReference<Observer<? super Integer>> obs1 = new AtomicReference<>(); + final Observable<Integer> ps1 = new Observable<Integer>() { + @Override + protected void subscribeActual( + Observer<? super Integer> observer) { + obs1.set(observer); + } + }; + final AtomicReference<Observer<? super Integer>> obs2 = new AtomicReference<>(); + final Observable<Integer> ps2 = new Observable<Integer>() { + @Override + protected void subscribeActual( + Observer<? super Integer> observer) { + obs2.set(observer); + } + }; + + ps1.switchMap(new Function<Integer, ObservableSource<Integer>>() { + @Override + public ObservableSource<Integer> apply(Integer v) throws Exception { + if (v == 1) { + return ps2; + } + return Observable.never(); + } + }) + .test(); + + obs1.get().onSubscribe(Disposable.empty()); + obs1.get().onNext(1); + + obs2.get().onSubscribe(Disposable.empty()); + + final TestException ex1 = new TestException(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + obs1.get().onError(ex1); + } + }; + + final TestException ex2 = new TestException(); + + Runnable r2 = new Runnable() { + @Override + public void run() { + obs2.get().onError(ex2); + } + }; + + TestHelper.race(r1, r2); + + for (Throwable e : errors) { + assertTrue(e.toString(), e.getCause() instanceof TestException); + } + } finally { + RxJavaPlugins.reset(); + } + } + } + + @Test + public void asyncFused() { + Observable.just(1).hide() + .switchMap(Functions.justFunction( + Observable.range(1, 5) + .observeOn(ImmediateThinScheduler.INSTANCE) + )) + .test() + .assertResult(1, 2, 3, 4, 5); + } + + @Test + public void syncFusedMaybe() { + Observable.range(1, 5).hide() + .switchMap(Functions.justFunction( + Maybe.just(1).toObservable() + )) + .test() + .assertResult(1, 1, 1, 1, 1); + } + + @Test + public void syncFusedSingle() { + Observable.range(1, 5).hide() + .switchMap(Functions.justFunction( + Single.just(1).toObservable() + )) + .test() + .assertResult(1, 1, 1, 1, 1); + } + + @Test + public void syncFusedCompletable() { + Observable.range(1, 5).hide() + .switchMap(Functions.justFunction( + Completable.complete().toObservable() + )) + .test() + .assertResult(); + } + + @Test + public void asyncFusedRejecting() { + Observable.just(1).hide() + .switchMap(Functions.justFunction( + TestHelper.rejectObservableFusion() + )) + .test() + .assertEmpty(); + } + + @Test + public void asyncFusedPollCrash() { + PublishSubject<Integer> ps = PublishSubject.create(); + + TestObserver<Integer> to = ps + .switchMap(Functions.justFunction( + Observable.range(1, 5) + .observeOn(ImmediateThinScheduler.INSTANCE) + .map(new Function<Integer, Integer>() { + @Override + public Integer apply(Integer v) throws Exception { + throw new TestException(); + } + }) + .compose(TestHelper.<Integer>observableStripBoundary()) + )) + .test(); + + to.assertEmpty(); + + ps.onNext(1); + + to + .assertFailure(TestException.class); + + assertFalse(ps.hasObservers()); + } + + @Test + public void asyncFusedPollCrashDelayError() { + PublishSubject<Integer> ps = PublishSubject.create(); + + TestObserver<Integer> to = ps + .switchMapDelayError(Functions.justFunction( + Observable.range(1, 5) + .observeOn(ImmediateThinScheduler.INSTANCE) + .map(new Function<Integer, Integer>() { + @Override + public Integer apply(Integer v) throws Exception { + throw new TestException(); + } + }) + .compose(TestHelper.<Integer>observableStripBoundary()) + )) + .test(); + + to.assertEmpty(); + + ps.onNext(1); + + assertTrue(ps.hasObservers()); + + to.assertEmpty(); + + ps.onComplete(); + + to + .assertFailure(TestException.class); + + assertFalse(ps.hasObservers()); + } + + @Test + public void fusedBoundary() { + String thread = Thread.currentThread().getName(); + + TestObserver<Object> to = Observable.range(1, 10000) + .switchMap(new Function<Integer, ObservableSource<? extends Object>>() { + @Override + public ObservableSource<? extends Object> apply(Integer v) + throws Exception { + return Observable.just(2).hide() + .observeOn(Schedulers.single()) + .map(new Function<Integer, Object>() { + @Override + public Object apply(Integer w) throws Exception { + return Thread.currentThread().getName(); + } + }); + } + }) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertNoErrors() + .assertComplete(); + + for (Object o : to.values()) { + assertNotEquals(thread, o); + } + } + + @Test + public void undeliverableUponCancel() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + final TestObserverEx<Integer> to = new TestObserverEx<>(); + + Observable.just(1) + .map(new Function<Integer, Integer>() { + @Override + public Integer apply(Integer v) throws Throwable { + to.dispose(); + throw new TestException(); + } + }) + .switchMap(new Function<Integer, Observable<Integer>>() { + @Override + public Observable<Integer> apply(Integer v) throws Throwable { + return Observable.just(v).hide(); + } + }) + .subscribe(to); + + to.assertEmpty(); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void switchMapFusedIterable() { + Observable.range(1, 2) + .switchMap(new Function<Integer, Observable<Integer>>() { + @Override + public Observable<Integer> apply(Integer v) + throws Throwable { + return Observable.fromIterable(Arrays.asList(v * 10)); + } + }) + .test() + .assertResult(10, 20); + } + + @Test + public void switchMapHiddenIterable() { + Observable.range(1, 2) + .switchMap(new Function<Integer, Observable<Integer>>() { + @Override + public Observable<Integer> apply(Integer v) + throws Throwable { + return Observable.fromIterable(Arrays.asList(v * 10)).hide(); + } + }) + .test() + .assertResult(10, 20); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeObservable(f -> f.switchMap(v -> Observable.never())); + } + + @Test + public void mainCompleteCancelRace() { + for (int i = 0; i < TestHelper.RACE_LONG_LOOPS; i++) { + AtomicReference<Observer<? super Integer>> ref = new AtomicReference<>(); + Observable<Integer> o = new Observable<Integer>() { + @Override + protected void subscribeActual(@NonNull Observer<? super @NonNull Integer> observer) { + ref.set(observer); + } + }; + + TestObserver<Object> to = o.switchMap(v -> Observable.never()) + .test(); + + ref.get().onSubscribe(Disposable.empty()); + + TestHelper.race( + () -> ref.get().onComplete(), + () -> to.dispose() + ); + } + } + + @Test + public void mainCompleteInnerErrorRace() { + TestException ex = new TestException(); + + for (int i = 0; i < TestHelper.RACE_LONG_LOOPS; i++) { + AtomicReference<Observer<? super Integer>> ref1 = new AtomicReference<>(); + Observable<Integer> o1 = new Observable<Integer>() { + @Override + protected void subscribeActual(@NonNull Observer<? super @NonNull Integer> observer) { + ref1.set(observer); + } + }; + AtomicReference<Observer<? super Integer>> ref2 = new AtomicReference<>(); + Observable<Integer> o2 = new Observable<Integer>() { + @Override + protected void subscribeActual(@NonNull Observer<? super @NonNull Integer> observer) { + ref2.set(observer); + } + }; + + o1.switchMap(v -> o2) + .test(); + + ref1.get().onSubscribe(Disposable.empty()); + ref1.get().onNext(1); + ref2.get().onSubscribe(Disposable.empty()); + + TestHelper.race( + () -> ref1.get().onComplete(), + () -> ref2.get().onError(ex) + ); + } + } + + @Test + public void innerNoSubscriptionYet() { + AtomicReference<Observer<? super Integer>> ref1 = new AtomicReference<>(); + Observable<Integer> o1 = new Observable<Integer>() { + @Override + protected void subscribeActual(@NonNull Observer<? super @NonNull Integer> observer) { + ref1.set(observer); + } + }; + AtomicReference<Observer<? super Integer>> ref2 = new AtomicReference<>(); + Observable<Integer> o2 = new Observable<Integer>() { + @Override + protected void subscribeActual(@NonNull Observer<? super @NonNull Integer> observer) { + ref2.set(observer); + } + }; + + o1.switchMap(v -> o2) + .test(); + + ref1.get().onSubscribe(Disposable.empty()); + ref1.get().onNext(1); + ref1.get().onComplete(); + } + + @Test + public void switchDuringOnNext() { + PublishSubject<Integer> ps = PublishSubject.create(); + + TestObserver<Integer> to = ps.switchMap(v -> Observable.range(v, 5)) + .doOnNext(v -> { + if (v == 1) { + ps.onNext(2); + } + }) + .test(); + + ps.onNext(1); + + to + .assertValuesOnly(1, 2, 3, 4, 5, 6); + } + + @Test + public void mainCompleteWhileInnerActive() { + PublishSubject<Integer> ps1 = PublishSubject.create(); + PublishSubject<Integer> ps2 = PublishSubject.create(); + + TestObserver<Integer> to = ps1.switchMapDelayError(v -> ps2) + .test(); + + ps1.onNext(1); + ps1.onComplete(); + + ps2.onComplete(); + + to.assertResult(); + } + + @Test + public void innerIgnoresCancelAndErrors() throws Throwable { + TestHelper.withErrorTracking(errors -> { + PublishSubject<Integer> ps = PublishSubject.create(); + + TestObserver<Object> to = ps + .switchMap(v -> { + if (v == 1) { + return Observable.unsafeCreate(s -> { + s.onSubscribe(Disposable.empty()); + ps.onNext(2); + s.onError(new TestException()); + }); + } + return Observable.never(); + }) + .test(); + + ps.onNext(1); + + to.assertEmpty(); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + }); + } + + @Test + public void cancellationShouldTriggerInnerCancellationRace() throws Throwable { + AtomicInteger outer = new AtomicInteger(); + AtomicInteger inner = new AtomicInteger(); + + int n = 10_000; + for (int i = 0; i < n; i++) { + Observable.<Integer>create(it -> { + it.onNext(0); + }) + .switchMap(v -> createObservable(inner)) + .observeOn(Schedulers.computation()) + .doFinally(() -> { + outer.incrementAndGet(); + }) + .take(1) + .blockingSubscribe(v -> { }, Throwable::printStackTrace); + } + + Thread.sleep(100); + assertEquals(inner.get(), outer.get()); + assertEquals(n, inner.get()); + } + + Observable<Integer> createObservable(AtomicInteger inner) { + return Observable.<Integer>unsafeCreate(s -> { + SerializedObserver<Integer> it = new SerializedObserver<>(s); + it.onSubscribe(Disposable.empty()); + Schedulers.io().scheduleDirect(() -> { + it.onNext(1); + }, 0, TimeUnit.MILLISECONDS); + Schedulers.io().scheduleDirect(() -> { + it.onNext(2); + }, 0, TimeUnit.MILLISECONDS); + }) + .doFinally(() -> { + inner.incrementAndGet(); + }); + } + + @Test + public void innerOnSubscribeOuterCancelRace() { + TestObserver<Integer> to = new TestObserver<Integer>(); + + Observable.just(1) + .hide() + .switchMap(v -> Observable.just(1) + .doOnSubscribe(d -> to.dispose()) + .scan(1, (a, b) -> a) + ) + .subscribe(to); + + to.assertEmpty(); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableTakeLastOneTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableTakeLastOneTest.java new file mode 100644 index 0000000000..1bd19d6bc9 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableTakeLastOneTest.java @@ -0,0 +1,112 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import static org.junit.Assert.*; + +import java.util.concurrent.atomic.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.testsupport.*; + +public class ObservableTakeLastOneTest extends RxJavaTest { + + @Test + public void lastOfManyReturnsLast() { + TestObserverEx<Integer> to = new TestObserverEx<>(); + Observable.range(1, 10).takeLast(1).subscribe(to); + to.assertValue(10); + to.assertNoErrors(); + to.assertTerminated(); + } + + @Test + public void lastOfEmptyReturnsEmpty() { + TestObserverEx<Object> to = new TestObserverEx<>(); + Observable.empty().takeLast(1).subscribe(to); + to.assertNoValues(); + to.assertNoErrors(); + to.assertTerminated(); + } + + @Test + public void lastOfOneReturnsLast() { + TestObserverEx<Integer> to = new TestObserverEx<>(); + Observable.just(1).takeLast(1).subscribe(to); + to.assertValue(1); + to.assertNoErrors(); + to.assertTerminated(); + } + + @Test + public void unsubscribesFromUpstream() { + final AtomicBoolean unsubscribed = new AtomicBoolean(false); + Action unsubscribeAction = new Action() { + @Override + public void run() { + unsubscribed.set(true); + } + }; + Observable.just(1) + .concatWith(Observable.<Integer>never()) + .doOnDispose(unsubscribeAction) + .takeLast(1) + .subscribe() + .dispose(); + + assertTrue(unsubscribed.get()); + } + + @Test + public void takeLastZeroProcessesAllItemsButIgnoresThem() { + final AtomicInteger upstreamCount = new AtomicInteger(); + final int num = 10; + long count = Observable.range(1, num).doOnNext(new Consumer<Integer>() { + + @Override + public void accept(Integer t) { + upstreamCount.incrementAndGet(); + }}) + .takeLast(0).count().blockingGet(); + assertEquals(num, upstreamCount.get()); + assertEquals(0L, count); + } + + @Test + public void dispose() { + TestHelper.checkDisposed(Observable.just(1).takeLast(1)); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeObservable(new Function<Observable<Object>, ObservableSource<Object>>() { + @Override + public ObservableSource<Object> apply(Observable<Object> f) throws Exception { + return f.takeLast(1); + } + }); + } + + @Test + public void error() { + Observable.error(new TestException()) + .takeLast(1) + .test() + .assertFailure(TestException.class); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableTakeLastTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableTakeLastTest.java new file mode 100644 index 0000000000..f0a342e2e1 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableTakeLastTest.java @@ -0,0 +1,203 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.Test; +import org.mockito.InOrder; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.observers.*; +import io.reactivex.rxjava3.schedulers.Schedulers; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class ObservableTakeLastTest extends RxJavaTest { + + @Test + public void takeLastEmpty() { + Observable<String> w = Observable.empty(); + Observable<String> take = w.takeLast(2); + + Observer<String> observer = TestHelper.mockObserver(); + take.subscribe(observer); + verify(observer, never()).onNext(any(String.class)); + verify(observer, never()).onError(any(Throwable.class)); + verify(observer, times(1)).onComplete(); + } + + @Test + public void takeLast1() { + Observable<String> w = Observable.just("one", "two", "three"); + Observable<String> take = w.takeLast(2); + + Observer<String> observer = TestHelper.mockObserver(); + InOrder inOrder = inOrder(observer); + take.subscribe(observer); + inOrder.verify(observer, times(1)).onNext("two"); + inOrder.verify(observer, times(1)).onNext("three"); + verify(observer, never()).onNext("one"); + verify(observer, never()).onError(any(Throwable.class)); + verify(observer, times(1)).onComplete(); + } + + @Test + public void takeLast2() { + Observable<String> w = Observable.just("one"); + Observable<String> take = w.takeLast(10); + + Observer<String> observer = TestHelper.mockObserver(); + take.subscribe(observer); + verify(observer, times(1)).onNext("one"); + verify(observer, never()).onError(any(Throwable.class)); + verify(observer, times(1)).onComplete(); + } + + @Test + public void takeLastWithZeroCount() { + Observable<String> w = Observable.just("one"); + Observable<String> take = w.takeLast(0); + + Observer<String> observer = TestHelper.mockObserver(); + take.subscribe(observer); + verify(observer, never()).onNext("one"); + verify(observer, never()).onError(any(Throwable.class)); + verify(observer, times(1)).onComplete(); + } + + @Test(expected = IllegalArgumentException.class) + public void takeLastWithNegativeCount() { + Observable.just("one").takeLast(-1); + } + + @Test + public void backpressure1() { + TestObserver<Integer> to = new TestObserver<>(); + Observable.range(1, 100000).takeLast(1) + .observeOn(Schedulers.newThread()) + .map(newSlowProcessor()).subscribe(to); + to.awaitDone(5, TimeUnit.SECONDS); + to.assertNoErrors(); + to.assertValue(100000); + } + + @Test + public void backpressure2() { + TestObserver<Integer> to = new TestObserver<>(); + Observable.range(1, 100000).takeLast(Flowable.bufferSize() * 4) + .observeOn(Schedulers.newThread()).map(newSlowProcessor()).subscribe(to); + to.awaitDone(5, TimeUnit.SECONDS); + to.assertNoErrors(); + assertEquals(Flowable.bufferSize() * 4, to.values().size()); + } + + private Function<Integer, Integer> newSlowProcessor() { + return new Function<Integer, Integer>() { + int c; + + @Override + public Integer apply(Integer i) { + if (c++ < 100) { + try { + Thread.sleep(1); + } catch (InterruptedException e) { + } + } + return i; + } + + }; + } + + @Test + public void issue1522() { + // https://github.com/ReactiveX/RxJava/issues/1522 + assertNull(Observable + .empty() + .count() + .filter(new Predicate<Long>() { + @Override + public boolean test(Long v) { + return false; + } + }) + .blockingGet()); + } + + @Test + public void unsubscribeTakesEffectEarlyOnFastPath() { + final AtomicInteger count = new AtomicInteger(); + Observable.range(0, 100000).takeLast(100000).subscribe(new DefaultObserver<Integer>() { + + @Override + public void onStart() { + } + + @Override + public void onComplete() { + + } + + @Override + public void onError(Throwable e) { + } + + @Override + public void onNext(Integer integer) { + count.incrementAndGet(); + cancel(); + } + }); + assertEquals(1, count.get()); + } + + @Test + public void dispose() { + TestHelper.checkDisposed(Observable.range(1, 10).takeLast(5)); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeObservable(new Function<Observable<Object>, ObservableSource<Object>>() { + @Override + public ObservableSource<Object> apply(Observable<Object> o) throws Exception { + return o.takeLast(5); + } + }); + } + + @Test + public void error() { + Observable.error(new TestException()) + .takeLast(5) + .test() + .assertFailure(TestException.class); + } + + @Test + public void takeLastTake() { + Observable.range(1, 10) + .takeLast(5) + .take(2) + .test() + .assertResult(6, 7); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableTakeLastTimedTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableTakeLastTimedTest.java new file mode 100644 index 0000000000..4e2bd6a5c0 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableTakeLastTimedTest.java @@ -0,0 +1,308 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import java.util.concurrent.TimeUnit; + +import org.junit.Test; +import org.mockito.InOrder; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.schedulers.*; +import io.reactivex.rxjava3.subjects.PublishSubject; +import io.reactivex.rxjava3.testsupport.*; + +public class ObservableTakeLastTimedTest extends RxJavaTest { + + @Test(expected = IllegalArgumentException.class) + public void takeLastTimedWithNegativeCount() { + Observable.just("one").takeLast(-1, 1, TimeUnit.SECONDS); + } + + @Test + public void takeLastTimed() { + TestScheduler scheduler = new TestScheduler(); + + PublishSubject<Object> source = PublishSubject.create(); + + // FIXME time unit now matters! + Observable<Object> result = source.takeLast(1000, TimeUnit.MILLISECONDS, scheduler); + + Observer<Object> o = TestHelper.mockObserver(); + + InOrder inOrder = inOrder(o); + + result.subscribe(o); + + source.onNext(1); // T: 0ms + scheduler.advanceTimeBy(250, TimeUnit.MILLISECONDS); + source.onNext(2); // T: 250ms + scheduler.advanceTimeBy(250, TimeUnit.MILLISECONDS); + source.onNext(3); // T: 500ms + scheduler.advanceTimeBy(250, TimeUnit.MILLISECONDS); + source.onNext(4); // T: 750ms + scheduler.advanceTimeBy(250, TimeUnit.MILLISECONDS); + source.onNext(5); // T: 1000ms + scheduler.advanceTimeBy(250, TimeUnit.MILLISECONDS); + source.onComplete(); // T: 1250ms + + inOrder.verify(o, times(1)).onNext(2); + inOrder.verify(o, times(1)).onNext(3); + inOrder.verify(o, times(1)).onNext(4); + inOrder.verify(o, times(1)).onNext(5); + inOrder.verify(o, times(1)).onComplete(); + + verify(o, never()).onError(any(Throwable.class)); + } + + @Test + public void takeLastTimedDelayCompletion() { + TestScheduler scheduler = new TestScheduler(); + + PublishSubject<Object> source = PublishSubject.create(); + + // FIXME time unit now matters + Observable<Object> result = source.takeLast(1000, TimeUnit.MILLISECONDS, scheduler); + + Observer<Object> o = TestHelper.mockObserver(); + + InOrder inOrder = inOrder(o); + + result.subscribe(o); + + source.onNext(1); // T: 0ms + scheduler.advanceTimeBy(250, TimeUnit.MILLISECONDS); + source.onNext(2); // T: 250ms + scheduler.advanceTimeBy(250, TimeUnit.MILLISECONDS); + source.onNext(3); // T: 500ms + scheduler.advanceTimeBy(250, TimeUnit.MILLISECONDS); + source.onNext(4); // T: 750ms + scheduler.advanceTimeBy(250, TimeUnit.MILLISECONDS); + source.onNext(5); // T: 1000ms + scheduler.advanceTimeBy(1250, TimeUnit.MILLISECONDS); + source.onComplete(); // T: 2250ms + + inOrder.verify(o, times(1)).onComplete(); + + verify(o, never()).onNext(any()); + verify(o, never()).onError(any(Throwable.class)); + } + + @Test + public void takeLastTimedWithCapacity() { + TestScheduler scheduler = new TestScheduler(); + + PublishSubject<Object> source = PublishSubject.create(); + + // FIXME time unit now matters! + Observable<Object> result = source.takeLast(2, 1000, TimeUnit.MILLISECONDS, scheduler); + + Observer<Object> o = TestHelper.mockObserver(); + + InOrder inOrder = inOrder(o); + + result.subscribe(o); + + source.onNext(1); // T: 0ms + scheduler.advanceTimeBy(250, TimeUnit.MILLISECONDS); + source.onNext(2); // T: 250ms + scheduler.advanceTimeBy(250, TimeUnit.MILLISECONDS); + source.onNext(3); // T: 500ms + scheduler.advanceTimeBy(250, TimeUnit.MILLISECONDS); + source.onNext(4); // T: 750ms + scheduler.advanceTimeBy(250, TimeUnit.MILLISECONDS); + source.onNext(5); // T: 1000ms + scheduler.advanceTimeBy(250, TimeUnit.MILLISECONDS); + source.onComplete(); // T: 1250ms + + inOrder.verify(o, times(1)).onNext(4); + inOrder.verify(o, times(1)).onNext(5); + inOrder.verify(o, times(1)).onComplete(); + + verify(o, never()).onError(any(Throwable.class)); + } + + @Test + public void takeLastTimedThrowingSource() { + TestScheduler scheduler = new TestScheduler(); + + PublishSubject<Object> source = PublishSubject.create(); + + Observable<Object> result = source.takeLast(1, TimeUnit.SECONDS, scheduler); + + Observer<Object> o = TestHelper.mockObserver(); + + InOrder inOrder = inOrder(o); + + result.subscribe(o); + + source.onNext(1); // T: 0ms + scheduler.advanceTimeBy(250, TimeUnit.MILLISECONDS); + source.onNext(2); // T: 250ms + scheduler.advanceTimeBy(250, TimeUnit.MILLISECONDS); + source.onNext(3); // T: 500ms + scheduler.advanceTimeBy(250, TimeUnit.MILLISECONDS); + source.onNext(4); // T: 750ms + scheduler.advanceTimeBy(250, TimeUnit.MILLISECONDS); + source.onNext(5); // T: 1000ms + scheduler.advanceTimeBy(250, TimeUnit.MILLISECONDS); + source.onError(new TestException()); // T: 1250ms + + inOrder.verify(o, times(1)).onError(any(TestException.class)); + + verify(o, never()).onNext(any()); + verify(o, never()).onComplete(); + } + + @Test + public void takeLastTimedWithZeroCapacity() { + TestScheduler scheduler = new TestScheduler(); + + PublishSubject<Object> source = PublishSubject.create(); + + Observable<Object> result = source.takeLast(0, 1, TimeUnit.SECONDS, scheduler); + + Observer<Object> o = TestHelper.mockObserver(); + + InOrder inOrder = inOrder(o); + + result.subscribe(o); + + source.onNext(1); // T: 0ms + scheduler.advanceTimeBy(250, TimeUnit.MILLISECONDS); + source.onNext(2); // T: 250ms + scheduler.advanceTimeBy(250, TimeUnit.MILLISECONDS); + source.onNext(3); // T: 500ms + scheduler.advanceTimeBy(250, TimeUnit.MILLISECONDS); + source.onNext(4); // T: 750ms + scheduler.advanceTimeBy(250, TimeUnit.MILLISECONDS); + source.onNext(5); // T: 1000ms + scheduler.advanceTimeBy(250, TimeUnit.MILLISECONDS); + source.onComplete(); // T: 1250ms + + inOrder.verify(o, times(1)).onComplete(); + + verify(o, never()).onNext(any()); + verify(o, never()).onError(any(Throwable.class)); + } + + @Test + public void takeLastTimeAndSize() { + Observable.just(1, 2) + .takeLast(1, 1, TimeUnit.MINUTES) + .test() + .assertResult(2); + } + + @Test + public void takeLastTime() { + Observable.just(1, 2) + .takeLast(1, TimeUnit.MINUTES) + .test() + .assertResult(1, 2); + } + + @Test + public void takeLastTimeDelayError() { + Observable.just(1, 2).concatWith(Observable.<Integer>error(new TestException())) + .takeLast(1, TimeUnit.MINUTES, true) + .test() + .assertFailure(TestException.class, 1, 2); + } + + @Test + public void takeLastTimeDelayErrorCustomScheduler() { + Observable.just(1, 2).concatWith(Observable.<Integer>error(new TestException())) + .takeLast(1, TimeUnit.MINUTES, Schedulers.io(), true) + .test() + .assertFailure(TestException.class, 1, 2); + } + + @Test + public void disposed() { + TestHelper.checkDisposed(PublishSubject.create().takeLast(1, TimeUnit.MINUTES)); + } + + @Test + public void observeOn() { + Observable.range(1, 1000) + .takeLast(1, TimeUnit.DAYS) + .take(500) + .observeOn(Schedulers.single(), true, 1) + .to(TestHelper.<Integer>testConsumer()) + .awaitDone(5, TimeUnit.SECONDS) + .assertSubscribed() + .assertValueCount(500) + .assertNoErrors() + .assertComplete(); + } + + @Test + public void cancelCompleteRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final PublishSubject<Integer> ps = PublishSubject.create(); + + final TestObserver<Integer> to = ps.takeLast(1, TimeUnit.DAYS).test(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + ps.onComplete(); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + to.dispose(); + } + }; + + TestHelper.race(r1, r2); + } + } + + @Test + public void lastWindowIsFixedInTime() { + TimesteppingScheduler scheduler = new TimesteppingScheduler(); + scheduler.stepEnabled = false; + + PublishSubject<Integer> ps = PublishSubject.create(); + + TestObserver<Integer> to = ps + .takeLast(2, TimeUnit.SECONDS, scheduler) + .test(); + + ps.onNext(1); + ps.onNext(2); + ps.onNext(3); + ps.onNext(4); + + scheduler.stepEnabled = true; + + ps.onComplete(); + + to.assertResult(1, 2, 3, 4); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeObservable(o -> o.takeLast(1, TimeUnit.SECONDS)); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableTakeTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableTakeTest.java new file mode 100644 index 0000000000..0ecff80446 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableTakeTest.java @@ -0,0 +1,384 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +import java.util.*; +import java.util.concurrent.*; +import java.util.concurrent.atomic.*; + +import org.junit.Test; +import org.mockito.InOrder; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.core.Observer; +import io.reactivex.rxjava3.disposables.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.schedulers.Schedulers; +import io.reactivex.rxjava3.subjects.PublishSubject; +import io.reactivex.rxjava3.testsupport.*; + +public class ObservableTakeTest extends RxJavaTest { + + @Test + public void take1() { + Observable<String> w = Observable.fromIterable(Arrays.asList("one", "two", "three")); + Observable<String> take = w.take(2); + + Observer<String> observer = TestHelper.mockObserver(); + take.subscribe(observer); + verify(observer, times(1)).onNext("one"); + verify(observer, times(1)).onNext("two"); + verify(observer, never()).onNext("three"); + verify(observer, never()).onError(any(Throwable.class)); + verify(observer, times(1)).onComplete(); + } + + @Test + public void take2() { + Observable<String> w = Observable.fromIterable(Arrays.asList("one", "two", "three")); + Observable<String> take = w.take(1); + + Observer<String> observer = TestHelper.mockObserver(); + take.subscribe(observer); + verify(observer, times(1)).onNext("one"); + verify(observer, never()).onNext("two"); + verify(observer, never()).onNext("three"); + verify(observer, never()).onError(any(Throwable.class)); + verify(observer, times(1)).onComplete(); + } + + @Test(expected = IllegalArgumentException.class) + public void takeWithError() { + Observable.fromIterable(Arrays.asList(1, 2, 3)).take(1) + .map(new Function<Integer, Integer>() { + @Override + public Integer apply(Integer t1) { + throw new IllegalArgumentException("some error"); + } + }).blockingSingle(); + } + + @Test + public void takeWithErrorHappeningInOnNext() { + Observable<Integer> w = Observable.fromIterable(Arrays.asList(1, 2, 3)) + .take(2).map(new Function<Integer, Integer>() { + @Override + public Integer apply(Integer t1) { + throw new IllegalArgumentException("some error"); + } + }); + + Observer<Integer> observer = TestHelper.mockObserver(); + w.subscribe(observer); + InOrder inOrder = inOrder(observer); + inOrder.verify(observer, times(1)).onError(any(IllegalArgumentException.class)); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void takeWithErrorHappeningInTheLastOnNext() { + Observable<Integer> w = Observable.fromIterable(Arrays.asList(1, 2, 3)).take(1).map(new Function<Integer, Integer>() { + @Override + public Integer apply(Integer t1) { + throw new IllegalArgumentException("some error"); + } + }); + + Observer<Integer> observer = TestHelper.mockObserver(); + w.subscribe(observer); + InOrder inOrder = inOrder(observer); + inOrder.verify(observer, times(1)).onError(any(IllegalArgumentException.class)); + inOrder.verifyNoMoreInteractions(); + } + + @Test + @SuppressUndeliverable + public void takeDoesntLeakErrors() { + Observable<String> source = Observable.unsafeCreate(new ObservableSource<String>() { + @Override + public void subscribe(Observer<? super String> observer) { + observer.onSubscribe(Disposable.empty()); + observer.onNext("one"); + observer.onError(new Throwable("test failed")); + } + }); + + Observer<String> observer = TestHelper.mockObserver(); + + source.take(1).subscribe(observer); + + verify(observer).onSubscribe((Disposable)notNull()); + verify(observer, times(1)).onNext("one"); + // even though onError is called we take(1) so shouldn't see it + verify(observer, never()).onError(any(Throwable.class)); + verify(observer, times(1)).onComplete(); + verifyNoMoreInteractions(observer); + } + + @Test + public void unsubscribeAfterTake() { + TestObservableFunc f = new TestObservableFunc("one", "two", "three"); + Observable<String> w = Observable.unsafeCreate(f); + + Observer<String> observer = TestHelper.mockObserver(); + + Observable<String> take = w.take(1); + take.subscribe(observer); + + // wait for the Observable to complete + try { + f.t.join(); + } catch (Throwable e) { + e.printStackTrace(); + fail(e.getMessage()); + } + + System.out.println("TestObservable thread finished"); + verify(observer).onSubscribe((Disposable)notNull()); + verify(observer, times(1)).onNext("one"); + verify(observer, never()).onNext("two"); + verify(observer, never()).onNext("three"); + verify(observer, times(1)).onComplete(); + verifyNoMoreInteractions(observer); + } + + @Test + public void unsubscribeFromSynchronousInfiniteObservable() { + final AtomicLong count = new AtomicLong(); + INFINITE_OBSERVABLE.take(10).subscribe(new Consumer<Long>() { + + @Override + public void accept(Long l) { + count.set(l); + } + + }); + assertEquals(10, count.get()); + } + + @Test + public void multiTake() { + final AtomicInteger count = new AtomicInteger(); + Observable.unsafeCreate(new ObservableSource<Integer>() { + + @Override + public void subscribe(Observer<? super Integer> observer) { + Disposable bs = Disposable.empty(); + observer.onSubscribe(bs); + for (int i = 0; !bs.isDisposed(); i++) { + System.out.println("Emit: " + i); + count.incrementAndGet(); + observer.onNext(i); + } + } + + }).take(100).take(1).blockingForEach(new Consumer<Integer>() { + + @Override + public void accept(Integer t1) { + System.out.println("Receive: " + t1); + + } + + }); + + assertEquals(1, count.get()); + } + + static class TestObservableFunc implements ObservableSource<String> { + + final String[] values; + Thread t; + + TestObservableFunc(String... values) { + this.values = values; + } + + @Override + public void subscribe(final Observer<? super String> observer) { + observer.onSubscribe(Disposable.empty()); + System.out.println("TestObservable subscribed to ..."); + t = new Thread(new Runnable() { + + @Override + public void run() { + try { + System.out.println("running TestObservable thread"); + for (String s : values) { + System.out.println("TestObservable onNext: " + s); + observer.onNext(s); + } + observer.onComplete(); + } catch (Throwable e) { + throw new RuntimeException(e); + } + } + + }); + System.out.println("starting TestObservable thread"); + t.start(); + System.out.println("done starting TestObservable thread"); + } + } + + private static Observable<Long> INFINITE_OBSERVABLE = Observable.unsafeCreate(new ObservableSource<Long>() { + + @Override + public void subscribe(Observer<? super Long> op) { + Disposable d = Disposable.empty(); + op.onSubscribe(d); + long l = 1; + while (!d.isDisposed()) { + op.onNext(l++); + } + op.onComplete(); + } + + }); + + @Test + public void takeObserveOn() { + Observer<Object> o = TestHelper.mockObserver(); + TestObserver<Object> to = new TestObserver<>(o); + + INFINITE_OBSERVABLE + .observeOn(Schedulers.newThread()).take(1).subscribe(to); + to.awaitDone(5, TimeUnit.SECONDS); + to.assertNoErrors(); + + verify(o).onNext(1L); + verify(o, never()).onNext(2L); + verify(o).onComplete(); + verify(o, never()).onError(any(Throwable.class)); + } + + @Test + public void interrupt() throws InterruptedException { + final AtomicReference<Object> exception = new AtomicReference<>(); + final CountDownLatch latch = new CountDownLatch(1); + Observable.just(1).subscribeOn(Schedulers.computation()).take(1) + .subscribe(new Consumer<Integer>() { + + @Override + public void accept(Integer t1) { + try { + Thread.sleep(100); + } catch (Exception e) { + exception.set(e); + e.printStackTrace(); + } finally { + latch.countDown(); + } + } + + }); + + latch.await(); + assertNull(exception.get()); + } + + @Test + public void takeFinalValueThrows() { + Observable<Integer> source = Observable.just(1).take(1); + + TestObserver<Integer> to = new TestObserver<Integer>() { + @Override + public void onNext(Integer t) { + throw new TestException(); + } + }; + + source.safeSubscribe(to); + + to.assertNoValues(); + to.assertError(TestException.class); + to.assertNotComplete(); + } + + @Test + public void reentrantTake() { + final PublishSubject<Integer> source = PublishSubject.create(); + + TestObserver<Integer> to = new TestObserver<>(); + + source.take(1).doOnNext(new Consumer<Integer>() { + @Override + public void accept(Integer v) { + source.onNext(2); + } + }).subscribe(to); + + source.onNext(1); + + to.assertValue(1); + to.assertNoErrors(); + to.assertComplete(); + } + + @Test + public void takeNegative() { + try { + Observable.just(1).take(-99); + fail("Should have thrown"); + } catch (IllegalArgumentException ex) { + assertEquals("count >= 0 required but it was -99", ex.getMessage()); + } + } + + @Test + public void takeZero() { + Observable.just(1) + .take(0) + .test() + .assertResult(); + } + + @Test + public void dispose() { + TestHelper.checkDisposed(PublishSubject.create().take(2)); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeObservable(new Function<Observable<Object>, ObservableSource<Object>>() { + @Override + public ObservableSource<Object> apply(Observable<Object> o) throws Exception { + return o.take(2); + } + }); + } + + @Test + public void errorAfterLimitReached() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + Observable.error(new TestException()) + .take(0) + .test() + .assertResult(); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableTakeTimedTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableTakeTimedTest.java new file mode 100644 index 0000000000..7edbfbcf20 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableTakeTimedTest.java @@ -0,0 +1,134 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import java.util.concurrent.TimeUnit; + +import org.junit.Test; +import org.mockito.InOrder; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.schedulers.TestScheduler; +import io.reactivex.rxjava3.subjects.PublishSubject; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class ObservableTakeTimedTest extends RxJavaTest { + + @Test + public void takeTimed() { + TestScheduler scheduler = new TestScheduler(); + + PublishSubject<Integer> source = PublishSubject.create(); + + Observable<Integer> result = source.take(1, TimeUnit.SECONDS, scheduler); + + Observer<Object> o = TestHelper.mockObserver(); + + result.subscribe(o); + + source.onNext(1); + source.onNext(2); + source.onNext(3); + + scheduler.advanceTimeBy(1, TimeUnit.SECONDS); + + source.onNext(4); + + InOrder inOrder = inOrder(o); + inOrder.verify(o).onNext(1); + inOrder.verify(o).onNext(2); + inOrder.verify(o).onNext(3); + inOrder.verify(o).onComplete(); + inOrder.verifyNoMoreInteractions(); + + verify(o, never()).onNext(4); + verify(o, never()).onError(any(Throwable.class)); + } + + @Test + public void takeTimedErrorBeforeTime() { + TestScheduler scheduler = new TestScheduler(); + + PublishSubject<Integer> source = PublishSubject.create(); + + Observable<Integer> result = source.take(1, TimeUnit.SECONDS, scheduler); + + Observer<Object> o = TestHelper.mockObserver(); + + result.subscribe(o); + + source.onNext(1); + source.onNext(2); + source.onNext(3); + source.onError(new TestException()); + + scheduler.advanceTimeBy(1, TimeUnit.SECONDS); + + source.onNext(4); + + InOrder inOrder = inOrder(o); + inOrder.verify(o).onNext(1); + inOrder.verify(o).onNext(2); + inOrder.verify(o).onNext(3); + inOrder.verify(o).onError(any(TestException.class)); + inOrder.verifyNoMoreInteractions(); + + verify(o, never()).onComplete(); + verify(o, never()).onNext(4); + } + + @Test + public void takeTimedErrorAfterTime() { + TestScheduler scheduler = new TestScheduler(); + + PublishSubject<Integer> source = PublishSubject.create(); + + Observable<Integer> result = source.take(1, TimeUnit.SECONDS, scheduler); + + Observer<Object> o = TestHelper.mockObserver(); + + result.subscribe(o); + + source.onNext(1); + source.onNext(2); + source.onNext(3); + + scheduler.advanceTimeBy(1, TimeUnit.SECONDS); + + source.onNext(4); + source.onError(new TestException()); + + InOrder inOrder = inOrder(o); + inOrder.verify(o).onNext(1); + inOrder.verify(o).onNext(2); + inOrder.verify(o).onNext(3); + inOrder.verify(o).onComplete(); + inOrder.verifyNoMoreInteractions(); + + verify(o, never()).onNext(4); + verify(o, never()).onError(any(TestException.class)); + } + + @Test + public void timedDefaultScheduler() { + Observable.range(1, 5).take(1, TimeUnit.MINUTES) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(1, 2, 3, 4, 5); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableTakeUntilPredicateTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableTakeUntilPredicateTest.java new file mode 100644 index 0000000000..04d0ab5537 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableTakeUntilPredicateTest.java @@ -0,0 +1,199 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import java.util.List; + +import io.reactivex.rxjava3.disposables.Disposable; +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.subjects.PublishSubject; +import io.reactivex.rxjava3.testsupport.*; +; + +public class ObservableTakeUntilPredicateTest extends RxJavaTest { + @Test + public void takeEmpty() { + Observer<Object> o = TestHelper.mockObserver(); + + Observable.empty().takeUntil(new Predicate<Object>() { + @Override + public boolean test(Object v) { + return true; + } + }).subscribe(o); + + verify(o, never()).onNext(any()); + verify(o, never()).onError(any(Throwable.class)); + verify(o).onComplete(); + } + + @Test + public void takeAll() { + Observer<Object> o = TestHelper.mockObserver(); + + Observable.just(1, 2).takeUntil(new Predicate<Integer>() { + @Override + public boolean test(Integer v) { + return false; + } + }).subscribe(o); + + verify(o).onNext(1); + verify(o).onNext(2); + verify(o, never()).onError(any(Throwable.class)); + verify(o).onComplete(); + } + + @Test + public void takeFirst() { + Observer<Object> o = TestHelper.mockObserver(); + + Observable.just(1, 2).takeUntil(new Predicate<Integer>() { + @Override + public boolean test(Integer v) { + return true; + } + }).subscribe(o); + + verify(o).onNext(1); + verify(o, never()).onNext(2); + verify(o, never()).onError(any(Throwable.class)); + verify(o).onComplete(); + } + + @Test + public void takeSome() { + Observer<Object> o = TestHelper.mockObserver(); + + Observable.just(1, 2, 3).takeUntil(new Predicate<Integer>() { + @Override + public boolean test(Integer t1) { + return t1 == 2; + } + }) + .subscribe(o); + + verify(o).onNext(1); + verify(o).onNext(2); + verify(o, never()).onNext(3); + verify(o, never()).onError(any(Throwable.class)); + verify(o).onComplete(); + } + + @Test + public void functionThrows() { + Observer<Object> o = TestHelper.mockObserver(); + + Predicate<Integer> predicate = (new Predicate<Integer>() { + @Override + public boolean test(Integer t1) { + throw new TestException("Forced failure"); + } + }); + Observable.just(1, 2, 3).takeUntil(predicate).subscribe(o); + + verify(o).onNext(1); + verify(o, never()).onNext(2); + verify(o, never()).onNext(3); + verify(o).onError(any(TestException.class)); + verify(o, never()).onComplete(); + } + + @Test + public void sourceThrows() { + Observer<Object> o = TestHelper.mockObserver(); + + Observable.just(1) + .concatWith(Observable.<Integer>error(new TestException())) + .concatWith(Observable.just(2)) + .takeUntil(new Predicate<Integer>() { + @Override + public boolean test(Integer v) { + return false; + } + }).subscribe(o); + + verify(o).onNext(1); + verify(o, never()).onNext(2); + verify(o).onError(any(TestException.class)); + verify(o, never()).onComplete(); + } + + @Test + public void errorIncludesLastValueAsCause() { + TestObserverEx<String> to = new TestObserverEx<>(); + final TestException e = new TestException("Forced failure"); + Predicate<String> predicate = (new Predicate<String>() { + @Override + public boolean test(String t) { + throw e; + } + }); + Observable.just("abc").takeUntil(predicate).subscribe(to); + + to.assertTerminated(); + to.assertNotComplete(); + to.assertError(TestException.class); + // FIXME last cause value is not saved +// assertTrue(ts.errors().get(0).getCause().getMessage().contains("abc")); + } + + @Test + public void dispose() { + TestHelper.checkDisposed(PublishSubject.create().takeUntil(Functions.alwaysFalse())); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeObservable(new Function<Observable<Object>, ObservableSource<Object>>() { + @Override + public ObservableSource<Object> apply(Observable<Object> o) throws Exception { + return o.takeUntil(Functions.alwaysFalse()); + } + }); + } + + @Test + public void badSource() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + new Observable<Integer>() { + @Override + protected void subscribeActual(Observer<? super Integer> observer) { + observer.onSubscribe(Disposable.empty()); + observer.onComplete(); + observer.onNext(1); + observer.onError(new TestException()); + observer.onComplete(); + } + } + .takeUntil(Functions.alwaysFalse()) + .test() + .assertResult(); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableTakeUntilTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableTakeUntilTest.java new file mode 100644 index 0000000000..4bbf042acc --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableTakeUntilTest.java @@ -0,0 +1,407 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.subjects.PublishSubject; +import io.reactivex.rxjava3.testsupport.*; + +public class ObservableTakeUntilTest extends RxJavaTest { + + @Test + public void takeUntil() { + Disposable sSource = mock(Disposable.class); + Disposable sOther = mock(Disposable.class); + TestObservable source = new TestObservable(sSource); + TestObservable other = new TestObservable(sOther); + + Observer<String> result = TestHelper.mockObserver(); + Observable<String> stringObservable = Observable.unsafeCreate(source) + .takeUntil(Observable.unsafeCreate(other)); + stringObservable.subscribe(result); + source.sendOnNext("one"); + source.sendOnNext("two"); + other.sendOnNext("three"); + source.sendOnNext("four"); + source.sendOnCompleted(); + other.sendOnCompleted(); + + verify(result, times(1)).onNext("one"); + verify(result, times(1)).onNext("two"); + verify(result, times(0)).onNext("three"); + verify(result, times(0)).onNext("four"); + verify(sSource, times(1)).dispose(); + verify(sOther, times(1)).dispose(); + + } + + @Test + public void takeUntilSourceCompleted() { + Disposable sSource = mock(Disposable.class); + Disposable sOther = mock(Disposable.class); + TestObservable source = new TestObservable(sSource); + TestObservable other = new TestObservable(sOther); + + Observer<String> result = TestHelper.mockObserver(); + Observable<String> stringObservable = Observable.unsafeCreate(source).takeUntil(Observable.unsafeCreate(other)); + stringObservable.subscribe(result); + source.sendOnNext("one"); + source.sendOnNext("two"); + source.sendOnCompleted(); + + verify(result, times(1)).onNext("one"); + verify(result, times(1)).onNext("two"); + verify(sSource, never()).dispose(); // no longer disposing itself on terminal events + verify(sOther, times(1)).dispose(); + + } + + @Test + public void takeUntilSourceError() { + Disposable sSource = mock(Disposable.class); + Disposable sOther = mock(Disposable.class); + TestObservable source = new TestObservable(sSource); + TestObservable other = new TestObservable(sOther); + Throwable error = new Throwable(); + + Observer<String> result = TestHelper.mockObserver(); + Observable<String> stringObservable = Observable.unsafeCreate(source).takeUntil(Observable.unsafeCreate(other)); + stringObservable.subscribe(result); + source.sendOnNext("one"); + source.sendOnNext("two"); + source.sendOnError(error); + source.sendOnNext("three"); + + verify(result, times(1)).onNext("one"); + verify(result, times(1)).onNext("two"); + verify(result, times(0)).onNext("three"); + verify(result, times(1)).onError(error); + verify(sSource, never()).dispose(); // no longer disposing itself on terminal events + verify(sOther, times(1)).dispose(); + + } + + @Test + public void takeUntilOtherError() { + Disposable sSource = mock(Disposable.class); + Disposable sOther = mock(Disposable.class); + TestObservable source = new TestObservable(sSource); + TestObservable other = new TestObservable(sOther); + Throwable error = new Throwable(); + + Observer<String> result = TestHelper.mockObserver(); + Observable<String> stringObservable = Observable.unsafeCreate(source).takeUntil(Observable.unsafeCreate(other)); + stringObservable.subscribe(result); + source.sendOnNext("one"); + source.sendOnNext("two"); + other.sendOnError(error); + source.sendOnNext("three"); + + verify(result, times(1)).onNext("one"); + verify(result, times(1)).onNext("two"); + verify(result, times(0)).onNext("three"); + verify(result, times(1)).onError(error); + verify(result, times(0)).onComplete(); + verify(sSource, times(1)).dispose(); + verify(sOther, never()).dispose(); // no longer disposing itself on termination + + } + + /** + * If the 'other' onCompletes then we unsubscribe from the source and onComplete. + */ + @Test + public void takeUntilOtherCompleted() { + Disposable sSource = mock(Disposable.class); + Disposable sOther = mock(Disposable.class); + TestObservable source = new TestObservable(sSource); + TestObservable other = new TestObservable(sOther); + + Observer<String> result = TestHelper.mockObserver(); + Observable<String> stringObservable = Observable.unsafeCreate(source).takeUntil(Observable.unsafeCreate(other)); + stringObservable.subscribe(result); + source.sendOnNext("one"); + source.sendOnNext("two"); + other.sendOnCompleted(); + source.sendOnNext("three"); + + verify(result, times(1)).onNext("one"); + verify(result, times(1)).onNext("two"); + verify(result, times(0)).onNext("three"); + verify(result, times(1)).onComplete(); + verify(sSource, times(1)).dispose(); + verify(sOther, never()).dispose(); // no longer disposing itself on terminal events + + } + + private static class TestObservable implements ObservableSource<String> { + + Observer<? super String> observer; + Disposable upstream; + + TestObservable(Disposable d) { + this.upstream = d; + } + + /* used to simulate subscription */ + public void sendOnCompleted() { + observer.onComplete(); + } + + /* used to simulate subscription */ + public void sendOnNext(String value) { + observer.onNext(value); + } + + /* used to simulate subscription */ + public void sendOnError(Throwable e) { + observer.onError(e); + } + + @Override + public void subscribe(Observer<? super String> observer) { + this.observer = observer; + observer.onSubscribe(upstream); + } + } + + @Test + public void untilFires() { + PublishSubject<Integer> source = PublishSubject.create(); + PublishSubject<Integer> until = PublishSubject.create(); + + TestObserverEx<Integer> to = new TestObserverEx<>(); + + source.takeUntil(until).subscribe(to); + + assertTrue(source.hasObservers()); + assertTrue(until.hasObservers()); + + source.onNext(1); + + to.assertValue(1); + until.onNext(1); + + to.assertValue(1); + to.assertNoErrors(); + to.assertTerminated(); + + assertFalse("Source still has observers", source.hasObservers()); + assertFalse("Until still has observers", until.hasObservers()); + // 2.0.2 - not anymore +// assertTrue("Not cancelled!", ts.isCancelled()); + } + + @Test + public void mainCompletes() { + PublishSubject<Integer> source = PublishSubject.create(); + PublishSubject<Integer> until = PublishSubject.create(); + + TestObserverEx<Integer> to = new TestObserverEx<>(); + + source.takeUntil(until).subscribe(to); + + assertTrue(source.hasObservers()); + assertTrue(until.hasObservers()); + + source.onNext(1); + source.onComplete(); + + to.assertValue(1); + to.assertNoErrors(); + to.assertTerminated(); + + assertFalse("Source still has observers", source.hasObservers()); + assertFalse("Until still has observers", until.hasObservers()); + // 2.0.2 - not anymore +// assertTrue("Not cancelled!", ts.isCancelled()); + } + + @Test + public void downstreamUnsubscribes() { + PublishSubject<Integer> source = PublishSubject.create(); + PublishSubject<Integer> until = PublishSubject.create(); + + TestObserverEx<Integer> to = new TestObserverEx<>(); + + source.takeUntil(until).take(1).subscribe(to); + + assertTrue(source.hasObservers()); + assertTrue(until.hasObservers()); + + source.onNext(1); + + to.assertValue(1); + to.assertNoErrors(); + to.assertTerminated(); + + assertFalse("Source still has observers", source.hasObservers()); + assertFalse("Until still has observers", until.hasObservers()); + // 2.0.2 - not anymore +// assertTrue("Not cancelled!", ts.isCancelled()); + } + + @Test + public void dispose() { + TestHelper.checkDisposed(PublishSubject.create().takeUntil(Observable.never())); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeObservable(new Function<Observable<Integer>, Observable<Integer>>() { + @Override + public Observable<Integer> apply(Observable<Integer> o) throws Exception { + return o.takeUntil(Observable.never()); + } + }); + } + + @Test + public void untilPublisherMainSuccess() { + PublishSubject<Integer> main = PublishSubject.create(); + PublishSubject<Integer> other = PublishSubject.create(); + + TestObserver<Integer> to = main.takeUntil(other).test(); + + assertTrue("Main no observers?", main.hasObservers()); + assertTrue("Other no observers?", other.hasObservers()); + + main.onNext(1); + main.onNext(2); + main.onComplete(); + + assertFalse("Main has observers?", main.hasObservers()); + assertFalse("Other has observers?", other.hasObservers()); + + to.assertResult(1, 2); + } + + @Test + public void untilPublisherMainComplete() { + PublishSubject<Integer> main = PublishSubject.create(); + PublishSubject<Integer> other = PublishSubject.create(); + + TestObserver<Integer> to = main.takeUntil(other).test(); + + assertTrue("Main no observers?", main.hasObservers()); + assertTrue("Other no observers?", other.hasObservers()); + + main.onComplete(); + + assertFalse("Main has observers?", main.hasObservers()); + assertFalse("Other has observers?", other.hasObservers()); + + to.assertResult(); + } + + @Test + public void untilPublisherMainError() { + PublishSubject<Integer> main = PublishSubject.create(); + PublishSubject<Integer> other = PublishSubject.create(); + + TestObserver<Integer> to = main.takeUntil(other).test(); + + assertTrue("Main no observers?", main.hasObservers()); + assertTrue("Other no observers?", other.hasObservers()); + + main.onError(new TestException()); + + assertFalse("Main has observers?", main.hasObservers()); + assertFalse("Other has observers?", other.hasObservers()); + + to.assertFailure(TestException.class); + } + + @Test + public void untilPublisherOtherOnNext() { + PublishSubject<Integer> main = PublishSubject.create(); + PublishSubject<Integer> other = PublishSubject.create(); + + TestObserver<Integer> to = main.takeUntil(other).test(); + + assertTrue("Main no observers?", main.hasObservers()); + assertTrue("Other no observers?", other.hasObservers()); + + other.onNext(1); + + assertFalse("Main has observers?", main.hasObservers()); + assertFalse("Other has observers?", other.hasObservers()); + + to.assertResult(); + } + + @Test + public void untilPublisherOtherOnComplete() { + PublishSubject<Integer> main = PublishSubject.create(); + PublishSubject<Integer> other = PublishSubject.create(); + + TestObserver<Integer> to = main.takeUntil(other).test(); + + assertTrue("Main no observers?", main.hasObservers()); + assertTrue("Other no observers?", other.hasObservers()); + + other.onComplete(); + + assertFalse("Main has observers?", main.hasObservers()); + assertFalse("Other has observers?", other.hasObservers()); + + to.assertResult(); + } + + @Test + public void untilPublisherOtherError() { + PublishSubject<Integer> main = PublishSubject.create(); + PublishSubject<Integer> other = PublishSubject.create(); + + TestObserver<Integer> to = main.takeUntil(other).test(); + + assertTrue("Main no observers?", main.hasObservers()); + assertTrue("Other no observers?", other.hasObservers()); + + other.onError(new TestException()); + + assertFalse("Main has observers?", main.hasObservers()); + assertFalse("Other has observers?", other.hasObservers()); + + to.assertFailure(TestException.class); + } + + @Test + public void untilPublisherDispose() { + PublishSubject<Integer> main = PublishSubject.create(); + PublishSubject<Integer> other = PublishSubject.create(); + + TestObserver<Integer> to = main.takeUntil(other).test(); + + assertTrue("Main no observers?", main.hasObservers()); + assertTrue("Other no observers?", other.hasObservers()); + + to.dispose(); + + assertFalse("Main has observers?", main.hasObservers()); + assertFalse("Other has observers?", other.hasObservers()); + + to.assertEmpty(); + } + +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableTakeWhileTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableTakeWhileTest.java new file mode 100644 index 0000000000..0a9fc27136 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableTakeWhileTest.java @@ -0,0 +1,284 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import static org.junit.Assert.fail; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.subjects.*; +import io.reactivex.rxjava3.testsupport.*; + +public class ObservableTakeWhileTest extends RxJavaTest { + + @Test + public void takeWhile1() { + Observable<Integer> w = Observable.just(1, 2, 3); + Observable<Integer> take = w.takeWhile(new Predicate<Integer>() { + @Override + public boolean test(Integer input) { + return input < 3; + } + }); + + Observer<Integer> observer = TestHelper.mockObserver(); + take.subscribe(observer); + verify(observer, times(1)).onNext(1); + verify(observer, times(1)).onNext(2); + verify(observer, never()).onNext(3); + verify(observer, never()).onError(any(Throwable.class)); + verify(observer, times(1)).onComplete(); + } + + @Test + public void takeWhileOnSubject1() { + Subject<Integer> s = PublishSubject.create(); + Observable<Integer> take = s.takeWhile(new Predicate<Integer>() { + @Override + public boolean test(Integer input) { + return input < 3; + } + }); + + Observer<Integer> observer = TestHelper.mockObserver(); + take.subscribe(observer); + + s.onNext(1); + s.onNext(2); + s.onNext(3); + s.onNext(4); + s.onNext(5); + s.onComplete(); + + verify(observer, times(1)).onNext(1); + verify(observer, times(1)).onNext(2); + verify(observer, never()).onNext(3); + verify(observer, never()).onNext(4); + verify(observer, never()).onNext(5); + verify(observer, never()).onError(any(Throwable.class)); + verify(observer, times(1)).onComplete(); + } + + @Test + public void takeWhile2() { + Observable<String> w = Observable.just("one", "two", "three"); + Observable<String> take = w.takeWhile(new Predicate<String>() { + int index; + + @Override + public boolean test(String input) { + return index++ < 2; + } + }); + + Observer<String> observer = TestHelper.mockObserver(); + take.subscribe(observer); + verify(observer, times(1)).onNext("one"); + verify(observer, times(1)).onNext("two"); + verify(observer, never()).onNext("three"); + verify(observer, never()).onError(any(Throwable.class)); + verify(observer, times(1)).onComplete(); + } + + @Test + @SuppressUndeliverable + public void takeWhileDoesntLeakErrors() { + Observable<String> source = Observable.unsafeCreate(new ObservableSource<String>() { + @Override + public void subscribe(Observer<? super String> observer) { + observer.onSubscribe(Disposable.empty()); + observer.onNext("one"); + observer.onError(new Throwable("test failed")); + } + }); + + source.takeWhile(new Predicate<String>() { + @Override + public boolean test(String s) { + return false; + } + }).blockingLast(""); + } + + @Test + public void takeWhileProtectsPredicateCall() { + TestObservable source = new TestObservable(mock(Disposable.class), "one"); + final RuntimeException testException = new RuntimeException("test exception"); + + Observer<String> observer = TestHelper.mockObserver(); + Observable<String> take = Observable.unsafeCreate(source) + .takeWhile(new Predicate<String>() { + @Override + public boolean test(String s) { + throw testException; + } + }); + take.subscribe(observer); + + // wait for the Observable to complete + try { + source.t.join(); + } catch (Throwable e) { + e.printStackTrace(); + fail(e.getMessage()); + } + + verify(observer, never()).onNext(any(String.class)); + verify(observer, times(1)).onError(testException); + } + + @Test + public void unsubscribeAfterTake() { + Disposable upstream = mock(Disposable.class); + TestObservable w = new TestObservable(upstream, "one", "two", "three"); + + Observer<String> observer = TestHelper.mockObserver(); + Observable<String> take = Observable.unsafeCreate(w) + .takeWhile(new Predicate<String>() { + int index; + + @Override + public boolean test(String s) { + return index++ < 1; + } + }); + take.subscribe(observer); + + // wait for the Observable to complete + try { + w.t.join(); + } catch (Throwable e) { + e.printStackTrace(); + fail(e.getMessage()); + } + + System.out.println("TestObservable thread finished"); + verify(observer, times(1)).onNext("one"); + verify(observer, never()).onNext("two"); + verify(observer, never()).onNext("three"); + verify(upstream, times(1)).dispose(); + } + + private static class TestObservable implements ObservableSource<String> { + + final Disposable upstream; + final String[] values; + Thread t; + + TestObservable(Disposable upstream, String... values) { + this.upstream = upstream; + this.values = values; + } + + @Override + public void subscribe(final Observer<? super String> observer) { + System.out.println("TestObservable subscribed to ..."); + observer.onSubscribe(upstream); + t = new Thread(new Runnable() { + + @Override + public void run() { + try { + System.out.println("running TestObservable thread"); + for (String s : values) { + System.out.println("TestObservable onNext: " + s); + observer.onNext(s); + } + observer.onComplete(); + } catch (Throwable e) { + throw new RuntimeException(e); + } + } + + }); + System.out.println("starting TestObservable thread"); + t.start(); + System.out.println("done starting TestObservable thread"); + } + } + + @Test + public void noUnsubscribeDownstream() { + Observable<Integer> source = Observable.range(1, 1000).takeWhile(new Predicate<Integer>() { + @Override + public boolean test(Integer t1) { + return t1 < 2; + } + }); + TestObserver<Integer> to = new TestObserver<>(); + + source.subscribe(to); + + to.assertNoErrors(); + to.assertValue(1); + + // 2.0.2 - not anymore +// Assert.assertTrue("Not cancelled!", ts.isCancelled()); + } + + @Test + public void errorCauseIncludesLastValue() { + TestObserverEx<String> to = new TestObserverEx<>(); + Observable.just("abc").takeWhile(new Predicate<String>() { + @Override + public boolean test(String t1) { + throw new TestException(); + } + }).subscribe(to); + + to.assertTerminated(); + to.assertNoValues(); + to.assertError(TestException.class); + // FIXME last cause value not recorded +// assertTrue(ts.getOnErrorEvents().get(0).getCause().getMessage().contains("abc")); + } + + @Test + public void dispose() { + TestHelper.checkDisposed(PublishSubject.create().takeWhile(Functions.alwaysTrue())); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeObservable(new Function<Observable<Object>, ObservableSource<Object>>() { + @Override + public ObservableSource<Object> apply(Observable<Object> o) throws Exception { + return o.takeWhile(Functions.alwaysTrue()); + } + }); + } + + @Test + public void badSource() { + new Observable<Integer>() { + @Override + protected void subscribeActual(Observer<? super Integer> observer) { + observer.onSubscribe(Disposable.empty()); + observer.onComplete(); + observer.onComplete(); + } + } + .takeWhile(Functions.alwaysTrue()) + .test() + .assertResult(); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableThrottleFirstTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableThrottleFirstTest.java new file mode 100644 index 0000000000..fe296d572b --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableThrottleFirstTest.java @@ -0,0 +1,242 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import java.util.concurrent.TimeUnit; + +import io.reactivex.rxjava3.functions.Action; +import org.junit.*; +import org.mockito.InOrder; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.schedulers.TestScheduler; +import io.reactivex.rxjava3.subjects.PublishSubject; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class ObservableThrottleFirstTest extends RxJavaTest { + + private TestScheduler scheduler; + private Scheduler.Worker innerScheduler; + private Observer<String> observer; + + @Before + public void before() { + scheduler = new TestScheduler(); + innerScheduler = scheduler.createWorker(); + observer = TestHelper.mockObserver(); + } + + @Test + public void throttlingWithDropCallbackCrashes() throws Throwable { + Observable<String> source = Observable.unsafeCreate(new ObservableSource<String>() { + @Override + public void subscribe(Observer<? super String> innerObserver) { + innerObserver.onSubscribe(Disposable.empty()); + publishNext(innerObserver, 100, "one"); // publish as it's first + publishNext(innerObserver, 300, "two"); // skip as it's last within the first 400 + publishNext(innerObserver, 900, "three"); // publish + publishNext(innerObserver, 905, "four"); // skip + publishCompleted(innerObserver, 1000); // Should be published as soon as the timeout expires. + } + }); + + Action whenDisposed = mock(Action.class); + Observable<String> sampled = source + .doOnDispose(whenDisposed) + .throttleFirst(400, TimeUnit.MILLISECONDS, scheduler, e -> { + if ("two".equals(e)) { + throw new TestException("forced"); + } + }); + sampled.subscribe(observer); + + InOrder inOrder = inOrder(observer); + + scheduler.advanceTimeTo(1000, TimeUnit.MILLISECONDS); + inOrder.verify(observer, times(1)).onNext("one"); + inOrder.verify(observer, times(1)).onError(any(TestException.class)); + inOrder.verify(observer, times(0)).onNext("two"); + inOrder.verify(observer, times(0)).onNext("three"); + inOrder.verify(observer, times(0)).onNext("four"); + inOrder.verify(observer, times(0)).onComplete(); + inOrder.verifyNoMoreInteractions(); + verify(whenDisposed).run(); + } + + @Test + public void throttlingWithDropCallback() { + Observable<String> source = Observable.unsafeCreate(new ObservableSource<String>() { + @Override + public void subscribe(Observer<? super String> innerObserver) { + innerObserver.onSubscribe(Disposable.empty()); + publishNext(innerObserver, 100, "one"); // publish as it's first + publishNext(innerObserver, 300, "two"); // skip as it's last within the first 400 + publishNext(innerObserver, 900, "three"); // publish + publishNext(innerObserver, 905, "four"); // skip + publishCompleted(innerObserver, 1000); // Should be published as soon as the timeout expires. + } + }); + + Observer<Object> dropCallbackObserver = TestHelper.mockObserver(); + Observable<String> sampled = source.throttleFirst(400, TimeUnit.MILLISECONDS, scheduler, dropCallbackObserver::onNext); + sampled.subscribe(observer); + + InOrder inOrder = inOrder(observer); + InOrder dropCallbackOrder = inOrder(dropCallbackObserver); + + scheduler.advanceTimeTo(1000, TimeUnit.MILLISECONDS); + inOrder.verify(observer, times(1)).onNext("one"); + inOrder.verify(observer, times(0)).onNext("two"); + dropCallbackOrder.verify(dropCallbackObserver, times(1)).onNext("two"); + inOrder.verify(observer, times(1)).onNext("three"); + inOrder.verify(observer, times(0)).onNext("four"); + dropCallbackOrder.verify(dropCallbackObserver, times(1)).onNext("four"); + inOrder.verify(observer, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + dropCallbackOrder.verifyNoMoreInteractions(); + } + + @Test + public void throttlingWithCompleted() { + Observable<String> source = Observable.unsafeCreate(new ObservableSource<String>() { + @Override + public void subscribe(Observer<? super String> innerObserver) { + innerObserver.onSubscribe(Disposable.empty()); + publishNext(innerObserver, 100, "one"); // publish as it's first + publishNext(innerObserver, 300, "two"); // skip as it's last within the first 400 + publishNext(innerObserver, 900, "three"); // publish + publishNext(innerObserver, 905, "four"); // skip + publishCompleted(innerObserver, 1000); // Should be published as soon as the timeout expires. + } + }); + + Observable<String> sampled = source.throttleFirst(400, TimeUnit.MILLISECONDS, scheduler); + sampled.subscribe(observer); + + InOrder inOrder = inOrder(observer); + + scheduler.advanceTimeTo(1000, TimeUnit.MILLISECONDS); + inOrder.verify(observer, times(1)).onNext("one"); + inOrder.verify(observer, times(0)).onNext("two"); + inOrder.verify(observer, times(1)).onNext("three"); + inOrder.verify(observer, times(0)).onNext("four"); + inOrder.verify(observer, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void throttlingWithError() { + Observable<String> source = Observable.unsafeCreate(new ObservableSource<String>() { + @Override + public void subscribe(Observer<? super String> innerObserver) { + innerObserver.onSubscribe(Disposable.empty()); + Exception error = new TestException(); + publishNext(innerObserver, 100, "one"); // Should be published since it is first + publishNext(innerObserver, 200, "two"); // Should be skipped since onError will arrive before the timeout expires + publishError(innerObserver, 300, error); // Should be published as soon as the timeout expires. + } + }); + + Observable<String> sampled = source.throttleFirst(400, TimeUnit.MILLISECONDS, scheduler); + sampled.subscribe(observer); + + InOrder inOrder = inOrder(observer); + + scheduler.advanceTimeTo(400, TimeUnit.MILLISECONDS); + inOrder.verify(observer).onNext("one"); + inOrder.verify(observer).onError(any(TestException.class)); + inOrder.verifyNoMoreInteractions(); + } + + private <T> void publishCompleted(final Observer<T> innerObserver, long delay) { + innerScheduler.schedule(new Runnable() { + @Override + public void run() { + innerObserver.onComplete(); + } + }, delay, TimeUnit.MILLISECONDS); + } + + private <T> void publishError(final Observer<T> innerObserver, long delay, final Exception error) { + innerScheduler.schedule(new Runnable() { + @Override + public void run() { + innerObserver.onError(error); + } + }, delay, TimeUnit.MILLISECONDS); + } + + private <T> void publishNext(final Observer<T> innerObserver, long delay, final T value) { + innerScheduler.schedule(new Runnable() { + @Override + public void run() { + innerObserver.onNext(value); + } + }, delay, TimeUnit.MILLISECONDS); + } + + @Test + public void throttle() { + Observer<Integer> observer = TestHelper.mockObserver(); + TestScheduler s = new TestScheduler(); + PublishSubject<Integer> o = PublishSubject.create(); + o.throttleFirst(500, TimeUnit.MILLISECONDS, s).subscribe(observer); + + // send events with simulated time increments + s.advanceTimeTo(0, TimeUnit.MILLISECONDS); + o.onNext(1); // deliver + o.onNext(2); // skip + s.advanceTimeTo(501, TimeUnit.MILLISECONDS); + o.onNext(3); // deliver + s.advanceTimeTo(600, TimeUnit.MILLISECONDS); + o.onNext(4); // skip + s.advanceTimeTo(700, TimeUnit.MILLISECONDS); + o.onNext(5); // skip + o.onNext(6); // skip + s.advanceTimeTo(1001, TimeUnit.MILLISECONDS); + o.onNext(7); // deliver + s.advanceTimeTo(1501, TimeUnit.MILLISECONDS); + o.onComplete(); + + InOrder inOrder = inOrder(observer); + inOrder.verify(observer).onNext(1); + inOrder.verify(observer).onNext(3); + inOrder.verify(observer).onNext(7); + inOrder.verify(observer).onComplete(); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void throttleFirstDefaultScheduler() { + Observable.just(1).throttleFirst(100, TimeUnit.MILLISECONDS) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(1); + } + + @Test + public void dispose() { + TestHelper.checkDisposed(Observable.just(1).throttleFirst(1, TimeUnit.DAYS)); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeObservable(o -> o.throttleFirst(1, TimeUnit.SECONDS)); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableThrottleLatestTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableThrottleLatestTest.java new file mode 100644 index 0000000000..e1c15f3b1a --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableThrottleLatestTest.java @@ -0,0 +1,645 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import static org.mockito.Mockito.*; + +import java.util.concurrent.TimeUnit; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.schedulers.TestScheduler; +import io.reactivex.rxjava3.subjects.PublishSubject; +import io.reactivex.rxjava3.testsupport.*; + +public class ObservableThrottleLatestTest extends RxJavaTest { + + @Test + public void just() { + Observable.just(1) + .throttleLatest(1, TimeUnit.MINUTES) + .test() + .assertResult(1); + } + + @Test + public void range() { + Observable.range(1, 5) + .throttleLatest(1, TimeUnit.MINUTES) + .test() + .assertResult(1); + } + + @Test + public void rangeEmitLatest() { + Observable.range(1, 5) + .throttleLatest(1, TimeUnit.MINUTES, true) + .test() + .assertResult(1, 5); + } + + @Test + public void error() { + Observable.error(new TestException()) + .throttleLatest(1, TimeUnit.MINUTES) + .test() + .assertFailure(TestException.class); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeObservable(new Function<Observable<Object>, Observable<Object>>() { + @Override + public Observable<Object> apply(Observable<Object> f) throws Exception { + return f.throttleLatest(1, TimeUnit.MINUTES); + } + }); + } + + @Test + public void disposed() { + TestHelper.checkDisposed( + Observable.never() + .throttleLatest(1, TimeUnit.MINUTES) + ); + } + + @Test + public void normal() { + TestScheduler sch = new TestScheduler(); + PublishSubject<Integer> ps = PublishSubject.create(); + + TestObserver<Integer> to = ps.throttleLatest(1, TimeUnit.SECONDS, sch).test(); + + ps.onNext(1); + + to.assertValuesOnly(1); + + ps.onNext(2); + + to.assertValuesOnly(1); + + ps.onNext(3); + + to.assertValuesOnly(1); + + sch.advanceTimeBy(1, TimeUnit.SECONDS); + + to.assertValuesOnly(1, 3); + + ps.onNext(4); + + to.assertValuesOnly(1, 3); + + ps.onNext(5); + + sch.advanceTimeBy(1, TimeUnit.SECONDS); + + to.assertValuesOnly(1, 3, 5); + + sch.advanceTimeBy(1, TimeUnit.SECONDS); + + to.assertValuesOnly(1, 3, 5); + + ps.onNext(6); + + to.assertValuesOnly(1, 3, 5, 6); + + ps.onNext(7); + ps.onComplete(); + + to.assertResult(1, 3, 5, 6); + + sch.advanceTimeBy(1, TimeUnit.SECONDS); + + to.assertResult(1, 3, 5, 6); + } + + @Test + public void normalEmitLast() { + TestScheduler sch = new TestScheduler(); + PublishSubject<Integer> ps = PublishSubject.create(); + + TestObserver<Integer> to = ps.throttleLatest(1, TimeUnit.SECONDS, sch, true).test(); + + ps.onNext(1); + + to.assertValuesOnly(1); + + ps.onNext(2); + + to.assertValuesOnly(1); + + ps.onNext(3); + + to.assertValuesOnly(1); + + sch.advanceTimeBy(1, TimeUnit.SECONDS); + + to.assertValuesOnly(1, 3); + + ps.onNext(4); + + to.assertValuesOnly(1, 3); + + ps.onNext(5); + + sch.advanceTimeBy(1, TimeUnit.SECONDS); + + to.assertValuesOnly(1, 3, 5); + + sch.advanceTimeBy(1, TimeUnit.SECONDS); + + to.assertValuesOnly(1, 3, 5); + + ps.onNext(6); + + to.assertValuesOnly(1, 3, 5, 6); + + ps.onNext(7); + ps.onComplete(); + + to.assertResult(1, 3, 5, 6, 7); + + sch.advanceTimeBy(1, TimeUnit.SECONDS); + + to.assertResult(1, 3, 5, 6, 7); + } + + @Test + public void take() throws Throwable { + Action onCancel = mock(Action.class); + + Observable.range(1, 5) + .doOnDispose(onCancel) + .throttleLatest(1, TimeUnit.MINUTES) + .take(1) + .test() + .assertResult(1); + + verify(onCancel).run(); + } + + @Test + public void reentrantComplete() { + TestScheduler sch = new TestScheduler(); + final PublishSubject<Integer> ps = PublishSubject.create(); + + TestObserver<Integer> to = new TestObserver<Integer>() { + @Override + public void onNext(Integer t) { + super.onNext(t); + if (t == 1) { + ps.onNext(2); + } + if (t == 2) { + ps.onComplete(); + } + } + }; + + ps.throttleLatest(1, TimeUnit.SECONDS, sch).subscribe(to); + + ps.onNext(1); + + sch.advanceTimeBy(1, TimeUnit.SECONDS); + + to.assertResult(1, 2); + } + + /** Emit 1, 2, 3, then advance time by a second; 1 and 3 should end up in downstream, 2 should be dropped. */ + @Test + public void onDroppedBasicNoEmitLast() { + PublishSubject<Integer> ps = PublishSubject.create(); + + TestScheduler sch = new TestScheduler(); + + TestObserver<Object> drops = new TestObserver<>(); + drops.onSubscribe(Disposable.empty()); + + TestObserver<Integer> to = ps.throttleLatest(1, TimeUnit.SECONDS, sch, false, drops::onNext) + .test(); + + to.assertEmpty(); + drops.assertEmpty(); + + ps.onNext(1); + + to.assertValuesOnly(1); + drops.assertEmpty(); + + ps.onNext(2); + + to.assertValuesOnly(1); + drops.assertEmpty(); + + ps.onNext(3); + + to.assertValuesOnly(1); + drops.assertValuesOnly(2); + + sch.advanceTimeBy(1, TimeUnit.SECONDS); + + to.assertValuesOnly(1, 3); + drops.assertValuesOnly(2); + + ps.onComplete(); + + to.assertResult(1, 3); + + drops.assertValuesOnly(2); + } + + /** Emit 1, 2, 3; 1 should end up in downstream, 2, 3 should be dropped. */ + @Test + public void onDroppedBasicNoEmitLastDropLast() { + PublishSubject<Integer> ps = PublishSubject.create(); + + TestScheduler sch = new TestScheduler(); + + TestObserver<Object> drops = new TestObserver<>(); + drops.onSubscribe(Disposable.empty()); + + TestObserver<Integer> to = ps.throttleLatest(1, TimeUnit.SECONDS, sch, false, drops::onNext) + .test(); + + to.assertEmpty(); + drops.assertEmpty(); + + ps.onNext(1); + + to.assertValuesOnly(1); + drops.assertEmpty(); + + ps.onNext(2); + + to.assertValuesOnly(1); + drops.assertEmpty(); + + ps.onNext(3); + + to.assertValuesOnly(1); + drops.assertValuesOnly(2); + + ps.onComplete(); + + to.assertResult(1); + + drops.assertValuesOnly(2, 3); + } + + /** Emit 1, 2, 3; 1 and 3 should end up in downstream, 2 should be dropped. */ + @Test + public void onDroppedBasicEmitLast() { + PublishSubject<Integer> ps = PublishSubject.create(); + + TestScheduler sch = new TestScheduler(); + + TestObserver<Object> drops = new TestObserver<>(); + drops.onSubscribe(Disposable.empty()); + + TestObserver<Integer> to = ps.throttleLatest(1, TimeUnit.SECONDS, sch, true, drops::onNext) + .test(); + + to.assertEmpty(); + drops.assertEmpty(); + + ps.onNext(1); + + to.assertValuesOnly(1); + drops.assertEmpty(); + + ps.onNext(2); + + to.assertValuesOnly(1); + drops.assertEmpty(); + + ps.onNext(3); + + to.assertValuesOnly(1); + drops.assertValuesOnly(2); + + ps.onComplete(); + + to.assertResult(1, 3); + + drops.assertValuesOnly(2); + } + + /** Emit 1, 2, 3; 3 should trigger an error to the downstream because 2 is dropped and the callback crashes. */ + @Test + public void onDroppedBasicNoEmitLastFirstDropCrash() throws Throwable { + PublishSubject<Integer> ps = PublishSubject.create(); + + TestScheduler sch = new TestScheduler(); + + Action whenDisposed = mock(Action.class); + + TestObserver<Integer> to = ps + .doOnDispose(whenDisposed) + .throttleLatest(1, TimeUnit.SECONDS, sch, false, d -> { + if (d == 2) { + throw new TestException("forced"); + } + }) + .test(); + + to.assertEmpty(); + + ps.onNext(1); + + to.assertValuesOnly(1); + + ps.onNext(2); + + to.assertValuesOnly(1); + + ps.onNext(3); + + to.assertFailure(TestException.class, 1); + + verify(whenDisposed).run(); + } + + /** + * Emit 1, 2, Error; the error should trigger the drop callback and crash it too, + * downstream gets 1, composite(source, drop-crash). + */ + @Test + public void onDroppedBasicNoEmitLastOnErrorDropCrash() throws Throwable { + PublishSubject<Integer> ps = PublishSubject.create(); + + TestScheduler sch = new TestScheduler(); + + Action whenDisposed = mock(Action.class); + + TestObserverEx<Integer> to = ps + .doOnDispose(whenDisposed) + .throttleLatest(1, TimeUnit.SECONDS, sch, false, d -> { throw new TestException("forced " + d); }) + .subscribeWith(new TestObserverEx<>()); + + to.assertEmpty(); + + ps.onNext(1); + + to.assertValuesOnly(1); + + ps.onNext(2); + + to.assertValuesOnly(1); + + ps.onError(new TestException("source")); + + to.assertFailure(CompositeException.class, 1); + + TestHelper.assertCompositeExceptions(to, TestException.class, "source", TestException.class, "forced 2"); + + verify(whenDisposed, never()).run(); + } + + /** + * Emit 1, 2, 3; 3 should trigger a drop-crash for 2, which then would trigger the error path and drop-crash for 3, + * the last item not delivered, downstream gets 1, composite(drop-crash 2, drop-crash 3). + */ + @Test + public void onDroppedBasicEmitLastOnErrorDropCrash() throws Throwable { + PublishSubject<Integer> ps = PublishSubject.create(); + + TestScheduler sch = new TestScheduler(); + + Action whenDisposed = mock(Action.class); + + TestObserverEx<Integer> to = ps + .doOnDispose(whenDisposed) + .throttleLatest(1, TimeUnit.SECONDS, sch, true, d -> { throw new TestException("forced " + d); }) + .subscribeWith(new TestObserverEx<>()); + + to.assertEmpty(); + + ps.onNext(1); + + to.assertValuesOnly(1); + + ps.onNext(2); + + to.assertValuesOnly(1); + + ps.onNext(3); + + to.assertFailure(CompositeException.class, 1); + + TestHelper.assertCompositeExceptions(to, TestException.class, "forced 2", TestException.class, "forced 3"); + + verify(whenDisposed).run(); + } + + /** Emit 1, complete; Downstream gets 1, complete, no drops. */ + @Test + public void onDroppedBasicNoEmitLastNoLastToDrop() { + PublishSubject<Integer> ps = PublishSubject.create(); + + TestScheduler sch = new TestScheduler(); + + TestObserver<Object> drops = new TestObserver<>(); + drops.onSubscribe(Disposable.empty()); + + TestObserver<Integer> to = ps.throttleLatest(1, TimeUnit.SECONDS, sch, false, drops::onNext) + .test(); + + to.assertEmpty(); + drops.assertEmpty(); + + ps.onNext(1); + + to.assertValuesOnly(1); + drops.assertEmpty(); + + ps.onComplete(); + + to.assertResult(1); + drops.assertEmpty(); + } + + /** Emit 1, error; Downstream gets 1, error, no drops. */ + @Test + public void onDroppedErrorNoEmitLastNoLastToDrop() { + PublishSubject<Integer> ps = PublishSubject.create(); + + TestScheduler sch = new TestScheduler(); + + TestObserver<Object> drops = new TestObserver<>(); + drops.onSubscribe(Disposable.empty()); + + TestObserver<Integer> to = ps.throttleLatest(1, TimeUnit.SECONDS, sch, false, drops::onNext) + .test(); + + to.assertEmpty(); + drops.assertEmpty(); + + ps.onNext(1); + + to.assertValuesOnly(1); + drops.assertEmpty(); + + ps.onError(new TestException()); + + to.assertFailure(TestException.class, 1); + drops.assertEmpty(); + } + + /** + * Emit 1, 2, complete; complete should crash drop, downstream gets 1, drop-crash 2. + */ + @Test + public void onDroppedHasLastNoEmitLastDropCrash() throws Throwable { + PublishSubject<Integer> ps = PublishSubject.create(); + + TestScheduler sch = new TestScheduler(); + + Action whenDisposed = mock(Action.class); + + TestObserverEx<Integer> to = ps + .doOnDispose(whenDisposed) + .throttleLatest(1, TimeUnit.SECONDS, sch, false, d -> { throw new TestException("forced " + d); }) + .subscribeWith(new TestObserverEx<>()); + + to.assertEmpty(); + + ps.onNext(1); + + to.assertValuesOnly(1); + + ps.onNext(2); + + to.assertValuesOnly(1); + + ps.onComplete(); + + to.assertFailureAndMessage(TestException.class, "forced 2", 1); + + verify(whenDisposed, never()).run(); + } + + /** + * Emit 1, 2 then dispose the sequence; downstream gets 1, drop should get for 2. + */ + @Test + public void onDroppedDisposeDrops() throws Throwable { + PublishSubject<Integer> ps = PublishSubject.create(); + + TestScheduler sch = new TestScheduler(); + + Action whenDisposed = mock(Action.class); + + TestObserver<Object> drops = new TestObserver<>(); + drops.onSubscribe(Disposable.empty()); + + TestObserverEx<Integer> to = ps + .doOnDispose(whenDisposed) + .throttleLatest(1, TimeUnit.SECONDS, sch, false, drops::onNext) + .subscribeWith(new TestObserverEx<>()); + + to.assertEmpty(); + + ps.onNext(1); + + to.assertValuesOnly(1); + + ps.onNext(2); + + to.assertValuesOnly(1); + + to.dispose(); + + to.assertValuesOnly(1); + drops.assertValuesOnly(2); + + verify(whenDisposed).run(); + } + + /** + * Emit 1 then dispose the sequence; downstream gets 1, drop should not get called. + */ + @Test + public void onDroppedDisposeNoDrops() throws Throwable { + PublishSubject<Integer> ps = PublishSubject.create(); + + TestScheduler sch = new TestScheduler(); + + Action whenDisposed = mock(Action.class); + + TestObserver<Object> drops = new TestObserver<>(); + drops.onSubscribe(Disposable.empty()); + + TestObserverEx<Integer> to = ps + .doOnDispose(whenDisposed) + .throttleLatest(1, TimeUnit.SECONDS, sch, false, drops::onNext) + .subscribeWith(new TestObserverEx<>()); + + to.assertEmpty(); + + ps.onNext(1); + + to.assertValuesOnly(1); + + to.dispose(); + + to.assertValuesOnly(1); + drops.assertEmpty(); + + verify(whenDisposed).run(); + } + + /** + * Emit 1, 2 then dispose the sequence; downstream gets 1, global error handler should get drop-crash 2. + */ + @Test + public void onDroppedDisposeCrashesDrop() throws Throwable { + TestHelper.withErrorTracking(errors -> { + PublishSubject<Integer> ps = PublishSubject.create(); + + TestScheduler sch = new TestScheduler(); + + Action whenDisposed = mock(Action.class); + + TestObserverEx<Integer> to = ps + .doOnDispose(whenDisposed) + .throttleLatest(1, TimeUnit.SECONDS, sch, false, d -> { throw new TestException("forced " + d); }) + .subscribeWith(new TestObserverEx<>()); + + to.assertEmpty(); + + ps.onNext(1); + + to.assertValuesOnly(1); + + ps.onNext(2); + + to.assertValuesOnly(1); + + to.dispose(); + + to.assertValuesOnly(1); + + verify(whenDisposed).run(); + + TestHelper.assertUndeliverable(errors, 0, TestException.class, "forced 2"); + }); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableTimeIntervalTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableTimeIntervalTest.java new file mode 100644 index 0000000000..ce418200d8 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableTimeIntervalTest.java @@ -0,0 +1,149 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import static org.mockito.Mockito.*; + +import java.util.concurrent.TimeUnit; + +import org.junit.*; +import org.mockito.InOrder; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.schedulers.*; +import io.reactivex.rxjava3.subjects.PublishSubject; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class ObservableTimeIntervalTest extends RxJavaTest { + + private static final TimeUnit TIME_UNIT = TimeUnit.MILLISECONDS; + + private Observer<Timed<Integer>> observer; + + private TestScheduler testScheduler; + private PublishSubject<Integer> subject; + private Observable<Timed<Integer>> observable; + + @Before + public void setUp() { + observer = TestHelper.mockObserver(); + testScheduler = new TestScheduler(); + subject = PublishSubject.create(); + observable = subject.timeInterval(testScheduler); + } + + @Test + public void timeInterval() { + InOrder inOrder = inOrder(observer); + observable.subscribe(observer); + + testScheduler.advanceTimeBy(1000, TIME_UNIT); + subject.onNext(1); + testScheduler.advanceTimeBy(2000, TIME_UNIT); + subject.onNext(2); + testScheduler.advanceTimeBy(3000, TIME_UNIT); + subject.onNext(3); + subject.onComplete(); + + inOrder.verify(observer, times(1)).onNext( + new Timed<>(1, 1000, TIME_UNIT)); + inOrder.verify(observer, times(1)).onNext( + new Timed<>(2, 2000, TIME_UNIT)); + inOrder.verify(observer, times(1)).onNext( + new Timed<>(3, 3000, TIME_UNIT)); + inOrder.verify(observer, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void timeIntervalDefault() { + final TestScheduler scheduler = new TestScheduler(); + + RxJavaPlugins.setComputationSchedulerHandler(new Function<Scheduler, Scheduler>() { + @Override + public Scheduler apply(Scheduler v) throws Exception { + return scheduler; + } + }); + + try { + Observable.range(1, 5) + .timeInterval() + .map(new Function<Timed<Integer>, Long>() { + @Override + public Long apply(Timed<Integer> v) throws Exception { + return v.time(); + } + }) + .test() + .assertResult(0L, 0L, 0L, 0L, 0L); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void timeIntervalDefaultSchedulerCustomUnit() { + final TestScheduler scheduler = new TestScheduler(); + + RxJavaPlugins.setComputationSchedulerHandler(new Function<Scheduler, Scheduler>() { + @Override + public Scheduler apply(Scheduler v) throws Exception { + return scheduler; + } + }); + + try { + Observable.range(1, 5) + .timeInterval(TimeUnit.SECONDS) + .map(new Function<Timed<Integer>, Long>() { + @Override + public Long apply(Timed<Integer> v) throws Exception { + return v.time(); + } + }) + .test() + .assertResult(0L, 0L, 0L, 0L, 0L); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void dispose() { + TestHelper.checkDisposed(Observable.just(1).timeInterval()); + } + + @Test + public void error() { + Observable.error(new TestException()) + .timeInterval() + .test() + .assertFailure(TestException.class); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeObservable(new Function<Observable<Object>, Observable<Timed<Object>>>() { + @Override + public Observable<Timed<Object>> apply(Observable<Object> f) + throws Exception { + return f.timeInterval(); + } + }); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableTimeoutTests.java b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableTimeoutTests.java new file mode 100644 index 0000000000..570eb3de5e --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableTimeoutTests.java @@ -0,0 +1,528 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import static io.reactivex.rxjava3.internal.util.ExceptionHelper.timeoutMessage; +import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import java.util.List; +import java.util.concurrent.*; + +import org.junit.*; +import org.mockito.InOrder; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.schedulers.TestScheduler; +import io.reactivex.rxjava3.subjects.PublishSubject; +import io.reactivex.rxjava3.testsupport.*; + +public class ObservableTimeoutTests extends RxJavaTest { + private PublishSubject<String> underlyingSubject; + private TestScheduler testScheduler; + private Observable<String> withTimeout; + private static final long TIMEOUT = 3; + private static final TimeUnit TIME_UNIT = TimeUnit.SECONDS; + + @Before + public void setUp() { + + underlyingSubject = PublishSubject.create(); + testScheduler = new TestScheduler(); + withTimeout = underlyingSubject.timeout(TIMEOUT, TIME_UNIT, testScheduler); + } + + @Test + public void shouldNotTimeoutIfOnNextWithinTimeout() { + Observer<String> observer = TestHelper.mockObserver(); + TestObserver<String> to = new TestObserver<>(observer); + + withTimeout.subscribe(to); + + testScheduler.advanceTimeBy(2, TimeUnit.SECONDS); + underlyingSubject.onNext("One"); + verify(observer).onNext("One"); + testScheduler.advanceTimeBy(2, TimeUnit.SECONDS); + verify(observer, never()).onError(any(Throwable.class)); + to.dispose(); + } + + @Test + public void shouldNotTimeoutIfSecondOnNextWithinTimeout() { + Observer<String> observer = TestHelper.mockObserver(); + TestObserver<String> to = new TestObserver<>(observer); + + withTimeout.subscribe(to); + + testScheduler.advanceTimeBy(2, TimeUnit.SECONDS); + underlyingSubject.onNext("One"); + testScheduler.advanceTimeBy(2, TimeUnit.SECONDS); + underlyingSubject.onNext("Two"); + verify(observer).onNext("Two"); + testScheduler.advanceTimeBy(2, TimeUnit.SECONDS); + verify(observer, never()).onError(any(Throwable.class)); + to.dispose(); + } + + @Test + public void shouldTimeoutIfOnNextNotWithinTimeout() { + TestObserverEx<String> observer = new TestObserverEx<>(); + + withTimeout.subscribe(observer); + + testScheduler.advanceTimeBy(TIMEOUT + 1, TimeUnit.SECONDS); + observer.assertFailureAndMessage(TimeoutException.class, timeoutMessage(TIMEOUT, TIME_UNIT)); + } + + @Test + public void shouldTimeoutIfSecondOnNextNotWithinTimeout() { + TestObserverEx<String> observer = new TestObserverEx<>(); + withTimeout.subscribe(observer); + testScheduler.advanceTimeBy(2, TimeUnit.SECONDS); + underlyingSubject.onNext("One"); + observer.assertValue("One"); + testScheduler.advanceTimeBy(TIMEOUT + 1, TimeUnit.SECONDS); + observer.assertFailureAndMessage(TimeoutException.class, timeoutMessage(TIMEOUT, TIME_UNIT), "One"); + } + + @Test + public void shouldCompleteIfUnderlyingComletes() { + Observer<String> observer = TestHelper.mockObserver(); + TestObserver<String> to = new TestObserver<>(observer); + withTimeout.subscribe(observer); + testScheduler.advanceTimeBy(2, TimeUnit.SECONDS); + underlyingSubject.onComplete(); + testScheduler.advanceTimeBy(2, TimeUnit.SECONDS); + verify(observer).onComplete(); + verify(observer, never()).onError(any(Throwable.class)); + to.dispose(); + } + + @Test + public void shouldErrorIfUnderlyingErrors() { + Observer<String> observer = TestHelper.mockObserver(); + TestObserver<String> to = new TestObserver<>(observer); + withTimeout.subscribe(observer); + testScheduler.advanceTimeBy(2, TimeUnit.SECONDS); + underlyingSubject.onError(new UnsupportedOperationException()); + testScheduler.advanceTimeBy(2, TimeUnit.SECONDS); + verify(observer).onError(any(UnsupportedOperationException.class)); + to.dispose(); + } + + @Test + public void shouldSwitchToOtherIfOnNextNotWithinTimeout() { + Observable<String> other = Observable.just("a", "b", "c"); + Observable<String> source = underlyingSubject.timeout(TIMEOUT, TIME_UNIT, testScheduler, other); + + Observer<String> observer = TestHelper.mockObserver(); + TestObserver<String> to = new TestObserver<>(observer); + source.subscribe(to); + + testScheduler.advanceTimeBy(2, TimeUnit.SECONDS); + underlyingSubject.onNext("One"); + testScheduler.advanceTimeBy(4, TimeUnit.SECONDS); + underlyingSubject.onNext("Two"); + InOrder inOrder = inOrder(observer); + inOrder.verify(observer, times(1)).onNext("One"); + inOrder.verify(observer, times(1)).onNext("a"); + inOrder.verify(observer, times(1)).onNext("b"); + inOrder.verify(observer, times(1)).onNext("c"); + inOrder.verify(observer, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + to.dispose(); + } + + @Test + public void shouldSwitchToOtherIfOnErrorNotWithinTimeout() { + Observable<String> other = Observable.just("a", "b", "c"); + Observable<String> source = underlyingSubject.timeout(TIMEOUT, TIME_UNIT, testScheduler, other); + + Observer<String> observer = TestHelper.mockObserver(); + TestObserver<String> to = new TestObserver<>(observer); + source.subscribe(to); + + testScheduler.advanceTimeBy(2, TimeUnit.SECONDS); + underlyingSubject.onNext("One"); + testScheduler.advanceTimeBy(4, TimeUnit.SECONDS); + underlyingSubject.onError(new UnsupportedOperationException()); + InOrder inOrder = inOrder(observer); + inOrder.verify(observer, times(1)).onNext("One"); + inOrder.verify(observer, times(1)).onNext("a"); + inOrder.verify(observer, times(1)).onNext("b"); + inOrder.verify(observer, times(1)).onNext("c"); + inOrder.verify(observer, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + to.dispose(); + } + + @Test + public void shouldSwitchToOtherIfOnCompletedNotWithinTimeout() { + Observable<String> other = Observable.just("a", "b", "c"); + Observable<String> source = underlyingSubject.timeout(TIMEOUT, TIME_UNIT, testScheduler, other); + + Observer<String> observer = TestHelper.mockObserver(); + TestObserver<String> to = new TestObserver<>(observer); + source.subscribe(to); + + testScheduler.advanceTimeBy(2, TimeUnit.SECONDS); + underlyingSubject.onNext("One"); + testScheduler.advanceTimeBy(4, TimeUnit.SECONDS); + underlyingSubject.onComplete(); + InOrder inOrder = inOrder(observer); + inOrder.verify(observer, times(1)).onNext("One"); + inOrder.verify(observer, times(1)).onNext("a"); + inOrder.verify(observer, times(1)).onNext("b"); + inOrder.verify(observer, times(1)).onNext("c"); + inOrder.verify(observer, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + to.dispose(); + } + + @Test + public void shouldSwitchToOtherAndCanBeUnsubscribedIfOnNextNotWithinTimeout() { + PublishSubject<String> other = PublishSubject.create(); + Observable<String> source = underlyingSubject.timeout(TIMEOUT, TIME_UNIT, testScheduler, other); + + Observer<String> observer = TestHelper.mockObserver(); + TestObserver<String> to = new TestObserver<>(observer); + source.subscribe(to); + + testScheduler.advanceTimeBy(2, TimeUnit.SECONDS); + underlyingSubject.onNext("One"); + testScheduler.advanceTimeBy(4, TimeUnit.SECONDS); + underlyingSubject.onNext("Two"); + + other.onNext("a"); + other.onNext("b"); + to.dispose(); + + // The following messages should not be delivered. + other.onNext("c"); + other.onNext("d"); + other.onComplete(); + + InOrder inOrder = inOrder(observer); + inOrder.verify(observer, times(1)).onNext("One"); + inOrder.verify(observer, times(1)).onNext("a"); + inOrder.verify(observer, times(1)).onNext("b"); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void shouldTimeoutIfSynchronizedObservableEmitFirstOnNextNotWithinTimeout() + throws InterruptedException { + final CountDownLatch exit = new CountDownLatch(1); + final CountDownLatch timeoutSetuped = new CountDownLatch(1); + + final TestObserverEx<String> observer = new TestObserverEx<>(); + + new Thread(new Runnable() { + + @Override + public void run() { + Observable.unsafeCreate(new ObservableSource<String>() { + + @Override + public void subscribe(Observer<? super String> observer) { + observer.onSubscribe(Disposable.empty()); + try { + timeoutSetuped.countDown(); + exit.await(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + observer.onNext("a"); + observer.onComplete(); + } + + }).timeout(1, TimeUnit.SECONDS, testScheduler) + .subscribe(observer); + } + }).start(); + + timeoutSetuped.await(); + testScheduler.advanceTimeBy(2, TimeUnit.SECONDS); + + observer.assertFailureAndMessage(TimeoutException.class, timeoutMessage(1, TimeUnit.SECONDS)); + + exit.countDown(); // exit the thread + } + + @Test + public void shouldUnsubscribeFromUnderlyingSubscriptionOnTimeout() throws InterruptedException { + // From https://github.com/ReactiveX/RxJava/pull/951 + final Disposable upstream = mock(Disposable.class); + + Observable<String> never = Observable.unsafeCreate(new ObservableSource<String>() { + @Override + public void subscribe(Observer<? super String> observer) { + observer.onSubscribe(upstream); + } + }); + + TestScheduler testScheduler = new TestScheduler(); + Observable<String> observableWithTimeout = never.timeout(1000, TimeUnit.MILLISECONDS, testScheduler); + + TestObserverEx<String> observer = new TestObserverEx<>(); + observableWithTimeout.subscribe(observer); + + testScheduler.advanceTimeBy(2000, TimeUnit.MILLISECONDS); + + observer.assertFailureAndMessage(TimeoutException.class, timeoutMessage(1000, TimeUnit.MILLISECONDS)); + + verify(upstream, times(1)).dispose(); + } + + @Test + public void shouldUnsubscribeFromUnderlyingSubscriptionOnDispose() { + final PublishSubject<String> subject = PublishSubject.create(); + final TestScheduler scheduler = new TestScheduler(); + + final TestObserver<String> observer = subject + .timeout(100, TimeUnit.MILLISECONDS, scheduler) + .test(); + + assertTrue(subject.hasObservers()); + + observer.dispose(); + + assertFalse(subject.hasObservers()); + } + + @Test + public void timedAndOther() { + Observable.never().timeout(100, TimeUnit.MILLISECONDS, Observable.just(1)) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(1); + } + + @Test + public void disposed() { + TestHelper.checkDisposed(PublishSubject.create().timeout(1, TimeUnit.DAYS)); + + TestHelper.checkDisposed(PublishSubject.create().timeout(1, TimeUnit.DAYS, Observable.just(1))); + } + + @Test + public void timedErrorOther() { + Observable.error(new TestException()) + .timeout(1, TimeUnit.DAYS, Observable.just(1)) + .test() + .assertFailure(TestException.class); + } + + @Test + public void timedError() { + Observable.error(new TestException()) + .timeout(1, TimeUnit.DAYS) + .test() + .assertFailure(TestException.class); + } + + @Test + public void timedEmptyOther() { + Observable.empty() + .timeout(1, TimeUnit.DAYS, Observable.just(1)) + .test() + .assertResult(); + } + + @Test + public void timedEmpty() { + Observable.empty() + .timeout(1, TimeUnit.DAYS) + .test() + .assertResult(); + } + + @Test + public void badSource() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + new Observable<Integer>() { + @Override + protected void subscribeActual(Observer<? super Integer> observer) { + observer.onSubscribe(Disposable.empty()); + + observer.onNext(1); + observer.onComplete(); + observer.onNext(2); + observer.onError(new TestException()); + observer.onComplete(); + } + } + .timeout(1, TimeUnit.DAYS) + .test() + .assertResult(1); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void badSourceOther() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + new Observable<Integer>() { + @Override + protected void subscribeActual(Observer<? super Integer> observer) { + observer.onSubscribe(Disposable.empty()); + + observer.onNext(1); + observer.onComplete(); + observer.onNext(2); + observer.onError(new TestException()); + observer.onComplete(); + } + } + .timeout(1, TimeUnit.DAYS, Observable.just(3)) + .test() + .assertResult(1); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void timedTake() { + PublishSubject<Integer> ps = PublishSubject.create(); + + TestObserver<Integer> to = ps.timeout(1, TimeUnit.DAYS) + .take(1) + .test(); + + assertTrue(ps.hasObservers()); + + ps.onNext(1); + + assertFalse(ps.hasObservers()); + + to.assertResult(1); + } + + @Test + public void timedFallbackTake() { + PublishSubject<Integer> ps = PublishSubject.create(); + + TestObserver<Integer> to = ps.timeout(1, TimeUnit.DAYS, Observable.just(2)) + .take(1) + .test(); + + assertTrue(ps.hasObservers()); + + ps.onNext(1); + + assertFalse(ps.hasObservers()); + + to.assertResult(1); + } + + @Test + public void fallbackErrors() { + Observable.never() + .timeout(1, TimeUnit.MILLISECONDS, Observable.error(new TestException())) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertFailure(TestException.class); + } + + @Test + public void onNextOnTimeoutRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final TestScheduler sch = new TestScheduler(); + + final PublishSubject<Integer> ps = PublishSubject.create(); + + TestObserverEx<Integer> to = ps.timeout(1, TimeUnit.SECONDS, sch).to(TestHelper.<Integer>testConsumer()); + + Runnable r1 = new Runnable() { + @Override + public void run() { + ps.onNext(1); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + sch.advanceTimeBy(1, TimeUnit.SECONDS); + } + }; + + TestHelper.race(r1, r2); + + if (to.values().size() != 0) { + if (to.errors().size() != 0) { + to.assertFailure(TimeoutException.class, 1); + to.assertErrorMessage(timeoutMessage(1, TimeUnit.SECONDS)); + } else { + to.assertValuesOnly(1); + } + } else { + to.assertFailure(TimeoutException.class); + to.assertErrorMessage(timeoutMessage(1, TimeUnit.SECONDS)); + } + } + } + + @Test + public void onNextOnTimeoutRaceFallback() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final TestScheduler sch = new TestScheduler(); + + final PublishSubject<Integer> ps = PublishSubject.create(); + + TestObserverEx<Integer> to = ps.timeout(1, TimeUnit.SECONDS, sch, Observable.just(2)).to(TestHelper.<Integer>testConsumer()); + + Runnable r1 = new Runnable() { + @Override + public void run() { + ps.onNext(1); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + sch.advanceTimeBy(1, TimeUnit.SECONDS); + } + }; + + TestHelper.race(r1, r2); + + if (to.isTerminated()) { + int c = to.values().size(); + if (c == 1) { + int v = to.values().get(0); + assertTrue("" + v, v == 1 || v == 2); + } else { + to.assertResult(1, 2); + } + } else { + to.assertValuesOnly(1); + } + } + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableTimeoutWithSelectorTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableTimeoutWithSelectorTest.java new file mode 100644 index 0000000000..2c6710cc89 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableTimeoutWithSelectorTest.java @@ -0,0 +1,862 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +import java.util.*; +import java.util.concurrent.*; +import java.util.concurrent.atomic.*; + +import org.junit.Test; +import org.mockito.InOrder; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.core.Observer; +import io.reactivex.rxjava3.disposables.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.schedulers.Schedulers; +import io.reactivex.rxjava3.subjects.PublishSubject; +import io.reactivex.rxjava3.testsupport.*; + +public class ObservableTimeoutWithSelectorTest extends RxJavaTest { + + @Test + public void timeoutSelectorNormal1() { + PublishSubject<Integer> source = PublishSubject.create(); + final PublishSubject<Integer> timeout = PublishSubject.create(); + + Function<Integer, Observable<Integer>> timeoutFunc = new Function<Integer, Observable<Integer>>() { + @Override + public Observable<Integer> apply(Integer t1) { + return timeout; + } + }; + + Observable<Integer> other = Observable.fromIterable(Arrays.asList(100)); + + Observer<Object> o = TestHelper.mockObserver(); + InOrder inOrder = inOrder(o); + + source.timeout(timeout, timeoutFunc, other).subscribe(o); + + source.onNext(1); + source.onNext(2); + source.onNext(3); + timeout.onNext(1); + + inOrder.verify(o).onNext(1); + inOrder.verify(o).onNext(2); + inOrder.verify(o).onNext(3); + inOrder.verify(o).onNext(100); + inOrder.verify(o).onComplete(); + verify(o, never()).onError(any(Throwable.class)); + + } + + @Test + public void timeoutSelectorTimeoutFirst() throws InterruptedException { + Observable<Integer> source = Observable.<Integer>never(); + final PublishSubject<Integer> timeout = PublishSubject.create(); + + Function<Integer, Observable<Integer>> timeoutFunc = new Function<Integer, Observable<Integer>>() { + @Override + public Observable<Integer> apply(Integer t1) { + return timeout; + } + }; + + Observable<Integer> other = Observable.fromIterable(Arrays.asList(100)); + + Observer<Object> o = TestHelper.mockObserver(); + InOrder inOrder = inOrder(o); + + source.timeout(timeout, timeoutFunc, other).subscribe(o); + + timeout.onNext(1); + + inOrder.verify(o).onNext(100); + inOrder.verify(o).onComplete(); + verify(o, never()).onError(any(Throwable.class)); + + } + + @Test + public void timeoutSelectorFirstThrows() { + Observable<Integer> source = Observable.<Integer>never(); + final PublishSubject<Integer> timeout = PublishSubject.create(); + + Function<Integer, Observable<Integer>> timeoutFunc = new Function<Integer, Observable<Integer>>() { + @Override + public Observable<Integer> apply(Integer t1) { + return timeout; + } + }; + + Supplier<Observable<Integer>> firstTimeoutFunc = new Supplier<Observable<Integer>>() { + @Override + public Observable<Integer> get() { + throw new TestException(); + } + }; + + Observable<Integer> other = Observable.fromIterable(Arrays.asList(100)); + + Observer<Object> o = TestHelper.mockObserver(); + + source.timeout(Observable.defer(firstTimeoutFunc), timeoutFunc, other).subscribe(o); + + verify(o).onError(any(TestException.class)); + verify(o, never()).onNext(any()); + verify(o, never()).onComplete(); + + } + + @Test + public void timeoutSelectorSubsequentThrows() { + PublishSubject<Integer> source = PublishSubject.create(); + final PublishSubject<Integer> timeout = PublishSubject.create(); + + Function<Integer, Observable<Integer>> timeoutFunc = new Function<Integer, Observable<Integer>>() { + @Override + public Observable<Integer> apply(Integer t1) { + throw new TestException(); + } + }; + + Observable<Integer> other = Observable.fromIterable(Arrays.asList(100)); + + Observer<Object> o = TestHelper.mockObserver(); + InOrder inOrder = inOrder(o); + + source.timeout(timeout, timeoutFunc, other).subscribe(o); + + source.onNext(1); + + inOrder.verify(o).onNext(1); + inOrder.verify(o).onError(any(TestException.class)); + verify(o, never()).onComplete(); + + } + + @Test + public void timeoutSelectorFirstObservableThrows() { + PublishSubject<Integer> source = PublishSubject.create(); + final PublishSubject<Integer> timeout = PublishSubject.create(); + + Function<Integer, Observable<Integer>> timeoutFunc = new Function<Integer, Observable<Integer>>() { + @Override + public Observable<Integer> apply(Integer t1) { + return timeout; + } + }; + + Observable<Integer> other = Observable.fromIterable(Arrays.asList(100)); + + Observer<Object> o = TestHelper.mockObserver(); + + source.timeout(Observable.<Integer> error(new TestException()), timeoutFunc, other).subscribe(o); + + verify(o).onError(any(TestException.class)); + verify(o, never()).onNext(any()); + verify(o, never()).onComplete(); + + } + + @Test + public void timeoutSelectorSubsequentObservableThrows() { + PublishSubject<Integer> source = PublishSubject.create(); + final PublishSubject<Integer> timeout = PublishSubject.create(); + + Function<Integer, Observable<Integer>> timeoutFunc = new Function<Integer, Observable<Integer>>() { + @Override + public Observable<Integer> apply(Integer t1) { + return Observable.<Integer> error(new TestException()); + } + }; + + Observable<Integer> other = Observable.fromIterable(Arrays.asList(100)); + + Observer<Object> o = TestHelper.mockObserver(); + InOrder inOrder = inOrder(o); + + source.timeout(timeout, timeoutFunc, other).subscribe(o); + + source.onNext(1); + + inOrder.verify(o).onNext(1); + inOrder.verify(o).onError(any(TestException.class)); + verify(o, never()).onComplete(); + + } + + @Test + public void timeoutSelectorWithFirstTimeoutFirstAndNoOtherObservable() { + PublishSubject<Integer> source = PublishSubject.create(); + final PublishSubject<Integer> timeout = PublishSubject.create(); + + Function<Integer, Observable<Integer>> timeoutFunc = new Function<Integer, Observable<Integer>>() { + @Override + public Observable<Integer> apply(Integer t1) { + return PublishSubject.create(); + } + }; + + Observer<Object> o = TestHelper.mockObserver(); + source.timeout(timeout, timeoutFunc).subscribe(o); + + timeout.onNext(1); + + InOrder inOrder = inOrder(o); + inOrder.verify(o).onError(isA(TimeoutException.class)); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void timeoutSelectorWithTimeoutFirstAndNoOtherObservable() { + PublishSubject<Integer> source = PublishSubject.create(); + final PublishSubject<Integer> timeout = PublishSubject.create(); + + Function<Integer, Observable<Integer>> timeoutFunc = new Function<Integer, Observable<Integer>>() { + @Override + public Observable<Integer> apply(Integer t1) { + return timeout; + } + }; + + Observer<Object> o = TestHelper.mockObserver(); + source.timeout(PublishSubject.create(), timeoutFunc).subscribe(o); + source.onNext(1); + + timeout.onNext(1); + + InOrder inOrder = inOrder(o); + inOrder.verify(o).onNext(1); + inOrder.verify(o).onError(isA(TimeoutException.class)); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void timeoutSelectorWithTimeoutAndOnNextRaceCondition() throws InterruptedException { + // Thread 1 Thread 2 + // + // observer.onNext(1) + // start timeout + // unsubscribe timeout in thread 2 start to do some long-time work in "unsubscribe" + // observer.onNext(2) + // timeout.onNext(1) + // "unsubscribe" done + // + // + // In the above case, the timeout operator should ignore "timeout.onNext(1)" + // since "observer" has already seen 2. + final CountDownLatch observerReceivedTwo = new CountDownLatch(1); + final CountDownLatch timeoutEmittedOne = new CountDownLatch(1); + final CountDownLatch observerCompleted = new CountDownLatch(1); + final CountDownLatch enteredTimeoutOne = new CountDownLatch(1); + final AtomicBoolean latchTimeout = new AtomicBoolean(false); + + final Function<Integer, Observable<Integer>> timeoutFunc = new Function<Integer, Observable<Integer>>() { + @Override + public Observable<Integer> apply(Integer t1) { + if (t1 == 1) { + // Force "unsubscribe" run on another thread + return Observable.unsafeCreate(new ObservableSource<Integer>() { + @Override + public void subscribe(Observer<? super Integer> observer) { + observer.onSubscribe(Disposable.empty()); + enteredTimeoutOne.countDown(); + // force the timeout message be sent after observer.onNext(2) + while (true) { + try { + if (!observerReceivedTwo.await(30, TimeUnit.SECONDS)) { + // CountDownLatch timeout + // There should be something wrong + latchTimeout.set(true); + } + break; + } catch (InterruptedException e) { + // Since we just want to emulate a busy method, + // we ignore the interrupt signal from Scheduler. + } + } + observer.onNext(1); + timeoutEmittedOne.countDown(); + } + }).subscribeOn(Schedulers.newThread()); + } else { + return PublishSubject.create(); + } + } + }; + + final Observer<Integer> o = TestHelper.mockObserver(); + doAnswer(new Answer<Void>() { + + @Override + public Void answer(InvocationOnMock invocation) throws Throwable { + observerReceivedTwo.countDown(); + return null; + } + + }).when(o).onNext(2); + doAnswer(new Answer<Void>() { + + @Override + public Void answer(InvocationOnMock invocation) throws Throwable { + observerCompleted.countDown(); + return null; + } + + }).when(o).onComplete(); + + final TestObserver<Integer> to = new TestObserver<>(o); + + new Thread(new Runnable() { + + @Override + public void run() { + PublishSubject<Integer> source = PublishSubject.create(); + source.timeout(timeoutFunc, Observable.just(3)).subscribe(to); + source.onNext(1); // start timeout + try { + if (!enteredTimeoutOne.await(30, TimeUnit.SECONDS)) { + latchTimeout.set(true); + } + } catch (InterruptedException e) { + e.printStackTrace(); + } + source.onNext(2); // disable timeout + try { + if (!timeoutEmittedOne.await(30, TimeUnit.SECONDS)) { + latchTimeout.set(true); + } + } catch (InterruptedException e) { + e.printStackTrace(); + } + source.onComplete(); + } + + }).start(); + + if (!observerCompleted.await(30, TimeUnit.SECONDS)) { + latchTimeout.set(true); + } + + assertFalse("CoundDownLatch timeout", latchTimeout.get()); + + InOrder inOrder = inOrder(o); + inOrder.verify(o).onSubscribe((Disposable)notNull()); + inOrder.verify(o).onNext(1); + inOrder.verify(o).onNext(2); + inOrder.verify(o, never()).onNext(3); + inOrder.verify(o).onComplete(); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void dispose() { + TestHelper.checkDisposed(PublishSubject.create().timeout(Functions.justFunction(Observable.never()))); + + TestHelper.checkDisposed(PublishSubject.create().timeout(Functions.justFunction(Observable.never()), Observable.never())); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeObservable(new Function<Observable<Object>, ObservableSource<Object>>() { + @Override + public ObservableSource<Object> apply(Observable<Object> o) throws Exception { + return o.timeout(Functions.justFunction(Observable.never())); + } + }); + + TestHelper.checkDoubleOnSubscribeObservable(new Function<Observable<Object>, ObservableSource<Object>>() { + @Override + public ObservableSource<Object> apply(Observable<Object> o) throws Exception { + return o.timeout(Functions.justFunction(Observable.never()), Observable.never()); + } + }); + } + + @Test + public void empty() { + Observable.empty() + .timeout(Functions.justFunction(Observable.never())) + .test() + .assertResult(); + } + + @Test + public void error() { + Observable.error(new TestException()) + .timeout(Functions.justFunction(Observable.never())) + .test() + .assertFailure(TestException.class); + } + + @Test + public void emptyInner() { + PublishSubject<Integer> ps = PublishSubject.create(); + + TestObserver<Integer> to = ps + .timeout(Functions.justFunction(Observable.empty())) + .test(); + + ps.onNext(1); + + to.assertFailure(TimeoutException.class, 1); + } + + @Test + public void badInnerSource() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + PublishSubject<Integer> ps = PublishSubject.create(); + + TestObserverEx<Integer> to = ps + .timeout(Functions.justFunction(new Observable<Integer>() { + @Override + protected void subscribeActual(Observer<? super Integer> observer) { + observer.onSubscribe(Disposable.empty()); + observer.onError(new TestException("First")); + observer.onNext(2); + observer.onError(new TestException("Second")); + observer.onComplete(); + } + })) + .to(TestHelper.<Integer>testConsumer()); + + ps.onNext(1); + + to.assertFailureAndMessage(TestException.class, "First", 1); + + TestHelper.assertUndeliverable(errors, 0, TestException.class, "Second"); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void badInnerSourceOther() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + PublishSubject<Integer> ps = PublishSubject.create(); + + TestObserverEx<Integer> to = ps + .timeout(Functions.justFunction(new Observable<Integer>() { + @Override + protected void subscribeActual(Observer<? super Integer> observer) { + observer.onSubscribe(Disposable.empty()); + observer.onError(new TestException("First")); + observer.onNext(2); + observer.onError(new TestException("Second")); + observer.onComplete(); + } + }), Observable.just(2)) + .to(TestHelper.<Integer>testConsumer()); + + ps.onNext(1); + + to.assertFailureAndMessage(TestException.class, "First", 1); + + TestHelper.assertUndeliverable(errors, 0, TestException.class, "Second"); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void withOtherMainError() { + Observable.error(new TestException()) + .timeout(Functions.justFunction(Observable.never()), Observable.never()) + .test() + .assertFailure(TestException.class); + } + + @Test + @SuppressUndeliverable + public void badSourceTimeout() { + new Observable<Integer>() { + @Override + protected void subscribeActual(Observer<? super Integer> observer) { + observer.onSubscribe(Disposable.empty()); + observer.onNext(1); + observer.onNext(2); + observer.onError(new TestException("First")); + observer.onNext(3); + observer.onComplete(); + observer.onError(new TestException("Second")); + } + } + .timeout(Functions.justFunction(Observable.never()), Observable.<Integer>never()) + .take(1) + .test() + .assertResult(1); + } + + @Test + public void selectorTake() { + PublishSubject<Integer> ps = PublishSubject.create(); + + TestObserver<Integer> to = ps + .timeout(Functions.justFunction(Observable.never())) + .take(1) + .test(); + + assertTrue(ps.hasObservers()); + + ps.onNext(1); + + assertFalse(ps.hasObservers()); + + to.assertResult(1); + } + + @Test + public void selectorFallbackTake() { + PublishSubject<Integer> ps = PublishSubject.create(); + + TestObserver<Integer> to = ps + .timeout(Functions.justFunction(Observable.never()), Observable.just(2)) + .take(1) + .test(); + + assertTrue(ps.hasObservers()); + + ps.onNext(1); + + assertFalse(ps.hasObservers()); + + to.assertResult(1); + } + + @Test + public void lateOnTimeoutError() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + final PublishSubject<Integer> ps = PublishSubject.create(); + + final Observer<?>[] sub = { null, null }; + + final Observable<Integer> pp2 = new Observable<Integer>() { + + int count; + + @Override + protected void subscribeActual( + Observer<? super Integer> observer) { + observer.onSubscribe(Disposable.empty()); + sub[count++] = observer; + } + }; + + TestObserver<Integer> to = ps.timeout(Functions.justFunction(pp2)).test(); + + ps.onNext(0); + + Runnable r1 = new Runnable() { + @Override + public void run() { + ps.onNext(1); + } + }; + + final Throwable ex = new TestException(); + + Runnable r2 = new Runnable() { + @Override + public void run() { + sub[0].onError(ex); + } + }; + + TestHelper.race(r1, r2); + + to.assertValueAt(0, 0); + + if (!errors.isEmpty()) { + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } + } finally { + RxJavaPlugins.reset(); + } + } + } + + @Test + public void lateOnTimeoutFallbackRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + final PublishSubject<Integer> ps = PublishSubject.create(); + + final Observer<?>[] sub = { null, null }; + + final Observable<Integer> pp2 = new Observable<Integer>() { + + int count; + + @Override + protected void subscribeActual( + Observer<? super Integer> observer) { + assertFalse(((Disposable)observer).isDisposed()); + observer.onSubscribe(Disposable.empty()); + sub[count++] = observer; + } + }; + + TestObserver<Integer> to = ps.timeout(Functions.justFunction(pp2), Observable.<Integer>never()).test(); + + ps.onNext(0); + + Runnable r1 = new Runnable() { + @Override + public void run() { + ps.onNext(1); + } + }; + + final Throwable ex = new TestException(); + + Runnable r2 = new Runnable() { + @Override + public void run() { + sub[0].onError(ex); + } + }; + + TestHelper.race(r1, r2); + + to.assertValueAt(0, 0); + + if (!errors.isEmpty()) { + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } + } finally { + RxJavaPlugins.reset(); + } + } + } + + @Test + public void onErrorOnTimeoutRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + final PublishSubject<Integer> ps = PublishSubject.create(); + + final Observer<?>[] sub = { null, null }; + + final Observable<Integer> pp2 = new Observable<Integer>() { + + int count; + + @Override + protected void subscribeActual( + Observer<? super Integer> observer) { + assertFalse(((Disposable)observer).isDisposed()); + observer.onSubscribe(Disposable.empty()); + sub[count++] = observer; + } + }; + + TestObserver<Integer> to = ps.timeout(Functions.justFunction(pp2)).test(); + + ps.onNext(0); + + final Throwable ex = new TestException(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + ps.onError(ex); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + sub[0].onComplete(); + } + }; + + TestHelper.race(r1, r2); + + to.assertValueAt(0, 0); + + if (!errors.isEmpty()) { + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } + } finally { + RxJavaPlugins.reset(); + } + } + } + + @Test + public void onCompleteOnTimeoutRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + final PublishSubject<Integer> ps = PublishSubject.create(); + + final Observer<?>[] sub = { null, null }; + + final Observable<Integer> pp2 = new Observable<Integer>() { + + int count; + + @Override + protected void subscribeActual( + Observer<? super Integer> observer) { + assertFalse(((Disposable)observer).isDisposed()); + observer.onSubscribe(Disposable.empty()); + sub[count++] = observer; + } + }; + + TestObserver<Integer> to = ps.timeout(Functions.justFunction(pp2)).test(); + + ps.onNext(0); + + Runnable r1 = new Runnable() { + @Override + public void run() { + ps.onComplete(); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + sub[0].onComplete(); + } + }; + + TestHelper.race(r1, r2); + + to.assertValueAt(0, 0); + + if (!errors.isEmpty()) { + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } + } finally { + RxJavaPlugins.reset(); + } + } + } + + @Test + public void onCompleteOnTimeoutRaceFallback() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + final PublishSubject<Integer> ps = PublishSubject.create(); + + final Observer<?>[] sub = { null, null }; + + final Observable<Integer> pp2 = new Observable<Integer>() { + + int count; + + @Override + protected void subscribeActual( + Observer<? super Integer> observer) { + assertFalse(((Disposable)observer).isDisposed()); + observer.onSubscribe(Disposable.empty()); + sub[count++] = observer; + } + }; + + TestObserver<Integer> to = ps.timeout(Functions.justFunction(pp2), Observable.<Integer>never()).test(); + + ps.onNext(0); + + Runnable r1 = new Runnable() { + @Override + public void run() { + ps.onComplete(); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + sub[0].onComplete(); + } + }; + + TestHelper.race(r1, r2); + + to.assertValueAt(0, 0); + + if (!errors.isEmpty()) { + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } + } finally { + RxJavaPlugins.reset(); + } + } + } + + @Test + public void disposedUpfront() { + PublishSubject<Integer> ps = PublishSubject.create(); + final AtomicInteger counter = new AtomicInteger(); + + Observable<Object> timeoutAndFallback = Observable.never().doOnSubscribe(new Consumer<Disposable>() { + @Override + public void accept(Disposable d) throws Exception { + counter.incrementAndGet(); + } + }); + + ps + .timeout(timeoutAndFallback, Functions.justFunction(timeoutAndFallback)) + .test(true) + .assertEmpty(); + + assertEquals(0, counter.get()); + } + + @Test + public void disposedUpfrontFallback() { + PublishSubject<Object> ps = PublishSubject.create(); + final AtomicInteger counter = new AtomicInteger(); + + Observable<Object> timeoutAndFallback = Observable.never().doOnSubscribe(new Consumer<Disposable>() { + @Override + public void accept(Disposable d) throws Exception { + counter.incrementAndGet(); + } + }); + + ps + .timeout(timeoutAndFallback, Functions.justFunction(timeoutAndFallback), timeoutAndFallback) + .test(true) + .assertEmpty(); + + assertEquals(0, counter.get()); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableTimerTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableTimerTest.java new file mode 100644 index 0000000000..9229d24fe3 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableTimerTest.java @@ -0,0 +1,361 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +import java.util.List; +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicBoolean; + +import io.reactivex.rxjava3.disposables.Disposable; +import org.junit.*; +import org.mockito.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.operators.observable.ObservableTimer.TimerObserver; +import io.reactivex.rxjava3.observables.ConnectableObservable; +import io.reactivex.rxjava3.observers.*; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.schedulers.*; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class ObservableTimerTest extends RxJavaTest { + @Mock + Observer<Object> observer; + @Mock + Observer<Long> observer2; + + TestScheduler scheduler; + + @Before + public void before() { + observer = TestHelper.mockObserver(); + + observer2 = TestHelper.mockObserver(); + + scheduler = new TestScheduler(); + } + + @Test + public void timerOnce() { + Observable.timer(100, TimeUnit.MILLISECONDS, scheduler).subscribe(observer); + scheduler.advanceTimeBy(100, TimeUnit.MILLISECONDS); + + verify(observer, times(1)).onNext(0L); + verify(observer, times(1)).onComplete(); + verify(observer, never()).onError(any(Throwable.class)); + } + + @Test + public void timerPeriodically() { + TestObserver<Long> to = new TestObserver<>(); + + Observable.interval(100, 100, TimeUnit.MILLISECONDS, scheduler).subscribe(to); + + scheduler.advanceTimeBy(100, TimeUnit.MILLISECONDS); + + to.assertValue(0L); + + scheduler.advanceTimeBy(100, TimeUnit.MILLISECONDS); + to.assertValues(0L, 1L); + + scheduler.advanceTimeBy(100, TimeUnit.MILLISECONDS); + to.assertValues(0L, 1L, 2L); + + scheduler.advanceTimeBy(100, TimeUnit.MILLISECONDS); + to.assertValues(0L, 1L, 2L, 3L); + + to.dispose(); + scheduler.advanceTimeBy(100, TimeUnit.MILLISECONDS); + to.assertValues(0L, 1L, 2L, 3L); + + to.assertNotComplete(); + to.assertNoErrors(); + } + + @Test + public void interval() { + Observable<Long> w = Observable.interval(1, TimeUnit.SECONDS, scheduler); + TestObserver<Long> to = new TestObserver<>(); + w.subscribe(to); + + to.assertNoValues(); + to.assertNoErrors(); + to.assertNotComplete(); + + scheduler.advanceTimeTo(2, TimeUnit.SECONDS); + + to.assertValues(0L, 1L); + to.assertNoErrors(); + to.assertNotComplete(); + + to.dispose(); + + scheduler.advanceTimeTo(4, TimeUnit.SECONDS); + to.assertValues(0L, 1L); + to.assertNoErrors(); + to.assertNotComplete(); + } + + @Test + public void withMultipleSubscribersStartingAtSameTime() { + Observable<Long> w = Observable.interval(1, TimeUnit.SECONDS, scheduler); + + TestObserver<Long> to1 = new TestObserver<>(); + TestObserver<Long> to2 = new TestObserver<>(); + + w.subscribe(to1); + w.subscribe(to2); + + to1.assertNoValues(); + to2.assertNoValues(); + + scheduler.advanceTimeTo(2, TimeUnit.SECONDS); + + to1.assertValues(0L, 1L); + to1.assertNoErrors(); + to1.assertNotComplete(); + + to2.assertValues(0L, 1L); + to2.assertNoErrors(); + to2.assertNotComplete(); + + to1.dispose(); + to2.dispose(); + + scheduler.advanceTimeTo(4, TimeUnit.SECONDS); + + to1.assertValues(0L, 1L); + to1.assertNoErrors(); + to1.assertNotComplete(); + + to2.assertValues(0L, 1L); + to2.assertNoErrors(); + to2.assertNotComplete(); + } + + @Test + public void withMultipleStaggeredSubscribers() { + Observable<Long> w = Observable.interval(1, TimeUnit.SECONDS, scheduler); + + TestObserver<Long> to1 = new TestObserver<>(); + + w.subscribe(to1); + + to1.assertNoErrors(); + + scheduler.advanceTimeTo(2, TimeUnit.SECONDS); + + TestObserver<Long> to2 = new TestObserver<>(); + + w.subscribe(to2); + + to1.assertValues(0L, 1L); + to1.assertNoErrors(); + to1.assertNotComplete(); + + to2.assertNoValues(); + + scheduler.advanceTimeTo(4, TimeUnit.SECONDS); + + to1.assertValues(0L, 1L, 2L, 3L); + + to2.assertValues(0L, 1L); + + to1.dispose(); + to2.dispose(); + + to1.assertValues(0L, 1L, 2L, 3L); + to1.assertNoErrors(); + to1.assertNotComplete(); + + to2.assertValues(0L, 1L); + to2.assertNoErrors(); + to2.assertNotComplete(); + } + + @Test + public void withMultipleStaggeredSubscribersAndPublish() { + ConnectableObservable<Long> w = Observable.interval(1, TimeUnit.SECONDS, scheduler).publish(); + + TestObserver<Long> to1 = new TestObserver<>(); + + w.subscribe(to1); + w.connect(); + + to1.assertNoValues(); + + scheduler.advanceTimeTo(2, TimeUnit.SECONDS); + + TestObserver<Long> to2 = new TestObserver<>(); + w.subscribe(to2); + + to1.assertValues(0L, 1L); + to1.assertNoErrors(); + to1.assertNotComplete(); + + to2.assertNoValues(); + + scheduler.advanceTimeTo(4, TimeUnit.SECONDS); + + to1.assertValues(0L, 1L, 2L, 3L); + + to2.assertValues(2L, 3L); + + to1.dispose(); + to2.dispose(); + + to1.assertValues(0L, 1L, 2L, 3L); + to1.assertNoErrors(); + to1.assertNotComplete(); + + to2.assertValues(2L, 3L); + to2.assertNoErrors(); + to2.assertNotComplete(); + } + + @Test + public void onceObserverThrows() { + Observable<Long> source = Observable.timer(100, TimeUnit.MILLISECONDS, scheduler); + + source.safeSubscribe(new DefaultObserver<Long>() { + + @Override + public void onNext(Long t) { + throw new TestException(); + } + + @Override + public void onError(Throwable e) { + observer.onError(e); + } + + @Override + public void onComplete() { + observer.onComplete(); + } + }); + + scheduler.advanceTimeBy(1, TimeUnit.SECONDS); + + verify(observer).onError(any(TestException.class)); + verify(observer, never()).onNext(anyLong()); + verify(observer, never()).onComplete(); + } + + @Test + public void periodicObserverThrows() { + Observable<Long> source = Observable.interval(100, 100, TimeUnit.MILLISECONDS, scheduler); + + InOrder inOrder = inOrder(observer); + + source.safeSubscribe(new DefaultObserver<Long>() { + + @Override + public void onNext(Long t) { + if (t > 0) { + throw new TestException(); + } + observer.onNext(t); + } + + @Override + public void onError(Throwable e) { + observer.onError(e); + } + + @Override + public void onComplete() { + observer.onComplete(); + } + }); + + scheduler.advanceTimeBy(1, TimeUnit.SECONDS); + + inOrder.verify(observer).onNext(0L); + inOrder.verify(observer).onError(any(TestException.class)); + inOrder.verifyNoMoreInteractions(); + verify(observer, never()).onComplete(); + } + + @Test + public void disposed() { + TestHelper.checkDisposed(Observable.timer(1, TimeUnit.DAYS)); + } + + @Test + public void timerDelayZero() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + for (int i = 0; i < 1000; i++) { + Observable.timer(0, TimeUnit.MILLISECONDS).blockingFirst(); + } + + assertTrue(errors.toString(), errors.isEmpty()); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void timerInterruptible() throws Exception { + ScheduledExecutorService exec = Executors.newSingleThreadScheduledExecutor(); + try { + for (Scheduler s : new Scheduler[] { Schedulers.single(), Schedulers.computation(), Schedulers.newThread(), Schedulers.io(), Schedulers.from(exec, true) }) { + final AtomicBoolean interrupted = new AtomicBoolean(); + TestObserver<Long> to = Observable.timer(1, TimeUnit.MILLISECONDS, s) + .map(new Function<Long, Long>() { + @Override + public Long apply(Long v) throws Exception { + try { + Thread.sleep(3000); + } catch (InterruptedException ex) { + interrupted.set(true); + } + return v; + } + }) + .test(); + + Thread.sleep(500); + + to.dispose(); + + Thread.sleep(500); + + assertTrue(s.getClass().getSimpleName(), interrupted.get()); + } + } finally { + exec.shutdown(); + } + } + + @Test + public void cancelledAndRun() { + TestObserver<Long> to = new TestObserver<>(); + to.onSubscribe(Disposable.empty()); + TimerObserver tm = new TimerObserver(to); + + tm.dispose(); + + tm.run(); + + to.assertEmpty(); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableTimestampTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableTimestampTest.java new file mode 100644 index 0000000000..084db67473 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableTimestampTest.java @@ -0,0 +1,141 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import java.util.concurrent.TimeUnit; + +import org.junit.*; +import org.mockito.InOrder; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.schedulers.*; +import io.reactivex.rxjava3.subjects.PublishSubject; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class ObservableTimestampTest extends RxJavaTest { + Observer<Object> observer; + + @Before + public void before() { + observer = TestHelper.mockObserver(); + } + + @Test + public void timestampWithScheduler() { + TestScheduler scheduler = new TestScheduler(); + + PublishSubject<Integer> source = PublishSubject.create(); + Observable<Timed<Integer>> m = source.timestamp(scheduler); + m.subscribe(observer); + + source.onNext(1); + scheduler.advanceTimeBy(100, TimeUnit.MILLISECONDS); + source.onNext(2); + scheduler.advanceTimeBy(100, TimeUnit.MILLISECONDS); + source.onNext(3); + + InOrder inOrder = inOrder(observer); + + inOrder.verify(observer, times(1)).onNext(new Timed<>(1, 0, TimeUnit.MILLISECONDS)); + inOrder.verify(observer, times(1)).onNext(new Timed<>(2, 100, TimeUnit.MILLISECONDS)); + inOrder.verify(observer, times(1)).onNext(new Timed<>(3, 200, TimeUnit.MILLISECONDS)); + + verify(observer, never()).onError(any(Throwable.class)); + verify(observer, never()).onComplete(); + } + + @Test + public void timestampWithScheduler2() { + TestScheduler scheduler = new TestScheduler(); + + PublishSubject<Integer> source = PublishSubject.create(); + Observable<Timed<Integer>> m = source.timestamp(scheduler); + m.subscribe(observer); + + source.onNext(1); + source.onNext(2); + scheduler.advanceTimeBy(100, TimeUnit.MILLISECONDS); + scheduler.advanceTimeBy(100, TimeUnit.MILLISECONDS); + source.onNext(3); + + InOrder inOrder = inOrder(observer); + + inOrder.verify(observer, times(1)).onNext(new Timed<>(1, 0, TimeUnit.MILLISECONDS)); + inOrder.verify(observer, times(1)).onNext(new Timed<>(2, 0, TimeUnit.MILLISECONDS)); + inOrder.verify(observer, times(1)).onNext(new Timed<>(3, 200, TimeUnit.MILLISECONDS)); + + verify(observer, never()).onError(any(Throwable.class)); + verify(observer, never()).onComplete(); + } + + @Test + public void timeIntervalDefault() { + final TestScheduler scheduler = new TestScheduler(); + + RxJavaPlugins.setComputationSchedulerHandler(new Function<Scheduler, Scheduler>() { + @Override + public Scheduler apply(Scheduler v) throws Exception { + return scheduler; + } + }); + + try { + Observable.range(1, 5) + .timestamp() + .map(new Function<Timed<Integer>, Long>() { + @Override + public Long apply(Timed<Integer> v) throws Exception { + return v.time(); + } + }) + .test() + .assertResult(0L, 0L, 0L, 0L, 0L); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void timeIntervalDefaultSchedulerCustomUnit() { + final TestScheduler scheduler = new TestScheduler(); + + RxJavaPlugins.setComputationSchedulerHandler(new Function<Scheduler, Scheduler>() { + @Override + public Scheduler apply(Scheduler v) throws Exception { + return scheduler; + } + }); + + try { + Observable.range(1, 5) + .timestamp(TimeUnit.SECONDS) + .map(new Function<Timed<Integer>, Long>() { + @Override + public Long apply(Timed<Integer> v) throws Exception { + return v.time(); + } + }) + .test() + .assertResult(0L, 0L, 0L, 0L, 0L); + } finally { + RxJavaPlugins.reset(); + } + } + +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableToFutureTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableToFutureTest.java new file mode 100644 index 0000000000..0ac13be46a --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableToFutureTest.java @@ -0,0 +1,162 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.schedulers.*; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class ObservableToFutureTest extends RxJavaTest { + + @Test + public void success() throws Exception { + @SuppressWarnings("unchecked") + Future<Object> future = mock(Future.class); + Object value = new Object(); + when(future.get()).thenReturn(value); + + Observer<Object> o = TestHelper.mockObserver(); + + TestObserver<Object> to = new TestObserver<>(o); + + Observable.fromFuture(future).subscribe(to); + + to.dispose(); + + verify(o, times(1)).onNext(value); + verify(o, times(1)).onComplete(); + verify(o, never()).onError(any(Throwable.class)); + verify(future, never()).cancel(true); + } + + @Test + public void successOperatesOnSuppliedScheduler() throws Exception { + @SuppressWarnings("unchecked") + Future<Object> future = mock(Future.class); + Object value = new Object(); + when(future.get()).thenReturn(value); + + Observer<Object> o = TestHelper.mockObserver(); + + TestScheduler scheduler = new TestScheduler(); + TestObserver<Object> to = new TestObserver<>(o); + + Observable.fromFuture(future).subscribeOn(scheduler).subscribe(to); + + verify(o, never()).onNext(value); + + scheduler.triggerActions(); + + verify(o, times(1)).onNext(value); + } + + @Test + public void failure() throws Exception { + @SuppressWarnings("unchecked") + Future<Object> future = mock(Future.class); + RuntimeException e = new RuntimeException(); + when(future.get()).thenThrow(e); + + Observer<Object> o = TestHelper.mockObserver(); + + TestObserver<Object> to = new TestObserver<>(o); + + Observable.fromFuture(future).subscribe(to); + + to.dispose(); + + verify(o, never()).onNext(null); + verify(o, never()).onComplete(); + verify(o, times(1)).onError(e); + verify(future, never()).cancel(true); + } + + @Test + public void cancelledBeforeSubscribe() throws Exception { + @SuppressWarnings("unchecked") + Future<Object> future = mock(Future.class); + CancellationException e = new CancellationException("unit test synthetic cancellation"); + when(future.get()).thenThrow(e); + + Observer<Object> o = TestHelper.mockObserver(); + + TestObserver<Object> to = new TestObserver<>(o); + to.dispose(); + + Observable.fromFuture(future).subscribe(to); + + to.assertNoErrors(); + to.assertNotComplete(); + } + + @Test + public void cancellationDuringFutureGet() throws Exception { + Future<Object> future = new Future<Object>() { + private AtomicBoolean isCancelled = new AtomicBoolean(false); + private AtomicBoolean isDone = new AtomicBoolean(false); + + @Override + public boolean cancel(boolean mayInterruptIfRunning) { + isCancelled.compareAndSet(false, true); + return true; + } + + @Override + public boolean isCancelled() { + return isCancelled.get(); + } + + @Override + public boolean isDone() { + return isCancelled() || isDone.get(); + } + + @Override + public Object get() throws InterruptedException, ExecutionException { + Thread.sleep(500); + isDone.compareAndSet(false, true); + return "foo"; + } + + @Override + public Object get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException { + return get(); + } + }; + + Observer<Object> o = TestHelper.mockObserver(); + + TestObserver<Object> to = new TestObserver<>(o); + Observable<Object> futureObservable = Observable.fromFuture(future); + + futureObservable.subscribeOn(Schedulers.computation()).subscribe(to); + + Thread.sleep(100); + + to.dispose(); + + to.assertNoErrors(); + to.assertNoValues(); + to.assertNotComplete(); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableToListTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableToListTest.java new file mode 100644 index 0000000000..2d4185407d --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableToListTest.java @@ -0,0 +1,261 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import java.util.*; +import java.util.concurrent.*; + +import org.junit.*; +import org.mockito.Mockito; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.core.Observer; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.util.ExceptionHelper; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class ObservableToListTest extends RxJavaTest { + + @Test + public void listObservable() { + Observable<String> w = Observable.fromIterable(Arrays.asList("one", "two", "three")); + Observable<List<String>> observable = w.toList().toObservable(); + + Observer<List<String>> observer = TestHelper.mockObserver(); + observable.subscribe(observer); + verify(observer, times(1)).onNext(Arrays.asList("one", "two", "three")); + verify(observer, Mockito.never()).onError(any(Throwable.class)); + verify(observer, times(1)).onComplete(); + } + + @Test + public void listViaObservableObservable() { + Observable<String> w = Observable.fromIterable(Arrays.asList("one", "two", "three")); + Observable<List<String>> observable = w.toList().toObservable(); + + Observer<List<String>> observer = TestHelper.mockObserver(); + observable.subscribe(observer); + verify(observer, times(1)).onNext(Arrays.asList("one", "two", "three")); + verify(observer, Mockito.never()).onError(any(Throwable.class)); + verify(observer, times(1)).onComplete(); + } + + @Test + public void listMultipleSubscribersObservable() { + Observable<String> w = Observable.fromIterable(Arrays.asList("one", "two", "three")); + Observable<List<String>> observable = w.toList().toObservable(); + + Observer<List<String>> o1 = TestHelper.mockObserver(); + observable.subscribe(o1); + + Observer<List<String>> o2 = TestHelper.mockObserver(); + observable.subscribe(o2); + + List<String> expected = Arrays.asList("one", "two", "three"); + + verify(o1, times(1)).onNext(expected); + verify(o1, Mockito.never()).onError(any(Throwable.class)); + verify(o1, times(1)).onComplete(); + + verify(o2, times(1)).onNext(expected); + verify(o2, Mockito.never()).onError(any(Throwable.class)); + verify(o2, times(1)).onComplete(); + } + + @Test + public void listWithBlockingFirstObservable() { + Observable<String> o = Observable.fromIterable(Arrays.asList("one", "two", "three")); + List<String> actual = o.toList().toObservable().blockingFirst(); + Assert.assertEquals(Arrays.asList("one", "two", "three"), actual); + } + + @Test + public void capacityHintObservable() { + Observable.range(1, 10) + .toList(4) + .toObservable() + .test() + .assertResult(Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)); + } + + @Test + public void list() { + Observable<String> w = Observable.fromIterable(Arrays.asList("one", "two", "three")); + Single<List<String>> single = w.toList(); + + SingleObserver<List<String>> observer = TestHelper.mockSingleObserver(); + single.subscribe(observer); + verify(observer, times(1)).onSuccess(Arrays.asList("one", "two", "three")); + verify(observer, Mockito.never()).onError(any(Throwable.class)); + } + + @Test + public void listViaObservable() { + Observable<String> w = Observable.fromIterable(Arrays.asList("one", "two", "three")); + Single<List<String>> single = w.toList(); + + SingleObserver<List<String>> observer = TestHelper.mockSingleObserver(); + single.subscribe(observer); + verify(observer, times(1)).onSuccess(Arrays.asList("one", "two", "three")); + verify(observer, Mockito.never()).onError(any(Throwable.class)); + } + + @Test + public void listMultipleSubscribers() { + Observable<String> w = Observable.fromIterable(Arrays.asList("one", "two", "three")); + Single<List<String>> single = w.toList(); + + SingleObserver<List<String>> o1 = TestHelper.mockSingleObserver(); + single.subscribe(o1); + + SingleObserver<List<String>> o2 = TestHelper.mockSingleObserver(); + single.subscribe(o2); + + List<String> expected = Arrays.asList("one", "two", "three"); + + verify(o1, times(1)).onSuccess(expected); + verify(o1, Mockito.never()).onError(any(Throwable.class)); + + verify(o2, times(1)).onSuccess(expected); + verify(o2, Mockito.never()).onError(any(Throwable.class)); + } + + @Test + public void listWithBlockingFirst() { + Observable<String> o = Observable.fromIterable(Arrays.asList("one", "two", "three")); + List<String> actual = o.toList().blockingGet(); + Assert.assertEquals(Arrays.asList("one", "two", "three"), actual); + } + + static void await(CyclicBarrier cb) { + try { + cb.await(); + } catch (InterruptedException ex) { + ex.printStackTrace(); + } catch (BrokenBarrierException ex) { + ex.printStackTrace(); + } + } + + @Test + public void capacityHint() { + Observable.range(1, 10) + .toList(4) + .test() + .assertResult(Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)); + } + + @Test + public void dispose() { + TestHelper.checkDisposed(Observable.just(1).toList().toObservable()); + + TestHelper.checkDisposed(Observable.just(1).toList()); + } + + @Test + public void error() { + Observable.error(new TestException()) + .toList() + .toObservable() + .test() + .assertFailure(TestException.class); + } + + @Test + public void errorSingle() { + Observable.error(new TestException()) + .toList() + .test() + .assertFailure(TestException.class); + } + + @Test + public void collectionSupplierThrows() { + Observable.just(1) + .toList(new Supplier<Collection<Integer>>() { + @Override + public Collection<Integer> get() throws Exception { + throw new TestException(); + } + }) + .toObservable() + .test() + .assertFailure(TestException.class); + } + + @Test + public void collectionSupplierReturnsNull() { + Observable.just(1) + .toList(new Supplier<Collection<Integer>>() { + @Override + public Collection<Integer> get() throws Exception { + return null; + } + }) + .toObservable() + .to(TestHelper.<Collection<Integer>>testConsumer()) + .assertFailure(NullPointerException.class) + .assertErrorMessage(ExceptionHelper.nullWarning("The collectionSupplier returned a null Collection.")); + } + + @Test + public void singleCollectionSupplierThrows() { + Observable.just(1) + .toList(new Supplier<Collection<Integer>>() { + @Override + public Collection<Integer> get() throws Exception { + throw new TestException(); + } + }) + .test() + .assertFailure(TestException.class); + } + + @Test + public void singleCollectionSupplierReturnsNull() { + Observable.just(1) + .toList(new Supplier<Collection<Integer>>() { + @Override + public Collection<Integer> get() throws Exception { + return null; + } + }) + .to(TestHelper.<Collection<Integer>>testConsumer()) + .assertFailure(NullPointerException.class) + .assertErrorMessage(ExceptionHelper.nullWarning("The collectionSupplier returned a null Collection.")); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeObservable(new Function<Observable<Object>, Observable<List<Object>>>() { + @Override + public Observable<List<Object>> apply(Observable<Object> f) + throws Exception { + return f.toList().toObservable(); + } + }); + TestHelper.checkDoubleOnSubscribeObservableToSingle(new Function<Observable<Object>, Single<List<Object>>>() { + @Override + public Single<List<Object>> apply(Observable<Object> f) + throws Exception { + return f.toList(); + } + }); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableToMapTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableToMapTest.java new file mode 100644 index 0000000000..72b39d0025 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableToMapTest.java @@ -0,0 +1,400 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import java.util.*; + +import org.junit.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.core.Observer; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class ObservableToMapTest extends RxJavaTest { + Observer<Object> objectObserver; + SingleObserver<Object> singleObserver; + + @Before + public void before() { + objectObserver = TestHelper.mockObserver(); + singleObserver = TestHelper.mockSingleObserver(); + } + + Function<String, Integer> lengthFunc = new Function<String, Integer>() { + @Override + public Integer apply(String t1) { + return t1.length(); + } + }; + Function<String, String> duplicate = new Function<String, String>() { + @Override + public String apply(String t1) { + return t1 + t1; + } + }; + + @Test + public void toMapObservable() { + Observable<String> source = Observable.just("a", "bb", "ccc", "dddd"); + + Observable<Map<Integer, String>> mapped = source.toMap(lengthFunc).toObservable(); + + Map<Integer, String> expected = new HashMap<>(); + expected.put(1, "a"); + expected.put(2, "bb"); + expected.put(3, "ccc"); + expected.put(4, "dddd"); + + mapped.subscribe(objectObserver); + + verify(objectObserver, never()).onError(any(Throwable.class)); + verify(objectObserver, times(1)).onNext(expected); + verify(objectObserver, times(1)).onComplete(); + } + + @Test + public void toMapWithValueSelectorObservable() { + Observable<String> source = Observable.just("a", "bb", "ccc", "dddd"); + + Observable<Map<Integer, String>> mapped = source.toMap(lengthFunc, duplicate).toObservable(); + + Map<Integer, String> expected = new HashMap<>(); + expected.put(1, "aa"); + expected.put(2, "bbbb"); + expected.put(3, "cccccc"); + expected.put(4, "dddddddd"); + + mapped.subscribe(objectObserver); + + verify(objectObserver, never()).onError(any(Throwable.class)); + verify(objectObserver, times(1)).onNext(expected); + verify(objectObserver, times(1)).onComplete(); + } + + @Test + public void toMapWithErrorObservable() { + Observable<String> source = Observable.just("a", "bb", "ccc", "dddd"); + + Function<String, Integer> lengthFuncErr = new Function<String, Integer>() { + @Override + public Integer apply(String t1) { + if ("bb".equals(t1)) { + throw new RuntimeException("Forced Failure"); + } + return t1.length(); + } + }; + Observable<Map<Integer, String>> mapped = source.toMap(lengthFuncErr).toObservable(); + + Map<Integer, String> expected = new HashMap<>(); + expected.put(1, "a"); + expected.put(2, "bb"); + expected.put(3, "ccc"); + expected.put(4, "dddd"); + + mapped.subscribe(objectObserver); + + verify(objectObserver, never()).onNext(expected); + verify(objectObserver, never()).onComplete(); + verify(objectObserver, times(1)).onError(any(Throwable.class)); + + } + + @Test + public void toMapWithErrorInValueSelectorObservable() { + Observable<String> source = Observable.just("a", "bb", "ccc", "dddd"); + + Function<String, String> duplicateErr = new Function<String, String>() { + @Override + public String apply(String t1) { + if ("bb".equals(t1)) { + throw new RuntimeException("Forced failure"); + } + return t1 + t1; + } + }; + + Observable<Map<Integer, String>> mapped = source.toMap(lengthFunc, duplicateErr).toObservable(); + + Map<Integer, String> expected = new HashMap<>(); + expected.put(1, "aa"); + expected.put(2, "bbbb"); + expected.put(3, "cccccc"); + expected.put(4, "dddddddd"); + + mapped.subscribe(objectObserver); + + verify(objectObserver, never()).onNext(expected); + verify(objectObserver, never()).onComplete(); + verify(objectObserver, times(1)).onError(any(Throwable.class)); + + } + + @Test + public void toMapWithFactoryObservable() { + Observable<String> source = Observable.just("a", "bb", "ccc", "dddd"); + + Supplier<Map<Integer, String>> mapFactory = new Supplier<Map<Integer, String>>() { + @Override + public Map<Integer, String> get() { + return new LinkedHashMap<Integer, String>() { + + private static final long serialVersionUID = -3296811238780863394L; + + @Override + protected boolean removeEldestEntry(Map.Entry<Integer, String> eldest) { + return size() > 3; + } + }; + } + }; + + Function<String, Integer> lengthFunc = new Function<String, Integer>() { + @Override + public Integer apply(String t1) { + return t1.length(); + } + }; + Observable<Map<Integer, String>> mapped = source.toMap(lengthFunc, new Function<String, String>() { + @Override + public String apply(String v) { + return v; + } + }, mapFactory).toObservable(); + + Map<Integer, String> expected = new LinkedHashMap<>(); + expected.put(2, "bb"); + expected.put(3, "ccc"); + expected.put(4, "dddd"); + + mapped.subscribe(objectObserver); + + verify(objectObserver, never()).onError(any(Throwable.class)); + verify(objectObserver, times(1)).onNext(expected); + verify(objectObserver, times(1)).onComplete(); + } + + @Test + public void toMapWithErrorThrowingFactoryObservable() { + Observable<String> source = Observable.just("a", "bb", "ccc", "dddd"); + + Supplier<Map<Integer, String>> mapFactory = new Supplier<Map<Integer, String>>() { + @Override + public Map<Integer, String> get() { + throw new RuntimeException("Forced failure"); + } + }; + + Function<String, Integer> lengthFunc = new Function<String, Integer>() { + @Override + public Integer apply(String t1) { + return t1.length(); + } + }; + Observable<Map<Integer, String>> mapped = source.toMap(lengthFunc, new Function<String, String>() { + @Override + public String apply(String v) { + return v; + } + }, mapFactory).toObservable(); + + Map<Integer, String> expected = new LinkedHashMap<>(); + expected.put(2, "bb"); + expected.put(3, "ccc"); + expected.put(4, "dddd"); + + mapped.subscribe(objectObserver); + + verify(objectObserver, never()).onNext(expected); + verify(objectObserver, never()).onComplete(); + verify(objectObserver, times(1)).onError(any(Throwable.class)); + } + + @Test + public void toMap() { + Observable<String> source = Observable.just("a", "bb", "ccc", "dddd"); + + Single<Map<Integer, String>> mapped = source.toMap(lengthFunc); + + Map<Integer, String> expected = new HashMap<>(); + expected.put(1, "a"); + expected.put(2, "bb"); + expected.put(3, "ccc"); + expected.put(4, "dddd"); + + mapped.subscribe(singleObserver); + + verify(singleObserver, never()).onError(any(Throwable.class)); + verify(singleObserver, times(1)).onSuccess(expected); + } + + @Test + public void toMapWithValueSelector() { + Observable<String> source = Observable.just("a", "bb", "ccc", "dddd"); + + Single<Map<Integer, String>> mapped = source.toMap(lengthFunc, duplicate); + + Map<Integer, String> expected = new HashMap<>(); + expected.put(1, "aa"); + expected.put(2, "bbbb"); + expected.put(3, "cccccc"); + expected.put(4, "dddddddd"); + + mapped.subscribe(singleObserver); + + verify(singleObserver, never()).onError(any(Throwable.class)); + verify(singleObserver, times(1)).onSuccess(expected); + } + + @Test + public void toMapWithError() { + Observable<String> source = Observable.just("a", "bb", "ccc", "dddd"); + + Function<String, Integer> lengthFuncErr = new Function<String, Integer>() { + @Override + public Integer apply(String t1) { + if ("bb".equals(t1)) { + throw new RuntimeException("Forced Failure"); + } + return t1.length(); + } + }; + Single<Map<Integer, String>> mapped = source.toMap(lengthFuncErr); + + Map<Integer, String> expected = new HashMap<>(); + expected.put(1, "a"); + expected.put(2, "bb"); + expected.put(3, "ccc"); + expected.put(4, "dddd"); + + mapped.subscribe(singleObserver); + + verify(singleObserver, never()).onSuccess(expected); + verify(singleObserver, times(1)).onError(any(Throwable.class)); + + } + + @Test + public void toMapWithErrorInValueSelector() { + Observable<String> source = Observable.just("a", "bb", "ccc", "dddd"); + + Function<String, String> duplicateErr = new Function<String, String>() { + @Override + public String apply(String t1) { + if ("bb".equals(t1)) { + throw new RuntimeException("Forced failure"); + } + return t1 + t1; + } + }; + + Single<Map<Integer, String>> mapped = source.toMap(lengthFunc, duplicateErr); + + Map<Integer, String> expected = new HashMap<>(); + expected.put(1, "aa"); + expected.put(2, "bbbb"); + expected.put(3, "cccccc"); + expected.put(4, "dddddddd"); + + mapped.subscribe(singleObserver); + + verify(singleObserver, never()).onSuccess(expected); + verify(singleObserver, times(1)).onError(any(Throwable.class)); + + } + + @Test + public void toMapWithFactory() { + Observable<String> source = Observable.just("a", "bb", "ccc", "dddd"); + + Supplier<Map<Integer, String>> mapFactory = new Supplier<Map<Integer, String>>() { + @Override + public Map<Integer, String> get() { + return new LinkedHashMap<Integer, String>() { + + private static final long serialVersionUID = -3296811238780863394L; + + @Override + protected boolean removeEldestEntry(Map.Entry<Integer, String> eldest) { + return size() > 3; + } + }; + } + }; + + Function<String, Integer> lengthFunc = new Function<String, Integer>() { + @Override + public Integer apply(String t1) { + return t1.length(); + } + }; + Single<Map<Integer, String>> mapped = source.toMap(lengthFunc, new Function<String, String>() { + @Override + public String apply(String v) { + return v; + } + }, mapFactory); + + Map<Integer, String> expected = new LinkedHashMap<>(); + expected.put(2, "bb"); + expected.put(3, "ccc"); + expected.put(4, "dddd"); + + mapped.subscribe(singleObserver); + + verify(singleObserver, never()).onError(any(Throwable.class)); + verify(singleObserver, times(1)).onSuccess(expected); + } + + @Test + public void toMapWithErrorThrowingFactory() { + Observable<String> source = Observable.just("a", "bb", "ccc", "dddd"); + + Supplier<Map<Integer, String>> mapFactory = new Supplier<Map<Integer, String>>() { + @Override + public Map<Integer, String> get() { + throw new RuntimeException("Forced failure"); + } + }; + + Function<String, Integer> lengthFunc = new Function<String, Integer>() { + @Override + public Integer apply(String t1) { + return t1.length(); + } + }; + Single<Map<Integer, String>> mapped = source.toMap(lengthFunc, new Function<String, String>() { + @Override + public String apply(String v) { + return v; + } + }, mapFactory); + + Map<Integer, String> expected = new LinkedHashMap<>(); + expected.put(2, "bb"); + expected.put(3, "ccc"); + expected.put(4, "dddd"); + + mapped.subscribe(singleObserver); + + verify(singleObserver, never()).onSuccess(expected); + verify(singleObserver, times(1)).onError(any(Throwable.class)); + } + +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableToMultimapTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableToMultimapTest.java new file mode 100644 index 0000000000..10bda16080 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableToMultimapTest.java @@ -0,0 +1,539 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import java.util.*; + +import org.junit.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.core.Observer; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class ObservableToMultimapTest extends RxJavaTest { + Observer<Object> objectObserver; + SingleObserver<Object> singleObserver; + + @Before + public void before() { + objectObserver = TestHelper.mockObserver(); + singleObserver = TestHelper.mockSingleObserver(); + } + + Function<String, Integer> lengthFunc = new Function<String, Integer>() { + @Override + public Integer apply(String t1) { + return t1.length(); + } + }; + Function<String, String> duplicate = new Function<String, String>() { + @Override + public String apply(String t1) { + return t1 + t1; + } + }; + + @Test + public void toMultimapObservable() { + Observable<String> source = Observable.just("a", "b", "cc", "dd"); + + Observable<Map<Integer, Collection<String>>> mapped = source.toMultimap(lengthFunc).toObservable(); + + Map<Integer, Collection<String>> expected = new HashMap<>(); + expected.put(1, Arrays.asList("a", "b")); + expected.put(2, Arrays.asList("cc", "dd")); + + mapped.subscribe(objectObserver); + + verify(objectObserver, never()).onError(any(Throwable.class)); + verify(objectObserver, times(1)).onNext(expected); + verify(objectObserver, times(1)).onComplete(); + } + + @Test + public void toMultimapWithValueSelectorObservable() { + Observable<String> source = Observable.just("a", "b", "cc", "dd"); + + Observable<Map<Integer, Collection<String>>> mapped = source.toMultimap(lengthFunc, duplicate).toObservable(); + + Map<Integer, Collection<String>> expected = new HashMap<>(); + expected.put(1, Arrays.asList("aa", "bb")); + expected.put(2, Arrays.asList("cccc", "dddd")); + + mapped.subscribe(objectObserver); + + verify(objectObserver, never()).onError(any(Throwable.class)); + verify(objectObserver, times(1)).onNext(expected); + verify(objectObserver, times(1)).onComplete(); + } + + @Test + public void toMultimapWithMapFactoryObservable() { + Observable<String> source = Observable.just("a", "b", "cc", "dd", "eee", "fff"); + + Supplier<Map<Integer, Collection<String>>> mapFactory = new Supplier<Map<Integer, Collection<String>>>() { + @Override + public Map<Integer, Collection<String>> get() { + return new LinkedHashMap<Integer, Collection<String>>() { + + private static final long serialVersionUID = -2084477070717362859L; + + @Override + protected boolean removeEldestEntry(Map.Entry<Integer, Collection<String>> eldest) { + return size() > 2; + } + }; + } + }; + + Function<String, String> identity = new Function<String, String>() { + @Override + public String apply(String v) { + return v; + } + }; + + Observable<Map<Integer, Collection<String>>> mapped = source.toMultimap( + lengthFunc, identity, + mapFactory, new Function<Integer, Collection<String>>() { + @Override + public Collection<String> apply(Integer v) { + return new ArrayList<>(); + } + }).toObservable(); + + Map<Integer, Collection<String>> expected = new HashMap<>(); + expected.put(2, Arrays.asList("cc", "dd")); + expected.put(3, Arrays.asList("eee", "fff")); + + mapped.subscribe(objectObserver); + + verify(objectObserver, never()).onError(any(Throwable.class)); + verify(objectObserver, times(1)).onNext(expected); + verify(objectObserver, times(1)).onComplete(); + } + + @Test + public void toMultimapWithCollectionFactoryObservable() { + Observable<String> source = Observable.just("cc", "dd", "eee", "eee"); + + Function<Integer, Collection<String>> collectionFactory = new Function<Integer, Collection<String>>() { + @Override + public Collection<String> apply(Integer t1) { + if (t1 == 2) { + return new ArrayList<>(); + } else { + return new HashSet<>(); + } + } + }; + + Function<String, String> identity = new Function<String, String>() { + @Override + public String apply(String v) { + return v; + } + }; + Supplier<Map<Integer, Collection<String>>> mapSupplier = new Supplier<Map<Integer, Collection<String>>>() { + @Override + public Map<Integer, Collection<String>> get() { + return new HashMap<>(); + } + }; + + Observable<Map<Integer, Collection<String>>> mapped = source + .toMultimap(lengthFunc, identity, mapSupplier, collectionFactory).toObservable(); + + Map<Integer, Collection<String>> expected = new HashMap<>(); + expected.put(2, Arrays.asList("cc", "dd")); + expected.put(3, new HashSet<>(Arrays.asList("eee"))); + + mapped.subscribe(objectObserver); + + verify(objectObserver, never()).onError(any(Throwable.class)); + verify(objectObserver, times(1)).onNext(expected); + verify(objectObserver, times(1)).onComplete(); + } + + @Test + public void toMultimapWithErrorObservable() { + Observable<String> source = Observable.just("a", "b", "cc", "dd"); + + Function<String, Integer> lengthFuncErr = new Function<String, Integer>() { + @Override + public Integer apply(String t1) { + if ("b".equals(t1)) { + throw new RuntimeException("Forced Failure"); + } + return t1.length(); + } + }; + + Observable<Map<Integer, Collection<String>>> mapped = source.toMultimap(lengthFuncErr).toObservable(); + + Map<Integer, Collection<String>> expected = new HashMap<>(); + expected.put(1, Arrays.asList("a", "b")); + expected.put(2, Arrays.asList("cc", "dd")); + + mapped.subscribe(objectObserver); + + verify(objectObserver, times(1)).onError(any(Throwable.class)); + verify(objectObserver, never()).onNext(expected); + verify(objectObserver, never()).onComplete(); + } + + @Test + public void toMultimapWithErrorInValueSelectorObservable() { + Observable<String> source = Observable.just("a", "b", "cc", "dd"); + + Function<String, String> duplicateErr = new Function<String, String>() { + @Override + public String apply(String t1) { + if ("b".equals(t1)) { + throw new RuntimeException("Forced failure"); + } + return t1 + t1; + } + }; + + Observable<Map<Integer, Collection<String>>> mapped = source.toMultimap(lengthFunc, duplicateErr).toObservable(); + + Map<Integer, Collection<String>> expected = new HashMap<>(); + expected.put(1, Arrays.asList("aa", "bb")); + expected.put(2, Arrays.asList("cccc", "dddd")); + + mapped.subscribe(objectObserver); + + verify(objectObserver, times(1)).onError(any(Throwable.class)); + verify(objectObserver, never()).onNext(expected); + verify(objectObserver, never()).onComplete(); + } + + @Test + public void toMultimapWithMapThrowingFactoryObservable() { + Observable<String> source = Observable.just("a", "b", "cc", "dd", "eee", "fff"); + + Supplier<Map<Integer, Collection<String>>> mapFactory = new Supplier<Map<Integer, Collection<String>>>() { + @Override + public Map<Integer, Collection<String>> get() { + throw new RuntimeException("Forced failure"); + } + }; + + Observable<Map<Integer, Collection<String>>> mapped = source + .toMultimap(lengthFunc, new Function<String, String>() { + @Override + public String apply(String v) { + return v; + } + }, mapFactory).toObservable(); + + Map<Integer, Collection<String>> expected = new HashMap<>(); + expected.put(2, Arrays.asList("cc", "dd")); + expected.put(3, Arrays.asList("eee", "fff")); + + mapped.subscribe(objectObserver); + + verify(objectObserver, times(1)).onError(any(Throwable.class)); + verify(objectObserver, never()).onNext(expected); + verify(objectObserver, never()).onComplete(); + } + + @Test + public void toMultimapWithThrowingCollectionFactoryObservable() { + Observable<String> source = Observable.just("cc", "cc", "eee", "eee"); + + Function<Integer, Collection<String>> collectionFactory = new Function<Integer, Collection<String>>() { + @Override + public Collection<String> apply(Integer t1) { + if (t1 == 2) { + throw new RuntimeException("Forced failure"); + } else { + return new HashSet<>(); + } + } + }; + + Function<String, String> identity = new Function<String, String>() { + @Override + public String apply(String v) { + return v; + } + }; + Supplier<Map<Integer, Collection<String>>> mapSupplier = new Supplier<Map<Integer, Collection<String>>>() { + @Override + public Map<Integer, Collection<String>> get() { + return new HashMap<>(); + } + }; + + Observable<Map<Integer, Collection<String>>> mapped = source.toMultimap(lengthFunc, + identity, mapSupplier, collectionFactory).toObservable(); + + Map<Integer, Collection<String>> expected = new HashMap<>(); + expected.put(2, Arrays.asList("cc", "dd")); + expected.put(3, Collections.singleton("eee")); + + mapped.subscribe(objectObserver); + + verify(objectObserver, times(1)).onError(any(Throwable.class)); + verify(objectObserver, never()).onNext(expected); + verify(objectObserver, never()).onComplete(); + } + + @Test + public void toMultimap() { + Observable<String> source = Observable.just("a", "b", "cc", "dd"); + + Single<Map<Integer, Collection<String>>> mapped = source.toMultimap(lengthFunc); + + Map<Integer, Collection<String>> expected = new HashMap<>(); + expected.put(1, Arrays.asList("a", "b")); + expected.put(2, Arrays.asList("cc", "dd")); + + mapped.subscribe(singleObserver); + + verify(singleObserver, never()).onError(any(Throwable.class)); + verify(singleObserver, times(1)).onSuccess(expected); + } + + @Test + public void toMultimapWithValueSelector() { + Observable<String> source = Observable.just("a", "b", "cc", "dd"); + + Single<Map<Integer, Collection<String>>> mapped = source.toMultimap(lengthFunc, duplicate); + + Map<Integer, Collection<String>> expected = new HashMap<>(); + expected.put(1, Arrays.asList("aa", "bb")); + expected.put(2, Arrays.asList("cccc", "dddd")); + + mapped.subscribe(singleObserver); + + verify(singleObserver, never()).onError(any(Throwable.class)); + verify(singleObserver, times(1)).onSuccess(expected); + } + + @Test + public void toMultimapWithMapFactory() { + Observable<String> source = Observable.just("a", "b", "cc", "dd", "eee", "fff"); + + Supplier<Map<Integer, Collection<String>>> mapFactory = new Supplier<Map<Integer, Collection<String>>>() { + @Override + public Map<Integer, Collection<String>> get() { + return new LinkedHashMap<Integer, Collection<String>>() { + + private static final long serialVersionUID = -2084477070717362859L; + + @Override + protected boolean removeEldestEntry(Map.Entry<Integer, Collection<String>> eldest) { + return size() > 2; + } + }; + } + }; + + Function<String, String> identity = new Function<String, String>() { + @Override + public String apply(String v) { + return v; + } + }; + + Single<Map<Integer, Collection<String>>> mapped = source.toMultimap( + lengthFunc, identity, + mapFactory, new Function<Integer, Collection<String>>() { + @Override + public Collection<String> apply(Integer v) { + return new ArrayList<>(); + } + }); + + Map<Integer, Collection<String>> expected = new HashMap<>(); + expected.put(2, Arrays.asList("cc", "dd")); + expected.put(3, Arrays.asList("eee", "fff")); + + mapped.subscribe(singleObserver); + + verify(singleObserver, never()).onError(any(Throwable.class)); + verify(singleObserver, times(1)).onSuccess(expected); + } + + @Test + public void toMultimapWithCollectionFactory() { + Observable<String> source = Observable.just("cc", "dd", "eee", "eee"); + + Function<Integer, Collection<String>> collectionFactory = new Function<Integer, Collection<String>>() { + @Override + public Collection<String> apply(Integer t1) { + if (t1 == 2) { + return new ArrayList<>(); + } else { + return new HashSet<>(); + } + } + }; + + Function<String, String> identity = new Function<String, String>() { + @Override + public String apply(String v) { + return v; + } + }; + Supplier<Map<Integer, Collection<String>>> mapSupplier = new Supplier<Map<Integer, Collection<String>>>() { + @Override + public Map<Integer, Collection<String>> get() { + return new HashMap<>(); + } + }; + + Single<Map<Integer, Collection<String>>> mapped = source + .toMultimap(lengthFunc, identity, mapSupplier, collectionFactory); + + Map<Integer, Collection<String>> expected = new HashMap<>(); + expected.put(2, Arrays.asList("cc", "dd")); + expected.put(3, new HashSet<>(Arrays.asList("eee"))); + + mapped.subscribe(singleObserver); + + verify(singleObserver, never()).onError(any(Throwable.class)); + verify(singleObserver, times(1)).onSuccess(expected); + } + + @Test + public void toMultimapWithError() { + Observable<String> source = Observable.just("a", "b", "cc", "dd"); + + Function<String, Integer> lengthFuncErr = new Function<String, Integer>() { + @Override + public Integer apply(String t1) { + if ("b".equals(t1)) { + throw new RuntimeException("Forced Failure"); + } + return t1.length(); + } + }; + + Single<Map<Integer, Collection<String>>> mapped = source.toMultimap(lengthFuncErr); + + Map<Integer, Collection<String>> expected = new HashMap<>(); + expected.put(1, Arrays.asList("a", "b")); + expected.put(2, Arrays.asList("cc", "dd")); + + mapped.subscribe(singleObserver); + + verify(singleObserver, times(1)).onError(any(Throwable.class)); + verify(singleObserver, never()).onSuccess(expected); + } + + @Test + public void toMultimapWithErrorInValueSelector() { + Observable<String> source = Observable.just("a", "b", "cc", "dd"); + + Function<String, String> duplicateErr = new Function<String, String>() { + @Override + public String apply(String t1) { + if ("b".equals(t1)) { + throw new RuntimeException("Forced failure"); + } + return t1 + t1; + } + }; + + Single<Map<Integer, Collection<String>>> mapped = source.toMultimap(lengthFunc, duplicateErr); + + Map<Integer, Collection<String>> expected = new HashMap<>(); + expected.put(1, Arrays.asList("aa", "bb")); + expected.put(2, Arrays.asList("cccc", "dddd")); + + mapped.subscribe(singleObserver); + + verify(singleObserver, times(1)).onError(any(Throwable.class)); + verify(singleObserver, never()).onSuccess(expected); + } + + @Test + public void toMultimapWithMapThrowingFactory() { + Observable<String> source = Observable.just("a", "b", "cc", "dd", "eee", "fff"); + + Supplier<Map<Integer, Collection<String>>> mapFactory = new Supplier<Map<Integer, Collection<String>>>() { + @Override + public Map<Integer, Collection<String>> get() { + throw new RuntimeException("Forced failure"); + } + }; + + Single<Map<Integer, Collection<String>>> mapped = source + .toMultimap(lengthFunc, new Function<String, String>() { + @Override + public String apply(String v) { + return v; + } + }, mapFactory); + + Map<Integer, Collection<String>> expected = new HashMap<>(); + expected.put(2, Arrays.asList("cc", "dd")); + expected.put(3, Arrays.asList("eee", "fff")); + + mapped.subscribe(singleObserver); + + verify(singleObserver, times(1)).onError(any(Throwable.class)); + verify(singleObserver, never()).onSuccess(expected); + } + + @Test + public void toMultimapWithThrowingCollectionFactory() { + Observable<String> source = Observable.just("cc", "cc", "eee", "eee"); + + Function<Integer, Collection<String>> collectionFactory = new Function<Integer, Collection<String>>() { + @Override + public Collection<String> apply(Integer t1) { + if (t1 == 2) { + throw new RuntimeException("Forced failure"); + } else { + return new HashSet<>(); + } + } + }; + + Function<String, String> identity = new Function<String, String>() { + @Override + public String apply(String v) { + return v; + } + }; + Supplier<Map<Integer, Collection<String>>> mapSupplier = new Supplier<Map<Integer, Collection<String>>>() { + @Override + public Map<Integer, Collection<String>> get() { + return new HashMap<>(); + } + }; + + Single<Map<Integer, Collection<String>>> mapped = source.toMultimap(lengthFunc, + identity, mapSupplier, collectionFactory); + + Map<Integer, Collection<String>> expected = new HashMap<>(); + expected.put(2, Arrays.asList("cc", "dd")); + expected.put(3, Collections.singleton("eee")); + + mapped.subscribe(singleObserver); + + verify(singleObserver, times(1)).onError(any(Throwable.class)); + verify(singleObserver, never()).onSuccess(expected); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableToSortedListTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableToSortedListTest.java new file mode 100644 index 0000000000..0ea0be21b9 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableToSortedListTest.java @@ -0,0 +1,172 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import java.util.*; +import java.util.concurrent.*; + +import org.junit.Test; +import org.mockito.Mockito; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.core.Observer; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class ObservableToSortedListTest extends RxJavaTest { + + @Test + public void sortedListObservable() { + Observable<Integer> w = Observable.just(1, 3, 2, 5, 4); + Observable<List<Integer>> observable = w.toSortedList().toObservable(); + + Observer<List<Integer>> observer = TestHelper.mockObserver(); + observable.subscribe(observer); + verify(observer, times(1)).onNext(Arrays.asList(1, 2, 3, 4, 5)); + verify(observer, Mockito.never()).onError(any(Throwable.class)); + verify(observer, times(1)).onComplete(); + } + + @Test + public void sortedListWithCustomFunctionFlowable() { + Observable<Integer> w = Observable.just(1, 3, 2, 5, 4); + Observable<List<Integer>> observable = w.toSortedList(new Comparator<Integer>() { + + @Override + public int compare(Integer t1, Integer t2) { + return t2 - t1; + } + + }).toObservable(); + + Observer<List<Integer>> observer = TestHelper.mockObserver(); + observable.subscribe(observer); + verify(observer, times(1)).onNext(Arrays.asList(5, 4, 3, 2, 1)); + verify(observer, Mockito.never()).onError(any(Throwable.class)); + verify(observer, times(1)).onComplete(); + } + + @Test + public void withFollowingFirstObservable() { + Observable<Integer> o = Observable.just(1, 3, 2, 5, 4); + assertEquals(Arrays.asList(1, 2, 3, 4, 5), o.toSortedList().toObservable().blockingFirst()); + } + + static void await(CyclicBarrier cb) { + try { + cb.await(); + } catch (InterruptedException ex) { + ex.printStackTrace(); + } catch (BrokenBarrierException ex) { + ex.printStackTrace(); + } + } + + @Test + public void sorted() { + Observable.just(5, 1, 2, 4, 3).sorted() + .test() + .assertResult(1, 2, 3, 4, 5); + } + + @Test + public void sortedComparator() { + Observable.just(5, 1, 2, 4, 3).sorted(new Comparator<Integer>() { + @Override + public int compare(Integer a, Integer b) { + return b - a; + } + }) + .test() + .assertResult(5, 4, 3, 2, 1); + } + + @Test + public void toSortedListCapacityObservable() { + Observable.just(5, 1, 2, 4, 3).toSortedList(4).toObservable() + .test() + .assertResult(Arrays.asList(1, 2, 3, 4, 5)); + } + + @Test + public void toSortedListComparatorCapacityObservable() { + Observable.just(5, 1, 2, 4, 3).toSortedList(new Comparator<Integer>() { + @Override + public int compare(Integer a, Integer b) { + return b - a; + } + }, 4).toObservable() + .test() + .assertResult(Arrays.asList(5, 4, 3, 2, 1)); + } + + @Test + public void sortedList() { + Observable<Integer> w = Observable.just(1, 3, 2, 5, 4); + Single<List<Integer>> single = w.toSortedList(); + + SingleObserver<List<Integer>> observer = TestHelper.mockSingleObserver(); + single.subscribe(observer); + verify(observer, times(1)).onSuccess(Arrays.asList(1, 2, 3, 4, 5)); + verify(observer, Mockito.never()).onError(any(Throwable.class)); + } + + @Test + public void sortedListWithCustomFunction() { + Observable<Integer> w = Observable.just(1, 3, 2, 5, 4); + Single<List<Integer>> single = w.toSortedList(new Comparator<Integer>() { + + @Override + public int compare(Integer t1, Integer t2) { + return t2 - t1; + } + + }); + + SingleObserver<List<Integer>> observer = TestHelper.mockSingleObserver(); + single.subscribe(observer); + verify(observer, times(1)).onSuccess(Arrays.asList(5, 4, 3, 2, 1)); + verify(observer, Mockito.never()).onError(any(Throwable.class)); + } + + @Test + public void withFollowingFirst() { + Observable<Integer> o = Observable.just(1, 3, 2, 5, 4); + assertEquals(Arrays.asList(1, 2, 3, 4, 5), o.toSortedList().blockingGet()); + } + + @Test + public void toSortedListCapacity() { + Observable.just(5, 1, 2, 4, 3).toSortedList(4) + .test() + .assertResult(Arrays.asList(1, 2, 3, 4, 5)); + } + + @Test + public void toSortedListComparatorCapacity() { + Observable.just(5, 1, 2, 4, 3).toSortedList(new Comparator<Integer>() { + @Override + public int compare(Integer a, Integer b) { + return b - a; + } + }, 4) + .test() + .assertResult(Arrays.asList(5, 4, 3, 2, 1)); + } + +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableToXTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableToXTest.java new file mode 100644 index 0000000000..c7cc6f3151 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableToXTest.java @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.MissingBackpressureException; +import io.reactivex.rxjava3.subscribers.TestSubscriber; + +public class ObservableToXTest extends RxJavaTest { + + @Test + public void toFlowableBuffer() { + Observable.range(1, 5) + .toFlowable(BackpressureStrategy.BUFFER) + .test(2L) + .assertValues(1, 2) + .assertNoErrors() + .assertNotComplete(); + } + + @Test + public void toFlowableDrop() { + Observable.range(1, 5) + .toFlowable(BackpressureStrategy.DROP) + .test(1) + .assertResult(1); + } + + @Test + public void toFlowableLatest() { + TestSubscriber<Integer> ts = Observable.range(1, 5) + .toFlowable(BackpressureStrategy.LATEST) + .test(0); + + ts.request(1); + ts + .assertResult(5); + } + + @Test + public void toFlowableError1() { + Observable.range(1, 5) + .toFlowable(BackpressureStrategy.ERROR) + .test(1) + .assertFailure(MissingBackpressureException.class, 1); + } + + @Test + public void toFlowableError2() { + Observable.range(1, 5) + .toFlowable(BackpressureStrategy.ERROR) + .test(5) + .assertResult(1, 2, 3, 4, 5); + } + + @Test + public void toFlowableMissing() { + TestSubscriber<Integer> ts = Observable.range(1, 5) + .toFlowable(BackpressureStrategy.MISSING) + .test(0); + + ts.request(2); + ts + .assertResult(1, 2, 3, 4, 5); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableUnsubscribeOnTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableUnsubscribeOnTest.java new file mode 100644 index 0000000000..23f6d7256b --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableUnsubscribeOnTest.java @@ -0,0 +1,274 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import static org.junit.Assert.*; + +import java.util.List; +import java.util.concurrent.*; +import java.util.concurrent.atomic.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.annotations.NonNull; +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.Action; +import io.reactivex.rxjava3.internal.schedulers.ImmediateThinScheduler; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.schedulers.Schedulers; +import io.reactivex.rxjava3.testsupport.*; + +public class ObservableUnsubscribeOnTest extends RxJavaTest { + + @Test + public void unsubscribeWhenSubscribeOnAndUnsubscribeOnAreOnSameThread() throws InterruptedException { + UIEventLoopScheduler uiEventLoop = new UIEventLoopScheduler(); + try { + final ThreadSubscription subscription = new ThreadSubscription(); + final AtomicReference<Thread> subscribeThread = new AtomicReference<>(); + Observable<Integer> w = Observable.unsafeCreate(new ObservableSource<Integer>() { + + @Override + public void subscribe(Observer<? super Integer> t1) { + subscribeThread.set(Thread.currentThread()); + t1.onSubscribe(subscription); + t1.onNext(1); + t1.onNext(2); + // observeOn will prevent canceling the upstream upon its termination now + // this call is racing for that state in this test + // not doing it will make sure the unsubscribeOn always gets through + // t1.onComplete(); + } + }); + + TestObserverEx<Integer> observer = new TestObserverEx<>(); + + w.subscribeOn(uiEventLoop).observeOn(Schedulers.computation()) + .unsubscribeOn(uiEventLoop) + .take(2) + .subscribe(observer); + + observer.awaitDone(5, TimeUnit.SECONDS); + + Thread unsubscribeThread = subscription.getThread(); + + assertNotNull(unsubscribeThread); + assertNotSame(Thread.currentThread(), unsubscribeThread); + + assertNotNull(subscribeThread.get()); + assertNotSame(Thread.currentThread(), subscribeThread.get()); + // True for Schedulers.newThread() + + System.out.println("unsubscribeThread: " + unsubscribeThread); + System.out.println("subscribeThread.get(): " + subscribeThread.get()); + assertSame(unsubscribeThread.toString(), unsubscribeThread, uiEventLoop.getThread()); + + observer.assertValues(1, 2); + observer.assertTerminated(); + } finally { + uiEventLoop.shutdown(); + } + } + + @Test + public void unsubscribeWhenSubscribeOnAndUnsubscribeOnAreOnDifferentThreads() throws InterruptedException { + UIEventLoopScheduler uiEventLoop = new UIEventLoopScheduler(); + try { + final ThreadSubscription subscription = new ThreadSubscription(); + final AtomicReference<Thread> subscribeThread = new AtomicReference<>(); + Observable<Integer> w = Observable.unsafeCreate(new ObservableSource<Integer>() { + + @Override + public void subscribe(Observer<? super Integer> t1) { + subscribeThread.set(Thread.currentThread()); + t1.onSubscribe(subscription); + t1.onNext(1); + t1.onNext(2); + // observeOn will prevent canceling the upstream upon its termination now + // this call is racing for that state in this test + // not doing it will make sure the unsubscribeOn always gets through + // t1.onComplete(); + } + }); + + TestObserverEx<Integer> observer = new TestObserverEx<>(); + + w.subscribeOn(Schedulers.newThread()).observeOn(Schedulers.computation()) + .unsubscribeOn(uiEventLoop) + .take(2) + .subscribe(observer); + + observer.awaitDone(1, TimeUnit.SECONDS); + + Thread unsubscribeThread = subscription.getThread(); + + assertNotNull(unsubscribeThread); + assertNotSame(Thread.currentThread(), unsubscribeThread); + + assertNotNull(subscribeThread.get()); + assertNotSame(Thread.currentThread(), subscribeThread.get()); + // True for Schedulers.newThread() + + System.out.println("UI Thread: " + uiEventLoop.getThread()); + System.out.println("unsubscribeThread: " + unsubscribeThread); + System.out.println("subscribeThread.get(): " + subscribeThread.get()); + assertSame(unsubscribeThread, uiEventLoop.getThread()); + + observer.assertValues(1, 2); + observer.assertTerminated(); + } finally { + uiEventLoop.shutdown(); + } + } + + private static class ThreadSubscription extends AtomicBoolean implements Disposable { + + private static final long serialVersionUID = -5011338112974328771L; + + private volatile Thread thread; + + private final CountDownLatch latch = new CountDownLatch(1); + + @Override + public void dispose() { + set(true); + System.out.println("unsubscribe invoked: " + Thread.currentThread()); + thread = Thread.currentThread(); + latch.countDown(); + } + + @Override public boolean isDisposed() { + return get(); + } + + public Thread getThread() throws InterruptedException { + latch.await(); + return thread; + } + } + + public static class UIEventLoopScheduler extends Scheduler { + + private final Scheduler eventLoop; + private volatile Thread t; + + public UIEventLoopScheduler() { + + eventLoop = Schedulers.single(); + + /* + * DON'T DO THIS IN PRODUCTION CODE + */ + final CountDownLatch latch = new CountDownLatch(1); + eventLoop.scheduleDirect(new Runnable() { + + @Override + public void run() { + t = Thread.currentThread(); + latch.countDown(); + } + + }); + try { + latch.await(); + } catch (InterruptedException e) { + throw new RuntimeException("failed to initialize and get inner thread"); + } + } + + @NonNull + @Override + public Worker createWorker() { + return eventLoop.createWorker(); + } + + public Thread getThread() { + return t; + } + + } + + @Test + public void dispose() { + TestHelper.checkDisposed(Observable.just(1).unsubscribeOn(Schedulers.single())); + } + + @Test + public void normal() { + final int[] calls = { 0 }; + + Observable.just(1) + .doOnDispose(new Action() { + @Override + public void run() throws Exception { + calls[0]++; + } + }) + .unsubscribeOn(Schedulers.single()) + .test() + .assertResult(1); + + assertEquals(0, calls[0]); + } + + @Test + public void error() { + final int[] calls = { 0 }; + + Observable.error(new TestException()) + .doOnDispose(new Action() { + @Override + public void run() throws Exception { + calls[0]++; + } + }) + .unsubscribeOn(Schedulers.single()) + .test() + .assertFailure(TestException.class); + + assertEquals(0, calls[0]); + } + + @Test + public void signalAfterDispose() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + new Observable<Integer>() { + @Override + protected void subscribeActual(Observer<? super Integer> observer) { + observer.onSubscribe(Disposable.empty()); + observer.onNext(1); + observer.onNext(2); + observer.onError(new TestException()); + observer.onComplete(); + } + } + .unsubscribeOn(Schedulers.single()) + .take(1) + .test() + .assertResult(1); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeObservable(o -> o.unsubscribeOn(ImmediateThinScheduler.INSTANCE)); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableUsingTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableUsingTest.java new file mode 100644 index 0000000000..de8d73fa36 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableUsingTest.java @@ -0,0 +1,622 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +import java.util.*; + +import org.junit.Test; +import org.mockito.InOrder; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.core.Observer; +import io.reactivex.rxjava3.disposables.*; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.testsupport.*; + +public class ObservableUsingTest extends RxJavaTest { + + interface Resource { + String getTextFromWeb(); + + void dispose(); + } + + private static class DisposeAction implements Consumer<Resource> { + + @Override + public void accept(Resource r) { + r.dispose(); + } + + } + + private final Consumer<Disposable> disposeSubscription = new Consumer<Disposable>() { + + @Override + public void accept(Disposable d) { + d.dispose(); + } + + }; + + @Test + public void using() { + performTestUsing(false); + } + + @Test + public void usingEagerly() { + performTestUsing(true); + } + + private void performTestUsing(boolean disposeEagerly) { + final Resource resource = mock(Resource.class); + when(resource.getTextFromWeb()).thenReturn("Hello world!"); + + Supplier<Resource> resourceFactory = new Supplier<Resource>() { + @Override + public Resource get() { + return resource; + } + }; + + Function<Resource, Observable<String>> observableFactory = new Function<Resource, Observable<String>>() { + @Override + public Observable<String> apply(Resource res) { + return Observable.fromArray(res.getTextFromWeb().split(" ")); + } + }; + + Observer<String> observer = TestHelper.mockObserver(); + + Observable<String> o = Observable.using(resourceFactory, observableFactory, + new DisposeAction(), disposeEagerly); + o.subscribe(observer); + + InOrder inOrder = inOrder(observer); + inOrder.verify(observer, times(1)).onNext("Hello"); + inOrder.verify(observer, times(1)).onNext("world!"); + inOrder.verify(observer, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + + // The resouce should be closed + verify(resource, times(1)).dispose(); + } + + @Test + public void usingWithSubscribingTwice() { + performTestUsingWithSubscribingTwice(false); + } + + @Test + public void usingWithSubscribingTwiceDisposeEagerly() { + performTestUsingWithSubscribingTwice(true); + } + + private void performTestUsingWithSubscribingTwice(boolean disposeEagerly) { + // When subscribe is called, a new resource should be created. + Supplier<Resource> resourceFactory = new Supplier<Resource>() { + @Override + public Resource get() { + return new Resource() { + + boolean first = true; + + @Override + public String getTextFromWeb() { + if (first) { + first = false; + return "Hello world!"; + } + return "Nothing"; + } + + @Override + public void dispose() { + // do nothing + } + + }; + } + }; + + Function<Resource, Observable<String>> observableFactory = new Function<Resource, Observable<String>>() { + @Override + public Observable<String> apply(Resource res) { + return Observable.fromArray(res.getTextFromWeb().split(" ")); + } + }; + + Observer<String> observer = TestHelper.mockObserver(); + + Observable<String> o = Observable.using(resourceFactory, observableFactory, + new DisposeAction(), disposeEagerly); + o.subscribe(observer); + o.subscribe(observer); + + InOrder inOrder = inOrder(observer); + + inOrder.verify(observer, times(1)).onNext("Hello"); + inOrder.verify(observer, times(1)).onNext("world!"); + inOrder.verify(observer, times(1)).onComplete(); + + inOrder.verify(observer, times(1)).onNext("Hello"); + inOrder.verify(observer, times(1)).onNext("world!"); + inOrder.verify(observer, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + } + + @Test(expected = TestException.class) + public void usingWithResourceFactoryError() { + performTestUsingWithResourceFactoryError(false); + } + + @Test(expected = TestException.class) + public void usingWithResourceFactoryErrorDisposeEagerly() { + performTestUsingWithResourceFactoryError(true); + } + + private void performTestUsingWithResourceFactoryError(boolean disposeEagerly) { + Supplier<Disposable> resourceFactory = new Supplier<Disposable>() { + @Override + public Disposable get() { + throw new TestException(); + } + }; + + Function<Disposable, Observable<Integer>> observableFactory = new Function<Disposable, Observable<Integer>>() { + @Override + public Observable<Integer> apply(Disposable d) { + return Observable.empty(); + } + }; + + Observable.using(resourceFactory, observableFactory, disposeSubscription) + .blockingLast(); + } + + @Test + public void usingWithObservableFactoryError() { + performTestUsingWithObservableFactoryError(false); + } + + @Test + public void usingWithObservableFactoryErrorDisposeEagerly() { + performTestUsingWithObservableFactoryError(true); + } + + private void performTestUsingWithObservableFactoryError(boolean disposeEagerly) { + final Runnable unsubscribe = mock(Runnable.class); + Supplier<Disposable> resourceFactory = new Supplier<Disposable>() { + @Override + public Disposable get() { + return Disposable.fromRunnable(unsubscribe); + } + }; + + Function<Disposable, Observable<Integer>> observableFactory = new Function<Disposable, Observable<Integer>>() { + @Override + public Observable<Integer> apply(Disposable subscription) { + throw new TestException(); + } + }; + + try { + Observable.using(resourceFactory, observableFactory, disposeSubscription).blockingLast(); + fail("Should throw a TestException when the observableFactory throws it"); + } catch (TestException e) { + // Make sure that unsubscribe is called so that users can close + // the resource if some error happens. + verify(unsubscribe, times(1)).run(); + } + } + + @Test + public void usingDisposesEagerlyBeforeCompletion() { + final List<String> events = new ArrayList<>(); + Supplier<Resource> resourceFactory = createResourceFactory(events); + final Action completion = createOnCompletedAction(events); + final Action unsub = createUnsubAction(events); + + Function<Resource, Observable<String>> observableFactory = new Function<Resource, Observable<String>>() { + @Override + public Observable<String> apply(Resource resource) { + return Observable.fromArray(resource.getTextFromWeb().split(" ")); + } + }; + + Observer<String> observer = TestHelper.mockObserver(); + + Observable<String> o = Observable.using(resourceFactory, observableFactory, + new DisposeAction(), true) + .doOnDispose(unsub) + .doOnComplete(completion); + + o.safeSubscribe(observer); + + assertEquals(Arrays.asList("disposed", "completed" /* , "unsub" */), events); + + } + + @Test + public void usingDoesNotDisposesEagerlyBeforeCompletion() { + final List<String> events = new ArrayList<>(); + Supplier<Resource> resourceFactory = createResourceFactory(events); + final Action completion = createOnCompletedAction(events); + final Action unsub = createUnsubAction(events); + + Function<Resource, Observable<String>> observableFactory = new Function<Resource, Observable<String>>() { + @Override + public Observable<String> apply(Resource resource) { + return Observable.fromArray(resource.getTextFromWeb().split(" ")); + } + }; + + Observer<String> observer = TestHelper.mockObserver(); + + Observable<String> o = Observable.using(resourceFactory, observableFactory, + new DisposeAction(), false) + .doOnDispose(unsub) + .doOnComplete(completion); + + o.safeSubscribe(observer); + + assertEquals(Arrays.asList("completed", /*"unsub",*/ "disposed"), events); + + } + + @Test + public void usingDisposesEagerlyBeforeError() { + final List<String> events = new ArrayList<>(); + Supplier<Resource> resourceFactory = createResourceFactory(events); + final Consumer<Throwable> onError = createOnErrorAction(events); + final Action unsub = createUnsubAction(events); + + Function<Resource, Observable<String>> observableFactory = new Function<Resource, Observable<String>>() { + @Override + public Observable<String> apply(Resource resource) { + return Observable.fromArray(resource.getTextFromWeb().split(" ")) + .concatWith(Observable.<String>error(new RuntimeException())); + } + }; + + Observer<String> observer = TestHelper.mockObserver(); + + Observable<String> o = Observable.using(resourceFactory, observableFactory, + new DisposeAction(), true) + .doOnDispose(unsub) + .doOnError(onError); + + o.safeSubscribe(observer); + + assertEquals(Arrays.asList("disposed", "error" /*, "unsub"*/), events); + + } + + @Test + public void usingDoesNotDisposesEagerlyBeforeError() { + final List<String> events = new ArrayList<>(); + final Supplier<Resource> resourceFactory = createResourceFactory(events); + final Consumer<Throwable> onError = createOnErrorAction(events); + final Action unsub = createUnsubAction(events); + + Function<Resource, Observable<String>> observableFactory = new Function<Resource, Observable<String>>() { + @Override + public Observable<String> apply(Resource resource) { + return Observable.fromArray(resource.getTextFromWeb().split(" ")) + .concatWith(Observable.<String>error(new RuntimeException())); + } + }; + + Observer<String> observer = TestHelper.mockObserver(); + + Observable<String> o = Observable.using(resourceFactory, observableFactory, + new DisposeAction(), false) + .doOnDispose(unsub) + .doOnError(onError); + + o.safeSubscribe(observer); + + assertEquals(Arrays.asList("error", /* "unsub",*/ "disposed"), events); + } + + private static Action createUnsubAction(final List<String> events) { + return new Action() { + @Override + public void run() { + events.add("unsub"); + } + }; + } + + private static Consumer<Throwable> createOnErrorAction(final List<String> events) { + return new Consumer<Throwable>() { + @Override + public void accept(Throwable t) { + events.add("error"); + } + }; + } + + private static Supplier<Resource> createResourceFactory(final List<String> events) { + return new Supplier<Resource>() { + @Override + public Resource get() { + return new Resource() { + + @Override + public String getTextFromWeb() { + return "hello world"; + } + + @Override + public void dispose() { + events.add("disposed"); + } + }; + } + }; + } + + private static Action createOnCompletedAction(final List<String> events) { + return new Action() { + @Override + public void run() { + events.add("completed"); + } + }; + } + + @Test + public void dispose() { + TestHelper.checkDisposed(Observable.using( + new Supplier<Object>() { + @Override + public Object get() throws Exception { + return 1; + } + }, + new Function<Object, ObservableSource<Object>>() { + @Override + public ObservableSource<Object> apply(Object v) throws Exception { + return Observable.never(); + } + }, + Functions.emptyConsumer() + )); + } + + @Test + public void supplierDisposerCrash() { + TestObserverEx<Object> to = Observable.using(new Supplier<Object>() { + @Override + public Object get() throws Exception { + return 1; + } + }, new Function<Object, ObservableSource<Object>>() { + @Override + public ObservableSource<Object> apply(Object v) throws Exception { + throw new TestException("First"); + } + }, new Consumer<Object>() { + @Override + public void accept(Object e) throws Exception { + throw new TestException("Second"); + } + }) + .to(TestHelper.<Object>testConsumer()) + .assertFailure(CompositeException.class); + + List<Throwable> errors = TestHelper.compositeList(to.errors().get(0)); + + TestHelper.assertError(errors, 0, TestException.class, "First"); + TestHelper.assertError(errors, 1, TestException.class, "Second"); + } + + @Test + public void eagerOnErrorDisposerCrash() { + TestObserverEx<Object> to = Observable.using(new Supplier<Object>() { + @Override + public Object get() throws Exception { + return 1; + } + }, new Function<Object, ObservableSource<Object>>() { + @Override + public ObservableSource<Object> apply(Object v) throws Exception { + return Observable.error(new TestException("First")); + } + }, new Consumer<Object>() { + @Override + public void accept(Object e) throws Exception { + throw new TestException("Second"); + } + }) + .to(TestHelper.<Object>testConsumer()) + .assertFailure(CompositeException.class); + + List<Throwable> errors = TestHelper.compositeList(to.errors().get(0)); + + TestHelper.assertError(errors, 0, TestException.class, "First"); + TestHelper.assertError(errors, 1, TestException.class, "Second"); + } + + @Test + public void eagerOnCompleteDisposerCrash() { + Observable.using(new Supplier<Object>() { + @Override + public Object get() throws Exception { + return 1; + } + }, new Function<Object, ObservableSource<Object>>() { + @Override + public ObservableSource<Object> apply(Object v) throws Exception { + return Observable.empty(); + } + }, new Consumer<Object>() { + @Override + public void accept(Object e) throws Exception { + throw new TestException("Second"); + } + }) + .to(TestHelper.<Object>testConsumer()) + .assertFailureAndMessage(TestException.class, "Second"); + } + + @Test + public void nonEagerDisposerCrash() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + Observable.using(new Supplier<Object>() { + @Override + public Object get() throws Exception { + return 1; + } + }, new Function<Object, ObservableSource<Object>>() { + @Override + public ObservableSource<Object> apply(Object v) throws Exception { + return Observable.empty(); + } + }, new Consumer<Object>() { + @Override + public void accept(Object e) throws Exception { + throw new TestException("Second"); + } + }, false) + .test() + .assertResult(); + + TestHelper.assertUndeliverable(errors, 0, TestException.class, "Second"); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void sourceSupplierReturnsNull() { + Observable.using(Functions.justSupplier(1), + Functions.justFunction((Observable<Object>)null), + Functions.emptyConsumer()) + .to(TestHelper.<Object>testConsumer()) + .assertFailureAndMessage(NullPointerException.class, "The sourceSupplier returned a null ObservableSource") + ; + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeObservable(new Function<Observable<Object>, ObservableSource<Object>>() { + @Override + public ObservableSource<Object> apply(Observable<Object> o) + throws Exception { + return Observable.using(Functions.justSupplier(1), Functions.justFunction(o), Functions.emptyConsumer()); + } + }); + } + + @Test + public void eagerDisposedOnComplete() { + final TestObserver<Integer> to = new TestObserver<>(); + + Observable.using(Functions.justSupplier(1), Functions.justFunction(new Observable<Integer>() { + @Override + protected void subscribeActual(Observer<? super Integer> observer) { + observer.onSubscribe(Disposable.empty()); + to.dispose(); + observer.onComplete(); + } + }), Functions.emptyConsumer(), true) + .subscribe(to); + } + + @Test + public void eagerDisposedOnError() { + final TestObserver<Integer> to = new TestObserver<>(); + + Observable.using(Functions.justSupplier(1), Functions.justFunction(new Observable<Integer>() { + @Override + protected void subscribeActual(Observer<? super Integer> observer) { + observer.onSubscribe(Disposable.empty()); + to.dispose(); + observer.onError(new TestException()); + } + }), Functions.emptyConsumer(), true) + .subscribe(to); + } + + @Test + public void eagerDisposeResourceThenDisposeUpstream() { + final StringBuilder sb = new StringBuilder(); + + Observable.using(Functions.justSupplier(1), + new Function<Integer, Observable<Integer>>() { + @Override + public Observable<Integer> apply(Integer t) throws Throwable { + return Observable.range(1, 2) + .doOnDispose(new Action() { + @Override + public void run() throws Throwable { + sb.append("Dispose"); + } + }) + ; + } + }, new Consumer<Integer>() { + @Override + public void accept(Integer t) throws Throwable { + sb.append("Resource"); + } + }, true) + .take(1) + .test() + .assertResult(1); + + assertEquals("ResourceDispose", sb.toString()); + } + + @Test + public void nonEagerDisposeUpstreamThenDisposeResource() { + final StringBuilder sb = new StringBuilder(); + + Observable.using(Functions.justSupplier(1), + new Function<Integer, Observable<Integer>>() { + @Override + public Observable<Integer> apply(Integer t) throws Throwable { + return Observable.range(1, 2) + .doOnDispose(new Action() { + @Override + public void run() throws Throwable { + sb.append("Dispose"); + } + }); + } + }, new Consumer<Integer>() { + @Override + public void accept(Integer t) throws Throwable { + sb.append("Resource"); + } + }, false) + .take(1) + .test() + .assertResult(1); + + assertEquals("DisposeResource", sb.toString()); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableWindowWithObservableTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableWindowWithObservableTest.java new file mode 100644 index 0000000000..edfca3989d --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableWindowWithObservableTest.java @@ -0,0 +1,707 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import java.util.*; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.core.Observer; +import io.reactivex.rxjava3.disposables.*; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.observers.*; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.subjects.*; +import io.reactivex.rxjava3.testsupport.*; + +public class ObservableWindowWithObservableTest extends RxJavaTest { + + @Test + public void windowViaObservableNormal1() { + PublishSubject<Integer> source = PublishSubject.create(); + PublishSubject<Integer> boundary = PublishSubject.create(); + + final Observer<Object> o = TestHelper.mockObserver(); + + final List<Observer<Object>> values = new ArrayList<>(); + + Observer<Observable<Integer>> wo = new DefaultObserver<Observable<Integer>>() { + @Override + public void onNext(Observable<Integer> args) { + final Observer<Object> mo = TestHelper.mockObserver(); + values.add(mo); + + args.subscribe(mo); + } + + @Override + public void onError(Throwable e) { + o.onError(e); + } + + @Override + public void onComplete() { + o.onComplete(); + } + }; + + source.window(boundary).subscribe(wo); + + int n = 30; + for (int i = 0; i < n; i++) { + source.onNext(i); + if (i % 3 == 2 && i < n - 1) { + boundary.onNext(i / 3); + } + } + source.onComplete(); + + verify(o, never()).onError(any(Throwable.class)); + + assertEquals(n / 3, values.size()); + + int j = 0; + for (Observer<Object> mo : values) { + verify(mo, never()).onError(any(Throwable.class)); + for (int i = 0; i < 3; i++) { + verify(mo).onNext(j + i); + } + verify(mo).onComplete(); + j += 3; + } + + verify(o).onComplete(); + } + + @Test + public void windowViaObservableBoundaryCompletes() { + PublishSubject<Integer> source = PublishSubject.create(); + PublishSubject<Integer> boundary = PublishSubject.create(); + + final Observer<Object> o = TestHelper.mockObserver(); + + final List<Observer<Object>> values = new ArrayList<>(); + + Observer<Observable<Integer>> wo = new DefaultObserver<Observable<Integer>>() { + @Override + public void onNext(Observable<Integer> args) { + final Observer<Object> mo = TestHelper.mockObserver(); + values.add(mo); + + args.subscribe(mo); + } + + @Override + public void onError(Throwable e) { + o.onError(e); + } + + @Override + public void onComplete() { + o.onComplete(); + } + }; + + source.window(boundary).subscribe(wo); + + int n = 30; + for (int i = 0; i < n; i++) { + source.onNext(i); + if (i % 3 == 2 && i < n - 1) { + boundary.onNext(i / 3); + } + } + boundary.onComplete(); + + assertEquals(n / 3, values.size()); + + int j = 0; + for (Observer<Object> mo : values) { + for (int i = 0; i < 3; i++) { + verify(mo).onNext(j + i); + } + verify(mo).onComplete(); + verify(mo, never()).onError(any(Throwable.class)); + j += 3; + } + + verify(o).onComplete(); + verify(o, never()).onError(any(Throwable.class)); + } + + @Test + public void windowViaObservableBoundaryThrows() { + PublishSubject<Integer> source = PublishSubject.create(); + PublishSubject<Integer> boundary = PublishSubject.create(); + + final Observer<Object> o = TestHelper.mockObserver(); + + final List<Observer<Object>> values = new ArrayList<>(); + + Observer<Observable<Integer>> wo = new DefaultObserver<Observable<Integer>>() { + @Override + public void onNext(Observable<Integer> args) { + final Observer<Object> mo = TestHelper.mockObserver(); + values.add(mo); + + args.subscribe(mo); + } + + @Override + public void onError(Throwable e) { + o.onError(e); + } + + @Override + public void onComplete() { + o.onComplete(); + } + }; + + source.window(boundary).subscribe(wo); + + source.onNext(0); + source.onNext(1); + source.onNext(2); + + boundary.onError(new TestException()); + + assertEquals(1, values.size()); + + Observer<Object> mo = values.get(0); + + verify(mo).onNext(0); + verify(mo).onNext(1); + verify(mo).onNext(2); + verify(mo).onError(any(TestException.class)); + + verify(o, never()).onComplete(); + verify(o).onError(any(TestException.class)); + } + + @Test + public void windowViaObservableSourceThrows() { + PublishSubject<Integer> source = PublishSubject.create(); + PublishSubject<Integer> boundary = PublishSubject.create(); + + final Observer<Object> o = TestHelper.mockObserver(); + + final List<Observer<Object>> values = new ArrayList<>(); + + Observer<Observable<Integer>> wo = new DefaultObserver<Observable<Integer>>() { + @Override + public void onNext(Observable<Integer> args) { + final Observer<Object> mo = TestHelper.mockObserver(); + values.add(mo); + + args.subscribe(mo); + } + + @Override + public void onError(Throwable e) { + o.onError(e); + } + + @Override + public void onComplete() { + o.onComplete(); + } + }; + + source.window(boundary).subscribe(wo); + + source.onNext(0); + source.onNext(1); + source.onNext(2); + + source.onError(new TestException()); + + assertEquals(1, values.size()); + + Observer<Object> mo = values.get(0); + + verify(mo).onNext(0); + verify(mo).onNext(1); + verify(mo).onNext(2); + verify(mo).onError(any(TestException.class)); + + verify(o, never()).onComplete(); + verify(o).onError(any(TestException.class)); + } + + @Test + public void boundaryDispose() { + TestHelper.checkDisposed(Observable.never().window(Observable.never())); + } + + @Test + public void boundaryOnError() { + TestObserverEx<Object> to = Observable.error(new TestException()) + .window(Observable.never()) + .flatMap(Functions.<Observable<Object>>identity(), true) + .to(TestHelper.testConsumer()) + .assertFailure(CompositeException.class); + + List<Throwable> errors = TestHelper.compositeList(to.errors().get(0)); + + TestHelper.assertError(errors, 0, TestException.class); + } + + @Test + public void innerBadSource() { + TestHelper.checkBadSourceObservable(new Function<Observable<Integer>, Object>() { + @Override + public Object apply(Observable<Integer> o) throws Exception { + return Observable.just(1).window(o).flatMap(new Function<Observable<Integer>, ObservableSource<Integer>>() { + @Override + public ObservableSource<Integer> apply(Observable<Integer> v) throws Exception { + return v; + } + }); + } + }, false, 1, 1, (Object[])null); + } + + @Test + public void reentrant() { + final Subject<Integer> ps = PublishSubject.<Integer>create(); + + TestObserver<Integer> to = new TestObserver<Integer>() { + @Override + public void onNext(Integer t) { + super.onNext(t); + if (t == 1) { + ps.onNext(2); + ps.onComplete(); + } + } + }; + + ps.window(BehaviorSubject.createDefault(1)) + .flatMap(new Function<Observable<Integer>, ObservableSource<Integer>>() { + @Override + public ObservableSource<Integer> apply(Observable<Integer> v) throws Exception { + return v; + } + }) + .subscribe(to); + + ps.onNext(1); + + to + .awaitDone(1, TimeUnit.SECONDS) + .assertResult(1, 2); + } + + @Test + public void badSource() { + TestHelper.checkBadSourceObservable(new Function<Observable<Object>, Object>() { + @Override + public Object apply(Observable<Object> o) throws Exception { + return o.window(Observable.never()).flatMap(new Function<Observable<Object>, ObservableSource<Object>>() { + @Override + public ObservableSource<Object> apply(Observable<Object> v) throws Exception { + return v; + } + }); + } + }, false, 1, 1, 1); + } + + @Test + public void boundaryDirectDoubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeObservable(new Function<Observable<Object>, Observable<Observable<Object>>>() { + @Override + public Observable<Observable<Object>> apply(Observable<Object> f) + throws Exception { + return f.window(Observable.never()).takeLast(1); + } + }); + } + + @Test + public void upstreamDisposedWhenOutputsDisposed() { + PublishSubject<Integer> source = PublishSubject.create(); + PublishSubject<Integer> boundary = PublishSubject.create(); + + TestObserver<Integer> to = source.window(boundary) + .take(1) + .flatMap(new Function<Observable<Integer>, ObservableSource<Integer>>() { + @Override + public ObservableSource<Integer> apply( + Observable<Integer> w) throws Exception { + return w.take(1); + } + }) + .test(); + + source.onNext(1); + + assertFalse("source not disposed", source.hasObservers()); + assertFalse("boundary not disposed", boundary.hasObservers()); + + to.assertResult(1); + } + + @Test + public void mainAndBoundaryBothError() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + final AtomicReference<Observer<? super Object>> ref = new AtomicReference<>(); + + TestObserverEx<Observable<Object>> to = Observable.error(new TestException("main")) + .window(new Observable<Object>() { + @Override + protected void subscribeActual(Observer<? super Object> observer) { + observer.onSubscribe(Disposable.empty()); + ref.set(observer); + } + }) + .doOnNext(new Consumer<Observable<Object>>() { + @Override + public void accept(Observable<Object> w) throws Throwable { + w.subscribe(Functions.emptyConsumer(), Functions.emptyConsumer()); // avoid abandonment + } + }) + .to(TestHelper.<Observable<Object>>testConsumer()); + + to + .assertValueCount(1) + .assertError(TestException.class) + .assertErrorMessage("main") + .assertNotComplete(); + + ref.get().onError(new TestException("inner")); + + TestHelper.assertUndeliverable(errors, 0, TestException.class, "inner"); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void mainCompleteBoundaryErrorRace() { + final TestException ex = new TestException(); + + for (int i = 0; i < TestHelper.RACE_LONG_LOOPS; i++) { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + final AtomicReference<Observer<? super Object>> refMain = new AtomicReference<>(); + final AtomicReference<Observer<? super Object>> ref = new AtomicReference<>(); + + TestObserverEx<Observable<Object>> to = new Observable<Object>() { + @Override + protected void subscribeActual(Observer<? super Object> observer) { + observer.onSubscribe(Disposable.empty()); + refMain.set(observer); + } + } + .window(new Observable<Object>() { + @Override + protected void subscribeActual(Observer<? super Object> observer) { + observer.onSubscribe(Disposable.empty()); + ref.set(observer); + } + }) + .to(TestHelper.<Observable<Object>>testConsumer()); + + Runnable r1 = new Runnable() { + @Override + public void run() { + refMain.get().onComplete(); + } + }; + Runnable r2 = new Runnable() { + @Override + public void run() { + ref.get().onError(ex); + } + }; + + TestHelper.race(r1, r2); + + to + .assertValueCount(1) + .assertTerminated(); + + if (!errors.isEmpty()) { + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } + } finally { + RxJavaPlugins.reset(); + } + } + } + + @Test + public void mainNextBoundaryNextRace() { + for (int i = 0; i < TestHelper.RACE_LONG_LOOPS; i++) { + final AtomicReference<Observer<? super Object>> refMain = new AtomicReference<>(); + final AtomicReference<Observer<? super Object>> ref = new AtomicReference<>(); + + TestObserver<Observable<Object>> to = new Observable<Object>() { + @Override + protected void subscribeActual(Observer<? super Object> observer) { + observer.onSubscribe(Disposable.empty()); + refMain.set(observer); + } + } + .window(new Observable<Object>() { + @Override + protected void subscribeActual(Observer<? super Object> observer) { + observer.onSubscribe(Disposable.empty()); + ref.set(observer); + } + }) + .test(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + refMain.get().onNext(1); + } + }; + Runnable r2 = new Runnable() { + @Override + public void run() { + ref.get().onNext(1); + } + }; + + TestHelper.race(r1, r2); + + to + .assertValueCount(2) + .assertNotComplete() + .assertNoErrors(); + } + } + + @Test + public void takeOneAnotherBoundary() { + final AtomicReference<Observer<? super Object>> refMain = new AtomicReference<>(); + final AtomicReference<Observer<? super Object>> ref = new AtomicReference<>(); + + TestObserverEx<Observable<Object>> to = new Observable<Object>() { + @Override + protected void subscribeActual(Observer<? super Object> observer) { + observer.onSubscribe(Disposable.empty()); + refMain.set(observer); + } + } + .window(new Observable<Object>() { + @Override + protected void subscribeActual(Observer<? super Object> observer) { + observer.onSubscribe(Disposable.empty()); + ref.set(observer); + } + }) + .to(TestHelper.<Observable<Object>>testConsumer()); + + to.assertValueCount(1) + .assertNotTerminated() + .dispose(); + + ref.get().onNext(1); + + to.assertValueCount(1) + .assertNotTerminated(); + } + + @Test + public void disposeMainBoundaryCompleteRace() { + for (int i = 0; i < TestHelper.RACE_LONG_LOOPS; i++) { + final AtomicReference<Observer<? super Object>> refMain = new AtomicReference<>(); + final AtomicReference<Observer<? super Object>> ref = new AtomicReference<>(); + + final TestObserver<Observable<Object>> to = new Observable<Object>() { + @Override + protected void subscribeActual(Observer<? super Object> observer) { + observer.onSubscribe(Disposable.empty()); + refMain.set(observer); + } + } + .window(new Observable<Object>() { + @Override + protected void subscribeActual(Observer<? super Object> observer) { + final AtomicInteger counter = new AtomicInteger(); + observer.onSubscribe(new Disposable() { + + @Override + public void dispose() { + // about a microsecond + for (int i = 0; i < 100; i++) { + counter.incrementAndGet(); + } + } + + @Override + public boolean isDisposed() { + return false; + } + }); + ref.set(observer); + } + }) + .test(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + to.dispose(); + } + }; + Runnable r2 = new Runnable() { + @Override + public void run() { + Observer<Object> o = ref.get(); + o.onNext(1); + o.onComplete(); + } + }; + + TestHelper.race(r1, r2); + } + } + + @Test + @SuppressUndeliverable + public void disposeMainBoundaryErrorRace() { + final TestException ex = new TestException(); + + for (int i = 0; i < TestHelper.RACE_LONG_LOOPS; i++) { + final AtomicReference<Observer<? super Object>> refMain = new AtomicReference<>(); + final AtomicReference<Observer<? super Object>> ref = new AtomicReference<>(); + + final TestObserver<Observable<Object>> to = new Observable<Object>() { + @Override + protected void subscribeActual(Observer<? super Object> observer) { + observer.onSubscribe(Disposable.empty()); + refMain.set(observer); + } + } + .window(new Observable<Object>() { + @Override + protected void subscribeActual(Observer<? super Object> observer) { + final AtomicInteger counter = new AtomicInteger(); + observer.onSubscribe(new Disposable() { + + @Override + public void dispose() { + // about a microsecond + for (int i = 0; i < 100; i++) { + counter.incrementAndGet(); + } + } + + @Override + public boolean isDisposed() { + return false; + } + }); + ref.set(observer); + } + }) + .test(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + to.dispose(); + } + }; + Runnable r2 = new Runnable() { + @Override + public void run() { + Observer<Object> o = ref.get(); + o.onNext(1); + o.onError(ex); + } + }; + + TestHelper.race(r1, r2); + } + } + + @Test + public void cancellingWindowCancelsUpstream() { + PublishSubject<Integer> ps = PublishSubject.create(); + + TestObserver<Integer> to = ps.window(Observable.<Integer>never()) + .take(1) + .flatMap(new Function<Observable<Integer>, Observable<Integer>>() { + @Override + public Observable<Integer> apply(Observable<Integer> w) throws Throwable { + return w.take(1); + } + }) + .test(); + + assertTrue(ps.hasObservers()); + + ps.onNext(1); + + to + .assertResult(1); + + assertFalse("Subject still has observers!", ps.hasObservers()); + } + + @Test + public void windowAbandonmentCancelsUpstream() { + PublishSubject<Integer> ps = PublishSubject.create(); + + final AtomicReference<Observable<Integer>> inner = new AtomicReference<>(); + + TestObserver<Observable<Integer>> to = ps.window(Observable.<Integer>never()) + .doOnNext(new Consumer<Observable<Integer>>() { + @Override + public void accept(Observable<Integer> v) throws Throwable { + inner.set(v); + } + }) + .test(); + + assertTrue(ps.hasObservers()); + + to + .assertValueCount(1) + ; + + ps.onNext(1); + + assertTrue(ps.hasObservers()); + + to.dispose(); + + to + .assertValueCount(1) + .assertNoErrors() + .assertNotComplete(); + + assertFalse("Subject still has observers!", ps.hasObservers()); + + inner.get().test().assertResult(); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableWindowWithSizeTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableWindowWithSizeTest.java new file mode 100644 index 0000000000..81d076e718 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableWindowWithSizeTest.java @@ -0,0 +1,628 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import static org.junit.Assert.*; + +import java.util.*; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.core.Observer; +import io.reactivex.rxjava3.disposables.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.schedulers.Schedulers; +import io.reactivex.rxjava3.subjects.PublishSubject; +import io.reactivex.rxjava3.testsupport.*; + +public class ObservableWindowWithSizeTest extends RxJavaTest { + + private static <T> List<List<T>> toLists(Observable<Observable<T>> observables) { + + final List<List<T>> lists = new ArrayList<>(); + Observable.concatEager(observables.map(new Function<Observable<T>, Observable<List<T>>>() { + @Override + public Observable<List<T>> apply(Observable<T> xs) { + return xs.toList().toObservable(); + } + })) + .blockingForEach(new Consumer<List<T>>() { + @Override + public void accept(List<T> xs) { + lists.add(xs); + } + }); + return lists; + } + + @Test + public void nonOverlappingWindows() { + Observable<String> subject = Observable.just("one", "two", "three", "four", "five"); + Observable<Observable<String>> windowed = subject.window(3); + + List<List<String>> windows = toLists(windowed); + + assertEquals(2, windows.size()); + assertEquals(list("one", "two", "three"), windows.get(0)); + assertEquals(list("four", "five"), windows.get(1)); + } + + @Test + public void skipAndCountGaplessWindows() { + Observable<String> subject = Observable.just("one", "two", "three", "four", "five"); + Observable<Observable<String>> windowed = subject.window(3, 3); + + List<List<String>> windows = toLists(windowed); + + assertEquals(2, windows.size()); + assertEquals(list("one", "two", "three"), windows.get(0)); + assertEquals(list("four", "five"), windows.get(1)); + } + + @Test + public void overlappingWindows() { + Observable<String> subject = Observable.fromArray(new String[] { "zero", "one", "two", "three", "four", "five" }); + Observable<Observable<String>> windowed = subject.window(3, 1); + + List<List<String>> windows = toLists(windowed); + + assertEquals(6, windows.size()); + assertEquals(list("zero", "one", "two"), windows.get(0)); + assertEquals(list("one", "two", "three"), windows.get(1)); + assertEquals(list("two", "three", "four"), windows.get(2)); + assertEquals(list("three", "four", "five"), windows.get(3)); + assertEquals(list("four", "five"), windows.get(4)); + assertEquals(list("five"), windows.get(5)); + } + + @Test + public void skipAndCountWindowsWithGaps() { + Observable<String> subject = Observable.just("one", "two", "three", "four", "five"); + Observable<Observable<String>> windowed = subject.window(2, 3); + + List<List<String>> windows = toLists(windowed); + + assertEquals(2, windows.size()); + assertEquals(list("one", "two"), windows.get(0)); + assertEquals(list("four", "five"), windows.get(1)); + } + + @Test + public void windowUnsubscribeNonOverlapping() { + TestObserverEx<Integer> to = new TestObserverEx<>(); + + final AtomicInteger count = new AtomicInteger(); + Observable.merge(Observable.range(1, 10000).doOnNext(new Consumer<Integer>() { + + @Override + public void accept(Integer t1) { + count.incrementAndGet(); + } + + }).window(5).take(2)) + .subscribe(to); + + to.awaitDone(500, TimeUnit.MILLISECONDS); + to.assertTerminated(); + to.assertValues(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + // System.out.println(ts.getOnNextEvents()); + assertEquals(10, count.get()); + } + + @Test + public void windowUnsubscribeNonOverlappingAsyncSource() { + TestObserverEx<Integer> to = new TestObserverEx<>(); + + final AtomicInteger count = new AtomicInteger(); + Observable.merge(Observable.range(1, 100000) + .doOnNext(new Consumer<Integer>() { + + @Override + public void accept(Integer t1) { + if (count.incrementAndGet() == 500000) { + // give it a small break halfway through + try { + Thread.sleep(50); + } catch (InterruptedException ex) { + // ignored + } + } + } + + }) + .observeOn(Schedulers.computation()) + .window(5) + .take(2)) + .subscribe(to); + + to.awaitDone(500, TimeUnit.MILLISECONDS); + to.assertTerminated(); + to.assertValues(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + // make sure we don't emit all values ... the unsubscribe should propagate + assertTrue(count.get() < 100000); + } + + @Test + public void windowUnsubscribeOverlapping() { + TestObserverEx<Integer> to = new TestObserverEx<>(); + + final AtomicInteger count = new AtomicInteger(); + Observable.merge(Observable.range(1, 10000).doOnNext(new Consumer<Integer>() { + + @Override + public void accept(Integer t1) { + count.incrementAndGet(); + } + + }).window(5, 4).take(2)) + .subscribe(to); + + to.awaitDone(500, TimeUnit.MILLISECONDS); + to.assertTerminated(); + // System.out.println(ts.getOnNextEvents()); + to.assertValues(1, 2, 3, 4, 5, 5, 6, 7, 8, 9); + assertEquals(9, count.get()); + } + + @Test + public void windowUnsubscribeOverlappingAsyncSource() { + TestObserverEx<Integer> to = new TestObserverEx<>(); + + final AtomicInteger count = new AtomicInteger(); + Observable.merge(Observable.range(1, 100000) + .doOnNext(new Consumer<Integer>() { + + @Override + public void accept(Integer t1) { + count.incrementAndGet(); + } + + }) + .observeOn(Schedulers.computation()) + .window(5, 4) + .take(2), 128) + .subscribe(to); + + to.awaitDone(500, TimeUnit.MILLISECONDS); + to.assertTerminated(); + to.assertValues(1, 2, 3, 4, 5, 5, 6, 7, 8, 9); + // make sure we don't emit all values ... the unsubscribe should propagate + // assertTrue(count.get() < 100000); // disabled: a small hiccup in the consumption may allow the source to run to completion + } + + private List<String> list(String... args) { + List<String> list = new ArrayList<>(); + for (String arg : args) { + list.add(arg); + } + return list; + } + + public static Observable<Integer> hotStream() { + return Observable.unsafeCreate(new ObservableSource<Integer>() { + @Override + public void subscribe(Observer<? super Integer> observer) { + Disposable d = Disposable.empty(); + observer.onSubscribe(d); + while (!d.isDisposed()) { + // burst some number of items + for (int i = 0; i < Math.random() * 20; i++) { + observer.onNext(i); + } + try { + // sleep for a random amount of time + // NOTE: Only using Thread.sleep here as an artificial demo. + Thread.sleep((long) (Math.random() * 200)); + } catch (Exception e) { + // do nothing + } + } + System.out.println("Hot done."); + } + }).subscribeOn(Schedulers.newThread()); // use newThread since we are using sleep to block + } + + @Test + public void takeFlatMapCompletes() { + TestObserver<Integer> to = new TestObserver<>(); + + final int indicator = 999999999; + + hotStream() + .window(10) + .take(2) + .flatMap(new Function<Observable<Integer>, Observable<Integer>>() { + @Override + public Observable<Integer> apply(Observable<Integer> w) { + return w.startWithItem(indicator); + } + }).subscribe(to); + + to.awaitDone(2, TimeUnit.SECONDS); + to.assertComplete(); + to.assertValueCount(22); + } + + @Test + public void dispose() { + TestHelper.checkDisposed(PublishSubject.create().window(1)); + + TestHelper.checkDisposed(PublishSubject.create().window(2, 1)); + + TestHelper.checkDisposed(PublishSubject.create().window(1, 2)); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeObservable(new Function<Observable<Object>, ObservableSource<Observable<Object>>>() { + @Override + public ObservableSource<Observable<Object>> apply(Observable<Object> o) throws Exception { + return o.window(1); + } + }); + + TestHelper.checkDoubleOnSubscribeObservable(new Function<Observable<Object>, ObservableSource<Observable<Object>>>() { + @Override + public ObservableSource<Observable<Object>> apply(Observable<Object> o) throws Exception { + return o.window(2, 1); + } + }); + + TestHelper.checkDoubleOnSubscribeObservable(new Function<Observable<Object>, ObservableSource<Observable<Object>>>() { + @Override + public ObservableSource<Observable<Object>> apply(Observable<Object> o) throws Exception { + return o.window(1, 2); + } + }); + } + + @Test + public void errorExact() { + Observable.error(new TestException()) + .window(1) + .test() + .assertFailure(TestException.class); + } + + @Test + public void errorSkip() { + Observable.error(new TestException()) + .window(1, 2) + .test() + .assertFailure(TestException.class); + } + + @Test + public void errorOverlap() { + Observable.error(new TestException()) + .window(2, 1) + .test() + .assertFailure(TestException.class); + } + + @SuppressWarnings("unchecked") + @Test + public void errorExactInner() { + @SuppressWarnings("rawtypes") + final TestObserver[] to = { null }; + Observable.just(1).concatWith(Observable.<Integer>error(new TestException())) + .window(2) + .doOnNext(new Consumer<Observable<Integer>>() { + @Override + public void accept(Observable<Integer> w) throws Exception { + to[0] = w.test(); + } + }) + .test() + .assertError(TestException.class); + + to[0].assertFailure(TestException.class, 1); + } + + @SuppressWarnings("unchecked") + @Test + public void errorSkipInner() { + @SuppressWarnings("rawtypes") + final TestObserver[] to = { null }; + Observable.just(1).concatWith(Observable.<Integer>error(new TestException())) + .window(2, 3) + .doOnNext(new Consumer<Observable<Integer>>() { + @Override + public void accept(Observable<Integer> w) throws Exception { + to[0] = w.test(); + } + }) + .test() + .assertError(TestException.class); + + to[0].assertFailure(TestException.class, 1); + } + + @SuppressWarnings("unchecked") + @Test + public void errorOverlapInner() { + @SuppressWarnings("rawtypes") + final TestObserver[] to = { null }; + Observable.just(1).concatWith(Observable.<Integer>error(new TestException())) + .window(3, 2) + .doOnNext(new Consumer<Observable<Integer>>() { + @Override + public void accept(Observable<Integer> w) throws Exception { + to[0] = w.test(); + } + }) + .test() + .assertError(TestException.class); + + to[0].assertFailure(TestException.class, 1); + } + + @Test + public void cancellingWindowCancelsUpstreamSize() { + PublishSubject<Integer> ps = PublishSubject.create(); + + TestObserver<Integer> to = ps.window(10) + .take(1) + .flatMap(new Function<Observable<Integer>, Observable<Integer>>() { + @Override + public Observable<Integer> apply(Observable<Integer> w) throws Throwable { + return w.take(1); + } + }) + .test(); + + assertTrue(ps.hasObservers()); + + ps.onNext(1); + + to + .assertResult(1); + + assertFalse("Subject still has observers!", ps.hasObservers()); + } + + @Test + public void windowAbandonmentCancelsUpstreamSize() { + PublishSubject<Integer> ps = PublishSubject.create(); + + final AtomicReference<Observable<Integer>> inner = new AtomicReference<>(); + + TestObserver<Observable<Integer>> to = ps.window(10) + .take(1) + .doOnNext(new Consumer<Observable<Integer>>() { + @Override + public void accept(Observable<Integer> v) throws Throwable { + inner.set(v); + } + }) + .test(); + + assertTrue(ps.hasObservers()); + + ps.onNext(1); + + to + .assertValueCount(1) + .assertNoErrors() + .assertComplete(); + + assertFalse("Subject still has observers!", ps.hasObservers()); + + inner.get().test().assertResult(1); + } + + @Test + public void cancellingWindowCancelsUpstreamSkip() { + PublishSubject<Integer> ps = PublishSubject.create(); + + TestObserver<Integer> to = ps.window(5, 10) + .take(1) + .flatMap(new Function<Observable<Integer>, Observable<Integer>>() { + @Override + public Observable<Integer> apply(Observable<Integer> w) throws Throwable { + return w.take(1); + } + }) + .test(); + + assertTrue(ps.hasObservers()); + + ps.onNext(1); + + to + .assertResult(1); + + assertFalse("Subject still has observers!", ps.hasObservers()); + } + + @Test + public void windowAbandonmentCancelsUpstreamSkip() { + PublishSubject<Integer> ps = PublishSubject.create(); + + final AtomicReference<Observable<Integer>> inner = new AtomicReference<>(); + + TestObserver<Observable<Integer>> to = ps.window(5, 10) + .take(1) + .doOnNext(new Consumer<Observable<Integer>>() { + @Override + public void accept(Observable<Integer> v) throws Throwable { + inner.set(v); + } + }) + .test(); + + assertTrue(ps.hasObservers()); + + ps.onNext(1); + + to + .assertValueCount(1) + .assertNoErrors() + .assertComplete(); + + assertFalse("Subject still has observers!", ps.hasObservers()); + + inner.get().test().assertResult(1); + } + + @Test + public void cancellingWindowCancelsUpstreamOverlap() { + PublishSubject<Integer> ps = PublishSubject.create(); + + TestObserver<Integer> to = ps.window(5, 3) + .take(1) + .flatMap(new Function<Observable<Integer>, Observable<Integer>>() { + @Override + public Observable<Integer> apply(Observable<Integer> w) throws Throwable { + return w.take(1); + } + }) + .test(); + + assertTrue(ps.hasObservers()); + + ps.onNext(1); + + to + .assertResult(1); + + assertFalse("Subject still has observers!", ps.hasObservers()); + } + + @Test + public void windowAbandonmentCancelsUpstreamOverlap() { + PublishSubject<Integer> ps = PublishSubject.create(); + + final AtomicReference<Observable<Integer>> inner = new AtomicReference<>(); + + TestObserver<Observable<Integer>> to = ps.window(5, 3) + .take(1) + .doOnNext(new Consumer<Observable<Integer>>() { + @Override + public void accept(Observable<Integer> v) throws Throwable { + inner.set(v); + } + }) + .test(); + + assertTrue(ps.hasObservers()); + + ps.onNext(1); + + to + .assertValueCount(1) + .assertNoErrors() + .assertComplete(); + + assertFalse("Subject still has observers!", ps.hasObservers()); + + inner.get().test().assertResult(1); + } + + @Test + public void cancelWithoutWindowSize() { + PublishSubject<Integer> ps = PublishSubject.create(); + + TestObserver<Observable<Integer>> to = ps.window(10) + .test(); + + assertTrue(ps.hasObservers()); + + to.dispose(); + + assertFalse("Subject still has observers!", ps.hasObservers()); + } + + @Test + public void cancelAfterAbandonmentSize() { + PublishSubject<Integer> ps = PublishSubject.create(); + + TestObserver<Observable<Integer>> to = ps.window(10) + .test(); + + assertTrue(ps.hasObservers()); + + ps.onNext(1); + + to.dispose(); + + assertFalse("Subject still has observers!", ps.hasObservers()); + } + + @Test + public void cancelWithoutWindowSkip() { + PublishSubject<Integer> ps = PublishSubject.create(); + + TestObserver<Observable<Integer>> to = ps.window(10, 15) + .test(); + + assertTrue(ps.hasObservers()); + + to.dispose(); + + assertFalse("Subject still has observers!", ps.hasObservers()); + } + + @Test + public void cancelAfterAbandonmentSkip() { + PublishSubject<Integer> ps = PublishSubject.create(); + + TestObserver<Observable<Integer>> to = ps.window(10, 15) + .test(); + + assertTrue(ps.hasObservers()); + + ps.onNext(1); + + to.dispose(); + + assertFalse("Subject still has observers!", ps.hasObservers()); + } + + @Test + public void cancelWithoutWindowOverlap() { + PublishSubject<Integer> ps = PublishSubject.create(); + + TestObserver<Observable<Integer>> to = ps.window(10, 5) + .test(); + + assertTrue(ps.hasObservers()); + + to.dispose(); + + assertFalse("Subject still has observers!", ps.hasObservers()); + } + + @Test + public void cancelAfterAbandonmentOverlap() { + PublishSubject<Integer> ps = PublishSubject.create(); + + TestObserver<Observable<Integer>> to = ps.window(10, 5) + .test(); + + assertTrue(ps.hasObservers()); + + ps.onNext(1); + + to.dispose(); + + assertFalse("Subject still has observers!", ps.hasObservers()); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableWindowWithStartEndObservableTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableWindowWithStartEndObservableTest.java new file mode 100644 index 0000000000..659daaf7d1 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableWindowWithStartEndObservableTest.java @@ -0,0 +1,705 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import static org.junit.Assert.*; + +import java.io.IOException; +import java.util.*; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.*; + +import org.junit.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.core.Observer; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.observers.*; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.schedulers.TestScheduler; +import io.reactivex.rxjava3.subjects.*; +import io.reactivex.rxjava3.testsupport.*; + +public class ObservableWindowWithStartEndObservableTest extends RxJavaTest { + + private TestScheduler scheduler; + private Scheduler.Worker innerScheduler; + + @Before + public void before() { + scheduler = new TestScheduler(); + innerScheduler = scheduler.createWorker(); + } + + @Test + public void observableBasedOpenerAndCloser() { + final List<String> list = new ArrayList<>(); + final List<List<String>> lists = new ArrayList<>(); + + Observable<String> source = Observable.unsafeCreate(new ObservableSource<String>() { + @Override + public void subscribe(Observer<? super String> innerObserver) { + innerObserver.onSubscribe(Disposable.empty()); + push(innerObserver, "one", 10); + push(innerObserver, "two", 60); + push(innerObserver, "three", 110); + push(innerObserver, "four", 160); + push(innerObserver, "five", 210); + complete(innerObserver, 500); + } + }); + + Observable<Object> openings = Observable.unsafeCreate(new ObservableSource<Object>() { + @Override + public void subscribe(Observer<? super Object> innerObserver) { + innerObserver.onSubscribe(Disposable.empty()); + push(innerObserver, new Object(), 50); + push(innerObserver, new Object(), 200); + complete(innerObserver, 250); + } + }); + + Function<Object, Observable<Object>> closer = new Function<Object, Observable<Object>>() { + @Override + public Observable<Object> apply(Object opening) { + return Observable.unsafeCreate(new ObservableSource<Object>() { + @Override + public void subscribe(Observer<? super Object> innerObserver) { + innerObserver.onSubscribe(Disposable.empty()); + push(innerObserver, new Object(), 100); + complete(innerObserver, 101); + } + }); + } + }; + + Observable<Observable<String>> windowed = source.window(openings, closer); + windowed.subscribe(observeWindow(list, lists)); + + scheduler.advanceTimeTo(500, TimeUnit.MILLISECONDS); + assertEquals(2, lists.size()); + assertEquals(lists.get(0), list("two", "three")); + assertEquals(lists.get(1), list("five")); + } + + private List<String> list(String... args) { + List<String> list = new ArrayList<>(); + for (String arg : args) { + list.add(arg); + } + return list; + } + + private <T> void push(final Observer<T> observer, final T value, int delay) { + innerScheduler.schedule(new Runnable() { + @Override + public void run() { + observer.onNext(value); + } + }, delay, TimeUnit.MILLISECONDS); + } + + private void complete(final Observer<?> observer, int delay) { + innerScheduler.schedule(new Runnable() { + @Override + public void run() { + observer.onComplete(); + } + }, delay, TimeUnit.MILLISECONDS); + } + + private Consumer<Observable<String>> observeWindow(final List<String> list, final List<List<String>> lists) { + return new Consumer<Observable<String>>() { + @Override + public void accept(Observable<String> stringObservable) { + stringObservable.subscribe(new DefaultObserver<String>() { + @Override + public void onComplete() { + lists.add(new ArrayList<>(list)); + list.clear(); + } + + @Override + public void onError(Throwable e) { + fail(e.getMessage()); + } + + @Override + public void onNext(String args) { + list.add(args); + } + }); + } + }; + } + + @Test + public void noUnsubscribeAndNoLeak() { + PublishSubject<Integer> source = PublishSubject.create(); + + PublishSubject<Integer> open = PublishSubject.create(); + final PublishSubject<Integer> close = PublishSubject.create(); + + TestObserver<Observable<Integer>> to = new TestObserver<>(); + + source.window(open, new Function<Integer, Observable<Integer>>() { + @Override + public Observable<Integer> apply(Integer t) { + return close; + } + }) + .doOnNext(new Consumer<Observable<Integer>>() { + @Override + public void accept(Observable<Integer> w) throws Throwable { + w.subscribe(Functions.emptyConsumer(), Functions.emptyConsumer()); // avoid abandonment + } + }) + .subscribe(to); + + open.onNext(1); + source.onNext(1); + + assertTrue(open.hasObservers()); + assertTrue(close.hasObservers()); + + close.onNext(1); + + assertFalse(close.hasObservers()); + + source.onComplete(); + + to.assertComplete(); + to.assertNoErrors(); + to.assertValueCount(1); + + // 2.0.2 - not anymore +// assertTrue("Not cancelled!", ts.isCancelled()); + assertFalse(open.hasObservers()); + assertFalse(close.hasObservers()); + } + + @Test + public void unsubscribeAll() { + PublishSubject<Integer> source = PublishSubject.create(); + + PublishSubject<Integer> open = PublishSubject.create(); + final PublishSubject<Integer> close = PublishSubject.create(); + + TestObserver<Observable<Integer>> to = new TestObserver<>(); + + source.window(open, new Function<Integer, Observable<Integer>>() { + @Override + public Observable<Integer> apply(Integer t) { + return close; + } + }) + .doOnNext(new Consumer<Observable<Integer>>() { + @Override + public void accept(Observable<Integer> w) throws Throwable { + w.subscribe(Functions.emptyConsumer(), Functions.emptyConsumer()); // avoid abandonment + } + }) + .subscribe(to); + + open.onNext(1); + + assertTrue(open.hasObservers()); + assertTrue(close.hasObservers()); + + to.dispose(); + + // Disposing the outer sequence stops the opening of new windows + assertFalse(open.hasObservers()); + // FIXME subject has subscribers because of the open window + assertTrue(close.hasObservers()); + } + + @Test + public void boundarySelectorNormal() { + PublishSubject<Integer> source = PublishSubject.create(); + PublishSubject<Integer> start = PublishSubject.create(); + final PublishSubject<Integer> end = PublishSubject.create(); + + TestObserver<Integer> to = source.window(start, new Function<Integer, ObservableSource<Integer>>() { + @Override + public ObservableSource<Integer> apply(Integer v) throws Exception { + return end; + } + }) + .flatMap(Functions.<Observable<Integer>>identity()) + .test(); + + start.onNext(0); + + source.onNext(1); + source.onNext(2); + source.onNext(3); + source.onNext(4); + + start.onNext(1); + + source.onNext(5); + source.onNext(6); + + end.onNext(1); + + start.onNext(2); + + TestHelper.emit(source, 7, 8); + + to.assertResult(1, 2, 3, 4, 5, 5, 6, 6, 7, 8); + } + + @Test + public void startError() { + PublishSubject<Integer> source = PublishSubject.create(); + PublishSubject<Integer> start = PublishSubject.create(); + final PublishSubject<Integer> end = PublishSubject.create(); + + TestObserver<Integer> to = source.window(start, new Function<Integer, ObservableSource<Integer>>() { + @Override + public ObservableSource<Integer> apply(Integer v) throws Exception { + return end; + } + }) + .flatMap(Functions.<Observable<Integer>>identity()) + .test(); + + start.onError(new TestException()); + + to.assertFailure(TestException.class); + + assertFalse("Source has observers!", source.hasObservers()); + assertFalse("Start has observers!", start.hasObservers()); + assertFalse("End has observers!", end.hasObservers()); + } + + @Test + @SuppressUndeliverable + public void endError() { + PublishSubject<Integer> source = PublishSubject.create(); + PublishSubject<Integer> start = PublishSubject.create(); + final PublishSubject<Integer> end = PublishSubject.create(); + + TestObserver<Integer> to = source.window(start, new Function<Integer, ObservableSource<Integer>>() { + @Override + public ObservableSource<Integer> apply(Integer v) throws Exception { + return end; + } + }) + .flatMap(Functions.<Observable<Integer>>identity()) + .test(); + + start.onNext(1); + end.onError(new TestException()); + + to.assertFailure(TestException.class); + + assertFalse("Source has observers!", source.hasObservers()); + assertFalse("Start has observers!", start.hasObservers()); + assertFalse("End has observers!", end.hasObservers()); + } + + @Test + public void dispose() { + TestHelper.checkDisposed(Observable.just(1).window(Observable.just(2), Functions.justFunction(Observable.never()))); + } + + @Test + public void reentrant() { + final Subject<Integer> ps = PublishSubject.<Integer>create(); + + TestObserver<Integer> to = new TestObserver<Integer>() { + @Override + public void onNext(Integer t) { + super.onNext(t); + if (t == 1) { + ps.onNext(2); + ps.onComplete(); + } + } + }; + + ps.window(BehaviorSubject.createDefault(1), Functions.justFunction(Observable.never())) + .flatMap(new Function<Observable<Integer>, ObservableSource<Integer>>() { + @Override + public ObservableSource<Integer> apply(Observable<Integer> v) throws Exception { + return v; + } + }) + .subscribe(to); + + ps.onNext(1); + + to + .awaitDone(1, TimeUnit.SECONDS) + .assertResult(1, 2); + } + + @Test + public void badSourceCallable() { + TestHelper.checkBadSourceObservable(new Function<Observable<Object>, Object>() { + @Override + public Object apply(Observable<Object> o) throws Exception { + return o.window(Observable.just(1), Functions.justFunction(Observable.never())); + } + }, false, 1, 1, (Object[])null); + } + + @Test + public void windowCloseIngoresCancel() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + BehaviorSubject.createDefault(1) + .window(BehaviorSubject.createDefault(1), new Function<Integer, Observable<Integer>>() { + @Override + public Observable<Integer> apply(Integer f) throws Exception { + return new Observable<Integer>() { + @Override + protected void subscribeActual( + Observer<? super Integer> observer) { + observer.onSubscribe(Disposable.empty()); + observer.onNext(1); + observer.onNext(2); + observer.onError(new TestException()); + } + }; + } + }) + .doOnNext(new Consumer<Observable<Integer>>() { + @Override + public void accept(Observable<Integer> w) throws Throwable { + w.subscribe(Functions.emptyConsumer(), Functions.emptyConsumer()); // avoid abandonment + } + }) + .test() + .assertValueCount(1) + .assertNoErrors() + .assertNotComplete(); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + static Observable<Integer> observableDisposed(final AtomicBoolean ref) { + return Observable.just(1).concatWith(Observable.<Integer>never()) + .doOnDispose(new Action() { + @Override + public void run() throws Exception { + ref.set(true); + } + }); + } + + @Test + public void mainAndBoundaryDisposeOnNoWindows() { + AtomicBoolean mainDisposed = new AtomicBoolean(); + AtomicBoolean openDisposed = new AtomicBoolean(); + final AtomicBoolean closeDisposed = new AtomicBoolean(); + + observableDisposed(mainDisposed) + .window(observableDisposed(openDisposed), new Function<Integer, ObservableSource<Integer>>() { + @Override + public ObservableSource<Integer> apply(Integer v) throws Exception { + return observableDisposed(closeDisposed); + } + }) + .doOnNext(new Consumer<Observable<Integer>>() { + @Override + public void accept(Observable<Integer> w) throws Throwable { + w.subscribe(Functions.emptyConsumer(), Functions.emptyConsumer()); // avoid abandonment + } + }) + .to(TestHelper.<Observable<Integer>>testConsumer()) + .assertSubscribed() + .assertNoErrors() + .assertNotComplete() + .dispose(); + + assertTrue(mainDisposed.get()); + assertTrue(openDisposed.get()); + assertTrue(closeDisposed.get()); + } + + @Test + public void cancellingWindowCancelsUpstream() { + PublishSubject<Integer> ps = PublishSubject.create(); + + TestObserver<Integer> to = ps.window(Observable.just(1).concatWith(Observable.<Integer>never()), Functions.justFunction(Observable.never())) + .take(1) + .flatMap(new Function<Observable<Integer>, Observable<Integer>>() { + @Override + public Observable<Integer> apply(Observable<Integer> w) throws Throwable { + return w.take(1); + } + }) + .test(); + + assertTrue(ps.hasObservers()); + + ps.onNext(1); + + to + .assertResult(1); + + assertFalse("Subject still has observers!", ps.hasObservers()); + } + + @Test + public void windowAbandonmentCancelsUpstream() { + PublishSubject<Integer> ps = PublishSubject.create(); + + final AtomicReference<Observable<Integer>> inner = new AtomicReference<>(); + + TestObserver<Observable<Integer>> to = ps.window(Observable.<Integer>just(1).concatWith(Observable.<Integer>never()), + Functions.justFunction(Observable.never())) + .doOnNext(new Consumer<Observable<Integer>>() { + @Override + public void accept(Observable<Integer> v) throws Throwable { + inner.set(v); + } + }) + .test(); + + assertTrue(ps.hasObservers()); + + to + .assertValueCount(1) + ; + + ps.onNext(1); + + assertTrue(ps.hasObservers()); + + to.dispose(); + + to + .assertValueCount(1) + .assertNoErrors() + .assertNotComplete(); + + assertFalse("Subject still has observers!", ps.hasObservers()); + + inner.get().test().assertResult(); + } + + @Test + public void closingIndicatorFunctionCrash() { + + PublishSubject<Integer> source = PublishSubject.create(); + PublishSubject<Integer> boundary = PublishSubject.create(); + + TestObserver<Observable<Integer>> to = source.window(boundary, new Function<Integer, Observable<Object>>() { + @Override + public Observable<Object> apply(Integer end) throws Throwable { + throw new TestException(); + } + }) + .test() + ; + + to.assertEmpty(); + + boundary.onNext(1); + + to.assertFailure(TestException.class); + + assertFalse(source.hasObservers()); + assertFalse(boundary.hasObservers()); + } + + @Test + public void mainError() { + Observable.error(new TestException()) + .window(Observable.never(), Functions.justFunction(Observable.never())) + .test() + .assertFailure(TestException.class); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeObservable(o -> o.window(Observable.never(), v -> Observable.never())); + } + + @Test + public void openError() throws Throwable { + TestHelper.withErrorTracking(errors -> { + TestException ex1 = new TestException(); + TestException ex2 = new TestException(); + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + AtomicReference<Observer<? super Integer>> ref1 = new AtomicReference<>(); + AtomicReference<Observer<? super Integer>> ref2 = new AtomicReference<>(); + + Observable<Integer> o1 = Observable.<Integer>unsafeCreate(ref1::set); + Observable<Integer> o2 = Observable.<Integer>unsafeCreate(ref2::set); + + TestObserver<Observable<Integer>> to = BehaviorSubject.createDefault(1) + .window(o1, v -> o2) + .doOnNext(w -> w.test()) + .test(); + + ref1.get().onSubscribe(Disposable.empty()); + ref1.get().onNext(1); + ref2.get().onSubscribe(Disposable.empty()); + + TestHelper.race( + () -> ref1.get().onError(ex1), + () -> ref2.get().onError(ex2) + ); + + to.assertError(RuntimeException.class); + + if (!errors.isEmpty()) { + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } + + errors.clear(); + } + }); + } + + @Test + public void closeError() throws Throwable { + TestHelper.withErrorTracking(errors -> { + AtomicReference<Observer<? super Integer>> ref1 = new AtomicReference<>(); + AtomicReference<Observer<? super Integer>> ref2 = new AtomicReference<>(); + + Observable<Integer> o1 = Observable.<Integer>unsafeCreate(ref1::set); + Observable<Integer> o2 = Observable.<Integer>unsafeCreate(ref2::set); + + TestObserver<Integer> to = BehaviorSubject.createDefault(1) + .window(o1, v -> o2) + .flatMap(v -> v) + .test(); + + ref1.get().onSubscribe(Disposable.empty()); + ref1.get().onNext(1); + ref2.get().onSubscribe(Disposable.empty()); + + ref2.get().onError(new TestException()); + ref2.get().onError(new TestException()); + + to.assertFailure(TestException.class); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + }); + } + + @Test + public void upstreamFailsBeforeFirstWindow() { + Observable.error(new TestException()) + .window(Observable.never(), v -> Observable.never()) + .test() + .assertFailure(TestException.class); + } + + @Test + public void windowOpenMainCompletes() { + AtomicReference<Observer<? super Integer>> ref1 = new AtomicReference<>(); + + PublishSubject<Object> ps = PublishSubject.create(); + Observable<Integer> o1 = Observable.<Integer>unsafeCreate(ref1::set); + + AtomicInteger counter = new AtomicInteger(); + + TestObserver<Observable<Object>> to = ps + .window(o1, v -> Observable.never()) + .doOnNext(w -> { + if (counter.getAndIncrement() == 0) { + ref1.get().onNext(2); + ps.onNext(1); + ps.onComplete(); + } + w.test(); + }) + .test(); + + ref1.get().onSubscribe(Disposable.empty()); + ref1.get().onNext(1); + + to.assertComplete(); + } + + @Test + public void windowOpenMainError() { + AtomicReference<Observer<? super Integer>> ref1 = new AtomicReference<>(); + + PublishSubject<Object> ps = PublishSubject.create(); + Observable<Integer> o1 = Observable.<Integer>unsafeCreate(ref1::set); + + AtomicInteger counter = new AtomicInteger(); + + TestObserver<Observable<Object>> to = ps + .window(o1, v -> Observable.never()) + .doOnNext(w -> { + if (counter.getAndIncrement() == 0) { + ref1.get().onNext(2); + ps.onNext(1); + ps.onError(new TestException()); + } + w.test(); + }) + .test(); + + ref1.get().onSubscribe(Disposable.empty()); + ref1.get().onNext(1); + + to.assertError(TestException.class); + } + + @Test + public void windowOpenIgnoresDispose() { + AtomicReference<Observer<? super Integer>> ref1 = new AtomicReference<>(); + + PublishSubject<Object> ps = PublishSubject.create(); + Observable<Integer> o1 = Observable.<Integer>unsafeCreate(ref1::set); + + TestObserver<Observable<Object>> to = ps + .window(o1, v -> Observable.never()) + .take(1) + .doOnNext(w -> { + w.test(); + }) + .test(); + + ref1.get().onSubscribe(Disposable.empty()); + ref1.get().onNext(1); + ref1.get().onNext(2); + + to.assertValueCount(1); + } + + @Test + public void mainIgnoresCancelBeforeOnError() throws Throwable { + TestHelper.withErrorTracking(errors -> { + Observable.unsafeCreate(s -> { + s.onSubscribe(Disposable.empty()); + s.onNext(1); + s.onError(new IOException()); + }) + .window(BehaviorSubject.createDefault(1), v -> Observable.error(new TestException())) + .doOnNext(w -> w.test()) + .test() + .assertError(TestException.class); + + TestHelper.assertUndeliverable(errors, 0, IOException.class); + }); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableWindowWithTimeTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableWindowWithTimeTest.java new file mode 100644 index 0000000000..ca8f90d04f --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableWindowWithTimeTest.java @@ -0,0 +1,1131 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import static org.junit.Assert.*; + +import java.util.*; +import java.util.concurrent.*; +import java.util.concurrent.atomic.*; + +import org.junit.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.core.Observer; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.observers.*; +import io.reactivex.rxjava3.schedulers.*; +import io.reactivex.rxjava3.subjects.*; +import io.reactivex.rxjava3.testsupport.*; + +public class ObservableWindowWithTimeTest extends RxJavaTest { + + private TestScheduler scheduler; + private Scheduler.Worker innerScheduler; + + @Before + public void before() { + scheduler = new TestScheduler(); + innerScheduler = scheduler.createWorker(); + } + + @Test + public void timedAndCount() { + final List<String> list = new ArrayList<>(); + final List<List<String>> lists = new ArrayList<>(); + + Observable<String> source = Observable.unsafeCreate(new ObservableSource<String>() { + @Override + public void subscribe(Observer<? super String> observer) { + observer.onSubscribe(Disposable.empty()); + push(observer, "one", 10); + push(observer, "two", 90); + push(observer, "three", 110); + push(observer, "four", 190); + push(observer, "five", 210); + complete(observer, 250); + } + }); + + Observable<Observable<String>> windowed = source.window(100, TimeUnit.MILLISECONDS, scheduler, 2); + windowed.subscribe(observeWindow(list, lists)); + + scheduler.advanceTimeTo(95, TimeUnit.MILLISECONDS); + assertEquals(1, lists.size()); + assertEquals(lists.get(0), list("one", "two")); + + scheduler.advanceTimeTo(195, TimeUnit.MILLISECONDS); + assertEquals(3, lists.size()); + assertTrue(lists.get(1).isEmpty()); + assertEquals(lists.get(2), list("three", "four")); + + scheduler.advanceTimeTo(300, TimeUnit.MILLISECONDS); + assertEquals(5, lists.size()); + assertTrue(lists.get(3).isEmpty()); + assertEquals(lists.get(4), list("five")); + } + + @Test + public void timed() { + final List<String> list = new ArrayList<>(); + final List<List<String>> lists = new ArrayList<>(); + + Observable<String> source = Observable.unsafeCreate(new ObservableSource<String>() { + @Override + public void subscribe(Observer<? super String> observer) { + observer.onSubscribe(Disposable.empty()); + push(observer, "one", 98); + push(observer, "two", 99); + push(observer, "three", 99); // FIXME happens after the window is open + push(observer, "four", 101); + push(observer, "five", 102); + complete(observer, 150); + } + }); + + Observable<Observable<String>> windowed = source.window(100, TimeUnit.MILLISECONDS, scheduler); + windowed.subscribe(observeWindow(list, lists)); + + scheduler.advanceTimeTo(101, TimeUnit.MILLISECONDS); + assertEquals(1, lists.size()); + assertEquals(lists.get(0), list("one", "two", "three")); + + scheduler.advanceTimeTo(201, TimeUnit.MILLISECONDS); + assertEquals(2, lists.size()); + assertEquals(lists.get(1), list("four", "five")); + } + + private List<String> list(String... args) { + List<String> list = new ArrayList<>(); + for (String arg : args) { + list.add(arg); + } + return list; + } + + private <T> void push(final Observer<T> observer, final T value, int delay) { + innerScheduler.schedule(new Runnable() { + @Override + public void run() { + observer.onNext(value); + } + }, delay, TimeUnit.MILLISECONDS); + } + + private void complete(final Observer<?> observer, int delay) { + innerScheduler.schedule(new Runnable() { + @Override + public void run() { + observer.onComplete(); + } + }, delay, TimeUnit.MILLISECONDS); + } + + private <T> Consumer<Observable<T>> observeWindow(final List<T> list, final List<List<T>> lists) { + return new Consumer<Observable<T>>() { + @Override + public void accept(Observable<T> stringObservable) { + stringObservable.subscribe(new DefaultObserver<T>() { + @Override + public void onComplete() { + lists.add(new ArrayList<>(list)); + list.clear(); + } + + @Override + public void onError(Throwable e) { + Assert.fail(e.getMessage()); + } + + @Override + public void onNext(T args) { + list.add(args); + } + }); + } + }; + } + + @Test + public void exactWindowSize() { + Observable<Observable<Integer>> source = Observable.range(1, 10) + .window(1, TimeUnit.MINUTES, scheduler, 3); + + final List<Integer> list = new ArrayList<>(); + final List<List<Integer>> lists = new ArrayList<>(); + + source.subscribe(observeWindow(list, lists)); + + assertEquals(4, lists.size()); + assertEquals(3, lists.get(0).size()); + assertEquals(Arrays.asList(1, 2, 3), lists.get(0)); + assertEquals(3, lists.get(1).size()); + assertEquals(Arrays.asList(4, 5, 6), lists.get(1)); + assertEquals(3, lists.get(2).size()); + assertEquals(Arrays.asList(7, 8, 9), lists.get(2)); + assertEquals(1, lists.get(3).size()); + assertEquals(Arrays.asList(10), lists.get(3)); + } + + @Test + public void takeFlatMapCompletes() { + TestObserver<Integer> to = new TestObserver<>(); + + final AtomicInteger wip = new AtomicInteger(); + + final int indicator = 999999999; + + ObservableWindowWithSizeTest.hotStream() + .window(300, TimeUnit.MILLISECONDS) + .take(10) + .doOnComplete(new Action() { + @Override + public void run() { + System.out.println("Main done!"); + } + }) + .flatMap(new Function<Observable<Integer>, Observable<Integer>>() { + @Override + public Observable<Integer> apply(Observable<Integer> w) { + return w.startWithItem(indicator) + .doOnComplete(new Action() { + @Override + public void run() { + System.out.println("inner done: " + wip.incrementAndGet()); + } + }) + ; + } + }) + .doOnNext(new Consumer<Integer>() { + @Override + public void accept(Integer pv) { + System.out.println(pv); + } + }) + .subscribe(to); + + to.awaitDone(5, TimeUnit.SECONDS); + to.assertComplete(); + Assert.assertTrue(to.values().size() != 0); + } + + @Test + public void timespanTimeskipDefaultScheduler() { + Observable.just(1) + .window(1, 1, TimeUnit.MINUTES) + .flatMap(Functions.<Observable<Integer>>identity()) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(1); + } + + @Test + public void timespanTimeskipCustomScheduler() { + Observable.just(1) + .window(1, 1, TimeUnit.MINUTES, Schedulers.io()) + .flatMap(Functions.<Observable<Integer>>identity()) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(1); + } + + @Test + public void timespanTimeskipCustomSchedulerBufferSize() { + Observable.range(1, 10) + .window(1, 1, TimeUnit.MINUTES, Schedulers.io(), 2) + .flatMap(Functions.<Observable<Integer>>identity()) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + } + + @Test + public void timespanDefaultSchedulerSize() { + Observable.range(1, 10) + .window(1, TimeUnit.MINUTES, 20) + .flatMap(Functions.<Observable<Integer>>identity()) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + } + + @Test + public void timespanDefaultSchedulerSizeRestart() { + Observable.range(1, 10) + .window(1, TimeUnit.MINUTES, 20, true) + .flatMap(Functions.<Observable<Integer>>identity(), true) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + } + + @Test + public void invalidSpan() { + try { + Observable.just(1).window(-99, 1, TimeUnit.SECONDS); + fail("Should have thrown!"); + } catch (IllegalArgumentException ex) { + assertEquals("timespan > 0 required but it was -99", ex.getMessage()); + } + } + + @Test + public void timeskipJustOverlap() { + Observable.just(1) + .window(2, 1, TimeUnit.MINUTES, Schedulers.single()) + .flatMap(Functions.<Observable<Integer>>identity()) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(1); + } + + @Test + public void timeskipJustSkip() { + Observable.just(1) + .window(1, 2, TimeUnit.MINUTES, Schedulers.single()) + .flatMap(Functions.<Observable<Integer>>identity()) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(1); + } + + @Test + public void timeskipSkipping() { + TestScheduler scheduler = new TestScheduler(); + + PublishSubject<Integer> ps = PublishSubject.create(); + + TestObserver<Integer> to = ps.window(1, 2, TimeUnit.SECONDS, scheduler) + .flatMap(Functions.<Observable<Integer>>identity()) + .test(); + + ps.onNext(1); + ps.onNext(2); + + scheduler.advanceTimeBy(1, TimeUnit.SECONDS); + + ps.onNext(3); + ps.onNext(4); + + scheduler.advanceTimeBy(1, TimeUnit.SECONDS); + + ps.onNext(5); + ps.onNext(6); + + scheduler.advanceTimeBy(1, TimeUnit.SECONDS); + + ps.onNext(7); + ps.onComplete(); + + to.assertResult(1, 2, 5, 6); + } + + @Test + public void timeskipOverlapping() { + TestScheduler scheduler = new TestScheduler(); + + PublishSubject<Integer> ps = PublishSubject.create(); + + TestObserver<Integer> to = ps.window(2, 1, TimeUnit.SECONDS, scheduler) + .flatMap(Functions.<Observable<Integer>>identity()) + .test(); + + ps.onNext(1); + ps.onNext(2); + + scheduler.advanceTimeBy(1, TimeUnit.SECONDS); + + ps.onNext(3); + ps.onNext(4); + + scheduler.advanceTimeBy(1, TimeUnit.SECONDS); + + ps.onNext(5); + ps.onNext(6); + + scheduler.advanceTimeBy(1, TimeUnit.SECONDS); + + ps.onNext(7); + ps.onComplete(); + + to.assertResult(1, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7); + } + + @Test + @SuppressUndeliverable + public void exactOnError() { + TestScheduler scheduler = new TestScheduler(); + + PublishSubject<Integer> ps = PublishSubject.create(); + + TestObserver<Integer> to = ps.window(1, 1, TimeUnit.SECONDS, scheduler) + .flatMap(Functions.<Observable<Integer>>identity()) + .test(); + + ps.onError(new TestException()); + + to.assertFailure(TestException.class); + } + + @Test + @SuppressUndeliverable + public void overlappingOnError() { + TestScheduler scheduler = new TestScheduler(); + + PublishSubject<Integer> ps = PublishSubject.create(); + + TestObserver<Integer> to = ps.window(2, 1, TimeUnit.SECONDS, scheduler) + .flatMap(Functions.<Observable<Integer>>identity()) + .test(); + + ps.onError(new TestException()); + + to.assertFailure(TestException.class); + } + + @Test + @SuppressUndeliverable + public void skipOnError() { + TestScheduler scheduler = new TestScheduler(); + + PublishSubject<Integer> ps = PublishSubject.create(); + + TestObserver<Integer> to = ps.window(1, 2, TimeUnit.SECONDS, scheduler) + .flatMap(Functions.<Observable<Integer>>identity()) + .test(); + + ps.onError(new TestException()); + + to.assertFailure(TestException.class); + } + + @Test + public void dispose() { + TestHelper.checkDisposed(Observable.range(1, 5).window(1, TimeUnit.DAYS, Schedulers.single())); + + TestHelper.checkDisposed(Observable.range(1, 5).window(2, 1, TimeUnit.DAYS, Schedulers.single())); + + TestHelper.checkDisposed(Observable.range(1, 5).window(1, 2, TimeUnit.DAYS, Schedulers.single())); + + TestHelper.checkDisposed(Observable.never() + .window(1, TimeUnit.DAYS, Schedulers.single(), 2, true)); + } + + @Test + public void restartTimer() { + Observable.range(1, 5) + .window(1, TimeUnit.DAYS, Schedulers.single(), 2, true) + .flatMap(Functions.<Observable<Integer>>identity()) + .test() + .assertResult(1, 2, 3, 4, 5); + } + + @Test + @SuppressUndeliverable + public void exactBoundaryError() { + Observable.error(new TestException()) + .window(1, TimeUnit.DAYS, Schedulers.single(), 2, true) + .to(TestHelper.<Observable<Object>>testConsumer()) + .assertSubscribed() + .assertError(TestException.class) + .assertNotComplete(); + } + + @Test + public void restartTimerMany() { + Observable.intervalRange(1, 1000, 1, 1, TimeUnit.MILLISECONDS) + .window(1, TimeUnit.MILLISECONDS, Schedulers.single(), 2, true) + .flatMap(Functions.<Observable<Long>>identity()) + .take(500) + .to(TestHelper.<Long>testConsumer()) + .awaitDone(5, TimeUnit.SECONDS) + .assertSubscribed() + .assertValueCount(500) + .assertNoErrors() + .assertComplete(); + } + + @Test + public void exactUnboundedReentrant() { + TestScheduler scheduler = new TestScheduler(); + + final Subject<Integer> ps = PublishSubject.<Integer>create(); + + TestObserver<Integer> to = new TestObserver<Integer>() { + @Override + public void onNext(Integer t) { + super.onNext(t); + if (t == 1) { + ps.onNext(2); + ps.onComplete(); + } + } + }; + + ps.window(1, TimeUnit.MILLISECONDS, scheduler) + .flatMap(new Function<Observable<Integer>, ObservableSource<Integer>>() { + @Override + public ObservableSource<Integer> apply(Observable<Integer> v) throws Exception { + return v; + } + }) + .subscribe(to); + + ps.onNext(1); + + to + .awaitDone(1, TimeUnit.SECONDS) + .assertResult(1, 2); + } + + @Test + public void exactBoundedReentrant() { + TestScheduler scheduler = new TestScheduler(); + + final Subject<Integer> ps = PublishSubject.<Integer>create(); + + TestObserver<Integer> to = new TestObserver<Integer>() { + @Override + public void onNext(Integer t) { + super.onNext(t); + if (t == 1) { + ps.onNext(2); + ps.onComplete(); + } + } + }; + + ps.window(1, TimeUnit.MILLISECONDS, scheduler, 10, true) + .flatMap(new Function<Observable<Integer>, ObservableSource<Integer>>() { + @Override + public ObservableSource<Integer> apply(Observable<Integer> v) throws Exception { + return v; + } + }) + .subscribe(to); + + ps.onNext(1); + + to + .awaitDone(1, TimeUnit.SECONDS) + .assertResult(1, 2); + } + + @Test + public void exactBoundedReentrant2() { + TestScheduler scheduler = new TestScheduler(); + + final Subject<Integer> ps = PublishSubject.<Integer>create(); + + TestObserver<Integer> to = new TestObserver<Integer>() { + @Override + public void onNext(Integer t) { + super.onNext(t); + if (t == 1) { + ps.onNext(2); + ps.onComplete(); + } + } + }; + + ps.window(1, TimeUnit.MILLISECONDS, scheduler, 2, true) + .flatMap(new Function<Observable<Integer>, ObservableSource<Integer>>() { + @Override + public ObservableSource<Integer> apply(Observable<Integer> v) throws Exception { + return v; + } + }) + .subscribe(to); + + ps.onNext(1); + + to + .awaitDone(1, TimeUnit.SECONDS) + .assertResult(1, 2); + } + + @Test + public void skipReentrant() { + TestScheduler scheduler = new TestScheduler(); + + final Subject<Integer> ps = PublishSubject.<Integer>create(); + + TestObserver<Integer> to = new TestObserver<Integer>() { + @Override + public void onNext(Integer t) { + super.onNext(t); + if (t == 1) { + ps.onNext(2); + ps.onComplete(); + } + } + }; + + ps.window(1, 2, TimeUnit.MILLISECONDS, scheduler) + .flatMap(new Function<Observable<Integer>, ObservableSource<Integer>>() { + @Override + public ObservableSource<Integer> apply(Observable<Integer> v) throws Exception { + return v; + } + }) + .subscribe(to); + + ps.onNext(1); + + to + .awaitDone(1, TimeUnit.SECONDS) + .assertResult(1, 2); + } + + @Test + public void sizeTimeTimeout() { + TestScheduler scheduler = new TestScheduler(); + Subject<Integer> ps = PublishSubject.<Integer>create(); + + TestObserver<Observable<Integer>> to = ps.window(5, TimeUnit.MILLISECONDS, scheduler, 100) + .test() + .assertValueCount(1); + + scheduler.advanceTimeBy(5, TimeUnit.MILLISECONDS); + + to.assertValueCount(2) + .assertNoErrors() + .assertNotComplete(); + + to.values().get(0).test().assertResult(); + } + + @Test + public void periodicWindowCompletion() { + TestScheduler scheduler = new TestScheduler(); + Subject<Integer> ps = PublishSubject.<Integer>create(); + + TestObserver<Observable<Integer>> to = ps.window(5, TimeUnit.MILLISECONDS, scheduler, Long.MAX_VALUE, false) + .test(); + + scheduler.advanceTimeBy(100, TimeUnit.MILLISECONDS); + + to.assertValueCount(21) + .assertNoErrors() + .assertNotComplete(); + } + + @Test + public void periodicWindowCompletionRestartTimer() { + TestScheduler scheduler = new TestScheduler(); + Subject<Integer> ps = PublishSubject.<Integer>create(); + + TestObserver<Observable<Integer>> to = ps.window(5, TimeUnit.MILLISECONDS, scheduler, Long.MAX_VALUE, true) + .test(); + + scheduler.advanceTimeBy(100, TimeUnit.MILLISECONDS); + + to.assertValueCount(21) + .assertNoErrors() + .assertNotComplete(); + } + + @Test + public void periodicWindowCompletionBounded() { + TestScheduler scheduler = new TestScheduler(); + Subject<Integer> ps = PublishSubject.<Integer>create(); + + TestObserver<Observable<Integer>> to = ps.window(5, TimeUnit.MILLISECONDS, scheduler, 5, false) + .test(); + + scheduler.advanceTimeBy(100, TimeUnit.MILLISECONDS); + + to.assertValueCount(21) + .assertNoErrors() + .assertNotComplete(); + } + + @Test + public void periodicWindowCompletionRestartTimerBounded() { + TestScheduler scheduler = new TestScheduler(); + Subject<Integer> ps = PublishSubject.<Integer>create(); + + TestObserver<Observable<Integer>> to = ps.window(5, TimeUnit.MILLISECONDS, scheduler, 5, true) + .test(); + + scheduler.advanceTimeBy(100, TimeUnit.MILLISECONDS); + + to.assertValueCount(21) + .assertNoErrors() + .assertNotComplete(); + } + + @Test + public void periodicWindowCompletionRestartTimerBoundedSomeData() { + TestScheduler scheduler = new TestScheduler(); + Subject<Integer> ps = PublishSubject.<Integer>create(); + + TestObserver<Observable<Integer>> to = ps.window(5, TimeUnit.MILLISECONDS, scheduler, 2, true) + .test(); + + ps.onNext(1); + ps.onNext(2); + + scheduler.advanceTimeBy(100, TimeUnit.MILLISECONDS); + + to.assertValueCount(22) + .assertNoErrors() + .assertNotComplete(); + } + + @Test + public void countRestartsOnTimeTick() { + TestScheduler scheduler = new TestScheduler(); + Subject<Integer> ps = PublishSubject.<Integer>create(); + + TestObserver<Observable<Integer>> to = ps.window(5, TimeUnit.MILLISECONDS, scheduler, 5, true) + .test(); + + // window #1 + ps.onNext(1); + ps.onNext(2); + + scheduler.advanceTimeBy(5, TimeUnit.MILLISECONDS); + + // window #2 + ps.onNext(3); + ps.onNext(4); + ps.onNext(5); + ps.onNext(6); + + to.assertValueCount(2) + .assertNoErrors() + .assertNotComplete(); + } + + @Test + public void exactTimeBoundNoInterruptWindowOutputOnComplete() throws Exception { + final AtomicBoolean isInterrupted = new AtomicBoolean(); + + final PublishSubject<Integer> ps = PublishSubject.create(); + + final CountDownLatch doOnNextDone = new CountDownLatch(1); + final CountDownLatch secondWindowProcessing = new CountDownLatch(1); + + ps.window(100, TimeUnit.MILLISECONDS) + .doOnNext(new Consumer<Observable<Integer>>() { + int count; + @Override + public void accept(Observable<Integer> v) throws Exception { + System.out.println(Thread.currentThread()); + if (count++ == 1) { + secondWindowProcessing.countDown(); + try { + Thread.sleep(200); + isInterrupted.set(Thread.interrupted()); + } catch (InterruptedException ex) { + isInterrupted.set(true); + } + doOnNextDone.countDown(); + } + } + }) + .test(); + + ps.onNext(1); + + assertTrue(secondWindowProcessing.await(5, TimeUnit.SECONDS)); + + ps.onComplete(); + + assertTrue(doOnNextDone.await(5, TimeUnit.SECONDS)); + + assertFalse("The doOnNext got interrupted!", isInterrupted.get()); + } + + @Test + @SuppressUndeliverable + public void exactTimeBoundNoInterruptWindowOutputOnError() throws Exception { + final AtomicBoolean isInterrupted = new AtomicBoolean(); + + final PublishSubject<Integer> ps = PublishSubject.create(); + + final CountDownLatch doOnNextDone = new CountDownLatch(1); + final CountDownLatch secondWindowProcessing = new CountDownLatch(1); + + ps.window(100, TimeUnit.MILLISECONDS) + .doOnNext(new Consumer<Observable<Integer>>() { + int count; + @Override + public void accept(Observable<Integer> v) throws Exception { + System.out.println(Thread.currentThread()); + if (count++ == 1) { + secondWindowProcessing.countDown(); + try { + Thread.sleep(200); + isInterrupted.set(Thread.interrupted()); + } catch (InterruptedException ex) { + isInterrupted.set(true); + } + doOnNextDone.countDown(); + } + } + }) + .test(); + + ps.onNext(1); + + assertTrue(secondWindowProcessing.await(5, TimeUnit.SECONDS)); + + ps.onError(new TestException()); + + assertTrue(doOnNextDone.await(5, TimeUnit.SECONDS)); + + assertFalse("The doOnNext got interrupted!", isInterrupted.get()); + } + + @Test + public void exactTimeAndSizeBoundNoInterruptWindowOutputOnComplete() throws Exception { + final AtomicBoolean isInterrupted = new AtomicBoolean(); + + final PublishSubject<Integer> ps = PublishSubject.create(); + + final CountDownLatch doOnNextDone = new CountDownLatch(1); + final CountDownLatch secondWindowProcessing = new CountDownLatch(1); + + ps.window(100, TimeUnit.MILLISECONDS, 10) + .doOnNext(new Consumer<Observable<Integer>>() { + int count; + @Override + public void accept(Observable<Integer> v) throws Exception { + System.out.println(Thread.currentThread()); + if (count++ == 1) { + secondWindowProcessing.countDown(); + try { + Thread.sleep(200); + isInterrupted.set(Thread.interrupted()); + } catch (InterruptedException ex) { + isInterrupted.set(true); + } + doOnNextDone.countDown(); + } + } + }) + .test(); + + ps.onNext(1); + + assertTrue(secondWindowProcessing.await(5, TimeUnit.SECONDS)); + + ps.onComplete(); + + assertTrue(doOnNextDone.await(5, TimeUnit.SECONDS)); + + assertFalse("The doOnNext got interrupted!", isInterrupted.get()); + } + + @Test + @SuppressUndeliverable + public void exactTimeAndSizeBoundNoInterruptWindowOutputOnError() throws Exception { + final AtomicBoolean isInterrupted = new AtomicBoolean(); + + final PublishSubject<Integer> ps = PublishSubject.create(); + + final CountDownLatch doOnNextDone = new CountDownLatch(1); + final CountDownLatch secondWindowProcessing = new CountDownLatch(1); + + ps.window(100, TimeUnit.MILLISECONDS, 10) + .doOnNext(new Consumer<Observable<Integer>>() { + int count; + @Override + public void accept(Observable<Integer> v) throws Exception { + System.out.println(Thread.currentThread()); + if (count++ == 1) { + secondWindowProcessing.countDown(); + try { + Thread.sleep(200); + isInterrupted.set(Thread.interrupted()); + } catch (InterruptedException ex) { + isInterrupted.set(true); + } + doOnNextDone.countDown(); + } + } + }) + .test(); + + ps.onNext(1); + + assertTrue(secondWindowProcessing.await(5, TimeUnit.SECONDS)); + + ps.onError(new TestException()); + + assertTrue(doOnNextDone.await(5, TimeUnit.SECONDS)); + + assertFalse("The doOnNext got interrupted!", isInterrupted.get()); + } + + @Test + public void skipTimeAndSizeBoundNoInterruptWindowOutputOnComplete() throws Exception { + final AtomicBoolean isInterrupted = new AtomicBoolean(); + + final PublishSubject<Integer> ps = PublishSubject.create(); + + final CountDownLatch doOnNextDone = new CountDownLatch(1); + final CountDownLatch secondWindowProcessing = new CountDownLatch(1); + + ps.window(90, 100, TimeUnit.MILLISECONDS) + .doOnNext(new Consumer<Observable<Integer>>() { + int count; + @Override + public void accept(Observable<Integer> v) throws Exception { + System.out.println(Thread.currentThread()); + if (count++ == 1) { + secondWindowProcessing.countDown(); + try { + Thread.sleep(200); + isInterrupted.set(Thread.interrupted()); + } catch (InterruptedException ex) { + isInterrupted.set(true); + } + doOnNextDone.countDown(); + } + } + }) + .test(); + + ps.onNext(1); + + assertTrue(secondWindowProcessing.await(5, TimeUnit.SECONDS)); + + ps.onComplete(); + + assertTrue(doOnNextDone.await(5, TimeUnit.SECONDS)); + + assertFalse("The doOnNext got interrupted!", isInterrupted.get()); + } + + @Test + @SuppressUndeliverable + public void skipTimeAndSizeBoundNoInterruptWindowOutputOnError() throws Exception { + final AtomicBoolean isInterrupted = new AtomicBoolean(); + + final PublishSubject<Integer> ps = PublishSubject.create(); + + final CountDownLatch doOnNextDone = new CountDownLatch(1); + final CountDownLatch secondWindowProcessing = new CountDownLatch(1); + + ps.window(90, 100, TimeUnit.MILLISECONDS) + .doOnNext(new Consumer<Observable<Integer>>() { + int count; + @Override + public void accept(Observable<Integer> v) throws Exception { + System.out.println(Thread.currentThread()); + if (count++ == 1) { + secondWindowProcessing.countDown(); + try { + Thread.sleep(200); + isInterrupted.set(Thread.interrupted()); + } catch (InterruptedException ex) { + isInterrupted.set(true); + } + doOnNextDone.countDown(); + } + } + }) + .test(); + + ps.onNext(1); + + assertTrue(secondWindowProcessing.await(5, TimeUnit.SECONDS)); + + ps.onError(new TestException()); + + assertTrue(doOnNextDone.await(5, TimeUnit.SECONDS)); + + assertFalse("The doOnNext got interrupted!", isInterrupted.get()); + } + + @Test + public void cancellingWindowCancelsUpstreamExactTime() { + PublishSubject<Integer> ps = PublishSubject.create(); + + TestObserver<Integer> to = ps.window(10, TimeUnit.MINUTES) + .take(1) + .flatMap(new Function<Observable<Integer>, Observable<Integer>>() { + @Override + public Observable<Integer> apply(Observable<Integer> w) throws Throwable { + return w.take(1); + } + }) + .test(); + + assertTrue(ps.hasObservers()); + + ps.onNext(1); + + to + .assertResult(1); + + assertFalse("Subject still has observers!", ps.hasObservers()); + } + + @Test + public void windowAbandonmentCancelsUpstreamExactTime() { + PublishSubject<Integer> ps = PublishSubject.create(); + + final AtomicReference<Observable<Integer>> inner = new AtomicReference<>(); + + TestObserver<Observable<Integer>> to = ps.window(10, TimeUnit.MINUTES) + .take(1) + .doOnNext(new Consumer<Observable<Integer>>() { + @Override + public void accept(Observable<Integer> v) throws Throwable { + inner.set(v); + } + }) + .test(); + + assertFalse("Subject still has observers!", ps.hasObservers()); + + to + .assertValueCount(1) + .assertNoErrors() + .assertComplete(); + + inner.get().test().assertResult(); + } + + @Test + public void cancellingWindowCancelsUpstreamExactTimeAndSize() { + PublishSubject<Integer> ps = PublishSubject.create(); + + TestObserver<Integer> to = ps.window(10, TimeUnit.MINUTES, 100) + .take(1) + .flatMap(new Function<Observable<Integer>, Observable<Integer>>() { + @Override + public Observable<Integer> apply(Observable<Integer> w) throws Throwable { + return w.take(1); + } + }) + .test(); + + assertTrue(ps.hasObservers()); + + ps.onNext(1); + + to + .assertResult(1); + + assertFalse("Subject still has observers!", ps.hasObservers()); + } + + @Test + public void windowAbandonmentCancelsUpstreamExactTimeAndSize() { + PublishSubject<Integer> ps = PublishSubject.create(); + + final AtomicReference<Observable<Integer>> inner = new AtomicReference<>(); + + TestObserver<Observable<Integer>> to = ps.window(10, TimeUnit.MINUTES, 100) + .take(1) + .doOnNext(new Consumer<Observable<Integer>>() { + @Override + public void accept(Observable<Integer> v) throws Throwable { + inner.set(v); + } + }) + .test(); + + assertFalse("Subject still has observers!", ps.hasObservers()); + + to + .assertValueCount(1) + .assertNoErrors() + .assertComplete(); + + inner.get().test().assertResult(); + } + + @Test + public void cancellingWindowCancelsUpstreamExactTimeSkip() { + PublishSubject<Integer> ps = PublishSubject.create(); + + TestObserver<Integer> to = ps.window(10, 15, TimeUnit.MINUTES) + .take(1) + .flatMap(new Function<Observable<Integer>, Observable<Integer>>() { + @Override + public Observable<Integer> apply(Observable<Integer> w) throws Throwable { + return w.take(1); + } + }) + .test(); + + assertTrue(ps.hasObservers()); + + ps.onNext(1); + + to + .assertResult(1); + + assertFalse("Subject still has observers!", ps.hasObservers()); + } + + @Test + public void windowAbandonmentCancelsUpstreamExactTimeSkip() { + PublishSubject<Integer> ps = PublishSubject.create(); + + final AtomicReference<Observable<Integer>> inner = new AtomicReference<>(); + + TestObserver<Observable<Integer>> to = ps.window(10, 15, TimeUnit.MINUTES) + .take(1) + .doOnNext(new Consumer<Observable<Integer>>() { + @Override + public void accept(Observable<Integer> v) throws Throwable { + inner.set(v); + } + }) + .test(); + + assertFalse("Subject still has observers!", ps.hasObservers()); + + to + .assertValueCount(1) + .assertNoErrors() + .assertComplete(); + + inner.get().test().assertResult(); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeObservable(o -> o.window(1, TimeUnit.SECONDS)); + } + + @Test + public void timedBoundarySignalAndDisposeRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + TestScheduler scheduler = new TestScheduler(); + + PublishSubject<Integer> ps = PublishSubject.create(); + + TestObserver<Observable<Integer>> to = ps.window(1, TimeUnit.MINUTES, scheduler, 1) + .test(); + + TestHelper.race( + () -> ps.onNext(1), + () -> to.dispose() + ); + } + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableWithLatestFromTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableWithLatestFromTest.java new file mode 100644 index 0000000000..cfe093ed6b --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableWithLatestFromTest.java @@ -0,0 +1,602 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +import java.util.*; + +import io.reactivex.rxjava3.disposables.Disposable; +import org.junit.Test; +import org.mockito.InOrder; + +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.core.Observer; +import io.reactivex.rxjava3.core.RxJavaTest; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.internal.util.CrashingMappedIterable; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.subjects.PublishSubject; +import io.reactivex.rxjava3.testsupport.*; + +public class ObservableWithLatestFromTest extends RxJavaTest { + static final BiFunction<Integer, Integer, Integer> COMBINER = new BiFunction<Integer, Integer, Integer>() { + @Override + public Integer apply(Integer t1, Integer t2) { + return (t1 << 8) + t2; + } + }; + static final BiFunction<Integer, Integer, Integer> COMBINER_ERROR = new BiFunction<Integer, Integer, Integer>() { + @Override + public Integer apply(Integer t1, Integer t2) { + throw new TestException("Forced failure"); + } + }; + @Test + public void simple() { + PublishSubject<Integer> source = PublishSubject.create(); + PublishSubject<Integer> other = PublishSubject.create(); + + Observer<Integer> o = TestHelper.mockObserver(); + InOrder inOrder = inOrder(o); + + Observable<Integer> result = source.withLatestFrom(other, COMBINER); + + result.subscribe(o); + + source.onNext(1); + inOrder.verify(o, never()).onNext(anyInt()); + + other.onNext(1); + inOrder.verify(o, never()).onNext(anyInt()); + + source.onNext(2); + inOrder.verify(o).onNext((2 << 8) + 1); + + other.onNext(2); + inOrder.verify(o, never()).onNext(anyInt()); + + other.onComplete(); + inOrder.verify(o, never()).onComplete(); + + source.onNext(3); + inOrder.verify(o).onNext((3 << 8) + 2); + + source.onComplete(); + inOrder.verify(o).onComplete(); + + verify(o, never()).onError(any(Throwable.class)); + } + + @Test + public void emptySource() { + PublishSubject<Integer> source = PublishSubject.create(); + PublishSubject<Integer> other = PublishSubject.create(); + + Observable<Integer> result = source.withLatestFrom(other, COMBINER); + + TestObserverEx<Integer> to = new TestObserverEx<>(); + + result.subscribe(to); + + assertTrue(source.hasObservers()); + assertTrue(other.hasObservers()); + + other.onNext(1); + + source.onComplete(); + + to.assertNoErrors(); + to.assertTerminated(); + to.assertNoValues(); + + assertFalse(source.hasObservers()); + assertFalse(other.hasObservers()); + } + + @Test + public void emptyOther() { + PublishSubject<Integer> source = PublishSubject.create(); + PublishSubject<Integer> other = PublishSubject.create(); + + Observable<Integer> result = source.withLatestFrom(other, COMBINER); + + TestObserverEx<Integer> to = new TestObserverEx<>(); + + result.subscribe(to); + + assertTrue(source.hasObservers()); + assertTrue(other.hasObservers()); + + source.onNext(1); + + source.onComplete(); + + to.assertNoErrors(); + to.assertTerminated(); + to.assertNoValues(); + + assertFalse(source.hasObservers()); + assertFalse(other.hasObservers()); + } + + @Test + public void unsubscription() { + PublishSubject<Integer> source = PublishSubject.create(); + PublishSubject<Integer> other = PublishSubject.create(); + + Observable<Integer> result = source.withLatestFrom(other, COMBINER); + + TestObserver<Integer> to = new TestObserver<>(); + + result.subscribe(to); + + assertTrue(source.hasObservers()); + assertTrue(other.hasObservers()); + + other.onNext(1); + source.onNext(1); + + to.dispose(); + + to.assertValue((1 << 8) + 1); + to.assertNoErrors(); + to.assertNotComplete(); + + assertFalse(source.hasObservers()); + assertFalse(other.hasObservers()); + } + + @Test + public void sourceThrows() { + PublishSubject<Integer> source = PublishSubject.create(); + PublishSubject<Integer> other = PublishSubject.create(); + + Observable<Integer> result = source.withLatestFrom(other, COMBINER); + + TestObserverEx<Integer> to = new TestObserverEx<>(); + + result.subscribe(to); + + assertTrue(source.hasObservers()); + assertTrue(other.hasObservers()); + + other.onNext(1); + source.onNext(1); + + source.onError(new TestException()); + + to.assertTerminated(); + to.assertValue((1 << 8) + 1); + to.assertError(TestException.class); + to.assertNotComplete(); + + assertFalse(source.hasObservers()); + assertFalse(other.hasObservers()); + } + + @Test + public void otherThrows() { + PublishSubject<Integer> source = PublishSubject.create(); + PublishSubject<Integer> other = PublishSubject.create(); + + Observable<Integer> result = source.withLatestFrom(other, COMBINER); + + TestObserverEx<Integer> to = new TestObserverEx<>(); + + result.subscribe(to); + + assertTrue(source.hasObservers()); + assertTrue(other.hasObservers()); + + other.onNext(1); + source.onNext(1); + + other.onError(new TestException()); + + to.assertTerminated(); + to.assertValue((1 << 8) + 1); + to.assertNotComplete(); + to.assertError(TestException.class); + + assertFalse(source.hasObservers()); + assertFalse(other.hasObservers()); + } + + @Test + public void functionThrows() { + PublishSubject<Integer> source = PublishSubject.create(); + PublishSubject<Integer> other = PublishSubject.create(); + + Observable<Integer> result = source.withLatestFrom(other, COMBINER_ERROR); + + TestObserverEx<Integer> to = new TestObserverEx<>(); + + result.subscribe(to); + + assertTrue(source.hasObservers()); + assertTrue(other.hasObservers()); + + other.onNext(1); + source.onNext(1); + + to.assertTerminated(); + to.assertNotComplete(); + to.assertNoValues(); + to.assertError(TestException.class); + + assertFalse(source.hasObservers()); + assertFalse(other.hasObservers()); + } + + @Test + public void noDownstreamUnsubscribe() { + PublishSubject<Integer> source = PublishSubject.create(); + PublishSubject<Integer> other = PublishSubject.create(); + + Observable<Integer> result = source.withLatestFrom(other, COMBINER); + + TestObserver<Integer> to = new TestObserver<>(); + + result.subscribe(to); + + source.onComplete(); + + // 2.0.2 - not anymore +// assertTrue("Not cancelled!", ts.isCancelled()); + } + + static final Function<Object[], String> toArray = new Function<Object[], String>() { + @Override + public String apply(Object[] args) { + return Arrays.toString(args); + } + }; + + @Test + public void manySources() { + PublishSubject<String> ps1 = PublishSubject.create(); + PublishSubject<String> ps2 = PublishSubject.create(); + PublishSubject<String> ps3 = PublishSubject.create(); + PublishSubject<String> main = PublishSubject.create(); + + TestObserver<String> to = new TestObserver<>(); + + main.withLatestFrom(new Observable[] { ps1, ps2, ps3 }, toArray) + .subscribe(to); + + main.onNext("1"); + to.assertNoValues(); + ps1.onNext("a"); + to.assertNoValues(); + ps2.onNext("A"); + to.assertNoValues(); + ps3.onNext("="); + to.assertNoValues(); + + main.onNext("2"); + to.assertValues("[2, a, A, =]"); + + ps2.onNext("B"); + + to.assertValues("[2, a, A, =]"); + + ps3.onComplete(); + to.assertValues("[2, a, A, =]"); + + ps1.onNext("b"); + + main.onNext("3"); + + to.assertValues("[2, a, A, =]", "[3, b, B, =]"); + + main.onComplete(); + to.assertValues("[2, a, A, =]", "[3, b, B, =]"); + to.assertNoErrors(); + to.assertComplete(); + + assertFalse("ps1 has subscribers?", ps1.hasObservers()); + assertFalse("ps2 has subscribers?", ps2.hasObservers()); + assertFalse("ps3 has subscribers?", ps3.hasObservers()); + } + + @Test + public void manySourcesIterable() { + PublishSubject<String> ps1 = PublishSubject.create(); + PublishSubject<String> ps2 = PublishSubject.create(); + PublishSubject<String> ps3 = PublishSubject.create(); + PublishSubject<String> main = PublishSubject.create(); + + TestObserver<String> to = new TestObserver<>(); + + main.withLatestFrom(Arrays.<Observable<?>>asList(ps1, ps2, ps3), toArray) + .subscribe(to); + + main.onNext("1"); + to.assertNoValues(); + ps1.onNext("a"); + to.assertNoValues(); + ps2.onNext("A"); + to.assertNoValues(); + ps3.onNext("="); + to.assertNoValues(); + + main.onNext("2"); + to.assertValues("[2, a, A, =]"); + + ps2.onNext("B"); + + to.assertValues("[2, a, A, =]"); + + ps3.onComplete(); + to.assertValues("[2, a, A, =]"); + + ps1.onNext("b"); + + main.onNext("3"); + + to.assertValues("[2, a, A, =]", "[3, b, B, =]"); + + main.onComplete(); + to.assertValues("[2, a, A, =]", "[3, b, B, =]"); + to.assertNoErrors(); + to.assertComplete(); + + assertFalse("ps1 has subscribers?", ps1.hasObservers()); + assertFalse("ps2 has subscribers?", ps2.hasObservers()); + assertFalse("ps3 has subscribers?", ps3.hasObservers()); + } + + @Test + public void manySourcesIterableSweep() { + for (String val : new String[] { "1" /*, null*/ }) { + int n = 35; + for (int i = 0; i < n; i++) { + List<Observable<?>> sources = new ArrayList<>(); + List<String> expected = new ArrayList<>(); + expected.add(val); + + for (int j = 0; j < i; j++) { + sources.add(Observable.just(val)); + expected.add(String.valueOf(val)); + } + + TestObserver<String> to = new TestObserver<>(); + + PublishSubject<String> main = PublishSubject.create(); + + main.withLatestFrom(sources, toArray).subscribe(to); + + to.assertNoValues(); + + main.onNext(val); + main.onComplete(); + + to.assertValue(expected.toString()); + to.assertNoErrors(); + to.assertComplete(); + } + } + } + + @Test + public void withEmpty() { + TestObserver<String> to = new TestObserver<>(); + + Observable.range(1, 3).withLatestFrom( + new Observable<?>[] { Observable.just(1), Observable.empty() }, toArray) + .subscribe(to); + + to.assertNoValues(); + to.assertNoErrors(); + to.assertComplete(); + } + + @Test + public void withError() { + TestObserver<String> to = new TestObserver<>(); + + Observable.range(1, 3).withLatestFrom( + new Observable<?>[] { Observable.just(1), Observable.error(new TestException()) }, toArray) + .subscribe(to); + + to.assertNoValues(); + to.assertError(TestException.class); + to.assertNotComplete(); + } + + @Test + public void withMainError() { + TestObserver<String> to = new TestObserver<>(); + + Observable.error(new TestException()).withLatestFrom( + new Observable<?>[] { Observable.just(1), Observable.just(1) }, toArray) + .subscribe(to); + + to.assertNoValues(); + to.assertError(TestException.class); + to.assertNotComplete(); + } + + @Test + public void with2Others() { + Observable<Integer> just = Observable.just(1); + + TestObserver<List<Integer>> to = new TestObserver<>(); + + just.withLatestFrom(just, just, new Function3<Integer, Integer, Integer, List<Integer>>() { + @Override + public List<Integer> apply(Integer a, Integer b, Integer c) { + return Arrays.asList(a, b, c); + } + }) + .subscribe(to); + + to.assertValue(Arrays.asList(1, 1, 1)); + to.assertNoErrors(); + to.assertComplete(); + } + + @Test + public void with3Others() { + Observable<Integer> just = Observable.just(1); + + TestObserver<List<Integer>> to = new TestObserver<>(); + + just.withLatestFrom(just, just, just, new Function4<Integer, Integer, Integer, Integer, List<Integer>>() { + @Override + public List<Integer> apply(Integer a, Integer b, Integer c, Integer d) { + return Arrays.asList(a, b, c, d); + } + }) + .subscribe(to); + + to.assertValue(Arrays.asList(1, 1, 1, 1)); + to.assertNoErrors(); + to.assertComplete(); + } + + @Test + public void with4Others() { + Observable<Integer> just = Observable.just(1); + + TestObserver<List<Integer>> to = new TestObserver<>(); + + just.withLatestFrom(just, just, just, just, new Function5<Integer, Integer, Integer, Integer, Integer, List<Integer>>() { + @Override + public List<Integer> apply(Integer a, Integer b, Integer c, Integer d, Integer e) { + return Arrays.asList(a, b, c, d, e); + } + }) + .subscribe(to); + + to.assertValue(Arrays.asList(1, 1, 1, 1, 1)); + to.assertNoErrors(); + to.assertComplete(); + } + + @Test + public void dispose() { + TestHelper.checkDisposed(Observable.just(1).withLatestFrom(Observable.just(2), new BiFunction<Integer, Integer, Object>() { + @Override + public Object apply(Integer a, Integer b) throws Exception { + return a; + } + })); + + TestHelper.checkDisposed(Observable.just(1).withLatestFrom(Observable.just(2), Observable.just(3), new Function3<Integer, Integer, Integer, Object>() { + @Override + public Object apply(Integer a, Integer b, Integer c) throws Exception { + return a; + } + })); + } + + @Test + public void manyIteratorThrows() { + Observable.just(1) + .withLatestFrom(new CrashingMappedIterable<>(1, 100, 100, new Function<Integer, Observable<Integer>>() { + @Override + public Observable<Integer> apply(Integer v) throws Exception { + return Observable.just(2); + } + }), new Function<Object[], Object>() { + @Override + public Object apply(Object[] a) throws Exception { + return a; + } + }) + .to(TestHelper.<Object>testConsumer()) + .assertFailureAndMessage(TestException.class, "iterator()"); + } + + @Test + public void manyCombinerThrows() { + Observable.just(1).withLatestFrom(Observable.just(2), Observable.just(3), new Function3<Integer, Integer, Integer, Object>() { + @Override + public Object apply(Integer a, Integer b, Integer c) throws Exception { + throw new TestException(); + } + }) + .test() + .assertFailure(TestException.class); + } + + @Test + public void manyErrors() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + new Observable<Integer>() { + @Override + protected void subscribeActual(Observer<? super Integer> observer) { + observer.onSubscribe(Disposable.empty()); + observer.onError(new TestException("First")); + observer.onNext(1); + observer.onError(new TestException("Second")); + observer.onComplete(); + } + }.withLatestFrom(Observable.just(2), Observable.just(3), new Function3<Integer, Integer, Integer, Object>() { + @Override + public Object apply(Integer a, Integer b, Integer c) throws Exception { + return a; + } + }) + .to(TestHelper.<Object>testConsumer()) + .assertFailureAndMessage(TestException.class, "First"); + + TestHelper.assertUndeliverable(errors, 0, TestException.class, "Second"); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void combineToNull1() { + Observable.just(1) + .withLatestFrom(Observable.just(2), new BiFunction<Integer, Integer, Object>() { + @Override + public Object apply(Integer a, Integer b) throws Exception { + return null; + } + }) + .test() + .assertFailure(NullPointerException.class); + } + + @Test + public void combineToNull2() { + Observable.just(1) + .withLatestFrom(Arrays.asList(Observable.just(2), Observable.just(3)), new Function<Object[], Object>() { + @Override + public Object apply(Object[] o) throws Exception { + return null; + } + }) + .test() + .assertFailure(NullPointerException.class); + } + + @Test + public void zeroOtherCombinerReturnsNull() { + Observable.just(1) + .withLatestFrom(new Observable[0], Functions.justFunction(null)) + .to(TestHelper.<Object>testConsumer()) + .assertFailureAndMessage(NullPointerException.class, "The combiner returned a null value"); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableZipCompletionTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableZipCompletionTest.java new file mode 100644 index 0000000000..89dde2cdc5 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableZipCompletionTest.java @@ -0,0 +1,112 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import static org.mockito.Mockito.*; + +import org.junit.*; +import org.mockito.InOrder; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.functions.BiFunction; +import io.reactivex.rxjava3.subjects.PublishSubject; +import io.reactivex.rxjava3.testsupport.TestHelper; + +/** + * Systematically tests that when zipping an infinite and a finite Observable, + * the resulting Observable is finite. + * + */ +public class ObservableZipCompletionTest extends RxJavaTest { + BiFunction<String, String, String> concat2Strings; + + PublishSubject<String> s1; + PublishSubject<String> s2; + Observable<String> zipped; + + Observer<String> observer; + InOrder inOrder; + + @Before + public void setUp() { + concat2Strings = new BiFunction<String, String, String>() { + @Override + public String apply(String t1, String t2) { + return t1 + "-" + t2; + } + }; + + s1 = PublishSubject.create(); + s2 = PublishSubject.create(); + zipped = Observable.zip(s1, s2, concat2Strings); + + observer = TestHelper.mockObserver(); + inOrder = inOrder(observer); + + zipped.subscribe(observer); + } + + @Test + public void firstCompletesThenSecondInfinite() { + s1.onNext("a"); + s1.onNext("b"); + s1.onComplete(); + s2.onNext("1"); + inOrder.verify(observer, times(1)).onNext("a-1"); + s2.onNext("2"); + inOrder.verify(observer, times(1)).onNext("b-2"); + inOrder.verify(observer, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void secondInfiniteThenFirstCompletes() { + s2.onNext("1"); + s2.onNext("2"); + s1.onNext("a"); + inOrder.verify(observer, times(1)).onNext("a-1"); + s1.onNext("b"); + inOrder.verify(observer, times(1)).onNext("b-2"); + s1.onComplete(); + inOrder.verify(observer, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void secondCompletesThenFirstInfinite() { + s2.onNext("1"); + s2.onNext("2"); + s2.onComplete(); + s1.onNext("a"); + inOrder.verify(observer, times(1)).onNext("a-1"); + s1.onNext("b"); + inOrder.verify(observer, times(1)).onNext("b-2"); + inOrder.verify(observer, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void firstInfiniteThenSecondCompletes() { + s1.onNext("a"); + s1.onNext("b"); + s2.onNext("1"); + inOrder.verify(observer, times(1)).onNext("a-1"); + s2.onNext("2"); + inOrder.verify(observer, times(1)).onNext("b-2"); + s2.onComplete(); + inOrder.verify(observer, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + } + +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableZipIterableTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableZipIterableTest.java new file mode 100644 index 0000000000..ce4ee90939 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableZipIterableTest.java @@ -0,0 +1,434 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import java.util.*; +import java.util.concurrent.atomic.AtomicInteger; + +import io.reactivex.rxjava3.disposables.Disposable; +import org.junit.*; +import org.mockito.InOrder; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.core.Observer; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.util.CrashingIterable; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.subjects.PublishSubject; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class ObservableZipIterableTest extends RxJavaTest { + BiFunction<String, String, String> concat2Strings; + PublishSubject<String> s1; + PublishSubject<String> s2; + Observable<String> zipped; + + Observer<String> observer; + InOrder inOrder; + + @Before + public void setUp() { + concat2Strings = new BiFunction<String, String, String>() { + @Override + public String apply(String t1, String t2) { + return t1 + "-" + t2; + } + }; + + s1 = PublishSubject.create(); + s2 = PublishSubject.create(); + zipped = Observable.zip(s1, s2, concat2Strings); + + observer = TestHelper.mockObserver(); + inOrder = inOrder(observer); + + zipped.subscribe(observer); + } + + BiFunction<Object, Object, String> zipr2 = new BiFunction<Object, Object, String>() { + + @Override + public String apply(Object t1, Object t2) { + return "" + t1 + t2; + } + + }; + Function3<Object, Object, Object, String> zipr3 = new Function3<Object, Object, Object, String>() { + + @Override + public String apply(Object t1, Object t2, Object t3) { + return "" + t1 + t2 + t3; + } + + }; + + @Test + public void zipIterableSameSize() { + PublishSubject<String> r1 = PublishSubject.create(); + /* define an Observer to receive aggregated events */ + Observer<String> o = TestHelper.mockObserver(); + InOrder io = inOrder(o); + + Iterable<String> r2 = Arrays.asList("1", "2", "3"); + + r1.zipWith(r2, zipr2).subscribe(o); + + r1.onNext("one-"); + r1.onNext("two-"); + r1.onNext("three-"); + r1.onComplete(); + + io.verify(o).onNext("one-1"); + io.verify(o).onNext("two-2"); + io.verify(o).onNext("three-3"); + io.verify(o).onComplete(); + + verify(o, never()).onError(any(Throwable.class)); + + } + + @Test + public void zipIterableEmptyFirstSize() { + PublishSubject<String> r1 = PublishSubject.create(); + /* define an Observer to receive aggregated events */ + Observer<String> o = TestHelper.mockObserver(); + InOrder io = inOrder(o); + + Iterable<String> r2 = Arrays.asList("1", "2", "3"); + + r1.zipWith(r2, zipr2).subscribe(o); + + r1.onComplete(); + + io.verify(o).onComplete(); + + verify(o, never()).onNext(any(String.class)); + verify(o, never()).onError(any(Throwable.class)); + + } + + @Test + public void zipIterableEmptySecond() { + PublishSubject<String> r1 = PublishSubject.create(); + /* define an Observer to receive aggregated events */ + Observer<String> o = TestHelper.mockObserver(); + InOrder io = inOrder(o); + + Iterable<String> r2 = Arrays.asList(); + + r1.zipWith(r2, zipr2).subscribe(o); + + r1.onNext("one-"); + r1.onNext("two-"); + r1.onNext("three-"); + r1.onComplete(); + + io.verify(o).onComplete(); + + verify(o, never()).onNext(any(String.class)); + verify(o, never()).onError(any(Throwable.class)); + } + + @Test + public void zipIterableFirstShorter() { + PublishSubject<String> r1 = PublishSubject.create(); + /* define an Observer to receive aggregated events */ + Observer<String> o = TestHelper.mockObserver(); + InOrder io = inOrder(o); + + Iterable<String> r2 = Arrays.asList("1", "2", "3"); + + r1.zipWith(r2, zipr2).subscribe(o); + + r1.onNext("one-"); + r1.onNext("two-"); + r1.onComplete(); + + io.verify(o).onNext("one-1"); + io.verify(o).onNext("two-2"); + io.verify(o).onComplete(); + + verify(o, never()).onError(any(Throwable.class)); + + } + + @Test + public void zipIterableSecondShorter() { + PublishSubject<String> r1 = PublishSubject.create(); + /* define an Observer to receive aggregated events */ + Observer<String> o = TestHelper.mockObserver(); + InOrder io = inOrder(o); + + Iterable<String> r2 = Arrays.asList("1", "2"); + + r1.zipWith(r2, zipr2).subscribe(o); + + r1.onNext("one-"); + r1.onNext("two-"); + r1.onNext("three-"); + r1.onComplete(); + + io.verify(o).onNext("one-1"); + io.verify(o).onNext("two-2"); + io.verify(o).onComplete(); + + verify(o, never()).onError(any(Throwable.class)); + + } + + @Test + public void zipIterableFirstThrows() { + PublishSubject<String> r1 = PublishSubject.create(); + /* define an Observer to receive aggregated events */ + Observer<String> o = TestHelper.mockObserver(); + InOrder io = inOrder(o); + + Iterable<String> r2 = Arrays.asList("1", "2", "3"); + + r1.zipWith(r2, zipr2).subscribe(o); + + r1.onNext("one-"); + r1.onNext("two-"); + r1.onError(new TestException()); + + io.verify(o).onNext("one-1"); + io.verify(o).onNext("two-2"); + io.verify(o).onError(any(TestException.class)); + + verify(o, never()).onComplete(); + + } + + @Test + public void zipIterableIteratorThrows() { + PublishSubject<String> r1 = PublishSubject.create(); + /* define an Observer to receive aggregated events */ + Observer<String> o = TestHelper.mockObserver(); + InOrder io = inOrder(o); + + Iterable<String> r2 = new Iterable<String>() { + @Override + public Iterator<String> iterator() { + throw new TestException(); + } + }; + + r1.zipWith(r2, zipr2).subscribe(o); + + r1.onNext("one-"); + r1.onNext("two-"); + r1.onError(new TestException()); + + io.verify(o).onError(any(TestException.class)); + + verify(o, never()).onComplete(); + verify(o, never()).onNext(any(String.class)); + + } + + @Test + public void zipIterableHasNextThrows() { + PublishSubject<String> r1 = PublishSubject.create(); + /* define an Observer to receive aggregated events */ + Observer<String> o = TestHelper.mockObserver(); + InOrder io = inOrder(o); + + Iterable<String> r2 = new Iterable<String>() { + + @Override + public Iterator<String> iterator() { + return new Iterator<String>() { + int count; + + @Override + public boolean hasNext() { + if (count == 0) { + return true; + } + throw new TestException(); + } + + @Override + public String next() { + count++; + return "1"; + } + + @Override + public void remove() { + throw new UnsupportedOperationException("Not supported yet."); + } + + }; + } + + }; + + r1.zipWith(r2, zipr2).subscribe(o); + + r1.onNext("one-"); + r1.onError(new TestException()); + + io.verify(o).onNext("one-1"); + io.verify(o).onError(any(TestException.class)); + + verify(o, never()).onComplete(); + + } + + @Test + public void zipIterableNextThrows() { + PublishSubject<String> r1 = PublishSubject.create(); + /* define an Observer to receive aggregated events */ + Observer<String> o = TestHelper.mockObserver(); + InOrder io = inOrder(o); + + Iterable<String> r2 = new Iterable<String>() { + + @Override + public Iterator<String> iterator() { + return new Iterator<String>() { + @Override + public boolean hasNext() { + return true; + } + + @Override + public String next() { + throw new TestException(); + } + + @Override + public void remove() { + throw new UnsupportedOperationException("Not supported yet."); + } + + }; + } + + }; + + r1.zipWith(r2, zipr2).subscribe(o); + + r1.onError(new TestException()); + + io.verify(o).onError(any(TestException.class)); + + verify(o, never()).onNext(any(String.class)); + verify(o, never()).onComplete(); + + } + + Consumer<String> printer = new Consumer<String>() { + @Override + public void accept(String pv) { + System.out.println(pv); + } + }; + + static final class SquareStr implements Function<Integer, String> { + final AtomicInteger counter = new AtomicInteger(); + @Override + public String apply(Integer t1) { + counter.incrementAndGet(); + System.out.println("Omg I'm calculating so hard: " + t1 + "*" + t1 + "=" + (t1 * t1)); + return " " + (t1 * t1); + } + } + + @Test + public void take2() { + Observable<Integer> o = Observable.just(1, 2, 3, 4, 5); + Iterable<String> it = Arrays.asList("a", "b", "c", "d", "e"); + + SquareStr squareStr = new SquareStr(); + + o.map(squareStr).zipWith(it, concat2Strings).take(2).subscribe(printer); + + assertEquals(2, squareStr.counter.get()); + } + + @Test + public void dispose() { + TestHelper.checkDisposed(Observable.just(1).zipWith(Arrays.asList(1), new BiFunction<Integer, Integer, Object>() { + @Override + public Object apply(Integer a, Integer b) throws Exception { + return a + b; + } + })); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeObservable(new Function<Observable<Integer>, ObservableSource<Object>>() { + @Override + public ObservableSource<Object> apply(Observable<Integer> o) throws Exception { + return o.zipWith(Arrays.asList(1), new BiFunction<Integer, Integer, Object>() { + @Override + public Object apply(Integer a, Integer b) throws Exception { + return a + b; + } + }); + } + }); + } + + @Test + public void iteratorThrows() { + Observable.just(1).zipWith(new CrashingIterable(100, 1, 100), new BiFunction<Integer, Integer, Object>() { + @Override + public Object apply(Integer a, Integer b) throws Exception { + return a + b; + } + }) + .to(TestHelper.<Object>testConsumer()) + .assertFailureAndMessage(TestException.class, "hasNext()"); + } + + @Test + public void badSource() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + new Observable<Integer>() { + @Override + protected void subscribeActual(Observer<? super Integer> observer) { + observer.onSubscribe(Disposable.empty()); + observer.onNext(1); + observer.onComplete(); + observer.onNext(2); + observer.onError(new TestException()); + observer.onComplete(); + } + } + .zipWith(Arrays.asList(1), new BiFunction<Integer, Integer, Object>() { + @Override + public Object apply(Integer a, Integer b) throws Exception { + return a + b; + } + }) + .test() + .assertResult(2); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableZipTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableZipTest.java new file mode 100644 index 0000000000..ae7de66d6f --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/observable/ObservableZipTest.java @@ -0,0 +1,1450 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.observable; + +import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +import java.util.*; +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.*; +import org.mockito.InOrder; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.core.Observer; +import io.reactivex.rxjava3.disposables.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.observers.*; +import io.reactivex.rxjava3.schedulers.Schedulers; +import io.reactivex.rxjava3.subjects.PublishSubject; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class ObservableZipTest extends RxJavaTest { + BiFunction<String, String, String> concat2Strings; + PublishSubject<String> s1; + PublishSubject<String> s2; + Observable<String> zipped; + + Observer<String> observer; + InOrder inOrder; + + @Before + public void setUp() { + concat2Strings = new BiFunction<String, String, String>() { + @Override + public String apply(String t1, String t2) { + return t1 + "-" + t2; + } + }; + + s1 = PublishSubject.create(); + s2 = PublishSubject.create(); + zipped = Observable.zip(s1, s2, concat2Strings); + + observer = TestHelper.mockObserver(); + inOrder = inOrder(observer); + + zipped.subscribe(observer); + } + + @SuppressWarnings("unchecked") + @Test + public void collectionSizeDifferentThanFunction() { + Function<Object[], String> zipr = Functions.toFunction(getConcatStringIntegerIntArrayZipr()); + //Function3<String, Integer, int[], String> + + /* define an Observer to receive aggregated events */ + Observer<String> observer = TestHelper.mockObserver(); + + @SuppressWarnings("rawtypes") + Collection ws = java.util.Collections.singleton(Observable.just("one", "two")); + Observable<String> w = Observable.zip(ws, zipr); + w.subscribe(observer); + + verify(observer, times(1)).onError(any(Throwable.class)); + verify(observer, never()).onComplete(); + verify(observer, never()).onNext(any(String.class)); + } + + @Test + public void startpingDifferentLengthObservableSequences1() { + Observer<String> w = TestHelper.mockObserver(); + + TestObservable w1 = new TestObservable(); + TestObservable w2 = new TestObservable(); + TestObservable w3 = new TestObservable(); + + Observable<String> zipW = Observable.zip( + Observable.unsafeCreate(w1), Observable.unsafeCreate(w2), + Observable.unsafeCreate(w3), getConcat3StringsZipr()); + zipW.subscribe(w); + + /* simulate sending data */ + // once for w1 + w1.observer.onNext("1a"); + w1.observer.onComplete(); + // twice for w2 + w2.observer.onNext("2a"); + w2.observer.onNext("2b"); + w2.observer.onComplete(); + // 4 times for w3 + w3.observer.onNext("3a"); + w3.observer.onNext("3b"); + w3.observer.onNext("3c"); + w3.observer.onNext("3d"); + w3.observer.onComplete(); + + /* we should have been called 1 time on the Observer */ + InOrder io = inOrder(w); + io.verify(w).onNext("1a2a3a"); + + io.verify(w, times(1)).onComplete(); + } + + @Test + public void startpingDifferentLengthObservableSequences2() { + Observer<String> w = TestHelper.mockObserver(); + + TestObservable w1 = new TestObservable(); + TestObservable w2 = new TestObservable(); + TestObservable w3 = new TestObservable(); + + Observable<String> zipW = Observable.zip(Observable.unsafeCreate(w1), Observable.unsafeCreate(w2), Observable.unsafeCreate(w3), getConcat3StringsZipr()); + zipW.subscribe(w); + + /* simulate sending data */ + // 4 times for w1 + w1.observer.onNext("1a"); + w1.observer.onNext("1b"); + w1.observer.onNext("1c"); + w1.observer.onNext("1d"); + w1.observer.onComplete(); + // twice for w2 + w2.observer.onNext("2a"); + w2.observer.onNext("2b"); + w2.observer.onComplete(); + // 1 times for w3 + w3.observer.onNext("3a"); + w3.observer.onComplete(); + + /* we should have been called 1 time on the Observer */ + InOrder io = inOrder(w); + io.verify(w).onNext("1a2a3a"); + + io.verify(w, times(1)).onComplete(); + + } + + BiFunction<Object, Object, String> zipr2 = new BiFunction<Object, Object, String>() { + + @Override + public String apply(Object t1, Object t2) { + return "" + t1 + t2; + } + + }; + Function3<Object, Object, Object, String> zipr3 = new Function3<Object, Object, Object, String>() { + + @Override + public String apply(Object t1, Object t2, Object t3) { + return "" + t1 + t2 + t3; + } + + }; + + /** + * Testing internal private logic due to the complexity so I want to use TDD to test as a I build it rather than relying purely on the overall functionality expected by the public methods. + */ + @Test + public void aggregatorSimple() { + PublishSubject<String> r1 = PublishSubject.create(); + PublishSubject<String> r2 = PublishSubject.create(); + /* define an Observer to receive aggregated events */ + Observer<String> observer = TestHelper.mockObserver(); + + Observable.zip(r1, r2, zipr2).subscribe(observer); + + /* simulate the Observables pushing data into the aggregator */ + r1.onNext("hello"); + r2.onNext("world"); + + InOrder inOrder = inOrder(observer); + + verify(observer, never()).onError(any(Throwable.class)); + verify(observer, never()).onComplete(); + inOrder.verify(observer, times(1)).onNext("helloworld"); + + r1.onNext("hello "); + r2.onNext("again"); + + verify(observer, never()).onError(any(Throwable.class)); + verify(observer, never()).onComplete(); + inOrder.verify(observer, times(1)).onNext("hello again"); + + r1.onComplete(); + r2.onComplete(); + + inOrder.verify(observer, never()).onNext(anyString()); + verify(observer, times(1)).onComplete(); + } + + @Test + public void aggregatorDifferentSizedResultsWithOnComplete() { + /* create the aggregator which will execute the zip function when all Observables provide values */ + /* define an Observer to receive aggregated events */ + PublishSubject<String> r1 = PublishSubject.create(); + PublishSubject<String> r2 = PublishSubject.create(); + /* define an Observer to receive aggregated events */ + Observer<String> observer = TestHelper.mockObserver(); + Observable.zip(r1, r2, zipr2).subscribe(observer); + + /* simulate the Observables pushing data into the aggregator */ + r1.onNext("hello"); + r2.onNext("world"); + r2.onComplete(); + + InOrder inOrder = inOrder(observer); + + inOrder.verify(observer, never()).onError(any(Throwable.class)); + inOrder.verify(observer, times(1)).onNext("helloworld"); + inOrder.verify(observer, times(1)).onComplete(); + + r1.onNext("hi"); + r1.onComplete(); + + inOrder.verify(observer, never()).onError(any(Throwable.class)); + inOrder.verify(observer, never()).onComplete(); + inOrder.verify(observer, never()).onNext(anyString()); + } + + @Test + public void aggregateMultipleTypes() { + PublishSubject<String> r1 = PublishSubject.create(); + PublishSubject<Integer> r2 = PublishSubject.create(); + /* define an Observer to receive aggregated events */ + Observer<String> observer = TestHelper.mockObserver(); + + Observable.zip(r1, r2, zipr2).subscribe(observer); + + /* simulate the Observables pushing data into the aggregator */ + r1.onNext("hello"); + r2.onNext(1); + r2.onComplete(); + + InOrder inOrder = inOrder(observer); + + inOrder.verify(observer, never()).onError(any(Throwable.class)); + inOrder.verify(observer, times(1)).onNext("hello1"); + inOrder.verify(observer, times(1)).onComplete(); + + r1.onNext("hi"); + r1.onComplete(); + + inOrder.verify(observer, never()).onError(any(Throwable.class)); + inOrder.verify(observer, never()).onComplete(); + inOrder.verify(observer, never()).onNext(anyString()); + } + + @Test + public void aggregate3Types() { + PublishSubject<String> r1 = PublishSubject.create(); + PublishSubject<Integer> r2 = PublishSubject.create(); + PublishSubject<List<Integer>> r3 = PublishSubject.create(); + /* define an Observer to receive aggregated events */ + Observer<String> observer = TestHelper.mockObserver(); + + Observable.zip(r1, r2, r3, zipr3).subscribe(observer); + + /* simulate the Observables pushing data into the aggregator */ + r1.onNext("hello"); + r2.onNext(2); + r3.onNext(Arrays.asList(5, 6, 7)); + + verify(observer, never()).onError(any(Throwable.class)); + verify(observer, never()).onComplete(); + verify(observer, times(1)).onNext("hello2[5, 6, 7]"); + } + + @Test + public void aggregatorsWithDifferentSizesAndTiming() { + PublishSubject<String> r1 = PublishSubject.create(); + PublishSubject<String> r2 = PublishSubject.create(); + /* define an Observer to receive aggregated events */ + Observer<String> observer = TestHelper.mockObserver(); + + Observable.zip(r1, r2, zipr2).subscribe(observer); + + /* simulate the Observables pushing data into the aggregator */ + r1.onNext("one"); + r1.onNext("two"); + r1.onNext("three"); + r2.onNext("A"); + + verify(observer, never()).onError(any(Throwable.class)); + verify(observer, never()).onComplete(); + verify(observer, times(1)).onNext("oneA"); + + r1.onNext("four"); + r1.onComplete(); + r2.onNext("B"); + verify(observer, times(1)).onNext("twoB"); + r2.onNext("C"); + verify(observer, times(1)).onNext("threeC"); + r2.onNext("D"); + verify(observer, times(1)).onNext("fourD"); + r2.onNext("E"); + verify(observer, never()).onNext("E"); + r2.onComplete(); + + verify(observer, never()).onError(any(Throwable.class)); + verify(observer, times(1)).onComplete(); + } + + @Test + public void aggregatorError() { + PublishSubject<String> r1 = PublishSubject.create(); + PublishSubject<String> r2 = PublishSubject.create(); + /* define an Observer to receive aggregated events */ + Observer<String> observer = TestHelper.mockObserver(); + + Observable.zip(r1, r2, zipr2).subscribe(observer); + + /* simulate the Observables pushing data into the aggregator */ + r1.onNext("hello"); + r2.onNext("world"); + + verify(observer, never()).onError(any(Throwable.class)); + verify(observer, never()).onComplete(); + verify(observer, times(1)).onNext("helloworld"); + + r1.onError(new RuntimeException("")); + r1.onNext("hello"); + r2.onNext("again"); + + verify(observer, times(1)).onError(any(Throwable.class)); + verify(observer, never()).onComplete(); + // we don't want to be called again after an error + verify(observer, times(0)).onNext("helloagain"); + } + + @Test + public void aggregatorUnsubscribe() { + PublishSubject<String> r1 = PublishSubject.create(); + PublishSubject<String> r2 = PublishSubject.create(); + /* define an Observer to receive aggregated events */ + Observer<String> observer = TestHelper.mockObserver(); + TestObserver<String> to = new TestObserver<>(observer); + + Observable.zip(r1, r2, zipr2).subscribe(to); + + /* simulate the Observables pushing data into the aggregator */ + r1.onNext("hello"); + r2.onNext("world"); + + verify(observer, never()).onError(any(Throwable.class)); + verify(observer, never()).onComplete(); + verify(observer, times(1)).onNext("helloworld"); + + to.dispose(); + r1.onNext("hello"); + r2.onNext("again"); + + verify(observer, times(0)).onError(any(Throwable.class)); + verify(observer, never()).onComplete(); + // we don't want to be called again after an error + verify(observer, times(0)).onNext("helloagain"); + } + + @Test + public void aggregatorEarlyCompletion() { + PublishSubject<String> r1 = PublishSubject.create(); + PublishSubject<String> r2 = PublishSubject.create(); + /* define an Observer to receive aggregated events */ + Observer<String> observer = TestHelper.mockObserver(); + + Observable.zip(r1, r2, zipr2).subscribe(observer); + + /* simulate the Observables pushing data into the aggregator */ + r1.onNext("one"); + r1.onNext("two"); + r1.onComplete(); + r2.onNext("A"); + + InOrder inOrder = inOrder(observer); + + inOrder.verify(observer, never()).onError(any(Throwable.class)); + inOrder.verify(observer, never()).onComplete(); + inOrder.verify(observer, times(1)).onNext("oneA"); + + r2.onComplete(); + + inOrder.verify(observer, never()).onError(any(Throwable.class)); + inOrder.verify(observer, times(1)).onComplete(); + inOrder.verify(observer, never()).onNext(anyString()); + } + + @Test + public void start2Types() { + BiFunction<String, Integer, String> zipr = getConcatStringIntegerZipr(); + + /* define an Observer to receive aggregated events */ + Observer<String> observer = TestHelper.mockObserver(); + + Observable<String> w = Observable.zip(Observable.just("one", "two"), Observable.just(2, 3, 4), zipr); + w.subscribe(observer); + + verify(observer, never()).onError(any(Throwable.class)); + verify(observer, times(1)).onComplete(); + verify(observer, times(1)).onNext("one2"); + verify(observer, times(1)).onNext("two3"); + verify(observer, never()).onNext("4"); + } + + @Test + public void start3Types() { + Function3<String, Integer, int[], String> zipr = getConcatStringIntegerIntArrayZipr(); + + /* define an Observer to receive aggregated events */ + Observer<String> observer = TestHelper.mockObserver(); + + Observable<String> w = Observable.zip(Observable.just("one", "two"), Observable.just(2), Observable.just(new int[] { 4, 5, 6 }), zipr); + w.subscribe(observer); + + verify(observer, never()).onError(any(Throwable.class)); + verify(observer, times(1)).onComplete(); + verify(observer, times(1)).onNext("one2[4, 5, 6]"); + verify(observer, never()).onNext("two"); + } + + @Test + public void onNextExceptionInvokesOnError() { + BiFunction<Integer, Integer, Integer> zipr = getDivideZipr(); + + Observer<Integer> observer = TestHelper.mockObserver(); + + Observable<Integer> w = Observable.zip(Observable.just(10, 20, 30), Observable.just(0, 1, 2), zipr); + w.subscribe(observer); + + verify(observer, times(1)).onError(any(Throwable.class)); + } + + @Test + public void onFirstCompletion() { + PublishSubject<String> oA = PublishSubject.create(); + PublishSubject<String> oB = PublishSubject.create(); + + Observer<String> obs = TestHelper.mockObserver(); + + Observable<String> o = Observable.zip(oA, oB, getConcat2Strings()); + o.subscribe(obs); + + InOrder io = inOrder(obs); + + oA.onNext("a1"); + io.verify(obs, never()).onNext(anyString()); + oB.onNext("b1"); + io.verify(obs, times(1)).onNext("a1-b1"); + oB.onNext("b2"); + io.verify(obs, never()).onNext(anyString()); + oA.onNext("a2"); + io.verify(obs, times(1)).onNext("a2-b2"); + + oA.onNext("a3"); + oA.onNext("a4"); + oA.onNext("a5"); + oA.onComplete(); + + // SHOULD ONCOMPLETE BE EMITTED HERE INSTEAD OF WAITING + // FOR B3, B4, B5 TO BE EMITTED? + + oB.onNext("b3"); + oB.onNext("b4"); + oB.onNext("b5"); + + io.verify(obs, times(1)).onNext("a3-b3"); + io.verify(obs, times(1)).onNext("a4-b4"); + io.verify(obs, times(1)).onNext("a5-b5"); + + // WE RECEIVE THE ONCOMPLETE HERE + io.verify(obs, times(1)).onComplete(); + + oB.onNext("b6"); + oB.onNext("b7"); + oB.onNext("b8"); + oB.onNext("b9"); + // never completes (infinite stream for example) + + // we should receive nothing else despite oB continuing after oA completed + io.verifyNoMoreInteractions(); + } + + @Test + public void onErrorTermination() { + PublishSubject<String> oA = PublishSubject.create(); + PublishSubject<String> oB = PublishSubject.create(); + + Observer<String> obs = TestHelper.mockObserver(); + + Observable<String> o = Observable.zip(oA, oB, getConcat2Strings()); + o.subscribe(obs); + + InOrder io = inOrder(obs); + + oA.onNext("a1"); + io.verify(obs, never()).onNext(anyString()); + oB.onNext("b1"); + io.verify(obs, times(1)).onNext("a1-b1"); + oB.onNext("b2"); + io.verify(obs, never()).onNext(anyString()); + oA.onNext("a2"); + io.verify(obs, times(1)).onNext("a2-b2"); + + oA.onNext("a3"); + oA.onNext("a4"); + oA.onNext("a5"); + oA.onError(new RuntimeException("forced failure")); + + // it should emit failure immediately + io.verify(obs, times(1)).onError(any(RuntimeException.class)); + + oB.onNext("b3"); + oB.onNext("b4"); + oB.onNext("b5"); + oB.onNext("b6"); + oB.onNext("b7"); + oB.onNext("b8"); + oB.onNext("b9"); + // never completes (infinite stream for example) + + // we should receive nothing else despite oB continuing after oA completed + io.verifyNoMoreInteractions(); + } + + private BiFunction<String, String, String> getConcat2Strings() { + return new BiFunction<String, String, String>() { + + @Override + public String apply(String t1, String t2) { + return t1 + "-" + t2; + } + }; + } + + private BiFunction<Integer, Integer, Integer> getDivideZipr() { + BiFunction<Integer, Integer, Integer> zipr = new BiFunction<Integer, Integer, Integer>() { + + @Override + public Integer apply(Integer i1, Integer i2) { + return i1 / i2; + } + + }; + return zipr; + } + + private Function3<String, String, String, String> getConcat3StringsZipr() { + Function3<String, String, String, String> zipr = new Function3<String, String, String, String>() { + + @Override + public String apply(String a1, String a2, String a3) { + if (a1 == null) { + a1 = ""; + } + if (a2 == null) { + a2 = ""; + } + if (a3 == null) { + a3 = ""; + } + return a1 + a2 + a3; + } + + }; + return zipr; + } + + private BiFunction<String, Integer, String> getConcatStringIntegerZipr() { + BiFunction<String, Integer, String> zipr = new BiFunction<String, Integer, String>() { + + @Override + public String apply(String s, Integer i) { + return getStringValue(s) + getStringValue(i); + } + + }; + return zipr; + } + + private Function3<String, Integer, int[], String> getConcatStringIntegerIntArrayZipr() { + Function3<String, Integer, int[], String> zipr = new Function3<String, Integer, int[], String>() { + + @Override + public String apply(String s, Integer i, int[] iArray) { + return getStringValue(s) + getStringValue(i) + getStringValue(iArray); + } + + }; + return zipr; + } + + private static String getStringValue(Object o) { + if (o == null) { + return ""; + } else { + if (o instanceof int[]) { + return Arrays.toString((int[]) o); + } else { + return String.valueOf(o); + } + } + } + + private static class TestObservable implements ObservableSource<String> { + + Observer<? super String> observer; + + @Override + public void subscribe(Observer<? super String> observer) { + // just store the variable where it can be accessed so we can manually trigger it + this.observer = observer; + observer.onSubscribe(Disposable.empty()); + } + + } + + @Test + public void firstCompletesThenSecondInfinite() { + s1.onNext("a"); + s1.onNext("b"); + s1.onComplete(); + s2.onNext("1"); + inOrder.verify(observer, times(1)).onNext("a-1"); + s2.onNext("2"); + inOrder.verify(observer, times(1)).onNext("b-2"); + inOrder.verify(observer, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void secondInfiniteThenFirstCompletes() { + s2.onNext("1"); + s2.onNext("2"); + s1.onNext("a"); + inOrder.verify(observer, times(1)).onNext("a-1"); + s1.onNext("b"); + inOrder.verify(observer, times(1)).onNext("b-2"); + s1.onComplete(); + inOrder.verify(observer, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void secondCompletesThenFirstInfinite() { + s2.onNext("1"); + s2.onNext("2"); + s2.onComplete(); + s1.onNext("a"); + inOrder.verify(observer, times(1)).onNext("a-1"); + s1.onNext("b"); + inOrder.verify(observer, times(1)).onNext("b-2"); + inOrder.verify(observer, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void firstInfiniteThenSecondCompletes() { + s1.onNext("a"); + s1.onNext("b"); + s2.onNext("1"); + inOrder.verify(observer, times(1)).onNext("a-1"); + s2.onNext("2"); + inOrder.verify(observer, times(1)).onNext("b-2"); + s2.onComplete(); + inOrder.verify(observer, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void firstFails() { + s2.onNext("a"); + s1.onError(new RuntimeException("Forced failure")); + + inOrder.verify(observer, times(1)).onError(any(RuntimeException.class)); + + s2.onNext("b"); + s1.onNext("1"); + s1.onNext("2"); + + inOrder.verify(observer, never()).onComplete(); + inOrder.verify(observer, never()).onNext(any(String.class)); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void secondFails() { + s1.onNext("a"); + s1.onNext("b"); + s2.onError(new RuntimeException("Forced failure")); + + inOrder.verify(observer, times(1)).onError(any(RuntimeException.class)); + + s2.onNext("1"); + s2.onNext("2"); + + inOrder.verify(observer, never()).onComplete(); + inOrder.verify(observer, never()).onNext(any(String.class)); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void startWithOnCompletedTwice() { + // issue: https://groups.google.com/forum/#!topic/rxjava/79cWTv3TFp0 + // The problem is the original "zip" implementation does not wrap + // an internal observer with a SafeSubscriber. However, in the "zip", + // it may calls "onComplete" twice. That breaks the Rx contract. + + // This test tries to emulate this case. + // As "TestHelper.mockObserver()" will create an instance in the package "rx", + // we need to wrap "TestHelper.mockObserver()" with an observer instance + // which is in the package "rx.operators". + final Observer<Integer> observer = TestHelper.mockObserver(); + + Observable.zip(Observable.just(1), + Observable.just(1), new BiFunction<Integer, Integer, Integer>() { + @Override + public Integer apply(Integer a, Integer b) { + return a + b; + } + }).subscribe(new DefaultObserver<Integer>() { + + @Override + public void onComplete() { + observer.onComplete(); + } + + @Override + public void onError(Throwable e) { + observer.onError(e); + } + + @Override + public void onNext(Integer args) { + observer.onNext(args); + } + + }); + + InOrder inOrder = inOrder(observer); + inOrder.verify(observer, times(1)).onNext(2); + inOrder.verify(observer, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void start() { + Observable<String> os = OBSERVABLE_OF_5_INTEGERS + .zipWith(OBSERVABLE_OF_5_INTEGERS, new BiFunction<Integer, Integer, String>() { + + @Override + public String apply(Integer a, Integer b) { + return a + "-" + b; + } + }); + + final ArrayList<String> list = new ArrayList<>(); + os.subscribe(new Consumer<String>() { + + @Override + public void accept(String s) { + System.out.println(s); + list.add(s); + } + }); + + assertEquals(5, list.size()); + assertEquals("1-1", list.get(0)); + assertEquals("2-2", list.get(1)); + assertEquals("5-5", list.get(4)); + } + + @Test + public void startAsync() throws InterruptedException { + Observable<String> os = ASYNC_OBSERVABLE_OF_INFINITE_INTEGERS(new CountDownLatch(1)) + .zipWith(ASYNC_OBSERVABLE_OF_INFINITE_INTEGERS(new CountDownLatch(1)), new BiFunction<Integer, Integer, String>() { + + @Override + public String apply(Integer a, Integer b) { + return a + "-" + b; + } + }).take(5); + + TestObserver<String> to = new TestObserver<>(); + os.subscribe(to); + + to.awaitDone(5, TimeUnit.SECONDS); + to.assertNoErrors(); + + assertEquals(5, to.values().size()); + assertEquals("1-1", to.values().get(0)); + assertEquals("2-2", to.values().get(1)); + assertEquals("5-5", to.values().get(4)); + } + + @Test + public void startInfiniteAndFinite() throws InterruptedException { + final CountDownLatch latch = new CountDownLatch(1); + final CountDownLatch infiniteObservable = new CountDownLatch(1); + Observable<String> os = OBSERVABLE_OF_5_INTEGERS + .zipWith(ASYNC_OBSERVABLE_OF_INFINITE_INTEGERS(infiniteObservable), new BiFunction<Integer, Integer, String>() { + + @Override + public String apply(Integer a, Integer b) { + return a + "-" + b; + } + }); + + final ArrayList<String> list = new ArrayList<>(); + os.subscribe(new DefaultObserver<String>() { + + @Override + public void onComplete() { + latch.countDown(); + } + + @Override + public void onError(Throwable e) { + e.printStackTrace(); + latch.countDown(); + } + + @Override + public void onNext(String s) { + System.out.println(s); + list.add(s); + } + }); + + latch.await(1000, TimeUnit.MILLISECONDS); + if (!infiniteObservable.await(2000, TimeUnit.MILLISECONDS)) { + throw new RuntimeException("didn't unsubscribe"); + } + + assertEquals(5, list.size()); + assertEquals("1-1", list.get(0)); + assertEquals("2-2", list.get(1)); + assertEquals("5-5", list.get(4)); + } + + @SuppressWarnings("rawtypes") + static String kind(Notification notification) { + if (notification.isOnError()) { + return "OnError"; + } + if (notification.isOnNext()) { + return "OnNext"; + } + return "OnComplete"; + } + + @SuppressWarnings("rawtypes") + static String value(Notification notification) { + if (notification.isOnNext()) { + return String.valueOf(notification.getValue()); + } + return "null"; + } + + @Test + public void emitMaterializedNotifications() { + Observable<Notification<Integer>> oi = Observable.just(1, 2, 3).materialize(); + Observable<Notification<String>> os = Observable.just("a", "b", "c").materialize(); + Observable<String> o = Observable.zip(oi, os, new BiFunction<Notification<Integer>, Notification<String>, String>() { + + @Override + public String apply(Notification<Integer> t1, Notification<String> t2) { + return kind(t1) + "_" + value(t1) + "-" + kind(t2) + "_" + value(t2); + } + + }); + + final ArrayList<String> list = new ArrayList<>(); + o.subscribe(new Consumer<String>() { + + @Override + public void accept(String s) { + System.out.println(s); + list.add(s); + } + }); + + assertEquals(4, list.size()); + assertEquals("OnNext_1-OnNext_a", list.get(0)); + assertEquals("OnNext_2-OnNext_b", list.get(1)); + assertEquals("OnNext_3-OnNext_c", list.get(2)); + assertEquals("OnComplete_null-OnComplete_null", list.get(3)); + } + + @Test + public void startEmptyObservables() { + + Observable<String> o = Observable.zip(Observable.<Integer> empty(), Observable.<String> empty(), new BiFunction<Integer, String, String>() { + + @Override + public String apply(Integer t1, String t2) { + return t1 + "-" + t2; + } + + }); + + final ArrayList<String> list = new ArrayList<>(); + o.subscribe(new Consumer<String>() { + + @Override + public void accept(String s) { + System.out.println(s); + list.add(s); + } + }); + + assertEquals(0, list.size()); + } + + @Test + public void startEmptyList() { + + final Object invoked = new Object(); + Collection<Observable<Object>> observables = Collections.emptyList(); + + Observable<Object> o = Observable.zip(observables, new Function<Object[], Object>() { + @Override + public Object apply(final Object[] args) { + assertEquals("No argument should have been passed", 0, args.length); + return invoked; + } + }); + + TestObserver<Object> to = new TestObserver<>(); + o.subscribe(to); + to.awaitDone(200, TimeUnit.MILLISECONDS); + to.assertNoValues(); + } + + /** + * Expect NoSuchElementException instead of blocking forever as zip should emit onComplete and no onNext + * and last() expects at least a single response. + */ + @Test(expected = NoSuchElementException.class) + public void startEmptyListBlocking() { + + final Object invoked = new Object(); + Collection<Observable<Object>> observables = Collections.emptyList(); + + Observable<Object> o = Observable.zip(observables, new Function<Object[], Object>() { + @Override + public Object apply(final Object[] args) { + assertEquals("No argument should have been passed", 0, args.length); + return invoked; + } + }); + + o.blockingLast(); + } + + @Test + public void downstreamBackpressureRequestsWithFiniteSyncObservables() { + AtomicInteger generatedA = new AtomicInteger(); + AtomicInteger generatedB = new AtomicInteger(); + Observable<Integer> o1 = createInfiniteObservable(generatedA).take(Observable.bufferSize() * 2); + Observable<Integer> o2 = createInfiniteObservable(generatedB).take(Observable.bufferSize() * 2); + + TestObserver<String> to = new TestObserver<>(); + Observable.zip(o1, o2, new BiFunction<Integer, Integer, String>() { + + @Override + public String apply(Integer t1, Integer t2) { + return t1 + "-" + t2; + } + + }).observeOn(Schedulers.computation()).take(Observable.bufferSize() * 2).subscribe(to); + + to.awaitDone(5, TimeUnit.SECONDS); + to.assertNoErrors(); + assertEquals(Observable.bufferSize() * 2, to.values().size()); + System.out.println("Generated => A: " + generatedA.get() + " B: " + generatedB.get()); + assertTrue(generatedA.get() < (Observable.bufferSize() * 3)); + assertTrue(generatedB.get() < (Observable.bufferSize() * 3)); + } + + private Observable<Integer> createInfiniteObservable(final AtomicInteger generated) { + Observable<Integer> o = Observable.fromIterable(new Iterable<Integer>() { + @Override + public Iterator<Integer> iterator() { + return new Iterator<Integer>() { + + @Override + public void remove() { + } + + @Override + public Integer next() { + return generated.getAndIncrement(); + } + + @Override + public boolean hasNext() { + return true; + } + }; + } + }); + return o; + } + + Observable<Integer> OBSERVABLE_OF_5_INTEGERS = OBSERVABLE_OF_5_INTEGERS(new AtomicInteger()); + + Observable<Integer> OBSERVABLE_OF_5_INTEGERS(final AtomicInteger numEmitted) { + return Observable.unsafeCreate(new ObservableSource<Integer>() { + + @Override + public void subscribe(final Observer<? super Integer> o) { + Disposable d = Disposable.empty(); + o.onSubscribe(d); + for (int i = 1; i <= 5; i++) { + if (d.isDisposed()) { + break; + } + numEmitted.incrementAndGet(); + o.onNext(i); + Thread.yield(); + } + o.onComplete(); + } + + }); + } + + Observable<Integer> ASYNC_OBSERVABLE_OF_INFINITE_INTEGERS(final CountDownLatch latch) { + return Observable.unsafeCreate(new ObservableSource<Integer>() { + + @Override + public void subscribe(final Observer<? super Integer> o) { + final Disposable d = Disposable.empty(); + o.onSubscribe(d); + Thread t = new Thread(new Runnable() { + + @Override + public void run() { + System.out.println("-------> subscribe to infinite sequence"); + System.out.println("Starting thread: " + Thread.currentThread()); + int i = 1; + while (!d.isDisposed()) { + o.onNext(i++); + Thread.yield(); + } + o.onComplete(); + latch.countDown(); + System.out.println("Ending thread: " + Thread.currentThread()); + } + }); + t.start(); + + } + + }); + } + + @Test + public void issue1812() { + // https://github.com/ReactiveX/RxJava/issues/1812 + Observable<Integer> zip1 = Observable.zip(Observable.range(0, 1026), Observable.range(0, 1026), + new BiFunction<Integer, Integer, Integer>() { + + @Override + public Integer apply(Integer i1, Integer i2) { + return i1 + i2; + } + }); + Observable<Integer> zip2 = Observable.zip(zip1, Observable.range(0, 1026), + new BiFunction<Integer, Integer, Integer>() { + + @Override + public Integer apply(Integer i1, Integer i2) { + return i1 + i2; + } + }); + List<Integer> expected = new ArrayList<>(); + for (int i = 0; i < 1026; i++) { + expected.add(i * 3); + } + assertEquals(expected, zip2.toList().blockingGet()); + } + + @Test + public void zipRace() { + long startTime = System.currentTimeMillis(); + Observable<Integer> src = Observable.just(1).subscribeOn(Schedulers.computation()); + + // now try and generate a hang by zipping src with itself repeatedly. A + // time limit of 9 seconds ( 1 second less than the test timeout) is + // used so that this test will not timeout on slow machines. + int i = 0; + while (System.currentTimeMillis() - startTime < 9000 && i++ < 100000) { + int value = Observable.zip(src, src, new BiFunction<Integer, Integer, Integer>() { + @Override + public Integer apply(Integer t1, Integer t2) { + return t1 + t2 * 10; + } + }).blockingSingle(0); + + Assert.assertEquals(11, value); + } + } + + @Test + public void zip2() { + Observable.zip(Observable.just(1), + Observable.just(2), + new BiFunction<Integer, Integer, Object>() { + @Override + public Object apply(Integer a, Integer b) throws Exception { + return "" + a + b; + } + } + ) + .test() + .assertResult("12"); + } + + @Test + public void zip3() { + Observable.zip(Observable.just(1), + Observable.just(2), Observable.just(3), + new Function3<Integer, Integer, Integer, Object>() { + @Override + public Object apply(Integer a, Integer b, Integer c) throws Exception { + return "" + a + b + c; + } + } + ) + .test() + .assertResult("123"); + } + + @Test + public void zip4() { + Observable.zip(Observable.just(1), + Observable.just(2), Observable.just(3), + Observable.just(4), + new Function4<Integer, Integer, Integer, Integer, Object>() { + @Override + public Object apply(Integer a, Integer b, Integer c, Integer d) throws Exception { + return "" + a + b + c + d; + } + } + ) + .test() + .assertResult("1234"); + } + + @Test + public void zip5() { + Observable.zip(Observable.just(1), + Observable.just(2), Observable.just(3), + Observable.just(4), Observable.just(5), + new Function5<Integer, Integer, Integer, Integer, Integer, Object>() { + @Override + public Object apply(Integer a, Integer b, Integer c, Integer d, Integer e) throws Exception { + return "" + a + b + c + d + e; + } + } + ) + .test() + .assertResult("12345"); + } + + @Test + public void zip6() { + Observable.zip(Observable.just(1), + Observable.just(2), Observable.just(3), + Observable.just(4), Observable.just(5), + Observable.just(6), + new Function6<Integer, Integer, Integer, Integer, Integer, Integer, Object>() { + @Override + public Object apply(Integer a, Integer b, Integer c, Integer d, Integer e, Integer f) throws Exception { + return "" + a + b + c + d + e + f; + } + } + ) + .test() + .assertResult("123456"); + } + + @Test + public void zip7() { + Observable.zip(Observable.just(1), + Observable.just(2), Observable.just(3), + Observable.just(4), Observable.just(5), + Observable.just(6), Observable.just(7), + new Function7<Integer, Integer, Integer, Integer, Integer, Integer, Integer, Object>() { + @Override + public Object apply(Integer a, Integer b, Integer c, Integer d, Integer e, Integer f, Integer g) + throws Exception { + return "" + a + b + c + d + e + f + g; + } + } + ) + .test() + .assertResult("1234567"); + } + + @Test + public void zip8() { + Observable.zip(Observable.just(1), + Observable.just(2), Observable.just(3), + Observable.just(4), Observable.just(5), + Observable.just(6), Observable.just(7), + Observable.just(8), + new Function8<Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Object>() { + @Override + public Object apply(Integer a, Integer b, Integer c, Integer d, Integer e, Integer f, Integer g, + Integer h) throws Exception { + return "" + a + b + c + d + e + f + g + h; + } + } + ) + .test() + .assertResult("12345678"); + } + + @Test + public void zip9() { + Observable.zip(Observable.just(1), + Observable.just(2), Observable.just(3), + Observable.just(4), Observable.just(5), + Observable.just(6), Observable.just(7), + Observable.just(8), Observable.just(9), + new Function9<Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Object>() { + @Override + public Object apply(Integer a, Integer b, Integer c, Integer d, Integer e, Integer f, Integer g, + Integer h, Integer i) throws Exception { + return "" + a + b + c + d + e + f + g + h + i; + } + } + ) + .test() + .assertResult("123456789"); + } + + @Test + public void zip2DelayError() { + Observable.zip(Observable.just(1).concatWith(Observable.<Integer>error(new TestException())), + Observable.just(2), + new BiFunction<Integer, Integer, Object>() { + @Override + public Object apply(Integer a, Integer b) throws Exception { + return "" + a + b; + } + }, true + ) + .test() + .assertFailure(TestException.class, "12"); + } + + @Test + public void zip2Prefetch() { + Observable.zip(Observable.range(1, 9), + Observable.range(21, 9), + new BiFunction<Integer, Integer, Object>() { + @Override + public Object apply(Integer a, Integer b) throws Exception { + return "" + a + b; + } + }, false, 2 + ) + .takeLast(1) + .test() + .assertResult("929"); + } + + @Test + public void zip2DelayErrorPrefetch() { + Observable.zip(Observable.range(1, 9).concatWith(Observable.<Integer>error(new TestException())), + Observable.range(21, 9), + new BiFunction<Integer, Integer, Object>() { + @Override + public Object apply(Integer a, Integer b) throws Exception { + return "" + a + b; + } + }, true, 2 + ) + .skip(8) + .test() + .assertFailure(TestException.class, "929"); + } + + @Test + public void zipArrayEmpty() { + assertSame(Observable.empty(), Observable.zipArray(Functions.<Object[]>identity(), false, 16)); + } + + @Test + public void zipArrayMany() { + @SuppressWarnings("unchecked") + Observable<Integer>[] arr = new Observable[10]; + + Arrays.fill(arr, Observable.just(1)); + + Observable.zip(Arrays.asList(arr), new Function<Object[], Object>() { + @Override + public Object apply(Object[] a) throws Exception { + return Arrays.toString(a); + } + }) + .test() + .assertResult("[1, 1, 1, 1, 1, 1, 1, 1, 1, 1]"); + } + + @Test + public void dispose() { + TestHelper.checkDisposed(Observable.zip(Observable.just(1), Observable.just(1), new BiFunction<Integer, Integer, Object>() { + @Override + public Object apply(Integer a, Integer b) throws Exception { + return a + b; + } + })); + } + + @Test + public void noCrossBoundaryFusion() { + for (int i = 0; i < 500; i++) { + TestObserver<List<Object>> to = Observable.zip( + Observable.just(1).observeOn(Schedulers.single()).map(new Function<Integer, Object>() { + @Override + public Object apply(Integer v) throws Exception { + return Thread.currentThread().getName().substring(0, 4); + } + }), + Observable.just(1).observeOn(Schedulers.computation()).map(new Function<Integer, Object>() { + @Override + public Object apply(Integer v) throws Exception { + return Thread.currentThread().getName().substring(0, 4); + } + }), + new BiFunction<Object, Object, List<Object>>() { + @Override + public List<Object> apply(Object t1, Object t2) throws Exception { + return Arrays.asList(t1, t2); + } + } + ) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertValueCount(1); + + List<Object> list = to.values().get(0); + + assertTrue(list.toString(), list.contains("RxSi")); + assertTrue(list.toString(), list.contains("RxCo")); + } + } + + @Test + public void eagerDispose() { + final PublishSubject<Integer> ps1 = PublishSubject.create(); + final PublishSubject<Integer> ps2 = PublishSubject.create(); + + TestObserver<Integer> to = new TestObserver<Integer>() { + @Override + public void onNext(Integer t) { + super.onNext(t); + dispose(); + if (ps1.hasObservers()) { + onError(new IllegalStateException("ps1 not disposed")); + } else + if (ps2.hasObservers()) { + onError(new IllegalStateException("ps2 not disposed")); + } else { + onComplete(); + } + } + }; + + Observable.zip(ps1, ps2, new BiFunction<Integer, Integer, Integer>() { + @Override + public Integer apply(Integer t1, Integer t2) throws Exception { + return t1 + t2; + } + }) + .subscribe(to); + + ps1.onNext(1); + ps2.onNext(2); + to.assertResult(3); + } + + @Test + public void firstErrorPreventsSecondSubscription() { + final AtomicInteger counter = new AtomicInteger(); + + List<Observable<?>> observableList = new ArrayList<>(); + observableList.add(Observable.create(new ObservableOnSubscribe<Object>() { + @Override + public void subscribe(ObservableEmitter<Object> e) + throws Exception { throw new TestException(); } + })); + observableList.add(Observable.create(new ObservableOnSubscribe<Object>() { + @Override + public void subscribe(ObservableEmitter<Object> e) + throws Exception { counter.getAndIncrement(); } + })); + + Observable.zip(observableList, + new Function<Object[], Object>() { + @Override + public Object apply(Object[] a) throws Exception { + return a; + } + }) + .test() + .assertFailure(TestException.class) + ; + + assertEquals(0, counter.get()); + } + + @Test + public void observableSourcesInIterable() { + ObservableSource<Integer> source = new ObservableSource<Integer>() { + @Override + public void subscribe(Observer<? super Integer> observer) { + Observable.just(1).subscribe(observer); + } + }; + + Observable.zip(Arrays.asList(source, source), new Function<Object[], Integer>() { + @Override + public Integer apply(Object[] t) throws Throwable { + return 2; + } + }) + .test() + .assertResult(2); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleAmbTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleAmbTest.java new file mode 100644 index 0000000000..083ee46238 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleAmbTest.java @@ -0,0 +1,347 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.single; + +import static org.junit.Assert.*; + +import java.util.*; +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.BiConsumer; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.processors.PublishProcessor; +import io.reactivex.rxjava3.schedulers.Schedulers; +import io.reactivex.rxjava3.subjects.*; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class SingleAmbTest extends RxJavaTest { + @Test + public void ambWithFirstFires() { + PublishProcessor<Integer> pp1 = PublishProcessor.create(); + PublishProcessor<Integer> pp2 = PublishProcessor.create(); + + TestObserver<Integer> to = pp1.single(-99).ambWith(pp2.single(-99)).test(); + + assertTrue(pp1.hasSubscribers()); + assertTrue(pp2.hasSubscribers()); + + pp1.onNext(1); + pp1.onComplete(); + + assertFalse(pp1.hasSubscribers()); + assertFalse(pp2.hasSubscribers()); + + to.assertResult(1); + + } + + @Test + public void ambWithSecondFires() { + PublishProcessor<Integer> pp1 = PublishProcessor.create(); + PublishProcessor<Integer> pp2 = PublishProcessor.create(); + + TestObserver<Integer> to = pp1.single(-99).ambWith(pp2.single(-99)).test(); + + assertTrue(pp1.hasSubscribers()); + assertTrue(pp2.hasSubscribers()); + + pp2.onNext(2); + pp2.onComplete(); + + assertFalse(pp1.hasSubscribers()); + assertFalse(pp2.hasSubscribers()); + + to.assertResult(2); + } + + @Test + public void ambIterableWithFirstFires() { + PublishProcessor<Integer> pp1 = PublishProcessor.create(); + PublishProcessor<Integer> pp2 = PublishProcessor.create(); + + List<Single<Integer>> singles = Arrays.asList(pp1.single(-99), pp2.single(-99)); + TestObserver<Integer> to = Single.amb(singles).test(); + + assertTrue(pp1.hasSubscribers()); + assertTrue(pp2.hasSubscribers()); + + pp1.onNext(1); + pp1.onComplete(); + + assertFalse(pp1.hasSubscribers()); + assertFalse(pp2.hasSubscribers()); + + to.assertResult(1); + + } + + @Test + public void ambIterableWithSecondFires() { + PublishProcessor<Integer> pp1 = PublishProcessor.create(); + PublishProcessor<Integer> pp2 = PublishProcessor.create(); + + List<Single<Integer>> singles = Arrays.asList(pp1.single(-99), pp2.single(-99)); + TestObserver<Integer> to = Single.amb(singles).test(); + + assertTrue(pp1.hasSubscribers()); + assertTrue(pp2.hasSubscribers()); + + pp2.onNext(2); + pp2.onComplete(); + + assertFalse(pp1.hasSubscribers()); + assertFalse(pp2.hasSubscribers()); + + to.assertResult(2); + } + + @Test + public void ambArrayEmpty() { + Single.ambArray() + .test() + .assertFailure(NoSuchElementException.class); + } + + @Test + public void ambSingleSource() { + assertSame(Single.never(), Single.ambArray(Single.never())); + } + + @Test + public void error() { + Single.ambArray(Single.error(new TestException()), Single.just(1)) + .test() + .assertFailure(TestException.class); + } + + @Test + public void nullSourceSuccessRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + List<Throwable> errors = TestHelper.trackPluginErrors(); + + try { + + final Subject<Integer> ps = ReplaySubject.create(); + ps.onNext(1); + + final Single<Integer> source = Single.ambArray(ps.singleOrError(), Single.<Integer>never(), Single.<Integer>never(), null); + + Runnable r1 = new Runnable() { + @Override + public void run() { + source.test(); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + ps.onComplete(); + } + }; + + TestHelper.race(r1, r2); + + if (!errors.isEmpty()) { + TestHelper.assertError(errors, 0, NullPointerException.class); + } + } finally { + RxJavaPlugins.reset(); + } + } + } + + @Test + public void multipleErrorRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + List<Throwable> errors = TestHelper.trackPluginErrors(); + + try { + + final Subject<Integer> ps1 = PublishSubject.create(); + final Subject<Integer> ps2 = PublishSubject.create(); + + Single.ambArray(ps1.singleOrError(), ps2.singleOrError()).test(); + + final TestException ex = new TestException(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + ps1.onError(ex); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + ps2.onError(ex); + } + }; + + TestHelper.race(r1, r2); + + if (!errors.isEmpty()) { + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } + } finally { + RxJavaPlugins.reset(); + } + } + } + + @Test + public void successErrorRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + List<Throwable> errors = TestHelper.trackPluginErrors(); + + try { + + final Subject<Integer> ps1 = PublishSubject.create(); + final Subject<Integer> ps2 = PublishSubject.create(); + + Single.ambArray(ps1.singleOrError(), ps2.singleOrError()).test(); + + final TestException ex = new TestException(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + ps1.onNext(1); + ps1.onComplete(); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + ps2.onError(ex); + } + }; + + TestHelper.race(r1, r2); + + if (!errors.isEmpty()) { + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } + } finally { + RxJavaPlugins.reset(); + } + } + } + + @Test + public void manySources() { + Single<?>[] sources = new Single[32]; + Arrays.fill(sources, Single.never()); + sources[31] = Single.just(31); + + Single.amb(Arrays.asList(sources)) + .test() + .assertResult(31); + } + + @Test + public void ambWithOrder() { + Single<Integer> error = Single.error(new RuntimeException()); + Single.just(1).ambWith(error).test().assertValue(1); + } + + @Test + public void ambIterableOrder() { + Single<Integer> error = Single.error(new RuntimeException()); + Single.amb(Arrays.asList(Single.just(1), error)).test().assertValue(1); + } + + @Test + public void ambArrayOrder() { + Single<Integer> error = Single.error(new RuntimeException()); + Single.ambArray(Single.just(1), error).test().assertValue(1); + } + + @Test + public void noWinnerSuccessDispose() throws Exception { + for (int i = 0; i < TestHelper.RACE_LONG_LOOPS; i++) { + final AtomicBoolean interrupted = new AtomicBoolean(); + final CountDownLatch cdl = new CountDownLatch(1); + + Single.ambArray( + Single.just(1) + .subscribeOn(Schedulers.single()) + .observeOn(Schedulers.computation()), + Single.never() + ) + .subscribe(new BiConsumer<Object, Throwable>() { + @Override + public void accept(Object v, Throwable e) throws Exception { + assertNotNull(v); + assertNull(e); + interrupted.set(Thread.currentThread().isInterrupted()); + cdl.countDown(); + } + }); + + assertTrue(cdl.await(500, TimeUnit.SECONDS)); + assertFalse("Interrupted!", interrupted.get()); + } + } + + @Test + public void noWinnerErrorDispose() throws Exception { + final TestException ex = new TestException(); + for (int i = 0; i < TestHelper.RACE_LONG_LOOPS; i++) { + final AtomicBoolean interrupted = new AtomicBoolean(); + final CountDownLatch cdl = new CountDownLatch(1); + + Single.ambArray( + Single.error(ex) + .subscribeOn(Schedulers.single()) + .observeOn(Schedulers.computation()), + Single.never() + ) + .subscribe(new BiConsumer<Object, Throwable>() { + @Override + public void accept(Object v, Throwable e) throws Exception { + assertNull(v); + assertNotNull(e); + interrupted.set(Thread.currentThread().isInterrupted()); + cdl.countDown(); + } + }); + + assertTrue(cdl.await(500, TimeUnit.SECONDS)); + assertFalse("Interrupted!", interrupted.get()); + } + } + + @Test + public void singleSourcesInIterable() { + SingleSource<Integer> source = new SingleSource<Integer>() { + @Override + public void subscribe(SingleObserver<? super Integer> observer) { + Single.just(1).subscribe(observer); + } + }; + + Single.amb(Arrays.asList(source, source)) + .test() + .assertResult(1); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleBlockingSubscribeTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleBlockingSubscribeTest.java new file mode 100644 index 0000000000..a29c10975f --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleBlockingSubscribeTest.java @@ -0,0 +1,343 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.single; + +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import java.util.concurrent.TimeUnit; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.Single; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.schedulers.Schedulers; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class SingleBlockingSubscribeTest { + + @Test + public void noArgSuccess() { + Single.just(1) + .blockingSubscribe(); + } + + @Test + public void noArgSuccessAsync() { + Single.just(1) + .delay(100, TimeUnit.MILLISECONDS) + .blockingSubscribe(); + } + + @Test + public void noArgError() throws Throwable { + TestHelper.withErrorTracking(errors -> { + Single.error(new TestException()) + .blockingSubscribe(); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + }); + } + + @Test + public void noArgErrorAsync() throws Throwable { + TestHelper.withErrorTracking(errors -> { + Single.error(new TestException()) + .delay(100, TimeUnit.MILLISECONDS, Schedulers.computation(), true) + .blockingSubscribe(); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + }); + } + + @Test + public void oneArgSuccess() throws Throwable { + @SuppressWarnings("unchecked") + Consumer<Integer> success = mock(Consumer.class); + + Single.just(1) + .blockingSubscribe(success); + + verify(success).accept(1); + } + + @Test + public void oneArgSuccessAsync() throws Throwable { + @SuppressWarnings("unchecked") + Consumer<Integer> success = mock(Consumer.class); + + Single.just(1) + .delay(50, TimeUnit.MILLISECONDS) + .blockingSubscribe(success); + + verify(success).accept(1); + } + + @Test + public void oneArgSuccessFails() throws Throwable { + TestHelper.withErrorTracking(errors -> { + @SuppressWarnings("unchecked") + Consumer<Integer> success = mock(Consumer.class); + doThrow(new TestException()).when(success).accept(any()); + + Single.just(1) + .blockingSubscribe(success); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + + verify(success).accept(1); + }); + } + + @Test + public void oneArgError() throws Throwable { + TestHelper.withErrorTracking(errors -> { + @SuppressWarnings("unchecked") + Consumer<Integer> success = mock(Consumer.class); + + Single.<Integer>error(new TestException()) + .blockingSubscribe(success); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + + verify(success, never()).accept(any()); + }); + } + + @Test + public void oneArgErrorAsync() throws Throwable { + TestHelper.withErrorTracking(errors -> { + @SuppressWarnings("unchecked") + Consumer<Integer> success = mock(Consumer.class); + + Single.<Integer>error(new TestException()) + .delay(50, TimeUnit.MILLISECONDS, Schedulers.computation(), true) + .blockingSubscribe(success); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + + verify(success, never()).accept(any()); + }); + } + + @Test + public void twoArgSuccess() throws Throwable { + @SuppressWarnings("unchecked") + Consumer<Integer> success = mock(Consumer.class); + @SuppressWarnings("unchecked") + Consumer<? super Throwable> consumer = mock(Consumer.class); + + Single.just(1) + .blockingSubscribe(success, consumer); + + verify(success).accept(1); + verify(consumer, never()).accept(any()); + } + + @Test + public void twoArgSuccessAsync() throws Throwable { + @SuppressWarnings("unchecked") + Consumer<Integer> success = mock(Consumer.class); + @SuppressWarnings("unchecked") + Consumer<? super Throwable> consumer = mock(Consumer.class); + + Single.just(1) + .delay(50, TimeUnit.MILLISECONDS) + .blockingSubscribe(success, consumer); + + verify(success).accept(any()); + verify(consumer, never()).accept(any()); + } + + @Test + public void twoArgSuccessFails() throws Throwable { + TestHelper.withErrorTracking(errors -> { + @SuppressWarnings("unchecked") + Consumer<Integer> success = mock(Consumer.class); + doThrow(new TestException()).when(success).accept(any()); + @SuppressWarnings("unchecked") + Consumer<? super Throwable> consumer = mock(Consumer.class); + + Single.just(1) + .blockingSubscribe(success, consumer); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + + verify(success).accept(any()); + verify(consumer, never()).accept(any()); + }); + } + + @Test + public void twoArgError() throws Throwable { + TestHelper.withErrorTracking(errors -> { + @SuppressWarnings("unchecked") + Consumer<Integer> success = mock(Consumer.class); + @SuppressWarnings("unchecked") + Consumer<? super Throwable> consumer = mock(Consumer.class); + + Single.<Integer>error(new TestException()) + .blockingSubscribe(success, consumer); + + assertTrue("" + errors, errors.isEmpty()); + + verify(success, never()).accept(any()); + verify(consumer).accept(any(TestException.class)); + }); + } + + @Test + public void twoArgErrorAsync() throws Throwable { + TestHelper.withErrorTracking(errors -> { + @SuppressWarnings("unchecked") + Consumer<Integer> success = mock(Consumer.class); + @SuppressWarnings("unchecked") + Consumer<? super Throwable> consumer = mock(Consumer.class); + + Single.<Integer>error(new TestException()) + .delay(50, TimeUnit.MILLISECONDS, Schedulers.computation(), true) + .blockingSubscribe(success, consumer); + + assertTrue("" + errors, errors.isEmpty()); + + verify(success, never()).accept(any()); + verify(consumer).accept(any(TestException.class)); + }); + } + + @Test + public void twoArgErrorFails() throws Throwable { + TestHelper.withErrorTracking(errors -> { + @SuppressWarnings("unchecked") + Consumer<Integer> success = mock(Consumer.class); + @SuppressWarnings("unchecked") + Consumer<? super Throwable> consumer = mock(Consumer.class); + doThrow(new TestException()).when(consumer).accept(any()); + + Single.<Integer>error(new TestException()) + .delay(50, TimeUnit.MILLISECONDS, Schedulers.computation(), true) + .blockingSubscribe(success, consumer); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + + verify(success, never()).accept(any()); + verify(consumer).accept(any(TestException.class)); + }); + } + + @Test + public void twoArgInterrupted() throws Throwable { + TestHelper.withErrorTracking(errors -> { + Action onDispose = mock(Action.class); + + @SuppressWarnings("unchecked") + Consumer<Integer> success = mock(Consumer.class); + @SuppressWarnings("unchecked") + Consumer<? super Throwable> consumer = mock(Consumer.class); + + Thread.currentThread().interrupt(); + + Single.<Integer>never() + .doOnDispose(onDispose) + .blockingSubscribe(success, consumer); + + assertTrue("" + errors, errors.isEmpty()); + + verify(onDispose).run(); + verify(success, never()).accept(any()); + verify(consumer).accept(any(InterruptedException.class)); + }); + } + + @Test + public void observerSuccess() { + TestObserver<Integer> to = new TestObserver<>(); + + Single.just(1) + .blockingSubscribe(to); + + to.assertResult(1); + } + + @Test + public void observerSuccessAsync() { + TestObserver<Integer> to = new TestObserver<>(); + + Single.just(1) + .delay(50, TimeUnit.MILLISECONDS, Schedulers.computation(), true) + .blockingSubscribe(to); + + to.assertResult(1); + } + + @Test + public void observerError() { + TestObserver<Object> to = new TestObserver<>(); + + Single.error(new TestException()) + .blockingSubscribe(to); + + to.assertFailure(TestException.class); + } + + @Test + public void observerErrorAsync() { + TestObserver<Object> to = new TestObserver<>(); + + Single.error(new TestException()) + .delay(50, TimeUnit.MILLISECONDS, Schedulers.computation(), true) + .blockingSubscribe(to); + + to.assertFailure(TestException.class); + } + + @Test + public void observerDispose() throws Throwable { + Action onDispose = mock(Action.class); + + TestObserver<Object> to = new TestObserver<>(); + to.dispose(); + + Single.never() + .doOnDispose(onDispose) + .blockingSubscribe(to); + + to.assertEmpty(); + + verify(onDispose).run(); + } + + @Test + public void ovserverInterrupted() throws Throwable { + TestHelper.withErrorTracking(errors -> { + Action onDispose = mock(Action.class); + + TestObserver<Object> to = new TestObserver<>(); + + Thread.currentThread().interrupt(); + + Single.never() + .doOnDispose(onDispose) + .blockingSubscribe(to); + + assertTrue("" + errors, errors.isEmpty()); + + verify(onDispose).run(); + to.assertFailure(InterruptedException.class); + }); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleCacheTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleCacheTest.java new file mode 100644 index 0000000000..ca46a92f12 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleCacheTest.java @@ -0,0 +1,99 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.single; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.processors.PublishProcessor; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class SingleCacheTest extends RxJavaTest { + + @Test + public void cancelImmediately() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + + Single<Integer> cached = pp.single(-99).cache(); + + TestObserver<Integer> to = cached.test(true); + + pp.onNext(1); + pp.onComplete(); + + to.assertEmpty(); + + cached.test().assertResult(1); + } + + @Test + public void addRemoveRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + PublishProcessor<Integer> pp = PublishProcessor.create(); + + final Single<Integer> cached = pp.single(-99).cache(); + + final TestObserver<Integer> to1 = cached.test(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + to1.dispose(); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + cached.test(); + } + }; + + TestHelper.race(r1, r2); + } + } + + @Test + public void doubleDispose() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + + final Single<Integer> cached = pp.single(-99).cache(); + + SingleObserver<Integer> doubleDisposer = new SingleObserver<Integer>() { + + @Override + public void onSubscribe(Disposable d) { + d.dispose(); + d.dispose(); + } + + @Override + public void onSuccess(Integer value) { + + } + + @Override + public void onError(Throwable e) { + + } + }; + cached.subscribe(doubleDisposer); + + cached.test(); + + cached.subscribe(doubleDisposer); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleConcatArrayDelayErrorTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleConcatArrayDelayErrorTest.java new file mode 100644 index 0000000000..00ff0d5ad1 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleConcatArrayDelayErrorTest.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.single; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.Single; +import io.reactivex.rxjava3.exceptions.TestException; + +public class SingleConcatArrayDelayErrorTest { + + @Test + public void normal() { + Single.concatArrayDelayError( + Single.just(1), + Single.<Integer>error(new TestException()), + Single.just(2) + ) + .test() + .assertFailure(TestException.class, 1, 2); + } + +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleConcatArrayEagerDelayErrorTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleConcatArrayEagerDelayErrorTest.java new file mode 100644 index 0000000000..bfd96ad3a9 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleConcatArrayEagerDelayErrorTest.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.single; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.Single; +import io.reactivex.rxjava3.exceptions.TestException; + +public class SingleConcatArrayEagerDelayErrorTest { + + @Test + public void normal() { + Single.concatArrayEagerDelayError( + Single.just(1), + Single.<Integer>error(new TestException()), + Single.just(2) + ) + .test() + .assertFailure(TestException.class, 1, 2); + } + +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleConcatDelayErrorTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleConcatDelayErrorTest.java new file mode 100644 index 0000000000..18dc2c83d2 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleConcatDelayErrorTest.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.single; + +import java.util.Arrays; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; + +public class SingleConcatDelayErrorTest { + + @Test + public void normalIterable() { + Single.concatDelayError(Arrays.asList( + Single.just(1), + Single.<Integer>error(new TestException()), + Single.just(2) + )) + .test() + .assertFailure(TestException.class, 1, 2); + } + + @Test + public void normalPublisher() { + Single.concatDelayError(Flowable.fromArray( + Single.just(1), + Single.<Integer>error(new TestException()), + Single.just(2) + )) + .test() + .assertFailure(TestException.class, 1, 2); + } + + @Test + public void normalPublisherPrefetch() { + Single.concatDelayError(Flowable.fromArray( + Single.just(1), + Single.<Integer>error(new TestException()), + Single.just(2) + ), 1) + .test() + .assertFailure(TestException.class, 1, 2); + } + +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleConcatEagerTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleConcatEagerTest.java new file mode 100644 index 0000000000..892ed8dd4d --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleConcatEagerTest.java @@ -0,0 +1,141 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.single; + +import java.util.Arrays; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; + +public class SingleConcatEagerTest { + + @Test + public void iterableNormal() { + Single.concatEager(Arrays.asList( + Single.just(1), + Single.just(2) + )) + .test() + .assertResult(1, 2); + } + + @Test + public void iterableNormalMaxConcurrency() { + Single.concatEager(Arrays.asList( + Single.just(1), + Single.just(2) + ), 1) + .test() + .assertResult(1, 2); + } + + @Test + public void iterableError() { + Single.concatEager(Arrays.asList( + Single.just(1), + Single.error(new TestException()), + Single.just(2) + )) + .test() + .assertFailure(TestException.class, 1); + } + + @Test + public void iterableErrorMaxConcurrency() { + Single.concatEager(Arrays.asList( + Single.just(1), + Single.error(new TestException()), + Single.just(2) + ), 1) + .test() + .assertFailure(TestException.class, 1); + } + + @Test + public void publisherNormal() { + Single.concatEager(Flowable.fromArray( + Single.just(1), + Single.just(2) + )) + .test() + .assertResult(1, 2); + } + + @Test + public void publisherNormalMaxConcurrency() { + Single.concatEager(Flowable.fromArray( + Single.just(1), + Single.just(2) + ), 1) + .test() + .assertResult(1, 2); + } + + @Test + public void publisherError() { + Single.concatEager(Flowable.fromArray( + Single.just(1), + Single.error(new TestException()), + Single.just(2) + )) + .test() + .assertFailure(TestException.class, 1); + } + + @Test + public void iterableDelayError() { + Single.concatEagerDelayError(Arrays.asList( + Single.just(1), + Single.error(new TestException()), + Single.just(2) + )) + .test() + .assertFailure(TestException.class, 1, 2); + } + + @Test + public void iterableDelayErrorMaxConcurrency() { + Single.concatEagerDelayError(Arrays.asList( + Single.just(1), + Single.error(new TestException()), + Single.just(2) + ), 1) + .test() + .assertFailure(TestException.class, 1, 2); + } + + @Test + public void publisherDelayError() { + Single.concatEagerDelayError(Flowable.fromArray( + Single.just(1), + Single.error(new TestException()), + Single.just(2) + )) + .test() + .assertFailure(TestException.class, 1, 2); + } + + @Test + public void publisherDelayErrorMaxConcurrency() { + Single.concatEagerDelayError(Flowable.fromArray( + Single.just(1), + Single.error(new TestException()), + Single.just(2) + ), 1) + .test() + .assertFailure(TestException.class, 1, 2); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleConcatMapCompletableTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleConcatMapCompletableTest.java new file mode 100644 index 0000000000..3c603b26b9 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleConcatMapCompletableTest.java @@ -0,0 +1,114 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.single; + +import static org.junit.Assert.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class SingleConcatMapCompletableTest extends RxJavaTest { + + @Test + public void dispose() { + TestHelper.checkDisposed(Single.just(1).concatMapCompletable(new Function<Integer, Completable>() { + @Override + public Completable apply(Integer v) throws Exception { + return Completable.complete(); + } + })); + } + + @Test + public void normal() { + final boolean[] b = { false }; + + Single.just(1) + .concatMapCompletable(new Function<Integer, Completable>() { + @Override + public Completable apply(Integer t) throws Exception { + return Completable.complete().doOnComplete(new Action() { + @Override + public void run() throws Exception { + b[0] = true; + } + }); + } + }) + .test() + .assertResult(); + + assertTrue(b[0]); + } + + @Test + public void error() { + final boolean[] b = { false }; + + Single.<Integer>error(new TestException()) + .concatMapCompletable(new Function<Integer, Completable>() { + @Override + public Completable apply(Integer t) throws Exception { + return Completable.complete().doOnComplete(new Action() { + @Override + public void run() throws Exception { + b[0] = true; + } + }); + } + }) + .test() + .assertFailure(TestException.class); + + assertFalse(b[0]); + } + + @Test + public void mapperThrows() { + final boolean[] b = { false }; + + Single.just(1) + .concatMapCompletable(new Function<Integer, Completable>() { + @Override + public Completable apply(Integer t) throws Exception { + throw new TestException(); + } + }) + .test() + .assertFailure(TestException.class); + + assertFalse(b[0]); + } + + @Test + public void mapperReturnsNull() { + final boolean[] b = { false }; + + Single.just(1) + .concatMapCompletable(new Function<Integer, Completable>() { + @Override + public Completable apply(Integer t) throws Exception { + return null; + } + }) + .test() + .assertFailure(NullPointerException.class); + + assertFalse(b[0]); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleConcatMapMaybeTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleConcatMapMaybeTest.java new file mode 100644 index 0000000000..24710d824a --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleConcatMapMaybeTest.java @@ -0,0 +1,141 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.single; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class SingleConcatMapMaybeTest extends RxJavaTest { + @Test + public void concatMapMaybeValue() { + Single.just(1).concatMapMaybe(new Function<Integer, MaybeSource<Integer>>() { + @Override public MaybeSource<Integer> apply(final Integer integer) throws Exception { + if (integer == 1) { + return Maybe.just(2); + } + + return Maybe.just(1); + } + }) + .test() + .assertResult(2); + } + + @Test + public void concatMapMaybeValueDifferentType() { + Single.just(1).concatMapMaybe(new Function<Integer, MaybeSource<String>>() { + @Override public MaybeSource<String> apply(final Integer integer) throws Exception { + if (integer == 1) { + return Maybe.just("2"); + } + + return Maybe.just("1"); + } + }) + .test() + .assertResult("2"); + } + + @Test + public void concatMapMaybeValueNull() { + Single.just(1).concatMapMaybe(new Function<Integer, MaybeSource<Integer>>() { + @Override public MaybeSource<Integer> apply(final Integer integer) throws Exception { + return null; + } + }) + .to(TestHelper.<Integer>testConsumer()) + .assertNoValues() + .assertError(NullPointerException.class) + .assertErrorMessage("The mapper returned a null MaybeSource"); + } + + @Test + public void concatMapMaybeValueErrorThrown() { + Single.just(1).concatMapMaybe(new Function<Integer, MaybeSource<Integer>>() { + @Override public MaybeSource<Integer> apply(final Integer integer) throws Exception { + throw new RuntimeException("something went terribly wrong!"); + } + }) + .to(TestHelper.<Integer>testConsumer()) + .assertNoValues() + .assertError(RuntimeException.class) + .assertErrorMessage("something went terribly wrong!"); + } + + @Test + public void concatMapMaybeError() { + RuntimeException exception = new RuntimeException("test"); + + Single.error(exception).concatMapMaybe(new Function<Object, MaybeSource<Object>>() { + @Override public MaybeSource<Object> apply(final Object integer) throws Exception { + return Maybe.just(new Object()); + } + }) + .test() + .assertError(exception); + } + + @Test + public void dispose() { + TestHelper.checkDisposed(Single.just(1).concatMapMaybe(new Function<Integer, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Integer v) throws Exception { + return Maybe.just(1); + } + })); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeSingleToMaybe(new Function<Single<Integer>, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Single<Integer> v) throws Exception { + return v.concatMapMaybe(new Function<Integer, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Integer v) throws Exception { + return Maybe.just(1); + } + }); + } + }); + } + + @Test + public void mapsToError() { + Single.just(1).concatMapMaybe(new Function<Integer, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Integer v) throws Exception { + return Maybe.error(new TestException()); + } + }) + .test() + .assertFailure(TestException.class); + } + + @Test + public void mapsToEmpty() { + Single.just(1).concatMapMaybe(new Function<Integer, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Integer v) throws Exception { + return Maybe.empty(); + } + }) + .test() + .assertResult(); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleConcatMapTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleConcatMapTest.java new file mode 100644 index 0000000000..1fa2ebaf0b --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleConcatMapTest.java @@ -0,0 +1,132 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.single; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class SingleConcatMapTest extends RxJavaTest { + + @Test + public void concatMapValue() { + Single.just(1).concatMap(new Function<Integer, SingleSource<Integer>>() { + @Override public SingleSource<Integer> apply(final Integer integer) throws Exception { + if (integer == 1) { + return Single.just(2); + } + + return Single.just(1); + } + }) + .test() + .assertResult(2); + } + + @Test + public void concatMapValueDifferentType() { + Single.just(1).concatMap(new Function<Integer, SingleSource<String>>() { + @Override public SingleSource<String> apply(final Integer integer) throws Exception { + if (integer == 1) { + return Single.just("2"); + } + + return Single.just("1"); + } + }) + .test() + .assertResult("2"); + } + + @Test + public void concatMapValueNull() { + Single.just(1).concatMap(new Function<Integer, SingleSource<Integer>>() { + @Override public SingleSource<Integer> apply(final Integer integer) throws Exception { + return null; + } + }) + .to(TestHelper.<Integer>testConsumer()) + .assertNoValues() + .assertError(NullPointerException.class) + .assertErrorMessage("The single returned by the mapper is null"); + } + + @Test + public void concatMapValueErrorThrown() { + Single.just(1).concatMap(new Function<Integer, SingleSource<Integer>>() { + @Override public SingleSource<Integer> apply(final Integer integer) throws Exception { + throw new RuntimeException("something went terribly wrong!"); + } + }) + .to(TestHelper.<Integer>testConsumer()) + .assertNoValues() + .assertError(RuntimeException.class) + .assertErrorMessage("something went terribly wrong!"); + } + + @Test + public void concatMapError() { + RuntimeException exception = new RuntimeException("test"); + + Single.error(exception).concatMap(new Function<Object, SingleSource<Object>>() { + @Override public SingleSource<Object> apply(final Object integer) throws Exception { + return Single.just(new Object()); + } + }) + .test() + .assertError(exception); + } + + @Test + public void dispose() { + TestHelper.checkDisposed(Single.just(1).concatMap(new Function<Integer, SingleSource<Integer>>() { + @Override + public SingleSource<Integer> apply(Integer v) throws Exception { + return Single.just(2); + } + })); + } + + @Test + public void mappedSingleOnError() { + Single.just(1).concatMap(new Function<Integer, SingleSource<Integer>>() { + @Override + public SingleSource<Integer> apply(Integer v) throws Exception { + return Single.error(new TestException()); + } + }) + .test() + .assertFailure(TestException.class); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeSingle(new Function<Single<Object>, SingleSource<Object>>() { + @Override + public SingleSource<Object> apply(Single<Object> s) + throws Exception { + return s.concatMap(new Function<Object, SingleSource<? extends Object>>() { + @Override + public SingleSource<? extends Object> apply(Object v) + throws Exception { + return Single.just(v); + } + }); + } + }); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleConcatPublisherTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleConcatPublisherTest.java new file mode 100644 index 0000000000..883903e1d9 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleConcatPublisherTest.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.single; + +import java.util.concurrent.Callable; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; + +public class SingleConcatPublisherTest extends RxJavaTest { + + @Test + public void scalar() { + Single.concat(Flowable.just(Single.just(1))) + .test() + .assertResult(1); + } + + @Test + public void callable() { + Single.concat(Flowable.fromCallable(new Callable<Single<Integer>>() { + @Override + public Single<Integer> call() throws Exception { + return Single.just(1); + } + })) + .test() + .assertResult(1); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleConcatTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleConcatTest.java new file mode 100644 index 0000000000..fb94277e43 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleConcatTest.java @@ -0,0 +1,181 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.single; + +import static org.junit.Assert.*; + +import java.util.Arrays; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.processors.PublishProcessor; +import io.reactivex.rxjava3.subscribers.TestSubscriber; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class SingleConcatTest extends RxJavaTest { + @Test + public void concatWith() { + Single.just(1).concatWith(Single.just(2)) + .test() + .assertResult(1, 2); + } + + @Test + public void concat2() { + Single.concat(Single.just(1), Single.just(2)) + .test() + .assertResult(1, 2); + } + + @Test + public void concat3() { + Single.concat(Single.just(1), Single.just(2), Single.just(3)) + .test() + .assertResult(1, 2, 3); + } + + @Test + public void concat4() { + Single.concat(Single.just(1), Single.just(2), Single.just(3), Single.just(4)) + .test() + .assertResult(1, 2, 3, 4); + } + + @SuppressWarnings("unchecked") + @Test + public void concatArray() { + for (int i = 1; i < 100; i++) { + Single<Integer>[] array = new Single[i]; + + Arrays.fill(array, Single.just(1)); + + Single.concatArray(array) + .to(TestHelper.<Integer>testConsumer()) + .assertSubscribed() + .assertValueCount(i) + .assertNoErrors() + .assertComplete(); + } + } + + @Test + public void concatArrayEagerTest() { + PublishProcessor<String> pp1 = PublishProcessor.create(); + PublishProcessor<String> pp2 = PublishProcessor.create(); + + TestSubscriber<String> ts = Single.concatArrayEager(pp1.single("1"), pp2.single("2")).test(); + + assertTrue(pp1.hasSubscribers()); + assertTrue(pp2.hasSubscribers()); + + pp2.onComplete(); + ts.assertEmpty(); + pp1.onComplete(); + + ts.assertResult("1", "2"); + ts.assertComplete(); + } + + @Test + public void concatEagerIterableTest() { + PublishProcessor<String> pp1 = PublishProcessor.create(); + PublishProcessor<String> pp2 = PublishProcessor.create(); + + TestSubscriber<String> ts = Single.concatEager(Arrays.asList(pp1.single("2"), pp2.single("1"))).test(); + + assertTrue(pp1.hasSubscribers()); + assertTrue(pp2.hasSubscribers()); + + pp2.onComplete(); + ts.assertEmpty(); + pp1.onComplete(); + + ts.assertResult("2", "1"); + ts.assertComplete(); + } + + @Test + public void concatEagerPublisherTest() { + PublishProcessor<String> pp1 = PublishProcessor.create(); + PublishProcessor<String> pp2 = PublishProcessor.create(); + + TestSubscriber<String> ts = Single.concatEager(Flowable.just(pp1.single("1"), pp2.single("2"))).test(); + + assertTrue(pp1.hasSubscribers()); + assertTrue(pp2.hasSubscribers()); + + pp2.onComplete(); + ts.assertEmpty(); + pp1.onComplete(); + + ts.assertResult("1", "2"); + ts.assertComplete(); + } + + @SuppressWarnings("unchecked") + @Test + public void concatObservable() { + for (int i = 1; i < 100; i++) { + Single<Integer>[] array = new Single[i]; + + Arrays.fill(array, Single.just(1)); + + Single.concat(Observable.fromArray(array)) + .to(TestHelper.<Integer>testConsumer()) + .assertSubscribed() + .assertValueCount(i) + .assertNoErrors() + .assertComplete(); + } + } + + @Test + public void noSubsequentSubscription() { + final int[] calls = { 0 }; + + Single<Integer> source = Single.create(new SingleOnSubscribe<Integer>() { + @Override + public void subscribe(SingleEmitter<Integer> s) throws Exception { + calls[0]++; + s.onSuccess(1); + } + }); + + Single.concatArray(source, source).firstElement() + .test() + .assertResult(1); + + assertEquals(1, calls[0]); + } + + @Test + public void noSubsequentSubscriptionIterable() { + final int[] calls = { 0 }; + + Single<Integer> source = Single.create(new SingleOnSubscribe<Integer>() { + @Override + public void subscribe(SingleEmitter<Integer> s) throws Exception { + calls[0]++; + s.onSuccess(1); + } + }); + + Single.concat(Arrays.asList(source, source)).firstElement() + .test() + .assertResult(1); + + assertEquals(1, calls[0]); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleContainstTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleContainstTest.java new file mode 100644 index 0000000000..2f5baa4ed0 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleContainstTest.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.single; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.BiPredicate; + +public class SingleContainstTest extends RxJavaTest { + + @Test + public void comparerThrows() { + Single.just(1) + .contains(2, new BiPredicate<Object, Object>() { + @Override + public boolean test(Object a, Object b) throws Exception { + throw new TestException(); + } + }) + .test() + .assertFailure(TestException.class); + } + + @Test + public void error() { + Single.error(new TestException()) + .contains(2) + .test() + .assertFailure(TestException.class); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleCreateTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleCreateTest.java new file mode 100644 index 0000000000..862757560a --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleCreateTest.java @@ -0,0 +1,319 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.single; + +import static org.junit.Assert.*; + +import java.io.IOException; +import java.util.List; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.Cancellable; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.testsupport.*; + +public class SingleCreateTest extends RxJavaTest { + + @Test + @SuppressUndeliverable + public void basic() { + final Disposable d = Disposable.empty(); + + Single.<Integer>create(new SingleOnSubscribe<Integer>() { + @Override + public void subscribe(SingleEmitter<Integer> e) throws Exception { + e.setDisposable(d); + + e.onSuccess(1); + e.onError(new TestException()); + e.onSuccess(2); + e.onError(new TestException()); + } + }) + .test() + .assertResult(1); + + assertTrue(d.isDisposed()); + } + + @Test + @SuppressUndeliverable + public void basicWithCancellable() { + final Disposable d1 = Disposable.empty(); + final Disposable d2 = Disposable.empty(); + + Single.<Integer>create(new SingleOnSubscribe<Integer>() { + @Override + public void subscribe(SingleEmitter<Integer> e) throws Exception { + e.setDisposable(d1); + e.setCancellable(new Cancellable() { + @Override + public void cancel() throws Exception { + d2.dispose(); + } + }); + + e.onSuccess(1); + e.onError(new TestException()); + e.onSuccess(2); + e.onError(new TestException()); + } + }) + .test() + .assertResult(1); + + assertTrue(d1.isDisposed()); + assertTrue(d2.isDisposed()); + } + + @Test + @SuppressUndeliverable + public void basicWithError() { + final Disposable d = Disposable.empty(); + + Single.<Integer>create(new SingleOnSubscribe<Integer>() { + @Override + public void subscribe(SingleEmitter<Integer> e) throws Exception { + e.setDisposable(d); + + e.onError(new TestException()); + e.onSuccess(2); + e.onError(new TestException()); + } + }) + .test() + .assertFailure(TestException.class); + + assertTrue(d.isDisposed()); + } + + @Test(expected = IllegalArgumentException.class) + public void unsafeCreate() { + Single.unsafeCreate(Single.just(1)); + } + + @Test + public void createCallbackThrows() { + Single.create(new SingleOnSubscribe<Object>() { + @Override + public void subscribe(SingleEmitter<Object> s) throws Exception { + throw new TestException(); + } + }) + .test() + .assertFailure(TestException.class); + } + + @Test + public void dispose() { + TestHelper.checkDisposed(Single.create(new SingleOnSubscribe<Object>() { + @Override + public void subscribe(SingleEmitter<Object> s) throws Exception { + s.onSuccess(1); + } + })); + } + + @Test + public void createNullSuccess() { + Single.create(new SingleOnSubscribe<Object>() { + @Override + public void subscribe(SingleEmitter<Object> s) throws Exception { + s.onSuccess(null); + s.onSuccess(null); + } + }) + .test() + .assertFailure(NullPointerException.class); + } + + @Test + public void createNullError() { + Single.create(new SingleOnSubscribe<Object>() { + @Override + public void subscribe(SingleEmitter<Object> s) throws Exception { + s.onError(null); + s.onError(null); + } + }) + .test() + .assertFailure(NullPointerException.class); + } + + @Test + public void createConsumerThrows() { + Single.create(new SingleOnSubscribe<Object>() { + @Override + public void subscribe(SingleEmitter<Object> s) throws Exception { + try { + s.onSuccess(1); + fail("Should have thrown"); + } catch (TestException ex) { + // expected + } + } + }) + .subscribe(new SingleObserver<Object>() { + @Override + public void onSubscribe(Disposable d) { + + } + + @Override + public void onSuccess(Object value) { + throw new TestException(); + } + + @Override + public void onError(Throwable e) { + + } + }); + } + + @Test + public void createConsumerThrowsResource() { + Single.create(new SingleOnSubscribe<Object>() { + @Override + public void subscribe(SingleEmitter<Object> s) throws Exception { + Disposable d = Disposable.empty(); + s.setDisposable(d); + try { + s.onSuccess(1); + fail("Should have thrown"); + } catch (TestException ex) { + // expected + } + + assertTrue(d.isDisposed()); + } + }) + .subscribe(new SingleObserver<Object>() { + @Override + public void onSubscribe(Disposable d) { + + } + + @Override + public void onSuccess(Object value) { + throw new TestException(); + } + + @Override + public void onError(Throwable e) { + + } + }); + } + + @Test + public void createConsumerThrowsOnError() { + Single.create(new SingleOnSubscribe<Object>() { + @Override + public void subscribe(SingleEmitter<Object> s) throws Exception { + try { + s.onError(new IOException()); + fail("Should have thrown"); + } catch (TestException ex) { + // expected + } + } + }) + .subscribe(new SingleObserver<Object>() { + @Override + public void onSubscribe(Disposable d) { + } + + @Override + public void onSuccess(Object value) { + } + + @Override + public void onError(Throwable e) { + throw new TestException(); + } + }); + } + + @Test + public void createConsumerThrowsResourceOnError() { + Single.create(new SingleOnSubscribe<Object>() { + @Override + public void subscribe(SingleEmitter<Object> s) throws Exception { + Disposable d = Disposable.empty(); + s.setDisposable(d); + try { + s.onError(new IOException()); + fail("Should have thrown"); + } catch (TestException ex) { + // expected + } + + assertTrue(d.isDisposed()); + } + }) + .subscribe(new SingleObserver<Object>() { + @Override + public void onSubscribe(Disposable d) { + + } + + @Override + public void onSuccess(Object value) { + } + + @Override + public void onError(Throwable e) { + throw new TestException(); + } + }); + } + + @Test + public void tryOnError() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + final Boolean[] response = { null }; + Single.create(new SingleOnSubscribe<Object>() { + @Override + public void subscribe(SingleEmitter<Object> e) throws Exception { + e.onSuccess(1); + response[0] = e.tryOnError(new TestException()); + } + }) + .test() + .assertResult(1); + + assertFalse(response[0]); + + assertTrue(errors.toString(), errors.isEmpty()); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void emitterHasToString() { + Single.create(new SingleOnSubscribe<Object>() { + @Override + public void subscribe(SingleEmitter<Object> emitter) throws Exception { + assertTrue(emitter.toString().contains(SingleCreate.Emitter.class.getSimpleName())); + } + }).test().assertEmpty(); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleDeferTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleDeferTest.java new file mode 100644 index 0000000000..7fed907ab4 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleDeferTest.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.single; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.functions.Supplier; + +public class SingleDeferTest extends RxJavaTest { + + @Test + public void normal() { + + Single<Integer> s = Single.defer(new Supplier<Single<Integer>>() { + int counter; + @Override + public Single<Integer> get() throws Exception { + return Single.just(++counter); + } + }); + + for (int i = 1; i < 33; i++) { + s.test().assertResult(i); + } + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleDelayTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleDelayTest.java new file mode 100644 index 0000000000..bc137d1612 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleDelayTest.java @@ -0,0 +1,285 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.single; + +import static org.junit.Assert.assertNotEquals; + +import java.util.List; +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicReference; + +import org.junit.Test; +import org.reactivestreams.Subscriber; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.subscriptions.BooleanSubscription; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.schedulers.*; +import io.reactivex.rxjava3.subjects.*; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class SingleDelayTest extends RxJavaTest { + @Test + public void delayOnSuccess() { + final TestScheduler scheduler = new TestScheduler(); + final TestObserver<Integer> observer = Single.just(1) + .delay(5, TimeUnit.SECONDS, scheduler) + .test(); + + scheduler.advanceTimeTo(2, TimeUnit.SECONDS); + observer.assertNoValues(); + + scheduler.advanceTimeTo(5, TimeUnit.SECONDS); + observer.assertValue(1); + } + + @Test + public void delayOnError() { + final TestScheduler scheduler = new TestScheduler(); + final TestObserver<?> observer = Single.error(new TestException()) + .delay(5, TimeUnit.SECONDS, scheduler) + .test(); + + scheduler.triggerActions(); + observer.assertError(TestException.class); + } + + @Test + public void delayedErrorOnSuccess() { + final TestScheduler scheduler = new TestScheduler(); + final TestObserver<Integer> observer = Single.just(1) + .delay(5, TimeUnit.SECONDS, scheduler, true) + .test(); + + scheduler.advanceTimeTo(2, TimeUnit.SECONDS); + observer.assertNoValues(); + + scheduler.advanceTimeTo(5, TimeUnit.SECONDS); + observer.assertValue(1); + } + + @Test + public void delayedErrorOnError() { + final TestScheduler scheduler = new TestScheduler(); + final TestObserver<?> observer = Single.error(new TestException()) + .delay(5, TimeUnit.SECONDS, scheduler, true) + .test(); + + scheduler.advanceTimeTo(2, TimeUnit.SECONDS); + observer.assertNoErrors(); + + scheduler.advanceTimeTo(5, TimeUnit.SECONDS); + observer.assertError(TestException.class); + } + + @Test + public void delaySubscriptionCompletable() throws Exception { + Single.just(1).delaySubscription(Completable.complete().delay(100, TimeUnit.MILLISECONDS)) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(1); + } + + @Test + public void delaySubscriptionObservable() throws Exception { + Single.just(1).delaySubscription(Observable.timer(100, TimeUnit.MILLISECONDS)) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(1); + } + + @Test + public void delaySubscriptionFlowable() throws Exception { + Single.just(1).delaySubscription(Flowable.timer(100, TimeUnit.MILLISECONDS)) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(1); + } + + @Test + public void delaySubscriptionSingle() throws Exception { + Single.just(1).delaySubscription(Single.timer(100, TimeUnit.MILLISECONDS)) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(1); + } + + @Test + public void delaySubscriptionTime() throws Exception { + Single.just(1).delaySubscription(100, TimeUnit.MILLISECONDS) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(1); + } + + @Test + public void delaySubscriptionTimeCustomScheduler() throws Exception { + Single.just(1).delaySubscription(100, TimeUnit.MILLISECONDS, Schedulers.io()) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(1); + } + + @Test + public void onErrorCalledOnScheduler() throws Exception { + final CountDownLatch latch = new CountDownLatch(1); + final AtomicReference<Thread> thread = new AtomicReference<>(); + + Single.<String>error(new Exception()) + .delay(0, TimeUnit.MILLISECONDS, Schedulers.newThread()) + .doOnError(new Consumer<Throwable>() { + @Override + public void accept(Throwable throwable) throws Exception { + thread.set(Thread.currentThread()); + latch.countDown(); + } + }) + .onErrorResumeWith(Single.just("")) + .subscribe(); + + latch.await(); + + assertNotEquals(Thread.currentThread(), thread.get()); + } + + @Test + public void withPublisherDispose() { + TestHelper.checkDisposed(PublishSubject.create().singleOrError().delaySubscription(Flowable.just(1))); + } + + @Test + public void withPublisherError() { + Single.just(1) + .delaySubscription(Flowable.error(new TestException())) + .test() + .assertFailure(TestException.class); + } + + @Test + public void withPublisherError2() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + + try { + Single.just(1) + .delaySubscription(new Flowable<Integer>() { + @Override + protected void subscribeActual(Subscriber<? super Integer> s) { + s.onSubscribe(new BooleanSubscription()); + s.onNext(1); + s.onError(new TestException()); + } + }) + .test() + .assertResult(1); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void withObservableDispose() { + TestHelper.checkDisposed(PublishSubject.create().singleOrError().delaySubscription(Observable.just(1))); + } + + @Test + public void withObservableError() { + Single.just(1) + .delaySubscription(Observable.error(new TestException())) + .test() + .assertFailure(TestException.class); + } + + @Test + public void withObservableError2() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + + try { + Single.just(1) + .delaySubscription(new Observable<Integer>() { + @Override + protected void subscribeActual(Observer<? super Integer> observer) { + observer.onSubscribe(Disposable.empty()); + observer.onNext(1); + observer.onError(new TestException()); + } + }) + .test() + .assertResult(1); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void withSingleErrors() { + Single.just(1) + .delaySubscription(Single.error(new TestException())) + .test() + .assertFailure(TestException.class); + } + + @Test + public void withSingleDispose() { + TestHelper.checkDisposed(Single.just(1).delaySubscription(Single.just(2))); + } + + @Test + public void withCompletableDispose() { + TestHelper.checkDisposed(Completable.complete().andThen(Single.just(1))); + } + + @Test + public void withCompletableDoubleOnSubscribe() { + + TestHelper.checkDoubleOnSubscribeCompletableToSingle(new Function<Completable, Single<Object>>() { + @Override + public Single<Object> apply(Completable c) throws Exception { + return c.andThen(Single.just((Object)1)); + } + }); + + } + + @Test + public void withSingleDoubleOnSubscribe() { + + TestHelper.checkDoubleOnSubscribeSingle(new Function<Single<Object>, Single<Object>>() { + @Override + public Single<Object> apply(Single<Object> s) throws Exception { + return Single.just((Object)1).delaySubscription(s); + } + }); + + } + + @Test + public void withPublisherDoubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeFlowableToSingle( + f -> SingleSubject.create().delaySubscription(f)); + } + + @Test + public void withObservableDoubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeObservableToSingle( + o -> SingleSubject.create().delaySubscription(o)); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleDematerializeTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleDematerializeTest.java new file mode 100644 index 0000000000..6cd05b7128 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleDematerializeTest.java @@ -0,0 +1,108 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.single; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.subjects.SingleSubject; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class SingleDematerializeTest extends RxJavaTest { + + @Test + public void success() { + Single.just(Notification.createOnNext(1)) + .dematerialize(Functions.<Notification<Integer>>identity()) + .test() + .assertResult(1); + } + + @Test + public void empty() { + Single.just(Notification.<Integer>createOnComplete()) + .dematerialize(Functions.<Notification<Integer>>identity()) + .test() + .assertResult(); + } + + @Test + public void error() { + Single.<Notification<Integer>>error(new TestException()) + .dematerialize(Functions.<Notification<Integer>>identity()) + .test() + .assertFailure(TestException.class); + } + + @Test + public void errorNotification() { + Single.just(Notification.<Integer>createOnError(new TestException())) + .dematerialize(Functions.<Notification<Integer>>identity()) + .test() + .assertFailure(TestException.class); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeSingleToMaybe(new Function<Single<Object>, MaybeSource<Object>>() { + @SuppressWarnings({ "unchecked", "rawtypes" }) + @Override + public MaybeSource<Object> apply(Single<Object> v) throws Exception { + return v.dematerialize((Function)Functions.identity()); + } + }); + } + + @Test + public void dispose() { + TestHelper.checkDisposed(SingleSubject.<Notification<Integer>>create().dematerialize(Functions.<Notification<Integer>>identity())); + } + + @Test + public void selectorCrash() { + Single.just(Notification.createOnNext(1)) + .dematerialize(new Function<Notification<Integer>, Notification<Integer>>() { + @Override + public Notification<Integer> apply(Notification<Integer> v) throws Exception { + throw new TestException(); + } + }) + .test() + .assertFailure(TestException.class); + } + + @Test + public void selectorNull() { + Single.just(Notification.createOnNext(1)) + .dematerialize(Functions.justFunction((Notification<Integer>)null)) + .test() + .assertFailure(NullPointerException.class); + } + + @Test + public void selectorDifferentType() { + Single.just(Notification.createOnNext(1)) + .dematerialize(new Function<Notification<Integer>, Notification<String>>() { + @Override + public Notification<String> apply(Notification<Integer> v) throws Exception { + return Notification.createOnNext("Value-" + 1); + } + }) + .test() + .assertResult("Value-1"); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleDetachTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleDetachTest.java new file mode 100644 index 0000000000..17812c8957 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleDetachTest.java @@ -0,0 +1,142 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.single; + +import static org.junit.Assert.assertNull; + +import java.io.IOException; +import java.lang.ref.WeakReference; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.processors.PublishProcessor; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class SingleDetachTest extends RxJavaTest { + + @Test + public void doubleSubscribe() { + + TestHelper.checkDoubleOnSubscribeSingle(new Function<Single<Object>, SingleSource<Object>>() { + @Override + public SingleSource<Object> apply(Single<Object> m) throws Exception { + return m.onTerminateDetach(); + } + }); + } + + @Test + public void dispose() { + TestHelper.checkDisposed(PublishProcessor.create().singleOrError().onTerminateDetach()); + } + + @Test + public void onError() { + Single.error(new TestException()) + .onTerminateDetach() + .test() + .assertFailure(TestException.class); + } + + @Test + public void onSuccess() { + Single.just(1) + .onTerminateDetach() + .test() + .assertResult(1); + } + + @Test + public void cancelDetaches() throws Exception { + Disposable d = Disposable.empty(); + final WeakReference<Disposable> wr = new WeakReference<>(d); + + TestObserver<Object> to = new Single<Object>() { + @Override + protected void subscribeActual(SingleObserver<? super Object> observer) { + observer.onSubscribe(wr.get()); + }; + } + .onTerminateDetach() + .test(); + + d = null; + + to.dispose(); + + System.gc(); + Thread.sleep(200); + + to.assertEmpty(); + + assertNull(wr.get()); + } + + @Test + public void errorDetaches() throws Exception { + Disposable d = Disposable.empty(); + final WeakReference<Disposable> wr = new WeakReference<>(d); + + TestObserver<Integer> to = new Single<Integer>() { + @Override + protected void subscribeActual(SingleObserver<? super Integer> observer) { + observer.onSubscribe(wr.get()); + observer.onError(new TestException()); + observer.onError(new IOException()); + }; + } + .onTerminateDetach() + .test(); + + d = null; + + System.gc(); + Thread.sleep(200); + + to.assertFailure(TestException.class); + + assertNull(wr.get()); + } + + @Test + public void successDetaches() throws Exception { + Disposable d = Disposable.empty(); + final WeakReference<Disposable> wr = new WeakReference<>(d); + + TestObserver<Integer> to = new Single<Integer>() { + @Override + protected void subscribeActual(SingleObserver<? super Integer> observer) { + observer.onSubscribe(wr.get()); + observer.onSuccess(1); + observer.onSuccess(2); + }; + } + .onTerminateDetach() + .test(); + + d = null; + + System.gc(); + Thread.sleep(200); + + to.assertResult(1); + + assertNull(wr.get()); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleDoAfterSuccessTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleDoAfterSuccessTest.java new file mode 100644 index 0000000000..de83cecb1a --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleDoAfterSuccessTest.java @@ -0,0 +1,126 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.single; + +import static org.junit.Assert.*; + +import java.util.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.subjects.PublishSubject; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class SingleDoAfterSuccessTest extends RxJavaTest { + + final List<Integer> values = new ArrayList<>(); + + final Consumer<Integer> afterSuccess = new Consumer<Integer>() { + @Override + public void accept(Integer e) throws Exception { + values.add(-e); + } + }; + + final TestObserver<Integer> to = new TestObserver<Integer>() { + @Override + public void onNext(Integer t) { + super.onNext(t); + SingleDoAfterSuccessTest.this.values.add(t); + } + }; + + @Test + public void just() { + Single.just(1) + .doAfterSuccess(afterSuccess) + .subscribeWith(to) + .assertResult(1); + + assertEquals(Arrays.asList(1, -1), values); + } + + @Test + public void error() { + Single.<Integer>error(new TestException()) + .doAfterSuccess(afterSuccess) + .subscribeWith(to) + .assertFailure(TestException.class); + + assertTrue(values.isEmpty()); + } + + @Test + public void justConditional() { + Single.just(1) + .doAfterSuccess(afterSuccess) + .filter(Functions.alwaysTrue()) + .subscribeWith(to) + .assertResult(1); + + assertEquals(Arrays.asList(1, -1), values); + } + + @Test + public void errorConditional() { + Single.<Integer>error(new TestException()) + .doAfterSuccess(afterSuccess) + .filter(Functions.alwaysTrue()) + .subscribeWith(to) + .assertFailure(TestException.class); + + assertTrue(values.isEmpty()); + } + + @Test + public void consumerThrows() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + Single.just(1) + .doAfterSuccess(new Consumer<Integer>() { + @Override + public void accept(Integer e) throws Exception { + throw new TestException(); + } + }) + .test() + .assertResult(1); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void dispose() { + TestHelper.checkDisposed(PublishSubject.<Integer>create().singleOrError().doAfterSuccess(afterSuccess)); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeSingle(new Function<Single<Integer>, SingleSource<Integer>>() { + @Override + public SingleSource<Integer> apply(Single<Integer> m) throws Exception { + return m.doAfterSuccess(afterSuccess); + } + }); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleDoAfterTerminateTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleDoAfterTerminateTest.java new file mode 100644 index 0000000000..6b98da8c66 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleDoAfterTerminateTest.java @@ -0,0 +1,124 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.single; + +import static org.junit.Assert.assertEquals; + +import java.util.List; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.subjects.PublishSubject; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class SingleDoAfterTerminateTest extends RxJavaTest { + + private final int[] call = { 0 }; + + private final Action afterTerminate = new Action() { + @Override + public void run() throws Exception { + call[0]++; + } + }; + + private final TestObserver<Integer> to = new TestObserver<>(); + + @Test + public void just() { + Single.just(1) + .doAfterTerminate(afterTerminate) + .subscribeWith(to) + .assertResult(1); + + assertAfterTerminateCalledOnce(); + } + + @Test + public void error() { + Single.<Integer>error(new TestException()) + .doAfterTerminate(afterTerminate) + .subscribeWith(to) + .assertFailure(TestException.class); + + assertAfterTerminateCalledOnce(); + } + + @Test + public void justConditional() { + Single.just(1) + .doAfterTerminate(afterTerminate) + .filter(Functions.alwaysTrue()) + .subscribeWith(to) + .assertResult(1); + + assertAfterTerminateCalledOnce(); + } + + @Test + public void errorConditional() { + Single.<Integer>error(new TestException()) + .doAfterTerminate(afterTerminate) + .filter(Functions.alwaysTrue()) + .subscribeWith(to) + .assertFailure(TestException.class); + + assertAfterTerminateCalledOnce(); + } + + @Test + public void actionThrows() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + Single.just(1) + .doAfterTerminate(new Action() { + @Override + public void run() throws Exception { + throw new TestException(); + } + }) + .test() + .assertResult(1); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void dispose() { + TestHelper.checkDisposed(PublishSubject.<Integer>create().singleOrError().doAfterTerminate(afterTerminate)); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeSingle(new Function<Single<Integer>, SingleSource<Integer>>() { + @Override + public SingleSource<Integer> apply(Single<Integer> m) throws Exception { + return m.doAfterTerminate(afterTerminate); + } + }); + } + + private void assertAfterTerminateCalledOnce() { + assertEquals(1, call[0]); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleDoFinallyTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleDoFinallyTest.java new file mode 100644 index 0000000000..64afc620ac --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleDoFinallyTest.java @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.single; + +import static org.junit.Assert.assertEquals; + +import java.util.List; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.subjects.PublishSubject; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class SingleDoFinallyTest extends RxJavaTest implements Action { + + int calls; + + @Override + public void run() throws Exception { + calls++; + } + + @Test + public void normalJust() { + Single.just(1) + .doFinally(this) + .test() + .assertResult(1); + + assertEquals(1, calls); + } + + @Test + public void normalError() { + Single.error(new TestException()) + .doFinally(this) + .test() + .assertFailure(TestException.class); + + assertEquals(1, calls); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeSingle(new Function<Single<Object>, Single<Object>>() { + @Override + public Single<Object> apply(Single<Object> f) throws Exception { + return f.doFinally(SingleDoFinallyTest.this); + } + }); + } + + @Test + public void actionThrows() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + Single.just(1) + .doFinally(new Action() { + @Override + public void run() throws Exception { + throw new TestException(); + } + }) + .test() + .assertResult(1) + .dispose(); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void disposed() { + TestHelper.checkDisposed(PublishSubject.create().singleOrError().doFinally(this)); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleDoOnLifecycleTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleDoOnLifecycleTest.java new file mode 100644 index 0000000000..f14d03cc8d --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleDoOnLifecycleTest.java @@ -0,0 +1,152 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.single; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.subjects.SingleSubject; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class SingleDoOnLifecycleTest extends RxJavaTest { + + @Test + public void success() throws Throwable { + @SuppressWarnings("unchecked") + Consumer<? super Disposable> onSubscribe = mock(Consumer.class); + Action onDispose = mock(Action.class); + + Single.just(1) + .doOnLifecycle(onSubscribe, onDispose) + .test() + .assertResult(1); + + verify(onSubscribe).accept(any()); + verify(onDispose, never()).run(); + } + + @Test + public void error() throws Throwable { + @SuppressWarnings("unchecked") + Consumer<? super Disposable> onSubscribe = mock(Consumer.class); + Action onDispose = mock(Action.class); + + Single.error(new TestException()) + .doOnLifecycle(onSubscribe, onDispose) + .test() + .assertFailure(TestException.class); + + verify(onSubscribe).accept(any()); + verify(onDispose, never()).run(); + } + + @Test + public void onSubscribeCrash() throws Throwable { + TestHelper.withErrorTracking(errors -> { + @SuppressWarnings("unchecked") + Consumer<? super Disposable> onSubscribe = mock(Consumer.class); + Action onDispose = mock(Action.class); + + doThrow(new TestException("First")).when(onSubscribe).accept(any()); + + Disposable bs = Disposable.empty(); + + new Single<Integer>() { + @Override + protected void subscribeActual(SingleObserver<? super Integer> observer) { + observer.onSubscribe(bs); + observer.onError(new TestException("Second")); + observer.onSuccess(1); + } + } + .doOnLifecycle(onSubscribe, onDispose) + .to(TestHelper.<Integer>testConsumer()) + .assertFailureAndMessage(TestException.class, "First"); + + assertTrue(bs.isDisposed()); + + TestHelper.assertUndeliverable(errors, 0, TestException.class, "Second"); + + verify(onSubscribe).accept(any()); + verify(onDispose, never()).run(); + }); + } + + @Test + public void onDisposeCrash() throws Throwable { + TestHelper.withErrorTracking(errors -> { + @SuppressWarnings("unchecked") + Consumer<? super Disposable> onSubscribe = mock(Consumer.class); + Action onDispose = mock(Action.class); + + doThrow(new TestException("First")).when(onDispose).run(); + + SingleSubject<Integer> ss = SingleSubject.create(); + + TestObserver<Integer> to = ss + .doOnLifecycle(onSubscribe, onDispose) + .test(); + + assertTrue(ss.hasObservers()); + + to.dispose(); + + assertFalse(ss.hasObservers()); + + TestHelper.assertUndeliverable(errors, 0, TestException.class, "First"); + + verify(onSubscribe).accept(any()); + verify(onDispose).run(); + }); + } + + @Test + public void dispose() throws Throwable { + @SuppressWarnings("unchecked") + Consumer<? super Disposable> onSubscribe = mock(Consumer.class); + Action onDispose = mock(Action.class); + + SingleSubject<Integer> ss = SingleSubject.create(); + + TestObserver<Integer> to = ss + .doOnLifecycle(onSubscribe, onDispose) + .test(); + + assertTrue(ss.hasObservers()); + + to.dispose(); + + assertFalse(ss.hasObservers()); + + verify(onSubscribe).accept(any()); + verify(onDispose).run(); + } + + @Test + public void isDisposed() { + TestHelper.checkDisposed(SingleSubject.create().doOnLifecycle(d -> { }, () -> { })); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeSingle(m -> m.doOnLifecycle(d -> { }, () -> { })); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleDoOnTerminateTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleDoOnTerminateTest.java new file mode 100644 index 0000000000..814a38bded --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleDoOnTerminateTest.java @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.single; + +import static org.junit.Assert.assertTrue; + +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.Action; +import io.reactivex.rxjava3.testsupport.*; + +public class SingleDoOnTerminateTest extends RxJavaTest { + + @Test + public void doOnTerminateSuccess() { + final AtomicBoolean atomicBoolean = new AtomicBoolean(); + + Single.just(1).doOnTerminate(new Action() { + @Override + public void run() throws Exception { + atomicBoolean.set(true); + } + }) + .test() + .assertResult(1); + + assertTrue(atomicBoolean.get()); + } + + @Test + public void doOnTerminateError() { + final AtomicBoolean atomicBoolean = new AtomicBoolean(); + Single.error(new TestException()).doOnTerminate(new Action() { + @Override + public void run() { + atomicBoolean.set(true); + } + }) + .test() + .assertFailure(TestException.class); + + assertTrue(atomicBoolean.get()); + } + + @Test + public void doOnTerminateSuccessCrash() { + Single.just(1).doOnTerminate(new Action() { + @Override + public void run() throws Exception { + throw new TestException(); + } + }) + .test() + .assertFailure(TestException.class); + } + + @Test + public void doOnTerminateErrorCrash() { + TestObserverEx<Object> to = Single.error(new TestException("Outer")).doOnTerminate(new Action() { + @Override + public void run() { + throw new TestException("Inner"); + } + }) + .to(TestHelper.testConsumer()) + .assertFailure(CompositeException.class); + + List<Throwable> errors = TestHelper.compositeList(to.errors().get(0)); + + TestHelper.assertError(errors, 0, TestException.class, "Outer"); + TestHelper.assertError(errors, 1, TestException.class, "Inner"); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleDoOnTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleDoOnTest.java new file mode 100644 index 0000000000..92ce91b65a --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleDoOnTest.java @@ -0,0 +1,361 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.single; + +import static org.junit.Assert.*; + +import java.util.List; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.*; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.subjects.PublishSubject; +import io.reactivex.rxjava3.testsupport.*; + +public class SingleDoOnTest extends RxJavaTest { + + @Test + public void doOnDispose() { + final int[] count = { 0 }; + + Single.never().doOnDispose(new Action() { + @Override + public void run() throws Exception { + count[0]++; + } + }).test(true); + + assertEquals(1, count[0]); + } + + @Test + public void doOnError() { + final Object[] event = { null }; + + Single.error(new TestException()).doOnError(new Consumer<Throwable>() { + @Override + public void accept(Throwable e) throws Exception { + event[0] = e; + } + }) + .test(); + + assertTrue(event[0].toString(), event[0] instanceof TestException); + } + + @Test + public void doOnSubscribe() { + final int[] count = { 0 }; + + Single.never().doOnSubscribe(new Consumer<Disposable>() { + @Override + public void accept(Disposable d) throws Exception { + count[0]++; + } + }).test(); + + assertEquals(1, count[0]); + } + + @Test + public void doOnSuccess() { + final Object[] event = { null }; + + Single.just(1).doOnSuccess(new Consumer<Integer>() { + @Override + public void accept(Integer e) throws Exception { + event[0] = e; + } + }) + .test(); + + assertEquals(1, event[0]); + } + + @Test + public void doOnSubscribeNormal() { + final int[] count = { 0 }; + + Single.just(1).doOnSubscribe(new Consumer<Disposable>() { + @Override + public void accept(Disposable d) throws Exception { + count[0]++; + } + }) + .test() + .assertResult(1); + + assertEquals(1, count[0]); + } + + @Test + public void doOnSubscribeError() { + final int[] count = { 0 }; + + Single.error(new TestException()).doOnSubscribe(new Consumer<Disposable>() { + @Override + public void accept(Disposable d) throws Exception { + count[0]++; + } + }) + .test() + .assertFailure(TestException.class); + + assertEquals(1, count[0]); + } + + @Test + public void doOnSubscribeJustCrash() { + + Single.just(1).doOnSubscribe(new Consumer<Disposable>() { + @Override + public void accept(Disposable d) throws Exception { + throw new TestException(); + } + }) + .test() + .assertFailure(TestException.class); + } + + @Test + public void doOnSubscribeErrorCrash() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + + try { + Single.error(new TestException("Outer")).doOnSubscribe(new Consumer<Disposable>() { + @Override + public void accept(Disposable d) throws Exception { + throw new TestException("Inner"); + } + }) + .to(TestHelper.testConsumer()) + .assertFailureAndMessage(TestException.class, "Inner"); + + TestHelper.assertUndeliverable(errors, 0, TestException.class, "Outer"); + } finally { + RxJavaPlugins.reset(); + } + + } + + @Test + public void onErrorSuccess() { + final int[] call = { 0 }; + + Single.just(1) + .doOnError(new Consumer<Throwable>() { + @Override + public void accept(Throwable v) throws Exception { + call[0]++; + } + }) + .test() + .assertResult(1); + + assertEquals(0, call[0]); + } + + @Test + public void onErrorCrashes() { + TestObserverEx<Object> to = Single.error(new TestException("Outer")) + .doOnError(new Consumer<Throwable>() { + @Override + public void accept(Throwable v) throws Exception { + throw new TestException("Inner"); + } + }) + .to(TestHelper.testConsumer()) + .assertFailure(CompositeException.class); + + List<Throwable> errors = TestHelper.compositeList(to.errors().get(0)); + + TestHelper.assertError(errors, 0, TestException.class, "Outer"); + TestHelper.assertError(errors, 1, TestException.class, "Inner"); + } + + @Test + public void doOnEventThrowsSuccess() { + Single.just(1) + .doOnEvent(new BiConsumer<Integer, Throwable>() { + @Override + public void accept(Integer v, Throwable e) throws Exception { + throw new TestException(); + } + }) + .test() + .assertFailure(TestException.class); + } + + @Test + public void doOnEventThrowsError() { + TestObserverEx<Integer> to = Single.<Integer>error(new TestException("Main")) + .doOnEvent(new BiConsumer<Integer, Throwable>() { + @Override + public void accept(Integer v, Throwable e) throws Exception { + throw new TestException("Inner"); + } + }) + .to(TestHelper.<Integer>testConsumer()) + .assertFailure(CompositeException.class); + + List<Throwable> errors = TestHelper.compositeList(to.errors().get(0)); + + TestHelper.assertError(errors, 0, TestException.class, "Main"); + TestHelper.assertError(errors, 1, TestException.class, "Inner"); + } + + @Test + public void doOnDisposeDispose() { + final int[] calls = { 0 }; + TestHelper.checkDisposed(PublishSubject.create().singleOrError().doOnDispose(new Action() { + @Override + public void run() throws Exception { + calls[0]++; + } + })); + + assertEquals(1, calls[0]); + } + + @Test + public void doOnDisposeSuccess() { + final int[] calls = { 0 }; + + Single.just(1) + .doOnDispose(new Action() { + @Override + public void run() throws Exception { + calls[0]++; + } + }) + .test() + .assertResult(1); + + assertEquals(0, calls[0]); + } + + @Test + public void doOnDisposeError() { + final int[] calls = { 0 }; + + Single.error(new TestException()) + .doOnDispose(new Action() { + @Override + public void run() throws Exception { + calls[0]++; + } + }) + .test() + .assertFailure(TestException.class); + + assertEquals(0, calls[0]); + } + + @Test + public void doOnDisposeDoubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeSingle(new Function<Single<Object>, SingleSource<Object>>() { + @Override + public SingleSource<Object> apply(Single<Object> s) throws Exception { + return s.doOnDispose(Functions.EMPTY_ACTION); + } + }); + } + + @Test + public void doOnDisposeCrash() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + PublishSubject<Integer> ps = PublishSubject.create(); + + ps.singleOrError().doOnDispose(new Action() { + @Override + public void run() throws Exception { + throw new TestException(); + } + }) + .test() + .dispose(); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void doOnSuccessErrors() { + final int[] call = { 0 }; + + Single.error(new TestException()) + .doOnSuccess(new Consumer<Object>() { + @Override + public void accept(Object v) throws Exception { + call[0]++; + } + }) + .test() + .assertFailure(TestException.class); + + assertEquals(0, call[0]); + } + + @Test + public void doOnSuccessCrash() { + Single.just(1) + .doOnSuccess(new Consumer<Integer>() { + @Override + public void accept(Integer v) throws Exception { + throw new TestException(); + } + }) + .test() + .assertFailure(TestException.class); + } + + @Test + public void onSubscribeCrash() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + final Disposable bs = Disposable.empty(); + + new Single<Integer>() { + @Override + protected void subscribeActual(SingleObserver<? super Integer> observer) { + observer.onSubscribe(bs); + observer.onError(new TestException("Second")); + observer.onSuccess(1); + } + } + .doOnSubscribe(new Consumer<Disposable>() { + @Override + public void accept(Disposable d) throws Exception { + throw new TestException("First"); + } + }) + .to(TestHelper.<Integer>testConsumer()) + .assertFailureAndMessage(TestException.class, "First"); + + assertTrue(bs.isDisposed()); + + TestHelper.assertUndeliverable(errors, 0, TestException.class, "Second"); + } finally { + RxJavaPlugins.reset(); + } + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleEqualsTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleEqualsTest.java new file mode 100644 index 0000000000..9526299231 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleEqualsTest.java @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.single; + +import java.util.List; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class SingleEqualsTest extends RxJavaTest { + + @Test + public void bothSucceedEqual() { + Single.sequenceEqual(Single.just(1), Single.just(1)) + .test() + .assertResult(true); + } + + @Test + public void bothSucceedNotEqual() { + Single.sequenceEqual(Single.just(1), Single.just(2)) + .test() + .assertResult(false); + } + + @Test + public void firstSucceedOtherError() { + Single.sequenceEqual(Single.just(1), Single.error(new TestException())) + .test() + .assertFailure(TestException.class); + } + + @Test + public void firstErrorOtherSucceed() { + Single.sequenceEqual(Single.error(new TestException()), Single.just(1)) + .test() + .assertFailure(TestException.class); + } + + @Test + public void bothError() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + Single.sequenceEqual(Single.error(new TestException("One")), Single.error(new TestException("Two"))) + .to(TestHelper.<Boolean>testConsumer()) + .assertFailureAndMessage(TestException.class, "One"); + + TestHelper.assertUndeliverable(errors, 0, TestException.class, "Two"); + } finally { + RxJavaPlugins.reset(); + } + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleErrorTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleErrorTest.java new file mode 100644 index 0000000000..835ad2ad09 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleErrorTest.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.single; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.Supplier; + +public class SingleErrorTest extends RxJavaTest { + + @Test + public void errorSupplierThrows() { + Single.error(new Supplier<Throwable>() { + @Override + public Throwable get() throws Exception { + throw new TestException(); + } + }) + .test() + .assertFailure(TestException.class); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleFlatMapBiSelectorTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleFlatMapBiSelectorTest.java new file mode 100644 index 0000000000..78705fe166 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleFlatMapBiSelectorTest.java @@ -0,0 +1,205 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.single; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.subjects.SingleSubject; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class SingleFlatMapBiSelectorTest extends RxJavaTest { + + BiFunction<Integer, Integer, String> stringCombine() { + return new BiFunction<Integer, Integer, String>() { + @Override + public String apply(Integer a, Integer b) throws Exception { + return a + ":" + b; + } + }; + } + + @Test + public void normal() { + Single.just(1) + .flatMap(new Function<Integer, SingleSource<Integer>>() { + @Override + public SingleSource<Integer> apply(Integer v) throws Exception { + return Single.just(2); + } + }, stringCombine()) + .test() + .assertResult("1:2"); + } + + @Test + public void errorWithJust() { + final int[] call = { 0 }; + + Single.<Integer>error(new TestException()) + .flatMap(new Function<Integer, SingleSource<Integer>>() { + @Override + public SingleSource<Integer> apply(Integer v) throws Exception { + call[0]++; + return Single.just(1); + } + }, stringCombine()) + .test() + .assertFailure(TestException.class); + + assertEquals(0, call[0]); + } + + @Test + public void justWithError() { + final int[] call = { 0 }; + + Single.just(1) + .flatMap(new Function<Integer, SingleSource<Integer>>() { + @Override + public SingleSource<Integer> apply(Integer v) throws Exception { + call[0]++; + return Single.<Integer>error(new TestException()); + } + }, stringCombine()) + .test() + .assertFailure(TestException.class); + + assertEquals(1, call[0]); + } + + @Test + public void dispose() { + TestHelper.checkDisposed(SingleSubject.create() + .flatMap(new Function<Object, SingleSource<Integer>>() { + @Override + public SingleSource<Integer> apply(Object v) throws Exception { + return Single.just(1); + } + }, new BiFunction<Object, Integer, Object>() { + @Override + public Object apply(Object a, Integer b) throws Exception { + return b; + } + })); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeSingle(new Function<Single<Object>, SingleSource<Object>>() { + @Override + public SingleSource<Object> apply(Single<Object> v) throws Exception { + return v.flatMap(new Function<Object, SingleSource<Integer>>() { + @Override + public SingleSource<Integer> apply(Object v) throws Exception { + return Single.just(1); + } + }, new BiFunction<Object, Integer, Object>() { + @Override + public Object apply(Object a, Integer b) throws Exception { + return b; + } + }); + } + }); + } + + @Test + public void mapperThrows() { + Single.just(1) + .flatMap(new Function<Integer, SingleSource<Integer>>() { + @Override + public SingleSource<Integer> apply(Integer v) throws Exception { + throw new TestException(); + } + }, stringCombine()) + .test() + .assertFailure(TestException.class); + } + + @Test + public void mapperReturnsNull() { + Single.just(1) + .flatMap(new Function<Integer, SingleSource<Integer>>() { + @Override + public SingleSource<Integer> apply(Integer v) throws Exception { + return null; + } + }, stringCombine()) + .test() + .assertFailure(NullPointerException.class); + } + + @Test + public void resultSelectorThrows() { + Single.just(1) + .flatMap(new Function<Integer, SingleSource<Integer>>() { + @Override + public SingleSource<Integer> apply(Integer v) throws Exception { + return Single.just(2); + } + }, new BiFunction<Integer, Integer, Object>() { + @Override + public Object apply(Integer a, Integer b) throws Exception { + throw new TestException(); + } + }) + .test() + .assertFailure(TestException.class); + } + + @Test + public void resultSelectorReturnsNull() { + Single.just(1) + .flatMap(new Function<Integer, SingleSource<Integer>>() { + @Override + public SingleSource<Integer> apply(Integer v) throws Exception { + return Single.just(2); + } + }, new BiFunction<Integer, Integer, Object>() { + @Override + public Object apply(Integer a, Integer b) throws Exception { + return null; + } + }) + .test() + .assertFailure(NullPointerException.class); + } + + @Test + public void mapperCancels() { + final TestObserver<Integer> to = new TestObserver<>(); + + Single.just(1) + .flatMap(new Function<Integer, SingleSource<Integer>>() { + @Override + public SingleSource<Integer> apply(Integer v) throws Exception { + to.dispose(); + return Single.just(2); + } + }, new BiFunction<Integer, Integer, Integer>() { + @Override + public Integer apply(Integer a, Integer b) throws Exception { + throw new IllegalStateException(); + } + }) + .subscribeWith(to) + .assertEmpty(); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleFlatMapCompletableTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleFlatMapCompletableTest.java new file mode 100644 index 0000000000..c4af049aec --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleFlatMapCompletableTest.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.single; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class SingleFlatMapCompletableTest extends RxJavaTest { + + @Test + public void dispose() { + TestHelper.checkDisposed(Single.just(1).flatMapCompletable(new Function<Integer, Completable>() { + @Override + public Completable apply(Integer v) throws Exception { + return Completable.complete(); + } + })); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleFlatMapIterableFlowableTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleFlatMapIterableFlowableTest.java new file mode 100644 index 0000000000..277b9aa13b --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleFlatMapIterableFlowableTest.java @@ -0,0 +1,637 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.single; + +import static org.junit.Assert.*; + +import java.util.*; +import java.util.concurrent.TimeUnit; + +import org.junit.Test; +import org.reactivestreams.Subscription; + +import io.reactivex.rxjava3.annotations.NonNull; +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.util.CrashingIterable; +import io.reactivex.rxjava3.operators.QueueFuseable; +import io.reactivex.rxjava3.operators.QueueSubscription; +import io.reactivex.rxjava3.schedulers.Schedulers; +import io.reactivex.rxjava3.subjects.*; +import io.reactivex.rxjava3.subscribers.TestSubscriber; +import io.reactivex.rxjava3.testsupport.*; + +public class SingleFlatMapIterableFlowableTest extends RxJavaTest { + + @Test + public void normal() { + + Single.just(1).flattenAsFlowable(new Function<Integer, Iterable<Integer>>() { + @Override + public Iterable<Integer> apply(Integer v) throws Exception { + return Arrays.asList(v, v + 1); + } + }) + .test() + .assertResult(1, 2); + } + + @Test + public void emptyIterable() { + + Single.just(1).flattenAsFlowable(new Function<Integer, Iterable<Integer>>() { + @Override + public Iterable<Integer> apply(Integer v) throws Exception { + return Collections.<Integer>emptyList(); + } + }) + .test() + .assertResult(); + } + + @Test + public void error() { + + Single.<Integer>error(new TestException()).flattenAsFlowable(new Function<Integer, Iterable<Integer>>() { + @Override + public Iterable<Integer> apply(Integer v) throws Exception { + return Arrays.asList(v, v + 1); + } + }) + .test() + .assertFailure(TestException.class); + } + + @Test + public void backpressure() { + + TestSubscriber<Integer> ts = Single.just(1).flattenAsFlowable(new Function<Integer, Iterable<Integer>>() { + @Override + public Iterable<Integer> apply(Integer v) throws Exception { + return Arrays.asList(v, v + 1); + } + }) + .test(0); + + ts.assertEmpty(); + + ts.request(1); + + ts.assertValue(1); + + ts.request(1); + + ts.assertResult(1, 2); + } + + @Test + public void take() { + Single.just(1).flattenAsFlowable(new Function<Integer, Iterable<Integer>>() { + @Override + public Iterable<Integer> apply(Integer v) throws Exception { + return Arrays.asList(v, v + 1); + } + }) + .take(1) + .test() + .assertResult(1); + } + + @Test + public void fused() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<Integer>().setInitialFusionMode(QueueFuseable.ANY); + + Single.just(1).flattenAsFlowable(new Function<Integer, Iterable<Integer>>() { + @Override + public Iterable<Integer> apply(Integer v) throws Exception { + return Arrays.asList(v, v + 1); + } + }) + .subscribe(ts); + + ts.assertFuseable() + .assertFusionMode(QueueFuseable.ASYNC) + .assertResult(1, 2); + ; + } + + @Test + public void fusedNoSync() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<Integer>().setInitialFusionMode(QueueFuseable.SYNC); + + Single.just(1).flattenAsFlowable(new Function<Integer, Iterable<Integer>>() { + @Override + public Iterable<Integer> apply(Integer v) throws Exception { + return Arrays.asList(v, v + 1); + } + }) + .subscribe(ts); + + ts.assertFuseable() + .assertFusionMode(QueueFuseable.NONE) + .assertResult(1, 2); + ; + } + + @Test + public void iteratorCrash() { + + Single.just(1).flattenAsFlowable(new Function<Integer, Iterable<Integer>>() { + @Override + public Iterable<Integer> apply(Integer v) throws Exception { + return new CrashingIterable(1, 100, 100); + } + }) + .to(TestHelper.<Integer>testConsumer()) + .assertFailureAndMessage(TestException.class, "iterator()"); + } + + @Test + public void hasNextCrash() { + + Single.just(1).flattenAsFlowable(new Function<Integer, Iterable<Integer>>() { + @Override + public Iterable<Integer> apply(Integer v) throws Exception { + return new CrashingIterable(100, 1, 100); + } + }) + .to(TestHelper.<Integer>testConsumer()) + .assertFailureAndMessage(TestException.class, "hasNext()"); + } + + @Test + public void nextCrash() { + + Single.just(1).flattenAsFlowable(new Function<Integer, Iterable<Integer>>() { + @Override + public Iterable<Integer> apply(Integer v) throws Exception { + return new CrashingIterable(100, 100, 1); + } + }) + .to(TestHelper.<Integer>testConsumer()) + .assertFailureAndMessage(TestException.class, "next()"); + } + + @Test + public void hasNextCrash2() { + + Single.just(1).flattenAsFlowable(new Function<Integer, Iterable<Integer>>() { + @Override + public Iterable<Integer> apply(Integer v) throws Exception { + return new CrashingIterable(100, 2, 100); + } + }) + .to(TestHelper.<Integer>testConsumer()) + .assertFailureAndMessage(TestException.class, "hasNext()", 0); + } + + @Test + public void async1() { + Single.just(1) + .flattenAsFlowable(new Function<Object, Iterable<Integer>>() { + @Override + public Iterable<Integer> apply(Object v) throws Exception { + Integer[] array = new Integer[1000 * 1000]; + Arrays.fill(array, 1); + return Arrays.asList(array); + } + }) + .hide() + .observeOn(Schedulers.single()) + .to(TestHelper.<Integer>testConsumer()) + .awaitDone(5, TimeUnit.SECONDS) + .assertSubscribed() + .assertValueCount(1000 * 1000) + .assertNoErrors() + .assertComplete(); + } + + @Test + public void async2() { + Single.just(1) + .flattenAsFlowable(new Function<Object, Iterable<Integer>>() { + @Override + public Iterable<Integer> apply(Object v) throws Exception { + Integer[] array = new Integer[1000 * 1000]; + Arrays.fill(array, 1); + return Arrays.asList(array); + } + }) + .observeOn(Schedulers.single()) + .to(TestHelper.<Integer>testConsumer()) + .awaitDone(5, TimeUnit.SECONDS) + .assertSubscribed() + .assertValueCount(1000 * 1000) + .assertNoErrors() + .assertComplete(); + } + + @Test + public void async3() { + Single.just(1) + .flattenAsFlowable(new Function<Object, Iterable<Integer>>() { + @Override + public Iterable<Integer> apply(Object v) throws Exception { + Integer[] array = new Integer[1000 * 1000]; + Arrays.fill(array, 1); + return Arrays.asList(array); + } + }) + .take(500 * 1000) + .observeOn(Schedulers.single()) + .to(TestHelper.<Integer>testConsumer()) + .awaitDone(5, TimeUnit.SECONDS) + .assertSubscribed() + .assertValueCount(500 * 1000) + .assertNoErrors() + .assertComplete(); + } + + @Test + public void async4() { + Single.just(1) + .flattenAsFlowable(new Function<Object, Iterable<Integer>>() { + @Override + public Iterable<Integer> apply(Object v) throws Exception { + Integer[] array = new Integer[1000 * 1000]; + Arrays.fill(array, 1); + return Arrays.asList(array); + } + }) + .observeOn(Schedulers.single()) + .take(500 * 1000) + .to(TestHelper.<Integer>testConsumer()) + .awaitDone(5, TimeUnit.SECONDS) + .assertSubscribed() + .assertValueCount(500 * 1000) + .assertNoErrors() + .assertComplete(); + } + + @Test + public void fusedEmptyCheck() { + Single.just(1) + .flattenAsFlowable(new Function<Object, Iterable<Integer>>() { + @Override + public Iterable<Integer> apply(Object v) throws Exception { + return Arrays.asList(1, 2, 3); + } + }).subscribe(new FlowableSubscriber<Integer>() { + QueueSubscription<Integer> qs; + @SuppressWarnings("unchecked") + @Override + public void onSubscribe(Subscription s) { + qs = (QueueSubscription<Integer>)s; + + assertEquals(QueueFuseable.ASYNC, qs.requestFusion(QueueFuseable.ANY)); + } + + @Override + public void onNext(Integer value) { + assertFalse(qs.isEmpty()); + + qs.clear(); + + assertTrue(qs.isEmpty()); + + qs.cancel(); + } + + @Override + public void onError(Throwable e) { + } + + @Override + public void onComplete() { + } + }); + } + + @Test + public void hasNextThrowsUnbounded() { + Single.just(1) + .flattenAsFlowable(new Function<Object, Iterable<Integer>>() { + @Override + public Iterable<Integer> apply(Object v) throws Exception { + return new CrashingIterable(100, 2, 100); + } + }) + .to(TestHelper.<Integer>testConsumer()) + .assertFailureAndMessage(TestException.class, "hasNext()", 0); + } + + @Test + public void nextThrowsUnbounded() { + Single.just(1) + .flattenAsFlowable(new Function<Object, Iterable<Integer>>() { + @Override + public Iterable<Integer> apply(Object v) throws Exception { + return new CrashingIterable(100, 100, 1); + } + }) + .to(TestHelper.<Integer>testConsumer()) + .assertFailureAndMessage(TestException.class, "next()"); + } + + @Test + public void hasNextThrows() { + Single.just(1) + .flattenAsFlowable(new Function<Object, Iterable<Integer>>() { + @Override + public Iterable<Integer> apply(Object v) throws Exception { + return new CrashingIterable(100, 2, 100); + } + }) + .to(TestHelper.<Integer>testSubscriber(2L)) + .assertFailureAndMessage(TestException.class, "hasNext()", 0); + } + + @Test + public void nextThrows() { + Single.just(1) + .flattenAsFlowable(new Function<Object, Iterable<Integer>>() { + @Override + public Iterable<Integer> apply(Object v) throws Exception { + return new CrashingIterable(100, 100, 1); + } + }) + .to(TestHelper.<Integer>testSubscriber(2L)) + .assertFailureAndMessage(TestException.class, "next()"); + } + + @Test + public void requestBefore() { + for (int i = 0; i < 500; i++) { + final PublishSubject<Integer> ps = PublishSubject.create(); + + ps.singleElement().flattenAsFlowable( + new Function<Integer, Iterable<Integer>>() { + @Override + public Iterable<Integer> apply(Integer v) throws Exception { + return Arrays.asList(1, 2, 3); + } + }) + .test(5L) + .assertEmpty(); + } + } + + @Test + public void requestCreateInnerRace() { + final Integer[] a = new Integer[1000]; + Arrays.fill(a, 1); + + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final PublishSubject<Integer> ps = PublishSubject.create(); + + ps.onNext(1); + + final TestSubscriber<Integer> ts = ps.singleElement().flattenAsFlowable( + new Function<Integer, Iterable<Integer>>() { + @Override + public Iterable<Integer> apply(Integer v) throws Exception { + return Arrays.asList(a); + } + }) + .test(0L); + + Runnable r1 = new Runnable() { + @Override + public void run() { + ps.onComplete(); + for (int i = 0; i < 500; i++) { + ts.request(1); + } + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + for (int i = 0; i < 500; i++) { + ts.request(1); + } + } + }; + + TestHelper.race(r1, r2); + } + } + + @Test + public void cancelCreateInnerRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final PublishSubject<Integer> ps = PublishSubject.create(); + + ps.onNext(1); + + final TestSubscriber<Integer> ts = ps.singleElement().flattenAsFlowable( + new Function<Integer, Iterable<Integer>>() { + @Override + public Iterable<Integer> apply(Integer v) throws Exception { + return Arrays.asList(1, 2, 3); + } + }) + .test(0L); + + Runnable r1 = new Runnable() { + @Override + public void run() { + ps.onComplete(); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + ts.cancel(); + } + }; + + TestHelper.race(r1, r2); + } + } + + @Test + public void slowPathCancelAfterHasNext() { + final Integer[] a = new Integer[1000]; + Arrays.fill(a, 1); + + final TestSubscriber<Integer> ts = new TestSubscriber<>(0L); + + Single.just(1) + .flattenAsFlowable(new Function<Integer, Iterable<Integer>>() { + @Override + public Iterable<Integer> apply(Integer v) throws Exception { + return new Iterable<Integer>() { + @Override + public Iterator<Integer> iterator() { + return new Iterator<Integer>() { + int count; + @Override + public boolean hasNext() { + if (count++ == 2) { + ts.cancel(); + } + return true; + } + + @Override + public Integer next() { + return 1; + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + }; + } + }; + } + }) + .subscribe(ts); + + ts.request(3); + ts.assertValues(1, 1).assertNoErrors().assertNotComplete(); + } + + @Test + public void fastPathCancelAfterHasNext() { + final Integer[] a = new Integer[1000]; + Arrays.fill(a, 1); + + final TestSubscriber<Integer> ts = new TestSubscriber<>(0L); + + Single.just(1) + .flattenAsFlowable(new Function<Integer, Iterable<Integer>>() { + @Override + public Iterable<Integer> apply(Integer v) throws Exception { + return new Iterable<Integer>() { + @Override + public Iterator<Integer> iterator() { + return new Iterator<Integer>() { + int count; + @Override + public boolean hasNext() { + if (count++ == 2) { + ts.cancel(); + } + return true; + } + + @Override + public Integer next() { + return 1; + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + }; + } + }; + } + }) + .subscribe(ts); + + ts.request(Long.MAX_VALUE); + ts.assertValues(1, 1).assertNoErrors().assertNotComplete(); + } + + @Test + public void requestIteratorRace() { + final Integer[] a = new Integer[1000]; + Arrays.fill(a, 1); + + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final PublishSubject<Integer> ps = PublishSubject.create(); + + final TestSubscriber<Integer> ts = ps.singleOrError().flattenAsFlowable(new Function<Integer, Iterable<Integer>>() { + @Override + public Iterable<Integer> apply(Integer v) throws Exception { + return Arrays.asList(a); + } + }).test(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + for (int i = 0; i < 1000; i++) { + ts.request(1); + } + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + ps.onNext(1); + ps.onComplete(); + } + }; + + TestHelper.race(r1, r2); + } + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeSingleToFlowable(s -> s.flattenAsFlowable(v -> Collections.emptyList())); + } + + @Test + public void badRequest() { + TestHelper.assertBadRequestReported(SingleSubject.create().flattenAsFlowable(v -> Collections.emptyList())); + } + + @Test + public void slowPatchCancelAfterOnNext() { + TestSubscriber<Integer> ts = new TestSubscriber<Integer>() { + @Override + public void onNext(@NonNull Integer t) { + super.onNext(t); + cancel(); + onComplete(); + } + }; + + Single.just(1) + .flattenAsFlowable(v -> Arrays.asList(1, 2)) + .subscribe(ts); + + ts.assertResult(1); + } + + @Test + public void onSuccessRequestRace() { + List<Object> list = Arrays.asList(1); + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + + SingleSubject<Integer> ss = SingleSubject.create(); + + TestSubscriber<Object> ts = ss.flattenAsFlowable(v -> list) + .test(0L); + + TestHelper.race( + () -> ss.onSuccess(1), + () -> ts.request(1) + ); + + ts.assertResult(1); + } + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleFlatMapIterableObservableTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleFlatMapIterableObservableTest.java new file mode 100644 index 0000000000..82448746b8 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleFlatMapIterableObservableTest.java @@ -0,0 +1,322 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.single; + +import static org.junit.Assert.*; + +import java.util.*; +import java.util.concurrent.TimeUnit; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.core.Observer; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.util.CrashingIterable; +import io.reactivex.rxjava3.operators.QueueDisposable; +import io.reactivex.rxjava3.operators.QueueFuseable; +import io.reactivex.rxjava3.schedulers.Schedulers; +import io.reactivex.rxjava3.testsupport.*; + +public class SingleFlatMapIterableObservableTest extends RxJavaTest { + + @Test + public void normal() { + + Single.just(1).flattenAsObservable(new Function<Integer, Iterable<Integer>>() { + @Override + public Iterable<Integer> apply(Integer v) throws Exception { + return Arrays.asList(v, v + 1); + } + }) + .test() + .assertResult(1, 2); + } + + @Test + public void emptyIterable() { + + Single.just(1).flattenAsObservable(new Function<Integer, Iterable<Integer>>() { + @Override + public Iterable<Integer> apply(Integer v) throws Exception { + return Collections.<Integer>emptyList(); + } + }) + .test() + .assertResult(); + } + + @Test + public void error() { + + Single.<Integer>error(new TestException()).flattenAsObservable(new Function<Integer, Iterable<Integer>>() { + @Override + public Iterable<Integer> apply(Integer v) throws Exception { + return Arrays.asList(v, v + 1); + } + }) + .test() + .assertFailure(TestException.class); + } + + @Test + public void take() { + Single.just(1).flattenAsObservable(new Function<Integer, Iterable<Integer>>() { + @Override + public Iterable<Integer> apply(Integer v) throws Exception { + return Arrays.asList(v, v + 1); + } + }) + .take(1) + .test() + .assertResult(1); + } + + @Test + public void fused() { + TestObserverEx<Integer> to = new TestObserverEx<>(QueueFuseable.ANY); + + Single.just(1).flattenAsObservable(new Function<Integer, Iterable<Integer>>() { + @Override + public Iterable<Integer> apply(Integer v) throws Exception { + return Arrays.asList(v, v + 1); + } + }) + .subscribe(to); + + to.assertFuseable() + .assertFusionMode(QueueFuseable.ASYNC) + .assertResult(1, 2); + ; + } + + @Test + public void fusedNoSync() { + TestObserverEx<Integer> to = new TestObserverEx<>(QueueFuseable.SYNC); + + Single.just(1).flattenAsObservable(new Function<Integer, Iterable<Integer>>() { + @Override + public Iterable<Integer> apply(Integer v) throws Exception { + return Arrays.asList(v, v + 1); + } + }) + .subscribe(to); + + to.assertFuseable() + .assertFusionMode(QueueFuseable.NONE) + .assertResult(1, 2); + ; + } + + @Test + public void iteratorCrash() { + + Single.just(1).flattenAsObservable(new Function<Integer, Iterable<Integer>>() { + @Override + public Iterable<Integer> apply(Integer v) throws Exception { + return new CrashingIterable(1, 100, 100); + } + }) + .to(TestHelper.<Integer>testConsumer()) + .assertFailureAndMessage(TestException.class, "iterator()"); + } + + @Test + public void hasNextCrash() { + + Single.just(1).flattenAsObservable(new Function<Integer, Iterable<Integer>>() { + @Override + public Iterable<Integer> apply(Integer v) throws Exception { + return new CrashingIterable(100, 1, 100); + } + }) + .to(TestHelper.<Integer>testConsumer()) + .assertFailureAndMessage(TestException.class, "hasNext()"); + } + + @Test + public void nextCrash() { + + Single.just(1).flattenAsObservable(new Function<Integer, Iterable<Integer>>() { + @Override + public Iterable<Integer> apply(Integer v) throws Exception { + return new CrashingIterable(100, 100, 1); + } + }) + .to(TestHelper.<Integer>testConsumer()) + .assertFailureAndMessage(TestException.class, "next()"); + } + + @Test + public void hasNextCrash2() { + + Single.just(1).flattenAsObservable(new Function<Integer, Iterable<Integer>>() { + @Override + public Iterable<Integer> apply(Integer v) throws Exception { + return new CrashingIterable(100, 2, 100); + } + }) + .to(TestHelper.<Integer>testConsumer()) + .assertFailureAndMessage(TestException.class, "hasNext()", 0); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeSingleToObservable(new Function<Single<Object>, ObservableSource<Integer>>() { + @Override + public ObservableSource<Integer> apply(Single<Object> o) throws Exception { + return o.flattenAsObservable(new Function<Object, Iterable<Integer>>() { + @Override + public Iterable<Integer> apply(Object v) throws Exception { + return Collections.singleton(1); + } + }); + } + }); + } + + @Test + public void dispose() { + TestHelper.checkDisposed(Single.just(1).flattenAsObservable(new Function<Object, Iterable<Integer>>() { + @Override + public Iterable<Integer> apply(Object v) throws Exception { + return Collections.singleton(1); + } + })); + } + + @Test + public void async1() { + Single.just(1) + .flattenAsObservable(new Function<Object, Iterable<Integer>>() { + @Override + public Iterable<Integer> apply(Object v) throws Exception { + Integer[] array = new Integer[1000 * 1000]; + Arrays.fill(array, 1); + return Arrays.asList(array); + } + }) + .hide() + .observeOn(Schedulers.single()) + .to(TestHelper.<Integer>testConsumer()) + .awaitDone(5, TimeUnit.SECONDS) + .assertSubscribed() + .assertValueCount(1000 * 1000) + .assertNoErrors() + .assertComplete(); + } + + @Test + public void async2() { + Single.just(1) + .flattenAsObservable(new Function<Object, Iterable<Integer>>() { + @Override + public Iterable<Integer> apply(Object v) throws Exception { + Integer[] array = new Integer[1000 * 1000]; + Arrays.fill(array, 1); + return Arrays.asList(array); + } + }) + .observeOn(Schedulers.single()) + .to(TestHelper.<Integer>testConsumer()) + .awaitDone(5, TimeUnit.SECONDS) + .assertSubscribed() + .assertValueCount(1000 * 1000) + .assertNoErrors() + .assertComplete(); + } + + @Test + public void async3() { + Single.just(1) + .flattenAsObservable(new Function<Object, Iterable<Integer>>() { + @Override + public Iterable<Integer> apply(Object v) throws Exception { + Integer[] array = new Integer[1000 * 1000]; + Arrays.fill(array, 1); + return Arrays.asList(array); + } + }) + .take(500 * 1000) + .observeOn(Schedulers.single()) + .to(TestHelper.<Integer>testConsumer()) + .awaitDone(5, TimeUnit.SECONDS) + .assertSubscribed() + .assertValueCount(500 * 1000) + .assertNoErrors() + .assertComplete(); + } + + @Test + public void async4() { + Single.just(1) + .flattenAsObservable(new Function<Object, Iterable<Integer>>() { + @Override + public Iterable<Integer> apply(Object v) throws Exception { + Integer[] array = new Integer[1000 * 1000]; + Arrays.fill(array, 1); + return Arrays.asList(array); + } + }) + .observeOn(Schedulers.single()) + .take(500 * 1000) + .to(TestHelper.<Integer>testConsumer()) + .awaitDone(5, TimeUnit.SECONDS) + .assertSubscribed() + .assertValueCount(500 * 1000) + .assertNoErrors() + .assertComplete(); + } + + @Test + public void fusedEmptyCheck() { + Single.just(1) + .flattenAsObservable(new Function<Object, Iterable<Integer>>() { + @Override + public Iterable<Integer> apply(Object v) throws Exception { + return Arrays.asList(1, 2, 3); + } + }).subscribe(new Observer<Integer>() { + QueueDisposable<Integer> qd; + @SuppressWarnings("unchecked") + @Override + public void onSubscribe(Disposable d) { + qd = (QueueDisposable<Integer>)d; + + assertEquals(QueueFuseable.ASYNC, qd.requestFusion(QueueFuseable.ANY)); + } + + @Override + public void onNext(Integer value) { + assertFalse(qd.isEmpty()); + + qd.clear(); + + assertTrue(qd.isEmpty()); + + qd.dispose(); + } + + @Override + public void onError(Throwable e) { + } + + @Override + public void onComplete() { + } + }); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleFlatMapMaybeTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleFlatMapMaybeTest.java new file mode 100644 index 0000000000..6081a621e8 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleFlatMapMaybeTest.java @@ -0,0 +1,141 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.single; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class SingleFlatMapMaybeTest extends RxJavaTest { + @Test + public void flatMapMaybeValue() { + Single.just(1).flatMapMaybe(new Function<Integer, MaybeSource<Integer>>() { + @Override public MaybeSource<Integer> apply(final Integer integer) throws Exception { + if (integer == 1) { + return Maybe.just(2); + } + + return Maybe.just(1); + } + }) + .test() + .assertResult(2); + } + + @Test + public void flatMapMaybeValueDifferentType() { + Single.just(1).flatMapMaybe(new Function<Integer, MaybeSource<String>>() { + @Override public MaybeSource<String> apply(final Integer integer) throws Exception { + if (integer == 1) { + return Maybe.just("2"); + } + + return Maybe.just("1"); + } + }) + .test() + .assertResult("2"); + } + + @Test + public void flatMapMaybeValueNull() { + Single.just(1).flatMapMaybe(new Function<Integer, MaybeSource<Integer>>() { + @Override public MaybeSource<Integer> apply(final Integer integer) throws Exception { + return null; + } + }) + .to(TestHelper.<Integer>testConsumer()) + .assertNoValues() + .assertError(NullPointerException.class) + .assertErrorMessage("The mapper returned a null MaybeSource"); + } + + @Test + public void flatMapMaybeValueErrorThrown() { + Single.just(1).flatMapMaybe(new Function<Integer, MaybeSource<Integer>>() { + @Override public MaybeSource<Integer> apply(final Integer integer) throws Exception { + throw new RuntimeException("something went terribly wrong!"); + } + }) + .to(TestHelper.<Integer>testConsumer()) + .assertNoValues() + .assertError(RuntimeException.class) + .assertErrorMessage("something went terribly wrong!"); + } + + @Test + public void flatMapMaybeError() { + RuntimeException exception = new RuntimeException("test"); + + Single.error(exception).flatMapMaybe(new Function<Object, MaybeSource<Object>>() { + @Override public MaybeSource<Object> apply(final Object integer) throws Exception { + return Maybe.just(new Object()); + } + }) + .test() + .assertError(exception); + } + + @Test + public void dispose() { + TestHelper.checkDisposed(Single.just(1).flatMapMaybe(new Function<Integer, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Integer v) throws Exception { + return Maybe.just(1); + } + })); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeSingleToMaybe(new Function<Single<Integer>, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Single<Integer> v) throws Exception { + return v.flatMapMaybe(new Function<Integer, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Integer v) throws Exception { + return Maybe.just(1); + } + }); + } + }); + } + + @Test + public void mapsToError() { + Single.just(1).flatMapMaybe(new Function<Integer, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Integer v) throws Exception { + return Maybe.error(new TestException()); + } + }) + .test() + .assertFailure(TestException.class); + } + + @Test + public void mapsToEmpty() { + Single.just(1).flatMapMaybe(new Function<Integer, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Integer v) throws Exception { + return Maybe.empty(); + } + }) + .test() + .assertResult(); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleFlatMapNotificationTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleFlatMapNotificationTest.java new file mode 100644 index 0000000000..00f1337b2c --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleFlatMapNotificationTest.java @@ -0,0 +1,113 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.single; + +import static org.junit.Assert.assertTrue; + +import java.io.IOException; +import java.util.List; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.testsupport.*; + +public class SingleFlatMapNotificationTest extends RxJavaTest { + + @Test + public void dispose() { + TestHelper.checkDisposed(Single.just(1) + .flatMap(Functions.justFunction(Single.just(1)), + Functions.justFunction(Single.just(1)))); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeSingle(new Function<Single<Integer>, SingleSource<Integer>>() { + @Override + public SingleSource<Integer> apply(Single<Integer> m) throws Exception { + return m + .flatMap(Functions.justFunction(Single.just(1)), + Functions.justFunction(Single.just(1))); + } + }); + } + + @Test + public void onSuccessNull() { + Single.just(1) + .flatMap(Functions.justFunction((Single<Integer>)null), + Functions.justFunction(Single.just(1))) + .test() + .assertFailure(NullPointerException.class); + } + + @Test + public void onErrorNull() { + TestObserverEx<Integer> to = Single.<Integer>error(new TestException()) + .flatMap(Functions.justFunction(Single.just(1)), + Functions.justFunction((Single<Integer>)null)) + .to(TestHelper.<Integer>testConsumer()) + .assertFailure(CompositeException.class); + + List<Throwable> ce = TestHelper.compositeList(to.errors().get(0)); + + TestHelper.assertError(ce, 0, TestException.class); + TestHelper.assertError(ce, 1, NullPointerException.class); + } + + @Test + public void onSuccessError() { + Single.just(1) + .flatMap(Functions.justFunction(Single.<Integer>error(new TestException())), + Functions.justFunction((Single<Integer>)null)) + .test() + .assertFailure(TestException.class); + } + + @Test + public void onSucccessSuccess() { + Single.just(1) + .flatMap(v -> Single.just(2), e -> Single.just(3)) + .test() + .assertResult(2); + } + + @Test + public void onErrorSuccess() throws Throwable { + TestHelper.withErrorTracking(errors -> { + Single.error(new TestException()) + .flatMap(v -> Single.just(2), e -> Single.just(3)) + .test() + .assertResult(3); + + assertTrue("" + errors, errors.isEmpty()); + }); + } + + @Test + public void onErrorError() throws Throwable { + TestHelper.withErrorTracking(errors -> { + Single.error(new TestException()) + .flatMap(v -> Single.just(2), e -> Single.<Integer>error(new IOException())) + .test() + .assertFailure(IOException.class); + + assertTrue("" + errors, errors.isEmpty()); + }); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleFlatMapTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleFlatMapTest.java new file mode 100644 index 0000000000..08d3d24c5a --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleFlatMapTest.java @@ -0,0 +1,320 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.single; + +import static org.junit.Assert.*; + +import java.util.concurrent.atomic.AtomicBoolean; + +import org.junit.Test; +import org.reactivestreams.Publisher; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.testsupport.*; + +public class SingleFlatMapTest extends RxJavaTest { + + @Test + public void normal() { + final boolean[] b = { false }; + + Single.just(1) + .flatMapCompletable(new Function<Integer, Completable>() { + @Override + public Completable apply(Integer t) throws Exception { + return Completable.complete().doOnComplete(new Action() { + @Override + public void run() throws Exception { + b[0] = true; + } + }); + } + }) + .test() + .assertResult(); + + assertTrue(b[0]); + } + + @Test + public void error() { + final boolean[] b = { false }; + + Single.<Integer>error(new TestException()) + .flatMapCompletable(new Function<Integer, Completable>() { + @Override + public Completable apply(Integer t) throws Exception { + return Completable.complete().doOnComplete(new Action() { + @Override + public void run() throws Exception { + b[0] = true; + } + }); + } + }) + .test() + .assertFailure(TestException.class); + + assertFalse(b[0]); + } + + @Test + public void mapperThrows() { + final boolean[] b = { false }; + + Single.just(1) + .flatMapCompletable(new Function<Integer, Completable>() { + @Override + public Completable apply(Integer t) throws Exception { + throw new TestException(); + } + }) + .test() + .assertFailure(TestException.class); + + assertFalse(b[0]); + } + + @Test + public void mapperReturnsNull() { + final boolean[] b = { false }; + + Single.just(1) + .flatMapCompletable(new Function<Integer, Completable>() { + @Override + public Completable apply(Integer t) throws Exception { + return null; + } + }) + .test() + .assertFailure(NullPointerException.class); + + assertFalse(b[0]); + } + + @Test + public void flatMapObservable() { + Single.just(1).flatMapObservable(new Function<Integer, Observable<Integer>>() { + @Override + public Observable<Integer> apply(Integer v) throws Exception { + return Observable.range(v, 5); + } + }) + .test() + .assertResult(1, 2, 3, 4, 5); + } + + @Test + public void flatMapPublisher() { + Single.just(1).flatMapPublisher(new Function<Integer, Publisher<Integer>>() { + @Override + public Publisher<Integer> apply(Integer v) throws Exception { + return Flowable.range(v, 5); + } + }) + .test() + .assertResult(1, 2, 3, 4, 5); + } + + @Test + public void flatMapPublisherMapperThrows() { + final TestException ex = new TestException(); + Single.just(1) + .flatMapPublisher(new Function<Integer, Publisher<Integer>>() { + @Override + public Publisher<Integer> apply(Integer v) throws Exception { + throw ex; + } + }) + .test() + .assertNoValues() + .assertError(ex); + } + + @Test + public void flatMapPublisherSingleError() { + final TestException ex = new TestException(); + Single.<Integer>error(ex) + .flatMapPublisher(new Function<Integer, Publisher<Integer>>() { + @Override + public Publisher<Integer> apply(Integer v) throws Exception { + return Flowable.just(1); + } + }) + .test() + .assertNoValues() + .assertError(ex); + } + + @Test + public void flatMapPublisherCancelDuringSingle() { + final AtomicBoolean disposed = new AtomicBoolean(); + TestSubscriberEx<Integer> ts = Single.<Integer>never() + .doOnDispose(new Action() { + @Override + public void run() throws Exception { + disposed.set(true); + } + }) + .flatMapPublisher(new Function<Integer, Publisher<Integer>>() { + @Override + public Publisher<Integer> apply(Integer v) throws Exception { + return Flowable.range(v, 5); + } + }) + .to(TestHelper.<Integer>testConsumer()) + .assertNoValues() + .assertNotTerminated(); + assertFalse(disposed.get()); + ts.cancel(); + assertTrue(disposed.get()); + ts.assertNotTerminated(); + } + + @Test + public void flatMapPublisherCancelDuringFlowable() { + final AtomicBoolean disposed = new AtomicBoolean(); + TestSubscriberEx<Integer> ts = + Single.just(1) + .flatMapPublisher(new Function<Integer, Publisher<Integer>>() { + @Override + public Publisher<Integer> apply(Integer v) throws Exception { + return Flowable.<Integer>never() + .doOnCancel(new Action() { + @Override + public void run() throws Exception { + disposed.set(true); + } + }); + } + }) + .to(TestHelper.<Integer>testConsumer()) + .assertNoValues() + .assertNotTerminated(); + assertFalse(disposed.get()); + ts.cancel(); + assertTrue(disposed.get()); + ts.assertNotTerminated(); + } + + @Test + public void flatMapValue() { + Single.just(1).flatMap(new Function<Integer, SingleSource<Integer>>() { + @Override public SingleSource<Integer> apply(final Integer integer) throws Exception { + if (integer == 1) { + return Single.just(2); + } + + return Single.just(1); + } + }) + .test() + .assertResult(2); + } + + @Test + public void flatMapValueDifferentType() { + Single.just(1).flatMap(new Function<Integer, SingleSource<String>>() { + @Override public SingleSource<String> apply(final Integer integer) throws Exception { + if (integer == 1) { + return Single.just("2"); + } + + return Single.just("1"); + } + }) + .test() + .assertResult("2"); + } + + @Test + public void flatMapValueNull() { + Single.just(1).flatMap(new Function<Integer, SingleSource<Integer>>() { + @Override public SingleSource<Integer> apply(final Integer integer) throws Exception { + return null; + } + }) + .to(TestHelper.<Integer>testConsumer()) + .assertNoValues() + .assertError(NullPointerException.class) + .assertErrorMessage("The single returned by the mapper is null"); + } + + @Test + public void flatMapValueErrorThrown() { + Single.just(1).flatMap(new Function<Integer, SingleSource<Integer>>() { + @Override public SingleSource<Integer> apply(final Integer integer) throws Exception { + throw new RuntimeException("something went terribly wrong!"); + } + }) + .to(TestHelper.<Integer>testConsumer()) + .assertNoValues() + .assertError(RuntimeException.class) + .assertErrorMessage("something went terribly wrong!"); + } + + @Test + public void flatMapError() { + RuntimeException exception = new RuntimeException("test"); + + Single.error(exception).flatMap(new Function<Object, SingleSource<Object>>() { + @Override public SingleSource<Object> apply(final Object integer) throws Exception { + return Single.just(new Object()); + } + }) + .test() + .assertError(exception); + } + + @Test + public void dispose() { + TestHelper.checkDisposed(Single.just(1).flatMap(new Function<Integer, SingleSource<Integer>>() { + @Override + public SingleSource<Integer> apply(Integer v) throws Exception { + return Single.just(2); + } + })); + } + + @Test + public void mappedSingleOnError() { + Single.just(1).flatMap(new Function<Integer, SingleSource<Integer>>() { + @Override + public SingleSource<Integer> apply(Integer v) throws Exception { + return Single.error(new TestException()); + } + }) + .test() + .assertFailure(TestException.class); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeSingle(new Function<Single<Object>, SingleSource<Object>>() { + @Override + public SingleSource<Object> apply(Single<Object> s) + throws Exception { + return s.flatMap(new Function<Object, SingleSource<? extends Object>>() { + @Override + public SingleSource<? extends Object> apply(Object v) + throws Exception { + return Single.just(v); + } + }); + } + }); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleFromCallableTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleFromCallableTest.java new file mode 100644 index 0000000000..3352029989 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleFromCallableTest.java @@ -0,0 +1,274 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.single; + +import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import java.util.List; +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.Test; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.schedulers.Schedulers; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class SingleFromCallableTest extends RxJavaTest { + + @Test + public void fromCallableValue() { + Single.fromCallable(new Callable<Integer>() { + @Override public Integer call() throws Exception { + return 5; + } + }) + .test() + .assertResult(5); + } + + @Test + public void fromCallableError() { + Single.fromCallable(new Callable<Integer>() { + @Override public Integer call() throws Exception { + throw new UnsupportedOperationException(); + } + }) + .test() + .assertFailure(UnsupportedOperationException.class); + } + + @Test + public void fromCallableNull() { + Single.fromCallable(new Callable<Integer>() { + @Override public Integer call() throws Exception { + return null; + } + }) + .to(TestHelper.<Integer>testConsumer()) + .assertFailureAndMessage(NullPointerException.class, "The callable returned a null value"); + } + + @Test + public void fromCallableTwice() { + final AtomicInteger atomicInteger = new AtomicInteger(); + + Callable<Integer> callable = new Callable<Integer>() { + @Override + public Integer call() throws Exception { + return atomicInteger.incrementAndGet(); + } + }; + + Single.fromCallable(callable) + .test() + .assertResult(1); + + assertEquals(1, atomicInteger.get()); + + Single.fromCallable(callable) + .test() + .assertResult(2); + + assertEquals(2, atomicInteger.get()); + } + + @SuppressWarnings("unchecked") + @Test + public void shouldNotInvokeFuncUntilSubscription() throws Exception { + Callable<Object> func = mock(Callable.class); + + when(func.call()).thenReturn(new Object()); + + Single<Object> fromCallableSingle = Single.fromCallable(func); + + verifyNoInteractions(func); + + fromCallableSingle.subscribe(); + + verify(func).call(); + } + + @Test + public void noErrorLoss() throws Exception { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + final CountDownLatch cdl1 = new CountDownLatch(1); + final CountDownLatch cdl2 = new CountDownLatch(1); + + TestObserver<Integer> to = Single.fromCallable(new Callable<Integer>() { + @Override + public Integer call() throws Exception { + cdl1.countDown(); + cdl2.await(5, TimeUnit.SECONDS); + return 1; + } + }).subscribeOn(Schedulers.single()).test(); + + assertTrue(cdl1.await(5, TimeUnit.SECONDS)); + + to.dispose(); + + int timeout = 10; + + while (timeout-- > 0 && errors.isEmpty()) { + Thread.sleep(100); + } + + TestHelper.assertUndeliverable(errors, 0, InterruptedException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @SuppressWarnings("unchecked") + @Test + public void shouldNotDeliverResultIfSubscriberUnsubscribedBeforeEmission() throws Exception { + Callable<String> func = mock(Callable.class); + + final CountDownLatch funcLatch = new CountDownLatch(1); + final CountDownLatch observerLatch = new CountDownLatch(1); + + when(func.call()).thenAnswer(new Answer<String>() { + @Override + public String answer(InvocationOnMock invocation) throws Throwable { + observerLatch.countDown(); + + try { + funcLatch.await(); + } catch (InterruptedException e) { + // It's okay, unsubscription causes Thread interruption + + // Restoring interruption status of the Thread + Thread.currentThread().interrupt(); + } + + return "should_not_be_delivered"; + } + }); + + Single<String> fromCallableObservable = Single.fromCallable(func); + + Observer<Object> observer = TestHelper.mockObserver(); + + TestObserver<String> outer = new TestObserver<>(observer); + + fromCallableObservable + .subscribeOn(Schedulers.computation()) + .subscribe(outer); + + // Wait until func will be invoked + observerLatch.await(); + + // Unsubscribing before emission + outer.dispose(); + + // Emitting result + funcLatch.countDown(); + + // func must be invoked + verify(func).call(); + + // Observer must not be notified at all + verify(observer).onSubscribe(any(Disposable.class)); + verifyNoMoreInteractions(observer); + } + + @Test + public void shouldAllowToThrowCheckedException() { + final Exception checkedException = new Exception("test exception"); + + Single<Object> fromCallableObservable = Single.fromCallable(new Callable<Object>() { + @Override + public Object call() throws Exception { + throw checkedException; + } + }); + + SingleObserver<Object> observer = TestHelper.mockSingleObserver(); + + fromCallableObservable.subscribe(observer); + + verify(observer).onSubscribe(any(Disposable.class)); + verify(observer).onError(checkedException); + verifyNoMoreInteractions(observer); + } + + @Test + public void disposedOnArrival() { + final int[] count = { 0 }; + Single.fromCallable(new Callable<Object>() { + @Override + public Object call() throws Exception { + count[0]++; + return 1; + } + }) + .test(true) + .assertEmpty(); + + assertEquals(0, count[0]); + } + + @Test + public void disposedOnCall() { + final TestObserver<Integer> to = new TestObserver<>(); + + Single.fromCallable(new Callable<Integer>() { + @Override + public Integer call() throws Exception { + to.dispose(); + return 1; + } + }) + .subscribe(to); + + to.assertEmpty(); + } + + @Test + public void toObservableTake() { + Single.fromCallable(new Callable<Object>() { + @Override + public Object call() throws Exception { + return 1; + } + }) + .toObservable() + .take(1) + .test() + .assertResult(1); + } + + @Test + public void toObservableAndBack() { + Single.fromCallable(new Callable<Integer>() { + @Override + public Integer call() throws Exception { + return 1; + } + }) + .toObservable() + .singleOrError() + .test() + .assertResult(1); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleFromMaybeTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleFromMaybeTest.java new file mode 100644 index 0000000000..293cedf574 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleFromMaybeTest.java @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.single; + +import static org.junit.Assert.*; + +import java.util.NoSuchElementException; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.subjects.MaybeSubject; + +public class SingleFromMaybeTest extends RxJavaTest { + + @Test + public void success() { + Single.fromMaybe(Maybe.just(1).hide()) + .test() + .assertResult(1); + } + + @Test + public void empty() { + Single.fromMaybe(Maybe.empty().hide()) + .test() + .assertFailure(NoSuchElementException.class); + } + + @Test + public void emptyDefault() { + Single.fromMaybe(Maybe.empty().hide(), 1) + .test() + .assertResult(1); + } + + @Test + public void error() { + Single.fromMaybe(Maybe.error(new TestException()).hide()) + .test() + .assertFailure(TestException.class); + } + + @Test + public void cancelComposes() { + MaybeSubject<Integer> ms = MaybeSubject.create(); + + TestObserver<Integer> to = Single.fromMaybe(ms) + .test(); + + to.assertEmpty(); + + assertTrue(ms.hasObservers()); + + to.dispose(); + + assertFalse(ms.hasObservers()); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleFromPublisherTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleFromPublisherTest.java new file mode 100644 index 0000000000..080870b228 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleFromPublisherTest.java @@ -0,0 +1,110 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.single; + +import static org.junit.Assert.*; + +import java.util.*; + +import org.junit.Test; +import org.reactivestreams.Subscriber; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.internal.subscriptions.BooleanSubscription; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.processors.PublishProcessor; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class SingleFromPublisherTest extends RxJavaTest { + + @Test + public void just() { + Single.fromPublisher(Flowable.just(1)) + .test() + .assertResult(1); + } + + @Test + public void range() { + Single.fromPublisher(Flowable.range(1, 3)) + .test() + .assertFailure(IndexOutOfBoundsException.class); + } + + @Test + public void empty() { + Single.fromPublisher(Flowable.empty()) + .test() + .assertFailure(NoSuchElementException.class); + } + + @Test + public void error() { + Single.fromPublisher(Flowable.error(new TestException())) + .test() + .assertFailure(TestException.class); + } + + @Test + public void dispose() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + + TestObserver<Integer> to = Single.fromPublisher(pp).test(); + + assertTrue(pp.hasSubscribers()); + + pp.onNext(1); + + to.dispose(); + + assertFalse(pp.hasSubscribers()); + } + + @Test + public void isDisposed() { + TestHelper.checkDisposed(Single.fromPublisher(Flowable.never())); + } + + @Test + public void badSource() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + + try { + Single.fromPublisher(new Flowable<Integer>() { + @Override + protected void subscribeActual(Subscriber<? super Integer> s) { + s.onSubscribe(new BooleanSubscription()); + BooleanSubscription s2 = new BooleanSubscription(); + s.onSubscribe(s2); + assertTrue(s2.isCancelled()); + + s.onNext(1); + s.onComplete(); + s.onNext(2); + s.onError(new TestException()); + s.onComplete(); + } + }) + .test() + .assertResult(1); + + TestHelper.assertError(errors, 0, IllegalStateException.class, "Subscription already set!"); + TestHelper.assertUndeliverable(errors, 1, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleFromSupplierTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleFromSupplierTest.java new file mode 100644 index 0000000000..927ec7f837 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleFromSupplierTest.java @@ -0,0 +1,275 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.single; + +import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import java.util.List; +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.Test; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.functions.Supplier; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.schedulers.Schedulers; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class SingleFromSupplierTest extends RxJavaTest { + + @Test + public void fromSupplierValue() { + Single.fromSupplier(new Supplier<Integer>() { + @Override public Integer get() throws Exception { + return 5; + } + }) + .test() + .assertResult(5); + } + + @Test + public void fromSupplierError() { + Single.fromSupplier(new Supplier<Integer>() { + @Override public Integer get() throws Exception { + throw new UnsupportedOperationException(); + } + }) + .test() + .assertFailure(UnsupportedOperationException.class); + } + + @Test + public void fromSupplierNull() { + Single.fromSupplier(new Supplier<Integer>() { + @Override public Integer get() throws Exception { + return null; + } + }) + .to(TestHelper.<Integer>testConsumer()) + .assertFailureAndMessage(NullPointerException.class, "The supplier returned a null value"); + } + + @Test + public void fromSupplierTwice() { + final AtomicInteger atomicInteger = new AtomicInteger(); + + Supplier<Integer> supplier = new Supplier<Integer>() { + @Override + public Integer get() throws Exception { + return atomicInteger.incrementAndGet(); + } + }; + + Single.fromSupplier(supplier) + .test() + .assertResult(1); + + assertEquals(1, atomicInteger.get()); + + Single.fromSupplier(supplier) + .test() + .assertResult(2); + + assertEquals(2, atomicInteger.get()); + } + + @SuppressWarnings("unchecked") + @Test + public void shouldNotInvokeFuncUntilSubscription() throws Throwable { + Supplier<Object> func = mock(Supplier.class); + + when(func.get()).thenReturn(new Object()); + + Single<Object> fromSupplierSingle = Single.fromSupplier(func); + + verifyNoInteractions(func); + + fromSupplierSingle.subscribe(); + + verify(func).get(); + } + + @Test + public void noErrorLoss() throws Exception { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + final CountDownLatch cdl1 = new CountDownLatch(1); + final CountDownLatch cdl2 = new CountDownLatch(1); + + TestObserver<Integer> to = Single.fromSupplier(new Supplier<Integer>() { + @Override + public Integer get() throws Exception { + cdl1.countDown(); + cdl2.await(5, TimeUnit.SECONDS); + return 1; + } + }).subscribeOn(Schedulers.single()).test(); + + assertTrue(cdl1.await(5, TimeUnit.SECONDS)); + + to.dispose(); + + int timeout = 10; + + while (timeout-- > 0 && errors.isEmpty()) { + Thread.sleep(100); + } + + TestHelper.assertUndeliverable(errors, 0, InterruptedException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @SuppressWarnings("unchecked") + @Test + public void shouldNotDeliverResultIfSubscriberUnsubscribedBeforeEmission() throws Throwable { + Supplier<String> func = mock(Supplier.class); + + final CountDownLatch funcLatch = new CountDownLatch(1); + final CountDownLatch observerLatch = new CountDownLatch(1); + + when(func.get()).thenAnswer(new Answer<String>() { + @Override + public String answer(InvocationOnMock invocation) throws Throwable { + observerLatch.countDown(); + + try { + funcLatch.await(); + } catch (InterruptedException e) { + // It's okay, unsubscription causes Thread interruption + + // Restoring interruption status of the Thread + Thread.currentThread().interrupt(); + } + + return "should_not_be_delivered"; + } + }); + + Single<String> fromSupplierObservable = Single.fromSupplier(func); + + Observer<Object> observer = TestHelper.mockObserver(); + + TestObserver<String> outer = new TestObserver<>(observer); + + fromSupplierObservable + .subscribeOn(Schedulers.computation()) + .subscribe(outer); + + // Wait until func will be invoked + observerLatch.await(); + + // Unsubscribing before emission + outer.dispose(); + + // Emitting result + funcLatch.countDown(); + + // func must be invoked + verify(func).get(); + + // Observer must not be notified at all + verify(observer).onSubscribe(any(Disposable.class)); + verifyNoMoreInteractions(observer); + } + + @Test + public void shouldAllowToThrowCheckedException() { + final Exception checkedException = new Exception("test exception"); + + Single<Object> fromSupplierObservable = Single.fromSupplier(new Supplier<Object>() { + @Override + public Object get() throws Exception { + throw checkedException; + } + }); + + SingleObserver<Object> observer = TestHelper.mockSingleObserver(); + + fromSupplierObservable.subscribe(observer); + + verify(observer).onSubscribe(any(Disposable.class)); + verify(observer).onError(checkedException); + verifyNoMoreInteractions(observer); + } + + @Test + public void disposedOnArrival() { + final int[] count = { 0 }; + Single.fromSupplier(new Supplier<Object>() { + @Override + public Object get() throws Exception { + count[0]++; + return 1; + } + }) + .test(true) + .assertEmpty(); + + assertEquals(0, count[0]); + } + + @Test + public void disposedOnCall() { + final TestObserver<Integer> to = new TestObserver<>(); + + Single.fromSupplier(new Supplier<Integer>() { + @Override + public Integer get() throws Exception { + to.dispose(); + return 1; + } + }) + .subscribe(to); + + to.assertEmpty(); + } + + @Test + public void toObservableTake() { + Single.fromSupplier(new Supplier<Object>() { + @Override + public Object get() throws Exception { + return 1; + } + }) + .toObservable() + .take(1) + .test() + .assertResult(1); + } + + @Test + public void toObservableAndBack() { + Single.fromSupplier(new Supplier<Integer>() { + @Override + public Integer get() throws Exception { + return 1; + } + }) + .toObservable() + .singleOrError() + .test() + .assertResult(1); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleFromTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleFromTest.java new file mode 100644 index 0000000000..a322d8852e --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleFromTest.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.single; + +import java.util.concurrent.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.schedulers.Schedulers; + +public class SingleFromTest extends RxJavaTest { + + @Test + public void fromFuture() throws Exception { + Single.fromFuture(Flowable.just(1).toFuture()) + .subscribeOn(Schedulers.io()) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(1); + } + + @Test + public void fromFutureTimeout() throws Exception { + Single.fromFuture(Flowable.never().toFuture(), 1, TimeUnit.SECONDS) + .subscribeOn(Schedulers.io()) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertFailure(TimeoutException.class); + } + + @Test + public void fromPublisher() { + Single.fromPublisher(Flowable.just(1)) + .test() + .assertResult(1); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleHideTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleHideTest.java new file mode 100644 index 0000000000..d13218bc84 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleHideTest.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.single; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.subjects.PublishSubject; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class SingleHideTest extends RxJavaTest { + + @Test + public void error() { + Single.error(new TestException()) + .hide() + .test() + .assertFailure(TestException.class); + } + + @Test + public void dispose() { + TestHelper.checkDisposed(PublishSubject.create().singleOrError().hide()); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeSingle(new Function<Single<Object>, SingleSource<Object>>() { + @Override + public SingleSource<Object> apply(Single<Object> s) throws Exception { + return s.hide(); + } + }); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleInternalHelperTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleInternalHelperTest.java new file mode 100644 index 0000000000..478a03d9ff --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleInternalHelperTest.java @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.single; + +import static org.junit.Assert.*; + +import java.util.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class SingleInternalHelperTest extends RxJavaTest { + + @Test + public void utilityClass() { + TestHelper.checkUtilityClass(SingleInternalHelper.class); + } + + @Test + public void noSuchElementCallableEnum() { + assertEquals(1, SingleInternalHelper.NoSuchElementSupplier.values().length); + assertNotNull(SingleInternalHelper.NoSuchElementSupplier.valueOf("INSTANCE")); + } + + @Test + public void toFlowableEnum() { + assertEquals(1, SingleInternalHelper.ToFlowable.values().length); + assertNotNull(SingleInternalHelper.ToFlowable.valueOf("INSTANCE")); + } + + @Test + public void singleIterableToFlowableIterable() { + Iterable<? extends Flowable<Integer>> it = SingleInternalHelper.iterableToFlowable( + Collections.singletonList(Single.just(1))); + + Iterator<? extends Flowable<Integer>> iter = it.iterator(); + + if (iter.hasNext()) { + iter.next().test().assertResult(1); + if (iter.hasNext()) { + fail("Iterator reports an additional element"); + } + } else { + fail("Iterator was empty"); + } + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleLiftTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleLiftTest.java new file mode 100644 index 0000000000..6fdd626bd3 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleLiftTest.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.single; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; + +public class SingleLiftTest extends RxJavaTest { + + @Test + public void normal() { + + Single.just(1).lift(new SingleOperator<Integer, Integer>() { + @Override + public SingleObserver<Integer> apply(final SingleObserver<? super Integer> observer) throws Exception { + return new SingleObserver<Integer>() { + + @Override + public void onSubscribe(Disposable d) { + observer.onSubscribe(d); + } + + @Override + public void onSuccess(Integer value) { + observer.onSuccess(value + 1); + } + + @Override + public void onError(Throwable e) { + observer.onError(e); + } + }; + } + }) + .test() + .assertResult(2); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleMapTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleMapTest.java new file mode 100644 index 0000000000..eb220e0580 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleMapTest.java @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.single; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class SingleMapTest extends RxJavaTest { + + @Test + public void mapValue() { + Single.just(1).map(new Function<Integer, Integer>() { + @Override + public Integer apply(final Integer integer) throws Exception { + if (integer == 1) { + return 2; + } + + return 1; + } + }) + .test() + .assertResult(2); + } + + @Test + public void mapValueNull() { + Single.just(1).map(new Function<Integer, SingleSource<Integer>>() { + @Override + public SingleSource<Integer> apply(final Integer integer) throws Exception { + return null; + } + }) + .to(TestHelper.<SingleSource<Integer>>testConsumer()) + .assertNoValues() + .assertError(NullPointerException.class) + .assertErrorMessage("The mapper function returned a null value."); + } + + @Test + public void mapValueErrorThrown() { + Single.just(1).map(new Function<Integer, SingleSource<Integer>>() { + @Override + public SingleSource<Integer> apply(final Integer integer) throws Exception { + throw new RuntimeException("something went terribly wrong!"); + } + }) + .to(TestHelper.<SingleSource<Integer>>testConsumer()) + .assertNoValues() + .assertError(RuntimeException.class) + .assertErrorMessage("something went terribly wrong!"); + } + + @Test + public void mapError() { + RuntimeException exception = new RuntimeException("test"); + + Single.error(exception).map(new Function<Object, Object>() { + @Override + public Object apply(final Object integer) throws Exception { + return new Object(); + } + }) + .test() + .assertError(exception); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleMaterializeTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleMaterializeTest.java new file mode 100644 index 0000000000..46625b80ab --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleMaterializeTest.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.single; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.subjects.SingleSubject; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class SingleMaterializeTest extends RxJavaTest { + + @Test + public void success() { + Single.just(1) + .materialize() + .test() + .assertResult(Notification.createOnNext(1)); + } + + @Test + public void error() { + TestException ex = new TestException(); + Maybe.error(ex) + .materialize() + .test() + .assertResult(Notification.createOnError(ex)); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeSingle(new Function<Single<Object>, SingleSource<Notification<Object>>>() { + @Override + public SingleSource<Notification<Object>> apply(Single<Object> v) throws Exception { + return v.materialize(); + } + }); + } + + @Test + public void dispose() { + TestHelper.checkDisposed(SingleSubject.create().materialize()); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleMergeArrayTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleMergeArrayTest.java new file mode 100644 index 0000000000..b2bda3a7d7 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleMergeArrayTest.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.single; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; + +public class SingleMergeArrayTest extends RxJavaTest { + + @Test + public void normal() { + Single.mergeArray(Single.just(1), Single.just(2), Single.just(3)) + .test() + .assertResult(1, 2, 3); + } + + @Test + public void error() { + Single.mergeArray(Single.just(1), Single.error(new TestException()), Single.just(3)) + .test() + .assertFailure(TestException.class, 1); + } + + @Test + public void normalDelayError() { + Single.mergeArrayDelayError(Single.just(1), Single.just(2), Single.just(3)) + .test() + .assertResult(1, 2, 3); + } + + @Test + public void errorDelayError() { + Single.mergeArrayDelayError(Single.just(1), Single.error(new TestException()), Single.just(3)) + .test() + .assertFailure(TestException.class, 1, 3); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleMergeTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleMergeTest.java new file mode 100644 index 0000000000..dd4844da5b --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleMergeTest.java @@ -0,0 +1,139 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.single; + +import static org.junit.Assert.assertTrue; + +import java.util.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class SingleMergeTest extends RxJavaTest { + + @Test + public void mergeSingleSingle() { + + Single.merge(Single.just(Single.just(1))) + .test() + .assertResult(1); + } + + @Test + public void merge2() { + Single.merge(Single.just(1), Single.just(2)) + .test() + .assertResult(1, 2); + } + + @Test + public void merge3() { + Single.merge(Single.just(1), Single.just(2), Single.just(3)) + .test() + .assertResult(1, 2, 3); + } + + @Test + public void merge4() { + Single.merge(Single.just(1), Single.just(2), Single.just(3), Single.just(4)) + .test() + .assertResult(1, 2, 3, 4); + } + + @Test + public void mergeErrors() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + Single<Integer> source1 = Single.error(new TestException("First")); + Single<Integer> source2 = Single.error(new TestException("Second")); + + Single.merge(source1, source2) + .to(TestHelper.<Integer>testConsumer()) + .assertFailureAndMessage(TestException.class, "First"); + + assertTrue(errors.toString(), errors.isEmpty()); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void mergeDelayErrorIterable() { + Single.mergeDelayError(Arrays.asList( + Single.just(1), + Single.<Integer>error(new TestException()), + Single.just(2)) + ) + .test() + .assertFailure(TestException.class, 1, 2); + } + + @Test + public void mergeDelayErrorPublisher() { + Single.mergeDelayError(Flowable.just( + Single.just(1), + Single.<Integer>error(new TestException()), + Single.just(2)) + ) + .test() + .assertFailure(TestException.class, 1, 2); + } + + @Test + public void mergeDelayError2() { + Single.mergeDelayError( + Single.just(1), + Single.<Integer>error(new TestException()) + ) + .test() + .assertFailure(TestException.class, 1); + } + + @Test + public void mergeDelayError2ErrorFirst() { + Single.mergeDelayError( + Single.<Integer>error(new TestException()), + Single.just(1) + ) + .test() + .assertFailure(TestException.class, 1); + } + + @Test + public void mergeDelayError3() { + Single.mergeDelayError( + Single.just(1), + Single.<Integer>error(new TestException()), + Single.just(2) + ) + .test() + .assertFailure(TestException.class, 1, 2); + } + + @Test + public void mergeDelayError4() { + Single.mergeDelayError( + Single.just(1), + Single.<Integer>error(new TestException()), + Single.just(2), + Single.just(3) + ) + .test() + .assertFailure(TestException.class, 1, 2, 3); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleMiscTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleMiscTest.java new file mode 100644 index 0000000000..93eebfe7ec --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleMiscTest.java @@ -0,0 +1,292 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.single; + +import static org.junit.Assert.*; + +import java.util.concurrent.*; +import java.util.concurrent.atomic.*; + +import io.reactivex.rxjava3.disposables.Disposable; +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.schedulers.Schedulers; + +public class SingleMiscTest extends RxJavaTest { + @Test + public void never() { + Single.never() + .test() + .assertNoValues() + .assertNoErrors() + .assertNotComplete(); + } + + @Test + public void timer() throws Exception { + Single.timer(100, TimeUnit.MILLISECONDS) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(0L); + } + + @Test + public void wrap() { + assertSame(Single.never(), Single.wrap(Single.never())); + + Single.wrap(new SingleSource<Object>() { + @Override + public void subscribe(SingleObserver<? super Object> observer) { + observer.onSubscribe(Disposable.empty()); + observer.onSuccess(1); + } + }) + .test() + .assertResult(1); + } + + @Test + public void cast() { + Single<Number> source = Single.just(1d) + .cast(Number.class); + source.test() + .assertResult((Number)1d); + } + + @Test + public void contains() { + Single.just(1).contains(1).test().assertResult(true); + + Single.just(2).contains(1).test().assertResult(false); + } + + @Test + public void compose() { + + Single.just(1) + .compose(new SingleTransformer<Integer, Object>() { + @Override + public SingleSource<Object> apply(Single<Integer> f) { + return f.map(new Function<Integer, Object>() { + @Override + public Object apply(Integer v) throws Exception { + return v + 1; + } + }); + } + }) + .test() + .assertResult(2); + } + + @Test + public void hide() { + assertNotSame(Single.never(), Single.never().hide()); + } + + @Test + public void onErrorResumeWith() { + Single.<Integer>error(new TestException()) + .onErrorResumeWith(Single.just(1)) + .test() + .assertResult(1); + } + + @Test + public void onErrorReturnValue() { + Single.<Integer>error(new TestException()) + .onErrorReturnItem(1) + .test() + .assertResult(1); + } + + @Test + public void repeat() { + Single.just(1).repeat().take(5) + .test() + .assertResult(1, 1, 1, 1, 1); + } + + @Test + public void repeatTimes() { + Single.just(1).repeat(5) + .test() + .assertResult(1, 1, 1, 1, 1); + } + + @Test + public void repeatUntil() { + final AtomicBoolean flag = new AtomicBoolean(); + + Single.just(1) + .doOnSuccess(new Consumer<Integer>() { + int c; + @Override + public void accept(Integer v) throws Exception { + if (++c == 5) { + flag.set(true); + } + + } + }) + .repeatUntil(new BooleanSupplier() { + @Override + public boolean getAsBoolean() throws Exception { + return flag.get(); + } + }) + .test() + .assertResult(1, 1, 1, 1, 1); + } + + @Test + public void retry() { + Single.fromCallable(new Callable<Object>() { + int c; + @Override + public Object call() throws Exception { + if (++c != 5) { + throw new TestException(); + } + return 1; + } + }) + .retry() + .test() + .assertResult(1); + } + + @Test + public void retryBiPredicate() { + Single.fromCallable(new Callable<Object>() { + int c; + @Override + public Object call() throws Exception { + if (++c != 5) { + throw new TestException(); + } + return 1; + } + }) + .retry(new BiPredicate<Integer, Throwable>() { + @Override + public boolean test(Integer i, Throwable e) throws Exception { + return true; + } + }) + .test() + .assertResult(1); + } + + @Test + public void retryTimes() { + final AtomicInteger calls = new AtomicInteger(); + + Single.fromCallable(new Callable<Object>() { + + @Override + public Object call() throws Exception { + if (calls.incrementAndGet() != 6) { + throw new TestException(); + } + return 1; + } + }) + .retry(5) + .test() + .assertResult(1); + + assertEquals(6, calls.get()); + } + + @Test + public void retryPredicate() { + Single.fromCallable(new Callable<Object>() { + int c; + @Override + public Object call() throws Exception { + if (++c != 5) { + throw new TestException(); + } + return 1; + } + }) + .retry(new Predicate<Throwable>() { + @Override + public boolean test(Throwable e) throws Exception { + return true; + } + }) + .test() + .assertResult(1); + } + + @Test + public void timeout() throws Exception { + Single.never().timeout(100, TimeUnit.MILLISECONDS, Schedulers.io()) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertFailure(TimeoutException.class); + } + + @Test + public void timeoutOther() throws Exception { + Single.never() + .timeout(100, TimeUnit.MILLISECONDS, Schedulers.io(), Single.just(1)) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(1); + } + + @Test + public void ignoreElement() { + Single.just(1) + .ignoreElement() + .test() + .assertResult(); + + Single.error(new TestException()) + .ignoreElement() + .test() + .assertFailure(TestException.class); + } + + @Test + public void toObservable() { + Single.just(1) + .toObservable() + .test() + .assertResult(1); + + Single.error(new TestException()) + .toObservable() + .test() + .assertFailure(TestException.class); + } + + @Test + public void equals() { + Single.sequenceEqual(Single.just(1), Single.just(1).hide()) + .test() + .assertResult(true); + + Single.sequenceEqual(Single.just(1), Single.just(2)) + .test() + .assertResult(false); + } + +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleObserveOnTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleObserveOnTest.java new file mode 100644 index 0000000000..4abc87e51f --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleObserveOnTest.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.single; + +import java.util.concurrent.TimeUnit; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.schedulers.Schedulers; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class SingleObserveOnTest extends RxJavaTest { + + @Test + public void dispose() { + TestHelper.checkDisposed(Single.just(1).observeOn(Schedulers.single())); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeSingle(new Function<Single<Object>, SingleSource<Object>>() { + @Override + public SingleSource<Object> apply(Single<Object> s) throws Exception { + return s.observeOn(Schedulers.single()); + } + }); + } + + @Test + public void error() { + Single.error(new TestException()) + .observeOn(Schedulers.single()) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertFailure(TestException.class); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleOfTypeTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleOfTypeTest.java new file mode 100644 index 0000000000..9569e9a35d --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleOfTypeTest.java @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.single; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.processors.PublishProcessor; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class SingleOfTypeTest extends RxJavaTest { + + @Test + public void normal() { + Single.just(1).ofType(Integer.class) + .test() + .assertResult(1); + } + + @Test + public void normalDowncast() { + TestObserver<Number> to = Single.just(1) + .ofType(Number.class) + .test(); + // don't make this fluent, target type required! + to.assertResult((Number)1); + } + + @Test + public void notInstance() { + TestObserver<String> to = Single.just(1) + .ofType(String.class) + .test(); + // don't make this fluent, target type required! + to.assertResult(); + } + + @Test + public void error() { + TestObserver<Number> to = Single.<Integer>error(new TestException()) + .ofType(Number.class) + .test(); + // don't make this fluent, target type required! + to.assertFailure(TestException.class); + } + + @Test + public void errorNotInstance() { + TestObserver<String> to = Single.<Integer>error(new TestException()) + .ofType(String.class) + .test(); + // don't make this fluent, target type required! + to.assertFailure(TestException.class); + } + + @Test + public void dispose() { + TestHelper.checkDisposedSingleToMaybe(new Function<Single<Object>, Maybe<Object>>() { + @Override + public Maybe<Object> apply(Single<Object> m) throws Exception { + return m.ofType(Object.class); + } + }); + } + + @Test + public void isDisposed() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + + TestHelper.checkDisposed(pp.singleElement().ofType(Object.class)); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeSingleToMaybe(new Function<Single<Object>, Maybe<Object>>() { + @Override + public Maybe<Object> apply(Single<Object> f) throws Exception { + return f.ofType(Object.class); + } + }); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleOnErrorCompleteTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleOnErrorCompleteTest.java new file mode 100644 index 0000000000..9b184ae7f8 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleOnErrorCompleteTest.java @@ -0,0 +1,113 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.single; + +import static org.junit.Assert.*; + +import java.io.IOException; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.Single; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.subjects.*; +import io.reactivex.rxjava3.testsupport.*; + +public class SingleOnErrorCompleteTest { + + @Test + public void normal() { + Single.just(1) + .onErrorComplete() + .test() + .assertResult(1); + } + + @Test + public void error() throws Throwable { + TestHelper.withErrorTracking(errors -> { + Single.error(new TestException()) + .onErrorComplete() + .test() + .assertResult(); + + assertTrue("" + errors, errors.isEmpty()); + }); + } + + @Test + public void errorMatches() throws Throwable { + TestHelper.withErrorTracking(errors -> { + Single.error(new TestException()) + .onErrorComplete(error -> error instanceof TestException) + .test() + .assertResult(); + + assertTrue("" + errors, errors.isEmpty()); + }); + } + + @Test + public void errorNotMatches() throws Throwable { + TestHelper.withErrorTracking(errors -> { + Single.error(new IOException()) + .onErrorComplete(error -> error instanceof TestException) + .test() + .assertFailure(IOException.class); + + assertTrue("" + errors, errors.isEmpty()); + }); + } + + @Test + public void errorPredicateCrash() throws Throwable { + TestHelper.withErrorTracking(errors -> { + TestObserverEx<Object> to = Single.error(new IOException()) + .onErrorComplete(error -> { throw new TestException(); }) + .subscribeWith(new TestObserverEx<>()) + .assertFailure(CompositeException.class); + + TestHelper.assertError(to, 0, IOException.class); + TestHelper.assertError(to, 1, TestException.class); + + assertTrue("" + errors, errors.isEmpty()); + }); + } + + @Test + public void dispose() { + SingleSubject<Integer> ss = SingleSubject.create(); + + TestObserver<Integer> to = ss + .onErrorComplete() + .test(); + + assertTrue("No subscribers?!", ss.hasObservers()); + + to.dispose(); + + assertFalse("Still subscribers?!", ss.hasObservers()); + } + + @Test + public void onSubscribe() { + TestHelper.checkDoubleOnSubscribeSingleToMaybe(f -> f.onErrorComplete()); + } + + @Test + public void isDisposed() { + TestHelper.checkDisposed(SingleSubject.create().onErrorComplete()); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleOnErrorXTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleOnErrorXTest.java new file mode 100644 index 0000000000..42e36eaba8 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleOnErrorXTest.java @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.single; + +import java.util.List; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.testsupport.*; + +public class SingleOnErrorXTest extends RxJavaTest { + + @Test + public void returnSuccess() { + Single.just(1) + .onErrorReturnItem(2) + .test() + .assertResult(1); + } + + @Test + public void resumeThrows() { + TestObserverEx<Integer> to = Single.<Integer>error(new TestException("Outer")) + .onErrorReturn(new Function<Throwable, Integer>() { + @Override + public Integer apply(Throwable e) throws Exception { + throw new TestException("Inner"); + } + }) + .to(TestHelper.<Integer>testConsumer()) + .assertFailure(CompositeException.class); + + List<Throwable> errors = TestHelper.compositeList(to.errors().get(0)); + + TestHelper.assertError(errors, 0, TestException.class, "Outer"); + TestHelper.assertError(errors, 1, TestException.class, "Inner"); + } + + @Test + public void resumeErrors() { + Single.error(new TestException("Main")) + .onErrorResumeWith(Single.error(new TestException("Resume"))) + .to(TestHelper.<Object>testConsumer()) + .assertFailureAndMessage(TestException.class, "Resume"); + } + + @Test + public void resumeDispose() { + TestHelper.checkDisposed(Single.error(new TestException("Main")) + .onErrorResumeWith(Single.just(1))); + } + + @Test + public void resumeDoubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeSingle(new Function<Single<Object>, SingleSource<Object>>() { + @Override + public SingleSource<Object> apply(Single<Object> s) throws Exception { + return s.onErrorResumeWith(Single.just(1)); + } + }); + } + + @Test + public void resumeSuccess() { + Single.just(1) + .onErrorResumeWith(Single.just(2)) + .test() + .assertResult(1); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleSafeSubscribeTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleSafeSubscribeTest.java new file mode 100644 index 0000000000..d9711c67c0 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleSafeSubscribeTest.java @@ -0,0 +1,154 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.single; + +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import java.io.IOException; + +import org.junit.Test; +import org.mockito.InOrder; + +import io.reactivex.rxjava3.annotations.NonNull; +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class SingleSafeSubscribeTest { + + @Test + public void normalSuccess() throws Throwable { + TestHelper.withErrorTracking(errors -> { + @SuppressWarnings("unchecked") + SingleObserver<Integer> consumer = mock(SingleObserver.class); + + Single.just(1) + .safeSubscribe(consumer); + + InOrder order = inOrder(consumer); + order.verify(consumer).onSubscribe(any(Disposable.class)); + order.verify(consumer).onSuccess(1); + order.verifyNoMoreInteractions(); + + assertTrue("" + errors, errors.isEmpty()); + }); + } + + @Test + public void normalError() throws Throwable { + TestHelper.withErrorTracking(errors -> { + @SuppressWarnings("unchecked") + SingleObserver<Integer> consumer = mock(SingleObserver.class); + + Single.<Integer>error(new TestException()) + .safeSubscribe(consumer); + + InOrder order = inOrder(consumer); + order.verify(consumer).onSubscribe(any(Disposable.class)); + order.verify(consumer).onError(any(TestException.class)); + order.verifyNoMoreInteractions(); + + assertTrue("" + errors, errors.isEmpty()); + }); + } + + @Test + public void onSubscribeCrash() throws Throwable { + TestHelper.withErrorTracking(errors -> { + @SuppressWarnings("unchecked") + SingleObserver<Integer> consumer = mock(SingleObserver.class); + doThrow(new TestException()).when(consumer).onSubscribe(any()); + + Disposable d = Disposable.empty(); + + new Single<Integer>() { + @Override + protected void subscribeActual(@NonNull SingleObserver<? super Integer> observer) { + observer.onSubscribe(d); + // none of the following should arrive at the consumer + observer.onSuccess(1); + observer.onError(new IOException()); + } + } + .safeSubscribe(consumer); + + InOrder order = inOrder(consumer); + order.verify(consumer).onSubscribe(any(Disposable.class)); + order.verifyNoMoreInteractions(); + + assertTrue(d.isDisposed()); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + TestHelper.assertUndeliverable(errors, 1, IOException.class); + }); + } + + @Test + public void onSuccessCrash() throws Throwable { + TestHelper.withErrorTracking(errors -> { + @SuppressWarnings("unchecked") + SingleObserver<Integer> consumer = mock(SingleObserver.class); + doThrow(new TestException()).when(consumer).onSuccess(any()); + + new Single<Integer>() { + @Override + protected void subscribeActual(@NonNull SingleObserver<? super Integer> observer) { + observer.onSubscribe(Disposable.empty()); + observer.onSuccess(1); + } + } + .safeSubscribe(consumer); + + InOrder order = inOrder(consumer); + order.verify(consumer).onSubscribe(any(Disposable.class)); + order.verify(consumer).onSuccess(1); + order.verifyNoMoreInteractions(); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + }); + } + + @Test + public void onErrorCrash() throws Throwable { + TestHelper.withErrorTracking(errors -> { + @SuppressWarnings("unchecked") + SingleObserver<Integer> consumer = mock(SingleObserver.class); + doThrow(new TestException()).when(consumer).onError(any()); + + new Single<Integer>() { + @Override + protected void subscribeActual(@NonNull SingleObserver<? super Integer> observer) { + observer.onSubscribe(Disposable.empty()); + // none of the following should arrive at the consumer + observer.onError(new IOException()); + } + } + .safeSubscribe(consumer); + + InOrder order = inOrder(consumer); + order.verify(consumer).onSubscribe(any(Disposable.class)); + order.verify(consumer).onError(any(IOException.class)); + order.verifyNoMoreInteractions(); + + TestHelper.assertError(errors, 0, CompositeException.class); + + CompositeException compositeException = (CompositeException)errors.get(0); + TestHelper.assertError(compositeException.getExceptions(), 0, IOException.class); + TestHelper.assertError(compositeException.getExceptions(), 1, TestException.class); + }); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleStartWithTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleStartWithTest.java new file mode 100644 index 0000000000..3d01226d9a --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleStartWithTest.java @@ -0,0 +1,126 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.single; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; + +public class SingleStartWithTest { + + @Test + public void justCompletableComplete() { + Single.just(1) + .startWith(Completable.complete()) + .test() + .assertResult(1); + } + + @Test + public void justCompletableError() { + Single.just(1) + .startWith(Completable.error(new TestException())) + .test() + .assertFailure(TestException.class); + } + + @Test + public void justSingleJust() { + Single.just(1) + .startWith(Single.just(0)) + .test() + .assertResult(0, 1); + } + + @Test + public void justSingleError() { + Single.just(1) + .startWith(Single.error(new TestException())) + .test() + .assertFailure(TestException.class); + } + + @Test + public void justMaybeJust() { + Single.just(1) + .startWith(Maybe.just(0)) + .test() + .assertResult(0, 1); + } + + @Test + public void justMaybeEmpty() { + Single.just(1) + .startWith(Maybe.empty()) + .test() + .assertResult(1); + } + + @Test + public void justMaybeError() { + Single.just(1) + .startWith(Maybe.error(new TestException())) + .test() + .assertFailure(TestException.class); + } + + @Test + public void justObservableJust() { + Single.just(1) + .startWith(Observable.just(-1, 0)) + .test() + .assertResult(-1, 0, 1); + } + + @Test + public void justObservableEmpty() { + Single.just(1) + .startWith(Observable.empty()) + .test() + .assertResult(1); + } + + @Test + public void justObservableError() { + Single.just(1) + .startWith(Observable.error(new TestException())) + .test() + .assertFailure(TestException.class); + } + + @Test + public void justFlowableJust() { + Single.just(1) + .startWith(Flowable.just(-1, 0)) + .test() + .assertResult(-1, 0, 1); + } + + @Test + public void justFlowableEmpty() { + Single.just(1) + .startWith(Observable.empty()) + .test() + .assertResult(1); + } + + @Test + public void justFlowableError() { + Single.just(1) + .startWith(Flowable.error(new TestException())) + .test() + .assertFailure(TestException.class); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleSubscribeOnTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleSubscribeOnTest.java new file mode 100644 index 0000000000..bec9782b0e --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleSubscribeOnTest.java @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.single; + +import static org.junit.Assert.assertTrue; + +import java.util.List; +import java.util.concurrent.TimeUnit; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.schedulers.*; +import io.reactivex.rxjava3.subjects.PublishSubject; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class SingleSubscribeOnTest extends RxJavaTest { + + @Test + public void normal() { + List<Throwable> list = TestHelper.trackPluginErrors(); + try { + TestScheduler scheduler = new TestScheduler(); + + TestObserver<Integer> to = Single.just(1) + .subscribeOn(scheduler) + .test(); + + scheduler.advanceTimeBy(1, TimeUnit.SECONDS); + + to.assertResult(1); + + assertTrue(list.toString(), list.isEmpty()); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void dispose() { + TestHelper.checkDisposed(PublishSubject.create().singleOrError().subscribeOn(new TestScheduler())); + } + + @Test + public void error() { + Single.error(new TestException()) + .subscribeOn(Schedulers.single()) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertFailure(TestException.class); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleSwitchOnNextTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleSwitchOnNextTest.java new file mode 100644 index 0000000000..f3c119222d --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleSwitchOnNextTest.java @@ -0,0 +1,123 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.single; + +import static org.junit.Assert.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.processors.PublishProcessor; +import io.reactivex.rxjava3.subjects.SingleSubject; +import io.reactivex.rxjava3.subscribers.TestSubscriber; + +public class SingleSwitchOnNextTest extends RxJavaTest { + + @Test + public void normal() { + Single.switchOnNext( + Flowable.range(1, 5) + .map(v -> { + if (v % 2 == 0) { + return Single.just(v); + } + return Single.just(10 + v); + }) + ) + .test() + .assertResult(11, 2, 13, 4, 15); + } + + @Test + public void normalDelayError() { + Single.switchOnNextDelayError( + Flowable.range(1, 5) + .map(v -> { + if (v % 2 == 0) { + return Single.just(v); + } + return Single.just(10 + v); + }) + ) + .test() + .assertResult(11, 2, 13, 4, 15); + } + + @Test + public void noDelaySwitch() { + PublishProcessor<Single<Integer>> pp = PublishProcessor.create(); + + TestSubscriber<Integer> ts = Single.switchOnNext(pp).test(); + + assertTrue(pp.hasSubscribers()); + + ts.assertEmpty(); + + SingleSubject<Integer> ss1 = SingleSubject.create(); + SingleSubject<Integer> ss2 = SingleSubject.create(); + + pp.onNext(ss1); + + assertTrue(ss1.hasObservers()); + + pp.onNext(ss2); + + assertFalse(ss1.hasObservers()); + assertTrue(ss2.hasObservers()); + + pp.onComplete(); + + assertTrue(ss2.hasObservers()); + + ss2.onSuccess(1); + + ts.assertResult(1); + } + + @Test + public void delaySwitch() { + PublishProcessor<Single<Integer>> pp = PublishProcessor.create(); + + TestSubscriber<Integer> ts = Single.switchOnNextDelayError(pp).test(); + + assertTrue(pp.hasSubscribers()); + + ts.assertEmpty(); + + SingleSubject<Integer> ss1 = SingleSubject.create(); + SingleSubject<Integer> ss2 = SingleSubject.create(); + + pp.onNext(ss1); + + assertTrue(ss1.hasObservers()); + + pp.onNext(ss2); + + assertFalse(ss1.hasObservers()); + assertTrue(ss2.hasObservers()); + + assertTrue(ss2.hasObservers()); + + ss2.onError(new TestException()); + + assertTrue(pp.hasSubscribers()); + + ts.assertEmpty(); + + pp.onComplete(); + + ts.assertFailure(TestException.class); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleTakeUntilTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleTakeUntilTest.java new file mode 100644 index 0000000000..c5389b0bbd --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleTakeUntilTest.java @@ -0,0 +1,583 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.single; + +import static org.junit.Assert.*; + +import java.util.List; +import java.util.concurrent.CancellationException; + +import org.junit.Test; +import org.reactivestreams.Subscriber; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.internal.subscriptions.BooleanSubscription; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.processors.PublishProcessor; +import io.reactivex.rxjava3.subjects.*; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class SingleTakeUntilTest extends RxJavaTest { + + @Test + public void mainSuccessPublisher() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + PublishProcessor<Integer> source = PublishProcessor.create(); + + TestObserver<Integer> to = source.single(-99).takeUntil(pp) + .test(); + + source.onNext(1); + source.onComplete(); + + to.assertResult(1); + } + + @Test + public void mainSuccessSingle() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + PublishProcessor<Integer> source = PublishProcessor.create(); + + TestObserver<Integer> to = source.single(-99).takeUntil(pp.single(-99)) + .test(); + + source.onNext(1); + source.onComplete(); + + to.assertResult(1); + } + + @Test + public void mainSuccessCompletable() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + PublishProcessor<Integer> source = PublishProcessor.create(); + + TestObserver<Integer> to = source.single(-99).takeUntil(pp.ignoreElements()) + .test(); + + source.onNext(1); + source.onComplete(); + + to.assertResult(1); + } + + @Test + public void mainErrorPublisher() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + PublishProcessor<Integer> source = PublishProcessor.create(); + + TestObserver<Integer> to = source.single(-99).takeUntil(pp) + .test(); + + source.onError(new TestException()); + + to.assertFailure(TestException.class); + } + + @Test + public void mainErrorSingle() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + PublishProcessor<Integer> source = PublishProcessor.create(); + + TestObserver<Integer> to = source.single(-99).takeUntil(pp.single(-99)) + .test(); + + source.onError(new TestException()); + + to.assertFailure(TestException.class); + } + + @Test + public void mainErrorCompletable() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + PublishProcessor<Integer> source = PublishProcessor.create(); + + TestObserver<Integer> to = source.single(-99).takeUntil(pp.ignoreElements()) + .test(); + + source.onError(new TestException()); + + to.assertFailure(TestException.class); + } + + @Test + public void otherOnNextPublisher() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + PublishProcessor<Integer> source = PublishProcessor.create(); + + TestObserver<Integer> to = source.single(-99).takeUntil(pp) + .test(); + + pp.onNext(1); + + to.assertFailure(CancellationException.class); + } + + @Test + public void otherOnNextSingle() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + PublishProcessor<Integer> source = PublishProcessor.create(); + + TestObserver<Integer> to = source.single(-99).takeUntil(pp.single(-99)) + .test(); + + pp.onNext(1); + pp.onComplete(); + + to.assertFailure(CancellationException.class); + } + + @Test + public void otherOnNextCompletable() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + PublishProcessor<Integer> source = PublishProcessor.create(); + + TestObserver<Integer> to = source.single(-99).takeUntil(pp.ignoreElements()) + .test(); + + pp.onNext(1); + pp.onComplete(); + + to.assertFailure(CancellationException.class); + } + + @Test + public void otherOnCompletePublisher() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + PublishProcessor<Integer> source = PublishProcessor.create(); + + TestObserver<Integer> to = source.single(-99).takeUntil(pp) + .test(); + + pp.onComplete(); + + to.assertFailure(CancellationException.class); + } + + @Test + public void otherOnCompleteCompletable() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + PublishProcessor<Integer> source = PublishProcessor.create(); + + TestObserver<Integer> to = source.single(-99).takeUntil(pp.ignoreElements()) + .test(); + + pp.onComplete(); + + to.assertFailure(CancellationException.class); + } + + @Test + public void otherErrorPublisher() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + PublishProcessor<Integer> source = PublishProcessor.create(); + + TestObserver<Integer> to = source.single(-99).takeUntil(pp) + .test(); + + pp.onError(new TestException()); + + to.assertFailure(TestException.class); + } + + @Test + public void otherErrorSingle() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + PublishProcessor<Integer> source = PublishProcessor.create(); + + TestObserver<Integer> to = source.single(-99).takeUntil(pp.single(-99)) + .test(); + + pp.onError(new TestException()); + + to.assertFailure(TestException.class); + } + + @Test + public void otherErrorCompletable() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + PublishProcessor<Integer> source = PublishProcessor.create(); + + TestObserver<Integer> to = source.single(-99).takeUntil(pp.ignoreElements()) + .test(); + + pp.onError(new TestException()); + + to.assertFailure(TestException.class); + } + + @Test + public void withPublisherDispose() { + TestHelper.checkDisposed(Single.never().takeUntil(Flowable.never())); + } + + @Test + public void onErrorRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + List<Throwable> errors = TestHelper.trackPluginErrors(); + + try { + final PublishProcessor<Integer> pp1 = PublishProcessor.create(); + final PublishProcessor<Integer> pp2 = PublishProcessor.create(); + + TestObserver<Integer> to = pp1.singleOrError().takeUntil(pp2).test(); + + final TestException ex = new TestException(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + pp1.onError(ex); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + pp2.onError(ex); + } + }; + + TestHelper.race(r1, r2); + + to.assertFailure(TestException.class); + + if (!errors.isEmpty()) { + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } + } finally { + RxJavaPlugins.reset(); + } + } + } + + @Test + public void otherSignalsAndCompletes() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + Single.just(1).takeUntil(Flowable.just(1).take(1)) + .test() + .assertFailure(CancellationException.class); + + assertTrue(errors.toString(), errors.isEmpty()); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void flowableCancelDelayed() { + Single.never() + .takeUntil(new Flowable<Integer>() { + @Override + protected void subscribeActual(Subscriber<? super Integer> s) { + s.onSubscribe(new BooleanSubscription()); + s.onNext(1); + s.onNext(2); + } + }) + .test() + .assertFailure(CancellationException.class); + } + + @Test + public void untilSingleMainSuccess() { + SingleSubject<Integer> main = SingleSubject.create(); + SingleSubject<Integer> other = SingleSubject.create(); + + TestObserver<Integer> to = main.takeUntil(other).test(); + + assertTrue("Main no observers?", main.hasObservers()); + assertTrue("Other no observers?", other.hasObservers()); + + main.onSuccess(1); + + assertFalse("Main has observers?", main.hasObservers()); + assertFalse("Other has observers?", other.hasObservers()); + + to.assertResult(1); + } + + @Test + public void untilSingleMainError() { + SingleSubject<Integer> main = SingleSubject.create(); + SingleSubject<Integer> other = SingleSubject.create(); + + TestObserver<Integer> to = main.takeUntil(other).test(); + + assertTrue("Main no observers?", main.hasObservers()); + assertTrue("Other no observers?", other.hasObservers()); + + main.onError(new TestException()); + + assertFalse("Main has observers?", main.hasObservers()); + assertFalse("Other has observers?", other.hasObservers()); + + to.assertFailure(TestException.class); + } + + @Test + public void untilSingleOtherSuccess() { + SingleSubject<Integer> main = SingleSubject.create(); + SingleSubject<Integer> other = SingleSubject.create(); + + TestObserver<Integer> to = main.takeUntil(other).test(); + + assertTrue("Main no observers?", main.hasObservers()); + assertTrue("Other no observers?", other.hasObservers()); + + other.onSuccess(1); + + assertFalse("Main has observers?", main.hasObservers()); + assertFalse("Other has observers?", other.hasObservers()); + + to.assertFailure(CancellationException.class); + } + + @Test + public void untilSingleOtherError() { + SingleSubject<Integer> main = SingleSubject.create(); + SingleSubject<Integer> other = SingleSubject.create(); + + TestObserver<Integer> to = main.takeUntil(other).test(); + + assertTrue("Main no observers?", main.hasObservers()); + assertTrue("Other no observers?", other.hasObservers()); + + other.onError(new TestException()); + + assertFalse("Main has observers?", main.hasObservers()); + assertFalse("Other has observers?", other.hasObservers()); + + to.assertFailure(TestException.class); + } + + @Test + public void untilSingleDispose() { + SingleSubject<Integer> main = SingleSubject.create(); + SingleSubject<Integer> other = SingleSubject.create(); + + TestObserver<Integer> to = main.takeUntil(other).test(); + + assertTrue("Main no observers?", main.hasObservers()); + assertTrue("Other no observers?", other.hasObservers()); + + to.dispose(); + + assertFalse("Main has observers?", main.hasObservers()); + assertFalse("Other has observers?", other.hasObservers()); + + to.assertEmpty(); + } + + @Test + public void untilPublisherMainSuccess() { + SingleSubject<Integer> main = SingleSubject.create(); + PublishProcessor<Integer> other = PublishProcessor.create(); + + TestObserver<Integer> to = main.takeUntil(other).test(); + + assertTrue("Main no observers?", main.hasObservers()); + assertTrue("Other no observers?", other.hasSubscribers()); + + main.onSuccess(1); + + assertFalse("Main has observers?", main.hasObservers()); + assertFalse("Other has observers?", other.hasSubscribers()); + + to.assertResult(1); + } + + @Test + public void untilPublisherMainError() { + SingleSubject<Integer> main = SingleSubject.create(); + PublishProcessor<Integer> other = PublishProcessor.create(); + + TestObserver<Integer> to = main.takeUntil(other).test(); + + assertTrue("Main no observers?", main.hasObservers()); + assertTrue("Other no observers?", other.hasSubscribers()); + + main.onError(new TestException()); + + assertFalse("Main has observers?", main.hasObservers()); + assertFalse("Other has observers?", other.hasSubscribers()); + + to.assertFailure(TestException.class); + } + + @Test + public void untilPublisherOtherOnNext() { + SingleSubject<Integer> main = SingleSubject.create(); + PublishProcessor<Integer> other = PublishProcessor.create(); + + TestObserver<Integer> to = main.takeUntil(other).test(); + + assertTrue("Main no observers?", main.hasObservers()); + assertTrue("Other no observers?", other.hasSubscribers()); + + other.onNext(1); + + assertFalse("Main has observers?", main.hasObservers()); + assertFalse("Other has observers?", other.hasSubscribers()); + + to.assertFailure(CancellationException.class); + } + + @Test + public void untilPublisherOtherOnComplete() { + SingleSubject<Integer> main = SingleSubject.create(); + PublishProcessor<Integer> other = PublishProcessor.create(); + + TestObserver<Integer> to = main.takeUntil(other).test(); + + assertTrue("Main no observers?", main.hasObservers()); + assertTrue("Other no observers?", other.hasSubscribers()); + + other.onComplete(); + + assertFalse("Main has observers?", main.hasObservers()); + assertFalse("Other has observers?", other.hasSubscribers()); + + to.assertFailure(CancellationException.class); + } + + @Test + public void untilPublisherOtherError() { + SingleSubject<Integer> main = SingleSubject.create(); + PublishProcessor<Integer> other = PublishProcessor.create(); + + TestObserver<Integer> to = main.takeUntil(other).test(); + + assertTrue("Main no observers?", main.hasObservers()); + assertTrue("Other no observers?", other.hasSubscribers()); + + other.onError(new TestException()); + + assertFalse("Main has observers?", main.hasObservers()); + assertFalse("Other has observers?", other.hasSubscribers()); + + to.assertFailure(TestException.class); + } + + @Test + public void untilPublisherDispose() { + SingleSubject<Integer> main = SingleSubject.create(); + PublishProcessor<Integer> other = PublishProcessor.create(); + + TestObserver<Integer> to = main.takeUntil(other).test(); + + assertTrue("Main no observers?", main.hasObservers()); + assertTrue("Other no observers?", other.hasSubscribers()); + + to.dispose(); + + assertFalse("Main has observers?", main.hasObservers()); + assertFalse("Other has observers?", other.hasSubscribers()); + + to.assertEmpty(); + } + + @Test + public void untilCompletableMainSuccess() { + SingleSubject<Integer> main = SingleSubject.create(); + CompletableSubject other = CompletableSubject.create(); + + TestObserver<Integer> to = main.takeUntil(other).test(); + + assertTrue("Main no observers?", main.hasObservers()); + assertTrue("Other no observers?", other.hasObservers()); + + main.onSuccess(1); + + assertFalse("Main has observers?", main.hasObservers()); + assertFalse("Other has observers?", other.hasObservers()); + + to.assertResult(1); + } + + @Test + public void untilCompletableMainError() { + SingleSubject<Integer> main = SingleSubject.create(); + CompletableSubject other = CompletableSubject.create(); + + TestObserver<Integer> to = main.takeUntil(other).test(); + + assertTrue("Main no observers?", main.hasObservers()); + assertTrue("Other no observers?", other.hasObservers()); + + main.onError(new TestException()); + + assertFalse("Main has observers?", main.hasObservers()); + assertFalse("Other has observers?", other.hasObservers()); + + to.assertFailure(TestException.class); + } + + @Test + public void untilCompletableOtherOnComplete() { + SingleSubject<Integer> main = SingleSubject.create(); + CompletableSubject other = CompletableSubject.create(); + + TestObserver<Integer> to = main.takeUntil(other).test(); + + assertTrue("Main no observers?", main.hasObservers()); + assertTrue("Other no observers?", other.hasObservers()); + + other.onComplete(); + + assertFalse("Main has observers?", main.hasObservers()); + assertFalse("Other has observers?", other.hasObservers()); + + to.assertFailure(CancellationException.class); + } + + @Test + public void untilCompletableOtherError() { + SingleSubject<Integer> main = SingleSubject.create(); + CompletableSubject other = CompletableSubject.create(); + + TestObserver<Integer> to = main.takeUntil(other).test(); + + assertTrue("Main no observers?", main.hasObservers()); + assertTrue("Other no observers?", other.hasObservers()); + + other.onError(new TestException()); + + assertFalse("Main has observers?", main.hasObservers()); + assertFalse("Other has observers?", other.hasObservers()); + + to.assertFailure(TestException.class); + } + + @Test + public void untilCompletableDispose() { + SingleSubject<Integer> main = SingleSubject.create(); + CompletableSubject other = CompletableSubject.create(); + + TestObserver<Integer> to = main.takeUntil(other).test(); + + assertTrue("Main no observers?", main.hasObservers()); + assertTrue("Other no observers?", other.hasObservers()); + + to.dispose(); + + assertFalse("Main has observers?", main.hasObservers()); + assertFalse("Other has observers?", other.hasObservers()); + + to.assertEmpty(); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleTimeIntervalTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleTimeIntervalTest.java new file mode 100644 index 0000000000..ade198db50 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleTimeIntervalTest.java @@ -0,0 +1,103 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.single; + +import java.util.concurrent.TimeUnit; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.Single; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.schedulers.*; +import io.reactivex.rxjava3.subjects.SingleSubject; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class SingleTimeIntervalTest { + + @Test + public void just() { + Single.just(1) + .timestamp() + .test() + .assertValueCount(1) + .assertNoErrors() + .assertComplete(); + } + + @Test + public void error() { + Single.error(new TestException()) + .timestamp() + .test() + .assertFailure(TestException.class); + } + + @Test + public void justSeconds() { + Single.just(1) + .timestamp(TimeUnit.SECONDS) + .test() + .assertValueCount(1) + .assertNoErrors() + .assertComplete(); + } + + @Test + public void justScheduler() { + Single.just(1) + .timestamp(Schedulers.single()) + .test() + .assertValueCount(1) + .assertNoErrors() + .assertComplete(); + } + + @Test + public void justSecondsScheduler() { + Single.just(1) + .timestamp(TimeUnit.SECONDS, Schedulers.single()) + .test() + .assertValueCount(1) + .assertNoErrors() + .assertComplete(); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeSingle(m -> m.timestamp()); + } + + @Test + public void dispose() { + TestHelper.checkDisposed(SingleSubject.create().timestamp()); + } + + @Test + public void timeInfo() { + TestScheduler scheduler = new TestScheduler(); + + SingleSubject<Integer> ss = SingleSubject.create(); + + TestObserver<Timed<Integer>> to = ss + .timestamp(scheduler) + .test(); + + scheduler.advanceTimeBy(1000, TimeUnit.MILLISECONDS); + + ss.onSuccess(1); + + to.assertResult(new Timed<>(1, 1000L, TimeUnit.MILLISECONDS)); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleTimeoutTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleTimeoutTest.java new file mode 100644 index 0000000000..e54fc11207 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleTimeoutTest.java @@ -0,0 +1,278 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.single; + +import static io.reactivex.rxjava3.internal.util.ExceptionHelper.timeoutMessage; +import static org.junit.Assert.*; + +import java.util.List; +import java.util.concurrent.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.annotations.NonNull; +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.Action; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.schedulers.*; +import io.reactivex.rxjava3.subjects.*; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class SingleTimeoutTest extends RxJavaTest { + + @Test + public void shouldUnsubscribeFromUnderlyingSubscriptionOnDispose() { + final PublishSubject<String> subject = PublishSubject.create(); + final TestScheduler scheduler = new TestScheduler(); + + final TestObserver<String> observer = subject.single("") + .timeout(100, TimeUnit.MILLISECONDS, scheduler) + .test(); + + assertTrue(subject.hasObservers()); + + observer.dispose(); + + assertFalse(subject.hasObservers()); + } + + @Test + public void otherErrors() { + Single.never() + .timeout(1, TimeUnit.MILLISECONDS, Single.error(new TestException())) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertFailure(TestException.class); + } + + @Test + public void mainSuccess() { + Single.just(1) + .timeout(1, TimeUnit.DAYS) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(1); + } + + @Test + public void mainError() { + Single.error(new TestException()) + .timeout(1, TimeUnit.DAYS) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertFailure(TestException.class); + } + + @Test + public void disposeWhenFallback() { + TestScheduler sch = new TestScheduler(); + + SingleSubject<Integer> subj = SingleSubject.create(); + + subj.timeout(1, TimeUnit.SECONDS, sch, Single.just(1)) + .test(true) + .assertEmpty(); + + assertFalse(subj.hasObservers()); + } + + @Test + public void isDisposed() { + TestHelper.checkDisposed(SingleSubject.create().timeout(1, TimeUnit.DAYS)); + } + + @Test + public void fallbackDispose() { + TestScheduler sch = new TestScheduler(); + + SingleSubject<Integer> subj = SingleSubject.create(); + + SingleSubject<Integer> fallback = SingleSubject.create(); + + TestObserver<Integer> to = subj.timeout(1, TimeUnit.SECONDS, sch, fallback) + .test(); + + assertFalse(fallback.hasObservers()); + + sch.advanceTimeBy(1, TimeUnit.SECONDS); + + assertFalse(subj.hasObservers()); + assertTrue(fallback.hasObservers()); + + to.dispose(); + + assertFalse(fallback.hasObservers()); + } + + @Test + public void normalSuccessDoesntDisposeMain() { + final int[] calls = { 0 }; + + Single.just(1) + .doOnDispose(new Action() { + @Override + public void run() throws Exception { + calls[0]++; + } + }) + .timeout(1, TimeUnit.DAYS) + .test() + .assertResult(1); + + assertEquals(0, calls[0]); + } + + @Test + public void successTimeoutRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final SingleSubject<Integer> subj = SingleSubject.create(); + SingleSubject<Integer> fallback = SingleSubject.create(); + + final TestScheduler sch = new TestScheduler(); + + TestObserver<Integer> to = subj.timeout(1, TimeUnit.MILLISECONDS, sch, fallback).test(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + subj.onSuccess(1); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + sch.advanceTimeBy(1, TimeUnit.MILLISECONDS); + } + }; + + TestHelper.race(r1, r2); + + if (!fallback.hasObservers()) { + to.assertResult(1); + } else { + to.assertEmpty(); + } + } + } + + @Test + public void errorTimeoutRace() { + final TestException ex = new TestException(); + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final SingleSubject<Integer> subj = SingleSubject.create(); + SingleSubject<Integer> fallback = SingleSubject.create(); + + final TestScheduler sch = new TestScheduler(); + + TestObserver<Integer> to = subj.timeout(1, TimeUnit.MILLISECONDS, sch, fallback).test(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + subj.onError(ex); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + sch.advanceTimeBy(1, TimeUnit.MILLISECONDS); + } + }; + + TestHelper.race(r1, r2); + + if (!fallback.hasObservers()) { + to.assertFailure(TestException.class); + } else { + to.assertEmpty(); + } + if (!errors.isEmpty()) { + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } + } + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void mainTimedOut() { + Single + .never() + .timeout(1, TimeUnit.MILLISECONDS) + .to(TestHelper.<Object>testConsumer()) + .awaitDone(5, TimeUnit.SECONDS) + .assertFailureAndMessage(TimeoutException.class, timeoutMessage(1, TimeUnit.MILLISECONDS)); + } + + @Test + public void mainTimeoutFallbackSuccess() { + Single.never() + .timeout(1, TimeUnit.MILLISECONDS, Single.just(1)) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(1); + } + + @Test + public void timeoutBeforeOnSubscribeFromMain() { + Disposable d = Disposable.empty(); + + new Single<Integer>() { + @Override + protected void subscribeActual(@NonNull SingleObserver<? super @NonNull Integer> observer) { + try { + Thread.sleep(100); + } catch (InterruptedException e) { + e.printStackTrace(); + } + observer.onSubscribe(d); + } + } + .timeout(1, TimeUnit.MILLISECONDS, Single.just(1)) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(1); + + assertTrue(d.isDisposed()); + } + + @Test + public void timeoutWithZero() throws InterruptedException { + int n = 10_000; + Scheduler sch = Schedulers.single(); + for (int i = 0; i < n; i++) { + final int y = i; + final CountDownLatch latch = new CountDownLatch(1); + Disposable d = Single.never() + .timeout(0, TimeUnit.NANOSECONDS, sch) + .subscribe(v -> {}, e -> { + //System.out.println("timeout " + y); + latch.countDown(); + }); + if (!latch.await(2, TimeUnit.SECONDS)) { + System.out.println(d + " " + sch); + throw new IllegalStateException("Timeout did not work at y = " + y); + } + } + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleTimerTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleTimerTest.java new file mode 100644 index 0000000000..3b5797f498 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleTimerTest.java @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.single; + +import static org.junit.Assert.assertTrue; + +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.schedulers.*; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class SingleTimerTest extends RxJavaTest { + + @Test + public void disposed() { + TestHelper.checkDisposed(Single.timer(1, TimeUnit.SECONDS, new TestScheduler())); + } + + @Test + public void timerInterruptible() throws Exception { + ScheduledExecutorService exec = Executors.newSingleThreadScheduledExecutor(); + try { + for (Scheduler s : new Scheduler[] { Schedulers.single(), Schedulers.computation(), Schedulers.newThread(), Schedulers.io(), Schedulers.from(exec, true) }) { + final AtomicBoolean interrupted = new AtomicBoolean(); + TestObserver<Long> to = Single.timer(1, TimeUnit.MILLISECONDS, s) + .map(new Function<Long, Long>() { + @Override + public Long apply(Long v) throws Exception { + try { + Thread.sleep(3000); + } catch (InterruptedException ex) { + interrupted.set(true); + } + return v; + } + }) + .test(); + + Thread.sleep(500); + + to.dispose(); + + Thread.sleep(500); + + assertTrue(s.getClass().getSimpleName(), interrupted.get()); + } + } finally { + exec.shutdown(); + } + } + +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleTimestampTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleTimestampTest.java new file mode 100644 index 0000000000..206da8f428 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleTimestampTest.java @@ -0,0 +1,103 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.single; + +import java.util.concurrent.TimeUnit; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.Single; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.schedulers.*; +import io.reactivex.rxjava3.subjects.SingleSubject; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class SingleTimestampTest { + + @Test + public void just() { + Single.just(1) + .timeInterval() + .test() + .assertValueCount(1) + .assertNoErrors() + .assertComplete(); + } + + @Test + public void error() { + Single.error(new TestException()) + .timeInterval() + .test() + .assertFailure(TestException.class); + } + + @Test + public void justSeconds() { + Single.just(1) + .timeInterval(TimeUnit.SECONDS) + .test() + .assertValueCount(1) + .assertNoErrors() + .assertComplete(); + } + + @Test + public void justScheduler() { + Single.just(1) + .timeInterval(Schedulers.single()) + .test() + .assertValueCount(1) + .assertNoErrors() + .assertComplete(); + } + + @Test + public void justSecondsScheduler() { + Single.just(1) + .timeInterval(TimeUnit.SECONDS, Schedulers.single()) + .test() + .assertValueCount(1) + .assertNoErrors() + .assertComplete(); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeSingle(m -> m.timeInterval()); + } + + @Test + public void dispose() { + TestHelper.checkDisposed(SingleSubject.create().timeInterval()); + } + + @Test + public void timeInfo() { + TestScheduler scheduler = new TestScheduler(); + + SingleSubject<Integer> ss = SingleSubject.create(); + + TestObserver<Timed<Integer>> to = ss + .timeInterval(scheduler) + .test(); + + scheduler.advanceTimeBy(1000, TimeUnit.MILLISECONDS); + + ss.onSuccess(1); + + to.assertResult(new Timed<>(1, 1000L, TimeUnit.MILLISECONDS)); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleToFlowableTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleToFlowableTest.java new file mode 100644 index 0000000000..1c9cf06385 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleToFlowableTest.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.single; + +import org.junit.Test; +import org.reactivestreams.Publisher; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.subjects.PublishSubject; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class SingleToFlowableTest extends RxJavaTest { + + @Test + public void dispose() { + TestHelper.checkDisposed(PublishSubject.create().singleOrError().toFlowable()); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeSingleToFlowable(new Function<Single<Object>, Publisher<Object>>() { + @Override + public Publisher<Object> apply(Single<Object> s) throws Exception { + return s.toFlowable(); + } + }); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleToObservableTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleToObservableTest.java new file mode 100644 index 0000000000..f72bd4ac60 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleToObservableTest.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.single; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.subjects.PublishSubject; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class SingleToObservableTest extends RxJavaTest { + + @Test + public void dispose() { + TestHelper.checkDisposed(PublishSubject.create().singleOrError().toObservable()); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeSingleToObservable(new Function<Single<Object>, ObservableSource<Object>>() { + @Override + public ObservableSource<Object> apply(Single<Object> s) throws Exception { + return s.toObservable(); + } + }); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleUnsubscribeOnTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleUnsubscribeOnTest.java new file mode 100644 index 0000000000..fa07de729c --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleUnsubscribeOnTest.java @@ -0,0 +1,131 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.single; + +import static org.junit.Assert.*; + +import java.util.concurrent.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.processors.PublishProcessor; +import io.reactivex.rxjava3.schedulers.Schedulers; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class SingleUnsubscribeOnTest extends RxJavaTest { + + @Test + public void normal() throws Exception { + PublishProcessor<Integer> pp = PublishProcessor.create(); + + final String[] name = { null }; + + final CountDownLatch cdl = new CountDownLatch(1); + + pp.doOnCancel(new Action() { + @Override + public void run() throws Exception { + name[0] = Thread.currentThread().getName(); + cdl.countDown(); + } + }) + .single(-99) + .unsubscribeOn(Schedulers.single()) + .test(true) + ; + + assertTrue(cdl.await(5, TimeUnit.SECONDS)); + + int times = 10; + + while (times-- > 0 && pp.hasSubscribers()) { + Thread.sleep(100); + } + + assertFalse(pp.hasSubscribers()); + + assertNotEquals(Thread.currentThread().getName(), name[0]); + } + + @Test + public void just() { + Single.just(1) + .unsubscribeOn(Schedulers.single()) + .test() + .assertResult(1); + } + + @Test + public void error() { + Single.<Integer>error(new TestException()) + .unsubscribeOn(Schedulers.single()) + .test() + .assertFailure(TestException.class); + } + + @Test + public void dispose() { + TestHelper.checkDisposed(Single.just(1) + .unsubscribeOn(Schedulers.single())); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeSingle(new Function<Single<Object>, SingleSource<Object>>() { + @Override + public SingleSource<Object> apply(Single<Object> v) throws Exception { + return v.unsubscribeOn(Schedulers.single()); + } + }); + } + + @Test + public void disposeRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + PublishProcessor<Integer> pp = PublishProcessor.create(); + + final Disposable[] ds = { null }; + pp.single(-99).unsubscribeOn(Schedulers.computation()) + .subscribe(new SingleObserver<Integer>() { + @Override + public void onSubscribe(Disposable d) { + ds[0] = d; + } + + @Override + public void onSuccess(Integer value) { + + } + + @Override + public void onError(Throwable e) { + + } + }); + + Runnable r = new Runnable() { + @Override + public void run() { + ds[0].dispose(); + } + }; + + TestHelper.race(r, r); + } + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleUsingTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleUsingTest.java new file mode 100644 index 0000000000..28c6b34ef7 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleUsingTest.java @@ -0,0 +1,395 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.single; + +import static org.junit.Assert.*; + +import java.util.List; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.*; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.processors.PublishProcessor; +import io.reactivex.rxjava3.testsupport.*; + +public class SingleUsingTest extends RxJavaTest { + + Function<Disposable, Single<Integer>> mapper = new Function<Disposable, Single<Integer>>() { + @Override + public Single<Integer> apply(Disposable d) throws Exception { + return Single.just(1); + } + }; + + Function<Disposable, Single<Integer>> mapperThrows = new Function<Disposable, Single<Integer>>() { + @Override + public Single<Integer> apply(Disposable d) throws Exception { + throw new TestException("Mapper"); + } + }; + + Consumer<Disposable> disposer = new Consumer<Disposable>() { + @Override + public void accept(Disposable d) throws Exception { + d.dispose(); + } + }; + + Consumer<Disposable> disposerThrows = new Consumer<Disposable>() { + @Override + public void accept(Disposable d) throws Exception { + throw new TestException("Disposer"); + } + }; + + @Test + public void resourceSupplierThrows() { + Single.using(new Supplier<Integer>() { + @Override + public Integer get() throws Exception { + throw new TestException(); + } + }, Functions.justFunction(Single.just(1)), Functions.emptyConsumer()) + .test() + .assertFailure(TestException.class); + } + + @Test + public void normalEager() { + Single.using(Functions.justSupplier(1), Functions.justFunction(Single.just(1)), Functions.emptyConsumer()) + .test() + .assertResult(1); + } + + @Test + public void normalNonEager() { + Single.using(Functions.justSupplier(1), Functions.justFunction(Single.just(1)), Functions.emptyConsumer(), false) + .test() + .assertResult(1); + } + + @Test + public void errorEager() { + Single.using(Functions.justSupplier(1), Functions.justFunction(Single.error(new TestException())), Functions.emptyConsumer()) + .test() + .assertFailure(TestException.class); + } + + @Test + public void errorNonEager() { + Single.using(Functions.justSupplier(1), Functions.justFunction(Single.error(new TestException())), Functions.emptyConsumer(), false) + .test() + .assertFailure(TestException.class); + } + + @Test + public void eagerMapperThrowsDisposerThrows() { + TestObserverEx<Integer> to = Single.using(Functions.justSupplier(Disposable.empty()), mapperThrows, disposerThrows) + .to(TestHelper.<Integer>testConsumer()) + .assertFailure(CompositeException.class); + + List<Throwable> ce = TestHelper.compositeList(to.errors().get(0)); + TestHelper.assertError(ce, 0, TestException.class, "Mapper"); + TestHelper.assertError(ce, 1, TestException.class, "Disposer"); + } + + @Test + public void noneagerMapperThrowsDisposerThrows() { + + List<Throwable> errors = TestHelper.trackPluginErrors(); + + try { + Single.using(Functions.justSupplier(Disposable.empty()), mapperThrows, disposerThrows, false) + .to(TestHelper.<Integer>testConsumer()) + .assertFailureAndMessage(TestException.class, "Mapper"); + + TestHelper.assertUndeliverable(errors, 0, TestException.class, "Disposer"); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void resourceDisposedIfMapperCrashes() { + Disposable d = Disposable.empty(); + + Single.using(Functions.justSupplier(d), mapperThrows, disposer) + .test() + .assertFailure(TestException.class); + + assertTrue(d.isDisposed()); + } + + @Test + public void resourceDisposedIfMapperCrashesNonEager() { + Disposable d = Disposable.empty(); + + Single.using(Functions.justSupplier(d), mapperThrows, disposer, false) + .test() + .assertFailure(TestException.class); + + assertTrue(d.isDisposed()); + } + + @Test + public void dispose() { + Disposable d = Disposable.empty(); + + Single.using(Functions.justSupplier(d), mapper, disposer, false) + .test(true); + + assertTrue(d.isDisposed()); + } + + @Test + public void disposerThrowsEager() { + Single.using(Functions.justSupplier(Disposable.empty()), mapper, disposerThrows) + .test() + .assertFailure(TestException.class); + } + + @Test + public void disposerThrowsNonEager() { + + List<Throwable> errors = TestHelper.trackPluginErrors(); + + try { + Single.using(Functions.justSupplier(Disposable.empty()), mapper, disposerThrows, false) + .test() + .assertResult(1); + TestHelper.assertUndeliverable(errors, 0, TestException.class, "Disposer"); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void errorAndDisposerThrowsEager() { + TestObserverEx<Integer> to = Single.using(Functions.justSupplier(Disposable.empty()), + new Function<Disposable, SingleSource<Integer>>() { + @Override + public SingleSource<Integer> apply(Disposable v) throws Exception { + return Single.<Integer>error(new TestException("Mapper-run")); + } + }, disposerThrows) + .to(TestHelper.<Integer>testConsumer()) + .assertFailure(CompositeException.class); + + List<Throwable> ce = TestHelper.compositeList(to.errors().get(0)); + TestHelper.assertError(ce, 0, TestException.class, "Mapper-run"); + TestHelper.assertError(ce, 1, TestException.class, "Disposer"); + } + + @Test + public void errorAndDisposerThrowsNonEager() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + + try { + Single.using(Functions.justSupplier(Disposable.empty()), + new Function<Disposable, SingleSource<Integer>>() { + @Override + public SingleSource<Integer> apply(Disposable v) throws Exception { + return Single.<Integer>error(new TestException("Mapper-run")); + } + }, disposerThrows, false) + .test() + .assertFailure(TestException.class); + TestHelper.assertUndeliverable(errors, 0, TestException.class, "Disposer"); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void successDisposeRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final PublishProcessor<Integer> pp = PublishProcessor.create(); + + Disposable d = Disposable.empty(); + + final TestObserver<Integer> to = Single.using(Functions.justSupplier(d), new Function<Disposable, SingleSource<Integer>>() { + @Override + public SingleSource<Integer> apply(Disposable v) throws Exception { + return pp.single(-99); + } + }, disposer) + .test(); + + pp.onNext(1); + + Runnable r1 = new Runnable() { + @Override + public void run() { + pp.onComplete(); + } + }; + Runnable r2 = new Runnable() { + @Override + public void run() { + to.dispose(); + } + }; + + TestHelper.race(r1, r2); + + assertTrue(d.isDisposed()); + } + } + + @Test + public void doubleOnSubscribe() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + + try { + Single.using(Functions.justSupplier(1), new Function<Integer, SingleSource<Integer>>() { + @Override + public SingleSource<Integer> apply(Integer v) throws Exception { + return new Single<Integer>() { + @Override + protected void subscribeActual(SingleObserver<? super Integer> observer) { + observer.onSubscribe(Disposable.empty()); + + assertFalse(((Disposable)observer).isDisposed()); + + Disposable d = Disposable.empty(); + observer.onSubscribe(d); + + assertTrue(d.isDisposed()); + + assertFalse(((Disposable)observer).isDisposed()); + + observer.onSuccess(1); + + assertTrue(((Disposable)observer).isDisposed()); + } + }; + } + }, Functions.emptyConsumer()) + .test() + .assertResult(1) + ; + + TestHelper.assertError(errors, 0, IllegalStateException.class, "Disposable already set!"); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + @SuppressUndeliverable + public void errorDisposeRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final PublishProcessor<Integer> pp = PublishProcessor.create(); + + Disposable d = Disposable.empty(); + + final TestObserver<Integer> to = Single.using(Functions.justSupplier(d), new Function<Disposable, SingleSource<Integer>>() { + @Override + public SingleSource<Integer> apply(Disposable v) throws Exception { + return pp.single(-99); + } + }, disposer) + .test(); + + final TestException ex = new TestException(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + pp.onError(ex); + } + }; + Runnable r2 = new Runnable() { + @Override + public void run() { + to.dispose(); + } + }; + + TestHelper.race(r1, r2); + + assertTrue(d.isDisposed()); + } + } + + @Test + public void eagerDisposeResourceThenDisposeUpstream() { + final StringBuilder sb = new StringBuilder(); + + TestObserver<Integer> to = Single.using(Functions.justSupplier(1), + new Function<Integer, Single<Integer>>() { + @Override + public Single<Integer> apply(Integer t) throws Throwable { + return Single.<Integer>never() + .doOnDispose(new Action() { + @Override + public void run() throws Throwable { + sb.append("Dispose"); + } + }) + ; + } + }, new Consumer<Integer>() { + @Override + public void accept(Integer t) throws Throwable { + sb.append("Resource"); + } + }, true) + .test() + ; + to.assertEmpty(); + + to.dispose(); + + assertEquals("ResourceDispose", sb.toString()); + } + + @Test + public void nonEagerDisposeUpstreamThenDisposeResource() { + final StringBuilder sb = new StringBuilder(); + + TestObserver<Integer> to = Single.using(Functions.justSupplier(1), + new Function<Integer, Single<Integer>>() { + @Override + public Single<Integer> apply(Integer t) throws Throwable { + return Single.<Integer>never() + .doOnDispose(new Action() { + @Override + public void run() throws Throwable { + sb.append("Dispose"); + } + }) + ; + } + }, new Consumer<Integer>() { + @Override + public void accept(Integer t) throws Throwable { + sb.append("Resource"); + } + }, false) + .test() + ; + to.assertEmpty(); + + to.dispose(); + + assertEquals("DisposeResource", sb.toString()); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleZipArrayTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleZipArrayTest.java new file mode 100644 index 0000000000..df41235e38 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleZipArrayTest.java @@ -0,0 +1,235 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.single; + +import static org.junit.Assert.*; + +import java.util.*; +import java.util.concurrent.atomic.AtomicReference; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.processors.PublishProcessor; +import io.reactivex.rxjava3.subjects.SingleSubject; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class SingleZipArrayTest extends RxJavaTest { + + final BiFunction<Object, Object, Object> addString = new BiFunction<Object, Object, Object>() { + @Override + public Object apply(Object a, Object b) throws Exception { + return "" + a + b; + } + }; + + final Function3<Object, Object, Object, Object> addString3 = new Function3<Object, Object, Object, Object>() { + @Override + public Object apply(Object a, Object b, Object c) throws Exception { + return "" + a + b + c; + } + }; + + @Test + public void firstError() { + Single.zip(Single.error(new TestException()), Single.just(1), addString) + .test() + .assertFailure(TestException.class); + } + + @Test + public void secondError() { + Single.zip(Single.just(1), Single.<Integer>error(new TestException()), addString) + .test() + .assertFailure(TestException.class); + } + + @Test + public void dispose() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + + TestObserver<Object> to = Single.zip(pp.single(0), pp.single(0), addString) + .test(); + + assertTrue(pp.hasSubscribers()); + + to.dispose(); + + assertFalse(pp.hasSubscribers()); + } + + @Test + public void zipperThrows() { + Single.zip(Single.just(1), Single.just(2), new BiFunction<Integer, Integer, Object>() { + @Override + public Object apply(Integer a, Integer b) throws Exception { + throw new TestException(); + } + }) + .test() + .assertFailure(TestException.class); + } + + @Test + public void zipperReturnsNull() { + Single.zip(Single.just(1), Single.just(2), new BiFunction<Integer, Integer, Object>() { + @Override + public Object apply(Integer a, Integer b) throws Exception { + return null; + } + }) + .test() + .assertFailure(NullPointerException.class); + } + + @Test + public void middleError() { + PublishProcessor<Integer> pp0 = PublishProcessor.create(); + PublishProcessor<Integer> pp1 = PublishProcessor.create(); + + TestObserver<Object> to = Single.zip(pp0.single(0), pp1.single(0), pp0.single(0), addString3) + .test(); + + pp1.onError(new TestException()); + + assertFalse(pp0.hasSubscribers()); + + to.assertFailure(TestException.class); + } + + @Test + public void innerErrorRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + final PublishProcessor<Integer> pp0 = PublishProcessor.create(); + final PublishProcessor<Integer> pp1 = PublishProcessor.create(); + + final TestObserver<Object> to = Single.zip(pp0.single(0), pp1.single(0), addString) + .test(); + + final TestException ex = new TestException(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + pp0.onError(ex); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + pp1.onError(ex); + } + }; + + TestHelper.race(r1, r2); + + to.assertFailure(TestException.class); + + if (!errors.isEmpty()) { + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } + } finally { + RxJavaPlugins.reset(); + } + } + } + + @Test(expected = NullPointerException.class) + public void zipArrayOneIsNull() { + Single.zipArray(new Function<Object[], Object>() { + @Override + public Object apply(Object[] v) { + return 1; + } + }, Single.just(1), null) + .blockingGet(); + } + + @SuppressWarnings("unchecked") + @Test + public void emptyArray() { + Single.zipArray(new Function<Object[], Object[]>() { + @Override + public Object[] apply(Object[] a) throws Exception { + return a; + } + }, new SingleSource[0]) + .test() + .assertFailure(NoSuchElementException.class); + } + + @Test + public void oneArray() { + Single.zipArray(new Function<Object[], Object>() { + @Override + public Object apply(Object[] a) throws Exception { + return (Integer)a[0] + 1; + } + }, Single.just(1)) + .test() + .assertResult(2); + } + + @Test + public void singleSourceZipperReturnsNull() { + Single.zipArray(Functions.justFunction(null), Single.just(1)) + .to(TestHelper.<Object>testConsumer()) + .assertFailureAndMessage(NullPointerException.class, "The zipper returned a null value"); + } + + @Test + public void singleSourceZipperReturnsNull2() { + Single.zipArray(Functions.justFunction(null), Single.just(1), Single.just(2)) + .to(TestHelper.<Object>testConsumer()) + .assertFailureAndMessage(NullPointerException.class, "The zipper returned a null value"); + } + + @Test + public void dispose2() { + TestHelper.checkDisposed(Single.zipArray(Functions.justFunction(1), SingleSubject.create(), SingleSubject.create())); + } + + @Test + public void bothSucceed() { + Single.zipArray(a -> Arrays.asList(a), Single.just(1), Single.just(2)) + .test() + .assertResult(Arrays.asList(1, 2)); + } + + @Test + public void onSuccessAfterDispose() { + AtomicReference<SingleObserver<? super Integer>> emitter = new AtomicReference<>(); + + TestObserver<List<Object>> to = Single.zipArray(Arrays::asList, + (SingleSource<Integer>)o -> emitter.set(o), Single.<Integer>never()) + .test(); + + emitter.get().onSubscribe(Disposable.empty()); + + to.dispose(); + + emitter.get().onSuccess(1); + + to.assertEmpty(); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleZipIterableTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleZipIterableTest.java new file mode 100644 index 0000000000..c2293a2b89 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleZipIterableTest.java @@ -0,0 +1,257 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.single; + +import static org.junit.Assert.*; + +import java.util.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.internal.util.CrashingMappedIterable; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.processors.PublishProcessor; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class SingleZipIterableTest extends RxJavaTest { + + final Function<Object[], Object> addString = new Function<Object[], Object>() { + @Override + public Object apply(Object[] a) throws Exception { + return Arrays.toString(a); + } + }; + + @Test + public void firstError() { + Single.zip(Arrays.asList(Single.error(new TestException()), Single.just(1)), addString) + .test() + .assertFailure(TestException.class); + } + + @Test + public void secondError() { + Single.zip(Arrays.asList(Single.just(1), Single.<Integer>error(new TestException())), addString) + .test() + .assertFailure(TestException.class); + } + + @Test + public void dispose() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + + TestObserver<Object> to = Single.zip(Arrays.asList(pp.single(0), pp.single(0)), addString) + .test(); + + assertTrue(pp.hasSubscribers()); + + to.dispose(); + + assertFalse(pp.hasSubscribers()); + } + + @Test + public void zipperThrows() { + Single.zip(Arrays.asList(Single.just(1), Single.just(2)), new Function<Object[], Object>() { + @Override + public Object apply(Object[] b) throws Exception { + throw new TestException(); + } + }) + .test() + .assertFailure(TestException.class); + } + + @Test + public void zipperReturnsNull() { + Single.zip(Arrays.asList(Single.just(1), Single.just(2)), new Function<Object[], Object>() { + @Override + public Object apply(Object[] a) throws Exception { + return null; + } + }) + .test() + .assertFailure(NullPointerException.class); + } + + @Test + public void middleError() { + PublishProcessor<Integer> pp0 = PublishProcessor.create(); + PublishProcessor<Integer> pp1 = PublishProcessor.create(); + + TestObserver<Object> to = Single.zip( + Arrays.asList(pp0.single(0), pp1.single(0), pp0.single(0)), addString) + .test(); + + pp1.onError(new TestException()); + + assertFalse(pp0.hasSubscribers()); + + to.assertFailure(TestException.class); + } + + @Test + public void innerErrorRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + final PublishProcessor<Integer> pp0 = PublishProcessor.create(); + final PublishProcessor<Integer> pp1 = PublishProcessor.create(); + + final TestObserver<Object> to = Single.zip( + Arrays.asList(pp0.single(0), pp1.single(0)), addString) + .test(); + + final TestException ex = new TestException(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + pp0.onError(ex); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + pp1.onError(ex); + } + }; + + TestHelper.race(r1, r2); + + to.assertFailure(TestException.class); + + if (!errors.isEmpty()) { + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } + } finally { + RxJavaPlugins.reset(); + } + } + } + + @Test + public void iteratorThrows() { + Single.zip(new CrashingMappedIterable<>(1, 100, 100, new Function<Integer, Single<Integer>>() { + @Override + public Single<Integer> apply(Integer v) throws Exception { + return Single.just(v); + } + }), addString) + .to(TestHelper.<Object>testConsumer()) + .assertFailureAndMessage(TestException.class, "iterator()"); + } + + @Test + public void hasNextThrows() { + Single.zip(new CrashingMappedIterable<>(100, 20, 100, new Function<Integer, Single<Integer>>() { + @Override + public Single<Integer> apply(Integer v) throws Exception { + return Single.just(v); + } + }), addString) + .to(TestHelper.<Object>testConsumer()) + .assertFailureAndMessage(TestException.class, "hasNext()"); + } + + @Test + public void nextThrows() { + Single.zip(new CrashingMappedIterable<>(100, 100, 5, new Function<Integer, Single<Integer>>() { + @Override + public Single<Integer> apply(Integer v) throws Exception { + return Single.just(v); + } + }), addString) + .to(TestHelper.<Object>testConsumer()) + .assertFailureAndMessage(TestException.class, "next()"); + } + + @Test(expected = NullPointerException.class) + public void zipIterableOneIsNull() { + Single.zip(Arrays.asList(null, Single.just(1)), new Function<Object[], Object>() { + @Override + public Object apply(Object[] v) { + return 1; + } + }) + .blockingGet(); + } + + @Test(expected = NullPointerException.class) + public void zipIterableTwoIsNull() { + Single.zip(Arrays.asList(Single.just(1), null), new Function<Object[], Object>() { + @Override + public Object apply(Object[] v) { + return 1; + } + }) + .blockingGet(); + } + + @Test + public void emptyIterable() { + Single.zip(Collections.<SingleSource<Integer>>emptyList(), new Function<Object[], Object[]>() { + @Override + public Object[] apply(Object[] a) throws Exception { + return a; + } + }) + .test() + .assertFailure(NoSuchElementException.class); + } + + @Test + public void oneIterable() { + Single.zip(Collections.singleton(Single.just(1)), new Function<Object[], Object>() { + @Override + public Object apply(Object[] a) throws Exception { + return (Integer)a[0] + 1; + } + }) + .test() + .assertResult(2); + } + + @Test + public void singleSourceZipperReturnsNull() { + Single.zip(Arrays.asList(Single.just(1)), Functions.justFunction(null)) + .to(TestHelper.<Object>testConsumer()) + .assertFailureAndMessage(NullPointerException.class, "The zipper returned a null value"); + } + + @Test + public void singleSourcesInIterable() { + SingleSource<Integer> source = new SingleSource<Integer>() { + @Override + public void subscribe(SingleObserver<? super Integer> observer) { + Single.just(1).subscribe(observer); + } + }; + + Single.zip(Arrays.asList(source, source), new Function<Object[], Integer>() { + @Override + public Integer apply(Object[] t) throws Throwable { + return 2; + } + }) + .test() + .assertResult(2); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleZipTest.java b/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleZipTest.java new file mode 100644 index 0000000000..14f2e68267 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/operators/single/SingleZipTest.java @@ -0,0 +1,188 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.operators.single; + +import static org.junit.Assert.assertEquals; + +import java.util.Arrays; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.functions.*; + +public class SingleZipTest extends RxJavaTest { + + @Test + public void zip2() { + Single.zip(Single.just(1), Single.just(2), new BiFunction<Integer, Integer, Object>() { + @Override + public Object apply(Integer a, Integer b) throws Exception { + return a + "" + b; + } + }) + .test() + .assertResult("12"); + } + + @Test + public void zip3() { + Single.zip(Single.just(1), Single.just(2), Single.just(3), new Function3<Integer, Integer, Integer, Object>() { + @Override + public Object apply(Integer a, Integer b, Integer c) throws Exception { + return a + "" + b + c; + } + }) + .test() + .assertResult("123"); + } + + @Test + public void zip4() { + Single.zip(Single.just(1), Single.just(2), Single.just(3), + Single.just(4), + new Function4<Integer, Integer, Integer, Integer, Object>() { + @Override + public Object apply(Integer a, Integer b, Integer c, Integer d) throws Exception { + return a + "" + b + c + d; + } + }) + .test() + .assertResult("1234"); + } + + @Test + public void zip5() { + Single.zip(Single.just(1), Single.just(2), Single.just(3), + Single.just(4), Single.just(5), + new Function5<Integer, Integer, Integer, Integer, Integer, Object>() { + @Override + public Object apply(Integer a, Integer b, Integer c, Integer d, Integer e) throws Exception { + return a + "" + b + c + d + e; + } + }) + .test() + .assertResult("12345"); + } + + @Test + public void zip6() { + Single.zip(Single.just(1), Single.just(2), Single.just(3), + Single.just(4), Single.just(5), Single.just(6), + new Function6<Integer, Integer, Integer, Integer, Integer, Integer, Object>() { + @Override + public Object apply(Integer a, Integer b, Integer c, Integer d, Integer e, Integer f) + throws Exception { + return a + "" + b + c + d + e + f; + } + }) + .test() + .assertResult("123456"); + } + + @Test + public void zip7() { + Single.zip(Single.just(1), Single.just(2), Single.just(3), + Single.just(4), Single.just(5), Single.just(6), + Single.just(7), + new Function7<Integer, Integer, Integer, Integer, Integer, Integer, Integer, Object>() { + @Override + public Object apply(Integer a, Integer b, Integer c, Integer d, Integer e, Integer f, Integer g) + throws Exception { + return a + "" + b + c + d + e + f + g; + } + }) + .test() + .assertResult("1234567"); + } + + @Test + public void zip8() { + Single.zip(Single.just(1), Single.just(2), Single.just(3), + Single.just(4), Single.just(5), Single.just(6), + Single.just(7), Single.just(8), + new Function8<Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Object>() { + @Override + public Object apply(Integer a, Integer b, Integer c, Integer d, Integer e, Integer f, Integer g, + Integer h) throws Exception { + return a + "" + b + c + d + e + f + g + h; + } + }) + .test() + .assertResult("12345678"); + } + + @Test + public void zip9() { + Single.zip(Single.just(1), Single.just(2), Single.just(3), + Single.just(4), Single.just(5), Single.just(6), + Single.just(7), Single.just(8), Single.just(9), + new Function9<Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Object>() { + @Override + public Object apply(Integer a, Integer b, Integer c, Integer d, Integer e, Integer f, Integer g, + Integer h, Integer i) throws Exception { + return a + "" + b + c + d + e + f + g + h + i; + } + }) + .test() + .assertResult("123456789"); + } + + @Test + public void noDisposeOnAllSuccess() { + final AtomicInteger counter = new AtomicInteger(); + + Single<Integer> source = Single.just(1).doOnDispose(new Action() { + @Override + public void run() throws Exception { + counter.getAndIncrement(); + } + }); + + Single.zip(source, source, new BiFunction<Integer, Integer, Object>() { + @Override + public Integer apply(Integer a, Integer b) throws Exception { + return a + b; + } + }) + .test() + .assertResult(2); + + assertEquals(0, counter.get()); + } + + @Test + public void noDisposeOnAllSuccess2() { + final AtomicInteger counter = new AtomicInteger(); + + Single<Integer> source = Single.just(1).doOnDispose(new Action() { + @Override + public void run() throws Exception { + counter.getAndIncrement(); + } + }); + + Single.zip(Arrays.asList(source, source), new Function<Object[], Object>() { + @Override + public Integer apply(Object[] o) throws Exception { + return (Integer)o[0] + (Integer)o[1]; + } + }) + .test() + .assertResult(2); + + assertEquals(0, counter.get()); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/schedulers/AbstractDirectTaskTest.java b/src/test/java/io/reactivex/rxjava3/internal/schedulers/AbstractDirectTaskTest.java new file mode 100644 index 0000000000..cd10a10443 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/schedulers/AbstractDirectTaskTest.java @@ -0,0 +1,271 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.schedulers; + +import static org.junit.Assert.*; + +import java.util.concurrent.FutureTask; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.RxJavaTest; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class AbstractDirectTaskTest extends RxJavaTest { + + @Test + public void cancelSetFuture() { + AbstractDirectTask task = new AbstractDirectTask(Functions.EMPTY_RUNNABLE, true) { + private static final long serialVersionUID = 208585707945686116L; + }; + final Boolean[] interrupted = { null }; + + assertFalse(task.isDisposed()); + + task.dispose(); + + assertTrue(task.isDisposed()); + + task.dispose(); + + assertTrue(task.isDisposed()); + + FutureTask<Void> ft = new FutureTask<Void>(Functions.EMPTY_RUNNABLE, null) { + @Override + public boolean cancel(boolean mayInterruptIfRunning) { + interrupted[0] = mayInterruptIfRunning; + return super.cancel(mayInterruptIfRunning); + } + }; + task.setFuture(ft); + + assertTrue(interrupted[0]); + + assertTrue(task.isDisposed()); + } + + @Test + public void cancelSetFutureCurrentThread() { + AbstractDirectTask task = new AbstractDirectTask(Functions.EMPTY_RUNNABLE, true) { + private static final long serialVersionUID = 208585707945686116L; + }; + final Boolean[] interrupted = { null }; + + assertFalse(task.isDisposed()); + + task.runner = Thread.currentThread(); + + task.dispose(); + + assertTrue(task.isDisposed()); + + task.dispose(); + + assertTrue(task.isDisposed()); + + FutureTask<Void> ft = new FutureTask<Void>(Functions.EMPTY_RUNNABLE, null) { + @Override + public boolean cancel(boolean mayInterruptIfRunning) { + interrupted[0] = mayInterruptIfRunning; + return super.cancel(mayInterruptIfRunning); + } + }; + task.setFuture(ft); + + assertFalse(interrupted[0]); + + assertTrue(task.isDisposed()); + } + + @Test + public void setFutureCancel() { + AbstractDirectTask task = new AbstractDirectTask(Functions.EMPTY_RUNNABLE, true) { + private static final long serialVersionUID = 208585707945686116L; + }; + final Boolean[] interrupted = { null }; + + FutureTask<Void> ft = new FutureTask<Void>(Functions.EMPTY_RUNNABLE, null) { + @Override + public boolean cancel(boolean mayInterruptIfRunning) { + interrupted[0] = mayInterruptIfRunning; + return super.cancel(mayInterruptIfRunning); + } + }; + + assertFalse(task.isDisposed()); + + task.setFuture(ft); + + assertFalse(task.isDisposed()); + + task.dispose(); + + assertTrue(task.isDisposed()); + + assertTrue(interrupted[0]); + } + + @Test + public void setFutureCancelSameThread() { + AbstractDirectTask task = new AbstractDirectTask(Functions.EMPTY_RUNNABLE, true) { + private static final long serialVersionUID = 208585707945686116L; + }; + final Boolean[] interrupted = { null }; + + FutureTask<Void> ft = new FutureTask<Void>(Functions.EMPTY_RUNNABLE, null) { + @Override + public boolean cancel(boolean mayInterruptIfRunning) { + interrupted[0] = mayInterruptIfRunning; + return super.cancel(mayInterruptIfRunning); + } + }; + assertFalse(task.isDisposed()); + + task.setFuture(ft); + + task.runner = Thread.currentThread(); + + assertFalse(task.isDisposed()); + + task.dispose(); + + assertTrue(task.isDisposed()); + + assertFalse(interrupted[0]); + } + + @Test + public void finished() { + AbstractDirectTask task = new AbstractDirectTask(Functions.EMPTY_RUNNABLE, true) { + private static final long serialVersionUID = 208585707945686116L; + }; + final Boolean[] interrupted = { null }; + FutureTask<Void> ft = new FutureTask<Void>(Functions.EMPTY_RUNNABLE, null) { + @Override + public boolean cancel(boolean mayInterruptIfRunning) { + interrupted[0] = mayInterruptIfRunning; + return super.cancel(mayInterruptIfRunning); + } + }; + + task.set(AbstractDirectTask.FINISHED); + + task.setFuture(ft); + + assertTrue(task.isDisposed()); + + assertNull(interrupted[0]); + + task.dispose(); + + assertTrue(task.isDisposed()); + + assertNull(interrupted[0]); + } + + @Test + public void finishedCancel() { + AbstractDirectTask task = new AbstractDirectTask(Functions.EMPTY_RUNNABLE, true) { + private static final long serialVersionUID = 208585707945686116L; + }; + final Boolean[] interrupted = { null }; + FutureTask<Void> ft = new FutureTask<Void>(Functions.EMPTY_RUNNABLE, null) { + @Override + public boolean cancel(boolean mayInterruptIfRunning) { + interrupted[0] = mayInterruptIfRunning; + return super.cancel(mayInterruptIfRunning); + } + }; + + task.set(AbstractDirectTask.FINISHED); + + assertTrue(task.isDisposed()); + + task.dispose(); + + assertTrue(task.isDisposed()); + + task.setFuture(ft); + + assertTrue(task.isDisposed()); + + assertNull(interrupted[0]); + + assertTrue(task.isDisposed()); + + assertNull(interrupted[0]); + } + + @Test + public void disposeSetFutureRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final AbstractDirectTask task = new AbstractDirectTask(Functions.EMPTY_RUNNABLE, true) { + private static final long serialVersionUID = 208585707945686116L; + }; + + final Boolean[] interrupted = { null }; + final FutureTask<Void> ft = new FutureTask<Void>(Functions.EMPTY_RUNNABLE, null) { + @Override + public boolean cancel(boolean mayInterruptIfRunning) { + interrupted[0] = mayInterruptIfRunning; + return super.cancel(mayInterruptIfRunning); + } + }; + + Runnable r1 = new Runnable() { + @Override + public void run() { + task.dispose(); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + task.setFuture(ft); + } + }; + + TestHelper.race(r1, r2); + } + } + + static class TestDirectTask extends AbstractDirectTask { + private static final long serialVersionUID = 587679821055711738L; + + TestDirectTask() { + super(Functions.EMPTY_RUNNABLE, true); + } + } + + @Test + public void toStringStates() { + TestDirectTask task = new TestDirectTask(); + + assertEquals("TestDirectTask[Waiting]", task.toString()); + + task.runner = Thread.currentThread(); + + assertEquals("TestDirectTask[Running on " + Thread.currentThread() + "]", task.toString()); + + task.dispose(); + + assertEquals("TestDirectTask[Disposed]", task.toString()); + + task.set(AbstractDirectTask.FINISHED); + + assertEquals("TestDirectTask[Finished]", task.toString()); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/schedulers/BooleanRunnableTest.java b/src/test/java/io/reactivex/rxjava3/internal/schedulers/BooleanRunnableTest.java new file mode 100644 index 0000000000..237bb233eb --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/schedulers/BooleanRunnableTest.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.schedulers; + +import static org.junit.Assert.fail; + +import java.util.List; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.RxJavaTest; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.internal.schedulers.ExecutorScheduler.ExecutorWorker.BooleanRunnable; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class BooleanRunnableTest extends RxJavaTest { + + @Test + public void runnableThrows() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + BooleanRunnable task = new BooleanRunnable(() -> { + throw new TestException(); + }); + + try { + task.run(); + fail("Should have thrown!"); + } catch (TestException expected) { + // expected + } + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/schedulers/ComputationSchedulerInternalTest.java b/src/test/java/io/reactivex/rxjava3/internal/schedulers/ComputationSchedulerInternalTest.java new file mode 100644 index 0000000000..5c8aa1a65f --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/schedulers/ComputationSchedulerInternalTest.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.schedulers; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.RxJavaTest; + +public class ComputationSchedulerInternalTest extends RxJavaTest { + + @Test + public void capPoolSize() { + assertEquals(8, ComputationScheduler.cap(8, -1)); + assertEquals(8, ComputationScheduler.cap(8, 0)); + assertEquals(4, ComputationScheduler.cap(8, 4)); + assertEquals(8, ComputationScheduler.cap(8, 8)); + assertEquals(8, ComputationScheduler.cap(8, 9)); + assertEquals(8, ComputationScheduler.cap(8, 16)); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/schedulers/DisposeOnCancelTest.java b/src/test/java/io/reactivex/rxjava3/internal/schedulers/DisposeOnCancelTest.java new file mode 100644 index 0000000000..224e33ae7d --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/schedulers/DisposeOnCancelTest.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.schedulers; + +import static org.junit.Assert.*; + +import java.util.concurrent.TimeUnit; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.RxJavaTest; +import io.reactivex.rxjava3.disposables.*; + +public class DisposeOnCancelTest extends RxJavaTest { + + @Test + public void basicCoverage() throws Exception { + Disposable d = Disposable.empty(); + + DisposeOnCancel doc = new DisposeOnCancel(d); + + assertFalse(doc.cancel(true)); + + assertFalse(doc.isCancelled()); + + assertFalse(doc.isDone()); + + assertNull(doc.get()); + + assertNull(doc.get(1, TimeUnit.SECONDS)); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/schedulers/ExecutorSchedulerDelayedRunnableTest.java b/src/test/java/io/reactivex/rxjava3/internal/schedulers/ExecutorSchedulerDelayedRunnableTest.java new file mode 100644 index 0000000000..31c3112255 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/schedulers/ExecutorSchedulerDelayedRunnableTest.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.schedulers; + +import static org.junit.Assert.assertEquals; + +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.RxJavaTest; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.internal.schedulers.ExecutorScheduler.DelayedRunnable; +import io.reactivex.rxjava3.testsupport.SuppressUndeliverable; + +public class ExecutorSchedulerDelayedRunnableTest extends RxJavaTest { + + @Test(expected = TestException.class) + @SuppressUndeliverable + public void delayedRunnableCrash() { + DelayedRunnable dl = new DelayedRunnable(new Runnable() { + @Override + public void run() { + throw new TestException(); + } + }); + dl.run(); + } + + @Test + public void dispose() { + final AtomicInteger count = new AtomicInteger(); + DelayedRunnable dl = new DelayedRunnable(new Runnable() { + @Override + public void run() { + count.incrementAndGet(); + } + }); + + dl.dispose(); + dl.dispose(); + + dl.run(); + + assertEquals(0, count.get()); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/schedulers/ExecutorSchedulerInternalTest.java b/src/test/java/io/reactivex/rxjava3/internal/schedulers/ExecutorSchedulerInternalTest.java new file mode 100644 index 0000000000..64d4d1861c --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/schedulers/ExecutorSchedulerInternalTest.java @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.schedulers; + +import static org.junit.Assert.assertNotNull; + +import org.junit.Test; + +public class ExecutorSchedulerInternalTest { + + @Test + public void helperHolder() { + assertNotNull(new ExecutorScheduler.SingleHolder()); + } + +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/schedulers/ImmediateThinSchedulerTest.java b/src/test/java/io/reactivex/rxjava3/internal/schedulers/ImmediateThinSchedulerTest.java new file mode 100644 index 0000000000..75710644d9 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/schedulers/ImmediateThinSchedulerTest.java @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.schedulers; + +import static org.junit.Assert.*; + +import java.util.concurrent.TimeUnit; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.RxJavaTest; +import io.reactivex.rxjava3.core.Scheduler.Worker; +import io.reactivex.rxjava3.internal.functions.Functions; + +public class ImmediateThinSchedulerTest extends RxJavaTest { + + @Test + public void scheduleDirect() { + final int[] count = { 0 }; + + ImmediateThinScheduler.INSTANCE.scheduleDirect(new Runnable() { + @Override + public void run() { + count[0]++; + } + }); + + assertEquals(1, count[0]); + } + + @Test(expected = UnsupportedOperationException.class) + public void scheduleDirectTimed() { + ImmediateThinScheduler.INSTANCE.scheduleDirect(Functions.EMPTY_RUNNABLE, 1, TimeUnit.SECONDS); + } + + @Test(expected = UnsupportedOperationException.class) + public void scheduleDirectPeriodic() { + ImmediateThinScheduler.INSTANCE.schedulePeriodicallyDirect(Functions.EMPTY_RUNNABLE, 1, 1, TimeUnit.SECONDS); + } + + @Test + public void schedule() { + final int[] count = { 0 }; + + Worker w = ImmediateThinScheduler.INSTANCE.createWorker(); + + assertFalse(w.isDisposed()); + + w.schedule(new Runnable() { + @Override + public void run() { + count[0]++; + } + }); + + assertEquals(1, count[0]); + } + + @Test(expected = UnsupportedOperationException.class) + public void scheduleTimed() { + ImmediateThinScheduler.INSTANCE.createWorker().schedule(Functions.EMPTY_RUNNABLE, 1, TimeUnit.SECONDS); + } + + @Test(expected = UnsupportedOperationException.class) + public void schedulePeriodic() { + ImmediateThinScheduler.INSTANCE.createWorker().schedulePeriodically(Functions.EMPTY_RUNNABLE, 1, 1, TimeUnit.SECONDS); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/schedulers/InstantPeriodicTaskTest.java b/src/test/java/io/reactivex/rxjava3/internal/schedulers/InstantPeriodicTaskTest.java new file mode 100644 index 0000000000..4a57e86781 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/schedulers/InstantPeriodicTaskTest.java @@ -0,0 +1,280 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.schedulers; + +import static org.junit.Assert.*; + +import java.util.List; +import java.util.concurrent.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.RxJavaTest; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class InstantPeriodicTaskTest extends RxJavaTest { + + @Test + public void taskCrash() throws Exception { + ExecutorService exec = Executors.newSingleThreadExecutor(); + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + + InstantPeriodicTask task = new InstantPeriodicTask(new Runnable() { + @Override + public void run() { + throw new TestException(); + } + }, exec); + + try { + task.call(); + fail("Should have thrown!"); + } catch (TestException excepted) { + // excepted + } + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + exec.shutdownNow(); + RxJavaPlugins.reset(); + } + } + + @Test + public void dispose() throws Exception { + ExecutorService exec = Executors.newSingleThreadExecutor(); + try { + + InstantPeriodicTask task = new InstantPeriodicTask(new Runnable() { + @Override + public void run() { + throw new TestException(); + } + }, exec); + + assertFalse(task.isDisposed()); + + task.dispose(); + + assertTrue(task.isDisposed()); + + task.dispose(); + + assertTrue(task.isDisposed()); + } finally { + exec.shutdownNow(); + RxJavaPlugins.reset(); + } + } + + @Test + public void dispose2() throws Exception { + ExecutorService exec = Executors.newSingleThreadExecutor(); + try { + + InstantPeriodicTask task = new InstantPeriodicTask(new Runnable() { + @Override + public void run() { + throw new TestException(); + } + }, exec); + + task.setFirst(new FutureTask<Void>(Functions.EMPTY_RUNNABLE, null)); + task.setRest(new FutureTask<Void>(Functions.EMPTY_RUNNABLE, null)); + + assertFalse(task.isDisposed()); + + task.dispose(); + + assertTrue(task.isDisposed()); + + task.dispose(); + + assertTrue(task.isDisposed()); + } finally { + exec.shutdownNow(); + RxJavaPlugins.reset(); + } + } + + @Test + public void dispose2CurrentThread() throws Exception { + ExecutorService exec = Executors.newSingleThreadExecutor(); + try { + + InstantPeriodicTask task = new InstantPeriodicTask(new Runnable() { + @Override + public void run() { + throw new TestException(); + } + }, exec); + + task.runner = Thread.currentThread(); + + task.setFirst(new FutureTask<Void>(Functions.EMPTY_RUNNABLE, null)); + task.setRest(new FutureTask<Void>(Functions.EMPTY_RUNNABLE, null)); + + assertFalse(task.isDisposed()); + + task.dispose(); + + assertTrue(task.isDisposed()); + + task.dispose(); + + assertTrue(task.isDisposed()); + } finally { + exec.shutdownNow(); + RxJavaPlugins.reset(); + } + } + + @Test + public void dispose3() throws Exception { + ExecutorService exec = Executors.newSingleThreadExecutor(); + try { + + InstantPeriodicTask task = new InstantPeriodicTask(new Runnable() { + @Override + public void run() { + throw new TestException(); + } + }, exec); + + task.dispose(); + + FutureTask<Void> f1 = new FutureTask<>(Functions.EMPTY_RUNNABLE, null); + task.setFirst(f1); + + assertTrue(f1.isCancelled()); + + FutureTask<Void> f2 = new FutureTask<>(Functions.EMPTY_RUNNABLE, null); + task.setRest(f2); + + assertTrue(f2.isCancelled()); + } finally { + exec.shutdownNow(); + RxJavaPlugins.reset(); + } + } + + @Test + public void disposeOnCurrentThread() throws Exception { + ExecutorService exec = Executors.newSingleThreadExecutor(); + try { + + InstantPeriodicTask task = new InstantPeriodicTask(new Runnable() { + @Override + public void run() { + throw new TestException(); + } + }, exec); + + task.runner = Thread.currentThread(); + + task.dispose(); + + FutureTask<Void> f1 = new FutureTask<>(Functions.EMPTY_RUNNABLE, null); + task.setFirst(f1); + + assertTrue(f1.isCancelled()); + + FutureTask<Void> f2 = new FutureTask<>(Functions.EMPTY_RUNNABLE, null); + task.setRest(f2); + + assertTrue(f2.isCancelled()); + } finally { + exec.shutdownNow(); + RxJavaPlugins.reset(); + } + } + + @Test + public void firstCancelRace() throws Exception { + ExecutorService exec = Executors.newSingleThreadExecutor(); + try { + for (int i = 0; i < TestHelper.RACE_LONG_LOOPS; i++) { + final InstantPeriodicTask task = new InstantPeriodicTask(new Runnable() { + @Override + public void run() { + throw new TestException(); + } + }, exec); + + final FutureTask<Void> f1 = new FutureTask<>(Functions.EMPTY_RUNNABLE, null); + Runnable r1 = new Runnable() { + @Override + public void run() { + task.setFirst(f1); + } + }; + Runnable r2 = new Runnable() { + @Override + public void run() { + task.dispose(); + } + }; + + TestHelper.race(r1, r2); + + assertTrue(f1.isCancelled()); + assertTrue(task.isDisposed()); + } + } finally { + exec.shutdownNow(); + RxJavaPlugins.reset(); + } + } + + @Test + public void restCancelRace() throws Exception { + ExecutorService exec = Executors.newSingleThreadExecutor(); + try { + for (int i = 0; i < TestHelper.RACE_LONG_LOOPS; i++) { + final InstantPeriodicTask task = new InstantPeriodicTask(new Runnable() { + @Override + public void run() { + throw new TestException(); + } + }, exec); + + final FutureTask<Void> f1 = new FutureTask<>(Functions.EMPTY_RUNNABLE, null); + Runnable r1 = new Runnable() { + @Override + public void run() { + task.setRest(f1); + } + }; + Runnable r2 = new Runnable() { + @Override + public void run() { + task.dispose(); + } + }; + + TestHelper.race(r1, r2); + + assertTrue(f1.isCancelled()); + assertTrue(task.isDisposed()); + } + } finally { + exec.shutdownNow(); + RxJavaPlugins.reset(); + } + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/schedulers/InterruptibleRunnableTest.java b/src/test/java/io/reactivex/rxjava3/internal/schedulers/InterruptibleRunnableTest.java new file mode 100644 index 0000000000..e040b37419 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/schedulers/InterruptibleRunnableTest.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.schedulers; + +import static org.junit.Assert.fail; + +import java.util.List; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.RxJavaTest; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.internal.schedulers.ExecutorScheduler.ExecutorWorker.InterruptibleRunnable; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class InterruptibleRunnableTest extends RxJavaTest { + + @Test + public void runnableThrows() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + InterruptibleRunnable task = new InterruptibleRunnable(() -> { + throw new TestException(); + }, null); + + try { + task.run(); + fail("Should have thrown!"); + } catch (TestException expected) { + // expected + } + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/schedulers/IoScheduledReleaseTest.java b/src/test/java/io/reactivex/rxjava3/internal/schedulers/IoScheduledReleaseTest.java new file mode 100644 index 0000000000..e3f55b75fb --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/schedulers/IoScheduledReleaseTest.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.schedulers; + +import io.reactivex.rxjava3.core.Completable; +import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.core.RxJavaTest; +import io.reactivex.rxjava3.schedulers.Schedulers; +import org.junit.Test; + +import java.util.concurrent.TimeUnit; + +public class IoScheduledReleaseTest extends RxJavaTest { + + /* This test will be stuck in a deadlock if IoScheduler.USE_SCHEDULED_RELEASE is not set */ + @Test + public void scheduledRelease() { + boolean savedScheduledRelease = IoScheduler.USE_SCHEDULED_RELEASE; + IoScheduler.USE_SCHEDULED_RELEASE = true; + try { + Flowable.just("item") + .observeOn(Schedulers.io()) + .firstOrError() + .map(item -> { + for (int i = 0; i < 50; i++) { + Completable.complete() + .observeOn(Schedulers.io()) + .blockingAwait(); + } + return "Done"; + }) + .ignoreElement() + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertComplete(); + } finally { + IoScheduler.USE_SCHEDULED_RELEASE = savedScheduledRelease; + } + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/schedulers/IoSchedulerInternalTest.java b/src/test/java/io/reactivex/rxjava3/internal/schedulers/IoSchedulerInternalTest.java new file mode 100644 index 0000000000..9a4b6f3be0 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/schedulers/IoSchedulerInternalTest.java @@ -0,0 +1,95 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.schedulers; + +import static org.junit.Assert.*; +import java.util.concurrent.ConcurrentLinkedQueue; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.RxJavaTest; +import io.reactivex.rxjava3.disposables.CompositeDisposable; +import io.reactivex.rxjava3.internal.schedulers.IoScheduler.*; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class IoSchedulerInternalTest extends RxJavaTest { + + @Test + public void expiredQueueEmpty() { + ConcurrentLinkedQueue<ThreadWorker> expire = new ConcurrentLinkedQueue<>(); + CompositeDisposable cd = new CompositeDisposable(); + + CachedWorkerPool.evictExpiredWorkers(expire, cd); + } + + @Test + public void expiredWorkerRemoved() { + ConcurrentLinkedQueue<ThreadWorker> expire = new ConcurrentLinkedQueue<>(); + CompositeDisposable cd = new CompositeDisposable(); + + ThreadWorker tw = new ThreadWorker(new RxThreadFactory("IoExpiryTest")); + + try { + expire.add(tw); + cd.add(tw); + + CachedWorkerPool.evictExpiredWorkers(expire, cd); + + assertTrue(tw.isDisposed()); + assertTrue(expire.isEmpty()); + } finally { + tw.dispose(); + } + } + + @Test + public void noExpiredWorker() { + ConcurrentLinkedQueue<ThreadWorker> expire = new ConcurrentLinkedQueue<>(); + CompositeDisposable cd = new CompositeDisposable(); + + ThreadWorker tw = new ThreadWorker(new RxThreadFactory("IoExpiryTest")); + tw.setExpirationTime(System.nanoTime() + 10_000_000_000L); + + try { + expire.add(tw); + cd.add(tw); + + CachedWorkerPool.evictExpiredWorkers(expire, cd); + + assertFalse(tw.isDisposed()); + assertFalse(expire.isEmpty()); + } finally { + tw.dispose(); + } + } + + @Test + public void expireReuseRace() { + ConcurrentLinkedQueue<ThreadWorker> expire = new ConcurrentLinkedQueue<>(); + CompositeDisposable cd = new CompositeDisposable(); + + ThreadWorker tw = new ThreadWorker(new RxThreadFactory("IoExpiryTest")); + tw.dispose(); + + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + expire.add(tw); + cd.add(tw); + + TestHelper.race( + () -> CachedWorkerPool.evictExpiredWorkers(expire, cd), + () -> expire.remove(tw) + ); + } + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/schedulers/RxThreadFactoryTest.java b/src/test/java/io/reactivex/rxjava3/internal/schedulers/RxThreadFactoryTest.java new file mode 100644 index 0000000000..d0cbb6a476 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/schedulers/RxThreadFactoryTest.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.schedulers; + +import static org.junit.Assert.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.RxJavaTest; +import io.reactivex.rxjava3.internal.functions.Functions; + +public class RxThreadFactoryTest extends RxJavaTest { + + @Test + public void normal() { + RxThreadFactory tf = new RxThreadFactory("Test", 1); + + assertEquals("RxThreadFactory[Test]", tf.toString()); + + Thread t = tf.newThread(Functions.EMPTY_RUNNABLE); + + assertTrue(t.isDaemon()); + assertEquals(1, t.getPriority()); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/schedulers/ScheduledDirectPeriodicTaskTest.java b/src/test/java/io/reactivex/rxjava3/internal/schedulers/ScheduledDirectPeriodicTaskTest.java new file mode 100644 index 0000000000..d41906b13a --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/schedulers/ScheduledDirectPeriodicTaskTest.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.schedulers; + +import static org.junit.Assert.fail; + +import java.util.List; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.RxJavaTest; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class ScheduledDirectPeriodicTaskTest extends RxJavaTest { + + @Test + public void runnableThrows() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + ScheduledDirectPeriodicTask task = new ScheduledDirectPeriodicTask(new Runnable() { + @Override + public void run() { + throw new TestException(); + } + }, true); + + try { + task.run(); + fail("Should have thrown!"); + } catch (TestException expected) { + // expected + } + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/schedulers/ScheduledRunnableTest.java b/src/test/java/io/reactivex/rxjava3/internal/schedulers/ScheduledRunnableTest.java new file mode 100644 index 0000000000..8d2fc1f7cc --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/schedulers/ScheduledRunnableTest.java @@ -0,0 +1,427 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.schedulers; + +import static org.junit.Assert.*; + +import java.lang.Thread.UncaughtExceptionHandler; +import java.util.List; +import java.util.concurrent.FutureTask; +import java.util.concurrent.atomic.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.RxJavaTest; +import io.reactivex.rxjava3.disposables.CompositeDisposable; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class ScheduledRunnableTest extends RxJavaTest { + + @Test + public void dispose() { + CompositeDisposable set = new CompositeDisposable(); + ScheduledRunnable run = new ScheduledRunnable(Functions.EMPTY_RUNNABLE, set); + set.add(run); + + assertFalse(run.isDisposed()); + + set.dispose(); + + assertTrue(run.isDisposed()); + } + + @Test + public void disposeRun() { + CompositeDisposable set = new CompositeDisposable(); + ScheduledRunnable run = new ScheduledRunnable(Functions.EMPTY_RUNNABLE, set); + set.add(run); + + assertFalse(run.isDisposed()); + + run.dispose(); + run.dispose(); + + assertTrue(run.isDisposed()); + } + + @Test + public void setFutureCancelRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + CompositeDisposable set = new CompositeDisposable(); + final ScheduledRunnable run = new ScheduledRunnable(Functions.EMPTY_RUNNABLE, set); + set.add(run); + + final FutureTask<Object> ft = new FutureTask<>(Functions.EMPTY_RUNNABLE, 0); + + Runnable r1 = new Runnable() { + @Override + public void run() { + run.setFuture(ft); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + run.dispose(); + } + }; + + TestHelper.race(r1, r2); + + assertEquals(0, set.size()); + } + } + + @Test + public void setFutureRunRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + CompositeDisposable set = new CompositeDisposable(); + final ScheduledRunnable run = new ScheduledRunnable(Functions.EMPTY_RUNNABLE, set); + set.add(run); + + final FutureTask<Object> ft = new FutureTask<>(Functions.EMPTY_RUNNABLE, 0); + + Runnable r1 = new Runnable() { + @Override + public void run() { + run.setFuture(ft); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + run.run(); + } + }; + + TestHelper.race(r1, r2); + + assertEquals(0, set.size()); + } + } + + @Test + public void disposeRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + CompositeDisposable set = new CompositeDisposable(); + final ScheduledRunnable run = new ScheduledRunnable(Functions.EMPTY_RUNNABLE, set); + set.add(run); + + Runnable r1 = new Runnable() { + @Override + public void run() { + run.dispose(); + } + }; + + TestHelper.race(r1, r1); + + assertEquals(0, set.size()); + } + } + + @Test + public void runDispose() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + CompositeDisposable set = new CompositeDisposable(); + final ScheduledRunnable run = new ScheduledRunnable(Functions.EMPTY_RUNNABLE, set); + set.add(run); + + Runnable r1 = new Runnable() { + @Override + public void run() { + run.call(); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + run.dispose(); + } + }; + + TestHelper.race(r1, r2); + + assertEquals(0, set.size()); + } + } + + @Test + public void pluginCrash() { + Thread.currentThread().setUncaughtExceptionHandler(new UncaughtExceptionHandler() { + @Override + public void uncaughtException(Thread t, Throwable e) { + throw new TestException("Second"); + } + }); + + CompositeDisposable set = new CompositeDisposable(); + final ScheduledRunnable run = new ScheduledRunnable(new Runnable() { + @Override + public void run() { + throw new TestException("First"); + } + }, set); + set.add(run); + + try { + run.run(); + + fail("Should have thrown!"); + } catch (TestException ex) { + assertEquals("Second", ex.getMessage()); + } finally { + Thread.currentThread().setUncaughtExceptionHandler(null); + } + assertTrue(run.isDisposed()); + + assertEquals(0, set.size()); + } + + @Test + public void crashReported() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + CompositeDisposable set = new CompositeDisposable(); + final ScheduledRunnable run = new ScheduledRunnable(new Runnable() { + @Override + public void run() { + throw new TestException("First"); + } + }, set); + set.add(run); + + try { + run.run(); + fail("Should have thrown!"); + } catch (TestException expected) { + // expected + } + + assertTrue(run.isDisposed()); + + assertEquals(0, set.size()); + + TestHelper.assertUndeliverable(errors, 0, TestException.class, "First"); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void withoutParentDisposed() { + ScheduledRunnable run = new ScheduledRunnable(Functions.EMPTY_RUNNABLE, null); + run.dispose(); + run.call(); + } + + @Test + public void withParentDisposed() { + ScheduledRunnable run = new ScheduledRunnable(Functions.EMPTY_RUNNABLE, new CompositeDisposable()); + run.dispose(); + run.call(); + } + + @Test + public void withFutureDisposed() { + ScheduledRunnable run = new ScheduledRunnable(Functions.EMPTY_RUNNABLE, null); + run.setFuture(new FutureTask<Void>(Functions.EMPTY_RUNNABLE, null)); + run.dispose(); + run.call(); + } + + @Test + public void withFutureDisposed2() { + ScheduledRunnable run = new ScheduledRunnable(Functions.EMPTY_RUNNABLE, null); + run.dispose(); + run.setFuture(new FutureTask<Void>(Functions.EMPTY_RUNNABLE, null)); + run.call(); + } + + @Test + public void withFutureDisposed3() { + ScheduledRunnable run = new ScheduledRunnable(Functions.EMPTY_RUNNABLE, null); + run.dispose(); + run.set(2, Thread.currentThread()); + run.setFuture(new FutureTask<Void>(Functions.EMPTY_RUNNABLE, null)); + run.call(); + } + + @Test + public void runFuture() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + CompositeDisposable set = new CompositeDisposable(); + final ScheduledRunnable run = new ScheduledRunnable(Functions.EMPTY_RUNNABLE, set); + set.add(run); + + final FutureTask<Void> ft = new FutureTask<>(Functions.EMPTY_RUNNABLE, null); + + Runnable r1 = new Runnable() { + @Override + public void run() { + run.call(); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + run.setFuture(ft); + } + }; + + TestHelper.race(r1, r2); + } + } + + @Test + public void syncWorkerCancelRace() { + for (int i = 0; i < TestHelper.RACE_LONG_LOOPS; i++) { + final CompositeDisposable set = new CompositeDisposable(); + final AtomicBoolean interrupted = new AtomicBoolean(); + final AtomicInteger sync = new AtomicInteger(2); + final AtomicInteger syncb = new AtomicInteger(2); + + Runnable r0 = new Runnable() { + @Override + public void run() { + set.dispose(); + if (sync.decrementAndGet() != 0) { + while (sync.get() != 0) { } + } + if (syncb.decrementAndGet() != 0) { + while (syncb.get() != 0) { } + } + for (int j = 0; j < 1000; j++) { + if (Thread.currentThread().isInterrupted()) { + interrupted.set(true); + break; + } + } + } + }; + + final ScheduledRunnable run = new ScheduledRunnable(r0, set); + set.add(run); + + final FutureTask<Void> ft = new FutureTask<>(run, null); + + Runnable r2 = new Runnable() { + @Override + public void run() { + if (sync.decrementAndGet() != 0) { + while (sync.get() != 0) { } + } + run.setFuture(ft); + if (syncb.decrementAndGet() != 0) { + while (syncb.get() != 0) { } + } + } + }; + + TestHelper.race(ft, r2); + + assertFalse("The task was interrupted", interrupted.get()); + } + } + + @Test + public void disposeAfterRun() { + final ScheduledRunnable run = new ScheduledRunnable(Functions.EMPTY_RUNNABLE, null); + + run.run(); + assertEquals(ScheduledRunnable.DONE, run.get(ScheduledRunnable.FUTURE_INDEX)); + + run.dispose(); + assertEquals(ScheduledRunnable.DONE, run.get(ScheduledRunnable.FUTURE_INDEX)); + } + + @Test + public void syncDisposeIdempotent() { + final ScheduledRunnable run = new ScheduledRunnable(Functions.EMPTY_RUNNABLE, null); + run.set(ScheduledRunnable.THREAD_INDEX, Thread.currentThread()); + + run.dispose(); + assertEquals(ScheduledRunnable.SYNC_DISPOSED, run.get(ScheduledRunnable.FUTURE_INDEX)); + run.dispose(); + assertEquals(ScheduledRunnable.SYNC_DISPOSED, run.get(ScheduledRunnable.FUTURE_INDEX)); + run.run(); + assertEquals(ScheduledRunnable.SYNC_DISPOSED, run.get(ScheduledRunnable.FUTURE_INDEX)); + } + + @Test + public void asyncDisposeIdempotent() { + final ScheduledRunnable run = new ScheduledRunnable(Functions.EMPTY_RUNNABLE, null); + + run.dispose(); + assertEquals(ScheduledRunnable.ASYNC_DISPOSED, run.get(ScheduledRunnable.FUTURE_INDEX)); + run.dispose(); + assertEquals(ScheduledRunnable.ASYNC_DISPOSED, run.get(ScheduledRunnable.FUTURE_INDEX)); + run.run(); + assertEquals(ScheduledRunnable.ASYNC_DISPOSED, run.get(ScheduledRunnable.FUTURE_INDEX)); + } + + @Test + public void noParentIsDisposed() { + ScheduledRunnable run = new ScheduledRunnable(Functions.EMPTY_RUNNABLE, null); + assertFalse(run.isDisposed()); + run.run(); + assertTrue(run.isDisposed()); + } + + @Test + public void withParentIsDisposed() { + CompositeDisposable set = new CompositeDisposable(); + ScheduledRunnable run = new ScheduledRunnable(Functions.EMPTY_RUNNABLE, set); + set.add(run); + + assertFalse(run.isDisposed()); + + run.run(); + assertTrue(run.isDisposed()); + + assertFalse(set.remove(run)); + } + + @Test + public void toStringStates() { + CompositeDisposable set = new CompositeDisposable(); + ScheduledRunnable task = new ScheduledRunnable(Functions.EMPTY_RUNNABLE, set); + + assertEquals("ScheduledRunnable[Waiting]", task.toString()); + + task.set(ScheduledRunnable.THREAD_INDEX, Thread.currentThread()); + + assertEquals("ScheduledRunnable[Running on " + Thread.currentThread() + "]", task.toString()); + + task.dispose(); + + assertEquals("ScheduledRunnable[Disposed(Sync)]", task.toString()); + + task.set(ScheduledRunnable.FUTURE_INDEX, ScheduledRunnable.DONE); + + assertEquals("ScheduledRunnable[Finished]", task.toString()); + + task = new ScheduledRunnable(Functions.EMPTY_RUNNABLE, set); + task.dispose(); + + assertEquals("ScheduledRunnable[Disposed(Async)]", task.toString()); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/schedulers/SchedulerMultiWorkerSupportTest.java b/src/test/java/io/reactivex/rxjava3/internal/schedulers/SchedulerMultiWorkerSupportTest.java new file mode 100644 index 0000000000..3d16867a1a --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/schedulers/SchedulerMultiWorkerSupportTest.java @@ -0,0 +1,150 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.schedulers; + +import static org.junit.Assert.*; + +import java.util.*; +import java.util.concurrent.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.RxJavaTest; +import io.reactivex.rxjava3.core.Scheduler.Worker; +import io.reactivex.rxjava3.disposables.CompositeDisposable; +import io.reactivex.rxjava3.internal.schedulers.SchedulerMultiWorkerSupport.WorkerCallback; +import io.reactivex.rxjava3.schedulers.Schedulers; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class SchedulerMultiWorkerSupportTest extends RxJavaTest { + + final int max = ComputationScheduler.MAX_THREADS; + + @Test + public void moreThanMaxWorkers() { + final List<Worker> list = new ArrayList<>(); + + SchedulerMultiWorkerSupport mws = (SchedulerMultiWorkerSupport)Schedulers.computation(); + + mws.createWorkers(max * 2, new WorkerCallback() { + @Override + public void onWorker(int i, Worker w) { + list.add(w); + } + }); + + assertEquals(max * 2, list.size()); + } + + @Test + public void getShutdownWorkers() { + final List<Worker> list = new ArrayList<>(); + + ComputationScheduler.NONE.createWorkers(max * 2, new WorkerCallback() { + @Override + public void onWorker(int i, Worker w) { + list.add(w); + } + }); + + assertEquals(max * 2, list.size()); + + for (Worker w : list) { + assertEquals(ComputationScheduler.SHUTDOWN_WORKER, w); + } + } + + @Test + public void distinctThreads() throws Exception { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + + final CompositeDisposable composite = new CompositeDisposable(); + + try { + final CountDownLatch cdl = new CountDownLatch(max * 2); + + final Set<String> threads1 = Collections.synchronizedSet(new HashSet<>()); + + final Set<String> threads2 = Collections.synchronizedSet(new HashSet<>()); + + Runnable parallel1 = new Runnable() { + @Override + public void run() { + final List<Worker> list1 = new ArrayList<>(); + + SchedulerMultiWorkerSupport mws = (SchedulerMultiWorkerSupport)Schedulers.computation(); + + mws.createWorkers(max, new WorkerCallback() { + @Override + public void onWorker(int i, Worker w) { + list1.add(w); + composite.add(w); + } + }); + + Runnable run = new Runnable() { + @Override + public void run() { + threads1.add(Thread.currentThread().getName()); + cdl.countDown(); + } + }; + + for (Worker w : list1) { + w.schedule(run); + } + } + }; + + Runnable parallel2 = new Runnable() { + @Override + public void run() { + final List<Worker> list2 = new ArrayList<>(); + + SchedulerMultiWorkerSupport mws = (SchedulerMultiWorkerSupport)Schedulers.computation(); + + mws.createWorkers(max, new WorkerCallback() { + @Override + public void onWorker(int i, Worker w) { + list2.add(w); + composite.add(w); + } + }); + + Runnable run = new Runnable() { + @Override + public void run() { + threads2.add(Thread.currentThread().getName()); + cdl.countDown(); + } + }; + + for (Worker w : list2) { + w.schedule(run); + } + } + }; + + TestHelper.race(parallel1, parallel2); + + assertTrue(cdl.await(5, TimeUnit.SECONDS)); + + assertEquals(threads1.toString(), max, threads1.size()); + assertEquals(threads2.toString(), max, threads2.size()); + } finally { + composite.dispose(); + } + } + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/schedulers/SchedulerPoolFactoryTest.java b/src/test/java/io/reactivex/rxjava3/internal/schedulers/SchedulerPoolFactoryTest.java new file mode 100644 index 0000000000..12afa708c4 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/schedulers/SchedulerPoolFactoryTest.java @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.schedulers; + +import static org.junit.Assert.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.RxJavaTest; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class SchedulerPoolFactoryTest extends RxJavaTest { + + @Test + public void utilityClass() { + TestHelper.checkUtilityClass(SchedulerPoolFactory.class); + } + + @Test + public void boolPropertiesDisabledReturnsDefaultDisabled() throws Throwable { + assertTrue(SchedulerPoolFactory.getBooleanProperty(false, "key", false, true, failingPropertiesAccessor)); + assertFalse(SchedulerPoolFactory.getBooleanProperty(false, "key", true, false, failingPropertiesAccessor)); + } + + @Test + public void boolPropertiesEnabledMissingReturnsDefaultMissing() throws Throwable { + assertTrue(SchedulerPoolFactory.getBooleanProperty(true, "key", true, false, missingPropertiesAccessor)); + assertFalse(SchedulerPoolFactory.getBooleanProperty(true, "key", false, true, missingPropertiesAccessor)); + } + + @Test + public void boolPropertiesFailureReturnsDefaultMissing() throws Throwable { + assertTrue(SchedulerPoolFactory.getBooleanProperty(true, "key", true, false, failingPropertiesAccessor)); + assertFalse(SchedulerPoolFactory.getBooleanProperty(true, "key", false, true, failingPropertiesAccessor)); + } + + @Test + public void boolPropertiesReturnsValue() throws Throwable { + assertTrue(SchedulerPoolFactory.getBooleanProperty(true, "true", true, false, Functions.<String>identity())); + assertFalse(SchedulerPoolFactory.getBooleanProperty(true, "false", false, true, Functions.<String>identity())); + } + + static final Function<String, String> failingPropertiesAccessor = new Function<String, String>() { + @Override + public String apply(String v) throws Throwable { + throw new SecurityException(); + } + }; + + static final Function<String, String> missingPropertiesAccessor = new Function<String, String>() { + @Override + public String apply(String v) throws Throwable { + return null; + } + }; +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/schedulers/SchedulerWhenTest.java b/src/test/java/io/reactivex/rxjava3/internal/schedulers/SchedulerWhenTest.java new file mode 100644 index 0000000000..03e6a9300f --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/schedulers/SchedulerWhenTest.java @@ -0,0 +1,396 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.schedulers; + +import static io.reactivex.rxjava3.core.Flowable.*; +import static java.util.concurrent.TimeUnit.SECONDS; +import static org.junit.Assert.*; + +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.core.Scheduler.Worker; +import io.reactivex.rxjava3.disposables.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.schedulers.SchedulerWhen.*; +import io.reactivex.rxjava3.observers.DisposableCompletableObserver; +import io.reactivex.rxjava3.processors.PublishProcessor; +import io.reactivex.rxjava3.schedulers.*; +import io.reactivex.rxjava3.subscribers.TestSubscriber; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class SchedulerWhenTest extends RxJavaTest { + @Test + public void asyncMaxConcurrent() { + TestScheduler tSched = new TestScheduler(); + SchedulerWhen sched = maxConcurrentScheduler(tSched); + TestSubscriber<Long> tSub = TestSubscriber.create(); + + asyncWork(sched).subscribe(tSub); + + tSub.assertValueCount(0); + + tSched.advanceTimeBy(0, SECONDS); + tSub.assertValueCount(0); + + tSched.advanceTimeBy(1, SECONDS); + tSub.assertValueCount(2); + + tSched.advanceTimeBy(1, SECONDS); + tSub.assertValueCount(4); + + tSched.advanceTimeBy(1, SECONDS); + tSub.assertValueCount(5); + tSub.assertComplete(); + + sched.dispose(); + } + + @Test + public void asyncDelaySubscription() { + final TestScheduler tSched = new TestScheduler(); + SchedulerWhen sched = throttleScheduler(tSched); + TestSubscriber<Long> tSub = TestSubscriber.create(); + + asyncWork(sched).subscribe(tSub); + + tSub.assertValueCount(0); + + tSched.advanceTimeBy(0, SECONDS); + tSub.assertValueCount(0); + + tSched.advanceTimeBy(1, SECONDS); + tSub.assertValueCount(1); + + tSched.advanceTimeBy(1, SECONDS); + tSub.assertValueCount(1); + + tSched.advanceTimeBy(1, SECONDS); + tSub.assertValueCount(2); + + tSched.advanceTimeBy(1, SECONDS); + tSub.assertValueCount(2); + + tSched.advanceTimeBy(1, SECONDS); + tSub.assertValueCount(3); + + tSched.advanceTimeBy(1, SECONDS); + tSub.assertValueCount(3); + + tSched.advanceTimeBy(1, SECONDS); + tSub.assertValueCount(4); + + tSched.advanceTimeBy(1, SECONDS); + tSub.assertValueCount(4); + + tSched.advanceTimeBy(1, SECONDS); + tSub.assertValueCount(5); + tSub.assertComplete(); + + sched.dispose(); + } + + @Test + public void syncMaxConcurrent() { + TestScheduler tSched = new TestScheduler(); + SchedulerWhen sched = maxConcurrentScheduler(tSched); + TestSubscriber<Long> tSub = TestSubscriber.create(); + + syncWork(sched).subscribe(tSub); + + tSub.assertValueCount(0); + tSched.advanceTimeBy(0, SECONDS); + + // since all the work is synchronous nothing is blocked and its all done + tSub.assertValueCount(5); + tSub.assertComplete(); + + sched.dispose(); + } + + @Test + public void syncDelaySubscription() { + final TestScheduler tSched = new TestScheduler(); + SchedulerWhen sched = throttleScheduler(tSched); + TestSubscriber<Long> tSub = TestSubscriber.create(); + + syncWork(sched).subscribe(tSub); + + tSub.assertValueCount(0); + + tSched.advanceTimeBy(0, SECONDS); + tSub.assertValueCount(1); + + tSched.advanceTimeBy(1, SECONDS); + tSub.assertValueCount(2); + + tSched.advanceTimeBy(1, SECONDS); + tSub.assertValueCount(3); + + tSched.advanceTimeBy(1, SECONDS); + tSub.assertValueCount(4); + + tSched.advanceTimeBy(1, SECONDS); + tSub.assertValueCount(5); + tSub.assertComplete(); + + sched.dispose(); + } + + private Flowable<Long> asyncWork(final Scheduler sched) { + return Flowable.range(1, 5).flatMap(new Function<Integer, Flowable<Long>>() { + @Override + public Flowable<Long> apply(Integer t) { + return Flowable.timer(1, SECONDS, sched); + } + }); + } + + private Flowable<Long> syncWork(final Scheduler sched) { + return Flowable.range(1, 5).flatMap(new Function<Integer, Flowable<Long>>() { + @Override + public Flowable<Long> apply(Integer t) { + return Flowable.defer(new Supplier<Flowable<Long>>() { + @Override + public Flowable<Long> get() { + return Flowable.just(0l); + } + }).subscribeOn(sched); + } + }); + } + + private SchedulerWhen maxConcurrentScheduler(TestScheduler tSched) { + SchedulerWhen sched = new SchedulerWhen(new Function<Flowable<Flowable<Completable>>, Completable>() { + @Override + public Completable apply(Flowable<Flowable<Completable>> workerActions) { + Flowable<Completable> workers = workerActions.map(new Function<Flowable<Completable>, Completable>() { + @Override + public Completable apply(Flowable<Completable> actions) { + return Completable.concat(actions); + } + }); + return Completable.merge(workers, 2); + } + }, tSched); + return sched; + } + + private SchedulerWhen throttleScheduler(final TestScheduler tSched) { + SchedulerWhen sched = new SchedulerWhen(new Function<Flowable<Flowable<Completable>>, Completable>() { + @Override + public Completable apply(Flowable<Flowable<Completable>> workerActions) { + Flowable<Completable> workers = workerActions.map(new Function<Flowable<Completable>, Completable>() { + @Override + public Completable apply(Flowable<Completable> actions) { + return Completable.concat(actions); + } + }); + return Completable.concat(workers.map(new Function<Completable, Completable>() { + @Override + public Completable apply(Completable worker) { + return worker.delay(1, SECONDS, tSched); + } + })); + } + }, tSched); + return sched; + } + + @Test + public void raceConditions() { + Scheduler comp = Schedulers.computation(); + Scheduler limited = comp.when(new Function<Flowable<Flowable<Completable>>, Completable>() { + @Override + public Completable apply(Flowable<Flowable<Completable>> t) { + return Completable.merge(Flowable.merge(t, 10)); + } + }); + + merge(just(just(1).subscribeOn(limited).observeOn(comp)).repeat(1000)).blockingSubscribe(); + } + + @Test + public void subscribedDisposable() { + SchedulerWhen.SUBSCRIBED.dispose(); + assertFalse(SchedulerWhen.SUBSCRIBED.isDisposed()); + } + + @Test(expected = TestException.class) + public void combineCrashInConstructor() { + new SchedulerWhen(new Function<Flowable<Flowable<Completable>>, Completable>() { + @Override + public Completable apply(Flowable<Flowable<Completable>> v) + throws Exception { + throw new TestException(); + } + }, Schedulers.single()); + } + + @Test + public void disposed() { + SchedulerWhen sw = new SchedulerWhen(new Function<Flowable<Flowable<Completable>>, Completable>() { + @Override + public Completable apply(Flowable<Flowable<Completable>> v) + throws Exception { + return Completable.never(); + } + }, Schedulers.single()); + + assertFalse(sw.isDisposed()); + + sw.dispose(); + + assertTrue(sw.isDisposed()); + } + + @Test + public void scheduledActiondisposedSetRace() { + for (int i = 0; i < TestHelper.RACE_LONG_LOOPS; i++) { + final ScheduledAction sa = new ScheduledAction() { + + private static final long serialVersionUID = -672980251643733156L; + + @Override + protected Disposable callActual(Worker actualWorker, + CompletableObserver actionCompletable) { + return Disposable.empty(); + } + + }; + + assertFalse(sa.isDisposed()); + + Runnable r1 = new Runnable() { + @Override + public void run() { + sa.dispose(); + } + }; + + TestHelper.race(r1, r1); + + assertTrue(sa.isDisposed()); + } + } + + @Test + public void scheduledActionStates() { + final AtomicInteger count = new AtomicInteger(); + ScheduledAction sa = new ScheduledAction() { + + private static final long serialVersionUID = -672980251643733156L; + + @Override + protected Disposable callActual(Worker actualWorker, + CompletableObserver actionCompletable) { + count.incrementAndGet(); + return Disposable.empty(); + } + + }; + + assertFalse(sa.isDisposed()); + + sa.dispose(); + + assertTrue(sa.isDisposed()); + + sa.dispose(); + + assertTrue(sa.isDisposed()); + + // should not run when disposed + sa.call(Schedulers.single().createWorker(), null); + + assertEquals(0, count.get()); + + // should not run when already scheduled + sa.set(Disposable.empty()); + + sa.call(Schedulers.single().createWorker(), null); + + assertEquals(0, count.get()); + + // disposed while in call + sa = new ScheduledAction() { + + private static final long serialVersionUID = -672980251643733156L; + + @Override + protected Disposable callActual(Worker actualWorker, + CompletableObserver actionCompletable) { + count.incrementAndGet(); + dispose(); + return Disposable.empty(); + } + + }; + + sa.call(Schedulers.single().createWorker(), null); + + assertEquals(1, count.get()); + } + + @Test + public void onCompleteActionRunCrash() { + final AtomicInteger count = new AtomicInteger(); + + OnCompletedAction a = new OnCompletedAction(new Runnable() { + @Override + public void run() { + throw new TestException(); + } + }, new DisposableCompletableObserver() { + + @Override + public void onComplete() { + count.incrementAndGet(); + } + + @Override + public void onError(Throwable e) { + count.decrementAndGet(); + e.printStackTrace(); + } + }); + + try { + a.run(); + fail("Should have thrown"); + } catch (TestException expected) { + + } + + assertEquals(1, count.get()); + } + + @Test + public void queueWorkerDispose() { + QueueWorker qw = new QueueWorker(PublishProcessor.<ScheduledAction>create(), Schedulers.single().createWorker()); + + assertFalse(qw.isDisposed()); + + qw.dispose(); + + assertTrue(qw.isDisposed()); + + qw.dispose(); + + assertTrue(qw.isDisposed()); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/schedulers/SingleSchedulerTest.java b/src/test/java/io/reactivex/rxjava3/internal/schedulers/SingleSchedulerTest.java new file mode 100644 index 0000000000..c9f9cc5476 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/schedulers/SingleSchedulerTest.java @@ -0,0 +1,145 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.schedulers; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +import java.util.concurrent.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.Scheduler; +import io.reactivex.rxjava3.core.Scheduler.Worker; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.internal.schedulers.SingleScheduler.ScheduledWorker; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.schedulers.*; +import io.reactivex.rxjava3.testsupport.*; + +public class SingleSchedulerTest extends AbstractSchedulerTests { + + @Test + @SuppressUndeliverable + public void shutdownRejects() { + final int[] calls = { 0 }; + + Runnable r = new Runnable() { + @Override + public void run() { + calls[0]++; + } + }; + + Scheduler s = new SingleScheduler(); + s.shutdown(); + + assertEquals(Disposable.disposed(), s.scheduleDirect(r)); + + assertEquals(Disposable.disposed(), s.scheduleDirect(r, 1, TimeUnit.SECONDS)); + + assertEquals(Disposable.disposed(), s.schedulePeriodicallyDirect(r, 1, 1, TimeUnit.SECONDS)); + + Worker w = s.createWorker(); + ((ScheduledWorker)w).executor.shutdownNow(); + + assertEquals(Disposable.disposed(), w.schedule(r)); + + assertEquals(Disposable.disposed(), w.schedule(r, 1, TimeUnit.SECONDS)); + + assertEquals(Disposable.disposed(), w.schedulePeriodically(r, 1, 1, TimeUnit.SECONDS)); + + assertEquals(0, calls[0]); + + w.dispose(); + + assertTrue(w.isDisposed()); + } + + @Test + public void startRace() { + final Scheduler s = new SingleScheduler(); + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + s.shutdown(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + s.start(); + } + }; + + TestHelper.race(r1, r1); + } + } + + @Test + public void runnableDisposedAsync() throws Exception { + final Scheduler s = Schedulers.single(); + Disposable d = s.scheduleDirect(Functions.EMPTY_RUNNABLE); + + while (!d.isDisposed()) { + Thread.sleep(1); + } + } + + @Test + public void runnableDisposedAsyncCrash() throws Exception { + final Scheduler s = Schedulers.single(); + + Disposable d = s.scheduleDirect(new Runnable() { + @Override + public void run() { + throw new IllegalStateException(); + } + }); + + while (!d.isDisposed()) { + Thread.sleep(1); + } + } + + @Test + public void runnableDisposedAsyncTimed() throws Exception { + final Scheduler s = Schedulers.single(); + + Disposable d = s.scheduleDirect(Functions.EMPTY_RUNNABLE, 1, TimeUnit.MILLISECONDS); + + while (!d.isDisposed()) { + Thread.sleep(1); + } + } + + @Override protected Scheduler getScheduler() { + return Schedulers.single(); + } + + @Test + public void zeroPeriodRejectedExecution() throws Throwable { + TestHelper.withErrorTracking(errors -> { + Scheduler s = RxJavaPlugins.createSingleScheduler(new RxThreadFactory("Test")); + s.shutdown(); + Runnable run = mock(Runnable.class); + + s.schedulePeriodicallyDirect(run, 1, 0, TimeUnit.MILLISECONDS); + + Thread.sleep(100); + + verify(run, never()).run(); + + TestHelper.assertUndeliverable(errors, 0, RejectedExecutionException.class); + }); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/schedulers/TrampolineSchedulerInternalTest.java b/src/test/java/io/reactivex/rxjava3/internal/schedulers/TrampolineSchedulerInternalTest.java new file mode 100644 index 0000000000..b25620e2bf --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/schedulers/TrampolineSchedulerInternalTest.java @@ -0,0 +1,239 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.schedulers; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.core.Scheduler.Worker; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.disposables.EmptyDisposable; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.internal.schedulers.TrampolineScheduler.*; +import io.reactivex.rxjava3.schedulers.Schedulers; +import io.reactivex.rxjava3.testsupport.*; + +public class TrampolineSchedulerInternalTest extends RxJavaTest { + + @Test + @SuppressUndeliverable + public void scheduleDirectInterrupt() { + Thread.currentThread().interrupt(); + + final int[] calls = { 0 }; + + assertSame(EmptyDisposable.INSTANCE, Schedulers.trampoline().scheduleDirect(new Runnable() { + @Override + public void run() { + calls[0]++; + } + }, 1, TimeUnit.SECONDS)); + + assertTrue(Thread.interrupted()); + + assertEquals(0, calls[0]); + } + + @Test + public void dispose() { + Worker w = Schedulers.trampoline().createWorker(); + + assertFalse(w.isDisposed()); + + w.dispose(); + + assertTrue(w.isDisposed()); + + assertEquals(EmptyDisposable.INSTANCE, w.schedule(Functions.EMPTY_RUNNABLE)); + } + + @Test + public void reentrantScheduleDispose() { + final Worker w = Schedulers.trampoline().createWorker(); + try { + final int[] calls = { 0, 0 }; + w.schedule(new Runnable() { + @Override + public void run() { + calls[0]++; + w.schedule(new Runnable() { + @Override + public void run() { + calls[1]++; + } + }) + .dispose(); + } + }); + + assertEquals(1, calls[0]); + assertEquals(0, calls[1]); + } finally { + w.dispose(); + } + } + + @Test + public void reentrantScheduleShutdown() { + final Worker w = Schedulers.trampoline().createWorker(); + try { + final int[] calls = { 0, 0 }; + w.schedule(new Runnable() { + @Override + public void run() { + calls[0]++; + w.schedule(new Runnable() { + @Override + public void run() { + calls[1]++; + } + }, 1, TimeUnit.MILLISECONDS); + + w.dispose(); + } + }); + + assertEquals(1, calls[0]); + assertEquals(0, calls[1]); + } finally { + w.dispose(); + } + } + + @Test + public void reentrantScheduleShutdown2() { + final Worker w = Schedulers.trampoline().createWorker(); + try { + final int[] calls = { 0, 0 }; + w.schedule(new Runnable() { + @Override + public void run() { + calls[0]++; + w.dispose(); + + assertSame(EmptyDisposable.INSTANCE, w.schedule(new Runnable() { + @Override + public void run() { + calls[1]++; + } + }, 1, TimeUnit.MILLISECONDS)); + } + }); + + assertEquals(1, calls[0]); + assertEquals(0, calls[1]); + } finally { + w.dispose(); + } + } + + @Test + @SuppressUndeliverable + public void reentrantScheduleInterrupt() { + final Worker w = Schedulers.trampoline().createWorker(); + try { + final int[] calls = { 0 }; + Thread.currentThread().interrupt(); + w.schedule(new Runnable() { + @Override + public void run() { + calls[0]++; + } + }, 1, TimeUnit.DAYS); + + assertTrue(Thread.interrupted()); + + assertEquals(0, calls[0]); + } finally { + w.dispose(); + } + } + + @Test + public void sleepingRunnableDisposedOnRun() { + TrampolineWorker w = new TrampolineWorker(); + + Runnable r = mock(Runnable.class); + + SleepingRunnable run = new SleepingRunnable(r, w, 0); + w.dispose(); + run.run(); + + verify(r, never()).run(); + } + + @Test + public void sleepingRunnableNoDelayRun() { + TrampolineWorker w = new TrampolineWorker(); + + Runnable r = mock(Runnable.class); + + SleepingRunnable run = new SleepingRunnable(r, w, 0); + + run.run(); + + verify(r).run(); + } + + @Test + public void sleepingRunnableDisposedOnDelayedRun() { + final TrampolineWorker w = new TrampolineWorker(); + + Runnable r = mock(Runnable.class); + + SleepingRunnable run = new SleepingRunnable(r, w, System.currentTimeMillis() + 200); + + Schedulers.single().scheduleDirect(new Runnable() { + @Override + public void run() { + w.dispose(); + } + }, 100, TimeUnit.MILLISECONDS); + + run.run(); + + verify(r, never()).run(); + } + + @Test + public void submitAndDisposeNextTask() { + Scheduler.Worker w = Schedulers.trampoline().createWorker(); + + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + Runnable run = mock(Runnable.class); + + AtomicInteger sync = new AtomicInteger(2); + + w.schedule(() -> { + Disposable d = w.schedule(run); + + Schedulers.single().scheduleDirect(() -> { + if (sync.decrementAndGet() != 0) { + while (sync.get() != 0) { } + } + d.dispose(); + }); + if (sync.decrementAndGet() != 0) { + while (sync.get() != 0) { } + } + }); + } + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/subscribers/BasicFuseableConditionalSubscriberTest.java b/src/test/java/io/reactivex/rxjava3/internal/subscribers/BasicFuseableConditionalSubscriberTest.java new file mode 100644 index 0000000000..c58d015abc --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/subscribers/BasicFuseableConditionalSubscriberTest.java @@ -0,0 +1,257 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.subscribers; + +import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import org.junit.Test; +import org.reactivestreams.Subscription; + +import io.reactivex.rxjava3.annotations.*; +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.internal.subscriptions.*; +import io.reactivex.rxjava3.operators.ConditionalSubscriber; +import io.reactivex.rxjava3.operators.QueueFuseable; +import io.reactivex.rxjava3.testsupport.*; + +public class BasicFuseableConditionalSubscriberTest extends RxJavaTest { + + @Test + public void offerThrows() { + ConditionalSubscriber<Integer> cs = new ConditionalSubscriber<Integer>() { + + @Override + public void onSubscribe(Subscription s) { + } + + @Override + public void onNext(Integer t) { + } + + @Override + public void onError(Throwable t) { + } + + @Override + public void onComplete() { + } + + @Override + public boolean tryOnNext(Integer t) { + return false; + } + }; + + BasicFuseableConditionalSubscriber<Integer, Integer> fcs = new BasicFuseableConditionalSubscriber<Integer, Integer>(cs) { + + @Override + public boolean tryOnNext(Integer t) { + return false; + } + + @Override + public void onNext(Integer t) { + } + + @Override + public int requestFusion(int mode) { + return 0; + } + + @Nullable + @Override + public Integer poll() throws Exception { + return null; + } + }; + + fcs.onSubscribe(new ScalarSubscription<>(fcs, 1)); + + TestHelper.assertNoOffer(fcs); + + assertFalse(fcs.isEmpty()); + fcs.clear(); + assertTrue(fcs.isEmpty()); + } + + @Test + public void implementationStopsOnSubscribe() { + @SuppressWarnings("unchecked") + ConditionalSubscriber<Integer> ts = mock(ConditionalSubscriber.class); + + BasicFuseableConditionalSubscriber<Integer, Integer> bfs = new BasicFuseableConditionalSubscriber<Integer, Integer>(ts) { + + @Override + protected boolean beforeDownstream() { + return false; + } + + @Override + public void onNext(@NonNull Integer t) { + ts.onNext(t); + } + + @Override + public int requestFusion(int mode) { + // TODO Auto-generated method stub + return 0; + } + + @Override + public boolean tryOnNext(@NonNull Integer t) { + // TODO Auto-generated method stub + return false; + } + + @Override + public @Nullable Integer poll() throws Throwable { + return null; + } + }; + + bfs.onSubscribe(new BooleanSubscription()); + + verify(ts, never()).onSubscribe(any()); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeFlowable(f -> f + .map(v -> v) + .filter(v -> true) + ); + } + + @Test + public void transitiveBoundaryFusionNone() { + @SuppressWarnings("unchecked") + ConditionalSubscriber<Integer> ts = mock(ConditionalSubscriber.class); + + BasicFuseableConditionalSubscriber<Integer, Integer> bfs = new BasicFuseableConditionalSubscriber<Integer, Integer>(ts) { + + @Override + protected boolean beforeDownstream() { + return false; + } + + @Override + public void onNext(@NonNull Integer t) { + ts.onNext(t); + } + + @Override + public int requestFusion(int mode) { + // TODO Auto-generated method stub + return 0; + } + + @Override + public boolean tryOnNext(@NonNull Integer t) { + // TODO Auto-generated method stub + return false; + } + + @Override + public @Nullable Integer poll() throws Throwable { + return null; + } + }; + + bfs.onSubscribe(new BooleanSubscription()); + + assertEquals(QueueFuseable.NONE, bfs.transitiveBoundaryFusion(QueueFuseable.ANY)); + } + + @Test + public void transitiveBoundaryFusionAsync() { + @SuppressWarnings("unchecked") + ConditionalSubscriber<Integer> ts = mock(ConditionalSubscriber.class); + + BasicFuseableConditionalSubscriber<Integer, Integer> bfs = new BasicFuseableConditionalSubscriber<Integer, Integer>(ts) { + + @Override + protected boolean beforeDownstream() { + return false; + } + + @Override + public void onNext(@NonNull Integer t) { + ts.onNext(t); + } + + @Override + public int requestFusion(int mode) { + // TODO Auto-generated method stub + return 0; + } + + @Override + public boolean tryOnNext(@NonNull Integer t) { + // TODO Auto-generated method stub + return false; + } + + @Override + public @Nullable Integer poll() throws Throwable { + return null; + } + }; + + bfs.onSubscribe(EmptySubscription.INSTANCE); + + assertEquals(QueueFuseable.ASYNC, bfs.transitiveBoundaryFusion(QueueFuseable.ANY)); + } + + @Test + public void transitiveBoundaryFusionAsyncBoundary() { + @SuppressWarnings("unchecked") + ConditionalSubscriber<Integer> ts = mock(ConditionalSubscriber.class); + + BasicFuseableConditionalSubscriber<Integer, Integer> bfs = new BasicFuseableConditionalSubscriber<Integer, Integer>(ts) { + + @Override + protected boolean beforeDownstream() { + return false; + } + + @Override + public void onNext(@NonNull Integer t) { + ts.onNext(t); + } + + @Override + public int requestFusion(int mode) { + // TODO Auto-generated method stub + return 0; + } + + @Override + public boolean tryOnNext(@NonNull Integer t) { + // TODO Auto-generated method stub + return false; + } + + @Override + public @Nullable Integer poll() throws Throwable { + return null; + } + }; + + bfs.onSubscribe(EmptySubscription.INSTANCE); + + assertEquals(QueueFuseable.NONE, bfs.transitiveBoundaryFusion(QueueFuseable.ANY | QueueFuseable.BOUNDARY)); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/subscribers/BasicFuseableSubscriberTest.java b/src/test/java/io/reactivex/rxjava3/internal/subscribers/BasicFuseableSubscriberTest.java new file mode 100644 index 0000000000..d6f85acc46 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/subscribers/BasicFuseableSubscriberTest.java @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.subscribers; + +import static org.junit.Assert.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.annotations.*; +import io.reactivex.rxjava3.core.RxJavaTest; +import io.reactivex.rxjava3.internal.subscriptions.*; +import io.reactivex.rxjava3.subscribers.TestSubscriber; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class BasicFuseableSubscriberTest extends RxJavaTest { + + @Test + public void offerThrows() { + BasicFuseableSubscriber<Integer, Integer> fcs = new BasicFuseableSubscriber<Integer, Integer>(new TestSubscriber<>(0L)) { + + @Override + public void onNext(Integer t) { + } + + @Override + public int requestFusion(int mode) { + return 0; + } + + @Nullable + @Override + public Integer poll() throws Exception { + return null; + } + }; + + fcs.onSubscribe(new ScalarSubscription<>(fcs, 1)); + + TestHelper.assertNoOffer(fcs); + + assertFalse(fcs.isEmpty()); + fcs.clear(); + assertTrue(fcs.isEmpty()); + } + + @Test + public void implementationStopsOnSubscribe() { + TestSubscriber<Integer> ts = new TestSubscriber<>(); + BasicFuseableSubscriber<Integer, Integer> bfs = new BasicFuseableSubscriber<Integer, Integer>(ts) { + + @Override + protected boolean beforeDownstream() { + return false; + } + + @Override + public void onNext(@NonNull Integer t) { + ts.onNext(t); + } + + @Override + public int requestFusion(int mode) { + // TODO Auto-generated method stub + return 0; + } + + @Override + public @Nullable Integer poll() throws Throwable { + return null; + } + }; + + bfs.onSubscribe(new BooleanSubscription()); + + assertFalse(ts.hasSubscription()); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/subscribers/BlockingSubscriberTest.java b/src/test/java/io/reactivex/rxjava3/internal/subscribers/BlockingSubscriberTest.java new file mode 100644 index 0000000000..a97f4ee93e --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/subscribers/BlockingSubscriberTest.java @@ -0,0 +1,133 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.subscribers; + +import static org.junit.Assert.*; + +import java.util.ArrayDeque; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.junit.Test; +import org.reactivestreams.Subscription; + +import io.reactivex.rxjava3.core.RxJavaTest; +import io.reactivex.rxjava3.internal.subscriptions.BooleanSubscription; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class BlockingSubscriberTest extends RxJavaTest { + + @Test + public void doubleOnSubscribe() { + TestHelper.doubleOnSubscribe(new BlockingSubscriber<Integer>(new ArrayDeque<>())); + } + + @Test + public void cancel() { + BlockingSubscriber<Integer> bq = new BlockingSubscriber<>(new ArrayDeque<>()); + + assertFalse(bq.isCancelled()); + + bq.cancel(); + + assertTrue(bq.isCancelled()); + + bq.cancel(); + + assertTrue(bq.isCancelled()); + } + + @Test + public void blockingFirstDoubleOnSubscribe() { + TestHelper.doubleOnSubscribe(new BlockingFirstSubscriber<Integer>()); + } + + @Test + public void blockingFirstTimeout() { + BlockingFirstSubscriber<Integer> bf = new BlockingFirstSubscriber<>(); + + Thread.currentThread().interrupt(); + + try { + bf.blockingGet(); + fail("Should have thrown!"); + } catch (RuntimeException ex) { + assertTrue(ex.toString(), ex.getCause() instanceof InterruptedException); + } + } + + @Test + public void blockingFirstTimeout2() { + BlockingFirstSubscriber<Integer> bf = new BlockingFirstSubscriber<>(); + + bf.onSubscribe(new BooleanSubscription()); + + Thread.currentThread().interrupt(); + + try { + bf.blockingGet(); + fail("Should have thrown!"); + } catch (RuntimeException ex) { + assertTrue(ex.toString(), ex.getCause() instanceof InterruptedException); + } + } + + @Test + public void cancelOnRequest() { + + final BlockingFirstSubscriber<Integer> bf = new BlockingFirstSubscriber<>(); + + final AtomicBoolean b = new AtomicBoolean(); + + Subscription s = new Subscription() { + @Override + public void request(long n) { + bf.cancelled = true; + } + + @Override + public void cancel() { + b.set(true); + } + }; + + bf.onSubscribe(s); + + assertTrue(b.get()); + } + + @Test + public void cancelUpfront() { + + final BlockingFirstSubscriber<Integer> bf = new BlockingFirstSubscriber<>(); + + final AtomicBoolean b = new AtomicBoolean(); + + bf.cancelled = true; + + Subscription s = new Subscription() { + @Override + public void request(long n) { + b.set(true); + } + + @Override + public void cancel() { + } + }; + + bf.onSubscribe(s); + + assertFalse(b.get()); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/subscribers/BoundedSubscriberTest.java b/src/test/java/io/reactivex/rxjava3/internal/subscribers/BoundedSubscriberTest.java new file mode 100644 index 0000000000..68e955097d --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/subscribers/BoundedSubscriberTest.java @@ -0,0 +1,415 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.subscribers; + +import static org.junit.Assert.*; + +import java.util.*; + +import org.junit.Test; +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.internal.subscriptions.BooleanSubscription; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.processors.PublishProcessor; +import io.reactivex.rxjava3.testsupport.*; + +public class BoundedSubscriberTest extends RxJavaTest { + + @Test + public void onSubscribeThrows() { + final List<Object> received = new ArrayList<>(); + + BoundedSubscriber<Object> subscriber = new BoundedSubscriber<>(new Consumer<Object>() { + @Override + public void accept(Object o) throws Exception { + received.add(o); + } + }, new Consumer<Throwable>() { + @Override + public void accept(Throwable throwable) throws Exception { + received.add(throwable); + } + }, new Action() { + @Override + public void run() throws Exception { + received.add(1); + } + }, new Consumer<Subscription>() { + @Override + public void accept(Subscription subscription) throws Exception { + throw new TestException(); + } + }, 128); + + assertFalse(subscriber.isDisposed()); + + Flowable.just(1).subscribe(subscriber); + + assertTrue(received.toString(), received.get(0) instanceof TestException); + assertEquals(received.toString(), 1, received.size()); + + assertTrue(subscriber.isDisposed()); + } + + @Test + public void onNextThrows() { + final List<Object> received = new ArrayList<>(); + + BoundedSubscriber<Object> subscriber = new BoundedSubscriber<>(new Consumer<Object>() { + @Override + public void accept(Object o) throws Exception { + throw new TestException(); + } + }, new Consumer<Throwable>() { + @Override + public void accept(Throwable throwable) throws Exception { + received.add(throwable); + } + }, new Action() { + @Override + public void run() throws Exception { + received.add(1); + } + }, new Consumer<Subscription>() { + @Override + public void accept(Subscription subscription) throws Exception { + subscription.request(128); + } + }, 128); + + assertFalse(subscriber.isDisposed()); + + Flowable.just(1).subscribe(subscriber); + + assertTrue(received.toString(), received.get(0) instanceof TestException); + assertEquals(received.toString(), 1, received.size()); + + assertTrue(subscriber.isDisposed()); + } + + @Test + public void onErrorThrows() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + + try { + final List<Object> received = new ArrayList<>(); + + BoundedSubscriber<Object> subscriber = new BoundedSubscriber<>(new Consumer<Object>() { + @Override + public void accept(Object o) throws Exception { + received.add(o); + } + }, new Consumer<Throwable>() { + @Override + public void accept(Throwable throwable) throws Exception { + throw new TestException("Inner"); + } + }, new Action() { + @Override + public void run() throws Exception { + received.add(1); + } + }, new Consumer<Subscription>() { + @Override + public void accept(Subscription subscription) throws Exception { + subscription.request(128); + } + }, 128); + + assertFalse(subscriber.isDisposed()); + + Flowable.<Integer>error(new TestException("Outer")).subscribe(subscriber); + + assertTrue(received.toString(), received.isEmpty()); + + assertTrue(subscriber.isDisposed()); + + TestHelper.assertError(errors, 0, CompositeException.class); + List<Throwable> ce = TestHelper.compositeList(errors.get(0)); + TestHelper.assertError(ce, 0, TestException.class, "Outer"); + TestHelper.assertError(ce, 1, TestException.class, "Inner"); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void onCompleteThrows() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + + try { + final List<Object> received = new ArrayList<>(); + + BoundedSubscriber<Object> subscriber = new BoundedSubscriber<>(new Consumer<Object>() { + @Override + public void accept(Object o) throws Exception { + received.add(o); + } + }, new Consumer<Throwable>() { + @Override + public void accept(Throwable throwable) throws Exception { + received.add(throwable); + } + }, new Action() { + @Override + public void run() throws Exception { + throw new TestException(); + } + }, new Consumer<Subscription>() { + @Override + public void accept(Subscription subscription) throws Exception { + subscription.request(128); + } + }, 128); + + assertFalse(subscriber.isDisposed()); + + Flowable.<Integer>empty().subscribe(subscriber); + + assertTrue(received.toString(), received.isEmpty()); + + assertTrue(subscriber.isDisposed()); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void onNextThrowsCancelsUpstream() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + + final List<Throwable> errors = new ArrayList<>(); + + BoundedSubscriber<Integer> s = new BoundedSubscriber<>(new Consumer<Integer>() { + @Override + public void accept(Integer v) throws Exception { + throw new TestException(); + } + }, new Consumer<Throwable>() { + @Override + public void accept(Throwable e) throws Exception { + errors.add(e); + } + }, new Action() { + @Override + public void run() throws Exception { + + } + }, new Consumer<Subscription>() { + @Override + public void accept(Subscription subscription) throws Exception { + subscription.request(128); + } + }, 128); + + pp.subscribe(s); + + assertTrue("No observers?!", pp.hasSubscribers()); + assertTrue("Has errors already?!", errors.isEmpty()); + + pp.onNext(1); + + assertFalse("Has observers?!", pp.hasSubscribers()); + assertFalse("No errors?!", errors.isEmpty()); + + assertTrue(errors.toString(), errors.get(0) instanceof TestException); + } + + @Test + public void onSubscribeThrowsCancelsUpstream() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + + final List<Throwable> errors = new ArrayList<>(); + + BoundedSubscriber<Integer> s = new BoundedSubscriber<>(new Consumer<Integer>() { + @Override + public void accept(Integer v) throws Exception { + } + }, new Consumer<Throwable>() { + @Override + public void accept(Throwable e) throws Exception { + errors.add(e); + } + }, new Action() { + @Override + public void run() throws Exception { + } + }, new Consumer<Subscription>() { + @Override + public void accept(Subscription s) throws Exception { + throw new TestException(); + } + }, 128); + + pp.subscribe(s); + + assertFalse("Has observers?!", pp.hasSubscribers()); + assertFalse("No errors?!", errors.isEmpty()); + + assertTrue(errors.toString(), errors.get(0) instanceof TestException); + } + + @Test + public void badSourceOnSubscribe() { + Flowable<Integer> source = Flowable.fromPublisher(new Publisher<Integer>() { + @Override + public void subscribe(Subscriber<? super Integer> s) { + BooleanSubscription s1 = new BooleanSubscription(); + s.onSubscribe(s1); + BooleanSubscription s2 = new BooleanSubscription(); + s.onSubscribe(s2); + + assertFalse(s1.isCancelled()); + assertTrue(s2.isCancelled()); + + s.onNext(1); + s.onComplete(); + } + }); + + final List<Object> received = new ArrayList<>(); + + BoundedSubscriber<Object> subscriber = new BoundedSubscriber<>(new Consumer<Object>() { + @Override + public void accept(Object v) throws Exception { + received.add(v); + } + }, new Consumer<Throwable>() { + @Override + public void accept(Throwable e) throws Exception { + received.add(e); + } + }, new Action() { + @Override + public void run() throws Exception { + received.add(100); + } + }, new Consumer<Subscription>() { + @Override + public void accept(Subscription s) throws Exception { + s.request(128); + } + }, 128); + + source.subscribe(subscriber); + + assertEquals(Arrays.asList(1, 100), received); + } + + @Test + @SuppressUndeliverable + public void badSourceEmitAfterDone() { + Flowable<Integer> source = Flowable.fromPublisher(new Publisher<Integer>() { + @Override + public void subscribe(Subscriber<? super Integer> s) { + BooleanSubscription s1 = new BooleanSubscription(); + s.onSubscribe(s1); + + s.onNext(1); + s.onComplete(); + s.onNext(2); + s.onError(new TestException()); + s.onComplete(); + } + }); + + final List<Object> received = new ArrayList<>(); + + BoundedSubscriber<Object> subscriber = new BoundedSubscriber<>(new Consumer<Object>() { + @Override + public void accept(Object v) throws Exception { + received.add(v); + } + }, new Consumer<Throwable>() { + @Override + public void accept(Throwable e) throws Exception { + received.add(e); + } + }, new Action() { + @Override + public void run() throws Exception { + received.add(100); + } + }, new Consumer<Subscription>() { + @Override + public void accept(Subscription s) throws Exception { + s.request(128); + } + }, 128); + + source.subscribe(subscriber); + + assertEquals(Arrays.asList(1, 100), received); + } + + @Test + public void onErrorMissingShouldReportNoCustomOnError() { + BoundedSubscriber<Integer> subscriber = new BoundedSubscriber<>(Functions.<Integer>emptyConsumer(), + Functions.ON_ERROR_MISSING, + Functions.EMPTY_ACTION, + Functions.<Subscription>boundedConsumer(128), 128); + + assertFalse(subscriber.hasCustomOnError()); + } + + @Test + public void customOnErrorShouldReportCustomOnError() { + BoundedSubscriber<Integer> subscriber = new BoundedSubscriber<>(Functions.<Integer>emptyConsumer(), + Functions.<Throwable>emptyConsumer(), + Functions.EMPTY_ACTION, + Functions.<Subscription>boundedConsumer(128), 128); + + assertTrue(subscriber.hasCustomOnError()); + } + + @Test + public void cancel() { + BoundedSubscriber<Integer> subscriber = new BoundedSubscriber<>(Functions.<Integer>emptyConsumer(), + Functions.<Throwable>emptyConsumer(), + Functions.EMPTY_ACTION, + Functions.<Subscription>boundedConsumer(128), 128); + + BooleanSubscription bs = new BooleanSubscription(); + subscriber.onSubscribe(bs); + + subscriber.cancel(); + + assertTrue(bs.isCancelled()); + } + + @Test + public void dispose() { + BoundedSubscriber<Integer> subscriber = new BoundedSubscriber<>(Functions.<Integer>emptyConsumer(), + Functions.<Throwable>emptyConsumer(), + Functions.EMPTY_ACTION, + Functions.<Subscription>boundedConsumer(128), 128); + + BooleanSubscription bs = new BooleanSubscription(); + subscriber.onSubscribe(bs); + + assertFalse(subscriber.isDisposed()); + + subscriber.dispose(); + + assertTrue(bs.isCancelled()); + assertTrue(subscriber.isDisposed()); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/subscribers/DeferredScalarSubscriberTest.java b/src/test/java/io/reactivex/rxjava3/internal/subscribers/DeferredScalarSubscriberTest.java new file mode 100644 index 0000000000..dcfcb82377 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/subscribers/DeferredScalarSubscriberTest.java @@ -0,0 +1,435 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.subscribers; + +import static org.junit.Assert.*; + +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.Test; +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.core.Scheduler.Worker; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.internal.subscriptions.BooleanSubscription; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.processors.PublishProcessor; +import io.reactivex.rxjava3.schedulers.Schedulers; +import io.reactivex.rxjava3.subscribers.TestSubscriber; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class DeferredScalarSubscriberTest extends RxJavaTest { + + @Test + public void completeFirst() { + TestSubscriber<Integer> ts = TestSubscriber.create(0L); + TestingDeferredScalarSubscriber ds = new TestingDeferredScalarSubscriber(ts); + ds.setupDownstream(); + + ds.onNext(1); + + ts.assertNoValues(); + + ds.onComplete(); + + ts.assertNoValues(); + + ts.request(1); + + ts.assertValues(1); + ts.assertNoErrors(); + ts.assertComplete(); + } + + @Test + public void requestFirst() { + TestSubscriber<Integer> ts = TestSubscriber.create(1); + TestingDeferredScalarSubscriber ds = new TestingDeferredScalarSubscriber(ts); + ds.setupDownstream(); + + ds.onNext(1); + + ts.assertNoValues(); + + ds.onComplete(); + + ts.assertValues(1); + ts.assertNoErrors(); + ts.assertComplete(); + } + + @Test + public void empty() { + TestSubscriber<Integer> ts = TestSubscriber.create(0L); + TestingDeferredScalarSubscriber ds = new TestingDeferredScalarSubscriber(ts); + ds.setupDownstream(); + + ts.assertNoValues(); + + ds.onComplete(); + + ts.assertNoValues(); + ts.assertNoErrors(); + ts.assertComplete(); + } + + @Test + public void error() { + TestSubscriber<Integer> ts = TestSubscriber.create(0L); + TestingDeferredScalarSubscriber ds = new TestingDeferredScalarSubscriber(ts); + ds.setupDownstream(); + + ts.assertNoValues(); + + ds.onError(new TestException()); + + ts.assertNoValues(); + ts.assertError(TestException.class); + ts.assertNotComplete(); + } + + @Test + public void unsubscribeComposes() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + TestSubscriber<Integer> ts = TestSubscriber.create(0L); + TestingDeferredScalarSubscriber ds = new TestingDeferredScalarSubscriber(ts); + + pp.subscribe(ds); + + assertTrue("No subscribers?", pp.hasSubscribers()); + + ts.cancel(); + + ds.onNext(1); + ds.onComplete(); + + ts.request(1); + + ts.assertNoValues(); + ts.assertNoErrors(); + ts.assertNotComplete(); + + assertFalse("Subscribers?", pp.hasSubscribers()); + assertTrue("Deferred not unsubscribed?", ds.isCancelled()); + } + + @Test + public void emptySource() { + TestSubscriber<Integer> ts = TestSubscriber.create(0L); + TestingDeferredScalarSubscriber ds = new TestingDeferredScalarSubscriber(ts); + Flowable.just(1).ignoreElements().<Integer>toFlowable().subscribe(ds); // we need a producer from upstream + + ts.assertNoValues(); + + ts.assertNoValues(); + ts.assertNoErrors(); + ts.assertComplete(); + } + + @Test + public void justSource() { + TestSubscriber<Integer> ts = TestSubscriber.create(0L); + TestingDeferredScalarSubscriber ds = new TestingDeferredScalarSubscriber(ts); + ds.subscribeTo(Flowable.just(1)); + + ts.assertNoValues(); + + ts.request(1); + + ts.assertValue(1); + ts.assertNoErrors(); + ts.assertComplete(); + } + + @Test + public void rangeSource() { + TestSubscriber<Integer> ts = TestSubscriber.create(0); + TestingDeferredScalarSubscriber ds = new TestingDeferredScalarSubscriber(ts); + ds.subscribeTo(Flowable.range(1, 10)); + + ts.assertNoValues(); + + ts.request(1); + + ts.assertValue(10); + ts.assertNoErrors(); + ts.assertComplete(); + } + + @Test + public void completeAfterNext() { + TestSubscriber<Integer> ts = new TestSubscriber<Integer>() { + @Override + public void onNext(Integer t) { + super.onNext(t); + cancel(); + } + }; + TestingDeferredScalarSubscriber ds = new TestingDeferredScalarSubscriber(ts); + ds.setupDownstream(); + ds.onNext(1); + + ts.assertNoValues(); + + ds.onComplete(); + + ts.assertValue(1); + ts.assertNoErrors(); + ts.assertNotComplete(); + } + + @Test + public void completeAfterNextViaRequest() { + TestSubscriber<Integer> ts = new TestSubscriber<Integer>(0L) { + @Override + public void onNext(Integer t) { + super.onNext(t); + cancel(); + } + }; + TestingDeferredScalarSubscriber ds = new TestingDeferredScalarSubscriber(ts); + ds.setupDownstream(); + ds.onNext(1); + ds.onComplete(); + + ts.assertNoValues(); + + ts.request(1); + + ts.assertValue(1); + ts.assertNoErrors(); + ts.assertNotComplete(); + } + + @Test + public void doubleComplete() { + TestSubscriber<Integer> ts = TestSubscriber.create(0); + TestingDeferredScalarSubscriber ds = new TestingDeferredScalarSubscriber(ts); + ds.setupDownstream(); + + ds.onNext(1); + + ts.request(1); + + ds.onComplete(); + ds.onComplete(); + + ts.assertValue(1); + ts.assertNoErrors(); + ts.assertComplete(); + } + + @Test + public void doubleComplete2() { + TestSubscriber<Integer> ts = TestSubscriber.create(0); + TestingDeferredScalarSubscriber ds = new TestingDeferredScalarSubscriber(ts); + ds.setupDownstream(); + + ds.onNext(1); + + ds.onComplete(); + ds.onComplete(); + + ts.request(1); + + ts.assertValue(1); + ts.assertNoErrors(); + ts.assertComplete(); + } + + @Test + public void doubleRequest() { + TestSubscriber<Integer> ts = TestSubscriber.create(0); + TestingDeferredScalarSubscriber ds = new TestingDeferredScalarSubscriber(ts); + ds.setupDownstream(); + + ds.onNext(1); + + ts.request(1); + ts.request(1); + + ds.onComplete(); + + ts.assertValue(1); + ts.assertNoErrors(); + ts.assertComplete(); + } + + @Test + public void negativeRequest() { + List<Throwable> list = TestHelper.trackPluginErrors(); + + TestSubscriber<Integer> ts = TestSubscriber.create(0); + TestingDeferredScalarSubscriber ds = new TestingDeferredScalarSubscriber(ts); + ds.setupDownstream(); + + ds.downstreamRequest(-99); + + RxJavaPlugins.reset(); + TestHelper.assertError(list, 0, IllegalArgumentException.class, "n > 0 required but it was -99"); + } + + @Test + public void callsAfterUnsubscribe() { + TestSubscriber<Integer> ts = TestSubscriber.create(0); + TestingDeferredScalarSubscriber ds = new TestingDeferredScalarSubscriber(ts); + ds.setupDownstream(); + + ts.cancel(); + + ds.downstreamRequest(1); + ds.onNext(1); + ds.onComplete(); + ds.onComplete(); + + ts.assertNoValues(); + ts.assertNoErrors(); + ts.assertNotComplete(); + } + + @Test + public void emissionRequestRace() { + Worker w = Schedulers.computation().createWorker(); + try { + for (int i = 0; i < 10000; i++) { + + final TestSubscriber<Integer> ts = TestSubscriber.create(0L); + TestingDeferredScalarSubscriber ds = new TestingDeferredScalarSubscriber(ts); + ds.setupDownstream(); + ds.onNext(1); + + final AtomicInteger ready = new AtomicInteger(2); + + w.schedule(new Runnable() { + @Override + public void run() { + ready.decrementAndGet(); + while (ready.get() != 0) { } + + ts.request(1); + } + }); + + ready.decrementAndGet(); + while (ready.get() != 0) { } + + ds.onComplete(); + + ts.awaitDone(5, TimeUnit.SECONDS); + ts.assertValues(1); + ts.assertNoErrors(); + ts.assertComplete(); + + } + } finally { + w.dispose(); + } + } + + @Test + public void emissionRequestRace2() { + Worker w = Schedulers.io().createWorker(); + Worker w2 = Schedulers.io().createWorker(); + int m = 10000; + if (Runtime.getRuntime().availableProcessors() < 3) { + m = 1000; + } + try { + for (int i = 0; i < m; i++) { + + final TestSubscriber<Integer> ts = TestSubscriber.create(0L); + TestingDeferredScalarSubscriber ds = new TestingDeferredScalarSubscriber(ts); + ds.setupDownstream(); + ds.onNext(1); + + final AtomicInteger ready = new AtomicInteger(3); + + w.schedule(new Runnable() { + @Override + public void run() { + ready.decrementAndGet(); + while (ready.get() != 0) { } + + ts.request(1); + } + }); + + w2.schedule(new Runnable() { + @Override + public void run() { + ready.decrementAndGet(); + while (ready.get() != 0) { } + + ts.request(1); + } + }); + + ready.decrementAndGet(); + while (ready.get() != 0) { } + + ds.onComplete(); + + ts.awaitDone(5, TimeUnit.SECONDS); + ts.assertValues(1); + ts.assertNoErrors(); + ts.assertComplete(); + + } + } finally { + w.dispose(); + w2.dispose(); + } + } + + static final class TestingDeferredScalarSubscriber extends DeferredScalarSubscriber<Integer, Integer> { + + private static final long serialVersionUID = 6285096158319517837L; + + TestingDeferredScalarSubscriber(Subscriber<? super Integer> downstream) { + super(downstream); + } + + @Override + public void onNext(Integer t) { + value = t; + hasValue = true; + } + + public void setupDownstream() { + onSubscribe(new BooleanSubscription()); + } + + public void subscribeTo(Publisher<Integer> p) { + p.subscribe(this); + } + + public void downstreamRequest(long n) { + request(n); + } + } + + @Test + public void doubleOnSubscribe() { + TestHelper.doubleOnSubscribe(new DeferredScalarSubscriber<Integer, Integer>(new TestSubscriber<>()) { + private static final long serialVersionUID = -4445381578878059054L; + + @Override + public void onNext(Integer t) { + } + }); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/subscribers/EmptyComponentTest.java b/src/test/java/io/reactivex/rxjava3/internal/subscribers/EmptyComponentTest.java new file mode 100644 index 0000000000..c5cf3c88ca --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/subscribers/EmptyComponentTest.java @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.subscribers; + +import static org.junit.Assert.assertTrue; + +import java.util.List; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.RxJavaTest; +import io.reactivex.rxjava3.disposables.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.internal.subscriptions.BooleanSubscription; +import io.reactivex.rxjava3.internal.util.EmptyComponent; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class EmptyComponentTest extends RxJavaTest { + + @Test + public void normal() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + + try { + TestHelper.checkEnum(EmptyComponent.class); + + EmptyComponent c = EmptyComponent.INSTANCE; + + assertTrue(c.isDisposed()); + + c.request(10); + + c.request(-10); + + Disposable d = Disposable.empty(); + + c.onSubscribe(d); + + assertTrue(d.isDisposed()); + + BooleanSubscription s = new BooleanSubscription(); + + c.onSubscribe(s); + + assertTrue(s.isCancelled()); + + c.onNext(null); + + c.onNext(1); + + c.onComplete(); + + c.onError(new TestException()); + + c.onSuccess(2); + + c.cancel(); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/subscribers/FlowableConsumersTest.java b/src/test/java/io/reactivex/rxjava3/internal/subscribers/FlowableConsumersTest.java new file mode 100644 index 0000000000..cc7083cde7 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/subscribers/FlowableConsumersTest.java @@ -0,0 +1,328 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +/* + * Copyright 2016-2019 David Karnok + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.reactivex.rxjava3.internal.subscribers; + +import static org.junit.Assert.*; + +import java.io.IOException; +import java.util.*; + +import org.junit.Test; +import org.reactivestreams.Subscriber; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.*; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.internal.subscriptions.BooleanSubscription; +import io.reactivex.rxjava3.observers.LambdaConsumerIntrospection; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.processors.PublishProcessor; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class FlowableConsumersTest implements Consumer<Object>, Action { + + final CompositeDisposable composite = new CompositeDisposable(); + + final PublishProcessor<Integer> processor = PublishProcessor.create(); + + final List<Object> events = new ArrayList<>(); + + @Override + public void run() throws Exception { + events.add("OnComplete"); + } + + @Override + public void accept(Object t) throws Exception { + events.add(t); + } + + static <T> Disposable subscribeAutoDispose(Flowable<T> source, CompositeDisposable composite, + Consumer<? super T> onNext, Consumer<? super Throwable> onError, Action onComplete) { + return source.subscribe(onNext, onError, onComplete, composite); + } + + @Test + public void onNextNormal() { + + Disposable d = subscribeAutoDispose(processor, composite, this, Functions.ON_ERROR_MISSING, () -> { }); + + assertFalse(d.getClass().toString(), ((LambdaConsumerIntrospection)d).hasCustomOnError()); + + assertTrue(composite.size() > 0); + + assertTrue(events.toString(), events.isEmpty()); + + processor.onNext(1); + + assertTrue(composite.size() > 0); + + assertEquals(Arrays.<Object>asList(1), events); + + processor.onComplete(); + + assertEquals(Arrays.<Object>asList(1), events); + + assertEquals(0, composite.size()); + } + + @Test + public void onErrorNormal() { + + subscribeAutoDispose(processor, composite, this, this, () -> { }); + + assertTrue(composite.size() > 0); + + assertTrue(events.toString(), events.isEmpty()); + + processor.onNext(1); + + assertTrue(composite.size() > 0); + + assertEquals(Arrays.<Object>asList(1), events); + + processor.onComplete(); + + assertEquals(Arrays.<Object>asList(1), events); + + assertEquals(0, composite.size()); + } + + @Test + public void onErrorError() { + + Disposable d = subscribeAutoDispose(processor, composite, this, this, this); + + assertTrue(d.getClass().toString(), ((LambdaConsumerIntrospection)d).hasCustomOnError()); + + assertTrue(composite.size() > 0); + + assertTrue(events.toString(), events.isEmpty()); + + processor.onNext(1); + + assertTrue(composite.size() > 0); + + assertEquals(Arrays.<Object>asList(1), events); + + processor.onError(new IOException()); + + assertEquals(events.toString(), 1, events.get(0)); + assertTrue(events.toString(), events.get(1) instanceof IOException); + + assertEquals(0, composite.size()); + } + + @Test + public void onCompleteNormal() { + + subscribeAutoDispose(processor, composite, this, this, this); + + assertTrue(composite.size() > 0); + + assertTrue(events.toString(), events.isEmpty()); + + processor.onNext(1); + + assertTrue(composite.size() > 0); + + assertEquals(Arrays.<Object>asList(1), events); + + processor.onComplete(); + + assertEquals(Arrays.<Object>asList(1, "OnComplete"), events); + + assertEquals(0, composite.size()); + } + + @Test + public void onCompleteError() { + + subscribeAutoDispose(processor, composite, this, this, this); + + assertTrue(composite.size() > 0); + + assertTrue(events.toString(), events.isEmpty()); + + processor.onNext(1); + + assertTrue(composite.size() > 0); + + assertEquals(Arrays.<Object>asList(1), events); + + processor.onError(new IOException()); + + assertEquals(events.toString(), 1, events.get(0)); + assertTrue(events.toString(), events.get(1) instanceof IOException); + + assertEquals(0, composite.size()); + } + + @Test + public void onCompleteDispose() { + + Disposable d = subscribeAutoDispose(processor, composite, this, this, this); + + assertTrue(composite.size() > 0); + + assertTrue(events.toString(), events.isEmpty()); + + assertFalse(d.isDisposed()); + + d.dispose(); + d.dispose(); + + assertTrue(d.isDisposed()); + + assertEquals(0, composite.size()); + + assertFalse(processor.hasSubscribers()); + } + + @Test + public void onNextCrash() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + subscribeAutoDispose(processor, composite, new Consumer<Object>() { + @Override + public void accept(Object t) throws Exception { + throw new IOException(); + } + }, this, this); + + processor.onNext(1); + + assertTrue(errors.toString(), errors.isEmpty()); + + assertTrue(events.toString(), events.get(0) instanceof IOException); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void onNextCrashOnError() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + subscribeAutoDispose(processor, composite, this, new Consumer<Throwable>() { + @Override + public void accept(Throwable t) throws Exception { + throw new IOException(t); + } + }, this); + + processor.onError(new IllegalArgumentException()); + + assertTrue(events.toString(), events.isEmpty()); + + TestHelper.assertError(errors, 0, CompositeException.class); + List<Throwable> inners = TestHelper.compositeList(errors.get(0)); + TestHelper.assertError(inners, 0, IllegalArgumentException.class); + TestHelper.assertError(inners, 1, IOException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void onNextCrashNoError() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + subscribeAutoDispose(processor, composite, new Consumer<Object>() { + @Override + public void accept(Object t) throws Exception { + throw new IOException(); + } + }, Functions.ON_ERROR_MISSING, () -> { }); + + processor.onNext(1); + + assertTrue(events.toString(), events.isEmpty()); + + TestHelper.assertError(errors, 0, OnErrorNotImplementedException.class); + assertTrue(errors.get(0).getCause() instanceof IOException); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void onCompleteCrash() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + subscribeAutoDispose(processor, composite, this, this, new Action() { + @Override + public void run() throws Exception { + throw new IOException(); + } + }); + + processor.onNext(1); + processor.onComplete(); + + assertEquals(Arrays.asList(1), events); + + TestHelper.assertUndeliverable(errors, 0, IOException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void badSource() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + subscribeAutoDispose( + new Flowable<Integer>() { + @Override + protected void subscribeActual( + Subscriber<? super Integer> s) { + s.onSubscribe(new BooleanSubscription()); + s.onNext(1); + s.onComplete(); + + s.onSubscribe(new BooleanSubscription()); + s.onNext(2); + s.onComplete(); + s.onError(new IOException()); + } + }, composite, this, this, this + ); + + assertEquals(Arrays.<Object>asList(1, "OnComplete"), events); + + TestHelper.assertUndeliverable(errors, 0, IOException.class); + } finally { + RxJavaPlugins.reset(); + } + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/subscribers/FutureSubscriberTest.java b/src/test/java/io/reactivex/rxjava3/internal/subscribers/FutureSubscriberTest.java new file mode 100644 index 0000000000..56de26e5d6 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/subscribers/FutureSubscriberTest.java @@ -0,0 +1,317 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.subscribers; + +import static io.reactivex.rxjava3.internal.util.ExceptionHelper.timeoutMessage; +import static org.junit.Assert.*; + +import java.util.*; +import java.util.concurrent.*; + +import org.junit.*; + +import io.reactivex.rxjava3.core.RxJavaTest; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.internal.subscriptions.BooleanSubscription; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.schedulers.Schedulers; +import io.reactivex.rxjava3.testsupport.*; + +public class FutureSubscriberTest extends RxJavaTest { + + FutureSubscriber<Integer> fs; + + @Before + public void before() { + fs = new FutureSubscriber<>(); + } + + @Test + public void cancel() throws Exception { + assertFalse(fs.isDone()); + + assertFalse(fs.isCancelled()); + + fs.cancel(); + + fs.cancel(); + + fs.request(10); + + fs.request(-99); + + fs.cancel(false); + + assertTrue(fs.isDone()); + + assertTrue(fs.isCancelled()); + + try { + fs.get(); + fail("Should have thrown"); + } catch (CancellationException ex) { + // expected + } + + try { + fs.get(1, TimeUnit.MILLISECONDS); + fail("Should have thrown"); + } catch (CancellationException ex) { + // expected + } + } + + @Test + public void onError() throws Exception { + List<Throwable> errors = TestHelper.trackPluginErrors(); + + try { + fs.onError(new TestException("One")); + + fs.onError(new TestException("Two")); + + try { + fs.get(5, TimeUnit.MILLISECONDS); + } catch (ExecutionException ex) { + assertTrue(ex.toString(), ex.getCause() instanceof TestException); + assertEquals("One", ex.getCause().getMessage()); + } + + TestHelper.assertUndeliverable(errors, 0, TestException.class, "Two"); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void onNext() throws Exception { + fs.onNext(1); + fs.onComplete(); + + assertEquals(1, fs.get(5, TimeUnit.MILLISECONDS).intValue()); + } + + @Test + public void onSubscribe() throws Exception { + List<Throwable> errors = TestHelper.trackPluginErrors(); + + try { + + BooleanSubscription s = new BooleanSubscription(); + + fs.onSubscribe(s); + + BooleanSubscription s2 = new BooleanSubscription(); + + fs.onSubscribe(s2); + + assertFalse(s.isCancelled()); + assertTrue(s2.isCancelled()); + + TestHelper.assertError(errors, 0, IllegalStateException.class, "Subscription already set!"); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void cancelRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final FutureSubscriber<Integer> fs = new FutureSubscriber<>(); + + Runnable r = new Runnable() { + @Override + public void run() { + fs.cancel(false); + } + }; + + TestHelper.race(r, r); + } + } + + @Test + public void await() throws Exception { + Schedulers.single().scheduleDirect(new Runnable() { + @Override + public void run() { + fs.onNext(1); + fs.onComplete(); + } + }, 100, TimeUnit.MILLISECONDS); + + assertEquals(1, fs.get(5, TimeUnit.SECONDS).intValue()); + } + + @Test + @SuppressUndeliverable + public void onErrorCancelRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final FutureSubscriber<Integer> fs = new FutureSubscriber<>(); + + final TestException ex = new TestException(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + fs.cancel(false); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + fs.onError(ex); + } + }; + + TestHelper.race(r1, r2); + } + } + + @Test + @SuppressUndeliverable + public void onCompleteCancelRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final FutureSubscriber<Integer> fs = new FutureSubscriber<>(); + + if (i % 3 == 0) { + fs.onSubscribe(new BooleanSubscription()); + } + + if (i % 2 == 0) { + fs.onNext(1); + } + + Runnable r1 = new Runnable() { + @Override + public void run() { + fs.cancel(false); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + fs.onComplete(); + } + }; + + TestHelper.race(r1, r2); + } + } + + @Test + @SuppressUndeliverable + public void onErrorOnComplete() throws Exception { + fs.onError(new TestException("One")); + fs.onComplete(); + + try { + fs.get(5, TimeUnit.MILLISECONDS); + } catch (ExecutionException ex) { + assertTrue(ex.toString(), ex.getCause() instanceof TestException); + assertEquals("One", ex.getCause().getMessage()); + } + } + + @Test + @SuppressUndeliverable + public void onCompleteOnError() throws Exception { + fs.onComplete(); + fs.onError(new TestException("One")); + + try { + assertNull(fs.get(5, TimeUnit.MILLISECONDS)); + } catch (ExecutionException ex) { + assertTrue(ex.toString(), ex.getCause() instanceof NoSuchElementException); + } + } + + @Test + @SuppressUndeliverable + public void cancelOnError() throws Exception { + fs.cancel(true); + fs.onError(new TestException("One")); + + try { + fs.get(5, TimeUnit.MILLISECONDS); + fail("Should have thrown"); + } catch (CancellationException ex) { + // expected + } + } + + @Test + @SuppressUndeliverable + public void cancelOnComplete() throws Exception { + fs.cancel(true); + fs.onComplete(); + + try { + fs.get(5, TimeUnit.MILLISECONDS); + fail("Should have thrown"); + } catch (CancellationException ex) { + // expected + } + } + + @Test + public void onNextThenOnCompleteTwice() throws Exception { + fs.onNext(1); + fs.onComplete(); + fs.onComplete(); + + assertEquals(1, fs.get(5, TimeUnit.MILLISECONDS).intValue()); + } + + @Test + public void completeAsync() throws Exception { + Schedulers.single().scheduleDirect(new Runnable() { + @Override + public void run() { + fs.onNext(1); + fs.onComplete(); + } + }, 500, TimeUnit.MILLISECONDS); + + assertEquals(1, fs.get().intValue()); + } + + @Test + public void getTimedOut() throws Exception { + try { + fs.get(1, TimeUnit.NANOSECONDS); + fail("Should have thrown"); + } catch (TimeoutException expected) { + assertEquals(timeoutMessage(1, TimeUnit.NANOSECONDS), expected.getMessage()); + } + } + + @Test + public void onNextCompleteOnError() throws Exception { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + fs.onNext(1); + fs.onComplete(); + fs.onError(new TestException("One")); + + assertEquals((Integer)1, fs.get(5, TimeUnit.MILLISECONDS)); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/subscribers/InnerQueuedSubscriberTest.java b/src/test/java/io/reactivex/rxjava3/internal/subscribers/InnerQueuedSubscriberTest.java new file mode 100644 index 0000000000..7856ce5d3e --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/subscribers/InnerQueuedSubscriberTest.java @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.subscribers; + +import static org.junit.Assert.assertEquals; + +import java.util.*; + +import org.junit.Test; +import org.reactivestreams.Subscription; + +import io.reactivex.rxjava3.core.RxJavaTest; + +public class InnerQueuedSubscriberTest extends RxJavaTest { + + @Test + public void requestInBatches() { + InnerQueuedSubscriberSupport<Integer> support = new InnerQueuedSubscriberSupport<Integer>() { + @Override + public void innerNext(InnerQueuedSubscriber<Integer> inner, Integer value) { + } + + @Override + public void innerError(InnerQueuedSubscriber<Integer> inner, Throwable e) { + } + + @Override + public void innerComplete(InnerQueuedSubscriber<Integer> inner) { + } + + @Override + public void drain() { + } + }; + + InnerQueuedSubscriber<Integer> inner = new InnerQueuedSubscriber<>(support, 4); + + final List<Long> requests = new ArrayList<>(); + + inner.onSubscribe(new Subscription() { + @Override + public void request(long n) { + requests.add(n); + } + + @Override + public void cancel() { + // ignore + } + }); + + inner.request(1); + inner.request(1); + inner.request(1); + inner.request(1); + inner.request(1); + + assertEquals(Arrays.asList(4L, 3L), requests); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/subscribers/LambdaSubscriberTest.java b/src/test/java/io/reactivex/rxjava3/internal/subscribers/LambdaSubscriberTest.java new file mode 100644 index 0000000000..e7f3931096 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/subscribers/LambdaSubscriberTest.java @@ -0,0 +1,375 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.subscribers; + +import static org.junit.Assert.*; + +import java.util.*; + +import org.junit.Test; +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.internal.operators.flowable.FlowableInternalHelper; +import io.reactivex.rxjava3.internal.subscriptions.BooleanSubscription; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.processors.PublishProcessor; +import io.reactivex.rxjava3.testsupport.*; + +public class LambdaSubscriberTest extends RxJavaTest { + + @Test + public void onSubscribeThrows() { + final List<Object> received = new ArrayList<>(); + + LambdaSubscriber<Object> subscriber = new LambdaSubscriber<>(new Consumer<Object>() { + @Override + public void accept(Object v) throws Exception { + received.add(v); + } + }, + new Consumer<Throwable>() { + @Override + public void accept(Throwable e) throws Exception { + received.add(e); + } + }, new Action() { + @Override + public void run() throws Exception { + received.add(100); + } + }, new Consumer<Subscription>() { + @Override + public void accept(Subscription s) throws Exception { + throw new TestException(); + } + }); + + assertFalse(subscriber.isDisposed()); + + Flowable.just(1).subscribe(subscriber); + + assertTrue(received.toString(), received.get(0) instanceof TestException); + assertEquals(received.toString(), 1, received.size()); + + assertTrue(subscriber.isDisposed()); + } + + @Test + public void onNextThrows() { + final List<Object> received = new ArrayList<>(); + + LambdaSubscriber<Object> subscriber = new LambdaSubscriber<>(new Consumer<Object>() { + @Override + public void accept(Object v) throws Exception { + throw new TestException(); + } + }, + new Consumer<Throwable>() { + @Override + public void accept(Throwable e) throws Exception { + received.add(e); + } + }, new Action() { + @Override + public void run() throws Exception { + received.add(100); + } + }, new Consumer<Subscription>() { + @Override + public void accept(Subscription s) throws Exception { + s.request(Long.MAX_VALUE); + } + }); + + assertFalse(subscriber.isDisposed()); + + Flowable.just(1).subscribe(subscriber); + + assertTrue(received.toString(), received.get(0) instanceof TestException); + assertEquals(received.toString(), 1, received.size()); + + assertTrue(subscriber.isDisposed()); + } + + @Test + public void onErrorThrows() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + + try { + final List<Object> received = new ArrayList<>(); + + LambdaSubscriber<Object> subscriber = new LambdaSubscriber<>(new Consumer<Object>() { + @Override + public void accept(Object v) throws Exception { + received.add(v); + } + }, + new Consumer<Throwable>() { + @Override + public void accept(Throwable e) throws Exception { + throw new TestException("Inner"); + } + }, new Action() { + @Override + public void run() throws Exception { + received.add(100); + } + }, new Consumer<Subscription>() { + @Override + public void accept(Subscription s) throws Exception { + s.request(Long.MAX_VALUE); + } + }); + + assertFalse(subscriber.isDisposed()); + + Flowable.<Integer>error(new TestException("Outer")).subscribe(subscriber); + + assertTrue(received.toString(), received.isEmpty()); + + assertTrue(subscriber.isDisposed()); + + TestHelper.assertError(errors, 0, CompositeException.class); + List<Throwable> ce = TestHelper.compositeList(errors.get(0)); + TestHelper.assertError(ce, 0, TestException.class, "Outer"); + TestHelper.assertError(ce, 1, TestException.class, "Inner"); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void onCompleteThrows() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + + try { + final List<Object> received = new ArrayList<>(); + + LambdaSubscriber<Object> subscriber = new LambdaSubscriber<>(new Consumer<Object>() { + @Override + public void accept(Object v) throws Exception { + received.add(v); + } + }, + new Consumer<Throwable>() { + @Override + public void accept(Throwable e) throws Exception { + received.add(e); + } + }, new Action() { + @Override + public void run() throws Exception { + throw new TestException(); + } + }, new Consumer<Subscription>() { + @Override + public void accept(Subscription s) throws Exception { + s.request(Long.MAX_VALUE); + } + }); + + assertFalse(subscriber.isDisposed()); + + Flowable.<Integer>empty().subscribe(subscriber); + + assertTrue(received.toString(), received.isEmpty()); + + assertTrue(subscriber.isDisposed()); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void badSourceOnSubscribe() { + Flowable<Integer> source = Flowable.fromPublisher(new Publisher<Integer>() { + @Override + public void subscribe(Subscriber<? super Integer> s) { + BooleanSubscription s1 = new BooleanSubscription(); + s.onSubscribe(s1); + BooleanSubscription s2 = new BooleanSubscription(); + s.onSubscribe(s2); + + assertFalse(s1.isCancelled()); + assertTrue(s2.isCancelled()); + + s.onNext(1); + s.onComplete(); + } + }); + + final List<Object> received = new ArrayList<>(); + + LambdaSubscriber<Object> subscriber = new LambdaSubscriber<>(new Consumer<Object>() { + @Override + public void accept(Object v) throws Exception { + received.add(v); + } + }, + new Consumer<Throwable>() { + @Override + public void accept(Throwable e) throws Exception { + received.add(e); + } + }, new Action() { + @Override + public void run() throws Exception { + received.add(100); + } + }, new Consumer<Subscription>() { + @Override + public void accept(Subscription s) throws Exception { + s.request(Long.MAX_VALUE); + } + }); + + source.subscribe(subscriber); + + assertEquals(Arrays.asList(1, 100), received); + } + + @Test + @SuppressUndeliverable + public void badSourceEmitAfterDone() { + Flowable<Integer> source = Flowable.fromPublisher(new Publisher<Integer>() { + @Override + public void subscribe(Subscriber<? super Integer> s) { + BooleanSubscription s1 = new BooleanSubscription(); + s.onSubscribe(s1); + + s.onNext(1); + s.onComplete(); + s.onNext(2); + s.onError(new TestException()); + s.onComplete(); + } + }); + + final List<Object> received = new ArrayList<>(); + + LambdaSubscriber<Object> subscriber = new LambdaSubscriber<>(new Consumer<Object>() { + @Override + public void accept(Object v) throws Exception { + received.add(v); + } + }, + new Consumer<Throwable>() { + @Override + public void accept(Throwable e) throws Exception { + received.add(e); + } + }, new Action() { + @Override + public void run() throws Exception { + received.add(100); + } + }, new Consumer<Subscription>() { + @Override + public void accept(Subscription s) throws Exception { + s.request(Long.MAX_VALUE); + } + }); + + source.subscribe(subscriber); + + assertEquals(Arrays.asList(1, 100), received); + } + + @Test + public void onNextThrowsCancelsUpstream() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + + final List<Throwable> errors = new ArrayList<>(); + + pp.subscribe(new Consumer<Integer>() { + @Override + public void accept(Integer v) throws Exception { + throw new TestException(); + } + }, new Consumer<Throwable>() { + @Override + public void accept(Throwable e) throws Exception { + errors.add(e); + } + }); + + assertTrue("No observers?!", pp.hasSubscribers()); + assertTrue("Has errors already?!", errors.isEmpty()); + + pp.onNext(1); + + assertFalse("Has observers?!", pp.hasSubscribers()); + assertFalse("No errors?!", errors.isEmpty()); + + assertTrue(errors.toString(), errors.get(0) instanceof TestException); + } + + @Test + public void onSubscribeThrowsCancelsUpstream() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + + final List<Throwable> errors = new ArrayList<>(); + + pp.subscribe(new LambdaSubscriber<>(new Consumer<Integer>() { + @Override + public void accept(Integer v) throws Exception { + } + }, new Consumer<Throwable>() { + @Override + public void accept(Throwable e) throws Exception { + errors.add(e); + } + }, new Action() { + @Override + public void run() throws Exception { + } + }, new Consumer<Subscription>() { + @Override + public void accept(Subscription s) throws Exception { + throw new TestException(); + } + })); + + assertFalse("Has observers?!", pp.hasSubscribers()); + assertFalse("No errors?!", errors.isEmpty()); + + assertTrue(errors.toString(), errors.get(0) instanceof TestException); + } + + @Test + public void onErrorMissingShouldReportNoCustomOnError() { + LambdaSubscriber<Integer> subscriber = new LambdaSubscriber<>(Functions.<Integer>emptyConsumer(), + Functions.ON_ERROR_MISSING, + Functions.EMPTY_ACTION, + FlowableInternalHelper.RequestMax.INSTANCE); + + assertFalse(subscriber.hasCustomOnError()); + } + + @Test + public void customOnErrorShouldReportCustomOnError() { + LambdaSubscriber<Integer> subscriber = new LambdaSubscriber<>(Functions.<Integer>emptyConsumer(), + Functions.<Throwable>emptyConsumer(), + Functions.EMPTY_ACTION, + FlowableInternalHelper.RequestMax.INSTANCE); + + assertTrue(subscriber.hasCustomOnError()); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/subscribers/QueueDrainSubscriberTest.java b/src/test/java/io/reactivex/rxjava3/internal/subscribers/QueueDrainSubscriberTest.java new file mode 100644 index 0000000000..985705b0e7 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/subscribers/QueueDrainSubscriberTest.java @@ -0,0 +1,337 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.subscribers; + +import static org.junit.Assert.*; + +import java.util.List; + +import org.junit.Test; +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.RxJavaTest; +import io.reactivex.rxjava3.disposables.*; +import io.reactivex.rxjava3.exceptions.MissingBackpressureException; +import io.reactivex.rxjava3.internal.subscriptions.BooleanSubscription; +import io.reactivex.rxjava3.operators.SpscArrayQueue; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.subscribers.TestSubscriber; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class QueueDrainSubscriberTest extends RxJavaTest { + + static final QueueDrainSubscriber<Integer, Integer, Integer> createUnordered(TestSubscriber<Integer> ts, final Disposable d) { + return new QueueDrainSubscriber<Integer, Integer, Integer>(ts, new SpscArrayQueue<>(4)) { + @Override + public void onNext(Integer t) { + fastPathEmitMax(t, false, d); + } + + @Override + public void onError(Throwable t) { + } + + @Override + public void onComplete() { + } + + @Override + public void onSubscribe(Subscription s) { + } + + @Override + public boolean accept(Subscriber<? super Integer> a, Integer v) { + super.accept(a, v); + a.onNext(v); + return true; + } + }; + } + + static final QueueDrainSubscriber<Integer, Integer, Integer> createOrdered(TestSubscriber<Integer> ts, final Disposable d) { + return new QueueDrainSubscriber<Integer, Integer, Integer>(ts, new SpscArrayQueue<>(4)) { + @Override + public void onNext(Integer t) { + fastPathOrderedEmitMax(t, false, d); + } + + @Override + public void onError(Throwable t) { + } + + @Override + public void onComplete() { + } + + @Override + public void onSubscribe(Subscription s) { + } + + @Override + public boolean accept(Subscriber<? super Integer> a, Integer v) { + super.accept(a, v); + a.onNext(v); + return true; + } + }; + } + + static final QueueDrainSubscriber<Integer, Integer, Integer> createUnorderedReject(TestSubscriber<Integer> ts, final Disposable d) { + return new QueueDrainSubscriber<Integer, Integer, Integer>(ts, new SpscArrayQueue<>(4)) { + @Override + public void onNext(Integer t) { + fastPathEmitMax(t, false, d); + } + + @Override + public void onError(Throwable t) { + } + + @Override + public void onComplete() { + } + + @Override + public void onSubscribe(Subscription s) { + } + + @Override + public boolean accept(Subscriber<? super Integer> a, Integer v) { + super.accept(a, v); + a.onNext(v); + return false; + } + }; + } + + static final QueueDrainSubscriber<Integer, Integer, Integer> createOrderedReject(TestSubscriber<Integer> ts, final Disposable d) { + return new QueueDrainSubscriber<Integer, Integer, Integer>(ts, new SpscArrayQueue<>(4)) { + @Override + public void onNext(Integer t) { + fastPathOrderedEmitMax(t, false, d); + } + + @Override + public void onError(Throwable t) { + } + + @Override + public void onComplete() { + } + + @Override + public void onSubscribe(Subscription s) { + } + + @Override + public boolean accept(Subscriber<? super Integer> a, Integer v) { + super.accept(a, v); + a.onNext(v); + return false; + } + }; + } + + @Test + public void unorderedFastPathNoRequest() { + TestSubscriber<Integer> ts = new TestSubscriber<>(0); + Disposable d = Disposable.empty(); + QueueDrainSubscriber<Integer, Integer, Integer> qd = createUnordered(ts, d); + ts.onSubscribe(new BooleanSubscription()); + + qd.onNext(1); + + ts.assertFailure(MissingBackpressureException.class); + + assertTrue(d.isDisposed()); + } + + @Test + public void orderedFastPathNoRequest() { + TestSubscriber<Integer> ts = new TestSubscriber<>(0); + Disposable d = Disposable.empty(); + QueueDrainSubscriber<Integer, Integer, Integer> qd = createOrdered(ts, d); + ts.onSubscribe(new BooleanSubscription()); + + qd.onNext(1); + + ts.assertFailure(MissingBackpressureException.class); + + assertTrue(d.isDisposed()); + } + + @Test + public void acceptBadRequest() { + TestSubscriber<Integer> ts = new TestSubscriber<>(0); + Disposable d = Disposable.empty(); + QueueDrainSubscriber<Integer, Integer, Integer> qd = createUnordered(ts, d); + ts.onSubscribe(new BooleanSubscription()); + + assertTrue(qd.accept(ts, 0)); + + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + qd.requested(-1); + TestHelper.assertError(errors, 0, IllegalArgumentException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void unorderedFastPathRequest1() { + TestSubscriber<Integer> ts = new TestSubscriber<>(1); + Disposable d = Disposable.empty(); + QueueDrainSubscriber<Integer, Integer, Integer> qd = createUnordered(ts, d); + ts.onSubscribe(new BooleanSubscription()); + + qd.requested(1); + + qd.onNext(1); + + ts.assertValuesOnly(1); + } + + @Test + public void orderedFastPathRequest1() { + TestSubscriber<Integer> ts = new TestSubscriber<>(1); + Disposable d = Disposable.empty(); + QueueDrainSubscriber<Integer, Integer, Integer> qd = createOrdered(ts, d); + ts.onSubscribe(new BooleanSubscription()); + + qd.requested(1); + + qd.onNext(1); + + ts.assertValuesOnly(1); + } + + @Test + public void unorderedSlowPath() { + TestSubscriber<Integer> ts = new TestSubscriber<>(1); + Disposable d = Disposable.empty(); + QueueDrainSubscriber<Integer, Integer, Integer> qd = createUnordered(ts, d); + ts.onSubscribe(new BooleanSubscription()); + + qd.enter(); + qd.onNext(1); + + ts.assertEmpty(); + } + + @Test + public void orderedSlowPath() { + TestSubscriber<Integer> ts = new TestSubscriber<>(1); + Disposable d = Disposable.empty(); + QueueDrainSubscriber<Integer, Integer, Integer> qd = createOrdered(ts, d); + ts.onSubscribe(new BooleanSubscription()); + + qd.enter(); + qd.onNext(1); + + ts.assertEmpty(); + } + + @Test + public void orderedSlowPathNonEmptyQueue() { + TestSubscriber<Integer> ts = new TestSubscriber<>(1); + Disposable d = Disposable.empty(); + QueueDrainSubscriber<Integer, Integer, Integer> qd = createOrdered(ts, d); + ts.onSubscribe(new BooleanSubscription()); + + qd.queue.offer(0); + qd.requested(2); + qd.onNext(1); + + ts.assertValuesOnly(0, 1); + } + + @Test + public void unorderedOnNextRace() { + for (int i = 0; i < TestHelper.RACE_LONG_LOOPS; i++) { + + TestSubscriber<Integer> ts = new TestSubscriber<>(1); + Disposable d = Disposable.empty(); + final QueueDrainSubscriber<Integer, Integer, Integer> qd = createUnordered(ts, d); + ts.onSubscribe(new BooleanSubscription()); + + qd.requested(Long.MAX_VALUE); + Runnable r1 = new Runnable() { + @Override + public void run() { + qd.onNext(1); + } + }; + + TestHelper.race(r1, r1); + + ts.assertValuesOnly(1, 1); + } + } + + @Test + public void orderedOnNextRace() { + for (int i = 0; i < TestHelper.RACE_LONG_LOOPS; i++) { + + TestSubscriber<Integer> ts = new TestSubscriber<>(1); + Disposable d = Disposable.empty(); + final QueueDrainSubscriber<Integer, Integer, Integer> qd = createOrdered(ts, d); + ts.onSubscribe(new BooleanSubscription()); + + qd.requested(Long.MAX_VALUE); + Runnable r1 = new Runnable() { + @Override + public void run() { + qd.onNext(1); + } + }; + + TestHelper.race(r1, r1); + + ts.assertValuesOnly(1, 1); + } + } + + @Test + public void unorderedFastPathReject() { + TestSubscriber<Integer> ts = new TestSubscriber<>(1); + Disposable d = Disposable.empty(); + QueueDrainSubscriber<Integer, Integer, Integer> qd = createUnorderedReject(ts, d); + ts.onSubscribe(new BooleanSubscription()); + + qd.requested(1); + + qd.onNext(1); + + ts.assertValuesOnly(1); + + assertEquals(1, qd.requested()); + } + + @Test + public void orderedFastPathReject() { + TestSubscriber<Integer> ts = new TestSubscriber<>(1); + Disposable d = Disposable.empty(); + QueueDrainSubscriber<Integer, Integer, Integer> qd = createOrderedReject(ts, d); + ts.onSubscribe(new BooleanSubscription()); + + qd.requested(1); + + qd.onNext(1); + + ts.assertValuesOnly(1); + + assertEquals(1, qd.requested()); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/subscribers/SinglePostCompleteSubscriberTest.java b/src/test/java/io/reactivex/rxjava3/internal/subscribers/SinglePostCompleteSubscriberTest.java new file mode 100644 index 0000000000..8616e6ba3b --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/subscribers/SinglePostCompleteSubscriberTest.java @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.subscribers; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.RxJavaTest; +import io.reactivex.rxjava3.internal.subscriptions.BooleanSubscription; +import io.reactivex.rxjava3.subscribers.TestSubscriber; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class SinglePostCompleteSubscriberTest extends RxJavaTest { + + @Test + public void requestCompleteRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final TestSubscriber<Integer> ts = new TestSubscriber<>(0L); + + final SinglePostCompleteSubscriber<Integer, Integer> spc = new SinglePostCompleteSubscriber<Integer, Integer>(ts) { + private static final long serialVersionUID = -2848918821531562637L; + + @Override + public void onNext(Integer t) { + } + + @Override + public void onError(Throwable t) { + } + + @Override + public void onComplete() { + complete(1); + } + }; + + spc.onSubscribe(new BooleanSubscription()); + + Runnable r1 = new Runnable() { + @Override + public void run() { + spc.onComplete(); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + ts.request(1); + } + }; + + TestHelper.race(r1, r2); + + ts.assertResult(1); + } + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/subscribers/StrictSubscriberTest.java b/src/test/java/io/reactivex/rxjava3/internal/subscribers/StrictSubscriberTest.java new file mode 100644 index 0000000000..ba4548d02e --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/subscribers/StrictSubscriberTest.java @@ -0,0 +1,334 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.subscribers; + +import static org.junit.Assert.*; + +import java.util.*; + +import org.junit.Test; +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.internal.subscriptions.BooleanSubscription; +import io.reactivex.rxjava3.testsupport.TestSubscriberEx; + +public class StrictSubscriberTest extends RxJavaTest { + + @Test + public void strictMode() { + final List<Object> list = new ArrayList<>(); + Subscriber<Object> sub = new Subscriber<Object>() { + + @Override + public void onSubscribe(Subscription s) { + s.request(10); + } + + @Override + public void onNext(Object t) { + list.add(t); + } + + @Override + public void onError(Throwable t) { + list.add(t); + } + + @Override + public void onComplete() { + list.add("Done"); + } + }; + + new Flowable<Object>() { + @Override + protected void subscribeActual(Subscriber<? super Object> s) { + s.onSubscribe(new BooleanSubscription()); + s.onNext(s); + } + }.subscribe(sub); + + assertTrue(list.toString(), list.get(0) instanceof StrictSubscriber); + } + + static final class SubscriberWrapper<T> implements Subscriber<T> { + final TestSubscriberEx<T> tester; + + SubscriberWrapper(TestSubscriberEx<T> tester) { + this.tester = tester; + } + + @Override + public void onSubscribe(Subscription s) { + tester.onSubscribe(s); + } + + @Override + public void onNext(T t) { + tester.onNext(t); + } + + @Override + public void onError(Throwable t) { + tester.onError(t); + } + + @Override + public void onComplete() { + tester.onComplete(); + } + } + + @Test + public void normalOnNext() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(); + SubscriberWrapper<Integer> wrapper = new SubscriberWrapper<>(ts); + + Flowable.range(1, 5).subscribe(wrapper); + + ts.assertResult(1, 2, 3, 4, 5); + } + + @Test + public void normalOnNextBackpressured() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(0); + SubscriberWrapper<Integer> wrapper = new SubscriberWrapper<>(ts); + + Flowable.range(1, 5).subscribe(wrapper); + + ts.assertEmpty() + .requestMore(1) + .assertValue(1) + .requestMore(2) + .assertValues(1, 2, 3) + .requestMore(2) + .assertResult(1, 2, 3, 4, 5); + } + + @Test + public void normalOnError() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(); + SubscriberWrapper<Integer> wrapper = new SubscriberWrapper<>(ts); + + Flowable.range(1, 5).concatWith(Flowable.<Integer>error(new TestException())) + .subscribe(wrapper); + + ts.assertFailure(TestException.class, 1, 2, 3, 4, 5); + } + + @Test + public void deferredRequest() { + final List<Object> list = new ArrayList<>(); + Subscriber<Object> sub = new Subscriber<Object>() { + + @Override + public void onSubscribe(Subscription s) { + s.request(5); + list.add(0); + } + + @Override + public void onNext(Object t) { + list.add(t); + } + + @Override + public void onError(Throwable t) { + list.add(t); + } + + @Override + public void onComplete() { + list.add("Done"); + } + }; + + Flowable.range(1, 5).subscribe(sub); + + assertEquals(Arrays.<Object>asList(0, 1, 2, 3, 4, 5, "Done"), list); + } + + @Test + public void requestZero() { + final List<Object> list = new ArrayList<>(); + Subscriber<Object> sub = new Subscriber<Object>() { + + @Override + public void onSubscribe(Subscription s) { + s.request(0); + } + + @Override + public void onNext(Object t) { + list.add(t); + } + + @Override + public void onError(Throwable t) { + list.add(t); + } + + @Override + public void onComplete() { + list.add("Done"); + } + }; + + Flowable.range(1, 5).subscribe(sub); + + assertTrue(list.toString(), list.get(0) instanceof IllegalArgumentException); + assertTrue(list.toString(), list.get(0).toString().contains("3.9")); + } + + @Test + public void requestNegative() { + final List<Object> list = new ArrayList<>(); + Subscriber<Object> sub = new Subscriber<Object>() { + + @Override + public void onSubscribe(Subscription s) { + s.request(-99); + } + + @Override + public void onNext(Object t) { + list.add(t); + } + + @Override + public void onError(Throwable t) { + list.add(t); + } + + @Override + public void onComplete() { + list.add("Done"); + } + }; + + Flowable.range(1, 5).subscribe(sub); + + assertTrue(list.toString(), list.get(0) instanceof IllegalArgumentException); + assertTrue(list.toString(), list.get(0).toString().contains("3.9")); + } + + @Test + public void cancelAfterOnComplete() { + final List<Object> list = new ArrayList<>(); + Subscriber<Object> sub = new Subscriber<Object>() { + + Subscription upstream; + @Override + public void onSubscribe(Subscription s) { + this.upstream = s; + } + + @Override + public void onNext(Object t) { + list.add(t); + } + + @Override + public void onError(Throwable t) { + upstream.cancel(); + list.add(t); + } + + @Override + public void onComplete() { + upstream.cancel(); + list.add("Done"); + } + }; + + new Flowable<Object>() { + @Override + protected void subscribeActual(Subscriber<? super Object> s) { + BooleanSubscription b = new BooleanSubscription(); + s.onSubscribe(b); + s.onComplete(); + list.add(b.isCancelled()); + } + }.subscribe(sub); + + assertEquals(Arrays.<Object>asList("Done", false), list); + } + + @Test + public void cancelAfterOnError() { + final List<Object> list = new ArrayList<>(); + Subscriber<Object> sub = new Subscriber<Object>() { + + Subscription upstream; + @Override + public void onSubscribe(Subscription s) { + this.upstream = s; + } + + @Override + public void onNext(Object t) { + list.add(t); + } + + @Override + public void onError(Throwable t) { + upstream.cancel(); + list.add(t.getMessage()); + } + + @Override + public void onComplete() { + upstream.cancel(); + list.add("Done"); + } + }; + + new Flowable<Object>() { + @Override + protected void subscribeActual(Subscriber<? super Object> s) { + BooleanSubscription b = new BooleanSubscription(); + s.onSubscribe(b); + s.onError(new TestException("Forced failure")); + list.add(b.isCancelled()); + } + }.subscribe(sub); + + assertEquals(Arrays.<Object>asList("Forced failure", false), list); + } + + @Test + public void doubleOnSubscribe() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(); + SubscriberWrapper<Integer> wrapper = new SubscriberWrapper<>(ts); + + new Flowable<Integer>() { + @Override + protected void subscribeActual(Subscriber<? super Integer> s) { + BooleanSubscription b1 = new BooleanSubscription(); + s.onSubscribe(b1); + + BooleanSubscription b2 = new BooleanSubscription(); + s.onSubscribe(b2); + + assertTrue(b1.isCancelled()); + assertTrue(b2.isCancelled()); + } + }.subscribe(wrapper); + + ts.assertFailure(IllegalStateException.class); + assertTrue(ts.errors().toString(), ts.errors().get(0).getMessage().contains("2.12")); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/subscribers/SubscriberResourceWrapperTest.java b/src/test/java/io/reactivex/rxjava3/internal/subscribers/SubscriberResourceWrapperTest.java new file mode 100644 index 0000000000..65f2817354 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/subscribers/SubscriberResourceWrapperTest.java @@ -0,0 +1,114 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.subscribers; + +import static org.junit.Assert.*; + +import org.junit.Test; +import org.reactivestreams.Subscriber; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.subscriptions.BooleanSubscription; +import io.reactivex.rxjava3.subscribers.TestSubscriber; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class SubscriberResourceWrapperTest extends RxJavaTest { + + TestSubscriber<Integer> ts = new TestSubscriber<>(); + + SubscriberResourceWrapper<Integer> s = new SubscriberResourceWrapper<>(ts); + + @Test + public void cancel() { + BooleanSubscription bs = new BooleanSubscription(); + Disposable d = Disposable.empty(); + + s.setResource(d); + + s.onSubscribe(bs); + + assertFalse(d.isDisposed()); + assertFalse(s.isDisposed()); + + ts.cancel(); + + assertTrue(bs.isCancelled()); + assertTrue(d.isDisposed()); + assertTrue(s.isDisposed()); + } + + @Test + public void error() { + BooleanSubscription bs = new BooleanSubscription(); + Disposable d = Disposable.empty(); + + s.setResource(d); + + s.onSubscribe(bs); + + s.onError(new TestException()); + + assertTrue(d.isDisposed()); + assertFalse(bs.isCancelled()); + + ts.assertFailure(TestException.class); + } + + @Test + public void complete() { + BooleanSubscription bs = new BooleanSubscription(); + Disposable d = Disposable.empty(); + + s.setResource(d); + + s.onSubscribe(bs); + + s.onComplete(); + + assertTrue(d.isDisposed()); + assertFalse(bs.isCancelled()); + + ts.assertResult(); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeFlowable(new Function<Flowable<Object>, Flowable<Object>>() { + @Override + public Flowable<Object> apply(Flowable<Object> f) throws Exception { + return f.lift(new FlowableOperator<Object, Object>() { + @Override + public Subscriber<? super Object> apply( + Subscriber<? super Object> s) throws Exception { + return new SubscriberResourceWrapper<>(s); + } + }); + } + }); + } + + @Test + public void badRequest() { + TestHelper.assertBadRequestReported(Flowable.never().lift(new FlowableOperator<Object, Object>() { + @Override + public Subscriber<? super Object> apply( + Subscriber<? super Object> s) throws Exception { + return new SubscriberResourceWrapper<>(s); + } + })); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/subscriptions/ArrayCompositeSubscriptionTest.java b/src/test/java/io/reactivex/rxjava3/internal/subscriptions/ArrayCompositeSubscriptionTest.java new file mode 100644 index 0000000000..fc54cfe357 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/subscriptions/ArrayCompositeSubscriptionTest.java @@ -0,0 +1,137 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.subscriptions; + +import static org.junit.Assert.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.RxJavaTest; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class ArrayCompositeSubscriptionTest extends RxJavaTest { + + @Test + public void set() { + ArrayCompositeSubscription ac = new ArrayCompositeSubscription(1); + + BooleanSubscription bs1 = new BooleanSubscription(); + + ac.setResource(0, bs1); + + assertFalse(bs1.isCancelled()); + + BooleanSubscription bs2 = new BooleanSubscription(); + + ac.setResource(0, bs2); + + assertTrue(bs1.isCancelled()); + + assertFalse(bs2.isCancelled()); + + assertFalse(ac.isDisposed()); + + ac.dispose(); + + assertTrue(bs2.isCancelled()); + + assertTrue(ac.isDisposed()); + + BooleanSubscription bs3 = new BooleanSubscription(); + + assertFalse(ac.setResource(0, bs3)); + + assertTrue(bs3.isCancelled()); + + assertFalse(ac.setResource(0, null)); + } + + @Test + public void replace() { + ArrayCompositeSubscription ac = new ArrayCompositeSubscription(1); + + BooleanSubscription bs1 = new BooleanSubscription(); + + ac.replaceResource(0, bs1); + + assertFalse(bs1.isCancelled()); + + BooleanSubscription bs2 = new BooleanSubscription(); + + ac.replaceResource(0, bs2); + + assertFalse(bs1.isCancelled()); + + assertFalse(bs2.isCancelled()); + + assertFalse(ac.isDisposed()); + + ac.dispose(); + + assertTrue(bs2.isCancelled()); + + assertTrue(ac.isDisposed()); + + BooleanSubscription bs3 = new BooleanSubscription(); + + ac.replaceResource(0, bs3); + + assertTrue(bs3.isCancelled()); + + ac.replaceResource(0, null); + } + + @Test + public void disposeRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final ArrayCompositeSubscription ac = new ArrayCompositeSubscription(1000); + + Runnable r = new Runnable() { + @Override + public void run() { + ac.dispose(); + } + }; + + TestHelper.race(r, r); + } + } + + @Test + public void setReplaceRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final ArrayCompositeSubscription ac = new ArrayCompositeSubscription(1); + + final BooleanSubscription s1 = new BooleanSubscription(); + final BooleanSubscription s2 = new BooleanSubscription(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + ac.setResource(0, s1); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + ac.replaceResource(0, s2); + } + }; + + TestHelper.race(r1, r2); + } + } + +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/subscriptions/AsyncSubscriptionTest.java b/src/test/java/io/reactivex/rxjava3/internal/subscriptions/AsyncSubscriptionTest.java new file mode 100644 index 0000000000..0375d72ba5 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/subscriptions/AsyncSubscriptionTest.java @@ -0,0 +1,215 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.subscriptions; + +import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.*; + +import org.junit.Test; +import org.reactivestreams.Subscription; + +import io.reactivex.rxjava3.core.RxJavaTest; +import io.reactivex.rxjava3.disposables.Disposable; + +public class AsyncSubscriptionTest extends RxJavaTest { + @Test + public void noResource() { + AsyncSubscription as = new AsyncSubscription(); + + Subscription s = mock(Subscription.class); + + as.setSubscription(s); + + as.request(1); + + as.cancel(); + + verify(s).request(1); + verify(s).cancel(); + } + + @Test + public void requestBeforeSet() { + AsyncSubscription as = new AsyncSubscription(); + + Subscription s = mock(Subscription.class); + + as.request(1); + + as.setSubscription(s); + + as.cancel(); + + verify(s).request(1); + verify(s).cancel(); + } + + @Test + public void cancelBeforeSet() { + AsyncSubscription as = new AsyncSubscription(); + + Subscription s = mock(Subscription.class); + + as.request(1); + as.cancel(); + + as.setSubscription(s); + + verify(s, never()).request(1); + verify(s).cancel(); + } + + @Test + public void singleSet() { + AsyncSubscription as = new AsyncSubscription(); + + Subscription s = mock(Subscription.class); + + as.setSubscription(s); + + Subscription s1 = mock(Subscription.class); + + as.setSubscription(s1); + + assertSame(as.actual.get(), s); + + verify(s1).cancel(); + } + + @Test + public void initialResource() { + Disposable r = mock(Disposable.class); + AsyncSubscription as = new AsyncSubscription(r); + + as.cancel(); + + verify(r).dispose(); + } + + @Test + public void setResource() { + AsyncSubscription as = new AsyncSubscription(); + + Disposable r = mock(Disposable.class); + + assertTrue(as.setResource(r)); + + as.cancel(); + + verify(r).dispose(); + } + + @Test + public void replaceResource() { + AsyncSubscription as = new AsyncSubscription(); + + Disposable r = mock(Disposable.class); + + assertTrue(as.replaceResource(r)); + + as.cancel(); + + verify(r).dispose(); + } + + @Test + public void setResource2() { + AsyncSubscription as = new AsyncSubscription(); + + Disposable r = mock(Disposable.class); + + assertTrue(as.setResource(r)); + + Disposable r2 = mock(Disposable.class); + + assertTrue(as.setResource(r2)); + + as.cancel(); + + verify(r).dispose(); + verify(r2).dispose(); + } + + @Test + public void replaceResource2() { + AsyncSubscription as = new AsyncSubscription(); + + Disposable r = mock(Disposable.class); + + assertTrue(as.replaceResource(r)); + + Disposable r2 = mock(Disposable.class); + + as.replaceResource(r2); + + as.cancel(); + + verify(r, never()).dispose(); + verify(r2).dispose(); + } + + @Test + public void setResourceAfterCancel() { + AsyncSubscription as = new AsyncSubscription(); + + as.cancel(); + + Disposable r = mock(Disposable.class); + + as.setResource(r); + + verify(r).dispose(); + } + + @Test + public void replaceResourceAfterCancel() { + AsyncSubscription as = new AsyncSubscription(); + as.cancel(); + + Disposable r = mock(Disposable.class); + + as.replaceResource(r); + + verify(r).dispose(); + } + + @Test + public void cancelOnce() { + Disposable r = mock(Disposable.class); + AsyncSubscription as = new AsyncSubscription(r); + Subscription s = mock(Subscription.class); + + as.setSubscription(s); + + as.cancel(); + as.cancel(); + as.cancel(); + + verify(s, never()).request(anyLong()); + verify(s).cancel(); + verify(r).dispose(); + } + + @Test + public void disposed() { + AsyncSubscription as = new AsyncSubscription(); + + assertFalse(as.isDisposed()); + + as.dispose(); + + assertTrue(as.isDisposed()); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/subscriptions/DeferredScalarSubscriptionTest.java b/src/test/java/io/reactivex/rxjava3/internal/subscriptions/DeferredScalarSubscriptionTest.java new file mode 100644 index 0000000000..7ea8d58c3f --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/subscriptions/DeferredScalarSubscriptionTest.java @@ -0,0 +1,139 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.subscriptions; + +import static org.junit.Assert.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.RxJavaTest; +import io.reactivex.rxjava3.operators.QueueFuseable; +import io.reactivex.rxjava3.subscribers.TestSubscriber; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class DeferredScalarSubscriptionTest extends RxJavaTest { + + @Test + public void queueSubscriptionSyncRejected() { + DeferredScalarSubscription<Integer> ds = new DeferredScalarSubscription<>(new TestSubscriber<>()); + + assertEquals(QueueFuseable.NONE, ds.requestFusion(QueueFuseable.SYNC)); + } + + @Test + public void clear() { + DeferredScalarSubscription<Integer> ds = new DeferredScalarSubscription<>(new TestSubscriber<>()); + + ds.value = 1; + + ds.clear(); + + assertEquals(DeferredScalarSubscription.FUSED_CONSUMED, ds.get()); + assertNull(ds.value); + } + + @Test + public void cancel() { + DeferredScalarSubscription<Integer> ds = new DeferredScalarSubscription<>(new TestSubscriber<>()); + + assertTrue(ds.tryCancel()); + + assertFalse(ds.tryCancel()); + } + + @Test + public void completeCancelRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final DeferredScalarSubscription<Integer> ds = new DeferredScalarSubscription<>(new TestSubscriber<>()); + + Runnable r1 = new Runnable() { + @Override + public void run() { + ds.complete(1); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + ds.cancel(); + } + }; + + TestHelper.race(r1, r2); + } + } + + @Test + public void requestClearRace() { + for (int i = 0; i < TestHelper.RACE_LONG_LOOPS; i++) { + TestSubscriber<Integer> ts = new TestSubscriber<>(0L); + + final DeferredScalarSubscription<Integer> ds = new DeferredScalarSubscription<>(ts); + ts.onSubscribe(ds); + ds.complete(1); + + Runnable r1 = new Runnable() { + @Override + public void run() { + ds.request(1); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + ds.value = null; + } + }; + + TestHelper.race(r1, r2); + + if (ts.values().size() >= 1) { + ts.assertValue(1); + } + } + } + + @Test + public void requestCancelRace() { + for (int i = 0; i < TestHelper.RACE_LONG_LOOPS; i++) { + TestSubscriber<Integer> ts = new TestSubscriber<>(0L); + + final DeferredScalarSubscription<Integer> ds = new DeferredScalarSubscription<>(ts); + ts.onSubscribe(ds); + ds.complete(1); + + Runnable r1 = new Runnable() { + @Override + public void run() { + ds.request(1); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + ds.cancel(); + } + }; + + TestHelper.race(r1, r2); + + if (ts.values().size() >= 1) { + ts.assertValue(1); + } + } + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/subscriptions/QueueSubscriptionTest.java b/src/test/java/io/reactivex/rxjava3/internal/subscriptions/QueueSubscriptionTest.java new file mode 100644 index 0000000000..92c37e0e52 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/subscriptions/QueueSubscriptionTest.java @@ -0,0 +1,117 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.subscriptions; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; + +import io.reactivex.rxjava3.annotations.Nullable; +import io.reactivex.rxjava3.core.RxJavaTest; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class QueueSubscriptionTest extends RxJavaTest { + static final class EmptyQS extends BasicQueueSubscription<Integer> { + + private static final long serialVersionUID = -5312809687598840520L; + + @Override + public int requestFusion(int mode) { + return 0; + } + + @Nullable + @Override + public Integer poll() throws Exception { + return null; + } + + @Override + public boolean isEmpty() { + return false; + } + + @Override + public void clear() { + + } + + @Override + public void request(long n) { + + } + + @Override + public void cancel() { + + } + + } + + static final class EmptyIntQS extends BasicIntQueueSubscription<Integer> { + + private static final long serialVersionUID = -1374033403007296252L; + + @Override + public int requestFusion(int mode) { + return 0; + } + + @Nullable + @Override + public Integer poll() throws Exception { + return null; + } + + @Override + public boolean isEmpty() { + return false; + } + + @Override + public void clear() { + + } + + @Override + public void request(long n) { + + } + + @Override + public void cancel() { + + } + + } + + @Test + public void noOfferBasic() { + TestHelper.assertNoOffer(new EmptyQS()); + } + + @Test + public void noOfferBasicInt() { + TestHelper.assertNoOffer(new EmptyIntQS()); + } + + @Test + public void empty() { + TestHelper.checkEnum(EmptySubscription.class); + + assertEquals("EmptySubscription", EmptySubscription.INSTANCE.toString()); + + TestHelper.assertNoOffer(EmptySubscription.INSTANCE); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/subscriptions/ScalarSubscriptionTest.java b/src/test/java/io/reactivex/rxjava3/internal/subscriptions/ScalarSubscriptionTest.java new file mode 100644 index 0000000000..377691e0a2 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/subscriptions/ScalarSubscriptionTest.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.subscriptions; + +import java.util.List; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.RxJavaTest; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.subscribers.TestSubscriber; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class ScalarSubscriptionTest extends RxJavaTest { + + @Test + public void badRequest() { + TestSubscriber<Integer> ts = new TestSubscriber<>(0L); + + ScalarSubscription<Integer> sc = new ScalarSubscription<>(ts, 1); + + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + sc.request(-99); + + TestHelper.assertError(errors, 0, IllegalArgumentException.class, "n > 0 required but it was -99"); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void noOffer() { + TestSubscriber<Integer> ts = new TestSubscriber<>(0L); + + ScalarSubscription<Integer> sc = new ScalarSubscription<>(ts, 1); + + TestHelper.assertNoOffer(sc); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/subscriptions/SubscriptionArbiterTest.java b/src/test/java/io/reactivex/rxjava3/internal/subscriptions/SubscriptionArbiterTest.java new file mode 100644 index 0000000000..7bc7eb937a --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/subscriptions/SubscriptionArbiterTest.java @@ -0,0 +1,311 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.subscriptions; + +import static org.junit.Assert.*; + +import java.util.List; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.RxJavaTest; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class SubscriptionArbiterTest extends RxJavaTest { + + @Test + public void setSubscriptionMissed() { + SubscriptionArbiter sa = new SubscriptionArbiter(true); + + sa.getAndIncrement(); + + BooleanSubscription bs1 = new BooleanSubscription(); + + sa.setSubscription(bs1); + + BooleanSubscription bs2 = new BooleanSubscription(); + + sa.setSubscription(bs2); + + assertTrue(bs1.isCancelled()); + + assertFalse(bs2.isCancelled()); + } + + @Test + public void invalidDeferredRequest() { + SubscriptionArbiter sa = new SubscriptionArbiter(true); + + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + sa.request(-99); + + TestHelper.assertError(errors, 0, IllegalArgumentException.class, "n > 0 required but it was -99"); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void unbounded() { + SubscriptionArbiter sa = new SubscriptionArbiter(true); + + sa.request(Long.MAX_VALUE); + + assertEquals(Long.MAX_VALUE, sa.requested); + + assertTrue(sa.isUnbounded()); + + sa.unbounded = false; + + sa.request(Long.MAX_VALUE); + + assertEquals(Long.MAX_VALUE, sa.requested); + + sa.produced(1); + + assertEquals(Long.MAX_VALUE, sa.requested); + + sa.unbounded = false; + + sa.produced(Long.MAX_VALUE); + + assertEquals(Long.MAX_VALUE, sa.requested); + } + + @Test + public void cancelled() { + SubscriptionArbiter sa = new SubscriptionArbiter(true); + sa.cancelled = true; + + BooleanSubscription bs1 = new BooleanSubscription(); + + sa.missedSubscription.set(bs1); + + sa.getAndIncrement(); + + sa.drainLoop(); + + assertTrue(bs1.isCancelled()); + } + + @Test + public void drainUnbounded() { + SubscriptionArbiter sa = new SubscriptionArbiter(true); + + sa.getAndIncrement(); + + sa.requested = Long.MAX_VALUE; + + sa.drainLoop(); + } + + @Test + public void drainMissedRequested() { + SubscriptionArbiter sa = new SubscriptionArbiter(true); + + sa.getAndIncrement(); + + sa.requested = 0; + + sa.missedRequested.set(1); + + sa.drainLoop(); + + assertEquals(1, sa.requested); + } + + @Test + public void drainMissedRequestedProduced() { + SubscriptionArbiter sa = new SubscriptionArbiter(true); + + sa.getAndIncrement(); + + sa.requested = 0; + + sa.missedRequested.set(Long.MAX_VALUE); + + sa.missedProduced.set(1); + + sa.drainLoop(); + + assertEquals(Long.MAX_VALUE, sa.requested); + } + + @Test + public void drainMissedRequestedMoreProduced() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + SubscriptionArbiter sa = new SubscriptionArbiter(true); + + sa.getAndIncrement(); + + sa.requested = 0; + + sa.missedRequested.set(1); + + sa.missedProduced.set(2); + + sa.drainLoop(); + + assertEquals(0, sa.requested); + + TestHelper.assertError(errors, 0, IllegalStateException.class, "More produced than requested: -1"); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void missedSubscriptionNoPrior() { + SubscriptionArbiter sa = new SubscriptionArbiter(true); + + sa.getAndIncrement(); + + BooleanSubscription bs1 = new BooleanSubscription(); + + sa.missedSubscription.set(bs1); + + sa.drainLoop(); + + assertSame(bs1, sa.actual); + } + + @Test + public void noCancelFastPath() { + SubscriptionArbiter sa = new SubscriptionArbiter(false); + + BooleanSubscription bs1 = new BooleanSubscription(); + BooleanSubscription bs2 = new BooleanSubscription(); + + sa.setSubscription(bs1); + sa.setSubscription(bs2); + + assertFalse(bs1.isCancelled()); + assertFalse(bs2.isCancelled()); + } + + @Test + public void cancelFastPath() { + SubscriptionArbiter sa = new SubscriptionArbiter(true); + + BooleanSubscription bs1 = new BooleanSubscription(); + BooleanSubscription bs2 = new BooleanSubscription(); + + sa.setSubscription(bs1); + sa.setSubscription(bs2); + + assertTrue(bs1.isCancelled()); + assertFalse(bs2.isCancelled()); + } + + @Test + public void noCancelSlowPathReplace() { + SubscriptionArbiter sa = new SubscriptionArbiter(false); + + BooleanSubscription bs1 = new BooleanSubscription(); + BooleanSubscription bs2 = new BooleanSubscription(); + BooleanSubscription bs3 = new BooleanSubscription(); + + sa.setSubscription(bs1); + + sa.getAndIncrement(); + + sa.setSubscription(bs2); + sa.setSubscription(bs3); + + sa.drainLoop(); + + assertFalse(bs1.isCancelled()); + assertFalse(bs2.isCancelled()); + assertFalse(bs3.isCancelled()); + } + + @Test + public void cancelSlowPathReplace() { + SubscriptionArbiter sa = new SubscriptionArbiter(true); + + BooleanSubscription bs1 = new BooleanSubscription(); + BooleanSubscription bs2 = new BooleanSubscription(); + BooleanSubscription bs3 = new BooleanSubscription(); + + sa.setSubscription(bs1); + + sa.getAndIncrement(); + + sa.setSubscription(bs2); + sa.setSubscription(bs3); + + sa.drainLoop(); + + assertTrue(bs1.isCancelled()); + assertTrue(bs2.isCancelled()); + assertFalse(bs3.isCancelled()); + } + + @Test + public void noCancelSlowPath() { + SubscriptionArbiter sa = new SubscriptionArbiter(false); + + BooleanSubscription bs1 = new BooleanSubscription(); + BooleanSubscription bs2 = new BooleanSubscription(); + + sa.setSubscription(bs1); + + sa.getAndIncrement(); + + sa.setSubscription(bs2); + + sa.drainLoop(); + + assertFalse(bs1.isCancelled()); + assertFalse(bs2.isCancelled()); + } + + @Test + public void cancelSlowPath() { + SubscriptionArbiter sa = new SubscriptionArbiter(true); + + BooleanSubscription bs1 = new BooleanSubscription(); + BooleanSubscription bs2 = new BooleanSubscription(); + + sa.setSubscription(bs1); + + sa.getAndIncrement(); + + sa.setSubscription(bs2); + + sa.drainLoop(); + + assertTrue(bs1.isCancelled()); + assertFalse(bs2.isCancelled()); + } + + @Test + public void moreProducedViolationFastPath() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + SubscriptionArbiter sa = new SubscriptionArbiter(true); + + sa.produced(2); + + assertEquals(0, sa.requested); + + TestHelper.assertError(errors, 0, IllegalStateException.class, "More produced than requested: -2"); + } finally { + RxJavaPlugins.reset(); + } + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/subscriptions/SubscriptionHelperTest.java b/src/test/java/io/reactivex/rxjava3/internal/subscriptions/SubscriptionHelperTest.java new file mode 100644 index 0000000000..9bb222fbb8 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/subscriptions/SubscriptionHelperTest.java @@ -0,0 +1,263 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.subscriptions; + +import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.*; + +import java.util.List; +import java.util.concurrent.atomic.*; + +import org.junit.Test; +import org.reactivestreams.Subscription; + +import io.reactivex.rxjava3.core.RxJavaTest; +import io.reactivex.rxjava3.exceptions.ProtocolViolationException; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class SubscriptionHelperTest extends RxJavaTest { + + @Test + public void checkEnum() { + TestHelper.checkEnum(SubscriptionHelper.class); + } + + @Test + public void validateNullThrows() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + SubscriptionHelper.validate(null, null); + + TestHelper.assertError(errors, 0, NullPointerException.class, "next is null"); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void cancelNoOp() { + SubscriptionHelper.CANCELLED.cancel(); + } + + @Test + public void set() { + AtomicReference<Subscription> atomicSubscription = new AtomicReference<>(); + + BooleanSubscription bs1 = new BooleanSubscription(); + + assertTrue(SubscriptionHelper.set(atomicSubscription, bs1)); + + BooleanSubscription bs2 = new BooleanSubscription(); + + assertTrue(SubscriptionHelper.set(atomicSubscription, bs2)); + + assertTrue(bs1.isCancelled()); + + assertFalse(bs2.isCancelled()); + } + + @Test + public void replace() { + AtomicReference<Subscription> atomicSubscription = new AtomicReference<>(); + + BooleanSubscription bs1 = new BooleanSubscription(); + + assertTrue(SubscriptionHelper.replace(atomicSubscription, bs1)); + + BooleanSubscription bs2 = new BooleanSubscription(); + + assertTrue(SubscriptionHelper.replace(atomicSubscription, bs2)); + + assertFalse(bs1.isCancelled()); + + assertFalse(bs2.isCancelled()); + } + + @Test + public void cancelRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final AtomicReference<Subscription> atomicSubscription = new AtomicReference<>(); + + Runnable r = new Runnable() { + @Override + public void run() { + SubscriptionHelper.cancel(atomicSubscription); + } + }; + + TestHelper.race(r, r); + } + } + + @Test + public void setRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final AtomicReference<Subscription> atomicSubscription = new AtomicReference<>(); + + final BooleanSubscription bs1 = new BooleanSubscription(); + final BooleanSubscription bs2 = new BooleanSubscription(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + SubscriptionHelper.set(atomicSubscription, bs1); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + SubscriptionHelper.set(atomicSubscription, bs2); + } + }; + + TestHelper.race(r1, r2); + + assertTrue(bs1.isCancelled() ^ bs2.isCancelled()); + } + } + + @Test + public void replaceRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final AtomicReference<Subscription> atomicSubscription = new AtomicReference<>(); + + final BooleanSubscription bs1 = new BooleanSubscription(); + final BooleanSubscription bs2 = new BooleanSubscription(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + SubscriptionHelper.replace(atomicSubscription, bs1); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + SubscriptionHelper.replace(atomicSubscription, bs2); + } + }; + + TestHelper.race(r1, r2); + + assertFalse(bs1.isCancelled()); + assertFalse(bs2.isCancelled()); + } + } + + @Test + public void cancelAndChange() { + AtomicReference<Subscription> atomicSubscription = new AtomicReference<>(); + + SubscriptionHelper.cancel(atomicSubscription); + + BooleanSubscription bs1 = new BooleanSubscription(); + assertFalse(SubscriptionHelper.set(atomicSubscription, bs1)); + assertTrue(bs1.isCancelled()); + + assertFalse(SubscriptionHelper.set(atomicSubscription, null)); + + BooleanSubscription bs2 = new BooleanSubscription(); + assertFalse(SubscriptionHelper.replace(atomicSubscription, bs2)); + assertTrue(bs2.isCancelled()); + + assertFalse(SubscriptionHelper.replace(atomicSubscription, null)); + } + + @Test + public void invalidDeferredRequest() { + AtomicReference<Subscription> atomicSubscription = new AtomicReference<>(); + AtomicLong r = new AtomicLong(); + + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + SubscriptionHelper.deferredRequest(atomicSubscription, r, -99); + + TestHelper.assertError(errors, 0, IllegalArgumentException.class, "n > 0 required but it was -99"); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void deferredRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final AtomicReference<Subscription> atomicSubscription = new AtomicReference<>(); + final AtomicLong r = new AtomicLong(); + + final AtomicLong q = new AtomicLong(); + + final Subscription a = new Subscription() { + @Override + public void request(long n) { + q.addAndGet(n); + } + + @Override + public void cancel() { + + } + }; + + Runnable r1 = new Runnable() { + @Override + public void run() { + SubscriptionHelper.deferredSetOnce(atomicSubscription, r, a); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + SubscriptionHelper.deferredRequest(atomicSubscription, r, 1); + } + }; + + TestHelper.race(r1, r2); + + assertSame(a, atomicSubscription.get()); + assertEquals(1, q.get()); + assertEquals(0, r.get()); + } + } + + @Test + public void setOnceAndRequest() { + AtomicReference<Subscription> ref = new AtomicReference<>(); + + Subscription sub = mock(Subscription.class); + + assertTrue(SubscriptionHelper.setOnce(ref, sub, 1)); + + verify(sub).request(1); + verify(sub, never()).cancel(); + + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + sub = mock(Subscription.class); + + assertFalse(SubscriptionHelper.setOnce(ref, sub, 1)); + + verify(sub, never()).request(anyLong()); + verify(sub).cancel(); + + TestHelper.assertError(errors, 0, ProtocolViolationException.class); + } finally { + RxJavaPlugins.reset(); + } + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/util/AtomicThrowableTest.java b/src/test/java/io/reactivex/rxjava3/internal/util/AtomicThrowableTest.java new file mode 100644 index 0000000000..152039fac3 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/util/AtomicThrowableTest.java @@ -0,0 +1,333 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.util; + +import static org.junit.Assert.*; + +import java.util.List; + +import io.reactivex.rxjava3.disposables.Disposable; +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.internal.subscriptions.BooleanSubscription; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.subscribers.TestSubscriber; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class AtomicThrowableTest extends RxJavaTest { + + @Test + public void isTerminated() { + AtomicThrowable ex = new AtomicThrowable(); + + assertFalse(ex.isTerminated()); + + assertNull(ex.terminate()); + + assertTrue(ex.isTerminated()); + } + + @Test + public void tryTerminateAndReportNull() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + + AtomicThrowable ex = new AtomicThrowable(); + ex.tryTerminateAndReport(); + + assertTrue("" + errors, errors.isEmpty()); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void tryTerminateAndReportAlreadyTerminated() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + + AtomicThrowable ex = new AtomicThrowable(); + ex.terminate(); + + ex.tryTerminateAndReport(); + + assertTrue("" + errors, errors.isEmpty()); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void tryTerminateAndReportHasError() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + + AtomicThrowable ex = new AtomicThrowable(); + ex.set(new TestException()); + + ex.tryTerminateAndReport(); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + + assertEquals(1, errors.size()); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void tryTerminateConsumerSubscriberNoError() { + TestSubscriber<Object> ts = new TestSubscriber<>(); + ts.onSubscribe(new BooleanSubscription()); + + AtomicThrowable ex = new AtomicThrowable(); + ex.tryTerminateConsumer(ts); + ts.assertResult(); + } + + @Test + public void tryTerminateConsumerSubscriberError() { + TestSubscriber<Object> ts = new TestSubscriber<>(); + ts.onSubscribe(new BooleanSubscription()); + + AtomicThrowable ex = new AtomicThrowable(); + ex.set(new TestException()); + ex.tryTerminateConsumer(ts); + ts.assertFailure(TestException.class); + } + + @Test + public void tryTerminateConsumerSubscriberTerminated() { + TestSubscriber<Object> ts = new TestSubscriber<>(); + ts.onSubscribe(new BooleanSubscription()); + + AtomicThrowable ex = new AtomicThrowable(); + ex.terminate(); + ex.tryTerminateConsumer(ts); + ts.assertEmpty(); + } + + @Test + public void tryTerminateConsumerObserverNoError() { + TestObserver<Object> to = new TestObserver<>(); + to.onSubscribe(Disposable.empty()); + + AtomicThrowable ex = new AtomicThrowable(); + ex.tryTerminateConsumer((Observer<Object>)to); + to.assertResult(); + } + + @Test + public void tryTerminateConsumerObserverError() { + TestObserver<Object> to = new TestObserver<>(); + to.onSubscribe(Disposable.empty()); + + AtomicThrowable ex = new AtomicThrowable(); + ex.set(new TestException()); + ex.tryTerminateConsumer((Observer<Object>)to); + to.assertFailure(TestException.class); + } + + @Test + public void tryTerminateConsumerObserverTerminated() { + TestObserver<Object> to = new TestObserver<>(); + to.onSubscribe(Disposable.empty()); + + AtomicThrowable ex = new AtomicThrowable(); + ex.terminate(); + ex.tryTerminateConsumer((Observer<Object>)to); + to.assertEmpty(); + } + + @Test + public void tryTerminateConsumerMaybeObserverNoError() { + TestObserver<Object> to = new TestObserver<>(); + to.onSubscribe(Disposable.empty()); + + AtomicThrowable ex = new AtomicThrowable(); + ex.tryTerminateConsumer((MaybeObserver<Object>)to); + to.assertResult(); + } + + @Test + public void tryTerminateConsumerMaybeObserverError() { + TestObserver<Object> to = new TestObserver<>(); + to.onSubscribe(Disposable.empty()); + + AtomicThrowable ex = new AtomicThrowable(); + ex.set(new TestException()); + ex.tryTerminateConsumer((MaybeObserver<Object>)to); + to.assertFailure(TestException.class); + } + + @Test + public void tryTerminateConsumerMaybeObserverTerminated() { + TestObserver<Object> to = new TestObserver<>(); + to.onSubscribe(Disposable.empty()); + + AtomicThrowable ex = new AtomicThrowable(); + ex.terminate(); + ex.tryTerminateConsumer((MaybeObserver<Object>)to); + to.assertEmpty(); + } + + @Test + public void tryTerminateConsumerSingleNoError() { + TestObserver<Object> to = new TestObserver<>(); + to.onSubscribe(Disposable.empty()); + + AtomicThrowable ex = new AtomicThrowable(); + ex.tryTerminateConsumer((SingleObserver<Object>)to); + to.assertEmpty(); + } + + @Test + public void tryTerminateConsumerSingleError() { + TestObserver<Object> to = new TestObserver<>(); + to.onSubscribe(Disposable.empty()); + + AtomicThrowable ex = new AtomicThrowable(); + ex.set(new TestException()); + ex.tryTerminateConsumer((SingleObserver<Object>)to); + to.assertFailure(TestException.class); + } + + @Test + public void tryTerminateConsumerSingleTerminated() { + TestObserver<Object> to = new TestObserver<>(); + to.onSubscribe(Disposable.empty()); + + AtomicThrowable ex = new AtomicThrowable(); + ex.terminate(); + ex.tryTerminateConsumer((SingleObserver<Object>)to); + to.assertEmpty(); + } + + @Test + public void tryTerminateConsumerCompletableObserverNoError() { + TestObserver<Object> to = new TestObserver<>(); + to.onSubscribe(Disposable.empty()); + + AtomicThrowable ex = new AtomicThrowable(); + ex.tryTerminateConsumer((CompletableObserver)to); + to.assertResult(); + } + + @Test + public void tryTerminateConsumerCompletableObserverError() { + TestObserver<Object> to = new TestObserver<>(); + to.onSubscribe(Disposable.empty()); + + AtomicThrowable ex = new AtomicThrowable(); + ex.set(new TestException()); + ex.tryTerminateConsumer((CompletableObserver)to); + to.assertFailure(TestException.class); + } + + @Test + public void tryTerminateConsumerCompletableObserverTerminated() { + TestObserver<Object> to = new TestObserver<>(); + to.onSubscribe(Disposable.empty()); + + AtomicThrowable ex = new AtomicThrowable(); + ex.terminate(); + ex.tryTerminateConsumer((CompletableObserver)to); + to.assertEmpty(); + } + + static <T> Emitter<T> wrapToEmitter(final Observer<T> observer) { + return new Emitter<T>() { + @Override + public void onNext(T value) { + observer.onNext(value); + } + + @Override + public void onError(Throwable error) { + observer.onError(error); + } + + @Override + public void onComplete() { + observer.onComplete(); + } + }; + } + + @Test + public void tryTerminateConsumerEmitterNoError() { + TestObserver<Object> to = new TestObserver<>(); + to.onSubscribe(Disposable.empty()); + + AtomicThrowable ex = new AtomicThrowable(); + ex.tryTerminateConsumer(wrapToEmitter(to)); + to.assertResult(); + } + + @Test + public void tryTerminateConsumerEmitterError() { + TestObserver<Object> to = new TestObserver<>(); + to.onSubscribe(Disposable.empty()); + + AtomicThrowable ex = new AtomicThrowable(); + ex.set(new TestException()); + ex.tryTerminateConsumer(wrapToEmitter(to)); + to.assertFailure(TestException.class); + } + + @Test + public void tryTerminateConsumerEmitterTerminated() { + TestObserver<Object> to = new TestObserver<>(); + to.onSubscribe(Disposable.empty()); + + AtomicThrowable ex = new AtomicThrowable(); + ex.terminate(); + ex.tryTerminateConsumer(wrapToEmitter(to)); + to.assertEmpty(); + } + + @Test + public void tryAddThrowableOrReportNull() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + + AtomicThrowable ex = new AtomicThrowable(); + ex.tryAddThrowableOrReport(new TestException()); + + assertTrue("" + errors, errors.isEmpty()); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void tryAddThrowableOrReportTerminated() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + + AtomicThrowable ex = new AtomicThrowable(); + ex.terminate(); + + assertFalse(ex.tryAddThrowableOrReport(new TestException())); + + assertFalse("" + errors, errors.isEmpty()); + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/util/BackpressureHelperTest.java b/src/test/java/io/reactivex/rxjava3/internal/util/BackpressureHelperTest.java new file mode 100644 index 0000000000..d13cde57be --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/util/BackpressureHelperTest.java @@ -0,0 +1,163 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.util; + +import static org.junit.Assert.assertEquals; + +import java.util.List; +import java.util.concurrent.atomic.AtomicLong; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.RxJavaTest; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class BackpressureHelperTest extends RxJavaTest { + @Test + public void constructorShouldBePrivate() { + TestHelper.checkUtilityClass(BackpressureHelper.class); + } + + @Test + public void addCap() { + assertEquals(2L, BackpressureHelper.addCap(1, 1)); + assertEquals(Long.MAX_VALUE, BackpressureHelper.addCap(1, Long.MAX_VALUE - 1)); + assertEquals(Long.MAX_VALUE, BackpressureHelper.addCap(1, Long.MAX_VALUE)); + assertEquals(Long.MAX_VALUE, BackpressureHelper.addCap(Long.MAX_VALUE - 1, Long.MAX_VALUE - 1)); + assertEquals(Long.MAX_VALUE, BackpressureHelper.addCap(Long.MAX_VALUE, Long.MAX_VALUE)); + } + + @Test + public void multiplyCap() { + assertEquals(6, BackpressureHelper.multiplyCap(2, 3)); + assertEquals(Long.MAX_VALUE, BackpressureHelper.multiplyCap(2, Long.MAX_VALUE)); + assertEquals(Long.MAX_VALUE, BackpressureHelper.multiplyCap(Long.MAX_VALUE, Long.MAX_VALUE)); + assertEquals(Long.MAX_VALUE, BackpressureHelper.multiplyCap(1L << 32, 1L << 32)); + + } + + @Test + public void producedMore() { + List<Throwable> list = TestHelper.trackPluginErrors(); + + try { + AtomicLong requested = new AtomicLong(1); + + assertEquals(0, BackpressureHelper.produced(requested, 2)); + + TestHelper.assertError(list, 0, IllegalStateException.class, "More produced than requested: -1"); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void producedMoreCancel() { + List<Throwable> list = TestHelper.trackPluginErrors(); + + try { + AtomicLong requested = new AtomicLong(1); + + assertEquals(0, BackpressureHelper.producedCancel(requested, 2)); + + TestHelper.assertError(list, 0, IllegalStateException.class, "More produced than requested: -1"); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void requestProduceRace() { + final AtomicLong requested = new AtomicLong(1); + + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + + Runnable r1 = new Runnable() { + @Override + public void run() { + BackpressureHelper.produced(requested, 1); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + BackpressureHelper.add(requested, 1); + } + }; + + TestHelper.race(r1, r2); + } + } + + @Test + public void requestCancelProduceRace() { + final AtomicLong requested = new AtomicLong(1); + + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + + Runnable r1 = new Runnable() { + @Override + public void run() { + BackpressureHelper.produced(requested, 1); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + BackpressureHelper.addCancel(requested, 1); + } + }; + + TestHelper.race(r1, r2); + } + } + + @Test + public void utilityClass() { + TestHelper.checkUtilityClass(BackpressureHelper.class); + } + + @Test + public void capped() { + final AtomicLong requested = new AtomicLong(Long.MIN_VALUE); + + assertEquals(Long.MIN_VALUE, BackpressureHelper.addCancel(requested, 1)); + assertEquals(Long.MIN_VALUE, BackpressureHelper.addCancel(requested, Long.MAX_VALUE)); + + requested.set(0); + + assertEquals(0, BackpressureHelper.addCancel(requested, Long.MAX_VALUE)); + assertEquals(Long.MAX_VALUE, BackpressureHelper.addCancel(requested, 1)); + assertEquals(Long.MAX_VALUE, BackpressureHelper.addCancel(requested, Long.MAX_VALUE)); + + requested.set(0); + + assertEquals(0, BackpressureHelper.add(requested, Long.MAX_VALUE)); + assertEquals(Long.MAX_VALUE, BackpressureHelper.add(requested, 1)); + assertEquals(Long.MAX_VALUE, BackpressureHelper.add(requested, Long.MAX_VALUE)); + + assertEquals(Long.MAX_VALUE, BackpressureHelper.produced(requested, 1)); + assertEquals(Long.MAX_VALUE, BackpressureHelper.produced(requested, Long.MAX_VALUE)); + } + + @Test + public void multiplyCap2() { + assertEquals(Long.MAX_VALUE, BackpressureHelper.multiplyCap(3, Long.MAX_VALUE >> 1)); + + assertEquals(Long.MAX_VALUE, BackpressureHelper.multiplyCap(1, Long.MAX_VALUE)); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/util/BlockingHelperTest.java b/src/test/java/io/reactivex/rxjava3/internal/util/BlockingHelperTest.java new file mode 100644 index 0000000000..0218185f08 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/util/BlockingHelperTest.java @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.util; + +import static org.junit.Assert.*; + +import java.util.concurrent.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.RxJavaTest; +import io.reactivex.rxjava3.disposables.*; +import io.reactivex.rxjava3.schedulers.Schedulers; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class BlockingHelperTest extends RxJavaTest { + + @Test + public void emptyEnum() { + TestHelper.checkUtilityClass(BlockingHelper.class); + } + + @Test + public void interrupted() { + CountDownLatch cdl = new CountDownLatch(1); + Disposable d = Disposable.empty(); + + Thread.currentThread().interrupt(); + + try { + BlockingHelper.awaitForComplete(cdl, d); + } catch (IllegalStateException ex) { + // expected + } + assertTrue(d.isDisposed()); + assertTrue(Thread.interrupted()); + } + + @Test + public void unblock() { + final CountDownLatch cdl = new CountDownLatch(1); + Disposable d = Disposable.empty(); + + Schedulers.computation().scheduleDirect(new Runnable() { + @Override + public void run() { + cdl.countDown(); + } + }, 100, TimeUnit.MILLISECONDS); + + BlockingHelper.awaitForComplete(cdl, d); + + assertFalse(d.isDisposed()); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/util/CrashingIterable.java b/src/test/java/io/reactivex/rxjava3/internal/util/CrashingIterable.java new file mode 100644 index 0000000000..6bdbea27b1 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/util/CrashingIterable.java @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.util; + +import java.util.Iterator; + +import io.reactivex.rxjava3.exceptions.TestException; + +/** + * An Iterable and Iterator that crashes with TestException after the given number + * of method invocations on iterator(), hasNext() and next(). + */ +public final class CrashingIterable implements Iterable<Integer> { + int crashOnIterator; + + final int crashOnHasNext; + + final int crashOnNext; + + public CrashingIterable(int crashOnIterator, int crashOnHasNext, int crashOnNext) { + this.crashOnIterator = crashOnIterator; + this.crashOnHasNext = crashOnHasNext; + this.crashOnNext = crashOnNext; + } + + @Override + public Iterator<Integer> iterator() { + if (--crashOnIterator <= 0) { + throw new TestException("iterator()"); + } + return new CrashingIterator(crashOnHasNext, crashOnNext); + } + + static final class CrashingIterator implements Iterator<Integer> { + int crashOnHasNext; + + int crashOnNext; + + int count; + + CrashingIterator(int crashOnHasNext, int crashOnNext) { + this.crashOnHasNext = crashOnHasNext; + this.crashOnNext = crashOnNext; + } + + @Override + public boolean hasNext() { + if (--crashOnHasNext <= 0) { + throw new TestException("hasNext()"); + } + return true; + } + + @Override + public Integer next() { + if (--crashOnNext <= 0) { + throw new TestException("next()"); + } + return count++; + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/util/CrashingMappedIterable.java b/src/test/java/io/reactivex/rxjava3/internal/util/CrashingMappedIterable.java new file mode 100644 index 0000000000..d6b9491e2b --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/util/CrashingMappedIterable.java @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.util; + +import java.util.Iterator; + +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.Function; + +/** + * An Iterable and Iterator that crashes with TestException after the given number + * of method invocations on iterator(), hasNext() and next(). + * + * @param <T> the result type + */ +public final class CrashingMappedIterable<T> implements Iterable<T> { + int crashOnIterator; + + final int crashOnHasNext; + + final int crashOnNext; + + final Function<Integer, T> mapper; + + public CrashingMappedIterable(int crashOnIterator, int crashOnHasNext, int crashOnNext, Function<Integer, T> mapper) { + this.crashOnIterator = crashOnIterator; + this.crashOnHasNext = crashOnHasNext; + this.crashOnNext = crashOnNext; + this.mapper = mapper; + } + + @Override + public Iterator<T> iterator() { + if (--crashOnIterator <= 0) { + throw new TestException("iterator()"); + } + return new CrashingMapperIterator<>(crashOnHasNext, crashOnNext, mapper); + } + + static final class CrashingMapperIterator<T> implements Iterator<T> { + int crashOnHasNext; + + int crashOnNext; + + int count; + + final Function<Integer, T> mapper; + + CrashingMapperIterator(int crashOnHasNext, int crashOnNext, Function<Integer, T> mapper) { + this.crashOnHasNext = crashOnHasNext; + this.crashOnNext = crashOnNext; + this.mapper = mapper; + } + + @Override + public boolean hasNext() { + if (--crashOnHasNext <= 0) { + throw new TestException("hasNext()"); + } + return true; + } + + @Override + public T next() { + if (--crashOnNext <= 0) { + throw new TestException("next()"); + } + try { + return mapper.apply(count++); + } catch (Throwable ex) { + throw ExceptionHelper.wrapOrThrow(ex); + } + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/util/EndConsumerHelperTest.java b/src/test/java/io/reactivex/rxjava3/internal/util/EndConsumerHelperTest.java new file mode 100644 index 0000000000..8ad1942946 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/util/EndConsumerHelperTest.java @@ -0,0 +1,516 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.util; + +import static org.junit.Assert.*; + +import java.util.List; + +import org.junit.*; +import org.reactivestreams.Subscriber; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.*; +import io.reactivex.rxjava3.exceptions.ProtocolViolationException; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; +import io.reactivex.rxjava3.internal.subscriptions.*; +import io.reactivex.rxjava3.observers.*; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.subscribers.*; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class EndConsumerHelperTest extends RxJavaTest { + + List<Throwable> errors; + + @Before + public void before() { + errors = TestHelper.trackPluginErrors(); + } + + @After + public void after() { + RxJavaPlugins.reset(); + } + + @Test + public void utilityClass() { + TestHelper.checkUtilityClass(EndConsumerHelper.class); + } + + @Test + public void checkDoubleDefaultSubscriber() { + Subscriber<Integer> consumer = new DefaultSubscriber<Integer>() { + @Override + public void onNext(Integer t) { + } + + @Override + public void onError(Throwable t) { + } + + @Override + public void onComplete() { + } + }; + + BooleanSubscription sub1 = new BooleanSubscription(); + + consumer.onSubscribe(sub1); + + assertFalse(sub1.isCancelled()); + + BooleanSubscription sub2 = new BooleanSubscription(); + + consumer.onSubscribe(sub2); + + assertFalse(sub1.isCancelled()); + + assertTrue(sub2.isCancelled()); + + TestHelper.assertError(errors, 0, ProtocolViolationException.class); + assertEquals(EndConsumerHelper.composeMessage(consumer.getClass().getName()), errors.get(0).getMessage()); + assertEquals(errors.toString(), 1, errors.size()); + } + + static final class EndDefaultSubscriber extends DefaultSubscriber<Integer> { + @Override + public void onNext(Integer t) { + } + + @Override + public void onError(Throwable t) { + } + + @Override + public void onComplete() { + } + } + + @Test + public void checkDoubleDefaultSubscriberNonAnonymous() { + Subscriber<Integer> consumer = new EndDefaultSubscriber(); + + BooleanSubscription sub1 = new BooleanSubscription(); + + consumer.onSubscribe(sub1); + + assertFalse(sub1.isCancelled()); + + BooleanSubscription sub2 = new BooleanSubscription(); + + consumer.onSubscribe(sub2); + + assertFalse(sub1.isCancelled()); + + assertTrue(sub2.isCancelled()); + + TestHelper.assertError(errors, 0, ProtocolViolationException.class); + + // with this consumer, the class name should be predictable + assertEquals(EndConsumerHelper.composeMessage("io.reactivex.rxjava3.internal.util.EndConsumerHelperTest$EndDefaultSubscriber"), errors.get(0).getMessage()); + assertEquals(errors.toString(), 1, errors.size()); + } + + @Test + public void checkDoubleDisposableSubscriber() { + Subscriber<Integer> consumer = new DisposableSubscriber<Integer>() { + @Override + public void onNext(Integer t) { + } + + @Override + public void onError(Throwable t) { + } + + @Override + public void onComplete() { + } + }; + + BooleanSubscription sub1 = new BooleanSubscription(); + + consumer.onSubscribe(sub1); + + assertFalse(sub1.isCancelled()); + + BooleanSubscription sub2 = new BooleanSubscription(); + + consumer.onSubscribe(sub2); + + assertFalse(sub1.isCancelled()); + + assertTrue(sub2.isCancelled()); + + TestHelper.assertError(errors, 0, ProtocolViolationException.class); + assertEquals(EndConsumerHelper.composeMessage(consumer.getClass().getName()), errors.get(0).getMessage()); + assertEquals(errors.toString(), 1, errors.size()); + } + + @Test + public void checkDoubleResourceSubscriber() { + Subscriber<Integer> consumer = new ResourceSubscriber<Integer>() { + @Override + public void onNext(Integer t) { + } + + @Override + public void onError(Throwable t) { + } + + @Override + public void onComplete() { + } + }; + + BooleanSubscription sub1 = new BooleanSubscription(); + + consumer.onSubscribe(sub1); + + assertFalse(sub1.isCancelled()); + + BooleanSubscription sub2 = new BooleanSubscription(); + + consumer.onSubscribe(sub2); + + assertFalse(sub1.isCancelled()); + + assertTrue(sub2.isCancelled()); + + TestHelper.assertError(errors, 0, ProtocolViolationException.class); + assertEquals(EndConsumerHelper.composeMessage(consumer.getClass().getName()), errors.get(0).getMessage()); + assertEquals(errors.toString(), 1, errors.size()); + } + + @Test + public void checkDoubleDefaultObserver() { + Observer<Integer> consumer = new DefaultObserver<Integer>() { + @Override + public void onNext(Integer t) { + } + + @Override + public void onError(Throwable t) { + } + + @Override + public void onComplete() { + } + }; + + Disposable sub1 = Disposable.empty(); + + consumer.onSubscribe(sub1); + + assertFalse(sub1.isDisposed()); + + Disposable sub2 = Disposable.empty(); + + consumer.onSubscribe(sub2); + + assertFalse(sub1.isDisposed()); + + assertTrue(sub2.isDisposed()); + + TestHelper.assertError(errors, 0, ProtocolViolationException.class); + assertEquals(EndConsumerHelper.composeMessage(consumer.getClass().getName()), errors.get(0).getMessage()); + assertEquals(errors.toString(), 1, errors.size()); + } + + @Test + public void checkDoubleDisposableObserver() { + Observer<Integer> consumer = new DisposableObserver<Integer>() { + @Override + public void onNext(Integer t) { + } + + @Override + public void onError(Throwable t) { + } + + @Override + public void onComplete() { + } + }; + + Disposable sub1 = Disposable.empty(); + + consumer.onSubscribe(sub1); + + assertFalse(sub1.isDisposed()); + + Disposable sub2 = Disposable.empty(); + + consumer.onSubscribe(sub2); + + assertFalse(sub1.isDisposed()); + + assertTrue(sub2.isDisposed()); + + TestHelper.assertError(errors, 0, ProtocolViolationException.class); + assertEquals(EndConsumerHelper.composeMessage(consumer.getClass().getName()), errors.get(0).getMessage()); + assertEquals(errors.toString(), 1, errors.size()); + } + + @Test + public void checkDoubleResourceObserver() { + Observer<Integer> consumer = new ResourceObserver<Integer>() { + @Override + public void onNext(Integer t) { + } + + @Override + public void onError(Throwable t) { + } + + @Override + public void onComplete() { + } + }; + + Disposable sub1 = Disposable.empty(); + + consumer.onSubscribe(sub1); + + assertFalse(sub1.isDisposed()); + + Disposable sub2 = Disposable.empty(); + + consumer.onSubscribe(sub2); + + assertFalse(sub1.isDisposed()); + + assertTrue(sub2.isDisposed()); + + TestHelper.assertError(errors, 0, ProtocolViolationException.class); + assertEquals(EndConsumerHelper.composeMessage(consumer.getClass().getName()), errors.get(0).getMessage()); + assertEquals(errors.toString(), 1, errors.size()); + } + + @Test + public void checkDoubleDisposableSingleObserver() { + SingleObserver<Integer> consumer = new DisposableSingleObserver<Integer>() { + @Override + public void onSuccess(Integer t) { + } + + @Override + public void onError(Throwable t) { + } + }; + + Disposable sub1 = Disposable.empty(); + + consumer.onSubscribe(sub1); + + assertFalse(sub1.isDisposed()); + + Disposable sub2 = Disposable.empty(); + + consumer.onSubscribe(sub2); + + assertFalse(sub1.isDisposed()); + + assertTrue(sub2.isDisposed()); + + TestHelper.assertError(errors, 0, ProtocolViolationException.class); + assertEquals(EndConsumerHelper.composeMessage(consumer.getClass().getName()), errors.get(0).getMessage()); + assertEquals(errors.toString(), 1, errors.size()); + } + + @Test + public void checkDoubleResourceSingleObserver() { + SingleObserver<Integer> consumer = new ResourceSingleObserver<Integer>() { + @Override + public void onSuccess(Integer t) { + } + + @Override + public void onError(Throwable t) { + } + }; + + Disposable sub1 = Disposable.empty(); + + consumer.onSubscribe(sub1); + + assertFalse(sub1.isDisposed()); + + Disposable sub2 = Disposable.empty(); + + consumer.onSubscribe(sub2); + + assertFalse(sub1.isDisposed()); + + assertTrue(sub2.isDisposed()); + + TestHelper.assertError(errors, 0, ProtocolViolationException.class); + assertEquals(EndConsumerHelper.composeMessage(consumer.getClass().getName()), errors.get(0).getMessage()); + assertEquals(errors.toString(), 1, errors.size()); + } + + @Test + public void checkDoubleDisposableMaybeObserver() { + MaybeObserver<Integer> consumer = new DisposableMaybeObserver<Integer>() { + @Override + public void onSuccess(Integer t) { + } + + @Override + public void onError(Throwable t) { + } + + @Override + public void onComplete() { + } + }; + + Disposable sub1 = Disposable.empty(); + + consumer.onSubscribe(sub1); + + assertFalse(sub1.isDisposed()); + + Disposable sub2 = Disposable.empty(); + + consumer.onSubscribe(sub2); + + assertFalse(sub1.isDisposed()); + + assertTrue(sub2.isDisposed()); + + TestHelper.assertError(errors, 0, ProtocolViolationException.class); + assertEquals(EndConsumerHelper.composeMessage(consumer.getClass().getName()), errors.get(0).getMessage()); + assertEquals(errors.toString(), 1, errors.size()); + } + + @Test + public void checkDoubleResourceMaybeObserver() { + MaybeObserver<Integer> consumer = new ResourceMaybeObserver<Integer>() { + @Override + public void onSuccess(Integer t) { + } + + @Override + public void onError(Throwable t) { + } + + @Override + public void onComplete() { + } + }; + + Disposable sub1 = Disposable.empty(); + + consumer.onSubscribe(sub1); + + assertFalse(sub1.isDisposed()); + + Disposable sub2 = Disposable.empty(); + + consumer.onSubscribe(sub2); + + assertFalse(sub1.isDisposed()); + + assertTrue(sub2.isDisposed()); + + TestHelper.assertError(errors, 0, ProtocolViolationException.class); + assertEquals(EndConsumerHelper.composeMessage(consumer.getClass().getName()), errors.get(0).getMessage()); + assertEquals(errors.toString(), 1, errors.size()); + } + + @Test + public void checkDoubleDisposableCompletableObserver() { + CompletableObserver consumer = new DisposableCompletableObserver() { + @Override + public void onError(Throwable t) { + } + + @Override + public void onComplete() { + } + }; + + Disposable sub1 = Disposable.empty(); + + consumer.onSubscribe(sub1); + + assertFalse(sub1.isDisposed()); + + Disposable sub2 = Disposable.empty(); + + consumer.onSubscribe(sub2); + + assertFalse(sub1.isDisposed()); + + assertTrue(sub2.isDisposed()); + + TestHelper.assertError(errors, 0, ProtocolViolationException.class); + assertEquals(EndConsumerHelper.composeMessage(consumer.getClass().getName()), errors.get(0).getMessage()); + assertEquals(errors.toString(), 1, errors.size()); + } + + @Test + public void checkDoubleResourceCompletableObserver() { + CompletableObserver consumer = new ResourceCompletableObserver() { + @Override + public void onError(Throwable t) { + } + + @Override + public void onComplete() { + } + }; + + Disposable sub1 = Disposable.empty(); + + consumer.onSubscribe(sub1); + + assertFalse(sub1.isDisposed()); + + Disposable sub2 = Disposable.empty(); + + consumer.onSubscribe(sub2); + + assertFalse(sub1.isDisposed()); + + assertTrue(sub2.isDisposed()); + + TestHelper.assertError(errors, 0, ProtocolViolationException.class); + assertEquals(EndConsumerHelper.composeMessage(consumer.getClass().getName()), errors.get(0).getMessage()); + assertEquals(errors.toString(), 1, errors.size()); + } + + @Test + public void validateDisposable() { + Disposable d1 = Disposable.empty(); + + assertFalse(EndConsumerHelper.validate(DisposableHelper.DISPOSED, d1, getClass())); + + assertTrue(d1.isDisposed()); + + assertTrue(errors.toString(), errors.isEmpty()); + } + + @Test + public void validateSubscription() { + BooleanSubscription bs1 = new BooleanSubscription(); + + assertFalse(EndConsumerHelper.validate(SubscriptionHelper.CANCELLED, bs1, getClass())); + + assertTrue(bs1.isCancelled()); + + assertTrue(errors.toString(), errors.isEmpty()); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/util/ExceptionHelperTest.java b/src/test/java/io/reactivex/rxjava3/internal/util/ExceptionHelperTest.java new file mode 100644 index 0000000000..130cf5767b --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/util/ExceptionHelperTest.java @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.util; + +import static org.junit.Assert.assertTrue; + +import java.util.concurrent.atomic.AtomicReference; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.RxJavaTest; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class ExceptionHelperTest extends RxJavaTest { + @Test + public void utilityClass() { + TestHelper.checkUtilityClass(ExceptionHelper.class); + } + + @Test + public void addRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + + final AtomicReference<Throwable> error = new AtomicReference<>(); + + final TestException ex = new TestException(); + + Runnable r = new Runnable() { + @Override + public void run() { + assertTrue(ExceptionHelper.addThrowable(error, ex)); + } + }; + + TestHelper.race(r, r); + } + } + + @Test(expected = InternalError.class) + public void throwIfThrowable() throws Exception { + ExceptionHelper.<Exception>throwIfThrowable(new InternalError()); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/util/HalfSerializerObserverTest.java b/src/test/java/io/reactivex/rxjava3/internal/util/HalfSerializerObserverTest.java new file mode 100644 index 0000000000..ea8c405849 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/util/HalfSerializerObserverTest.java @@ -0,0 +1,276 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.util; + +import static org.junit.Assert.assertTrue; + +import java.io.IOException; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.testsupport.*; + +public class HalfSerializerObserverTest extends RxJavaTest { + + @Test + @SuppressWarnings({ "rawtypes", "unchecked" }) + public void reentrantOnNextOnNext() { + final AtomicInteger wip = new AtomicInteger(); + final AtomicThrowable error = new AtomicThrowable(); + + final Observer[] a = { null }; + + final TestObserver to = new TestObserver(); + + Observer observer = new Observer() { + @Override + public void onSubscribe(Disposable d) { + to.onSubscribe(d); + } + + @Override + public void onNext(Object t) { + if (t.equals(1)) { + HalfSerializer.onNext(a[0], 2, wip, error); + } + to.onNext(t); + } + + @Override + public void onError(Throwable t) { + to.onError(t); + } + + @Override + public void onComplete() { + to.onComplete(); + } + }; + + a[0] = observer; + + observer.onSubscribe(Disposable.empty()); + + HalfSerializer.onNext(observer, 1, wip, error); + + to.assertValue(1).assertNoErrors().assertNotComplete(); + } + + @Test + @SuppressWarnings({ "rawtypes", "unchecked" }) + public void reentrantOnNextOnError() { + final AtomicInteger wip = new AtomicInteger(); + final AtomicThrowable error = new AtomicThrowable(); + + final Observer[] a = { null }; + + final TestObserver to = new TestObserver(); + + Observer observer = new Observer() { + @Override + public void onSubscribe(Disposable d) { + to.onSubscribe(d); + } + + @Override + public void onNext(Object t) { + if (t.equals(1)) { + HalfSerializer.onError(a[0], new TestException(), wip, error); + } + to.onNext(t); + } + + @Override + public void onError(Throwable t) { + to.onError(t); + } + + @Override + public void onComplete() { + to.onComplete(); + } + }; + + a[0] = observer; + + observer.onSubscribe(Disposable.empty()); + + HalfSerializer.onNext(observer, 1, wip, error); + + to.assertFailure(TestException.class, 1); + } + + @Test + @SuppressWarnings({ "rawtypes", "unchecked" }) + public void reentrantOnNextOnComplete() { + final AtomicInteger wip = new AtomicInteger(); + final AtomicThrowable error = new AtomicThrowable(); + + final Observer[] a = { null }; + + final TestObserver to = new TestObserver(); + + Observer observer = new Observer() { + @Override + public void onSubscribe(Disposable d) { + to.onSubscribe(d); + } + + @Override + public void onNext(Object t) { + if (t.equals(1)) { + HalfSerializer.onComplete(a[0], wip, error); + } + to.onNext(t); + } + + @Override + public void onError(Throwable t) { + to.onError(t); + } + + @Override + public void onComplete() { + to.onComplete(); + } + }; + + a[0] = observer; + + observer.onSubscribe(Disposable.empty()); + + HalfSerializer.onNext(observer, 1, wip, error); + + to.assertResult(1); + } + + @Test + @SuppressUndeliverable + @SuppressWarnings({ "rawtypes", "unchecked" }) + public void reentrantErrorOnError() { + final AtomicInteger wip = new AtomicInteger(); + final AtomicThrowable error = new AtomicThrowable(); + + final Observer[] a = { null }; + + final TestObserver to = new TestObserver(); + + Observer observer = new Observer() { + @Override + public void onSubscribe(Disposable d) { + to.onSubscribe(d); + } + + @Override + public void onNext(Object t) { + to.onNext(t); + } + + @Override + public void onError(Throwable t) { + to.onError(t); + HalfSerializer.onError(a[0], new IOException(), wip, error); + } + + @Override + public void onComplete() { + to.onComplete(); + } + }; + + a[0] = observer; + + observer.onSubscribe(Disposable.empty()); + + HalfSerializer.onError(observer, new TestException(), wip, error); + + to.assertFailure(TestException.class); + } + + @Test + public void onNextOnCompleteRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + + final AtomicInteger wip = new AtomicInteger(); + final AtomicThrowable error = new AtomicThrowable(); + + final TestObserver<Integer> to = new TestObserver<>(); + to.onSubscribe(Disposable.empty()); + + Runnable r1 = new Runnable() { + @Override + public void run() { + HalfSerializer.onNext(to, 1, wip, error); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + HalfSerializer.onComplete(to, wip, error); + } + }; + + TestHelper.race(r1, r2); + + to.assertComplete().assertNoErrors(); + + assertTrue(to.values().size() <= 1); + } + } + + @Test + @SuppressUndeliverable + public void onErrorOnCompleteRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + + final AtomicInteger wip = new AtomicInteger(); + final AtomicThrowable error = new AtomicThrowable(); + + final TestObserverEx<Integer> to = new TestObserverEx<>(); + + to.onSubscribe(Disposable.empty()); + + final TestException ex = new TestException(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + HalfSerializer.onError(to, ex, wip, error); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + HalfSerializer.onComplete(to, wip, error); + } + }; + + TestHelper.race(r1, r2); + + if (to.completions() != 0) { + to.assertResult(); + } else { + to.assertFailure(TestException.class); + } + } + } + +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/util/HalfSerializerSubscriberTest.java b/src/test/java/io/reactivex/rxjava3/internal/util/HalfSerializerSubscriberTest.java new file mode 100644 index 0000000000..1919864648 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/util/HalfSerializerSubscriberTest.java @@ -0,0 +1,282 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.util; + +import static org.junit.Assert.assertTrue; + +import java.io.IOException; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.Test; +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.internal.subscriptions.BooleanSubscription; +import io.reactivex.rxjava3.subscribers.TestSubscriber; +import io.reactivex.rxjava3.testsupport.*; + +public class HalfSerializerSubscriberTest extends RxJavaTest { + + @Test + public void utilityClass() { + TestHelper.checkUtilityClass(HalfSerializer.class); + } + + @Test + @SuppressWarnings({ "rawtypes", "unchecked" }) + public void reentrantOnNextOnNext() { + final AtomicInteger wip = new AtomicInteger(); + final AtomicThrowable error = new AtomicThrowable(); + + final Subscriber[] a = { null }; + + final TestSubscriber ts = new TestSubscriber(); + + FlowableSubscriber s = new FlowableSubscriber() { + @Override + public void onSubscribe(Subscription s) { + ts.onSubscribe(s); + } + + @Override + public void onNext(Object t) { + if (t.equals(1)) { + HalfSerializer.onNext(a[0], 2, wip, error); + } + ts.onNext(t); + } + + @Override + public void onError(Throwable t) { + ts.onError(t); + } + + @Override + public void onComplete() { + ts.onComplete(); + } + }; + + a[0] = s; + + s.onSubscribe(new BooleanSubscription()); + + HalfSerializer.onNext(s, 1, wip, error); + + ts.assertValue(1).assertNoErrors().assertNotComplete(); + } + + @Test + @SuppressWarnings({ "rawtypes", "unchecked" }) + public void reentrantOnNextOnError() { + final AtomicInteger wip = new AtomicInteger(); + final AtomicThrowable error = new AtomicThrowable(); + + final Subscriber[] a = { null }; + + final TestSubscriber ts = new TestSubscriber(); + + FlowableSubscriber s = new FlowableSubscriber() { + @Override + public void onSubscribe(Subscription s) { + ts.onSubscribe(s); + } + + @Override + public void onNext(Object t) { + if (t.equals(1)) { + HalfSerializer.onError(a[0], new TestException(), wip, error); + } + ts.onNext(t); + } + + @Override + public void onError(Throwable t) { + ts.onError(t); + } + + @Override + public void onComplete() { + ts.onComplete(); + } + }; + + a[0] = s; + + s.onSubscribe(new BooleanSubscription()); + + HalfSerializer.onNext(s, 1, wip, error); + + ts.assertFailure(TestException.class, 1); + } + + @Test + @SuppressWarnings({ "rawtypes", "unchecked" }) + public void reentrantOnNextOnComplete() { + final AtomicInteger wip = new AtomicInteger(); + final AtomicThrowable error = new AtomicThrowable(); + + final Subscriber[] a = { null }; + + final TestSubscriber ts = new TestSubscriber(); + + FlowableSubscriber s = new FlowableSubscriber() { + @Override + public void onSubscribe(Subscription s) { + ts.onSubscribe(s); + } + + @Override + public void onNext(Object t) { + if (t.equals(1)) { + HalfSerializer.onComplete(a[0], wip, error); + } + ts.onNext(t); + } + + @Override + public void onError(Throwable t) { + ts.onError(t); + } + + @Override + public void onComplete() { + ts.onComplete(); + } + }; + + a[0] = s; + + s.onSubscribe(new BooleanSubscription()); + + HalfSerializer.onNext(s, 1, wip, error); + + ts.assertResult(1); + } + + @Test + @SuppressUndeliverable + @SuppressWarnings({ "rawtypes", "unchecked" }) + public void reentrantErrorOnError() { + final AtomicInteger wip = new AtomicInteger(); + final AtomicThrowable error = new AtomicThrowable(); + + final Subscriber[] a = { null }; + + final TestSubscriber ts = new TestSubscriber(); + + FlowableSubscriber s = new FlowableSubscriber() { + @Override + public void onSubscribe(Subscription s) { + ts.onSubscribe(s); + } + + @Override + public void onNext(Object t) { + ts.onNext(t); + } + + @Override + public void onError(Throwable t) { + ts.onError(t); + HalfSerializer.onError(a[0], new IOException(), wip, error); + } + + @Override + public void onComplete() { + ts.onComplete(); + } + }; + + a[0] = s; + + s.onSubscribe(new BooleanSubscription()); + + HalfSerializer.onError(s, new TestException(), wip, error); + + ts.assertFailure(TestException.class); + } + + @Test + public void onNextOnCompleteRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + + final AtomicInteger wip = new AtomicInteger(); + final AtomicThrowable error = new AtomicThrowable(); + + final TestSubscriber<Integer> ts = new TestSubscriber<>(); + ts.onSubscribe(new BooleanSubscription()); + + Runnable r1 = new Runnable() { + @Override + public void run() { + HalfSerializer.onNext(ts, 1, wip, error); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + HalfSerializer.onComplete(ts, wip, error); + } + }; + + TestHelper.race(r1, r2); + + ts.assertComplete().assertNoErrors(); + + assertTrue(ts.values().size() <= 1); + } + } + + @Test + @SuppressUndeliverable + public void onErrorOnCompleteRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + + final AtomicInteger wip = new AtomicInteger(); + final AtomicThrowable error = new AtomicThrowable(); + + final TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(); + + ts.onSubscribe(new BooleanSubscription()); + + final TestException ex = new TestException(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + HalfSerializer.onError(ts, ex, wip, error); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + HalfSerializer.onComplete(ts, wip, error); + } + }; + + TestHelper.race(r1, r2); + + if (ts.completions() != 0) { + ts.assertResult(); + } else { + ts.assertFailure(TestException.class); + } + } + } + +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/util/JavadocNoThrows.java b/src/test/java/io/reactivex/rxjava3/internal/util/JavadocNoThrows.java new file mode 100644 index 0000000000..1a55807dd0 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/util/JavadocNoThrows.java @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.util; + +import java.io.File; +import java.nio.file.Files; +import java.util.List; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.parallel.ParallelFlowable; +import io.reactivex.rxjava3.testsupport.TestHelper; + +/** + * Scan the JavaDocs of the base classes and list those which do not have the {@code @throws} tag. + * The lack is not an error by itself but worth looking at. + */ +public final class JavadocNoThrows { + + private JavadocNoThrows() { + throw new IllegalArgumentException("No instances!"); + } + + public static void main(String[] args) throws Exception { + for (Class<?> clazz : CLASSES) { + String clazzName = clazz.getSimpleName(); + String packageName = clazz.getPackage().getName(); + File f = TestHelper.findSource(clazzName, packageName); + + List<String> lines = Files.readAllLines(f.toPath()); + + for (int i = 1; i < lines.size(); i++) { + String line = lines.get(i).trim(); + + if (line.startsWith("/**")) { + boolean found = false; + for (int j = i + 1; j < lines.size(); j++) { + + String line2 = lines.get(j).trim(); + if (line2.startsWith("public")) { + if (line2.endsWith("() {")) { + found = true; + } + break; + } + if (line2.startsWith("* @throws")) { + found = true; + break; + } + } + + if (!found) { + System.out.printf(" at %s.%s.method(%s.java:%s)%n%n", packageName, clazzName, clazzName, i + 1); + } + } + } + } + } + + static final Class<?>[] CLASSES = { + Flowable.class, Observable.class, Maybe.class, Single.class, Completable.class, ParallelFlowable.class + }; +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/util/MarbleDimensions.java b/src/test/java/io/reactivex/rxjava3/internal/util/MarbleDimensions.java new file mode 100644 index 0000000000..2fdc7fe2d6 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/util/MarbleDimensions.java @@ -0,0 +1,107 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.util; + +import java.awt.image.BufferedImage; +import java.io.*; +import java.net.URL; +import java.nio.file.Files; +import java.util.*; +import java.util.regex.*; + +import javax.imageio.ImageIO; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.parallel.ParallelFlowable; +import io.reactivex.rxjava3.testsupport.TestHelper; + +/** + * Parses the main sources, locates the {@code <img>} tags, downloads + * the referenced image and checks if the scaled dimensions are correct. + */ +public final class MarbleDimensions { + + /** Helper program. */ + private MarbleDimensions() { + throw new IllegalStateException("No instances!"); + } + + public static void main(String[] args) throws Throwable { + Pattern p = Pattern.compile("\\s*\\*\\s*\\<img\\s+width\\=('|\")(\\d+)('|\")\\s+height\\=('|\")(\\d+)('|\")\\s+src\\=('|\")(.+?)('|\").*"); + + Map<String, Integer[]> dimensions = new HashMap<>(); + + for (Class<?> clazz : CLASSES) { + String simpleName = clazz.getSimpleName(); + System.out.println(simpleName); + System.out.println("----"); + String packageName = clazz.getPackage().getName(); + + File f = TestHelper.findSource(clazz.getSimpleName(), packageName); + if (f == null) { + System.err.println("Unable to locate " + clazz); + continue; + } + + List<String> lines = Files.readAllLines(f.toPath()); + + for (int i = 0; i < lines.size(); i++) { + Matcher m = p.matcher(lines.get(i)); + if (m.matches()) { + int width = Integer.parseInt(m.group(2)); + int height = Integer.parseInt(m.group(5)); + String url = m.group(8); + + Integer[] imageDim = dimensions.get(url); + if (imageDim == null) { + Thread.sleep(SLEEP_PER_IMAGE_MILLIS); + + try { + BufferedImage bimg = ImageIO.read(new URL(url)); + + if (bimg == null) { + throw new IOException("not found"); + } + imageDim = new Integer[] { 0, 0 }; + imageDim[0] = bimg.getWidth(); + imageDim[1] = bimg.getHeight(); + + dimensions.put(url, imageDim); + } catch (IOException ex) { + System.err.printf("%s => %s%n", url, ex); + System.err.printf(" at %s.%s.method(%s.java:%d)%n", packageName, simpleName, simpleName, i + 1); + } + } + + if (imageDim != null) { + int expectedHeight = (int)Math.round(1.0 * width / imageDim[0] * imageDim[1]); + + if (expectedHeight != height) { + System.out.printf(" %d => %d%n", height, expectedHeight); + System.out.printf(" at %s.%s.method(%s.java:%d)%n", packageName, simpleName, simpleName, i + 1); + } + } + // System.out.printf("%d: %d x %d => %s%n", i + 1, width, height, url); + } + } + } + } + + static final int SLEEP_PER_IMAGE_MILLIS = 25; + + static final Class<?>[] CLASSES = { + Flowable.class, Observable.class, Maybe.class, Single.class, Completable.class, ParallelFlowable.class + }; +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/util/MergerBiFunctionTest.java b/src/test/java/io/reactivex/rxjava3/internal/util/MergerBiFunctionTest.java new file mode 100644 index 0000000000..b8a5f8b843 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/util/MergerBiFunctionTest.java @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.util; + +import static org.junit.Assert.assertEquals; + +import java.util.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.RxJavaTest; + +public class MergerBiFunctionTest extends RxJavaTest { + + @Test + public void firstEmpty() throws Exception { + MergerBiFunction<Integer> merger = new MergerBiFunction<>(new Comparator<Integer>() { + @Override + public int compare(Integer o1, Integer o2) { + return o1.compareTo(o2); + } + }); + List<Integer> list = merger.apply(Collections.<Integer>emptyList(), Arrays.asList(3, 5)); + + assertEquals(Arrays.asList(3, 5), list); + } + + @Test + public void bothEmpty() throws Exception { + MergerBiFunction<Integer> merger = new MergerBiFunction<>(new Comparator<Integer>() { + @Override + public int compare(Integer o1, Integer o2) { + return o1.compareTo(o2); + } + }); + List<Integer> list = merger.apply(Collections.<Integer>emptyList(), Collections.<Integer>emptyList()); + + assertEquals(Collections.<Integer>emptyList(), list); + } + + @Test + public void secondEmpty() throws Exception { + MergerBiFunction<Integer> merger = new MergerBiFunction<>(new Comparator<Integer>() { + @Override + public int compare(Integer o1, Integer o2) { + return o1.compareTo(o2); + } + }); + List<Integer> list = merger.apply(Arrays.asList(2, 4), Collections.<Integer>emptyList()); + + assertEquals(Arrays.asList(2, 4), list); + } + + @Test + public void sameSize() throws Exception { + MergerBiFunction<Integer> merger = new MergerBiFunction<>(new Comparator<Integer>() { + @Override + public int compare(Integer o1, Integer o2) { + return o1.compareTo(o2); + } + }); + List<Integer> list = merger.apply(Arrays.asList(2, 4), Arrays.asList(3, 5)); + + assertEquals(Arrays.asList(2, 3, 4, 5), list); + } + + @Test + public void sameSizeReverse() throws Exception { + MergerBiFunction<Integer> merger = new MergerBiFunction<>(new Comparator<Integer>() { + @Override + public int compare(Integer o1, Integer o2) { + return o1.compareTo(o2); + } + }); + List<Integer> list = merger.apply(Arrays.asList(3, 5), Arrays.asList(2, 4)); + + assertEquals(Arrays.asList(2, 3, 4, 5), list); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/util/MiscUtilTest.java b/src/test/java/io/reactivex/rxjava3/internal/util/MiscUtilTest.java new file mode 100644 index 0000000000..adc779a302 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/util/MiscUtilTest.java @@ -0,0 +1,266 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.util; + +import static org.junit.Assert.*; + +import java.util.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.RxJavaTest; +import io.reactivex.rxjava3.functions.BiPredicate; +import io.reactivex.rxjava3.internal.util.AppendOnlyLinkedArrayList.NonThrowingPredicate; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class MiscUtilTest extends RxJavaTest { + @Test + public void pow2UtilityClass() { + TestHelper.checkUtilityClass(Pow2.class); + } + + @Test + public void isPowerOf2() { + for (int i = 1; i > 0; i *= 2) { + assertTrue(Pow2.isPowerOfTwo(i)); + } + + assertFalse(Pow2.isPowerOfTwo(3)); + assertFalse(Pow2.isPowerOfTwo(5)); + assertFalse(Pow2.isPowerOfTwo(6)); + assertFalse(Pow2.isPowerOfTwo(7)); + } + + @Test + public void hashMapSupplier() { + TestHelper.checkEnum(HashMapSupplier.class); + } + + @Test + public void arrayListSupplier() { + TestHelper.checkEnum(ArrayListSupplier.class); + } + + @Test + public void errorModeEnum() { + TestHelper.checkEnum(ErrorMode.class); + } + + @Test + public void linkedArrayList() { + LinkedArrayList list = new LinkedArrayList(2); + assertEquals(0, list.size()); + list.add(1); + assertEquals(1, list.size()); + list.add(2); + assertEquals(2, list.size()); + list.add(3); + assertEquals(3, list.size()); + assertEquals("[1, 2, 3]", list.toString()); + } + + @Test + public void appendOnlyLinkedArrayListForEachWhile() throws Exception { + AppendOnlyLinkedArrayList<Integer> list = new AppendOnlyLinkedArrayList<>(2); + + list.add(1); + list.add(2); + list.add(3); + + final List<Integer> out = new ArrayList<>(); + + list.forEachWhile(new NonThrowingPredicate<Integer>() { + @Override + public boolean test(Integer t2) { + out.add(t2); + return t2 == 2; + } + }); + + assertEquals(Arrays.asList(1, 2), out); + } + + @Test + public void appendOnlyLinkedArrayListForEachWhileBi() throws Throwable { + AppendOnlyLinkedArrayList<Integer> list = new AppendOnlyLinkedArrayList<>(2); + + list.add(1); + list.add(2); + list.add(3); + + final List<Integer> out = new ArrayList<>(); + + list.forEachWhile(2, new BiPredicate<Integer, Integer>() { + @Override + public boolean test(Integer t1, Integer t2) throws Throwable { + out.add(t2); + return t1.equals(t2); + } + }); + + assertEquals(Arrays.asList(1, 2), out); + } + + @Test + public void appendOnlyLinkedArrayListForEachWhilePreGrow() throws Exception { + AppendOnlyLinkedArrayList<Integer> list = new AppendOnlyLinkedArrayList<>(12); + + list.add(1); + list.add(2); + list.add(3); + + final List<Integer> out = new ArrayList<>(); + + list.forEachWhile(new NonThrowingPredicate<Integer>() { + @Override + public boolean test(Integer t2) { + out.add(t2); + return t2 == 2; + } + }); + + assertEquals(Arrays.asList(1, 2), out); + } + + @Test + public void appendOnlyLinkedArrayListForEachWhileExact() throws Exception { + AppendOnlyLinkedArrayList<Integer> list = new AppendOnlyLinkedArrayList<>(3); + + list.add(1); + list.add(2); + list.add(3); + + final List<Integer> out = new ArrayList<>(); + + list.forEachWhile(new NonThrowingPredicate<Integer>() { + @Override + public boolean test(Integer t2) { + out.add(t2); + return t2 == 2; + } + }); + + assertEquals(Arrays.asList(1, 2), out); + } + + @Test + public void appendOnlyLinkedArrayListForEachWhileAll() throws Exception { + AppendOnlyLinkedArrayList<Integer> list = new AppendOnlyLinkedArrayList<>(2); + + list.add(1); + list.add(2); + list.add(3); + + final List<Integer> out = new ArrayList<>(); + + list.forEachWhile(new NonThrowingPredicate<Integer>() { + @Override + public boolean test(Integer t2) { + out.add(t2); + return t2 == 3; + } + }); + + assertEquals(Arrays.asList(1, 2, 3), out); + } + + @Test + public void appendOnlyLinkedArrayListForEachWhileBigger() throws Exception { + AppendOnlyLinkedArrayList<Integer> list = new AppendOnlyLinkedArrayList<>(4); + + list.add(1); + list.add(2); + list.add(3); + + final List<Integer> out = new ArrayList<>(); + + list.forEachWhile(new NonThrowingPredicate<Integer>() { + @Override + public boolean test(Integer t2) { + out.add(t2); + return false; + } + }); + + assertEquals(Arrays.asList(1, 2, 3), out); + } + + @Test + public void appendOnlyLinkedArrayListForEachWhileBiPreGrow() throws Throwable { + AppendOnlyLinkedArrayList<Integer> list = new AppendOnlyLinkedArrayList<>(12); + + list.add(1); + list.add(2); + list.add(3); + + final List<Integer> out = new ArrayList<>(); + + list.forEachWhile(2, new BiPredicate<Integer, Integer>() { + @Override + public boolean test(Integer t1, Integer t2) throws Throwable { + out.add(t2); + return t1.equals(t2); + } + }); + + assertEquals(Arrays.asList(1, 2), out); + } + + @Test + public void appendOnlyLinkedArrayListForEachWhileBiExact() throws Throwable { + AppendOnlyLinkedArrayList<Integer> list = new AppendOnlyLinkedArrayList<>(3); + + list.add(1); + list.add(2); + list.add(3); + + final List<Integer> out = new ArrayList<>(); + + list.forEachWhile(2, new BiPredicate<Integer, Integer>() { + @Override + public boolean test(Integer t1, Integer t2) throws Exception { + out.add(t2); + return t1.equals(t2); + } + }); + + assertEquals(Arrays.asList(1, 2), out); + } + + @Test + public void appendOnlyLinkedArrayListForEachWhileBiAll() throws Throwable { + AppendOnlyLinkedArrayList<Integer> list = new AppendOnlyLinkedArrayList<>(2); + + list.add(1); + list.add(2); + list.add(3); + + final List<Integer> out = new ArrayList<>(); + + list.forEachWhile(3, new BiPredicate<Integer, Integer>() { + @Override + public boolean test(Integer t1, Integer t2) throws Exception { + out.add(t2); + return false; + } + }); + + assertEquals(Arrays.asList(1, 2, 3), out); + } + + @Test + public void queueDrainHelperUtility() { + TestHelper.checkUtilityClass(QueueDrainHelper.class); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/util/NotificationLiteTest.java b/src/test/java/io/reactivex/rxjava3/internal/util/NotificationLiteTest.java new file mode 100644 index 0000000000..7638a98ead --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/util/NotificationLiteTest.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.util; + +import static org.junit.Assert.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.RxJavaTest; +import io.reactivex.rxjava3.disposables.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.testsupport.TestObserverEx; + +public class NotificationLiteTest extends RxJavaTest { + + @Test + public void acceptFullObserver() { + TestObserverEx<Integer> to = new TestObserverEx<>(); + + Disposable d = Disposable.empty(); + + assertFalse(NotificationLite.acceptFull(NotificationLite.disposable(d), to)); + + to.assertSubscribed(); + + to.dispose(); + + assertTrue(d.isDisposed()); + } + + @Test + public void errorNotificationCompare() { + TestException ex = new TestException(); + Object n1 = NotificationLite.error(ex); + + assertEquals(ex.hashCode(), n1.hashCode()); + + assertNotEquals(n1, NotificationLite.complete()); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/util/ObservableToFlowabeTestSync.java b/src/test/java/io/reactivex/rxjava3/internal/util/ObservableToFlowabeTestSync.java new file mode 100644 index 0000000000..7bf668e2f1 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/util/ObservableToFlowabeTestSync.java @@ -0,0 +1,119 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.util; + +import java.io.*; +import java.lang.reflect.Method; +import java.util.*; + +/** + * Utility class that lists tests related to Observable that is not present in Flowable tests. + */ +public final class ObservableToFlowabeTestSync { + private ObservableToFlowabeTestSync() { + throw new IllegalStateException("No instances!"); + } + + static List<String> readAllLines(File f) { + List<String> result = new ArrayList<>(); + try { + BufferedReader in = new BufferedReader(new FileReader(f)); + try { + String line; + + while ((line = in.readLine()) != null) { + result.add(line); + } + } finally { + in.close(); + } + } catch (IOException ex) { + ex.printStackTrace(); + } + return result; + } + + static void list(String basepath, String basepackage) throws Exception { + File[] observables = new File(basepath + "observable/").listFiles(); + + int count = 0; + + for (File f : observables) { + if (!f.getName().endsWith(".java")) { + continue; + } + Class<?> clazz = Class.forName(basepackage + "observable." + f.getName().replace(".java", "")); + + String cn = f.getName().replace(".java", "").replace("Observable", "Flowable"); + + File f2 = new File(basepath + "/flowable/" + cn + ".java"); + + if (!f2.exists()) { + continue; + } + + Class<?> clazz2 = Class.forName(basepackage + "flowable." + cn); + + Set<String> methods2 = new HashSet<>(); + + for (Method m : clazz2.getMethods()) { + methods2.add(m.getName()); + } + + for (Method m : clazz.getMethods()) { + if (!methods2.contains(m.getName()) && !methods2.contains(m.getName().replace("Observable", "Flowable"))) { + count++; + System.out.println(); + System.out.print("java.lang.RuntimeException: missing > "); + System.out.println(m.getName()); + System.out.print(" at "); + System.out.print(clazz.getName()); + System.out.print(" ("); + System.out.print(clazz.getSimpleName()); + System.out.print(".java:"); + + List<String> lines = readAllLines(f); + + int j = 1; + for (int i = 1; i <= lines.size(); i++) { + if (lines.get(i - 1).contains("public void " + m.getName() + "(")) { + j = i; + } + } + System.out.print(j); + System.out.println(")"); + + System.out.print(" at "); + System.out.print(clazz2.getName()); + System.out.print(" ("); + System.out.print(clazz2.getSimpleName()); + + lines = readAllLines(f2); + + System.out.print(".java:"); + System.out.print(lines.size() - 1); + System.out.println(")"); + } + } + } + + System.out.println(); + System.out.println(count); + } + + public static void main(String[] args) throws Exception { + list("src/test/java/io/reactivex/internal/operators/", "io.reactivex.internal.operators."); +// list("src/test/java/io/reactivex/", "io.reactivex."); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/util/OpenHashSetTest.java b/src/test/java/io/reactivex/rxjava3/internal/util/OpenHashSetTest.java new file mode 100644 index 0000000000..f8c1bdf04d --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/util/OpenHashSetTest.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.util; + +import static org.junit.Assert.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.RxJavaTest; + +public class OpenHashSetTest extends RxJavaTest { + + static class Value { + + @Override + public int hashCode() { + return 1; + } + + @Override + public boolean equals(Object o) { + return this == o; + } + } + + @Test + public void addRemoveCollision() { + Value v1 = new Value(); + Value v2 = new Value(); + + OpenHashSet<Value> set = new OpenHashSet<>(); + + assertTrue(set.add(v1)); + + assertFalse(set.add(v1)); + + assertFalse(set.remove(v2)); + + assertTrue(set.add(v2)); + + assertFalse(set.add(v2)); + + assertTrue(set.remove(v2)); + + assertFalse(set.remove(v2)); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/util/OperatorArgumentNaming.java b/src/test/java/io/reactivex/rxjava3/internal/util/OperatorArgumentNaming.java new file mode 100644 index 0000000000..1814a26bbc --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/util/OperatorArgumentNaming.java @@ -0,0 +1,169 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.util; + +import java.lang.reflect.*; +import java.util.*; + +import org.reactivestreams.*; + +import com.google.common.base.Strings; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.core.Observer; +import io.reactivex.rxjava3.core.Observable; + +/** + * Compare method argument naming across base classes. + * This is not a full test because some naming mismatch is legitimate, such as singular in Maybe/Single and + * plural in Flowable/Observable + */ +public final class OperatorArgumentNaming { + + private OperatorArgumentNaming() { + throw new IllegalStateException("No instances!"); + } + + /** Classes to compare with each other. */ + static final Class<?>[] CLASSES = { Flowable.class, Observable.class, Maybe.class, Single.class, Completable.class }; + + /** Types that refer to a reactive type and is generally matching the parent class; for comparison, these have to be unified. */ + static final Set<Class<?>> BASE_TYPE_SET = new HashSet<>(Arrays.asList( + Flowable.class, Publisher.class, Subscriber.class, FlowableSubscriber.class, + Observable.class, ObservableSource.class, Observer.class, + Maybe.class, MaybeSource.class, MaybeObserver.class, + Single.class, SingleSource.class, SingleObserver.class, + Completable.class, CompletableSource.class, CompletableObserver.class + )); + + public static void main(String[] args) { + // className -> methodName -> overloads -> arguments + Map<String, Map<String, List<List<ArgumentNameAndType>>>> map = new HashMap<>(); + + for (Class<?> clazz : CLASSES) { + Map<String, List<List<ArgumentNameAndType>>> classMethods = map.computeIfAbsent(clazz.getSimpleName(), v -> new HashMap<>()); + for (Method method : clazz.getDeclaredMethods()) { + if (method.getDeclaringClass() == clazz && method.getParameterCount() != 0) { + List<List<ArgumentNameAndType>> overloads = classMethods.computeIfAbsent(method.getName(), v -> new ArrayList<>()); + + List<ArgumentNameAndType> overload = new ArrayList<>(); + overloads.add(overload); + + for (Parameter param : method.getParameters()) { + String typeName; + Class<?> type = param.getType(); + if (type.isArray()) { + Class<?> componentType = type.getComponentType(); + if (BASE_TYPE_SET.contains(componentType)) { + typeName = "BaseType"; + } else { + typeName = type.getComponentType().getSimpleName() + "[]"; + } + } else + if (BASE_TYPE_SET.contains(type)) { + typeName = "BaseType"; + } else { + typeName = type.getSimpleName(); + } + String name = param.getName(); + if (name.equals("bufferSize") || name.equals("prefetch") || name.equals("capacityHint")) { + name = "bufferSize|prefetch|capacityHint"; + } + if (name.equals("subscriber") || name.equals("observer")) { + name = "subscriber|observer"; + } + if (name.contains("onNext")) { + name = name.replace("onNext", "onNext|onSuccess"); + } else + if (name.contains("onSuccess")) { + name = name.replace("onSuccess", "onNext|onSuccess"); + } + overload.add(new ArgumentNameAndType(typeName, name)); + } + } + } + } + + int counter = 0; + + for (int i = 0; i < CLASSES.length - 1; i++) { + String firstName = CLASSES[i].getSimpleName(); + Map<String, List<List<ArgumentNameAndType>>> firstClassMethods = map.get(firstName); + for (int j = i + 1; j < CLASSES.length; j++) { + String secondName = CLASSES[j].getSimpleName(); + Map<String, List<List<ArgumentNameAndType>>> secondClassMethods = map.get(secondName); + + for (Map.Entry<String, List<List<ArgumentNameAndType>>> methodOverloadsFirst : firstClassMethods.entrySet()) { + + List<List<ArgumentNameAndType>> methodOverloadsSecond = secondClassMethods.get(methodOverloadsFirst.getKey()); + + if (methodOverloadsSecond != null) { + for (List<ArgumentNameAndType> overloadFirst : methodOverloadsFirst.getValue()) { + for (List<ArgumentNameAndType> overloadSecond : methodOverloadsSecond) { + if (overloadFirst.size() == overloadSecond.size()) { + // Argument types match? + boolean match = true; + for (int k = 0; k < overloadFirst.size(); k++) { + if (!overloadFirst.get(k).type.equals(overloadSecond.get(k).type)) { + match = false; + break; + } + } + // Argument names match? + if (match) { + for (int k = 0; k < overloadFirst.size(); k++) { + if (!overloadFirst.get(k).name.equals(overloadSecond.get(k).name)) { + System.out.print("Argument naming mismatch #"); + System.out.println(++counter); + + System.out.print(" "); + System.out.print(Strings.padEnd(firstName, Math.max(firstName.length(), secondName.length()) + 1, ' ')); + System.out.print(methodOverloadsFirst.getKey()); + System.out.print(" "); + System.out.println(overloadFirst); + + System.out.print(" "); + System.out.print(Strings.padEnd(secondName, Math.max(firstName.length(), secondName.length()) + 1, ' ')); + System.out.print(methodOverloadsFirst.getKey()); + System.out.print(" "); + System.out.println(overloadSecond); + System.out.println(); + break; + } + } + } + } + } + } + } + } + } + } + } + + static final class ArgumentNameAndType { + final String type; + final String name; + + ArgumentNameAndType(String type, String name) { + this.type = type; + this.name = name; + } + + @Override + public String toString() { + return type + " " + name; + } + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/util/OperatorMatrixGenerator.java b/src/test/java/io/reactivex/rxjava3/internal/util/OperatorMatrixGenerator.java new file mode 100644 index 0000000000..9c5f058ff4 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/util/OperatorMatrixGenerator.java @@ -0,0 +1,522 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.util; + +import java.io.*; +import java.lang.reflect.Method; +import java.nio.file.*; +import java.util.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.core.Observable; + +/** + * Generate a table of available operators across base classes in {@code Operator-Matrix.md}. + * + * Should be run with the main project directory as working directory where the {@code docs} + * folder is. + */ +public final class OperatorMatrixGenerator { + + private OperatorMatrixGenerator() { + throw new IllegalStateException("No instances!"); + } + + static final String PRESENT = "![present](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_on.png)"; + static final String ABSENT = "![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_off.png)"; + static final String TBD = "![absent](https://raw.github.com/wiki/ReactiveX/RxJava/images/checkmark_half.png)"; + + static final Class<?>[] CLASSES = { + Flowable.class, Observable.class, Maybe.class, Single.class, Completable.class + }; + + static String header(String type) { + return "![" + type + "](https://raw.github.com/wiki/ReactiveX/RxJava/images/opmatrix-" + type.toLowerCase() + ".png)"; + } + + public static void main(String[] args) throws IOException { + Set<String> operatorSet = new HashSet<>(); + Map<Class<?>, Set<String>> operatorMap = new HashMap<>(); + + for (Class<?> clazz : CLASSES) { + Set<String> set = operatorMap.computeIfAbsent(clazz, c -> new HashSet<>()); + + for (Method m : clazz.getMethods()) { + String name = m.getName(); + if (!name.equals("bufferSize") + && m.getDeclaringClass() == clazz + && !m.isSynthetic()) { + operatorSet.add(m.getName()); + set.add(m.getName()); + } + } + } + + List<String> sortedOperators = new ArrayList<>(operatorSet); + sortedOperators.sort(Comparator.naturalOrder()); + + try (PrintWriter out = new PrintWriter(Files.newBufferedWriter(Paths.get("docs", "Operator-Matrix.md"), StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING))) { + out.print("Operator |"); + for (Class<?> clazz : CLASSES) { + out.print(" "); + out.print(header(clazz.getSimpleName())); + out.print(" |"); + } + out.println(); + out.print("-----|"); + for (int i = 0; i < CLASSES.length; i++) { + out.print("---|"); + } + out.println(); + + Map<String, Integer> notesMap = new HashMap<>(); + List<String> notesList = new ArrayList<>(); + List<String> tbdList = new ArrayList<>(); + int[] counters = new int[CLASSES.length]; + + for (String operatorName : sortedOperators) { + out.print("<a name='"); + out.print(operatorName); + out.print("'></a>`"); + out.print(operatorName); + out.print("`|"); + int m = 0; + for (Class<?> clazz : CLASSES) { + if (operatorMap.get(clazz).contains(operatorName)) { + out.print(PRESENT); + counters[m]++; + } else { + String notes = findNotes(clazz.getSimpleName(), operatorName); + if (notes != null) { + out.print(ABSENT); + Integer index = notesMap.get(notes); + if (index == null) { + index = notesMap.size() + 1; + notesMap.put(notes, index); + notesList.add(notes); + } + out.print(" <sup title='"); + out.print(notes.replace("`", "").replace("[", "").replace("]", "").replaceAll("\\(#.+\\)", "")); + out.print("'>(["); + out.print(index); + out.print("](#notes-"); + out.print(index); + out.print("))</sup>"); + } else { + out.print(TBD); + tbdList.add(clazz.getSimpleName() + "." + operatorName + "()"); + } + } + out.print("|"); + m++; + } + out.println(); + } + out.print("<a name='total'></a>**"); + out.print(sortedOperators.size()); + out.print(" operators** |"); + for (int m = 0; m < counters.length; m++) { + out.print(" **"); + out.print(counters[m]); + out.print("** |"); + } + out.println(); + + if (!notesList.isEmpty()) { + out.println(); + out.println("#### Notes"); + + for (int i = 0; i < notesList.size(); i++) { + out.print("<a name='notes-"); + out.print(i + 1); + out.print("'></a><sup>"); + out.print(i + 1); + out.print("</sup> "); + out.print(notesList.get(i)); + out.println("<br/>"); + } + } + if (tbdList.isEmpty()) { + out.println(); + out.println("#### Under development"); + out.println(); + out.println("*Currently, all intended operators are implemented.*"); + } else { + out.println(); + out.println("#### Under development"); + out.println(); + + for (int i = 0; i < tbdList.size(); i++) { + out.print(i + 1); + out.print(". "); + out.println(tbdList.get(i)); + } + } + } + } + + static String findNotes(String clazzName, String operatorName) { + Map<String, String> classNotes = NOTES_MAP.get(operatorName); + if (classNotes != null) { + return classNotes.get(clazzName.substring(0, 1)); + } + switch (operatorName) { + case "empty": { + if ("Completable".equals(clazzName)) { + return "Use [`complete()`](#complete)."; + } + if ("Single".equals(clazzName)) { + return "Never empty."; + } + break; + } + } + return null; + } + + static final String[] NOTES = { + // Format + // FOMSC methodName note + " MS all Use [`contains()`](#contains).", + " C all Always empty.", + "FOMS andThen Use [`concatWith`](#concatWith).", + " MS any Use [`contains()`](#contains).", + " C any Always empty.", + "FO blockingAwait Use [`blockingFirst()`](#blockingFirst), [`blockingSingle()`](#blockingSingle) or [`blockingLast()`](#blockingLast).", + " MS blockingAwait Use [`blockingGet()`](#blockingGet).", + " MS blockingFirst At most one element to get. Use [`blockingGet()`](#blockingGet).", + " C blockingFirst No elements to get. Use [`blockingAwait()`](#blockingAwait).", + " MSC blockingForEach Use [`blockingSubscribe()`](#blockingSubscribe)", + "FO blockingGet Use [`blockingFirst()`](#blockingFirst), [`blockingSingle()`](#blockingSingle) or [`blockingLast()`](#blockingLast).", + " C blockingGet No elements to get. Use [`blockingAwait()`](#blockingAwait).", + " MS blockingIterable At most one element to get. Use [`blockingGet()`](#blockingGet).", + " C blockingIterable No elements to get. Use [`blockingAwait()`](#blockingAwait).", + " MS blockingLast At most one element to get. Use [`blockingGet()`](#blockingGet).", + " C blockingLast No elements to get. Use [`blockingAwait()`](#blockingAwait).", + " MS blockingLatest At most one element to get. Use [`blockingGet()`](#blockingGet).", + " C blockingLatest No elements to get. Use [`blockingAwait()`](#blockingAwait).", + " MS blockingMostRecent At most one element to get. Use [`blockingGet()`](#blockingGet).", + " C blockingMostRecent No elements to get. Use [`blockingAwait()`](#blockingAwait).", + " MS blockingNext At most one element to get. Use [`blockingGet()`](#blockingGet).", + " C blockingNext No elements to get. Use [`blockingAwait()`](#blockingAwait).", + " MS blockingSingle At most one element to get. Use [`blockingGet()`](#blockingGet).", + " C blockingSingle No elements to get. Use [`blockingAwait()`](#blockingAwait).", + " MS blockingStream At most one element to get. Use [`blockingGet()`](#blockingGet).", + " C blockingStream No elements to get. Use [`blockingAwait()`](#blockingAwait).", + " M buffer Use [`map()`](#map) and [`switchIfEmpty()`](#switchIfEmpty) to transform into a list/collection.", + " S buffer Use [`map()`](#map) to transform into a list/collection.", + " C buffer Always empty. Use [`andThen()`](#andThen) to bring in a list/collection.", + " MSC cacheWithInitialCapacity At most one element to store. Use [`cache()`](#cache).", + " C cast Always empty.", + " M collect At most one element to collect. Use [`map()`](#map) and [`switchIfEmpty()`](#switchIfEmpty) to transform into a list/collection.", + " S collect One element to collect. Use [`map()`](#map) to transform into a list/collection.", + " C collect Always empty. Use [`andThen()`](#andThen) to bring in a collection.", + " M collectInto At most one element to collect. Use [`map()`](#map) and [`switchIfEmpty()`](#switchIfEmpty) to transform into a list/collection.", + " S collectInto One element to collect. Use [`map()`](#map) to transform into a list/collection.", + " C collectInto Always empty. Use [`andThen()`](#andThen) to bring in a collection.", + " MS combineLatest At most one element per source. Use [`zip()`](#zip).", + " C combineLatest Always empty. Use [`merge()`](#merge).", + " MS combineLatestArray At most one element per source. Use [`zipArray()`](#zipArray).", + " C combineLatestArray Always empty. Use [`mergeArray()`](#mergeArray).", + " MS combineLatestDelayError At most one element per source. Use [`zip()`](#zip).", + " C combineLatestDelayError Always empty. Use [`mergeDelayError()`](#mergeDelayError).", + " MS combineLatestArrayDelayError At most one element per source. Use [`zipArray()`](#zipArray).", + " C combineLatestArrayDelayError Always empty. Use [`mergeArrayDelayError()`](#mergeArrayDelayError).", + "FOM complete Use [`empty()`](#empty).", + " S complete Never empty.", + " C concatArrayEager No items to keep ordered. Use [`mergeArray()`](#mergeArray).", + " C concatArrayEagerDelayError No items to keep ordered. Use [`mergeArrayDelayError()`](#mergeArrayDelayError).", + " C concatEager No items to keep ordered. Use [`merge()`](#merge).", + " C concatEagerDelayError No items to keep ordered. Use [`mergeDelayError()`](#mergeDelayError).", + " C concatMap Always empty thus no items to map.", + " C concatMapCompletable Always empty thus no items to map.", + " MS concatMapCompletableDelayError Either the upstream fails (thus no inner) or the mapped-in source, but never both. Use [`concatMapCompletable`](#concatMapCompletable).", + " C concatMapCompletableDelayError Always empty thus no items to map.", + " MS concatMapDelayError Either the upstream fails (thus no inner) or the mapped-in source, but never both. Use [`concatMap`](#concatMap).", + " C concatMapDelayError Always empty thus no items to map.", + " MS concatMapEager At most one item to map. Use [`concatMap()`](#concatMap).", + " C concatMapEager Always empty thus no items to map.", + " MS concatMapEagerDelayError At most one item to map. Use [`concatMap()`](#concatMap).", + " C concatMapEagerDelayError Always empty thus no items to map.", + " C concatMapIterable Always empty thus no items to map.", + " M concatMapMaybe Use [`concatMap`](#concatMap).", + " C concatMapMaybe Always empty thus no items to map.", + " MS concatMapMaybeDelayError Either the upstream fails (thus no inner) or the mapped-in source, but never both. Use [`concatMapMaybe`](#concatMapMaybe).", + " C concatMapMaybeDelayError Always empty thus no items to map.", + " S concatMapSingle Use [`concatMap()`](#concatMap).", + " C concatMapSingle Always empty thus no items to map.", + " MS concatMapSingleDelayError Either the upstream fails (thus no inner) or the mapped-in source, but never both. Use [`concatMapSingle`](#concatMapSingle).", + " C concatMapSingleDelayError Always empty thus no items to map.", + " C concatMapStream Always empty thus no items to map.", + " MS concatMapIterable At most one item. Use [`flattenAsFlowable`](#flattenAsFlowable) or [`flattenAsObservable`](#flattenAsObservable).", + " MS concatMapStream At most one item. Use [`flattenStreamAsFlowable`](#flattenStreamAsFlowable) or [`flattenStreamAsObservable`](#flattenStreamAsObservable).", + " C contains Always empty.", + " S count Never empty thus always 1.", + " C count Always empty thus always 0.", + " MS debounce At most one item signaled so no subsequent items to work with.", + " C debounce Always empty thus no items to work with.", + " S defaultIfEmpty Never empty.", + " C defaultIfEmpty Always empty. Use [`andThen()`](#andThen) to chose the follow-up sequence.", + " C dematerialize Always empty thus no items to work with.", + " MS distinct At most one item, always distinct.", + " C distinct Always empty thus no items to work with.", + " MS distinctUntilChanged At most one item, always distinct.", + " C distinctUntilChanged Always empty thus no items to work with.", + " MS doAfterNext Different terminology. Use [`doAfterSuccess()`](#doAfterSuccess).", + " C doAfterNext Always empty.", + "FO doAfterSuccess Different terminology. Use [`doAfterNext()`](#doAfterNext).", + " C doAfterSuccess Always empty thus no items to work with.", + " OMSC doOnCancel Different terminology. Use [`doOnDispose()`](#doOnDispose).", + " S doOnComplete Always succeeds or fails, there is no `onComplete` signal.", + "F doOnDispose Different terminology. Use [`doOnCancel()`](#doOnCancel).", + " MS doOnEach At most one item. Use [`doOnEvent()`](#doOnEvent).", + " C doOnEach Always empty thus no items to work with.", + "FO doOnEvent Use [`doOnEach()`](#doOnEach).", + " MS doOnNext Different terminology. Use [`doOnSuccess()`](#doOnSuccess).", + " C doOnNext Always empty thus no items to work with.", + " OMSC doOnRequest Backpressure related and not supported outside `Flowable`.", + "FO doOnSuccess Different terminology. Use [`doOnNext()`](#doOnNext).", + " C doOnSuccess Always empty thus no items to work with.", + " M elementAt At most one item with index 0. Use [`defaultIfEmpty`](#defaultIfEmpty).", + " S elementAt Always one item with index 0.", + " C elementAt Always empty thus no items to work with.", + " M elementAtOrError At most one item with index 0. Use [`toSingle`](#toSingle).", + " S elementAtOrError Always one item with index 0.", + " C elementAtOrError Always empty thus no items to work with.", + " S empty Never empty.", + " C empty Use [`complete()`](#complete).", + " C filter Always empty thus no items to work with.", + " M first At most one item. Use [`defaultIfEmpty`](#defaultIfEmpty).", + " S first Always one item.", + " C first Always empty. Use [`andThen()`](#andThen) to chose the follow-up sequence.", + " M firstElement At most one item, would be no-op.", + " S firstElement Always one item, would be no-op.", + " C firstElement Always empty.", + " M firstOrError At most one item, would be no-op.", + " S firstOrError Always one item, would be no-op.", + " C firstOrError Always empty. Use [`andThen()`](#andThen) and [`error()`](#error).", + " MS firstOrErrorStage At most one item. Use [`toCompletionStage()`](#toCompletionStage).", + " C firstOrErrorStage Always empty. Use [`andThen()`](#andThen), [`error()`](#error) and [`toCompletionStage()`](#toCompletionStage).", + " MSC firstStage At most one item. Use [`toCompletionStage()`](#toCompletionStage).", + " C flatMap Always empty thus no items to map.", + " C flatMapCompletable Always empty thus no items to map.", + " C flatMapCompletableDelayError Always empty thus no items to map.", + " C flatMapIterable Always empty thus no items to map.", + " M flatMapMaybe Use [`flatMap()`](#flatMap).", + " C flatMapMaybe Always empty thus no items to map.", + " C flatMapMaybeDelayError Always empty thus no items to map.", + " S flatMapSingle Use [`flatMap()`](#flatMap).", + " C flatMapSingle Always empty thus no items to map.", + " C flatMapStream Always empty thus no items to map.", + " MS flatMapIterable At most one item. Use [`flattenAsFlowable`](#flattenAsFlowable) or [`flattenAsObservable`](#flattenAsObservable).", + " MS flatMapStream At most one item. Use [`flattenStreamAsFlowable`](#flattenStreamAsFlowable) or [`flattenStreamAsObservable`](#flattenStreamAsObservable).", + "F flatMapObservable Not supported. Use [`flatMap`](#flatMap) and [`toFlowable()`](#toFlowable).", + " O flatMapObservable Use [`flatMap`](#flatMap).", + " C flatMapObservable Always empty thus no items to map.", + " O flatMapPublisher Not supported. Use [`flatMap`](#flatMap) and [`toObservable()`](#toFlowable).", + "F flatMapPublisher Use [`flatMap`](#flatMap).", + " C flatMapPublisher Always empty thus no items to map.", + "FO flatMapSingleElement Use [`flatMapSingle`](#flatMapSingle).", + " S flatMapSingleElement Use [`flatMap`](#flatMap).", + " C flatMapSingleElement Always empty thus no items to map.", + "FO flattenAsFlowable Use [`flatMapIterable()`](#flatMapIterable).", + " C flattenAsFlowable Always empty thus no items to map.", + "FO flattenAsObservable Use [`flatMapIterable()`](#flatMapIterable).", + " C flattenAsObservable Always empty thus no items to map.", + "FO flattenStreamAsFlowable Use [`flatMapStream()`](#flatMapStream).", + " C flattenStreamAsFlowable Always empty thus no items to map.", + "FO flattenStreamAsObservable Use [`flatMapStream()`](#flatMapStream).", + " C flattenStreamAsObservable Always empty thus no items to map.", + " MSC forEach Use [`subscribe()`](#subscribe).", + " MSC forEachWhile Use [`subscribe()`](#subscribe).", + " S fromAction Never empty.", + " M fromArray At most one item. Use [`just()`](#just) or [`empty()`](#empty).", + " S fromArray Always one item. Use [`just()`](#just).", + " C fromArray Always empty. Use [`complete()`](#complete).", + " S fromCompletable Always error.", + " C fromCompletable Use [`wrap()`](#wrap).", + " M fromIterable At most one item. Use [`just()`](#just) or [`empty()`](#empty).", + " S fromIterable Always one item. Use [`just()`](#just).", + " C fromIterable Always empty. Use [`complete()`](#complete).", + " M fromMaybe Use [`wrap()`](#wrap).", + " O fromObservable Use [`wrap()`](#wrap).", + " S fromOptional Always one item. Use [`just()`](#just).", + " C fromOptional Always empty. Use [`complete()`](#complete).", + " S fromRunnable Never empty.", + " S fromSingle Use [`wrap()`](#wrap).", + " M fromStream At most one item. Use [`just()`](#just) or [`empty()`](#empty).", + " S fromStream Always one item. Use [`just()`](#just).", + " C fromStream Always empty. Use [`complete()`](#complete).", + " MSC generate Use [`fromSupplier()`](#fromSupplier).", + " MS groupBy At most one item.", + " C groupBy Always empty thus no items to group.", + " MS groupJoin At most one item.", + " C groupJoin Always empty thus no items to join.", + "FO ignoreElement Use [`ignoreElements()`](#ignoreElements).", + " C ignoreElement Always empty.", + " MS ignoreElements Use [`ignoreElement()`](#ignoreElement).", + " C ignoreElements Always empty.", + " MSC interval At most one item. Use [`timer()`](#timer).", + " MSC intervalRange At most one item. Use [`timer()`](#timer).", + " S isEmpty Always one item.", + " C isEmpty Always empty.", + " MS join At most one item. Use [`zip()`](#zip)", + " C join Always empty thus no items to join.", + " C just Always empty.", + " M last At most one item. Use [`defaultIfEmpty`](#defaultIfEmpty).", + " S last Always one item.", + " C last Always empty. Use [`andThen()`](#andThen) to chose the follow-up sequence.", + " M lastElement At most one item, would be no-op.", + " S lastElement Always one item, would be no-op.", + " C lastElement Always empty.", + " M lastOrError At most one item, would be no-op.", + " S lastOrError Always one item, would be no-op.", + " C lastOrError Always empty. Use [`andThen()`](#andThen) and [`error()`](#error).", + " MS lastOrErrorStage At most one item. Use [`toCompletionStage()`](#toCompletionStage).", + " C lastOrErrorStage Always empty. Use [`andThen()`](#andThen), [`error()`](#error) and [`toCompletionStage()`](#toCompletionStage).", + " MSC lastStage At most one item. Use [`toCompletionStage()`](#toCompletionStage).", + " C map Always empty thus no items to map.", + " C mapOptional Always empty thus no items to map.", + " C ofType Always empty thus no items to filter.", + " OMSC onBackpressureBuffer Backpressure related and not supported outside `Flowable`.", + " OMSC onBackpressureDrop Backpressure related and not supported outside `Flowable`.", + " OMSC onBackpressureLatest Backpressure related and not supported outside `Flowable`.", + " OMSC parallel Needs backpressure thus not supported outside `Flowable`.", + " M publish Connectable sources not supported outside `Flowable` and `Observable`. Use a `MaybeSubject`.", + " S publish Connectable sources not supported outside `Flowable` and `Observable`. Use a `SingleSubject`.", + " C publish Connectable sources not supported outside `Flowable` and `Observable`. Use a `ConnectableSubject`.", + " MS range At most one item. Use [`just()`](#just).", + " C range Always empty. Use [`complete()`](#complete).", + " MS rangeLong At most one item. Use [`just()`](#just).", + " C rangeLong Always empty. Use [`complete()`](#complete).", + " OMSC rebatchRequests Backpressure related and not supported outside `Flowable`.", + " MS reduce At most one item. Use [`map()`](#map).", + " C reduce Always empty thus no items to reduce.", + " MS reduceWith At most one item. Use [`map()`](#map).", + " C reduceWith Always empty thus no items to reduce.", + " M replay Connectable sources not supported outside `Flowable` and `Observable`. Use a `MaybeSubject`.", + " S replay Connectable sources not supported outside `Flowable` and `Observable`. Use a `SingleSubject`.", + " C replay Connectable sources not supported outside `Flowable` and `Observable`. Use a `ConnectableSubject`.", + " MS sample At most one item, would be no-op.", + " C sample Always empty thus no items to work with.", + " MS scan At most one item. Use [`map()`](#map).", + " C scan Always empty thus no items to reduce.", + " MS scanWith At most one item. Use [`map()`](#map).", + " C scanWith Always empty thus no items to reduce.", + " MSC serialize At most one signal type.", + " M share Connectable sources not supported outside `Flowable` and `Observable`. Use a `MaybeSubject`.", + " S share Connectable sources not supported outside `Flowable` and `Observable`. Use a `SingleSubject`.", + " C share Connectable sources not supported outside `Flowable` and `Observable`. Use a `ConnectableSubject`.", + " M single At most one item. Use [`defaultIfEmpty`](#defaultIfEmpty).", + " S single Always one item.", + " C single Always empty. Use [`andThen()`](#andThen) to chose the follow-up sequence.", + " M singleElement At most one item, would be no-op.", + " S singleElement Always one item, would be no-op.", + " C singleElement Always empty.", + " M singleOrError At most one item, would be no-op.", + " S singleOrError Always one item, would be no-op.", + " C singleOrError Always empty. Use [`andThen()`](#andThen) and [`error()`](#error).", + " MS singleOrErrorStage At most one item. Use [`toCompletionStage()`](#toCompletionStage).", + " C singleOrErrorStage Always empty. Use [`andThen()`](#andThen), [`error()`](#error) and [`toCompletionStage()`](#toCompletionStage).", + " MSC singleStage At most one item. Use [`toCompletionStage()`](#toCompletionStage).", + " MSC skip At most one item, would be no-op.", + " MSC skipLast At most one item, would be no-op.", + " MS skipWhile At most one item. Use [`filter()`](#filter).", + " C skipWhile Always empty.", + " MSC skipUntil At most one item. Use [`takeUntil()`](#takeUntil).", + " MSC sorted At most one item.", + " MSC startWithArray Use [`startWith()`](#startWith) and [`fromArray()`](#fromArray) of `Flowable` or `Observable`.", + " MSC startWithItem Use [`startWith()`](#startWith) and [`just()`](#just) of another reactive type.", + " MSC startWithIterable Use [`startWith()`](#startWith) and [`fromIterable()`](#fromArray) of `Flowable` or `Observable`.", + " S switchIfEmpty Never empty.", + " C switchIfEmpty Always empty. Use [`defaultIfEmpty()`](#defaultIfEmpty).", + " MS switchMap At most one item. Use [`flatMap()`](#flatMap).", + " C switchMap Always empty thus no items to map.", + " MS switchMapDelayError At most one item. Use [`flatMap()`](#flatMap).", + " C switchMapDelayError Always empty thus no items to map.", + " MS switchMapCompletable At most one item. Use [`flatMap()`](#flatMap).", + " C switchMapCompletable Always empty thus no items to map.", + " MS switchMapCompletableDelayError At most one item. Use [`flatMap()`](#flatMap).", + " C switchMapCompletableDelayError Always empty thus no items to map.", + " MS switchMapMaybe At most one item. Use [`flatMap()`](#flatMap).", + " C switchMapMaybe Always empty thus no items to map.", + " MS switchMapMaybeDelayError At most one item. Use [`flatMap()`](#flatMap).", + " C switchMapMaybeDelayError Always empty thus no items to map.", + " MS switchMapSingle At most one item. Use [`flatMap()`](#flatMap).", + " C switchMapSingle Always empty thus no items to map.", + " MS switchMapSingleDelayError At most one item. Use [`flatMap()`](#flatMap).", + " C switchMapSingleDelayError Always empty thus no items to map.", + " MSC take At most one item, would be no-op.", + " MSC takeLast At most one item, would be no-op.", + " MS takeWhile At most one item. Use [`filter()`](#filter).", + " C takeWhile Always empty.", + " MS throttleFirst At most one item signaled so no subsequent items to work with.", + " C throttleFirst Always empty thus no items to work with.", + " MS throttleLast At most one item signaled so no subsequent items to work with.", + " C throttleLast Always empty thus no items to work with.", + " MS throttleLatest At most one item signaled so no subsequent items to work with.", + " C throttleLatest Always empty thus no items to work with.", + " MS throttleWithTimeout At most one item signaled so no subsequent items to work with.", + " C throttleWithTimeout Always empty thus no items to work with.", + " C timeInterval Always empty thus no items to work with.", + " C timestamp Always empty thus no items to work with.", + "FO toCompletionStage Use [`firstStage`](#firstStage), [`lastStage`](#lastStage) or [`singleStage`](#singleStage).", + "F toFlowable Would be no-op.", + " M toList At most one element to collect. Use [`map()`](#map) and [`switchIfEmpty()`](#switchIfEmpty) to transform into a list/collection.", + " S toList One element to collect. Use [`map()`](#map) to transform into a list/collection.", + " C toList Always empty. Use [`andThen()`](#andThen) to bring in a collection.", + " M toMap At most one element to collect. Use [`map()`](#map) and [`switchIfEmpty()`](#switchIfEmpty) to transform into a list/collection.", + " S toMap One element to collect. Use [`map()`](#map) to transform into a list/collection.", + " C toMap Always empty. Use [`andThen()`](#andThen) to bring in a collection.", + " M toMultimap At most one element to collect. Use [`map()`](#map) and [`switchIfEmpty()`](#switchIfEmpty) to transform into a list/collection.", + " S toMultimap One element to collect. Use [`map()`](#map) to transform into a list/collection.", + " C toMultimap Always empty. Use [`andThen()`](#andThen) to bring in a collection.", + "FO toMaybe Use [`firstElement`](#firstElement), [`lastElement`](#lastElement) or [`singleElement`](#singleElement).", + " M toMaybe Would be no-op.", + " O toObservable Would be no-op.", + "FO toSingle Use [`firstOrError`](#firstOrError), [`lastOrError`](#lastOrError) or [`singleOrError`](#singleOrError).", + " S toSingle Would be no-op.", + "FO toSingleDefault Use [`first`](#first), [`last`](#last) or [`single`](#single).", + " M toSingleDefault Use [`defaultIfEmpty()`](#defaultIfEmpty).", + " S toSingleDefault Would be no-op.", + " M toSortedList At most one element to collect. Use [`map()`](#map) and [`switchIfEmpty()`](#switchIfEmpty) to transform into a list/collection.", + " S toSortedList One element to collect. Use [`map()`](#map) to transform into a list/collection.", + " C toSortedList Always empty. Use [`andThen()`](#andThen) to bring in a collection.", + " M window Use [`map()`](#map) and [`switchIfEmpty()`](#switchIfEmpty) to transform into a nested source.", + " S window Use [`map()`](#map) to transform into a nested source.", + " C window Always empty. Use [`andThen()`](#andThen) to bring in a nested source.", + " MS withLatestFrom At most one element per source. Use [`zip()`](#zip).", + " C withLatestFrom Always empty. Use [`merge()`](#merge).", + "F wrap Use [`fromPublisher()`](#fromPublisher).", + " C zip Use [`merge()`](#merge).", + " C zipArray Use [`mergeArray()`](#mergeArray).", + " C zipWith Use [`mergeWith()`](#mergeWith).", + }; + + static final Map<String, Map<String, String>> NOTES_MAP; + static { + NOTES_MAP = new HashMap<>(); + for (String s : NOTES) { + char[] classes = s.substring(0, 5).trim().toCharArray(); + int idx = s.indexOf(' ', 7); + String method = s.substring(6, idx); + String note = s.substring(idx).trim(); + + for (char c : classes) { + NOTES_MAP.computeIfAbsent(method, v -> new HashMap<>()) + .put(String.valueOf(c), note); + } + } + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/util/QueueDrainHelperTest.java b/src/test/java/io/reactivex/rxjava3/internal/util/QueueDrainHelperTest.java new file mode 100644 index 0000000000..7929ea8782 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/util/QueueDrainHelperTest.java @@ -0,0 +1,883 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.util; + +import static org.junit.Assert.*; + +import java.io.IOException; +import java.util.*; +import java.util.concurrent.atomic.AtomicLong; + +import org.junit.Test; +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.Observer; +import io.reactivex.rxjava3.core.RxJavaTest; +import io.reactivex.rxjava3.disposables.*; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.BooleanSupplier; +import io.reactivex.rxjava3.internal.subscriptions.BooleanSubscription; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.operators.SpscArrayQueue; +import io.reactivex.rxjava3.subscribers.TestSubscriber; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class QueueDrainHelperTest extends RxJavaTest { + + @Test + public void isCancelled() { + assertTrue(QueueDrainHelper.isCancelled(new BooleanSupplier() { + @Override + public boolean getAsBoolean() throws Exception { + throw new IOException(); + } + })); + } + + @Test + public void requestMaxInt() { + QueueDrainHelper.request(new Subscription() { + @Override + public void request(long n) { + assertEquals(Integer.MAX_VALUE, n); + } + + @Override + public void cancel() { + } + }, Integer.MAX_VALUE); + } + + @Test + public void requestMinInt() { + QueueDrainHelper.request(new Subscription() { + @Override + public void request(long n) { + assertEquals(Long.MAX_VALUE, n); + } + + @Override + public void cancel() { + } + }, Integer.MIN_VALUE); + } + + @Test + public void requestAlmostMaxInt() { + QueueDrainHelper.request(new Subscription() { + @Override + public void request(long n) { + assertEquals(Integer.MAX_VALUE - 1, n); + } + + @Override + public void cancel() { + } + }, Integer.MAX_VALUE - 1); + } + + @Test + public void postCompleteEmpty() { + TestSubscriber<Integer> ts = new TestSubscriber<>(); + ArrayDeque<Integer> queue = new ArrayDeque<>(); + AtomicLong state = new AtomicLong(); + BooleanSupplier isCancelled = new BooleanSupplier() { + @Override + public boolean getAsBoolean() throws Exception { + return false; + } + }; + + ts.onSubscribe(new BooleanSubscription()); + + QueueDrainHelper.postComplete(ts, queue, state, isCancelled); + + ts.assertResult(); + } + + @Test + public void postCompleteWithRequest() { + TestSubscriber<Integer> ts = new TestSubscriber<>(); + ArrayDeque<Integer> queue = new ArrayDeque<>(); + AtomicLong state = new AtomicLong(); + BooleanSupplier isCancelled = new BooleanSupplier() { + @Override + public boolean getAsBoolean() throws Exception { + return false; + } + }; + + ts.onSubscribe(new BooleanSubscription()); + queue.offer(1); + state.getAndIncrement(); + + QueueDrainHelper.postComplete(ts, queue, state, isCancelled); + + ts.assertResult(1); + } + + @Test + public void completeRequestRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final TestSubscriber<Integer> ts = new TestSubscriber<>(); + final ArrayDeque<Integer> queue = new ArrayDeque<>(); + final AtomicLong state = new AtomicLong(); + final BooleanSupplier isCancelled = new BooleanSupplier() { + @Override + public boolean getAsBoolean() throws Exception { + return false; + } + }; + + ts.onSubscribe(new BooleanSubscription()); + queue.offer(1); + + Runnable r1 = new Runnable() { + @Override + public void run() { + QueueDrainHelper.postCompleteRequest(1, ts, queue, state, isCancelled); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + QueueDrainHelper.postComplete(ts, queue, state, isCancelled); + } + }; + + TestHelper.race(r1, r2); + + ts.assertResult(1); + } + } + + @Test + public void postCompleteCancelled() { + final TestSubscriber<Integer> ts = new TestSubscriber<>(); + ArrayDeque<Integer> queue = new ArrayDeque<>(); + AtomicLong state = new AtomicLong(); + BooleanSupplier isCancelled = new BooleanSupplier() { + @Override + public boolean getAsBoolean() throws Exception { + return ts.isCancelled(); + } + }; + + ts.onSubscribe(new BooleanSubscription()); + queue.offer(1); + state.getAndIncrement(); + ts.cancel(); + + QueueDrainHelper.postComplete(ts, queue, state, isCancelled); + + ts.assertEmpty(); + } + + @Test + public void postCompleteCancelledAfterOne() { + final TestSubscriber<Integer> ts = new TestSubscriber<Integer>() { + @Override + public void onNext(Integer t) { + super.onNext(t); + cancel(); + } + }; + ArrayDeque<Integer> queue = new ArrayDeque<>(); + AtomicLong state = new AtomicLong(); + BooleanSupplier isCancelled = new BooleanSupplier() { + @Override + public boolean getAsBoolean() throws Exception { + return ts.isCancelled(); + } + }; + + ts.onSubscribe(new BooleanSubscription()); + queue.offer(1); + state.getAndIncrement(); + + QueueDrainHelper.postComplete(ts, queue, state, isCancelled); + + ts.assertValue(1).assertNoErrors().assertNotComplete(); + } + + @Test + public void drainMaxLoopMissingBackpressure() { + TestSubscriber<Integer> ts = new TestSubscriber<>(); + ts.onSubscribe(new BooleanSubscription()); + + QueueDrain<Integer, Integer> qd = new QueueDrain<Integer, Integer>() { + @Override + public boolean cancelled() { + return false; + } + + @Override + public boolean done() { + return false; + } + + @Override + public Throwable error() { + return null; + } + + @Override + public boolean enter() { + return true; + } + + @Override + public long requested() { + return 0; + } + + @Override + public long produced(long n) { + return 0; + } + + @Override + public int leave(int m) { + return 0; + } + + @Override + public boolean accept(Subscriber<? super Integer> a, Integer v) { + return false; + } + }; + + SpscArrayQueue<Integer> q = new SpscArrayQueue<>(32); + q.offer(1); + + QueueDrainHelper.drainMaxLoop(q, ts, false, null, qd); + + ts.assertFailure(MissingBackpressureException.class); + } + + @Test + public void drainMaxLoopMissingBackpressureWithResource() { + TestSubscriber<Integer> ts = new TestSubscriber<>(); + ts.onSubscribe(new BooleanSubscription()); + + QueueDrain<Integer, Integer> qd = new QueueDrain<Integer, Integer>() { + @Override + public boolean cancelled() { + return false; + } + + @Override + public boolean done() { + return false; + } + + @Override + public Throwable error() { + return null; + } + + @Override + public boolean enter() { + return true; + } + + @Override + public long requested() { + return 0; + } + + @Override + public long produced(long n) { + return 0; + } + + @Override + public int leave(int m) { + return 0; + } + + @Override + public boolean accept(Subscriber<? super Integer> a, Integer v) { + return false; + } + }; + + SpscArrayQueue<Integer> q = new SpscArrayQueue<>(32); + q.offer(1); + + Disposable d = Disposable.empty(); + + QueueDrainHelper.drainMaxLoop(q, ts, false, d, qd); + + ts.assertFailure(MissingBackpressureException.class); + + assertTrue(d.isDisposed()); + } + + @Test + public void drainMaxLoopDontAccept() { + TestSubscriber<Integer> ts = new TestSubscriber<>(); + ts.onSubscribe(new BooleanSubscription()); + + QueueDrain<Integer, Integer> qd = new QueueDrain<Integer, Integer>() { + @Override + public boolean cancelled() { + return false; + } + + @Override + public boolean done() { + return false; + } + + @Override + public Throwable error() { + return null; + } + + @Override + public boolean enter() { + return true; + } + + @Override + public long requested() { + return 1; + } + + @Override + public long produced(long n) { + return 0; + } + + @Override + public int leave(int m) { + return 0; + } + + @Override + public boolean accept(Subscriber<? super Integer> a, Integer v) { + return false; + } + }; + + SpscArrayQueue<Integer> q = new SpscArrayQueue<>(32); + q.offer(1); + + QueueDrainHelper.drainMaxLoop(q, ts, false, null, qd); + + ts.assertEmpty(); + } + + @Test + public void checkTerminatedDelayErrorEmpty() { + TestSubscriber<Integer> ts = new TestSubscriber<>(); + ts.onSubscribe(new BooleanSubscription()); + + QueueDrain<Integer, Integer> qd = new QueueDrain<Integer, Integer>() { + @Override + public boolean cancelled() { + return false; + } + + @Override + public boolean done() { + return false; + } + + @Override + public Throwable error() { + return null; + } + + @Override + public boolean enter() { + return true; + } + + @Override + public long requested() { + return 0; + } + + @Override + public long produced(long n) { + return 0; + } + + @Override + public int leave(int m) { + return 0; + } + + @Override + public boolean accept(Subscriber<? super Integer> a, Integer v) { + return false; + } + }; + + SpscArrayQueue<Integer> q = new SpscArrayQueue<>(32); + + QueueDrainHelper.checkTerminated(true, true, ts, true, q, qd); + + ts.assertResult(); + } + + @Test + public void checkTerminatedDelayErrorNonEmpty() { + TestSubscriber<Integer> ts = new TestSubscriber<>(); + ts.onSubscribe(new BooleanSubscription()); + + QueueDrain<Integer, Integer> qd = new QueueDrain<Integer, Integer>() { + @Override + public boolean cancelled() { + return false; + } + + @Override + public boolean done() { + return false; + } + + @Override + public Throwable error() { + return null; + } + + @Override + public boolean enter() { + return true; + } + + @Override + public long requested() { + return 0; + } + + @Override + public long produced(long n) { + return 0; + } + + @Override + public int leave(int m) { + return 0; + } + + @Override + public boolean accept(Subscriber<? super Integer> a, Integer v) { + return false; + } + }; + + SpscArrayQueue<Integer> q = new SpscArrayQueue<>(32); + + QueueDrainHelper.checkTerminated(true, false, ts, true, q, qd); + + ts.assertEmpty(); + } + + @Test + public void checkTerminatedDelayErrorEmptyError() { + TestSubscriber<Integer> ts = new TestSubscriber<>(); + ts.onSubscribe(new BooleanSubscription()); + + QueueDrain<Integer, Integer> qd = new QueueDrain<Integer, Integer>() { + @Override + public boolean cancelled() { + return false; + } + + @Override + public boolean done() { + return false; + } + + @Override + public Throwable error() { + return new TestException(); + } + + @Override + public boolean enter() { + return true; + } + + @Override + public long requested() { + return 0; + } + + @Override + public long produced(long n) { + return 0; + } + + @Override + public int leave(int m) { + return 0; + } + + @Override + public boolean accept(Subscriber<? super Integer> a, Integer v) { + return false; + } + }; + + SpscArrayQueue<Integer> q = new SpscArrayQueue<>(32); + + QueueDrainHelper.checkTerminated(true, true, ts, true, q, qd); + + ts.assertFailure(TestException.class); + } + + @Test + public void checkTerminatedNonDelayErrorError() { + TestSubscriber<Integer> ts = new TestSubscriber<>(); + ts.onSubscribe(new BooleanSubscription()); + + QueueDrain<Integer, Integer> qd = new QueueDrain<Integer, Integer>() { + @Override + public boolean cancelled() { + return false; + } + + @Override + public boolean done() { + return false; + } + + @Override + public Throwable error() { + return new TestException(); + } + + @Override + public boolean enter() { + return true; + } + + @Override + public long requested() { + return 0; + } + + @Override + public long produced(long n) { + return 0; + } + + @Override + public int leave(int m) { + return 0; + } + + @Override + public boolean accept(Subscriber<? super Integer> a, Integer v) { + return false; + } + }; + + SpscArrayQueue<Integer> q = new SpscArrayQueue<>(32); + + QueueDrainHelper.checkTerminated(true, false, ts, false, q, qd); + + ts.assertFailure(TestException.class); + } + + @Test + public void observerCheckTerminatedDelayErrorEmpty() { + TestObserver<Integer> to = new TestObserver<>(); + to.onSubscribe(Disposable.empty()); + + ObservableQueueDrain<Integer, Integer> qd = new ObservableQueueDrain<Integer, Integer>() { + @Override + public boolean cancelled() { + return false; + } + + @Override + public boolean done() { + return false; + } + + @Override + public Throwable error() { + return null; + } + + @Override + public boolean enter() { + return true; + } + + @Override + public int leave(int m) { + return 0; + } + + @Override + public void accept(Observer<? super Integer> a, Integer v) { + } + }; + + SpscArrayQueue<Integer> q = new SpscArrayQueue<>(32); + + QueueDrainHelper.checkTerminated(true, true, to, true, q, null, qd); + + to.assertResult(); + } + + @Test + public void observerCheckTerminatedDelayErrorEmptyResource() { + TestObserver<Integer> to = new TestObserver<>(); + to.onSubscribe(Disposable.empty()); + + ObservableQueueDrain<Integer, Integer> qd = new ObservableQueueDrain<Integer, Integer>() { + @Override + public boolean cancelled() { + return false; + } + + @Override + public boolean done() { + return false; + } + + @Override + public Throwable error() { + return null; + } + + @Override + public boolean enter() { + return true; + } + + @Override + public int leave(int m) { + return 0; + } + + @Override + public void accept(Observer<? super Integer> a, Integer v) { + } + }; + + SpscArrayQueue<Integer> q = new SpscArrayQueue<>(32); + + Disposable d = Disposable.empty(); + + QueueDrainHelper.checkTerminated(true, true, to, true, q, d, qd); + + to.assertResult(); + + assertTrue(d.isDisposed()); + } + + @Test + public void observerCheckTerminatedDelayErrorNonEmpty() { + TestObserver<Integer> to = new TestObserver<>(); + to.onSubscribe(Disposable.empty()); + + ObservableQueueDrain<Integer, Integer> qd = new ObservableQueueDrain<Integer, Integer>() { + @Override + public boolean cancelled() { + return false; + } + + @Override + public boolean done() { + return false; + } + + @Override + public Throwable error() { + return null; + } + + @Override + public boolean enter() { + return true; + } + + @Override + public int leave(int m) { + return 0; + } + + @Override + public void accept(Observer<? super Integer> a, Integer v) { + } + }; + + SpscArrayQueue<Integer> q = new SpscArrayQueue<>(32); + + QueueDrainHelper.checkTerminated(true, false, to, true, q, null, qd); + + to.assertEmpty(); + } + + @Test + public void observerCheckTerminatedDelayErrorEmptyError() { + TestObserver<Integer> to = new TestObserver<>(); + to.onSubscribe(Disposable.empty()); + + ObservableQueueDrain<Integer, Integer> qd = new ObservableQueueDrain<Integer, Integer>() { + @Override + public boolean cancelled() { + return false; + } + + @Override + public boolean done() { + return false; + } + + @Override + public Throwable error() { + return new TestException(); + } + + @Override + public boolean enter() { + return true; + } + + @Override + public int leave(int m) { + return 0; + } + + @Override + public void accept(Observer<? super Integer> a, Integer v) { + } + }; + + SpscArrayQueue<Integer> q = new SpscArrayQueue<>(32); + + QueueDrainHelper.checkTerminated(true, true, to, true, q, null, qd); + + to.assertFailure(TestException.class); + } + + @Test + public void observerCheckTerminatedNonDelayErrorError() { + TestObserver<Integer> to = new TestObserver<>(); + to.onSubscribe(Disposable.empty()); + + ObservableQueueDrain<Integer, Integer> qd = new ObservableQueueDrain<Integer, Integer>() { + @Override + public boolean cancelled() { + return false; + } + + @Override + public boolean done() { + return false; + } + + @Override + public Throwable error() { + return new TestException(); + } + + @Override + public boolean enter() { + return true; + } + + @Override + public int leave(int m) { + return 0; + } + + @Override + public void accept(Observer<? super Integer> a, Integer v) { + } + }; + + SpscArrayQueue<Integer> q = new SpscArrayQueue<>(32); + + QueueDrainHelper.checkTerminated(true, false, to, false, q, null, qd); + + to.assertFailure(TestException.class); + } + + @Test + public void observerCheckTerminatedNonDelayErrorErrorResource() { + TestObserver<Integer> to = new TestObserver<>(); + to.onSubscribe(Disposable.empty()); + + ObservableQueueDrain<Integer, Integer> qd = new ObservableQueueDrain<Integer, Integer>() { + @Override + public boolean cancelled() { + return false; + } + + @Override + public boolean done() { + return false; + } + + @Override + public Throwable error() { + return new TestException(); + } + + @Override + public boolean enter() { + return true; + } + + @Override + public int leave(int m) { + return 0; + } + + @Override + public void accept(Observer<? super Integer> a, Integer v) { + } + }; + + SpscArrayQueue<Integer> q = new SpscArrayQueue<>(32); + + Disposable d = Disposable.empty(); + + QueueDrainHelper.checkTerminated(true, false, to, false, q, d, qd); + + to.assertFailure(TestException.class); + + assertTrue(d.isDisposed()); + } + + @Test + public void postCompleteAlreadyComplete() { + + TestSubscriber<Integer> ts = new TestSubscriber<>(); + + Queue<Integer> q = new ArrayDeque<>(); + q.offer(1); + + AtomicLong state = new AtomicLong(QueueDrainHelper.COMPLETED_MASK); + + QueueDrainHelper.postComplete(ts, q, state, new BooleanSupplier() { + @Override + public boolean getAsBoolean() throws Exception { + return false; + } + }); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/util/TestingHelper.java b/src/test/java/io/reactivex/rxjava3/internal/util/TestingHelper.java new file mode 100644 index 0000000000..207c85236d --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/util/TestingHelper.java @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.util; + +import java.util.*; + +import io.reactivex.rxjava3.functions.*; + +public final class TestingHelper { + + private TestingHelper() { + // prevent instantiation + } + + public static <T> Consumer<T> addToList(final List<T> list) { + return new Consumer<T>() { + + @Override + public void accept(T t) { + list.add(t); + } + }; + } + + public static <T> Supplier<List<T>> supplierListCreator() { + return new Supplier<List<T>>() { + + @Override + public List<T> get() { + return new ArrayList<>(); + } + }; + } + + public static BiConsumer<Object, Object> biConsumerThrows(final RuntimeException e) { + return new BiConsumer<Object, Object>() { + + @Override + public void accept(Object t1, Object t2) { + throw e; + } + }; + } +} diff --git a/src/test/java/io/reactivex/rxjava3/internal/util/VolatileSizeArrayListTest.java b/src/test/java/io/reactivex/rxjava3/internal/util/VolatileSizeArrayListTest.java new file mode 100644 index 0000000000..3ce08aa3dc --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/internal/util/VolatileSizeArrayListTest.java @@ -0,0 +1,114 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.internal.util; + +import static org.junit.Assert.*; + +import java.util.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.RxJavaTest; + +public class VolatileSizeArrayListTest extends RxJavaTest { + + @Test + public void normal() { + List<Integer> list = new VolatileSizeArrayList<>(); + + assertTrue(list.isEmpty()); + assertEquals(0, list.size()); + assertFalse(list.contains(1)); + assertFalse(list.remove((Integer)1)); + + list = new VolatileSizeArrayList<>(16); + assertTrue(list.add(1)); + assertTrue(list.addAll(Arrays.asList(3, 4, 7))); + list.add(1, 2); + assertTrue(list.addAll(4, Arrays.asList(5, 6))); + + assertTrue(list.contains(2)); + assertFalse(list.remove((Integer)10)); + + assertEquals(Arrays.asList(1, 2, 3, 4, 5, 6, 7), list); + assertFalse(list.isEmpty()); + assertEquals(7, list.size()); + + Iterator<Integer> it = list.iterator(); + for (int i = 1; i < 8; i++) { + assertEquals(i, it.next().intValue()); + } + + assertArrayEquals(new Object[] { 1, 2, 3, 4, 5, 6, 7 }, list.toArray()); + assertArrayEquals(new Integer[] { 1, 2, 3, 4, 5, 6, 7 }, list.toArray(new Integer[7])); + + assertTrue(list.containsAll(Arrays.asList(2, 4, 6))); + assertFalse(list.containsAll(Arrays.asList(2, 4, 6, 10))); + + assertFalse(list.removeAll(Arrays.asList(10, 11, 12))); + + assertFalse(list.retainAll(Arrays.asList(1, 2, 3, 4, 5, 6, 7))); + + assertEquals(7, list.size()); + + for (int i = 1; i < 8; i++) { + assertEquals(i, list.get(i - 1).intValue()); + } + + for (int i = 1; i < 8; i++) { + assertEquals(i, list.set(i - 1, i).intValue()); + } + + assertEquals(2, list.indexOf(3)); + + assertEquals(5, list.lastIndexOf(6)); + + ListIterator<Integer> lit = list.listIterator(7); + for (int i = 7; i > 0; i--) { + assertEquals(i, lit.previous().intValue()); + } + + assertEquals(Arrays.asList(3, 4, 5), list.subList(2, 5)); + + VolatileSizeArrayList<Integer> list2 = new VolatileSizeArrayList<>(); + list2.addAll(Arrays.asList(1, 2, 3, 4, 5, 6)); + + assertNotEquals(list2, list); + assertNotEquals(list, list2); + + list2.add(7); + assertEquals(list2, list); + assertEquals(list, list2); + + List<Integer> list3 = new ArrayList<>(); + list3.addAll(Arrays.asList(1, 2, 3, 4, 5, 6)); + + assertNotEquals(list3, list); + assertNotEquals(list, list3); + + list3.add(7); + assertEquals(list3, list); + assertEquals(list, list3); + + assertEquals(list.hashCode(), list3.hashCode()); + assertEquals(list.toString(), list3.toString()); + + list.remove(0); + assertEquals(6, list.size()); + + list.clear(); + assertEquals(0, list.size()); + assertTrue(list.isEmpty()); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/maybe/MaybeCreateTest.java b/src/test/java/io/reactivex/rxjava3/maybe/MaybeCreateTest.java new file mode 100644 index 0000000000..e3bbfb4bfc --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/maybe/MaybeCreateTest.java @@ -0,0 +1,143 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.maybe; + +import static org.junit.Assert.assertTrue; + +import java.util.List; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.Cancellable; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class MaybeCreateTest extends RxJavaTest { + @Test + public void basic() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + final Disposable d = Disposable.empty(); + + Maybe.<Integer>create(new MaybeOnSubscribe<Integer>() { + @Override + public void subscribe(MaybeEmitter<Integer> e) throws Exception { + e.setDisposable(d); + + e.onSuccess(1); + e.onError(new TestException()); + e.onSuccess(2); + e.onError(new TestException()); + } + }).test().assertResult(1); + + assertTrue(d.isDisposed()); + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void basicWithCancellable() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + final Disposable d1 = Disposable.empty(); + final Disposable d2 = Disposable.empty(); + + Maybe.<Integer>create(new MaybeOnSubscribe<Integer>() { + @Override + public void subscribe(MaybeEmitter<Integer> e) throws Exception { + e.setDisposable(d1); + e.setCancellable(new Cancellable() { + @Override + public void cancel() throws Exception { + d2.dispose(); + } + }); + + e.onSuccess(1); + e.onError(new TestException()); + e.onSuccess(2); + e.onError(new TestException()); + } + }).test().assertResult(1); + + assertTrue(d1.isDisposed()); + assertTrue(d2.isDisposed()); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void basicWithError() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + final Disposable d = Disposable.empty(); + + Maybe.<Integer>create(new MaybeOnSubscribe<Integer>() { + @Override + public void subscribe(MaybeEmitter<Integer> e) throws Exception { + e.setDisposable(d); + + e.onError(new TestException()); + e.onSuccess(2); + e.onError(new TestException()); + } + }).test().assertFailure(TestException.class); + + assertTrue(d.isDisposed()); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void basicWithCompletion() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + final Disposable d = Disposable.empty(); + + Maybe.<Integer>create(new MaybeOnSubscribe<Integer>() { + @Override + public void subscribe(MaybeEmitter<Integer> e) throws Exception { + e.setDisposable(d); + + e.onComplete(); + e.onSuccess(2); + e.onError(new TestException()); + } + }).test().assertComplete(); + + assertTrue(d.isDisposed()); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test(expected = IllegalArgumentException.class) + public void unsafeCreate() { + Maybe.unsafeCreate(Maybe.just(1)); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/maybe/MaybeRetryTest.java b/src/test/java/io/reactivex/rxjava3/maybe/MaybeRetryTest.java new file mode 100644 index 0000000000..731dd03e23 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/maybe/MaybeRetryTest.java @@ -0,0 +1,178 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.maybe; + +import static org.junit.Assert.assertEquals; + +import java.util.concurrent.Callable; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.Predicate; +import io.reactivex.rxjava3.internal.functions.Functions; + +public class MaybeRetryTest extends RxJavaTest { + @Test + public void retryTimesPredicateWithMatchingPredicate() { + final AtomicInteger atomicInteger = new AtomicInteger(3); + final AtomicInteger numberOfSubscribeCalls = new AtomicInteger(0); + + Maybe.fromCallable(new Callable<Boolean>() { + @Override public Boolean call() throws Exception { + numberOfSubscribeCalls.incrementAndGet(); + + if (atomicInteger.decrementAndGet() != 0) { + throw new RuntimeException(); + } + + throw new IllegalArgumentException(); + } + }) + .retry(Integer.MAX_VALUE, new Predicate<Throwable>() { + @Override public boolean test(final Throwable throwable) throws Exception { + return !(throwable instanceof IllegalArgumentException); + } + }) + .test() + .assertFailure(IllegalArgumentException.class); + + assertEquals(3, numberOfSubscribeCalls.get()); + } + + @Test + public void retryTimesPredicateWithMatchingRetryAmount() { + final AtomicInteger atomicInteger = new AtomicInteger(3); + final AtomicInteger numberOfSubscribeCalls = new AtomicInteger(0); + + Maybe.fromCallable(new Callable<Boolean>() { + @Override public Boolean call() throws Exception { + numberOfSubscribeCalls.incrementAndGet(); + + if (atomicInteger.decrementAndGet() != 0) { + throw new RuntimeException(); + } + + return true; + } + }) + .retry(2, Functions.alwaysTrue()) + .test() + .assertResult(true); + + assertEquals(3, numberOfSubscribeCalls.get()); + } + + @Test + public void retryTimesPredicateWithNotMatchingRetryAmount() { + final AtomicInteger atomicInteger = new AtomicInteger(3); + final AtomicInteger numberOfSubscribeCalls = new AtomicInteger(0); + + Maybe.fromCallable(new Callable<Boolean>() { + @Override public Boolean call() throws Exception { + numberOfSubscribeCalls.incrementAndGet(); + + if (atomicInteger.decrementAndGet() != 0) { + throw new RuntimeException(); + } + + return true; + } + }) + .retry(1, Functions.alwaysTrue()) + .test() + .assertFailure(RuntimeException.class); + + assertEquals(2, numberOfSubscribeCalls.get()); + } + + @Test + public void retryTimesPredicateWithZeroRetries() { + final AtomicInteger atomicInteger = new AtomicInteger(2); + final AtomicInteger numberOfSubscribeCalls = new AtomicInteger(0); + + Maybe.fromCallable(new Callable<Boolean>() { + @Override public Boolean call() throws Exception { + numberOfSubscribeCalls.incrementAndGet(); + + if (atomicInteger.decrementAndGet() != 0) { + throw new RuntimeException(); + } + + return true; + } + }) + .retry(0, Functions.alwaysTrue()) + .test() + .assertFailure(RuntimeException.class); + + assertEquals(1, numberOfSubscribeCalls.get()); + } + + @Test + public void untilTrueJust() { + Maybe.just(1) + .retryUntil(() -> true) + .test() + .assertResult(1); + } + + @Test + public void untilFalseJust() { + Maybe.just(1) + .retryUntil(() -> false) + .test() + .assertResult(1); + } + + @Test + public void untilTrueEmpty() { + Maybe.empty() + .retryUntil(() -> true) + .test() + .assertResult(); + } + + @Test + public void untilFalseEmpty() { + Maybe.empty() + .retryUntil(() -> false) + .test() + .assertResult(); + } + + @Test + public void untilTrueError() { + Maybe.error(new TestException()) + .retryUntil(() -> true) + .test() + .assertFailure(TestException.class); + } + + @Test + public void untilFalseError() { + AtomicInteger counter = new AtomicInteger(); + Maybe.defer(() -> { + if (counter.getAndIncrement() == 0) { + return Maybe.error(new TestException()); + } + return Maybe.just(1); + }) + .retryUntil(() -> false) + .test() + .assertResult(1); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/maybe/MaybeTest.java b/src/test/java/io/reactivex/rxjava3/maybe/MaybeTest.java new file mode 100644 index 0000000000..acc7066c29 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/maybe/MaybeTest.java @@ -0,0 +1,3154 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.maybe; + +import static org.junit.Assert.*; + +import java.io.IOException; +import java.lang.management.*; +import java.util.*; +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.Test; +import org.reactivestreams.Publisher; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.disposables.*; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.internal.operators.flowable.FlowableZipTest.ArgsToString; +import io.reactivex.rxjava3.internal.operators.maybe.*; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.operators.QueueFuseable; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.processors.PublishProcessor; +import io.reactivex.rxjava3.schedulers.Schedulers; +import io.reactivex.rxjava3.subjects.PublishSubject; +import io.reactivex.rxjava3.subscribers.TestSubscriber; +import io.reactivex.rxjava3.testsupport.*; + +public class MaybeTest extends RxJavaTest { + @Test + public void fromFlowableEmpty() { + + Flowable.empty() + .singleElement() + .test() + .assertResult(); + } + + @Test + public void fromFlowableJust() { + + Flowable.just(1) + .singleElement() + .test() + .assertResult(1); + } + + @Test + public void fromFlowableError() { + + Flowable.error(new TestException()) + .singleElement() + .test() + .assertFailure(TestException.class); + } + + @Test + public void fromFlowableValueAndError() { + Flowable.just(1).concatWith(Flowable.<Integer>error(new TestException())) + .singleElement() + .test() + .assertFailure(TestException.class); + } + + @Test + public void fromFlowableMany() { + + Flowable.range(1, 2) + .singleElement() + .test() + .assertFailure(IllegalArgumentException.class); + } + + @Test + public void fromFlowableDisposeComposesThrough() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + + TestObserver<Integer> to = pp.singleElement().test(); + + assertTrue(pp.hasSubscribers()); + + to.dispose(); + + assertFalse(pp.hasSubscribers()); + } + + @Test + public void fromObservableEmpty() { + + Observable.empty() + .singleElement() + .test() + .assertResult(); + } + + @Test + public void fromObservableJust() { + + Observable.just(1) + .singleElement() + .test() + .assertResult(1); + } + + @Test + public void fromObservableError() { + + Observable.error(new TestException()) + .singleElement() + .test() + .assertFailure(TestException.class); + } + + @Test + public void fromObservableValueAndError() { + Flowable.just(1).concatWith(Flowable.<Integer>error(new TestException())) + .singleElement() + .test() + .assertFailure(TestException.class); + } + + @Test + public void fromObservableMany() { + + Observable.range(1, 2) + .singleElement() + .test() + .assertFailure(IllegalArgumentException.class); + } + + @Test + public void fromObservableDisposeComposesThrough() { + PublishSubject<Integer> ps = PublishSubject.create(); + + TestObserver<Integer> to = ps.singleElement().test(false); + + assertTrue(ps.hasObservers()); + + to.dispose(); + + assertFalse(ps.hasObservers()); + } + + @Test + public void fromObservableDisposeComposesThroughImmediatelyCancelled() { + PublishSubject<Integer> ps = PublishSubject.create(); + + ps.singleElement().test(true); + + assertFalse(ps.hasObservers()); + } + + @Test + public void just() { + Maybe.just(1) + .test() + .assertResult(1); + } + + @Test + public void empty() { + Maybe.empty() + .test() + .assertResult(); + } + + @Test + public void never() { + Maybe.never() + .to(TestHelper.testConsumer()) + .assertSubscribed() + .assertNoValues() + .assertNoErrors() + .assertNotComplete(); + } + + @Test + public void error() { + Maybe.error(new TestException()) + .test() + .assertFailure(TestException.class); + } + + @Test + public void errorCallable() { + Maybe.error(Functions.justSupplier(new TestException())) + .test() + .assertFailure(TestException.class); + } + + @Test + public void errorCallableReturnsNull() { + Maybe.error(Functions.justSupplier((Throwable)null)) + .test() + .assertFailure(NullPointerException.class); + } + + @Test + public void wrapCustom() { + Maybe.wrap(new MaybeSource<Integer>() { + @Override + public void subscribe(MaybeObserver<? super Integer> observer) { + observer.onSubscribe(Disposable.empty()); + observer.onSuccess(1); + } + }) + .test() + .assertResult(1); + } + + @Test + public void wrapMaybe() { + assertSame(Maybe.empty(), Maybe.wrap(Maybe.empty())); + } + + @Test + public void emptySingleton() { + assertSame(Maybe.empty(), Maybe.empty()); + } + + @Test + public void neverSingleton() { + assertSame(Maybe.never(), Maybe.never()); + } + + @Test + public void liftJust() { + Maybe.just(1).lift(new MaybeOperator<Integer, Integer>() { + @Override + public MaybeObserver<? super Integer> apply(MaybeObserver<? super Integer> t) throws Exception { + return t; + } + }) + .test() + .assertResult(1); + } + + @Test + public void liftThrows() { + Maybe.just(1).lift(new MaybeOperator<Integer, Integer>() { + @Override + public MaybeObserver<? super Integer> apply(MaybeObserver<? super Integer> t) throws Exception { + throw new TestException(); + } + }) + .test() + .assertFailure(TestException.class); + } + + @Test + public void deferThrows() { + Maybe.defer(new Supplier<Maybe<Integer>>() { + @Override + public Maybe<Integer> get() throws Exception { + throw new TestException(); + } + }) + .test() + .assertFailure(TestException.class); + } + + @Test + public void deferReturnsNull() { + Maybe.defer(new Supplier<Maybe<Integer>>() { + @Override + public Maybe<Integer> get() throws Exception { + return null; + } + }) + .test() + .assertFailure(NullPointerException.class); + } + + @Test + public void defer() { + Maybe<Integer> source = Maybe.defer(new Supplier<Maybe<Integer>>() { + int count; + @Override + public Maybe<Integer> get() throws Exception { + return Maybe.just(count++); + } + }); + + for (int i = 0; i < 128; i++) { + source.test().assertResult(i); + } + } + + @Test + public void flowableMaybeFlowable() { + Flowable.just(1).singleElement().toFlowable().test().assertResult(1); + } + + @Test + public void obervableMaybeobervable() { + Observable.just(1).singleElement().toObservable().test().assertResult(1); + } + + @Test + public void singleMaybeSingle() { + Single.just(1).toMaybe().toSingle().test().assertResult(1); + } + + @Test + public void completableMaybeCompletable() { + Completable.complete().toMaybe().ignoreElement().test().assertResult(); + } + + @Test + public void unsafeCreate() { + Maybe.unsafeCreate(new MaybeSource<Integer>() { + @Override + public void subscribe(MaybeObserver<? super Integer> observer) { + observer.onSubscribe(Disposable.empty()); + observer.onSuccess(1); + } + }) + .test() + .assertResult(1); + } + + @Test + public void to() { + Maybe.just(1).to(new MaybeConverter<Integer, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Maybe<Integer> v) { + return v.toFlowable(); + } + }) + .test() + .assertResult(1); + } + + @Test + public void as() { + Maybe.just(1).to(new MaybeConverter<Integer, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Maybe<Integer> v) { + return v.toFlowable(); + } + }) + .test() + .assertResult(1); + } + + @Test + public void compose() { + Maybe.just(1).compose(new MaybeTransformer<Integer, Integer>() { + @Override + public MaybeSource<Integer> apply(Maybe<Integer> m) { + return m.map(new Function<Integer, Integer>() { + @Override + public Integer apply(Integer w) throws Exception { + return w + 1; + } + }); + } + }) + .test() + .assertResult(2); + } + + @Test + public void mapReturnNull() { + Maybe.just(1).map(new Function<Integer, Object>() { + @Override + public Object apply(Integer v) throws Exception { + return null; + } + }).test().assertFailure(NullPointerException.class); + } + + @Test + public void mapThrows() { + Maybe.just(1).map(new Function<Integer, Object>() { + @Override + public Object apply(Integer v) throws Exception { + throw new IOException(); + } + }).test().assertFailure(IOException.class); + } + + @Test + public void map() { + Maybe.just(1).map(new Function<Integer, String>() { + @Override + public String apply(Integer v) throws Exception { + return v.toString(); + } + }).test().assertResult("1"); + } + + @Test + public void filterThrows() { + Maybe.just(1).filter(new Predicate<Integer>() { + @Override + public boolean test(Integer v) throws Exception { + throw new IOException(); + } + }).test().assertFailure(IOException.class); + } + + @Test + public void filterTrue() { + Maybe.just(1).filter(new Predicate<Integer>() { + @Override + public boolean test(Integer v) throws Exception { + return v == 1; + } + }).test().assertResult(1); + } + + @Test + public void filterFalse() { + Maybe.just(2).filter(new Predicate<Integer>() { + @Override + public boolean test(Integer v) throws Exception { + return v == 1; + } + }).test().assertResult(); + } + + @Test + public void filterEmpty() { + Maybe.<Integer>empty().filter(new Predicate<Integer>() { + @Override + public boolean test(Integer v) throws Exception { + return v == 1; + } + }).test().assertResult(); + } + + @Test + public void singleFilterThrows() { + Single.just(1).filter(new Predicate<Integer>() { + @Override + public boolean test(Integer v) throws Exception { + throw new IOException(); + } + }).test().assertFailure(IOException.class); + } + + @Test + public void singleFilterTrue() { + Single.just(1).filter(new Predicate<Integer>() { + @Override + public boolean test(Integer v) throws Exception { + return v == 1; + } + }).test().assertResult(1); + } + + @Test + public void singleFilterFalse() { + Single.just(2).filter(new Predicate<Integer>() { + @Override + public boolean test(Integer v) throws Exception { + return v == 1; + } + }).test().assertResult(); + } + + @Test + public void cast() { + TestObserver<Number> to = Maybe.just(1).cast(Number.class).test(); + // don'n inline this due to the generic type + to.assertResult((Number)1); + } + + @Test + public void observeOnSuccess() { + String main = Thread.currentThread().getName(); + TestObserver<String> to = Maybe.just(1) + .observeOn(Schedulers.single()) + .map(new Function<Integer, String>() { + @Override + public String apply(Integer v) throws Exception { + return v + ": " + Thread.currentThread().getName(); + } + }) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertValueCount(1) + ; + + assertNotEquals("1: " + main, to.values().get(0)); + } + + @Test + public void observeOnError() { + Maybe.error(new TestException()) + .observeOn(Schedulers.single()) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertFailure(TestException.class) + ; + } + + @Test + public void observeOnComplete() { + Maybe.empty() + .observeOn(Schedulers.single()) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertResult() + ; + } + + @Test + public void observeOnDispose2() { + TestHelper.checkDisposed(Maybe.empty().observeOn(Schedulers.single())); + } + + @Test + public void observeOnDoubleSubscribe() { + TestHelper.checkDoubleOnSubscribeMaybe(new Function<Maybe<Object>, MaybeSource<Object>>() { + @Override + public MaybeSource<Object> apply(Maybe<Object> m) throws Exception { + return m.observeOn(Schedulers.single()); + } + }); + } + + @Test + public void subscribeOnSuccess() { + String main = Thread.currentThread().getName(); + TestObserver<String> to = Maybe.fromCallable(new Callable<String>() { + @Override + public String call() throws Exception { + return Thread.currentThread().getName(); + } + }) + .subscribeOn(Schedulers.single()) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertValueCount(1) + ; + + assertNotEquals(main, to.values().get(0)); + } + + @Test + public void observeOnErrorThread() { + String main = Thread.currentThread().getName(); + + final String[] name = { null }; + + Maybe.error(new TestException()).observeOn(Schedulers.single()) + .doOnError(new Consumer<Throwable>() { + @Override + public void accept(Throwable e) throws Exception { + name[0] = Thread.currentThread().getName(); + } + }) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertFailure(TestException.class) + ; + + assertNotEquals(main, name[0]); + } + + @Test + public void observeOnCompleteThread() { + String main = Thread.currentThread().getName(); + + final String[] name = { null }; + + Maybe.empty().observeOn(Schedulers.single()) + .doOnComplete(new Action() { + @Override + public void run() throws Exception { + name[0] = Thread.currentThread().getName(); + } + }) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertResult() + ; + + assertNotEquals(main, name[0]); + } + + @Test + public void subscribeOnError() { + Maybe.error(new TestException()) + .subscribeOn(Schedulers.single()) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertFailure(TestException.class) + ; + } + + @Test + public void subscribeOnComplete() { + Maybe.empty() + .subscribeOn(Schedulers.single()) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertResult() + ; + } + + @Test + public void fromAction() { + final int[] call = { 0 }; + + Maybe.fromAction(new Action() { + @Override + public void run() throws Exception { + call[0]++; + } + }) + .test() + .assertResult(); + + assertEquals(1, call[0]); + } + + @Test + public void fromActionThrows() { + Maybe.fromAction(new Action() { + @Override + public void run() throws Exception { + throw new TestException(); + } + }) + .test() + .assertFailure(TestException.class); + } + + @Test + public void fromRunnable() { + final int[] call = { 0 }; + + Maybe.fromRunnable(new Runnable() { + @Override + public void run() { + call[0]++; + } + }) + .test() + .assertResult(); + + assertEquals(1, call[0]); + } + + @Test + public void fromRunnableThrows() { + Maybe.fromRunnable(new Runnable() { + @Override + public void run() { + throw new TestException(); + } + }) + .test() + .assertFailure(TestException.class); + } + + @Test + public void fromCallableThrows() { + Maybe.fromCallable(new Callable<Object>() { + @Override + public Object call() throws Exception { + throw new TestException(); + } + }) + .test() + .assertFailure(TestException.class); + } + + @Test + public void doOnSuccess() { + final Integer[] value = { null }; + + Maybe.just(1).doOnSuccess(new Consumer<Integer>() { + @Override + public void accept(Integer v) throws Exception { + value[0] = v; + } + }) + .test() + .assertResult(1); + + assertEquals(1, value[0].intValue()); + } + + @Test + public void doOnSuccessEmpty() { + final Integer[] value = { null }; + + Maybe.<Integer>empty().doOnSuccess(new Consumer<Integer>() { + @Override + public void accept(Integer v) throws Exception { + value[0] = v; + } + }) + .test() + .assertResult(); + + assertNull(value[0]); + } + + @Test + public void doOnSuccessThrows() { + Maybe.just(1).doOnSuccess(new Consumer<Integer>() { + @Override + public void accept(Integer v) throws Exception { + throw new TestException(); + } + }) + .test() + .assertFailure(TestException.class); + } + + @Test + public void doOnSubscribe() { + final Disposable[] value = { null }; + + Maybe.just(1).doOnSubscribe(new Consumer<Disposable>() { + @Override + public void accept(Disposable v) throws Exception { + value[0] = v; + } + }) + .test() + .assertResult(1); + + assertNotNull(value[0]); + } + + @Test + public void doOnSubscribeThrows() { + Maybe.just(1).doOnSubscribe(new Consumer<Disposable>() { + @Override + public void accept(Disposable v) throws Exception { + throw new TestException(); + } + }) + .test() + .assertFailure(TestException.class); + } + + @Test + public void doOnCompleteThrows() { + Maybe.empty().doOnComplete(new Action() { + @Override + public void run() throws Exception { + throw new TestException(); + } + }) + .test() + .assertFailure(TestException.class); + } + + @Test + public void doOnDispose() { + final int[] call = { 0 }; + + Maybe.just(1).doOnDispose(new Action() { + @Override + public void run() throws Exception { + call[0]++; + } + }) + .to(TestHelper.<Integer>testConsumer(true)) + .assertSubscribed() + .assertNoValues() + .assertNoErrors() + .assertNotComplete(); + + assertEquals(1, call[0]); + } + + @Test + public void doOnDisposeThrows() { + List<Throwable> list = TestHelper.trackPluginErrors(); + + try { + PublishProcessor<Integer> pp = PublishProcessor.create(); + + TestObserverEx<Integer> to = pp.singleElement().doOnDispose(new Action() { + @Override + public void run() throws Exception { + throw new TestException(); + } + }) + .to(TestHelper.<Integer>testConsumer()); + + assertTrue(pp.hasSubscribers()); + + to.dispose(); + + assertFalse(pp.hasSubscribers()); + + to.assertSubscribed() + .assertNoValues() + .assertNoErrors() + .assertNotComplete(); + + TestHelper.assertUndeliverable(list, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void observeOnDispose() throws Exception { + final TestSubscriber<Integer> ts = new TestSubscriber<>(); + + final CountDownLatch cdl = new CountDownLatch(1); + + Maybe.just(1) + .observeOn(Schedulers.single()) + .doOnSuccess(new Consumer<Integer>() { + @Override + public void accept(Integer v) throws Exception { + if (!cdl.await(5, TimeUnit.SECONDS)) { + throw new TimeoutException(); + } + } + }) + .toFlowable().subscribe(ts); + + Thread.sleep(250); + + ts.cancel(); + + ts.awaitDone(5, TimeUnit.SECONDS) + .assertFailure(InterruptedException.class); + } + + @Test + public void doAfterTerminateSuccess() { + final int[] call = { 0 }; + + Maybe.just(1) + .doOnSuccess(new Consumer<Integer>() { + @Override + public void accept(Integer v) throws Exception { + call[0]++; + } + }) + .doAfterTerminate(new Action() { + @Override + public void run() throws Exception { + if (call[0] == 1) { + call[0] = -1; + } + } + }) + .test() + .assertResult(1); + + assertEquals(-1, call[0]); + } + + @Test + public void doAfterTerminateError() { + final int[] call = { 0 }; + + Maybe.error(new TestException()) + .doOnError(new Consumer<Object>() { + @Override + public void accept(Object v) throws Exception { + call[0]++; + } + }) + .doAfterTerminate(new Action() { + @Override + public void run() throws Exception { + if (call[0] == 1) { + call[0] = -1; + } + } + }) + .test() + .assertFailure(TestException.class); + + assertEquals(-1, call[0]); + } + + @Test + public void doAfterTerminateComplete() { + final int[] call = { 0 }; + + Maybe.empty() + .doOnComplete(new Action() { + @Override + public void run() throws Exception { + call[0]++; + } + }) + .doAfterTerminate(new Action() { + @Override + public void run() throws Exception { + if (call[0] == 1) { + call[0] = -1; + } + } + }) + .test() + .assertResult(); + + assertEquals(-1, call[0]); + } + + @Test + public void sourceThrowsNPE() { + try { + Maybe.unsafeCreate(new MaybeSource<Object>() { + @Override + public void subscribe(MaybeObserver<? super Object> observer) { + throw new NullPointerException("Forced failure"); + } + }).test(); + + fail("Should have thrown!"); + } catch (NullPointerException ex) { + assertEquals("Forced failure", ex.getMessage()); + } + } + + @Test + public void sourceThrowsIAE() { + try { + Maybe.unsafeCreate(new MaybeSource<Object>() { + @Override + public void subscribe(MaybeObserver<? super Object> observer) { + throw new IllegalArgumentException("Forced failure"); + } + }).test(); + + fail("Should have thrown!"); + } catch (NullPointerException ex) { + assertTrue(ex.toString(), ex.getCause() instanceof IllegalArgumentException); + assertEquals("Forced failure", ex.getCause().getMessage()); + } + } + + @Test + public void flatMap() { + Maybe.just(1).flatMap(new Function<Integer, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Integer v) throws Exception { + return Maybe.just(v * 10); + } + }) + .test() + .assertResult(10); + } + + @Test + public void concatMap() { + Maybe.just(1).concatMap(new Function<Integer, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Integer v) throws Exception { + return Maybe.just(v * 10); + } + }) + .test() + .assertResult(10); + } + + @Test + public void flatMapEmpty() { + Maybe.just(1).flatMap(new Function<Integer, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Integer v) throws Exception { + return Maybe.empty(); + } + }) + .test() + .assertResult(); + } + + @Test + public void flatMapError() { + Maybe.just(1).flatMap(new Function<Integer, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Integer v) throws Exception { + return Maybe.error(new TestException()); + } + }) + .test() + .assertFailure(TestException.class); + } + + @Test + public void flatMapNotifySuccess() { + Maybe.just(1) + .flatMap(new Function<Integer, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Integer v) throws Exception { + return Maybe.just(v * 10); + } + }, + new Function<Throwable, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Throwable v) throws Exception { + return Maybe.just(100); + } + }, + + new Supplier<MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> get() throws Exception { + return Maybe.just(200); + } + }) + .test() + .assertResult(10); + } + + @Test + public void flatMapNotifyError() { + Maybe.<Integer>error(new TestException()) + .flatMap(new Function<Integer, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Integer v) throws Exception { + return Maybe.just(v * 10); + } + }, + new Function<Throwable, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Throwable v) throws Exception { + return Maybe.just(100); + } + }, + + new Supplier<MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> get() throws Exception { + return Maybe.just(200); + } + }) + .test() + .assertResult(100); + } + + @Test + public void flatMapNotifyComplete() { + Maybe.<Integer>empty() + .flatMap(new Function<Integer, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Integer v) throws Exception { + return Maybe.just(v * 10); + } + }, + new Function<Throwable, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Throwable v) throws Exception { + return Maybe.just(100); + } + }, + + new Supplier<MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> get() throws Exception { + return Maybe.just(200); + } + }) + .test() + .assertResult(200); + } + + @Test + public void ignoreElementSuccess() { + Maybe.just(1) + .ignoreElement() + .test() + .assertResult(); + } + + @Test + public void ignoreElementError() { + Maybe.error(new TestException()) + .ignoreElement() + .test() + .assertFailure(TestException.class); + } + + @Test + public void ignoreElementComplete() { + Maybe.empty() + .ignoreElement() + .test() + .assertResult(); + } + + @Test + public void ignoreElementSuccessMaybe() { + Maybe.just(1) + .ignoreElement() + .toMaybe() + .test() + .assertResult(); + } + + @Test + public void ignoreElementErrorMaybe() { + Maybe.error(new TestException()) + .ignoreElement() + .toMaybe() + .test() + .assertFailure(TestException.class); + } + + @Test + public void ignoreElementCompleteMaybe() { + Maybe.empty() + .ignoreElement() + .toMaybe() + .test() + .assertResult(); + } + + @Test + public void singleToMaybe() { + Single.just(1) + .toMaybe() + .test() + .assertResult(1); + } + + @Test + public void singleToMaybeError() { + Single.error(new TestException()) + .toMaybe() + .test() + .assertFailure(TestException.class); + } + + @Test + public void completableToMaybe() { + Completable.complete() + .toMaybe() + .test() + .assertResult(); + } + + @Test + public void completableToMaybeError() { + Completable.error(new TestException()) + .toMaybe() + .test() + .assertFailure(TestException.class); + } + + @Test + public void emptyToSingle() { + Maybe.empty() + .toSingle() + .test() + .assertFailure(NoSuchElementException.class); + } + + @Test + public void errorToSingle() { + Maybe.error(new TestException()) + .toSingle() + .test() + .assertFailure(TestException.class); + } + + @Test + public void emptyToCompletable() { + Maybe.empty() + .ignoreElement() + .test() + .assertResult(); + } + + @Test + public void errorToCompletable() { + Maybe.error(new TestException()) + .ignoreElement() + .test() + .assertFailure(TestException.class); + } + + @Test + public void concat2() { + Maybe.concat(Maybe.just(1), Maybe.just(2)) + .test() + .assertResult(1, 2); + } + + @Test + public void concat2Empty() { + Maybe.concat(Maybe.empty(), Maybe.empty()) + .test() + .assertResult(); + } + + @Test + public void concat2Backpressured() { + TestSubscriber<Integer> ts = Maybe.concat(Maybe.just(1), Maybe.just(2)) + .test(0L); + + ts.assertEmpty(); + + ts.request(1); + + ts.assertValue(1); + + ts.request(1); + + ts.assertResult(1, 2); + } + + @Test + public void concat2BackpressuredNonEager() { + PublishProcessor<Integer> pp1 = PublishProcessor.create(); + PublishProcessor<Integer> pp2 = PublishProcessor.create(); + + TestSubscriber<Integer> ts = Maybe.concat(pp1.singleElement(), pp2.singleElement()) + .test(0L); + + assertTrue(pp1.hasSubscribers()); + assertFalse(pp2.hasSubscribers()); + + ts.assertEmpty(); + + ts.request(1); + + assertTrue(pp1.hasSubscribers()); + assertFalse(pp2.hasSubscribers()); + + pp1.onNext(1); + pp1.onComplete(); + + ts.assertValue(1); + + assertFalse(pp1.hasSubscribers()); + assertTrue(pp2.hasSubscribers()); + + ts.request(1); + + ts.assertValue(1); + + pp2.onNext(2); + pp2.onComplete(); + + ts.assertResult(1, 2); + + assertFalse(pp1.hasSubscribers()); + assertFalse(pp2.hasSubscribers()); + } + + @Test + public void concat3() { + Maybe.concat(Maybe.just(1), Maybe.just(2), Maybe.just(3)) + .test() + .assertResult(1, 2, 3); + } + + @Test + public void concat3Empty() { + Maybe.concat(Maybe.empty(), Maybe.empty(), Maybe.empty()) + .test() + .assertResult(); + } + + @Test + public void concat3Mixed1() { + Maybe.concat(Maybe.just(1), Maybe.empty(), Maybe.just(3)) + .test() + .assertResult(1, 3); + } + + @Test + public void concat3Mixed2() { + Maybe.concat(Maybe.just(1), Maybe.just(2), Maybe.empty()) + .test() + .assertResult(1, 2); + } + + @Test + public void concat3Backpressured() { + TestSubscriber<Integer> ts = Maybe.concat(Maybe.just(1), Maybe.just(2), Maybe.just(3)) + .test(0L); + + ts.assertEmpty(); + + ts.request(1); + + ts.assertValue(1); + + ts.request(2); + + ts.assertResult(1, 2, 3); + } + + @Test + public void concatArrayZero() { + assertSame(Flowable.empty(), Maybe.concatArray()); + } + + @Test + public void concatArrayOne() { + Maybe.concatArray(Maybe.just(1)).test().assertResult(1); + } + + @Test + public void concat4() { + Maybe.concat(Maybe.just(1), Maybe.just(2), Maybe.just(3), Maybe.just(4)) + .test() + .assertResult(1, 2, 3, 4); + } + + @Test + public void concatIterable() { + Maybe.concat(Arrays.asList(Maybe.just(1), Maybe.just(2))) + .test() + .assertResult(1, 2); + } + + @Test + public void concatIterableEmpty() { + Maybe.concat(Arrays.asList(Maybe.empty(), Maybe.empty())) + .test() + .assertResult(); + } + + @Test + public void concatIterableBackpressured() { + TestSubscriber<Integer> ts = Maybe.concat(Arrays.asList(Maybe.just(1), Maybe.just(2))) + .test(0L); + + ts.assertEmpty(); + + ts.request(1); + + ts.assertValue(1); + + ts.request(1); + + ts.assertResult(1, 2); + } + + @Test + public void concatIterableBackpressuredNonEager() { + PublishProcessor<Integer> pp1 = PublishProcessor.create(); + PublishProcessor<Integer> pp2 = PublishProcessor.create(); + + TestSubscriber<Integer> ts = Maybe.concat(Arrays.asList(pp1.singleElement(), pp2.singleElement())) + .test(0L); + + assertTrue(pp1.hasSubscribers()); + assertFalse(pp2.hasSubscribers()); + + ts.assertEmpty(); + + ts.request(1); + + assertTrue(pp1.hasSubscribers()); + assertFalse(pp2.hasSubscribers()); + + pp1.onNext(1); + pp1.onComplete(); + + ts.assertValue(1); + + assertFalse(pp1.hasSubscribers()); + assertTrue(pp2.hasSubscribers()); + + ts.request(1); + + ts.assertValue(1); + + pp2.onNext(2); + pp2.onComplete(); + + ts.assertResult(1, 2); + + assertFalse(pp1.hasSubscribers()); + assertFalse(pp2.hasSubscribers()); + } + + @Test + public void concatIterableZero() { + Maybe.concat(Collections.<Maybe<Integer>>emptyList()).test().assertResult(); + } + + @Test + public void concatIterableOne() { + Maybe.concat(Collections.<Maybe<Integer>>singleton(Maybe.just(1))).test().assertResult(1); + } + + @Test + public void concatPublisher() { + Maybe.concat(Flowable.just(Maybe.just(1), Maybe.just(2))).test().assertResult(1, 2); + } + + @Test + public void concatPublisherPrefetch() { + Maybe.concat(Flowable.just(Maybe.just(1), Maybe.just(2)), 1).test().assertResult(1, 2); + } + + @Test + public void basic() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + final Disposable d = Disposable.empty(); + + Maybe.<Integer>create(new MaybeOnSubscribe<Integer>() { + @Override + public void subscribe(MaybeEmitter<Integer> e) throws Exception { + e.setDisposable(d); + + e.onSuccess(1); + e.onError(new TestException()); + e.onSuccess(2); + e.onError(new TestException()); + e.onComplete(); + } + }) + .test() + .assertResult(1); + + assertTrue(d.isDisposed()); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + TestHelper.assertUndeliverable(errors, 1, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void basicWithError() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + final Disposable d = Disposable.empty(); + + Maybe.<Integer>create(new MaybeOnSubscribe<Integer>() { + @Override + public void subscribe(MaybeEmitter<Integer> e) throws Exception { + e.setDisposable(d); + + e.onError(new TestException()); + e.onSuccess(2); + e.onError(new TestException()); + e.onComplete(); + } + }) + .test() + .assertFailure(TestException.class); + + assertTrue(d.isDisposed()); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void basicWithComplete() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + final Disposable d = Disposable.empty(); + + Maybe.<Integer>create(new MaybeOnSubscribe<Integer>() { + @Override + public void subscribe(MaybeEmitter<Integer> e) throws Exception { + e.setDisposable(d); + + e.onComplete(); + e.onSuccess(1); + e.onError(new TestException()); + e.onComplete(); + e.onSuccess(2); + e.onError(new TestException()); + } + }) + .test() + .assertResult(); + + assertTrue(d.isDisposed()); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + TestHelper.assertUndeliverable(errors, 1, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test(expected = IllegalArgumentException.class) + public void unsafeCreateWithMaybe() { + Maybe.unsafeCreate(Maybe.just(1)); + } + + @Test + public void maybeToPublisherEnum() { + TestHelper.checkEnum(MaybeToPublisher.class); + } + + @Test + public void ambArrayOneIsNull() { + Maybe.ambArray(null, Maybe.just(1)) + .test() + .assertError(NullPointerException.class); + } + + @Test + public void ambArrayEmpty() { + assertSame(Maybe.empty(), Maybe.ambArray()); + } + + @Test + public void ambArrayOne() { + assertSame(Maybe.never(), Maybe.ambArray(Maybe.never())); + } + + @Test + public void ambWithOrder() { + Maybe<Integer> error = Maybe.error(new RuntimeException()); + Maybe.just(1).ambWith(error).test().assertValue(1); + } + + @Test + public void ambIterableOrder() { + Maybe<Integer> error = Maybe.error(new RuntimeException()); + Maybe.amb(Arrays.asList(Maybe.just(1), error)).test().assertValue(1); + } + + @Test + public void ambArrayOrder() { + Maybe<Integer> error = Maybe.error(new RuntimeException()); + Maybe.ambArray(Maybe.just(1), error).test().assertValue(1); + } + + @Test + public void ambArray1SignalsSuccess() { + PublishProcessor<Integer> pp1 = PublishProcessor.create(); + PublishProcessor<Integer> pp2 = PublishProcessor.create(); + + TestObserver<Integer> to = Maybe.ambArray(pp1.singleElement(), pp2.singleElement()) + .test(); + + to.assertEmpty(); + + assertTrue(pp1.hasSubscribers()); + assertTrue(pp2.hasSubscribers()); + + pp1.onNext(1); + pp1.onComplete(); + + assertFalse(pp1.hasSubscribers()); + assertFalse(pp2.hasSubscribers()); + + to.assertResult(1); + } + + @Test + public void ambArray2SignalsSuccess() { + PublishProcessor<Integer> pp1 = PublishProcessor.create(); + PublishProcessor<Integer> pp2 = PublishProcessor.create(); + + TestObserver<Integer> to = Maybe.ambArray(pp1.singleElement(), pp2.singleElement()) + .test(); + + to.assertEmpty(); + + assertTrue(pp1.hasSubscribers()); + assertTrue(pp2.hasSubscribers()); + + pp2.onNext(2); + pp2.onComplete(); + + assertFalse(pp1.hasSubscribers()); + assertFalse(pp2.hasSubscribers()); + + to.assertResult(2); + } + + @Test + public void ambArray1SignalsError() { + PublishProcessor<Integer> pp1 = PublishProcessor.create(); + PublishProcessor<Integer> pp2 = PublishProcessor.create(); + + TestObserver<Integer> to = Maybe.ambArray(pp1.singleElement(), pp2.singleElement()) + .test(); + + to.assertEmpty(); + + assertTrue(pp1.hasSubscribers()); + assertTrue(pp2.hasSubscribers()); + + pp1.onError(new TestException()); + + assertFalse(pp1.hasSubscribers()); + assertFalse(pp2.hasSubscribers()); + + to.assertFailure(TestException.class); + } + + @Test + public void ambArray2SignalsError() { + PublishProcessor<Integer> pp1 = PublishProcessor.create(); + PublishProcessor<Integer> pp2 = PublishProcessor.create(); + + TestObserver<Integer> to = Maybe.ambArray(pp1.singleElement(), pp2.singleElement()) + .test(); + + to.assertEmpty(); + + assertTrue(pp1.hasSubscribers()); + assertTrue(pp2.hasSubscribers()); + + pp2.onError(new TestException()); + + assertFalse(pp1.hasSubscribers()); + assertFalse(pp2.hasSubscribers()); + + to.assertFailure(TestException.class); + } + + @Test + public void ambArray1SignalsComplete() { + PublishProcessor<Integer> pp1 = PublishProcessor.create(); + PublishProcessor<Integer> pp2 = PublishProcessor.create(); + + TestObserver<Integer> to = Maybe.ambArray(pp1.singleElement(), pp2.singleElement()) + .test(); + + to.assertEmpty(); + + assertTrue(pp1.hasSubscribers()); + assertTrue(pp2.hasSubscribers()); + + pp1.onComplete(); + + assertFalse(pp1.hasSubscribers()); + assertFalse(pp2.hasSubscribers()); + + to.assertResult(); + } + + @Test + public void ambArray2SignalsComplete() { + PublishProcessor<Integer> pp1 = PublishProcessor.create(); + PublishProcessor<Integer> pp2 = PublishProcessor.create(); + + TestObserver<Integer> to = Maybe.ambArray(pp1.singleElement(), pp2.singleElement()) + .test(); + + to.assertEmpty(); + + assertTrue(pp1.hasSubscribers()); + assertTrue(pp2.hasSubscribers()); + + pp2.onComplete(); + + assertFalse(pp1.hasSubscribers()); + assertFalse(pp2.hasSubscribers()); + + to.assertResult(); + } + + @Test + public void ambIterable1SignalsSuccess() { + PublishProcessor<Integer> pp1 = PublishProcessor.create(); + PublishProcessor<Integer> pp2 = PublishProcessor.create(); + + TestObserver<Integer> to = Maybe.amb(Arrays.asList(pp1.singleElement(), pp2.singleElement())) + .test(); + + to.assertEmpty(); + + assertTrue(pp1.hasSubscribers()); + assertTrue(pp2.hasSubscribers()); + + pp1.onNext(1); + pp1.onComplete(); + + assertFalse(pp1.hasSubscribers()); + assertFalse(pp2.hasSubscribers()); + + to.assertResult(1); + } + + @Test + public void ambIterable2SignalsSuccess() { + PublishProcessor<Integer> pp1 = PublishProcessor.create(); + PublishProcessor<Integer> pp2 = PublishProcessor.create(); + + TestObserver<Integer> to = Maybe.amb(Arrays.asList(pp1.singleElement(), pp2.singleElement())) + .test(); + + to.assertEmpty(); + + assertTrue(pp1.hasSubscribers()); + assertTrue(pp2.hasSubscribers()); + + pp2.onNext(2); + pp2.onComplete(); + + assertFalse(pp1.hasSubscribers()); + assertFalse(pp2.hasSubscribers()); + + to.assertResult(2); + } + + @Test + public void ambIterable2SignalsSuccessWithOverlap() { + PublishProcessor<Integer> pp1 = PublishProcessor.create(); + PublishProcessor<Integer> pp2 = PublishProcessor.create(); + + TestObserver<Integer> to = Maybe.amb(Arrays.asList(pp1.singleElement(), pp2.singleElement())) + .test(); + + to.assertEmpty(); + + assertTrue(pp1.hasSubscribers()); + assertTrue(pp2.hasSubscribers()); + + pp2.onNext(2); + pp1.onNext(1); + pp2.onComplete(); + + assertFalse(pp1.hasSubscribers()); + assertFalse(pp2.hasSubscribers()); + + to.assertResult(2); + } + + @Test + public void ambIterable1SignalsError() { + PublishProcessor<Integer> pp1 = PublishProcessor.create(); + PublishProcessor<Integer> pp2 = PublishProcessor.create(); + + TestObserver<Integer> to = Maybe.amb(Arrays.asList(pp1.singleElement(), pp2.singleElement())) + .test(); + + to.assertEmpty(); + + assertTrue(pp1.hasSubscribers()); + assertTrue(pp2.hasSubscribers()); + + pp1.onError(new TestException()); + + assertFalse(pp1.hasSubscribers()); + assertFalse(pp2.hasSubscribers()); + + to.assertFailure(TestException.class); + } + + @Test + public void ambIterable2SignalsError() { + PublishProcessor<Integer> pp1 = PublishProcessor.create(); + PublishProcessor<Integer> pp2 = PublishProcessor.create(); + + TestObserver<Integer> to = Maybe.amb(Arrays.asList(pp1.singleElement(), pp2.singleElement())) + .test(); + + to.assertEmpty(); + + assertTrue(pp1.hasSubscribers()); + assertTrue(pp2.hasSubscribers()); + + pp2.onError(new TestException()); + + assertFalse(pp1.hasSubscribers()); + assertFalse(pp2.hasSubscribers()); + + to.assertFailure(TestException.class); + } + + @Test + public void ambIterable2SignalsErrorWithOverlap() { + PublishProcessor<Integer> pp1 = PublishProcessor.create(); + PublishProcessor<Integer> pp2 = PublishProcessor.create(); + + TestObserverEx<Integer> to = Maybe.amb(Arrays.asList(pp1.singleElement(), pp2.singleElement())) + .to(TestHelper.<Integer>testConsumer()); + + to.assertEmpty(); + + assertTrue(pp1.hasSubscribers()); + assertTrue(pp2.hasSubscribers()); + + pp2.onError(new TestException("2")); + pp1.onError(new TestException("1")); + + assertFalse(pp1.hasSubscribers()); + assertFalse(pp2.hasSubscribers()); + + to.assertFailureAndMessage(TestException.class, "2"); + } + + @Test + public void ambIterable1SignalsComplete() { + PublishProcessor<Integer> pp1 = PublishProcessor.create(); + PublishProcessor<Integer> pp2 = PublishProcessor.create(); + + TestObserver<Integer> to = Maybe.amb(Arrays.asList(pp1.singleElement(), pp2.singleElement())) + .test(); + + to.assertEmpty(); + + assertTrue(pp1.hasSubscribers()); + assertTrue(pp2.hasSubscribers()); + + pp1.onComplete(); + + assertFalse(pp1.hasSubscribers()); + assertFalse(pp2.hasSubscribers()); + + to.assertResult(); + } + + @Test + public void ambIterable2SignalsComplete() { + PublishProcessor<Integer> pp1 = PublishProcessor.create(); + PublishProcessor<Integer> pp2 = PublishProcessor.create(); + + TestObserver<Integer> to = Maybe.amb(Arrays.asList(pp1.singleElement(), pp2.singleElement())) + .test(); + + to.assertEmpty(); + + assertTrue(pp1.hasSubscribers()); + assertTrue(pp2.hasSubscribers()); + + pp2.onComplete(); + + assertFalse(pp1.hasSubscribers()); + assertFalse(pp2.hasSubscribers()); + + to.assertResult(); + } + + @Test + public void ambIterableIteratorNull() { + Maybe.amb(new Iterable<Maybe<Object>>() { + @Override + public Iterator<Maybe<Object>> iterator() { + return null; + } + }).test().assertError(NullPointerException.class); + } + + @Test + public void ambIterableOneIsNull() { + Maybe.amb(Arrays.asList(null, Maybe.just(1))) + .test() + .assertError(NullPointerException.class); + } + + @Test + public void ambIterableEmpty() { + Maybe.amb(Collections.<Maybe<Integer>>emptyList()).test().assertResult(); + } + + @Test + public void ambIterableOne() { + Maybe.amb(Collections.singleton(Maybe.just(1))).test().assertResult(1); + } + + @Test + public void mergeArray() { + Maybe.mergeArray(Maybe.just(1), Maybe.just(2), Maybe.just(3)) + .test() + .assertResult(1, 2, 3); + } + + @Test + public void merge2() { + Maybe.merge(Maybe.just(1), Maybe.just(2)) + .test() + .assertResult(1, 2); + } + + @Test + public void merge3() { + Maybe.merge(Maybe.just(1), Maybe.just(2), Maybe.just(3)) + .test() + .assertResult(1, 2, 3); + } + + @Test + public void merge4() { + Maybe.merge(Maybe.just(1), Maybe.just(2), Maybe.just(3), Maybe.just(4)) + .test() + .assertResult(1, 2, 3, 4); + } + + @Test + public void merge4Take2() { + Maybe.merge(Maybe.just(1), Maybe.just(2), Maybe.just(3), Maybe.just(4)) + .take(2) + .test() + .assertResult(1, 2); + } + + @Test + public void mergeArrayBackpressured() { + TestSubscriber<Integer> ts = Maybe.mergeArray(Maybe.just(1), Maybe.just(2), Maybe.just(3)) + .test(0L); + + ts.assertEmpty(); + + ts.request(1); + + ts.assertValue(1); + + ts.request(1); + + ts.assertValues(1, 2); + + ts.request(1); + ts.assertResult(1, 2, 3); + } + + @Test + public void mergeArrayBackpressuredMixed1() { + TestSubscriber<Integer> ts = Maybe.mergeArray(Maybe.just(1), Maybe.<Integer>empty(), Maybe.just(3)) + .test(0L); + + ts.assertEmpty(); + + ts.request(1); + + ts.assertValue(1); + + ts.request(1); + + ts.assertResult(1, 3); + } + + @Test + public void mergeArrayBackpressuredMixed2() { + TestSubscriber<Integer> ts = Maybe.mergeArray(Maybe.just(1), Maybe.just(2), Maybe.<Integer>empty()) + .test(0L); + + ts.assertEmpty(); + + ts.request(1); + + ts.assertValue(1); + + ts.request(1); + + ts.assertResult(1, 2); + } + + @Test + public void mergeArrayBackpressuredMixed3() { + TestSubscriber<Integer> ts = Maybe.mergeArray(Maybe.<Integer>empty(), Maybe.just(2), Maybe.just(3)) + .test(0L); + + ts.assertEmpty(); + + ts.request(1); + + ts.assertValue(2); + + ts.request(1); + + ts.assertResult(2, 3); + } + + @Test + public void mergeArrayFused() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<Integer>().setInitialFusionMode(QueueFuseable.ANY); + + Maybe.mergeArray(Maybe.just(1), Maybe.just(2), Maybe.just(3)).subscribe(ts); + + ts.assertSubscribed() + .assertFuseable() + .assertFusionMode(QueueFuseable.ASYNC) + .assertResult(1, 2, 3); + } + + @Test + public void mergeArrayFusedRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final PublishProcessor<Integer> pp1 = PublishProcessor.create(); + final PublishProcessor<Integer> pp2 = PublishProcessor.create(); + + TestSubscriberEx<Integer> ts = new TestSubscriberEx<Integer>().setInitialFusionMode(QueueFuseable.ANY); + + Maybe.mergeArray(pp1.singleElement(), pp2.singleElement()).subscribe(ts); + + ts.assertSubscribed() + .assertFuseable() + .assertFusionMode(QueueFuseable.ASYNC) + ; + + TestHelper.race(new Runnable() { + @Override + public void run() { + pp1.onNext(1); + pp1.onComplete(); + } + }, new Runnable() { + @Override + public void run() { + pp2.onNext(1); + pp2.onComplete(); + } + }); + + ts + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(1, 1); + } + } + + @Test + public void mergeArrayZero() { + assertSame(Flowable.empty(), Maybe.mergeArray()); + } + + @Test + public void mergeArrayOne() { + Maybe.mergeArray(Maybe.just(1)).test().assertResult(1); + } + + @Test + public void mergePublisher() { + Maybe.merge(Flowable.just(Maybe.just(1), Maybe.just(2), Maybe.just(3))) + .test() + .assertResult(1, 2, 3); + } + + @Test + public void mergePublisherMaxConcurrent() { + final PublishProcessor<Integer> pp1 = PublishProcessor.create(); + final PublishProcessor<Integer> pp2 = PublishProcessor.create(); + + TestSubscriber<Integer> ts = Maybe.merge(Flowable.just(pp1.singleElement(), pp2.singleElement()), 1).test(0L); + + assertTrue(pp1.hasSubscribers()); + assertFalse(pp2.hasSubscribers()); + + pp1.onNext(1); + pp1.onComplete(); + + ts.request(1); + + assertFalse(pp1.hasSubscribers()); + assertTrue(pp2.hasSubscribers()); + } + + @Test + public void mergeMaybe() { + Maybe.merge(Maybe.just(Maybe.just(1))) + .test() + .assertResult(1); + } + + @Test + public void mergeIterable() { + Maybe.merge(Arrays.asList(Maybe.just(1), Maybe.just(2), Maybe.just(3))) + .test() + .assertResult(1, 2, 3); + } + + @Test + public void mergeALot() { + @SuppressWarnings("unchecked") + Maybe<Integer>[] sources = new Maybe[Flowable.bufferSize() * 2]; + Arrays.fill(sources, Maybe.just(1)); + + Maybe.mergeArray(sources) + .to(TestHelper.<Integer>testConsumer()) + .assertSubscribed() + .assertValueCount(sources.length) + .assertNoErrors() + .assertComplete(); + } + + @Test + public void mergeALotLastEmpty() { + @SuppressWarnings("unchecked") + Maybe<Integer>[] sources = new Maybe[Flowable.bufferSize() * 2]; + Arrays.fill(sources, Maybe.just(1)); + sources[sources.length - 1] = Maybe.empty(); + + Maybe.mergeArray(sources) + .to(TestHelper.<Integer>testConsumer()) + .assertSubscribed() + .assertValueCount(sources.length - 1) + .assertNoErrors() + .assertComplete(); + } + + @Test + public void mergeALotFused() { + @SuppressWarnings("unchecked") + Maybe<Integer>[] sources = new Maybe[Flowable.bufferSize() * 2]; + Arrays.fill(sources, Maybe.just(1)); + + TestSubscriberEx<Integer> ts = new TestSubscriberEx<Integer>().setInitialFusionMode(QueueFuseable.ANY); + + Maybe.mergeArray(sources).subscribe(ts); + + ts + .assertSubscribed() + .assertFuseable() + .assertFusionMode(QueueFuseable.ASYNC) + .assertValueCount(sources.length) + .assertNoErrors() + .assertComplete(); + } + + @Test + public void mergeErrorSuccess() { + Maybe.merge(Maybe.error(new TestException()), Maybe.just(1)) + .test() + .assertFailure(TestException.class); + } + + @Test + public void mergeSuccessError() { + Maybe.merge(Maybe.just(1), Maybe.error(new TestException())) + .test() + .assertFailure(TestException.class, 1); + } + + @Test + public void subscribeZero() { + assertTrue(Maybe.just(1) + .subscribe().isDisposed()); + } + + @Test + public void subscribeZeroError() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + + try { + assertTrue(Maybe.error(new TestException()) + .subscribe().isDisposed()); + + TestHelper.assertError(errors, 0, OnErrorNotImplementedException.class); + Throwable c = errors.get(0).getCause(); + assertTrue("" + c, c instanceof TestException); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void subscribeToOnSuccess() { + final List<Integer> values = new ArrayList<>(); + + Consumer<Integer> onSuccess = new Consumer<Integer>() { + @Override + public void accept(Integer e) throws Exception { + values.add(e); + } + }; + + Maybe<Integer> source = Maybe.just(1); + + source.subscribe(onSuccess); + source.subscribe(onSuccess, Functions.emptyConsumer()); + source.subscribe(onSuccess, Functions.emptyConsumer(), Functions.EMPTY_ACTION); + + assertEquals(Arrays.asList(1, 1, 1), values); + } + + @Test + public void subscribeToOnError() { + final List<Throwable> values = new ArrayList<>(); + + Consumer<Throwable> onError = new Consumer<Throwable>() { + @Override + public void accept(Throwable e) throws Exception { + values.add(e); + } + }; + + TestException ex = new TestException(); + + Maybe<Integer> source = Maybe.error(ex); + + source.subscribe(Functions.emptyConsumer(), onError); + source.subscribe(Functions.emptyConsumer(), onError, Functions.EMPTY_ACTION); + + assertEquals(Arrays.asList(ex, ex), values); + } + + @Test + public void subscribeToOnComplete() { + final List<Integer> values = new ArrayList<>(); + + Action onComplete = new Action() { + @Override + public void run() throws Exception { + values.add(100); + } + }; + + Maybe<Integer> source = Maybe.empty(); + + source.subscribe(Functions.emptyConsumer(), Functions.emptyConsumer(), onComplete); + + assertEquals(Arrays.asList(100), values); + } + + @Test + public void subscribeWith() { + MaybeObserver<Integer> mo = new MaybeObserver<Integer>() { + + @Override + public void onSubscribe(Disposable d) { + + } + + @Override + public void onSuccess(Integer value) { + + } + + @Override + public void onError(Throwable e) { + + } + + @Override + public void onComplete() { + + } + + }; + + assertSame(mo, Maybe.just(1).subscribeWith(mo)); + } + + @Test + public void doOnEventSuccess() { + final List<Object> list = new ArrayList<>(); + + assertTrue(Maybe.just(1) + .doOnEvent(new BiConsumer<Integer, Throwable>() { + @Override + public void accept(Integer v, Throwable e) throws Exception { + list.add(v); + list.add(e); + } + }) + .subscribe().isDisposed()); + + assertEquals(Arrays.asList(1, null), list); + } + + @Test + public void doOnEventError() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + final List<Object> list = new ArrayList<>(); + + TestException ex = new TestException(); + + assertTrue(Maybe.<Integer>error(ex) + .doOnEvent(new BiConsumer<Integer, Throwable>() { + @Override + public void accept(Integer v, Throwable e) throws Exception { + list.add(v); + list.add(e); + } + }) + .subscribe().isDisposed()); + + assertEquals(Arrays.asList(null, ex), list); + + TestHelper.assertError(errors, 0, OnErrorNotImplementedException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void doOnEventComplete() { + final List<Object> list = new ArrayList<>(); + + assertTrue(Maybe.<Integer>empty() + .doOnEvent(new BiConsumer<Integer, Throwable>() { + @Override + public void accept(Integer v, Throwable e) throws Exception { + list.add(v); + list.add(e); + } + }) + .subscribe().isDisposed()); + + assertEquals(Arrays.asList(null, null), list); + } + + @Test + public void doOnEventSuccessThrows() { + Maybe.just(1) + .doOnEvent(new BiConsumer<Integer, Throwable>() { + @Override + public void accept(Integer v, Throwable e) throws Exception { + throw new TestException(); + } + }) + .test() + .assertFailure(TestException.class); + } + + @Test + public void doOnEventErrorThrows() { + TestObserverEx<Integer> to = Maybe.<Integer>error(new TestException("Outer")) + .doOnEvent(new BiConsumer<Integer, Throwable>() { + @Override + public void accept(Integer v, Throwable e) throws Exception { + throw new TestException("Inner"); + } + }) + .to(TestHelper.<Integer>testConsumer()) + .assertFailure(CompositeException.class); + + List<Throwable> list = TestHelper.compositeList(to.errors().get(0)); + TestHelper.assertError(list, 0, TestException.class, "Outer"); + TestHelper.assertError(list, 1, TestException.class, "Inner"); + assertEquals(2, list.size()); + } + + @Test + public void doOnEventCompleteThrows() { + Maybe.<Integer>empty() + .doOnEvent(new BiConsumer<Integer, Throwable>() { + @Override + public void accept(Integer v, Throwable e) throws Exception { + throw new TestException(); + } + }) + .test() + .assertFailure(TestException.class); + } + + @Test + public void concatArrayDelayError() { + Maybe.concatArrayDelayError(Maybe.empty(), Maybe.just(1), Maybe.error(new TestException())) + .test() + .assertFailure(TestException.class, 1); + + Maybe.concatArrayDelayError(Maybe.error(new TestException()), Maybe.empty(), Maybe.just(1)) + .test() + .assertFailure(TestException.class, 1); + + assertSame(Flowable.empty(), Maybe.concatArrayDelayError()); + + assertFalse(Maybe.concatArrayDelayError(Maybe.never()) instanceof MaybeConcatArrayDelayError); + } + + @Test + public void concatIterableDelayError() { + Maybe.concatDelayError(Arrays.asList(Maybe.empty(), Maybe.just(1), Maybe.error(new TestException()))) + .test() + .assertFailure(TestException.class, 1); + + Maybe.concatDelayError(Arrays.asList(Maybe.error(new TestException()), Maybe.empty(), Maybe.just(1))) + .test() + .assertFailure(TestException.class, 1); + } + + @Test + public void concatPublisherDelayError() { + Maybe.concatDelayError(Flowable.just(Maybe.empty(), Maybe.just(1), Maybe.error(new TestException()))) + .test() + .assertFailure(TestException.class, 1); + + Maybe.concatDelayError(Flowable.just(Maybe.error(new TestException()), Maybe.empty(), Maybe.just(1))) + .test() + .assertFailure(TestException.class, 1); + } + + @Test + public void concatPublisherDelayErrorPrefetch() { + Maybe.concatDelayError(Flowable.just(Maybe.empty(), Maybe.just(1), Maybe.error(new TestException())), 1) + .test() + .assertFailure(TestException.class, 1); + + Maybe.concatDelayError(Flowable.just(Maybe.error(new TestException()), Maybe.empty(), Maybe.just(1)), 1) + .test() + .assertFailure(TestException.class, 1); + } + + @Test + public void concatEagerArray() { + PublishProcessor<Integer> pp1 = PublishProcessor.create(); + PublishProcessor<Integer> pp2 = PublishProcessor.create(); + + TestSubscriber<Integer> ts = Maybe.concatArrayEager(pp1.singleElement(), pp2.singleElement()).test(); + + assertTrue(pp1.hasSubscribers()); + assertTrue(pp2.hasSubscribers()); + + pp2.onNext(2); + pp2.onComplete(); + + ts.assertEmpty(); + + pp1.onNext(1); + pp1.onComplete(); + + ts.assertResult(1, 2); + + } + + @Test + public void concatEagerIterable() { + PublishProcessor<Integer> pp1 = PublishProcessor.create(); + PublishProcessor<Integer> pp2 = PublishProcessor.create(); + + TestSubscriber<Integer> ts = Maybe.concatEager(Arrays.asList(pp1.singleElement(), pp2.singleElement())).test(); + + assertTrue(pp1.hasSubscribers()); + assertTrue(pp2.hasSubscribers()); + + pp2.onNext(2); + pp2.onComplete(); + + ts.assertEmpty(); + + pp1.onNext(1); + pp1.onComplete(); + + ts.assertResult(1, 2); + + } + + @Test + public void concatEagerPublisher() { + PublishProcessor<Integer> pp1 = PublishProcessor.create(); + PublishProcessor<Integer> pp2 = PublishProcessor.create(); + + TestSubscriber<Integer> ts = Maybe.concatEager(Flowable.just(pp1.singleElement(), pp2.singleElement())).test(); + + assertTrue(pp1.hasSubscribers()); + assertTrue(pp2.hasSubscribers()); + + pp2.onNext(2); + pp2.onComplete(); + + ts.assertEmpty(); + + pp1.onNext(1); + pp1.onComplete(); + + ts.assertResult(1, 2); + + } + + static Future<Integer> emptyFuture() { + final ScheduledExecutorService exec = Executors.newSingleThreadScheduledExecutor(); + + return exec.schedule(new Callable<Integer>() { + + @Override + public Integer call() throws Exception { + exec.shutdown(); + return null; + } + }, 200, TimeUnit.MILLISECONDS); + } + + @Test + public void fromFuture() { + Maybe.fromFuture(Flowable.just(1).delay(200, TimeUnit.MILLISECONDS).toFuture()) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(1) + ; + + Maybe.fromFuture(emptyFuture()) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertResult() + ; + + Maybe.fromFuture(Flowable.error(new TestException()).delay(200, TimeUnit.MILLISECONDS, true).toFuture()) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertFailure(TestException.class) + ; + + Maybe.fromFuture(Flowable.empty().delay(10, TimeUnit.SECONDS).toFuture(), 100, TimeUnit.MILLISECONDS) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertFailure(TimeoutException.class) + ; + } + + @Test + public void mergeArrayDelayError() { + Maybe.mergeArrayDelayError(Maybe.empty(), Maybe.just(1), Maybe.error(new TestException())) + .test() + .assertFailure(TestException.class, 1); + + Maybe.mergeArrayDelayError(Maybe.error(new TestException()), Maybe.empty(), Maybe.just(1)) + .test() + .assertFailure(TestException.class, 1); + } + + @Test + public void mergeIterableDelayError() { + Maybe.mergeDelayError(Arrays.asList(Maybe.empty(), Maybe.just(1), Maybe.error(new TestException()))) + .test() + .assertFailure(TestException.class, 1); + + Maybe.mergeDelayError(Arrays.asList(Maybe.error(new TestException()), Maybe.empty(), Maybe.just(1))) + .test() + .assertFailure(TestException.class, 1); + } + + @Test + public void mergePublisherDelayError() { + Maybe.mergeDelayError(Flowable.just(Maybe.empty(), Maybe.just(1), Maybe.error(new TestException()))) + .test() + .assertFailure(TestException.class, 1); + + Maybe.mergeDelayError(Flowable.just(Maybe.error(new TestException()), Maybe.empty(), Maybe.just(1))) + .test() + .assertFailure(TestException.class, 1); + } + + @Test + public void mergeDelayError2() { + Maybe.mergeDelayError(Maybe.just(1), Maybe.error(new TestException())) + .test() + .assertFailure(TestException.class, 1); + + Maybe.mergeDelayError(Maybe.error(new TestException()), Maybe.just(1)) + .test() + .assertFailure(TestException.class, 1); + } + + @Test + public void mergeDelayError3() { + Maybe.mergeDelayError(Maybe.just(1), Maybe.error(new TestException()), Maybe.just(2)) + .test() + .assertFailure(TestException.class, 1, 2); + + Maybe.mergeDelayError(Maybe.error(new TestException()), Maybe.just(1), Maybe.just(2)) + .test() + .assertFailure(TestException.class, 1, 2); + + Maybe.mergeDelayError(Maybe.just(1), Maybe.just(2), Maybe.error(new TestException())) + .test() + .assertFailure(TestException.class, 1, 2); + } + + @Test + public void mergeDelayError4() { + Maybe.mergeDelayError(Maybe.just(1), Maybe.error(new TestException()), Maybe.just(2), Maybe.just(3)) + .test() + .assertFailure(TestException.class, 1, 2, 3); + + Maybe.mergeDelayError(Maybe.error(new TestException()), Maybe.just(1), Maybe.just(2), Maybe.just(3)) + .test() + .assertFailure(TestException.class, 1, 2, 3); + + Maybe.mergeDelayError(Maybe.just(1), Maybe.just(2), Maybe.just(3), Maybe.error(new TestException())) + .test() + .assertFailure(TestException.class, 1, 2, 3); + } + + @Test + public void sequenceEqual() { + Maybe.sequenceEqual(Maybe.just(1_000_000), Maybe.just(Integer.valueOf(1_000_000))).test().assertResult(true); + + Maybe.sequenceEqual(Maybe.just(1), Maybe.just(2)).test().assertResult(false); + + Maybe.sequenceEqual(Maybe.just(1), Maybe.empty()).test().assertResult(false); + + Maybe.sequenceEqual(Maybe.empty(), Maybe.just(2)).test().assertResult(false); + + Maybe.sequenceEqual(Maybe.empty(), Maybe.empty()).test().assertResult(true); + + Maybe.sequenceEqual(Maybe.just(1), Maybe.error(new TestException())).test().assertFailure(TestException.class); + + Maybe.sequenceEqual(Maybe.error(new TestException()), Maybe.just(1)).test().assertFailure(TestException.class); + + List<Throwable> errors = TestHelper.trackPluginErrors(); + + try { + Maybe.sequenceEqual(Maybe.error(new TestException("One")), Maybe.error(new TestException("Two"))) + .to(TestHelper.<Boolean>testConsumer()) + .assertFailureAndMessage(TestException.class, "One"); + + TestHelper.assertUndeliverable(errors, 0, TestException.class, "Two"); + } finally { + RxJavaPlugins.reset(); + } + + Maybe.sequenceEqual(Maybe.just(1), Maybe.error(new TestException()), new BiPredicate<Object, Object>() { + @Override + public boolean test(Object t1, Object t2) throws Exception { + throw new TestException(); + } + }) + .test().assertFailure(TestException.class); + } + + @Test + public void timer() { + Maybe.timer(100, TimeUnit.MILLISECONDS) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(0L); + } + + @Test + public void blockingGet() { + assertEquals(1, Maybe.just(1).blockingGet().intValue()); + + assertEquals(100, Maybe.empty().blockingGet(100)); + + try { + Maybe.error(new TestException()).blockingGet(); + fail("Should have thrown!"); + } catch (TestException ex) { + // expected + } + + try { + Maybe.error(new TestException()).blockingGet(100); + fail("Should have thrown!"); + } catch (TestException ex) { + // expected + } + } + + @Test + public void flatMapContinuation() { + Maybe.just(1).flatMapCompletable(new Function<Integer, Completable>() { + @Override + public Completable apply(Integer v) throws Exception { + return Completable.complete(); + } + }) + .test().assertResult(); + + Maybe.just(1).flatMapCompletable(new Function<Integer, Completable>() { + @Override + public Completable apply(Integer v) throws Exception { + return Completable.error(new TestException()); + } + }) + .test().assertFailure(TestException.class); + + Maybe.just(1).flatMapPublisher(new Function<Integer, Publisher<Integer>>() { + @Override + public Publisher<Integer> apply(Integer v) throws Exception { + return Flowable.range(1, 5); + } + }) + .test().assertResult(1, 2, 3, 4, 5); + + Maybe.just(1).flatMapPublisher(new Function<Integer, Publisher<Integer>>() { + @Override + public Publisher<Integer> apply(Integer v) throws Exception { + return Flowable.error(new TestException()); + } + }) + .test().assertFailure(TestException.class); + + Maybe.just(1).flatMapObservable(new Function<Integer, Observable<Integer>>() { + @Override + public Observable<Integer> apply(Integer v) throws Exception { + return Observable.range(1, 5); + } + }) + .test().assertResult(1, 2, 3, 4, 5); + + Maybe.just(1).flatMapObservable(new Function<Integer, Observable<Integer>>() { + @Override + public Observable<Integer> apply(Integer v) throws Exception { + return Observable.error(new TestException()); + } + }) + .test().assertFailure(TestException.class); + } + + @Test + public void using() { + final AtomicInteger disposeCount = new AtomicInteger(); + + Maybe.using(Functions.justSupplier(1), new Function<Integer, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Integer v) throws Exception { + return Maybe.just(v); + } + }, new Consumer<Integer>() { + @Override + public void accept(Integer d) throws Exception { + disposeCount.set(d); + } + }) + .map(new Function<Integer, Object>() { + @Override + public String apply(Integer v) throws Exception { + return "" + disposeCount.get() + v * 10; + } + }) + .test() + .assertResult("110"); + } + + @Test + public void usingNonEager() { + final AtomicInteger disposeCount = new AtomicInteger(); + + Maybe.using(Functions.justSupplier(1), new Function<Integer, MaybeSource<Integer>>() { + @Override + public MaybeSource<Integer> apply(Integer v) throws Exception { + return Maybe.just(v); + } + }, new Consumer<Integer>() { + @Override + public void accept(Integer d) throws Exception { + disposeCount.set(d); + } + }, false) + .map(new Function<Integer, Object>() { + @Override + public String apply(Integer v) throws Exception { + return "" + disposeCount.get() + v * 10; + } + }) + .test() + .assertResult("010"); + + assertEquals(1, disposeCount.get()); + } + + Function<Object[], String> arrayToString = new Function<Object[], String>() { + @Override + public String apply(Object[] a) throws Exception { + return Arrays.toString(a); + } + }; + + @SuppressWarnings("unchecked") + @Test + public void zipArray() { + Maybe.zipArray(arrayToString, Maybe.just(1), Maybe.just(2)) + .test() + .assertResult("[1, 2]"); + + Maybe.zipArray(arrayToString, Maybe.just(1), Maybe.empty()) + .test() + .assertResult(); + + Maybe.zipArray(arrayToString, Maybe.just(1), Maybe.error(new TestException())) + .test() + .assertFailure(TestException.class); + + assertSame(Maybe.empty(), Maybe.zipArray(ArgsToString.INSTANCE)); + + Maybe.zipArray(arrayToString, Maybe.just(1)) + .test() + .assertResult("[1]"); + } + + @Test + public void zipIterable() { + Maybe.zip( + Arrays.asList(Maybe.just(1), Maybe.just(2)), + arrayToString) + .test() + .assertResult("[1, 2]"); + + Maybe.zip(Collections.<Maybe<Integer>>emptyList(), arrayToString) + .test() + .assertResult(); + + Maybe.zip(Collections.singletonList(Maybe.just(1)), arrayToString) + .test() + .assertResult("[1]"); + } + + @SuppressWarnings("unchecked") + @Test + public void zip2() { + Maybe.zip(Maybe.just(1), Maybe.just(2), ArgsToString.INSTANCE) + .test() + .assertResult("12"); + } + + @SuppressWarnings("unchecked") + @Test + public void zipWith() { + Maybe.just(1).zipWith(Maybe.just(2), ArgsToString.INSTANCE) + .test() + .assertResult("12"); + } + + @SuppressWarnings("unchecked") + @Test + public void zip3() { + Maybe.zip(Maybe.just(1), Maybe.just(2), Maybe.just(3), + ArgsToString.INSTANCE) + .test() + .assertResult("123"); + } + + @SuppressWarnings("unchecked") + @Test + public void zip4() { + Maybe.zip(Maybe.just(1), Maybe.just(2), Maybe.just(3), Maybe.just(4), + ArgsToString.INSTANCE) + .test() + .assertResult("1234"); + } + + @SuppressWarnings("unchecked") + @Test + public void zip5() { + Maybe.zip(Maybe.just(1), Maybe.just(2), Maybe.just(3), Maybe.just(4), + Maybe.just(5), + ArgsToString.INSTANCE) + .test() + .assertResult("12345"); + } + + @SuppressWarnings("unchecked") + @Test + public void zip6() { + Maybe.zip(Maybe.just(1), Maybe.just(2), Maybe.just(3), Maybe.just(4), + Maybe.just(5), Maybe.just(6), + ArgsToString.INSTANCE) + .test() + .assertResult("123456"); + } + + @SuppressWarnings("unchecked") + @Test + public void zip7() { + Maybe.zip(Maybe.just(1), Maybe.just(2), Maybe.just(3), Maybe.just(4), + Maybe.just(5), Maybe.just(6), Maybe.just(7), + ArgsToString.INSTANCE) + .test() + .assertResult("1234567"); + } + + @SuppressWarnings("unchecked") + @Test + public void zip8() { + Maybe.zip(Maybe.just(1), Maybe.just(2), Maybe.just(3), Maybe.just(4), + Maybe.just(5), Maybe.just(6), Maybe.just(7), Maybe.just(8), + ArgsToString.INSTANCE) + .test() + .assertResult("12345678"); + } + + @SuppressWarnings("unchecked") + @Test + public void zip9() { + Maybe.zip(Maybe.just(1), Maybe.just(2), Maybe.just(3), Maybe.just(4), + Maybe.just(5), Maybe.just(6), Maybe.just(7), Maybe.just(8), Maybe.just(9), + ArgsToString.INSTANCE) + .test() + .assertResult("123456789"); + } + + @Test + public void ambWith1SignalsSuccess() { + PublishProcessor<Integer> pp1 = PublishProcessor.create(); + PublishProcessor<Integer> pp2 = PublishProcessor.create(); + + TestObserver<Integer> to = pp1.singleElement().ambWith(pp2.singleElement()) + .test(); + + to.assertEmpty(); + + assertTrue(pp1.hasSubscribers()); + assertTrue(pp2.hasSubscribers()); + + pp1.onNext(1); + pp1.onComplete(); + + assertFalse(pp1.hasSubscribers()); + assertFalse(pp2.hasSubscribers()); + + to.assertResult(1); + } + + @Test + public void ambWith2SignalsSuccess() { + PublishProcessor<Integer> pp1 = PublishProcessor.create(); + PublishProcessor<Integer> pp2 = PublishProcessor.create(); + + TestObserver<Integer> to = pp1.singleElement().ambWith(pp2.singleElement()) + .test(); + + to.assertEmpty(); + + assertTrue(pp1.hasSubscribers()); + assertTrue(pp2.hasSubscribers()); + + pp2.onNext(2); + pp2.onComplete(); + + assertFalse(pp1.hasSubscribers()); + assertFalse(pp2.hasSubscribers()); + + to.assertResult(2); + } + + @Test + public void zipIterableObject() { + final List<Maybe<Integer>> maybes = Arrays.asList(Maybe.just(1), Maybe.just(4)); + Maybe.zip(maybes, new Function<Object[], Object>() { + @Override + public Object apply(final Object[] o) throws Exception { + int sum = 0; + for (Object i : o) { + sum += (Integer) i; + } + return sum; + } + }).test().assertResult(5); + } + + static long usedMemoryNow() { + MemoryMXBean memoryMXBean = ManagementFactory.getMemoryMXBean(); + + MemoryUsage heapMemoryUsage = memoryMXBean.getHeapMemoryUsage(); + + return heapMemoryUsage.getUsed(); + } + + @Test + public void onTerminateDetach() throws Exception { + System.gc(); + + Thread.sleep(150); + + long before = usedMemoryNow(); + + Maybe<Object> source = Flowable.just((Object)new Object[10000000]).singleElement(); + + long middle = usedMemoryNow(); + + MaybeObserver<Object> observer = new MaybeObserver<Object>() { + @SuppressWarnings("unused") + Disposable u; + + @Override + public void onSubscribe(Disposable d) { + this.u = d; + } + + @Override + public void onSuccess(Object value) { + + } + + @Override + public void onError(Throwable e) { + + } + + @Override + public void onComplete() { + + } + + }; + source.onTerminateDetach().subscribe(observer); + + source = null; + + System.gc(); + + Thread.sleep(250); + + long after = usedMemoryNow(); + + String log = String.format("%.2f MB -> %.2f MB -> %.2f MB%n", + before / 1024.0 / 1024.0, + middle / 1024.0 / 1024.0, + after / 1024.0 / 1024.0); + + System.out.printf(log); + + if (before * 1.3 < after) { + fail("There seems to be a memory leak: " + log); + } + + assertNotNull(observer); // hold onto the reference to prevent premature GC + } + + @Test + public void repeat() { + Maybe.just(1).repeat().take(5).test().assertResult(1, 1, 1, 1, 1); + + Maybe.just(1).repeat(5).test().assertResult(1, 1, 1, 1, 1); + + Maybe.just(1).repeatUntil(new BooleanSupplier() { + @Override + public boolean getAsBoolean() throws Exception { + return false; + } + }).take(5).test().assertResult(1, 1, 1, 1, 1); + + Maybe.just(1).repeatWhen(new Function<Flowable<Object>, Publisher<Object>>() { + @Override + public Publisher<Object> apply(Flowable<Object> v) throws Exception { + return v; + } + }).take(5).test().assertResult(1, 1, 1, 1, 1); + } + + @Test + public void retry() { + Maybe.just(1).retry().test().assertResult(1); + + Maybe.just(1).retry(5).test().assertResult(1); + + Maybe.just(1).retry(Functions.alwaysTrue()).test().assertResult(1); + + Maybe.just(1).retry(5, Functions.alwaysTrue()).test().assertResult(1); + + Maybe.just(1).retry(new BiPredicate<Integer, Throwable>() { + @Override + public boolean test(Integer a, Throwable e) throws Exception { + return true; + } + }).test().assertResult(1); + + Maybe.just(1).retryUntil(new BooleanSupplier() { + @Override + public boolean getAsBoolean() throws Exception { + return false; + } + }).test().assertResult(1); + + Maybe.just(1).retryWhen(new Function<Flowable<? extends Throwable>, Publisher<Object>>() { + @SuppressWarnings({ "rawtypes", "unchecked" }) + @Override + public Publisher<Object> apply(Flowable<? extends Throwable> v) throws Exception { + return (Publisher)v; + } + }).test().assertResult(1); + + final AtomicInteger calls = new AtomicInteger(); + try { + Maybe.error(new Supplier<Throwable>() { + @Override + public Throwable get() { + calls.incrementAndGet(); + return new TestException(); + } + }).retry(5).test(); + } finally { + assertEquals(6, calls.get()); + } + } + + @Test + public void onErrorResumeWithEmpty() { + Maybe.empty() + .onErrorResumeWith(Maybe.just(1)) + .test() + .assertNoValues() + .assertNoErrors() + .assertComplete(); + } + + @Test + public void onErrorResumeWithValue() { + Maybe.just(1) + .onErrorResumeWith(Maybe.<Integer>empty()) + .test() + .assertNoErrors() + .assertValue(1); + } + + @Test + public void onErrorResumeWithError() { + Maybe.error(new RuntimeException("some error")) + .onErrorResumeWith(Maybe.empty()) + .test() + .assertNoValues() + .assertNoErrors() + .assertComplete(); + } + + @Test + public void valueConcatWithValue() { + Maybe.just(1) + .concatWith(Maybe.just(2)) + .test() + .assertNoErrors() + .assertComplete() + .assertValues(1, 2); + } + + @Test + public void errorConcatWithValue() { + Maybe.<Integer>error(new RuntimeException("error")) + .concatWith(Maybe.just(2)) + .to(TestHelper.<Integer>testConsumer()) + .assertError(RuntimeException.class) + .assertErrorMessage("error") + .assertNoValues(); + } + + @Test + public void valueConcatWithError() { + Maybe.just(1) + .concatWith(Maybe.<Integer>error(new RuntimeException("error"))) + .to(TestHelper.<Integer>testConsumer()) + .assertValue(1) + .assertError(RuntimeException.class) + .assertErrorMessage("error"); + } + + @Test + public void emptyConcatWithValue() { + Maybe.<Integer>empty() + .concatWith(Maybe.just(2)) + .test() + .assertNoErrors() + .assertComplete() + .assertValues(2); + } + + @Test + public void emptyConcatWithError() { + Maybe.<Integer>empty() + .concatWith(Maybe.<Integer>error(new RuntimeException("error"))) + .to(TestHelper.<Integer>testConsumer()) + .assertNoValues() + .assertError(RuntimeException.class) + .assertErrorMessage("error"); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/maybe/MaybeTimerTest.java b/src/test/java/io/reactivex/rxjava3/maybe/MaybeTimerTest.java new file mode 100644 index 0000000000..286639042d --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/maybe/MaybeTimerTest.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.maybe; + +import static org.junit.Assert.assertEquals; + +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.functions.Consumer; +import io.reactivex.rxjava3.schedulers.TestScheduler; + +public class MaybeTimerTest extends RxJavaTest { + @Test + public void timer() { + final TestScheduler testScheduler = new TestScheduler(); + + final AtomicLong atomicLong = new AtomicLong(); + Maybe.timer(2, TimeUnit.SECONDS, testScheduler).subscribe(new Consumer<Long>() { + @Override + public void accept(final Long value) throws Exception { + atomicLong.incrementAndGet(); + } + }); + + assertEquals(0, atomicLong.get()); + + testScheduler.advanceTimeBy(1, TimeUnit.SECONDS); + + assertEquals(0, atomicLong.get()); + + testScheduler.advanceTimeBy(1, TimeUnit.SECONDS); + + assertEquals(1, atomicLong.get()); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/observable/ObservableCombineLatestTests.java b/src/test/java/io/reactivex/rxjava3/observable/ObservableCombineLatestTests.java new file mode 100644 index 0000000000..957268ecf1 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/observable/ObservableCombineLatestTests.java @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.observable; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.observable.ObservableCovarianceTest.*; + +public class ObservableCombineLatestTests extends RxJavaTest { + /** + * This won't compile if super/extends isn't done correctly on generics. + */ + @Test + public void covarianceOfCombineLatest() { + Observable<HorrorMovie> horrors = Observable.just(new HorrorMovie()); + Observable<CoolRating> ratings = Observable.just(new CoolRating()); + + Observable.<Movie, CoolRating, Result> combineLatest(horrors, ratings, combine).blockingForEach(action); + Observable.<Movie, CoolRating, Result> combineLatest(horrors, ratings, combine).blockingForEach(action); + Observable.<Media, Rating, ExtendedResult> combineLatest(horrors, ratings, combine).blockingForEach(extendedAction); + Observable.<Media, Rating, Result> combineLatest(horrors, ratings, combine).blockingForEach(action); + Observable.<Media, Rating, ExtendedResult> combineLatest(horrors, ratings, combine).blockingForEach(action); + + Observable.<Movie, CoolRating, Result> combineLatest(horrors, ratings, combine); + } + + BiFunction<Media, Rating, ExtendedResult> combine = new BiFunction<Media, Rating, ExtendedResult>() { + @Override + public ExtendedResult apply(Media m, Rating r) { + return new ExtendedResult(); + } + }; + + Consumer<Result> action = new Consumer<Result>() { + @Override + public void accept(Result t1) { + System.out.println("Result: " + t1); + } + }; + + Consumer<ExtendedResult> extendedAction = new Consumer<ExtendedResult>() { + @Override + public void accept(ExtendedResult t1) { + System.out.println("Result: " + t1); + } + }; +} diff --git a/src/test/java/io/reactivex/rxjava3/observable/ObservableConcatTests.java b/src/test/java/io/reactivex/rxjava3/observable/ObservableConcatTests.java new file mode 100644 index 0000000000..53c4f99029 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/observable/ObservableConcatTests.java @@ -0,0 +1,168 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.observable; + +import static org.junit.Assert.assertEquals; + +import java.util.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.core.Observer; +import io.reactivex.rxjava3.observable.ObservableCovarianceTest.*; + +public class ObservableConcatTests extends RxJavaTest { + + @Test + public void concatSimple() { + Observable<String> o1 = Observable.just("one", "two"); + Observable<String> o2 = Observable.just("three", "four"); + + List<String> values = Observable.concat(o1, o2).toList().blockingGet(); + + assertEquals("one", values.get(0)); + assertEquals("two", values.get(1)); + assertEquals("three", values.get(2)); + assertEquals("four", values.get(3)); + } + + @Test + public void concatWithObservableOfObservable() { + Observable<String> o1 = Observable.just("one", "two"); + Observable<String> o2 = Observable.just("three", "four"); + Observable<String> o3 = Observable.just("five", "six"); + + Observable<Observable<String>> os = Observable.just(o1, o2, o3); + + List<String> values = Observable.concat(os).toList().blockingGet(); + + assertEquals("one", values.get(0)); + assertEquals("two", values.get(1)); + assertEquals("three", values.get(2)); + assertEquals("four", values.get(3)); + assertEquals("five", values.get(4)); + assertEquals("six", values.get(5)); + } + + @Test + public void concatWithIterableOfObservable() { + Observable<String> o1 = Observable.just("one", "two"); + Observable<String> o2 = Observable.just("three", "four"); + Observable<String> o3 = Observable.just("five", "six"); + + Iterable<Observable<String>> is = Arrays.asList(o1, o2, o3); + + List<String> values = Observable.concat(Observable.fromIterable(is)).toList().blockingGet(); + + assertEquals("one", values.get(0)); + assertEquals("two", values.get(1)); + assertEquals("three", values.get(2)); + assertEquals("four", values.get(3)); + assertEquals("five", values.get(4)); + assertEquals("six", values.get(5)); + } + + @Test + public void concatCovariance() { + HorrorMovie horrorMovie1 = new HorrorMovie(); + Movie movie = new Movie(); + Media media = new Media(); + HorrorMovie horrorMovie2 = new HorrorMovie(); + + Observable<Media> o1 = Observable.<Media> just(horrorMovie1, movie); + Observable<Media> o2 = Observable.just(media, horrorMovie2); + + Observable<Observable<Media>> os = Observable.just(o1, o2); + + List<Media> values = Observable.concat(os).toList().blockingGet(); + + assertEquals(horrorMovie1, values.get(0)); + assertEquals(movie, values.get(1)); + assertEquals(media, values.get(2)); + assertEquals(horrorMovie2, values.get(3)); + assertEquals(4, values.size()); + } + + @Test + public void concatCovariance2() { + HorrorMovie horrorMovie1 = new HorrorMovie(); + Movie movie = new Movie(); + Media media1 = new Media(); + Media media2 = new Media(); + HorrorMovie horrorMovie2 = new HorrorMovie(); + + Observable<Media> o1 = Observable.just(horrorMovie1, movie, media1); + Observable<Media> o2 = Observable.just(media2, horrorMovie2); + + Observable<Observable<Media>> os = Observable.just(o1, o2); + + List<Media> values = Observable.concat(os).toList().blockingGet(); + + assertEquals(horrorMovie1, values.get(0)); + assertEquals(movie, values.get(1)); + assertEquals(media1, values.get(2)); + assertEquals(media2, values.get(3)); + assertEquals(horrorMovie2, values.get(4)); + assertEquals(5, values.size()); + } + + @Test + public void concatCovariance3() { + HorrorMovie horrorMovie1 = new HorrorMovie(); + Movie movie = new Movie(); + Media media = new Media(); + HorrorMovie horrorMovie2 = new HorrorMovie(); + + Observable<Movie> o1 = Observable.just(horrorMovie1, movie); + Observable<Media> o2 = Observable.just(media, horrorMovie2); + + List<Media> values = Observable.concat(o1, o2).toList().blockingGet(); + + assertEquals(horrorMovie1, values.get(0)); + assertEquals(movie, values.get(1)); + assertEquals(media, values.get(2)); + assertEquals(horrorMovie2, values.get(3)); + assertEquals(4, values.size()); + } + + @Test + public void concatCovariance4() { + final HorrorMovie horrorMovie1 = new HorrorMovie(); + final Movie movie = new Movie(); + Media media = new Media(); + HorrorMovie horrorMovie2 = new HorrorMovie(); + + Observable<Movie> o1 = Observable.unsafeCreate(new ObservableSource<Movie>() { + @Override + public void subscribe(Observer<? super Movie> o) { + o.onNext(horrorMovie1); + o.onNext(movie); + // o.onNext(new Media()); // correctly doesn't compile + o.onComplete(); + } + }); + + Observable<Media> o2 = Observable.just(media, horrorMovie2); + + List<Media> values = Observable.concat(o1, o2).toList().blockingGet(); + + assertEquals(horrorMovie1, values.get(0)); + assertEquals(movie, values.get(1)); + assertEquals(media, values.get(2)); + assertEquals(horrorMovie2, values.get(3)); + assertEquals(4, values.size()); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/observable/ObservableCovarianceTest.java b/src/test/java/io/reactivex/rxjava3/observable/ObservableCovarianceTest.java new file mode 100644 index 0000000000..54cf0f9908 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/observable/ObservableCovarianceTest.java @@ -0,0 +1,251 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.observable; + +import static org.junit.Assert.assertEquals; + +import java.util.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.observables.GroupedObservable; +import io.reactivex.rxjava3.testsupport.TestObserverEx; + +/** + * Test super/extends of generics. + * + * See https://github.com/Netflix/RxJava/pull/331 + */ +public class ObservableCovarianceTest extends RxJavaTest { + + /** + * This won't compile if super/extends isn't done correctly on generics. + */ + @Test + public void covarianceOfFrom() { + Observable.<Movie> just(new HorrorMovie()); + Observable.<Movie> fromIterable(new ArrayList<HorrorMovie>()); + // Observable.<HorrorMovie>from(new Movie()); // may not compile + } + + @Test + public void sortedList() { + Comparator<Media> sortFunction = new Comparator<Media>() { + @Override + public int compare(Media t1, Media t2) { + return 1; + } + }; + + // this one would work without the covariance generics + Observable<Media> o = Observable.just(new Movie(), new TVSeason(), new Album()); + o.toSortedList(sortFunction); + + // this one would NOT work without the covariance generics + Observable<Movie> o2 = Observable.just(new Movie(), new ActionMovie(), new HorrorMovie()); + o2.toSortedList(sortFunction); + } + + @Test + public void groupByCompose() { + Observable<Movie> movies = Observable.just(new HorrorMovie(), new ActionMovie(), new Movie()); + TestObserverEx<String> to = new TestObserverEx<>(); + movies + .groupBy(new Function<Movie, Object>() { + @Override + public Object apply(Movie v) { + return v.getClass(); + } + }) + .doOnNext(new Consumer<GroupedObservable<Object, Movie>>() { + @Override + public void accept(GroupedObservable<Object, Movie> g) { + System.out.println(g.getKey()); + } + }) + .flatMap(new Function<GroupedObservable<Object, Movie>, Observable<String>>() { + @Override + public Observable<String> apply(GroupedObservable<Object, Movie> g) { + return g + .doOnNext(new Consumer<Movie>() { + @Override + public void accept(Movie pv) { + System.out.println(pv); + } + }) + .compose(new ObservableTransformer<Movie, Movie>() { + @Override + public Observable<Movie> apply(Observable<Movie> m) { + return m.concatWith(Observable.just(new ActionMovie())); + } + } + ) + .map(new Function<Movie, String>() { + @Override + public String apply(Movie v) { + return v.toString(); + } + }); + } + }) + .subscribe(to); + to.assertTerminated(); + to.assertNoErrors(); + // System.out.println(ts.getOnNextEvents()); + assertEquals(6, to.values().size()); + } + + @SuppressWarnings("unused") + @Test + public void covarianceOfCompose() { + Observable<HorrorMovie> movie = Observable.just(new HorrorMovie()); + Observable<Movie> movie2 = movie.compose(new ObservableTransformer<HorrorMovie, Movie>() { + @Override + public Observable<Movie> apply(Observable<HorrorMovie> t) { + return Observable.just(new Movie()); + } + }); + } + + @SuppressWarnings("unused") + @Test + public void covarianceOfCompose2() { + Observable<Movie> movie = Observable.<Movie> just(new HorrorMovie()); + Observable<HorrorMovie> movie2 = movie.compose(new ObservableTransformer<Movie, HorrorMovie>() { + @Override + public Observable<HorrorMovie> apply(Observable<Movie> t) { + return Observable.just(new HorrorMovie()); + } + }); + } + + @SuppressWarnings("unused") + @Test + public void covarianceOfCompose3() { + Observable<Movie> movie = Observable.<Movie>just(new HorrorMovie()); + Observable<HorrorMovie> movie2 = movie.compose(new ObservableTransformer<Movie, HorrorMovie>() { + @Override + public Observable<HorrorMovie> apply(Observable<Movie> t) { + return Observable.just(new HorrorMovie()).map(new Function<HorrorMovie, HorrorMovie>() { + @Override + public HorrorMovie apply(HorrorMovie v) { + return v; + } + }); + } + } + ); + } + + @SuppressWarnings("unused") + @Test + public void covarianceOfCompose4() { + Observable<HorrorMovie> movie = Observable.just(new HorrorMovie()); + Observable<HorrorMovie> movie2 = movie.compose(new ObservableTransformer<HorrorMovie, HorrorMovie>() { + @Override + public Observable<HorrorMovie> apply(Observable<HorrorMovie> t1) { + return t1.map(new Function<HorrorMovie, HorrorMovie>() { + @Override + public HorrorMovie apply(HorrorMovie v) { + return v; + } + }); + } + }); + } + + @Test + public void composeWithDeltaLogic() { + List<Movie> list1 = Arrays.asList(new Movie(), new HorrorMovie(), new ActionMovie()); + List<Movie> list2 = Arrays.asList(new ActionMovie(), new Movie(), new HorrorMovie(), new ActionMovie()); + Observable<List<Movie>> movies = Observable.just(list1, list2); + movies.compose(deltaTransformer); + } + + static Function<List<List<Movie>>, Observable<Movie>> calculateDelta = new Function<List<List<Movie>>, Observable<Movie>>() { + @Override + public Observable<Movie> apply(List<List<Movie>> listOfLists) { + if (listOfLists.size() == 1) { + return Observable.fromIterable(listOfLists.get(0)); + } else { + // diff the two + List<Movie> newList = listOfLists.get(1); + List<Movie> oldList = new ArrayList<>(listOfLists.get(0)); + + Set<Movie> delta = new LinkedHashSet<>(); + delta.addAll(newList); + // remove all that match in old + delta.removeAll(oldList); + + // filter oldList to those that aren't in the newList + oldList.removeAll(newList); + + // for all left in the oldList we'll create DROP events + for (@SuppressWarnings("unused") Movie old : oldList) { + delta.add(new Movie()); + } + + return Observable.fromIterable(delta); + } + } + }; + + static ObservableTransformer<List<Movie>, Movie> deltaTransformer = new ObservableTransformer<List<Movie>, Movie>() { + @Override + public Observable<Movie> apply(Observable<List<Movie>> movieList) { + return movieList + .startWithItem(new ArrayList<>()) + .buffer(2, 1) + .skip(1) + .flatMap(calculateDelta); + } + }; + + /* + * Most tests are moved into their applicable classes such as [Operator]Tests.java + */ + + static class Media { + } + + static class Movie extends Media { + } + + static class HorrorMovie extends Movie { + } + + static class ActionMovie extends Movie { + } + + static class Album extends Media { + } + + static class TVSeason extends Media { + } + + static class Rating { + } + + static class CoolRating extends Rating { + } + + static class Result { + } + + static class ExtendedResult extends Result { + } +} diff --git a/src/test/java/io/reactivex/rxjava3/observable/ObservableDoOnTest.java b/src/test/java/io/reactivex/rxjava3/observable/ObservableDoOnTest.java new file mode 100644 index 0000000000..0e7ed9ef88 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/observable/ObservableDoOnTest.java @@ -0,0 +1,105 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.observable; + +import static org.junit.Assert.*; + +import java.util.concurrent.atomic.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.*; + +public class ObservableDoOnTest extends RxJavaTest { + + @Test + public void doOnEach() { + final AtomicReference<String> r = new AtomicReference<>(); + String output = Observable.just("one").doOnNext(new Consumer<String>() { + @Override + public void accept(String v) { + r.set(v); + } + }).blockingSingle(); + + assertEquals("one", output); + assertEquals("one", r.get()); + } + + @Test + public void doOnError() { + final AtomicReference<Throwable> r = new AtomicReference<>(); + Throwable t = null; + try { + Observable.<String> error(new RuntimeException("an error")) + .doOnError(new Consumer<Throwable>() { + @Override + public void accept(Throwable v) { + r.set(v); + } + }).blockingSingle(); + fail("expected exception, not a return value"); + } catch (Throwable e) { + t = e; + } + + assertNotNull(t); + assertEquals(t, r.get()); + } + + @Test + public void doOnCompleted() { + final AtomicBoolean r = new AtomicBoolean(); + String output = Observable.just("one").doOnComplete(new Action() { + @Override + public void run() { + r.set(true); + } + }).blockingSingle(); + + assertEquals("one", output); + assertTrue(r.get()); + } + + @Test + public void doOnTerminateComplete() { + final AtomicBoolean r = new AtomicBoolean(); + String output = Observable.just("one").doOnTerminate(new Action() { + @Override + public void run() { + r.set(true); + } + }).blockingSingle(); + + assertEquals("one", output); + assertTrue(r.get()); + + } + + @Test + public void doOnTerminateError() { + final AtomicBoolean r = new AtomicBoolean(); + Observable.<String>error(new TestException()).doOnTerminate(new Action() { + @Override + public void run() { + r.set(true); + } + }) + .test() + .assertFailure(TestException.class); + assertTrue(r.get()); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/observable/ObservableErrorHandlingTests.java b/src/test/java/io/reactivex/rxjava3/observable/ObservableErrorHandlingTests.java new file mode 100644 index 0000000000..9ec13895b0 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/observable/ObservableErrorHandlingTests.java @@ -0,0 +1,100 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.observable; + +import static org.junit.Assert.assertNotNull; + +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicReference; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.observers.DefaultObserver; +import io.reactivex.rxjava3.schedulers.Schedulers; + +public class ObservableErrorHandlingTests extends RxJavaTest { + + /** + * Test that an error from a user provided Observer.onNext is handled and emitted to the onError. + * @throws InterruptedException if the test is interrupted + */ + @Test + public void onNextError() throws InterruptedException { + final CountDownLatch latch = new CountDownLatch(1); + final AtomicReference<Throwable> caughtError = new AtomicReference<>(); + Observable<Long> o = Observable.interval(50, TimeUnit.MILLISECONDS); + Observer<Long> observer = new DefaultObserver<Long>() { + + @Override + public void onComplete() { + System.out.println("completed"); + latch.countDown(); + } + + @Override + public void onError(Throwable e) { + System.out.println("error: " + e); + caughtError.set(e); + latch.countDown(); + } + + @Override + public void onNext(Long args) { + throw new RuntimeException("forced failure"); + } + }; + o.safeSubscribe(observer); + + latch.await(2000, TimeUnit.MILLISECONDS); + assertNotNull(caughtError.get()); + } + + /** + * Test that an error from a user provided Observer.onNext is handled and emitted to the onError + * even when done across thread boundaries with observeOn. + * @throws InterruptedException if the test is interrupted + */ + @Test + public void onNextErrorAcrossThread() throws InterruptedException { + final CountDownLatch latch = new CountDownLatch(1); + final AtomicReference<Throwable> caughtError = new AtomicReference<>(); + Observable<Long> o = Observable.interval(50, TimeUnit.MILLISECONDS); + Observer<Long> observer = new DefaultObserver<Long>() { + + @Override + public void onComplete() { + System.out.println("completed"); + latch.countDown(); + } + + @Override + public void onError(Throwable e) { + System.out.println("error: " + e); + caughtError.set(e); + latch.countDown(); + } + + @Override + public void onNext(Long args) { + throw new RuntimeException("forced failure"); + } + }; + o.observeOn(Schedulers.newThread()) + .safeSubscribe(observer); + + latch.await(2000, TimeUnit.MILLISECONDS); + assertNotNull(caughtError.get()); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/observable/ObservableEventStream.java b/src/test/java/io/reactivex/rxjava3/observable/ObservableEventStream.java new file mode 100644 index 0000000000..48e637f5c3 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/observable/ObservableEventStream.java @@ -0,0 +1,92 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.observable; + +import java.util.*; + +import io.reactivex.rxjava3.core.Emitter; +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.functions.Consumer; +import io.reactivex.rxjava3.schedulers.Schedulers; + +/** + * Utility for retrieving a mock eventstream for testing. + */ +public final class ObservableEventStream { + private ObservableEventStream() { + throw new IllegalStateException("No instances!"); + } + public static Observable<Event> getEventStream(final String type, final int numInstances) { + + return Observable.<Event>generate(new EventConsumer(numInstances, type)).subscribeOn(Schedulers.newThread()); + } + + public static Event randomEvent(String type, int numInstances) { + Map<String, Object> values = new LinkedHashMap<>(); + values.put("count200", randomIntFrom0to(4000)); + values.put("count4xx", randomIntFrom0to(300)); + values.put("count5xx", randomIntFrom0to(500)); + return new Event(type, "instance_" + randomIntFrom0to(numInstances), values); + } + + private static int randomIntFrom0to(int max) { + // XORShift instead of Math.random http://javamex.com/tutorials/random_numbers/xorshift.shtml + long x = System.nanoTime(); + x ^= (x << 21); + x ^= (x >>> 35); + x ^= (x << 4); + return Math.abs((int) x % max); + } + + static final class EventConsumer implements Consumer<Emitter<Event>> { + private final int numInstances; + private final String type; + + EventConsumer(int numInstances, String type) { + this.numInstances = numInstances; + this.type = type; + } + + @Override + public void accept(Emitter<Event> s) { + s.onNext(randomEvent(type, numInstances)); + try { + // slow it down somewhat + Thread.sleep(50); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + s.onError(e); + } + } + } + + public static class Event { + public final String type; + public final String instanceId; + public final Map<String, Object> values; + + /** + * Construct an event with the provided parameters. + * @param type the event type + * @param instanceId the instance identifier + * @param values + * This does NOT deep-copy, so do not mutate this Map after passing it in. + */ + public Event(String type, String instanceId, Map<String, Object> values) { + this.type = type; + this.instanceId = instanceId; + this.values = Collections.unmodifiableMap(values); + } + } +} diff --git a/src/test/java/io/reactivex/rxjava3/observable/ObservableFuseableTest.java b/src/test/java/io/reactivex/rxjava3/observable/ObservableFuseableTest.java new file mode 100644 index 0000000000..8de9b9bc0b --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/observable/ObservableFuseableTest.java @@ -0,0 +1,94 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.observable; + +import java.util.Arrays; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.operators.QueueFuseable; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class ObservableFuseableTest extends RxJavaTest { + + @Test + public void syncRange() { + + Observable.range(1, 10) + .to(TestHelper.<Integer>testConsumer(QueueFuseable.ANY, false)) + .assertFusionMode(QueueFuseable.SYNC) + .assertValues(1, 2, 3, 4, 5, 6, 7, 8, 9, 10) + .assertNoErrors() + .assertComplete(); + } + + @Test + public void syncArray() { + + Observable.fromArray(new Integer[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }) + .to(TestHelper.<Integer>testConsumer(QueueFuseable.ANY, false)) + .assertFusionMode(QueueFuseable.SYNC) + .assertValues(1, 2, 3, 4, 5, 6, 7, 8, 9, 10) + .assertNoErrors() + .assertComplete(); + } + + @Test + public void syncIterable() { + + Observable.fromIterable(Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)) + .to(TestHelper.<Integer>testConsumer(QueueFuseable.ANY, false)) + .assertFusionMode(QueueFuseable.SYNC) + .assertValues(1, 2, 3, 4, 5, 6, 7, 8, 9, 10) + .assertNoErrors() + .assertComplete(); + } + + @Test + public void syncRangeHidden() { + + Observable.range(1, 10).hide() + .to(TestHelper.<Integer>testConsumer(QueueFuseable.ANY, false)) + .assertNotFuseable() + .assertFusionMode(QueueFuseable.NONE) + .assertValues(1, 2, 3, 4, 5, 6, 7, 8, 9, 10) + .assertNoErrors() + .assertComplete(); + } + + @Test + public void syncArrayHidden() { + Observable.fromArray(new Integer[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }) + .hide() + .to(TestHelper.<Integer>testConsumer(QueueFuseable.ANY, false)) + .assertNotFuseable() + .assertFusionMode(QueueFuseable.NONE) + .assertValues(1, 2, 3, 4, 5, 6, 7, 8, 9, 10) + .assertNoErrors() + .assertComplete(); + } + + @Test + public void syncIterableHidden() { + Observable.fromIterable(Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)) + .hide() + .to(TestHelper.<Integer>testConsumer(QueueFuseable.ANY, false)) + .assertNotFuseable() + .assertFusionMode(QueueFuseable.NONE) + .assertValues(1, 2, 3, 4, 5, 6, 7, 8, 9, 10) + .assertNoErrors() + .assertComplete(); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/observable/ObservableGroupByTests.java b/src/test/java/io/reactivex/rxjava3/observable/ObservableGroupByTests.java new file mode 100644 index 0000000000..1db6ebb908 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/observable/ObservableGroupByTests.java @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.observable; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.observable.ObservableEventStream.Event; +import io.reactivex.rxjava3.observables.GroupedObservable; + +public class ObservableGroupByTests extends RxJavaTest { + + @Test + public void takeUnsubscribesOnGroupBy() throws Exception { + Observable.merge( + ObservableEventStream.getEventStream("HTTP-ClusterA", 50), + ObservableEventStream.getEventStream("HTTP-ClusterB", 20) + ) + // group by type (2 clusters) + .groupBy(new Function<Event, String>() { + @Override + public String apply(Event event) { + return event.type; + } + }) + .take(1) + .blockingForEach(new Consumer<GroupedObservable<String, Event>>() { + @Override + public void accept(GroupedObservable<String, Event> v) { + System.out.println(v); + v.take(1).subscribe(); // FIXME groups need consumption to a certain degree to cancel upstream + } + }); + + System.out.println("**** finished"); + + Thread.sleep(200); // make sure the event streams receive their interrupt + } + + @Test + public void takeUnsubscribesOnFlatMapOfGroupBy() throws Exception { + Observable.merge( + ObservableEventStream.getEventStream("HTTP-ClusterA", 50), + ObservableEventStream.getEventStream("HTTP-ClusterB", 20) + ) + // group by type (2 clusters) + .groupBy(new Function<Event, String>() { + @Override + public String apply(Event event) { + return event.type; + } + }) + .flatMap(new Function<GroupedObservable<String, Event>, Observable<Object>>() { + @Override + public Observable<Object> apply(GroupedObservable<String, Event> g) { + return g.map(new Function<Event, Object>() { + @Override + public Object apply(Event event) { + return event.instanceId + " - " + event.values.get("count200"); + } + }); + } + }) + .take(20) + .blockingForEach(new Consumer<Object>() { + @Override + public void accept(Object pv) { + System.out.println(pv); + } + }); + + System.out.println("**** finished"); + + Thread.sleep(200); // make sure the event streams receive their interrupt + } +} diff --git a/src/test/java/io/reactivex/rxjava3/observable/ObservableMergeTests.java b/src/test/java/io/reactivex/rxjava3/observable/ObservableMergeTests.java new file mode 100644 index 0000000000..25a3317b7f --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/observable/ObservableMergeTests.java @@ -0,0 +1,98 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.observable; + +import static org.junit.Assert.*; + +import java.util.List; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.functions.Supplier; +import io.reactivex.rxjava3.observable.ObservableCovarianceTest.*; + +public class ObservableMergeTests extends RxJavaTest { + + /** + * This won't compile if super/extends isn't done correctly on generics. + */ + @Test + public void covarianceOfMerge() { + Observable<HorrorMovie> horrors = Observable.just(new HorrorMovie()); + Observable<Observable<HorrorMovie>> metaHorrors = Observable.just(horrors); + Observable.<Media> merge(metaHorrors); + } + + @Test + public void mergeCovariance() { + Observable<Media> o1 = Observable.<Media> just(new HorrorMovie(), new Movie()); + Observable<Media> o2 = Observable.just(new Media(), new HorrorMovie()); + + Observable<Observable<Media>> os = Observable.just(o1, o2); + + List<Media> values = Observable.merge(os).toList().blockingGet(); + + assertEquals(4, values.size()); + } + + @Test + public void mergeCovariance2() { + Observable<Media> o1 = Observable.just(new HorrorMovie(), new Movie(), new Media()); + Observable<Media> o2 = Observable.just(new Media(), new HorrorMovie()); + + Observable<Observable<Media>> os = Observable.just(o1, o2); + + List<Media> values = Observable.merge(os).toList().blockingGet(); + + assertEquals(5, values.size()); + } + + @Test + public void mergeCovariance3() { + Observable<Movie> o1 = Observable.just(new HorrorMovie(), new Movie()); + Observable<Media> o2 = Observable.just(new Media(), new HorrorMovie()); + + List<Media> values = Observable.merge(o1, o2).toList().blockingGet(); + + assertTrue(values.get(0) instanceof HorrorMovie); + assertTrue(values.get(1) instanceof Movie); + assertNotNull(values.get(2)); + assertTrue(values.get(3) instanceof HorrorMovie); + } + + @Test + public void mergeCovariance4() { + + Observable<Movie> o1 = Observable.defer(new Supplier<Observable<Movie>>() { + @Override + public Observable<Movie> get() { + return Observable.just( + new HorrorMovie(), + new Movie() + ); + } + }); + + Observable<Media> o2 = Observable.just(new Media(), new HorrorMovie()); + + List<Media> values = Observable.merge(o1, o2).toList().blockingGet(); + + assertTrue(values.get(0) instanceof HorrorMovie); + assertTrue(values.get(1) instanceof Movie); + assertNotNull(values.get(2)); + assertTrue(values.get(3) instanceof HorrorMovie); + } + +} diff --git a/src/test/java/io/reactivex/rxjava3/observable/ObservableNullTests.java b/src/test/java/io/reactivex/rxjava3/observable/ObservableNullTests.java new file mode 100644 index 0000000000..178adb6230 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/observable/ObservableNullTests.java @@ -0,0 +1,1108 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.observable; + +import java.lang.reflect.*; +import java.util.*; +import java.util.concurrent.*; + +import org.junit.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.core.Observer; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.schedulers.Schedulers; + +/** + * Verifies the operators handle null values properly by emitting/throwing NullPointerExceptions. + */ +public class ObservableNullTests extends RxJavaTest { + + Observable<Integer> just1 = Observable.just(1); + + //*********************************************************** + // Static methods + //*********************************************************** + + @Test + public void ambIterableIteratorNull() { + Observable.amb(new Iterable<Observable<Object>>() { + @Override + public Iterator<Observable<Object>> iterator() { + return null; + } + }).test().assertError(NullPointerException.class); + } + + @Test + public void ambIterableOneIsNull() { + Observable.amb(Arrays.asList(Observable.never(), null)) + .test() + .assertError(NullPointerException.class); + } + + @Test(expected = NullPointerException.class) + public void combineLatestIterableIteratorNull() { + Observable.combineLatest(new Iterable<Observable<Object>>() { + @Override + public Iterator<Observable<Object>> iterator() { + return null; + } + }, new Function<Object[], Object>() { + @Override + public Object apply(Object[] v) { + return 1; + } + }, 128).blockingLast(); + } + + @Test(expected = NullPointerException.class) + public void combineLatestIterableOneIsNull() { + Observable.combineLatest(Arrays.asList(Observable.never(), null), new Function<Object[], Object>() { + @Override + public Object apply(Object[] v) { + return 1; + } + }, 128).blockingLast(); + } + + @Test(expected = NullPointerException.class) + public void combineLatestIterableFunctionReturnsNull() { + Observable.combineLatest(Arrays.asList(just1), new Function<Object[], Object>() { + @Override + public Object apply(Object[] v) { + return null; + } + }, 128).blockingLast(); + } + + @Test(expected = NullPointerException.class) + public void combineLatestDelayErrorIterableIteratorNull() { + Observable.combineLatestDelayError(new Iterable<Observable<Object>>() { + @Override + public Iterator<Observable<Object>> iterator() { + return null; + } + }, new Function<Object[], Object>() { + @Override + public Object apply(Object[] v) { + return 1; + } + }, 128).blockingLast(); + } + + @Test(expected = NullPointerException.class) + public void combineLatestDelayErrorIterableOneIsNull() { + Observable.combineLatestDelayError(Arrays.asList(Observable.never(), null), new Function<Object[], Object>() { + @Override + public Object apply(Object[] v) { + return 1; + } + }, 128).blockingLast(); + } + + @Test(expected = NullPointerException.class) + public void combineLatestDelayErrorIterableFunctionReturnsNull() { + Observable.combineLatestDelayError(Arrays.asList(just1), new Function<Object[], Object>() { + @Override + public Object apply(Object[] v) { + return null; + } + }, 128).blockingLast(); + } + + @Test(expected = NullPointerException.class) + public void concatIterableIteratorNull() { + Observable.concat(new Iterable<Observable<Object>>() { + @Override + public Iterator<Observable<Object>> iterator() { + return null; + } + }).blockingLast(); + } + + @Test(expected = NullPointerException.class) + public void concatIterableOneIsNull() { + Observable.concat(Arrays.asList(just1, null)).blockingLast(); + } + + @Test(expected = NullPointerException.class) + public void concatArrayOneIsNull() { + Observable.concatArray(just1, null).blockingLast(); + } + + @Test(expected = NullPointerException.class) + public void deferFunctionReturnsNull() { + Observable.defer(new Supplier<Observable<Object>>() { + @Override + public Observable<Object> get() { + return null; + } + }).blockingLast(); + } + + @Test(expected = NullPointerException.class) + public void errorFunctionReturnsNull() { + Observable.error(new Supplier<Throwable>() { + @Override + public Throwable get() { + return null; + } + }).blockingSubscribe(); + } + + @Test(expected = NullPointerException.class) + public void fromArrayOneIsNull() { + Observable.fromArray(1, null).blockingLast(); + } + + @Test(expected = NullPointerException.class) + public void fromCallableReturnsNull() { + Observable.fromCallable(new Callable<Object>() { + @Override + public Object call() throws Exception { + return null; + } + }).blockingLast(); + } + + @Test + public void fromFutureReturnsNull() { + FutureTask<Object> f = new FutureTask<>(Functions.EMPTY_RUNNABLE, null); + f.run(); + + TestObserver<Object> to = new TestObserver<>(); + Observable.fromFuture(f).subscribe(to); + to.assertNoValues(); + to.assertNotComplete(); + to.assertError(NullPointerException.class); + } + + @Test(expected = NullPointerException.class) + public void fromFutureTimedReturnsNull() { + FutureTask<Object> f = new FutureTask<>(Functions.EMPTY_RUNNABLE, null); + f.run(); + Observable.fromFuture(f, 1, TimeUnit.SECONDS).blockingLast(); + } + + @Test(expected = NullPointerException.class) + public void fromIterableIteratorNull() { + Observable.fromIterable(new Iterable<Object>() { + @Override + public Iterator<Object> iterator() { + return null; + } + }).blockingLast(); + } + + @Test(expected = NullPointerException.class) + public void fromIterableValueNull() { + Observable.fromIterable(Arrays.asList(1, null)).blockingLast(); + } + + @Test(expected = NullPointerException.class) + public void generateConsumerEmitsNull() { + Observable.generate(new Consumer<Emitter<Object>>() { + @Override + public void accept(Emitter<Object> s) { + s.onNext(null); + } + }).blockingLast(); + } + + @Test + public void generateConsumerStateNullAllowed() { + BiConsumer<Integer, Emitter<Integer>> generator = new BiConsumer<Integer, Emitter<Integer>>() { + @Override + public void accept(Integer s, Emitter<Integer> o) { + o.onComplete(); + } + }; + Observable.generate(new Supplier<Integer>() { + @Override + public Integer get() { + return null; + } + }, generator).blockingSubscribe(); + } + + @Test + public void generateFunctionStateNullAllowed() { + Observable.generate(new Supplier<Object>() { + @Override + public Object get() { + return null; + } + }, new BiFunction<Object, Emitter<Object>, Object>() { + @Override + public Object apply(Object s, Emitter<Object> o) { o.onComplete(); return s; } + }).blockingSubscribe(); + } + + public void intervalSchedulerNull() { + Observable.interval(1, TimeUnit.SECONDS, null); + } + + @Test + public void justNull() throws Exception { + @SuppressWarnings("rawtypes") + Class<Observable> clazz = Observable.class; + for (int argCount = 1; argCount < 10; argCount++) { + for (int argNull = 1; argNull <= argCount; argNull++) { + Class<?>[] params = new Class[argCount]; + Arrays.fill(params, Object.class); + + Object[] values = new Object[argCount]; + Arrays.fill(values, 1); + values[argNull - 1] = null; + + Method m = clazz.getMethod("just", params); + + try { + m.invoke(null, values); + Assert.fail("No exception for argCount " + argCount + " / argNull " + argNull); + } catch (InvocationTargetException ex) { + if (!(ex.getCause() instanceof NullPointerException)) { + Assert.fail("Unexpected exception for argCount " + argCount + " / argNull " + argNull + ": " + ex); + } + } + } + } + } + + @Test(expected = NullPointerException.class) + public void mergeIterableIteratorNull() { + Observable.merge(new Iterable<Observable<Object>>() { + @Override + public Iterator<Observable<Object>> iterator() { + return null; + } + }, 128, 128).blockingLast(); + } + + @Test(expected = NullPointerException.class) + public void mergeIterableOneIsNull() { + Observable.merge(Arrays.asList(just1, null), 128, 128).blockingLast(); + } + + @Test(expected = NullPointerException.class) + public void mergeDelayErrorIterableIteratorNull() { + Observable.mergeDelayError(new Iterable<Observable<Object>>() { + @Override + public Iterator<Observable<Object>> iterator() { + return null; + } + }, 128, 128).blockingLast(); + } + + @Test(expected = NullPointerException.class) + public void mergeDelayErrorIterableOneIsNull() { + Observable.mergeDelayError(Arrays.asList(just1, null), 128, 128).blockingLast(); + } + + @Test(expected = NullPointerException.class) + public void usingObservableSupplierReturnsNull() { + Observable.using(new Supplier<Object>() { + @Override + public Object get() { + return 1; + } + }, new Function<Object, Observable<Object>>() { + @Override + public Observable<Object> apply(Object d) { + return null; + } + }, new Consumer<Object>() { + @Override + public void accept(Object d) { } + }).blockingLast(); + } + + @Test(expected = NullPointerException.class) + public void zipIterableIteratorNull() { + Observable.zip(new Iterable<Observable<Object>>() { + @Override + public Iterator<Observable<Object>> iterator() { + return null; + } + }, new Function<Object[], Object>() { + @Override + public Object apply(Object[] v) { + return 1; + } + }).blockingLast(); + } + + @Test(expected = NullPointerException.class) + public void zipIterableFunctionReturnsNull() { + Observable.zip(Arrays.asList(just1, just1), new Function<Object[], Object>() { + @Override + public Object apply(Object[] a) { + return null; + } + }).blockingLast(); + } + + @Test(expected = NullPointerException.class) + public void zipIterable2IteratorNull() { + Observable.zip(new Iterable<Observable<Object>>() { + @Override + public Iterator<Observable<Object>> iterator() { + return null; + } + }, new Function<Object[], Object>() { + @Override + public Object apply(Object[] a) { + return 1; + } + }, true, 128).blockingLast(); + } + + @Test(expected = NullPointerException.class) + public void zipIterable2FunctionReturnsNull() { + Observable.zip(Arrays.asList(just1, just1), new Function<Object[], Object>() { + @Override + public Object apply(Object[] a) { + return null; + } + }, true, 128).blockingLast(); + } + + //************************************************************* + // Instance methods + //************************************************************* + + @Test(expected = NullPointerException.class) + public void bufferSupplierReturnsNull() { + just1.buffer(1, 1, new Supplier<Collection<Integer>>() { + @Override + public Collection<Integer> get() { + return null; + } + }).blockingSubscribe(); + } + + @Test(expected = NullPointerException.class) + public void bufferTimedSupplierReturnsNull() { + just1.buffer(1L, 1L, TimeUnit.SECONDS, Schedulers.single(), new Supplier<Collection<Integer>>() { + @Override + public Collection<Integer> get() { + return null; + } + }).blockingSubscribe(); + } + + @Test(expected = NullPointerException.class) + public void bufferOpenCloseCloseReturnsNull() { + just1.buffer(just1, new Function<Integer, Observable<Object>>() { + @Override + public Observable<Object> apply(Integer v) { + return null; + } + }).blockingSubscribe(); + } + + @Test(expected = NullPointerException.class) + public void bufferBoundarySupplierReturnsNull() { + just1.buffer(just1, new Supplier<Collection<Integer>>() { + @Override + public Collection<Integer> get() { + return null; + } + }).blockingSubscribe(); + } + + @Test(expected = NullPointerException.class) + public void collectInitialSupplierReturnsNull() { + just1.collect(new Supplier<Object>() { + @Override + public Object get() { + return null; + } + }, new BiConsumer<Object, Integer>() { + @Override + public void accept(Object a, Integer b) { } + }).blockingGet(); + } + + @Test(expected = NullPointerException.class) + public void collectInitialCollectorNull() { + just1.collect(new Supplier<Object>() { + @Override + public Object get() { + return 1; + } + }, null); + } + + @Test(expected = NullPointerException.class) + public void concatMapReturnsNull() { + just1.concatMap(new Function<Integer, Observable<Object>>() { + @Override + public Observable<Object> apply(Integer v) { + return null; + } + }).blockingSubscribe(); + } + + @Test(expected = NullPointerException.class) + public void concatMapIterableReturnNull() { + just1.concatMapIterable(new Function<Integer, Iterable<Object>>() { + @Override + public Iterable<Object> apply(Integer v) { + return null; + } + }).blockingSubscribe(); + } + + @Test(expected = NullPointerException.class) + public void concatMapIterableIteratorNull() { + just1.concatMapIterable(new Function<Integer, Iterable<Object>>() { + @Override + public Iterable<Object> apply(Integer v) { + return new Iterable<Object>() { + @Override + public Iterator<Object> iterator() { + return null; + } + }; + } + }).blockingSubscribe(); + } + + @Test(expected = NullPointerException.class) + public void debounceFunctionReturnsNull() { + just1.debounce(new Function<Integer, Observable<Object>>() { + @Override + public Observable<Object> apply(Integer v) { + return null; + } + }).blockingSubscribe(); + } + + @Test(expected = NullPointerException.class) + public void delayWithFunctionReturnsNull() { + just1.delay(new Function<Integer, Observable<Object>>() { + @Override + public Observable<Object> apply(Integer v) { + return null; + } + }).blockingSubscribe(); + } + + @Test(expected = NullPointerException.class) + public void delayBothItemSupplierReturnsNull() { + just1.delay(just1 + , new Function<Integer, Observable<Object>>() { + @Override + public Observable<Object> apply(Integer v) { + return null; + } + }).blockingSubscribe(); + } + + @Test(expected = NullPointerException.class) + public void distinctSupplierReturnsNull() { + just1.distinct(new Function<Integer, Object>() { + @Override + public Object apply(Integer v) { + return v; + } + }, new Supplier<Collection<Object>>() { + @Override + public Collection<Object> get() { + return null; + } + }).blockingSubscribe(); + } + + @Test(expected = NullPointerException.class) + public void distinctFunctionReturnsNull() { + just1.distinct(new Function<Integer, Object>() { + @Override + public Object apply(Integer v) { + return null; + } + }).blockingSubscribe(); + } + + @Test + public void distinctUntilChangedFunctionReturnsNull() { + Observable.range(1, 2).distinctUntilChanged(new Function<Integer, Object>() { + @Override + public Object apply(Integer v) { + return null; + } + }).test().assertResult(1); + } + + @Test(expected = NullPointerException.class) + public void flatMapFunctionReturnsNull() { + just1.flatMap(new Function<Integer, Observable<Object>>() { + @Override + public Observable<Object> apply(Integer v) { + return null; + } + }).blockingSubscribe(); + } + + @Test(expected = NullPointerException.class) + public void flatMapNotificationOnNextReturnsNull() { + just1.flatMap(new Function<Integer, Observable<Integer>>() { + @Override + public Observable<Integer> apply(Integer v) { + return null; + } + }, new Function<Throwable, Observable<Integer>>() { + @Override + public Observable<Integer> apply(Throwable e) { + return just1; + } + }, new Supplier<Observable<Integer>>() { + @Override + public Observable<Integer> get() { + return just1; + } + }).blockingSubscribe(); + } + + @Test(expected = NullPointerException.class) + public void flatMapNotificationOnCompleteReturnsNull() { + just1.flatMap(new Function<Integer, Observable<Integer>>() { + @Override + public Observable<Integer> apply(Integer v) { + return just1; + } + }, new Function<Throwable, Observable<Integer>>() { + @Override + public Observable<Integer> apply(Throwable e) { + return just1; + } + }, new Supplier<Observable<Integer>>() { + @Override + public Observable<Integer> get() { + return null; + } + }).blockingSubscribe(); + } + + @Test(expected = NullPointerException.class) + public void flatMapCombinerMapperReturnsNull() { + just1.flatMap(new Function<Integer, Observable<Object>>() { + @Override + public Observable<Object> apply(Integer v) { + return null; + } + }, new BiFunction<Integer, Object, Object>() { + @Override + public Object apply(Integer a, Object b) { + return 1; + } + }).blockingSubscribe(); + } + + @Test(expected = NullPointerException.class) + public void flatMapCombinerCombinerReturnsNull() { + just1.flatMap(new Function<Integer, Observable<Integer>>() { + @Override + public Observable<Integer> apply(Integer v) { + return just1; + } + }, new BiFunction<Integer, Integer, Object>() { + @Override + public Object apply(Integer a, Integer b) { + return null; + } + }).blockingSubscribe(); + } + + @Test(expected = NullPointerException.class) + public void flatMapIterableMapperReturnsNull() { + just1.flatMapIterable(new Function<Integer, Iterable<Integer>>() { + @Override + public Iterable<Integer> apply(Integer v) { + return null; + } + }).blockingSubscribe(); + } + + @Test(expected = NullPointerException.class) + public void flatMapIterableMapperIteratorNull() { + just1.flatMapIterable(new Function<Integer, Iterable<Object>>() { + @Override + public Iterable<Object> apply(Integer v) { + return new Iterable<Object>() { + @Override + public Iterator<Object> iterator() { + return null; + } + }; + } + }).blockingSubscribe(); + } + + @Test(expected = NullPointerException.class) + public void flatMapIterableMapperIterableOneNull() { + just1.flatMapIterable(new Function<Integer, Iterable<Integer>>() { + @Override + public Iterable<Integer> apply(Integer v) { + return Arrays.asList(1, null); + } + }).blockingSubscribe(); + } + + @Test(expected = NullPointerException.class) + public void flatMapIterableCombinerReturnsNull() { + just1.flatMapIterable(new Function<Integer, Iterable<Integer>>() { + @Override + public Iterable<Integer> apply(Integer v) { + return Arrays.asList(1); + } + }, new BiFunction<Integer, Integer, Object>() { + @Override + public Object apply(Integer a, Integer b) { + return null; + } + }).blockingSubscribe(); + } + + public void groupByKeyNull() { + just1.groupBy(new Function<Integer, Object>() { + @Override + public Object apply(Integer v) { + return null; + } + }).blockingSubscribe(); + } + + @Test(expected = NullPointerException.class) + public void groupByValueReturnsNull() { + just1.groupBy(new Function<Integer, Object>() { + @Override + public Object apply(Integer v) { + return v; + } + }, new Function<Integer, Object>() { + @Override + public Object apply(Integer v) { + return null; + } + }).blockingSubscribe(); + } + + @Test(expected = NullPointerException.class) + public void liftReturnsNull() { + just1.lift(new ObservableOperator<Object, Integer>() { + @Override + public Observer<? super Integer> apply(Observer<? super Object> observer) { + return null; + } + }).blockingSubscribe(); + } + + @Test(expected = NullPointerException.class) + public void mapReturnsNull() { + just1.map(new Function<Integer, Object>() { + @Override + public Object apply(Integer v) { + return null; + } + }).blockingSubscribe(); + } + + @Test(expected = NullPointerException.class) + public void onErrorResumeNextFunctionReturnsNull() { + Observable.error(new TestException()).onErrorResumeNext(new Function<Throwable, Observable<Object>>() { + @Override + public Observable<Object> apply(Throwable e) { + return null; + } + }).blockingSubscribe(); + } + + @Test(expected = NullPointerException.class) + public void onErrorReturnFunctionReturnsNull() { + Observable.error(new TestException()).onErrorReturn(new Function<Throwable, Object>() { + @Override + public Object apply(Throwable e) { + return null; + } + }).blockingSubscribe(); + } + + @Test(expected = NullPointerException.class) + public void publishFunctionReturnsNull() { + just1.publish(new Function<Observable<Integer>, Observable<Object>>() { + @Override + public Observable<Object> apply(Observable<Integer> v) { + return null; + } + }).blockingSubscribe(); + } + + @Test(expected = NullPointerException.class) + public void reduceFunctionReturnsNull() { + Observable.just(1, 1).reduce(new BiFunction<Integer, Integer, Integer>() { + @Override + public Integer apply(Integer a, Integer b) { + return null; + } + }).blockingGet(); + } + + @Test(expected = NullPointerException.class) + public void reduceSeedFunctionReturnsNull() { + just1.reduce(1, new BiFunction<Integer, Integer, Integer>() { + @Override + public Integer apply(Integer a, Integer b) { + return null; + } + }).blockingGet(); + } + + @Test(expected = NullPointerException.class) + public void reduceWithSeedReturnsNull() { + just1.reduceWith(new Supplier<Object>() { + @Override + public Object get() { + return null; + } + }, new BiFunction<Object, Integer, Object>() { + @Override + public Object apply(Object a, Integer b) { + return 1; + } + }).blockingGet(); + } + + @Test(expected = NullPointerException.class) + public void repeatWhenFunctionReturnsNull() { + just1.repeatWhen(new Function<Observable<Object>, Observable<Object>>() { + @Override + public Observable<Object> apply(Observable<Object> v) { + return null; + } + }).blockingSubscribe(); + } + + @Test(expected = NullPointerException.class) + public void replaySelectorReturnsNull() { + just1.replay(new Function<Observable<Integer>, Observable<Object>>() { + @Override + public Observable<Object> apply(Observable<Integer> o) { + return null; + } + }).blockingSubscribe(); + } + + @Test(expected = NullPointerException.class) + public void replayBoundedSelectorReturnsNull() { + just1.replay(new Function<Observable<Integer>, Observable<Object>>() { + @Override + public Observable<Object> apply(Observable<Integer> v) { + return null; + } + }, 1, 1, TimeUnit.SECONDS).blockingSubscribe(); + } + + @Test(expected = NullPointerException.class) + public void replayTimeBoundedSelectorReturnsNull() { + just1.replay(new Function<Observable<Integer>, Observable<Object>>() { + @Override + public Observable<Object> apply(Observable<Integer> v) { + return null; + } + }, 1, TimeUnit.SECONDS, Schedulers.single()).blockingSubscribe(); + } + + @Test(expected = NullPointerException.class) + public void retryWhenFunctionReturnsNull() { + Observable.error(new TestException()).retryWhen(new Function<Observable<? extends Throwable>, Observable<Object>>() { + @Override + public Observable<Object> apply(Observable<? extends Throwable> f) { + return null; + } + }).blockingSubscribe(); + } + + @Test(expected = NullPointerException.class) + public void scanFunctionReturnsNull() { + Observable.just(1, 1).scan(new BiFunction<Integer, Integer, Integer>() { + @Override + public Integer apply(Integer a, Integer b) { + return null; + } + }).blockingSubscribe(); + } + + @Test(expected = NullPointerException.class) + public void scanSeedFunctionReturnsNull() { + just1.scan(1, new BiFunction<Integer, Integer, Integer>() { + @Override + public Integer apply(Integer a, Integer b) { + return null; + } + }).blockingSubscribe(); + } + + @Test(expected = NullPointerException.class) + public void scanSeedSupplierReturnsNull() { + just1.scanWith(new Supplier<Object>() { + @Override + public Object get() { + return null; + } + }, new BiFunction<Object, Integer, Object>() { + @Override + public Object apply(Object a, Integer b) { + return 1; + } + }).blockingSubscribe(); + } + + @Test(expected = NullPointerException.class) + public void scanSeedSupplierFunctionReturnsNull() { + just1.scanWith(new Supplier<Object>() { + @Override + public Object get() { + return 1; + } + }, new BiFunction<Object, Integer, Object>() { + @Override + public Object apply(Object a, Integer b) { + return null; + } + }).blockingSubscribe(); + } + + @Test(expected = NullPointerException.class) + public void startWithIterableIteratorNull() { + just1.startWithIterable(new Iterable<Integer>() { + @Override + public Iterator<Integer> iterator() { + return null; + } + }).blockingSubscribe(); + } + + @Test(expected = NullPointerException.class) + public void startWithIterableOneNull() { + just1.startWithIterable(Arrays.asList(1, null)).blockingSubscribe(); + } + + @Test(expected = NullPointerException.class) + public void switchMapFunctionReturnsNull() { + just1.switchMap(new Function<Integer, Observable<Object>>() { + @Override + public Observable<Object> apply(Integer v) { + return null; + } + }).blockingSubscribe(); + } + + @Test(expected = NullPointerException.class) + public void timeoutSelectorReturnsNull() { + just1.timeout(new Function<Integer, Observable<Object>>() { + @Override + public Observable<Object> apply(Integer v) { + return null; + } + }).blockingSubscribe(); + } + + @Test(expected = NullPointerException.class) + public void timeoutFirstItemReturnsNull() { + Observable.just(1, 1).timeout(Observable.never(), new Function<Integer, Observable<Object>>() { + @Override + public Observable<Object> apply(Integer v) { + return null; + } + }).blockingSubscribe(); + } + + @Test(expected = NullPointerException.class) + public void toListSupplierReturnsNull() { + just1.toList(new Supplier<Collection<Integer>>() { + @Override + public Collection<Integer> get() { + return null; + } + }).blockingGet(); + } + + @Test + public void toMapValueSelectorReturnsNull() { + just1.toMap(new Function<Integer, Object>() { + @Override + public Object apply(Integer v) { + return v; + } + }, new Function<Integer, Object>() { + @Override + public Object apply(Integer v) { + return null; + } + }).blockingGet(); + } + + @Test(expected = NullPointerException.class) + public void toMapMapSupplierReturnsNull() { + just1.toMap(new Function<Integer, Object>() { + @Override + public Object apply(Integer v) { + return v; + } + }, new Function<Integer, Object>() { + @Override + public Object apply(Integer v) { + return v; + } + }, new Supplier<Map<Object, Object>>() { + @Override + public Map<Object, Object> get() { + return null; + } + }).blockingGet(); + } + + @Test + public void toMultiMapValueSelectorReturnsNullAllowed() { + just1.toMap(new Function<Integer, Object>() { + @Override + public Object apply(Integer v) { + return v; + } + }, new Function<Integer, Object>() { + @Override + public Object apply(Integer v) { + return null; + } + }).blockingGet(); + } + + @Test(expected = NullPointerException.class) + public void toMultimapMapSupplierReturnsNull() { + just1.toMultimap(new Function<Integer, Object>() { + @Override + public Object apply(Integer v) { + return v; + } + }, new Function<Integer, Object>() { + @Override + public Object apply(Integer v) { + return v; + } + }, new Supplier<Map<Object, Collection<Object>>>() { + @Override + public Map<Object, Collection<Object>> get() { + return null; + } + }).blockingGet(); + } + + @Test(expected = NullPointerException.class) + public void toMultimapMapCollectionSupplierReturnsNull() { + just1.toMultimap(new Function<Integer, Integer>() { + @Override + public Integer apply(Integer v) { + return v; + } + }, new Function<Integer, Integer>() { + @Override + public Integer apply(Integer v) { + return v; + } + }, new Supplier<Map<Integer, Collection<Integer>>>() { + @Override + public Map<Integer, Collection<Integer>> get() { + return new HashMap<>(); + } + }, new Function<Integer, Collection<Integer>>() { + @Override + public Collection<Integer> apply(Integer v) { + return null; + } + }).blockingGet(); + } + + @Test(expected = NullPointerException.class) + public void windowOpenCloseCloseReturnsNull() { + Observable.never().window(just1, new Function<Integer, Observable<Object>>() { + @Override + public Observable<Object> apply(Integer v) { + return null; + } + }).blockingSubscribe(); + } + + @Test(expected = NullPointerException.class) + public void withLatestFromCombinerReturnsNull() { + just1.withLatestFrom(just1, new BiFunction<Integer, Integer, Object>() { + @Override + public Object apply(Integer a, Integer b) { + return null; + } + }).blockingSubscribe(); + } + + @Test(expected = NullPointerException.class) + public void zipWithIterableCombinerReturnsNull() { + just1.zipWith(Arrays.asList(1), new BiFunction<Integer, Integer, Object>() { + @Override + public Object apply(Integer a, Integer b) { + return null; + } + }).blockingSubscribe(); + } + + @Test(expected = NullPointerException.class) + public void zipWithIterableIteratorNull() { + just1.zipWith(new Iterable<Object>() { + @Override + public Iterator<Object> iterator() { + return null; + } + }, new BiFunction<Integer, Object, Object>() { + @Override + public Object apply(Integer a, Object b) { + return 1; + } + }).blockingSubscribe(); + } + + @Test(expected = NullPointerException.class) + public void zipWithIterableOneIsNull() { + Observable.just(1, 2).zipWith(Arrays.asList(1, null), new BiFunction<Integer, Integer, Object>() { + @Override + public Object apply(Integer a, Integer b) { + return 1; + } + }).blockingSubscribe(); + } + + @Test(expected = NullPointerException.class) + public void zipWithCombinerReturnsNull() { + just1.zipWith(just1, new BiFunction<Integer, Integer, Object>() { + @Override + public Object apply(Integer a, Integer b) { + return null; + } + }).blockingSubscribe(); + } + +} diff --git a/src/test/java/io/reactivex/rxjava3/observable/ObservableReduceTests.java b/src/test/java/io/reactivex/rxjava3/observable/ObservableReduceTests.java new file mode 100644 index 0000000000..01c2106aff --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/observable/ObservableReduceTests.java @@ -0,0 +1,159 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.observable; + +import static org.junit.Assert.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.functions.BiFunction; +import io.reactivex.rxjava3.observable.ObservableCovarianceTest.*; + +public class ObservableReduceTests extends RxJavaTest { + + @Test + public void reduceIntsObservable() { + Observable<Integer> o = Observable.just(1, 2, 3); + int value = o.reduce(new BiFunction<Integer, Integer, Integer>() { + @Override + public Integer apply(Integer t1, Integer t2) { + return t1 + t2; + } + }).toObservable().blockingSingle(); + + assertEquals(6, value); + } + + @SuppressWarnings("unused") + @Test + public void reduceWithObjectsObservable() { + Observable<Movie> horrorMovies = Observable.<Movie> just(new HorrorMovie()); + + Observable<Movie> reduceResult = horrorMovies.scan(new BiFunction<Movie, Movie, Movie>() { + @Override + public Movie apply(Movie t1, Movie t2) { + return t2; + } + }).takeLast(1); + + Observable<Movie> reduceResult2 = horrorMovies.reduce(new BiFunction<Movie, Movie, Movie>() { + @Override + public Movie apply(Movie t1, Movie t2) { + return t2; + } + }).toObservable(); + + assertNotNull(reduceResult2); + } + + /** + * Reduce consumes and produces T so can't do covariance. + * + * https://github.com/ReactiveX/RxJava/issues/360#issuecomment-24203016 + */ + @Test + public void reduceWithCovariantObjectsObservable() { + Observable<Movie> horrorMovies = Observable.<Movie> just(new HorrorMovie()); + + Observable<Movie> reduceResult2 = horrorMovies.reduce(new BiFunction<Movie, Movie, Movie>() { + @Override + public Movie apply(Movie t1, Movie t2) { + return t2; + } + }).toObservable(); + + assertNotNull(reduceResult2); + } + + @Test + public void reduceInts() { + Observable<Integer> o = Observable.just(1, 2, 3); + int value = o.reduce(new BiFunction<Integer, Integer, Integer>() { + @Override + public Integer apply(Integer t1, Integer t2) { + return t1 + t2; + } + }).blockingGet(); + + assertEquals(6, value); + } + + @SuppressWarnings("unused") + @Test + public void reduceWithObjects() { + Observable<Movie> horrorMovies = Observable.<Movie> just(new HorrorMovie()); + + Observable<Movie> reduceResult = horrorMovies.scan(new BiFunction<Movie, Movie, Movie>() { + @Override + public Movie apply(Movie t1, Movie t2) { + return t2; + } + }).takeLast(1); + + Maybe<Movie> reduceResult2 = horrorMovies.reduce(new BiFunction<Movie, Movie, Movie>() { + @Override + public Movie apply(Movie t1, Movie t2) { + return t2; + } + }); + + assertNotNull(reduceResult2); + } + + /** + * Reduce consumes and produces T so can't do covariance. + * + * https://github.com/ReactiveX/RxJava/issues/360#issuecomment-24203016 + */ + @Test + public void reduceWithCovariantObjects() { + Observable<Movie> horrorMovies = Observable.<Movie> just(new HorrorMovie()); + + Maybe<Movie> reduceResult2 = horrorMovies.reduce(new BiFunction<Movie, Movie, Movie>() { + @Override + public Movie apply(Movie t1, Movie t2) { + return t2; + } + }); + + assertNotNull(reduceResult2); + } + + /** + * Reduce consumes and produces T so can't do covariance. + * + * https://github.com/ReactiveX/RxJava/issues/360#issuecomment-24203016 + */ + @Test + public void reduceCovariance() { + // must type it to <Movie> + Observable<Movie> horrorMovies = Observable.<Movie> just(new HorrorMovie()); + libraryFunctionActingOnMovieObservables(horrorMovies); + } + + /* + * This accepts <Movie> instead of <? super Movie> since `reduce` can't handle covariants + */ + public void libraryFunctionActingOnMovieObservables(Observable<Movie> obs) { + + obs.reduce(new BiFunction<Movie, Movie, Movie>() { + @Override + public Movie apply(Movie t1, Movie t2) { + return t2; + } + }); + } + +} diff --git a/src/test/java/io/reactivex/rxjava3/observable/ObservableScanTests.java b/src/test/java/io/reactivex/rxjava3/observable/ObservableScanTests.java new file mode 100644 index 0000000000..8bc33ac19f --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/observable/ObservableScanTests.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.observable; + +import java.util.HashMap; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.RxJavaTest; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.observable.ObservableEventStream.Event; + +public class ObservableScanTests extends RxJavaTest { + + @Test + public void unsubscribeScan() throws Exception { + + ObservableEventStream.getEventStream("HTTP-ClusterB", 20) + .scan(new HashMap<>(), new BiFunction<HashMap<String, String>, Event, HashMap<String, String>>() { + @Override + public HashMap<String, String> apply(HashMap<String, String> accum, Event perInstanceEvent) { + accum.put("instance", perInstanceEvent.instanceId); + return accum; + } + }) + .take(10) + .blockingForEach(new Consumer<HashMap<String, String>>() { + @Override + public void accept(HashMap<String, String> pv) { + System.out.println(pv); + } + }); + + Thread.sleep(200); // make sure the event streams receive their interrupt + } +} diff --git a/src/test/java/io/reactivex/rxjava3/observable/ObservableStartWithTests.java b/src/test/java/io/reactivex/rxjava3/observable/ObservableStartWithTests.java new file mode 100644 index 0000000000..8e0fe35add --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/observable/ObservableStartWithTests.java @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.observable; + +import static org.junit.Assert.assertEquals; + +import java.util.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.core.RxJavaTest; + +public class ObservableStartWithTests extends RxJavaTest { + + @Test + public void startWith1() { + List<String> values = Observable.just("one", "two") + .startWithArray("zero").toList().blockingGet(); + + assertEquals("zero", values.get(0)); + assertEquals("two", values.get(2)); + } + + @Test + public void startWithIterable() { + List<String> li = new ArrayList<>(); + li.add("alpha"); + li.add("beta"); + List<String> values = Observable.just("one", "two").startWithIterable(li).toList().blockingGet(); + + assertEquals("alpha", values.get(0)); + assertEquals("beta", values.get(1)); + assertEquals("one", values.get(2)); + assertEquals("two", values.get(3)); + } + + @Test + public void startWithObservable() { + List<String> li = new ArrayList<>(); + li.add("alpha"); + li.add("beta"); + List<String> values = Observable.just("one", "two") + .startWith(Observable.fromIterable(li)) + .toList() + .blockingGet(); + + assertEquals("alpha", values.get(0)); + assertEquals("beta", values.get(1)); + assertEquals("one", values.get(2)); + assertEquals("two", values.get(3)); + } + + @Test + public void startWithEmpty() { + Observable.just(1).startWithArray().test().assertResult(1); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/observable/ObservableSubscriberTest.java b/src/test/java/io/reactivex/rxjava3/observable/ObservableSubscriberTest.java new file mode 100644 index 0000000000..c8a1047fe3 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/observable/ObservableSubscriberTest.java @@ -0,0 +1,242 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.observable; + +import static org.junit.Assert.*; + +import java.util.*; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.core.Observer; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.observers.*; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.subjects.PublishSubject; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class ObservableSubscriberTest extends RxJavaTest { + @Test + public void onStartCalledOnceViaSubscribe() { + final AtomicInteger c = new AtomicInteger(); + Observable.just(1, 2, 3, 4).take(2).subscribe(new DefaultObserver<Integer>() { + + @Override + public void onStart() { + c.incrementAndGet(); + } + + @Override + public void onComplete() { + + } + + @Override + public void onError(Throwable e) { + + } + + @Override + public void onNext(Integer t) { + } + + }); + + assertEquals(1, c.get()); + } + + @Test + public void onStartCalledOnceViaUnsafeSubscribe() { + final AtomicInteger c = new AtomicInteger(); + Observable.just(1, 2, 3, 4).take(2).subscribe(new DefaultObserver<Integer>() { + + @Override + public void onStart() { + c.incrementAndGet(); + } + + @Override + public void onComplete() { + + } + + @Override + public void onError(Throwable e) { + + } + + @Override + public void onNext(Integer t) { + } + + }); + + assertEquals(1, c.get()); + } + + @Test + public void onStartCalledOnceViaLift() { + final AtomicInteger c = new AtomicInteger(); + Observable.just(1, 2, 3, 4).lift(new ObservableOperator<Integer, Integer>() { + + @Override + public Observer<? super Integer> apply(final Observer<? super Integer> child) { + return new DefaultObserver<Integer>() { + + @Override + public void onStart() { + c.incrementAndGet(); + } + + @Override + public void onComplete() { + child.onComplete(); + } + + @Override + public void onError(Throwable e) { + child.onError(e); + } + + @Override + public void onNext(Integer t) { + child.onNext(t); + } + + }; + } + + }).subscribe(); + + assertEquals(1, c.get()); + } + + @Test + public void subscribeConsumerConsumer() { + final List<Integer> list = new ArrayList<>(); + + Observable.just(1).subscribe(new Consumer<Integer>() { + @Override + public void accept(Integer v) throws Exception { + list.add(v); + } + }, new Consumer<Throwable>() { + @Override + public void accept(Throwable e) throws Exception { + list.add(100); + } + }); + + assertEquals(Arrays.asList(1), list); + } + + @Test + public void subscribeConsumerConsumerWithError() { + final List<Integer> list = new ArrayList<>(); + + Observable.<Integer>error(new TestException()).subscribe(new Consumer<Integer>() { + @Override + public void accept(Integer v) throws Exception { + list.add(v); + } + }, new Consumer<Throwable>() { + @Override + public void accept(Throwable e) throws Exception { + list.add(100); + } + }); + + assertEquals(Arrays.asList(100), list); + } + + @Test + public void methodTestCancelled() { + PublishSubject<Integer> ps = PublishSubject.create(); + + ps.test(true); + + assertFalse(ps.hasObservers()); + } + + @Test + public void safeSubscriberAlreadySafe() { + TestObserver<Integer> to = new TestObserver<>(); + Observable.just(1).safeSubscribe(new SafeObserver<>(to)); + + to.assertResult(1); + } + + @Test + public void methodTestNoCancel() { + PublishSubject<Integer> ps = PublishSubject.create(); + + ps.test(false); + + assertTrue(ps.hasObservers()); + } + + @SuppressWarnings("rawtypes") + @Test + public void pluginNull() { + RxJavaPlugins.setOnObservableSubscribe(new BiFunction<Observable, Observer, Observer>() { + @Override + public Observer apply(Observable a, Observer b) throws Exception { + return null; + } + }); + + try { + try { + + Observable.just(1).test(); + fail("Should have thrown"); + } catch (NullPointerException ex) { + assertEquals("The RxJavaPlugins.onSubscribe hook returned a null Observer. Please change the handler provided to RxJavaPlugins.setOnObservableSubscribe for invalid null returns. Further reading: https://github.com/ReactiveX/RxJava/wiki/Plugins", ex.getMessage()); + } + } finally { + RxJavaPlugins.reset(); + } + } + + static final class BadObservable extends Observable<Integer> { + @Override + protected void subscribeActual(Observer<? super Integer> observer) { + throw new IllegalArgumentException(); + } + } + + @Test + public void subscribeActualThrows() { + List<Throwable> list = TestHelper.trackPluginErrors(); + try { + try { + new BadObservable().test(); + fail("Should have thrown!"); + } catch (NullPointerException ex) { + if (!(ex.getCause() instanceof IllegalArgumentException)) { + fail(ex.toString() + ": Should be NPE(IAE)"); + } + } + + TestHelper.assertError(list, 0, IllegalArgumentException.class); + } finally { + RxJavaPlugins.reset(); + } + } + +} diff --git a/src/test/java/io/reactivex/rxjava3/observable/ObservableTest.java b/src/test/java/io/reactivex/rxjava3/observable/ObservableTest.java new file mode 100644 index 0000000000..87e6463d3c --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/observable/ObservableTest.java @@ -0,0 +1,1143 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.observable; + +import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +import java.util.*; +import java.util.concurrent.*; +import java.util.concurrent.atomic.*; + +import org.junit.*; +import org.mockito.InOrder; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.core.Observer; +import io.reactivex.rxjava3.disposables.*; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.observables.ConnectableObservable; +import io.reactivex.rxjava3.observers.*; +import io.reactivex.rxjava3.schedulers.*; +import io.reactivex.rxjava3.subjects.*; +import io.reactivex.rxjava3.testsupport.*; + +public class ObservableTest extends RxJavaTest { + + Observer<Number> w; + SingleObserver<Number> wo; + MaybeObserver<Number> wm; + + private static final Predicate<Integer> IS_EVEN = new Predicate<Integer>() { + @Override + public boolean test(Integer v) { + return v % 2 == 0; + } + }; + + @Before + public void before() { + w = TestHelper.mockObserver(); + wo = TestHelper.mockSingleObserver(); + wm = TestHelper.mockMaybeObserver(); + } + + @Test + public void fromArray() { + String[] items = new String[] { "one", "two", "three" }; + assertEquals((Long)3L, Observable.fromArray(items).count().blockingGet()); + assertEquals("two", Observable.fromArray(items).skip(1).take(1).blockingSingle()); + assertEquals("three", Observable.fromArray(items).takeLast(1).blockingSingle()); + } + + @Test + public void fromIterable() { + ArrayList<String> items = new ArrayList<>(); + items.add("one"); + items.add("two"); + items.add("three"); + + assertEquals((Long)3L, Observable.fromIterable(items).count().blockingGet()); + assertEquals("two", Observable.fromIterable(items).skip(1).take(1).blockingSingle()); + assertEquals("three", Observable.fromIterable(items).takeLast(1).blockingSingle()); + } + + @Test + public void fromArityArgs3() { + Observable<String> items = Observable.just("one", "two", "three"); + + assertEquals((Long)3L, items.count().blockingGet()); + assertEquals("two", items.skip(1).take(1).blockingSingle()); + assertEquals("three", items.takeLast(1).blockingSingle()); + } + + @Test + public void fromArityArgs1() { + Observable<String> items = Observable.just("one"); + + assertEquals((Long)1L, items.count().blockingGet()); + assertEquals("one", items.takeLast(1).blockingSingle()); + } + + @Test + public void create() { + + Observable<String> o = Observable.just("one", "two", "three"); + + Observer<String> observer = TestHelper.mockObserver(); + + o.subscribe(observer); + + verify(observer, times(1)).onNext("one"); + verify(observer, times(1)).onNext("two"); + verify(observer, times(1)).onNext("three"); + verify(observer, never()).onError(any(Throwable.class)); + verify(observer, times(1)).onComplete(); + } + + @Test + public void countAFewItemsObservable() { + Observable<String> o = Observable.just("a", "b", "c", "d"); + + o.count().toObservable().subscribe(w); + + // we should be called only once + verify(w, times(1)).onNext(anyLong()); + verify(w).onNext(4L); + verify(w, never()).onError(any(Throwable.class)); + verify(w, times(1)).onComplete(); + } + + @Test + public void countZeroItemsObservable() { + Observable<String> o = Observable.empty(); + o.count().toObservable().subscribe(w); + // we should be called only once + verify(w, times(1)).onNext(anyLong()); + verify(w).onNext(0L); + verify(w, never()).onError(any(Throwable.class)); + verify(w, times(1)).onComplete(); + } + + @Test + public void countErrorObservable() { + Observable<String> o = Observable.error(new Supplier<Throwable>() { + @Override + public Throwable get() { + return new RuntimeException(); + } + }); + + o.count().toObservable().subscribe(w); + verify(w, never()).onNext(anyInt()); + verify(w, never()).onComplete(); + verify(w, times(1)).onError(any(RuntimeException.class)); + } + + @Test + public void countAFewItems() { + Observable<String> o = Observable.just("a", "b", "c", "d"); + + o.count().subscribe(wo); + + // we should be called only once + verify(wo, times(1)).onSuccess(anyLong()); + verify(wo).onSuccess(4L); + verify(wo, never()).onError(any(Throwable.class)); + } + + @Test + public void countZeroItems() { + Observable<String> o = Observable.empty(); + o.count().subscribe(wo); + // we should be called only once + verify(wo, times(1)).onSuccess(anyLong()); + verify(wo).onSuccess(0L); + verify(wo, never()).onError(any(Throwable.class)); + } + + @Test + public void countError() { + Observable<String> o = Observable.error(new Supplier<Throwable>() { + @Override + public Throwable get() { + return new RuntimeException(); + } + }); + + o.count().subscribe(wo); + verify(wo, never()).onSuccess(anyInt()); + verify(wo, times(1)).onError(any(RuntimeException.class)); + } + + @Test + public void takeFirstWithPredicateOfSome() { + Observable<Integer> o = Observable.just(1, 3, 5, 4, 6, 3); + o.filter(IS_EVEN).take(1).subscribe(w); + verify(w, times(1)).onNext(anyInt()); + verify(w).onNext(4); + verify(w, times(1)).onComplete(); + verify(w, never()).onError(any(Throwable.class)); + } + + @Test + public void takeFirstWithPredicateOfNoneMatchingThePredicate() { + Observable<Integer> o = Observable.just(1, 3, 5, 7, 9, 7, 5, 3, 1); + o.filter(IS_EVEN).take(1).subscribe(w); + verify(w, never()).onNext(anyInt()); + verify(w, times(1)).onComplete(); + verify(w, never()).onError(any(Throwable.class)); + } + + @Test + public void takeFirstOfSome() { + Observable<Integer> o = Observable.just(1, 2, 3); + o.take(1).subscribe(w); + verify(w, times(1)).onNext(anyInt()); + verify(w).onNext(1); + verify(w, times(1)).onComplete(); + verify(w, never()).onError(any(Throwable.class)); + } + + @Test + public void takeFirstOfNone() { + Observable<Integer> o = Observable.empty(); + o.take(1).subscribe(w); + verify(w, never()).onNext(anyInt()); + verify(w, times(1)).onComplete(); + verify(w, never()).onError(any(Throwable.class)); + } + + @Test + public void firstOfNone() { + Observable<Integer> o = Observable.empty(); + o.firstElement().subscribe(wm); + verify(wm, never()).onSuccess(anyInt()); + verify(wm).onComplete(); + verify(wm, never()).onError(any(Throwable.class)); + } + + @Test + public void firstWithPredicateOfNoneMatchingThePredicate() { + Observable<Integer> o = Observable.just(1, 3, 5, 7, 9, 7, 5, 3, 1); + o.filter(IS_EVEN).firstElement().subscribe(wm); + verify(wm, never()).onSuccess(anyInt()); + verify(wm).onComplete(); + verify(wm, never()).onError(any(Throwable.class)); + } + + @Test + public void reduce() { + Observable<Integer> o = Observable.just(1, 2, 3, 4); + o.reduce(new BiFunction<Integer, Integer, Integer>() { + @Override + public Integer apply(Integer t1, Integer t2) { + return t1 + t2; + } + }) + .subscribe(wm); + // we should be called only once + verify(wm, times(1)).onSuccess(anyInt()); + verify(wm).onSuccess(10); + verify(wm, never()).onError(any(Throwable.class)); + verify(wm, never()).onComplete(); + } + + @Test + public void reduceObservable() { + Observable<Integer> o = Observable.just(1, 2, 3, 4); + o.reduce(new BiFunction<Integer, Integer, Integer>() { + @Override + public Integer apply(Integer t1, Integer t2) { + return t1 + t2; + } + }) + .toObservable() + .subscribe(w); + // we should be called only once + verify(w, times(1)).onNext(anyInt()); + verify(w).onNext(10); + verify(w, never()).onError(any(Throwable.class)); + verify(w).onComplete(); + } + + @Test + public void reduceWithEmptyObservable() { + Observable<Integer> o = Observable.range(1, 0); + o.reduce(new BiFunction<Integer, Integer, Integer>() { + @Override + public Integer apply(Integer t1, Integer t2) { + return t1 + t2; + } + }) + .toObservable() + .test() + .assertResult(); + } + + /** + * A reduce on an empty Observable and a seed should just pass the seed through. + * + * This is confirmed at https://github.com/ReactiveX/RxJava/issues/423#issuecomment-27642456 + */ + @Test + public void reduceWithEmptyObservableAndSeed() { + Observable<Integer> o = Observable.range(1, 0); + int value = o.reduce(1, new BiFunction<Integer, Integer, Integer>() { + @Override + public Integer apply(Integer t1, Integer t2) { + return t1 + t2; + } + }) + .blockingGet(); + + assertEquals(1, value); + } + + @Test + public void reduceWithInitialValue() { + Observable<Integer> o = Observable.just(1, 2, 3, 4); + o.reduce(50, new BiFunction<Integer, Integer, Integer>() { + @Override + public Integer apply(Integer t1, Integer t2) { + return t1 + t2; + } + }) + .subscribe(wo); + // we should be called only once + verify(wo, times(1)).onSuccess(anyInt()); + verify(wo).onSuccess(60); + verify(wo, never()).onError(any(Throwable.class)); + } + + @Test + public void reduceWithInitialValueObservable() { + Observable<Integer> o = Observable.just(1, 2, 3, 4); + o.reduce(50, new BiFunction<Integer, Integer, Integer>() { + @Override + public Integer apply(Integer t1, Integer t2) { + return t1 + t2; + } + }) + .toObservable() + .subscribe(w); + // we should be called only once + verify(w, times(1)).onNext(anyInt()); + verify(w).onNext(60); + } + + @Test + public void materializeDematerializeChaining() { + Observable<Integer> obs = Observable.just(1); + Observable<Integer> chained = obs.materialize() + .dematerialize(Functions.<Notification<Integer>>identity()); + + Observer<Integer> observer = TestHelper.mockObserver(); + + chained.subscribe(observer); + + verify(observer, times(1)).onNext(1); + verify(observer, times(1)).onComplete(); + verify(observer, times(0)).onError(any(Throwable.class)); + } + + /** + * The error from the user provided Observer is not handled by the subscribe method try/catch. + * + * It is handled by the AtomicObserver that wraps the provided Observer. + * + * Result: Passes (if AtomicObserver functionality exists) + * @throws InterruptedException if the test is interrupted + */ + @Test + public void customObservableWithErrorInObserverAsynchronous() throws InterruptedException { + final CountDownLatch latch = new CountDownLatch(1); + final AtomicInteger count = new AtomicInteger(); + final AtomicReference<Throwable> error = new AtomicReference<>(); + + // FIXME custom built??? + Observable.just("1", "2", "three", "4") + .subscribeOn(Schedulers.newThread()) + .safeSubscribe(new DefaultObserver<String>() { + @Override + public void onComplete() { + System.out.println("completed"); + latch.countDown(); + } + + @Override + public void onError(Throwable e) { + error.set(e); + System.out.println("error"); + e.printStackTrace(); + latch.countDown(); + } + + @Override + public void onNext(String v) { + int num = Integer.parseInt(v); + System.out.println(num); + // doSomething(num); + count.incrementAndGet(); + } + + }); + + // wait for async sequence to complete + latch.await(); + + assertEquals(2, count.get()); + assertNotNull(error.get()); + if (!(error.get() instanceof NumberFormatException)) { + fail("It should be a NumberFormatException"); + } + } + + /** + * The error from the user provided Observer is handled by the subscribe try/catch because this is synchronous. + * + * Result: Passes + */ + @Test + public void customObservableWithErrorInObserverSynchronous() { + final AtomicInteger count = new AtomicInteger(); + final AtomicReference<Throwable> error = new AtomicReference<>(); + + // FIXME custom built??? + Observable.just("1", "2", "three", "4") + .safeSubscribe(new DefaultObserver<String>() { + + @Override + public void onComplete() { + System.out.println("completed"); + } + + @Override + public void onError(Throwable e) { + error.set(e); + System.out.println("error"); + e.printStackTrace(); + } + + @Override + public void onNext(String v) { + int num = Integer.parseInt(v); + System.out.println(num); + // doSomething(num); + count.incrementAndGet(); + } + + }); + assertEquals(2, count.get()); + assertNotNull(error.get()); + if (!(error.get() instanceof NumberFormatException)) { + fail("It should be a NumberFormatException"); + } + } + + /** + * The error from the user provided Observable is handled by the subscribe try/catch because this is synchronous. + * + * + * Result: Passes + */ + @Test + public void customObservableWithErrorInObservableSynchronous() { + final AtomicInteger count = new AtomicInteger(); + final AtomicReference<Throwable> error = new AtomicReference<>(); + // FIXME custom built??? + Observable.just("1", "2").concatWith(Observable.<String>error(new Supplier<Throwable>() { + @Override + public Throwable get() { + return new NumberFormatException(); + } + })) + .subscribe(new DefaultObserver<String>() { + + @Override + public void onComplete() { + System.out.println("completed"); + } + + @Override + public void onError(Throwable e) { + error.set(e); + System.out.println("error"); + e.printStackTrace(); + } + + @Override + public void onNext(String v) { + System.out.println(v); + count.incrementAndGet(); + } + + }); + assertEquals(2, count.get()); + assertNotNull(error.get()); + if (!(error.get() instanceof NumberFormatException)) { + fail("It should be a NumberFormatException"); + } + } + + @Test + public void publishLast() throws InterruptedException { + final AtomicInteger count = new AtomicInteger(); + ConnectableObservable<String> connectable = Observable.<String>unsafeCreate(new ObservableSource<String>() { + @Override + public void subscribe(final Observer<? super String> observer) { + observer.onSubscribe(Disposable.empty()); + count.incrementAndGet(); + new Thread(new Runnable() { + @Override + public void run() { + observer.onNext("first"); + observer.onNext("last"); + observer.onComplete(); + } + }).start(); + } + }).takeLast(1).publish(); + + // subscribe once + final CountDownLatch latch = new CountDownLatch(1); + connectable.subscribe(new Consumer<String>() { + @Override + public void accept(String value) { + assertEquals("last", value); + latch.countDown(); + } + }); + + // subscribe twice + connectable.subscribe(); + + Disposable subscription = connectable.connect(); + assertTrue(latch.await(1000, TimeUnit.MILLISECONDS)); + assertEquals(1, count.get()); + subscription.dispose(); + } + + @Test + public void replay() throws InterruptedException { + final AtomicInteger counter = new AtomicInteger(); + ConnectableObservable<String> o = Observable.<String>unsafeCreate(new ObservableSource<String>() { + @Override + public void subscribe(final Observer<? super String> observer) { + observer.onSubscribe(Disposable.empty()); + new Thread(new Runnable() { + + @Override + public void run() { + counter.incrementAndGet(); + observer.onNext("one"); + observer.onComplete(); + } + }).start(); + } + }).replay(); + + // we connect immediately and it will emit the value + Disposable connection = o.connect(); + try { + + // we then expect the following 2 subscriptions to get that same value + final CountDownLatch latch = new CountDownLatch(2); + + // subscribe once + o.subscribe(new Consumer<String>() { + @Override + public void accept(String v) { + assertEquals("one", v); + latch.countDown(); + } + }); + + // subscribe again + o.subscribe(new Consumer<String>() { + @Override + public void accept(String v) { + assertEquals("one", v); + latch.countDown(); + } + }); + + if (!latch.await(1000, TimeUnit.MILLISECONDS)) { + fail("subscriptions did not receive values"); + } + assertEquals(1, counter.get()); + } finally { + connection.dispose(); + } + } + + @Test + public void cache() throws InterruptedException { + final AtomicInteger counter = new AtomicInteger(); + Observable<String> o = Observable.<String>unsafeCreate(new ObservableSource<String>() { + @Override + public void subscribe(final Observer<? super String> observer) { + observer.onSubscribe(Disposable.empty()); + new Thread(new Runnable() { + @Override + public void run() { + counter.incrementAndGet(); + observer.onNext("one"); + observer.onComplete(); + } + }).start(); + } + }).cache(); + + // we then expect the following 2 subscriptions to get that same value + final CountDownLatch latch = new CountDownLatch(2); + + // subscribe once + o.subscribe(new Consumer<String>() { + @Override + public void accept(String v) { + assertEquals("one", v); + latch.countDown(); + } + }); + + // subscribe again + o.subscribe(new Consumer<String>() { + @Override + public void accept(String v) { + assertEquals("one", v); + latch.countDown(); + } + }); + + if (!latch.await(1000, TimeUnit.MILLISECONDS)) { + fail("subscriptions did not receive values"); + } + assertEquals(1, counter.get()); + } + + @Test + public void cacheWithCapacity() throws InterruptedException { + final AtomicInteger counter = new AtomicInteger(); + Observable<String> o = Observable.<String>unsafeCreate(new ObservableSource<String>() { + @Override + public void subscribe(final Observer<? super String> observer) { + observer.onSubscribe(Disposable.empty()); + new Thread(new Runnable() { + @Override + public void run() { + counter.incrementAndGet(); + observer.onNext("one"); + observer.onComplete(); + } + }).start(); + } + }).cacheWithInitialCapacity(1); + + // we then expect the following 2 subscriptions to get that same value + final CountDownLatch latch = new CountDownLatch(2); + + // subscribe once + o.subscribe(new Consumer<String>() { + @Override + public void accept(String v) { + assertEquals("one", v); + latch.countDown(); + } + }); + + // subscribe again + o.subscribe(new Consumer<String>() { + @Override + public void accept(String v) { + assertEquals("one", v); + latch.countDown(); + } + }); + + if (!latch.await(1000, TimeUnit.MILLISECONDS)) { + fail("subscriptions did not receive values"); + } + assertEquals(1, counter.get()); + } + + @Test + public void takeWithErrorInObserver() { + final AtomicInteger count = new AtomicInteger(); + final AtomicReference<Throwable> error = new AtomicReference<>(); + Observable.just("1", "2", "three", "4").take(3) + .safeSubscribe(new DefaultObserver<String>() { + + @Override + public void onComplete() { + System.out.println("completed"); + } + + @Override + public void onError(Throwable e) { + error.set(e); + System.out.println("error"); + e.printStackTrace(); + } + + @Override + public void onNext(String v) { + int num = Integer.parseInt(v); + System.out.println(num); + // doSomething(num); + count.incrementAndGet(); + } + + }); + assertEquals(2, count.get()); + assertNotNull(error.get()); + if (!(error.get() instanceof NumberFormatException)) { + fail("It should be a NumberFormatException"); + } + } + + @Test + public void ofType() { + Observable<String> o = Observable.just(1, "abc", false, 2L).ofType(String.class); + + Observer<Object> observer = TestHelper.mockObserver(); + + o.subscribe(observer); + + verify(observer, never()).onNext(1); + verify(observer, times(1)).onNext("abc"); + verify(observer, never()).onNext(false); + verify(observer, never()).onNext(2L); + verify(observer, never()).onError( + any(Throwable.class)); + verify(observer, times(1)).onComplete(); + } + + @Test + public void ofTypeWithPolymorphism() { + ArrayList<Integer> l1 = new ArrayList<>(); + l1.add(1); + LinkedList<Integer> l2 = new LinkedList<>(); + l2.add(2); + + @SuppressWarnings("rawtypes") + Observable<List> o = Observable.<Object> just(l1, l2, "123").ofType(List.class); + + Observer<Object> observer = TestHelper.mockObserver(); + + o.subscribe(observer); + + verify(observer, times(1)).onNext(l1); + verify(observer, times(1)).onNext(l2); + verify(observer, never()).onNext("123"); + verify(observer, never()).onError( + any(Throwable.class)); + verify(observer, times(1)).onComplete(); + } + + @Test + public void containsObservable() { + Observable<Boolean> o = Observable.just("a", "b", "c").contains("b").toObservable(); + + Observer<Boolean> observer = TestHelper.mockObserver(); + + o.subscribe(observer); + + verify(observer, times(1)).onNext(true); + verify(observer, never()).onNext(false); + verify(observer, never()).onError( + any(Throwable.class)); + verify(observer, times(1)).onComplete(); + } + + @Test + public void containsWithInexistenceObservable() { + Observable<Boolean> o = Observable.just("a", "b").contains("c").toObservable(); + + Observer<Object> observer = TestHelper.mockObserver(); + + o.subscribe(observer); + + verify(observer, times(1)).onNext(false); + verify(observer, never()).onNext(true); + verify(observer, never()).onError( + any(Throwable.class)); + verify(observer, times(1)).onComplete(); + } + + @Test + public void containsWithEmptyObservableObservable() { + Observable<Boolean> o = Observable.<String> empty().contains("a").toObservable(); + + Observer<Object> observer = TestHelper.mockObserver(); + + o.subscribe(observer); + + verify(observer, times(1)).onNext(false); + verify(observer, never()).onNext(true); + verify(observer, never()).onError( + any(Throwable.class)); + verify(observer, times(1)).onComplete(); + } + + @Test + public void contains() { + Single<Boolean> o = Observable.just("a", "b", "c").contains("b"); // FIXME nulls not allowed, changed to "c" + + SingleObserver<Boolean> observer = TestHelper.mockSingleObserver(); + + o.subscribe(observer); + + verify(observer, times(1)).onSuccess(true); + verify(observer, never()).onSuccess(false); + verify(observer, never()).onError( + any(Throwable.class)); + } + + @Test + public void containsWithInexistence() { + Single<Boolean> o = Observable.just("a", "b").contains("c"); // FIXME null values are not allowed, removed + + SingleObserver<Object> observer = TestHelper.mockSingleObserver(); + + o.subscribe(observer); + + verify(observer, times(1)).onSuccess(false); + verify(observer, never()).onSuccess(true); + verify(observer, never()).onError( + any(Throwable.class)); + } + + @Test + public void containsWithEmptyObservable() { + Single<Boolean> o = Observable.<String> empty().contains("a"); + + SingleObserver<Object> observer = TestHelper.mockSingleObserver(); + + o.subscribe(observer); + + verify(observer, times(1)).onSuccess(false); + verify(observer, never()).onSuccess(true); + verify(observer, never()).onError( + any(Throwable.class)); + } + + @Test + public void ignoreElements() { + Completable o = Observable.just(1, 2, 3).ignoreElements(); + + CompletableObserver observer = TestHelper.mockCompletableObserver(); + + o.subscribe(observer); + + verify(observer, never()).onError(any(Throwable.class)); + verify(observer, times(1)).onComplete(); + } + + @Test + public void ignoreElementsObservable() { + Observable<Integer> o = Observable.just(1, 2, 3).ignoreElements().toObservable(); + + Observer<Object> observer = TestHelper.mockObserver(); + + o.subscribe(observer); + + verify(observer, never()).onNext(any(Integer.class)); + verify(observer, never()).onError(any(Throwable.class)); + verify(observer, times(1)).onComplete(); + } + + @Test + public void justWithScheduler() { + TestScheduler scheduler = new TestScheduler(); + Observable<Integer> o = Observable.fromArray(1, 2).subscribeOn(scheduler); + + Observer<Integer> observer = TestHelper.mockObserver(); + + o.subscribe(observer); + + scheduler.advanceTimeBy(1, TimeUnit.MILLISECONDS); + + InOrder inOrder = inOrder(observer); + inOrder.verify(observer, times(1)).onNext(1); + inOrder.verify(observer, times(1)).onNext(2); + inOrder.verify(observer, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void startWithWithScheduler() { + TestScheduler scheduler = new TestScheduler(); + Observable<Integer> o = Observable.just(3, 4).startWithIterable(Arrays.asList(1, 2)).subscribeOn(scheduler); + + Observer<Integer> observer = TestHelper.mockObserver(); + + o.subscribe(observer); + + scheduler.advanceTimeBy(1, TimeUnit.MILLISECONDS); + + InOrder inOrder = inOrder(observer); + inOrder.verify(observer, times(1)).onNext(1); + inOrder.verify(observer, times(1)).onNext(2); + inOrder.verify(observer, times(1)).onNext(3); + inOrder.verify(observer, times(1)).onNext(4); + inOrder.verify(observer, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void rangeWithScheduler() { + TestScheduler scheduler = new TestScheduler(); + Observable<Integer> o = Observable.range(3, 4).subscribeOn(scheduler); + + Observer<Integer> observer = TestHelper.mockObserver(); + + o.subscribe(observer); + + scheduler.advanceTimeBy(1, TimeUnit.MILLISECONDS); + + InOrder inOrder = inOrder(observer); + inOrder.verify(observer, times(1)).onNext(3); + inOrder.verify(observer, times(1)).onNext(4); + inOrder.verify(observer, times(1)).onNext(5); + inOrder.verify(observer, times(1)).onNext(6); + inOrder.verify(observer, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void mergeWith() { + TestObserver<Integer> to = new TestObserver<>(); + Observable.just(1).mergeWith(Observable.just(2)).subscribe(to); + to.assertValues(1, 2); + } + + @Test + public void concatWith() { + TestObserver<Integer> to = new TestObserver<>(); + Observable.just(1).concatWith(Observable.just(2)).subscribe(to); + to.assertValues(1, 2); + } + + @Test + public void ambWith() { + TestObserver<Integer> to = new TestObserver<>(); + Observable.just(1).ambWith(Observable.just(2)).subscribe(to); + to.assertValue(1); + } + + @Test + public void takeWhileToList() { + final int expectedCount = 3; + final AtomicInteger count = new AtomicInteger(); + for (int i = 0; i < expectedCount; i++) { + Observable + .just(Boolean.TRUE, Boolean.FALSE) + .takeWhile(new Predicate<Boolean>() { + @Override + public boolean test(Boolean v) { + return v; + } + }) + .toList() + .doOnSuccess(new Consumer<List<Boolean>>() { + @Override + public void accept(List<Boolean> booleans) { + count.incrementAndGet(); + } + }) + .subscribe(); + } + assertEquals(expectedCount, count.get()); + } + + @Test + public void compose() { + TestObserverEx<String> to = new TestObserverEx<>(); + + Observable.just(1, 2, 3).compose(new ObservableTransformer<Integer, String>() { + @Override + public Observable<String> apply(Observable<Integer> t1) { + return t1.map(new Function<Integer, String>() { + @Override + public String apply(Integer v) { + return String.valueOf(v); + } + }); + } + }) + .subscribe(to); + + to.assertTerminated(); + to.assertNoErrors(); + to.assertValues("1", "2", "3"); + } + + @Test + public void errorThrownIssue1685() { + Subject<Object> subject = ReplaySubject.create(); + + Observable.error(new RuntimeException("oops")) + .materialize() + .delay(1, TimeUnit.SECONDS) + .dematerialize(Functions.<Notification<Object>>identity()) + .subscribe(subject); + + subject.subscribe(); + subject.materialize().blockingFirst(); + + System.out.println("Done"); + } + + @Test + public void emptyIdentity() { + assertEquals(Observable.empty(), Observable.empty()); + } + + @Test + public void emptyIsEmpty() { + Observable.<Integer>empty().subscribe(w); + + verify(w).onComplete(); + verify(w, never()).onNext(any(Integer.class)); + verify(w, never()).onError(any(Throwable.class)); + } + +// FIXME this test doesn't make sense +// @Test // cf. https://github.com/ReactiveX/RxJava/issues/2599 +// public void testSubscribingSubscriberAsObserverMaintainsSubscriptionChain() { +// TestObserver<Object> observer = new TestObserver<T>(); +// Subscription subscription = Observable.just("event").subscribe((Observer<Object>) observer); +// subscription.unsubscribe(); +// +// subscriber.assertUnsubscribed(); +// } + +// FIXME subscribers can't throw +// @Test(expected=OnErrorNotImplementedException.class) +// public void testForEachWithError() { +// Observable.error(new Exception("boo")) +// // +// .forEach(new Action1<Object>() { +// @Override +// public void call(Object t) { +// //do nothing +// }}); +// } + + @Test + public void extend() { + final TestObserver<Object> to = new TestObserver<>(); + final Object value = new Object(); + Object returned = Observable.just(value).to(new ObservableConverter<Object, Object>() { + @Override + public Object apply(Observable<Object> onSubscribe) { + onSubscribe.subscribe(to); + to.assertNoErrors(); + to.assertComplete(); + to.assertValue(value); + return to.values().get(0); + } + }); + assertSame(returned, value); + } + + @Test + public void asExtend() { + final TestObserver<Object> to = new TestObserver<>(); + final Object value = new Object(); + Object returned = Observable.just(value).to(new ObservableConverter<Object, Object>() { + @Override + public Object apply(Observable<Object> onSubscribe) { + onSubscribe.subscribe(to); + to.assertNoErrors(); + to.assertComplete(); + to.assertValue(value); + return to.values().get(0); + } + }); + assertSame(returned, value); + } + + @Test + public void as() { + Observable.just(1).to(new ObservableConverter<Integer, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Observable<Integer> v) { + return v.toFlowable(BackpressureStrategy.MISSING); + } + }) + .test() + .assertResult(1); + } + + @Test + public void flatMap() { + List<Integer> list = Observable.range(1, 5).flatMap(new Function<Integer, Observable<Integer>>() { + @Override + public Observable<Integer> apply(Integer v) { + return Observable.range(v, 2); + } + }).toList().blockingGet(); + + Assert.assertEquals(Arrays.asList(1, 2, 2, 3, 3, 4, 4, 5, 5, 6), list); + } + + @Test + public void singleDefault() { + Observable.just(1).single(100).test().assertResult(1); + + Observable.empty().single(100).test().assertResult(100); + } + + @Test + public void singleDefaultObservable() { + Observable.just(1).single(100).toObservable().test().assertResult(1); + + Observable.empty().single(100).toObservable().test().assertResult(100); + } + + @Test + public void zipIterableObject() { + final List<Observable<Integer>> observables = Arrays.asList(Observable.just(1, 2, 3), Observable.just(1, 2, 3)); + Observable.zip(observables, new Function<Object[], Object>() { + @Override + public Object apply(Object[] o) throws Exception { + int sum = 0; + for (Object i : o) { + sum += (Integer) i; + } + return sum; + } + }).test().assertResult(2, 4, 6); + } + + @Test + public void combineLatestObject() { + final List<Observable<Integer>> observables = Arrays.asList(Observable.just(1, 2, 3), Observable.just(1, 2, 3)); + Observable.combineLatest(observables, new Function<Object[], Object>() { + @Override + public Object apply(final Object[] o) throws Exception { + int sum = 1; + for (Object i : o) { + sum *= (Integer) i; + } + return sum; + } + }).test().assertResult(3, 6, 9); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/observable/ObservableThrottleLastTests.java b/src/test/java/io/reactivex/rxjava3/observable/ObservableThrottleLastTests.java new file mode 100644 index 0000000000..d8107cc434 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/observable/ObservableThrottleLastTests.java @@ -0,0 +1,131 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.observable; + +import java.util.concurrent.TimeUnit; + +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.Action; +import org.junit.Test; +import org.mockito.InOrder; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.schedulers.TestScheduler; +import io.reactivex.rxjava3.subjects.PublishSubject; +import io.reactivex.rxjava3.testsupport.TestHelper; + +import static org.mockito.Mockito.*; + +public class ObservableThrottleLastTests extends RxJavaTest { + + @Test + public void throttleLastWithDropCallbackException() throws Throwable { + Observer<Integer> observer = TestHelper.mockObserver(); + + Action whenDisposed = mock(Action.class); + + TestScheduler s = new TestScheduler(); + PublishSubject<Integer> o = PublishSubject.create(); + o.doOnDispose(whenDisposed) + .throttleLast(500, TimeUnit.MILLISECONDS, s, e -> { + if (e == 1) { + throw new TestException("Forced"); + } + }) + .subscribe(observer); + + // send events with simulated time increments + s.advanceTimeTo(0, TimeUnit.MILLISECONDS); + o.onNext(1); // skip + o.onNext(2); // try to deliver + s.advanceTimeTo(501, TimeUnit.MILLISECONDS); + + InOrder inOrder = inOrder(observer); + inOrder.verify(observer).onError(any(TestException.class)); + inOrder.verifyNoMoreInteractions(); + verify(whenDisposed).run(); + } + + @Test + public void throttleLastWithDropCallback() { + Observer<Integer> observer = TestHelper.mockObserver(); + + Observer<Object> dropCallbackObserver = TestHelper.mockObserver(); + + TestScheduler s = new TestScheduler(); + PublishSubject<Integer> o = PublishSubject.create(); + o.throttleLast(500, TimeUnit.MILLISECONDS, s, dropCallbackObserver::onNext).subscribe(observer); + + // send events with simulated time increments + s.advanceTimeTo(0, TimeUnit.MILLISECONDS); + o.onNext(1); // skip + o.onNext(2); // deliver + s.advanceTimeTo(501, TimeUnit.MILLISECONDS); + o.onNext(3); // skip + s.advanceTimeTo(600, TimeUnit.MILLISECONDS); + o.onNext(4); // skip + s.advanceTimeTo(700, TimeUnit.MILLISECONDS); + o.onNext(5); // skip + o.onNext(6); // deliver + s.advanceTimeTo(1001, TimeUnit.MILLISECONDS); + o.onNext(7); // deliver + s.advanceTimeTo(1501, TimeUnit.MILLISECONDS); + o.onComplete(); + + InOrder inOrder = inOrder(observer); + InOrder dropCallbackOrder = inOrder(dropCallbackObserver); + dropCallbackOrder.verify(dropCallbackObserver).onNext(1); + inOrder.verify(observer).onNext(2); + dropCallbackOrder.verify(dropCallbackObserver).onNext(3); + dropCallbackOrder.verify(dropCallbackObserver).onNext(4); + dropCallbackOrder.verify(dropCallbackObserver).onNext(5); + inOrder.verify(observer).onNext(6); + inOrder.verify(observer).onNext(7); + inOrder.verify(observer).onComplete(); + inOrder.verifyNoMoreInteractions(); + dropCallbackOrder.verifyNoMoreInteractions(); + } + + @Test + public void throttle() { + Observer<Integer> observer = TestHelper.mockObserver(); + + TestScheduler s = new TestScheduler(); + PublishSubject<Integer> o = PublishSubject.create(); + o.throttleLast(500, TimeUnit.MILLISECONDS, s).subscribe(observer); + + // send events with simulated time increments + s.advanceTimeTo(0, TimeUnit.MILLISECONDS); + o.onNext(1); // skip + o.onNext(2); // deliver + s.advanceTimeTo(501, TimeUnit.MILLISECONDS); + o.onNext(3); // skip + s.advanceTimeTo(600, TimeUnit.MILLISECONDS); + o.onNext(4); // skip + s.advanceTimeTo(700, TimeUnit.MILLISECONDS); + o.onNext(5); // skip + o.onNext(6); // deliver + s.advanceTimeTo(1001, TimeUnit.MILLISECONDS); + o.onNext(7); // deliver + s.advanceTimeTo(1501, TimeUnit.MILLISECONDS); + o.onComplete(); + + InOrder inOrder = inOrder(observer); + inOrder.verify(observer).onNext(2); + inOrder.verify(observer).onNext(6); + inOrder.verify(observer).onNext(7); + inOrder.verify(observer).onComplete(); + inOrder.verifyNoMoreInteractions(); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/observable/ObservableThrottleWithTimeoutTests.java b/src/test/java/io/reactivex/rxjava3/observable/ObservableThrottleWithTimeoutTests.java new file mode 100644 index 0000000000..80f82fb5f4 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/observable/ObservableThrottleWithTimeoutTests.java @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.observable; + +import static org.mockito.Mockito.inOrder; + +import java.util.concurrent.TimeUnit; + +import org.junit.Test; +import org.mockito.InOrder; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.schedulers.TestScheduler; +import io.reactivex.rxjava3.subjects.PublishSubject; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class ObservableThrottleWithTimeoutTests extends RxJavaTest { + + @Test + public void throttle() { + Observer<Integer> observer = TestHelper.mockObserver(); + + TestScheduler s = new TestScheduler(); + PublishSubject<Integer> o = PublishSubject.create(); + o.throttleWithTimeout(500, TimeUnit.MILLISECONDS, s) + .subscribe(observer); + + // send events with simulated time increments + s.advanceTimeTo(0, TimeUnit.MILLISECONDS); + o.onNext(1); // skip + o.onNext(2); // deliver + s.advanceTimeTo(501, TimeUnit.MILLISECONDS); + o.onNext(3); // skip + s.advanceTimeTo(600, TimeUnit.MILLISECONDS); + o.onNext(4); // skip + s.advanceTimeTo(700, TimeUnit.MILLISECONDS); + o.onNext(5); // skip + o.onNext(6); // deliver at 1300 after 500ms has passed since onNext(5) + s.advanceTimeTo(1300, TimeUnit.MILLISECONDS); + o.onNext(7); // deliver + s.advanceTimeTo(1800, TimeUnit.MILLISECONDS); + o.onComplete(); + + InOrder inOrder = inOrder(observer); + inOrder.verify(observer).onNext(2); + inOrder.verify(observer).onNext(6); + inOrder.verify(observer).onNext(7); + inOrder.verify(observer).onComplete(); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void throttleFirstDefaultScheduler() { + Observable.just(1).throttleWithTimeout(100, TimeUnit.MILLISECONDS) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(1); + } + +} diff --git a/src/test/java/io/reactivex/rxjava3/observable/ObservableWindowTests.java b/src/test/java/io/reactivex/rxjava3/observable/ObservableWindowTests.java new file mode 100644 index 0000000000..4bc561bd1b --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/observable/ObservableWindowTests.java @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.observable; + +import static org.junit.Assert.*; + +import java.util.*; +import java.util.concurrent.TimeUnit; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.schedulers.*; +import io.reactivex.rxjava3.subjects.PublishSubject; + +public class ObservableWindowTests extends RxJavaTest { + + @Test + public void window() { + final ArrayList<List<Integer>> lists = new ArrayList<>(); + + Observable.concat( + Observable.just(1, 2, 3, 4, 5, 6) + .window(3) + .map(new Function<Observable<Integer>, Observable<List<Integer>>>() { + @Override + public Observable<List<Integer>> apply(Observable<Integer> xs) { + return xs.toList().toObservable(); + } + }) + ) + .blockingForEach(new Consumer<List<Integer>>() { + @Override + public void accept(List<Integer> xs) { + lists.add(xs); + } + }); + + assertArrayEquals(lists.get(0).toArray(new Integer[3]), new Integer[] { 1, 2, 3 }); + assertArrayEquals(lists.get(1).toArray(new Integer[3]), new Integer[] { 4, 5, 6 }); + assertEquals(2, lists.size()); + + } + + @Test + public void timeSizeWindowAlternatingBounds() { + TestScheduler scheduler = new TestScheduler(); + PublishSubject<Integer> ps = PublishSubject.create(); + + TestObserver<List<Integer>> to = ps.window(5, TimeUnit.SECONDS, scheduler, 2) + .flatMapSingle(new Function<Observable<Integer>, SingleSource<List<Integer>>>() { + @Override + public SingleSource<List<Integer>> apply(Observable<Integer> v) throws Throwable { + return v.toList(); + } + }) + .test(); + + ps.onNext(1); + ps.onNext(2); + to.assertValueCount(1); // size bound hit + + scheduler.advanceTimeBy(1, TimeUnit.SECONDS); + ps.onNext(3); + scheduler.advanceTimeBy(6, TimeUnit.SECONDS); + to.assertValueCount(2); // time bound hit + + ps.onNext(4); + ps.onNext(5); + + to.assertValueCount(3); // size bound hit again + + ps.onNext(4); + + scheduler.advanceTimeBy(6, TimeUnit.SECONDS); + + to.assertValueCount(4) + .assertNoErrors() + .assertNotComplete(); + + to.dispose(); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/observable/ObservableZipTests.java b/src/test/java/io/reactivex/rxjava3/observable/ObservableZipTests.java new file mode 100644 index 0000000000..b2b26719bb --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/observable/ObservableZipTests.java @@ -0,0 +1,160 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.observable; + +import static org.junit.Assert.assertSame; + +import java.util.*; + +import org.junit.*; + +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.core.RxJavaTest; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.observable.ObservableCovarianceTest.*; +import io.reactivex.rxjava3.observable.ObservableEventStream.Event; +import io.reactivex.rxjava3.observables.GroupedObservable; + +public class ObservableZipTests extends RxJavaTest { + + @Test + public void zipObservableOfObservables() throws Exception { + ObservableEventStream.getEventStream("HTTP-ClusterB", 20) + .groupBy(new Function<Event, String>() { + @Override + public String apply(Event e) { + return e.instanceId; + } + }) + // now we have streams of cluster+instanceId + .flatMap(new Function<GroupedObservable<String, Event>, Observable<HashMap<String, String>>>() { + @Override + public Observable<HashMap<String, String>> apply(final GroupedObservable<String, Event> ge) { + return ge.scan(new HashMap<>(), new BiFunction<HashMap<String, String>, Event, HashMap<String, String>>() { + @Override + public HashMap<String, String> apply(HashMap<String, String> accum, + Event perInstanceEvent) { + synchronized (accum) { + accum.put("instance", ge.getKey()); + } + return accum; + } + }); + } + }) + .take(10) + .blockingForEach(new Consumer<Object>() { + @Override + public void accept(Object pv) { + synchronized (pv) { + System.out.println(pv); + } + } + }); + + System.out.println("**** finished"); + + Thread.sleep(200); // make sure the event streams receive their interrupt + } + + /** + * This won't compile if super/extends isn't done correctly on generics. + */ + @Test + public void covarianceOfZip() { + Observable<HorrorMovie> horrors = Observable.just(new HorrorMovie()); + Observable<CoolRating> ratings = Observable.just(new CoolRating()); + + Observable.<Movie, CoolRating, Result> zip(horrors, ratings, combine).blockingForEach(action); + Observable.<Movie, CoolRating, Result> zip(horrors, ratings, combine).blockingForEach(action); + Observable.<Media, Rating, ExtendedResult> zip(horrors, ratings, combine).blockingForEach(extendedAction); + Observable.<Media, Rating, Result> zip(horrors, ratings, combine).blockingForEach(action); + Observable.<Media, Rating, ExtendedResult> zip(horrors, ratings, combine).blockingForEach(action); + + Observable.<Movie, CoolRating, Result> zip(horrors, ratings, combine); + } + + /** + * Occasionally zip may be invoked with 0 observables. Test that we don't block indefinitely instead + * of immediately invoking zip with 0 argument. + * + * We now expect an NoSuchElementException since last() requires at least one value and nothing will be emitted. + */ + @Test(expected = NoSuchElementException.class) + public void nonBlockingObservable() { + + final Object invoked = new Object(); + + Collection<Observable<Object>> observables = Collections.emptyList(); + + Observable<Object> result = Observable.zip(observables, new Function<Object[], Object>() { + @Override + public Object apply(Object[] args) { + System.out.println("received: " + args); + Assert.assertEquals("No argument should have been passed", 0, args.length); + return invoked; + } + }); + + assertSame(invoked, result.blockingLast()); + } + + BiFunction<Media, Rating, ExtendedResult> combine = new BiFunction<Media, Rating, ExtendedResult>() { + @Override + public ExtendedResult apply(Media m, Rating r) { + return new ExtendedResult(); + } + }; + + Consumer<Result> action = new Consumer<Result>() { + @Override + public void accept(Result t1) { + System.out.println("Result: " + t1); + } + }; + + Consumer<ExtendedResult> extendedAction = new Consumer<ExtendedResult>() { + @Override + public void accept(ExtendedResult t1) { + System.out.println("Result: " + t1); + } + }; + + @Test + public void zipWithDelayError() { + Observable.just(1) + .zipWith(Observable.just(2), new BiFunction<Integer, Integer, Integer>() { + @Override + public Integer apply(Integer a, Integer b) throws Exception { + return a + b; + } + }, true) + .test() + .assertResult(3); + } + + @Test + public void zipWithDelayErrorBufferSize() { + Observable.just(1) + .zipWith(Observable.just(2), new BiFunction<Integer, Integer, Integer>() { + @Override + public Integer apply(Integer a, Integer b) throws Exception { + return a + b; + } + }, true, 16) + .test() + .assertResult(3); + } + +} diff --git a/src/test/java/io/reactivex/rxjava3/observers/DisposableCompletableObserverTest.java b/src/test/java/io/reactivex/rxjava3/observers/DisposableCompletableObserverTest.java new file mode 100644 index 0000000000..fde1dc75de --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/observers/DisposableCompletableObserverTest.java @@ -0,0 +1,113 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.observers; + +import static org.junit.Assert.*; + +import java.util.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.*; +import io.reactivex.rxjava3.internal.util.EndConsumerHelper; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class DisposableCompletableObserverTest extends RxJavaTest { + + static final class TestCompletable extends DisposableCompletableObserver { + + int start; + + int complete; + + final List<Throwable> errors = new ArrayList<>(); + + @Override + protected void onStart() { + super.onStart(); + + start++; + } + + @Override + public void onComplete() { + complete++; + } + + @Override + public void onError(Throwable e) { + errors.add(e); + } + + } + + @Test + public void normal() { + TestCompletable tc = new TestCompletable(); + + assertFalse(tc.isDisposed()); + assertEquals(0, tc.start); + assertEquals(0, tc.complete); + assertTrue(tc.errors.isEmpty()); + + Completable.complete().subscribe(tc); + + assertFalse(tc.isDisposed()); + assertEquals(1, tc.start); + assertEquals(1, tc.complete); + assertTrue(tc.errors.isEmpty()); + } + + @Test + public void startOnce() { + + List<Throwable> error = TestHelper.trackPluginErrors(); + + try { + TestCompletable tc = new TestCompletable(); + + tc.onSubscribe(Disposable.empty()); + + Disposable d = Disposable.empty(); + + tc.onSubscribe(d); + + assertTrue(d.isDisposed()); + + assertEquals(1, tc.start); + + TestHelper.assertError(error, 0, IllegalStateException.class, EndConsumerHelper.composeMessage(tc.getClass().getName())); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void dispose() { + TestCompletable tc = new TestCompletable(); + tc.dispose(); + + assertTrue(tc.isDisposed()); + + Disposable d = Disposable.empty(); + + tc.onSubscribe(d); + + assertTrue(d.isDisposed()); + + assertEquals(0, tc.start); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/observers/DisposableMaybeObserverTest.java b/src/test/java/io/reactivex/rxjava3/observers/DisposableMaybeObserverTest.java new file mode 100644 index 0000000000..7bd2f20a95 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/observers/DisposableMaybeObserverTest.java @@ -0,0 +1,121 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.observers; + +import static org.junit.Assert.*; + +import java.util.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.*; +import io.reactivex.rxjava3.internal.util.EndConsumerHelper; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class DisposableMaybeObserverTest extends RxJavaTest { + + static final class TestMaybe<T> extends DisposableMaybeObserver<T> { + + int start; + + final List<T> values = new ArrayList<>(); + + final List<Throwable> errors = new ArrayList<>(); + + int complete; + + @Override + protected void onStart() { + super.onStart(); + + start++; + } + + @Override + public void onSuccess(T value) { + values.add(value); + } + + @Override + public void onError(Throwable e) { + errors.add(e); + } + + @Override + public void onComplete() { + complete++; + } + } + + @Test + public void normal() { + TestMaybe<Integer> tc = new TestMaybe<>(); + + assertFalse(tc.isDisposed()); + assertEquals(0, tc.start); + assertTrue(tc.values.isEmpty()); + assertTrue(tc.errors.isEmpty()); + assertEquals(0, tc.complete); + + Maybe.just(1).subscribe(tc); + + assertFalse(tc.isDisposed()); + assertEquals(1, tc.start); + assertEquals(1, tc.values.get(0).intValue()); + assertTrue(tc.errors.isEmpty()); + assertEquals(0, tc.complete); + } + + @Test + public void startOnce() { + + List<Throwable> error = TestHelper.trackPluginErrors(); + + try { + TestMaybe<Integer> tc = new TestMaybe<>(); + + tc.onSubscribe(Disposable.empty()); + + Disposable d = Disposable.empty(); + + tc.onSubscribe(d); + + assertTrue(d.isDisposed()); + + assertEquals(1, tc.start); + + TestHelper.assertError(error, 0, IllegalStateException.class, EndConsumerHelper.composeMessage(tc.getClass().getName())); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void dispose() { + TestMaybe<Integer> tc = new TestMaybe<>(); + tc.dispose(); + + assertTrue(tc.isDisposed()); + + Disposable d = Disposable.empty(); + + tc.onSubscribe(d); + + assertTrue(d.isDisposed()); + + assertEquals(0, tc.start); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/observers/DisposableObserverTest.java b/src/test/java/io/reactivex/rxjava3/observers/DisposableObserverTest.java new file mode 100644 index 0000000000..1b5b477552 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/observers/DisposableObserverTest.java @@ -0,0 +1,123 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.observers; + +import static org.junit.Assert.*; + +import java.util.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.core.RxJavaTest; +import io.reactivex.rxjava3.disposables.*; +import io.reactivex.rxjava3.internal.util.EndConsumerHelper; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class DisposableObserverTest extends RxJavaTest { + + static final class TestDisposableObserver<T> extends DisposableObserver<T> { + + int start; + + final List<T> values = new ArrayList<>(); + + final List<Throwable> errors = new ArrayList<>(); + + int completions; + + @Override + protected void onStart() { + super.onStart(); + + start++; + } + + @Override + public void onNext(T value) { + values.add(value); + } + + @Override + public void onError(Throwable e) { + errors.add(e); + } + + @Override + public void onComplete() { + completions++; + } + } + + @Test + public void normal() { + TestDisposableObserver<Integer> tc = new TestDisposableObserver<>(); + + assertFalse(tc.isDisposed()); + assertEquals(0, tc.start); + assertTrue(tc.values.isEmpty()); + assertTrue(tc.errors.isEmpty()); + + Observable.just(1).subscribe(tc); + + assertFalse(tc.isDisposed()); + assertEquals(1, tc.start); + assertEquals(1, tc.values.get(0).intValue()); + assertTrue(tc.errors.isEmpty()); + } + + @Test + public void startOnce() { + + List<Throwable> error = TestHelper.trackPluginErrors(); + + try { + TestDisposableObserver<Integer> tc = new TestDisposableObserver<>(); + + tc.onSubscribe(Disposable.empty()); + + Disposable d = Disposable.empty(); + + tc.onSubscribe(d); + + assertTrue(d.isDisposed()); + + assertEquals(1, tc.start); + + TestHelper.assertError(error, 0, IllegalStateException.class, EndConsumerHelper.composeMessage(tc.getClass().getName())); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void dispose() { + TestDisposableObserver<Integer> tc = new TestDisposableObserver<>(); + + assertFalse(tc.isDisposed()); + + tc.dispose(); + + assertTrue(tc.isDisposed()); + + Disposable d = Disposable.empty(); + + tc.onSubscribe(d); + + assertTrue(d.isDisposed()); + + assertEquals(0, tc.start); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/observers/DisposableSingleObserverTest.java b/src/test/java/io/reactivex/rxjava3/observers/DisposableSingleObserverTest.java new file mode 100644 index 0000000000..8dac144591 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/observers/DisposableSingleObserverTest.java @@ -0,0 +1,113 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.observers; + +import static org.junit.Assert.*; + +import java.util.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.*; +import io.reactivex.rxjava3.internal.util.EndConsumerHelper; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class DisposableSingleObserverTest extends RxJavaTest { + + static final class TestSingle<T> extends DisposableSingleObserver<T> { + + int start; + + final List<T> values = new ArrayList<>(); + + final List<Throwable> errors = new ArrayList<>(); + + @Override + protected void onStart() { + super.onStart(); + + start++; + } + + @Override + public void onSuccess(T value) { + values.add(value); + } + + @Override + public void onError(Throwable e) { + errors.add(e); + } + + } + + @Test + public void normal() { + TestSingle<Integer> tc = new TestSingle<>(); + + assertFalse(tc.isDisposed()); + assertEquals(0, tc.start); + assertTrue(tc.values.isEmpty()); + assertTrue(tc.errors.isEmpty()); + + Single.just(1).subscribe(tc); + + assertFalse(tc.isDisposed()); + assertEquals(1, tc.start); + assertEquals(1, tc.values.get(0).intValue()); + assertTrue(tc.errors.isEmpty()); + } + + @Test + public void startOnce() { + + List<Throwable> error = TestHelper.trackPluginErrors(); + + try { + TestSingle<Integer> tc = new TestSingle<>(); + + tc.onSubscribe(Disposable.empty()); + + Disposable d = Disposable.empty(); + + tc.onSubscribe(d); + + assertTrue(d.isDisposed()); + + assertEquals(1, tc.start); + + TestHelper.assertError(error, 0, IllegalStateException.class, EndConsumerHelper.composeMessage(tc.getClass().getName())); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void dispose() { + TestSingle<Integer> tc = new TestSingle<>(); + tc.dispose(); + + assertTrue(tc.isDisposed()); + + Disposable d = Disposable.empty(); + + tc.onSubscribe(d); + + assertTrue(d.isDisposed()); + + assertEquals(0, tc.start); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/observers/ResourceCompletableObserverTest.java b/src/test/java/io/reactivex/rxjava3/observers/ResourceCompletableObserverTest.java new file mode 100644 index 0000000000..e2d1fa34ba --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/observers/ResourceCompletableObserverTest.java @@ -0,0 +1,199 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.observers; + +import static org.junit.Assert.*; + +import java.util.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.internal.util.EndConsumerHelper; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class ResourceCompletableObserverTest extends RxJavaTest { + static final class TestResourceCompletableObserver extends ResourceCompletableObserver { + final List<Throwable> errors = new ArrayList<>(); + + int complete; + + int start; + + @Override + protected void onStart() { + super.onStart(); + + start++; + } + + @Override + public void onComplete() { + complete++; + + dispose(); + } + + @Override + public void onError(Throwable e) { + errors.add(e); + + dispose(); + } + } + + @Test(expected = NullPointerException.class) + public void nullResource() { + TestResourceCompletableObserver rco = new TestResourceCompletableObserver(); + rco.add(null); + } + + @Test + public void addResources() { + TestResourceCompletableObserver rco = new TestResourceCompletableObserver(); + + assertFalse(rco.isDisposed()); + + Disposable d = Disposable.empty(); + + rco.add(d); + + assertFalse(d.isDisposed()); + + rco.dispose(); + + assertTrue(rco.isDisposed()); + + assertTrue(d.isDisposed()); + + rco.dispose(); + + assertTrue(rco.isDisposed()); + + assertTrue(d.isDisposed()); + } + + @Test + public void onCompleteCleansUp() { + TestResourceCompletableObserver rco = new TestResourceCompletableObserver(); + + assertFalse(rco.isDisposed()); + + Disposable d = Disposable.empty(); + + rco.add(d); + + assertFalse(d.isDisposed()); + + rco.onComplete(); + + assertTrue(rco.isDisposed()); + + assertTrue(d.isDisposed()); + } + + @Test + public void onErrorCleansUp() { + TestResourceCompletableObserver rco = new TestResourceCompletableObserver(); + + assertFalse(rco.isDisposed()); + + Disposable d = Disposable.empty(); + + rco.add(d); + + assertFalse(d.isDisposed()); + + rco.onError(new TestException()); + + assertTrue(rco.isDisposed()); + + assertTrue(d.isDisposed()); + } + + @Test + public void normal() { + TestResourceCompletableObserver rco = new TestResourceCompletableObserver(); + + assertFalse(rco.isDisposed()); + assertEquals(0, rco.start); + assertTrue(rco.errors.isEmpty()); + + Completable.complete().subscribe(rco); + + assertTrue(rco.isDisposed()); + assertEquals(1, rco.start); + assertEquals(1, rco.complete); + assertTrue(rco.errors.isEmpty()); + } + + @Test + public void error() { + TestResourceCompletableObserver rco = new TestResourceCompletableObserver(); + + assertFalse(rco.isDisposed()); + assertEquals(0, rco.start); + assertTrue(rco.errors.isEmpty()); + + final RuntimeException error = new RuntimeException("error"); + Completable.error(error).subscribe(rco); + + assertTrue(rco.isDisposed()); + assertEquals(1, rco.start); + assertEquals(0, rco.complete); + assertEquals(1, rco.errors.size()); + assertTrue(rco.errors.contains(error)); + } + + @Test + public void startOnce() { + + List<Throwable> error = TestHelper.trackPluginErrors(); + + try { + TestResourceCompletableObserver rco = new TestResourceCompletableObserver(); + + rco.onSubscribe(Disposable.empty()); + + Disposable d = Disposable.empty(); + + rco.onSubscribe(d); + + assertTrue(d.isDisposed()); + + assertEquals(1, rco.start); + + TestHelper.assertError(error, 0, IllegalStateException.class, EndConsumerHelper.composeMessage(rco.getClass().getName())); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void dispose() { + TestResourceCompletableObserver rco = new TestResourceCompletableObserver(); + rco.dispose(); + + Disposable d = Disposable.empty(); + + rco.onSubscribe(d); + + assertTrue(d.isDisposed()); + + assertEquals(0, rco.start); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/observers/ResourceMaybeObserverTest.java b/src/test/java/io/reactivex/rxjava3/observers/ResourceMaybeObserverTest.java new file mode 100644 index 0000000000..257a5785f8 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/observers/ResourceMaybeObserverTest.java @@ -0,0 +1,249 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.observers; + +import static org.junit.Assert.*; + +import java.util.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.internal.util.EndConsumerHelper; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class ResourceMaybeObserverTest extends RxJavaTest { + static final class TestResourceMaybeObserver<T> extends ResourceMaybeObserver<T> { + T value; + + final List<Throwable> errors = new ArrayList<>(); + + int complete; + + int start; + + @Override + protected void onStart() { + super.onStart(); + + start++; + } + + @Override + public void onSuccess(final T value) { + this.value = value; + + dispose(); + } + + @Override + public void onComplete() { + complete++; + + dispose(); + } + + @Override + public void onError(Throwable e) { + errors.add(e); + + dispose(); + } + } + + @Test(expected = NullPointerException.class) + public void nullResource() { + TestResourceMaybeObserver<Integer> rmo = new TestResourceMaybeObserver<>(); + rmo.add(null); + } + + @Test + public void addResources() { + TestResourceMaybeObserver<Integer> rmo = new TestResourceMaybeObserver<>(); + + assertFalse(rmo.isDisposed()); + + Disposable d = Disposable.empty(); + + rmo.add(d); + + assertFalse(d.isDisposed()); + + rmo.dispose(); + + assertTrue(rmo.isDisposed()); + + assertTrue(d.isDisposed()); + + rmo.dispose(); + + assertTrue(rmo.isDisposed()); + + assertTrue(d.isDisposed()); + } + + @Test + public void onCompleteCleansUp() { + TestResourceMaybeObserver<Integer> rmo = new TestResourceMaybeObserver<>(); + + assertFalse(rmo.isDisposed()); + + Disposable d = Disposable.empty(); + + rmo.add(d); + + assertFalse(d.isDisposed()); + + rmo.onComplete(); + + assertTrue(rmo.isDisposed()); + + assertTrue(d.isDisposed()); + } + + @Test + public void onSuccessCleansUp() { + TestResourceMaybeObserver<Integer> rmo = new TestResourceMaybeObserver<>(); + + assertFalse(rmo.isDisposed()); + + Disposable d = Disposable.empty(); + + rmo.add(d); + + assertFalse(d.isDisposed()); + + rmo.onSuccess(1); + + assertTrue(rmo.isDisposed()); + + assertTrue(d.isDisposed()); + } + + @Test + public void onErrorCleansUp() { + TestResourceMaybeObserver<Integer> rmo = new TestResourceMaybeObserver<>(); + + assertFalse(rmo.isDisposed()); + + Disposable d = Disposable.empty(); + + rmo.add(d); + + assertFalse(d.isDisposed()); + + rmo.onError(new TestException()); + + assertTrue(rmo.isDisposed()); + + assertTrue(d.isDisposed()); + } + + @Test + public void normal() { + TestResourceMaybeObserver<Integer> rmo = new TestResourceMaybeObserver<>(); + + assertFalse(rmo.isDisposed()); + assertEquals(0, rmo.start); + assertNull(rmo.value); + assertTrue(rmo.errors.isEmpty()); + + Maybe.just(1).subscribe(rmo); + + assertTrue(rmo.isDisposed()); + assertEquals(1, rmo.start); + assertEquals(Integer.valueOf(1), rmo.value); + assertEquals(0, rmo.complete); + assertTrue(rmo.errors.isEmpty()); + } + + @Test + public void empty() { + TestResourceMaybeObserver<Integer> rmo = new TestResourceMaybeObserver<>(); + + assertFalse(rmo.isDisposed()); + assertEquals(0, rmo.start); + assertNull(rmo.value); + assertTrue(rmo.errors.isEmpty()); + + Maybe.<Integer>empty().subscribe(rmo); + + assertTrue(rmo.isDisposed()); + assertEquals(1, rmo.start); + assertNull(rmo.value); + assertEquals(1, rmo.complete); + assertTrue(rmo.errors.isEmpty()); + } + + @Test + public void error() { + TestResourceMaybeObserver<Integer> rmo = new TestResourceMaybeObserver<>(); + + assertFalse(rmo.isDisposed()); + assertEquals(0, rmo.start); + assertNull(rmo.value); + assertTrue(rmo.errors.isEmpty()); + + final RuntimeException error = new RuntimeException("error"); + Maybe.<Integer>error(error).subscribe(rmo); + + assertTrue(rmo.isDisposed()); + assertEquals(1, rmo.start); + assertNull(rmo.value); + assertEquals(0, rmo.complete); + assertEquals(1, rmo.errors.size()); + assertTrue(rmo.errors.contains(error)); + } + + @Test + public void startOnce() { + + List<Throwable> error = TestHelper.trackPluginErrors(); + + try { + TestResourceMaybeObserver<Integer> rmo = new TestResourceMaybeObserver<>(); + + rmo.onSubscribe(Disposable.empty()); + + Disposable d = Disposable.empty(); + + rmo.onSubscribe(d); + + assertTrue(d.isDisposed()); + + assertEquals(1, rmo.start); + + TestHelper.assertError(error, 0, IllegalStateException.class, EndConsumerHelper.composeMessage(rmo.getClass().getName())); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void dispose() { + TestResourceMaybeObserver<Integer> rmo = new TestResourceMaybeObserver<>(); + rmo.dispose(); + + Disposable d = Disposable.empty(); + + rmo.onSubscribe(d); + + assertTrue(d.isDisposed()); + + assertEquals(0, rmo.start); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/observers/ResourceObserverTest.java b/src/test/java/io/reactivex/rxjava3/observers/ResourceObserverTest.java new file mode 100644 index 0000000000..543095582b --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/observers/ResourceObserverTest.java @@ -0,0 +1,210 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.observers; + +import static org.junit.Assert.*; + +import java.util.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.core.RxJavaTest; +import io.reactivex.rxjava3.disposables.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.internal.util.EndConsumerHelper; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class ResourceObserverTest extends RxJavaTest { + + static final class TestResourceObserver<T> extends ResourceObserver<T> { + final List<T> values = new ArrayList<>(); + + final List<Throwable> errors = new ArrayList<>(); + + int complete; + + int start; + + @Override + protected void onStart() { + super.onStart(); + + start++; + } + + @Override + public void onNext(T value) { + values.add(value); + } + + @Override + public void onError(Throwable e) { + errors.add(e); + + dispose(); + } + + @Override + public void onComplete() { + complete++; + + dispose(); + } + } + + @Test(expected = NullPointerException.class) + public void nullResource() { + TestResourceObserver<Integer> ro = new TestResourceObserver<>(); + ro.add(null); + } + + @Test + public void addResources() { + TestResourceObserver<Integer> ro = new TestResourceObserver<>(); + + assertFalse(ro.isDisposed()); + + Disposable d = Disposable.empty(); + + ro.add(d); + + assertFalse(d.isDisposed()); + + ro.dispose(); + + assertTrue(ro.isDisposed()); + + assertTrue(d.isDisposed()); + + ro.dispose(); + + assertTrue(ro.isDisposed()); + + assertTrue(d.isDisposed()); + } + + @Test + public void onCompleteCleansUp() { + TestResourceObserver<Integer> ro = new TestResourceObserver<>(); + + assertFalse(ro.isDisposed()); + + Disposable d = Disposable.empty(); + + ro.add(d); + + assertFalse(d.isDisposed()); + + ro.onComplete(); + + assertTrue(ro.isDisposed()); + + assertTrue(d.isDisposed()); + } + + @Test + public void onErrorCleansUp() { + TestResourceObserver<Integer> ro = new TestResourceObserver<>(); + + assertFalse(ro.isDisposed()); + + Disposable d = Disposable.empty(); + + ro.add(d); + + assertFalse(d.isDisposed()); + + ro.onError(new TestException()); + + assertTrue(ro.isDisposed()); + + assertTrue(d.isDisposed()); + } + + @Test + public void normal() { + TestResourceObserver<Integer> tc = new TestResourceObserver<>(); + + assertFalse(tc.isDisposed()); + assertEquals(0, tc.start); + assertTrue(tc.values.isEmpty()); + assertTrue(tc.errors.isEmpty()); + + Observable.just(1).subscribe(tc); + + assertTrue(tc.isDisposed()); + assertEquals(1, tc.start); + assertEquals(1, tc.values.get(0).intValue()); + assertTrue(tc.errors.isEmpty()); + } + + @Test + public void error() { + TestResourceObserver<Integer> tc = new TestResourceObserver<>(); + + assertFalse(tc.isDisposed()); + assertEquals(0, tc.start); + assertTrue(tc.values.isEmpty()); + assertTrue(tc.errors.isEmpty()); + + final RuntimeException error = new RuntimeException("error"); + Observable.<Integer>error(error).subscribe(tc); + + assertTrue(tc.isDisposed()); + assertEquals(1, tc.start); + assertTrue(tc.values.isEmpty()); + assertEquals(1, tc.errors.size()); + assertTrue(tc.errors.contains(error)); + } + + @Test + public void startOnce() { + + List<Throwable> error = TestHelper.trackPluginErrors(); + + try { + TestResourceObserver<Integer> tc = new TestResourceObserver<>(); + + tc.onSubscribe(Disposable.empty()); + + Disposable d = Disposable.empty(); + + tc.onSubscribe(d); + + assertTrue(d.isDisposed()); + + assertEquals(1, tc.start); + + TestHelper.assertError(error, 0, IllegalStateException.class, EndConsumerHelper.composeMessage(tc.getClass().getName())); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void dispose() { + TestResourceObserver<Integer> tc = new TestResourceObserver<>(); + tc.dispose(); + + Disposable d = Disposable.empty(); + + tc.onSubscribe(d); + + assertTrue(d.isDisposed()); + + assertEquals(0, tc.start); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/observers/ResourceSingleObserverTest.java b/src/test/java/io/reactivex/rxjava3/observers/ResourceSingleObserverTest.java new file mode 100644 index 0000000000..10f17e18d6 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/observers/ResourceSingleObserverTest.java @@ -0,0 +1,201 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.observers; + +import static org.junit.Assert.*; + +import java.util.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.internal.util.EndConsumerHelper; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class ResourceSingleObserverTest extends RxJavaTest { + static final class TestResourceSingleObserver<T> extends ResourceSingleObserver<T> { + T value; + + final List<Throwable> errors = new ArrayList<>(); + + int start; + + @Override + protected void onStart() { + super.onStart(); + + start++; + } + + @Override + public void onSuccess(final T value) { + this.value = value; + + dispose(); + } + + @Override + public void onError(Throwable e) { + errors.add(e); + + dispose(); + } + } + + @Test(expected = NullPointerException.class) + public void nullResource() { + TestResourceSingleObserver<Integer> rso = new TestResourceSingleObserver<>(); + rso.add(null); + } + + @Test + public void addResources() { + TestResourceSingleObserver<Integer> rso = new TestResourceSingleObserver<>(); + + assertFalse(rso.isDisposed()); + + Disposable d = Disposable.empty(); + + rso.add(d); + + assertFalse(d.isDisposed()); + + rso.dispose(); + + assertTrue(rso.isDisposed()); + + assertTrue(d.isDisposed()); + + rso.dispose(); + + assertTrue(rso.isDisposed()); + + assertTrue(d.isDisposed()); + } + + @Test + public void onSuccessCleansUp() { + TestResourceSingleObserver<Integer> rso = new TestResourceSingleObserver<>(); + + assertFalse(rso.isDisposed()); + + Disposable d = Disposable.empty(); + + rso.add(d); + + assertFalse(d.isDisposed()); + + rso.onSuccess(1); + + assertTrue(rso.isDisposed()); + + assertTrue(d.isDisposed()); + } + + @Test + public void onErrorCleansUp() { + TestResourceSingleObserver<Integer> rso = new TestResourceSingleObserver<>(); + + assertFalse(rso.isDisposed()); + + Disposable d = Disposable.empty(); + + rso.add(d); + + assertFalse(d.isDisposed()); + + rso.onError(new TestException()); + + assertTrue(rso.isDisposed()); + + assertTrue(d.isDisposed()); + } + + @Test + public void normal() { + TestResourceSingleObserver<Integer> rso = new TestResourceSingleObserver<>(); + + assertFalse(rso.isDisposed()); + assertEquals(0, rso.start); + assertNull(rso.value); + assertTrue(rso.errors.isEmpty()); + + Single.just(1).subscribe(rso); + + assertTrue(rso.isDisposed()); + assertEquals(1, rso.start); + assertEquals(Integer.valueOf(1), rso.value); + assertTrue(rso.errors.isEmpty()); + } + + @Test + public void error() { + TestResourceSingleObserver<Integer> rso = new TestResourceSingleObserver<>(); + + assertFalse(rso.isDisposed()); + assertEquals(0, rso.start); + assertNull(rso.value); + assertTrue(rso.errors.isEmpty()); + + final RuntimeException error = new RuntimeException("error"); + Single.<Integer>error(error).subscribe(rso); + + assertTrue(rso.isDisposed()); + assertEquals(1, rso.start); + assertNull(rso.value); + assertEquals(1, rso.errors.size()); + assertTrue(rso.errors.contains(error)); + } + + @Test + public void startOnce() { + + List<Throwable> error = TestHelper.trackPluginErrors(); + + try { + TestResourceSingleObserver<Integer> rso = new TestResourceSingleObserver<>(); + + rso.onSubscribe(Disposable.empty()); + + Disposable d = Disposable.empty(); + + rso.onSubscribe(d); + + assertTrue(d.isDisposed()); + + assertEquals(1, rso.start); + + TestHelper.assertError(error, 0, IllegalStateException.class, EndConsumerHelper.composeMessage(rso.getClass().getName())); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void dispose() { + TestResourceSingleObserver<Integer> rso = new TestResourceSingleObserver<>(); + rso.dispose(); + + Disposable d = Disposable.empty(); + + rso.onSubscribe(d); + + assertTrue(d.isDisposed()); + + assertEquals(0, rso.start); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/observers/SafeObserverTest.java b/src/test/java/io/reactivex/rxjava3/observers/SafeObserverTest.java new file mode 100644 index 0000000000..77b3d7b9b9 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/observers/SafeObserverTest.java @@ -0,0 +1,635 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.observers; + +import static org.junit.Assert.*; + +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.*; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.testsupport.*; + +public class SafeObserverTest extends RxJavaTest { + + @Test + public void onNextFailure() { + AtomicReference<Throwable> onError = new AtomicReference<>(); + try { + OBSERVER_ONNEXT_FAIL(onError).onNext("one"); + fail("expects exception to be thrown"); + } catch (Exception e) { + assertNull(onError.get()); + assertTrue(e instanceof SafeObserverTestException); + assertEquals("onNextFail", e.getMessage()); + } + } + + @Test + public void onNextFailureSafe() { + AtomicReference<Throwable> onError = new AtomicReference<>(); + try { + SafeObserver<String> safeObserver = new SafeObserver<>(OBSERVER_ONNEXT_FAIL(onError)); + safeObserver.onSubscribe(Disposable.empty()); + safeObserver.onNext("one"); + assertNotNull(onError.get()); + assertTrue(onError.get() instanceof SafeObserverTestException); + assertEquals("onNextFail", onError.get().getMessage()); + } catch (Exception e) { + fail("expects exception to be passed to onError"); + } + } + + @Test + public void onCompleteFailure() { + AtomicReference<Throwable> onError = new AtomicReference<>(); + try { + OBSERVER_ONCOMPLETED_FAIL(onError).onComplete(); + fail("expects exception to be thrown"); + } catch (Exception e) { + assertNull(onError.get()); + assertTrue(e instanceof SafeObserverTestException); + assertEquals("onCompleteFail", e.getMessage()); + } + } + + @Test + public void onErrorFailure() { + try { + OBSERVER_ONERROR_FAIL().onError(new SafeObserverTestException("error!")); + fail("expects exception to be thrown"); + } catch (Exception e) { + assertTrue(e instanceof SafeObserverTestException); + assertEquals("onErrorFail", e.getMessage()); + } + } + + @Test + public void onNextOnErrorFailure() { + try { + OBSERVER_ONNEXT_ONERROR_FAIL().onNext("one"); + fail("expects exception to be thrown"); + } catch (Exception e) { + e.printStackTrace(); + assertTrue(e instanceof SafeObserverTestException); + assertEquals("onNextFail", e.getMessage()); + } + } + + private static Observer<String> OBSERVER_ONNEXT_FAIL(final AtomicReference<Throwable> onError) { + return new DefaultObserver<String>() { + + @Override + public void onComplete() { + + } + + @Override + public void onError(Throwable e) { + onError.set(e); + } + + @Override + public void onNext(String args) { + throw new SafeObserverTestException("onNextFail"); + } + }; + + } + + private static Observer<String> OBSERVER_ONNEXT_ONERROR_FAIL() { + return new DefaultObserver<String>() { + + @Override + public void onComplete() { + + } + + @Override + public void onError(Throwable e) { + throw new SafeObserverTestException("onErrorFail"); + } + + @Override + public void onNext(String args) { + throw new SafeObserverTestException("onNextFail"); + } + + }; + } + + private static Observer<String> OBSERVER_ONERROR_FAIL() { + return new DefaultObserver<String>() { + + @Override + public void onComplete() { + + } + + @Override + public void onError(Throwable e) { + throw new SafeObserverTestException("onErrorFail"); + } + + @Override + public void onNext(String args) { + + } + + }; + } + + private static Observer<String> OBSERVER_ONCOMPLETED_FAIL(final AtomicReference<Throwable> onError) { + return new DefaultObserver<String>() { + + @Override + public void onComplete() { + throw new SafeObserverTestException("onCompleteFail"); + } + + @Override + public void onError(Throwable e) { + onError.set(e); + } + + @Override + public void onNext(String args) { + + } + + }; + } + + @SuppressWarnings("serial") + static class SafeObserverTestException extends RuntimeException { + SafeObserverTestException(String message) { + super(message); + } + } + + @Test + public void actual() { + Observer<Integer> actual = new DefaultObserver<Integer>() { + @Override + public void onNext(Integer t) { + } + + @Override + public void onError(Throwable e) { + } + + @Override + public void onComplete() { + } + }; + SafeObserver<Integer> observer = new SafeObserver<>(actual); + + assertSame(actual, observer.downstream); + } + + @Test + public void dispose() { + TestObserver<Integer> to = new TestObserver<>(); + + SafeObserver<Integer> so = new SafeObserver<>(to); + + Disposable d = Disposable.empty(); + + so.onSubscribe(d); + + to.dispose(); + + assertTrue(d.isDisposed()); + + assertTrue(so.isDisposed()); + } + + @Test + @SuppressUndeliverable + public void onNextAfterComplete() { + TestObserver<Integer> to = new TestObserver<>(); + + SafeObserver<Integer> so = new SafeObserver<>(to); + + Disposable d = Disposable.empty(); + + so.onSubscribe(d); + + so.onComplete(); + + so.onNext(1); + + so.onError(new TestException()); + + so.onComplete(); + + to.assertResult(); + } + + @Test + public void onNextNull() { + TestObserver<Integer> to = new TestObserver<>(); + + SafeObserver<Integer> so = new SafeObserver<>(to); + + Disposable d = Disposable.empty(); + + so.onSubscribe(d); + + so.onNext(null); + + to.assertFailure(NullPointerException.class); + } + + @Test + public void onNextWithoutOnSubscribe() { + TestObserverEx<Integer> to = new TestObserverEx<>(); + + SafeObserver<Integer> so = new SafeObserver<>(to); + + so.onNext(1); + + to.assertFailureAndMessage(NullPointerException.class, "Subscription not set!"); + } + + @Test + public void onErrorWithoutOnSubscribe() { + TestObserverEx<Integer> to = new TestObserverEx<>(); + + SafeObserver<Integer> so = new SafeObserver<>(to); + + so.onError(new TestException()); + + to.assertFailure(CompositeException.class); + + TestHelper.assertError(to, 0, TestException.class); + TestHelper.assertError(to, 1, NullPointerException.class, "Subscription not set!"); + } + + @Test + public void onCompleteWithoutOnSubscribe() { + TestObserverEx<Integer> to = new TestObserverEx<>(); + + SafeObserver<Integer> so = new SafeObserver<>(to); + + so.onComplete(); + + to.assertFailureAndMessage(NullPointerException.class, "Subscription not set!"); + } + + @Test + public void onNextNormal() { + TestObserver<Integer> to = new TestObserver<>(); + + SafeObserver<Integer> so = new SafeObserver<>(to); + + Disposable d = Disposable.empty(); + + so.onSubscribe(d); + + so.onNext(1); + so.onComplete(); + + to.assertResult(1); + } + + static final class CrashDummy implements Observer<Object>, Disposable { + boolean crashOnSubscribe; + int crashOnNext; + boolean crashOnError; + boolean crashOnComplete; + + boolean crashDispose; + + Throwable error; + + CrashDummy(boolean crashOnSubscribe, int crashOnNext, + boolean crashOnError, boolean crashOnComplete, boolean crashDispose) { + this.crashOnSubscribe = crashOnSubscribe; + this.crashOnNext = crashOnNext; + this.crashOnError = crashOnError; + this.crashOnComplete = crashOnComplete; + this.crashDispose = crashDispose; + } + + @Override + public void dispose() { + if (crashDispose) { + throw new TestException("dispose()"); + } + } + + @Override + public boolean isDisposed() { + return false; + } + + @Override + public void onSubscribe(Disposable d) { + if (crashOnSubscribe) { + throw new TestException("onSubscribe()"); + } + } + + @Override + public void onNext(Object value) { + if (--crashOnNext == 0) { + throw new TestException("onNext(" + value + ")"); + } + } + + @Override + public void onError(Throwable e) { + if (crashOnError) { + throw new TestException("onError(" + e + ")"); + } + error = e; + } + + @Override + public void onComplete() { + if (crashOnComplete) { + throw new TestException("onComplete()"); + } + } + + public SafeObserver<Object> toSafe() { + return new SafeObserver<>(this); + } + + public CrashDummy assertError(Class<? extends Throwable> clazz) { + if (!clazz.isInstance(error)) { + throw new AssertionError("Different error: " + error); + } + return this; + } + + public CrashDummy assertInnerError(int index, Class<? extends Throwable> clazz) { + List<Throwable> cel = TestHelper.compositeList(error); + TestHelper.assertError(cel, index, clazz); + return this; + } + + public CrashDummy assertInnerError(int index, Class<? extends Throwable> clazz, String message) { + List<Throwable> cel = TestHelper.compositeList(error); + TestHelper.assertError(cel, index, clazz, message); + return this; + } + + } + + @Test + public void onNextOnErrorCrash() { + List<Throwable> list = TestHelper.trackPluginErrors(); + + try { + CrashDummy cd = new CrashDummy(false, 1, true, false, false); + SafeObserver<Object> so = cd.toSafe(); + so.onSubscribe(cd); + + so.onNext(1); + + TestHelper.assertError(list, 0, CompositeException.class); + List<Throwable> ce = TestHelper.compositeList(list.get(0)); + TestHelper.assertError(ce, 0, TestException.class, "onNext(1)"); + TestHelper.assertError(ce, 1, TestException.class, "onError(io.reactivex.rxjava3.exceptions.TestException: onNext(1))"); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void onNextDisposeCrash() { + CrashDummy cd = new CrashDummy(false, 1, false, false, true); + SafeObserver<Object> so = cd.toSafe(); + so.onSubscribe(cd); + + so.onNext(1); + + cd.assertError(CompositeException.class); + cd.assertInnerError(0, TestException.class, "onNext(1)"); + cd.assertInnerError(1, TestException.class, "dispose()"); + } + + @Test + public void onSubscribeTwice() { + List<Throwable> list = TestHelper.trackPluginErrors(); + + try { + CrashDummy cd = new CrashDummy(false, 1, false, false, false); + SafeObserver<Object> so = cd.toSafe(); + so.onSubscribe(cd); + so.onSubscribe(cd); + + TestHelper.assertError(list, 0, IllegalStateException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void onSubscribeCrashes() { + List<Throwable> list = TestHelper.trackPluginErrors(); + + try { + CrashDummy cd = new CrashDummy(true, 1, false, false, false); + SafeObserver<Object> so = cd.toSafe(); + so.onSubscribe(cd); + + TestHelper.assertUndeliverable(list, 0, TestException.class, "onSubscribe()"); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void onSubscribeAndDisposeCrashes() { + List<Throwable> list = TestHelper.trackPluginErrors(); + + try { + CrashDummy cd = new CrashDummy(true, 1, false, false, true); + SafeObserver<Object> so = cd.toSafe(); + so.onSubscribe(cd); + + TestHelper.assertError(list, 0, CompositeException.class); + List<Throwable> ce = TestHelper.compositeList(list.get(0)); + TestHelper.assertError(ce, 0, TestException.class, "onSubscribe()"); + TestHelper.assertError(ce, 1, TestException.class, "dispose()"); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void onNextOnSubscribeCrash() { + List<Throwable> list = TestHelper.trackPluginErrors(); + + try { + CrashDummy cd = new CrashDummy(true, 1, false, false, false); + SafeObserver<Object> so = cd.toSafe(); + + so.onNext(1); + + TestHelper.assertError(list, 0, CompositeException.class); + List<Throwable> ce = TestHelper.compositeList(list.get(0)); + TestHelper.assertError(ce, 0, NullPointerException.class, "Subscription not set!"); + TestHelper.assertError(ce, 1, TestException.class, "onSubscribe()"); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void onNextNullDisposeCrashes() { + CrashDummy cd = new CrashDummy(false, 1, false, false, true); + SafeObserver<Object> so = cd.toSafe(); + so.onSubscribe(cd); + + so.onNext(null); + + cd.assertInnerError(0, NullPointerException.class); + cd.assertInnerError(1, TestException.class, "dispose()"); + } + + @Test + public void noSubscribeOnErrorCrashes() { + List<Throwable> list = TestHelper.trackPluginErrors(); + + try { + CrashDummy cd = new CrashDummy(false, 1, true, false, false); + SafeObserver<Object> so = cd.toSafe(); + + so.onNext(1); + + TestHelper.assertError(list, 0, CompositeException.class); + List<Throwable> ce = TestHelper.compositeList(list.get(0)); + TestHelper.assertError(ce, 0, NullPointerException.class, "Subscription not set!"); + TestHelper.assertError(ce, 1, TestException.class, "onError(java.lang.NullPointerException: Subscription not set!)"); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void onErrorNull() { + CrashDummy cd = new CrashDummy(false, 1, false, false, false); + SafeObserver<Object> so = cd.toSafe(); + so.onSubscribe(cd); + + so.onError(null); + + cd.assertError(NullPointerException.class); + } + + @Test + public void onErrorNoSubscribeCrash() { + List<Throwable> list = TestHelper.trackPluginErrors(); + + try { + CrashDummy cd = new CrashDummy(true, 1, false, false, false); + SafeObserver<Object> so = cd.toSafe(); + + so.onError(new TestException()); + + TestHelper.assertError(list, 0, CompositeException.class); + List<Throwable> ce = TestHelper.compositeList(list.get(0)); + TestHelper.assertError(ce, 0, TestException.class); + TestHelper.assertError(ce, 1, NullPointerException.class, "Subscription not set!"); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void onErrorNoSubscribeOnErrorCrash() { + List<Throwable> list = TestHelper.trackPluginErrors(); + + try { + CrashDummy cd = new CrashDummy(false, 1, true, false, false); + SafeObserver<Object> so = cd.toSafe(); + + so.onError(new TestException()); + + TestHelper.assertError(list, 0, CompositeException.class); + List<Throwable> ce = TestHelper.compositeList(list.get(0)); + TestHelper.assertError(ce, 0, TestException.class); + TestHelper.assertError(ce, 1, NullPointerException.class, "Subscription not set!"); + TestHelper.assertError(ce, 2, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void onCompleteteCrash() { + List<Throwable> list = TestHelper.trackPluginErrors(); + + try { + CrashDummy cd = new CrashDummy(false, 1, false, true, false); + SafeObserver<Object> so = cd.toSafe(); + + so.onSubscribe(cd); + + so.onComplete(); + + TestHelper.assertUndeliverable(list, 0, TestException.class, "onComplete()"); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void onCompleteteNoSubscribeCrash() { + List<Throwable> list = TestHelper.trackPluginErrors(); + + try { + CrashDummy cd = new CrashDummy(true, 1, false, true, false); + SafeObserver<Object> so = cd.toSafe(); + + so.onComplete(); + + TestHelper.assertError(list, 0, CompositeException.class); + List<Throwable> ce = TestHelper.compositeList(list.get(0)); + TestHelper.assertError(ce, 0, NullPointerException.class, "Subscription not set!"); + TestHelper.assertError(ce, 1, TestException.class, "onSubscribe()"); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void onCompleteteNoSubscribeOnErrorCrash() { + List<Throwable> list = TestHelper.trackPluginErrors(); + + try { + CrashDummy cd = new CrashDummy(false, 1, true, true, false); + SafeObserver<Object> so = cd.toSafe(); + + so.onComplete(); + + TestHelper.assertError(list, 0, CompositeException.class); + List<Throwable> ce = TestHelper.compositeList(list.get(0)); + TestHelper.assertError(ce, 0, NullPointerException.class, "Subscription not set!"); + TestHelper.assertError(ce, 1, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + +} diff --git a/src/test/java/io/reactivex/rxjava3/observers/SerializedObserverTest.java b/src/test/java/io/reactivex/rxjava3/observers/SerializedObserverTest.java new file mode 100644 index 0000000000..689908f59f --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/observers/SerializedObserverTest.java @@ -0,0 +1,1170 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.observers; + +import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import java.util.List; +import java.util.concurrent.*; +import java.util.concurrent.atomic.*; + +import org.junit.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.internal.util.ExceptionHelper; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.schedulers.Schedulers; +import io.reactivex.rxjava3.testsupport.*; + +public class SerializedObserverTest extends RxJavaTest { + + Observer<String> observer; + + @Before + public void before() { + observer = TestHelper.mockObserver(); + } + + private Observer<String> serializedObserver(Observer<String> o) { + return new SerializedObserver<>(o); + } + + @Test + public void singleThreadedBasic() { + TestSingleThreadedObservable onSubscribe = new TestSingleThreadedObservable("one", "two", "three"); + Observable<String> w = Observable.unsafeCreate(onSubscribe); + + Observer<String> aw = serializedObserver(observer); + + w.subscribe(aw); + onSubscribe.waitToFinish(); + + verify(observer, times(1)).onNext("one"); + verify(observer, times(1)).onNext("two"); + verify(observer, times(1)).onNext("three"); + verify(observer, never()).onError(any(Throwable.class)); + verify(observer, times(1)).onComplete(); + // non-deterministic because unsubscribe happens after 'waitToFinish' releases + // so commenting out for now as this is not a critical thing to test here + // verify(s, times(1)).unsubscribe(); + } + + @Test + public void multiThreadedBasic() { + TestMultiThreadedObservable onSubscribe = new TestMultiThreadedObservable("one", "two", "three"); + Observable<String> w = Observable.unsafeCreate(onSubscribe); + + BusyObserver busySubscriber = new BusyObserver(); + Observer<String> aw = serializedObserver(busySubscriber); + + w.subscribe(aw); + onSubscribe.waitToFinish(); + + assertEquals(3, busySubscriber.onNextCount.get()); + assertFalse(busySubscriber.onError); + assertTrue(busySubscriber.onComplete); + // non-deterministic because unsubscribe happens after 'waitToFinish' releases + // so commenting out for now as this is not a critical thing to test here + // verify(s, times(1)).unsubscribe(); + + // we can have concurrency ... + assertTrue(onSubscribe.maxConcurrentThreads.get() > 1); + // ... but the onNext execution should be single threaded + assertEquals(1, busySubscriber.maxConcurrentThreads.get()); + } + + @Test + public void multiThreadedWithNPE() throws InterruptedException { + TestMultiThreadedObservable onSubscribe = new TestMultiThreadedObservable("one", "two", "three", null); + Observable<String> w = Observable.unsafeCreate(onSubscribe); + + BusyObserver busySubscriber = new BusyObserver(); + Observer<String> aw = serializedObserver(busySubscriber); + + w.subscribe(aw); + onSubscribe.waitToFinish(); + busySubscriber.terminalEvent.await(); + + System.out.println("OnSubscribe maxConcurrentThreads: " + onSubscribe.maxConcurrentThreads.get() + " Observer maxConcurrentThreads: " + busySubscriber.maxConcurrentThreads.get()); + + // we can't know how many onNext calls will occur since they each run on a separate thread + // that depends on thread scheduling so 0, 1, 2 and 3 are all valid options + // assertEquals(3, busySubscriber.onNextCount.get()); + assertTrue(busySubscriber.onNextCount.get() < 4); + assertTrue(busySubscriber.onError); + // no onComplete because onError was invoked + assertFalse(busySubscriber.onComplete); + // non-deterministic because unsubscribe happens after 'waitToFinish' releases + // so commenting out for now as this is not a critical thing to test here + //verify(s, times(1)).unsubscribe(); + + // we can have concurrency ... + assertTrue(onSubscribe.maxConcurrentThreads.get() > 1); + // ... but the onNext execution should be single threaded + assertEquals(1, busySubscriber.maxConcurrentThreads.get()); + } + + @Test + public void multiThreadedWithNPEinMiddle() { + int n = 10; + for (int i = 0; i < n; i++) { + TestMultiThreadedObservable onSubscribe = new TestMultiThreadedObservable("one", "two", "three", null, + "four", "five", "six", "seven", "eight", "nine"); + Observable<String> w = Observable.unsafeCreate(onSubscribe); + + BusyObserver busySubscriber = new BusyObserver(); + Observer<String> aw = serializedObserver(busySubscriber); + + w.subscribe(aw); + onSubscribe.waitToFinish(); + + System.out.println("OnSubscribe maxConcurrentThreads: " + onSubscribe.maxConcurrentThreads.get() + " Observer maxConcurrentThreads: " + busySubscriber.maxConcurrentThreads.get()); + + // we can have concurrency ... + assertTrue(onSubscribe.maxConcurrentThreads.get() > 1); + // ... but the onNext execution should be single threaded + assertEquals(1, busySubscriber.maxConcurrentThreads.get()); + + // this should not be the full number of items since the error should stop it before it completes all 9 + System.out.println("onNext count: " + busySubscriber.onNextCount.get()); + assertFalse(busySubscriber.onComplete); + assertTrue(busySubscriber.onError); + assertTrue(busySubscriber.onNextCount.get() < 9); + // no onComplete because onError was invoked + // non-deterministic because unsubscribe happens after 'waitToFinish' releases + // so commenting out for now as this is not a critical thing to test here + // verify(s, times(1)).unsubscribe(); + } + } + + /** + * A non-realistic use case that tries to expose thread-safety issues by throwing lots of out-of-order + * events on many threads. + */ + @Test + public void runOutOfOrderConcurrencyTest() { + ExecutorService tp = Executors.newFixedThreadPool(20); + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + TestConcurrencySubscriber tw = new TestConcurrencySubscriber(); + // we need Synchronized + SafeObserver to handle synchronization plus life-cycle + Observer<String> w = serializedObserver(new SafeObserver<>(tw)); + + Future<?> f1 = tp.submit(new OnNextThread(w, 12000)); + Future<?> f2 = tp.submit(new OnNextThread(w, 5000)); + Future<?> f3 = tp.submit(new OnNextThread(w, 75000)); + Future<?> f4 = tp.submit(new OnNextThread(w, 13500)); + Future<?> f5 = tp.submit(new OnNextThread(w, 22000)); + Future<?> f6 = tp.submit(new OnNextThread(w, 15000)); + Future<?> f7 = tp.submit(new OnNextThread(w, 7500)); + Future<?> f8 = tp.submit(new OnNextThread(w, 23500)); + + Future<?> f10 = tp.submit(new CompletionThread(w, TestConcurrencySubscriberEvent.onComplete, f1, f2, f3, f4)); + try { + Thread.sleep(1); + } catch (InterruptedException e) { + // ignore + } + Future<?> f11 = tp.submit(new CompletionThread(w, TestConcurrencySubscriberEvent.onComplete, f4, f6, f7)); + Future<?> f12 = tp.submit(new CompletionThread(w, TestConcurrencySubscriberEvent.onComplete, f4, f6, f7)); + Future<?> f13 = tp.submit(new CompletionThread(w, TestConcurrencySubscriberEvent.onComplete, f4, f6, f7)); + Future<?> f14 = tp.submit(new CompletionThread(w, TestConcurrencySubscriberEvent.onComplete, f4, f6, f7)); + // // the next 4 onError events should wait on same as f10 + Future<?> f15 = tp.submit(new CompletionThread(w, TestConcurrencySubscriberEvent.onError, f1, f2, f3, f4)); + Future<?> f16 = tp.submit(new CompletionThread(w, TestConcurrencySubscriberEvent.onError, f1, f2, f3, f4)); + Future<?> f17 = tp.submit(new CompletionThread(w, TestConcurrencySubscriberEvent.onError, f1, f2, f3, f4)); + Future<?> f18 = tp.submit(new CompletionThread(w, TestConcurrencySubscriberEvent.onError, f1, f2, f3, f4)); + + waitOnThreads(f1, f2, f3, f4, f5, f6, f7, f8, f10, f11, f12, f13, f14, f15, f16, f17, f18); + @SuppressWarnings("unused") + int numNextEvents = tw.assertEvents(null); // no check of type since we don't want to test barging results here, just interleaving behavior + // System.out.println("Number of events executed: " + numNextEvents); + + for (int i = 0; i < errors.size(); i++) { + TestHelper.assertUndeliverable(errors, i, RuntimeException.class); + } + } catch (Throwable e) { + fail("Concurrency test failed: " + e.getMessage()); + e.printStackTrace(); + } finally { + tp.shutdown(); + try { + tp.awaitTermination(5000, TimeUnit.MILLISECONDS); + } catch (InterruptedException e) { + e.printStackTrace(); + } + + RxJavaPlugins.reset(); + } + } + + @Test + public void runConcurrencyTest() { + ExecutorService tp = Executors.newFixedThreadPool(20); + try { + TestConcurrencySubscriber tw = new TestConcurrencySubscriber(); + // we need Synchronized + SafeObserver to handle synchronization plus life-cycle + Observer<String> w = serializedObserver(new SafeObserver<>(tw)); + w.onSubscribe(Disposable.empty()); + + Future<?> f1 = tp.submit(new OnNextThread(w, 12000)); + Future<?> f2 = tp.submit(new OnNextThread(w, 5000)); + Future<?> f3 = tp.submit(new OnNextThread(w, 75000)); + Future<?> f4 = tp.submit(new OnNextThread(w, 13500)); + Future<?> f5 = tp.submit(new OnNextThread(w, 22000)); + Future<?> f6 = tp.submit(new OnNextThread(w, 15000)); + Future<?> f7 = tp.submit(new OnNextThread(w, 7500)); + Future<?> f8 = tp.submit(new OnNextThread(w, 23500)); + + // 12000 + 5000 + 75000 + 13500 + 22000 + 15000 + 7500 + 23500 = 173500 + + Future<?> f10 = tp.submit(new CompletionThread(w, TestConcurrencySubscriberEvent.onComplete, f1, f2, f3, f4, f5, f6, f7, f8)); + try { + Thread.sleep(1); + } catch (InterruptedException e) { + // ignore + } + + waitOnThreads(f1, f2, f3, f4, f5, f6, f7, f8, f10); + int numNextEvents = tw.assertEvents(null); // no check of type since we don't want to test barging results here, just interleaving behavior + assertEquals(173500, numNextEvents); + // System.out.println("Number of events executed: " + numNextEvents); + } catch (Throwable e) { + fail("Concurrency test failed: " + e.getMessage()); + e.printStackTrace(); + } finally { + tp.shutdown(); + try { + tp.awaitTermination(25000, TimeUnit.MILLISECONDS); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + } + + /** + * Test that a notification does not get delayed in the queue waiting for the next event to push it through. + * + * @throws InterruptedException if the await is interrupted + */ + @Ignore("this is non-deterministic ... haven't figured out what's wrong with the test yet (benjchristensen: July 2014)") + @Test + public void notificationDelay() throws InterruptedException { + ExecutorService tp1 = Executors.newFixedThreadPool(1); + ExecutorService tp2 = Executors.newFixedThreadPool(1); + try { + int n = 10; + for (int i = 0; i < n; i++) { + final CountDownLatch firstOnNext = new CountDownLatch(1); + final CountDownLatch onNextCount = new CountDownLatch(2); + final CountDownLatch latch = new CountDownLatch(1); + final CountDownLatch running = new CountDownLatch(2); + + TestObserverEx<String> to = new TestObserverEx<>(new DefaultObserver<String>() { + + @Override + public void onComplete() { + + } + + @Override + public void onError(Throwable e) { + + } + + @Override + public void onNext(String t) { + firstOnNext.countDown(); + // force it to take time when delivering so the second one is enqueued + try { + latch.await(); + } catch (InterruptedException e) { + } + } + + }); + Observer<String> o = serializedObserver(to); + + Future<?> f1 = tp1.submit(new OnNextThread(o, 1, onNextCount, running)); + Future<?> f2 = tp2.submit(new OnNextThread(o, 1, onNextCount, running)); + + running.await(); // let one of the OnNextThread actually run before proceeding + + firstOnNext.await(); + + Thread t1 = to.lastThread(); + System.out.println("first onNext on thread: " + t1); + + latch.countDown(); + + waitOnThreads(f1, f2); + // not completed yet + + assertEquals(2, to.values().size()); + + Thread t2 = to.lastThread(); + System.out.println("second onNext on thread: " + t2); + + assertSame(t1, t2); + + System.out.println(to.values()); + o.onComplete(); + System.out.println(to.values()); + } + } finally { + tp1.shutdown(); + tp2.shutdown(); + } + } + + /** + * Demonstrates thread starvation problem. + * + * No solution on this for now. Trade-off in this direction as per https://github.com/ReactiveX/RxJava/issues/998#issuecomment-38959474 + * Probably need backpressure for this to work + * + * When using SynchronizedSubscriber we get this output: + * + * {@code p1: 18 p2: 68 =>} should be close to each other unless we have thread starvation + * + * When using SerializedObserver we get: + * + * {@code p1: 1 p2: 2445261 =>} should be close to each other unless we have thread starvation + * + * This demonstrates how SynchronizedSubscriber balances back and forth better, and blocks emission. + * The real issue in this example is the async buffer-bloat, so we need backpressure. + * + * + * @throws InterruptedException if the await is interrupted + */ + @Ignore("Demonstrates thread starvation problem. Read JavaDoc") + @Test + public void threadStarvation() throws InterruptedException { + + TestObserver<String> to = new TestObserver<>(new DefaultObserver<String>() { + + @Override + public void onComplete() { + + } + + @Override + public void onError(Throwable e) { + + } + + @Override + public void onNext(String t) { + // force it to take time when delivering + try { + Thread.sleep(1); + } catch (InterruptedException e) { + } + } + + }); + final Observer<String> o = serializedObserver(to); + + AtomicInteger p1 = new AtomicInteger(); + AtomicInteger p2 = new AtomicInteger(); + + o.onSubscribe(Disposable.empty()); + DisposableObserver<String> as1 = new DisposableObserver<String>() { + @Override + public void onNext(String t) { + o.onNext(t); + } + + @Override + public void onError(Throwable t) { + RxJavaPlugins.onError(t); + } + + @Override + public void onComplete() { + + } + }; + + DisposableObserver<String> as2 = new DisposableObserver<String>() { + @Override + public void onNext(String t) { + o.onNext(t); + } + + @Override + public void onError(Throwable t) { + RxJavaPlugins.onError(t); + } + + @Override + public void onComplete() { + + } + }; + + infinite(p1).subscribe(as1); + infinite(p2).subscribe(as2); + + Thread.sleep(100); + + System.out.println("p1: " + p1.get() + " p2: " + p2.get() + " => should be close to each other unless we have thread starvation"); + assertEquals(p1.get(), p2.get(), 10000); // fairly distributed within 10000 of each other + + as1.dispose(); + as2.dispose(); + } + + private static void waitOnThreads(Future<?>... futures) { + for (Future<?> f : futures) { + try { + f.get(20, TimeUnit.SECONDS); + } catch (Throwable e) { + System.err.println("Failed while waiting on future."); + e.printStackTrace(); + } + } + } + + private static Observable<String> infinite(final AtomicInteger produced) { + return Observable.unsafeCreate(new ObservableSource<String>() { + + @Override + public void subscribe(Observer<? super String> observer) { + Disposable bs = Disposable.empty(); + observer.onSubscribe(bs); + while (!bs.isDisposed()) { + observer.onNext("onNext"); + produced.incrementAndGet(); + } + } + + }).subscribeOn(Schedulers.newThread()); + } + + /** + * A thread that will pass data to onNext. + */ + public static class OnNextThread implements Runnable { + + private final CountDownLatch latch; + private final Observer<String> observer; + private final int numStringsToSend; + final AtomicInteger produced; + private final CountDownLatch running; + + OnNextThread(Observer<String> observer, int numStringsToSend, CountDownLatch latch, CountDownLatch running) { + this(observer, numStringsToSend, new AtomicInteger(), latch, running); + } + + OnNextThread(Observer<String> observer, int numStringsToSend, AtomicInteger produced) { + this(observer, numStringsToSend, produced, null, null); + } + + OnNextThread(Observer<String> observer, int numStringsToSend, AtomicInteger produced, CountDownLatch latch, CountDownLatch running) { + this.observer = observer; + this.numStringsToSend = numStringsToSend; + this.produced = produced; + this.latch = latch; + this.running = running; + } + + OnNextThread(Observer<String> observer, int numStringsToSend) { + this(observer, numStringsToSend, new AtomicInteger()); + } + + @Override + public void run() { + if (running != null) { + running.countDown(); + } + for (int i = 0; i < numStringsToSend; i++) { + observer.onNext(Thread.currentThread().getId() + "-" + i); + if (latch != null) { + latch.countDown(); + } + produced.incrementAndGet(); + } + } + } + + /** + * A thread that will call onError or onNext. + */ + public static class CompletionThread implements Runnable { + + private final Observer<String> observer; + private final TestConcurrencySubscriberEvent event; + private final Future<?>[] waitOnThese; + + CompletionThread(Observer<String> Observer, TestConcurrencySubscriberEvent event, Future<?>... waitOnThese) { + this.observer = Observer; + this.event = event; + this.waitOnThese = waitOnThese; + } + + @Override + public void run() { + /* if we have 'waitOnThese' futures, we'll wait on them before proceeding */ + if (waitOnThese != null) { + for (Future<?> f : waitOnThese) { + try { + f.get(); + } catch (Throwable e) { + System.err.println("Error while waiting on future in CompletionThread"); + } + } + } + + /* send the event */ + if (event == TestConcurrencySubscriberEvent.onError) { + observer.onError(new RuntimeException("mocked exception")); + } else if (event == TestConcurrencySubscriberEvent.onComplete) { + observer.onComplete(); + + } else { + throw new IllegalArgumentException("Expecting either onError or onComplete"); + } + } + } + + enum TestConcurrencySubscriberEvent { + onComplete, onError, onNext + } + + private static class TestConcurrencySubscriber extends DefaultObserver<String> { + + /** + * used to store the order and number of events received. + */ + private final LinkedBlockingQueue<TestConcurrencySubscriberEvent> events = new LinkedBlockingQueue<>(); + private final int waitTime; + + @SuppressWarnings("unused") + TestConcurrencySubscriber(int waitTimeInNext) { + this.waitTime = waitTimeInNext; + } + + TestConcurrencySubscriber() { + this.waitTime = 0; + } + + @Override + public void onComplete() { + events.add(TestConcurrencySubscriberEvent.onComplete); + } + + @Override + public void onError(Throwable e) { + events.add(TestConcurrencySubscriberEvent.onError); + } + + @Override + public void onNext(String args) { + events.add(TestConcurrencySubscriberEvent.onNext); + // do some artificial work to make the thread scheduling/timing vary + int s = 0; + for (int i = 0; i < 20; i++) { + s += s * i; + } + + if (waitTime > 0) { + try { + Thread.sleep(waitTime); + } catch (InterruptedException e) { + // ignore + } + } + } + + /** + * Assert the order of events is correct and return the number of onNext executions. + * + * @param expectedEndingEvent the expected last event + * @return int count of onNext calls + * @throws IllegalStateException + * If order of events was invalid. + */ + public int assertEvents(TestConcurrencySubscriberEvent expectedEndingEvent) throws IllegalStateException { + int nextCount = 0; + boolean finished = false; + for (TestConcurrencySubscriberEvent e : events) { + if (e == TestConcurrencySubscriberEvent.onNext) { + if (finished) { + // already finished, we shouldn't get this again + throw new IllegalStateException("Received onNext but we're already finished."); + } + nextCount++; + } else if (e == TestConcurrencySubscriberEvent.onError) { + if (finished) { + // already finished, we shouldn't get this again + throw new IllegalStateException("Received onError but we're already finished."); + } + if (expectedEndingEvent != null && TestConcurrencySubscriberEvent.onError != expectedEndingEvent) { + throw new IllegalStateException("Received onError ending event but expected " + expectedEndingEvent); + } + finished = true; + } else if (e == TestConcurrencySubscriberEvent.onComplete) { + if (finished) { + // already finished, we shouldn't get this again + throw new IllegalStateException("Received onComplete but we're already finished."); + } + if (expectedEndingEvent != null && TestConcurrencySubscriberEvent.onComplete != expectedEndingEvent) { + throw new IllegalStateException("Received onComplete ending event but expected " + expectedEndingEvent); + } + finished = true; + } + } + + return nextCount; + } + + } + + /** + * This spawns a single thread for the subscribe execution. + */ + private static class TestSingleThreadedObservable implements ObservableSource<String> { + + final String[] values; + private Thread t; + + TestSingleThreadedObservable(final String... values) { + this.values = values; + + } + + @Override + public void subscribe(final Observer<? super String> observer) { + observer.onSubscribe(Disposable.empty()); + System.out.println("TestSingleThreadedObservable subscribed to ..."); + t = new Thread(new Runnable() { + + @Override + public void run() { + try { + System.out.println("running TestSingleThreadedObservable thread"); + for (String s : values) { + System.out.println("TestSingleThreadedObservable onNext: " + s); + observer.onNext(s); + } + observer.onComplete(); + } catch (Throwable e) { + throw new RuntimeException(e); + } + } + + }); + System.out.println("starting TestSingleThreadedObservable thread"); + t.start(); + System.out.println("done starting TestSingleThreadedObservable thread"); + } + + public void waitToFinish() { + try { + t.join(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + + } + + /** + * This spawns a thread for the subscription, then a separate thread for each onNext call. + */ + private static class TestMultiThreadedObservable implements ObservableSource<String> { + + final String[] values; + Thread t; + AtomicInteger threadsRunning = new AtomicInteger(); + AtomicInteger maxConcurrentThreads = new AtomicInteger(); + ExecutorService threadPool; + + TestMultiThreadedObservable(String... values) { + this.values = values; + this.threadPool = Executors.newCachedThreadPool(); + } + + @Override + public void subscribe(final Observer<? super String> observer) { + observer.onSubscribe(Disposable.empty()); + final NullPointerException npe = new NullPointerException(); + System.out.println("TestMultiThreadedObservable subscribed to ..."); + t = new Thread(new Runnable() { + + @Override + public void run() { + try { + System.out.println("running TestMultiThreadedObservable thread"); + int j = 0; + for (final String s : values) { + final int fj = ++j; + threadPool.execute(new Runnable() { + + @Override + public void run() { + threadsRunning.incrementAndGet(); + try { + // perform onNext call + System.out.println("TestMultiThreadedObservable onNext: " + s + " on thread " + Thread.currentThread().getName()); + if (s == null) { + // force an error + throw npe; + } else { + // allow the exception to queue up + int sleep = (fj % 3) * 10; + if (sleep != 0) { + Thread.sleep(sleep); + } + } + observer.onNext(s); + // capture 'maxThreads' + int concurrentThreads = threadsRunning.get(); + int maxThreads = maxConcurrentThreads.get(); + if (concurrentThreads > maxThreads) { + maxConcurrentThreads.compareAndSet(maxThreads, concurrentThreads); + } + } catch (Throwable e) { + observer.onError(e); + } finally { + threadsRunning.decrementAndGet(); + } + } + }); + } + // we are done spawning threads + threadPool.shutdown(); + } catch (Throwable e) { + throw new RuntimeException(e); + } + + // wait until all threads are done, then mark it as COMPLETED + try { + // wait for all the threads to finish + if (!threadPool.awaitTermination(5, TimeUnit.SECONDS)) { + System.out.println("Threadpool did not terminate in time."); + } + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + observer.onComplete(); + } + }); + System.out.println("starting TestMultiThreadedObservable thread"); + t.start(); + System.out.println("done starting TestMultiThreadedObservable thread"); + } + + public void waitToFinish() { + try { + t.join(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + } + + static class BusyObserver extends DefaultObserver<String> { + volatile boolean onComplete; + volatile boolean onError; + AtomicInteger onNextCount = new AtomicInteger(); + AtomicInteger threadsRunning = new AtomicInteger(); + AtomicInteger maxConcurrentThreads = new AtomicInteger(); + final CountDownLatch terminalEvent = new CountDownLatch(1); + + @Override + public void onComplete() { + threadsRunning.incrementAndGet(); + try { + onComplete = true; + } finally { + captureMaxThreads(); + threadsRunning.decrementAndGet(); + terminalEvent.countDown(); + } + } + + @Override + public void onError(Throwable e) { + System.out.println(">>>>>>>>>>>>>>>>>>>> onError received: " + e); + threadsRunning.incrementAndGet(); + try { + onError = true; + } finally { + captureMaxThreads(); + threadsRunning.decrementAndGet(); + terminalEvent.countDown(); + } + } + + @Override + public void onNext(String args) { + threadsRunning.incrementAndGet(); + try { + onNextCount.incrementAndGet(); + try { + // simulate doing something computational + Thread.sleep(100); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } finally { + // capture 'maxThreads' + captureMaxThreads(); + threadsRunning.decrementAndGet(); + } + } + + protected void captureMaxThreads() { + int concurrentThreads = threadsRunning.get(); + int maxThreads = maxConcurrentThreads.get(); + if (concurrentThreads > maxThreads) { + maxConcurrentThreads.compareAndSet(maxThreads, concurrentThreads); + if (concurrentThreads > 1) { + new RuntimeException("should not be greater than 1").printStackTrace(); + } + } + } + + } + + @Test + public void errorReentry() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + final AtomicReference<Observer<Integer>> serial = new AtomicReference<>(); + + TestObserver<Integer> to = new TestObserver<Integer>() { + @Override + public void onNext(Integer v) { + serial.get().onError(new TestException()); + serial.get().onError(new TestException()); + super.onNext(v); + } + }; + SerializedObserver<Integer> sobs = new SerializedObserver<>(to); + sobs.onSubscribe(Disposable.empty()); + serial.set(sobs); + + sobs.onNext(1); + + to.assertValue(1); + to.assertError(TestException.class); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void completeReentry() { + final AtomicReference<Observer<Integer>> serial = new AtomicReference<>(); + + TestObserver<Integer> to = new TestObserver<Integer>() { + @Override + public void onNext(Integer v) { + serial.get().onComplete(); + serial.get().onComplete(); + super.onNext(v); + } + }; + SerializedObserver<Integer> sobs = new SerializedObserver<>(to); + sobs.onSubscribe(Disposable.empty()); + serial.set(sobs); + + sobs.onNext(1); + + to.assertValue(1); + to.assertComplete(); + to.assertNoErrors(); + } + + @Test + public void dispose() { + TestObserver<Integer> to = new TestObserver<>(); + + SerializedObserver<Integer> so = new SerializedObserver<>(to); + + Disposable d = Disposable.empty(); + + so.onSubscribe(d); + + assertFalse(so.isDisposed()); + + to.dispose(); + + assertTrue(so.isDisposed()); + + assertTrue(d.isDisposed()); + } + + @Test + public void onCompleteRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + TestObserver<Integer> to = new TestObserver<>(); + + final SerializedObserver<Integer> so = new SerializedObserver<>(to); + + Disposable d = Disposable.empty(); + + so.onSubscribe(d); + + Runnable r = new Runnable() { + @Override + public void run() { + so.onComplete(); + } + }; + + TestHelper.race(r, r); + + to.awaitDone(5, TimeUnit.SECONDS) + .assertResult(); + } + + } + + @Test + public void onNextOnCompleteRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + TestObserver<Integer> to = new TestObserver<>(); + + final SerializedObserver<Integer> so = new SerializedObserver<>(to); + + Disposable d = Disposable.empty(); + + so.onSubscribe(d); + + Runnable r1 = new Runnable() { + @Override + public void run() { + so.onComplete(); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + so.onNext(1); + } + }; + + TestHelper.race(r1, r2); + + to.awaitDone(5, TimeUnit.SECONDS) + .assertNoErrors() + .assertComplete(); + + assertTrue(to.values().size() <= 1); + } + + } + + @Test + public void onNextOnErrorRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + TestObserver<Integer> to = new TestObserver<>(); + + final SerializedObserver<Integer> so = new SerializedObserver<>(to); + + Disposable d = Disposable.empty(); + + so.onSubscribe(d); + + final Throwable ex = new TestException(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + so.onError(ex); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + so.onNext(1); + } + }; + + TestHelper.race(r1, r2); + + to.awaitDone(5, TimeUnit.SECONDS) + .assertError(ex) + .assertNotComplete(); + + assertTrue(to.values().size() <= 1); + } + + } + + @Test + public void onNextOnErrorRaceDelayError() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + TestObserver<Integer> to = new TestObserver<>(); + + final SerializedObserver<Integer> so = new SerializedObserver<>(to, true); + + Disposable d = Disposable.empty(); + + so.onSubscribe(d); + + final Throwable ex = new TestException(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + so.onError(ex); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + so.onNext(1); + } + }; + + TestHelper.race(r1, r2); + + to.awaitDone(5, TimeUnit.SECONDS) + .assertError(ex) + .assertNotComplete(); + + assertTrue(to.values().size() <= 1); + } + + } + + @Test + public void startOnce() { + + List<Throwable> error = TestHelper.trackPluginErrors(); + + try { + TestObserver<Integer> to = new TestObserver<>(); + + final SerializedObserver<Integer> so = new SerializedObserver<>(to); + + so.onSubscribe(Disposable.empty()); + + Disposable d = Disposable.empty(); + + so.onSubscribe(d); + + assertTrue(d.isDisposed()); + + TestHelper.assertError(error, 0, IllegalStateException.class, "Disposable already set!"); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void onCompleteOnErrorRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + TestObserverEx<Integer> to = new TestObserverEx<>(); + + final SerializedObserver<Integer> so = new SerializedObserver<>(to); + + Disposable d = Disposable.empty(); + + so.onSubscribe(d); + + final Throwable ex = new TestException(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + so.onError(ex); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + so.onComplete(); + } + }; + + TestHelper.race(r1, r2); + + to.awaitDone(5, TimeUnit.SECONDS); + + if (to.completions() != 0) { + to.assertResult(); + } else { + to.assertFailure(TestException.class).assertError(ex); + } + + for (Throwable e : errors) { + assertTrue(e.toString(), e.getCause() instanceof TestException); + } + } finally { + RxJavaPlugins.reset(); + } + } + + } + + @Test + public void nullOnNext() { + + TestObserverEx<Integer> to = new TestObserverEx<>(); + + final SerializedObserver<Integer> so = new SerializedObserver<>(to); + + Disposable d = Disposable.empty(); + + so.onSubscribe(d); + + so.onNext(null); + + to.assertFailureAndMessage(NullPointerException.class, ExceptionHelper.nullWarning("onNext called with a null value.")); + } + + @Test + @SuppressUndeliverable + public void onErrorQueuedUp() { + AtomicReference<SerializedObserver<Integer>> soRef = new AtomicReference<>(); + TestObserverEx<Integer> to = new TestObserverEx<Integer>() { + @Override + public void onNext(Integer t) { + super.onNext(t); + soRef.get().onNext(2); + soRef.get().onError(new TestException()); + } + }; + + final SerializedObserver<Integer> so = new SerializedObserver<>(to, true); + soRef.set(so); + + Disposable d = Disposable.empty(); + + so.onSubscribe(d); + + so.onNext(1); + + to.assertFailure(TestException.class, 1, 2); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/observers/TestObserverTest.java b/src/test/java/io/reactivex/rxjava3/observers/TestObserverTest.java new file mode 100644 index 0000000000..f2d5f206c3 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/observers/TestObserverTest.java @@ -0,0 +1,1245 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.observers; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +import java.io.IOException; +import java.util.*; +import java.util.concurrent.TimeUnit; + +import org.junit.Test; +import org.junit.function.ThrowingRunnable; +import org.mockito.InOrder; +import org.reactivestreams.Subscriber; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.core.Observer; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.Predicate; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.internal.operators.observable.ObservableScalarXMap.ScalarDisposable; +import io.reactivex.rxjava3.processors.PublishProcessor; +import io.reactivex.rxjava3.schedulers.Schedulers; +import io.reactivex.rxjava3.subscribers.TestSubscriber; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class TestObserverTest extends RxJavaTest { + + static void assertThrowsWithMessage(String message, Class<? extends Throwable> clazz, ThrowingRunnable run) { + assertEquals(message, assertThrows(clazz, run).getMessage()); + } + + static void assertThrowsWithMessageMatchRegex(String regex, Class<? extends Throwable> clazz, ThrowingRunnable run) { + assertTrue(assertThrows(clazz, run).getMessage().matches(regex)); + } + + private static final String ASSERT_MESSAGE_REGEX = "\nexpected: (.*)\n\\s*got: (.*)"; + + @Test + public void assertTestObserver() { + Flowable<Integer> oi = Flowable.fromIterable(Arrays.asList(1, 2)); + TestSubscriber<Integer> subscriber = new TestSubscriber<>(); + oi.subscribe(subscriber); + + subscriber.assertValues(1, 2); + subscriber.assertValueCount(2); + subscriber.assertComplete().assertNoErrors(); + } + + @Test + public void assertNotMatchCount() { + assertThrows(AssertionError.class, () -> { + Flowable<Integer> oi = Flowable.fromIterable(Arrays.asList(1, 2)); + TestSubscriber<Integer> subscriber = new TestSubscriber<>(); + oi.subscribe(subscriber); + + subscriber.assertValue(1); + subscriber.assertValueCount(2); + subscriber.assertComplete().assertNoErrors(); + }); + } + + @Test + public void assertNotMatchValue() { + assertThrows(AssertionError.class, () -> { + Flowable<Integer> oi = Flowable.fromIterable(Arrays.asList(1, 2)); + TestSubscriber<Integer> subscriber = new TestSubscriber<>(); + oi.subscribe(subscriber); + + subscriber.assertValues(1, 3); + subscriber.assertValueCount(2); + subscriber.assertComplete().assertNoErrors(); + }); + } + + @Test + public void assertTerminalEventNotReceived() { + assertThrows(AssertionError.class, () -> { + PublishProcessor<Integer> p = PublishProcessor.create(); + TestSubscriber<Integer> subscriber = new TestSubscriber<>(); + p.subscribe(subscriber); + + p.onNext(1); + p.onNext(2); + + subscriber.assertValues(1, 2); + subscriber.assertValueCount(2); + subscriber.assertComplete().assertNoErrors(); + }); + } + + @Test + public void wrappingMock() { + Flowable<Integer> oi = Flowable.fromIterable(Arrays.asList(1, 2)); + + Subscriber<Integer> mockSubscriber = TestHelper.mockSubscriber(); + + oi.subscribe(new TestSubscriber<>(mockSubscriber)); + + InOrder inOrder = inOrder(mockSubscriber); + inOrder.verify(mockSubscriber, times(1)).onNext(1); + inOrder.verify(mockSubscriber, times(1)).onNext(2); + inOrder.verify(mockSubscriber, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void wrappingMockWhenUnsubscribeInvolved() { + Flowable<Integer> oi = Flowable.fromIterable(Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9)).take(2); + Subscriber<Integer> mockSubscriber = TestHelper.mockSubscriber(); + oi.subscribe(new TestSubscriber<>(mockSubscriber)); + + InOrder inOrder = inOrder(mockSubscriber); + inOrder.verify(mockSubscriber, times(1)).onNext(1); + inOrder.verify(mockSubscriber, times(1)).onNext(2); + inOrder.verify(mockSubscriber, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void errorSwallowed() { + Flowable.error(new RuntimeException()).subscribe(new TestSubscriber<>()); + } + + @Test + public void nullExpected() { + TestSubscriber<Integer> ts = new TestSubscriber<>(); + ts.onNext(1); + + try { + ts.assertValue((Integer) null); + } catch (AssertionError ex) { + // this is expected + return; + } + fail("Null element check assertion didn't happen!"); + } + + @Test + public void nullActual() { + TestSubscriber<Integer> ts = new TestSubscriber<>(); + ts.onNext(null); + + try { + ts.assertValue(1); + } catch (AssertionError ex) { + // this is expected + return; + } + fail("Null element check assertion didn't happen!"); + } + + @Test + public void createDelegate() { + TestObserver<Integer> to1 = TestObserver.create(); + + TestObserver<Integer> to = TestObserver.create(to1); + + assertFalse(to.hasSubscription()); + + to.onSubscribe(Disposable.empty()); + + assertTrue(to.hasSubscription()); + + assertFalse(to.isDisposed()); + + to.onNext(1); + to.onError(new TestException()); + to.onComplete(); + + to1.assertValue(1).assertError(TestException.class).assertComplete(); + + to.dispose(); + + assertTrue(to.isDisposed()); + + try { + to.assertNoValues(); + throw new RuntimeException("Should have thrown"); + } catch (AssertionError exc) { + // expected + } + + try { + to.assertValueCount(0); + throw new RuntimeException("Should have thrown"); + } catch (AssertionError exc) { + // expected + } + + to.assertValueSequence(Collections.singletonList(1)); + + try { + to.assertValueSequence(Collections.singletonList(2)); + throw new RuntimeException("Should have thrown"); + } catch (AssertionError exc) { + // expected + } + } + + @Test + public void assertError() { + TestObserver<Integer> to = TestObserver.create(); + + try { + to.assertError(TestException.class); + throw new RuntimeException("Should have thrown"); + } catch (AssertionError ex) { + // expected + } + + try { + to.assertError(new TestException()); + throw new RuntimeException("Should have thrown"); + } catch (AssertionError ex) { + // expected + } + + try { + to.assertError(Functions.<Throwable>alwaysTrue()); + throw new RuntimeException("Should have thrown"); + } catch (AssertionError ex) { + // expected + } + + try { + to.assertSubscribed(); + throw new RuntimeException("Should have thrown"); + } catch (AssertionError exc) { + // expected + } + + to.onSubscribe(Disposable.empty()); + + to.assertSubscribed(); + + to.assertNoErrors(); + + TestException ex = new TestException("Forced failure"); + + to.onError(ex); + + to.assertError(ex); + + to.assertError(TestException.class); + + to.assertError(Functions.<Throwable>alwaysTrue()); + + to.assertError(new Predicate<Throwable>() { + @Override + public boolean test(Throwable t) throws Exception { + return t.getMessage() != null && t.getMessage().contains("Forced"); + } + }); + + try { + to.assertError(new RuntimeException()); + throw new RuntimeException("Should have thrown"); + } catch (AssertionError exc) { + // expected + } + + try { + to.assertError(IOException.class); + throw new RuntimeException("Should have thrown"); + } catch (AssertionError exc) { + // expected + } + + try { + to.assertError(Functions.<Throwable>alwaysFalse()); + throw new RuntimeException("Should have thrown"); + } catch (AssertionError exc) { + // expected + } + + try { + to.assertNoErrors(); + throw new RuntimeException("Should have thrown"); + } catch (AssertionError exc) { + // expected + } + + to.assertValueCount(0); + + to.assertNoValues(); + } + + @Test + public void emptyObserverEnum() { + assertEquals(1, TestObserver.EmptyObserver.values().length); + assertNotNull(TestObserver.EmptyObserver.valueOf("INSTANCE")); + } + + @Test + public void valueAndClass() { + assertEquals("null", TestObserver.valueAndClass(null)); + assertEquals("1 (class: Integer)", TestObserver.valueAndClass(1)); + } + + @Test + public void assertFailure() { + TestObserver<Integer> to = TestObserver.create(); + + to.onSubscribe(Disposable.empty()); + + to.onError(new TestException("Forced failure")); + + to.assertFailure(TestException.class); + + to.onNext(1); + + to.assertFailure(TestException.class, 1); + } + + @Test + public void assertFuseable() { + TestObserver<Integer> to = TestObserver.create(); + + to.onSubscribe(Disposable.empty()); + + to = TestObserver.create(); + + to.onSubscribe(new ScalarDisposable<>(to, 1)); + } + + @Test + public void assertResult() { + TestObserver<Integer> to = TestObserver.create(); + + to.onSubscribe(Disposable.empty()); + + to.onComplete(); + + to.assertResult(); + + try { + to.assertResult(1); + throw new RuntimeException("Should have thrown"); + } catch (AssertionError ex) { + // expected + } + + to.onNext(1); + + to.assertResult(1); + + try { + to.assertResult(2); + throw new RuntimeException("Should have thrown"); + } catch (AssertionError ex) { + // expected + } + + try { + to.assertResult(); + throw new RuntimeException("Should have thrown"); + } catch (AssertionError ex) { + // expected + } + + } + + @Test + public void await() throws Exception { + TestObserver<Integer> to = TestObserver.create(); + + to.onSubscribe(Disposable.empty()); + + assertFalse(to.await(100, TimeUnit.MILLISECONDS)); + + to.awaitDone(100, TimeUnit.MILLISECONDS); + + assertTrue(to.isDisposed()); + + assertFalse(to.await(100, TimeUnit.MILLISECONDS)); + + to.assertNotComplete(); + to.assertNoErrors(); + + to.onComplete(); + + assertTrue(to.await(100, TimeUnit.MILLISECONDS)); + + to.await(); + + to.awaitDone(5, TimeUnit.SECONDS); + + to.assertComplete(); + to.assertNoErrors(); + + assertTrue(to.await(5, TimeUnit.SECONDS)); + + final TestObserver<Integer> to1 = TestObserver.create(); + + to1.onSubscribe(Disposable.empty()); + + Schedulers.single().scheduleDirect(new Runnable() { + @Override + public void run() { + to1.onComplete(); + } + }, 200, TimeUnit.MILLISECONDS); + + to1.await(); + } + + @Test + public void onNext() { + TestObserver<Integer> to = TestObserver.create(); + + to.onSubscribe(Disposable.empty()); + + assertEquals(0, to.values().size()); + + assertEquals(Collections.emptyList(), to.values()); + + to.onNext(1); + + assertEquals(Collections.singletonList(1), to.values()); + + to.dispose(); + + assertTrue(to.isDisposed()); + + to.assertValue(1); + + to.onComplete(); + } + + @Test + public void multipleTerminals() { + TestObserver<Integer> to = TestObserver.create(); + + to.onSubscribe(Disposable.empty()); + + to.assertNotComplete(); + + to.onComplete(); + + try { + to.assertNotComplete(); + throw new RuntimeException("Should have thrown"); + } catch (Throwable ex) { + // expected + } + + to.onComplete(); + + try { + to.assertComplete(); + throw new RuntimeException("Should have thrown"); + } catch (Throwable ex) { + // expected + } + + try { + to.assertNotComplete(); + throw new RuntimeException("Should have thrown"); + } catch (Throwable ex) { + // expected + } + } + + @Test + public void assertValue() { + TestObserver<Integer> to = TestObserver.create(); + + to.onSubscribe(Disposable.empty()); + + try { + to.assertValue(1); + throw new RuntimeException("Should have thrown"); + } catch (Throwable ex) { + // expected + } + + to.onNext(1); + + to.assertValue(1); + + try { + to.assertValue(2); + throw new RuntimeException("Should have thrown"); + } catch (Throwable ex) { + // expected + } + + to.onNext(2); + + try { + to.assertValue(1); + throw new RuntimeException("Should have thrown"); + } catch (Throwable ex) { + // expected + } + } + + @Test + public void onNextMisbehave() { + TestObserver<Integer> to = TestObserver.create(); + + to.onNext(1); + + to.assertError(IllegalStateException.class); + + to = TestObserver.create(); + + to.onSubscribe(Disposable.empty()); + + to.onNext(null); + + to.assertFailure(NullPointerException.class, (Integer)null); + } + + @Test + public void awaitTerminalEventInterrupt() { + final TestObserver<Integer> to = TestObserver.create(); + + to.onSubscribe(Disposable.empty()); + + Thread.currentThread().interrupt(); + + try { + to.awaitDone(5, TimeUnit.SECONDS); + } catch (RuntimeException allowed) { + assertTrue(allowed.toString(), allowed.getCause() instanceof InterruptedException); + } + + // FIXME ? catch consumes this flag + // assertTrue(Thread.interrupted()); + + Thread.currentThread().interrupt(); + + try { + to.awaitDone(5, TimeUnit.SECONDS); + } catch (RuntimeException allowed) { + assertTrue(allowed.toString(), allowed.getCause() instanceof InterruptedException); + } + + // FIXME ? catch consumes this flag + // assertTrue(Thread.interrupted()); + } + + @Test + public void assertTerminated2() { + TestObserver<Integer> to = TestObserver.create(); + + to.onSubscribe(Disposable.empty()); + + to.onError(new TestException()); + to.onError(new IOException()); + + try { + to.assertError(TestException.class); + throw new RuntimeException("Should have thrown"); + } catch (AssertionError ex) { + // expected + } + + to = TestObserver.create(); + + to.onSubscribe(Disposable.empty()); + + to.onError(new TestException()); + to.onComplete(); + } + + @Test + public void onSubscribe() { + TestObserver<Integer> to = TestObserver.create(); + + to.onSubscribe(null); + + to.assertError(NullPointerException.class); + + to = TestObserver.create(); + + to.onSubscribe(Disposable.empty()); + + Disposable d1 = Disposable.empty(); + + to.onSubscribe(d1); + + assertTrue(d1.isDisposed()); + + to.assertError(IllegalStateException.class); + + to = TestObserver.create(); + to.dispose(); + + d1 = Disposable.empty(); + + to.onSubscribe(d1); + + assertTrue(d1.isDisposed()); + + } + + @Test + public void assertValueSequence() { + TestObserver<Integer> to = TestObserver.create(); + + to.onSubscribe(Disposable.empty()); + + to.onNext(1); + to.onNext(2); + + try { + to.assertValueSequence(Collections.<Integer>emptyList()); + throw new RuntimeException("Should have thrown"); + } catch (AssertionError expected) { + assertTrue(expected.getMessage(), expected.getMessage().startsWith("More values received than expected (0)")); + } + + try { + to.assertValueSequence(Collections.singletonList(1)); + throw new RuntimeException("Should have thrown"); + } catch (AssertionError expected) { + assertTrue(expected.getMessage(), expected.getMessage().startsWith("More values received than expected (1)")); + } + + to.assertValueSequence(Arrays.asList(1, 2)); + + try { + to.assertValueSequence(Arrays.asList(1, 2, 3)); + throw new RuntimeException("Should have thrown"); + } catch (AssertionError expected) { + assertTrue(expected.getMessage(), expected.getMessage().startsWith("Fewer values received than expected (2)")); + } + } + + @Test + public void assertEmpty() { + TestObserver<Integer> to = new TestObserver<>(); + + try { + to.assertEmpty(); + throw new RuntimeException("Should have thrown!"); + } catch (AssertionError ex) { + // expected + } + + to.onSubscribe(Disposable.empty()); + + to.assertEmpty(); + + to.onNext(1); + + try { + to.assertEmpty(); + throw new RuntimeException("Should have thrown!"); + } catch (AssertionError ex) { + // expected + } + } + + @Test + public void awaitDoneTimed() { + TestObserver<Integer> to = new TestObserver<>(); + + Thread.currentThread().interrupt(); + + try { + to.awaitDone(5, TimeUnit.SECONDS); + } catch (RuntimeException ex) { + assertTrue(ex.toString(), ex.getCause() instanceof InterruptedException); + } + } + + @Test + public void assertErrorMultiple() { + TestObserver<Integer> to = new TestObserver<>(); + + TestException e = new TestException(); + to.onError(e); + to.onError(new TestException()); + + try { + to.assertError(TestException.class); + throw new RuntimeException("Should have thrown!"); + } catch (AssertionError ex) { + // expected + } + try { + to.assertError(e); + throw new RuntimeException("Should have thrown!"); + } catch (AssertionError ex) { + // expected + } + try { + to.assertError(Functions.<Throwable>alwaysTrue()); + throw new RuntimeException("Should have thrown!"); + } catch (AssertionError ex) { + // expected + } + } + + @Test + public void errorInPredicate() { + TestObserver<Object> to = new TestObserver<>(); + to.onError(new RuntimeException()); + try { + to.assertError(new Predicate<Throwable>() { + @Override + public boolean test(Throwable throwable) throws Exception { + throw new TestException(); + } + }); + } catch (TestException ex) { + // expected + return; + } + fail("Error in predicate but not thrown!"); + } + + @Test + public void assertComplete() { + TestObserver<Integer> to = new TestObserver<>(); + + to.onSubscribe(Disposable.empty()); + + try { + to.assertComplete(); + throw new RuntimeException("Should have thrown!"); + } catch (AssertionError ex) { + // expected + } + + to.onComplete(); + + to.assertComplete(); + + to.onComplete(); + + try { + to.assertComplete(); + throw new RuntimeException("Should have thrown!"); + } catch (AssertionError ex) { + // expected + } + } + + @Test + public void completeWithoutOnSubscribe() { + TestObserver<Integer> to = new TestObserver<>(); + + to.onComplete(); + + to.assertError(IllegalStateException.class); + } + + @Test + public void completeDelegateThrows() { + TestObserver<Integer> to = new TestObserver<>(new Observer<Integer>() { + + @Override + public void onSubscribe(Disposable d) { + + } + + @Override + public void onNext(Integer value) { + + } + + @Override + public void onError(Throwable e) { + throw new TestException(); + } + + @Override + public void onComplete() { + throw new TestException(); + } + + }); + + to.onSubscribe(Disposable.empty()); + + try { + to.onComplete(); + throw new RuntimeException("Should have thrown!"); + } catch (TestException ex) { + to.assertComplete().assertNoErrors(); + } + } + + @Test + public void errorDelegateThrows() { + TestObserver<Integer> to = new TestObserver<>(new Observer<Integer>() { + + @Override + public void onSubscribe(Disposable d) { + + } + + @Override + public void onNext(Integer value) { + + } + + @Override + public void onError(Throwable e) { + throw new TestException(); + } + + @Override + public void onComplete() { + throw new TestException(); + } + + }); + + to.onSubscribe(Disposable.empty()); + + try { + to.onError(new IOException()); + throw new RuntimeException("Should have thrown!"); + } catch (TestException ex) { + to.assertNotComplete().assertError(Throwable.class); + } + } + + @Test + public void completedMeansDisposed() { + // 2.0.2 - a terminated TestObserver no longer reports isDisposed + assertFalse(Observable.just(1) + .test() + .assertResult(1).isDisposed()); + } + + @Test + public void errorMeansDisposed() { + // 2.0.2 - a terminated TestObserver no longer reports isDisposed + assertFalse(Observable.error(new TestException()) + .test() + .assertFailure(TestException.class).isDisposed()); + } + + @Test + public void assertValuePredicateEmpty() { + assertThrowsWithMessage("No values (latch = 0, values = 0, errors = 0, completions = 1)", AssertionError.class, () -> { + TestObserver<Object> to = new TestObserver<>(); + + Observable.empty().subscribe(to); + + to.assertValue(new Predicate<Object>() { + @Override public boolean test(final Object o) throws Exception { + return false; + } + }); + }); + } + + @Test + public void assertValuePredicateMatch() { + TestObserver<Integer> to = new TestObserver<>(); + + Observable.just(1).subscribe(to); + + to.assertValue(new Predicate<Integer>() { + @Override public boolean test(final Integer o) throws Exception { + return o == 1; + } + }); + } + + @Test + public void assertValuePredicateNoMatch() { + assertThrowsWithMessage("Value 1 (class: Integer) at position 0 did not pass the predicate (latch = 0, values = 1, errors = 0, completions = 1)", AssertionError.class, () -> { + TestObserver<Integer> to = new TestObserver<>(); + + Observable.just(1).subscribe(to); + + to.assertValue(new Predicate<Integer>() { + @Override public boolean test(final Integer o) throws Exception { + return o != 1; + } + }); + }); + } + + @Test + public void assertValuePredicateMatchButMore() { + assertThrowsWithMessage("The first value passed the predicate but this consumer received more than one value (latch = 0, values = 2, errors = 0, completions = 1)", AssertionError.class, () -> { + TestObserver<Integer> to = new TestObserver<>(); + + Observable.just(1, 2).subscribe(to); + + to.assertValue(new Predicate<Integer>() { + @Override public boolean test(final Integer o) throws Exception { + return o == 1; + } + }); + }); + } + + @Test + public void assertValueAtPredicateEmpty() { + assertThrowsWithMessage("No values (latch = 0, values = 0, errors = 0, completions = 1)", AssertionError.class, () -> { + TestObserver<Object> to = new TestObserver<>(); + + Observable.empty().subscribe(to); + + to.assertValueAt(0, new Predicate<Object>() { + @Override public boolean test(final Object o) throws Exception { + return false; + } + }); + }); + } + + @Test + public void assertValueAtPredicateMatch() { + TestObserver<Integer> to = new TestObserver<>(); + + Observable.just(1, 2).subscribe(to); + + to.assertValueAt(1, new Predicate<Integer>() { + @Override public boolean test(final Integer o) throws Exception { + return o == 2; + } + }); + } + + @Test + public void assertValueAtPredicateNoMatch() { + assertThrowsWithMessage("Value 3 (class: Integer) at position 2 did not pass the predicate (latch = 0, values = 3, errors = 0, completions = 1)", AssertionError.class, () -> { + TestObserver<Integer> to = new TestObserver<>(); + + Observable.just(1, 2, 3).subscribe(to); + + to.assertValueAt(2, new Predicate<Integer>() { + @Override public boolean test(final Integer o) throws Exception { + return o != 3; + } + }); + }); + } + + @Test + public void assertValueAtInvalidIndex() { + assertThrowsWithMessage("Index 2 is out of range [0, 2) (latch = 0, values = 2, errors = 0, completions = 1)", AssertionError.class, () -> { + TestObserver<Integer> to = new TestObserver<>(); + + Observable.just(1, 2).subscribe(to); + + to.assertValueAt(2, new Predicate<Integer>() { + @Override public boolean test(final Integer o) throws Exception { + return o == 1; + } + }); + }); + } + + @Test + public void assertValueAtInvalidIndexNegative() { + assertThrowsWithMessage("Index -2 is out of range [0, 2) (latch = 0, values = 2, errors = 0, completions = 1)", AssertionError.class, () -> { + TestObserver<Integer> to = new TestObserver<>(); + + Observable.just(1, 2).subscribe(to); + + to.assertValueAt(-2, new Predicate<Integer>() { + @Override public boolean test(final Integer o) throws Exception { + return o == 1; + } + }); + }); + } + + @Test + public void assertValueAtIndexEmpty() { + assertThrowsWithMessage("No values (latch = 0, values = 0, errors = 0, completions = 1)", AssertionError.class, () -> { + TestObserver<Object> to = new TestObserver<>(); + + Observable.empty().subscribe(to); + + to.assertValueAt(0, "a"); + }); + } + + @Test + public void assertValueAtIndexMatch() { + TestObserver<String> to = new TestObserver<>(); + + Observable.just("a", "b").subscribe(to); + + to.assertValueAt(1, "b"); + } + + @Test + public void assertValueAtIndexNoMatch() { + assertThrowsWithMessage("\nexpected: b (class: String)\ngot: c (class: String); Value at position 2 differ (latch = 0, values = 3, errors = 0, completions = 1)", AssertionError.class, () -> { + TestObserver<String> to = new TestObserver<>(); + + Observable.just("a", "b", "c").subscribe(to); + + to.assertValueAt(2, "b"); + }); + } + + @Test + public void assertValueAtIndexThrowsMessageMatchRegex() { + assertThrowsWithMessageMatchRegex(ASSERT_MESSAGE_REGEX, AssertionError.class, () -> { + TestObserver<String> to = new TestObserver<>(); + + Observable.just("a", "b", "c").subscribe(to); + + to.assertValueAt(2, "b"); + }); + } + + @Test + public void assertValuesCountNoMatch() { + assertThrowsWithMessage("\nexpected: 2 [a, b]\ngot: 3 [a, b, c]; Value count differs (latch = 0, values = 3, errors = 0, completions = 1)", AssertionError.class, () -> { + TestObserver<String> to = new TestObserver<>(); + + Observable.just("a", "b", "c").subscribe(to); + + to.assertValues("a", "b"); + }); + } + + @Test + public void assertValuesCountThrowsMessageMatchRegex() { + assertThrowsWithMessageMatchRegex(ASSERT_MESSAGE_REGEX, AssertionError.class, () -> { + TestObserver<String> to = new TestObserver<>(); + + Observable.just("a", "b", "c").subscribe(to); + + to.assertValues("a", "b"); + }); + } + + @Test + public void assertValuesNoMatch() { + assertThrowsWithMessage("\nexpected: d (class: String)\ngot: c (class: String); Value at position 2 differ (latch = 0, values = 3, errors = 0, completions = 1)", AssertionError.class, () -> { + TestObserver<String> to = new TestObserver<>(); + + Observable.just("a", "b", "c").subscribe(to); + + to.assertValues("a", "b", "d"); + }); + } + + @Test + public void assertValuesThrowsMessageMatchRegex() { + assertThrowsWithMessageMatchRegex(ASSERT_MESSAGE_REGEX, AssertionError.class, () -> { + TestObserver<String> to = new TestObserver<>(); + + Observable.just("a", "b", "c").subscribe(to); + + to.assertValues("a", "b", "d"); + }); + } + + @Test + public void assertValueCountNoMatch() { + assertThrowsWithMessage("\nexpected: 2\ngot: 3; Value counts differ (latch = 0, values = 3, errors = 0, completions = 1)", AssertionError.class, () -> { + TestObserver<String> to = new TestObserver<>(); + + Observable.just("a", "b", "c").subscribe(to); + + to.assertValueCount(2); + }); + } + + @Test + public void assertValueCountThrowsMessageMatchRegex() { + assertThrowsWithMessageMatchRegex(ASSERT_MESSAGE_REGEX, AssertionError.class, () -> { + TestObserver<String> to = new TestObserver<>(); + + Observable.just("a", "b", "c").subscribe(to); + + to.assertValueCount(2); + }); + } + + @Test + public void assertValueSequenceNoMatch() { + assertThrowsWithMessage("\nexpected: d (class: String)\ngot: c (class: String); Value at position 2 differ (latch = 0, values = 3, errors = 0, completions = 1)", AssertionError.class, () -> { + TestObserver<String> to = new TestObserver<>(); + + Observable.just("a", "b", "c").subscribe(to); + + to.assertValueSequence(Arrays.asList("a", "b", "d")); + }); + } + + @Test + public void assertValueSequenceThrowsMessageMatchRegex() { + assertThrowsWithMessageMatchRegex(ASSERT_MESSAGE_REGEX, AssertionError.class, () -> { + TestObserver<String> to = new TestObserver<>(); + + Observable.just("a", "b", "c").subscribe(to); + + to.assertValueSequence(Arrays.asList("a", "b", "d")); + }); + } + + @Test + public void assertValueAtIndexInvalidIndex() { + assertThrowsWithMessage("Index 2 is out of range [0, 2) (latch = 0, values = 2, errors = 0, completions = 1)", AssertionError.class, () -> { + TestObserver<String> to = new TestObserver<>(); + + Observable.just("a", "b").subscribe(to); + + to.assertValueAt(2, "c"); + }); + } + + @Test + public void assertValueAtIndexInvalidIndexNegative() { + assertThrowsWithMessage("Index -2 is out of range [0, 2) (latch = 0, values = 2, errors = 0, completions = 1)", AssertionError.class, () -> { + TestObserver<String> to = new TestObserver<>(); + + Observable.just("a", "b").subscribe(to); + + to.assertValueAt(-2, "c"); + }); + } + + @Test + public void withTag() { + try { + for (int i = 1; i < 3; i++) { + Observable.just(i) + .test() + .withTag("testing with item=" + i) + .assertResult(1) + ; + } + throw new RuntimeException("Should have thrown!"); + } catch (AssertionError ex) { + assertTrue(ex.toString(), ex.toString().contains("testing with item=2")); + } + } + + @Test + public void assertValuesOnly() { + TestObserver<Integer> to = TestObserver.create(); + to.onSubscribe(Disposable.empty()); + to.assertValuesOnly(); + + to.onNext(5); + to.assertValuesOnly(5); + + to.onNext(-1); + to.assertValuesOnly(5, -1); + } + + @Test + public void assertValuesOnlyThrowsOnUnexpectedValue() { + TestObserver<Integer> to = TestObserver.create(); + to.onSubscribe(Disposable.empty()); + to.assertValuesOnly(); + + to.onNext(5); + to.assertValuesOnly(5); + + to.onNext(-1); + + try { + to.assertValuesOnly(5); + throw new RuntimeException(); + } catch (AssertionError ex) { + // expected + } + } + + @Test + public void assertValuesOnlyThrowsWhenCompleted() { + TestObserver<Integer> to = TestObserver.create(); + to.onSubscribe(Disposable.empty()); + + to.onComplete(); + + try { + to.assertValuesOnly(); + throw new RuntimeException(); + } catch (AssertionError ex) { + // expected + } + } + + @Test + public void assertValuesOnlyThrowsWhenErrored() { + TestObserver<Integer> to = TestObserver.create(); + to.onSubscribe(Disposable.empty()); + + to.onError(new TestException()); + + try { + to.assertValuesOnly(); + throw new RuntimeException(); + } catch (AssertionError ex) { + // expected + } + } + + @Test + public void onErrorIsNull() { + TestObserver<Integer> to = TestObserver.create(); + to.onSubscribe(Disposable.empty()); + + to.onError(null); + + to.assertFailure(NullPointerException.class); + } + + @Test + public void awaitCountTimeout() { + TestObserver<Integer> to = TestObserver.create(); + to.onSubscribe(Disposable.empty()); + to.awaitCount(1); + assertTrue(to.timeout); + } + + @Test(expected = RuntimeException.class) + public void awaitCountInterrupted() { + try { + TestObserver<Integer> to = TestObserver.create(); + to.onSubscribe(Disposable.empty()); + Thread.currentThread().interrupt(); + to.awaitCount(1); + } finally { + Thread.interrupted(); + } + } +} diff --git a/src/test/java/io/reactivex/rxjava3/operators/SimpleQueueTest.java b/src/test/java/io/reactivex/rxjava3/operators/SimpleQueueTest.java new file mode 100644 index 0000000000..b9f998c1de --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/operators/SimpleQueueTest.java @@ -0,0 +1,180 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +/* + * The code was inspired by the similarly named JCTools class: + * https://github.com/JCTools/JCTools/blob/master/jctools-core/src/main/java/org/jctools/queues/atomic + */ + +package io.reactivex.rxjava3.operators; + +import static org.junit.Assert.*; + +import java.util.concurrent.atomic.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.RxJavaTest; +import io.reactivex.rxjava3.internal.queue.MpscLinkedQueue; + +public class SimpleQueueTest extends RxJavaTest { + + @Test(expected = NullPointerException.class) + public void spscArrayQueueNull() { + SpscArrayQueue<Object> q = new SpscArrayQueue<>(16); + q.offer(null); + } + + @Test(expected = NullPointerException.class) + public void spscLinkedArrayQueueNull() { + SpscLinkedArrayQueue<Object> q = new SpscLinkedArrayQueue<>(16); + q.offer(null); + } + + @Test(expected = NullPointerException.class) + public void mpscLinkedQueueNull() { + MpscLinkedQueue<Object> q = new MpscLinkedQueue<>(); + q.offer(null); + } + + @Test + public void spscArrayQueueBiOffer() { + SpscArrayQueue<Object> q = new SpscArrayQueue<>(16); + q.offer(1, 2); + + assertEquals(1, q.poll()); + assertEquals(2, q.poll()); + assertNull(q.poll()); + } + + @Test + public void spscLinkedArrayQueueBiOffer() { + SpscLinkedArrayQueue<Object> q = new SpscLinkedArrayQueue<>(16); + q.offer(1, 2); + + assertEquals(1, q.poll()); + assertEquals(2, q.poll()); + assertNull(q.poll()); + } + + @Test + public void mpscLinkedQueueBiOffer() { + MpscLinkedQueue<Object> q = new MpscLinkedQueue<>(); + q.offer(1, 2); + + assertEquals(1, q.poll()); + assertEquals(2, q.poll()); + assertNull(q.poll()); + } + + @Test + public void spscBiOfferCapacity() { + SpscArrayQueue<Integer> q = new SpscArrayQueue<>(8); + assertTrue(q.offer(1, 2)); + assertTrue(q.offer(3, 4)); + assertTrue(q.offer(5, 6)); + assertTrue(q.offer(7)); + + assertFalse(q.offer(8, 9)); + assertFalse(q.offer(9, 10)); + } + + @Test + public void spscLinkedNewBufferPeek() { + SpscLinkedArrayQueue<Integer> q = new SpscLinkedArrayQueue<>(8); + assertTrue(q.offer(1, 2)); + assertTrue(q.offer(3, 4)); + assertTrue(q.offer(5, 6)); + assertTrue(q.offer(7, 8)); // this should trigger a new buffer + + for (int i = 0; i < 8; i++) { + assertEquals(i + 1, q.peek().intValue()); + assertEquals(i + 1, q.poll().intValue()); + } + + assertNull(q.peek()); + assertNull(q.poll()); + } + + @Test + public void mpscOfferPollRace() throws Exception { + final MpscLinkedQueue<Integer> q = new MpscLinkedQueue<>(); + + final AtomicInteger c = new AtomicInteger(3); + + Thread t1 = new Thread(new Runnable() { + int i; + @Override + public void run() { + c.decrementAndGet(); + while (c.get() != 0) { } + + while (i++ < 10000) { + q.offer(i); + } + } + }); + t1.start(); + + Thread t2 = new Thread(new Runnable() { + int i = 10000; + @Override + public void run() { + c.decrementAndGet(); + while (c.get() != 0) { } + + while (i++ < 10000) { + q.offer(i); + } + } + }); + t2.start(); + + Runnable r3 = new Runnable() { + int i = 20000; + @Override + public void run() { + c.decrementAndGet(); + while (c.get() != 0) { } + + while (--i > 0) { + q.poll(); + } + } + }; + + r3.run(); + + t1.join(); + t2.join(); + } + + @Test + public void spscLinkedArrayQueueNoNepotism() { + SpscLinkedArrayQueue<Integer> q = new SpscLinkedArrayQueue<>(16); + + AtomicReferenceArray<Object> ara = q.producerBuffer; + + for (int i = 0; i < 20; i++) { + q.offer(i); + } + + assertNotNull(ara.get(16)); + + for (int i = 0; i < 20; i++) { + assertEquals(i, q.poll().intValue()); + } + + assertNull(ara.get(16)); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/parallel/ParallelCollectTest.java b/src/test/java/io/reactivex/rxjava3/parallel/ParallelCollectTest.java new file mode 100644 index 0000000000..f582966cf9 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/parallel/ParallelCollectTest.java @@ -0,0 +1,173 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.parallel; + +import static org.junit.Assert.*; + +import java.util.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.processors.PublishProcessor; +import io.reactivex.rxjava3.subscribers.TestSubscriber; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class ParallelCollectTest extends RxJavaTest { + + @Test + public void subscriberCount() { + ParallelFlowableTest.checkSubscriberCount(Flowable.range(1, 5).parallel() + .collect(new Supplier<List<Integer>>() { + @Override + public List<Integer> get() throws Exception { + return new ArrayList<>(); + } + }, new BiConsumer<List<Integer>, Integer>() { + @Override + public void accept(List<Integer> a, Integer b) throws Exception { + a.add(b); + } + })); + } + + @Test + public void initialCrash() { + Flowable.range(1, 5) + .parallel() + .collect(new Supplier<List<Integer>>() { + @Override + public List<Integer> get() throws Exception { + throw new TestException(); + } + }, new BiConsumer<List<Integer>, Integer>() { + @Override + public void accept(List<Integer> a, Integer b) throws Exception { + a.add(b); + } + }) + .sequential() + .test() + .assertFailure(TestException.class); + } + + @Test + public void reducerCrash() { + Flowable.range(1, 5) + .parallel() + .collect(new Supplier<List<Integer>>() { + @Override + public List<Integer> get() throws Exception { + return new ArrayList<>(); + } + }, new BiConsumer<List<Integer>, Integer>() { + @Override + public void accept(List<Integer> a, Integer b) throws Exception { + if (b == 3) { + throw new TestException(); + } + a.add(b); + } + }) + .sequential() + .test() + .assertFailure(TestException.class); + } + + @Test + public void cancel() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + + TestSubscriber<List<Integer>> ts = pp + .parallel() + .collect(new Supplier<List<Integer>>() { + @Override + public List<Integer> get() throws Exception { + return new ArrayList<>(); + } + }, new BiConsumer<List<Integer>, Integer>() { + @Override + public void accept(List<Integer> a, Integer b) throws Exception { + a.add(b); + } + }) + .sequential() + .test(); + + assertTrue(pp.hasSubscribers()); + + ts.cancel(); + + assertFalse(pp.hasSubscribers()); + } + + @Test + public void error() { + Flowable.<Integer>error(new TestException()) + .parallel() + .collect(new Supplier<List<Integer>>() { + @Override + public List<Integer> get() throws Exception { + return new ArrayList<>(); + } + }, new BiConsumer<List<Integer>, Integer>() { + @Override + public void accept(List<Integer> a, Integer b) throws Exception { + a.add(b); + } + }) + .sequential() + .test() + .assertFailure(TestException.class); + } + + @Test + public void doubleError() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + new ParallelInvalid() + .collect(new Supplier<List<Object>>() { + @Override + public List<Object> get() throws Exception { + return new ArrayList<>(); + } + }, new BiConsumer<List<Object>, Object>() { + @Override + public void accept(List<Object> a, Object b) throws Exception { + a.add(b); + } + }) + .sequential() + .test() + .assertFailure(TestException.class); + + assertFalse(errors.isEmpty()); + for (Throwable ex : errors) { + assertTrue(ex.toString(), ex.getCause() instanceof TestException); + } + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeParallel( + pf -> pf.collect(ArrayList::new, ArrayList::add) + ); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/parallel/ParallelDoOnNextTryTest.java b/src/test/java/io/reactivex/rxjava3/parallel/ParallelDoOnNextTryTest.java new file mode 100644 index 0000000000..ba76f69cfb --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/parallel/ParallelDoOnNextTryTest.java @@ -0,0 +1,406 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.parallel; + +import static org.junit.Assert.assertEquals; + +import java.util.List; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.testsupport.*; + +public class ParallelDoOnNextTryTest extends RxJavaTest implements Consumer<Object> { + + volatile int calls; + + @Override + public void accept(Object t) throws Exception { + calls++; + } + + @Test + public void doOnNextNoError() { + for (ParallelFailureHandling e : ParallelFailureHandling.values()) { + Flowable.just(1) + .parallel(1) + .doOnNext(this, e) + .sequential() + .test() + .assertResult(1); + + assertEquals(calls, 1); + calls = 0; + } + } + + @Test + public void doOnNextErrorNoError() { + for (ParallelFailureHandling e : ParallelFailureHandling.values()) { + Flowable.<Integer>error(new TestException()) + .parallel(1) + .doOnNext(this, e) + .sequential() + .test() + .assertFailure(TestException.class); + + assertEquals(calls, 0); + } + } + + @Test + public void doOnNextConditionalNoError() { + for (ParallelFailureHandling e : ParallelFailureHandling.values()) { + Flowable.just(1) + .parallel(1) + .doOnNext(this, e) + .filter(Functions.alwaysTrue()) + .sequential() + .test() + .assertResult(1); + + assertEquals(calls, 1); + calls = 0; + } + } + + @Test + public void doOnNextErrorConditionalNoError() { + for (ParallelFailureHandling e : ParallelFailureHandling.values()) { + Flowable.<Integer>error(new TestException()) + .parallel(1) + .doOnNext(this, e) + .filter(Functions.alwaysTrue()) + .sequential() + .test() + .assertFailure(TestException.class); + + assertEquals(calls, 0); + } + } + + @Test + public void doOnNextFailWithError() { + Flowable.range(0, 2) + .parallel(1) + .doOnNext(new Consumer<Integer>() { + @Override + public void accept(Integer v) throws Exception { + if (1 / v < 0) { + System.out.println("Should not happen!"); + } + } + }, ParallelFailureHandling.ERROR) + .sequential() + .test() + .assertFailure(ArithmeticException.class); + } + + @Test + public void doOnNextFailWithStop() { + Flowable.range(0, 2) + .parallel(1) + .doOnNext(new Consumer<Integer>() { + @Override + public void accept(Integer v) throws Exception { + if (1 / v < 0) { + System.out.println("Should not happen!"); + } + } + }, ParallelFailureHandling.STOP) + .sequential() + .test() + .assertResult(); + } + + @Test + public void doOnNextFailWithRetry() { + Flowable.range(0, 2) + .parallel(1) + .doOnNext(new Consumer<Integer>() { + int count; + @Override + public void accept(Integer v) throws Exception { + if (count++ == 1) { + return; + } + if (1 / v < 0) { + System.out.println("Should not happen!"); + } + } + }, ParallelFailureHandling.RETRY) + .sequential() + .test() + .assertResult(0, 1); + } + + @Test + public void doOnNextFailWithRetryLimited() { + Flowable.range(0, 2) + .parallel(1) + .doOnNext(new Consumer<Integer>() { + @Override + public void accept(Integer v) throws Exception { + if (1 / v < 0) { + System.out.println("Should not happen!"); + } + } + }, new BiFunction<Long, Throwable, ParallelFailureHandling>() { + @Override + public ParallelFailureHandling apply(Long n, Throwable e) throws Exception { + return n < 5 ? ParallelFailureHandling.RETRY : ParallelFailureHandling.SKIP; + } + }) + .sequential() + .test() + .assertResult(1); + } + + @Test + public void doOnNextFailWithSkip() { + Flowable.range(0, 2) + .parallel(1) + .doOnNext(new Consumer<Integer>() { + @Override + public void accept(Integer v) throws Exception { + if (1 / v < 0) { + System.out.println("Should not happen!"); + } + } + }, ParallelFailureHandling.SKIP) + .sequential() + .test() + .assertResult(1); + } + + @Test + public void doOnNextFailHandlerThrows() { + TestSubscriberEx<Integer> ts = Flowable.range(0, 2) + .parallel(1) + .doOnNext(new Consumer<Integer>() { + @Override + public void accept(Integer v) throws Exception { + if (1 / v < 0) { + System.out.println("Should not happen!"); + } + } + }, new BiFunction<Long, Throwable, ParallelFailureHandling>() { + @Override + public ParallelFailureHandling apply(Long n, Throwable e) throws Exception { + throw new TestException(); + } + }) + .sequential() + .to(TestHelper.<Integer>testConsumer()) + .assertFailure(CompositeException.class); + + TestHelper.assertCompositeExceptions(ts, ArithmeticException.class, TestException.class); + } + + @Test + public void doOnNextWrongParallelism() { + TestHelper.checkInvalidParallelSubscribers( + Flowable.just(1).parallel(1) + .doOnNext(Functions.emptyConsumer(), ParallelFailureHandling.ERROR) + ); + } + + @Test + public void filterInvalidSource() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + new ParallelInvalid() + .doOnNext(Functions.emptyConsumer(), ParallelFailureHandling.ERROR) + .sequential() + .test(); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void doOnNextFailWithErrorConditional() { + Flowable.range(0, 2) + .parallel(1) + .doOnNext(new Consumer<Integer>() { + @Override + public void accept(Integer v) throws Exception { + if (1 / v < 0) { + System.out.println("Should not happen!"); + } + } + }, ParallelFailureHandling.ERROR) + .filter(Functions.alwaysTrue()) + .sequential() + .test() + .assertFailure(ArithmeticException.class); + } + + @Test + public void doOnNextFailWithStopConditional() { + Flowable.range(0, 2) + .parallel(1) + .doOnNext(new Consumer<Integer>() { + @Override + public void accept(Integer v) throws Exception { + if (1 / v < 0) { + System.out.println("Should not happen!"); + } + } + }, ParallelFailureHandling.STOP) + .filter(Functions.alwaysTrue()) + .sequential() + .test() + .assertResult(); + } + + @Test + public void doOnNextFailWithRetryConditional() { + Flowable.range(0, 2) + .parallel(1) + .doOnNext(new Consumer<Integer>() { + int count; + @Override + public void accept(Integer v) throws Exception { + if (count++ == 1) { + return; + } + if (1 / v < 0) { + System.out.println("Should not happen!"); + } + } + }, ParallelFailureHandling.RETRY) + .filter(Functions.alwaysTrue()) + .sequential() + .test() + .assertResult(0, 1); + } + + @Test + public void doOnNextFailWithRetryLimitedConditional() { + Flowable.range(0, 2) + .parallel(1) + .doOnNext(new Consumer<Integer>() { + @Override + public void accept(Integer v) throws Exception { + if (1 / v < 0) { + System.out.println("Should not happen!"); + } + } + }, new BiFunction<Long, Throwable, ParallelFailureHandling>() { + @Override + public ParallelFailureHandling apply(Long n, Throwable e) throws Exception { + return n < 5 ? ParallelFailureHandling.RETRY : ParallelFailureHandling.SKIP; + } + }) + .filter(Functions.alwaysTrue()) + .sequential() + .test() + .assertResult(1); + } + + @Test + public void doOnNextFailWithSkipConditional() { + Flowable.range(0, 2) + .parallel(1) + .doOnNext(new Consumer<Integer>() { + @Override + public void accept(Integer v) throws Exception { + if (1 / v < 0) { + System.out.println("Should not happen!"); + } + } + }, ParallelFailureHandling.SKIP) + .filter(Functions.alwaysTrue()) + .sequential() + .test() + .assertResult(1); + } + + @Test + public void doOnNextFailHandlerThrowsConditional() { + TestSubscriberEx<Integer> ts = Flowable.range(0, 2) + .parallel(1) + .doOnNext(new Consumer<Integer>() { + @Override + public void accept(Integer v) throws Exception { + if (1 / v < 0) { + System.out.println("Should not happen!"); + } + } + }, new BiFunction<Long, Throwable, ParallelFailureHandling>() { + @Override + public ParallelFailureHandling apply(Long n, Throwable e) throws Exception { + throw new TestException(); + } + }) + .filter(Functions.alwaysTrue()) + .sequential() + .to(TestHelper.<Integer>testConsumer()) + .assertFailure(CompositeException.class); + + TestHelper.assertCompositeExceptions(ts, ArithmeticException.class, TestException.class); + } + + @Test + public void doOnNextWrongParallelismConditional() { + TestHelper.checkInvalidParallelSubscribers( + Flowable.just(1).parallel(1) + .doOnNext(Functions.emptyConsumer(), ParallelFailureHandling.ERROR) + .filter(Functions.alwaysTrue()) + ); + } + + @Test + public void filterInvalidSourceConditional() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + new ParallelInvalid() + .doOnNext(Functions.emptyConsumer(), ParallelFailureHandling.ERROR) + .filter(Functions.alwaysTrue()) + .sequential() + .test(); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeFlowable(f -> + ParallelFlowable.fromArray(f) + .doOnNext(v -> { }, ParallelFailureHandling.SKIP) + .sequential() + ); + } + + @Test + public void doubleOnSubscribeConditional() { + TestHelper.checkDoubleOnSubscribeFlowable(f -> + ParallelFlowable.fromArray(f) + .doOnNext(v -> { }, ParallelFailureHandling.SKIP) + .filter(v -> true, ParallelFailureHandling.SKIP) + .sequential() + ); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/parallel/ParallelFilterTest.java b/src/test/java/io/reactivex/rxjava3/parallel/ParallelFilterTest.java new file mode 100644 index 0000000000..31e9b9a7c4 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/parallel/ParallelFilterTest.java @@ -0,0 +1,163 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.parallel; + +import static org.junit.Assert.*; + +import java.util.List; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.Predicate; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class ParallelFilterTest extends RxJavaTest { + + @Test + public void subscriberCount() { + ParallelFlowableTest.checkSubscriberCount(Flowable.range(1, 5).parallel() + .filter(Functions.alwaysTrue())); + } + + @Test + public void doubleFilter() { + Flowable.range(1, 10) + .parallel() + .filter(new Predicate<Integer>() { + @Override + public boolean test(Integer v) throws Exception { + return v % 2 == 0; + } + }) + .filter(new Predicate<Integer>() { + @Override + public boolean test(Integer v) throws Exception { + return v % 3 == 0; + } + }) + .sequential() + .test() + .assertResult(6); + } + + @Test + public void doubleError() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + new ParallelInvalid() + .filter(Functions.alwaysTrue()) + .sequential() + .test() + .assertFailure(TestException.class); + + assertFalse(errors.isEmpty()); + for (Throwable ex : errors) { + assertTrue(ex.toString(), ex.getCause() instanceof TestException); + } + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void doubleError2() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + new ParallelInvalid() + .filter(Functions.alwaysTrue()) + .filter(Functions.alwaysTrue()) + .sequential() + .test() + .assertFailure(TestException.class); + + assertFalse(errors.isEmpty()); + for (Throwable ex : errors) { + assertTrue(ex.toString(), ex.getCause() instanceof TestException); + } + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void error() { + Flowable.error(new TestException()) + .parallel() + .filter(Functions.alwaysTrue()) + .sequential() + .test() + .assertFailure(TestException.class); + } + + @Test + public void predicateThrows() { + Flowable.just(1) + .parallel() + .filter(new Predicate<Integer>() { + @Override + public boolean test(Integer v) throws Exception { + throw new TestException(); + } + }) + .filter(Functions.alwaysTrue()) + .sequential() + .test() + .assertFailure(TestException.class); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeFlowable(f -> + ParallelFlowable.fromArray(f) + .filter(v -> true) + .sequential() + ); + } + + @Test + public void doubleOnSubscribeConditional() { + TestHelper.checkDoubleOnSubscribeFlowable(f -> + ParallelFlowable.fromArray(f) + .filter(v -> true) + .filter(v -> true) + .sequential() + ); + } + + @Test + public void conditionalFalseTrue() { + Flowable.just(1) + .parallel() + .filter(v -> false) + .filter(v -> true) + .sequential() + .test() + .assertResult(); + } + + @Test + public void conditionalTrueFalse() { + Flowable.just(1) + .parallel() + .filter(v -> true) + .filter(v -> false) + .sequential() + .test() + .assertResult(); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/parallel/ParallelFilterTryTest.java b/src/test/java/io/reactivex/rxjava3/parallel/ParallelFilterTryTest.java new file mode 100644 index 0000000000..6940062989 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/parallel/ParallelFilterTryTest.java @@ -0,0 +1,417 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.parallel; + +import java.util.List; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.testsupport.*; + +public class ParallelFilterTryTest extends RxJavaTest implements Consumer<Object> { + + volatile int calls; + + @Override + public void accept(Object t) throws Exception { + calls++; + } + + @Test + public void filterNoError() { + for (ParallelFailureHandling e : ParallelFailureHandling.values()) { + Flowable.just(1) + .parallel(1) + .filter(Functions.alwaysTrue(), e) + .sequential() + .test() + .assertResult(1); + } + } + + @Test + public void filterFalse() { + for (ParallelFailureHandling e : ParallelFailureHandling.values()) { + Flowable.just(1) + .parallel(1) + .filter(Functions.alwaysFalse(), e) + .sequential() + .test() + .assertResult(); + } + } + + @Test + public void filterFalseConditional() { + for (ParallelFailureHandling e : ParallelFailureHandling.values()) { + Flowable.just(1) + .parallel(1) + .filter(Functions.alwaysFalse(), e) + .filter(Functions.alwaysTrue()) + .sequential() + .test() + .assertResult(); + } + } + + @Test + public void filterErrorNoError() { + for (ParallelFailureHandling e : ParallelFailureHandling.values()) { + Flowable.<Integer>error(new TestException()) + .parallel(1) + .filter(Functions.alwaysTrue(), e) + .sequential() + .test() + .assertFailure(TestException.class); + } + } + + @Test + public void filterConditionalNoError() { + for (ParallelFailureHandling e : ParallelFailureHandling.values()) { + Flowable.just(1) + .parallel(1) + .filter(Functions.alwaysTrue(), e) + .filter(Functions.alwaysTrue()) + .sequential() + .test() + .assertResult(1); + } + } + + @Test + public void filterErrorConditionalNoError() { + for (ParallelFailureHandling e : ParallelFailureHandling.values()) { + Flowable.<Integer>error(new TestException()) + .parallel(1) + .filter(Functions.alwaysTrue(), e) + .filter(Functions.alwaysTrue()) + .sequential() + .test() + .assertFailure(TestException.class); + } + } + + @Test + public void filterFailWithError() { + Flowable.range(0, 2) + .parallel(1) + .filter(new Predicate<Integer>() { + @Override + public boolean test(Integer v) throws Exception { + return 1 / v > 0; + } + }, ParallelFailureHandling.ERROR) + .sequential() + .test() + .assertFailure(ArithmeticException.class); + } + + @Test + public void filterFailWithStop() { + Flowable.range(0, 2) + .parallel(1) + .filter(new Predicate<Integer>() { + @Override + public boolean test(Integer v) throws Exception { + return 1 / v > 0; + } + }, ParallelFailureHandling.STOP) + .sequential() + .test() + .assertResult(); + } + + @Test + public void filterFailWithRetry() { + Flowable.range(0, 2) + .parallel(1) + .filter(new Predicate<Integer>() { + int count; + @Override + public boolean test(Integer v) throws Exception { + if (count++ == 1) { + return true; + } + return 1 / v > 0; + } + }, ParallelFailureHandling.RETRY) + .sequential() + .test() + .assertResult(0, 1); + } + + @Test + public void filterFailWithRetryLimited() { + Flowable.range(0, 2) + .parallel(1) + .filter(new Predicate<Integer>() { + @Override + public boolean test(Integer v) throws Exception { + return 1 / v > 0; + } + }, new BiFunction<Long, Throwable, ParallelFailureHandling>() { + @Override + public ParallelFailureHandling apply(Long n, Throwable e) throws Exception { + return n < 5 ? ParallelFailureHandling.RETRY : ParallelFailureHandling.SKIP; + } + }) + .sequential() + .test() + .assertResult(1); + } + + @Test + public void filterFailWithSkip() { + Flowable.range(0, 2) + .parallel(1) + .filter(new Predicate<Integer>() { + @Override + public boolean test(Integer v) throws Exception { + return 1 / v > 0; + } + }, ParallelFailureHandling.SKIP) + .sequential() + .test() + .assertResult(1); + } + + @Test + public void filterFailHandlerThrows() { + TestSubscriberEx<Integer> ts = Flowable.range(0, 2) + .parallel(1) + .filter(new Predicate<Integer>() { + @Override + public boolean test(Integer v) throws Exception { + return 1 / v > 0; + } + }, new BiFunction<Long, Throwable, ParallelFailureHandling>() { + @Override + public ParallelFailureHandling apply(Long n, Throwable e) throws Exception { + throw new TestException(); + } + }) + .sequential() + .to(TestHelper.<Integer>testConsumer()) + .assertFailure(CompositeException.class); + + TestHelper.assertCompositeExceptions(ts, ArithmeticException.class, TestException.class); + } + + @Test + public void filterWrongParallelism() { + TestHelper.checkInvalidParallelSubscribers( + Flowable.just(1).parallel(1) + .filter(Functions.alwaysTrue(), ParallelFailureHandling.ERROR) + ); + } + + @Test + public void filterInvalidSource() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + new ParallelInvalid() + .filter(Functions.alwaysTrue(), ParallelFailureHandling.ERROR) + .sequential() + .test(); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void filterFailWithErrorConditional() { + Flowable.range(0, 2) + .parallel(1) + .filter(new Predicate<Integer>() { + @Override + public boolean test(Integer v) throws Exception { + return 1 / v > 0; + } + }, ParallelFailureHandling.ERROR) + .filter(Functions.alwaysTrue()) + .sequential() + .test() + .assertFailure(ArithmeticException.class); + } + + @Test + public void filterFailWithStopConditional() { + Flowable.range(0, 2) + .parallel(1) + .filter(new Predicate<Integer>() { + @Override + public boolean test(Integer v) throws Exception { + return 1 / v > 0; + } + }, ParallelFailureHandling.STOP) + .filter(Functions.alwaysTrue()) + .sequential() + .test() + .assertResult(); + } + + @Test + public void filterFailWithRetryConditional() { + Flowable.range(0, 2) + .parallel(1) + .filter(new Predicate<Integer>() { + int count; + @Override + public boolean test(Integer v) throws Exception { + if (count++ == 1) { + return true; + } + return 1 / v > 0; + } + }, ParallelFailureHandling.RETRY) + .filter(Functions.alwaysTrue()) + .sequential() + .test() + .assertResult(0, 1); + } + + @Test + public void filterFailWithRetryLimitedConditional() { + Flowable.range(0, 2) + .parallel(1) + .filter(new Predicate<Integer>() { + @Override + public boolean test(Integer v) throws Exception { + return 1 / v > 0; + } + }, new BiFunction<Long, Throwable, ParallelFailureHandling>() { + @Override + public ParallelFailureHandling apply(Long n, Throwable e) throws Exception { + return n < 5 ? ParallelFailureHandling.RETRY : ParallelFailureHandling.SKIP; + } + }) + .filter(Functions.alwaysTrue()) + .sequential() + .test() + .assertResult(1); + } + + @Test + public void filterFailWithSkipConditional() { + Flowable.range(0, 2) + .parallel(1) + .filter(new Predicate<Integer>() { + @Override + public boolean test(Integer v) throws Exception { + return 1 / v > 0; + } + }, ParallelFailureHandling.SKIP) + .filter(Functions.alwaysTrue()) + .sequential() + .test() + .assertResult(1); + } + + @Test + public void filterFailHandlerThrowsConditional() { + TestSubscriberEx<Integer> ts = Flowable.range(0, 2) + .parallel(1) + .filter(new Predicate<Integer>() { + @Override + public boolean test(Integer v) throws Exception { + return 1 / v > 0; + } + }, new BiFunction<Long, Throwable, ParallelFailureHandling>() { + @Override + public ParallelFailureHandling apply(Long n, Throwable e) throws Exception { + throw new TestException(); + } + }) + .filter(Functions.alwaysTrue()) + .sequential() + .to(TestHelper.<Integer>testConsumer()) + .assertFailure(CompositeException.class); + + TestHelper.assertCompositeExceptions(ts, ArithmeticException.class, TestException.class); + } + + @Test + public void filterWrongParallelismConditional() { + TestHelper.checkInvalidParallelSubscribers( + Flowable.just(1).parallel(1) + .filter(Functions.alwaysTrue(), ParallelFailureHandling.ERROR) + .filter(Functions.alwaysTrue()) + ); + } + + @Test + public void filterInvalidSourceConditional() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + new ParallelInvalid() + .filter(Functions.alwaysTrue(), ParallelFailureHandling.ERROR) + .filter(Functions.alwaysTrue()) + .sequential() + .test(); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeFlowable(f -> + ParallelFlowable.fromArray(f) + .filter(v -> true, ParallelFailureHandling.SKIP) + .sequential() + ); + } + + @Test + public void doubleOnSubscribeConditional() { + TestHelper.checkDoubleOnSubscribeFlowable(f -> + ParallelFlowable.fromArray(f) + .filter(v -> true, ParallelFailureHandling.SKIP) + .filter(v -> true, ParallelFailureHandling.SKIP) + .sequential() + ); + } + + @Test + public void conditionalFalseTrue() { + Flowable.just(1) + .parallel() + .filter(v -> false, ParallelFailureHandling.SKIP) + .filter(v -> true, ParallelFailureHandling.SKIP) + .sequential() + .test() + .assertResult(); + } + + @Test + public void conditionalTrueFalse() { + Flowable.just(1) + .parallel() + .filter(v -> true, ParallelFailureHandling.SKIP) + .filter(v -> false, ParallelFailureHandling.SKIP) + .sequential() + .test() + .assertResult(); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/parallel/ParallelFlatMapIterableTest.java b/src/test/java/io/reactivex/rxjava3/parallel/ParallelFlatMapIterableTest.java new file mode 100644 index 0000000000..8c18de1b0b --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/parallel/ParallelFlatMapIterableTest.java @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.parallel; + +import java.util.Arrays; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; + +public class ParallelFlatMapIterableTest extends RxJavaTest { + + @Test + public void subscriberCount() { + ParallelFlowableTest.checkSubscriberCount(Flowable.range(1, 5).parallel() + .flatMapIterable(v -> Arrays.asList(1, 2, 3))); + } + + @Test + public void normal() { + for (int i = 1; i < 32; i++) { + Flowable.range(1, 1000) + .parallel(i) + .flatMapIterable(v -> Arrays.asList(v, v + 1)) + .sequential() + .test() + .withTag("Parallelism: " + i) + .assertValueCount(2000) + .assertNoErrors() + .assertComplete(); + } + } + + @Test + public void none() { + for (int i = 1; i < 32; i++) { + Flowable.range(1, 1000) + .parallel(i) + .flatMapIterable(v -> Arrays.asList()) + .sequential() + .test() + .withTag("Parallelism: " + i) + .assertResult(); + } + } + + @Test + public void mixed() { + for (int i = 1; i < 32; i++) { + Flowable.range(1, 1000) + .parallel(i) + .flatMapIterable(v -> v % 2 == 0 ? Arrays.asList(v) : Arrays.asList()) + .sequential() + .test() + .withTag("Parallelism: " + i) + .assertValueCount(500) + .assertNoErrors() + .assertComplete(); + } + } +} diff --git a/src/test/java/io/reactivex/rxjava3/parallel/ParallelFlowableTest.java b/src/test/java/io/reactivex/rxjava3/parallel/ParallelFlowableTest.java new file mode 100644 index 0000000000..c97bc5cf56 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/parallel/ParallelFlowableTest.java @@ -0,0 +1,1363 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.parallel; + +import static org.junit.Assert.*; + +import java.io.IOException; +import java.util.*; +import java.util.concurrent.*; + +import org.junit.*; +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.internal.util.*; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.processors.UnicastProcessor; +import io.reactivex.rxjava3.schedulers.Schedulers; +import io.reactivex.rxjava3.subscribers.TestSubscriber; +import io.reactivex.rxjava3.testsupport.*; + +public class ParallelFlowableTest extends RxJavaTest { + + @Test + public void sequentialMode() { + Flowable<Integer> source = Flowable.range(1, 1000000).hide(); + for (int i = 1; i < 33; i++) { + Flowable<Integer> result = ParallelFlowable.from(source, i) + .map(new Function<Integer, Integer>() { + @Override + public Integer apply(Integer v) throws Exception { + return v + 1; + } + }) + .sequential() + ; + + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(); + + result.subscribe(ts); + + ts + .assertSubscribed() + .assertValueCount(1000000) + .assertComplete() + .assertNoErrors() + ; + } + + } + + @Test + public void sequentialModeFused() { + Flowable<Integer> source = Flowable.range(1, 1000000); + for (int i = 1; i < 33; i++) { + Flowable<Integer> result = ParallelFlowable.from(source, i) + .map(new Function<Integer, Integer>() { + @Override + public Integer apply(Integer v) throws Exception { + return v + 1; + } + }) + .sequential() + ; + + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(); + + result.subscribe(ts); + + ts + .assertSubscribed() + .assertValueCount(1000000) + .assertComplete() + .assertNoErrors() + ; + } + + } + + @Test + public void parallelMode() { + Flowable<Integer> source = Flowable.range(1, 1000000).hide(); + int ncpu = Math.max(8, Runtime.getRuntime().availableProcessors()); + for (int i = 1; i < ncpu + 1; i++) { + + ExecutorService exec = Executors.newFixedThreadPool(i); + + Scheduler scheduler = Schedulers.from(exec); + + try { + Flowable<Integer> result = ParallelFlowable.from(source, i) + .runOn(scheduler) + .map(new Function<Integer, Integer>() { + @Override + public Integer apply(Integer v) throws Exception { + return v + 1; + } + }) + .sequential() + ; + + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(); + + result.subscribe(ts); + + ts.awaitDone(10, TimeUnit.SECONDS); + + ts + .assertSubscribed() + .assertValueCount(1000000) + .assertComplete() + .assertNoErrors() + ; + } finally { + exec.shutdown(); + } + } + + } + + @Test + public void parallelModeFused() { + Flowable<Integer> source = Flowable.range(1, 1000000); + int ncpu = Math.max(8, Runtime.getRuntime().availableProcessors()); + for (int i = 1; i < ncpu + 1; i++) { + + ExecutorService exec = Executors.newFixedThreadPool(i); + + Scheduler scheduler = Schedulers.from(exec); + + try { + Flowable<Integer> result = ParallelFlowable.from(source, i) + .runOn(scheduler) + .map(new Function<Integer, Integer>() { + @Override + public Integer apply(Integer v) throws Exception { + return v + 1; + } + }) + .sequential() + ; + + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(); + + result.subscribe(ts); + + ts.awaitDone(10, TimeUnit.SECONDS); + + ts + .assertSubscribed() + .assertValueCount(1000000) + .assertComplete() + .assertNoErrors() + ; + } finally { + exec.shutdown(); + } + } + + } + + @Test + public void reduceFull() { + for (int i = 1; i <= Runtime.getRuntime().availableProcessors() * 2; i++) { + TestSubscriber<Integer> ts = new TestSubscriber<>(); + + Flowable.range(1, 10) + .parallel(i) + .reduce(new BiFunction<Integer, Integer, Integer>() { + @Override + public Integer apply(Integer a, Integer b) throws Exception { + return a + b; + } + }) + .subscribe(ts); + + ts.assertResult(55); + } + } + + @Test + public void parallelReduceFull() { + int m = 100000; + for (int n = 1; n <= m; n *= 10) { +// System.out.println(n); + for (int i = 1; i <= Runtime.getRuntime().availableProcessors(); i++) { +// System.out.println(" " + i); + + ExecutorService exec = Executors.newFixedThreadPool(i); + + Scheduler scheduler = Schedulers.from(exec); + + try { + TestSubscriber<Long> ts = new TestSubscriber<>(); + + Flowable.range(1, n) + .map(new Function<Integer, Long>() { + @Override + public Long apply(Integer v) throws Exception { + return (long)v; + } + }) + .parallel(i) + .runOn(scheduler) + .reduce(new BiFunction<Long, Long, Long>() { + @Override + public Long apply(Long a, Long b) throws Exception { + return a + b; + } + }) + .subscribe(ts); + + ts.awaitDone(500, TimeUnit.SECONDS); + + long e = ((long)n) * (1 + n) / 2; + + ts.assertResult(e); + } finally { + exec.shutdown(); + } + } + } + } + + @Test + public void toSortedList() { + TestSubscriber<List<Integer>> ts = new TestSubscriber<>(); + + Flowable.fromArray(10, 9, 8, 7, 6, 5, 4, 3, 2, 1) + .parallel() + .toSortedList(Functions.naturalComparator()) + .subscribe(ts); + + ts.assertResult(Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)); + } + + @Test + public void sorted() { + TestSubscriber<Integer> ts = new TestSubscriber<>(0); + + Flowable.fromArray(10, 9, 8, 7, 6, 5, 4, 3, 2, 1) + .parallel() + .sorted(Functions.naturalComparator()) + .subscribe(ts); + + ts.assertNoValues(); + + ts.request(2); + + ts.assertValues(1, 2); + + ts.request(5); + + ts.assertValues(1, 2, 3, 4, 5, 6, 7); + + ts.request(3); + + ts.assertResult(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + } + + @Test + public void collect() { + Supplier<List<Integer>> as = new Supplier<List<Integer>>() { + @Override + public List<Integer> get() throws Exception { + return new ArrayList<>(); + } + }; + + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(); + Flowable.range(1, 10) + .parallel() + .collect(as, new BiConsumer<List<Integer>, Integer>() { + @Override + public void accept(List<Integer> a, Integer b) throws Exception { + a.add(b); + } + }) + .sequential() + .flatMapIterable(new Function<List<Integer>, Iterable<Integer>>() { + @Override + public Iterable<Integer> apply(List<Integer> v) throws Exception { + return v; + } + }) + .subscribe(ts); + + ts + .assertNoErrors() + .assertComplete(); + + TestHelper.assertValueSet(ts, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + } + + @Test + public void from() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(); + + ParallelFlowable.fromArray(Flowable.range(1, 5), Flowable.range(6, 5)) + .sequential() + .subscribe(ts); + + ts + .assertNoErrors() + .assertComplete(); + + TestHelper.assertValueSet(ts, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + } + + @Test + public void concatMapUnordered() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(); + + Flowable.range(1, 5) + .parallel() + .concatMap(new Function<Integer, Publisher<Integer>>() { + @Override + public Publisher<Integer> apply(Integer v) throws Exception { + return Flowable.range(v * 10 + 1, 3); + } + }) + .sequential() + .subscribe(ts); + + ts + .assertNoErrors() + .assertComplete(); + + TestHelper.assertValueSet(ts, 11, 12, 13, 21, 22, 23, 31, 32, 33, 41, 42, 43, 51, 52, 53); + } + + @Test + public void flatMapUnordered() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(); + + Flowable.range(1, 5) + .parallel() + .flatMap(new Function<Integer, Publisher<Integer>>() { + @Override + public Publisher<Integer> apply(Integer v) throws Exception { + return Flowable.range(v * 10 + 1, 3); + } + }) + .sequential() + .subscribe(ts); + + ts + .assertNoErrors() + .assertComplete(); + + TestHelper.assertValueSet(ts, 11, 12, 13, 21, 22, 23, 31, 32, 33, 41, 42, 43, 51, 52, 53); + } + + @Test + public void collectAsyncFused() { + ExecutorService exec = Executors.newFixedThreadPool(3); + + Scheduler s = Schedulers.from(exec); + + try { + Supplier<List<Integer>> as = new Supplier<List<Integer>>() { + @Override + public List<Integer> get() throws Exception { + return new ArrayList<>(); + } + }; + TestSubscriber<List<Integer>> ts = new TestSubscriber<>(); + + Flowable.range(1, 100000) + .parallel(3) + .runOn(s) + .collect(as, new BiConsumer<List<Integer>, Integer>() { + @Override + public void accept(List<Integer> a, Integer b) throws Exception { + a.add(b); + } + }) + .doOnNext(new Consumer<List<Integer>>() { + @Override + public void accept(List<Integer> v) throws Exception { + System.out.println(v.size()); + } + }) + .sequential() + .subscribe(ts); + + ts.awaitDone(5, TimeUnit.SECONDS); + ts.assertValueCount(3) + .assertNoErrors() + .assertComplete() + ; + + List<List<Integer>> list = ts.values(); + + Assert.assertEquals(100000, list.get(0).size() + list.get(1).size() + list.get(2).size()); + } finally { + exec.shutdown(); + } + } + + @Test + public void collectAsync() { + ExecutorService exec = Executors.newFixedThreadPool(3); + + Scheduler s = Schedulers.from(exec); + + try { + Supplier<List<Integer>> as = new Supplier<List<Integer>>() { + @Override + public List<Integer> get() throws Exception { + return new ArrayList<>(); + } + }; + TestSubscriber<List<Integer>> ts = new TestSubscriber<>(); + + Flowable.range(1, 100000).hide() + .parallel(3) + .runOn(s) + .collect(as, new BiConsumer<List<Integer>, Integer>() { + @Override + public void accept(List<Integer> a, Integer b) throws Exception { + a.add(b); + } + }) + .doOnNext(new Consumer<List<Integer>>() { + @Override + public void accept(List<Integer> v) throws Exception { + System.out.println(v.size()); + } + }) + .sequential() + .subscribe(ts); + + ts.awaitDone(5, TimeUnit.SECONDS); + ts.assertValueCount(3) + .assertNoErrors() + .assertComplete() + ; + + List<List<Integer>> list = ts.values(); + + Assert.assertEquals(100000, list.get(0).size() + list.get(1).size() + list.get(2).size()); + } finally { + exec.shutdown(); + } + } + + @Test + public void collectAsync2() { + ExecutorService exec = Executors.newFixedThreadPool(3); + + Scheduler s = Schedulers.from(exec); + + try { + Supplier<List<Integer>> as = new Supplier<List<Integer>>() { + @Override + public List<Integer> get() throws Exception { + return new ArrayList<>(); + } + }; + TestSubscriber<List<Integer>> ts = new TestSubscriber<>(); + + Flowable.range(1, 100000).hide() + .observeOn(s) + .parallel(3) + .runOn(s) + .collect(as, new BiConsumer<List<Integer>, Integer>() { + @Override + public void accept(List<Integer> a, Integer b) throws Exception { + a.add(b); + } + }) + .doOnNext(new Consumer<List<Integer>>() { + @Override + public void accept(List<Integer> v) throws Exception { + System.out.println(v.size()); + } + }) + .sequential() + .subscribe(ts); + + ts.awaitDone(5, TimeUnit.SECONDS); + ts.assertValueCount(3) + .assertNoErrors() + .assertComplete() + ; + + List<List<Integer>> list = ts.values(); + + Assert.assertEquals(100000, list.get(0).size() + list.get(1).size() + list.get(2).size()); + } finally { + exec.shutdown(); + } + } + + @Test + public void collectAsync3() { + ExecutorService exec = Executors.newFixedThreadPool(3); + + Scheduler s = Schedulers.from(exec); + + try { + Supplier<List<Integer>> as = new Supplier<List<Integer>>() { + @Override + public List<Integer> get() throws Exception { + return new ArrayList<>(); + } + }; + TestSubscriber<List<Integer>> ts = new TestSubscriber<>(); + + Flowable.range(1, 100000).hide() + .observeOn(s) + .parallel(3) + .runOn(s) + .collect(as, new BiConsumer<List<Integer>, Integer>() { + @Override + public void accept(List<Integer> a, Integer b) throws Exception { + a.add(b); + } + }) + .doOnNext(new Consumer<List<Integer>>() { + @Override + public void accept(List<Integer> v) throws Exception { + System.out.println(v.size()); + } + }) + .sequential() + .subscribe(ts); + + ts.awaitDone(5, TimeUnit.SECONDS); + ts.assertValueCount(3) + .assertNoErrors() + .assertComplete() + ; + + List<List<Integer>> list = ts.values(); + + Assert.assertEquals(100000, list.get(0).size() + list.get(1).size() + list.get(2).size()); + } finally { + exec.shutdown(); + } + } + + @Test + public void collectAsync3Fused() { + ExecutorService exec = Executors.newFixedThreadPool(3); + + Scheduler s = Schedulers.from(exec); + + try { + Supplier<List<Integer>> as = new Supplier<List<Integer>>() { + @Override + public List<Integer> get() throws Exception { + return new ArrayList<>(); + } + }; + TestSubscriber<List<Integer>> ts = new TestSubscriber<>(); + + Flowable.range(1, 100000) + .observeOn(s) + .parallel(3) + .runOn(s) + .collect(as, new BiConsumer<List<Integer>, Integer>() { + @Override + public void accept(List<Integer> a, Integer b) throws Exception { + a.add(b); + } + }) + .doOnNext(new Consumer<List<Integer>>() { + @Override + public void accept(List<Integer> v) throws Exception { + System.out.println(v.size()); + } + }) + .sequential() + .subscribe(ts); + + ts.awaitDone(5, TimeUnit.SECONDS); + ts.assertValueCount(3) + .assertNoErrors() + .assertComplete() + ; + + List<List<Integer>> list = ts.values(); + + Assert.assertEquals(100000, list.get(0).size() + list.get(1).size() + list.get(2).size()); + } finally { + exec.shutdown(); + } + } + + @Test + public void collectAsync3Take() { + ExecutorService exec = Executors.newFixedThreadPool(4); + + Scheduler s = Schedulers.from(exec); + + try { + Supplier<List<Integer>> as = new Supplier<List<Integer>>() { + @Override + public List<Integer> get() throws Exception { + return new ArrayList<>(); + } + }; + TestSubscriber<List<Integer>> ts = new TestSubscriber<>(); + + Flowable.range(1, 100000) + .take(1000) + .observeOn(s) + .parallel(3) + .runOn(s) + .collect(as, new BiConsumer<List<Integer>, Integer>() { + @Override + public void accept(List<Integer> a, Integer b) throws Exception { + a.add(b); + } + }) + .doOnNext(new Consumer<List<Integer>>() { + @Override + public void accept(List<Integer> v) throws Exception { + System.out.println(v.size()); + } + }) + .sequential() + .subscribe(ts); + + ts.awaitDone(5, TimeUnit.SECONDS); + ts.assertValueCount(3) + .assertNoErrors() + .assertComplete() + ; + + List<List<Integer>> list = ts.values(); + + Assert.assertEquals(1000, list.get(0).size() + list.get(1).size() + list.get(2).size()); + } finally { + exec.shutdown(); + } + } + + @Test + public void collectAsync4Take() { + ExecutorService exec = Executors.newFixedThreadPool(3); + + Scheduler s = Schedulers.from(exec); + + try { + Supplier<List<Integer>> as = new Supplier<List<Integer>>() { + @Override + public List<Integer> get() throws Exception { + return new ArrayList<>(); + } + }; + TestSubscriber<List<Integer>> ts = new TestSubscriber<>(); + + UnicastProcessor<Integer> up = UnicastProcessor.create(); + + for (int i = 0; i < 1000; i++) { + up.onNext(i); + } + + up + .take(1000) + .observeOn(s) + .parallel(3) + .runOn(s) + .collect(as, new BiConsumer<List<Integer>, Integer>() { + @Override + public void accept(List<Integer> a, Integer b) throws Exception { + a.add(b); + } + }) + .doOnNext(new Consumer<List<Integer>>() { + @Override + public void accept(List<Integer> v) throws Exception { + System.out.println(v.size()); + } + }) + .sequential() + .subscribe(ts); + + ts.awaitDone(5, TimeUnit.SECONDS); + ts.assertValueCount(3) + .assertNoErrors() + .assertComplete() + ; + + List<List<Integer>> list = ts.values(); + + Assert.assertEquals(1000, list.get(0).size() + list.get(1).size() + list.get(2).size()); + } finally { + exec.shutdown(); + } + } + + @Test + public void emptySourceZeroRequest() { + TestSubscriber<Object> ts = new TestSubscriber<>(0); + + Flowable.range(1, 3).parallel(3).sequential().subscribe(ts); + + ts.request(1); + + ts.assertValue(1); + } + + @Test + public void parallelismAndPrefetch() { + for (int parallelism = 1; parallelism <= 8; parallelism++) { + for (int prefetch = 1; prefetch <= 1024; prefetch *= 2) { + Flowable.range(1, 1024 * 1024) + .parallel(parallelism, prefetch) + .map(Functions.<Integer>identity()) + .sequential() + .to(TestHelper.<Integer>testConsumer()) + .assertSubscribed() + .assertValueCount(1024 * 1024) + .assertNoErrors() + .assertComplete(); + } + } + } + + @Test + public void parallelismAndPrefetchAsync() { + for (int parallelism = 1; parallelism <= 8; parallelism *= 2) { + for (int prefetch = 1; prefetch <= 1024; prefetch *= 2) { + System.out.println("parallelismAndPrefetchAsync >> " + parallelism + ", " + prefetch); + + Flowable.range(1, 1024 * 1024) + .parallel(parallelism, prefetch) + .runOn(Schedulers.computation()) + .map(Functions.<Integer>identity()) + .sequential(prefetch) + .to(TestHelper.<Integer>testConsumer()) + .withTag("parallelism = " + parallelism + ", prefetch = " + prefetch) + .awaitDone(30, TimeUnit.SECONDS) + .assertSubscribed() + .assertValueCount(1024 * 1024) + .assertNoErrors() + .assertComplete(); + } + } + } + + @SuppressWarnings("unchecked") + @Test + public void badParallelismStage() { + TestSubscriber<Integer> ts = new TestSubscriber<>(); + + Flowable.range(1, 10) + .parallel(2) + .subscribe(new Subscriber[] { ts }); + + ts.assertFailure(IllegalArgumentException.class); + } + + @SuppressWarnings("unchecked") + @Test + public void badParallelismStage2() { + TestSubscriber<Integer> ts1 = new TestSubscriber<>(); + TestSubscriber<Integer> ts2 = new TestSubscriber<>(); + TestSubscriber<Integer> ts3 = new TestSubscriber<>(); + + Flowable.range(1, 10) + .parallel(2) + .subscribe(new Subscriber[] { ts1, ts2, ts3 }); + + ts1.assertFailure(IllegalArgumentException.class); + ts2.assertFailure(IllegalArgumentException.class); + ts3.assertFailure(IllegalArgumentException.class); + } + + @Test + public void filter() { + TestSubscriberEx<Integer> ts = Flowable.range(1, 20) + .parallel() + .runOn(Schedulers.computation()) + .filter(new Predicate<Integer>() { + @Override + public boolean test(Integer v) throws Exception { + return v % 2 == 0; + } + }) + .sequential() + .to(TestHelper.<Integer>testConsumer()) + .awaitDone(5, TimeUnit.SECONDS) + .assertNoErrors() + .assertComplete(); + + TestHelper.assertValueSet(ts, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20); + } + + @Test + public void filterThrows() throws Exception { + final boolean[] cancelled = { false }; + Flowable.range(1, 20).concatWith(Flowable.<Integer>never()) + .doOnCancel(new Action() { + @Override + public void run() throws Exception { + cancelled[0] = true; + } + }) + .parallel() + .runOn(Schedulers.computation()) + .filter(new Predicate<Integer>() { + @Override + public boolean test(Integer v) throws Exception { + if (v == 10) { + throw new TestException(); + } + return v % 2 == 0; + } + }) + .sequential() + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertError(TestException.class) + .assertNotComplete(); + + Thread.sleep(100); + + assertTrue(cancelled[0]); + } + + @Test + public void doAfterNext() { + final int[] count = { 0 }; + + Flowable.range(1, 5) + .parallel() + .doAfterNext(new Consumer<Integer>() { + @Override + public void accept(Integer v) throws Exception { + count[0]++; + } + }) + .sequential() + .test() + .assertResult(1, 2, 3, 4, 5); + } + + @Test + public void doOnNextThrows() { + final int[] count = { 0 }; + + Flowable.range(1, 5) + .parallel() + .doOnNext(new Consumer<Integer>() { + @Override + public void accept(Integer v) throws Exception { + if (v == 3) { + throw new TestException(); + } else { + count[0]++; + } + } + }) + .sequential() + .test() + .assertError(TestException.class) + .assertNotComplete(); + + assertTrue("" + count[0], count[0] < 5); + } + + @Test + public void doAfterNextThrows() { + final int[] count = { 0 }; + + Flowable.range(1, 5) + .parallel() + .doAfterNext(new Consumer<Integer>() { + @Override + public void accept(Integer v) throws Exception { + if (v == 3) { + throw new TestException(); + } else { + count[0]++; + } + } + }) + .sequential() + .test() + .assertError(TestException.class) + .assertNotComplete(); + + assertTrue("" + count[0], count[0] < 5); + } + + @Test + public void errorNotRepeating() throws Exception { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + Flowable.error(new TestException()) + .parallel() + .runOn(Schedulers.computation()) + .sequential() + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertFailure(TestException.class) + ; + + Thread.sleep(300); + + for (Throwable ex : errors) { + ex.printStackTrace(); + } + assertTrue(errors.toString(), errors.isEmpty()); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void doOnError() { + final int[] count = { 0 }; + + Flowable.range(1, 5) + .parallel(2) + .map(new Function<Integer, Integer>() { + @Override + public Integer apply(Integer v) throws Exception { + if (v == 3) { + throw new TestException(); + } + return v; + } + }) + .doOnError(new Consumer<Throwable>() { + @Override + public void accept(Throwable e) throws Exception { + if (e instanceof TestException) { + count[0]++; + } + } + }) + .sequential() + .test() + .assertError(TestException.class) + .assertNotComplete(); + + assertEquals(1, count[0]); + } + + @Test + public void doOnErrorThrows() { + TestSubscriberEx<Integer> ts = Flowable.range(1, 5) + .parallel(2) + .map(new Function<Integer, Integer>() { + @Override + public Integer apply(Integer v) throws Exception { + if (v == 3) { + throw new TestException(); + } + return v; + } + }) + .doOnError(new Consumer<Throwable>() { + @Override + public void accept(Throwable e) throws Exception { + if (e instanceof TestException) { + throw new IOException(); + } + } + }) + .sequential() + .to(TestHelper.<Integer>testConsumer()) + .assertError(CompositeException.class) + .assertNotComplete(); + + List<Throwable> errors = TestHelper.errorList(ts); + TestHelper.assertError(errors, 0, TestException.class); + TestHelper.assertError(errors, 1, IOException.class); + } + + @Test + public void doOnComplete() { + final int[] count = { 0 }; + + Flowable.range(1, 5) + .parallel(2) + .doOnComplete(new Action() { + @Override + public void run() throws Exception { + count[0]++; + } + }) + .sequential() + .test() + .assertResult(1, 2, 3, 4, 5); + + assertEquals(2, count[0]); + } + + @Test + public void doAfterTerminate() { + final int[] count = { 0 }; + + Flowable.range(1, 5) + .parallel(2) + .doAfterTerminated(new Action() { + @Override + public void run() throws Exception { + count[0]++; + } + }) + .sequential() + .test() + .assertResult(1, 2, 3, 4, 5); + + assertEquals(2, count[0]); + } + + @Test + public void doOnSubscribe() { + final int[] count = { 0 }; + + Flowable.range(1, 5) + .parallel(2) + .doOnSubscribe(new Consumer<Subscription>() { + @Override + public void accept(Subscription s) throws Exception { + count[0]++; + } + }) + .sequential() + .test() + .assertResult(1, 2, 3, 4, 5); + + assertEquals(2, count[0]); + } + + @Test + public void doOnRequest() { + final int[] count = { 0 }; + + Flowable.range(1, 5) + .parallel(2) + .doOnRequest(new LongConsumer() { + @Override + public void accept(long s) throws Exception { + count[0]++; + } + }) + .sequential() + .test() + .assertResult(1, 2, 3, 4, 5); + + assertEquals(2, count[0]); + } + + @Test + public void doOnCancel() { + final int[] count = { 0 }; + + Flowable.range(1, 5) + .parallel(2) + .doOnCancel(new Action() { + @Override + public void run() throws Exception { + count[0]++; + } + }) + .sequential() + .take(2) + .test() + .assertResult(1, 2); + + assertEquals(2, count[0]); + } + + @SuppressWarnings("unchecked") + @Test(expected = IllegalArgumentException.class) + public void fromPublishers() { + ParallelFlowable.fromArray(new Publisher[0]); + } + + @Test + public void to() { + Flowable.range(1, 5) + .parallel() + .to(new ParallelFlowableConverter<Integer, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(ParallelFlowable<Integer> pf) { + return pf.sequential(); + } + }) + .test() + .assertResult(1, 2, 3, 4, 5); + } + + @Test(expected = TestException.class) + public void toThrows() { + Flowable.range(1, 5) + .parallel() + .to(new ParallelFlowableConverter<Integer, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(ParallelFlowable<Integer> pf) { + throw new TestException(); + } + }); + } + + @Test + public void compose() { + Flowable.range(1, 5) + .parallel() + .compose(new ParallelTransformer<Integer, Integer>() { + @Override + public ParallelFlowable<Integer> apply(ParallelFlowable<Integer> pf) { + return pf.map(new Function<Integer, Integer>() { + @Override + public Integer apply(Integer v) throws Exception { + return v + 1; + } + }); + } + }) + .sequential() + .test() + .assertResult(2, 3, 4, 5, 6); + } + + @Test + public void flatMapDelayError() { + final int[] count = { 0 }; + + Flowable.range(1, 5) + .parallel(2) + .flatMap(new Function<Integer, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Integer v) throws Exception { + if (v == 3) { + return Flowable.error(new TestException()); + } + return Flowable.just(v); + } + }, true) + .doOnError(new Consumer<Throwable>() { + @Override + public void accept(Throwable e) throws Exception { + if (e instanceof TestException) { + count[0]++; + } + } + }) + .sequential() + .test() + .assertValues(1, 2, 4, 5) + .assertError(TestException.class) + .assertNotComplete(); + + assertEquals(1, count[0]); + } + + @Test + public void flatMapDelayErrorMaxConcurrency() { + final int[] count = { 0 }; + + Flowable.range(1, 5) + .parallel(2) + .flatMap(new Function<Integer, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Integer v) throws Exception { + if (v == 3) { + return Flowable.error(new TestException()); + } + return Flowable.just(v); + } + }, true, 1) + .doOnError(new Consumer<Throwable>() { + @Override + public void accept(Throwable e) throws Exception { + if (e instanceof TestException) { + count[0]++; + } + } + }) + .sequential() + .test() + .assertValues(1, 2, 4, 5) + .assertError(TestException.class) + .assertNotComplete(); + + assertEquals(1, count[0]); + } + + @Test + public void concatMapDelayError() { + final int[] count = { 0 }; + + Flowable.range(1, 5) + .parallel(2) + .concatMapDelayError(new Function<Integer, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Integer v) throws Exception { + if (v == 3) { + return Flowable.error(new TestException()); + } + return Flowable.just(v); + } + }, true) + .doOnError(new Consumer<Throwable>() { + @Override + public void accept(Throwable e) throws Exception { + if (e instanceof TestException) { + count[0]++; + } + } + }) + .sequential() + .test() + .assertValues(1, 2, 4, 5) + .assertError(TestException.class) + .assertNotComplete(); + + assertEquals(1, count[0]); + } + + @Test + public void concatMapDelayErrorPrefetch() { + final int[] count = { 0 }; + + Flowable.range(1, 5) + .parallel(2) + .concatMapDelayError(new Function<Integer, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Integer v) throws Exception { + if (v == 3) { + return Flowable.error(new TestException()); + } + return Flowable.just(v); + } + }, 1, true) + .doOnError(new Consumer<Throwable>() { + @Override + public void accept(Throwable e) throws Exception { + if (e instanceof TestException) { + count[0]++; + } + } + }) + .sequential() + .test() + .assertValues(1, 2, 4, 5) + .assertError(TestException.class) + .assertNotComplete(); + + assertEquals(1, count[0]); + } + + @Test + public void concatMapDelayErrorBoundary() { + final int[] count = { 0 }; + + Flowable.range(1, 5) + .parallel(2) + .concatMapDelayError(new Function<Integer, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Integer v) throws Exception { + if (v == 3) { + return Flowable.error(new TestException()); + } + return Flowable.just(v); + } + }, false) + .doOnError(new Consumer<Throwable>() { + @Override + public void accept(Throwable e) throws Exception { + if (e instanceof TestException) { + count[0]++; + } + } + }) + .sequential() + .test() + .assertValues(1, 2) + .assertError(TestException.class) + .assertNotComplete(); + + assertEquals(1, count[0]); + } + + public static void checkSubscriberCount(ParallelFlowable<?> source) { + int n = source.parallelism(); + + @SuppressWarnings("unchecked") + TestSubscriber<Object>[] consumers = new TestSubscriber[n + 1]; + + for (int i = 0; i <= n; i++) { + consumers[i] = new TestSubscriber<>(); + } + + source.subscribe(consumers); + + for (int i = 0; i <= n; i++) { + consumers[i].awaitDone(5, TimeUnit.SECONDS) + .assertFailure(IllegalArgumentException.class); + } + } + + @Test + public void checkAddBiConsumer() { + TestHelper.checkEnum(ListAddBiConsumer.class); + } + + @Test + public void mergeBiFunction() throws Exception { + MergerBiFunction<Integer> f = new MergerBiFunction<>(Functions.<Integer>naturalComparator()); + + assertEquals(0, f.apply(Collections.<Integer>emptyList(), Collections.<Integer>emptyList()).size()); + + assertEquals(Arrays.asList(1, 2), f.apply(Collections.<Integer>emptyList(), Arrays.asList(1, 2))); + + for (int i = 0; i < 4; i++) { + int k = 0; + List<Integer> list1 = new ArrayList<>(); + for (int j = 0; j < i; j++) { + list1.add(k++); + } + + List<Integer> list2 = new ArrayList<>(); + for (int j = i; j < 4; j++) { + list2.add(k++); + } + + assertEquals(Arrays.asList(0, 1, 2, 3), f.apply(list1, list2)); + } + } + + @Test + public void concatMapSubscriberCount() { + ParallelFlowableTest.checkSubscriberCount(Flowable.range(1, 5).parallel() + .concatMap(Functions.justFunction(Flowable.just(1)))); + } + + @Test + public void flatMapSubscriberCount() { + ParallelFlowableTest.checkSubscriberCount(Flowable.range(1, 5).parallel() + .flatMap(Functions.justFunction(Flowable.just(1)))); + } + + @SuppressWarnings("unchecked") + @Test + public void fromArraySubscriberCount() { + ParallelFlowableTest.checkSubscriberCount(ParallelFlowable.fromArray(new Publisher[] { Flowable.just(1) })); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/parallel/ParallelFromPublisherTest.java b/src/test/java/io/reactivex/rxjava3/parallel/ParallelFromPublisherTest.java new file mode 100644 index 0000000000..7d4e684bb6 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/parallel/ParallelFromPublisherTest.java @@ -0,0 +1,295 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.parallel; + +import static org.junit.Assert.*; + +import java.util.*; +import java.util.concurrent.*; + +import org.junit.Test; +import org.reactivestreams.*; + +import io.reactivex.rxjava3.annotations.NonNull; +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.internal.subscribers.BasicFuseableSubscriber; +import io.reactivex.rxjava3.internal.subscriptions.BooleanSubscription; +import io.reactivex.rxjava3.operators.QueueFuseable; +import io.reactivex.rxjava3.operators.QueueSubscription; +import io.reactivex.rxjava3.processors.*; +import io.reactivex.rxjava3.schedulers.Schedulers; +import io.reactivex.rxjava3.testsupport.*; + +public class ParallelFromPublisherTest extends RxJavaTest { + + @Test + public void sourceOverflow() { + new Flowable<Integer>() { + @Override + protected void subscribeActual(Subscriber<? super Integer> s) { + s.onSubscribe(new BooleanSubscription()); + for (int i = 0; i < 10; i++) { + s.onNext(i); + } + } + } + .parallel(1, 1) + .sequential(1) + .test(0) + .assertFailure(QueueOverflowException.class); + } + + @Test + public void fusedFilterBecomesEmpty() { + Flowable.just(1) + .filter(Functions.alwaysFalse()) + .parallel() + .sequential() + .test() + .assertResult(); + } + + static final class StripBoundary<T> extends Flowable<T> implements FlowableTransformer<T, T> { + + final Flowable<T> source; + + StripBoundary(Flowable<T> source) { + this.source = source; + } + + @Override + public Publisher<T> apply(Flowable<T> upstream) { + return new StripBoundary<>(upstream); + } + + @Override + protected void subscribeActual(Subscriber<? super T> s) { + source.subscribe(new StripBoundarySubscriber<>(s)); + } + + static final class StripBoundarySubscriber<T> extends BasicFuseableSubscriber<T, T> { + + StripBoundarySubscriber(Subscriber<? super T> downstream) { + super(downstream); + } + + @Override + public void onNext(T t) { + downstream.onNext(t); + } + + @Override + public int requestFusion(int mode) { + QueueSubscription<T> fs = qs; + if (fs != null) { + int m = fs.requestFusion(mode & ~QueueFuseable.BOUNDARY); + this.sourceMode = m; + return m; + } + return QueueFuseable.NONE; + } + + @Override + public T poll() throws Throwable { + return qs.poll(); + } + } + } + + @Test + public void syncFusedMapCrash() { + Flowable.just(1) + .map(new Function<Integer, Object>() { + @Override + public Object apply(Integer v) throws Exception { + throw new TestException(); + } + }) + .compose(new StripBoundary<>(null)) + .parallel() + .sequential() + .test() + .assertFailure(TestException.class); + } + + @Test + public void asyncFusedMapCrash() { + UnicastProcessor<Integer> up = UnicastProcessor.create(); + + up.onNext(1); + + up + .map(new Function<Integer, Object>() { + @Override + public Object apply(Integer v) throws Exception { + throw new TestException(); + } + }) + .compose(new StripBoundary<>(null)) + .parallel() + .sequential() + .test() + .assertFailure(TestException.class); + + assertFalse(up.hasSubscribers()); + } + + @Test + public void boundaryConfinement() { + final Set<String> between = new HashSet<>(); + final ConcurrentHashMap<String, String> processing = new ConcurrentHashMap<>(); + + TestSubscriberEx<Object> ts = Flowable.range(1, 10) + .observeOn(Schedulers.single(), false, 1) + .doOnNext(new Consumer<Integer>() { + @Override + public void accept(Integer v) throws Exception { + between.add(Thread.currentThread().getName()); + } + }) + .parallel(2, 1) + .runOn(Schedulers.computation(), 1) + .map(new Function<Integer, Object>() { + @Override + public Object apply(Integer v) throws Exception { + processing.putIfAbsent(Thread.currentThread().getName(), ""); + return v; + } + }) + .sequential() + .to(TestHelper.<Object>testConsumer()) + .awaitDone(5, TimeUnit.SECONDS) + .assertSubscribed() + .assertComplete() + .assertNoErrors() + ; + + TestHelper.assertValueSet(ts, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + + assertEquals(between.toString(), 1, between.size()); + assertTrue(between.toString(), between.iterator().next().contains("RxSingleScheduler")); + + Map<String, String> map = processing; // AnimalSniffer: CHM.keySet() in Java 8 returns KeySetView + + for (String e : map.keySet()) { + assertTrue(map.toString(), e.contains("RxComputationThreadPool")); + } + } + + @Test + public void badRequest() { + TestHelper.assertBadRequestReported(PublishProcessor.create().parallel()); + } + + @Test + public void syncFusedEmptyPoll() { + Flowable.just(1, 2) + .filter(v -> v == 1) + .compose(TestHelper.flowableStripBoundary()) + .parallel(1) + .sequential() + .test() + .assertResult(1); + } + + @Test + public void asyncFusedEmptyPoll() { + UnicastProcessor<Integer> up = UnicastProcessor.create(); + up.onNext(1); + up.onNext(2); + up.onComplete(); + + up + .filter(v -> v == 1) + .compose(TestHelper.flowableStripBoundary()) + .parallel(1) + .sequential() + .test() + .assertResult(1); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeFlowable(f -> f.parallel().sequential()); + } + + @SuppressWarnings("unchecked") + @Test + public void requestUnboundedRace() { + FlowableSubscriber<Integer> fs = new FlowableSubscriber<Integer>() { + + @Override + public void onNext(@NonNull Integer t) { + } + + @Override + public void onError(Throwable t) { + } + + @Override + public void onComplete() { + } + + @Override + public void onSubscribe(@NonNull Subscription s) { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + TestHelper.race( + () -> s.request(Long.MAX_VALUE), + () -> s.request(Long.MAX_VALUE) + ); + } + } + }; + + PublishProcessor.create() + .parallel(1) + .subscribe(new FlowableSubscriber[] { fs }); + } + + @SuppressWarnings("unchecked") + @Test + public void requestRace() { + FlowableSubscriber<Integer> fs = new FlowableSubscriber<Integer>() { + + @Override + public void onNext(@NonNull Integer t) { + } + + @Override + public void onError(Throwable t) { + } + + @Override + public void onComplete() { + } + + @Override + public void onSubscribe(@NonNull Subscription s) { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + TestHelper.race( + () -> s.request(1), + () -> s.request(1) + ); + } + } + }; + + PublishProcessor.create() + .parallel(1) + .subscribe(new FlowableSubscriber[] { fs }); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/parallel/ParallelInvalid.java b/src/test/java/io/reactivex/rxjava3/parallel/ParallelInvalid.java new file mode 100644 index 0000000000..6b54ca6847 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/parallel/ParallelInvalid.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.parallel; + +import org.reactivestreams.Subscriber; + +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.internal.subscriptions.EmptySubscription; + +/** + * Signals two onErrors to each subscriber for testing purposes. + */ +public final class ParallelInvalid extends ParallelFlowable<Object> { + + @Override + public void subscribe(Subscriber<? super Object>[] subscribers) { + TestException ex = new TestException(); + for (Subscriber<? super Object> s : subscribers) { + EmptySubscription.error(ex, s); + s.onError(ex); + s.onNext(0); + s.onComplete(); + s.onComplete(); + } + } + + @Override + public int parallelism() { + return 4; + } + +} diff --git a/src/test/java/io/reactivex/rxjava3/parallel/ParallelJoinTest.java b/src/test/java/io/reactivex/rxjava3/parallel/ParallelJoinTest.java new file mode 100644 index 0000000000..8ee41cb973 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/parallel/ParallelJoinTest.java @@ -0,0 +1,526 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.parallel; + +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; + +import org.junit.Test; +import org.reactivestreams.Subscriber; + +import io.reactivex.rxjava3.annotations.NonNull; +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.subscriptions.BooleanSubscription; +import io.reactivex.rxjava3.processors.PublishProcessor; +import io.reactivex.rxjava3.subscribers.TestSubscriber; +import io.reactivex.rxjava3.testsupport.*; + +public class ParallelJoinTest extends RxJavaTest { + + @Test + public void overflowFastpath() { + new ParallelFlowable<Integer>() { + @Override + public void subscribe(Subscriber<? super Integer>[] subscribers) { + subscribers[0].onSubscribe(new BooleanSubscription()); + subscribers[0].onNext(1); + subscribers[0].onNext(2); + subscribers[0].onNext(3); + } + + @Override + public int parallelism() { + return 1; + } + } + .sequential(1) + .test(0) + .assertFailure(QueueOverflowException.class); + } + + @Test + public void overflowSlowpath() { + @SuppressWarnings("unchecked") + final Subscriber<? super Integer>[] subs = new Subscriber[1]; + + TestSubscriber<Integer> ts = new TestSubscriber<Integer>(1) { + @Override + public void onNext(Integer t) { + super.onNext(t); + subs[0].onNext(2); + subs[0].onNext(3); + } + }; + + new ParallelFlowable<Integer>() { + @Override + public void subscribe(Subscriber<? super Integer>[] subscribers) { + subs[0] = subscribers[0]; + subscribers[0].onSubscribe(new BooleanSubscription()); + subscribers[0].onNext(1); + } + + @Override + public int parallelism() { + return 1; + } + } + .sequential(1) + .subscribe(ts); + + ts.assertFailure(QueueOverflowException.class, 1); + } + + @Test + public void emptyBackpressured() { + Flowable.empty() + .parallel() + .sequential() + .test(0) + .assertResult(); + } + + @Test + public void overflowFastpathDelayError() { + new ParallelFlowable<Integer>() { + @Override + public void subscribe(Subscriber<? super Integer>[] subscribers) { + subscribers[0].onSubscribe(new BooleanSubscription()); + subscribers[0].onNext(1); + subscribers[0].onNext(2); + } + + @Override + public int parallelism() { + return 1; + } + } + .sequentialDelayError(1) + .test(0) + .requestMore(1) + .assertFailure(QueueOverflowException.class, 1); + } + + @Test + public void overflowSlowpathDelayError() { + @SuppressWarnings("unchecked") + final Subscriber<? super Integer>[] subs = new Subscriber[1]; + + TestSubscriber<Integer> ts = new TestSubscriber<Integer>(1) { + @Override + public void onNext(Integer t) { + super.onNext(t); + if (t == 1) { + subs[0].onNext(2); + subs[0].onNext(3); + } + } + }; + + new ParallelFlowable<Integer>() { + @Override + public void subscribe(Subscriber<? super Integer>[] subscribers) { + subs[0] = subscribers[0]; + subscribers[0].onSubscribe(new BooleanSubscription()); + subscribers[0].onNext(1); + } + + @Override + public int parallelism() { + return 1; + } + } + .sequentialDelayError(1) + .subscribe(ts); + + ts.request(1); + + ts.assertFailure(QueueOverflowException.class, 1, 2); + } + + @Test + public void emptyBackpressuredDelayError() { + Flowable.empty() + .parallel() + .sequentialDelayError() + .test(0) + .assertResult(); + } + + @Test + public void delayError() { + TestSubscriberEx<Integer> flow = Flowable.range(1, 2) + .parallel(2) + .map(new Function<Integer, Integer>() { + @Override + public Integer apply(Integer v) throws Exception { + throw new TestException(); + } + }) + .sequentialDelayError() + .to(TestHelper.<Integer>testConsumer()) + .assertFailure(CompositeException.class); + + List<Throwable> error = TestHelper.errorList(flow); + TestHelper.assertError(error, 0, TestException.class); + TestHelper.assertError(error, 1, TestException.class); + } + + @Test + public void normalDelayError() { + Flowable.just(1) + .parallel(1) + .sequentialDelayError(1) + .test() + .assertResult(1); + } + + @Test + public void rangeDelayError() { + Flowable.range(1, 2) + .parallel(1) + .sequentialDelayError(1) + .take(1) + .test() + .assertResult(1); + } + + @Test + public void rangeDelayErrorBackpressure() { + Flowable.range(1, 3) + .parallel(1) + .sequentialDelayError(1) + .take(2) + .rebatchRequests(1) + .test() + .assertResult(1, 2); + } + + @Test + public void rangeDelayErrorBackpressure2() { + Flowable.range(1, 3) + .parallel(1) + .sequentialDelayError(1) + .rebatchRequests(1) + .test() + .assertResult(1, 2, 3); + } + + @Test + public void delayErrorCancelBackpressured() { + TestSubscriber<Integer> ts = Flowable.range(1, 3) + .parallel(1) + .sequentialDelayError(1) + .test(0); + + ts + .cancel(); + + ts.assertEmpty(); + } + + @Test + public void delayErrorCancelBackpressured2() { + TestSubscriber<Integer> ts = Flowable.<Integer>empty() + .parallel(1) + .sequentialDelayError(1) + .test(0); + + ts.assertResult(); + } + + @Test + public void consumerCancelsAfterOne() { + TestSubscriber<Integer> ts = new TestSubscriber<Integer>(1) { + @Override + public void onNext(Integer t) { + super.onNext(t); + cancel(); + onComplete(); + } + }; + + Flowable.range(1, 3) + .parallel(1) + .sequential() + .subscribe(ts); + + ts.assertResult(1); + } + + @Test + public void delayErrorConsumerCancelsAfterOne() { + TestSubscriber<Integer> ts = new TestSubscriber<Integer>(1) { + @Override + public void onNext(Integer t) { + super.onNext(t); + cancel(); + onComplete(); + } + }; + + Flowable.range(1, 3) + .parallel(1) + .sequentialDelayError() + .subscribe(ts); + + ts.assertResult(1); + } + + @Test + public void delayErrorDrainTrigger() { + Flowable.range(1, 3) + .parallel(1) + .sequentialDelayError() + .test(0) + .requestMore(1) + .assertValues(1) + .requestMore(1) + .assertValues(1, 2) + .requestMore(1) + .assertResult(1, 2, 3); + } + + @Test + public void failedRailIsIgnored() { + Flowable.range(1, 4) + .parallel(2) + .map(new Function<Integer, Integer>() { + @Override + public Integer apply(Integer v) throws Exception { + if (v == 1) { + throw new TestException(); + } + return v; + } + }) + .sequentialDelayError() + .test() + .assertFailure(TestException.class, 2, 3, 4); + } + + @Test + public void failedRailIsIgnoredHidden() { + Flowable.range(1, 4).hide() + .parallel(2) + .map(new Function<Integer, Integer>() { + @Override + public Integer apply(Integer v) throws Exception { + if (v == 1) { + throw new TestException(); + } + return v; + } + }) + .sequentialDelayError() + .test() + .assertFailure(TestException.class, 2, 3, 4); + } + + @Test + public void takeUntil() { + Flowable.range(1, 10) + .parallel(1) + .sequential() + .takeUntil(v -> true) + .test(0L) + .requestMore(100) + .assertResult(1); + } + + @Test + public void takeUntilDelayError() { + Flowable.range(1, 10) + .parallel(1) + .sequentialDelayError() + .takeUntil(v -> true) + .test(0L) + .requestMore(100) + .assertResult(1); + } + + @Test + public void oneItemNext() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + + TestSubscriber<Integer> ts = pp.parallel(1) + .sequential() + .test(0L); + + pp.onNext(1); + + ts.requestMore(10) + .assertValuesOnly(1); + } + + @Test + public void delayErrorOneItemNext() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + + TestSubscriber<Integer> ts = pp.parallel(1) + .sequentialDelayError() + .test(0L); + + pp.onNext(1); + + ts.requestMore(10) + .assertValuesOnly(1); + } + + @Test + public void onNextWhileProcessingSlowPath() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + + TestSubscriber<Integer> ts = new TestSubscriber<Integer>() { + @Override + public void onNext(@NonNull Integer t) { + super.onNext(t); + if (t == 1) { + pp.onNext(2); + } + } + }; + + ParallelFlowable.fromArray(pp) + .sequential() + .subscribeWith(ts); + + pp.onNext(1); + + ts + .assertValuesOnly(1, 2); + } + + @Test + public void delayErrorOnNextWhileProcessingSlowPath() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + + TestSubscriber<Integer> ts = new TestSubscriber<Integer>() { + @Override + public void onNext(@NonNull Integer t) { + super.onNext(t); + if (t == 1) { + pp.onNext(2); + } + } + }; + + ParallelFlowable.fromArray(pp) + .sequentialDelayError() + .subscribeWith(ts); + + pp.onNext(1); + + ts + .assertValuesOnly(1, 2); + } + + @Test + public void badRequest() { + TestHelper.assertBadRequestReported( + ParallelFlowable.fromArray(PublishProcessor.create()) + .sequential() + ); + } + + @Test + public void onNextMissingBackpressureRace() throws Throwable { + TestHelper.withErrorTracking(errors -> { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + + AtomicReference<Subscriber<? super Integer>> ref1 = new AtomicReference<>(); + AtomicReference<Subscriber<? super Integer>> ref2 = new AtomicReference<>(); + + Flowable<Integer> f1 = new Flowable<Integer>() { + @Override + public void subscribeActual(Subscriber<? super Integer> s) { + s.onSubscribe(new BooleanSubscription()); + ref1.set(s); + } + }; + Flowable<Integer> f2 = new Flowable<Integer>() { + @Override + public void subscribeActual(Subscriber<? super Integer> s) { + s.onSubscribe(new BooleanSubscription()); + ref2.set(s); + } + }; + + ParallelFlowable.fromArray(f1, f2) + .sequential(1) + .test(0) + ; + + TestHelper.race( + () -> { + ref1.get().onNext(1); + ref1.get().onNext(2); + }, + () -> { + ref2.get().onNext(3); + ref2.get().onNext(4); + } + ); + + errors.clear(); + } + }); + } + + @Test + public void onNextMissingBackpressureDelayErrorRace() throws Throwable { + TestHelper.withErrorTracking(errors -> { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + + AtomicReference<Subscriber<? super Integer>> ref1 = new AtomicReference<>(); + AtomicReference<Subscriber<? super Integer>> ref2 = new AtomicReference<>(); + + Flowable<Integer> f1 = new Flowable<Integer>() { + @Override + public void subscribeActual(Subscriber<? super Integer> s) { + s.onSubscribe(new BooleanSubscription()); + ref1.set(s); + } + }; + Flowable<Integer> f2 = new Flowable<Integer>() { + @Override + public void subscribeActual(Subscriber<? super Integer> s) { + s.onSubscribe(new BooleanSubscription()); + ref2.set(s); + } + }; + + ParallelFlowable.fromArray(f1, f2) + .sequentialDelayError(1) + .test(0) + ; + + TestHelper.race( + () -> { + ref1.get().onNext(1); + ref1.get().onNext(2); + }, + () -> { + ref2.get().onNext(3); + ref2.get().onNext(4); + } + ); + + errors.clear(); + } + }); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/parallel/ParallelMapTest.java b/src/test/java/io/reactivex/rxjava3/parallel/ParallelMapTest.java new file mode 100644 index 0000000000..493b74d34e --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/parallel/ParallelMapTest.java @@ -0,0 +1,227 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.parallel; + +import static org.junit.Assert.*; + +import java.util.List; +import java.util.concurrent.TimeUnit; + +import org.junit.Test; +import org.reactivestreams.Subscriber; + +import io.reactivex.rxjava3.annotations.NonNull; +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.internal.subscriptions.BooleanSubscription; +import io.reactivex.rxjava3.operators.ConditionalSubscriber; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.schedulers.Schedulers; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class ParallelMapTest extends RxJavaTest { + + @Test + public void subscriberCount() { + ParallelFlowableTest.checkSubscriberCount(Flowable.range(1, 5).parallel() + .map(Functions.identity())); + } + + @Test + public void doubleFilter() { + Flowable.range(1, 10) + .parallel() + .map(Functions.<Integer>identity()) + .filter(new Predicate<Integer>() { + @Override + public boolean test(Integer v) throws Exception { + return v % 2 == 0; + } + }) + .filter(new Predicate<Integer>() { + @Override + public boolean test(Integer v) throws Exception { + return v % 3 == 0; + } + }) + .sequential() + .test() + .assertResult(6); + } + + @Test + public void doubleFilterAsync() { + Flowable.range(1, 10) + .parallel() + .runOn(Schedulers.computation()) + .map(Functions.<Integer>identity()) + .filter(new Predicate<Integer>() { + @Override + public boolean test(Integer v) throws Exception { + return v % 2 == 0; + } + }) + .filter(new Predicate<Integer>() { + @Override + public boolean test(Integer v) throws Exception { + return v % 3 == 0; + } + }) + .sequential() + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(6); + } + + @Test + public void doubleError() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + new ParallelInvalid() + .map(Functions.<Object>identity()) + .sequential() + .test() + .assertFailure(TestException.class); + + assertFalse(errors.isEmpty()); + for (Throwable ex : errors) { + assertTrue(ex.toString(), ex.getCause() instanceof TestException); + } + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void doubleError2() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + new ParallelInvalid() + .map(Functions.<Object>identity()) + .filter(Functions.alwaysTrue()) + .sequential() + .test() + .assertFailure(TestException.class); + + assertFalse(errors.isEmpty()); + for (Throwable ex : errors) { + assertTrue(ex.toString(), ex.getCause() instanceof TestException); + } + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void error() { + Flowable.error(new TestException()) + .parallel() + .map(Functions.<Object>identity()) + .sequential() + .test() + .assertFailure(TestException.class); + } + + @Test + public void mapCrash() { + Flowable.just(1) + .parallel() + .map(new Function<Integer, Object>() { + @Override + public Object apply(Integer v) throws Exception { + throw new TestException(); + } + }) + .sequential() + .test() + .assertFailure(TestException.class); + } + + @Test + public void mapCrashConditional() { + Flowable.just(1) + .parallel() + .map(new Function<Integer, Object>() { + @Override + public Object apply(Integer v) throws Exception { + throw new TestException(); + } + }) + .filter(Functions.alwaysTrue()) + .sequential() + .test() + .assertFailure(TestException.class); + } + + @Test + public void mapCrashConditional2() { + Flowable.just(1) + .parallel() + .runOn(Schedulers.computation()) + .map(new Function<Integer, Object>() { + @Override + public Object apply(Integer v) throws Exception { + throw new TestException(); + } + }) + .filter(Functions.alwaysTrue()) + .sequential() + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertFailure(TestException.class); + } + + @Test + public void invalidSubscriberCount() { + TestHelper.checkInvalidParallelSubscribers( + Flowable.range(1, 10).parallel() + .map(v -> v) + ); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeParallel( + p -> p.map(v -> v) + ); + + TestHelper.checkDoubleOnSubscribeParallel( + p -> p.map(v -> v) + .filter(v -> true) + ); + } + + @Test + public void conditionalCancelIgnored() { + Flowable<Integer> f = new Flowable<Integer>() { + @Override + protected void subscribeActual(@NonNull Subscriber<@NonNull ? super @NonNull Integer> s) { + @SuppressWarnings("unchecked") + ConditionalSubscriber<Integer> subscriber = (ConditionalSubscriber<Integer>)s; + subscriber.onSubscribe(new BooleanSubscription()); + subscriber.tryOnNext(1); + subscriber.tryOnNext(2); + } + }; + + ParallelFlowable.fromArray(f) + .map(v -> { throw new TestException(); }) + .filter(v -> true) + .sequential() + .test() + .assertFailure(TestException.class); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/parallel/ParallelMapTryTest.java b/src/test/java/io/reactivex/rxjava3/parallel/ParallelMapTryTest.java new file mode 100644 index 0000000000..a6d0de2d92 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/parallel/ParallelMapTryTest.java @@ -0,0 +1,376 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.parallel; + +import java.util.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.testsupport.*; + +public class ParallelMapTryTest extends RxJavaTest implements Consumer<Object> { + + volatile int calls; + + @Override + public void accept(Object t) throws Exception { + calls++; + } + + @Test + public void mapNoError() { + for (ParallelFailureHandling e : ParallelFailureHandling.values()) { + Flowable.just(1) + .parallel(1) + .map(Functions.identity(), e) + .sequential() + .test() + .assertResult(1); + } + } + + @Test + public void mapErrorNoError() { + for (ParallelFailureHandling e : ParallelFailureHandling.values()) { + Flowable.<Integer>error(new TestException()) + .parallel(1) + .map(Functions.identity(), e) + .sequential() + .test() + .assertFailure(TestException.class); + } + } + + @Test + public void mapConditionalNoError() { + for (ParallelFailureHandling e : ParallelFailureHandling.values()) { + Flowable.just(1) + .parallel(1) + .map(Functions.identity(), e) + .filter(Functions.alwaysTrue()) + .sequential() + .test() + .assertResult(1); + } + } + + @Test + public void mapErrorConditionalNoError() { + for (ParallelFailureHandling e : ParallelFailureHandling.values()) { + Flowable.<Integer>error(new TestException()) + .parallel(1) + .map(Functions.identity(), e) + .filter(Functions.alwaysTrue()) + .sequential() + .test() + .assertFailure(TestException.class); + } + } + + @Test + public void mapFailWithError() { + Flowable.range(0, 2) + .parallel(1) + .map(new Function<Integer, Integer>() { + @Override + public Integer apply(Integer v) throws Exception { + return 1 / v; + } + }, ParallelFailureHandling.ERROR) + .sequential() + .test() + .assertFailure(ArithmeticException.class); + } + + @Test + public void mapFailWithStop() { + Flowable.range(0, 2) + .parallel(1) + .map(new Function<Integer, Integer>() { + @Override + public Integer apply(Integer v) throws Exception { + return 1 / v; + } + }, ParallelFailureHandling.STOP) + .sequential() + .test() + .assertResult(); + } + + @Test + public void mapFailWithRetry() { + Flowable.range(0, 2) + .parallel(1) + .map(new Function<Integer, Integer>() { + int count; + @Override + public Integer apply(Integer v) throws Exception { + if (count++ == 1) { + return -1; + } + return 1 / v; + } + }, ParallelFailureHandling.RETRY) + .sequential() + .test() + .assertResult(-1, 1); + } + + @Test + public void mapFailWithRetryLimited() { + Flowable.range(0, 2) + .parallel(1) + .map(new Function<Integer, Integer>() { + @Override + public Integer apply(Integer v) throws Exception { + return 1 / v; + } + }, new BiFunction<Long, Throwable, ParallelFailureHandling>() { + @Override + public ParallelFailureHandling apply(Long n, Throwable e) throws Exception { + return n < 5 ? ParallelFailureHandling.RETRY : ParallelFailureHandling.SKIP; + } + }) + .sequential() + .test() + .assertResult(1); + } + + @Test + public void mapFailWithSkip() { + Flowable.range(0, 2) + .parallel(1) + .map(new Function<Integer, Integer>() { + @Override + public Integer apply(Integer v) throws Exception { + return 1 / v; + } + }, ParallelFailureHandling.SKIP) + .sequential() + .test() + .assertResult(1); + } + + @Test + public void mapFailHandlerThrows() { + TestSubscriberEx<Integer> ts = Flowable.range(0, 2) + .parallel(1) + .map(new Function<Integer, Integer>() { + @Override + public Integer apply(Integer v) throws Exception { + return 1 / v; + } + }, new BiFunction<Long, Throwable, ParallelFailureHandling>() { + @Override + public ParallelFailureHandling apply(Long n, Throwable e) throws Exception { + throw new TestException(); + } + }) + .sequential() + .to(TestHelper.<Integer>testConsumer()) + .assertFailure(CompositeException.class); + + TestHelper.assertCompositeExceptions(ts, ArithmeticException.class, TestException.class); + } + + @Test + public void mapWrongParallelism() { + TestHelper.checkInvalidParallelSubscribers( + Flowable.just(1).parallel(1) + .map(Functions.identity(), ParallelFailureHandling.ERROR) + ); + } + + @Test + public void mapInvalidSource() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + new ParallelInvalid() + .map(Functions.identity(), ParallelFailureHandling.ERROR) + .sequential() + .test(); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void mapFailWithErrorConditional() { + Flowable.range(0, 2) + .parallel(1) + .map(new Function<Integer, Integer>() { + @Override + public Integer apply(Integer v) throws Exception { + return 1 / v; + } + }, ParallelFailureHandling.ERROR) + .filter(Functions.alwaysTrue()) + .sequential() + .test() + .assertFailure(ArithmeticException.class); + } + + @Test + public void mapFailWithStopConditional() { + Flowable.range(0, 2) + .parallel(1) + .map(new Function<Integer, Integer>() { + @Override + public Integer apply(Integer v) throws Exception { + return 1 / v; + } + }, ParallelFailureHandling.STOP) + .filter(Functions.alwaysTrue()) + .sequential() + .test() + .assertResult(); + } + + @Test + public void mapFailWithRetryConditional() { + Flowable.range(0, 2) + .parallel(1) + .map(new Function<Integer, Integer>() { + int count; + @Override + public Integer apply(Integer v) throws Exception { + if (count++ == 1) { + return -1; + } + return 1 / v; + } + }, ParallelFailureHandling.RETRY) + .filter(Functions.alwaysTrue()) + .sequential() + .test() + .assertResult(-1, 1); + } + + @Test + public void mapFailWithRetryLimitedConditional() { + Flowable.range(0, 2) + .parallel(1) + .map(new Function<Integer, Integer>() { + @Override + public Integer apply(Integer v) throws Exception { + return 1 / v; + } + }, new BiFunction<Long, Throwable, ParallelFailureHandling>() { + @Override + public ParallelFailureHandling apply(Long n, Throwable e) throws Exception { + return n < 5 ? ParallelFailureHandling.RETRY : ParallelFailureHandling.SKIP; + } + }) + .filter(Functions.alwaysTrue()) + .sequential() + .test() + .assertResult(1); + } + + @Test + public void mapFailWithSkipConditional() { + Flowable.range(0, 2) + .parallel(1) + .map(new Function<Integer, Integer>() { + @Override + public Integer apply(Integer v) throws Exception { + return 1 / v; + } + }, ParallelFailureHandling.SKIP) + .filter(Functions.alwaysTrue()) + .sequential() + .test() + .assertResult(1); + } + + @Test + public void mapFailHandlerThrowsConditional() { + TestSubscriberEx<Integer> ts = Flowable.range(0, 2) + .parallel(1) + .map(new Function<Integer, Integer>() { + @Override + public Integer apply(Integer v) throws Exception { + return 1 / v; + } + }, new BiFunction<Long, Throwable, ParallelFailureHandling>() { + @Override + public ParallelFailureHandling apply(Long n, Throwable e) throws Exception { + throw new TestException(); + } + }) + .filter(Functions.alwaysTrue()) + .sequential() + .to(TestHelper.<Integer>testConsumer()) + .assertFailure(CompositeException.class); + + TestHelper.assertCompositeExceptions(ts, ArithmeticException.class, TestException.class); + } + + @Test + public void mapWrongParallelismConditional() { + TestHelper.checkInvalidParallelSubscribers( + Flowable.just(1).parallel(1) + .map(Functions.identity(), ParallelFailureHandling.ERROR) + .filter(Functions.alwaysTrue()) + ); + } + + @Test + public void mapInvalidSourceConditional() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + new ParallelInvalid() + .map(Functions.identity(), ParallelFailureHandling.ERROR) + .filter(Functions.alwaysTrue()) + .sequential() + .test(); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void failureHandlingEnum() { + TestHelper.checkEnum(ParallelFailureHandling.class); + } + + @Test + public void invalidSubscriberCount() { + TestHelper.checkInvalidParallelSubscribers( + Flowable.range(1, 10).parallel() + .map(v -> v, ParallelFailureHandling.SKIP) + ); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeParallel( + p -> p.map(v -> v, ParallelFailureHandling.ERROR) + ); + + TestHelper.checkDoubleOnSubscribeParallel( + p -> p.map(v -> v, ParallelFailureHandling.ERROR) + .filter(v -> true) + ); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/parallel/ParallelPeekTest.java b/src/test/java/io/reactivex/rxjava3/parallel/ParallelPeekTest.java new file mode 100644 index 0000000000..93d72ef9f6 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/parallel/ParallelPeekTest.java @@ -0,0 +1,208 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.parallel; + +import static org.junit.Assert.*; + +import java.io.IOException; +import java.util.List; + +import org.junit.Test; +import org.reactivestreams.Subscription; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.testsupport.*; + +public class ParallelPeekTest extends RxJavaTest { + + @Test + public void subscriberCount() { + ParallelFlowableTest.checkSubscriberCount(Flowable.range(1, 5).parallel() + .doOnNext(Functions.emptyConsumer())); + } + + @Test + @SuppressUndeliverable + public void onSubscribeCrash() { + Flowable.range(1, 5) + .parallel() + .doOnSubscribe(new Consumer<Subscription>() { + @Override + public void accept(Subscription s) throws Exception { + throw new TestException(); + } + }) + .sequential() + .test() + .assertFailure(TestException.class); + } + + @Test + public void doubleError() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + new ParallelInvalid() + .doOnNext(Functions.emptyConsumer()) + .sequential() + .test() + .assertFailure(TestException.class); + + assertFalse(errors.isEmpty()); + for (Throwable ex : errors) { + assertTrue(ex.toString(), ex.getCause() instanceof TestException); + } + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void requestCrash() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + + try { + Flowable.range(1, 5) + .parallel() + .doOnRequest(new LongConsumer() { + @Override + public void accept(long n) throws Exception { + throw new TestException(); + } + }) + .sequential() + .test() + .assertResult(1, 2, 3, 4, 5); + + assertFalse(errors.isEmpty()); + + for (Throwable ex : errors) { + assertTrue(ex.toString(), ex.getCause() instanceof TestException); + } + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void cancelCrash() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + + try { + Flowable.<Integer>never() + .parallel() + .doOnCancel(new Action() { + @Override + public void run() throws Exception { + throw new TestException(); + } + }) + .sequential() + .test() + .cancel(); + + assertFalse(errors.isEmpty()); + + for (Throwable ex : errors) { + assertTrue(ex.toString(), ex.getCause() instanceof TestException); + } + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + @SuppressUndeliverable + public void onCompleteCrash() { + Flowable.just(1) + .parallel() + .doOnComplete(new Action() { + @Override + public void run() throws Exception { + throw new TestException(); + } + }) + .sequential() + .test() + .assertFailure(TestException.class, 1); + } + + @Test + public void onAfterTerminatedCrash() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + + try { + Flowable.just(1) + .parallel() + .doAfterTerminated(new Action() { + @Override + public void run() throws Exception { + throw new TestException(); + } + }) + .sequential() + .test() + .assertResult(1); + + assertFalse(errors.isEmpty()); + + for (Throwable ex : errors) { + assertTrue(ex.toString(), ex.getCause() instanceof TestException); + } + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void onAfterTerminatedCrash2() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + + try { + Flowable.<Integer>error(new IOException()) + .parallel() + .doAfterTerminated(new Action() { + @Override + public void run() throws Exception { + throw new TestException(); + } + }) + .sequential() + .test() + .assertFailure(IOException.class); + + assertFalse(errors.isEmpty()); + + for (Throwable ex : errors) { + Throwable exc = ex.getCause(); + assertTrue(ex.toString(), exc instanceof TestException + || exc instanceof IOException); + } + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeFlowable(f -> + ParallelFlowable.fromArray(f) + .doOnComplete(() -> { }) + .sequential() + ); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/parallel/ParallelReduceFullTest.java b/src/test/java/io/reactivex/rxjava3/parallel/ParallelReduceFullTest.java new file mode 100644 index 0000000000..d9a43a247c --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/parallel/ParallelReduceFullTest.java @@ -0,0 +1,174 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.parallel; + +import static org.junit.Assert.*; + +import java.io.IOException; +import java.util.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.BiFunction; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.processors.PublishProcessor; +import io.reactivex.rxjava3.subscribers.TestSubscriber; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class ParallelReduceFullTest extends RxJavaTest { + + @Test + public void cancel() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + + TestSubscriber<Integer> ts = pp + .parallel() + .reduce(new BiFunction<Integer, Integer, Integer>() { + @Override + public Integer apply(Integer a, Integer b) throws Exception { + return a + b; + } + }) + .test(); + + assertTrue(pp.hasSubscribers()); + + ts.cancel(); + + assertFalse(pp.hasSubscribers()); + } + + @Test + public void error() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + + try { + Flowable.<Integer>error(new TestException()) + .parallel() + .reduce(new BiFunction<Integer, Integer, Integer>() { + @Override + public Integer apply(Integer a, Integer b) throws Exception { + return a + b; + } + }) + .test() + .assertFailure(TestException.class); + + assertTrue(errors.isEmpty()); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void error2() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + + try { + ParallelFlowable.fromArray(Flowable.<Integer>error(new IOException()), Flowable.<Integer>error(new TestException())) + .reduce(new BiFunction<Integer, Integer, Integer>() { + @Override + public Integer apply(Integer a, Integer b) throws Exception { + return a + b; + } + }) + .test() + .assertFailure(IOException.class); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void empty() { + Flowable.<Integer>empty() + .parallel() + .reduce(new BiFunction<Integer, Integer, Integer>() { + @Override + public Integer apply(Integer a, Integer b) throws Exception { + return a + b; + } + }) + .test() + .assertResult(); + } + + @Test + public void doubleError() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + new ParallelInvalid() + .reduce(new BiFunction<Object, Object, Object>() { + @Override + public Object apply(Object a, Object b) throws Exception { + return "" + a + b; + } + }) + .test() + .assertFailure(TestException.class); + + assertFalse(errors.isEmpty()); + for (Throwable ex : errors) { + assertTrue(ex.toString(), ex.getCause() instanceof TestException); + } + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void reducerCrash() { + Flowable.range(1, 4) + .parallel(2) + .reduce(new BiFunction<Integer, Integer, Integer>() { + @Override + public Integer apply(Integer a, Integer b) throws Exception { + if (b == 3) { + throw new TestException(); + } + return a + b; + } + }) + .test() + .assertFailure(TestException.class); + } + + @Test + public void reducerCrash2() { + Flowable.range(1, 4) + .parallel(2) + .reduce(new BiFunction<Integer, Integer, Integer>() { + @Override + public Integer apply(Integer a, Integer b) throws Exception { + if (a == 1 + 3) { + throw new TestException(); + } + return a + b; + } + }) + .test() + .assertFailure(TestException.class); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeParallelToFlowable( + pf -> pf.reduce((a, b) -> a) + ); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/parallel/ParallelReduceTest.java b/src/test/java/io/reactivex/rxjava3/parallel/ParallelReduceTest.java new file mode 100644 index 0000000000..9c32f6afc8 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/parallel/ParallelReduceTest.java @@ -0,0 +1,179 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.parallel; + +import static org.junit.Assert.*; + +import java.util.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.processors.PublishProcessor; +import io.reactivex.rxjava3.subscribers.TestSubscriber; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class ParallelReduceTest extends RxJavaTest { + + @Test + public void subscriberCount() { + ParallelFlowableTest.checkSubscriberCount(Flowable.range(1, 5).parallel() + .reduce(new Supplier<List<Integer>>() { + @Override + public List<Integer> get() throws Exception { + return new ArrayList<>(); + } + }, new BiFunction<List<Integer>, Integer, List<Integer>>() { + @Override + public List<Integer> apply(List<Integer> a, Integer b) throws Exception { + a.add(b); + return a; + } + })); + } + + @Test + public void initialCrash() { + Flowable.range(1, 5) + .parallel() + .reduce(new Supplier<List<Integer>>() { + @Override + public List<Integer> get() throws Exception { + throw new TestException(); + } + }, new BiFunction<List<Integer>, Integer, List<Integer>>() { + @Override + public List<Integer> apply(List<Integer> a, Integer b) throws Exception { + a.add(b); + return a; + } + }) + .sequential() + .test() + .assertFailure(TestException.class); + } + + @Test + public void reducerCrash() { + Flowable.range(1, 5) + .parallel() + .reduce(new Supplier<List<Integer>>() { + @Override + public List<Integer> get() throws Exception { + return new ArrayList<>(); + } + }, new BiFunction<List<Integer>, Integer, List<Integer>>() { + @Override + public List<Integer> apply(List<Integer> a, Integer b) throws Exception { + if (b == 3) { + throw new TestException(); + } + a.add(b); + return a; + } + }) + .sequential() + .test() + .assertFailure(TestException.class); + } + + @Test + public void cancel() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + + TestSubscriber<List<Integer>> ts = pp + .parallel() + .reduce(new Supplier<List<Integer>>() { + @Override + public List<Integer> get() throws Exception { + return new ArrayList<>(); + } + }, new BiFunction<List<Integer>, Integer, List<Integer>>() { + @Override + public List<Integer> apply(List<Integer> a, Integer b) throws Exception { + a.add(b); + return a; + } + }) + .sequential() + .test(); + + assertTrue(pp.hasSubscribers()); + + ts.cancel(); + + assertFalse(pp.hasSubscribers()); + } + + @Test + public void error() { + Flowable.<Integer>error(new TestException()) + .parallel() + .reduce(new Supplier<List<Integer>>() { + @Override + public List<Integer> get() throws Exception { + return new ArrayList<>(); + } + }, new BiFunction<List<Integer>, Integer, List<Integer>>() { + @Override + public List<Integer> apply(List<Integer> a, Integer b) throws Exception { + a.add(b); + return a; + } + }) + .sequential() + .test() + .assertFailure(TestException.class); + } + + @Test + public void doubleError() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + new ParallelInvalid() + .reduce(new Supplier<List<Object>>() { + @Override + public List<Object> get() throws Exception { + return new ArrayList<>(); + } + }, new BiFunction<List<Object>, Object, List<Object>>() { + @Override + public List<Object> apply(List<Object> a, Object b) throws Exception { + a.add(b); + return a; + } + }) + .sequential() + .test() + .assertFailure(TestException.class); + + assertFalse(errors.isEmpty()); + for (Throwable ex : errors) { + assertTrue(ex.toString(), ex.getCause() instanceof TestException); + } + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeParallel( + pf -> pf.reduce(ArrayList::new, (a, b) -> a) + ); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/parallel/ParallelRunOnTest.java b/src/test/java/io/reactivex/rxjava3/parallel/ParallelRunOnTest.java new file mode 100644 index 0000000000..44a860263a --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/parallel/ParallelRunOnTest.java @@ -0,0 +1,381 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.parallel; + +import static org.junit.Assert.*; + +import java.util.List; +import java.util.concurrent.TimeUnit; + +import org.junit.Test; +import org.reactivestreams.Subscriber; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.Predicate; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.internal.schedulers.ImmediateThinScheduler; +import io.reactivex.rxjava3.internal.subscriptions.BooleanSubscription; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.processors.PublishProcessor; +import io.reactivex.rxjava3.schedulers.Schedulers; +import io.reactivex.rxjava3.subscribers.TestSubscriber; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class ParallelRunOnTest extends RxJavaTest { + + @Test + public void subscriberCount() { + ParallelFlowableTest.checkSubscriberCount(Flowable.range(1, 5).parallel() + .runOn(Schedulers.computation())); + } + + @Test + public void doubleError() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + new ParallelInvalid() + .runOn(ImmediateThinScheduler.INSTANCE) + .sequential() + .test() + .assertFailure(TestException.class); + + assertFalse(errors.isEmpty()); + for (Throwable ex : errors) { + assertTrue(ex.toString(), ex.getCause() instanceof TestException); + } + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void conditionalPath() { + Flowable.range(1, 1000) + .parallel(2) + .runOn(Schedulers.computation()) + .filter(new Predicate<Integer>() { + @Override + public boolean test(Integer v) throws Exception { + return v % 2 == 0; + } + }) + .sequential() + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertValueCount(500) + .assertNoErrors() + .assertComplete(); + } + + @Test + public void missingBackpressure() { + new ParallelFlowable<Integer>() { + @Override + public int parallelism() { + return 1; + } + + @Override + public void subscribe(Subscriber<? super Integer>[] subscribers) { + subscribers[0].onSubscribe(new BooleanSubscription()); + subscribers[0].onNext(1); + subscribers[0].onNext(2); + subscribers[0].onNext(3); + } + } + .runOn(ImmediateThinScheduler.INSTANCE, 1) + .sequential(1) + .test(0) + .assertFailure(QueueOverflowException.class); + } + + @Test + public void error() { + Flowable.error(new TestException()) + .parallel(1) + .runOn(ImmediateThinScheduler.INSTANCE) + .sequential() + .test() + .assertFailure(TestException.class); + } + + @Test + public void errorBackpressured() { + Flowable.error(new TestException()) + .parallel(1) + .runOn(ImmediateThinScheduler.INSTANCE) + .sequential(1) + .test(0) + .assertFailure(TestException.class); + } + + @Test + public void errorConditional() { + Flowable.error(new TestException()) + .parallel(1) + .runOn(ImmediateThinScheduler.INSTANCE) + .filter(Functions.alwaysTrue()) + .sequential() + .test() + .assertFailure(TestException.class); + } + + @SuppressWarnings("unchecked") + @Test + public void errorConditionalBackpressured() { + TestSubscriber<Object> ts = new TestSubscriber<>(0L); + + Flowable.error(new TestException()) + .parallel(1) + .runOn(ImmediateThinScheduler.INSTANCE) + .filter(Functions.alwaysTrue()) + .subscribe(new Subscriber[] { ts }); + + ts + .assertFailure(TestException.class); + } + + @SuppressWarnings("unchecked") + @Test + public void emptyConditionalBackpressured() { + TestSubscriber<Object> ts = new TestSubscriber<>(0L); + + Flowable.empty() + .parallel(1) + .runOn(ImmediateThinScheduler.INSTANCE) + .filter(Functions.alwaysTrue()) + .subscribe(new Subscriber[] { ts }); + + ts + .assertResult(); + } + + @Test + public void nextCancelRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final PublishProcessor<Integer> pp = PublishProcessor.create(); + + final TestSubscriber<Integer> ts = pp.parallel(1) + .runOn(Schedulers.computation()) + .sequential() + .test(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + pp.onNext(1); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + ts.cancel(); + } + }; + + TestHelper.race(r1, r2); + } + } + + @SuppressWarnings("unchecked") + @Test + public void nextCancelRaceBackpressured() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final PublishProcessor<Integer> pp = PublishProcessor.create(); + + final TestSubscriber<Integer> ts = TestSubscriber.create(0L); + + pp.parallel(1) + .runOn(Schedulers.computation()) + .subscribe(new Subscriber[] { ts }); + + Runnable r1 = new Runnable() { + @Override + public void run() { + pp.onNext(1); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + ts.cancel(); + } + }; + + TestHelper.race(r1, r2); + } + } + + @Test + public void nextCancelRaceConditional() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final PublishProcessor<Integer> pp = PublishProcessor.create(); + + final TestSubscriber<Integer> ts = pp.parallel(1) + .runOn(Schedulers.computation()) + .filter(Functions.alwaysTrue()) + .sequential() + .test(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + pp.onNext(1); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + ts.cancel(); + } + }; + + TestHelper.race(r1, r2); + } + } + + @SuppressWarnings("unchecked") + @Test + public void nextCancelRaceBackpressuredConditional() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final PublishProcessor<Integer> pp = PublishProcessor.create(); + + final TestSubscriber<Integer> ts = TestSubscriber.create(0L); + + pp.parallel(1) + .runOn(Schedulers.computation()) + .filter(Functions.alwaysTrue()) + .subscribe(new Subscriber[] { ts }); + + Runnable r1 = new Runnable() { + @Override + public void run() { + pp.onNext(1); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + ts.cancel(); + } + }; + + TestHelper.race(r1, r2); + } + } + + @SuppressWarnings("unchecked") + @Test + public void normalCancelAfterRequest1() { + + TestSubscriber<Integer> ts = new TestSubscriber<Integer>(1) { + @Override + public void onNext(Integer t) { + super.onNext(t); + cancel(); + onComplete(); + } + }; + + Flowable.range(1, 5) + .parallel(1) + .runOn(ImmediateThinScheduler.INSTANCE) + .subscribe(new Subscriber[] { ts }); + + ts.assertResult(1); + } + + @SuppressWarnings("unchecked") + @Test + public void conditionalCancelAfterRequest1() { + + TestSubscriber<Integer> ts = new TestSubscriber<Integer>(1) { + @Override + public void onNext(Integer t) { + super.onNext(t); + cancel(); + onComplete(); + } + }; + + Flowable.range(1, 5) + .parallel(1) + .runOn(ImmediateThinScheduler.INSTANCE) + .filter(Functions.alwaysTrue()) + .subscribe(new Subscriber[] { ts }); + + ts.assertResult(1); + } + + @Test + public void doubleOnSubscribe() { + TestHelper.checkDoubleOnSubscribeParallel(pf -> pf.runOn(ImmediateThinScheduler.INSTANCE)); + } + + @Test + public void doubleOnSubscribeConditional() { + TestHelper.checkDoubleOnSubscribeParallel(pf -> + pf.runOn(ImmediateThinScheduler.INSTANCE) + .filter(v -> true) + ); + } + + @Test + public void badRequest() { + TestHelper.assertBadRequestReported( + ParallelFlowable.fromArray(PublishProcessor.create()) + .runOn(ImmediateThinScheduler.INSTANCE) + ); + } + + @SuppressWarnings("unchecked") + @Test + public void asManyItemsAsRequested() { + TestSubscriber<Integer> ts = new TestSubscriber<>(0); + + Flowable.range(1, 5) + .parallel(1) + .runOn(ImmediateThinScheduler.INSTANCE) + .subscribe(new Subscriber[] { + ts + }); + + ts + .requestMore(5) + .assertResult(1, 2, 3, 4, 5); + } + + @SuppressWarnings("unchecked") + @Test + public void asManyItemsAsRequestedConditional() { + TestSubscriber<Integer> ts = new TestSubscriber<>(0); + + Flowable.range(1, 5) + .parallel(1) + .runOn(ImmediateThinScheduler.INSTANCE) + .filter(v -> true) + .subscribe(new Subscriber[] { + ts + }); + + ts + .requestMore(5) + .assertResult(1, 2, 3, 4, 5); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/parallel/ParallelSortedJoinTest.java b/src/test/java/io/reactivex/rxjava3/parallel/ParallelSortedJoinTest.java new file mode 100644 index 0000000000..9ba23c30c2 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/parallel/ParallelSortedJoinTest.java @@ -0,0 +1,239 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.parallel; + +import static org.junit.Assert.*; + +import java.io.IOException; +import java.util.*; +import java.util.concurrent.TimeUnit; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.internal.operators.parallel.ParallelSortedJoin; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.processors.*; +import io.reactivex.rxjava3.schedulers.Schedulers; +import io.reactivex.rxjava3.subscribers.TestSubscriber; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class ParallelSortedJoinTest extends RxJavaTest { + + @Test + public void cancel() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + + TestSubscriber<Integer> ts = pp + .parallel() + .sorted(Functions.<Integer>naturalComparator()) + .test(); + + assertTrue(pp.hasSubscribers()); + + ts.cancel(); + + assertFalse(pp.hasSubscribers()); + } + + @Test + public void error() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + + try { + Flowable.<Integer>error(new TestException()) + .parallel() + .sorted(Functions.<Integer>naturalComparator()) + .test() + .assertFailure(TestException.class); + + assertTrue(errors.isEmpty()); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void error3() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + + try { + Flowable.<Integer>error(new TestException()) + .parallel() + .sorted(Functions.<Integer>naturalComparator()) + .test(0) + .assertFailure(TestException.class); + + assertTrue(errors.isEmpty()); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void error2() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + + try { + ParallelFlowable.fromArray(Flowable.<Integer>error(new IOException()), Flowable.<Integer>error(new TestException())) + .sorted(Functions.<Integer>naturalComparator()) + .test() + .assertFailure(IOException.class); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void comparerCrash() { + Flowable.fromArray(4, 3, 2, 1) + .parallel(2) + .sorted(new Comparator<Integer>() { + @Override + public int compare(Integer o1, Integer o2) { + if (o1 == 4 && o2 == 3) { + throw new TestException(); + } + return o1.compareTo(o2); + } + }) + .test() + .assertFailure(TestException.class, 1, 2); + } + + @Test + public void empty() { + Flowable.<Integer>empty() + .parallel() + .sorted(Functions.<Integer>naturalComparator()) + .test() + .assertResult(); + } + + @Test + public void asyncDrain() { + Integer[] values = new Integer[100 * 1000]; + for (int i = 0; i < values.length; i++) { + values[i] = values.length - i; + } + + TestSubscriber<Integer> ts = Flowable.fromArray(values) + .parallel(2) + .runOn(Schedulers.computation()) + .sorted(Functions.naturalComparator()) + .observeOn(Schedulers.single()) + .test(); + + ts + .awaitDone(5, TimeUnit.SECONDS) + .assertValueCount(values.length) + .assertNoErrors() + .assertComplete(); + + List<Integer> list = ts.values(); + for (int i = 0; i < values.length; i++) { + assertEquals(i + 1, list.get(i).intValue()); + } + } + + @Test + public void sortCancelRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final ReplayProcessor<Integer> pp = ReplayProcessor.create(); + pp.onNext(1); + pp.onNext(2); + + final TestSubscriber<Integer> ts = pp.parallel(2) + .sorted(Functions.naturalComparator()) + .test(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + pp.onComplete(); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + ts.cancel(); + } + }; + + TestHelper.race(r1, r2); + } + } + + @Test + public void sortCancelRace2() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final ReplayProcessor<Integer> pp = ReplayProcessor.create(); + pp.onNext(1); + pp.onNext(2); + + final TestSubscriber<Integer> ts = pp.parallel(2) + .sorted(Functions.naturalComparator()) + .test(0); + + Runnable r1 = new Runnable() { + @Override + public void run() { + pp.onComplete(); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + ts.cancel(); + } + }; + + TestHelper.race(r1, r2); + } + } + + @Test + public void badRequest() { + TestHelper.assertBadRequestReported(PublishProcessor.<Integer>create().parallel().sorted(Functions.naturalComparator())); + } + + @Test + public void comparatorCrashWhileMainOnError() throws Throwable { + TestHelper.withErrorTracking(errors -> { + PublishProcessor<List<Integer>> pp1 = PublishProcessor.create(); + PublishProcessor<List<Integer>> pp2 = PublishProcessor.create(); + + new ParallelSortedJoin<>(ParallelFlowable.fromArray(pp1, pp2) + , (a, b) -> { + pp1.onError(new IOException()); + throw new TestException(); + }) + .test(); + + pp1.onNext(Arrays.asList(1)); + pp2.onNext(Arrays.asList(2)); + + pp1.onComplete(); + pp2.onComplete(); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + }); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/plugins/RxJavaPluginsTest.java b/src/test/java/io/reactivex/rxjava3/plugins/RxJavaPluginsTest.java new file mode 100644 index 0000000000..6aa166d118 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/plugins/RxJavaPluginsTest.java @@ -0,0 +1,1799 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.plugins; + +import static org.junit.Assert.*; + +import java.io.*; +import java.lang.Thread.UncaughtExceptionHandler; +import java.lang.reflect.*; +import java.util.*; +import java.util.concurrent.*; +import java.util.concurrent.atomic.*; + +import org.junit.Test; +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.core.Observer; +import io.reactivex.rxjava3.core.Scheduler.Worker; +import io.reactivex.rxjava3.disposables.*; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.flowables.ConnectableFlowable; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.internal.operators.completable.CompletableError; +import io.reactivex.rxjava3.internal.operators.flowable.FlowableRange; +import io.reactivex.rxjava3.internal.operators.maybe.MaybeError; +import io.reactivex.rxjava3.internal.operators.observable.ObservableRange; +import io.reactivex.rxjava3.internal.operators.parallel.ParallelFromPublisher; +import io.reactivex.rxjava3.internal.operators.single.SingleJust; +import io.reactivex.rxjava3.internal.schedulers.ImmediateThinScheduler; +import io.reactivex.rxjava3.internal.subscriptions.ScalarSubscription; +import io.reactivex.rxjava3.internal.util.ExceptionHelper; +import io.reactivex.rxjava3.observables.ConnectableObservable; +import io.reactivex.rxjava3.parallel.ParallelFlowable; +import io.reactivex.rxjava3.schedulers.Schedulers; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class RxJavaPluginsTest extends RxJavaTest { + + @Test + public void constructorShouldBePrivate() { + TestHelper.checkUtilityClass(RxJavaPlugins.class); + } + + @SuppressWarnings({ "rawtypes" }) + @Test + public void lockdown() throws Exception { + RxJavaPlugins.reset(); + RxJavaPlugins.lockdown(); + try { + assertTrue(RxJavaPlugins.isLockdown()); + Consumer a1 = Functions.emptyConsumer(); + Supplier f0 = new Supplier() { + @Override + public Object get() { + return null; + } + }; + Function f1 = Functions.identity(); + BiFunction f2 = new BiFunction() { + @Override + public Object apply(Object t1, Object t2) { + return t2; + } + }; + + BooleanSupplier bs = new BooleanSupplier() { + @Override + public boolean getAsBoolean() throws Exception { + return true; + } + }; + + for (Method m : RxJavaPlugins.class.getMethods()) { + if (m.getName().startsWith("set")) { + + Method getter; + + Class<?> paramType = m.getParameterTypes()[0]; + + if (paramType == Boolean.TYPE) { + getter = RxJavaPlugins.class.getMethod("is" + m.getName().substring(3)); + } else { + getter = RxJavaPlugins.class.getMethod("get" + m.getName().substring(3)); + } + + Object before = getter.invoke(null); + + try { + if (paramType.isAssignableFrom(Boolean.TYPE)) { + m.invoke(null, true); + } else + if (paramType.isAssignableFrom(Supplier.class)) { + m.invoke(null, f0); + } else + if (paramType.isAssignableFrom(Function.class)) { + m.invoke(null, f1); + } else + if (paramType.isAssignableFrom(Consumer.class)) { + m.invoke(null, a1); + } else + if (paramType.isAssignableFrom(BooleanSupplier.class)) { + m.invoke(null, bs); + } else { + m.invoke(null, f2); + } + fail("Should have thrown InvocationTargetException(IllegalStateException)"); + } catch (InvocationTargetException ex) { + if (ex.getCause() instanceof IllegalStateException) { + assertEquals("Plugins can't be changed anymore", ex.getCause().getMessage()); + } else { + fail("Should have thrown InvocationTargetException(IllegalStateException)"); + } + } + + Object after = getter.invoke(null); + + if (paramType.isPrimitive()) { + assertEquals(m.toString(), before, after); + } else { + assertSame(m.toString(), before, after); + } + } + } + } finally { + RxJavaPlugins.unlock(); + RxJavaPlugins.reset(); + assertFalse(RxJavaPlugins.isLockdown()); + } + } + + Function<Scheduler, Scheduler> replaceWithImmediate = new Function<Scheduler, Scheduler>() { + @Override + public Scheduler apply(Scheduler t) { + return ImmediateThinScheduler.INSTANCE; + } + }; + + @Test + public void overrideSingleScheduler() { + try { + RxJavaPlugins.setSingleSchedulerHandler(replaceWithImmediate); + + assertSame(ImmediateThinScheduler.INSTANCE, Schedulers.single()); + } finally { + RxJavaPlugins.reset(); + } + // make sure the reset worked + assertNotSame(ImmediateThinScheduler.INSTANCE, Schedulers.single()); + } + + @Test + public void overrideComputationScheduler() { + try { + RxJavaPlugins.setComputationSchedulerHandler(replaceWithImmediate); + + assertSame(ImmediateThinScheduler.INSTANCE, Schedulers.computation()); + } finally { + RxJavaPlugins.reset(); + } + // make sure the reset worked + assertNotSame(ImmediateThinScheduler.INSTANCE, Schedulers.computation()); + } + + @Test + public void overrideIoScheduler() { + try { + RxJavaPlugins.setIoSchedulerHandler(replaceWithImmediate); + + assertSame(ImmediateThinScheduler.INSTANCE, Schedulers.io()); + } finally { + RxJavaPlugins.reset(); + } + // make sure the reset worked + assertNotSame(ImmediateThinScheduler.INSTANCE, Schedulers.io()); + } + + @Test + public void overrideNewThreadScheduler() { + try { + RxJavaPlugins.setNewThreadSchedulerHandler(replaceWithImmediate); + + assertSame(ImmediateThinScheduler.INSTANCE, Schedulers.newThread()); + } finally { + RxJavaPlugins.reset(); + } + // make sure the reset worked + assertNotSame(ImmediateThinScheduler.INSTANCE, Schedulers.newThread()); + } + + Function<Supplier<Scheduler>, Scheduler> initReplaceWithImmediate = new Function<Supplier<Scheduler>, Scheduler>() { + @Override + public Scheduler apply(Supplier<Scheduler> t) { + return ImmediateThinScheduler.INSTANCE; + } + }; + + @Test + public void overrideInitSingleScheduler() { + final Scheduler s = Schedulers.single(); // make sure the Schedulers is initialized + Supplier<Scheduler> c = new Supplier<Scheduler>() { + @Override + public Scheduler get() throws Exception { + return s; + } + }; + try { + RxJavaPlugins.setInitSingleSchedulerHandler(initReplaceWithImmediate); + + assertSame(ImmediateThinScheduler.INSTANCE, RxJavaPlugins.initSingleScheduler(c)); + } finally { + RxJavaPlugins.reset(); + } + // make sure the reset worked + assertSame(s, RxJavaPlugins.initSingleScheduler(c)); + } + + @Test + public void overrideInitComputationScheduler() { + final Scheduler s = Schedulers.computation(); // make sure the Schedulers is initialized + Supplier<Scheduler> c = new Supplier<Scheduler>() { + @Override + public Scheduler get() throws Exception { + return s; + } + }; + try { + RxJavaPlugins.setInitComputationSchedulerHandler(initReplaceWithImmediate); + + assertSame(ImmediateThinScheduler.INSTANCE, RxJavaPlugins.initComputationScheduler(c)); + } finally { + RxJavaPlugins.reset(); + } + // make sure the reset worked + assertSame(s, RxJavaPlugins.initComputationScheduler(c)); + } + + @Test + public void overrideInitIoScheduler() { + final Scheduler s = Schedulers.io(); // make sure the Schedulers is initialized; + Supplier<Scheduler> c = new Supplier<Scheduler>() { + @Override + public Scheduler get() throws Exception { + return s; + } + }; + try { + RxJavaPlugins.setInitIoSchedulerHandler(initReplaceWithImmediate); + + assertSame(ImmediateThinScheduler.INSTANCE, RxJavaPlugins.initIoScheduler(c)); + } finally { + RxJavaPlugins.reset(); + } + // make sure the reset worked + assertSame(s, RxJavaPlugins.initIoScheduler(c)); + } + + @Test + public void overrideInitNewThreadScheduler() { + final Scheduler s = Schedulers.newThread(); // make sure the Schedulers is initialized; + Supplier<Scheduler> c = new Supplier<Scheduler>() { + @Override + public Scheduler get() throws Exception { + return s; + } + }; + try { + RxJavaPlugins.setInitNewThreadSchedulerHandler(initReplaceWithImmediate); + + assertSame(ImmediateThinScheduler.INSTANCE, RxJavaPlugins.initNewThreadScheduler(c)); + } finally { + RxJavaPlugins.reset(); + } + // make sure the reset worked + assertSame(s, RxJavaPlugins.initNewThreadScheduler(c)); + } + + Supplier<Scheduler> nullResultSupplier = new Supplier<Scheduler>() { + @Override + public Scheduler get() throws Exception { + return null; + } + }; + + @Test + public void overrideInitSingleSchedulerCrashes() { + // fail when Supplier is null + try { + RxJavaPlugins.initSingleScheduler(null); + fail("Should have thrown NullPointerException"); + } catch (NullPointerException npe) { + assertEquals("Scheduler Supplier can't be null", npe.getMessage()); + } + + // fail when Supplier result is null + try { + RxJavaPlugins.initSingleScheduler(nullResultSupplier); + fail("Should have thrown NullPointerException"); + } catch (NullPointerException npe) { + assertEquals("Scheduler Supplier result can't be null", npe.getMessage()); + } + } + + @Test + public void overrideInitComputationSchedulerCrashes() { + // fail when Supplier is null + try { + RxJavaPlugins.initComputationScheduler(null); + fail("Should have thrown NullPointerException"); + } catch (NullPointerException npe) { + assertEquals("Scheduler Supplier can't be null", npe.getMessage()); + } + + // fail when Supplier result is null + try { + RxJavaPlugins.initComputationScheduler(nullResultSupplier); + fail("Should have thrown NullPointerException"); + } catch (NullPointerException npe) { + assertEquals("Scheduler Supplier result can't be null", npe.getMessage()); + } + } + + @Test + public void overrideInitIoSchedulerCrashes() { + // fail when Supplier is null + try { + RxJavaPlugins.initIoScheduler(null); + fail("Should have thrown NullPointerException"); + } catch (NullPointerException npe) { + assertEquals("Scheduler Supplier can't be null", npe.getMessage()); + } + + // fail when Supplier result is null + try { + RxJavaPlugins.initIoScheduler(nullResultSupplier); + fail("Should have thrown NullPointerException"); + } catch (NullPointerException npe) { + assertEquals("Scheduler Supplier result can't be null", npe.getMessage()); + } + } + + @Test + public void overrideInitNewThreadSchedulerCrashes() { + // fail when Supplier is null + try { + RxJavaPlugins.initNewThreadScheduler(null); + fail("Should have thrown NullPointerException"); + } catch (NullPointerException npe) { + // expected + assertEquals("Scheduler Supplier can't be null", npe.getMessage()); + } + + // fail when Supplier result is null + try { + RxJavaPlugins.initNewThreadScheduler(nullResultSupplier); + fail("Should have thrown NullPointerException"); + } catch (NullPointerException npe) { + assertEquals("Scheduler Supplier result can't be null", npe.getMessage()); + } + } + + Supplier<Scheduler> unsafeDefault = new Supplier<Scheduler>() { + @Override + public Scheduler get() throws Exception { + throw new AssertionError("Default Scheduler instance should not have been evaluated"); + } + }; + + @Test + public void defaultSingleSchedulerIsInitializedLazily() { + // unsafe default Scheduler Supplier should not be evaluated + try { + RxJavaPlugins.setInitSingleSchedulerHandler(initReplaceWithImmediate); + RxJavaPlugins.initSingleScheduler(unsafeDefault); + } finally { + RxJavaPlugins.reset(); + } + + // make sure the reset worked + assertNotSame(ImmediateThinScheduler.INSTANCE, Schedulers.single()); + } + + @Test + public void defaultIoSchedulerIsInitializedLazily() { + // unsafe default Scheduler Supplier should not be evaluated + try { + RxJavaPlugins.setInitIoSchedulerHandler(initReplaceWithImmediate); + RxJavaPlugins.initIoScheduler(unsafeDefault); + } finally { + RxJavaPlugins.reset(); + } + + // make sure the reset worked + assertNotSame(ImmediateThinScheduler.INSTANCE, Schedulers.io()); + } + + @Test + public void defaultComputationSchedulerIsInitializedLazily() { + // unsafe default Scheduler Supplier should not be evaluated + try { + RxJavaPlugins.setInitComputationSchedulerHandler(initReplaceWithImmediate); + RxJavaPlugins.initComputationScheduler(unsafeDefault); + } finally { + RxJavaPlugins.reset(); + } + + // make sure the reset worked + assertNotSame(ImmediateThinScheduler.INSTANCE, Schedulers.computation()); + } + + @Test + public void defaultNewThreadSchedulerIsInitializedLazily() { + // unsafe default Scheduler Supplier should not be evaluated + try { + RxJavaPlugins.setInitNewThreadSchedulerHandler(initReplaceWithImmediate); + RxJavaPlugins.initNewThreadScheduler(unsafeDefault); + } finally { + RxJavaPlugins.reset(); + } + + // make sure the reset worked + assertNotSame(ImmediateThinScheduler.INSTANCE, Schedulers.newThread()); + } + + @SuppressWarnings("rawtypes") + @Test + public void observableCreate() { + try { + RxJavaPlugins.setOnObservableAssembly(new Function<Observable, Observable>() { + @Override + public Observable apply(Observable t) { + return new ObservableRange(1, 2); + } + }); + + Observable.range(10, 3) + .test() + .assertValues(1, 2) + .assertNoErrors() + .assertComplete(); + } finally { + RxJavaPlugins.reset(); + } + // make sure the reset worked + Observable.range(10, 3) + .test() + .assertValues(10, 11, 12) + .assertNoErrors() + .assertComplete(); + } + + @SuppressWarnings("rawtypes") + @Test + public void flowableCreate() { + try { + RxJavaPlugins.setOnFlowableAssembly(new Function<Flowable, Flowable>() { + @Override + public Flowable apply(Flowable t) { + return new FlowableRange(1, 2); + } + }); + + Flowable.range(10, 3) + .test() + .assertValues(1, 2) + .assertNoErrors() + .assertComplete(); + } finally { + RxJavaPlugins.reset(); + } + // make sure the reset worked + Flowable.range(10, 3) + .test() + .assertValues(10, 11, 12) + .assertNoErrors() + .assertComplete(); + } + + @SuppressWarnings("rawtypes") + @Test + public void observableStart() { + try { + RxJavaPlugins.setOnObservableSubscribe(new BiFunction<Observable, Observer, Observer>() { + @Override + public Observer apply(Observable o, final Observer t) { + return new Observer() { + + @Override + public void onSubscribe(Disposable d) { + t.onSubscribe(d); + } + + @SuppressWarnings("unchecked") + @Override + public void onNext(Object value) { + t.onNext((Integer)value - 9); + } + + @Override + public void onError(Throwable e) { + t.onError(e); + } + + @Override + public void onComplete() { + t.onComplete(); + } + + }; + } + }); + + Observable.range(10, 3) + .test() + .assertValues(1, 2, 3) + .assertNoErrors() + .assertComplete(); + } finally { + RxJavaPlugins.reset(); + } + // make sure the reset worked + Observable.range(10, 3) + .test() + .assertValues(10, 11, 12) + .assertNoErrors() + .assertComplete(); + } + + @SuppressWarnings("rawtypes") + @Test + public void flowableStart() { + try { + RxJavaPlugins.setOnFlowableSubscribe(new BiFunction<Flowable, Subscriber, Subscriber>() { + @Override + public Subscriber apply(Flowable f, final Subscriber t) { + return new Subscriber() { + + @Override + public void onSubscribe(Subscription s) { + t.onSubscribe(s); + } + + @SuppressWarnings("unchecked") + @Override + public void onNext(Object value) { + t.onNext((Integer)value - 9); + } + + @Override + public void onError(Throwable e) { + t.onError(e); + } + + @Override + public void onComplete() { + t.onComplete(); + } + + }; + } + }); + + Flowable.range(10, 3) + .test() + .assertValues(1, 2, 3) + .assertNoErrors() + .assertComplete(); + } finally { + RxJavaPlugins.reset(); + } + // make sure the reset worked + Flowable.range(10, 3) + .test() + .assertValues(10, 11, 12) + .assertNoErrors() + .assertComplete(); + } + + @SuppressWarnings("rawtypes") + @Test + public void parallelFlowableStart() { + try { + RxJavaPlugins.setOnParallelSubscribe(new BiFunction<ParallelFlowable, Subscriber[], Subscriber[]>() { + @Override + public Subscriber[] apply(ParallelFlowable f, final Subscriber[] t) { + return new Subscriber[] { new Subscriber() { + + @Override + public void onSubscribe(Subscription s) { + t[0].onSubscribe(s); + } + + @SuppressWarnings("unchecked") + @Override + public void onNext(Object value) { + t[0].onNext((Integer)value - 9); + } + + @Override + public void onError(Throwable e) { + t[0].onError(e); + } + + @Override + public void onComplete() { + t[0].onComplete(); + } + + } + }; + } + }); + + Flowable.range(10, 3) + .parallel(1) + .sequential() + .test() + .assertValues(1, 2, 3) + .assertNoErrors() + .assertComplete(); + } finally { + RxJavaPlugins.reset(); + } + // make sure the reset worked + Flowable.range(10, 3) + .parallel(1) + .sequential() + .test() + .assertValues(10, 11, 12) + .assertNoErrors() + .assertComplete(); + } + + @SuppressWarnings("rawtypes") + @Test + public void singleCreate() { + try { + RxJavaPlugins.setOnSingleAssembly(new Function<Single, Single>() { + @Override + public Single apply(Single t) { + return new SingleJust<>(10); + } + }); + + Single.just(1) + .test() + .assertValue(10) + .assertNoErrors() + .assertComplete(); + } finally { + RxJavaPlugins.reset(); + } + // make sure the reset worked + Single.just(1) + .test() + .assertValue(1) + .assertNoErrors() + .assertComplete(); + } + + @SuppressWarnings("rawtypes") + @Test + public void singleStart() { + try { + RxJavaPlugins.setOnSingleSubscribe(new BiFunction<Single, SingleObserver, SingleObserver>() { + @Override + public SingleObserver apply(Single o, final SingleObserver t) { + return new SingleObserver<Object>() { + + @Override + public void onSubscribe(Disposable d) { + t.onSubscribe(d); + } + + @SuppressWarnings("unchecked") + @Override + public void onSuccess(Object value) { + t.onSuccess(10); + } + + @Override + public void onError(Throwable e) { + t.onError(e); + } + + }; + } + }); + + Single.just(1) + .test() + .assertValue(10) + .assertNoErrors() + .assertComplete(); + } finally { + RxJavaPlugins.reset(); + } + // make sure the reset worked + Single.just(1) + .test() + .assertValue(1) + .assertNoErrors() + .assertComplete(); + } + + @Test + public void completableCreate() { + try { + RxJavaPlugins.setOnCompletableAssembly(new Function<Completable, Completable>() { + @Override + public Completable apply(Completable t) { + return new CompletableError(new TestException()); + } + }); + + Completable.complete() + .test() + .assertNoValues() + .assertNotComplete() + .assertError(TestException.class); + } finally { + RxJavaPlugins.reset(); + } + // make sure the reset worked + Completable.complete() + .test() + .assertNoValues() + .assertNoErrors() + .assertComplete(); + } + + @Test + public void completableStart() { + try { + RxJavaPlugins.setOnCompletableSubscribe(new BiFunction<Completable, CompletableObserver, CompletableObserver>() { + @Override + public CompletableObserver apply(Completable o, final CompletableObserver t) { + return new CompletableObserver() { + @Override + public void onSubscribe(Disposable d) { + t.onSubscribe(d); + } + + @Override + public void onError(Throwable e) { + t.onError(e); + } + + @Override + public void onComplete() { + t.onError(new TestException()); + } + }; + } + }); + + Completable.complete() + .test() + .assertNoValues() + .assertNotComplete() + .assertError(TestException.class); + } finally { + RxJavaPlugins.reset(); + } + // make sure the reset worked + + Completable.complete() + .test() + .assertNoValues() + .assertNoErrors() + .assertComplete(); + } + + void onSchedule(Worker w) throws InterruptedException { + try { + try { + final AtomicInteger value = new AtomicInteger(); + final CountDownLatch cdl = new CountDownLatch(1); + + RxJavaPlugins.setScheduleHandler(new Function<Runnable, Runnable>() { + @Override + public Runnable apply(Runnable t) { + return new Runnable() { + @Override + public void run() { + value.set(10); + cdl.countDown(); + } + }; + } + }); + + w.schedule(new Runnable() { + @Override + public void run() { + value.set(1); + cdl.countDown(); + } + }); + + cdl.await(); + + assertEquals(10, value.get()); + + } finally { + + RxJavaPlugins.reset(); + } + + // make sure the reset worked + final AtomicInteger value = new AtomicInteger(); + final CountDownLatch cdl = new CountDownLatch(1); + + w.schedule(new Runnable() { + @Override + public void run() { + value.set(1); + cdl.countDown(); + } + }); + + cdl.await(); + + assertEquals(1, value.get()); + } finally { + w.dispose(); + } + } + + @Test + public void onScheduleComputation() throws InterruptedException { + onSchedule(Schedulers.computation().createWorker()); + } + + @Test + public void onScheduleIO() throws InterruptedException { + onSchedule(Schedulers.io().createWorker()); + } + + @Test + public void onScheduleNewThread() throws InterruptedException { + onSchedule(Schedulers.newThread().createWorker()); + } + + @Test + public void onError() { + try { + final List<Throwable> list = new ArrayList<>(); + + RxJavaPlugins.setErrorHandler(new Consumer<Throwable>() { + @Override + public void accept(Throwable t) { + list.add(t); + } + }); + + RxJavaPlugins.onError(new TestException("Forced failure")); + + assertEquals(1, list.size()); + assertUndeliverableTestException(list, 0, "Forced failure"); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void onErrorNoHandler() { + try { + final List<Throwable> list = new ArrayList<>(); + + RxJavaPlugins.setErrorHandler(null); + + Thread.currentThread().setUncaughtExceptionHandler(new UncaughtExceptionHandler() { + + @Override + public void uncaughtException(Thread t, Throwable e) { + list.add(e); + + } + }); + + RxJavaPlugins.onError(new TestException("Forced failure")); + + Thread.currentThread().setUncaughtExceptionHandler(null); + + // this will be printed on the console and should not crash + RxJavaPlugins.onError(new TestException("Forced failure 3")); + + assertEquals(1, list.size()); + assertUndeliverableTestException(list, 0, "Forced failure"); + } finally { + RxJavaPlugins.reset(); + Thread.currentThread().setUncaughtExceptionHandler(null); + } + } + + @Test + public void onErrorCrashes() { + try { + final List<Throwable> list = new ArrayList<>(); + + RxJavaPlugins.setErrorHandler(new Consumer<Throwable>() { + @Override + public void accept(Throwable t) { + throw new TestException("Forced failure 2"); + } + }); + + Thread.currentThread().setUncaughtExceptionHandler(new UncaughtExceptionHandler() { + + @Override + public void uncaughtException(Thread t, Throwable e) { + list.add(e); + + } + }); + + RxJavaPlugins.onError(new TestException("Forced failure")); + + assertEquals(2, list.size()); + assertTestException(list, 0, "Forced failure 2"); + assertUndeliverableTestException(list, 1, "Forced failure"); + + Thread.currentThread().setUncaughtExceptionHandler(null); + + } finally { + RxJavaPlugins.reset(); + Thread.currentThread().setUncaughtExceptionHandler(null); + } + } + + @Test + public void onErrorWithNull() { + try { + final List<Throwable> list = new ArrayList<>(); + + RxJavaPlugins.setErrorHandler(new Consumer<Throwable>() { + @Override + public void accept(Throwable t) { + throw new TestException("Forced failure 2"); + } + }); + + Thread.currentThread().setUncaughtExceptionHandler(new UncaughtExceptionHandler() { + + @Override + public void uncaughtException(Thread t, Throwable e) { + list.add(e); + + } + }); + + RxJavaPlugins.onError(null); + + assertEquals(2, list.size()); + assertTestException(list, 0, "Forced failure 2"); + assertNPE(list, 1); + + RxJavaPlugins.reset(); + + RxJavaPlugins.onError(null); + + assertNPE(list, 2); + + } finally { + RxJavaPlugins.reset(); + + Thread.currentThread().setUncaughtExceptionHandler(null); + } + } + + /** + * Ensure set*() accepts a consumers/functions with wider bounds. + * @throws Exception on error + */ + @Test + @SuppressWarnings("rawtypes") + public void onErrorWithSuper() throws Exception { + try { + Consumer<? super Throwable> errorHandler = new Consumer<Throwable>() { + @Override + public void accept(Throwable t) { + throw new TestException("Forced failure 2"); + } + }; + RxJavaPlugins.setErrorHandler(errorHandler); + + Consumer<? super Throwable> errorHandler1 = RxJavaPlugins.getErrorHandler(); + assertSame(errorHandler, errorHandler1); + + Function<? super Scheduler, ? extends Scheduler> scheduler2scheduler = new Function<Scheduler, Scheduler>() { + @Override + public Scheduler apply(Scheduler scheduler) throws Exception { + return scheduler; + } + }; + Function<? super Supplier<Scheduler>, ? extends Scheduler> callable2scheduler = new Function<Supplier<Scheduler>, Scheduler>() { + @Override + public Scheduler apply(Supplier<Scheduler> schedulerSupplier) throws Throwable { + return schedulerSupplier.get(); + } + }; + Function<? super ConnectableFlowable, ? extends ConnectableFlowable> connectableFlowable2ConnectableFlowable = new Function<ConnectableFlowable, ConnectableFlowable>() { + @Override + public ConnectableFlowable apply(ConnectableFlowable connectableFlowable) throws Exception { + return connectableFlowable; + } + }; + Function<? super ConnectableObservable, ? extends ConnectableObservable> connectableObservable2ConnectableObservable = new Function<ConnectableObservable, ConnectableObservable>() { + @Override + public ConnectableObservable apply(ConnectableObservable connectableObservable) throws Exception { + return connectableObservable; + } + }; + Function<? super Flowable, ? extends Flowable> flowable2Flowable = new Function<Flowable, Flowable>() { + @Override + public Flowable apply(Flowable flowable) throws Exception { + return flowable; + } + }; + BiFunction<? super Flowable, ? super Subscriber, ? extends Subscriber> flowable2subscriber = new BiFunction<Flowable, Subscriber, Subscriber>() { + @Override + public Subscriber apply(Flowable flowable, Subscriber subscriber) throws Exception { + return subscriber; + } + }; + Function<Maybe, Maybe> maybe2maybe = new Function<Maybe, Maybe>() { + @Override + public Maybe apply(Maybe maybe) throws Exception { + return maybe; + } + }; + BiFunction<Maybe, MaybeObserver, MaybeObserver> maybe2observer = new BiFunction<Maybe, MaybeObserver, MaybeObserver>() { + @Override + public MaybeObserver apply(Maybe maybe, MaybeObserver maybeObserver) throws Exception { + return maybeObserver; + } + }; + Function<Observable, Observable> observable2observable = new Function<Observable, Observable>() { + @Override + public Observable apply(Observable observable) throws Exception { + return observable; + } + }; + BiFunction<? super Observable, ? super Observer, ? extends Observer> observable2observer = new BiFunction<Observable, Observer, Observer>() { + @Override + public Observer apply(Observable observable, Observer observer) throws Exception { + return observer; + } + }; + Function<? super ParallelFlowable, ? extends ParallelFlowable> parallelFlowable2parallelFlowable = new Function<ParallelFlowable, ParallelFlowable>() { + @Override + public ParallelFlowable apply(ParallelFlowable parallelFlowable) throws Exception { + return parallelFlowable; + } + }; + Function<Single, Single> single2single = new Function<Single, Single>() { + @Override + public Single apply(Single single) throws Exception { + return single; + } + }; + BiFunction<? super Single, ? super SingleObserver, ? extends SingleObserver> single2observer = new BiFunction<Single, SingleObserver, SingleObserver>() { + @Override + public SingleObserver apply(Single single, SingleObserver singleObserver) throws Exception { + return singleObserver; + } + }; + Function<? super Runnable, ? extends Runnable> runnable2runnable = new Function<Runnable, Runnable>() { + @Override + public Runnable apply(Runnable runnable) throws Exception { + return runnable; + } + }; + BiFunction<? super Completable, ? super CompletableObserver, ? extends CompletableObserver> completableObserver2completableObserver = new BiFunction<Completable, CompletableObserver, CompletableObserver>() { + @Override + public CompletableObserver apply(Completable completable, CompletableObserver completableObserver) throws Exception { + return completableObserver; + } + }; + Function<? super Completable, ? extends Completable> completable2completable = new Function<Completable, Completable>() { + @Override + public Completable apply(Completable completable) throws Exception { + return completable; + } + }; + + RxJavaPlugins.setInitComputationSchedulerHandler(callable2scheduler); + RxJavaPlugins.setComputationSchedulerHandler(scheduler2scheduler); + RxJavaPlugins.setIoSchedulerHandler(scheduler2scheduler); + RxJavaPlugins.setNewThreadSchedulerHandler(scheduler2scheduler); + RxJavaPlugins.setOnConnectableFlowableAssembly(connectableFlowable2ConnectableFlowable); + RxJavaPlugins.setOnConnectableObservableAssembly(connectableObservable2ConnectableObservable); + RxJavaPlugins.setOnFlowableAssembly(flowable2Flowable); + RxJavaPlugins.setOnFlowableSubscribe(flowable2subscriber); + RxJavaPlugins.setOnMaybeAssembly(maybe2maybe); + RxJavaPlugins.setOnMaybeSubscribe(maybe2observer); + RxJavaPlugins.setOnObservableAssembly(observable2observable); + RxJavaPlugins.setOnObservableSubscribe(observable2observer); + RxJavaPlugins.setOnParallelAssembly(parallelFlowable2parallelFlowable); + RxJavaPlugins.setOnSingleAssembly(single2single); + RxJavaPlugins.setOnSingleSubscribe(single2observer); + RxJavaPlugins.setScheduleHandler(runnable2runnable); + RxJavaPlugins.setSingleSchedulerHandler(scheduler2scheduler); + RxJavaPlugins.setOnCompletableSubscribe(completableObserver2completableObserver); + RxJavaPlugins.setOnCompletableAssembly(completable2completable); + RxJavaPlugins.setInitSingleSchedulerHandler(callable2scheduler); + RxJavaPlugins.setInitNewThreadSchedulerHandler(callable2scheduler); + RxJavaPlugins.setInitIoSchedulerHandler(callable2scheduler); + } finally { + RxJavaPlugins.reset(); + } + } + + @SuppressWarnings({"rawtypes", "unchecked" }) + @Test + public void clearIsPassthrough() { + try { + RxJavaPlugins.reset(); + + assertNull(RxJavaPlugins.onAssembly((Observable)null)); + + assertNull(RxJavaPlugins.onAssembly((ConnectableObservable)null)); + + assertNull(RxJavaPlugins.onAssembly((Flowable)null)); + + assertNull(RxJavaPlugins.onAssembly((ConnectableFlowable)null)); + + Observable oos = new Observable() { + @Override + public void subscribeActual(Observer t) { + + } + }; + + Flowable fos = new Flowable() { + @Override + public void subscribeActual(Subscriber t) { + + } + }; + + assertSame(oos, RxJavaPlugins.onAssembly(oos)); + + assertSame(fos, RxJavaPlugins.onAssembly(fos)); + + assertNull(RxJavaPlugins.onAssembly((Single)null)); + + Single sos = new Single() { + @Override + public void subscribeActual(SingleObserver t) { + + } + }; + + assertSame(sos, RxJavaPlugins.onAssembly(sos)); + + assertNull(RxJavaPlugins.onAssembly((Completable)null)); + + Completable cos = new Completable() { + @Override + public void subscribeActual(CompletableObserver t) { + + } + }; + + assertSame(cos, RxJavaPlugins.onAssembly(cos)); + + assertNull(RxJavaPlugins.onAssembly((Maybe)null)); + + Maybe myb = new Maybe() { + @Override + public void subscribeActual(MaybeObserver t) { + + } + }; + + assertSame(myb, RxJavaPlugins.onAssembly(myb)); + + Runnable action = Functions.EMPTY_RUNNABLE; + assertSame(action, RxJavaPlugins.onSchedule(action)); + + class AllSubscriber implements Subscriber, Observer, SingleObserver, CompletableObserver, MaybeObserver { + + @Override + public void onSuccess(Object value) { + + } + + @Override + public void onSubscribe(Disposable d) { + + } + + @Override + public void onSubscribe(Subscription s) { + + } + + @Override + public void onNext(Object t) { + + } + + @Override + public void onError(Throwable t) { + + } + + @Override + public void onComplete() { + + } + + } + + AllSubscriber all = new AllSubscriber(); + Subscriber[] allArray = { all }; + + assertNull(RxJavaPlugins.onSubscribe(Observable.never(), null)); + + assertSame(all, RxJavaPlugins.onSubscribe(Observable.never(), all)); + + assertNull(RxJavaPlugins.onSubscribe(Flowable.never(), null)); + + assertSame(all, RxJavaPlugins.onSubscribe(Flowable.never(), all)); + + assertNull(RxJavaPlugins.onSubscribe(Single.just(1), null)); + + assertSame(all, RxJavaPlugins.onSubscribe(Single.just(1), all)); + + assertNull(RxJavaPlugins.onSubscribe(Completable.never(), null)); + + assertSame(all, RxJavaPlugins.onSubscribe(Completable.never(), all)); + + assertNull(RxJavaPlugins.onSubscribe(Maybe.never(), null)); + + assertSame(all, RxJavaPlugins.onSubscribe(Maybe.never(), all)); + + assertNull(RxJavaPlugins.onSubscribe(Flowable.never().parallel(), null)); + + assertSame(allArray, RxJavaPlugins.onSubscribe(Flowable.never().parallel(), allArray)); + + final Scheduler s = ImmediateThinScheduler.INSTANCE; + Supplier<Scheduler> c = new Supplier<Scheduler>() { + @Override + public Scheduler get() throws Exception { + return s; + } + }; + assertSame(s, RxJavaPlugins.onComputationScheduler(s)); + + assertSame(s, RxJavaPlugins.onIoScheduler(s)); + + assertSame(s, RxJavaPlugins.onNewThreadScheduler(s)); + + assertSame(s, RxJavaPlugins.onSingleScheduler(s)); + + assertSame(s, RxJavaPlugins.initComputationScheduler(c)); + + assertSame(s, RxJavaPlugins.initIoScheduler(c)); + + assertSame(s, RxJavaPlugins.initNewThreadScheduler(c)); + + assertSame(s, RxJavaPlugins.initSingleScheduler(c)); + + } finally { + RxJavaPlugins.reset(); + } + } + + static void assertTestException(List<Throwable> list, int index, String message) { + assertTrue(list.get(index).toString(), list.get(index) instanceof TestException); + assertEquals(message, list.get(index).getMessage()); + } + + static void assertUndeliverableTestException(List<Throwable> list, int index, String message) { + assertTrue(list.get(index).toString(), list.get(index).getCause() instanceof TestException); + assertEquals(message, list.get(index).getCause().getMessage()); + } + + static void assertNPE(List<Throwable> list, int index) { + assertTrue(list.get(index).toString(), list.get(index) instanceof NullPointerException); + } + + @SuppressWarnings("rawtypes") + @Test + public void overrideConnectableObservable() { + try { + RxJavaPlugins.setOnConnectableObservableAssembly(new Function<ConnectableObservable, ConnectableObservable>() { + @Override + public ConnectableObservable apply(ConnectableObservable co) throws Exception { + return new ConnectableObservable() { + + @Override + public void connect(Consumer connection) { + + } + + @Override + public void reset() { + // nothing to do in this test + } + + @SuppressWarnings("unchecked") + @Override + protected void subscribeActual(Observer observer) { + observer.onSubscribe(Disposable.empty()); + observer.onNext(10); + observer.onComplete(); + } + }; + } + }); + + Observable + .just(1) + .publish() + .autoConnect() + .test() + .assertResult(10); + + } finally { + RxJavaPlugins.reset(); + } + + Observable + .just(1) + .publish() + .autoConnect() + .test() + .assertResult(1); + } + + @SuppressWarnings("rawtypes") + @Test + public void overrideConnectableFlowable() { + try { + RxJavaPlugins.setOnConnectableFlowableAssembly(new Function<ConnectableFlowable, ConnectableFlowable>() { + @Override + public ConnectableFlowable apply(ConnectableFlowable co) throws Exception { + return new ConnectableFlowable() { + + @Override + public void connect(Consumer connection) { + + } + + @Override + public void reset() { + // nothing to do in this test + } + + @SuppressWarnings("unchecked") + @Override + protected void subscribeActual(Subscriber subscriber) { + subscriber.onSubscribe(new ScalarSubscription(subscriber, 10)); + } + }; + } + }); + + Flowable + .just(1) + .publish() + .autoConnect() + .test() + .assertResult(10); + + } finally { + RxJavaPlugins.reset(); + } + + Flowable + .just(1) + .publish() + .autoConnect() + .test() + .assertResult(1); + } + + @SuppressWarnings("rawtypes") + @Test + public void assemblyHookCrashes() { + try { + RxJavaPlugins.setOnFlowableAssembly(new Function<Flowable, Flowable>() { + @Override + public Flowable apply(Flowable f) throws Exception { + throw new IllegalArgumentException(); + } + }); + + try { + Flowable.empty(); + fail("Should have thrown!"); + } catch (IllegalArgumentException ex) { + // expected + } + + RxJavaPlugins.setOnFlowableAssembly(new Function<Flowable, Flowable>() { + @Override + public Flowable apply(Flowable f) throws Exception { + throw new InternalError(); + } + }); + + try { + Flowable.empty(); + fail("Should have thrown!"); + } catch (InternalError ex) { + // expected + } + + RxJavaPlugins.setOnFlowableAssembly(new Function<Flowable, Flowable>() { + @Override + public Flowable apply(Flowable f) throws Exception { + throw new IOException(); + } + }); + + try { + Flowable.empty(); + fail("Should have thrown!"); + } catch (RuntimeException ex) { + if (!(ex.getCause() instanceof IOException)) { + fail(ex.getCause().toString() + ": Should have thrown RuntimeException(IOException)"); + } + } + } finally { + RxJavaPlugins.reset(); + } + } + + @SuppressWarnings("rawtypes") + @Test + public void subscribeHookCrashes() { + try { + RxJavaPlugins.setOnFlowableSubscribe(new BiFunction<Flowable, Subscriber, Subscriber>() { + @Override + public Subscriber apply(Flowable f, Subscriber s) throws Exception { + throw new IllegalArgumentException(); + } + }); + + try { + Flowable.empty().test(); + fail("Should have thrown!"); + } catch (NullPointerException ex) { + if (!(ex.getCause() instanceof IllegalArgumentException)) { + fail(ex.getCause().toString() + ": Should have thrown NullPointerException(IllegalArgumentException)"); + } + } + + RxJavaPlugins.setOnFlowableSubscribe(new BiFunction<Flowable, Subscriber, Subscriber>() { + @Override + public Subscriber apply(Flowable f, Subscriber s) throws Exception { + throw new InternalError(); + } + }); + + try { + Flowable.empty().test(); + fail("Should have thrown!"); + } catch (InternalError ex) { + // expected + } + + RxJavaPlugins.setOnFlowableSubscribe(new BiFunction<Flowable, Subscriber, Subscriber>() { + @Override + public Subscriber apply(Flowable f, Subscriber s) throws Exception { + throw new IOException(); + } + }); + + try { + Flowable.empty().test(); + fail("Should have thrown!"); + } catch (NullPointerException ex) { + if (!(ex.getCause() instanceof RuntimeException)) { + fail(ex.getCause().toString() + ": Should have thrown NullPointerException(RuntimeException(IOException))"); + } + if (!(ex.getCause().getCause() instanceof IOException)) { + fail(ex.getCause().toString() + ": Should have thrown NullPointerException(RuntimeException(IOException))"); + } + } + } finally { + RxJavaPlugins.reset(); + } + } + + @SuppressWarnings("rawtypes") + @Test + public void maybeCreate() { + try { + RxJavaPlugins.setOnMaybeAssembly(new Function<Maybe, Maybe>() { + @Override + public Maybe apply(Maybe t) { + return new MaybeError(new TestException()); + } + }); + + Maybe.empty() + .test() + .assertNoValues() + .assertNotComplete() + .assertError(TestException.class); + } finally { + RxJavaPlugins.reset(); + } + // make sure the reset worked + Maybe.empty() + .test() + .assertNoValues() + .assertNoErrors() + .assertComplete(); + } + + @Test + @SuppressWarnings("rawtypes") + public void maybeStart() { + try { + RxJavaPlugins.setOnMaybeSubscribe(new BiFunction<Maybe, MaybeObserver, MaybeObserver>() { + @Override + public MaybeObserver apply(Maybe o, final MaybeObserver t) { + return new MaybeObserver() { + @Override + public void onSubscribe(Disposable d) { + t.onSubscribe(d); + } + + @SuppressWarnings("unchecked") + @Override + public void onSuccess(Object value) { + t.onSuccess(value); + } + + @Override + public void onError(Throwable e) { + t.onError(e); + } + + @Override + public void onComplete() { + t.onError(new TestException()); + } + }; + } + }); + + Maybe.empty() + .test() + .assertNoValues() + .assertNotComplete() + .assertError(TestException.class); + } finally { + RxJavaPlugins.reset(); + } + // make sure the reset worked + + Maybe.empty() + .test() + .assertNoValues() + .assertNoErrors() + .assertComplete(); + } + + @Test + public void onErrorNull() { + try { + final AtomicReference<Throwable> t = new AtomicReference<>(); + + RxJavaPlugins.setErrorHandler(new Consumer<Throwable>() { + @Override + public void accept(final Throwable throwable) throws Exception { + t.set(throwable); + } + }); + + RxJavaPlugins.onError(null); + + final Throwable throwable = t.get(); + assertEquals(ExceptionHelper.nullWarning("onError called with a null Throwable."), throwable.getMessage()); + assertTrue(throwable instanceof NullPointerException); + } finally { + RxJavaPlugins.reset(); + } + } + + private static void verifyThread(Scheduler scheduler, String expectedThreadName) + throws AssertionError { + assertNotNull(scheduler); + Worker w = scheduler.createWorker(); + try { + final AtomicReference<Thread> value = new AtomicReference<>(); + final CountDownLatch cdl = new CountDownLatch(1); + + w.schedule(new Runnable() { + @Override + public void run() { + value.set(Thread.currentThread()); + cdl.countDown(); + } + }); + + cdl.await(); + + Thread t = value.get(); + assertNotNull(t); + assertEquals(expectedThreadName, t.getName()); + } catch (Exception e) { + fail(); + } finally { + w.dispose(); + } + } + + @Test + public void createComputationScheduler() { + final String name = "ComputationSchedulerTest"; + ThreadFactory factory = new ThreadFactory() { + @Override + public Thread newThread(Runnable r) { + return new Thread(r, name); + } + }; + + final Scheduler customScheduler = RxJavaPlugins.createComputationScheduler(factory); + RxJavaPlugins.setComputationSchedulerHandler(new Function<Scheduler, Scheduler>() { + @Override + public Scheduler apply(Scheduler scheduler) throws Exception { + return customScheduler; + } + }); + + try { + verifyThread(Schedulers.computation(), name); + } finally { + customScheduler.shutdown(); + RxJavaPlugins.reset(); + } + } + + @Test + public void createIoScheduler() { + final String name = "IoSchedulerTest"; + ThreadFactory factory = new ThreadFactory() { + @Override + public Thread newThread(Runnable r) { + return new Thread(r, name); + } + }; + + final Scheduler customScheduler = RxJavaPlugins.createIoScheduler(factory); + RxJavaPlugins.setIoSchedulerHandler(new Function<Scheduler, Scheduler>() { + @Override + public Scheduler apply(Scheduler scheduler) throws Exception { + return customScheduler; + } + }); + + try { + verifyThread(Schedulers.io(), name); + } finally { + customScheduler.shutdown(); + RxJavaPlugins.reset(); + } + } + + @Test + public void createNewThreadScheduler() { + final String name = "NewThreadSchedulerTest"; + ThreadFactory factory = new ThreadFactory() { + @Override + public Thread newThread(Runnable r) { + return new Thread(r, name); + } + }; + + final Scheduler customScheduler = RxJavaPlugins.createNewThreadScheduler(factory); + RxJavaPlugins.setNewThreadSchedulerHandler(new Function<Scheduler, Scheduler>() { + @Override + public Scheduler apply(Scheduler scheduler) throws Exception { + return customScheduler; + } + }); + + try { + verifyThread(Schedulers.newThread(), name); + } finally { + customScheduler.shutdown(); + RxJavaPlugins.reset(); + } + } + + @Test + public void createSingleScheduler() { + final String name = "SingleSchedulerTest"; + ThreadFactory factory = new ThreadFactory() { + @Override + public Thread newThread(Runnable r) { + return new Thread(r, name); + } + }; + + final Scheduler customScheduler = RxJavaPlugins.createSingleScheduler(factory); + + RxJavaPlugins.setSingleSchedulerHandler(new Function<Scheduler, Scheduler>() { + @Override + public Scheduler apply(Scheduler scheduler) throws Exception { + return customScheduler; + } + }); + + try { + verifyThread(Schedulers.single(), name); + } finally { + customScheduler.shutdown(); + RxJavaPlugins.reset(); + } + } + + @Test + public void onBeforeBlocking() { + try { + RxJavaPlugins.setOnBeforeBlocking(new BooleanSupplier() { + @Override + public boolean getAsBoolean() throws Exception { + throw new IllegalArgumentException(); + } + }); + + try { + RxJavaPlugins.onBeforeBlocking(); + fail("Should have thrown"); + } catch (IllegalArgumentException ex) { + // expected + } + } finally { + RxJavaPlugins.reset(); + } + } + + @SuppressWarnings("rawtypes") + @Test + public void onParallelAssembly() { + try { + RxJavaPlugins.setOnParallelAssembly(new Function<ParallelFlowable, ParallelFlowable>() { + @Override + public ParallelFlowable apply(ParallelFlowable pf) throws Exception { + return new ParallelFromPublisher<>(Flowable.just(2), 2, 2); + } + }); + + Flowable.just(1) + .parallel() + .sequential() + .test() + .assertResult(2); + } finally { + RxJavaPlugins.reset(); + } + + Flowable.just(1) + .parallel() + .sequential() + .test() + .assertResult(1); + } + + @Test + public void isBug() { + assertFalse(RxJavaPlugins.isBug(new RuntimeException())); + assertFalse(RxJavaPlugins.isBug(new IOException())); + assertFalse(RxJavaPlugins.isBug(new InterruptedException())); + assertFalse(RxJavaPlugins.isBug(new InterruptedIOException())); + + assertTrue(RxJavaPlugins.isBug(new NullPointerException())); + assertTrue(RxJavaPlugins.isBug(new IllegalArgumentException())); + assertTrue(RxJavaPlugins.isBug(new IllegalStateException())); + assertTrue(RxJavaPlugins.isBug(new MissingBackpressureException())); + assertTrue(RxJavaPlugins.isBug(new ProtocolViolationException(""))); + assertTrue(RxJavaPlugins.isBug(new UndeliverableException(new TestException()))); + assertTrue(RxJavaPlugins.isBug(new CompositeException(new TestException()))); + assertTrue(RxJavaPlugins.isBug(new OnErrorNotImplementedException(new TestException()))); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/processors/AsyncProcessorTest.java b/src/test/java/io/reactivex/rxjava3/processors/AsyncProcessorTest.java new file mode 100644 index 0000000000..81afc972dc --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/processors/AsyncProcessorTest.java @@ -0,0 +1,534 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.processors; + +import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +import org.junit.Test; +import org.mockito.*; +import org.reactivestreams.Subscriber; + +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.Consumer; +import io.reactivex.rxjava3.internal.subscriptions.BooleanSubscription; +import io.reactivex.rxjava3.operators.QueueFuseable; +import io.reactivex.rxjava3.subscribers.TestSubscriber; +import io.reactivex.rxjava3.testsupport.*; + +public class AsyncProcessorTest extends FlowableProcessorTest<Object> { + + private final Throwable testException = new Throwable(); + + @Override + protected FlowableProcessor<Object> create() { + return AsyncProcessor.create(); + } + + @Test + public void neverCompleted() { + AsyncProcessor<String> processor = AsyncProcessor.create(); + + Subscriber<String> subscriber = TestHelper.mockSubscriber(); + processor.subscribe(subscriber); + + processor.onNext("one"); + processor.onNext("two"); + processor.onNext("three"); + + verify(subscriber, Mockito.never()).onNext(anyString()); + verify(subscriber, Mockito.never()).onError(testException); + verify(subscriber, Mockito.never()).onComplete(); + } + + @Test + public void completed() { + AsyncProcessor<String> processor = AsyncProcessor.create(); + + Subscriber<String> subscriber = TestHelper.mockSubscriber(); + processor.subscribe(subscriber); + + processor.onNext("one"); + processor.onNext("two"); + processor.onNext("three"); + processor.onComplete(); + + verify(subscriber, times(1)).onNext("three"); + verify(subscriber, Mockito.never()).onError(any(Throwable.class)); + verify(subscriber, times(1)).onComplete(); + } + + @Test + public void subscribeAfterCompleted() { + AsyncProcessor<String> processor = AsyncProcessor.create(); + + Subscriber<String> subscriber = TestHelper.mockSubscriber(); + + processor.onNext("one"); + processor.onNext("two"); + processor.onNext("three"); + processor.onComplete(); + + processor.subscribe(subscriber); + + verify(subscriber, times(1)).onNext("three"); + verify(subscriber, Mockito.never()).onError(any(Throwable.class)); + verify(subscriber, times(1)).onComplete(); + } + + @Test + public void subscribeAfterError() { + AsyncProcessor<String> processor = AsyncProcessor.create(); + + Subscriber<String> subscriber = TestHelper.mockSubscriber(); + + processor.onNext("one"); + processor.onNext("two"); + processor.onNext("three"); + + RuntimeException re = new RuntimeException("failed"); + processor.onError(re); + + processor.subscribe(subscriber); + + verify(subscriber, times(1)).onError(re); + verify(subscriber, Mockito.never()).onNext(any(String.class)); + verify(subscriber, Mockito.never()).onComplete(); + } + + @Test + @SuppressUndeliverable + public void error() { + AsyncProcessor<String> processor = AsyncProcessor.create(); + + Subscriber<String> subscriber = TestHelper.mockSubscriber(); + processor.subscribe(subscriber); + + processor.onNext("one"); + processor.onNext("two"); + processor.onNext("three"); + processor.onError(testException); + processor.onNext("four"); + processor.onError(new Throwable()); + processor.onComplete(); + + verify(subscriber, Mockito.never()).onNext(anyString()); + verify(subscriber, times(1)).onError(testException); + verify(subscriber, Mockito.never()).onComplete(); + } + + @Test + public void unsubscribeBeforeCompleted() { + AsyncProcessor<String> processor = AsyncProcessor.create(); + + Subscriber<String> subscriber = TestHelper.mockSubscriber(); + TestSubscriber<String> ts = new TestSubscriber<>(subscriber); + processor.subscribe(ts); + + processor.onNext("one"); + processor.onNext("two"); + + ts.cancel(); + + verify(subscriber, Mockito.never()).onNext(anyString()); + verify(subscriber, Mockito.never()).onError(any(Throwable.class)); + verify(subscriber, Mockito.never()).onComplete(); + + processor.onNext("three"); + processor.onComplete(); + + verify(subscriber, Mockito.never()).onNext(anyString()); + verify(subscriber, Mockito.never()).onError(any(Throwable.class)); + verify(subscriber, Mockito.never()).onComplete(); + } + + @Test + public void emptySubjectCompleted() { + AsyncProcessor<String> processor = AsyncProcessor.create(); + + Subscriber<String> subscriber = TestHelper.mockSubscriber(); + processor.subscribe(subscriber); + + processor.onComplete(); + + InOrder inOrder = inOrder(subscriber); + inOrder.verify(subscriber, never()).onNext(null); + inOrder.verify(subscriber, never()).onNext(any(String.class)); + inOrder.verify(subscriber, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + } + + /** + * Can receive timeout if subscribe never receives an onError/onComplete ... which reveals a race condition. + */ + @Test + public void subscribeCompletionRaceCondition() { + /* + * With non-threadsafe code this fails most of the time on my dev laptop and is non-deterministic enough + * to act as a unit test to the race conditions. + * + * With the synchronization code in place I can not get this to fail on my laptop. + */ + for (int i = 0; i < 50; i++) { + final AsyncProcessor<String> processor = AsyncProcessor.create(); + final AtomicReference<String> value1 = new AtomicReference<>(); + + processor.subscribe(new Consumer<String>() { + + @Override + public void accept(String t1) { + try { + // simulate a slow observer + Thread.sleep(50); + } catch (InterruptedException e) { + e.printStackTrace(); + } + value1.set(t1); + } + + }); + + Thread t1 = new Thread(new Runnable() { + + @Override + public void run() { + processor.onNext("value"); + processor.onComplete(); + } + }); + + SubjectSubscriberThread t2 = new SubjectSubscriberThread(processor); + SubjectSubscriberThread t3 = new SubjectSubscriberThread(processor); + SubjectSubscriberThread t4 = new SubjectSubscriberThread(processor); + SubjectSubscriberThread t5 = new SubjectSubscriberThread(processor); + + t2.start(); + t3.start(); + t1.start(); + t4.start(); + t5.start(); + try { + t1.join(); + t2.join(); + t3.join(); + t4.join(); + t5.join(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + + assertEquals("value", value1.get()); + assertEquals("value", t2.value.get()); + assertEquals("value", t3.value.get()); + assertEquals("value", t4.value.get()); + assertEquals("value", t5.value.get()); + } + + } + + private static class SubjectSubscriberThread extends Thread { + + private final AsyncProcessor<String> processor; + private final AtomicReference<String> value = new AtomicReference<>(); + + SubjectSubscriberThread(AsyncProcessor<String> processor) { + this.processor = processor; + } + + @Override + public void run() { + try { + // a timeout exception will happen if we don't get a terminal state + String v = processor.timeout(2000, TimeUnit.MILLISECONDS).blockingSingle(); + value.set(v); + } catch (Exception e) { + e.printStackTrace(); + } + } + } + + @Test + public void currentStateMethodsNormal() { + AsyncProcessor<Object> as = AsyncProcessor.create(); + + assertFalse(as.hasValue()); + assertFalse(as.hasThrowable()); + assertFalse(as.hasComplete()); + assertNull(as.getValue()); + assertNull(as.getThrowable()); + + as.onNext(1); + + assertFalse(as.hasValue()); // AP no longer reports it has a value until it is terminated + assertFalse(as.hasThrowable()); + assertFalse(as.hasComplete()); + assertNull(as.getValue()); // AP no longer reports it has a value until it is terminated + assertNull(as.getThrowable()); + + as.onComplete(); + assertTrue(as.hasValue()); + assertFalse(as.hasThrowable()); + assertTrue(as.hasComplete()); + assertEquals(1, as.getValue()); + assertNull(as.getThrowable()); + } + + @Test + public void currentStateMethodsEmpty() { + AsyncProcessor<Object> as = AsyncProcessor.create(); + + assertFalse(as.hasValue()); + assertFalse(as.hasThrowable()); + assertFalse(as.hasComplete()); + assertNull(as.getValue()); + assertNull(as.getThrowable()); + + as.onComplete(); + + assertFalse(as.hasValue()); + assertFalse(as.hasThrowable()); + assertTrue(as.hasComplete()); + assertNull(as.getValue()); + assertNull(as.getThrowable()); + } + + @Test + public void currentStateMethodsError() { + AsyncProcessor<Object> as = AsyncProcessor.create(); + + assertFalse(as.hasValue()); + assertFalse(as.hasThrowable()); + assertFalse(as.hasComplete()); + assertNull(as.getValue()); + assertNull(as.getThrowable()); + + as.onError(new TestException()); + + assertFalse(as.hasValue()); + assertTrue(as.hasThrowable()); + assertFalse(as.hasComplete()); + assertNull(as.getValue()); + assertTrue(as.getThrowable() instanceof TestException); + } + + @Test + public void fusionLive() { + AsyncProcessor<Integer> ap = new AsyncProcessor<>(); + + TestSubscriberEx<Integer> ts = new TestSubscriberEx<Integer>().setInitialFusionMode(QueueFuseable.ANY); + + ap.subscribe(ts); + + ts + .assertFuseable() + .assertFusionMode(QueueFuseable.ASYNC); + + ts.assertNoValues().assertNoErrors().assertNotComplete(); + + ap.onNext(1); + + ts.assertNoValues().assertNoErrors().assertNotComplete(); + + ap.onComplete(); + + ts.assertResult(1); + } + + @Test + public void fusionOfflie() { + AsyncProcessor<Integer> ap = new AsyncProcessor<>(); + ap.onNext(1); + ap.onComplete(); + + TestSubscriberEx<Integer> ts = new TestSubscriberEx<Integer>().setInitialFusionMode(QueueFuseable.ANY); + + ap.subscribe(ts); + + ts + .assertFuseable() + .assertFusionMode(QueueFuseable.ASYNC) + .assertResult(1); + } + + @Test + public void onSubscribeAfterDone() { + AsyncProcessor<Object> p = AsyncProcessor.create(); + + BooleanSubscription bs = new BooleanSubscription(); + p.onSubscribe(bs); + + assertFalse(bs.isCancelled()); + + p.onComplete(); + + bs = new BooleanSubscription(); + p.onSubscribe(bs); + + assertTrue(bs.isCancelled()); + + p.test().assertResult(); + } + + @Test + public void cancelUpfront() { + AsyncProcessor<Object> p = AsyncProcessor.create(); + + assertFalse(p.hasSubscribers()); + + p.test().assertEmpty(); + p.test().assertEmpty(); + + p.test(0L, true).assertEmpty(); + + assertTrue(p.hasSubscribers()); + } + + @Test + public void cancelRace() { + AsyncProcessor<Object> p = AsyncProcessor.create(); + + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final TestSubscriber<Object> ts1 = p.test(); + final TestSubscriber<Object> ts2 = p.test(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + ts1.cancel(); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + ts2.cancel(); + } + }; + + TestHelper.race(r1, r2); + } + } + + @Test + @SuppressUndeliverable + public void onErrorCancelRace() { + + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final AsyncProcessor<Object> p = AsyncProcessor.create(); + + final TestSubscriberEx<Object> ts1 = p.to(TestHelper.<Object>testConsumer()); + + Runnable r1 = new Runnable() { + @Override + public void run() { + ts1.cancel(); + } + }; + + final TestException ex = new TestException(); + + Runnable r2 = new Runnable() { + @Override + public void run() { + p.onError(ex); + } + }; + + TestHelper.race(r1, r2); + + if (ts1.errors().size() != 0) { + ts1.assertFailure(TestException.class); + } else { + ts1.assertEmpty(); + } + } + } + + @Test + public void onNextCrossCancel() { + AsyncProcessor<Object> p = AsyncProcessor.create(); + + final TestSubscriber<Object> ts2 = new TestSubscriber<>(); + TestSubscriber<Object> ts1 = new TestSubscriber<Object>() { + @Override + public void onNext(Object t) { + ts2.cancel(); + super.onNext(t); + } + }; + + p.subscribe(ts1); + p.subscribe(ts2); + + p.onNext(1); + p.onComplete(); + + ts1.assertResult(1); + ts2.assertEmpty(); + } + + @Test + @SuppressUndeliverable + public void onErrorCrossCancel() { + AsyncProcessor<Object> p = AsyncProcessor.create(); + + final TestSubscriber<Object> ts2 = new TestSubscriber<>(); + TestSubscriber<Object> ts1 = new TestSubscriber<Object>() { + @Override + public void onError(Throwable t) { + ts2.cancel(); + super.onError(t); + } + }; + + p.subscribe(ts1); + p.subscribe(ts2); + + p.onError(new TestException()); + + ts1.assertFailure(TestException.class); + ts2.assertEmpty(); + } + + @Test + public void onCompleteCrossCancel() { + AsyncProcessor<Object> p = AsyncProcessor.create(); + + final TestSubscriber<Object> ts2 = new TestSubscriber<>(); + TestSubscriber<Object> ts1 = new TestSubscriber<Object>() { + @Override + public void onComplete() { + ts2.cancel(); + super.onComplete(); + } + }; + + p.subscribe(ts1); + p.subscribe(ts2); + + p.onComplete(); + + ts1.assertResult(); + ts2.assertEmpty(); + } + + @Test + public void cancel() { + TestHelper.checkDisposed(AsyncProcessor.create()); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/processors/BehaviorProcessorTest.java b/src/test/java/io/reactivex/rxjava3/processors/BehaviorProcessorTest.java new file mode 100644 index 0000000000..991a599b82 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/processors/BehaviorProcessorTest.java @@ -0,0 +1,958 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.processors; + +import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import java.util.List; +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicReference; + +import org.junit.*; +import org.mockito.*; +import org.reactivestreams.Subscriber; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.subscriptions.BooleanSubscription; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.processors.BehaviorProcessor.BehaviorSubscription; +import io.reactivex.rxjava3.schedulers.Schedulers; +import io.reactivex.rxjava3.subscribers.*; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class BehaviorProcessorTest extends FlowableProcessorTest<Object> { + + private final Throwable testException = new Throwable(); + + @Override + protected FlowableProcessor<Object> create() { + return BehaviorProcessor.create(); + } + + @Test + public void thatSubscriberReceivesDefaultValueAndSubsequentEvents() { + BehaviorProcessor<String> processor = BehaviorProcessor.createDefault("default"); + + Subscriber<String> subscriber = TestHelper.mockSubscriber(); + processor.subscribe(subscriber); + + processor.onNext("one"); + processor.onNext("two"); + processor.onNext("three"); + + verify(subscriber, times(1)).onNext("default"); + verify(subscriber, times(1)).onNext("one"); + verify(subscriber, times(1)).onNext("two"); + verify(subscriber, times(1)).onNext("three"); + verify(subscriber, Mockito.never()).onError(testException); + verify(subscriber, Mockito.never()).onComplete(); + } + + @Test + public void thatSubscriberReceivesLatestAndThenSubsequentEvents() { + BehaviorProcessor<String> processor = BehaviorProcessor.createDefault("default"); + + processor.onNext("one"); + + Subscriber<String> subscriber = TestHelper.mockSubscriber(); + processor.subscribe(subscriber); + + processor.onNext("two"); + processor.onNext("three"); + + verify(subscriber, Mockito.never()).onNext("default"); + verify(subscriber, times(1)).onNext("one"); + verify(subscriber, times(1)).onNext("two"); + verify(subscriber, times(1)).onNext("three"); + verify(subscriber, Mockito.never()).onError(testException); + verify(subscriber, Mockito.never()).onComplete(); + } + + @Test + public void subscribeThenOnComplete() { + BehaviorProcessor<String> processor = BehaviorProcessor.createDefault("default"); + + Subscriber<String> subscriber = TestHelper.mockSubscriber(); + processor.subscribe(subscriber); + + processor.onNext("one"); + processor.onComplete(); + + verify(subscriber, times(1)).onNext("default"); + verify(subscriber, times(1)).onNext("one"); + verify(subscriber, Mockito.never()).onError(any(Throwable.class)); + verify(subscriber, times(1)).onComplete(); + } + + @Test + public void subscribeToCompletedOnlyEmitsOnComplete() { + BehaviorProcessor<String> processor = BehaviorProcessor.createDefault("default"); + processor.onNext("one"); + processor.onComplete(); + + Subscriber<String> subscriber = TestHelper.mockSubscriber(); + processor.subscribe(subscriber); + + verify(subscriber, never()).onNext("default"); + verify(subscriber, never()).onNext("one"); + verify(subscriber, Mockito.never()).onError(any(Throwable.class)); + verify(subscriber, times(1)).onComplete(); + } + + @Test + public void subscribeToErrorOnlyEmitsOnError() { + BehaviorProcessor<String> processor = BehaviorProcessor.createDefault("default"); + processor.onNext("one"); + RuntimeException re = new RuntimeException("test error"); + processor.onError(re); + + Subscriber<String> subscriber = TestHelper.mockSubscriber(); + processor.subscribe(subscriber); + + verify(subscriber, never()).onNext("default"); + verify(subscriber, never()).onNext("one"); + verify(subscriber, times(1)).onError(re); + verify(subscriber, never()).onComplete(); + } + + @Test + public void completedStopsEmittingData() { + BehaviorProcessor<Integer> channel = BehaviorProcessor.createDefault(2013); + Subscriber<Object> observerA = TestHelper.mockSubscriber(); + Subscriber<Object> observerB = TestHelper.mockSubscriber(); + Subscriber<Object> observerC = TestHelper.mockSubscriber(); + + TestSubscriber<Object> ts = new TestSubscriber<>(observerA); + + channel.subscribe(ts); + channel.subscribe(observerB); + + InOrder inOrderA = inOrder(observerA); + InOrder inOrderB = inOrder(observerB); + InOrder inOrderC = inOrder(observerC); + + inOrderA.verify(observerA).onNext(2013); + inOrderB.verify(observerB).onNext(2013); + + channel.onNext(42); + + inOrderA.verify(observerA).onNext(42); + inOrderB.verify(observerB).onNext(42); + + ts.cancel(); + inOrderA.verifyNoMoreInteractions(); + + channel.onNext(4711); + + inOrderB.verify(observerB).onNext(4711); + + channel.onComplete(); + + inOrderB.verify(observerB).onComplete(); + + channel.subscribe(observerC); + + inOrderC.verify(observerC).onComplete(); + + channel.onNext(13); + + inOrderB.verifyNoMoreInteractions(); + inOrderC.verifyNoMoreInteractions(); + } + + @Test + public void completedAfterErrorIsNotSent() { + BehaviorProcessor<String> processor = BehaviorProcessor.createDefault("default"); + + Subscriber<String> subscriber = TestHelper.mockSubscriber(); + processor.subscribe(subscriber); + + processor.onNext("one"); + processor.onError(testException); + processor.onNext("two"); + processor.onComplete(); + + verify(subscriber, times(1)).onNext("default"); + verify(subscriber, times(1)).onNext("one"); + verify(subscriber, times(1)).onError(testException); + verify(subscriber, never()).onNext("two"); + verify(subscriber, never()).onComplete(); + } + + @Test + public void completedAfterErrorIsNotSent2() { + BehaviorProcessor<String> processor = BehaviorProcessor.createDefault("default"); + + Subscriber<String> subscriber = TestHelper.mockSubscriber(); + processor.subscribe(subscriber); + + processor.onNext("one"); + processor.onError(testException); + processor.onNext("two"); + processor.onComplete(); + + verify(subscriber, times(1)).onNext("default"); + verify(subscriber, times(1)).onNext("one"); + verify(subscriber, times(1)).onError(testException); + verify(subscriber, never()).onNext("two"); + verify(subscriber, never()).onComplete(); + + Subscriber<Object> subscriber2 = TestHelper.mockSubscriber(); + processor.subscribe(subscriber2); + verify(subscriber2, times(1)).onError(testException); + verify(subscriber2, never()).onNext(any()); + verify(subscriber2, never()).onComplete(); + } + + @Test + public void completedAfterErrorIsNotSent3() { + BehaviorProcessor<String> processor = BehaviorProcessor.createDefault("default"); + + Subscriber<String> subscriber = TestHelper.mockSubscriber(); + processor.subscribe(subscriber); + + processor.onNext("one"); + processor.onComplete(); + processor.onNext("two"); + processor.onComplete(); + + verify(subscriber, times(1)).onNext("default"); + verify(subscriber, times(1)).onNext("one"); + verify(subscriber, times(1)).onComplete(); + verify(subscriber, never()).onError(any(Throwable.class)); + verify(subscriber, never()).onNext("two"); + + Subscriber<Object> subscriber2 = TestHelper.mockSubscriber(); + processor.subscribe(subscriber2); + verify(subscriber2, times(1)).onComplete(); + verify(subscriber2, never()).onNext(any()); + verify(subscriber, never()).onError(any(Throwable.class)); + } + + @Test + public void unsubscriptionCase() { + BehaviorProcessor<String> src = BehaviorProcessor.createDefault("null"); // FIXME was plain null which is not allowed + + for (int i = 0; i < 10; i++) { + final Subscriber<Object> subscriber = TestHelper.mockSubscriber(); + InOrder inOrder = inOrder(subscriber); + String v = "" + i; + src.onNext(v); + System.out.printf("Turn: %d%n", i); + src.firstElement().toFlowable() + .flatMap(new Function<String, Flowable<String>>() { + + @Override + public Flowable<String> apply(String t1) { + return Flowable.just(t1 + ", " + t1); + } + }) + .subscribe(new DefaultSubscriber<String>() { + @Override + public void onNext(String t) { + subscriber.onNext(t); + } + + @Override + public void onError(Throwable e) { + subscriber.onError(e); + } + + @Override + public void onComplete() { + subscriber.onComplete(); + } + }); + inOrder.verify(subscriber).onNext(v + ", " + v); + inOrder.verify(subscriber).onComplete(); + verify(subscriber, never()).onError(any(Throwable.class)); + } + } + + @Test + public void startEmpty() { + BehaviorProcessor<Integer> source = BehaviorProcessor.create(); + final Subscriber<Object> subscriber = TestHelper.mockSubscriber(); + InOrder inOrder = inOrder(subscriber); + + source.subscribe(subscriber); + + inOrder.verify(subscriber, never()).onNext(any()); + inOrder.verify(subscriber, never()).onComplete(); + + source.onNext(1); + + source.onComplete(); + + source.onNext(2); + + verify(subscriber, never()).onError(any(Throwable.class)); + + inOrder.verify(subscriber).onNext(1); + inOrder.verify(subscriber).onComplete(); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void startEmptyThenAddOne() { + BehaviorProcessor<Integer> source = BehaviorProcessor.create(); + final Subscriber<Object> subscriber = TestHelper.mockSubscriber(); + InOrder inOrder = inOrder(subscriber); + + source.onNext(1); + + source.subscribe(subscriber); + + inOrder.verify(subscriber).onNext(1); + + source.onComplete(); + + source.onNext(2); + + inOrder.verify(subscriber).onComplete(); + inOrder.verifyNoMoreInteractions(); + + verify(subscriber, never()).onError(any(Throwable.class)); + + } + + @Test + public void startEmptyCompleteWithOne() { + BehaviorProcessor<Integer> source = BehaviorProcessor.create(); + final Subscriber<Object> subscriber = TestHelper.mockSubscriber(); + + source.onNext(1); + source.onComplete(); + + source.onNext(2); + + source.subscribe(subscriber); + + verify(subscriber).onComplete(); + verify(subscriber, never()).onError(any(Throwable.class)); + verify(subscriber, never()).onNext(any()); + } + + @Test + public void takeOneSubscriber() { + BehaviorProcessor<Integer> source = BehaviorProcessor.createDefault(1); + final Subscriber<Object> subscriber = TestHelper.mockSubscriber(); + + source.take(1).subscribe(subscriber); + + verify(subscriber).onNext(1); + verify(subscriber).onComplete(); + verify(subscriber, never()).onError(any(Throwable.class)); + + assertEquals(0, source.subscriberCount()); + assertFalse(source.hasSubscribers()); + } + + @Test + public void emissionSubscriptionRace() throws Exception { + Scheduler s = Schedulers.io(); + Scheduler.Worker worker = Schedulers.io().createWorker(); + try { + for (int i = 0; i < 50000; i++) { + if (i % 1000 == 0) { + System.out.println(i); + } + final BehaviorProcessor<Object> rs = BehaviorProcessor.create(); + + final CountDownLatch finish = new CountDownLatch(1); + final CountDownLatch start = new CountDownLatch(1); + + worker.schedule(new Runnable() { + @Override + public void run() { + try { + start.await(); + } catch (Exception e1) { + e1.printStackTrace(); + } + rs.onNext(1); + } + }); + + final AtomicReference<Object> o = new AtomicReference<>(); + + rs.subscribeOn(s).observeOn(Schedulers.io()) + .subscribe(new DefaultSubscriber<Object>() { + + @Override + public void onComplete() { + o.set(-1); + finish.countDown(); + } + + @Override + public void onError(Throwable e) { + o.set(e); + finish.countDown(); + } + + @Override + public void onNext(Object t) { + o.set(t); + finish.countDown(); + } + + }); + start.countDown(); + + if (!finish.await(5, TimeUnit.SECONDS)) { + System.out.println(o.get()); + System.out.println(rs.hasSubscribers()); + rs.onComplete(); + Assert.fail("Timeout @ " + i); + break; + } else { + Assert.assertEquals(1, o.get()); + worker.schedule(new Runnable() { + @Override + public void run() { + rs.onComplete(); + } + }); + } + } + } finally { + worker.dispose(); + } + } + + @Test + public void currentStateMethodsNormalEmptyStart() { + BehaviorProcessor<Object> as = BehaviorProcessor.create(); + + assertFalse(as.hasValue()); + assertFalse(as.hasThrowable()); + assertFalse(as.hasComplete()); + assertNull(as.getValue()); + assertNull(as.getThrowable()); + + as.onNext(1); + + assertTrue(as.hasValue()); + assertFalse(as.hasThrowable()); + assertFalse(as.hasComplete()); + assertEquals(1, as.getValue()); + assertNull(as.getThrowable()); + + as.onComplete(); + + assertFalse(as.hasValue()); + assertFalse(as.hasThrowable()); + assertTrue(as.hasComplete()); + assertNull(as.getValue()); + assertNull(as.getThrowable()); + } + + @Test + public void currentStateMethodsNormalSomeStart() { + BehaviorProcessor<Object> as = BehaviorProcessor.createDefault((Object)1); + + assertTrue(as.hasValue()); + assertFalse(as.hasThrowable()); + assertFalse(as.hasComplete()); + assertEquals(1, as.getValue()); + assertNull(as.getThrowable()); + + as.onNext(2); + + assertTrue(as.hasValue()); + assertFalse(as.hasThrowable()); + assertFalse(as.hasComplete()); + assertEquals(2, as.getValue()); + assertNull(as.getThrowable()); + + as.onComplete(); + assertFalse(as.hasValue()); + assertFalse(as.hasThrowable()); + assertTrue(as.hasComplete()); + assertNull(as.getValue()); + assertNull(as.getThrowable()); + } + + @Test + public void currentStateMethodsEmpty() { + BehaviorProcessor<Object> as = BehaviorProcessor.create(); + + assertFalse(as.hasValue()); + assertFalse(as.hasThrowable()); + assertFalse(as.hasComplete()); + assertNull(as.getValue()); + assertNull(as.getThrowable()); + + as.onComplete(); + + assertFalse(as.hasValue()); + assertFalse(as.hasThrowable()); + assertTrue(as.hasComplete()); + assertNull(as.getValue()); + assertNull(as.getThrowable()); + } + + @Test + public void currentStateMethodsError() { + BehaviorProcessor<Object> as = BehaviorProcessor.create(); + + assertFalse(as.hasValue()); + assertFalse(as.hasThrowable()); + assertFalse(as.hasComplete()); + assertNull(as.getValue()); + assertNull(as.getThrowable()); + + as.onError(new TestException()); + + assertFalse(as.hasValue()); + assertTrue(as.hasThrowable()); + assertFalse(as.hasComplete()); + assertNull(as.getValue()); + assertTrue(as.getThrowable() instanceof TestException); + } + + @Test + public void cancelOnArrival() { + BehaviorProcessor<Object> p = BehaviorProcessor.create(); + + assertFalse(p.hasSubscribers()); + + p.test(0L, true).assertEmpty(); + + assertFalse(p.hasSubscribers()); + } + + @Test + public void onSubscribe() { + BehaviorProcessor<Object> p = BehaviorProcessor.create(); + + BooleanSubscription bs = new BooleanSubscription(); + + p.onSubscribe(bs); + + assertFalse(bs.isCancelled()); + + p.onComplete(); + + bs = new BooleanSubscription(); + + p.onSubscribe(bs); + + assertTrue(bs.isCancelled()); + } + + @Test + public void onErrorAfterComplete() { + BehaviorProcessor<Object> p = BehaviorProcessor.create(); + + p.onComplete(); + + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + p.onError(new TestException()); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void cancelOnArrival2() { + BehaviorProcessor<Object> p = BehaviorProcessor.create(); + + TestSubscriber<Object> ts = p.test(); + + p.test(0L, true).assertEmpty(); + + p.onNext(1); + p.onComplete(); + + ts.assertResult(1); + } + + @Test + public void addRemoveRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final BehaviorProcessor<Object> p = BehaviorProcessor.create(); + + final TestSubscriber<Object> ts = p.test(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + p.test(); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + ts.cancel(); + } + }; + + TestHelper.race(r1, r2); + } + } + + @Test + public void multipleSubscribersRemoveSomeRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final BehaviorProcessor<Object> p = BehaviorProcessor.create(); + + final TestSubscriber<Object> ts1 = p.test(); + final TestSubscriber<Object> ts2 = p.test(); + final TestSubscriber<Object> ts3 = p.test(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + ts1.cancel(); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + ts2.cancel(); + } + }; + + TestHelper.race(r1, r2); + + p.onNext(1); + ts3.assertValuesOnly(1); + } + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + @Test + public void subscribeOnNextRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final BehaviorProcessor<Object> p = BehaviorProcessor.createDefault((Object)1); + + final TestSubscriber[] ts = { null }; + + Runnable r1 = new Runnable() { + @Override + public void run() { + ts[0] = p.test(); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + p.onNext(2); + } + }; + + TestHelper.race(r1, r2); + + if (ts[0].values().size() == 1) { + ts[0].assertValue(2).assertNoErrors().assertNotComplete(); + } else { + ts[0].assertValues(1, 2).assertNoErrors().assertNotComplete(); + } + } + } + + @Test + public void firstBackpressured() { + BehaviorProcessor<Object> p = BehaviorProcessor.createDefault((Object)1); + + p.test(0L, false).assertFailure(MissingBackpressureException.class); + + assertFalse(p.hasSubscribers()); + } + + @Test + public void offer() { + BehaviorProcessor<Integer> pp = BehaviorProcessor.create(); + + TestSubscriber<Integer> ts = pp.test(0); + + assertFalse(pp.offer(1)); + + ts.request(1); + + assertTrue(pp.offer(1)); + + assertFalse(pp.offer(2)); + + ts.cancel(); + + assertTrue(pp.offer(2)); + + ts = pp.test(1); + + try { + pp.offer(null); + fail("Should have thrown NPE!"); + } catch (NullPointerException expected) { + // expected + } + + ts.assertValuesOnly(2); + } + + @Test + public void offerAsync() throws Exception { + final BehaviorProcessor<Integer> pp = BehaviorProcessor.create(); + + Schedulers.single().scheduleDirect(new Runnable() { + @Override + public void run() { + while (!pp.hasSubscribers()) { + try { + Thread.sleep(1); + } catch (InterruptedException ex) { + return; + } + } + + for (int i = 1; i <= 10; i++) { + while (!pp.offer(i)) { } + } + pp.onComplete(); + } + }); + + Thread.sleep(1); + + pp.test() + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + } + + @Test + public void completeSubscribeRace() throws Exception { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final BehaviorProcessor<Object> p = BehaviorProcessor.create(); + + final TestSubscriber<Object> ts = new TestSubscriber<>(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + p.subscribe(ts); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + p.onComplete(); + } + }; + + TestHelper.race(r1, r2); + + ts.assertResult(); + } + } + + @Test + public void errorSubscribeRace() throws Exception { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final BehaviorProcessor<Object> p = BehaviorProcessor.create(); + + final TestSubscriber<Object> ts = new TestSubscriber<>(); + + final TestException ex = new TestException(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + p.subscribe(ts); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + p.onError(ex); + } + }; + + TestHelper.race(r1, r2); + + ts.assertFailure(TestException.class); + } + } + + @Test + public void subscriberCancelOfferRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final BehaviorProcessor<Integer> pp = BehaviorProcessor.create(); + + final TestSubscriber<Integer> ts = pp.test(1); + + Runnable r1 = new Runnable() { + @Override + public void run() { + for (int i = 0; i < 2; i++) { + while (!pp.offer(i)) { } + } + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + ts.cancel(); + } + }; + + TestHelper.race(r1, r2); + + if (ts.values().size() > 0) { + ts.assertValuesOnly(0); + } else { + ts.assertEmpty(); + } + } + } + + @Test + public void behaviorDisposableDisposeState() { + BehaviorProcessor<Integer> bp = BehaviorProcessor.create(); + bp.onNext(1); + + TestSubscriber<Integer> ts = new TestSubscriber<>(); + + BehaviorSubscription<Integer> bs = new BehaviorSubscription<>(ts, bp); + ts.onSubscribe(bs); + + assertFalse(bs.cancelled); + + bs.cancel(); + + assertTrue(bs.cancelled); + + bs.cancel(); + + assertTrue(bs.cancelled); + + assertTrue(bs.test(2)); + + bs.emitFirst(); + + ts.assertEmpty(); + + bs.emitNext(2, 0); + } + + @Test + public void emitFirstDisposeRace() { + for (int i = 0; i < TestHelper.RACE_LONG_LOOPS; i++) { + + BehaviorProcessor<Integer> bp = BehaviorProcessor.create(); + bp.onNext(1); + + TestSubscriber<Integer> ts = new TestSubscriber<>(); + + final BehaviorSubscription<Integer> bs = new BehaviorSubscription<>(ts, bp); + ts.onSubscribe(bs); + + Runnable r1 = new Runnable() { + @Override + public void run() { + bs.emitFirst(); + } + }; + Runnable r2 = new Runnable() { + @Override + public void run() { + bs.cancel(); + } + }; + + TestHelper.race(r1, r2); + } + } + + @Test + public void emitNextDisposeRace() { + for (int i = 0; i < TestHelper.RACE_LONG_LOOPS; i++) { + + BehaviorProcessor<Integer> bp = BehaviorProcessor.create(); + bp.onNext(1); + + TestSubscriber<Integer> ts = new TestSubscriber<>(); + + final BehaviorSubscription<Integer> bs = new BehaviorSubscription<>(ts, bp); + ts.onSubscribe(bs); + + Runnable r1 = new Runnable() { + @Override + public void run() { + bs.emitNext(2, 0); + } + }; + Runnable r2 = new Runnable() { + @Override + public void run() { + bs.cancel(); + } + }; + + TestHelper.race(r1, r2); + } + } + + @Test + public void emittingEmitNext() { + BehaviorProcessor<Integer> bp = BehaviorProcessor.create(); + bp.onNext(1); + + TestSubscriber<Integer> ts = new TestSubscriber<>(); + + final BehaviorSubscription<Integer> bs = new BehaviorSubscription<>(ts, bp); + ts.onSubscribe(bs); + + bs.emitting = true; + bs.emitNext(2, 1); + bs.emitNext(3, 2); + + assertNotNull(bs.queue); + } + + @Test + public void badRequest() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + BehaviorProcessor<Integer> bp = BehaviorProcessor.create(); + bp.onNext(1); + + TestSubscriber<Integer> ts = new TestSubscriber<>(); + + final BehaviorSubscription<Integer> bs = new BehaviorSubscription<>(ts, bp); + ts.onSubscribe(bs); + + bs.request(-1); + + TestHelper.assertError(errors, 0, IllegalArgumentException.class); + } finally { + RxJavaPlugins.reset(); + } + } + +} diff --git a/src/test/java/io/reactivex/rxjava3/processors/FlowableProcessorTest.java b/src/test/java/io/reactivex/rxjava3/processors/FlowableProcessorTest.java new file mode 100644 index 0000000000..1575a5669c --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/processors/FlowableProcessorTest.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.processors; + +import static org.junit.Assert.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.RxJavaTest; +import io.reactivex.rxjava3.internal.util.ExceptionHelper; + +public abstract class FlowableProcessorTest<T> extends RxJavaTest { + + protected abstract FlowableProcessor<T> create(); + + @Test + public void onNextNull() { + FlowableProcessor<T> p = create(); + + try { + p.onNext(null); + fail("No NullPointerException thrown"); + } catch (NullPointerException ex) { + assertEquals(ExceptionHelper.nullWarning("onNext called with a null value."), ex.getMessage()); + } + + p.test().assertEmpty().cancel(); + } + + @Test + public void onErrorNull() { + FlowableProcessor<T> p = create(); + + try { + p.onError(null); + fail("No NullPointerException thrown"); + } catch (NullPointerException ex) { + assertEquals(ExceptionHelper.nullWarning("onError called with a null Throwable."), ex.getMessage()); + } + + p.test().assertEmpty().cancel(); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/processors/MulticastProcessorTest.java b/src/test/java/io/reactivex/rxjava3/processors/MulticastProcessorTest.java new file mode 100644 index 0000000000..0ac22cf1b2 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/processors/MulticastProcessorTest.java @@ -0,0 +1,829 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.processors; + +import static org.junit.Assert.*; + +import java.io.IOException; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import org.junit.Test; +import org.reactivestreams.Subscription; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.subscriptions.BooleanSubscription; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.subscribers.TestSubscriber; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class MulticastProcessorTest extends RxJavaTest { + + @Test + public void complete() { + MulticastProcessor<Integer> mp = MulticastProcessor.create(); + mp.start(); + + assertFalse(mp.hasSubscribers()); + assertFalse(mp.hasComplete()); + assertFalse(mp.hasThrowable()); + assertNull(mp.getThrowable()); + + TestSubscriber<Integer> ts = mp.test(); + + assertTrue(mp.hasSubscribers()); + assertFalse(mp.hasComplete()); + assertFalse(mp.hasThrowable()); + assertNull(mp.getThrowable()); + + mp.onNext(1); + mp.onComplete(); + + ts.assertResult(1); + + assertFalse(mp.hasSubscribers()); + assertTrue(mp.hasComplete()); + assertFalse(mp.hasThrowable()); + assertNull(mp.getThrowable()); + + mp.test().assertResult(); + } + + @Test + public void error() { + MulticastProcessor<Integer> mp = MulticastProcessor.create(); + mp.start(); + + assertFalse(mp.hasSubscribers()); + assertFalse(mp.hasComplete()); + assertFalse(mp.hasThrowable()); + assertNull(mp.getThrowable()); + + TestSubscriber<Integer> ts = mp.test(); + + assertTrue(mp.hasSubscribers()); + assertFalse(mp.hasComplete()); + assertFalse(mp.hasThrowable()); + assertNull(mp.getThrowable()); + + mp.onNext(1); + mp.onError(new IOException()); + + ts.assertFailure(IOException.class, 1); + + assertFalse(mp.hasSubscribers()); + assertFalse(mp.hasComplete()); + assertTrue(mp.hasThrowable()); + assertNotNull(mp.getThrowable()); + assertTrue("" + mp.getThrowable(), mp.getThrowable() instanceof IOException); + + mp.test().assertFailure(IOException.class); + } + + @Test + public void overflow() { + MulticastProcessor<Integer> mp = MulticastProcessor.create(1); + mp.start(); + + TestSubscriber<Integer> ts = mp.test(0); + + assertTrue(mp.offer(1)); + assertFalse(mp.offer(2)); + + mp.onNext(3); + + ts.assertEmpty(); + + ts.request(1); + + ts.assertFailure(MissingBackpressureException.class, 1); + + mp.test().assertFailure(MissingBackpressureException.class); + } + + @Test + public void backpressure() { + MulticastProcessor<Integer> mp = MulticastProcessor.create(16, false); + mp.start(); + + for (int i = 0; i < 10; i++) { + mp.onNext(i); + } + mp.onComplete(); + + mp.test(0) + .assertEmpty() + .requestMore(1) + .assertValuesOnly(0) + .requestMore(2) + .assertValuesOnly(0, 1, 2) + .requestMore(3) + .assertValuesOnly(0, 1, 2, 3, 4, 5) + .requestMore(4) + .assertResult(0, 1, 2, 3, 4, 5, 6, 7, 8, 9); + } + + @Test + public void refCounted() { + MulticastProcessor<Integer> mp = MulticastProcessor.create(true); + BooleanSubscription bs = new BooleanSubscription(); + + mp.onSubscribe(bs); + + assertFalse(bs.isCancelled()); + + mp.test().cancel(); + + assertTrue(bs.isCancelled()); + + assertFalse(mp.hasSubscribers()); + assertTrue(mp.hasComplete()); + assertFalse(mp.hasThrowable()); + assertNull(mp.getThrowable()); + + mp.test().assertResult(); + } + + @Test + public void refCounted2() { + MulticastProcessor<Integer> mp = MulticastProcessor.create(16, true); + BooleanSubscription bs = new BooleanSubscription(); + + mp.onSubscribe(bs); + + assertFalse(bs.isCancelled()); + + mp.test(1, true); + + assertTrue(bs.isCancelled()); + + assertFalse(mp.hasSubscribers()); + assertTrue(mp.hasComplete()); + assertFalse(mp.hasThrowable()); + assertNull(mp.getThrowable()); + + mp.test().assertResult(); + } + + @Test + public void longRunning() { + MulticastProcessor<Integer> mp = MulticastProcessor.create(16); + Flowable.range(1, 1000).subscribe(mp); + + mp.test().assertValueCount(1000).assertNoErrors().assertComplete(); + } + + @Test + public void oneByOne() { + MulticastProcessor<Integer> mp = MulticastProcessor.create(16); + Flowable.range(1, 1000).subscribe(mp); + + mp + .rebatchRequests(1) + .test() + .assertValueCount(1000) + .assertNoErrors() + .assertComplete(); + } + + @Test + public void take() { + MulticastProcessor<Integer> mp = MulticastProcessor.create(16); + Flowable.range(1, 1000).subscribe(mp); + + mp.take(10).test().assertResult(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + } + + @Test + public void takeRefCount() { + MulticastProcessor<Integer> mp = MulticastProcessor.create(16, true); + Flowable.range(1, 1000).subscribe(mp); + + mp.take(10).test().assertResult(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + } + + @Test + public void takeRefCountExact() { + MulticastProcessor<Integer> mp = MulticastProcessor.create(16, true); + Flowable.range(1, 10).subscribe(mp); + + mp + .rebatchRequests(10) + .take(10) + .test().assertResult(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + } + + @Test + public void crossCancel() { + + final TestSubscriber<Integer> ts1 = new TestSubscriber<>(); + + TestSubscriber<Integer> ts2 = new TestSubscriber<Integer>() { + @Override + public void onNext(Integer t) { + super.onNext(t); + ts1.cancel(); + ts1.onComplete(); + } + }; + + MulticastProcessor<Integer> mp = MulticastProcessor.create(false); + + mp.subscribe(ts2); + mp.subscribe(ts1); + + mp.start(); + + mp.onNext(1); + mp.onComplete(); + + ts1.assertResult(); + ts2.assertResult(1); + } + + @Test + public void crossCancelError() { + + final TestSubscriber<Integer> ts1 = new TestSubscriber<>(); + + TestSubscriber<Integer> ts2 = new TestSubscriber<Integer>() { + @Override + public void onError(Throwable t) { + super.onError(t); + ts1.cancel(); + ts1.onComplete(); + } + }; + + MulticastProcessor<Integer> mp = MulticastProcessor.create(false); + + mp.subscribe(ts2); + mp.subscribe(ts1); + + mp.start(); + + mp.onNext(1); + mp.onError(new IOException()); + + ts1.assertResult(1); + ts2.assertFailure(IOException.class, 1); + } + + @Test + public void crossCancelComplete() { + + final TestSubscriber<Integer> ts1 = new TestSubscriber<>(); + + TestSubscriber<Integer> ts2 = new TestSubscriber<Integer>() { + @Override + public void onComplete() { + super.onComplete(); + ts1.cancel(); + ts1.onNext(2); + ts1.onComplete(); + } + }; + + MulticastProcessor<Integer> mp = MulticastProcessor.create(false); + + mp.subscribe(ts2); + mp.subscribe(ts1); + + mp.start(); + + mp.onNext(1); + mp.onComplete(); + + ts1.assertResult(1, 2); + ts2.assertResult(1); + } + + @Test + public void crossCancel1() { + + final TestSubscriber<Integer> ts1 = new TestSubscriber<>(1); + + TestSubscriber<Integer> ts2 = new TestSubscriber<Integer>(1) { + @Override + public void onNext(Integer t) { + super.onNext(t); + ts1.cancel(); + ts1.onComplete(); + } + }; + + MulticastProcessor<Integer> mp = MulticastProcessor.create(false); + + mp.subscribe(ts2); + mp.subscribe(ts1); + + mp.start(); + + mp.onNext(1); + mp.onComplete(); + + ts1.assertResult(); + ts2.assertResult(1); + } + + @Test + public void requestCancel() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + MulticastProcessor<Integer> mp = MulticastProcessor.create(false); + + mp.subscribe(new FlowableSubscriber<Integer>() { + + @Override + public void onNext(Integer t) { + } + + @Override + public void onError(Throwable t) { + } + + @Override + public void onComplete() { + } + + @Override + public void onSubscribe(Subscription t) { + t.request(-1); + t.request(1); + t.request(Long.MAX_VALUE); + t.request(Long.MAX_VALUE); + t.cancel(); + t.cancel(); + t.request(2); + } + }); + + TestHelper.assertError(errors, 0, IllegalArgumentException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void unbounded() { + MulticastProcessor<Integer> mp = MulticastProcessor.create(4, false); + mp.startUnbounded(); + + for (int i = 0; i < 10; i++) { + assertTrue(mp.offer(i)); + } + mp.onComplete(); + + mp.test().assertResult(0, 1, 2, 3, 4, 5, 6, 7, 8, 9); + } + + @Test + public void multiStart() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + MulticastProcessor<Integer> mp = MulticastProcessor.create(4, false); + + mp.start(); + mp.start(); + mp.startUnbounded(); + BooleanSubscription bs = new BooleanSubscription(); + mp.onSubscribe(bs); + + assertTrue(bs.isCancelled()); + + TestHelper.assertError(errors, 0, ProtocolViolationException.class); + TestHelper.assertError(errors, 1, ProtocolViolationException.class); + TestHelper.assertError(errors, 2, ProtocolViolationException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test(expected = NullPointerException.class) + public void onNextNull() { + MulticastProcessor<Integer> mp = MulticastProcessor.create(4, false); + mp.start(); + mp.onNext(null); + } + + @Test(expected = NullPointerException.class) + public void onOfferNull() { + MulticastProcessor<Integer> mp = MulticastProcessor.create(4, false); + mp.start(); + mp.offer(null); + } + + @Test(expected = NullPointerException.class) + public void onErrorNull() { + MulticastProcessor<Integer> mp = MulticastProcessor.create(4, false); + mp.start(); + mp.onError(null); + } + + @Test + public void afterTerminated() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + MulticastProcessor<Integer> mp = MulticastProcessor.create(); + mp.start(); + mp.onComplete(); + mp.onComplete(); + mp.onError(new IOException()); + mp.onNext(1); + mp.offer(1); + + mp.test().assertResult(); + + TestHelper.assertUndeliverable(errors, 0, IOException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void asyncFused() { + UnicastProcessor<Integer> up = UnicastProcessor.create(); + MulticastProcessor<Integer> mp = MulticastProcessor.create(4); + + up.subscribe(mp); + + TestSubscriber<Integer> ts = mp.test(); + + for (int i = 0; i < 10; i++) { + up.onNext(i); + } + + try { + mp.offer(10); + fail("Should have thrown IllegalStateException"); + } catch (IllegalStateException expected) { + // expected + } + + up.onComplete(); + + ts.assertResult(0, 1, 2, 3, 4, 5, 6, 7, 8, 9); + } + + @Test + public void fusionCrash() { + MulticastProcessor<Integer> mp = Flowable.range(1, 5) + .map(new Function<Integer, Integer>() { + @Override + public Integer apply(Integer v) throws Exception { + throw new IOException(); + } + }) + .subscribeWith(MulticastProcessor.<Integer>create()); + + mp.test().assertFailure(IOException.class); + } + + @Test + public void lockstep() { + MulticastProcessor<Integer> mp = MulticastProcessor.create(); + + TestSubscriber<Integer> ts1 = mp.test(); + mp.start(); + + mp.onNext(1); + mp.onNext(2); + + ts1.assertValues(1, 2); + + TestSubscriber<Integer> ts2 = mp.test(0); + + ts2.assertEmpty(); + + mp.onNext(3); + + ts1.assertValues(1, 2); + ts2.assertEmpty(); + + mp.onComplete(); + + ts1.assertValues(1, 2); + ts2.assertEmpty(); + + ts2.request(1); + + ts1.assertResult(1, 2, 3); + ts2.assertResult(3); + } + + @Test + public void rejectedFusion() { + + MulticastProcessor<Integer> mp = MulticastProcessor.create(); + + TestHelper.<Integer>rejectFlowableFusion() + .subscribe(mp); + + mp.test().assertEmpty(); + } + + @Test + public void addRemoveRaceNoRefCount() { + for (int i = 0; i < TestHelper.RACE_LONG_LOOPS; i++) { + final MulticastProcessor<Integer> mp = MulticastProcessor.create(); + + final TestSubscriber<Integer> ts = mp.test(); + final TestSubscriber<Integer> ts2 = new TestSubscriber<>(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + ts.cancel(); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + mp.subscribe(ts2); + } + }; + + TestHelper.race(r1, r2); + + assertTrue(mp.hasSubscribers()); + } + } + + @Test + public void addRemoveRaceNoRefCountNonEmpty() { + for (int i = 0; i < TestHelper.RACE_LONG_LOOPS; i++) { + final MulticastProcessor<Integer> mp = MulticastProcessor.create(); + + mp.test(); + final TestSubscriber<Integer> ts = mp.test(); + final TestSubscriber<Integer> ts2 = new TestSubscriber<>(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + ts.cancel(); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + mp.subscribe(ts2); + } + }; + + TestHelper.race(r1, r2); + + assertTrue(mp.hasSubscribers()); + } + } + + @Test + public void addRemoveRaceWitRefCount() { + for (int i = 0; i < TestHelper.RACE_LONG_LOOPS; i++) { + final MulticastProcessor<Integer> mp = MulticastProcessor.create(true); + + final TestSubscriber<Integer> ts = mp.test(); + final TestSubscriber<Integer> ts2 = new TestSubscriber<>(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + ts.cancel(); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + mp.subscribe(ts2); + } + }; + + TestHelper.race(r1, r2); + } + } + + @Test + public void cancelUpfront() { + MulticastProcessor<Integer> mp = MulticastProcessor.create(); + + mp.test(0, true).assertEmpty(); + + assertFalse(mp.hasSubscribers()); + } + + @Test + public void cancelUpfrontOtherConsumersPresent() { + MulticastProcessor<Integer> mp = MulticastProcessor.create(); + + mp.test(); + + mp.test(0, true).assertEmpty(); + + assertTrue(mp.hasSubscribers()); + } + + @Test + public void consumerRequestRace() { + for (int i = 0; i < TestHelper.RACE_LONG_LOOPS; i++) { + final MulticastProcessor<Integer> mp = MulticastProcessor.create(true); + mp.startUnbounded(); + mp.onNext(1); + mp.onNext(2); + + final TestSubscriber<Integer> ts = mp.test(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + ts.request(1); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + ts.request(1); + } + }; + + TestHelper.race(r1, r2); + + ts.assertValuesOnly(1, 2); + } + } + + @Test + public void consumerUpstreamRace() { + for (int i = 0; i < TestHelper.RACE_LONG_LOOPS; i++) { + final MulticastProcessor<Integer> mp = MulticastProcessor.create(true); + + final Flowable<Integer> source = Flowable.range(1, 5); + + final TestSubscriber<Integer> ts = mp.test(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + ts.request(5); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + source.subscribe(mp); + } + }; + + TestHelper.race(r1, r2); + + ts + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(1, 2, 3, 4, 5); + } + } + + @Test + public void emitCancelRace() { + for (int i = 0; i < TestHelper.RACE_LONG_LOOPS; i++) { + final MulticastProcessor<Integer> mp = MulticastProcessor.create(true); + mp.startUnbounded(); + + final TestSubscriber<Integer> ts = mp.test(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + ts.cancel(); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + mp.onNext(1); + } + }; + + TestHelper.race(r1, r2); + } + } + + @Test + public void cancelCancelDrain() { + for (int i = 0; i < TestHelper.RACE_LONG_LOOPS; i++) { + final MulticastProcessor<Integer> mp = MulticastProcessor.create(true); + + final TestSubscriber<Integer> ts1 = mp.test(); + final TestSubscriber<Integer> ts2 = mp.test(); + + mp.test(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + ts1.cancel(); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + ts2.cancel(); + } + }; + + TestHelper.race(r1, r2); + } + } + + @Test + public void requestCancelRace() { + for (int i = 0; i < TestHelper.RACE_LONG_LOOPS; i++) { + final MulticastProcessor<Integer> mp = MulticastProcessor.create(true); + + final TestSubscriber<Integer> ts1 = mp.test(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + ts1.cancel(); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + ts1.request(1); + } + }; + + TestHelper.race(r1, r2); + } + } + + @Test + public void noUpstream() { + MulticastProcessor<Integer> mp = MulticastProcessor.create(); + + TestSubscriber<Integer> ts = mp.test(0); + + ts.request(1); + + assertTrue(mp.hasSubscribers()); + } + + @Test + public void requestUpstreamPrefetchNonFused() { + for (int j = 1; j < 12; j++) { + MulticastProcessor<Integer> mp = MulticastProcessor.create(j, true); + + TestSubscriber<Integer> ts = mp.test(0).withTag("Prefetch: " + j); + + Flowable.range(1, 10).hide().subscribe(mp); + + ts.assertEmpty() + .requestMore(3) + .assertValuesOnly(1, 2, 3) + .requestMore(3) + .assertValuesOnly(1, 2, 3, 4, 5, 6) + .requestMore(4) + .assertResult(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + } + } + + @Test + public void requestUpstreamPrefetchNonFused2() { + for (int j = 1; j < 12; j++) { + MulticastProcessor<Integer> mp = MulticastProcessor.create(j, true); + + TestSubscriber<Integer> ts = mp.test(0).withTag("Prefetch: " + j); + + Flowable.range(1, 10).hide().subscribe(mp); + + ts.assertEmpty() + .requestMore(2) + .assertValuesOnly(1, 2) + .requestMore(2) + .assertValuesOnly(1, 2, 3, 4) + .requestMore(6) + .assertResult(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + } + } +} diff --git a/src/test/java/io/reactivex/rxjava3/processors/PublishProcessorTest.java b/src/test/java/io/reactivex/rxjava3/processors/PublishProcessorTest.java new file mode 100644 index 0000000000..380f644518 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/processors/PublishProcessorTest.java @@ -0,0 +1,674 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.processors; + +import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import java.util.ArrayList; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.Test; +import org.mockito.*; +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.schedulers.Schedulers; +import io.reactivex.rxjava3.subscribers.*; +import io.reactivex.rxjava3.testsupport.*; + +public class PublishProcessorTest extends FlowableProcessorTest<Object> { + + @Override + protected FlowableProcessor<Object> create() { + return PublishProcessor.create(); + } + + @Test + @SuppressUndeliverable + public void completed() { + PublishProcessor<String> processor = PublishProcessor.create(); + + Subscriber<String> subscriber = TestHelper.mockSubscriber(); + processor.subscribe(subscriber); + + processor.onNext("one"); + processor.onNext("two"); + processor.onNext("three"); + processor.onComplete(); + + Subscriber<String> anotherSubscriber = TestHelper.mockSubscriber(); + processor.subscribe(anotherSubscriber); + + processor.onNext("four"); + processor.onComplete(); + processor.onError(new Throwable()); + + assertCompletedSubscriber(subscriber); + // todo bug? assertNeverSubscriber(anotherSubscriber); + } + + @Test + public void completedStopsEmittingData() { + PublishProcessor<Object> channel = PublishProcessor.create(); + Subscriber<Object> observerA = TestHelper.mockSubscriber(); + Subscriber<Object> observerB = TestHelper.mockSubscriber(); + Subscriber<Object> observerC = TestHelper.mockSubscriber(); + + TestSubscriber<Object> ts = new TestSubscriber<>(observerA); + + channel.subscribe(ts); + channel.subscribe(observerB); + + InOrder inOrderA = inOrder(observerA); + InOrder inOrderB = inOrder(observerB); + InOrder inOrderC = inOrder(observerC); + + channel.onNext(42); + + inOrderA.verify(observerA).onNext(42); + inOrderB.verify(observerB).onNext(42); + + ts.cancel(); + inOrderA.verifyNoMoreInteractions(); + + channel.onNext(4711); + + inOrderB.verify(observerB).onNext(4711); + + channel.onComplete(); + + inOrderB.verify(observerB).onComplete(); + + channel.subscribe(observerC); + + inOrderC.verify(observerC).onComplete(); + + channel.onNext(13); + + inOrderB.verifyNoMoreInteractions(); + inOrderC.verifyNoMoreInteractions(); + } + + private void assertCompletedSubscriber(Subscriber<String> subscriber) { + verify(subscriber, times(1)).onNext("one"); + verify(subscriber, times(1)).onNext("two"); + verify(subscriber, times(1)).onNext("three"); + verify(subscriber, Mockito.never()).onError(any(Throwable.class)); + verify(subscriber, times(1)).onComplete(); + } + + @Test + @SuppressUndeliverable + public void error() { + PublishProcessor<String> processor = PublishProcessor.create(); + + Subscriber<String> subscriber = TestHelper.mockSubscriber(); + processor.subscribe(subscriber); + + processor.onNext("one"); + processor.onNext("two"); + processor.onNext("three"); + processor.onError(testException); + + Subscriber<String> anotherSubscriber = TestHelper.mockSubscriber(); + processor.subscribe(anotherSubscriber); + + processor.onNext("four"); + processor.onError(new Throwable()); + processor.onComplete(); + + assertErrorSubscriber(subscriber); + // todo bug? assertNeverSubscriber(anotherSubscriber); + } + + private void assertErrorSubscriber(Subscriber<String> subscriber) { + verify(subscriber, times(1)).onNext("one"); + verify(subscriber, times(1)).onNext("two"); + verify(subscriber, times(1)).onNext("three"); + verify(subscriber, times(1)).onError(testException); + verify(subscriber, Mockito.never()).onComplete(); + } + + @Test + public void subscribeMidSequence() { + PublishProcessor<String> processor = PublishProcessor.create(); + + Subscriber<String> subscriber = TestHelper.mockSubscriber(); + processor.subscribe(subscriber); + + processor.onNext("one"); + processor.onNext("two"); + + assertObservedUntilTwo(subscriber); + + Subscriber<String> anotherSubscriber = TestHelper.mockSubscriber(); + processor.subscribe(anotherSubscriber); + + processor.onNext("three"); + processor.onComplete(); + + assertCompletedSubscriber(subscriber); + assertCompletedStartingWithThreeSubscriber(anotherSubscriber); + } + + private void assertCompletedStartingWithThreeSubscriber(Subscriber<String> subscriber) { + verify(subscriber, Mockito.never()).onNext("one"); + verify(subscriber, Mockito.never()).onNext("two"); + verify(subscriber, times(1)).onNext("three"); + verify(subscriber, Mockito.never()).onError(any(Throwable.class)); + verify(subscriber, times(1)).onComplete(); + } + + @Test + public void unsubscribeFirstSubscriber() { + PublishProcessor<String> processor = PublishProcessor.create(); + + Subscriber<String> subscriber = TestHelper.mockSubscriber(); + TestSubscriber<String> ts = new TestSubscriber<>(subscriber); + processor.subscribe(ts); + + processor.onNext("one"); + processor.onNext("two"); + + ts.cancel(); + assertObservedUntilTwo(subscriber); + + Subscriber<String> anotherSubscriber = TestHelper.mockSubscriber(); + processor.subscribe(anotherSubscriber); + + processor.onNext("three"); + processor.onComplete(); + + assertObservedUntilTwo(subscriber); + assertCompletedStartingWithThreeSubscriber(anotherSubscriber); + } + + private void assertObservedUntilTwo(Subscriber<String> subscriber) { + verify(subscriber, times(1)).onNext("one"); + verify(subscriber, times(1)).onNext("two"); + verify(subscriber, Mockito.never()).onNext("three"); + verify(subscriber, Mockito.never()).onError(any(Throwable.class)); + verify(subscriber, Mockito.never()).onComplete(); + } + + @Test + public void nestedSubscribe() { + final PublishProcessor<Integer> s = PublishProcessor.create(); + + final AtomicInteger countParent = new AtomicInteger(); + final AtomicInteger countChildren = new AtomicInteger(); + final AtomicInteger countTotal = new AtomicInteger(); + + final ArrayList<String> list = new ArrayList<>(); + + s.flatMap(new Function<Integer, Flowable<String>>() { + + @Override + public Flowable<String> apply(final Integer v) { + countParent.incrementAndGet(); + + // then subscribe to processor again (it will not receive the previous value) + return s.map(new Function<Integer, String>() { + + @Override + public String apply(Integer v2) { + countChildren.incrementAndGet(); + return "Parent: " + v + " Child: " + v2; + } + + }); + } + + }).subscribe(new Consumer<String>() { + + @Override + public void accept(String v) { + countTotal.incrementAndGet(); + list.add(v); + } + + }); + + for (int i = 0; i < 10; i++) { + s.onNext(i); + } + s.onComplete(); + + // System.out.println("countParent: " + countParent.get()); + // System.out.println("countChildren: " + countChildren.get()); + // System.out.println("countTotal: " + countTotal.get()); + + // 9+8+7+6+5+4+3+2+1+0 == 45 + assertEquals(45, list.size()); + } + + /** + * Should be able to unsubscribe all Subscribers, have it stop emitting, then subscribe new ones and it start emitting again. + */ + @Test + public void reSubscribe() { + final PublishProcessor<Integer> pp = PublishProcessor.create(); + + Subscriber<Integer> subscriber1 = TestHelper.mockSubscriber(); + TestSubscriber<Integer> ts = new TestSubscriber<>(subscriber1); + pp.subscribe(ts); + + // emit + pp.onNext(1); + + // validate we got it + InOrder inOrder1 = inOrder(subscriber1); + inOrder1.verify(subscriber1, times(1)).onNext(1); + inOrder1.verifyNoMoreInteractions(); + + // unsubscribe + ts.cancel(); + + // emit again but nothing will be there to receive it + pp.onNext(2); + + Subscriber<Integer> subscriber2 = TestHelper.mockSubscriber(); + TestSubscriber<Integer> ts2 = new TestSubscriber<>(subscriber2); + pp.subscribe(ts2); + + // emit + pp.onNext(3); + + // validate we got it + InOrder inOrder2 = inOrder(subscriber2); + inOrder2.verify(subscriber2, times(1)).onNext(3); + inOrder2.verifyNoMoreInteractions(); + + ts2.cancel(); + } + + private final Throwable testException = new Throwable(); + + @Test + public void unsubscriptionCase() { + PublishProcessor<String> src = PublishProcessor.create(); + + for (int i = 0; i < 10; i++) { + final Subscriber<Object> subscriber = TestHelper.mockSubscriber(); + InOrder inOrder = inOrder(subscriber); + String v = "" + i; + System.out.printf("Turn: %d%n", i); + src.firstElement().toFlowable() + .flatMap(new Function<String, Flowable<String>>() { + + @Override + public Flowable<String> apply(String t1) { + return Flowable.just(t1 + ", " + t1); + } + }) + .subscribe(new DefaultSubscriber<String>() { + @Override + public void onNext(String t) { + subscriber.onNext(t); + } + + @Override + public void onError(Throwable e) { + subscriber.onError(e); + } + + @Override + public void onComplete() { + subscriber.onComplete(); + } + }); + src.onNext(v); + + inOrder.verify(subscriber).onNext(v + ", " + v); + inOrder.verify(subscriber).onComplete(); + verify(subscriber, never()).onError(any(Throwable.class)); + } + } + + @Test + public void currentStateMethodsNormal() { + PublishProcessor<Object> as = PublishProcessor.create(); + + assertFalse(as.hasThrowable()); + assertFalse(as.hasComplete()); + assertNull(as.getThrowable()); + + as.onNext(1); + + assertFalse(as.hasThrowable()); + assertFalse(as.hasComplete()); + assertNull(as.getThrowable()); + + as.onComplete(); + + assertFalse(as.hasThrowable()); + assertTrue(as.hasComplete()); + assertNull(as.getThrowable()); + } + + @Test + public void currentStateMethodsEmpty() { + PublishProcessor<Object> as = PublishProcessor.create(); + + assertFalse(as.hasThrowable()); + assertFalse(as.hasComplete()); + assertNull(as.getThrowable()); + + as.onComplete(); + + assertFalse(as.hasThrowable()); + assertTrue(as.hasComplete()); + assertNull(as.getThrowable()); + } + + @Test + public void currentStateMethodsError() { + PublishProcessor<Object> as = PublishProcessor.create(); + + assertFalse(as.hasThrowable()); + assertFalse(as.hasComplete()); + assertNull(as.getThrowable()); + + as.onError(new TestException()); + + assertTrue(as.hasThrowable()); + assertFalse(as.hasComplete()); + assertTrue(as.getThrowable() instanceof TestException); + } + + @Test + public void subscribeTo() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + + Flowable.range(1, 10).subscribe(pp); + + assertTrue(pp.hasComplete()); + + PublishProcessor<Integer> pp2 = PublishProcessor.create(); + + pp2.subscribe(pp); + + assertFalse(pp2.hasSubscribers()); + } + + @Test + public void requestValidation() { + TestHelper.assertBadRequestReported(PublishProcessor.create()); + } + + @Test + public void crossCancel() { + final TestSubscriber<Integer> ts1 = new TestSubscriber<>(); + TestSubscriber<Integer> ts2 = new TestSubscriber<Integer>() { + @Override + public void onNext(Integer t) { + super.onNext(t); + ts1.cancel(); + } + }; + + PublishProcessor<Integer> pp = PublishProcessor.create(); + + pp.subscribe(ts2); + pp.subscribe(ts1); + + pp.onNext(1); + + ts2.assertValue(1); + + ts1.assertNoValues(); + } + + @Test + @SuppressUndeliverable + public void crossCancelOnError() { + final TestSubscriber<Integer> ts1 = new TestSubscriber<>(); + TestSubscriber<Integer> ts2 = new TestSubscriber<Integer>() { + @Override + public void onError(Throwable t) { + super.onError(t); + ts1.cancel(); + } + }; + + PublishProcessor<Integer> pp = PublishProcessor.create(); + + pp.subscribe(ts2); + pp.subscribe(ts1); + + pp.onError(new TestException()); + + ts2.assertError(TestException.class); + + ts1.assertNoErrors(); + } + + @Test + public void crossCancelOnComplete() { + final TestSubscriber<Integer> ts1 = new TestSubscriber<>(); + TestSubscriber<Integer> ts2 = new TestSubscriber<Integer>() { + @Override + public void onComplete() { + super.onComplete(); + ts1.cancel(); + } + }; + + PublishProcessor<Integer> pp = PublishProcessor.create(); + + pp.subscribe(ts2); + pp.subscribe(ts1); + + pp.onComplete(); + + ts2.assertComplete(); + + ts1.assertNotComplete(); + } + + @Test + public void backpressureOverflow() { + + PublishProcessor<Integer> pp = PublishProcessor.create(); + + TestSubscriber<Integer> ts = pp.test(0L); + + pp.onNext(1); + + ts.assertNoValues() + .assertNotComplete() + .assertError(MissingBackpressureException.class) + ; + } + + @Test + public void onSubscribeCancelsImmediately() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + + TestSubscriber<Integer> ts = pp.test(); + + pp.subscribe(new FlowableSubscriber<Integer>() { + + @Override + public void onSubscribe(Subscription s) { + s.cancel(); + } + + @Override + public void onNext(Integer t) { + + } + + @Override + public void onError(Throwable t) { + + } + + @Override + public void onComplete() { + + } + + }); + + ts.cancel(); + + assertFalse(pp.hasSubscribers()); + } + + @Test + public void terminateRace() throws Exception { + + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final PublishProcessor<Integer> pp = PublishProcessor.create(); + + TestSubscriber<Integer> ts = pp.test(); + + Runnable task = new Runnable() { + @Override + public void run() { + pp.onComplete(); + } + }; + + TestHelper.race(task, task); + + ts + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(); + } + } + + @Test + public void addRemoveRance() throws Exception { + + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final PublishProcessor<Integer> pp = PublishProcessor.create(); + + final TestSubscriber<Integer> ts = pp.test(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + pp.subscribe(); + } + }; + Runnable r2 = new Runnable() { + @Override + public void run() { + ts.cancel(); + } + }; + + TestHelper.race(r1, r2); + } + } + + @Test + public void offer() { + PublishProcessor<Integer> pp = PublishProcessor.create(); + + TestSubscriber<Integer> ts = pp.test(0); + + assertFalse(pp.offer(1)); + + ts.request(1); + + assertTrue(pp.offer(1)); + + assertFalse(pp.offer(2)); + + ts.cancel(); + + assertTrue(pp.offer(2)); + + ts = pp.test(0); + + try { + pp.offer(null); + fail("Should have thrown NPE!"); + } catch (NullPointerException expected) { + // expected + } + + ts.assertEmpty(); + } + + @Test + public void offerAsync() throws Exception { + final PublishProcessor<Integer> pp = PublishProcessor.create(); + + Schedulers.single().scheduleDirect(new Runnable() { + @Override + public void run() { + while (!pp.hasSubscribers()) { + try { + Thread.sleep(1); + } catch (InterruptedException ex) { + return; + } + } + + for (int i = 1; i <= 10; i++) { + while (!pp.offer(i)) { } + } + pp.onComplete(); + } + }); + + Thread.sleep(1); + + pp.test() + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + } + + @Test + public void subscriberCancelOfferRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final PublishProcessor<Integer> pp = PublishProcessor.create(); + + final TestSubscriber<Integer> ts = pp.test(1); + + Runnable r1 = new Runnable() { + @Override + public void run() { + for (int i = 0; i < 2; i++) { + while (!pp.offer(i)) { } + } + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + ts.cancel(); + } + }; + + TestHelper.race(r1, r2); + + if (ts.values().size() > 0) { + ts.assertValuesOnly(0); + } else { + ts.assertEmpty(); + } + } + } +} diff --git a/src/test/java/io/reactivex/rxjava3/processors/ReplayProcessorBoundedConcurrencyTest.java b/src/test/java/io/reactivex/rxjava3/processors/ReplayProcessorBoundedConcurrencyTest.java new file mode 100644 index 0000000000..f080ce5e1d --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/processors/ReplayProcessorBoundedConcurrencyTest.java @@ -0,0 +1,559 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.processors; + +import static org.junit.Assert.assertEquals; + +import java.util.*; +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicReference; + +import org.junit.*; +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.functions.Consumer; +import io.reactivex.rxjava3.schedulers.Schedulers; +import io.reactivex.rxjava3.subscribers.DefaultSubscriber; +import io.reactivex.rxjava3.testsupport.TestSubscriberEx; + +public class ReplayProcessorBoundedConcurrencyTest extends RxJavaTest { + + @Test + public void replaySubjectConcurrentSubscribersDoingReplayDontBlockEachOther() throws InterruptedException { + final ReplayProcessor<Long> replay = ReplayProcessor.createUnbounded(); + Thread source = new Thread(new Runnable() { + + @Override + public void run() { + Flowable.unsafeCreate(new Publisher<Long>() { + + @Override + public void subscribe(Subscriber<? super Long> subscriber) { + System.out.println("********* Start Source Data ***********"); + for (long l = 1; l <= 10000; l++) { + subscriber.onNext(l); + } + System.out.println("********* Finished Source Data ***********"); + subscriber.onComplete(); + } + }).subscribe(replay); + } + }); + source.start(); + + long v = replay.blockingLast(); + assertEquals(10000, v); + + // it's been played through once so now it will all be replays + final CountDownLatch slowLatch = new CountDownLatch(1); + Thread slowThread = new Thread(new Runnable() { + + @Override + public void run() { + Subscriber<Long> slow = new DefaultSubscriber<Long>() { + + @Override + public void onComplete() { + System.out.println("*** Slow Observer completed"); + slowLatch.countDown(); + } + + @Override + public void onError(Throwable e) { + } + + @Override + public void onNext(Long args) { + if (args == 1) { + System.out.println("*** Slow Observer STARTED"); + } + try { + if (args % 10 == 0) { + Thread.sleep(1); + } + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + }; + replay.subscribe(slow); + try { + slowLatch.await(); + } catch (InterruptedException e1) { + e1.printStackTrace(); + } + } + }); + slowThread.start(); + + Thread fastThread = new Thread(new Runnable() { + + @Override + public void run() { + final CountDownLatch fastLatch = new CountDownLatch(1); + Subscriber<Long> fast = new DefaultSubscriber<Long>() { + + @Override + public void onComplete() { + System.out.println("*** Fast Observer completed"); + fastLatch.countDown(); + } + + @Override + public void onError(Throwable e) { + } + + @Override + public void onNext(Long args) { + if (args == 1) { + System.out.println("*** Fast Observer STARTED"); + } + } + }; + replay.subscribe(fast); + try { + fastLatch.await(); + } catch (InterruptedException e1) { + e1.printStackTrace(); + } + } + }); + fastThread.start(); + fastThread.join(); + + // slow should not yet be completed when fast completes + assertEquals(1, slowLatch.getCount()); + + slowThread.join(); + } + + @Test + public void replaySubjectConcurrentSubscriptions() throws InterruptedException { + final ReplayProcessor<Long> replay = ReplayProcessor.createUnbounded(); + Thread source = new Thread(new Runnable() { + + @Override + public void run() { + Flowable.unsafeCreate(new Publisher<Long>() { + + @Override + public void subscribe(Subscriber<? super Long> subscriber) { + System.out.println("********* Start Source Data ***********"); + for (long l = 1; l <= 10000; l++) { + subscriber.onNext(l); + } + System.out.println("********* Finished Source Data ***********"); + subscriber.onComplete(); + } + }).subscribe(replay); + } + }); + + // used to collect results of each thread + final List<List<Long>> listOfListsOfValues = Collections.synchronizedList(new ArrayList<>()); + final List<Thread> threads = Collections.synchronizedList(new ArrayList<>()); + + for (int i = 1; i <= 200; i++) { + final int count = i; + if (count == 20) { + // start source data after we have some already subscribed + // and while others are in process of subscribing + source.start(); + } + if (count == 100) { + // wait for source to finish then keep adding after it's done + source.join(); + } + Thread t = new Thread(new Runnable() { + + @Override + public void run() { + List<Long> values = replay.toList().blockingGet(); + listOfListsOfValues.add(values); + System.out.println("Finished thread: " + count); + } + }); + t.start(); + System.out.println("Started thread: " + i); + threads.add(t); + } + + // wait for all threads to complete + for (Thread t : threads) { + t.join(); + } + + // assert all threads got the same results + List<Long> sums = new ArrayList<>(); + for (List<Long> values : listOfListsOfValues) { + long v = 0; + for (long l : values) { + v += l; + } + sums.add(v); + } + + long expected = sums.get(0); + boolean success = true; + for (long l : sums) { + if (l != expected) { + success = false; + System.out.println("FAILURE => Expected " + expected + " but got: " + l); + } + } + + if (success) { + System.out.println("Success! " + sums.size() + " each had the same sum of " + expected); + } else { + throw new RuntimeException("Concurrency Bug"); + } + + } + + /** + * Can receive timeout if subscribe never receives an onError/onComplete ... which reveals a race condition. + */ + @Test + public void subscribeCompletionRaceCondition() { + for (int i = 0; i < 50; i++) { + final ReplayProcessor<String> processor = ReplayProcessor.createUnbounded(); + final AtomicReference<String> value1 = new AtomicReference<>(); + + processor.subscribe(new Consumer<String>() { + + @Override + public void accept(String t1) { + try { + // simulate a slow observer + Thread.sleep(50); + } catch (InterruptedException e) { + e.printStackTrace(); + } + value1.set(t1); + } + + }); + + Thread t1 = new Thread(new Runnable() { + + @Override + public void run() { + processor.onNext("value"); + processor.onComplete(); + } + }); + + SubjectObserverThread t2 = new SubjectObserverThread(processor); + SubjectObserverThread t3 = new SubjectObserverThread(processor); + SubjectObserverThread t4 = new SubjectObserverThread(processor); + SubjectObserverThread t5 = new SubjectObserverThread(processor); + + t2.start(); + t3.start(); + t1.start(); + t4.start(); + t5.start(); + try { + t1.join(); + t2.join(); + t3.join(); + t4.join(); + t5.join(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + + assertEquals("value", value1.get()); + assertEquals("value", t2.value.get()); + assertEquals("value", t3.value.get()); + assertEquals("value", t4.value.get()); + assertEquals("value", t5.value.get()); + } + + } + + /** + * Make sure emission-subscription races are handled correctly. + * https://github.com/ReactiveX/RxJava/issues/1147 + */ + @Test + public void raceForTerminalState() { + final List<Integer> expected = Arrays.asList(1); + for (int i = 0; i < 100000; i++) { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(); + Flowable.just(1).subscribeOn(Schedulers.computation()).cache().subscribe(ts); + ts.awaitDone(5, TimeUnit.SECONDS); + ts.assertValueSequence(expected); + ts.assertTerminated(); + } + } + + private static class SubjectObserverThread extends Thread { + + private final ReplayProcessor<String> processor; + private final AtomicReference<String> value = new AtomicReference<>(); + + SubjectObserverThread(ReplayProcessor<String> processor) { + this.processor = processor; + } + + @Override + public void run() { + try { + // a timeout exception will happen if we don't get a terminal state + String v = processor.timeout(2000, TimeUnit.MILLISECONDS).blockingSingle(); + value.set(v); + } catch (Exception e) { + e.printStackTrace(); + } + } + } + + @Test + public void replaySubjectEmissionSubscriptionRace() throws Exception { + Scheduler s = Schedulers.io(); + Scheduler.Worker worker = Schedulers.io().createWorker(); + try { + for (int i = 0; i < 50000; i++) { + if (i % 1000 == 0) { + System.out.println(i); + } + final ReplayProcessor<Object> rs = ReplayProcessor.createWithSize(2); + + final CountDownLatch finish = new CountDownLatch(1); + final CountDownLatch start = new CountDownLatch(1); + +// int j = i; + + worker.schedule(new Runnable() { + @Override + public void run() { + try { + start.await(); + } catch (Exception e1) { + e1.printStackTrace(); + } +// System.out.println("> " + j); + rs.onNext(1); + } + }); + + final AtomicReference<Object> o = new AtomicReference<>(); + + rs +// .doOnSubscribe(v -> System.out.println("!! " + j)) +// .doOnNext(e -> System.out.println(">> " + j)) + .subscribeOn(s) + .observeOn(Schedulers.io()) +// .doOnNext(e -> System.out.println(">>> " + j)) + .subscribe(new DefaultSubscriber<Object>() { + + @Override + protected void onStart() { + super.onStart(); + } + + @Override + public void onComplete() { + o.set(-1); + finish.countDown(); + } + + @Override + public void onError(Throwable e) { + o.set(e); + finish.countDown(); + } + + @Override + public void onNext(Object t) { + o.set(t); + finish.countDown(); + } + + }); + start.countDown(); + + if (!finish.await(5, TimeUnit.SECONDS)) { + System.out.println(o.get()); + System.out.println(rs.hasSubscribers()); + rs.onComplete(); + Assert.fail("Timeout @ " + i); + break; + } else { + Assert.assertEquals(1, o.get()); + worker.schedule(new Runnable() { + @Override + public void run() { + rs.onComplete(); + } + }); + } + } + } finally { + worker.dispose(); + } + } + + @Test + public void concurrentSizeAndHasAnyValue() throws InterruptedException { + final ReplayProcessor<Object> rs = ReplayProcessor.createUnbounded(); + final CyclicBarrier cb = new CyclicBarrier(2); + + Thread t = new Thread(new Runnable() { + @Override + public void run() { + try { + cb.await(); + } catch (InterruptedException e) { + return; + } catch (BrokenBarrierException e) { + return; + } + for (int i = 0; i < 1000000; i++) { + rs.onNext(i); + } + rs.onComplete(); + System.out.println("Replay fill Thread finished!"); + } + }); + t.start(); + try { + cb.await(); + } catch (InterruptedException e) { + return; + } catch (BrokenBarrierException e) { + return; + } + int lastSize = 0; + for (; !rs.hasThrowable() && !rs.hasComplete();) { + int size = rs.size(); + boolean hasAny = rs.hasValue(); + Object[] values = rs.getValues(); + if (size < lastSize) { + Assert.fail("Size decreased! " + lastSize + " -> " + size); + } + if ((size > 0) && !hasAny) { + Assert.fail("hasAnyValue reports emptyness but size doesn't"); + } + if (size > values.length) { + Assert.fail("Got fewer values than size! " + size + " -> " + values.length); + } + for (int i = 0; i < values.length - 1; i++) { + Integer v1 = (Integer)values[i]; + Integer v2 = (Integer)values[i + 1]; + assertEquals(1, v2 - v1); + } + lastSize = size; + } + + t.join(); + } + + @Test + public void concurrentSizeAndHasAnyValueBounded() throws InterruptedException { + final ReplayProcessor<Object> rs = ReplayProcessor.createWithSize(3); + final CyclicBarrier cb = new CyclicBarrier(2); + + Thread t = new Thread(new Runnable() { + @Override + public void run() { + try { + cb.await(); + } catch (InterruptedException e) { + return; + } catch (BrokenBarrierException e) { + return; + } + for (int i = 0; i < 1000000; i++) { + rs.onNext(i); + } + rs.onComplete(); + System.out.println("Replay fill Thread finished!"); + } + }); + t.start(); + try { + cb.await(); + } catch (InterruptedException e) { + return; + } catch (BrokenBarrierException e) { + return; + } + for (; !rs.hasThrowable() && !rs.hasComplete();) { + rs.size(); // can't use value so just call to detect hangs + rs.hasValue(); // can't use value so just call to detect hangs + Object[] values = rs.getValues(); + for (int i = 0; i < values.length - 1; i++) { + Integer v1 = (Integer)values[i]; + Integer v2 = (Integer)values[i + 1]; + assertEquals(1, v2 - v1); + } + } + + t.join(); + } + + @Test + public void concurrentSizeAndHasAnyValueTimeBounded() throws InterruptedException { + final ReplayProcessor<Object> rs = ReplayProcessor.createWithTime(1, TimeUnit.MILLISECONDS, Schedulers.computation()); + final CyclicBarrier cb = new CyclicBarrier(2); + + Thread t = new Thread(new Runnable() { + @Override + public void run() { + try { + cb.await(); + } catch (InterruptedException e) { + return; + } catch (BrokenBarrierException e) { + return; + } + for (int i = 0; i < 1000000; i++) { + rs.onNext(i); + if (i % 10000 == 0) { + try { + Thread.sleep(1); + } catch (InterruptedException e) { + return; + } + } + } + rs.onComplete(); + System.out.println("Replay fill Thread finished!"); + } + }); + t.start(); + try { + cb.await(); + } catch (InterruptedException e) { + return; + } catch (BrokenBarrierException e) { + return; + } + for (; !rs.hasThrowable() && !rs.hasComplete();) { + rs.size(); // can't use value so just call to detect hangs + rs.hasValue(); // can't use value so just call to detect hangs + Object[] values = rs.getValues(); + for (int i = 0; i < values.length - 1; i++) { + Integer v1 = (Integer)values[i]; + Integer v2 = (Integer)values[i + 1]; + assertEquals(1, v2 - v1); + } + } + + t.join(); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/processors/ReplayProcessorConcurrencyTest.java b/src/test/java/io/reactivex/rxjava3/processors/ReplayProcessorConcurrencyTest.java new file mode 100644 index 0000000000..3afe66dd25 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/processors/ReplayProcessorConcurrencyTest.java @@ -0,0 +1,447 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.processors; + +import static org.junit.Assert.assertEquals; + +import java.util.*; +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicReference; + +import org.junit.*; +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.functions.Consumer; +import io.reactivex.rxjava3.schedulers.Schedulers; +import io.reactivex.rxjava3.subscribers.DefaultSubscriber; +import io.reactivex.rxjava3.testsupport.TestSubscriberEx; + +public class ReplayProcessorConcurrencyTest extends RxJavaTest { + + @Test + public void replaySubjectConcurrentSubscribersDoingReplayDontBlockEachOther() throws InterruptedException { + final ReplayProcessor<Long> replay = ReplayProcessor.create(); + Thread source = new Thread(new Runnable() { + + @Override + public void run() { + Flowable.unsafeCreate(new Publisher<Long>() { + + @Override + public void subscribe(Subscriber<? super Long> subscriber) { + System.out.println("********* Start Source Data ***********"); + for (long l = 1; l <= 10000; l++) { + subscriber.onNext(l); + } + System.out.println("********* Finished Source Data ***********"); + subscriber.onComplete(); + } + }).subscribe(replay); + } + }); + source.start(); + + long v = replay.blockingLast(); + assertEquals(10000, v); + + // it's been played through once so now it will all be replays + final CountDownLatch slowLatch = new CountDownLatch(1); + Thread slowThread = new Thread(new Runnable() { + + @Override + public void run() { + Subscriber<Long> slow = new DefaultSubscriber<Long>() { + + @Override + public void onComplete() { + System.out.println("*** Slow Observer completed"); + slowLatch.countDown(); + } + + @Override + public void onError(Throwable e) { + } + + @Override + public void onNext(Long args) { + if (args == 1) { + System.out.println("*** Slow Observer STARTED"); + } + try { + if (args % 10 == 0) { + Thread.sleep(1); + } + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + }; + replay.subscribe(slow); + try { + slowLatch.await(); + } catch (InterruptedException e1) { + e1.printStackTrace(); + } + } + }); + slowThread.start(); + + Thread fastThread = new Thread(new Runnable() { + + @Override + public void run() { + final CountDownLatch fastLatch = new CountDownLatch(1); + Subscriber<Long> fast = new DefaultSubscriber<Long>() { + + @Override + public void onComplete() { + System.out.println("*** Fast Observer completed"); + fastLatch.countDown(); + } + + @Override + public void onError(Throwable e) { + } + + @Override + public void onNext(Long args) { + if (args == 1) { + System.out.println("*** Fast Observer STARTED"); + } + } + }; + replay.subscribe(fast); + try { + fastLatch.await(); + } catch (InterruptedException e1) { + e1.printStackTrace(); + } + } + }); + fastThread.start(); + fastThread.join(); + + // slow should not yet be completed when fast completes + assertEquals(1, slowLatch.getCount()); + + slowThread.join(); + } + + @Test + public void replaySubjectConcurrentSubscriptions() throws InterruptedException { + final ReplayProcessor<Long> replay = ReplayProcessor.create(); + Thread source = new Thread(new Runnable() { + + @Override + public void run() { + Flowable.unsafeCreate(new Publisher<Long>() { + + @Override + public void subscribe(Subscriber<? super Long> subscriber) { + System.out.println("********* Start Source Data ***********"); + for (long l = 1; l <= 10000; l++) { + subscriber.onNext(l); + } + System.out.println("********* Finished Source Data ***********"); + subscriber.onComplete(); + } + }).subscribe(replay); + } + }); + + // used to collect results of each thread + final List<List<Long>> listOfListsOfValues = Collections.synchronizedList(new ArrayList<>()); + final List<Thread> threads = Collections.synchronizedList(new ArrayList<>()); + + for (int i = 1; i <= 200; i++) { + final int count = i; + if (count == 20) { + // start source data after we have some already subscribed + // and while others are in process of subscribing + source.start(); + } + if (count == 100) { + // wait for source to finish then keep adding after it's done + source.join(); + } + Thread t = new Thread(new Runnable() { + + @Override + public void run() { + List<Long> values = replay.toList().blockingGet(); + listOfListsOfValues.add(values); + System.out.println("Finished thread: " + count); + } + }); + t.start(); + System.out.println("Started thread: " + i); + threads.add(t); + } + + // wait for all threads to complete + for (Thread t : threads) { + t.join(); + } + + // assert all threads got the same results + List<Long> sums = new ArrayList<>(); + for (List<Long> values : listOfListsOfValues) { + long v = 0; + for (long l : values) { + v += l; + } + sums.add(v); + } + + long expected = sums.get(0); + boolean success = true; + for (long l : sums) { + if (l != expected) { + success = false; + System.out.println("FAILURE => Expected " + expected + " but got: " + l); + } + } + + if (success) { + System.out.println("Success! " + sums.size() + " each had the same sum of " + expected); + } else { + throw new RuntimeException("Concurrency Bug"); + } + + } + + /** + * Can receive timeout if subscribe never receives an onError/onComplete ... which reveals a race condition. + */ + @Test + public void subscribeCompletionRaceCondition() { + for (int i = 0; i < 50; i++) { + final ReplayProcessor<String> processor = ReplayProcessor.create(); + final AtomicReference<String> value1 = new AtomicReference<>(); + + processor.subscribe(new Consumer<String>() { + + @Override + public void accept(String t1) { + try { + // simulate a slow observer + Thread.sleep(50); + } catch (InterruptedException e) { + e.printStackTrace(); + } + value1.set(t1); + } + + }); + + Thread t1 = new Thread(new Runnable() { + + @Override + public void run() { + processor.onNext("value"); + processor.onComplete(); + } + }); + + SubjectObserverThread t2 = new SubjectObserverThread(processor); + SubjectObserverThread t3 = new SubjectObserverThread(processor); + SubjectObserverThread t4 = new SubjectObserverThread(processor); + SubjectObserverThread t5 = new SubjectObserverThread(processor); + + t2.start(); + t3.start(); + t1.start(); + t4.start(); + t5.start(); + try { + t1.join(); + t2.join(); + t3.join(); + t4.join(); + t5.join(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + + assertEquals("value", value1.get()); + assertEquals("value", t2.value.get()); + assertEquals("value", t3.value.get()); + assertEquals("value", t4.value.get()); + assertEquals("value", t5.value.get()); + } + + } + + /** + * Make sure emission-subscription races are handled correctly. + * https://github.com/ReactiveX/RxJava/issues/1147 + */ + @Test + public void raceForTerminalState() { + final List<Integer> expected = Arrays.asList(1); + for (int i = 0; i < 100000; i++) { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(); + Flowable.just(1).subscribeOn(Schedulers.computation()).cache().subscribe(ts); + ts.awaitDone(5, TimeUnit.SECONDS); + ts.assertValueSequence(expected); + ts.assertTerminated(); + } + } + + static class SubjectObserverThread extends Thread { + + private final ReplayProcessor<String> processor; + private final AtomicReference<String> value = new AtomicReference<>(); + + SubjectObserverThread(ReplayProcessor<String> processor) { + this.processor = processor; + } + + @Override + public void run() { + try { + // a timeout exception will happen if we don't get a terminal state + String v = processor.timeout(2000, TimeUnit.MILLISECONDS).blockingSingle(); + value.set(v); + } catch (Exception e) { + e.printStackTrace(); + } + } + } + + @Test + public void replaySubjectEmissionSubscriptionRace() throws Exception { + Scheduler s = Schedulers.io(); + Scheduler.Worker worker = Schedulers.io().createWorker(); + try { + for (int i = 0; i < 50000; i++) { + if (i % 1000 == 0) { + System.out.println(i); + } + final ReplayProcessor<Object> rs = ReplayProcessor.create(); + + final CountDownLatch finish = new CountDownLatch(1); + final CountDownLatch start = new CountDownLatch(1); + + worker.schedule(new Runnable() { + @Override + public void run() { + try { + start.await(); + } catch (Exception e1) { + e1.printStackTrace(); + } + rs.onNext(1); + } + }); + + final AtomicReference<Object> o = new AtomicReference<>(); + + rs.subscribeOn(s).observeOn(Schedulers.io()) + .subscribe(new DefaultSubscriber<Object>() { + + @Override + public void onComplete() { + o.set(-1); + finish.countDown(); + } + + @Override + public void onError(Throwable e) { + o.set(e); + finish.countDown(); + } + + @Override + public void onNext(Object t) { + o.set(t); + finish.countDown(); + } + + }); + start.countDown(); + + if (!finish.await(5, TimeUnit.SECONDS)) { + System.out.println(o.get()); + System.out.println(rs.hasSubscribers()); + rs.onComplete(); + Assert.fail("Timeout @ " + i); + break; + } else { + Assert.assertEquals(1, o.get()); + worker.schedule(new Runnable() { + @Override + public void run() { + rs.onComplete(); + } + }); + + } + } + } finally { + worker.dispose(); + } + } + + @Test + public void concurrentSizeAndHasAnyValue() throws InterruptedException { + final ReplayProcessor<Object> rs = ReplayProcessor.create(); + final CyclicBarrier cb = new CyclicBarrier(2); + + Thread t = new Thread(new Runnable() { + @Override + public void run() { + try { + cb.await(); + } catch (InterruptedException e) { + return; + } catch (BrokenBarrierException e) { + return; + } + for (int i = 0; i < 1000000; i++) { + rs.onNext(i); + } + rs.onComplete(); + System.out.println("Replay fill Thread finished!"); + } + }); + t.start(); + try { + cb.await(); + } catch (InterruptedException e) { + return; + } catch (BrokenBarrierException e) { + return; + } + int lastSize = 0; + for (; !rs.hasThrowable() && !rs.hasComplete();) { + int size = rs.size(); + boolean hasAny = rs.hasValue(); + Object[] values = rs.getValues(); + if (size < lastSize) { + Assert.fail("Size decreased! " + lastSize + " -> " + size); + } + if ((size > 0) && !hasAny) { + Assert.fail("hasAnyValue reports emptyness but size doesn't"); + } + if (size > values.length) { + Assert.fail("Got fewer values than size! " + size + " -> " + values.length); + } + lastSize = size; + } + + t.join(); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/processors/ReplayProcessorTest.java b/src/test/java/io/reactivex/rxjava3/processors/ReplayProcessorTest.java new file mode 100644 index 0000000000..aa7026a9ff --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/processors/ReplayProcessorTest.java @@ -0,0 +1,1835 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.processors; + +import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +import java.lang.management.*; +import java.util.Arrays; +import java.util.concurrent.*; +import java.util.concurrent.atomic.*; + +import org.junit.*; +import org.mockito.*; +import org.reactivestreams.*; + +import io.reactivex.rxjava3.annotations.NonNull; +import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.subscriptions.BooleanSubscription; +import io.reactivex.rxjava3.processors.ReplayProcessor.*; +import io.reactivex.rxjava3.schedulers.*; +import io.reactivex.rxjava3.subscribers.*; +import io.reactivex.rxjava3.testsupport.*; + +public class ReplayProcessorTest extends FlowableProcessorTest<Object> { + + private final Throwable testException = new Throwable(); + + @Override + protected FlowableProcessor<Object> create() { + return ReplayProcessor.create(); + } + + @Test + @SuppressUndeliverable + public void completed() { + ReplayProcessor<String> processor = ReplayProcessor.create(); + + Subscriber<String> subscriber1 = TestHelper.mockSubscriber(); + processor.subscribe(subscriber1); + + processor.onNext("one"); + processor.onNext("two"); + processor.onNext("three"); + processor.onComplete(); + + processor.onNext("four"); + processor.onComplete(); + processor.onError(new Throwable()); + + assertCompletedSubscriber(subscriber1); + + // assert that subscribing a 2nd time gets the same data + Subscriber<String> subscriber2 = TestHelper.mockSubscriber(); + processor.subscribe(subscriber2); + assertCompletedSubscriber(subscriber2); + } + + @Test + @SuppressUndeliverable + public void completedStopsEmittingData() { + ReplayProcessor<Integer> channel = ReplayProcessor.create(); + Subscriber<Object> observerA = TestHelper.mockSubscriber(); + Subscriber<Object> observerB = TestHelper.mockSubscriber(); + Subscriber<Object> observerC = TestHelper.mockSubscriber(); + Subscriber<Object> observerD = TestHelper.mockSubscriber(); + TestSubscriber<Object> ts = new TestSubscriber<>(observerA); + + channel.subscribe(ts); + channel.subscribe(observerB); + + InOrder inOrderA = inOrder(observerA); + InOrder inOrderB = inOrder(observerB); + InOrder inOrderC = inOrder(observerC); + InOrder inOrderD = inOrder(observerD); + + channel.onNext(42); + + // both A and B should have received 42 from before subscription + inOrderA.verify(observerA).onNext(42); + inOrderB.verify(observerB).onNext(42); + + ts.cancel(); + + // a should receive no more + inOrderA.verifyNoMoreInteractions(); + + channel.onNext(4711); + + // only be should receive 4711 at this point + inOrderB.verify(observerB).onNext(4711); + + channel.onComplete(); + + // B is subscribed so should receive onComplete + inOrderB.verify(observerB).onComplete(); + + channel.subscribe(observerC); + + // when C subscribes it should receive 42, 4711, onComplete + inOrderC.verify(observerC).onNext(42); + inOrderC.verify(observerC).onNext(4711); + inOrderC.verify(observerC).onComplete(); + + // if further events are propagated they should be ignored + channel.onNext(13); + channel.onNext(14); + channel.onNext(15); + channel.onError(new RuntimeException()); + + // a new subscription should only receive what was emitted prior to terminal state onComplete + channel.subscribe(observerD); + + inOrderD.verify(observerD).onNext(42); + inOrderD.verify(observerD).onNext(4711); + inOrderD.verify(observerD).onComplete(); + + verify(observerA).onSubscribe((Subscription)notNull()); + verify(observerB).onSubscribe((Subscription)notNull()); + verify(observerC).onSubscribe((Subscription)notNull()); + verify(observerD).onSubscribe((Subscription)notNull()); + Mockito.verifyNoMoreInteractions(observerA); + Mockito.verifyNoMoreInteractions(observerB); + Mockito.verifyNoMoreInteractions(observerC); + Mockito.verifyNoMoreInteractions(observerD); + + } + + @Test + @SuppressUndeliverable + public void completedAfterError() { + ReplayProcessor<String> processor = ReplayProcessor.create(); + + Subscriber<String> subscriber = TestHelper.mockSubscriber(); + + processor.onNext("one"); + processor.onError(testException); + processor.onNext("two"); + processor.onComplete(); + processor.onError(new RuntimeException()); + + processor.subscribe(subscriber); + verify(subscriber).onSubscribe((Subscription)notNull()); + verify(subscriber, times(1)).onNext("one"); + verify(subscriber, times(1)).onError(testException); + verifyNoMoreInteractions(subscriber); + } + + private void assertCompletedSubscriber(Subscriber<String> subscriber) { + InOrder inOrder = inOrder(subscriber); + + inOrder.verify(subscriber, times(1)).onNext("one"); + inOrder.verify(subscriber, times(1)).onNext("two"); + inOrder.verify(subscriber, times(1)).onNext("three"); + inOrder.verify(subscriber, Mockito.never()).onError(any(Throwable.class)); + inOrder.verify(subscriber, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + } + + @Test + @SuppressUndeliverable + public void error() { + ReplayProcessor<String> processor = ReplayProcessor.create(); + + Subscriber<String> subscriber = TestHelper.mockSubscriber(); + processor.subscribe(subscriber); + + processor.onNext("one"); + processor.onNext("two"); + processor.onNext("three"); + processor.onError(testException); + + processor.onNext("four"); + processor.onError(new Throwable()); + processor.onComplete(); + + assertErrorSubscriber(subscriber); + + subscriber = TestHelper.mockSubscriber(); + processor.subscribe(subscriber); + assertErrorSubscriber(subscriber); + } + + private void assertErrorSubscriber(Subscriber<String> subscriber) { + verify(subscriber, times(1)).onNext("one"); + verify(subscriber, times(1)).onNext("two"); + verify(subscriber, times(1)).onNext("three"); + verify(subscriber, times(1)).onError(testException); + verify(subscriber, Mockito.never()).onComplete(); + } + + @Test + public void subscribeMidSequence() { + ReplayProcessor<String> processor = ReplayProcessor.create(); + + Subscriber<String> subscriber = TestHelper.mockSubscriber(); + processor.subscribe(subscriber); + + processor.onNext("one"); + processor.onNext("two"); + + assertObservedUntilTwo(subscriber); + + Subscriber<String> anotherSubscriber = TestHelper.mockSubscriber(); + processor.subscribe(anotherSubscriber); + assertObservedUntilTwo(anotherSubscriber); + + processor.onNext("three"); + processor.onComplete(); + + assertCompletedSubscriber(subscriber); + assertCompletedSubscriber(anotherSubscriber); + } + + @Test + public void unsubscribeFirstSubscriber() { + ReplayProcessor<String> processor = ReplayProcessor.create(); + + Subscriber<String> subscriber = TestHelper.mockSubscriber(); + TestSubscriber<String> ts = new TestSubscriber<>(subscriber); + processor.subscribe(ts); + + processor.onNext("one"); + processor.onNext("two"); + + ts.cancel(); + assertObservedUntilTwo(subscriber); + + Subscriber<String> anotherSubscriber = TestHelper.mockSubscriber(); + processor.subscribe(anotherSubscriber); + assertObservedUntilTwo(anotherSubscriber); + + processor.onNext("three"); + processor.onComplete(); + + assertObservedUntilTwo(subscriber); + assertCompletedSubscriber(anotherSubscriber); + } + + private void assertObservedUntilTwo(Subscriber<String> subscriber) { + verify(subscriber, times(1)).onNext("one"); + verify(subscriber, times(1)).onNext("two"); + verify(subscriber, Mockito.never()).onNext("three"); + verify(subscriber, Mockito.never()).onError(any(Throwable.class)); + verify(subscriber, Mockito.never()).onComplete(); + } + + @Test + public void newSubscriberDoesntBlockExisting() throws InterruptedException { + + final AtomicReference<String> lastValueForSubscriber1 = new AtomicReference<>(); + Subscriber<String> subscriber1 = new DefaultSubscriber<String>() { + + @Override + public void onComplete() { + + } + + @Override + public void onError(Throwable e) { + + } + + @Override + public void onNext(String v) { + System.out.println("observer1: " + v); + lastValueForSubscriber1.set(v); + } + + }; + + final AtomicReference<String> lastValueForSubscriber2 = new AtomicReference<>(); + final CountDownLatch oneReceived = new CountDownLatch(1); + final CountDownLatch makeSlow = new CountDownLatch(1); + final CountDownLatch completed = new CountDownLatch(1); + Subscriber<String> subscriber2 = new DefaultSubscriber<String>() { + + @Override + public void onComplete() { + completed.countDown(); + } + + @Override + public void onError(Throwable e) { + + } + + @Override + public void onNext(String v) { + System.out.println("observer2: " + v); + if (v.equals("one")) { + oneReceived.countDown(); + } else { + try { + makeSlow.await(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + lastValueForSubscriber2.set(v); + } + } + + }; + + ReplayProcessor<String> processor = ReplayProcessor.create(); + processor.subscribe(subscriber1); + processor.onNext("one"); + assertEquals("one", lastValueForSubscriber1.get()); + processor.onNext("two"); + assertEquals("two", lastValueForSubscriber1.get()); + + // use subscribeOn to make this async otherwise we deadlock as we are using CountDownLatches + processor.subscribeOn(Schedulers.newThread()).subscribe(subscriber2); + + System.out.println("before waiting for one"); + + // wait until observer2 starts having replay occur + oneReceived.await(); + + System.out.println("after waiting for one"); + + processor.onNext("three"); + + System.out.println("sent three"); + + // if subscription blocked existing subscribers then 'makeSlow' would cause this to not be there yet + assertEquals("three", lastValueForSubscriber1.get()); + + System.out.println("about to send onComplete"); + + processor.onComplete(); + + System.out.println("completed processor"); + + // release + makeSlow.countDown(); + + System.out.println("makeSlow released"); + + completed.await(); + // all of them should be emitted with the last being "three" + assertEquals("three", lastValueForSubscriber2.get()); + + } + + @Test + public void subscriptionLeak() { + ReplayProcessor<Object> replaySubject = ReplayProcessor.create(); + + Disposable connection = replaySubject.subscribe(); + + assertEquals(1, replaySubject.subscriberCount()); + + connection.dispose(); + + assertEquals(0, replaySubject.subscriberCount()); + } + + @Test + public void unsubscriptionCase() { + ReplayProcessor<String> src = ReplayProcessor.create(); + + for (int i = 0; i < 10; i++) { + final Subscriber<Object> subscriber = TestHelper.mockSubscriber(); + InOrder inOrder = inOrder(subscriber); + String v = "" + i; + src.onNext(v); + System.out.printf("Turn: %d%n", i); + src.firstElement().toFlowable() + .flatMap(new Function<String, Flowable<String>>() { + + @Override + public Flowable<String> apply(String t1) { + return Flowable.just(t1 + ", " + t1); + } + }) + .subscribe(new DefaultSubscriber<String>() { + @Override + public void onNext(String t) { + System.out.println(t); + subscriber.onNext(t); + } + + @Override + public void onError(Throwable e) { + subscriber.onError(e); + } + + @Override + public void onComplete() { + subscriber.onComplete(); + } + }); + inOrder.verify(subscriber).onNext("0, 0"); + inOrder.verify(subscriber).onComplete(); + verify(subscriber, never()).onError(any(Throwable.class)); + } + } + + @Test + public void terminateOnce() { + ReplayProcessor<Integer> source = ReplayProcessor.create(); + source.onNext(1); + source.onNext(2); + source.onComplete(); + + final Subscriber<Integer> subscriber = TestHelper.mockSubscriber(); + + source.subscribe(new DefaultSubscriber<Integer>() { + + @Override + public void onNext(Integer t) { + subscriber.onNext(t); + } + + @Override + public void onError(Throwable e) { + subscriber.onError(e); + } + + @Override + public void onComplete() { + subscriber.onComplete(); + } + }); + + verify(subscriber).onNext(1); + verify(subscriber).onNext(2); + verify(subscriber).onComplete(); + verify(subscriber, never()).onError(any(Throwable.class)); + } + + @Test + public void replay1AfterTermination() { + ReplayProcessor<Integer> source = ReplayProcessor.createWithSize(1); + + source.onNext(1); + source.onNext(2); + source.onComplete(); + + for (int i = 0; i < 1; i++) { + Subscriber<Integer> subscriber = TestHelper.mockSubscriber(); + + source.subscribe(subscriber); + + verify(subscriber, never()).onNext(1); + verify(subscriber).onNext(2); + verify(subscriber).onComplete(); + verify(subscriber, never()).onError(any(Throwable.class)); + } + } + + @Test + public void replay1Directly() { + ReplayProcessor<Integer> source = ReplayProcessor.createWithSize(1); + + Subscriber<Integer> subscriber = TestHelper.mockSubscriber(); + + source.onNext(1); + source.onNext(2); + + source.subscribe(subscriber); + + source.onNext(3); + source.onComplete(); + + verify(subscriber, never()).onNext(1); + verify(subscriber).onNext(2); + verify(subscriber).onNext(3); + verify(subscriber).onComplete(); + verify(subscriber, never()).onError(any(Throwable.class)); + } + + @Test + public void replayTimestampedAfterTermination() { + TestScheduler scheduler = new TestScheduler(); + ReplayProcessor<Integer> source = ReplayProcessor.createWithTime(1, TimeUnit.SECONDS, scheduler); + + source.onNext(1); + + scheduler.advanceTimeBy(1, TimeUnit.SECONDS); + + source.onNext(2); + + scheduler.advanceTimeBy(1, TimeUnit.SECONDS); + + source.onNext(3); + source.onComplete(); + + scheduler.advanceTimeBy(1, TimeUnit.SECONDS); + + Subscriber<Integer> subscriber = TestHelper.mockSubscriber(); + + source.subscribe(subscriber); + + verify(subscriber, never()).onNext(1); + verify(subscriber, never()).onNext(2); + verify(subscriber, never()).onNext(3); + verify(subscriber).onComplete(); + verify(subscriber, never()).onError(any(Throwable.class)); + } + + @Test + public void replayTimestampedDirectly() { + TestScheduler scheduler = new TestScheduler(); + ReplayProcessor<Integer> source = ReplayProcessor.createWithTime(1, TimeUnit.SECONDS, scheduler); + + source.onNext(1); + + scheduler.advanceTimeBy(1, TimeUnit.SECONDS); + + Subscriber<Integer> subscriber = TestHelper.mockSubscriber(); + + source.subscribe(subscriber); + + source.onNext(2); + + scheduler.advanceTimeBy(1, TimeUnit.SECONDS); + + source.onNext(3); + + scheduler.advanceTimeBy(1, TimeUnit.SECONDS); + + source.onComplete(); + + scheduler.advanceTimeBy(1, TimeUnit.SECONDS); + + verify(subscriber, never()).onError(any(Throwable.class)); + verify(subscriber, never()).onNext(1); + verify(subscriber).onNext(2); + verify(subscriber).onNext(3); + verify(subscriber).onComplete(); + } + + @Test + public void currentStateMethodsNormal() { + ReplayProcessor<Object> as = ReplayProcessor.create(); + + assertFalse(as.hasThrowable()); + assertFalse(as.hasComplete()); + assertNull(as.getThrowable()); + + as.onNext(1); + + assertFalse(as.hasThrowable()); + assertFalse(as.hasComplete()); + assertNull(as.getThrowable()); + + as.onComplete(); + + assertFalse(as.hasThrowable()); + assertTrue(as.hasComplete()); + assertNull(as.getThrowable()); + } + + @Test + public void currentStateMethodsEmpty() { + ReplayProcessor<Object> as = ReplayProcessor.create(); + + assertFalse(as.hasThrowable()); + assertFalse(as.hasComplete()); + assertNull(as.getThrowable()); + + as.onComplete(); + + assertFalse(as.hasThrowable()); + assertTrue(as.hasComplete()); + assertNull(as.getThrowable()); + } + + @Test + public void currentStateMethodsError() { + ReplayProcessor<Object> as = ReplayProcessor.create(); + + assertFalse(as.hasThrowable()); + assertFalse(as.hasComplete()); + assertNull(as.getThrowable()); + + as.onError(new TestException()); + + assertTrue(as.hasThrowable()); + assertFalse(as.hasComplete()); + assertTrue(as.getThrowable() instanceof TestException); + } + + @Test + public void sizeAndHasAnyValueUnbounded() { + ReplayProcessor<Object> rs = ReplayProcessor.create(); + + assertEquals(0, rs.size()); + assertFalse(rs.hasValue()); + + rs.onNext(1); + + assertEquals(1, rs.size()); + assertTrue(rs.hasValue()); + + rs.onNext(1); + + assertEquals(2, rs.size()); + assertTrue(rs.hasValue()); + + rs.onComplete(); + + assertEquals(2, rs.size()); + assertTrue(rs.hasValue()); + } + + @Test + public void sizeAndHasAnyValueEffectivelyUnbounded() { + ReplayProcessor<Object> rs = ReplayProcessor.createUnbounded(); + + assertEquals(0, rs.size()); + assertFalse(rs.hasValue()); + + rs.onNext(1); + + assertEquals(1, rs.size()); + assertTrue(rs.hasValue()); + + rs.onNext(1); + + assertEquals(2, rs.size()); + assertTrue(rs.hasValue()); + + rs.onComplete(); + + assertEquals(2, rs.size()); + assertTrue(rs.hasValue()); + } + + @Test + public void sizeAndHasAnyValueUnboundedError() { + ReplayProcessor<Object> rs = ReplayProcessor.create(); + + assertEquals(0, rs.size()); + assertFalse(rs.hasValue()); + + rs.onNext(1); + + assertEquals(1, rs.size()); + assertTrue(rs.hasValue()); + + rs.onNext(1); + + assertEquals(2, rs.size()); + assertTrue(rs.hasValue()); + + rs.onError(new TestException()); + + assertEquals(2, rs.size()); + assertTrue(rs.hasValue()); + } + + @Test + public void sizeAndHasAnyValueEffectivelyUnboundedError() { + ReplayProcessor<Object> rs = ReplayProcessor.createUnbounded(); + + assertEquals(0, rs.size()); + assertFalse(rs.hasValue()); + + rs.onNext(1); + + assertEquals(1, rs.size()); + assertTrue(rs.hasValue()); + + rs.onNext(1); + + assertEquals(2, rs.size()); + assertTrue(rs.hasValue()); + + rs.onError(new TestException()); + + assertEquals(2, rs.size()); + assertTrue(rs.hasValue()); + } + + @Test + public void sizeAndHasAnyValueUnboundedEmptyError() { + ReplayProcessor<Object> rs = ReplayProcessor.create(); + + rs.onError(new TestException()); + + assertEquals(0, rs.size()); + assertFalse(rs.hasValue()); + } + + @Test + public void sizeAndHasAnyValueEffectivelyUnboundedEmptyError() { + ReplayProcessor<Object> rs = ReplayProcessor.createUnbounded(); + + rs.onError(new TestException()); + + assertEquals(0, rs.size()); + assertFalse(rs.hasValue()); + } + + @Test + public void sizeAndHasAnyValueUnboundedEmptyCompleted() { + ReplayProcessor<Object> rs = ReplayProcessor.create(); + + rs.onComplete(); + + assertEquals(0, rs.size()); + assertFalse(rs.hasValue()); + } + + @Test + public void sizeAndHasAnyValueEffectivelyUnboundedEmptyCompleted() { + ReplayProcessor<Object> rs = ReplayProcessor.createUnbounded(); + + rs.onComplete(); + + assertEquals(0, rs.size()); + assertFalse(rs.hasValue()); + } + + @Test + public void sizeAndHasAnyValueSizeBounded() { + ReplayProcessor<Object> rs = ReplayProcessor.createWithSize(1); + + assertEquals(0, rs.size()); + assertFalse(rs.hasValue()); + + for (int i = 0; i < 1000; i++) { + rs.onNext(i); + + assertEquals(1, rs.size()); + assertTrue(rs.hasValue()); + } + + rs.onComplete(); + + assertEquals(1, rs.size()); + assertTrue(rs.hasValue()); + } + + @Test + public void sizeAndHasAnyValueTimeBounded() { + TestScheduler ts = new TestScheduler(); + ReplayProcessor<Object> rs = ReplayProcessor.createWithTime(1, TimeUnit.SECONDS, ts); + + assertEquals(0, rs.size()); + assertFalse(rs.hasValue()); + + for (int i = 0; i < 1000; i++) { + rs.onNext(i); + assertEquals(1, rs.size()); + assertTrue(rs.hasValue()); + ts.advanceTimeBy(2, TimeUnit.SECONDS); + assertEquals(0, rs.size()); + assertFalse(rs.hasValue()); + } + + rs.onComplete(); + + assertEquals(0, rs.size()); + assertFalse(rs.hasValue()); + } + + @Test + public void getValues() { + ReplayProcessor<Object> rs = ReplayProcessor.create(); + Object[] expected = new Object[10]; + for (int i = 0; i < expected.length; i++) { + expected[i] = i; + rs.onNext(i); + assertArrayEquals(Arrays.copyOf(expected, i + 1), rs.getValues()); + } + rs.onComplete(); + + assertArrayEquals(expected, rs.getValues()); + + } + + @Test + public void getValuesUnbounded() { + ReplayProcessor<Object> rs = ReplayProcessor.createUnbounded(); + Object[] expected = new Object[10]; + for (int i = 0; i < expected.length; i++) { + expected[i] = i; + rs.onNext(i); + assertArrayEquals(Arrays.copyOf(expected, i + 1), rs.getValues()); + } + rs.onComplete(); + + assertArrayEquals(expected, rs.getValues()); + + } + + @Test + public void backpressureHonored() { + ReplayProcessor<Integer> rs = ReplayProcessor.create(); + rs.onNext(1); + rs.onNext(2); + rs.onNext(3); + rs.onComplete(); + + TestSubscriber<Integer> ts = new TestSubscriber<>(0L); + + rs.subscribe(ts); + + ts.request(1); + ts.assertValue(1); + ts.assertNotComplete(); + ts.assertNoErrors(); + + ts.request(1); + ts.assertValues(1, 2); + ts.assertNotComplete(); + ts.assertNoErrors(); + + ts.request(1); + ts.assertValues(1, 2, 3); + ts.assertComplete(); + ts.assertNoErrors(); + } + + @Test + public void backpressureHonoredSizeBound() { + ReplayProcessor<Integer> rs = ReplayProcessor.createWithSize(100); + rs.onNext(1); + rs.onNext(2); + rs.onNext(3); + rs.onComplete(); + + TestSubscriber<Integer> ts = new TestSubscriber<>(0L); + + rs.subscribe(ts); + + ts.request(1); + ts.assertValue(1); + ts.assertNotComplete(); + ts.assertNoErrors(); + + ts.request(1); + ts.assertValues(1, 2); + ts.assertNotComplete(); + ts.assertNoErrors(); + + ts.request(1); + ts.assertValues(1, 2, 3); + ts.assertComplete(); + ts.assertNoErrors(); + } + + @Test + public void backpressureHonoredTimeBound() { + ReplayProcessor<Integer> rs = ReplayProcessor.createWithTime(1, TimeUnit.DAYS, Schedulers.trampoline()); + rs.onNext(1); + rs.onNext(2); + rs.onNext(3); + rs.onComplete(); + + TestSubscriber<Integer> ts = new TestSubscriber<>(0L); + + rs.subscribe(ts); + + ts.request(1); + ts.assertValue(1); + ts.assertNotComplete(); + ts.assertNoErrors(); + + ts.request(1); + ts.assertValues(1, 2); + ts.assertNotComplete(); + ts.assertNoErrors(); + + ts.request(1); + ts.assertValues(1, 2, 3); + ts.assertComplete(); + ts.assertNoErrors(); + } + + @Test + public void createInvalidCapacity() { + try { + ReplayProcessor.create(-99); + fail("Didn't throw IllegalArgumentException"); + } catch (IllegalArgumentException ex) { + assertEquals("capacityHint > 0 required but it was -99", ex.getMessage()); + } + } + + @Test + public void createWithSizeInvalidCapacity() { + try { + ReplayProcessor.createWithSize(-99); + fail("Didn't throw IllegalArgumentException"); + } catch (IllegalArgumentException ex) { + assertEquals("maxSize > 0 required but it was -99", ex.getMessage()); + } + } + + @Test + public void createWithTimeAndSizeInvalidCapacity() { + try { + ReplayProcessor.createWithTimeAndSize(1, TimeUnit.DAYS, Schedulers.computation(), -99); + fail("Didn't throw IllegalArgumentException"); + } catch (IllegalArgumentException ex) { + assertEquals("maxSize > 0 required but it was -99", ex.getMessage()); + } + } + + @Test + public void hasSubscribers() { + ReplayProcessor<Integer> rp = ReplayProcessor.create(); + + assertFalse(rp.hasSubscribers()); + + TestSubscriber<Integer> ts = rp.test(); + + assertTrue(rp.hasSubscribers()); + + ts.cancel(); + + assertFalse(rp.hasSubscribers()); + } + + @Test + public void peekStateUnbounded() { + ReplayProcessor<Integer> rp = ReplayProcessor.create(); + + rp.onNext(1); + + assertEquals((Integer)1, rp.getValue()); + + assertEquals(1, rp.getValues()[0]); + } + + @Test + public void peekStateTimeAndSize() { + ReplayProcessor<Integer> rp = ReplayProcessor.createWithTimeAndSize(1, TimeUnit.DAYS, Schedulers.computation(), 1); + + rp.onNext(1); + + assertEquals((Integer)1, rp.getValue()); + + assertEquals(1, rp.getValues()[0]); + + rp.onNext(2); + + assertEquals((Integer)2, rp.getValue()); + + assertEquals(2, rp.getValues()[0]); + + assertEquals((Integer)2, rp.getValues(new Integer[0])[0]); + + assertEquals((Integer)2, rp.getValues(new Integer[1])[0]); + + Integer[] a = new Integer[2]; + assertEquals((Integer)2, rp.getValues(a)[0]); + assertNull(a[1]); + } + + @Test + public void peekStateTimeAndSizeValue() { + ReplayProcessor<Integer> rp = ReplayProcessor.createWithTimeAndSize(1, TimeUnit.DAYS, Schedulers.computation(), 1); + + assertNull(rp.getValue()); + + assertEquals(0, rp.getValues().length); + + assertNull(rp.getValues(new Integer[2])[0]); + + rp.onComplete(); + + assertNull(rp.getValue()); + + assertEquals(0, rp.getValues().length); + + assertNull(rp.getValues(new Integer[2])[0]); + + rp = ReplayProcessor.createWithTimeAndSize(1, TimeUnit.DAYS, Schedulers.computation(), 1); + rp.onError(new TestException()); + + assertNull(rp.getValue()); + + assertEquals(0, rp.getValues().length); + + assertNull(rp.getValues(new Integer[2])[0]); + } + + @Test + public void peekStateTimeAndSizeValueExpired() { + TestScheduler scheduler = new TestScheduler(); + ReplayProcessor<Integer> rp = ReplayProcessor.createWithTime(1, TimeUnit.DAYS, scheduler); + + assertNull(rp.getValue()); + assertNull(rp.getValues(new Integer[2])[0]); + + rp.onNext(2); + + assertEquals((Integer)2, rp.getValue()); + assertEquals(2, rp.getValues()[0]); + + scheduler.advanceTimeBy(2, TimeUnit.DAYS); + + assertNull(rp.getValue()); + assertEquals(0, rp.getValues().length); + assertNull(rp.getValues(new Integer[2])[0]); + } + + @Test + public void capacityHint() { + ReplayProcessor<Integer> rp = ReplayProcessor.create(8); + + for (int i = 0; i < 15; i++) { + rp.onNext(i); + } + rp.onComplete(); + + rp.test().assertResult(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14); + } + + @Test + public void subscribeCancelRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final TestSubscriber<Integer> ts = new TestSubscriber<>(); + + final ReplayProcessor<Integer> rp = ReplayProcessor.create(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + rp.subscribe(ts); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + ts.cancel(); + } + }; + + TestHelper.race(r1, r2); + } + } + + @Test + public void subscribeAfterDone() { + ReplayProcessor<Integer> rp = ReplayProcessor.create(); + rp.onComplete(); + + BooleanSubscription bs = new BooleanSubscription(); + + rp.onSubscribe(bs); + + assertTrue(bs.isCancelled()); + } + + @Test + public void subscribeRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final ReplayProcessor<Integer> rp = ReplayProcessor.create(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + rp.test(); + } + }; + + TestHelper.race(r1, r1); + } + } + + @Test + public void cancelUpfront() { + ReplayProcessor<Integer> rp = ReplayProcessor.create(); + rp.test(); + rp.test(); + + TestSubscriber<Integer> ts = rp.test(0L, true); + + assertEquals(2, rp.subscriberCount()); + + ts.assertEmpty(); + } + + @Test + public void cancelRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + + final ReplayProcessor<Integer> rp = ReplayProcessor.create(); + final TestSubscriber<Integer> ts1 = rp.test(); + final TestSubscriber<Integer> ts2 = rp.test(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + ts1.cancel(); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + ts2.cancel(); + } + }; + + TestHelper.race(r1, r2); + + assertFalse(rp.hasSubscribers()); + } + } + + @Test + public void sizeboundReplayError() { + ReplayProcessor<Integer> rp = ReplayProcessor.createWithSize(2); + + rp.onNext(1); + rp.onNext(2); + rp.onNext(3); + rp.onNext(4); + rp.onError(new TestException()); + + rp.test() + .assertFailure(TestException.class, 3, 4); + } + + @Test + public void sizeAndTimeBoundReplayError() { + ReplayProcessor<Integer> rp = ReplayProcessor.createWithTimeAndSize(1, TimeUnit.DAYS, Schedulers.single(), 2); + + rp.onNext(1); + rp.onNext(2); + rp.onNext(3); + rp.onNext(4); + rp.onError(new TestException()); + + rp.test() + .assertFailure(TestException.class, 3, 4); + } + + @Test + public void replayRequestRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + + final ReplayProcessor<Integer> rp = ReplayProcessor.createWithTimeAndSize(1, TimeUnit.DAYS, Schedulers.single(), 2); + final TestSubscriber<Integer> ts = rp.test(0L); + + Runnable r1 = new Runnable() { + @Override + public void run() { + ts.request(1); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + rp.onNext(1); + } + }; + + TestHelper.race(r1, r2); + } + } + + @Test + public void timedSkipOld() { + TestScheduler scheduler = new TestScheduler(); + + ReplayProcessor<Integer> rp = ReplayProcessor.createWithTimeAndSize(1, TimeUnit.SECONDS, scheduler, 2); + + rp.onNext(1); + scheduler.advanceTimeBy(1, TimeUnit.SECONDS); + + rp.test() + .assertEmpty(); + } + + @Test + public void takeSizeAndTime() { + TestScheduler scheduler = new TestScheduler(); + + ReplayProcessor<Integer> rp = ReplayProcessor.createWithTimeAndSize(1, TimeUnit.SECONDS, scheduler, 2); + + rp.onNext(1); + rp.onNext(2); + rp.onNext(3); + + rp + .take(1) + .test() + .assertResult(2); + } + + @Test + public void takeSizeAndTime2() { + TestScheduler scheduler = new TestScheduler(); + + ReplayProcessor<Integer> rp = ReplayProcessor.createWithTimeAndSize(1, TimeUnit.SECONDS, scheduler, 2); + + rp.onNext(1); + rp.onNext(2); + rp.onNext(3); + + TestSubscriber<Integer> ts = new TestSubscriber<Integer>() { + @Override + public void onNext(@NonNull Integer t) { + super.onNext(t); + cancel(); + onComplete(); + } + }; + + rp + .subscribeWith(ts) + .assertResult(2); + } + + @Test + public void takeSize() { + ReplayProcessor<Integer> rp = ReplayProcessor.createWithSize(2); + + rp.onNext(1); + rp.onNext(2); + rp.onNext(3); + + rp + .take(1) + .test() + .assertResult(2); + } + + @Test + public void takeSize2() { + ReplayProcessor<Integer> rp = ReplayProcessor.createWithSize(2); + + rp.onNext(1); + rp.onNext(2); + rp.onNext(3); + + TestSubscriber<Integer> ts = new TestSubscriber<Integer>() { + @Override + public void onNext(@NonNull Integer t) { + super.onNext(t); + cancel(); + onComplete(); + } + }; + + rp + .subscribeWith(ts) + .assertResult(2); + } + + @Test + public void reentrantDrain() { + TestScheduler scheduler = new TestScheduler(); + + final ReplayProcessor<Integer> rp = ReplayProcessor.createWithTimeAndSize(1, TimeUnit.SECONDS, scheduler, 2); + + TestSubscriber<Integer> ts = new TestSubscriber<Integer>() { + @Override + public void onNext(Integer t) { + if (t == 1) { + rp.onNext(2); + } + super.onNext(t); + } + }; + + rp.subscribe(ts); + + rp.onNext(1); + rp.onComplete(); + + ts.assertResult(1, 2); + } + + @Test + public void reentrantDrainBackpressured() { + TestScheduler scheduler = new TestScheduler(); + + final ReplayProcessor<Integer> rp = ReplayProcessor.createWithTimeAndSize(1, TimeUnit.SECONDS, scheduler, 2); + + TestSubscriber<Integer> ts = new TestSubscriber<Integer>(1L) { + @Override + public void onNext(Integer t) { + if (t == 1) { + rp.onNext(2); + } + super.onNext(t); + } + }; + + rp.subscribe(ts); + + rp.onNext(1); + rp.onComplete(); + + ts.request(1); + + ts.assertResult(1, 2); + } + + @Test + public void timedNoOutdatedData() { + TestScheduler scheduler = new TestScheduler(); + + ReplayProcessor<Integer> source = ReplayProcessor.createWithTime(2, TimeUnit.SECONDS, scheduler); + source.onNext(1); + source.onComplete(); + + source.test().assertResult(1); + + source.test().assertResult(1); + + scheduler.advanceTimeBy(3, TimeUnit.SECONDS); + + source.test().assertResult(); + } + + @Test + public void unboundedRequestCompleteRace() { + for (int i = 0; i < TestHelper.RACE_LONG_LOOPS; i++) { + final ReplayProcessor<Integer> source = ReplayProcessor.create(); + + final TestSubscriber<Integer> ts = source.test(0); + + Runnable r1 = new Runnable() { + @Override + public void run() { + source.onComplete(); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + ts.request(1); + } + }; + + TestHelper.race(r1, r2); + + ts.assertResult(); + } + } + + @Test + public void sizeRequestCompleteRace() { + for (int i = 0; i < TestHelper.RACE_LONG_LOOPS; i++) { + final ReplayProcessor<Integer> source = ReplayProcessor.createWithSize(10); + + final TestSubscriber<Integer> ts = source.test(0); + + Runnable r1 = new Runnable() { + @Override + public void run() { + source.onComplete(); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + ts.request(1); + } + }; + + TestHelper.race(r1, r2); + + ts.assertResult(); + } + } + + @Test + public void timedRequestCompleteRace() { + for (int i = 0; i < TestHelper.RACE_LONG_LOOPS; i++) { + final ReplayProcessor<Integer> source = ReplayProcessor.createWithTime(2, TimeUnit.HOURS, Schedulers.single()); + + final TestSubscriber<Integer> ts = source.test(0); + + Runnable r1 = new Runnable() { + @Override + public void run() { + source.onComplete(); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + ts.request(1); + } + }; + + TestHelper.race(r1, r2); + + ts.assertResult(); + } + } + + @Test + public void timeAndSizeRequestCompleteRace() { + for (int i = 0; i < TestHelper.RACE_LONG_LOOPS; i++) { + final ReplayProcessor<Integer> source = ReplayProcessor.createWithTimeAndSize(2, TimeUnit.HOURS, Schedulers.single(), 100); + + final TestSubscriber<Integer> ts = source.test(0); + + Runnable r1 = new Runnable() { + @Override + public void run() { + source.onComplete(); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + ts.request(1); + } + }; + + TestHelper.race(r1, r2); + + ts.assertResult(); + } + } + + @Test + public void unboundedZeroRequestComplete() { + final ReplayProcessor<Integer> source = ReplayProcessor.create(); + + source.onComplete(); + + source.test(0).assertResult(); + } + + @Test + public void unboundedZeroRequestError() { + final ReplayProcessor<Integer> source = ReplayProcessor.create(); + + source.onError(new TestException()); + + source.test(0).assertFailure(TestException.class); + } + + @Test + public void sizeBoundZeroRequestComplete() { + final ReplayProcessor<Integer> source = ReplayProcessor.createWithSize(16); + + source.onComplete(); + + source.test(0).assertResult(); + } + + @Test + public void sizeBoundZeroRequestError() { + final ReplayProcessor<Integer> source = ReplayProcessor.createWithSize(16); + + source.onError(new TestException()); + + source.test(0).assertFailure(TestException.class); + } + + @Test + public void timeBoundZeroRequestComplete() { + final ReplayProcessor<Integer> source = ReplayProcessor.createWithTime(1, TimeUnit.MINUTES, Schedulers.single()); + + source.onComplete(); + + source.test(0).assertResult(); + } + + @Test + public void timeBoundZeroRequestError() { + final ReplayProcessor<Integer> source = ReplayProcessor.createWithTime(1, TimeUnit.MINUTES, Schedulers.single()); + + source.onError(new TestException()); + + source.test(0).assertFailure(TestException.class); + } + + @Test + public void timeAndSizeBoundZeroRequestComplete() { + final ReplayProcessor<Integer> source = ReplayProcessor.createWithTimeAndSize(1, TimeUnit.MINUTES, Schedulers.single(), 16); + + source.onComplete(); + + source.test(0).assertResult(); + } + + @Test + public void timeAndSizeBoundZeroRequestError() { + final ReplayProcessor<Integer> source = ReplayProcessor.createWithTimeAndSize(1, TimeUnit.MINUTES, Schedulers.single(), 16); + + source.onError(new TestException()); + + source.test(0).assertFailure(TestException.class); + } + + TestSubscriber<Integer> take1AndCancel() { + return new TestSubscriber<Integer>(1) { + @Override + public void onNext(Integer t) { + super.onNext(t); + cancel(); + onComplete(); + } + }; + } + + @Test + public void unboundedCancelAfterOne() { + ReplayProcessor<Integer> source = ReplayProcessor.create(); + source.onNext(1); + + source.subscribeWith(take1AndCancel()) + .assertResult(1); + } + + @Test + public void sizeBoundCancelAfterOne() { + ReplayProcessor<Integer> source = ReplayProcessor.createWithSize(16); + source.onNext(1); + + source.subscribeWith(take1AndCancel()) + .assertResult(1); + } + + @Test + public void timeBoundCancelAfterOne() { + ReplayProcessor<Integer> source = ReplayProcessor.createWithTime(1, TimeUnit.MINUTES, Schedulers.single()); + source.onNext(1); + + source.subscribeWith(take1AndCancel()) + .assertResult(1); + } + + @Test + public void timeAndSizeBoundCancelAfterOne() { + ReplayProcessor<Integer> source = ReplayProcessor.createWithTimeAndSize(1, TimeUnit.MINUTES, Schedulers.single(), 16); + source.onNext(1); + + source.subscribeWith(take1AndCancel()) + .assertResult(1); + } + + @Test + public void noHeadRetentionCompleteSize() { + ReplayProcessor<Integer> source = ReplayProcessor.createWithSize(1); + + source.onNext(1); + source.onNext(2); + source.onComplete(); + + SizeBoundReplayBuffer<Integer> buf = (SizeBoundReplayBuffer<Integer>)source.buffer; + + assertNull(buf.head.value); + + Object o = buf.head; + + source.cleanupBuffer(); + + assertSame(o, buf.head); + } + + @Test + public void noHeadRetentionErrorSize() { + ReplayProcessor<Integer> source = ReplayProcessor.createWithSize(1); + + source.onNext(1); + source.onNext(2); + source.onError(new TestException()); + + SizeBoundReplayBuffer<Integer> buf = (SizeBoundReplayBuffer<Integer>)source.buffer; + + assertNull(buf.head.value); + + Object o = buf.head; + + source.cleanupBuffer(); + + assertSame(o, buf.head); + } + + @Test + public void unboundedCleanupBufferNoOp() { + ReplayProcessor<Integer> source = ReplayProcessor.create(1); + + source.onNext(1); + source.onNext(2); + + source.cleanupBuffer(); + + source.test().assertValuesOnly(1, 2); + } + + @Test + public void noHeadRetentionSize() { + ReplayProcessor<Integer> source = ReplayProcessor.createWithSize(1); + + source.onNext(1); + source.onNext(2); + + SizeBoundReplayBuffer<Integer> buf = (SizeBoundReplayBuffer<Integer>)source.buffer; + + assertNotNull(buf.head.value); + + source.cleanupBuffer(); + + assertNull(buf.head.value); + + Object o = buf.head; + + source.cleanupBuffer(); + + assertSame(o, buf.head); + } + + @Test + public void noHeadRetentionCompleteTime() { + ReplayProcessor<Integer> source = ReplayProcessor.createWithTime(1, TimeUnit.MINUTES, Schedulers.computation()); + + source.onNext(1); + source.onNext(2); + source.onComplete(); + + SizeAndTimeBoundReplayBuffer<Integer> buf = (SizeAndTimeBoundReplayBuffer<Integer>)source.buffer; + + assertNull(buf.head.value); + + Object o = buf.head; + + source.cleanupBuffer(); + + assertSame(o, buf.head); + } + + @Test + public void noHeadRetentionErrorTime() { + ReplayProcessor<Integer> source = ReplayProcessor.createWithTime(1, TimeUnit.MINUTES, Schedulers.computation()); + + source.onNext(1); + source.onNext(2); + source.onError(new TestException()); + + SizeAndTimeBoundReplayBuffer<Integer> buf = (SizeAndTimeBoundReplayBuffer<Integer>)source.buffer; + + assertNull(buf.head.value); + + Object o = buf.head; + + source.cleanupBuffer(); + + assertSame(o, buf.head); + } + + @Test + public void noHeadRetentionTime() { + TestScheduler sch = new TestScheduler(); + + ReplayProcessor<Integer> source = ReplayProcessor.createWithTime(1, TimeUnit.MILLISECONDS, sch); + + source.onNext(1); + + sch.advanceTimeBy(2, TimeUnit.MILLISECONDS); + + source.onNext(2); + + SizeAndTimeBoundReplayBuffer<Integer> buf = (SizeAndTimeBoundReplayBuffer<Integer>)source.buffer; + + assertNotNull(buf.head.value); + + source.cleanupBuffer(); + + assertNull(buf.head.value); + + Object o = buf.head; + + source.cleanupBuffer(); + + assertSame(o, buf.head); + } + + @Test + public void invalidRequest() { + TestHelper.assertBadRequestReported(ReplayProcessor.create()); + } + + @Test + public void noBoundedRetentionViaThreadLocal() throws Exception { + final ReplayProcessor<byte[]> rp = ReplayProcessor.createWithSize(1); + + Flowable<byte[]> source = rp.take(1) + .concatMap(new Function<byte[], Publisher<byte[]>>() { + @Override + public Publisher<byte[]> apply(byte[] v) throws Exception { + return rp; + } + }) + .takeLast(1) + ; + + System.out.println("Bounded Replay Leak check: Wait before GC"); + Thread.sleep(1000); + + System.out.println("Bounded Replay Leak check: GC"); + System.gc(); + + Thread.sleep(500); + + final MemoryMXBean memoryMXBean = ManagementFactory.getMemoryMXBean(); + MemoryUsage memHeap = memoryMXBean.getHeapMemoryUsage(); + long initial = memHeap.getUsed(); + + System.out.printf("Bounded Replay Leak check: Starting: %.3f MB%n", initial / 1024.0 / 1024.0); + + final AtomicLong after = new AtomicLong(); + + source.subscribe(new Consumer<byte[]>() { + @Override + public void accept(byte[] v) throws Exception { + System.out.println("Bounded Replay Leak check: Wait before GC 2"); + Thread.sleep(1000); + + System.out.println("Bounded Replay Leak check: GC 2"); + System.gc(); + + Thread.sleep(500); + + after.set(memoryMXBean.getHeapMemoryUsage().getUsed()); + } + }); + + for (int i = 0; i < 200; i++) { + rp.onNext(new byte[1024 * 1024]); + } + rp.onComplete(); + + System.out.printf("Bounded Replay Leak check: After: %.3f MB%n", after.get() / 1024.0 / 1024.0); + + if (initial + 100 * 1024 * 1024 < after.get()) { + Assert.fail("Bounded Replay Leak check: Memory leak detected: " + (initial / 1024.0 / 1024.0) + + " -> " + after.get() / 1024.0 / 1024.0); + } + } + + @Test + public void timeAndSizeNoTerminalTruncationOnTimechange() { + ReplayProcessor<Integer> rp = ReplayProcessor.createWithTimeAndSize(1, TimeUnit.SECONDS, new TimesteppingScheduler(), 1); + + TestSubscriber<Integer> ts = rp.test(); + + rp.onNext(1); + rp.cleanupBuffer(); + rp.onComplete(); + + ts.assertNoErrors() + .assertComplete(); + } + + @Test + public void timeAndSizeNoTerminalTruncationOnTimechange2() { + ReplayProcessor<Integer> rp = ReplayProcessor.createWithTimeAndSize(1, TimeUnit.SECONDS, new TimesteppingScheduler(), 1); + + TestSubscriber<Integer> ts = rp.test(); + + rp.onNext(1); + rp.cleanupBuffer(); + rp.onNext(2); + rp.cleanupBuffer(); + rp.onComplete(); + + ts.assertNoErrors() + .assertComplete(); + } + + @Test + public void timeAndSizeNoTerminalTruncationOnTimechange3() { + ReplayProcessor<Integer> rp = ReplayProcessor.createWithTimeAndSize(1, TimeUnit.SECONDS, new TimesteppingScheduler(), 1); + + TestSubscriber<Integer> ts = rp.test(); + + rp.onNext(1); + rp.onNext(2); + rp.onComplete(); + + ts.assertNoErrors() + .assertComplete(); + } + + @Test + public void timeAndSizeNoTerminalTruncationOnTimechange4() { + ReplayProcessor<Integer> rp = ReplayProcessor.createWithTimeAndSize(1, TimeUnit.SECONDS, new TimesteppingScheduler(), 10); + + TestSubscriber<Integer> ts = rp.test(); + + rp.onNext(1); + rp.onNext(2); + rp.onComplete(); + + ts.assertNoErrors() + .assertComplete(); + } + + @Test + public void timeAndSizeRemoveCorrectNumberOfOld() { + TestScheduler scheduler = new TestScheduler(); + ReplayProcessor<Integer> rp = ReplayProcessor.createWithTimeAndSize(1, TimeUnit.SECONDS, scheduler, 2); + + rp.onNext(1); + rp.onNext(2); + rp.onNext(3); + + scheduler.advanceTimeBy(2, TimeUnit.SECONDS); + + rp.onNext(4); + rp.onNext(5); + + rp.test().assertValuesOnly(4, 5); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/processors/SerializedProcessorTest.java b/src/test/java/io/reactivex/rxjava3/processors/SerializedProcessorTest.java new file mode 100644 index 0000000000..515b8d1430 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/processors/SerializedProcessorTest.java @@ -0,0 +1,691 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.processors; + +import static org.junit.Assert.*; + +import java.util.*; +import java.util.concurrent.TimeUnit; + +import org.junit.Test; + +import io.reactivex.rxjava3.annotations.NonNull; +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.internal.subscriptions.BooleanSubscription; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.subscribers.TestSubscriber; +import io.reactivex.rxjava3.testsupport.*; + +public class SerializedProcessorTest extends RxJavaTest { + + @Test + public void basic() { + SerializedProcessor<String> processor = new SerializedProcessor<>(PublishProcessor.<String>create()); + TestSubscriber<String> ts = new TestSubscriber<>(); + processor.subscribe(ts); + processor.onNext("hello"); + processor.onComplete(); + ts.awaitDone(5, TimeUnit.SECONDS); + ts.assertValue("hello"); + } + + @Test + public void asyncSubjectValueRelay() { + AsyncProcessor<Integer> async = AsyncProcessor.create(); + async.onNext(1); + async.onComplete(); + FlowableProcessor<Integer> serial = async.toSerialized(); + + assertFalse(serial.hasSubscribers()); + assertTrue(serial.hasComplete()); + assertFalse(serial.hasThrowable()); + assertNull(serial.getThrowable()); + assertEquals((Integer)1, async.getValue()); + assertTrue(async.hasValue()); + } + + @Test + public void asyncSubjectValueEmpty() { + AsyncProcessor<Integer> async = AsyncProcessor.create(); + async.onComplete(); + FlowableProcessor<Integer> serial = async.toSerialized(); + + assertFalse(serial.hasSubscribers()); + assertTrue(serial.hasComplete()); + assertFalse(serial.hasThrowable()); + assertNull(serial.getThrowable()); + assertNull(async.getValue()); + assertFalse(async.hasValue()); + } + + @Test + public void asyncSubjectValueError() { + AsyncProcessor<Integer> async = AsyncProcessor.create(); + TestException te = new TestException(); + async.onError(te); + FlowableProcessor<Integer> serial = async.toSerialized(); + + assertFalse(serial.hasSubscribers()); + assertFalse(serial.hasComplete()); + assertTrue(serial.hasThrowable()); + assertSame(te, serial.getThrowable()); + assertNull(async.getValue()); + assertFalse(async.hasValue()); + } + + @Test + public void publishSubjectValueRelay() { + PublishProcessor<Integer> async = PublishProcessor.create(); + async.onNext(1); + async.onComplete(); + FlowableProcessor<Integer> serial = async.toSerialized(); + + assertFalse(serial.hasSubscribers()); + assertTrue(serial.hasComplete()); + assertFalse(serial.hasThrowable()); + assertNull(serial.getThrowable()); + } + + @Test + public void publishSubjectValueEmpty() { + PublishProcessor<Integer> async = PublishProcessor.create(); + async.onComplete(); + FlowableProcessor<Integer> serial = async.toSerialized(); + + assertFalse(serial.hasSubscribers()); + assertTrue(serial.hasComplete()); + assertFalse(serial.hasThrowable()); + assertNull(serial.getThrowable()); + } + + @Test + public void publishSubjectValueError() { + PublishProcessor<Integer> async = PublishProcessor.create(); + TestException te = new TestException(); + async.onError(te); + FlowableProcessor<Integer> serial = async.toSerialized(); + + assertFalse(serial.hasSubscribers()); + assertFalse(serial.hasComplete()); + assertTrue(serial.hasThrowable()); + assertSame(te, serial.getThrowable()); + } + + @Test + public void behaviorSubjectValueRelay() { + BehaviorProcessor<Integer> async = BehaviorProcessor.create(); + async.onNext(1); + async.onComplete(); + FlowableProcessor<Integer> serial = async.toSerialized(); + + assertFalse(serial.hasSubscribers()); + assertTrue(serial.hasComplete()); + assertFalse(serial.hasThrowable()); + assertNull(serial.getThrowable()); + assertNull(async.getValue()); + assertFalse(async.hasValue()); + } + + @Test + public void behaviorSubjectValueRelayIncomplete() { + BehaviorProcessor<Integer> async = BehaviorProcessor.create(); + async.onNext(1); + FlowableProcessor<Integer> serial = async.toSerialized(); + + assertFalse(serial.hasSubscribers()); + assertFalse(serial.hasComplete()); + assertFalse(serial.hasThrowable()); + assertNull(serial.getThrowable()); + assertEquals((Integer)1, async.getValue()); + assertTrue(async.hasValue()); + } + + @Test + public void behaviorSubjectIncompleteEmpty() { + BehaviorProcessor<Integer> async = BehaviorProcessor.create(); + FlowableProcessor<Integer> serial = async.toSerialized(); + + assertFalse(serial.hasSubscribers()); + assertFalse(serial.hasComplete()); + assertFalse(serial.hasThrowable()); + assertNull(serial.getThrowable()); + assertNull(async.getValue()); + assertFalse(async.hasValue()); + } + + @Test + public void behaviorSubjectEmpty() { + BehaviorProcessor<Integer> async = BehaviorProcessor.create(); + async.onComplete(); + FlowableProcessor<Integer> serial = async.toSerialized(); + + assertFalse(serial.hasSubscribers()); + assertTrue(serial.hasComplete()); + assertFalse(serial.hasThrowable()); + assertNull(serial.getThrowable()); + assertNull(async.getValue()); + assertFalse(async.hasValue()); + } + + @Test + public void behaviorSubjectError() { + BehaviorProcessor<Integer> async = BehaviorProcessor.create(); + TestException te = new TestException(); + async.onError(te); + FlowableProcessor<Integer> serial = async.toSerialized(); + + assertFalse(serial.hasSubscribers()); + assertFalse(serial.hasComplete()); + assertTrue(serial.hasThrowable()); + assertSame(te, serial.getThrowable()); + assertNull(async.getValue()); + assertFalse(async.hasValue()); + } + + @Test + public void replaySubjectValueRelay() { + ReplayProcessor<Integer> async = ReplayProcessor.create(); + async.onNext(1); + async.onComplete(); + FlowableProcessor<Integer> serial = async.toSerialized(); + + assertFalse(serial.hasSubscribers()); + assertTrue(serial.hasComplete()); + assertFalse(serial.hasThrowable()); + assertNull(serial.getThrowable()); + assertEquals((Integer)1, async.getValue()); + assertTrue(async.hasValue()); + assertArrayEquals(new Object[] { 1 }, async.getValues()); + assertArrayEquals(new Integer[] { 1 }, async.getValues(new Integer[0])); + assertArrayEquals(new Integer[] { 1 }, async.getValues(new Integer[] { 0 })); + assertArrayEquals(new Integer[] { 1, null }, async.getValues(new Integer[] { 0, 0 })); + } + + @Test + public void replaySubjectValueRelayIncomplete() { + ReplayProcessor<Integer> async = ReplayProcessor.create(); + async.onNext(1); + FlowableProcessor<Integer> serial = async.toSerialized(); + + assertFalse(serial.hasSubscribers()); + assertFalse(serial.hasComplete()); + assertFalse(serial.hasThrowable()); + assertNull(serial.getThrowable()); + assertEquals((Integer)1, async.getValue()); + assertTrue(async.hasValue()); + assertArrayEquals(new Object[] { 1 }, async.getValues()); + assertArrayEquals(new Integer[] { 1 }, async.getValues(new Integer[0])); + assertArrayEquals(new Integer[] { 1 }, async.getValues(new Integer[] { 0 })); + assertArrayEquals(new Integer[] { 1, null }, async.getValues(new Integer[] { 0, 0 })); + } + + @Test + public void replaySubjectValueRelayBounded() { + ReplayProcessor<Integer> async = ReplayProcessor.createWithSize(1); + async.onNext(0); + async.onNext(1); + async.onComplete(); + FlowableProcessor<Integer> serial = async.toSerialized(); + + assertFalse(serial.hasSubscribers()); + assertTrue(serial.hasComplete()); + assertFalse(serial.hasThrowable()); + assertNull(serial.getThrowable()); + assertEquals((Integer)1, async.getValue()); + assertTrue(async.hasValue()); + assertArrayEquals(new Object[] { 1 }, async.getValues()); + assertArrayEquals(new Integer[] { 1 }, async.getValues(new Integer[0])); + assertArrayEquals(new Integer[] { 1 }, async.getValues(new Integer[] { 0 })); + assertArrayEquals(new Integer[] { 1, null }, async.getValues(new Integer[] { 0, 0 })); + } + + @Test + public void replaySubjectValueRelayBoundedIncomplete() { + ReplayProcessor<Integer> async = ReplayProcessor.createWithSize(1); + async.onNext(0); + async.onNext(1); + FlowableProcessor<Integer> serial = async.toSerialized(); + + assertFalse(serial.hasSubscribers()); + assertFalse(serial.hasComplete()); + assertFalse(serial.hasThrowable()); + assertNull(serial.getThrowable()); + assertEquals((Integer)1, async.getValue()); + assertTrue(async.hasValue()); + assertArrayEquals(new Object[] { 1 }, async.getValues()); + assertArrayEquals(new Integer[] { 1 }, async.getValues(new Integer[0])); + assertArrayEquals(new Integer[] { 1 }, async.getValues(new Integer[] { 0 })); + assertArrayEquals(new Integer[] { 1, null }, async.getValues(new Integer[] { 0, 0 })); + } + + @Test + public void replaySubjectValueRelayBoundedEmptyIncomplete() { + ReplayProcessor<Integer> async = ReplayProcessor.createWithSize(1); + FlowableProcessor<Integer> serial = async.toSerialized(); + + assertFalse(serial.hasSubscribers()); + assertFalse(serial.hasComplete()); + assertFalse(serial.hasThrowable()); + assertNull(serial.getThrowable()); + assertNull(async.getValue()); + assertFalse(async.hasValue()); + assertArrayEquals(new Object[] { }, async.getValues()); + assertArrayEquals(new Integer[] { }, async.getValues(new Integer[0])); + assertArrayEquals(new Integer[] { null }, async.getValues(new Integer[] { 0 })); + assertArrayEquals(new Integer[] { null, 0 }, async.getValues(new Integer[] { 0, 0 })); + } + + @Test + public void replaySubjectValueRelayEmptyIncomplete() { + ReplayProcessor<Integer> async = ReplayProcessor.create(); + FlowableProcessor<Integer> serial = async.toSerialized(); + + assertFalse(serial.hasSubscribers()); + assertFalse(serial.hasComplete()); + assertFalse(serial.hasThrowable()); + assertNull(serial.getThrowable()); + assertNull(async.getValue()); + assertFalse(async.hasValue()); + assertArrayEquals(new Object[] { }, async.getValues()); + assertArrayEquals(new Integer[] { }, async.getValues(new Integer[0])); + assertArrayEquals(new Integer[] { null }, async.getValues(new Integer[] { 0 })); + assertArrayEquals(new Integer[] { null, 0 }, async.getValues(new Integer[] { 0, 0 })); + } + + @Test + public void replaySubjectEmpty() { + ReplayProcessor<Integer> async = ReplayProcessor.create(); + async.onComplete(); + FlowableProcessor<Integer> serial = async.toSerialized(); + + assertFalse(serial.hasSubscribers()); + assertTrue(serial.hasComplete()); + assertFalse(serial.hasThrowable()); + assertNull(serial.getThrowable()); + assertNull(async.getValue()); + assertFalse(async.hasValue()); + assertArrayEquals(new Object[] { }, async.getValues()); + assertArrayEquals(new Integer[] { }, async.getValues(new Integer[0])); + assertArrayEquals(new Integer[] { null }, async.getValues(new Integer[] { 0 })); + assertArrayEquals(new Integer[] { null, 0 }, async.getValues(new Integer[] { 0, 0 })); + } + + @Test + public void replaySubjectError() { + ReplayProcessor<Integer> async = ReplayProcessor.create(); + TestException te = new TestException(); + async.onError(te); + FlowableProcessor<Integer> serial = async.toSerialized(); + + assertFalse(serial.hasSubscribers()); + assertFalse(serial.hasComplete()); + assertTrue(serial.hasThrowable()); + assertSame(te, serial.getThrowable()); + assertNull(async.getValue()); + assertFalse(async.hasValue()); + assertArrayEquals(new Object[] { }, async.getValues()); + assertArrayEquals(new Integer[] { }, async.getValues(new Integer[0])); + assertArrayEquals(new Integer[] { null }, async.getValues(new Integer[] { 0 })); + assertArrayEquals(new Integer[] { null, 0 }, async.getValues(new Integer[] { 0, 0 })); + } + + @Test + public void replaySubjectBoundedEmpty() { + ReplayProcessor<Integer> async = ReplayProcessor.createWithSize(1); + async.onComplete(); + FlowableProcessor<Integer> serial = async.toSerialized(); + + assertFalse(serial.hasSubscribers()); + assertTrue(serial.hasComplete()); + assertFalse(serial.hasThrowable()); + assertNull(serial.getThrowable()); + assertNull(async.getValue()); + assertFalse(async.hasValue()); + assertArrayEquals(new Object[] { }, async.getValues()); + assertArrayEquals(new Integer[] { }, async.getValues(new Integer[0])); + assertArrayEquals(new Integer[] { null }, async.getValues(new Integer[] { 0 })); + assertArrayEquals(new Integer[] { null, 0 }, async.getValues(new Integer[] { 0, 0 })); + } + + @Test + public void replaySubjectBoundedError() { + ReplayProcessor<Integer> async = ReplayProcessor.createWithSize(1); + TestException te = new TestException(); + async.onError(te); + FlowableProcessor<Integer> serial = async.toSerialized(); + + assertFalse(serial.hasSubscribers()); + assertFalse(serial.hasComplete()); + assertTrue(serial.hasThrowable()); + assertSame(te, serial.getThrowable()); + assertNull(async.getValue()); + assertFalse(async.hasValue()); + assertArrayEquals(new Object[] { }, async.getValues()); + assertArrayEquals(new Integer[] { }, async.getValues(new Integer[0])); + assertArrayEquals(new Integer[] { null }, async.getValues(new Integer[] { 0 })); + assertArrayEquals(new Integer[] { null, 0 }, async.getValues(new Integer[] { 0, 0 })); + } + + @Test + public void dontWrapSerializedSubjectAgain() { + PublishProcessor<Object> s = PublishProcessor.create(); + FlowableProcessor<Object> s1 = s.toSerialized(); + FlowableProcessor<Object> s2 = s1.toSerialized(); + assertSame(s1, s2); + } + + @Test + public void normal() { + FlowableProcessor<Integer> s = PublishProcessor.<Integer>create().toSerialized(); + + TestSubscriber<Integer> ts = s.test(); + + Flowable.range(1, 10).subscribe(s); + + ts.assertResult(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + + assertFalse(s.hasSubscribers()); + + s.onNext(11); + + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + s.onError(new TestException()); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + s.onComplete(); + + BooleanSubscription bs = new BooleanSubscription(); + s.onSubscribe(bs); + assertTrue(bs.isCancelled()); + } + + @Test + public void onNextOnNextRace() { + Set<Integer> expectedSet = new HashSet<>(Arrays.asList(1, 2)); + + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final FlowableProcessor<Integer> s = PublishProcessor.<Integer>create().toSerialized(); + + TestSubscriberEx<Integer> ts = s.to(TestHelper.<Integer>testConsumer()); + + Runnable r1 = new Runnable() { + @Override + public void run() { + s.onNext(1); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + s.onNext(2); + } + }; + + TestHelper.race(r1, r2); + + ts.assertSubscribed() + .assertNoErrors() + .assertNotComplete() + .assertValueCount(2) + ; + + Set<Integer> actualSet = new HashSet<>(ts.values()); + assertEquals("" + actualSet, expectedSet, actualSet); + } + } + + @Test + public void onNextOnErrorRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final FlowableProcessor<Integer> s = PublishProcessor.<Integer>create().toSerialized(); + + TestSubscriber<Integer> ts = s.test(); + + final TestException ex = new TestException(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + s.onNext(1); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + s.onError(ex); + } + }; + + TestHelper.race(r1, r2); + + ts.assertError(ex).assertNotComplete(); + + if (ts.values().size() != 0) { + ts.assertValue(1); + } + } + } + + @Test + public void onNextOnCompleteRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final FlowableProcessor<Integer> s = PublishProcessor.<Integer>create().toSerialized(); + + TestSubscriber<Integer> ts = s.test(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + s.onNext(1); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + s.onComplete(); + } + }; + + TestHelper.race(r1, r2); + + ts.assertComplete().assertNoErrors(); + + if (ts.values().size() != 0) { + ts.assertValue(1); + } + } + } + + @Test + public void onNextOnSubscribeRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final FlowableProcessor<Integer> s = PublishProcessor.<Integer>create().toSerialized(); + + TestSubscriber<Integer> ts = s.test(); + + final BooleanSubscription bs = new BooleanSubscription(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + s.onNext(1); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + s.onSubscribe(bs); + } + }; + + TestHelper.race(r1, r2); + + ts.assertValue(1).assertNotComplete().assertNoErrors(); + } + } + + @Test + public void onCompleteOnSubscribeRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final FlowableProcessor<Integer> s = PublishProcessor.<Integer>create().toSerialized(); + + TestSubscriber<Integer> ts = s.test(); + + final BooleanSubscription bs = new BooleanSubscription(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + s.onComplete(); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + s.onSubscribe(bs); + } + }; + + TestHelper.race(r1, r2); + + ts.assertResult(); + } + } + + @Test + public void onCompleteOnCompleteRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final FlowableProcessor<Integer> s = PublishProcessor.<Integer>create().toSerialized(); + + TestSubscriber<Integer> ts = s.test(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + s.onComplete(); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + s.onComplete(); + } + }; + + TestHelper.race(r1, r2); + + ts.assertResult(); + } + } + + @Test + public void onErrorOnErrorRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final FlowableProcessor<Integer> s = PublishProcessor.<Integer>create().toSerialized(); + + TestSubscriber<Integer> ts = s.test(); + + final TestException ex = new TestException(); + + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + Runnable r1 = new Runnable() { + @Override + public void run() { + s.onError(ex); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + s.onError(ex); + } + }; + + TestHelper.race(r1, r2); + + ts.assertFailure(TestException.class); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + } + + @Test + public void onSubscribeOnSubscribeRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final FlowableProcessor<Integer> s = PublishProcessor.<Integer>create().toSerialized(); + + TestSubscriber<Integer> ts = s.test(); + + final BooleanSubscription bs1 = new BooleanSubscription(); + final BooleanSubscription bs2 = new BooleanSubscription(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + s.onSubscribe(bs1); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + s.onSubscribe(bs2); + } + }; + + TestHelper.race(r1, r2); + + ts.assertEmpty(); + } + } + + @Test + public void onErrorQueued() { + FlowableProcessor<Integer> sp = PublishProcessor.<Integer>create().toSerialized(); + + TestSubscriber<Integer> ts = new TestSubscriber<Integer>() { + @Override + public void onNext(@NonNull Integer t) { + super.onNext(t); + if (t == 1) { + sp.onNext(2); + sp.onSubscribe(new BooleanSubscription()); + sp.onError(new TestException()); + } + } + }; + + sp.subscribe(ts); + + sp.onNext(1); + + ts.assertFailure(TestException.class, 1); // errors skip ahead + } +} diff --git a/src/test/java/io/reactivex/rxjava3/processors/UnicastProcessorTest.java b/src/test/java/io/reactivex/rxjava3/processors/UnicastProcessorTest.java new file mode 100644 index 0000000000..f65cc46a5e --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/processors/UnicastProcessorTest.java @@ -0,0 +1,479 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.processors; + +import static org.junit.Assert.*; + +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.internal.subscriptions.BooleanSubscription; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.operators.QueueFuseable; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.schedulers.Schedulers; +import io.reactivex.rxjava3.subscribers.TestSubscriber; +import io.reactivex.rxjava3.testsupport.*; + +public class UnicastProcessorTest extends FlowableProcessorTest<Object> { + + @Override + protected FlowableProcessor<Object> create() { + return UnicastProcessor.create(); + } + + @Test + public void fusionLive() { + UnicastProcessor<Integer> ap = UnicastProcessor.create(); + + TestSubscriberEx<Integer> ts = new TestSubscriberEx<Integer>().setInitialFusionMode(QueueFuseable.ANY); + + ap.subscribe(ts); + + ts + .assertFuseable() + .assertFusionMode(QueueFuseable.ASYNC); + + ts.assertNoValues().assertNoErrors().assertNotComplete(); + + ap.onNext(1); + + ts.assertValue(1).assertNoErrors().assertNotComplete(); + + ap.onComplete(); + + ts.assertResult(1); + } + + @Test + public void fusionOfflie() { + UnicastProcessor<Integer> ap = UnicastProcessor.create(); + ap.onNext(1); + ap.onComplete(); + + TestSubscriberEx<Integer> ts = new TestSubscriberEx<Integer>().setInitialFusionMode(QueueFuseable.ANY); + + ap.subscribe(ts); + + ts + .assertFuseable() + .assertFusionMode(QueueFuseable.ASYNC) + .assertResult(1); + } + + @Test + public void failFast() { + UnicastProcessor<Integer> ap = UnicastProcessor.create(false); + ap.onNext(1); + ap.onError(new RuntimeException()); + + TestSubscriber<Integer> ts = TestSubscriber.create(); + + ap.subscribe(ts); + + ts + .assertValueCount(0) + .assertError(RuntimeException.class); + } + + @Test + public void failFastFusionOffline() { + UnicastProcessor<Integer> ap = UnicastProcessor.create(false); + ap.onNext(1); + ap.onError(new RuntimeException()); + + TestSubscriberEx<Integer> ts = new TestSubscriberEx<Integer>().setInitialFusionMode(QueueFuseable.ANY); + + ap.subscribe(ts); + ts + .assertValueCount(0) + .assertError(RuntimeException.class); + } + + @Test + public void threeArgsFactory() { + Runnable noop = new Runnable() { + @Override + public void run() { + } + }; + UnicastProcessor<Integer> ap = UnicastProcessor.create(16, noop, false); + ap.onNext(1); + ap.onError(new RuntimeException()); + + TestSubscriber<Integer> ts = TestSubscriber.create(); + + ap.subscribe(ts); + ts + .assertValueCount(0) + .assertError(RuntimeException.class); + } + + @Test + public void onTerminateCalledWhenOnError() { + final AtomicBoolean didRunOnTerminate = new AtomicBoolean(); + + UnicastProcessor<Integer> up = UnicastProcessor.create(Observable.bufferSize(), new Runnable() { + @Override public void run() { + didRunOnTerminate.set(true); + } + }); + + assertFalse(didRunOnTerminate.get()); + up.onError(new RuntimeException("some error")); + assertTrue(didRunOnTerminate.get()); + } + + @Test + public void onTerminateCalledWhenOnComplete() { + final AtomicBoolean didRunOnTerminate = new AtomicBoolean(); + + UnicastProcessor<Integer> up = UnicastProcessor.create(Observable.bufferSize(), new Runnable() { + @Override public void run() { + didRunOnTerminate.set(true); + } + }); + + assertFalse(didRunOnTerminate.get()); + up.onComplete(); + assertTrue(didRunOnTerminate.get()); + } + + @Test + public void onTerminateCalledWhenCanceled() { + final AtomicBoolean didRunOnTerminate = new AtomicBoolean(); + + UnicastProcessor<Integer> up = UnicastProcessor.create(Observable.bufferSize(), new Runnable() { + @Override public void run() { + didRunOnTerminate.set(true); + } + }); + + final Disposable subscribe = up.subscribe(); + + assertFalse(didRunOnTerminate.get()); + subscribe.dispose(); + assertTrue(didRunOnTerminate.get()); + } + + @Test(expected = NullPointerException.class) + public void nullOnTerminate() { + UnicastProcessor.create(5, null); + } + + @Test(expected = IllegalArgumentException.class) + public void negativeCapacityHint() { + UnicastProcessor.create(-1); + } + + @Test(expected = IllegalArgumentException.class) + public void zeroCapacityHint() { + UnicastProcessor.create(0); + } + + @Test + public void completeCancelRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final int[] calls = { 0 }; + final UnicastProcessor<Object> up = UnicastProcessor.create(100, new Runnable() { + @Override + public void run() { + calls[0]++; + } + }); + + final TestSubscriber<Object> ts = up.test(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + ts.cancel(); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + up.onComplete(); + } + }; + + TestHelper.race(r1, r2); + + assertEquals(1, calls[0]); + } + } + + @Test + public void afterDone() { + UnicastProcessor<Object> p = UnicastProcessor.create(); + p.onComplete(); + + BooleanSubscription bs = new BooleanSubscription(); + p.onSubscribe(bs); + + p.onNext(1); + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + p.onError(new TestException()); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + p.onComplete(); + + p.test().assertResult(); + + assertNull(p.getThrowable()); + assertTrue(p.hasComplete()); + assertFalse(p.hasThrowable()); + } + + @Test + public void onErrorStatePeeking() { + UnicastProcessor<Object> p = UnicastProcessor.create(); + + assertFalse(p.hasComplete()); + assertFalse(p.hasThrowable()); + assertNull(p.getThrowable()); + + TestException ex = new TestException(); + p.onError(ex); + + assertFalse(p.hasComplete()); + assertTrue(p.hasThrowable()); + assertSame(ex, p.getThrowable()); + } + + @Test + public void rejectSyncFusion() { + UnicastProcessor<Object> p = UnicastProcessor.create(); + + TestSubscriberEx<Object> ts = new TestSubscriberEx<>().setInitialFusionMode(QueueFuseable.SYNC); + + p.subscribe(ts); + + ts.assertFusionMode(QueueFuseable.NONE); + } + + @Test + public void cancelOnArrival() { + UnicastProcessor.create() + .test(0L, true) + .assertEmpty(); + } + + @Test + public void multiSubscriber() { + UnicastProcessor<Object> p = UnicastProcessor.create(); + + TestSubscriber<Object> ts = p.test(); + + p.test() + .assertFailure(IllegalStateException.class); + + p.onNext(1); + p.onComplete(); + + ts.assertResult(1); + } + + @Test + public void fusedDrainCancel() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final UnicastProcessor<Object> p = UnicastProcessor.create(); + + final TestSubscriberEx<Object> ts = new TestSubscriberEx<>().setInitialFusionMode(QueueFuseable.ANY); + + p.subscribe(ts); + + Runnable r1 = new Runnable() { + @Override + public void run() { + p.onNext(1); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + ts.cancel(); + } + }; + + TestHelper.race(r1, r2); + } + } + + @Test + public void subscribeRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final UnicastProcessor<Integer> up = UnicastProcessor.create(); + + final TestSubscriberEx<Integer> ts1 = new TestSubscriberEx<>(); + final TestSubscriberEx<Integer> ts2 = new TestSubscriberEx<>(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + up.subscribe(ts1); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + up.subscribe(ts2); + } + }; + + TestHelper.race(r1, r2); + + if (ts1.errors().size() == 0) { + ts2.assertFailure(IllegalStateException.class); + } else + if (ts2.errors().size() == 0) { + ts1.assertFailure(IllegalStateException.class); + } else { + fail("Neither TestObserver failed"); + } + } + } + + @Test + public void hasObservers() { + UnicastProcessor<Integer> up = UnicastProcessor.create(); + + assertFalse(up.hasSubscribers()); + + TestSubscriber<Integer> ts = up.test(); + + assertTrue(up.hasSubscribers()); + + ts.cancel(); + + assertFalse(up.hasSubscribers()); + } + + @Test + public void drainFusedFailFast() { + UnicastProcessor<Integer> up = UnicastProcessor.create(false); + + TestSubscriberEx<Integer> ts = up.to(TestHelper.<Integer>testSubscriber(1, QueueFuseable.ANY, false)); + + up.done = true; + up.drainFused(ts); + + ts.assertResult(); + } + + @Test + public void drainFusedFailFastEmpty() { + UnicastProcessor<Integer> up = UnicastProcessor.create(false); + + TestSubscriberEx<Integer> ts = up.to(TestHelper.<Integer>testSubscriber(1, QueueFuseable.ANY, false)); + + up.drainFused(ts); + + ts.assertEmpty(); + } + + @Test + public void checkTerminatedFailFastEmpty() { + UnicastProcessor<Integer> up = UnicastProcessor.create(false); + + TestSubscriberEx<Integer> ts = up.to(TestHelper.<Integer>testSubscriber(1, QueueFuseable.ANY, false)); + + up.checkTerminated(true, true, false, ts, up.queue); + + ts.assertEmpty(); + } + + @Test + public void alreadyCancelled() { + UnicastProcessor<Integer> up = UnicastProcessor.create(false); + + up.test().cancel(); + + BooleanSubscription bs = new BooleanSubscription(); + up.onSubscribe(bs); + + assertTrue(bs.isCancelled()); + + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + up.onError(new TestException()); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void unicastSubscriptionBadRequest() { + UnicastProcessor<Integer> up = UnicastProcessor.create(false); + + UnicastProcessor<Integer>.UnicastQueueSubscription usc = (UnicastProcessor<Integer>.UnicastQueueSubscription)up.wip; + + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + usc.request(-1); + TestHelper.assertError(errors, 0, IllegalArgumentException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void fusedNoConcurrentCleanDueToCancel() { + for (int j = 0; j < TestHelper.RACE_LONG_LOOPS; j++) { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + final UnicastProcessor<Integer> up = UnicastProcessor.create(); + + TestObserver<Integer> to = up + .observeOn(Schedulers.io()) + .map(Functions.<Integer>identity()) + .observeOn(Schedulers.single()) + .firstOrError() + .test(); + + for (int i = 0; up.hasSubscribers(); i++) { + up.onNext(i); + } + + to + .awaitDone(5, TimeUnit.SECONDS) + ; + + if (!errors.isEmpty()) { + throw new CompositeException(errors); + } + + to.assertResult(0); + } finally { + RxJavaPlugins.reset(); + } + } + } +} diff --git a/src/test/java/io/reactivex/rxjava3/schedulers/AbstractSchedulerConcurrencyTests.java b/src/test/java/io/reactivex/rxjava3/schedulers/AbstractSchedulerConcurrencyTests.java new file mode 100644 index 0000000000..ae7d1b24b0 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/schedulers/AbstractSchedulerConcurrencyTests.java @@ -0,0 +1,426 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.schedulers; + +import static org.junit.Assert.*; + +import java.util.concurrent.*; +import java.util.concurrent.atomic.*; + +import org.junit.Test; +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.core.Scheduler.Worker; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.subscribers.*; + +/** + * Base tests for schedulers that involve threads (concurrency). + * + * These can only run on Schedulers that launch threads since they expect async/concurrent behavior. + * + * The Current/Immediate schedulers will not work with these tests. + */ +public abstract class AbstractSchedulerConcurrencyTests extends AbstractSchedulerTests { + + /** + * Make sure canceling through {@code subscribeOn} works. + * Bug report: https://github.com/ReactiveX/RxJava/issues/431 + * @throws InterruptedException if the test is interrupted + */ + @Test + public final void unSubscribeForScheduler() throws InterruptedException { + final AtomicInteger countReceived = new AtomicInteger(); + final AtomicInteger countGenerated = new AtomicInteger(); + final CountDownLatch latch = new CountDownLatch(1); + + Flowable.interval(50, TimeUnit.MILLISECONDS) + .map(new Function<Long, Long>() { + @Override + public Long apply(Long aLong) { + countGenerated.incrementAndGet(); + return aLong; + } + }) + .subscribeOn(getScheduler()) + .observeOn(getScheduler()) + .subscribe(new DefaultSubscriber<Long>() { + @Override + public void onComplete() { + System.out.println("--- completed"); + } + + @Override + public void onError(Throwable e) { + System.out.println("--- onError"); + } + + @Override + public void onNext(Long args) { + if (countReceived.incrementAndGet() == 2) { + cancel(); + latch.countDown(); + } + System.out.println("==> Received " + args); + } + }); + + latch.await(1000, TimeUnit.MILLISECONDS); + + System.out.println("----------- it thinks it is finished ------------------ "); + + int timeout = 10; + + while (timeout-- > 0 && countGenerated.get() != 2) { + Thread.sleep(100); + } + + assertEquals(2, countGenerated.get()); + } + + @Test + public void unsubscribeRecursiveScheduleFromOutside() throws InterruptedException { + final CountDownLatch latch = new CountDownLatch(1); + final CountDownLatch unsubscribeLatch = new CountDownLatch(1); + final AtomicInteger counter = new AtomicInteger(); + final Worker inner = getScheduler().createWorker(); + try { + inner.schedule(new Runnable() { + + @Override + public void run() { + inner.schedule(new Runnable() { + + int i; + + @Override + public void run() { + System.out.println("Run: " + i++); + if (i == 10) { + latch.countDown(); + try { + // wait for unsubscribe to finish so we are not racing it + unsubscribeLatch.await(); + } catch (InterruptedException e) { + // we expect the countDown if unsubscribe is not working + // or to be interrupted if unsubscribe is successful since + // the unsubscribe will interrupt it as it is calling Future.cancel(true) + // so we will ignore the stacktrace + } + } + + counter.incrementAndGet(); + inner.schedule(this); + } + }); + } + + }); + + latch.await(); + inner.dispose(); + unsubscribeLatch.countDown(); + Thread.sleep(200); // let time pass to see if the scheduler is still doing work + assertEquals(10, counter.get()); + } finally { + inner.dispose(); + } + } + + @Test + public void unsubscribeRecursiveScheduleFromInside() throws InterruptedException { + final CountDownLatch unsubscribeLatch = new CountDownLatch(1); + final AtomicInteger counter = new AtomicInteger(); + final Worker inner = getScheduler().createWorker(); + try { + inner.schedule(new Runnable() { + + @Override + public void run() { + inner.schedule(new Runnable() { + + int i; + + @Override + public void run() { + System.out.println("Run: " + i++); + if (i == 10) { + inner.dispose(); + } + + counter.incrementAndGet(); + inner.schedule(this); + } + }); + } + + }); + + unsubscribeLatch.countDown(); + Thread.sleep(200); // let time pass to see if the scheduler is still doing work + assertEquals(10, counter.get()); + } finally { + inner.dispose(); + } + } + + @Test + public void unsubscribeRecursiveScheduleWithDelay() throws InterruptedException { + final CountDownLatch latch = new CountDownLatch(1); + final CountDownLatch unsubscribeLatch = new CountDownLatch(1); + final AtomicInteger counter = new AtomicInteger(); + final Worker inner = getScheduler().createWorker(); + + try { + inner.schedule(new Runnable() { + + @Override + public void run() { + inner.schedule(new Runnable() { + + long i = 1L; + + @Override + public void run() { + if (i++ == 10) { + latch.countDown(); + try { + // wait for unsubscribe to finish so we are not racing it + unsubscribeLatch.await(); + } catch (InterruptedException e) { + // we expect the countDown if unsubscribe is not working + // or to be interrupted if unsubscribe is successful since + // the unsubscribe will interrupt it as it is calling Future.cancel(true) + // so we will ignore the stacktrace + } + } + + counter.incrementAndGet(); + inner.schedule(this, 10, TimeUnit.MILLISECONDS); + } + }, 10, TimeUnit.MILLISECONDS); + } + }); + + latch.await(); + inner.dispose(); + unsubscribeLatch.countDown(); + Thread.sleep(200); // let time pass to see if the scheduler is still doing work + assertEquals(10, counter.get()); + } finally { + inner.dispose(); + } + } + + @Test + public void recursionFromOuterActionAndUnsubscribeInside() throws InterruptedException { + final CountDownLatch latch = new CountDownLatch(1); + final Worker inner = getScheduler().createWorker(); + try { + inner.schedule(new Runnable() { + + int i; + + @Override + public void run() { + i++; + if (i % 100000 == 0) { + System.out.println(i + " Total Memory: " + Runtime.getRuntime().totalMemory() + " Free: " + Runtime.getRuntime().freeMemory()); + } + if (i < 1000000L) { + inner.schedule(this); + } else { + latch.countDown(); + } + } + }); + + latch.await(); + } finally { + inner.dispose(); + } + } + + @Test + public void recursion() throws InterruptedException { + final CountDownLatch latch = new CountDownLatch(1); + final Worker inner = getScheduler().createWorker(); + try { + inner.schedule(new Runnable() { + + private long i; + + @Override + public void run() { + i++; + if (i % 100000 == 0) { + System.out.println(i + " Total Memory: " + Runtime.getRuntime().totalMemory() + " Free: " + Runtime.getRuntime().freeMemory()); + } + if (i < 1000000L) { + inner.schedule(this); + } else { + latch.countDown(); + } + } + }); + + latch.await(); + } finally { + inner.dispose(); + } + } + + @Test + public void recursionAndOuterUnsubscribe() throws InterruptedException { + // use latches instead of Thread.sleep + final CountDownLatch latch = new CountDownLatch(10); + final CountDownLatch completionLatch = new CountDownLatch(1); + final Worker inner = getScheduler().createWorker(); + try { + Flowable<Integer> obs = Flowable.unsafeCreate(new Publisher<Integer>() { + @Override + public void subscribe(final Subscriber<? super Integer> subscriber) { + inner.schedule(new Runnable() { + @Override + public void run() { + subscriber.onNext(42); + latch.countDown(); + + // this will recursively schedule this task for execution again + inner.schedule(this); + } + }); + + subscriber.onSubscribe(new Subscription() { + + @Override + public void cancel() { + inner.dispose(); + subscriber.onComplete(); + completionLatch.countDown(); + } + + @Override + public void request(long n) { + + } + }); + + } + }); + + final AtomicInteger count = new AtomicInteger(); + final AtomicBoolean completed = new AtomicBoolean(false); + ResourceSubscriber<Integer> s = new ResourceSubscriber<Integer>() { + @Override + public void onComplete() { + System.out.println("Completed"); + completed.set(true); + } + + @Override + public void onError(Throwable e) { + System.out.println("Error"); + } + + @Override + public void onNext(Integer args) { + count.incrementAndGet(); + System.out.println(args); + } + }; + obs.subscribe(s); + + if (!latch.await(5000, TimeUnit.MILLISECONDS)) { + fail("Timed out waiting on onNext latch"); + } + + // now unsubscribe and ensure it stops the recursive loop + s.dispose(); + System.out.println("unsubscribe"); + + if (!completionLatch.await(5000, TimeUnit.MILLISECONDS)) { + fail("Timed out waiting on completion latch"); + } + + // the count can be 10 or higher due to thread scheduling of the unsubscribe vs the scheduler looping to emit the count + assertTrue(count.get() >= 10); + assertTrue(completed.get()); + } finally { + inner.dispose(); + } + } + + @Test + public final void subscribeWithScheduler() throws InterruptedException { + final Scheduler scheduler = getScheduler(); + + final AtomicInteger count = new AtomicInteger(); + + Flowable<Integer> f1 = Flowable.<Integer> just(1, 2, 3, 4, 5); + + f1.subscribe(new Consumer<Integer>() { + + @Override + public void accept(Integer t) { + System.out.println("Thread: " + Thread.currentThread().getName()); + System.out.println("t: " + t); + count.incrementAndGet(); + } + }); + + // the above should be blocking so we should see a count of 5 + assertEquals(5, count.get()); + + count.set(0); + + // now we'll subscribe with a scheduler and it should be async + + final String currentThreadName = Thread.currentThread().getName(); + + // latches for deterministically controlling the test below across threads + final CountDownLatch latch = new CountDownLatch(5); + final CountDownLatch first = new CountDownLatch(1); + + f1.subscribeOn(scheduler).subscribe(new Consumer<Integer>() { + + @Override + public void accept(Integer t) { + try { + // we block the first one so we can assert this executes asynchronously with a count + first.await(1000, TimeUnit.SECONDS); + } catch (InterruptedException e) { + throw new RuntimeException("The latch should have released if we are async.", e); + } + + assertNotEquals(Thread.currentThread().getName(), currentThreadName); + System.out.println("Thread: " + Thread.currentThread().getName()); + System.out.println("t: " + t); + count.incrementAndGet(); + latch.countDown(); + } + }); + + // assert we are async + assertEquals(0, count.get()); + // release the latch so it can go forward + first.countDown(); + + // wait for all 5 responses + latch.await(); + assertEquals(5, count.get()); + } + +} diff --git a/src/test/java/io/reactivex/rxjava3/schedulers/AbstractSchedulerTests.java b/src/test/java/io/reactivex/rxjava3/schedulers/AbstractSchedulerTests.java new file mode 100644 index 0000000000..2419fe557c --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/schedulers/AbstractSchedulerTests.java @@ -0,0 +1,822 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.schedulers; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +import java.util.*; +import java.util.concurrent.*; +import java.util.concurrent.atomic.*; + +import org.junit.Test; +import org.mockito.InOrder; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.core.Scheduler.Worker; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.disposables.SequentialDisposable; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.internal.schedulers.TrampolineScheduler; +import io.reactivex.rxjava3.internal.subscriptions.*; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.subscribers.DefaultSubscriber; + +/** + * Base tests for all schedulers including Immediate/Current. + */ +public abstract class AbstractSchedulerTests extends RxJavaTest { + + /** + * The scheduler to test. + * + * @return the Scheduler instance + */ + protected abstract Scheduler getScheduler(); + + @Test + public void nestedActions() throws InterruptedException { + Scheduler scheduler = getScheduler(); + final Scheduler.Worker inner = scheduler.createWorker(); + try { + final CountDownLatch latch = new CountDownLatch(1); + + final Runnable firstStepStart = mock(Runnable.class); + final Runnable firstStepEnd = mock(Runnable.class); + + final Runnable secondStepStart = mock(Runnable.class); + final Runnable secondStepEnd = mock(Runnable.class); + + final Runnable thirdStepStart = mock(Runnable.class); + final Runnable thirdStepEnd = mock(Runnable.class); + + final Runnable firstAction = new Runnable() { + @Override + public void run() { + firstStepStart.run(); + firstStepEnd.run(); + latch.countDown(); + } + }; + final Runnable secondAction = new Runnable() { + @Override + public void run() { + secondStepStart.run(); + inner.schedule(firstAction); + secondStepEnd.run(); + + } + }; + final Runnable thirdAction = new Runnable() { + @Override + public void run() { + thirdStepStart.run(); + inner.schedule(secondAction); + thirdStepEnd.run(); + } + }; + + InOrder inOrder = inOrder(firstStepStart, firstStepEnd, secondStepStart, secondStepEnd, thirdStepStart, thirdStepEnd); + + inner.schedule(thirdAction); + + latch.await(); + + inOrder.verify(thirdStepStart, times(1)).run(); + inOrder.verify(thirdStepEnd, times(1)).run(); + inOrder.verify(secondStepStart, times(1)).run(); + inOrder.verify(secondStepEnd, times(1)).run(); + inOrder.verify(firstStepStart, times(1)).run(); + inOrder.verify(firstStepEnd, times(1)).run(); + } finally { + inner.dispose(); + } + } + + @Test + public final void nestedScheduling() { + + Flowable<Integer> ids = Flowable.fromIterable(Arrays.asList(1, 2)).subscribeOn(getScheduler()); + + Flowable<String> m = ids.flatMap(new Function<Integer, Flowable<String>>() { + + @Override + public Flowable<String> apply(Integer id) { + return Flowable.fromIterable(Arrays.asList("a-" + id, "b-" + id)).subscribeOn(getScheduler()) + .map(new Function<String, String>() { + + @Override + public String apply(String s) { + return "names=>" + s; + } + }); + } + + }); + + List<String> strings = m.toList().blockingGet(); + + assertEquals(4, strings.size()); + // because flatMap does a merge there is no guarantee of order + assertTrue(strings.contains("names=>a-1")); + assertTrue(strings.contains("names=>a-2")); + assertTrue(strings.contains("names=>b-1")); + assertTrue(strings.contains("names=>b-2")); + } + + /** + * The order of execution is nondeterministic. + * + * @throws InterruptedException if the await is interrupted + */ + @SuppressWarnings("rawtypes") + @Test + public final void sequenceOfActions() throws InterruptedException { + final Scheduler scheduler = getScheduler(); + final Scheduler.Worker inner = scheduler.createWorker(); + try { + final CountDownLatch latch = new CountDownLatch(2); + final Runnable first = mock(Runnable.class); + final Runnable second = mock(Runnable.class); + + // make it wait until both the first and second are called + doAnswer(new Answer() { + + @Override + public Object answer(InvocationOnMock invocation) throws Throwable { + try { + return invocation.getMock(); + } finally { + latch.countDown(); + } + } + }).when(first).run(); + doAnswer(new Answer() { + + @Override + public Object answer(InvocationOnMock invocation) throws Throwable { + try { + return invocation.getMock(); + } finally { + latch.countDown(); + } + } + }).when(second).run(); + + inner.schedule(first); + inner.schedule(second); + + latch.await(); + + verify(first, times(1)).run(); + verify(second, times(1)).run(); + } finally { + inner.dispose(); + } + } + + @Test + public void sequenceOfDelayedActions() throws InterruptedException { + Scheduler scheduler = getScheduler(); + final Scheduler.Worker inner = scheduler.createWorker(); + + try { + final CountDownLatch latch = new CountDownLatch(1); + final Runnable first = mock(Runnable.class); + final Runnable second = mock(Runnable.class); + + inner.schedule(new Runnable() { + @Override + public void run() { + inner.schedule(first, 30, TimeUnit.MILLISECONDS); + inner.schedule(second, 10, TimeUnit.MILLISECONDS); + inner.schedule(new Runnable() { + + @Override + public void run() { + latch.countDown(); + } + }, 40, TimeUnit.MILLISECONDS); + } + }); + + latch.await(); + InOrder inOrder = inOrder(first, second); + + inOrder.verify(second, times(1)).run(); + inOrder.verify(first, times(1)).run(); + } finally { + inner.dispose(); + } + } + + @Test + public void mixOfDelayedAndNonDelayedActions() throws InterruptedException { + Scheduler scheduler = getScheduler(); + final Scheduler.Worker inner = scheduler.createWorker(); + + try { + final CountDownLatch latch = new CountDownLatch(1); + final Runnable first = mock(Runnable.class); + final Runnable second = mock(Runnable.class); + final Runnable third = mock(Runnable.class); + final Runnable fourth = mock(Runnable.class); + + inner.schedule(new Runnable() { + @Override + public void run() { + inner.schedule(first); + inner.schedule(second, 300, TimeUnit.MILLISECONDS); + inner.schedule(third, 100, TimeUnit.MILLISECONDS); + inner.schedule(fourth); + inner.schedule(new Runnable() { + + @Override + public void run() { + latch.countDown(); + } + }, 400, TimeUnit.MILLISECONDS); + } + }); + + latch.await(); + InOrder inOrder = inOrder(first, second, third, fourth); + + inOrder.verify(first, times(1)).run(); + inOrder.verify(fourth, times(1)).run(); + inOrder.verify(third, times(1)).run(); + inOrder.verify(second, times(1)).run(); + } finally { + inner.dispose(); + } + } + + @Test + public final void recursiveExecution() throws InterruptedException { + final Scheduler scheduler = getScheduler(); + final Scheduler.Worker inner = scheduler.createWorker(); + + try { + + final AtomicInteger i = new AtomicInteger(); + final CountDownLatch latch = new CountDownLatch(1); + inner.schedule(new Runnable() { + + @Override + public void run() { + if (i.incrementAndGet() < 100) { + inner.schedule(this); + } else { + latch.countDown(); + } + } + }); + + latch.await(); + assertEquals(100, i.get()); + } finally { + inner.dispose(); + } + } + + @Test + public final void recursiveExecutionWithDelayTime() throws InterruptedException { + Scheduler scheduler = getScheduler(); + final Scheduler.Worker inner = scheduler.createWorker(); + + try { + final AtomicInteger i = new AtomicInteger(); + final CountDownLatch latch = new CountDownLatch(1); + + inner.schedule(new Runnable() { + + int state; + + @Override + public void run() { + i.set(state); + if (state++ < 100) { + inner.schedule(this, 1, TimeUnit.MILLISECONDS); + } else { + latch.countDown(); + } + } + + }); + + latch.await(); + assertEquals(100, i.get()); + } finally { + inner.dispose(); + } + } + + @Test + public final void recursiveSchedulerInObservable() { + Flowable<Integer> obs = Flowable.unsafeCreate(new Publisher<Integer>() { + @Override + public void subscribe(final Subscriber<? super Integer> subscriber) { + final Scheduler.Worker inner = getScheduler().createWorker(); + + AsyncSubscription as = new AsyncSubscription(); + subscriber.onSubscribe(as); + as.setResource(inner); + + inner.schedule(new Runnable() { + int i; + + @Override + public void run() { + if (i > 42) { + try { + subscriber.onComplete(); + } finally { + inner.dispose(); + } + return; + } + + subscriber.onNext(i++); + + inner.schedule(this); + } + }); + } + }); + + final AtomicInteger lastValue = new AtomicInteger(); + obs.blockingForEach(new Consumer<Integer>() { + + @Override + public void accept(Integer v) { + System.out.println("Value: " + v); + lastValue.set(v); + } + }); + + assertEquals(42, lastValue.get()); + } + + @Test + public final void concurrentOnNextFailsValidation() throws InterruptedException { + final int count = 10; + final CountDownLatch latch = new CountDownLatch(count); + Flowable<String> f = Flowable.unsafeCreate(new Publisher<String>() { + + @Override + public void subscribe(final Subscriber<? super String> subscriber) { + subscriber.onSubscribe(new BooleanSubscription()); + for (int i = 0; i < count; i++) { + final int v = i; + new Thread(new Runnable() { + + @Override + public void run() { + subscriber.onNext("v: " + v); + + latch.countDown(); + } + }).start(); + } + } + }); + + ConcurrentObserverValidator<String> observer = new ConcurrentObserverValidator<>(); + // this should call onNext concurrently + f.subscribe(observer); + + if (!observer.completed.await(3000, TimeUnit.MILLISECONDS)) { + fail("timed out"); + } + + if (observer.error.get() == null) { + fail("We expected error messages due to concurrency"); + } + } + + @Test + public final void observeOn() throws InterruptedException { + final Scheduler scheduler = getScheduler(); + + Flowable<String> f = Flowable.fromArray("one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten"); + + ConcurrentObserverValidator<String> observer = new ConcurrentObserverValidator<>(); + + f.observeOn(scheduler).subscribe(observer); + + if (!observer.completed.await(3000, TimeUnit.MILLISECONDS)) { + fail("timed out"); + } + + if (observer.error.get() != null) { + observer.error.get().printStackTrace(); + fail("Error: " + observer.error.get().getMessage()); + } + } + + @Test + public final void subscribeOnNestedConcurrency() throws InterruptedException { + final Scheduler scheduler = getScheduler(); + + Flowable<String> f = Flowable.fromArray("one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten") + .flatMap(new Function<String, Flowable<String>>() { + + @Override + public Flowable<String> apply(final String v) { + return Flowable.unsafeCreate(new Publisher<String>() { + + @Override + public void subscribe(Subscriber<? super String> subscriber) { + subscriber.onSubscribe(new BooleanSubscription()); + subscriber.onNext("value_after_map-" + v); + subscriber.onComplete(); + } + }).subscribeOn(scheduler); + } + }); + + ConcurrentObserverValidator<String> observer = new ConcurrentObserverValidator<>(); + + f.subscribe(observer); + + if (!observer.completed.await(3000, TimeUnit.MILLISECONDS)) { + fail("timed out"); + } + + if (observer.error.get() != null) { + observer.error.get().printStackTrace(); + fail("Error: " + observer.error.get().getMessage()); + } + } + + /** + * Used to determine if onNext is being invoked concurrently. + * + * @param <T> the element type + */ + private static class ConcurrentObserverValidator<T> extends DefaultSubscriber<T> { + + final AtomicInteger concurrentCounter = new AtomicInteger(); + final AtomicReference<Throwable> error = new AtomicReference<>(); + final CountDownLatch completed = new CountDownLatch(1); + + @Override + public void onComplete() { + completed.countDown(); + } + + @Override + public void onError(Throwable e) { + error.set(e); + completed.countDown(); + } + + @Override + public void onNext(T args) { + int count = concurrentCounter.incrementAndGet(); + System.out.println("ConcurrentObserverValidator.onNext: " + args); + if (count > 1) { + onError(new RuntimeException("we should not have concurrent execution of onNext")); + } + try { + try { + // take some time so other onNext calls could pile up (I haven't yet thought of a way to do this without sleeping) + Thread.sleep(50); + } catch (InterruptedException e) { + // ignore + } + } finally { + concurrentCounter.decrementAndGet(); + } + } + + } + + @Test + public void scheduleDirect() throws Exception { + Scheduler s = getScheduler(); + + final CountDownLatch cdl = new CountDownLatch(1); + + s.scheduleDirect(new Runnable() { + @Override + public void run() { + cdl.countDown(); + } + }); + + assertTrue(cdl.await(5, TimeUnit.SECONDS)); + } + + @Test + public void scheduleDirectDelayed() throws Exception { + Scheduler s = getScheduler(); + + final CountDownLatch cdl = new CountDownLatch(1); + + s.scheduleDirect(new Runnable() { + @Override + public void run() { + cdl.countDown(); + } + }, 50, TimeUnit.MILLISECONDS); + + assertTrue(cdl.await(5, TimeUnit.SECONDS)); + } + + @Test + public void scheduleDirectPeriodic() throws Exception { + Scheduler s = getScheduler(); + if (s instanceof TrampolineScheduler) { + // can't properly stop a trampolined periodic task + return; + } + + final CountDownLatch cdl = new CountDownLatch(5); + + Disposable d = s.schedulePeriodicallyDirect(new Runnable() { + @Override + public void run() { + cdl.countDown(); + } + }, 10, 10, TimeUnit.MILLISECONDS); + + try { + assertTrue(cdl.await(5, TimeUnit.SECONDS)); + } finally { + d.dispose(); + } + assertTrue(d.isDisposed()); + } + + @Test + public void schedulePeriodicallyDirectZeroPeriod() throws Exception { + Scheduler s = getScheduler(); + if (s instanceof TrampolineScheduler) { + // can't properly stop a trampolined periodic task + return; + } + + for (int initial = 0; initial < 2; initial++) { + final CountDownLatch cdl = new CountDownLatch(1); + + final SequentialDisposable sd = new SequentialDisposable(); + + try { + sd.replace(s.schedulePeriodicallyDirect(new Runnable() { + int count; + + @Override + public void run() { + if (++count == 10) { + sd.dispose(); + cdl.countDown(); + } + } + }, initial, 0, TimeUnit.MILLISECONDS)); + + assertTrue("" + initial, cdl.await(5, TimeUnit.SECONDS)); + } finally { + sd.dispose(); + } + } + } + + @Test + public void schedulePeriodicallyZeroPeriod() throws Exception { + Scheduler s = getScheduler(); + if (s instanceof TrampolineScheduler) { + // can't properly stop a trampolined periodic task + return; + } + + for (int initial = 0; initial < 2; initial++) { + final CountDownLatch cdl = new CountDownLatch(1); + + final SequentialDisposable sd = new SequentialDisposable(); + + Scheduler.Worker w = s.createWorker(); + + try { + sd.replace(w.schedulePeriodically(new Runnable() { + int count; + + @Override + public void run() { + if (++count == 10) { + sd.dispose(); + cdl.countDown(); + } + } + }, initial, 0, TimeUnit.MILLISECONDS)); + + assertTrue("" + initial, cdl.await(5, TimeUnit.SECONDS)); + } finally { + sd.dispose(); + w.dispose(); + } + } + } + + private void assertRunnableDecorated(Runnable scheduleCall) throws InterruptedException { + try { + final CountDownLatch decoratedCalled = new CountDownLatch(1); + + RxJavaPlugins.setScheduleHandler(new Function<Runnable, Runnable>() { + @Override + public Runnable apply(final Runnable actual) throws Exception { + return new Runnable() { + @Override + public void run() { + decoratedCalled.countDown(); + actual.run(); + } + }; + } + }); + + scheduleCall.run(); + + assertTrue(decoratedCalled.await(5, TimeUnit.SECONDS)); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void scheduleDirectDecoratesRunnable() throws InterruptedException { + assertRunnableDecorated(new Runnable() { + @Override + public void run() { + getScheduler().scheduleDirect(Functions.EMPTY_RUNNABLE); + } + }); + } + + @Test + public void scheduleDirectWithDelayDecoratesRunnable() throws InterruptedException { + assertRunnableDecorated(new Runnable() { + @Override + public void run() { + getScheduler().scheduleDirect(Functions.EMPTY_RUNNABLE, 1, TimeUnit.MILLISECONDS); + } + }); + } + + @Test + public void schedulePeriodicallyDirectDecoratesRunnable() throws InterruptedException { + final Scheduler scheduler = getScheduler(); + if (scheduler instanceof TrampolineScheduler) { + // Can't properly stop a trampolined periodic task. + return; + } + + final AtomicReference<Disposable> disposable = new AtomicReference<>(); + + try { + assertRunnableDecorated(new Runnable() { + @Override + public void run() { + disposable.set(scheduler.schedulePeriodicallyDirect(Functions.EMPTY_RUNNABLE, 1, 10000, TimeUnit.MILLISECONDS)); + } + }); + } finally { + disposable.get().dispose(); + } + } + + @Test + public void unwrapDefaultPeriodicTask() throws InterruptedException { + Scheduler s = getScheduler(); + if (s instanceof TrampolineScheduler) { + // TrampolineScheduler always return EmptyDisposable + return; + } + + final CountDownLatch cdl = new CountDownLatch(1); + Runnable countDownRunnable = new Runnable() { + @Override + public void run() { + cdl.countDown(); + } + }; + Disposable disposable = s.schedulePeriodicallyDirect(countDownRunnable, 100, 100, TimeUnit.MILLISECONDS); + SchedulerRunnableIntrospection wrapper = (SchedulerRunnableIntrospection) disposable; + + assertSame(countDownRunnable, wrapper.getWrappedRunnable()); + assertTrue(cdl.await(5, TimeUnit.SECONDS)); + disposable.dispose(); + } + + @Test + public void unwrapScheduleDirectTask() { + Scheduler scheduler = getScheduler(); + if (scheduler instanceof TrampolineScheduler) { + // TrampolineScheduler always return EmptyDisposable + return; + } + final CountDownLatch cdl = new CountDownLatch(1); + Runnable countDownRunnable = new Runnable() { + @Override + public void run() { + cdl.countDown(); + } + }; + Disposable disposable = scheduler.scheduleDirect(countDownRunnable, 100, TimeUnit.MILLISECONDS); + SchedulerRunnableIntrospection wrapper = (SchedulerRunnableIntrospection) disposable; + assertSame(countDownRunnable, wrapper.getWrappedRunnable()); + disposable.dispose(); + } + + @Test + public void scheduleDirectNullRunnable() { + try { + getScheduler().scheduleDirect(null); + fail(); + } catch (NullPointerException npe) { + assertEquals("run is null", npe.getMessage()); + } + } + + @Test + public void scheduleDirectWithDelayNullRunnable() { + try { + getScheduler().scheduleDirect(null, 10, TimeUnit.MILLISECONDS); + fail(); + } catch (NullPointerException npe) { + assertEquals("run is null", npe.getMessage()); + } + } + + @Test + public void schedulePeriodicallyDirectNullRunnable() { + try { + getScheduler().schedulePeriodicallyDirect(null, 5, 10, TimeUnit.MILLISECONDS); + fail(); + } catch (NullPointerException npe) { + assertEquals("run is null", npe.getMessage()); + } + } + + void schedulePrint(Function<Runnable, Disposable> onSchedule) { + CountDownLatch waitForBody = new CountDownLatch(1); + CountDownLatch waitForPrint = new CountDownLatch(1); + + try { + Disposable d = onSchedule.apply(() -> { + waitForBody.countDown(); + try { + waitForPrint.await(); + } catch (InterruptedException ex) { + ex.printStackTrace(); + } + }); + + waitForBody.await(); + + assertNotEquals("", d.toString()); + } catch (Throwable ex) { + throw new AssertionError(ex); + } finally { + waitForPrint.countDown(); + } + } + + @Test + public void scheduleDirectPrint() { + if (getScheduler() instanceof TrampolineScheduler) { + // no concurrency with Trampoline + return; + } + schedulePrint(r -> getScheduler().scheduleDirect(r)); + } + + @Test + public void schedulePrint() { + if (getScheduler() instanceof TrampolineScheduler) { + // no concurrency with Trampoline + return; + } + Worker worker = getScheduler().createWorker(); + try { + schedulePrint(worker::schedule); + } finally { + worker.dispose(); + } + } +} diff --git a/src/test/java/io/reactivex/rxjava3/schedulers/CachedThreadSchedulerTest.java b/src/test/java/io/reactivex/rxjava3/schedulers/CachedThreadSchedulerTest.java new file mode 100644 index 0000000000..b4576ebb53 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/schedulers/CachedThreadSchedulerTest.java @@ -0,0 +1,127 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.schedulers; + +import static org.junit.Assert.*; + +import java.util.concurrent.TimeUnit; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.core.Scheduler.Worker; +import io.reactivex.rxjava3.disposables.*; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.schedulers.IoScheduler; +import io.reactivex.rxjava3.testsupport.SuppressUndeliverable; + +public class CachedThreadSchedulerTest extends AbstractSchedulerConcurrencyTests { + + @Override + protected Scheduler getScheduler() { + return Schedulers.io(); + } + + /** + * IO scheduler defaults to using CachedThreadScheduler. + */ + @Test + public final void iOScheduler() { + + Flowable<Integer> f1 = Flowable.just(1, 2, 3, 4, 5); + Flowable<Integer> f2 = Flowable.just(6, 7, 8, 9, 10); + Flowable<String> f = Flowable.merge(f1, f2).map(new Function<Integer, String>() { + + @Override + public String apply(Integer t) { + assertTrue(Thread.currentThread().getName().startsWith("RxCachedThreadScheduler")); + return "Value_" + t + "_Thread_" + Thread.currentThread().getName(); + } + }); + + f.subscribeOn(Schedulers.io()).blockingForEach(new Consumer<String>() { + + @Override + public void accept(String t) { + System.out.println("t: " + t); + } + }); + } + + @Test + public final void handledErrorIsNotDeliveredToThreadHandler() throws InterruptedException { + SchedulerTestHelper.handledErrorIsNotDeliveredToThreadHandler(getScheduler()); + } + + @Test + public void cancelledTaskRetention() throws InterruptedException { + Worker w = Schedulers.io().createWorker(); + try { + ExecutorSchedulerTest.cancelledRetention(w, false); + } finally { + w.dispose(); + } + w = Schedulers.io().createWorker(); + try { + ExecutorSchedulerTest.cancelledRetention(w, true); + } finally { + w.dispose(); + } + } + + @Test + public void workerDisposed() { + Worker w = Schedulers.io().createWorker(); + + assertFalse(((Disposable)w).isDisposed()); + + w.dispose(); + + assertTrue(((Disposable)w).isDisposed()); + } + + @Test + @SuppressUndeliverable + public void shutdownRejects() { + final int[] calls = { 0 }; + + Runnable r = new Runnable() { + @Override + public void run() { + calls[0]++; + } + }; + + IoScheduler s = new IoScheduler(); + s.shutdown(); + s.shutdown(); + + s.scheduleDirect(r); + + s.scheduleDirect(r, 1, TimeUnit.SECONDS); + + s.schedulePeriodicallyDirect(r, 1, 1, TimeUnit.SECONDS); + + Worker w = s.createWorker(); + w.dispose(); + + assertEquals(Disposable.disposed(), w.schedule(r)); + + assertEquals(Disposable.disposed(), w.schedule(r, 1, TimeUnit.SECONDS)); + + assertEquals(Disposable.disposed(), w.schedulePeriodically(r, 1, 1, TimeUnit.SECONDS)); + + assertEquals(0, calls[0]); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/schedulers/ComputationSchedulerTests.java b/src/test/java/io/reactivex/rxjava3/schedulers/ComputationSchedulerTests.java new file mode 100644 index 0000000000..c88415209d --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/schedulers/ComputationSchedulerTests.java @@ -0,0 +1,324 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.schedulers; + +import static org.junit.Assert.*; + +import java.util.HashMap; +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.core.Scheduler.Worker; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.schedulers.ComputationScheduler; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.testsupport.SuppressUndeliverable; + +public class ComputationSchedulerTests extends AbstractSchedulerConcurrencyTests { + + @Override + protected Scheduler getScheduler() { + // this is an implementation of ExecutorScheduler + return Schedulers.computation(); + } + + @Test + public void threadSafetyWhenSchedulerIsHoppingBetweenThreads() { + + final int NUM = 1000000; + final CountDownLatch latch = new CountDownLatch(1); + final HashMap<String, Integer> map = new HashMap<>(); + + final Scheduler.Worker inner = Schedulers.computation().createWorker(); + + try { + inner.schedule(new Runnable() { + + private HashMap<String, Integer> statefulMap = map; + int nonThreadSafeCounter; + + @Override + public void run() { + Integer i = statefulMap.get("a"); + if (i == null) { + i = 1; + statefulMap.put("a", i); + statefulMap.put("b", i); + } else { + i++; + statefulMap.put("a", i); + statefulMap.put("b", i); + } + nonThreadSafeCounter++; + statefulMap.put("nonThreadSafeCounter", nonThreadSafeCounter); + if (i < NUM) { + inner.schedule(this); + } else { + latch.countDown(); + } + } + }); + + try { + latch.await(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + + System.out.println("Count A: " + map.get("a")); + System.out.println("Count B: " + map.get("b")); + System.out.println("nonThreadSafeCounter: " + map.get("nonThreadSafeCounter")); + + assertEquals(NUM, map.get("a").intValue()); + assertEquals(NUM, map.get("b").intValue()); + assertEquals(NUM, map.get("nonThreadSafeCounter").intValue()); + } finally { + inner.dispose(); + } + } + + @Test + public final void computationThreadPool1() { + Flowable<Integer> f1 = Flowable.<Integer> just(1, 2, 3, 4, 5); + Flowable<Integer> f2 = Flowable.<Integer> just(6, 7, 8, 9, 10); + Flowable<String> f = Flowable.<Integer> merge(f1, f2).map(new Function<Integer, String>() { + + @Override + public String apply(Integer t) { + assertTrue(Thread.currentThread().getName().startsWith("RxComputationThreadPool")); + return "Value_" + t + "_Thread_" + Thread.currentThread().getName(); + } + }); + + f.subscribeOn(Schedulers.computation()).blockingForEach(new Consumer<String>() { + + @Override + public void accept(String t) { + System.out.println("t: " + t); + } + }); + } + + @Test + public final void mergeWithExecutorScheduler() { + + final String currentThreadName = Thread.currentThread().getName(); + + Flowable<Integer> f1 = Flowable.<Integer> just(1, 2, 3, 4, 5); + Flowable<Integer> f2 = Flowable.<Integer> just(6, 7, 8, 9, 10); + Flowable<String> f = Flowable.<Integer> merge(f1, f2).subscribeOn(Schedulers.computation()).map(new Function<Integer, String>() { + + @Override + public String apply(Integer t) { + assertNotEquals(Thread.currentThread().getName(), currentThreadName); + assertTrue(Thread.currentThread().getName().startsWith("RxComputationThreadPool")); + return "Value_" + t + "_Thread_" + Thread.currentThread().getName(); + } + }); + + f.blockingForEach(new Consumer<String>() { + + @Override + public void accept(String t) { + System.out.println("t: " + t); + } + }); + } + + @Test + public final void handledErrorIsNotDeliveredToThreadHandler() throws InterruptedException { + SchedulerTestHelper.handledErrorIsNotDeliveredToThreadHandler(getScheduler()); + } + + @Test + public void cancelledTaskRetention() throws InterruptedException { + Worker w = Schedulers.computation().createWorker(); + try { + ExecutorSchedulerTest.cancelledRetention(w, false); + } finally { + w.dispose(); + } + w = Schedulers.computation().createWorker(); + try { + ExecutorSchedulerTest.cancelledRetention(w, true); + } finally { + w.dispose(); + } + } + + @Test + @SuppressUndeliverable + public void shutdownRejects() { + final int[] calls = { 0 }; + + Runnable r = new Runnable() { + @Override + public void run() { + calls[0]++; + } + }; + + Scheduler s = new ComputationScheduler(); + s.shutdown(); + s.shutdown(); + + assertEquals(Disposable.disposed(), s.scheduleDirect(r)); + + assertEquals(Disposable.disposed(), s.scheduleDirect(r, 1, TimeUnit.SECONDS)); + + assertEquals(Disposable.disposed(), s.schedulePeriodicallyDirect(r, 1, 1, TimeUnit.SECONDS)); + + Worker w = s.createWorker(); + w.dispose(); + + assertTrue(w.isDisposed()); + + assertEquals(Disposable.disposed(), w.schedule(r)); + + assertEquals(Disposable.disposed(), w.schedule(r, 1, TimeUnit.SECONDS)); + + assertEquals(Disposable.disposed(), w.schedulePeriodically(r, 1, 1, TimeUnit.SECONDS)); + + assertEquals(0, calls[0]); + } + + @Test + public void exceptionFromObservableShouldNotBeSwallowed() throws Exception { + CountDownLatch latch = new CountDownLatch(1); + + // #3 thread's uncaught exception handler + Scheduler computationScheduler = new ComputationScheduler(new ThreadFactory() { + @Override + public Thread newThread(Runnable r) { + Thread t = new Thread(r); + t.setUncaughtExceptionHandler((thread, throwable) -> { + latch.countDown(); + }); + return t; + } + }); + + // #2 RxJava exception handler + RxJavaPlugins.setErrorHandler(h -> { + latch.countDown(); + }); + + // Exceptions, fatal or not, should be handled by + // #1 observer's onError(), or + // #2 RxJava exception handler, or + // #3 thread's uncaught exception handler, + // and should not be swallowed. + try { + + // #1 observer's onError() + Observable.create(s -> { + + s.onNext(1); + throw new OutOfMemoryError(); + }) + .subscribeOn(computationScheduler) + .subscribe(v -> { }, + e -> { latch.countDown(); } + ); + + assertTrue(latch.await(2, TimeUnit.SECONDS)); + } finally { + RxJavaPlugins.reset(); + computationScheduler.shutdown(); + } + } + + @Test + public void exceptionFromObserverShouldNotBeSwallowed() throws Exception { + CountDownLatch latch = new CountDownLatch(1); + + // #3 thread's uncaught exception handler + Scheduler computationScheduler = new ComputationScheduler(new ThreadFactory() { + @Override + public Thread newThread(Runnable r) { + Thread t = new Thread(r); + t.setUncaughtExceptionHandler((thread, throwable) -> { + latch.countDown(); + }); + return t; + } + }); + + // #2 RxJava exception handler + RxJavaPlugins.setErrorHandler(h -> { + latch.countDown(); + }); + + // Exceptions, fatal or not, should be handled by + // #1 observer's onError(), or + // #2 RxJava exception handler, or + // #3 thread's uncaught exception handler, + // and should not be swallowed. + try { + + // #1 observer's onError() + Flowable.interval(500, TimeUnit.MILLISECONDS, computationScheduler) + .subscribe(v -> { + throw new OutOfMemoryError(); + }, e -> { + latch.countDown(); + }); + + assertTrue(latch.await(2, TimeUnit.SECONDS)); + } finally { + RxJavaPlugins.reset(); + computationScheduler.shutdown(); + } + } + + @Test + @SuppressUndeliverable + public void periodicTaskShouldStopOnError() throws Exception { + AtomicInteger repeatCount = new AtomicInteger(); + + Schedulers.computation().schedulePeriodicallyDirect(new Runnable() { + @Override + public void run() { + repeatCount.incrementAndGet(); + throw new OutOfMemoryError(); + } + }, 0, 1, TimeUnit.MILLISECONDS); + + Thread.sleep(200); + + assertEquals(1, repeatCount.get()); + } + + @Test + @SuppressUndeliverable + public void periodicTaskShouldStopOnError2() throws Exception { + AtomicInteger repeatCount = new AtomicInteger(); + + Schedulers.computation().schedulePeriodicallyDirect(new Runnable() { + @Override + public void run() { + repeatCount.incrementAndGet(); + throw new OutOfMemoryError(); + } + }, 0, 1, TimeUnit.NANOSECONDS); + + Thread.sleep(200); + + assertEquals(1, repeatCount.get()); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/schedulers/ExecutorSchedulerFairTest.java b/src/test/java/io/reactivex/rxjava3/schedulers/ExecutorSchedulerFairTest.java new file mode 100644 index 0000000000..c01f0f9f1f --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/schedulers/ExecutorSchedulerFairTest.java @@ -0,0 +1,515 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.schedulers; + +import static org.junit.Assert.*; + +import java.util.List; +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.core.Scheduler.Worker; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.disposables.EmptyDisposable; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.internal.schedulers.RxThreadFactory; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.processors.PublishProcessor; +import io.reactivex.rxjava3.subscribers.TestSubscriber; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class ExecutorSchedulerFairTest extends AbstractSchedulerConcurrencyTests { + + static final Executor executor = Executors.newFixedThreadPool(2, new RxThreadFactory("TestCustomPool")); + + @Override + protected Scheduler getScheduler() { + return Schedulers.from(executor, false, true); + } + + @Test + public final void handledErrorIsNotDeliveredToThreadHandler() throws InterruptedException { + SchedulerTestHelper.handledErrorIsNotDeliveredToThreadHandler(getScheduler()); + } + + @Test + public void cancelledTaskRetention() throws InterruptedException { + ExecutorService exec = Executors.newSingleThreadExecutor(); + Scheduler s = Schedulers.from(exec, false, true); + try { + Scheduler.Worker w = s.createWorker(); + try { + ExecutorSchedulerTest.cancelledRetention(w, false); + } finally { + w.dispose(); + } + + w = s.createWorker(); + try { + ExecutorSchedulerTest.cancelledRetention(w, true); + } finally { + w.dispose(); + } + } finally { + exec.shutdownNow(); + } + } + + /** A simple executor which queues tasks and executes them one-by-one if executeOne() is called. */ + static final class TestExecutor implements Executor { + final ConcurrentLinkedQueue<Runnable> queue = new ConcurrentLinkedQueue<>(); + @Override + public void execute(Runnable command) { + queue.offer(command); + } + public void executeOne() { + Runnable r = queue.poll(); + if (r != null) { + r.run(); + } + } + public void executeAll() { + Runnable r; + while ((r = queue.poll()) != null) { + r.run(); + } + } + } + + @Test + public void cancelledTasksDontRun() { + final AtomicInteger calls = new AtomicInteger(); + Runnable task = new Runnable() { + @Override + public void run() { + calls.getAndIncrement(); + } + }; + TestExecutor exec = new TestExecutor(); + Scheduler custom = Schedulers.from(exec, false, true); + Worker w = custom.createWorker(); + try { + Disposable d1 = w.schedule(task); + Disposable d2 = w.schedule(task); + Disposable d3 = w.schedule(task); + + d1.dispose(); + d2.dispose(); + d3.dispose(); + + exec.executeAll(); + + assertEquals(0, calls.get()); + } finally { + w.dispose(); + } + } + + @Test + public void cancelledWorkerDoesntRunTasks() { + final AtomicInteger calls = new AtomicInteger(); + Runnable task = new Runnable() { + @Override + public void run() { + calls.getAndIncrement(); + } + }; + TestExecutor exec = new TestExecutor(); + Scheduler custom = Schedulers.from(exec, false, true); + Worker w = custom.createWorker(); + try { + w.schedule(task); + w.schedule(task); + w.schedule(task); + } finally { + w.dispose(); + } + exec.executeAll(); + assertEquals(0, calls.get()); + } + + @Test + public void plainExecutor() throws Exception { + Scheduler s = Schedulers.from(new Executor() { + @Override + public void execute(Runnable r) { + r.run(); + } + }, false, true); + + final CountDownLatch cdl = new CountDownLatch(5); + + Runnable r = new Runnable() { + @Override + public void run() { + cdl.countDown(); + } + }; + + s.scheduleDirect(r); + + s.scheduleDirect(r, 50, TimeUnit.MILLISECONDS); + + Disposable d = s.schedulePeriodicallyDirect(r, 10, 10, TimeUnit.MILLISECONDS); + + try { + assertTrue(cdl.await(5, TimeUnit.SECONDS)); + } finally { + d.dispose(); + } + + assertTrue(d.isDisposed()); + } + + @Test + public void rejectingExecutor() { + ScheduledExecutorService exec = Executors.newSingleThreadScheduledExecutor(); + exec.shutdown(); + + Scheduler s = Schedulers.from(exec, false, true); + + List<Throwable> errors = TestHelper.trackPluginErrors(); + + try { + assertSame(EmptyDisposable.INSTANCE, s.scheduleDirect(Functions.EMPTY_RUNNABLE)); + + assertSame(EmptyDisposable.INSTANCE, s.scheduleDirect(Functions.EMPTY_RUNNABLE, 10, TimeUnit.MILLISECONDS)); + + assertSame(EmptyDisposable.INSTANCE, s.schedulePeriodicallyDirect(Functions.EMPTY_RUNNABLE, 10, 10, TimeUnit.MILLISECONDS)); + + TestHelper.assertUndeliverable(errors, 0, RejectedExecutionException.class); + TestHelper.assertUndeliverable(errors, 1, RejectedExecutionException.class); + TestHelper.assertUndeliverable(errors, 2, RejectedExecutionException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void rejectingExecutorWorker() { + ScheduledExecutorService exec = Executors.newSingleThreadScheduledExecutor(); + exec.shutdown(); + + List<Throwable> errors = TestHelper.trackPluginErrors(); + + try { + Worker s = Schedulers.from(exec, false, true).createWorker(); + assertSame(EmptyDisposable.INSTANCE, s.schedule(Functions.EMPTY_RUNNABLE)); + + s = Schedulers.from(exec, false, true).createWorker(); + assertSame(EmptyDisposable.INSTANCE, s.schedule(Functions.EMPTY_RUNNABLE, 10, TimeUnit.MILLISECONDS)); + + s = Schedulers.from(exec, false, true).createWorker(); + assertSame(EmptyDisposable.INSTANCE, s.schedulePeriodically(Functions.EMPTY_RUNNABLE, 10, 10, TimeUnit.MILLISECONDS)); + + TestHelper.assertUndeliverable(errors, 0, RejectedExecutionException.class); + TestHelper.assertUndeliverable(errors, 1, RejectedExecutionException.class); + TestHelper.assertUndeliverable(errors, 2, RejectedExecutionException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void reuseScheduledExecutor() throws Exception { + ScheduledExecutorService exec = Executors.newSingleThreadScheduledExecutor(); + + try { + Scheduler s = Schedulers.from(exec, false, true); + + final CountDownLatch cdl = new CountDownLatch(8); + + Runnable r = new Runnable() { + @Override + public void run() { + cdl.countDown(); + } + }; + + s.scheduleDirect(r); + + s.scheduleDirect(r, 10, TimeUnit.MILLISECONDS); + + Disposable d = s.schedulePeriodicallyDirect(r, 10, 10, TimeUnit.MILLISECONDS); + + try { + assertTrue(cdl.await(5, TimeUnit.SECONDS)); + } finally { + d.dispose(); + } + } finally { + exec.shutdown(); + } + } + + @Test + public void reuseScheduledExecutorAsWorker() throws Exception { + ScheduledExecutorService exec = Executors.newSingleThreadScheduledExecutor(); + + Worker s = Schedulers.from(exec, false, true).createWorker(); + + assertFalse(s.isDisposed()); + try { + + final CountDownLatch cdl = new CountDownLatch(8); + + Runnable r = new Runnable() { + @Override + public void run() { + cdl.countDown(); + } + }; + + s.schedule(r); + + s.schedule(r, 10, TimeUnit.MILLISECONDS); + + Disposable d = s.schedulePeriodically(r, 10, 10, TimeUnit.MILLISECONDS); + + try { + assertTrue(cdl.await(5, TimeUnit.SECONDS)); + } finally { + d.dispose(); + } + } finally { + s.dispose(); + exec.shutdown(); + } + + assertTrue(s.isDisposed()); + } + + @Test + public void disposeRace() { + ExecutorService exec = Executors.newSingleThreadExecutor(); + final Scheduler s = Schedulers.from(exec, false, true); + try { + for (int i = 0; i < 500; i++) { + final Worker w = s.createWorker(); + + final AtomicInteger c = new AtomicInteger(2); + + w.schedule(new Runnable() { + @Override + public void run() { + c.decrementAndGet(); + while (c.get() != 0) { } + } + }); + + c.decrementAndGet(); + while (c.get() != 0) { } + w.dispose(); + } + } finally { + exec.shutdownNow(); + } + } + + @Test + public void runnableDisposed() { + final Scheduler s = Schedulers.from(new Executor() { + @Override + public void execute(Runnable r) { + r.run(); + } + }, false, true); + Disposable d = s.scheduleDirect(Functions.EMPTY_RUNNABLE); + + assertTrue(d.isDisposed()); + } + + @Test + public void runnableDisposedAsync() throws Exception { + final Scheduler s = Schedulers.from(new Executor() { + @Override + public void execute(Runnable r) { + new Thread(r).start(); + } + }, false, true); + Disposable d = s.scheduleDirect(Functions.EMPTY_RUNNABLE); + + while (!d.isDisposed()) { + Thread.sleep(1); + } + } + + @Test + public void runnableDisposedAsync2() throws Exception { + final Scheduler s = Schedulers.from(executor, false, true); + Disposable d = s.scheduleDirect(Functions.EMPTY_RUNNABLE); + + while (!d.isDisposed()) { + Thread.sleep(1); + } + } + + @Test + public void runnableDisposedAsyncCrash() throws Exception { + final Scheduler s = Schedulers.from(new Executor() { + @Override + public void execute(Runnable r) { + new Thread(r).start(); + } + }, false, true); + Disposable d = s.scheduleDirect(new Runnable() { + @Override + public void run() { + throw new IllegalStateException(); + } + }); + + while (!d.isDisposed()) { + Thread.sleep(1); + } + } + + @Test + public void runnableDisposedAsyncTimed() throws Exception { + final Scheduler s = Schedulers.from(new Executor() { + @Override + public void execute(Runnable r) { + new Thread(r).start(); + } + }, false, true); + Disposable d = s.scheduleDirect(Functions.EMPTY_RUNNABLE, 1, TimeUnit.MILLISECONDS); + + while (!d.isDisposed()) { + Thread.sleep(1); + } + } + + @Test + public void runnableDisposedAsyncTimed2() throws Exception { + ExecutorService executorScheduler = Executors.newScheduledThreadPool(1, new RxThreadFactory("TestCustomPoolTimed")); + try { + final Scheduler s = Schedulers.from(executorScheduler, false, true); + Disposable d = s.scheduleDirect(Functions.EMPTY_RUNNABLE, 1, TimeUnit.MILLISECONDS); + + while (!d.isDisposed()) { + Thread.sleep(1); + } + } finally { + executorScheduler.shutdownNow(); + } + } + + @Test + public void unwrapScheduleDirectTaskAfterDispose() { + Scheduler scheduler = getScheduler(); + final CountDownLatch cdl = new CountDownLatch(1); + Runnable countDownRunnable = new Runnable() { + @Override + public void run() { + cdl.countDown(); + } + }; + Disposable disposable = scheduler.scheduleDirect(countDownRunnable, 100, TimeUnit.MILLISECONDS); + SchedulerRunnableIntrospection wrapper = (SchedulerRunnableIntrospection) disposable; + assertSame(countDownRunnable, wrapper.getWrappedRunnable()); + disposable.dispose(); + + assertSame(Functions.EMPTY_RUNNABLE, wrapper.getWrappedRunnable()); + } + + @Test + public void fairInterleaving() { + ScheduledExecutorService exec = Executors.newSingleThreadScheduledExecutor(); + try { + final Scheduler sch = Schedulers.from(exec, false, true); + + PublishProcessor<Integer> pp = PublishProcessor.create(); + + TestSubscriber<Integer> ts = pp + .publish(new Function<Flowable<Integer>, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Flowable<Integer> v) throws Throwable { + return Flowable.merge( + v.filter(new Predicate<Integer>() { + @Override + public boolean test(Integer w) throws Throwable { + return w % 2 == 0; + } + }).observeOn(sch, false, 1).hide(), + v.filter(new Predicate<Integer>() { + @Override + public boolean test(Integer w) throws Throwable { + return w % 2 != 0; + } + }).observeOn(sch, false, 1).hide() + ); + } + }) + .test(); + + for (int i = 1; i < 11; i++) { + pp.onNext(i); + } + pp.onComplete(); + + ts + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + } finally { + exec.shutdown(); + } + } + + @Test + public void fairInterleavingWithDelay() { + ScheduledExecutorService exec = Executors.newSingleThreadScheduledExecutor(); + try { + final Scheduler sch = Schedulers.from(exec, false, true); + + PublishProcessor<Integer> pp = PublishProcessor.create(); + + TestSubscriber<Integer> ts = pp + .publish(new Function<Flowable<Integer>, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Flowable<Integer> v) throws Throwable { + return Flowable.merge( + v.filter(new Predicate<Integer>() { + @Override + public boolean test(Integer w) throws Throwable { + return w % 2 == 0; + } + }).delay(0, TimeUnit.SECONDS, sch).hide(), + v.filter(new Predicate<Integer>() { + @Override + public boolean test(Integer w) throws Throwable { + return w % 2 != 0; + } + }).delay(0, TimeUnit.SECONDS, sch).hide() + ); + } + }) + .test(); + + for (int i = 1; i < 11; i++) { + pp.onNext(i); + } + pp.onComplete(); + + ts + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + } finally { + exec.shutdown(); + } + } +} diff --git a/src/test/java/io/reactivex/rxjava3/schedulers/ExecutorSchedulerInterruptibleTest.java b/src/test/java/io/reactivex/rxjava3/schedulers/ExecutorSchedulerInterruptibleTest.java new file mode 100644 index 0000000000..074ac8039a --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/schedulers/ExecutorSchedulerInterruptibleTest.java @@ -0,0 +1,1113 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.schedulers; + +import static org.junit.Assert.*; + +import java.util.List; +import java.util.concurrent.*; +import java.util.concurrent.atomic.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.Scheduler; +import io.reactivex.rxjava3.core.Scheduler.Worker; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.disposables.EmptyDisposable; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.internal.schedulers.RxThreadFactory; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class ExecutorSchedulerInterruptibleTest extends AbstractSchedulerConcurrencyTests { + + static final Executor executor = Executors.newFixedThreadPool(2, new RxThreadFactory("TestCustomPool")); + + @Override + protected Scheduler getScheduler() { + return Schedulers.from(executor, true); + } + + @Test + public final void handledErrorIsNotDeliveredToThreadHandler() throws InterruptedException { + SchedulerTestHelper.handledErrorIsNotDeliveredToThreadHandler(getScheduler()); + } + + @Test + public void cancelledTaskRetention() throws InterruptedException { + ExecutorService exec = Executors.newSingleThreadExecutor(); + Scheduler s = Schedulers.from(exec, true); + try { + Scheduler.Worker w = s.createWorker(); + try { + ExecutorSchedulerTest.cancelledRetention(w, false); + } finally { + w.dispose(); + } + + w = s.createWorker(); + try { + ExecutorSchedulerTest.cancelledRetention(w, true); + } finally { + w.dispose(); + } + } finally { + exec.shutdownNow(); + } + } + + /** A simple executor which queues tasks and executes them one-by-one if executeOne() is called. */ + static final class TestExecutor implements Executor { + final ConcurrentLinkedQueue<Runnable> queue = new ConcurrentLinkedQueue<>(); + @Override + public void execute(Runnable command) { + queue.offer(command); + } + public void executeOne() { + Runnable r = queue.poll(); + if (r != null) { + r.run(); + } + } + public void executeAll() { + Runnable r; + while ((r = queue.poll()) != null) { + r.run(); + } + } + } + + @Test + public void cancelledTasksDontRun() { + final AtomicInteger calls = new AtomicInteger(); + Runnable task = new Runnable() { + @Override + public void run() { + calls.getAndIncrement(); + } + }; + TestExecutor exec = new TestExecutor(); + Scheduler custom = Schedulers.from(exec, true); + Worker w = custom.createWorker(); + try { + Disposable d1 = w.schedule(task); + Disposable d2 = w.schedule(task); + Disposable d3 = w.schedule(task); + + d1.dispose(); + d2.dispose(); + d3.dispose(); + + exec.executeAll(); + + assertEquals(0, calls.get()); + } finally { + w.dispose(); + } + } + + @Test + public void cancelledWorkerDoesntRunTasks() { + final AtomicInteger calls = new AtomicInteger(); + Runnable task = new Runnable() { + @Override + public void run() { + calls.getAndIncrement(); + } + }; + TestExecutor exec = new TestExecutor(); + Scheduler custom = Schedulers.from(exec, true); + Worker w = custom.createWorker(); + try { + w.schedule(task); + w.schedule(task); + w.schedule(task); + } finally { + w.dispose(); + } + exec.executeAll(); + assertEquals(0, calls.get()); + } + + @Test + public void plainExecutor() throws Exception { + Scheduler s = Schedulers.from(new Executor() { + @Override + public void execute(Runnable r) { + r.run(); + } + }, true); + + final CountDownLatch cdl = new CountDownLatch(5); + + Runnable r = new Runnable() { + @Override + public void run() { + cdl.countDown(); + } + }; + + s.scheduleDirect(r); + + s.scheduleDirect(r, 50, TimeUnit.MILLISECONDS); + + Disposable d = s.schedulePeriodicallyDirect(r, 10, 10, TimeUnit.MILLISECONDS); + + try { + assertTrue(cdl.await(5, TimeUnit.SECONDS)); + } finally { + d.dispose(); + } + + assertTrue(d.isDisposed()); + } + + @Test + public void rejectingExecutor() { + ScheduledExecutorService exec = Executors.newSingleThreadScheduledExecutor(); + exec.shutdown(); + + Scheduler s = Schedulers.from(exec, true); + + List<Throwable> errors = TestHelper.trackPluginErrors(); + + try { + assertSame(EmptyDisposable.INSTANCE, s.scheduleDirect(Functions.EMPTY_RUNNABLE)); + + assertSame(EmptyDisposable.INSTANCE, s.scheduleDirect(Functions.EMPTY_RUNNABLE, 10, TimeUnit.MILLISECONDS)); + + assertSame(EmptyDisposable.INSTANCE, s.schedulePeriodicallyDirect(Functions.EMPTY_RUNNABLE, 10, 10, TimeUnit.MILLISECONDS)); + + TestHelper.assertUndeliverable(errors, 0, RejectedExecutionException.class); + TestHelper.assertUndeliverable(errors, 1, RejectedExecutionException.class); + TestHelper.assertUndeliverable(errors, 2, RejectedExecutionException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void rejectingExecutorWorker() { + ScheduledExecutorService exec = Executors.newSingleThreadScheduledExecutor(); + exec.shutdown(); + + List<Throwable> errors = TestHelper.trackPluginErrors(); + + try { + Worker s = Schedulers.from(exec, true).createWorker(); + assertSame(EmptyDisposable.INSTANCE, s.schedule(Functions.EMPTY_RUNNABLE)); + + s = Schedulers.from(exec, true).createWorker(); + assertSame(EmptyDisposable.INSTANCE, s.schedule(Functions.EMPTY_RUNNABLE, 10, TimeUnit.MILLISECONDS)); + + s = Schedulers.from(exec, true).createWorker(); + assertSame(EmptyDisposable.INSTANCE, s.schedulePeriodically(Functions.EMPTY_RUNNABLE, 10, 10, TimeUnit.MILLISECONDS)); + + TestHelper.assertUndeliverable(errors, 0, RejectedExecutionException.class); + TestHelper.assertUndeliverable(errors, 1, RejectedExecutionException.class); + TestHelper.assertUndeliverable(errors, 2, RejectedExecutionException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void reuseScheduledExecutor() throws Exception { + ScheduledExecutorService exec = Executors.newSingleThreadScheduledExecutor(); + + try { + Scheduler s = Schedulers.from(exec, true); + + final CountDownLatch cdl = new CountDownLatch(8); + + Runnable r = new Runnable() { + @Override + public void run() { + cdl.countDown(); + } + }; + + s.scheduleDirect(r); + + s.scheduleDirect(r, 10, TimeUnit.MILLISECONDS); + + Disposable d = s.schedulePeriodicallyDirect(r, 10, 10, TimeUnit.MILLISECONDS); + + try { + assertTrue(cdl.await(5, TimeUnit.SECONDS)); + } finally { + d.dispose(); + } + } finally { + exec.shutdown(); + } + } + + @Test + public void reuseScheduledExecutorAsWorker() throws Exception { + ScheduledExecutorService exec = Executors.newSingleThreadScheduledExecutor(); + + Worker s = Schedulers.from(exec, true).createWorker(); + + assertFalse(s.isDisposed()); + try { + + final CountDownLatch cdl = new CountDownLatch(8); + + Runnable r = new Runnable() { + @Override + public void run() { + cdl.countDown(); + } + }; + + s.schedule(r); + + s.schedule(r, 10, TimeUnit.MILLISECONDS); + + Disposable d = s.schedulePeriodically(r, 10, 10, TimeUnit.MILLISECONDS); + + try { + assertTrue(cdl.await(5, TimeUnit.SECONDS)); + } finally { + d.dispose(); + } + } finally { + s.dispose(); + exec.shutdown(); + } + + assertTrue(s.isDisposed()); + } + + @Test + public void disposeRace() { + ExecutorService exec = Executors.newSingleThreadExecutor(); + final Scheduler s = Schedulers.from(exec, true); + try { + for (int i = 0; i < 500; i++) { + final Worker w = s.createWorker(); + + final AtomicInteger c = new AtomicInteger(2); + + w.schedule(new Runnable() { + @Override + public void run() { + c.decrementAndGet(); + while (c.get() != 0) { } + } + }); + + c.decrementAndGet(); + while (c.get() != 0) { } + w.dispose(); + } + } finally { + exec.shutdownNow(); + } + } + + @Test + public void runnableDisposed() { + final Scheduler s = Schedulers.from(new Executor() { + @Override + public void execute(Runnable r) { + r.run(); + } + }, true); + Disposable d = s.scheduleDirect(Functions.EMPTY_RUNNABLE); + + assertTrue(d.isDisposed()); + } + + @Test + public void runnableDisposedAsync() throws Exception { + final Scheduler s = Schedulers.from(new Executor() { + @Override + public void execute(Runnable r) { + new Thread(r).start(); + } + }, true); + Disposable d = s.scheduleDirect(Functions.EMPTY_RUNNABLE); + + while (!d.isDisposed()) { + Thread.sleep(1); + } + } + + @Test + public void runnableDisposedAsync2() throws Exception { + final Scheduler s = Schedulers.from(executor, true); + Disposable d = s.scheduleDirect(Functions.EMPTY_RUNNABLE); + + while (!d.isDisposed()) { + Thread.sleep(1); + } + } + + @Test + public void runnableDisposedAsyncCrash() throws Exception { + final Scheduler s = Schedulers.from(new Executor() { + @Override + public void execute(Runnable r) { + new Thread(r).start(); + } + }, true); + Disposable d = s.scheduleDirect(new Runnable() { + @Override + public void run() { + throw new IllegalStateException(); + } + }); + + while (!d.isDisposed()) { + Thread.sleep(1); + } + } + + @Test + public void runnableDisposedAsyncTimed() throws Exception { + final Scheduler s = Schedulers.from(new Executor() { + @Override + public void execute(Runnable r) { + new Thread(r).start(); + } + }, true); + Disposable d = s.scheduleDirect(Functions.EMPTY_RUNNABLE, 1, TimeUnit.MILLISECONDS); + + while (!d.isDisposed()) { + Thread.sleep(1); + } + } + + @Test + public void runnableDisposedAsyncTimed2() throws Exception { + ExecutorService executorScheduler = Executors.newScheduledThreadPool(1, new RxThreadFactory("TestCustomPoolTimed")); + try { + final Scheduler s = Schedulers.from(executorScheduler, true); + Disposable d = s.scheduleDirect(Functions.EMPTY_RUNNABLE, 1, TimeUnit.MILLISECONDS); + + while (!d.isDisposed()) { + Thread.sleep(1); + } + } finally { + executorScheduler.shutdownNow(); + } + } + + @Test + public void unwrapScheduleDirectTaskAfterDispose() { + Scheduler scheduler = getScheduler(); + final CountDownLatch cdl = new CountDownLatch(1); + Runnable countDownRunnable = new Runnable() { + @Override + public void run() { + cdl.countDown(); + } + }; + Disposable disposable = scheduler.scheduleDirect(countDownRunnable, 100, TimeUnit.MILLISECONDS); + SchedulerRunnableIntrospection wrapper = (SchedulerRunnableIntrospection) disposable; + assertSame(countDownRunnable, wrapper.getWrappedRunnable()); + disposable.dispose(); + + assertSame(Functions.EMPTY_RUNNABLE, wrapper.getWrappedRunnable()); + } + + @Test + public void interruptibleDirectTask() throws Exception { + Scheduler scheduler = getScheduler(); + + final AtomicInteger sync = new AtomicInteger(2); + + final AtomicBoolean isInterrupted = new AtomicBoolean(); + + Disposable d = scheduler.scheduleDirect(new Runnable() { + @Override + public void run() { + if (sync.decrementAndGet() != 0) { + while (sync.get() != 0) { } + } + try { + Thread.sleep(1000); + } catch (InterruptedException ex) { + isInterrupted.set(true); + } + } + }); + + if (sync.decrementAndGet() != 0) { + while (sync.get() != 0) { } + } + + Thread.sleep(500); + + d.dispose(); + + int i = 20; + while (i-- > 0 && !isInterrupted.get()) { + Thread.sleep(50); + } + + assertTrue("Interruption did not propagate", isInterrupted.get()); + } + + @Test + public void interruptibleWorkerTask() throws Exception { + Scheduler scheduler = getScheduler(); + + Worker worker = scheduler.createWorker(); + + try { + final AtomicInteger sync = new AtomicInteger(2); + + final AtomicBoolean isInterrupted = new AtomicBoolean(); + + Disposable d = worker.schedule(new Runnable() { + @Override + public void run() { + if (sync.decrementAndGet() != 0) { + while (sync.get() != 0) { } + } + try { + Thread.sleep(1000); + } catch (InterruptedException ex) { + isInterrupted.set(true); + } + } + }); + + if (sync.decrementAndGet() != 0) { + while (sync.get() != 0) { } + } + + Thread.sleep(500); + + d.dispose(); + + int i = 20; + while (i-- > 0 && !isInterrupted.get()) { + Thread.sleep(50); + } + + assertTrue("Interruption did not propagate", isInterrupted.get()); + } finally { + worker.dispose(); + } + } + + @Test + public void interruptibleDirectTaskScheduledExecutor() throws Exception { + ScheduledExecutorService exec = Executors.newScheduledThreadPool(1); + try { + Scheduler scheduler = Schedulers.from(exec, true); + + final AtomicInteger sync = new AtomicInteger(2); + + final AtomicBoolean isInterrupted = new AtomicBoolean(); + + Disposable d = scheduler.scheduleDirect(new Runnable() { + @Override + public void run() { + if (sync.decrementAndGet() != 0) { + while (sync.get() != 0) { } + } + try { + Thread.sleep(1000); + } catch (InterruptedException ex) { + isInterrupted.set(true); + } + } + }); + + if (sync.decrementAndGet() != 0) { + while (sync.get() != 0) { } + } + + Thread.sleep(500); + + d.dispose(); + + int i = 20; + while (i-- > 0 && !isInterrupted.get()) { + Thread.sleep(50); + } + + assertTrue("Interruption did not propagate", isInterrupted.get()); + } finally { + exec.shutdown(); + } + } + + @Test + public void interruptibleWorkerTaskScheduledExecutor() throws Exception { + ScheduledExecutorService exec = Executors.newScheduledThreadPool(1); + try { + Scheduler scheduler = Schedulers.from(exec, true); + + Worker worker = scheduler.createWorker(); + + try { + final AtomicInteger sync = new AtomicInteger(2); + + final AtomicBoolean isInterrupted = new AtomicBoolean(); + + Disposable d = worker.schedule(new Runnable() { + @Override + public void run() { + if (sync.decrementAndGet() != 0) { + while (sync.get() != 0) { } + } + try { + Thread.sleep(1000); + } catch (InterruptedException ex) { + isInterrupted.set(true); + } + } + }); + + if (sync.decrementAndGet() != 0) { + while (sync.get() != 0) { } + } + + Thread.sleep(500); + + d.dispose(); + + int i = 20; + while (i-- > 0 && !isInterrupted.get()) { + Thread.sleep(50); + } + + assertTrue("Interruption did not propagate", isInterrupted.get()); + } finally { + worker.dispose(); + } + } finally { + exec.shutdown(); + } + } + + @Test + public void nonInterruptibleDirectTask() throws Exception { + ExecutorService exec = Executors.newSingleThreadExecutor(); + try { + Scheduler scheduler = Schedulers.from(exec, false); + + final AtomicInteger sync = new AtomicInteger(2); + + final AtomicBoolean isInterrupted = new AtomicBoolean(); + + Disposable d = scheduler.scheduleDirect(new Runnable() { + @Override + public void run() { + if (sync.decrementAndGet() != 0) { + while (sync.get() != 0) { } + } + try { + Thread.sleep(1000); + } catch (InterruptedException ex) { + isInterrupted.set(true); + } + } + }); + + if (sync.decrementAndGet() != 0) { + while (sync.get() != 0) { } + } + + Thread.sleep(500); + + d.dispose(); + + int i = 20; + while (i-- > 0 && !isInterrupted.get()) { + Thread.sleep(50); + } + + assertFalse("Interruption happened", isInterrupted.get()); + } finally { + exec.shutdown(); + } + } + + @Test + public void nonInterruptibleWorkerTask() throws Exception { + ExecutorService exec = Executors.newSingleThreadExecutor(); + try { + Scheduler scheduler = Schedulers.from(exec, false); + + Worker worker = scheduler.createWorker(); + + try { + final AtomicInteger sync = new AtomicInteger(2); + + final AtomicBoolean isInterrupted = new AtomicBoolean(); + + Disposable d = worker.schedule(new Runnable() { + @Override + public void run() { + if (sync.decrementAndGet() != 0) { + while (sync.get() != 0) { } + } + try { + Thread.sleep(1000); + } catch (InterruptedException ex) { + isInterrupted.set(true); + } + } + }); + + if (sync.decrementAndGet() != 0) { + while (sync.get() != 0) { } + } + + Thread.sleep(500); + + d.dispose(); + + int i = 20; + while (i-- > 0 && !isInterrupted.get()) { + Thread.sleep(50); + } + + assertFalse("Interruption happened", isInterrupted.get()); + } finally { + worker.dispose(); + } + } finally { + exec.shutdown(); + } + } + + @Test + public void nonInterruptibleDirectTaskScheduledExecutor() throws Exception { + ScheduledExecutorService exec = Executors.newScheduledThreadPool(1); + try { + Scheduler scheduler = Schedulers.from(exec, false); + + final AtomicInteger sync = new AtomicInteger(2); + + final AtomicBoolean isInterrupted = new AtomicBoolean(); + + Disposable d = scheduler.scheduleDirect(new Runnable() { + @Override + public void run() { + if (sync.decrementAndGet() != 0) { + while (sync.get() != 0) { } + } + try { + Thread.sleep(1000); + } catch (InterruptedException ex) { + isInterrupted.set(true); + } + } + }); + + if (sync.decrementAndGet() != 0) { + while (sync.get() != 0) { } + } + + Thread.sleep(500); + + d.dispose(); + + int i = 20; + while (i-- > 0 && !isInterrupted.get()) { + Thread.sleep(50); + } + + assertFalse("Interruption happened", isInterrupted.get()); + } finally { + exec.shutdown(); + } + } + + @Test + public void nonInterruptibleWorkerTaskScheduledExecutor() throws Exception { + ScheduledExecutorService exec = Executors.newScheduledThreadPool(1); + try { + Scheduler scheduler = Schedulers.from(exec, false); + + Worker worker = scheduler.createWorker(); + + try { + final AtomicInteger sync = new AtomicInteger(2); + + final AtomicBoolean isInterrupted = new AtomicBoolean(); + + Disposable d = worker.schedule(new Runnable() { + @Override + public void run() { + if (sync.decrementAndGet() != 0) { + while (sync.get() != 0) { } + } + try { + Thread.sleep(1000); + } catch (InterruptedException ex) { + isInterrupted.set(true); + } + } + }); + + if (sync.decrementAndGet() != 0) { + while (sync.get() != 0) { } + } + + Thread.sleep(500); + + d.dispose(); + + int i = 20; + while (i-- > 0 && !isInterrupted.get()) { + Thread.sleep(50); + } + + assertFalse("Interruption happened", isInterrupted.get()); + } finally { + worker.dispose(); + } + } finally { + exec.shutdown(); + } + } + + @Test + public void nonInterruptibleDirectTaskTimed() throws Exception { + ExecutorService exec = Executors.newSingleThreadExecutor(); + try { + Scheduler scheduler = Schedulers.from(exec, false); + + final AtomicInteger sync = new AtomicInteger(2); + + final AtomicBoolean isInterrupted = new AtomicBoolean(); + + Disposable d = scheduler.scheduleDirect(new Runnable() { + @Override + public void run() { + if (sync.decrementAndGet() != 0) { + while (sync.get() != 0) { } + } + try { + Thread.sleep(1000); + } catch (InterruptedException ex) { + isInterrupted.set(true); + } + } + }, 1, TimeUnit.MILLISECONDS); + + if (sync.decrementAndGet() != 0) { + while (sync.get() != 0) { } + } + + Thread.sleep(500); + + d.dispose(); + + int i = 20; + while (i-- > 0 && !isInterrupted.get()) { + Thread.sleep(50); + } + + assertFalse("Interruption happened", isInterrupted.get()); + } finally { + exec.shutdown(); + } + } + + @Test + public void nonInterruptibleWorkerTaskTimed() throws Exception { + ExecutorService exec = Executors.newSingleThreadExecutor(); + try { + Scheduler scheduler = Schedulers.from(exec, false); + + Worker worker = scheduler.createWorker(); + + try { + final AtomicInteger sync = new AtomicInteger(2); + + final AtomicBoolean isInterrupted = new AtomicBoolean(); + + Disposable d = worker.schedule(new Runnable() { + @Override + public void run() { + if (sync.decrementAndGet() != 0) { + while (sync.get() != 0) { } + } + try { + Thread.sleep(1000); + } catch (InterruptedException ex) { + isInterrupted.set(true); + } + } + }, 1, TimeUnit.MILLISECONDS); + + if (sync.decrementAndGet() != 0) { + while (sync.get() != 0) { } + } + + Thread.sleep(500); + + d.dispose(); + + int i = 20; + while (i-- > 0 && !isInterrupted.get()) { + Thread.sleep(50); + } + + assertFalse("Interruption happened", isInterrupted.get()); + } finally { + worker.dispose(); + } + } finally { + exec.shutdown(); + } + } + + @Test + public void nonInterruptibleDirectTaskScheduledExecutorTimed() throws Exception { + ScheduledExecutorService exec = Executors.newScheduledThreadPool(1); + try { + Scheduler scheduler = Schedulers.from(exec, false); + + final AtomicInteger sync = new AtomicInteger(2); + + final AtomicBoolean isInterrupted = new AtomicBoolean(); + + Disposable d = scheduler.scheduleDirect(new Runnable() { + @Override + public void run() { + if (sync.decrementAndGet() != 0) { + while (sync.get() != 0) { } + } + try { + Thread.sleep(1000); + } catch (InterruptedException ex) { + isInterrupted.set(true); + } + } + }, 1, TimeUnit.MILLISECONDS); + + if (sync.decrementAndGet() != 0) { + while (sync.get() != 0) { } + } + + Thread.sleep(500); + + d.dispose(); + + int i = 20; + while (i-- > 0 && !isInterrupted.get()) { + Thread.sleep(50); + } + + assertFalse("Interruption happened", isInterrupted.get()); + } finally { + exec.shutdown(); + } + } + + @Test + public void nonInterruptibleWorkerTaskScheduledExecutorTimed() throws Exception { + ScheduledExecutorService exec = Executors.newScheduledThreadPool(1); + try { + Scheduler scheduler = Schedulers.from(exec, false); + + Worker worker = scheduler.createWorker(); + + try { + final AtomicInteger sync = new AtomicInteger(2); + + final AtomicBoolean isInterrupted = new AtomicBoolean(); + + Disposable d = worker.schedule(new Runnable() { + @Override + public void run() { + if (sync.decrementAndGet() != 0) { + while (sync.get() != 0) { } + } + try { + Thread.sleep(1000); + } catch (InterruptedException ex) { + isInterrupted.set(true); + } + } + }, 1, TimeUnit.MILLISECONDS); + + if (sync.decrementAndGet() != 0) { + while (sync.get() != 0) { } + } + + Thread.sleep(500); + + d.dispose(); + + int i = 20; + while (i-- > 0 && !isInterrupted.get()) { + Thread.sleep(50); + } + + assertFalse("Interruption happened", isInterrupted.get()); + } finally { + worker.dispose(); + } + } finally { + exec.shutdown(); + } + } + + public static class TrackInterruptScheduledExecutor extends ScheduledThreadPoolExecutor { + + public final AtomicBoolean interruptReceived = new AtomicBoolean(); + + public TrackInterruptScheduledExecutor() { + super(10); + } + + @Override + public <V> ScheduledFuture<V> schedule(Callable<V> callable, long delay, TimeUnit unit) { + return new TrackingScheduledFuture<V>(super.schedule(callable, delay, unit)); + } + + class TrackingScheduledFuture<V> implements ScheduledFuture<V> { + + ScheduledFuture<V> original; + + TrackingScheduledFuture(ScheduledFuture<V> original) { + this.original = original; + } + + @Override + public long getDelay(TimeUnit unit) { + return original.getDelay(unit); + } + + @Override + public int compareTo(Delayed o) { + return original.compareTo(o); + } + + @Override + public boolean cancel(boolean mayInterruptIfRunning) { + if (mayInterruptIfRunning) { + interruptReceived.set(true); + } + return original.cancel(mayInterruptIfRunning); + } + + @Override + public boolean isCancelled() { + return original.isCancelled(); + } + + @Override + public boolean isDone() { + return original.isDone(); + } + + @Override + public V get() throws InterruptedException, ExecutionException { + return original.get(); + } + + @Override + public V get(long timeout, TimeUnit unit) + throws InterruptedException, ExecutionException, TimeoutException { + return get(timeout, unit); + } + } + } + + @Test + public void noInterruptBeforeRunningDelayedWorker() throws Throwable { + TrackInterruptScheduledExecutor exec = new TrackInterruptScheduledExecutor(); + + try { + Scheduler sch = Schedulers.from(exec, false); + + Worker worker = sch.createWorker(); + + Disposable d = worker.schedule(() -> { }, 1, TimeUnit.SECONDS); + + d.dispose(); + + int i = 150; + + while (i-- > 0) { + assertFalse("Task interrupt detected", exec.interruptReceived.get()); + Thread.sleep(10); + } + + } finally { + exec.shutdownNow(); + } + } + + @Test + public void hasInterruptBeforeRunningDelayedWorker() throws Throwable { + TrackInterruptScheduledExecutor exec = new TrackInterruptScheduledExecutor(); + + try { + Scheduler sch = Schedulers.from(exec, true); + + Worker worker = sch.createWorker(); + + Disposable d = worker.schedule(() -> { }, 1, TimeUnit.SECONDS); + + d.dispose(); + + Thread.sleep(100); + assertTrue("Task interrupt detected", exec.interruptReceived.get()); + + } finally { + exec.shutdownNow(); + } + } + + @Test + public void noInterruptAfterRunningDelayedWorker() throws Throwable { + TrackInterruptScheduledExecutor exec = new TrackInterruptScheduledExecutor(); + + try { + Scheduler sch = Schedulers.from(exec, false); + + Worker worker = sch.createWorker(); + AtomicBoolean taskRun = new AtomicBoolean(); + + Disposable d = worker.schedule(() -> { + taskRun.set(true); + try { + Thread.sleep(1000); + } catch (InterruptedException ex) { + exec.interruptReceived.set(true); + } + }, 100, TimeUnit.MILLISECONDS); + + Thread.sleep(150); + ; + d.dispose(); + + int i = 50; + + while (i-- > 0) { + assertFalse("Task interrupt detected", exec.interruptReceived.get()); + Thread.sleep(10); + } + + assertTrue("Task run at all", taskRun.get()); + + } finally { + exec.shutdownNow(); + } + } +} diff --git a/src/test/java/io/reactivex/rxjava3/schedulers/ExecutorSchedulerTest.java b/src/test/java/io/reactivex/rxjava3/schedulers/ExecutorSchedulerTest.java new file mode 100644 index 0000000000..bb3e759884 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/schedulers/ExecutorSchedulerTest.java @@ -0,0 +1,550 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.schedulers; + +import static org.junit.Assert.*; + +import java.lang.management.*; +import java.util.List; +import java.util.concurrent.*; +import java.util.concurrent.atomic.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.Scheduler; +import io.reactivex.rxjava3.core.Scheduler.Worker; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.disposables.*; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.internal.schedulers.*; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class ExecutorSchedulerTest extends AbstractSchedulerConcurrencyTests { + + static final Executor executor = Executors.newFixedThreadPool(2, new RxThreadFactory("TestCustomPool")); + + @Override + protected Scheduler getScheduler() { + return Schedulers.from(executor); + } + + @Test + public final void handledErrorIsNotDeliveredToThreadHandler() throws InterruptedException { + SchedulerTestHelper.handledErrorIsNotDeliveredToThreadHandler(getScheduler()); + } + + public static void cancelledRetention(Scheduler.Worker w, boolean periodic) throws InterruptedException { + System.out.println("Wait before GC"); + Thread.sleep(1000); + + System.out.println("GC"); + System.gc(); + + Thread.sleep(1000); + + MemoryMXBean memoryMXBean = ManagementFactory.getMemoryMXBean(); + long initial = memoryMXBean.getHeapMemoryUsage().getUsed(); + + System.out.printf("Starting: %.3f MB%n", initial / 1024.0 / 1024.0); + + int n = 100 * 1000; + if (periodic) { + final CountDownLatch cdl = new CountDownLatch(n); + final Runnable action = new Runnable() { + @Override + public void run() { + cdl.countDown(); + } + }; + for (int i = 0; i < n; i++) { + if (i % 50000 == 0) { + System.out.println(" -> still scheduling: " + i); + } + w.schedulePeriodically(action, 0, 1, TimeUnit.DAYS); + } + + System.out.println("Waiting for the first round to finish..."); + cdl.await(); + } else { + for (int i = 0; i < n; i++) { + if (i % 50000 == 0) { + System.out.println(" -> still scheduling: " + i); + } + w.schedule(Functions.EMPTY_RUNNABLE, 1, TimeUnit.DAYS); + } + } + + long after = memoryMXBean.getHeapMemoryUsage().getUsed(); + System.out.printf("Peak: %.3f MB%n", after / 1024.0 / 1024.0); + + w.dispose(); + + System.out.println("Wait before second GC"); + System.out.println("JDK 6 purge is N log N because it removes and shifts one by one"); + int t = (int)(n * Math.log(n) / 100) + 1000; + int sleepStep = 100; + while (t > 0) { + System.out.printf(" >> Waiting for purge: %.2f s remaining%n", t / 1000d); + + System.gc(); + + long finish = memoryMXBean.getHeapMemoryUsage().getUsed(); + System.out.printf("After: %.3f MB%n", finish / 1024.0 / 1024.0); + if (finish <= initial * 5) { + break; + } + + Thread.sleep(sleepStep); + t -= sleepStep; + } + + System.out.println("Second GC"); + System.gc(); + + t = 2000; + long finish = memoryMXBean.getHeapMemoryUsage().getUsed(); + + while (t > 0) { + System.out.printf("After: %.3f MB%n", finish / 1024.0 / 1024.0); + + if (finish <= initial * 5) { + return; + } + Thread.sleep(sleepStep); + t -= sleepStep; + finish = memoryMXBean.getHeapMemoryUsage().getUsed(); + } + + fail(String.format("Tasks retained: %.3f -> %.3f -> %.3f", initial / 1024 / 1024.0, after / 1024 / 1024.0, finish / 1024 / 1024d)); + } + + @Test + public void cancelledTaskRetention() throws InterruptedException { + ExecutorService exec = Executors.newSingleThreadExecutor(); + Scheduler s = Schedulers.from(exec); + try { + Scheduler.Worker w = s.createWorker(); + try { + cancelledRetention(w, false); + } finally { + w.dispose(); + } + + w = s.createWorker(); + try { + cancelledRetention(w, true); + } finally { + w.dispose(); + } + } finally { + exec.shutdownNow(); + } + } + + /** A simple executor which queues tasks and executes them one-by-one if executeOne() is called. */ + static final class TestExecutor implements Executor { + final ConcurrentLinkedQueue<Runnable> queue = new ConcurrentLinkedQueue<>(); + @Override + public void execute(Runnable command) { + queue.offer(command); + } + public void executeOne() { + Runnable r = queue.poll(); + if (r != null) { + r.run(); + } + } + public void executeAll() { + Runnable r; + while ((r = queue.poll()) != null) { + r.run(); + } + } + } + + @Test + public void cancelledTasksDontRun() { + final AtomicInteger calls = new AtomicInteger(); + Runnable task = new Runnable() { + @Override + public void run() { + calls.getAndIncrement(); + } + }; + TestExecutor exec = new TestExecutor(); + Scheduler custom = Schedulers.from(exec); + Worker w = custom.createWorker(); + try { + Disposable d1 = w.schedule(task); + Disposable d2 = w.schedule(task); + Disposable d3 = w.schedule(task); + + d1.dispose(); + d2.dispose(); + d3.dispose(); + + exec.executeAll(); + + assertEquals(0, calls.get()); + } finally { + w.dispose(); + } + } + + @Test + public void cancelledWorkerDoesntRunTasks() { + final AtomicInteger calls = new AtomicInteger(); + Runnable task = new Runnable() { + @Override + public void run() { + calls.getAndIncrement(); + } + }; + TestExecutor exec = new TestExecutor(); + Scheduler custom = Schedulers.from(exec); + Worker w = custom.createWorker(); + try { + w.schedule(task); + w.schedule(task); + w.schedule(task); + } finally { + w.dispose(); + } + exec.executeAll(); + assertEquals(0, calls.get()); + } + + @Test + public void plainExecutor() throws Exception { + Scheduler s = Schedulers.from(new Executor() { + @Override + public void execute(Runnable r) { + r.run(); + } + }); + + final CountDownLatch cdl = new CountDownLatch(5); + + Runnable r = new Runnable() { + @Override + public void run() { + cdl.countDown(); + } + }; + + s.scheduleDirect(r); + + s.scheduleDirect(r, 50, TimeUnit.MILLISECONDS); + + Disposable d = s.schedulePeriodicallyDirect(r, 10, 10, TimeUnit.MILLISECONDS); + + try { + assertTrue(cdl.await(5, TimeUnit.SECONDS)); + } finally { + d.dispose(); + } + + assertTrue(d.isDisposed()); + } + + @Test + public void rejectingExecutor() { + ScheduledExecutorService exec = Executors.newSingleThreadScheduledExecutor(); + exec.shutdown(); + + Scheduler s = Schedulers.from(exec); + + List<Throwable> errors = TestHelper.trackPluginErrors(); + + try { + assertSame(EmptyDisposable.INSTANCE, s.scheduleDirect(Functions.EMPTY_RUNNABLE)); + + assertSame(EmptyDisposable.INSTANCE, s.scheduleDirect(Functions.EMPTY_RUNNABLE, 10, TimeUnit.MILLISECONDS)); + + assertSame(EmptyDisposable.INSTANCE, s.schedulePeriodicallyDirect(Functions.EMPTY_RUNNABLE, 10, 10, TimeUnit.MILLISECONDS)); + + TestHelper.assertUndeliverable(errors, 0, RejectedExecutionException.class); + TestHelper.assertUndeliverable(errors, 1, RejectedExecutionException.class); + TestHelper.assertUndeliverable(errors, 2, RejectedExecutionException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void rejectingExecutorWorker() { + ScheduledExecutorService exec = Executors.newSingleThreadScheduledExecutor(); + exec.shutdown(); + + List<Throwable> errors = TestHelper.trackPluginErrors(); + + try { + Worker s = Schedulers.from(exec).createWorker(); + assertSame(EmptyDisposable.INSTANCE, s.schedule(Functions.EMPTY_RUNNABLE)); + + s = Schedulers.from(exec).createWorker(); + assertSame(EmptyDisposable.INSTANCE, s.schedule(Functions.EMPTY_RUNNABLE, 10, TimeUnit.MILLISECONDS)); + + s = Schedulers.from(exec).createWorker(); + assertSame(EmptyDisposable.INSTANCE, s.schedulePeriodically(Functions.EMPTY_RUNNABLE, 10, 10, TimeUnit.MILLISECONDS)); + + TestHelper.assertUndeliverable(errors, 0, RejectedExecutionException.class); + TestHelper.assertUndeliverable(errors, 1, RejectedExecutionException.class); + TestHelper.assertUndeliverable(errors, 2, RejectedExecutionException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void reuseScheduledExecutor() throws Exception { + ScheduledExecutorService exec = Executors.newSingleThreadScheduledExecutor(); + + try { + Scheduler s = Schedulers.from(exec); + + final CountDownLatch cdl = new CountDownLatch(8); + + Runnable r = new Runnable() { + @Override + public void run() { + cdl.countDown(); + } + }; + + s.scheduleDirect(r); + + s.scheduleDirect(r, 10, TimeUnit.MILLISECONDS); + + Disposable d = s.schedulePeriodicallyDirect(r, 10, 10, TimeUnit.MILLISECONDS); + + try { + assertTrue(cdl.await(5, TimeUnit.SECONDS)); + } finally { + d.dispose(); + } + } finally { + exec.shutdown(); + } + } + + @Test + public void reuseScheduledExecutorAsWorker() throws Exception { + ScheduledExecutorService exec = Executors.newSingleThreadScheduledExecutor(); + + Worker s = Schedulers.from(exec).createWorker(); + + assertFalse(s.isDisposed()); + try { + + final CountDownLatch cdl = new CountDownLatch(8); + + Runnable r = new Runnable() { + @Override + public void run() { + cdl.countDown(); + } + }; + + s.schedule(r); + + s.schedule(r, 10, TimeUnit.MILLISECONDS); + + Disposable d = s.schedulePeriodically(r, 10, 10, TimeUnit.MILLISECONDS); + + try { + assertTrue(cdl.await(5, TimeUnit.SECONDS)); + } finally { + d.dispose(); + } + } finally { + s.dispose(); + exec.shutdown(); + } + + assertTrue(s.isDisposed()); + } + + @Test + public void disposeRace() { + ExecutorService exec = Executors.newSingleThreadExecutor(); + final Scheduler s = Schedulers.from(exec); + try { + for (int i = 0; i < 500; i++) { + final Worker w = s.createWorker(); + + final AtomicInteger c = new AtomicInteger(2); + + w.schedule(new Runnable() { + @Override + public void run() { + c.decrementAndGet(); + while (c.get() != 0) { } + } + }); + + c.decrementAndGet(); + while (c.get() != 0) { } + w.dispose(); + } + } finally { + exec.shutdownNow(); + } + } + + @Test + public void runnableDisposed() { + final Scheduler s = Schedulers.from(new Executor() { + @Override + public void execute(Runnable r) { + r.run(); + } + }); + Disposable d = s.scheduleDirect(Functions.EMPTY_RUNNABLE); + + assertTrue(d.isDisposed()); + } + + @Test + public void runnableDisposedAsync() throws Exception { + final Scheduler s = Schedulers.from(new Executor() { + @Override + public void execute(Runnable r) { + new Thread(r).start(); + } + }); + Disposable d = s.scheduleDirect(Functions.EMPTY_RUNNABLE); + + while (!d.isDisposed()) { + Thread.sleep(1); + } + } + + @Test + public void runnableDisposedAsync2() throws Exception { + final Scheduler s = Schedulers.from(executor); + Disposable d = s.scheduleDirect(Functions.EMPTY_RUNNABLE); + + while (!d.isDisposed()) { + Thread.sleep(1); + } + } + + @Test + public void runnableDisposedAsyncCrash() throws Exception { + final Scheduler s = Schedulers.from(new Executor() { + @Override + public void execute(Runnable r) { + new Thread(r).start(); + } + }); + Disposable d = s.scheduleDirect(new Runnable() { + @Override + public void run() { + throw new IllegalStateException(); + } + }); + + while (!d.isDisposed()) { + Thread.sleep(1); + } + } + + @Test + public void runnableDisposedAsyncTimed() throws Exception { + final Scheduler s = Schedulers.from(new Executor() { + @Override + public void execute(Runnable r) { + new Thread(r).start(); + } + }); + Disposable d = s.scheduleDirect(Functions.EMPTY_RUNNABLE, 1, TimeUnit.MILLISECONDS); + + while (!d.isDisposed()) { + Thread.sleep(1); + } + } + + @Test + public void runnableDisposedAsyncTimed2() throws Exception { + ExecutorService executorScheduler = Executors.newScheduledThreadPool(1, new RxThreadFactory("TestCustomPoolTimed")); + try { + final Scheduler s = Schedulers.from(executorScheduler); + Disposable d = s.scheduleDirect(Functions.EMPTY_RUNNABLE, 1, TimeUnit.MILLISECONDS); + + while (!d.isDisposed()) { + Thread.sleep(1); + } + } finally { + executorScheduler.shutdownNow(); + } + } + + @Test + public void unwrapScheduleDirectTaskAfterDispose() { + Scheduler scheduler = getScheduler(); + final CountDownLatch cdl = new CountDownLatch(1); + Runnable countDownRunnable = new Runnable() { + @Override + public void run() { + cdl.countDown(); + } + }; + Disposable disposable = scheduler.scheduleDirect(countDownRunnable, 100, TimeUnit.MILLISECONDS); + SchedulerRunnableIntrospection wrapper = (SchedulerRunnableIntrospection) disposable; + assertSame(countDownRunnable, wrapper.getWrappedRunnable()); + disposable.dispose(); + + assertSame(Functions.EMPTY_RUNNABLE, wrapper.getWrappedRunnable()); + } + + @Test + public void interruptibleRunnableRunDisposeRace() { + ExecutorService exec = Executors.newSingleThreadExecutor(); + try { + Scheduler s = Schedulers.from(r -> exec.execute(r), true); + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + SequentialDisposable sd = new SequentialDisposable(); + + TestHelper.race( + () -> sd.update(s.scheduleDirect(() -> { })), + () -> sd.dispose() + ); + } + } finally { + exec.shutdown(); + } + } + + @Test + public void interruptibleRunnableRunDispose() { + try { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + AtomicReference<Runnable> runRef = new AtomicReference<>(); + Scheduler s = Schedulers.from(r -> { + runRef.set(r); + }, true); + + Disposable d = s.scheduleDirect(() -> { }); + TestHelper.race( + () -> runRef.get().run(), + () -> d.dispose() + ); + } + } finally { + Thread.interrupted(); + } + } +} diff --git a/src/test/java/io/reactivex/rxjava3/schedulers/FailOnBlockingTest.java b/src/test/java/io/reactivex/rxjava3/schedulers/FailOnBlockingTest.java new file mode 100644 index 0000000000..7cb4c15e07 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/schedulers/FailOnBlockingTest.java @@ -0,0 +1,682 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.schedulers; + +import java.util.concurrent.TimeUnit; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +public class FailOnBlockingTest extends RxJavaTest { + + @Test + public void failComputationFlowableBlockingFirst() { + + try { + RxJavaPlugins.setFailOnNonBlockingScheduler(true); + + Flowable.just(1) + .subscribeOn(Schedulers.computation()) + .map(new Function<Integer, Integer>() { + @Override + public Integer apply(Integer v) throws Exception { + + Flowable.just(1).delay(10, TimeUnit.SECONDS).blockingFirst(); + + return v; + } + }) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertFailure(IllegalStateException.class); + } finally { + RxJavaPlugins.reset(); + } + + } + + @Test + public void failComputationFlowableBlockingLast() { + + try { + RxJavaPlugins.setFailOnNonBlockingScheduler(true); + + Flowable.just(1) + .subscribeOn(Schedulers.computation()) + .map(new Function<Integer, Integer>() { + @Override + public Integer apply(Integer v) throws Exception { + + Flowable.just(1).delay(10, TimeUnit.SECONDS).blockingLast(); + + return v; + } + }) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertFailure(IllegalStateException.class); + } finally { + RxJavaPlugins.reset(); + } + + } + + @Test + public void failComputationFlowableBlockingIterable() { + + try { + RxJavaPlugins.setFailOnNonBlockingScheduler(true); + + Flowable.just(1) + .subscribeOn(Schedulers.computation()) + .map(new Function<Integer, Integer>() { + @Override + public Integer apply(Integer v) throws Exception { + + Flowable.just(1).delay(10, TimeUnit.SECONDS).blockingIterable().iterator().next(); + + return v; + } + }) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertFailure(IllegalStateException.class); + } finally { + RxJavaPlugins.reset(); + } + + } + + @Test + public void failComputationFlowableBlockingSubscribe() { + + try { + RxJavaPlugins.setFailOnNonBlockingScheduler(true); + + Flowable.just(1) + .subscribeOn(Schedulers.computation()) + .map(new Function<Integer, Integer>() { + @Override + public Integer apply(Integer v) throws Exception { + + Flowable.just(1).delay(10, TimeUnit.SECONDS).blockingSubscribe(); + + return v; + } + }) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertFailure(IllegalStateException.class); + } finally { + RxJavaPlugins.reset(); + } + + } + + @Test + public void failComputationFlowableBlockingSingle() { + + try { + RxJavaPlugins.setFailOnNonBlockingScheduler(true); + + Flowable.just(1) + .subscribeOn(Schedulers.computation()) + .map(new Function<Integer, Integer>() { + @Override + public Integer apply(Integer v) throws Exception { + + Flowable.just(1).delay(10, TimeUnit.SECONDS).blockingSingle(); + + return v; + } + }) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertFailure(IllegalStateException.class); + } finally { + RxJavaPlugins.reset(); + } + + } + + @Test + public void failComputationFlowableBlockingForEach() { + + try { + RxJavaPlugins.setFailOnNonBlockingScheduler(true); + + Flowable.just(1) + .subscribeOn(Schedulers.computation()) + .map(new Function<Integer, Integer>() { + @Override + public Integer apply(Integer v) throws Exception { + + Flowable.just(1).delay(10, TimeUnit.SECONDS).blockingForEach(Functions.emptyConsumer()); + + return v; + } + }) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertFailure(IllegalStateException.class); + } finally { + RxJavaPlugins.reset(); + } + + } + + @Test + public void failComputationFlowableBlockingLatest() { + + try { + RxJavaPlugins.setFailOnNonBlockingScheduler(true); + + Flowable.just(1) + .subscribeOn(Schedulers.computation()) + .map(new Function<Integer, Integer>() { + @Override + public Integer apply(Integer v) throws Exception { + + Flowable.just(1).delay(10, TimeUnit.SECONDS).blockingLatest().iterator().hasNext(); + + return v; + } + }) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertFailure(IllegalStateException.class); + } finally { + RxJavaPlugins.reset(); + } + + } + + @Test + public void failComputationFlowableBlockingNext() { + + try { + RxJavaPlugins.setFailOnNonBlockingScheduler(true); + + Flowable.just(1) + .subscribeOn(Schedulers.computation()) + .map(new Function<Integer, Integer>() { + @Override + public Integer apply(Integer v) throws Exception { + + Flowable.just(1).delay(10, TimeUnit.SECONDS).blockingNext().iterator().hasNext(); + + return v; + } + }) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertFailure(IllegalStateException.class); + } finally { + RxJavaPlugins.reset(); + } + + } + + @Test + public void failComputationFlowableToFuture() { + + try { + RxJavaPlugins.setFailOnNonBlockingScheduler(true); + + Flowable.just(1) + .subscribeOn(Schedulers.computation()) + .map(new Function<Integer, Integer>() { + @Override + public Integer apply(Integer v) throws Exception { + + Flowable.just(1).delay(10, TimeUnit.SECONDS).toFuture().get(); + + return v; + } + }) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertFailure(IllegalStateException.class); + } finally { + RxJavaPlugins.reset(); + } + + } + + @Test + public void failComputationObservableBlockingFirst() { + + try { + RxJavaPlugins.setFailOnNonBlockingScheduler(true); + + Observable.just(1) + .subscribeOn(Schedulers.computation()) + .map(new Function<Integer, Integer>() { + @Override + public Integer apply(Integer v) throws Exception { + + Observable.just(1).delay(10, TimeUnit.SECONDS).blockingFirst(); + + return v; + } + }) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertFailure(IllegalStateException.class); + } finally { + RxJavaPlugins.reset(); + } + + } + + @Test + public void failComputationObservableBlockingLast() { + + try { + RxJavaPlugins.setFailOnNonBlockingScheduler(true); + + Observable.just(1) + .subscribeOn(Schedulers.computation()) + .map(new Function<Integer, Integer>() { + @Override + public Integer apply(Integer v) throws Exception { + + Flowable.just(1).delay(10, TimeUnit.SECONDS).blockingLast(); + + return v; + } + }) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertFailure(IllegalStateException.class); + } finally { + RxJavaPlugins.reset(); + } + + } + + @Test + public void failComputationObservableBlockingIterable() { + + try { + RxJavaPlugins.setFailOnNonBlockingScheduler(true); + + Observable.just(1) + .subscribeOn(Schedulers.computation()) + .map(new Function<Integer, Integer>() { + @Override + public Integer apply(Integer v) throws Exception { + + Observable.just(1).delay(10, TimeUnit.SECONDS).blockingIterable().iterator().next(); + + return v; + } + }) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertFailure(IllegalStateException.class); + } finally { + RxJavaPlugins.reset(); + } + + } + + @Test + public void failComputationObservableBlockingSubscribe() { + + try { + RxJavaPlugins.setFailOnNonBlockingScheduler(true); + + Observable.just(1) + .subscribeOn(Schedulers.computation()) + .map(new Function<Integer, Integer>() { + @Override + public Integer apply(Integer v) throws Exception { + + Observable.just(1).delay(10, TimeUnit.SECONDS).blockingSubscribe(); + + return v; + } + }) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertFailure(IllegalStateException.class); + } finally { + RxJavaPlugins.reset(); + } + + } + + @Test + public void failComputationObservableBlockingSingle() { + + try { + RxJavaPlugins.setFailOnNonBlockingScheduler(true); + + Observable.just(1) + .subscribeOn(Schedulers.computation()) + .map(new Function<Integer, Integer>() { + @Override + public Integer apply(Integer v) throws Exception { + + Observable.just(1).delay(10, TimeUnit.SECONDS).blockingSingle(); + + return v; + } + }) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertFailure(IllegalStateException.class); + } finally { + RxJavaPlugins.reset(); + } + + } + + @Test + public void failComputationObservableBlockingForEach() { + + try { + RxJavaPlugins.setFailOnNonBlockingScheduler(true); + + Observable.just(1) + .subscribeOn(Schedulers.computation()) + .map(new Function<Integer, Integer>() { + @Override + public Integer apply(Integer v) throws Exception { + + Observable.just(1).delay(10, TimeUnit.SECONDS).blockingForEach(Functions.emptyConsumer()); + + return v; + } + }) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertFailure(IllegalStateException.class); + } finally { + RxJavaPlugins.reset(); + } + + } + + @Test + public void failComputationObservableBlockingLatest() { + + try { + RxJavaPlugins.setFailOnNonBlockingScheduler(true); + + Observable.just(1) + .subscribeOn(Schedulers.computation()) + .map(new Function<Integer, Integer>() { + @Override + public Integer apply(Integer v) throws Exception { + + Observable.just(1).delay(10, TimeUnit.SECONDS).blockingLatest().iterator().hasNext(); + + return v; + } + }) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertFailure(IllegalStateException.class); + } finally { + RxJavaPlugins.reset(); + } + + } + + @Test + public void failComputationObservableBlockingNext() { + + try { + RxJavaPlugins.setFailOnNonBlockingScheduler(true); + + Observable.just(1) + .subscribeOn(Schedulers.computation()) + .map(new Function<Integer, Integer>() { + @Override + public Integer apply(Integer v) throws Exception { + + Observable.just(1).delay(10, TimeUnit.SECONDS).blockingNext().iterator().hasNext(); + + return v; + } + }) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertFailure(IllegalStateException.class); + } finally { + RxJavaPlugins.reset(); + } + + } + + @Test + public void failComputationObservableToFuture() { + + try { + RxJavaPlugins.setFailOnNonBlockingScheduler(true); + + Observable.just(1) + .subscribeOn(Schedulers.computation()) + .map(new Function<Integer, Integer>() { + @Override + public Integer apply(Integer v) throws Exception { + + Observable.just(1).delay(10, TimeUnit.SECONDS).toFuture().get(); + + return v; + } + }) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertFailure(IllegalStateException.class); + } finally { + RxJavaPlugins.reset(); + } + + } + + @Test + public void failSingleObservableBlockingFirst() { + + try { + RxJavaPlugins.setFailOnNonBlockingScheduler(true); + + Observable.just(1) + .subscribeOn(Schedulers.single()) + .map(new Function<Integer, Integer>() { + @Override + public Integer apply(Integer v) throws Exception { + + Observable.just(1).delay(10, TimeUnit.SECONDS).blockingFirst(); + + return v; + } + }) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertFailure(IllegalStateException.class); + } finally { + RxJavaPlugins.reset(); + } + + } + + @Test + public void failSingleSingleBlockingGet() { + + try { + RxJavaPlugins.setFailOnNonBlockingScheduler(true); + + Single.just(1) + .subscribeOn(Schedulers.single()) + .map(new Function<Integer, Integer>() { + @Override + public Integer apply(Integer v) throws Exception { + + Single.just(1).delay(10, TimeUnit.SECONDS).blockingGet(); + + return v; + } + }) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertFailure(IllegalStateException.class); + } finally { + RxJavaPlugins.reset(); + } + + } + + @Test + public void failSingleMaybeBlockingGet() { + + try { + RxJavaPlugins.setFailOnNonBlockingScheduler(true); + + Maybe.just(1) + .subscribeOn(Schedulers.single()) + .map(new Function<Integer, Integer>() { + @Override + public Integer apply(Integer v) throws Exception { + + Maybe.just(1).delay(10, TimeUnit.SECONDS).blockingGet(); + + return v; + } + }) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertFailure(IllegalStateException.class); + } finally { + RxJavaPlugins.reset(); + } + + } + + @Test + public void failSingleCompletableBlockingGet() { + + try { + RxJavaPlugins.setFailOnNonBlockingScheduler(true); + + Completable.complete() + .subscribeOn(Schedulers.single()) + .doOnComplete(new Action() { + @Override + public void run() throws Exception { + Completable.complete().delay(10, TimeUnit.SECONDS).blockingAwait(); + } + }) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertFailure(IllegalStateException.class); + } finally { + RxJavaPlugins.reset(); + } + + } + + @Test + public void failSingleCompletableBlockingAwait() { + + try { + RxJavaPlugins.setFailOnNonBlockingScheduler(true); + + Completable.complete() + .subscribeOn(Schedulers.single()) + .doOnComplete(new Action() { + @Override + public void run() throws Exception { + Completable.complete().delay(10, TimeUnit.SECONDS).blockingAwait(); + } + }) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertFailure(IllegalStateException.class); + } finally { + RxJavaPlugins.reset(); + } + + } + + @Test + public void dontfailIOObservableBlockingFirst() { + + try { + RxJavaPlugins.setFailOnNonBlockingScheduler(true); + + Observable.just(1) + .subscribeOn(Schedulers.io()) + .map(new Function<Integer, Integer>() { + @Override + public Integer apply(Integer v) throws Exception { + return Observable.just(2).delay(100, TimeUnit.MILLISECONDS).blockingFirst(); + } + }) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(2); + } finally { + RxJavaPlugins.reset(); + } + + } + + @Test + public void failWithCustomHandler() { + try { + RxJavaPlugins.setOnBeforeBlocking(new BooleanSupplier() { + @Override + public boolean getAsBoolean() throws Exception { + return true; + } + }); + RxJavaPlugins.setFailOnNonBlockingScheduler(true); + + Flowable.just(1) + .map(new Function<Integer, Integer>() { + @Override + public Integer apply(Integer v) throws Exception { + + Flowable.just(1).delay(10, TimeUnit.SECONDS).blockingLast(); + + return v; + } + }) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertFailure(IllegalStateException.class); + + } finally { + RxJavaPlugins.reset(); + } + + Flowable.just(1) + .map(new Function<Integer, Integer>() { + @Override + public Integer apply(Integer v) throws Exception { + return Flowable.just(2).delay(100, TimeUnit.MILLISECONDS).blockingLast(); + } + }) + .test() + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(2); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/schedulers/NewThreadSchedulerTest.java b/src/test/java/io/reactivex/rxjava3/schedulers/NewThreadSchedulerTest.java new file mode 100644 index 0000000000..37c7b85f87 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/schedulers/NewThreadSchedulerTest.java @@ -0,0 +1,94 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.schedulers; + +import static org.junit.Assert.*; + +import java.util.concurrent.TimeUnit; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.Scheduler; +import io.reactivex.rxjava3.core.Scheduler.Worker; +import io.reactivex.rxjava3.disposables.*; +import io.reactivex.rxjava3.internal.schedulers.NewThreadWorker; +import io.reactivex.rxjava3.testsupport.SuppressUndeliverable; + +public class NewThreadSchedulerTest extends AbstractSchedulerConcurrencyTests { + + @Override + protected Scheduler getScheduler() { + return Schedulers.newThread(); + } + + @Test + public final void handledErrorIsNotDeliveredToThreadHandler() throws InterruptedException { + SchedulerTestHelper.handledErrorIsNotDeliveredToThreadHandler(getScheduler()); + } + + @Test + @SuppressUndeliverable + public void shutdownRejects() { + final int[] calls = { 0 }; + + Runnable r = new Runnable() { + @Override + public void run() { + calls[0]++; + } + }; + + Scheduler s = getScheduler(); + Worker w = s.createWorker(); + w.dispose(); + + assertTrue(w.isDisposed()); + + assertEquals(Disposable.disposed(), w.schedule(r)); + + assertEquals(Disposable.disposed(), w.schedule(r, 1, TimeUnit.SECONDS)); + + assertEquals(Disposable.disposed(), w.schedulePeriodically(r, 1, 1, TimeUnit.SECONDS)); + + NewThreadWorker actual = (NewThreadWorker)w; + + CompositeDisposable cd = new CompositeDisposable(); + + actual.scheduleActual(r, 1, TimeUnit.SECONDS, cd); + + assertEquals(0, cd.size()); + + assertEquals(0, calls[0]); + } + + /** + * Regression test to ensure there is no NPE when the worker has been disposed. + * @throws Exception on error + */ + @Test + @SuppressUndeliverable + public void npeRegression() throws Exception { + Scheduler s = getScheduler(); + NewThreadWorker w = (NewThreadWorker) s.createWorker(); + w.dispose(); + + //This method used to throw a NPE when the worker has been disposed and the parent is null + w.scheduleActual(new Runnable() { + @Override + public void run() { + } + }, 0, TimeUnit.MILLISECONDS, null); + + } +} diff --git a/src/test/java/io/reactivex/rxjava3/schedulers/SchedulerLifecycleTest.java b/src/test/java/io/reactivex/rxjava3/schedulers/SchedulerLifecycleTest.java new file mode 100644 index 0000000000..58b51254d8 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/schedulers/SchedulerLifecycleTest.java @@ -0,0 +1,136 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.schedulers; + +import static org.junit.Assert.fail; + +import java.util.*; +import java.util.concurrent.*; + +import org.junit.*; + +import io.reactivex.rxjava3.core.RxJavaTest; +import io.reactivex.rxjava3.core.Scheduler.Worker; +import io.reactivex.rxjava3.disposables.CompositeDisposable; + +public class SchedulerLifecycleTest extends RxJavaTest { + @Test + public void shutdown() throws InterruptedException { + tryOutSchedulers(); + + System.out.println("testShutdown >> Giving time threads to spin-up"); + Thread.sleep(500); + + Set<Thread> rxThreads = new HashSet<>(); + for (Thread t : Thread.getAllStackTraces().keySet()) { + if (t.getName().startsWith("Rx")) { + rxThreads.add(t); + } + } + Schedulers.shutdown(); + System.out.println("testShutdown >> Giving time to threads to stop"); + Thread.sleep(500); + + StringBuilder b = new StringBuilder(); + for (Thread t : rxThreads) { + if (t.isAlive()) { + b.append("Thread " + t + " failed to shutdown\r\n"); + for (StackTraceElement ste : t.getStackTrace()) { + b.append(" ").append(ste).append("\r\n"); + } + } + } + if (b.length() > 0) { + System.out.print(b); + System.out.println("testShutdown >> Restarting schedulers..."); + Schedulers.start(); // restart them anyways + fail("Rx Threads were still alive:\r\n" + b); + } + + System.out.println("testShutdown >> Restarting schedulers..."); + Schedulers.start(); + + tryOutSchedulers(); + } + + private void tryOutSchedulers() throws InterruptedException { + final CountDownLatch cdl = new CountDownLatch(4); + + final Runnable countAction = new Runnable() { + @Override + public void run() { + cdl.countDown(); + } + }; + + CompositeDisposable cd = new CompositeDisposable(); + + try { + Worker w1 = Schedulers.computation().createWorker(); + cd.add(w1); + w1.schedule(countAction); + + Worker w2 = Schedulers.io().createWorker(); + cd.add(w2); + w2.schedule(countAction); + + Worker w3 = Schedulers.newThread().createWorker(); + cd.add(w3); + w3.schedule(countAction); + + Worker w4 = Schedulers.single().createWorker(); + cd.add(w4); + w4.schedule(countAction); + + if (!cdl.await(3, TimeUnit.SECONDS)) { + fail("countAction was not run by every worker"); + } + } finally { + cd.dispose(); + } + } + + @Test + public void startIdempotence() throws InterruptedException { + tryOutSchedulers(); + + System.out.println("testStartIdempotence >> giving some time"); + Thread.sleep(500); + + Set<Thread> rxThreadsBefore = new HashSet<>(); + for (Thread t : Thread.getAllStackTraces().keySet()) { + if (t.getName().startsWith("Rx")) { + rxThreadsBefore.add(t); + System.out.println("testStartIdempotence >> " + t); + } + } + System.out.println("testStartIdempotence >> trying to start again"); + Schedulers.start(); + System.out.println("testStartIdempotence >> giving some time again"); + Thread.sleep(500); + + Set<Thread> rxThreadsAfter = new HashSet<>(); + for (Thread t : Thread.getAllStackTraces().keySet()) { + if (t.getName().startsWith("Rx")) { + rxThreadsAfter.add(t); + System.out.println("testStartIdempotence >>>> " + t); + } + } + + // cached threads may get dropped between the two checks + rxThreadsAfter.removeAll(rxThreadsBefore); + + Assert.assertTrue("Some new threads appeared: " + rxThreadsAfter, rxThreadsAfter.isEmpty()); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/schedulers/SchedulerTest.java b/src/test/java/io/reactivex/rxjava3/schedulers/SchedulerTest.java new file mode 100644 index 0000000000..a9ec2205a7 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/schedulers/SchedulerTest.java @@ -0,0 +1,382 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.schedulers; + +import static org.junit.Assert.*; + +import java.util.List; +import java.util.concurrent.TimeUnit; + +import org.junit.Test; + +import io.reactivex.rxjava3.annotations.NonNull; +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.core.Scheduler.Worker; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.internal.disposables.*; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class SchedulerTest extends RxJavaTest { + + @Test + public void defaultPeriodicTask() { + final int[] count = { 0 }; + + TestScheduler scheduler = new TestScheduler(); + + Disposable d = scheduler.schedulePeriodicallyDirect(new Runnable() { + @Override + public void run() { + count[0]++; + } + }, 100, 100, TimeUnit.MILLISECONDS); + + assertEquals(0, count[0]); + assertFalse(d.isDisposed()); + + scheduler.advanceTimeBy(200, TimeUnit.MILLISECONDS); + + assertEquals(2, count[0]); + + d.dispose(); + + assertTrue(d.isDisposed()); + + scheduler.advanceTimeBy(200, TimeUnit.MILLISECONDS); + + assertEquals(2, count[0]); + } + + @Test + public void periodicDirectThrows() throws Throwable { + TestHelper.withErrorTracking(errors -> { + TestScheduler scheduler = new TestScheduler(); + + try { + scheduler.schedulePeriodicallyDirect(new Runnable() { + @Override + public void run() { + throw new TestException(); + } + }, 100, 100, TimeUnit.MILLISECONDS); + + scheduler.advanceTimeBy(100, TimeUnit.MILLISECONDS); + + fail("Should have thrown!"); + } catch (TestException expected) { + // expected + } + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + }); + } + + @Test + public void disposePeriodicDirect() { + final int[] count = { 0 }; + + TestScheduler scheduler = new TestScheduler(); + + Disposable d = scheduler.schedulePeriodicallyDirect(new Runnable() { + @Override + public void run() { + count[0]++; + } + }, 100, 100, TimeUnit.MILLISECONDS); + + d.dispose(); + + assertEquals(0, count[0]); + assertTrue(d.isDisposed()); + + scheduler.advanceTimeBy(200, TimeUnit.MILLISECONDS); + + assertEquals(0, count[0]); + assertTrue(d.isDisposed()); + } + + @Test + public void scheduleDirect() { + final int[] count = { 0 }; + + TestScheduler scheduler = new TestScheduler(); + + scheduler.scheduleDirect(new Runnable() { + @Override + public void run() { + count[0]++; + } + }, 100, TimeUnit.MILLISECONDS); + + assertEquals(0, count[0]); + + scheduler.advanceTimeBy(200, TimeUnit.MILLISECONDS); + + assertEquals(1, count[0]); + } + + @Test + public void disposeSelfPeriodicDirect() { + final int[] count = { 0 }; + + TestScheduler scheduler = new TestScheduler(); + + final SequentialDisposable sd = new SequentialDisposable(); + + Disposable d = scheduler.schedulePeriodicallyDirect(new Runnable() { + @Override + public void run() { + count[0]++; + sd.dispose(); + } + }, 100, 100, TimeUnit.MILLISECONDS); + + sd.set(d); + + assertEquals(0, count[0]); + assertFalse(d.isDisposed()); + + scheduler.advanceTimeBy(400, TimeUnit.MILLISECONDS); + + assertEquals(1, count[0]); + assertTrue(d.isDisposed()); + } + + @Test + public void disposeSelfPeriodic() { + final int[] count = { 0 }; + + TestScheduler scheduler = new TestScheduler(); + + Worker worker = scheduler.createWorker(); + + try { + final SequentialDisposable sd = new SequentialDisposable(); + + Disposable d = worker.schedulePeriodically(new Runnable() { + @Override + public void run() { + count[0]++; + sd.dispose(); + } + }, 100, 100, TimeUnit.MILLISECONDS); + + sd.set(d); + + assertEquals(0, count[0]); + assertFalse(d.isDisposed()); + + scheduler.advanceTimeBy(400, TimeUnit.MILLISECONDS); + + assertEquals(1, count[0]); + assertTrue(d.isDisposed()); + } finally { + worker.dispose(); + } + } + + @Test + public void periodicDirectTaskRace() { + final TestScheduler scheduler = new TestScheduler(); + + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final Disposable d = scheduler.schedulePeriodicallyDirect(Functions.EMPTY_RUNNABLE, 1, 1, TimeUnit.MILLISECONDS); + + Runnable r1 = new Runnable() { + @Override + public void run() { + d.dispose(); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + scheduler.advanceTimeBy(1, TimeUnit.SECONDS); + } + }; + + TestHelper.race(r1, r2); + } + + } + + @Test + public void periodicDirectTaskRaceIO() throws Exception { + final Scheduler scheduler = Schedulers.io(); + + for (int i = 0; i < 100; i++) { + final Disposable d = scheduler.schedulePeriodicallyDirect( + Functions.EMPTY_RUNNABLE, 0, 0, TimeUnit.MILLISECONDS); + + Thread.sleep(1); + + d.dispose(); + } + + } + + @Test + public void scheduleDirectThrows() throws Exception { + List<Throwable> list = TestHelper.trackPluginErrors(); + try { + Schedulers.io().scheduleDirect(new Runnable() { + @Override + public void run() { + throw new TestException(); + } + }); + + Thread.sleep(250); + + assertTrue(list.size() >= 1); + TestHelper.assertUndeliverable(list, 0, TestException.class, null); + + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void schedulersUtility() { + TestHelper.checkUtilityClass(Schedulers.class); + } + + @Test + public void defaultSchedulePeriodicallyDirectRejects() { + Scheduler s = new Scheduler() { + @NonNull + @Override + public Worker createWorker() { + return new Worker() { + @NonNull + @Override + public Disposable schedule(@NonNull Runnable run, long delay, @NonNull TimeUnit unit) { + return EmptyDisposable.INSTANCE; + } + + @Override + public void dispose() { + + } + + @Override + public boolean isDisposed() { + return false; + } + }; + } + }; + + assertSame(EmptyDisposable.INSTANCE, s.schedulePeriodicallyDirect(Functions.EMPTY_RUNNABLE, 1, 1, TimeUnit.MILLISECONDS)); + } + + @Test + public void holders() { + assertNotNull(new Schedulers.ComputationHolder()); + + assertNotNull(new Schedulers.IoHolder()); + + assertNotNull(new Schedulers.NewThreadHolder()); + + assertNotNull(new Schedulers.SingleHolder()); + } + + static final class CustomScheduler extends Scheduler { + + @Override + public Worker createWorker() { + return Schedulers.single().createWorker(); + } + + } + + @Test + public void customScheduleDirectDisposed() { + CustomScheduler scheduler = new CustomScheduler(); + + Disposable d = scheduler.scheduleDirect(Functions.EMPTY_RUNNABLE, 1, TimeUnit.MINUTES); + + assertFalse(d.isDisposed()); + + d.dispose(); + + assertTrue(d.isDisposed()); + } + + @Test + public void unwrapDefaultPeriodicTask() { + TestScheduler scheduler = new TestScheduler(); + + Runnable runnable = new Runnable() { + @Override + public void run() { + } + }; + SchedulerRunnableIntrospection wrapper = (SchedulerRunnableIntrospection) scheduler.schedulePeriodicallyDirect(runnable, 100, 100, TimeUnit.MILLISECONDS); + + assertSame(runnable, wrapper.getWrappedRunnable()); + } + + @Test + public void unwrapScheduleDirectTask() { + TestScheduler scheduler = new TestScheduler(); + + Runnable runnable = new Runnable() { + @Override + public void run() { + } + }; + SchedulerRunnableIntrospection wrapper = (SchedulerRunnableIntrospection) scheduler.scheduleDirect(runnable, 100, TimeUnit.MILLISECONDS); + assertSame(runnable, wrapper.getWrappedRunnable()); + } + + @Test + public void unwrapWorkerPeriodicTask() { + final Runnable runnable = new Runnable() { + @Override + public void run() { + } + }; + + Scheduler scheduler = new Scheduler() { + @Override + public Worker createWorker() { + return new Worker() { + @Override + public Disposable schedule(Runnable run, long delay, TimeUnit unit) { + SchedulerRunnableIntrospection outerWrapper = (SchedulerRunnableIntrospection) run; + SchedulerRunnableIntrospection innerWrapper = (SchedulerRunnableIntrospection) outerWrapper.getWrappedRunnable(); + assertSame(runnable, innerWrapper.getWrappedRunnable()); + return (Disposable) innerWrapper; + } + + @Override + public void dispose() { + } + + @Override + public boolean isDisposed() { + return false; + } + }; + } + }; + + scheduler.schedulePeriodicallyDirect(runnable, 100, 100, TimeUnit.MILLISECONDS); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/schedulers/SchedulerTestHelper.java b/src/test/java/io/reactivex/rxjava3/schedulers/SchedulerTestHelper.java new file mode 100644 index 0000000000..3c899f262e --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/schedulers/SchedulerTestHelper.java @@ -0,0 +1,103 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.schedulers; + +import static org.junit.Assert.*; + +import java.util.concurrent.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.subscribers.DefaultSubscriber; + +final class SchedulerTestHelper { + private SchedulerTestHelper() { + // No instances. + } + + /** + * Verifies that the given Scheduler does not deliver handled errors to its executing Thread's + * {@link java.lang.Thread.UncaughtExceptionHandler}. + * + * @param scheduler {@link Scheduler} to verify. + */ + static void handledErrorIsNotDeliveredToThreadHandler(Scheduler scheduler) throws InterruptedException { + Thread.UncaughtExceptionHandler originalHandler = Thread.getDefaultUncaughtExceptionHandler(); + try { + CapturingUncaughtExceptionHandler handler = new CapturingUncaughtExceptionHandler(); + CapturingObserver<Object> observer = new CapturingObserver<>(); + Thread.setDefaultUncaughtExceptionHandler(handler); + IllegalStateException error = new IllegalStateException("Should be delivered to handler"); + Flowable.error(error) + .subscribeOn(scheduler) + .subscribe(observer); + + if (!observer.completed.await(3, TimeUnit.SECONDS)) { + fail("timed out"); + } + + if (handler.count != 0) { + handler.caught.printStackTrace(); + } + assertEquals("Handler should not have received anything: " + handler.caught, 0, handler.count); + assertEquals("Observer should have received an error", 1, observer.errorCount); + assertEquals("Observer should not have received a next value", 0, observer.nextCount); + + Throwable cause = observer.error; + while (cause != null) { + if (error.equals(cause)) { break; } + if (cause == cause.getCause()) { break; } + cause = cause.getCause(); + } + assertEquals("Our error should have been delivered to the observer", error, cause); + } finally { + Thread.setDefaultUncaughtExceptionHandler(originalHandler); + } + } + + private static final class CapturingUncaughtExceptionHandler implements Thread.UncaughtExceptionHandler { + int count; + Throwable caught; + CountDownLatch completed = new CountDownLatch(1); + + @Override + public void uncaughtException(Thread t, Throwable e) { + count++; + caught = e; + completed.countDown(); + } + } + + static final class CapturingObserver<T> extends DefaultSubscriber<T> { + CountDownLatch completed = new CountDownLatch(1); + int errorCount; + int nextCount; + Throwable error; + + @Override + public void onComplete() { + } + + @Override + public void onError(Throwable e) { + errorCount++; + error = e; + completed.countDown(); + } + + @Override + public void onNext(T t) { + nextCount++; + } + } +} diff --git a/src/test/java/io/reactivex/rxjava3/schedulers/SchedulerWorkerTest.java b/src/test/java/io/reactivex/rxjava3/schedulers/SchedulerWorkerTest.java new file mode 100644 index 0000000000..17b7fa5dd7 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/schedulers/SchedulerWorkerTest.java @@ -0,0 +1,155 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.schedulers; + +import static org.junit.Assert.assertTrue; + +import java.util.*; +import java.util.concurrent.TimeUnit; + +import org.junit.Test; + +import io.reactivex.rxjava3.annotations.NonNull; +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; + +public class SchedulerWorkerTest extends RxJavaTest { + + static final class CustomDriftScheduler extends Scheduler { + public volatile long drift; + @NonNull + @Override + public Worker createWorker() { + final Worker w = Schedulers.computation().createWorker(); + return new Worker() { + + @Override + public void dispose() { + w.dispose(); + } + + @Override + public boolean isDisposed() { + return w.isDisposed(); + } + + @NonNull + @Override + public Disposable schedule(@NonNull Runnable action) { + return w.schedule(action); + } + + @NonNull + @Override + public Disposable schedule(@NonNull Runnable action, long delayTime, @NonNull TimeUnit unit) { + return w.schedule(action, delayTime, unit); + } + + @Override + public long now(TimeUnit unit) { + return super.now(unit) + unit.convert(drift, TimeUnit.NANOSECONDS); + } + }; + } + + @Override + public long now(@NonNull TimeUnit unit) { + return super.now(unit) + unit.convert(drift, TimeUnit.NANOSECONDS); + } + } + + @Test + public void currentTimeDriftBackwards() throws Exception { + CustomDriftScheduler s = new CustomDriftScheduler(); + + Scheduler.Worker w = s.createWorker(); + + try { + final List<Long> times = new ArrayList<>(); + + Disposable d = w.schedulePeriodically(new Runnable() { + @Override + public void run() { + times.add(System.currentTimeMillis()); + } + }, 100, 100, TimeUnit.MILLISECONDS); + + Thread.sleep(150); + + s.drift = -TimeUnit.SECONDS.toNanos(1) - Scheduler.clockDriftTolerance(); + + Thread.sleep(400); + + d.dispose(); + + Thread.sleep(150); + + System.out.println("Runs: " + times.size()); + + for (int i = 0; i < times.size() - 1 ; i++) { + long diff = times.get(i + 1) - times.get(i); + System.out.println("Diff #" + i + ": " + diff); + assertTrue("" + i + ":" + diff, diff < 150 && diff > 50); + } + + assertTrue("Too few invocations: " + times.size(), times.size() > 2); + + } finally { + w.dispose(); + } + + } + + @Test + public void currentTimeDriftForwards() throws Exception { + CustomDriftScheduler s = new CustomDriftScheduler(); + + Scheduler.Worker w = s.createWorker(); + + try { + final List<Long> times = new ArrayList<>(); + + Disposable d = w.schedulePeriodically(new Runnable() { + @Override + public void run() { + times.add(System.currentTimeMillis()); + } + }, 100, 100, TimeUnit.MILLISECONDS); + + Thread.sleep(150); + + s.drift = TimeUnit.SECONDS.toNanos(1) + Scheduler.clockDriftTolerance(); + + Thread.sleep(400); + + d.dispose(); + + Thread.sleep(150); + + System.out.println("Runs: " + times.size()); + + assertTrue(times.size() > 2); + + for (int i = 0; i < times.size() - 1 ; i++) { + long diff = times.get(i + 1) - times.get(i); + System.out.println("Diff #" + i + ": " + diff); + assertTrue("Diff out of range: " + diff, diff < 250 && diff > 50); + } + + } finally { + w.dispose(); + } + + } +} diff --git a/src/test/java/io/reactivex/rxjava3/schedulers/TestSchedulerTest.java b/src/test/java/io/reactivex/rxjava3/schedulers/TestSchedulerTest.java new file mode 100644 index 0000000000..4c2e776e0d --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/schedulers/TestSchedulerTest.java @@ -0,0 +1,355 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.schedulers; + +import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.*; + +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.Test; +import org.mockito.*; +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.core.Scheduler.Worker; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.subscriptions.BooleanSubscription; +import io.reactivex.rxjava3.internal.util.ExceptionHelper; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.schedulers.TestScheduler.*; + +public class TestSchedulerTest extends RxJavaTest { + + @SuppressWarnings("unchecked") + // mocking is unchecked, unfortunately + @Test + public final void periodicScheduling() throws Throwable { + final Function<Long, Void> calledOp = mock(Function.class); + + final TestScheduler scheduler = new TestScheduler(); + final Scheduler.Worker inner = scheduler.createWorker(); + + try { + inner.schedulePeriodically(new Runnable() { + @Override + public void run() { + System.out.println(scheduler.now(TimeUnit.MILLISECONDS)); + try { + calledOp.apply(scheduler.now(TimeUnit.MILLISECONDS)); + } catch (Throwable ex) { + ExceptionHelper.wrapOrThrow(ex); + } + } + }, 1, 2, TimeUnit.SECONDS); + + verify(calledOp, never()).apply(anyLong()); + + InOrder inOrder = Mockito.inOrder(calledOp); + + scheduler.advanceTimeBy(999L, TimeUnit.MILLISECONDS); + inOrder.verify(calledOp, never()).apply(anyLong()); + + scheduler.advanceTimeBy(1L, TimeUnit.MILLISECONDS); + inOrder.verify(calledOp, times(1)).apply(1000L); + + scheduler.advanceTimeBy(1999L, TimeUnit.MILLISECONDS); + inOrder.verify(calledOp, never()).apply(3000L); + + scheduler.advanceTimeBy(1L, TimeUnit.MILLISECONDS); + inOrder.verify(calledOp, times(1)).apply(3000L); + + scheduler.advanceTimeBy(5L, TimeUnit.SECONDS); + inOrder.verify(calledOp, times(1)).apply(5000L); + inOrder.verify(calledOp, times(1)).apply(7000L); + + inner.dispose(); + scheduler.advanceTimeBy(11L, TimeUnit.SECONDS); + inOrder.verify(calledOp, never()).apply(anyLong()); + } finally { + inner.dispose(); + } + } + + @SuppressWarnings("unchecked") + // mocking is unchecked, unfortunately + @Test + public final void periodicSchedulingUnsubscription() throws Throwable { + final Function<Long, Void> calledOp = mock(Function.class); + + final TestScheduler scheduler = new TestScheduler(); + final Scheduler.Worker inner = scheduler.createWorker(); + + try { + final Disposable subscription = inner.schedulePeriodically(new Runnable() { + @Override + public void run() { + System.out.println(scheduler.now(TimeUnit.MILLISECONDS)); + try { + calledOp.apply(scheduler.now(TimeUnit.MILLISECONDS)); + } catch (Throwable ex) { + ExceptionHelper.wrapOrThrow(ex); + } + } + }, 1, 2, TimeUnit.SECONDS); + + verify(calledOp, never()).apply(anyLong()); + + InOrder inOrder = Mockito.inOrder(calledOp); + + scheduler.advanceTimeBy(999L, TimeUnit.MILLISECONDS); + inOrder.verify(calledOp, never()).apply(anyLong()); + + scheduler.advanceTimeBy(1L, TimeUnit.MILLISECONDS); + inOrder.verify(calledOp, times(1)).apply(1000L); + + scheduler.advanceTimeBy(1999L, TimeUnit.MILLISECONDS); + inOrder.verify(calledOp, never()).apply(3000L); + + scheduler.advanceTimeBy(1L, TimeUnit.MILLISECONDS); + inOrder.verify(calledOp, times(1)).apply(3000L); + + scheduler.advanceTimeBy(5L, TimeUnit.SECONDS); + inOrder.verify(calledOp, times(1)).apply(5000L); + inOrder.verify(calledOp, times(1)).apply(7000L); + + subscription.dispose(); + scheduler.advanceTimeBy(11L, TimeUnit.SECONDS); + inOrder.verify(calledOp, never()).apply(anyLong()); + } finally { + inner.dispose(); + } + } + + @Test + public final void immediateUnsubscribes() { + TestScheduler s = new TestScheduler(); + final Scheduler.Worker inner = s.createWorker(); + final AtomicInteger counter = new AtomicInteger(0); + + try { + inner.schedule(new Runnable() { + + @Override + public void run() { + counter.incrementAndGet(); + System.out.println("counter: " + counter.get()); + inner.schedule(this); + } + + }); + inner.dispose(); + assertEquals(0, counter.get()); + } finally { + inner.dispose(); + } + } + + @Test + public final void immediateUnsubscribes2() { + TestScheduler s = new TestScheduler(); + final Scheduler.Worker inner = s.createWorker(); + try { + final AtomicInteger counter = new AtomicInteger(0); + + final Disposable subscription = inner.schedule(new Runnable() { + + @Override + public void run() { + counter.incrementAndGet(); + System.out.println("counter: " + counter.get()); + inner.schedule(this); + } + + }); + subscription.dispose(); + assertEquals(0, counter.get()); + } finally { + inner.dispose(); + } + } + + @Test + public final void nestedSchedule() { + final TestScheduler scheduler = new TestScheduler(); + final Scheduler.Worker inner = scheduler.createWorker(); + + try { + final Runnable calledOp = mock(Runnable.class); + + Flowable<Object> poller; + poller = Flowable.unsafeCreate(new Publisher<Object>() { + @Override + public void subscribe(final Subscriber<? super Object> aSubscriber) { + final BooleanSubscription bs = new BooleanSubscription(); + aSubscriber.onSubscribe(bs); + inner.schedule(new Runnable() { + @Override + public void run() { + if (!bs.isCancelled()) { + calledOp.run(); + inner.schedule(this, 5, TimeUnit.SECONDS); + } + } + }); + } + }); + + InOrder inOrder = Mockito.inOrder(calledOp); + + Disposable sub; + sub = poller.subscribe(); + + scheduler.advanceTimeTo(6, TimeUnit.SECONDS); + inOrder.verify(calledOp, times(2)).run(); + + sub.dispose(); + scheduler.advanceTimeTo(11, TimeUnit.SECONDS); + inOrder.verify(calledOp, never()).run(); + + sub = poller.subscribe(); + scheduler.advanceTimeTo(12, TimeUnit.SECONDS); + inOrder.verify(calledOp, times(1)).run(); + } finally { + inner.dispose(); + } + } + + @Test + public void timedRunnableToString() { + TimedRunnable r = new TimedRunnable((TestWorker) new TestScheduler().createWorker(), 5, new Runnable() { + @Override + public void run() { + // deliberately no-op + } + + @Override + public String toString() { + return "Runnable"; + } + }, 1); + + assertEquals("TimedRunnable(time = 5, run = Runnable)", r.toString()); + } + + @Test + public void workerDisposed() { + TestScheduler scheduler = new TestScheduler(); + + Worker w = scheduler.createWorker(); + w.dispose(); + assertTrue(w.isDisposed()); + } + + @Test + public void constructorTimeSetsTime() { + TestScheduler ts = new TestScheduler(5, TimeUnit.SECONDS); + assertEquals(5, ts.now(TimeUnit.SECONDS)); + assertEquals(5000, ts.now(TimeUnit.MILLISECONDS)); + } + + @Test + public void withOnScheduleHook() { + AtomicInteger run = new AtomicInteger(); + AtomicInteger counter = new AtomicInteger(); + RxJavaPlugins.setScheduleHandler(r -> { + counter.getAndIncrement(); + return r; + }); + try { + Runnable r = () -> run.getAndIncrement(); + TestScheduler ts = new TestScheduler(true); + + ts.createWorker().schedule(r); + ts.createWorker().schedule(r, 1, TimeUnit.SECONDS); + + ts.advanceTimeBy(1, TimeUnit.SECONDS); + + assertEquals(2, run.get()); + assertEquals(2, counter.get()); + + ts = new TestScheduler(); + + ts.createWorker().schedule(r); + ts.createWorker().schedule(r, 1, TimeUnit.SECONDS); + + ts.advanceTimeBy(1, TimeUnit.SECONDS); + + assertEquals(4, run.get()); + assertEquals(2, counter.get()); + } finally { + RxJavaPlugins.setScheduleHandler(null); + } + } + + @Test + public void withOnScheduleHookInitialTime() { + AtomicInteger run = new AtomicInteger(); + AtomicInteger counter = new AtomicInteger(); + RxJavaPlugins.setScheduleHandler(r -> { + counter.getAndIncrement(); + return r; + }); + try { + Runnable r = () -> run.getAndIncrement(); + TestScheduler ts = new TestScheduler(1, TimeUnit.HOURS, true); + + ts.createWorker().schedule(r); + ts.createWorker().schedule(r, 1, TimeUnit.SECONDS); + + ts.advanceTimeBy(1, TimeUnit.SECONDS); + + assertEquals(2, run.get()); + assertEquals(2, counter.get()); + + ts = new TestScheduler(1, TimeUnit.HOURS); + + ts.createWorker().schedule(r); + ts.createWorker().schedule(r, 1, TimeUnit.SECONDS); + + ts.advanceTimeBy(1, TimeUnit.SECONDS); + + assertEquals(4, run.get()); + assertEquals(2, counter.get()); + } finally { + RxJavaPlugins.setScheduleHandler(null); + } + } + + @Test + public void disposeWork() { + AtomicInteger run = new AtomicInteger(); + Runnable r = () -> run.getAndIncrement(); + TestScheduler ts = new TestScheduler(1, TimeUnit.HOURS, true); + + Disposable d = ts.createWorker().schedule(r); + + assertFalse(d.isDisposed()); + + d.dispose(); + + assertTrue(d.isDisposed()); + + d.dispose(); + + assertTrue(d.isDisposed()); + + ts.advanceTimeBy(1, TimeUnit.SECONDS); + + assertEquals(0, run.get()); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/schedulers/TimedTest.java b/src/test/java/io/reactivex/rxjava3/schedulers/TimedTest.java new file mode 100644 index 0000000000..7d82af675e --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/schedulers/TimedTest.java @@ -0,0 +1,95 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.schedulers; + +import static org.junit.Assert.*; + +import java.util.concurrent.TimeUnit; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.RxJavaTest; + +public class TimedTest extends RxJavaTest { + + @Test + public void properties() { + Timed<Integer> timed = new Timed<>(1, 5, TimeUnit.SECONDS); + + assertEquals(1, timed.value().intValue()); + assertEquals(5, timed.time()); + assertEquals(5000, timed.time(TimeUnit.MILLISECONDS)); + assertSame(TimeUnit.SECONDS, timed.unit()); + } + + @Test + public void hashCodeOf() { + Timed<Integer> t1 = new Timed<>(1, 5, TimeUnit.SECONDS); + + assertEquals(TimeUnit.SECONDS.hashCode() + 31 * (5 + 31 * 1), t1.hashCode()); + + Timed<Integer> t2 = new Timed<>(0, 5, TimeUnit.SECONDS); + + assertEquals(TimeUnit.SECONDS.hashCode() + 31 * (5 + 31 * 0), t2.hashCode()); + } + + @Test + public void equalsWith() { + Timed<Integer> t1 = new Timed<>(1, 5, TimeUnit.SECONDS); + Timed<Integer> t2 = new Timed<>(1, 5, TimeUnit.SECONDS); + Timed<Integer> t3 = new Timed<>(2, 5, TimeUnit.SECONDS); + Timed<Integer> t4 = new Timed<>(1, 4, TimeUnit.SECONDS); + Timed<Integer> t5 = new Timed<>(1, 5, TimeUnit.MINUTES); + + assertEquals(t1, t1); + assertEquals(t1, t2); + + assertNotEquals(t1, t3); + assertNotEquals(t1, t4); + assertNotEquals(t2, t3); + assertNotEquals(t2, t4); + assertNotEquals(t2, t5); + + assertNotEquals(t3, t1); + assertNotEquals(t3, t2); + assertNotEquals(t3, t4); + assertNotEquals(t3, t5); + + assertNotEquals(t4, t1); + assertNotEquals(t4, t2); + assertNotEquals(t4, t3); + assertNotEquals(t4, t5); + + assertNotEquals(t5, t1); + assertNotEquals(t5, t2); + assertNotEquals(t5, t3); + assertNotEquals(t5, t4); + + assertNotEquals(new Object(), t1); + + assertNotEquals(t1, new Object()); + } + + @Test + public void toStringOf() { + Timed<Integer> t1 = new Timed<>(1, 5, TimeUnit.SECONDS); + + assertEquals("Timed[time=5, unit=SECONDS, value=1]", t1.toString()); + } + + @Test(expected = NullPointerException.class) + public void timeUnitNullFail() throws Exception { + new Timed<>(1, 5, null); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/schedulers/TrampolineSchedulerTest.java b/src/test/java/io/reactivex/rxjava3/schedulers/TrampolineSchedulerTest.java new file mode 100644 index 0000000000..d98c93a435 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/schedulers/TrampolineSchedulerTest.java @@ -0,0 +1,162 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.schedulers; + +import static org.junit.Assert.assertEquals; + +import java.util.*; +import java.util.concurrent.TimeUnit; + +import org.junit.Test; +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.core.Scheduler.Worker; +import io.reactivex.rxjava3.disposables.*; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.subscribers.TestSubscriber; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class TrampolineSchedulerTest extends AbstractSchedulerTests { + + @Override + protected Scheduler getScheduler() { + return Schedulers.trampoline(); + } + + @Test + public final void mergeWithCurrentThreadScheduler1() { + + final String currentThreadName = Thread.currentThread().getName(); + + Flowable<Integer> f1 = Flowable.<Integer> just(1, 2, 3, 4, 5); + Flowable<Integer> f2 = Flowable.<Integer> just(6, 7, 8, 9, 10); + Flowable<String> f = Flowable.<Integer> merge(f1, f2).subscribeOn(Schedulers.trampoline()).map(new Function<Integer, String>() { + + @Override + public String apply(Integer t) { + assertEquals(Thread.currentThread().getName(), currentThreadName); + return "Value_" + t + "_Thread_" + Thread.currentThread().getName(); + } + }); + + f.blockingForEach(new Consumer<String>() { + + @Override + public void accept(String t) { + System.out.println("t: " + t); + } + }); + } + + @Test + public void nestedTrampolineWithUnsubscribe() { + final ArrayList<String> workDone = new ArrayList<>(); + final CompositeDisposable workers = new CompositeDisposable(); + Worker worker = Schedulers.trampoline().createWorker(); + try { + workers.add(worker); + worker.schedule(new Runnable() { + + @Override + public void run() { + workers.add(doWorkOnNewTrampoline("A", workDone)); + } + + }); + + final Worker worker2 = Schedulers.trampoline().createWorker(); + workers.add(worker2); + worker2.schedule(new Runnable() { + + @Override + public void run() { + workers.add(doWorkOnNewTrampoline("B", workDone)); + // we unsubscribe worker2 ... it should not affect work scheduled on a separate Trampline.Worker + worker2.dispose(); + } + + }); + + assertEquals(6, workDone.size()); + assertEquals(Arrays.asList("A.1", "A.B.1", "A.B.2", "B.1", "B.B.1", "B.B.2"), workDone); + } finally { + workers.dispose(); + } + } + + /** + * This is a regression test for #1702. Concurrent work scheduling that is improperly synchronized can cause an + * action to be added or removed onto the priority queue during a poll, which can result in NPEs during queue + * sifting. While it is difficult to isolate the issue directly, we can easily trigger the behavior by spamming the + * trampoline with enqueue requests from multiple threads concurrently. + */ + @Test + public void trampolineWorkerHandlesConcurrentScheduling() { + final Worker trampolineWorker = Schedulers.trampoline().createWorker(); + final Subscriber<Object> subscriber = TestHelper.mockSubscriber(); + final TestSubscriber<Disposable> ts = new TestSubscriber<>(subscriber); + + // Spam the trampoline with actions. + Flowable.range(0, 50) + .flatMap(new Function<Integer, Publisher<Disposable>>() { + @Override + public Publisher<Disposable> apply(Integer count) { + return Flowable + .interval(1, TimeUnit.MICROSECONDS) + .map(new Function<Long, Disposable>() { + @Override + public Disposable apply(Long ount1) { + return trampolineWorker.schedule(Functions.EMPTY_RUNNABLE); + } + }).take(100); + } + }) + .subscribeOn(Schedulers.computation()) + .subscribe(ts); + ts.awaitDone(5, TimeUnit.SECONDS); + ts.assertNoErrors(); + } + + private static Worker doWorkOnNewTrampoline(final String key, final ArrayList<String> workDone) { + Worker worker = Schedulers.trampoline().createWorker(); + worker.schedule(new Runnable() { + + @Override + public void run() { + String msg = key + ".1"; + workDone.add(msg); + System.out.println(msg); + Worker worker3 = Schedulers.trampoline().createWorker(); + worker3.schedule(createPrintAction(key + ".B.1", workDone)); + worker3.schedule(createPrintAction(key + ".B.2", workDone)); + } + + }); + return worker; + } + + private static Runnable createPrintAction(final String message, final ArrayList<String> workDone) { + return new Runnable() { + + @Override + public void run() { + System.out.println(message); + workDone.add(message); + } + + }; + } +} diff --git a/src/test/java/io/reactivex/rxjava3/single/SingleCacheTest.java b/src/test/java/io/reactivex/rxjava3/single/SingleCacheTest.java new file mode 100644 index 0000000000..3fe2da558c --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/single/SingleCacheTest.java @@ -0,0 +1,136 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.single; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.subjects.PublishSubject; +import io.reactivex.rxjava3.subscribers.TestSubscriber; + +public class SingleCacheTest extends RxJavaTest { + + @Test + public void normal() { + Single<Integer> cache = Single.just(1).cache(); + + cache + .test() + .assertResult(1); + + cache + .test() + .assertResult(1); + } + + @Test + public void error() { + Single<Object> cache = Single.error(new TestException()) + .cache(); + + cache + .test() + .assertFailure(TestException.class); + + cache + .test() + .assertFailure(TestException.class); + } + + @Test + public void delayed() { + PublishSubject<Integer> ps = PublishSubject.create(); + Single<Integer> cache = ps.single(-99).cache(); + + TestObserver<Integer> to1 = cache.test(); + + TestObserver<Integer> to2 = cache.test(); + + ps.onNext(1); + ps.onComplete(); + + to1.assertResult(1); + to2.assertResult(1); + } + + @Test + public void delayedDisposed() { + PublishSubject<Integer> ps = PublishSubject.create(); + Single<Integer> cache = ps.single(-99).cache(); + + TestObserver<Integer> to1 = cache.test(); + + TestObserver<Integer> to2 = cache.test(); + + to1.dispose(); + + ps.onNext(1); + ps.onComplete(); + + to1.assertNoValues().assertNoErrors().assertNotComplete(); + to2.assertResult(1); + } + + @Test + public void crossCancel() { + PublishSubject<Integer> ps = PublishSubject.create(); + Single<Integer> cache = ps.single(-99).cache(); + + final TestSubscriber<Integer> ts1 = new TestSubscriber<>(); + + TestSubscriber<Integer> ts2 = new TestSubscriber<Integer>() { + @Override + public void onNext(Integer t) { + super.onNext(t); + ts1.cancel(); + } + }; + + cache.toFlowable().subscribe(ts2); + cache.toFlowable().subscribe(ts1); + + ps.onNext(1); + ps.onComplete(); + + ts1.assertNoValues().assertNoErrors().assertNotComplete(); + ts2.assertResult(1); + } + + @Test + public void crossCancelOnError() { + PublishSubject<Integer> ps = PublishSubject.create(); + Single<Integer> cache = ps.single(-99).cache(); + + final TestSubscriber<Integer> ts1 = new TestSubscriber<>(); + + TestSubscriber<Integer> ts2 = new TestSubscriber<Integer>() { + @Override + public void onError(Throwable t) { + super.onError(t); + ts1.cancel(); + } + }; + + cache.toFlowable().subscribe(ts2); + cache.toFlowable().subscribe(ts1); + + ps.onError(new TestException()); + + ts1.assertNoValues().assertNoErrors().assertNotComplete(); + ts2.assertFailure(TestException.class); + } + +} diff --git a/src/test/java/io/reactivex/rxjava3/single/SingleNullTests.java b/src/test/java/io/reactivex/rxjava3/single/SingleNullTests.java new file mode 100644 index 0000000000..058ecdc6b0 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/single/SingleNullTests.java @@ -0,0 +1,431 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.single; + +import static org.junit.Assert.assertTrue; + +import java.lang.reflect.*; +import java.util.*; +import java.util.concurrent.*; + +import org.junit.*; +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.functions.Functions; + +public class SingleNullTests extends RxJavaTest { + + Single<Integer> just1 = Single.just(1); + + Single<Integer> error = Single.error(new TestException()); + + @Test + public void ambIterableIteratorNull() { + Single.amb(new Iterable<Single<Object>>() { + @Override + public Iterator<Single<Object>> iterator() { + return null; + } + }).test().assertError(NullPointerException.class); + } + + @Test + public void ambIterableOneIsNull() { + Single.amb(Arrays.asList(null, just1)) + .test() + .assertError(NullPointerException.class); + } + + @Test + public void ambArrayOneIsNull() { + Single.ambArray(null, just1) + .test() + .assertError(NullPointerException.class); + } + + @Test(expected = NullPointerException.class) + public void concatIterableIteratorNull() { + Single.concat(new Iterable<Single<Object>>() { + @Override + public Iterator<Single<Object>> iterator() { + return null; + } + }).blockingSubscribe(); + } + + @Test(expected = NullPointerException.class) + public void concatIterableOneIsNull() { + Single.concat(Arrays.asList(just1, null)).blockingSubscribe(); + } + + @Test + public void concatNull() throws Exception { + int maxArgs = 4; + + @SuppressWarnings("rawtypes") + Class<Single> clazz = Single.class; + for (int argCount = 2; argCount <= maxArgs; argCount++) { + for (int argNull = 1; argNull <= argCount; argNull++) { + Class<?>[] params = new Class[argCount]; + Arrays.fill(params, SingleSource.class); + + Object[] values = new Object[argCount]; + Arrays.fill(values, just1); + values[argNull - 1] = null; + + Method m = clazz.getMethod("concat", params); + + try { + m.invoke(null, values); + Assert.fail("No exception for argCount " + argCount + " / argNull " + argNull); + } catch (InvocationTargetException ex) { + if (!(ex.getCause() instanceof NullPointerException)) { + Assert.fail("Unexpected exception for argCount " + argCount + " / argNull " + argNull + ": " + ex); + } + } + } + } + } + + @Test(expected = NullPointerException.class) + public void deferReturnsNull() { + Single.defer(Functions.<Single<Object>>nullSupplier()).blockingGet(); + } + + @Test(expected = NullPointerException.class) + public void errorSupplierReturnsNull() { + Single.error(Functions.<Throwable>nullSupplier()).blockingGet(); + } + + @Test(expected = NullPointerException.class) + public void fromCallableReturnsNull() { + Single.fromCallable(new Callable<Object>() { + @Override + public Object call() throws Exception { + return null; + } + }).blockingGet(); + } + + @Test(expected = NullPointerException.class) + public void fromFutureReturnsNull() { + FutureTask<Object> f = new FutureTask<>(Functions.EMPTY_RUNNABLE, null); + f.run(); + Single.fromFuture(f).blockingGet(); + } + + @Test(expected = NullPointerException.class) + public void fromFutureTimedReturnsNull() { + FutureTask<Object> f = new FutureTask<>(Functions.EMPTY_RUNNABLE, null); + f.run(); + Single.fromFuture(f, 1, TimeUnit.SECONDS).blockingGet(); + } + + @Test(expected = NullPointerException.class) + public void mergeIterableIteratorNull() { + Single.merge(new Iterable<Single<Object>>() { + @Override + public Iterator<Single<Object>> iterator() { + return null; + } + }).blockingSubscribe(); + } + + @Test(expected = NullPointerException.class) + public void mergeIterableOneIsNull() { + Single.merge(Arrays.asList(null, just1)).blockingSubscribe(); + } + + @Test + public void mergeNull() throws Exception { + int maxArgs = 4; + + @SuppressWarnings("rawtypes") + Class<Single> clazz = Single.class; + for (int argCount = 2; argCount <= maxArgs; argCount++) { + for (int argNull = 1; argNull <= argCount; argNull++) { + Class<?>[] params = new Class[argCount]; + Arrays.fill(params, SingleSource.class); + + Object[] values = new Object[argCount]; + Arrays.fill(values, just1); + values[argNull - 1] = null; + + Method m = clazz.getMethod("merge", params); + + try { + m.invoke(null, values); + Assert.fail("No exception for argCount " + argCount + " / argNull " + argNull); + } catch (InvocationTargetException ex) { + if (!(ex.getCause() instanceof NullPointerException)) { + Assert.fail("Unexpected exception for argCount " + argCount + " / argNull " + argNull + ": " + ex); + } + } + } + } + } + + @Test(expected = NullPointerException.class) + public void usingSingleSupplierReturnsNull() { + Single.using(new Supplier<Object>() { + @Override + public Object get() { + return 1; + } + }, new Function<Object, Single<Object>>() { + @Override + public Single<Object> apply(Object d) { + return null; + } + }, Functions.emptyConsumer()).blockingGet(); + } + + @Test(expected = NullPointerException.class) + public void zipIterableIteratorNull() { + Single.zip(new Iterable<Single<Object>>() { + @Override + public Iterator<Single<Object>> iterator() { + return null; + } + }, new Function<Object[], Object>() { + @Override + public Object apply(Object[] v) { + return 1; + } + }).blockingGet(); + } + + @Test(expected = NullPointerException.class) + public void zipIterableOneIsNull() { + Single.zip(Arrays.asList(null, just1), new Function<Object[], Object>() { + @Override + public Object apply(Object[] v) { + return 1; + } + }).blockingGet(); + } + + @Test(expected = NullPointerException.class) + public void zipIterableOneFunctionReturnsNull() { + Single.zip(Arrays.asList(just1, just1), new Function<Object[], Object>() { + @Override + public Object apply(Object[] v) { + return null; + } + }).blockingGet(); + } + + @SuppressWarnings("unchecked") + @Test + public void zipNull() throws Exception { + @SuppressWarnings("rawtypes") + Class<Single> clazz = Single.class; + for (int argCount = 3; argCount < 10; argCount++) { + for (int argNull = 1; argNull <= argCount; argNull++) { + Class<?>[] params = new Class[argCount + 1]; + Arrays.fill(params, SingleSource.class); + Class<?> fniClass = Class.forName("io.reactivex.rxjava3.functions.Function" + argCount); + params[argCount] = fniClass; + + Object[] values = new Object[argCount + 1]; + Arrays.fill(values, just1); + values[argNull - 1] = null; + values[argCount] = Proxy.newProxyInstance(getClass().getClassLoader(), new Class[] { fniClass }, new InvocationHandler() { + @Override + public Object invoke(Object o, Method m, Object[] a) throws Throwable { + return 1; + } + }); + + Method m = clazz.getMethod("zip", params); + + try { + m.invoke(null, values); + Assert.fail("No exception for argCount " + argCount + " / argNull " + argNull); + } catch (InvocationTargetException ex) { + if (!(ex.getCause() instanceof NullPointerException)) { + Assert.fail("Unexpected exception for argCount " + argCount + " / argNull " + argNull + ": " + ex); + } + } + + values[argCount] = Proxy.newProxyInstance(getClass().getClassLoader(), new Class[] { fniClass }, new InvocationHandler() { + @Override + public Object invoke(Object o, Method m1, Object[] a) throws Throwable { + return null; + } + }); + try { + ((Single<Object>)m.invoke(null, values)).blockingGet(); + Assert.fail("No exception for argCount " + argCount + " / argNull " + argNull); + } catch (InvocationTargetException ex) { + if (!(ex.getCause() instanceof NullPointerException)) { + Assert.fail("Unexpected exception for argCount " + argCount + " / argNull " + argNull + ": " + ex); + } + } + + } + + Class<?>[] params = new Class[argCount + 1]; + Arrays.fill(params, SingleSource.class); + Class<?> fniClass = Class.forName("io.reactivex.rxjava3.functions.Function" + argCount); + params[argCount] = fniClass; + + Object[] values = new Object[argCount + 1]; + Arrays.fill(values, just1); + values[argCount] = null; + + Method m = clazz.getMethod("zip", params); + + try { + m.invoke(null, values); + Assert.fail("No exception for argCount " + argCount + " / zipper function "); + } catch (InvocationTargetException ex) { + if (!(ex.getCause() instanceof NullPointerException)) { + Assert.fail("Unexpected exception for argCount " + argCount + " / zipper function: " + ex); + } + } + } + } + + @Test(expected = NullPointerException.class) + public void zipIterableTwoIsNull() { + Single.zip(Arrays.asList(just1, null), new Function<Object[], Object>() { + @Override + public Object apply(Object[] v) { + return 1; + } + }) + .blockingGet(); + } + + @Test(expected = NullPointerException.class) + public void zipArrayOneIsNull() { + Single.zipArray(new Function<Object[], Object>() { + @Override + public Object apply(Object[] v) { + return 1; + } + }, just1, null) + .blockingGet(); + } + + @Test(expected = NullPointerException.class) + public void zipArrayFunctionReturnsNull() { + Single.zipArray(new Function<Object[], Object>() { + @Override + public Object apply(Object[] v) { + return null; + } + }, just1, just1).blockingGet(); + } + + //************************************************** + // Instance methods + //************************************************** + + @Test(expected = NullPointerException.class) + public void flatMapFunctionReturnsNull() { + just1.flatMap(new Function<Integer, Single<Object>>() { + @Override + public Single<Object> apply(Integer v) { + return null; + } + }).blockingGet(); + } + + @Test(expected = NullPointerException.class) + public void flatMapPublisherFunctionReturnsNull() { + just1.flatMapPublisher(new Function<Integer, Publisher<Object>>() { + @Override + public Publisher<Object> apply(Integer v) { + return null; + } + }).blockingSubscribe(); + } + + @Test(expected = NullPointerException.class) + public void liftFunctionReturnsNull() { + just1.lift(new SingleOperator<Object, Integer>() { + @Override + public SingleObserver<? super Integer> apply(SingleObserver<? super Object> observer) { + return null; + } + }).blockingGet(); + } + + @Test(expected = NullPointerException.class) + public void onErrorReturnsSupplierReturnsNull() { + error.onErrorReturn(new Function<Throwable, Integer>() { + @Override + public Integer apply(Throwable t) throws Exception { + return null; + } + }).blockingGet(); + } + + @Test + public void onErrorResumeNextFunctionReturnsNull() { + try { + error.onErrorResumeNext(new Function<Throwable, Single<Integer>>() { + @Override + public Single<Integer> apply(Throwable e) { + return null; + } + }).blockingGet(); + } catch (CompositeException ex) { + assertTrue(ex.toString(), ex.getExceptions().get(1) instanceof NullPointerException); + } + } + + @Test(expected = NullPointerException.class) + public void repeatWhenFunctionReturnsNull() { + error.repeatWhen(new Function<Flowable<Object>, Publisher<Object>>() { + @Override + public Publisher<Object> apply(Flowable<Object> v) { + return null; + } + }).blockingSubscribe(); + } + + @Test(expected = NullPointerException.class) + public void retryWhenFunctionReturnsNull() { + error.retryWhen(new Function<Flowable<? extends Throwable>, Publisher<Object>>() { + @Override + public Publisher<Object> apply(Flowable<? extends Throwable> e) { + return null; + } + }).blockingGet(); + } + + @Test(expected = NullPointerException.class) + public void subscribeOnSuccessNull() { + just1.subscribe(null, new Consumer<Throwable>() { + @Override + public void accept(Throwable e) { } + }); + } + + @Test(expected = NullPointerException.class) + public void zipWithFunctionReturnsNull() { + just1.zipWith(just1, new BiFunction<Integer, Integer, Object>() { + @Override + public Object apply(Integer a, Integer b) { + return null; + } + }).blockingGet(); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/single/SingleRetryTest.java b/src/test/java/io/reactivex/rxjava3/single/SingleRetryTest.java new file mode 100644 index 0000000000..e38322a4ab --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/single/SingleRetryTest.java @@ -0,0 +1,162 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.single; + +import static org.junit.Assert.assertEquals; + +import java.util.concurrent.Callable; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.Predicate; +import io.reactivex.rxjava3.internal.functions.Functions; + +public class SingleRetryTest extends RxJavaTest { + @Test + public void retryTimesPredicateWithMatchingPredicate() { + final AtomicInteger atomicInteger = new AtomicInteger(3); + final AtomicInteger numberOfSubscribeCalls = new AtomicInteger(0); + + Single.fromCallable(new Callable<Boolean>() { + @Override public Boolean call() throws Exception { + numberOfSubscribeCalls.incrementAndGet(); + + if (atomicInteger.decrementAndGet() != 0) { + throw new RuntimeException(); + } + + throw new IllegalArgumentException(); + } + }) + .retry(Integer.MAX_VALUE, new Predicate<Throwable>() { + @Override public boolean test(final Throwable throwable) throws Exception { + return !(throwable instanceof IllegalArgumentException); + } + }) + .test() + .assertFailure(IllegalArgumentException.class); + + assertEquals(3, numberOfSubscribeCalls.get()); + } + + @Test + public void retryTimesPredicateWithMatchingRetryAmount() { + final AtomicInteger atomicInteger = new AtomicInteger(3); + final AtomicInteger numberOfSubscribeCalls = new AtomicInteger(0); + + Single.fromCallable(new Callable<Boolean>() { + @Override public Boolean call() throws Exception { + numberOfSubscribeCalls.incrementAndGet(); + + if (atomicInteger.decrementAndGet() != 0) { + throw new RuntimeException(); + } + + return true; + } + }) + .retry(2, Functions.alwaysTrue()) + .test() + .assertResult(true); + + assertEquals(3, numberOfSubscribeCalls.get()); + } + + @Test + public void retryTimesPredicateWithNotMatchingRetryAmount() { + final AtomicInteger atomicInteger = new AtomicInteger(3); + final AtomicInteger numberOfSubscribeCalls = new AtomicInteger(0); + + Single.fromCallable(new Callable<Boolean>() { + @Override public Boolean call() throws Exception { + numberOfSubscribeCalls.incrementAndGet(); + + if (atomicInteger.decrementAndGet() != 0) { + throw new RuntimeException(); + } + + return true; + } + }) + .retry(1, Functions.alwaysTrue()) + .test() + .assertFailure(RuntimeException.class); + + assertEquals(2, numberOfSubscribeCalls.get()); + } + + @Test + public void retryTimesPredicateWithZeroRetries() { + final AtomicInteger atomicInteger = new AtomicInteger(2); + final AtomicInteger numberOfSubscribeCalls = new AtomicInteger(0); + + Single.fromCallable(new Callable<Boolean>() { + @Override public Boolean call() throws Exception { + numberOfSubscribeCalls.incrementAndGet(); + + if (atomicInteger.decrementAndGet() != 0) { + throw new RuntimeException(); + } + + return true; + } + }) + .retry(0, Functions.alwaysTrue()) + .test() + .assertFailure(RuntimeException.class); + + assertEquals(1, numberOfSubscribeCalls.get()); + } + + @Test + public void untilTrueJust() { + Single.just(1) + .retryUntil(() -> true) + .test() + .assertResult(1); + } + + @Test + public void untilFalseJust() { + Single.just(1) + .retryUntil(() -> false) + .test() + .assertResult(1); + } + + @Test + public void untilTrueError() { + Single.error(new TestException()) + .retryUntil(() -> true) + .test() + .assertFailure(TestException.class); + } + + @Test + public void untilFalseError() { + AtomicInteger counter = new AtomicInteger(); + Single.defer(() -> { + if (counter.getAndIncrement() == 0) { + return Single.error(new TestException()); + } + return Single.just(1); + }) + .retryUntil(() -> false) + .test() + .assertResult(1); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/single/SingleSubscribeTest.java b/src/test/java/io/reactivex/rxjava3/single/SingleSubscribeTest.java new file mode 100644 index 0000000000..df363bee83 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/single/SingleSubscribeTest.java @@ -0,0 +1,268 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.single; + +import static org.junit.Assert.*; + +import java.io.IOException; +import java.util.List; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.subjects.PublishSubject; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class SingleSubscribeTest extends RxJavaTest { + + @Test + public void consumer() { + final Integer[] value = { null }; + + Single.just(1).subscribe(new Consumer<Integer>() { + @Override + public void accept(Integer v) throws Exception { + value[0] = v; + } + }); + + assertEquals((Integer)1, value[0]); + } + + @Test + public void biconsumer() { + final Object[] value = { null, null }; + + Single.just(1).subscribe(new BiConsumer<Integer, Throwable>() { + @Override + public void accept(Integer v, Throwable e) throws Exception { + value[0] = v; + value[1] = e; + } + }); + + assertEquals(1, value[0]); + assertNull(value[1]); + } + + @Test + public void biconsumerError() { + final Object[] value = { null, null }; + + TestException ex = new TestException(); + + Single.error(ex).subscribe(new BiConsumer<Object, Throwable>() { + @Override + public void accept(Object v, Throwable e) throws Exception { + value[0] = v; + value[1] = e; + } + }); + + assertNull(value[0]); + assertEquals(ex, value[1]); + } + + @Test + public void subscribeThrows() { + try { + new Single<Integer>() { + @Override + protected void subscribeActual(SingleObserver<? super Integer> observer) { + throw new IllegalArgumentException(); + } + }.test(); + } catch (NullPointerException ex) { + if (!(ex.getCause() instanceof IllegalArgumentException)) { + fail(ex.toString() + ": should have thrown NPE(IAE)"); + } + } + } + + @Test + public void biConsumerDispose() { + PublishSubject<Integer> ps = PublishSubject.create(); + + Disposable d = ps.single(-99).subscribe(new BiConsumer<Object, Object>() { + @Override + public void accept(Object t1, Object t2) throws Exception { + + } + }); + + assertFalse(d.isDisposed()); + + d.dispose(); + + assertTrue(d.isDisposed()); + + assertFalse(ps.hasObservers()); + } + + @Test + public void consumerDispose() { + PublishSubject<Integer> ps = PublishSubject.create(); + + Disposable d = ps.single(-99).subscribe(Functions.<Integer>emptyConsumer()); + + assertFalse(d.isDisposed()); + + d.dispose(); + + assertTrue(d.isDisposed()); + + assertFalse(ps.hasObservers()); + } + + @Test + public void consumerSuccessThrows() { + List<Throwable> list = TestHelper.trackPluginErrors(); + + try { + Single.just(1).subscribe(new Consumer<Integer>() { + @Override + public void accept(Integer t) throws Exception { + throw new TestException(); + } + }); + + TestHelper.assertUndeliverable(list, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void consumerErrorThrows() { + List<Throwable> list = TestHelper.trackPluginErrors(); + + try { + Single.<Integer>error(new TestException("Outer failure")).subscribe( + Functions.<Integer>emptyConsumer(), + new Consumer<Throwable>() { + @Override + public void accept(Throwable t) throws Exception { + throw new TestException("Inner failure"); + } + }); + + TestHelper.assertError(list, 0, CompositeException.class); + List<Throwable> cel = TestHelper.compositeList(list.get(0)); + TestHelper.assertError(cel, 0, TestException.class, "Outer failure"); + TestHelper.assertError(cel, 1, TestException.class, "Inner failure"); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void biConsumerThrows() { + List<Throwable> list = TestHelper.trackPluginErrors(); + + try { + Single.just(1).subscribe(new BiConsumer<Integer, Throwable>() { + @Override + public void accept(Integer t, Throwable e) throws Exception { + throw new TestException(); + } + }); + + TestHelper.assertUndeliverable(list, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void biConsumerErrorThrows() { + List<Throwable> list = TestHelper.trackPluginErrors(); + + try { + Single.<Integer>error(new TestException("Outer failure")).subscribe( + new BiConsumer<Integer, Throwable>() { + @Override + public void accept(Integer a, Throwable t) throws Exception { + throw new TestException("Inner failure"); + } + }); + + TestHelper.assertError(list, 0, CompositeException.class); + List<Throwable> cel = TestHelper.compositeList(list.get(0)); + TestHelper.assertError(cel, 0, TestException.class, "Outer failure"); + TestHelper.assertError(cel, 1, TestException.class, "Inner failure"); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void methodTestNoCancel() { + PublishSubject<Integer> ps = PublishSubject.create(); + + ps.single(-99).test(false); + + assertTrue(ps.hasObservers()); + } + + @Test + public void successIsDisposed() { + assertTrue(Single.just(1).subscribe().isDisposed()); + } + + @Test + public void errorIsDisposed() { + assertTrue(Single.error(new TestException()).subscribe(Functions.emptyConsumer(), Functions.emptyConsumer()).isDisposed()); + } + + @Test + public void biConsumerIsDisposedOnSuccess() { + final Object[] result = { null, null }; + + Disposable d = Single.just(1) + .subscribe(new BiConsumer<Integer, Throwable>() { + @Override + public void accept(Integer t1, Throwable t2) throws Exception { + result[0] = t1; + result[1] = t2; + } + }); + + assertTrue("Not disposed?!", d.isDisposed()); + assertEquals(1, result[0]); + assertNull(result[1]); + } + + @Test + public void biConsumerIsDisposedOnError() { + final Object[] result = { null, null }; + + Disposable d = Single.<Integer>error(new IOException()) + .subscribe(new BiConsumer<Integer, Throwable>() { + @Override + public void accept(Integer t1, Throwable t2) throws Exception { + result[0] = t1; + result[1] = t2; + } + }); + + assertTrue("Not disposed?!", d.isDisposed()); + assertNull(result[0]); + assertTrue("" + result[1], result[1] instanceof IOException); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/single/SingleTest.java b/src/test/java/io/reactivex/rxjava3/single/SingleTest.java new file mode 100644 index 0000000000..7b51c97471 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/single/SingleTest.java @@ -0,0 +1,592 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.single; + +import static org.junit.Assert.*; + +import java.util.*; +import java.util.concurrent.*; +import java.util.concurrent.atomic.*; + +import org.junit.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.disposables.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.operators.single.SingleInternalHelper; +import io.reactivex.rxjava3.schedulers.Schedulers; +import io.reactivex.rxjava3.subscribers.TestSubscriber; +import io.reactivex.rxjava3.testsupport.*; + +public class SingleTest extends RxJavaTest { + + @Test + public void helloWorld() { + TestSubscriber<String> ts = new TestSubscriber<>(); + Single.just("Hello World!").toFlowable().subscribe(ts); + ts.assertValueSequence(Arrays.asList("Hello World!")); + } + + @Test + public void helloWorld2() { + final AtomicReference<String> v = new AtomicReference<>(); + Single.just("Hello World!").subscribe(new SingleObserver<String>() { + + @Override + public void onSubscribe(Disposable d) { + + } + + @Override + public void onSuccess(String value) { + v.set(value); + } + + @Override + public void onError(Throwable error) { + + } + + }); + assertEquals("Hello World!", v.get()); + } + + @Test + public void map() { + TestSubscriber<String> ts = new TestSubscriber<>(); + Single.just("A") + .map(new Function<String, String>() { + @Override + public String apply(String s) { + return s + "B"; + } + }) + .toFlowable().subscribe(ts); + ts.assertValueSequence(Arrays.asList("AB")); + } + + @Test + public void zip() { + TestSubscriber<String> ts = new TestSubscriber<>(); + Single<String> a = Single.just("A"); + Single<String> b = Single.just("B"); + + Single.zip(a, b, new BiFunction<String, String, String>() { + @Override + public String apply(String a1, String b1) { + return a1 + b1; + } + }) + .toFlowable().subscribe(ts); + ts.assertValueSequence(Arrays.asList("AB")); + } + + @Test + public void zipWith() { + TestSubscriber<String> ts = new TestSubscriber<>(); + + Single.just("A").zipWith(Single.just("B"), new BiFunction<String, String, String>() { + @Override + public String apply(String a1, String b1) { + return a1 + b1; + } + }) + .toFlowable().subscribe(ts); + ts.assertValueSequence(Arrays.asList("AB")); + } + + @Test + public void merge() { + TestSubscriber<String> ts = new TestSubscriber<>(); + Single<String> a = Single.just("A"); + Single<String> b = Single.just("B"); + + Single.merge(a, b).subscribe(ts); + ts.assertValueSequence(Arrays.asList("A", "B")); + } + + @Test + public void mergeWith() { + TestSubscriber<String> ts = new TestSubscriber<>(); + + Single.just("A").mergeWith(Single.just("B")).subscribe(ts); + ts.assertValueSequence(Arrays.asList("A", "B")); + } + + @Test + public void createSuccess() { + TestSubscriber<Object> ts = new TestSubscriber<>(); + + Single.unsafeCreate(new SingleSource<Object>() { + @Override + public void subscribe(SingleObserver<? super Object> observer) { + observer.onSubscribe(Disposable.empty()); + observer.onSuccess("Hello"); + } + }).toFlowable().subscribe(ts); + + ts.assertValueSequence(Arrays.asList("Hello")); + } + + @Test + public void createError() { + TestSubscriberEx<Object> ts = new TestSubscriberEx<>(); + Single.unsafeCreate(new SingleSource<Object>() { + @Override + public void subscribe(SingleObserver<? super Object> observer) { + observer.onSubscribe(Disposable.empty()); + observer.onError(new RuntimeException("fail")); + } + }).toFlowable().subscribe(ts); + + ts.assertError(RuntimeException.class); + ts.assertErrorMessage("fail"); + } + + @Test + public void async() { + TestSubscriber<String> ts = new TestSubscriber<>(); + Single.just("Hello") + .subscribeOn(Schedulers.io()) + .map(new Function<String, String>() { + @Override + public String apply(String v) { + System.out.println("SubscribeOn Thread: " + Thread.currentThread()); + return v; + } + }) + .observeOn(Schedulers.computation()) + .map(new Function<String, String>() { + @Override + public String apply(String v) { + System.out.println("ObserveOn Thread: " + Thread.currentThread()); + return v; + } + }) + .toFlowable().subscribe(ts); + ts.awaitDone(5, TimeUnit.SECONDS); + ts.assertValueSequence(Arrays.asList("Hello")); + } + + @Test + public void flatMap() throws InterruptedException { + TestSubscriberEx<String> ts = new TestSubscriberEx<>(); + Single.just("Hello").flatMap(new Function<String, Single<String>>() { + @Override + public Single<String> apply(String s) { + return Single.just(s + " World!").subscribeOn(Schedulers.computation()); + } + } + ).toFlowable().subscribe(ts); + if (!ts.await(5, TimeUnit.SECONDS)) { + ts.cancel(); + Assert.fail("TestSubscriber timed out."); + } + ts.assertValueSequence(Arrays.asList("Hello World!")); + } + + @Test + public void timeout() { + TestSubscriber<String> ts = new TestSubscriber<>(); + Single<String> s1 = Single.<String>unsafeCreate(new SingleSource<String>() { + @Override + public void subscribe(SingleObserver<? super String> observer) { + observer.onSubscribe(Disposable.empty()); + try { + Thread.sleep(5000); + } catch (InterruptedException e) { + // ignore as we expect this for the test + } + observer.onSuccess("success"); + } + }).subscribeOn(Schedulers.io()); + + s1.timeout(100, TimeUnit.MILLISECONDS).toFlowable().subscribe(ts); + + ts.awaitDone(5, TimeUnit.SECONDS); + ts.assertError(TimeoutException.class); + } + + @Test + public void timeoutWithFallback() { + TestSubscriber<String> ts = new TestSubscriber<>(); + Single<String> s1 = Single.<String>unsafeCreate(new SingleSource<String>() { + @Override + public void subscribe(SingleObserver<? super String> observer) { + observer.onSubscribe(Disposable.empty()); + try { + Thread.sleep(5000); + } catch (InterruptedException e) { + // ignore as we expect this for the test + } + observer.onSuccess("success"); + } + }).subscribeOn(Schedulers.io()); + + s1.timeout(100, TimeUnit.MILLISECONDS, Single.just("hello")).toFlowable().subscribe(ts); + + ts.awaitDone(5, TimeUnit.SECONDS); + ts.assertNoErrors(); + ts.assertValue("hello"); + } + + @Test + public void unsubscribe() throws InterruptedException { + TestSubscriber<String> ts = new TestSubscriber<>(); + final AtomicBoolean unsubscribed = new AtomicBoolean(); + final AtomicBoolean interrupted = new AtomicBoolean(); + final CountDownLatch latch = new CountDownLatch(2); + + Single<String> s1 = Single.<String>unsafeCreate(new SingleSource<String>() { + @Override + public void subscribe(final SingleObserver<? super String> observer) { + SerialDisposable sd = new SerialDisposable(); + observer.onSubscribe(sd); + final Thread t = new Thread(new Runnable() { + + @Override + public void run() { + try { + Thread.sleep(5000); + observer.onSuccess("success"); + } catch (InterruptedException e) { + interrupted.set(true); + latch.countDown(); + } + } + + }); + sd.replace(Disposable.fromRunnable(new Runnable() { + @Override + public void run() { + unsubscribed.set(true); + t.interrupt(); + latch.countDown(); + } + })); + t.start(); + } + }); + + s1.toFlowable().subscribe(ts); + + Thread.sleep(100); + + ts.cancel(); + + if (latch.await(1000, TimeUnit.MILLISECONDS)) { + assertTrue(unsubscribed.get()); + assertTrue(interrupted.get()); + } else { + fail("timed out waiting for latch"); + } + } + + /** + * Assert that unsubscribe propagates when passing in a SingleObserver and not a Subscriber. + * @throws InterruptedException if the test is interrupted + */ + @Test + public void unsubscribe2() throws InterruptedException { + final SerialDisposable sd = new SerialDisposable(); + SingleObserver<String> ts = new SingleObserver<String>() { + + @Override + public void onSubscribe(Disposable d) { + sd.replace(d); + } + + @Override + public void onSuccess(String value) { + // not interested in value + } + + @Override + public void onError(Throwable error) { + // not interested in value + } + + }; + final AtomicBoolean unsubscribed = new AtomicBoolean(); + final AtomicBoolean interrupted = new AtomicBoolean(); + final CountDownLatch latch = new CountDownLatch(2); + + Single<String> s1 = Single.unsafeCreate(new SingleSource<String>() { + @Override + public void subscribe(final SingleObserver<? super String> observer) { + SerialDisposable sd = new SerialDisposable(); + observer.onSubscribe(sd); + final Thread t = new Thread(new Runnable() { + + @Override + public void run() { + try { + Thread.sleep(5000); + observer.onSuccess("success"); + } catch (InterruptedException e) { + interrupted.set(true); + latch.countDown(); + } + } + + }); + sd.replace(Disposable.fromRunnable(new Runnable() { + @Override + public void run() { + unsubscribed.set(true); + t.interrupt(); + latch.countDown(); + } + })); + t.start(); + + } + }); + + s1.subscribe(ts); + + Thread.sleep(100); + + sd.dispose(); + + if (latch.await(1000, TimeUnit.MILLISECONDS)) { + assertTrue(unsubscribed.get()); + assertTrue(interrupted.get()); + } else { + fail("timed out waiting for latch"); + } + } + + /** + * Assert that unsubscribe propagates when passing in a SingleObserver and not a Subscriber. + * @throws InterruptedException if the test is interrupted + */ + @Test + public void unsubscribeViaReturnedSubscription() throws InterruptedException { + final AtomicBoolean unsubscribed = new AtomicBoolean(); + final AtomicBoolean interrupted = new AtomicBoolean(); + final CountDownLatch latch = new CountDownLatch(2); + + Single<String> s1 = Single.unsafeCreate(new SingleSource<String>() { + @Override + public void subscribe(final SingleObserver<? super String> observer) { + SerialDisposable sd = new SerialDisposable(); + observer.onSubscribe(sd); + final Thread t = new Thread(new Runnable() { + + @Override + public void run() { + try { + Thread.sleep(5000); + observer.onSuccess("success"); + } catch (InterruptedException e) { + interrupted.set(true); + latch.countDown(); + } + } + + }); + sd.replace(Disposable.fromRunnable(new Runnable() { + @Override + public void run() { + unsubscribed.set(true); + t.interrupt(); + latch.countDown(); + } + })); + t.start(); + + } + }); + + Disposable subscription = s1.subscribe(); + + Thread.sleep(100); + + subscription.dispose(); + + if (latch.await(1000, TimeUnit.MILLISECONDS)) { + assertTrue(unsubscribed.get()); + assertTrue(interrupted.get()); + } else { + fail("timed out waiting for latch"); + } + } + + @Test + public void backpressureAsObservable() { + Single<String> s = Single.unsafeCreate(new SingleSource<String>() { + @Override + public void subscribe(SingleObserver<? super String> t) { + t.onSubscribe(Disposable.empty()); + t.onSuccess("hello"); + } + }); + + TestSubscriber<String> ts = new TestSubscriber<>(0L); + + s.toFlowable().subscribe(ts); + + ts.assertNoValues(); + + ts.request(1); + + ts.assertValue("hello"); + } + + @Test + public void toObservable() { + Flowable<String> a = Single.just("a").toFlowable(); + TestSubscriber<String> ts = new TestSubscriber<>(); + a.subscribe(ts); + ts.assertValue("a"); + ts.assertNoErrors(); + ts.assertComplete(); + } + + @Test(expected = NullPointerException.class) + public void doOnEventNullEvent() { + Single.just(1).doOnEvent(null); + } + + @Test + public void doOnEventComplete() { + final AtomicInteger atomicInteger = new AtomicInteger(0); + + Single.just(1).doOnEvent(new BiConsumer<Integer, Throwable>() { + @Override + public void accept(final Integer integer, final Throwable throwable) throws Exception { + if (integer != null) { + atomicInteger.incrementAndGet(); + } + } + }).subscribe(); + + assertEquals(1, atomicInteger.get()); + } + + @Test + public void doOnEventError() { + final AtomicInteger atomicInteger = new AtomicInteger(0); + + Single.error(new RuntimeException()).doOnEvent(new BiConsumer<Object, Throwable>() { + @Override + public void accept(final Object o, final Throwable throwable) throws Exception { + if (throwable != null) { + atomicInteger.incrementAndGet(); + } + } + }).subscribe(); + + assertEquals(1, atomicInteger.get()); + } + + @Test + public void toFuture() throws Exception { + assertEquals(1, Single.just(1).toFuture().get().intValue()); + } + + @Test + public void toFutureThrows() throws Exception { + try { + Single.error(new TestException()).toFuture().get(); + } catch (ExecutionException ex) { + assertTrue(ex.toString(), ex.getCause() instanceof TestException); + } + } + + @Test(expected = UnsupportedOperationException.class) + public void toFlowableIterableRemove() { + Iterable<? extends Flowable<Integer>> f = SingleInternalHelper.iterableToFlowable(Arrays.asList(Single.just(1))); + + Iterator<? extends Flowable<Integer>> iterator = f.iterator(); + iterator.next(); + iterator.remove(); + } + + @Test + public void zipIterableObject() { + final List<Single<Integer>> singles = Arrays.asList(Single.just(1), Single.just(4)); + Single.zip(singles, new Function<Object[], Object>() { + @Override + public Object apply(final Object[] o) throws Exception { + int sum = 0; + for (Object i : o) { + sum += (Integer) i; + } + return sum; + } + }).test().assertResult(5); + } + + @Test + public void to() { + Single.just(1).to(new SingleConverter<Integer, Flowable<Integer>>() { + @Override + public Flowable<Integer> apply(Single<Integer> v) { + return v.toFlowable(); + } + }) + .test() + .assertResult(1); + } + + @Test(expected = NullPointerException.class) + public void fromObservableNull() { + Single.fromObservable(null); + } + + @Test + public void fromObservableEmpty() { + Single.fromObservable(Observable.empty()) + .test() + .assertFailure(NoSuchElementException.class); + } + + @Test + public void fromObservableMoreThan1Elements() { + Single.fromObservable(Observable.just(1, 2)) + .to(TestHelper.<Integer>testConsumer()) + .assertFailure(IllegalArgumentException.class) + .assertErrorMessage("Sequence contains more than one element!"); + } + + @Test + public void fromObservableOneElement() { + Single.fromObservable(Observable.just(1)) + .test() + .assertResult(1); + } + + @Test + public void fromObservableError() { + Single.fromObservable(Observable.error(new RuntimeException("some error"))) + .to(TestHelper.testConsumer()) + .assertFailure(RuntimeException.class) + .assertErrorMessage("some error"); + } + + @Test(expected = NullPointerException.class) + public void implementationThrows() { + new Single<Integer>() { + @Override + protected void subscribeActual(SingleObserver<? super Integer> observer) { + throw new NullPointerException(); + } + }.test(); + } +} + diff --git a/src/test/java/io/reactivex/rxjava3/single/SingleTimerTest.java b/src/test/java/io/reactivex/rxjava3/single/SingleTimerTest.java new file mode 100644 index 0000000000..7fefa15467 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/single/SingleTimerTest.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.single; + +import static org.junit.Assert.assertEquals; + +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.functions.Consumer; +import io.reactivex.rxjava3.schedulers.TestScheduler; + +public class SingleTimerTest extends RxJavaTest { + @Test + public void timer() { + final TestScheduler testScheduler = new TestScheduler(); + + final AtomicLong atomicLong = new AtomicLong(); + Single.timer(2, TimeUnit.SECONDS, testScheduler).subscribe(new Consumer<Long>() { + @Override + public void accept(final Long value) throws Exception { + atomicLong.incrementAndGet(); + } + }); + + assertEquals(0, atomicLong.get()); + + testScheduler.advanceTimeBy(1, TimeUnit.SECONDS); + + assertEquals(0, atomicLong.get()); + + testScheduler.advanceTimeBy(1, TimeUnit.SECONDS); + + assertEquals(1, atomicLong.get()); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/subjects/AsyncSubjectTest.java b/src/test/java/io/reactivex/rxjava3/subjects/AsyncSubjectTest.java new file mode 100644 index 0000000000..8228b74b5d --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/subjects/AsyncSubjectTest.java @@ -0,0 +1,528 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.subjects; + +import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +import org.junit.Test; +import org.mockito.*; + +import io.reactivex.rxjava3.core.Observer; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.Consumer; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.operators.QueueFuseable; +import io.reactivex.rxjava3.testsupport.*; + +public class AsyncSubjectTest extends SubjectTest<Integer> { + + private final Throwable testException = new Throwable(); + + @Override + protected Subject<Integer> create() { + return AsyncSubject.create(); + } + + @Test + public void neverCompleted() { + AsyncSubject<String> subject = AsyncSubject.create(); + + Observer<String> observer = TestHelper.mockObserver(); + subject.subscribe(observer); + + subject.onNext("one"); + subject.onNext("two"); + subject.onNext("three"); + + verify(observer, Mockito.never()).onNext(anyString()); + verify(observer, Mockito.never()).onError(testException); + verify(observer, Mockito.never()).onComplete(); + } + + @Test + public void completed() { + AsyncSubject<String> subject = AsyncSubject.create(); + + Observer<String> observer = TestHelper.mockObserver(); + subject.subscribe(observer); + + subject.onNext("one"); + subject.onNext("two"); + subject.onNext("three"); + subject.onComplete(); + + verify(observer, times(1)).onNext("three"); + verify(observer, Mockito.never()).onError(any(Throwable.class)); + verify(observer, times(1)).onComplete(); + } + + @Test + public void subscribeAfterCompleted() { + AsyncSubject<String> subject = AsyncSubject.create(); + + Observer<String> observer = TestHelper.mockObserver(); + + subject.onNext("one"); + subject.onNext("two"); + subject.onNext("three"); + subject.onComplete(); + + subject.subscribe(observer); + + verify(observer, times(1)).onNext("three"); + verify(observer, Mockito.never()).onError(any(Throwable.class)); + verify(observer, times(1)).onComplete(); + } + + @Test + public void subscribeAfterError() { + AsyncSubject<String> subject = AsyncSubject.create(); + + Observer<String> observer = TestHelper.mockObserver(); + + subject.onNext("one"); + subject.onNext("two"); + subject.onNext("three"); + + RuntimeException re = new RuntimeException("failed"); + subject.onError(re); + + subject.subscribe(observer); + + verify(observer, times(1)).onError(re); + verify(observer, Mockito.never()).onNext(any(String.class)); + verify(observer, Mockito.never()).onComplete(); + } + + @Test + @SuppressUndeliverable + public void error() { + AsyncSubject<String> subject = AsyncSubject.create(); + + Observer<String> observer = TestHelper.mockObserver(); + subject.subscribe(observer); + + subject.onNext("one"); + subject.onNext("two"); + subject.onNext("three"); + subject.onError(testException); + subject.onNext("four"); + subject.onError(new Throwable()); + subject.onComplete(); + + verify(observer, Mockito.never()).onNext(anyString()); + verify(observer, times(1)).onError(testException); + verify(observer, Mockito.never()).onComplete(); + } + + @Test + public void unsubscribeBeforeCompleted() { + AsyncSubject<String> subject = AsyncSubject.create(); + + Observer<String> observer = TestHelper.mockObserver(); + TestObserver<String> to = new TestObserver<>(observer); + subject.subscribe(to); + + subject.onNext("one"); + subject.onNext("two"); + + to.dispose(); + + verify(observer, Mockito.never()).onNext(anyString()); + verify(observer, Mockito.never()).onError(any(Throwable.class)); + verify(observer, Mockito.never()).onComplete(); + + subject.onNext("three"); + subject.onComplete(); + + verify(observer, Mockito.never()).onNext(anyString()); + verify(observer, Mockito.never()).onError(any(Throwable.class)); + verify(observer, Mockito.never()).onComplete(); + } + + @Test + public void emptySubjectCompleted() { + AsyncSubject<String> subject = AsyncSubject.create(); + + Observer<String> observer = TestHelper.mockObserver(); + subject.subscribe(observer); + + subject.onComplete(); + + InOrder inOrder = inOrder(observer); + inOrder.verify(observer, never()).onNext(null); + inOrder.verify(observer, never()).onNext(any(String.class)); + inOrder.verify(observer, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + } + + /** + * Can receive timeout if subscribe never receives an onError/onComplete ... which reveals a race condition. + */ + @Test + public void subscribeCompletionRaceCondition() { + /* + * With non-threadsafe code this fails most of the time on my dev laptop and is non-deterministic enough + * to act as a unit test to the race conditions. + * + * With the synchronization code in place I can not get this to fail on my laptop. + */ + for (int i = 0; i < 50; i++) { + final AsyncSubject<String> subject = AsyncSubject.create(); + final AtomicReference<String> value1 = new AtomicReference<>(); + + subject.subscribe(new Consumer<String>() { + + @Override + public void accept(String t1) { + try { + // simulate a slow observer + Thread.sleep(50); + } catch (InterruptedException e) { + e.printStackTrace(); + } + value1.set(t1); + } + + }); + + Thread t1 = new Thread(new Runnable() { + + @Override + public void run() { + subject.onNext("value"); + subject.onComplete(); + } + }); + + SubjectSubscriberThread t2 = new SubjectSubscriberThread(subject); + SubjectSubscriberThread t3 = new SubjectSubscriberThread(subject); + SubjectSubscriberThread t4 = new SubjectSubscriberThread(subject); + SubjectSubscriberThread t5 = new SubjectSubscriberThread(subject); + + t2.start(); + t3.start(); + t1.start(); + t4.start(); + t5.start(); + try { + t1.join(); + t2.join(); + t3.join(); + t4.join(); + t5.join(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + + assertEquals("value", value1.get()); + assertEquals("value", t2.value.get()); + assertEquals("value", t3.value.get()); + assertEquals("value", t4.value.get()); + assertEquals("value", t5.value.get()); + } + + } + + private static class SubjectSubscriberThread extends Thread { + + private final AsyncSubject<String> subject; + private final AtomicReference<String> value = new AtomicReference<>(); + + SubjectSubscriberThread(AsyncSubject<String> subject) { + this.subject = subject; + } + + @Override + public void run() { + try { + // a timeout exception will happen if we don't get a terminal state + String v = subject.timeout(2000, TimeUnit.MILLISECONDS).blockingSingle(); + value.set(v); + } catch (Exception e) { + e.printStackTrace(); + } + } + } + + @Test + public void currentStateMethodsNormal() { + AsyncSubject<Object> as = AsyncSubject.create(); + + assertFalse(as.hasValue()); + assertFalse(as.hasThrowable()); + assertFalse(as.hasComplete()); + assertNull(as.getValue()); + assertNull(as.getThrowable()); + + as.onNext(1); + + assertFalse(as.hasValue()); // AS no longer reports a value until it has completed + assertFalse(as.hasThrowable()); + assertFalse(as.hasComplete()); + assertNull(as.getValue()); // AS no longer reports a value until it has completed + assertNull(as.getThrowable()); + + as.onComplete(); + assertTrue(as.hasValue()); + assertFalse(as.hasThrowable()); + assertTrue(as.hasComplete()); + assertEquals(1, as.getValue()); + assertNull(as.getThrowable()); + } + + @Test + public void currentStateMethodsEmpty() { + AsyncSubject<Object> as = AsyncSubject.create(); + + assertFalse(as.hasValue()); + assertFalse(as.hasThrowable()); + assertFalse(as.hasComplete()); + assertNull(as.getValue()); + assertNull(as.getThrowable()); + + as.onComplete(); + + assertFalse(as.hasValue()); + assertFalse(as.hasThrowable()); + assertTrue(as.hasComplete()); + assertNull(as.getValue()); + assertNull(as.getThrowable()); + } + + @Test + public void currentStateMethodsError() { + AsyncSubject<Object> as = AsyncSubject.create(); + + assertFalse(as.hasValue()); + assertFalse(as.hasThrowable()); + assertFalse(as.hasComplete()); + assertNull(as.getValue()); + assertNull(as.getThrowable()); + + as.onError(new TestException()); + + assertFalse(as.hasValue()); + assertTrue(as.hasThrowable()); + assertFalse(as.hasComplete()); + assertNull(as.getValue()); + assertTrue(as.getThrowable() instanceof TestException); + } + + @Test + public void fusionLive() { + AsyncSubject<Integer> ap = new AsyncSubject<>(); + + TestObserverEx<Integer> to = ap.to(TestHelper.<Integer>testConsumer(false, QueueFuseable.ANY)); + + to.assertFuseable() + .assertFusionMode(QueueFuseable.ASYNC); + + to.assertNoValues().assertNoErrors().assertNotComplete(); + + ap.onNext(1); + + to.assertNoValues().assertNoErrors().assertNotComplete(); + + ap.onComplete(); + + to.assertResult(1); + } + + @Test + public void fusionOfflie() { + AsyncSubject<Integer> ap = new AsyncSubject<>(); + ap.onNext(1); + ap.onComplete(); + + TestObserverEx<Integer> to = ap.to(TestHelper.<Integer>testConsumer(false, QueueFuseable.ANY)); + + to.assertFuseable() + .assertFusionMode(QueueFuseable.ASYNC) + .assertResult(1); + } + + @Test + public void onSubscribeAfterDone() { + AsyncSubject<Object> p = AsyncSubject.create(); + + Disposable bs = Disposable.empty(); + p.onSubscribe(bs); + + assertFalse(bs.isDisposed()); + + p.onComplete(); + + bs = Disposable.empty(); + p.onSubscribe(bs); + + assertTrue(bs.isDisposed()); + + p.test().assertResult(); + } + + @Test + public void cancelUpfront() { + AsyncSubject<Object> p = AsyncSubject.create(); + + assertFalse(p.hasObservers()); + + p.test().assertEmpty(); + p.test().assertEmpty(); + + p.test(true).assertEmpty(); + + assertTrue(p.hasObservers()); + } + + @Test + public void cancelRace() { + AsyncSubject<Object> p = AsyncSubject.create(); + + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final TestObserver<Object> to1 = p.test(); + final TestObserver<Object> to2 = p.test(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + to1.dispose(); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + to2.dispose(); + } + }; + + TestHelper.race(r1, r2); + } + } + + @Test + @SuppressUndeliverable + public void onErrorCancelRace() { + + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final AsyncSubject<Object> p = AsyncSubject.create(); + + final TestObserverEx<Object> to1 = p.to(TestHelper.testConsumer()); + + Runnable r1 = new Runnable() { + @Override + public void run() { + to1.dispose(); + } + }; + + final TestException ex = new TestException(); + + Runnable r2 = new Runnable() { + @Override + public void run() { + p.onError(ex); + } + }; + + TestHelper.race(r1, r2); + + if (to1.errors().size() != 0) { + to1.assertFailure(TestException.class); + } else { + to1.assertEmpty(); + } + } + } + + @Test + public void onNextCrossCancel() { + AsyncSubject<Object> p = AsyncSubject.create(); + + final TestObserver<Object> to2 = new TestObserver<>(); + TestObserver<Object> to1 = new TestObserver<Object>() { + @Override + public void onNext(Object t) { + to2.dispose(); + super.onNext(t); + } + }; + + p.subscribe(to1); + p.subscribe(to2); + + p.onNext(1); + p.onComplete(); + + to1.assertResult(1); + to2.assertEmpty(); + } + + @Test + @SuppressUndeliverable + public void onErrorCrossCancel() { + AsyncSubject<Object> p = AsyncSubject.create(); + + final TestObserver<Object> to2 = new TestObserver<>(); + TestObserver<Object> to1 = new TestObserver<Object>() { + @Override + public void onError(Throwable t) { + to2.dispose(); + super.onError(t); + } + }; + + p.subscribe(to1); + p.subscribe(to2); + + p.onError(new TestException()); + + to1.assertFailure(TestException.class); + to2.assertEmpty(); + } + + @Test + public void onCompleteCrossCancel() { + AsyncSubject<Object> p = AsyncSubject.create(); + + final TestObserver<Object> to2 = new TestObserver<>(); + TestObserver<Object> to1 = new TestObserver<Object>() { + @Override + public void onComplete() { + to2.dispose(); + super.onComplete(); + } + }; + + p.subscribe(to1); + p.subscribe(to2); + + p.onComplete(); + + to1.assertResult(); + to2.assertEmpty(); + } + + @Test + public void dispose() { + TestHelper.checkDisposed(AsyncSubject.create()); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/subjects/BehaviorSubjectTest.java b/src/test/java/io/reactivex/rxjava3/subjects/BehaviorSubjectTest.java new file mode 100644 index 0000000000..8871ee8e7e --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/subjects/BehaviorSubjectTest.java @@ -0,0 +1,851 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.subjects; + +import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import java.util.List; +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicReference; + +import org.junit.*; +import org.mockito.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.observers.*; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.schedulers.Schedulers; +import io.reactivex.rxjava3.subjects.BehaviorSubject.BehaviorDisposable; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class BehaviorSubjectTest extends SubjectTest<Integer> { + + private final Throwable testException = new Throwable(); + + @Override + protected Subject<Integer> create() { + return BehaviorSubject.create(); + } + + @Test + public void thatSubscriberReceivesDefaultValueAndSubsequentEvents() { + BehaviorSubject<String> subject = BehaviorSubject.createDefault("default"); + + Observer<String> observer = TestHelper.mockObserver(); + subject.subscribe(observer); + + subject.onNext("one"); + subject.onNext("two"); + subject.onNext("three"); + + verify(observer, times(1)).onNext("default"); + verify(observer, times(1)).onNext("one"); + verify(observer, times(1)).onNext("two"); + verify(observer, times(1)).onNext("three"); + verify(observer, Mockito.never()).onError(testException); + verify(observer, Mockito.never()).onComplete(); + } + + @Test + public void thatSubscriberReceivesLatestAndThenSubsequentEvents() { + BehaviorSubject<String> subject = BehaviorSubject.createDefault("default"); + + subject.onNext("one"); + + Observer<String> observer = TestHelper.mockObserver(); + subject.subscribe(observer); + + subject.onNext("two"); + subject.onNext("three"); + + verify(observer, Mockito.never()).onNext("default"); + verify(observer, times(1)).onNext("one"); + verify(observer, times(1)).onNext("two"); + verify(observer, times(1)).onNext("three"); + verify(observer, Mockito.never()).onError(testException); + verify(observer, Mockito.never()).onComplete(); + } + + @Test + public void subscribeThenOnComplete() { + BehaviorSubject<String> subject = BehaviorSubject.createDefault("default"); + + Observer<String> observer = TestHelper.mockObserver(); + subject.subscribe(observer); + + subject.onNext("one"); + subject.onComplete(); + + verify(observer, times(1)).onNext("default"); + verify(observer, times(1)).onNext("one"); + verify(observer, Mockito.never()).onError(any(Throwable.class)); + verify(observer, times(1)).onComplete(); + } + + @Test + public void subscribeToCompletedOnlyEmitsOnComplete() { + BehaviorSubject<String> subject = BehaviorSubject.createDefault("default"); + subject.onNext("one"); + subject.onComplete(); + + Observer<String> observer = TestHelper.mockObserver(); + subject.subscribe(observer); + + verify(observer, never()).onNext("default"); + verify(observer, never()).onNext("one"); + verify(observer, Mockito.never()).onError(any(Throwable.class)); + verify(observer, times(1)).onComplete(); + } + + @Test + public void subscribeToErrorOnlyEmitsOnError() { + BehaviorSubject<String> subject = BehaviorSubject.createDefault("default"); + subject.onNext("one"); + RuntimeException re = new RuntimeException("test error"); + subject.onError(re); + + Observer<String> observer = TestHelper.mockObserver(); + subject.subscribe(observer); + + verify(observer, never()).onNext("default"); + verify(observer, never()).onNext("one"); + verify(observer, times(1)).onError(re); + verify(observer, never()).onComplete(); + } + + @Test + public void completedStopsEmittingData() { + BehaviorSubject<Integer> channel = BehaviorSubject.createDefault(2013); + Observer<Object> observerA = TestHelper.mockObserver(); + Observer<Object> observerB = TestHelper.mockObserver(); + Observer<Object> observerC = TestHelper.mockObserver(); + + TestObserver<Object> to = new TestObserver<>(observerA); + + channel.subscribe(to); + channel.subscribe(observerB); + + InOrder inOrderA = inOrder(observerA); + InOrder inOrderB = inOrder(observerB); + InOrder inOrderC = inOrder(observerC); + + inOrderA.verify(observerA).onNext(2013); + inOrderB.verify(observerB).onNext(2013); + + channel.onNext(42); + + inOrderA.verify(observerA).onNext(42); + inOrderB.verify(observerB).onNext(42); + + to.dispose(); + inOrderA.verifyNoMoreInteractions(); + + channel.onNext(4711); + + inOrderB.verify(observerB).onNext(4711); + + channel.onComplete(); + + inOrderB.verify(observerB).onComplete(); + + channel.subscribe(observerC); + + inOrderC.verify(observerC).onComplete(); + + channel.onNext(13); + + inOrderB.verifyNoMoreInteractions(); + inOrderC.verifyNoMoreInteractions(); + } + + @Test + public void completedAfterErrorIsNotSent() { + BehaviorSubject<String> subject = BehaviorSubject.createDefault("default"); + + Observer<String> observer = TestHelper.mockObserver(); + subject.subscribe(observer); + + subject.onNext("one"); + subject.onError(testException); + subject.onNext("two"); + subject.onComplete(); + + verify(observer, times(1)).onNext("default"); + verify(observer, times(1)).onNext("one"); + verify(observer, times(1)).onError(testException); + verify(observer, never()).onNext("two"); + verify(observer, never()).onComplete(); + } + + @Test + public void completedAfterErrorIsNotSent2() { + BehaviorSubject<String> subject = BehaviorSubject.createDefault("default"); + + Observer<String> observer = TestHelper.mockObserver(); + subject.subscribe(observer); + + subject.onNext("one"); + subject.onError(testException); + subject.onNext("two"); + subject.onComplete(); + + verify(observer, times(1)).onNext("default"); + verify(observer, times(1)).onNext("one"); + verify(observer, times(1)).onError(testException); + verify(observer, never()).onNext("two"); + verify(observer, never()).onComplete(); + + Observer<Object> o2 = TestHelper.mockObserver(); + subject.subscribe(o2); + verify(o2, times(1)).onError(testException); + verify(o2, never()).onNext(any()); + verify(o2, never()).onComplete(); + } + + @Test + public void completedAfterErrorIsNotSent3() { + BehaviorSubject<String> subject = BehaviorSubject.createDefault("default"); + + Observer<String> observer = TestHelper.mockObserver(); + subject.subscribe(observer); + + subject.onNext("one"); + subject.onComplete(); + subject.onNext("two"); + subject.onComplete(); + + verify(observer, times(1)).onNext("default"); + verify(observer, times(1)).onNext("one"); + verify(observer, times(1)).onComplete(); + verify(observer, never()).onError(any(Throwable.class)); + verify(observer, never()).onNext("two"); + + Observer<Object> o2 = TestHelper.mockObserver(); + subject.subscribe(o2); + verify(o2, times(1)).onComplete(); + verify(o2, never()).onNext(any()); + verify(observer, never()).onError(any(Throwable.class)); + } + + @Test + public void unsubscriptionCase() { + BehaviorSubject<String> src = BehaviorSubject.createDefault("null"); // FIXME was plain null which is not allowed + + for (int i = 0; i < 10; i++) { + final Observer<Object> o = TestHelper.mockObserver(); + InOrder inOrder = inOrder(o); + String v = "" + i; + src.onNext(v); + System.out.printf("Turn: %d%n", i); + src.firstElement() + .toObservable() + .flatMap(new Function<String, Observable<String>>() { + + @Override + public Observable<String> apply(String t1) { + return Observable.just(t1 + ", " + t1); + } + }) + .subscribe(new DefaultObserver<String>() { + @Override + public void onNext(String t) { + o.onNext(t); + } + + @Override + public void onError(Throwable e) { + o.onError(e); + } + + @Override + public void onComplete() { + o.onComplete(); + } + }); + inOrder.verify(o).onNext(v + ", " + v); + inOrder.verify(o).onComplete(); + verify(o, never()).onError(any(Throwable.class)); + } + } + + @Test + public void startEmpty() { + BehaviorSubject<Integer> source = BehaviorSubject.create(); + final Observer<Object> o = TestHelper.mockObserver(); + InOrder inOrder = inOrder(o); + + source.subscribe(o); + + inOrder.verify(o, never()).onNext(any()); + inOrder.verify(o, never()).onComplete(); + + source.onNext(1); + + source.onComplete(); + + source.onNext(2); + + verify(o, never()).onError(any(Throwable.class)); + + inOrder.verify(o).onNext(1); + inOrder.verify(o).onComplete(); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void startEmptyThenAddOne() { + BehaviorSubject<Integer> source = BehaviorSubject.create(); + final Observer<Object> o = TestHelper.mockObserver(); + InOrder inOrder = inOrder(o); + + source.onNext(1); + + source.subscribe(o); + + inOrder.verify(o).onNext(1); + + source.onComplete(); + + source.onNext(2); + + inOrder.verify(o).onComplete(); + inOrder.verifyNoMoreInteractions(); + + verify(o, never()).onError(any(Throwable.class)); + + } + + @Test + public void startEmptyCompleteWithOne() { + BehaviorSubject<Integer> source = BehaviorSubject.create(); + final Observer<Object> o = TestHelper.mockObserver(); + + source.onNext(1); + source.onComplete(); + + source.onNext(2); + + source.subscribe(o); + + verify(o).onComplete(); + verify(o, never()).onError(any(Throwable.class)); + verify(o, never()).onNext(any()); + } + + @Test + public void takeOneSubscriber() { + BehaviorSubject<Integer> source = BehaviorSubject.createDefault(1); + final Observer<Object> o = TestHelper.mockObserver(); + + source.take(1).subscribe(o); + + verify(o).onNext(1); + verify(o).onComplete(); + verify(o, never()).onError(any(Throwable.class)); + + assertEquals(0, source.subscriberCount()); + assertFalse(source.hasObservers()); + } + + @Test + public void emissionSubscriptionRace() throws Exception { + Scheduler s = Schedulers.io(); + Scheduler.Worker worker = Schedulers.io().createWorker(); + try { + for (int i = 0; i < 50000; i++) { + if (i % 1000 == 0) { + System.out.println(i); + } + final BehaviorSubject<Object> rs = BehaviorSubject.create(); + + final CountDownLatch finish = new CountDownLatch(1); + final CountDownLatch start = new CountDownLatch(1); + + worker.schedule(new Runnable() { + @Override + public void run() { + try { + start.await(); + } catch (Exception e1) { + e1.printStackTrace(); + } + rs.onNext(1); + } + }); + + final AtomicReference<Object> o = new AtomicReference<>(); + + rs.subscribeOn(s).observeOn(Schedulers.io()) + .subscribe(new DefaultObserver<Object>() { + + @Override + public void onComplete() { + o.set(-1); + finish.countDown(); + } + + @Override + public void onError(Throwable e) { + o.set(e); + finish.countDown(); + } + + @Override + public void onNext(Object t) { + o.set(t); + finish.countDown(); + } + + }); + start.countDown(); + + if (!finish.await(5, TimeUnit.SECONDS)) { + System.out.println(o.get()); + System.out.println(rs.hasObservers()); + rs.onComplete(); + Assert.fail("Timeout @ " + i); + break; + } else { + Assert.assertEquals(1, o.get()); + worker.schedule(new Runnable() { + @Override + public void run() { + rs.onComplete(); + } + }); + } + } + } finally { + worker.dispose(); + } + } + + @Test + public void currentStateMethodsNormalEmptyStart() { + BehaviorSubject<Object> as = BehaviorSubject.create(); + + assertFalse(as.hasValue()); + assertFalse(as.hasThrowable()); + assertFalse(as.hasComplete()); + assertNull(as.getValue()); + assertNull(as.getThrowable()); + + as.onNext(1); + + assertTrue(as.hasValue()); + assertFalse(as.hasThrowable()); + assertFalse(as.hasComplete()); + assertEquals(1, as.getValue()); + assertNull(as.getThrowable()); + + as.onComplete(); + + assertFalse(as.hasValue()); + assertFalse(as.hasThrowable()); + assertTrue(as.hasComplete()); + assertNull(as.getValue()); + assertNull(as.getThrowable()); + } + + @Test + public void currentStateMethodsNormalSomeStart() { + BehaviorSubject<Object> as = BehaviorSubject.createDefault((Object)1); + + assertTrue(as.hasValue()); + assertFalse(as.hasThrowable()); + assertFalse(as.hasComplete()); + assertEquals(1, as.getValue()); + assertNull(as.getThrowable()); + + as.onNext(2); + + assertTrue(as.hasValue()); + assertFalse(as.hasThrowable()); + assertFalse(as.hasComplete()); + assertEquals(2, as.getValue()); + assertNull(as.getThrowable()); + + as.onComplete(); + assertFalse(as.hasValue()); + assertFalse(as.hasThrowable()); + assertTrue(as.hasComplete()); + assertNull(as.getValue()); + assertNull(as.getThrowable()); + } + + @Test + public void currentStateMethodsEmpty() { + BehaviorSubject<Object> as = BehaviorSubject.create(); + + assertFalse(as.hasValue()); + assertFalse(as.hasThrowable()); + assertFalse(as.hasComplete()); + assertNull(as.getValue()); + assertNull(as.getThrowable()); + + as.onComplete(); + + assertFalse(as.hasValue()); + assertFalse(as.hasThrowable()); + assertTrue(as.hasComplete()); + assertNull(as.getValue()); + assertNull(as.getThrowable()); + } + + @Test + public void currentStateMethodsError() { + BehaviorSubject<Object> as = BehaviorSubject.create(); + + assertFalse(as.hasValue()); + assertFalse(as.hasThrowable()); + assertFalse(as.hasComplete()); + assertNull(as.getValue()); + assertNull(as.getThrowable()); + + as.onError(new TestException()); + + assertFalse(as.hasValue()); + assertTrue(as.hasThrowable()); + assertFalse(as.hasComplete()); + assertNull(as.getValue()); + assertTrue(as.getThrowable() instanceof TestException); + } + + @Test + public void cancelOnArrival() { + BehaviorSubject<Object> p = BehaviorSubject.create(); + + assertFalse(p.hasObservers()); + + p.test(true).assertEmpty(); + + assertFalse(p.hasObservers()); + } + + @Test + public void onSubscribe() { + BehaviorSubject<Object> p = BehaviorSubject.create(); + + Disposable bs = Disposable.empty(); + + p.onSubscribe(bs); + + assertFalse(bs.isDisposed()); + + p.onComplete(); + + bs = Disposable.empty(); + + p.onSubscribe(bs); + + assertTrue(bs.isDisposed()); + } + + @Test + public void onErrorAfterComplete() { + BehaviorSubject<Object> p = BehaviorSubject.create(); + + p.onComplete(); + + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + p.onError(new TestException()); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void cancelOnArrival2() { + BehaviorSubject<Object> p = BehaviorSubject.create(); + + TestObserver<Object> to = p.test(); + + p.test(true).assertEmpty(); + + p.onNext(1); + p.onComplete(); + + to.assertResult(1); + } + + @Test + public void addRemoveRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final BehaviorSubject<Object> p = BehaviorSubject.create(); + + final TestObserver<Object> to = p.test(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + p.test(); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + to.dispose(); + } + }; + + TestHelper.race(r1, r2); + } + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + @Test + public void subscribeOnNextRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final BehaviorSubject<Object> p = BehaviorSubject.createDefault((Object)1); + + final TestObserver[] to = { null }; + + Runnable r1 = new Runnable() { + @Override + public void run() { + to[0] = p.test(); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + p.onNext(2); + } + }; + + TestHelper.race(r1, r2); + + if (to[0].values().size() == 1) { + to[0].assertValue(2).assertNoErrors().assertNotComplete(); + } else { + to[0].assertValues(1, 2).assertNoErrors().assertNotComplete(); + } + } + } + + @Test + public void innerDisposed() { + BehaviorSubject.create() + .subscribe(new Observer<Object>() { + @Override + public void onSubscribe(Disposable d) { + assertFalse(d.isDisposed()); + + d.dispose(); + + assertTrue(d.isDisposed()); + } + + @Override + public void onNext(Object value) { + + } + + @Override + public void onError(Throwable e) { + + } + + @Override + public void onComplete() { + + } + }); + } + + @Test + public void completeSubscribeRace() throws Exception { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final BehaviorSubject<Object> p = BehaviorSubject.create(); + + final TestObserver<Object> to = new TestObserver<>(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + p.subscribe(to); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + p.onComplete(); + } + }; + + TestHelper.race(r1, r2); + + to.assertResult(); + } + } + + @Test + public void errorSubscribeRace() throws Exception { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final BehaviorSubject<Object> p = BehaviorSubject.create(); + + final TestObserver<Object> to = new TestObserver<>(); + + final TestException ex = new TestException(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + p.subscribe(to); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + p.onError(ex); + } + }; + + TestHelper.race(r1, r2); + + to.assertFailure(TestException.class); + } + } + + @Test + public void behaviorDisposableDisposeState() { + BehaviorSubject<Integer> bs = BehaviorSubject.create(); + bs.onNext(1); + + TestObserver<Integer> to = new TestObserver<>(); + + BehaviorDisposable<Integer> bd = new BehaviorDisposable<>(to, bs); + to.onSubscribe(bd); + + assertFalse(bd.isDisposed()); + + bd.dispose(); + + assertTrue(bd.isDisposed()); + + bd.dispose(); + + assertTrue(bd.isDisposed()); + + assertTrue(bd.test(2)); + + bd.emitFirst(); + + to.assertEmpty(); + + bd.emitNext(2, 0); + } + + @Test + public void emitFirstDisposeRace() { + for (int i = 0; i < TestHelper.RACE_LONG_LOOPS; i++) { + + BehaviorSubject<Integer> bs = BehaviorSubject.create(); + bs.onNext(1); + + TestObserver<Integer> to = new TestObserver<>(); + + final BehaviorDisposable<Integer> bd = new BehaviorDisposable<>(to, bs); + to.onSubscribe(bd); + + Runnable r1 = new Runnable() { + @Override + public void run() { + bd.emitFirst(); + } + }; + Runnable r2 = new Runnable() { + @Override + public void run() { + bd.dispose(); + } + }; + + TestHelper.race(r1, r2); + } + } + + @Test + public void emitNextDisposeRace() { + for (int i = 0; i < TestHelper.RACE_LONG_LOOPS; i++) { + + BehaviorSubject<Integer> bs = BehaviorSubject.create(); + bs.onNext(1); + + TestObserver<Integer> to = new TestObserver<>(); + + final BehaviorDisposable<Integer> bd = new BehaviorDisposable<>(to, bs); + to.onSubscribe(bd); + + Runnable r1 = new Runnable() { + @Override + public void run() { + bd.emitNext(2, 0); + } + }; + Runnable r2 = new Runnable() { + @Override + public void run() { + bd.dispose(); + } + }; + + TestHelper.race(r1, r2); + } + } + + @Test + public void emittingEmitNext() { + BehaviorSubject<Integer> bs = BehaviorSubject.create(); + bs.onNext(1); + + TestObserver<Integer> to = new TestObserver<>(); + + final BehaviorDisposable<Integer> bd = new BehaviorDisposable<>(to, bs); + to.onSubscribe(bd); + + bd.emitting = true; + bd.emitNext(2, 1); + bd.emitNext(3, 2); + + assertNotNull(bd.queue); + } + + @Test + public void hasObservers() { + BehaviorSubject<Integer> bs = BehaviorSubject.create(); + + assertFalse(bs.hasObservers()); + + TestObserver<Integer> to = bs.test(); + + assertTrue(bs.hasObservers()); + + to.dispose(); + + assertFalse(bs.hasObservers()); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/subjects/CompletableSubjectTest.java b/src/test/java/io/reactivex/rxjava3/subjects/CompletableSubjectTest.java new file mode 100644 index 0000000000..6b5a7a6085 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/subjects/CompletableSubjectTest.java @@ -0,0 +1,230 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.subjects; + +import static org.junit.Assert.*; + +import java.io.IOException; +import java.util.List; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.*; +import io.reactivex.rxjava3.internal.util.ExceptionHelper; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class CompletableSubjectTest extends RxJavaTest { + + @Test + public void once() { + CompletableSubject cs = CompletableSubject.create(); + + TestObserver<Void> to = cs.test(); + + cs.onComplete(); + + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + cs.onError(new IOException()); + + TestHelper.assertUndeliverable(errors, 0, IOException.class); + } finally { + RxJavaPlugins.reset(); + } + cs.onComplete(); + + to.assertResult(); + } + + @Test + public void error() { + CompletableSubject cs = CompletableSubject.create(); + + assertFalse(cs.hasComplete()); + assertFalse(cs.hasThrowable()); + assertNull(cs.getThrowable()); + assertFalse(cs.hasObservers()); + assertEquals(0, cs.observerCount()); + + TestObserver<Void> to = cs.test(); + + to.assertEmpty(); + + assertTrue(cs.hasObservers()); + assertEquals(1, cs.observerCount()); + + cs.onError(new IOException()); + + assertFalse(cs.hasComplete()); + assertTrue(cs.hasThrowable()); + assertTrue(cs.getThrowable().toString(), cs.getThrowable() instanceof IOException); + assertFalse(cs.hasObservers()); + assertEquals(0, cs.observerCount()); + + to.assertFailure(IOException.class); + + cs.test().assertFailure(IOException.class); + + assertFalse(cs.hasComplete()); + assertTrue(cs.hasThrowable()); + assertTrue(cs.getThrowable().toString(), cs.getThrowable() instanceof IOException); + assertFalse(cs.hasObservers()); + assertEquals(0, cs.observerCount()); + } + + @Test + public void complete() { + CompletableSubject cs = CompletableSubject.create(); + + assertFalse(cs.hasComplete()); + assertFalse(cs.hasThrowable()); + assertNull(cs.getThrowable()); + assertFalse(cs.hasObservers()); + assertEquals(0, cs.observerCount()); + + TestObserver<Void> to = cs.test(); + + to.assertEmpty(); + + assertTrue(cs.hasObservers()); + assertEquals(1, cs.observerCount()); + + cs.onComplete(); + + assertTrue(cs.hasComplete()); + assertFalse(cs.hasThrowable()); + assertNull(cs.getThrowable()); + assertFalse(cs.hasObservers()); + assertEquals(0, cs.observerCount()); + + to.assertResult(); + + cs.test().assertResult(); + + assertTrue(cs.hasComplete()); + assertFalse(cs.hasThrowable()); + assertNull(cs.getThrowable()); + assertFalse(cs.hasObservers()); + assertEquals(0, cs.observerCount()); + } + + @Test + public void nullThrowable() { + CompletableSubject cs = CompletableSubject.create(); + + try { + cs.onError(null); + fail("No NullPointerException thrown"); + } catch (NullPointerException ex) { + assertEquals(ExceptionHelper.nullWarning("onError called with a null Throwable."), ex.getMessage()); + } + + cs.test().assertEmpty().dispose(); + } + + @Test + public void cancelOnArrival() { + CompletableSubject.create() + .test(true) + .assertEmpty(); + } + + @Test + public void cancelOnArrival2() { + CompletableSubject cs = CompletableSubject.create(); + + cs.test(); + + cs + .test(true) + .assertEmpty(); + } + + @Test + public void dispose() { + TestHelper.checkDisposed(CompletableSubject.create()); + } + + @Test + public void disposeTwice() { + CompletableSubject.create() + .subscribe(new CompletableObserver() { + @Override + public void onSubscribe(Disposable d) { + assertFalse(d.isDisposed()); + + d.dispose(); + d.dispose(); + + assertTrue(d.isDisposed()); + } + + @Override + public void onError(Throwable e) { + + } + + @Override + public void onComplete() { + + } + }); + } + + @Test + public void onSubscribeDispose() { + CompletableSubject cs = CompletableSubject.create(); + + Disposable d = Disposable.empty(); + + cs.onSubscribe(d); + + assertFalse(d.isDisposed()); + + cs.onComplete(); + + d = Disposable.empty(); + + cs.onSubscribe(d); + + assertTrue(d.isDisposed()); + } + + @Test + public void addRemoveRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final CompletableSubject cs = CompletableSubject.create(); + + final TestObserver<Void> to = cs.test(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + cs.test(); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + to.dispose(); + } + }; + TestHelper.race(r1, r2); + } + } +} diff --git a/src/test/java/io/reactivex/rxjava3/subjects/MaybeSubjectTest.java b/src/test/java/io/reactivex/rxjava3/subjects/MaybeSubjectTest.java new file mode 100644 index 0000000000..c741f02508 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/subjects/MaybeSubjectTest.java @@ -0,0 +1,275 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.subjects; + +import static org.junit.Assert.*; + +import java.io.IOException; +import java.util.List; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.*; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class MaybeSubjectTest extends RxJavaTest { + + @Test + public void success() { + MaybeSubject<Integer> ms = MaybeSubject.create(); + + assertFalse(ms.hasValue()); + assertNull(ms.getValue()); + assertFalse(ms.hasComplete()); + assertFalse(ms.hasThrowable()); + assertNull(ms.getThrowable()); + assertFalse(ms.hasObservers()); + assertEquals(0, ms.observerCount()); + + TestObserver<Integer> to = ms.test(); + + to.assertEmpty(); + + assertTrue(ms.hasObservers()); + assertEquals(1, ms.observerCount()); + + ms.onSuccess(1); + + assertTrue(ms.hasValue()); + assertEquals(1, ms.getValue().intValue()); + assertFalse(ms.hasComplete()); + assertFalse(ms.hasThrowable()); + assertNull(ms.getThrowable()); + assertFalse(ms.hasObservers()); + assertEquals(0, ms.observerCount()); + + to.assertResult(1); + + ms.test().assertResult(1); + + assertTrue(ms.hasValue()); + assertEquals(1, ms.getValue().intValue()); + assertFalse(ms.hasComplete()); + assertFalse(ms.hasThrowable()); + assertNull(ms.getThrowable()); + assertFalse(ms.hasObservers()); + assertEquals(0, ms.observerCount()); + } + + @Test + public void once() { + MaybeSubject<Integer> ms = MaybeSubject.create(); + + TestObserver<Integer> to = ms.test(); + + ms.onSuccess(1); + ms.onSuccess(2); + + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + ms.onError(new IOException()); + + TestHelper.assertUndeliverable(errors, 0, IOException.class); + } finally { + RxJavaPlugins.reset(); + } + ms.onComplete(); + + to.assertResult(1); + } + + @Test + public void error() { + MaybeSubject<Integer> ms = MaybeSubject.create(); + + assertFalse(ms.hasValue()); + assertNull(ms.getValue()); + assertFalse(ms.hasComplete()); + assertFalse(ms.hasThrowable()); + assertNull(ms.getThrowable()); + assertFalse(ms.hasObservers()); + assertEquals(0, ms.observerCount()); + + TestObserver<Integer> to = ms.test(); + + to.assertEmpty(); + + assertTrue(ms.hasObservers()); + assertEquals(1, ms.observerCount()); + + ms.onError(new IOException()); + + assertFalse(ms.hasValue()); + assertNull(ms.getValue()); + assertFalse(ms.hasComplete()); + assertTrue(ms.hasThrowable()); + assertTrue(ms.getThrowable().toString(), ms.getThrowable() instanceof IOException); + assertFalse(ms.hasObservers()); + assertEquals(0, ms.observerCount()); + + to.assertFailure(IOException.class); + + ms.test().assertFailure(IOException.class); + + assertFalse(ms.hasValue()); + assertNull(ms.getValue()); + assertFalse(ms.hasComplete()); + assertTrue(ms.hasThrowable()); + assertTrue(ms.getThrowable().toString(), ms.getThrowable() instanceof IOException); + assertFalse(ms.hasObservers()); + assertEquals(0, ms.observerCount()); + } + + @Test + public void complete() { + MaybeSubject<Integer> ms = MaybeSubject.create(); + + assertFalse(ms.hasValue()); + assertNull(ms.getValue()); + assertFalse(ms.hasComplete()); + assertFalse(ms.hasThrowable()); + assertNull(ms.getThrowable()); + assertFalse(ms.hasObservers()); + assertEquals(0, ms.observerCount()); + + TestObserver<Integer> to = ms.test(); + + to.assertEmpty(); + + assertTrue(ms.hasObservers()); + assertEquals(1, ms.observerCount()); + + ms.onComplete(); + + assertFalse(ms.hasValue()); + assertNull(ms.getValue()); + assertTrue(ms.hasComplete()); + assertFalse(ms.hasThrowable()); + assertNull(ms.getThrowable()); + assertFalse(ms.hasObservers()); + assertEquals(0, ms.observerCount()); + + to.assertResult(); + + ms.test().assertResult(); + + assertFalse(ms.hasValue()); + assertNull(ms.getValue()); + assertTrue(ms.hasComplete()); + assertFalse(ms.hasThrowable()); + assertNull(ms.getThrowable()); + assertFalse(ms.hasObservers()); + assertEquals(0, ms.observerCount()); + } + + @Test + public void cancelOnArrival() { + MaybeSubject.create() + .test(true) + .assertEmpty(); + } + + @Test + public void cancelOnArrival2() { + MaybeSubject<Integer> ms = MaybeSubject.create(); + + ms.test(); + + ms + .test(true) + .assertEmpty(); + } + + @Test + public void dispose() { + TestHelper.checkDisposed(MaybeSubject.create()); + } + + @Test + public void disposeTwice() { + MaybeSubject.create() + .subscribe(new MaybeObserver<Object>() { + @Override + public void onSubscribe(Disposable d) { + assertFalse(d.isDisposed()); + + d.dispose(); + d.dispose(); + + assertTrue(d.isDisposed()); + } + + @Override + public void onSuccess(Object value) { + + } + + @Override + public void onError(Throwable e) { + + } + + @Override + public void onComplete() { + + } + }); + } + + @Test + public void onSubscribeDispose() { + MaybeSubject<Integer> ms = MaybeSubject.create(); + + Disposable d = Disposable.empty(); + + ms.onSubscribe(d); + + assertFalse(d.isDisposed()); + + ms.onComplete(); + + d = Disposable.empty(); + + ms.onSubscribe(d); + + assertTrue(d.isDisposed()); + } + + @Test + public void addRemoveRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final MaybeSubject<Integer> ms = MaybeSubject.create(); + + final TestObserver<Integer> to = ms.test(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + ms.test(); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + to.dispose(); + } + }; + TestHelper.race(r1, r2); + } + } +} diff --git a/src/test/java/io/reactivex/rxjava3/subjects/PublishSubjectTest.java b/src/test/java/io/reactivex/rxjava3/subjects/PublishSubjectTest.java new file mode 100644 index 0000000000..a4b464c512 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/subjects/PublishSubjectTest.java @@ -0,0 +1,628 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.subjects; + +import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import java.util.ArrayList; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.Test; +import org.mockito.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.observers.*; +import io.reactivex.rxjava3.testsupport.*; + +public class PublishSubjectTest extends SubjectTest<Integer> { + + @Override + protected Subject<Integer> create() { + return PublishSubject.create(); + } + + @Test + @SuppressUndeliverable + public void completed() { + PublishSubject<String> subject = PublishSubject.create(); + + Observer<String> observer = TestHelper.mockObserver(); + subject.subscribe(observer); + + subject.onNext("one"); + subject.onNext("two"); + subject.onNext("three"); + subject.onComplete(); + + Observer<String> anotherSubscriber = TestHelper.mockObserver(); + subject.subscribe(anotherSubscriber); + + subject.onNext("four"); + subject.onComplete(); + subject.onError(new Throwable()); + + assertCompletedSubscriber(observer); + // todo bug? assertNeverSubscriber(anotherSubscriber); + } + + @Test + public void completedStopsEmittingData() { + PublishSubject<Object> channel = PublishSubject.create(); + Observer<Object> observerA = TestHelper.mockObserver(); + Observer<Object> observerB = TestHelper.mockObserver(); + Observer<Object> observerC = TestHelper.mockObserver(); + + TestObserver<Object> to = new TestObserver<>(observerA); + + channel.subscribe(to); + channel.subscribe(observerB); + + InOrder inOrderA = inOrder(observerA); + InOrder inOrderB = inOrder(observerB); + InOrder inOrderC = inOrder(observerC); + + channel.onNext(42); + + inOrderA.verify(observerA).onNext(42); + inOrderB.verify(observerB).onNext(42); + + to.dispose(); + inOrderA.verifyNoMoreInteractions(); + + channel.onNext(4711); + + inOrderB.verify(observerB).onNext(4711); + + channel.onComplete(); + + inOrderB.verify(observerB).onComplete(); + + channel.subscribe(observerC); + + inOrderC.verify(observerC).onComplete(); + + channel.onNext(13); + + inOrderB.verifyNoMoreInteractions(); + inOrderC.verifyNoMoreInteractions(); + } + + private void assertCompletedSubscriber(Observer<String> observer) { + verify(observer, times(1)).onNext("one"); + verify(observer, times(1)).onNext("two"); + verify(observer, times(1)).onNext("three"); + verify(observer, Mockito.never()).onError(any(Throwable.class)); + verify(observer, times(1)).onComplete(); + } + + @Test + @SuppressUndeliverable + public void error() { + PublishSubject<String> subject = PublishSubject.create(); + + Observer<String> observer = TestHelper.mockObserver(); + subject.subscribe(observer); + + subject.onNext("one"); + subject.onNext("two"); + subject.onNext("three"); + subject.onError(testException); + + Observer<String> anotherSubscriber = TestHelper.mockObserver(); + subject.subscribe(anotherSubscriber); + + subject.onNext("four"); + subject.onError(new Throwable()); + subject.onComplete(); + + assertErrorSubscriber(observer); + // todo bug? assertNeverSubscriber(anotherSubscriber); + } + + private void assertErrorSubscriber(Observer<String> observer) { + verify(observer, times(1)).onNext("one"); + verify(observer, times(1)).onNext("two"); + verify(observer, times(1)).onNext("three"); + verify(observer, times(1)).onError(testException); + verify(observer, Mockito.never()).onComplete(); + } + + @Test + public void subscribeMidSequence() { + PublishSubject<String> subject = PublishSubject.create(); + + Observer<String> observer = TestHelper.mockObserver(); + subject.subscribe(observer); + + subject.onNext("one"); + subject.onNext("two"); + + assertObservedUntilTwo(observer); + + Observer<String> anotherSubscriber = TestHelper.mockObserver(); + subject.subscribe(anotherSubscriber); + + subject.onNext("three"); + subject.onComplete(); + + assertCompletedSubscriber(observer); + assertCompletedStartingWithThreeSubscriber(anotherSubscriber); + } + + private void assertCompletedStartingWithThreeSubscriber(Observer<String> observer) { + verify(observer, Mockito.never()).onNext("one"); + verify(observer, Mockito.never()).onNext("two"); + verify(observer, times(1)).onNext("three"); + verify(observer, Mockito.never()).onError(any(Throwable.class)); + verify(observer, times(1)).onComplete(); + } + + @Test + public void unsubscribeFirstSubscriber() { + PublishSubject<String> subject = PublishSubject.create(); + + Observer<String> observer = TestHelper.mockObserver(); + TestObserver<String> to = new TestObserver<>(observer); + subject.subscribe(to); + + subject.onNext("one"); + subject.onNext("two"); + + to.dispose(); + assertObservedUntilTwo(observer); + + Observer<String> anotherSubscriber = TestHelper.mockObserver(); + subject.subscribe(anotherSubscriber); + + subject.onNext("three"); + subject.onComplete(); + + assertObservedUntilTwo(observer); + assertCompletedStartingWithThreeSubscriber(anotherSubscriber); + } + + private void assertObservedUntilTwo(Observer<String> observer) { + verify(observer, times(1)).onNext("one"); + verify(observer, times(1)).onNext("two"); + verify(observer, Mockito.never()).onNext("three"); + verify(observer, Mockito.never()).onError(any(Throwable.class)); + verify(observer, Mockito.never()).onComplete(); + } + + @Test + public void nestedSubscribe() { + final PublishSubject<Integer> s = PublishSubject.create(); + + final AtomicInteger countParent = new AtomicInteger(); + final AtomicInteger countChildren = new AtomicInteger(); + final AtomicInteger countTotal = new AtomicInteger(); + + final ArrayList<String> list = new ArrayList<>(); + + s.flatMap(new Function<Integer, Observable<String>>() { + + @Override + public Observable<String> apply(final Integer v) { + countParent.incrementAndGet(); + + // then subscribe to subject again (it will not receive the previous value) + return s.map(new Function<Integer, String>() { + + @Override + public String apply(Integer v2) { + countChildren.incrementAndGet(); + return "Parent: " + v + " Child: " + v2; + } + + }); + } + + }).subscribe(new Consumer<String>() { + + @Override + public void accept(String v) { + countTotal.incrementAndGet(); + list.add(v); + } + + }); + + for (int i = 0; i < 10; i++) { + s.onNext(i); + } + s.onComplete(); + + // System.out.println("countParent: " + countParent.get()); + // System.out.println("countChildren: " + countChildren.get()); + // System.out.println("countTotal: " + countTotal.get()); + + // 9+8+7+6+5+4+3+2+1+0 == 45 + assertEquals(45, list.size()); + } + + /** + * Should be able to unsubscribe all Subscribers, have it stop emitting, then subscribe new ones and it start emitting again. + */ + @Test + public void reSubscribe() { + final PublishSubject<Integer> ps = PublishSubject.create(); + + Observer<Integer> o1 = TestHelper.mockObserver(); + TestObserver<Integer> to = new TestObserver<>(o1); + ps.subscribe(to); + + // emit + ps.onNext(1); + + // validate we got it + InOrder inOrder1 = inOrder(o1); + inOrder1.verify(o1, times(1)).onNext(1); + inOrder1.verifyNoMoreInteractions(); + + // unsubscribe + to.dispose(); + + // emit again but nothing will be there to receive it + ps.onNext(2); + + Observer<Integer> o2 = TestHelper.mockObserver(); + TestObserver<Integer> to2 = new TestObserver<>(o2); + ps.subscribe(to2); + + // emit + ps.onNext(3); + + // validate we got it + InOrder inOrder2 = inOrder(o2); + inOrder2.verify(o2, times(1)).onNext(3); + inOrder2.verifyNoMoreInteractions(); + + to2.dispose(); + } + + private final Throwable testException = new Throwable(); + + @Test + public void unsubscriptionCase() { + PublishSubject<String> src = PublishSubject.create(); + + for (int i = 0; i < 10; i++) { + final Observer<Object> o = TestHelper.mockObserver(); + InOrder inOrder = inOrder(o); + String v = "" + i; + System.out.printf("Turn: %d%n", i); + src.firstElement() + .toObservable() + .flatMap(new Function<String, Observable<String>>() { + + @Override + public Observable<String> apply(String t1) { + return Observable.just(t1 + ", " + t1); + } + }) + .subscribe(new DefaultObserver<String>() { + @Override + public void onNext(String t) { + o.onNext(t); + } + + @Override + public void onError(Throwable e) { + o.onError(e); + } + + @Override + public void onComplete() { + o.onComplete(); + } + }); + src.onNext(v); + + inOrder.verify(o).onNext(v + ", " + v); + inOrder.verify(o).onComplete(); + verify(o, never()).onError(any(Throwable.class)); + } + } + + @Test + public void currentStateMethodsNormal() { + PublishSubject<Object> as = PublishSubject.create(); + + assertFalse(as.hasThrowable()); + assertFalse(as.hasComplete()); + assertNull(as.getThrowable()); + + as.onNext(1); + + assertFalse(as.hasThrowable()); + assertFalse(as.hasComplete()); + assertNull(as.getThrowable()); + + as.onComplete(); + + assertFalse(as.hasThrowable()); + assertTrue(as.hasComplete()); + assertNull(as.getThrowable()); + } + + @Test + public void currentStateMethodsEmpty() { + PublishSubject<Object> as = PublishSubject.create(); + + assertFalse(as.hasThrowable()); + assertFalse(as.hasComplete()); + assertNull(as.getThrowable()); + + as.onComplete(); + + assertFalse(as.hasThrowable()); + assertTrue(as.hasComplete()); + assertNull(as.getThrowable()); + } + + @Test + public void currentStateMethodsError() { + PublishSubject<Object> as = PublishSubject.create(); + + assertFalse(as.hasThrowable()); + assertFalse(as.hasComplete()); + assertNull(as.getThrowable()); + + as.onError(new TestException()); + + assertTrue(as.hasThrowable()); + assertFalse(as.hasComplete()); + assertTrue(as.getThrowable() instanceof TestException); + } + + @Test + public void crossCancel() { + final TestObserver<Integer> to1 = new TestObserver<>(); + TestObserver<Integer> to2 = new TestObserver<Integer>() { + @Override + public void onNext(Integer t) { + super.onNext(t); + to1.dispose(); + } + }; + + PublishSubject<Integer> ps = PublishSubject.create(); + + ps.subscribe(to2); + ps.subscribe(to1); + + ps.onNext(1); + + to2.assertValue(1); + + to1.assertNoValues(); + } + + @Test + @SuppressUndeliverable + public void crossCancelOnError() { + final TestObserver<Integer> to1 = new TestObserver<>(); + TestObserver<Integer> to2 = new TestObserver<Integer>() { + @Override + public void onError(Throwable t) { + super.onError(t); + to1.dispose(); + } + }; + + PublishSubject<Integer> ps = PublishSubject.create(); + + ps.subscribe(to2); + ps.subscribe(to1); + + ps.onError(new TestException()); + + to2.assertError(TestException.class); + + to1.assertNoErrors(); + } + + @Test + public void crossCancelOnComplete() { + final TestObserver<Integer> to1 = new TestObserver<>(); + TestObserver<Integer> to2 = new TestObserver<Integer>() { + @Override + public void onComplete() { + super.onComplete(); + to1.dispose(); + } + }; + + PublishSubject<Integer> ps = PublishSubject.create(); + + ps.subscribe(to2); + ps.subscribe(to1); + + ps.onComplete(); + + to2.assertComplete(); + + to1.assertNotComplete(); + } + + @Test + public void onSubscribeCancelsImmediately() { + PublishSubject<Integer> ps = PublishSubject.create(); + + TestObserver<Integer> to = ps.test(); + + ps.subscribe(new Observer<Integer>() { + + @Override + public void onSubscribe(Disposable d) { + d.dispose(); + d.dispose(); + } + + @Override + public void onNext(Integer t) { + + } + + @Override + public void onError(Throwable t) { + + } + + @Override + public void onComplete() { + + } + + }); + + to.dispose(); + + assertFalse(ps.hasObservers()); + } + + @Test + public void terminateRace() throws Exception { + + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final PublishSubject<Integer> ps = PublishSubject.create(); + + TestObserver<Integer> to = ps.test(); + + Runnable task = new Runnable() { + @Override + public void run() { + ps.onComplete(); + } + }; + + TestHelper.race(task, task); + + to + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(); + } + } + + @Test + public void addRemoveRance() throws Exception { + + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final PublishSubject<Integer> ps = PublishSubject.create(); + + final TestObserver<Integer> to = ps.test(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + ps.subscribe(); + } + }; + Runnable r2 = new Runnable() { + @Override + public void run() { + to.dispose(); + } + }; + + TestHelper.race(r1, r2); + } + } + + @Test + public void addTerminateRance() throws Exception { + + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final PublishSubject<Integer> ps = PublishSubject.create(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + ps.subscribe(); + } + }; + Runnable r2 = new Runnable() { + @Override + public void run() { + ps.onComplete(); + } + }; + + TestHelper.race(r1, r2); + } + } + + @Test + public void addCompleteRance() throws Exception { + + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final PublishSubject<Integer> ps = PublishSubject.create(); + + final TestObserver<Integer> to = new TestObserver<>(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + ps.subscribe(to); + } + }; + Runnable r2 = new Runnable() { + @Override + public void run() { + ps.onComplete(); + } + }; + + TestHelper.race(r1, r2); + + to.awaitDone(5, TimeUnit.SECONDS) + .assertResult(); + } + } + + @Test + public void subscribeToAfterComplete() { + PublishSubject<Integer> ps = PublishSubject.create(); + + ps.onComplete(); + + PublishSubject<Integer> ps2 = PublishSubject.create(); + + ps2.subscribe(ps); + + assertFalse(ps2.hasObservers()); + } + + @Test + public void subscribedTo() { + PublishSubject<Integer> ps = PublishSubject.create(); + PublishSubject<Integer> ps2 = PublishSubject.create(); + + ps.subscribe(ps2); + + TestObserver<Integer> to = ps2.test(); + + ps.onNext(1); + ps.onNext(2); + ps.onComplete(); + + to.assertResult(1, 2); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/subjects/ReplaySubjectBoundedConcurrencyTest.java b/src/test/java/io/reactivex/rxjava3/subjects/ReplaySubjectBoundedConcurrencyTest.java new file mode 100644 index 0000000000..419e5c92a9 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/subjects/ReplaySubjectBoundedConcurrencyTest.java @@ -0,0 +1,563 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.subjects; + +import static org.junit.Assert.assertEquals; + +import java.util.*; +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicReference; + +import io.reactivex.rxjava3.disposables.Disposable; +import org.junit.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.core.Observer; +import io.reactivex.rxjava3.functions.Consumer; +import io.reactivex.rxjava3.observers.DefaultObserver; +import io.reactivex.rxjava3.schedulers.Schedulers; +import io.reactivex.rxjava3.testsupport.TestObserverEx; + +public class ReplaySubjectBoundedConcurrencyTest extends RxJavaTest { + + @Test + public void replaySubjectConcurrentSubscribersDoingReplayDontBlockEachOther() throws InterruptedException { + final ReplaySubject<Long> replay = ReplaySubject.createUnbounded(); + Thread source = new Thread(new Runnable() { + + @Override + public void run() { + Observable.unsafeCreate(new ObservableSource<Long>() { + + @Override + public void subscribe(Observer<? super Long> o) { + o.onSubscribe(Disposable.empty()); + System.out.println("********* Start Source Data ***********"); + for (long l = 1; l <= 10000; l++) { + o.onNext(l); + } + System.out.println("********* Finished Source Data ***********"); + o.onComplete(); + } + }).subscribe(replay); + } + }); + source.start(); + + long v = replay.blockingLast(); + assertEquals(10000, v); + + // it's been played through once so now it will all be replays + final CountDownLatch slowLatch = new CountDownLatch(1); + Thread slowThread = new Thread(new Runnable() { + + @Override + public void run() { + Observer<Long> slow = new DefaultObserver<Long>() { + + @Override + public void onComplete() { + System.out.println("*** Slow Observer completed"); + slowLatch.countDown(); + } + + @Override + public void onError(Throwable e) { + } + + @Override + public void onNext(Long args) { + if (args == 1) { + System.out.println("*** Slow Observer STARTED"); + } + try { + if (args % 10 == 0) { + Thread.sleep(1); + } + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + }; + replay.subscribe(slow); + try { + slowLatch.await(); + } catch (InterruptedException e1) { + e1.printStackTrace(); + } + } + }); + slowThread.start(); + + Thread fastThread = new Thread(new Runnable() { + + @Override + public void run() { + final CountDownLatch fastLatch = new CountDownLatch(1); + Observer<Long> fast = new DefaultObserver<Long>() { + + @Override + public void onComplete() { + System.out.println("*** Fast Observer completed"); + fastLatch.countDown(); + } + + @Override + public void onError(Throwable e) { + } + + @Override + public void onNext(Long args) { + if (args == 1) { + System.out.println("*** Fast Observer STARTED"); + } + } + }; + replay.subscribe(fast); + try { + fastLatch.await(); + } catch (InterruptedException e1) { + e1.printStackTrace(); + } + } + }); + fastThread.start(); + fastThread.join(); + + // slow should not yet be completed when fast completes + assertEquals(1, slowLatch.getCount()); + + slowThread.join(); + } + + @Test + public void replaySubjectConcurrentSubscriptions() throws InterruptedException { + final ReplaySubject<Long> replay = ReplaySubject.createUnbounded(); + Thread source = new Thread(new Runnable() { + + @Override + public void run() { + Observable.unsafeCreate(new ObservableSource<Long>() { + + @Override + public void subscribe(Observer<? super Long> o) { + o.onSubscribe(Disposable.empty()); + System.out.println("********* Start Source Data ***********"); + for (long l = 1; l <= 10000; l++) { + o.onNext(l); + } + System.out.println("********* Finished Source Data ***********"); + o.onComplete(); + } + }).subscribe(replay); + } + }); + + // used to collect results of each thread + final List<List<Long>> listOfListsOfValues = Collections.synchronizedList(new ArrayList<>()); + final List<Thread> threads = Collections.synchronizedList(new ArrayList<>()); + + for (int i = 1; i <= 200; i++) { + final int count = i; + if (count == 20) { + // start source data after we have some already subscribed + // and while others are in process of subscribing + source.start(); + } + if (count == 100) { + // wait for source to finish then keep adding after it's done + source.join(); + } + Thread t = new Thread(new Runnable() { + + @Override + public void run() { + List<Long> values = replay.toList().blockingGet(); + listOfListsOfValues.add(values); + System.out.println("Finished thread: " + count); + } + }); + t.start(); + System.out.println("Started thread: " + i); + threads.add(t); + } + + // wait for all threads to complete + for (Thread t : threads) { + t.join(); + } + + // assert all threads got the same results + List<Long> sums = new ArrayList<>(); + for (List<Long> values : listOfListsOfValues) { + long v = 0; + for (long l : values) { + v += l; + } + sums.add(v); + } + + long expected = sums.get(0); + boolean success = true; + for (long l : sums) { + if (l != expected) { + success = false; + System.out.println("FAILURE => Expected " + expected + " but got: " + l); + } + } + + if (success) { + System.out.println("Success! " + sums.size() + " each had the same sum of " + expected); + } else { + throw new RuntimeException("Concurrency Bug"); + } + + } + + /** + * Can receive timeout if subscribe never receives an onError/onComplete ... which reveals a race condition. + */ + @Test + public void subscribeCompletionRaceCondition() { + for (int i = 0; i < 50; i++) { + final ReplaySubject<String> subject = ReplaySubject.createUnbounded(); + final AtomicReference<String> value1 = new AtomicReference<>(); + + subject.subscribe(new Consumer<String>() { + + @Override + public void accept(String t1) { + try { + // simulate a slow observer + Thread.sleep(50); + } catch (InterruptedException e) { + e.printStackTrace(); + } + value1.set(t1); + } + + }); + + Thread t1 = new Thread(new Runnable() { + + @Override + public void run() { + subject.onNext("value"); + subject.onComplete(); + } + }); + + SubjectObserverThread t2 = new SubjectObserverThread(subject); + SubjectObserverThread t3 = new SubjectObserverThread(subject); + SubjectObserverThread t4 = new SubjectObserverThread(subject); + SubjectObserverThread t5 = new SubjectObserverThread(subject); + + t2.start(); + t3.start(); + t1.start(); + t4.start(); + t5.start(); + try { + t1.join(); + t2.join(); + t3.join(); + t4.join(); + t5.join(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + + assertEquals("value", value1.get()); + assertEquals("value", t2.value.get()); + assertEquals("value", t3.value.get()); + assertEquals("value", t4.value.get()); + assertEquals("value", t5.value.get()); + } + + } + + /** + * Make sure emission-subscription races are handled correctly. + * https://github.com/ReactiveX/RxJava/issues/1147 + */ + @Test + public void raceForTerminalState() { + final List<Integer> expected = Arrays.asList(1); + for (int i = 0; i < 100000; i++) { + TestObserverEx<Integer> to = new TestObserverEx<>(); + Observable.just(1).subscribeOn(Schedulers.computation()).cache().subscribe(to); + to.awaitDone(5, TimeUnit.SECONDS); + to.assertValueSequence(expected); + to.assertTerminated(); + } + } + + static class SubjectObserverThread extends Thread { + + private final ReplaySubject<String> subject; + private final AtomicReference<String> value = new AtomicReference<>(); + + SubjectObserverThread(ReplaySubject<String> subject) { + this.subject = subject; + } + + @Override + public void run() { + try { + // a timeout exception will happen if we don't get a terminal state + String v = subject.timeout(2000, TimeUnit.MILLISECONDS).blockingSingle(); + value.set(v); + } catch (Exception e) { + e.printStackTrace(); + } + } + } + + @Test + public void replaySubjectEmissionSubscriptionRace() throws Exception { + Scheduler s = Schedulers.io(); + Scheduler.Worker worker = Schedulers.io().createWorker(); + try { + for (int i = 0; i < 50000; i++) { + if (i % 1000 == 0) { + System.out.println(i); + } + final ReplaySubject<Object> rs = ReplaySubject.createWithSize(2); + + final CountDownLatch finish = new CountDownLatch(1); + final CountDownLatch start = new CountDownLatch(1); + +// int j = i; + + worker.schedule(new Runnable() { + @Override + public void run() { + try { + start.await(); + } catch (Exception e1) { + e1.printStackTrace(); + } +// System.out.println("> " + j); + rs.onNext(1); + } + }); + + final AtomicReference<Object> o = new AtomicReference<>(); + + rs +// .doOnSubscribe(v -> System.out.println("!! " + j)) +// .doOnNext(e -> System.out.println(">> " + j)) + .subscribeOn(s) + .observeOn(Schedulers.io()) +// .doOnNext(e -> System.out.println(">>> " + j)) + .subscribe(new DefaultObserver<Object>() { + + @Override + protected void onStart() { + super.onStart(); + } + + @Override + public void onComplete() { + o.set(-1); + finish.countDown(); + } + + @Override + public void onError(Throwable e) { + o.set(e); + finish.countDown(); + } + + @Override + public void onNext(Object t) { + o.set(t); + finish.countDown(); + } + + }); + start.countDown(); + + if (!finish.await(5, TimeUnit.SECONDS)) { + System.out.println(o.get()); + System.out.println(rs.hasObservers()); + rs.onComplete(); + Assert.fail("Timeout @ " + i); + break; + } else { + Assert.assertEquals(1, o.get()); + worker.schedule(new Runnable() { + @Override + public void run() { + rs.onComplete(); + } + }); + } + } + } finally { + worker.dispose(); + } + } + + @Test + public void concurrentSizeAndHasAnyValue() throws InterruptedException { + final ReplaySubject<Object> rs = ReplaySubject.createUnbounded(); + final CyclicBarrier cb = new CyclicBarrier(2); + + Thread t = new Thread(new Runnable() { + @Override + public void run() { + try { + cb.await(); + } catch (InterruptedException e) { + return; + } catch (BrokenBarrierException e) { + return; + } + for (int i = 0; i < 1000000; i++) { + rs.onNext(i); + } + rs.onComplete(); + System.out.println("Replay fill Thread finished!"); + } + }); + t.start(); + try { + cb.await(); + } catch (InterruptedException e) { + return; + } catch (BrokenBarrierException e) { + return; + } + int lastSize = 0; + for (; !rs.hasThrowable() && !rs.hasComplete();) { + int size = rs.size(); + boolean hasAny = rs.hasValue(); + Object[] values = rs.getValues(); + if (size < lastSize) { + Assert.fail("Size decreased! " + lastSize + " -> " + size); + } + if ((size > 0) && !hasAny) { + Assert.fail("hasAnyValue reports emptyness but size doesn't"); + } + if (size > values.length) { + Assert.fail("Got fewer values than size! " + size + " -> " + values.length); + } + for (int i = 0; i < values.length - 1; i++) { + Integer v1 = (Integer)values[i]; + Integer v2 = (Integer)values[i + 1]; + assertEquals(1, v2 - v1); + } + lastSize = size; + } + + t.join(); + } + + @Test + public void concurrentSizeAndHasAnyValueBounded() throws InterruptedException { + final ReplaySubject<Object> rs = ReplaySubject.createWithSize(3); + final CyclicBarrier cb = new CyclicBarrier(2); + + Thread t = new Thread(new Runnable() { + @Override + public void run() { + try { + cb.await(); + } catch (InterruptedException e) { + return; + } catch (BrokenBarrierException e) { + return; + } + for (int i = 0; i < 1000000; i++) { + rs.onNext(i); + } + rs.onComplete(); + System.out.println("Replay fill Thread finished!"); + } + }); + t.start(); + try { + cb.await(); + } catch (InterruptedException e) { + return; + } catch (BrokenBarrierException e) { + return; + } + for (; !rs.hasThrowable() && !rs.hasComplete();) { + rs.size(); // can't use value so just call to detect hangs + rs.hasValue(); // can't use value so just call to detect hangs + Object[] values = rs.getValues(); + for (int i = 0; i < values.length - 1; i++) { + Integer v1 = (Integer)values[i]; + Integer v2 = (Integer)values[i + 1]; + assertEquals(1, v2 - v1); + } + } + + t.join(); + } + + @Test + public void concurrentSizeAndHasAnyValueTimeBounded() throws InterruptedException { + final ReplaySubject<Object> rs = ReplaySubject.createWithTime(1, TimeUnit.MILLISECONDS, Schedulers.computation()); + final CyclicBarrier cb = new CyclicBarrier(2); + + Thread t = new Thread(new Runnable() { + @Override + public void run() { + try { + cb.await(); + } catch (InterruptedException e) { + return; + } catch (BrokenBarrierException e) { + return; + } + for (int i = 0; i < 1000000; i++) { + rs.onNext(i); + if (i % 10000 == 0) { + try { + Thread.sleep(1); + } catch (InterruptedException e) { + return; + } + } + } + rs.onComplete(); + System.out.println("Replay fill Thread finished!"); + } + }); + t.start(); + try { + cb.await(); + } catch (InterruptedException e) { + return; + } catch (BrokenBarrierException e) { + return; + } + for (; !rs.hasThrowable() && !rs.hasComplete();) { + rs.size(); // can't use value so just call to detect hangs + rs.hasValue(); // can't use value so just call to detect hangs + Object[] values = rs.getValues(); + for (int i = 0; i < values.length - 1; i++) { + Integer v1 = (Integer)values[i]; + Integer v2 = (Integer)values[i + 1]; + assertEquals(1, v2 - v1); + } + } + + t.join(); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/subjects/ReplaySubjectConcurrencyTest.java b/src/test/java/io/reactivex/rxjava3/subjects/ReplaySubjectConcurrencyTest.java new file mode 100644 index 0000000000..454b56e515 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/subjects/ReplaySubjectConcurrencyTest.java @@ -0,0 +1,451 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.subjects; + +import static org.junit.Assert.assertEquals; + +import java.util.*; +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicReference; + +import io.reactivex.rxjava3.disposables.Disposable; +import org.junit.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.core.Observer; +import io.reactivex.rxjava3.functions.Consumer; +import io.reactivex.rxjava3.observers.DefaultObserver; +import io.reactivex.rxjava3.schedulers.Schedulers; +import io.reactivex.rxjava3.testsupport.TestObserverEx; + +public class ReplaySubjectConcurrencyTest extends RxJavaTest { + + @Test + public void replaySubjectConcurrentSubscribersDoingReplayDontBlockEachOther() throws InterruptedException { + final ReplaySubject<Long> replay = ReplaySubject.create(); + Thread source = new Thread(new Runnable() { + + @Override + public void run() { + Observable.unsafeCreate(new ObservableSource<Long>() { + + @Override + public void subscribe(Observer<? super Long> o) { + o.onSubscribe(Disposable.empty()); + System.out.println("********* Start Source Data ***********"); + for (long l = 1; l <= 10000; l++) { + o.onNext(l); + } + System.out.println("********* Finished Source Data ***********"); + o.onComplete(); + } + }).subscribe(replay); + } + }); + source.start(); + + long v = replay.blockingLast(); + assertEquals(10000, v); + + // it's been played through once so now it will all be replays + final CountDownLatch slowLatch = new CountDownLatch(1); + Thread slowThread = new Thread(new Runnable() { + + @Override + public void run() { + Observer<Long> slow = new DefaultObserver<Long>() { + + @Override + public void onComplete() { + System.out.println("*** Slow Observer completed"); + slowLatch.countDown(); + } + + @Override + public void onError(Throwable e) { + } + + @Override + public void onNext(Long args) { + if (args == 1) { + System.out.println("*** Slow Observer STARTED"); + } + try { + if (args % 10 == 0) { + Thread.sleep(1); + } + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + }; + replay.subscribe(slow); + try { + slowLatch.await(); + } catch (InterruptedException e1) { + e1.printStackTrace(); + } + } + }); + slowThread.start(); + + Thread fastThread = new Thread(new Runnable() { + + @Override + public void run() { + final CountDownLatch fastLatch = new CountDownLatch(1); + Observer<Long> fast = new DefaultObserver<Long>() { + + @Override + public void onComplete() { + System.out.println("*** Fast Observer completed"); + fastLatch.countDown(); + } + + @Override + public void onError(Throwable e) { + } + + @Override + public void onNext(Long args) { + if (args == 1) { + System.out.println("*** Fast Observer STARTED"); + } + } + }; + replay.subscribe(fast); + try { + fastLatch.await(); + } catch (InterruptedException e1) { + e1.printStackTrace(); + } + } + }); + fastThread.start(); + fastThread.join(); + + // slow should not yet be completed when fast completes + assertEquals(1, slowLatch.getCount()); + + slowThread.join(); + } + + @Test + public void replaySubjectConcurrentSubscriptions() throws InterruptedException { + final ReplaySubject<Long> replay = ReplaySubject.create(); + Thread source = new Thread(new Runnable() { + + @Override + public void run() { + Observable.unsafeCreate(new ObservableSource<Long>() { + + @Override + public void subscribe(Observer<? super Long> o) { + o.onSubscribe(Disposable.empty()); + System.out.println("********* Start Source Data ***********"); + for (long l = 1; l <= 10000; l++) { + o.onNext(l); + } + System.out.println("********* Finished Source Data ***********"); + o.onComplete(); + } + }).subscribe(replay); + } + }); + + // used to collect results of each thread + final List<List<Long>> listOfListsOfValues = Collections.synchronizedList(new ArrayList<>()); + final List<Thread> threads = Collections.synchronizedList(new ArrayList<>()); + + for (int i = 1; i <= 200; i++) { + final int count = i; + if (count == 20) { + // start source data after we have some already subscribed + // and while others are in process of subscribing + source.start(); + } + if (count == 100) { + // wait for source to finish then keep adding after it's done + source.join(); + } + Thread t = new Thread(new Runnable() { + + @Override + public void run() { + List<Long> values = replay.toList().blockingGet(); + listOfListsOfValues.add(values); + System.out.println("Finished thread: " + count); + } + }); + t.start(); + System.out.println("Started thread: " + i); + threads.add(t); + } + + // wait for all threads to complete + for (Thread t : threads) { + t.join(); + } + + // assert all threads got the same results + List<Long> sums = new ArrayList<>(); + for (List<Long> values : listOfListsOfValues) { + long v = 0; + for (long l : values) { + v += l; + } + sums.add(v); + } + + long expected = sums.get(0); + boolean success = true; + for (long l : sums) { + if (l != expected) { + success = false; + System.out.println("FAILURE => Expected " + expected + " but got: " + l); + } + } + + if (success) { + System.out.println("Success! " + sums.size() + " each had the same sum of " + expected); + } else { + throw new RuntimeException("Concurrency Bug"); + } + + } + + /** + * Can receive timeout if subscribe never receives an onError/onComplete ... which reveals a race condition. + */ + @Test + public void subscribeCompletionRaceCondition() { + for (int i = 0; i < 50; i++) { + final ReplaySubject<String> subject = ReplaySubject.create(); + final AtomicReference<String> value1 = new AtomicReference<>(); + + subject.subscribe(new Consumer<String>() { + + @Override + public void accept(String t1) { + try { + // simulate a slow observer + Thread.sleep(50); + } catch (InterruptedException e) { + e.printStackTrace(); + } + value1.set(t1); + } + + }); + + Thread t1 = new Thread(new Runnable() { + + @Override + public void run() { + subject.onNext("value"); + subject.onComplete(); + } + }); + + SubjectObserverThread t2 = new SubjectObserverThread(subject); + SubjectObserverThread t3 = new SubjectObserverThread(subject); + SubjectObserverThread t4 = new SubjectObserverThread(subject); + SubjectObserverThread t5 = new SubjectObserverThread(subject); + + t2.start(); + t3.start(); + t1.start(); + t4.start(); + t5.start(); + try { + t1.join(); + t2.join(); + t3.join(); + t4.join(); + t5.join(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + + assertEquals("value", value1.get()); + assertEquals("value", t2.value.get()); + assertEquals("value", t3.value.get()); + assertEquals("value", t4.value.get()); + assertEquals("value", t5.value.get()); + } + + } + + /** + * Make sure emission-subscription races are handled correctly. + * https://github.com/ReactiveX/RxJava/issues/1147 + */ + @Test + public void raceForTerminalState() { + final List<Integer> expected = Arrays.asList(1); + for (int i = 0; i < 100000; i++) { + TestObserverEx<Integer> to = new TestObserverEx<>(); + Observable.just(1).subscribeOn(Schedulers.computation()).cache().subscribe(to); + to.awaitDone(5, TimeUnit.SECONDS); + to.assertValueSequence(expected); + to.assertTerminated(); + } + } + + static class SubjectObserverThread extends Thread { + + private final ReplaySubject<String> subject; + private final AtomicReference<String> value = new AtomicReference<>(); + + SubjectObserverThread(ReplaySubject<String> subject) { + this.subject = subject; + } + + @Override + public void run() { + try { + // a timeout exception will happen if we don't get a terminal state + String v = subject.timeout(2000, TimeUnit.MILLISECONDS).blockingSingle(); + value.set(v); + } catch (Exception e) { + e.printStackTrace(); + } + } + } + + @Test + public void replaySubjectEmissionSubscriptionRace() throws Exception { + Scheduler s = Schedulers.io(); + Scheduler.Worker worker = Schedulers.io().createWorker(); + try { + for (int i = 0; i < 50000; i++) { + if (i % 1000 == 0) { + System.out.println(i); + } + final ReplaySubject<Object> rs = ReplaySubject.create(); + + final CountDownLatch finish = new CountDownLatch(1); + final CountDownLatch start = new CountDownLatch(1); + + worker.schedule(new Runnable() { + @Override + public void run() { + try { + start.await(); + } catch (Exception e1) { + e1.printStackTrace(); + } + rs.onNext(1); + } + }); + + final AtomicReference<Object> o = new AtomicReference<>(); + + rs.subscribeOn(s).observeOn(Schedulers.io()) + .subscribe(new DefaultObserver<Object>() { + + @Override + public void onComplete() { + o.set(-1); + finish.countDown(); + } + + @Override + public void onError(Throwable e) { + o.set(e); + finish.countDown(); + } + + @Override + public void onNext(Object t) { + o.set(t); + finish.countDown(); + } + + }); + start.countDown(); + + if (!finish.await(5, TimeUnit.SECONDS)) { + System.out.println(o.get()); + System.out.println(rs.hasObservers()); + rs.onComplete(); + Assert.fail("Timeout @ " + i); + break; + } else { + Assert.assertEquals(1, o.get()); + worker.schedule(new Runnable() { + @Override + public void run() { + rs.onComplete(); + } + }); + + } + } + } finally { + worker.dispose(); + } + } + + @Test + public void concurrentSizeAndHasAnyValue() throws InterruptedException { + final ReplaySubject<Object> rs = ReplaySubject.create(); + final CyclicBarrier cb = new CyclicBarrier(2); + + Thread t = new Thread(new Runnable() { + @Override + public void run() { + try { + cb.await(); + } catch (InterruptedException e) { + return; + } catch (BrokenBarrierException e) { + return; + } + for (int i = 0; i < 1000000; i++) { + rs.onNext(i); + } + rs.onComplete(); + System.out.println("Replay fill Thread finished!"); + } + }); + t.start(); + try { + cb.await(); + } catch (InterruptedException e) { + return; + } catch (BrokenBarrierException e) { + return; + } + int lastSize = 0; + for (; !rs.hasThrowable() && !rs.hasComplete();) { + int size = rs.size(); + boolean hasAny = rs.hasValue(); + Object[] values = rs.getValues(); + if (size < lastSize) { + Assert.fail("Size decreased! " + lastSize + " -> " + size); + } + if ((size > 0) && !hasAny) { + Assert.fail("hasAnyValue reports emptyness but size doesn't"); + } + if (size > values.length) { + Assert.fail("Got fewer values than size! " + size + " -> " + values.length); + } + lastSize = size; + } + + t.join(); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/subjects/ReplaySubjectTest.java b/src/test/java/io/reactivex/rxjava3/subjects/ReplaySubjectTest.java new file mode 100644 index 0000000000..e752278b2f --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/subjects/ReplaySubjectTest.java @@ -0,0 +1,1381 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.subjects; + +import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +import java.lang.management.*; +import java.util.Arrays; +import java.util.concurrent.*; +import java.util.concurrent.atomic.*; + +import org.junit.*; +import org.mockito.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.observers.*; +import io.reactivex.rxjava3.schedulers.*; +import io.reactivex.rxjava3.subjects.ReplaySubject.*; +import io.reactivex.rxjava3.testsupport.*; + +public class ReplaySubjectTest extends SubjectTest<Integer> { + + private final Throwable testException = new Throwable(); + + @Override + protected Subject<Integer> create() { + return ReplaySubject.create(); + } + + @Test + @SuppressUndeliverable + public void completed() { + ReplaySubject<String> subject = ReplaySubject.create(); + + Observer<String> o1 = TestHelper.mockObserver(); + subject.subscribe(o1); + + subject.onNext("one"); + subject.onNext("two"); + subject.onNext("three"); + subject.onComplete(); + + subject.onNext("four"); + subject.onComplete(); + subject.onError(new Throwable()); + + assertCompletedSubscriber(o1); + + // assert that subscribing a 2nd time gets the same data + Observer<String> o2 = TestHelper.mockObserver(); + subject.subscribe(o2); + assertCompletedSubscriber(o2); + } + + @Test + @SuppressUndeliverable + public void completedStopsEmittingData() { + ReplaySubject<Integer> channel = ReplaySubject.create(); + Observer<Object> observerA = TestHelper.mockObserver(); + Observer<Object> observerB = TestHelper.mockObserver(); + Observer<Object> observerC = TestHelper.mockObserver(); + Observer<Object> observerD = TestHelper.mockObserver(); + TestObserver<Object> to = new TestObserver<>(observerA); + + channel.subscribe(to); + channel.subscribe(observerB); + + InOrder inOrderA = inOrder(observerA); + InOrder inOrderB = inOrder(observerB); + InOrder inOrderC = inOrder(observerC); + InOrder inOrderD = inOrder(observerD); + + channel.onNext(42); + + // both A and B should have received 42 from before subscription + inOrderA.verify(observerA).onNext(42); + inOrderB.verify(observerB).onNext(42); + + to.dispose(); + + // a should receive no more + inOrderA.verifyNoMoreInteractions(); + + channel.onNext(4711); + + // only be should receive 4711 at this point + inOrderB.verify(observerB).onNext(4711); + + channel.onComplete(); + + // B is subscribed so should receive onComplete + inOrderB.verify(observerB).onComplete(); + + channel.subscribe(observerC); + + // when C subscribes it should receive 42, 4711, onComplete + inOrderC.verify(observerC).onNext(42); + inOrderC.verify(observerC).onNext(4711); + inOrderC.verify(observerC).onComplete(); + + // if further events are propagated they should be ignored + channel.onNext(13); + channel.onNext(14); + channel.onNext(15); + channel.onError(new RuntimeException()); + + // a new subscription should only receive what was emitted prior to terminal state onComplete + channel.subscribe(observerD); + + inOrderD.verify(observerD).onNext(42); + inOrderD.verify(observerD).onNext(4711); + inOrderD.verify(observerD).onComplete(); + + verify(observerA).onSubscribe((Disposable)notNull()); + verify(observerB).onSubscribe((Disposable)notNull()); + verify(observerC).onSubscribe((Disposable)notNull()); + verify(observerD).onSubscribe((Disposable)notNull()); + Mockito.verifyNoMoreInteractions(observerA); + Mockito.verifyNoMoreInteractions(observerB); + Mockito.verifyNoMoreInteractions(observerC); + Mockito.verifyNoMoreInteractions(observerD); + + } + + @Test + @SuppressUndeliverable + public void completedAfterError() { + ReplaySubject<String> subject = ReplaySubject.create(); + + Observer<String> observer = TestHelper.mockObserver(); + + subject.onNext("one"); + subject.onError(testException); + subject.onNext("two"); + subject.onComplete(); + subject.onError(new RuntimeException()); + + subject.subscribe(observer); + verify(observer).onSubscribe((Disposable)notNull()); + verify(observer, times(1)).onNext("one"); + verify(observer, times(1)).onError(testException); + verifyNoMoreInteractions(observer); + } + + private void assertCompletedSubscriber(Observer<String> observer) { + InOrder inOrder = inOrder(observer); + + inOrder.verify(observer, times(1)).onNext("one"); + inOrder.verify(observer, times(1)).onNext("two"); + inOrder.verify(observer, times(1)).onNext("three"); + inOrder.verify(observer, Mockito.never()).onError(any(Throwable.class)); + inOrder.verify(observer, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + } + + @Test + @SuppressUndeliverable + public void error() { + ReplaySubject<String> subject = ReplaySubject.create(); + + Observer<String> observer = TestHelper.mockObserver(); + subject.subscribe(observer); + + subject.onNext("one"); + subject.onNext("two"); + subject.onNext("three"); + subject.onError(testException); + + subject.onNext("four"); + subject.onError(new Throwable()); + subject.onComplete(); + + assertErrorSubscriber(observer); + + observer = TestHelper.mockObserver(); + subject.subscribe(observer); + assertErrorSubscriber(observer); + } + + private void assertErrorSubscriber(Observer<String> observer) { + verify(observer, times(1)).onNext("one"); + verify(observer, times(1)).onNext("two"); + verify(observer, times(1)).onNext("three"); + verify(observer, times(1)).onError(testException); + verify(observer, Mockito.never()).onComplete(); + } + + @Test + public void subscribeMidSequence() { + ReplaySubject<String> subject = ReplaySubject.create(); + + Observer<String> observer = TestHelper.mockObserver(); + subject.subscribe(observer); + + subject.onNext("one"); + subject.onNext("two"); + + assertObservedUntilTwo(observer); + + Observer<String> anotherSubscriber = TestHelper.mockObserver(); + subject.subscribe(anotherSubscriber); + assertObservedUntilTwo(anotherSubscriber); + + subject.onNext("three"); + subject.onComplete(); + + assertCompletedSubscriber(observer); + assertCompletedSubscriber(anotherSubscriber); + } + + @Test + public void unsubscribeFirstSubscriber() { + ReplaySubject<String> subject = ReplaySubject.create(); + + Observer<String> observer = TestHelper.mockObserver(); + TestObserver<String> to = new TestObserver<>(observer); + subject.subscribe(to); + + subject.onNext("one"); + subject.onNext("two"); + + to.dispose(); + assertObservedUntilTwo(observer); + + Observer<String> anotherSubscriber = TestHelper.mockObserver(); + subject.subscribe(anotherSubscriber); + assertObservedUntilTwo(anotherSubscriber); + + subject.onNext("three"); + subject.onComplete(); + + assertObservedUntilTwo(observer); + assertCompletedSubscriber(anotherSubscriber); + } + + private void assertObservedUntilTwo(Observer<String> observer) { + verify(observer, times(1)).onNext("one"); + verify(observer, times(1)).onNext("two"); + verify(observer, Mockito.never()).onNext("three"); + verify(observer, Mockito.never()).onError(any(Throwable.class)); + verify(observer, Mockito.never()).onComplete(); + } + + @Test + public void newSubscriberDoesntBlockExisting() throws InterruptedException { + + final AtomicReference<String> lastValueForSubscriber1 = new AtomicReference<>(); + Observer<String> observer1 = new DefaultObserver<String>() { + + @Override + public void onComplete() { + + } + + @Override + public void onError(Throwable e) { + + } + + @Override + public void onNext(String v) { + System.out.println("observer1: " + v); + lastValueForSubscriber1.set(v); + } + + }; + + final AtomicReference<String> lastValueForSubscriber2 = new AtomicReference<>(); + final CountDownLatch oneReceived = new CountDownLatch(1); + final CountDownLatch makeSlow = new CountDownLatch(1); + final CountDownLatch completed = new CountDownLatch(1); + Observer<String> observer2 = new DefaultObserver<String>() { + + @Override + public void onComplete() { + completed.countDown(); + } + + @Override + public void onError(Throwable e) { + + } + + @Override + public void onNext(String v) { + System.out.println("observer2: " + v); + if (v.equals("one")) { + oneReceived.countDown(); + } else { + try { + makeSlow.await(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + lastValueForSubscriber2.set(v); + } + } + + }; + + ReplaySubject<String> subject = ReplaySubject.create(); + subject.subscribe(observer1); + subject.onNext("one"); + assertEquals("one", lastValueForSubscriber1.get()); + subject.onNext("two"); + assertEquals("two", lastValueForSubscriber1.get()); + + // use subscribeOn to make this async otherwise we deadlock as we are using CountDownLatches + subject.subscribeOn(Schedulers.newThread()).subscribe(observer2); + + System.out.println("before waiting for one"); + + // wait until observer2 starts having replay occur + oneReceived.await(); + + System.out.println("after waiting for one"); + + subject.onNext("three"); + + System.out.println("sent three"); + + // if subscription blocked existing subscribers then 'makeSlow' would cause this to not be there yet + assertEquals("three", lastValueForSubscriber1.get()); + + System.out.println("about to send onComplete"); + + subject.onComplete(); + + System.out.println("completed subject"); + + // release + makeSlow.countDown(); + + System.out.println("makeSlow released"); + + completed.await(); + // all of them should be emitted with the last being "three" + assertEquals("three", lastValueForSubscriber2.get()); + + } + + @Test + public void subscriptionLeak() { + ReplaySubject<Object> subject = ReplaySubject.create(); + + Disposable d = subject.subscribe(); + + assertEquals(1, subject.observerCount()); + + d.dispose(); + + assertEquals(0, subject.observerCount()); + } + + @Test + public void unsubscriptionCase() { + ReplaySubject<String> src = ReplaySubject.create(); + + for (int i = 0; i < 10; i++) { + final Observer<Object> o = TestHelper.mockObserver(); + InOrder inOrder = inOrder(o); + String v = "" + i; + src.onNext(v); + System.out.printf("Turn: %d%n", i); + src.firstElement() + .toObservable() + .flatMap(new Function<String, Observable<String>>() { + + @Override + public Observable<String> apply(String t1) { + return Observable.just(t1 + ", " + t1); + } + }) + .subscribe(new DefaultObserver<String>() { + @Override + public void onNext(String t) { + System.out.println(t); + o.onNext(t); + } + + @Override + public void onError(Throwable e) { + o.onError(e); + } + + @Override + public void onComplete() { + o.onComplete(); + } + }); + inOrder.verify(o).onNext("0, 0"); + inOrder.verify(o).onComplete(); + verify(o, never()).onError(any(Throwable.class)); + } + } + + @Test + public void terminateOnce() { + ReplaySubject<Integer> source = ReplaySubject.create(); + source.onNext(1); + source.onNext(2); + source.onComplete(); + + final Observer<Integer> o = TestHelper.mockObserver(); + + source.subscribe(new DefaultObserver<Integer>() { + + @Override + public void onNext(Integer t) { + o.onNext(t); + } + + @Override + public void onError(Throwable e) { + o.onError(e); + } + + @Override + public void onComplete() { + o.onComplete(); + } + }); + + verify(o).onNext(1); + verify(o).onNext(2); + verify(o).onComplete(); + verify(o, never()).onError(any(Throwable.class)); + } + + @Test + public void replay1AfterTermination() { + ReplaySubject<Integer> source = ReplaySubject.createWithSize(1); + + source.onNext(1); + source.onNext(2); + source.onComplete(); + + for (int i = 0; i < 1; i++) { + Observer<Integer> o = TestHelper.mockObserver(); + + source.subscribe(o); + + verify(o, never()).onNext(1); + verify(o).onNext(2); + verify(o).onComplete(); + verify(o, never()).onError(any(Throwable.class)); + } + } + + @Test + public void replay1Directly() { + ReplaySubject<Integer> source = ReplaySubject.createWithSize(1); + + Observer<Integer> o = TestHelper.mockObserver(); + + source.onNext(1); + source.onNext(2); + + source.subscribe(o); + + source.onNext(3); + source.onComplete(); + + verify(o, never()).onNext(1); + verify(o).onNext(2); + verify(o).onNext(3); + verify(o).onComplete(); + verify(o, never()).onError(any(Throwable.class)); + } + + @Test + public void replayTimestampedAfterTermination() { + TestScheduler scheduler = new TestScheduler(); + ReplaySubject<Integer> source = ReplaySubject.createWithTime(1, TimeUnit.SECONDS, scheduler); + + source.onNext(1); + + scheduler.advanceTimeBy(1, TimeUnit.SECONDS); + + source.onNext(2); + + scheduler.advanceTimeBy(1, TimeUnit.SECONDS); + + source.onNext(3); + source.onComplete(); + + scheduler.advanceTimeBy(1, TimeUnit.SECONDS); + + Observer<Integer> o = TestHelper.mockObserver(); + + source.subscribe(o); + + verify(o, never()).onNext(1); + verify(o, never()).onNext(2); + verify(o, never()).onNext(3); + verify(o).onComplete(); + verify(o, never()).onError(any(Throwable.class)); + } + + @Test + public void replayTimestampedDirectly() { + TestScheduler scheduler = new TestScheduler(); + ReplaySubject<Integer> source = ReplaySubject.createWithTime(1, TimeUnit.SECONDS, scheduler); + + source.onNext(1); + + scheduler.advanceTimeBy(1, TimeUnit.SECONDS); + + Observer<Integer> o = TestHelper.mockObserver(); + + source.subscribe(o); + + source.onNext(2); + + scheduler.advanceTimeBy(1, TimeUnit.SECONDS); + + source.onNext(3); + + scheduler.advanceTimeBy(1, TimeUnit.SECONDS); + + source.onComplete(); + + scheduler.advanceTimeBy(1, TimeUnit.SECONDS); + + verify(o, never()).onError(any(Throwable.class)); + verify(o, never()).onNext(1); + verify(o).onNext(2); + verify(o).onNext(3); + verify(o).onComplete(); + } + + @Test + public void currentStateMethodsNormal() { + ReplaySubject<Object> as = ReplaySubject.create(); + + assertFalse(as.hasThrowable()); + assertFalse(as.hasComplete()); + assertNull(as.getThrowable()); + + as.onNext(1); + + assertFalse(as.hasThrowable()); + assertFalse(as.hasComplete()); + assertNull(as.getThrowable()); + + as.onComplete(); + + assertFalse(as.hasThrowable()); + assertTrue(as.hasComplete()); + assertNull(as.getThrowable()); + } + + @Test + public void currentStateMethodsEmpty() { + ReplaySubject<Object> as = ReplaySubject.create(); + + assertFalse(as.hasThrowable()); + assertFalse(as.hasComplete()); + assertNull(as.getThrowable()); + + as.onComplete(); + + assertFalse(as.hasThrowable()); + assertTrue(as.hasComplete()); + assertNull(as.getThrowable()); + } + + @Test + public void currentStateMethodsError() { + ReplaySubject<Object> as = ReplaySubject.create(); + + assertFalse(as.hasThrowable()); + assertFalse(as.hasComplete()); + assertNull(as.getThrowable()); + + as.onError(new TestException()); + + assertTrue(as.hasThrowable()); + assertFalse(as.hasComplete()); + assertTrue(as.getThrowable() instanceof TestException); + } + + @Test + public void sizeAndHasAnyValueUnbounded() { + ReplaySubject<Object> rs = ReplaySubject.create(); + + assertEquals(0, rs.size()); + assertFalse(rs.hasValue()); + + rs.onNext(1); + + assertEquals(1, rs.size()); + assertTrue(rs.hasValue()); + + rs.onNext(1); + + assertEquals(2, rs.size()); + assertTrue(rs.hasValue()); + + rs.onComplete(); + + assertEquals(2, rs.size()); + assertTrue(rs.hasValue()); + } + + @Test + public void sizeAndHasAnyValueEffectivelyUnbounded() { + ReplaySubject<Object> rs = ReplaySubject.createUnbounded(); + + assertEquals(0, rs.size()); + assertFalse(rs.hasValue()); + + rs.onNext(1); + + assertEquals(1, rs.size()); + assertTrue(rs.hasValue()); + + rs.onNext(1); + + assertEquals(2, rs.size()); + assertTrue(rs.hasValue()); + + rs.onComplete(); + + assertEquals(2, rs.size()); + assertTrue(rs.hasValue()); + } + + @Test + public void sizeAndHasAnyValueUnboundedError() { + ReplaySubject<Object> rs = ReplaySubject.create(); + + assertEquals(0, rs.size()); + assertFalse(rs.hasValue()); + + rs.onNext(1); + + assertEquals(1, rs.size()); + assertTrue(rs.hasValue()); + + rs.onNext(1); + + assertEquals(2, rs.size()); + assertTrue(rs.hasValue()); + + rs.onError(new TestException()); + + assertEquals(2, rs.size()); + assertTrue(rs.hasValue()); + } + + @Test + public void sizeAndHasAnyValueEffectivelyUnboundedError() { + ReplaySubject<Object> rs = ReplaySubject.createUnbounded(); + + assertEquals(0, rs.size()); + assertFalse(rs.hasValue()); + + rs.onNext(1); + + assertEquals(1, rs.size()); + assertTrue(rs.hasValue()); + + rs.onNext(1); + + assertEquals(2, rs.size()); + assertTrue(rs.hasValue()); + + rs.onError(new TestException()); + + assertEquals(2, rs.size()); + assertTrue(rs.hasValue()); + } + + @Test + public void sizeAndHasAnyValueUnboundedEmptyError() { + ReplaySubject<Object> rs = ReplaySubject.create(); + + rs.onError(new TestException()); + + assertEquals(0, rs.size()); + assertFalse(rs.hasValue()); + } + + @Test + public void sizeAndHasAnyValueEffectivelyUnboundedEmptyError() { + ReplaySubject<Object> rs = ReplaySubject.createUnbounded(); + + rs.onError(new TestException()); + + assertEquals(0, rs.size()); + assertFalse(rs.hasValue()); + } + + @Test + public void sizeAndHasAnyValueUnboundedEmptyCompleted() { + ReplaySubject<Object> rs = ReplaySubject.create(); + + rs.onComplete(); + + assertEquals(0, rs.size()); + assertFalse(rs.hasValue()); + } + + @Test + public void sizeAndHasAnyValueEffectivelyUnboundedEmptyCompleted() { + ReplaySubject<Object> rs = ReplaySubject.createUnbounded(); + + rs.onComplete(); + + assertEquals(0, rs.size()); + assertFalse(rs.hasValue()); + } + + @Test + public void sizeAndHasAnyValueSizeBounded() { + ReplaySubject<Object> rs = ReplaySubject.createWithSize(1); + + assertEquals(0, rs.size()); + assertFalse(rs.hasValue()); + + for (int i = 0; i < 1000; i++) { + rs.onNext(i); + + assertEquals(1, rs.size()); + assertTrue(rs.hasValue()); + } + + rs.onComplete(); + + assertEquals(1, rs.size()); + assertTrue(rs.hasValue()); + } + + @Test + public void sizeAndHasAnyValueTimeBounded() { + TestScheduler to = new TestScheduler(); + ReplaySubject<Object> rs = ReplaySubject.createWithTime(1, TimeUnit.SECONDS, to); + + assertEquals(0, rs.size()); + assertFalse(rs.hasValue()); + + for (int i = 0; i < 1000; i++) { + rs.onNext(i); + assertEquals(1, rs.size()); + assertTrue(rs.hasValue()); + to.advanceTimeBy(2, TimeUnit.SECONDS); + assertEquals(0, rs.size()); + assertFalse(rs.hasValue()); + } + + rs.onComplete(); + + assertEquals(0, rs.size()); + assertFalse(rs.hasValue()); + } + + @Test + public void getValues() { + ReplaySubject<Object> rs = ReplaySubject.create(); + Object[] expected = new Object[10]; + for (int i = 0; i < expected.length; i++) { + expected[i] = i; + rs.onNext(i); + assertArrayEquals(Arrays.copyOf(expected, i + 1), rs.getValues()); + } + rs.onComplete(); + + assertArrayEquals(expected, rs.getValues()); + + } + + @Test + public void getValuesUnbounded() { + ReplaySubject<Object> rs = ReplaySubject.createUnbounded(); + Object[] expected = new Object[10]; + for (int i = 0; i < expected.length; i++) { + expected[i] = i; + rs.onNext(i); + assertArrayEquals(Arrays.copyOf(expected, i + 1), rs.getValues()); + } + rs.onComplete(); + + assertArrayEquals(expected, rs.getValues()); + + } + + @Test + public void createInvalidCapacity() { + try { + ReplaySubject.create(-99); + fail("Didn't throw IllegalArgumentException"); + } catch (IllegalArgumentException ex) { + assertEquals("capacityHint > 0 required but it was -99", ex.getMessage()); + } + } + + @Test + public void createWithSizeInvalidCapacity() { + try { + ReplaySubject.createWithSize(-99); + fail("Didn't throw IllegalArgumentException"); + } catch (IllegalArgumentException ex) { + assertEquals("maxSize > 0 required but it was -99", ex.getMessage()); + } + } + + @Test + public void createWithTimeAndSizeInvalidCapacity() { + try { + ReplaySubject.createWithTimeAndSize(1, TimeUnit.DAYS, Schedulers.computation(), -99); + fail("Didn't throw IllegalArgumentException"); + } catch (IllegalArgumentException ex) { + assertEquals("maxSize > 0 required but it was -99", ex.getMessage()); + } + } + + @Test + public void hasSubscribers() { + ReplaySubject<Integer> rp = ReplaySubject.create(); + + assertFalse(rp.hasObservers()); + + TestObserver<Integer> to = rp.test(); + + assertTrue(rp.hasObservers()); + + to.dispose(); + + assertFalse(rp.hasObservers()); + } + + @Test + public void peekStateUnbounded() { + ReplaySubject<Integer> rp = ReplaySubject.create(); + + rp.onNext(1); + + assertEquals((Integer)1, rp.getValue()); + + assertEquals(1, rp.getValues()[0]); + } + + @Test + public void peekStateTimeAndSize() { + ReplaySubject<Integer> rp = ReplaySubject.createWithTimeAndSize(1, TimeUnit.DAYS, Schedulers.computation(), 1); + + rp.onNext(1); + + assertEquals((Integer)1, rp.getValue()); + + assertEquals(1, rp.getValues()[0]); + + rp.onNext(2); + + assertEquals((Integer)2, rp.getValue()); + + assertEquals(2, rp.getValues()[0]); + + assertEquals((Integer)2, rp.getValues(new Integer[0])[0]); + + assertEquals((Integer)2, rp.getValues(new Integer[1])[0]); + + Integer[] a = new Integer[2]; + assertEquals((Integer)2, rp.getValues(a)[0]); + assertNull(a[1]); + } + + @Test + public void peekStateTimeAndSizeValue() { + ReplaySubject<Integer> rp = ReplaySubject.createWithTimeAndSize(1, TimeUnit.DAYS, Schedulers.computation(), 1); + + assertNull(rp.getValue()); + + assertEquals(0, rp.getValues().length); + + assertNull(rp.getValues(new Integer[2])[0]); + + rp.onComplete(); + + assertNull(rp.getValue()); + + assertEquals(0, rp.getValues().length); + + assertNull(rp.getValues(new Integer[2])[0]); + + rp = ReplaySubject.createWithTimeAndSize(1, TimeUnit.DAYS, Schedulers.computation(), 1); + rp.onError(new TestException()); + + assertNull(rp.getValue()); + + assertEquals(0, rp.getValues().length); + + assertNull(rp.getValues(new Integer[2])[0]); + } + + @Test + public void peekStateTimeAndSizeValueExpired() { + TestScheduler scheduler = new TestScheduler(); + ReplaySubject<Integer> rp = ReplaySubject.createWithTime(1, TimeUnit.DAYS, scheduler); + + assertNull(rp.getValue()); + assertNull(rp.getValues(new Integer[2])[0]); + + rp.onNext(2); + + assertEquals((Integer)2, rp.getValue()); + assertEquals(2, rp.getValues()[0]); + + scheduler.advanceTimeBy(2, TimeUnit.DAYS); + + assertNull(rp.getValue()); + assertEquals(0, rp.getValues().length); + assertNull(rp.getValues(new Integer[2])[0]); + } + + @Test + public void capacityHint() { + ReplaySubject<Integer> rp = ReplaySubject.create(8); + + for (int i = 0; i < 15; i++) { + rp.onNext(i); + } + rp.onComplete(); + + rp.test().assertResult(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14); + } + + @Test + public void subscribeCancelRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final TestObserver<Integer> to = new TestObserver<>(); + + final ReplaySubject<Integer> rp = ReplaySubject.create(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + rp.subscribe(to); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + to.dispose(); + } + }; + + TestHelper.race(r1, r2); + } + } + + @Test + public void subscribeAfterDone() { + ReplaySubject<Integer> rp = ReplaySubject.create(); + rp.onComplete(); + + Disposable bs = Disposable.empty(); + + rp.onSubscribe(bs); + + assertTrue(bs.isDisposed()); + } + + @Test + public void subscribeRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final ReplaySubject<Integer> rp = ReplaySubject.create(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + rp.test(); + } + }; + + TestHelper.race(r1, r1); + } + } + + @Test + public void cancelUpfront() { + ReplaySubject<Integer> rp = ReplaySubject.create(); + rp.test(); + rp.test(); + + TestObserver<Integer> to = rp.test(true); + + assertEquals(2, rp.observerCount()); + + to.assertEmpty(); + } + + @Test + public void cancelRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + + final ReplaySubject<Integer> rp = ReplaySubject.create(); + final TestObserver<Integer> to1 = rp.test(); + final TestObserver<Integer> to2 = rp.test(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + to1.dispose(); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + to2.dispose(); + } + }; + + TestHelper.race(r1, r2); + + assertFalse(rp.hasObservers()); + } + } + + @Test + public void sizeboundReplayError() { + ReplaySubject<Integer> rp = ReplaySubject.createWithSize(2); + + rp.onNext(1); + rp.onNext(2); + rp.onNext(3); + rp.onNext(4); + rp.onError(new TestException()); + + rp.test() + .assertFailure(TestException.class, 3, 4); + } + + @Test + public void sizeAndTimeBoundReplayError() { + ReplaySubject<Integer> rp = ReplaySubject.createWithTimeAndSize(1, TimeUnit.DAYS, Schedulers.single(), 2); + + rp.onNext(1); + rp.onNext(2); + rp.onNext(3); + rp.onNext(4); + rp.onError(new TestException()); + + rp.test() + .assertFailure(TestException.class, 3, 4); + } + + @Test + public void timedSkipOld() { + TestScheduler scheduler = new TestScheduler(); + + ReplaySubject<Integer> rp = ReplaySubject.createWithTimeAndSize(1, TimeUnit.SECONDS, scheduler, 2); + + rp.onNext(1); + scheduler.advanceTimeBy(1, TimeUnit.SECONDS); + + rp.test() + .assertEmpty(); + } + + @Test + public void takeSizeAndTime() { + TestScheduler scheduler = new TestScheduler(); + + ReplaySubject<Integer> rp = ReplaySubject.createWithTimeAndSize(1, TimeUnit.SECONDS, scheduler, 2); + + rp.onNext(1); + rp.onNext(2); + rp.onNext(3); + + rp + .take(1) + .test() + .assertResult(2); + } + + @Test + public void takeSize() { + ReplaySubject<Integer> rp = ReplaySubject.createWithSize(2); + + rp.onNext(1); + rp.onNext(2); + rp.onNext(3); + + rp + .take(1) + .test() + .assertResult(2); + } + + @Test + public void reentrantDrain() { + TestScheduler scheduler = new TestScheduler(); + + final ReplaySubject<Integer> rp = ReplaySubject.createWithTimeAndSize(1, TimeUnit.SECONDS, scheduler, 2); + + TestObserver<Integer> to = new TestObserver<Integer>() { + @Override + public void onNext(Integer t) { + if (t == 1) { + rp.onNext(2); + } + super.onNext(t); + } + }; + + rp.subscribe(to); + + rp.onNext(1); + rp.onComplete(); + + to.assertResult(1, 2); + } + + @Test + public void dispose() { + TestHelper.checkDisposed(ReplaySubject.create()); + + TestHelper.checkDisposed(ReplaySubject.createUnbounded()); + + TestHelper.checkDisposed(ReplaySubject.createWithSize(10)); + + TestHelper.checkDisposed(ReplaySubject.createWithTimeAndSize(1, TimeUnit.SECONDS, Schedulers.single(), 10)); + } + + @Test + public void timedNoOutdatedData() { + TestScheduler scheduler = new TestScheduler(); + + ReplaySubject<Integer> source = ReplaySubject.createWithTime(2, TimeUnit.SECONDS, scheduler); + source.onNext(1); + source.onComplete(); + + source.test().assertResult(1); + + source.test().assertResult(1); + + scheduler.advanceTimeBy(3, TimeUnit.SECONDS); + + source.test().assertResult(); + } + + @Test + public void noHeadRetentionCompleteSize() { + ReplaySubject<Integer> source = ReplaySubject.createWithSize(1); + + source.onNext(1); + source.onNext(2); + source.onComplete(); + + SizeBoundReplayBuffer<Integer> buf = (SizeBoundReplayBuffer<Integer>)source.buffer; + + assertNull(buf.head.value); + + Object o = buf.head; + + source.cleanupBuffer(); + + assertSame(o, buf.head); + } + + @Test + public void noHeadRetentionSize() { + ReplaySubject<Integer> source = ReplaySubject.createWithSize(1); + + source.onNext(1); + source.onNext(2); + + SizeBoundReplayBuffer<Integer> buf = (SizeBoundReplayBuffer<Integer>)source.buffer; + + assertNotNull(buf.head.value); + + source.cleanupBuffer(); + + assertNull(buf.head.value); + + Object o = buf.head; + + source.cleanupBuffer(); + + assertSame(o, buf.head); + } + + @Test + public void noHeadRetentionCompleteTime() { + ReplaySubject<Integer> source = ReplaySubject.createWithTime(1, TimeUnit.MINUTES, Schedulers.computation()); + + source.onNext(1); + source.onNext(2); + source.onComplete(); + + SizeAndTimeBoundReplayBuffer<Integer> buf = (SizeAndTimeBoundReplayBuffer<Integer>)source.buffer; + + assertNull(buf.head.value); + + Object o = buf.head; + + source.cleanupBuffer(); + + assertSame(o, buf.head); + } + + @Test + public void noHeadRetentionTime() { + TestScheduler sch = new TestScheduler(); + + ReplaySubject<Integer> source = ReplaySubject.createWithTime(1, TimeUnit.MILLISECONDS, sch); + + source.onNext(1); + + sch.advanceTimeBy(2, TimeUnit.MILLISECONDS); + + source.onNext(2); + + SizeAndTimeBoundReplayBuffer<Integer> buf = (SizeAndTimeBoundReplayBuffer<Integer>)source.buffer; + + assertNotNull(buf.head.value); + + source.cleanupBuffer(); + + assertNull(buf.head.value); + + Object o = buf.head; + + source.cleanupBuffer(); + + assertSame(o, buf.head); + } + + @Test + public void noBoundedRetentionViaThreadLocal() throws Exception { + final ReplaySubject<byte[]> rs = ReplaySubject.createWithSize(1); + + Observable<byte[]> source = rs.take(1) + .concatMap(new Function<byte[], Observable<byte[]>>() { + @Override + public Observable<byte[]> apply(byte[] v) throws Exception { + return rs; + } + }) + .takeLast(1) + ; + + System.out.println("Bounded Replay Leak check: Wait before GC"); + Thread.sleep(1000); + + System.out.println("Bounded Replay Leak check: GC"); + System.gc(); + + Thread.sleep(500); + + final MemoryMXBean memoryMXBean = ManagementFactory.getMemoryMXBean(); + MemoryUsage memHeap = memoryMXBean.getHeapMemoryUsage(); + long initial = memHeap.getUsed(); + + System.out.printf("Bounded Replay Leak check: Starting: %.3f MB%n", initial / 1024.0 / 1024.0); + + final AtomicLong after = new AtomicLong(); + + source.subscribe(new Consumer<byte[]>() { + @Override + public void accept(byte[] v) throws Exception { + System.out.println("Bounded Replay Leak check: Wait before GC 2"); + Thread.sleep(1000); + + System.out.println("Bounded Replay Leak check: GC 2"); + System.gc(); + + Thread.sleep(500); + + after.set(memoryMXBean.getHeapMemoryUsage().getUsed()); + } + }); + + for (int i = 0; i < 200; i++) { + rs.onNext(new byte[1024 * 1024]); + } + rs.onComplete(); + + System.out.printf("Bounded Replay Leak check: After: %.3f MB%n", after.get() / 1024.0 / 1024.0); + + if (initial + 100 * 1024 * 1024 < after.get()) { + Assert.fail("Bounded Replay Leak check: Memory leak detected: " + (initial / 1024.0 / 1024.0) + + " -> " + after.get() / 1024.0 / 1024.0); + } + } + + @Test + public void timeAndSizeNoTerminalTruncationOnTimechange() { + ReplaySubject<Integer> rs = ReplaySubject.createWithTimeAndSize(1, TimeUnit.SECONDS, new TimesteppingScheduler(), 1); + + TestObserver<Integer> to = rs.test(); + + rs.onNext(1); + rs.cleanupBuffer(); + rs.onComplete(); + + to.assertNoErrors() + .assertComplete(); + } + + @Test + public void timeAndSizeNoTerminalTruncationOnTimechange2() { + ReplaySubject<Integer> rs = ReplaySubject.createWithTimeAndSize(1, TimeUnit.SECONDS, new TimesteppingScheduler(), 1); + + TestObserver<Integer> to = rs.test(); + + rs.onNext(1); + rs.cleanupBuffer(); + rs.onNext(2); + rs.cleanupBuffer(); + rs.onComplete(); + + to.assertNoErrors() + .assertComplete(); + } + + @Test + public void timeAndSizeNoTerminalTruncationOnTimechange3() { + ReplaySubject<Integer> rs = ReplaySubject.createWithTimeAndSize(1, TimeUnit.SECONDS, new TimesteppingScheduler(), 1); + + TestObserver<Integer> to = rs.test(); + + rs.onNext(1); + rs.onNext(2); + rs.onComplete(); + + to.assertNoErrors() + .assertComplete(); + } + + @Test + public void timeAndSizeNoTerminalTruncationOnTimechange4() { + ReplaySubject<Integer> rs = ReplaySubject.createWithTimeAndSize(1, TimeUnit.SECONDS, new TimesteppingScheduler(), 10); + + TestObserver<Integer> to = rs.test(); + + rs.onNext(1); + rs.onNext(2); + rs.onComplete(); + + to.assertNoErrors() + .assertComplete(); + } + + @Test + public void timeAndSizeRemoveCorrectNumberOfOld() { + TestScheduler scheduler = new TestScheduler(); + ReplaySubject<Integer> rs = ReplaySubject.createWithTimeAndSize(1, TimeUnit.SECONDS, scheduler, 2); + + rs.onNext(1); + rs.onNext(2); + rs.onNext(3); // remove 1 due to maxSize, size == 2 + + scheduler.advanceTimeBy(2, TimeUnit.SECONDS); + + rs.onNext(4); // remove 2 due to maxSize, remove 3 due to age, size == 1 + rs.onNext(5); // size == 2 + + rs.test().assertValuesOnly(4, 5); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/subjects/SerializedSubjectTest.java b/src/test/java/io/reactivex/rxjava3/subjects/SerializedSubjectTest.java new file mode 100644 index 0000000000..18ff503339 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/subjects/SerializedSubjectTest.java @@ -0,0 +1,716 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.subjects; + +import static org.junit.Assert.*; + +import java.util.*; +import java.util.concurrent.TimeUnit; + +import org.junit.Test; + +import io.reactivex.rxjava3.annotations.NonNull; +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.core.RxJavaTest; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.testsupport.*; + +public class SerializedSubjectTest extends RxJavaTest { + + @Test + public void basic() { + SerializedSubject<String> subject = new SerializedSubject<>(PublishSubject.<String>create()); + TestObserver<String> to = new TestObserver<>(); + subject.subscribe(to); + subject.onNext("hello"); + subject.onComplete(); + to.awaitDone(5, TimeUnit.SECONDS); + to.assertValue("hello"); + } + + @Test + public void asyncSubjectValueRelay() { + AsyncSubject<Integer> async = AsyncSubject.create(); + async.onNext(1); + async.onComplete(); + Subject<Integer> serial = async.toSerialized(); + + assertFalse(serial.hasObservers()); + assertTrue(serial.hasComplete()); + assertFalse(serial.hasThrowable()); + assertNull(serial.getThrowable()); + assertEquals((Integer)1, async.getValue()); + assertTrue(async.hasValue()); + } + + @Test + public void asyncSubjectValueEmpty() { + AsyncSubject<Integer> async = AsyncSubject.create(); + async.onComplete(); + Subject<Integer> serial = async.toSerialized(); + + assertFalse(serial.hasObservers()); + assertTrue(serial.hasComplete()); + assertFalse(serial.hasThrowable()); + assertNull(serial.getThrowable()); + assertNull(async.getValue()); + assertFalse(async.hasValue()); + } + + @Test + public void asyncSubjectValueError() { + AsyncSubject<Integer> async = AsyncSubject.create(); + TestException te = new TestException(); + async.onError(te); + Subject<Integer> serial = async.toSerialized(); + + assertFalse(serial.hasObservers()); + assertFalse(serial.hasComplete()); + assertTrue(serial.hasThrowable()); + assertSame(te, serial.getThrowable()); + assertNull(async.getValue()); + assertFalse(async.hasValue()); + } + + @Test + public void publishSubjectValueRelay() { + PublishSubject<Integer> async = PublishSubject.create(); + async.onNext(1); + async.onComplete(); + Subject<Integer> serial = async.toSerialized(); + + assertFalse(serial.hasObservers()); + assertTrue(serial.hasComplete()); + assertFalse(serial.hasThrowable()); + assertNull(serial.getThrowable()); + } + + @Test + public void publishSubjectValueEmpty() { + PublishSubject<Integer> async = PublishSubject.create(); + async.onComplete(); + Subject<Integer> serial = async.toSerialized(); + + assertFalse(serial.hasObservers()); + assertTrue(serial.hasComplete()); + assertFalse(serial.hasThrowable()); + assertNull(serial.getThrowable()); + } + + @Test + public void publishSubjectValueError() { + PublishSubject<Integer> async = PublishSubject.create(); + TestException te = new TestException(); + async.onError(te); + Subject<Integer> serial = async.toSerialized(); + + assertFalse(serial.hasObservers()); + assertFalse(serial.hasComplete()); + assertTrue(serial.hasThrowable()); + assertSame(te, serial.getThrowable()); + } + + @Test + public void behaviorSubjectValueRelay() { + BehaviorSubject<Integer> async = BehaviorSubject.create(); + async.onNext(1); + async.onComplete(); + Subject<Integer> serial = async.toSerialized(); + + assertFalse(serial.hasObservers()); + assertTrue(serial.hasComplete()); + assertFalse(serial.hasThrowable()); + assertNull(serial.getThrowable()); + assertNull(async.getValue()); + assertFalse(async.hasValue()); + } + + @Test + public void behaviorSubjectValueRelayIncomplete() { + BehaviorSubject<Integer> async = BehaviorSubject.create(); + async.onNext(1); + Subject<Integer> serial = async.toSerialized(); + + assertFalse(serial.hasObservers()); + assertFalse(serial.hasComplete()); + assertFalse(serial.hasThrowable()); + assertNull(serial.getThrowable()); + assertEquals((Integer)1, async.getValue()); + assertTrue(async.hasValue()); + } + + @Test + public void behaviorSubjectIncompleteEmpty() { + BehaviorSubject<Integer> async = BehaviorSubject.create(); + Subject<Integer> serial = async.toSerialized(); + + assertFalse(serial.hasObservers()); + assertFalse(serial.hasComplete()); + assertFalse(serial.hasThrowable()); + assertNull(serial.getThrowable()); + assertNull(async.getValue()); + assertFalse(async.hasValue()); + } + + @Test + public void behaviorSubjectEmpty() { + BehaviorSubject<Integer> async = BehaviorSubject.create(); + async.onComplete(); + Subject<Integer> serial = async.toSerialized(); + + assertFalse(serial.hasObservers()); + assertTrue(serial.hasComplete()); + assertFalse(serial.hasThrowable()); + assertNull(serial.getThrowable()); + assertNull(async.getValue()); + assertFalse(async.hasValue()); + } + + @Test + public void behaviorSubjectError() { + BehaviorSubject<Integer> async = BehaviorSubject.create(); + TestException te = new TestException(); + async.onError(te); + Subject<Integer> serial = async.toSerialized(); + + assertFalse(serial.hasObservers()); + assertFalse(serial.hasComplete()); + assertTrue(serial.hasThrowable()); + assertSame(te, serial.getThrowable()); + assertNull(async.getValue()); + assertFalse(async.hasValue()); + } + + @Test + public void replaySubjectValueRelay() { + ReplaySubject<Integer> async = ReplaySubject.create(); + async.onNext(1); + async.onComplete(); + Subject<Integer> serial = async.toSerialized(); + + assertFalse(serial.hasObservers()); + assertTrue(serial.hasComplete()); + assertFalse(serial.hasThrowable()); + assertNull(serial.getThrowable()); + assertEquals((Integer)1, async.getValue()); + assertTrue(async.hasValue()); + assertArrayEquals(new Object[] { 1 }, async.getValues()); + assertArrayEquals(new Integer[] { 1 }, async.getValues(new Integer[0])); + assertArrayEquals(new Integer[] { 1 }, async.getValues(new Integer[] { 0 })); + assertArrayEquals(new Integer[] { 1, null }, async.getValues(new Integer[] { 0, 0 })); + } + + @Test + public void replaySubjectValueRelayIncomplete() { + ReplaySubject<Integer> async = ReplaySubject.create(); + async.onNext(1); + Subject<Integer> serial = async.toSerialized(); + + assertFalse(serial.hasObservers()); + assertFalse(serial.hasComplete()); + assertFalse(serial.hasThrowable()); + assertNull(serial.getThrowable()); + assertEquals((Integer)1, async.getValue()); + assertTrue(async.hasValue()); + assertArrayEquals(new Object[] { 1 }, async.getValues()); + assertArrayEquals(new Integer[] { 1 }, async.getValues(new Integer[0])); + assertArrayEquals(new Integer[] { 1 }, async.getValues(new Integer[] { 0 })); + assertArrayEquals(new Integer[] { 1, null }, async.getValues(new Integer[] { 0, 0 })); + } + + @Test + public void replaySubjectValueRelayBounded() { + ReplaySubject<Integer> async = ReplaySubject.createWithSize(1); + async.onNext(0); + async.onNext(1); + async.onComplete(); + Subject<Integer> serial = async.toSerialized(); + + assertFalse(serial.hasObservers()); + assertTrue(serial.hasComplete()); + assertFalse(serial.hasThrowable()); + assertNull(serial.getThrowable()); + assertEquals((Integer)1, async.getValue()); + assertTrue(async.hasValue()); + assertArrayEquals(new Object[] { 1 }, async.getValues()); + assertArrayEquals(new Integer[] { 1 }, async.getValues(new Integer[0])); + assertArrayEquals(new Integer[] { 1 }, async.getValues(new Integer[] { 0 })); + assertArrayEquals(new Integer[] { 1, null }, async.getValues(new Integer[] { 0, 0 })); + } + + @Test + public void replaySubjectValueRelayBoundedIncomplete() { + ReplaySubject<Integer> async = ReplaySubject.createWithSize(1); + async.onNext(0); + async.onNext(1); + Subject<Integer> serial = async.toSerialized(); + + assertFalse(serial.hasObservers()); + assertFalse(serial.hasComplete()); + assertFalse(serial.hasThrowable()); + assertNull(serial.getThrowable()); + assertEquals((Integer)1, async.getValue()); + assertTrue(async.hasValue()); + assertArrayEquals(new Object[] { 1 }, async.getValues()); + assertArrayEquals(new Integer[] { 1 }, async.getValues(new Integer[0])); + assertArrayEquals(new Integer[] { 1 }, async.getValues(new Integer[] { 0 })); + assertArrayEquals(new Integer[] { 1, null }, async.getValues(new Integer[] { 0, 0 })); + } + + @Test + public void replaySubjectValueRelayBoundedEmptyIncomplete() { + ReplaySubject<Integer> async = ReplaySubject.createWithSize(1); + Subject<Integer> serial = async.toSerialized(); + + assertFalse(serial.hasObservers()); + assertFalse(serial.hasComplete()); + assertFalse(serial.hasThrowable()); + assertNull(serial.getThrowable()); + assertNull(async.getValue()); + assertFalse(async.hasValue()); + assertArrayEquals(new Object[] { }, async.getValues()); + assertArrayEquals(new Integer[] { }, async.getValues(new Integer[0])); + assertArrayEquals(new Integer[] { null }, async.getValues(new Integer[] { 0 })); + assertArrayEquals(new Integer[] { null, 0 }, async.getValues(new Integer[] { 0, 0 })); + } + + @Test + public void replaySubjectValueRelayEmptyIncomplete() { + ReplaySubject<Integer> async = ReplaySubject.create(); + Subject<Integer> serial = async.toSerialized(); + + assertFalse(serial.hasObservers()); + assertFalse(serial.hasComplete()); + assertFalse(serial.hasThrowable()); + assertNull(serial.getThrowable()); + assertNull(async.getValue()); + assertFalse(async.hasValue()); + assertArrayEquals(new Object[] { }, async.getValues()); + assertArrayEquals(new Integer[] { }, async.getValues(new Integer[0])); + assertArrayEquals(new Integer[] { null }, async.getValues(new Integer[] { 0 })); + assertArrayEquals(new Integer[] { null, 0 }, async.getValues(new Integer[] { 0, 0 })); + } + + @Test + public void replaySubjectEmpty() { + ReplaySubject<Integer> async = ReplaySubject.create(); + async.onComplete(); + Subject<Integer> serial = async.toSerialized(); + + assertFalse(serial.hasObservers()); + assertTrue(serial.hasComplete()); + assertFalse(serial.hasThrowable()); + assertNull(serial.getThrowable()); + assertNull(async.getValue()); + assertFalse(async.hasValue()); + assertArrayEquals(new Object[] { }, async.getValues()); + assertArrayEquals(new Integer[] { }, async.getValues(new Integer[0])); + assertArrayEquals(new Integer[] { null }, async.getValues(new Integer[] { 0 })); + assertArrayEquals(new Integer[] { null, 0 }, async.getValues(new Integer[] { 0, 0 })); + } + + @Test + public void replaySubjectError() { + ReplaySubject<Integer> async = ReplaySubject.create(); + TestException te = new TestException(); + async.onError(te); + Subject<Integer> serial = async.toSerialized(); + + assertFalse(serial.hasObservers()); + assertFalse(serial.hasComplete()); + assertTrue(serial.hasThrowable()); + assertSame(te, serial.getThrowable()); + assertNull(async.getValue()); + assertFalse(async.hasValue()); + assertArrayEquals(new Object[] { }, async.getValues()); + assertArrayEquals(new Integer[] { }, async.getValues(new Integer[0])); + assertArrayEquals(new Integer[] { null }, async.getValues(new Integer[] { 0 })); + assertArrayEquals(new Integer[] { null, 0 }, async.getValues(new Integer[] { 0, 0 })); + } + + @Test + public void replaySubjectBoundedEmpty() { + ReplaySubject<Integer> async = ReplaySubject.createWithSize(1); + async.onComplete(); + Subject<Integer> serial = async.toSerialized(); + + assertFalse(serial.hasObservers()); + assertTrue(serial.hasComplete()); + assertFalse(serial.hasThrowable()); + assertNull(serial.getThrowable()); + assertNull(async.getValue()); + assertFalse(async.hasValue()); + assertArrayEquals(new Object[] { }, async.getValues()); + assertArrayEquals(new Integer[] { }, async.getValues(new Integer[0])); + assertArrayEquals(new Integer[] { null }, async.getValues(new Integer[] { 0 })); + assertArrayEquals(new Integer[] { null, 0 }, async.getValues(new Integer[] { 0, 0 })); + } + + @Test + public void replaySubjectBoundedError() { + ReplaySubject<Integer> async = ReplaySubject.createWithSize(1); + TestException te = new TestException(); + async.onError(te); + Subject<Integer> serial = async.toSerialized(); + + assertFalse(serial.hasObservers()); + assertFalse(serial.hasComplete()); + assertTrue(serial.hasThrowable()); + assertSame(te, serial.getThrowable()); + assertNull(async.getValue()); + assertFalse(async.hasValue()); + assertArrayEquals(new Object[] { }, async.getValues()); + assertArrayEquals(new Integer[] { }, async.getValues(new Integer[0])); + assertArrayEquals(new Integer[] { null }, async.getValues(new Integer[] { 0 })); + assertArrayEquals(new Integer[] { null, 0 }, async.getValues(new Integer[] { 0, 0 })); + } + + @Test + public void dontWrapSerializedSubjectAgain() { + PublishSubject<Object> s = PublishSubject.create(); + Subject<Object> s1 = s.toSerialized(); + Subject<Object> s2 = s1.toSerialized(); + assertSame(s1, s2); + } + + @Test + public void normal() { + Subject<Integer> s = PublishSubject.<Integer>create().toSerialized(); + + TestObserver<Integer> to = s.test(); + + Observable.range(1, 10).subscribe(s); + + to.assertResult(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + + assertFalse(s.hasObservers()); + + s.onNext(11); + + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + s.onError(new TestException()); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + s.onComplete(); + + Disposable bs = Disposable.empty(); + s.onSubscribe(bs); + assertTrue(bs.isDisposed()); + } + + @Test + public void onNextOnNextRace() { + Set<Integer> expectedSet = new HashSet<>(Arrays.asList(1, 2)); + + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final Subject<Integer> s = PublishSubject.<Integer>create().toSerialized(); + + TestObserverEx<Integer> to = s.to(TestHelper.<Integer>testConsumer()); + + Runnable r1 = new Runnable() { + @Override + public void run() { + s.onNext(1); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + s.onNext(2); + } + }; + + TestHelper.race(r1, r2); + + to.assertSubscribed() + .assertNoErrors() + .assertNotComplete() + .assertValueCount(2) + ; + + Set<Integer> actualSet = new HashSet<>(to.values()); + assertEquals("" + actualSet, expectedSet, actualSet); + } + } + + @Test + public void onNextOnErrorRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final Subject<Integer> s = PublishSubject.<Integer>create().toSerialized(); + + TestObserver<Integer> to = s.test(); + + final TestException ex = new TestException(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + s.onNext(1); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + s.onError(ex); + } + }; + + TestHelper.race(r1, r2); + + to.assertError(ex).assertNotComplete(); + + if (to.values().size() != 0) { + to.assertValue(1); + } + } + } + + @Test + public void onNextOnCompleteRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final Subject<Integer> s = PublishSubject.<Integer>create().toSerialized(); + + TestObserver<Integer> to = s.test(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + s.onNext(1); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + s.onComplete(); + } + }; + + TestHelper.race(r1, r2); + + to.assertComplete().assertNoErrors(); + + if (to.values().size() != 0) { + to.assertValue(1); + } + } + } + + @Test + public void onNextOnSubscribeRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final Subject<Integer> s = PublishSubject.<Integer>create().toSerialized(); + + TestObserver<Integer> to = s.test(); + + final Disposable bs = Disposable.empty(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + s.onNext(1); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + s.onSubscribe(bs); + } + }; + + TestHelper.race(r1, r2); + + to.assertValue(1).assertNotComplete().assertNoErrors(); + } + } + + @Test + public void onCompleteOnSubscribeRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final Subject<Integer> s = PublishSubject.<Integer>create().toSerialized(); + + TestObserver<Integer> to = s.test(); + + final Disposable bs = Disposable.empty(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + s.onComplete(); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + s.onSubscribe(bs); + } + }; + + TestHelper.race(r1, r2); + + to.assertResult(); + } + } + + @Test + public void onCompleteOnCompleteRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final Subject<Integer> s = PublishSubject.<Integer>create().toSerialized(); + + TestObserver<Integer> to = s.test(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + s.onComplete(); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + s.onComplete(); + } + }; + + TestHelper.race(r1, r2); + + to.assertResult(); + } + } + + @Test + public void onErrorOnErrorRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final Subject<Integer> s = PublishSubject.<Integer>create().toSerialized(); + + TestObserver<Integer> to = s.test(); + + final TestException ex = new TestException(); + + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + Runnable r1 = new Runnable() { + @Override + public void run() { + s.onError(ex); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + s.onError(ex); + } + }; + + TestHelper.race(r1, r2); + + to.assertFailure(TestException.class); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + } + + @Test + public void onSubscribeOnSubscribeRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final Subject<Integer> s = PublishSubject.<Integer>create().toSerialized(); + + TestObserver<Integer> to = s.test(); + + final Disposable bs1 = Disposable.empty(); + final Disposable bs2 = Disposable.empty(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + s.onSubscribe(bs1); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + s.onSubscribe(bs2); + } + }; + + TestHelper.race(r1, r2); + + to.assertEmpty(); + } + } + + @Test + public void onErrorQueued() { + Subject<Integer> sp = PublishSubject.<Integer>create().toSerialized(); + + TestObserver<Integer> to = new TestObserver<Integer>() { + @Override + public void onNext(@NonNull Integer t) { + super.onNext(t); + if (t == 1) { + sp.onNext(2); + sp.onNext(3); + sp.onSubscribe(Disposable.empty()); + sp.onError(new TestException()); + } + } + }; + + sp.subscribe(to); + + sp.onNext(1); + + to.assertFailure(TestException.class, 1); // errors skip ahead + } + + @Test + public void onCompleteQueued() { + Subject<Integer> sp = PublishSubject.<Integer>create().toSerialized(); + + TestObserver<Integer> to = new TestObserver<Integer>() { + @Override + public void onNext(@NonNull Integer t) { + super.onNext(t); + if (t == 1) { + sp.onNext(2); + sp.onNext(3); + sp.onComplete(); + } + } + }; + + sp.subscribe(to); + + sp.onNext(1); + + to.assertResult(1, 2, 3); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/subjects/SingleSubjectTest.java b/src/test/java/io/reactivex/rxjava3/subjects/SingleSubjectTest.java new file mode 100644 index 0000000000..751f0c72d4 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/subjects/SingleSubjectTest.java @@ -0,0 +1,250 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.subjects; + +import static org.junit.Assert.*; + +import java.io.IOException; +import java.util.List; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.*; +import io.reactivex.rxjava3.internal.util.ExceptionHelper; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class SingleSubjectTest extends RxJavaTest { + + @Test + public void success() { + SingleSubject<Integer> ss = SingleSubject.create(); + + assertFalse(ss.hasValue()); + assertNull(ss.getValue()); + assertFalse(ss.hasThrowable()); + assertNull(ss.getThrowable()); + assertFalse(ss.hasObservers()); + assertEquals(0, ss.observerCount()); + + TestObserver<Integer> to = ss.test(); + + to.assertEmpty(); + + assertTrue(ss.hasObservers()); + assertEquals(1, ss.observerCount()); + + ss.onSuccess(1); + + assertTrue(ss.hasValue()); + assertEquals(1, ss.getValue().intValue()); + assertFalse(ss.hasThrowable()); + assertNull(ss.getThrowable()); + assertFalse(ss.hasObservers()); + assertEquals(0, ss.observerCount()); + + to.assertResult(1); + + ss.test().assertResult(1); + + assertTrue(ss.hasValue()); + assertEquals(1, ss.getValue().intValue()); + assertFalse(ss.hasThrowable()); + assertNull(ss.getThrowable()); + assertFalse(ss.hasObservers()); + assertEquals(0, ss.observerCount()); + } + + @Test + public void once() { + SingleSubject<Integer> ss = SingleSubject.create(); + + TestObserver<Integer> to = ss.test(); + + ss.onSuccess(1); + ss.onSuccess(2); + + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + ss.onError(new IOException()); + + TestHelper.assertUndeliverable(errors, 0, IOException.class); + } finally { + RxJavaPlugins.reset(); + } + + to.assertResult(1); + } + + @Test + public void error() { + SingleSubject<Integer> ss = SingleSubject.create(); + + assertFalse(ss.hasValue()); + assertNull(ss.getValue()); + assertFalse(ss.hasThrowable()); + assertNull(ss.getThrowable()); + assertFalse(ss.hasObservers()); + assertEquals(0, ss.observerCount()); + + TestObserver<Integer> to = ss.test(); + + to.assertEmpty(); + + assertTrue(ss.hasObservers()); + assertEquals(1, ss.observerCount()); + + ss.onError(new IOException()); + + assertFalse(ss.hasValue()); + assertNull(ss.getValue()); + assertTrue(ss.hasThrowable()); + assertTrue(ss.getThrowable().toString(), ss.getThrowable() instanceof IOException); + assertFalse(ss.hasObservers()); + assertEquals(0, ss.observerCount()); + + to.assertFailure(IOException.class); + + ss.test().assertFailure(IOException.class); + + assertFalse(ss.hasValue()); + assertNull(ss.getValue()); + assertTrue(ss.hasThrowable()); + assertTrue(ss.getThrowable().toString(), ss.getThrowable() instanceof IOException); + assertFalse(ss.hasObservers()); + assertEquals(0, ss.observerCount()); + } + + @Test + public void nullValue() { + SingleSubject<Integer> ss = SingleSubject.create(); + + try { + ss.onSuccess(null); + fail("No NullPointerException thrown"); + } catch (NullPointerException ex) { + assertEquals(ExceptionHelper.nullWarning("onSuccess called with a null value."), ex.getMessage()); + } + + ss.test().assertEmpty().dispose(); + } + + @Test + public void nullThrowable() { + SingleSubject<Integer> ss = SingleSubject.create(); + + try { + ss.onError(null); + fail("No NullPointerException thrown"); + } catch (NullPointerException ex) { + assertEquals(ExceptionHelper.nullWarning("onError called with a null Throwable."), ex.getMessage()); + } + + ss.test().assertEmpty().dispose(); + } + + @Test + public void cancelOnArrival() { + SingleSubject.create() + .test(true) + .assertEmpty(); + } + + @Test + public void cancelOnArrival2() { + SingleSubject<Integer> ss = SingleSubject.create(); + + ss.test(); + + ss + .test(true) + .assertEmpty(); + } + + @Test + public void dispose() { + TestHelper.checkDisposed(SingleSubject.create()); + } + + @Test + public void disposeTwice() { + SingleSubject.create() + .subscribe(new SingleObserver<Object>() { + @Override + public void onSubscribe(Disposable d) { + assertFalse(d.isDisposed()); + + d.dispose(); + d.dispose(); + + assertTrue(d.isDisposed()); + } + + @Override + public void onSuccess(Object value) { + + } + + @Override + public void onError(Throwable e) { + + } + }); + } + + @Test + public void onSubscribeDispose() { + SingleSubject<Integer> ss = SingleSubject.create(); + + Disposable d = Disposable.empty(); + + ss.onSubscribe(d); + + assertFalse(d.isDisposed()); + + ss.onSuccess(1); + + d = Disposable.empty(); + + ss.onSubscribe(d); + + assertTrue(d.isDisposed()); + } + + @Test + public void addRemoveRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final SingleSubject<Integer> ss = SingleSubject.create(); + + final TestObserver<Integer> to = ss.test(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + ss.test(); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + to.dispose(); + } + }; + TestHelper.race(r1, r2); + } + } +} diff --git a/src/test/java/io/reactivex/rxjava3/subjects/SubjectTest.java b/src/test/java/io/reactivex/rxjava3/subjects/SubjectTest.java new file mode 100644 index 0000000000..ce7177a58d --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/subjects/SubjectTest.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.subjects; + +import static org.junit.Assert.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.RxJavaTest; +import io.reactivex.rxjava3.internal.util.ExceptionHelper; + +public abstract class SubjectTest<T> extends RxJavaTest { + + protected abstract Subject<T> create(); + + @Test + public void onNextNull() { + Subject<T> p = create(); + + try { + p.onNext(null); + fail("No NullPointerException thrown"); + } catch (NullPointerException ex) { + assertEquals(ExceptionHelper.nullWarning("onNext called with a null value."), ex.getMessage()); + } + + p.test().assertEmpty().dispose(); + } + + @Test + public void onErrorNull() { + Subject<T> p = create(); + + try { + p.onError(null); + fail("No NullPointerException thrown"); + } catch (NullPointerException ex) { + assertEquals(ExceptionHelper.nullWarning("onError called with a null Throwable."), ex.getMessage()); + } + + p.test().assertEmpty().dispose(); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/subjects/UnicastSubjectTest.java b/src/test/java/io/reactivex/rxjava3/subjects/UnicastSubjectTest.java new file mode 100644 index 0000000000..8ae618319b --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/subjects/UnicastSubjectTest.java @@ -0,0 +1,512 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.subjects; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.mock; + +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.disposables.*; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.operators.QueueFuseable; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.schedulers.Schedulers; +import io.reactivex.rxjava3.testsupport.*; + +public class UnicastSubjectTest extends SubjectTest<Integer> { + + @Override + protected Subject<Integer> create() { + return UnicastSubject.create(); + } + + @Test + public void fusionLive() { + UnicastSubject<Integer> ap = UnicastSubject.create(); + + TestObserverEx<Integer> to = new TestObserverEx<>(QueueFuseable.ANY); + + ap.subscribe(to); + + to + .assertFuseable() + .assertFusionMode(QueueFuseable.ASYNC); + + to.assertNoValues().assertNoErrors().assertNotComplete(); + + ap.onNext(1); + + to.assertValue(1).assertNoErrors().assertNotComplete(); + + ap.onComplete(); + + to.assertResult(1); + } + + @Test + public void fusionOfflie() { + UnicastSubject<Integer> ap = UnicastSubject.create(); + ap.onNext(1); + ap.onComplete(); + + TestObserverEx<Integer> to = new TestObserverEx<>(QueueFuseable.ANY); + + ap.subscribe(to); + + to + .assertFuseable() + .assertFusionMode(QueueFuseable.ASYNC) + .assertResult(1); + } + + @Test + public void failFast() { + UnicastSubject<Integer> ap = UnicastSubject.create(false); + ap.onNext(1); + ap.onError(new RuntimeException()); + TestObserver<Integer> to = TestObserver.create(); + ap.subscribe(to); + + to + .assertValueCount(0) + .assertError(RuntimeException.class); + } + + @Test + public void threeArgsFactoryFailFast() { + Runnable noop = mock(Runnable.class); + UnicastSubject<Integer> ap = UnicastSubject.create(16, noop, false); + ap.onNext(1); + ap.onError(new RuntimeException()); + TestObserver<Integer> to = TestObserver.create(); + ap.subscribe(to); + + to + .assertValueCount(0) + .assertError(RuntimeException.class); + } + + @Test + public void threeArgsFactoryDelayError() { + Runnable noop = mock(Runnable.class); + UnicastSubject<Integer> ap = UnicastSubject.create(16, noop, true); + ap.onNext(1); + ap.onError(new RuntimeException()); + TestObserver<Integer> to = TestObserver.create(); + ap.subscribe(to); + + to + .assertValueCount(1) + .assertError(RuntimeException.class); + } + + @Test + public void fusionOfflineFailFast() { + UnicastSubject<Integer> ap = UnicastSubject.create(false); + ap.onNext(1); + ap.onError(new RuntimeException()); + TestObserverEx<Integer> to = new TestObserverEx<>(QueueFuseable.ANY); + ap.subscribe(to); + + to + .assertValueCount(0) + .assertError(RuntimeException.class); + } + + @Test + public void fusionOfflineFailFastMultipleEvents() { + UnicastSubject<Integer> ap = UnicastSubject.create(false); + ap.onNext(1); + ap.onNext(2); + ap.onNext(3); + ap.onComplete(); + TestObserverEx<Integer> to = new TestObserverEx<>(QueueFuseable.ANY); + ap.subscribe(to); + + to + .assertValueCount(3) + .assertComplete(); + } + + @Test + public void failFastMultipleEvents() { + UnicastSubject<Integer> ap = UnicastSubject.create(false); + ap.onNext(1); + ap.onNext(2); + ap.onNext(3); + ap.onComplete(); + TestObserver<Integer> to = TestObserver.create(); + ap.subscribe(to); + + to + .assertValueCount(3) + .assertComplete(); + } + + @Test + public void onTerminateCalledWhenOnError() { + final AtomicBoolean didRunOnTerminate = new AtomicBoolean(); + + UnicastSubject<Integer> us = UnicastSubject.create(Observable.bufferSize(), new Runnable() { + @Override public void run() { + didRunOnTerminate.set(true); + } + }); + + assertFalse(didRunOnTerminate.get()); + us.onError(new RuntimeException("some error")); + assertTrue(didRunOnTerminate.get()); + } + + @Test + public void onTerminateCalledWhenOnComplete() { + final AtomicBoolean didRunOnTerminate = new AtomicBoolean(); + + UnicastSubject<Integer> us = UnicastSubject.create(Observable.bufferSize(), new Runnable() { + @Override public void run() { + didRunOnTerminate.set(true); + } + }); + + assertFalse(didRunOnTerminate.get()); + us.onComplete(); + assertTrue(didRunOnTerminate.get()); + } + + @Test + public void onTerminateCalledWhenCanceled() { + final AtomicBoolean didRunOnTerminate = new AtomicBoolean(); + + UnicastSubject<Integer> us = UnicastSubject.create(Observable.bufferSize(), new Runnable() { + @Override public void run() { + didRunOnTerminate.set(true); + } + }); + + final Disposable subscribe = us.subscribe(); + + assertFalse(didRunOnTerminate.get()); + subscribe.dispose(); + assertTrue(didRunOnTerminate.get()); + } + + @Test(expected = NullPointerException.class) + public void nullOnTerminate() { + UnicastSubject.create(5, null); + } + + @Test(expected = IllegalArgumentException.class) + public void negativeCapacityHint() { + UnicastSubject.create(-1); + } + + @Test(expected = IllegalArgumentException.class) + public void zeroCapacityHint() { + UnicastSubject.create(0); + } + + @Test + public void completeCancelRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final int[] calls = { 0 }; + final UnicastSubject<Object> us = UnicastSubject.create(100, new Runnable() { + @Override + public void run() { + calls[0]++; + } + }); + + final TestObserver<Object> to = us.test(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + to.dispose(); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + us.onComplete(); + } + }; + + TestHelper.race(r1, r2); + + assertEquals(1, calls[0]); + } + } + + @Test + public void afterDone() { + UnicastSubject<Object> p = UnicastSubject.create(); + p.onComplete(); + + Disposable bs = Disposable.empty(); + p.onSubscribe(bs); + + p.onNext(1); + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + p.onError(new TestException()); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + p.onComplete(); + + p.test().assertResult(); + + assertNull(p.getThrowable()); + assertTrue(p.hasComplete()); + assertFalse(p.hasThrowable()); + } + + @Test + public void onErrorStatePeeking() { + UnicastSubject<Object> p = UnicastSubject.create(); + + assertFalse(p.hasComplete()); + assertFalse(p.hasThrowable()); + assertNull(p.getThrowable()); + + TestException ex = new TestException(); + p.onError(ex); + + assertFalse(p.hasComplete()); + assertTrue(p.hasThrowable()); + assertSame(ex, p.getThrowable()); + } + + @Test + public void rejectSyncFusion() { + UnicastSubject<Object> p = UnicastSubject.create(); + + TestObserverEx<Object> to = new TestObserverEx<>(QueueFuseable.SYNC); + + p.subscribe(to); + + to.assertFusionMode(QueueFuseable.NONE); + } + + @Test + public void cancelOnArrival() { + UnicastSubject.create() + .test(true) + .assertEmpty(); + } + + @Test + public void multiSubscriber() { + UnicastSubject<Object> p = UnicastSubject.create(); + + TestObserver<Object> to = p.test(); + + p.test() + .assertFailure(IllegalStateException.class); + + p.onNext(1); + p.onComplete(); + + to.assertResult(1); + } + + @Test + public void fusedDrainCancel() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final UnicastSubject<Object> p = UnicastSubject.create(); + + final TestObserverEx<Object> to = new TestObserverEx<>(QueueFuseable.ANY); + + p.subscribe(to); + + Runnable r1 = new Runnable() { + @Override + public void run() { + p.onNext(1); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + to.dispose(); + } + }; + + TestHelper.race(r1, r2); + } + } + + @Test + public void dispose() { + final int[] calls = { 0 }; + + UnicastSubject<Integer> us = new UnicastSubject<>(128, new Runnable() { + @Override + public void run() { + calls[0]++; + } + }, true); + + TestHelper.checkDisposed(us); + + assertEquals(1, calls[0]); + + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + us.onError(new TestException()); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + + Disposable d = Disposable.empty(); + + us.onSubscribe(d); + + assertTrue(d.isDisposed()); + } + + @Test + public void subscribeRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + final UnicastSubject<Integer> us = UnicastSubject.create(); + + final TestObserverEx<Integer> to1 = new TestObserverEx<>(); + final TestObserverEx<Integer> to2 = new TestObserverEx<>(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + us.subscribe(to1); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + us.subscribe(to2); + } + }; + + TestHelper.race(r1, r2); + + if (to1.errors().size() == 0) { + to2.assertFailure(IllegalStateException.class); + } else + if (to2.errors().size() == 0) { + to1.assertFailure(IllegalStateException.class); + } else { + fail("Neither TestObserver failed"); + } + } + } + + @Test + public void hasObservers() { + UnicastSubject<Integer> us = UnicastSubject.create(); + + assertFalse(us.hasObservers()); + + TestObserver<Integer> to = us.test(); + + assertTrue(us.hasObservers()); + + to.dispose(); + + assertFalse(us.hasObservers()); + } + + @Test + public void drainFusedFailFast() { + UnicastSubject<Integer> us = UnicastSubject.create(false); + + TestObserverEx<Integer> to = us.to(TestHelper.<Integer>testConsumer(QueueFuseable.ANY, false)); + + us.done = true; + us.drainFused(to); + + to.assertResult(); + } + + @Test + public void drainFusedFailFastEmpty() { + UnicastSubject<Integer> us = UnicastSubject.create(false); + + TestObserverEx<Integer> to = us.to(TestHelper.<Integer>testConsumer(QueueFuseable.ANY, false)); + + us.drainFused(to); + + to.assertEmpty(); + } + + @Test + public void fusedNoConcurrentCleanDueToCancel() { + for (int j = 0; j < TestHelper.RACE_LONG_LOOPS; j++) { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + final UnicastSubject<Integer> us = UnicastSubject.create(); + + TestObserver<Integer> to = us + .observeOn(Schedulers.io()) + .map(Functions.<Integer>identity()) + .observeOn(Schedulers.single()) + .firstOrError() + .test(); + + for (int i = 0; us.hasObservers(); i++) { + us.onNext(i); + } + + to.awaitDone(10, TimeUnit.SECONDS); + + if (!errors.isEmpty()) { + throw new CompositeException(errors); + } + + to.assertResult(0); + } finally { + RxJavaPlugins.reset(); + } + } + } + + @Test + public void withCapacityHint() { + UnicastSubject<Integer> us = UnicastSubject.create(16); + + TestObserver<Integer> to = us.test(); + + for (int i = 0; i < 256; i++) { + us.onNext(i); + } + us.onComplete(); + + to.assertValueCount(256) + .assertComplete() + .assertNoErrors(); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/subscribers/DefaultSubscriberTest.java b/src/test/java/io/reactivex/rxjava3/subscribers/DefaultSubscriberTest.java new file mode 100644 index 0000000000..b68a6fe695 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/subscribers/DefaultSubscriberTest.java @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.subscribers; + +import static org.junit.Assert.assertEquals; + +import java.util.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; + +public class DefaultSubscriberTest extends RxJavaTest { + + static final class RequestEarly extends DefaultSubscriber<Integer> { + + final List<Object> events = new ArrayList<>(); + + RequestEarly() { + request(5); + } + + @Override + protected void onStart() { + } + + @Override + public void onNext(Integer t) { + events.add(t); + } + + @Override + public void onError(Throwable t) { + events.add(t); + } + + @Override + public void onComplete() { + events.add("Done"); + } + + } + + @Test + public void requestUpfront() { + RequestEarly sub = new RequestEarly(); + + Flowable.range(1, 10).subscribe(sub); + + assertEquals(Collections.emptyList(), sub.events); + } + +} diff --git a/src/test/java/io/reactivex/rxjava3/subscribers/DisposableSubscriberTest.java b/src/test/java/io/reactivex/rxjava3/subscribers/DisposableSubscriberTest.java new file mode 100644 index 0000000000..6aa5c1ff51 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/subscribers/DisposableSubscriberTest.java @@ -0,0 +1,122 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.subscribers; + +import static org.junit.Assert.*; + +import java.util.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.internal.subscriptions.BooleanSubscription; +import io.reactivex.rxjava3.internal.util.EndConsumerHelper; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class DisposableSubscriberTest extends RxJavaTest { + + static final class TestDisposableSubscriber<T> extends DisposableSubscriber<T> { + + int start; + + final List<T> values = new ArrayList<>(); + + final List<Throwable> errors = new ArrayList<>(); + + int completions; + + @Override + protected void onStart() { + request(1); + + start++; + } + + @Override + public void onNext(T value) { + values.add(value); + } + + @Override + public void onError(Throwable e) { + errors.add(e); + } + + @Override + public void onComplete() { + completions++; + } + } + + @Test + public void normal() { + TestDisposableSubscriber<Integer> tc = new TestDisposableSubscriber<>(); + + assertFalse(tc.isDisposed()); + assertEquals(0, tc.start); + assertTrue(tc.values.isEmpty()); + assertTrue(tc.errors.isEmpty()); + + Flowable.just(1).subscribe(tc); + + assertFalse(tc.isDisposed()); + assertEquals(1, tc.start); + assertEquals(1, tc.values.get(0).intValue()); + assertTrue(tc.errors.isEmpty()); + } + + @Test + public void startOnce() { + + List<Throwable> error = TestHelper.trackPluginErrors(); + + try { + TestDisposableSubscriber<Integer> tc = new TestDisposableSubscriber<>(); + + tc.onSubscribe(new BooleanSubscription()); + + BooleanSubscription bs = new BooleanSubscription(); + + tc.onSubscribe(bs); + + assertTrue(bs.isCancelled()); + + assertEquals(1, tc.start); + + TestHelper.assertError(error, 0, IllegalStateException.class, EndConsumerHelper.composeMessage(tc.getClass().getName())); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void dispose() { + TestDisposableSubscriber<Integer> tc = new TestDisposableSubscriber<>(); + + assertFalse(tc.isDisposed()); + + tc.dispose(); + + assertTrue(tc.isDisposed()); + + BooleanSubscription bs = new BooleanSubscription(); + + tc.onSubscribe(bs); + + assertTrue(bs.isCancelled()); + + assertEquals(0, tc.start); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/subscribers/ResourceSubscriberTest.java b/src/test/java/io/reactivex/rxjava3/subscribers/ResourceSubscriberTest.java new file mode 100644 index 0000000000..6bbc127cda --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/subscribers/ResourceSubscriberTest.java @@ -0,0 +1,257 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.subscribers; + +import static org.junit.Assert.*; + +import java.util.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.internal.subscriptions.BooleanSubscription; +import io.reactivex.rxjava3.internal.util.EndConsumerHelper; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class ResourceSubscriberTest extends RxJavaTest { + + static class TestResourceSubscriber<T> extends ResourceSubscriber<T> { + final List<T> values = new ArrayList<>(); + + final List<Throwable> errors = new ArrayList<>(); + + int complete; + + int start; + + @Override + protected void onStart() { + super.onStart(); + + start++; + } + + @Override + public void onNext(T value) { + values.add(value); + } + + @Override + public void onError(Throwable e) { + errors.add(e); + + dispose(); + } + + @Override + public void onComplete() { + complete++; + + dispose(); + } + + void requestMore(long n) { + request(n); + } + } + + @Test(expected = NullPointerException.class) + public void nullResource() { + TestResourceSubscriber<Integer> ro = new TestResourceSubscriber<>(); + ro.add(null); + } + + @Test + public void addResources() { + TestResourceSubscriber<Integer> ro = new TestResourceSubscriber<>(); + + assertFalse(ro.isDisposed()); + + Disposable d = Disposable.empty(); + + ro.add(d); + + assertFalse(d.isDisposed()); + + ro.dispose(); + + assertTrue(ro.isDisposed()); + + assertTrue(d.isDisposed()); + + ro.dispose(); + + assertTrue(ro.isDisposed()); + + assertTrue(d.isDisposed()); + } + + @Test + public void onCompleteCleansUp() { + TestResourceSubscriber<Integer> ro = new TestResourceSubscriber<>(); + + assertFalse(ro.isDisposed()); + + Disposable d = Disposable.empty(); + + ro.add(d); + + assertFalse(d.isDisposed()); + + ro.onComplete(); + + assertTrue(ro.isDisposed()); + + assertTrue(d.isDisposed()); + } + + @Test + public void onErrorCleansUp() { + TestResourceSubscriber<Integer> ro = new TestResourceSubscriber<>(); + + assertFalse(ro.isDisposed()); + + Disposable d = Disposable.empty(); + + ro.add(d); + + assertFalse(d.isDisposed()); + + ro.onError(new TestException()); + + assertTrue(ro.isDisposed()); + + assertTrue(d.isDisposed()); + } + + @Test + public void normal() { + TestResourceSubscriber<Integer> tc = new TestResourceSubscriber<>(); + + assertFalse(tc.isDisposed()); + assertEquals(0, tc.start); + assertTrue(tc.values.isEmpty()); + assertTrue(tc.errors.isEmpty()); + + Flowable.just(1).subscribe(tc); + + assertTrue(tc.isDisposed()); + assertEquals(1, tc.start); + assertEquals(1, tc.values.get(0).intValue()); + assertTrue(tc.errors.isEmpty()); + } + + @Test + public void startOnce() { + + List<Throwable> error = TestHelper.trackPluginErrors(); + + try { + TestResourceSubscriber<Integer> tc = new TestResourceSubscriber<>(); + + tc.onSubscribe(new BooleanSubscription()); + + BooleanSubscription bs = new BooleanSubscription(); + + tc.onSubscribe(bs); + + assertTrue(bs.isCancelled()); + + assertEquals(1, tc.start); + + TestHelper.assertError(error, 0, IllegalStateException.class, EndConsumerHelper.composeMessage(tc.getClass().getName())); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void dispose() { + TestResourceSubscriber<Integer> tc = new TestResourceSubscriber<>(); + tc.dispose(); + + BooleanSubscription bs = new BooleanSubscription(); + + tc.onSubscribe(bs); + + assertTrue(bs.isCancelled()); + + assertEquals(0, tc.start); + } + + @Test + public void request() { + TestResourceSubscriber<Integer> tc = new TestResourceSubscriber<Integer>() { + @Override + protected void onStart() { + start++; + } + }; + + Flowable.just(1).subscribe(tc); + + assertEquals(1, tc.start); + assertEquals(Collections.emptyList(), tc.values); + assertTrue(tc.errors.isEmpty()); + assertEquals(0, tc.complete); + + tc.requestMore(1); + + assertEquals(1, tc.start); + assertEquals(1, tc.values.get(0).intValue()); + assertTrue(tc.errors.isEmpty()); + assertEquals(1, tc.complete); + } + + static final class RequestEarly extends ResourceSubscriber<Integer> { + + final List<Object> events = new ArrayList<>(); + + RequestEarly() { + request(5); + } + + @Override + protected void onStart() { + } + + @Override + public void onNext(Integer t) { + events.add(t); + } + + @Override + public void onError(Throwable t) { + events.add(t); + } + + @Override + public void onComplete() { + events.add("Done"); + } + + } + + @Test + public void requestUpfront() { + RequestEarly sub = new RequestEarly(); + + Flowable.range(1, 10).subscribe(sub); + + assertEquals(Arrays.<Object>asList(1, 2, 3, 4, 5), sub.events); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/subscribers/SafeSubscriberTest.java b/src/test/java/io/reactivex/rxjava3/subscribers/SafeSubscriberTest.java new file mode 100644 index 0000000000..cd97fa11a3 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/subscribers/SafeSubscriberTest.java @@ -0,0 +1,841 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.subscribers; + +import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; + +import org.junit.Test; +import org.mockito.Mockito; +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.internal.subscriptions.BooleanSubscription; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.testsupport.*; + +public class SafeSubscriberTest extends RxJavaTest { + + /** + * Ensure onNext can not be called after onError. + */ + @Test + public void onNextAfterOnError() { + TestObservable t = new TestObservable(); + Flowable<String> st = Flowable.unsafeCreate(t); + + Subscriber<String> w = TestHelper.mockSubscriber(); + st.subscribe(new SafeSubscriber<>(new TestSubscriber<>(w))); + + t.sendOnNext("one"); + t.sendOnError(new RuntimeException("bad")); + t.sendOnNext("two"); + + verify(w, times(1)).onNext("one"); + verify(w, times(1)).onError(any(Throwable.class)); + verify(w, Mockito.never()).onNext("two"); + } + + /** + * Ensure onComplete can not be called after onError. + */ + @Test + public void onCompletedAfterOnError() { + TestObservable t = new TestObservable(); + Flowable<String> st = Flowable.unsafeCreate(t); + + Subscriber<String> w = TestHelper.mockSubscriber(); + + st.subscribe(new SafeSubscriber<>(new TestSubscriber<>(w))); + + t.sendOnNext("one"); + t.sendOnError(new RuntimeException("bad")); + t.sendOnCompleted(); + + verify(w, times(1)).onNext("one"); + verify(w, times(1)).onError(any(Throwable.class)); + verify(w, Mockito.never()).onComplete(); + } + + /** + * Ensure onNext can not be called after onComplete. + */ + @Test + public void onNextAfterOnCompleted() { + TestObservable t = new TestObservable(); + Flowable<String> st = Flowable.unsafeCreate(t); + + Subscriber<String> w = TestHelper.mockSubscriber(); + st.subscribe(new SafeSubscriber<>(new TestSubscriber<>(w))); + + t.sendOnNext("one"); + t.sendOnCompleted(); + t.sendOnNext("two"); + + verify(w, times(1)).onNext("one"); + verify(w, Mockito.never()).onNext("two"); + verify(w, times(1)).onComplete(); + verify(w, Mockito.never()).onError(any(Throwable.class)); + } + + /** + * Ensure onError can not be called after onComplete. + */ + @Test + @SuppressUndeliverable + public void onErrorAfterOnCompleted() { + TestObservable t = new TestObservable(); + Flowable<String> st = Flowable.unsafeCreate(t); + + Subscriber<String> w = TestHelper.mockSubscriber(); + st.subscribe(new SafeSubscriber<>(new TestSubscriber<>(w))); + + t.sendOnNext("one"); + t.sendOnCompleted(); + t.sendOnError(new RuntimeException("bad")); + + verify(w, times(1)).onNext("one"); + verify(w, times(1)).onComplete(); + verify(w, Mockito.never()).onError(any(Throwable.class)); + } + + /** + * An Observable that doesn't do the right thing on UnSubscribe/Error/etc in that it will keep sending events down the pipe regardless of what happens. + */ + private static class TestObservable implements Publisher<String> { + + Subscriber<? super String> subscriber; + + /* used to simulate subscription */ + public void sendOnCompleted() { + subscriber.onComplete(); + } + + /* used to simulate subscription */ + public void sendOnNext(String value) { + subscriber.onNext(value); + } + + /* used to simulate subscription */ + public void sendOnError(Throwable e) { + subscriber.onError(e); + } + + @Override + public void subscribe(Subscriber<? super String> subscriber) { + this.subscriber = subscriber; + subscriber.onSubscribe(new Subscription() { + + @Override + public void cancel() { + // going to do nothing to pretend I'm a bad Observable that keeps allowing events to be sent + System.out.println("==> SynchronizeTest unsubscribe that does nothing!"); + } + + @Override + public void request(long n) { + + } + + }); + } + + } + + @Test + public void onNextFailure() { + AtomicReference<Throwable> onError = new AtomicReference<>(); + try { + OBSERVER_ONNEXT_FAIL(onError).onNext("one"); + fail("expects exception to be thrown"); + } catch (Exception e) { + assertNull(onError.get()); + assertTrue(e instanceof SafeSubscriberTestException); + assertEquals("onNextFail", e.getMessage()); + } + } + + @Test + public void onNextFailureSafe() { + AtomicReference<Throwable> onError = new AtomicReference<>(); + try { + SafeSubscriber<String> safeObserver = new SafeSubscriber<>(OBSERVER_ONNEXT_FAIL(onError)); + safeObserver.onSubscribe(new BooleanSubscription()); + safeObserver.onNext("one"); + assertNotNull(onError.get()); + assertTrue(onError.get() instanceof SafeSubscriberTestException); + assertEquals("onNextFail", onError.get().getMessage()); + } catch (Exception e) { + fail("expects exception to be passed to onError"); + } + } + + @Test + public void onCompleteFailure() { + AtomicReference<Throwable> onError = new AtomicReference<>(); + try { + OBSERVER_ONCOMPLETED_FAIL(onError).onComplete(); + fail("expects exception to be thrown"); + } catch (Exception e) { + assertNull(onError.get()); + assertTrue(e instanceof SafeSubscriberTestException); + assertEquals("onCompleteFail", e.getMessage()); + } + } + + @Test + public void onErrorFailure() { + try { + subscriberOnErrorFail().onError(new SafeSubscriberTestException("error!")); + fail("expects exception to be thrown"); + } catch (Exception e) { + assertTrue(e instanceof SafeSubscriberTestException); + assertEquals("onErrorFail", e.getMessage()); + } + } + + @Test + public void onNextOnErrorFailure() { + try { + OBSERVER_ONNEXT_ONERROR_FAIL().onNext("one"); + fail("expects exception to be thrown"); + } catch (Exception e) { + e.printStackTrace(); + assertTrue(e instanceof SafeSubscriberTestException); + assertEquals("onNextFail", e.getMessage()); + } + } + + static final Subscription THROWING_DISPOSABLE = new Subscription() { + + @Override + public void cancel() { + // break contract by throwing exception + throw new SafeSubscriberTestException("failure from unsubscribe"); + } + + @Override + public void request(long n) { + // ignored + } + }; + + private static Subscriber<String> OBSERVER_ONNEXT_FAIL(final AtomicReference<Throwable> onError) { + return new DefaultSubscriber<String>() { + + @Override + public void onComplete() { + + } + + @Override + public void onError(Throwable e) { + onError.set(e); + } + + @Override + public void onNext(String args) { + throw new SafeSubscriberTestException("onNextFail"); + } + }; + + } + + private static Subscriber<String> OBSERVER_ONNEXT_ONERROR_FAIL() { + return new DefaultSubscriber<String>() { + + @Override + public void onComplete() { + + } + + @Override + public void onError(Throwable e) { + throw new SafeSubscriberTestException("onErrorFail"); + } + + @Override + public void onNext(String args) { + throw new SafeSubscriberTestException("onNextFail"); + } + + }; + } + + private static Subscriber<String> subscriberOnErrorFail() { + return new DefaultSubscriber<String>() { + + @Override + public void onComplete() { + + } + + @Override + public void onError(Throwable e) { + throw new SafeSubscriberTestException("onErrorFail"); + } + + @Override + public void onNext(String args) { + + } + + }; + } + + private static Subscriber<String> OBSERVER_ONCOMPLETED_FAIL(final AtomicReference<Throwable> onError) { + return new DefaultSubscriber<String>() { + + @Override + public void onComplete() { + throw new SafeSubscriberTestException("onCompleteFail"); + } + + @Override + public void onError(Throwable e) { + onError.set(e); + } + + @Override + public void onNext(String args) { + + } + + }; + } + + @SuppressWarnings("serial") + private static class SafeSubscriberTestException extends RuntimeException { + SafeSubscriberTestException(String message) { + super(message); + } + } + + @Test + public void actual() { + Subscriber<Integer> actual = new DefaultSubscriber<Integer>() { + @Override + public void onNext(Integer t) { + } + + @Override + public void onError(Throwable e) { + } + + @Override + public void onComplete() { + } + }; + SafeSubscriber<Integer> s = new SafeSubscriber<>(actual); + + assertSame(actual, s.downstream); + } + + @Test + public void dispose() { + TestSubscriber<Integer> ts = new TestSubscriber<>(); + + SafeSubscriber<Integer> so = new SafeSubscriber<>(ts); + + BooleanSubscription bs = new BooleanSubscription(); + + so.onSubscribe(bs); + + ts.dispose(); + + assertTrue(bs.isCancelled()); + +// assertTrue(so.isDisposed()); + } + + @Test + @SuppressUndeliverable + public void onNextAfterComplete() { + TestSubscriber<Integer> ts = new TestSubscriber<>(); + + SafeSubscriber<Integer> so = new SafeSubscriber<>(ts); + + BooleanSubscription bs = new BooleanSubscription(); + + so.onSubscribe(bs); + + so.onComplete(); + + so.onNext(1); + + so.onError(new TestException()); + + so.onComplete(); + + ts.assertResult(); + } + + @Test + public void onNextNull() { + TestSubscriber<Integer> ts = new TestSubscriber<>(); + + SafeSubscriber<Integer> so = new SafeSubscriber<>(ts); + + BooleanSubscription bs = new BooleanSubscription(); + + so.onSubscribe(bs); + + so.onNext(null); + + ts.assertFailure(NullPointerException.class); + } + + @Test + public void onNextWithoutOnSubscribe() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(); + + SafeSubscriber<Integer> so = new SafeSubscriber<>(ts); + + so.onNext(1); + + ts.assertFailureAndMessage(NullPointerException.class, "Subscription not set!"); + } + + @Test + public void onErrorWithoutOnSubscribe() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(); + + SafeSubscriber<Integer> so = new SafeSubscriber<>(ts); + + so.onError(new TestException()); + + ts.assertFailure(CompositeException.class); + + TestHelper.assertError(ts, 0, TestException.class); + TestHelper.assertError(ts, 1, NullPointerException.class, "Subscription not set!"); + } + + @Test + public void onCompleteWithoutOnSubscribe() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(); + + SafeSubscriber<Integer> so = new SafeSubscriber<>(ts); + + so.onComplete(); + + ts.assertFailureAndMessage(NullPointerException.class, "Subscription not set!"); + } + + @Test + public void onNextNormal() { + TestSubscriber<Integer> ts = new TestSubscriber<>(); + + SafeSubscriber<Integer> so = new SafeSubscriber<>(ts); + + BooleanSubscription bs = new BooleanSubscription(); + + so.onSubscribe(bs); + + so.onNext(1); + so.onComplete(); + + ts.assertResult(1); + } + + static final class CrashDummy implements FlowableSubscriber<Object>, Subscription { + final boolean crashOnSubscribe; + + int crashOnNext; + + final boolean crashOnError; + + final boolean crashOnComplete; + + final boolean crashDispose; + + final boolean crashRequest; + + Throwable error; + + CrashDummy(boolean crashOnSubscribe, int crashOnNext, + boolean crashOnError, boolean crashOnComplete, boolean crashDispose, boolean crashRequest) { + this.crashOnSubscribe = crashOnSubscribe; + this.crashOnNext = crashOnNext; + this.crashOnError = crashOnError; + this.crashOnComplete = crashOnComplete; + this.crashDispose = crashDispose; + this.crashRequest = crashRequest; + } + + @Override + public void cancel() { + if (crashDispose) { + throw new TestException("cancel()"); + } + } + + @Override + public void request(long n) { + if (crashRequest) { + throw new TestException("request()"); + } + } + + @Override + public void onSubscribe(Subscription s) { + if (crashOnSubscribe) { + throw new TestException("onSubscribe()"); + } + } + + @Override + public void onNext(Object value) { + if (--crashOnNext == 0) { + throw new TestException("onNext(" + value + ")"); + } + } + + @Override + public void onError(Throwable e) { + if (crashOnError) { + throw new TestException("onError(" + e + ")"); + } + error = e; + } + + @Override + public void onComplete() { + if (crashOnComplete) { + throw new TestException("onComplete()"); + } + } + + public SafeSubscriber<Object> toSafe() { + return new SafeSubscriber<>(this); + } + + public CrashDummy assertError(Class<? extends Throwable> clazz) { + if (!clazz.isInstance(error)) { + throw new AssertionError("Different error: " + error); + } + return this; + } + + public CrashDummy assertInnerError(int index, Class<? extends Throwable> clazz) { + List<Throwable> cel = TestHelper.compositeList(error); + TestHelper.assertError(cel, index, clazz); + return this; + } + + public CrashDummy assertInnerError(int index, Class<? extends Throwable> clazz, String message) { + List<Throwable> cel = TestHelper.compositeList(error); + TestHelper.assertError(cel, index, clazz, message); + return this; + } + + } + + @Test + public void onNextOnErrorCrash() { + List<Throwable> list = TestHelper.trackPluginErrors(); + + try { + CrashDummy cd = new CrashDummy(false, 1, true, false, false, false); + SafeSubscriber<Object> so = cd.toSafe(); + so.onSubscribe(cd); + + so.onNext(1); + + TestHelper.assertError(list, 0, CompositeException.class); + List<Throwable> ce = TestHelper.compositeList(list.get(0)); + TestHelper.assertError(ce, 0, TestException.class, "onNext(1)"); + TestHelper.assertError(ce, 1, TestException.class, "onError(io.reactivex.rxjava3.exceptions.TestException: onNext(1))"); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void onNextDisposeCrash() { + CrashDummy cd = new CrashDummy(false, 1, false, false, true, false); + SafeSubscriber<Object> so = cd.toSafe(); + so.onSubscribe(cd); + + so.onNext(1); + + cd.assertError(CompositeException.class); + cd.assertInnerError(0, TestException.class, "onNext(1)"); + cd.assertInnerError(1, TestException.class, "cancel()"); + } + + @Test + public void onSubscribeTwice() { + List<Throwable> list = TestHelper.trackPluginErrors(); + + try { + CrashDummy cd = new CrashDummy(false, 1, false, false, false, false); + SafeSubscriber<Object> so = cd.toSafe(); + so.onSubscribe(cd); + so.onSubscribe(cd); + + TestHelper.assertError(list, 0, IllegalStateException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void onSubscribeCrashes() { + List<Throwable> list = TestHelper.trackPluginErrors(); + + try { + CrashDummy cd = new CrashDummy(true, 1, false, false, false, false); + SafeSubscriber<Object> so = cd.toSafe(); + so.onSubscribe(cd); + + TestHelper.assertUndeliverable(list, 0, TestException.class, "onSubscribe()"); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void onSubscribeAndDisposeCrashes() { + List<Throwable> list = TestHelper.trackPluginErrors(); + + try { + CrashDummy cd = new CrashDummy(true, 1, false, false, true, false); + SafeSubscriber<Object> so = cd.toSafe(); + so.onSubscribe(cd); + + TestHelper.assertError(list, 0, CompositeException.class); + List<Throwable> ce = TestHelper.compositeList(list.get(0)); + TestHelper.assertError(ce, 0, TestException.class, "onSubscribe()"); + TestHelper.assertError(ce, 1, TestException.class, "cancel()"); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void onNextOnSubscribeCrash() { + List<Throwable> list = TestHelper.trackPluginErrors(); + + try { + CrashDummy cd = new CrashDummy(true, 1, false, false, false, false); + SafeSubscriber<Object> so = cd.toSafe(); + + so.onNext(1); + + TestHelper.assertError(list, 0, CompositeException.class); + List<Throwable> ce = TestHelper.compositeList(list.get(0)); + TestHelper.assertError(ce, 0, NullPointerException.class, "Subscription not set!"); + TestHelper.assertError(ce, 1, TestException.class, "onSubscribe()"); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void onNextNullDisposeCrashes() { + CrashDummy cd = new CrashDummy(false, 1, false, false, true, false); + SafeSubscriber<Object> so = cd.toSafe(); + so.onSubscribe(cd); + + so.onNext(null); + + cd.assertInnerError(0, NullPointerException.class); + cd.assertInnerError(1, TestException.class, "cancel()"); + } + + @Test + public void noSubscribeOnErrorCrashes() { + List<Throwable> list = TestHelper.trackPluginErrors(); + + try { + CrashDummy cd = new CrashDummy(false, 1, true, false, false, false); + SafeSubscriber<Object> so = cd.toSafe(); + + so.onNext(1); + + TestHelper.assertError(list, 0, CompositeException.class); + List<Throwable> ce = TestHelper.compositeList(list.get(0)); + TestHelper.assertError(ce, 0, NullPointerException.class, "Subscription not set!"); + TestHelper.assertError(ce, 1, TestException.class, "onError(java.lang.NullPointerException: Subscription not set!)"); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void onErrorNull() { + CrashDummy cd = new CrashDummy(false, 1, false, false, false, false); + SafeSubscriber<Object> so = cd.toSafe(); + so.onSubscribe(cd); + + so.onError(null); + + cd.assertError(NullPointerException.class); + } + + @Test + public void onErrorNoSubscribeCrash() { + List<Throwable> list = TestHelper.trackPluginErrors(); + + try { + CrashDummy cd = new CrashDummy(true, 1, false, false, false, false); + SafeSubscriber<Object> so = cd.toSafe(); + + so.onError(new TestException()); + + TestHelper.assertError(list, 0, CompositeException.class); + List<Throwable> ce = TestHelper.compositeList(list.get(0)); + TestHelper.assertError(ce, 0, TestException.class); + TestHelper.assertError(ce, 1, NullPointerException.class, "Subscription not set!"); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void onErrorNoSubscribeOnErrorCrash() { + List<Throwable> list = TestHelper.trackPluginErrors(); + + try { + CrashDummy cd = new CrashDummy(false, 1, true, false, false, false); + SafeSubscriber<Object> so = cd.toSafe(); + + so.onError(new TestException()); + + TestHelper.assertError(list, 0, CompositeException.class); + List<Throwable> ce = TestHelper.compositeList(list.get(0)); + TestHelper.assertError(ce, 0, TestException.class); + TestHelper.assertError(ce, 1, NullPointerException.class, "Subscription not set!"); + TestHelper.assertError(ce, 2, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void onCompleteteCrash() { + List<Throwable> list = TestHelper.trackPluginErrors(); + + try { + CrashDummy cd = new CrashDummy(false, 1, false, true, false, false); + SafeSubscriber<Object> so = cd.toSafe(); + + so.onSubscribe(cd); + + so.onComplete(); + + TestHelper.assertUndeliverable(list, 0, TestException.class, "onComplete()"); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void onCompleteteNoSubscribeCrash() { + List<Throwable> list = TestHelper.trackPluginErrors(); + + try { + CrashDummy cd = new CrashDummy(true, 1, false, true, false, false); + SafeSubscriber<Object> so = cd.toSafe(); + + so.onComplete(); + + TestHelper.assertError(list, 0, CompositeException.class); + List<Throwable> ce = TestHelper.compositeList(list.get(0)); + TestHelper.assertError(ce, 0, NullPointerException.class, "Subscription not set!"); + TestHelper.assertError(ce, 1, TestException.class, "onSubscribe()"); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void onCompleteteNoSubscribeOnErrorCrash() { + List<Throwable> list = TestHelper.trackPluginErrors(); + + try { + CrashDummy cd = new CrashDummy(false, 1, true, true, false, false); + SafeSubscriber<Object> so = cd.toSafe(); + + so.onComplete(); + + TestHelper.assertError(list, 0, CompositeException.class); + List<Throwable> ce = TestHelper.compositeList(list.get(0)); + TestHelper.assertError(ce, 0, NullPointerException.class, "Subscription not set!"); + TestHelper.assertError(ce, 1, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void requestCrash() { + List<Throwable> list = TestHelper.trackPluginErrors(); + + try { + CrashDummy cd = new CrashDummy(false, 1, false, false, false, true); + SafeSubscriber<Object> so = cd.toSafe(); + so.onSubscribe(cd); + + so.request(1); + + TestHelper.assertUndeliverable(list, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void cancelCrash() { + List<Throwable> list = TestHelper.trackPluginErrors(); + + try { + CrashDummy cd = new CrashDummy(false, 1, false, false, true, false); + SafeSubscriber<Object> so = cd.toSafe(); + so.onSubscribe(cd); + + so.cancel(); + + TestHelper.assertUndeliverable(list, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void requestCancelCrash() { + List<Throwable> list = TestHelper.trackPluginErrors(); + + try { + CrashDummy cd = new CrashDummy(false, 1, false, false, true, true); + SafeSubscriber<Object> so = cd.toSafe(); + so.onSubscribe(cd); + + so.request(1); + + TestHelper.assertError(list, 0, CompositeException.class); + List<Throwable> ce = TestHelper.compositeList(list.get(0)); + TestHelper.assertError(ce, 0, TestException.class, "request()"); + TestHelper.assertError(ce, 1, TestException.class, "cancel()"); + } finally { + RxJavaPlugins.reset(); + } + } + +} diff --git a/src/test/java/io/reactivex/rxjava3/subscribers/SerializedSubscriberTest.java b/src/test/java/io/reactivex/rxjava3/subscribers/SerializedSubscriberTest.java new file mode 100644 index 0000000000..2402762d93 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/subscribers/SerializedSubscriberTest.java @@ -0,0 +1,1162 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.subscribers; + +import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import java.util.List; +import java.util.concurrent.*; +import java.util.concurrent.atomic.*; + +import org.junit.*; +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.internal.subscriptions.BooleanSubscription; +import io.reactivex.rxjava3.internal.util.ExceptionHelper; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.schedulers.Schedulers; +import io.reactivex.rxjava3.testsupport.*; + +public class SerializedSubscriberTest extends RxJavaTest { + + Subscriber<String> subscriber; + + @Before + public void before() { + subscriber = TestHelper.mockSubscriber(); + } + + private Subscriber<String> serializedSubscriber(Subscriber<String> subscriber) { + return new SerializedSubscriber<>(subscriber); + } + + @Test + public void singleThreadedBasic() { + TestSingleThreadedPublisher onSubscribe = new TestSingleThreadedPublisher("one", "two", "three"); + Flowable<String> w = Flowable.unsafeCreate(onSubscribe); + + Subscriber<String> aw = serializedSubscriber(subscriber); + + w.subscribe(aw); + onSubscribe.waitToFinish(); + + verify(subscriber, times(1)).onNext("one"); + verify(subscriber, times(1)).onNext("two"); + verify(subscriber, times(1)).onNext("three"); + verify(subscriber, never()).onError(any(Throwable.class)); + verify(subscriber, times(1)).onComplete(); + // non-deterministic because unsubscribe happens after 'waitToFinish' releases + // so commenting out for now as this is not a critical thing to test here + // verify(s, times(1)).unsubscribe(); + } + + @Test + public void multiThreadedBasic() { + TestMultiThreadedObservable onSubscribe = new TestMultiThreadedObservable("one", "two", "three"); + Flowable<String> w = Flowable.unsafeCreate(onSubscribe); + + BusySubscriber busySubscriber = new BusySubscriber(); + Subscriber<String> aw = serializedSubscriber(busySubscriber); + + w.subscribe(aw); + onSubscribe.waitToFinish(); + + assertEquals(3, busySubscriber.onNextCount.get()); + assertFalse(busySubscriber.onError); + assertTrue(busySubscriber.onComplete); + // non-deterministic because unsubscribe happens after 'waitToFinish' releases + // so commenting out for now as this is not a critical thing to test here + // verify(s, times(1)).unsubscribe(); + + // we can have concurrency ... + assertTrue(onSubscribe.maxConcurrentThreads.get() > 1); + // ... but the onNext execution should be single threaded + assertEquals(1, busySubscriber.maxConcurrentThreads.get()); + } + + @Test + public void multiThreadedWithNPE() throws InterruptedException { + TestMultiThreadedObservable onSubscribe = new TestMultiThreadedObservable("one", "two", "three", null); + Flowable<String> w = Flowable.unsafeCreate(onSubscribe); + + BusySubscriber busySubscriber = new BusySubscriber(); + Subscriber<String> aw = serializedSubscriber(busySubscriber); + + w.subscribe(aw); + onSubscribe.waitToFinish(); + busySubscriber.terminalEvent.await(); + + System.out.println("OnSubscribe maxConcurrentThreads: " + onSubscribe.maxConcurrentThreads.get() + " Subscriber maxConcurrentThreads: " + busySubscriber.maxConcurrentThreads.get()); + + // we can't know how many onNext calls will occur since they each run on a separate thread + // that depends on thread scheduling so 0, 1, 2 and 3 are all valid options + // assertEquals(3, busySubscriber.onNextCount.get()); + assertTrue(busySubscriber.onNextCount.get() < 4); + assertTrue(busySubscriber.onError); + // no onComplete because onError was invoked + assertFalse(busySubscriber.onComplete); + // non-deterministic because unsubscribe happens after 'waitToFinish' releases + // so commenting out for now as this is not a critical thing to test here + //verify(s, times(1)).unsubscribe(); + + // we can have concurrency ... + assertTrue(onSubscribe.maxConcurrentThreads.get() > 1); + // ... but the onNext execution should be single threaded + assertEquals(1, busySubscriber.maxConcurrentThreads.get()); + } + + @Test + public void multiThreadedWithNPEinMiddle() { + int n = 10; + for (int i = 0; i < n; i++) { + TestMultiThreadedObservable onSubscribe = new TestMultiThreadedObservable("one", "two", "three", null, + "four", "five", "six", "seven", "eight", "nine"); + Flowable<String> w = Flowable.unsafeCreate(onSubscribe); + + BusySubscriber busySubscriber = new BusySubscriber(); + Subscriber<String> aw = serializedSubscriber(busySubscriber); + + w.subscribe(aw); + onSubscribe.waitToFinish(); + + System.out.println("OnSubscribe maxConcurrentThreads: " + onSubscribe.maxConcurrentThreads.get() + " Subscriber maxConcurrentThreads: " + busySubscriber.maxConcurrentThreads.get()); + + // we can have concurrency ... + assertTrue(onSubscribe.maxConcurrentThreads.get() > 1); + // ... but the onNext execution should be single threaded + assertEquals(1, busySubscriber.maxConcurrentThreads.get()); + + // this should not be the full number of items since the error should stop it before it completes all 9 + System.out.println("onNext count: " + busySubscriber.onNextCount.get()); + assertFalse(busySubscriber.onComplete); + assertTrue(busySubscriber.onError); + assertTrue(busySubscriber.onNextCount.get() < 9); + // no onComplete because onError was invoked + // non-deterministic because unsubscribe happens after 'waitToFinish' releases + // so commenting out for now as this is not a critical thing to test here + // verify(s, times(1)).unsubscribe(); + } + } + + /** + * A non-realistic use case that tries to expose thread-safety issues by throwing lots of out-of-order + * events on many threads. + */ + @Test + public void runOutOfOrderConcurrencyTest() { + ExecutorService tp = Executors.newFixedThreadPool(20); + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + TestConcurrencySubscriber tw = new TestConcurrencySubscriber(); + // we need Synchronized + SafeSubscriber to handle synchronization plus life-cycle + Subscriber<String> w = serializedSubscriber(new SafeSubscriber<>(tw)); + + Future<?> f1 = tp.submit(new OnNextThread(w, 12000)); + Future<?> f2 = tp.submit(new OnNextThread(w, 5000)); + Future<?> f3 = tp.submit(new OnNextThread(w, 75000)); + Future<?> f4 = tp.submit(new OnNextThread(w, 13500)); + Future<?> f5 = tp.submit(new OnNextThread(w, 22000)); + Future<?> f6 = tp.submit(new OnNextThread(w, 15000)); + Future<?> f7 = tp.submit(new OnNextThread(w, 7500)); + Future<?> f8 = tp.submit(new OnNextThread(w, 23500)); + + Future<?> f10 = tp.submit(new CompletionThread(w, TestConcurrencySubscriberEvent.onComplete, f1, f2, f3, f4)); + try { + Thread.sleep(1); + } catch (InterruptedException e) { + // ignore + } + Future<?> f11 = tp.submit(new CompletionThread(w, TestConcurrencySubscriberEvent.onComplete, f4, f6, f7)); + Future<?> f12 = tp.submit(new CompletionThread(w, TestConcurrencySubscriberEvent.onComplete, f4, f6, f7)); + Future<?> f13 = tp.submit(new CompletionThread(w, TestConcurrencySubscriberEvent.onComplete, f4, f6, f7)); + Future<?> f14 = tp.submit(new CompletionThread(w, TestConcurrencySubscriberEvent.onComplete, f4, f6, f7)); + // // the next 4 onError events should wait on same as f10 + Future<?> f15 = tp.submit(new CompletionThread(w, TestConcurrencySubscriberEvent.onError, f1, f2, f3, f4)); + Future<?> f16 = tp.submit(new CompletionThread(w, TestConcurrencySubscriberEvent.onError, f1, f2, f3, f4)); + Future<?> f17 = tp.submit(new CompletionThread(w, TestConcurrencySubscriberEvent.onError, f1, f2, f3, f4)); + Future<?> f18 = tp.submit(new CompletionThread(w, TestConcurrencySubscriberEvent.onError, f1, f2, f3, f4)); + + waitOnThreads(f1, f2, f3, f4, f5, f6, f7, f8, f10, f11, f12, f13, f14, f15, f16, f17, f18); + @SuppressWarnings("unused") + int numNextEvents = tw.assertEvents(null); // no check of type since we don't want to test barging results here, just interleaving behavior + // System.out.println("Number of events executed: " + numNextEvents); + + for (int i = 0; i < errors.size(); i++) { + TestHelper.assertUndeliverable(errors, i, RuntimeException.class); + } + } catch (Throwable e) { + fail("Concurrency test failed: " + e.getMessage()); + e.printStackTrace(); + } finally { + tp.shutdown(); + try { + tp.awaitTermination(5000, TimeUnit.MILLISECONDS); + } catch (InterruptedException e) { + e.printStackTrace(); + } + + RxJavaPlugins.reset(); + } + } + + @Test + public void runConcurrencyTest() { + ExecutorService tp = Executors.newFixedThreadPool(20); + try { + TestConcurrencySubscriber tw = new TestConcurrencySubscriber(); + // we need Synchronized + SafeSubscriber to handle synchronization plus life-cycle + Subscriber<String> w = serializedSubscriber(new SafeSubscriber<>(tw)); + w.onSubscribe(new BooleanSubscription()); + + Future<?> f1 = tp.submit(new OnNextThread(w, 12000)); + Future<?> f2 = tp.submit(new OnNextThread(w, 5000)); + Future<?> f3 = tp.submit(new OnNextThread(w, 75000)); + Future<?> f4 = tp.submit(new OnNextThread(w, 13500)); + Future<?> f5 = tp.submit(new OnNextThread(w, 22000)); + Future<?> f6 = tp.submit(new OnNextThread(w, 15000)); + Future<?> f7 = tp.submit(new OnNextThread(w, 7500)); + Future<?> f8 = tp.submit(new OnNextThread(w, 23500)); + + // 12000 + 5000 + 75000 + 13500 + 22000 + 15000 + 7500 + 23500 = 173500 + + Future<?> f10 = tp.submit(new CompletionThread(w, TestConcurrencySubscriberEvent.onComplete, f1, f2, f3, f4, f5, f6, f7, f8)); + try { + Thread.sleep(1); + } catch (InterruptedException e) { + // ignore + } + + waitOnThreads(f1, f2, f3, f4, f5, f6, f7, f8, f10); + int numNextEvents = tw.assertEvents(null); // no check of type since we don't want to test barging results here, just interleaving behavior + assertEquals(173500, numNextEvents); + // System.out.println("Number of events executed: " + numNextEvents); + } catch (Throwable e) { + fail("Concurrency test failed: " + e.getMessage()); + e.printStackTrace(); + } finally { + tp.shutdown(); + try { + tp.awaitTermination(25000, TimeUnit.MILLISECONDS); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + } + + /** + * Test that a notification does not get delayed in the queue waiting for the next event to push it through. + * + * @throws InterruptedException if the await is interrupted + */ + @Ignore("this is non-deterministic ... haven't figured out what's wrong with the test yet (benjchristensen: July 2014)") + @Test + public void notificationDelay() throws InterruptedException { + ExecutorService tp1 = Executors.newFixedThreadPool(1); + ExecutorService tp2 = Executors.newFixedThreadPool(1); + try { + int n = 10; + for (int i = 0; i < n; i++) { + final CountDownLatch firstOnNext = new CountDownLatch(1); + final CountDownLatch onNextCount = new CountDownLatch(2); + final CountDownLatch latch = new CountDownLatch(1); + final CountDownLatch running = new CountDownLatch(2); + + TestSubscriberEx<String> ts = new TestSubscriberEx<>(new DefaultSubscriber<String>() { + + @Override + public void onComplete() { + + } + + @Override + public void onError(Throwable e) { + + } + + @Override + public void onNext(String t) { + firstOnNext.countDown(); + // force it to take time when delivering so the second one is enqueued + try { + latch.await(); + } catch (InterruptedException e) { + } + } + + }); + Subscriber<String> subscriber = serializedSubscriber(ts); + + Future<?> f1 = tp1.submit(new OnNextThread(subscriber, 1, onNextCount, running)); + Future<?> f2 = tp2.submit(new OnNextThread(subscriber, 1, onNextCount, running)); + + running.await(); // let one of the OnNextThread actually run before proceeding + + firstOnNext.await(); + + Thread t1 = ts.lastThread(); + System.out.println("first onNext on thread: " + t1); + + latch.countDown(); + + waitOnThreads(f1, f2); + // not completed yet + + assertEquals(2, ts.values().size()); + + Thread t2 = ts.lastThread(); + System.out.println("second onNext on thread: " + t2); + + assertSame(t1, t2); + + System.out.println(ts.values()); + subscriber.onComplete(); + System.out.println(ts.values()); + } + } finally { + tp1.shutdown(); + tp2.shutdown(); + } + } + + /** + * Demonstrates thread starvation problem. + * + * No solution on this for now. Trade-off in this direction as per https://github.com/ReactiveX/RxJava/issues/998#issuecomment-38959474 + * Probably need backpressure for this to work + * + * When using SynchronizedSubscriber we get this output: + * + * {@code p1: 18 p2: 68 =>} should be close to each other unless we have thread starvation + * + * When using SerializedSubscriber we get: + * + * {@code p1: 1 p2: 2445261 =>} should be close to each other unless we have thread starvation + * + * This demonstrates how SynchronizedSubscriber balances back and forth better, and blocks emission. + * The real issue in this example is the async buffer-bloat, so we need backpressure. + * + * + * @throws InterruptedException if the await is interrupted + */ + @Ignore("Demonstrates thread starvation problem. Read JavaDoc") + @Test + public void threadStarvation() throws InterruptedException { + + TestSubscriber<String> ts = new TestSubscriber<>(new DefaultSubscriber<String>() { + + @Override + public void onComplete() { + + } + + @Override + public void onError(Throwable e) { + + } + + @Override + public void onNext(String t) { + // force it to take time when delivering + try { + Thread.sleep(1); + } catch (InterruptedException e) { + } + } + + }); + final Subscriber<String> subscriber = serializedSubscriber(ts); + + AtomicInteger p1 = new AtomicInteger(); + AtomicInteger p2 = new AtomicInteger(); + + subscriber.onSubscribe(new BooleanSubscription()); + ResourceSubscriber<String> as1 = new ResourceSubscriber<String>() { + @Override + public void onNext(String t) { + subscriber.onNext(t); + } + + @Override + public void onError(Throwable t) { + RxJavaPlugins.onError(t); + } + + @Override + public void onComplete() { + + } + }; + + ResourceSubscriber<String> as2 = new ResourceSubscriber<String>() { + @Override + public void onNext(String t) { + subscriber.onNext(t); + } + + @Override + public void onError(Throwable t) { + RxJavaPlugins.onError(t); + } + + @Override + public void onComplete() { + + } + }; + + infinite(p1).subscribe(as1); + infinite(p2).subscribe(as2); + + Thread.sleep(100); + + System.out.println("p1: " + p1.get() + " p2: " + p2.get() + " => should be close to each other unless we have thread starvation"); + assertEquals(p1.get(), p2.get(), 10000); // fairly distributed within 10000 of each other + + as1.dispose(); + as2.dispose(); + } + + private static void waitOnThreads(Future<?>... futures) { + for (Future<?> f : futures) { + try { + f.get(20, TimeUnit.SECONDS); + } catch (Throwable e) { + System.err.println("Failed while waiting on future."); + e.printStackTrace(); + } + } + } + + private static Flowable<String> infinite(final AtomicInteger produced) { + return Flowable.unsafeCreate(new Publisher<String>() { + + @Override + public void subscribe(Subscriber<? super String> s) { + BooleanSubscription bs = new BooleanSubscription(); + s.onSubscribe(bs); + while (!bs.isCancelled()) { + s.onNext("onNext"); + produced.incrementAndGet(); + } + } + + }).subscribeOn(Schedulers.newThread()); + } + + /** + * A thread that will pass data to onNext. + */ + public static class OnNextThread implements Runnable { + + private final CountDownLatch latch; + private final Subscriber<String> subscriber; + private final int numStringsToSend; + final AtomicInteger produced; + private final CountDownLatch running; + + OnNextThread(Subscriber<String> subscriber, int numStringsToSend, CountDownLatch latch, CountDownLatch running) { + this(subscriber, numStringsToSend, new AtomicInteger(), latch, running); + } + + OnNextThread(Subscriber<String> subscriber, int numStringsToSend, AtomicInteger produced) { + this(subscriber, numStringsToSend, produced, null, null); + } + + OnNextThread(Subscriber<String> subscriber, int numStringsToSend, AtomicInteger produced, CountDownLatch latch, CountDownLatch running) { + this.subscriber = subscriber; + this.numStringsToSend = numStringsToSend; + this.produced = produced; + this.latch = latch; + this.running = running; + } + + OnNextThread(Subscriber<String> subscriber, int numStringsToSend) { + this(subscriber, numStringsToSend, new AtomicInteger()); + } + + @Override + public void run() { + if (running != null) { + running.countDown(); + } + for (int i = 0; i < numStringsToSend; i++) { + subscriber.onNext(Thread.currentThread().getId() + "-" + i); + if (latch != null) { + latch.countDown(); + } + produced.incrementAndGet(); + } + } + } + + /** + * A thread that will call onError or onNext. + */ + public static class CompletionThread implements Runnable { + + private final Subscriber<String> subscriber; + private final TestConcurrencySubscriberEvent event; + private final Future<?>[] waitOnThese; + + CompletionThread(Subscriber<String> Subscriber, TestConcurrencySubscriberEvent event, Future<?>... waitOnThese) { + this.subscriber = Subscriber; + this.event = event; + this.waitOnThese = waitOnThese; + } + + @Override + public void run() { + /* if we have 'waitOnThese' futures, we'll wait on them before proceeding */ + if (waitOnThese != null) { + for (Future<?> f : waitOnThese) { + try { + f.get(); + } catch (Throwable e) { + System.err.println("Error while waiting on future in CompletionThread"); + } + } + } + + /* send the event */ + if (event == TestConcurrencySubscriberEvent.onError) { + subscriber.onError(new RuntimeException("mocked exception")); + } else if (event == TestConcurrencySubscriberEvent.onComplete) { + subscriber.onComplete(); + + } else { + throw new IllegalArgumentException("Expecting either onError or onComplete"); + } + } + } + + enum TestConcurrencySubscriberEvent { + onComplete, onError, onNext + } + + private static class TestConcurrencySubscriber extends DefaultSubscriber<String> { + + /** + * Used to store the order and number of events received. + */ + private final LinkedBlockingQueue<TestConcurrencySubscriberEvent> events = new LinkedBlockingQueue<>(); + private final int waitTime; + + @SuppressWarnings("unused") + TestConcurrencySubscriber(int waitTimeInNext) { + this.waitTime = waitTimeInNext; + } + + TestConcurrencySubscriber() { + this.waitTime = 0; + } + + @Override + public void onComplete() { + events.add(TestConcurrencySubscriberEvent.onComplete); + } + + @Override + public void onError(Throwable e) { + events.add(TestConcurrencySubscriberEvent.onError); + } + + @Override + public void onNext(String args) { + events.add(TestConcurrencySubscriberEvent.onNext); + // do some artificial work to make the thread scheduling/timing vary + int s = 0; + for (int i = 0; i < 20; i++) { + s += s * i; + } + + if (waitTime > 0) { + try { + Thread.sleep(waitTime); + } catch (InterruptedException e) { + // ignore + } + } + } + + /** + * Assert the order of events is correct and return the number of onNext executions. + * + * @param expectedEndingEvent the last event + * @return int count of onNext calls + * @throws IllegalStateException + * If order of events was invalid. + */ + public int assertEvents(TestConcurrencySubscriberEvent expectedEndingEvent) throws IllegalStateException { + int nextCount = 0; + boolean finished = false; + for (TestConcurrencySubscriberEvent e : events) { + if (e == TestConcurrencySubscriberEvent.onNext) { + if (finished) { + // already finished, we shouldn't get this again + throw new IllegalStateException("Received onNext but we're already finished."); + } + nextCount++; + } else if (e == TestConcurrencySubscriberEvent.onError) { + if (finished) { + // already finished, we shouldn't get this again + throw new IllegalStateException("Received onError but we're already finished."); + } + if (expectedEndingEvent != null && TestConcurrencySubscriberEvent.onError != expectedEndingEvent) { + throw new IllegalStateException("Received onError ending event but expected " + expectedEndingEvent); + } + finished = true; + } else if (e == TestConcurrencySubscriberEvent.onComplete) { + if (finished) { + // already finished, we shouldn't get this again + throw new IllegalStateException("Received onComplete but we're already finished."); + } + if (expectedEndingEvent != null && TestConcurrencySubscriberEvent.onComplete != expectedEndingEvent) { + throw new IllegalStateException("Received onComplete ending event but expected " + expectedEndingEvent); + } + finished = true; + } + } + + return nextCount; + } + + } + + /** + * This spawns a single thread for the subscribe execution. + */ + static class TestSingleThreadedPublisher implements Publisher<String> { + + final String[] values; + private Thread t; + + TestSingleThreadedPublisher(final String... values) { + this.values = values; + + } + + @Override + public void subscribe(final Subscriber<? super String> subscriber) { + subscriber.onSubscribe(new BooleanSubscription()); + System.out.println("TestSingleThreadedObservable subscribed to ..."); + t = new Thread(new Runnable() { + + @Override + public void run() { + try { + System.out.println("running TestSingleThreadedObservable thread"); + for (String s : values) { + System.out.println("TestSingleThreadedObservable onNext: " + s); + subscriber.onNext(s); + } + subscriber.onComplete(); + } catch (Throwable e) { + throw new RuntimeException(e); + } + } + + }); + System.out.println("starting TestSingleThreadedObservable thread"); + t.start(); + System.out.println("done starting TestSingleThreadedObservable thread"); + } + + public void waitToFinish() { + try { + t.join(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + + } + + /** + * This spawns a thread for the subscription, then a separate thread for each onNext call. + */ + static class TestMultiThreadedObservable implements Publisher<String> { + + final String[] values; + Thread t; + AtomicInteger threadsRunning = new AtomicInteger(); + AtomicInteger maxConcurrentThreads = new AtomicInteger(); + ExecutorService threadPool; + + TestMultiThreadedObservable(String... values) { + this.values = values; + this.threadPool = Executors.newCachedThreadPool(); + } + + @Override + public void subscribe(final Subscriber<? super String> subscriber) { + subscriber.onSubscribe(new BooleanSubscription()); + final NullPointerException npe = new NullPointerException(); + System.out.println("TestMultiThreadedObservable subscribed to ..."); + t = new Thread(new Runnable() { + + @Override + public void run() { + try { + System.out.println("running TestMultiThreadedObservable thread"); + int j = 0; + for (final String s : values) { + final int fj = ++j; + threadPool.execute(new Runnable() { + + @Override + public void run() { + threadsRunning.incrementAndGet(); + try { + // perform onNext call + System.out.println("TestMultiThreadedObservable onNext: " + s + " on thread " + Thread.currentThread().getName()); + if (s == null) { + // force an error + throw npe; + } else { + // allow the exception to queue up + int sleep = (fj % 3) * 10; + if (sleep != 0) { + Thread.sleep(sleep); + } + } + subscriber.onNext(s); + // capture 'maxThreads' + int concurrentThreads = threadsRunning.get(); + int maxThreads = maxConcurrentThreads.get(); + if (concurrentThreads > maxThreads) { + maxConcurrentThreads.compareAndSet(maxThreads, concurrentThreads); + } + } catch (Throwable e) { + subscriber.onError(e); + } finally { + threadsRunning.decrementAndGet(); + } + } + }); + } + // we are done spawning threads + threadPool.shutdown(); + } catch (Throwable e) { + throw new RuntimeException(e); + } + + // wait until all threads are done, then mark it as COMPLETED + try { + // wait for all the threads to finish + if (!threadPool.awaitTermination(5, TimeUnit.SECONDS)) { + System.out.println("Threadpool did not terminate in time."); + } + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + subscriber.onComplete(); + } + }); + System.out.println("starting TestMultiThreadedObservable thread"); + t.start(); + System.out.println("done starting TestMultiThreadedObservable thread"); + } + + public void waitToFinish() { + try { + t.join(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + } + + private static class BusySubscriber extends DefaultSubscriber<String> { + volatile boolean onComplete; + volatile boolean onError; + AtomicInteger onNextCount = new AtomicInteger(); + AtomicInteger threadsRunning = new AtomicInteger(); + AtomicInteger maxConcurrentThreads = new AtomicInteger(); + final CountDownLatch terminalEvent = new CountDownLatch(1); + + @Override + public void onComplete() { + threadsRunning.incrementAndGet(); + try { + onComplete = true; + } finally { + captureMaxThreads(); + threadsRunning.decrementAndGet(); + terminalEvent.countDown(); + } + } + + @Override + public void onError(Throwable e) { + System.out.println(">>>>>>>>>>>>>>>>>>>> onError received: " + e); + threadsRunning.incrementAndGet(); + try { + onError = true; + } finally { + captureMaxThreads(); + threadsRunning.decrementAndGet(); + terminalEvent.countDown(); + } + } + + @Override + public void onNext(String args) { + threadsRunning.incrementAndGet(); + try { + onNextCount.incrementAndGet(); + try { + // simulate doing something computational + Thread.sleep(100); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } finally { + // capture 'maxThreads' + captureMaxThreads(); + threadsRunning.decrementAndGet(); + } + } + + protected void captureMaxThreads() { + int concurrentThreads = threadsRunning.get(); + int maxThreads = maxConcurrentThreads.get(); + if (concurrentThreads > maxThreads) { + maxConcurrentThreads.compareAndSet(maxThreads, concurrentThreads); + if (concurrentThreads > 1) { + new RuntimeException("should not be greater than 1").printStackTrace(); + } + } + } + + } + + @Test + public void errorReentry() { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + final AtomicReference<Subscriber<Integer>> serial = new AtomicReference<>(); + + TestSubscriber<Integer> ts = new TestSubscriber<Integer>() { + @Override + public void onNext(Integer v) { + serial.get().onError(new TestException()); + serial.get().onError(new TestException()); + super.onNext(v); + } + }; + SerializedSubscriber<Integer> sobs = new SerializedSubscriber<>(ts); + sobs.onSubscribe(new BooleanSubscription()); + serial.set(sobs); + + sobs.onNext(1); + + ts.assertValue(1); + ts.assertError(TestException.class); + + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void completeReentry() { + final AtomicReference<Subscriber<Integer>> serial = new AtomicReference<>(); + + TestSubscriber<Integer> ts = new TestSubscriber<Integer>() { + @Override + public void onNext(Integer v) { + serial.get().onComplete(); + serial.get().onComplete(); + super.onNext(v); + } + }; + SerializedSubscriber<Integer> sobs = new SerializedSubscriber<>(ts); + sobs.onSubscribe(new BooleanSubscription()); + serial.set(sobs); + + sobs.onNext(1); + + ts.assertValue(1); + ts.assertComplete(); + ts.assertNoErrors(); + } + + @Test + public void dispose() { + TestSubscriber<Integer> ts = new TestSubscriber<>(); + + SerializedSubscriber<Integer> so = new SerializedSubscriber<>(ts); + + BooleanSubscription bs = new BooleanSubscription(); + + so.onSubscribe(bs); + + ts.cancel(); + + assertTrue(bs.isCancelled()); + } + + @Test + public void onCompleteRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + TestSubscriber<Integer> ts = new TestSubscriber<>(); + + final SerializedSubscriber<Integer> so = new SerializedSubscriber<>(ts); + + BooleanSubscription bs = new BooleanSubscription(); + + so.onSubscribe(bs); + + Runnable r = new Runnable() { + @Override + public void run() { + so.onComplete(); + } + }; + + TestHelper.race(r, r); + + ts.awaitDone(5, TimeUnit.SECONDS) + .assertResult(); + } + + } + + @Test + public void onNextOnCompleteRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + TestSubscriber<Integer> ts = new TestSubscriber<>(); + + final SerializedSubscriber<Integer> so = new SerializedSubscriber<>(ts); + + BooleanSubscription bs = new BooleanSubscription(); + + so.onSubscribe(bs); + + Runnable r1 = new Runnable() { + @Override + public void run() { + so.onComplete(); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + so.onNext(1); + } + }; + + TestHelper.race(r1, r2); + + ts.awaitDone(5, TimeUnit.SECONDS) + .assertNoErrors() + .assertComplete(); + + assertTrue(ts.values().size() <= 1); + } + + } + + @Test + public void onNextOnErrorRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + TestSubscriber<Integer> ts = new TestSubscriber<>(); + + final SerializedSubscriber<Integer> so = new SerializedSubscriber<>(ts); + + BooleanSubscription bs = new BooleanSubscription(); + + so.onSubscribe(bs); + + final Throwable ex = new TestException(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + so.onError(ex); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + so.onNext(1); + } + }; + + TestHelper.race(r1, r2); + + ts.awaitDone(5, TimeUnit.SECONDS) + .assertError(ex) + .assertNotComplete(); + + assertTrue(ts.values().size() <= 1); + } + + } + + @Test + public void onNextOnErrorRaceDelayError() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + TestSubscriber<Integer> ts = new TestSubscriber<>(); + + final SerializedSubscriber<Integer> so = new SerializedSubscriber<>(ts, true); + + BooleanSubscription bs = new BooleanSubscription(); + + so.onSubscribe(bs); + + final Throwable ex = new TestException(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + so.onError(ex); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + so.onNext(1); + } + }; + + TestHelper.race(r1, r2); + + ts.awaitDone(5, TimeUnit.SECONDS) + .assertError(ex) + .assertNotComplete(); + + assertTrue(ts.values().size() <= 1); + } + + } + + @Test + public void startOnce() { + + List<Throwable> error = TestHelper.trackPluginErrors(); + + try { + TestSubscriber<Integer> ts = new TestSubscriber<>(); + + final SerializedSubscriber<Integer> so = new SerializedSubscriber<>(ts); + + so.onSubscribe(new BooleanSubscription()); + + BooleanSubscription bs = new BooleanSubscription(); + + so.onSubscribe(bs); + + assertTrue(bs.isCancelled()); + + TestHelper.assertError(error, 0, IllegalStateException.class, "Subscription already set!"); + } finally { + RxJavaPlugins.reset(); + } + } + + @Test + public void onCompleteOnErrorRace() { + for (int i = 0; i < TestHelper.RACE_DEFAULT_LOOPS; i++) { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(); + + final SerializedSubscriber<Integer> so = new SerializedSubscriber<>(ts); + + BooleanSubscription bs = new BooleanSubscription(); + + so.onSubscribe(bs); + + final Throwable ex = new TestException(); + + Runnable r1 = new Runnable() { + @Override + public void run() { + so.onError(ex); + } + }; + + Runnable r2 = new Runnable() { + @Override + public void run() { + so.onComplete(); + } + }; + + TestHelper.race(r1, r2); + + ts.awaitDone(5, TimeUnit.SECONDS); + + if (ts.completions() != 0) { + ts.assertResult(); + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } else { + ts.assertFailure(TestException.class).assertError(ex); + assertTrue("" + errors, errors.isEmpty()); + } + } finally { + RxJavaPlugins.reset(); + } + } + + } + + @Test + public void nullOnNext() { + + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(); + + final SerializedSubscriber<Integer> so = new SerializedSubscriber<>(ts); + + so.onSubscribe(new BooleanSubscription()); + + so.onNext(null); + + ts.assertFailureAndMessage(NullPointerException.class, ExceptionHelper.nullWarning("onNext called with a null value.")); + } + + @Test + @SuppressUndeliverable + public void onErrorQueuedUp() { + AtomicReference<SerializedSubscriber<Integer>> ssRef = new AtomicReference<>(); + TestSubscriberEx<Integer> ts = new TestSubscriberEx<Integer>() { + @Override + public void onNext(Integer t) { + super.onNext(t); + ssRef.get().onNext(2); + ssRef.get().onError(new TestException()); + } + }; + + final SerializedSubscriber<Integer> so = new SerializedSubscriber<>(ts, true); + ssRef.set(so); + + BooleanSubscription bs = new BooleanSubscription(); + + so.onSubscribe(bs); + + so.onNext(1); + + ts.assertFailure(TestException.class, 1, 2); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/subscribers/TestSubscriberTest.java b/src/test/java/io/reactivex/rxjava3/subscribers/TestSubscriberTest.java new file mode 100644 index 0000000000..40bdcb95cc --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/subscribers/TestSubscriberTest.java @@ -0,0 +1,1742 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.subscribers; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +import java.io.IOException; +import java.util.*; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.junit.Test; +import org.junit.function.ThrowingRunnable; +import org.mockito.InOrder; +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.core.Scheduler.Worker; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.internal.subscriptions.*; +import io.reactivex.rxjava3.processors.PublishProcessor; +import io.reactivex.rxjava3.schedulers.Schedulers; +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class TestSubscriberTest extends RxJavaTest { + + @Test + public void assertTestSubscriber() { + Flowable<Integer> oi = Flowable.fromIterable(Arrays.asList(1, 2)); + TestSubscriber<Integer> ts = new TestSubscriber<>(); + oi.subscribe(ts); + + ts.assertValues(1, 2); + ts.assertValueCount(2); + ts.assertComplete(); + ts.assertNoErrors(); + } + + @Test + public void assertNotMatchCount() { + assertThrows(AssertionError.class, () -> { + Flowable<Integer> oi = Flowable.fromIterable(Arrays.asList(1, 2)); + TestSubscriber<Integer> ts = new TestSubscriber<>(); + oi.subscribe(ts); + + ts.assertValues(1); + ts.assertValueCount(2); + ts.assertComplete(); + ts.assertNoErrors(); + }); + } + + @Test + public void assertNotMatchValue() { + assertThrows(AssertionError.class, () -> { + Flowable<Integer> oi = Flowable.fromIterable(Arrays.asList(1, 2)); + TestSubscriber<Integer> ts = new TestSubscriber<>(); + oi.subscribe(ts); + + ts.assertValues(1, 3); + ts.assertValueCount(2); + ts.assertComplete(); + ts.assertNoErrors(); + }); + } + + @Test + public void assertTerminalEventNotReceived() { + assertThrows(AssertionError.class, () -> { + PublishProcessor<Integer> p = PublishProcessor.create(); + TestSubscriber<Integer> ts = new TestSubscriber<>(); + p.subscribe(ts); + + p.onNext(1); + p.onNext(2); + + ts.assertValues(1, 2); + ts.assertValueCount(2); + ts.assertComplete(); + ts.assertNoErrors(); + }); + } + + @Test + public void wrappingMock() { + Flowable<Integer> oi = Flowable.fromIterable(Arrays.asList(1, 2)); + Subscriber<Integer> mockSubscriber = TestHelper.mockSubscriber(); + + oi.subscribe(new TestSubscriber<>(mockSubscriber)); + + InOrder inOrder = inOrder(mockSubscriber); + inOrder.verify(mockSubscriber, times(1)).onNext(1); + inOrder.verify(mockSubscriber, times(1)).onNext(2); + inOrder.verify(mockSubscriber, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void wrappingMockWhenUnsubscribeInvolved() { + Flowable<Integer> oi = Flowable.fromIterable(Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9)).take(2); + Subscriber<Integer> mockSubscriber = TestHelper.mockSubscriber(); + oi.subscribe(new TestSubscriber<>(mockSubscriber)); + + InOrder inOrder = inOrder(mockSubscriber); + inOrder.verify(mockSubscriber, times(1)).onNext(1); + inOrder.verify(mockSubscriber, times(1)).onNext(2); + inOrder.verify(mockSubscriber, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void assertError() { + RuntimeException e = new RuntimeException("Oops"); + TestSubscriber<Object> subscriber = new TestSubscriber<>(); + Flowable.error(e).subscribe(subscriber); + subscriber.assertError(e); + } + + @Test + public void awaitTerminalEventWithDuration() { + TestSubscriber<Object> ts = new TestSubscriber<>(); + Flowable.just(1).subscribe(ts); + ts.awaitDone(1, TimeUnit.SECONDS); + ts.assertComplete(); + ts.assertNoErrors(); + } + + @Test + public void awaitTerminalEventWithDurationAndUnsubscribeOnTimeout() { + TestSubscriber<Object> ts = new TestSubscriber<>(); + final AtomicBoolean unsub = new AtomicBoolean(false); + Flowable.just(1) + // + .doOnCancel(new Action() { + @Override + public void run() { + unsub.set(true); + } + }) + // + .delay(1000, TimeUnit.MILLISECONDS).subscribe(ts); + ts.awaitDone(100, TimeUnit.MILLISECONDS); + ts.dispose(); + assertTrue(unsub.get()); + } + + @Test(expected = NullPointerException.class) + public void nullDelegate1() { + TestSubscriber<Integer> ts = new TestSubscriber<>(null); + ts.onComplete(); + } + + @Test(expected = NullPointerException.class) + public void nullDelegate2() { + TestSubscriber<Integer> ts = new TestSubscriber<>(null); + ts.onComplete(); + } + + @Test(expected = NullPointerException.class) + public void nullDelegate3() { + TestSubscriber<Integer> ts = new TestSubscriber<>(null, 0L); + ts.onComplete(); + } + + @Test + public void delegate1() { + TestSubscriber<Integer> ts0 = new TestSubscriber<>(); + ts0.onSubscribe(EmptySubscription.INSTANCE); + + TestSubscriber<Integer> ts = new TestSubscriber<>(ts0); + ts.onComplete(); + + ts0.assertComplete(); + ts0.assertNoErrors(); + } + + @Test + public void delegate2() { + TestSubscriber<Integer> ts1 = new TestSubscriber<>(); + TestSubscriber<Integer> ts2 = new TestSubscriber<>(ts1); + ts2.onComplete(); + + ts1.assertComplete(); + } + + @Test + public void delegate3() { + TestSubscriber<Integer> ts1 = new TestSubscriber<>(); + TestSubscriber<Integer> ts2 = new TestSubscriber<>(ts1, 0L); + ts2.onComplete(); + ts1.assertComplete(); + } + + @Test + public void unsubscribed() { + TestSubscriber<Integer> ts = new TestSubscriber<>(); + assertFalse(ts.isCancelled()); + } + + @Test + public void noErrors() { + TestSubscriber<Integer> ts = new TestSubscriber<>(); + ts.onError(new TestException()); + try { + ts.assertNoErrors(); + } catch (AssertionError ex) { + // expected + return; + } + fail("Error present but no assertion error!"); + } + + @Test + public void notCompleted() { + TestSubscriber<Integer> ts = new TestSubscriber<>(); + try { + ts.assertComplete(); + } catch (AssertionError ex) { + // expected + return; + } + fail("Not completed and no assertion error!"); + } + + @Test + public void multipleCompletions() { + TestSubscriber<Integer> ts = new TestSubscriber<>(); + ts.onComplete(); + ts.onComplete(); + try { + ts.assertComplete(); + } catch (AssertionError ex) { + // expected + return; + } + fail("Multiple completions and no assertion error!"); + } + + @Test + public void completed() { + TestSubscriber<Integer> ts = new TestSubscriber<>(); + ts.onComplete(); + try { + ts.assertNotComplete(); + } catch (AssertionError ex) { + // expected + return; + } + fail("Completed and no assertion error!"); + } + + @Test + public void multipleCompletions2() { + TestSubscriber<Integer> ts = new TestSubscriber<>(); + ts.onComplete(); + ts.onComplete(); + try { + ts.assertNotComplete(); + } catch (AssertionError ex) { + // expected + return; + } + fail("Multiple completions and no assertion error!"); + } + + @Test + public void multipleErrors() { + TestSubscriber<Integer> ts = new TestSubscriber<>(); + ts.onSubscribe(EmptySubscription.INSTANCE); + ts.onError(new TestException()); + ts.onError(new TestException()); + try { + ts.assertNoErrors(); + } catch (AssertionError ex) { + Throwable e = ex.getCause(); + if (!(e instanceof CompositeException)) { + fail("Multiple Error present but the reported error doesn't have a composite cause!"); + } + CompositeException ce = (CompositeException)e; + if (ce.size() != 2) { + ce.printStackTrace(); + } + assertEquals(2, ce.size()); + // expected + return; + } + fail("Multiple Error present but no assertion error!"); + } + + @Test + public void multipleErrors2() { + TestSubscriber<Integer> ts = new TestSubscriber<>(); + ts.onSubscribe(EmptySubscription.INSTANCE); + ts.onError(new TestException()); + ts.onError(new TestException()); + try { + ts.assertError(TestException.class); + } catch (AssertionError ex) { + Throwable e = ex.getCause(); + if (!(e instanceof CompositeException)) { + fail("Multiple Error present but the reported error doesn't have a composite cause!"); + } + CompositeException ce = (CompositeException)e; + assertEquals(2, ce.size()); + // expected + return; + } + fail("Multiple Error present but no assertion error!"); + } + + @Test + public void multipleErrors3() { + TestSubscriber<Integer> ts = new TestSubscriber<>(); + ts.onSubscribe(EmptySubscription.INSTANCE); + ts.onError(new TestException()); + ts.onError(new TestException()); + try { + ts.assertError(new TestException()); + } catch (AssertionError ex) { + Throwable e = ex.getCause(); + if (!(e instanceof CompositeException)) { + fail("Multiple Error present but the reported error doesn't have a composite cause!"); + } + CompositeException ce = (CompositeException)e; + assertEquals(2, ce.size()); + // expected + return; + } + fail("Multiple Error present but no assertion error!"); + } + + @Test + public void multipleErrors4() { + TestSubscriber<Integer> ts = new TestSubscriber<>(); + ts.onSubscribe(EmptySubscription.INSTANCE); + ts.onError(new TestException()); + ts.onError(new TestException()); + try { + ts.assertError(Functions.<Throwable>alwaysTrue()); + } catch (AssertionError ex) { + Throwable e = ex.getCause(); + if (!(e instanceof CompositeException)) { + fail("Multiple Error present but the reported error doesn't have a composite cause!"); + } + CompositeException ce = (CompositeException)e; + assertEquals(2, ce.size()); + // expected + return; + } + fail("Multiple Error present but no assertion error!"); + } + + @Test + public void differentError() { + TestSubscriber<Integer> ts = new TestSubscriber<>(); + ts.onError(new TestException()); + try { + ts.assertError(new TestException()); + } catch (AssertionError ex) { + // expected + return; + } + fail("Different Error present but no assertion error!"); + } + + @Test + public void differentError2() { + TestSubscriber<Integer> ts = new TestSubscriber<>(); + ts.onError(new RuntimeException()); + try { + ts.assertError(new TestException()); + } catch (AssertionError ex) { + // expected + return; + } + fail("Different Error present but no assertion error!"); + } + + @Test + public void differentError3() { + TestSubscriber<Integer> ts = new TestSubscriber<>(); + ts.onError(new RuntimeException()); + try { + ts.assertError(TestException.class); + } + catch (AssertionError ex) { + // expected + return; + } + fail("Different Error present but no assertion error!"); + } + + @Test + public void differentError4() { + TestSubscriber<Integer> ts = new TestSubscriber<>(); + ts.onError(new RuntimeException()); + try { + ts.assertError(Functions.<Throwable>alwaysFalse()); + } catch (AssertionError ex) { + // expected + return; + } + fail("Different Error present but no assertion error!"); + } + + @Test + public void errorInPredicate() { + TestSubscriber<Integer> ts = new TestSubscriber<>(); + ts.onError(new RuntimeException()); + try { + ts.assertError(new Predicate<Throwable>() { + @Override + public boolean test(Throwable throwable) throws Exception { + throw new TestException(); + } + }); + } catch (TestException ex) { + // expected + return; + } + fail("Error in predicate but not thrown!"); + } + + @Test + public void noError() { + TestSubscriber<Integer> ts = new TestSubscriber<>(); + try { + ts.assertError(TestException.class); + } catch (AssertionError ex) { + // expected + return; + } + fail("No present but no assertion error!"); + } + + @Test + public void noError2() { + TestSubscriber<Integer> ts = new TestSubscriber<>(); + try { + ts.assertError(new TestException()); + } catch (AssertionError ex) { + // expected + return; + } + fail("No present but no assertion error!"); + } + + @Test + public void noError3() { + TestSubscriber<Integer> ts = new TestSubscriber<>(); + try { + ts.assertError(Functions.<Throwable>alwaysTrue()); + } catch (AssertionError ex) { + // expected + return; + } + fail("No present but no assertion error!"); + } + + @Test + public void interruptTerminalEventAwait() { + TestSubscriber<Integer> ts = new TestSubscriber<>(); + + final Thread t0 = Thread.currentThread(); + Worker w = Schedulers.computation().createWorker(); + try { + w.schedule(new Runnable() { + @Override + public void run() { + t0.interrupt(); + } + }, 200, TimeUnit.MILLISECONDS); + + try { + if (ts.await(5, TimeUnit.SECONDS)) { + fail("Did not interrupt wait!"); + } + } catch (InterruptedException expected) { + // expected + } + } finally { + w.dispose(); + } + } + + @Test + public void interruptTerminalEventAwaitTimed() { + TestSubscriber<Integer> ts = new TestSubscriber<>(); + + final Thread t0 = Thread.currentThread(); + Worker w = Schedulers.computation().createWorker(); + try { + w.schedule(new Runnable() { + @Override + public void run() { + t0.interrupt(); + } + }, 200, TimeUnit.MILLISECONDS); + + try { + if (ts.await(5, TimeUnit.SECONDS)) { + fail("Did not interrupt wait!"); + } + } catch (InterruptedException expected) { + // expected + } + } finally { + Thread.interrupted(); // clear interrupted flag + w.dispose(); + } + } + + @Test + public void interruptTerminalEventAwaitAndUnsubscribe() { + TestSubscriber<Integer> ts = new TestSubscriber<>(); + + final Thread t0 = Thread.currentThread(); + Worker w = Schedulers.computation().createWorker(); + try { + w.schedule(new Runnable() { + @Override + public void run() { + t0.interrupt(); + } + }, 200, TimeUnit.MILLISECONDS); + + try { + ts.awaitDone(5, TimeUnit.SECONDS); + } catch (RuntimeException allowed) { + assertTrue(allowed.toString(), allowed.getCause() instanceof InterruptedException); + } + ts.dispose(); + if (!ts.isCancelled()) { + fail("Did not unsubscribe!"); + } + } finally { + w.dispose(); + } + } + + @Test + public void noTerminalEventBut1Completed() { + TestSubscriber<Integer> ts = new TestSubscriber<>(); + + ts.onComplete(); + + try { + ts.assertNotComplete(); + throw new RuntimeException("Failed to report there were terminal event(s)!"); + } catch (AssertionError ex) { + // expected + } + } + + @Test + public void noTerminalEventBut1Error() { + TestSubscriber<Integer> ts = new TestSubscriber<>(); + + ts.onError(new TestException()); + + try { + ts.assertNoErrors(); + throw new RuntimeException("Failed to report there were terminal event(s)!"); + } catch (AssertionError ex) { + // expected + } + } + + @Test + public void noTerminalEventBut1Error1Complete() { + TestSubscriber<Integer> ts = new TestSubscriber<>(); + + ts.onComplete(); + ts.onError(new TestException()); + + try { + ts.assertNotComplete(); + throw new RuntimeException("Failed to report there were terminal event(s)!"); + } catch (AssertionError ex) { + // expected + } + + try { + ts.assertNoErrors(); + throw new RuntimeException("Failed to report there were terminal event(s)!"); + } catch (AssertionError ex) { + // expected + } + } + + @Test + public void noTerminalEventBut2Errors() { + TestSubscriber<Integer> ts = new TestSubscriber<>(); + ts.onSubscribe(EmptySubscription.INSTANCE); + + ts.onError(new TestException()); + ts.onError(new TestException()); + + try { + ts.assertNoErrors(); + throw new RuntimeException("Failed to report there were terminal event(s)!"); + } catch (AssertionError ex) { + // expected + Throwable e = ex.getCause(); + if (!(e instanceof CompositeException)) { + fail("Multiple Error present but the reported error doesn't have a composite cause!"); + } + CompositeException ce = (CompositeException)e; + assertEquals(2, ce.size()); + } + } + + @Test + public void noValues() { + TestSubscriber<Integer> ts = new TestSubscriber<>(); + ts.onNext(1); + + try { + ts.assertNoValues(); + throw new RuntimeException("Failed to report there were values!"); + } catch (AssertionError ex) { + // expected + } + } + + @Test + public void valueCount() { + TestSubscriber<Integer> ts = new TestSubscriber<>(); + ts.onNext(1); + ts.onNext(2); + + try { + ts.assertValueCount(3); + throw new RuntimeException("Failed to report there were values!"); + } catch (AssertionError ex) { + // expected + } + } + + @Test + public void onCompletedCrashCountsDownLatch() { + TestSubscriber<Integer> ts0 = new TestSubscriber<Integer>() { + @Override + public void onComplete() { + throw new TestException(); + } + }; + TestSubscriber<Integer> ts = new TestSubscriber<>(ts0); + + try { + ts.onComplete(); + } catch (TestException ex) { + // expected + } + + ts.awaitDone(5, TimeUnit.SECONDS); + } + + @Test + public void onErrorCrashCountsDownLatch() { + TestSubscriber<Integer> ts0 = new TestSubscriber<Integer>() { + @Override + public void onError(Throwable e) { + throw new TestException(); + } + }; + TestSubscriber<Integer> ts = new TestSubscriber<>(ts0); + + try { + ts.onError(new RuntimeException()); + } catch (TestException ex) { + // expected + } + + ts.awaitDone(5, TimeUnit.SECONDS); + } + + @Test + public void createDelegate() { + TestSubscriber<Integer> ts1 = TestSubscriber.create(); + + TestSubscriber<Integer> ts = TestSubscriber.create(ts1); + + assertFalse(ts.hasSubscription()); + + ts.onSubscribe(new BooleanSubscription()); + + assertTrue(ts.hasSubscription()); + + assertFalse(ts.isDisposed()); + + ts.onNext(1); + ts.onError(new TestException()); + ts.onComplete(); + + ts1.assertValue(1).assertError(TestException.class).assertComplete(); + + ts.dispose(); + + assertTrue(ts.isDisposed()); + + try { + ts.assertNoValues(); + throw new RuntimeException("Should have thrown"); + } catch (AssertionError exc) { + // expected + } + + try { + ts.assertValueCount(0); + throw new RuntimeException("Should have thrown"); + } catch (AssertionError exc) { + // expected + } + + ts.assertValueSequence(Collections.singletonList(1)); + + try { + ts.assertValueSequence(Collections.singletonList(2)); + throw new RuntimeException("Should have thrown"); + } catch (AssertionError exc) { + // expected + } + } + + @Test + public void assertError2() { + TestSubscriber<Integer> ts = TestSubscriber.create(); + + try { + ts.assertError(TestException.class); + throw new RuntimeException("Should have thrown"); + } catch (AssertionError ex) { + // expected + } + + try { + ts.assertError(new TestException()); + throw new RuntimeException("Should have thrown"); + } catch (AssertionError ex) { + // expected + } + + try { + ts.assertError(Functions.<Throwable>alwaysTrue()); + throw new RuntimeException("Should have thrown"); + } catch (AssertionError ex) { + // expected + } + + try { + ts.assertSubscribed(); + throw new RuntimeException("Should have thrown"); + } catch (AssertionError exc) { + // expected + } + + ts.onSubscribe(new BooleanSubscription()); + + ts.assertSubscribed(); + + ts.assertNoErrors(); + + TestException ex = new TestException("Forced failure"); + + ts.onError(ex); + + ts.assertError(ex); + + ts.assertError(TestException.class); + + ts.assertError(Functions.<Throwable>alwaysTrue()); + + ts.assertError(new Predicate<Throwable>() { + @Override + public boolean test(Throwable t) { + return t.getMessage() != null && t.getMessage().contains("Forced"); + } + }); + + try { + ts.assertError(new RuntimeException()); + throw new RuntimeException("Should have thrown"); + } catch (AssertionError exc) { + // expected + } + + try { + ts.assertError(IOException.class); + throw new RuntimeException("Should have thrown"); + } catch (AssertionError exc) { + // expected + } + + try { + ts.assertNoErrors(); + throw new RuntimeException("Should have thrown"); + } catch (AssertionError exc) { + // expected + } + + try { + ts.assertError(Functions.<Throwable>alwaysFalse()); + throw new RuntimeException("Should have thrown"); + } catch (AssertionError exc) { + // expected + } + + ts.assertValueCount(0); + + ts.assertNoValues(); + } + + @Test + public void emptyObserverEnum() { + assertEquals(1, TestSubscriber.EmptySubscriber.values().length); + assertNotNull(TestSubscriber.EmptySubscriber.valueOf("INSTANCE")); + } + + @Test + public void valueAndClass() { + assertEquals("null", TestSubscriber.valueAndClass(null)); + assertEquals("1 (class: Integer)", TestSubscriber.valueAndClass(1)); + } + + @Test + public void assertFailure() { + TestSubscriber<Integer> ts = TestSubscriber.create(); + + ts.onSubscribe(new BooleanSubscription()); + + ts.onError(new TestException("Forced failure")); + + ts.assertFailure(TestException.class); + + ts.onNext(1); + + ts.assertFailure(TestException.class, 1); + } + + @Test + public void assertResult() { + TestSubscriber<Integer> ts = TestSubscriber.create(); + + ts.onSubscribe(new BooleanSubscription()); + + ts.onComplete(); + + ts.assertResult(); + + try { + ts.assertResult(1); + throw new RuntimeException("Should have thrown"); + } catch (AssertionError ex) { + // expected + } + + ts.onNext(1); + + ts.assertResult(1); + + try { + ts.assertResult(2); + throw new RuntimeException("Should have thrown"); + } catch (AssertionError ex) { + // expected + } + + try { + ts.assertResult(); + throw new RuntimeException("Should have thrown"); + } catch (AssertionError ex) { + // expected + } + + } + + @Test + public void await() throws Exception { + TestSubscriber<Integer> ts = TestSubscriber.create(); + + ts.onSubscribe(new BooleanSubscription()); + + assertFalse(ts.await(100, TimeUnit.MILLISECONDS)); + + ts.awaitDone(100, TimeUnit.MILLISECONDS); + + assertTrue(ts.isDisposed()); + + assertFalse(ts.await(100, TimeUnit.MILLISECONDS)); + + ts.assertNoErrors(); + ts.assertNotComplete(); + + ts.onComplete(); + + assertTrue(ts.await(100, TimeUnit.MILLISECONDS)); + + ts.await(); + + ts.awaitDone(5, TimeUnit.SECONDS); + + ts.assertComplete(); + ts.assertNoErrors(); + + assertTrue(ts.await(5, TimeUnit.SECONDS)); + + final TestSubscriber<Integer> ts1 = TestSubscriber.create(); + + ts1.onSubscribe(new BooleanSubscription()); + + Schedulers.single().scheduleDirect(new Runnable() { + @Override + public void run() { + ts1.onComplete(); + } + }, 200, TimeUnit.MILLISECONDS); + + ts1.await(); + } + + @Test + public void onNext() { + TestSubscriber<Integer> ts = TestSubscriber.create(); + + ts.onSubscribe(new BooleanSubscription()); + + ts.assertNoValues(); + + assertEquals(Collections.emptyList(), ts.values()); + + ts.onNext(1); + + assertEquals(Collections.singletonList(1), ts.values()); + + ts.cancel(); + + assertTrue(ts.isCancelled()); + assertTrue(ts.isDisposed()); + + ts.assertValue(1); + + ts.onComplete(); + } + + @Test + public void multipleTerminals() { + TestSubscriber<Integer> ts = TestSubscriber.create(); + + ts.onSubscribe(new BooleanSubscription()); + + ts.assertNotComplete(); + + ts.onComplete(); + + try { + ts.assertNotComplete(); + throw new RuntimeException("Should have thrown"); + } catch (Throwable ex) { + // expected + } + + ts.assertComplete(); + + ts.onComplete(); + + try { + ts.assertComplete(); + throw new RuntimeException("Should have thrown"); + } catch (Throwable ex) { + // expected + } + + try { + ts.assertComplete(); + throw new RuntimeException("Should have thrown"); + } catch (Throwable ex) { + // expected + } + + try { + ts.assertNotComplete(); + throw new RuntimeException("Should have thrown"); + } catch (Throwable ex) { + // expected + } + } + + @Test + public void assertValue() { + TestSubscriber<Integer> ts = TestSubscriber.create(); + + ts.onSubscribe(new BooleanSubscription()); + + try { + ts.assertValue(1); + throw new RuntimeException("Should have thrown"); + } catch (Throwable ex) { + // expected + } + + ts.onNext(1); + + ts.assertValue(1); + + try { + ts.assertValue(2); + throw new RuntimeException("Should have thrown"); + } catch (Throwable ex) { + // expected + } + + ts.onNext(2); + + try { + ts.assertValue(1); + throw new RuntimeException("Should have thrown"); + } catch (Throwable ex) { + // expected + } + } + + @Test + public void onNextMisbehave() { + TestSubscriber<Integer> ts = TestSubscriber.create(); + + ts.onNext(1); + + ts.assertError(IllegalStateException.class); + + ts = TestSubscriber.create(); + + ts.onSubscribe(new BooleanSubscription()); + + ts.onNext(null); + + ts.assertFailure(NullPointerException.class, (Integer)null); + } + + @Test + public void awaitTerminalEventInterrupt() { + final TestSubscriber<Integer> ts = TestSubscriber.create(); + + ts.onSubscribe(new BooleanSubscription()); + + Thread.currentThread().interrupt(); + + try { + ts.awaitDone(5, TimeUnit.SECONDS); + } catch (RuntimeException allowed) { + assertTrue(allowed.toString(), allowed.getCause() instanceof InterruptedException); + } + + // FIXME ? catch consumes this flag + // assertTrue(Thread.interrupted()); + + Thread.currentThread().interrupt(); + + try { + ts.awaitDone(5, TimeUnit.SECONDS); + } catch (RuntimeException allowed) { + assertTrue(allowed.toString(), allowed.getCause() instanceof InterruptedException); + } + + // FIXME ? catch consumes this flag + // assertTrue(Thread.interrupted()); + } + + @Test + public void assertTerminated2() { + TestSubscriber<Integer> ts = TestSubscriber.create(); + + ts.onSubscribe(new BooleanSubscription()); + + ts.assertNotComplete().assertNoErrors(); + + ts.onError(new TestException()); + + ts.assertError(TestException.class); + + ts.onError(new IOException()); + + try { + ts.assertNoErrors(); + throw new RuntimeException("Should have thrown"); + } catch (AssertionError ex) { + // expected + } + + try { + ts.assertError(TestException.class); + throw new RuntimeException("Should have thrown"); + } catch (AssertionError ex) { + // expected + } + + ts = TestSubscriber.create(); + + ts.onSubscribe(new BooleanSubscription()); + + ts.onError(new TestException()); + ts.onComplete(); + + try { + ts.assertComplete() + .assertNoErrors(); + throw new RuntimeException("Should have thrown"); + } catch (AssertionError ex) { + // expected + } + + try { + ts.assertError(Throwable.class) + .assertNotComplete(); + throw new RuntimeException("Should have thrown"); + } catch (AssertionError ex) { + // expected + } + } + + @Test + public void onSubscribe() { + TestSubscriber<Integer> ts = TestSubscriber.create(); + + ts.onSubscribe(null); + + ts.assertError(NullPointerException.class); + + ts = TestSubscriber.create(); + + ts.onSubscribe(new BooleanSubscription()); + + BooleanSubscription bs1 = new BooleanSubscription(); + + ts.onSubscribe(bs1); + + assertTrue(bs1.isCancelled()); + + ts.assertError(IllegalStateException.class); + + ts = TestSubscriber.create(); + ts.dispose(); + + bs1 = new BooleanSubscription(); + + ts.onSubscribe(bs1); + + assertTrue(bs1.isCancelled()); + + } + + @Test + public void assertValueSequence() { + TestSubscriber<Integer> ts = TestSubscriber.create(); + + ts.onSubscribe(new BooleanSubscription()); + + ts.onNext(1); + ts.onNext(2); + + try { + ts.assertValueSequence(Collections.<Integer>emptyList()); + throw new RuntimeException("Should have thrown"); + } catch (AssertionError ex) { + // expected + } + + try { + ts.assertValueSequence(Collections.singletonList(1)); + throw new RuntimeException("Should have thrown"); + } catch (AssertionError ex) { + // expected + } + + ts.assertValueSequence(Arrays.asList(1, 2)); + + try { + ts.assertValueSequence(Arrays.asList(1, 2, 3)); + throw new RuntimeException("Should have thrown"); + } catch (AssertionError ex) { + // expected + } + } + + @Test + public void assertEmpty() { + TestSubscriber<Integer> ts = new TestSubscriber<>(); + + try { + ts.assertEmpty(); + throw new RuntimeException("Should have thrown!"); + } catch (AssertionError ex) { + // expected + } + + ts.onSubscribe(new BooleanSubscription()); + + ts.assertEmpty(); + + ts.onNext(1); + + try { + ts.assertEmpty(); + throw new RuntimeException("Should have thrown!"); + } catch (AssertionError ex) { + // expected + } + } + + @Test + public void awaitDoneTimed() { + TestSubscriber<Integer> ts = new TestSubscriber<>(); + + Thread.currentThread().interrupt(); + + try { + ts.awaitDone(5, TimeUnit.SECONDS); + } catch (RuntimeException ex) { + assertTrue(ex.toString(), ex.getCause() instanceof InterruptedException); + } + } + + @Test + public void assertErrorMultiple() { + TestSubscriber<Integer> ts = new TestSubscriber<>(); + + TestException e = new TestException(); + ts.onError(e); + ts.onError(new TestException()); + + try { + ts.assertError(TestException.class); + throw new RuntimeException("Should have thrown!"); + } catch (AssertionError ex) { + // expected + } + try { + ts.assertError(e); + throw new RuntimeException("Should have thrown!"); + } catch (AssertionError ex) { + // expected + } + } + + @Test + public void assertComplete() { + TestSubscriber<Integer> ts = new TestSubscriber<>(); + + ts.onSubscribe(new BooleanSubscription()); + + try { + ts.assertComplete(); + throw new RuntimeException("Should have thrown!"); + } catch (AssertionError ex) { + // expected + } + + ts.onComplete(); + + ts.assertComplete(); + + ts.onComplete(); + + try { + ts.assertComplete(); + throw new RuntimeException("Should have thrown!"); + } catch (AssertionError ex) { + // expected + } + } + + @Test + public void completeWithoutOnSubscribe() { + TestSubscriber<Integer> ts = new TestSubscriber<>(); + + ts.onComplete(); + + ts.assertError(IllegalStateException.class); + } + + @Test + public void completeDelegateThrows() throws Exception { + TestSubscriber<Integer> ts = new TestSubscriber<>(new FlowableSubscriber<Integer>() { + + @Override + public void onSubscribe(Subscription s) { + + } + + @Override + public void onNext(Integer value) { + + } + + @Override + public void onError(Throwable e) { + throw new TestException(); + } + + @Override + public void onComplete() { + throw new TestException(); + } + + }); + + ts.onSubscribe(new BooleanSubscription()); + + try { + ts.onComplete(); + throw new RuntimeException("Should have thrown!"); + } catch (TestException ex) { + assertTrue(ts.await(1, TimeUnit.SECONDS)); + } + } + + @Test + public void errorDelegateThrows() throws Exception { + TestSubscriber<Integer> ts = new TestSubscriber<>(new FlowableSubscriber<Integer>() { + + @Override + public void onSubscribe(Subscription s) { + + } + + @Override + public void onNext(Integer value) { + + } + + @Override + public void onError(Throwable e) { + throw new TestException(); + } + + @Override + public void onComplete() { + throw new TestException(); + } + + }); + + ts.onSubscribe(new BooleanSubscription()); + + try { + ts.onError(new IOException()); + throw new RuntimeException("Should have thrown!"); + } catch (TestException ex) { + assertTrue(ts.await(1, TimeUnit.SECONDS)); + } + } + + @Test + public void assertValuePredicateEmpty() { + assertThrows(AssertionError.class, () -> { + TestSubscriber<Object> ts = new TestSubscriber<>(); + + Flowable.empty().subscribe(ts); + + ts.assertValue(new Predicate<Object>() { + @Override public boolean test(final Object o) throws Exception { + return false; + } + }); + }); + } + + @Test + public void assertValuePredicateMatch() { + TestSubscriber<Integer> ts = new TestSubscriber<>(); + + Flowable.just(1).subscribe(ts); + + ts.assertValue(new Predicate<Integer>() { + @Override public boolean test(final Integer o) throws Exception { + return o == 1; + } + }); + } + + static void assertThrowsWithMessage(String message, Class<? extends Throwable> clazz, ThrowingRunnable run) { + assertEquals(message, assertThrows(clazz, run).getMessage()); + } + + @Test + public void assertValuePredicateNoMatch() { + assertThrowsWithMessage("Value 1 (class: Integer) at position 0 did not pass the predicate (latch = 0, values = 1, errors = 0, completions = 1)", AssertionError.class, () -> { + TestSubscriber<Integer> ts = new TestSubscriber<>(); + + Flowable.just(1).subscribe(ts); + + ts.assertValue(new Predicate<Integer>() { + @Override public boolean test(final Integer o) throws Exception { + return o != 1; + } + }); + }); + } + + @Test + public void assertValuePredicateMatchButMore() { + assertThrowsWithMessage("The first value passed the predicate but this consumer received more than one value (latch = 0, values = 2, errors = 0, completions = 1)", AssertionError.class, () -> { + TestSubscriber<Integer> ts = new TestSubscriber<>(); + + Flowable.just(1, 2).subscribe(ts); + + ts.assertValue(new Predicate<Integer>() { + @Override public boolean test(final Integer o) throws Exception { + return o == 1; + } + }); + }); + } + + @Test + public void assertValueAtPredicateEmpty() { + assertThrowsWithMessage("No values (latch = 0, values = 0, errors = 0, completions = 1)", AssertionError.class, () -> { + TestSubscriber<Object> ts = new TestSubscriber<>(); + + Flowable.empty().subscribe(ts); + + ts.assertValueAt(0, new Predicate<Object>() { + @Override public boolean test(final Object o) throws Exception { + return false; + } + }); + }); + } + + @Test + public void assertValueAtPredicateMatch() { + TestSubscriber<Integer> ts = new TestSubscriber<>(); + + Flowable.just(1, 2).subscribe(ts); + + ts.assertValueAt(1, new Predicate<Integer>() { + @Override public boolean test(final Integer o) throws Exception { + return o == 2; + } + }); + } + + @Test + public void assertValueAtPredicateNoMatch() { + assertThrowsWithMessage("Value 3 (class: Integer) at position 2 did not pass the predicate (latch = 0, values = 3, errors = 0, completions = 1)", AssertionError.class, () -> { + TestSubscriber<Integer> ts = new TestSubscriber<>(); + + Flowable.just(1, 2, 3).subscribe(ts); + + ts.assertValueAt(2, new Predicate<Integer>() { + @Override public boolean test(final Integer o) throws Exception { + return o != 3; + } + }); + }); + } + + @Test + public void assertValueAtInvalidIndex() { + assertThrowsWithMessage("Index 2 is out of range [0, 2) (latch = 0, values = 2, errors = 0, completions = 1)", AssertionError.class, () -> { + TestSubscriber<Integer> ts = new TestSubscriber<>(); + + Flowable.just(1, 2).subscribe(ts); + + ts.assertValueAt(2, new Predicate<Integer>() { + @Override public boolean test(final Integer o) throws Exception { + return o == 1; + } + }); + }); + } + + @Test + public void assertValueAtIndexInvalidIndex() { + assertThrowsWithMessage("Index 2 is out of range [0, 2) (latch = 0, values = 2, errors = 0, completions = 1)", AssertionError.class, () -> { + TestSubscriber<Integer> ts = new TestSubscriber<>(); + + Flowable.just(1, 2).subscribe(ts); + + ts.assertValueAt(2, 3); + }); + } + + @Test + public void assertValueAtIndexInvalidIndexNegative() { + assertThrowsWithMessage("Index -2 is out of range [0, 2) (latch = 0, values = 2, errors = 0, completions = 1)", AssertionError.class, () -> { + TestSubscriber<Integer> ts = new TestSubscriber<>(); + + Flowable.just(1, 2).subscribe(ts); + + ts.assertValueAt(-2, 3); + }); + } + + @Test + public void assertValueAtInvalidIndexNegative() { + assertThrowsWithMessage("Index -2 is out of range [0, 2) (latch = 0, values = 2, errors = 0, completions = 1)", AssertionError.class, () -> { + TestSubscriber<Integer> ts = new TestSubscriber<>(); + + Flowable.just(1, 2).subscribe(ts); + + ts.assertValueAt(-2, new Predicate<Integer>() { + @Override public boolean test(final Integer o) throws Exception { + return o == 1; + } + }); + }); + } + + @Test + public void requestMore() { + Flowable.range(1, 5) + .test(0) + .requestMore(1) + .assertValue(1) + .requestMore(2) + .assertValues(1, 2, 3) + .requestMore(3) + .assertResult(1, 2, 3, 4, 5); + } + + @Test + public void withTag() { + try { + for (int i = 1; i < 3; i++) { + Flowable.just(i) + .test() + .withTag("testing with item=" + i) + .assertResult(1) + ; + } + throw new RuntimeException("Should have thrown!"); + } catch (AssertionError ex) { + assertTrue(ex.toString(), ex.toString().contains("testing with item=2")); + } + } + + @Test + public void timeoutIndicated() throws InterruptedException { + Thread.interrupted(); // clear flag + + TestSubscriber<Object> ts = Flowable.never() + .test(); + assertFalse(ts.await(1, TimeUnit.MILLISECONDS)); + + try { + ts.assertResult(1); + throw new RuntimeException("Should have thrown!"); + } catch (AssertionError ex) { + assertTrue(ex.toString(), ex.toString().contains("timeout!")); + } + } + + @Test + public void timeoutIndicated2() throws InterruptedException { + try { + Flowable.never() + .test() + .awaitDone(1, TimeUnit.MILLISECONDS) + .assertResult(1); + + throw new RuntimeException("Should have thrown!"); + } catch (AssertionError ex) { + assertTrue(ex.toString(), ex.toString().contains("timeout!")); + } + } + + @Test + public void timeoutIndicated3() throws InterruptedException { + TestSubscriber<Object> ts = Flowable.never() + .test(); + assertFalse(ts.await(1, TimeUnit.MILLISECONDS)); + + try { + ts.assertResult(1); + throw new RuntimeException("Should have thrown!"); + } catch (AssertionError ex) { + assertTrue(ex.toString(), ex.toString().contains("timeout!")); + } + } + + @Test + public void disposeIndicated() { + TestSubscriber<Object> ts = new TestSubscriber<>(); + ts.cancel(); + + try { + ts.assertResult(1); + throw new RuntimeException("Should have thrown!"); + } catch (Throwable ex) { + assertTrue(ex.toString(), ex.toString().contains("disposed!")); + } + } + + @Test + public void awaitCount() { + Flowable.range(1, 10).delay(100, TimeUnit.MILLISECONDS) + .test(5) + .awaitCount(5) + .assertValues(1, 2, 3, 4, 5) + .requestMore(5) + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + } + + @Test + public void awaitCountLess() { + Flowable.range(1, 4) + .test() + .awaitCount(5) + .assertResult(1, 2, 3, 4); + } + + @Test + public void assertValueAtPredicateThrows() { + try { + Flowable.just(1) + .test() + .assertValueAt(0, new Predicate<Integer>() { + @Override + public boolean test(Integer t) throws Exception { + throw new IllegalArgumentException(); + } + }); + throw new RuntimeException("Should have thrown!"); + } catch (IllegalArgumentException ex) { + // expected + } + } + + @Test + public void assertValuesOnly() { + TestSubscriber<Integer> ts = TestSubscriber.create(); + ts.onSubscribe(new BooleanSubscription()); + ts.assertValuesOnly(); + + ts.onNext(5); + ts.assertValuesOnly(5); + + ts.onNext(-1); + ts.assertValuesOnly(5, -1); + } + + @Test + public void assertValuesOnlyThrowsOnUnexpectedValue() { + TestSubscriber<Integer> ts = TestSubscriber.create(); + ts.onSubscribe(new BooleanSubscription()); + ts.assertValuesOnly(); + + ts.onNext(5); + ts.assertValuesOnly(5); + + ts.onNext(-1); + + try { + ts.assertValuesOnly(5); + throw new RuntimeException(); + } catch (AssertionError ex) { + // expected + } + } + + @Test + public void assertValuesOnlyThrowsWhenCompleted() { + TestSubscriber<Integer> ts = TestSubscriber.create(); + ts.onSubscribe(new BooleanSubscription()); + + ts.onComplete(); + + try { + ts.assertValuesOnly(); + throw new RuntimeException(); + } catch (AssertionError ex) { + // expected + } + } + + @Test + public void assertValuesOnlyThrowsWhenErrored() { + TestSubscriber<Integer> ts = TestSubscriber.create(); + ts.onSubscribe(new BooleanSubscription()); + + ts.onError(new TestException()); + + try { + ts.assertValuesOnly(); + throw new RuntimeException(); + } catch (AssertionError ex) { + // expected + } + } + + @Test + public void onErrorIsNull() { + TestSubscriber<Integer> ts = TestSubscriber.create(); + ts.onSubscribe(new BooleanSubscription()); + + ts.onError(null); + + ts.assertFailure(NullPointerException.class); + } + + static final class TestSubscriberImpl<T> extends TestSubscriber<T> { + public boolean isTimeout() { + return timeout; + } + } + + @Test + public void awaitCountTimeout() { + TestSubscriberImpl<Integer> ts = new TestSubscriberImpl<>(); + ts.onSubscribe(new BooleanSubscription()); + ts.awaitCount(1); + assertTrue(ts.isTimeout()); + } + + @Test(expected = RuntimeException.class) + public void awaitCountInterrupted() { + try { + TestSubscriber<Integer> ts = TestSubscriber.create(); + ts.onSubscribe(new BooleanSubscription()); + Thread.currentThread().interrupt(); + ts.awaitCount(1); + } finally { + Thread.interrupted(); + } + } +} diff --git a/src/test/java/io/reactivex/rxjava3/tck/AllTckTest.java b/src/test/java/io/reactivex/rxjava3/tck/AllTckTest.java new file mode 100644 index 0000000000..4b8060fba9 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/tck/AllTckTest.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.tck; + +import org.reactivestreams.Publisher; +import org.testng.annotations.Test; + +import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.functions.Predicate; + +@Test +public class AllTckTest extends BaseTck<Boolean> { + + @Override + public Publisher<Boolean> createPublisher(final long elements) { + return + Flowable.range(1, 1000).all(new Predicate<Integer>() { + @Override + public boolean test(Integer e) throws Exception { + return e < 800; + } + }).toFlowable() + ; + } + + @Override + public long maxElementsFromPublisher() { + return 1; + } +} diff --git a/src/test/java/io/reactivex/rxjava3/tck/AmbArrayTckTest.java b/src/test/java/io/reactivex/rxjava3/tck/AmbArrayTckTest.java new file mode 100644 index 0000000000..1260dd2d58 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/tck/AmbArrayTckTest.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.tck; + +import org.reactivestreams.Publisher; +import org.testng.annotations.Test; + +import io.reactivex.rxjava3.core.Flowable; + +@Test +public class AmbArrayTckTest extends BaseTck<Long> { + + @Override + public Publisher<Long> createPublisher(long elements) { + return + Flowable.ambArray( + Flowable.fromIterable(iterate(elements)), + Flowable.<Long>never() + ) + ; + } +} diff --git a/src/test/java/io/reactivex/rxjava3/tck/AmbTckTest.java b/src/test/java/io/reactivex/rxjava3/tck/AmbTckTest.java new file mode 100644 index 0000000000..6c687b7461 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/tck/AmbTckTest.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.tck; + +import java.util.Arrays; + +import org.reactivestreams.Publisher; +import org.testng.annotations.Test; + +import io.reactivex.rxjava3.core.Flowable; + +@Test +public class AmbTckTest extends BaseTck<Long> { + + @Override + public Publisher<Long> createPublisher(long elements) { + return + Flowable.amb(Arrays.asList( + Flowable.fromIterable(iterate(elements)), + Flowable.<Long>never() + ) + ) + ; + } +} diff --git a/src/test/java/io/reactivex/rxjava3/tck/AnyTckTest.java b/src/test/java/io/reactivex/rxjava3/tck/AnyTckTest.java new file mode 100644 index 0000000000..34e9fe5517 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/tck/AnyTckTest.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.tck; + +import org.reactivestreams.Publisher; +import org.testng.annotations.Test; + +import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.functions.Predicate; + +@Test +public class AnyTckTest extends BaseTck<Boolean> { + + @Override + public Publisher<Boolean> createPublisher(final long elements) { + return + Flowable.range(1, 1000).any(new Predicate<Integer>() { + @Override + public boolean test(Integer e) throws Exception { + return e == 500; + } + }).toFlowable() + ; + } + + @Override + public long maxElementsFromPublisher() { + return 1; + } +} diff --git a/src/test/java/io/reactivex/rxjava3/tck/AsyncProcessorAsPublisherTckTest.java b/src/test/java/io/reactivex/rxjava3/tck/AsyncProcessorAsPublisherTckTest.java new file mode 100644 index 0000000000..87a75f0052 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/tck/AsyncProcessorAsPublisherTckTest.java @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.tck; + +import org.reactivestreams.Publisher; +import org.testng.annotations.Test; + +import io.reactivex.rxjava3.processors.AsyncProcessor; +import io.reactivex.rxjava3.schedulers.Schedulers; + +@Test +public class AsyncProcessorAsPublisherTckTest extends BaseTck<Integer> { + + public AsyncProcessorAsPublisherTckTest() { + super(100); + } + + @Override + public Publisher<Integer> createPublisher(final long elements) { + final AsyncProcessor<Integer> pp = AsyncProcessor.create(); + + Schedulers.io().scheduleDirect(new Runnable() { + @Override + public void run() { + long start = System.currentTimeMillis(); + while (!pp.hasSubscribers()) { + try { + Thread.sleep(1); + } catch (InterruptedException ex) { + return; + } + + if (System.currentTimeMillis() - start > 200) { + return; + } + } + + for (int i = 0; i < elements; i++) { + pp.onNext(i); + } + pp.onComplete(); + } + }); + return pp; + } + + @Override + public long maxElementsFromPublisher() { + return 1; + } +} diff --git a/src/test/java/io/reactivex/rxjava3/tck/BaseTck.java b/src/test/java/io/reactivex/rxjava3/tck/BaseTck.java new file mode 100644 index 0000000000..aa42bcc03a --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/tck/BaseTck.java @@ -0,0 +1,145 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.tck; + +import java.util.*; + +import org.reactivestreams.Publisher; +import org.reactivestreams.tck.*; +import org.testng.annotations.Test; + +import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.exceptions.TestException; + +/** + * Base abstract class for Flowable verifications, contains support for creating + * Iterable range of values. + * + * @param <T> the element type + */ +@Test +public abstract class BaseTck<T> extends PublisherVerification<T> { + + public BaseTck() { + this(25L); + } + + public BaseTck(long timeout) { + super(new TestEnvironment(timeout)); + } + + @Override + public Publisher<T> createFailedPublisher() { + return Flowable.error(new TestException()); + } + + @Override + public long maxElementsFromPublisher() { + return 1024; + } + + /** + * Creates an Iterable with the specified number of elements or an infinite one if + * {@code elements >} {@link Integer#MAX_VALUE}. + * @param elements the number of elements to return, {@link Integer#MAX_VALUE} means an infinite sequence + * @return the Iterable + */ + protected Iterable<Long> iterate(long elements) { + return iterate(elements > Integer.MAX_VALUE, elements); + } + + protected Iterable<Long> iterate(boolean useInfinite, long elements) { + return useInfinite ? new InfiniteRange() : new FiniteRange(elements); + } + + /** + * Create an array of Long values, ranging from 0L to elements - 1L. + * @param elements the number of elements to return + * @return the array + */ + protected Long[] array(long elements) { + Long[] a = new Long[(int)elements]; + for (int i = 0; i < elements; i++) { + a[i] = (long)i; + } + return a; + } + + static final class FiniteRange implements Iterable<Long> { + final long end; + FiniteRange(long end) { + this.end = end; + } + + @Override + public Iterator<Long> iterator() { + return new FiniteRangeIterator(end); + } + + static final class FiniteRangeIterator implements Iterator<Long> { + final long end; + long count; + + FiniteRangeIterator(long end) { + this.end = end; + } + + @Override + public boolean hasNext() { + return count != end; + } + + @Override + public Long next() { + long c = count; + if (c != end) { + count = c + 1; + return c; + } + throw new NoSuchElementException(); + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + } + } + + static final class InfiniteRange implements Iterable<Long> { + @Override + public Iterator<Long> iterator() { + return new InfiniteRangeIterator(); + } + + static final class InfiniteRangeIterator implements Iterator<Long> { + long count; + + @Override + public boolean hasNext() { + return true; + } + + @Override + public Long next() { + return count++; + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + } + } +} diff --git a/src/test/java/io/reactivex/rxjava3/tck/BehaviorProcessorAsPublisherTckTest.java b/src/test/java/io/reactivex/rxjava3/tck/BehaviorProcessorAsPublisherTckTest.java new file mode 100644 index 0000000000..94c5e15c14 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/tck/BehaviorProcessorAsPublisherTckTest.java @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.tck; + +import org.reactivestreams.Publisher; +import org.testng.annotations.Test; + +import io.reactivex.rxjava3.processors.BehaviorProcessor; +import io.reactivex.rxjava3.schedulers.Schedulers; + +@Test +public class BehaviorProcessorAsPublisherTckTest extends BaseTck<Integer> { + + public BehaviorProcessorAsPublisherTckTest() { + super(50); + } + + @Override + public Publisher<Integer> createPublisher(final long elements) { + final BehaviorProcessor<Integer> pp = BehaviorProcessor.create(); + + Schedulers.io().scheduleDirect(new Runnable() { + @Override + public void run() { + long start = System.currentTimeMillis(); + while (!pp.hasSubscribers()) { + try { + Thread.sleep(1); + } catch (InterruptedException ex) { + return; + } + + if (System.currentTimeMillis() - start > 200) { + return; + } + } + + for (int i = 0; i < elements; i++) { + while (!pp.offer(i)) { + Thread.yield(); + if (System.currentTimeMillis() - start > 1000) { + return; + } + } + } + pp.onComplete(); + } + }); + return pp; + } +} diff --git a/src/test/java/io/reactivex/rxjava3/tck/BufferBoundaryTckTest.java b/src/test/java/io/reactivex/rxjava3/tck/BufferBoundaryTckTest.java new file mode 100644 index 0000000000..cf79b210d6 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/tck/BufferBoundaryTckTest.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.tck; + +import java.util.List; + +import org.reactivestreams.Publisher; +import org.testng.annotations.Test; + +import io.reactivex.rxjava3.core.Flowable; + +@Test +public class BufferBoundaryTckTest extends BaseTck<List<Long>> { + + @Override + public Publisher<List<Long>> createPublisher(long elements) { + return + Flowable.fromIterable(iterate(elements)) + .buffer(Flowable.just(1).concatWith(Flowable.<Integer>never())) + .onBackpressureLatest() + ; + } + + @Override + public long maxElementsFromPublisher() { + return 1; + } +} diff --git a/src/test/java/io/reactivex/rxjava3/tck/BufferExactSizeTckTest.java b/src/test/java/io/reactivex/rxjava3/tck/BufferExactSizeTckTest.java new file mode 100644 index 0000000000..d4b7a7abf5 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/tck/BufferExactSizeTckTest.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.tck; + +import java.util.List; + +import org.reactivestreams.Publisher; +import org.testng.annotations.Test; + +import io.reactivex.rxjava3.core.Flowable; + +@Test +public class BufferExactSizeTckTest extends BaseTck<List<Long>> { + + @Override + public Publisher<List<Long>> createPublisher(long elements) { + return + Flowable.fromIterable(iterate(elements * 2)) + .buffer(2) + ; + } +} diff --git a/src/test/java/io/reactivex/rxjava3/tck/CacheTckTest.java b/src/test/java/io/reactivex/rxjava3/tck/CacheTckTest.java new file mode 100644 index 0000000000..ec9c481fdc --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/tck/CacheTckTest.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.tck; + +import org.reactivestreams.Publisher; +import org.testng.annotations.Test; + +import io.reactivex.rxjava3.core.Flowable; + +@Test +public class CacheTckTest extends BaseTck<Long> { + + @Override + public Publisher<Long> createPublisher(long elements) { + return + Flowable.fromIterable(iterate(elements)).cache() + ; + } +} diff --git a/src/test/java/io/reactivex/rxjava3/tck/CollectTckTest.java b/src/test/java/io/reactivex/rxjava3/tck/CollectTckTest.java new file mode 100644 index 0000000000..76c359f327 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/tck/CollectTckTest.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.tck; + +import java.util.List; + +import org.reactivestreams.Publisher; +import org.testng.annotations.Test; + +import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.functions.BiConsumer; +import io.reactivex.rxjava3.internal.functions.Functions; + +@Test +public class CollectTckTest extends BaseTck<List<Integer>> { + + @Override + public Publisher<List<Integer>> createPublisher(final long elements) { + return + Flowable.range(1, 1000).collect(Functions.<Integer>createArrayList(128), new BiConsumer<List<Integer>, Integer>() { + @Override + public void accept(List<Integer> a, Integer b) throws Exception { + a.add(b); + } + }).toFlowable() + ; + } + + @Override + public long maxElementsFromPublisher() { + return 1; + } +} diff --git a/src/test/java/io/reactivex/rxjava3/tck/CombineLatestArrayDelayErrorTckTest.java b/src/test/java/io/reactivex/rxjava3/tck/CombineLatestArrayDelayErrorTckTest.java new file mode 100644 index 0000000000..80bccac273 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/tck/CombineLatestArrayDelayErrorTckTest.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.tck; + +import org.reactivestreams.Publisher; +import org.testng.annotations.Test; + +import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.functions.Function; + +@Test +public class CombineLatestArrayDelayErrorTckTest extends BaseTck<Long> { + + @SuppressWarnings("unchecked") + @Override + public Publisher<Long> createPublisher(long elements) { + return + Flowable.combineLatestArrayDelayError( + new Publisher[] { Flowable.just(1L), Flowable.fromIterable(iterate(elements)) }, + new Function<Object[], Long>() { + @Override + public Long apply(Object[] a) throws Exception { + return (Long)a[0]; + } + } + ) + ; + } +} diff --git a/src/test/java/io/reactivex/rxjava3/tck/CombineLatestArrayTckTest.java b/src/test/java/io/reactivex/rxjava3/tck/CombineLatestArrayTckTest.java new file mode 100644 index 0000000000..c5a040dc25 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/tck/CombineLatestArrayTckTest.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.tck; + +import org.reactivestreams.Publisher; +import org.testng.annotations.Test; + +import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.functions.Function; + +@Test +public class CombineLatestArrayTckTest extends BaseTck<Long> { + + @SuppressWarnings("unchecked") + @Override + public Publisher<Long> createPublisher(long elements) { + return + Flowable.combineLatestArray( + new Publisher[] { Flowable.just(1L), Flowable.fromIterable(iterate(elements)) }, + new Function<Object[], Long>() { + @Override + public Long apply(Object[] a) throws Exception { + return (Long)a[0]; + } + } + ) + ; + } +} diff --git a/src/test/java/io/reactivex/rxjava3/tck/CombineLatestIterableDelayErrorTckTest.java b/src/test/java/io/reactivex/rxjava3/tck/CombineLatestIterableDelayErrorTckTest.java new file mode 100644 index 0000000000..41883ab6c1 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/tck/CombineLatestIterableDelayErrorTckTest.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.tck; + +import java.util.Arrays; + +import org.reactivestreams.Publisher; +import org.testng.annotations.Test; + +import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.functions.Function; + +@Test +public class CombineLatestIterableDelayErrorTckTest extends BaseTck<Long> { + + @Override + public Publisher<Long> createPublisher(long elements) { + return + Flowable.combineLatestDelayError(Arrays.asList( + Flowable.just(1L), + Flowable.fromIterable(iterate(elements)) + ), + new Function<Object[], Long>() { + @Override + public Long apply(Object[] a) throws Exception { + return (Long)a[0]; + } + } + ) + ; + } +} diff --git a/src/test/java/io/reactivex/rxjava3/tck/CombineLatestIterableTckTest.java b/src/test/java/io/reactivex/rxjava3/tck/CombineLatestIterableTckTest.java new file mode 100644 index 0000000000..a7b9279a56 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/tck/CombineLatestIterableTckTest.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.tck; + +import java.util.Arrays; + +import org.reactivestreams.Publisher; +import org.testng.annotations.Test; + +import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.functions.Function; + +@Test +public class CombineLatestIterableTckTest extends BaseTck<Long> { + + @Override + public Publisher<Long> createPublisher(long elements) { + return + Flowable.combineLatest(Arrays.asList( + Flowable.just(1L), + Flowable.fromIterable(iterate(elements)) + ), + new Function<Object[], Long>() { + @Override + public Long apply(Object[] a) throws Exception { + return (Long)a[0]; + } + } + ) + ; + } +} diff --git a/src/test/java/io/reactivex/rxjava3/tck/CompletableAndThenPublisherTckTest.java b/src/test/java/io/reactivex/rxjava3/tck/CompletableAndThenPublisherTckTest.java new file mode 100644 index 0000000000..22c1d4ceef --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/tck/CompletableAndThenPublisherTckTest.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.tck; + +import org.reactivestreams.Publisher; +import org.testng.annotations.Test; + +import io.reactivex.rxjava3.core.*; + +@Test +public class CompletableAndThenPublisherTckTest extends BaseTck<Integer> { + + @Override + public Publisher<Integer> createPublisher(final long elements) { + return + Completable.complete().hide().andThen(Flowable.range(0, (int)elements)) + ; + } +} diff --git a/src/test/java/io/reactivex/rxjava3/tck/ConcatArrayEagerTckTest.java b/src/test/java/io/reactivex/rxjava3/tck/ConcatArrayEagerTckTest.java new file mode 100644 index 0000000000..25afb45060 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/tck/ConcatArrayEagerTckTest.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.tck; + +import java.util.Arrays; + +import org.reactivestreams.Publisher; +import org.testng.annotations.Test; + +import io.reactivex.rxjava3.core.Flowable; + +@Test +public class ConcatArrayEagerTckTest extends BaseTck<Long> { + + @Override + public Publisher<Long> createPublisher(long elements) { + return + Flowable.concatEager(Arrays.asList( + Flowable.fromIterable(iterate(elements / 2)), + Flowable.fromIterable(iterate(elements - elements / 2)) + ) + ) + ; + } +} diff --git a/src/test/java/io/reactivex/rxjava3/tck/ConcatIterableEagerTckTest.java b/src/test/java/io/reactivex/rxjava3/tck/ConcatIterableEagerTckTest.java new file mode 100644 index 0000000000..491f211cc2 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/tck/ConcatIterableEagerTckTest.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.tck; + +import org.reactivestreams.Publisher; +import org.testng.annotations.Test; + +import io.reactivex.rxjava3.core.Flowable; + +@Test +public class ConcatIterableEagerTckTest extends BaseTck<Long> { + + @Override + public Publisher<Long> createPublisher(long elements) { + return + Flowable.concatArrayEager( + Flowable.fromIterable(iterate(elements / 2)), + Flowable.fromIterable(iterate(elements - elements / 2)) + ) + ; + } +} diff --git a/src/test/java/io/reactivex/rxjava3/tck/ConcatMapIterableTckTest.java b/src/test/java/io/reactivex/rxjava3/tck/ConcatMapIterableTckTest.java new file mode 100644 index 0000000000..1a5ab46193 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/tck/ConcatMapIterableTckTest.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.tck; + +import java.util.Collections; + +import org.reactivestreams.Publisher; +import org.testng.annotations.Test; + +import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.functions.Function; + +@Test +public class ConcatMapIterableTckTest extends BaseTck<Integer> { + + @Override + public Publisher<Integer> createPublisher(long elements) { + return + Flowable.range(0, (int)elements) + .concatMapIterable(new Function<Integer, Iterable<Integer>>() { + @Override + public Iterable<Integer> apply(Integer v) throws Exception { + return Collections.singletonList(v); + } + }) + ; + } +} diff --git a/src/test/java/io/reactivex/rxjava3/tck/ConcatMapMaybeTckTest.java b/src/test/java/io/reactivex/rxjava3/tck/ConcatMapMaybeTckTest.java new file mode 100644 index 0000000000..5216b6a50f --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/tck/ConcatMapMaybeTckTest.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.tck; + +import org.reactivestreams.Publisher; +import org.testng.annotations.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.functions.Function; + +@Test +public class ConcatMapMaybeTckTest extends BaseTck<Integer> { + + @Override + public Publisher<Integer> createPublisher(long elements) { + return + Flowable.range(0, (int)elements) + .concatMapMaybe(new Function<Integer, Maybe<Integer>>() { + @Override + public Maybe<Integer> apply(Integer v) throws Exception { + return Maybe.just(v); + } + }) + ; + } +} diff --git a/src/test/java/io/reactivex/rxjava3/tck/ConcatMapSingleTckTest.java b/src/test/java/io/reactivex/rxjava3/tck/ConcatMapSingleTckTest.java new file mode 100644 index 0000000000..5d41ff6775 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/tck/ConcatMapSingleTckTest.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.tck; + +import org.reactivestreams.Publisher; +import org.testng.annotations.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.functions.Function; + +@Test +public class ConcatMapSingleTckTest extends BaseTck<Integer> { + + @Override + public Publisher<Integer> createPublisher(long elements) { + return + Flowable.range(0, (int)elements) + .concatMapSingle(new Function<Integer, Single<Integer>>() { + @Override + public Single<Integer> apply(Integer v) throws Exception { + return Single.just(v); + } + }) + ; + } +} diff --git a/src/test/java/io/reactivex/rxjava3/tck/ConcatMapTckTest.java b/src/test/java/io/reactivex/rxjava3/tck/ConcatMapTckTest.java new file mode 100644 index 0000000000..83a3774193 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/tck/ConcatMapTckTest.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.tck; + +import org.reactivestreams.Publisher; +import org.testng.annotations.Test; + +import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.functions.Function; + +@Test +public class ConcatMapTckTest extends BaseTck<Integer> { + + @Override + public Publisher<Integer> createPublisher(long elements) { + return + Flowable.range(0, (int)elements) + .concatMap(new Function<Integer, Publisher<Integer>>() { + @Override + public Publisher<Integer> apply(Integer v) throws Exception { + return Flowable.just(v); + } + }) + ; + } +} diff --git a/src/test/java/io/reactivex/rxjava3/tck/ConcatPublisherEagerTckTest.java b/src/test/java/io/reactivex/rxjava3/tck/ConcatPublisherEagerTckTest.java new file mode 100644 index 0000000000..194d7e79de --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/tck/ConcatPublisherEagerTckTest.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.tck; + +import org.reactivestreams.Publisher; +import org.testng.annotations.Test; + +import io.reactivex.rxjava3.core.Flowable; + +@Test +public class ConcatPublisherEagerTckTest extends BaseTck<Long> { + + @Override + public Publisher<Long> createPublisher(long elements) { + return + Flowable.concatEager(Flowable.just( + Flowable.fromIterable(iterate(elements / 2)), + Flowable.fromIterable(iterate(elements - elements / 2)) + ) + ) + ; + } +} diff --git a/src/test/java/io/reactivex/rxjava3/tck/ConcatPublisherTckTest.java b/src/test/java/io/reactivex/rxjava3/tck/ConcatPublisherTckTest.java new file mode 100644 index 0000000000..ad61abd029 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/tck/ConcatPublisherTckTest.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.tck; + +import org.reactivestreams.Publisher; +import org.testng.annotations.Test; + +import io.reactivex.rxjava3.core.Flowable; + +@Test +public class ConcatPublisherTckTest extends BaseTck<Long> { + + @Override + public Publisher<Long> createPublisher(long elements) { + return + Flowable.concat(Flowable.just( + Flowable.fromIterable(iterate(elements / 2)), + Flowable.fromIterable(iterate(elements - elements / 2)) + ) + ) + ; + } +} diff --git a/src/test/java/io/reactivex/rxjava3/tck/ConcatTckTest.java b/src/test/java/io/reactivex/rxjava3/tck/ConcatTckTest.java new file mode 100644 index 0000000000..c5eb360c73 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/tck/ConcatTckTest.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.tck; + +import org.reactivestreams.Publisher; +import org.testng.annotations.Test; + +import io.reactivex.rxjava3.core.Flowable; + +@Test +public class ConcatTckTest extends BaseTck<Long> { + + @Override + public Publisher<Long> createPublisher(long elements) { + return + Flowable.concat( + Flowable.fromIterable(iterate(elements / 2)), + Flowable.fromIterable(iterate(elements - elements / 2)) + ) + ; + } +} diff --git a/src/test/java/io/reactivex/rxjava3/tck/ConcatWithCompletableTckTest.java b/src/test/java/io/reactivex/rxjava3/tck/ConcatWithCompletableTckTest.java new file mode 100644 index 0000000000..a5734952f5 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/tck/ConcatWithCompletableTckTest.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.tck; + +import org.reactivestreams.Publisher; +import org.testng.annotations.Test; + +import io.reactivex.rxjava3.core.*; + +@Test +public class ConcatWithCompletableTckTest extends BaseTck<Integer> { + + @Override + public Publisher<Integer> createPublisher(long elements) { + return + Flowable.range(1, (int)elements) + .concatWith(Completable.complete()) + ; + } +} diff --git a/src/test/java/io/reactivex/rxjava3/tck/ConcatWithMaybeEmptyTckTest.java b/src/test/java/io/reactivex/rxjava3/tck/ConcatWithMaybeEmptyTckTest.java new file mode 100644 index 0000000000..e333457b82 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/tck/ConcatWithMaybeEmptyTckTest.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.tck; + +import org.reactivestreams.Publisher; +import org.testng.annotations.Test; + +import io.reactivex.rxjava3.core.*; + +@Test +public class ConcatWithMaybeEmptyTckTest extends BaseTck<Integer> { + + @Override + public Publisher<Integer> createPublisher(long elements) { + return + Flowable.range(1, (int)elements) + .concatWith(Maybe.<Integer>empty()) + ; + } +} diff --git a/src/test/java/io/reactivex/rxjava3/tck/ConcatWithMaybeTckTest.java b/src/test/java/io/reactivex/rxjava3/tck/ConcatWithMaybeTckTest.java new file mode 100644 index 0000000000..b56ac86eac --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/tck/ConcatWithMaybeTckTest.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.tck; + +import org.reactivestreams.Publisher; +import org.testng.annotations.Test; + +import io.reactivex.rxjava3.core.*; + +@Test +public class ConcatWithMaybeTckTest extends BaseTck<Integer> { + + @Override + public Publisher<Integer> createPublisher(long elements) { + return + Flowable.range(1, Math.max(0, (int)elements - 1)) + .concatWith(Maybe.just((int)elements)) + ; + } +} diff --git a/src/test/java/io/reactivex/rxjava3/tck/ConcatWithSingleTckTest.java b/src/test/java/io/reactivex/rxjava3/tck/ConcatWithSingleTckTest.java new file mode 100644 index 0000000000..56746e24fc --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/tck/ConcatWithSingleTckTest.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.tck; + +import org.reactivestreams.Publisher; +import org.testng.annotations.Test; + +import io.reactivex.rxjava3.core.*; + +@Test +public class ConcatWithSingleTckTest extends BaseTck<Integer> { + + @Override + public Publisher<Integer> createPublisher(long elements) { + return + Flowable.range(1, Math.max(0, (int)elements - 1)) + .concatWith(Single.just((int)elements)) + ; + } +} diff --git a/src/test/java/io/reactivex/rxjava3/tck/CreateTckTest.java b/src/test/java/io/reactivex/rxjava3/tck/CreateTckTest.java new file mode 100644 index 0000000000..ff4b0597d8 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/tck/CreateTckTest.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.tck; + +import org.reactivestreams.Publisher; +import org.testng.annotations.Test; + +import io.reactivex.rxjava3.core.*; + +@Test +public class CreateTckTest extends BaseTck<Long> { + + @Override + public Publisher<Long> createPublisher(final long elements) { + return + Flowable.create(new FlowableOnSubscribe<Long>() { + @Override + public void subscribe(FlowableEmitter<Long> e) throws Exception { + for (long i = 0; i < elements && !e.isCancelled(); i++) { + e.onNext(i); + } + e.onComplete(); + } + }, BackpressureStrategy.BUFFER) + ; + } +} diff --git a/src/test/java/io/reactivex/rxjava3/tck/DefaultIfEmptyTckTest.java b/src/test/java/io/reactivex/rxjava3/tck/DefaultIfEmptyTckTest.java new file mode 100644 index 0000000000..c9d9078bf5 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/tck/DefaultIfEmptyTckTest.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.tck; + +import org.reactivestreams.Publisher; +import org.testng.annotations.Test; + +import io.reactivex.rxjava3.core.Flowable; + +@Test +public class DefaultIfEmptyTckTest extends BaseTck<Integer> { + + @Override + public Publisher<Integer> createPublisher(long elements) { + return + Flowable.range(1, (int)elements).defaultIfEmpty(0) + ; + } +} diff --git a/src/test/java/io/reactivex/rxjava3/tck/DeferTckTest.java b/src/test/java/io/reactivex/rxjava3/tck/DeferTckTest.java new file mode 100644 index 0000000000..f834c1e637 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/tck/DeferTckTest.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.tck; + +import org.reactivestreams.Publisher; +import org.testng.annotations.Test; + +import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.functions.Supplier; + +@Test +public class DeferTckTest extends BaseTck<Long> { + + @Override + public Publisher<Long> createPublisher(final long elements) { + return + Flowable.defer(new Supplier<Publisher<Long>>() { + @Override + public Publisher<Long> get() throws Exception { + return Flowable.fromIterable(iterate(elements)); + } + } + ) + ; + } +} diff --git a/src/test/java/io/reactivex/rxjava3/tck/DelaySubscriptionTckTest.java b/src/test/java/io/reactivex/rxjava3/tck/DelaySubscriptionTckTest.java new file mode 100644 index 0000000000..5db6d0b877 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/tck/DelaySubscriptionTckTest.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.tck; + +import java.util.concurrent.TimeUnit; + +import org.reactivestreams.Publisher; +import org.testng.annotations.Test; + +import io.reactivex.rxjava3.core.Flowable; + +@Test +public class DelaySubscriptionTckTest extends BaseTck<Integer> { + + public DelaySubscriptionTckTest() { + super(200L); + } + + @Override + public Publisher<Integer> createPublisher(long elements) { + return + Flowable.range(0, (int)elements).delaySubscription(1, TimeUnit.MILLISECONDS) + ; + } +} diff --git a/src/test/java/io/reactivex/rxjava3/tck/DelayTckTest.java b/src/test/java/io/reactivex/rxjava3/tck/DelayTckTest.java new file mode 100644 index 0000000000..87ccaa37d2 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/tck/DelayTckTest.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.tck; + +import java.util.concurrent.TimeUnit; + +import org.reactivestreams.Publisher; +import org.testng.annotations.Test; + +import io.reactivex.rxjava3.core.Flowable; + +@Test +public class DelayTckTest extends BaseTck<Integer> { + + public DelayTckTest() { + super(100L); + } + + @Override + public Publisher<Integer> createPublisher(long elements) { + return + Flowable.range(0, (int)elements).delay(1, TimeUnit.MILLISECONDS) + ; + } +} diff --git a/src/test/java/io/reactivex/rxjava3/tck/DistinctTckTest.java b/src/test/java/io/reactivex/rxjava3/tck/DistinctTckTest.java new file mode 100644 index 0000000000..629d96fee6 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/tck/DistinctTckTest.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.tck; + +import org.reactivestreams.Publisher; +import org.testng.annotations.Test; + +import io.reactivex.rxjava3.core.Flowable; + +@Test +public class DistinctTckTest extends BaseTck<Integer> { + + @Override + public Publisher<Integer> createPublisher(long elements) { + return + Flowable.range(0, (int)elements) + .concatWith(Flowable.range(0, (int)elements)) + .distinct() + ; + } +} diff --git a/src/test/java/io/reactivex/rxjava3/tck/DistinctUntilChangedTckTest.java b/src/test/java/io/reactivex/rxjava3/tck/DistinctUntilChangedTckTest.java new file mode 100644 index 0000000000..ba69771a4f --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/tck/DistinctUntilChangedTckTest.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.tck; + +import org.reactivestreams.Publisher; +import org.testng.annotations.Test; + +import io.reactivex.rxjava3.core.Flowable; + +@Test +public class DistinctUntilChangedTckTest extends BaseTck<Integer> { + + @Override + public Publisher<Integer> createPublisher(long elements) { + return + Flowable.range(0, (int)elements) + .distinctUntilChanged() + ; + } +} diff --git a/src/test/java/io/reactivex/rxjava3/tck/DoAfterNextTckTest.java b/src/test/java/io/reactivex/rxjava3/tck/DoAfterNextTckTest.java new file mode 100644 index 0000000000..3b8fbebecb --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/tck/DoAfterNextTckTest.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.tck; + +import org.reactivestreams.Publisher; +import org.testng.annotations.Test; + +import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.internal.functions.Functions; + +@Test +public class DoAfterNextTckTest extends BaseTck<Integer> { + + @Override + public Publisher<Integer> createPublisher(long elements) { + return + Flowable.range(0, (int)elements).doAfterNext(Functions.emptyConsumer()) + ; + } +} diff --git a/src/test/java/io/reactivex/rxjava3/tck/DoFinallyTckTest.java b/src/test/java/io/reactivex/rxjava3/tck/DoFinallyTckTest.java new file mode 100644 index 0000000000..029892f440 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/tck/DoFinallyTckTest.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.tck; + +import org.reactivestreams.Publisher; +import org.testng.annotations.Test; + +import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.internal.functions.Functions; + +@Test +public class DoFinallyTckTest extends BaseTck<Integer> { + + @Override + public Publisher<Integer> createPublisher(long elements) { + return + Flowable.range(0, (int)elements).doFinally(Functions.EMPTY_ACTION) + ; + } +} diff --git a/src/test/java/io/reactivex/rxjava3/tck/DoOnNextTckTest.java b/src/test/java/io/reactivex/rxjava3/tck/DoOnNextTckTest.java new file mode 100644 index 0000000000..f7e343803b --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/tck/DoOnNextTckTest.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.tck; + +import org.reactivestreams.Publisher; +import org.testng.annotations.Test; + +import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.internal.functions.Functions; + +@Test +public class DoOnNextTckTest extends BaseTck<Integer> { + + @Override + public Publisher<Integer> createPublisher(long elements) { + return + Flowable.range(0, (int)elements).doOnNext(Functions.emptyConsumer()) + ; + } +} diff --git a/src/test/java/io/reactivex/rxjava3/tck/ElementAtTckTest.java b/src/test/java/io/reactivex/rxjava3/tck/ElementAtTckTest.java new file mode 100644 index 0000000000..58e18778d0 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/tck/ElementAtTckTest.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.tck; + +import org.reactivestreams.Publisher; +import org.testng.annotations.Test; + +import io.reactivex.rxjava3.core.Flowable; + +@Test +public class ElementAtTckTest extends BaseTck<Integer> { + + @Override + public Publisher<Integer> createPublisher(final long elements) { + return + Flowable.range(1, 10).elementAt(5).toFlowable() + ; + } + + @Override + public long maxElementsFromPublisher() { + return 1; + } +} diff --git a/src/test/java/io/reactivex/rxjava3/tck/EmptyTckTest.java b/src/test/java/io/reactivex/rxjava3/tck/EmptyTckTest.java new file mode 100644 index 0000000000..da65d54fe4 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/tck/EmptyTckTest.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.tck; + +import org.reactivestreams.Publisher; +import org.testng.annotations.Test; + +import io.reactivex.rxjava3.core.Flowable; + +@Test +public class EmptyTckTest extends BaseTck<Long> { + + @Override + public Publisher<Long> createPublisher(final long elements) { + return Flowable.<Long>empty(); + } + + @Override + public long maxElementsFromPublisher() { + return 0; + } +} diff --git a/src/test/java/io/reactivex/rxjava3/tck/FilterTckTest.java b/src/test/java/io/reactivex/rxjava3/tck/FilterTckTest.java new file mode 100644 index 0000000000..6c975dabfc --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/tck/FilterTckTest.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.tck; + +import org.reactivestreams.Publisher; +import org.testng.annotations.Test; + +import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.functions.Predicate; + +@Test +public class FilterTckTest extends BaseTck<Integer> { + + @Override + public Publisher<Integer> createPublisher(long elements) { + return + Flowable.range(0, (int)elements * 2).filter(new Predicate<Integer>() { + @Override + public boolean test(Integer v) throws Exception { + return (v & 1) == 0; + } + }) + ; + } +} diff --git a/src/test/java/io/reactivex/rxjava3/tck/FirstTckTest.java b/src/test/java/io/reactivex/rxjava3/tck/FirstTckTest.java new file mode 100644 index 0000000000..29082117dd --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/tck/FirstTckTest.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.tck; + +import org.reactivestreams.Publisher; +import org.testng.annotations.Test; + +import io.reactivex.rxjava3.core.Flowable; + +@Test +public class FirstTckTest extends BaseTck<Integer> { + + @Override + public Publisher<Integer> createPublisher(final long elements) { + return + Flowable.range(1, 10).firstElement().toFlowable() + ; + } + + @Override + public long maxElementsFromPublisher() { + return 1; + } +} diff --git a/src/test/java/io/reactivex/rxjava3/tck/FlatMapTckTest.java b/src/test/java/io/reactivex/rxjava3/tck/FlatMapTckTest.java new file mode 100644 index 0000000000..88e937f282 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/tck/FlatMapTckTest.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.tck; + +import org.reactivestreams.Publisher; +import org.testng.annotations.Test; + +import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.functions.Function; + +@Test +public class FlatMapTckTest extends BaseTck<Integer> { + + @Override + public Publisher<Integer> createPublisher(long elements) { + return + Flowable.range(0, (int)elements) + .flatMap(new Function<Integer, Publisher<Integer>>() { + @Override + public Publisher<Integer> apply(Integer v) throws Exception { + return Flowable.just(v); + } + }) + ; + } +} diff --git a/src/test/java/io/reactivex/rxjava3/tck/FromArrayTckTest.java b/src/test/java/io/reactivex/rxjava3/tck/FromArrayTckTest.java new file mode 100644 index 0000000000..84bcb0b70d --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/tck/FromArrayTckTest.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.tck; + +import org.reactivestreams.Publisher; +import org.testng.annotations.Test; + +import io.reactivex.rxjava3.core.Flowable; + +@Test +public class FromArrayTckTest extends BaseTck<Long> { + + @Override + public Publisher<Long> createPublisher(long elements) { + return + Flowable.fromArray(array(elements)) + ; + } + + @Override + public long maxElementsFromPublisher() { + return 1024 * 1024; + } +} diff --git a/src/test/java/io/reactivex/rxjava3/tck/FromCallableTckTest.java b/src/test/java/io/reactivex/rxjava3/tck/FromCallableTckTest.java new file mode 100644 index 0000000000..d786060933 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/tck/FromCallableTckTest.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.tck; + +import java.util.concurrent.Callable; + +import org.reactivestreams.Publisher; +import org.testng.annotations.Test; + +import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.exceptions.TestException; + +@Test +public class FromCallableTckTest extends BaseTck<Long> { + + @Override + public Publisher<Long> createPublisher(final long elements) { + return + Flowable.fromCallable(new Callable<Long>() { + @Override + public Long call() throws Exception { + return 1L; + } + } + ) + ; + } + + @Override + public Publisher<Long> createFailedPublisher() { + return + Flowable.fromCallable(new Callable<Long>() { + @Override + public Long call() throws Exception { + throw new TestException(); + } + } + ) + ; + } + + @Override + public long maxElementsFromPublisher() { + return 1; + } +} diff --git a/src/test/java/io/reactivex/rxjava3/tck/FromFutureTckTest.java b/src/test/java/io/reactivex/rxjava3/tck/FromFutureTckTest.java new file mode 100644 index 0000000000..edff3c277b --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/tck/FromFutureTckTest.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.tck; + +import java.util.concurrent.*; + +import org.reactivestreams.Publisher; +import org.testng.annotations.Test; + +import io.reactivex.rxjava3.core.Flowable; + +@Test +public class FromFutureTckTest extends BaseTck<Long> { + + @Override + public Publisher<Long> createPublisher(final long elements) { + FutureTask<Long> ft = new FutureTask<>(new Callable<Long>() { + @Override + public Long call() throws Exception { + return 1L; + } + }); + + ft.run(); + return Flowable.fromFuture(ft); + } + + @Override + public long maxElementsFromPublisher() { + return 1; + } +} diff --git a/src/test/java/io/reactivex/rxjava3/tck/FromIterableTckTest.java b/src/test/java/io/reactivex/rxjava3/tck/FromIterableTckTest.java new file mode 100644 index 0000000000..94dc77c555 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/tck/FromIterableTckTest.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.tck; + +import org.reactivestreams.Publisher; +import org.testng.annotations.Test; + +import io.reactivex.rxjava3.core.Flowable; + +@Test +public class FromIterableTckTest extends BaseTck<Long> { + + @Override + public Publisher<Long> createPublisher(long elements) { + return Flowable.fromIterable(iterate(elements)); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/tck/FromSupplierTckTest.java b/src/test/java/io/reactivex/rxjava3/tck/FromSupplierTckTest.java new file mode 100644 index 0000000000..acbe1df8b0 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/tck/FromSupplierTckTest.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.tck; + +import org.reactivestreams.Publisher; +import org.testng.annotations.Test; + +import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.Supplier; + +@Test +public class FromSupplierTckTest extends BaseTck<Long> { + + @Override + public Publisher<Long> createPublisher(final long elements) { + return + Flowable.fromSupplier(new Supplier<Long>() { + @Override + public Long get() throws Throwable { + return 1L; + } + } + ) + ; + } + + @Override + public Publisher<Long> createFailedPublisher() { + return + Flowable.fromSupplier(new Supplier<Long>() { + @Override + public Long get() throws Throwable { + throw new TestException(); + } + } + ) + ; + } + + @Override + public long maxElementsFromPublisher() { + return 1; + } +} diff --git a/src/test/java/io/reactivex/rxjava3/tck/GenerateTckTest.java b/src/test/java/io/reactivex/rxjava3/tck/GenerateTckTest.java new file mode 100644 index 0000000000..b5c5ef30f6 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/tck/GenerateTckTest.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.tck; + +import org.reactivestreams.Publisher; +import org.testng.annotations.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.functions.BiFunction; +import io.reactivex.rxjava3.internal.functions.Functions; + +@Test +public class GenerateTckTest extends BaseTck<Long> { + + @Override + public Publisher<Long> createPublisher(final long elements) { + return + Flowable.generate(Functions.justSupplier(0L), + new BiFunction<Long, Emitter<Long>, Long>() { + @Override + public Long apply(Long s, Emitter<Long> e) throws Exception { + e.onNext(s); + if (++s == elements) { + e.onComplete(); + } + return s; + } + }, Functions.<Long>emptyConsumer()) + ; + } +} diff --git a/src/test/java/io/reactivex/rxjava3/tck/GroupByTckTest.java b/src/test/java/io/reactivex/rxjava3/tck/GroupByTckTest.java new file mode 100644 index 0000000000..ebaedb86ae --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/tck/GroupByTckTest.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.tck; + +import org.reactivestreams.Publisher; +import org.testng.annotations.Test; + +import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.functions.Functions; + +@Test +public class GroupByTckTest extends BaseTck<Integer> { + + @SuppressWarnings({ "rawtypes", "unchecked" }) + @Override + public Publisher<Integer> createPublisher(long elements) { + return + Flowable.range(0, (int)elements).groupBy(new Function<Integer, Integer>() { + @Override + public Integer apply(Integer v) throws Exception { + return v & 1; + } + }) + .flatMap((Function)Functions.identity()) + ; + } +} diff --git a/src/test/java/io/reactivex/rxjava3/tck/HideTckTest.java b/src/test/java/io/reactivex/rxjava3/tck/HideTckTest.java new file mode 100644 index 0000000000..62c3e76c4e --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/tck/HideTckTest.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.tck; + +import org.reactivestreams.Publisher; +import org.testng.annotations.Test; + +import io.reactivex.rxjava3.core.Flowable; + +@Test +public class HideTckTest extends BaseTck<Integer> { + + @Override + public Publisher<Integer> createPublisher(long elements) { + return Flowable.range(0, (int)elements).hide(); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/tck/IgnoreElementsTckTest.java b/src/test/java/io/reactivex/rxjava3/tck/IgnoreElementsTckTest.java new file mode 100644 index 0000000000..289e2e90eb --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/tck/IgnoreElementsTckTest.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.tck; + +import org.reactivestreams.Publisher; +import org.testng.annotations.Test; + +import io.reactivex.rxjava3.core.Flowable; + +@Test +public class IgnoreElementsTckTest extends BaseTck<Integer> { + + @Override + public Publisher<Integer> createPublisher(final long elements) { + return + Flowable.range(1, 1000).ignoreElements().<Integer>toFlowable() + ; + } + + @Override + public long maxElementsFromPublisher() { + return 0; + } +} diff --git a/src/test/java/io/reactivex/rxjava3/tck/IntervalRangeTckTest.java b/src/test/java/io/reactivex/rxjava3/tck/IntervalRangeTckTest.java new file mode 100644 index 0000000000..b6c09d3d21 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/tck/IntervalRangeTckTest.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.tck; + +import java.util.concurrent.TimeUnit; + +import org.reactivestreams.Publisher; +import org.testng.annotations.Test; + +import io.reactivex.rxjava3.core.Flowable; + +@Test +public class IntervalRangeTckTest extends BaseTck<Long> { + + @Override + public Publisher<Long> createPublisher(long elements) { + return + Flowable.intervalRange(0, elements, 0, 1, TimeUnit.MILLISECONDS) + .onBackpressureBuffer() + ; + } +} diff --git a/src/test/java/io/reactivex/rxjava3/tck/IntervalTckTest.java b/src/test/java/io/reactivex/rxjava3/tck/IntervalTckTest.java new file mode 100644 index 0000000000..b9a01afc32 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/tck/IntervalTckTest.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.tck; + +import java.util.concurrent.TimeUnit; + +import org.reactivestreams.Publisher; +import org.testng.annotations.Test; + +import io.reactivex.rxjava3.core.Flowable; + +@Test +public class IntervalTckTest extends BaseTck<Long> { + + public IntervalTckTest() { + super(50); + } + + @Override + public Publisher<Long> createPublisher(long elements) { + return + Flowable.interval(0, 1, TimeUnit.MILLISECONDS).take(elements) + .onBackpressureBuffer() + ; + } +} diff --git a/src/test/java/io/reactivex/rxjava3/tck/IsEmptyTckTest.java b/src/test/java/io/reactivex/rxjava3/tck/IsEmptyTckTest.java new file mode 100644 index 0000000000..a9ffce0294 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/tck/IsEmptyTckTest.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.tck; + +import org.reactivestreams.Publisher; +import org.testng.annotations.Test; + +import io.reactivex.rxjava3.core.Flowable; + +@Test +public class IsEmptyTckTest extends BaseTck<Boolean> { + + @Override + public Publisher<Boolean> createPublisher(final long elements) { + return + Flowable.range(1, 10).isEmpty().toFlowable() + ; + } + + @Override + public long maxElementsFromPublisher() { + return 1; + } +} diff --git a/src/test/java/io/reactivex/rxjava3/tck/JustTckTest.java b/src/test/java/io/reactivex/rxjava3/tck/JustTckTest.java new file mode 100644 index 0000000000..668b32a31a --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/tck/JustTckTest.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.tck; + +import org.reactivestreams.Publisher; +import org.testng.annotations.Test; + +import io.reactivex.rxjava3.core.Flowable; + +@Test +public class JustTckTest extends BaseTck<Long> { + + @Override + public Publisher<Long> createPublisher(long elements) { + return + Flowable.just(0L) + ; + } + + @Override + public long maxElementsFromPublisher() { + return 1L; + } +} diff --git a/src/test/java/io/reactivex/rxjava3/tck/LastTckTest.java b/src/test/java/io/reactivex/rxjava3/tck/LastTckTest.java new file mode 100644 index 0000000000..eb75e0342a --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/tck/LastTckTest.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.tck; + +import org.reactivestreams.Publisher; +import org.testng.annotations.Test; + +import io.reactivex.rxjava3.core.Flowable; + +@Test +public class LastTckTest extends BaseTck<Integer> { + + @Override + public Publisher<Integer> createPublisher(final long elements) { + return + Flowable.range(1, 10).lastElement().toFlowable() + ; + } + + @Override + public long maxElementsFromPublisher() { + return 1; + } +} diff --git a/src/test/java/io/reactivex/rxjava3/tck/LimitTckTest.java b/src/test/java/io/reactivex/rxjava3/tck/LimitTckTest.java new file mode 100644 index 0000000000..08c526af03 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/tck/LimitTckTest.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.tck; + +import org.reactivestreams.Publisher; +import org.testng.annotations.Test; + +import io.reactivex.rxjava3.core.Flowable; + +@Test +public class LimitTckTest extends BaseTck<Integer> { + + @Override + public Publisher<Integer> createPublisher(long elements) { + return + Flowable.range(0, (int)elements * 2).take(elements) + ; + } +} diff --git a/src/test/java/io/reactivex/rxjava3/tck/MapTckTest.java b/src/test/java/io/reactivex/rxjava3/tck/MapTckTest.java new file mode 100644 index 0000000000..9875c412ac --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/tck/MapTckTest.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.tck; + +import org.reactivestreams.Publisher; +import org.testng.annotations.Test; + +import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.internal.functions.Functions; + +@Test +public class MapTckTest extends BaseTck<Integer> { + + @Override + public Publisher<Integer> createPublisher(long elements) { + return + Flowable.range(0, (int)elements).map(Functions.<Integer>identity()) + ; + } +} diff --git a/src/test/java/io/reactivex/rxjava3/tck/MaybeFlatMapPublisherTckTest.java b/src/test/java/io/reactivex/rxjava3/tck/MaybeFlatMapPublisherTckTest.java new file mode 100644 index 0000000000..d23521b8ab --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/tck/MaybeFlatMapPublisherTckTest.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.tck; + +import org.reactivestreams.Publisher; +import org.testng.annotations.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.functions.Function; + +@Test +public class MaybeFlatMapPublisherTckTest extends BaseTck<Integer> { + + @Override + public Publisher<Integer> createPublisher(final long elements) { + return + Maybe.just(1).hide().flatMapPublisher(new Function<Integer, Publisher<Integer>>() { + @Override + public Publisher<Integer> apply(Integer v) + throws Exception { + return Flowable.range(0, (int)elements); + } + }) + ; + } +} diff --git a/src/test/java/io/reactivex/rxjava3/tck/MergeIterableTckTest.java b/src/test/java/io/reactivex/rxjava3/tck/MergeIterableTckTest.java new file mode 100644 index 0000000000..38a105e2be --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/tck/MergeIterableTckTest.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.tck; + +import java.util.Arrays; + +import org.reactivestreams.Publisher; +import org.testng.annotations.Test; + +import io.reactivex.rxjava3.core.Flowable; + +@Test +public class MergeIterableTckTest extends BaseTck<Long> { + + @Override + public Publisher<Long> createPublisher(long elements) { + return + Flowable.merge(Arrays.asList( + Flowable.fromIterable(iterate(elements / 2)), + Flowable.fromIterable(iterate(elements - elements / 2)) + ) + ) + ; + } +} diff --git a/src/test/java/io/reactivex/rxjava3/tck/MergePublisherTckTest.java b/src/test/java/io/reactivex/rxjava3/tck/MergePublisherTckTest.java new file mode 100644 index 0000000000..42baefbf64 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/tck/MergePublisherTckTest.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.tck; + +import org.reactivestreams.Publisher; +import org.testng.annotations.Test; + +import io.reactivex.rxjava3.core.Flowable; + +@Test +public class MergePublisherTckTest extends BaseTck<Long> { + + @Override + public Publisher<Long> createPublisher(long elements) { + return + Flowable.merge(Flowable.just( + Flowable.fromIterable(iterate(elements / 2)), + Flowable.fromIterable(iterate(elements - elements / 2)) + ) + ) + ; + } +} diff --git a/src/test/java/io/reactivex/rxjava3/tck/MergeTckTest.java b/src/test/java/io/reactivex/rxjava3/tck/MergeTckTest.java new file mode 100644 index 0000000000..d0280ca83d --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/tck/MergeTckTest.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.tck; + +import org.reactivestreams.Publisher; +import org.testng.annotations.Test; + +import io.reactivex.rxjava3.core.Flowable; + +@Test +public class MergeTckTest extends BaseTck<Long> { + + @Override + public Publisher<Long> createPublisher(long elements) { + return + Flowable.merge( + Flowable.fromIterable(iterate(elements / 2)), + Flowable.fromIterable(iterate(elements - elements / 2)) + ) + ; + } +} diff --git a/src/test/java/io/reactivex/rxjava3/tck/MergeWithCompletableTckTest.java b/src/test/java/io/reactivex/rxjava3/tck/MergeWithCompletableTckTest.java new file mode 100644 index 0000000000..e7cd1a2aac --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/tck/MergeWithCompletableTckTest.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.tck; + +import org.reactivestreams.Publisher; +import org.testng.annotations.Test; + +import io.reactivex.rxjava3.core.*; + +@Test +public class MergeWithCompletableTckTest extends BaseTck<Long> { + + @Override + public Publisher<Long> createPublisher(long elements) { + return + Flowable.rangeLong(1, elements) + .mergeWith(Completable.complete()) + ; + } +} diff --git a/src/test/java/io/reactivex/rxjava3/tck/MergeWithMaybeEmptyTckTest.java b/src/test/java/io/reactivex/rxjava3/tck/MergeWithMaybeEmptyTckTest.java new file mode 100644 index 0000000000..cd8bec5079 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/tck/MergeWithMaybeEmptyTckTest.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.tck; + +import org.reactivestreams.Publisher; +import org.testng.annotations.Test; + +import io.reactivex.rxjava3.core.*; + +@Test +public class MergeWithMaybeEmptyTckTest extends BaseTck<Long> { + + @Override + public Publisher<Long> createPublisher(long elements) { + return + Flowable.rangeLong(1, elements) + .mergeWith(Maybe.<Long>empty()) + ; + } +} diff --git a/src/test/java/io/reactivex/rxjava3/tck/MergeWithMaybeTckTest.java b/src/test/java/io/reactivex/rxjava3/tck/MergeWithMaybeTckTest.java new file mode 100644 index 0000000000..1da618c609 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/tck/MergeWithMaybeTckTest.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.tck; + +import org.reactivestreams.Publisher; +import org.testng.annotations.Test; + +import io.reactivex.rxjava3.core.*; + +@Test +public class MergeWithMaybeTckTest extends BaseTck<Long> { + + @Override + public Publisher<Long> createPublisher(long elements) { + if (elements == 0) { + return Flowable.<Long>empty() + .mergeWith(Maybe.<Long>empty()); + } + return + Flowable.rangeLong(1, elements - 1) + .mergeWith(Maybe.just(elements)) + ; + } +} diff --git a/src/test/java/io/reactivex/rxjava3/tck/MergeWithSingleTckTest.java b/src/test/java/io/reactivex/rxjava3/tck/MergeWithSingleTckTest.java new file mode 100644 index 0000000000..0933253418 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/tck/MergeWithSingleTckTest.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.tck; + +import org.reactivestreams.Publisher; +import org.testng.annotations.Test; + +import io.reactivex.rxjava3.core.*; + +@Test +public class MergeWithSingleTckTest extends BaseTck<Long> { + + @Override + public Publisher<Long> createPublisher(long elements) { + if (elements == 0) { + return Flowable.empty(); + } + return + Flowable.rangeLong(1, elements - 1) + .mergeWith(Single.just(elements)) + ; + } +} diff --git a/src/test/java/io/reactivex/rxjava3/tck/MulticastProcessorAsPublisherTckTest.java b/src/test/java/io/reactivex/rxjava3/tck/MulticastProcessorAsPublisherTckTest.java new file mode 100644 index 0000000000..b4479181d3 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/tck/MulticastProcessorAsPublisherTckTest.java @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.tck; + +import org.reactivestreams.Publisher; +import org.testng.annotations.Test; + +import io.reactivex.rxjava3.processors.MulticastProcessor; +import io.reactivex.rxjava3.schedulers.Schedulers; + +@Test +public class MulticastProcessorAsPublisherTckTest extends BaseTck<Integer> { + + public MulticastProcessorAsPublisherTckTest() { + super(100); + } + + @Override + public Publisher<Integer> createPublisher(final long elements) { + final MulticastProcessor<Integer> mp = MulticastProcessor.create(); + mp.start(); + + Schedulers.io().scheduleDirect(new Runnable() { + @Override + public void run() { + long start = System.currentTimeMillis(); + while (!mp.hasSubscribers()) { + try { + Thread.sleep(1); + } catch (InterruptedException ex) { + return; + } + + if (System.currentTimeMillis() - start > 200) { + return; + } + } + + for (int i = 0; i < elements; i++) { + while (!mp.offer(i)) { + Thread.yield(); + if (System.currentTimeMillis() - start > 1000) { + return; + } + } + } + mp.onComplete(); + } + }); + return mp; + } +} diff --git a/src/test/java/io/reactivex/rxjava3/tck/MulticastProcessorRefCountedTckTest.java b/src/test/java/io/reactivex/rxjava3/tck/MulticastProcessorRefCountedTckTest.java new file mode 100644 index 0000000000..907d3e3f3f --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/tck/MulticastProcessorRefCountedTckTest.java @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.tck; + +import java.util.concurrent.*; + +import org.reactivestreams.*; +import org.reactivestreams.tck.*; +import org.testng.annotations.Test; + +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.processors.MulticastProcessor; + +@Test +public class MulticastProcessorRefCountedTckTest extends IdentityProcessorVerification<Integer> { + + public MulticastProcessorRefCountedTckTest() { + super(new TestEnvironment(200)); + } + + @Override + public Processor<Integer, Integer> createIdentityProcessor(int bufferSize) { + MulticastProcessor<Integer> mp = MulticastProcessor.create(true); + return mp; + } + + @Override + public Publisher<Integer> createFailedPublisher() { + MulticastProcessor<Integer> mp = MulticastProcessor.create(); + mp.start(); + mp.onError(new TestException()); + return mp; + } + + @Override + public ExecutorService publisherExecutorService() { + return Executors.newCachedThreadPool(); + } + + @Override + public Integer createElement(int element) { + return element; + } + + @Override + public long maxSupportedSubscribers() { + return 1; + } + + @Override + public long maxElementsFromPublisher() { + return 1024; + } +} diff --git a/src/test/java/io/reactivex/rxjava3/tck/MulticastProcessorTckTest.java b/src/test/java/io/reactivex/rxjava3/tck/MulticastProcessorTckTest.java new file mode 100644 index 0000000000..ac82f963f7 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/tck/MulticastProcessorTckTest.java @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.tck; + +import java.util.concurrent.*; + +import org.reactivestreams.*; +import org.reactivestreams.tck.*; +import org.testng.annotations.Test; + +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.processors.MulticastProcessor; + +@Test +public class MulticastProcessorTckTest extends IdentityProcessorVerification<Integer> { + + public MulticastProcessorTckTest() { + super(new TestEnvironment(50)); + } + + @Override + public Processor<Integer, Integer> createIdentityProcessor(int bufferSize) { + MulticastProcessor<Integer> mp = MulticastProcessor.create(); + return new RefCountProcessor<>(mp); + } + + @Override + public Publisher<Integer> createFailedPublisher() { + MulticastProcessor<Integer> mp = MulticastProcessor.create(); + mp.start(); + mp.onError(new TestException()); + return mp; + } + + @Override + public ExecutorService publisherExecutorService() { + return Executors.newCachedThreadPool(); + } + + @Override + public Integer createElement(int element) { + return element; + } + + @Override + public long maxSupportedSubscribers() { + return 1; + } + + @Override + public long maxElementsFromPublisher() { + return 1024; + } +} diff --git a/src/test/java/io/reactivex/rxjava3/tck/ObserveOnTckTest.java b/src/test/java/io/reactivex/rxjava3/tck/ObserveOnTckTest.java new file mode 100644 index 0000000000..d6e555be9c --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/tck/ObserveOnTckTest.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.tck; + +import org.reactivestreams.Publisher; +import org.testng.annotations.Test; + +import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.schedulers.Schedulers; + +@Test +public class ObserveOnTckTest extends BaseTck<Integer> { + + public ObserveOnTckTest() { + super(100L); + } + + @Override + public Publisher<Integer> createPublisher(long elements) { + return + Flowable.range(0, (int)elements).observeOn(Schedulers.single()) + ; + } +} diff --git a/src/test/java/io/reactivex/rxjava3/tck/OnBackpressureBufferTckTest.java b/src/test/java/io/reactivex/rxjava3/tck/OnBackpressureBufferTckTest.java new file mode 100644 index 0000000000..58053819aa --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/tck/OnBackpressureBufferTckTest.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.tck; + +import org.reactivestreams.Publisher; +import org.testng.annotations.Test; + +import io.reactivex.rxjava3.core.Flowable; + +@Test +public class OnBackpressureBufferTckTest extends BaseTck<Integer> { + + @Override + public Publisher<Integer> createPublisher(long elements) { + return + Flowable.range(0, (int)elements).onBackpressureBuffer() + ; + } +} diff --git a/src/test/java/io/reactivex/rxjava3/tck/OnErrorResumeWithTckTest.java b/src/test/java/io/reactivex/rxjava3/tck/OnErrorResumeWithTckTest.java new file mode 100644 index 0000000000..4d239c9f4d --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/tck/OnErrorResumeWithTckTest.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.tck; + +import org.reactivestreams.Publisher; +import org.testng.annotations.Test; + +import io.reactivex.rxjava3.core.Flowable; + +@Test +public class OnErrorResumeWithTckTest extends BaseTck<Integer> { + + @Override + public Publisher<Integer> createPublisher(long elements) { + return + Flowable.range(0, (int)elements).onErrorResumeWith(Flowable.<Integer>never()) + ; + } +} diff --git a/src/test/java/io/reactivex/rxjava3/tck/OnErrorReturnItemTckTest.java b/src/test/java/io/reactivex/rxjava3/tck/OnErrorReturnItemTckTest.java new file mode 100644 index 0000000000..abd7b28a4f --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/tck/OnErrorReturnItemTckTest.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.tck; + +import org.reactivestreams.Publisher; +import org.testng.annotations.Test; + +import io.reactivex.rxjava3.core.Flowable; + +@Test +public class OnErrorReturnItemTckTest extends BaseTck<Integer> { + + @Override + public Publisher<Integer> createPublisher(long elements) { + return + Flowable.range(0, (int)elements).onErrorReturnItem(0) + ; + } +} diff --git a/src/test/java/io/reactivex/rxjava3/tck/PublishProcessorAsPublisherTckTest.java b/src/test/java/io/reactivex/rxjava3/tck/PublishProcessorAsPublisherTckTest.java new file mode 100644 index 0000000000..8e596b1995 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/tck/PublishProcessorAsPublisherTckTest.java @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.tck; + +import org.reactivestreams.Publisher; +import org.testng.annotations.Test; + +import io.reactivex.rxjava3.processors.PublishProcessor; +import io.reactivex.rxjava3.schedulers.Schedulers; + +@Test +public class PublishProcessorAsPublisherTckTest extends BaseTck<Integer> { + + public PublishProcessorAsPublisherTckTest() { + super(100); + } + + @Override + public Publisher<Integer> createPublisher(final long elements) { + final PublishProcessor<Integer> pp = PublishProcessor.create(); + + Schedulers.io().scheduleDirect(new Runnable() { + @Override + public void run() { + long start = System.currentTimeMillis(); + while (!pp.hasSubscribers()) { + try { + Thread.sleep(1); + } catch (InterruptedException ex) { + return; + } + + if (System.currentTimeMillis() - start > 200) { + return; + } + } + + for (int i = 0; i < elements; i++) { + while (!pp.offer(i)) { + Thread.yield(); + if (System.currentTimeMillis() - start > 1000) { + return; + } + } + } + pp.onComplete(); + } + }); + return pp; + } +} diff --git a/src/test/java/io/reactivex/rxjava3/tck/PublishSelectorTckTest.java b/src/test/java/io/reactivex/rxjava3/tck/PublishSelectorTckTest.java new file mode 100644 index 0000000000..f996fabe36 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/tck/PublishSelectorTckTest.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.tck; + +import org.reactivestreams.Publisher; +import org.testng.annotations.Test; + +import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.functions.Functions; + +@Test +public class PublishSelectorTckTest extends BaseTck<Integer> { + + @SuppressWarnings({ "unchecked", "rawtypes" }) + @Override + public Publisher<Integer> createPublisher(long elements) { + return + Flowable.range(0, (int)elements).publish((Function)Functions.identity()) + ; + } +} diff --git a/src/test/java/io/reactivex/rxjava3/tck/PublishTckTest.java b/src/test/java/io/reactivex/rxjava3/tck/PublishTckTest.java new file mode 100644 index 0000000000..b85684d1da --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/tck/PublishTckTest.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.tck; + +import org.reactivestreams.Publisher; +import org.testng.annotations.Test; + +import io.reactivex.rxjava3.core.Flowable; + +@Test +public class PublishTckTest extends BaseTck<Integer> { + + @Override + public Publisher<Integer> createPublisher(long elements) { + return + Flowable.range(0, (int)elements).publish().autoConnect() + ; + } +} diff --git a/src/test/java/io/reactivex/rxjava3/tck/RangeTckTest.java b/src/test/java/io/reactivex/rxjava3/tck/RangeTckTest.java new file mode 100644 index 0000000000..53f3ca180e --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/tck/RangeTckTest.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.tck; + +import org.reactivestreams.Publisher; +import org.testng.annotations.Test; + +import io.reactivex.rxjava3.core.Flowable; + +@Test +public class RangeTckTest extends BaseTck<Integer> { + + @Override + public Publisher<Integer> createPublisher(long elements) { + return + Flowable.range(0, (int)elements) + ; + } +} diff --git a/src/test/java/io/reactivex/rxjava3/tck/RebatchRequestsTckTest.java b/src/test/java/io/reactivex/rxjava3/tck/RebatchRequestsTckTest.java new file mode 100644 index 0000000000..c08682d237 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/tck/RebatchRequestsTckTest.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.tck; + +import org.reactivestreams.Publisher; +import org.testng.annotations.Test; + +import io.reactivex.rxjava3.core.Flowable; + +@Test +public class RebatchRequestsTckTest extends BaseTck<Integer> { + + @Override + public Publisher<Integer> createPublisher(long elements) { + return + Flowable.range(0, (int)elements).rebatchRequests(2) + ; + } +} diff --git a/src/test/java/io/reactivex/rxjava3/tck/ReduceTckTest.java b/src/test/java/io/reactivex/rxjava3/tck/ReduceTckTest.java new file mode 100644 index 0000000000..9495021488 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/tck/ReduceTckTest.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.tck; + +import org.reactivestreams.Publisher; +import org.testng.annotations.Test; + +import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.functions.BiFunction; + +@Test +public class ReduceTckTest extends BaseTck<Integer> { + + @Override + public Publisher<Integer> createPublisher(final long elements) { + return + Flowable.range(1, 1000).reduce(new BiFunction<Integer, Integer, Integer>() { + @Override + public Integer apply(Integer a, Integer b) throws Exception { + return a + b; + } + }).toFlowable() + ; + } + + @Override + public long maxElementsFromPublisher() { + return 1; + } +} diff --git a/src/test/java/io/reactivex/rxjava3/tck/ReduceWithTckTest.java b/src/test/java/io/reactivex/rxjava3/tck/ReduceWithTckTest.java new file mode 100644 index 0000000000..9a4f404982 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/tck/ReduceWithTckTest.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.tck; + +import org.reactivestreams.Publisher; +import org.testng.annotations.Test; + +import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.functions.BiFunction; +import io.reactivex.rxjava3.internal.functions.Functions; + +@Test +public class ReduceWithTckTest extends BaseTck<Integer> { + + @Override + public Publisher<Integer> createPublisher(final long elements) { + return + Flowable.range(1, 1000).reduceWith(Functions.justSupplier(0), + new BiFunction<Integer, Integer, Integer>() { + @Override + public Integer apply(Integer a, Integer b) throws Exception { + return a + b; + } + }).toFlowable() + ; + } + + @Override + public long maxElementsFromPublisher() { + return 1; + } +} diff --git a/src/test/java/io/reactivex/rxjava3/tck/RefCountProcessor.java b/src/test/java/io/reactivex/rxjava3/tck/RefCountProcessor.java new file mode 100644 index 0000000000..f7c62cf752 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/tck/RefCountProcessor.java @@ -0,0 +1,219 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.tck; + +import java.util.concurrent.atomic.*; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.FlowableSubscriber; +import io.reactivex.rxjava3.internal.subscriptions.*; +import io.reactivex.rxjava3.processors.FlowableProcessor; + +/** + * A FlowableProcessor wrapper that disposes the Subscription set via + * onSubscribe if the number of subscribers reaches zero. + * + * @param <T> the upstream and downstream value type + * @since 2.1.8 + */ +/* public */final class RefCountProcessor<T> extends FlowableProcessor<T> implements Subscription { + + final FlowableProcessor<T> actual; + + final AtomicReference<Subscription> upstream; + + final AtomicReference<RefCountSubscriber<T>[]> subscribers; + + @SuppressWarnings("rawtypes") + static final RefCountSubscriber[] EMPTY = new RefCountSubscriber[0]; + + @SuppressWarnings("rawtypes") + static final RefCountSubscriber[] TERMINATED = new RefCountSubscriber[0]; + + @SuppressWarnings("unchecked") + RefCountProcessor(FlowableProcessor<T> actual) { + this.actual = actual; + this.upstream = new AtomicReference<>(); + this.subscribers = new AtomicReference<>(EMPTY); + } + + @Override + public void onSubscribe(Subscription s) { + if (SubscriptionHelper.setOnce(upstream, s)) { + actual.onSubscribe(this); + } + } + + @Override + public void onNext(T t) { + actual.onNext(t); + } + + @Override + public void onError(Throwable t) { + upstream.lazySet(SubscriptionHelper.CANCELLED); + actual.onError(t); + } + + @Override + public void onComplete() { + upstream.lazySet(SubscriptionHelper.CANCELLED); + actual.onComplete(); + } + + @Override + protected void subscribeActual(Subscriber<? super T> s) { + RefCountSubscriber<T> rcs = new RefCountSubscriber<>(s, this); + if (!add(rcs)) { + EmptySubscription.error(new IllegalStateException("RefCountProcessor terminated"), s); + return; + } + actual.subscribe(rcs); + } + + @Override + public boolean hasComplete() { + return actual.hasComplete(); + } + + @Override + public boolean hasThrowable() { + return actual.hasThrowable(); + } + + @Override + public Throwable getThrowable() { + return actual.getThrowable(); + } + + @Override + public boolean hasSubscribers() { + return actual.hasSubscribers(); + } + + @Override + public void cancel() { + SubscriptionHelper.cancel(upstream); + } + + @Override + public void request(long n) { + upstream.get().request(n); + } + + boolean add(RefCountSubscriber<T> rcs) { + for (;;) { + RefCountSubscriber<T>[] a = subscribers.get(); + if (a == TERMINATED) { + return false; + } + int n = a.length; + @SuppressWarnings("unchecked") + RefCountSubscriber<T>[] b = new RefCountSubscriber[n + 1]; + System.arraycopy(a, 0, b, 0, n); + b[n] = rcs; + if (subscribers.compareAndSet(a, b)) { + return true; + } + } + } + + @SuppressWarnings("unchecked") + void remove(RefCountSubscriber<T> rcs) { + for (;;) { + RefCountSubscriber<T>[] a = subscribers.get(); + int n = a.length; + if (n == 0) { + break; + } + int j = -1; + + for (int i = 0; i < n; i++) { + if (rcs == a[i]) { + j = i; + break; + } + } + + if (j < 0) { + break; + } + + RefCountSubscriber<T>[] b; + if (n == 1) { + b = TERMINATED; + } else { + b = new RefCountSubscriber[n - 1]; + System.arraycopy(a, 0, b, 0, j); + System.arraycopy(a, j + 1, b, j, n - j - 1); + } + if (subscribers.compareAndSet(a, b)) { + if (b == TERMINATED) { + cancel(); + } + break; + } + } + } + + static final class RefCountSubscriber<T> extends AtomicBoolean implements FlowableSubscriber<T>, Subscription { + + private static final long serialVersionUID = -4317488092687530631L; + + final Subscriber<? super T> downstream; + + final RefCountProcessor<T> parent; + + Subscription upstream; + + RefCountSubscriber(Subscriber<? super T> actual, RefCountProcessor<T> parent) { + this.downstream = actual; + this.parent = parent; + } + + @Override + public void request(long n) { + upstream.request(n); + } + + @Override + public void cancel() { + lazySet(true); + upstream.cancel(); + parent.remove(this); + } + + @Override + public void onSubscribe(Subscription s) { + this.upstream = s; + downstream.onSubscribe(this); + } + + @Override + public void onNext(T t) { + downstream.onNext(t); + } + + @Override + public void onError(Throwable t) { + downstream.onError(t); + } + + @Override + public void onComplete() { + downstream.onComplete(); + } + } +} diff --git a/src/test/java/io/reactivex/rxjava3/tck/RepeatTckTest.java b/src/test/java/io/reactivex/rxjava3/tck/RepeatTckTest.java new file mode 100644 index 0000000000..32f5832afd --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/tck/RepeatTckTest.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.tck; + +import org.reactivestreams.Publisher; +import org.testng.annotations.Test; + +import io.reactivex.rxjava3.core.Flowable; + +@Test +public class RepeatTckTest extends BaseTck<Integer> { + + @Override + public Publisher<Integer> createPublisher(long elements) { + return Flowable.just(1).repeat(elements); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/tck/ReplayProcessorSizeBoundAsPublisherTckTest.java b/src/test/java/io/reactivex/rxjava3/tck/ReplayProcessorSizeBoundAsPublisherTckTest.java new file mode 100644 index 0000000000..a32e49c6b9 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/tck/ReplayProcessorSizeBoundAsPublisherTckTest.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.tck; + +import org.reactivestreams.Publisher; +import org.testng.annotations.Test; + +import io.reactivex.rxjava3.processors.ReplayProcessor; +import io.reactivex.rxjava3.schedulers.Schedulers; + +@Test +public class ReplayProcessorSizeBoundAsPublisherTckTest extends BaseTck<Integer> { + + public ReplayProcessorSizeBoundAsPublisherTckTest() { + super(100); + } + + @Override + public Publisher<Integer> createPublisher(final long elements) { + final ReplayProcessor<Integer> pp = ReplayProcessor.createWithSize((int)elements + 10); + + Schedulers.io().scheduleDirect(new Runnable() { + @Override + public void run() { + long start = System.currentTimeMillis(); + while (!pp.hasSubscribers()) { + try { + Thread.sleep(1); + } catch (InterruptedException ex) { + return; + } + + if (System.currentTimeMillis() - start > 200) { + return; + } + } + + for (int i = 0; i < elements; i++) { + pp.onNext(i); + } + pp.onComplete(); + } + }); + return pp; + } +} diff --git a/src/test/java/io/reactivex/rxjava3/tck/ReplayProcessorTimeBoundAsPublisherTckTest.java b/src/test/java/io/reactivex/rxjava3/tck/ReplayProcessorTimeBoundAsPublisherTckTest.java new file mode 100644 index 0000000000..f53df02ae6 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/tck/ReplayProcessorTimeBoundAsPublisherTckTest.java @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.tck; + +import java.util.concurrent.TimeUnit; + +import org.reactivestreams.Publisher; +import org.testng.annotations.Test; + +import io.reactivex.rxjava3.processors.ReplayProcessor; +import io.reactivex.rxjava3.schedulers.Schedulers; + +@Test +public class ReplayProcessorTimeBoundAsPublisherTckTest extends BaseTck<Integer> { + + public ReplayProcessorTimeBoundAsPublisherTckTest() { + super(100); + } + + @Override + public Publisher<Integer> createPublisher(final long elements) { + final ReplayProcessor<Integer> pp = ReplayProcessor.createWithTime(1, TimeUnit.MINUTES, Schedulers.computation()); + + Schedulers.io().scheduleDirect(new Runnable() { + @Override + public void run() { + long start = System.currentTimeMillis(); + while (!pp.hasSubscribers()) { + try { + Thread.sleep(1); + } catch (InterruptedException ex) { + return; + } + + if (System.currentTimeMillis() - start > 200) { + return; + } + } + + for (int i = 0; i < elements; i++) { + pp.onNext(i); + } + pp.onComplete(); + } + }); + return pp; + } +} diff --git a/src/test/java/io/reactivex/rxjava3/tck/ReplayProcessorUnboundedAsPublisherTckTest.java b/src/test/java/io/reactivex/rxjava3/tck/ReplayProcessorUnboundedAsPublisherTckTest.java new file mode 100644 index 0000000000..6f286d68de --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/tck/ReplayProcessorUnboundedAsPublisherTckTest.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.tck; + +import org.reactivestreams.Publisher; +import org.testng.annotations.Test; + +import io.reactivex.rxjava3.processors.ReplayProcessor; +import io.reactivex.rxjava3.schedulers.Schedulers; + +@Test +public class ReplayProcessorUnboundedAsPublisherTckTest extends BaseTck<Integer> { + + public ReplayProcessorUnboundedAsPublisherTckTest() { + super(100); + } + + @Override + public Publisher<Integer> createPublisher(final long elements) { + final ReplayProcessor<Integer> pp = ReplayProcessor.create(); + + Schedulers.io().scheduleDirect(new Runnable() { + @Override + public void run() { + long start = System.currentTimeMillis(); + while (!pp.hasSubscribers()) { + try { + Thread.sleep(1); + } catch (InterruptedException ex) { + return; + } + + if (System.currentTimeMillis() - start > 200) { + return; + } + } + + for (int i = 0; i < elements; i++) { + pp.onNext(i); + } + pp.onComplete(); + } + }); + return pp; + } +} diff --git a/src/test/java/io/reactivex/rxjava3/tck/ReplaySelectorTckTest.java b/src/test/java/io/reactivex/rxjava3/tck/ReplaySelectorTckTest.java new file mode 100644 index 0000000000..84b1a86d45 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/tck/ReplaySelectorTckTest.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.tck; + +import org.reactivestreams.Publisher; +import org.testng.annotations.Test; + +import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.functions.Functions; + +@Test +public class ReplaySelectorTckTest extends BaseTck<Integer> { + + @SuppressWarnings({ "unchecked", "rawtypes" }) + @Override + public Publisher<Integer> createPublisher(long elements) { + return + Flowable.range(0, (int)elements).replay((Function)Functions.identity()) + ; + } +} diff --git a/src/test/java/io/reactivex/rxjava3/tck/ReplayTckTest.java b/src/test/java/io/reactivex/rxjava3/tck/ReplayTckTest.java new file mode 100644 index 0000000000..f62ffb08f3 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/tck/ReplayTckTest.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.tck; + +import org.reactivestreams.Publisher; +import org.testng.annotations.Test; + +import io.reactivex.rxjava3.core.Flowable; + +@Test +public class ReplayTckTest extends BaseTck<Integer> { + + @Override + public Publisher<Integer> createPublisher(long elements) { + return + Flowable.range(0, (int)elements).replay().autoConnect() + ; + } +} diff --git a/src/test/java/io/reactivex/rxjava3/tck/RetryTckTest.java b/src/test/java/io/reactivex/rxjava3/tck/RetryTckTest.java new file mode 100644 index 0000000000..ba0a0f4b1f --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/tck/RetryTckTest.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.tck; + +import org.reactivestreams.Publisher; +import org.testng.annotations.Test; + +import io.reactivex.rxjava3.core.Flowable; + +@Test +public class RetryTckTest extends BaseTck<Integer> { + + @Override + public Publisher<Integer> createPublisher(long elements) { + return + Flowable.range(0, (int)elements).retry(1) + ; + } +} diff --git a/src/test/java/io/reactivex/rxjava3/tck/ScanTckTest.java b/src/test/java/io/reactivex/rxjava3/tck/ScanTckTest.java new file mode 100644 index 0000000000..8a43120c02 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/tck/ScanTckTest.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.tck; + +import org.reactivestreams.Publisher; +import org.testng.annotations.Test; + +import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.functions.BiFunction; + +@Test +public class ScanTckTest extends BaseTck<Integer> { + + @Override + public Publisher<Integer> createPublisher(long elements) { + return + Flowable.range(0, (int)elements).scan(new BiFunction<Integer, Integer, Integer>() { + @Override + public Integer apply(Integer a, Integer b) throws Exception { + return a + b; + } + }) + ; + } +} diff --git a/src/test/java/io/reactivex/rxjava3/tck/SequenceEqualTckTest.java b/src/test/java/io/reactivex/rxjava3/tck/SequenceEqualTckTest.java new file mode 100644 index 0000000000..a4af22ed7e --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/tck/SequenceEqualTckTest.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.tck; + +import org.reactivestreams.Publisher; +import org.testng.annotations.Test; + +import io.reactivex.rxjava3.core.Flowable; + +@Test +public class SequenceEqualTckTest extends BaseTck<Boolean> { + + @Override + public Publisher<Boolean> createPublisher(final long elements) { + return + Flowable.sequenceEqual( + Flowable.range(1, 1000), + Flowable.range(1, 1001)) + .toFlowable() + ; + } + + @Override + public long maxElementsFromPublisher() { + return 1; + } +} diff --git a/src/test/java/io/reactivex/rxjava3/tck/ShareTckTest.java b/src/test/java/io/reactivex/rxjava3/tck/ShareTckTest.java new file mode 100644 index 0000000000..35c9b08e4b --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/tck/ShareTckTest.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.tck; + +import org.reactivestreams.Publisher; +import org.testng.annotations.Test; + +import io.reactivex.rxjava3.core.Flowable; + +@Test +public class ShareTckTest extends BaseTck<Integer> { + + @Override + public Publisher<Integer> createPublisher(long elements) { + return + Flowable.range(0, (int)elements).share() + ; + } +} diff --git a/src/test/java/io/reactivex/rxjava3/tck/SingleFlatMapFlowableTckTest.java b/src/test/java/io/reactivex/rxjava3/tck/SingleFlatMapFlowableTckTest.java new file mode 100644 index 0000000000..62565e6470 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/tck/SingleFlatMapFlowableTckTest.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.tck; + +import org.reactivestreams.Publisher; +import org.testng.annotations.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.functions.Function; + +@Test +public class SingleFlatMapFlowableTckTest extends BaseTck<Integer> { + + @Override + public Publisher<Integer> createPublisher(final long elements) { + return + Single.just(1).hide().flatMapPublisher(new Function<Integer, Publisher<Integer>>() { + @Override + public Publisher<Integer> apply(Integer v) + throws Exception { + return Flowable.range(0, (int)elements); + } + }) + ; + } +} diff --git a/src/test/java/io/reactivex/rxjava3/tck/SingleTckTest.java b/src/test/java/io/reactivex/rxjava3/tck/SingleTckTest.java new file mode 100644 index 0000000000..96af86f6bb --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/tck/SingleTckTest.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.tck; + +import org.reactivestreams.Publisher; +import org.testng.annotations.Test; + +import io.reactivex.rxjava3.core.Flowable; + +@Test +public class SingleTckTest extends BaseTck<Integer> { + + @Override + public Publisher<Integer> createPublisher(final long elements) { + return + Flowable.just(1).singleElement().toFlowable() + ; + } + + @Override + public long maxElementsFromPublisher() { + return 1; + } +} diff --git a/src/test/java/io/reactivex/rxjava3/tck/SkipLastTckTest.java b/src/test/java/io/reactivex/rxjava3/tck/SkipLastTckTest.java new file mode 100644 index 0000000000..953986813f --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/tck/SkipLastTckTest.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.tck; + +import org.reactivestreams.Publisher; +import org.testng.annotations.Test; + +import io.reactivex.rxjava3.core.Flowable; + +@Test +public class SkipLastTckTest extends BaseTck<Integer> { + + @Override + public Publisher<Integer> createPublisher(long elements) { + return + Flowable.range(0, (int)elements * 2).skipLast((int)elements) + ; + } +} diff --git a/src/test/java/io/reactivex/rxjava3/tck/SkipTckTest.java b/src/test/java/io/reactivex/rxjava3/tck/SkipTckTest.java new file mode 100644 index 0000000000..4f29940485 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/tck/SkipTckTest.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.tck; + +import org.reactivestreams.Publisher; +import org.testng.annotations.Test; + +import io.reactivex.rxjava3.core.Flowable; + +@Test +public class SkipTckTest extends BaseTck<Integer> { + + @Override + public Publisher<Integer> createPublisher(long elements) { + return + Flowable.range(0, (int)elements * 2).skip(elements) + ; + } +} diff --git a/src/test/java/io/reactivex/rxjava3/tck/SkipUntilTckTest.java b/src/test/java/io/reactivex/rxjava3/tck/SkipUntilTckTest.java new file mode 100644 index 0000000000..976c4983f1 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/tck/SkipUntilTckTest.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.tck; + +import org.reactivestreams.Publisher; +import org.testng.annotations.Test; + +import io.reactivex.rxjava3.core.Flowable; + +@Test +public class SkipUntilTckTest extends BaseTck<Integer> { + + @Override + public Publisher<Integer> createPublisher(long elements) { + return + Flowable.range(0, (int)elements).skipUntil(Flowable.just(1)) + ; + } +} diff --git a/src/test/java/io/reactivex/rxjava3/tck/SkipWhileTckTest.java b/src/test/java/io/reactivex/rxjava3/tck/SkipWhileTckTest.java new file mode 100644 index 0000000000..e4995aedd8 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/tck/SkipWhileTckTest.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.tck; + +import org.reactivestreams.Publisher; +import org.testng.annotations.Test; + +import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.internal.functions.Functions; + +@Test +public class SkipWhileTckTest extends BaseTck<Integer> { + + @Override + public Publisher<Integer> createPublisher(long elements) { + return + Flowable.range(0, (int)elements).skipWhile(Functions.alwaysFalse()) + ; + } +} diff --git a/src/test/java/io/reactivex/rxjava3/tck/SortedTckTest.java b/src/test/java/io/reactivex/rxjava3/tck/SortedTckTest.java new file mode 100644 index 0000000000..51ff134a9c --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/tck/SortedTckTest.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.tck; + +import org.reactivestreams.Publisher; +import org.testng.annotations.Test; + +import io.reactivex.rxjava3.core.Flowable; + +@Test +public class SortedTckTest extends BaseTck<Integer> { + + @Override + public Publisher<Integer> createPublisher(long elements) { + return + Flowable.range(0, (int)elements).sorted() + ; + } +} diff --git a/src/test/java/io/reactivex/rxjava3/tck/SubscribeOnTckTest.java b/src/test/java/io/reactivex/rxjava3/tck/SubscribeOnTckTest.java new file mode 100644 index 0000000000..f4303d5605 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/tck/SubscribeOnTckTest.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.tck; + +import org.reactivestreams.Publisher; +import org.testng.annotations.Test; + +import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.schedulers.Schedulers; + +@Test +public class SubscribeOnTckTest extends BaseTck<Integer> { + + @Override + public Publisher<Integer> createPublisher(long elements) { + return + Flowable.range(0, (int)elements).subscribeOn(Schedulers.single()) + ; + } +} diff --git a/src/test/java/io/reactivex/rxjava3/tck/SwitchIfEmptyTckTest.java b/src/test/java/io/reactivex/rxjava3/tck/SwitchIfEmptyTckTest.java new file mode 100644 index 0000000000..eb792e45f0 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/tck/SwitchIfEmptyTckTest.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.tck; + +import org.reactivestreams.Publisher; +import org.testng.annotations.Test; + +import io.reactivex.rxjava3.core.Flowable; + +@Test +public class SwitchIfEmptyTckTest extends BaseTck<Integer> { + + @Override + public Publisher<Integer> createPublisher(long elements) { + return + Flowable.<Integer>empty().switchIfEmpty(Flowable.range(1, (int)elements)) + ; + } +} diff --git a/src/test/java/io/reactivex/rxjava3/tck/SwitchMapDelayErrorTckTest.java b/src/test/java/io/reactivex/rxjava3/tck/SwitchMapDelayErrorTckTest.java new file mode 100644 index 0000000000..d9086e275d --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/tck/SwitchMapDelayErrorTckTest.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.tck; + +import org.reactivestreams.Publisher; +import org.testng.annotations.Test; + +import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.internal.functions.Functions; + +@Test +public class SwitchMapDelayErrorTckTest extends BaseTck<Long> { + + @Override + public Publisher<Long> createPublisher(long elements) { + return + Flowable.just(1).switchMapDelayError(Functions.justFunction( + Flowable.fromIterable(iterate(elements))) + ) + ; + } +} diff --git a/src/test/java/io/reactivex/rxjava3/tck/SwitchMapTckTest.java b/src/test/java/io/reactivex/rxjava3/tck/SwitchMapTckTest.java new file mode 100644 index 0000000000..1d41a6d11b --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/tck/SwitchMapTckTest.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.tck; + +import org.reactivestreams.Publisher; +import org.testng.annotations.Test; + +import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.internal.functions.Functions; + +@Test +public class SwitchMapTckTest extends BaseTck<Long> { + + @Override + public Publisher<Long> createPublisher(long elements) { + return + Flowable.just(1).switchMap(Functions.justFunction( + Flowable.fromIterable(iterate(elements))) + ) + ; + } +} diff --git a/src/test/java/io/reactivex/rxjava3/tck/SwitchOnNextTckTest.java b/src/test/java/io/reactivex/rxjava3/tck/SwitchOnNextTckTest.java new file mode 100644 index 0000000000..196bf4f825 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/tck/SwitchOnNextTckTest.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.tck; + +import org.reactivestreams.Publisher; +import org.testng.annotations.Test; + +import io.reactivex.rxjava3.core.Flowable; + +@Test +public class SwitchOnNextTckTest extends BaseTck<Long> { + + @Override + public Publisher<Long> createPublisher(long elements) { + return + Flowable.switchOnNext(Flowable.just( + Flowable.fromIterable(iterate(elements))) + ) + ; + } +} diff --git a/src/test/java/io/reactivex/rxjava3/tck/TakeLastTckTest.java b/src/test/java/io/reactivex/rxjava3/tck/TakeLastTckTest.java new file mode 100644 index 0000000000..c737554363 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/tck/TakeLastTckTest.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.tck; + +import org.reactivestreams.Publisher; +import org.testng.annotations.Test; + +import io.reactivex.rxjava3.core.Flowable; + +@Test +public class TakeLastTckTest extends BaseTck<Integer> { + + @Override + public Publisher<Integer> createPublisher(long elements) { + return + Flowable.range(0, (int)elements * 2).takeLast((int)elements) + ; + } +} diff --git a/src/test/java/io/reactivex/rxjava3/tck/TakeTckTest.java b/src/test/java/io/reactivex/rxjava3/tck/TakeTckTest.java new file mode 100644 index 0000000000..50fe18caf1 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/tck/TakeTckTest.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.tck; + +import org.reactivestreams.Publisher; +import org.testng.annotations.Test; + +import io.reactivex.rxjava3.core.Flowable; + +@Test +public class TakeTckTest extends BaseTck<Integer> { + + @Override + public Publisher<Integer> createPublisher(long elements) { + return + Flowable.range(0, (int)elements * 2).take(elements) + ; + } +} diff --git a/src/test/java/io/reactivex/rxjava3/tck/TakeUntilTckTest.java b/src/test/java/io/reactivex/rxjava3/tck/TakeUntilTckTest.java new file mode 100644 index 0000000000..9695560615 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/tck/TakeUntilTckTest.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.tck; + +import org.reactivestreams.Publisher; +import org.testng.annotations.Test; + +import io.reactivex.rxjava3.core.Flowable; + +@Test +public class TakeUntilTckTest extends BaseTck<Integer> { + + @Override + public Publisher<Integer> createPublisher(long elements) { + return + Flowable.range(0, (int)elements).takeUntil(Flowable.never()) + ; + } +} diff --git a/src/test/java/io/reactivex/rxjava3/tck/TakeWhileTckTest.java b/src/test/java/io/reactivex/rxjava3/tck/TakeWhileTckTest.java new file mode 100644 index 0000000000..24f6bd4d0d --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/tck/TakeWhileTckTest.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.tck; + +import org.reactivestreams.Publisher; +import org.testng.annotations.Test; + +import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.internal.functions.Functions; + +@Test +public class TakeWhileTckTest extends BaseTck<Integer> { + + @Override + public Publisher<Integer> createPublisher(long elements) { + return + Flowable.range(0, (int)elements).takeWhile(Functions.alwaysTrue()) + ; + } +} diff --git a/src/test/java/io/reactivex/rxjava3/tck/TimeIntervalTckTest.java b/src/test/java/io/reactivex/rxjava3/tck/TimeIntervalTckTest.java new file mode 100644 index 0000000000..3daebf143a --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/tck/TimeIntervalTckTest.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.tck; + +import org.reactivestreams.Publisher; +import org.testng.annotations.Test; + +import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.schedulers.Timed; + +@Test +public class TimeIntervalTckTest extends BaseTck<Timed<Integer>> { + + @Override + public Publisher<Timed<Integer>> createPublisher(long elements) { + return + Flowable.range(0, (int)elements).timeInterval() + ; + } +} diff --git a/src/test/java/io/reactivex/rxjava3/tck/TimeoutTckTest.java b/src/test/java/io/reactivex/rxjava3/tck/TimeoutTckTest.java new file mode 100644 index 0000000000..447a60e951 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/tck/TimeoutTckTest.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.tck; + +import java.util.concurrent.TimeUnit; + +import org.reactivestreams.Publisher; +import org.testng.annotations.Test; + +import io.reactivex.rxjava3.core.Flowable; + +@Test +public class TimeoutTckTest extends BaseTck<Integer> { + + @Override + public Publisher<Integer> createPublisher(long elements) { + return + Flowable.range(0, (int)elements).timeout(1, TimeUnit.DAYS) + ; + } +} diff --git a/src/test/java/io/reactivex/rxjava3/tck/TimerTckTest.java b/src/test/java/io/reactivex/rxjava3/tck/TimerTckTest.java new file mode 100644 index 0000000000..4a6eb00005 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/tck/TimerTckTest.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.tck; + +import java.util.concurrent.TimeUnit; + +import org.reactivestreams.Publisher; +import org.testng.annotations.Test; + +import io.reactivex.rxjava3.core.Flowable; + +@Test +public class TimerTckTest extends BaseTck<Long> { + + @Override + public Publisher<Long> createPublisher(final long elements) { + return + Flowable.timer(1, TimeUnit.MILLISECONDS) + .onBackpressureLatest() + ; + } + + @Override + public long maxElementsFromPublisher() { + return 1; + } +} diff --git a/src/test/java/io/reactivex/rxjava3/tck/TimestampTckTest.java b/src/test/java/io/reactivex/rxjava3/tck/TimestampTckTest.java new file mode 100644 index 0000000000..48d34c8751 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/tck/TimestampTckTest.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.tck; + +import org.reactivestreams.Publisher; +import org.testng.annotations.Test; + +import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.schedulers.Timed; + +@Test +public class TimestampTckTest extends BaseTck<Timed<Integer>> { + + @Override + public Publisher<Timed<Integer>> createPublisher(long elements) { + return + Flowable.range(0, (int)elements).timestamp() + ; + } +} diff --git a/src/test/java/io/reactivex/rxjava3/tck/ToListTckTest.java b/src/test/java/io/reactivex/rxjava3/tck/ToListTckTest.java new file mode 100644 index 0000000000..e9d17f370f --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/tck/ToListTckTest.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.tck; + +import java.util.List; + +import org.reactivestreams.Publisher; +import org.testng.annotations.Test; + +import io.reactivex.rxjava3.core.Flowable; + +@Test +public class ToListTckTest extends BaseTck<List<Integer>> { + + @Override + public Publisher<List<Integer>> createPublisher(final long elements) { + return + Flowable.range(1, 1000).toList().toFlowable() + ; + } + + @Override + public long maxElementsFromPublisher() { + return 1; + } +} diff --git a/src/test/java/io/reactivex/rxjava3/tck/ToMapTckTest.java b/src/test/java/io/reactivex/rxjava3/tck/ToMapTckTest.java new file mode 100644 index 0000000000..ae27183ca0 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/tck/ToMapTckTest.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.tck; + +import java.util.Map; + +import org.reactivestreams.Publisher; +import org.testng.annotations.Test; + +import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.internal.functions.Functions; + +@Test +public class ToMapTckTest extends BaseTck<Map<Integer, Integer>> { + + @Override + public Publisher<Map<Integer, Integer>> createPublisher(final long elements) { + return + Flowable.range(1, 1000).toMap(Functions.<Integer>identity()).toFlowable() + ; + } + + @Override + public long maxElementsFromPublisher() { + return 1; + } +} diff --git a/src/test/java/io/reactivex/rxjava3/tck/ToMultimapTckTest.java b/src/test/java/io/reactivex/rxjava3/tck/ToMultimapTckTest.java new file mode 100644 index 0000000000..c0d65cbd33 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/tck/ToMultimapTckTest.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.tck; + +import java.util.*; + +import org.reactivestreams.Publisher; +import org.testng.annotations.Test; + +import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.internal.functions.Functions; + +@Test +public class ToMultimapTckTest extends BaseTck<Map<Integer, Collection<Integer>>> { + + @Override + public Publisher<Map<Integer, Collection<Integer>>> createPublisher(final long elements) { + return + Flowable.range(1, 1000).toMultimap(Functions.<Integer>identity()).toFlowable() + ; + } + + @Override + public long maxElementsFromPublisher() { + return 1; + } +} diff --git a/src/test/java/io/reactivex/rxjava3/tck/ToSortedListTckTest.java b/src/test/java/io/reactivex/rxjava3/tck/ToSortedListTckTest.java new file mode 100644 index 0000000000..11d1029fc2 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/tck/ToSortedListTckTest.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.tck; + +import java.util.List; + +import org.reactivestreams.Publisher; +import org.testng.annotations.Test; + +import io.reactivex.rxjava3.core.Flowable; + +@Test +public class ToSortedListTckTest extends BaseTck<List<Integer>> { + + @Override + public Publisher<List<Integer>> createPublisher(final long elements) { + return + Flowable.range(1, 1000).toSortedList().toFlowable() + ; + } + + @Override + public long maxElementsFromPublisher() { + return 1; + } +} diff --git a/src/test/java/io/reactivex/rxjava3/tck/UnicastProcessorAsPublisherTckTest.java b/src/test/java/io/reactivex/rxjava3/tck/UnicastProcessorAsPublisherTckTest.java new file mode 100644 index 0000000000..a888ce35fa --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/tck/UnicastProcessorAsPublisherTckTest.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.tck; + +import org.reactivestreams.Publisher; +import org.testng.annotations.Test; + +import io.reactivex.rxjava3.processors.UnicastProcessor; +import io.reactivex.rxjava3.schedulers.Schedulers; + +@Test +public class UnicastProcessorAsPublisherTckTest extends BaseTck<Integer> { + + public UnicastProcessorAsPublisherTckTest() { + super(100); + } + + @Override + public Publisher<Integer> createPublisher(final long elements) { + final UnicastProcessor<Integer> pp = UnicastProcessor.create(); + + Schedulers.io().scheduleDirect(new Runnable() { + @Override + public void run() { + long start = System.currentTimeMillis(); + while (!pp.hasSubscribers()) { + try { + Thread.sleep(1); + } catch (InterruptedException ex) { + return; + } + + if (System.currentTimeMillis() - start > 200) { + return; + } + } + + for (int i = 0; i < elements; i++) { + pp.onNext(i); + } + pp.onComplete(); + } + }); + return pp; + } +} diff --git a/src/test/java/io/reactivex/rxjava3/tck/UnicastProcessorTckTest.java b/src/test/java/io/reactivex/rxjava3/tck/UnicastProcessorTckTest.java new file mode 100644 index 0000000000..44bcc5a142 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/tck/UnicastProcessorTckTest.java @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.tck; + +import java.util.concurrent.*; + +import org.reactivestreams.*; +import org.reactivestreams.tck.*; +import org.testng.annotations.Test; + +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.processors.UnicastProcessor; + +@Test +public class UnicastProcessorTckTest extends IdentityProcessorVerification<Integer> { + + public UnicastProcessorTckTest() { + super(new TestEnvironment(50)); + } + + @Override + public Processor<Integer, Integer> createIdentityProcessor(int bufferSize) { + UnicastProcessor<Integer> up = UnicastProcessor.create(); + return new RefCountProcessor<>(up); + } + + @Override + public Publisher<Integer> createFailedPublisher() { + UnicastProcessor<Integer> up = UnicastProcessor.create(); + up.onError(new TestException()); + return up; + } + + @Override + public ExecutorService publisherExecutorService() { + return Executors.newCachedThreadPool(); + } + + @Override + public Integer createElement(int element) { + return element; + } + + @Override + public long maxSupportedSubscribers() { + return 1; + } + + @Override + public long maxElementsFromPublisher() { + return 1024; + } +} diff --git a/src/test/java/io/reactivex/rxjava3/tck/UnsubscribeOnTckTest.java b/src/test/java/io/reactivex/rxjava3/tck/UnsubscribeOnTckTest.java new file mode 100644 index 0000000000..1ab8398b74 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/tck/UnsubscribeOnTckTest.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.tck; + +import org.reactivestreams.Publisher; +import org.testng.annotations.Test; + +import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.schedulers.Schedulers; + +@Test +public class UnsubscribeOnTckTest extends BaseTck<Integer> { + + @Override + public Publisher<Integer> createPublisher(long elements) { + return + Flowable.range(0, (int)elements * 2) + .unsubscribeOn(Schedulers.single()) + .take(elements) + ; + } +} diff --git a/src/test/java/io/reactivex/rxjava3/tck/UsingTckTest.java b/src/test/java/io/reactivex/rxjava3/tck/UsingTckTest.java new file mode 100644 index 0000000000..03460fdaba --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/tck/UsingTckTest.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.tck; + +import org.reactivestreams.Publisher; +import org.testng.annotations.Test; + +import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.internal.functions.Functions; + +@Test +public class UsingTckTest extends BaseTck<Long> { + + @Override + public Publisher<Long> createPublisher(long elements) { + return + Flowable.using(Functions.justSupplier(1), + Functions.justFunction(Flowable.fromIterable(iterate(elements))), + Functions.emptyConsumer() + ) + ; + } +} diff --git a/src/test/java/io/reactivex/rxjava3/tck/WindowBoundaryTckTest.java b/src/test/java/io/reactivex/rxjava3/tck/WindowBoundaryTckTest.java new file mode 100644 index 0000000000..5b01ad696c --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/tck/WindowBoundaryTckTest.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.tck; + +import java.util.List; + +import org.reactivestreams.Publisher; +import org.testng.annotations.Test; + +import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.functions.Functions; + +@Test +public class WindowBoundaryTckTest extends BaseTck<List<Long>> { + + @SuppressWarnings({ "rawtypes", "unchecked" }) + @Override + public Publisher<List<Long>> createPublisher(long elements) { + return + Flowable.fromIterable(iterate(elements)) + .window(Flowable.just(1).concatWith(Flowable.<Integer>never())) + .onBackpressureBuffer() + .flatMap((Function)Functions.identity()) + ; + } +} diff --git a/src/test/java/io/reactivex/rxjava3/tck/WindowExactSizeTckTest.java b/src/test/java/io/reactivex/rxjava3/tck/WindowExactSizeTckTest.java new file mode 100644 index 0000000000..dae3bf0c45 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/tck/WindowExactSizeTckTest.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.tck; + +import java.util.List; + +import org.reactivestreams.Publisher; +import org.testng.annotations.Test; + +import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.functions.Function; +import io.reactivex.rxjava3.internal.functions.Functions; + +@Test +public class WindowExactSizeTckTest extends BaseTck<List<Long>> { + + @SuppressWarnings({ "rawtypes", "unchecked" }) + @Override + public Publisher<List<Long>> createPublisher(long elements) { + return + Flowable.fromIterable(iterate(elements)) + .window(2) + .flatMap((Function)Functions.identity()) + ; + } +} diff --git a/src/test/java/io/reactivex/rxjava3/tck/WithLatestFromTckTest.java b/src/test/java/io/reactivex/rxjava3/tck/WithLatestFromTckTest.java new file mode 100644 index 0000000000..ae7cddb7f9 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/tck/WithLatestFromTckTest.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.tck; + +import org.reactivestreams.Publisher; +import org.testng.annotations.Test; + +import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.functions.BiFunction; + +@Test +public class WithLatestFromTckTest extends BaseTck<Integer> { + + @Override + public Publisher<Integer> createPublisher(long elements) { + return + Flowable.range(0, (int)elements) + .withLatestFrom(Flowable.just(1), new BiFunction<Integer, Integer, Integer>() { + @Override + public Integer apply(Integer a, Integer b) throws Exception { + return a + b; + } + }) + ; + } +} diff --git a/src/test/java/io/reactivex/rxjava3/tck/ZipIterableTckTest.java b/src/test/java/io/reactivex/rxjava3/tck/ZipIterableTckTest.java new file mode 100644 index 0000000000..9dae4bcd9a --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/tck/ZipIterableTckTest.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.tck; + +import java.util.Arrays; + +import org.reactivestreams.Publisher; +import org.testng.annotations.Test; + +import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.functions.Function; + +@Test +public class ZipIterableTckTest extends BaseTck<Long> { + + @Override + public Publisher<Long> createPublisher(long elements) { + return + Flowable.zip(Arrays.asList( + Flowable.fromIterable(iterate(elements)), + Flowable.fromIterable(iterate(elements)) + ), + new Function<Object[], Long>() { + @Override + public Long apply(Object[] a) throws Exception { + return (Long)a[0] + (Long)a[1]; + } + } + ) + ; + } +} diff --git a/src/test/java/io/reactivex/rxjava3/tck/ZipTckTest.java b/src/test/java/io/reactivex/rxjava3/tck/ZipTckTest.java new file mode 100644 index 0000000000..c9427876d8 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/tck/ZipTckTest.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.tck; + +import org.reactivestreams.Publisher; +import org.testng.annotations.Test; + +import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.functions.BiFunction; + +@Test +public class ZipTckTest extends BaseTck<Long> { + + @Override + public Publisher<Long> createPublisher(long elements) { + return + Flowable.zip( + Flowable.fromIterable(iterate(elements)), + Flowable.fromIterable(iterate(elements)), + new BiFunction<Long, Long, Long>() { + @Override + public Long apply(Long a, Long b) throws Exception { + return a + b; + } + } + ) + ; + } +} diff --git a/src/test/java/io/reactivex/rxjava3/tck/ZipWithIterableTckTest.java b/src/test/java/io/reactivex/rxjava3/tck/ZipWithIterableTckTest.java new file mode 100644 index 0000000000..6a4cde43f5 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/tck/ZipWithIterableTckTest.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.tck; + +import org.reactivestreams.Publisher; +import org.testng.annotations.Test; + +import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.functions.BiFunction; + +@Test +public class ZipWithIterableTckTest extends BaseTck<Integer> { + + @Override + public Publisher<Integer> createPublisher(long elements) { + return + Flowable.range(0, (int)elements) + .zipWith(iterate(elements), new BiFunction<Integer, Long, Integer>() { + @Override + public Integer apply(Integer a, Long b) throws Exception { + return a + b.intValue(); + } + }) + ; + } +} diff --git a/src/test/java/io/reactivex/rxjava3/tck/ZipWithTckTest.java b/src/test/java/io/reactivex/rxjava3/tck/ZipWithTckTest.java new file mode 100644 index 0000000000..6992605ad8 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/tck/ZipWithTckTest.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.tck; + +import org.reactivestreams.Publisher; +import org.testng.annotations.Test; + +import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.functions.BiFunction; + +@Test +public class ZipWithTckTest extends BaseTck<Integer> { + + @Override + public Publisher<Integer> createPublisher(long elements) { + return + Flowable.range(0, (int)elements) + .zipWith(Flowable.range((int)elements, (int)elements), new BiFunction<Integer, Integer, Integer>() { + @Override + public Integer apply(Integer a, Integer b) throws Exception { + return a + b; + } + }) + ; + } +} diff --git a/src/test/java/io/reactivex/rxjava3/testsupport/BaseTestConsumerEx.java b/src/test/java/io/reactivex/rxjava3/testsupport/BaseTestConsumerEx.java new file mode 100644 index 0000000000..47016f1fa5 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/testsupport/BaseTestConsumerEx.java @@ -0,0 +1,307 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.testsupport; + +import java.util.List; + +import io.reactivex.rxjava3.functions.Predicate; +import io.reactivex.rxjava3.internal.util.ExceptionHelper; +import io.reactivex.rxjava3.observers.BaseTestConsumer; +import io.reactivex.rxjava3.operators.QueueFuseable; + +import java.util.Objects; + +/** + * Base class with shared infrastructure to support TestSubscriber and TestObserver. + * @param <T> the value type consumed + * @param <U> the subclass of this BaseTestConsumer + */ +public abstract class BaseTestConsumerEx<T, U extends BaseTestConsumerEx<T, U>> +extends BaseTestConsumer<T, U> { + + protected int initialFusionMode; + + protected int establishedFusionMode; + + /** + * The optional tag associated with this test consumer. + * @since 2.0.7 + */ + protected CharSequence tag; + + /** + * Indicates that one of the awaitX method has timed out. + * @since 2.0.7 + */ + protected boolean timeout; + + public BaseTestConsumerEx() { + super(); + } + + /** + * Returns the last thread which called the onXXX methods of this TestObserver/TestSubscriber. + * @return the last thread which called the onXXX methods + */ + public final Thread lastThread() { + return lastThread; + } + + // assertion methods + + /** + * Assert that this TestObserver/TestSubscriber did not receive an onNext value which is equal to + * the given value with respect to null-safe Object.equals. + * + * <p>History: 2.0.5 - experimental + * @param value the value to expect not being received + * @return this + * @since 2.1 + */ + @SuppressWarnings("unchecked") + public final U assertNever(T value) { + int s = values.size(); + + for (int i = 0; i < s; i++) { + T v = this.values.get(i); + if (Objects.equals(v, value)) { + throw fail("Value at position " + i + " is equal to " + valueAndClass(value) + "; Expected them to be different"); + } + } + return (U) this; + } + + /** + * Asserts that this TestObserver/TestSubscriber did not receive any onNext value for which + * the provided predicate returns true. + * + * <p>History: 2.0.5 - experimental + * @param valuePredicate the predicate that receives the onNext value + * and should return true for the expected value. + * @return this + * @since 2.1 + */ + @SuppressWarnings("unchecked") + public final U assertNever(Predicate<? super T> valuePredicate) { + int s = values.size(); + + for (int i = 0; i < s; i++) { + T v = this.values.get(i); + try { + if (valuePredicate.test(v)) { + throw fail("Value at position " + i + " matches predicate " + valuePredicate.toString() + ", which was not expected."); + } + } catch (Throwable ex) { + throw ExceptionHelper.wrapOrThrow(ex); + } + } + return (U)this; + } + + /** + * Assert that the TestObserver/TestSubscriber terminated (i.e., the terminal latch reached zero). + * @return this + */ + @SuppressWarnings("unchecked") + public final U assertTerminated() { + if (done.getCount() != 0) { + throw fail("Subscriber still running!"); + } + long c = completions; + if (c > 1) { + throw fail("Terminated with multiple completions: " + c); + } + int s = errors.size(); + if (s > 1) { + throw fail("Terminated with multiple errors: " + s); + } + + if (c != 0 && s != 0) { + throw fail("Terminated with multiple completions and errors: " + c); + } + return (U)this; + } + + /** + * Assert that the TestObserver/TestSubscriber has not terminated (i.e., the terminal latch is still non-zero). + * @return this + */ + @SuppressWarnings("unchecked") + public final U assertNotTerminated() { + if (done.getCount() == 0) { + throw fail("Subscriber terminated!"); + } + return (U)this; + } + + /** + * Assert that there is a single error and it has the given message. + * @param message the message expected + * @return this + */ + @SuppressWarnings("unchecked") + public final U assertErrorMessage(String message) { + int s = errors.size(); + if (s == 0) { + throw fail("No errors"); + } else + if (s == 1) { + Throwable e = errors.get(0); + String errorMessage = e.getMessage(); + if (!Objects.equals(message, errorMessage)) { + throw fail("\nexpected: " + message + "\ngot: " + errorMessage + + "; Error message differs"); + } + } else { + throw fail("Multiple errors"); + } + return (U)this; + } + + /** + * Assert that the upstream signalled the specified values in order and then failed + * with a Throwable for which the provided predicate returns true. + * @param errorPredicate + * the predicate that receives the error Throwable + * and should return true for expected errors. + * @param values the expected values, asserted in order + * @return this + */ + @SafeVarargs + public final U assertFailure(Predicate<Throwable> errorPredicate, T... values) { + return assertSubscribed() + .assertValues(values) + .assertError(errorPredicate) + .assertNotComplete(); + } + + /** + * Assert that the upstream signalled the specified values in order, + * then failed with a specific class or subclass of Throwable + * and with the given exact error message. + * @param error the expected exception (parent) class + * @param message the expected failure message + * @param values the expected values, asserted in order + * @return this + */ + @SafeVarargs + public final U assertFailureAndMessage(Class<? extends Throwable> error, + String message, T... values) { + return assertSubscribed() + .assertValues(values) + .assertError(error) + .assertErrorMessage(message) + .assertNotComplete(); + } + + /** + * Returns true if an await timed out. + * @return true if one of the timeout-based await methods has timed out. + * <p>History: 2.0.7 - experimental + * @see #clearTimeout() + * @see #assertTimeout() + * @see #assertNoTimeout() + * @since 2.1 + */ + public final boolean isTimeout() { + return timeout; + } + + /** + * Clears the timeout flag set by the await methods when they timed out. + * <p>History: 2.0.7 - experimental + * @return this + * @since 2.1 + * @see #isTimeout() + */ + @SuppressWarnings("unchecked") + public final U clearTimeout() { + timeout = false; + return (U)this; + } + + /** + * Asserts that some awaitX method has timed out. + * <p>History: 2.0.7 - experimental + * @return this + * @since 2.1 + */ + @SuppressWarnings("unchecked") + public final U assertTimeout() { + if (!timeout) { + throw fail("No timeout?!"); + } + return (U)this; + } + + /** + * Asserts that some awaitX method has not timed out. + * <p>History: 2.0.7 - experimental + * @return this + * @since 2.1 + */ + @SuppressWarnings("unchecked") + public final U assertNoTimeout() { + if (timeout) { + throw fail("Timeout?!"); + } + return (U)this; + } + + /** + * Returns the internal shared list of errors. + * @return Returns the internal shared list of errors. + */ + public final List<Throwable> errors() { + return errors; + } + + /** + * Returns true if this test consumer has terminated in any fashion. + * @return true if this test consumer has terminated in any fashion + */ + public final boolean isTerminated() { + return done.getCount() == 0; + } + + /** + * Returns the number of times onComplete() was called. + * @return the number of times onComplete() was called + */ + public final long completions() { + return completions; + } + + /** + * Fail with the given message and add the sequence of errors as suppressed ones. + * <p>Note this is deliberately the only fail method. Most of the times an assertion + * would fail but it is possible it was due to an exception somewhere. This construct + * will capture those potential errors and report it along with the original failure. + * + * @param message the message to use + * @return AssertionError the prepared AssertionError instance + */ + public final AssertionError failWith(String message) { + return fail(message); + } + + static String fusionModeToString(int mode) { + switch (mode) { + case QueueFuseable.NONE : return "NONE"; + case QueueFuseable.SYNC : return "SYNC"; + case QueueFuseable.ASYNC : return "ASYNC"; + default: return "Unknown(" + mode + ")"; + } + } +} diff --git a/src/test/java/io/reactivex/rxjava3/testsupport/SuppressUndeliverable.java b/src/test/java/io/reactivex/rxjava3/testsupport/SuppressUndeliverable.java new file mode 100644 index 0000000000..4e701137ab --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/testsupport/SuppressUndeliverable.java @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.testsupport; + +import java.lang.annotation.*; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface SuppressUndeliverable { +} diff --git a/src/test/java/io/reactivex/rxjava3/testsupport/SuppressUndeliverableRule.java b/src/test/java/io/reactivex/rxjava3/testsupport/SuppressUndeliverableRule.java new file mode 100644 index 0000000000..28f6d2f89e --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/testsupport/SuppressUndeliverableRule.java @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.testsupport; + +import org.junit.rules.TestRule; +import org.junit.runner.Description; +import org.junit.runners.model.Statement; + +import io.reactivex.rxjava3.exceptions.UndeliverableException; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; + +/** + * A rule for suppressing UndeliverableException handling. + * + * <p>Test classes that use this rule can suppress UndeliverableException + * handling by annotating the test method with SuppressUndeliverable. + */ +public class SuppressUndeliverableRule implements TestRule { + + static final class SuppressUndeliverableRuleStatement extends Statement { + private Statement base; + + SuppressUndeliverableRuleStatement(Statement base) { + this.base = base; + } + + @Override + public void evaluate() throws Throwable { + try { + RxJavaPlugins.setErrorHandler(throwable -> { + if (!(throwable instanceof UndeliverableException)) { + throwable.printStackTrace(); + Thread currentThread = Thread.currentThread(); + currentThread.getUncaughtExceptionHandler().uncaughtException(currentThread, throwable); + } + }); + base.evaluate(); + } finally { + RxJavaPlugins.setErrorHandler(null); + } + } + } + + @Override + public Statement apply(Statement base, Description description) { + if (description != null && description.getAnnotation(SuppressUndeliverable.class) != null) { + return new SuppressUndeliverableRuleStatement(base); + } else { + return base; + } + } +} diff --git a/src/test/java/io/reactivex/rxjava3/testsupport/TestHelper.java b/src/test/java/io/reactivex/rxjava3/testsupport/TestHelper.java new file mode 100644 index 0000000000..d38ad8f998 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/testsupport/TestHelper.java @@ -0,0 +1,3863 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.testsupport; + +import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; + +import java.io.File; +import java.lang.management.*; +import java.lang.reflect.*; +import java.net.URL; +import java.util.*; +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicInteger; + +import org.mockito.Mockito; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; +import org.reactivestreams.*; + +import io.reactivex.rxjava3.annotations.*; +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.core.Observer; +import io.reactivex.rxjava3.disposables.*; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.operators.completable.CompletableToFlowable; +import io.reactivex.rxjava3.internal.operators.maybe.MaybeToFlowable; +import io.reactivex.rxjava3.internal.operators.single.SingleToFlowable; +import io.reactivex.rxjava3.internal.subscriptions.*; +import io.reactivex.rxjava3.internal.util.ExceptionHelper; +import io.reactivex.rxjava3.observers.BaseTestConsumer; +import io.reactivex.rxjava3.operators.ConditionalSubscriber; +import io.reactivex.rxjava3.operators.QueueDisposable; +import io.reactivex.rxjava3.operators.QueueFuseable; +import io.reactivex.rxjava3.operators.QueueSubscription; +import io.reactivex.rxjava3.operators.SimpleQueue; +import io.reactivex.rxjava3.parallel.ParallelFlowable; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.processors.PublishProcessor; +import io.reactivex.rxjava3.schedulers.Schedulers; +import io.reactivex.rxjava3.subjects.Subject; +import io.reactivex.rxjava3.subscribers.TestSubscriber; + +/** + * Common methods for helping with tests. + */ +public enum TestHelper { + ; + + /** + * Number of times to loop a {@link #race(Runnable, Runnable)} invocation + * by default. + */ + public static final int RACE_DEFAULT_LOOPS = 2500; + + /** + * Number of times to loop a {@link #race(Runnable, Runnable)} invocation + * in tests with race conditions requiring more runs to check. + */ + public static final int RACE_LONG_LOOPS = 10000; + + /** + * Mocks a subscriber and prepares it to request {@link Long#MAX_VALUE}. + * @param <T> the value type + * @return the mocked subscriber + */ + @SuppressWarnings("unchecked") + public static <T> FlowableSubscriber<T> mockSubscriber() { + FlowableSubscriber<T> w = mock(FlowableSubscriber.class); + + Mockito.doAnswer(new Answer<Object>() { + @Override + public Object answer(InvocationOnMock a) throws Throwable { + Subscription s = a.getArgument(0); + s.request(Long.MAX_VALUE); + return null; + } + }).when(w).onSubscribe((Subscription)any()); + + return w; + } + + /** + * Mocks an Observer with the proper receiver type. + * @param <T> the value type + * @return the mocked observer + */ + @SuppressWarnings("unchecked") + public static <T> Observer<T> mockObserver() { + return mock(Observer.class); + } + + /** + * Mocks an MaybeObserver with the proper receiver type. + * @param <T> the value type + * @return the mocked observer + */ + @SuppressWarnings("unchecked") + public static <T> MaybeObserver<T> mockMaybeObserver() { + return mock(MaybeObserver.class); + } + + /** + * Mocks an SingleObserver with the proper receiver type. + * @param <T> the value type + * @return the mocked observer + */ + @SuppressWarnings("unchecked") + public static <T> SingleObserver<T> mockSingleObserver() { + return mock(SingleObserver.class); + } + + /** + * Mocks an CompletableObserver. + * @return the mocked observer + */ + public static CompletableObserver mockCompletableObserver() { + return mock(CompletableObserver.class); + } + + /** + * Validates that the given class, when forcefully instantiated throws + * an IllegalArgumentException("No instances!") exception. + * @param clazz the class to test, not null + */ + public static void checkUtilityClass(Class<?> clazz) { + try { + Constructor<?> c = clazz.getDeclaredConstructor(); + + c.setAccessible(true); + + try { + c.newInstance(); + fail("Should have thrown InvocationTargetException(IllegalStateException)"); + } catch (InvocationTargetException ex) { + assertEquals("No instances!", ex.getCause().getMessage()); + } + } catch (Exception ex) { + AssertionError ae = new AssertionError(ex.toString()); + ae.initCause(ex); + throw ae; + } + } + + public static List<Throwable> trackPluginErrors() { + final List<Throwable> list = Collections.synchronizedList(new ArrayList<>()); + + RxJavaPlugins.setErrorHandler(new Consumer<Throwable>() { + @Override + public void accept(Throwable t) { + list.add(t); + } + }); + + return list; + } + + public static void assertError(List<Throwable> list, int index, Class<? extends Throwable> clazz) { + Throwable ex = list.get(index); + if (!clazz.isInstance(ex)) { + AssertionError err = new AssertionError(clazz + " expected but got " + list.get(index)); + err.initCause(list.get(index)); + throw err; + } + } + + public static void assertUndeliverable(List<Throwable> list, int index, Class<? extends Throwable> clazz) { + Throwable ex = list.get(index); + if (!(ex instanceof UndeliverableException)) { + AssertionError err = new AssertionError("Outer exception UndeliverableException expected but got " + list.get(index)); + err.initCause(list.get(index)); + throw err; + } + ex = ex.getCause(); + if (!clazz.isInstance(ex)) { + AssertionError err = new AssertionError("Inner exception " + clazz + " expected but got " + list.get(index)); + err.initCause(list.get(index)); + throw err; + } + } + + public static void assertError(List<Throwable> list, int index, Class<? extends Throwable> clazz, String message) { + Throwable ex = list.get(index); + if (!clazz.isInstance(ex)) { + AssertionError err = new AssertionError("Type " + clazz + " expected but got " + ex); + err.initCause(ex); + throw err; + } + if (!Objects.equals(message, ex.getMessage())) { + AssertionError err = new AssertionError("Message " + message + " expected but got " + ex.getMessage()); + err.initCause(ex); + throw err; + } + } + + public static void assertUndeliverable(List<Throwable> list, int index, Class<? extends Throwable> clazz, String message) { + Throwable ex = list.get(index); + if (!(ex instanceof UndeliverableException)) { + AssertionError err = new AssertionError("Outer exception UndeliverableException expected but got " + list.get(index)); + err.initCause(list.get(index)); + throw err; + } + ex = ex.getCause(); + if (!clazz.isInstance(ex)) { + AssertionError err = new AssertionError("Inner exception " + clazz + " expected but got " + list.get(index)); + err.initCause(list.get(index)); + throw err; + } + if (!Objects.equals(message, ex.getMessage())) { + AssertionError err = new AssertionError("Message " + message + " expected but got " + ex.getMessage()); + err.initCause(ex); + throw err; + } + } + + public static void assertError(TestObserverEx<?> to, int index, Class<? extends Throwable> clazz) { + Throwable ex = to.errors().get(0); + try { + if (ex instanceof CompositeException) { + CompositeException ce = (CompositeException) ex; + List<Throwable> cel = ce.getExceptions(); + assertTrue(cel.get(index).toString(), clazz.isInstance(cel.get(index))); + } else { + fail(ex.toString() + ": not a CompositeException"); + } + } catch (AssertionError e) { + ex.printStackTrace(); + throw e; + } + } + + public static void assertError(TestSubscriberEx<?> ts, int index, Class<? extends Throwable> clazz) { + Throwable ex = ts.errors().get(0); + if (ex instanceof CompositeException) { + CompositeException ce = (CompositeException) ex; + List<Throwable> cel = ce.getExceptions(); + assertTrue(cel.get(index).toString(), clazz.isInstance(cel.get(index))); + } else { + fail(ex.toString() + ": not a CompositeException"); + } + } + + public static void assertError(TestObserverEx<?> to, int index, Class<? extends Throwable> clazz, String message) { + Throwable ex = to.errors().get(0); + if (ex instanceof CompositeException) { + CompositeException ce = (CompositeException) ex; + List<Throwable> cel = ce.getExceptions(); + assertTrue(cel.get(index).toString(), clazz.isInstance(cel.get(index))); + assertEquals(message, cel.get(index).getMessage()); + } else { + fail(ex.toString() + ": not a CompositeException"); + } + } + + public static void assertError(TestSubscriberEx<?> ts, int index, Class<? extends Throwable> clazz, String message) { + Throwable ex = ts.errors().get(0); + if (ex instanceof CompositeException) { + CompositeException ce = (CompositeException) ex; + List<Throwable> cel = ce.getExceptions(); + assertTrue(cel.get(index).toString(), clazz.isInstance(cel.get(index))); + assertEquals(message, cel.get(index).getMessage()); + } else { + fail(ex.toString() + ": not a CompositeException"); + } + } + + /** + * Verify that a specific enum type has no enum constants. + * @param <E> the enum type + * @param e the enum class instance + */ + public static <E extends Enum<E>> void assertEmptyEnum(Class<E> e) { + assertEquals(0, e.getEnumConstants().length); + + try { + try { + Method m0 = e.getDeclaredMethod("values"); + + Object[] a = (Object[])m0.invoke(null); + assertEquals(0, a.length); + + Method m = e.getDeclaredMethod("valueOf", String.class); + + m.invoke("INSTANCE"); + fail("Should have thrown!"); + } catch (InvocationTargetException ex) { + fail(ex.toString()); + } catch (IllegalAccessException ex) { + fail(ex.toString()); + } catch (IllegalArgumentException ex) { + // we expected this + } + } catch (NoSuchMethodException ex) { + fail(ex.toString()); + } + } + + /** + * Assert that by consuming the Publisher with a bad request amount, it is + * reported to the plugin error handler promptly. + * @param source the source to consume + */ + public static void assertBadRequestReported(Publisher<?> source) { + List<Throwable> list = trackPluginErrors(); + try { + final CountDownLatch cdl = new CountDownLatch(1); + + source.subscribe(new FlowableSubscriber<Object>() { + + @Override + public void onSubscribe(Subscription s) { + try { + s.request(-99); + s.cancel(); + s.cancel(); + } finally { + cdl.countDown(); + } + } + + @Override + public void onNext(Object t) { + + } + + @Override + public void onError(Throwable t) { + + } + + @Override + public void onComplete() { + + } + + }); + + try { + assertTrue(cdl.await(5, TimeUnit.SECONDS)); + } catch (InterruptedException ex) { + throw new AssertionError(ex.getMessage()); + } + + assertTrue(list.toString(), list.get(0) instanceof IllegalArgumentException); + assertEquals("n > 0 required but it was -99", list.get(0).getMessage()); + } finally { + RxJavaPlugins.setErrorHandler(null); + } + } + + /** + * Assert that by consuming the Publisher with a bad request amount, it is + * reported to the plugin error handler promptly. + * @param source the source to consume + */ + public static void assertBadRequestReported(ParallelFlowable<?> source) { + List<Throwable> list = trackPluginErrors(); + try { + final CountDownLatch cdl = new CountDownLatch(1); + + FlowableSubscriber<Object> bad = new FlowableSubscriber<Object>() { + + @Override + public void onSubscribe(Subscription s) { + try { + s.request(-99); + s.cancel(); + s.cancel(); + } finally { + cdl.countDown(); + } + } + + @Override + public void onNext(Object t) { + + } + + @Override + public void onError(Throwable t) { + + } + + @Override + public void onComplete() { + + } + + }; + + @SuppressWarnings("unchecked") + FlowableSubscriber<Object>[] subs = new FlowableSubscriber[source.parallelism()]; + subs[0] = bad; + for (int i = 1; i < subs.length; i++) { + subs[i] = NoOpConsumer.INSTANCE; + } + source.subscribe(subs); + + try { + assertTrue(cdl.await(5, TimeUnit.SECONDS)); + } catch (InterruptedException ex) { + throw new AssertionError(ex.getMessage()); + } + + assertTrue(list.toString(), list.get(0) instanceof IllegalArgumentException); + assertEquals("n > 0 required but it was -99", list.get(0).getMessage()); + } finally { + RxJavaPlugins.setErrorHandler(null); + } + } + + /** + * Synchronizes the execution of two runnables (as much as possible) + * to test race conditions. + * <p>The method blocks until both have run to completion. + * @param r1 the first runnable + * @param r2 the second runnable + * @see #RACE_DEFAULT_LOOPS + * @see #RACE_LONG_LOOPS + */ + public static void race(final Runnable r1, final Runnable r2) { + race(r1, r2, Schedulers.single()); + } + /** + * Synchronizes the execution of two runnables (as much as possible) + * to test race conditions. + * <p>The method blocks until both have run to completion. + * @param r1 the first runnable + * @param r2 the second runnable + * @param s the scheduler to use + * @see #RACE_DEFAULT_LOOPS + * @see #RACE_LONG_LOOPS + */ + public static void race(final Runnable r1, final Runnable r2, Scheduler s) { + final AtomicInteger count = new AtomicInteger(2); + final CountDownLatch cdl = new CountDownLatch(2); + + final Throwable[] errors = { null, null }; + + s.scheduleDirect(new Runnable() { + @Override + public void run() { + if (count.decrementAndGet() != 0) { + while (count.get() != 0) { } + } + + try { + try { + r1.run(); + } catch (Throwable ex) { + errors[0] = ex; + } + } finally { + cdl.countDown(); + } + } + }); + + if (count.decrementAndGet() != 0) { + while (count.get() != 0) { } + } + + try { + try { + r2.run(); + } catch (Throwable ex) { + errors[1] = ex; + } + } finally { + cdl.countDown(); + } + + try { + if (!cdl.await(5, TimeUnit.SECONDS)) { + throw new AssertionError("The wait timed out!"); + } + } catch (InterruptedException ex) { + throw new RuntimeException(ex); + } + if (errors[0] != null && errors[1] == null) { + throw ExceptionHelper.wrapOrThrow(errors[0]); + } + + if (errors[0] == null && errors[1] != null) { + throw ExceptionHelper.wrapOrThrow(errors[1]); + } + + if (errors[0] != null && errors[1] != null) { + throw new CompositeException(errors); + } + } + + /** + * Cast the given Throwable to CompositeException and returns its inner + * Throwable list. + * @param ex the target Throwable + * @return the list of Throwables + */ + public static List<Throwable> compositeList(Throwable ex) { + if (ex instanceof UndeliverableException) { + ex = ex.getCause(); + } + return ((CompositeException)ex).getExceptions(); + } + + /** + * Assert that the offer methods throw UnsupportedOperationExcetpion. + * @param q the queue implementation + */ + public static void assertNoOffer(SimpleQueue<?> q) { + try { + q.offer(null); + fail("Should have thrown!"); + } catch (UnsupportedOperationException ex) { + // expected + } + try { + q.offer(null, null); + fail("Should have thrown!"); + } catch (UnsupportedOperationException ex) { + // expected + } + } + + @SuppressWarnings("unchecked") + public static <E extends Enum<E>> void checkEnum(Class<E> enumClass) { + try { + Method m = enumClass.getMethod("values"); + m.setAccessible(true); + Method e = enumClass.getMethod("valueOf", String.class); + m.setAccessible(true); + + for (Enum<E> o : (Enum<E>[])m.invoke(null)) { + assertSame(o, e.invoke(null, o.name())); + } + + } catch (Throwable ex) { + throw ExceptionHelper.wrapOrThrow(ex); + } + } + + /** + * Calls onSubscribe twice and checks if it doesn't affect the first Subscription while + * reporting it to plugin error handler. + * @param subscriber the target + */ + public static void doubleOnSubscribe(Subscriber<?> subscriber) { + List<Throwable> errors = trackPluginErrors(); + try { + BooleanSubscription s1 = new BooleanSubscription(); + + subscriber.onSubscribe(s1); + + BooleanSubscription s2 = new BooleanSubscription(); + + subscriber.onSubscribe(s2); + + assertFalse(s1.isCancelled()); + + assertTrue(s2.isCancelled()); + + assertError(errors, 0, IllegalStateException.class, "Subscription already set!"); + } finally { + RxJavaPlugins.reset(); + } + } + + /** + * Calls onSubscribe twice and checks if it doesn't affect the first Disposable while + * reporting it to plugin error handler. + * @param observer the target + */ + public static void doubleOnSubscribe(Observer<?> observer) { + List<Throwable> errors = trackPluginErrors(); + try { + Disposable d1 = Disposable.empty(); + + observer.onSubscribe(d1); + + Disposable d2 = Disposable.empty(); + + observer.onSubscribe(d2); + + assertFalse(d1.isDisposed()); + + assertTrue(d2.isDisposed()); + + assertError(errors, 0, IllegalStateException.class, "Disposable already set!"); + } finally { + RxJavaPlugins.reset(); + } + } + + /** + * Calls onSubscribe twice and checks if it doesn't affect the first Disposable while + * reporting it to plugin error handler. + * @param observer the target + */ + public static void doubleOnSubscribe(SingleObserver<?> observer) { + List<Throwable> errors = trackPluginErrors(); + try { + Disposable d1 = Disposable.empty(); + + observer.onSubscribe(d1); + + Disposable d2 = Disposable.empty(); + + observer.onSubscribe(d2); + + assertFalse(d1.isDisposed()); + + assertTrue(d2.isDisposed()); + + assertError(errors, 0, IllegalStateException.class, "Disposable already set!"); + } finally { + RxJavaPlugins.reset(); + } + } + + /** + * Calls onSubscribe twice and checks if it doesn't affect the first Disposable while + * reporting it to plugin error handler. + * @param observer the target + */ + public static void doubleOnSubscribe(CompletableObserver observer) { + List<Throwable> errors = trackPluginErrors(); + try { + Disposable d1 = Disposable.empty(); + + observer.onSubscribe(d1); + + Disposable d2 = Disposable.empty(); + + observer.onSubscribe(d2); + + assertFalse(d1.isDisposed()); + + assertTrue(d2.isDisposed()); + + assertError(errors, 0, IllegalStateException.class, "Disposable already set!"); + } finally { + RxJavaPlugins.reset(); + } + } + + /** + * Calls onSubscribe twice and checks if it doesn't affect the first Disposable while + * reporting it to plugin error handler. + * @param observer the target + */ + public static void doubleOnSubscribe(MaybeObserver<?> observer) { + List<Throwable> errors = trackPluginErrors(); + try { + Disposable d1 = Disposable.empty(); + + observer.onSubscribe(d1); + + Disposable d2 = Disposable.empty(); + + observer.onSubscribe(d2); + + assertFalse(d1.isDisposed()); + + assertTrue(d2.isDisposed()); + + assertError(errors, 0, IllegalStateException.class, "Disposable already set!"); + } finally { + RxJavaPlugins.reset(); + } + } + + public static void checkDisposed(Disposable d) { + assertFalse("Disposed upfront?!", d.isDisposed()); + + d.dispose(); + + assertTrue("Not disposed?!", d.isDisposed()); + + d.dispose(); + + assertTrue("Not disposed again?!", d.isDisposed()); + } + + /** + * Checks if the upstream's Subscription sent through the onSubscribe reports + * isCancelled properly before and after calling dispose. + * @param <T> the input value type + * @param source the source to test + */ + public static <T> void checkDisposed(Flowable<T> source) { + final TestSubscriber<Object> ts = new TestSubscriber<>(0L); + source.subscribe(new FlowableSubscriber<Object>() { + @Override + public void onSubscribe(Subscription s) { + ts.onSubscribe(new BooleanSubscription()); + + s.cancel(); + + s.cancel(); + } + + @Override + public void onNext(Object t) { + ts.onNext(t); + } + + @Override + public void onError(Throwable t) { + ts.onError(t); + } + + @Override + public void onComplete() { + ts.onComplete(); + } + }); + ts.assertEmpty(); + } + /** + * Checks if the upstream's Disposable sent through the onSubscribe reports + * isDisposed properly before and after calling dispose. + * @param source the source to test + */ + public static void checkDisposed(Maybe<?> source) { + final Boolean[] b = { null, null }; + final CountDownLatch cdl = new CountDownLatch(1); + source.subscribe(new MaybeObserver<Object>() { + + @Override + public void onSubscribe(Disposable d) { + try { + b[0] = d.isDisposed(); + + d.dispose(); + + b[1] = d.isDisposed(); + + d.dispose(); + } finally { + cdl.countDown(); + } + } + + @Override + public void onSuccess(Object value) { + // ignored + } + + @Override + public void onError(Throwable e) { + // ignored + } + + @Override + public void onComplete() { + // ignored + } + }); + + try { + assertTrue("Timed out", cdl.await(5, TimeUnit.SECONDS)); + } catch (InterruptedException ex) { + throw ExceptionHelper.wrapOrThrow(ex); + } + + assertEquals("Reports disposed upfront?", false, b[0]); + assertEquals("Didn't report disposed after?", true, b[1]); + } + + /** + * Checks if the upstream's Disposable sent through the onSubscribe reports + * isDisposed properly before and after calling dispose. + * @param source the source to test + */ + public static void checkDisposed(Observable<?> source) { + final Boolean[] b = { null, null }; + final CountDownLatch cdl = new CountDownLatch(1); + source.subscribe(new Observer<Object>() { + + @Override + public void onSubscribe(Disposable d) { + try { + b[0] = d.isDisposed(); + + d.dispose(); + + b[1] = d.isDisposed(); + + d.dispose(); + } finally { + cdl.countDown(); + } + } + + @Override + public void onNext(Object value) { + // ignored + } + + @Override + public void onError(Throwable e) { + // ignored + } + + @Override + public void onComplete() { + // ignored + } + }); + + try { + assertTrue("Timed out", cdl.await(5, TimeUnit.SECONDS)); + } catch (InterruptedException ex) { + throw ExceptionHelper.wrapOrThrow(ex); + } + + assertEquals("Reports disposed upfront?", false, b[0]); + assertEquals("Didn't report disposed after?", true, b[1]); + } + + /** + * Checks if the upstream's Disposable sent through the onSubscribe reports + * isDisposed properly before and after calling dispose. + * @param source the source to test + */ + public static void checkDisposed(Single<?> source) { + final Boolean[] b = { null, null }; + final CountDownLatch cdl = new CountDownLatch(1); + source.subscribe(new SingleObserver<Object>() { + + @Override + public void onSubscribe(Disposable d) { + try { + b[0] = d.isDisposed(); + + d.dispose(); + + b[1] = d.isDisposed(); + + d.dispose(); + } finally { + cdl.countDown(); + } + } + + @Override + public void onSuccess(Object value) { + // ignored + } + + @Override + public void onError(Throwable e) { + // ignored + } + }); + + try { + assertTrue("Timed out", cdl.await(5, TimeUnit.SECONDS)); + } catch (InterruptedException ex) { + throw ExceptionHelper.wrapOrThrow(ex); + } + + assertEquals("Reports disposed upfront?", false, b[0]); + assertEquals("Didn't report disposed after?", true, b[1]); + } + + /** + * Checks if the upstream's Disposable sent through the onSubscribe reports + * isDisposed properly before and after calling dispose. + * @param source the source to test + */ + public static void checkDisposed(Completable source) { + final Boolean[] b = { null, null }; + final CountDownLatch cdl = new CountDownLatch(1); + source.subscribe(new CompletableObserver() { + + @Override + public void onSubscribe(Disposable d) { + try { + b[0] = d.isDisposed(); + + d.dispose(); + + b[1] = d.isDisposed(); + + d.dispose(); + } finally { + cdl.countDown(); + } + } + + @Override + public void onError(Throwable e) { + // ignored + } + + @Override + public void onComplete() { + // ignored + } + }); + + try { + assertTrue("Timed out", cdl.await(5, TimeUnit.SECONDS)); + } catch (InterruptedException ex) { + throw ExceptionHelper.wrapOrThrow(ex); + } + + assertEquals("Reports disposed upfront?", false, b[0]); + assertEquals("Didn't report disposed after?", true, b[1]); + } + + /** + * Consumer for all base reactive types. + */ + enum NoOpConsumer implements FlowableSubscriber<Object>, Observer<Object>, MaybeObserver<Object>, SingleObserver<Object>, CompletableObserver { + INSTANCE; + + @Override + public void onSubscribe(Disposable d) { + // deliberately no-op + } + + @Override + public void onSuccess(Object value) { + // deliberately no-op + } + + @Override + public void onError(Throwable e) { + // deliberately no-op + } + + @Override + public void onComplete() { + // deliberately no-op + } + + @Override + public void onSubscribe(Subscription s) { + // deliberately no-op + } + + @Override + public void onNext(Object t) { + // deliberately no-op + } + } + + /** + * Check if the given transformed reactive type reports multiple onSubscribe calls to + * RxJavaPlugins. + * @param <T> the input value type + * @param <R> the output value type + * @param transform the transform to drive an operator + */ + public static <T, R> void checkDoubleOnSubscribeMaybe(Function<Maybe<T>, ? extends MaybeSource<R>> transform) { + List<Throwable> errors = trackPluginErrors(); + try { + final Boolean[] b = { null, null }; + final CountDownLatch cdl = new CountDownLatch(1); + + Maybe<T> source = new Maybe<T>() { + @Override + protected void subscribeActual(MaybeObserver<? super T> observer) { + try { + Disposable d1 = Disposable.empty(); + + observer.onSubscribe(d1); + + Disposable d2 = Disposable.empty(); + + observer.onSubscribe(d2); + + b[0] = d1.isDisposed(); + b[1] = d2.isDisposed(); + } finally { + cdl.countDown(); + } + } + }; + + MaybeSource<R> out = transform.apply(source); + + out.subscribe(NoOpConsumer.INSTANCE); + + try { + assertTrue("Timed out", cdl.await(5, TimeUnit.SECONDS)); + } catch (InterruptedException ex) { + throw ExceptionHelper.wrapOrThrow(ex); + } + + assertEquals("First disposed?", false, b[0]); + assertEquals("Second not disposed?", true, b[1]); + + assertError(errors, 0, IllegalStateException.class, "Disposable already set!"); + } catch (Throwable ex) { + throw ExceptionHelper.wrapOrThrow(ex); + } finally { + RxJavaPlugins.reset(); + } + } + + /** + * Check if the given transformed reactive type reports multiple onSubscribe calls to + * RxJavaPlugins. + * @param <T> the input value type + * @param <R> the output value type + * @param transform the transform to drive an operator + */ + public static <T, R> void checkDoubleOnSubscribeMaybeToSingle(Function<Maybe<T>, ? extends SingleSource<R>> transform) { + List<Throwable> errors = trackPluginErrors(); + try { + final Boolean[] b = { null, null }; + final CountDownLatch cdl = new CountDownLatch(1); + + Maybe<T> source = new Maybe<T>() { + @Override + protected void subscribeActual(MaybeObserver<? super T> observer) { + try { + Disposable d1 = Disposable.empty(); + + observer.onSubscribe(d1); + + Disposable d2 = Disposable.empty(); + + observer.onSubscribe(d2); + + b[0] = d1.isDisposed(); + b[1] = d2.isDisposed(); + } finally { + cdl.countDown(); + } + } + }; + + SingleSource<R> out = transform.apply(source); + + out.subscribe(NoOpConsumer.INSTANCE); + + try { + assertTrue("Timed out", cdl.await(5, TimeUnit.SECONDS)); + } catch (InterruptedException ex) { + throw ExceptionHelper.wrapOrThrow(ex); + } + + assertEquals("First disposed?", false, b[0]); + assertEquals("Second not disposed?", true, b[1]); + + assertError(errors, 0, IllegalStateException.class, "Disposable already set!"); + } catch (Throwable ex) { + throw ExceptionHelper.wrapOrThrow(ex); + } finally { + RxJavaPlugins.reset(); + } + } + + /** + * Check if the given transformed reactive type reports multiple onSubscribe calls to + * RxJavaPlugins. + * @param <T> the input value type + * @param <R> the output value type + * @param transform the transform to drive an operator + */ + public static <T, R> void checkDoubleOnSubscribeMaybeToObservable(Function<Maybe<T>, ? extends ObservableSource<R>> transform) { + List<Throwable> errors = trackPluginErrors(); + try { + final Boolean[] b = { null, null }; + final CountDownLatch cdl = new CountDownLatch(1); + + Maybe<T> source = new Maybe<T>() { + @Override + protected void subscribeActual(MaybeObserver<? super T> observer) { + try { + Disposable d1 = Disposable.empty(); + + observer.onSubscribe(d1); + + Disposable d2 = Disposable.empty(); + + observer.onSubscribe(d2); + + b[0] = d1.isDisposed(); + b[1] = d2.isDisposed(); + } finally { + cdl.countDown(); + } + } + }; + + ObservableSource<R> out = transform.apply(source); + + out.subscribe(NoOpConsumer.INSTANCE); + + try { + assertTrue("Timed out", cdl.await(5, TimeUnit.SECONDS)); + } catch (InterruptedException ex) { + throw ExceptionHelper.wrapOrThrow(ex); + } + + assertEquals("First disposed?", false, b[0]); + assertEquals("Second not disposed?", true, b[1]); + + assertError(errors, 0, IllegalStateException.class, "Disposable already set!"); + } catch (Throwable ex) { + throw ExceptionHelper.wrapOrThrow(ex); + } finally { + RxJavaPlugins.reset(); + } + } + + /** + * Check if the given transformed reactive type reports multiple onSubscribe calls to + * RxJavaPlugins. + * @param <T> the input value type + * @param <R> the output value type + * @param transform the transform to drive an operator + */ + public static <T, R> void checkDoubleOnSubscribeMaybeToFlowable(Function<Maybe<T>, ? extends Publisher<R>> transform) { + List<Throwable> errors = trackPluginErrors(); + try { + final Boolean[] b = { null, null }; + final CountDownLatch cdl = new CountDownLatch(1); + + Maybe<T> source = new Maybe<T>() { + @Override + protected void subscribeActual(MaybeObserver<? super T> observer) { + try { + Disposable d1 = Disposable.empty(); + + observer.onSubscribe(d1); + + Disposable d2 = Disposable.empty(); + + observer.onSubscribe(d2); + + b[0] = d1.isDisposed(); + b[1] = d2.isDisposed(); + } finally { + cdl.countDown(); + } + } + }; + + Publisher<R> out = transform.apply(source); + + out.subscribe(NoOpConsumer.INSTANCE); + + try { + assertTrue("Timed out", cdl.await(5, TimeUnit.SECONDS)); + } catch (InterruptedException ex) { + throw ExceptionHelper.wrapOrThrow(ex); + } + + assertEquals("First disposed?", false, b[0]); + assertEquals("Second not disposed?", true, b[1]); + + assertError(errors, 0, IllegalStateException.class, "Disposable already set!"); + } catch (Throwable ex) { + throw ExceptionHelper.wrapOrThrow(ex); + } finally { + RxJavaPlugins.reset(); + } + } + + /** + * Check if the given transformed reactive type reports multiple onSubscribe calls to + * RxJavaPlugins. + * @param <T> the input value type + * @param <R> the output value type + * @param transform the transform to drive an operator + */ + public static <T, R> void checkDoubleOnSubscribeSingleToMaybe(Function<Single<T>, ? extends MaybeSource<R>> transform) { + List<Throwable> errors = trackPluginErrors(); + try { + final Boolean[] b = { null, null }; + final CountDownLatch cdl = new CountDownLatch(1); + + Single<T> source = new Single<T>() { + @Override + protected void subscribeActual(SingleObserver<? super T> observer) { + try { + Disposable d1 = Disposable.empty(); + + observer.onSubscribe(d1); + + Disposable d2 = Disposable.empty(); + + observer.onSubscribe(d2); + + b[0] = d1.isDisposed(); + b[1] = d2.isDisposed(); + } finally { + cdl.countDown(); + } + } + }; + + MaybeSource<R> out = transform.apply(source); + + out.subscribe(NoOpConsumer.INSTANCE); + + try { + assertTrue("Timed out", cdl.await(5, TimeUnit.SECONDS)); + } catch (InterruptedException ex) { + throw ExceptionHelper.wrapOrThrow(ex); + } + + assertEquals("First disposed?", false, b[0]); + assertEquals("Second not disposed?", true, b[1]); + + assertError(errors, 0, IllegalStateException.class, "Disposable already set!"); + } catch (Throwable ex) { + throw ExceptionHelper.wrapOrThrow(ex); + } finally { + RxJavaPlugins.reset(); + } + } + + /** + * Check if the given transformed reactive type reports multiple onSubscribe calls to + * RxJavaPlugins. + * @param <T> the input value type + * @param <R> the output value type + * @param transform the transform to drive an operator + */ + public static <T, R> void checkDoubleOnSubscribeSingleToObservable(Function<Single<T>, ? extends ObservableSource<R>> transform) { + List<Throwable> errors = trackPluginErrors(); + try { + final Boolean[] b = { null, null }; + final CountDownLatch cdl = new CountDownLatch(1); + + Single<T> source = new Single<T>() { + @Override + protected void subscribeActual(SingleObserver<? super T> observer) { + try { + Disposable d1 = Disposable.empty(); + + observer.onSubscribe(d1); + + Disposable d2 = Disposable.empty(); + + observer.onSubscribe(d2); + + b[0] = d1.isDisposed(); + b[1] = d2.isDisposed(); + } finally { + cdl.countDown(); + } + } + }; + + ObservableSource<R> out = transform.apply(source); + + out.subscribe(NoOpConsumer.INSTANCE); + + try { + assertTrue("Timed out", cdl.await(5, TimeUnit.SECONDS)); + } catch (InterruptedException ex) { + throw ExceptionHelper.wrapOrThrow(ex); + } + + assertEquals("First disposed?", false, b[0]); + assertEquals("Second not disposed?", true, b[1]); + + assertError(errors, 0, IllegalStateException.class, "Disposable already set!"); + } catch (Throwable ex) { + throw ExceptionHelper.wrapOrThrow(ex); + } finally { + RxJavaPlugins.reset(); + } + } + + /** + * Check if the given transformed reactive type reports multiple onSubscribe calls to + * RxJavaPlugins. + * @param <T> the input value type + * @param <R> the output value type + * @param transform the transform to drive an operator + */ + public static <T, R> void checkDoubleOnSubscribeSingleToFlowable(Function<Single<T>, ? extends Publisher<R>> transform) { + List<Throwable> errors = trackPluginErrors(); + try { + final Boolean[] b = { null, null }; + final CountDownLatch cdl = new CountDownLatch(1); + + Single<T> source = new Single<T>() { + @Override + protected void subscribeActual(SingleObserver<? super T> observer) { + try { + Disposable d1 = Disposable.empty(); + + observer.onSubscribe(d1); + + Disposable d2 = Disposable.empty(); + + observer.onSubscribe(d2); + + b[0] = d1.isDisposed(); + b[1] = d2.isDisposed(); + } finally { + cdl.countDown(); + } + } + }; + + Publisher<R> out = transform.apply(source); + + out.subscribe(NoOpConsumer.INSTANCE); + + try { + assertTrue("Timed out", cdl.await(5, TimeUnit.SECONDS)); + } catch (InterruptedException ex) { + throw ExceptionHelper.wrapOrThrow(ex); + } + + assertEquals("First disposed?", false, b[0]); + assertEquals("Second not disposed?", true, b[1]); + + assertError(errors, 0, IllegalStateException.class, "Disposable already set!"); + } catch (Throwable ex) { + throw ExceptionHelper.wrapOrThrow(ex); + } finally { + RxJavaPlugins.reset(); + } + } + + /** + * Check if the given transformed reactive type reports multiple onSubscribe calls to + * RxJavaPlugins. + * @param <T> the input value type + * @param transform the transform to drive an operator + */ + public static <T> void checkDoubleOnSubscribeMaybeToCompletable(Function<Maybe<T>, ? extends CompletableSource> transform) { + List<Throwable> errors = trackPluginErrors(); + try { + final Boolean[] b = { null, null }; + final CountDownLatch cdl = new CountDownLatch(1); + + Maybe<T> source = new Maybe<T>() { + @Override + protected void subscribeActual(MaybeObserver<? super T> observer) { + try { + Disposable d1 = Disposable.empty(); + + observer.onSubscribe(d1); + + Disposable d2 = Disposable.empty(); + + observer.onSubscribe(d2); + + b[0] = d1.isDisposed(); + b[1] = d2.isDisposed(); + } finally { + cdl.countDown(); + } + } + }; + + CompletableSource out = transform.apply(source); + + out.subscribe(NoOpConsumer.INSTANCE); + + try { + assertTrue("Timed out", cdl.await(5, TimeUnit.SECONDS)); + } catch (InterruptedException ex) { + throw ExceptionHelper.wrapOrThrow(ex); + } + + assertEquals("First disposed?", false, b[0]); + assertEquals("Second not disposed?", true, b[1]); + + assertError(errors, 0, IllegalStateException.class, "Disposable already set!"); + } catch (Throwable ex) { + throw ExceptionHelper.wrapOrThrow(ex); + } finally { + RxJavaPlugins.reset(); + } + } + + /** + * Check if the given transformed reactive type reports multiple onSubscribe calls to + * RxJavaPlugins. + * @param <T> the input value type + * @param <R> the output value type + * @param transform the transform to drive an operator + */ + public static <T, R> void checkDoubleOnSubscribeSingle(Function<Single<T>, ? extends SingleSource<R>> transform) { + List<Throwable> errors = trackPluginErrors(); + try { + final Boolean[] b = { null, null }; + final CountDownLatch cdl = new CountDownLatch(1); + + Single<T> source = new Single<T>() { + @Override + protected void subscribeActual(SingleObserver<? super T> observer) { + try { + Disposable d1 = Disposable.empty(); + + observer.onSubscribe(d1); + + Disposable d2 = Disposable.empty(); + + observer.onSubscribe(d2); + + b[0] = d1.isDisposed(); + b[1] = d2.isDisposed(); + } finally { + cdl.countDown(); + } + } + }; + + SingleSource<R> out = transform.apply(source); + + out.subscribe(NoOpConsumer.INSTANCE); + + try { + assertTrue("Timed out", cdl.await(5, TimeUnit.SECONDS)); + } catch (InterruptedException ex) { + throw ExceptionHelper.wrapOrThrow(ex); + } + + assertEquals("First disposed?", false, b[0]); + assertEquals("Second not disposed?", true, b[1]); + + assertError(errors, 0, IllegalStateException.class, "Disposable already set!"); + } catch (Throwable ex) { + throw ExceptionHelper.wrapOrThrow(ex); + } finally { + RxJavaPlugins.reset(); + } + } + + /** + * Check if the given transformed reactive type reports multiple onSubscribe calls to + * RxJavaPlugins. + * @param <T> the input value type + * @param <R> the output value type + * @param transform the transform to drive an operator + */ + public static <T, R> void checkDoubleOnSubscribeFlowable(Function<Flowable<T>, ? extends Publisher<R>> transform) { + List<Throwable> errors = trackPluginErrors(); + try { + final Boolean[] b = { null, null }; + final CountDownLatch cdl = new CountDownLatch(1); + + Flowable<T> source = new Flowable<T>() { + @Override + protected void subscribeActual(Subscriber<? super T> subscriber) { + try { + BooleanSubscription bs1 = new BooleanSubscription(); + + subscriber.onSubscribe(bs1); + + BooleanSubscription bs2 = new BooleanSubscription(); + + subscriber.onSubscribe(bs2); + + b[0] = bs1.isCancelled(); + b[1] = bs2.isCancelled(); + } finally { + cdl.countDown(); + } + } + }; + + Publisher<R> out = transform.apply(source); + + out.subscribe(NoOpConsumer.INSTANCE); + + try { + assertTrue("Timed out", cdl.await(5, TimeUnit.SECONDS)); + } catch (InterruptedException ex) { + throw ExceptionHelper.wrapOrThrow(ex); + } + + assertEquals("First disposed?", false, b[0]); + assertEquals("Second not disposed?", true, b[1]); + + assertError(errors, 0, IllegalStateException.class, "Subscription already set!"); + } catch (Throwable ex) { + throw ExceptionHelper.wrapOrThrow(ex); + } finally { + RxJavaPlugins.reset(); + } + } + + /** + * Check if the given transformed reactive type reports multiple onSubscribe calls to + * RxJavaPlugins. + * @param <T> the input value type + * @param transform the transform to drive an operator + */ + @SuppressWarnings("unchecked") + public static <T> void checkDoubleOnSubscribeParallel(Function<ParallelFlowable<T>, ? extends ParallelFlowable<?>> transform) { + List<Throwable> errors = trackPluginErrors(); + try { + final Boolean[] b = { null, null, null, null }; + final CountDownLatch cdl = new CountDownLatch(2); + + ParallelFlowable<T> source = new ParallelFlowable<T>() { + @Override + public void subscribe(Subscriber<? super T>[] subscribers) { + for (int i = 0; i < subscribers.length; i++) { + try { + BooleanSubscription bs1 = new BooleanSubscription(); + + subscribers[i].onSubscribe(bs1); + + BooleanSubscription bs2 = new BooleanSubscription(); + + subscribers[i].onSubscribe(bs2); + + b[i * 2 + 0] = bs1.isCancelled(); + b[i * 2 + 1] = bs2.isCancelled(); + } finally { + cdl.countDown(); + } + } + } + + @Override + public int parallelism() { + return 2; + } + }; + + ParallelFlowable<?> out = transform.apply(source); + + out.subscribe(new Subscriber[] { NoOpConsumer.INSTANCE, NoOpConsumer.INSTANCE }); + + try { + assertTrue("Timed out", cdl.await(5, TimeUnit.SECONDS)); + } catch (InterruptedException ex) { + throw ExceptionHelper.wrapOrThrow(ex); + } + + assertEquals("Rail 1 First disposed?", false, b[0]); + assertEquals("Rail 1 Second not disposed?", true, b[1]); + + assertEquals("Rail 2 First disposed?", false, b[2]); + assertEquals("Rail 2 Second not disposed?", true, b[3]); + + assertError(errors, 0, IllegalStateException.class, "Subscription already set!"); + assertError(errors, 1, IllegalStateException.class, "Subscription already set!"); + } catch (Throwable ex) { + throw ExceptionHelper.wrapOrThrow(ex); + } finally { + RxJavaPlugins.reset(); + } + } + /** + * Check if the given transformed reactive type reports multiple onSubscribe calls to + * RxJavaPlugins. + * @param <T> the input value type + * @param transform the transform to drive an operator + */ + public static <T> void checkDoubleOnSubscribeParallelToFlowable(Function<ParallelFlowable<T>, ? extends Flowable<?>> transform) { + List<Throwable> errors = trackPluginErrors(); + try { + final Boolean[] b = { null, null, null, null }; + final CountDownLatch cdl = new CountDownLatch(2); + + ParallelFlowable<T> source = new ParallelFlowable<T>() { + @Override + public void subscribe(Subscriber<? super T>[] subscribers) { + for (int i = 0; i < subscribers.length; i++) { + try { + BooleanSubscription bs1 = new BooleanSubscription(); + + subscribers[i].onSubscribe(bs1); + + BooleanSubscription bs2 = new BooleanSubscription(); + + subscribers[i].onSubscribe(bs2); + + b[i * 2 + 0] = bs1.isCancelled(); + b[i * 2 + 1] = bs2.isCancelled(); + } finally { + cdl.countDown(); + } + } + } + + @Override + public int parallelism() { + return 2; + } + }; + + Flowable<?> out = transform.apply(source); + + out.subscribe(NoOpConsumer.INSTANCE); + + try { + assertTrue("Timed out", cdl.await(5, TimeUnit.SECONDS)); + } catch (InterruptedException ex) { + throw ExceptionHelper.wrapOrThrow(ex); + } + + assertEquals("Rail 1 First disposed?", false, b[0]); + assertEquals("Rail 1 Second not disposed?", true, b[1]); + + assertEquals("Rail 2 First disposed?", false, b[2]); + assertEquals("Rail 2 Second not disposed?", true, b[3]); + + assertError(errors, 0, IllegalStateException.class, "Subscription already set!"); + assertError(errors, 1, IllegalStateException.class, "Subscription already set!"); + } catch (Throwable ex) { + throw ExceptionHelper.wrapOrThrow(ex); + } finally { + RxJavaPlugins.reset(); + } + } + + /** + * Check if the given transformed reactive type reports multiple onSubscribe calls to + * RxJavaPlugins. + * @param <T> the input value type + * @param <R> the output value type + * @param transform the transform to drive an operator + */ + public static <T, R> void checkDoubleOnSubscribeObservable(Function<Observable<T>, ? extends ObservableSource<R>> transform) { + List<Throwable> errors = trackPluginErrors(); + try { + final Boolean[] b = { null, null }; + final CountDownLatch cdl = new CountDownLatch(1); + + Observable<T> source = new Observable<T>() { + @Override + protected void subscribeActual(Observer<? super T> observer) { + try { + Disposable d1 = Disposable.empty(); + + observer.onSubscribe(d1); + + Disposable d2 = Disposable.empty(); + + observer.onSubscribe(d2); + + b[0] = d1.isDisposed(); + b[1] = d2.isDisposed(); + } finally { + cdl.countDown(); + } + } + }; + + ObservableSource<R> out = transform.apply(source); + + out.subscribe(NoOpConsumer.INSTANCE); + + try { + assertTrue("Timed out", cdl.await(5, TimeUnit.SECONDS)); + } catch (InterruptedException ex) { + throw ExceptionHelper.wrapOrThrow(ex); + } + + assertEquals("First disposed?", false, b[0]); + assertEquals("Second not disposed?", true, b[1]); + + assertError(errors, 0, IllegalStateException.class, "Disposable already set!"); + } catch (Throwable ex) { + throw ExceptionHelper.wrapOrThrow(ex); + } finally { + RxJavaPlugins.reset(); + } + } + + /** + * Check if the given transformed reactive type reports multiple onSubscribe calls to + * RxJavaPlugins. + * @param <T> the input value type + * @param <R> the output value type + * @param transform the transform to drive an operator + */ + public static <T, R> void checkDoubleOnSubscribeObservableToSingle(Function<Observable<T>, ? extends SingleSource<R>> transform) { + List<Throwable> errors = trackPluginErrors(); + try { + final Boolean[] b = { null, null }; + final CountDownLatch cdl = new CountDownLatch(1); + + Observable<T> source = new Observable<T>() { + @Override + protected void subscribeActual(Observer<? super T> observer) { + try { + Disposable d1 = Disposable.empty(); + + observer.onSubscribe(d1); + + Disposable d2 = Disposable.empty(); + + observer.onSubscribe(d2); + + b[0] = d1.isDisposed(); + b[1] = d2.isDisposed(); + } finally { + cdl.countDown(); + } + } + }; + + SingleSource<R> out = transform.apply(source); + + out.subscribe(NoOpConsumer.INSTANCE); + + try { + assertTrue("Timed out", cdl.await(5, TimeUnit.SECONDS)); + } catch (InterruptedException ex) { + throw ExceptionHelper.wrapOrThrow(ex); + } + + assertEquals("First disposed?", false, b[0]); + assertEquals("Second not disposed?", true, b[1]); + + assertError(errors, 0, IllegalStateException.class, "Disposable already set!"); + } catch (Throwable ex) { + throw ExceptionHelper.wrapOrThrow(ex); + } finally { + RxJavaPlugins.reset(); + } + } + + /** + * Check if the given transformed reactive type reports multiple onSubscribe calls to + * RxJavaPlugins. + * @param <T> the input value type + * @param <R> the output value type + * @param transform the transform to drive an operator + */ + public static <T, R> void checkDoubleOnSubscribeObservableToMaybe(Function<Observable<T>, ? extends MaybeSource<R>> transform) { + List<Throwable> errors = trackPluginErrors(); + try { + final Boolean[] b = { null, null }; + final CountDownLatch cdl = new CountDownLatch(1); + + Observable<T> source = new Observable<T>() { + @Override + protected void subscribeActual(Observer<? super T> observer) { + try { + Disposable d1 = Disposable.empty(); + + observer.onSubscribe(d1); + + Disposable d2 = Disposable.empty(); + + observer.onSubscribe(d2); + + b[0] = d1.isDisposed(); + b[1] = d2.isDisposed(); + } finally { + cdl.countDown(); + } + } + }; + + MaybeSource<R> out = transform.apply(source); + + out.subscribe(NoOpConsumer.INSTANCE); + + try { + assertTrue("Timed out", cdl.await(5, TimeUnit.SECONDS)); + } catch (InterruptedException ex) { + throw ExceptionHelper.wrapOrThrow(ex); + } + + assertEquals("First disposed?", false, b[0]); + assertEquals("Second not disposed?", true, b[1]); + + assertError(errors, 0, IllegalStateException.class, "Disposable already set!"); + } catch (Throwable ex) { + throw ExceptionHelper.wrapOrThrow(ex); + } finally { + RxJavaPlugins.reset(); + } + } + + /** + * Check if the given transformed reactive type reports multiple onSubscribe calls to + * RxJavaPlugins. + * @param <T> the input value type + * @param transform the transform to drive an operator + */ + public static <T> void checkDoubleOnSubscribeObservableToCompletable(Function<Observable<T>, ? extends CompletableSource> transform) { + List<Throwable> errors = trackPluginErrors(); + try { + final Boolean[] b = { null, null }; + final CountDownLatch cdl = new CountDownLatch(1); + + Observable<T> source = new Observable<T>() { + @Override + protected void subscribeActual(Observer<? super T> observer) { + try { + Disposable d1 = Disposable.empty(); + + observer.onSubscribe(d1); + + Disposable d2 = Disposable.empty(); + + observer.onSubscribe(d2); + + b[0] = d1.isDisposed(); + b[1] = d2.isDisposed(); + } finally { + cdl.countDown(); + } + } + }; + + CompletableSource out = transform.apply(source); + + out.subscribe(NoOpConsumer.INSTANCE); + + try { + assertTrue("Timed out", cdl.await(5, TimeUnit.SECONDS)); + } catch (InterruptedException ex) { + throw ExceptionHelper.wrapOrThrow(ex); + } + + assertEquals("First disposed?", false, b[0]); + assertEquals("Second not disposed?", true, b[1]); + + assertError(errors, 0, IllegalStateException.class, "Disposable already set!"); + } catch (Throwable ex) { + throw ExceptionHelper.wrapOrThrow(ex); + } finally { + RxJavaPlugins.reset(); + } + } + + /** + * Check if the given transformed reactive type reports multiple onSubscribe calls to + * RxJavaPlugins. + * @param <T> the input value type + * @param <R> the output value type + * @param transform the transform to drive an operator + */ + public static <T, R> void checkDoubleOnSubscribeFlowableToObservable(Function<Flowable<T>, ? extends ObservableSource<R>> transform) { + List<Throwable> errors = trackPluginErrors(); + try { + final Boolean[] b = { null, null }; + final CountDownLatch cdl = new CountDownLatch(1); + + Flowable<T> source = new Flowable<T>() { + @Override + protected void subscribeActual(Subscriber<? super T> subscriber) { + try { + BooleanSubscription bs1 = new BooleanSubscription(); + + subscriber.onSubscribe(bs1); + + BooleanSubscription bs2 = new BooleanSubscription(); + + subscriber.onSubscribe(bs2); + + b[0] = bs1.isCancelled(); + b[1] = bs2.isCancelled(); + } finally { + cdl.countDown(); + } + } + }; + + ObservableSource<R> out = transform.apply(source); + + out.subscribe(NoOpConsumer.INSTANCE); + + try { + assertTrue("Timed out", cdl.await(5, TimeUnit.SECONDS)); + } catch (InterruptedException ex) { + throw ExceptionHelper.wrapOrThrow(ex); + } + + assertEquals("First cancelled?", false, b[0]); + assertEquals("Second not cancelled?", true, b[1]); + + assertError(errors, 0, IllegalStateException.class, "Subscription already set!"); + } catch (Throwable ex) { + throw ExceptionHelper.wrapOrThrow(ex); + } finally { + RxJavaPlugins.reset(); + } + } + + /** + * Check if the given transformed reactive type reports multiple onSubscribe calls to + * RxJavaPlugins. + * @param <T> the input value type + * @param <R> the output value type + * @param transform the transform to drive an operator + */ + public static <T, R> void checkDoubleOnSubscribeFlowableToSingle(Function<Flowable<T>, ? extends SingleSource<R>> transform) { + List<Throwable> errors = trackPluginErrors(); + try { + final Boolean[] b = { null, null }; + final CountDownLatch cdl = new CountDownLatch(1); + + Flowable<T> source = new Flowable<T>() { + @Override + protected void subscribeActual(Subscriber<? super T> subscriber) { + try { + BooleanSubscription bs1 = new BooleanSubscription(); + + subscriber.onSubscribe(bs1); + + BooleanSubscription bs2 = new BooleanSubscription(); + + subscriber.onSubscribe(bs2); + + b[0] = bs1.isCancelled(); + b[1] = bs2.isCancelled(); + } finally { + cdl.countDown(); + } + } + }; + + SingleSource<R> out = transform.apply(source); + + out.subscribe(NoOpConsumer.INSTANCE); + + try { + assertTrue("Timed out", cdl.await(5, TimeUnit.SECONDS)); + } catch (InterruptedException ex) { + throw ExceptionHelper.wrapOrThrow(ex); + } + + assertEquals("First cancelled?", false, b[0]); + assertEquals("Second not cancelled?", true, b[1]); + + assertError(errors, 0, IllegalStateException.class, "Subscription already set!"); + } catch (Throwable ex) { + throw ExceptionHelper.wrapOrThrow(ex); + } finally { + RxJavaPlugins.reset(); + } + } + + /** + * Check if the given transformed reactive type reports multiple onSubscribe calls to + * RxJavaPlugins. + * @param <T> the input value type + * @param <R> the output value type + * @param transform the transform to drive an operator + */ + public static <T, R> void checkDoubleOnSubscribeFlowableToMaybe(Function<Flowable<T>, ? extends MaybeSource<R>> transform) { + List<Throwable> errors = trackPluginErrors(); + try { + final Boolean[] b = { null, null }; + final CountDownLatch cdl = new CountDownLatch(1); + + Flowable<T> source = new Flowable<T>() { + @Override + protected void subscribeActual(Subscriber<? super T> subscriber) { + try { + BooleanSubscription bs1 = new BooleanSubscription(); + + subscriber.onSubscribe(bs1); + + BooleanSubscription bs2 = new BooleanSubscription(); + + subscriber.onSubscribe(bs2); + + b[0] = bs1.isCancelled(); + b[1] = bs2.isCancelled(); + } finally { + cdl.countDown(); + } + } + }; + + MaybeSource<R> out = transform.apply(source); + + out.subscribe(NoOpConsumer.INSTANCE); + + try { + assertTrue("Timed out", cdl.await(5, TimeUnit.SECONDS)); + } catch (InterruptedException ex) { + throw ExceptionHelper.wrapOrThrow(ex); + } + + assertEquals("First cancelled?", false, b[0]); + assertEquals("Second not cancelled?", true, b[1]); + + assertError(errors, 0, IllegalStateException.class, "Subscription already set!"); + } catch (Throwable ex) { + throw ExceptionHelper.wrapOrThrow(ex); + } finally { + RxJavaPlugins.reset(); + } + } + + /** + * Check if the given transformed reactive type reports multiple onSubscribe calls to + * RxJavaPlugins. + * @param <T> the input value type + * @param transform the transform to drive an operator + */ + public static <T> void checkDoubleOnSubscribeFlowableToCompletable(Function<Flowable<T>, ? extends Completable> transform) { + List<Throwable> errors = trackPluginErrors(); + try { + final Boolean[] b = { null, null }; + final CountDownLatch cdl = new CountDownLatch(1); + + Flowable<T> source = new Flowable<T>() { + @Override + protected void subscribeActual(Subscriber<? super T> subscriber) { + try { + BooleanSubscription bs1 = new BooleanSubscription(); + + subscriber.onSubscribe(bs1); + + BooleanSubscription bs2 = new BooleanSubscription(); + + subscriber.onSubscribe(bs2); + + b[0] = bs1.isCancelled(); + b[1] = bs2.isCancelled(); + } finally { + cdl.countDown(); + } + } + }; + + Completable out = transform.apply(source); + + out.subscribe(NoOpConsumer.INSTANCE); + + try { + assertTrue("Timed out", cdl.await(5, TimeUnit.SECONDS)); + } catch (InterruptedException ex) { + throw ExceptionHelper.wrapOrThrow(ex); + } + + assertEquals("First cancelled?", false, b[0]); + assertEquals("Second not cancelled?", true, b[1]); + + assertError(errors, 0, IllegalStateException.class, "Subscription already set!"); + } catch (Throwable ex) { + throw ExceptionHelper.wrapOrThrow(ex); + } finally { + RxJavaPlugins.reset(); + } + } + + /** + * Check if the given transformed reactive type reports multiple onSubscribe calls to + * RxJavaPlugins. + * @param transform the transform to drive an operator + */ + public static void checkDoubleOnSubscribeCompletable(Function<Completable, ? extends CompletableSource> transform) { + List<Throwable> errors = trackPluginErrors(); + try { + final Boolean[] b = { null, null }; + final CountDownLatch cdl = new CountDownLatch(1); + + Completable source = new Completable() { + @Override + protected void subscribeActual(CompletableObserver observer) { + try { + Disposable d1 = Disposable.empty(); + + observer.onSubscribe(d1); + + Disposable d2 = Disposable.empty(); + + observer.onSubscribe(d2); + + b[0] = d1.isDisposed(); + b[1] = d2.isDisposed(); + } finally { + cdl.countDown(); + } + } + }; + + CompletableSource out = transform.apply(source); + + out.subscribe(NoOpConsumer.INSTANCE); + + try { + assertTrue("Timed out", cdl.await(5, TimeUnit.SECONDS)); + } catch (InterruptedException ex) { + throw ExceptionHelper.wrapOrThrow(ex); + } + + assertEquals("First disposed?", false, b[0]); + assertEquals("Second not disposed?", true, b[1]); + + assertError(errors, 0, IllegalStateException.class, "Disposable already set!"); + } catch (Throwable ex) { + throw ExceptionHelper.wrapOrThrow(ex); + } finally { + RxJavaPlugins.reset(); + } + } + + /** + * Check if the given transformed reactive type reports multiple onSubscribe calls to + * RxJavaPlugins. + * @param <T> the output value tye + * @param transform the transform to drive an operator + */ + public static <T> void checkDoubleOnSubscribeCompletableToMaybe(Function<Completable, ? extends MaybeSource<T>> transform) { + List<Throwable> errors = trackPluginErrors(); + try { + final Boolean[] b = { null, null }; + final CountDownLatch cdl = new CountDownLatch(1); + + Completable source = new Completable() { + @Override + protected void subscribeActual(CompletableObserver observer) { + try { + Disposable d1 = Disposable.empty(); + + observer.onSubscribe(d1); + + Disposable d2 = Disposable.empty(); + + observer.onSubscribe(d2); + + b[0] = d1.isDisposed(); + b[1] = d2.isDisposed(); + } finally { + cdl.countDown(); + } + } + }; + + MaybeSource<T> out = transform.apply(source); + + out.subscribe(NoOpConsumer.INSTANCE); + + try { + assertTrue("Timed out", cdl.await(5, TimeUnit.SECONDS)); + } catch (InterruptedException ex) { + throw ExceptionHelper.wrapOrThrow(ex); + } + + assertEquals("First disposed?", false, b[0]); + assertEquals("Second not disposed?", true, b[1]); + + assertError(errors, 0, IllegalStateException.class, "Disposable already set!"); + } catch (Throwable ex) { + throw ExceptionHelper.wrapOrThrow(ex); + } finally { + RxJavaPlugins.reset(); + } + } + + /** + * Check if the given transformed reactive type reports multiple onSubscribe calls to + * RxJavaPlugins. + * @param <T> the output value tye + * @param transform the transform to drive an operator + */ + public static <T> void checkDoubleOnSubscribeCompletableToSingle(Function<Completable, ? extends SingleSource<T>> transform) { + List<Throwable> errors = trackPluginErrors(); + try { + final Boolean[] b = { null, null }; + final CountDownLatch cdl = new CountDownLatch(1); + + Completable source = new Completable() { + @Override + protected void subscribeActual(CompletableObserver observer) { + try { + Disposable d1 = Disposable.empty(); + + observer.onSubscribe(d1); + + Disposable d2 = Disposable.empty(); + + observer.onSubscribe(d2); + + b[0] = d1.isDisposed(); + b[1] = d2.isDisposed(); + } finally { + cdl.countDown(); + } + } + }; + + SingleSource<T> out = transform.apply(source); + + out.subscribe(NoOpConsumer.INSTANCE); + + try { + assertTrue("Timed out", cdl.await(5, TimeUnit.SECONDS)); + } catch (InterruptedException ex) { + throw ExceptionHelper.wrapOrThrow(ex); + } + + assertEquals("First disposed?", false, b[0]); + assertEquals("Second not disposed?", true, b[1]); + + assertError(errors, 0, IllegalStateException.class, "Disposable already set!"); + } catch (Throwable ex) { + throw ExceptionHelper.wrapOrThrow(ex); + } finally { + RxJavaPlugins.reset(); + } + } + + /** + * Check if the given transformed reactive type reports multiple onSubscribe calls to + * RxJavaPlugins. + * @param transform the transform to drive an operator + */ + public static void checkDoubleOnSubscribeCompletableToFlowable(Function<Completable, ? extends Publisher<?>> transform) { + List<Throwable> errors = trackPluginErrors(); + try { + final Boolean[] b = { null, null }; + final CountDownLatch cdl = new CountDownLatch(1); + + Completable source = new Completable() { + @Override + protected void subscribeActual(CompletableObserver observer) { + try { + Disposable d1 = Disposable.empty(); + + observer.onSubscribe(d1); + + Disposable d2 = Disposable.empty(); + + observer.onSubscribe(d2); + + b[0] = d1.isDisposed(); + b[1] = d2.isDisposed(); + } finally { + cdl.countDown(); + } + } + }; + + Publisher<?> out = transform.apply(source); + + out.subscribe(NoOpConsumer.INSTANCE); + + try { + assertTrue("Timed out", cdl.await(5, TimeUnit.SECONDS)); + } catch (InterruptedException ex) { + throw ExceptionHelper.wrapOrThrow(ex); + } + + assertEquals("First disposed?", false, b[0]); + assertEquals("Second not disposed?", true, b[1]); + + assertError(errors, 0, IllegalStateException.class, "Disposable already set!"); + } catch (Throwable ex) { + throw ExceptionHelper.wrapOrThrow(ex); + } finally { + RxJavaPlugins.reset(); + } + } + + /** + * Check if the given transformed reactive type reports multiple onSubscribe calls to + * RxJavaPlugins. + * @param transform the transform to drive an operator + */ + public static void checkDoubleOnSubscribeCompletableToObservable(Function<Completable, ? extends ObservableSource<?>> transform) { + List<Throwable> errors = trackPluginErrors(); + try { + final Boolean[] b = { null, null }; + final CountDownLatch cdl = new CountDownLatch(1); + + Completable source = new Completable() { + @Override + protected void subscribeActual(CompletableObserver observer) { + try { + Disposable d1 = Disposable.empty(); + + observer.onSubscribe(d1); + + Disposable d2 = Disposable.empty(); + + observer.onSubscribe(d2); + + b[0] = d1.isDisposed(); + b[1] = d2.isDisposed(); + } finally { + cdl.countDown(); + } + } + }; + + ObservableSource<?> out = transform.apply(source); + + out.subscribe(NoOpConsumer.INSTANCE); + + try { + assertTrue("Timed out", cdl.await(5, TimeUnit.SECONDS)); + } catch (InterruptedException ex) { + throw ExceptionHelper.wrapOrThrow(ex); + } + + assertEquals("First disposed?", false, b[0]); + assertEquals("Second not disposed?", true, b[1]); + + assertError(errors, 0, IllegalStateException.class, "Disposable already set!"); + } catch (Throwable ex) { + throw ExceptionHelper.wrapOrThrow(ex); + } finally { + RxJavaPlugins.reset(); + } + } + + /** + * Check if the operator applied to a Maybe source propagates dispose properly. + * @param <T> the source value type + * @param <U> the output value type + * @param composer the function to apply an operator to the provided Maybe source + */ + public static <T, U> void checkDisposedMaybe(Function<Maybe<T>, ? extends MaybeSource<U>> composer) { + PublishProcessor<T> pp = PublishProcessor.create(); + + TestSubscriber<U> ts = new TestSubscriber<>(); + + try { + new MaybeToFlowable<>(composer.apply(pp.singleElement())).subscribe(ts); + } catch (Throwable ex) { + throw ExceptionHelper.wrapOrThrow(ex); + } + + assertTrue("Not subscribed to source!", pp.hasSubscribers()); + + ts.cancel(); + + assertFalse("Dispose not propagated!", pp.hasSubscribers()); + } + + /** + * Check if the operator applied to a Completable source propagates dispose properly. + * @param composer the function to apply an operator to the provided Completable source + */ + public static void checkDisposedCompletable(Function<Completable, ? extends CompletableSource> composer) { + PublishProcessor<Integer> pp = PublishProcessor.create(); + + TestSubscriber<Integer> ts = new TestSubscriber<>(); + + try { + new CompletableToFlowable<Integer>(composer.apply(pp.ignoreElements())).subscribe(ts); + } catch (Throwable ex) { + throw ExceptionHelper.wrapOrThrow(ex); + } + + assertTrue("Not subscribed to source!", pp.hasSubscribers()); + + ts.cancel(); + + assertFalse("Dispose not propagated!", pp.hasSubscribers()); + } + + /** + * Check if the operator applied to a Maybe source propagates dispose properly. + * @param <T> the source value type + * @param <U> the output value type + * @param composer the function to apply an operator to the provided Maybe source + */ + public static <T, U> void checkDisposedMaybeToSingle(Function<Maybe<T>, ? extends SingleSource<U>> composer) { + PublishProcessor<T> pp = PublishProcessor.create(); + + TestSubscriber<U> ts = new TestSubscriber<>(); + + try { + new SingleToFlowable<>(composer.apply(pp.singleElement())).subscribe(ts); + } catch (Throwable ex) { + throw ExceptionHelper.wrapOrThrow(ex); + } + + assertTrue(pp.hasSubscribers()); + + ts.cancel(); + + assertFalse(pp.hasSubscribers()); + } + + /** + * Check if the operator applied to a Maybe source propagates dispose properly. + * @param <T> the source value type + * @param <U> the output value type + * @param composer the function to apply an operator to the provided Maybe source + */ + public static <T, U> void checkDisposedSingleToMaybe(Function<Single<T>, ? extends MaybeSource<U>> composer) { + PublishProcessor<T> pp = PublishProcessor.create(); + + TestSubscriber<U> ts = new TestSubscriber<>(); + + try { + new MaybeToFlowable<>(composer.apply(pp.singleOrError())).subscribe(ts); + } catch (Throwable ex) { + throw ExceptionHelper.wrapOrThrow(ex); + } + + assertTrue(pp.hasSubscribers()); + + ts.cancel(); + + assertFalse(pp.hasSubscribers()); + } + + /** + * Check if the TestSubscriber has a CompositeException with the specified class + * of Throwables in the given order. + * @param ts the TestSubscriber instance + * @param classes the array of expected Throwables inside the Composite + */ + @SafeVarargs + public static void assertCompositeExceptions(TestSubscriberEx<?> ts, Class<? extends Throwable>... classes) { + ts + .assertSubscribed() + .assertError(CompositeException.class) + .assertNotComplete(); + + List<Throwable> list = compositeList(ts.errors().get(0)); + + assertEquals(classes.length, list.size()); + + for (int i = 0; i < classes.length; i++) { + assertError(list, i, classes[i]); + } + } + + /** + * Check if the TestSubscriber has a CompositeException with the specified class + * of Throwables in the given order. + * @param ts the TestSubscriber instance + * @param classesAndMessages the array of subsequent Class and String instances representing the + * expected Throwable class and the expected error message + */ + public static void assertCompositeExceptions(TestSubscriberEx<?> ts, Object... classesAndMessages) { + ts + .assertSubscribed() + .assertError(CompositeException.class) + .assertNotComplete(); + + assertCompositeExceptionListOf(ts.errors().get(0), classesAndMessages); + } + + /** + * Check if the TestSubscriber has a CompositeException with the specified class + * of Throwables in the given order. + * @param to the TestSubscriber instance + * @param classes the array of expected Throwables inside the Composite + */ + @SafeVarargs + public static void assertCompositeExceptions(TestObserverEx<?> to, Class<? extends Throwable>... classes) { + to + .assertSubscribed() + .assertError(CompositeException.class) + .assertNotComplete(); + + List<Throwable> list = compositeList(to.errors().get(0)); + + assertEquals(classes.length, list.size()); + + for (int i = 0; i < classes.length; i++) { + assertError(list, i, classes[i]); + } + } + + /** + * Check if the TestSubscriber has a CompositeException with the specified class + * of Throwables in the given order. + * @param to the TestSubscriber instance + * @param classesAndMessages the array of subsequent Class and String instances representing the + * expected Throwable class and the expected error message + */ + public static void assertCompositeExceptions(TestObserverEx<?> to, Object... classesAndMessages) { + to + .assertSubscribed() + .assertError(CompositeException.class) + .assertNotComplete(); + + assertCompositeExceptionListOf(to.errors().get(0), classesAndMessages); + } + + @SuppressWarnings("unchecked") + static void assertCompositeExceptionListOf(Throwable ex, Object... classesAndMessages) { + List<Throwable> list = compositeList(ex); + + assertEquals(classesAndMessages.length, 2 * list.size()); + + for (int i = 0; i < list.size(); i++) { + assertError(list, i, (Class<Throwable>)classesAndMessages[2 * i], (String)classesAndMessages[2 * i + 1]); + } + } + + /** + * Emit the given values and complete the Processor. + * @param <T> the value type + * @param p the target processor + * @param values the values to emit + */ + @SafeVarargs + public static <T> void emit(Processor<T, ?> p, T... values) { + for (T v : values) { + p.onNext(v); + } + p.onComplete(); + } + + /** + * Emit the given values and complete the Subject. + * @param <T> the value type + * @param p the target subject + * @param values the values to emit + */ + @SafeVarargs + public static <T> void emit(Subject<T> p, T... values) { + for (T v : values) { + p.onNext(v); + } + p.onComplete(); + } + + /** + * Checks if the source is fuseable and its isEmpty/clear works properly. + * @param <T> the value type + * @param source the source sequence + */ + public static <T> void checkFusedIsEmptyClear(Observable<T> source) { + final CountDownLatch cdl = new CountDownLatch(1); + + final Boolean[] state = { null, null, null, null }; + + source.subscribe(new Observer<T>() { + @Override + public void onSubscribe(Disposable d) { + try { + if (d instanceof QueueDisposable) { + @SuppressWarnings("unchecked") + QueueDisposable<Object> qd = (QueueDisposable<Object>) d; + state[0] = true; + + int m = qd.requestFusion(QueueFuseable.ANY); + + if (m != QueueFuseable.NONE) { + state[1] = true; + + state[2] = qd.isEmpty(); + + qd.clear(); + + state[3] = qd.isEmpty(); + } + } + cdl.countDown(); + } finally { + d.dispose(); + } + } + + @Override + public void onNext(T value) { + + } + + @Override + public void onError(Throwable e) { + + } + + @Override + public void onComplete() { + + } + }); + + try { + assertTrue(cdl.await(5, TimeUnit.SECONDS)); + + assertTrue("Not fuseable", state[0]); + assertTrue("Fusion rejected", state[1]); + + assertNotNull(state[2]); + assertTrue("Did not empty", state[3]); + } catch (InterruptedException ex) { + throw new RuntimeException(ex); + } + } + + /** + * Checks if the source is fuseable and its isEmpty/clear works properly. + * @param <T> the value type + * @param source the source sequence + */ + public static <T> void checkFusedIsEmptyClear(Flowable<T> source) { + final CountDownLatch cdl = new CountDownLatch(1); + + final Boolean[] state = { null, null, null, null }; + + source.subscribe(new FlowableSubscriber<T>() { + @Override + public void onSubscribe(Subscription s) { + try { + if (s instanceof QueueSubscription) { + @SuppressWarnings("unchecked") + QueueSubscription<Object> qs = (QueueSubscription<Object>) s; + state[0] = true; + + int m = qs.requestFusion(QueueFuseable.ANY); + + if (m != QueueFuseable.NONE) { + state[1] = true; + + state[2] = qs.isEmpty(); + + qs.clear(); + + state[3] = qs.isEmpty(); + } + } + cdl.countDown(); + } finally { + s.cancel(); + } + } + + @Override + public void onNext(T value) { + + } + + @Override + public void onError(Throwable e) { + + } + + @Override + public void onComplete() { + + } + }); + + try { + assertTrue(cdl.await(5, TimeUnit.SECONDS)); + + assertTrue("Not fuseable", state[0]); + assertTrue("Fusion rejected", state[1]); + + assertNotNull(state[2]); + assertTrue("Did not empty", state[3]); + } catch (InterruptedException ex) { + throw new RuntimeException(ex); + } + } + + /** + * Returns an expanded error list of the given test consumer. + * @param to the test consumer instance + * @return the list + */ + public static List<Throwable> errorList(TestObserverEx<?> to) { + return compositeList(to.errors().get(0)); + } + + /** + * Returns an expanded error list of the given test consumer. + * @param ts the test consumer instance + * @return the list + */ + public static List<Throwable> errorList(TestSubscriberEx<?> ts) { + return compositeList(ts.errors().get(0)); + } + + /** + * Tests the given mapping of a bad Observable by emitting the good values, then an error/completion and then + * a bad value followed by a TestException and and a completion. + * @param <T> the value type + * @param mapper the mapper that receives a bad Observable and returns a reactive base type (detected via reflection). + * @param error if true, the good value emission is followed by a TestException("error"), if false then onComplete is called + * @param badValue the bad value to emit if not null + * @param goodValue the good value to emit before turning bad, if not null + * @param expected the expected resulting values, null to ignore values received + */ + public static <T> void checkBadSourceObservable(Function<Observable<T>, Object> mapper, + final boolean error, final T goodValue, final T badValue, final Object... expected) { + List<Throwable> errors = trackPluginErrors(); + try { + Observable<T> bad = new Observable<T>() { + boolean once; + @Override + protected void subscribeActual(Observer<? super T> observer) { + observer.onSubscribe(Disposable.empty()); + + if (once) { + return; + } + once = true; + + if (goodValue != null) { + observer.onNext(goodValue); + } + + if (error) { + observer.onError(new TestException("error")); + } else { + observer.onComplete(); + } + + if (badValue != null) { + observer.onNext(badValue); + } + observer.onError(new TestException("second")); + observer.onComplete(); + } + }; + + Object o = mapper.apply(bad); + + if (o instanceof ObservableSource) { + ObservableSource<?> os = (ObservableSource<?>) o; + TestObserverEx<Object> to = new TestObserverEx<>(); + + os.subscribe(to); + + to.awaitDone(5, TimeUnit.SECONDS); + + to.assertSubscribed(); + + if (expected != null) { + to.assertValues(expected); + } + if (error) { + to.assertError(TestException.class) + .assertErrorMessage("error") + .assertNotComplete(); + } else { + to.assertNoErrors().assertComplete(); + } + } + + if (o instanceof Publisher) { + Publisher<?> os = (Publisher<?>) o; + TestSubscriberEx<Object> ts = new TestSubscriberEx<>(); + + os.subscribe(ts); + + ts.awaitDone(5, TimeUnit.SECONDS); + + ts.assertSubscribed(); + + if (expected != null) { + ts.assertValues(expected); + } + if (error) { + ts.assertError(TestException.class) + .assertErrorMessage("error") + .assertNotComplete(); + } else { + ts.assertNoErrors().assertComplete(); + } + } + + if (o instanceof SingleSource) { + SingleSource<?> os = (SingleSource<?>) o; + TestObserverEx<Object> to = new TestObserverEx<>(); + + os.subscribe(to); + + to.awaitDone(5, TimeUnit.SECONDS); + + to.assertSubscribed(); + + if (expected != null) { + to.assertValues(expected); + } + if (error) { + to.assertError(TestException.class) + .assertErrorMessage("error") + .assertNotComplete(); + } else { + to.assertNoErrors().assertComplete(); + } + } + + if (o instanceof MaybeSource) { + MaybeSource<?> os = (MaybeSource<?>) o; + TestObserverEx<Object> to = new TestObserverEx<>(); + + os.subscribe(to); + + to.awaitDone(5, TimeUnit.SECONDS); + + to.assertSubscribed(); + + if (expected != null) { + to.assertValues(expected); + } + if (error) { + to.assertError(TestException.class) + .assertErrorMessage("error") + .assertNotComplete(); + } else { + to.assertNoErrors().assertComplete(); + } + } + + if (o instanceof CompletableSource) { + CompletableSource os = (CompletableSource) o; + TestObserverEx<Object> to = new TestObserverEx<>(); + + os.subscribe(to); + + to.awaitDone(5, TimeUnit.SECONDS); + + to.assertSubscribed(); + + if (expected != null) { + to.assertValues(expected); + } + if (error) { + to.assertError(TestException.class) + .assertErrorMessage("error") + .assertNotComplete(); + } else { + to.assertNoErrors().assertComplete(); + } + } + + assertUndeliverable(errors, 0, TestException.class, "second"); + } catch (AssertionError ex) { + throw ex; + } catch (Throwable ex) { + throw new RuntimeException(ex); + } finally { + RxJavaPlugins.reset(); + } + } + + /** + * Tests the given mapping of a bad Observable by emitting the good values, then an error/completion and then + * a bad value followed by a TestException and and a completion. + * @param <T> the value type + * @param mapper the mapper that receives a bad Observable and returns a reactive base type (detected via reflection). + * @param error if true, the good value emission is followed by a TestException("error"), if false then onComplete is called + * @param badValue the bad value to emit if not null + * @param goodValue the good value to emit before turning bad, if not null + * @param expected the expected resulting values, null to ignore values received + */ + public static <T> void checkBadSourceFlowable(Function<Flowable<T>, Object> mapper, + final boolean error, final T goodValue, final T badValue, final Object... expected) { + List<Throwable> errors = trackPluginErrors(); + try { + Flowable<T> bad = new Flowable<T>() { + @Override + protected void subscribeActual(Subscriber<? super T> subscriber) { + subscriber.onSubscribe(new BooleanSubscription()); + + if (goodValue != null) { + subscriber.onNext(goodValue); + } + + if (error) { + subscriber.onError(new TestException("error")); + } else { + subscriber.onComplete(); + } + + if (badValue != null) { + subscriber.onNext(badValue); + } + subscriber.onError(new TestException("second")); + subscriber.onComplete(); + } + }; + + Object o = mapper.apply(bad); + + if (o instanceof ObservableSource) { + ObservableSource<?> os = (ObservableSource<?>) o; + TestObserverEx<Object> to = new TestObserverEx<>(); + + os.subscribe(to); + + to.awaitDone(5, TimeUnit.SECONDS); + + to.assertSubscribed(); + + if (expected != null) { + to.assertValues(expected); + } + if (error) { + to.assertError(TestException.class) + .assertErrorMessage("error") + .assertNotComplete(); + } else { + to.assertNoErrors().assertComplete(); + } + } + + if (o instanceof Publisher) { + Publisher<?> os = (Publisher<?>) o; + TestSubscriberEx<Object> ts = new TestSubscriberEx<>(); + + os.subscribe(ts); + + ts.awaitDone(5, TimeUnit.SECONDS); + + ts.assertSubscribed(); + + if (expected != null) { + ts.assertValues(expected); + } + if (error) { + ts.assertError(TestException.class) + .assertErrorMessage("error") + .assertNotComplete(); + } else { + ts.assertNoErrors().assertComplete(); + } + } + + if (o instanceof SingleSource) { + SingleSource<?> os = (SingleSource<?>) o; + TestObserverEx<Object> to = new TestObserverEx<>(); + + os.subscribe(to); + + to.awaitDone(5, TimeUnit.SECONDS); + + to.assertSubscribed(); + + if (expected != null) { + to.assertValues(expected); + } + if (error) { + to.assertError(TestException.class) + .assertErrorMessage("error") + .assertNotComplete(); + } else { + to.assertNoErrors().assertComplete(); + } + } + + if (o instanceof MaybeSource) { + MaybeSource<?> os = (MaybeSource<?>) o; + TestObserverEx<Object> to = new TestObserverEx<>(); + + os.subscribe(to); + + to.awaitDone(5, TimeUnit.SECONDS); + + to.assertSubscribed(); + + if (expected != null) { + to.assertValues(expected); + } + if (error) { + to.assertError(TestException.class) + .assertErrorMessage("error") + .assertNotComplete(); + } else { + to.assertNoErrors().assertComplete(); + } + } + + if (o instanceof CompletableSource) { + CompletableSource os = (CompletableSource) o; + TestObserverEx<Object> to = new TestObserverEx<>(); + + os.subscribe(to); + + to.awaitDone(5, TimeUnit.SECONDS); + + to.assertSubscribed(); + + if (expected != null) { + to.assertValues(expected); + } + if (error) { + to.assertError(TestException.class) + .assertErrorMessage("error") + .assertNotComplete(); + } else { + to.assertNoErrors().assertComplete(); + } + } + + assertUndeliverable(errors, 0, TestException.class, "second"); + } catch (AssertionError ex) { + throw ex; + } catch (Throwable ex) { + throw new RuntimeException(ex); + } finally { + RxJavaPlugins.reset(); + } + } + + public static <T> void checkInvalidParallelSubscribers(ParallelFlowable<T> source) { + int n = source.parallelism(); + + @SuppressWarnings("unchecked") + TestSubscriber<Object>[] tss = new TestSubscriber[n + 1]; + for (int i = 0; i <= n; i++) { + tss[i] = new TestSubscriber<>().withTag("" + i); + } + + source.subscribe(tss); + + for (int i = 0; i <= n; i++) { + tss[i].assertFailure(IllegalArgumentException.class); + } + } + + /** + * Creates a fuseable Observable that does not emit anything but rejects + * fusion requests. + * @param <T> the element type + * @return the new Observable + */ + public static <T> Observable<T> rejectObservableFusion() { + return new Observable<T>() { + @Override + protected void subscribeActual(Observer<? super T> observer) { + observer.onSubscribe(new QueueDisposable<T>() { + + @Override + public int requestFusion(int mode) { + return 0; + } + + @Override + public boolean offer(T value) { + throw new IllegalStateException(); + } + + @Override + public boolean offer(T v1, T v2) { + throw new IllegalStateException(); + } + + @Override + public T poll() throws Exception { + return null; + } + + @Override + public boolean isEmpty() { + return true; + } + + @Override + public void clear() { + } + + @Override + public void dispose() { + } + + @Override + public boolean isDisposed() { + return false; + } + }); + } + }; + } + + /** + * Creates a fuseable Flowable that does not emit anything but rejects + * fusion requests. + * @param <T> the element type + * @return the new Observable + */ + public static <T> Flowable<T> rejectFlowableFusion() { + return new Flowable<T>() { + @Override + protected void subscribeActual(Subscriber<? super T> subscriber) { + subscriber.onSubscribe(new QueueSubscription<T>() { + + @Override + public int requestFusion(int mode) { + return 0; + } + + @Override + public boolean offer(T value) { + throw new IllegalStateException(); + } + + @Override + public boolean offer(T v1, T v2) { + throw new IllegalStateException(); + } + + @Override + public T poll() throws Exception { + return null; + } + + @Override + public boolean isEmpty() { + return true; + } + + @Override + public void clear() { + } + + @Override + public void cancel() { + } + + @Override + public void request(long n) { + } + }); + } + }; + } + + static final class FlowableStripBoundary<T> extends Flowable<T> implements FlowableTransformer<T, T> { + + final Flowable<T> source; + + FlowableStripBoundary(Flowable<T> source) { + this.source = source; + } + + @Override + public Flowable<T> apply(Flowable<T> upstream) { + return new FlowableStripBoundary<>(upstream); + } + + @Override + protected void subscribeActual(Subscriber<? super T> s) { + source.subscribe(new StripBoundarySubscriber<>(s)); + } + + static final class StripBoundarySubscriber<T> implements FlowableSubscriber<T>, QueueSubscription<T> { + + final Subscriber<? super T> downstream; + + Subscription upstream; + + QueueSubscription<T> qs; + + StripBoundarySubscriber(Subscriber<? super T> downstream) { + this.downstream = downstream; + } + + @SuppressWarnings("unchecked") + @Override + public void onSubscribe(Subscription subscription) { + this.upstream = subscription; + if (subscription instanceof QueueSubscription) { + qs = (QueueSubscription<T>)subscription; + } + downstream.onSubscribe(this); + } + + @Override + public void onNext(T t) { + downstream.onNext(t); + } + + @Override + public void onError(Throwable throwable) { + downstream.onError(throwable); + } + + @Override + public void onComplete() { + downstream.onComplete(); + } + + @Override + public int requestFusion(int mode) { + QueueSubscription<T> fs = qs; + if (fs != null) { + return fs.requestFusion(mode & ~BOUNDARY); + } + return NONE; + } + + @Override + public boolean offer(T value) { + throw new UnsupportedOperationException("Should not be called"); + } + + @Override + public boolean offer(T v1, T v2) { + throw new UnsupportedOperationException("Should not be called"); + } + + @Override + public T poll() throws Throwable { + return qs.poll(); + } + + @Override + public void clear() { + qs.clear(); + } + + @Override + public boolean isEmpty() { + return qs.isEmpty(); + } + + @Override + public void request(long n) { + upstream.request(n); + } + + @Override + public void cancel() { + upstream.cancel(); + } + } + } + + /** + * Strips the {@link QueueFuseable#BOUNDARY} mode flag when the downstream calls {@link QueueSubscription#requestFusion(int)}. + * <p> + * By default, many operators use {@link QueueFuseable#BOUNDARY} to indicate upstream side-effects + * should not leak over a fused boundary. However, some tests want to verify if {@link QueueSubscription#poll()} crashes + * are handled correctly and the most convenient way is to crash {@link Flowable#map} that won't fuse with {@code BOUNDARY} + * flag. This transformer strips this flag and thus allows the function of {@code map} to be executed as part of the + * {@code poll()} chain. + * @param <T> the element type of the flow + * @return the new Transformer instance + */ + public static <T> FlowableTransformer<T, T> flowableStripBoundary() { + return new FlowableStripBoundary<>(null); + } + + static final class ObservableStripBoundary<T> extends Observable<T> implements ObservableTransformer<T, T> { + + final Observable<T> source; + + ObservableStripBoundary(Observable<T> source) { + this.source = source; + } + + @Override + public Observable<T> apply(Observable<T> upstream) { + return new ObservableStripBoundary<>(upstream); + } + + @Override + protected void subscribeActual(Observer<? super T> observer) { + source.subscribe(new StripBoundaryObserver<>(observer)); + } + + static final class StripBoundaryObserver<T> implements Observer<T>, QueueDisposable<T> { + + final Observer<? super T> downstream; + + Disposable upstream; + + QueueDisposable<T> qd; + + StripBoundaryObserver(Observer<? super T> downstream) { + this.downstream = downstream; + } + + @SuppressWarnings("unchecked") + @Override + public void onSubscribe(Disposable d) { + this.upstream = d; + if (d instanceof QueueDisposable) { + qd = (QueueDisposable<T>)d; + } + downstream.onSubscribe(this); + } + + @Override + public void onNext(T t) { + downstream.onNext(t); + } + + @Override + public void onError(Throwable throwable) { + downstream.onError(throwable); + } + + @Override + public void onComplete() { + downstream.onComplete(); + } + + @Override + public int requestFusion(int mode) { + QueueDisposable<T> fs = qd; + if (fs != null) { + return fs.requestFusion(mode & ~BOUNDARY); + } + return NONE; + } + + @Override + public boolean offer(T value) { + throw new UnsupportedOperationException("Should not be called"); + } + + @Override + public boolean offer(T v1, T v2) { + throw new UnsupportedOperationException("Should not be called"); + } + + @Override + public T poll() throws Throwable { + return qd.poll(); + } + + @Override + public void clear() { + qd.clear(); + } + + @Override + public boolean isEmpty() { + return qd.isEmpty(); + } + + @Override + public void dispose() { + upstream.dispose(); + } + + @Override + public boolean isDisposed() { + return upstream.isDisposed(); + } + } + } + + /** + * Strips the {@link QueueFuseable#BOUNDARY} mode flag when the downstream calls {@link QueueDisposable#requestFusion(int)}. + * <p> + * By default, many operators use {@link QueueFuseable#BOUNDARY} to indicate upstream side-effects + * should not leak over a fused boundary. However, some tests want to verify if {@link QueueDisposable#poll()} crashes + * are handled correctly and the most convenient way is to crash {@link Observable#map} that won't fuse with {@code BOUNDARY} + * flag. This transformer strips this flag and thus allows the function of {@code map} to be executed as part of the + * {@code poll()} chain. + * @param <T> the element type of the flow + * @return the new Transformer instance + */ + public static <T> ObservableTransformer<T, T> observableStripBoundary() { + return new ObservableStripBoundary<>(null); + } + + public static <T> TestConsumerExConverters<T> testConsumer() { + return new TestConsumerExConverters<>(false, 0); + } + + public static <T> TestConsumerExConverters<T> testConsumer(boolean cancelled) { + return new TestConsumerExConverters<>(cancelled, 0); + } + + public static <T> TestConsumerExConverters<T> testConsumer(final int fusionMode, final boolean cancelled) { + return new TestConsumerExConverters<>(cancelled, fusionMode); + } + + public static <T> TestConsumerExConverters<T> testConsumer(final boolean cancelled, final int fusionMode) { + return new TestConsumerExConverters<>(cancelled, fusionMode); + } + + public static <T> FlowableConverter<T, TestSubscriberEx<T>> testSubscriber(final long initialRequest) { + return testSubscriber(initialRequest, false, 0); + } + + public static <T> FlowableConverter<T, TestSubscriberEx<T>> testSubscriber(final long initialRequest, final boolean cancelled) { + return testSubscriber(initialRequest, cancelled, 0); + } + + public static <T> FlowableConverter<T, TestSubscriberEx<T>> testSubscriber(final long initialRequest, final int fusionMode, final boolean cancelled) { + return testSubscriber(initialRequest, cancelled, fusionMode); + } + + public static <T> FlowableConverter<T, TestSubscriberEx<T>> testSubscriber(final long initialRequest, final boolean cancelled, final int fusionMode) { + return new FlowableConverter<T, TestSubscriberEx<T>>() { + @Override + public TestSubscriberEx<T> apply(Flowable<T> f) { + TestSubscriberEx<T> tse = new TestSubscriberEx<>(initialRequest); + if (cancelled) { + tse.cancel(); + } + tse.setInitialFusionMode(fusionMode); + return f.subscribeWith(tse); + } + }; + } + + public static final class TestConsumerExConverters<T> implements + ObservableConverter<T, TestObserverEx<T>>, + SingleConverter<T, TestObserverEx<T>>, + MaybeConverter<T, TestObserverEx<T>>, + CompletableConverter<TestObserverEx<Void>>, + FlowableConverter<T, TestSubscriberEx<T>> { + + final boolean cancelled; + + final int fusionMode; + + TestConsumerExConverters(boolean cancelled, int fusionMode) { + this.cancelled = cancelled; + this.fusionMode = fusionMode; + } + + @Override + public TestObserverEx<Void> apply(Completable upstream) { + TestObserverEx<Void> toe = new TestObserverEx<>(); + if (cancelled) { + toe.dispose(); + } + toe.setInitialFusionMode(fusionMode); + return upstream.subscribeWith(toe); + } + + @Override + public TestObserverEx<T> apply(Maybe<T> upstream) { + TestObserverEx<T> toe = new TestObserverEx<>(); + if (cancelled) { + toe.dispose(); + } + toe.setInitialFusionMode(fusionMode); + return upstream.subscribeWith(toe); + } + + @Override + public TestObserverEx<T> apply(Single<T> upstream) { + TestObserverEx<T> toe = new TestObserverEx<>(); + if (cancelled) { + toe.dispose(); + } + toe.setInitialFusionMode(fusionMode); + return upstream.subscribeWith(toe); + } + + @Override + public TestObserverEx<T> apply(Observable<T> upstream) { + TestObserverEx<T> toe = new TestObserverEx<>(); + if (cancelled) { + toe.dispose(); + } + toe.setInitialFusionMode(fusionMode); + return upstream.subscribeWith(toe); + } + + @Override + public TestSubscriberEx<T> apply(Flowable<T> upstream) { + TestSubscriberEx<T> tse = new TestSubscriberEx<>(); + if (cancelled) { + tse.dispose(); + } + tse.setInitialFusionMode(fusionMode); + return upstream.subscribeWith(tse); + } + } + + @SafeVarargs + public static <T> TestSubscriberEx<T> assertValueSet(TestSubscriberEx<T> ts, T... values) { + Set<T> expectedSet = new HashSet<>(Arrays.asList(values)); + for (T t : ts.values()) { + if (!expectedSet.contains(t)) { + throw ts.failWith("Item not in the set: " + BaseTestConsumer.valueAndClass(t)); + } + } + return ts; + } + + @SafeVarargs + public static <T> TestObserverEx<T> assertValueSet(TestObserverEx<T> to, T... values) { + Set<T> expectedSet = new HashSet<>(Arrays.asList(values)); + for (T t : to.values()) { + if (!expectedSet.contains(t)) { + throw to.failWith("Item not in the set: " + BaseTestConsumer.valueAndClass(t)); + } + } + return to; + } + + /** + * Given a base reactive type name, try to find its source in the current runtime + * path and return a file to it or null if not found. + * @param baseClassName the class name such as {@code Maybe} + * @return the File pointing to the source + * @throws Exception on error + */ + public static File findSource(String baseClassName) throws Exception { + return findSource(baseClassName, "io.reactivex.rxjava3.core"); + } + + /** + * Given a base reactive type name, try to find its source in the current runtime + * path and return a file to it or null if not found. + * @param baseClassName the class name such as {@code Maybe} + * @param parentPackage the parent package such as {@code io.reactivex.rxjava3.core} + * @return the File pointing to the source + * @throws Exception on error + */ + public static File findSource(String baseClassName, String parentPackage) throws Exception { + URL u = TestHelper.class.getResource(TestHelper.class.getSimpleName() + ".class"); + + String path = new File(u.toURI()).toString().replace('\\', '/'); + + parentPackage = parentPackage.replace(".", "/"); +// System.out.println(path); + + // Locate the src/main/java directory + String p = null; + while (true) { + int idx = path.lastIndexOf("/"); + if (idx < 0) { + break; + } + path = path.substring(0, idx); + String check = path + "/src/main/java"; + + if (new File(check).exists()) { + p = check + "/" + parentPackage + "/" + baseClassName + ".java"; + break; + } + } + + if (p == null) { + System.err.println("Unable to locate the RxJava sources"); + return null; + } + + File f = new File(p); + + if (f.canRead()) { + return f; + } + + System.out.println("Can't read " + p); + return null; + } + + /** + * Cancels a flow before notifying a transformation and checks if an undeliverable exception + * has been signaled due to the cancellation. + * @param transform the operator to test + * @param <T> the output type of the transformation + */ + public static <T> void checkUndeliverableUponCancel(FlowableConverter<Integer, T> transform) { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + + final SerialDisposable disposable = new SerialDisposable(); + + T result = Flowable.just(1) + .map(new Function<Integer, Integer>() { + @Override + public Integer apply(Integer v) throws Throwable { + disposable.dispose(); + throw new TestException(); + } + }) + .to(transform); + + if (result instanceof MaybeSource) { + TestObserverEx<Object> to = new TestObserverEx<>(); + disposable.set(to); + + ((MaybeSource<?>)result) + .subscribe(to); + to.assertEmpty(); + } else if (result instanceof SingleSource) { + TestObserverEx<Object> to = new TestObserverEx<>(); + disposable.set(to); + + ((SingleSource<?>)result) + .subscribe(to); + to.assertEmpty(); + } else if (result instanceof CompletableSource) { + TestObserverEx<Object> to = new TestObserverEx<>(); + disposable.set(to); + + ((CompletableSource)result) + .subscribe(to); + to.assertEmpty(); + } else if (result instanceof ObservableSource) { + TestObserverEx<Object> to = new TestObserverEx<>(); + disposable.set(to); + + ((ObservableSource<?>)result) + .subscribe(to); + to.assertEmpty(); + } else if (result instanceof Publisher) { + TestSubscriberEx<Object> ts = new TestSubscriberEx<>(); + disposable.set(Disposable.fromSubscription(ts)); + + ((Publisher<?>)result) + .subscribe(ts); + ts.assertEmpty(); + } else { + fail("Unsupported transformation output: " + result + " of class " + (result != null ? result.getClass() : " <null>")); + } + + assertFalse("No undeliverable errors received", errors.isEmpty()); + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + /** + * Cancels a flow before notifying a transformation and checks if an undeliverable exception + * has been signaled due to the cancellation. + * @param transform the operator to test + * @param <T> the output type of the transformation + */ + public static <T> void checkUndeliverableUponCancel(ObservableConverter<Integer, T> transform) { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + + final SerialDisposable disposable = new SerialDisposable(); + + T result = Observable.just(1) + .map(new Function<Integer, Integer>() { + @Override + public Integer apply(Integer v) throws Throwable { + disposable.dispose(); + throw new TestException(); + } + }) + .to(transform); + + if (result instanceof MaybeSource) { + TestObserverEx<Object> to = new TestObserverEx<>(); + disposable.set(to); + + ((MaybeSource<?>)result) + .subscribe(to); + to.assertEmpty(); + } else if (result instanceof SingleSource) { + TestObserverEx<Object> to = new TestObserverEx<>(); + disposable.set(to); + + ((SingleSource<?>)result) + .subscribe(to); + to.assertEmpty(); + } else if (result instanceof CompletableSource) { + TestObserverEx<Object> to = new TestObserverEx<>(); + disposable.set(to); + + ((CompletableSource)result) + .subscribe(to); + to.assertEmpty(); + } else if (result instanceof ObservableSource) { + TestObserverEx<Object> to = new TestObserverEx<>(); + disposable.set(to); + + ((ObservableSource<?>)result) + .subscribe(to); + to.assertEmpty(); + } else if (result instanceof Publisher) { + TestSubscriberEx<Object> ts = new TestSubscriberEx<>(); + disposable.set(Disposable.fromSubscription(ts)); + + ((Publisher<?>)result) + .subscribe(ts); + ts.assertEmpty(); + } else { + fail("Unsupported transformation output: " + result + " of class " + (result != null ? result.getClass() : " <null>")); + } + + assertFalse("No undeliverable errors received", errors.isEmpty()); + TestHelper.assertUndeliverable(errors, 0, TestException.class); + } finally { + RxJavaPlugins.reset(); + } + } + + /** + * Repeatedly calls System.gc() and sleeps until the current memory usage + * is less than the given expected usage or the given number of wait loop/time + * has passed. + * @param oneSleep how many milliseconds to sleep after a GC. + * @param maxLoop the maximum number of GC/sleep calls. + * @param expectedMemoryUsage the memory usage in bytes at max + * @return the actual memory usage after the loop + * @throws InterruptedException if the sleep is interrupted + */ + public static long awaitGC(long oneSleep, int maxLoop, long expectedMemoryUsage) throws InterruptedException { + MemoryMXBean bean = ManagementFactory.getMemoryMXBean(); + + System.gc(); + + int i = maxLoop; + while (i-- != 0) { + long usage = bean.getHeapMemoryUsage().getUsed(); + if (usage <= expectedMemoryUsage) { + return usage; + } + System.gc(); + Thread.sleep(oneSleep); + } + return bean.getHeapMemoryUsage().getUsed(); + } + + /** + * Enable thracking of the global errors for the duration of the action. + * @param action the action to run with a list of errors encountered + * @throws Throwable the exception rethrown from the action + */ + public static void withErrorTracking(Consumer<List<Throwable>> action) throws Throwable { + List<Throwable> errors = trackPluginErrors(); + try { + action.accept(errors); + } finally { + RxJavaPlugins.reset(); + } + } + + /** + * Assert if the given CompletableFuture fails with a specified error inside an ExecutionException. + * @param cf the CompletableFuture to test + * @param error the error class expected + */ + public static void assertError(CompletableFuture<?> cf, Class<? extends Throwable> error) { + try { + cf.get(); + fail("Should have thrown!"); + } catch (Throwable ex) { + if (!error.isInstance(ex.getCause())) { + ex.printStackTrace(); + fail("Wrong cause: " + ex.getCause()); + } + } + } + + /** + * Syncs the execution of the given runnable with the execution of the + * current thread. + * @param run the other task to run in sync with the current thread + * @param resume the latch to count down after the {@code run} + */ + public static void raceOther(Runnable run, CountDownLatch resume) { + AtomicInteger sync = new AtomicInteger(2); + + Schedulers.single().scheduleDirect(() -> { + if (sync.decrementAndGet() != 0) { + while (sync.get() != 0) { } + } + + run.run(); + + resume.countDown(); + }); + + if (sync.decrementAndGet() != 0) { + while (sync.get() != 0) { } + } + } + + /** + * Inserts a ConditionalSubscriber into the chain to trigger the conditional paths + * without interfering with the requestFusion parts. + * @param <T> the element type + * @return the new FlowableTransformer instance + */ + public static <T> FlowableTransformer<T, T> conditional() { + return f -> new Flowable<T>() { + @Override + protected void subscribeActual(@NonNull Subscriber<@NonNull ? super T> subscriber) { + f.subscribe(new ForwardingConditionalSubscriber<>(subscriber)); + } + }; + } + + /** + * Wraps a Subscriber and exposes it as a fuseable conditional subscriber without interfering with + * requestFusion. + * @param <T> the element type + */ + static final class ForwardingConditionalSubscriber<T> extends BasicQueueSubscription<T> implements ConditionalSubscriber<T> { + + private static final long serialVersionUID = 365317603608134078L; + + final Subscriber<? super T> downstream; + + Subscription upstream; + + QueueSubscription<T> qs; + + ForwardingConditionalSubscriber(Subscriber<? super T> downstream) { + this.downstream = downstream; + } + + @SuppressWarnings("unchecked") + @Override + public void onSubscribe(@NonNull Subscription s) { + this.upstream = s; + if (s instanceof QueueSubscription) { + this.qs = (QueueSubscription<T>)s; + } + downstream.onSubscribe(this); + } + + @Override + public void onNext(@NonNull T t) { + downstream.onNext(t); + } + + @Override + public boolean tryOnNext(@NonNull T t) { + downstream.onNext(t); + return true; + } + + @Override + public void onError(Throwable t) { + downstream.onError(t); + } + + @Override + public void onComplete() { + downstream.onComplete(); + } + + @Override + public int requestFusion(int mode) { + return qs != null ? qs.requestFusion(mode) : 0; + } + + @Override + public @Nullable T poll() throws Throwable { + return qs.poll(); + } + + @Override + public boolean isEmpty() { + return qs.isEmpty(); + } + + @Override + public void clear() { + qs.clear(); + } + + @Override + public void request(long n) { + upstream.request(n); + } + + @Override + public void cancel() { + upstream.cancel(); + } + } +} diff --git a/src/test/java/io/reactivex/rxjava3/testsupport/TestObserverEx.java b/src/test/java/io/reactivex/rxjava3/testsupport/TestObserverEx.java new file mode 100644 index 0000000000..f029ce5457 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/testsupport/TestObserverEx.java @@ -0,0 +1,320 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.testsupport; + +import java.util.concurrent.atomic.AtomicReference; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.internal.disposables.DisposableHelper; +import io.reactivex.rxjava3.operators.QueueDisposable; +import io.reactivex.rxjava3.operators.QueueFuseable; + +/** + * An extended test Observer that records events and allows making assertions about them. + * + * <p>You can override the onSubscribe, onNext, onError, onComplete, onSuccess and + * cancel methods but not the others (this is by design). + * + * <p>The TestObserver implements Disposable for convenience where dispose calls cancel. + * + * @param <T> the value type + */ +public class TestObserverEx<T> +extends BaseTestConsumerEx<T, TestObserverEx<T>> +implements Observer<T>, Disposable, MaybeObserver<T>, SingleObserver<T>, CompletableObserver { + /** The actual observer to forward events to. */ + private final Observer<? super T> downstream; + + /** Holds the current subscription if any. */ + private final AtomicReference<Disposable> upstream = new AtomicReference<>(); + + private QueueDisposable<T> qd; + + /** + * Constructs a non-forwarding TestObserver. + */ + public TestObserverEx() { + this(EmptyObserver.INSTANCE); + } + + /** + * Constructs a forwarding TestObserver. + * @param downstream the actual Observer to forward events to + */ + public TestObserverEx(Observer<? super T> downstream) { + this.downstream = downstream; + } + + /** + * Constructs a TestObserverEx with the given initial fusion mode. + * @param fusionMode the fusion mode, see {@link QueueFuseable} + */ + public TestObserverEx(int fusionMode) { + this(); + setInitialFusionMode(fusionMode); + } + + @SuppressWarnings("unchecked") + @Override + public void onSubscribe(Disposable d) { + lastThread = Thread.currentThread(); + + if (d == null) { + errors.add(new NullPointerException("onSubscribe received a null Subscription")); + return; + } + if (!upstream.compareAndSet(null, d)) { + d.dispose(); + if (upstream.get() != DisposableHelper.DISPOSED) { + errors.add(new IllegalStateException("onSubscribe received multiple subscriptions: " + d)); + } + return; + } + + if (initialFusionMode != 0) { + if (d instanceof QueueDisposable) { + qd = (QueueDisposable<T>)d; + + int m = qd.requestFusion(initialFusionMode); + establishedFusionMode = m; + + if (m == QueueFuseable.SYNC) { + checkSubscriptionOnce = true; + lastThread = Thread.currentThread(); + try { + T t; + while ((t = qd.poll()) != null) { + values.add(t); + } + completions++; + + upstream.lazySet(DisposableHelper.DISPOSED); + } catch (Throwable ex) { + errors.add(ex); + } + return; + } + } + } + + downstream.onSubscribe(d); + } + + @Override + public void onNext(T t) { + if (!checkSubscriptionOnce) { + checkSubscriptionOnce = true; + if (upstream.get() == null) { + errors.add(new IllegalStateException("onSubscribe not called in proper order")); + } + } + + lastThread = Thread.currentThread(); + + if (establishedFusionMode == QueueFuseable.ASYNC) { + try { + while ((t = qd.poll()) != null) { + values.add(t); + } + } catch (Throwable ex) { + errors.add(ex); + qd.dispose(); + } + return; + } + + values.add(t); + + if (t == null) { + errors.add(new NullPointerException("onNext received a null value")); + } + + downstream.onNext(t); + } + + @Override + public void onError(Throwable t) { + if (!checkSubscriptionOnce) { + checkSubscriptionOnce = true; + if (upstream.get() == null) { + errors.add(new IllegalStateException("onSubscribe not called in proper order")); + } + } + + try { + lastThread = Thread.currentThread(); + if (t == null) { + errors.add(new NullPointerException("onError received a null Throwable")); + } else { + errors.add(t); + } + + downstream.onError(t); + } finally { + done.countDown(); + } + } + + @Override + public void onComplete() { + if (!checkSubscriptionOnce) { + checkSubscriptionOnce = true; + if (upstream.get() == null) { + errors.add(new IllegalStateException("onSubscribe not called in proper order")); + } + } + + try { + lastThread = Thread.currentThread(); + completions++; + + downstream.onComplete(); + } finally { + done.countDown(); + } + } + + @Override + public final void dispose() { + DisposableHelper.dispose(upstream); + } + + @Override + public final boolean isDisposed() { + return DisposableHelper.isDisposed(upstream.get()); + } + + // state retrieval methods + /** + * Returns true if this TestObserver received a subscription. + * @return true if this TestObserver received a subscription + */ + public final boolean hasSubscription() { + return upstream.get() != null; + } + + /** + * Assert that the onSubscribe method was called exactly once. + * @return this; + */ + @Override + public final TestObserverEx<T> assertSubscribed() { + if (upstream.get() == null) { + throw fail("Not subscribed!"); + } + return this; + } + + /** + * Assert that the onSubscribe method hasn't been called at all. + * @return this; + */ + public final TestObserverEx<T> assertNotSubscribed() { + if (upstream.get() != null) { + throw fail("Subscribed!"); + } else + if (!errors.isEmpty()) { + throw fail("Not subscribed but errors found"); + } + return this; + } + + /** + * Sets the initial fusion mode if the upstream supports fusion. + * <p>Package-private: avoid leaking the now internal fusion properties into the public API. + * Use ObserverFusion to work with such tests. + * @param mode the mode to establish, see the {@link QueueDisposable} constants + * @return this + */ + public final TestObserverEx<T> setInitialFusionMode(int mode) { + this.initialFusionMode = mode; + return this; + } + + /** + * Asserts that the given fusion mode has been established + * <p>Package-private: avoid leaking the now internal fusion properties into the public API. + * Use ObserverFusion to work with such tests. + * @param mode the expected mode + * @return this + */ + public final TestObserverEx<T> assertFusionMode(int mode) { + int m = establishedFusionMode; + if (m != mode) { + if (qd != null) { + throw new AssertionError("\nexpected: " + fusionModeToString(mode) + + "\ngot: " + fusionModeToString(m) + "; Fusion mode different"); + } else { + throw fail("Upstream is not fuseable"); + } + } + return this; + } + + /** + * Assert that the upstream is a fuseable source. + * <p>Package-private: avoid leaking the now internal fusion properties into the public API. + * Use ObserverFusion to work with such tests. + * @return this + */ + public final TestObserverEx<T> assertFuseable() { + if (qd == null) { + throw new AssertionError("Upstream is not fuseable."); + } + return this; + } + + /** + * Assert that the upstream is not a fuseable source. + * <p>Package-private: avoid leaking the now internal fusion properties into the public API. + * Use ObserverFusion to work with such tests. + * @return this + */ + public final TestObserverEx<T> assertNotFuseable() { + if (qd != null) { + throw new AssertionError("Upstream is fuseable."); + } + return this; + } + + @Override + public void onSuccess(T value) { + onNext(value); + onComplete(); + } + + /** + * An observer that ignores all events and does not report errors. + */ + enum EmptyObserver implements Observer<Object> { + INSTANCE; + + @Override + public void onSubscribe(Disposable d) { + } + + @Override + public void onNext(Object t) { + } + + @Override + public void onError(Throwable t) { + } + + @Override + public void onComplete() { + } + } +} diff --git a/src/test/java/io/reactivex/rxjava3/testsupport/TestObserverExTest.java b/src/test/java/io/reactivex/rxjava3/testsupport/TestObserverExTest.java new file mode 100644 index 0000000000..2296ceb757 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/testsupport/TestObserverExTest.java @@ -0,0 +1,1374 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.testsupport; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +import java.io.IOException; +import java.util.*; +import java.util.concurrent.TimeUnit; + +import org.junit.Test; +import org.mockito.InOrder; + +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.core.Observer; +import io.reactivex.rxjava3.core.RxJavaTest; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.internal.operators.observable.ObservableScalarXMap.ScalarDisposable; +import io.reactivex.rxjava3.observers.TestObserver; +import io.reactivex.rxjava3.operators.QueueFuseable; +import io.reactivex.rxjava3.schedulers.Schedulers; +import io.reactivex.rxjava3.subjects.*; + +public class TestObserverExTest extends RxJavaTest { + + @Test + public void assertTestObserverEx() { + Observable<Integer> oi = Observable.fromIterable(Arrays.asList(1, 2)); + TestObserverEx<Integer> subscriber = new TestObserverEx<>(); + oi.subscribe(subscriber); + + subscriber.assertValues(1, 2); + subscriber.assertValueCount(2); + subscriber.assertTerminated(); + } + + @Test + public void assertNotMatchCount() { + assertThrows(AssertionError.class, () -> { + Observable<Integer> oi = Observable.fromIterable(Arrays.asList(1, 2)); + TestObserverEx<Integer> subscriber = new TestObserverEx<>(); + oi.subscribe(subscriber); + + subscriber.assertValue(1); + subscriber.assertValueCount(2); + subscriber.assertTerminated(); + }); + } + + @Test + public void assertNotMatchValue() { + assertThrows(AssertionError.class, () -> { + Observable<Integer> oi = Observable.fromIterable(Arrays.asList(1, 2)); + TestObserverEx<Integer> subscriber = new TestObserverEx<>(); + oi.subscribe(subscriber); + + subscriber.assertValues(1, 3); + subscriber.assertValueCount(2); + subscriber.assertTerminated(); + }); + } + + @Test + public void assertNeverAtNotMatchingValue() { + Observable<Integer> oi = Observable.fromIterable(Arrays.asList(1, 2)); + TestObserverEx<Integer> subscriber = new TestObserverEx<>(); + oi.subscribe(subscriber); + + subscriber.assertNever(3); + subscriber.assertValueCount(2); + subscriber.assertTerminated(); + } + + @Test + public void assertNeverAtMatchingValue() { + assertThrows(AssertionError.class, () -> { + Observable<Integer> oi = Observable.fromIterable(Arrays.asList(1, 2)); + TestObserverEx<Integer> subscriber = new TestObserverEx<>(); + oi.subscribe(subscriber); + + subscriber.assertValues(1, 2); + + subscriber.assertNever(2); + subscriber.assertValueCount(2); + subscriber.assertTerminated(); + }); + } + + @Test + public void assertNeverAtMatchingPredicate() { + assertThrows(AssertionError.class, () -> { + TestObserverEx<Integer> to = new TestObserverEx<>(); + + Observable.just(1, 2).subscribe(to); + + to.assertValues(1, 2); + + to.assertNever(new Predicate<Integer>() { + @Override + public boolean test(final Integer o) throws Exception { + return o == 1; + } + }); + }); + } + + @Test + public void assertNeverAtNotMatchingPredicate() { + TestObserverEx<Integer> to = new TestObserverEx<>(); + + Observable.just(2, 3).subscribe(to); + + to.assertNever(new Predicate<Integer>() { + @Override + public boolean test(final Integer o) throws Exception { + return o == 1; + } + }); + } + + @Test + public void assertTerminalEventNotReceived() { + assertThrows(AssertionError.class, () -> { + PublishSubject<Integer> p = PublishSubject.create(); + TestObserverEx<Integer> subscriber = new TestObserverEx<>(); + p.subscribe(subscriber); + + p.onNext(1); + p.onNext(2); + + subscriber.assertValues(1, 2); + subscriber.assertValueCount(2); + subscriber.assertTerminated(); + }); + } + + @Test + public void wrappingMock() { + Observable<Integer> oi = Observable.fromIterable(Arrays.asList(1, 2)); + + Observer<Integer> mockSubscriber = TestHelper.mockObserver(); + + oi.subscribe(new TestObserverEx<>(mockSubscriber)); + + InOrder inOrder = inOrder(mockSubscriber); + inOrder.verify(mockSubscriber, times(1)).onNext(1); + inOrder.verify(mockSubscriber, times(1)).onNext(2); + inOrder.verify(mockSubscriber, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void wrappingMockWhenUnsubscribeInvolved() { + Observable<Integer> oi = Observable.fromIterable(Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9)).take(2); + Observer<Integer> mockSubscriber = TestHelper.mockObserver(); + oi.subscribe(new TestObserverEx<>(mockSubscriber)); + + InOrder inOrder = inOrder(mockSubscriber); + inOrder.verify(mockSubscriber, times(1)).onNext(1); + inOrder.verify(mockSubscriber, times(1)).onNext(2); + inOrder.verify(mockSubscriber, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void errorSwallowed() { + Observable.error(new RuntimeException()).subscribe(new TestObserverEx<>()); + } + + @Test + public void nullExpected() { + TestObserverEx<Integer> to = new TestObserverEx<>(); + to.onNext(1); + + try { + to.assertValue((Integer) null); + } catch (AssertionError ex) { + // this is expected + return; + } + fail("Null element check assertion didn't happen!"); + } + + @Test + public void nullActual() { + TestObserverEx<Integer> to = new TestObserverEx<>(); + to.onNext(null); + + try { + to.assertValue(1); + } catch (AssertionError ex) { + // this is expected + return; + } + fail("Null element check assertion didn't happen!"); + } + + @Test + public void terminalErrorOnce() { + TestObserverEx<Integer> to = new TestObserverEx<>(); + to.onError(new TestException()); + to.onError(new TestException()); + + try { + to.assertTerminated(); + } catch (AssertionError ex) { + // this is expected + return; + } + fail("Failed to report multiple onError terminal events!"); + } + + @Test + public void terminalCompletedOnce() { + TestObserverEx<Integer> to = new TestObserverEx<>(); + to.onComplete(); + to.onComplete(); + + try { + to.assertTerminated(); + } catch (AssertionError ex) { + // this is expected + return; + } + fail("Failed to report multiple onComplete terminal events!"); + } + + @Test + public void terminalOneKind() { + TestObserverEx<Integer> to = new TestObserverEx<>(); + to.onError(new TestException()); + to.onComplete(); + + try { + to.assertTerminated(); + } catch (AssertionError ex) { + // this is expected + return; + } + fail("Failed to report multiple kinds of events!"); + } + + @Test + public void createDelegate() { + TestObserverEx<Integer> to1 = new TestObserverEx<>(); + + TestObserverEx<Integer> to = new TestObserverEx<>(to1); + + to.assertNotSubscribed(); + + assertFalse(to.hasSubscription()); + + to.onSubscribe(Disposable.empty()); + + try { + to.assertNotSubscribed(); + throw new RuntimeException("Should have thrown"); + } catch (AssertionError ex) { + // expected + } + + assertTrue(to.hasSubscription()); + + assertFalse(to.isDisposed()); + + to.onNext(1); + to.onError(new TestException()); + to.onComplete(); + + to1.assertValue(1).assertError(TestException.class).assertComplete(); + + to.dispose(); + + assertTrue(to.isDisposed()); + + assertSame(Thread.currentThread(), to.lastThread()); + + try { + to.assertNoValues(); + throw new RuntimeException("Should have thrown"); + } catch (AssertionError exc) { + // expected + } + + try { + to.assertValueCount(0); + throw new RuntimeException("Should have thrown"); + } catch (AssertionError exc) { + // expected + } + + to.assertValueSequence(Collections.singletonList(1)); + + try { + to.assertValueSequence(Collections.singletonList(2)); + throw new RuntimeException("Should have thrown"); + } catch (AssertionError exc) { + // expected + } + } + + @Test + public void assertError() { + TestObserverEx<Integer> to = new TestObserverEx<>(); + + try { + to.assertError(TestException.class); + throw new RuntimeException("Should have thrown"); + } catch (AssertionError ex) { + // expected + } + + try { + to.assertError(new TestException()); + throw new RuntimeException("Should have thrown"); + } catch (AssertionError ex) { + // expected + } + + try { + to.assertError(Functions.<Throwable>alwaysTrue()); + throw new RuntimeException("Should have thrown"); + } catch (AssertionError ex) { + // expected + } + + try { + to.assertErrorMessage(""); + throw new RuntimeException("Should have thrown"); + } catch (AssertionError exc) { + // expected + } + + try { + to.assertSubscribed(); + throw new RuntimeException("Should have thrown"); + } catch (AssertionError exc) { + // expected + } + + try { + to.assertTerminated(); + throw new RuntimeException("Should have thrown"); + } catch (AssertionError exc) { + // expected + } + + to.onSubscribe(Disposable.empty()); + + to.assertSubscribed(); + + to.assertNoErrors(); + + TestException ex = new TestException("Forced failure"); + + to.onError(ex); + + to.assertError(ex); + + to.assertError(TestException.class); + + to.assertError(Functions.<Throwable>alwaysTrue()); + + to.assertError(new Predicate<Throwable>() { + @Override + public boolean test(Throwable t) throws Exception { + return t.getMessage() != null && t.getMessage().contains("Forced"); + } + }); + + to.assertErrorMessage("Forced failure"); + + try { + to.assertErrorMessage(""); + throw new RuntimeException("Should have thrown"); + } catch (AssertionError exc) { + // expected + } + + try { + to.assertError(new RuntimeException()); + throw new RuntimeException("Should have thrown"); + } catch (AssertionError exc) { + // expected + } + + try { + to.assertError(IOException.class); + throw new RuntimeException("Should have thrown"); + } catch (AssertionError exc) { + // expected + } + + try { + to.assertError(Functions.<Throwable>alwaysFalse()); + throw new RuntimeException("Should have thrown"); + } catch (AssertionError exc) { + // expected + } + + try { + to.assertNoErrors(); + throw new RuntimeException("Should have thrown"); + } catch (AssertionError exc) { + // expected + } + + to.assertTerminated(); + + to.assertValueCount(0); + + to.assertNoValues(); + } + + @Test + public void emptyObserverEnum() { + assertEquals(1, TestObserverEx.EmptyObserver.values().length); + assertNotNull(TestObserverEx.EmptyObserver.valueOf("INSTANCE")); + } + + @Test + public void valueAndClass() { + assertEquals("null", TestObserver.valueAndClass(null)); + assertEquals("1 (class: Integer)", TestObserver.valueAndClass(1)); + } + + @Test + public void assertFailure() { + TestObserverEx<Integer> to = new TestObserverEx<>(); + + to.onSubscribe(Disposable.empty()); + + to.onError(new TestException("Forced failure")); + + to.assertFailure(TestException.class); + + to.assertFailure(Functions.<Throwable>alwaysTrue()); + + to.assertFailureAndMessage(TestException.class, "Forced failure"); + + to.onNext(1); + + to.assertFailure(TestException.class, 1); + + to.assertFailure(Functions.<Throwable>alwaysTrue(), 1); + + to.assertFailureAndMessage(TestException.class, "Forced failure", 1); + } + + @Test + public void assertFuseable() { + TestObserverEx<Integer> to = new TestObserverEx<>(); + + to.onSubscribe(Disposable.empty()); + + to.assertNotFuseable(); + + try { + to.assertFuseable(); + throw new RuntimeException("Should have thrown"); + } catch (AssertionError ex) { + // expected + } + + try { + to.assertFusionMode(QueueFuseable.SYNC); + throw new RuntimeException("Should have thrown"); + } catch (AssertionError ex) { + // expected + } + + to = new TestObserverEx<>(); + to.setInitialFusionMode(QueueFuseable.ANY); + + to.onSubscribe(new ScalarDisposable<>(to, 1)); + + to.assertFuseable(); + + to.assertFusionMode(QueueFuseable.SYNC); + + try { + to.assertFusionMode(QueueFuseable.NONE); + throw new RuntimeException("Should have thrown"); + } catch (AssertionError ex) { + // expected + } + + try { + to.assertNotFuseable(); + throw new RuntimeException("Should have thrown"); + } catch (AssertionError ex) { + // expected + } + + } + + @Test + public void assertTerminated() { + TestObserverEx<Integer> to = new TestObserverEx<>(); + + to.assertNotTerminated(); + + to.onError(null); + + try { + to.assertNotTerminated(); + throw new RuntimeException("Should have thrown!"); + } catch (AssertionError ex) { + // expected + } + } + + @Test + public void assertResult() { + TestObserverEx<Integer> to = new TestObserverEx<>(); + + to.onSubscribe(Disposable.empty()); + + to.onComplete(); + + to.assertResult(); + + try { + to.assertResult(1); + throw new RuntimeException("Should have thrown"); + } catch (AssertionError ex) { + // expected + } + + to.onNext(1); + + to.assertResult(1); + + try { + to.assertResult(2); + throw new RuntimeException("Should have thrown"); + } catch (AssertionError ex) { + // expected + } + + try { + to.assertResult(); + throw new RuntimeException("Should have thrown"); + } catch (AssertionError ex) { + // expected + } + + } + + @Test + public void await() throws Exception { + TestObserverEx<Integer> to = new TestObserverEx<>(); + + to.onSubscribe(Disposable.empty()); + + assertFalse(to.await(100, TimeUnit.MILLISECONDS)); + + to.awaitDone(100, TimeUnit.MILLISECONDS); + + assertTrue(to.isDisposed()); + + to.assertNotTerminated(); + + to.onComplete(); + + assertTrue(to.await(100, TimeUnit.MILLISECONDS)); + + to.await(); + + to.awaitDone(5, TimeUnit.SECONDS); + + to.assertNoErrors().assertComplete(); + + final TestObserverEx<Integer> to1 = new TestObserverEx<>(); + + to1.onSubscribe(Disposable.empty()); + + Schedulers.single().scheduleDirect(new Runnable() { + @Override + public void run() { + to1.onComplete(); + } + }, 200, TimeUnit.MILLISECONDS); + + to1.await(); + } + + @Test + public void errors() { + TestObserverEx<Integer> to = new TestObserverEx<>(); + + to.onSubscribe(Disposable.empty()); + + assertEquals(0, to.errors().size()); + + to.onError(new TestException()); + + assertEquals(1, to.errors().size()); + + TestHelper.assertError(to.errors(), 0, TestException.class); + } + + @Test + public void onNext() { + TestObserverEx<Integer> to = new TestObserverEx<>(); + + to.onSubscribe(Disposable.empty()); + + to.assertValueCount(0); + + assertEquals(Collections.emptyList(), to.values()); + + to.onNext(1); + + assertEquals(Collections.singletonList(1), to.values()); + + to.dispose(); + + assertTrue(to.isDisposed()); + + to.assertValue(1); + + to.onComplete(); + } + + @Test + public void fusionModeToString() { + assertEquals("NONE", TestObserverEx.fusionModeToString(QueueFuseable.NONE)); + assertEquals("SYNC", TestObserverEx.fusionModeToString(QueueFuseable.SYNC)); + assertEquals("ASYNC", TestObserverEx.fusionModeToString(QueueFuseable.ASYNC)); + assertEquals("Unknown(100)", TestObserverEx.fusionModeToString(100)); + } + + @Test + public void multipleTerminals() { + TestObserverEx<Integer> to = new TestObserverEx<>(); + + to.onSubscribe(Disposable.empty()); + + to.assertNotComplete(); + + to.onComplete(); + + try { + to.assertNotComplete(); + throw new RuntimeException("Should have thrown"); + } catch (Throwable ex) { + // expected + } + + to.assertTerminated(); + + to.onComplete(); + + try { + to.assertComplete(); + throw new RuntimeException("Should have thrown"); + } catch (Throwable ex) { + // expected + } + + try { + to.assertTerminated(); + throw new RuntimeException("Should have thrown"); + } catch (Throwable ex) { + // expected + } + + try { + to.assertNotComplete(); + throw new RuntimeException("Should have thrown"); + } catch (Throwable ex) { + // expected + } + } + + @Test + public void assertValue() { + TestObserverEx<Integer> to = new TestObserverEx<>(); + + to.onSubscribe(Disposable.empty()); + + try { + to.assertValue(1); + throw new RuntimeException("Should have thrown"); + } catch (Throwable ex) { + // expected + } + + to.onNext(1); + + to.assertValue(1); + + try { + to.assertValue(2); + throw new RuntimeException("Should have thrown"); + } catch (Throwable ex) { + // expected + } + + to.onNext(2); + + try { + to.assertValue(1); + throw new RuntimeException("Should have thrown"); + } catch (Throwable ex) { + // expected + } + } + + @Test + public void onNextMisbehave() { + TestObserverEx<Integer> to = new TestObserverEx<>(); + + to.onNext(1); + + to.assertError(IllegalStateException.class); + + to = new TestObserverEx<>(); + + to.onSubscribe(Disposable.empty()); + + to.onNext(null); + + to.assertFailure(NullPointerException.class, (Integer)null); + } + + @Test + public void assertTerminated2() { + TestObserverEx<Integer> to = new TestObserverEx<>(); + + to.onSubscribe(Disposable.empty()); + + to.assertNotTerminated(); + + to.onError(new TestException()); + + to.assertTerminated(); + + to.onError(new IOException()); + + try { + to.assertTerminated(); + throw new RuntimeException("Should have thrown"); + } catch (AssertionError ex) { + // expected + } + + try { + to.assertError(TestException.class); + throw new RuntimeException("Should have thrown"); + } catch (AssertionError ex) { + // expected + } + + to = new TestObserverEx<>(); + + to.onSubscribe(Disposable.empty()); + + to.onError(new TestException()); + to.onComplete(); + + try { + to.assertTerminated(); + throw new RuntimeException("Should have thrown"); + } catch (AssertionError ex) { + // expected + } + } + + @Test + public void onSubscribe() { + TestObserverEx<Integer> to = new TestObserverEx<>(); + + to.onSubscribe(null); + + to.assertError(NullPointerException.class); + + to = new TestObserverEx<>(); + + to.onSubscribe(Disposable.empty()); + + Disposable d1 = Disposable.empty(); + + to.onSubscribe(d1); + + assertTrue(d1.isDisposed()); + + to.assertError(IllegalStateException.class); + + to = new TestObserverEx<>(); + to.dispose(); + + d1 = Disposable.empty(); + + to.onSubscribe(d1); + + assertTrue(d1.isDisposed()); + + } + + @Test + public void assertValueSequence() { + TestObserverEx<Integer> to = new TestObserverEx<>(); + + to.onSubscribe(Disposable.empty()); + + to.onNext(1); + to.onNext(2); + + try { + to.assertValueSequence(Collections.<Integer>emptyList()); + throw new RuntimeException("Should have thrown"); + } catch (AssertionError expected) { + assertTrue(expected.getMessage(), expected.getMessage().startsWith("More values received than expected (0)")); + } + + try { + to.assertValueSequence(Collections.singletonList(1)); + throw new RuntimeException("Should have thrown"); + } catch (AssertionError expected) { + assertTrue(expected.getMessage(), expected.getMessage().startsWith("More values received than expected (1)")); + } + + to.assertValueSequence(Arrays.asList(1, 2)); + + try { + to.assertValueSequence(Arrays.asList(1, 2, 3)); + throw new RuntimeException("Should have thrown"); + } catch (AssertionError expected) { + assertTrue(expected.getMessage(), expected.getMessage().startsWith("Fewer values received than expected (2)")); + } + } + + @Test + public void assertEmpty() { + TestObserverEx<Integer> to = new TestObserverEx<>(); + + try { + to.assertEmpty(); + throw new RuntimeException("Should have thrown!"); + } catch (AssertionError ex) { + // expected + } + + to.onSubscribe(Disposable.empty()); + + to.assertEmpty(); + + to.onNext(1); + + try { + to.assertEmpty(); + throw new RuntimeException("Should have thrown!"); + } catch (AssertionError ex) { + // expected + } + } + + @Test + public void awaitDoneTimed() { + TestObserverEx<Integer> to = new TestObserverEx<>(); + + Thread.currentThread().interrupt(); + + try { + to.awaitDone(5, TimeUnit.SECONDS); + } catch (RuntimeException ex) { + assertTrue(ex.toString(), ex.getCause() instanceof InterruptedException); + } + } + + @Test + public void assertNotSubscribed() { + TestObserverEx<Integer> to = new TestObserverEx<>(); + + to.assertNotSubscribed(); + + to.errors().add(new TestException()); + + try { + to.assertNotSubscribed(); + throw new RuntimeException("Should have thrown!"); + } catch (AssertionError ex) { + // expected + } + } + + @Test + public void assertErrorMultiple() { + TestObserverEx<Integer> to = new TestObserverEx<>(); + + TestException e = new TestException(); + to.errors().add(e); + to.errors().add(new TestException()); + + try { + to.assertError(TestException.class); + throw new RuntimeException("Should have thrown!"); + } catch (AssertionError ex) { + // expected + } + try { + to.assertError(e); + throw new RuntimeException("Should have thrown!"); + } catch (AssertionError ex) { + // expected + } + try { + to.assertError(Functions.<Throwable>alwaysTrue()); + throw new RuntimeException("Should have thrown!"); + } catch (AssertionError ex) { + // expected + } + try { + to.assertErrorMessage(""); + throw new RuntimeException("Should have thrown!"); + } catch (AssertionError ex) { + // expected + } + } + + @Test + public void errorInPredicate() { + TestObserverEx<Object> to = new TestObserverEx<>(); + to.onError(new RuntimeException()); + try { + to.assertError(new Predicate<Throwable>() { + @Override + public boolean test(Throwable throwable) throws Exception { + throw new TestException(); + } + }); + } catch (TestException ex) { + // expected + return; + } + fail("Error in predicate but not thrown!"); + } + + @Test + public void assertComplete() { + TestObserverEx<Integer> to = new TestObserverEx<>(); + + to.onSubscribe(Disposable.empty()); + + try { + to.assertComplete(); + throw new RuntimeException("Should have thrown!"); + } catch (AssertionError ex) { + // expected + } + + to.onComplete(); + + to.assertComplete(); + + to.onComplete(); + + try { + to.assertComplete(); + throw new RuntimeException("Should have thrown!"); + } catch (AssertionError ex) { + // expected + } + } + + @Test + public void completeWithoutOnSubscribe() { + TestObserverEx<Integer> to = new TestObserverEx<>(); + + to.onComplete(); + + to.assertError(IllegalStateException.class); + } + + @Test + public void completeDelegateThrows() { + TestObserverEx<Integer> to = new TestObserverEx<>(new Observer<Integer>() { + + @Override + public void onSubscribe(Disposable d) { + + } + + @Override + public void onNext(Integer value) { + + } + + @Override + public void onError(Throwable e) { + throw new TestException(); + } + + @Override + public void onComplete() { + throw new TestException(); + } + + }); + + to.onSubscribe(Disposable.empty()); + + try { + to.onComplete(); + throw new RuntimeException("Should have thrown!"); + } catch (TestException ex) { + to.assertTerminated(); + } + } + + @Test + public void errorDelegateThrows() { + TestObserverEx<Integer> to = new TestObserverEx<>(new Observer<Integer>() { + + @Override + public void onSubscribe(Disposable d) { + + } + + @Override + public void onNext(Integer value) { + + } + + @Override + public void onError(Throwable e) { + throw new TestException(); + } + + @Override + public void onComplete() { + throw new TestException(); + } + + }); + + to.onSubscribe(Disposable.empty()); + + try { + to.onError(new IOException()); + throw new RuntimeException("Should have thrown!"); + } catch (TestException ex) { + to.assertTerminated(); + } + } + + @Test + public void syncQueueThrows() { + TestObserverEx<Object> to = new TestObserverEx<>(); + to.setInitialFusionMode(QueueFuseable.SYNC); + + Observable.range(1, 5) + .map(new Function<Integer, Object>() { + @Override + public Object apply(Integer v) throws Exception { throw new TestException(); } + }) + .subscribe(to); + + to.assertSubscribed() + .assertFuseable() + .assertFusionMode(QueueFuseable.SYNC) + .assertFailure(TestException.class); + } + + @Test + public void asyncQueueThrows() { + TestObserverEx<Object> to = new TestObserverEx<>(); + to.setInitialFusionMode(QueueFuseable.ANY); + + UnicastSubject<Integer> us = UnicastSubject.create(); + + us + .map(new Function<Integer, Object>() { + @Override + public Object apply(Integer v) throws Exception { throw new TestException(); } + }) + .subscribe(to); + + us.onNext(1); + + to.assertSubscribed() + .assertFuseable() + .assertFusionMode(QueueFuseable.ASYNC) + .assertFailure(TestException.class); + } + + @Test + public void completedMeansDisposed() { + // 2.0.2 - a terminated TestObserver no longer reports isDisposed + assertFalse(Observable.just(1) + .test() + .assertResult(1).isDisposed()); + } + + @Test + public void errorMeansDisposed() { + // 2.0.2 - a terminated TestObserver no longer reports isDisposed + assertFalse(Observable.error(new TestException()) + .test() + .assertFailure(TestException.class).isDisposed()); + } + + @Test + public void asyncFusion() { + TestObserverEx<Object> to = new TestObserverEx<>(); + to.setInitialFusionMode(QueueFuseable.ANY); + + UnicastSubject<Integer> us = UnicastSubject.create(); + + us + .subscribe(to); + + us.onNext(1); + us.onComplete(); + + to.assertSubscribed() + .assertFuseable() + .assertFusionMode(QueueFuseable.ASYNC) + .assertResult(1); + } + + @Test + public void assertValuePredicateEmpty() { + assertThrows("No values", AssertionError.class, () -> { + TestObserverEx<Object> to = new TestObserverEx<>(); + + Observable.empty().subscribe(to); + + to.assertValue(new Predicate<Object>() { + @Override public boolean test(final Object o) throws Exception { + return false; + } + }); + }); + } + + @Test + public void assertValuePredicateMatch() { + TestObserverEx<Integer> to = new TestObserverEx<>(); + + Observable.just(1).subscribe(to); + + to.assertValue(new Predicate<Integer>() { + @Override public boolean test(final Integer o) throws Exception { + return o == 1; + } + }); + } + + @Test + public void assertValuePredicateNoMatch() { + assertThrows("Value not present", AssertionError.class, () -> { + TestObserverEx<Integer> to = new TestObserverEx<>(); + + Observable.just(1).subscribe(to); + + to.assertValue(new Predicate<Integer>() { + @Override public boolean test(final Integer o) throws Exception { + return o != 1; + } + }); + }); + } + + @Test + public void assertValuePredicateMatchButMore() { + assertThrows("Value present but other values as well", AssertionError.class, () -> { + TestObserverEx<Integer> to = new TestObserverEx<>(); + + Observable.just(1, 2).subscribe(to); + + to.assertValue(new Predicate<Integer>() { + @Override public boolean test(final Integer o) throws Exception { + return o == 1; + } + }); + }); + } + + @Test + public void assertValueAtPredicateEmpty() { + assertThrows("No values", AssertionError.class, () -> { + TestObserverEx<Object> to = new TestObserverEx<>(); + + Observable.empty().subscribe(to); + + to.assertValueAt(0, new Predicate<Object>() { + @Override public boolean test(final Object o) throws Exception { + return false; + } + }); + }); + } + + @Test + public void assertValueAtPredicateMatch() { + TestObserverEx<Integer> to = new TestObserverEx<>(); + + Observable.just(1, 2).subscribe(to); + + to.assertValueAt(1, new Predicate<Integer>() { + @Override public boolean test(final Integer o) throws Exception { + return o == 2; + } + }); + } + + @Test + public void assertValueAtPredicateNoMatch() { + assertThrows("Value not present", AssertionError.class, () -> { + TestObserverEx<Integer> to = new TestObserverEx<>(); + + Observable.just(1, 2, 3).subscribe(to); + + to.assertValueAt(2, new Predicate<Integer>() { + @Override public boolean test(final Integer o) throws Exception { + return o != 3; + } + }); + }); + } + + @Test + public void assertValueAtInvalidIndex() { + assertThrows("Invalid index: 2 (latch = 0, values = 2, errors = 0, completions = 1)", AssertionError.class, () -> { + TestObserverEx<Integer> to = new TestObserverEx<>(); + + Observable.just(1, 2).subscribe(to); + + to.assertValueAt(2, new Predicate<Integer>() { + @Override public boolean test(final Integer o) throws Exception { + return o == 1; + } + }); + }); + } + + @Test + public void assertValueAtIndexEmpty() { + assertThrows("No values", AssertionError.class, () -> { + TestObserverEx<Object> to = new TestObserverEx<>(); + + Observable.empty().subscribe(to); + + to.assertValueAt(0, "a"); + }); + } + + @Test + public void assertValueAtIndexMatch() { + TestObserverEx<String> to = new TestObserverEx<>(); + + Observable.just("a", "b").subscribe(to); + + to.assertValueAt(1, "b"); + } + + @Test + public void assertValueAtIndexNoMatch() { + assertThrows("\nexpected: b (class: String)\ngot: c (class: String) (latch = 0, values = 3, errors = 0, completions = 1)", AssertionError.class, () -> { + TestObserverEx<String> to = new TestObserverEx<>(); + + Observable.just("a", "b", "c").subscribe(to); + + to.assertValueAt(2, "b"); + }); + } + + @Test + public void assertValueAtIndexInvalidIndex() { + assertThrows("Invalid index: 2 (latch = 0, values = 2, errors = 0, completions = 1)", AssertionError.class, () -> { + TestObserverEx<String> to = new TestObserverEx<>(); + + Observable.just("a", "b").subscribe(to); + + to.assertValueAt(2, "c"); + }); + } + + @Test + public void withTag() { + try { + for (int i = 1; i < 3; i++) { + Observable.just(i) + .test() + .withTag("testing with item=" + i) + .assertResult(1) + ; + } + throw new RuntimeException("Should have thrown!"); + } catch (AssertionError ex) { + assertTrue(ex.toString(), ex.toString().contains("testing with item=2")); + } + } + + @Test + public void assertValuesOnly() { + TestObserverEx<Integer> to = new TestObserverEx<>(); + to.onSubscribe(Disposable.empty()); + to.assertValuesOnly(); + + to.onNext(5); + to.assertValuesOnly(5); + + to.onNext(-1); + to.assertValuesOnly(5, -1); + } + + @Test + public void assertValuesOnlyThrowsOnUnexpectedValue() { + TestObserverEx<Integer> to = new TestObserverEx<>(); + to.onSubscribe(Disposable.empty()); + to.assertValuesOnly(); + + to.onNext(5); + to.assertValuesOnly(5); + + to.onNext(-1); + + try { + to.assertValuesOnly(5); + throw new RuntimeException(); + } catch (AssertionError ex) { + // expected + } + } + + @Test + public void assertValuesOnlyThrowsWhenCompleted() { + TestObserverEx<Integer> to = new TestObserverEx<>(); + to.onSubscribe(Disposable.empty()); + + to.onComplete(); + + try { + to.assertValuesOnly(); + throw new RuntimeException(); + } catch (AssertionError ex) { + // expected + } + } + + @Test + public void assertValuesOnlyThrowsWhenErrored() { + TestObserverEx<Integer> to = new TestObserverEx<>(); + to.onSubscribe(Disposable.empty()); + + to.onError(new TestException()); + + try { + to.assertValuesOnly(); + throw new RuntimeException(); + } catch (AssertionError ex) { + // expected + } + } +} diff --git a/src/test/java/io/reactivex/rxjava3/testsupport/TestSubscriberEx.java b/src/test/java/io/reactivex/rxjava3/testsupport/TestSubscriberEx.java new file mode 100644 index 0000000000..c3d9837c2b --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/testsupport/TestSubscriberEx.java @@ -0,0 +1,389 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.testsupport; + +import java.util.concurrent.atomic.*; + +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.FlowableSubscriber; +import io.reactivex.rxjava3.internal.subscriptions.SubscriptionHelper; +import io.reactivex.rxjava3.operators.QueueFuseable; +import io.reactivex.rxjava3.operators.QueueSubscription; + +/** + * An extended test subscriber that records events and allows making assertions about them. + * + * <p>You can override the onSubscribe, onNext, onError, onComplete, request and + * cancel methods but not the others (this is by design). + * + * <p>The TestSubscriber implements Disposable for convenience where dispose calls cancel. + * + * <p>When calling the default request method, you are requesting on behalf of the + * wrapped actual subscriber. + * + * @param <T> the value type + */ +public class TestSubscriberEx<T> +extends BaseTestConsumerEx<T, TestSubscriberEx<T>> +implements FlowableSubscriber<T>, Subscription { + /** The actual subscriber to forward events to. */ + private final Subscriber<? super T> downstream; + + /** Makes sure the incoming Subscriptions get cancelled immediately. */ + private volatile boolean cancelled; + + /** Holds the current subscription if any. */ + private final AtomicReference<Subscription> upstream; + + /** Holds the requested amount until a subscription arrives. */ + private final AtomicLong missedRequested; + + private QueueSubscription<T> qs; + + /** + * Constructs a non-forwarding TestSubscriber with an initial request value of {@link Long#MAX_VALUE}. + */ + public TestSubscriberEx() { + this(EmptySubscriber.INSTANCE, Long.MAX_VALUE); + } + + /** + * Constructs a non-forwarding TestSubscriber with the specified initial request value. + * <p>The TestSubscriber doesn't validate the initialRequest value so one can + * test sources with invalid values as well. + * @param initialRequest the initial request value + */ + public TestSubscriberEx(long initialRequest) { + this(EmptySubscriber.INSTANCE, initialRequest); + } + + /** + * Constructs a forwarding TestSubscriber but leaves the requesting to the wrapped subscriber. + * @param downstream the actual Subscriber to forward events to + */ + public TestSubscriberEx(Subscriber<? super T> downstream) { + this(downstream, Long.MAX_VALUE); + } + + /** + * Constructs a forwarding TestSubscriber with the specified initial request value. + * <p>The TestSubscriber doesn't validate the initialRequest value so one can + * test sources with invalid values as well. + * @param actual the actual Subscriber to forward events to + * @param initialRequest the initial request value + */ + public TestSubscriberEx(Subscriber<? super T> actual, long initialRequest) { + super(); + if (initialRequest < 0) { + throw new IllegalArgumentException("Negative initial request not allowed"); + } + this.downstream = actual; + this.upstream = new AtomicReference<>(); + this.missedRequested = new AtomicLong(initialRequest); + } + + @SuppressWarnings("unchecked") + @Override + public void onSubscribe(Subscription s) { + lastThread = Thread.currentThread(); + + if (s == null) { + errors.add(new NullPointerException("onSubscribe received a null Subscription")); + return; + } + if (!upstream.compareAndSet(null, s)) { + s.cancel(); + if (upstream.get() != SubscriptionHelper.CANCELLED) { + errors.add(new IllegalStateException("onSubscribe received multiple subscriptions: " + s)); + } + return; + } + + if (initialFusionMode != 0) { + if (s instanceof QueueSubscription) { + qs = (QueueSubscription<T>)s; + + int m = qs.requestFusion(initialFusionMode); + establishedFusionMode = m; + + if (m == QueueFuseable.SYNC) { + checkSubscriptionOnce = true; + lastThread = Thread.currentThread(); + try { + T t; + while ((t = qs.poll()) != null) { + values.add(t); + } + completions++; + } catch (Throwable ex) { + // Exceptions.throwIfFatal(e); TODO add fatal exceptions? + errors.add(ex); + } + return; + } + } + } + + downstream.onSubscribe(s); + + long mr = missedRequested.getAndSet(0L); + if (mr != 0L) { + s.request(mr); + } + + onStart(); + } + + /** + * Called after the onSubscribe is called and handled. + */ + protected void onStart() { + + } + + @Override + public void onNext(T t) { + if (!checkSubscriptionOnce) { + checkSubscriptionOnce = true; + if (upstream.get() == null) { + errors.add(new IllegalStateException("onSubscribe not called in proper order")); + } + } + lastThread = Thread.currentThread(); + + if (establishedFusionMode == QueueFuseable.ASYNC) { + try { + while ((t = qs.poll()) != null) { + values.add(t); + } + } catch (Throwable ex) { + // Exceptions.throwIfFatal(e); TODO add fatal exceptions? + errors.add(ex); + qs.cancel(); + } + return; + } + + values.add(t); + + if (t == null) { + errors.add(new NullPointerException("onNext received a null value")); + } + + downstream.onNext(t); + } + + @Override + public void onError(Throwable t) { + if (!checkSubscriptionOnce) { + checkSubscriptionOnce = true; + if (upstream.get() == null) { + errors.add(new NullPointerException("onSubscribe not called in proper order")); + } + } + try { + lastThread = Thread.currentThread(); + errors.add(t); + + if (t == null) { + errors.add(new IllegalStateException("onError received a null Throwable")); + } + + downstream.onError(t); + } finally { + done.countDown(); + } + } + + @Override + public void onComplete() { + if (!checkSubscriptionOnce) { + checkSubscriptionOnce = true; + if (upstream.get() == null) { + errors.add(new IllegalStateException("onSubscribe not called in proper order")); + } + } + try { + lastThread = Thread.currentThread(); + completions++; + + downstream.onComplete(); + } finally { + done.countDown(); + } + } + + @Override + public final void request(long n) { + SubscriptionHelper.deferredRequest(upstream, missedRequested, n); + } + + @Override + public final void cancel() { + if (!cancelled) { + cancelled = true; + SubscriptionHelper.cancel(upstream); + } + } + + /** + * Returns true if this TestSubscriber has been cancelled. + * @return true if this TestSubscriber has been cancelled + */ + public final boolean isCancelled() { + return cancelled; + } + + @Override + protected final void dispose() { + cancel(); + } + + @Override + protected final boolean isDisposed() { + return cancelled; + } + + // state retrieval methods + + /** + * Returns true if this TestSubscriber received a subscription. + * @return true if this TestSubscriber received a subscription + */ + public final boolean hasSubscription() { + return upstream.get() != null; + } + + // assertion methods + + /** + * Assert that the onSubscribe method was called exactly once. + * @return this + */ + @Override + public final TestSubscriberEx<T> assertSubscribed() { + if (upstream.get() == null) { + throw fail("Not subscribed!"); + } + return this; + } + + /** + * Assert that the onSubscribe method hasn't been called at all. + * @return this + */ + public final TestSubscriberEx<T> assertNotSubscribed() { + if (upstream.get() != null) { + throw fail("Subscribed!"); + } else + if (!errors.isEmpty()) { + throw fail("Not subscribed but errors found"); + } + return this; + } + + /** + * Sets the initial fusion mode if the upstream supports fusion. + * <p>Package-private: avoid leaking the now internal fusion properties into the public API. + * Use SubscriberFusion to work with such tests. + * @param mode the mode to establish, see the {@link QueueSubscription} constants + * @return this + */ + public final TestSubscriberEx<T> setInitialFusionMode(int mode) { + this.initialFusionMode = mode; + return this; + } + + /** + * Asserts that the given fusion mode has been established + * <p>Package-private: avoid leaking the now internal fusion properties into the public API. + * Use SubscriberFusion to work with such tests. + * @param mode the expected mode + * @return this + */ + public final TestSubscriberEx<T> assertFusionMode(int mode) { + int m = establishedFusionMode; + if (m != mode) { + if (qs != null) { + throw new AssertionError("\nexpected: " + fusionModeToString(mode) + + "\ngot: " + fusionModeToString(m) + "; Fusion mode different"); + } else { + throw fail("Upstream is not fuseable"); + } + } + return this; + } + + /** + * Assert that the upstream is a fuseable source. + * <p>Package-private: avoid leaking the now internal fusion properties into the public API. + * Use SubscriberFusion to work with such tests. + * @return this + */ + public final TestSubscriberEx<T> assertFuseable() { + if (qs == null) { + throw new AssertionError("Upstream is not fuseable."); + } + return this; + } + + /** + * Assert that the upstream is not a fuseable source. + * <p>Package-private: avoid leaking the now internal fusion properties into the public API. + * Use SubscriberFusion to work with such tests. + * @return this + */ + public final TestSubscriberEx<T> assertNotFuseable() { + if (qs != null) { + throw new AssertionError("Upstream is fuseable."); + } + return this; + } + + /** + * Calls {@link #request(long)} and returns this. + * <p>History: 2.0.1 - experimental + * @param n the request amount + * @return this + * @since 2.1 + */ + public final TestSubscriberEx<T> requestMore(long n) { + request(n); + return this; + } + + /** + * A subscriber that ignores all events and does not report errors. + */ + enum EmptySubscriber implements FlowableSubscriber<Object> { + INSTANCE; + + @Override + public void onSubscribe(Subscription s) { + } + + @Override + public void onNext(Object t) { + } + + @Override + public void onError(Throwable t) { + } + + @Override + public void onComplete() { + } + } +} diff --git a/src/test/java/io/reactivex/rxjava3/testsupport/TestSubscriberExTest.java b/src/test/java/io/reactivex/rxjava3/testsupport/TestSubscriberExTest.java new file mode 100644 index 0000000000..f3d19f9cc7 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/testsupport/TestSubscriberExTest.java @@ -0,0 +1,1882 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.testsupport; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +import java.io.IOException; +import java.util.*; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.junit.Test; +import org.mockito.InOrder; +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.core.Scheduler.Worker; +import io.reactivex.rxjava3.exceptions.*; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.internal.subscriptions.*; +import io.reactivex.rxjava3.operators.QueueFuseable; +import io.reactivex.rxjava3.processors.*; +import io.reactivex.rxjava3.schedulers.Schedulers; + +public class TestSubscriberExTest extends RxJavaTest { + + @Test + public void assertTestSubscriberEx() { + Flowable<Integer> oi = Flowable.fromIterable(Arrays.asList(1, 2)); + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(); + oi.subscribe(ts); + + ts.assertValues(1, 2); + ts.assertValueCount(2); + ts.assertTerminated(); + } + + @Test + public void assertNotMatchCount() { + assertThrows(AssertionError.class, () -> { + Flowable<Integer> oi = Flowable.fromIterable(Arrays.asList(1, 2)); + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(); + oi.subscribe(ts); + + ts.assertValues(1); + ts.assertValueCount(2); + ts.assertTerminated(); + }); + } + + @Test + public void assertNotMatchValue() { + assertThrows(AssertionError.class, () -> { + Flowable<Integer> oi = Flowable.fromIterable(Arrays.asList(1, 2)); + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(); + oi.subscribe(ts); + + ts.assertValues(1, 3); + ts.assertValueCount(2); + ts.assertTerminated(); + }); + } + + @Test + public void assertNeverAtNotMatchingValue() { + Flowable<Integer> oi = Flowable.fromIterable(Arrays.asList(1, 2)); + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(); + oi.subscribe(ts); + + ts.assertNever(3); + ts.assertValueCount(2); + ts.assertTerminated(); + } + + @Test + public void assertNeverAtMatchingValue() { + assertThrows(AssertionError.class, () -> { + Flowable<Integer> oi = Flowable.fromIterable(Arrays.asList(1, 2)); + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(); + oi.subscribe(ts); + + ts.assertValues(1, 2); + + ts.assertNever(2); + ts.assertValueCount(2); + ts.assertTerminated(); + }); + } + + @Test + public void assertNeverAtMatchingPredicate() { + assertThrows(AssertionError.class, () -> { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(); + + Flowable.just(1, 2).subscribe(ts); + + ts.assertValues(1, 2); + + ts.assertNever(new Predicate<Integer>() { + @Override + public boolean test(final Integer o) throws Exception { + return o == 1; + } + }); + }); + } + + @Test + public void assertNeverAtNotMatchingPredicate() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(); + + Flowable.just(2, 3).subscribe(ts); + + ts.assertNever(new Predicate<Integer>() { + @Override + public boolean test(final Integer o) throws Exception { + return o == 1; + } + }); + } + + @Test + public void assertTerminalEventNotReceived() { + assertThrows(AssertionError.class, () -> { + PublishProcessor<Integer> p = PublishProcessor.create(); + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(); + p.subscribe(ts); + + p.onNext(1); + p.onNext(2); + + ts.assertValues(1, 2); + ts.assertValueCount(2); + ts.assertTerminated(); + }); + } + + @Test + public void wrappingMock() { + Flowable<Integer> oi = Flowable.fromIterable(Arrays.asList(1, 2)); + Subscriber<Integer> mockSubscriber = TestHelper.mockSubscriber(); + + oi.subscribe(new TestSubscriberEx<>(mockSubscriber)); + + InOrder inOrder = inOrder(mockSubscriber); + inOrder.verify(mockSubscriber, times(1)).onNext(1); + inOrder.verify(mockSubscriber, times(1)).onNext(2); + inOrder.verify(mockSubscriber, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void wrappingMockWhenUnsubscribeInvolved() { + Flowable<Integer> oi = Flowable.fromIterable(Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9)).take(2); + Subscriber<Integer> mockSubscriber = TestHelper.mockSubscriber(); + oi.subscribe(new TestSubscriberEx<>(mockSubscriber)); + + InOrder inOrder = inOrder(mockSubscriber); + inOrder.verify(mockSubscriber, times(1)).onNext(1); + inOrder.verify(mockSubscriber, times(1)).onNext(2); + inOrder.verify(mockSubscriber, times(1)).onComplete(); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void assertError() { + RuntimeException e = new RuntimeException("Oops"); + TestSubscriberEx<Object> subscriber = new TestSubscriberEx<>(); + Flowable.error(e).subscribe(subscriber); + subscriber.assertError(e); + } + + @Test + public void awaitTerminalEventWithDurationAndUnsubscribeOnTimeout() { + TestSubscriberEx<Object> ts = new TestSubscriberEx<>(); + final AtomicBoolean unsub = new AtomicBoolean(false); + Flowable.just(1) + // + .doOnCancel(new Action() { + @Override + public void run() { + unsub.set(true); + } + }) + // + .delay(1000, TimeUnit.MILLISECONDS).subscribe(ts); + ts.awaitDone(100, TimeUnit.MILLISECONDS); + ts.dispose(); + assertTrue(unsub.get()); + } + + @Test(expected = NullPointerException.class) + public void nullDelegate1() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(null); + ts.onComplete(); + } + + @Test(expected = NullPointerException.class) + public void nullDelegate2() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(null); + ts.onComplete(); + } + + @Test(expected = NullPointerException.class) + public void nullDelegate3() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(null, 0L); + ts.onComplete(); + } + + @Test + public void delegate1() { + TestSubscriberEx<Integer> ts0 = new TestSubscriberEx<>(); + ts0.onSubscribe(EmptySubscription.INSTANCE); + + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(ts0); + ts.onComplete(); + + ts0.assertTerminated(); + } + + @Test + public void delegate2() { + TestSubscriberEx<Integer> ts1 = new TestSubscriberEx<>(); + TestSubscriberEx<Integer> ts2 = new TestSubscriberEx<>(ts1); + ts2.onComplete(); + + ts1.assertComplete(); + } + + @Test + public void delegate3() { + TestSubscriberEx<Integer> ts1 = new TestSubscriberEx<>(); + TestSubscriberEx<Integer> ts2 = new TestSubscriberEx<>(ts1, 0L); + ts2.onComplete(); + ts1.assertComplete(); + } + + @Test + public void unsubscribed() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(); + assertFalse(ts.isCancelled()); + } + + @Test + public void noErrors() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(); + ts.onError(new TestException()); + try { + ts.assertNoErrors(); + } catch (AssertionError ex) { + // expected + return; + } + fail("Error present but no assertion error!"); + } + + @Test + public void notCompleted() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(); + try { + ts.assertComplete(); + } catch (AssertionError ex) { + // expected + return; + } + fail("Not completed and no assertion error!"); + } + + @Test + public void multipleCompletions() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(); + ts.onComplete(); + ts.onComplete(); + try { + ts.assertComplete(); + } catch (AssertionError ex) { + // expected + return; + } + fail("Multiple completions and no assertion error!"); + } + + @Test + public void completed() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(); + ts.onComplete(); + try { + ts.assertNotComplete(); + } catch (AssertionError ex) { + // expected + return; + } + fail("Completed and no assertion error!"); + } + + @Test + public void multipleCompletions2() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(); + ts.onComplete(); + ts.onComplete(); + try { + ts.assertNotComplete(); + } catch (AssertionError ex) { + // expected + return; + } + fail("Multiple completions and no assertion error!"); + } + + @Test + public void multipleErrors() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(); + ts.onSubscribe(EmptySubscription.INSTANCE); + ts.onError(new TestException()); + ts.onError(new TestException()); + try { + ts.assertNoErrors(); + } catch (AssertionError ex) { + Throwable e = ex.getCause(); + if (!(e instanceof CompositeException)) { + fail("Multiple Error present but the reported error doesn't have a composite cause!"); + } + CompositeException ce = (CompositeException)e; + if (ce.size() != 2) { + ce.printStackTrace(); + } + assertEquals(2, ce.size()); + // expected + return; + } + fail("Multiple Error present but no assertion error!"); + } + + @Test + public void multipleErrors2() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(); + ts.onSubscribe(EmptySubscription.INSTANCE); + ts.onError(new TestException()); + ts.onError(new TestException()); + try { + ts.assertError(TestException.class); + } catch (AssertionError ex) { + Throwable e = ex.getCause(); + if (!(e instanceof CompositeException)) { + fail("Multiple Error present but the reported error doesn't have a composite cause!"); + } + CompositeException ce = (CompositeException)e; + assertEquals(2, ce.size()); + // expected + return; + } + fail("Multiple Error present but no assertion error!"); + } + + @Test + public void multipleErrors3() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(); + ts.onSubscribe(EmptySubscription.INSTANCE); + ts.onError(new TestException()); + ts.onError(new TestException()); + try { + ts.assertError(new TestException()); + } catch (AssertionError ex) { + Throwable e = ex.getCause(); + if (!(e instanceof CompositeException)) { + fail("Multiple Error present but the reported error doesn't have a composite cause!"); + } + CompositeException ce = (CompositeException)e; + assertEquals(2, ce.size()); + // expected + return; + } + fail("Multiple Error present but no assertion error!"); + } + + @Test + public void multipleErrors4() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(); + ts.onSubscribe(EmptySubscription.INSTANCE); + ts.onError(new TestException()); + ts.onError(new TestException()); + try { + ts.assertError(Functions.<Throwable>alwaysTrue()); + } catch (AssertionError ex) { + Throwable e = ex.getCause(); + if (!(e instanceof CompositeException)) { + fail("Multiple Error present but the reported error doesn't have a composite cause!"); + } + CompositeException ce = (CompositeException)e; + assertEquals(2, ce.size()); + // expected + return; + } + fail("Multiple Error present but no assertion error!"); + } + + @Test + public void differentError() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(); + ts.onError(new TestException()); + try { + ts.assertError(new TestException()); + } catch (AssertionError ex) { + // expected + return; + } + fail("Different Error present but no assertion error!"); + } + + @Test + public void differentError2() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(); + ts.onError(new RuntimeException()); + try { + ts.assertError(new TestException()); + } catch (AssertionError ex) { + // expected + return; + } + fail("Different Error present but no assertion error!"); + } + + @Test + public void differentError3() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(); + ts.onError(new RuntimeException()); + try { + ts.assertError(TestException.class); + } + catch (AssertionError ex) { + // expected + return; + } + fail("Different Error present but no assertion error!"); + } + + @Test + public void differentError4() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(); + ts.onError(new RuntimeException()); + try { + ts.assertError(Functions.<Throwable>alwaysFalse()); + } catch (AssertionError ex) { + // expected + return; + } + fail("Different Error present but no assertion error!"); + } + + @Test + public void errorInPredicate() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(); + ts.onError(new RuntimeException()); + try { + ts.assertError(new Predicate<Throwable>() { + @Override + public boolean test(Throwable throwable) throws Exception { + throw new TestException(); + } + }); + } catch (TestException ex) { + // expected + return; + } + fail("Error in predicate but not thrown!"); + } + + @Test + public void noError() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(); + try { + ts.assertError(TestException.class); + } catch (AssertionError ex) { + // expected + return; + } + fail("No present but no assertion error!"); + } + + @Test + public void noError2() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(); + try { + ts.assertError(new TestException()); + } catch (AssertionError ex) { + // expected + return; + } + fail("No present but no assertion error!"); + } + + @Test + public void noError3() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(); + try { + ts.assertError(Functions.<Throwable>alwaysTrue()); + } catch (AssertionError ex) { + // expected + return; + } + fail("No present but no assertion error!"); + } + + @Test + public void interruptTerminalEventAwait() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(); + + final Thread t0 = Thread.currentThread(); + Worker w = Schedulers.computation().createWorker(); + try { + w.schedule(new Runnable() { + @Override + public void run() { + t0.interrupt(); + } + }, 200, TimeUnit.MILLISECONDS); + + try { + if (ts.await(5, TimeUnit.SECONDS)) { + fail("Did not interrupt wait!"); + } + } catch (InterruptedException expected) { + // expected + } + } finally { + w.dispose(); + } + } + + @Test + public void interruptTerminalEventAwaitTimed() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(); + + final Thread t0 = Thread.currentThread(); + Worker w = Schedulers.computation().createWorker(); + try { + w.schedule(new Runnable() { + @Override + public void run() { + t0.interrupt(); + } + }, 200, TimeUnit.MILLISECONDS); + + try { + if (ts.await(5, TimeUnit.SECONDS)) { + fail("Did not interrupt wait!"); + } + } catch (InterruptedException expected) { + // expected + } + } finally { + Thread.interrupted(); // clear interrupted flag + w.dispose(); + } + } + + @Test + public void interruptTerminalEventAwaitAndUnsubscribe() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(); + + final Thread t0 = Thread.currentThread(); + Worker w = Schedulers.computation().createWorker(); + try { + w.schedule(new Runnable() { + @Override + public void run() { + t0.interrupt(); + } + }, 200, TimeUnit.MILLISECONDS); + + try { + ts.awaitDone(5, TimeUnit.SECONDS); + } catch (RuntimeException allowed) { + assertTrue(allowed.toString(), allowed.getCause() instanceof InterruptedException); + } + + ts.dispose(); + if (!ts.isCancelled()) { + fail("Did not unsubscribe!"); + } + } finally { + w.dispose(); + } + } + + @Test + public void noTerminalEventBut1Completed() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(); + + ts.onComplete(); + + try { + ts.assertNotTerminated(); + throw new RuntimeException("Failed to report there were terminal event(s)!"); + } catch (AssertionError ex) { + // expected + } + } + + @Test + public void noTerminalEventBut1Error() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(); + + ts.onError(new TestException()); + + try { + ts.assertNotTerminated(); + throw new RuntimeException("Failed to report there were terminal event(s)!"); + } catch (AssertionError ex) { + // expected + } + } + + @Test + public void noTerminalEventBut1Error1Completed() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(); + + ts.onComplete(); + ts.onError(new TestException()); + + try { + ts.assertNotTerminated(); + throw new RuntimeException("Failed to report there were terminal event(s)!"); + } catch (AssertionError ex) { + // expected + } + } + + @Test + public void noTerminalEventBut2Errors() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(); + ts.onSubscribe(EmptySubscription.INSTANCE); + + ts.onError(new TestException()); + ts.onError(new TestException()); + + try { + ts.assertNotTerminated(); + throw new RuntimeException("Failed to report there were terminal event(s)!"); + } catch (AssertionError ex) { + // expected + Throwable e = ex.getCause(); + if (!(e instanceof CompositeException)) { + fail("Multiple Error present but the reported error doesn't have a composite cause!"); + } + CompositeException ce = (CompositeException)e; + assertEquals(2, ce.size()); + } + } + + @Test + public void noValues() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(); + ts.onNext(1); + + try { + ts.assertNoValues(); + throw new RuntimeException("Failed to report there were values!"); + } catch (AssertionError ex) { + // expected + } + } + + @Test + public void valueCount() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(); + ts.onNext(1); + ts.onNext(2); + + try { + ts.assertValueCount(3); + throw new RuntimeException("Failed to report there were values!"); + } catch (AssertionError ex) { + // expected + } + } + + @Test + public void onCompletedCrashCountsDownLatch() { + TestSubscriberEx<Integer> ts0 = new TestSubscriberEx<Integer>() { + @Override + public void onComplete() { + throw new TestException(); + } + }; + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(ts0); + + try { + ts.onComplete(); + } catch (TestException ex) { + // expected + } + + ts.awaitDone(5, TimeUnit.SECONDS); + } + + @Test + public void onErrorCrashCountsDownLatch() { + TestSubscriberEx<Integer> ts0 = new TestSubscriberEx<Integer>() { + @Override + public void onError(Throwable e) { + throw new TestException(); + } + }; + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(ts0); + + try { + ts.onError(new RuntimeException()); + } catch (TestException ex) { + // expected + } + + ts.awaitDone(5, TimeUnit.SECONDS); + } + + @Test + public void createDelegate() { + TestSubscriberEx<Integer> ts1 = new TestSubscriberEx<>(); + + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(ts1); + + ts.assertNotSubscribed(); + + assertFalse(ts.hasSubscription()); + + ts.onSubscribe(new BooleanSubscription()); + + try { + ts.assertNotSubscribed(); + throw new RuntimeException("Should have thrown"); + } catch (AssertionError ex) { + // expected + } + + assertTrue(ts.hasSubscription()); + + assertFalse(ts.isDisposed()); + + ts.onNext(1); + ts.onError(new TestException()); + ts.onComplete(); + + ts1.assertValue(1).assertError(TestException.class).assertComplete(); + + ts.dispose(); + + assertTrue(ts.isDisposed()); + + assertSame(Thread.currentThread(), ts.lastThread()); + + try { + ts.assertNoValues(); + throw new RuntimeException("Should have thrown"); + } catch (AssertionError exc) { + // expected + } + + try { + ts.assertValueCount(0); + throw new RuntimeException("Should have thrown"); + } catch (AssertionError exc) { + // expected + } + + ts.assertValueSequence(Collections.singletonList(1)); + + try { + ts.assertValueSequence(Collections.singletonList(2)); + throw new RuntimeException("Should have thrown"); + } catch (AssertionError exc) { + // expected + } + } + + @Test + public void assertError2() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(); + + try { + ts.assertError(TestException.class); + throw new RuntimeException("Should have thrown"); + } catch (AssertionError ex) { + // expected + } + + try { + ts.assertError(new TestException()); + throw new RuntimeException("Should have thrown"); + } catch (AssertionError ex) { + // expected + } + + try { + ts.assertErrorMessage(""); + throw new RuntimeException("Should have thrown"); + } catch (AssertionError exc) { + // expected + } + + try { + ts.assertError(Functions.<Throwable>alwaysTrue()); + throw new RuntimeException("Should have thrown"); + } catch (AssertionError ex) { + // expected + } + + try { + ts.assertSubscribed(); + throw new RuntimeException("Should have thrown"); + } catch (AssertionError exc) { + // expected + } + + try { + ts.assertTerminated(); + throw new RuntimeException("Should have thrown"); + } catch (AssertionError exc) { + // expected + } + + ts.onSubscribe(new BooleanSubscription()); + + ts.assertSubscribed(); + + ts.assertNoErrors(); + + TestException ex = new TestException("Forced failure"); + + ts.onError(ex); + + ts.assertError(ex); + + ts.assertError(TestException.class); + + ts.assertErrorMessage("Forced failure"); + + ts.assertError(Functions.<Throwable>alwaysTrue()); + + ts.assertError(new Predicate<Throwable>() { + @Override + public boolean test(Throwable t) { + return t.getMessage() != null && t.getMessage().contains("Forced"); + } + }); + + try { + ts.assertErrorMessage(""); + throw new RuntimeException("Should have thrown"); + } catch (AssertionError exc) { + // expected + } + + try { + ts.assertError(new RuntimeException()); + throw new RuntimeException("Should have thrown"); + } catch (AssertionError exc) { + // expected + } + + try { + ts.assertError(IOException.class); + throw new RuntimeException("Should have thrown"); + } catch (AssertionError exc) { + // expected + } + + try { + ts.assertNoErrors(); + throw new RuntimeException("Should have thrown"); + } catch (AssertionError exc) { + // expected + } + + try { + ts.assertError(Functions.<Throwable>alwaysFalse()); + throw new RuntimeException("Should have thrown"); + } catch (AssertionError exc) { + // expected + } + + ts.assertTerminated(); + + ts.assertValueCount(0); + + ts.assertNoValues(); + } + + @Test + public void emptyObserverEnum() { + assertEquals(1, TestSubscriberEx.EmptySubscriber.values().length); + assertNotNull(TestSubscriberEx.EmptySubscriber.valueOf("INSTANCE")); + } + + @Test + public void valueAndClass() { + assertEquals("null", TestSubscriberEx.valueAndClass(null)); + assertEquals("1 (class: Integer)", TestSubscriberEx.valueAndClass(1)); + } + + @Test + public void assertFailure() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(); + + ts.onSubscribe(new BooleanSubscription()); + + ts.onError(new TestException("Forced failure")); + + ts.assertFailure(TestException.class); + + ts.assertFailure(Functions.<Throwable>alwaysTrue()); + + ts.assertFailureAndMessage(TestException.class, "Forced failure"); + + ts.onNext(1); + + ts.assertFailure(TestException.class, 1); + + ts.assertFailure(Functions.<Throwable>alwaysTrue(), 1); + + ts.assertFailureAndMessage(TestException.class, "Forced failure", 1); + } + + @Test + public void assertFuseable() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(); + + ts.onSubscribe(new BooleanSubscription()); + + ts.assertNotFuseable(); + + try { + ts.assertFuseable(); + throw new RuntimeException("Should have thrown"); + } catch (AssertionError ex) { + // expected + } + + try { + ts.assertFusionMode(QueueFuseable.SYNC); + throw new RuntimeException("Should have thrown"); + } catch (AssertionError ex) { + // expected + } + ts = new TestSubscriberEx<>(); + ts.setInitialFusionMode(QueueFuseable.ANY); + + ts.onSubscribe(new ScalarSubscription<>(ts, 1)); + + ts.assertFuseable(); + + ts.assertFusionMode(QueueFuseable.SYNC); + + try { + ts.assertFusionMode(QueueFuseable.NONE); + throw new RuntimeException("Should have thrown"); + } catch (AssertionError ex) { + // expected + } + + try { + ts.assertNotFuseable(); + throw new RuntimeException("Should have thrown"); + } catch (AssertionError ex) { + // expected + } + + } + + @Test + public void assertTerminated() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(); + + ts.assertNotTerminated(); + + ts.onError(null); + + try { + ts.assertNotTerminated(); + throw new RuntimeException("Should have thrown!"); + } catch (AssertionError ex) { + // expected + } + } + + @Test + public void assertResult() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(); + + ts.onSubscribe(new BooleanSubscription()); + + ts.onComplete(); + + ts.assertResult(); + + try { + ts.assertResult(1); + throw new RuntimeException("Should have thrown"); + } catch (AssertionError ex) { + // expected + } + + ts.onNext(1); + + ts.assertResult(1); + + try { + ts.assertResult(2); + throw new RuntimeException("Should have thrown"); + } catch (AssertionError ex) { + // expected + } + + try { + ts.assertResult(); + throw new RuntimeException("Should have thrown"); + } catch (AssertionError ex) { + // expected + } + + } + + @Test + public void await() throws Exception { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(); + + ts.onSubscribe(new BooleanSubscription()); + + assertFalse(ts.await(100, TimeUnit.MILLISECONDS)); + + ts.awaitDone(100, TimeUnit.MILLISECONDS); + + assertTrue(ts.isDisposed()); + + assertFalse(ts.await(100, TimeUnit.MILLISECONDS)); + + ts.assertNotComplete().assertNoErrors(); + + ts.onComplete(); + + assertTrue(ts.await(100, TimeUnit.MILLISECONDS)); + + ts.await(); + + ts.awaitDone(5, TimeUnit.SECONDS); + + ts.assertComplete().assertNoErrors(); + + assertTrue(ts.await(5, TimeUnit.SECONDS)); + + final TestSubscriberEx<Integer> ts1 = new TestSubscriberEx<>(); + + ts1.onSubscribe(new BooleanSubscription()); + + Schedulers.single().scheduleDirect(new Runnable() { + @Override + public void run() { + ts1.onComplete(); + } + }, 200, TimeUnit.MILLISECONDS); + + ts1.await(); + } + + @Test + public void errors() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(); + + ts.onSubscribe(new BooleanSubscription()); + + assertEquals(0, ts.errors().size()); + + ts.onError(new TestException()); + + assertEquals(1, ts.errors().size()); + + TestHelper.assertError(ts.errors(), 0, TestException.class); + } + + @Test + public void onNext() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(); + + ts.onSubscribe(new BooleanSubscription()); + + ts.assertNoValues(); + + assertEquals(Collections.emptyList(), ts.values()); + + ts.onNext(1); + + assertEquals(Collections.singletonList(1), ts.values()); + + ts.cancel(); + + assertTrue(ts.isCancelled()); + assertTrue(ts.isDisposed()); + + ts.assertValue(1); + + ts.onComplete(); + } + + @Test + public void fusionModeToString() { + assertEquals("NONE", TestSubscriberEx.fusionModeToString(QueueFuseable.NONE)); + assertEquals("SYNC", TestSubscriberEx.fusionModeToString(QueueFuseable.SYNC)); + assertEquals("ASYNC", TestSubscriberEx.fusionModeToString(QueueFuseable.ASYNC)); + assertEquals("Unknown(100)", TestSubscriberEx.fusionModeToString(100)); + } + + @Test + public void multipleTerminals() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(); + + ts.onSubscribe(new BooleanSubscription()); + + ts.assertNotComplete(); + + ts.onComplete(); + + try { + ts.assertNotComplete(); + throw new RuntimeException("Should have thrown"); + } catch (Throwable ex) { + // expected + } + + ts.assertTerminated(); + + ts.onComplete(); + + try { + ts.assertComplete(); + throw new RuntimeException("Should have thrown"); + } catch (Throwable ex) { + // expected + } + + try { + ts.assertTerminated(); + throw new RuntimeException("Should have thrown"); + } catch (Throwable ex) { + // expected + } + + try { + ts.assertNotComplete(); + throw new RuntimeException("Should have thrown"); + } catch (Throwable ex) { + // expected + } + } + + @Test + public void assertValue() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(); + + ts.onSubscribe(new BooleanSubscription()); + + try { + ts.assertValue(1); + throw new RuntimeException("Should have thrown"); + } catch (Throwable ex) { + // expected + } + + ts.onNext(1); + + ts.assertValue(1); + + try { + ts.assertValue(2); + throw new RuntimeException("Should have thrown"); + } catch (Throwable ex) { + // expected + } + + ts.onNext(2); + + try { + ts.assertValue(1); + throw new RuntimeException("Should have thrown"); + } catch (Throwable ex) { + // expected + } + } + + @Test + public void onNextMisbehave() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(); + + ts.onNext(1); + + ts.assertError(IllegalStateException.class); + + ts = new TestSubscriberEx<>(); + + ts.onSubscribe(new BooleanSubscription()); + + ts.onNext(null); + + ts.assertFailure(NullPointerException.class, (Integer)null); + } + + @Test + public void awaitTerminalEventInterrupt() { + final TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(); + + ts.onSubscribe(new BooleanSubscription()); + + Thread.currentThread().interrupt(); + + try { + ts.awaitDone(5, TimeUnit.SECONDS); + } catch (RuntimeException allowed) { + assertTrue(allowed.toString(), allowed.getCause() instanceof InterruptedException); + } + + // FIXME ? catch consumes this flag + // assertTrue(Thread.interrupted()); + + Thread.currentThread().interrupt(); + + try { + ts.awaitDone(5, TimeUnit.SECONDS); + } catch (RuntimeException allowed) { + assertTrue(allowed.toString(), allowed.getCause() instanceof InterruptedException); + } + + // FIXME ? catch consumes this flag + // assertTrue(Thread.interrupted()); + } + + @Test + public void assertTerminated2() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(); + + ts.onSubscribe(new BooleanSubscription()); + + ts.assertNotTerminated(); + + ts.onError(new TestException()); + + ts.assertTerminated(); + + ts.onError(new IOException()); + + try { + ts.assertTerminated(); + throw new RuntimeException("Should have thrown"); + } catch (AssertionError ex) { + // expected + } + + try { + ts.assertError(TestException.class); + throw new RuntimeException("Should have thrown"); + } catch (AssertionError ex) { + // expected + } + + ts = new TestSubscriberEx<>(); + + ts.onSubscribe(new BooleanSubscription()); + + ts.onError(new TestException()); + ts.onComplete(); + + try { + ts.assertTerminated(); + throw new RuntimeException("Should have thrown"); + } catch (AssertionError ex) { + // expected + } + } + + @Test + public void onSubscribe() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(); + + ts.onSubscribe(null); + + ts.assertError(NullPointerException.class); + + ts = new TestSubscriberEx<>(); + + ts.onSubscribe(new BooleanSubscription()); + + BooleanSubscription bs1 = new BooleanSubscription(); + + ts.onSubscribe(bs1); + + assertTrue(bs1.isCancelled()); + + ts.assertError(IllegalStateException.class); + + ts = new TestSubscriberEx<>(); + ts.dispose(); + + bs1 = new BooleanSubscription(); + + ts.onSubscribe(bs1); + + assertTrue(bs1.isCancelled()); + + } + + @Test + public void assertValueSequence() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(); + + ts.onSubscribe(new BooleanSubscription()); + + ts.onNext(1); + ts.onNext(2); + + try { + ts.assertValueSequence(Collections.<Integer>emptyList()); + throw new RuntimeException("Should have thrown"); + } catch (AssertionError ex) { + // expected + } + + try { + ts.assertValueSequence(Collections.singletonList(1)); + throw new RuntimeException("Should have thrown"); + } catch (AssertionError ex) { + // expected + } + + ts.assertValueSequence(Arrays.asList(1, 2)); + + try { + ts.assertValueSequence(Arrays.asList(1, 2, 3)); + throw new RuntimeException("Should have thrown"); + } catch (AssertionError ex) { + // expected + } + } + + @Test + public void assertEmpty() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(); + + try { + ts.assertEmpty(); + throw new RuntimeException("Should have thrown!"); + } catch (AssertionError ex) { + // expected + } + + ts.onSubscribe(new BooleanSubscription()); + + ts.assertEmpty(); + + ts.onNext(1); + + try { + ts.assertEmpty(); + throw new RuntimeException("Should have thrown!"); + } catch (AssertionError ex) { + // expected + } + } + + @Test + public void awaitDoneTimed() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(); + + Thread.currentThread().interrupt(); + + try { + ts.awaitDone(5, TimeUnit.SECONDS); + } catch (RuntimeException ex) { + assertTrue(ex.toString(), ex.getCause() instanceof InterruptedException); + } + } + + @Test + public void assertNotSubscribed() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(); + + ts.assertNotSubscribed(); + + ts.errors().add(new TestException()); + + try { + ts.assertNotSubscribed(); + throw new RuntimeException("Should have thrown!"); + } catch (AssertionError ex) { + // expected + } + } + + @Test + public void assertErrorMultiple() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(); + + TestException e = new TestException(); + ts.errors().add(e); + ts.errors().add(new TestException()); + + try { + ts.assertError(TestException.class); + throw new RuntimeException("Should have thrown!"); + } catch (AssertionError ex) { + // expected + } + try { + ts.assertError(e); + throw new RuntimeException("Should have thrown!"); + } catch (AssertionError ex) { + // expected + } + try { + ts.assertErrorMessage(""); + throw new RuntimeException("Should have thrown!"); + } catch (AssertionError ex) { + // expected + } + } + + @Test + public void assertComplete() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(); + + ts.onSubscribe(new BooleanSubscription()); + + try { + ts.assertComplete(); + throw new RuntimeException("Should have thrown!"); + } catch (AssertionError ex) { + // expected + } + + ts.onComplete(); + + ts.assertComplete(); + + ts.onComplete(); + + try { + ts.assertComplete(); + throw new RuntimeException("Should have thrown!"); + } catch (AssertionError ex) { + // expected + } + } + + @Test + public void completeWithoutOnSubscribe() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(); + + ts.onComplete(); + + ts.assertError(IllegalStateException.class); + } + + @Test + public void completeDelegateThrows() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(new FlowableSubscriber<Integer>() { + + @Override + public void onSubscribe(Subscription s) { + + } + + @Override + public void onNext(Integer value) { + + } + + @Override + public void onError(Throwable e) { + throw new TestException(); + } + + @Override + public void onComplete() { + throw new TestException(); + } + + }); + + ts.onSubscribe(new BooleanSubscription()); + + try { + ts.onComplete(); + throw new RuntimeException("Should have thrown!"); + } catch (TestException ex) { + ts.assertTerminated(); + } + } + + @Test + public void errorDelegateThrows() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(new FlowableSubscriber<Integer>() { + + @Override + public void onSubscribe(Subscription s) { + + } + + @Override + public void onNext(Integer value) { + + } + + @Override + public void onError(Throwable e) { + throw new TestException(); + } + + @Override + public void onComplete() { + throw new TestException(); + } + + }); + + ts.onSubscribe(new BooleanSubscription()); + + try { + ts.onError(new IOException()); + throw new RuntimeException("Should have thrown!"); + } catch (TestException ex) { + ts.assertTerminated(); + } + } + + @Test + public void syncQueueThrows() { + TestSubscriberEx<Object> ts = new TestSubscriberEx<>(); + ts.setInitialFusionMode(QueueFuseable.SYNC); + + Flowable.range(1, 5) + .map(new Function<Integer, Object>() { + @Override + public Object apply(Integer v) throws Exception { throw new TestException(); } + }) + .subscribe(ts); + + ts.assertSubscribed() + .assertFuseable() + .assertFusionMode(QueueFuseable.SYNC) + .assertFailure(TestException.class); + } + + @Test + public void asyncQueueThrows() { + TestSubscriberEx<Object> ts = new TestSubscriberEx<>(); + ts.setInitialFusionMode(QueueFuseable.ANY); + + UnicastProcessor<Integer> up = UnicastProcessor.create(); + + up + .map(new Function<Integer, Object>() { + @Override + public Object apply(Integer v) throws Exception { throw new TestException(); } + }) + .subscribe(ts); + + up.onNext(1); + + ts.assertSubscribed() + .assertFuseable() + .assertFusionMode(QueueFuseable.ASYNC) + .assertFailure(TestException.class); + } + + @Test + public void assertValuePredicateEmpty() { + assertThrows("No values", AssertionError.class, () -> { + TestSubscriberEx<Object> ts = new TestSubscriberEx<>(); + + Flowable.empty().subscribe(ts); + + ts.assertValue(new Predicate<Object>() { + @Override public boolean test(final Object o) throws Exception { + return false; + } + }); + }); + } + + @Test + public void assertValuePredicateMatch() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(); + + Flowable.just(1).subscribe(ts); + + ts.assertValue(new Predicate<Integer>() { + @Override public boolean test(final Integer o) throws Exception { + return o == 1; + } + }); + } + + @Test + public void assertValuePredicateNoMatch() { + assertThrows("Value not present", AssertionError.class, () -> { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(); + + Flowable.just(1).subscribe(ts); + + ts.assertValue(new Predicate<Integer>() { + @Override public boolean test(final Integer o) throws Exception { + return o != 1; + } + }); + }); + } + + @Test + public void assertValuePredicateMatchButMore() { + assertThrows("Value present but other values as well", AssertionError.class, () -> { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(); + + Flowable.just(1, 2).subscribe(ts); + + ts.assertValue(new Predicate<Integer>() { + @Override public boolean test(final Integer o) throws Exception { + return o == 1; + } + }); + }); + } + + @Test + public void assertValueAtPredicateEmpty() { + assertThrows("No values", AssertionError.class, () -> { + TestSubscriberEx<Object> ts = new TestSubscriberEx<>(); + + Flowable.empty().subscribe(ts); + + ts.assertValueAt(0, new Predicate<Object>() { + @Override public boolean test(final Object o) throws Exception { + return false; + } + }); + }); + } + + @Test + public void assertValueAtPredicateMatch() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(); + + Flowable.just(1, 2).subscribe(ts); + + ts.assertValueAt(1, new Predicate<Integer>() { + @Override public boolean test(final Integer o) throws Exception { + return o == 2; + } + }); + } + + @Test + public void assertValueAtPredicateNoMatch() { + assertThrows("Value not present", AssertionError.class, () -> { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(); + + Flowable.just(1, 2, 3).subscribe(ts); + + ts.assertValueAt(2, new Predicate<Integer>() { + @Override public boolean test(final Integer o) throws Exception { + return o != 3; + } + }); + }); + } + + @Test + public void assertValueAtInvalidIndex() { + assertThrows("Invalid index: 2 (latch = 0, values = 2, errors = 0, completions = 1)", AssertionError.class, () -> { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(); + + Flowable.just(1, 2).subscribe(ts); + + ts.assertValueAt(2, new Predicate<Integer>() { + @Override public boolean test(final Integer o) throws Exception { + return o == 1; + } + }); + }); + } + + @Test + public void requestMore() { + Flowable.range(1, 5) + .test(0) + .requestMore(1) + .assertValue(1) + .requestMore(2) + .assertValues(1, 2, 3) + .requestMore(3) + .assertResult(1, 2, 3, 4, 5); + } + + @Test + public void withTag() { + try { + for (int i = 1; i < 3; i++) { + Flowable.just(i) + .test() + .withTag("testing with item=" + i) + .assertResult(1) + ; + } + throw new RuntimeException("Should have thrown!"); + } catch (AssertionError ex) { + assertTrue(ex.toString(), ex.toString().contains("testing with item=2")); + } + } + + @Test + public void timeoutIndicated() throws InterruptedException { + Thread.interrupted(); // clear flag + + TestSubscriberEx<Object> ts = Flowable.never() + .to(TestHelper.<Object>testConsumer()); + assertFalse(ts.await(1, TimeUnit.MILLISECONDS)); + + try { + ts.assertResult(1); + throw new RuntimeException("Should have thrown!"); + } catch (AssertionError ex) { + assertTrue(ex.toString(), ex.toString().contains("timeout!")); + } + } + + @Test + public void timeoutIndicated2() throws InterruptedException { + try { + Flowable.never() + .test() + .awaitDone(1, TimeUnit.MILLISECONDS) + .assertResult(1); + + throw new RuntimeException("Should have thrown!"); + } catch (AssertionError ex) { + assertTrue(ex.toString(), ex.toString().contains("timeout!")); + } + } + + @Test + public void timeoutIndicated3() throws InterruptedException { + TestSubscriberEx<Object> ts = Flowable.never() + .to(TestHelper.<Object>testConsumer()); + assertFalse(ts.await(1, TimeUnit.MILLISECONDS)); + + try { + ts.assertResult(1); + throw new RuntimeException("Should have thrown!"); + } catch (AssertionError ex) { + assertTrue(ex.toString(), ex.toString().contains("timeout!")); + } + } + + @Test + public void disposeIndicated() { + TestSubscriberEx<Object> ts = new TestSubscriberEx<>(); + ts.cancel(); + + try { + ts.assertResult(1); + throw new RuntimeException("Should have thrown!"); + } catch (Throwable ex) { + assertTrue(ex.toString(), ex.toString().contains("disposed!")); + } + } + + @Test + public void awaitCount() { + Flowable.range(1, 10).delay(100, TimeUnit.MILLISECONDS) + .test(5) + .awaitCount(5) + .assertValues(1, 2, 3, 4, 5) + .requestMore(5) + .awaitDone(5, TimeUnit.SECONDS) + .assertResult(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + } + + @Test + public void awaitCountLess() { + Flowable.range(1, 4) + .test() + .awaitCount(5) + .assertResult(1, 2, 3, 4); + } + + @Test + public void assertValueAtPredicateThrows() { + try { + Flowable.just(1) + .test() + .assertValueAt(0, new Predicate<Integer>() { + @Override + public boolean test(Integer t) throws Exception { + throw new IllegalArgumentException(); + } + }); + throw new RuntimeException("Should have thrown!"); + } catch (IllegalArgumentException ex) { + // expected + } + } + + @Test + public void assertValuesOnly() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(); + ts.onSubscribe(new BooleanSubscription()); + ts.assertValuesOnly(); + + ts.onNext(5); + ts.assertValuesOnly(5); + + ts.onNext(-1); + ts.assertValuesOnly(5, -1); + } + + @Test + public void assertValuesOnlyThrowsOnUnexpectedValue() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(); + ts.onSubscribe(new BooleanSubscription()); + ts.assertValuesOnly(); + + ts.onNext(5); + ts.assertValuesOnly(5); + + ts.onNext(-1); + + try { + ts.assertValuesOnly(5); + throw new RuntimeException(); + } catch (AssertionError ex) { + // expected + } + } + + @Test + public void assertValuesOnlyThrowsWhenCompleted() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(); + ts.onSubscribe(new BooleanSubscription()); + + ts.onComplete(); + + try { + ts.assertValuesOnly(); + throw new RuntimeException(); + } catch (AssertionError ex) { + // expected + } + } + + @Test + public void assertValuesOnlyThrowsWhenErrored() { + TestSubscriberEx<Integer> ts = new TestSubscriberEx<>(); + ts.onSubscribe(new BooleanSubscription()); + + ts.onError(new TestException()); + + try { + ts.assertValuesOnly(); + throw new RuntimeException(); + } catch (AssertionError ex) { + // expected + } + } +} diff --git a/src/test/java/io/reactivex/rxjava3/testsupport/TimesteppingScheduler.java b/src/test/java/io/reactivex/rxjava3/testsupport/TimesteppingScheduler.java new file mode 100644 index 0000000000..06804fa282 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/testsupport/TimesteppingScheduler.java @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.testsupport; + +import java.util.concurrent.TimeUnit; + +import io.reactivex.rxjava3.core.Scheduler; +import io.reactivex.rxjava3.disposables.*; + +/** + * Basic scheduler that produces an ever increasing {@link #now(TimeUnit)} value. + * Use this scheduler only as a time source! + */ +public final class TimesteppingScheduler extends Scheduler { + + final class TimesteppingWorker extends Worker { + @Override + public void dispose() { + } + + @Override + public boolean isDisposed() { + return false; + } + + @Override + public Disposable schedule(Runnable run, long delay, TimeUnit unit) { + run.run(); + return Disposable.disposed(); + } + + @Override + public long now(TimeUnit unit) { + return TimesteppingScheduler.this.now(unit); + } + } + + public long time; + + public boolean stepEnabled = true; + + @Override + public Worker createWorker() { + return new TimesteppingWorker(); + } + + @Override + public long now(TimeUnit unit) { + if (stepEnabled) { + return time++; + } + return time; + } +} diff --git a/src/test/java/io/reactivex/rxjava3/validators/BaseTypeAnnotations.java b/src/test/java/io/reactivex/rxjava3/validators/BaseTypeAnnotations.java new file mode 100644 index 0000000000..582df0fa71 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/validators/BaseTypeAnnotations.java @@ -0,0 +1,374 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.validators; + +import static org.junit.Assert.fail; + +import java.lang.reflect.*; + +import org.junit.Test; +import org.reactivestreams.Publisher; + +import io.reactivex.rxjava3.annotations.*; +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.disposables.DisposableContainer; +import io.reactivex.rxjava3.flowables.ConnectableFlowable; +import io.reactivex.rxjava3.observables.ConnectableObservable; +import io.reactivex.rxjava3.parallel.ParallelFlowable; +import io.reactivex.rxjava3.processors.*; +import io.reactivex.rxjava3.subjects.*; + +/** + * Verifies several properties. + * <ul> + * <li>Certain public base type methods have the {@link CheckReturnValue} present</li> + * <li>All public base type methods have the {@link SchedulerSupport} present</li> + * <li>All public base type methods which return Flowable have the {@link BackpressureSupport} present</li> + * <li>All public base types that don't return Flowable don't have the {@link BackpressureSupport} present (these are copy-paste errors)</li> + * </ul> + */ +public class BaseTypeAnnotations { + + static void checkCheckReturnValueSupport(Class<?> clazz) { + StringBuilder b = new StringBuilder(); + + for (Method m : clazz.getMethods()) { + if (m.getDeclaringClass() == clazz) { + boolean isSubscribeMethod = "subscribe".equals(m.getName()) && + (m.getParameterTypes().length == 0 || m.getParameterTypes()[m.getParameterCount() - 1] == DisposableContainer.class); + boolean isConnectMethod = "connect".equals(m.getName()) && m.getParameterTypes().length == 0; + boolean isAnnotationPresent = m.isAnnotationPresent(CheckReturnValue.class); + + if (isSubscribeMethod || isConnectMethod) { + if (isAnnotationPresent) { + b.append(m.getName()).append(" method has @CheckReturnValue: ").append(m).append("\r\n"); + } + continue; + } + + if (Modifier.isPrivate(m.getModifiers()) && isAnnotationPresent) { + b.append("Private method has @CheckReturnValue: ").append(m).append("\r\n"); + continue; + } + + if (m.getReturnType().equals(Void.TYPE)) { + if (isAnnotationPresent) { + b.append("Void method has @CheckReturnValue: ").append(m).append("\r\n"); + } + continue; + } + + if (!isAnnotationPresent) { + b.append("Missing @CheckReturnValue: ").append(m).append("\r\n"); + } + } + } + + if (b.length() != 0) { + System.out.println(clazz); + System.out.println("------------------------"); + System.out.println(b); + + fail(b.toString()); + } + } + + static void checkSchedulerSupport(Class<?> clazz) { + StringBuilder b = new StringBuilder(); + + for (Method m : clazz.getMethods()) { + if (m.getName().equals("bufferSize") + || m.getName().equals("parallelism")) { + continue; + } + if (m.getDeclaringClass() == clazz) { + if (!m.isAnnotationPresent(SchedulerSupport.class)) { + b.append("Missing @SchedulerSupport: ").append(m).append("\r\n"); + } else { + SchedulerSupport ann = m.getAnnotation(SchedulerSupport.class); + + if (ann.value().equals(SchedulerSupport.CUSTOM)) { + boolean found = false; + for (Class<?> paramclazz : m.getParameterTypes()) { + if (Scheduler.class.isAssignableFrom(paramclazz)) { + found = true; + break; + } + } + if (!found) { + b.append("Marked with CUSTOM scheduler but no Scheduler parameter: ").append(m).append("\r\n"); + } + } else { + for (Class<?> paramclazz : m.getParameterTypes()) { + if (Scheduler.class.isAssignableFrom(paramclazz)) { + if (!m.getName().equals("timestamp") && !m.getName().equals("timeInterval")) { + b.append("Marked with specific scheduler but Scheduler parameter found: ").append(m).append("\r\n"); + break; + } + } + } + } + } + } + } + + if (b.length() != 0) { + System.out.println(clazz); + System.out.println("------------------------"); + System.out.println(b); + + fail(b.toString()); + } + } + + static void checkBackpressureSupport(Class<?> clazz) { + StringBuilder b = new StringBuilder(); + + for (Method m : clazz.getMethods()) { + if (m.getName().equals("bufferSize") + || m.getName().equals("parallelism")) { + continue; + } + if (m.getDeclaringClass() == clazz) { + if (clazz == Flowable.class || clazz == ParallelFlowable.class) { + if (!m.isAnnotationPresent(BackpressureSupport.class)) { + b.append("No @BackpressureSupport annotation (being ") + .append(clazz.getSimpleName()) + .append("): ").append(m).append("\r\n"); + } + } else { + if (m.getReturnType() == Flowable.class + || m.getReturnType() == ParallelFlowable.class) { + if (!m.isAnnotationPresent(BackpressureSupport.class)) { + b.append("No @BackpressureSupport annotation (having ") + .append(m.getReturnType().getSimpleName()) + .append(" return): ").append(m).append("\r\n"); + } + } else { + boolean found = false; + for (Class<?> paramclazz : m.getParameterTypes()) { + if (Publisher.class.isAssignableFrom(paramclazz)) { + found = true; + break; + } + } + + if (found) { + if (!m.isAnnotationPresent(BackpressureSupport.class)) { + b.append("No @BackpressureSupport annotation (has Publisher param): ").append(m).append("\r\n"); + } + } else { + if (m.isAnnotationPresent(BackpressureSupport.class)) { + b.append("Unnecessary @BackpressureSupport annotation: ").append(m).append("\r\n"); + } + } + } + } + } + } + + if (b.length() != 0) { + System.out.println(clazz); + System.out.println("------------------------"); + System.out.println(b); + + fail(b.toString()); + } + } + + @Test + public void checkReturnValueFlowable() { + checkCheckReturnValueSupport(Flowable.class); + } + + @Test + public void checkReturnValueObservable() { + checkCheckReturnValueSupport(Observable.class); + } + + @Test + public void checkReturnValueSingle() { + checkCheckReturnValueSupport(Single.class); + } + + @Test + public void checkReturnValueCompletable() { + checkCheckReturnValueSupport(Completable.class); + } + + @Test + public void checkReturnValueMaybe() { + checkCheckReturnValueSupport(Maybe.class); + } + + @Test + public void checkReturnValueConnectableObservable() { + checkCheckReturnValueSupport(ConnectableObservable.class); + } + + @Test + public void checkReturnValueConnectableFlowable() { + checkCheckReturnValueSupport(ConnectableFlowable.class); + } + + @Test + public void checkReturnValueParallelFlowable() { + checkCheckReturnValueSupport(ParallelFlowable.class); + } + + @Test + public void checkReturnValueAsyncSubject() { + checkCheckReturnValueSupport(AsyncSubject.class); + } + + @Test + public void checkReturnValueBehaviorSubject() { + checkCheckReturnValueSupport(BehaviorSubject.class); + } + + @Test + public void checkReturnValuePublishSubject() { + checkCheckReturnValueSupport(PublishSubject.class); + } + + @Test + public void checkReturnValueReplaySubject() { + checkCheckReturnValueSupport(ReplaySubject.class); + } + + @Test + public void checkReturnValueUnicastSubject() { + checkCheckReturnValueSupport(UnicastSubject.class); + } + + @Test + public void checkReturnValueAsyncProcessor() { + checkCheckReturnValueSupport(AsyncProcessor.class); + } + + @Test + public void checkReturnValueBehaviorProcessor() { + checkCheckReturnValueSupport(BehaviorProcessor.class); + } + + @Test + public void checkReturnValuePublishProcessor() { + checkCheckReturnValueSupport(PublishProcessor.class); + } + + @Test + public void checkReturnValueReplayProcessor() { + checkCheckReturnValueSupport(ReplayProcessor.class); + } + + @Test + public void checkReturnValueUnicastProcessor() { + checkCheckReturnValueSupport(UnicastProcessor.class); + } + + @Test + public void checkReturnValueMulticastProcessor() { + checkCheckReturnValueSupport(MulticastProcessor.class); + } + + @Test + public void checkReturnValueSubject() { + checkCheckReturnValueSupport(Subject.class); + } + + @Test + public void checkReturnValueFlowableProcessor() { + checkCheckReturnValueSupport(FlowableProcessor.class); + } + + @Test + public void schedulerSupportFlowable() { + checkSchedulerSupport(Flowable.class); + } + + @Test + public void schedulerSupportObservable() { + checkSchedulerSupport(Observable.class); + } + + @Test + public void schedulerSupportSingle() { + checkSchedulerSupport(Single.class); + } + + @Test + public void schedulerSupportCompletable() { + checkSchedulerSupport(Completable.class); + } + + @Test + public void schedulerSupportMaybe() { + checkSchedulerSupport(Maybe.class); + } + + @Test + public void schedulerSupportConnectableObservable() { + checkSchedulerSupport(ConnectableObservable.class); + } + + @Test + public void schedulerSupportConnectableFlowable() { + checkSchedulerSupport(ConnectableFlowable.class); + } + + @Test + public void schedulerSupportParallelFlowable() { + checkSchedulerSupport(ParallelFlowable.class); + } + + @Test + public void backpressureSupportFlowable() { + checkBackpressureSupport(Flowable.class); + } + + @Test + public void backpressureSupportObservable() { + checkBackpressureSupport(Observable.class); + } + + @Test + public void backpressureSupportSingle() { + checkBackpressureSupport(Single.class); + } + + @Test + public void backpressureSupportCompletable() { + checkBackpressureSupport(Completable.class); + } + + @Test + public void backpressureSupportMaybe() { + checkBackpressureSupport(Maybe.class); + } + + @Test + public void backpressureSupportConnectableFlowable() { + checkBackpressureSupport(ConnectableFlowable.class); + } + + @Test + public void backpressureSupportConnectableObservable() { + checkBackpressureSupport(ConnectableObservable.class); + } + + @Test + public void backpressureSupportParallelFlowable() { + checkBackpressureSupport(ParallelFlowable.class); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/validators/BaseTypeParser.java b/src/test/java/io/reactivex/rxjava3/validators/BaseTypeParser.java new file mode 100644 index 0000000000..528ee6de97 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/validators/BaseTypeParser.java @@ -0,0 +1,145 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.validators; + +import java.io.File; +import java.util.*; + +/** + * Parses the java file of a reactive base type to allow discovering Javadoc mistakes algorithmically. + */ +public final class BaseTypeParser { + + private BaseTypeParser() { + throw new IllegalStateException("No instances!"); + } + + public static class RxMethod { + public String signature; + + public String backpressureKind; + + public String schedulerKind; + + public String javadoc; + + public String backpressureDocumentation; + + public String schedulerDocumentation; + + public int javadocLine; + + public int methodLine; + + public int backpressureDocLine; + + public int schedulerDocLine; + } + + public static List<RxMethod> parse(File f, String baseClassName) throws Exception { + List<RxMethod> list = new ArrayList<>(); + + StringBuilder b = JavadocForAnnotations.readFile(f); + + int baseIndex = b.indexOf("public abstract class " + baseClassName); + + if (baseIndex < 0) { + throw new AssertionError("Wrong base class file: " + baseClassName); + } + + for (;;) { + RxMethod m = new RxMethod(); + + int javadocStart = b.indexOf("/**", baseIndex); + + if (javadocStart < 0) { + break; + } + + int javadocEnd = b.indexOf("*/", javadocStart + 2); + + m.javadoc = b.substring(javadocStart, javadocEnd + 2); + m.javadocLine = JavadocForAnnotations.lineNumber(b, javadocStart); + + int backpressureDoc = b.indexOf("<dt><b>Backpressure:</b></dt>", javadocStart); + if (backpressureDoc > 0 && backpressureDoc < javadocEnd) { + m.backpressureDocLine = JavadocForAnnotations.lineNumber(b, backpressureDoc); + int nextDD = b.indexOf("</dd>", backpressureDoc); + if (nextDD > 0 && nextDD < javadocEnd) { + m.backpressureDocumentation = b.substring(backpressureDoc, nextDD + 5); + } + } + + int schedulerDoc = b.indexOf("<dt><b>Scheduler:</b></dt>", javadocStart); + if (schedulerDoc > 0 && schedulerDoc < javadocEnd) { + m.schedulerDocLine = JavadocForAnnotations.lineNumber(b, schedulerDoc); + int nextDD = b.indexOf("</dd>", schedulerDoc); + if (nextDD > 0 && nextDD < javadocEnd) { + m.schedulerDocumentation = b.substring(schedulerDoc, nextDD + 5); + } + } + + int staticMethodDef = b.indexOf("public static ", javadocEnd + 2); + if (staticMethodDef < 0) { + staticMethodDef = Integer.MAX_VALUE; + } + int instanceMethodDef = b.indexOf("public final ", javadocEnd + 2); + if (instanceMethodDef < 0) { + instanceMethodDef = Integer.MAX_VALUE; + } + + int javadocStartNext = b.indexOf("/**", javadocEnd + 2); + if (javadocStartNext < 0) { + javadocStartNext = Integer.MAX_VALUE; + } + + int definitionStart = -1; + + if (staticMethodDef > 0 && staticMethodDef < javadocStartNext && staticMethodDef < instanceMethodDef) { + definitionStart = staticMethodDef; + } + if (instanceMethodDef > 0 && instanceMethodDef < javadocStartNext && instanceMethodDef < staticMethodDef) { + definitionStart = instanceMethodDef; + } + + if (definitionStart > 0) { + int methodDefEnd = b.indexOf("{", definitionStart); + + m.signature = b.substring(definitionStart, methodDefEnd + 1); + + m.methodLine = JavadocForAnnotations.lineNumber(b, definitionStart); + + int backpressureSpec = b.indexOf("@BackpressureSupport(", javadocEnd); + if (backpressureSpec > 0 && backpressureSpec < definitionStart) { + int backpressureSpecEnd = b.indexOf(")", backpressureSpec + 21); + m.backpressureKind = b.substring(backpressureSpec + 21, backpressureSpecEnd); + } + + int schhedulerSpec = b.indexOf("@SchedulerSupport(", javadocEnd); + if (schhedulerSpec > 0 && schhedulerSpec < definitionStart) { + int schedulerSpecEnd = b.indexOf(")", schhedulerSpec + 18); + m.schedulerKind = b.substring(schhedulerSpec + 18, schedulerSpecEnd); + } + + list.add(m); + baseIndex = methodDefEnd; + } else { + baseIndex = javadocEnd + 2; + } + + } + + return list; + } +} diff --git a/src/test/java/io/reactivex/rxjava3/validators/CatchThrowIfFatalCheck.java b/src/test/java/io/reactivex/rxjava3/validators/CatchThrowIfFatalCheck.java new file mode 100644 index 0000000000..45594fe17b --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/validators/CatchThrowIfFatalCheck.java @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.validators; + +import java.io.File; +import java.nio.file.Files; +import java.util.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.testsupport.TestHelper; + +/** + * Check if a {@code catch(Throwable} is followed by a + * {@code Exceptions.throwIfFatal}, {@code Exceptions.wrapOrThrow} + * or {@code fail} call. + * @since 3.0.0 + */ +public class CatchThrowIfFatalCheck { + + @Test + public void check() throws Exception { + File f = TestHelper.findSource("Flowable"); + if (f == null) { + System.out.println("Unable to find sources of RxJava"); + return; + } + + Queue<File> dirs = new ArrayDeque<>(); + + StringBuilder fail = new StringBuilder(); + int errors = 0; + + File parent = f.getParentFile().getParentFile(); + + dirs.offer(new File(parent.getAbsolutePath().replace('\\', '/'))); + + while (!dirs.isEmpty()) { + f = dirs.poll(); + + File[] list = f.listFiles(); + if (list != null && list.length != 0) { + + for (File u : list) { + if (u.isDirectory()) { + dirs.offer(u); + } else { + List<String> lines = Files.readAllLines(u.toPath()); + + for (int i = 0; i < lines.size(); i++) { + String line = lines.get(i).trim(); + + if (line.startsWith("} catch (Throwable ")) { + String next = lines.get(i + 1).trim(); + boolean throwIfFatal = next.contains("Exceptions.throwIfFatal"); + boolean wrapOrThrow = next.contains("ExceptionHelper.wrapOrThrow"); + boolean failCall = next.startsWith("fail("); + + if (!(throwIfFatal || wrapOrThrow || failCall)) { + errors++; + fail.append("Missing Exceptions.throwIfFatal\n ") + .append(next) + .append("\n at ") + .append(u.getName().replace(".java", "")) + .append(".method(") + .append(u.getName()) + .append(":") + .append(i + 1) + .append(")\n") + ; + } + } + } + } + } + } + } + + if (errors != 0) { + fail.insert(0, "Found " + errors + " cases\n"); + System.out.println(fail); + throw new AssertionError(fail.toString()); + } + } +} diff --git a/src/test/java/io/reactivex/rxjava3/validators/CheckLocalVariablesInTests.java b/src/test/java/io/reactivex/rxjava3/validators/CheckLocalVariablesInTests.java new file mode 100644 index 0000000000..0ec5952f8b --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/validators/CheckLocalVariablesInTests.java @@ -0,0 +1,444 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.validators; + +import java.io.*; +import java.util.*; +import java.util.regex.Pattern; + +import org.junit.Test; + +import io.reactivex.rxjava3.testsupport.TestHelper; + +/** + * Checks for commonly copy-pasted but not-renamed local variables in unit tests. + * <ul> + * <li>{@code TestSubscriber} named as {@code to*}</li> + * <li>{@code TestObserver} named as {@code ts*}</li> + * <li>{@code PublishProcessor} named as {@code ps*}</li> + * <li>{@code PublishSubject} named as {@code pp*}</li> + * <li>{@code Subscription} with single letter name such as "s" or "d"</li> + * <li>{@code Disposable} with single letter name such as "s" or "d"</li> + * <li>{@code Flowable} named as {@code o|observable} + number</li> + * <li>{@code Observable} named as {@code f|flowable} + number</li> + * <li>{@code Subscriber} named as "o" or "observer"</li> + * <li>{@code Observer} named as "s" or "subscriber"</li> + * </ul> + */ +public class CheckLocalVariablesInTests { + + static void findPattern(String pattern) throws Exception { + findPattern(pattern, false); + } + + static void findPattern(String pattern, boolean checkMain) throws Exception { + File f = TestHelper.findSource("Flowable"); + if (f == null) { + System.out.println("Unable to find sources of RxJava"); + return; + } + + Queue<File> dirs = new ArrayDeque<>(); + + StringBuilder fail = new StringBuilder(); + fail.append("The following code pattern was found: ").append(pattern).append("\n"); + + File parent = f.getParentFile().getParentFile(); + + if (checkMain) { + dirs.offer(new File(parent.getAbsolutePath().replace('\\', '/'))); + } + dirs.offer(new File(parent.getAbsolutePath().replace('\\', '/').replace("src/main/java", "src/test/java"))); + + Pattern p = Pattern.compile(pattern); + + int total = 0; + + while (!dirs.isEmpty()) { + f = dirs.poll(); + + File[] list = f.listFiles(); + if (list != null && list.length != 0) { + + for (File u : list) { + if (u.isDirectory()) { + dirs.offer(u); + } else { + String fname = u.getName(); + if (fname.endsWith(".java")) { + + int lineNum = 0; + BufferedReader in = new BufferedReader(new FileReader(u)); + try { + for (;;) { + String line = in.readLine(); + if (line != null) { + lineNum++; + + line = line.trim(); + + if (!line.startsWith("//") && !line.startsWith("*")) { + if (p.matcher(line).find()) { + fail + .append(fname) + .append("#L").append(lineNum) + .append(" ").append(line) + .append("\n") + .append(" at ") + .append(fname.replace(".java", "")) + .append(".method(") + .append(fname) + .append(":") + .append(lineNum) + .append(")\n"); + + total++; + } + } + } else { + break; + } + } + } finally { + in.close(); + } + } + } + } + } + } + if (total != 0) { + fail.insert(0, "Found " + total + " instances"); + System.out.println(fail); + throw new AssertionError(fail.toString()); + } + } + + @Test + public void subscriberAsTo() throws Exception { + findPattern("TestSubscriber(Ex)?<.*>\\s+to"); + } + + @Test + public void observerAsTs() throws Exception { + findPattern("TestObserver(Ex)?<.*>\\s+ts"); + } + + @Test + public void subscriberNoArgAsTo() throws Exception { + findPattern("TestSubscriber(Ex)?\\s+to"); + } + + @Test + public void observerNoArgAsTs() throws Exception { + findPattern("TestObserver(Ex)?\\s+ts"); + } + + @Test + public void publishSubjectAsPp() throws Exception { + findPattern("PublishSubject<.*>\\s+pp"); + } + + @Test + public void publishProcessorAsPs() throws Exception { + findPattern("PublishProcessor<.*>\\s+ps"); + } + + @Test + public void unicastSubjectAsUp() throws Exception { + findPattern("UnicastSubject<.*>\\s+up"); + } + + @Test + public void unicastProcessorAsUs() throws Exception { + findPattern("UnicastProcessor<.*>\\s+us"); + } + + @Test + public void behaviorProcessorAsBs() throws Exception { + findPattern("BehaviorProcessor<.*>\\s+bs"); + } + + @Test + public void behaviorSubjectAsBp() throws Exception { + findPattern("BehaviorSubject<.*>\\s+bp"); + } + + @Test + public void connectableFlowableAsCo() throws Exception { + findPattern("ConnectableFlowable<.*>\\s+co(0-9|\\b)"); + } + + @Test + public void connectableObservableAsCf() throws Exception { + findPattern("ConnectableObservable<.*>\\s+cf(0-9|\\b)"); + } + + @Test + public void queueDisposableInsteadOfQueueFuseable() throws Exception { + findPattern("QueueDisposable\\.(NONE|SYNC|ASYNC|ANY|BOUNDARY)"); + } + + @Test + public void queueSubscriptionInsteadOfQueueFuseable() throws Exception { + findPattern("QueueSubscription\\.(NONE|SYNC|ASYNC|ANY|BOUNDARY)"); + } + + @Test + public void singleSourceAsMs() throws Exception { + findPattern("SingleSource<.*>\\s+ms"); + } + + @Test + public void singleSourceAsCs() throws Exception { + findPattern("SingleSource<.*>\\s+cs"); + } + + @Test + public void maybeSourceAsSs() throws Exception { + findPattern("MaybeSource<.*>\\s+ss"); + } + + @Test + public void maybeSourceAsCs() throws Exception { + findPattern("MaybeSource<.*>\\s+cs"); + } + + @Test + public void completableSourceAsSs() throws Exception { + findPattern("CompletableSource<.*>\\s+ss"); + } + + @Test + public void completableSourceAsMs() throws Exception { + findPattern("CompletableSource<.*>\\s+ms"); + } + + @Test + public void observableAsC() throws Exception { + findPattern("Observable<.*>\\s+c\\b"); + } + + @Test + public void subscriberAsObserver() throws Exception { + findPattern("Subscriber<.*>\\s+observer[0-9]?\\b"); + } + + @Test + public void subscriberAsO() throws Exception { + findPattern("Subscriber<.*>\\s+o[0-9]?\\b"); + } + + @Test + public void singleAsObservable() throws Exception { + findPattern("Single<.*>\\s+observable\\b"); + } + + @Test + public void singleAsFlowable() throws Exception { + findPattern("Single<.*>\\s+flowable\\b"); + } + + @Test + public void observerAsSubscriber() throws Exception { + findPattern("Observer<.*>\\s+subscriber[0-9]?\\b"); + } + + @Test + public void observerAsS() throws Exception { + findPattern("Observer<.*>\\s+s[0-9]?\\b"); + } + + @Test + public void observerNoArgAsSubscriber() throws Exception { + findPattern("Observer\\s+subscriber[0-9]?\\b"); + } + + @Test + public void observerNoArgAsS() throws Exception { + findPattern("Observer\\s+s[0-9]?\\b"); + } + + @Test + public void flowableAsObservable() throws Exception { + findPattern("Flowable<.*>\\s+observable[0-9]?\\b"); + } + + @Test + public void flowableAsO() throws Exception { + findPattern("Flowable<.*>\\s+o[0-9]?\\b"); + } + + @Test + public void flowableNoArgAsO() throws Exception { + findPattern("Flowable\\s+o[0-9]?\\b"); + } + + @Test + public void flowableNoArgAsObservable() throws Exception { + findPattern("Flowable\\s+observable[0-9]?\\b"); + } + + @Test + public void processorAsSubject() throws Exception { + findPattern("Processor<.*>\\s+subject(0-9)?\\b"); + } + + @Test + public void maybeAsObservable() throws Exception { + findPattern("Maybe<.*>\\s+observable\\b"); + } + + @Test + public void maybeAsFlowable() throws Exception { + findPattern("Maybe<.*>\\s+flowable\\b"); + } + + @Test + public void completableAsObservable() throws Exception { + findPattern("Completable\\s+observable\\b"); + } + + @Test + public void completableAsFlowable() throws Exception { + findPattern("Completable\\s+flowable\\b"); + } + + @Test + public void subscriptionAsFieldS() throws Exception { + findPattern("Subscription\\s+s[0-9]?;", true); + } + + @Test + public void subscriptionAsD() throws Exception { + findPattern("Subscription\\s+d[0-9]?", true); + } + + @Test + public void subscriptionAsSubscription() throws Exception { + findPattern("Subscription\\s+subscription[0-9]?;", true); + } + + @Test + public void subscriptionAsDParenthesis() throws Exception { + findPattern("Subscription\\s+d[0-9]?\\)", true); + } + + @Test + public void queueSubscriptionAsD() throws Exception { + findPattern("Subscription<.*>\\s+q?d[0-9]?\\b", true); + } + + @Test + public void booleanSubscriptionAsbd() throws Exception { + findPattern("BooleanSubscription\\s+bd[0-9]?;", true); + } + + @Test + public void atomicSubscriptionAsS() throws Exception { + findPattern("AtomicReference<Subscription>\\s+s[0-9]?;", true); + } + + @Test + public void atomicSubscriptionAsSInit() throws Exception { + findPattern("AtomicReference<Subscription>\\s+s[0-9]?\\s", true); + } + + @Test + public void atomicSubscriptionAsSubscription() throws Exception { + findPattern("AtomicReference<Subscription>\\s+subscription[0-9]?", true); + } + + @Test + public void atomicSubscriptionAsD() throws Exception { + findPattern("AtomicReference<Subscription>\\s+d[0-9]?", true); + } + + @Test + public void disposableAsS() throws Exception { + // the space before makes sure it doesn't match onSubscribe(Subscription) unnecessarily + findPattern("Disposable\\s+s[0-9]?\\b", true); + } + + @Test + public void disposableAsFieldD() throws Exception { + findPattern("Disposable\\s+d[0-9]?;", true); + } + + @Test + public void atomicDisposableAsS() throws Exception { + findPattern("AtomicReference<Disposable>\\s+s[0-9]?", true); + } + + @Test + public void atomicDisposableAsD() throws Exception { + findPattern("AtomicReference<Disposable>\\s+d[0-9]?;", true); + } + + @Test + public void subscriberAsFieldActual() throws Exception { + findPattern("Subscriber<.*>\\s+actual[;\\)]", true); + } + + @Test + public void subscriberNoArgAsFieldActual() throws Exception { + findPattern("Subscriber\\s+actual[;\\)]", true); + } + + @Test + public void subscriberAsFieldS() throws Exception { + findPattern("Subscriber<.*>\\s+s[0-9]?;", true); + } + + @Test + public void observerAsFieldActual() throws Exception { + findPattern("Observer<.*>\\s+actual[;\\)]", true); + } + + @Test + public void observerAsFieldSO() throws Exception { + findPattern("Observer<.*>\\s+[so][0-9]?;", true); + } + + @Test + public void observerNoArgAsFieldActual() throws Exception { + findPattern("Observer\\s+actual[;\\)]", true); + } + + @Test + public void observerNoArgAsFieldCs() throws Exception { + findPattern("Observer\\s+cs[;\\)]", true); + } + + @Test + public void observerNoArgAsFieldSO() throws Exception { + findPattern("Observer\\s+[so][0-9]?;", true); + } + + @Test + public void queueDisposableAsD() throws Exception { + findPattern("Disposable<.*>\\s+q?s[0-9]?\\b", true); + } + + @Test + public void disposableAsDParenthesis() throws Exception { + findPattern("Disposable\\s+s[0-9]?\\)", true); + } + + @Test + public void compositeDisposableAsCs() throws Exception { + findPattern("CompositeDisposable\\s+cs[0-9]?", true); + } + +} diff --git a/src/test/java/io/reactivex/rxjava3/validators/FixLicenseHeaders.java b/src/test/java/io/reactivex/rxjava3/validators/FixLicenseHeaders.java new file mode 100644 index 0000000000..5d7223cbd1 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/validators/FixLicenseHeaders.java @@ -0,0 +1,134 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.validators; + +import java.io.*; +import java.util.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.testsupport.TestHelper; + +/** + * Adds license header to java files. + */ +public class FixLicenseHeaders { + + String[] header = { + "/*", + " * Copyright (c) 2016-present, RxJava Contributors.", + " *", + " * Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except in", + " * compliance with the License. You may obtain a copy of the License at", + " *", + " * http://www.apache.org/licenses/LICENSE-2.0", + " *", + " * Unless required by applicable law or agreed to in writing, software distributed under the License is", + " * distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See", + " * the License for the specific language governing permissions and limitations under the License.", + " */", + "" + }; + + @Test + public void checkAndUpdateLicenses() throws Exception { + if (System.getenv("CI") != null) { + // no point in changing the files in CI + return; + } + File f = TestHelper.findSource("Flowable"); + if (f == null) { + return; + } + + Queue<File> dirs = new ArrayDeque<>(); + + File parent = f.getParentFile().getParentFile(); + dirs.offer(parent); + dirs.offer(new File(parent.getAbsolutePath().replace('\\', '/').replace("src/main/java", "src/perf/java"))); + dirs.offer(new File(parent.getAbsolutePath().replace('\\', '/').replace("src/main/java", "src/test/java"))); + + StringBuilder fail = new StringBuilder(); + + while (!dirs.isEmpty()) { + f = dirs.poll(); + + File[] list = f.listFiles(); + if (list != null && list.length != 0) { + + for (File u : list) { + if (u.isDirectory()) { + dirs.offer(u); + } else { + if (u.getName().endsWith(".java")) { + + List<String> lines = new ArrayList<>(); + BufferedReader in = new BufferedReader(new FileReader(u)); + try { + for (;;) { + String line = in.readLine(); + if (line == null) { + break; + } + + lines.add(line); + } + } finally { + in.close(); + } + + if (!lines.get(0).equals(header[0]) || !lines.get(1).equals(header[1])) { + fail.append("java.lang.RuntimeException: missing header added, refresh and re-run tests!\r\n") + .append(" at ") + ; + + String fn = u.toString().replace('\\', '/'); + + int idx = fn.indexOf("io/reactivex/"); + + fn = fn.substring(idx).replace('/', '.').replace(".java", ""); + + fail.append(fn).append(" (") + ; + + int jdx = fn.lastIndexOf('.'); + + fail.append(fn.substring(jdx + 1)); + + fail.append(".java:1)\r\n\r\n"); + + lines.addAll(0, Arrays.asList(header)); + + PrintWriter w = new PrintWriter(new FileWriter(u)); + + try { + for (String s : lines) { + w.println(s); + } + } finally { + w.close(); + } + } + } + } + } + } + } + + if (fail.length() != 0) { + System.out.println(fail); + throw new AssertionError(fail.toString()); + } + } +} diff --git a/src/test/java/io/reactivex/rxjava3/validators/InternalWrongNaming.java b/src/test/java/io/reactivex/rxjava3/validators/InternalWrongNaming.java new file mode 100644 index 0000000000..bbf86c4188 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/validators/InternalWrongNaming.java @@ -0,0 +1,196 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.validators; + +import java.io.*; +import java.util.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.testsupport.TestHelper; + +/** + * Adds license header to java files. + */ +public class InternalWrongNaming { + + static void checkInternalOperatorNaming(String baseClassName, String consumerClassName, String... ignore) throws Exception { + File f = TestHelper.findSource(baseClassName); + if (f == null) { + return; + } + + String rxdir = f.getParentFile().getParentFile().getAbsolutePath().replace('\\', '/'); + + if (!rxdir.endsWith("/")) { + rxdir += "/"; + } + + rxdir += "internal/operators/" + baseClassName.toLowerCase() + "/"; + + File[] list = new File(rxdir).listFiles(); + if (list != null && list.length != 0) { + + StringBuilder fail = new StringBuilder(); + + int count = 0; + + outer: + for (File g : list) { + for (String s : ignore) { + if (g.getName().equals(s + ".java")) { + continue outer; + } + } + List<String> lines = readFile(g); + + for (int i = 0; i < lines.size(); i++) { + String line = lines.get(i); + if (line.contains(consumerClassName)) { + + fail.append("java.lang.RuntimeException: " + g.getName() + " mentions " + consumerClassName) + .append("\r\n at io.reactivex.internal.operators.") + .append(baseClassName.toLowerCase()).append(".").append(g.getName().replace(".java", "")) + .append(".method(").append(g.getName()).append(":").append(i + 1).append(")\r\n\r\n"); + + count++; + } + } + } + + if (fail.length() != 0) { + System.out.println(fail); + + System.out.println(); + System.out.println("Total: " + count); + throw new AssertionError(fail.toString()); + } + + } + } + + static List<String> readFile(File u) throws Exception { + List<String> lines = new ArrayList<>(); + + BufferedReader in = new BufferedReader(new FileReader(u)); + try { + for (;;) { + String line = in.readLine(); + if (line == null) { + break; + } + + lines.add(line); + } + } finally { + in.close(); + } + return lines; + } + + @Test + public void observableNoSubscriber() throws Exception { + checkInternalOperatorNaming("Observable", "Subscriber", + "ObservableFromPublisher" + ); + } + + @Test + public void observableNoSubscribers() throws Exception { + checkInternalOperatorNaming("Observable", "subscribers"); + } + + @Test + public void observableNoSubscription() throws Exception { + checkInternalOperatorNaming("Observable", "Subscription", + "ObservableFromPublisher", "ObservableDelaySubscriptionOther"); + } + + @Test + public void observableNoPublisher() throws Exception { + checkInternalOperatorNaming("Observable", "Publisher", + "ObservableFromPublisher"); + } + + @Test + public void observableNoFlowable() throws Exception { + checkInternalOperatorNaming("Observable", "Flowable", "ObservableFromPublisher"); + } + + @Test + public void observableProducer() throws Exception { + checkInternalOperatorNaming("Observable", "Producer"); + } + + @Test + public void observableProducers() throws Exception { + checkInternalOperatorNaming("Observable", "producers"); + } + + @Test + public void flowableNoProducer() throws Exception { + checkInternalOperatorNaming("Flowable", "Producer"); + } + + @Test + public void flowableNoProducers() throws Exception { + checkInternalOperatorNaming("Flowable", "producers"); + } + + @Test + public void flowableNoUnsubscrib() throws Exception { + checkInternalOperatorNaming("Flowable", "unsubscrib"); + } + + @Test + public void observableNoUnsubscrib() throws Exception { + checkInternalOperatorNaming("Observable", "unsubscrib"); + } + + @Test + public void flowableNoObserver() throws Exception { + checkInternalOperatorNaming("Flowable", "Observer", + "FlowableFromObservable", + "FlowableLastSingle", + "FlowableAnySingle", + "FlowableAllSingle", + "FlowableToListSingle", + "FlowableCollectSingle", + "FlowableCountSingle", + "FlowableElementAtMaybe", + "FlowableElementAtSingle", + "FlowableElementAtMaybePublisher", + "FlowableElementAtSinglePublisher", + "FlowableFromCompletable", + "FlowableSingleSingle", + "FlowableSingleMaybe", + "FlowableLastMaybe", + "FlowableIgnoreElementsCompletable", + "FlowableReduceMaybe", + "FlowableReduceWithSingle", + "FlowableReduceSeedSingle", + "FlowableFlatMapCompletable", + "FlowableFlatMapCompletableCompletable", + "FlowableFlatMapSingle", + "FlowableFlatMapMaybe", + "FlowableSequenceEqualSingle", + "FlowableConcatWithSingle", + "FlowableConcatWithMaybe", + "FlowableConcatWithCompletable", + "FlowableMergeWithSingle", + "FlowableMergeWithMaybe", + "FlowableMergeWithCompletable" + ); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/validators/JavadocCodesAndLinks.java b/src/test/java/io/reactivex/rxjava3/validators/JavadocCodesAndLinks.java new file mode 100644 index 0000000000..698fdf7e83 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/validators/JavadocCodesAndLinks.java @@ -0,0 +1,436 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.validators; + +import java.io.File; +import java.nio.file.Files; +import java.util.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.testsupport.TestHelper; + +/** + * Scan the Javadocs of a source and check if mentions of other classes, + * interfaces and enums are using at-link and at-code wrapping for style. + * <p> + * The check ignores html tag content on a line, @see and @throws entries + * and <code></code> lines. + */ +public class JavadocCodesAndLinks { + + @Test + public void checkFlowable() throws Exception { + checkSource("Flowable", "io.reactivex.rxjava3.core"); + } + + @Test + public void checkCompletable() throws Exception { + checkSource("Completable", "io.reactivex.rxjava3.core"); + } + + @Test + public void checkSingle() throws Exception { + checkSource("Single", "io.reactivex.rxjava3.core"); + } + + @Test + public void checkMaybe() throws Exception { + checkSource("Maybe", "io.reactivex.rxjava3.core"); + } + + @Test + public void checkObservable() throws Exception { + checkSource("Observable", "io.reactivex.rxjava3.core"); + } + + @Test + public void checkParallelFlowable() throws Exception { + checkSource("ParallelFlowable", "io.reactivex.rxjava3.parallel"); + } + + @Test + public void checkCompositeDisposable() throws Exception { + checkSource("CompositeDisposable", "io.reactivex.rxjava3.disposables"); + } + + @Test + public void checkConnectableFlowable() throws Exception { + checkSource("ConnectableFlowable", "io.reactivex.rxjava3.flowables"); + } + + @Test + public void checkConnectableObservable() throws Exception { + checkSource("ConnectableObservable", "io.reactivex.rxjava3.observables"); + } + + @Test + public void checkSchedulers() throws Exception { + checkSource("Schedulers", "io.reactivex.rxjava3.schedulers"); + } + + static void checkSource(String baseClassName, String packageName) throws Exception { + File f = TestHelper.findSource(baseClassName, packageName); + if (f == null) { + return; + } + + StringBuilder errors = new StringBuilder(2048); + int errorCount = 0; + + List<String> lines = Files.readAllLines(f.toPath()); + + List<String> docs = new ArrayList<>(); + + // i = 1 skip the header javadoc + for (int i = 1; i < lines.size(); i++) { + + if (lines.get(i).trim().equals("/**")) { + docs.clear(); + + boolean skipCode = false; + for (int j = i + 1; j < lines.size(); j++) { + String line = lines.get(j).trim(); + if (line.contains("<code>")) { + skipCode = true; + } + if (line.equals("*/")) { + break; + } + if (!skipCode) { + // strip <a> </a> + line = stripTags(line); + if (line.startsWith("@see")) { + docs.add(""); + } + else if (line.startsWith("@throws") || line.startsWith("@param")) { + int space = line.indexOf(' '); + if (space < 0) { + docs.add(""); + } else { + space = line.indexOf(" ", space + 1); + if (space < 0) { + docs.add(""); + } else { + docs.add(line.substring(space + 1)); + } + } + } else { + docs.add(line); + } + } else { + docs.add(""); + } + if (line.contains("</code>")) { + skipCode = false; + } + } + + for (String name : NAMES) { + boolean isHostType = name.equals(baseClassName); + boolean isAlwaysCode = ALWAYS_CODE.contains(name); + String asLink = "{@link " + name + "}"; + String asCode = "{@code " + name + "}"; + boolean seenBefore = false; + + for (int j = 0; j < docs.size(); j++) { + String line = docs.get(j); + + int idxLink = line.indexOf(asLink); + if (idxLink >= 0 && !isHostType && !isAlwaysCode) { + int k = idxLink + asLink.length(); + for (;;) { + int jdxLink = line.indexOf(asLink, k); + if (jdxLink < 0) { + break; + } + if (jdxLink >= 0) { + errorCount++; + errors.append("The subsequent mention should be code: ") + .append("{@code ").append(name) + .append("}\r\n at ") + .append(packageName) + .append(".") + .append(baseClassName) + .append(".method(") + .append(baseClassName) + .append(".java:") + .append(i + 2 + j) + .append(")\r\n"); + } + k = jdxLink + asLink.length(); + } + } + if (seenBefore) { + if (idxLink >= 0 && !isHostType && !isAlwaysCode) { + errorCount++; + errors.append("The subsequent mention should be code: ") + .append("{@code ").append(name) + .append("}\r\n at ") + .append(packageName) + .append(".") + .append(baseClassName) + .append(".method(") + .append(baseClassName) + .append(".java:") + .append(i + 2 + j) + .append(")\r\n"); + } + } else { + int idxCode = line.indexOf(asCode); + + if (isHostType) { + if (idxLink >= 0) { + errorCount++; + errors.append("The host type mention should be code: ") + .append("{@code ").append(name) + .append("}\r\n at ") + .append(packageName) + .append(".") + .append(baseClassName) + .append(".method(") + .append(baseClassName) + .append(".java:") + .append(i + 2 + j) + .append(")\r\n"); + } + } else { + if ((idxLink < 0 && idxCode >= 0 && !isAlwaysCode) + || (idxLink >= 0 && idxCode >= 0 && idxCode < idxLink)) { + errorCount++; + if (isAlwaysCode) { + errors.append("The first mention should be code: ") + .append("{@code ") + ; + } else { + errors.append("The first mention should be link: ") + .append("{@link ") + ; + } + errors + .append(name) + .append("}\r\n at ") + .append(packageName) + .append(".") + .append(baseClassName) + .append(".method(") + .append(baseClassName) + .append(".java:") + .append(i + 2 + j) + .append(")\r\n"); + } + } + + seenBefore = idxLink >= 0 || idxCode >= 0; + } + + // strip out all existing {@code } and {@link } instances + String noCurly = removeCurlies(line); + + int k = 0; + + for (;;) { + int idx = noCurly.indexOf(name, k); + if (idx < 0) { + break; + } + k = idx + name.length(); + + if (isHostType) { + errorCount++; + errors.append("The host type mention should be code: ") + .append("{@code ").append(name) + .append("}\r\n at ") + .append(packageName) + .append(".") + .append(baseClassName) + .append(".method(") + .append(baseClassName) + .append(".java:") + .append(i + 2 + j) + .append(")\r\n"); + } + else if (!seenBefore) { + errorCount++; + if (isAlwaysCode) { + errors.append("The first mention should be code: ") + .append("{@code "); + } else { + errors.append("The first mention should be link: ") + .append("{@link "); + } + errors + .append(name) + .append("}\r\n at ") + .append(packageName) + .append(".") + .append(baseClassName) + .append(".method(") + .append(baseClassName) + .append(".java:") + .append(i + 2 + j) + .append(")\r\n"); + } else { + errorCount++; + errors.append("The subsequent mention should be code: ") + .append("{@code ").append(name) + .append("}\r\n at ") + .append(packageName) + .append(".") + .append(baseClassName) + .append(".method(") + .append(baseClassName) + .append(".java:") + .append(i + 2 + j) + .append(")\r\n"); + } + + seenBefore = true; + } + } + } + + i += docs.size(); + + if (errorCount >= ERROR_LIMIT) { + break; + } + } + } + + if (errorCount != 0) { + errors.insert(0, "Found " + (errorCount > ERROR_LIMIT ? ERROR_LIMIT + "+" : errorCount + "") + " cases\r\n"); + throw new AssertionError(errors.toString()); + } + } + + static String removeCurlies(String input) { + StringBuilder result = new StringBuilder(input.length()); + + boolean skip = false; + for (int i = 0; i < input.length(); i++) { + char c = input.charAt(i); + if (c == '{') { + skip = true; + } + if (!skip) { + result.append(c); + } + if (c == '}') { + skip = false; + } + } + + return result.toString(); + } + + static String stripTags(String input) { + StringBuilder result = new StringBuilder(input.length()); + result.append(input, input.length() > 1 ? 2 : 1, input.length()); + + clearTag(result, "<a ", "</a>"); + clearTag(result, "<b>", "</b>"); + clearTag(result, "<strong>", "</strong>"); + clearTag(result, "<em>", "</em>"); + clearTag(result, "<img ", ">"); + + return result.toString(); + } + + static void clearTag(StringBuilder builder, String startTag, String endTag) { + int k = 0; + for (;;) { + int j = builder.indexOf(startTag, k); + if (j < 0) { + break; + } + + int e = builder.indexOf(endTag, j); + if (e < 0) { + e = builder.length(); + } + + blankRange(builder, j, e); + + k = e + endTag.length(); + } + } + + static void blankRange(StringBuilder builder, int start, int end) { + for (int i = start; i < end; i++) { + int c = builder.charAt(i); + if (c != '\r' && c != '\n') { + builder.setCharAt(i, ' '); + } + } + } + + static final List<String> NAMES = Arrays.asList( + "Flowable", "Observable", "Maybe", "Single", "Completable", "ParallelFlowable", + + "Publisher", "ObservableSource", "MaybeSource", "SingleSource", "CompletableSource", + + "FlowableSubscriber", "Subscriber", "Observer", "MaybeObserver", "SingleObserver", "CompletableObserver", + + "FlowableOperator", "ObservableOperator", "MaybeOperator", "SingleOperator", "CompletableOperator", + + "FlowableOnSubscribe", "ObservableOnSubscribe", "MaybeOnSubscribe", "SingleOnSubscribe", "CompletableOnSubscribe", + + "FlowableTransformer", "ObservableTransformer", "MaybeTransformer", "SingleTransformer", "CompletableTransformer", "ParallelTransformer", + + "FlowableConverter", "ObservableConverter", "MaybeConverter", "SingleConverter", "CompletableConverter", + + "FlowableEmitter", "ObservableEmitter", "MaybeEmitter", "SingleEmitter", "CompletableEmitter", + + "Iterable", "Stream", + + "Function", "BiFunction", "Function3", "Function4", "Function5", "Function6", "Function7", "Function8", "Function9", + + "Action", "Runnable", "Disposable", "Subscription", "Consumer", "BiConsumer", "Future", + + "Supplier", "Callable", "TimeUnit", + + "BackpressureOverflowStrategy", "ParallelFailureHandling", + + "Exception", "Throwable", "NullPointerException", "IllegalStateException", "IllegalArgumentException", "MissingBackpressureException", "UndeliverableException", + "OutOfMemoryError", "StackOverflowError", "NoSuchElementException", "ClassCastException", "CompositeException", + "RuntimeException", "Error", "TimeoutException", "OnErrorNotImplementedException", + + "false", "true", "onNext", "onError", "onComplete", "onSuccess", "onSubscribe", "null", + + "ConnectableFlowable", "ConnectableObservable", "Subject", "FlowableProcessor", "Processor", "Scheduler", + + "Optional", "CompletionStage", "Collector", "Collectors", "Schedulers", "RxJavaPlugins", "CompletableFuture", + + "Object", "Integer", "Long", "Boolean", "LongConsumer", "BooleanSupplier", + + "GroupedFlowable", "GroupedObservable", "UnicastSubject", "UnicastProcessor", + + "Notification", "Comparable", "Comparator", "Collection", + + "SafeSubscriber", "SafeObserver", + + "List", "ArrayList", "HashMap", "HashSet", "CharSequence", + + "TestSubscriber", "TestObserver", "Class", + + "ThreadFactory", "Runnable", "Executor", "ExecutorService", "Executors", "RejectedExecutionException" + ); + + static final Set<String> ALWAYS_CODE = new HashSet<>(Arrays.asList( + "false", "true", "null", "onSuccess", "onNext", "onError", "onComplete", "onSubscribe" + )); + + static final int ERROR_LIMIT = 5000; +} diff --git a/src/test/java/io/reactivex/rxjava3/validators/JavadocFindUnescapedAngleBrackets.java b/src/test/java/io/reactivex/rxjava3/validators/JavadocFindUnescapedAngleBrackets.java new file mode 100644 index 0000000000..016e1b02a2 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/validators/JavadocFindUnescapedAngleBrackets.java @@ -0,0 +1,142 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.validators; + +import java.io.*; +import java.util.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class JavadocFindUnescapedAngleBrackets { + + @Test + public void find() throws Exception { + File base = TestHelper.findSource("Flowable"); + + if (base == null) { + return; + } + + base = base.getParentFile().getParentFile(); + + Queue<File[]> files = new ArrayDeque<>(); + + files.offer(base.listFiles()); + + StringBuilder b = new StringBuilder(); + int count = 0; + + while (!files.isEmpty()) { + for (File file : files.poll()) { + if (file.getName().endsWith(".java")) { + + String s = readFile(file); + + String fn = file.toString().substring(base.toString().length()); + fn = fn.replace("\\", "."); + fn = fn.replace("//", "."); + fn = fn.replace(".java", ""); + fn = "io.reactivex" + fn; + + int j = 0; + for (;;) { + int idx = s.indexOf("<code>", j); + if (idx < 0) { + break; + } + int jdx = s.indexOf("</code>", idx + 6); + + int k = idx + 6; + for (;;) { + int kdx = s.indexOf('>', k); + if (kdx < 0) { + break; + } + + if (kdx < jdx) { + b.append("at ") + .append(fn) + .append(".gt(") + .append(file.getName()).append(":") + .append(countLine(s, kdx)) + .append(")\r\n"); + count++; + } else { + break; + } + k = kdx + 1; + } + + k = idx + 6; + for (;;) { + int kdx = s.indexOf('<', k); + if (kdx < 0) { + break; + } + + if (kdx < jdx) { + b.append("at ") + .append(fn) + .append(".lt(") + .append(file.getName()).append(":") + .append(countLine(s, kdx)) + .append(")\r\n"); + count++; + } else { + break; + } + k = kdx + 1; + } + + j = jdx + 7; + } + } + } + } + + if (b.length() > 0) { + System.err.println("Should escape < and > in <code> blocks! " + count); + System.err.println(b); + throw new Exception("Should escape < and > in <code> blocks! " + count + "\r\n" + b); + } + } + + static int countLine(String s, int kdx) { + int c = 1; + for (int i = kdx; i >= 0; i--) { + if (s.charAt(i) == '\n') { + c++; + } + } + return c; + } + + static String readFile(File f) throws IOException { + StringBuilder b = new StringBuilder((int)f.length()); + BufferedReader in = new BufferedReader(new FileReader(f)); + try { + String line = null; + + while ((line = in.readLine()) != null) { + b.append(line).append("\n"); + } + + } finally { + in.close(); + } + return b.toString(); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/validators/JavadocForAnnotations.java b/src/test/java/io/reactivex/rxjava3/validators/JavadocForAnnotations.java new file mode 100644 index 0000000000..1c683783d6 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/validators/JavadocForAnnotations.java @@ -0,0 +1,288 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.validators; + +import static org.junit.Assert.fail; + +import java.io.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.testsupport.TestHelper; + +/** + * Checks the source code of the base reactive types and locates missing + * mention of {@code Backpressure:} and {@code Scheduler:} of methods. + */ +public class JavadocForAnnotations { + + static void checkSource(String baseClassName, boolean scheduler) throws Exception { + File f = TestHelper.findSource(baseClassName); + if (f == null) { + return; + } + + StringBuilder b = readFile(f); + + StringBuilder e = new StringBuilder(); + + if (scheduler) { + scanFor(b, "@SchedulerSupport", "Scheduler:", e, baseClassName); + } else { + scanFor(b, "@BackpressureSupport", "Backpressure:", e, baseClassName); + } + + if (e.length() != 0) { + System.out.println(e); + + fail(e.toString()); + } + } + + public static StringBuilder readFile(File f) throws Exception { + StringBuilder b = new StringBuilder(); + + BufferedReader in = new BufferedReader(new FileReader(f)); + try { + for (;;) { + String line = in.readLine(); + + if (line == null) { + break; + } + + b.append(line).append('\n'); + } + } finally { + in.close(); + } + + return b; + } + + static final void scanFor(StringBuilder sourceCode, String annotation, String inDoc, + StringBuilder e, String baseClassName) { + int index = 0; + for (;;) { + int idx = sourceCode.indexOf(annotation, index); + + if (idx < 0) { + break; + } + + int j = sourceCode.lastIndexOf("/**", idx); + + // see if the last /** is not before the index (last time the annotation was found + // indicating an uncommented method like subscribe() + if (j > index) { + int k = sourceCode.indexOf(inDoc, j); + + if (k < 0 || k > idx) { + // when printed on the console, IDEs will create a clickable link to help navigate to the offending point + e.append("java.lang.RuntimeException: missing ").append(inDoc).append(" section\r\n") + ; + int lc = lineNumber(sourceCode, idx); + + e.append(" at io.reactivex.rxjava3.core.").append(baseClassName) + .append(" (").append(baseClassName).append(".java:") + .append(lc).append(")").append("\r\n\r\n"); + } + } + + index = idx + annotation.length(); + } + } + + static final void scanForBadMethod(StringBuilder sourceCode, String annotation, String inDoc, + StringBuilder e, String baseClassName) { + int index = 0; + for (;;) { + int idx = sourceCode.indexOf(annotation, index); + + if (idx < 0) { + break; + } + + int j = sourceCode.lastIndexOf("/**", idx); + + // see if the last /** is not before the index (last time the annotation was found + // indicating an uncommented method like subscribe() + if (j > index) { + int k = sourceCode.indexOf(inDoc, j); + + if (k >= 0 && k <= idx) { + + int ll = sourceCode.indexOf("You specify", k); + if (ll < 0) { + ll = sourceCode.indexOf("you specify", k); + } + int lm = sourceCode.indexOf("This operator", k); + if (lm < 0) { + lm = sourceCode.indexOf("this operator", k); + } + if ((ll < 0 || ll > idx) && (lm < 0 || lm > idx)) { + + int n = sourceCode.indexOf("{@code ", k); + int endDD = sourceCode.indexOf("</dd>", k); + // make sure the {@code is within the dt/dd section + + if (n < idx && n < endDD) { + int m = sourceCode.indexOf("}", n); + + if (m < idx) { + String mname = sourceCode.substring(n + 7, m); + + if (!"Scheduler".equals(mname)) { + + int q = sourceCode.indexOf("@SuppressWarnings({", idx); + + int o = sourceCode.indexOf("{", idx); + + if (q + 18 == o) { + o = sourceCode.indexOf("{", q + 20); + } + + if (o >= 0) { + + int p = sourceCode.indexOf(" " + mname + "(", idx); + + if (p < 0 || p > o) { + // when printed on the console, IDEs will create a clickable link to help navigate to the offending point + e.append("java.lang.RuntimeException: wrong method name in description of ").append(inDoc).append(" '").append(mname).append("'\r\n") + ; + int lc = lineNumber(sourceCode, idx); + + e.append(" at io.reactivex.rxjava3.core.").append(baseClassName) + .append(".method(").append(baseClassName).append(".java:") + .append(lc).append(")").append("\r\n"); + } + } + } + } + + } + + } + } + } + + index = idx + annotation.length(); + } + } + + static void checkSchedulerBadMethod(String baseClassName) throws Exception { + File f = TestHelper.findSource(baseClassName); + if (f == null) { + return; + } + + StringBuilder b = readFile(f); + + StringBuilder e = new StringBuilder(); + + scanForBadMethod(b, "@SchedulerSupport", "Scheduler:", e, baseClassName); + + if (e.length() != 0) { + System.out.println(e); + + fail(e.toString()); + } + } + + public static int lineNumber(StringBuilder s, int index) { + int cnt = 1; + for (int i = 0; i < index; i++) { + if (s.charAt(i) == '\n') { + cnt++; + } + } + return cnt; + } + + @Test + public void checkFlowableBackpressure() throws Exception { + checkSource(Flowable.class.getSimpleName(), false); + } + + @Test + public void checkFlowableScheduler() throws Exception { + checkSource(Flowable.class.getSimpleName(), true); + } + + @Test + public void checkObservableBackpressure() throws Exception { + checkSource(Observable.class.getSimpleName(), false); + } + + @Test + public void checkObservableScheduler() throws Exception { + checkSource(Observable.class.getSimpleName(), true); + } + + @Test + public void checkSingleBackpressure() throws Exception { + checkSource(Single.class.getSimpleName(), false); + } + + @Test + public void checkSingleScheduler() throws Exception { + checkSource(Single.class.getSimpleName(), true); + } + + @Test + public void checkCompletableBackpressure() throws Exception { + checkSource(Completable.class.getSimpleName(), false); + } + + @Test + public void checkCompletableScheduler() throws Exception { + checkSource(Completable.class.getSimpleName(), true); + } + + @Test + public void checkMaybeBackpressure() throws Exception { + checkSource(Maybe.class.getSimpleName(), false); + } + + @Test + public void checkMaybeScheduler() throws Exception { + checkSource(Maybe.class.getSimpleName(), true); + } + + @Test + public void checkFlowableSchedulerDoc() throws Exception { + checkSchedulerBadMethod(Flowable.class.getSimpleName()); + } + + @Test + public void checkObservableSchedulerDoc() throws Exception { + checkSchedulerBadMethod(Observable.class.getSimpleName()); + } + + @Test + public void checkSingleSchedulerDoc() throws Exception { + checkSchedulerBadMethod(Single.class.getSimpleName()); + } + + @Test + public void checkCompletableSchedulerDoc() throws Exception { + checkSchedulerBadMethod(Completable.class.getSimpleName()); + } + + @Test + public void checkMaybeSchedulerDoc() throws Exception { + checkSchedulerBadMethod(Maybe.class.getSimpleName()); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/validators/JavadocWording.java b/src/test/java/io/reactivex/rxjava3/validators/JavadocWording.java new file mode 100644 index 0000000000..3ab41589a3 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/validators/JavadocWording.java @@ -0,0 +1,1174 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.validators; + +import static org.junit.Assert.*; + +import java.util.List; +import java.util.regex.Pattern; + +import org.junit.Test; + +import io.reactivex.rxjava3.testsupport.TestHelper; +import io.reactivex.rxjava3.validators.BaseTypeParser.RxMethod; + +/** + * Check if the method wording is consistent with the target base type. + */ +public class JavadocWording { + + public static int lineNumber(CharSequence s, int index) { + int cnt = 1; + for (int i = 0; i < index; i++) { + if (s.charAt(i) == '\n') { + cnt++; + } + } + return cnt; + } + + @Test + public void maybeDocRefersToMaybeTypes() throws Exception { + List<RxMethod> list = BaseTypeParser.parse(TestHelper.findSource("Maybe"), "Maybe"); + + assertFalse(list.isEmpty()); + + StringBuilder e = new StringBuilder(); + + for (RxMethod m : list) { + int jdx; + if (m.javadoc != null) { + jdx = 0; + for (;;) { + int idx = m.javadoc.indexOf("onNext", jdx); + if (idx >= 0) { + if (!m.signature.contains("Publisher") + && !m.signature.contains("Flowable") + && !m.signature.contains("Observable") + && !m.signature.contains("ObservableSource")) { + e.append("java.lang.RuntimeException: Maybe doc mentions onNext but no Flowable/Observable in signature\r\n at io.reactivex.rxjava3.core.") + .append("Maybe.method(Maybe.java:").append(m.javadocLine + lineNumber(m.javadoc, idx) - 1).append(")\r\n\r\n"); + } + + jdx = idx + 6; + } else { + break; + } + } + jdx = 0; + for (;;) { + int idx = m.javadoc.indexOf("Subscriber", jdx); + if (idx >= 0) { + if (!m.signature.contains("Publisher") + && !m.signature.contains("Flowable") + && !m.signature.contains("TestSubscriber") + ) { + e.append("java.lang.RuntimeException: Maybe doc mentions Subscriber but not using Flowable\r\n at io.reactivex.rxjava3.core.") + .append("Maybe.method(Maybe.java:").append(m.javadocLine + lineNumber(m.javadoc, idx) - 1).append(")\r\n\r\n"); + } + + jdx = idx + 6; + } else { + break; + } + } + jdx = 0; + for (;;) { + int idx = m.javadoc.indexOf(" Subscription", jdx); + if (idx >= 0) { + if (!m.signature.contains("Publisher") + && !m.signature.contains("Flowable") + ) { + e.append("java.lang.RuntimeException: Maybe doc mentions Subscription but not using Flowable\r\n at io.reactivex.rxjava3.core.") + .append("Maybe.method(Maybe.java:").append(m.javadocLine + lineNumber(m.javadoc, idx) - 1).append(")\r\n\r\n"); + } + + jdx = idx + 6; + } else { + break; + } + } + jdx = 0; + for (;;) { + int idx = m.javadoc.indexOf("Observer", jdx); + if (idx >= 0) { + if (!m.signature.contains("ObservableSource") + && !m.signature.contains("Observable") + && !m.signature.contains("TestObserver")) { + + if (idx < 5 || !m.javadoc.substring(idx - 5, idx + 8).equals("MaybeObserver")) { + e.append("java.lang.RuntimeException: Maybe doc mentions Observer but not using Observable\r\n at io.reactivex.rxjava3.core.") + .append("Maybe.method(Maybe.java:").append(m.javadocLine + lineNumber(m.javadoc, idx) - 1).append(")\r\n\r\n"); + } + } + + jdx = idx + 6; + } else { + break; + } + } + jdx = 0; + for (;;) { + int idx = m.javadoc.indexOf("Publisher", jdx); + if (idx >= 0) { + if (!m.signature.contains("Publisher")) { + if (idx == 0 || !m.javadoc.substring(idx - 1, idx + 9).equals("(Publisher")) { + e.append("java.lang.RuntimeException: Maybe doc mentions Publisher but not in the signature\r\n at io.reactivex.rxjava3.core.") + .append("Maybe.method(Maybe.java:").append(m.javadocLine + lineNumber(m.javadoc, idx) - 1).append(")\r\n\r\n"); + } + } + + jdx = idx + 6; + } else { + break; + } + } + jdx = 0; + for (;;) { + int idx = m.javadoc.indexOf("Flowable", jdx); + if (idx >= 0) { + if (!m.signature.contains("Flowable")) { + Pattern p = Pattern.compile("@see\\s+#[A-Za-z0-9 _.,()]*Flowable"); + if (!p.matcher(m.javadoc).find()) { + e.append("java.lang.RuntimeException: Maybe doc mentions Flowable but not in the signature\r\n at io.reactivex.rxjava3.core.") + .append("Maybe.method(Maybe.java:").append(m.javadocLine + lineNumber(m.javadoc, idx) - 1).append(")\r\n\r\n"); + } + } + jdx = idx + 6; + } else { + break; + } + } + jdx = 0; + for (;;) { + int idx = m.javadoc.indexOf("Single", jdx); + if (idx >= 0 && m.javadoc.indexOf("Single#", jdx) != idx) { + int j = m.javadoc.indexOf("#toSingle", jdx); + int k = m.javadoc.indexOf("{@code Single", jdx); + if (!m.signature.contains("Single") && (j + 3 != idx && k + 7 != idx)) { + e.append("java.lang.RuntimeException: Maybe doc mentions Single but not in the signature\r\n at io.reactivex.rxjava3.core.") + .append("Maybe.method(Maybe.java:").append(m.javadocLine + lineNumber(m.javadoc, idx) - 1).append(")\r\n\r\n"); + } + jdx = idx + 6; + } else { + break; + } + } + jdx = 0; + for (;;) { + int idx = m.javadoc.indexOf("SingleSource", jdx); + if (idx >= 0) { + if (!m.signature.contains("SingleSource")) { + e.append("java.lang.RuntimeException: Maybe doc mentions SingleSource but not in the signature\r\n at io.reactivex.rxjava3.core.") + .append("Maybe.method(Maybe.java:").append(m.javadocLine + lineNumber(m.javadoc, idx) - 1).append(")\r\n\r\n"); + } + jdx = idx + 6; + } else { + break; + } + } + jdx = 0; + for (;;) { + int idx = m.javadoc.indexOf("Observable", jdx); + if (idx >= 0) { + if (!m.signature.contains("Observable")) { + Pattern p = Pattern.compile("@see\\s+#[A-Za-z0-9 _.,()]*Observable"); + if (!p.matcher(m.javadoc).find()) { + e.append("java.lang.RuntimeException: Maybe doc mentions Observable but not in the signature\r\n at io.reactivex.rxjava3.core.") + .append("Maybe.method(Maybe.java:").append(m.javadocLine + lineNumber(m.javadoc, idx) - 1).append(")\r\n\r\n"); + } + } + jdx = idx + 6; + } else { + break; + } + } + jdx = 0; + for (;;) { + int idx = m.javadoc.indexOf("ObservableSource", jdx); + if (idx >= 0) { + if (!m.signature.contains("ObservableSource")) { + e.append("java.lang.RuntimeException: Maybe doc mentions ObservableSource but not in the signature\r\n at io.reactivex.rxjava3.core.") + .append("Maybe.method(Maybe.java:").append(m.javadocLine + lineNumber(m.javadoc, idx) - 1).append(")\r\n\r\n"); + } + jdx = idx + 6; + } else { + break; + } + } + + checkAtReturnAndSignatureMatch("Maybe", m, e, "Flowable", "Observable", "Maybe", "Single", "Completable", "Disposable", "Iterable", "Stream", "Future", "CompletionStage"); + + aOrAn(e, m, "Maybe"); + missingClosingDD(e, m, "Maybe", "io.reactivex.rxjava3.core"); + backpressureMentionedWithoutAnnotation(e, m, "Maybe"); + } + } + + if (e.length() != 0) { + System.out.println(e); + + fail(e.toString()); + } + } + + @Test + public void flowableDocRefersToFlowableTypes() throws Exception { + List<RxMethod> list = BaseTypeParser.parse(TestHelper.findSource("Flowable"), "Flowable"); + + assertFalse(list.isEmpty()); + + StringBuilder e = new StringBuilder(); + + for (RxMethod m : list) { + int jdx; + if (m.javadoc != null) { + jdx = 0; + for (;;) { + int idx = m.javadoc.indexOf("onSuccess", jdx); + if (idx >= 0) { + if (!m.signature.contains("Maybe") + && !m.signature.contains("MaybeSource") + && !m.signature.contains("Single") + && !m.signature.contains("SingleSource")) { + e.append("java.lang.RuntimeException: Flowable doc mentions onSuccess\r\n at io.reactivex.rxjava3.core.") + .append("Flowable.method(Flowable.java:").append(m.javadocLine + lineNumber(m.javadoc, idx) - 1).append(")\r\n\r\n"); + } + + jdx = idx + 6; + } else { + break; + } + } + jdx = 0; + for (;;) { + int idx = m.javadoc.indexOf(" Observer", jdx); + if (idx >= 0) { + if (!m.signature.contains("ObservableSource") + && !m.signature.contains("Observable")) { + e.append("java.lang.RuntimeException: Flowable doc mentions Observer but not using Observable\r\n at io.reactivex.rxjava3.core.") + .append("Flowable.method(Flowable.java:").append(m.javadocLine + lineNumber(m.javadoc, idx) - 1).append(")\r\n\r\n"); + } + + jdx = idx + 6; + } else { + break; + } + } + jdx = 0; + for (;;) { + int idx = m.javadoc.indexOf(" SingleObserver", jdx); + if (idx >= 0) { + if (!m.signature.contains("SingleSource") + && !m.signature.contains("Single")) { + e.append("java.lang.RuntimeException: Flowable doc mentions SingleObserver but not using Single\r\n at io.reactivex.rxjava3.core.") + .append("Flowable.method(Flowable.java:").append(m.javadocLine + lineNumber(m.javadoc, idx) - 1).append(")\r\n\r\n"); + } + + jdx = idx + 6; + } else { + break; + } + } + jdx = 0; + for (;;) { + int idx = m.javadoc.indexOf(" MaybeObserver", jdx); + if (idx >= 0) { + if (!m.signature.contains("MaybeSource") + && !m.signature.contains("Maybe")) { + e.append("java.lang.RuntimeException: Flowable doc mentions MaybeObserver but not using Maybe\r\n at io.reactivex.rxjava3.core.") + .append("Flowable.method(Flowable.java:").append(m.javadocLine + lineNumber(m.javadoc, idx) - 1).append(")\r\n\r\n"); + } + + jdx = idx + 6; + } else { + break; + } + } + jdx = 0; + for (;;) { + int idx = m.javadoc.indexOf(" Disposable", jdx); + if (idx >= 0) { + if (!m.signature.contains("Observable") + && !m.signature.contains("ObservableSource") + && !m.signature.contains("Single") + && !m.signature.contains("SingleSource") + && !m.signature.contains("Completable") + && !m.signature.contains("CompletableSource") + && !m.signature.contains("Maybe") + && !m.signature.contains("MaybeSource") + && !m.signature.contains("Disposable") + && !m.signature.contains("void subscribe") + ) { + CharSequence subSequence = m.javadoc.subSequence(idx - 6, idx + 11); + if (idx < 6 || !subSequence.equals("{@link Disposable")) { + e.append("java.lang.RuntimeException: Flowable doc mentions Disposable but not using Flowable\r\n at io.reactivex.rxjava3.core.") + .append("Flowable.method(Flowable.java:").append(m.javadocLine + lineNumber(m.javadoc, idx) - 1).append(")\r\n\r\n"); + } + } + + jdx = idx + 6; + } else { + break; + } + } + jdx = 0; + for (;;) { + int idx = m.javadoc.indexOf("Observable", jdx); + if (idx >= 0) { + if (!m.signature.contains("Observable")) { + e.append("java.lang.RuntimeException: Flowable doc mentions Observable but not in the signature\r\n at io.reactivex.rxjava3.core.") + .append("Flowable.method(Flowable.java:").append(m.javadocLine + lineNumber(m.javadoc, idx) - 1).append(")\r\n\r\n"); + } + + jdx = idx + 6; + } else { + break; + } + } + jdx = 0; + for (;;) { + int idx = m.javadoc.indexOf("ObservableSource", jdx); + if (idx >= 0) { + if (!m.signature.contains("ObservableSource")) { + e.append("java.lang.RuntimeException: Flowable doc mentions ObservableSource but not in the signature\r\n at io.reactivex.rxjava3.core.") + .append("Flowable.method(Flowable.java:").append(m.javadocLine + lineNumber(m.javadoc, idx) - 1).append(")\r\n\r\n"); + } + jdx = idx + 6; + } else { + break; + } + } + + checkAtReturnAndSignatureMatch("Flowable", m, e, "Flowable", "Observable", "Maybe", "Single", "Completable", "ConnectableFlowable", "ParallelFlowable", "Disposable", "Iterable", "Stream", "Future", "CompletionStage"); + + aOrAn(e, m, "Flowable"); + missingClosingDD(e, m, "Flowable", "io.reactivex.rxjava3.core"); + backpressureMentionedWithoutAnnotation(e, m, "Flowable"); + } + } + + if (e.length() != 0) { + System.out.println(e); + + fail(e.toString()); + } + } + + @Test + public void parallelFlowableDocRefersToCorrectTypes() throws Exception { + List<RxMethod> list = BaseTypeParser.parse(TestHelper.findSource("ParallelFlowable", "io.reactivex.rxjava3.parallel"), "ParallelFlowable"); + + assertFalse(list.isEmpty()); + + StringBuilder e = new StringBuilder(); + + for (RxMethod m : list) { + int jdx; + if (m.javadoc != null) { + jdx = 0; + for (;;) { + int idx = m.javadoc.indexOf("onSuccess", jdx); + if (idx >= 0) { + if (!m.signature.contains("Maybe") + && !m.signature.contains("MaybeSource") + && !m.signature.contains("Single") + && !m.signature.contains("SingleSource")) { + e.append("java.lang.RuntimeException: Flowable doc mentions onSuccess\r\n at io.reactivex.rxjava3.core.") + .append("Flowable.method(Flowable.java:").append(m.javadocLine + lineNumber(m.javadoc, idx) - 1).append(")\r\n\r\n"); + } + + jdx = idx + 6; + } else { + break; + } + } + jdx = 0; + for (;;) { + int idx = m.javadoc.indexOf(" Observer", jdx); + if (idx >= 0) { + if (!m.signature.contains("ObservableSource") + && !m.signature.contains("Observable")) { + e.append("java.lang.RuntimeException: Flowable doc mentions Observer but not using Observable\r\n at io.reactivex.rxjava3.core.") + .append("Flowable.method(Flowable.java:").append(m.javadocLine + lineNumber(m.javadoc, idx) - 1).append(")\r\n\r\n"); + } + + jdx = idx + 6; + } else { + break; + } + } + jdx = 0; + for (;;) { + int idx = m.javadoc.indexOf(" SingleObserver", jdx); + if (idx >= 0) { + if (!m.signature.contains("SingleSource") + && !m.signature.contains("Single")) { + e.append("java.lang.RuntimeException: Flowable doc mentions SingleObserver but not using Single\r\n at io.reactivex.rxjava3.core.") + .append("Flowable.method(Flowable.java:").append(m.javadocLine + lineNumber(m.javadoc, idx) - 1).append(")\r\n\r\n"); + } + + jdx = idx + 6; + } else { + break; + } + } + jdx = 0; + for (;;) { + int idx = m.javadoc.indexOf(" MaybeObserver", jdx); + if (idx >= 0) { + if (!m.signature.contains("MaybeSource") + && !m.signature.contains("Maybe")) { + e.append("java.lang.RuntimeException: Flowable doc mentions MaybeObserver but not using Maybe\r\n at io.reactivex.rxjava3.core.") + .append("Flowable.method(Flowable.java:").append(m.javadocLine + lineNumber(m.javadoc, idx) - 1).append(")\r\n\r\n"); + } + + jdx = idx + 6; + } else { + break; + } + } + jdx = 0; + for (;;) { + int idx = m.javadoc.indexOf(" Disposable", jdx); + if (idx >= 0) { + if (!m.signature.contains("Observable") + && !m.signature.contains("ObservableSource") + && !m.signature.contains("Single") + && !m.signature.contains("SingleSource") + && !m.signature.contains("Completable") + && !m.signature.contains("CompletableSource") + && !m.signature.contains("Maybe") + && !m.signature.contains("MaybeSource") + && !m.signature.contains("Disposable") + ) { + CharSequence subSequence = m.javadoc.subSequence(idx - 6, idx + 11); + if (idx < 6 || !subSequence.equals("{@link Disposable")) { + e.append("java.lang.RuntimeException: Flowable doc mentions Disposable but not using Flowable\r\n at io.reactivex.rxjava3.core.") + .append("Flowable.method(Flowable.java:").append(m.javadocLine + lineNumber(m.javadoc, idx) - 1).append(")\r\n\r\n"); + } + } + + jdx = idx + 6; + } else { + break; + } + } + jdx = 0; + for (;;) { + int idx = m.javadoc.indexOf("Observable", jdx); + if (idx >= 0) { + if (!m.signature.contains("Observable")) { + e.append("java.lang.RuntimeException: Flowable doc mentions Observable but not in the signature\r\n at io.reactivex.rxjava3.core.") + .append("Flowable.method(Flowable.java:").append(m.javadocLine + lineNumber(m.javadoc, idx) - 1).append(")\r\n\r\n"); + } + + jdx = idx + 6; + } else { + break; + } + } + jdx = 0; + for (;;) { + int idx = m.javadoc.indexOf("ObservableSource", jdx); + if (idx >= 0) { + if (!m.signature.contains("ObservableSource")) { + e.append("java.lang.RuntimeException: Flowable doc mentions ObservableSource but not in the signature\r\n at io.reactivex.rxjava3.core.") + .append("Flowable.method(Flowable.java:").append(m.javadocLine + lineNumber(m.javadoc, idx) - 1).append(")\r\n\r\n"); + } + jdx = idx + 6; + } else { + break; + } + } + + checkAtReturnAndSignatureMatch("ParallelFlowable", m, e, "Flowable", "Observable", "Maybe", "Single", "Completable", "ConnectableFlowable", "ParallelFlowable", "Disposable", "Iterable", "Stream", "Future", "CompletionStage"); + + aOrAn(e, m, "ParallelFlowable"); + missingClosingDD(e, m, "ParallelFlowable", "io.reactivex.rxjava3.parallel"); + backpressureMentionedWithoutAnnotation(e, m, "ParallelFlowable"); + } + } + + if (e.length() != 0) { + System.out.println(e); + + fail(e.toString()); + } + } + + @Test + public void observableDocRefersToObservableTypes() throws Exception { + List<RxMethod> list = BaseTypeParser.parse(TestHelper.findSource("Observable"), "Observable"); + + assertFalse(list.isEmpty()); + + StringBuilder e = new StringBuilder(); + + for (RxMethod m : list) { + int jdx; + if (m.javadoc != null) { + jdx = 0; + for (;;) { + int idx = m.javadoc.indexOf("onSuccess", jdx); + if (idx >= 0) { + if (!m.signature.contains("Maybe") + && !m.signature.contains("MaybeSource") + && !m.signature.contains("Single") + && !m.signature.contains("SingleSource")) { + e.append("java.lang.RuntimeException: Observable doc mentions onSuccess\r\n at io.reactivex.rxjava3.core.") + .append("Observable.method(Observable.java:").append(m.javadocLine + lineNumber(m.javadoc, idx) - 1).append(")\r\n\r\n"); + } + + jdx = idx + 6; + } else { + break; + } + } + jdx = 0; + for (;;) { + int idx = m.javadoc.indexOf(" Subscription", jdx); + if (idx >= 0) { + if (!m.signature.contains("Flowable") + && !m.signature.contains("Publisher") + ) { + e.append("java.lang.RuntimeException: Observable doc mentions Subscription but not using Flowable\r\n at io.reactivex.rxjava3.core.") + .append("Observable.method(Observable.java:").append(m.javadocLine + lineNumber(m.javadoc, idx) - 1).append(")\r\n\r\n"); + } + + jdx = idx + 6; + } else { + break; + } + } + jdx = 0; + for (;;) { + int idx = m.javadoc.indexOf("Flowable", jdx); + if (idx >= 0) { + if (!m.signature.contains("Flowable")) { + if (idx < 6 || !m.javadoc.substring(idx - 6, idx + 8).equals("@link Flowable")) { + e.append("java.lang.RuntimeException: Observable doc mentions Flowable but not in the signature\r\n at io.reactivex.rxjava3.core.") + .append("Observable.method(Observable.java:").append(m.javadocLine + lineNumber(m.javadoc, idx) - 1).append(")\r\n\r\n"); + } + } + + jdx = idx + 6; + } else { + break; + } + } + jdx = 0; + for (;;) { + int idx = m.javadoc.indexOf("Publisher", jdx); + if (idx >= 0) { + if (!m.signature.contains("Publisher")) { + e.append("java.lang.RuntimeException: Observable doc mentions Publisher but not in the signature\r\n at io.reactivex.rxjava3.core.") + .append("Observable.method(Observable.java:").append(m.javadocLine + lineNumber(m.javadoc, idx) - 1).append(")\r\n\r\n"); + } + jdx = idx + 6; + } else { + break; + } + } + jdx = 0; + for (;;) { + int idx = m.javadoc.indexOf("Subscriber", jdx); + if (idx >= 0) { + if (!m.signature.contains("Publisher") + && !m.signature.contains("Flowable")) { + e.append("java.lang.RuntimeException: Observable doc mentions Subscriber but not using Flowable\r\n at io.reactivex.rxjava3.core.") + .append("Observable.method(Observable.java:").append(m.javadocLine + lineNumber(m.javadoc, idx) - 1).append(")\r\n\r\n"); + } + + jdx = idx + 6; + } else { + break; + } + } + checkAtReturnAndSignatureMatch("Observable", m, e, "Flowable", "Observable", "Maybe", "Single", "Completable", "ConnectableObservable", "Disposable", "Iterable", "Stream", "Future", "CompletionStage"); + + aOrAn(e, m, "Observable"); + missingClosingDD(e, m, "Observable", "io.reactivex.rxjava3.core"); + backpressureMentionedWithoutAnnotation(e, m, "Observable"); + } + } + + if (e.length() != 0) { + System.out.println(e); + + fail(e.toString()); + } + } + + @Test + public void singleDocRefersToSingleTypes() throws Exception { + List<RxMethod> list = BaseTypeParser.parse(TestHelper.findSource("Single"), "Single"); + + assertFalse(list.isEmpty()); + + StringBuilder e = new StringBuilder(); + + for (RxMethod m : list) { + int jdx; + if (m.javadoc != null) { + jdx = 0; + for (;;) { + int idx = m.javadoc.indexOf("onNext", jdx); + if (idx >= 0) { + if (!m.signature.contains("Publisher") + && !m.signature.contains("Flowable") + && !m.signature.contains("Observable") + && !m.signature.contains("ObservableSource")) { + e.append("java.lang.RuntimeException: Single doc mentions onNext but no Flowable/Observable in signature\r\n at io.reactivex.rxjava3.core.") + .append("Single.method(Single.java:").append(m.javadocLine + lineNumber(m.javadoc, idx) - 1).append(")\r\n\r\n"); + } + + jdx = idx + 6; + } else { + break; + } + } + jdx = 0; + for (;;) { + int idx = m.javadoc.indexOf("Subscriber", jdx); + if (idx >= 0) { + if (!m.signature.contains("Publisher") + && !m.signature.contains("Flowable") + && !m.signature.contains("TestSubscriber")) { + e.append("java.lang.RuntimeException: Single doc mentions Subscriber but not using Flowable\r\n at io.reactivex.rxjava3.core.") + .append("Single.method(Single.java:").append(m.javadocLine + lineNumber(m.javadoc, idx) - 1).append(")\r\n\r\n"); + } + + jdx = idx + 6; + } else { + break; + } + } + jdx = 0; + for (;;) { + int idx = m.javadoc.indexOf(" Subscription", jdx); + if (idx >= 0) { + if (!m.signature.contains("Flowable") + && !m.signature.contains("Publisher") + ) { + e.append("java.lang.RuntimeException: Single doc mentions Subscription but not using Flowable\r\n at io.reactivex.rxjava3.core.") + .append("Single.method(Single.java:").append(m.javadocLine + lineNumber(m.javadoc, idx) - 1).append(")\r\n\r\n"); + } + + jdx = idx + 6; + } else { + break; + } + } + jdx = 0; + for (;;) { + int idx = m.javadoc.indexOf("Observer", jdx); + if (idx >= 0) { + if (!m.signature.contains("ObservableSource") + && !m.signature.contains("Observable") + && !m.signature.contains("TestObserver")) { + + if (idx < 6 || !m.javadoc.substring(idx - 6, idx + 8).equals("SingleObserver")) { + e.append("java.lang.RuntimeException: Single doc mentions Observer but not using Observable\r\n at io.reactivex.rxjava3.core.") + .append("Single.method(Single.java:").append(m.javadocLine + lineNumber(m.javadoc, idx) - 1).append(")\r\n\r\n"); + } + } + + jdx = idx + 6; + } else { + break; + } + } + jdx = 0; + for (;;) { + int idx = m.javadoc.indexOf("Publisher", jdx); + if (idx >= 0) { + if (!m.signature.contains("Publisher")) { + if (idx == 0 || !m.javadoc.substring(idx - 1, idx + 9).equals("(Publisher")) { + e.append("java.lang.RuntimeException: Single doc mentions Publisher but not in the signature\r\n at io.reactivex.rxjava3.core.") + .append("Single.method(Single.java:").append(m.javadocLine + lineNumber(m.javadoc, idx) - 1).append(")\r\n\r\n"); + } + } + + jdx = idx + 6; + } else { + break; + } + } + jdx = 0; + for (;;) { + int idx = m.javadoc.indexOf(" Flowable", jdx); + if (idx >= 0) { + if (!m.signature.contains("Flowable")) { + e.append("java.lang.RuntimeException: Single doc mentions Flowable but not in the signature\r\n at io.reactivex.rxjava3.core.") + .append("Single.method(Single.java:").append(m.javadocLine + lineNumber(m.javadoc, idx) - 1).append(")\r\n\r\n"); + } + jdx = idx + 6; + } else { + break; + } + } + jdx = 0; + for (;;) { + int idx = m.javadoc.indexOf(" Maybe", jdx); + if (idx >= 0) { + if (!m.signature.contains("Maybe")) { + e.append("java.lang.RuntimeException: Single doc mentions Maybe but not in the signature\r\n at io.reactivex.rxjava3.core.") + .append("Single.method(Single.java:").append(m.javadocLine + lineNumber(m.javadoc, idx) - 1).append(")\r\n\r\n"); + } + jdx = idx + 6; + } else { + break; + } + } + jdx = 0; + for (;;) { + int idx = m.javadoc.indexOf(" MaybeSource", jdx); + if (idx >= 0) { + if (!m.signature.contains("MaybeSource")) { + e.append("java.lang.RuntimeException: Single doc mentions SingleSource but not in the signature\r\n at io.reactivex.rxjava3.core.") + .append("Maybe.method(Maybe.java:").append(m.javadocLine + lineNumber(m.javadoc, idx) - 1).append(")\r\n\r\n"); + } + jdx = idx + 6; + } else { + break; + } + } + jdx = 0; + for (;;) { + int idx = m.javadoc.indexOf(" Observable", jdx); + if (idx >= 0) { + if (!m.signature.contains("Observable")) { + e.append("java.lang.RuntimeException: Single doc mentions Observable but not in the signature\r\n at io.reactivex.rxjava3.core.") + .append("Single.method(Single.java:").append(m.javadocLine + lineNumber(m.javadoc, idx) - 1).append(")\r\n\r\n"); + } + jdx = idx + 6; + } else { + break; + } + } + jdx = 0; + for (;;) { + int idx = m.javadoc.indexOf(" ObservableSource", jdx); + if (idx >= 0) { + if (!m.signature.contains("ObservableSource")) { + e.append("java.lang.RuntimeException: Single doc mentions ObservableSource but not in the signature\r\n at io.reactivex.rxjava3.core.") + .append("Single.method(Single.java:").append(m.javadocLine + lineNumber(m.javadoc, idx) - 1).append(")\r\n\r\n"); + } + jdx = idx + 6; + } else { + break; + } + } + + checkAtReturnAndSignatureMatch("Single", m, e, "Flowable", "Observable", "Maybe", "Single", "Completable", "Disposable", "Iterable", "Stream", "Future", "CompletionStage"); + + aOrAn(e, m, "Single"); + missingClosingDD(e, m, "Single", "io.reactivex.rxjava3.core"); + backpressureMentionedWithoutAnnotation(e, m, "Single"); + } + } + + if (e.length() != 0) { + System.out.println(e); + + fail(e.toString()); + } + } + + @Test + public void completableDocRefersToCompletableTypes() throws Exception { + List<RxMethod> list = BaseTypeParser.parse(TestHelper.findSource("Completable"), "Completable"); + + assertFalse(list.isEmpty()); + + StringBuilder e = new StringBuilder(); + + for (RxMethod m : list) { + int jdx; + if (m.javadoc != null) { + jdx = 0; + for (;;) { + int idx = m.javadoc.indexOf("onNext", jdx); + if (idx >= 0) { + if (!m.signature.contains("Publisher") + && !m.signature.contains("Flowable") + && !m.signature.contains("Observable") + && !m.signature.contains("ObservableSource")) { + e.append("java.lang.RuntimeException: Completable doc mentions onNext but no Flowable/Observable in signature\r\n at io.reactivex.rxjava3.core.") + .append("Completable.method(Completable.java:").append(m.javadocLine + lineNumber(m.javadoc, idx) - 1).append(")\r\n\r\n"); + } + + jdx = idx + 6; + } else { + break; + } + } + jdx = 0; + for (;;) { + int idx = m.javadoc.indexOf("Subscriber", jdx); + if (idx >= 0) { + if (!m.signature.contains("Publisher") + && !m.signature.contains("Flowable") + && !m.signature.contains("TestSubscriber")) { + e.append("java.lang.RuntimeException: Completable doc mentions Subscriber but not using Flowable\r\n at io.reactivex.rxjava3.core.") + .append("Completable.method(Completable.java:").append(m.javadocLine + lineNumber(m.javadoc, idx) - 1).append(")\r\n\r\n"); + } + + jdx = idx + 6; + } else { + break; + } + } + jdx = 0; + for (;;) { + int idx = m.javadoc.indexOf(" Subscription", jdx); + if (idx >= 0) { + if (!m.signature.contains("Flowable") + && !m.signature.contains("Publisher") + ) { + e.append("java.lang.RuntimeException: Completable doc mentions Subscription but not using Flowable\r\n at io.reactivex.rxjava3.core.") + .append("Completable.method(Completable.java:").append(m.javadocLine + lineNumber(m.javadoc, idx) - 1).append(")\r\n\r\n"); + } + + jdx = idx + 6; + } else { + break; + } + } + jdx = 0; + for (;;) { + int idx = m.javadoc.indexOf("Observer", jdx); + if (idx >= 0) { + if (!m.signature.contains("ObservableSource") + && !m.signature.contains("Observable") + && !m.signature.contains("TestObserver")) { + + if (idx < 11 || !m.javadoc.substring(idx - 11, idx + 8).equals("CompletableObserver")) { + e.append("java.lang.RuntimeException: Completable doc mentions Observer but not using Observable\r\n at io.reactivex.rxjava3.core.") + .append("Completable.method(Completable.java:").append(m.javadocLine + lineNumber(m.javadoc, idx) - 1).append(")\r\n\r\n"); + } + } + + jdx = idx + 6; + } else { + break; + } + } + jdx = 0; + for (;;) { + int idx = m.javadoc.indexOf("Publisher", jdx); + if (idx >= 0) { + if (!m.signature.contains("Publisher")) { + if (idx == 0 || !m.javadoc.substring(idx - 1, idx + 9).equals("(Publisher")) { + e.append("java.lang.RuntimeException: Completable doc mentions Publisher but not in the signature\r\n at io.reactivex.rxjava3.core.") + .append("Completable.method(Completable.java:").append(m.javadocLine + lineNumber(m.javadoc, idx) - 1).append(")\r\n\r\n"); + } + } + + jdx = idx + 6; + } else { + break; + } + } + jdx = 0; + for (;;) { + int idx = m.javadoc.indexOf("Flowable", jdx); + if (idx >= 0) { + if (!m.signature.contains("Flowable")) { + Pattern p = Pattern.compile("@see\\s+#[A-Za-z0-9 _.,()]*Flowable"); + if (!p.matcher(m.javadoc).find()) { + e.append("java.lang.RuntimeException: Completable doc mentions Flowable but not in the signature\r\n at io.reactivex.rxjava3.core.") + .append("Completable.method(Completable.java:").append(m.javadocLine + lineNumber(m.javadoc, idx) - 1).append(")\r\n\r\n"); + } + } + jdx = idx + 6; + } else { + break; + } + } + jdx = 0; + for (;;) { + int idx = m.javadoc.indexOf("Single", jdx); + if (idx >= 0) { + if (!m.signature.contains("Single")) { + Pattern p = Pattern.compile("@see\\s+#[A-Za-z0-9 _.,()]*Single"); + if (!p.matcher(m.javadoc).find()) { + e.append("java.lang.RuntimeException: Completable doc mentions Single but not in the signature\r\n at io.reactivex.rxjava3.core.") + .append("Completable.method(Completable.java:").append(m.javadocLine + lineNumber(m.javadoc, idx) - 1).append(")\r\n\r\n"); + } + } + jdx = idx + 6; + } else { + break; + } + } + jdx = 0; + for (;;) { + int idx = m.javadoc.indexOf("SingleSource", jdx); + if (idx >= 0) { + if (!m.signature.contains("SingleSource")) { + Pattern p = Pattern.compile("@see\\s+#[A-Za-z0-9 _.,()]*SingleSource"); + if (!p.matcher(m.javadoc).find()) { + e.append("java.lang.RuntimeException: Completable doc mentions SingleSource but not in the signature\r\n at io.reactivex.rxjava3.core.") + .append("Completable.method(Completable.java:").append(m.javadocLine + lineNumber(m.javadoc, idx) - 1).append(")\r\n\r\n"); + } + } + jdx = idx + 6; + } else { + break; + } + } + jdx = 0; + for (;;) { + int idx = m.javadoc.indexOf(" Observable", jdx); + if (idx >= 0) { + if (!m.signature.contains("Observable")) { + Pattern p = Pattern.compile("@see\\s+#[A-Za-z0-9 _.,()]*Observable"); + if (!p.matcher(m.javadoc).find()) { + e.append("java.lang.RuntimeException: Completable doc mentions Observable but not in the signature\r\n at io.reactivex.rxjava3.core.") + .append("Completable.method(Completable.java:").append(m.javadocLine + lineNumber(m.javadoc, idx) - 1).append(")\r\n\r\n"); + } + } + jdx = idx + 6; + } else { + break; + } + } + jdx = 0; + for (;;) { + int idx = m.javadoc.indexOf("ObservableSource", jdx); + if (idx >= 0) { + if (!m.signature.contains("ObservableSource")) { + Pattern p = Pattern.compile("@see\\s+#[A-Za-z0-9 _.,()]*ObservableSource"); + if (!p.matcher(m.javadoc).find()) { + e.append("java.lang.RuntimeException: Completable doc mentions ObservableSource but not in the signature\r\n at io.reactivex.rxjava3.core.") + .append("Completable.method(Completable.java:").append(m.javadocLine + lineNumber(m.javadoc, idx) - 1).append(")\r\n\r\n"); + } + } + jdx = idx + 6; + } else { + break; + } + } + + checkAtReturnAndSignatureMatch("Completable", m, e, "Flowable", "Observable", "Maybe", "Single", "Completable", "Disposable", "Iterable", "Stream", "Future", "CompletionStage"); + + aOrAn(e, m, "Completable"); + missingClosingDD(e, m, "Completable", "io.reactivex.rxjava3.core"); + backpressureMentionedWithoutAnnotation(e, m, "Completable"); + } + } + + if (e.length() != 0) { + System.out.println(e); + + fail(e.toString()); + } + } + + static void checkAtReturnAndSignatureMatch(String className, RxMethod m, StringBuilder e, String... types) { + for (String t : types) { + String regex; + if (t.contains("Completable")) { + regex = "(?s).*?\\s" + t + "\\s+\\w+\\(.*"; + } else { + regex = "(?s).*?\\s" + t + "\\<.*?\\>\\s+\\w+\\(.*"; + } + if (m.signature.matches(regex)) { + for (String at : AT_RETURN_WORDS) { + for (String u : types) { + if (!t.equals(u)) { + int idx = m.javadoc.indexOf(at + "{@code " + u); + if (idx >= 0) { + e.append("Returns ").append(t) + .append(" but docs return ") + .append(u) + .append("\r\n at io.reactivex.rxjava3.core.") + .append(className) + .append(".method(") + .append(className) + .append(".java:") + .append(m.javadocLine + lineNumber(m.javadoc, idx) - 1) + .append(")\r\n\r\n"); + } + } + } + } + } + } + } + + static void aOrAn(StringBuilder e, RxMethod m, String baseTypeName) { + aOrAn(e, m, " an", "Single", baseTypeName); + aOrAn(e, m, " an", "Maybe", baseTypeName); + aOrAn(e, m, " a", "Observer", baseTypeName); + aOrAn(e, m, " a", "Observable", baseTypeName); + aOrAn(e, m, " an", "Publisher", baseTypeName); + aOrAn(e, m, " an", "Subscriber", baseTypeName); + aOrAn(e, m, " an", "Flowable", baseTypeName); + + aOrAn(e, m, " a", "Observable", baseTypeName); + + } + + static void aOrAn(StringBuilder e, RxMethod m, String wrongPre, String word, String baseTypeName) { + int jdx = 0; + int idx; + for (;;) { + idx = m.javadoc.indexOf(wrongPre + " " + word, jdx); + if (idx >= 0) { + e.append("java.lang.RuntimeException: a/an typo ") + .append(word) + .append("\r\n at io.reactivex.rxjava3.core.") + .append(baseTypeName) + .append(".method(") + .append(baseTypeName) + .append(".java:").append(m.javadocLine + lineNumber(m.javadoc, idx) - 1).append(")\r\n\r\n"); + jdx = idx + 6; + } else { + break; + } + } + + jdx = 0; + for (;;) { + idx = m.javadoc.indexOf(wrongPre + " {@link " + word, jdx); + if (idx >= 0) { + e.append("java.lang.RuntimeException: a/an typo ") + .append(word) + .append("\r\n at io.reactivex.rxjava3.core.") + .append(baseTypeName) + .append(".method(") + .append(baseTypeName) + .append(".java:").append(m.javadocLine + lineNumber(m.javadoc, idx) - 1).append(")\r\n\r\n"); + jdx = idx + 6; + } else { + break; + } + } + + jdx = 0; + for (;;) { + idx = m.javadoc.indexOf(wrongPre + " {@linkplain " + word, jdx); + if (idx >= 0) { + e.append("java.lang.RuntimeException: a/an typo ") + .append(word) + .append("\r\n at io.reactivex.rxjava3.core.") + .append(baseTypeName) + .append(".method(") + .append(baseTypeName) + .append(".java:").append(m.javadocLine + lineNumber(m.javadoc, idx) - 1).append(")\r\n\r\n"); + jdx = idx + 6; + } else { + break; + } + } + + jdx = 0; + for (;;) { + idx = m.javadoc.indexOf(wrongPre + " {@code " + word, jdx); + if (idx >= 0) { + e.append("java.lang.RuntimeException: a/an typo ") + .append(word) + .append("\r\n at io.reactivex.rxjava3.core.") + .append(baseTypeName) + .append(".method(") + .append(baseTypeName) + .append(".java:").append(m.javadocLine + lineNumber(m.javadoc, idx) - 1).append(")\r\n\r\n"); + jdx = idx + 6; + } else { + break; + } + } + + // remove linebreaks and multi-spaces + String javadoc2 = m.javadoc.replace("\n", " ").replace("\r", " ") + .replace(" * ", " ") + .replaceAll("\\s+", " "); + + // strip {@xxx } tags + int kk = 0; + for (;;) { + int jj = javadoc2.indexOf("{@", kk); + if (jj < 0) { + break; + } + int nn = javadoc2.indexOf(" ", jj + 2); + int mm = javadoc2.indexOf("}", jj + 2); + + javadoc2 = javadoc2.substring(0, jj) + javadoc2.substring(nn + 1, mm) + javadoc2.substring(mm + 1); + + kk = mm + 1; + } + + jdx = 0; + for (;;) { + idx = javadoc2.indexOf(wrongPre + " " + word, jdx); + if (idx >= 0) { + e.append("java.lang.RuntimeException: a/an typo ") + .append(word) + .append("\r\n at io.reactivex.rxjava3.core.") + .append(baseTypeName) + .append(".method(") + .append(baseTypeName) + .append(".java:").append(m.javadocLine).append(")\r\n\r\n"); + jdx = idx + wrongPre.length() + 1 + word.length(); + } else { + break; + } + } + } + + static void missingClosingDD(StringBuilder e, RxMethod m, String baseTypeName, String packageName) { + int jdx = 0; + for (;;) { + int idx1 = m.javadoc.indexOf("<dd>", jdx); + int idx2 = m.javadoc.indexOf("</dd>", jdx); + + if (idx1 < 0 && idx2 < 0) { + break; + } + + int idx3 = m.javadoc.indexOf("<dd>", idx1 + 4); + + if (idx1 > 0 && idx2 > 0 && (idx3 < 0 || (idx2 < idx3 && idx3 > 0))) { + jdx = idx2 + 5; + } else { + e.append("java.lang.RuntimeException: unbalanced <dd></dd> ") + .append("\r\n at ") + .append(packageName) + .append(".") + .append(baseTypeName) + .append(".method(") + .append(baseTypeName) + .append(".java:").append(m.javadocLine + lineNumber(m.javadoc, idx1) - 1).append(")\r\n\r\n"); + break; + } + } + } + + static void backpressureMentionedWithoutAnnotation(StringBuilder e, RxMethod m, String baseTypeName) { + if (m.backpressureDocLine > 0 && m.backpressureKind == null) { + e.append("java.lang.RuntimeException: backpressure documented but not annotated ") + .append("\r\n at io.reactivex.rxjava3.core.") + .append(baseTypeName) + .append(".method(") + .append(baseTypeName) + .append(".java:").append(m.backpressureDocLine).append(")\r\n\r\n"); + } + } + + static final String[] AT_RETURN_WORDS = { "@return a ", "@return an ", "@return the new ", "@return a new " }; +} diff --git a/src/test/java/io/reactivex/rxjava3/validators/MaybeNo2Dot0Since.java b/src/test/java/io/reactivex/rxjava3/validators/MaybeNo2Dot0Since.java new file mode 100644 index 0000000000..b894492894 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/validators/MaybeNo2Dot0Since.java @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.validators; + +import static org.junit.Assert.fail; + +import java.io.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.Maybe; +import io.reactivex.rxjava3.testsupport.TestHelper; + +/** + * Checks the source code of Maybe and finds unnecessary since 2.0 annotations in the + * method's javadocs. + */ +public class MaybeNo2Dot0Since { + + @Test + public void noSince20InMaybe() throws Exception { + + File f = TestHelper.findSource(Maybe.class.getSimpleName()); + + String line; + + StringBuilder b = new StringBuilder(); + + boolean classDefPassed = false; + + BufferedReader in = new BufferedReader(new FileReader(f)); + try { + int ln = 1; + while (true) { + line = in.readLine(); + + if (line == null) { + break; + } + + if (line.startsWith("public abstract class Maybe<")) { + classDefPassed = true; + } + + if (classDefPassed) { + if (line.contains("@since") && line.contains("2.0") && !line.contains("2.0.")) { + b.append("java.lang.RuntimeException: @since 2.0 found").append("\r\n") + .append(" at io.reactivex.Maybe (Maybe.java:").append(ln).append(")\r\n\r\n"); + ; + } + } + + ln++; + } + } finally { + in.close(); + } + + if (b.length() != 0) { + System.out.println(b); + + fail(b.toString()); + } + } +} diff --git a/src/test/java/io/reactivex/rxjava3/validators/NewLinesBeforeAnnotation.java b/src/test/java/io/reactivex/rxjava3/validators/NewLinesBeforeAnnotation.java new file mode 100644 index 0000000000..2c27fc6fd4 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/validators/NewLinesBeforeAnnotation.java @@ -0,0 +1,164 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.validators; + +import java.io.*; +import java.util.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.testsupport.TestHelper; + +/** + * These tests verify the code style that a typical closing curly brace + * and the next annotation @ indicator + * are not separated by less than or more than one empty line. + * <p>Thus this is detected: + * <pre><code> + * } + * @Override + * </code></pre> + * <p> + * as well as + * <pre><code> + * } + * + * + * @Override + * </code></pre> + */ +public class NewLinesBeforeAnnotation { + + @Test + public void missingEmptyNewLine() throws Exception { + findPattern(0); + } + + @Test + public void tooManyEmptyNewLines2() throws Exception { + findPattern(2); + } + + @Test + public void tooManyEmptyNewLines3() throws Exception { + findPattern(3); + } + + @Test + public void tooManyEmptyNewLines4() throws Exception { + findPattern(4); + } + + @Test + public void tooManyEmptyNewLines5() throws Exception { + findPattern(5); + } + + static void findPattern(int newLines) throws Exception { + File f = TestHelper.findSource("Flowable"); + if (f == null) { + System.out.println("Unable to find sources of RxJava"); + return; + } + + Queue<File> dirs = new ArrayDeque<>(); + + StringBuilder fail = new StringBuilder(); + fail.append("The following code pattern was found: "); + fail.append("\\}\\R"); + for (int i = 0; i < newLines; i++) { + fail.append("\\R"); + } + fail.append("[ ]+@\n"); + + File parent = f.getParentFile().getParentFile(); + + dirs.offer(new File(parent.getAbsolutePath().replace('\\', '/'))); + dirs.offer(new File(parent.getAbsolutePath().replace('\\', '/').replace("src/main/java", "src/test/java"))); + + int total = 0; + + while (!dirs.isEmpty()) { + f = dirs.poll(); + + File[] list = f.listFiles(); + if (list != null && list.length != 0) { + + for (File u : list) { + if (u.isDirectory()) { + dirs.offer(u); + } else { + String fname = u.getName(); + if (fname.endsWith(".java")) { + + List<String> lines = new ArrayList<>(); + BufferedReader in = new BufferedReader(new FileReader(u)); + try { + for (;;) { + String line = in.readLine(); + if (line == null) { + break; + } + lines.add(line); + } + } finally { + in.close(); + } + + for (int i = 0; i < lines.size() - 1; i++) { + String line = lines.get(i); + if (line.endsWith("}") && !line.trim().startsWith("*") && !line.trim().startsWith("//")) { + int emptyLines = 0; + boolean found = false; + for (int j = i + 1; j < lines.size(); j++) { + String line2 = lines.get(j); + if (line2.trim().startsWith("@")) { + found = true; + break; + } + if (!line2.trim().isEmpty()) { + break; + } + emptyLines++; + } + + if (emptyLines == newLines && found) { + fail + .append(fname) + .append("#L").append(i + 1) + .append(" "); + for (int k = 0; k < emptyLines + 2; k++) { + fail + .append(lines.get(k + i)) + .append("\\R"); + } + fail.append("\n"); + total++; + } + } + } + } + } + } + } + } + if (total != 0) { + fail.append("Found ") + .append(total) + .append(" instances"); + System.out.println(fail); + throw new AssertionError(fail.toString()); + } + } +} diff --git a/src/test/java/io/reactivex/rxjava3/validators/NoAnonymousInnerClassesTest.java b/src/test/java/io/reactivex/rxjava3/validators/NoAnonymousInnerClassesTest.java new file mode 100644 index 0000000000..61db410b5b --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/validators/NoAnonymousInnerClassesTest.java @@ -0,0 +1,128 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.validators; + +import java.io.*; +import java.net.URL; +import java.util.*; + +import org.junit.Test; + +public class NoAnonymousInnerClassesTest { + + @Test + public void verify() throws Exception { + URL u = NoAnonymousInnerClassesTest.class.getResource("/"); + File f = new File(u.toURI()); + + String fs = f.toString().toLowerCase().replace("\\", "/"); + + System.out.println("Found " + fs); + + // running this particular test from IntelliJ will have the wrong class directory + // gradle will generate test classes into a separate directory too + int idx = fs.indexOf("/test"); + if (idx >= 0) { + f = new File(fs.substring(0, idx)); + } + + StringBuilder b = new StringBuilder("Anonymous inner classes found:"); + + Queue<File> queue = new ArrayDeque<>(); + + queue.offer(f); + + String prefix = f.getAbsolutePath(); + int count = 0; + while (!queue.isEmpty()) { + + f = queue.poll(); + + if (f.isDirectory()) { + File[] dir = f.listFiles(); + if (dir != null && dir.length != 0) { + for (File g : dir) { + queue.offer(g); + } + } + } else { + String name = f.getName(); + if (name.endsWith(".class") && name.contains("$") + && !name.contains("Perf") && !name.contains("Test") + && !name.startsWith("Test")) { + String baseName = name.substring(0, name.length() - 6); + String[] parts = name.split("\\$"); + for (String s : parts) { + if (Character.isDigit(s.charAt(0))) { + String n = f.getAbsolutePath().substring(prefix.length()).replace('\\', '.').replace('/', '.'); + if (n.startsWith(".")) { + n = n.substring(1); + } + + // javac generates switch-map anonymous classes with the same $x.class pattern + // we have to look into the file and search for $SwitchMap$ + + boolean found = false; + + FileInputStream fin = new FileInputStream(f); + try { + byte[] data = new byte[fin.available()]; + fin.read(data); + + String content = new String(data, "ISO-8859-1"); + + if (content.contains("$SwitchMap$")) { + // the parent class can reference these synthetic inner classes + // and thus they also have $SwitchMap$ + // but the synthetic inner classes should not have further inner classes + + File[] filesInTheSameDir = f.getParentFile().listFiles(); + + for (File fsame : filesInTheSameDir) { + String fsameName = fsame.getName(); + if (fsameName.endsWith(".class")) { + fsameName = fsameName.substring(0, fsameName.length() - 6); + + if (fsameName.startsWith(baseName) + && fsameName.length() > baseName.length() + 1 + && fsameName.charAt(baseName.length()) == '$' + && Character.isDigit(fsameName.charAt(baseName.length() + 1))) { + found = true; + break; + } + } + } + } else { + found = true; + } + } finally { + fin.close(); + } + + if (found) { + b.append("\r\n").append(n); + count++; + break; + } + } + } + } + } + } + + if (count != 0) { + throw new AssertionError(b.toString()); + } + } +} diff --git a/src/test/java/io/reactivex/rxjava3/validators/NonNullMethodTypeArgumentCheck.java b/src/test/java/io/reactivex/rxjava3/validators/NonNullMethodTypeArgumentCheck.java new file mode 100644 index 0000000000..e8c4b81715 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/validators/NonNullMethodTypeArgumentCheck.java @@ -0,0 +1,149 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.validators; + +import static org.junit.Assert.assertEquals; + +import java.io.*; +import java.nio.file.Files; +import java.util.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.parallel.ParallelFlowable; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.testsupport.TestHelper; + +/** + * Verify static methods and final methods declaring type arguments + * declare {@code @NonNull} for said argument. + * + */ +public class NonNullMethodTypeArgumentCheck { + + static void process(Class<?> clazz) { + + String className = clazz.getSimpleName(); + String parentPackage = clazz.getPackage().getName(); + + StringBuilder result = new StringBuilder(); + int count = 0; + + try { + File f = TestHelper.findSource(className, parentPackage); + + try (BufferedReader in = Files.newBufferedReader(f.toPath())) { + int lineCount = 1; + String line = null; + + while ((line = in.readLine()) != null) { + line = line.trim(); + + if (!line.contains(" to(")) { + if (line.startsWith("public static <") || line.startsWith("public final <")) { + + for (String ta : parseTypeArguments(line)) { + if (!ta.startsWith("@NonNull") && !ta.startsWith("@Nullable")) { + if (!("Maybe".equals(clazz.getSimpleName()) && (line.contains("fromCallable(") || line.contains("fromSupplier(")))) { + result.append("Missing annotation on argument ").append(ta).append("\r\nat ") + .append(parentPackage).append(".").append(className).append(".method(") + .append(className).append(".java:").append(lineCount).append(")\r\n"); + count++; + } + } + } + } + } + lineCount++; + } + } + } catch (Exception ex) { + throw new RuntimeException(ex); + } + + if (count != 0) { + throw new IllegalArgumentException("Found " + count + " cases\r\n" + result.toString()); + } + } + + static List<String> parseTypeArguments(String line) { + List<String> result = new ArrayList<>(); + int offset = line.indexOf("<"); + int c = 1; + int i = offset + 1; + int j = i; + for (; i < line.length(); i++) { + if (line.charAt(i) == '<') { + c++; + } else + if (line.charAt(i) == '>') { + c--; + if (c == 0) { + break; + } + } else + if (line.charAt(i) == ',' && c == 1) { + result.add(line.substring(j, i).trim()); + j = i + 1; + } + } + result.add(line.substring(j, i).trim()); + return result; + } + + @Test + public void parseTypeArguments() { + assertEquals(new ArrayList<>(Arrays.asList("T")), parseTypeArguments("<T>")); + assertEquals(new ArrayList<>(Arrays.asList("T", "U")), parseTypeArguments("<T, U>")); + assertEquals(new ArrayList<>(Arrays.asList("T", "Flowable<U>")), parseTypeArguments("<T, Flowable<U>>")); + assertEquals(new ArrayList<>(Arrays.asList("T", "Flowable<U, V>")), parseTypeArguments("<T, Flowable<U, V>>")); + } + + @Test + public void flowable() { + process(Flowable.class); + } + + @Test + public void observable() { + process(Observable.class); + } + + @Test + public void maybe() { + process(Maybe.class); + } + + @Test + public void single() { + process(Single.class); + } + + @Test + public void completable() { + process(Completable.class); + } + + @Test + public void parallel() { + process(ParallelFlowable.class); + } + + @Test + public void plugins() { + process(RxJavaPlugins.class); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/validators/OperatorsAreFinal.java b/src/test/java/io/reactivex/rxjava3/validators/OperatorsAreFinal.java new file mode 100644 index 0000000000..a59e3aefa2 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/validators/OperatorsAreFinal.java @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.validators; + +import java.io.File; +import java.lang.reflect.Modifier; + +import org.junit.Test; + +import io.reactivex.rxjava3.testsupport.TestHelper; + +public class OperatorsAreFinal { + + File directoryOf(String baseClassName) throws Exception { + File f = TestHelper.findSource("Flowable"); + if (f == null) { + return null; + } + + String parent = f.getParentFile().getParentFile().getAbsolutePath().replace('\\', '/'); + if (!parent.endsWith("/")) { + parent += "/"; + } + + parent += "internal/operators/" + baseClassName.toLowerCase() + "/"; + return new File(parent); + } + + void check(String baseClassName) throws Exception { + File f = directoryOf(baseClassName); + if (f == null) { + return; + } + + StringBuilder e = new StringBuilder(); + + File[] files = f.listFiles(); + if (files != null) { + for (File g : files) { + if (g.getName().startsWith(baseClassName) && g.getName().endsWith(".java")) { + String className = "io.reactivex.rxjava3.internal.operators." + baseClassName.toLowerCase() + "." + g.getName().replace(".java", ""); + + Class<?> clazz = Class.forName(className); + + if ((clazz.getModifiers() & Modifier.FINAL) == 0 && (clazz.getModifiers() & Modifier.ABSTRACT) == 0) { + e.append("java.lang.RuntimeException: ").append(className).append(" is not final\r\n"); + e.append(" at ").append(className).append(" (").append(g.getName()).append(":14)\r\n\r\n"); + } + } + } + } + + if (e.length() != 0) { + System.out.println(e); + + throw new AssertionError(e.toString()); + } + } + + @Test + public void flowable() throws Exception { + check("Flowable"); + } + + @Test + public void observable() throws Exception { + check("Observable"); + } + + @Test + public void single() throws Exception { + check("Single"); + } + + @Test + public void completable() throws Exception { + check("Completable"); + } + + @Test + public void maybe() throws Exception { + check("Maybe"); + } + +} diff --git a/src/test/java/io/reactivex/rxjava3/validators/OperatorsUseInterfaces.java b/src/test/java/io/reactivex/rxjava3/validators/OperatorsUseInterfaces.java new file mode 100644 index 0000000000..76d4a836f9 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/validators/OperatorsUseInterfaces.java @@ -0,0 +1,189 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.validators; + +import static org.junit.Assert.*; + +import java.lang.reflect.*; +import java.util.*; +import java.util.concurrent.Callable; + +import org.junit.Test; +import org.reactivestreams.Publisher; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.parallel.ParallelFlowable; + +/** + * Verify that an operator method uses base interfaces as its direct input or + * has lambdas returning base interfaces. + */ +public class OperatorsUseInterfaces { + + @Test + public void checkFlowable() { + checkClass(Flowable.class); + } + + @Test + public void checkObservable() { + checkClass(Observable.class); + } + + @Test + public void checkMaybe() { + checkClass(Maybe.class); + } + + @Test + public void checkSingle() { + checkClass(Single.class); + } + + @Test + public void checkCompletable() { + checkClass(Completable.class); + } + + @Test + public void checkParallelFlowable() { + checkClass(ParallelFlowable.class); + } + + void checkClass(Class<?> clazz) { + StringBuilder error = new StringBuilder(); + int errors = 0; + + for (Method method : clazz.getMethods()) { + if (method.getDeclaringClass() == clazz) { + int pidx = 1; + for (Parameter param : method.getParameters()) { + Class<?> type = param.getType(); + if (type.isArray()) { + type = type.getComponentType(); + } + if (CLASSES.contains(type)) { + errors++; + error.append("Non-interface input parameter #") + .append(pidx) + .append(": ") + .append(type) + .append("\r\n") + .append(" ") + .append(method) + .append("\r\n") + ; + } + if (CAN_RETURN.contains(type)) { + Type gtype = method.getGenericParameterTypes()[pidx - 1]; + if (gtype instanceof GenericArrayType) { + gtype = ((GenericArrayType)gtype).getGenericComponentType(); + } + ParameterizedType ptype = (ParameterizedType)gtype; + for (;;) { + Type[] parameterArgTypes = ptype.getActualTypeArguments(); + Type argType = parameterArgTypes[parameterArgTypes.length - 1]; + if (argType instanceof GenericArrayType) { + argType = ((GenericArrayType)argType).getGenericComponentType(); + } + if (argType instanceof ParameterizedType) { + ParameterizedType lastArg = (ParameterizedType)argType; + + if (CLASSES.contains(lastArg.getRawType())) { + errors++; + error.append("Non-interface lambda return #") + .append(pidx) + .append(": ") + .append(type) + .append("\r\n") + .append(" ") + .append(method) + .append("\r\n") + ; + } + + if (CAN_RETURN.contains(lastArg.getRawType())) { + ptype = lastArg; + continue; + } + } + break; + } + } + pidx++; + } + } + } + + if (errors != 0) { + error.insert(0, "Found " + errors + " issues\r\n"); + fail(error.toString()); + } + } + + public void method1(Flowable<?> f) { + // self-test + } + + public void method2(Callable<Flowable<?>> c) { + // self-test + } + + public void method3(Supplier<Publisher<Flowable<?>>> c) { + // self-test + } + + public void method4(Flowable<?>[] array) { + // self-test + } + + public void method5(Callable<Flowable<?>[]> c) { + // self-test + } + + public void method6(Callable<Publisher<Flowable<?>[]>> c) { + // self-test + } + + @Test + public void checkSelf() { + try { + checkClass(OperatorsUseInterfaces.class); + throw new RuntimeException("Should have failed"); + } catch (AssertionError expected) { + assertTrue(expected.toString(), expected.toString().contains("method1")); + assertTrue(expected.toString(), expected.toString().contains("method2")); + assertTrue(expected.toString(), expected.toString().contains("method3")); + assertTrue(expected.toString(), expected.toString().contains("method4")); + assertTrue(expected.toString(), expected.toString().contains("method5")); + assertTrue(expected.toString(), expected.toString().contains("method6")); + } + } + + static final Set<Class<?>> CLASSES = new HashSet<>(Arrays.asList( + Flowable.class, Observable.class, + Maybe.class, Single.class, + Completable.class + )); + + static final Set<Class<?>> CAN_RETURN = new HashSet<>(Arrays.asList( + Callable.class, Supplier.class, + Function.class, BiFunction.class, Function3.class, Function4.class, + Function5.class, Function6.class, Function7.class, Function8.class, + Function9.class, + Publisher.class, ObservableSource.class, MaybeSource.class, SingleSource.class + )); +} diff --git a/src/test/java/io/reactivex/rxjava3/validators/ParamValidationCheckerTest.java b/src/test/java/io/reactivex/rxjava3/validators/ParamValidationCheckerTest.java new file mode 100644 index 0000000000..c2ca87d4d2 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/validators/ParamValidationCheckerTest.java @@ -0,0 +1,1228 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.validators; + +import java.lang.reflect.*; +import java.time.Duration; +import java.util.*; +import java.util.concurrent.*; +import java.util.stream.*; + +import org.junit.Test; +import org.reactivestreams.*; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.core.Observer; +import io.reactivex.rxjava3.disposables.*; +import io.reactivex.rxjava3.exceptions.TestException; +import io.reactivex.rxjava3.functions.*; +import io.reactivex.rxjava3.internal.functions.Functions; +import io.reactivex.rxjava3.parallel.*; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.schedulers.Schedulers; +import io.reactivex.rxjava3.testsupport.TestHelper; + +/** + * Check that static and instance methods validate their parameters against + * null and invalid values properly. + */ +public class ParamValidationCheckerTest { + + @Test(timeout = 30000) + public void checkFlowable() { + checkClass(Flowable.class); + } + + @Test(timeout = 30000) + public void checkObservable() { + checkClass(Observable.class); + } + + @Test(timeout = 30000) + public void checkSingle() { + checkClass(Single.class); + } + + @Test(timeout = 30000) + public void checkMaybe() { + checkClass(Maybe.class); + } + + @Test(timeout = 30000) + public void checkCompletable() { + checkClass(Completable.class); + } + + @Test(timeout = 30000) + public void checkParallelFlowable() { + checkClass(ParallelFlowable.class); + } + + // --------------------------------------------------------------------------------------- + // --------------------------------------------------------------------------------------- + + static Map<String, List<ParamOverride>> overrides; + + static Map<String, List<ParamIgnore>> ignores; + + static Map<Class<?>, Object> defaultValues; + + static Map<Class<?>, List<Object>> defaultInstances; + + static { + overrides = new HashMap<>(); + + // *********************************************************************************************************************** + + // zero index allowed + addOverride(new ParamOverride(Flowable.class, 0, ParamMode.NON_NEGATIVE, "elementAt", Long.TYPE)); + addOverride(new ParamOverride(Flowable.class, 0, ParamMode.NON_NEGATIVE, "elementAt", Long.TYPE, Object.class)); + + addOverride(new ParamOverride(Flowable.class, 0, ParamMode.NON_NEGATIVE, "elementAtOrError", Long.TYPE)); + + // negative skip count is ignored + addOverride(new ParamOverride(Flowable.class, 0, ParamMode.NON_NEGATIVE, "skip", Long.TYPE)); + // negative skip time is considered as zero skip time + addOverride(new ParamOverride(Flowable.class, 0, ParamMode.ANY, "skip", Long.TYPE, TimeUnit.class)); + addOverride(new ParamOverride(Flowable.class, 0, ParamMode.ANY, "skip", Long.TYPE, TimeUnit.class, Scheduler.class)); + + // can start with zero initial request + addOverride(new ParamOverride(Flowable.class, 0, ParamMode.NON_NEGATIVE, "test", Long.TYPE)); + addOverride(new ParamOverride(Flowable.class, 0, ParamMode.NON_NEGATIVE, "test", Long.TYPE, Boolean.TYPE)); + + // negative timeout time is considered as zero timeout time + addOverride(new ParamOverride(Flowable.class, 0, ParamMode.ANY, "timeout", Long.TYPE, TimeUnit.class)); + addOverride(new ParamOverride(Flowable.class, 0, ParamMode.ANY, "timeout", Long.TYPE, TimeUnit.class, Scheduler.class)); + addOverride(new ParamOverride(Flowable.class, 0, ParamMode.ANY, "timeout", Long.TYPE, TimeUnit.class, Publisher.class)); + addOverride(new ParamOverride(Flowable.class, 0, ParamMode.ANY, "timeout", Long.TYPE, TimeUnit.class, Scheduler.class, Publisher.class)); + + // negative buffer time is considered as zero buffer time + addOverride(new ParamOverride(Flowable.class, 0, ParamMode.ANY, "buffer", Long.TYPE, TimeUnit.class)); + addOverride(new ParamOverride(Flowable.class, 0, ParamMode.ANY, "buffer", Long.TYPE, TimeUnit.class, Integer.TYPE)); + addOverride(new ParamOverride(Flowable.class, 0, ParamMode.ANY, "buffer", Long.TYPE, TimeUnit.class, Scheduler.class)); + addOverride(new ParamOverride(Flowable.class, 0, ParamMode.ANY, "buffer", Long.TYPE, TimeUnit.class, Scheduler.class, Integer.TYPE)); + addOverride(new ParamOverride(Flowable.class, 0, ParamMode.ANY, "buffer", Long.TYPE, TimeUnit.class, Scheduler.class, Integer.TYPE, Supplier.class, Boolean.TYPE)); + + // negative time/skip is considered zero time/skip + addOverride(new ParamOverride(Flowable.class, 0, ParamMode.ANY, "buffer", Long.TYPE, Long.TYPE, TimeUnit.class)); + addOverride(new ParamOverride(Flowable.class, 0, ParamMode.ANY, "buffer", Long.TYPE, Long.TYPE, TimeUnit.class, Scheduler.class)); + addOverride(new ParamOverride(Flowable.class, 0, ParamMode.ANY, "buffer", Long.TYPE, Long.TYPE, TimeUnit.class, Scheduler.class, Supplier.class)); + addOverride(new ParamOverride(Flowable.class, 1, ParamMode.ANY, "buffer", Long.TYPE, Long.TYPE, TimeUnit.class)); + addOverride(new ParamOverride(Flowable.class, 1, ParamMode.ANY, "buffer", Long.TYPE, Long.TYPE, TimeUnit.class, Scheduler.class)); + addOverride(new ParamOverride(Flowable.class, 1, ParamMode.ANY, "buffer", Long.TYPE, Long.TYPE, TimeUnit.class, Scheduler.class, Supplier.class)); + + // negative timeout is allowed + addOverride(new ParamOverride(Flowable.class, 1, ParamMode.ANY, "fromFuture", Future.class, Long.TYPE, TimeUnit.class)); + + // negative time is considered as zero time + addOverride(new ParamOverride(Flowable.class, 0, ParamMode.ANY, "timer", Long.TYPE, TimeUnit.class)); + addOverride(new ParamOverride(Flowable.class, 0, ParamMode.ANY, "timer", Long.TYPE, TimeUnit.class, Scheduler.class)); + addOverride(new ParamOverride(Flowable.class, 0, ParamMode.ANY, "interval", Long.TYPE, TimeUnit.class)); + addOverride(new ParamOverride(Flowable.class, 0, ParamMode.ANY, "interval", Long.TYPE, TimeUnit.class, Scheduler.class)); + addOverride(new ParamOverride(Flowable.class, 0, ParamMode.ANY, "interval", Long.TYPE, Long.TYPE, TimeUnit.class)); + addOverride(new ParamOverride(Flowable.class, 0, ParamMode.ANY, "interval", Long.TYPE, Long.TYPE, TimeUnit.class, Scheduler.class)); + addOverride(new ParamOverride(Flowable.class, 1, ParamMode.ANY, "interval", Long.TYPE, Long.TYPE, TimeUnit.class)); + addOverride(new ParamOverride(Flowable.class, 1, ParamMode.ANY, "interval", Long.TYPE, Long.TYPE, TimeUnit.class, Scheduler.class)); + + // negative time is considered as zero time + addOverride(new ParamOverride(Flowable.class, 0, ParamMode.ANY, "delay", Long.TYPE, TimeUnit.class)); + addOverride(new ParamOverride(Flowable.class, 0, ParamMode.ANY, "delay", Long.TYPE, TimeUnit.class, Boolean.TYPE)); + addOverride(new ParamOverride(Flowable.class, 0, ParamMode.ANY, "delay", Long.TYPE, TimeUnit.class, Scheduler.class)); + addOverride(new ParamOverride(Flowable.class, 0, ParamMode.ANY, "delay", Long.TYPE, TimeUnit.class, Scheduler.class, Boolean.TYPE)); + + // negative time is considered as zero time + addOverride(new ParamOverride(Flowable.class, 0, ParamMode.ANY, "delaySubscription", Long.TYPE, TimeUnit.class)); + addOverride(new ParamOverride(Flowable.class, 0, ParamMode.ANY, "delaySubscription", Long.TYPE, TimeUnit.class, Scheduler.class)); + + // negative time is considered as zero time + addOverride(new ParamOverride(Flowable.class, 0, ParamMode.ANY, "debounce", Long.TYPE, TimeUnit.class)); + addOverride(new ParamOverride(Flowable.class, 0, ParamMode.ANY, "debounce", Long.TYPE, TimeUnit.class, Scheduler.class)); + addOverride(new ParamOverride(Flowable.class, 0, ParamMode.ANY, "debounce", Long.TYPE, TimeUnit.class, Scheduler.class, Consumer.class)); + + // null Action allowed + addOverride(new ParamOverride(Flowable.class, 1, ParamMode.ANY, "onBackpressureBuffer", Long.TYPE, Action.class, BackpressureOverflowStrategy.class)); + addOverride(new ParamOverride(Flowable.class, 1, ParamMode.ANY, "onBackpressureBuffer", Long.TYPE, Action.class, BackpressureOverflowStrategy.class, Consumer.class)); + + // zero repeat is allowed + addOverride(new ParamOverride(Flowable.class, 0, ParamMode.NON_NEGATIVE, "repeat", Long.TYPE)); + + // negative time is considered as zero time + addOverride(new ParamOverride(Flowable.class, 0, ParamMode.ANY, "replay", Long.TYPE, TimeUnit.class)); + addOverride(new ParamOverride(Flowable.class, 0, ParamMode.ANY, "replay", Long.TYPE, TimeUnit.class, Scheduler.class)); + addOverride(new ParamOverride(Flowable.class, 0, ParamMode.ANY, "replay", Long.TYPE, TimeUnit.class, Scheduler.class, boolean.class)); + addOverride(new ParamOverride(Flowable.class, 1, ParamMode.ANY, "replay", Integer.TYPE, Long.TYPE, TimeUnit.class)); + addOverride(new ParamOverride(Flowable.class, 1, ParamMode.ANY, "replay", Integer.TYPE, Long.TYPE, TimeUnit.class, Scheduler.class)); + addOverride(new ParamOverride(Flowable.class, 1, ParamMode.ANY, "replay", Integer.TYPE, Long.TYPE, TimeUnit.class, Scheduler.class, boolean.class)); + addOverride(new ParamOverride(Flowable.class, 1, ParamMode.ANY, "replay", Function.class, Long.TYPE, TimeUnit.class)); + addOverride(new ParamOverride(Flowable.class, 1, ParamMode.ANY, "replay", Function.class, Long.TYPE, TimeUnit.class, Scheduler.class)); + addOverride(new ParamOverride(Flowable.class, 1, ParamMode.ANY, "replay", Function.class, Long.TYPE, TimeUnit.class, Scheduler.class, boolean.class)); + addOverride(new ParamOverride(Flowable.class, 2, ParamMode.ANY, "replay", Function.class, Integer.TYPE, Long.TYPE, TimeUnit.class)); + addOverride(new ParamOverride(Flowable.class, 2, ParamMode.ANY, "replay", Function.class, Integer.TYPE, Long.TYPE, TimeUnit.class, Scheduler.class)); + addOverride(new ParamOverride(Flowable.class, 2, ParamMode.ANY, "replay", Function.class, Integer.TYPE, Long.TYPE, TimeUnit.class, Scheduler.class, boolean.class)); + + // zero retry is allowed + addOverride(new ParamOverride(Flowable.class, 0, ParamMode.NON_NEGATIVE, "retry", Long.TYPE)); + addOverride(new ParamOverride(Flowable.class, 0, ParamMode.NON_NEGATIVE, "retry", Long.TYPE, Predicate.class)); + + // negative time is considered as zero time + addOverride(new ParamOverride(Flowable.class, 0, ParamMode.ANY, "throttleWithTimeout", Long.TYPE, TimeUnit.class)); + addOverride(new ParamOverride(Flowable.class, 0, ParamMode.ANY, "throttleWithTimeout", Long.TYPE, TimeUnit.class, Scheduler.class)); + addOverride(new ParamOverride(Flowable.class, 0, ParamMode.ANY, "throttleWithTimeout", Long.TYPE, TimeUnit.class, Scheduler.class, Consumer.class)); + + // negative time is considered as zero time + addOverride(new ParamOverride(Flowable.class, 0, ParamMode.ANY, "take", Long.TYPE, TimeUnit.class)); + addOverride(new ParamOverride(Flowable.class, 0, ParamMode.ANY, "take", Long.TYPE, TimeUnit.class, Scheduler.class)); + + // zero take is allowed + addOverride(new ParamOverride(Flowable.class, 0, ParamMode.NON_NEGATIVE, "take", Long.TYPE)); + + // negative time is considered as zero time + addOverride(new ParamOverride(Flowable.class, 0, ParamMode.ANY, "sample", Long.TYPE, TimeUnit.class)); + addOverride(new ParamOverride(Flowable.class, 0, ParamMode.ANY, "sample", Long.TYPE, TimeUnit.class, Boolean.TYPE)); + addOverride(new ParamOverride(Flowable.class, 0, ParamMode.ANY, "sample", Long.TYPE, TimeUnit.class, Scheduler.class)); + addOverride(new ParamOverride(Flowable.class, 0, ParamMode.ANY, "sample", Long.TYPE, TimeUnit.class, Scheduler.class, Boolean.TYPE)); + addOverride(new ParamOverride(Flowable.class, 0, ParamMode.ANY, "sample", Long.TYPE, TimeUnit.class, Scheduler.class, Boolean.TYPE, Consumer.class)); + + // negative time is considered as zero time + addOverride(new ParamOverride(Flowable.class, 0, ParamMode.ANY, "takeLast", Long.TYPE, TimeUnit.class)); + addOverride(new ParamOverride(Flowable.class, 0, ParamMode.ANY, "takeLast", Long.TYPE, TimeUnit.class, Boolean.TYPE)); + addOverride(new ParamOverride(Flowable.class, 0, ParamMode.ANY, "takeLast", Long.TYPE, TimeUnit.class, Scheduler.class)); + addOverride(new ParamOverride(Flowable.class, 0, ParamMode.ANY, "takeLast", Long.TYPE, TimeUnit.class, Scheduler.class, Boolean.TYPE)); + addOverride(new ParamOverride(Flowable.class, 0, ParamMode.ANY, "takeLast", Long.TYPE, TimeUnit.class, Scheduler.class, Boolean.TYPE, Integer.TYPE)); + addOverride(new ParamOverride(Flowable.class, 0, ParamMode.NON_NEGATIVE, "takeLast", Long.TYPE, Long.TYPE, TimeUnit.class)); + addOverride(new ParamOverride(Flowable.class, 0, ParamMode.NON_NEGATIVE, "takeLast", Long.TYPE, Long.TYPE, TimeUnit.class, Scheduler.class)); + addOverride(new ParamOverride(Flowable.class, 0, ParamMode.NON_NEGATIVE, "takeLast", Long.TYPE, Long.TYPE, TimeUnit.class, Scheduler.class, Boolean.TYPE, Integer.TYPE)); + addOverride(new ParamOverride(Flowable.class, 1, ParamMode.ANY, "takeLast", Long.TYPE, Long.TYPE, TimeUnit.class)); + addOverride(new ParamOverride(Flowable.class, 1, ParamMode.ANY, "takeLast", Long.TYPE, Long.TYPE, TimeUnit.class, Scheduler.class)); + addOverride(new ParamOverride(Flowable.class, 1, ParamMode.ANY, "takeLast", Long.TYPE, Long.TYPE, TimeUnit.class, Scheduler.class, Boolean.TYPE, Integer.TYPE)); + + // take last 0 is allowed + addOverride(new ParamOverride(Flowable.class, 0, ParamMode.NON_NEGATIVE, "takeLast", Integer.TYPE)); + + // skip last 0 is allowed + addOverride(new ParamOverride(Flowable.class, 0, ParamMode.NON_NEGATIVE, "skipLast", Integer.TYPE)); + + // negative time is considered as zero time + addOverride(new ParamOverride(Flowable.class, 0, ParamMode.ANY, "skipLast", Long.TYPE, TimeUnit.class)); + addOverride(new ParamOverride(Flowable.class, 0, ParamMode.ANY, "skipLast", Long.TYPE, TimeUnit.class, Boolean.TYPE)); + addOverride(new ParamOverride(Flowable.class, 0, ParamMode.ANY, "skipLast", Long.TYPE, TimeUnit.class, Scheduler.class)); + addOverride(new ParamOverride(Flowable.class, 0, ParamMode.ANY, "skipLast", Long.TYPE, TimeUnit.class, Scheduler.class, Boolean.TYPE)); + addOverride(new ParamOverride(Flowable.class, 0, ParamMode.ANY, "skipLast", Long.TYPE, TimeUnit.class, Scheduler.class, Boolean.TYPE, Integer.TYPE)); + + // negative time is considered as zero time + addOverride(new ParamOverride(Flowable.class, 0, ParamMode.ANY, "throttleFirst", Long.TYPE, TimeUnit.class)); + addOverride(new ParamOverride(Flowable.class, 0, ParamMode.ANY, "throttleFirst", Long.TYPE, TimeUnit.class, Scheduler.class)); + addOverride(new ParamOverride(Flowable.class, 0, ParamMode.ANY, "throttleFirst", Long.TYPE, TimeUnit.class, Scheduler.class, Consumer.class)); + + // negative time is considered as zero time + addOverride(new ParamOverride(Flowable.class, 0, ParamMode.ANY, "throttleLast", Long.TYPE, TimeUnit.class)); + addOverride(new ParamOverride(Flowable.class, 0, ParamMode.ANY, "throttleLast", Long.TYPE, TimeUnit.class, Scheduler.class)); + addOverride(new ParamOverride(Flowable.class, 0, ParamMode.ANY, "throttleLast", Long.TYPE, TimeUnit.class, Scheduler.class, Consumer.class)); + + // negative time is considered as zero time + addOverride(new ParamOverride(Flowable.class, 0, ParamMode.ANY, "throttleLatest", Long.TYPE, TimeUnit.class)); + addOverride(new ParamOverride(Flowable.class, 0, ParamMode.ANY, "throttleLatest", Long.TYPE, TimeUnit.class, Scheduler.class)); + addOverride(new ParamOverride(Flowable.class, 0, ParamMode.ANY, "throttleLatest", Long.TYPE, TimeUnit.class, Boolean.TYPE)); + addOverride(new ParamOverride(Flowable.class, 0, ParamMode.ANY, "throttleLatest", Long.TYPE, TimeUnit.class, Scheduler.class, Boolean.TYPE)); + addOverride(new ParamOverride(Flowable.class, 0, ParamMode.ANY, "throttleLatest", Long.TYPE, TimeUnit.class, Scheduler.class, Boolean.TYPE, Consumer.class)); + + // negative buffer time is considered as zero buffer time + addOverride(new ParamOverride(Flowable.class, 0, ParamMode.ANY, "window", Long.TYPE, TimeUnit.class)); + addOverride(new ParamOverride(Flowable.class, 0, ParamMode.ANY, "window", Long.TYPE, TimeUnit.class, Long.TYPE)); + addOverride(new ParamOverride(Flowable.class, 0, ParamMode.ANY, "window", Long.TYPE, TimeUnit.class, Long.TYPE, Boolean.TYPE)); + addOverride(new ParamOverride(Flowable.class, 0, ParamMode.ANY, "window", Long.TYPE, TimeUnit.class, Scheduler.class, Long.TYPE)); + addOverride(new ParamOverride(Flowable.class, 0, ParamMode.ANY, "window", Long.TYPE, TimeUnit.class, Scheduler.class)); + addOverride(new ParamOverride(Flowable.class, 0, ParamMode.ANY, "window", Long.TYPE, TimeUnit.class, Scheduler.class, Long.TYPE, Boolean.TYPE)); + addOverride(new ParamOverride(Flowable.class, 0, ParamMode.ANY, "window", Long.TYPE, TimeUnit.class, Scheduler.class, Long.TYPE, Boolean.TYPE, Integer.TYPE)); + + // *********************************************************************************************************************** + + // negative timeout time is considered as zero timeout time + addOverride(new ParamOverride(Completable.class, 0, ParamMode.ANY, "timeout", Long.TYPE, TimeUnit.class)); + addOverride(new ParamOverride(Completable.class, 0, ParamMode.ANY, "timeout", Long.TYPE, TimeUnit.class, Scheduler.class)); + addOverride(new ParamOverride(Completable.class, 0, ParamMode.ANY, "timeout", Long.TYPE, TimeUnit.class, CompletableSource.class)); + addOverride(new ParamOverride(Completable.class, 0, ParamMode.ANY, "timeout", Long.TYPE, TimeUnit.class, Scheduler.class, CompletableSource.class)); + + // negative time is considered as zero time + addOverride(new ParamOverride(Completable.class, 0, ParamMode.ANY, "timer", Long.TYPE, TimeUnit.class)); + addOverride(new ParamOverride(Completable.class, 0, ParamMode.ANY, "timer", Long.TYPE, TimeUnit.class, Scheduler.class)); + + // negative time is considered as zero time + addOverride(new ParamOverride(Completable.class, 0, ParamMode.ANY, "delay", Long.TYPE, TimeUnit.class)); + addOverride(new ParamOverride(Completable.class, 0, ParamMode.ANY, "delay", Long.TYPE, TimeUnit.class, Scheduler.class)); + addOverride(new ParamOverride(Completable.class, 0, ParamMode.ANY, "delay", Long.TYPE, TimeUnit.class, Scheduler.class, Boolean.TYPE)); + + // negative time is considered as zero time + addOverride(new ParamOverride(Completable.class, 0, ParamMode.ANY, "delaySubscription", Long.TYPE, TimeUnit.class)); + addOverride(new ParamOverride(Completable.class, 0, ParamMode.ANY, "delaySubscription", Long.TYPE, TimeUnit.class, Scheduler.class)); + + // zero repeat is allowed + addOverride(new ParamOverride(Completable.class, 0, ParamMode.NON_NEGATIVE, "repeat", Long.TYPE)); + + // zero retry is allowed + addOverride(new ParamOverride(Completable.class, 0, ParamMode.NON_NEGATIVE, "retry", Long.TYPE)); + addOverride(new ParamOverride(Completable.class, 0, ParamMode.NON_NEGATIVE, "retry", Long.TYPE, Predicate.class)); + + // negative time is considered as zero time + addOverride(new ParamOverride(Completable.class, 0, ParamMode.ANY, "blockingAwait", Long.TYPE, TimeUnit.class)); + + // *********************************************************************************************************************** + + // negative timeout time is considered as zero timeout time + addOverride(new ParamOverride(Maybe.class, 0, ParamMode.ANY, "timeout", Long.TYPE, TimeUnit.class)); + addOverride(new ParamOverride(Maybe.class, 0, ParamMode.ANY, "timeout", Long.TYPE, TimeUnit.class, Scheduler.class)); + addOverride(new ParamOverride(Maybe.class, 0, ParamMode.ANY, "timeout", Long.TYPE, TimeUnit.class, MaybeSource.class)); + addOverride(new ParamOverride(Maybe.class, 0, ParamMode.ANY, "timeout", Long.TYPE, TimeUnit.class, Scheduler.class, MaybeSource.class)); + + // negative time is considered as zero time + addOverride(new ParamOverride(Maybe.class, 0, ParamMode.ANY, "timer", Long.TYPE, TimeUnit.class)); + addOverride(new ParamOverride(Maybe.class, 0, ParamMode.ANY, "timer", Long.TYPE, TimeUnit.class, Scheduler.class)); + + // negative timeout is allowed + addOverride(new ParamOverride(Maybe.class, 1, ParamMode.ANY, "fromFuture", Future.class, Long.TYPE, TimeUnit.class)); + + // negative time is considered as zero time + addOverride(new ParamOverride(Maybe.class, 0, ParamMode.ANY, "delay", Long.TYPE, TimeUnit.class)); + addOverride(new ParamOverride(Maybe.class, 0, ParamMode.ANY, "delay", Long.TYPE, TimeUnit.class, Scheduler.class)); + addOverride(new ParamOverride(Maybe.class, 0, ParamMode.ANY, "delay", Long.TYPE, TimeUnit.class, Boolean.TYPE)); + addOverride(new ParamOverride(Maybe.class, 0, ParamMode.ANY, "delay", Long.TYPE, TimeUnit.class, Scheduler.class, Boolean.TYPE)); + + // zero repeat is allowed + addOverride(new ParamOverride(Maybe.class, 0, ParamMode.NON_NEGATIVE, "repeat", Long.TYPE)); + + // zero retry is allowed + addOverride(new ParamOverride(Maybe.class, 0, ParamMode.NON_NEGATIVE, "retry", Long.TYPE)); + addOverride(new ParamOverride(Maybe.class, 0, ParamMode.NON_NEGATIVE, "retry", Long.TYPE, Predicate.class)); + + // negative time is considered as zero time + addOverride(new ParamOverride(Maybe.class, 0, ParamMode.ANY, "delaySubscription", Long.TYPE, TimeUnit.class)); + addOverride(new ParamOverride(Maybe.class, 0, ParamMode.ANY, "delaySubscription", Long.TYPE, TimeUnit.class, Scheduler.class)); + + // *********************************************************************************************************************** + + // negative timeout time is considered as zero timeout time + addOverride(new ParamOverride(Single.class, 0, ParamMode.ANY, "timeout", Long.TYPE, TimeUnit.class)); + addOverride(new ParamOverride(Single.class, 0, ParamMode.ANY, "timeout", Long.TYPE, TimeUnit.class, Scheduler.class)); + addOverride(new ParamOverride(Single.class, 0, ParamMode.ANY, "timeout", Long.TYPE, TimeUnit.class, SingleSource.class)); + addOverride(new ParamOverride(Single.class, 0, ParamMode.ANY, "timeout", Long.TYPE, TimeUnit.class, Scheduler.class, SingleSource.class)); + + // negative time is considered as zero time + addOverride(new ParamOverride(Single.class, 0, ParamMode.ANY, "timer", Long.TYPE, TimeUnit.class)); + addOverride(new ParamOverride(Single.class, 0, ParamMode.ANY, "timer", Long.TYPE, TimeUnit.class, Scheduler.class)); + + // negative timeout is allowed + addOverride(new ParamOverride(Single.class, 1, ParamMode.ANY, "fromFuture", Future.class, Long.TYPE, TimeUnit.class)); + + // negative time is considered as zero time + addOverride(new ParamOverride(Single.class, 0, ParamMode.ANY, "delay", Long.TYPE, TimeUnit.class)); + addOverride(new ParamOverride(Single.class, 0, ParamMode.ANY, "delay", Long.TYPE, TimeUnit.class, Boolean.TYPE)); + addOverride(new ParamOverride(Single.class, 0, ParamMode.ANY, "delay", Long.TYPE, TimeUnit.class, Scheduler.class)); + addOverride(new ParamOverride(Single.class, 0, ParamMode.ANY, "delay", Long.TYPE, TimeUnit.class, Scheduler.class, Boolean.TYPE)); + + // zero repeat is allowed + addOverride(new ParamOverride(Single.class, 0, ParamMode.NON_NEGATIVE, "repeat", Long.TYPE)); + + // zero retry is allowed + addOverride(new ParamOverride(Single.class, 0, ParamMode.NON_NEGATIVE, "retry", Long.TYPE)); + addOverride(new ParamOverride(Single.class, 0, ParamMode.NON_NEGATIVE, "retry", Long.TYPE, Predicate.class)); + + // negative time is considered as zero time + addOverride(new ParamOverride(Single.class, 0, ParamMode.ANY, "delaySubscription", Long.TYPE, TimeUnit.class)); + addOverride(new ParamOverride(Single.class, 0, ParamMode.ANY, "delaySubscription", Long.TYPE, TimeUnit.class, Scheduler.class)); + + // *********************************************************************************************************************** + + // zero index allowed + addOverride(new ParamOverride(Observable.class, 0, ParamMode.NON_NEGATIVE, "elementAt", Long.TYPE)); + addOverride(new ParamOverride(Observable.class, 0, ParamMode.NON_NEGATIVE, "elementAt", Long.TYPE, Object.class)); + + addOverride(new ParamOverride(Observable.class, 0, ParamMode.NON_NEGATIVE, "elementAtOrError", Long.TYPE)); + + // negative skip count is ignored + addOverride(new ParamOverride(Observable.class, 0, ParamMode.NON_NEGATIVE, "skip", Long.TYPE)); + // negative skip time is considered as zero skip time + addOverride(new ParamOverride(Observable.class, 0, ParamMode.ANY, "skip", Long.TYPE, TimeUnit.class)); + addOverride(new ParamOverride(Observable.class, 0, ParamMode.ANY, "skip", Long.TYPE, TimeUnit.class, Scheduler.class)); + + // negative timeout time is considered as zero timeout time + addOverride(new ParamOverride(Observable.class, 0, ParamMode.ANY, "timeout", Long.TYPE, TimeUnit.class)); + addOverride(new ParamOverride(Observable.class, 0, ParamMode.ANY, "timeout", Long.TYPE, TimeUnit.class, Scheduler.class)); + addOverride(new ParamOverride(Observable.class, 0, ParamMode.ANY, "timeout", Long.TYPE, TimeUnit.class, ObservableSource.class)); + addOverride(new ParamOverride(Observable.class, 0, ParamMode.ANY, "timeout", Long.TYPE, TimeUnit.class, Scheduler.class, ObservableSource.class)); + + // negative buffer time is considered as zero buffer time + addOverride(new ParamOverride(Observable.class, 0, ParamMode.ANY, "buffer", Long.TYPE, TimeUnit.class)); + addOverride(new ParamOverride(Observable.class, 0, ParamMode.ANY, "buffer", Long.TYPE, TimeUnit.class, Integer.TYPE)); + addOverride(new ParamOverride(Observable.class, 0, ParamMode.ANY, "buffer", Long.TYPE, TimeUnit.class, Scheduler.class)); + addOverride(new ParamOverride(Observable.class, 0, ParamMode.ANY, "buffer", Long.TYPE, TimeUnit.class, Scheduler.class, Integer.TYPE)); + addOverride(new ParamOverride(Observable.class, 0, ParamMode.ANY, "buffer", Long.TYPE, TimeUnit.class, Scheduler.class, Integer.TYPE, Supplier.class, Boolean.TYPE)); + + // negative time/skip is considered zero time/skip + addOverride(new ParamOverride(Observable.class, 0, ParamMode.ANY, "buffer", Long.TYPE, Long.TYPE, TimeUnit.class)); + addOverride(new ParamOverride(Observable.class, 0, ParamMode.ANY, "buffer", Long.TYPE, Long.TYPE, TimeUnit.class, Scheduler.class)); + addOverride(new ParamOverride(Observable.class, 0, ParamMode.ANY, "buffer", Long.TYPE, Long.TYPE, TimeUnit.class, Scheduler.class, Supplier.class)); + addOverride(new ParamOverride(Observable.class, 1, ParamMode.ANY, "buffer", Long.TYPE, Long.TYPE, TimeUnit.class)); + addOverride(new ParamOverride(Observable.class, 1, ParamMode.ANY, "buffer", Long.TYPE, Long.TYPE, TimeUnit.class, Scheduler.class)); + addOverride(new ParamOverride(Observable.class, 1, ParamMode.ANY, "buffer", Long.TYPE, Long.TYPE, TimeUnit.class, Scheduler.class, Supplier.class)); + + // negative timeout is allowed + addOverride(new ParamOverride(Observable.class, 1, ParamMode.ANY, "fromFuture", Future.class, Long.TYPE, TimeUnit.class)); + + // negative time is considered as zero time + addOverride(new ParamOverride(Observable.class, 0, ParamMode.ANY, "timer", Long.TYPE, TimeUnit.class)); + addOverride(new ParamOverride(Observable.class, 0, ParamMode.ANY, "timer", Long.TYPE, TimeUnit.class, Scheduler.class)); + addOverride(new ParamOverride(Observable.class, 0, ParamMode.ANY, "interval", Long.TYPE, TimeUnit.class)); + addOverride(new ParamOverride(Observable.class, 0, ParamMode.ANY, "interval", Long.TYPE, TimeUnit.class, Scheduler.class)); + addOverride(new ParamOverride(Observable.class, 0, ParamMode.ANY, "interval", Long.TYPE, Long.TYPE, TimeUnit.class)); + addOverride(new ParamOverride(Observable.class, 0, ParamMode.ANY, "interval", Long.TYPE, Long.TYPE, TimeUnit.class, Scheduler.class)); + addOverride(new ParamOverride(Observable.class, 1, ParamMode.ANY, "interval", Long.TYPE, Long.TYPE, TimeUnit.class)); + addOverride(new ParamOverride(Observable.class, 1, ParamMode.ANY, "interval", Long.TYPE, Long.TYPE, TimeUnit.class, Scheduler.class)); + + // negative time is considered as zero time + addOverride(new ParamOverride(Observable.class, 0, ParamMode.ANY, "delay", Long.TYPE, TimeUnit.class)); + addOverride(new ParamOverride(Observable.class, 0, ParamMode.ANY, "delay", Long.TYPE, TimeUnit.class, Boolean.TYPE)); + addOverride(new ParamOverride(Observable.class, 0, ParamMode.ANY, "delay", Long.TYPE, TimeUnit.class, Scheduler.class)); + addOverride(new ParamOverride(Observable.class, 0, ParamMode.ANY, "delay", Long.TYPE, TimeUnit.class, Scheduler.class, Boolean.TYPE)); + + // negative time is considered as zero time + addOverride(new ParamOverride(Observable.class, 0, ParamMode.ANY, "delaySubscription", Long.TYPE, TimeUnit.class)); + addOverride(new ParamOverride(Observable.class, 0, ParamMode.ANY, "delaySubscription", Long.TYPE, TimeUnit.class, Scheduler.class)); + + // negative time is considered as zero time + addOverride(new ParamOverride(Observable.class, 0, ParamMode.ANY, "debounce", Long.TYPE, TimeUnit.class)); + addOverride(new ParamOverride(Observable.class, 0, ParamMode.ANY, "debounce", Long.TYPE, TimeUnit.class, Scheduler.class)); + addOverride(new ParamOverride(Observable.class, 0, ParamMode.ANY, "debounce", Long.TYPE, TimeUnit.class, Scheduler.class, Consumer.class)); + + // zero repeat is allowed + addOverride(new ParamOverride(Observable.class, 0, ParamMode.NON_NEGATIVE, "repeat", Long.TYPE)); + + // negative time is considered as zero time + addOverride(new ParamOverride(Observable.class, 0, ParamMode.ANY, "replay", Long.TYPE, TimeUnit.class)); + addOverride(new ParamOverride(Observable.class, 0, ParamMode.ANY, "replay", Long.TYPE, TimeUnit.class, Scheduler.class)); + addOverride(new ParamOverride(Observable.class, 0, ParamMode.ANY, "replay", Long.TYPE, TimeUnit.class, Scheduler.class, boolean.class)); + addOverride(new ParamOverride(Observable.class, 1, ParamMode.ANY, "replay", Integer.TYPE, Long.TYPE, TimeUnit.class)); + addOverride(new ParamOverride(Observable.class, 1, ParamMode.ANY, "replay", Integer.TYPE, Long.TYPE, TimeUnit.class, Scheduler.class)); + addOverride(new ParamOverride(Observable.class, 1, ParamMode.ANY, "replay", Integer.TYPE, Long.TYPE, TimeUnit.class, Scheduler.class, boolean.class)); + addOverride(new ParamOverride(Observable.class, 1, ParamMode.ANY, "replay", Function.class, Long.TYPE, TimeUnit.class)); + addOverride(new ParamOverride(Observable.class, 1, ParamMode.ANY, "replay", Function.class, Long.TYPE, TimeUnit.class, Scheduler.class)); + addOverride(new ParamOverride(Observable.class, 1, ParamMode.ANY, "replay", Function.class, Long.TYPE, TimeUnit.class, Scheduler.class, boolean.class)); + addOverride(new ParamOverride(Observable.class, 2, ParamMode.ANY, "replay", Function.class, Integer.TYPE, Long.TYPE, TimeUnit.class)); + addOverride(new ParamOverride(Observable.class, 2, ParamMode.ANY, "replay", Function.class, Integer.TYPE, Long.TYPE, TimeUnit.class, Scheduler.class)); + addOverride(new ParamOverride(Observable.class, 2, ParamMode.ANY, "replay", Function.class, Integer.TYPE, Long.TYPE, TimeUnit.class, Scheduler.class, boolean.class)); + + // zero retry is allowed + addOverride(new ParamOverride(Observable.class, 0, ParamMode.NON_NEGATIVE, "retry", Long.TYPE)); + addOverride(new ParamOverride(Observable.class, 0, ParamMode.NON_NEGATIVE, "retry", Long.TYPE, Predicate.class)); + + // negative time is considered as zero time + addOverride(new ParamOverride(Observable.class, 0, ParamMode.ANY, "throttleWithTimeout", Long.TYPE, TimeUnit.class)); + addOverride(new ParamOverride(Observable.class, 0, ParamMode.ANY, "throttleWithTimeout", Long.TYPE, TimeUnit.class, Scheduler.class)); + addOverride(new ParamOverride(Observable.class, 0, ParamMode.ANY, "throttleWithTimeout", Long.TYPE, TimeUnit.class, Scheduler.class, Consumer.class)); + + // negative time is considered as zero time + addOverride(new ParamOverride(Observable.class, 0, ParamMode.ANY, "take", Long.TYPE, TimeUnit.class)); + addOverride(new ParamOverride(Observable.class, 0, ParamMode.ANY, "take", Long.TYPE, TimeUnit.class, Scheduler.class)); + + // zero retry is allowed + addOverride(new ParamOverride(Observable.class, 0, ParamMode.NON_NEGATIVE, "take", Long.TYPE)); + + // negative time is considered as zero time + addOverride(new ParamOverride(Observable.class, 0, ParamMode.ANY, "sample", Long.TYPE, TimeUnit.class)); + addOverride(new ParamOverride(Observable.class, 0, ParamMode.ANY, "sample", Long.TYPE, TimeUnit.class, Boolean.TYPE)); + addOverride(new ParamOverride(Observable.class, 0, ParamMode.ANY, "sample", Long.TYPE, TimeUnit.class, Scheduler.class)); + addOverride(new ParamOverride(Observable.class, 0, ParamMode.ANY, "sample", Long.TYPE, TimeUnit.class, Scheduler.class, Boolean.TYPE)); + addOverride(new ParamOverride(Observable.class, 0, ParamMode.ANY, "sample", Long.TYPE, TimeUnit.class, Scheduler.class, Boolean.TYPE, Consumer.class)); + + // negative time is considered as zero time + addOverride(new ParamOverride(Observable.class, 0, ParamMode.ANY, "takeLast", Long.TYPE, TimeUnit.class)); + addOverride(new ParamOverride(Observable.class, 0, ParamMode.ANY, "takeLast", Long.TYPE, TimeUnit.class, Boolean.TYPE)); + addOverride(new ParamOverride(Observable.class, 0, ParamMode.ANY, "takeLast", Long.TYPE, TimeUnit.class, Scheduler.class)); + addOverride(new ParamOverride(Observable.class, 0, ParamMode.ANY, "takeLast", Long.TYPE, TimeUnit.class, Scheduler.class, Boolean.TYPE)); + addOverride(new ParamOverride(Observable.class, 0, ParamMode.ANY, "takeLast", Long.TYPE, TimeUnit.class, Scheduler.class, Boolean.TYPE, Integer.TYPE)); + addOverride(new ParamOverride(Observable.class, 0, ParamMode.NON_NEGATIVE, "takeLast", Long.TYPE, Long.TYPE, TimeUnit.class)); + addOverride(new ParamOverride(Observable.class, 0, ParamMode.NON_NEGATIVE, "takeLast", Long.TYPE, Long.TYPE, TimeUnit.class, Scheduler.class)); + addOverride(new ParamOverride(Observable.class, 0, ParamMode.NON_NEGATIVE, "takeLast", Long.TYPE, Long.TYPE, TimeUnit.class, Scheduler.class, Boolean.TYPE, Integer.TYPE)); + addOverride(new ParamOverride(Observable.class, 1, ParamMode.ANY, "takeLast", Long.TYPE, Long.TYPE, TimeUnit.class)); + addOverride(new ParamOverride(Observable.class, 1, ParamMode.ANY, "takeLast", Long.TYPE, Long.TYPE, TimeUnit.class, Scheduler.class)); + addOverride(new ParamOverride(Observable.class, 1, ParamMode.ANY, "takeLast", Long.TYPE, Long.TYPE, TimeUnit.class, Scheduler.class, Boolean.TYPE, Integer.TYPE)); + + // take last 0 is allowed + addOverride(new ParamOverride(Observable.class, 0, ParamMode.NON_NEGATIVE, "takeLast", Integer.TYPE)); + + // skip last 0 is allowed + addOverride(new ParamOverride(Observable.class, 0, ParamMode.NON_NEGATIVE, "skipLast", Integer.TYPE)); + + // negative time is considered as zero time + addOverride(new ParamOverride(Observable.class, 0, ParamMode.ANY, "skipLast", Long.TYPE, TimeUnit.class)); + addOverride(new ParamOverride(Observable.class, 0, ParamMode.ANY, "skipLast", Long.TYPE, TimeUnit.class, Boolean.TYPE)); + addOverride(new ParamOverride(Observable.class, 0, ParamMode.ANY, "skipLast", Long.TYPE, TimeUnit.class, Scheduler.class)); + addOverride(new ParamOverride(Observable.class, 0, ParamMode.ANY, "skipLast", Long.TYPE, TimeUnit.class, Scheduler.class, Boolean.TYPE)); + addOverride(new ParamOverride(Observable.class, 0, ParamMode.ANY, "skipLast", Long.TYPE, TimeUnit.class, Scheduler.class, Boolean.TYPE, Integer.TYPE)); + + // negative time is considered as zero time + addOverride(new ParamOverride(Observable.class, 0, ParamMode.ANY, "throttleFirst", Long.TYPE, TimeUnit.class)); + addOverride(new ParamOverride(Observable.class, 0, ParamMode.ANY, "throttleFirst", Long.TYPE, TimeUnit.class, Scheduler.class)); + addOverride(new ParamOverride(Observable.class, 0, ParamMode.ANY, "throttleFirst", Long.TYPE, TimeUnit.class, Scheduler.class, Consumer.class)); + + // negative time is considered as zero time + addOverride(new ParamOverride(Observable.class, 0, ParamMode.ANY, "throttleLast", Long.TYPE, TimeUnit.class)); + addOverride(new ParamOverride(Observable.class, 0, ParamMode.ANY, "throttleLast", Long.TYPE, TimeUnit.class, Scheduler.class)); + addOverride(new ParamOverride(Observable.class, 0, ParamMode.ANY, "throttleLast", Long.TYPE, TimeUnit.class, Scheduler.class, Consumer.class)); + + // negative time is considered as zero time + addOverride(new ParamOverride(Observable.class, 0, ParamMode.ANY, "throttleLatest", Long.TYPE, TimeUnit.class)); + addOverride(new ParamOverride(Observable.class, 0, ParamMode.ANY, "throttleLatest", Long.TYPE, TimeUnit.class, Scheduler.class)); + addOverride(new ParamOverride(Observable.class, 0, ParamMode.ANY, "throttleLatest", Long.TYPE, TimeUnit.class, Boolean.TYPE)); + addOverride(new ParamOverride(Observable.class, 0, ParamMode.ANY, "throttleLatest", Long.TYPE, TimeUnit.class, Scheduler.class, Boolean.TYPE)); + addOverride(new ParamOverride(Observable.class, 0, ParamMode.ANY, "throttleLatest", Long.TYPE, TimeUnit.class, Scheduler.class, Boolean.TYPE, Consumer.class)); + + // negative buffer time is considered as zero buffer time + addOverride(new ParamOverride(Observable.class, 0, ParamMode.ANY, "window", Long.TYPE, TimeUnit.class)); + addOverride(new ParamOverride(Observable.class, 0, ParamMode.ANY, "window", Long.TYPE, TimeUnit.class, Long.TYPE)); + addOverride(new ParamOverride(Observable.class, 0, ParamMode.ANY, "window", Long.TYPE, TimeUnit.class, Long.TYPE, Boolean.TYPE)); + addOverride(new ParamOverride(Observable.class, 0, ParamMode.ANY, "window", Long.TYPE, TimeUnit.class, Scheduler.class, Long.TYPE)); + addOverride(new ParamOverride(Observable.class, 0, ParamMode.ANY, "window", Long.TYPE, TimeUnit.class, Scheduler.class)); + addOverride(new ParamOverride(Observable.class, 0, ParamMode.ANY, "window", Long.TYPE, TimeUnit.class, Scheduler.class, Long.TYPE, Boolean.TYPE)); + addOverride(new ParamOverride(Observable.class, 0, ParamMode.ANY, "window", Long.TYPE, TimeUnit.class, Scheduler.class, Long.TYPE, Boolean.TYPE, Integer.TYPE)); + + // null value allowed + + addOverride(new ParamOverride(Flowable.class, 0, ParamMode.ANY, "firstStage", Object.class)); + addOverride(new ParamOverride(Flowable.class, 0, ParamMode.ANY, "singleStage", Object.class)); + addOverride(new ParamOverride(Flowable.class, 0, ParamMode.ANY, "lastStage", Object.class)); + + addOverride(new ParamOverride(Observable.class, 0, ParamMode.ANY, "firstStage", Object.class)); + addOverride(new ParamOverride(Observable.class, 0, ParamMode.ANY, "singleStage", Object.class)); + addOverride(new ParamOverride(Observable.class, 0, ParamMode.ANY, "lastStage", Object.class)); + + addOverride(new ParamOverride(Maybe.class, 0, ParamMode.ANY, "toCompletionStage", Object.class)); + addOverride(new ParamOverride(Completable.class, 0, ParamMode.ANY, "toCompletionStage", Object.class)); + + // ----------------------------------------------------------------------------------- + + ignores = new HashMap<>(); + + // needs special param validation due to (long)start + end - 1 <= Integer.MAX_VALUE + addIgnore(new ParamIgnore(Flowable.class, "range", Integer.TYPE, Integer.TYPE)); + addIgnore(new ParamIgnore(Flowable.class, "rangeLong", Long.TYPE, Long.TYPE)); + addIgnore(new ParamIgnore(Flowable.class, "intervalRange", Long.TYPE, Long.TYPE, Long.TYPE, TimeUnit.class)); + addIgnore(new ParamIgnore(Flowable.class, "intervalRange", Long.TYPE, Long.TYPE, Long.TYPE, Long.TYPE, TimeUnit.class)); + addIgnore(new ParamIgnore(Flowable.class, "intervalRange", Long.TYPE, Long.TYPE, Long.TYPE, TimeUnit.class, Scheduler.class)); + addIgnore(new ParamIgnore(Flowable.class, "intervalRange", Long.TYPE, Long.TYPE, Long.TYPE, Long.TYPE, TimeUnit.class, Scheduler.class)); + + addIgnore(new ParamIgnore(Flowable.class, "unsafeCreate", Publisher.class)); + + // needs special param validation due to (long)start + end - 1 <= Integer.MAX_VALUE + addIgnore(new ParamIgnore(Observable.class, "range", Integer.TYPE, Integer.TYPE)); + addIgnore(new ParamIgnore(Observable.class, "rangeLong", Long.TYPE, Long.TYPE)); + addIgnore(new ParamIgnore(Observable.class, "intervalRange", Long.TYPE, Long.TYPE, Long.TYPE, TimeUnit.class)); + addIgnore(new ParamIgnore(Observable.class, "intervalRange", Long.TYPE, Long.TYPE, Long.TYPE, Long.TYPE, TimeUnit.class)); + addIgnore(new ParamIgnore(Observable.class, "intervalRange", Long.TYPE, Long.TYPE, Long.TYPE, TimeUnit.class, Scheduler.class)); + addIgnore(new ParamIgnore(Observable.class, "intervalRange", Long.TYPE, Long.TYPE, Long.TYPE, Long.TYPE, TimeUnit.class, Scheduler.class)); + + addIgnore(new ParamIgnore(Observable.class, "unsafeCreate", ObservableSource.class)); + + addIgnore(new ParamIgnore(Maybe.class, "unsafeCreate", MaybeSource.class)); + + addIgnore(new ParamIgnore(Single.class, "unsafeCreate", SingleSource.class)); + + addIgnore(new ParamIgnore(Completable.class, "unsafeCreate", CompletableSource.class)); + + // ----------------------------------------------------------------------------------- + + defaultValues = new HashMap<>(); + + defaultValues.put(Publisher.class, new NeverPublisher()); + defaultValues.put(Flowable.class, new NeverPublisher()); + + defaultValues.put(ObservableSource.class, new NeverObservable()); + defaultValues.put(Observable.class, new NeverObservable()); + + defaultValues.put(SingleSource.class, new NeverSingle()); + defaultValues.put(Single.class, new NeverSingle()); + + defaultValues.put(MaybeSource.class, new NeverMaybe()); + defaultValues.put(Maybe.class, new NeverMaybe()); + + defaultValues.put(CompletableSource.class, new NeverCompletable()); + defaultValues.put(Completable.class, new NeverCompletable()); + + defaultValues.put(Action.class, Functions.EMPTY_ACTION); + defaultValues.put(Runnable.class, Functions.EMPTY_RUNNABLE); + defaultValues.put(Consumer.class, Functions.emptyConsumer()); + defaultValues.put(LongConsumer.class, Functions.EMPTY_LONG_CONSUMER); + defaultValues.put(Function.class, Functions.justFunction(1)); + defaultValues.put(Callable.class, Functions.justCallable(1)); + defaultValues.put(Supplier.class, Functions.justSupplier(1)); + defaultValues.put(Iterable.class, Collections.emptyList()); + defaultValues.put(Object.class, 1); + defaultValues.put(Class.class, Integer.class); + Object af = new AllFunctionals(); + for (Class<?> interfaces : AllFunctionals.class.getInterfaces()) { + defaultValues.put(interfaces, af); + } + defaultValues.put(Subscriber.class, af); + defaultValues.put(TimeUnit.class, TimeUnit.SECONDS); + defaultValues.put(Scheduler.class, Schedulers.single()); + defaultValues.put(BackpressureStrategy.class, BackpressureStrategy.MISSING); + defaultValues.put(BackpressureOverflowStrategy.class, BackpressureOverflowStrategy.ERROR); + defaultValues.put(Throwable.class, new TestException()); + + defaultValues.put(Publisher[].class, new Publisher[] { new NeverPublisher(), new NeverPublisher() }); + defaultValues.put(ObservableSource[].class, new ObservableSource[] { new NeverObservable(), new NeverObservable() }); + defaultValues.put(SingleSource[].class, new SingleSource[] { new NeverSingle(), new NeverSingle() }); + defaultValues.put(MaybeSource[].class, new MaybeSource[] { new NeverMaybe(), new NeverMaybe() }); + defaultValues.put(CompletableSource[].class, new CompletableSource[] { new NeverCompletable(), new NeverCompletable() }); + + defaultValues.put(Object[].class, new Object[] { new Object(), new Object() }); + defaultValues.put(Future.class, new FutureTask<Object>(Functions.EMPTY_RUNNABLE, 1)); + + defaultValues.put(ParallelFlowable.class, ParallelFlowable.from(Flowable.never())); + defaultValues.put(Subscriber[].class, new Subscriber[] { new AllFunctionals() }); + + defaultValues.put(ParallelFailureHandling.class, ParallelFailureHandling.ERROR); + + defaultValues.put(DisposableContainer.class, new CompositeDisposable()); + + // JDK 8 types + + defaultValues.put(Optional.class, Optional.of(1)); + defaultValues.put(CompletionStage.class, CompletableFuture.completedFuture(1)); + defaultValues.put(Stream.class, Stream.of(1, 2, 3)); + defaultValues.put(Duration.class, Duration.ofSeconds(1)); + defaultValues.put(Collector.class, Collectors.toList()); + + @SuppressWarnings("rawtypes") + class MixedConverters implements FlowableConverter, ObservableConverter, SingleConverter, + MaybeConverter, CompletableConverter, ParallelFlowableConverter { + + @Override + public Object apply(ParallelFlowable upstream) { + return upstream; + } + + @Override + public Object apply(Completable upstream) { + return upstream; + } + + @Override + public Object apply(Maybe upstream) { + return upstream; + } + + @Override + public Object apply(Single upstream) { + return upstream; + } + + @Override + public Object apply(Observable upstream) { + return upstream; + } + + @Override + public Object apply(Flowable upstream) { + return upstream; + } + } + + MixedConverters mc = new MixedConverters(); + for (Class<?> c : MixedConverters.class.getInterfaces()) { + defaultValues.put(c, mc); + } + + // ----------------------------------------------------------------------------------- + + defaultInstances = new HashMap<>(); + +// addDefaultInstance(Flowable.class, Flowable.empty(), "Empty()"); +// addDefaultInstance(Flowable.class, Flowable.empty().hide(), "Empty().Hide()"); + addDefaultInstance(Flowable.class, Flowable.just(1), "Just(1)"); + addDefaultInstance(Flowable.class, Flowable.just(1).hide(), "Just(1).Hide()"); +// addDefaultInstance(Flowable.class, Flowable.range(1, 3), "Range(1, 3)"); +// addDefaultInstance(Flowable.class, Flowable.range(1, 3).hide(), "Range(1, 3).Hide()"); + +// addDefaultInstance(Observable.class, Observable.empty(), "Empty()"); +// addDefaultInstance(Observable.class, Observable.empty().hide(), "Empty().Hide()"); + addDefaultInstance(Observable.class, Observable.just(1), "Just(1)"); + addDefaultInstance(Observable.class, Observable.just(1).hide(), "Just(1).Hide()"); +// addDefaultInstance(Observable.class, Observable.range(1, 3), "Range(1, 3)"); +// addDefaultInstance(Observable.class, Observable.range(1, 3).hide(), "Range(1, 3).Hide()"); + + addDefaultInstance(Completable.class, Completable.complete(), "Complete()"); + addDefaultInstance(Completable.class, Completable.complete().hide(), "Complete().hide()"); + + addDefaultInstance(Single.class, Single.just(1), "Just(1)"); + addDefaultInstance(Single.class, Single.just(1).hide(), "Just(1).Hide()"); + + addDefaultInstance(Maybe.class, Maybe.just(1), "Just(1)"); + addDefaultInstance(Maybe.class, Maybe.just(1).hide(), "Just(1).Hide()"); + + addDefaultInstance(ParallelFlowable.class, Flowable.just(1).parallel(), "Just(1)"); +} + + static void addIgnore(ParamIgnore ignore) { + String key = ignore.toString(); + List<ParamIgnore> list = ignores.get(key); + if (list == null) { + list = new ArrayList<>(); + ignores.put(key, list); + } + list.add(ignore); + } + + static void addOverride(ParamOverride ignore) { + String key = ignore.toString(); + List<ParamOverride> list = overrides.get(key); + if (list == null) { + list = new ArrayList<>(); + overrides.put(key, list); + } + list.add(ignore); + } + + static void addDefaultInstance(Class<?> clazz, Object o, String tag) { + List<Object> list = defaultInstances.get(clazz); + if (list == null) { + list = new ArrayList<>(); + defaultInstances.put(clazz, list); + } + list.add(o); + list.add(tag); + } + + Object defaultPrimitive(Class<?> clazz, ParamOverride override) { + if (Integer.TYPE == clazz) { + if (override != null) { + return 0; + } + return 1; + } + + if (Long.TYPE == clazz) { + if (override != null) { + return 0L; + } + return 1L; + } + + if (Boolean.TYPE == clazz) { + return true; + } + + return null; + } + + void addCheckPrimitive(Class<?> clazz, ParamOverride override, List<Object> values) { + if (Integer.TYPE == clazz) { + values.add(-2); + values.add(override != null && override.mode == ParamMode.ANY); + values.add(-1); + values.add(override != null && override.mode == ParamMode.ANY); + values.add(0); + values.add(override != null); + values.add(1); + values.add(true); // should succeed + values.add(2); + values.add(true); + } + + if (Long.TYPE == clazz) { + values.add(-2L); + values.add(override != null && override.mode == ParamMode.ANY); + values.add(-1L); + values.add(override != null && override.mode == ParamMode.ANY); + values.add(0L); + values.add(override != null); + values.add(1L); + values.add(true); // should succeed + values.add(2L); + values.add(true); + } + + if (Boolean.TYPE == clazz) { + values.add(false); + values.add(true); + values.add(true); + values.add(true); + } + } + + void checkClass(Class<?> clazz) { + List<Throwable> errors = TestHelper.trackPluginErrors(); + try { + StringBuilder b = new StringBuilder(); + int fail = 0; + + outer: + for (Method m : clazz.getMethods()) { + if (m.getDeclaringClass() != clazz) { + continue; + } + + String key = clazz.getName() + " " + m.getName(); + + List<ParamIgnore> ignoreList = ignores.get(key); + if (ignoreList != null) { + for (ParamIgnore e : ignoreList) { + if (Arrays.equals(e.arguments, m.getParameterTypes())) { + System.out.println("CheckClass - ignore: " + m); + continue outer; + } + } + } + + List<ParamOverride> overrideList = overrides.get(key); + + List<Object> baseObjects = new ArrayList<>(); + + if ((m.getModifiers() & Modifier.STATIC) != 0) { + baseObjects.add(null); + baseObjects.add("NULL"); + } else { + List<Object> defaultInstancesList = defaultInstances.get(clazz); + if (defaultInstancesList == null) { + b.append("\r\nNo default instances for " + clazz); + fail++; + continue outer; + } + baseObjects.addAll(defaultInstancesList); + } + + for (int ii = 0; ii < baseObjects.size(); ii += 2) { + Object baseObject = baseObjects.get(ii); + Object tag = baseObjects.get(ii + 1); + Class<?>[] params = m.getParameterTypes(); + int n = params.length; + + for (int i = 0; i < n; i++) { + ParamOverride overrideEntry = null; + if (overrideList != null) { + for (ParamOverride e : overrideList) { + if (e.index == i && Arrays.equals(e.arguments, params)) { + overrideEntry = e; + break; + } + } + } + + Class<?> entryClass = params[i]; + + Object[] callParams = new Object[n]; + + for (int j = 0; j < n; j++) { + if (j != i) { + if (params[j].isPrimitive()) { + ParamOverride overrideParam = null; + if (overrideList != null) { + for (ParamOverride e : overrideList) { + if (e.index == j && Arrays.equals(e.arguments, params)) { + overrideParam = e; + break; + } + } + } + Object def = defaultPrimitive(params[j], overrideParam); + if (def == null) { + b.append("\r\nMissing default non-null value for " + m + " # " + j + " (" + params[j] + ")"); + fail++; + continue outer; + } + callParams[j] = def; + } else { + Object def = defaultValues.get(params[j]); + if (def == null) { + b.append("\r\nMissing default non-null value for " + m + " # " + j + " (" + params[j] + ")"); + fail++; + continue outer; + } + callParams[j] = def; + } + } + } + + List<Object> entryValues = new ArrayList<>(); + + if (entryClass.isPrimitive()) { + addCheckPrimitive(params[i], overrideEntry, entryValues); + } else { + entryValues.add(null); + entryValues.add(overrideEntry != null && overrideEntry.mode == ParamMode.ANY); + + Object def = defaultValues.get(params[i]); + if (def == null) { + b.append("\r\nMissing default non-null value for " + m + " # " + i + " (" + params[i] + ")"); + fail++; + continue outer; + } + entryValues.add(def); + entryValues.add(true); + } + + for (int k = 0; k < entryValues.size(); k += 2) { + Object[] callParams2 = callParams.clone(); + + Object p = entryValues.get(k); + callParams2[i] = p; + boolean shouldSucceed = (Boolean)entryValues.get(k + 1); + + boolean success = false; + Throwable error = null; + errors.clear(); + try { + m.invoke(baseObject, callParams2); + success = true; + } catch (Throwable ex) { + // let it fail + error = ex; + } + + if (!success && error.getCause() instanceof NullPointerException) { + if (!error.getCause().toString().contains("is null")) { + fail++; + b.append("\r\nNPEs should indicate which argument failed: " + m + " # " + i + " = " + p + ", tag = " + tag + ", params = " + Arrays.toString(callParams2)); + } + } + if (success != shouldSucceed) { + fail++; + if (shouldSucceed) { + b.append("\r\nFailed (should have succeeded): " + m + " # " + i + " = " + p + ", tag = " + tag + ", params = " + Arrays.toString(callParams2)); + b.append("\r\n ").append(error); + if (error.getCause() != null) { + b.append("\r\n ").append(error.getCause()); + } + } else { + b.append("\r\nNo failure (should have failed): " + m + " # " + i + " = " + p + ", tag = " + tag + ", params = " + Arrays.toString(callParams2)); + } + continue outer; + } + if (!errors.isEmpty()) { + fail++; + b.append("\r\nUndeliverable errors:"); + for (Throwable err : errors) { + b.append("\r\n ").append(err); + if (err.getCause() != null) { + b.append("\r\n ").append(err.getCause()); + } + } + continue outer; + } + } + } + } + } + + if (fail != 0) { + throw new AssertionError("Parameter validation problems: " + fail + b.toString()); + } + } finally { + RxJavaPlugins.reset(); + } + } + + @SuppressWarnings("rawtypes") + static final class AllFunctionals + implements BiFunction, BiConsumer, + Predicate, BiPredicate, BooleanSupplier, + Function3, Function4, Function5, Function6, Function7, Function8, Function9, + FlowableOnSubscribe, ObservableOnSubscribe, SingleOnSubscribe, MaybeOnSubscribe, CompletableOnSubscribe, + FlowableTransformer, ObservableTransformer, SingleTransformer, MaybeTransformer, CompletableTransformer, + FlowableSubscriber, Observer, SingleObserver, MaybeObserver, CompletableObserver, + FlowableOperator, ObservableOperator, SingleOperator, MaybeOperator, CompletableOperator, + Comparator, ParallelTransformer + { + + @Override + public ParallelFlowable apply(ParallelFlowable upstream) { + return null; + } + + @Override + public Object apply(Object t1, Object t2, Object t3, Object t4, Object t5, Object t6, Object t7, Object t8, + Object t9) throws Exception { + return null; + } + + @Override + public Object apply(Object t1, Object t2, Object t3, Object t4, Object t5, Object t6, Object t7, Object t8) + throws Exception { + return null; + } + + @Override + public Object apply(Object t1, Object t2, Object t3, Object t4, Object t5, Object t6, Object t7) + throws Exception { + return null; + } + + @Override + public Object apply(Object t1, Object t2, Object t3, Object t4, Object t5, Object t6) throws Exception { + return null; + } + + @Override + public Object apply(Object t1, Object t2, Object t3, Object t4, Object t5) throws Exception { + return null; + } + + @Override + public Object apply(Object t1, Object t2, Object t3, Object t4) throws Exception { + return null; + } + + @Override + public Object apply(Object t1, Object t2, Object t3) throws Exception { + return null; + } + + @Override + public void accept(Object t1, Object t2) throws Exception { + } + + @Override + public Object apply(Object t1, Object t2) throws Exception { + return null; + } + + @Override + public void subscribe(CompletableEmitter e) throws Exception { + } + + @Override + public void subscribe(MaybeEmitter e) throws Exception { + } + + @Override + public void subscribe(SingleEmitter e) throws Exception { + } + + @Override + public void subscribe(ObservableEmitter e) throws Exception { + } + + @Override + public void subscribe(FlowableEmitter e) throws Exception { + } + + @Override + public boolean test(Object t1, Object t2) throws Exception { + return false; + } + + @Override + public boolean test(Object t) throws Exception { + return false; + } + + @Override + public void onSuccess(Object t) { + } + + @Override + public void onSubscribe(Disposable d) { + } + + @Override + public void onSubscribe(Subscription s) { + s.request(Long.MAX_VALUE); + } + + @Override + public void onNext(Object t) { + } + + @Override + public void onError(Throwable t) { + } + + @Override + public void onComplete() { + } + + @Override + public CompletableSource apply(Completable upstream) { + return upstream; + } + + @Override + public MaybeSource apply(Maybe upstream) { + return upstream; + } + + @Override + public SingleSource apply(Single upstream) { + return upstream; + } + + @Override + public ObservableSource apply(Observable upstream) { + return upstream; + } + + @Override + public Publisher apply(Flowable upstream) { + return upstream; + } + + @Override + public CompletableObserver apply(CompletableObserver observer) throws Exception { + return observer; + } + + @Override + public MaybeObserver apply(MaybeObserver observer) throws Exception { + return observer; + } + + @Override + public SingleObserver apply(SingleObserver observer) throws Exception { + return observer; + } + + @Override + public Observer apply(Observer observer) throws Exception { + return observer; + } + + @Override + public Subscriber apply(Subscriber observer) throws Exception { + return observer; + } + + @Override + public boolean getAsBoolean() throws Exception { + return false; + } + + @Override + public int compare(Object o1, Object o2) { + return 0; + } + } + + enum ParamMode { + ANY, + NON_NEGATIVE + } + + static final class ParamIgnore { + final Class<?> clazz; + final String name; + final Class<?>[] arguments; + + ParamIgnore(Class<?> clazz, String name, Class<?>... arguments) { + this.clazz = clazz; + this.name = name; + this.arguments = arguments; + } + + @Override + public String toString() { + return clazz.getName() + " " + name; + } + } + + static final class ParamOverride { + final Class<?> clazz; + final int index; + final ParamMode mode; + final String name; + final Class<?>[] arguments; + + ParamOverride(Class<?> clazz, int index, ParamMode mode, String name, Class<?>... arguments) { + this.clazz = clazz; + this.index = index; + this.mode = mode; + this.name = name; + this.arguments = arguments; + + try { + clazz.getMethod(name, arguments); + } catch (Exception ex) { + throw new AssertionError(ex); + } + } + + @Override + public String toString() { + return clazz.getName() + " " + name; + } + } + + static final class NeverPublisher extends Flowable<Object> { + + @Override + public void subscribeActual(Subscriber<? super Object> s) { + // not invoked, the class is a placeholder default value + } + + @Override + public String toString() { + return "NeverFlowable"; + } + } + + static final class NeverObservable extends Observable<Object> { + + @Override + public void subscribeActual(Observer<? super Object> observer) { + // not invoked, the class is a placeholder default value + } + + @Override + public String toString() { + return "NeverFlowable"; + } + } + + static final class NeverSingle extends Single<Object> { + + @Override + public void subscribeActual(SingleObserver<? super Object> observer) { + // not invoked, the class is a placeholder default value + } + + @Override + public String toString() { + return "NeverSingle"; + } + } + + static final class NeverMaybe extends Maybe<Object> { + + @Override + public void subscribeActual(MaybeObserver<? super Object> observer) { + // not invoked, the class is a placeholder default value + } + + @Override + public String toString() { + return "NeverMaybe"; + } + } + static final class NeverCompletable extends Completable { + + @Override + public void subscribeActual(CompletableObserver observer) { + // not invoked, the class is a placeholder default value + } + + @Override + public String toString() { + return "NeverCompletable"; + } + } +} \ No newline at end of file diff --git a/src/test/java/io/reactivex/rxjava3/validators/ParamValidationNaming.java b/src/test/java/io/reactivex/rxjava3/validators/ParamValidationNaming.java new file mode 100644 index 0000000000..1928fbee60 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/validators/ParamValidationNaming.java @@ -0,0 +1,542 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.validators; + +import java.io.File; +import java.nio.file.Files; +import java.util.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.disposables.*; +import io.reactivex.rxjava3.flowables.ConnectableFlowable; +import io.reactivex.rxjava3.observables.ConnectableObservable; +import io.reactivex.rxjava3.parallel.ParallelFlowable; +import io.reactivex.rxjava3.processors.*; +import io.reactivex.rxjava3.schedulers.Schedulers; +import io.reactivex.rxjava3.subjects.*; +import io.reactivex.rxjava3.testsupport.TestHelper; + +/** + * Check if the parameter name in Objects.requireNonNull + * and ObjectHelper.verifyPositive calls match the parameter + * name in the message. + */ +public class ParamValidationNaming { + + @Test + public void checkCompletable() throws Exception { + processFile(Completable.class); + } + + @Test + public void checkSingle() throws Exception { + processFile(Single.class); + } + + @Test + public void checkMaybe() throws Exception { + processFile(Maybe.class); + } + + @Test + public void checkObservable() throws Exception { + processFile(Observable.class); + } + + @Test + public void checkFlowable() throws Exception { + processFile(Flowable.class); + } + + @Test + public void checkParallelFlowable() throws Exception { + processFile(ParallelFlowable.class); + } + + @Test + public void checkConnectableObservable() throws Exception { + processFile(ConnectableObservable.class); + } + + @Test + public void checkConnectableFlowable() throws Exception { + processFile(ConnectableFlowable.class); + } + + @Test + public void checkSubject() throws Exception { + processFile(Subject.class); + } + + @Test + public void checkFlowableProcessor() throws Exception { + processFile(FlowableProcessor.class); + } + + @Test + public void checkDisposable() throws Exception { + processFile(Disposable.class); + } + + @Test + public void checkScheduler() throws Exception { + processFile(Scheduler.class); + } + + @Test + public void checkSchedulers() throws Exception { + processFile(Schedulers.class); + } + + @Test + public void checkAsyncSubject() throws Exception { + processFile(AsyncSubject.class); + } + + @Test + public void checkBehaviorSubject() throws Exception { + processFile(BehaviorSubject.class); + } + + @Test + public void checkPublishSubject() throws Exception { + processFile(PublishSubject.class); + } + + @Test + public void checkReplaySubject() throws Exception { + processFile(ReplaySubject.class); + } + + @Test + public void checkUnicastSubject() throws Exception { + processFile(UnicastSubject.class); + } + + @Test + public void checkSingleSubject() throws Exception { + processFile(SingleSubject.class); + } + + @Test + public void checkMaybeSubject() throws Exception { + processFile(MaybeSubject.class); + } + + @Test + public void checkCompletableSubject() throws Exception { + processFile(CompletableSubject.class); + } + + @Test + public void checkAsyncProcessor() throws Exception { + processFile(AsyncProcessor.class); + } + + @Test + public void checkBehaviorProcessor() throws Exception { + processFile(BehaviorProcessor.class); + } + + @Test + public void checkPublishProcessor() throws Exception { + processFile(PublishProcessor.class); + } + + @Test + public void checkReplayProcessor() throws Exception { + processFile(ReplayProcessor.class); + } + + @Test + public void checkUnicastProcessor() throws Exception { + processFile(UnicastProcessor.class); + } + + @Test + public void checkMulticastProcessor() throws Exception { + processFile(MulticastProcessor.class); + } + + @Test + public void checkCompositeDisposable() throws Exception { + processFile(CompositeDisposable.class); + } + + static void processFile(Class<?> clazz) throws Exception { + String baseClassName = clazz.getSimpleName(); + File f = TestHelper.findSource(baseClassName, clazz.getPackage().getName()); + if (f == null) { + return; + } + String fullClassName = clazz.getName(); + + int errorCount = 0; + StringBuilder errors = new StringBuilder(); + + List<String> lines = Files.readAllLines(f.toPath()); + + for (int j = 0; j < lines.size(); j++) { + String line = lines.get(j).trim(); + + for (ValidatorStrings validatorStr : VALIDATOR_STRINGS) { + int strIdx = line.indexOf(validatorStr.code); + if (strIdx >= 0) { + + int comma = line.indexOf(',', strIdx + validatorStr.code.length()); + + String paramName = line.substring(strIdx + validatorStr.code.length(), comma); + + int quote = line.indexOf('"', comma); + + String message = line.substring(quote + 1, Math.min(line.length(), quote + 2 + paramName.length())); + + if (line.contains("\"A Disposable")) { + continue; + } + + if (!line.contains("\"The RxJavaPlugins") + && !(message.startsWith(paramName) + && (message.endsWith(" ") || message.endsWith("\"")))) { + errorCount++; + errors.append("L") + .append(j) + .append(" : Wrong validator message parameter name\r\n ") + .append(line) + .append("\r\n") + .append(" ").append(paramName).append(" != ").append(message) + .append("\r\n at ") + .append(fullClassName) + .append(".method(") + .append(f.getName()) + .append(":") + .append(j + 1) + .append(")\r\n") + ; + } + + int midx = j - 1; + // find the method declaration + for (; midx >= 0; midx--) { + String linek = lines.get(midx).trim(); + if (linek.startsWith("public") || linek.startsWith("private") + || linek.startsWith("protected") + || linek.startsWith("static") + || linek.startsWith(baseClassName)) { + break; + } + } + + if (line.contains("\"The RxJavaPlugins")) { + continue; + } + + // find JavaDoc of throws + boolean found = false; + for (int k = midx - 1; k >= 0; k--) { + String linek = lines.get(k).trim(); + if (linek.startsWith("/**")) { + break; + } + if (linek.startsWith("}")) { + found = true; // no method JavaDoc present + break; + } + if (linek.startsWith(validatorStr.javadoc)) { + // see if a @code paramName is present + String paramStr = "{@code " + paramName + "}"; + for (int m = k; m < lines.size(); m++) { + String linem = lines.get(m).trim(); + if (linem.startsWith("* @see") + || linem.startsWith("* @since") + || linem.startsWith("*/")) { + break; + } + if (linem.contains(paramStr)) { + found = true; + break; + } + } + break; + } + } + + if (!found) { + errorCount++; + errors.append("L") + .append(j) + .append(" : missing '") + .append(validatorStr.javadoc) + .append("' for argument validation: ") + .append(paramName) + .append("\r\n ") + .append(line) + .append("\r\n at ") + .append(fullClassName) + .append(".method(") + .append(f.getName()) + .append(":") + .append(j + 1) + .append(")\r\n") + ; + } + } + } + + for (ValidatorStrings validatorStr : EXCEPTION_STRINGS) { + int strIdx = line.indexOf(validatorStr.code); + if (strIdx >= 0) { + + int midx = j - 1; + // find the method declaration + for (; midx >= 0; midx--) { + String linek = lines.get(midx).trim(); + if (linek.startsWith("public") || linek.startsWith("private") + || linek.startsWith("protected") + || linek.startsWith("static") + || linek.startsWith(baseClassName)) { + break; + } + } + + // find JavaDoc of throws + boolean found = false; + for (int k = midx - 1; k >= 0; k--) { + String linek = lines.get(k).trim(); + if (linek.startsWith("/**")) { + break; + } + if (linek.startsWith("}")) { + found = true; // no JavaDoc + break; + } + if (linek.startsWith(validatorStr.javadoc)) { + found = true; + } + } + + if (!found) { + errorCount++; + errors.append("L") + .append(j) + .append(" : missing '") + .append(validatorStr.javadoc) + .append("' for exception\r\n ") + .append(line) + .append("\r\n at ") + .append(fullClassName) + .append(".method(") + .append(f.getName()) + .append(":") + .append(j + 1) + .append(")\r\n") + ; + } + } + } + + if (line.startsWith("public") || line.startsWith("protected") || line.startsWith("final") || line.startsWith("private") + || line.startsWith("static")) { + for (ValidatorStrings validatorStr : TYPICAL_ARGUMENT_STRINGS) { + // find the method declaration ending { + for (int i = j; i < lines.size(); i++) { + String linei = lines.get(i).trim(); + + // space + code for capturing type declarations + String varPattern = " " + validatorStr.code; + if (linei.contains(varPattern + ")") + || linei.contains(varPattern + ",") + || linei.endsWith(varPattern)) { + // ignore nullable-annotated arguments + if (!linei.matches(".*\\@Nullable\\s.*" + validatorStr.code + ".*")) { + boolean found = false; + for (int k = i - 1; k >= 0; k--) { + String linek = lines.get(k).trim(); + if (linek.startsWith("/**")) { + break; + } + if (linek.startsWith("}")) { + found = true; // no method JavaDoc present + break; + } + if (linek.startsWith(validatorStr.javadoc)) { + // see if a @code paramName is present + String paramStr = "{@code " + validatorStr.code + "}"; + for (int m = k; m < lines.size(); m++) { + String linem = lines.get(m).trim(); + if (linem.startsWith("* @see") + || linem.startsWith("* @since") + || linem.startsWith("*/")) { + break; + } + if (linem.contains(paramStr)) { + found = true; + break; + } + } + break; + } + } + + if (!found) { + errorCount++; + errors.append("L") + .append(j) + .append(" : missing '") + .append(validatorStr.javadoc) + .append("' for typical argument: ") + .append(validatorStr.code) + .append("\r\n ") + .append(line) + .append("\r\n at ") + .append(fullClassName) + .append(".method(") + .append(f.getName()) + .append(":") + .append(j + 1) + .append(")\r\n") + ; + } + } + } + + if (linei.endsWith("{") || linei.endsWith(";")) { + break; + } + } + } + } + } + + if (errorCount != 0) { + errors.insert(0, errorCount + " problems\r\n"); + errors.setLength(errors.length() - 2); + throw new AssertionError(errors.toString()); + } + } + + static final class ValidatorStrings { + final String code; + final String javadoc; + ValidatorStrings(String code, String javadoc) { + this.code = code; + this.javadoc = javadoc; + } + } + + static final List<ValidatorStrings> VALIDATOR_STRINGS = Arrays.asList( + new ValidatorStrings("Objects.requireNonNull(", "* @throws NullPointerException"), + new ValidatorStrings("ObjectHelper.verifyPositive(", "* @throws IllegalArgumentException") + ); + + static final List<ValidatorStrings> EXCEPTION_STRINGS = Arrays.asList( + new ValidatorStrings("throw new NullPointerException(", "* @throws NullPointerException"), + new ValidatorStrings("throw new IllegalArgumentException(", "* @throws IllegalArgumentException"), + new ValidatorStrings("throw new IndexOutOfBoundsException(", "* @throws IndexOutOfBoundsException") + ); + + static final List<ValidatorStrings> TYPICAL_ARGUMENT_STRINGS = Arrays.asList( + new ValidatorStrings("source", "* @throws NullPointerException"), + new ValidatorStrings("source1", "* @throws NullPointerException"), + new ValidatorStrings("source2", "* @throws NullPointerException"), + new ValidatorStrings("source3", "* @throws NullPointerException"), + new ValidatorStrings("source4", "* @throws NullPointerException"), + new ValidatorStrings("source5", "* @throws NullPointerException"), + new ValidatorStrings("source6", "* @throws NullPointerException"), + new ValidatorStrings("source7", "* @throws NullPointerException"), + new ValidatorStrings("source8", "* @throws NullPointerException"), + new ValidatorStrings("source9", "* @throws NullPointerException"), + new ValidatorStrings("sources", "* @throws NullPointerException"), + new ValidatorStrings("mapper", "* @throws NullPointerException"), + new ValidatorStrings("combiner", "* @throws NullPointerException"), + new ValidatorStrings("zipper", "* @throws NullPointerException"), + new ValidatorStrings("predicate", "* @throws NullPointerException"), + new ValidatorStrings("item", "* @throws NullPointerException"), + new ValidatorStrings("item1", "* @throws NullPointerException"), + new ValidatorStrings("item2", "* @throws NullPointerException"), + new ValidatorStrings("item3", "* @throws NullPointerException"), + new ValidatorStrings("item4", "* @throws NullPointerException"), + new ValidatorStrings("item5", "* @throws NullPointerException"), + new ValidatorStrings("item6", "* @throws NullPointerException"), + new ValidatorStrings("item7", "* @throws NullPointerException"), + new ValidatorStrings("item8", "* @throws NullPointerException"), + new ValidatorStrings("item9", "* @throws NullPointerException"), + new ValidatorStrings("item10", "* @throws NullPointerException"), + new ValidatorStrings("unit", "* @throws NullPointerException"), + new ValidatorStrings("scheduler", "* @throws NullPointerException"), + new ValidatorStrings("other", "* @throws NullPointerException"), + new ValidatorStrings("fallback", "* @throws NullPointerException"), + new ValidatorStrings("defaultItem", "* @throws NullPointerException"), + new ValidatorStrings("defaultValue", "* @throws NullPointerException"), + new ValidatorStrings("stop", "* @throws NullPointerException"), + new ValidatorStrings("stopPredicate", "* @throws NullPointerException"), + new ValidatorStrings("handler", "* @throws NullPointerException"), + new ValidatorStrings("bufferSupplier", "* @throws NullPointerException"), + new ValidatorStrings("openingIndicator", "* @throws NullPointerException"), + new ValidatorStrings("closingIndicator", "* @throws NullPointerException"), + new ValidatorStrings("boundary", "* @throws NullPointerException"), + new ValidatorStrings("boundaryIndicator", "* @throws NullPointerException"), + new ValidatorStrings("selector", "* @throws NullPointerException"), + new ValidatorStrings("resultSelector", "* @throws NullPointerException"), + new ValidatorStrings("keySelector", "* @throws NullPointerException"), + new ValidatorStrings("valueSelector", "* @throws NullPointerException"), + new ValidatorStrings("valueSupplier", "* @throws NullPointerException"), + new ValidatorStrings("collectionSupplier", "* @throws NullPointerException"), + new ValidatorStrings("onNext", "* @throws NullPointerException"), + new ValidatorStrings("onError", "* @throws NullPointerException"), + new ValidatorStrings("onComplete", "* @throws NullPointerException"), + new ValidatorStrings("onEvent", "* @throws NullPointerException"), + new ValidatorStrings("onAfterNext", "* @throws NullPointerException"), + new ValidatorStrings("onAfterTerminate", "* @throws NullPointerException"), + new ValidatorStrings("onTerminate", "* @throws NullPointerException"), + new ValidatorStrings("onSuccess", "* @throws NullPointerException"), + new ValidatorStrings("onSubscribe", "* @throws NullPointerException"), + new ValidatorStrings("onNotification", "* @throws NullPointerException"), + new ValidatorStrings("onCancel", "* @throws NullPointerException"), + new ValidatorStrings("onDispose", "* @throws NullPointerException"), + new ValidatorStrings("onRequest", "* @throws NullPointerException"), + new ValidatorStrings("onNextMapper", "* @throws NullPointerException"), + new ValidatorStrings("onErrorMapper", "* @throws NullPointerException"), + new ValidatorStrings("onCompleteSupplier", "* @throws NullPointerException"), + new ValidatorStrings("clazz", "* @throws NullPointerException"), + new ValidatorStrings("next", "* @throws NullPointerException"), + new ValidatorStrings("reducer", "* @throws NullPointerException"), + new ValidatorStrings("seed", "* @throws NullPointerException"), + new ValidatorStrings("seedSupplier", "* @throws NullPointerException"), + new ValidatorStrings("mapSupplier", "* @throws NullPointerException"), + new ValidatorStrings("collectionFactory", "* @throws NullPointerException"), + new ValidatorStrings("factory", "* @throws NullPointerException"), + new ValidatorStrings("stage", "* @throws NullPointerException"), + new ValidatorStrings("stream", "* @throws NullPointerException"), + new ValidatorStrings("collector", "* @throws NullPointerException"), + new ValidatorStrings("subscriptionIndicator", "* @throws NullPointerException"), + new ValidatorStrings("itemDelayIndicator", "* @throws NullPointerException"), + new ValidatorStrings("future", "* @throws NullPointerException"), + + new ValidatorStrings("maxConcurrency", "* @throws IllegalArgumentException"), + new ValidatorStrings("parallelism", "* @throws IllegalArgumentException"), + new ValidatorStrings("prefetch", "* @throws IllegalArgumentException"), + new ValidatorStrings("bufferSize", "* @throws IllegalArgumentException"), + new ValidatorStrings("capacityHint", "* @throws IllegalArgumentException"), + new ValidatorStrings("capacity", "* @throws IllegalArgumentException"), + new ValidatorStrings("count", "* @throws IllegalArgumentException"), + new ValidatorStrings("skip", "* @throws IllegalArgumentException"), + new ValidatorStrings("times", "* @throws IllegalArgumentException"), + new ValidatorStrings("n", "* @throws IllegalArgumentException") + ); + +} diff --git a/src/test/java/io/reactivex/rxjava3/validators/ParameterNamesInClassesTest.java b/src/test/java/io/reactivex/rxjava3/validators/ParameterNamesInClassesTest.java new file mode 100644 index 0000000000..2284e32f6a --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/validators/ParameterNamesInClassesTest.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.validators; + +import static org.junit.Assert.assertEquals; +import org.junit.Test; + +public class ParameterNamesInClassesTest { + void method(int paramName) { + // deliberately empty + } + + @Test + public void javacParametersEnabled() throws Exception { + assertEquals("Please enable saving parameter names via the -parameters javac argument", + "paramName", + getClass() + .getDeclaredMethod("method", Integer.TYPE) + .getParameters()[0].getName()); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/validators/PublicFinalMethods.java b/src/test/java/io/reactivex/rxjava3/validators/PublicFinalMethods.java new file mode 100644 index 0000000000..ad7df44964 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/validators/PublicFinalMethods.java @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.validators; + +import static org.junit.Assert.fail; + +import java.lang.reflect.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; + +/** + * Verifies that instance methods of the base reactive classes are all declared final. + */ +public class PublicFinalMethods { + + static void scan(Class<?> clazz) { + for (Method m : clazz.getMethods()) { + if (m.getDeclaringClass() == clazz) { + if ((m.getModifiers() & Modifier.STATIC) == 0) { + if ((m.getModifiers() & (Modifier.PUBLIC | Modifier.FINAL)) == Modifier.PUBLIC) { + fail("Not final: " + m); + } + } + } + } + } + + @Test + public void flowable() { + scan(Flowable.class); + } + + @Test + public void observable() { + scan(Observable.class); + } + + @Test + public void single() { + scan(Single.class); + } + + @Test + public void completable() { + scan(Completable.class); + } + + @Test + public void maybe() { + scan(Maybe.class); + } +} diff --git a/src/test/java/io/reactivex/rxjava3/validators/SourceAnnotationCheck.java b/src/test/java/io/reactivex/rxjava3/validators/SourceAnnotationCheck.java new file mode 100644 index 0000000000..dfa4aea548 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/validators/SourceAnnotationCheck.java @@ -0,0 +1,501 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.validators; + +import java.io.File; +import java.nio.file.Files; +import java.util.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.core.*; +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.flowables.ConnectableFlowable; +import io.reactivex.rxjava3.observables.ConnectableObservable; +import io.reactivex.rxjava3.parallel.ParallelFlowable; +import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import io.reactivex.rxjava3.processors.*; +import io.reactivex.rxjava3.schedulers.Schedulers; +import io.reactivex.rxjava3.subjects.*; +import io.reactivex.rxjava3.testsupport.TestHelper; + +/** + * Parse the given files and check if all public final and public static methods have + * @NonNull or @Nullable annotations specified on their return type and object-type parameters + * as well as @SafeVarargs for varargs. + */ +public class SourceAnnotationCheck { + + @Test + public void checkCompletable() throws Exception { + processFile(Completable.class); + } + + @Test + public void checkSingle() throws Exception { + processFile(Single.class); + } + + @Test + public void checkMaybe() throws Exception { + processFile(Maybe.class); + } + + @Test + public void checkObservable() throws Exception { + processFile(Observable.class); + } + + @Test + public void checkFlowable() throws Exception { + processFile(Flowable.class); + } + + @Test + public void checkParallelFlowable() throws Exception { + processFile(ParallelFlowable.class); + } + + @Test + public void checkConnectableObservable() throws Exception { + processFile(ConnectableObservable.class); + } + + @Test + public void checkConnectableFlowable() throws Exception { + processFile(ConnectableFlowable.class); + } + + @Test + public void checkSubject() throws Exception { + processFile(Subject.class); + } + + @Test + public void checkFlowableProcessor() throws Exception { + processFile(FlowableProcessor.class); + } + + @Test + public void checkDisposable() throws Exception { + processFile(Disposable.class); + } + + @Test + public void checkScheduler() throws Exception { + processFile(Scheduler.class); + } + + @Test + public void checkSchedulers() throws Exception { + processFile(Schedulers.class); + } + + @Test + public void checkAsyncSubject() throws Exception { + processFile(AsyncSubject.class); + } + + @Test + public void checkBehaviorSubject() throws Exception { + processFile(BehaviorSubject.class); + } + + @Test + public void checkPublishSubject() throws Exception { + processFile(PublishSubject.class); + } + + @Test + public void checkReplaySubject() throws Exception { + processFile(ReplaySubject.class); + } + + @Test + public void checkUnicastSubject() throws Exception { + processFile(UnicastSubject.class); + } + + @Test + public void checkSingleSubject() throws Exception { + processFile(SingleSubject.class); + } + + @Test + public void checkMaybeSubject() throws Exception { + processFile(MaybeSubject.class); + } + + @Test + public void checkCompletableSubject() throws Exception { + processFile(CompletableSubject.class); + } + + @Test + public void checkAsyncProcessor() throws Exception { + processFile(AsyncProcessor.class); + } + + @Test + public void checkBehaviorProcessor() throws Exception { + processFile(BehaviorProcessor.class); + } + + @Test + public void checkPublishProcessor() throws Exception { + processFile(PublishProcessor.class); + } + + @Test + public void checkReplayProcessor() throws Exception { + processFile(ReplayProcessor.class); + } + + @Test + public void checkUnicastProcessor() throws Exception { + processFile(UnicastProcessor.class); + } + + @Test + public void checkMulticastProcessor() throws Exception { + processFile(MulticastProcessor.class); + } + + @Test + public void checkRxJavaPlugins() throws Exception { + processFile(RxJavaPlugins.class); + } + + static void processFile(Class<?> clazz) throws Exception { + String baseClassName = clazz.getSimpleName(); + File f = TestHelper.findSource(baseClassName, clazz.getPackage().getName()); + if (f == null) { + return; + } + String fullClassName = clazz.getName(); + + int errorCount = 0; + StringBuilder errors = new StringBuilder(); + + List<String> lines = Files.readAllLines(f.toPath()); + + for (int j = 0; j < lines.size(); j++) { + String line = lines.get(j).trim(); + + if (line.contains("class")) { + continue; + } + if (line.startsWith("public static") + || line.startsWith("public final") + || line.startsWith("protected final") + || line.startsWith("protected abstract") + || line.startsWith("public abstract")) { + int methodArgStart = line.indexOf("("); + + int isBoolean = line.indexOf(" boolean "); + int isInt = line.indexOf(" int "); + int isLong = line.indexOf(" long "); + int isVoid = line.indexOf(" void "); + int isElementType = line.indexOf(" R "); + + boolean hasSafeVarargsAnnotation = false; + + if (!((isBoolean > 0 && isBoolean < methodArgStart) + || (isInt > 0 && isInt < methodArgStart) + || (isLong > 0 && isLong < methodArgStart) + || (isVoid > 0 && isVoid < methodArgStart) + || (isElementType > 0 && isElementType < methodArgStart) + )) { + + boolean annotationFound = false; + for (int k = j - 1; k >= 0; k--) { + + String prevLine = lines.get(k).trim(); + + if (prevLine.startsWith("}") || prevLine.startsWith("*/")) { + break; + } + if (prevLine.startsWith("@NonNull") || prevLine.startsWith("@Nullable")) { + annotationFound = true; + } + if (prevLine.startsWith("@SafeVarargs")) { + hasSafeVarargsAnnotation = true; + } + } + + if (!annotationFound) { + errorCount++; + errors.append("L") + .append(j) + .append(" : Missing return type nullability annotation | ") + .append(line) + .append("\r\n") + .append(" at ") + .append(fullClassName) + .append(".method(") + .append(f.getName()) + .append(":") + .append(j + 1) + .append(")\r\n") + ; + } + } + + // Extract arguments + StringBuilder arguments = new StringBuilder(); + int methodArgEnd = line.indexOf(")", methodArgStart); + if (methodArgEnd > 0) { + arguments.append(line.substring(methodArgStart + 1, methodArgEnd)); + } else { + arguments.append(line.substring(methodArgStart + 1)); + for (int k = j + 1; k < lines.size(); k++) { + String ln = lines.get(k).trim(); + int idx = ln.indexOf(")"); + if (idx > 0) { + arguments.append(ln.substring(0, idx)); + break; + } + arguments.append(ln).append(" "); + } + } + + // Strip generics arguments + StringBuilder strippedArguments = new StringBuilder(); + int skippingDepth = 0; + for (int k = 0; k < arguments.length(); k++) { + char c = arguments.charAt(k); + if (c == '<') { + skippingDepth++; + } + else if (c == '>') { + skippingDepth--; + } + else if (skippingDepth == 0) { + strippedArguments.append(c); + } + } + + String strippedArgumentsStr = strippedArguments.toString(); + String[] args = strippedArgumentsStr.split("\\s*,\\s*"); + + for (int k = 0; k < args.length; k++) { + String typeDef = args[k]; + + for (String typeName : CLASS_NAMES) { + String typeNameSpaced = typeName + " "; + + if (typeDef.contains(typeNameSpaced) + && !typeDef.contains("@NonNull") + && !typeDef.contains("@Nullable")) { + + if (!line.contains("@Nullable " + typeName) + && !line.contains("@NonNull " + typeName)) { + errorCount++; + errors.append("L") + .append(j) + .append(" - argument ").append(k + 1) + .append(" : Missing argument type nullability annotation\r\n ") + .append(typeDef).append("\r\n ") + .append(strippedArgumentsStr) + .append("\r\n") + .append(" at ") + .append(fullClassName) + .append(".method(") + .append(f.getName()) + .append(":") + .append(j + 1) + .append(")\r\n") + ; + } + } + } + + if (typeDef.contains("final ")) { + errorCount++; + errors.append("L") + .append(j) + .append(" - argument ").append(k + 1) + .append(" : unnecessary final on argument\r\n ") + .append(typeDef).append("\r\n ") + .append(strippedArgumentsStr) + .append("\r\n") + .append(" at ") + .append(fullClassName) + .append(".method(") + .append(f.getName()) + .append(":") + .append(j + 1) + .append(")\r\n") + ; + } + if (typeDef.contains("@NonNull int") + || typeDef.contains("@NonNull long") + || typeDef.contains("@Nullable int") + || typeDef.contains("@Nullable long") + ) { + errorCount++; + errors.append("L") + .append(j) + .append(" - argument ").append(k + 1) + .append(" : unnecessary nullability annotation\r\n ") + .append(typeDef).append("\r\n ") + .append(strippedArgumentsStr) + .append("\r\n") + .append(" at ") + .append(fullClassName) + .append(".method(") + .append(f.getName()) + .append(":") + .append(j + 1) + .append(")\r\n") + ; + } + + } + + if (strippedArgumentsStr.contains("...") && !hasSafeVarargsAnnotation) { + errorCount++; + errors.append("L") + .append(j) + .append(" : Missing @SafeVarargs annotation\r\n ") + .append(strippedArgumentsStr) + .append("\r\n") + .append(" at ") + .append(fullClassName) + .append(".method(") + .append(f.getName()) + .append(":") + .append(j + 1) + .append(")\r\n") + ; + } + } + + for (String typeName : TYPES_REQUIRING_NONNULL_TYPEARG) { + String pattern = typeName + "<?"; + String patternRegex = ".*" + typeName + "\\<\\? (extends|super) " + COMMON_TYPE_ARG_NAMES + "\\>.*"; + if (line.contains(pattern) && !line.matches(patternRegex)) { + + errorCount++; + errors.append("L") + .append(j) + .append(" : Missing @NonNull type argument annotation on ") + .append(typeName) + .append("\r\n") + .append(" at ") + .append(fullClassName) + .append(".method(") + .append(f.getName()) + .append(":") + .append(j + 1) + .append(")\r\n") + ; + } + } + for (String typeName : TYPES_FORBIDDEN_NONNULL_TYPEARG) { + String patternRegex = ".*" + typeName + "\\<@NonNull (\\? (extends|super) )?" + COMMON_TYPE_ARG_NAMES + "\\>.*"; + + if (line.matches(patternRegex)) { + errorCount++; + errors.append("L") + .append(j) + .append(" : @NonNull type argument should be on the arg declaration ") + .append(typeName) + .append("\r\n") + .append(" at ") + .append(fullClassName) + .append(".method(") + .append(f.getName()) + .append(":") + .append(j + 1) + .append(")\r\n") + ; + } + } + + for (String typeName : TYPES_REQUIRING_NONNULL_TYPEARG_ON_FUNC) { + if (line.matches(".*Function[\\d]?\\<.*, (\\? (extends|super) )?" + typeName + ".*")) { + errorCount++; + errors.append("L") + .append(j) + .append(" : Missing @NonNull type argument annotation on Function argument ") + .append(typeName) + .append("\r\n") + .append(" at ") + .append(fullClassName) + .append(".method(") + .append(f.getName()) + .append(":") + .append(j + 1) + .append(")\r\n") + ; + } + } + } + + if (errorCount != 0) { + errors.insert(0, errorCount + " problems\r\n"); + errors.setLength(errors.length() - 2); + throw new AssertionError(errors.toString()); + } + } + + static final List<String> CLASS_NAMES = Arrays.asList( + "TimeUnit", "Scheduler", "Emitter", + + "Completable", "CompletableSource", "CompletableObserver", "CompletableOnSubscribe", + "CompletableTransformer", "CompletableOperator", "CompletableEmitter", "CompletableConverter", + + "Single", "SingleSource", "SingleObserver", "SingleOnSubscribe", + "SingleTransformer", "SingleOperator", "SingleEmitter", "SingleConverter", + + "Maybe", "MaybeSource", "MaybeObserver", "MaybeOnSubscribe", + "MaybeTransformer", "MaybeOperator", "MaybeEmitter", "MaybeConverter", + + "Observable", "ObservableSource", "Observer", "ObservableOnSubscribe", + "ObservableTransformer", "ObservableOperator", "ObservableEmitter", "ObservableConverter", + + "Flowable", "Publisher", "Subscriber", "FlowableSubscriber", "FlowableOnSubscribe", + "FlowableTransformer", "FlowableOperator", "FlowableEmitter", "FlowableConverter", + + "Function", "BiFunction", "Function3", "Function4", "Function5", "Function6", + "Function7", "Function8", "Function9", + + "Action", "Runnable", "Consumer", "BiConsumer", "Supplier", "Callable", "Void", + "Throwable", "Optional", "CompletionStage", "BooleanSupplier", "LongConsumer", + "Predicate", "BiPredicate", "Object", + + "Iterable", "Stream", "Iterator", + + "BackpressureOverflowStrategy", "BackpressureStrategy", + "Subject", "Processor", "FlowableProcessor", + + "T", "R", "U", "V" + ); + + static final List<String> TYPES_REQUIRING_NONNULL_TYPEARG = Arrays.asList( + "Iterable", "Stream", "Publisher", "Processor", "Subscriber", "Optional" + ); + static final List<String> TYPES_FORBIDDEN_NONNULL_TYPEARG = Arrays.asList( + "Iterable", "Stream", "Publisher", "Processor", "Subscriber", "Optional" + ); + + static final List<String> TYPES_REQUIRING_NONNULL_TYPEARG_ON_FUNC = Arrays.asList( + "Iterable", "Stream", "Publisher", "Processor", "Subscriber", "Optional", + "Observer", "SingleObserver", "MaybeObserver", "CompletableObserver" + ); + + static final String COMMON_TYPE_ARG_NAMES = "([A-Z][0-9]?|TOpening|TClosing|TLeft|TLeftEnd|TRight|TRightEnd)"; +} diff --git a/src/test/java/io/reactivex/rxjava3/validators/TestPrefixInMethodName.java b/src/test/java/io/reactivex/rxjava3/validators/TestPrefixInMethodName.java new file mode 100644 index 0000000000..7a75611b10 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/validators/TestPrefixInMethodName.java @@ -0,0 +1,129 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.validators; + +import java.io.*; +import java.util.*; +import java.util.regex.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.testsupport.TestHelper; + +/** + * Check verifying there are no methods with the prefix "test" in the name. + */ +public class TestPrefixInMethodName { + + private static final String pattern = "void\\s+test[a-zA-Z0-9]"; + private static final String replacement = "void "; + + @Test + public void checkAndUpdateTestMethodNames() throws Exception { + File f = TestHelper.findSource("Flowable"); + if (f == null) { + System.out.println("Unable to find sources of RxJava"); + return; + } + + Queue<File> dirs = new ArrayDeque<>(); + + StringBuilder fail = new StringBuilder(); + fail.append("The following code pattern was found: ").append(pattern).append("\n"); + fail.append("Refresh and re-run tests!\n\n"); + + File parent = f.getParentFile().getParentFile(); + + dirs.offer(new File(parent.getAbsolutePath().replace('\\', '/'))); + dirs.offer(new File(parent.getAbsolutePath().replace('\\', '/').replace("src/main/java", "src/test/java"))); + + Pattern p = Pattern.compile(pattern); + + int total = 0; + + while (!dirs.isEmpty()) { + f = dirs.poll(); + + File[] list = f.listFiles(); + if (list != null && list.length != 0) { + + for (File u : list) { + if (u.isDirectory()) { + dirs.offer(u); + } else { + String fname = u.getName(); + if (fname.endsWith(".java")) { + + int lineNum = 0; + List<String> lines = new ArrayList<>(); + BufferedReader in = new BufferedReader(new FileReader(u)); + //boolean found = false; + try { + for (; ; ) { + String line = in.readLine(); + if (line == null) { + break; + } + lineNum++; + + Matcher matcher = p.matcher(line); + if (!line.startsWith("//") && !line.startsWith("*") && matcher.find()) { + // found = true; + fail + .append(fname) + .append("#L").append(lineNum) + .append(" ").append(line) + .append("\n"); + total++; + + int methodNameStartIndex = matcher.end() - 1; + char firstChar = Character.toLowerCase(line.charAt(methodNameStartIndex)); + + String newLine = matcher.replaceAll(replacement + firstChar); + + lines.add(newLine); + } else { + lines.add(line); + } + + } + } finally { + in.close(); + } + + /*if (found && System.getenv("CI") == null) { + PrintWriter w = new PrintWriter(new FileWriter(u)); + + try { + for (String s : lines) { + w.println(s); + } + } finally { + w.close(); + } + }*/ + } + } + } + } + } + if (total != 0) { + fail.append("Found ") + .append(total) + .append(" instances"); + System.out.println(fail); + throw new AssertionError(fail.toString()); + } + } +} diff --git a/src/test/java/io/reactivex/rxjava3/validators/TextualAorAn.java b/src/test/java/io/reactivex/rxjava3/validators/TextualAorAn.java new file mode 100644 index 0000000000..654b4884b4 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/validators/TextualAorAn.java @@ -0,0 +1,197 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.validators; + +import java.io.*; +import java.util.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.testsupport.TestHelper; + +/** + * Adds license header to java files. + */ +public class TextualAorAn { + + @Test + public void checkFiles() throws Exception { + File f = TestHelper.findSource("Flowable"); + if (f == null) { + return; + } + + Queue<File> dirs = new ArrayDeque<>(); + + File parent = f.getParentFile().getParentFile(); + dirs.offer(parent); +// dirs.offer(new File(parent.getAbsolutePath().replace('\\', '/').replace("src/main/java", "src/perf/java"))); +// dirs.offer(new File(parent.getAbsolutePath().replace('\\', '/').replace("src/main/java", "src/test/java"))); + + StringBuilder fail = new StringBuilder(); + + while (!dirs.isEmpty()) { + f = dirs.poll(); + + File[] list = f.listFiles(); + if (list != null && list.length != 0) { + + for (File u : list) { + if (u.isDirectory()) { + dirs.offer(u); + } else { + if (u.getName().endsWith(".java")) { + + List<String> lines = new ArrayList<>(); + BufferedReader in = new BufferedReader(new FileReader(u)); + try { + for (;;) { + String line = in.readLine(); + if (line == null) { + break; + } + + lines.add(line); + } + } finally { + in.close(); + } + + String clazz = u.getAbsolutePath().replace('\\', '/'); + int idx = clazz.indexOf("/io/reactivex/"); + clazz = clazz.substring(idx + 14).replace(".java", ""); + + processFile(fail, lines, clazz, u.getName()); + } + } + } + } + } + + if (fail.length() != 0) { + System.out.println(fail); + throw new AssertionError(fail.toString()); + } + } + + static void processFile(StringBuilder b, List<String> lines, String className, String fileName) { + int i = 1; + for (String s : lines) { + + if (s.contains(" a Observer")) { + b.append("java.lang.RuntimeException: ' a Observer'\r\n at io.reactivex.") + .append(className).append(" (").append(fileName).append(":").append(i).append(")\r\n"); + ; + } + + if (s.contains("A Observer")) { + b.append("java.lang.RuntimeException: 'A Observer'\r\n at io.reactivex.") + .append(className).append(" (").append(fileName).append(":").append(i).append(")\r\n"); + ; + } + + if (s.contains(" a Observable")) { + b.append("java.lang.RuntimeException: ' a Observable'\r\n at io.reactivex.") + .append(className).append(" (").append(fileName).append(":").append(i).append(")\r\n"); + ; + } + + if (s.contains("A Observable")) { + b.append("java.lang.RuntimeException: 'A Observable'\r\n at io.reactivex.") + .append(className).append(" (").append(fileName).append(":").append(i).append(")\r\n"); + ; + } + + if (s.contains(" an Subscriber")) { + b.append("java.lang.RuntimeException: ' an Subscriber'\r\n at io.reactivex.") + .append(className).append(" (").append(fileName).append(":").append(i).append(")\r\n"); + ; + } + + if (s.contains("An Subscriber")) { + b.append("java.lang.RuntimeException: 'An Subscriber'\r\n at io.reactivex.") + .append(className).append(" (").append(fileName).append(":").append(i).append(")\r\n"); + ; + } + + if (s.contains(" an Publisher")) { + b.append("java.lang.RuntimeException: ' an Publisher'\r\n at io.reactivex.") + .append(className).append(" (").append(fileName).append(":").append(i).append(")\r\n"); + ; + } + + if (s.contains("An Publisher")) { + b.append("java.lang.RuntimeException: 'An Publisher'\r\n at io.reactivex.") + .append(className).append(" (").append(fileName).append(":").append(i).append(")\r\n"); + ; + } + + if (s.contains(" an Flowable")) { + b.append("java.lang.RuntimeException: ' an Flowable'\r\n at io.reactivex.") + .append(className).append(" (").append(fileName).append(":").append(i).append(")\r\n"); + ; + } + + if (s.contains("An Flowable")) { + b.append("java.lang.RuntimeException: 'An Flowable'\r\n at io.reactivex.") + .append(className).append(" (").append(fileName).append(":").append(i).append(")\r\n"); + ; + } + + if (s.contains(" an Single")) { + b.append("java.lang.RuntimeException: ' an Single'\r\n at io.reactivex.") + .append(className).append(" (").append(fileName).append(":").append(i).append(")\r\n"); + ; + } + + if (s.contains("An Single")) { + b.append("java.lang.RuntimeException: 'An Single'\r\n at io.reactivex.") + .append(className).append(" (").append(fileName).append(":").append(i).append(")\r\n"); + ; + } + + if (s.contains(" an Maybe")) { + b.append("java.lang.RuntimeException: ' an Maybe'\r\n at io.reactivex.") + .append(className).append(" (").append(fileName).append(":").append(i).append(")\r\n"); + ; + } + + if (s.contains("An Maybe")) { + b.append("java.lang.RuntimeException: 'An Maybe'\r\n at io.reactivex.") + .append(className).append(" (").append(fileName).append(":").append(i).append(")\r\n"); + ; + } + + if (s.contains(" an Completable")) { + b.append("java.lang.RuntimeException: ' an Completable'\r\n at io.reactivex.") + .append(className).append(" (").append(fileName).append(":").append(i).append(")\r\n"); + ; + } + + if (s.contains("An Completable")) { + b.append("java.lang.RuntimeException: 'An Completable'\r\n at io.reactivex.") + .append(className).append(" (").append(fileName).append(":").append(i).append(")\r\n"); + ; + } + + if (s.contains(" an cancel")) { + b.append("java.lang.RuntimeException: ' an cancel'\r\n at io.reactivex.") + .append(className).append(" (").append(fileName).append(":").append(i).append(")\r\n"); + ; + } + + i++; + } + } +} diff --git a/src/test/java/io/reactivex/rxjava3/validators/TooManyEmptyNewLines.java b/src/test/java/io/reactivex/rxjava3/validators/TooManyEmptyNewLines.java new file mode 100644 index 0000000000..c79c879337 --- /dev/null +++ b/src/test/java/io/reactivex/rxjava3/validators/TooManyEmptyNewLines.java @@ -0,0 +1,139 @@ +/* + * Copyright (c) 2016-present, RxJava Contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.rxjava3.validators; + +import java.io.*; +import java.util.*; + +import org.junit.Test; + +import io.reactivex.rxjava3.testsupport.TestHelper; + +/** + * Test verifying there are no 2..5 empty newlines in the code. + */ +public class TooManyEmptyNewLines { + + @Test + public void tooManyEmptyNewLines2() throws Exception { + findPattern(2); + } + + @Test + public void tooManyEmptyNewLines3() throws Exception { + findPattern(3); + } + + @Test + public void tooManyEmptyNewLines4() throws Exception { + findPattern(4); + } + + @Test + public void tooManyEmptyNewLines5() throws Exception { + findPattern(5); + } + + static void findPattern(int newLines) throws Exception { + File f = TestHelper.findSource("Flowable"); + if (f == null) { + System.out.println("Unable to find sources of TestHelper.findSourceDir()"); + return; + } + + Queue<File> dirs = new ArrayDeque<>(); + + StringBuilder fail = new StringBuilder(); + fail.append("The following code pattern was found: "); + fail.append("\\R"); + for (int i = 0; i < newLines; i++) { + fail.append("\\R"); + } + fail.append("\n"); + + File parent = f.getParentFile().getParentFile(); + + dirs.offer(new File(parent.getAbsolutePath().replace('\\', '/'))); + dirs.offer(new File(parent.getAbsolutePath().replace('\\', '/').replace("src/main/java", "src/test/java"))); + + int total = 0; + + while (!dirs.isEmpty()) { + f = dirs.poll(); + + File[] list = f.listFiles(); + if (list != null && list.length != 0) { + + for (File u : list) { + if (u.isDirectory()) { + dirs.offer(u); + } else { + String fname = u.getName(); + if (fname.endsWith(".java")) { + + List<String> lines = new ArrayList<>(); + BufferedReader in = new BufferedReader(new FileReader(u)); + try { + for (;;) { + String line = in.readLine(); + if (line == null) { + break; + } + lines.add(line); + } + } finally { + in.close(); + } + + for (int i = 0; i < lines.size() - newLines; i++) { + String line1 = lines.get(i); + if (line1.isEmpty()) { + int c = 1; + for (int j = i + 1; j < lines.size(); j++) { + if (lines.get(j).trim().isEmpty()) { + c++; + } else { + break; + } + } + + if (c == newLines) { + fail + .append(fname) + .append("#L").append(i + 1) + .append("\n") + .append(" at ") + .append(fname.replace(".java", "")) + .append(".method(") + .append(fname) + .append(":").append(i + 1) + .append(")\n") + ; + total++; + i += c; + } + } + } + } + } + } + } + } + if (total != 0) { + fail.insert(0, "Found " + total + " instances\n"); + System.out.println(fail); + throw new AssertionError(fail.toString()); + } + } +}